From 67ba3cd98dfcae81479c90213794417274bbe614 Mon Sep 17 00:00:00 2001 From: Cecile Tonglet Date: Fri, 12 Nov 2021 17:27:50 +0100 Subject: [PATCH] Add Dialog the capability to be focusable --- cursive-core/src/printer.rs | 35 ++++--- cursive-core/src/views/dialog.rs | 141 +++++++++++++++++++++++++-- cursive-core/src/views/panel.rs | 4 +- cursive/examples/focusable_dialog.rs | 76 +++++++++++++++ 4 files changed, 232 insertions(+), 24 deletions(-) create mode 100644 cursive/examples/focusable_dialog.rs diff --git a/cursive-core/src/printer.rs b/cursive-core/src/printer.rs index 38e4edb1..3e3a9397 100644 --- a/cursive-core/src/printer.rs +++ b/cursive-core/src/printer.rs @@ -398,6 +398,8 @@ impl<'a, 'b> Printer<'a, 'b> { /// If `invert` is `true`, and the theme uses `Outset` borders, then the /// box will use an "inset" style instead. /// + /// If `highlight` is `true`, then the "title" primary color will be used instead. + /// /// # Examples /// /// ```rust @@ -407,13 +409,14 @@ impl<'a, 'b> Printer<'a, 'b> { /// # let b = backend::Dummy::init(); /// # let t = theme::load_default(); /// # let printer = Printer::new((6,4), &t, &*b); - /// printer.print_box((0, 0), (6, 4), false); + /// printer.print_box((0, 0), (6, 4), false, false); /// ``` pub fn print_box, S: Into>( &self, start: T, size: S, invert: bool, + highlight: bool, ) { let start = start.into(); let size = size.into(); @@ -423,14 +426,14 @@ impl<'a, 'b> Printer<'a, 'b> { } let size = size - (1, 1); - self.with_high_border(invert, |s| { + self.with_high_border(invert, highlight, |s| { s.print(start, "┌"); s.print(start + size.keep_y(), "└"); s.print_hline(start + (1, 0), size.x - 1, "─"); s.print_vline(start + (0, 1), size.y - 1, "│"); }); - self.with_low_border(invert, |s| { + self.with_low_border(invert, highlight, |s| { s.print(start + size.keep_x(), "┐"); s.print(start + size, "┘"); s.print_hline(start + (1, 0) + size.keep_y(), size.x - 1, "─"); @@ -444,14 +447,17 @@ impl<'a, 'b> Printer<'a, 'b> { /// * If the theme's borders is "outset" and `invert` is `false`, /// use `ColorStyle::Tertiary`. /// * Otherwise, use `ColorStyle::Primary`. - pub fn with_high_border(&self, invert: bool, f: F) + pub fn with_high_border(&self, invert: bool, highlight: bool, f: F) where F: FnOnce(&Printer), { - let color = match self.theme.borders { - BorderStyle::None => return, - BorderStyle::Outset if !invert => ColorStyle::tertiary(), - _ => ColorStyle::primary(), + let color = match (self.theme.borders, highlight) { + (BorderStyle::None, true) => ColorStyle::primary(), + (BorderStyle::None, false) => return, + (BorderStyle::Outset, true) if !invert => ColorStyle::primary(), + (BorderStyle::Outset, false) if !invert => ColorStyle::tertiary(), + (_, false) => ColorStyle::primary(), + (_, true) => ColorStyle::title_primary(), }; self.with_color(color, f); @@ -463,14 +469,17 @@ impl<'a, 'b> Printer<'a, 'b> { /// * If the theme's borders is "outset" and `invert` is `true`, /// use `ColorStyle::tertiary()`. /// * Otherwise, use `ColorStyle::primary()`. - pub fn with_low_border(&self, invert: bool, f: F) + pub fn with_low_border(&self, invert: bool, highlight: bool, f: F) where F: FnOnce(&Printer), { - let color = match self.theme.borders { - BorderStyle::None => return, - BorderStyle::Outset if invert => ColorStyle::tertiary(), - _ => ColorStyle::primary(), + let color = match (self.theme.borders, highlight) { + (BorderStyle::None, true) => ColorStyle::primary(), + (BorderStyle::None, false) => return, + (BorderStyle::Outset, true) if invert => ColorStyle::primary(), + (BorderStyle::Outset, false) if invert => ColorStyle::tertiary(), + (_, false) => ColorStyle::primary(), + (_, true) => ColorStyle::title_primary(), }; self.with_color(color, f); diff --git a/cursive-core/src/views/dialog.rs b/cursive-core/src/views/dialog.rs index b00d8e61..8f1d2710 100644 --- a/cursive-core/src/views/dialog.rs +++ b/cursive-core/src/views/dialog.rs @@ -1,9 +1,9 @@ use crate::{ align::*, direction::{Absolute, Direction, Relative}, - event::{AnyCb, Event, EventResult, Key}, + event::{AnyCb, Callback, Event, EventResult, Key}, rect::Rect, - theme::ColorStyle, + theme::{BorderStyle, ColorStyle}, utils::markup::StyledString, view::{ CannotFocus, IntoBoxedView, Margins, Selector, View, ViewNotFound, @@ -18,10 +18,12 @@ use unicode_width::UnicodeWidthStr; /// Identifies currently focused element in [`Dialog`]. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum DialogFocus { - /// Content element focused + /// Content element focused. Content, - /// One of buttons focused + /// One of the buttons is focused. Button(usize), + /// The dialog itself focused. + Dialog, } struct ChildButton { @@ -78,6 +80,12 @@ pub struct Dialog { // `true` when we needs to relayout invalidated: bool, + + // Dialog can take focus itself. + dialog_focusable: bool, + + // Dialog callback when focused and Enter is pressed. + dialog_focus_callback: Option, } new_default!(Dialog); @@ -102,6 +110,8 @@ impl Dialog { borders: Margins::lrtb(1, 1, 1, 1), align: Align::top_right(), invalidated: true, + dialog_focusable: false, + dialog_focus_callback: None, } } @@ -442,6 +452,43 @@ impl Dialog { self.buttons.iter_mut().map(|b| &mut b.button.view) } + /// Set the dialog itself focusable. + pub fn focusable(self, dialog_focusable: bool) -> Self { + self.with(|s| s.set_focusable(dialog_focusable)) + } + + /// Set the dialog itself focusable. + pub fn set_focusable(&mut self, dialog_focusable: bool) { + self.dialog_focusable = dialog_focusable + } + + /// Sets the function to be called when the dialog is focused and Enter is pressed. + /// + /// Replaces the previous callback. + pub fn focus_callback(self, cb: F) -> Self + where + F: Fn(&mut Cursive) + 'static, + { + self.with(|s| s.set_focus_callback(cb)) + } + + /// Sets the function to be called when the dialog is focused and Enter is pressed. + /// + /// Replaces the previous callback. + pub fn set_focus_callback(&mut self, cb: F) + where + F: Fn(&mut Cursive) + 'static, + { + self.dialog_focus_callback = Some(Callback::from_fn(cb)); + } + + /// Sets the function to be called when the dialog is focused and Enter is pressed. + /// + /// Replaces the previous callback. + pub fn remove_focus_callback(&mut self) { + self.dialog_focus_callback = None; + } + /// Returns currently focused element pub fn focus(&self) -> DialogFocus { self.focus @@ -467,6 +514,7 @@ impl Dialog { // TODO: send Event::LostFocus? DialogFocus::Button(min(c, self.buttons.len() - 1)) } + DialogFocus::Dialog => DialogFocus::Dialog, }; } @@ -577,6 +625,59 @@ impl Dialog { } } + // An event is received while the dialog itself is focused + fn on_event_dialog(&mut self, event: Event) -> EventResult { + match event { + // Enter calls callback or goes into the content + Event::Key(Key::Enter) => { + if let Some(callback) = self.dialog_focus_callback.clone() { + EventResult::Consumed(Some(callback)) + } else if let Ok(res) = + self.content.take_focus(Direction::down()) + { + self.focus = DialogFocus::Content; + res + } else if !self.buttons.is_empty() { + self.focus = DialogFocus::Button(0); + EventResult::Consumed(None) + } else { + EventResult::Ignored + } + } + // Tab goes to the buttons + Event::Key(Key::Tab) => { + if !self.buttons.is_empty() { + self.focus = DialogFocus::Button(0); + EventResult::Consumed(None) + } else if let Ok(res) = + self.content.take_focus(Direction::down()) + { + self.focus = DialogFocus::Content; + res + } else { + EventResult::Ignored + } + } + // Tab goes to the buttons + Event::Shift(Key::Tab) => { + if !self.buttons.is_empty() { + self.focus = DialogFocus::Button( + self.buttons.len().saturating_sub(1), + ); + EventResult::Consumed(None) + } else if let Ok(res) = + self.content.take_focus(Direction::up()) + { + self.focus = DialogFocus::Content; + res + } else { + EventResult::Ignored + } + } + _ => EventResult::Ignored, + } + } + fn draw_buttons(&self, printer: &Printer) -> Option { let mut buttons_height = 0; // Current horizontal position of the next button we'll draw. @@ -656,10 +757,14 @@ impl Dialog { + self .title_position .get_offset(len, printer.size.x - spacing_both_ends); - printer.with_high_border(false, |printer| { - printer.print((x - 2, 0), "┤ "); - printer.print((x + len, 0), " ├"); - }); + printer.with_high_border( + false, + self.box_highlight(printer), + |printer| { + printer.print((x - 2, 0), "┤ "); + printer.print((x + len, 0), " ├"); + }, + ); printer.with_color(ColorStyle::title_primary(), |p| { p.print((x, 0), &self.title) @@ -708,6 +813,12 @@ impl Dialog { fn invalidate(&mut self) { self.invalidated = true; } + + fn box_highlight(&self, printer: &Printer) -> bool { + printer.focused + && (self.focus == DialogFocus::Dialog + || printer.theme.borders == BorderStyle::None) + } } impl View for Dialog { @@ -721,7 +832,12 @@ impl View for Dialog { self.draw_content(printer, buttons_height); // Print the borders - printer.print_box(Vec2::new(0, 0), printer.size, false); + printer.print_box( + Vec2::new(0, 0), + printer.size, + false, + self.box_highlight(printer), + ); self.draw_title(printer); } @@ -806,6 +922,7 @@ impl View for Dialog { DialogFocus::Content => self.on_event_content(event), // If we are on a button, we have more choice DialogFocus::Button(i) => self.on_event_button(event, i), + DialogFocus::Dialog => self.on_event_dialog(event), }) } @@ -813,6 +930,11 @@ impl View for Dialog { &mut self, source: Direction, ) -> Result { + if self.dialog_focusable { + self.focus = DialogFocus::Dialog; + return Ok(EventResult::Consumed(None)); + } + // TODO: This may depend on button position relative to the content? // match source { @@ -843,6 +965,7 @@ impl View for Dialog { } } } + (DialogFocus::Dialog, _) => unreachable!(), } } Direction::Rel(Relative::Front) diff --git a/cursive-core/src/views/panel.rs b/cursive-core/src/views/panel.rs index 2b44b325..b9499f4e 100644 --- a/cursive-core/src/views/panel.rs +++ b/cursive-core/src/views/panel.rs @@ -72,7 +72,7 @@ impl Panel { + self .title_position .get_offset(len, printer.size.x - 2 * TITLE_SPACING); - printer.with_high_border(false, |printer| { + printer.with_high_border(false, false, |printer| { printer.print((x - 2, 0), "┤ "); printer.print((x + len, 0), " ├"); }); @@ -111,7 +111,7 @@ impl ViewWrapper for Panel { } fn wrap_draw(&self, printer: &Printer) { - printer.print_box((0, 0), printer.size, true); + printer.print_box((0, 0), printer.size, true, false); self.draw_title(printer); let printer = printer.offset((1, 1)).shrinked((1, 1)); diff --git a/cursive/examples/focusable_dialog.rs b/cursive/examples/focusable_dialog.rs new file mode 100644 index 00000000..5233ea6f --- /dev/null +++ b/cursive/examples/focusable_dialog.rs @@ -0,0 +1,76 @@ +use cursive::theme::*; +use cursive::views::*; +use cursive::{ + views::{CircularFocus, Dialog, TextView}, + With as _, +}; + +fn main() { + // Creates the cursive root - required for every application. + let mut siv = cursive::default(); + + // Creates a dialog with a single "Quit" button + siv.add_layer( + // Most views can be configured in a chainable way + LinearLayout::vertical() + .child( + LinearLayout::horizontal() + .child( + Dialog::around(TextView::new("Select border theme")) + .title("Cursive") + .focusable(true) + .focus_callback(|s| { + s.update_theme(|t| match t.borders { + BorderStyle::Simple => { + t.borders = BorderStyle::Outset + } + BorderStyle::Outset => { + t.borders = BorderStyle::None + } + BorderStyle::None => { + t.borders = BorderStyle::Simple + } + }) + }) + .button("Simple", |s| { + s.update_theme(|t| { + t.borders = BorderStyle::Simple + }) + }) + .button("Outset", |s| { + s.update_theme(|t| { + t.borders = BorderStyle::Outset + }) + }) + .button("None", |s| { + s.update_theme(|t| { + t.borders = BorderStyle::None + }) + }) + .wrap_with(CircularFocus::new) + .wrap_tab(), + ) + .child( + Dialog::around(EditView::new().on_submit(|_, _| ())) + .title("Cursive") + .focusable(true) + .button("Foo", |_s| ()) + .button("Quit", |s| s.quit()) + .wrap_with(CircularFocus::new) + .wrap_tab(), + ), + ) + .child( + Dialog::around(TextView::new("Hello Dialog!")) + .title("Cursive") + .focusable(true) + .button("Foo", |_s| ()) + .button("Quit", |s| s.quit()) + .wrap_with(CircularFocus::new) + .wrap_tab(), + ), + ); + + // Starts the event loop. + siv.run(); +}