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
Manos Pitsidianakis 2019-10-03 01:03:20 +03:00
parent fb8a4b020d
commit cd761b3166
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
4 changed files with 512 additions and 232 deletions

View File

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

View File

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

View File

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

View File

@ -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),
),
)
}