Add CircularFocus view

Can be used to have focus wrap around when pressing Tab or Arrow keys.
This commit is contained in:
Alexandre Bury 2019-01-24 11:47:22 -08:00
parent 4ac418393a
commit 3e1eefd2db
3 changed files with 197 additions and 47 deletions

124
src/views/circular_focus.rs Normal file
View File

@ -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<T: View> {
view: T,
wrap_tab: bool,
wrap_arrows: bool,
}
impl<T: View> CircularFocus<T> {
/// 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<T: View> ViewWrapper for CircularFocus<T> {
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,
}
}
}

View File

@ -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,37 +300,17 @@ 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)
}
_ => 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,
}
}
}
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;
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)
} else {
}
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,7 +406,8 @@ impl Dialog {
if printer.size.x < overhead.horizontal() {
return None;
}
let mut offset = overhead.left + self
let mut offset = overhead.left
+ self
.align
.h
.get_offset(width, printer.size.x - overhead.horizontal());
@ -464,7 +465,8 @@ impl Dialog {
return;
}
let spacing = 3; //minimum distance to borders
let x = spacing + self
let x = spacing
+ self
.title_position
.get_offset(len, printer.size.x - 2 * spacing);
printer.with_high_border(false, |printer| {
@ -616,8 +618,14 @@ 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.
// 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
@ -628,6 +636,22 @@ impl View for Dialog {
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
}
}
}
}
fn call_on_any<'a>(&mut self, selector: &Selector, callback: AnyCb<'a>) {
self.content.call_on_any(selector, callback);

View File

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