/* * meli * * Copyright 2020 Manos Pitsidianakis * * This file is part of meli. * * meli is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * meli is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with meli. If not, see . */ use super::*; const OK: &str = "OK"; const CANCEL: &str = "Cancel"; const CANCEL_OFFSET: usize = "OK ".len(); #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum SelectorCursor { Unfocused, /// 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()` pub struct Selector< T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send, F: 'static + Sync + Send, > { /// allow only one selection single_only: bool, entries: Vec<(T, bool)>, entry_titles: Vec, theme_default: ThemeAttribute, cursor: SelectorCursor, scroll_x_cursor: usize, movement: Option, vertical_alignment: Alignment, horizontal_alignment: Alignment, title: String, content: Screen, initialized: bool, /// If `true`, user has finished their selection done: bool, done_fn: F, dirty: bool, id: ComponentId, } pub type UIConfirmationDialog = Selector< bool, Option Option + 'static + Sync + Send>>, >; pub type UIDialog = Selector< T, Option Option + 'static + Sync + Send>>, >; impl std::fmt::Debug for Selector { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { std::fmt::Display::fmt("Selector", f) } } impl std::fmt::Display for Selector { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { std::fmt::Display::fmt("Selector", f) } } impl PartialEq for Selector { fn eq(&self, other: &Self) -> bool { self.entries == other.entries } } impl Component for UIDialog { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { Selector::draw(self, grid, area, context); } fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool { if let UIEvent::ConfigReload { old_settings: _ } = event { self.initialise(context); self.set_dirty(true); return false; } let shortcuts = self.shortcuts(context); match (event, self.cursor) { (UIEvent::Input(Key::Char('\n')), _) if self.single_only => { /* User can only select one entry, so Enter key finalises the selection */ self.done = true; if let Some(event) = self.done() { context.replies.push_back(event); self.unrealize(context); } 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; self.dirty = true; self.initialized = false; return true; } (UIEvent::Input(Key::Char('\n')), SelectorCursor::Ok) if !self.single_only => { self.done = true; if let Some(event) = self.done() { context.replies.push_back(event); self.unrealize(context); } return true; } (UIEvent::Input(Key::Esc), _) => { for e in self.entries.iter_mut() { e.1 = false; } if !self.done { self.unrealize(context); } self.done = true; _ = self.done(); self.cancel(context); self.set_dirty(true); return false; } (UIEvent::Input(Key::Char('\n')), SelectorCursor::Cancel) if !self.single_only => { for e in self.entries.iter_mut() { e.1 = false; } self.done = true; if let Some(event) = self.done() { context.replies.push_back(event); self.unrealize(context); } return true; } (UIEvent::Input(ref key), SelectorCursor::Unfocused) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_down"]) => { if self.single_only { self.entries[0].1 = true; } self.cursor = SelectorCursor::Entry(0); self.dirty = true; self.initialized = false; return true; } (UIEvent::Input(ref key), SelectorCursor::Entry(c)) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_up"]) && c > 0 => { if self.single_only { // Redraw selection self.entries[c].1 = false; self.entries[c - 1].1 = true; } self.cursor = SelectorCursor::Entry(c - 1); self.dirty = true; self.initialized = false; return true; } (UIEvent::Input(ref key), SelectorCursor::Ok) | (UIEvent::Input(ref key), SelectorCursor::Cancel) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_up"]) => { let c = self.entries.len().saturating_sub(1); self.cursor = SelectorCursor::Entry(c); self.dirty = true; self.initialized = false; return true; } (UIEvent::Input(ref key), SelectorCursor::Entry(c)) if c < self.entries.len().saturating_sub(1) && shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_down"]) => { if self.single_only { // Redraw selection self.entries[c].1 = false; self.entries[c + 1].1 = true; } self.cursor = SelectorCursor::Entry(c + 1); self.dirty = true; self.initialized = false; return true; } (UIEvent::Input(ref key), SelectorCursor::Entry(_)) if !self.single_only && shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_down"]) => { self.cursor = SelectorCursor::Ok; self.set_dirty(true); self.initialized = false; return true; } (UIEvent::Input(ref key), SelectorCursor::Ok) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_right"]) => { self.cursor = SelectorCursor::Cancel; self.set_dirty(true); self.initialized = false; return true; } (UIEvent::Input(ref key), SelectorCursor::Cancel) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_left"]) => { self.cursor = SelectorCursor::Ok; self.set_dirty(true); self.initialized = false; return true; } (UIEvent::Input(ref key), _) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_left"]) => { self.movement = Some(PageMovement::Left(1)); self.set_dirty(true); self.initialized = false; return true; } (UIEvent::Input(ref key), _) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_right"]) => { self.movement = Some(PageMovement::Right(1)); self.set_dirty(true); self.initialized = false; return true; } (UIEvent::Input(ref key), _) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["prev_page"]) => { self.movement = Some(PageMovement::PageUp(1)); self.set_dirty(true); self.initialized = false; return true; } (UIEvent::Input(ref key), _) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["next_page"]) => { self.movement = Some(PageMovement::PageDown(1)); self.set_dirty(true); self.initialized = false; return true; } (UIEvent::Input(ref key), _) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["home_page"]) => { self.movement = Some(PageMovement::Home); self.set_dirty(true); self.initialized = false; return true; } (UIEvent::Input(ref key), _) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["end_page"]) => { self.movement = Some(PageMovement::End); self.set_dirty(true); self.initialized = false; return true; } (UIEvent::Input(ref key), _) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_up"]) || shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_down"]) => { return true; } _ => {} } false } fn shortcuts(&self, context: &Context) -> ShortcutMaps { let mut map = ShortcutMaps::default(); map.insert( Shortcuts::GENERAL, context.settings.shortcuts.general.key_values(), ); map } fn is_dirty(&self) -> bool { self.dirty } fn set_dirty(&mut self, value: bool) { self.dirty = value; self.initialized = false; } fn id(&self) -> ComponentId { self.id } } impl Component for UIConfirmationDialog { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { Selector::draw(self, grid, area, context); } fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool { if let UIEvent::ConfigReload { old_settings: _ } = event { self.initialise(context); self.set_dirty(true); return false; } let shortcuts = self.shortcuts(context); match (event, self.cursor) { (UIEvent::Input(Key::Char('\n')), _) if self.single_only => { /* User can only select one entry, so Enter key finalises the selection */ self.done = true; if let Some(event) = self.done() { context.replies.push_back(event); self.unrealize(context); } self.set_dirty(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; self.set_dirty(true); return true; } (UIEvent::Input(Key::Char('\n')), SelectorCursor::Ok) if !self.single_only => { self.done = true; if let Some(event) = self.done() { context.replies.push_back(event); self.unrealize(context); } self.set_dirty(true); return true; } (UIEvent::Input(Key::Esc), _) => { for e in self.entries.iter_mut() { e.1 = false; } if !self.done { self.unrealize(context); } self.done = true; _ = self.done(); self.cancel(context); self.set_dirty(true); return false; } (UIEvent::Input(Key::Char('\n')), SelectorCursor::Cancel) if !self.single_only => { for e in self.entries.iter_mut() { e.1 = false; } self.done = true; if let Some(event) = self.done() { context.replies.push_back(event); self.unrealize(context); } self.set_dirty(true); return true; } (UIEvent::Input(ref key), SelectorCursor::Entry(c)) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_up"]) && c > 0 => { if self.single_only { // Redraw selection self.entries[c].1 = false; self.entries[c - 1].1 = true; } self.cursor = SelectorCursor::Entry(c - 1); self.set_dirty(true); return true; } (UIEvent::Input(ref key), SelectorCursor::Ok) | (UIEvent::Input(ref key), SelectorCursor::Cancel) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_up"]) => { let c = self.entries.len().saturating_sub(1); self.cursor = SelectorCursor::Entry(c); self.set_dirty(true); return true; } (UIEvent::Input(ref key), SelectorCursor::Unfocused) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_down"]) => { if self.single_only { self.entries[0].1 = true; } self.cursor = SelectorCursor::Entry(0); self.set_dirty(true); return true; } (UIEvent::Input(ref key), SelectorCursor::Entry(c)) if c < self.entries.len().saturating_sub(1) && shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_down"]) => { if self.single_only { // Redraw selection self.entries[c].1 = false; self.entries[c + 1].1 = true; } self.cursor = SelectorCursor::Entry(c + 1); self.set_dirty(true); return true; } (UIEvent::Input(ref key), SelectorCursor::Entry(_)) if !self.single_only && shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_down"]) => { self.cursor = SelectorCursor::Ok; self.set_dirty(true); return true; } (UIEvent::Input(ref key), SelectorCursor::Ok) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_right"]) => { self.cursor = SelectorCursor::Cancel; self.set_dirty(true); return true; } (UIEvent::Input(ref key), SelectorCursor::Cancel) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_left"]) => { self.cursor = SelectorCursor::Ok; self.set_dirty(true); return true; } (UIEvent::Input(ref key), _) if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_left"]) || shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_right"]) || shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_up"]) || shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_down"]) => { return true } _ => {} } false } fn shortcuts(&self, context: &Context) -> ShortcutMaps { let mut map = ShortcutMaps::default(); map.insert( Shortcuts::GENERAL, context.settings.shortcuts.general.key_values(), ); map } fn is_dirty(&self) -> bool { self.dirty } fn set_dirty(&mut self, value: bool) { self.dirty = value; } fn id(&self) -> ComponentId { self.id } } impl Selector { pub fn new( title: &str, mut entries: Vec<(T, String)>, single_only: bool, done_fn: F, context: &Context, ) -> Self { let entry_titles = entries .iter_mut() .map(|(_id, ref mut title)| std::mem::take(title)) .collect::>(); let mut identifiers: Vec<(T, bool)> = entries.into_iter().map(|(id, _)| (id, false)).collect(); if single_only { /* set default option */ identifiers[0].1 = true; } let mut ret = Self { single_only, entries: identifiers, entry_titles, cursor: SelectorCursor::Unfocused, scroll_x_cursor: 0, movement: None, vertical_alignment: Alignment::Center, horizontal_alignment: Alignment::Center, title: title.to_string(), content: Screen::::new(), initialized: false, done: false, done_fn, dirty: true, theme_default: Default::default(), id: ComponentId::default(), }; ret.initialise(context); ret } fn initialise(&mut self, context: &Context) { self.theme_default = crate::conf::value(context, "theme_default"); } pub fn is_done(&self) -> bool { self.done } pub fn collect(self) -> Vec { self.entries .into_iter() .filter(|v| v.1) .map(|(id, _)| id) .collect() } fn initialize(&mut self, context: &Context) { let mut highlighted_attrs = crate::conf::value(context, "widgets.options.highlighted"); if !context.settings.terminal.use_color() { highlighted_attrs.attrs |= Attr::REVERSE; } let shortcuts = context.settings.shortcuts.general.key_values(); let navigate_help_string = format!( "Navigate options with {} to go down, {} to go up, select with {}", shortcuts["scroll_down"], shortcuts["scroll_up"], Key::Char('\n') ); let width = std::cmp::max( self.entry_titles.iter().map(|e| e.len()).max().unwrap_or(0) + 3, std::cmp::max(self.title.len(), navigate_help_string.len()) + 3, ) + 3; let height = self.entries.len() // padding + 3 // buttons row + if self.single_only { 1 } else { 5 }; if !self.content.resize_with_context(width, height, context) { self.dirty = false; return; } let inner_area = self.content.area(); let (_, y) = self.content.grid_mut().write_string( &self.title, self.theme_default.fg, self.theme_default.bg, self.theme_default.attrs | Attr::BOLD, inner_area.skip_cols(2), None, ); let y = self .content .grid_mut() .write_string( &navigate_help_string, self.theme_default.fg, self.theme_default.bg, self.theme_default.attrs | Attr::ITALICS, inner_area.skip_cols(2).skip_rows(y + 2), None, ) .1 + y + 2; let inner_area = inner_area.skip_cols(1).skip_rows(y + 2); /* Extra room for buttons Okay/Cancel */ if self.single_only { for (i, e) in self.entry_titles.iter().enumerate() { let attr = if matches!(self.cursor, SelectorCursor::Entry(e) if e == i) { highlighted_attrs } else { self.theme_default }; self.content.grid_mut().write_string( e, attr.fg, attr.bg, attr.attrs, inner_area.nth_row(i), None, ); } } else { for (i, e) in self.entry_titles.iter().enumerate() { let attr = if matches!(self.cursor, SelectorCursor::Entry(e) if e == i) { highlighted_attrs } else { self.theme_default }; self.content.grid_mut().write_string( &format!("[{}] {}", if self.entries[i].1 { "x" } else { " " }, e), attr.fg, attr.bg, attr.attrs, inner_area.nth_row(i), None, ); } let inner_area = inner_area.nth_row(self.entry_titles.len() + 2).skip_cols(2); let attr = if matches!(self.cursor, SelectorCursor::Ok) { highlighted_attrs } else { self.theme_default }; let (x, y) = self.content.grid_mut().write_string( OK, attr.fg, attr.bg, attr.attrs | Attr::BOLD, inner_area, None, ); let attr = if matches!(self.cursor, SelectorCursor::Cancel) { highlighted_attrs } else { self.theme_default }; self.content.grid_mut().write_string( CANCEL, attr.fg, attr.bg, attr.attrs, inner_area.skip(CANCEL_OFFSET + x, y), None, ); } self.initialized = true; } fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { let mut highlighted_attrs = crate::conf::value(context, "widgets.options.highlighted"); if !context.settings.terminal.use_color() { highlighted_attrs.attrs |= Attr::REVERSE; } if !self.initialized { // [ref:FIXME]: don't re-initialize when the only change is highlight index. self.initialize(context); } let (width, height) = self.content.area().size(); let dialog_area = area.align_inside( (width + 2, height + 2), self.horizontal_alignment, self.vertical_alignment, ); let inner_area = create_box(grid, dialog_area); let rows = inner_area.height(); if let Some(mvm) = self.movement.take() { match mvm { PageMovement::Up(_) | PageMovement::Down(_) => {} PageMovement::Right(amount) => { self.scroll_x_cursor = self.scroll_x_cursor.saturating_add(amount); } PageMovement::Left(amount) => { self.scroll_x_cursor = self.scroll_x_cursor.saturating_sub(amount); } PageMovement::PageUp(multiplier) => match self.cursor { SelectorCursor::Unfocused => { self.cursor = SelectorCursor::Entry(0); self.initialize(context); } SelectorCursor::Entry(c) => { self.cursor = SelectorCursor::Entry(c.saturating_sub(multiplier * rows)); self.initialize(context); } SelectorCursor::Ok | SelectorCursor::Cancel if !self.entry_titles.is_empty() => { self.cursor = SelectorCursor::Entry( self.entry_titles.len().saturating_sub(multiplier * rows), ); self.initialize(context); } SelectorCursor::Ok | SelectorCursor::Cancel => {} }, PageMovement::PageDown(multiplier) => match self.cursor { SelectorCursor::Unfocused => { self.cursor = SelectorCursor::Entry( self.entry_titles .len() .saturating_sub(1) .min(multiplier * rows), ); self.initialize(context); } SelectorCursor::Entry(c) if c.saturating_add(multiplier * rows) < self.entry_titles.len() && !self.entry_titles.is_empty() => { self.cursor = SelectorCursor::Entry( self.entry_titles .len() .saturating_sub(1) .min(c.saturating_add(multiplier * rows)), ); self.initialize(context); } SelectorCursor::Entry(_) => { self.cursor = SelectorCursor::Ok; self.initialize(context); } SelectorCursor::Ok | SelectorCursor::Cancel => {} }, PageMovement::Home if !self.entry_titles.is_empty() => { self.cursor = SelectorCursor::Entry(0); self.initialize(context); } PageMovement::End if matches!(self.cursor, SelectorCursor::Ok | SelectorCursor::Cancel) => {} PageMovement::End if !matches!(self.cursor, SelectorCursor::Entry(c) if c +1 == self.entry_titles.len()) && !self.entry_titles.is_empty() => { self.cursor = SelectorCursor::Entry(self.entry_titles.len().saturating_sub(1)); self.initialize(context); } PageMovement::Home | PageMovement::End => {} } } let skip_rows = match self.cursor { SelectorCursor::Unfocused => 0, SelectorCursor::Entry(e) if e >= rows => e.min(height.saturating_sub(rows)), SelectorCursor::Entry(_) => 0, SelectorCursor::Ok | SelectorCursor::Cancel => height.saturating_sub(rows), }; self.scroll_x_cursor = self .scroll_x_cursor .min(width.saturating_sub(inner_area.width())); grid.copy_area( self.content.grid(), inner_area, self.content .area() .skip_cols(self.scroll_x_cursor) .skip_rows(skip_rows), ); if height > dialog_area.height() { let inner_area = inner_area.skip_rows(1); ScrollBar::default().set_show_arrows(true).draw( grid, inner_area.nth_col(inner_area.width().saturating_sub(1)), context, // position skip_rows, // visible_rows inner_area.height(), // length height, ); } if width > dialog_area.width() { let inner_area = inner_area.skip_cols(1); ScrollBar::default().set_show_arrows(true).draw_horizontal( grid, inner_area.nth_row(inner_area.height().saturating_sub(1)), context, // position self.scroll_x_cursor, // visible_cols inner_area.width(), // length width, ); } context.dirty_areas.push_back(dialog_area); self.dirty = false; } } impl UIDialog { fn done(&mut self) -> Option { let Self { ref mut done_fn, ref mut entries, ref id, .. } = self; done_fn.take().and_then(|done_fn| { done_fn( *id, entries .iter() .filter(|v| v.1) .map(|(id, _)| id) .cloned() .collect::>() .as_slice(), ) }) } fn cancel(&mut self, context: &mut Context) { context.unrealized.insert(self.id()); context .replies .push_back(UIEvent::ComponentUnrealize(self.id())); } } impl UIConfirmationDialog { fn done(&mut self) -> Option { let Self { ref mut done_fn, ref mut entries, ref id, .. } = self; done_fn.take().and_then(|done_fn| { done_fn( *id, entries .iter() .filter(|v| v.1) .map(|(id, _)| id) .cloned() .any(std::convert::identity), ) }) } fn cancel(&mut self, context: &mut Context) { context.unrealized.insert(self.id()); context .replies .push_back(UIEvent::ComponentUnrealize(self.id())); } }