Skip to main content

densa_ui/
access_tree.rs

1//! Building a [`Ui`]'s accessibility tree and carrying out assistive technology's requests.
2
3use densa_core::Point;
4
5use crate::access::{AccessAction, AccessId, AccessNode, AccessRequest, AccessTree, Role};
6use crate::{EventCx, Handled, Overflow, PaintCx, Ui, WidgetId};
7
8/// How far a scroll request moves content, in logical pixels.
9const SCROLL_STEP: f32 = 48.0;
10
11impl Ui {
12    /// The accessibility node id of widget `id`, if it lives.
13    pub fn access_id(&self, id: WidgetId) -> Option<AccessId> {
14        self.state
15            .node(id)
16            .map(|node| AccessId::widget(node.access))
17    }
18
19    /// The accessibility tree of the shown widgets, laid out first if needed. It allocates:
20    /// build it only while assistive technology is listening.
21    pub fn access_tree(&mut self) -> AccessTree {
22        self.layout();
23        let mut nodes = Vec::new();
24        let root = self.state.root;
25        let root_id = self.access_node(root, &mut nodes);
26        let focus = match self.state.focus.filter(|f| self.state.is_shown(*f)) {
27            Some(widget) => {
28                let id = AccessId::widget(self.state.nodes[widget.slot()].access);
29                let part = nodes
30                    .iter()
31                    .find(|(i, _)| *i == id)
32                    .and_then(|(_, node)| node.focused_part())
33                    .map(|part| id.part(part));
34                // A focused part the widget did not add has no node: the widget has focus.
35                part.filter(|p| nodes.iter().any(|(i, _)| i == p))
36                    .unwrap_or(id)
37            }
38            None => root_id,
39        };
40        AccessTree {
41            nodes,
42            root: root_id,
43            focus,
44        }
45    }
46
47    /// Adds widget `id`'s node, its parts and its shown children to `out`; returns its id.
48    fn access_node(&self, id: WidgetId, out: &mut Vec<(AccessId, AccessNode)>) -> AccessId {
49        let state = &self.state.nodes[id.slot()];
50        let node_id = AccessId::widget(state.access);
51        let role = if id == self.state.root {
52            Role::Window
53        } else {
54            Role::Group
55        };
56        let mut node = AccessNode::new(role);
57        node.set_bounds(state.rect);
58        node.set_clips_children(state.style.overflow != Overflow::Visible);
59        if let Some(widget) = self.widgets[id.slot()].as_ref() {
60            widget.accessibility(
61                &PaintCx {
62                    state: &self.state,
63                    id,
64                },
65                &mut node,
66            );
67        }
68        if self.accepts_focus(id) {
69            node.add_action(AccessAction::Focus);
70        }
71        node.add_action(AccessAction::ScrollIntoView);
72        let at = out.len();
73        out.push((node_id, AccessNode::default()));
74        let mut seen = Vec::new();
75        for (part, sub) in node.take_parts() {
76            if let Some(child) = Self::access_part(node_id, part, sub, &mut seen, out) {
77                node.push_child(child);
78            }
79        }
80        for index in 0..self.state.nodes[id.slot()].children.len() {
81            let child = self.state.nodes[id.slot()].children[index];
82            if !self.state.nodes[child.slot()].style.hidden {
83                let child = self.access_node(child, out);
84                node.push_child(child);
85            }
86        }
87        out[at].1 = node;
88        node_id
89    }
90
91    /// Adds part `part` of the widget with node `widget`, and the parts inside it, to `out`;
92    /// `None` (nothing added) for a number past [`MAX_PART`](crate::MAX_PART) or one the
93    /// widget already used (`seen`), which would give two nodes one id.
94    fn access_part(
95        widget: AccessId,
96        part: u32,
97        mut node: AccessNode,
98        seen: &mut Vec<u32>,
99        out: &mut Vec<(AccessId, AccessNode)>,
100    ) -> Option<AccessId> {
101        if part > crate::MAX_PART || seen.contains(&part) {
102            return None;
103        }
104        seen.push(part);
105        let node_id = widget.part(part);
106        let at = out.len();
107        out.push((node_id, AccessNode::default()));
108        for (inner, sub) in node.take_parts() {
109            if let Some(child) = Self::access_part(widget, inner, sub, seen, out) {
110                node.push_child(child);
111            }
112        }
113        out[at].1 = node;
114        Some(node_id)
115    }
116
117    /// Carries out assistive technology's request to do `action` to node `target`, with
118    /// `value` for [`AccessAction::SetValue`]; `false` if nothing did it: the widget is gone
119    /// or not shown (a hidden widget is never acted on), or neither it nor, for scrolling, a
120    /// parent handles it.
121    pub fn access_action(
122        &mut self,
123        target: AccessId,
124        action: AccessAction,
125        value: Option<&str>,
126    ) -> bool {
127        let (serial, part) = target.split();
128        let Some(id) = self.widget_numbered(serial) else {
129            return false;
130        };
131        self.layout();
132        let request = AccessRequest {
133            action,
134            part,
135            value,
136        };
137        let done = match action {
138            AccessAction::Focus if part.is_none() && self.accepts_focus(id) => {
139                self.set_focus(Some(id));
140                true
141            }
142            AccessAction::ScrollIntoView if part.is_none() => {
143                self.scroll_into_view(id);
144                true
145            }
146            AccessAction::ScrollUp
147            | AccessAction::ScrollDown
148            | AccessAction::ScrollLeft
149            | AccessAction::ScrollRight => self.access_scroll(id, &request),
150            _ => self.deliver_access(id, &request) == Handled::Yes,
151        };
152        self.settle();
153        done
154    }
155
156    /// The shown widget numbered `serial`.
157    fn widget_numbered(&self, serial: u64) -> Option<WidgetId> {
158        let slot = self
159            .state
160            .nodes
161            .iter()
162            .position(|node| node.access == serial)?;
163        let id = WidgetId {
164            index: u32::try_from(slot).ok()?,
165            generation: self.state.nodes[slot].generation,
166        };
167        self.state.is_shown(id).then_some(id)
168    }
169
170    fn deliver_access(&mut self, id: WidgetId, request: &AccessRequest<'_>) -> Handled {
171        let Some(widget) = self.widgets.get_mut(id.slot()).and_then(Option::as_mut) else {
172            return Handled::No;
173        };
174        let mut cx = EventCx {
175            state: &mut self.state,
176            id,
177        };
178        widget.access_action(&mut cx, request)
179    }
180
181    /// A scroll request: the widget, else the nearest scrolling widget around it.
182    fn access_scroll(&mut self, id: WidgetId, request: &AccessRequest<'_>) -> bool {
183        let mut at = Some(id);
184        while let Some(current) = at {
185            if self.deliver_access(current, request) == Handled::Yes {
186                return true;
187            }
188            let node = &self.state.nodes[current.slot()];
189            if node.style.overflow == Overflow::Scroll {
190                let step = SCROLL_STEP * self.state.scale;
191                let (dx, dy) = match request.action {
192                    AccessAction::ScrollUp => (0.0, -step),
193                    AccessAction::ScrollDown => (0.0, step),
194                    AccessAction::ScrollLeft => (-step, 0.0),
195                    _ => (step, 0.0),
196                };
197                let to = Point::new(node.scroll.x + dx, node.scroll.y + dy);
198                let before = node.scroll;
199                return self.state.scroll_to(current, to) != before;
200            }
201            at = node.parent;
202        }
203        false
204    }
205
206    /// Scrolls every scrolling widget around `id` so that `id` is in view.
207    fn scroll_into_view(&mut self, id: WidgetId) {
208        let mut at = self.state.nodes[id.slot()].parent;
209        while let Some(container) = at {
210            self.layout();
211            let target = self.state.nodes[id.slot()].rect;
212            let node = &self.state.nodes[container.slot()];
213            if node.style.overflow == Overflow::Scroll {
214                let view = node.content;
215                let shift = |start: f32, end: f32, view_start: f32, view_end: f32| {
216                    if start < view_start {
217                        start - view_start
218                    } else if end > view_end {
219                        (end - view_end).min(start - view_start)
220                    } else {
221                        0.0
222                    }
223                };
224                let dx = shift(target.left(), target.right(), view.left(), view.right());
225                let dy = shift(target.top(), target.bottom(), view.top(), view.bottom());
226                if dx != 0.0 || dy != 0.0 {
227                    let to = Point::new(node.scroll.x + dx, node.scroll.y + dy);
228                    self.state.scroll_to(container, to);
229                }
230            }
231            at = self.state.nodes[container.slot()].parent;
232        }
233    }
234}