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