ui: revamp option dialog
Selector component shows choices/options to the user. Ok and Cancel buttons were added, along with a window border and window title.embed
parent
fb8a4b020d
commit
cd761b3166
|
@ -73,21 +73,13 @@ impl Default for Composer {
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum ViewMode {
|
enum ViewMode {
|
||||||
Discard(Uuid),
|
Discard(Uuid, Selector<char>),
|
||||||
Edit,
|
Edit,
|
||||||
//Selector(Selector),
|
//Selector(Selector),
|
||||||
ThreadView,
|
ThreadView,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ViewMode {
|
impl ViewMode {
|
||||||
fn is_discard(&self) -> bool {
|
|
||||||
if let ViewMode::Discard(_) = self {
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_edit(&self) -> bool {
|
fn is_edit(&self) -> bool {
|
||||||
if let ViewMode::Edit = self {
|
if let ViewMode::Edit = self {
|
||||||
true
|
true
|
||||||
|
@ -452,62 +444,11 @@ impl Component for Composer {
|
||||||
self.pager.set_dirty();
|
self.pager.set_dirty();
|
||||||
self.pager.draw(grid, body_area, context);
|
self.pager.draw(grid, body_area, context);
|
||||||
}
|
}
|
||||||
ViewMode::Discard(_) => {
|
ViewMode::Discard(_, ref mut s) => {
|
||||||
|
self.pager.set_dirty();
|
||||||
|
self.pager.draw(grid, body_area, context);
|
||||||
/* Let user choose whether to quit with/without saving or cancel */
|
/* Let user choose whether to quit with/without saving or cancel */
|
||||||
let mid_x = { std::cmp::max(width!(area) / 2, width / 2) - width / 2 };
|
s.draw(grid, center_area(area, s.content.size()), context);
|
||||||
let mid_y = { std::cmp::max(height!(area) / 2, 11) - 11 };
|
|
||||||
|
|
||||||
let upper_left = upper_left!(body_area);
|
|
||||||
let bottom_right = bottom_right!(body_area);
|
|
||||||
let area = (
|
|
||||||
pos_inc(upper_left, (mid_x, mid_y)),
|
|
||||||
pos_dec(bottom_right, (mid_x, mid_y)),
|
|
||||||
);
|
|
||||||
create_box(grid, area);
|
|
||||||
let area = (
|
|
||||||
pos_inc(upper_left, (mid_x + 2, mid_y + 2)),
|
|
||||||
pos_dec(
|
|
||||||
bottom_right,
|
|
||||||
(mid_x.saturating_sub(2), mid_y.saturating_sub(2)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
let (_, y) = write_string_to_grid(
|
|
||||||
&format!("Draft \"{:10}\"", self.draft.headers()["Subject"]),
|
|
||||||
grid,
|
|
||||||
Color::Default,
|
|
||||||
Color::Default,
|
|
||||||
Attr::Default,
|
|
||||||
area,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
let (_, y) = write_string_to_grid(
|
|
||||||
"[x] quit without saving",
|
|
||||||
grid,
|
|
||||||
Color::Byte(124),
|
|
||||||
Color::Default,
|
|
||||||
Attr::Default,
|
|
||||||
(set_y(upper_left!(area), y + 2), bottom_right!(area)),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
let (_, y) = write_string_to_grid(
|
|
||||||
"[y] save draft and quit",
|
|
||||||
grid,
|
|
||||||
Color::Byte(124),
|
|
||||||
Color::Default,
|
|
||||||
Attr::Default,
|
|
||||||
(set_y(upper_left!(area), y + 1), bottom_right!(area)),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
write_string_to_grid(
|
|
||||||
"[n] cancel",
|
|
||||||
grid,
|
|
||||||
Color::Byte(124),
|
|
||||||
Color::Default,
|
|
||||||
Attr::Default,
|
|
||||||
(set_y(upper_left!(area), y + 1), bottom_right!(area)),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -535,6 +476,95 @@ impl Component for Composer {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
(ViewMode::Discard(_, ref mut selector), _, _) => {
|
||||||
|
if selector.process_event(event, context) {
|
||||||
|
if selector.is_done() {
|
||||||
|
let (u, s) = match std::mem::replace(&mut self.mode, ViewMode::ThreadView) {
|
||||||
|
ViewMode::Discard(u, s) => (u, s),
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
let key = s.collect()[0] as char;
|
||||||
|
match key {
|
||||||
|
'x' => {
|
||||||
|
context.replies.push_back(UIEvent::Action(Tab(Kill(u))));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
'n' => {}
|
||||||
|
'y' => {
|
||||||
|
let mut failure = true;
|
||||||
|
let draft = std::mem::replace(&mut self.draft, Draft::default());
|
||||||
|
|
||||||
|
let draft = draft.finalise().unwrap();
|
||||||
|
for folder in &[
|
||||||
|
&context.accounts[self.account_cursor]
|
||||||
|
.special_use_folder(SpecialUseMailbox::Drafts),
|
||||||
|
&context.accounts[self.account_cursor]
|
||||||
|
.special_use_folder(SpecialUseMailbox::Inbox),
|
||||||
|
&context.accounts[self.account_cursor]
|
||||||
|
.special_use_folder(SpecialUseMailbox::Normal),
|
||||||
|
] {
|
||||||
|
if folder.is_none() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let folder = folder.unwrap();
|
||||||
|
if let Err(e) = context.accounts[self.account_cursor].save(
|
||||||
|
draft.as_bytes(),
|
||||||
|
folder,
|
||||||
|
Some(Flag::SEEN | Flag::DRAFT),
|
||||||
|
) {
|
||||||
|
debug!("{:?} could not save draft msg", e);
|
||||||
|
log(
|
||||||
|
format!(
|
||||||
|
"Could not save draft in '{}' folder: {}.",
|
||||||
|
folder,
|
||||||
|
e.to_string()
|
||||||
|
),
|
||||||
|
ERROR,
|
||||||
|
);
|
||||||
|
context.replies.push_back(UIEvent::Notification(
|
||||||
|
Some(format!(
|
||||||
|
"Could not save draft in '{}' folder.",
|
||||||
|
folder
|
||||||
|
)),
|
||||||
|
e.into(),
|
||||||
|
Some(NotificationType::ERROR),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
failure = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if failure {
|
||||||
|
let file =
|
||||||
|
create_temp_file(draft.as_bytes(), None, None, false);
|
||||||
|
debug!("message saved in {}", file.path.display());
|
||||||
|
log(
|
||||||
|
format!(
|
||||||
|
"Message was stored in {} so that you can restore it manually.",
|
||||||
|
file.path.display()
|
||||||
|
),
|
||||||
|
INFO,
|
||||||
|
);
|
||||||
|
context.replies.push_back(UIEvent::Notification(
|
||||||
|
Some("Could not save in any folder".into()),
|
||||||
|
format!(
|
||||||
|
"Message was stored in {} so that you can restore it manually.",
|
||||||
|
file.path.display()
|
||||||
|
),
|
||||||
|
Some(NotificationType::INFO),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
context.replies.push_back(UIEvent::Action(Tab(Kill(u))));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
self.set_dirty();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
if self.form.process_event(event, context) {
|
if self.form.process_event(event, context) {
|
||||||
|
@ -574,85 +604,6 @@ impl Component for Composer {
|
||||||
UIEvent::Input(Key::Down) => {
|
UIEvent::Input(Key::Down) => {
|
||||||
self.cursor = Cursor::Body;
|
self.cursor = Cursor::Body;
|
||||||
}
|
}
|
||||||
UIEvent::Input(Key::Char(key)) if self.mode.is_discard() => {
|
|
||||||
match (key, &self.mode) {
|
|
||||||
('x', ViewMode::Discard(u)) => {
|
|
||||||
context.replies.push_back(UIEvent::Action(Tab(Kill(*u))));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
('n', _) => {}
|
|
||||||
('y', ViewMode::Discard(u)) => {
|
|
||||||
let mut failure = true;
|
|
||||||
let draft = std::mem::replace(&mut self.draft, Draft::default());
|
|
||||||
|
|
||||||
let draft = draft.finalise().unwrap();
|
|
||||||
for folder in &[
|
|
||||||
&context.accounts[self.account_cursor]
|
|
||||||
.special_use_folder(SpecialUseMailbox::Drafts),
|
|
||||||
&context.accounts[self.account_cursor]
|
|
||||||
.special_use_folder(SpecialUseMailbox::Inbox),
|
|
||||||
&context.accounts[self.account_cursor]
|
|
||||||
.special_use_folder(SpecialUseMailbox::Normal),
|
|
||||||
] {
|
|
||||||
if folder.is_none() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let folder = folder.unwrap();
|
|
||||||
if let Err(e) = context.accounts[self.account_cursor].save(
|
|
||||||
draft.as_bytes(),
|
|
||||||
folder,
|
|
||||||
Some(Flag::SEEN | Flag::DRAFT),
|
|
||||||
) {
|
|
||||||
debug!("{:?} could not save draft msg", e);
|
|
||||||
log(
|
|
||||||
format!(
|
|
||||||
"Could not save draft in '{}' folder: {}.",
|
|
||||||
folder,
|
|
||||||
e.to_string()
|
|
||||||
),
|
|
||||||
ERROR,
|
|
||||||
);
|
|
||||||
context.replies.push_back(UIEvent::Notification(
|
|
||||||
Some(format!("Could not save draft in '{}' folder.", folder)),
|
|
||||||
e.into(),
|
|
||||||
Some(NotificationType::ERROR),
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
failure = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if failure {
|
|
||||||
let file = create_temp_file(draft.as_bytes(), None, None, false);
|
|
||||||
debug!("message saved in {}", file.path.display());
|
|
||||||
log(
|
|
||||||
format!(
|
|
||||||
"Message was stored in {} so that you can restore it manually.",
|
|
||||||
file.path.display()
|
|
||||||
),
|
|
||||||
INFO,
|
|
||||||
);
|
|
||||||
context.replies.push_back(UIEvent::Notification(
|
|
||||||
Some("Could not save in any folder".into()),
|
|
||||||
format!(
|
|
||||||
"Message was stored in {} so that you can restore it manually.",
|
|
||||||
file.path.display()
|
|
||||||
),
|
|
||||||
Some(NotificationType::INFO),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
context.replies.push_back(UIEvent::Action(Tab(Kill(*u))));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.mode = ViewMode::ThreadView;
|
|
||||||
self.set_dirty();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
/* Switch to thread view mode if we're on Edit mode */
|
/* Switch to thread view mode if we're on Edit mode */
|
||||||
UIEvent::Input(Key::Char('v')) if self.mode.is_edit() => {
|
UIEvent::Input(Key::Char('v')) if self.mode.is_edit() => {
|
||||||
self.mode = ViewMode::ThreadView;
|
self.mode = ViewMode::ThreadView;
|
||||||
|
@ -818,7 +769,18 @@ impl Component for Composer {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn kill(&mut self, uuid: Uuid, _context: &mut Context) {
|
fn kill(&mut self, uuid: Uuid, _context: &mut Context) {
|
||||||
self.mode = ViewMode::Discard(uuid);
|
self.mode = ViewMode::Discard(
|
||||||
|
uuid,
|
||||||
|
Selector::new(
|
||||||
|
"this draft has unsaved changes",
|
||||||
|
vec![
|
||||||
|
('x', "quit without saving".to_string()),
|
||||||
|
('y', "save draft and quit".to_string()),
|
||||||
|
('n', "cancel".to_string()),
|
||||||
|
],
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_shortcuts(&self, context: &Context) -> ShortcutMaps {
|
fn get_shortcuts(&self, context: &Context) -> ShortcutMaps {
|
||||||
|
@ -855,7 +817,18 @@ impl Component for Composer {
|
||||||
|
|
||||||
fn can_quit_cleanly(&mut self) -> bool {
|
fn can_quit_cleanly(&mut self) -> bool {
|
||||||
/* Play it safe and ask user for confirmation */
|
/* Play it safe and ask user for confirmation */
|
||||||
self.mode = ViewMode::Discard(self.id);
|
self.mode = ViewMode::Discard(
|
||||||
|
self.id,
|
||||||
|
Selector::new(
|
||||||
|
"this draft has unsaved changes",
|
||||||
|
vec![
|
||||||
|
('x', "quit without saving".to_string()),
|
||||||
|
('y', "save draft and quit".to_string()),
|
||||||
|
('n', "cancel".to_string()),
|
||||||
|
],
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
);
|
||||||
self.set_dirty();
|
self.set_dirty();
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,7 @@ enum ViewMode {
|
||||||
Attachment(usize),
|
Attachment(usize),
|
||||||
Raw,
|
Raw,
|
||||||
Subview,
|
Subview,
|
||||||
ContactSelector(Selector),
|
ContactSelector(Selector<Card>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ViewMode {
|
impl Default for ViewMode {
|
||||||
|
@ -60,6 +60,12 @@ impl ViewMode {
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fn is_contact_selector(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
ViewMode::ContactSelector(_) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Contains an Envelope view, with sticky headers, a pager for the body, and subviews for more
|
/// Contains an Envelope view, with sticky headers, a pager for the body, and subviews for more
|
||||||
|
@ -630,16 +636,15 @@ impl Component for MailView {
|
||||||
s.draw(grid, (set_y(upper_left, y + 1), bottom_right), context);
|
s.draw(grid, (set_y(upper_left, y + 1), bottom_right), context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ViewMode::ContactSelector(ref mut s) => {
|
|
||||||
clear_area(grid, (set_y(upper_left, y + 1), bottom_right));
|
|
||||||
s.draw(grid, (set_y(upper_left, y + 1), bottom_right), context);
|
|
||||||
}
|
|
||||||
_ => {
|
_ => {
|
||||||
if let Some(p) = self.pager.as_mut() {
|
if let Some(p) = self.pager.as_mut() {
|
||||||
p.draw(grid, (set_y(upper_left, y + 1), bottom_right), context);
|
p.draw(grid, (set_y(upper_left, y + 1), bottom_right), context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let ViewMode::ContactSelector(ref mut s) = self.mode {
|
||||||
|
s.draw(grid, center_area(area, s.content.size()), context);
|
||||||
|
}
|
||||||
self.dirty = false;
|
self.dirty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -654,6 +659,19 @@ impl Component for MailView {
|
||||||
}
|
}
|
||||||
ViewMode::ContactSelector(ref mut s) => {
|
ViewMode::ContactSelector(ref mut s) => {
|
||||||
if s.process_event(event, context) {
|
if s.process_event(event, context) {
|
||||||
|
if s.is_done() {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.set_dirty();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -667,55 +685,22 @@ impl Component for MailView {
|
||||||
}
|
}
|
||||||
|
|
||||||
match *event {
|
match *event {
|
||||||
UIEvent::Input(Key::Char('c')) => {
|
UIEvent::Input(Key::Char('c')) if !self.mode.is_contact_selector() => {
|
||||||
if let ViewMode::ContactSelector(_) = self.mode {
|
|
||||||
if let ViewMode::ContactSelector(s) =
|
|
||||||
std::mem::replace(&mut self.mode, ViewMode::Normal)
|
|
||||||
{
|
|
||||||
let account = &mut context.accounts[self.coordinates.0];
|
|
||||||
let mut results = Vec::new();
|
|
||||||
{
|
|
||||||
let envelope: &Envelope = &account.get_env(&self.coordinates.2);
|
|
||||||
for c in s.collect() {
|
|
||||||
let c = usize::from_ne_bytes({
|
|
||||||
[c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7]]
|
|
||||||
});
|
|
||||||
for (idx, env) in envelope
|
|
||||||
.from()
|
|
||||||
.iter()
|
|
||||||
.chain(envelope.to().iter())
|
|
||||||
.enumerate()
|
|
||||||
{
|
|
||||||
if idx != c {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut new_card: Card = Card::new();
|
|
||||||
new_card.set_email(env.get_email());
|
|
||||||
new_card.set_name(env.get_display_name());
|
|
||||||
results.push(new_card);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for c in results {
|
|
||||||
account.address_book.add_card(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
let account = &mut context.accounts[self.coordinates.0];
|
let account = &mut context.accounts[self.coordinates.0];
|
||||||
let envelope: &Envelope = &account.get_env(&self.coordinates.2);
|
let envelope: &Envelope = &account.get_env(&self.coordinates.2);
|
||||||
|
|
||||||
let mut entries = Vec::new();
|
let mut entries = Vec::new();
|
||||||
for (idx, env) in envelope
|
for addr in envelope.from().iter().chain(envelope.to().iter()) {
|
||||||
.from()
|
let mut new_card: Card = Card::new();
|
||||||
.iter()
|
new_card.set_email(addr.get_email());
|
||||||
.chain(envelope.to().iter())
|
new_card.set_name(addr.get_display_name());
|
||||||
.enumerate()
|
entries.push((new_card, format!("{}", addr)));
|
||||||
{
|
|
||||||
entries.push((idx.to_ne_bytes().to_vec(), format!("{}", env)));
|
|
||||||
}
|
}
|
||||||
self.mode = ViewMode::ContactSelector(Selector::new(entries, true));
|
self.mode = ViewMode::ContactSelector(Selector::new(
|
||||||
|
"select contacts to add",
|
||||||
|
entries,
|
||||||
|
false,
|
||||||
|
));
|
||||||
self.dirty = true;
|
self.dirty = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1568,29 +1568,44 @@ impl Component for Tabbed {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type EntryIdentifier = Vec<u8>;
|
#[derive(Debug, Copy, PartialEq, Clone)]
|
||||||
/// Shows selection to user
|
enum SelectorCursor {
|
||||||
|
/// Cursor is at an entry
|
||||||
|
Entry(usize),
|
||||||
|
/// Cursor is located on the Ok button
|
||||||
|
Ok,
|
||||||
|
/// Cursor is located on the Cancel button
|
||||||
|
Cancel,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows a little window with options for user to select.
|
||||||
|
///
|
||||||
|
/// Instantiate with Selector::new(). Set single_only to true if user should only choose one of 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
|
||||||
|
/// Selector::collect()
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
pub struct Selector {
|
pub struct Selector<T: PartialEq + Debug + Clone + Sync + Send> {
|
||||||
single_only: bool,
|
|
||||||
/// allow only one selection
|
/// allow only one selection
|
||||||
entries: Vec<(EntryIdentifier, bool)>,
|
single_only: bool,
|
||||||
selected_entry_count: u32,
|
entries: Vec<(T, bool)>,
|
||||||
content: CellBuffer,
|
pub content: CellBuffer,
|
||||||
|
|
||||||
cursor: usize,
|
cursor: SelectorCursor,
|
||||||
|
|
||||||
|
/// If true, user has finished their selection
|
||||||
|
done: bool,
|
||||||
dirty: bool,
|
dirty: bool,
|
||||||
id: ComponentId,
|
id: ComponentId,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Selector {
|
impl<T: PartialEq + Debug + Clone + Sync + Send> fmt::Display for Selector<T> {
|
||||||
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 Component for Selector {
|
impl<T: PartialEq + Debug + Clone + Sync + Send> Component for Selector<T> {
|
||||||
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)));
|
||||||
|
@ -1598,17 +1613,24 @@ impl Component for Selector {
|
||||||
}
|
}
|
||||||
fn process_event(&mut self, event: &mut UIEvent, _context: &mut Context) -> bool {
|
fn process_event(&mut self, event: &mut UIEvent, _context: &mut Context) -> bool {
|
||||||
let (width, height) = self.content.size();
|
let (width, height) = self.content.size();
|
||||||
match *event {
|
match (event, self.cursor) {
|
||||||
UIEvent::Input(Key::Char('\t')) => {
|
(UIEvent::Input(Key::Char('\n')), _) if self.single_only => {
|
||||||
self.entries[self.cursor].1 = !self.entries[self.cursor].1;
|
/* User can only select one entry, so Enter key finalises the selection */
|
||||||
if self.entries[self.cursor].1 {
|
self.done = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
(UIEvent::Input(Key::Char('\n')), SelectorCursor::Entry(c)) if !self.single_only => {
|
||||||
|
/* User can select multiple entries, so Enter key toggles the entry under the
|
||||||
|
* cursor */
|
||||||
|
self.entries[c].1 = !self.entries[c].1;
|
||||||
|
if self.entries[c].1 {
|
||||||
write_string_to_grid(
|
write_string_to_grid(
|
||||||
"x",
|
"x",
|
||||||
&mut self.content,
|
&mut self.content,
|
||||||
Color::Default,
|
Color::Default,
|
||||||
Color::Default,
|
Color::Default,
|
||||||
Attr::Default,
|
Attr::Default,
|
||||||
((1, self.cursor), (width, self.cursor)),
|
((3, c + 2), (width - 2, c + 2)),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -1618,23 +1640,170 @@ impl Component for Selector {
|
||||||
Color::Default,
|
Color::Default,
|
||||||
Color::Default,
|
Color::Default,
|
||||||
Attr::Default,
|
Attr::Default,
|
||||||
((1, self.cursor), (width, self.cursor)),
|
((3, c + 2), (width - 2, c + 2)),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
self.dirty = true;
|
self.dirty = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
UIEvent::Input(Key::Up) if self.cursor > 0 => {
|
(UIEvent::Input(Key::Char('\n')), SelectorCursor::Ok) if !self.single_only => {
|
||||||
self.cursor -= 1;
|
self.done = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
(UIEvent::Input(Key::Char('\n')), SelectorCursor::Cancel) if !self.single_only => {
|
||||||
|
for e in self.entries.iter_mut() {
|
||||||
|
e.1 = false;
|
||||||
|
}
|
||||||
|
self.done = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
(UIEvent::Input(Key::Up), SelectorCursor::Entry(c)) if c > 0 => {
|
||||||
|
if self.single_only {
|
||||||
|
// Redraw selection
|
||||||
|
change_colors(
|
||||||
|
&mut self.content,
|
||||||
|
((2, c + 2), (width - 2, c + 2)),
|
||||||
|
Color::Default,
|
||||||
|
Color::Default,
|
||||||
|
);
|
||||||
|
change_colors(
|
||||||
|
&mut self.content,
|
||||||
|
((2, c + 1), (width - 2, c + 1)),
|
||||||
|
Color::Default,
|
||||||
|
Color::Byte(8),
|
||||||
|
);
|
||||||
|
self.entries[c].1 = false;
|
||||||
|
self.entries[c - 1].1 = true;
|
||||||
|
} else {
|
||||||
|
// Redraw cursor
|
||||||
|
change_colors(
|
||||||
|
&mut self.content,
|
||||||
|
((2, c + 2), (4, c + 2)),
|
||||||
|
Color::Default,
|
||||||
|
Color::Default,
|
||||||
|
);
|
||||||
|
change_colors(
|
||||||
|
&mut self.content,
|
||||||
|
((2, c + 1), (4, c + 1)),
|
||||||
|
Color::Default,
|
||||||
|
Color::Byte(8),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.cursor = SelectorCursor::Entry(c - 1);
|
||||||
self.dirty = true;
|
self.dirty = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
UIEvent::Input(Key::Down) if self.cursor < height.saturating_sub(1) => {
|
(UIEvent::Input(Key::Up), SelectorCursor::Ok)
|
||||||
self.cursor += 1;
|
| (UIEvent::Input(Key::Up), SelectorCursor::Cancel) => {
|
||||||
|
change_colors(
|
||||||
|
&mut self.content,
|
||||||
|
((width / 2, height - 2), (width - 1, height - 2)),
|
||||||
|
Color::Default,
|
||||||
|
Color::Default,
|
||||||
|
);
|
||||||
|
let c = self.entries.len().saturating_sub(1);
|
||||||
|
self.cursor = SelectorCursor::Entry(c);
|
||||||
|
change_colors(
|
||||||
|
&mut self.content,
|
||||||
|
((2, c + 2), (4, c + 2)),
|
||||||
|
Color::Default,
|
||||||
|
Color::Byte(8),
|
||||||
|
);
|
||||||
self.dirty = true;
|
self.dirty = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
(UIEvent::Input(Key::Down), SelectorCursor::Entry(c))
|
||||||
|
if c < self.entries.len().saturating_sub(1) =>
|
||||||
|
{
|
||||||
|
if self.single_only {
|
||||||
|
// Redraw selection
|
||||||
|
change_colors(
|
||||||
|
&mut self.content,
|
||||||
|
((2, c + 2), (width - 2, c + 2)),
|
||||||
|
Color::Default,
|
||||||
|
Color::Default,
|
||||||
|
);
|
||||||
|
change_colors(
|
||||||
|
&mut self.content,
|
||||||
|
((2, c + 3), (width - 2, c + 3)),
|
||||||
|
Color::Default,
|
||||||
|
Color::Byte(8),
|
||||||
|
);
|
||||||
|
self.entries[c].1 = false;
|
||||||
|
self.entries[c + 1].1 = true;
|
||||||
|
} else {
|
||||||
|
// Redraw cursor
|
||||||
|
change_colors(
|
||||||
|
&mut self.content,
|
||||||
|
((2, c + 2), (4, c + 2)),
|
||||||
|
Color::Default,
|
||||||
|
Color::Default,
|
||||||
|
);
|
||||||
|
change_colors(
|
||||||
|
&mut self.content,
|
||||||
|
((2, c + 3), (4, c + 3)),
|
||||||
|
Color::Default,
|
||||||
|
Color::Byte(8),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.cursor = SelectorCursor::Entry(c + 1);
|
||||||
|
self.dirty = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
(UIEvent::Input(Key::Down), SelectorCursor::Entry(c)) if !self.single_only => {
|
||||||
|
self.cursor = SelectorCursor::Ok;
|
||||||
|
change_colors(
|
||||||
|
&mut self.content,
|
||||||
|
((2, c + 2), (4, c + 2)),
|
||||||
|
Color::Default,
|
||||||
|
Color::Default,
|
||||||
|
);
|
||||||
|
change_colors(
|
||||||
|
&mut self.content,
|
||||||
|
((width / 2, height - 2), (width / 2 + 1, height - 2)),
|
||||||
|
Color::Default,
|
||||||
|
Color::Byte(8),
|
||||||
|
);
|
||||||
|
self.dirty = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
(UIEvent::Input(Key::Down), _) | (UIEvent::Input(Key::Up), _) => return true,
|
||||||
|
(UIEvent::Input(Key::Right), SelectorCursor::Ok) => {
|
||||||
|
self.cursor = SelectorCursor::Cancel;
|
||||||
|
change_colors(
|
||||||
|
&mut self.content,
|
||||||
|
((width / 2, height - 2), (width / 2 + 1, height - 2)),
|
||||||
|
Color::Default,
|
||||||
|
Color::Default,
|
||||||
|
);
|
||||||
|
change_colors(
|
||||||
|
&mut self.content,
|
||||||
|
((width / 2 + 6, height - 2), (width / 2 + 11, height - 2)),
|
||||||
|
Color::Default,
|
||||||
|
Color::Byte(8),
|
||||||
|
);
|
||||||
|
self.dirty = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
(UIEvent::Input(Key::Left), SelectorCursor::Cancel) => {
|
||||||
|
self.cursor = SelectorCursor::Ok;
|
||||||
|
change_colors(
|
||||||
|
&mut self.content,
|
||||||
|
((width / 2, height - 2), (width / 2 + 1, height - 2)),
|
||||||
|
Color::Default,
|
||||||
|
Color::Byte(8),
|
||||||
|
);
|
||||||
|
change_colors(
|
||||||
|
&mut self.content,
|
||||||
|
((width / 2 + 6, height - 2), (width / 2 + 11, height - 2)),
|
||||||
|
Color::Default,
|
||||||
|
Color::Default,
|
||||||
|
);
|
||||||
|
self.dirty = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
(UIEvent::Input(Key::Left), _) | (UIEvent::Input(Key::Right), _) => return true,
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1655,44 +1824,179 @@ impl Component for Selector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Selector {
|
impl<T: PartialEq + Debug + Clone + Sync + Send> Selector<T> {
|
||||||
pub fn new(mut entries: Vec<(EntryIdentifier, String)>, single_only: bool) -> Selector {
|
pub fn new(title: &str, entries: Vec<(T, String)>, single_only: bool) -> Selector<T> {
|
||||||
let width = entries
|
let width = std::cmp::max(
|
||||||
.iter()
|
"OK Cancel".len(),
|
||||||
.max_by_key(|e| e.1.len())
|
std::cmp::max(
|
||||||
.map(|v| v.1.len())
|
entries
|
||||||
.unwrap_or(0)
|
.iter()
|
||||||
+ 4;
|
.max_by_key(|e| e.1.len())
|
||||||
let height = entries.len();
|
.map(|v| v.1.len())
|
||||||
|
.unwrap_or(0),
|
||||||
|
title.len(),
|
||||||
|
),
|
||||||
|
) + 7;
|
||||||
|
let height = entries.len()
|
||||||
|
+ 4
|
||||||
|
+ if single_only {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
/* Extra room for buttons Okay/Cancel */
|
||||||
|
3
|
||||||
|
};
|
||||||
let mut content = CellBuffer::new(width, height, Cell::with_char(' '));
|
let mut content = CellBuffer::new(width, height, Cell::with_char(' '));
|
||||||
let identifiers = entries
|
write_string_to_grid(
|
||||||
.iter_mut()
|
"┏━",
|
||||||
.map(|(id, _)| (std::mem::replace(&mut *id, Vec::new()), false))
|
&mut content,
|
||||||
.collect();
|
Color::Byte(8),
|
||||||
for (i, e) in entries.into_iter().enumerate() {
|
Color::Default,
|
||||||
|
Attr::Default,
|
||||||
|
((0, 0), (width - 1, 0)),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
let (x, _) = write_string_to_grid(
|
||||||
|
title,
|
||||||
|
&mut content,
|
||||||
|
Color::Default,
|
||||||
|
Color::Default,
|
||||||
|
Attr::Default,
|
||||||
|
((2, 0), (width - 1, 0)),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
for i in 1..(width - title.len() - 1) {
|
||||||
write_string_to_grid(
|
write_string_to_grid(
|
||||||
&format!("[ ] {}", e.1),
|
"━",
|
||||||
&mut content,
|
&mut content,
|
||||||
|
Color::Byte(8),
|
||||||
Color::Default,
|
Color::Default,
|
||||||
|
Attr::Default,
|
||||||
|
((x + i, 0), (width - 1, 0)),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
write_string_to_grid(
|
||||||
|
"┓",
|
||||||
|
&mut content,
|
||||||
|
Color::Byte(8),
|
||||||
|
Color::Default,
|
||||||
|
Attr::Default,
|
||||||
|
((width - 1, 0), (width - 1, 0)),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
write_string_to_grid(
|
||||||
|
"┗",
|
||||||
|
&mut content,
|
||||||
|
Color::Byte(8),
|
||||||
|
Color::Default,
|
||||||
|
Attr::Default,
|
||||||
|
((0, height - 1), (width - 1, height - 1)),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
write_string_to_grid(
|
||||||
|
&"━".repeat(width - 2),
|
||||||
|
&mut content,
|
||||||
|
Color::Byte(8),
|
||||||
|
Color::Default,
|
||||||
|
Attr::Default,
|
||||||
|
((1, height - 1), (width - 2, height - 1)),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
write_string_to_grid(
|
||||||
|
"┛",
|
||||||
|
&mut content,
|
||||||
|
Color::Byte(8),
|
||||||
|
Color::Default,
|
||||||
|
Attr::Default,
|
||||||
|
((width - 1, height - 1), (width - 1, height - 1)),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
for i in 1..height - 1 {
|
||||||
|
write_string_to_grid(
|
||||||
|
"┃",
|
||||||
|
&mut content,
|
||||||
|
Color::Byte(8),
|
||||||
Color::Default,
|
Color::Default,
|
||||||
Attr::Default,
|
Attr::Default,
|
||||||
((0, i), (width - 1, i)),
|
((0, i), (width - 1, i)),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
write_string_to_grid(
|
||||||
|
"┃",
|
||||||
|
&mut content,
|
||||||
|
Color::Byte(8),
|
||||||
|
Color::Default,
|
||||||
|
Attr::Default,
|
||||||
|
((width - 1, i), (width - 1, i)),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if single_only {
|
||||||
|
for (i, e) in entries.iter().enumerate() {
|
||||||
|
write_string_to_grid(
|
||||||
|
&e.1,
|
||||||
|
&mut content,
|
||||||
|
Color::Default,
|
||||||
|
if i == 0 {
|
||||||
|
Color::Byte(8)
|
||||||
|
} else {
|
||||||
|
Color::Default
|
||||||
|
},
|
||||||
|
Attr::Default,
|
||||||
|
((2, i + 2), (width - 1, i + 2)),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (i, e) in entries.iter().enumerate() {
|
||||||
|
write_string_to_grid(
|
||||||
|
&format!("[ ] {}", e.1),
|
||||||
|
&mut content,
|
||||||
|
Color::Default,
|
||||||
|
Color::Default,
|
||||||
|
Attr::Default,
|
||||||
|
((2, i + 2), (width - 1, i + 2)),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
if i == 0 {
|
||||||
|
content[(2, i + 2)].set_bg(Color::Byte(8));
|
||||||
|
content[(3, i + 2)].set_bg(Color::Byte(8));
|
||||||
|
content[(4, i + 2)].set_bg(Color::Byte(8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write_string_to_grid(
|
||||||
|
"OK Cancel",
|
||||||
|
&mut content,
|
||||||
|
Color::Default,
|
||||||
|
Color::Default,
|
||||||
|
Attr::Bold,
|
||||||
|
((width / 2, height - 2), (width - 1, height - 2)),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let mut identifiers: Vec<(T, bool)> =
|
||||||
|
entries.into_iter().map(|(id, _)| (id, false)).collect();
|
||||||
|
if single_only {
|
||||||
|
/* set default option */
|
||||||
|
identifiers[0].1 = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Selector {
|
Selector {
|
||||||
single_only,
|
single_only,
|
||||||
entries: identifiers,
|
entries: identifiers,
|
||||||
selected_entry_count: 0,
|
|
||||||
content,
|
content,
|
||||||
cursor: 0,
|
cursor: SelectorCursor::Entry(0),
|
||||||
|
done: false,
|
||||||
dirty: true,
|
dirty: true,
|
||||||
id: ComponentId::new_v4(),
|
id: ComponentId::new_v4(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn collect(self) -> Vec<EntryIdentifier> {
|
pub fn is_done(&self) -> bool {
|
||||||
|
self.done
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn collect(self) -> Vec<T> {
|
||||||
self.entries
|
self.entries
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|v| v.1)
|
.filter(|v| v.1)
|
||||||
|
|
|
@ -799,3 +799,21 @@ pub fn clear_area(grid: &mut CellBuffer, area: Area) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn center_area(area: Area, (width, height): (usize, usize)) -> Area {
|
||||||
|
let mid_x = { std::cmp::max(width!(area) / 2, width / 2) - width / 2 };
|
||||||
|
let mid_y = { std::cmp::max(height!(area) / 2, height / 2) - height / 2 };
|
||||||
|
|
||||||
|
let (upper_x, upper_y) = upper_left!(area);
|
||||||
|
let (max_x, max_y) = bottom_right!(area);
|
||||||
|
(
|
||||||
|
(
|
||||||
|
std::cmp::min(max_x, upper_x + mid_x),
|
||||||
|
std::cmp::min(max_y, upper_y + mid_y),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
std::cmp::min(max_x, upper_x + mid_x + width),
|
||||||
|
std::cmp::min(max_y, upper_y + mid_y + height),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue