Skip to main content

cranpose_ui/
focus_order.rs

1use cranpose_core::NodeId;
2use cranpose_foundation::{SemanticsConfiguration, SemanticsWidgetRole};
3use cranpose_ui_graphics::Rect;
4
5use crate::layout::{LayoutBox, LayoutTree};
6
7/// One focus target, with the bounds layout gave it.
8#[derive(Clone, Copy, Debug, PartialEq)]
9pub struct FocusEntry {
10    pub node_id: NodeId,
11    pub rect: Rect,
12}
13
14impl FocusEntry {
15    pub fn center(&self) -> (f32, f32) {
16        (
17            self.rect.x + self.rect.width * 0.5,
18            self.rect.y + self.rect.height * 0.5,
19        )
20    }
21}
22
23/// Replaces the focus order a later [`crate::FocusManager`] move reads. The
24/// order belongs to the current [`crate::AppContext`]. Keyboard navigation
25/// publishes the order from the latest layout before choosing a target.
26pub fn set_focus_order(entries: Vec<FocusEntry>) {
27    crate::render_state::with_focus_dispatch(|state| state.set_focus_order(entries));
28}
29
30/// Asks for focus to move one step in `direction` once the event at hand is
31/// handled. A keyboard action runs inside its key event, before the app
32/// shell knows the layout's focus order; the shell publishes it and moves
33/// afterwards, as it does for Tab.
34pub(crate) fn request_focus_move(direction: crate::FocusDirection) {
35    crate::render_state::with_focus_dispatch(|state| state.request_move(direction));
36}
37
38/// Takes the move a keyboard action asked for, which the app shell makes
39/// after each key and keyboard action event.
40pub fn take_requested_focus_move() -> Option<crate::FocusDirection> {
41    crate::render_state::with_focus_dispatch(
42        crate::focus_dispatch::FocusInvalidationState::take_requested_move,
43    )
44}
45
46/// Reads the published focus order.
47pub fn with_focus_order<T>(reader: impl FnOnce(&[FocusEntry]) -> T) -> T {
48    crate::render_state::with_focus_dispatch(|state| state.with_focus_order(reader))
49}
50
51/// How many focus targets the last published order holds.
52pub fn focus_order_len() -> usize {
53    with_focus_order(<[FocusEntry]>::len)
54}
55
56/// Walks `tree` in the order the layout pass placed it and keeps the nodes
57/// that registered a focus target and take space on screen.
58pub fn collect_focus_order(tree: &LayoutTree) -> Vec<FocusEntry> {
59    let mut entries = Vec::new();
60    collect_from_box(focus_root(tree.root()), &mut entries);
61    entries
62}
63
64fn collect_from_box(layout_box: &LayoutBox, entries: &mut Vec<FocusEntry>) {
65    let config = layout_box.node_data.semantics();
66    if config.is_some_and(|config| config.hidden) {
67        return;
68    }
69    if crate::focus_dispatch::has_focus_target(layout_box.node_id)
70        && takes_space(layout_box.rect)
71        && config.is_none_or(|config| config.enabled)
72    {
73        entries.push(FocusEntry {
74            node_id: layout_box.node_id,
75            rect: layout_box.rect,
76        });
77    }
78    for child in &layout_box.children {
79        collect_from_box(child, entries);
80    }
81}
82
83pub(crate) fn takes_space(rect: Rect) -> bool {
84    rect.width > 0.0
85        && rect.height > 0.0
86        && rect.x.is_finite()
87        && rect.y.is_finite()
88        && rect.width.is_finite()
89        && rect.height.is_finite()
90}
91
92/// The focus targets under one node, in the order layout gave them. An arrow
93/// key inside a selectable group moves among these and no others.
94pub fn collect_focus_order_under(tree: &LayoutTree, node_id: NodeId) -> Vec<FocusEntry> {
95    let mut entries = Vec::new();
96    if let Some(layout_box) = find_box(focus_root(tree.root()), node_id) {
97        collect_from_box(layout_box, &mut entries);
98    }
99    entries
100}
101
102/// The nearest node above the given one that declares
103/// [`selectable_group`](crate::Modifier::selectable_group), when there is one.
104pub fn selectable_group_of(tree: &LayoutTree, node_id: NodeId) -> Option<NodeId> {
105    group_above(focus_root(tree.root()), node_id, None)
106}
107
108pub(crate) fn focus_root(root: &LayoutBox) -> &LayoutBox {
109    top_modal(root).unwrap_or(root)
110}
111
112fn top_modal(layout_box: &LayoutBox) -> Option<&LayoutBox> {
113    let config = layout_box.node_data.semantics();
114    if config.is_some_and(|config| config.hidden) {
115        return None;
116    }
117    layout_box
118        .children
119        .iter()
120        .rev()
121        .find_map(top_modal)
122        .or_else(|| {
123            (takes_space(layout_box.rect) && config.is_some_and(|config| config.is_modal))
124                .then_some(layout_box)
125        })
126}
127
128fn group_above(layout_box: &LayoutBox, node_id: NodeId, group: Option<NodeId>) -> Option<NodeId> {
129    let group = if declares_selectable_group(layout_box) {
130        Some(layout_box.node_id)
131    } else {
132        group
133    };
134    if layout_box.node_id == node_id {
135        return group;
136    }
137    layout_box
138        .children
139        .iter()
140        .find_map(|child| group_above(child, node_id, group))
141}
142
143fn declares_selectable_group(layout_box: &LayoutBox) -> bool {
144    layout_box
145        .node_data
146        .semantics()
147        .is_some_and(is_selectable_group)
148}
149
150pub(crate) fn is_selectable_group(config: &SemanticsConfiguration) -> bool {
151    config.selectable_group
152        || matches!(
153            config.role,
154            Some(
155                SemanticsWidgetRole::Menu
156                    | SemanticsWidgetRole::RadioGroup
157                    | SemanticsWidgetRole::TabBar
158            )
159        )
160}
161
162fn find_box(layout_box: &LayoutBox, node_id: NodeId) -> Option<&LayoutBox> {
163    if layout_box.node_id == node_id {
164        return Some(layout_box);
165    }
166    layout_box
167        .children
168        .iter()
169        .find_map(|child| find_box(child, node_id))
170}