diff --git a/examples/focus-order.rs b/examples/focus-order.rs new file mode 100644 index 0000000..94776ac --- /dev/null +++ b/examples/focus-order.rs @@ -0,0 +1,84 @@ +use std::process::exit; + +use gooey::value::{Dynamic, MapEach, StringValue}; +use gooey::widget::{MakeWidget, MakeWidgetWithId, WidgetTag}; +use gooey::widgets::Expand; +use gooey::Run; +use kludgine::figures::units::Lp; + +/// This example is the same as login, but it has an explicit tab order to +/// change from the default order (username, password, cancel, log in) to +/// username, password, log in, cancel. +fn main() -> gooey::Result { + let username = Dynamic::default(); + let password = Dynamic::default(); + + let valid = + (&username, &password).map_each(|(username, password)| validate(username, password)); + + let (login_tag, login_id) = WidgetTag::new(); + let (cancel_tag, cancel_id) = WidgetTag::new(); + let (username_tag, username_id) = WidgetTag::new(); + + // TODO this should be a grid layout to ensure proper visual alignment. + let username_row = "Username" + .and( + username + .clone() + .into_input() + .make_with_id(username_tag) + .expand(), + ) + .into_columns(); + + let password_row = "Password" + .and( + // TODO secure input + password + .clone() + .into_input() + .with_next_focus(login_id) + .expand(), + ) + .into_columns(); + + let buttons = "Cancel" + .into_button() + .on_click(|_| { + eprintln!("Login cancelled"); + exit(0) + }) + .make_with_id(cancel_tag) + .into_escape() + .with_next_focus(username_id) + .and(Expand::empty()) + .and( + "Log In" + .into_button() + .enabled(valid) + .on_click(move |_| { + println!("Welcome, {}", username.get()); + exit(0); + }) + .make_with_id(login_tag) + .into_default() + .with_next_focus(cancel_id), + ) + .into_columns(); + + username_row + .pad() + .and(password_row.pad()) + .and(buttons.pad()) + .into_rows() + .contain() + .width(Lp::points(300)..Lp::points(600)) + .scroll() + .centered() + .expand() + .run() +} + +fn validate(username: &String, password: &String) -> bool { + !username.is_empty() && !password.is_empty() +} diff --git a/src/context.rs b/src/context.rs index e487a18..a7cc3ae 100644 --- a/src/context.rs +++ b/src/context.rs @@ -15,8 +15,8 @@ use kludgine::shapes::{Shape, StrokeOptions}; use kludgine::{Color, Kludgine}; use crate::graphics::Graphics; -use crate::styles::components::{HighlightColor, WidgetBackground}; -use crate::styles::{ComponentDefinition, Styles, Theme, ThemePair, VisualOrder}; +use crate::styles::components::{HighlightColor, LayoutOrder, WidgetBackground}; +use crate::styles::{ComponentDefinition, Styles, Theme, ThemePair}; use crate::utils::IgnorePoison; use crate::value::{Dynamic, IntoValue, Value}; use crate::widget::{EventHandling, ManagedWidget, WidgetId, WidgetInstance, WidgetRef}; @@ -235,10 +235,12 @@ impl<'context, 'window> EventContext<'context, 'window> { .accept_focus(&mut self.for_other(&focus)) { break Some(focus); - } else if let Some(next_focus) = focus.next_focus() { + } else if let Some(next_focus) = + focus.explicit_focus_target(self.pending_state.focus_is_advancing) + { focus = next_focus; } else { - break self.next_focus_after(focus, VisualOrder::left_to_right()); + break self.next_focus_after(focus, self.pending_state.focus_is_advancing); } }); let new = match self @@ -272,17 +274,17 @@ impl<'context, 'window> EventContext<'context, 'window> { fn next_focus_after( &mut self, mut focus: ManagedWidget, - order: VisualOrder, + advance: bool, ) -> Option { // First, look within the current focus for any focusable children. let stop_at = focus.id(); - if let Some(focus) = self.next_focus_within(&focus, None, stop_at, order) { + if let Some(focus) = self.next_focus_within(&focus, None, stop_at, advance) { return Some(focus); } // Now, look for the next widget in each hierarchy let root = loop { - if let Some(focus) = self.next_focus_sibling(&focus, stop_at, order) { + if let Some(focus) = self.next_focus_sibling(&focus, stop_at, advance) { return Some(focus); } let Some(parent) = focus.parent() else { @@ -293,16 +295,16 @@ impl<'context, 'window> EventContext<'context, 'window> { // We've exhausted a forward scan, we can now start searching the final // parent, which is the root. - self.next_focus_within(&root, None, stop_at, order) + self.next_focus_within(&root, None, stop_at, advance) } fn next_focus_sibling( &mut self, focus: &ManagedWidget, stop_at: WidgetId, - order: VisualOrder, + advance: bool, ) -> Option { - self.next_focus_within(&focus.parent()?, Some(focus.id()), stop_at, order) + self.next_focus_within(&focus.parent()?, Some(focus.id()), stop_at, advance) } /// Searches for the next focus inside of `focus`, returning `None` if @@ -313,10 +315,14 @@ impl<'context, 'window> EventContext<'context, 'window> { focus: &ManagedWidget, start_at: Option, stop_at: WidgetId, - order: VisualOrder, + advance: bool, ) -> Option { + let mut visual_order = self.get(&LayoutOrder); + if !advance { + visual_order = visual_order.rev(); + } let mut children = focus - .visually_ordered_children(order) + .visually_ordered_children(visual_order) .into_iter() .peekable(); if let Some(start_at) = start_at { @@ -340,7 +346,9 @@ impl<'context, 'window> EventContext<'context, 'window> { .accept_focus(&mut self.for_other(&child)) { return Some(child); - } else if let Some(focus) = self.next_focus_within(&child, None, stop_at, order) { + } else if let Some(next_focus) = self.widget().explicit_focus_target(advance) { + return Some(next_focus); + } else if let Some(focus) = self.next_focus_within(&child, None, stop_at, advance) { return Some(focus); } } @@ -348,14 +356,31 @@ impl<'context, 'window> EventContext<'context, 'window> { None } - /// Advances the focus from this widget to the next widget in `direction`. + /// Advances the focus to the next widget after this widget in the + /// configured focus order. /// - /// This widget does not need to be focused. - pub fn advance_focus(&mut self, direction: VisualOrder) { - // TODO check to see if the current node has an explicit next_focus (or - // if we're going in the opposite direction, previous_focus). + /// To focus in the reverse order, use [`EventContext::return_focus()`]. + pub fn advance_focus(&mut self) { + self.move_focus(true); + } - self.pending_state.focus = self.next_focus_after(self.current_node.clone(), direction); + /// Returns the focus to the previous widget before this widget in the + /// configured fous order. + /// + /// To focus in the forward order, use [`EventContext::advance_focus()`]. + pub fn return_focus(&mut self) { + self.move_focus(false); + } + + fn move_focus(&mut self, advance: bool) { + if let Some(explicit_next_focus) = self.current_node.explicit_focus_target(advance) { + self.for_other(&explicit_next_focus).focus(); + } else { + self.pending_state.focus = self.next_focus_after(self.current_node.clone(), advance); + } + // It is important to set focus-is_advancing after `focus()` because it + // sets it to `true` explicitly. + self.pending_state.focus_is_advancing = advance; } } @@ -702,6 +727,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> { .tree .active_widget() .and_then(|id| current_node.tree.widget_from_node(id)), + focus_is_advancing: false, }), effective_styles: current_node.effective_styles(), current_node, @@ -783,6 +809,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> { /// Widget events relating to focus changes are deferred until after the all /// contexts for the currently firing event are dropped. pub fn focus(&mut self) { + self.pending_state.focus_is_advancing = true; self.pending_state.focus = Some(self.current_node.clone()); } @@ -1045,6 +1072,7 @@ enum PendingState<'a> { #[derive(Default)] struct PendingWidgetState { + focus_is_advancing: bool, focus: Option, active: Option, } diff --git a/src/tree.rs b/src/tree.rs index 927e69b..3d23f82 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -56,12 +56,8 @@ impl Tree { let parent = &mut data.nodes[parent]; parent.children.push(node_id); } - if let Some(next_focus) = widget - .next_focus() - .and_then(|id| data.nodes_by_id.get(&id)) - .copied() - { - data.previous_focuses.insert(next_focus, node_id); + if let Some(next_focus) = widget.next_focus() { + data.previous_focuses.insert(next_focus, id); } ManagedWidget { node_id, @@ -252,6 +248,12 @@ impl Tree { data.update_tracked_widget(new_focus, self, |data| &mut data.focus) } + pub fn previous_focus(&self, focus: WidgetId) -> Option { + let data = self.data.lock().ignore_poison(); + let previous = *data.previous_focuses.get(&focus)?; + data.widget_from_id(previous, self) + } + pub fn activate( &self, new_active: Option<&ManagedWidget>, @@ -372,7 +374,7 @@ struct TreeData { defaults: Vec, escapes: Vec, render_order: Vec, - previous_focuses: AHashMap, + previous_focuses: AHashMap, } impl TreeData { @@ -435,17 +437,16 @@ impl TreeData { parent.children.remove(index); let mut detached_nodes = removed_node.children; - if let Some(next_focus) = removed_node - .widget - .next_focus() - .and_then(|id| self.nodes_by_id.get(&id)) - { - self.previous_focuses.remove(next_focus); + if let Some(next_focus) = removed_node.widget.next_focus() { + self.previous_focuses.remove(&next_focus); } while let Some(node) = detached_nodes.pop() { let mut node = self.nodes.remove(node).expect("detached node missing"); self.nodes_by_id.remove(&node.widget.id()); + if let Some(next_focus) = node.widget.next_focus() { + self.previous_focuses.remove(&next_focus); + } detached_nodes.append(&mut node.children); } } diff --git a/src/widget.rs b/src/widget.rs index 45cb12d..be850d9 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -1081,6 +1081,26 @@ impl ManagedWidget { .and_then(|next_focus| self.tree.widget(next_focus)) } + /// Returns the widget to focus before this widget. + /// + /// There is no direct way to set this value. This relationship is created + /// automatically using [`MakeWidget::with_next_focus()`]. + #[must_use] + pub fn previous_focus(&self) -> Option { + self.tree.previous_focus(self.id()) + } + + /// Returns the next or previous focus target, if one was set using + /// [`MakeWidget::with_next_focus()`]. + #[must_use] + pub fn explicit_focus_target(&self, advance: bool) -> Option { + if advance { + self.next_focus() + } else { + self.previous_focus() + } + } + /// Returns the region that the widget was last rendered at. #[must_use] pub fn last_layout(&self) -> Option> { diff --git a/src/window.rs b/src/window.rs index 8a0173f..a0ab113 100644 --- a/src/window.rs +++ b/src/window.rs @@ -31,7 +31,6 @@ use crate::context::{ WidgetContext, }; use crate::graphics::Graphics; -use crate::styles::components::LayoutOrder; use crate::styles::ThemePair; use crate::tree::Tree; use crate::utils::ModifiersExt; @@ -736,11 +735,11 @@ where ), kludgine, ); - let mut visual_order = target.get(&LayoutOrder); if reverse { - visual_order = visual_order.rev(); + target.return_focus(); + } else { + target.advance_focus(); } - target.advance_focus(visual_order); } } Key::Enter => {