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.
async
Manos Pitsidianakis 2020-02-19 16:57:37 +02:00
parent e22ab2b424
commit a806571322
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
9 changed files with 193 additions and 31 deletions

View File

@ -48,7 +48,7 @@ use uuid::Uuid;
use super::{Key, StatusEvent, UIEvent}; use super::{Key, StatusEvent, UIEvent};
type ComponentId = Uuid; pub type ComponentId = Uuid;
pub type ShortcutMap = FnvHashMap<&'static str, Key>; pub type ShortcutMap = FnvHashMap<&'static str, Key>;
pub type ShortcutMaps = FnvHashMap<&'static str, ShortcutMap>; 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. /// 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 /// 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. /// 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 draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context);
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool; fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool;
fn is_dirty(&self) -> bool; fn is_dirty(&self) -> bool;

View File

@ -29,7 +29,7 @@ pub use self::contact_list::*;
#[derive(Debug)] #[derive(Debug)]
enum ViewMode { enum ViewMode {
ReadOnly, ReadOnly,
Discard(Selector<char>), Discard(UIDialog<char>),
Edit, Edit,
//New, //New,
} }
@ -299,8 +299,9 @@ impl Component for ContactManager {
return true; return true;
} }
let parent_id = self.parent_id;
/* Play it safe and ask user for confirmation */ /* 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", "this contact has unsaved changes",
vec![ vec![
('x', "quit without saving".to_string()), ('x', "quit without saving".to_string()),
@ -308,6 +309,12 @@ impl Component for ContactManager {
('n', "cancel".to_string()), ('n', "cancel".to_string()),
], ],
true, 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, context,
)); ));
self.set_dirty(true); self.set_dirty(true);

View File

@ -111,11 +111,11 @@ impl Default for Composer {
#[derive(Debug)] #[derive(Debug)]
enum ViewMode { enum ViewMode {
Discard(Uuid, Selector<char>), Discard(Uuid, UIDialog<char>),
Edit, Edit,
Embed, Embed,
SelectRecipients(Selector<Address>), SelectRecipients(UIDialog<Address>),
Send(Selector<bool>), Send(UIDialog<bool>),
} }
impl ViewMode { impl ViewMode {
@ -183,7 +183,8 @@ impl Composer {
.map(|m| m.address) .map(|m| m.address)
{ {
let list_address_string = list_address.to_string(); 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", "select recipients",
vec![ vec![
( (
@ -193,6 +194,18 @@ impl Composer {
(list_address, list_address_string), (list_address, list_address_string),
], ],
false, false,
std::sync::Arc::new(move |results: &[Address]| {
Some(UIEvent::FinishedUIDialog(
id,
Box::new(
results
.into_iter()
.map(|a| a.to_string())
.collect::<Vec<String>>()
.join(", "),
),
))
}),
context, context,
)); ));
} }
@ -698,11 +711,15 @@ impl Component for Composer {
&& self.mode.is_edit() => && self.mode.is_edit() =>
{ {
self.update_draft(); self.update_draft();
self.mode = ViewMode::Send(Selector::new( let id = self.id;
self.mode = ViewMode::Send(UIDialog::new(
"send mail?", "send mail?",
vec![(true, "yes".to_string()), (false, "no".to_string())], vec![(true, "yes".to_string()), (false, "no".to_string())],
/* only one choice */ /* only one choice */
true, true,
std::sync::Arc::new(move |results: &[bool]| {
Some(UIEvent::FinishedUIDialog(id, Box::new(results[0])))
}),
context, context,
)); ));
return true; return true;
@ -1005,7 +1022,7 @@ impl Component for Composer {
self.mode = ViewMode::Discard( self.mode = ViewMode::Discard(
uuid, uuid,
Selector::new( UIDialog::new(
"this draft has unsaved changes", "this draft has unsaved changes",
vec![ vec![
('x', "quit without saving".to_string()), ('x', "quit without saving".to_string()),
@ -1013,6 +1030,9 @@ impl Component for Composer {
('n', "cancel".to_string()), ('n', "cancel".to_string()),
], ],
true, true,
std::sync::Arc::new(move |results: &[char]| {
Some(UIEvent::FinishedUIDialog(uuid, Box::new(results[0])))
}),
context, context,
), ),
); );
@ -1043,10 +1063,11 @@ impl Component for Composer {
return true; return true;
} }
let id = self.id;
/* Play it safe and ask user for confirmation */ /* Play it safe and ask user for confirmation */
self.mode = ViewMode::Discard( self.mode = ViewMode::Discard(
self.id, id,
Selector::new( UIDialog::new(
"this draft has unsaved changes", "this draft has unsaved changes",
vec![ vec![
('x', "quit without saving".to_string()), ('x', "quit without saving".to_string()),
@ -1054,6 +1075,9 @@ impl Component for Composer {
('n', "cancel".to_string()), ('n', "cancel".to_string()),
], ],
true, true,
std::sync::Arc::new(move |results: &[char]| {
Some(UIEvent::FinishedUIDialog(id, Box::new(results[0])))
}),
context, context,
), ),
); );

View File

@ -46,7 +46,7 @@ enum ViewMode {
Raw, Raw,
Ansi(RawBuffer), Ansi(RawBuffer),
Subview, Subview,
ContactSelector(Selector<Card>), ContactSelector(UIDialog<Card>),
} }
impl Default for ViewMode { impl Default for ViewMode {
@ -736,14 +736,12 @@ impl Component for MailView {
if let ViewMode::ContactSelector(s) = if let ViewMode::ContactSelector(s) =
std::mem::replace(&mut self.mode, ViewMode::Normal) std::mem::replace(&mut self.mode, ViewMode::Normal)
{ {
let account = &mut context.accounts[self.coordinates.0]; if let Some(event) = s.done() {
{ context.replies.push_back(event);
for card in s.collect() {
account.address_book.add_card(card);
}
} }
} else {
unsafe { std::hint::unreachable_unchecked() }
} }
self.set_dirty(true);
} }
return true; return true;
} }
@ -823,15 +821,35 @@ impl Component for MailView {
entries.push((new_card, format!("{}", addr))); entries.push((new_card, format!("{}", addr)));
} }
drop(envelope); drop(envelope);
let id = self.id;
self.mode = ViewMode::ContactSelector(Selector::new( self.mode = ViewMode::ContactSelector(Selector::new(
"select contacts to add", "select contacts to add",
entries, entries,
false, false,
std::sync::Arc::new(move |results: &[Card]| {
Some(UIEvent::FinishedUIDialog(
id,
Box::new(results.into_iter().cloned().collect::<Vec<Card>>()),
))
}),
context, context,
)); ));
self.dirty = true; self.dirty = true;
return 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::<Vec<Card>>() {
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('')) UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt(''))
if self.mode.is_contact_selector() => if self.mode.is_contact_selector() =>
{ {

View File

@ -1814,8 +1814,8 @@ enum SelectorCursor {
/// options. After passing input events to this component, check Selector::is_done to see if 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 /// user has finalised their choices. Collect the choices by consuming the Selector with
/// Selector::collect() /// Selector::collect()
#[derive(Debug, PartialEq, Clone)] #[derive(Clone)]
pub struct Selector<T: PartialEq + Debug + Clone + Sync + Send> { pub struct Selector<T: PartialEq + Debug + Clone + Sync + Send, F: 'static + Sync + Clone + Send> {
/// allow only one selection /// allow only one selection
single_only: bool, single_only: bool,
entries: Vec<(T, bool)>, entries: Vec<(T, bool)>,
@ -1825,17 +1825,44 @@ pub struct Selector<T: PartialEq + Debug + Clone + Sync + Send> {
/// If true, user has finished their selection /// If true, user has finished their selection
done: bool, done: bool,
done_fn: F,
dirty: bool, dirty: bool,
id: ComponentId, id: ComponentId,
} }
impl<T: PartialEq + Debug + Clone + Sync + Send> fmt::Display for Selector<T> { pub type UIConfirmationDialog =
Selector<bool, std::sync::Arc<dyn Fn(bool) -> Option<UIEvent> + 'static + Sync + Send>>;
pub type UIDialog<T> =
Selector<T, std::sync::Arc<dyn Fn(&[T]) -> Option<UIEvent> + 'static + Sync + Send>>;
impl<T: PartialEq + Debug + Clone + Sync + Send, F: 'static + Clone + Sync + Send> fmt::Debug
for Selector<T, F>
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
Display::fmt("Selector", f) Display::fmt("Selector", f)
} }
} }
impl<T: PartialEq + Debug + Clone + Sync + Send> Component for Selector<T> { impl<T: PartialEq + Debug + Clone + Sync + Send, F: 'static + Clone + Sync + Send> fmt::Display
for Selector<T, F>
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
Display::fmt("Selector", f)
}
}
impl<T: PartialEq + Debug + Clone + Sync + Send, F: 'static + Clone + Sync + Send> PartialEq
for Selector<T, F>
{
fn eq(&self, other: &Selector<T, F>) -> bool {
self.entries == other.entries
}
}
impl<T: PartialEq + Debug + Clone + Sync + Send, F: 'static + Clone + Sync + Send> Component
for Selector<T, F>
{
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
let (width, height) = self.content.size(); let (width, height) = self.content.size();
copy_area_with_break(grid, &self.content, area, ((0, 0), (width, height))); copy_area_with_break(grid, &self.content, area, ((0, 0), (width, height)));
@ -2111,13 +2138,14 @@ impl<T: PartialEq + Debug + Clone + Sync + Send> Component for Selector<T> {
} }
} }
impl<T: PartialEq + Debug + Clone + Sync + Send> Selector<T> { impl<T: PartialEq + Debug + Clone + Sync + Send, F: 'static + Clone + Sync + Send> Selector<T, F> {
pub fn new( pub fn new(
title: &str, title: &str,
entries: Vec<(T, String)>, entries: Vec<(T, String)>,
single_only: bool, single_only: bool,
done_fn: F,
context: &Context, context: &Context,
) -> Selector<T> { ) -> Selector<T, F> {
let width = std::cmp::max( let width = std::cmp::max(
"OK Cancel".len(), "OK Cancel".len(),
std::cmp::max( std::cmp::max(
@ -2302,6 +2330,7 @@ impl<T: PartialEq + Debug + Clone + Sync + Send> Selector<T> {
content, content,
cursor: SelectorCursor::Entry(0), cursor: SelectorCursor::Entry(0),
done: false, done: false,
done_fn,
dirty: true, dirty: true,
id: ComponentId::new_v4(), id: ComponentId::new_v4(),
} }
@ -2320,6 +2349,53 @@ impl<T: PartialEq + Debug + Clone + Sync + Send> Selector<T> {
} }
} }
impl<T: PartialEq + Debug + Clone + Sync + Send> UIDialog<T> {
pub fn done(self) -> Option<UIEvent> {
let Self {
done,
done_fn,
entries,
..
} = self;
if done {
(done_fn)(
entries
.iter()
.filter(|v| v.1)
.map(|(id, _)| id)
.cloned()
.collect::<Vec<_>>()
.as_slice(),
)
} else {
None
}
}
}
impl UIConfirmationDialog {
pub fn done(self) -> Option<UIEvent> {
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)] #[derive(Debug, Clone, PartialEq)]
pub struct RawBuffer { pub struct RawBuffer {
pub buf: CellBuffer, pub buf: CellBuffer,

View File

@ -22,7 +22,7 @@
use super::*; use super::*;
use fnv::FnvHashMap; use fnv::FnvHashMap;
type AutoCompleteFn = Box<dyn Fn(&Context, &str) -> Vec<AutoCompleteEntry> + Send>; type AutoCompleteFn = Box<dyn Fn(&Context, &str) -> Vec<AutoCompleteEntry> + Send + Sync>;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
enum FormFocus { enum FormFocus {
@ -588,7 +588,7 @@ impl Component for FormWidget {
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct ButtonWidget<T> pub struct ButtonWidget<T>
where where
T: std::fmt::Debug + Default + Send, T: std::fmt::Debug + Default + Send + Sync,
{ {
buttons: FnvHashMap<String, T>, buttons: FnvHashMap<String, T>,
layout: Vec<String>, layout: Vec<String>,
@ -603,7 +603,7 @@ where
impl<T> fmt::Display for ButtonWidget<T> impl<T> fmt::Display for ButtonWidget<T>
where where
T: std::fmt::Debug + Default + Send, T: std::fmt::Debug + Default + Send + Sync,
{ {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
Display::fmt("", f) Display::fmt("", f)
@ -612,7 +612,7 @@ where
impl<T> ButtonWidget<T> impl<T> ButtonWidget<T>
where where
T: std::fmt::Debug + Default + Send, T: std::fmt::Debug + Default + Send + Sync,
{ {
pub fn new(init_val: (String, T)) -> ButtonWidget<T> { pub fn new(init_val: (String, T)) -> ButtonWidget<T> {
ButtonWidget { ButtonWidget {
@ -642,7 +642,7 @@ where
impl<T> Component for ButtonWidget<T> impl<T> Component for ButtonWidget<T>
where 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) { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
if self.dirty { if self.dirty {

View File

@ -114,6 +114,26 @@ pub enum Action {
AccountAction(AccountName, AccountAction), 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 AccountName = String;
type FolderPath = String; type FolderPath = String;
type NewFolderPath = String; type NewFolderPath = String;

View File

@ -293,7 +293,8 @@ impl State {
stdout: None, stdout: None,
child: None, child: None,
mode: UIMode::Normal, mode: UIMode::Normal,
components: Vec::with_capacity(1), components: Vec::with_capacity(8),
ui_dialogs: Vec::new(),
timer, timer,
draw_rate_limit: RateLimit::new(1, 3), draw_rate_limit: RateLimit::new(1, 3),
draw_horizontal_segment_fn: if settings.terminal.use_color() { draw_horizontal_segment_fn: if settings.terminal.use_color() {

View File

@ -121,6 +121,10 @@ pub enum UIEvent {
EnvelopeUpdate(EnvelopeHash), EnvelopeUpdate(EnvelopeHash),
EnvelopeRename(EnvelopeHash, EnvelopeHash), // old_hash, new_hash EnvelopeRename(EnvelopeHash, EnvelopeHash), // old_hash, new_hash
EnvelopeRemove(EnvelopeHash), EnvelopeRemove(EnvelopeHash),
Contacts(ContactEvent),
Compose(ComposeEvent),
FinishedUIDialog(crate::components::ComponentId, UIMessage),
GlobalUIDialog(Box<dyn crate::components::Component>),
Timer(u8), Timer(u8),
} }
@ -311,3 +315,15 @@ impl RateLimit {
self.timer.si_value self.timer.si_value
} }
} }
#[derive(Debug)]
pub enum ContactEvent {
CreateContacts(Vec<melib::Card>),
}
#[derive(Debug)]
pub enum ComposeEvent {
SetReceipients(Vec<melib::Address>),
}
pub type UIMessage = Box<dyn 'static + std::any::Any + Send + Sync>;