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, layout::can_skip_scroll_reveal_from_applier};
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_with(focused, true);
32 }
33
34 fn reveal_node_in_context(&mut self, focused: NodeId) -> bool {
35 self.reveal_node_with(focused, false)
36 }
37
38 fn reveal_node_with(&mut self, focused: NodeId, remeasure_containers: bool) -> bool {
39 let mut revealed = false;
40 for index in 0..self.surfaces.len() {
41 let surface = &self.surfaces[index];
42 if surface.layout_tree.is_none() && surface.semantics_tree.is_none() {
43 self.app.flush_semantics_invalidations();
44 if let Some(root) = surface.root_node(&self.app)
45 && can_skip_scroll_reveal_from_applier(
46 &mut self.app.composition.applier_mut(),
47 root,
48 focused,
49 )
50 .unwrap_or(false)
51 {
52 continue;
53 }
54 }
55 let ancestors = self.surfaces[index]
56 .semantics_tree_for_input(&mut self.app)
57 .and_then(|tree| scroll_ancestors(tree.root(), focused))
58 .unwrap_or_default();
59 if remeasure_containers && !ancestors.is_empty() {
60 for &ancestor in &ancestors {
61 cranpose_ui::schedule_measure_repass(ancestor);
62 }
63 self.app.request_layout_pass();
64 self.run_layout_phase_in_context();
65 }
66 for ancestor in ancestors {
67 revealed |= self.reveal_within_container(index, focused, ancestor);
68 }
69 }
70 revealed
71 }
72
73 fn reveal_within_container(&mut self, index: usize, focused: NodeId, ancestor: NodeId) -> bool {
74 let mut revealed = false;
75 let mut previous_distance = f32::INFINITY;
76 loop {
77 let surface = &mut self.surfaces[index];
78 let request = surface
79 .layout_tree_in_context(&mut self.app)
80 .and_then(|tree| {
81 Some((
82 bounds_of(tree.root(), focused)?,
83 bounds_of(tree.root(), ancestor)?,
84 ))
85 })
86 .and_then(|(target, viewport)| {
87 let tree = surface.semantics_tree_for_input(&mut self.app)?;
88 scroll_request(semantics_of(tree.root(), ancestor)?, target, viewport)
89 });
90 let Some((action, dx, dy)) = request else {
91 break;
92 };
93 let distance = dx.abs() + dy.abs();
94 if distance >= previous_distance {
95 break;
96 }
97 previous_distance = distance;
98 if !run_in_mutable_snapshot(|| action.invoke(dx, dy)).unwrap_or(false) {
99 break;
100 }
101 revealed = true;
102 self.app.request_layout_pass();
103 self.run_layout_phase_in_context();
104 }
105 revealed
106 }
107}
108
109fn scroll_ancestors(node: &SemanticsNode, target: NodeId) -> Option<Vec<NodeId>> {
110 if node.hidden || !node.enabled {
111 return None;
112 }
113 if node.node_id == target {
114 return Some(Vec::new());
115 }
116 let mut path = node
117 .children
118 .iter()
119 .find_map(|node| scroll_ancestors(node, target))?;
120 if node.details().scroll_by.is_some() {
121 path.push(node.node_id);
122 }
123 Some(path)
124}
125
126fn semantics_of(node: &SemanticsNode, target: NodeId) -> Option<&SemanticsNode> {
127 if node.node_id == target {
128 return Some(node);
129 }
130 node.children
131 .iter()
132 .find_map(|node| semantics_of(node, target))
133}
134
135fn bounds_of(node: &LayoutBox, target: NodeId) -> Option<Rect> {
136 if node.node_id == target {
137 return Some(node.rect);
138 }
139 node.children
140 .iter()
141 .find_map(|node| bounds_of(node, target))
142}
143
144fn scroll_request(
145 node: &SemanticsNode,
146 target: Rect,
147 viewport: Rect,
148) -> Option<(SemanticsScrollBy, f32, f32)> {
149 let details = node.details();
150 let action = details.scroll_by.clone()?;
151 let dx = axis_delta(
152 target.x,
153 target.width,
154 viewport.x,
155 viewport.width,
156 details.horizontal_scroll,
157 );
158 let dy = axis_delta(
159 target.y,
160 target.height,
161 viewport.y,
162 viewport.height,
163 details.vertical_scroll,
164 );
165 (dx != 0.0 || dy != 0.0).then_some((action, dx, dy))
166}
167
168fn axis_delta(
169 start: f32,
170 size: f32,
171 viewport_start: f32,
172 viewport_size: f32,
173 range: Option<ScrollAxisRange>,
174) -> f32 {
175 let Some(range) = range else { return 0.0 };
176 if ![start, size, viewport_start, viewport_size]
177 .iter()
178 .all(|value| value.is_finite())
179 || size <= 0.0
180 || viewport_size <= 0.0
181 {
182 return 0.0;
183 }
184 let inset_start = range.content_padding_start.max(0.0);
185 let inset_end = range.content_padding_end.max(0.0);
186 let (viewport_start, viewport_size) = if viewport_size - inset_start - inset_end >= size {
187 (
188 viewport_start + inset_start,
189 viewport_size - inset_start - inset_end,
190 )
191 } else {
192 (viewport_start, viewport_size)
193 };
194 let visible_end = viewport_start + viewport_size;
195 let delta = if start < viewport_start && start + size < visible_end {
196 (start - viewport_start).max(start + size - visible_end)
197 } else if start + size > visible_end && start > viewport_start {
198 (start + size - visible_end).min(start - viewport_start)
199 } else {
200 0.0
201 };
202 let delta = if range.reverse { -delta } else { delta };
203 if (delta > 0.0 && range.can_scroll_forward()) || (delta < 0.0 && range.can_scroll_backward()) {
204 delta
205 } else {
206 0.0
207 }
208}
209
210#[cfg(test)]
211#[path = "tests/focus_reveal_tests.rs"]
212mod tests;