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.placeholder(edit_key).hash(&mut h);
1179                src.edit.caret_and_selection(edit_key).hash(&mut h);
1180                src.edit.selection_cursors(edit_key).hash(&mut h);
1181                // `runs` is the editor's shaped layout, which the text and
1182                // the box above do not fully determine — a font arriving
1183                // between frames reshapes it. The store's own version bumps
1184                // on every mutation that could, so it stands in for reading
1185                // the runs, which would cost what building them costs.
1186                src.edit.version(edit_key).hash(&mut h);
1187                let clip = edit_run_clip(tree, src, i, edit_key);
1188                for v in [clip.x, clip.y, clip.w, clip.h] {
1189                    f(&mut h, v);
1190                }
1191            }
1192            NodeContent::Cells(id) => src.cells.value(id).hash(&mut h),
1193            _ => {}
1194        }
1195
1196        // Scrolling: the offset is retained state, the max a layout output.
1197        if spec.layout.scroll_x || spec.layout.scroll_y {
1198            spec.layout.scroll_x.hash(&mut h);
1199            spec.layout.scroll_y.hash(&mut h);
1200            let off = src.scroll.drawn(key);
1201            let max = tree.scroll_max[i];
1202            for v in [off.x, off.y, max.x, max.y] {
1203                f(&mut h, v);
1204            }
1205        }
1206
1207        if sem.presentational {
1208            skip_until = tree.subtree_end(i);
1209        }
1210        i += 1;
1211    }
1212    Some(h.finish())
1213}
1214
1215/// Derives the access tree of a laid-out frame.
1216pub(crate) fn build(tree: &Tree, src: &Sources<'_>) -> AccessTree {
1217    let mut out = AccessTree::default();
1218    if tree.is_empty() {
1219        return out;
1220    }
1221    // Per tree node: the key of the nearest semantic ancestor (for elided
1222    // nodes, inherited from the parent).
1223    let mut parents: Vec<Option<Key>> = vec![None; tree.len()];
1224    let mut skip_until = 0usize;
1225    let mut i = 0usize;
1226    while i < tree.len() {
1227        if i < skip_until {
1228            i += 1;
1229            continue;
1230        }
1231        let parent = tree.parent[i];
1232        let inherited = if parent == NIL {
1233            None
1234        } else {
1235            parents[parent as usize]
1236        };
1237        let Some(sem) = semantic(tree, src.text, src.edit, src.title, i) else {
1238            parents[i] = inherited;
1239            i += 1;
1240            continue;
1241        };
1242        if sem.role == Role::None {
1243            skip_until = tree.subtree_end(i);
1244            i += 1;
1245            continue;
1246        }
1247        let key = tree.keys[i];
1248        parents[i] = Some(key);
1249        let spec: &NodeSpec = &tree.specs[i];
1250        let rect = node_rect(tree, src, i);
1251        let mut node = AccessNode {
1252            key,
1253            parent: inherited,
1254            origin: tree.origins[i],
1255            role: sem.role,
1256            name: sem.name,
1257            description: spec.access().description.as_ref().map(|d| d.to_string()),
1258            rect,
1259            value: None,
1260            caret: None,
1261            selection: None,
1262            runs: Vec::new(),
1263            anchor: None,
1264            focus: None,
1265            checked: None,
1266            mixed: false,
1267            selected: None,
1268            expanded: None,
1269            orientation: crate::composite::orientation(tree, i),
1270            pos_in_set: None,
1271            set_size: None,
1272            number: None,
1273            min: None,
1274            max: None,
1275            step: None,
1276            focused: src.focus == Some(key),
1277            disabled: spec.disabled,
1278            modal: src.modal == Some(key),
1279            scroll: None,
1280            actions: 0,
1281            live: spec.access().live,
1282        };
1283        let mut actions = 0u32;
1284        match spec.window {
1285            Some(WindowRole::Button(_)) => actions |= AccessAction::Click.bit(),
1286            Some(WindowRole::Drag) => {}
1287            None => {
1288                if spec.events().on_click.is_some() && !spec.disabled {
1289                    actions |= AccessAction::Click.bit();
1290                }
1291            }
1292        }
1293        // Anything the keyboard can reach, assistive technology can focus
1294        // (and AccessKit calls focusable only what supports `Focus`).
1295        if focusable(tree, i) {
1296            actions |= AccessAction::Focus.bit() | AccessAction::Blur.bit();
1297        }
1298        if let NodeContent::Edit(edit_key) = tree.content[i] {
1299            let pad = spec.layout.padding;
1300            let origin = Vec2::new(tree.pos[i].x + pad.l, tree.pos[i].y + pad.t);
1301            node.value = src.edit.text(edit_key);
1302            // A placeholder is what the field is for, said where the field
1303            // says nothing of its own (backlog F149).
1304            if node.description.is_none() {
1305                node.description = src.edit.placeholder(edit_key).map(str::to_string);
1306            }
1307            node.runs = src.edit.runs(edit_key, key, origin, src.scale);
1308            clip_runs(&mut node.runs, edit_run_clip(tree, src, i, edit_key));
1309            if let Some((caret, selection)) = src.edit.caret_and_selection(edit_key) {
1310                node.caret = Some(caret);
1311                node.selection = selection;
1312            }
1313            if let Some((anchor, focus)) = src.edit.selection_cursors(edit_key) {
1314                node.anchor = node.text_pos(anchor.0, anchor.1);
1315                node.focus = node.text_pos(focus.0, focus.1);
1316            }
1317            if !spec.disabled {
1318                actions |= AccessAction::Click.bit()
1319                    | AccessAction::SetValue.bit()
1320                    | AccessAction::SetTextSelection.bit()
1321                    | AccessAction::ReplaceSelectedText.bit();
1322            }
1323        } else if is_custom_editor(tree, i) {
1324            custom_editor(tree, src, i, &mut node);
1325            if !spec.disabled {
1326                actions |= AccessAction::SetValue.bit()
1327                    | AccessAction::SetTextSelection.bit()
1328                    | AccessAction::ReplaceSelectedText.bit();
1329            }
1330        } else if let NodeContent::Cells(id) = tree.content[i] {
1331            // A terminal's value is its screen, rows joined.
1332            node.value = Some(src.cells.value(id));
1333        }
1334        // A disclosure names its own state, so it lands wherever it is
1335        // declared: no role means "shows and hides something".
1336        let ax = spec.access();
1337        node.expanded = ax.expanded;
1338        match sem.role {
1339            Role::Checkbox | Role::Radio | Role::Switch => {
1340                node.checked = Some(ax.checked);
1341                node.mixed = ax.mixed && sem.role == Role::Checkbox;
1342            }
1343            // A tab is one of a set by definition, so it reports either
1344            // state; a row or a link reports only the one it declares,
1345            // since most lists and every navigation bar are not
1346            // selections and "not selected" on each of their nodes is the
1347            // noise AccessKit warns about.
1348            Role::Tab => node.selected = Some(ax.selected),
1349            Role::ListItem | Role::Link if ax.selected => node.selected = Some(true),
1350            // A menu row that is a setting reads as checked (the drawn
1351            // menu's checkmark, `widgets::menu_panel`); every other row is
1352            // a command and says nothing, the way a list row says nothing
1353            // about a selection it is not part of. Before this the widget
1354            // declared the fact and the tree dropped it, so a reader heard
1355            // "Wrap" where a sighted user saw "✓ Wrap".
1356            Role::MenuItem if ax.checked => node.checked = Some(true),
1357            Role::Slider => {
1358                node.number = ax.value_now;
1359                node.min = ax.value_min;
1360                node.max = ax.value_max;
1361                node.step = ax.value_step;
1362                // The declared reading, in the one string slot the
1363                // platform gives a node (see `value`). Slider-only, like
1364                // the three numbers: a `group` shaped like a progress bar
1365                // carries none of them either, and a role whose numbers
1366                // are ignored should not have a reading that is not.
1367                node.value = ax.value_text.as_deref().map(str::to_owned);
1368                // SetValue too: Windows' UI Automation has no increment,
1369                // and moves a slider only by setting it.
1370                if !spec.disabled {
1371                    actions |= AccessAction::Increment.bit()
1372                        | AccessAction::Decrement.bit()
1373                        | AccessAction::SetValue.bit();
1374                }
1375            }
1376            _ => {}
1377        }
1378        if spec.layout.scroll_x || spec.layout.scroll_y {
1379            let off = src.scroll.drawn(key);
1380            let max = tree.scroll_max[i];
1381            node.scroll = Some(ScrollState {
1382                x: off.x,
1383                y: off.y,
1384                max_x: max.x,
1385                max_y: max.y,
1386            });
1387            if spec.layout.scroll_y {
1388                actions |= AccessAction::ScrollUp.bit() | AccessAction::ScrollDown.bit();
1389            }
1390            if spec.layout.scroll_x {
1391                actions |= AccessAction::ScrollLeft.bit() | AccessAction::ScrollRight.bit();
1392            }
1393        }
1394        if i != 0 {
1395            actions |= AccessAction::ScrollIntoView.bit();
1396        }
1397        node.actions = actions;
1398        if node.focused {
1399            out.focus = Some(key);
1400        }
1401        out.nodes.push(node);
1402        if sem.presentational {
1403            skip_until = tree.subtree_end(i);
1404        }
1405        i += 1;
1406    }
1407    set_positions(tree, &mut out);
1408    out.hash = hash_of(&out);
1409    out
1410}
1411
1412/// "3 of 7", derived rather than declared: a composite container already
1413/// holds its items, so the core counts them and numbers each one instead
1414/// of making every view repeat itself (and get it wrong the moment a row
1415/// is filtered out). The count lands on the container and the zero-based
1416/// ordinal on the item, which is how AccessKit models a set.
1417///
1418/// The items come from `composite::items`, which is also the order the
1419/// arrow keys walk: "3 of 7" and that walk must be the same seven in the same
1420/// order or the announcement is a lie, so they are one function rather
1421/// than two that agree by inspection. A disabled item is still one of the
1422/// set — "2 of 3" is what a reader should hear on a disabled tab — even
1423/// though the arrows step over it, as the Tab ring does.
1424fn set_positions(tree: &Tree, out: &mut AccessTree) {
1425    let containers: Vec<(usize, Role)> = (0..tree.len())
1426        .filter_map(|i| {
1427            let item = crate::composite::item_role(tree.specs[i].access().role?)?;
1428            Some((i, item))
1429        })
1430        .collect();
1431    let mut items: Vec<usize> = Vec::new();
1432    for (container, item) in containers {
1433        crate::composite::items(tree, container, item, &mut items);
1434        if items.is_empty() {
1435            continue;
1436        }
1437        let size = items.len();
1438        let container = tree.keys[container];
1439        let keys: Vec<Key> = items.iter().map(|&i| tree.keys[i]).collect();
1440        for n in &mut out.nodes {
1441            if n.key == container {
1442                n.set_size = Some(size);
1443            } else if let Some(pos) = keys.iter().position(|k| *k == n.key) {
1444                n.pos_in_set = Some(pos);
1445            }
1446        }
1447    }
1448}
1449
1450/// The `role="line"` nodes under `i`, in tree order — the numbering a
1451/// custom editor's `access` events use for `line`, and the one a press
1452/// inside a key sink carries, so the two agree. Subtrees
1453/// under `role="none"` (a gutter) do not count, and a line's own subtree
1454/// is not searched for lines.
1455pub(crate) fn lines_under(tree: &Tree, i: usize) -> Vec<usize> {
1456    let end = tree.subtree_end(i);
1457    let mut lines: Vec<usize> = Vec::new();
1458    let mut j = i + 1;
1459    while j < end {
1460        let spec = &tree.specs[j];
1461        if spec.access().role == Some(Role::None) {
1462            j = tree.subtree_end(j);
1463            continue;
1464        }
1465        if spec.access().role == Some(Role::Line) {
1466            lines.push(j);
1467            j = tree.subtree_end(j);
1468            continue;
1469        }
1470        j += 1;
1471    }
1472    lines
1473}
1474
1475/// A custom editor's text: its `role="line"` descendants in order, each
1476/// line the text nodes inside it concatenated, runs from their buffers,
1477/// caret and anchor from the lines that declare them. Subtrees under
1478/// `role="none"` (a gutter) do not count.
1479fn custom_editor(tree: &Tree, src: &Sources<'_>, i: usize, node: &mut AccessNode) {
1480    let lines = lines_under(tree, i);
1481    let mut value = String::new();
1482    let mut run_no = 0usize;
1483    let mut caret: Option<(usize, usize)> = None;
1484    let mut anchor: Option<(usize, usize)> = None;
1485    for (ln, &l) in lines.iter().enumerate() {
1486        let last = ln + 1 == lines.len();
1487        let mut line_text = String::new();
1488        let mut texts: Vec<usize> = Vec::new();
1489        let lend = tree.subtree_end(l);
1490        let mut t = l;
1491        while t < lend {
1492            if tree.specs[t].access().role == Some(Role::None) {
1493                t = tree.subtree_end(t);
1494                continue;
1495            }
1496            if let NodeContent::Text(_) = tree.content[t] {
1497                texts.push(t);
1498            }
1499            t += 1;
1500        }
1501        for (k, &t) in texts.iter().enumerate() {
1502            let NodeContent::Text(id) = tree.content[t] else {
1503                continue;
1504            };
1505            let base = line_text.len();
1506            line_text.push_str(src.text.content(id));
1507            let newline = !last && k + 1 == texts.len();
1508            let first = node.runs.len();
1509            src.text.access_runs(
1510                id,
1511                RunSource {
1512                    key: node.key,
1513                    line: ln,
1514                    byte_base: base,
1515                    origin: tree.pos[t],
1516                    scale: src.scale,
1517                    newline_after_last: newline,
1518                },
1519                &mut run_no,
1520                &mut node.runs,
1521            );
1522            clip_runs(&mut node.runs[first..], clip_of(src, t));
1523        }
1524        if texts.is_empty() {
1525            // An empty line still has a place for the caret.
1526            node.runs.push(empty_run(
1527                node.key,
1528                run_no,
1529                ln,
1530                Rect::from_pos_size(tree.pos[l], tree.size[l]),
1531                !last,
1532            ));
1533            let at = node.runs.len() - 1;
1534            clip_runs(&mut node.runs[at..], clip_of(src, l));
1535            run_no += 1;
1536        }
1537        if let Some(c) = tree.specs[l].access().caret {
1538            caret = Some((ln, (c as usize).min(line_text.len())));
1539        }
1540        if let Some(a) = tree.specs[l].access().selection_anchor {
1541            anchor = Some((ln, (a as usize).min(line_text.len())));
1542        }
1543        if ln > 0 {
1544            value.push('\n');
1545        }
1546        value.push_str(&line_text);
1547    }
1548    node.value = Some(value);
1549    if let Some((line, offset)) = caret {
1550        node.focus = node.text_pos(line, offset);
1551        let anchor = anchor.unwrap_or((line, offset));
1552        node.anchor = node.text_pos(anchor.0, anchor.1);
1553        // Byte offsets into the joined value, for the flat view of it.
1554        let flat = |(line, offset): (usize, usize)| {
1555            node.value
1556                .as_ref()
1557                .map(|v| v.split('\n').take(line).map(|l| l.len() + 1).sum::<usize>() + offset)
1558        };
1559        node.caret = flat((line, offset));
1560        if anchor != (line, offset)
1561            && let (Some(a), Some(c)) = (flat(anchor), node.caret)
1562        {
1563            node.selection = Some((a.min(c), a.max(c)));
1564        }
1565    }
1566}
1567
1568/// Where a buffer's runs belong: which line and byte base of the editor
1569/// they land in, and where the buffer draws.
1570pub(crate) struct RunSource {
1571    pub key: Key,
1572    /// The line index the buffer's first line maps to.
1573    pub line: usize,
1574    /// Byte offset of the buffer's text inside that line (a line drawn as
1575    /// several text nodes).
1576    pub byte_base: usize,
1577    /// Where the buffer's (0, 0) sits, logical px, viewport coordinates.
1578    pub origin: Vec2,
1579    pub scale: f32,
1580    /// Whether the buffer's last line continues into another line of the
1581    /// editor (its last run then ends with `"\n"`).
1582    pub newline_after_last: bool,
1583}
1584
1585/// The id of run `n` of editor `key`; never a node key (the tag byte
1586/// keeps it out of the label and index namespaces).
1587fn run_key(key: Key, n: usize) -> Key {
1588    key.str("\u{1}run").index(n as u64)
1589}
1590
1591fn empty_run(key: Key, n: usize, line: usize, rect: Rect, newline: bool) -> AccessRun {
1592    AccessRun {
1593        key: run_key(key, n),
1594        line,
1595        start: 0,
1596        end: 0,
1597        text: if newline { "\n".into() } else { String::new() },
1598        rect: Rect::new(rect.x, rect.y, 0.0, rect.h),
1599        char_lengths: if newline { vec![1] } else { Vec::new() },
1600        char_positions: if newline { vec![0.0] } else { Vec::new() },
1601        char_widths: if newline { vec![0.0] } else { Vec::new() },
1602        word_starts: Vec::new(),
1603        rtl: false,
1604    }
1605}
1606
1607/// The runs of a laid-out buffer (one per visual line, split past
1608/// [`RUN_CHARS`] characters), appended to `out`. Only laid-out lines
1609/// produce runs: a long document's off-screen lines are in the value
1610/// but not walked.
1611pub(crate) fn runs_of_buffer(
1612    buffer: &Buffer,
1613    src: RunSource,
1614    run_no: &mut usize,
1615    out: &mut Vec<AccessRun>,
1616) {
1617    let line_count = buffer.lines.len();
1618    let runs: Vec<_> = buffer.layout_runs().collect();
1619    for (r, run) in runs.iter().enumerate() {
1620        let last_of_line = runs.get(r + 1).is_none_or(|next| next.line_i != run.line_i);
1621        let newline = last_of_line && (run.line_i + 1 < line_count || src.newline_after_last);
1622        push_row_runs(
1623            &RowGlyphs {
1624                glyphs: run.glyphs,
1625                text: run.text,
1626                line: run.line_i,
1627                top: run.line_top,
1628                height: run.line_height,
1629                rtl: run.rtl,
1630                dx: 0.0,
1631                base: 0,
1632                newline,
1633            },
1634            &src,
1635            run_no,
1636            out,
1637        );
1638    }
1639}
1640
1641/// One visual row's glyphs, for [`push_row_runs`]: a buffer's laid-out
1642/// run, or one row of a long line's chunk (`TextSystem::access_runs`),
1643/// whose glyphs sit `dx` physical px from where its unwrapped run put
1644/// them and whose `text` starts `base` bytes into the source's.
1645pub(crate) struct RowGlyphs<'a> {
1646    pub glyphs: &'a [cosmic_text::LayoutGlyph],
1647    /// The paragraph the glyphs index into.
1648    pub text: &'a str,
1649    /// The source's line this row belongs to, less `RunSource::line`.
1650    pub line: usize,
1651    pub top: f32,
1652    pub height: f32,
1653    pub rtl: bool,
1654    pub dx: f32,
1655    pub base: usize,
1656    /// Whether the row ends a line that continues into another.
1657    pub newline: bool,
1658}
1659
1660/// The runs of one visual row (split past [`RUN_CHARS`] characters),
1661/// appended to `out`.
1662pub(crate) fn push_row_runs(
1663    row: &RowGlyphs<'_>,
1664    src: &RunSource,
1665    run_no: &mut usize,
1666    out: &mut Vec<AccessRun>,
1667) {
1668    let scale = src.scale.max(f32::EPSILON);
1669    let newline = row.newline;
1670    {
1671        // The slice of the line this row lays out.
1672        let (start, end) = row.glyphs.iter().fold((usize::MAX, 0usize), |(s, e), g| {
1673            (s.min(g.start), e.max(g.end))
1674        });
1675        let (start, end) = if row.glyphs.is_empty() {
1676            (0, 0)
1677        } else {
1678            (start, end)
1679        };
1680        let slice = &row.text[start..end];
1681        // Where the run starts: its leftmost glyph (0 for an empty line).
1682        let x0 = row
1683            .glyphs
1684            .iter()
1685            .map(|g| g.x + row.dx)
1686            .fold(f32::INFINITY, f32::min);
1687        let x0 = if x0.is_finite() { x0 } else { 0.0 };
1688        // Every character: position and width from the glyph cluster
1689        // covering it (split evenly by bytes inside a cluster).
1690        let mut chars: Vec<(usize, u8, f32, f32)> = Vec::new(); // (byte, len, x, w) physical
1691        for (rel, c) in slice.char_indices() {
1692            let idx = start + rel;
1693            let len = c.len_utf8();
1694            let (x, w) = match row.glyphs.iter().find(|g| g.start <= idx && idx < g.end) {
1695                Some(g) => {
1696                    let span = (g.end - g.start).max(1) as f32;
1697                    (
1698                        g.x + row.dx + g.w * (idx - g.start) as f32 / span,
1699                        g.w * len as f32 / span,
1700                    )
1701                }
1702                None => chars.last().map_or((x0, 0.0), |&(_, _, x, w)| (x + w, 0.0)),
1703            };
1704            chars.push((idx, len as u8, x, w));
1705        }
1706        let line_end_x = chars.last().map_or(x0, |&(_, _, x, w)| x + w);
1707        if newline {
1708            chars.push((end, 1, line_end_x, 0.0));
1709        }
1710        // Word starts over the run's text (the newline is never one).
1711        let starts: Vec<usize> = slice
1712            .split_word_bound_indices()
1713            .filter(|(_, w)| !w.chars().all(char::is_whitespace))
1714            .map(|(b, _)| slice[..b].chars().count())
1715            .collect();
1716        // Chunk so character indices fit a byte.
1717        let mut at = 0usize;
1718        loop {
1719            let chunk_end = (at + RUN_CHARS).min(chars.len());
1720            let chunk = &chars[at..chunk_end];
1721            let chunk_x = chunk.first().map_or(x0, |c| c.2);
1722            let chunk_end_x = chunk.last().map_or(chunk_x, |c| c.2 + c.3);
1723            let byte_start = chunk.first().map_or(end, |c| c.0);
1724            let byte_end = chunk
1725                .last()
1726                .map_or(end, |c| {
1727                    if newline && c.0 == end {
1728                        end
1729                    } else {
1730                        c.0 + c.1 as usize
1731                    }
1732                })
1733                .min(end);
1734            let mut text = row.text[byte_start.min(end)..byte_end].to_string();
1735            if newline && chunk_end == chars.len() {
1736                text.push('\n');
1737            }
1738            out.push(AccessRun {
1739                key: run_key(src.key, *run_no),
1740                line: src.line + row.line,
1741                start: src.byte_base + row.base + byte_start.min(end),
1742                end: src.byte_base + row.base + byte_end,
1743                text,
1744                rect: Rect::new(
1745                    src.origin.x + chunk_x / scale,
1746                    src.origin.y + row.top / scale,
1747                    (chunk_end_x - chunk_x) / scale,
1748                    row.height / scale,
1749                ),
1750                char_lengths: chunk.iter().map(|c| c.1).collect(),
1751                char_positions: chunk.iter().map(|c| (c.2 - chunk_x) / scale).collect(),
1752                char_widths: chunk.iter().map(|c| c.3 / scale).collect(),
1753                word_starts: starts
1754                    .iter()
1755                    .filter(|&&s| s >= at && s < chunk_end)
1756                    .map(|&s| (s - at) as u8)
1757                    .collect(),
1758                rtl: row.rtl,
1759            });
1760            *run_no += 1;
1761            at = chunk_end;
1762            if at >= chars.len() {
1763                break;
1764            }
1765        }
1766    }
1767}
1768
1769fn hash_of(tree: &AccessTree) -> u64 {
1770    let mut h = crate::key::FNV_OFFSET;
1771    let mut mix = |bytes: &[u8]| h = crate::key::fnv(h, bytes);
1772    let mix_str = |mix: &mut dyn FnMut(&[u8]), s: &Option<String>| match s {
1773        Some(s) => {
1774            mix(&[1]);
1775            mix(s.as_bytes());
1776            mix(&[0]);
1777        }
1778        None => mix(&[0]),
1779    };
1780    let mix_f32 = |mix: &mut dyn FnMut(&[u8]), v: Option<f32>| match v {
1781        Some(v) => {
1782            mix(&[1]);
1783            mix(&v.to_bits().to_le_bytes());
1784        }
1785        None => mix(&[0]),
1786    };
1787    let mix_pos = |mix: &mut dyn FnMut(&[u8]), p: Option<TextPos>| match p {
1788        Some(p) => {
1789            mix(&p.run.0.to_le_bytes());
1790            mix(&p.character.to_le_bytes());
1791        }
1792        None => mix(&[0]),
1793    };
1794    for n in &tree.nodes {
1795        mix(&n.key.0.to_le_bytes());
1796        mix(&n.parent.map_or(0, |k| k.0).to_le_bytes());
1797        mix(&[n.role as u8]);
1798        mix_str(&mut mix, &n.name);
1799        mix_str(&mut mix, &n.description);
1800        for v in [n.rect.x, n.rect.y, n.rect.w, n.rect.h] {
1801            mix(&v.to_bits().to_le_bytes());
1802        }
1803        mix_str(&mut mix, &n.value);
1804        mix(&n.caret.unwrap_or(usize::MAX).to_le_bytes());
1805        let (a, b) = n.selection.unwrap_or((usize::MAX, usize::MAX));
1806        mix(&a.to_le_bytes());
1807        mix(&b.to_le_bytes());
1808        for r in &n.runs {
1809            mix(&r.key.0.to_le_bytes());
1810            mix(r.text.as_bytes());
1811            mix(&[0]);
1812            for v in [r.rect.x, r.rect.y, r.rect.w, r.rect.h] {
1813                mix(&v.to_bits().to_le_bytes());
1814            }
1815            for v in &r.char_positions {
1816                mix(&v.to_bits().to_le_bytes());
1817            }
1818        }
1819        mix_pos(&mut mix, n.anchor);
1820        mix_pos(&mut mix, n.focus);
1821        mix(&[n.checked.map_or(2, |c| c as u8), n.mixed as u8]);
1822        mix(&n.step.map_or(u32::MAX, f32::to_bits).to_le_bytes());
1823        mix(&[n.selected.map_or(2, |c| c as u8)]);
1824        mix(&[n.expanded.map_or(2, |c| c as u8)]);
1825        mix(&n.pos_in_set.unwrap_or(usize::MAX).to_le_bytes());
1826        mix(&n.set_size.unwrap_or(usize::MAX).to_le_bytes());
1827        mix_f32(&mut mix, n.number);
1828        mix_f32(&mut mix, n.min);
1829        mix_f32(&mut mix, n.max);
1830        mix(&[n.focused as u8, n.disabled as u8, n.modal as u8]);
1831        if let Some(s) = n.scroll {
1832            for v in [s.x, s.y, s.max_x, s.max_y] {
1833                mix(&v.to_bits().to_le_bytes());
1834            }
1835        }
1836        mix(&n.actions.to_le_bytes());
1837        mix(&[n.live as u8]);
1838    }
1839    mix(&tree.focus.map_or(0, |k| k.0).to_le_bytes());
1840    h
1841}
1842
1843/// The label type on `NodeSpec`: shared, so cloning a spec is a refcount
1844/// bump and a Rust view can keep one `Arc<str>` across frames.
1845pub type Label = Arc<str>;
1846
1847#[cfg(test)]
1848mod tests {
1849    use super::*;
1850
1851    #[test]
1852    fn role_names_round_trip() {
1853        for r in Role::ALL {
1854            assert_eq!(Role::parse(r.name()), Some(r));
1855        }
1856        assert_eq!(Role::parse("nope"), None);
1857    }
1858
1859    #[test]
1860    fn action_bits_are_distinct_and_named() {
1861        let mut seen = 0u32;
1862        for a in AccessAction::ALL {
1863            assert_eq!(seen & a.bit(), 0);
1864            seen |= a.bit();
1865            assert_eq!(AccessAction::parse(a.name()), Some(a));
1866        }
1867    }
1868
1869    #[test]
1870    fn run_keys_never_collide_with_node_keys() {
1871        let k = Key::ROOT.str("editor");
1872        assert_ne!(run_key(k, 0), k.index(0));
1873        assert_ne!(run_key(k, 0), k.str("run"));
1874        assert_ne!(run_key(k, 0), run_key(k, 1));
1875    }
1876}