Skip to main content

kui_core/
access.rs

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