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/// Reads the published focus order.
31pub fn with_focus_order<T>(reader: impl FnOnce(&[FocusEntry]) -> T) -> T {
32    crate::render_state::with_focus_dispatch(|state| state.with_focus_order(reader))
33}
34
35/// How many focus targets the last published order holds.
36pub fn focus_order_len() -> usize {
37    with_focus_order(<[FocusEntry]>::len)
38}
39
40/// Walks `tree` in the order the layout pass placed it and keeps the nodes
41/// that registered a focus target and take space on screen.
42pub fn collect_focus_order(tree: &LayoutTree) -> Vec<FocusEntry> {
43    let mut entries = Vec::new();
44    collect_from_box(focus_root(tree.root()), &mut entries);
45    entries
46}
47
48fn collect_from_box(layout_box: &LayoutBox, entries: &mut Vec<FocusEntry>) {
49    let config = crate::modifier::collect_semantics_from_modifier(&layout_box.node_data.modifier);
50    if config.as_ref().is_some_and(|config| config.hidden) {
51        return;
52    }
53    if crate::focus_dispatch::has_focus_target(layout_box.node_id)
54        && takes_space(layout_box.rect)
55        && config.as_ref().is_none_or(|config| config.enabled)
56    {
57        entries.push(FocusEntry {
58            node_id: layout_box.node_id,
59            rect: layout_box.rect,
60        });
61    }
62    for child in &layout_box.children {
63        collect_from_box(child, entries);
64    }
65}
66
67pub(crate) fn takes_space(rect: Rect) -> bool {
68    rect.width > 0.0
69        && rect.height > 0.0
70        && rect.x.is_finite()
71        && rect.y.is_finite()
72        && rect.width.is_finite()
73        && rect.height.is_finite()
74}
75
76/// The focus targets under one node, in the order layout gave them. An arrow
77/// key inside a selectable group moves among these and no others.
78pub fn collect_focus_order_under(tree: &LayoutTree, node_id: NodeId) -> Vec<FocusEntry> {
79    let mut entries = Vec::new();
80    if let Some(layout_box) = find_box(focus_root(tree.root()), node_id) {
81        collect_from_box(layout_box, &mut entries);
82    }
83    entries
84}
85
86/// The nearest node above the given one that declares
87/// [`selectable_group`](crate::Modifier::selectable_group), when there is one.
88pub fn selectable_group_of(tree: &LayoutTree, node_id: NodeId) -> Option<NodeId> {
89    group_above(focus_root(tree.root()), node_id, None)
90}
91
92pub(crate) fn focus_root(root: &LayoutBox) -> &LayoutBox {
93    top_modal(root).unwrap_or(root)
94}
95
96fn top_modal(layout_box: &LayoutBox) -> Option<&LayoutBox> {
97    let config = crate::modifier::collect_semantics_from_modifier(&layout_box.node_data.modifier);
98    if config.as_ref().is_some_and(|config| config.hidden) {
99        return None;
100    }
101    layout_box
102        .children
103        .iter()
104        .rev()
105        .find_map(top_modal)
106        .or_else(|| {
107            (takes_space(layout_box.rect) && config.is_some_and(|config| config.is_modal))
108                .then_some(layout_box)
109        })
110}
111
112fn group_above(layout_box: &LayoutBox, node_id: NodeId, group: Option<NodeId>) -> Option<NodeId> {
113    let group = if declares_selectable_group(layout_box) {
114        Some(layout_box.node_id)
115    } else {
116        group
117    };
118    if layout_box.node_id == node_id {
119        return group;
120    }
121    layout_box
122        .children
123        .iter()
124        .find_map(|child| group_above(child, node_id, group))
125}
126
127fn declares_selectable_group(layout_box: &LayoutBox) -> bool {
128    crate::modifier::collect_semantics_from_modifier(&layout_box.node_data.modifier)
129        .as_ref()
130        .is_some_and(is_selectable_group)
131}
132
133pub(crate) fn is_selectable_group(config: &SemanticsConfiguration) -> bool {
134    config.selectable_group
135        || matches!(
136            config.role,
137            Some(
138                SemanticsWidgetRole::Menu
139                    | SemanticsWidgetRole::RadioGroup
140                    | SemanticsWidgetRole::TabBar
141            )
142        )
143}
144
145fn find_box(layout_box: &LayoutBox, node_id: NodeId) -> Option<&LayoutBox> {
146    if layout_box.node_id == node_id {
147        return Some(layout_box);
148    }
149    layout_box
150        .children
151        .iter()
152        .find_map(|child| find_box(child, node_id))
153}