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};
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;

View File

@ -29,7 +29,7 @@ pub use self::contact_list::*;
#[derive(Debug)]
enum ViewMode {
ReadOnly,
Discard(Selector<char>),
Discard(UIDialog<char>),
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);

View File

@ -111,11 +111,11 @@ impl Default for Composer {
#[derive(Debug)]
enum ViewMode {
Discard(Uuid, Selector<char>),
Discard(Uuid, UIDialog<char>),
Edit,
Embed,
SelectRecipients(Selector<Address>),
Send(Selector<bool>),
SelectRecipients(UIDialog<Address>),
Send(UIDialog<bool>),
}
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::<Vec<String>>()
.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,
),
);

View File

@ -46,7 +46,7 @@ enum ViewMode {
Raw,
Ansi(RawBuffer),
Subview,
ContactSelector(Selector<Card>),
ContactSelector(UIDialog<Card>),
}
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::<Vec<Card>>()),
))
}),
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::<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(''))
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
/// user has finalised their choices. Collect the choices by consuming the Selector with
/// Selector::collect()
#[derive(Debug, PartialEq, Clone)]
pub struct Selector<T: PartialEq + Debug + Clone + Sync + Send> {
#[derive(Clone)]
pub struct Selector<T: PartialEq + Debug + Clone + Sync + Send, F: 'static + Sync + Clone + Send> {
/// allow only one selection
single_only: 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
done: bool,
done_fn: F,
dirty: bool,
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 {
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) {
let (width, height) = self.content.size();
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(
title: &str,
entries: Vec<(T, String)>,
single_only: bool,
done_fn: F,
context: &Context,
) -> Selector<T> {
) -> Selector<T, F> {
let width = std::cmp::max(
"OK Cancel".len(),
std::cmp::max(
@ -2302,6 +2330,7 @@ impl<T: PartialEq + Debug + Clone + Sync + Send> Selector<T> {
content,
cursor: SelectorCursor::Entry(0),
done: false,
done_fn,
dirty: true,
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)]
pub struct RawBuffer {
pub buf: CellBuffer,

View File

@ -22,7 +22,7 @@
use super::*;
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)]
enum FormFocus {
@ -588,7 +588,7 @@ impl Component for FormWidget {
#[derive(Debug, Default)]
pub struct ButtonWidget<T>
where
T: std::fmt::Debug + Default + Send,
T: std::fmt::Debug + Default + Send + Sync,
{
buttons: FnvHashMap<String, T>,
layout: Vec<String>,
@ -603,7 +603,7 @@ where
impl<T> fmt::Display for ButtonWidget<T>
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<T> ButtonWidget<T>
where
T: std::fmt::Debug + Default + Send,
T: std::fmt::Debug + Default + Send + Sync,
{
pub fn new(init_val: (String, T)) -> ButtonWidget<T> {
ButtonWidget {
@ -642,7 +642,7 @@ where
impl<T> Component for ButtonWidget<T>
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 {

View File

@ -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;

View File

@ -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() {

View File

@ -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<dyn crate::components::Component>),
Timer(u8),
}
@ -311,3 +315,15 @@ impl RateLimit {
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>;