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,
12 pub rect: Rect,
14}
15
16impl FocusEntry {
17 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
26pub fn set_focus_order(entries: Vec<FocusEntry>) {
30 crate::render_state::with_focus_dispatch(|state| state.set_focus_order(entries));
31}
32
33pub(crate) fn request_focus_move(direction: crate::FocusDirection) {
38 crate::render_state::with_focus_dispatch(|state| state.request_move(direction));
39}
40
41pub 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
49pub 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
54pub fn focus_order_len() -> usize {
56 with_focus_order(<[FocusEntry]>::len)
57}
58
59pub 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
95pub 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
105pub 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}