cranpose_app_shell/
focus_reveal.rs1use std::rc::Rc;
2
3use cranpose_core::{NodeId, run_in_mutable_snapshot};
4use cranpose_foundation::{ScrollAxisRange, SemanticsScrollBy};
5use cranpose_render_common::Renderer;
6use cranpose_ui::{LayoutBox, SemanticsNode};
7use cranpose_ui_graphics::Rect;
8
9use crate::AppShell;
10
11impl<R: Renderer> AppShell<R>
12where
13 R::Error: std::fmt::Debug,
14{
15 pub fn accessibility_reveal(&mut self, node_id: NodeId) -> bool {
20 let app_context = Rc::clone(&self.app.app_context);
21 app_context.enter(|| self.reveal_node_in_context(node_id))
22 }
23
24 pub(crate) fn reveal_new_focus(&mut self) {
25 let focused = cranpose_ui::active_focus_target();
26 if focused == self.app.revealed_focus {
27 return;
28 }
29 self.app.revealed_focus = focused;
30 let Some(focused) = focused else { return };
31 self.reveal_node_in_context(focused);
32 }
33
34 fn reveal_node_in_context(&mut self, focused: NodeId) -> bool {
35 let mut revealed = false;
36 for index in 0..self.surfaces.len() {
37 let ancestors = self.surfaces[index]
38 .semantics_tree_for_input(&mut self.app)
39 .and_then(|tree| scroll_ancestors(tree.root(), focused))
40 .unwrap_or_default();
41 for ancestor in ancestors {
42 revealed |= self.reveal_within_container(index, focused, ancestor);
43 }
44 }
45 revealed
46 }
47
48 fn reveal_within_container(&mut self, index: usize, focused: NodeId, ancestor: NodeId) -> bool {
49 let mut revealed = false;
50 let mut previous_distance = f32::INFINITY;
51 loop {
52 let surface = &mut self.surfaces[index];
53 let request = surface
54 .layout_tree_in_context(&mut self.app)
55 .and_then(|tree| {
56 Some((
57 bounds_of(tree.root(), focused)?,
58 bounds_of(tree.root(), ancestor)?,
59 ))
60 })
61 .and_then(|(target, viewport)| {
62 let tree = surface.semantics_tree_for_input(&mut self.app)?;
63 scroll_request(semantics_of(tree.root(), ancestor)?, target, viewport)
64 });
65 let Some((action, dx, dy)) = request else {
66 break;
67 };
68 let distance = dx.abs() + dy.abs();
69 if distance >= previous_distance {
70 break;
71 }
72 previous_distance = distance;
73 if !run_in_mutable_snapshot(|| action.invoke(dx, dy)).unwrap_or(false) {
74 break;
75 }
76 revealed = true;
77 self.app.request_layout_pass();
78 self.run_layout_phase_in_context();
79 }
80 revealed
81 }
82}
83
84fn scroll_ancestors(node: &SemanticsNode, target: NodeId) -> Option<Vec<NodeId>> {
85 if node.hidden || !node.enabled {
86 return None;
87 }
88 if node.node_id == target {
89 return Some(Vec::new());
90 }
91 let mut path = node
92 .children
93 .iter()
94 .find_map(|node| scroll_ancestors(node, target))?;
95 if node.scroll_by.is_some() {
96 path.push(node.node_id);
97 }
98 Some(path)
99}
100
101fn semantics_of(node: &SemanticsNode, target: NodeId) -> Option<&SemanticsNode> {
102 if node.node_id == target {
103 return Some(node);
104 }
105 node.children
106 .iter()
107 .find_map(|node| semantics_of(node, target))
108}
109
110fn bounds_of(node: &LayoutBox, target: NodeId) -> Option<Rect> {
111 if node.node_id == target {
112 return Some(node.rect);
113 }
114 node.children
115 .iter()
116 .find_map(|node| bounds_of(node, target))
117}
118
119fn scroll_request(
120 node: &SemanticsNode,
121 target: Rect,
122 viewport: Rect,
123) -> Option<(SemanticsScrollBy, f32, f32)> {
124 let action = node.scroll_by.clone()?;
125 let dx = axis_delta(
126 target.x,
127 target.width,
128 viewport.x,
129 viewport.width,
130 node.horizontal_scroll,
131 );
132 let dy = axis_delta(
133 target.y,
134 target.height,
135 viewport.y,
136 viewport.height,
137 node.vertical_scroll,
138 );
139 (dx != 0.0 || dy != 0.0).then_some((action, dx, dy))
140}
141
142fn axis_delta(
143 start: f32,
144 size: f32,
145 viewport_start: f32,
146 viewport_size: f32,
147 range: Option<ScrollAxisRange>,
148) -> f32 {
149 let Some(range) = range else { return 0.0 };
150 if ![start, size, viewport_start, viewport_size]
151 .iter()
152 .all(|value| value.is_finite())
153 || size <= 0.0
154 || viewport_size <= 0.0
155 {
156 return 0.0;
157 }
158 let visible_end = viewport_start + viewport_size;
159 let delta = if start < viewport_start && start + size < visible_end {
160 (start - viewport_start).max(start + size - visible_end)
161 } else if start + size > visible_end && start > viewport_start {
162 (start + size - visible_end).min(start - viewport_start)
163 } else {
164 0.0
165 };
166 let delta = if range.reverse { -delta } else { delta };
167 if (delta > 0.0 && range.can_scroll_forward()) || (delta < 0.0 && range.can_scroll_backward()) {
168 delta
169 } else {
170 0.0
171 }
172}
173
174#[cfg(test)]
175#[path = "tests/focus_reveal_tests.rs"]
176mod tests;