Skip to main content

frus_widgets/
focus.rs

1//! The **focus** wrappers: which parts of a tree the keyboard can reach, and in what
2//! order.
3//!
4//! Focus already existed here as a property of a widget — a text field is focusable, a
5//! label is not — and Tab already walked the tree. What was missing is the declarative
6//! surface the reference puts over it: the five wrappers that let a caller say *this
7//! subtree is out of reach*, *skip this one with Tab but let a click land on it*, *these
8//! come in this order*, and *resolve that order among these and nowhere else*.
9//!
10//! ```ignore
11//! FocusTraversalGroup::new(
12//!     column![
13//!         FocusTraversalOrder::new(2.0, name_field),   // second, whatever the tree says
14//!         FocusTraversalOrder::new(1.0, email_field),  // first
15//!         ExcludeFocusTraversal::new(help_button),     // clickable, not in the order
16//!     ],
17//! )
18//! ```
19//!
20//! All five are **single-child boxes** that take their child's sizing and add nothing to
21//! the picture. The flags they set are *subtree-scoped*: the walk pushes them on the way
22//! in and pops them on the way out, so a sibling never inherits one.
23//!
24//! ## The two questions Tab and a click ask separately
25//!
26//! [`ExcludeFocus`] removes a subtree from focus entirely — nothing in it registers a stop
27//! at all. [`ExcludeFocusTraversal`] keeps the stops and takes them out of *Tab's order*:
28//! a click still lands, the keyboard passes by. They are separate because the reasons are
29//! separate — a panel behind a sheet is unreachable, a toolbar button is reachable but
30//! does not belong in a form's keyboard order.
31
32use frus_core::{Rect, Scene};
33use frus_layout::Style;
34
35use crate::interaction::Status;
36use crate::theme::Theme;
37use crate::widget::{sizing_of, Widget};
38
39/// The shared body of all five: one child, its box, and a set of focus flags.
40struct Wrapper<Msg> {
41    child: Vec<Box<dyn Widget<Msg>>>,
42    focusable: bool,
43    descendants_focusable: bool,
44    skip_traversal: bool,
45    order: Option<f32>,
46    group: bool,
47}
48
49impl<Msg> Wrapper<Msg> {
50    fn new(child: Box<dyn Widget<Msg>>) -> Self {
51        Self {
52            child: vec![child],
53            focusable: false,
54            descendants_focusable: true,
55            skip_traversal: false,
56            order: None,
57            group: false,
58        }
59    }
60}
61
62impl<Msg: Clone> Widget<Msg> for Wrapper<Msg> {
63    fn style(&self) -> Style {
64        sizing_of(self.child[0].style())
65    }
66
67    fn style_themed(&self, theme: &Theme) -> Style {
68        sizing_of(self.child[0].style_themed(theme))
69    }
70
71    fn build_themed(&self, theme: &Theme) {
72        self.child[0].build_themed(theme);
73    }
74
75    fn children(&self) -> &[Box<dyn Widget<Msg>>] {
76        &self.child
77    }
78
79    fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
80
81    fn on_click(&self) -> Option<Msg> {
82        None
83    }
84
85    fn focusable(&self) -> bool {
86        self.focusable
87    }
88
89    fn descendants_focusable(&self) -> bool {
90        self.descendants_focusable
91    }
92
93    fn focus_skip_traversal(&self) -> bool {
94        self.skip_traversal
95    }
96
97    fn focus_order(&self) -> Option<f32> {
98        self.order
99    }
100
101    fn focus_group(&self) -> bool {
102        self.group
103    }
104}
105
106/// Makes its child a **focus stop**: somewhere Tab can land and a click can focus, even
107/// when the child itself has no opinion about focus.
108pub struct Focus<Msg = crate::callback::Callback>(Wrapper<Msg>);
109
110impl<Msg> Focus<Msg> {
111    /// A focus stop around `child`.
112    pub fn new(child: impl Widget<Msg> + 'static) -> Self {
113        let mut w = Wrapper::new(Box::new(child));
114        w.focusable = true;
115        Self(w)
116    }
117
118    /// Whether it can take focus at all — the reference's `canRequestFocus`. `false`
119    /// leaves the subtree in place and simply stops being a stop.
120    pub fn can_request_focus(mut self, can: bool) -> Self {
121        self.0.focusable = can;
122        self
123    }
124
125    /// Focusable by a click, passed over by Tab.
126    pub fn skip_traversal(mut self) -> Self {
127        self.0.skip_traversal = true;
128        self
129    }
130}
131
132/// **Nothing inside can take focus.** The subtree is still drawn and still measured; it
133/// is simply not somewhere the keyboard can go.
134pub struct ExcludeFocus<Msg = crate::callback::Callback>(Wrapper<Msg>);
135
136impl<Msg> ExcludeFocus<Msg> {
137    /// Puts `child` out of the keyboard's reach.
138    pub fn new(child: impl Widget<Msg> + 'static) -> Self {
139        let mut w = Wrapper::new(Box::new(child));
140        w.descendants_focusable = false;
141        Self(w)
142    }
143}
144
145/// **Skipped by Tab, reachable by a click** — a different question from
146/// [`ExcludeFocus`], which takes the subtree out of the keyboard's reach altogether. A
147/// panel behind a sheet is unreachable; a toolbar button is reachable and simply does not
148/// belong in a form's keyboard order.
149pub struct ExcludeFocusTraversal<Msg = crate::callback::Callback>(Wrapper<Msg>);
150
151impl<Msg> ExcludeFocusTraversal<Msg> {
152    /// Takes `child`'s focus stops out of Tab's order.
153    pub fn new(child: impl Widget<Msg> + 'static) -> Self {
154        let mut w = Wrapper::new(Box::new(child));
155        w.skip_traversal = true;
156        Self(w)
157    }
158}
159
160/// An explicit **traversal position** for a subtree, smallest first.
161///
162/// Ordered stops come before unordered ones, and everything without an order keeps tree
163/// order — so this is a local statement rather than a rearrangement of the frame.
164pub struct FocusTraversalOrder<Msg = crate::callback::Callback>(Wrapper<Msg>);
165
166impl<Msg> FocusTraversalOrder<Msg> {
167    /// Gives `child`'s focus stops the position `order`.
168    pub fn new(order: f32, child: impl Widget<Msg> + 'static) -> Self {
169        let mut w = Wrapper::new(Box::new(child));
170        w.order = Some(order);
171        Self(w)
172    }
173}
174
175/// A **traversal group**: an order set inside it is resolved among its own members and
176/// nowhere else, so a reordered dialog does not reshuffle the page behind it.
177pub struct FocusTraversalGroup<Msg = crate::callback::Callback>(Wrapper<Msg>);
178
179impl<Msg> FocusTraversalGroup<Msg> {
180    /// Scopes `child`'s traversal order to `child`.
181    pub fn new(child: impl Widget<Msg> + 'static) -> Self {
182        let mut w = Wrapper::new(Box::new(child));
183        w.group = true;
184        Self(w)
185    }
186}
187
188/// Each of the five is the same box with different flags; the delegation is mechanical
189/// and identical, so it is written once.
190macro_rules! delegate {
191    ($($ty:ident),* $(,)?) => {$(
192        impl<Msg: Clone> Widget<Msg> for $ty<Msg> {
193            fn style(&self) -> Style {
194                Widget::<Msg>::style(&self.0)
195            }
196            fn style_themed(&self, theme: &Theme) -> Style {
197                Widget::<Msg>::style_themed(&self.0, theme)
198            }
199            fn build_themed(&self, theme: &Theme) {
200                Widget::<Msg>::build_themed(&self.0, theme)
201            }
202            fn children(&self) -> &[Box<dyn Widget<Msg>>] {
203                Widget::<Msg>::children(&self.0)
204            }
205            fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
206                Widget::<Msg>::paint(&self.0, bounds, status, theme, scene)
207            }
208            fn on_click(&self) -> Option<Msg> {
209                Widget::<Msg>::on_click(&self.0)
210            }
211            fn focusable(&self) -> bool {
212                Widget::<Msg>::focusable(&self.0)
213            }
214            fn descendants_focusable(&self) -> bool {
215                Widget::<Msg>::descendants_focusable(&self.0)
216            }
217            fn focus_skip_traversal(&self) -> bool {
218                Widget::<Msg>::focus_skip_traversal(&self.0)
219            }
220            fn focus_order(&self) -> Option<f32> {
221                Widget::<Msg>::focus_order(&self.0)
222            }
223            fn focus_group(&self) -> bool {
224                Widget::<Msg>::focus_group(&self.0)
225            }
226        }
227    )*};
228}
229
230delegate!(
231    Focus,
232    ExcludeFocus,
233    ExcludeFocusTraversal,
234    FocusTraversalOrder,
235    FocusTraversalGroup,
236);
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241    use crate::runtime::Runtime;
242    use crate::ui::build_ui;
243    use crate::{Container, TextField};
244    use frus_core::Size;
245
246    #[derive(Clone, Debug, PartialEq)]
247    enum Msg {
248        Edited(String),
249    }
250
251    /// Three fields in a column, so there is a tree order to disturb.
252    fn field(label: &str) -> TextField<Msg> {
253        TextField::new(label.to_string())
254            .label(label.to_string())
255            .on_input(Msg::Edited)
256    }
257
258    fn order_of(root: &dyn Widget<Msg>) -> Vec<usize> {
259        let ui = build_ui(
260            root,
261            Size::new(400.0, 400.0),
262            &Runtime::default(),
263            &Theme::default(),
264        );
265        let ids: Vec<_> = ui.focusable_ids().collect();
266        ui.traversal_order()
267            .iter()
268            .map(|id| ids.iter().position(|x| x == id).expect("a known stop"))
269            .collect()
270    }
271
272    /// Untouched, Tab follows the tree.
273    #[test]
274    fn tab_follows_the_tree_by_default() {
275        let root = crate::column![field("a"), field("b"), field("c")];
276        assert_eq!(order_of(&root), vec![0, 1, 2]);
277    }
278
279    /// An order moves a subtree without moving anything else.
280    #[test]
281    fn an_order_moves_only_what_it_names() {
282        let root = crate::column![
283            FocusTraversalOrder::new(2.0, field("a")),
284            FocusTraversalOrder::new(1.0, field("b")),
285            field("c"),
286        ];
287        // b then a, both ordered; c has no order, so it comes after them.
288        assert_eq!(order_of(&root), vec![1, 0, 2]);
289    }
290
291    /// Skipped by Tab, and still a focus stop for a click.
292    #[test]
293    fn skipping_traversal_leaves_the_stop_where_it_was() {
294        let root = crate::column![
295            field("a"),
296            ExcludeFocusTraversal::new(field("b")),
297            field("c"),
298        ];
299        assert_eq!(order_of(&root), vec![0, 2], "Tab passes b by");
300
301        let ui = build_ui(
302            &root,
303            Size::new(400.0, 400.0),
304            &Runtime::default(),
305            &Theme::default(),
306        );
307        assert_eq!(
308            ui.focusable_ids().count(),
309            3,
310            "but b is still somewhere a click can land"
311        );
312    }
313
314    /// Excluded outright: not a stop at all.
315    #[test]
316    fn an_excluded_subtree_registers_nothing() {
317        let root = crate::column![field("a"), ExcludeFocus::new(field("b")), field("c")];
318        let ui = build_ui(
319            &root,
320            Size::new(400.0, 400.0),
321            &Runtime::default(),
322            &Theme::default(),
323        );
324        assert_eq!(ui.focusable_ids().count(), 2);
325        assert_eq!(order_of(&root), vec![0, 1]);
326    }
327
328    /// The flags are **subtree-scoped**: what follows an excluded subtree is not itself
329    /// excluded. This is the bug the scope exists to prevent, and the one a flag set on
330    /// the way in and never cleared would produce.
331    #[test]
332    fn the_flags_do_not_leak_to_what_comes_after() {
333        let root = crate::column![
334            ExcludeFocus::new(crate::column![field("a"), field("b")]),
335            field("c"),
336            field("d"),
337        ];
338        let ui = build_ui(
339            &root,
340            Size::new(400.0, 400.0),
341            &Runtime::default(),
342            &Theme::default(),
343        );
344        assert_eq!(ui.focusable_ids().count(), 2, "only c and d");
345    }
346
347    /// A group keeps an order to itself: the dialog reorders, the page behind it does not.
348    #[test]
349    fn a_group_resolves_its_order_among_its_own_members() {
350        let root = crate::column![
351            field("page-1"),
352            FocusTraversalGroup::new(crate::column![
353                FocusTraversalOrder::new(2.0, field("dialog-1")),
354                FocusTraversalOrder::new(1.0, field("dialog-2")),
355            ]),
356            field("page-2"),
357        ];
358        // The page keeps tree order around a dialog that swapped its two fields.
359        assert_eq!(order_of(&root), vec![0, 2, 1, 3]);
360    }
361
362    /// A `Focus` makes something focusable that had no opinion about it.
363    #[test]
364    fn it_makes_an_ordinary_box_a_focus_stop() {
365        let plain = Container::<Msg>::new().width(40.0).height(40.0);
366        assert!(!Widget::<Msg>::focusable(&plain));
367        let wrapped = Focus::new(Container::<Msg>::new().width(40.0).height(40.0));
368        assert!(Widget::<Msg>::focusable(&wrapped));
369        assert!(!Widget::<Msg>::focusable(
370            &Focus::new(Container::<Msg>::new()).can_request_focus(false)
371        ));
372    }
373}