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