Another project
1use std::collections::BTreeSet;
2
3use accesskit::{Node, NodeId, Rect, Tree, TreeId, TreeUpdate};
4
5pub use accesskit::{Role, Toggled};
6
7use crate::layout::LayoutRect;
8use crate::strings::{StringKey, StringTable};
9use crate::widget_id::WidgetId;
10use crate::widgets::LabelText;
11
12#[derive(Copy, Clone, Debug, Default, PartialEq)]
13pub struct AccessState {
14 pub disabled: bool,
15 pub selected: Option<bool>,
16 pub expanded: Option<bool>,
17 pub toggled: Option<Toggled>,
18}
19
20#[derive(Copy, Clone, Debug, PartialEq)]
21pub struct AccessRange {
22 pub value: f64,
23 pub min: f64,
24 pub max: f64,
25 pub step: f64,
26}
27
28#[derive(Clone, Debug, PartialEq)]
29pub struct AccessNode {
30 pub role: Role,
31 pub label: Option<LabelText>,
32 pub description: Option<StringKey>,
33 pub state: AccessState,
34 pub range: Option<AccessRange>,
35}
36
37impl AccessNode {
38 #[must_use]
39 pub const fn new(role: Role) -> Self {
40 Self {
41 role,
42 label: None,
43 description: None,
44 state: AccessState {
45 disabled: false,
46 selected: None,
47 expanded: None,
48 toggled: None,
49 },
50 range: None,
51 }
52 }
53
54 #[must_use]
55 pub fn with_label(self, key: StringKey) -> Self {
56 self.with_label_text(LabelText::Key(key))
57 }
58
59 #[must_use]
60 pub fn with_label_text(self, label: LabelText) -> Self {
61 Self {
62 label: Some(label),
63 ..self
64 }
65 }
66
67 #[must_use]
68 pub fn with_description(self, key: StringKey) -> Self {
69 Self {
70 description: Some(key),
71 ..self
72 }
73 }
74
75 #[must_use]
76 pub fn with_disabled(self, disabled: bool) -> Self {
77 Self {
78 state: AccessState {
79 disabled,
80 ..self.state
81 },
82 ..self
83 }
84 }
85
86 #[must_use]
87 pub fn with_selected(self, selected: bool) -> Self {
88 Self {
89 state: AccessState {
90 selected: Some(selected),
91 ..self.state
92 },
93 ..self
94 }
95 }
96
97 #[must_use]
98 pub fn with_expanded(self, expanded: bool) -> Self {
99 Self {
100 state: AccessState {
101 expanded: Some(expanded),
102 ..self.state
103 },
104 ..self
105 }
106 }
107
108 #[must_use]
109 pub fn with_toggled(self, toggled: Toggled) -> Self {
110 Self {
111 state: AccessState {
112 toggled: Some(toggled),
113 ..self.state
114 },
115 ..self
116 }
117 }
118
119 #[must_use]
120 pub fn with_range(self, range: AccessRange) -> Self {
121 Self {
122 range: Some(range),
123 ..self
124 }
125 }
126}
127
128#[derive(Clone, Debug, PartialEq)]
129struct AccessEntry {
130 id: WidgetId,
131 rect: LayoutRect,
132 node: AccessNode,
133}
134
135#[derive(Clone, Debug, Default)]
136pub struct AccessTreeBuilder {
137 entries: Vec<AccessEntry>,
138 seen: BTreeSet<WidgetId>,
139}
140
141impl AccessTreeBuilder {
142 #[must_use]
143 pub fn new() -> Self {
144 Self::default()
145 }
146
147 pub fn begin_frame(&mut self) {
148 self.entries.clear();
149 self.seen.clear();
150 }
151
152 pub fn push(&mut self, id: WidgetId, rect: LayoutRect, node: AccessNode) {
153 assert!(
154 self.seen.insert(id),
155 "AccessTreeBuilder::push duplicate widget id {id:?}; pushes must be unique per frame",
156 );
157 self.entries.push(AccessEntry { id, rect, node });
158 }
159
160 #[must_use]
161 pub fn contains(&self, id: WidgetId) -> bool {
162 self.seen.contains(&id)
163 }
164
165 #[must_use]
166 pub fn len(&self) -> usize {
167 self.entries.len()
168 }
169
170 #[must_use]
171 pub fn is_empty(&self) -> bool {
172 self.entries.is_empty()
173 }
174
175 pub fn ids(&self) -> impl Iterator<Item = WidgetId> + '_ {
176 self.entries.iter().map(|e| e.id)
177 }
178
179 #[must_use]
180 pub fn build(&self, strings: &StringTable, focused: Option<WidgetId>) -> TreeUpdate {
181 let root_id = node_id(WidgetId::ROOT);
182 let mut root = Node::new(Role::Window);
183 let children: Vec<NodeId> = self.entries.iter().map(|e| node_id(e.id)).collect();
184 if !children.is_empty() {
185 root.set_children(children);
186 }
187 let nodes = std::iter::once((root_id, root))
188 .chain(
189 self.entries
190 .iter()
191 .map(|entry| (node_id(entry.id), build_node(strings, entry))),
192 )
193 .collect();
194 TreeUpdate {
195 nodes,
196 tree: Some(Tree::new(root_id)),
197 tree_id: TreeId::ROOT,
198 focus: focused.map_or(root_id, node_id),
199 }
200 }
201}
202
203fn node_id(id: WidgetId) -> NodeId {
204 NodeId::from(id.raw().get())
205}
206
207fn build_node(strings: &StringTable, entry: &AccessEntry) -> Node {
208 let mut node = Node::new(entry.node.role);
209 node.set_bounds(rect_to_accesskit(entry.rect));
210 if let Some(label) = &entry.node.label {
211 node.set_label(label.resolve(strings));
212 }
213 if let Some(key) = entry.node.description {
214 node.set_description(strings.resolve(key));
215 }
216 if entry.node.state.disabled {
217 node.set_disabled();
218 }
219 if let Some(selected) = entry.node.state.selected {
220 node.set_selected(selected);
221 }
222 if let Some(expanded) = entry.node.state.expanded {
223 node.set_expanded(expanded);
224 }
225 if let Some(toggled) = entry.node.state.toggled {
226 node.set_toggled(toggled);
227 }
228 if let Some(range) = entry.node.range {
229 node.set_numeric_value(range.value);
230 node.set_min_numeric_value(range.min);
231 node.set_max_numeric_value(range.max);
232 node.set_numeric_value_step(range.step);
233 }
234 node
235}
236
237fn rect_to_accesskit(rect: LayoutRect) -> Rect {
238 Rect {
239 x0: f64::from(rect.min_x().value()),
240 y0: f64::from(rect.min_y().value()),
241 x1: f64::from(rect.max_x().value()),
242 y1: f64::from(rect.max_y().value()),
243 }
244}
245
246#[must_use]
247pub fn root_node_id() -> NodeId {
248 node_id(WidgetId::ROOT)
249}
250
251#[must_use]
252pub fn widget_node_id(id: WidgetId) -> NodeId {
253 node_id(id)
254}
255
256#[cfg(test)]
257mod tests {
258 use accesskit::{Role, Toggled};
259
260 use super::{AccessNode, AccessRange, AccessTreeBuilder};
261 use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize};
262 use crate::strings::{StringKey, StringTable};
263 use crate::widget_id::{WidgetId, WidgetKey};
264
265 const LABEL: StringKey = StringKey::new("smoke.label");
266 const DESC: StringKey = StringKey::new("smoke.desc");
267
268 fn id(name: &'static str) -> WidgetId {
269 WidgetId::ROOT.child(WidgetKey::new(name))
270 }
271
272 fn rect() -> LayoutRect {
273 LayoutRect::new(
274 LayoutPos::new(LayoutPx::new(0.0), LayoutPx::new(0.0)),
275 LayoutSize::new(LayoutPx::new(40.0), LayoutPx::new(20.0)),
276 )
277 }
278
279 #[test]
280 #[should_panic(expected = "AccessTreeBuilder::push duplicate widget id")]
281 fn push_panics_on_duplicate_id_in_debug() {
282 let mut builder = AccessTreeBuilder::new();
283 let node = AccessNode::new(Role::Button).with_label(LABEL);
284 builder.push(id("a"), rect(), node.clone());
285 builder.push(id("a"), rect(), node);
286 }
287
288 #[test]
289 fn begin_frame_clears() {
290 let mut builder = AccessTreeBuilder::new();
291 builder.push(id("a"), rect(), AccessNode::new(Role::Button));
292 builder.begin_frame();
293 assert!(builder.is_empty());
294 assert!(!builder.contains(id("a")));
295 }
296
297 #[test]
298 fn build_emits_root_with_children() {
299 let mut builder = AccessTreeBuilder::new();
300 builder.push(
301 id("a"),
302 rect(),
303 AccessNode::new(Role::Button).with_label(LABEL),
304 );
305 builder.push(
306 id("b"),
307 rect(),
308 AccessNode::new(Role::CheckBox)
309 .with_label(LABEL)
310 .with_toggled(Toggled::True),
311 );
312 let strings = StringTable::from_entries([(LABEL, "Save".to_owned())]);
313 let update = builder.build(&strings, Some(id("b")));
314 assert_eq!(update.nodes.len(), 3);
315 assert!(update.tree.is_some());
316 let (root_id, root_node) = &update.nodes[0];
317 assert_eq!(*root_id, super::root_node_id());
318 assert_eq!(root_node.children().len(), 2);
319 assert_eq!(update.focus, super::widget_node_id(id("b")));
320 }
321
322 #[test]
323 fn build_resolves_label_through_string_table() {
324 let mut builder = AccessTreeBuilder::new();
325 builder.push(
326 id("a"),
327 rect(),
328 AccessNode::new(Role::Button)
329 .with_label(LABEL)
330 .with_description(DESC),
331 );
332 let strings = StringTable::from_entries([
333 (LABEL, "Save".to_owned()),
334 (DESC, "Persist current part".to_owned()),
335 ]);
336 let update = builder.build(&strings, None);
337 let (_, button) = &update.nodes[1];
338 assert_eq!(button.label(), Some("Save"));
339 assert_eq!(button.description(), Some("Persist current part"));
340 }
341
342 #[test]
343 fn build_emits_numeric_value_for_ranges() {
344 let mut builder = AccessTreeBuilder::new();
345 builder.push(
346 id("s"),
347 rect(),
348 AccessNode::new(Role::Slider)
349 .with_label(LABEL)
350 .with_range(AccessRange {
351 value: 5.0,
352 min: 0.0,
353 max: 10.0,
354 step: 1.0,
355 }),
356 );
357 let update = builder.build(StringTable::empty(), None);
358 let (_, slider) = &update.nodes[1];
359 assert_eq!(slider.numeric_value(), Some(5.0));
360 assert_eq!(slider.min_numeric_value(), Some(0.0));
361 assert_eq!(slider.max_numeric_value(), Some(10.0));
362 assert_eq!(slider.numeric_value_step(), Some(1.0));
363 }
364
365 #[test]
366 fn build_marks_disabled_state() {
367 let mut builder = AccessTreeBuilder::new();
368 builder.push(
369 id("a"),
370 rect(),
371 AccessNode::new(Role::Button)
372 .with_label(LABEL)
373 .with_disabled(true),
374 );
375 let update = builder.build(StringTable::empty(), None);
376 let (_, node) = &update.nodes[1];
377 assert!(node.is_disabled());
378 }
379
380 #[test]
381 fn empty_builder_emits_root_only_focus_falls_back() {
382 let builder = AccessTreeBuilder::new();
383 let update = builder.build(StringTable::empty(), None);
384 assert_eq!(update.nodes.len(), 1);
385 assert_eq!(update.focus, super::root_node_id());
386 }
387}