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