mirror of
https://github.com/danbulant/cushy
synced 2026-05-24 12:28:23 +00:00
Explicit focus order is now fully supported
This commit is contained in:
parent
89c8805924
commit
eb063c82f0
5 changed files with 168 additions and 36 deletions
84
examples/focus-order.rs
Normal file
84
examples/focus-order.rs
Normal 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()
|
||||
}
|
||||
|
|
@ -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<ManagedWidget> {
|
||||
// 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<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
|
||||
|
|
@ -313,10 +315,14 @@ impl<'context, 'window> EventContext<'context, 'window> {
|
|||
focus: &ManagedWidget,
|
||||
start_at: Option<WidgetId>,
|
||||
stop_at: WidgetId,
|
||||
order: VisualOrder,
|
||||
advance: bool,
|
||||
) -> Option<ManagedWidget> {
|
||||
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<ManagedWidget>,
|
||||
active: Option<ManagedWidget>,
|
||||
}
|
||||
|
|
|
|||
27
src/tree.rs
27
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<ManagedWidget> {
|
||||
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<LotId>,
|
||||
escapes: Vec<LotId>,
|
||||
render_order: Vec<LotId>,
|
||||
previous_focuses: AHashMap<LotId, LotId>,
|
||||
previous_focuses: AHashMap<WidgetId, WidgetId>,
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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.
|
||||
#[must_use]
|
||||
pub fn last_layout(&self) -> Option<Rect<Px>> {
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue