From 3e1eefd2db62b36e6fd495c271c6780f93115d01 Mon Sep 17 00:00:00 2001 From: Alexandre Bury Date: Thu, 24 Jan 2019 11:47:22 -0800 Subject: [PATCH] Add CircularFocus view Can be used to have focus wrap around when pressing Tab or Arrow keys. --- src/views/circular_focus.rs | 124 ++++++++++++++++++++++++++++++++++++ src/views/dialog.rs | 118 ++++++++++++++++++++-------------- src/views/mod.rs | 2 + 3 files changed, 197 insertions(+), 47 deletions(-) create mode 100644 src/views/circular_focus.rs diff --git a/src/views/circular_focus.rs b/src/views/circular_focus.rs new file mode 100644 index 0000000..c7e9329 --- /dev/null +++ b/src/views/circular_focus.rs @@ -0,0 +1,124 @@ +use direction::Direction; +use event::{Event, EventResult, Key}; +use view::{View, ViewWrapper}; + +/// Adds circular focus to a wrapped view. +/// +/// Wrap a view in `CircularFocus` to enable wrap-around focus +/// (when the focus exits this view, it will come back the other side). +/// +/// It can be configured to wrap Tab (and Shift+Tab) keys, and/or Arrow keys. +pub struct CircularFocus { + view: T, + wrap_tab: bool, + wrap_arrows: bool, +} + +impl CircularFocus { + /// Creates a new `CircularFocus` around the given view. + /// + /// If `wrap_tab` is true, Tab keys will cause focus to wrap around. + /// If `wrap_arrows` is true, Arrow keys will cause focus to wrap around. + pub fn new(view: T, wrap_tab: bool, wrap_arrows: bool) -> Self { + CircularFocus { + view, + wrap_tab, + wrap_arrows, + } + } + + /// Creates a new `CircularFocus` view which will wrap around Tab-based + /// focus changes. + /// + /// Whenever `Tab` would leave focus from this view, the focus will be + /// brought back to the beginning of the view. + pub fn wrap_tab(view: T) -> Self { + CircularFocus::new(view, true, false) + } + + /// Creates a new `CircularFocus` view which will wrap around Tab-based + /// focus changes. + /// + /// Whenever an arrow key + pub fn wrap_arrows(view: T) -> Self { + CircularFocus::new(view, false, true) + } + + /// Returns `true` if Tab key cause focus to wrap around. + pub fn wraps_tab(&self) -> bool { + self.wrap_tab + } + + /// Returns `true` if Arrow keys cause focus to wrap around. + pub fn wraps_arrows(&self) -> bool { + self.wrap_arrows + } +} + +impl ViewWrapper for CircularFocus { + wrap_impl!(self.view: T); + + fn wrap_on_event(&mut self, event: Event) -> EventResult { + match (self.view.on_event(event.clone()), event) { + (EventResult::Ignored, Event::Key(Key::Tab)) if self.wrap_tab => { + // Focus comes back! + if self.view.take_focus(Direction::front()) { + EventResult::Consumed(None) + } else { + EventResult::Ignored + } + } + (EventResult::Ignored, Event::Shift(Key::Tab)) + if self.wrap_tab => + { + // Focus comes back! + if self.view.take_focus(Direction::back()) { + EventResult::Consumed(None) + } else { + EventResult::Ignored + } + } + (EventResult::Ignored, Event::Key(Key::Right)) + if self.wrap_arrows => + { + // Focus comes back! + if self.view.take_focus(Direction::left()) { + EventResult::Consumed(None) + } else { + EventResult::Ignored + } + } + (EventResult::Ignored, Event::Key(Key::Left)) + if self.wrap_arrows => + { + // Focus comes back! + if self.view.take_focus(Direction::right()) { + EventResult::Consumed(None) + } else { + EventResult::Ignored + } + } + (EventResult::Ignored, Event::Key(Key::Up)) + if self.wrap_arrows => + { + // Focus comes back! + if self.view.take_focus(Direction::down()) { + EventResult::Consumed(None) + } else { + EventResult::Ignored + } + } + (EventResult::Ignored, Event::Key(Key::Down)) + if self.wrap_arrows => + { + // Focus comes back! + if self.view.take_focus(Direction::up()) { + EventResult::Consumed(None) + } else { + EventResult::Ignored + } + } + (other, _) => other, + } + } +} diff --git a/src/views/dialog.rs b/src/views/dialog.rs index a4b56f4..860aa12 100644 --- a/src/views/dialog.rs +++ b/src/views/dialog.rs @@ -1,5 +1,5 @@ use align::*; -use direction::Direction; +use direction::{Absolute, Direction, Relative}; use event::{AnyCb, Event, EventResult, Key}; use rect::Rect; use std::cell::Cell; @@ -300,9 +300,7 @@ impl Dialog { EventResult::Ignored => { if !self.buttons.is_empty() { match event { - Event::Key(Key::Down) - | Event::Key(Key::Tab) - | Event::Shift(Key::Tab) => { + Event::Key(Key::Down) | Event::Key(Key::Tab) => { // Default to leftmost button when going down. self.focus = DialogFocus::Button(0); EventResult::Consumed(None) @@ -310,25 +308,7 @@ impl Dialog { _ => EventResult::Ignored, } } else { - match event { - Event::Shift(Key::Tab) => { - if self.content.take_focus(Direction::back()) { - self.focus = DialogFocus::Content; - EventResult::Consumed(None) - } else { - EventResult::Ignored - } - } - Event::Key(Key::Tab) => { - if self.content.take_focus(Direction::front()) { - self.focus = DialogFocus::Content; - EventResult::Consumed(None) - } else { - EventResult::Ignored - } - } - _ => EventResult::Ignored, - } + EventResult::Ignored } } res => res, @@ -357,7 +337,10 @@ impl Dialog { EventResult::Ignored } } - Event::Shift(Key::Tab) => { + Event::Shift(Key::Tab) + if self.focus == DialogFocus::Button(0) => + { + // If we're at the first button, jump back to the content. if self.content.take_focus(Direction::back()) { self.focus = DialogFocus::Content; EventResult::Consumed(None) @@ -365,13 +348,30 @@ impl Dialog { EventResult::Ignored } } - Event::Key(Key::Tab) => { - if self.content.take_focus(Direction::front()) { - self.focus = DialogFocus::Content; - EventResult::Consumed(None) - } else { - EventResult::Ignored + Event::Shift(Key::Tab) => { + // Otherwise, jump to the previous button. + if let DialogFocus::Button(ref mut i) = self.focus { + // This should always be the case. + *i -= 1; } + EventResult::Consumed(None) + } + Event::Key(Key::Tab) + if self.focus + == DialogFocus::Button( + self.buttons.len().saturating_sub(1), + ) => + { + // End of the line + EventResult::Ignored + } + Event::Key(Key::Tab) => { + // Otherwise, jump to the next button. + if let DialogFocus::Button(ref mut i) = self.focus { + // This should always be the case. + *i += 1; + } + EventResult::Consumed(None) } // Left and Right move to other buttons Event::Key(Key::Right) @@ -406,10 +406,11 @@ impl Dialog { if printer.size.x < overhead.horizontal() { return None; } - let mut offset = overhead.left + self - .align - .h - .get_offset(width, printer.size.x - overhead.horizontal()); + let mut offset = overhead.left + + self + .align + .h + .get_offset(width, printer.size.x - overhead.horizontal()); let overhead_bottom = self.padding.bottom + self.borders.bottom + 1; @@ -464,9 +465,10 @@ impl Dialog { return; } let spacing = 3; //minimum distance to borders - let x = spacing + self - .title_position - .get_offset(len, printer.size.x - 2 * spacing); + let x = spacing + + self + .title_position + .get_offset(len, printer.size.x - 2 * spacing); printer.with_high_border(false, |printer| { printer.print((x - 2, 0), "┤ "); printer.print((x + len, 0), " ├"); @@ -616,16 +618,38 @@ impl View for Dialog { } fn take_focus(&mut self, source: Direction) -> bool { - // Dialogs aren't meant to be used in layouts, so... - // Let's be super lazy and not even care about the focus source. - if self.content.take_focus(source) { - self.focus = DialogFocus::Content; - true - } else if !self.buttons.is_empty() { - self.focus = DialogFocus::Button(0); - true - } else { - false + // TODO: This may depend on button position relative to the content? + // + match source { + Direction::Abs(Absolute::None) + | Direction::Rel(Relative::Front) + | Direction::Abs(Absolute::Left) + | Direction::Abs(Absolute::Up) => { + // Forward focus: content, then buttons + if self.content.take_focus(source) { + self.focus = DialogFocus::Content; + true + } else if !self.buttons.is_empty() { + self.focus = DialogFocus::Button(0); + true + } else { + false + } + } + Direction::Rel(Relative::Back) + | Direction::Abs(Absolute::Right) + | Direction::Abs(Absolute::Down) => { + // Back focus: first buttons, then content + if !self.buttons.is_empty() { + self.focus = DialogFocus::Button(self.buttons.len() - 1); + true + } else if self.content.take_focus(source) { + self.focus = DialogFocus::Content; + true + } else { + false + } + } } } diff --git a/src/views/mod.rs b/src/views/mod.rs index 3248072..a6ae1ce 100644 --- a/src/views/mod.rs +++ b/src/views/mod.rs @@ -39,6 +39,7 @@ mod box_view; mod button; mod canvas; mod checkbox; +mod circular_focus; mod dialog; mod dummy; mod edit_view; @@ -70,6 +71,7 @@ pub use self::box_view::BoxView; pub use self::button::Button; pub use self::canvas::Canvas; pub use self::checkbox::Checkbox; +pub use self::circular_focus::CircularFocus; pub use self::dialog::{Dialog, DialogFocus}; pub use self::dummy::DummyView; pub use self::edit_view::EditView;