Skip to main content

smudge_a11y/
lib.rs

1//! The bridge between the widget tree's accessibility projection and
2//! AccessKit.
3//!
4//! [`Projection`] turns what an [`App`] reports about its
5//! widgets into the [`TreeUpdate`]s an `accesskit_winit::Adapter` sends to
6//! the platform's accessibility APIs. The platform layer owns the adapter
7//! and the window; this crate owns only the mapping, so it needs no window,
8//! no event loop and no thread, and everything it does can be tested in
9//! process.
10//!
11//! Accessibility for web and other wasm targets is not supported in this
12//! release: browsers expose their own accessibility trees, and projecting
13//! into them needs a different transport.
14
15#![forbid(unsafe_code)]
16
17use std::collections::{HashMap, HashSet};
18use std::hash::{DefaultHasher, Hash, Hasher};
19
20use smudge_core::app::App;
21use smudge_core::geometry::Rect;
22use smudge_core::semantics::Role;
23use smudge_core::semantics::SemanticsNode;
24use smudge_core::tree::WidgetId;
25
26use accesskit::{Node, NodeId, Rect as AccessKitRect};
27use accesskit::{Role as AccessKitRole, TreeId, TreeInfo, TreeUpdate};
28
29/// The node id of the synthesized window root.
30///
31/// A widget id encodes a slot-map version and index that both fit in 32
32/// bits, with the null key alone using all ones for the index, so an id of
33/// all ones can never collide with a widget.
34const WINDOW_ROOT: NodeId = NodeId(u64::MAX);
35
36/// Builds the AccessKit tree updates for one app.
37///
38/// Keep one projection per window. Call [`Projection::update`] once per
39/// frame, after the frame has run, and hand the result to the adapter.
40pub struct Projection {
41    /// The window title, which names the window root.
42    title: String,
43    /// The surface size the window root's bounds were last sent with.
44    surface: (f64, f64),
45    /// Every node AccessKit holds, as it was last sent: a digest of the
46    /// node, so an incremental update leaves out a node that a frame touched
47    /// without changing anything it projects, such as a text field whose
48    /// caret moved, and its children, so a node AccessKit dropped is
49    /// forgotten and sent again when it returns.
50    sent: HashMap<NodeId, Sent>,
51    /// The roots the window root was last sent with.
52    roots: Vec<NodeId>,
53}
54
55/// What the projection last sent for one node.
56struct Sent {
57    /// The digest of the node.
58    digest: u64,
59    /// The node's children.
60    children: Vec<NodeId>,
61}
62
63impl Projection {
64    /// Creates a projection for a window with `title`.
65    pub fn new(title: impl Into<String>) -> Projection {
66        Projection {
67            title: title.into(),
68            surface: (0.0, 0.0),
69            sent: HashMap::new(),
70            roots: Vec::new(),
71        }
72    }
73
74    /// Builds the update that brings AccessKit up to date with `app`.
75    ///
76    /// `full` rebuilds every node from scratch, which the first update after
77    /// an activation and the first update after a scale change need. The
78    /// surface size is in physical pixels and `scale` is the mapping from the
79    /// logical pixels the app reports its bounds in, so the projected bounds
80    /// land where the widgets are drawn.
81    ///
82    /// The update always carries the current focus, even when nothing else
83    /// changed, and it always names a node AccessKit knows, because a tree
84    /// update must name a focused node. An incremental update leaves out a
85    /// changed node whose projection is the same as the one last sent, so a
86    /// large text value is sent again only when the text changed.
87    pub fn update(
88        &mut self,
89        app: &mut App,
90        surface_width: f64,
91        surface_height: f64,
92        scale: f32,
93        full: bool,
94    ) -> TreeUpdate {
95        let changes = app.take_a11y_changes();
96        let surface = (surface_width, surface_height);
97        let mut update = if full || changes.reset {
98            self.build_full(app, surface, scale)
99        } else {
100            let mut nodes: Vec<(NodeId, Node)> = Vec::new();
101            for node in changes
102                .changed
103                .iter()
104                .filter_map(|id| app.semantics_node(*id))
105            {
106                let id = NodeId(node.id.as_u64());
107                let digest = digest(&node, scale);
108                if self.sent.get(&id).is_some_and(|sent| sent.digest == digest) {
109                    continue;
110                }
111                let children = node_ids(&node.children);
112                let previous = self.sent.insert(
113                    id,
114                    Sent {
115                        digest,
116                        children: children.clone(),
117                    },
118                );
119                if let Some(previous) = previous {
120                    self.forget_dropped(&previous.children, &children);
121                }
122                nodes.push((id, build_node(&node, scale)));
123            }
124            if changes.roots_changed || surface != self.surface {
125                let roots = app.semantics_roots();
126                let roots = node_ids(&roots);
127                let previous = std::mem::replace(&mut self.roots, roots.clone());
128                self.forget_dropped(&previous, &roots);
129                nodes.push((WINDOW_ROOT, self.window_node(roots, surface)));
130            }
131            TreeUpdate {
132                nodes,
133                tree: None,
134                tree_id: TreeId::ROOT,
135                focus: WINDOW_ROOT,
136            }
137        };
138        self.surface = surface;
139        update.focus = current_focus(app);
140        update
141    }
142
143    /// Builds an update that carries the window root and every projected
144    /// node.
145    fn build_full(&mut self, app: &App, surface: (f64, f64), scale: f32) -> TreeUpdate {
146        let projected = app.semantics_nodes();
147        let roots: Vec<WidgetId> = projected
148            .iter()
149            .filter(|node| node.parent.is_none())
150            .map(|node| node.id)
151            .collect();
152        let mut nodes = Vec::with_capacity(projected.len() + 1);
153        let roots = node_ids(&roots);
154        self.roots = roots.clone();
155        nodes.push((WINDOW_ROOT, self.window_node(roots, surface)));
156        self.sent.clear();
157        for node in &projected {
158            let id = NodeId(node.id.as_u64());
159            let sent = Sent {
160                digest: digest(node, scale),
161                children: node_ids(&node.children),
162            };
163            self.sent.insert(id, sent);
164            nodes.push((id, build_node(node, scale)));
165        }
166        let mut tree = TreeInfo::new(WINDOW_ROOT);
167        tree.toolkit_name = Some("smudge".to_owned());
168        TreeUpdate {
169            nodes,
170            tree: Some(tree),
171            tree_id: TreeId::ROOT,
172            focus: WINDOW_ROOT,
173        }
174    }
175
176    /// Forgets every node of `previous` that `current` no longer holds,
177    /// with the nodes below it, since AccessKit drops a node no parent
178    /// holds and it has to be sent whole when it returns.
179    fn forget_dropped(&mut self, previous: &[NodeId], current: &[NodeId]) {
180        if previous == current {
181            return;
182        }
183        let current: HashSet<NodeId> = current.iter().copied().collect();
184        let mut stack: Vec<NodeId> = previous
185            .iter()
186            .filter(|id| !current.contains(id))
187            .copied()
188            .collect();
189        while let Some(id) = stack.pop() {
190            if let Some(sent) = self.sent.remove(&id) {
191                stack.extend(sent.children);
192            }
193        }
194    }
195
196    /// Builds the window root, which holds `roots` and fills the surface.
197    fn window_node(&self, roots: Vec<NodeId>, (width, height): (f64, f64)) -> Node {
198        let mut window = Node::new(AccessKitRole::Window);
199        window.set_label(self.title.clone());
200        window.set_bounds(AccessKitRect {
201            x0: 0.0,
202            y0: 0.0,
203            x1: width,
204            y1: height,
205        });
206        window.set_children(roots);
207        window
208    }
209}
210
211/// Returns a digest of everything [`build_node`] reads from `node` at
212/// `scale`, which tells a node that changed from one that did not.
213fn digest(node: &SemanticsNode, scale: f32) -> u64 {
214    let mut hasher = DefaultHasher::new();
215    node.id.as_u64().hash(&mut hasher);
216    node.parent.map(WidgetId::as_u64).hash(&mut hasher);
217    for child in &node.children {
218        child.as_u64().hash(&mut hasher);
219    }
220    map_role(node.role).hash(&mut hasher);
221    node.label.hash(&mut hasher);
222    node.value.hash(&mut hasher);
223    let bounds = node.bounds;
224    for coordinate in [bounds.x0, bounds.y0, bounds.x1, bounds.y1, scale] {
225        coordinate.to_bits().hash(&mut hasher);
226    }
227    (node.focusable, node.disabled, node.focused, node.pressable).hash(&mut hasher);
228    hasher.finish()
229}
230
231/// Maps one projected node onto an AccessKit node.
232///
233/// The bounds are scaled to physical pixels and expanded outward, so a node
234/// never reports a box smaller than the one it paints into. The node carries
235/// its children, because AccessKit replaces a node it already knows with the
236/// one an update carries.
237fn build_node(node: &SemanticsNode, scale: f32) -> Node {
238    let mut out = Node::new(map_role(node.role));
239    if let Some(label) = &node.label {
240        out.set_label(label.clone());
241    }
242    if let Some(value) = &node.value {
243        out.set_value(value.clone());
244    }
245    out.set_bounds(scaled_bounds(node.bounds, scale));
246    if !node.children.is_empty() {
247        out.set_children(node_ids(&node.children));
248    }
249    if node.disabled {
250        out.set_disabled();
251    }
252    if node.pressable {
253        out.add_action(accesskit::Action::Click);
254    }
255    if node.focusable {
256        out.add_action(accesskit::Action::Focus);
257    }
258    out
259}
260
261/// Maps widget ids onto the node ids the projection gives them.
262fn node_ids(ids: &[WidgetId]) -> Vec<NodeId> {
263    ids.iter().map(|id| NodeId(id.as_u64())).collect()
264}
265
266/// Returns the node id AccessKit should treat as focused.
267///
268/// The focused widget maps through only when it projects; a focus on a node
269/// the projection leaves out falls back to the window root, which is how
270/// AccessKit reads "no focus inside the tree".
271fn current_focus(app: &App) -> NodeId {
272    match app.focused().filter(|id| app.semantics_node(*id).is_some()) {
273        Some(id) => NodeId(id.as_u64()),
274        None => WINDOW_ROOT,
275    }
276}
277
278/// Maps a projection role onto the AccessKit role with the same meaning.
279fn map_role(role: Role) -> AccessKitRole {
280    match role {
281        Role::Window => AccessKitRole::Window,
282        Role::Group => AccessKitRole::Group,
283        Role::Button => AccessKitRole::Button,
284        Role::Label => AccessKitRole::Label,
285        Role::TextInput => AccessKitRole::TextInput,
286        Role::MultilineTextInput => AccessKitRole::MultilineTextInput,
287        Role::ScrollView => AccessKitRole::ScrollView,
288        Role::List => AccessKitRole::List,
289        Role::ListItem => AccessKitRole::ListItem,
290        // A role added after this mapping reports as a plain group.
291        _ => AccessKitRole::Group,
292    }
293}
294
295/// Scales logical bounds to physical pixels, expanding outward.
296fn scaled_bounds(bounds: Rect, scale: f32) -> AccessKitRect {
297    AccessKitRect {
298        x0: f64::from((bounds.x0 * scale).floor()),
299        y0: f64::from((bounds.y0 * scale).floor()),
300        x1: f64::from((bounds.x1 * scale).ceil()),
301        y1: f64::from((bounds.y1 * scale).ceil()),
302    }
303}