Skip to main content

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}