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