ui: revamp option dialog
Selector component shows choices/options to the user. Ok and Cancel buttons were added, along with a window border and window title.embed
parent
fb8a4b020d
commit
cd761b3166
|
@ -73,21 +73,13 @@ impl Default for Composer {
|
|||
|
||||
#[derive(Debug)]
|
||||
enum ViewMode {
|
||||
Discard(Uuid),
|
||||
Discard(Uuid, Selector<char>),
|
||||
Edit,
|
||||
//Selector(Selector),
|
||||
ThreadView,
|
||||
}
|
||||
|
||||
impl ViewMode {
|
||||
fn is_discard(&self) -> bool {
|
||||
if let ViewMode::Discard(_) = self {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn is_edit(&self) -> bool {
|
||||
if let ViewMode::Edit = self {
|
||||
true
|
||||
|
@ -452,62 +444,11 @@ impl Component for Composer {
|
|||
self.pager.set_dirty();
|
||||
self.pager.draw(grid, body_area, context);
|
||||
}
|
||||
ViewMode::Discard(_) => {
|
||||
ViewMode::Discard(_, ref mut s) => {
|
||||
self.pager.set_dirty();
|
||||
self.pager.draw(grid, body_area, context);
|
||||
/* Let user choose whether to quit with/without saving or cancel */
|
||||
let mid_x = { std::cmp::max(width!(area) / 2, width / 2) - width / 2 };
|
||||
let mid_y = { std::cmp::max(height!(area) / 2, 11) - 11 };
|
||||
|
||||
let upper_left = upper_left!(body_area);
|
||||
let bottom_right = bottom_right!(body_area);
|
||||
let area = (
|
||||
pos_inc(upper_left, (mid_x, mid_y)),
|
||||
pos_dec(bottom_right, (mid_x, mid_y)),
|
||||
);
|
||||
create_box(grid, area);
|
||||
let area = (
|
||||
pos_inc(upper_left, (mid_x + 2, mid_y + 2)),
|
||||
pos_dec(
|
||||
bottom_right,
|
||||
(mid_x.saturating_sub(2), mid_y.saturating_sub(2)),
|
||||
),
|
||||
);
|
||||
|
||||
let (_, y) = write_string_to_grid(
|
||||
&format!("Draft \"{:10}\"", self.draft.headers()["Subject"]),
|
||||
grid,
|
||||
Color::Default,
|
||||
Color::Default,
|
||||
Attr::Default,
|
||||
area,
|
||||
true,
|
||||
);
|
||||
let (_, y) = write_string_to_grid(
|
||||
"[x] quit without saving",
|
||||
grid,
|
||||
Color::Byte(124),
|
||||
Color::Default,
|
||||
Attr::Default,
|
||||
(set_y(upper_left!(area), y + 2), bottom_right!(area)),
|
||||
true,
|
||||
);
|
||||
let (_, y) = write_string_to_grid(
|
||||
"[y] save draft and quit",
|
||||
grid,
|
||||
Color::Byte(124),
|
||||
Color::Default,
|
||||
Attr::Default,
|
||||
(set_y(upper_left!(area), y + 1), bottom_right!(area)),
|
||||
true,
|
||||
);
|
||||
write_string_to_grid(
|
||||
"[n] cancel",
|
||||
grid,
|
||||
Color::Byte(124),
|
||||
Color::Default,
|
||||
Attr::Default,
|
||||
(set_y(upper_left!(area), y + 1), bottom_right!(area)),
|
||||
true,
|
||||
);
|
||||
s.draw(grid, center_area(area, s.content.size()), context);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -535,6 +476,95 @@ impl Component for Composer {
|
|||
return true;
|
||||
}
|
||||
}
|
||||
(ViewMode::Discard(_, ref mut selector), _, _) => {
|
||||
if selector.process_event(event, context) {
|
||||
if selector.is_done() {
|
||||
let (u, s) = match std::mem::replace(&mut self.mode, ViewMode::ThreadView) {
|
||||
ViewMode::Discard(u, s) => (u, s),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let key = s.collect()[0] as char;
|
||||
match key {
|
||||
'x' => {
|
||||
context.replies.push_back(UIEvent::Action(Tab(Kill(u))));
|
||||
return true;
|
||||
}
|
||||
'n' => {}
|
||||
'y' => {
|
||||
let mut failure = true;
|
||||
let draft = std::mem::replace(&mut self.draft, Draft::default());
|
||||
|
||||
let draft = draft.finalise().unwrap();
|
||||
for folder in &[
|
||||
&context.accounts[self.account_cursor]
|
||||
.special_use_folder(SpecialUseMailbox::Drafts),
|
||||
&context.accounts[self.account_cursor]
|
||||
.special_use_folder(SpecialUseMailbox::Inbox),
|
||||
&context.accounts[self.account_cursor]
|
||||
.special_use_folder(SpecialUseMailbox::Normal),
|
||||
] {
|
||||
if folder.is_none() {
|
||||
continue;
|
||||
}
|
||||
let folder = folder.unwrap();
|
||||
if let Err(e) = context.accounts[self.account_cursor].save(
|
||||
draft.as_bytes(),
|
||||
folder,
|
||||
Some(Flag::SEEN | Flag::DRAFT),
|
||||
) {
|
||||
debug!("{:?} could not save draft msg", e);
|
||||
log(
|
||||
format!(
|
||||
"Could not save draft in '{}' folder: {}.",
|
||||
folder,
|
||||
e.to_string()
|
||||
),
|
||||
ERROR,
|
||||
);
|
||||
context.replies.push_back(UIEvent::Notification(
|
||||
Some(format!(
|
||||
"Could not save draft in '{}' folder.",
|
||||
folder
|
||||
)),
|
||||
e.into(),
|
||||
Some(NotificationType::ERROR),
|
||||
));
|
||||
} else {
|
||||
failure = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if failure {
|
||||
let file =
|
||||
create_temp_file(draft.as_bytes(), None, None, false);
|
||||
debug!("message saved in {}", file.path.display());
|
||||
log(
|
||||
format!(
|
||||
"Message was stored in {} so that you can restore it manually.",
|
||||
file.path.display()
|
||||
),
|
||||
INFO,
|
||||
);
|
||||
context.replies.push_back(UIEvent::Notification(
|
||||
Some("Could not save in any folder".into()),
|
||||
format!(
|
||||
"Message was stored in {} so that you can restore it manually.",
|
||||
file.path.display()
|
||||
),
|
||||
Some(NotificationType::INFO),
|
||||
));
|
||||
}
|
||||
context.replies.push_back(UIEvent::Action(Tab(Kill(u))));
|
||||
return true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
self.set_dirty();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if self.form.process_event(event, context) {
|
||||
|
@ -574,85 +604,6 @@ impl Component for Composer {
|
|||
UIEvent::Input(Key::Down) => {
|
||||
self.cursor = Cursor::Body;
|
||||
}
|
||||
UIEvent::Input(Key::Char(key)) if self.mode.is_discard() => {
|
||||
match (key, &self.mode) {
|
||||
('x', ViewMode::Discard(u)) => {
|
||||
context.replies.push_back(UIEvent::Action(Tab(Kill(*u))));
|
||||
return true;
|
||||
}
|
||||
('n', _) => {}
|
||||
('y', ViewMode::Discard(u)) => {
|
||||
let mut failure = true;
|
||||
let draft = std::mem::replace(&mut self.draft, Draft::default());
|
||||
|
||||
let draft = draft.finalise().unwrap();
|
||||
for folder in &[
|
||||
&context.accounts[self.account_cursor]
|
||||
.special_use_folder(SpecialUseMailbox::Drafts),
|
||||
&context.accounts[self.account_cursor]
|
||||
.special_use_folder(SpecialUseMailbox::Inbox),
|
||||
&context.accounts[self.account_cursor]
|
||||
.special_use_folder(SpecialUseMailbox::Normal),
|
||||
] {
|
||||
if folder.is_none() {
|
||||
continue;
|
||||
}
|
||||
let folder = folder.unwrap();
|
||||
if let Err(e) = context.accounts[self.account_cursor].save(
|
||||
draft.as_bytes(),
|
||||
folder,
|
||||
Some(Flag::SEEN | Flag::DRAFT),
|
||||
) {
|
||||
debug!("{:?} could not save draft msg", e);
|
||||
log(
|
||||
format!(
|
||||
"Could not save draft in '{}' folder: {}.",
|
||||
folder,
|
||||
e.to_string()
|
||||
),
|
||||
ERROR,
|
||||
);
|
||||
context.replies.push_back(UIEvent::Notification(
|
||||
Some(format!("Could not save draft in '{}' folder.", folder)),
|
||||
e.into(),
|
||||
Some(NotificationType::ERROR),
|
||||
));
|
||||
} else {
|
||||
failure = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if failure {
|
||||
let file = create_temp_file(draft.as_bytes(), None, None, false);
|
||||
debug!("message saved in {}", file.path.display());
|
||||
log(
|
||||
format!(
|
||||
"Message was stored in {} so that you can restore it manually.",
|
||||
file.path.display()
|
||||
),
|
||||
INFO,
|
||||
);
|
||||
context.replies.push_back(UIEvent::Notification(
|
||||
Some("Could not save in any folder".into()),
|
||||
format!(
|
||||
"Message was stored in {} so that you can restore it manually.",
|
||||
file.path.display()
|
||||
),
|
||||
Some(NotificationType::INFO),
|
||||
));
|
||||
}
|
||||
context.replies.push_back(UIEvent::Action(Tab(Kill(*u))));
|
||||
return true;
|
||||
}
|
||||
_ => {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
self.mode = ViewMode::ThreadView;
|
||||
self.set_dirty();
|
||||
return true;
|
||||
}
|
||||
/* Switch to thread view mode if we're on Edit mode */
|
||||
UIEvent::Input(Key::Char('v')) if self.mode.is_edit() => {
|
||||
self.mode = ViewMode::ThreadView;
|
||||
|
@ -818,7 +769,18 @@ impl Component for Composer {
|
|||
}
|
||||
|
||||
fn kill(&mut self, uuid: Uuid, _context: &mut Context) {
|
||||
self.mode = ViewMode::Discard(uuid);
|
||||
self.mode = ViewMode::Discard(
|
||||
uuid,
|
||||
Selector::new(
|
||||
"this draft has unsaved changes",
|
||||
vec![
|
||||
('x', "quit without saving".to_string()),
|
||||
('y', "save draft and quit".to_string()),
|
||||
('n', "cancel".to_string()),
|
||||
],
|
||||
true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
fn get_shortcuts(&self, context: &Context) -> ShortcutMaps {
|
||||
|
@ -855,7 +817,18 @@ impl Component for Composer {
|
|||
|
||||
fn can_quit_cleanly(&mut self) -> bool {
|
||||
/* Play it safe and ask user for confirmation */
|
||||
self.mode = ViewMode::Discard(self.id);
|
||||
self.mode = ViewMode::Discard(
|
||||
self.id,
|
||||
Selector::new(
|
||||
"this draft has unsaved changes",
|
||||
vec![
|
||||
('x', "quit without saving".to_string()),
|
||||
('y', "save draft and quit".to_string()),
|
||||
('n', "cancel".to_string()),
|
||||
],
|
||||
true,
|
||||
),
|
||||
);
|
||||
self.set_dirty();
|
||||
false
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ enum ViewMode {
|
|||
Attachment(usize),
|
||||
Raw,
|
||||
Subview,
|
||||
ContactSelector(Selector),
|
||||
ContactSelector(Selector<Card>),
|
||||
}
|
||||
|
||||
impl Default for ViewMode {
|
||||
|
@ -60,6 +60,12 @@ impl ViewMode {
|
|||
_ => false,
|
||||
}
|
||||
}
|
||||
fn is_contact_selector(&self) -> bool {
|
||||
match self {
|
||||
ViewMode::ContactSelector(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains an Envelope view, with sticky headers, a pager for the body, and subviews for more
|
||||
|
@ -630,16 +636,15 @@ impl Component for MailView {
|
|||
s.draw(grid, (set_y(upper_left, y + 1), bottom_right), context);
|
||||
}
|
||||
}
|
||||
ViewMode::ContactSelector(ref mut s) => {
|
||||
clear_area(grid, (set_y(upper_left, y + 1), bottom_right));
|
||||
s.draw(grid, (set_y(upper_left, y + 1), bottom_right), context);
|
||||
}
|
||||
_ => {
|
||||
if let Some(p) = self.pager.as_mut() {
|
||||
p.draw(grid, (set_y(upper_left, y + 1), bottom_right), context);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let ViewMode::ContactSelector(ref mut s) = self.mode {
|
||||
s.draw(grid, center_area(area, s.content.size()), context);
|
||||
}
|
||||
self.dirty = false;
|
||||
}
|
||||
|
||||
|
@ -654,6 +659,19 @@ impl Component for MailView {
|
|||
}
|
||||
ViewMode::ContactSelector(ref mut s) => {
|
||||
if s.process_event(event, context) {
|
||||
if s.is_done() {
|
||||
if let ViewMode::ContactSelector(s) =
|
||||
std::mem::replace(&mut self.mode, ViewMode::Normal)
|
||||
{
|
||||
let account = &mut context.accounts[self.coordinates.0];
|
||||
{
|
||||
for card in s.collect() {
|
||||
account.address_book.add_card(card);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.set_dirty();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -667,55 +685,22 @@ impl Component for MailView {
|
|||
}
|
||||
|
||||
match *event {
|
||||
UIEvent::Input(Key::Char('c')) => {
|
||||
if let ViewMode::ContactSelector(_) = self.mode {
|
||||
if let ViewMode::ContactSelector(s) =
|
||||
std::mem::replace(&mut self.mode, ViewMode::Normal)
|
||||
{
|
||||
let account = &mut context.accounts[self.coordinates.0];
|
||||
let mut results = Vec::new();
|
||||
{
|
||||
let envelope: &Envelope = &account.get_env(&self.coordinates.2);
|
||||
for c in s.collect() {
|
||||
let c = usize::from_ne_bytes({
|
||||
[c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7]]
|
||||
});
|
||||
for (idx, env) in envelope
|
||||
.from()
|
||||
.iter()
|
||||
.chain(envelope.to().iter())
|
||||
.enumerate()
|
||||
{
|
||||
if idx != c {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut new_card: Card = Card::new();
|
||||
new_card.set_email(env.get_email());
|
||||
new_card.set_name(env.get_display_name());
|
||||
results.push(new_card);
|
||||
}
|
||||
}
|
||||
}
|
||||
for c in results {
|
||||
account.address_book.add_card(c);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(Key::Char('c')) if !self.mode.is_contact_selector() => {
|
||||
let account = &mut context.accounts[self.coordinates.0];
|
||||
let envelope: &Envelope = &account.get_env(&self.coordinates.2);
|
||||
|
||||
let mut entries = Vec::new();
|
||||
for (idx, env) in envelope
|
||||
.from()
|
||||
.iter()
|
||||
.chain(envelope.to().iter())
|
||||
.enumerate()
|
||||
{
|
||||
entries.push((idx.to_ne_bytes().to_vec(), format!("{}", env)));
|
||||
for addr in envelope.from().iter().chain(envelope.to().iter()) {
|
||||
let mut new_card: Card = Card::new();
|
||||
new_card.set_email(addr.get_email());
|
||||
new_card.set_name(addr.get_display_name());
|
||||
entries.push((new_card, format!("{}", addr)));
|
||||
}
|
||||
self.mode = ViewMode::ContactSelector(Selector::new(entries, true));
|
||||
self.mode = ViewMode::ContactSelector(Selector::new(
|
||||
"select contacts to add",
|
||||
entries,
|
||||
false,
|
||||
));
|
||||
self.dirty = true;
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -1568,29 +1568,44 @@ impl Component for Tabbed {
|
|||
}
|
||||
}
|
||||
|
||||
type EntryIdentifier = Vec<u8>;
|
||||
/// Shows selection to user
|
||||
#[derive(Debug, Copy, PartialEq, Clone)]
|
||||
enum SelectorCursor {
|
||||
/// Cursor is at an entry
|
||||
Entry(usize),
|
||||
/// Cursor is located on the Ok button
|
||||
Ok,
|
||||
/// Cursor is located on the Cancel button
|
||||
Cancel,
|
||||
}
|
||||
|
||||
/// Shows a little window with options for user to select.
|
||||
///
|
||||
/// Instantiate with Selector::new(). Set single_only to true if user should only choose one of the
|
||||
/// options. After passing input events to this component, check Selector::is_done to see if the
|
||||
/// user has finalised their choices. Collect the choices by consuming the Selector with
|
||||
/// Selector::collect()
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct Selector {
|
||||
single_only: bool,
|
||||
pub struct Selector<T: PartialEq + Debug + Clone + Sync + Send> {
|
||||
/// allow only one selection
|
||||
entries: Vec<(EntryIdentifier, bool)>,
|
||||
selected_entry_count: u32,
|
||||
content: CellBuffer,
|
||||
single_only: bool,
|
||||
entries: Vec<(T, bool)>,
|
||||
pub content: CellBuffer,
|
||||
|
||||
cursor: usize,
|
||||
cursor: SelectorCursor,
|
||||
|
||||
/// If true, user has finished their selection
|
||||
done: bool,
|
||||
dirty: bool,
|
||||
id: ComponentId,
|
||||
}
|
||||
|
||||
impl fmt::Display for Selector {
|
||||
impl<T: PartialEq + Debug + Clone + Sync + Send> fmt::Display for Selector<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
Display::fmt("Selector", f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Selector {
|
||||
impl<T: PartialEq + Debug + Clone + Sync + Send> Component for Selector<T> {
|
||||
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
|
||||
let (width, height) = self.content.size();
|
||||
copy_area_with_break(grid, &self.content, area, ((0, 0), (width, height)));
|
||||
|
@ -1598,17 +1613,24 @@ impl Component for Selector {
|
|||
}
|
||||
fn process_event(&mut self, event: &mut UIEvent, _context: &mut Context) -> bool {
|
||||
let (width, height) = self.content.size();
|
||||
match *event {
|
||||
UIEvent::Input(Key::Char('\t')) => {
|
||||
self.entries[self.cursor].1 = !self.entries[self.cursor].1;
|
||||
if self.entries[self.cursor].1 {
|
||||
match (event, self.cursor) {
|
||||
(UIEvent::Input(Key::Char('\n')), _) if self.single_only => {
|
||||
/* User can only select one entry, so Enter key finalises the selection */
|
||||
self.done = true;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(Key::Char('\n')), SelectorCursor::Entry(c)) if !self.single_only => {
|
||||
/* User can select multiple entries, so Enter key toggles the entry under the
|
||||
* cursor */
|
||||
self.entries[c].1 = !self.entries[c].1;
|
||||
if self.entries[c].1 {
|
||||
write_string_to_grid(
|
||||
"x",
|
||||
&mut self.content,
|
||||
Color::Default,
|
||||
Color::Default,
|
||||
Attr::Default,
|
||||
((1, self.cursor), (width, self.cursor)),
|
||||
((3, c + 2), (width - 2, c + 2)),
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
|
@ -1618,23 +1640,170 @@ impl Component for Selector {
|
|||
Color::Default,
|
||||
Color::Default,
|
||||
Attr::Default,
|
||||
((1, self.cursor), (width, self.cursor)),
|
||||
((3, c + 2), (width - 2, c + 2)),
|
||||
false,
|
||||
);
|
||||
}
|
||||
self.dirty = true;
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(Key::Up) if self.cursor > 0 => {
|
||||
self.cursor -= 1;
|
||||
(UIEvent::Input(Key::Char('\n')), SelectorCursor::Ok) if !self.single_only => {
|
||||
self.done = true;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(Key::Char('\n')), SelectorCursor::Cancel) if !self.single_only => {
|
||||
for e in self.entries.iter_mut() {
|
||||
e.1 = false;
|
||||
}
|
||||
self.done = true;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(Key::Up), SelectorCursor::Entry(c)) if c > 0 => {
|
||||
if self.single_only {
|
||||
// Redraw selection
|
||||
change_colors(
|
||||
&mut self.content,
|
||||
((2, c + 2), (width - 2, c + 2)),
|
||||
Color::Default,
|
||||
Color::Default,
|
||||
);
|
||||
change_colors(
|
||||
&mut self.content,
|
||||
((2, c + 1), (width - 2, c + 1)),
|
||||
Color::Default,
|
||||
Color::Byte(8),
|
||||
);
|
||||
self.entries[c].1 = false;
|
||||
self.entries[c - 1].1 = true;
|
||||
} else {
|
||||
// Redraw cursor
|
||||
change_colors(
|
||||
&mut self.content,
|
||||
((2, c + 2), (4, c + 2)),
|
||||
Color::Default,
|
||||
Color::Default,
|
||||
);
|
||||
change_colors(
|
||||
&mut self.content,
|
||||
((2, c + 1), (4, c + 1)),
|
||||
Color::Default,
|
||||
Color::Byte(8),
|
||||
);
|
||||
}
|
||||
self.cursor = SelectorCursor::Entry(c - 1);
|
||||
self.dirty = true;
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(Key::Down) if self.cursor < height.saturating_sub(1) => {
|
||||
self.cursor += 1;
|
||||
(UIEvent::Input(Key::Up), SelectorCursor::Ok)
|
||||
| (UIEvent::Input(Key::Up), SelectorCursor::Cancel) => {
|
||||
change_colors(
|
||||
&mut self.content,
|
||||
((width / 2, height - 2), (width - 1, height - 2)),
|
||||
Color::Default,
|
||||
Color::Default,
|
||||
);
|
||||
let c = self.entries.len().saturating_sub(1);
|
||||
self.cursor = SelectorCursor::Entry(c);
|
||||
change_colors(
|
||||
&mut self.content,
|
||||
((2, c + 2), (4, c + 2)),
|
||||
Color::Default,
|
||||
Color::Byte(8),
|
||||
);
|
||||
self.dirty = true;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(Key::Down), SelectorCursor::Entry(c))
|
||||
if c < self.entries.len().saturating_sub(1) =>
|
||||
{
|
||||
if self.single_only {
|
||||
// Redraw selection
|
||||
change_colors(
|
||||
&mut self.content,
|
||||
((2, c + 2), (width - 2, c + 2)),
|
||||
Color::Default,
|
||||
Color::Default,
|
||||
);
|
||||
change_colors(
|
||||
&mut self.content,
|
||||
((2, c + 3), (width - 2, c + 3)),
|
||||
Color::Default,
|
||||
Color::Byte(8),
|
||||
);
|
||||
self.entries[c].1 = false;
|
||||
self.entries[c + 1].1 = true;
|
||||
} else {
|
||||
// Redraw cursor
|
||||
change_colors(
|
||||
&mut self.content,
|
||||
((2, c + 2), (4, c + 2)),
|
||||
Color::Default,
|
||||
Color::Default,
|
||||
);
|
||||
change_colors(
|
||||
&mut self.content,
|
||||
((2, c + 3), (4, c + 3)),
|
||||
Color::Default,
|
||||
Color::Byte(8),
|
||||
);
|
||||
}
|
||||
self.cursor = SelectorCursor::Entry(c + 1);
|
||||
self.dirty = true;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(Key::Down), SelectorCursor::Entry(c)) if !self.single_only => {
|
||||
self.cursor = SelectorCursor::Ok;
|
||||
change_colors(
|
||||
&mut self.content,
|
||||
((2, c + 2), (4, c + 2)),
|
||||
Color::Default,
|
||||
Color::Default,
|
||||
);
|
||||
change_colors(
|
||||
&mut self.content,
|
||||
((width / 2, height - 2), (width / 2 + 1, height - 2)),
|
||||
Color::Default,
|
||||
Color::Byte(8),
|
||||
);
|
||||
self.dirty = true;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(Key::Down), _) | (UIEvent::Input(Key::Up), _) => return true,
|
||||
(UIEvent::Input(Key::Right), SelectorCursor::Ok) => {
|
||||
self.cursor = SelectorCursor::Cancel;
|
||||
change_colors(
|
||||
&mut self.content,
|
||||
((width / 2, height - 2), (width / 2 + 1, height - 2)),
|
||||
Color::Default,
|
||||
Color::Default,
|
||||
);
|
||||
change_colors(
|
||||
&mut self.content,
|
||||
((width / 2 + 6, height - 2), (width / 2 + 11, height - 2)),
|
||||
Color::Default,
|
||||
Color::Byte(8),
|
||||
);
|
||||
self.dirty = true;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(Key::Left), SelectorCursor::Cancel) => {
|
||||
self.cursor = SelectorCursor::Ok;
|
||||
change_colors(
|
||||
&mut self.content,
|
||||
((width / 2, height - 2), (width / 2 + 1, height - 2)),
|
||||
Color::Default,
|
||||
Color::Byte(8),
|
||||
);
|
||||
change_colors(
|
||||
&mut self.content,
|
||||
((width / 2 + 6, height - 2), (width / 2 + 11, height - 2)),
|
||||
Color::Default,
|
||||
Color::Default,
|
||||
);
|
||||
self.dirty = true;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(Key::Left), _) | (UIEvent::Input(Key::Right), _) => return true,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
@ -1655,44 +1824,179 @@ impl Component for Selector {
|
|||
}
|
||||
}
|
||||
|
||||
impl Selector {
|
||||
pub fn new(mut entries: Vec<(EntryIdentifier, String)>, single_only: bool) -> Selector {
|
||||
let width = entries
|
||||
.iter()
|
||||
.max_by_key(|e| e.1.len())
|
||||
.map(|v| v.1.len())
|
||||
.unwrap_or(0)
|
||||
+ 4;
|
||||
let height = entries.len();
|
||||
impl<T: PartialEq + Debug + Clone + Sync + Send> Selector<T> {
|
||||
pub fn new(title: &str, entries: Vec<(T, String)>, single_only: bool) -> Selector<T> {
|
||||
let width = std::cmp::max(
|
||||
"OK Cancel".len(),
|
||||
std::cmp::max(
|
||||
entries
|
||||
.iter()
|
||||
.max_by_key(|e| e.1.len())
|
||||
.map(|v| v.1.len())
|
||||
.unwrap_or(0),
|
||||
title.len(),
|
||||
),
|
||||
) + 7;
|
||||
let height = entries.len()
|
||||
+ 4
|
||||
+ if single_only {
|
||||
0
|
||||
} else {
|
||||
/* Extra room for buttons Okay/Cancel */
|
||||
3
|
||||
};
|
||||
let mut content = CellBuffer::new(width, height, Cell::with_char(' '));
|
||||
let identifiers = entries
|
||||
.iter_mut()
|
||||
.map(|(id, _)| (std::mem::replace(&mut *id, Vec::new()), false))
|
||||
.collect();
|
||||
for (i, e) in entries.into_iter().enumerate() {
|
||||
write_string_to_grid(
|
||||
"┏━",
|
||||
&mut content,
|
||||
Color::Byte(8),
|
||||
Color::Default,
|
||||
Attr::Default,
|
||||
((0, 0), (width - 1, 0)),
|
||||
false,
|
||||
);
|
||||
let (x, _) = write_string_to_grid(
|
||||
title,
|
||||
&mut content,
|
||||
Color::Default,
|
||||
Color::Default,
|
||||
Attr::Default,
|
||||
((2, 0), (width - 1, 0)),
|
||||
false,
|
||||
);
|
||||
for i in 1..(width - title.len() - 1) {
|
||||
write_string_to_grid(
|
||||
&format!("[ ] {}", e.1),
|
||||
"━",
|
||||
&mut content,
|
||||
Color::Byte(8),
|
||||
Color::Default,
|
||||
Attr::Default,
|
||||
((x + i, 0), (width - 1, 0)),
|
||||
false,
|
||||
);
|
||||
}
|
||||
write_string_to_grid(
|
||||
"┓",
|
||||
&mut content,
|
||||
Color::Byte(8),
|
||||
Color::Default,
|
||||
Attr::Default,
|
||||
((width - 1, 0), (width - 1, 0)),
|
||||
false,
|
||||
);
|
||||
write_string_to_grid(
|
||||
"┗",
|
||||
&mut content,
|
||||
Color::Byte(8),
|
||||
Color::Default,
|
||||
Attr::Default,
|
||||
((0, height - 1), (width - 1, height - 1)),
|
||||
false,
|
||||
);
|
||||
write_string_to_grid(
|
||||
&"━".repeat(width - 2),
|
||||
&mut content,
|
||||
Color::Byte(8),
|
||||
Color::Default,
|
||||
Attr::Default,
|
||||
((1, height - 1), (width - 2, height - 1)),
|
||||
false,
|
||||
);
|
||||
write_string_to_grid(
|
||||
"┛",
|
||||
&mut content,
|
||||
Color::Byte(8),
|
||||
Color::Default,
|
||||
Attr::Default,
|
||||
((width - 1, height - 1), (width - 1, height - 1)),
|
||||
false,
|
||||
);
|
||||
for i in 1..height - 1 {
|
||||
write_string_to_grid(
|
||||
"┃",
|
||||
&mut content,
|
||||
Color::Byte(8),
|
||||
Color::Default,
|
||||
Attr::Default,
|
||||
((0, i), (width - 1, i)),
|
||||
false,
|
||||
);
|
||||
write_string_to_grid(
|
||||
"┃",
|
||||
&mut content,
|
||||
Color::Byte(8),
|
||||
Color::Default,
|
||||
Attr::Default,
|
||||
((width - 1, i), (width - 1, i)),
|
||||
false,
|
||||
);
|
||||
}
|
||||
if single_only {
|
||||
for (i, e) in entries.iter().enumerate() {
|
||||
write_string_to_grid(
|
||||
&e.1,
|
||||
&mut content,
|
||||
Color::Default,
|
||||
if i == 0 {
|
||||
Color::Byte(8)
|
||||
} else {
|
||||
Color::Default
|
||||
},
|
||||
Attr::Default,
|
||||
((2, i + 2), (width - 1, i + 2)),
|
||||
false,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
for (i, e) in entries.iter().enumerate() {
|
||||
write_string_to_grid(
|
||||
&format!("[ ] {}", e.1),
|
||||
&mut content,
|
||||
Color::Default,
|
||||
Color::Default,
|
||||
Attr::Default,
|
||||
((2, i + 2), (width - 1, i + 2)),
|
||||
false,
|
||||
);
|
||||
if i == 0 {
|
||||
content[(2, i + 2)].set_bg(Color::Byte(8));
|
||||
content[(3, i + 2)].set_bg(Color::Byte(8));
|
||||
content[(4, i + 2)].set_bg(Color::Byte(8));
|
||||
}
|
||||
}
|
||||
write_string_to_grid(
|
||||
"OK Cancel",
|
||||
&mut content,
|
||||
Color::Default,
|
||||
Color::Default,
|
||||
Attr::Bold,
|
||||
((width / 2, height - 2), (width - 1, height - 2)),
|
||||
false,
|
||||
);
|
||||
}
|
||||
let mut identifiers: Vec<(T, bool)> =
|
||||
entries.into_iter().map(|(id, _)| (id, false)).collect();
|
||||
if single_only {
|
||||
/* set default option */
|
||||
identifiers[0].1 = true;
|
||||
}
|
||||
|
||||
Selector {
|
||||
single_only,
|
||||
entries: identifiers,
|
||||
selected_entry_count: 0,
|
||||
content,
|
||||
cursor: 0,
|
||||
cursor: SelectorCursor::Entry(0),
|
||||
done: false,
|
||||
dirty: true,
|
||||
id: ComponentId::new_v4(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn collect(self) -> Vec<EntryIdentifier> {
|
||||
pub fn is_done(&self) -> bool {
|
||||
self.done
|
||||
}
|
||||
|
||||
pub fn collect(self) -> Vec<T> {
|
||||
self.entries
|
||||
.into_iter()
|
||||
.filter(|v| v.1)
|
||||
|
|
|
@ -799,3 +799,21 @@ pub fn clear_area(grid: &mut CellBuffer, area: Area) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn center_area(area: Area, (width, height): (usize, usize)) -> Area {
|
||||
let mid_x = { std::cmp::max(width!(area) / 2, width / 2) - width / 2 };
|
||||
let mid_y = { std::cmp::max(height!(area) / 2, height / 2) - height / 2 };
|
||||
|
||||
let (upper_x, upper_y) = upper_left!(area);
|
||||
let (max_x, max_y) = bottom_right!(area);
|
||||
(
|
||||
(
|
||||
std::cmp::min(max_x, upper_x + mid_x),
|
||||
std::cmp::min(max_y, upper_y + mid_y),
|
||||
),
|
||||
(
|
||||
std::cmp::min(max_x, upper_x + mid_x + width),
|
||||
std::cmp::min(max_y, upper_y + mid_y + height),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue