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;