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