kui_core/access.rs
1//! Accessibility as data: the semantic tree of a frame, derived from what
2//! nodes do and from the `role` and `label` props a view declares.
3//!
4//! A view rarely builds anything here. It sets `NodeSpec::role`,
5//! `NodeSpec::label`, `description`, `live` and the value props; the core
6//! derives an [`AccessTree`] of [`AccessNode`]s (roles from behaviour,
7//! names from labels or text, plain boxes elided) that a driver reads with
8//! [`crate::Core::access_tree`] and hands to the platform through
9//! AccessKit. Requests from assistive technology come back as
10//! [`crate::InputEvent::Access`] carrying an [`AccessRequest`] and resolve
11//! inside the core: activating a button emits the same event a click
12//! would. `Ui::announce` queues an [`Announcement`] for a one-off message
13//! with no node behind it. Headless tests assert on the tree directly.
14//!
15//! ```rust
16//! use kui_core::{AccessAction, Core, NodeSpec, Role, Size, TextStyle};
17//!
18//! let mut core = Core::new();
19//! let mut ui = core.frame(Size::new(400.0, 300.0), 1.0);
20//! ui.window_title("Demo");
21//! // An `on_click` node is a button; `label` names it when it has no text.
22//! ui.leaf_keyed("save", NodeSpec::row().size(24.0, 24.0).on_click("save").label("Save"));
23//! ui.text("Ready", TextStyle::new(14.0));
24//! ui.finish();
25//!
26//! let tree = core.access_tree();
27//! assert_eq!(tree.root().map(|n| n.role), Some(Role::Window));
28//! let button = tree.nodes.iter().find(|n| n.role == Role::Button).unwrap();
29//! assert_eq!(button.name.as_deref(), Some("Save"));
30//! assert!(button.supports(AccessAction::Click));
31//! ```
32//!
33//! Text is the one place the tree goes below the node: an editor carries
34//! its laid-out lines as [`AccessRun`]s and its caret and selection as
35//! positions in them, which is what a screen reader needs to read by
36//! character, word and line. An app that draws its own text in an
37//! `on_key` sink gets the same by declaring `role="multilineTextInput"`
38//! on the sink, `role="line"` on each line, and `caret` /
39//! `selectionAnchor` byte offsets on the lines that hold them.
40
41use std::sync::Arc;
42
43use cosmic_text::Buffer;
44use unicode_segmentation::UnicodeSegmentation;
45
46use crate::display::{Clip, NO_CLIP};
47use crate::edit::EditStore;
48use crate::geom::{Rect, Size, Vec2};
49use crate::key::Key;
50use crate::scroll::ScrollStore;
51use crate::spec::NodeSpec;
52use crate::text::TextSystem;
53use crate::tree::{NIL, NodeContent, OriginId, Tree};
54use crate::value::{Handles, Value};
55use crate::window::{WindowButton, WindowRole};
56
57/// What a node is to assistive technology. Most of these a view declares
58/// (`role` prop; `schema::ROLES` is that list, and the wire order); the
59/// ones the core derives from a node's content and behaviour instead are
60/// `schema::DERIVED_ONLY`, which says what derives each. Every variant is
61/// on one list or the other — `schema`'s
62/// `every_role_is_declarable_or_derived` fails when a new one is on
63/// neither.
64#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
65pub enum Role {
66 /// Decorative: the node and its whole subtree leave the access tree.
67 None,
68 Button,
69 Checkbox,
70 Radio,
71 Switch,
72 Slider,
73 Tab,
74 TabList,
75 Link,
76 Heading,
77 List,
78 ListItem,
79 Image,
80 Dialog,
81 Group,
82 // -- Derived, and declarable on a custom editor ------------------------
83 /// The root, named by the window title.
84 Window,
85 /// A `window="drag"` strip.
86 TitleBar,
87 /// A text node; its content is its name.
88 StaticText,
89 /// A single-line editor; the text is its value. Declared on an
90 /// `on_key` sink that draws its own text, it makes that sink one.
91 TextInput,
92 /// A multiline editor (see [`Role::TextInput`]).
93 MultilineTextInput,
94 /// A container that scrolls.
95 ScrollView,
96 /// One line of a custom editor (a `role="textInput"` sink): the text
97 /// nodes inside it, in order, are that line of the editor's value,
98 /// and its `caret` / `selectionAnchor` are byte offsets into it. Not a
99 /// node of its own.
100 Line,
101 // -- Appended later ----------------------------------------------------
102 // At the tail, and in the order [`Role::ALL`] lists them, because the
103 // tail is the only free position: `KUI_ROLE_*` is an `ALL` index plus
104 // one and the Lua and Node wires carry the `ROLES` index, so a role
105 // inserted anywhere else renumbers every role after it.
106 /// A set of `radio`s: one Tab stop, arrows moving the checked one.
107 RadioGroup,
108 /// A menu: one Tab stop, arrows moving focus without activating.
109 Menu,
110 /// One item of a `menu`. A control, so it is focusable by its role and
111 /// an unnamed one is reported.
112 MenuItem,
113 /// A cell grid (`crate::cells`): the screen of a terminal, its rows
114 /// joined as the value. Derived from the node.
115 Terminal,
116 /// A password field: an editor declared `secure`
117 /// (`EditOptions::secure`, backlog F167), its value the bullets it
118 /// draws and never its text. Derived from the node; a custom editor
119 /// that masks its own text can declare it in Rust, as
120 /// [`Role::TextInput`].
121 PasswordInput,
122}
123
124impl Role {
125 /// The camelCase spelling every binding uses.
126 pub fn name(self) -> &'static str {
127 match self {
128 Role::None => "none",
129 Role::Button => "button",
130 Role::Checkbox => "checkbox",
131 Role::Radio => "radio",
132 Role::Switch => "switch",
133 Role::Slider => "slider",
134 Role::Tab => "tab",
135 Role::TabList => "tabList",
136 Role::Link => "link",
137 Role::Heading => "heading",
138 Role::List => "list",
139 Role::ListItem => "listItem",
140 Role::Image => "image",
141 Role::Dialog => "dialog",
142 Role::Group => "group",
143 Role::RadioGroup => "radioGroup",
144 Role::Menu => "menu",
145 Role::MenuItem => "menuItem",
146 Role::Terminal => "terminal",
147 Role::PasswordInput => "passwordInput",
148 Role::Window => "window",
149 Role::TitleBar => "titleBar",
150 Role::StaticText => "staticText",
151 Role::TextInput => "textInput",
152 Role::MultilineTextInput => "multilineTextInput",
153 Role::ScrollView => "scrollView",
154 Role::Line => "line",
155 }
156 }
157
158 pub fn parse(name: &str) -> Option<Role> {
159 Role::ALL.iter().copied().find(|r| r.name() == name)
160 }
161
162 pub const ALL: [Role; 27] = [
163 Role::None,
164 Role::Button,
165 Role::Checkbox,
166 Role::Radio,
167 Role::Switch,
168 Role::Slider,
169 Role::Tab,
170 Role::TabList,
171 Role::Link,
172 Role::Heading,
173 Role::List,
174 Role::ListItem,
175 Role::Image,
176 Role::Dialog,
177 Role::Group,
178 Role::Window,
179 Role::TitleBar,
180 Role::StaticText,
181 Role::TextInput,
182 Role::MultilineTextInput,
183 Role::ScrollView,
184 Role::Line,
185 Role::RadioGroup,
186 Role::Menu,
187 Role::MenuItem,
188 Role::Terminal,
189 Role::PasswordInput,
190 ];
191
192 /// A control needs a name; one without is reported as a warning.
193 pub fn is_control(self) -> bool {
194 matches!(
195 self,
196 Role::Button
197 | Role::Checkbox
198 | Role::Radio
199 | Role::Switch
200 | Role::Slider
201 | Role::Tab
202 | Role::MenuItem
203 | Role::Link
204 | Role::TextInput
205 | Role::MultilineTextInput
206 | Role::PasswordInput
207 )
208 }
209
210 /// An editor role: carries text runs, a caret and a selection.
211 pub fn is_editor(self) -> bool {
212 matches!(
213 self,
214 Role::TextInput | Role::MultilineTextInput | Role::PasswordInput
215 )
216 }
217
218 /// Roles whose name, absent a `label`, is the text inside them (ARIA's
219 /// name-from-content), and whose children are presentational: the
220 /// subtree is read as the control, not as separate items.
221 pub(crate) fn presentational(self) -> bool {
222 matches!(
223 self,
224 Role::Button
225 | Role::Checkbox
226 | Role::Radio
227 | Role::Switch
228 | Role::Slider
229 | Role::Tab
230 | Role::MenuItem
231 | Role::Link
232 | Role::Heading
233 | Role::Image
234 )
235 }
236}
237
238/// How urgently a reader should read a change it was not asked to read:
239/// ARIA's `aria-live`, AccessKit's `Live`. Declared on the node holding
240/// the text (`live` prop) and, for a one-off with no node behind it, the
241/// politeness of an [`Announcement`].
242#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
243pub enum Live {
244 /// Not a live region: changes are read only when asked for.
245 #[default]
246 Off,
247 /// Read at the next pause, without interrupting.
248 Polite,
249 /// Read now, interrupting whatever is being said.
250 Assertive,
251}
252
253impl Live {
254 /// Every politeness, in wire order: `schema::LIVE` is `ALL` by `name`.
255 pub const ALL: &'static [Live] = &[Live::Off, Live::Polite, Live::Assertive];
256
257 /// The camelCase spelling every binding uses.
258 pub fn name(self) -> &'static str {
259 match self {
260 Live::Off => "off",
261 Live::Polite => "polite",
262 Live::Assertive => "assertive",
263 }
264 }
265
266 /// The variant `schema::LIVE` index `i` names; `Off` for an index
267 /// this build lacks.
268 pub fn from_index(i: usize) -> Live {
269 Self::ALL.get(i).copied().unwrap_or_default()
270 }
271}
272
273/// One thing to say once, with no node behind it: "Saved", "3 results".
274/// Queued by `Core::announce` and drained by `Core::take_announcements`,
275/// the way window commands, audio commands and warnings are: an
276/// announcement is an event on a timeline, and the frame's tree has no
277/// place to keep one.
278#[derive(Clone, Debug, PartialEq, Eq)]
279pub struct Announcement {
280 pub text: String,
281 /// Never [`Live::Off`]: `Core::announce` drops those rather than
282 /// queueing something no reader would say.
283 pub live: Live,
284}
285
286/// How a container arranges its items, for the platform to announce
287/// (`AXOrientation`, UIA's `Orientation`). Derived from the container's
288/// `dir` and never declared: the layout is what arranges the items, so a
289/// row that says it is a column would be a fact with two owners. It is an
290/// announcement and not a gate — the arrows move both ways whatever this
291/// says — so a container whose visual arrangement does not match its `dir`
292/// costs a less precise announcement rather than a dead keyboard.
293#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
294pub enum Orientation {
295 Horizontal,
296 Vertical,
297}
298
299impl Orientation {
300 pub fn name(self) -> &'static str {
301 match self {
302 Orientation::Horizontal => "horizontal",
303 Orientation::Vertical => "vertical",
304 }
305 }
306
307 pub fn parse(name: &str) -> Option<Orientation> {
308 Orientation::ALL.iter().copied().find(|o| o.name() == name)
309 }
310
311 pub const ALL: [Orientation; 2] = [Orientation::Horizontal, Orientation::Vertical];
312}
313
314/// What assistive technology can ask of a node. Each node advertises the
315/// subset it supports ([`AccessNode::actions`]), and a request for one
316/// arrives as [`crate::InputEvent::Access`].
317#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
318pub enum AccessAction {
319 /// Activate: the node's `on_click` payload is emitted (a window button
320 /// issues its command; an editor or key sink takes focus). On a node
321 /// behind the frame's modal it is the press outside: a `dismiss` on
322 /// the modal, nothing on the node.
323 Click,
324 /// Give the node keyboard focus — any focusable node (an editor, a key
325 /// sink, a control, a `focusable` box); it shows, as after Tab.
326 Focus,
327 Blur,
328 /// Replace an editor's text (`AccessRequest::value`); a `changed` event
329 /// follows when it differs. On a custom editor it arrives as
330 /// `{kind="access", action="setValue", text, tag}`.
331 SetValue,
332 /// Nudge a slider. The core cannot know what a step means, so these
333 /// reach the app as `{kind="access", action, tag}` on the node.
334 Increment,
335 Decrement,
336 /// Scroll the nearest scrolling ancestor so the node is visible.
337 ScrollIntoView,
338 /// Scroll a scroll view by most of a page.
339 ScrollUp,
340 ScrollDown,
341 ScrollLeft,
342 ScrollRight,
343 /// Move an editor's caret and selection (`AccessRequest::anchor` /
344 /// `focus`). On a custom editor it arrives as `{kind="access",
345 /// action="setTextSelection", anchor={line, offset}, focus={line,
346 /// offset}, tag}`.
347 SetTextSelection,
348 /// Type over an editor's selection (`AccessRequest::value`). On a
349 /// custom editor: `{kind="access", action="replaceSelectedText",
350 /// text, tag}`.
351 ReplaceSelectedText,
352}
353
354impl AccessAction {
355 pub const ALL: [AccessAction; 13] = [
356 AccessAction::Click,
357 AccessAction::Focus,
358 AccessAction::Blur,
359 AccessAction::SetValue,
360 AccessAction::Increment,
361 AccessAction::Decrement,
362 AccessAction::ScrollIntoView,
363 AccessAction::ScrollUp,
364 AccessAction::ScrollDown,
365 AccessAction::ScrollLeft,
366 AccessAction::ScrollRight,
367 AccessAction::SetTextSelection,
368 AccessAction::ReplaceSelectedText,
369 ];
370
371 /// The action's bit in [`AccessNode::actions`].
372 pub fn bit(self) -> u32 {
373 1 << (self as u32)
374 }
375
376 pub fn name(self) -> &'static str {
377 match self {
378 AccessAction::Click => "click",
379 AccessAction::Focus => "focus",
380 AccessAction::Blur => "blur",
381 AccessAction::SetValue => "setValue",
382 AccessAction::Increment => "increment",
383 AccessAction::Decrement => "decrement",
384 AccessAction::ScrollIntoView => "scrollIntoView",
385 AccessAction::ScrollUp => "scrollUp",
386 AccessAction::ScrollDown => "scrollDown",
387 AccessAction::ScrollLeft => "scrollLeft",
388 AccessAction::ScrollRight => "scrollRight",
389 AccessAction::SetTextSelection => "setTextSelection",
390 AccessAction::ReplaceSelectedText => "replaceSelectedText",
391 }
392 }
393
394 pub fn parse(name: &str) -> Option<AccessAction> {
395 AccessAction::ALL.iter().copied().find(|a| a.name() == name)
396 }
397}
398
399/// A position in an editor's text: a run and a character index into it
400/// (`character == char count` is the end of the run).
401#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
402pub struct TextPos {
403 pub run: Key,
404 pub character: usize,
405}
406
407impl TextPos {
408 /// `{run, character}`, the run spelled by `h`.
409 pub fn to_value(self, h: Handles) -> Value {
410 Value::map([
411 ("run", (h.key)(self.run)),
412 ("character", Value::Int(self.character as i64)),
413 ])
414 }
415}
416
417/// A request from assistive technology, delivered as
418/// [`crate::InputEvent::Access`].
419#[derive(Clone, Debug, PartialEq)]
420pub struct AccessRequest {
421 pub key: Key,
422 pub action: AccessAction,
423 /// The new text for [`AccessAction::SetValue`] /
424 /// [`AccessAction::ReplaceSelectedText`].
425 pub value: Option<String>,
426 /// The selection for [`AccessAction::SetTextSelection`]: `anchor` is
427 /// the end that stays put, `focus` the caret.
428 pub anchor: Option<TextPos>,
429 pub focus: Option<TextPos>,
430}
431
432impl AccessRequest {
433 pub fn new(key: Key, action: AccessAction) -> Self {
434 AccessRequest {
435 key,
436 action,
437 value: None,
438 anchor: None,
439 focus: None,
440 }
441 }
442
443 pub fn with_value(mut self, value: impl Into<String>) -> Self {
444 self.value = Some(value.into());
445 self
446 }
447
448 pub fn with_selection(mut self, anchor: TextPos, focus: TextPos) -> Self {
449 self.anchor = Some(anchor);
450 self.focus = Some(focus);
451 self
452 }
453}
454
455/// A scroll view's offsets and range (logical px).
456#[derive(Clone, Copy, Debug, Default, PartialEq)]
457pub struct ScrollState {
458 pub x: f32,
459 pub y: f32,
460 pub max_x: f32,
461 pub max_y: f32,
462}
463
464impl ScrollState {
465 /// `{x, y, max_x, max_y}`.
466 pub fn to_value(self) -> Value {
467 Value::map([
468 ("x", Value::float(self.x)),
469 ("y", Value::float(self.y)),
470 ("max_x", Value::float(self.max_x)),
471 ("max_y", Value::float(self.max_y)),
472 ])
473 }
474}
475
476/// One visual line (or a piece of one) of an editor's text, with what a
477/// screen reader needs to read it by character and word and to place a
478/// caret: every character's byte length, x position and width. A line
479/// that continues into another ends with its `"\n"`, counted as a
480/// character of zero width. Runs longer than [`RUN_CHARS`] characters are
481/// split, so indices fit the platform's byte-sized ones.
482#[derive(Clone, Debug, PartialEq)]
483pub struct AccessRun {
484 /// The run's own id (derived from the editor's key).
485 pub key: Key,
486 /// The line it belongs to: a buffer line for a built-in editor, the
487 /// ordinal of the `role="line"` node for a custom one.
488 pub line: usize,
489 /// Byte range of the run inside the line's text (the `"\n"` excluded).
490 pub start: usize,
491 pub end: usize,
492 pub text: String,
493 /// Logical px, viewport coordinates, cut to the clip the text is
494 /// drawn under as a node's `rect` is; `char_positions` still place
495 /// every character where it is drawn.
496 pub rect: Rect,
497 pub char_lengths: Vec<u8>,
498 /// Each character's x relative to `rect.x`, and its width.
499 pub char_positions: Vec<f32>,
500 pub char_widths: Vec<f32>,
501 /// Character indices where words start.
502 pub word_starts: Vec<u8>,
503 pub rtl: bool,
504}
505
506impl AccessRun {
507 /// The run as plain data, its key spelled by `h`.
508 pub fn to_value(&self, h: Handles) -> Value {
509 let bytes = |v: &[u8]| Value::list(v.iter().map(|b| Value::Int(*b as i64)));
510 Value::map([
511 ("key", (h.key)(self.key)),
512 ("line", Value::Int(self.line as i64)),
513 ("start", Value::Int(self.start as i64)),
514 ("end", Value::Int(self.end as i64)),
515 ("text", Value::Str(self.text.clone())),
516 ("rect", self.rect.to_value()),
517 ("char_lengths", bytes(&self.char_lengths)),
518 ("char_positions", Value::floats(&self.char_positions)),
519 ("char_widths", Value::floats(&self.char_widths)),
520 ("word_starts", bytes(&self.word_starts)),
521 ("rtl", Value::Bool(self.rtl)),
522 ])
523 }
524}
525
526/// Longest run, in characters (the platform indexes them in a byte).
527pub const RUN_CHARS: usize = 200;
528
529/// One semantic node of a frame.
530#[derive(Clone, Debug, PartialEq)]
531pub struct AccessNode {
532 pub key: Key,
533 /// The nearest semantic ancestor; None for the root.
534 pub parent: Option<Key>,
535 pub origin: OriginId,
536 pub role: Role,
537 /// The accessible name: `label`, else the node's own text, else (for
538 /// presentational roles) the text inside it, else the window title
539 /// for the root.
540 pub name: Option<String>,
541 /// `description` on the spec — the `description` prop, or a `tooltip`.
542 pub description: Option<String>,
543 /// Final laid-out rect, logical px, viewport coordinates, cut to the
544 /// clip the node is drawn under — the one its hit region carries
545 /// — so a reader's hover finds only what a pointer
546 /// could. A node wholly clipped away is a zero-size rect on the clip's
547 /// edge nearest it: still in the tree, still actionable, never hit.
548 pub rect: Rect,
549 /// The node's string value, which the platform has exactly one slot
550 /// for: an editor's committed text (a custom editor's: the lines it
551 /// draws, joined by `"\n"`), or a slider's declared `value_text`.
552 /// A slider that names its reading has *only* that reading — the
553 /// string wins over the number wherever both could be said, which is
554 /// what `aria-valuetext` means and what `accesskit_macos` does with
555 /// `AXValue`. `min` / `max` are unaffected,
556 /// and so are the increment actions.
557 pub value: Option<String>,
558 /// An editor's caret, a byte offset into `value`.
559 pub caret: Option<usize>,
560 /// An editor's non-empty selection as byte offsets into `value`.
561 pub selection: Option<(usize, usize)>,
562 /// An editor's laid-out text, run by run (see [`AccessRun`]).
563 pub runs: Vec<AccessRun>,
564 /// The caret (`focus`) and the other end of the selection (`anchor`,
565 /// equal to `focus` without one) as run positions.
566 pub anchor: Option<TextPos>,
567 pub focus: Option<TextPos>,
568 /// `checked` for checkbox / radio / switch roles.
569 pub checked: Option<bool>,
570 /// A checkbox that is neither on nor off:
571 /// reported as mixed whatever `checked` says.
572 pub mixed: bool,
573 /// The current one of a set: every `tab` carries it, a `listItem` or a
574 /// `link` only where the view set it (an ordinary list is not a
575 /// selection, and "not selected" on every row of one is noise).
576 /// None = the node has no such state.
577 pub selected: Option<bool>,
578 /// A disclosure's state, exactly as declared. None = it does not
579 /// expand, and a reader says nothing about it.
580 pub expanded: Option<bool>,
581 /// "3 of 7": this node's zero-based ordinal among the items of the
582 /// `list` / `tabList` holding it, with `set_size` on that container.
583 /// Derived, never declared — the core counts the semantic children it
584 /// already has (see `set_size`).
585 pub pos_in_set: Option<usize>,
586 /// How a composite container arranges its items, from its `dir` (see
587 /// [`Orientation`]). None = the node is not one of the four composite
588 /// containers, and says nothing about arrangement.
589 pub orientation: Option<Orientation>,
590 /// On a `list` / `tabList`: how many items it holds. AccessKit puts
591 /// the count on the container and the ordinal on the item, unlike
592 /// ARIA's `aria-setsize` on every item; this follows AccessKit.
593 pub set_size: Option<usize>,
594 /// `valueNow` / `valueMin` / `valueMax` for a slider. What the
595 /// position *reads as* is `valueText`, which lands in `value` above
596 /// because the platform has one string slot for both.
597 pub number: Option<f32>,
598 pub min: Option<f32>,
599 pub max: Option<f32>,
600 /// A slider's `valueStep`, where it declared one.
601 pub step: Option<f32>,
602 /// Holds keyboard focus (`Core::focus`).
603 pub focused: bool,
604 /// Declared `disabled`: inert, and not in the Tab ring.
605 pub disabled: bool,
606 /// The frame's modal surface (`aria-modal`): the Tab ring and every
607 /// pointer are confined to it, and everything else is inert. Only the
608 /// modal in effect carries it — the last one declared — so a confirm
609 /// inside a dialog leaves the dialog an ordinary node.
610 pub modal: bool,
611 pub scroll: Option<ScrollState>,
612 /// Bitset of [`AccessAction::bit`].
613 pub actions: u32,
614 /// Declared `live`: when the text inside this node changes, a reader
615 /// reads the change without being asked. Carried exactly where the
616 /// view declared it — the platform consumer inherits it down the
617 /// subtree, and duplicating that here would be a second copy of a
618 /// rule kui does not own.
619 pub live: Live,
620}
621
622impl AccessNode {
623 /// The node as plain data, every field under its snake_case name and
624 /// every key spelled by `h`. The slider's numbers are `value_now`,
625 /// `value_min`, `value_max` — the rows that set them, not the
626 /// fields that hold them; `actions` is the list of
627 /// action names, `live` and `role` and `orientation` their schema
628 /// names.
629 pub fn to_value(&self, h: Handles) -> Value {
630 Value::map([
631 ("key", (h.key)(self.key)),
632 ("parent", h.opt_key(self.parent)),
633 ("origin", Value::Int(self.origin.0 as i64)),
634 ("role", Value::str(self.role.name())),
635 ("name", Value::opt_str(&self.name)),
636 ("description", Value::opt_str(&self.description)),
637 ("rect", self.rect.to_value()),
638 ("value", Value::opt_str(&self.value)),
639 ("caret", Value::opt_usize(self.caret)),
640 (
641 "selection",
642 Value::opt(self.selection, |(a, b)| {
643 Value::list([Value::Int(a as i64), Value::Int(b as i64)])
644 }),
645 ),
646 ("anchor", Value::opt(self.anchor, |p| p.to_value(h))),
647 ("focus", Value::opt(self.focus, |p| p.to_value(h))),
648 ("runs", Value::list(self.runs.iter().map(|r| r.to_value(h)))),
649 ("checked", Value::opt_bool(self.checked)),
650 ("mixed", Value::Bool(self.mixed)),
651 ("selected", Value::opt_bool(self.selected)),
652 ("expanded", Value::opt_bool(self.expanded)),
653 ("pos_in_set", Value::opt_usize(self.pos_in_set)),
654 ("set_size", Value::opt_usize(self.set_size)),
655 (
656 "orientation",
657 Value::opt(self.orientation, |o| Value::str(o.name())),
658 ),
659 ("live", Value::str(self.live.name())),
660 ("value_now", Value::opt_float(self.number)),
661 ("value_min", Value::opt_float(self.min)),
662 ("value_max", Value::opt_float(self.max)),
663 ("value_step", Value::opt_float(self.step)),
664 ("focused", Value::Bool(self.focused)),
665 ("disabled", Value::Bool(self.disabled)),
666 ("modal", Value::Bool(self.modal)),
667 ("scroll", Value::opt(self.scroll, ScrollState::to_value)),
668 (
669 "actions",
670 Value::list(self.action_list().into_iter().map(|a| Value::str(a.name()))),
671 ),
672 ])
673 }
674
675 pub fn supports(&self, action: AccessAction) -> bool {
676 self.actions & action.bit() != 0
677 }
678
679 /// The actions this node advertises.
680 pub fn action_list(&self) -> Vec<AccessAction> {
681 AccessAction::ALL
682 .iter()
683 .copied()
684 .filter(|a| self.supports(*a))
685 .collect()
686 }
687
688 /// Turns a run position back into the line it is on and a byte offset
689 /// into that line's text (the `"\n"` counting as the line's end).
690 pub fn line_offset(&self, pos: TextPos) -> Option<(usize, usize)> {
691 let run = self.runs.iter().find(|r| r.key == pos.run)?;
692 let within = run
693 .text
694 .char_indices()
695 .nth(pos.character)
696 .map_or(run.text.len(), |(b, _)| b);
697 Some((run.line, run.start + within.min(run.end - run.start)))
698 }
699
700 /// The run position of a byte offset into line `line`'s text: the
701 /// run holding it, or the line's last run for its end. An offset inside
702 /// a character is the character's start: a custom editor's `caret` is
703 /// the app's number, and slicing there panicked, which emptied the
704 /// whole tree for every frame it stood.
705 pub fn text_pos(&self, line: usize, offset: usize) -> Option<TextPos> {
706 let chars_before =
707 |r: &AccessRun, at: usize| r.text[..r.text.floor_char_boundary(at)].chars().count();
708 let mut last = None;
709 for r in self.runs.iter().filter(|r| r.line == line) {
710 if offset >= r.start && offset < r.end {
711 return Some(TextPos {
712 run: r.key,
713 character: chars_before(r, offset - r.start),
714 });
715 }
716 last = Some(r);
717 }
718 let r = last?;
719 Some(TextPos {
720 run: r.key,
721 character: chars_before(r, (offset.max(r.start) - r.start).min(r.end - r.start)),
722 })
723 }
724}
725
726/// The semantic nodes of a finished frame, in tree order (a parent always
727/// precedes its descendants; the root comes first), each key once: of two
728/// nodes a frame gave one key, the first.
729#[derive(Clone, Debug, Default, PartialEq)]
730pub struct AccessTree {
731 pub nodes: Vec<AccessNode>,
732 /// The node holding keyboard focus, if any.
733 pub focus: Option<Key>,
734 /// A hash of everything above: a driver that sent the tree once sends
735 /// it again only when this changes.
736 pub hash: u64,
737}
738
739impl AccessTree {
740 /// `{nodes, focus, hash}`, every node by [`AccessNode::to_value`],
741 /// the focus key spelled by `h` and the hash as sixteen hex digits.
742 pub fn to_value(&self, h: Handles) -> Value {
743 Value::map([
744 (
745 "nodes",
746 Value::list(self.nodes.iter().map(|n| n.to_value(h))),
747 ),
748 ("focus", h.opt_key(self.focus)),
749 ("hash", Value::Str(format!("{:016x}", self.hash))),
750 ])
751 }
752
753 pub fn get(&self, key: Key) -> Option<&AccessNode> {
754 self.nodes.iter().find(|n| n.key == key)
755 }
756
757 /// The nodes whose `parent` is `key`, in order.
758 pub fn children(&self, key: Key) -> impl Iterator<Item = &AccessNode> {
759 self.nodes.iter().filter(move |n| n.parent == Some(key))
760 }
761
762 /// The root (the window), when a frame has been built.
763 pub fn root(&self) -> Option<&AccessNode> {
764 self.nodes.first()
765 }
766}
767
768/// What the tree walk decided for one node.
769pub(crate) struct Semantic {
770 pub role: Role,
771 pub name: Option<String>,
772 /// Descendants are read as part of this node, not as their own nodes.
773 pub presentational: bool,
774}
775
776/// The role a node's spec and content imply, before elision. None = plain
777/// structure (elided; its descendants still appear).
778pub(crate) fn derived_role(tree: &Tree, i: usize) -> Option<Role> {
779 let spec = &tree.specs[i];
780 if let Some(r) = spec.access().role {
781 // A line is structure of the editor around it; on its own it is
782 // nothing.
783 return (r != Role::Line).then_some(r);
784 }
785 if i == 0 {
786 return Some(Role::Window);
787 }
788 match tree.content[i] {
789 NodeContent::Text(_) => return Some(Role::StaticText),
790 NodeContent::Edit(_) => return Some(Role::TextInput),
791 NodeContent::Image(..) => return Some(Role::Image),
792 // A stroke or a fill is decoration on its own and elided like
793 // plain structure; one that takes input is hit by its shape, so
794 // the derivation below reaches it as it reaches a box —
795 // a clickable wedge is a button, a draggable connector a control.
796 NodeContent::Line(_) | NodeContent::Polygon(_) | NodeContent::Path(_) => {}
797 NodeContent::Cells(_) => return Some(Role::Terminal),
798 // A fragment is paint. On its own it is decoration and is elided
799 // like plain structure, but unlike a line it does take input, so
800 // the derivation below still reaches it: a fragment with an
801 // `on_click` is a button, and one that means something says so
802 // with its own `role` and `label`.
803 NodeContent::Fragment(_) => {}
804 NodeContent::Container => {}
805 }
806 match spec.window {
807 Some(WindowRole::Drag) => return Some(Role::TitleBar),
808 Some(WindowRole::Button(_)) => return Some(Role::Button),
809 None => {}
810 }
811 if spec.events().modal.is_some() {
812 // A modal surface is a dialog to assistive technology; anything
813 // else it might be, the view says with an explicit role.
814 return Some(Role::Dialog);
815 }
816 if spec.events().on_click.is_some() {
817 return Some(Role::Button);
818 }
819 if spec.layout.scroll_x || spec.layout.scroll_y {
820 return Some(Role::ScrollView);
821 }
822 if spec.events().on_key.is_some() || spec.focusable {
823 // A key sink or a focusable box is at least somewhere focus can
824 // land, so a reader has to be able to see it there.
825 return Some(Role::Group);
826 }
827 if spec.access().live != Live::Off {
828 // A live region that was elided would carry its liveness
829 // nowhere: its text would inherit the window's instead, and the
830 // change would go unread.
831 return Some(Role::Group);
832 }
833 None
834}
835
836/// Whether node `i` is an editor the app draws itself: an editor role on
837/// something other than a built-in edit node.
838pub(crate) fn is_custom_editor(tree: &Tree, i: usize) -> bool {
839 tree.specs[i].access().role.is_some_and(Role::is_editor)
840 && !matches!(tree.content[i], NodeContent::Edit(_))
841}
842
843/// Whether node `i` can hold keyboard focus: an editor, a key sink, a
844/// control role, a derived button (`on_click`), or a node declaring
845/// `focusable` — never a disabled node, decoration or window chrome. The
846/// Tab ring is these nodes in tree order; a `role="none"` subtree is
847/// skipped by the walk, not here.
848pub(crate) fn focusable(tree: &Tree, i: usize) -> bool {
849 let spec = &tree.specs[i];
850 if spec.disabled || spec.access().role == Some(Role::None) || spec.window.is_some() {
851 return false;
852 }
853 if spec.focusable
854 || spec.events().on_key.is_some()
855 || matches!(tree.content[i], NodeContent::Edit(_))
856 {
857 return true;
858 }
859 match spec.access().role {
860 Some(r) => r.is_control(),
861 None => spec.events().on_click.is_some(),
862 }
863}
864
865/// The role, name and presentation of node `i`, or None when it is plain
866/// structure. Shared by the tree build and the diagnostics check so both
867/// agree on what "unnamed" means.
868pub(crate) fn semantic(
869 tree: &Tree,
870 text: &TextSystem,
871 edit: &EditStore,
872 title: Option<&str>,
873 i: usize,
874) -> Option<Semantic> {
875 let mut role = derived_role(tree, i)?;
876 let spec = &tree.specs[i];
877 if role == Role::TextInput
878 && let NodeContent::Edit(key) = tree.content[i]
879 {
880 if edit.is_secure(key) {
881 role = Role::PasswordInput;
882 } else if edit.is_multiline(key) {
883 role = Role::MultilineTextInput;
884 }
885 }
886 // Window buttons read as one control; the drag strip keeps its
887 // children (the buttons sit inside it). A custom editor's lines are
888 // its text, not children.
889 //
890 // A live region joins them: it reads as **one message**, named by the
891 // text inside it, and that is what changes when the message does. The
892 // alternative — the region carrying only liveness and each platform
893 // announcing the changed descendant — is what was first built,
894 // and macOS does not deliver it: `accesskit_macos` derives a live
895 // node's announcement from `NodeWrapper::label()`, which for a
896 // `Role::Label` reads the node's *value*, so a live static text
897 // announces nothing at all. One named node is also one announcement
898 // on all three platforms rather than one per live descendant.
899 let presentational = role.presentational()
900 || spec.access().live != Live::Off
901 || matches!(spec.window, Some(WindowRole::Button(_)))
902 || is_custom_editor(tree, i);
903 let name = match (&spec.access().label, spec.window) {
904 (Some(label), _) => Some(label.to_string()),
905 (None, Some(WindowRole::Button(b))) => Some(
906 match b {
907 WindowButton::Close => "Close",
908 WindowButton::Minimize => "Minimize",
909 WindowButton::Maximize => "Maximize",
910 }
911 .to_string(),
912 ),
913 (None, _) => match role {
914 Role::StaticText => match tree.content[i] {
915 NodeContent::Text(id) => Some(text.content(id).to_string()),
916 _ => None,
917 },
918 // The window carries the title; a drawn titlebar naming
919 // itself the same thing would have a screen reader read it
920 // twice (it keeps its children, so its own text is read).
921 Role::Window => title.map(str::to_string),
922 _ if role.presentational() || spec.access().live != Live::Off => {
923 content_name(tree, text, i)
924 }
925 _ => None,
926 },
927 };
928 Some(Semantic {
929 role,
930 name,
931 presentational,
932 })
933}
934
935/// Whether a live region has anything a reader could ever say: a `label`
936/// of its own, or text somewhere inside it (which is where the string
937/// actually comes from — liveness inherits, and the changed descendant is
938/// what gets announced). Shared with the diagnostics so both agree on
939/// what a silent live region is.
940pub(crate) fn live_region_speaks(tree: &Tree, text: &TextSystem, i: usize) -> bool {
941 tree.specs[i].access().label.is_some() || content_name(tree, text, i).is_some()
942}
943
944/// The text inside node `i`, in order, joined by spaces — ARIA's
945/// name-from-content. None when there is none.
946///
947/// A subtree under `role="none"` is not content, as it is not for a
948/// custom editor's lines ([`lines_under`]): it is hidden from assistive
949/// technology, and what it draws is not what the control is called. The
950/// `tooltip` prop's hint is one (`widgets::hover_hint`) — built only while
951/// the pointer is over the node, it made a hovered button's name its label
952/// and its hint both.
953fn content_name(tree: &Tree, text: &TextSystem, i: usize) -> Option<String> {
954 let end = tree.subtree_end(i);
955 let mut out = String::new();
956 let mut j = i;
957 while j < end {
958 if j > i && tree.specs[j].access().role == Some(Role::None) {
959 j = tree.subtree_end(j);
960 continue;
961 }
962 let at = j;
963 j += 1;
964 if let NodeContent::Text(id) = tree.content[at] {
965 let s = text.content(id).trim();
966 if s.is_empty() {
967 continue;
968 }
969 if !out.is_empty() {
970 out.push(' ');
971 }
972 out.push_str(s);
973 }
974 }
975 (!out.is_empty()).then_some(out)
976}
977
978/// Everything the build reads besides the tree.
979pub(crate) struct Sources<'a> {
980 pub text: &'a TextSystem,
981 pub cells: &'a crate::cells::CellStore,
982 pub edit: &'a EditStore,
983 pub scroll: &'a ScrollStore,
984 pub title: Option<&'a str>,
985 /// The core's one keyboard focus (see `Core::focus`).
986 pub focus: Option<Key>,
987 /// The frame's modal in effect (see `Core::modal`).
988 pub modal: Option<Key>,
989 pub viewport: Size,
990 pub scale: f32,
991 /// The clip each node was emitted under, by tree index: its
992 /// ancestors' only, and the one its hit region carries, so a float
993 /// that escapes has none and a `clip` float has its parent's.
994 /// Empty when the frame clipped nothing.
995 pub clips: &'a [Clip],
996}
997
998/// The clip node `i` was emitted under (see [`Sources::clips`]).
999fn clip_of(src: &Sources<'_>, i: usize) -> Rect {
1000 src.clips.get(i).map_or(NO_CLIP, |c| c.rect)
1001}
1002
1003/// `rect` cut to `clip`, so assistive technology finds and highlights
1004/// only what is drawn. A rect wholly outside becomes a
1005/// zero-size one on the clip's edge nearest it: the node keeps its place
1006/// in reading order and its actions (a reader's "scroll into view" goes by
1007/// key), but a point never lands in it. The clip is a rect even where the
1008/// clipper's corners are round, as the hit test's is.
1009fn clipped(rect: Rect, clip: Rect) -> Rect {
1010 let x0 = rect.x.max(clip.x);
1011 let y0 = rect.y.max(clip.y);
1012 let x1 = (rect.x + rect.w).min(clip.x + clip.w);
1013 let y1 = (rect.y + rect.h).min(clip.y + clip.h);
1014 // Gone along an axis when nothing of it is left, which is emission's
1015 // cull: past the edge, or on it with no extent inside. An axis the
1016 // rect never had extent on (a caret-wide run) is not gone for that.
1017 let gone = |lo: f32, hi: f32, extent: f32| hi < lo || (hi == lo && extent > 0.0);
1018 if gone(x0, x1, rect.w) || gone(y0, y1, rect.h) {
1019 // `max` then `min` rather than `clamp`, which panics on a clip
1020 // whose edges cross.
1021 let x = rect.x.max(clip.x).min(clip.x + clip.w);
1022 let y = rect.y.max(clip.y).min(clip.y + clip.h);
1023 return Rect::new(x, y, 0.0, 0.0);
1024 }
1025 Rect::new(x0, y0, x1 - x0, y1 - y0)
1026}
1027
1028/// Node `i`'s access rect: the root's viewport, everyone else's box cut
1029/// to the clip it was emitted under.
1030fn node_rect(tree: &Tree, src: &Sources<'_>, i: usize) -> Rect {
1031 if i == 0 {
1032 return Rect::new(0.0, 0.0, src.viewport.w, src.viewport.h);
1033 }
1034 let rect = Rect::from_pos_size(tree.pos[i], tree.size[i]);
1035 // Under a turn the rect assistive technology gets is the bounding
1036 // box of the turned box, cut inside the turn first and outside it
1037 // after (ADR 0043, decision 7).
1038 if let Some(c) = src.clips.get(i)
1039 && c.turned()
1040 {
1041 let inner = clipped(rect, c.inner);
1042 if inner.w <= 0.0 && inner.h <= 0.0 {
1043 return inner;
1044 }
1045 return clipped(c.transform.bounds(inner), c.rect);
1046 }
1047 clipped(rect, clip_of(src, i))
1048}
1049
1050/// What an editor's runs are cut to: the node's clip and, for a field
1051/// that does not fold to its width, its content box across, as emission
1052/// cuts its glyphs — a scrolled field's text past its edge is not
1053/// drawn, so a reader should not find it there either.
1054fn edit_run_clip(tree: &Tree, src: &Sources<'_>, i: usize, edit_key: Key) -> Rect {
1055 let clip = clip_of(src, i);
1056 if src.edit.folds(edit_key) {
1057 return clip;
1058 }
1059 let pad = tree.specs[i].layout.padding;
1060 let across = Rect::new(
1061 tree.pos[i].x + pad.l,
1062 clip.y,
1063 (tree.size[i].w - pad.x()).max(0.0),
1064 clip.h,
1065 );
1066 clip.intersect(&across)
1067}
1068
1069/// Cuts runs to `clip` the way [`clipped`] cuts a node, keeping each
1070/// character where it is: `char_positions` are relative to the run's x,
1071/// so they move by what the x did.
1072fn clip_runs(runs: &mut [AccessRun], clip: Rect) {
1073 if clip == NO_CLIP {
1074 return;
1075 }
1076 for run in runs {
1077 let cut = clipped(run.rect, clip);
1078 let dx = run.rect.x - cut.x;
1079 if dx != 0.0 {
1080 for p in &mut run.char_positions {
1081 *p += dx;
1082 }
1083 }
1084 run.rect = cut;
1085 }
1086}
1087
1088/// A hash of every input [`build`] reads, taken by the same walk with
1089/// nothing built. `None` means "cannot answer, rebuild" — see the custom
1090/// editor note below.
1091///
1092/// **The invariant this rests on**: everything `build` reads must be mixed
1093/// in here. Miss one and a frame that changed only that thing serves the
1094/// previous frame's tree, which is a bug with no symptom in the core, no
1095/// failing test, and a wrong reading on somebody's screen. Two things hold
1096/// it. The walk is deliberately the *same* walk — the same skip rules
1097/// calling the same [`semantic`], [`focusable`] and
1098/// [`composite::orientation`](crate::composite::orientation) — so what can
1099/// drift is only the fields `build` reads directly out of the spec and the
1100/// sources; and `runtime::dispatch`'s tests mutate each of those in turn
1101/// and assert the tree moved.
1102///
1103/// It is worth taking because the walk is the cheap quarter of `build`:
1104/// **105 µs against 480 µs** over a 10,000-node frame on an M3 Pro, because
1105/// three quarters of that function is constructing `AccessNode`s and
1106/// pushing them, which is exactly what a cache hit skips.
1107///
1108/// A custom editor (`is_custom_editor`) answers `None` rather than a hash:
1109/// `custom_editor` fills a node from the `line` children's own text and
1110/// runs, and there is no reading of those inputs that does not amount to
1111/// building the node. Such a view is one whose text is changing anyway, so
1112/// it is the case a cache would miss on regardless.
1113pub(crate) fn inputs_hash(tree: &Tree, src: &Sources<'_>) -> Option<u64> {
1114 use std::hash::{Hash, Hasher};
1115 let mut h = rustc_hash::FxHasher::default();
1116 let f = |h: &mut rustc_hash::FxHasher, v: f32| v.to_bits().hash(h);
1117
1118 tree.len().hash(&mut h);
1119 src.focus.map(|k| k.0).hash(&mut h);
1120 src.modal.map(|k| k.0).hash(&mut h);
1121 src.title.hash(&mut h);
1122 f(&mut h, src.viewport.w);
1123 f(&mut h, src.viewport.h);
1124 f(&mut h, src.scale);
1125
1126 let mut skip_until = 0usize;
1127 let mut i = 0usize;
1128 while i < tree.len() {
1129 if i < skip_until {
1130 i += 1;
1131 continue;
1132 }
1133 let Some(sem) = semantic(tree, src.text, src.edit, src.title, i) else {
1134 i += 1;
1135 continue;
1136 };
1137 if sem.role == Role::None {
1138 skip_until = tree.subtree_end(i);
1139 i += 1;
1140 continue;
1141 }
1142 if is_custom_editor(tree, i) {
1143 return None;
1144 }
1145 let key = tree.keys[i];
1146 let spec = &tree.specs[i];
1147 let ax = spec.access();
1148
1149 // Identity and place in the tree.
1150 i.hash(&mut h);
1151 key.0.hash(&mut h);
1152 tree.parent[i].hash(&mut h);
1153 tree.origins[i].0.hash(&mut h);
1154
1155 // What `semantic` decided, and the node's own strings.
1156 sem.role.hash(&mut h);
1157 sem.name.hash(&mut h);
1158 sem.presentational.hash(&mut h);
1159 ax.description.as_deref().hash(&mut h);
1160
1161 // The rect, which is the root's viewport and everyone else's box
1162 // cut to its clip.
1163 let rect = node_rect(tree, src, i);
1164 for v in [rect.x, rect.y, rect.w, rect.h] {
1165 f(&mut h, v);
1166 }
1167
1168 // State the node reports, and everything the action bits read.
1169 (src.focus == Some(key)).hash(&mut h);
1170 (src.modal == Some(key)).hash(&mut h);
1171 spec.disabled.hash(&mut h);
1172 ax.live.hash(&mut h);
1173 spec.window.hash(&mut h);
1174 spec.events().on_click.is_some().hash(&mut h);
1175 focusable(tree, i).hash(&mut h);
1176 crate::composite::orientation(tree, i).hash(&mut h);
1177 ax.expanded.hash(&mut h);
1178 ax.checked.hash(&mut h);
1179 ax.mixed.hash(&mut h);
1180 ax.selected.hash(&mut h);
1181 ax.value_text.as_deref().hash(&mut h);
1182 for v in [ax.value_now, ax.value_min, ax.value_max, ax.value_step] {
1183 v.is_some().hash(&mut h);
1184 f(&mut h, v.unwrap_or(0.0));
1185 }
1186
1187 // The content's own value, through the same accessors `build` uses.
1188 match tree.content[i] {
1189 NodeContent::Edit(edit_key) => {
1190 let pad = spec.layout.padding;
1191 f(&mut h, tree.pos[i].x + pad.l);
1192 f(&mut h, tree.pos[i].y + pad.t);
1193 src.edit.shown_text(edit_key).hash(&mut h);
1194 src.edit.placeholder(edit_key).hash(&mut h);
1195 src.edit.caret_and_selection(edit_key).hash(&mut h);
1196 src.edit.selection_cursors(edit_key).hash(&mut h);
1197 // `runs` is the editor's shaped layout, which the text and
1198 // the box above do not fully determine — a font arriving
1199 // between frames reshapes it. The store's own version bumps
1200 // on every mutation that could, so it stands in for reading
1201 // the runs, which would cost what building them costs.
1202 src.edit.version(edit_key).hash(&mut h);
1203 let clip = edit_run_clip(tree, src, i, edit_key);
1204 for v in [clip.x, clip.y, clip.w, clip.h] {
1205 f(&mut h, v);
1206 }
1207 }
1208 NodeContent::Cells(id) => src.cells.value(id).hash(&mut h),
1209 _ => {}
1210 }
1211
1212 // Scrolling: the offset is retained state, the max a layout output.
1213 if spec.layout.scroll_x || spec.layout.scroll_y {
1214 spec.layout.scroll_x.hash(&mut h);
1215 spec.layout.scroll_y.hash(&mut h);
1216 let off = src.scroll.drawn(key);
1217 let max = tree.scroll_max[i];
1218 for v in [off.x, off.y, max.x, max.y] {
1219 f(&mut h, v);
1220 }
1221 }
1222
1223 if sem.presentational {
1224 skip_until = tree.subtree_end(i);
1225 }
1226 i += 1;
1227 }
1228 Some(h.finish())
1229}
1230
1231/// Derives the access tree of a laid-out frame.
1232pub(crate) fn build(tree: &Tree, src: &Sources<'_>) -> AccessTree {
1233 let mut out = AccessTree::default();
1234 if tree.is_empty() {
1235 return out;
1236 }
1237 // Per tree node: the key of the nearest semantic ancestor (for elided
1238 // nodes, inherited from the parent).
1239 let mut parents: Vec<Option<Key>> = vec![None; tree.len()];
1240 let mut emitted: rustc_hash::FxHashSet<Key> = rustc_hash::FxHashSet::default();
1241 let mut skip_until = 0usize;
1242 let mut i = 0usize;
1243 while i < tree.len() {
1244 if i < skip_until {
1245 i += 1;
1246 continue;
1247 }
1248 let parent = tree.parent[i];
1249 let inherited = if parent == NIL {
1250 None
1251 } else {
1252 parents[parent as usize]
1253 };
1254 let Some(sem) = semantic(tree, src.text, src.edit, src.title, i) else {
1255 parents[i] = inherited;
1256 i += 1;
1257 continue;
1258 };
1259 if sem.role == Role::None {
1260 skip_until = tree.subtree_end(i);
1261 i += 1;
1262 continue;
1263 }
1264 let key = tree.keys[i];
1265 // A key is the node's id to the platform, so the tree holds one
1266 // node under it: the first in tree order. A second (two siblings
1267 // given one label, an index key that is also an unkeyed
1268 // sibling's — the `duplicate-key` warning) is left out the way
1269 // plain structure is, its descendants under its nearest semantic
1270 // ancestor; AccessKit panics on a child listed twice (F165).
1271 if !emitted.insert(key) {
1272 parents[i] = inherited;
1273 if sem.presentational {
1274 skip_until = tree.subtree_end(i);
1275 }
1276 i += 1;
1277 continue;
1278 }
1279 parents[i] = Some(key);
1280 let spec: &NodeSpec = &tree.specs[i];
1281 let rect = node_rect(tree, src, i);
1282 let mut node = AccessNode {
1283 key,
1284 parent: inherited,
1285 origin: tree.origins[i],
1286 role: sem.role,
1287 name: sem.name,
1288 description: spec.access().description.as_ref().map(|d| d.to_string()),
1289 rect,
1290 value: None,
1291 caret: None,
1292 selection: None,
1293 runs: Vec::new(),
1294 anchor: None,
1295 focus: None,
1296 checked: None,
1297 mixed: false,
1298 selected: None,
1299 expanded: None,
1300 orientation: crate::composite::orientation(tree, i),
1301 pos_in_set: None,
1302 set_size: None,
1303 number: None,
1304 min: None,
1305 max: None,
1306 step: None,
1307 focused: src.focus == Some(key),
1308 disabled: spec.disabled,
1309 modal: src.modal == Some(key),
1310 scroll: None,
1311 actions: 0,
1312 live: spec.access().live,
1313 };
1314 let mut actions = 0u32;
1315 match spec.window {
1316 Some(WindowRole::Button(_)) => actions |= AccessAction::Click.bit(),
1317 Some(WindowRole::Drag) => {}
1318 None => {
1319 if spec.events().on_click.is_some() && !spec.disabled {
1320 actions |= AccessAction::Click.bit();
1321 }
1322 }
1323 }
1324 // Anything the keyboard can reach, assistive technology can focus
1325 // (and AccessKit calls focusable only what supports `Focus`).
1326 if focusable(tree, i) {
1327 actions |= AccessAction::Focus.bit() | AccessAction::Blur.bit();
1328 }
1329 if let NodeContent::Edit(edit_key) = tree.content[i] {
1330 let pad = spec.layout.padding;
1331 let origin = Vec2::new(tree.pos[i].x + pad.l, tree.pos[i].y + pad.t);
1332 // A secure editor's is the bullets it draws (backlog F167).
1333 node.value = src.edit.shown_text(edit_key);
1334 // A placeholder is what the field is for, said where the field
1335 // says nothing of its own (backlog F149).
1336 if node.description.is_none() {
1337 node.description = src.edit.placeholder(edit_key).map(str::to_string);
1338 }
1339 node.runs = src.edit.runs(edit_key, key, origin, src.scale);
1340 clip_runs(&mut node.runs, edit_run_clip(tree, src, i, edit_key));
1341 if let Some((caret, selection)) = src.edit.caret_and_selection(edit_key) {
1342 node.caret = Some(caret);
1343 node.selection = selection;
1344 }
1345 if let Some((anchor, focus)) = src.edit.selection_cursors(edit_key) {
1346 node.anchor = node.text_pos(anchor.0, anchor.1);
1347 node.focus = node.text_pos(focus.0, focus.1);
1348 }
1349 if !spec.disabled {
1350 actions |= AccessAction::Click.bit()
1351 | AccessAction::SetValue.bit()
1352 | AccessAction::SetTextSelection.bit()
1353 | AccessAction::ReplaceSelectedText.bit();
1354 }
1355 } else if is_custom_editor(tree, i) {
1356 custom_editor(tree, src, i, &mut node);
1357 if !spec.disabled {
1358 actions |= AccessAction::SetValue.bit()
1359 | AccessAction::SetTextSelection.bit()
1360 | AccessAction::ReplaceSelectedText.bit();
1361 }
1362 } else if let NodeContent::Cells(id) = tree.content[i] {
1363 // A terminal's value is its screen, rows joined.
1364 node.value = Some(src.cells.value(id));
1365 }
1366 // A disclosure names its own state, so it lands wherever it is
1367 // declared: no role means "shows and hides something".
1368 let ax = spec.access();
1369 node.expanded = ax.expanded;
1370 match sem.role {
1371 Role::Checkbox | Role::Radio | Role::Switch => {
1372 node.checked = Some(ax.checked);
1373 node.mixed = ax.mixed && sem.role == Role::Checkbox;
1374 }
1375 // A tab is one of a set by definition, so it reports either
1376 // state; a row or a link reports only the one it declares,
1377 // since most lists and every navigation bar are not
1378 // selections and "not selected" on each of their nodes is the
1379 // noise AccessKit warns about.
1380 Role::Tab => node.selected = Some(ax.selected),
1381 Role::ListItem | Role::Link if ax.selected => node.selected = Some(true),
1382 // A menu row that is a setting reads as checked (the drawn
1383 // menu's checkmark, `widgets::menu_panel`); every other row is
1384 // a command and says nothing, the way a list row says nothing
1385 // about a selection it is not part of. Before this the widget
1386 // declared the fact and the tree dropped it, so a reader heard
1387 // "Wrap" where a sighted user saw "✓ Wrap".
1388 Role::MenuItem if ax.checked => node.checked = Some(true),
1389 Role::Slider => {
1390 node.number = ax.value_now;
1391 node.min = ax.value_min;
1392 node.max = ax.value_max;
1393 node.step = ax.value_step;
1394 // The declared reading, in the one string slot the
1395 // platform gives a node (see `value`). Slider-only, like
1396 // the three numbers: a `group` shaped like a progress bar
1397 // carries none of them either, and a role whose numbers
1398 // are ignored should not have a reading that is not.
1399 node.value = ax.value_text.as_deref().map(str::to_owned);
1400 // SetValue too: Windows' UI Automation has no increment,
1401 // and moves a slider only by setting it.
1402 if !spec.disabled {
1403 actions |= AccessAction::Increment.bit()
1404 | AccessAction::Decrement.bit()
1405 | AccessAction::SetValue.bit();
1406 }
1407 }
1408 _ => {}
1409 }
1410 if spec.layout.scroll_x || spec.layout.scroll_y {
1411 let off = src.scroll.drawn(key);
1412 let max = tree.scroll_max[i];
1413 node.scroll = Some(ScrollState {
1414 x: off.x,
1415 y: off.y,
1416 max_x: max.x,
1417 max_y: max.y,
1418 });
1419 if spec.layout.scroll_y {
1420 actions |= AccessAction::ScrollUp.bit() | AccessAction::ScrollDown.bit();
1421 }
1422 if spec.layout.scroll_x {
1423 actions |= AccessAction::ScrollLeft.bit() | AccessAction::ScrollRight.bit();
1424 }
1425 }
1426 if i != 0 {
1427 actions |= AccessAction::ScrollIntoView.bit();
1428 }
1429 node.actions = actions;
1430 if node.focused {
1431 out.focus = Some(key);
1432 }
1433 out.nodes.push(node);
1434 if sem.presentational {
1435 skip_until = tree.subtree_end(i);
1436 }
1437 i += 1;
1438 }
1439 set_positions(tree, &mut out);
1440 out.hash = hash_of(&out);
1441 out
1442}
1443
1444/// "3 of 7", derived rather than declared: a composite container already
1445/// holds its items, so the core counts them and numbers each one instead
1446/// of making every view repeat itself (and get it wrong the moment a row
1447/// is filtered out). The count lands on the container and the zero-based
1448/// ordinal on the item, which is how AccessKit models a set.
1449///
1450/// The items come from `composite::items`, which is also the order the
1451/// arrow keys walk: "3 of 7" and that walk must be the same seven in the same
1452/// order or the announcement is a lie, so they are one function rather
1453/// than two that agree by inspection. A disabled item is still one of the
1454/// set — "2 of 3" is what a reader should hear on a disabled tab — even
1455/// though the arrows step over it, as the Tab ring does.
1456fn set_positions(tree: &Tree, out: &mut AccessTree) {
1457 let containers: Vec<(usize, Role)> = (0..tree.len())
1458 .filter_map(|i| {
1459 let item = crate::composite::item_role(tree.specs[i].access().role?)?;
1460 Some((i, item))
1461 })
1462 .collect();
1463 let mut items: Vec<usize> = Vec::new();
1464 for (container, item) in containers {
1465 crate::composite::items(tree, container, item, &mut items);
1466 if items.is_empty() {
1467 continue;
1468 }
1469 let size = items.len();
1470 let container = tree.keys[container];
1471 let keys: Vec<Key> = items.iter().map(|&i| tree.keys[i]).collect();
1472 for n in &mut out.nodes {
1473 if n.key == container {
1474 n.set_size = Some(size);
1475 } else if let Some(pos) = keys.iter().position(|k| *k == n.key) {
1476 n.pos_in_set = Some(pos);
1477 }
1478 }
1479 }
1480}
1481
1482/// The `role="line"` nodes under `i`, in tree order — the numbering a
1483/// custom editor's `access` events use for `line`, and the one a press
1484/// inside a key sink carries, so the two agree. Subtrees
1485/// under `role="none"` (a gutter) do not count, and a line's own subtree
1486/// is not searched for lines.
1487pub(crate) fn lines_under(tree: &Tree, i: usize) -> Vec<usize> {
1488 let end = tree.subtree_end(i);
1489 let mut lines: Vec<usize> = Vec::new();
1490 let mut j = i + 1;
1491 while j < end {
1492 let spec = &tree.specs[j];
1493 if spec.access().role == Some(Role::None) {
1494 j = tree.subtree_end(j);
1495 continue;
1496 }
1497 if spec.access().role == Some(Role::Line) {
1498 lines.push(j);
1499 j = tree.subtree_end(j);
1500 continue;
1501 }
1502 j += 1;
1503 }
1504 lines
1505}
1506
1507/// A custom editor's text: its `role="line"` descendants in order, each
1508/// line the text nodes inside it concatenated, runs from their buffers,
1509/// caret and anchor from the lines that declare them. Subtrees under
1510/// `role="none"` (a gutter) do not count.
1511fn custom_editor(tree: &Tree, src: &Sources<'_>, i: usize, node: &mut AccessNode) {
1512 let lines = lines_under(tree, i);
1513 let mut value = String::new();
1514 let mut run_no = 0usize;
1515 let mut caret: Option<(usize, usize)> = None;
1516 let mut anchor: Option<(usize, usize)> = None;
1517 for (ln, &l) in lines.iter().enumerate() {
1518 let last = ln + 1 == lines.len();
1519 let mut line_text = String::new();
1520 let mut texts: Vec<usize> = Vec::new();
1521 let lend = tree.subtree_end(l);
1522 let mut t = l;
1523 while t < lend {
1524 if tree.specs[t].access().role == Some(Role::None) {
1525 t = tree.subtree_end(t);
1526 continue;
1527 }
1528 if let NodeContent::Text(_) = tree.content[t] {
1529 texts.push(t);
1530 }
1531 t += 1;
1532 }
1533 for (k, &t) in texts.iter().enumerate() {
1534 let NodeContent::Text(id) = tree.content[t] else {
1535 continue;
1536 };
1537 let base = line_text.len();
1538 line_text.push_str(src.text.content(id));
1539 let newline = !last && k + 1 == texts.len();
1540 let first = node.runs.len();
1541 src.text.access_runs(
1542 id,
1543 RunSource {
1544 key: node.key,
1545 line: ln,
1546 byte_base: base,
1547 origin: tree.pos[t],
1548 scale: src.scale,
1549 newline_after_last: newline,
1550 },
1551 &mut run_no,
1552 &mut node.runs,
1553 );
1554 clip_runs(&mut node.runs[first..], clip_of(src, t));
1555 }
1556 if texts.is_empty() {
1557 // An empty line still has a place for the caret.
1558 node.runs.push(empty_run(
1559 node.key,
1560 run_no,
1561 ln,
1562 Rect::from_pos_size(tree.pos[l], tree.size[l]),
1563 !last,
1564 ));
1565 let at = node.runs.len() - 1;
1566 clip_runs(&mut node.runs[at..], clip_of(src, l));
1567 run_no += 1;
1568 }
1569 if let Some(c) = tree.specs[l].access().caret {
1570 caret = Some((ln, (c as usize).min(line_text.len())));
1571 }
1572 if let Some(a) = tree.specs[l].access().selection_anchor {
1573 anchor = Some((ln, (a as usize).min(line_text.len())));
1574 }
1575 if ln > 0 {
1576 value.push('\n');
1577 }
1578 value.push_str(&line_text);
1579 }
1580 node.value = Some(value);
1581 if let Some((line, offset)) = caret {
1582 node.focus = node.text_pos(line, offset);
1583 let anchor = anchor.unwrap_or((line, offset));
1584 node.anchor = node.text_pos(anchor.0, anchor.1);
1585 // Byte offsets into the joined value, for the flat view of it.
1586 let flat = |(line, offset): (usize, usize)| {
1587 node.value
1588 .as_ref()
1589 .map(|v| v.split('\n').take(line).map(|l| l.len() + 1).sum::<usize>() + offset)
1590 };
1591 node.caret = flat((line, offset));
1592 if anchor != (line, offset)
1593 && let (Some(a), Some(c)) = (flat(anchor), node.caret)
1594 {
1595 node.selection = Some((a.min(c), a.max(c)));
1596 }
1597 }
1598}
1599
1600/// Where a buffer's runs belong: which line and byte base of the editor
1601/// they land in, and where the buffer draws.
1602pub(crate) struct RunSource {
1603 pub key: Key,
1604 /// The line index the buffer's first line maps to.
1605 pub line: usize,
1606 /// Byte offset of the buffer's text inside that line (a line drawn as
1607 /// several text nodes).
1608 pub byte_base: usize,
1609 /// Where the buffer's (0, 0) sits, logical px, viewport coordinates.
1610 pub origin: Vec2,
1611 pub scale: f32,
1612 /// Whether the buffer's last line continues into another line of the
1613 /// editor (its last run then ends with `"\n"`).
1614 pub newline_after_last: bool,
1615}
1616
1617/// The id of run `n` of editor `key`; never a node key (the tag byte
1618/// keeps it out of the label and index namespaces).
1619fn run_key(key: Key, n: usize) -> Key {
1620 key.str("\u{1}run").index(n as u64)
1621}
1622
1623fn empty_run(key: Key, n: usize, line: usize, rect: Rect, newline: bool) -> AccessRun {
1624 AccessRun {
1625 key: run_key(key, n),
1626 line,
1627 start: 0,
1628 end: 0,
1629 text: if newline { "\n".into() } else { String::new() },
1630 rect: Rect::new(rect.x, rect.y, 0.0, rect.h),
1631 char_lengths: if newline { vec![1] } else { Vec::new() },
1632 char_positions: if newline { vec![0.0] } else { Vec::new() },
1633 char_widths: if newline { vec![0.0] } else { Vec::new() },
1634 word_starts: Vec::new(),
1635 rtl: false,
1636 }
1637}
1638
1639/// The runs of a laid-out buffer (one per visual line, split past
1640/// [`RUN_CHARS`] characters), appended to `out`. Only laid-out lines
1641/// produce runs: a long document's off-screen lines are in the value
1642/// but not walked.
1643///
1644/// `shift` is where a buffer line's runs are drawn from where the buffer
1645/// lays them, physical px. A line that goes on into the next without a
1646/// line ending — a paragraph a text's initial cut (backlog F164) — is
1647/// one source line with it: its rows end in no newline, and the next
1648/// line's bytes count on from where it ends.
1649pub(crate) fn runs_of_buffer(
1650 buffer: &Buffer,
1651 src: RunSource,
1652 run_no: &mut usize,
1653 out: &mut Vec<AccessRun>,
1654 shift: impl Fn(usize) -> (f32, f32),
1655) {
1656 let line_count = buffer.lines.len();
1657 // Each buffer line's source line and where in it the line starts.
1658 let mut at = Vec::with_capacity(line_count);
1659 let (mut line, mut base) = (0usize, 0usize);
1660 for (i, l) in buffer.lines.iter().enumerate() {
1661 at.push((line, base));
1662 if crate::initial::joined(buffer, i) {
1663 base += l.text().len();
1664 } else {
1665 (line, base) = (line + 1, 0);
1666 }
1667 }
1668 let runs: Vec<_> = buffer.layout_runs().collect();
1669 for (r, run) in runs.iter().enumerate() {
1670 let last_of_line = runs.get(r + 1).is_none_or(|next| next.line_i != run.line_i);
1671 let newline = last_of_line
1672 && !crate::initial::joined(buffer, run.line_i)
1673 && (run.line_i + 1 < line_count || src.newline_after_last);
1674 let (dx, dy) = shift(run.line_i);
1675 let (line, base) = at[run.line_i];
1676 push_row_runs(
1677 &RowGlyphs {
1678 glyphs: run.glyphs,
1679 text: run.text,
1680 line,
1681 top: run.line_top + dy,
1682 height: run.line_height,
1683 rtl: run.rtl,
1684 dx,
1685 base,
1686 newline,
1687 },
1688 &src,
1689 run_no,
1690 out,
1691 );
1692 }
1693}
1694
1695/// One visual row's glyphs, for [`push_row_runs`]: a buffer's laid-out
1696/// run, or one row of a long line's chunk (`TextSystem::access_runs`),
1697/// whose glyphs sit `dx` physical px from where its unwrapped run put
1698/// them and whose `text` starts `base` bytes into the source's.
1699pub(crate) struct RowGlyphs<'a> {
1700 pub glyphs: &'a [cosmic_text::LayoutGlyph],
1701 /// The paragraph the glyphs index into.
1702 pub text: &'a str,
1703 /// The source's line this row belongs to, less `RunSource::line`.
1704 pub line: usize,
1705 pub top: f32,
1706 pub height: f32,
1707 pub rtl: bool,
1708 pub dx: f32,
1709 pub base: usize,
1710 /// Whether the row ends a line that continues into another.
1711 pub newline: bool,
1712}
1713
1714/// The runs of one visual row (split past [`RUN_CHARS`] characters),
1715/// appended to `out`.
1716pub(crate) fn push_row_runs(
1717 row: &RowGlyphs<'_>,
1718 src: &RunSource,
1719 run_no: &mut usize,
1720 out: &mut Vec<AccessRun>,
1721) {
1722 let scale = src.scale.max(f32::EPSILON);
1723 let newline = row.newline;
1724 {
1725 // The slice of the line this row lays out.
1726 let (start, end) = row.glyphs.iter().fold((usize::MAX, 0usize), |(s, e), g| {
1727 (s.min(g.start), e.max(g.end))
1728 });
1729 let (start, end) = if row.glyphs.is_empty() {
1730 (0, 0)
1731 } else {
1732 (start, end)
1733 };
1734 let slice = &row.text[start..end];
1735 // Where the run starts: its leftmost glyph (0 for an empty line).
1736 let x0 = row
1737 .glyphs
1738 .iter()
1739 .map(|g| g.x + row.dx)
1740 .fold(f32::INFINITY, f32::min);
1741 let x0 = if x0.is_finite() { x0 } else { 0.0 };
1742 // Every character: position and width from the glyph cluster
1743 // covering it (split evenly by bytes inside a cluster).
1744 let mut chars: Vec<(usize, u8, f32, f32)> = Vec::new(); // (byte, len, x, w) physical
1745 for (rel, c) in slice.char_indices() {
1746 let idx = start + rel;
1747 let len = c.len_utf8();
1748 let (x, w) = match row.glyphs.iter().find(|g| g.start <= idx && idx < g.end) {
1749 Some(g) => {
1750 let span = (g.end - g.start).max(1) as f32;
1751 (
1752 g.x + row.dx + g.w * (idx - g.start) as f32 / span,
1753 g.w * len as f32 / span,
1754 )
1755 }
1756 None => chars.last().map_or((x0, 0.0), |&(_, _, x, w)| (x + w, 0.0)),
1757 };
1758 chars.push((idx, len as u8, x, w));
1759 }
1760 let line_end_x = chars.last().map_or(x0, |&(_, _, x, w)| x + w);
1761 if newline {
1762 chars.push((end, 1, line_end_x, 0.0));
1763 }
1764 // Word starts over the run's text (the newline is never one).
1765 let starts: Vec<usize> = slice
1766 .split_word_bound_indices()
1767 .filter(|(_, w)| !w.chars().all(char::is_whitespace))
1768 .map(|(b, _)| slice[..b].chars().count())
1769 .collect();
1770 // Chunk so character indices fit a byte.
1771 let mut at = 0usize;
1772 loop {
1773 let chunk_end = (at + RUN_CHARS).min(chars.len());
1774 let chunk = &chars[at..chunk_end];
1775 let chunk_x = chunk.first().map_or(x0, |c| c.2);
1776 let chunk_end_x = chunk.last().map_or(chunk_x, |c| c.2 + c.3);
1777 let byte_start = chunk.first().map_or(end, |c| c.0);
1778 let byte_end = chunk
1779 .last()
1780 .map_or(end, |c| {
1781 if newline && c.0 == end {
1782 end
1783 } else {
1784 c.0 + c.1 as usize
1785 }
1786 })
1787 .min(end);
1788 let mut text = row.text[byte_start.min(end)..byte_end].to_string();
1789 if newline && chunk_end == chars.len() {
1790 text.push('\n');
1791 }
1792 out.push(AccessRun {
1793 key: run_key(src.key, *run_no),
1794 line: src.line + row.line,
1795 start: src.byte_base + row.base + byte_start.min(end),
1796 end: src.byte_base + row.base + byte_end,
1797 text,
1798 rect: Rect::new(
1799 src.origin.x + chunk_x / scale,
1800 src.origin.y + row.top / scale,
1801 (chunk_end_x - chunk_x) / scale,
1802 row.height / scale,
1803 ),
1804 char_lengths: chunk.iter().map(|c| c.1).collect(),
1805 char_positions: chunk.iter().map(|c| (c.2 - chunk_x) / scale).collect(),
1806 char_widths: chunk.iter().map(|c| c.3 / scale).collect(),
1807 word_starts: starts
1808 .iter()
1809 .filter(|&&s| s >= at && s < chunk_end)
1810 .map(|&s| (s - at) as u8)
1811 .collect(),
1812 rtl: row.rtl,
1813 });
1814 *run_no += 1;
1815 at = chunk_end;
1816 if at >= chars.len() {
1817 break;
1818 }
1819 }
1820 }
1821}
1822
1823fn hash_of(tree: &AccessTree) -> u64 {
1824 let mut h = crate::key::FNV_OFFSET;
1825 let mut mix = |bytes: &[u8]| h = crate::key::fnv(h, bytes);
1826 let mix_str = |mix: &mut dyn FnMut(&[u8]), s: &Option<String>| match s {
1827 Some(s) => {
1828 mix(&[1]);
1829 mix(s.as_bytes());
1830 mix(&[0]);
1831 }
1832 None => mix(&[0]),
1833 };
1834 let mix_f32 = |mix: &mut dyn FnMut(&[u8]), v: Option<f32>| match v {
1835 Some(v) => {
1836 mix(&[1]);
1837 mix(&v.to_bits().to_le_bytes());
1838 }
1839 None => mix(&[0]),
1840 };
1841 let mix_pos = |mix: &mut dyn FnMut(&[u8]), p: Option<TextPos>| match p {
1842 Some(p) => {
1843 mix(&p.run.0.to_le_bytes());
1844 mix(&p.character.to_le_bytes());
1845 }
1846 None => mix(&[0]),
1847 };
1848 for n in &tree.nodes {
1849 mix(&n.key.0.to_le_bytes());
1850 mix(&n.parent.map_or(0, |k| k.0).to_le_bytes());
1851 mix(&[n.role as u8]);
1852 mix_str(&mut mix, &n.name);
1853 mix_str(&mut mix, &n.description);
1854 for v in [n.rect.x, n.rect.y, n.rect.w, n.rect.h] {
1855 mix(&v.to_bits().to_le_bytes());
1856 }
1857 mix_str(&mut mix, &n.value);
1858 mix(&n.caret.unwrap_or(usize::MAX).to_le_bytes());
1859 let (a, b) = n.selection.unwrap_or((usize::MAX, usize::MAX));
1860 mix(&a.to_le_bytes());
1861 mix(&b.to_le_bytes());
1862 for r in &n.runs {
1863 mix(&r.key.0.to_le_bytes());
1864 mix(r.text.as_bytes());
1865 mix(&[0]);
1866 for v in [r.rect.x, r.rect.y, r.rect.w, r.rect.h] {
1867 mix(&v.to_bits().to_le_bytes());
1868 }
1869 for v in &r.char_positions {
1870 mix(&v.to_bits().to_le_bytes());
1871 }
1872 }
1873 mix_pos(&mut mix, n.anchor);
1874 mix_pos(&mut mix, n.focus);
1875 mix(&[n.checked.map_or(2, |c| c as u8), n.mixed as u8]);
1876 mix(&n.step.map_or(u32::MAX, f32::to_bits).to_le_bytes());
1877 mix(&[n.selected.map_or(2, |c| c as u8)]);
1878 mix(&[n.expanded.map_or(2, |c| c as u8)]);
1879 mix(&n.pos_in_set.unwrap_or(usize::MAX).to_le_bytes());
1880 mix(&n.set_size.unwrap_or(usize::MAX).to_le_bytes());
1881 mix_f32(&mut mix, n.number);
1882 mix_f32(&mut mix, n.min);
1883 mix_f32(&mut mix, n.max);
1884 mix(&[n.focused as u8, n.disabled as u8, n.modal as u8]);
1885 if let Some(s) = n.scroll {
1886 for v in [s.x, s.y, s.max_x, s.max_y] {
1887 mix(&v.to_bits().to_le_bytes());
1888 }
1889 }
1890 mix(&n.actions.to_le_bytes());
1891 mix(&[n.live as u8]);
1892 }
1893 mix(&tree.focus.map_or(0, |k| k.0).to_le_bytes());
1894 h
1895}
1896
1897/// The label type on `NodeSpec`: shared, so cloning a spec is a refcount
1898/// bump and a Rust view can keep one `Arc<str>` across frames.
1899pub type Label = Arc<str>;
1900
1901#[cfg(test)]
1902mod tests {
1903 use super::*;
1904
1905 #[test]
1906 fn role_names_round_trip() {
1907 for r in Role::ALL {
1908 assert_eq!(Role::parse(r.name()), Some(r));
1909 }
1910 assert_eq!(Role::parse("nope"), None);
1911 }
1912
1913 #[test]
1914 fn action_bits_are_distinct_and_named() {
1915 let mut seen = 0u32;
1916 for a in AccessAction::ALL {
1917 assert_eq!(seen & a.bit(), 0);
1918 seen |= a.bit();
1919 assert_eq!(AccessAction::parse(a.name()), Some(a));
1920 }
1921 }
1922
1923 #[test]
1924 fn run_keys_never_collide_with_node_keys() {
1925 let k = Key::ROOT.str("editor");
1926 assert_ne!(run_key(k, 0), k.index(0));
1927 assert_ne!(run_key(k, 0), k.str("run"));
1928 assert_ne!(run_key(k, 0), run_key(k, 1));
1929 }
1930}