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.
690    pub fn text_pos(&self, line: usize, offset: usize) -> Option<TextPos> {
691        let mut last = None;
692        for r in self.runs.iter().filter(|r| r.line == line) {
693            if offset >= r.start && offset < r.end {
694                return Some(TextPos {
695                    run: r.key,
696                    character: r.text[..offset - r.start].chars().count(),
697                });
698            }
699            last = Some(r);
700        }
701        let r = last?;
702        Some(TextPos {
703            run: r.key,
704            character: r.text[..(offset.max(r.start) - r.start).min(r.end - r.start)]
705                .chars()
706                .count(),
707        })
708    }
709}
710
711/// The semantic nodes of a finished frame, in tree order (a parent always
712/// precedes its descendants; the root comes first).
713#[derive(Clone, Debug, Default, PartialEq)]
714pub struct AccessTree {
715    pub nodes: Vec<AccessNode>,
716    /// The node holding keyboard focus, if any.
717    pub focus: Option<Key>,
718    /// A hash of everything above: a driver that sent the tree once sends
719    /// it again only when this changes.
720    pub hash: u64,
721}
722
723impl AccessTree {
724    /// `{nodes, focus, hash}`, every node by [`AccessNode::to_value`],
725    /// the focus key spelled by `h` and the hash as sixteen hex digits.
726    pub fn to_value(&self, h: Handles) -> Value {
727        Value::map([
728            (
729                "nodes",
730                Value::list(self.nodes.iter().map(|n| n.to_value(h))),
731            ),
732            ("focus", h.opt_key(self.focus)),
733            ("hash", Value::Str(format!("{:016x}", self.hash))),
734        ])
735    }
736
737    pub fn get(&self, key: Key) -> Option<&AccessNode> {
738        self.nodes.iter().find(|n| n.key == key)
739    }
740
741    /// The nodes whose `parent` is `key`, in order.
742    pub fn children(&self, key: Key) -> impl Iterator<Item = &AccessNode> {
743        self.nodes.iter().filter(move |n| n.parent == Some(key))
744    }
745
746    /// The root (the window), when a frame has been built.
747    pub fn root(&self) -> Option<&AccessNode> {
748        self.nodes.first()
749    }
750}
751
752/// What the tree walk decided for one node.
753pub(crate) struct Semantic {
754    pub role: Role,
755    pub name: Option<String>,
756    /// Descendants are read as part of this node, not as their own nodes.
757    pub presentational: bool,
758}
759
760/// The role a node's spec and content imply, before elision. None = plain
761/// structure (elided; its descendants still appear).
762pub(crate) fn derived_role(tree: &Tree, i: usize) -> Option<Role> {
763    let spec = &tree.specs[i];
764    if let Some(r) = spec.access().role {
765        // A line is structure of the editor around it; on its own it is
766        // nothing.
767        return (r != Role::Line).then_some(r);
768    }
769    if i == 0 {
770        return Some(Role::Window);
771    }
772    match tree.content[i] {
773        NodeContent::Text(_) => return Some(Role::StaticText),
774        NodeContent::Edit(_) => return Some(Role::TextInput),
775        NodeContent::Image(..) => return Some(Role::Image),
776        // A stroke or a fill is decoration on its own and elided like
777        // plain structure; one that takes input is hit by its shape, so
778        // the derivation below reaches it as it reaches a box —
779        // a clickable wedge is a button, a draggable connector a control.
780        NodeContent::Line(_) | NodeContent::Polygon(_) | NodeContent::Path(_) => {}
781        NodeContent::Cells(_) => return Some(Role::Terminal),
782        // A fragment is paint. On its own it is decoration and is elided
783        // like plain structure, but unlike a line it does take input, so
784        // the derivation below still reaches it: a fragment with an
785        // `on_click` is a button, and one that means something says so
786        // with its own `role` and `label`.
787        NodeContent::Fragment(_) => {}
788        NodeContent::Container => {}
789    }
790    match spec.window {
791        Some(WindowRole::Drag) => return Some(Role::TitleBar),
792        Some(WindowRole::Button(_)) => return Some(Role::Button),
793        None => {}
794    }
795    if spec.events().modal.is_some() {
796        // A modal surface is a dialog to assistive technology; anything
797        // else it might be, the view says with an explicit role.
798        return Some(Role::Dialog);
799    }
800    if spec.events().on_click.is_some() {
801        return Some(Role::Button);
802    }
803    if spec.layout.scroll_x || spec.layout.scroll_y {
804        return Some(Role::ScrollView);
805    }
806    if spec.events().on_key.is_some() || spec.focusable {
807        // A key sink or a focusable box is at least somewhere focus can
808        // land, so a reader has to be able to see it there.
809        return Some(Role::Group);
810    }
811    if spec.access().live != Live::Off {
812        // A live region that was elided would carry its liveness
813        // nowhere: its text would inherit the window's instead, and the
814        // change would go unread.
815        return Some(Role::Group);
816    }
817    None
818}
819
820/// Whether node `i` is an editor the app draws itself: an editor role on
821/// something other than a built-in edit node.
822pub(crate) fn is_custom_editor(tree: &Tree, i: usize) -> bool {
823    tree.specs[i].access().role.is_some_and(Role::is_editor)
824        && !matches!(tree.content[i], NodeContent::Edit(_))
825}
826
827/// Whether node `i` can hold keyboard focus: an editor, a key sink, a
828/// control role, a derived button (`on_click`), or a node declaring
829/// `focusable` — never a disabled node, decoration or window chrome. The
830/// Tab ring is these nodes in tree order; a `role="none"` subtree is
831/// skipped by the walk, not here.
832pub(crate) fn focusable(tree: &Tree, i: usize) -> bool {
833    let spec = &tree.specs[i];
834    if spec.disabled || spec.access().role == Some(Role::None) || spec.window.is_some() {
835        return false;
836    }
837    if spec.focusable
838        || spec.events().on_key.is_some()
839        || matches!(tree.content[i], NodeContent::Edit(_))
840    {
841        return true;
842    }
843    match spec.access().role {
844        Some(r) => r.is_control(),
845        None => spec.events().on_click.is_some(),
846    }
847}
848
849/// The role, name and presentation of node `i`, or None when it is plain
850/// structure. Shared by the tree build and the diagnostics check so both
851/// agree on what "unnamed" means.
852pub(crate) fn semantic(
853    tree: &Tree,
854    text: &TextSystem,
855    edit: &EditStore,
856    title: Option<&str>,
857    i: usize,
858) -> Option<Semantic> {
859    let mut role = derived_role(tree, i)?;
860    let spec = &tree.specs[i];
861    if role == Role::TextInput
862        && let NodeContent::Edit(key) = tree.content[i]
863        && edit.is_multiline(key)
864    {
865        role = Role::MultilineTextInput;
866    }
867    // Window buttons read as one control; the drag strip keeps its
868    // children (the buttons sit inside it). A custom editor's lines are
869    // its text, not children.
870    //
871    // A live region joins them: it reads as **one message**, named by the
872    // text inside it, and that is what changes when the message does. The
873    // alternative — the region carrying only liveness and each platform
874    // announcing the changed descendant — is what was first built,
875    // and macOS does not deliver it: `accesskit_macos` derives a live
876    // node's announcement from `NodeWrapper::label()`, which for a
877    // `Role::Label` reads the node's *value*, so a live static text
878    // announces nothing at all. One named node is also one announcement
879    // on all three platforms rather than one per live descendant.
880    let presentational = role.presentational()
881        || spec.access().live != Live::Off
882        || matches!(spec.window, Some(WindowRole::Button(_)))
883        || is_custom_editor(tree, i);
884    let name = match (&spec.access().label, spec.window) {
885        (Some(label), _) => Some(label.to_string()),
886        (None, Some(WindowRole::Button(b))) => Some(
887            match b {
888                WindowButton::Close => "Close",
889                WindowButton::Minimize => "Minimize",
890                WindowButton::Maximize => "Maximize",
891            }
892            .to_string(),
893        ),
894        (None, _) => match role {
895            Role::StaticText => match tree.content[i] {
896                NodeContent::Text(id) => Some(text.content(id).to_string()),
897                _ => None,
898            },
899            // The window carries the title; a drawn titlebar naming
900            // itself the same thing would have a screen reader read it
901            // twice (it keeps its children, so its own text is read).
902            Role::Window => title.map(str::to_string),
903            _ if role.presentational() || spec.access().live != Live::Off => {
904                content_name(tree, text, i)
905            }
906            _ => None,
907        },
908    };
909    Some(Semantic {
910        role,
911        name,
912        presentational,
913    })
914}
915
916/// Whether a live region has anything a reader could ever say: a `label`
917/// of its own, or text somewhere inside it (which is where the string
918/// actually comes from — liveness inherits, and the changed descendant is
919/// what gets announced). Shared with the diagnostics so both agree on
920/// what a silent live region is.
921pub(crate) fn live_region_speaks(tree: &Tree, text: &TextSystem, i: usize) -> bool {
922    tree.specs[i].access().label.is_some() || content_name(tree, text, i).is_some()
923}
924
925/// The text inside node `i`, in order, joined by spaces — ARIA's
926/// name-from-content. None when there is none.
927///
928/// A subtree under `role="none"` is not content, as it is not for a
929/// custom editor's lines ([`lines_under`]): it is hidden from assistive
930/// technology, and what it draws is not what the control is called. The
931/// `tooltip` prop's hint is one (`widgets::hover_hint`) — built only while
932/// the pointer is over the node, it made a hovered button's name its label
933/// and its hint both.
934fn content_name(tree: &Tree, text: &TextSystem, i: usize) -> Option<String> {
935    let end = tree.subtree_end(i);
936    let mut out = String::new();
937    let mut j = i;
938    while j < end {
939        if j > i && tree.specs[j].access().role == Some(Role::None) {
940            j = tree.subtree_end(j);
941            continue;
942        }
943        let at = j;
944        j += 1;
945        if let NodeContent::Text(id) = tree.content[at] {
946            let s = text.content(id).trim();
947            if s.is_empty() {
948                continue;
949            }
950            if !out.is_empty() {
951                out.push(' ');
952            }
953            out.push_str(s);
954        }
955    }
956    (!out.is_empty()).then_some(out)
957}
958
959/// Everything the build reads besides the tree.
960pub(crate) struct Sources<'a> {
961    pub text: &'a TextSystem,
962    pub cells: &'a crate::cells::CellStore,
963    pub edit: &'a EditStore,
964    pub scroll: &'a ScrollStore,
965    pub title: Option<&'a str>,
966    /// The core's one keyboard focus (see `Core::focus`).
967    pub focus: Option<Key>,
968    /// The frame's modal in effect (see `Core::modal`).
969    pub modal: Option<Key>,
970    pub viewport: Size,
971    pub scale: f32,
972    /// The clip each node was emitted under, by tree index: its
973    /// ancestors' only, and the one its hit region carries, so a float
974    /// that escapes has none and a `clip` float has its parent's.
975    /// Empty when the frame clipped nothing.
976    pub clips: &'a [Clip],
977}
978
979/// The clip node `i` was emitted under (see [`Sources::clips`]).
980fn clip_of(src: &Sources<'_>, i: usize) -> Rect {
981    src.clips.get(i).map_or(NO_CLIP, |c| c.rect)
982}
983
984/// `rect` cut to `clip`, so assistive technology finds and highlights
985/// only what is drawn. A rect wholly outside becomes a
986/// zero-size one on the clip's edge nearest it: the node keeps its place
987/// in reading order and its actions (a reader's "scroll into view" goes by
988/// key), but a point never lands in it. The clip is a rect even where the
989/// clipper's corners are round, as the hit test's is.
990fn clipped(rect: Rect, clip: Rect) -> Rect {
991    let x0 = rect.x.max(clip.x);
992    let y0 = rect.y.max(clip.y);
993    let x1 = (rect.x + rect.w).min(clip.x + clip.w);
994    let y1 = (rect.y + rect.h).min(clip.y + clip.h);
995    // Gone along an axis when nothing of it is left, which is emission's
996    // cull: past the edge, or on it with no extent inside. An axis the
997    // rect never had extent on (a caret-wide run) is not gone for that.
998    let gone = |lo: f32, hi: f32, extent: f32| hi < lo || (hi == lo && extent > 0.0);
999    if gone(x0, x1, rect.w) || gone(y0, y1, rect.h) {
1000        // `max` then `min` rather than `clamp`, which panics on a clip
1001        // whose edges cross.
1002        let x = rect.x.max(clip.x).min(clip.x + clip.w);
1003        let y = rect.y.max(clip.y).min(clip.y + clip.h);
1004        return Rect::new(x, y, 0.0, 0.0);
1005    }
1006    Rect::new(x0, y0, x1 - x0, y1 - y0)
1007}
1008
1009/// Node `i`'s access rect: the root's viewport, everyone else's box cut
1010/// to the clip it was emitted under.
1011fn node_rect(tree: &Tree, src: &Sources<'_>, i: usize) -> Rect {
1012    if i == 0 {
1013        Rect::new(0.0, 0.0, src.viewport.w, src.viewport.h)
1014    } else {
1015        clipped(
1016            Rect::from_pos_size(tree.pos[i], tree.size[i]),
1017            clip_of(src, i),
1018        )
1019    }
1020}
1021
1022/// What an editor's runs are cut to: the node's clip and, for a field
1023/// that does not fold to its width, its content box across, as emission
1024/// cuts its glyphs — a scrolled field's text past its edge is not
1025/// drawn, so a reader should not find it there either.
1026fn edit_run_clip(tree: &Tree, src: &Sources<'_>, i: usize, edit_key: Key) -> Rect {
1027    let clip = clip_of(src, i);
1028    if src.edit.folds(edit_key) {
1029        return clip;
1030    }
1031    let pad = tree.specs[i].layout.padding;
1032    let across = Rect::new(
1033        tree.pos[i].x + pad.l,
1034        clip.y,
1035        (tree.size[i].w - pad.x()).max(0.0),
1036        clip.h,
1037    );
1038    clip.intersect(&across)
1039}
1040
1041/// Cuts runs to `clip` the way [`clipped`] cuts a node, keeping each
1042/// character where it is: `char_positions` are relative to the run's x,
1043/// so they move by what the x did.
1044fn clip_runs(runs: &mut [AccessRun], clip: Rect) {
1045    if clip == NO_CLIP {
1046        return;
1047    }
1048    for run in runs {
1049        let cut = clipped(run.rect, clip);
1050        let dx = run.rect.x - cut.x;
1051        if dx != 0.0 {
1052            for p in &mut run.char_positions {
1053                *p += dx;
1054            }
1055        }
1056        run.rect = cut;
1057    }
1058}
1059
1060/// A hash of every input [`build`] reads, taken by the same walk with
1061/// nothing built. `None` means "cannot answer, rebuild" — see the custom
1062/// editor note below.
1063///
1064/// **The invariant this rests on**: everything `build` reads must be mixed
1065/// in here. Miss one and a frame that changed only that thing serves the
1066/// previous frame's tree, which is a bug with no symptom in the core, no
1067/// failing test, and a wrong reading on somebody's screen. Two things hold
1068/// it. The walk is deliberately the *same* walk — the same skip rules
1069/// calling the same [`semantic`], [`focusable`] and
1070/// [`composite::orientation`](crate::composite::orientation) — so what can
1071/// drift is only the fields `build` reads directly out of the spec and the
1072/// sources; and `runtime::dispatch`'s tests mutate each of those in turn
1073/// and assert the tree moved.
1074///
1075/// It is worth taking because the walk is the cheap quarter of `build`:
1076/// **105 µs against 480 µs** over a 10,000-node frame on an M3 Pro, because
1077/// three quarters of that function is constructing `AccessNode`s and
1078/// pushing them, which is exactly what a cache hit skips.
1079///
1080/// A custom editor (`is_custom_editor`) answers `None` rather than a hash:
1081/// `custom_editor` fills a node from the `line` children's own text and
1082/// runs, and there is no reading of those inputs that does not amount to
1083/// building the node. Such a view is one whose text is changing anyway, so
1084/// it is the case a cache would miss on regardless.
1085pub(crate) fn inputs_hash(tree: &Tree, src: &Sources<'_>) -> Option<u64> {
1086    use std::hash::{Hash, Hasher};
1087    let mut h = rustc_hash::FxHasher::default();
1088    let f = |h: &mut rustc_hash::FxHasher, v: f32| v.to_bits().hash(h);
1089
1090    tree.len().hash(&mut h);
1091    src.focus.map(|k| k.0).hash(&mut h);
1092    src.modal.map(|k| k.0).hash(&mut h);
1093    src.title.hash(&mut h);
1094    f(&mut h, src.viewport.w);
1095    f(&mut h, src.viewport.h);
1096    f(&mut h, src.scale);
1097
1098    let mut skip_until = 0usize;
1099    let mut i = 0usize;
1100    while i < tree.len() {
1101        if i < skip_until {
1102            i += 1;
1103            continue;
1104        }
1105        let Some(sem) = semantic(tree, src.text, src.edit, src.title, i) else {
1106            i += 1;
1107            continue;
1108        };
1109        if sem.role == Role::None {
1110            skip_until = tree.subtree_end(i);
1111            i += 1;
1112            continue;
1113        }
1114        if is_custom_editor(tree, i) {
1115            return None;
1116        }
1117        let key = tree.keys[i];
1118        let spec = &tree.specs[i];
1119        let ax = spec.access();
1120
1121        // Identity and place in the tree.
1122        i.hash(&mut h);
1123        key.0.hash(&mut h);
1124        tree.parent[i].hash(&mut h);
1125        tree.origins[i].0.hash(&mut h);
1126
1127        // What `semantic` decided, and the node's own strings.
1128        sem.role.hash(&mut h);
1129        sem.name.hash(&mut h);
1130        sem.presentational.hash(&mut h);
1131        ax.description.as_deref().hash(&mut h);
1132
1133        // The rect, which is the root's viewport and everyone else's box
1134        // cut to its clip.
1135        let rect = node_rect(tree, src, i);
1136        for v in [rect.x, rect.y, rect.w, rect.h] {
1137            f(&mut h, v);
1138        }
1139
1140        // State the node reports, and everything the action bits read.
1141        (src.focus == Some(key)).hash(&mut h);
1142        (src.modal == Some(key)).hash(&mut h);
1143        spec.disabled.hash(&mut h);
1144        ax.live.hash(&mut h);
1145        spec.window.hash(&mut h);
1146        spec.events().on_click.is_some().hash(&mut h);
1147        focusable(tree, i).hash(&mut h);
1148        crate::composite::orientation(tree, i).hash(&mut h);
1149        ax.expanded.hash(&mut h);
1150        ax.checked.hash(&mut h);
1151        ax.mixed.hash(&mut h);
1152        ax.selected.hash(&mut h);
1153        ax.value_text.as_deref().hash(&mut h);
1154        for v in [ax.value_now, ax.value_min, ax.value_max, ax.value_step] {
1155            v.is_some().hash(&mut h);
1156            f(&mut h, v.unwrap_or(0.0));
1157        }
1158
1159        // The content's own value, through the same accessors `build` uses.
1160        match tree.content[i] {
1161            NodeContent::Edit(edit_key) => {
1162                let pad = spec.layout.padding;
1163                f(&mut h, tree.pos[i].x + pad.l);
1164                f(&mut h, tree.pos[i].y + pad.t);
1165                src.edit.text(edit_key).hash(&mut h);
1166                src.edit.caret_and_selection(edit_key).hash(&mut h);
1167                src.edit.selection_cursors(edit_key).hash(&mut h);
1168                // `runs` is the editor's shaped layout, which the text and
1169                // the box above do not fully determine — a font arriving
1170                // between frames reshapes it. The store's own version bumps
1171                // on every mutation that could, so it stands in for reading
1172                // the runs, which would cost what building them costs.
1173                src.edit.version(edit_key).hash(&mut h);
1174                let clip = edit_run_clip(tree, src, i, edit_key);
1175                for v in [clip.x, clip.y, clip.w, clip.h] {
1176                    f(&mut h, v);
1177                }
1178            }
1179            NodeContent::Cells(id) => src.cells.value(id).hash(&mut h),
1180            _ => {}
1181        }
1182
1183        // Scrolling: the offset is retained state, the max a layout output.
1184        if spec.layout.scroll_x || spec.layout.scroll_y {
1185            spec.layout.scroll_x.hash(&mut h);
1186            spec.layout.scroll_y.hash(&mut h);
1187            let off = src.scroll.drawn(key);
1188            let max = tree.scroll_max[i];
1189            for v in [off.x, off.y, max.x, max.y] {
1190                f(&mut h, v);
1191            }
1192        }
1193
1194        if sem.presentational {
1195            skip_until = tree.subtree_end(i);
1196        }
1197        i += 1;
1198    }
1199    Some(h.finish())
1200}
1201
1202/// Derives the access tree of a laid-out frame.
1203pub(crate) fn build(tree: &Tree, src: &Sources<'_>) -> AccessTree {
1204    let mut out = AccessTree::default();
1205    if tree.is_empty() {
1206        return out;
1207    }
1208    // Per tree node: the key of the nearest semantic ancestor (for elided
1209    // nodes, inherited from the parent).
1210    let mut parents: Vec<Option<Key>> = vec![None; tree.len()];
1211    let mut skip_until = 0usize;
1212    let mut i = 0usize;
1213    while i < tree.len() {
1214        if i < skip_until {
1215            i += 1;
1216            continue;
1217        }
1218        let parent = tree.parent[i];
1219        let inherited = if parent == NIL {
1220            None
1221        } else {
1222            parents[parent as usize]
1223        };
1224        let Some(sem) = semantic(tree, src.text, src.edit, src.title, i) else {
1225            parents[i] = inherited;
1226            i += 1;
1227            continue;
1228        };
1229        if sem.role == Role::None {
1230            skip_until = tree.subtree_end(i);
1231            i += 1;
1232            continue;
1233        }
1234        let key = tree.keys[i];
1235        parents[i] = Some(key);
1236        let spec: &NodeSpec = &tree.specs[i];
1237        let rect = node_rect(tree, src, i);
1238        let mut node = AccessNode {
1239            key,
1240            parent: inherited,
1241            origin: tree.origins[i],
1242            role: sem.role,
1243            name: sem.name,
1244            description: spec.access().description.as_ref().map(|d| d.to_string()),
1245            rect,
1246            value: None,
1247            caret: None,
1248            selection: None,
1249            runs: Vec::new(),
1250            anchor: None,
1251            focus: None,
1252            checked: None,
1253            mixed: false,
1254            selected: None,
1255            expanded: None,
1256            orientation: crate::composite::orientation(tree, i),
1257            pos_in_set: None,
1258            set_size: None,
1259            number: None,
1260            min: None,
1261            max: None,
1262            step: None,
1263            focused: src.focus == Some(key),
1264            disabled: spec.disabled,
1265            modal: src.modal == Some(key),
1266            scroll: None,
1267            actions: 0,
1268            live: spec.access().live,
1269        };
1270        let mut actions = 0u32;
1271        match spec.window {
1272            Some(WindowRole::Button(_)) => actions |= AccessAction::Click.bit(),
1273            Some(WindowRole::Drag) => {}
1274            None => {
1275                if spec.events().on_click.is_some() && !spec.disabled {
1276                    actions |= AccessAction::Click.bit();
1277                }
1278            }
1279        }
1280        // Anything the keyboard can reach, assistive technology can focus
1281        // (and AccessKit calls focusable only what supports `Focus`).
1282        if focusable(tree, i) {
1283            actions |= AccessAction::Focus.bit() | AccessAction::Blur.bit();
1284        }
1285        if let NodeContent::Edit(edit_key) = tree.content[i] {
1286            let pad = spec.layout.padding;
1287            let origin = Vec2::new(tree.pos[i].x + pad.l, tree.pos[i].y + pad.t);
1288            node.value = src.edit.text(edit_key);
1289            node.runs = src.edit.runs(edit_key, key, origin, src.scale);
1290            clip_runs(&mut node.runs, edit_run_clip(tree, src, i, edit_key));
1291            if let Some((caret, selection)) = src.edit.caret_and_selection(edit_key) {
1292                node.caret = Some(caret);
1293                node.selection = selection;
1294            }
1295            if let Some((anchor, focus)) = src.edit.selection_cursors(edit_key) {
1296                node.anchor = node.text_pos(anchor.0, anchor.1);
1297                node.focus = node.text_pos(focus.0, focus.1);
1298            }
1299            if !spec.disabled {
1300                actions |= AccessAction::Click.bit()
1301                    | AccessAction::SetValue.bit()
1302                    | AccessAction::SetTextSelection.bit()
1303                    | AccessAction::ReplaceSelectedText.bit();
1304            }
1305        } else if is_custom_editor(tree, i) {
1306            custom_editor(tree, src, i, &mut node);
1307            if !spec.disabled {
1308                actions |= AccessAction::SetValue.bit()
1309                    | AccessAction::SetTextSelection.bit()
1310                    | AccessAction::ReplaceSelectedText.bit();
1311            }
1312        } else if let NodeContent::Cells(id) = tree.content[i] {
1313            // A terminal's value is its screen, rows joined.
1314            node.value = Some(src.cells.value(id));
1315        }
1316        // A disclosure names its own state, so it lands wherever it is
1317        // declared: no role means "shows and hides something".
1318        let ax = spec.access();
1319        node.expanded = ax.expanded;
1320        match sem.role {
1321            Role::Checkbox | Role::Radio | Role::Switch => {
1322                node.checked = Some(ax.checked);
1323                node.mixed = ax.mixed && sem.role == Role::Checkbox;
1324            }
1325            // A tab is one of a set by definition, so it reports either
1326            // state; a row or a link reports only the one it declares,
1327            // since most lists and every navigation bar are not
1328            // selections and "not selected" on each of their nodes is the
1329            // noise AccessKit warns about.
1330            Role::Tab => node.selected = Some(ax.selected),
1331            Role::ListItem | Role::Link if ax.selected => node.selected = Some(true),
1332            // A menu row that is a setting reads as checked (the drawn
1333            // menu's checkmark, `widgets::menu_panel`); every other row is
1334            // a command and says nothing, the way a list row says nothing
1335            // about a selection it is not part of. Before this the widget
1336            // declared the fact and the tree dropped it, so a reader heard
1337            // "Wrap" where a sighted user saw "✓ Wrap".
1338            Role::MenuItem if ax.checked => node.checked = Some(true),
1339            Role::Slider => {
1340                node.number = ax.value_now;
1341                node.min = ax.value_min;
1342                node.max = ax.value_max;
1343                node.step = ax.value_step;
1344                // The declared reading, in the one string slot the
1345                // platform gives a node (see `value`). Slider-only, like
1346                // the three numbers: a `group` shaped like a progress bar
1347                // carries none of them either, and a role whose numbers
1348                // are ignored should not have a reading that is not.
1349                node.value = ax.value_text.as_deref().map(str::to_owned);
1350                // SetValue too: Windows' UI Automation has no increment,
1351                // and moves a slider only by setting it.
1352                if !spec.disabled {
1353                    actions |= AccessAction::Increment.bit()
1354                        | AccessAction::Decrement.bit()
1355                        | AccessAction::SetValue.bit();
1356                }
1357            }
1358            _ => {}
1359        }
1360        if spec.layout.scroll_x || spec.layout.scroll_y {
1361            let off = src.scroll.drawn(key);
1362            let max = tree.scroll_max[i];
1363            node.scroll = Some(ScrollState {
1364                x: off.x,
1365                y: off.y,
1366                max_x: max.x,
1367                max_y: max.y,
1368            });
1369            if spec.layout.scroll_y {
1370                actions |= AccessAction::ScrollUp.bit() | AccessAction::ScrollDown.bit();
1371            }
1372            if spec.layout.scroll_x {
1373                actions |= AccessAction::ScrollLeft.bit() | AccessAction::ScrollRight.bit();
1374            }
1375        }
1376        if i != 0 {
1377            actions |= AccessAction::ScrollIntoView.bit();
1378        }
1379        node.actions = actions;
1380        if node.focused {
1381            out.focus = Some(key);
1382        }
1383        out.nodes.push(node);
1384        if sem.presentational {
1385            skip_until = tree.subtree_end(i);
1386        }
1387        i += 1;
1388    }
1389    set_positions(tree, &mut out);
1390    out.hash = hash_of(&out);
1391    out
1392}
1393
1394/// "3 of 7", derived rather than declared: a composite container already
1395/// holds its items, so the core counts them and numbers each one instead
1396/// of making every view repeat itself (and get it wrong the moment a row
1397/// is filtered out). The count lands on the container and the zero-based
1398/// ordinal on the item, which is how AccessKit models a set.
1399///
1400/// The items come from `composite::items`, which is also the order the
1401/// arrow keys walk: "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, 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}