Explicit focus order is now fully supported

This commit is contained in:
Jonathan Johnson 2023-11-14 14:12:12 -08:00
parent 89c8805924
commit eb063c82f0
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
5 changed files with 168 additions and 36 deletions

84
examples/focus-order.rs Normal file
View file

@ -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()
}

View file

@ -15,8 +15,8 @@ use kludgine::shapes::{Shape, StrokeOptions};
use kludgine::{Color, Kludgine}; use kludgine::{Color, Kludgine};
use crate::graphics::Graphics; use crate::graphics::Graphics;
use crate::styles::components::{HighlightColor, WidgetBackground}; use crate::styles::components::{HighlightColor, LayoutOrder, WidgetBackground};
use crate::styles::{ComponentDefinition, Styles, Theme, ThemePair, VisualOrder}; use crate::styles::{ComponentDefinition, Styles, Theme, ThemePair};
use crate::utils::IgnorePoison; use crate::utils::IgnorePoison;
use crate::value::{Dynamic, IntoValue, Value}; use crate::value::{Dynamic, IntoValue, Value};
use crate::widget::{EventHandling, ManagedWidget, WidgetId, WidgetInstance, WidgetRef}; 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)) .accept_focus(&mut self.for_other(&focus))
{ {
break Some(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; focus = next_focus;
} else { } 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 let new = match self
@ -272,17 +274,17 @@ impl<'context, 'window> EventContext<'context, 'window> {
fn next_focus_after( fn next_focus_after(
&mut self, &mut self,
mut focus: ManagedWidget, mut focus: ManagedWidget,
order: VisualOrder, advance: bool,
) -> Option<ManagedWidget> { ) -> Option<ManagedWidget> {
// First, look within the current focus for any focusable children. // First, look within the current focus for any focusable children.
let stop_at = focus.id(); 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); return Some(focus);
} }
// Now, look for the next widget in each hierarchy // Now, look for the next widget in each hierarchy
let root = loop { 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); return Some(focus);
} }
let Some(parent) = focus.parent() else { 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 // We've exhausted a forward scan, we can now start searching the final
// parent, which is the root. // 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( fn next_focus_sibling(
&mut self, &mut self,
focus: &ManagedWidget, focus: &ManagedWidget,
stop_at: WidgetId, stop_at: WidgetId,
order: VisualOrder, advance: bool,
) -> Option<ManagedWidget> { ) -> Option<ManagedWidget> {
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 /// Searches for the next focus inside of `focus`, returning `None` if
@ -313,10 +315,14 @@ impl<'context, 'window> EventContext<'context, 'window> {
focus: &ManagedWidget, focus: &ManagedWidget,
start_at: Option<WidgetId>, start_at: Option<WidgetId>,
stop_at: WidgetId, stop_at: WidgetId,
order: VisualOrder, advance: bool,
) -> Option<ManagedWidget> { ) -> Option<ManagedWidget> {
let mut visual_order = self.get(&LayoutOrder);
if !advance {
visual_order = visual_order.rev();
}
let mut children = focus let mut children = focus
.visually_ordered_children(order) .visually_ordered_children(visual_order)
.into_iter() .into_iter()
.peekable(); .peekable();
if let Some(start_at) = start_at { if let Some(start_at) = start_at {
@ -340,7 +346,9 @@ impl<'context, 'window> EventContext<'context, 'window> {
.accept_focus(&mut self.for_other(&child)) .accept_focus(&mut self.for_other(&child))
{ {
return Some(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); return Some(focus);
} }
} }
@ -348,14 +356,31 @@ impl<'context, 'window> EventContext<'context, 'window> {
None 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. /// To focus in the reverse order, use [`EventContext::return_focus()`].
pub fn advance_focus(&mut self, direction: VisualOrder) { pub fn advance_focus(&mut self) {
// TODO check to see if the current node has an explicit next_focus (or self.move_focus(true);
// if we're going in the opposite direction, previous_focus). }
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 .tree
.active_widget() .active_widget()
.and_then(|id| current_node.tree.widget_from_node(id)), .and_then(|id| current_node.tree.widget_from_node(id)),
focus_is_advancing: false,
}), }),
effective_styles: current_node.effective_styles(), effective_styles: current_node.effective_styles(),
current_node, current_node,
@ -783,6 +809,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
/// Widget events relating to focus changes are deferred until after the all /// Widget events relating to focus changes are deferred until after the all
/// contexts for the currently firing event are dropped. /// contexts for the currently firing event are dropped.
pub fn focus(&mut self) { pub fn focus(&mut self) {
self.pending_state.focus_is_advancing = true;
self.pending_state.focus = Some(self.current_node.clone()); self.pending_state.focus = Some(self.current_node.clone());
} }
@ -1045,6 +1072,7 @@ enum PendingState<'a> {
#[derive(Default)] #[derive(Default)]
struct PendingWidgetState { struct PendingWidgetState {
focus_is_advancing: bool,
focus: Option<ManagedWidget>, focus: Option<ManagedWidget>,
active: Option<ManagedWidget>, active: Option<ManagedWidget>,
} }

View file

@ -56,12 +56,8 @@ impl Tree {
let parent = &mut data.nodes[parent]; let parent = &mut data.nodes[parent];
parent.children.push(node_id); parent.children.push(node_id);
} }
if let Some(next_focus) = widget if let Some(next_focus) = widget.next_focus() {
.next_focus() data.previous_focuses.insert(next_focus, id);
.and_then(|id| data.nodes_by_id.get(&id))
.copied()
{
data.previous_focuses.insert(next_focus, node_id);
} }
ManagedWidget { ManagedWidget {
node_id, node_id,
@ -252,6 +248,12 @@ impl Tree {
data.update_tracked_widget(new_focus, self, |data| &mut data.focus) data.update_tracked_widget(new_focus, self, |data| &mut data.focus)
} }
pub fn previous_focus(&self, focus: WidgetId) -> Option<ManagedWidget> {
let data = self.data.lock().ignore_poison();
let previous = *data.previous_focuses.get(&focus)?;
data.widget_from_id(previous, self)
}
pub fn activate( pub fn activate(
&self, &self,
new_active: Option<&ManagedWidget>, new_active: Option<&ManagedWidget>,
@ -372,7 +374,7 @@ struct TreeData {
defaults: Vec<LotId>, defaults: Vec<LotId>,
escapes: Vec<LotId>, escapes: Vec<LotId>,
render_order: Vec<LotId>, render_order: Vec<LotId>,
previous_focuses: AHashMap<LotId, LotId>, previous_focuses: AHashMap<WidgetId, WidgetId>,
} }
impl TreeData { impl TreeData {
@ -435,17 +437,16 @@ impl TreeData {
parent.children.remove(index); parent.children.remove(index);
let mut detached_nodes = removed_node.children; let mut detached_nodes = removed_node.children;
if let Some(next_focus) = removed_node if let Some(next_focus) = removed_node.widget.next_focus() {
.widget self.previous_focuses.remove(&next_focus);
.next_focus()
.and_then(|id| self.nodes_by_id.get(&id))
{
self.previous_focuses.remove(next_focus);
} }
while let Some(node) = detached_nodes.pop() { while let Some(node) = detached_nodes.pop() {
let mut node = self.nodes.remove(node).expect("detached node missing"); let mut node = self.nodes.remove(node).expect("detached node missing");
self.nodes_by_id.remove(&node.widget.id()); 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); detached_nodes.append(&mut node.children);
} }
} }

View file

@ -1081,6 +1081,26 @@ impl ManagedWidget {
.and_then(|next_focus| self.tree.widget(next_focus)) .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<ManagedWidget> {
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<ManagedWidget> {
if advance {
self.next_focus()
} else {
self.previous_focus()
}
}
/// Returns the region that the widget was last rendered at. /// Returns the region that the widget was last rendered at.
#[must_use] #[must_use]
pub fn last_layout(&self) -> Option<Rect<Px>> { pub fn last_layout(&self) -> Option<Rect<Px>> {

View file

@ -31,7 +31,6 @@ use crate::context::{
WidgetContext, WidgetContext,
}; };
use crate::graphics::Graphics; use crate::graphics::Graphics;
use crate::styles::components::LayoutOrder;
use crate::styles::ThemePair; use crate::styles::ThemePair;
use crate::tree::Tree; use crate::tree::Tree;
use crate::utils::ModifiersExt; use crate::utils::ModifiersExt;
@ -736,11 +735,11 @@ where
), ),
kludgine, kludgine,
); );
let mut visual_order = target.get(&LayoutOrder);
if reverse { if reverse {
visual_order = visual_order.rev(); target.return_focus();
} else {
target.advance_focus();
} }
target.advance_focus(visual_order);
} }
} }
Key::Enter => { Key::Enter => {