Skip to main content

cranpose_app_shell/
focus_reveal.rs

1use 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    /// Reveals a screen reader's target without changing keyboard focus.
16    ///
17    /// Hidden, disabled, removed and modal-background targets are rejected.
18    /// Returns whether a scroll action reports movement.
19    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;