cranpose_ui/
focus_order.rs1use std::cell::RefCell;
2
3use cranpose_core::NodeId;
4use cranpose_foundation::SemanticsWidgetRole;
5use cranpose_ui_graphics::Rect;
6
7use crate::layout::{LayoutBox, LayoutTree};
8
9#[derive(Clone, Copy, Debug, PartialEq)]
11pub struct FocusEntry {
12 pub node_id: NodeId,
13 pub rect: Rect,
14}
15
16impl FocusEntry {
17 pub fn center(&self) -> (f32, f32) {
18 (
19 self.rect.x + self.rect.width * 0.5,
20 self.rect.y + self.rect.height * 0.5,
21 )
22 }
23}
24
25thread_local! {
26 static FOCUS_ORDER: RefCell<Vec<FocusEntry>> = const { RefCell::new(Vec::new()) };
27}
28
29pub fn set_focus_order(entries: Vec<FocusEntry>) {
32 FOCUS_ORDER.with(|cell| *cell.borrow_mut() = entries);
33}
34
35pub fn with_focus_order<T>(reader: impl FnOnce(&[FocusEntry]) -> T) -> T {
37 FOCUS_ORDER.with(|cell| reader(&cell.borrow()))
38}
39
40pub fn focus_order_len() -> usize {
42 FOCUS_ORDER.with(|cell| cell.borrow().len())
43}
44
45pub fn collect_focus_order(tree: &LayoutTree) -> Vec<FocusEntry> {
48 let mut entries = Vec::new();
49 collect_from_box(tree.root(), &mut entries);
50 entries
51}
52
53fn collect_from_box(layout_box: &LayoutBox, entries: &mut Vec<FocusEntry>) {
54 if crate::focus_dispatch::has_focus_target(layout_box.node_id) && takes_space(layout_box.rect) {
55 entries.push(FocusEntry {
56 node_id: layout_box.node_id,
57 rect: layout_box.rect,
58 });
59 }
60 for child in &layout_box.children {
61 collect_from_box(child, entries);
62 }
63}
64
65fn takes_space(rect: Rect) -> bool {
66 rect.width > 0.0
67 && rect.height > 0.0
68 && rect.x.is_finite()
69 && rect.y.is_finite()
70 && rect.width.is_finite()
71 && rect.height.is_finite()
72}
73
74pub fn collect_focus_order_under(tree: &LayoutTree, node_id: NodeId) -> Vec<FocusEntry> {
77 let mut entries = Vec::new();
78 if let Some(layout_box) = find_box(tree.root(), node_id) {
79 collect_from_box(layout_box, &mut entries);
80 }
81 entries
82}
83
84pub fn selectable_group_of(tree: &LayoutTree, node_id: NodeId) -> Option<NodeId> {
87 group_above(tree.root(), node_id, None)
88}
89
90fn group_above(layout_box: &LayoutBox, node_id: NodeId, group: Option<NodeId>) -> Option<NodeId> {
91 let group = if declares_selectable_group(layout_box) {
92 Some(layout_box.node_id)
93 } else {
94 group
95 };
96 if layout_box.node_id == node_id {
97 return group;
98 }
99 layout_box
100 .children
101 .iter()
102 .find_map(|child| group_above(child, node_id, group))
103}
104
105fn declares_selectable_group(layout_box: &LayoutBox) -> bool {
106 crate::modifier::collect_semantics_from_modifier(&layout_box.node_data.modifier).is_some_and(
107 |config| config.selectable_group || config.role == Some(SemanticsWidgetRole::Menu),
108 )
109}
110
111fn find_box(layout_box: &LayoutBox, node_id: NodeId) -> Option<&LayoutBox> {
112 if layout_box.node_id == node_id {
113 return Some(layout_box);
114 }
115 layout_box
116 .children
117 .iter()
118 .find_map(|child| find_box(child, node_id))
119}