cranpose_ui/
focus_order.rs1use cranpose_core::NodeId;
2use cranpose_foundation::{SemanticsConfiguration, SemanticsWidgetRole};
3use cranpose_ui_graphics::Rect;
4
5use crate::layout::{LayoutBox, LayoutTree};
6
7#[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
23pub fn set_focus_order(entries: Vec<FocusEntry>) {
27 crate::render_state::with_focus_dispatch(|state| state.set_focus_order(entries));
28}
29
30pub(crate) fn request_focus_move(direction: crate::FocusDirection) {
35 crate::render_state::with_focus_dispatch(|state| state.request_move(direction));
36}
37
38pub 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
46pub 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
51pub fn focus_order_len() -> usize {
53 with_focus_order(<[FocusEntry]>::len)
54}
55
56pub 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
92pub 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
102pub 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}