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