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
parent
e22ab2b424
commit
a806571322
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
|
|
|
@ -46,7 +46,7 @@ enum ViewMode {
|
|||
Raw,
|
||||
Ansi(RawBuffer),
|
||||
Subview,
|
||||
ContactSelector(Selector<Card>),
|
||||
ContactSelector(UIDialog<Card>),
|
||||
}
|
||||
|
||||
impl Default for ViewMode {
|
||||
|
@ -736,15 +736,13 @@ 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;
|
||||
}
|
||||
if self.pager.process_event(event, context) {
|
||||
|
@ -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() =>
|
||||
{
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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() {
|
||||
|
|
16
src/types.rs
16
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<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>;
|
||||
|
|
Loading…
Reference in New Issue