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