From a8065713229c44fc8158746fe58da2cbea9d05ae Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Wed, 19 Feb 2020 16:57:37 +0200 Subject: [PATCH] Add UIDialog and UIConfirmationDialog widgets They are just typedefs for the Selector widget. The API is kind of messed up and this commit is part of the process of cleaning it up: right now to use this, you check the is_done() method which if returns true, the done() method executes the closure you defined when creating the widget. The closure returns a UIEvent which you can forward application-wide by context.replies.push_back(event) or handle it in process_event() immediately. --- src/components.rs | 4 +- src/components/contacts.rs | 11 +++- src/components/mail/compose.rs | 40 ++++++++++--- src/components/mail/view.rs | 32 ++++++++--- src/components/utilities.rs | 88 +++++++++++++++++++++++++++-- src/components/utilities/widgets.rs | 10 ++-- src/execute/actions.rs | 20 +++++++ src/state.rs | 3 +- src/types.rs | 16 ++++++ 9 files changed, 193 insertions(+), 31 deletions(-) diff --git a/src/components.rs b/src/components.rs index a4ec8dc40..0e5b005e9 100644 --- a/src/components.rs +++ b/src/components.rs @@ -48,7 +48,7 @@ use uuid::Uuid; use super::{Key, StatusEvent, UIEvent}; -type ComponentId = Uuid; +pub type ComponentId = Uuid; pub type ShortcutMap = FnvHashMap<&'static str, Key>; pub type ShortcutMaps = FnvHashMap<&'static str, ShortcutMap>; @@ -56,7 +56,7 @@ pub type ShortcutMaps = FnvHashMap<&'static str, ShortcutMap>; /// Types implementing this Trait can draw on the terminal and receive events. /// If a type wants to skip drawing if it has not changed anything, it can hold some flag in its /// fields (eg self.dirty = false) and act upon that in their `draw` implementation. -pub trait Component: Display + Debug + Send { +pub trait Component: Display + Debug + Send + Sync { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context); fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool; fn is_dirty(&self) -> bool; diff --git a/src/components/contacts.rs b/src/components/contacts.rs index 040f33b2b..cdc121ef7 100644 --- a/src/components/contacts.rs +++ b/src/components/contacts.rs @@ -29,7 +29,7 @@ pub use self::contact_list::*; #[derive(Debug)] enum ViewMode { ReadOnly, - Discard(Selector), + Discard(UIDialog), Edit, //New, } @@ -299,8 +299,9 @@ impl Component for ContactManager { return true; } + let parent_id = self.parent_id; /* Play it safe and ask user for confirmation */ - self.mode = ViewMode::Discard(Selector::new( + self.mode = ViewMode::Discard(UIDialog::new( "this contact has unsaved changes", vec![ ('x', "quit without saving".to_string()), @@ -308,6 +309,12 @@ impl Component for ContactManager { ('n', "cancel".to_string()), ], true, + std::sync::Arc::new(move |results: &[char]| match results[0] { + 'x' => Some(UIEvent::Action(Tab(Kill(parent_id)))), + 'n' => None, + 'y' => None, + _ => None, + }), context, )); self.set_dirty(true); diff --git a/src/components/mail/compose.rs b/src/components/mail/compose.rs index b1e69ab08..ffd155ede 100644 --- a/src/components/mail/compose.rs +++ b/src/components/mail/compose.rs @@ -111,11 +111,11 @@ impl Default for Composer { #[derive(Debug)] enum ViewMode { - Discard(Uuid, Selector), + Discard(Uuid, UIDialog), Edit, Embed, - SelectRecipients(Selector
), - Send(Selector), + SelectRecipients(UIDialog
), + Send(UIDialog), } impl ViewMode { @@ -183,7 +183,8 @@ impl Composer { .map(|m| m.address) { let list_address_string = list_address.to_string(); - ret.mode = ViewMode::SelectRecipients(Selector::new( + let id = ret.id; + ret.mode = ViewMode::SelectRecipients(UIDialog::new( "select recipients", vec![ ( @@ -193,6 +194,18 @@ impl Composer { (list_address, list_address_string), ], false, + std::sync::Arc::new(move |results: &[Address]| { + Some(UIEvent::FinishedUIDialog( + id, + Box::new( + results + .into_iter() + .map(|a| a.to_string()) + .collect::>() + .join(", "), + ), + )) + }), context, )); } @@ -698,11 +711,15 @@ impl Component for Composer { && self.mode.is_edit() => { self.update_draft(); - self.mode = ViewMode::Send(Selector::new( + let id = self.id; + self.mode = ViewMode::Send(UIDialog::new( "send mail?", vec![(true, "yes".to_string()), (false, "no".to_string())], /* only one choice */ true, + std::sync::Arc::new(move |results: &[bool]| { + Some(UIEvent::FinishedUIDialog(id, Box::new(results[0]))) + }), context, )); return true; @@ -1005,7 +1022,7 @@ impl Component for Composer { self.mode = ViewMode::Discard( uuid, - Selector::new( + UIDialog::new( "this draft has unsaved changes", vec![ ('x', "quit without saving".to_string()), @@ -1013,6 +1030,9 @@ impl Component for Composer { ('n', "cancel".to_string()), ], true, + std::sync::Arc::new(move |results: &[char]| { + Some(UIEvent::FinishedUIDialog(uuid, Box::new(results[0]))) + }), context, ), ); @@ -1043,10 +1063,11 @@ impl Component for Composer { return true; } + let id = self.id; /* Play it safe and ask user for confirmation */ self.mode = ViewMode::Discard( - self.id, - Selector::new( + id, + UIDialog::new( "this draft has unsaved changes", vec![ ('x', "quit without saving".to_string()), @@ -1054,6 +1075,9 @@ impl Component for Composer { ('n', "cancel".to_string()), ], true, + std::sync::Arc::new(move |results: &[char]| { + Some(UIEvent::FinishedUIDialog(id, Box::new(results[0]))) + }), context, ), ); diff --git a/src/components/mail/view.rs b/src/components/mail/view.rs index 6948d3de5..d1e3709a4 100644 --- a/src/components/mail/view.rs +++ b/src/components/mail/view.rs @@ -46,7 +46,7 @@ enum ViewMode { Raw, Ansi(RawBuffer), Subview, - ContactSelector(Selector), + ContactSelector(UIDialog), } impl Default for ViewMode { @@ -736,14 +736,12 @@ impl Component for MailView { 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); - } + if let Some(event) = s.done() { + context.replies.push_back(event); } + } else { + unsafe { std::hint::unreachable_unchecked() } } - self.set_dirty(true); } return true; } @@ -823,15 +821,35 @@ impl Component for MailView { entries.push((new_card, format!("{}", addr))); } drop(envelope); + let id = self.id; self.mode = ViewMode::ContactSelector(Selector::new( "select contacts to add", entries, false, + std::sync::Arc::new(move |results: &[Card]| { + Some(UIEvent::FinishedUIDialog( + id, + Box::new(results.into_iter().cloned().collect::>()), + )) + }), context, )); self.dirty = true; return true; } + UIEvent::FinishedUIDialog(ref id, ref results) + if self.mode.is_contact_selector() && self.id == *id => + { + if let Some(results) = results.downcast_ref::>() { + let account = &mut context.accounts[self.coordinates.0]; + { + for card in results.iter() { + account.address_book.add_card(card.clone()); + } + } + } + self.mode = ViewMode::Normal; + } UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt('')) if self.mode.is_contact_selector() => { diff --git a/src/components/utilities.rs b/src/components/utilities.rs index 591d5709b..20b018875 100644 --- a/src/components/utilities.rs +++ b/src/components/utilities.rs @@ -1814,8 +1814,8 @@ enum SelectorCursor { /// 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 { +#[derive(Clone)] +pub struct Selector { /// allow only one selection single_only: bool, entries: Vec<(T, bool)>, @@ -1825,17 +1825,44 @@ pub struct Selector { /// If true, user has finished their selection done: bool, + done_fn: F, dirty: bool, id: ComponentId, } -impl fmt::Display for Selector { +pub type UIConfirmationDialog = + Selector Option + 'static + Sync + Send>>; + +pub type UIDialog = + Selector Option + 'static + Sync + Send>>; + +impl fmt::Debug + for Selector +{ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Display::fmt("Selector", f) } } -impl Component for Selector { +impl fmt::Display + for Selector +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + Display::fmt("Selector", f) + } +} + +impl PartialEq + for Selector +{ + fn eq(&self, other: &Selector) -> bool { + self.entries == other.entries + } +} + +impl Component + for Selector +{ 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))); @@ -2111,13 +2138,14 @@ impl Component for Selector { } } -impl Selector { +impl Selector { pub fn new( title: &str, entries: Vec<(T, String)>, single_only: bool, + done_fn: F, context: &Context, - ) -> Selector { + ) -> Selector { let width = std::cmp::max( "OK Cancel".len(), std::cmp::max( @@ -2302,6 +2330,7 @@ impl Selector { content, cursor: SelectorCursor::Entry(0), done: false, + done_fn, dirty: true, id: ComponentId::new_v4(), } @@ -2320,6 +2349,53 @@ impl Selector { } } +impl UIDialog { + pub fn done(self) -> Option { + let Self { + done, + done_fn, + entries, + .. + } = self; + if done { + (done_fn)( + entries + .iter() + .filter(|v| v.1) + .map(|(id, _)| id) + .cloned() + .collect::>() + .as_slice(), + ) + } else { + None + } + } +} + +impl UIConfirmationDialog { + pub fn done(self) -> Option { + let Self { + done, + done_fn, + entries, + .. + } = self; + if done { + (done_fn)( + entries + .iter() + .filter(|v| v.1) + .map(|(id, _)| id) + .cloned() + .any(core::convert::identity), + ) + } else { + None + } + } +} + #[derive(Debug, Clone, PartialEq)] pub struct RawBuffer { pub buf: CellBuffer, diff --git a/src/components/utilities/widgets.rs b/src/components/utilities/widgets.rs index 778646b52..940ce55c6 100644 --- a/src/components/utilities/widgets.rs +++ b/src/components/utilities/widgets.rs @@ -22,7 +22,7 @@ use super::*; use fnv::FnvHashMap; -type AutoCompleteFn = Box Vec + Send>; +type AutoCompleteFn = Box Vec + Send + Sync>; #[derive(Debug, PartialEq)] enum FormFocus { @@ -588,7 +588,7 @@ impl Component for FormWidget { #[derive(Debug, Default)] pub struct ButtonWidget where - T: std::fmt::Debug + Default + Send, + T: std::fmt::Debug + Default + Send + Sync, { buttons: FnvHashMap, layout: Vec, @@ -603,7 +603,7 @@ where impl fmt::Display for ButtonWidget where - T: std::fmt::Debug + Default + Send, + T: std::fmt::Debug + Default + Send + Sync, { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Display::fmt("", f) @@ -612,7 +612,7 @@ where impl ButtonWidget where - T: std::fmt::Debug + Default + Send, + T: std::fmt::Debug + Default + Send + Sync, { pub fn new(init_val: (String, T)) -> ButtonWidget { ButtonWidget { @@ -642,7 +642,7 @@ where impl Component for ButtonWidget where - T: std::fmt::Debug + Default + Send, + T: std::fmt::Debug + Default + Send + Sync, { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { if self.dirty { diff --git a/src/execute/actions.rs b/src/execute/actions.rs index ba5eb0299..bdb7bae5f 100644 --- a/src/execute/actions.rs +++ b/src/execute/actions.rs @@ -114,6 +114,26 @@ pub enum Action { AccountAction(AccountName, AccountAction), } +impl Action { + pub fn needs_confirmation(&self) -> bool { + match self { + Action::Listing(_) => false, + Action::ViewMailbox(_) => false, + Action::Sort(_, _) => false, + Action::SubSort(_, _) => false, + Action::Tab(_) => false, + Action::ToggleThreadSnooze => false, + Action::MailingListAction(_) => true, + Action::View(_) => false, + Action::SetEnv(_, _) => false, + Action::PrintEnv(_) => false, + Action::Compose(_) => false, + Action::Folder(_, _) => true, + Action::AccountAction(_, _) => false, + } + } +} + type AccountName = String; type FolderPath = String; type NewFolderPath = String; diff --git a/src/state.rs b/src/state.rs index 433637231..477f6e6e3 100644 --- a/src/state.rs +++ b/src/state.rs @@ -293,7 +293,8 @@ impl State { stdout: None, child: None, mode: UIMode::Normal, - components: Vec::with_capacity(1), + components: Vec::with_capacity(8), + ui_dialogs: Vec::new(), timer, draw_rate_limit: RateLimit::new(1, 3), draw_horizontal_segment_fn: if settings.terminal.use_color() { diff --git a/src/types.rs b/src/types.rs index 0d9bd372a..5447b6755 100644 --- a/src/types.rs +++ b/src/types.rs @@ -121,6 +121,10 @@ pub enum UIEvent { EnvelopeUpdate(EnvelopeHash), EnvelopeRename(EnvelopeHash, EnvelopeHash), // old_hash, new_hash EnvelopeRemove(EnvelopeHash), + Contacts(ContactEvent), + Compose(ComposeEvent), + FinishedUIDialog(crate::components::ComponentId, UIMessage), + GlobalUIDialog(Box), Timer(u8), } @@ -311,3 +315,15 @@ impl RateLimit { self.timer.si_value } } + +#[derive(Debug)] +pub enum ContactEvent { + CreateContacts(Vec), +} + +#[derive(Debug)] +pub enum ComposeEvent { + SetReceipients(Vec), +} + +pub type UIMessage = Box;