Skip to main content

teksilo_core/
accessibility.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4use accesskit::{Action, Live, Node, NodeId, Role, TextDirection, TextPosition, TextSelection};
5
6use crate::widget_id::WidgetId;
7
8pub(crate) mod adapter_ids;
9pub(crate) mod announcements;
10pub mod audit;
11pub mod target_audit;
12pub mod text_runs;
13
14/// Builder wrapper around accesskit::Node for widget accessibility declarations.
15pub struct AccessNodeBuilder {
16    inner: Node,
17    name: Option<String>,
18    value: Option<String>,
19    role: Role,
20    actions: Vec<Action>,
21    toggled: Option<bool>,
22    expanded: Option<bool>,
23    selected: Option<bool>,
24    hidden: bool,
25    /// The owning widget's id. Set at construction time by the
26    /// tree walker (via `AccessNodeBuilder::for_widget`). Used by
27    /// the sub-tree API (`push_paragraph_child` / `push_text_run_child`)
28    /// to derive synthetic NodeIds without asking the caller to
29    /// pass the WidgetId at every call site.
30    owner: Option<WidgetId>,
31    /// Pending text selection targeting the widget's own node id.
32    /// Resolved at `build(id)` time because the widget doesn't know
33    /// its node id during `accessibility(&self, builder)`.
34    pending_self_selection: Option<(usize, usize)>,
35    /// Deferred text selection targeting synthetic child NodeIds
36    /// (TextRuns). Unlike `pending_self_selection`, these
37    /// TextPositions reference NodeIds that are already known at
38    /// the time the widget calls `set_text_selection_to`, so we
39    /// can populate the selection during `build(id)` directly —
40    /// the field just holds them until that point.
41    pending_explicit_selection: Option<(TextPosition, TextPosition)>,
42    /// Synthetic child nodes emitted by the widget via
43    /// `push_paragraph_child` / `push_text_run_child`. Drained by
44    /// the tree walker after `Widget::accessibility(&self, builder)`
45    /// returns and merged into the full AccessKit `TreeUpdate`.
46    children_collected: Vec<(NodeId, Node)>,
47    /// Bounds a widget declared for its synthetic children in **its own**
48    /// coordinate space, translated into window space by [`build`] —
49    /// which is the first moment the owner's absolute rect is known,
50    /// because the walker writes it onto the node just before calling.
51    ///
52    /// A widget that already holds absolute rects (a scene item, a
53    /// composite emitting geometry it got from a child it placed) calls
54    /// `set_bounds` on the child node directly and never appears here.
55    ///
56    /// [`build`]: AccessNodeBuilder::build
57    child_local_bounds: Vec<(NodeId, teksilo_canvas::Rect)>,
58    /// This builder exists only to harvest an accessible *name* — the
59    /// merge pass and the tooltip-description probe both run a widget's
60    /// `accessibility()` on a throwaway builder and read nothing but the
61    /// name off it. Emitting synthetic children there would allocate
62    /// node ids that never reach the tree, and, worse, would be
63    /// discarded along with the builder while the widget's real node
64    /// keeps its own copies.
65    name_probe: bool,
66    /// The name was set with [`set_default_name`](AccessNodeBuilder::set_default_name):
67    /// a fallback the widget supplies when nobody named it, which a
68    /// `labelled_by` relation replaces.
69    name_is_default: bool,
70}
71
72/// The ids of every `Role::TextRun` this node attached to **itself**, or
73/// `None` when there are none.
74///
75/// Scoped to the node's own child list on purpose. A builder's collected
76/// vector also holds *grandchildren* — a scene item is emitted through a
77/// nested builder whose whole subtree is folded into the outer one — and
78/// those belong to a parent of their own, which may well be a
79/// `Role::Label` that does support ranges. Dropping them because the
80/// *outer* node cannot carry runs would delete nodes the item's own
81/// children list still names, and `accesskit_consumer` panics on a
82/// child id that reaches no node.
83fn own_run_ids(
84    inner: &Node,
85    children: &[(NodeId, Node)],
86) -> Option<std::collections::HashSet<NodeId>> {
87    let attached: std::collections::HashSet<NodeId> = inner.children().iter().copied().collect();
88    let ids: std::collections::HashSet<NodeId> = children
89        .iter()
90        .filter(|(id, node)| node.role() == Role::TextRun && attached.contains(id))
91        .map(|(id, _)| *id)
92        .collect();
93    (!ids.is_empty()).then_some(ids)
94}
95
96/// Everything one `Role::TextRun` node needs.
97#[derive(Debug, Clone)]
98pub struct TextRunSpec {
99    /// Mixed with the owner and the run kind to derive the node id.
100    pub element_id: u64,
101    pub value: String,
102    /// UTF-8 byte length of each character of `value`. Their sum must be
103    /// `value.len()`; AccessKit panics otherwise.
104    pub character_lengths: Vec<u8>,
105    pub word_starts: Vec<u8>,
106    pub character_positions: Vec<f32>,
107    pub character_widths: Vec<f32>,
108    pub bounds: teksilo_canvas::Rect,
109    /// `true` when `bounds` is already in window space.
110    pub bounds_are_absolute: bool,
111    pub text_direction: TextDirection,
112    pub attrs: TextRunAttributes,
113}
114
115/// A Teksilo affine as AccessKit expresses one.
116///
117/// Both store a 3×2 matrix mapping `(x, y)` to
118/// `(a·x + c·y + tx, b·x + d·y + ty)`, and both spell it `[a, b, c, d, tx, ty]`,
119/// so this is a widening cast and not a change of convention.
120///
121/// Write the result with [`accesskit::Node::set_transform`] on a node whose
122/// own bounds — and every rectangle below it — are stated in a coordinate
123/// space of its own rather than in window space. That is the one supported way
124/// to publish such a rectangle: a consumer reads `bounds` "in the coordinate
125/// space of the nearest ancestor with a non-`None` transform", composes the
126/// chain for `bounding_box()`, and inverts it per step when hit-testing, so
127/// projecting by hand instead would be exact only for the rectangle and wrong
128/// for the per-character geometry and the hit path.
129///
130/// The framework does this for the children of a content-transform node (see
131/// `WidgetTree::build_accessibility_recursive`); a widget emitting synthetic
132/// children of its own in a non-window space — a scene item, a chart mark —
133/// calls it directly.
134pub fn to_accesskit_affine(t: teksilo_canvas::Transform2D) -> accesskit::Affine {
135    let [a, b, c, d, tx, ty] = t.m;
136    accesskit::Affine::new([a as f64, b as f64, c as f64, d as f64, tx as f64, ty as f64])
137}
138
139/// The text a node announces as its name, read the way every platform adapter
140/// reads it.
141///
142/// A `Role::Label` carries its text in `value`, not `label` — Windows UIA
143/// derives the Name from `value` for that role, macOS exposes it as
144/// `AXValue`, and `accesskit_consumer` reads `value` when another control is
145/// `labelled_by` it. `AccessNodeBuilder::build` moves it there. Everything
146/// else carries its name in `label`. A test or probe that reads one property
147/// sees nothing on half the tree.
148///
149/// Neither falls back to the other. The AT-SPI, UIA and macOS adapters all
150/// take the name from `label_comes_from_value() ? value : label`
151/// (`accesskit_atspi_common` `node.rs:38-44`, `accesskit_windows`
152/// `node.rs:404-413`, `accesskit_macos` `node.rs:328-334`, with
153/// `label_comes_from_value` true for `Role::Label` alone, consumer
154/// `node.rs:744-746`), so a `Status` that holds its text only as a value has
155/// no name, and no platform announces it as one. This reads one node's own
156/// properties: a name that comes through `labelled_by` takes the consumer's
157/// `NodeRef::label` to follow.
158pub fn announced_text(node: &Node) -> Option<&str> {
159    if node.role() == Role::Label {
160        node.value()
161    } else {
162        node.label()
163    }
164}
165
166/// Whether a node with this role can carry text ranges at all.
167///
168/// `accesskit_consumer::Node::supports_text_ranges` is
169/// `(is_text_input || role ∈ {Label, Document, Terminal}) && has runs`
170/// (`accesskit_consumer-0.39.0/src/text.rs:1402`). Runs under any other
171/// role are inert: no platform exposes them, and they still cost a node
172/// in every update.
173pub fn role_supports_text_ranges(role: Role) -> bool {
174    matches!(
175        role,
176        Role::Label
177            | Role::Document
178            | Role::Terminal
179            | Role::TextInput
180            | Role::MultilineTextInput
181            | Role::SearchInput
182            | Role::DateInput
183            | Role::DateTimeInput
184            | Role::WeekInput
185            | Role::MonthInput
186            | Role::TimeInput
187            | Role::EmailInput
188            | Role::NumberInput
189            | Role::PasswordInput
190            | Role::PhoneNumberInput
191            | Role::UrlInput
192            | Role::EditableComboBox
193            | Role::SpinButton
194    )
195}
196
197/// Discriminator kind for synthetic-NodeId hashing. Different
198/// kinds sharing the same (widget_id, element_id) tuple produce
199/// distinct NodeIds so paragraph and run nodes for the same
200/// source element don't collide.
201#[derive(Debug, Clone, Copy)]
202#[repr(u8)]
203pub enum SyntheticKind {
204    Paragraph = 1,
205    TextRun = 2,
206    ImageRun = 3,
207    /// An inline link span inside a label-style widget's text
208    /// (e.g. `[docs](url)` inside a TextWidget with `.markup(true)`
209    /// enabled). Each link span becomes one synthetic child of the
210    /// hosting widget's own node, with `Role::Link` and the link label
211    /// as its name.
212    Link = 4,
213    /// A lightweight `SceneItem` rendered by `teksilo_scene::SceneView`.
214    /// Items live outside the arena — `SceneView::accessibility`
215    /// emits one synthetic child per visible item using
216    /// `push_scene_child`.
217    SceneItem = 5,
218    /// A logical grouping declared via `Scene::add_a11y_group`.
219    /// Pure AT structure — no visual counterpart. The parent's
220    /// children list orders mixed `SceneItem` and `SceneGroup`
221    /// synthetic NodeIds however the app declared the logical tree.
222    SceneGroup = 6,
223    /// A magnetism anchor ("magnet") attached to a scene item. Emitted
224    /// as a synthetic child of the owning item's node by
225    /// `SceneView::accessibility` when magnetism is enabled, so the
226    /// anchor is screen-reader perceivable and can be the target of the
227    /// view's `active_descendant` during the keyboard connect flow.
228    SceneMagnet = 7,
229    /// A per-datum mark (bar / line point / pie slice) emitted by a
230    /// `teksilo-charts` widget's `accessibility()`.
231    ChartMark = 8,
232    /// An annotation body (a comment thread) attached to a run of text, emitted
233    /// by a rich-text widget alongside the `TextRun` that carries it. The run
234    /// points at this node through the `details` relation — AccessKit's
235    /// `aria-details` — which is what lets a screen reader say "has comment" and
236    /// let the user navigate in, rather than reciting the thread inline every
237    /// time the caret crosses the span.
238    Annotation = 9,
239    /// A positional mark in a margin lane — the strip beside a scroll area that
240    /// maps where things are in a document rather than picturing it.
241    ///
242    /// Emitted by the lane widget's `accessibility()`, one synthetic child per
243    /// mark, the same shape [`SyntheticKind::ChartMark`] uses per datum. Marks
244    /// get their own nodes, rather than riding the content they refer to, for
245    /// the reason `CodeGutter` gives for *not* doing so: line numbers are
246    /// uniform, dense and derivable from the text, so they belong on the
247    /// paragraph node as `position_in_set`; lane marks are sparse, heterogeneous
248    /// and not derivable from anything — "a comment from Marie at 62 percent" is
249    /// not in the paragraph, so there is no node to delegate to.
250    LaneMark = 10,
251    /// The selection transform frame and its handles, emitted by
252    /// `teksilo_scene::SceneView` when a transform controller is installed.
253    ///
254    /// One frame node per *view* plus at most nine handle nodes hang off it —
255    /// not one per item — because the frame belongs to the selection rather
256    /// than to any item, and a multi-item selection has no owner. The handles
257    /// are a paint pass, never scene items, so they add nothing to the item
258    /// walk, to `items_in_rect`, or to a marquee's result.
259    SceneHandle = 11,
260    /// A table inside a rich-text document, emitted by the rich-text
261    /// accessibility walk from a `FlowElementSnapshot::Table`. Keyed by the
262    /// document's own `table_id`, so the node survives every edit that does
263    /// not destroy the table.
264    RichTextTable = 12,
265    /// One row of a [`SyntheticKind::RichTextTable`], keyed by the table and
266    /// the row index together.
267    RichTextTableRow = 13,
268    /// One cell of a [`SyntheticKind::RichTextTable`], keyed by the table and
269    /// the cell's row and column together.
270    RichTextTableCell = 14,
271    /// The text container inside a [`SyntheticKind::RichTextTableCell`].
272    ///
273    /// A cell's text runs cannot hang off the cell directly. The platform
274    /// adapters route a changed run's text-change event to its *filtered*
275    /// parent, and `accesskit_consumer`'s `common_filter` makes only
276    /// `GenericContainer` and `TextRun` transparent — so a `Role::Cell`
277    /// parent is asked whether it `supports_text_ranges()`, answers no, and
278    /// every keystroke typed into the cell is dropped before it reaches a
279    /// screen reader. This node is the `Role::Label` between the two: it is
280    /// text-range capable, so the event survives, and it is what a reader
281    /// announces as the cell's content.
282    RichTextCellText = 15,
283    /// The text container inside a structural block-level node — a
284    /// `Role::Heading`, a `Role::Blockquote` or a `Role::ListItem`.
285    ///
286    /// Exactly the problem [`SyntheticKind::RichTextCellText`] solves, in the
287    /// other places a structural node sits between the editor and its runs:
288    /// none of those roles is text-range capable, so runs parented straight
289    /// onto one had every edit beneath it dropped before it reached a screen
290    /// reader.
291    /// Keyed by the **block's** own id — one per block, so a heading inside a
292    /// blockquote still has exactly one — which is a different id space from a
293    /// cell's packed `(table, row, column)`, so the two kinds stay distinct
294    /// rather than relying on those spaces never colliding.
295    RichTextBlockText = 16,
296    /// A blockquote in a rich-text document, emitted from a
297    /// `FlowElementSnapshot::Frame` whose `FrameFormat::is_blockquote` is set.
298    ///
299    /// Only a blockquote earns a node. A frame is otherwise a layout box — a
300    /// positioned or floating text frame — with no role to announce, and
301    /// wrapping one in a node a reader stops on would be AT noise.
302    RichTextBlockquote = 17,
303    /// A node of a web page, mirrored from the accessibility tree an engine
304    /// that renders offscreen publishes (`teksilo-webview`'s Servo backend).
305    /// Keyed by the page's own node id, so a node keeps its id across the
306    /// engine's incremental updates.
307    WebPageNode = 18,
308    /// A list in a rich-text document: consecutive items of one list at one
309    /// level, emitted from its blocks' `BlockSnapshot::list_info`.
310    ///
311    /// Keyed by the block id of the list's first item there, not by the
312    /// document's list id: a paragraph between two items of one list makes
313    /// two of these, and the list id would give both one `NodeId`.
314    RichTextList = 19,
315    /// One item of a [`SyntheticKind::RichTextList`], keyed by its block id.
316    RichTextListItem = 20,
317    /// A stand-in for an ancestor row of a tree that virtualization has not
318    /// realized, emitted by a tree view's body pane.
319    ///
320    /// A screen reader counts a tree row's level from the tree items above
321    /// it, and reads its "N of M" from the node it hangs from. When the
322    /// realized window starts inside a branch, the rows whose parent is
323    /// scrolled away hang from one of these instead, so they keep their true
324    /// ancestor count and their own sibling set size. Keyed by the ancestor's
325    /// flat index, which no scroll within the branch changes. It has no
326    /// visual counterpart and is not focusable.
327    TreeAncestor = 21,
328    /// The group a tree table row's child rows hang in, after the row's own
329    /// cells, emitted by the row. It carries the child rows' set size, so
330    /// the row's cells do not read it as theirs. No visual counterpart.
331    TreeChildRows = 22,
332}
333
334/// A live-region announcement every platform adapter would have made.
335///
336/// Teksilo has no OS accessibility layer in headless mode, and even with
337/// one there is no in-process way to observe what the platform *spoke*.
338/// So a tree that [records announcements](crate::WidgetTree::records_announcements)
339/// replays every update [`crate::WidgetTree::sync_accessibility`] returns
340/// through `accesskit_consumer`, the diff the AT-SPI, Windows and
341/// macOS adapters are all built on, and the announcements those adapters
342/// would raise are kept in a ring that an automation or test harness drains
343/// through [`crate::WidgetTree::announcements_since`]: a live node entering
344/// the filtered tree with a name, or a live node in it whose name changed.
345/// A live node the adapters cannot hear, because it is hidden, inside a
346/// hidden subtree, or carries its text as a `value` where its role takes a
347/// label, is not in the ring. This models what the adapters hand a screen
348/// reader, not what the screen reader then says, so it does not replace
349/// listening to one.
350#[derive(Debug, Clone, PartialEq, Eq)]
351pub struct Announcement {
352    /// Monotonic sequence number, starting at 1. `announcements_since(n)`
353    /// returns every announcement whose `seq > n`.
354    pub seq: u64,
355    /// The node's name as every adapter computes it, which is the string the
356    /// platform announces: the `value` of a `Role::Label`, the label of any
357    /// other role, followed through `labelled_by` when the node has none of
358    /// its own.
359    pub text: String,
360    /// `true` for `Live::Assertive`, `false` for `Live::Polite`. A node with
361    /// no setting of its own takes its nearest live ancestor's, as the
362    /// adapters do.
363    pub assertive: bool,
364}
365
366/// Top bit of the u64 NodeId encoding. Set for synthetic (widget-
367/// emitted child) NodeIds, clear for widget-derived NodeIds.
368/// Slotmap-derived WidgetIds never set bit 63 in practice because
369/// slotmap's KeyData encoding occupies bits 32-63 with a version
370/// counter that starts at 1.
371pub(crate) const SYNTHETIC_BIT: u64 = 1u64 << 63;
372
373/// Stable hash of (parent widget, element id, kind) producing a
374/// synthetic NodeId that survives edits for as long as the
375/// underlying element id is stable. Used by `AccessNodeBuilder`'s
376/// sub-tree API to allocate NodeIds for paragraph / text-run
377/// children without colliding with widget-derived NodeIds.
378pub fn synthetic_node_id(parent: WidgetId, element_id: u64, kind: SyntheticKind) -> NodeId {
379    use slotmap::Key;
380    let parent_raw = parent.data().as_ffi();
381    let h = fnv_mix_u64(parent_raw, element_id, kind as u64);
382    NodeId((h & !SYNTHETIC_BIT) | SYNTHETIC_BIT)
383}
384
385/// Convert an ARIA-style **1-based** ordinal to the **zero-based** integer
386/// AccessKit stores.
387///
388/// AccessKit deliberately departs from ARIA on four properties, and says so in
389/// its own documentation (`accesskit-0.25.0/src/lib.rs`, each carrying a
390/// "**Difference with ARIA**" paragraph):
391///
392/// | property | ARIA | AccessKit |
393/// |---|---|---|
394/// | `position_in_set` / `aria-posinset` | 1-based | **0-based** |
395/// | `row_index` / `aria-rowindex` | 1-based | **0-based** |
396/// | `column_index` / `aria-colindex` | 1-based | **0-based** |
397/// | `level` / `aria-level` | 1-based | **0-based** |
398///
399/// Two of the three platform adapters add the 1 back before handing the value
400/// to the platform — `accesskit_windows-0.35.0/src/node.rs:682-687` and
401/// `:698-701`, `accesskit_atspi_common-0.20.0/src/node.rs:394` — so writing an
402/// ARIA-shaped number straight through lands one too high in what the screen
403/// reader actually says. `accesskit_macos-0.27.0` reads none of the four, so
404/// the error is invisible there, which is part of why it went unnoticed.
405///
406/// Teksilo's public surface stays 1-based, because that is the convention every
407/// call site, every doc comment and ARIA itself already use, and because "the
408/// first tab is tab 1" is what the value means to a person. The conversion
409/// happens once, here, at the boundary.
410///
411/// Saturating rather than panicking on 0: an off-by-one in a caller should
412/// produce a slightly wrong announcement, not take down the application. A
413/// caller that passes 0 gets the same node it would have got for 1.
414fn to_accesskit_ordinal(one_based: usize) -> usize {
415    debug_assert!(
416        one_based >= 1,
417        "AccessKit ordinals are 1-based at this boundary; 0 is not a position, \
418         row, column or level"
419    );
420    one_based.saturating_sub(1)
421}
422
423/// Whether a given `NodeId` is a synthetic child node (emitted by a
424/// widget via `push_paragraph_child` / `push_text_run_child`) rather
425/// than a widget-derived NodeId.
426pub fn is_synthetic(id: NodeId) -> bool {
427    id.0 & SYNTHETIC_BIT != 0
428}
429
430/// FNV-1a-inspired 64-bit mixer for three u64 inputs. Not a
431/// cryptographic hash — just a fast, deterministic, well-distributed
432/// mix for collision-free synthetic NodeIds across the
433/// (widget, element, kind) space.
434pub(crate) fn fnv_mix_u64(a: u64, b: u64, c: u64) -> u64 {
435    const FNV_OFFSET: u64 = 0xcbf29ce484222325;
436    const FNV_PRIME: u64 = 0x100000001b3;
437    let mut h = FNV_OFFSET;
438    for byte in a
439        .to_le_bytes()
440        .iter()
441        .chain(b.to_le_bytes().iter())
442        .chain(c.to_le_bytes().iter())
443    {
444        h ^= *byte as u64;
445        h = h.wrapping_mul(FNV_PRIME);
446    }
447    h
448}
449
450/// Styling attributes surfaced to assistive technology on a synthetic
451/// `Role::TextRun` node (WCAG 1.3.1 Info and Relationships / EN 301 549
452/// 11.5.2.9 "text attributes"). All fields default to "unset"; a rich-text
453/// widget populates them per formatting run so a screen reader can convey
454/// bold / italic / underline / strikethrough spans. AccessKit has no
455/// dedicated `bold` property, so [`bold`](Self::bold) folds into
456/// `set_font_weight(700)` when no explicit [`font_weight`](Self::font_weight)
457/// is given.
458#[derive(Debug, Clone, Copy, Default)]
459pub struct TextRunAttributes {
460    /// Explicit numeric font weight (`100..=900`). Takes precedence over
461    /// [`bold`](Self::bold).
462    pub font_weight: Option<u16>,
463    /// Bold flag; folded to weight `700` when `font_weight` is `None`.
464    pub bold: bool,
465    pub italic: bool,
466    pub underline: bool,
467    pub strikethrough: bool,
468}
469
470/// Apply a run's formatting to its node.
471///
472/// WCAG 1.3.1 / EN 301 549 11.5.2.9. AccessKit has no bold flag, so an
473/// explicit weight wins and a bare `bold` folds to 700.
474fn apply_run_attrs(node: &mut Node, attrs: TextRunAttributes) {
475    if let Some(weight) = attrs.font_weight {
476        node.set_font_weight(weight as f32);
477    } else if attrs.bold {
478        node.set_font_weight(700.0);
479    }
480    if attrs.italic {
481        node.set_italic();
482    }
483    if attrs.underline {
484        node.set_underline(default_text_decoration());
485    }
486    if attrs.strikethrough {
487        node.set_strikethrough(default_text_decoration());
488    }
489}
490
491/// The `TextDecoration` used for underline / strikethrough on a text run.
492/// Screen readers key off the *presence* of a decoration, not its colour,
493/// so a solid neutral (black) decoration is sufficient; the run's own
494/// foreground colour is not plumbed through this synthetic-node path.
495fn default_text_decoration() -> accesskit::TextDecoration {
496    accesskit::TextDecoration {
497        style: accesskit::TextDecorationStyle::Solid,
498        color: accesskit::Color {
499            red: 0,
500            green: 0,
501            blue: 0,
502            alpha: 255,
503        },
504    }
505}
506
507impl AccessNodeBuilder {
508    pub fn new() -> Self {
509        Self {
510            inner: Node::new(Role::Unknown),
511            name: None,
512            value: None,
513            role: Role::Unknown,
514            actions: Vec::new(),
515            toggled: None,
516            expanded: None,
517            selected: None,
518            hidden: false,
519            owner: None,
520            pending_self_selection: None,
521            pending_explicit_selection: None,
522            children_collected: Vec::new(),
523            child_local_bounds: Vec::new(),
524            name_probe: false,
525            name_is_default: false,
526        }
527    }
528
529    /// Construct a builder with a known owner WidgetId. Used by the
530    /// tree walker when invoking `Widget::accessibility`; the owner
531    /// id drives synthetic NodeId derivation in the sub-tree API.
532    pub fn for_widget(owner: WidgetId) -> Self {
533        let mut b = Self::new();
534        b.owner = Some(owner);
535        b
536    }
537
538    /// A builder whose only purpose is to harvest a widget's accessible
539    /// *name*.
540    ///
541    /// The merge pass (`AccessSubtreeMode::Merge`) and the
542    /// tooltip-description probe both run a widget's `accessibility()`
543    /// against a throwaway builder and read nothing off it but the name.
544    /// Text runs must not be emitted there: their node ids would never
545    /// reach the tree, and the run-emitting helpers still need to compute
546    /// the merged text so the *name* comes out right.
547    pub fn for_name_probe(owner: WidgetId) -> Self {
548        let mut b = Self::for_widget(owner);
549        b.name_probe = true;
550        b
551    }
552
553    /// Whether this builder is a name probe (see
554    /// [`for_name_probe`](Self::for_name_probe)), or has no owner at all —
555    /// the two cases in which a widget must emit no synthetic children.
556    ///
557    /// An owner-less builder is a legitimate, if unusual, caller: the
558    /// overlay measurement path and the debug inspector both run
559    /// `accessibility()` on an arbitrary widget through
560    /// [`AccessNodeBuilder::new`] to read its role and name.
561    pub fn emits_no_children(&self) -> bool {
562        self.name_probe || self.owner.is_none()
563    }
564
565    pub fn set_role(&mut self, role: Role) {
566        self.role = role;
567        self.inner.set_role(role);
568    }
569
570    pub fn set_name(&mut self, name: impl Into<String>) {
571        let name: String = name.into();
572        self.inner.set_label(name.clone());
573        self.name = Some(name);
574        self.name_is_default = false;
575    }
576
577    /// Set the name a control falls back to when nothing names it: the
578    /// "Date" of a date field, the "Language" of a language switcher.
579    ///
580    /// Unlike [`set_name`](Self::set_name), this name gives way to a
581    /// `labelled_by` relation whose target has a name, such as the row label
582    /// a `FormLayout` links its field to, so the field is read by the label
583    /// beside it. An application's own name (`access_label`,
584    /// `access_customize`) still wins over both. A name the control takes
585    /// from its own visible text (a checkbox's or a button's) is not a
586    /// default and is set with `set_name`.
587    // Shared plumbing for teksilo-widgets (DateEdit, ComboBox and the other
588    // controls with a built-in name); whether to document it is open.
589    #[doc(hidden)]
590    pub fn set_default_name(&mut self, name: impl Into<String>) {
591        self.set_name(name);
592        self.name_is_default = true;
593    }
594
595    /// Whether the current name came from
596    /// [`set_default_name`](Self::set_default_name).
597    pub(crate) fn name_is_default(&self) -> bool {
598        self.name.is_some() && self.name_is_default
599    }
600
601    /// Remove the name, so a `labelled_by` relation names the node. A name
602    /// the widget wrote past the builder afterwards (through `inner_mut`) is
603    /// not the default any more, and is kept.
604    pub(crate) fn clear_default_name(&mut self) {
605        if self.inner.label() != self.name.as_deref() {
606            self.name_is_default = false;
607            return;
608        }
609        self.inner.clear_label();
610        self.name = None;
611        self.name_is_default = false;
612    }
613
614    pub fn set_disabled(&mut self) {
615        self.inner.set_disabled();
616    }
617
618    /// Clear the disabled flag set by an earlier `set_disabled()` call. Used
619    /// by the override layer to un-set state a widget emitted unconditionally
620    /// (e.g. a Panel that always calls `set_hidden`/`set_disabled`).
621    pub fn clear_disabled(&mut self) {
622        self.inner.clear_disabled();
623    }
624
625    pub fn add_action(&mut self, action: Action) {
626        self.inner.add_action(action);
627        self.actions.push(action);
628    }
629
630    /// Remove a previously-advertised action. Used by the override layer
631    /// (`access_remove_action`) to suppress an action a widget emitted but
632    /// that doesn't apply in this composition.
633    pub fn remove_action(&mut self, action: Action) {
634        self.inner.remove_action(action);
635        self.actions.retain(|a| *a != action);
636    }
637
638    pub fn set_value(&mut self, value: impl Into<String>) {
639        let v: String = value.into();
640        self.inner.set_value(v.clone());
641        self.value = Some(v);
642    }
643
644    /// Advertise a color value on this node — typically paired with
645    /// [`accesskit::Role::ColorWell`]. Takes a `teksilo_tokens::Color` (f32
646    /// channels) and quantizes to AccessKit's 8-bit `Color` representation.
647    pub fn set_color_value(&mut self, color: teksilo_tokens::Color) {
648        let ak = accesskit::Color {
649            red: (color.r() * 255.0).round().clamp(0.0, 255.0) as u8,
650            green: (color.g() * 255.0).round().clamp(0.0, 255.0) as u8,
651            blue: (color.b() * 255.0).round().clamp(0.0, 255.0) as u8,
652            alpha: (color.a() * 255.0).round().clamp(0.0, 255.0) as u8,
653        };
654        self.inner.set_color_value(ak);
655    }
656
657    pub fn set_description(&mut self, description: impl Into<String>) {
658        self.inner.set_description(description.into());
659    }
660
661    pub fn set_live(&mut self, live: Live) {
662        self.inner.set_live(live);
663    }
664
665    pub fn set_described_by(&mut self, ids: impl Into<Vec<NodeId>>) {
666        self.inner.set_described_by(ids);
667    }
668
669    /// Append one node to the `described_by` relationship list. Mirror of
670    /// the existing `push_controlled`; used by the override layer's
671    /// `access_described_by` builder method and by the framework's
672    /// tooltip wiring. The relation by itself reaches no screen reader
673    /// through AccessKit 0.25; the tree writes each target's text into the
674    /// node's description when it builds the update.
675    pub fn push_described_by(&mut self, id: NodeId) {
676        self.inner.push_described_by(id);
677    }
678
679    /// Append one node to the `labelled_by` relationship list. Used by
680    /// `access_labelled_by` to point at an external label widget.
681    pub fn push_labelled_by(&mut self, id: NodeId) {
682        self.inner.push_labelled_by(id);
683    }
684
685    /// Replace the `details` relationship list — AccessKit's analogue of
686    /// `aria-details`.
687    ///
688    /// Distinct from `described_by`, and deliberately so: a *description* is text
689    /// a screen reader appends when announcing the element, while *details* points
690    /// at a structured node the user can navigate **into**. The W3C annotations
691    /// pattern is built on that difference — an annotated run carries
692    /// `aria-details` to a `role="comment"` node, so the reader can say "has
693    /// comment" and let the user go read it, rather than reciting a whole thread
694    /// inline every time the caret crosses the span.
695    pub fn set_details(&mut self, ids: impl Into<Vec<NodeId>>) {
696        self.inner.set_details(ids);
697    }
698
699    /// Append one node to the `details` relationship list.
700    ///
701    /// It is a list, not a single id, because overlapping annotations are normal:
702    /// one run of text can carry several comments, and each gets its own entry.
703    pub fn push_detail(&mut self, id: NodeId) {
704        self.inner.push_detail(id);
705    }
706
707    /// Stable author-supplied identifier (test/debug id, equivalent to
708    /// `aria-label`-style `data-testid`). Maps to `accesskit::Node::set_author_id`.
709    pub fn set_author_id(&mut self, id: impl Into<String>) {
710        self.inner.set_author_id(id.into());
711    }
712
713    /// Replace the node's custom-action list with `actions`, and keep the
714    /// supported-action gate in step with it.
715    ///
716    /// **The gate is the whole reachability story, not the list.** A platform
717    /// adapter reports a node's custom actions through
718    /// [`accesskit::Action::CustomAction`] being
719    /// supported, not through the list being non-empty
720    /// (`accesskit_ios-0.2.0/src/node.rs:109` is the one that says so in code),
721    /// so a list published without it is decoration: named, announced by
722    /// nothing, invokable by nobody. Three separate widgets in this repo have
723    /// shipped that exact defect, each having to remember a second call beside
724    /// this one.
725    ///
726    /// So this method owns both halves. A non-empty list advertises the gate; an
727    /// empty one withdraws it, because a gate with nothing behind it offers an
728    /// assistive-technology user a menu that is not there. Callers that also
729    /// call `add_action(Action::CustomAction)` themselves are correct and
730    /// unaffected — both operations are idempotent.
731    pub fn set_custom_actions(&mut self, actions: Vec<accesskit::CustomAction>) {
732        let empty = actions.is_empty();
733        self.inner.set_custom_actions(actions);
734        if empty {
735            self.remove_action(accesskit::Action::CustomAction);
736        } else {
737            self.add_action(accesskit::Action::CustomAction);
738        }
739    }
740
741    pub fn set_toggled(&mut self, toggled: bool) {
742        self.toggled = Some(toggled);
743        self.inner.set_toggled(if toggled {
744            accesskit::Toggled::True
745        } else {
746            accesskit::Toggled::False
747        });
748    }
749
750    pub fn set_expanded(&mut self, expanded: bool) {
751        self.expanded = Some(expanded);
752        self.inner.set_expanded(expanded);
753    }
754
755    pub fn set_has_popup(&mut self, kind: accesskit::HasPopup) {
756        self.inner.set_has_popup(kind);
757    }
758
759    /// Placeholder text displayed when the widget has no user-entered value
760    /// yet. Screen readers treat this distinctly from `value` — they'll
761    /// announce the placeholder as hint text rather than as the current
762    /// value. Used by `ComboBox` when selection is `None`, by `TextInput`
763    /// before the user types, etc.
764    pub fn set_placeholder(&mut self, placeholder: impl Into<String>) {
765        self.inner.set_placeholder(placeholder.into());
766    }
767
768    /// Target URL for link-like widgets. Maps to `aria-url` / platform
769    /// link metadata so screen readers can announce the destination
770    /// (e.g. "link, `https://example.com`"). Informational only — does
771    /// not navigate when activated.
772    pub fn set_url(&mut self, url: impl Into<String>) {
773        self.inner.set_url(url.into());
774    }
775
776    /// Keyboard shortcut announcement (e.g. `"Ctrl+S"`). Maps to
777    /// `aria-keyshortcuts`. Used by menu items and buttons whose
778    /// chord is shown visually but must also be exposed to assistive
779    /// tech so shortcut users discover it.
780    pub fn set_keyboard_shortcut(&mut self, shortcut: impl Into<String>) {
781        self.inner.set_keyboard_shortcut(shortcut.into());
782    }
783
784    /// Autocomplete behavior for combobox / text input widgets. Maps to
785    /// ARIA `aria-autocomplete`: `Inline` completes within the field,
786    /// `List` shows a popup of matching values, `Both` does both.
787    pub fn set_auto_complete(&mut self, kind: accesskit::AutoComplete) {
788        self.inner.set_auto_complete(kind);
789    }
790
791    /// Selection state — used by `RadioButton`, `Tab`, `ListBoxOption`,
792    /// `TreeItem`, menu items in radio/check groups, etc. This is the
793    /// correct property for "this option in a mutually exclusive
794    /// group is the active one"; don't confuse with `set_toggled`,
795    /// which models checkbox/switch on-off state.
796    pub fn set_selected(&mut self, selected: bool) {
797        self.selected = Some(selected);
798        self.inner.set_selected(selected);
799    }
800
801    pub fn set_orientation(&mut self, orientation: accesskit::Orientation) {
802        self.inner.set_orientation(orientation);
803    }
804
805    /// **1-based** index of this item in its parent set — the ARIA
806    /// `aria-posinset` convention, so the first item is 1.
807    ///
808    /// Use on `Role::Tab`, `Role::ListBoxOption`, `Role::Row`,
809    /// `Role::MenuItem` and similar collection items, and set
810    /// [`set_size_of_set`](Self::set_size_of_set) on their **container** so
811    /// assistive technology can announce "tab 3 of 5".
812    ///
813    /// ⚠ AccessKit's own `position_in_set` is **zero-based**, unlike
814    /// `aria-posinset` — see `accesskit-0.25.0/src/lib.rs`, "Difference with
815    /// ARIA". This wrapper converts, so callers keep the ARIA convention that
816    /// every caller and every doc in this repository already assumed. Do not
817    /// reach past it with `inner_mut().set_position_in_set(..)`: that skips the
818    /// conversion, and the value then arrives one too high on Windows and
819    /// Linux, both of which add the 1 back
820    /// (`accesskit_windows-0.35.0/src/node.rs:682-687`,
821    /// `accesskit_atspi_common-0.20.0/src/node.rs:394`).
822    pub fn set_position_in_set(&mut self, position: usize) {
823        self.inner
824            .set_position_in_set(to_accesskit_ordinal(position));
825    }
826
827    /// Total number of items in a collection — maps to ARIA `aria-setsize`.
828    ///
829    /// ⚠ Set this on the **container** (`Role::ListBox`, `Role::TabList`,
830    /// `Role::Tree`, `Role::Menu`, …), not on each item. Unlike
831    /// `aria-setsize`, which is per item, AccessKit resolves an item's set size
832    /// by walking *up* from its parent: `size_of_set_from_container`
833    /// (`accesskit_consumer-0.39.0/src/node.rs:629-641`) starts at
834    /// `filtered_parent`, so a value written on the item itself is read by no
835    /// adapter on any platform.
836    ///
837    /// Report the **logical** set size, not the realized virtualization window:
838    /// 200 for a 200-row list even when 20 rows exist as widgets.
839    pub fn set_size_of_set(&mut self, size: usize) {
840        self.inner.set_size_of_set(size);
841    }
842
843    /// **1-based** hierarchical depth — the ARIA `aria-level` convention, so a
844    /// root tree item is level 1 and an `<h1>` is level 1.
845    ///
846    /// ⚠ AccessKit's `level` is **zero-based**, unlike `aria-level`. This
847    /// wrapper converts. Reaching past it with `inner_mut().set_level(..)`
848    /// makes every heading and every tree row announce one level too deep on
849    /// Windows, which adds the 1 back
850    /// (`accesskit_windows-0.35.0/src/node.rs:698-701`).
851    pub fn set_level(&mut self, level: usize) {
852        self.inner.set_level(to_accesskit_ordinal(level));
853    }
854
855    // ── Grid / table semantics (`aria-rowcount` / `aria-colindex` / …) ──
856    //
857    // Typed wrappers over the corresponding `accesskit::Node` setters so
858    // grid/table widgets don't have to drop to `inner_mut()`. On a
859    // `Role::Grid` / `Role::Table` container set the *logical* row/column
860    // counts (not the realized window); on each cell set its 1-based
861    // row/column index.
862
863    /// Total logical row count on a grid/table container (`aria-rowcount`).
864    pub fn set_row_count(&mut self, count: usize) {
865        self.inner.set_row_count(count);
866    }
867
868    /// Total logical column count on a grid/table container (`aria-colcount`).
869    pub fn set_column_count(&mut self, count: usize) {
870        self.inner.set_column_count(count);
871    }
872
873    /// **1-based** row index of a cell or row — the ARIA `aria-rowindex`
874    /// convention, so the header row is 1 and the first body row is 2.
875    ///
876    /// ⚠ AccessKit's `row_index` is **zero-based**, unlike `aria-rowindex`.
877    /// This wrapper converts; `inner_mut().set_row_index(..)` does not.
878    pub fn set_row_index(&mut self, index: usize) {
879        self.inner.set_row_index(to_accesskit_ordinal(index));
880    }
881
882    /// **1-based** column index of a cell — the ARIA `aria-colindex`
883    /// convention, so the leftmost column is 1.
884    ///
885    /// ⚠ AccessKit's `column_index` is **zero-based**, unlike `aria-colindex`.
886    /// This wrapper converts; `inner_mut().set_column_index(..)` does not.
887    pub fn set_column_index(&mut self, index: usize) {
888        self.inner.set_column_index(to_accesskit_ordinal(index));
889    }
890
891    /// Number of rows a cell spans (`aria-rowspan`).
892    pub fn set_row_span(&mut self, span: usize) {
893        self.inner.set_row_span(span);
894    }
895
896    /// Number of columns a cell spans (`aria-colspan`).
897    pub fn set_column_span(&mut self, span: usize) {
898        self.inner.set_column_span(span);
899    }
900
901    /// Whether the container allows multiple selected items
902    /// (`aria-multiselectable`). Set on the `Role::Grid` / `Role::ListBox`
903    /// container in multi-select mode.
904    pub fn set_multiselectable(&mut self, value: bool) {
905        if value {
906            self.inner.set_multiselectable();
907        } else {
908            self.inner.clear_multiselectable();
909        }
910    }
911
912    /// The currently-active descendant (`aria-activedescendant`) — the
913    /// roving-focus pattern where focus stays on a composite container and
914    /// this points at the focused child (e.g. the focused grid cell).
915    pub fn set_active_descendant(&mut self, id: NodeId) {
916        self.inner.set_active_descendant(id);
917    }
918
919    /// Flag the node as a modal dialog. Use on `Role::Dialog` /
920    /// `Role::AlertDialog` when input is blocked outside the dialog.
921    pub fn set_modal(&mut self) {
922        self.inner.set_modal();
923    }
924
925    /// Mark this node as the current item within its container
926    /// (e.g. the "current page" crumb inside a `Navigation`, the
927    /// current step in a wizard). Maps to ARIA `aria-current`.
928    pub fn set_aria_current(&mut self, current: accesskit::AriaCurrent) {
929        self.inner.set_aria_current(current);
930    }
931
932    /// Single-step delta for `Slider` / `SpinButton` — how much the
933    /// value changes per keyboard arrow or Action::Increment tick.
934    pub fn set_numeric_value_step(&mut self, step: f64) {
935        self.inner.set_numeric_value_step(step);
936    }
937
938    /// Page-step delta for `Slider` / `SpinButton` — how much the
939    /// value changes per PgUp/PgDown or coarse adjustment.
940    pub fn set_numeric_value_jump(&mut self, jump: f64) {
941        self.inner.set_numeric_value_jump(jump);
942    }
943
944    /// Append a controlled-node relationship — e.g. a `Tab` pointing
945    /// at its matching `TabPanel`, a `ComboBox` pointing at its
946    /// listbox popup. AccessKit / ARIA equivalent of `aria-controls`.
947    pub fn push_controlled(&mut self, id: NodeId) {
948        self.inner.push_controlled(id);
949    }
950
951    /// Declare this radio button's membership in a radio group.
952    /// Each `RadioButton` node should push every sibling in its
953    /// group (including itself); screen readers use this to
954    /// announce positional info like "2 of 3".
955    pub fn push_to_radio_group(&mut self, id: NodeId) {
956        self.inner.push_to_radio_group(id);
957    }
958
959    pub fn set_numeric_value(&mut self, value: f64) {
960        self.inner.set_numeric_value(value);
961    }
962
963    pub fn set_min_numeric_value(&mut self, value: f64) {
964        self.inner.set_min_numeric_value(value);
965    }
966
967    pub fn set_max_numeric_value(&mut self, value: f64) {
968        self.inner.set_max_numeric_value(value);
969    }
970
971    /// Hide this node **and everything under it** from all assistive
972    /// technologies (equivalent to `aria-hidden="true"`). The node is still
973    /// in the widget tree but is invisible to screen readers and other ATs.
974    /// Use for purely decorative elements, e.g. scrollbars (AT scrolls via
975    /// the parent `ScrollView`'s scroll actions instead).
976    ///
977    /// The flag covers the subtree. `accesskit_consumer`, which the AT-SPI,
978    /// UIA and macOS adapters all read the tree through, reads a hidden node
979    /// as `ExcludeSubtree` and treats a node as hidden when any ancestor is,
980    /// so nothing below it reaches a screen reader except a node that holds
981    /// focus, and that one is not listed among its parent's children. A
982    /// wrapper that is only chrome around content a reader needs (a panel,
983    /// a frame, a layout shell) must not call this: it is a
984    /// `Role::GenericContainer` carrying nothing else, which the walker
985    /// prunes and the adapters drop while keeping its children.
986    pub fn set_hidden(&mut self) {
987        self.hidden = true;
988        self.inner.set_hidden();
989    }
990
991    /// Clear the hidden flag set by an earlier `set_hidden()` call on this
992    /// node. Used by the override layer to re-expose a widget that hid
993    /// itself. It clears this node's own flag only: a node under a hidden
994    /// ancestor stays hidden, because the consumer the platform adapters
995    /// read through inherits the flag from every ancestor.
996    pub fn clear_hidden(&mut self) {
997        self.hidden = false;
998        self.inner.clear_hidden();
999    }
1000
1001    pub fn is_hidden(&self) -> bool {
1002        self.hidden
1003    }
1004
1005    pub fn role(&self) -> Role {
1006        self.role
1007    }
1008
1009    pub fn name(&self) -> Option<&str> {
1010        self.name.as_deref()
1011    }
1012
1013    pub fn actions(&self) -> &[Action] {
1014        &self.actions
1015    }
1016
1017    pub fn value(&self) -> Option<&str> {
1018        self.value.as_deref()
1019    }
1020
1021    pub fn toggled(&self) -> Option<bool> {
1022        self.toggled
1023    }
1024
1025    /// The node's numeric value (`Node::numeric_value`), as set by the widget
1026    /// or an `access_numeric_value` override.
1027    pub fn numeric_value(&self) -> Option<f64> {
1028        self.inner.numeric_value()
1029    }
1030
1031    /// The node's numeric lower bound (`Node::min_numeric_value`).
1032    pub fn min_numeric_value(&self) -> Option<f64> {
1033        self.inner.min_numeric_value()
1034    }
1035
1036    /// The node's numeric upper bound (`Node::max_numeric_value`).
1037    pub fn max_numeric_value(&self) -> Option<f64> {
1038        self.inner.max_numeric_value()
1039    }
1040
1041    /// The node's custom actions, in declaration order (the widget's own
1042    /// first, then the application's `access_custom_action` entries).
1043    pub fn custom_actions(&self) -> &[accesskit::CustomAction] {
1044        self.inner.custom_actions()
1045    }
1046
1047    pub fn expanded(&self) -> Option<bool> {
1048        self.expanded
1049    }
1050
1051    pub fn selected(&self) -> Option<bool> {
1052        self.selected
1053    }
1054
1055    /// Build the AccessKit Node with the given ID. Resolves any
1056    /// `pending_self_selection` recorded via `set_caret_position_on_self`
1057    /// or `set_text_selection_on_self` — at this point we know the
1058    /// widget's NodeId and can inject it into the text selection.
1059    /// Returns the primary `(NodeId, Node)` pair plus any synthetic
1060    /// child nodes emitted by the widget via `push_paragraph_child`
1061    /// / `push_text_run_child`. The tree walker is responsible for
1062    /// merging these into the final `TreeUpdate`.
1063    pub fn build(
1064        mut self,
1065        id: WidgetId,
1066    ) -> (
1067        NodeId,
1068        Node,
1069        Vec<(NodeId, Node)>,
1070        Vec<(NodeId, teksilo_canvas::Rect)>,
1071    ) {
1072        let node_id = widget_id_to_node_id(id);
1073        // Priority: explicit (child-targeting) selection wins over
1074        // self-targeting selection — widgets that emit TextRun
1075        // children use the explicit path.
1076        if let Some((anchor, focus)) = self.pending_explicit_selection.take() {
1077            let selection = TextSelection { anchor, focus };
1078            self.inner.set_text_selection(selection);
1079        } else if let Some((anchor, focus)) = self.pending_self_selection.take() {
1080            let selection = TextSelection {
1081                anchor: TextPosition {
1082                    node: node_id,
1083                    character_index: anchor,
1084                },
1085                focus: TextPosition {
1086                    node: node_id,
1087                    character_index: focus,
1088                },
1089            };
1090            self.inner.set_text_selection(selection);
1091        }
1092
1093        // accesskit contract: a `Role::Label` node carries its text in the
1094        // `value` property, NOT `label`. Every platform adapter reads it that
1095        // way — Windows UIA derives the node's Name from `value` (its
1096        // `label_comes_from_value()` returns true for `Role::Label`), macOS
1097        // maps `Role::Label` to `NSAccessibilityStaticTextRole` whose content
1098        // is exposed as AXValue, and `accesskit_consumer` reads `value` when
1099        // another control is `labelled_by` this node. A name left in the
1100        // `label` property is therefore silently dropped on Windows and stray
1101        // on macOS. Widgets set the accessible name uniformly via `set_name`
1102        // (-> the `label` property); re-serialize it to `value` here, the one
1103        // place every emitted node is finalized — widget nodes (via the tree
1104        // walker) and scene synthetic children (via `push_scene_child`, which
1105        // also funnels through `build`). The builder's logical `name()` view
1106        // is intentionally left untouched, so introspection / `find_by_label`
1107        // continue to report the accessible name regardless of role. Reading
1108        // the inner node directly keeps this robust against any label set
1109        // outside `set_name`, and idempotent (a second pass finds no label).
1110        if self.inner.role() == Role::Label
1111            && let Some(label) = self.inner.label().map(|s| s.to_string())
1112        {
1113            if self.inner.value().is_none() {
1114                self.inner.set_value(label);
1115            }
1116            self.inner.clear_label();
1117        }
1118
1119        // A role override away from a text-range-capable role leaves any
1120        // runs inert: no platform exposes them, and they would still cost
1121        // a node in every update and a child stop the walker has to
1122        // reconcile. Drop them rather than ship them.
1123        // `Role::Unknown` is exempt: it is the builder's initial value, so
1124        // dropping there would punish a caller that never set a role at all
1125        // rather than one that set a role text ranges cannot live under.
1126        // Such a node is pruned by the walker in any case.
1127        if !self.children_collected.is_empty()
1128            && self.inner.role() != Role::Unknown
1129            && !role_supports_text_ranges(self.inner.role())
1130            && let Some(run_ids) = own_run_ids(&self.inner, &self.children_collected)
1131        {
1132            self.children_collected
1133                .retain(|(cid, _)| !run_ids.contains(cid));
1134            self.child_local_bounds
1135                .retain(|(cid, _)| !run_ids.contains(cid));
1136            let kept: Vec<NodeId> = self
1137                .inner
1138                .children()
1139                .iter()
1140                .copied()
1141                .filter(|cid| !run_ids.contains(cid))
1142                .collect();
1143            self.inner.set_children(kept);
1144        }
1145
1146        // Synthetic children declared in the widget's own space become
1147        // absolute here: the walker has already written the owner's
1148        // window-space rect onto `inner`, and this is the only point at
1149        // which both halves are in hand.
1150        let origin = self
1151            .inner
1152            .bounds()
1153            .map(|r| (r.x0, r.y0))
1154            .unwrap_or((0.0, 0.0));
1155        let local_bounds = std::mem::take(&mut self.child_local_bounds);
1156        for (child_id, local) in &local_bounds {
1157            let absolute = accesskit::Rect {
1158                x0: origin.0 + local.x as f64,
1159                y0: origin.1 + local.y as f64,
1160                x1: origin.0 + (local.x + local.width) as f64,
1161                y1: origin.1 + (local.y + local.height) as f64,
1162            };
1163            for (id, node) in self.children_collected.iter_mut() {
1164                if id == child_id {
1165                    node.set_bounds(absolute);
1166                    break;
1167                }
1168            }
1169        }
1170
1171        (node_id, self.inner, self.children_collected, local_bounds)
1172    }
1173
1174    /// Get a reference to the inner node for advanced use.
1175    pub fn inner_mut(&mut self) -> &mut Node {
1176        &mut self.inner
1177    }
1178
1179    /// The widget id this builder was constructed for, if any. Set
1180    /// by [`AccessNodeBuilder::for_widget`]; used by the scene-tree
1181    /// walker to derive synthetic `NodeId`s for items / groups outside
1182    /// the closure form (`push_scene_child*`).
1183    pub fn owner_id(&self) -> Option<crate::widget_id::WidgetId> {
1184        self.owner
1185    }
1186
1187    /// Run a mutator over a synthetic child node previously pushed
1188    /// via `push_scene_child` (or its `_under` variant). Used by
1189    /// the scene walker to apply cross-tree decorations (relations /
1190    /// live regions / landmarks) after the initial hierarchy emit.
1191    /// Returns `true` if the node was found.
1192    ///
1193    /// Cannot be used to mutate widget-derived NodeIds — those live
1194    /// in the global TreeUpdate and are owned by other widgets.
1195    pub fn with_collected_node<F: FnOnce(&mut Node)>(&mut self, node_id: NodeId, f: F) -> bool {
1196        for (id, node) in self.children_collected.iter_mut() {
1197            if *id == node_id {
1198                f(node);
1199                return true;
1200            }
1201        }
1202        false
1203    }
1204
1205    /// Mark this node as read-only. Used by `RichTextEditor::read_only` so
1206    /// screen readers announce the widget as a document rather than a form
1207    /// field.
1208    pub fn set_read_only(&mut self) {
1209        self.inner.set_read_only();
1210    }
1211
1212    /// Declare the current text selection. `anchor` and `focus` are
1213    /// character indices into the widget's flat text representation; pass
1214    /// equal indices for a collapsed caret. Uses the same `NodeId` for
1215    /// both positions (typical for single-node text widgets that expose
1216    /// the document as one run, which is what the first milestone of
1217    /// `RichTextEditor` does).
1218    pub fn set_text_selection(&mut self, node_id: NodeId, anchor: usize, focus: usize) {
1219        let selection = TextSelection {
1220            anchor: TextPosition {
1221                node: node_id,
1222                character_index: anchor,
1223            },
1224            focus: TextPosition {
1225                node: node_id,
1226                character_index: focus,
1227            },
1228        };
1229        self.inner.set_text_selection(selection);
1230    }
1231
1232    /// Convenience for exposing a caret position as a collapsed selection.
1233    pub fn set_caret_position(&mut self, node_id: NodeId, character_index: usize) {
1234        self.set_text_selection(node_id, character_index, character_index);
1235    }
1236
1237    /// Declare a text selection whose anchor and focus live on the
1238    /// widget's own AccessKit node. The widget doesn't know its own
1239    /// `NodeId` inside `accessibility(&self, builder)` — it's only
1240    /// resolved when the tree walker calls `builder.build(widget_id)`.
1241    /// This method stashes the character indices and defers the
1242    /// `set_text_selection` call until `build()` knows the ID.
1243    pub fn set_text_selection_on_self(&mut self, anchor: usize, focus: usize) {
1244        self.pending_self_selection = Some((anchor, focus));
1245    }
1246
1247    /// Convenience wrapper for a collapsed caret on the widget's own node.
1248    pub fn set_caret_position_on_self(&mut self, character_index: usize) {
1249        self.set_text_selection_on_self(character_index, character_index);
1250    }
1251
1252    // ── Sub-tree API: multi-node widgets (rich text, etc.) ─────────────
1253
1254    /// Push a `Role::Paragraph` child on the current node and return
1255    /// its `NodeId`. The NodeId is synthetic (bit 63 set) and
1256    /// deterministic given the owning widget + `element_id`.
1257    ///
1258    /// The owning `WidgetId` comes from the builder's `owner`
1259    /// field, set by `AccessNodeBuilder::for_widget`. Returns
1260    /// `NodeId(0)` (a no-op placeholder) if the builder has no
1261    /// owner, which can only happen when a widget constructs a
1262    /// builder manually via `new()` instead of going through the
1263    /// tree walker. That's a programming error worth catching in
1264    /// debug.
1265    pub fn push_paragraph_child(&mut self, element_id: u64) -> NodeId {
1266        let Some(owner) = self.owner else {
1267            debug_assert!(
1268                false,
1269                "push_paragraph_child called on a builder with no owner — \
1270                 widgets must only call this from Widget::accessibility"
1271            );
1272            return NodeId(0);
1273        };
1274        let node_id = synthetic_node_id(owner, element_id, SyntheticKind::Paragraph);
1275        let node = Node::new(Role::Paragraph);
1276        self.children_collected.push((node_id, node));
1277        self.inner.push_child(node_id);
1278        node_id
1279    }
1280
1281    /// Push a `Role::Comment` child carrying an annotation's text, and return its
1282    /// `NodeId` so the annotated run can point at it via [`Self::push_detail`].
1283    ///
1284    /// `group_id` must be the annotation's own durable identity (a comment's uid,
1285    /// never a store id), so the node keeps the same `NodeId` across rebuilds and
1286    /// a screen reader's cursor is not thrown out of the thread by an unrelated
1287    /// edit elsewhere in the document.
1288    ///
1289    /// Per the W3C annotations pattern the *body* carries the name; the annotated
1290    /// span itself must NOT be given an accessible name (`role="mark"` forbids it)
1291    /// — naming the span would make the reader announce the comment's text in
1292    /// place of the prose.
1293    pub fn push_annotation_child(&mut self, group_id: u64, text: impl Into<String>) -> NodeId {
1294        let Some(owner) = self.owner else {
1295            debug_assert!(
1296                false,
1297                "push_annotation_child called on a builder with no owner — \
1298                 widgets must only call this from Widget::accessibility"
1299            );
1300            return NodeId(0);
1301        };
1302        let node_id = synthetic_node_id(owner, group_id, SyntheticKind::Annotation);
1303        let mut node = Node::new(Role::Comment);
1304        node.set_value(text.into());
1305        self.children_collected.push((node_id, node));
1306        self.inner.push_child(node_id);
1307        node_id
1308    }
1309
1310    /// Add a `details` target to an already-pushed **child** node.
1311    ///
1312    /// The sub-tree API builds children eagerly into `children_collected`, so a
1313    /// relation between two synthetic siblings (a `TextRun` and its annotation
1314    /// body) cannot go through the current node's own setters — it has to reach
1315    /// back into the collected child. A no-op if `child` was never pushed, which
1316    /// keeps a caller that emitted spans for a run it then skipped from panicking.
1317    pub fn push_detail_on_child(&mut self, child: NodeId, detail: NodeId) {
1318        if let Some((_, node)) = self
1319            .children_collected
1320            .iter_mut()
1321            .find(|(id, _)| *id == child)
1322        {
1323            node.push_detail(detail);
1324        }
1325    }
1326
1327    /// Push a `Role::Link` child on the current node. Used by label
1328    /// widgets (e.g. `TextWidget` with `.markup(true)` enabled) to
1329    /// expose inline `[label](url)` links as individual accessible
1330    /// nodes alongside the parent's own text.
1331    ///
1332    /// `element_id` should be a stable identifier for the link inside
1333    /// the parent widget (typically the byte offset of the `[` in the
1334    /// original markup source, so the NodeId survives identical
1335    /// re-layouts).
1336    ///
1337    /// The returned `NodeId` is synthetic (bit 63 set) and deterministic
1338    /// given `(owner, element_id)`.
1339    pub fn push_link_child(
1340        &mut self,
1341        element_id: u64,
1342        label: impl Into<String>,
1343        url: impl Into<String>,
1344    ) -> NodeId {
1345        let Some(owner) = self.owner else {
1346            debug_assert!(
1347                false,
1348                "push_link_child called on a builder with no owner — \
1349                 widgets must only call this from Widget::accessibility"
1350            );
1351            return NodeId(0);
1352        };
1353        let node_id = synthetic_node_id(owner, element_id, SyntheticKind::Link);
1354        let mut node = Node::new(Role::Link);
1355        let label: String = label.into();
1356        if !label.is_empty() {
1357            node.set_label(label);
1358        }
1359        // AccessKit exposes the link target through the `Value` property
1360        // on a `Role::Link` node — same convention the standalone
1361        // `Link` widget uses via `set_value(...)`.
1362        node.set_value(url.into());
1363        self.children_collected.push((node_id, node));
1364        self.inner.push_child(node_id);
1365        node_id
1366    }
1367
1368    /// Push a `Role::Link` child under `parent` when given, and under the
1369    /// widget's own node otherwise: the form of
1370    /// [`push_link_child`](Self::push_link_child) for a link that sits inside
1371    /// another synthetic node (a paragraph's text container, a table cell),
1372    /// so it can be placed in reading order among the runs around it.
1373    ///
1374    /// The node is named `label` and carries `url` as its value, as
1375    /// `push_link_child` does, and advertises no action either: a caller
1376    /// that follows links adds `Action::Click` through
1377    /// [`with_collected_node`](Self::with_collected_node) and routes it.
1378    /// [`text_runs::push_text_runs_with_links`] does both halves of that
1379    /// for a link inside laid-out text.
1380    ///
1381    /// `element_id` keys the node as it does for `push_link_child`, so the
1382    /// same id gives the same `NodeId` on every walk. Returns `None` when the
1383    /// builder has no owner or the id is already taken (a duplicate child id
1384    /// panics `accesskit_consumer`'s tree builder).
1385    pub fn push_link_child_under(
1386        &mut self,
1387        parent: Option<NodeId>,
1388        element_id: u64,
1389        label: impl Into<String>,
1390        url: impl Into<String>,
1391    ) -> Option<NodeId> {
1392        let owner = self.owner?;
1393        let node_id = synthetic_node_id(owner, element_id, SyntheticKind::Link);
1394        let mut node = Node::new(Role::Link);
1395        let label: String = label.into();
1396        if !label.is_empty() {
1397            node.set_label(label);
1398        }
1399        node.set_value(url.into());
1400        self.push_collected_child(parent, node_id, node)
1401    }
1402
1403    /// Push the `Role::Label` text container a link's runs hang from.
1404    ///
1405    /// `Role::Link` is not text-range capable, and a run whose filtered
1406    /// parent is not loses its text-change events on every platform, so the
1407    /// runs go under this container rather than under the link itself.
1408    ///
1409    /// Minted with `SyntheticKind::Link` although it is a `Role::Label`:
1410    /// `SyntheticKind` is exhaustive, so a variant of its own would break
1411    /// every match on it, and the caller's seed already keeps the id apart
1412    /// from the link's.
1413    pub(crate) fn push_link_text_child(&mut self, link: NodeId, element_id: u64) -> Option<NodeId> {
1414        let owner = self.owner?;
1415        let node_id = synthetic_node_id(owner, element_id, SyntheticKind::Link);
1416        self.push_collected_child(Some(link), node_id, Node::new(Role::Label))
1417    }
1418
1419    /// Push a synthetic child node representing a lightweight
1420    /// `SceneItem` (or `SceneGroup`) emitted by `teksilo_scene::SceneView`.
1421    /// The caller customizes a sub-`AccessNodeBuilder` (mirroring the
1422    /// `Widget::accessibility` shape) and gets back the
1423    /// deterministic synthetic `NodeId` allocated for the
1424    /// `(owner, element_id, kind)` tuple.
1425    ///
1426    /// `kind` must be one of the synthetic kinds this path
1427    /// allocates: [`SyntheticKind::SceneItem`],
1428    /// [`SyntheticKind::SceneGroup`], [`SyntheticKind::SceneMagnet`],
1429    /// [`SyntheticKind::SceneHandle`], [`SyntheticKind::ChartMark`],
1430    /// [`SyntheticKind::LaneMark`], [`SyntheticKind::WebPageNode`],
1431    /// [`SyntheticKind::TreeAncestor`] or [`SyntheticKind::TreeChildRows`];
1432    /// passing any other variant panics in debug. `ChartMark` and
1433    /// `LaneMark` are how a chart or a margin lane emits one node per
1434    /// datum from its own `accessibility()`; `TreeAncestor` is how a tree
1435    /// view stands in for an ancestor row it has not realized, and
1436    /// `TreeChildRows` how a tree table row groups its child rows.
1437    ///
1438    /// Any further synthetic children the closure pushes (a
1439    /// `SceneGroup` containing nested `SceneItem`s) are forwarded into the parent's
1440    /// `children_collected` and re-parented under the
1441    /// just-pushed node via the closure's own `inner.push_child`
1442    /// calls — same convention as `push_paragraph_child` →
1443    /// `push_text_run_child`.
1444    pub fn push_scene_child(
1445        &mut self,
1446        element_id: u64,
1447        kind: SyntheticKind,
1448        customize: impl FnOnce(&mut AccessNodeBuilder),
1449    ) -> NodeId {
1450        debug_assert!(
1451            matches!(
1452                kind,
1453                SyntheticKind::SceneItem
1454                    | SyntheticKind::SceneGroup
1455                    | SyntheticKind::SceneMagnet
1456                    | SyntheticKind::SceneHandle
1457                    | SyntheticKind::ChartMark
1458                    | SyntheticKind::LaneMark
1459                    | SyntheticKind::WebPageNode
1460                    | SyntheticKind::TreeAncestor
1461                    | SyntheticKind::TreeChildRows
1462            ),
1463            "push_scene_child requires SyntheticKind::SceneItem, ::SceneGroup, ::SceneMagnet, ::SceneHandle, ::ChartMark, ::LaneMark, ::WebPageNode, ::TreeAncestor or ::TreeChildRows"
1464        );
1465        let Some(owner) = self.owner else {
1466            debug_assert!(
1467                false,
1468                "push_scene_child called on a builder with no owner — \
1469                 widgets must only call this from Widget::accessibility"
1470            );
1471            return NodeId(0);
1472        };
1473        let node_id = synthetic_node_id(owner, element_id, kind);
1474        // Build the child against a fresh sub-builder so the item
1475        // sees the same `&mut AccessNodeBuilder` shape as widgets.
1476        // Owner-id is the SceneView's so further `push_scene_child`
1477        // calls inside the customize closure (a `SceneGroup`
1478        // emitting nested items) hash off the same owner.
1479        let mut child_builder = AccessNodeBuilder::for_widget(owner);
1480        customize(&mut child_builder);
1481        // `build(owner)` re-derives a widget-keyed NodeId we throw
1482        // away — we use our synthetic `node_id` instead. The
1483        // returned `Node` carries the role / label / bounds / etc
1484        // the customize closure populated; any *grand*children the
1485        // closure pushed via further `push_scene_child` calls come
1486        // back in the third tuple field and we forward them so the
1487        // main TreeUpdate sees the full subtree.
1488        let (_unused, node, grand_children, _local) = child_builder.build(owner);
1489        self.children_collected.push((node_id, node));
1490        for (gid, gnode) in grand_children {
1491            self.children_collected.push((gid, gnode));
1492        }
1493        self.inner.push_child(node_id);
1494        node_id
1495    }
1496
1497    /// Append an existing synthetic node id as a child of a
1498    /// previously-pushed `SceneGroup` (or `SceneItem`) child. Used by
1499    /// the scene logical-tree walker to re-parent items under their
1500    /// declared logical group rather than as direct children of the
1501    /// SceneView.
1502    ///
1503    /// Returns `true` if the parent was found (and the child was
1504    /// attached), `false` if the parent isn't in
1505    /// `children_collected` — the caller misordered the pushes.
1506    pub fn attach_scene_child_under(&mut self, parent: NodeId, child: NodeId) -> bool {
1507        for (id, node) in self.children_collected.iter_mut() {
1508            if *id == parent {
1509                node.push_child(child);
1510                return true;
1511            }
1512        }
1513        false
1514    }
1515
1516    /// Like `push_scene_child` but lets the caller pick the
1517    /// parent. `parent = None` attaches to the widget's own node
1518    /// (same behavior as `push_scene_child`); `parent = Some(...)`
1519    /// attaches to the previously-pushed scene-child with that id.
1520    /// The scene logical-tree walker uses this to nest scene items
1521    /// under declared `A11yGroup` parents.
1522    ///
1523    /// Returns the deterministic synthetic `NodeId` for the new
1524    /// child. If `parent` was `Some` but the parent wasn't found
1525    /// in `children_collected`, the child still gets created and
1526    /// recorded but ends up attached to the widget's own node as a
1527    /// fallback (and a debug-assert fires).
1528    pub fn push_scene_child_under(
1529        &mut self,
1530        parent: Option<NodeId>,
1531        element_id: u64,
1532        kind: SyntheticKind,
1533        customize: impl FnOnce(&mut AccessNodeBuilder),
1534    ) -> NodeId {
1535        debug_assert!(
1536            matches!(
1537                kind,
1538                SyntheticKind::SceneItem
1539                    | SyntheticKind::SceneGroup
1540                    | SyntheticKind::SceneMagnet
1541                    | SyntheticKind::SceneHandle
1542                    | SyntheticKind::ChartMark
1543                    | SyntheticKind::LaneMark
1544                    | SyntheticKind::WebPageNode
1545                    | SyntheticKind::TreeAncestor
1546                    | SyntheticKind::TreeChildRows
1547            ),
1548            "push_scene_child_under requires SyntheticKind::SceneItem, ::SceneGroup, ::SceneMagnet, ::SceneHandle, ::ChartMark, ::LaneMark, ::WebPageNode, ::TreeAncestor or ::TreeChildRows"
1549        );
1550        let Some(owner) = self.owner else {
1551            debug_assert!(
1552                false,
1553                "push_scene_child_under called on a builder with no owner — \
1554                 widgets must only call this from Widget::accessibility"
1555            );
1556            return NodeId(0);
1557        };
1558        let node_id = synthetic_node_id(owner, element_id, kind);
1559        let mut child_builder = AccessNodeBuilder::for_widget(owner);
1560        customize(&mut child_builder);
1561        let (_unused, node, grand_children, _local) = child_builder.build(owner);
1562        self.children_collected.push((node_id, node));
1563        for (gid, gnode) in grand_children {
1564            self.children_collected.push((gid, gnode));
1565        }
1566        match parent {
1567            Some(parent_id) => {
1568                let attached = self.attach_scene_child_under(parent_id, node_id);
1569                if !attached {
1570                    debug_assert!(
1571                        false,
1572                        "push_scene_child_under: parent {:?} not in children_collected — \
1573                         caller must push the parent before its children",
1574                        parent_id
1575                    );
1576                    self.inner.push_child(node_id);
1577                }
1578            }
1579            None => {
1580                self.inner.push_child(node_id);
1581            }
1582        }
1583        node_id
1584    }
1585
1586    /// Override a previously-pushed paragraph child's role to
1587    /// `Role::Heading` with the given hierarchical level. Used by
1588    /// the rich text editor when a block carries a
1589    /// `BlockFormat::heading_level`. Returns `true` if the node was
1590    /// found and updated, `false` otherwise (caller misused the
1591    /// api — the paragraph must have been pushed earlier).
1592    pub fn set_paragraph_as_heading(&mut self, node_id: NodeId, level: u8) -> bool {
1593        for (id, node) in self.children_collected.iter_mut() {
1594            if *id == node_id {
1595                node.set_role(Role::Heading);
1596                // AccessKit's `set_level` takes a usize (via the
1597                // usize_property_methods macro). Clamp to 1..=6 for
1598                // conventional heading semantics.
1599                // Clamped to the conventional 1..=6 heading range, then
1600                // converted: AccessKit's `level` is zero-based, so an H1 is 0.
1601                // Writing the 1-based number straight through made every
1602                // heading announce one level too deep on Windows, and made
1603                // "heading level 1" unreachable.
1604                let level: usize = to_accesskit_ordinal((level as usize).clamp(1, 6));
1605                node.set_level(level);
1606                return true;
1607            }
1608        }
1609        false
1610    }
1611
1612    /// Set the **1-based** position-in-set on a previously-pushed synthetic
1613    /// child (a paragraph, "line 42 of 200"), and the set size on this widget's
1614    /// own node, which is the container the child hangs from.
1615    ///
1616    /// AccessKit exposes `position_in_set` on every node, but
1617    /// [`set_position_in_set`](Self::set_position_in_set) only touches the
1618    /// widget's own node. This reaches a collected child by NodeId, the same
1619    /// way [`set_paragraph_as_heading`](Self::set_paragraph_as_heading) does,
1620    /// and applies the same 1-based-to-zero-based conversion.
1621    ///
1622    /// `size` deliberately lands on the parent rather than the child: AccessKit
1623    /// resolves a set size by walking up from the item, so a size written on
1624    /// the child is read by nobody. See
1625    /// [`set_size_of_set`](Self::set_size_of_set).
1626    ///
1627    /// Returns whether the child was found.
1628    pub fn set_child_position_in_set(
1629        &mut self,
1630        node_id: NodeId,
1631        position: usize,
1632        size: usize,
1633    ) -> bool {
1634        let found = self.with_collected_node(node_id, |node| {
1635            node.set_position_in_set(to_accesskit_ordinal(position));
1636        });
1637        if found {
1638            self.inner.set_size_of_set(size);
1639        }
1640        found
1641    }
1642
1643    /// Declare this node's base reading direction.
1644    ///
1645    /// AccessKit's text APIs read the direction from the *run*; the root
1646    /// carries it so a consumer that asks the container — and every
1647    /// platform that maps a paragraph direction onto its own attribute —
1648    /// gets an answer for an empty or geometry-less node too.
1649    pub fn set_text_direction(&mut self, direction: accesskit::TextDirection) {
1650        self.inner.set_text_direction(direction);
1651    }
1652
1653    /// Set the reading direction of a synthetic child pushed earlier.
1654    /// Returns `true` if the child was found.
1655    pub fn set_child_text_direction(
1656        &mut self,
1657        child: NodeId,
1658        direction: accesskit::TextDirection,
1659    ) -> bool {
1660        self.with_collected_node(child, |node| node.set_text_direction(direction))
1661    }
1662
1663    /// Declare a synthetic child's bounds in the **owner's own**
1664    /// coordinate space; [`build`](Self::build) translates them into
1665    /// window space once the owner's absolute rect is known.
1666    ///
1667    /// This is what a widget should use for geometry it derives from its
1668    /// own layout — a label's line boxes, an editor's rows. A widget
1669    /// holding rects that are *already* absolute (a scene item under a
1670    /// view transform) calls `set_bounds` on the child node instead and
1671    /// never comes through here.
1672    ///
1673    /// Returns `true` if the child was found.
1674    pub fn set_child_bounds_local(&mut self, child: NodeId, rect: teksilo_canvas::Rect) -> bool {
1675        if !self.children_collected.iter().any(|(id, _)| *id == child) {
1676            return false;
1677        }
1678        self.child_local_bounds.push((child, rect));
1679        true
1680    }
1681
1682    /// Link a run of `Role::TextRun` children as one visual line, so assistive
1683    /// technology navigating by line treats them as a continuous line rather
1684    /// than fracturing at each formatting or chunk boundary.
1685    ///
1686    /// Sets each run's `next_on_line` to its successor and each successor's
1687    /// `previous_on_line` to its predecessor (AccessKit's doubly-linked
1688    /// same-line chain); the first run keeps no `previous_on_line` and the last
1689    /// no `next_on_line`, which is how the consumer detects the line's ends. A
1690    /// slice of zero or one is a no-op. Every id must be a run pushed earlier via
1691    /// [`push_text_run`](Self::push_text_run) — or the older
1692    /// [`push_text_run_child`](Self::push_text_run_child).
1693    pub fn link_runs_on_line(&mut self, run_ids: &[NodeId]) {
1694        for pair in run_ids.windows(2) {
1695            let (a, b) = (pair[0], pair[1]);
1696            self.with_collected_node(a, |node| node.set_next_on_line(b));
1697            self.with_collected_node(b, |node| node.set_previous_on_line(a));
1698        }
1699    }
1700
1701    /// Attach an already-built synthetic child under `parent` (another
1702    /// collected child) or under the widget's own node.
1703    ///
1704    /// Returns `None` when `node_id` was already pushed: a duplicate child id
1705    /// panics `accesskit_consumer`'s tree builder, so it is dropped here with
1706    /// a diagnostic instead — the rule [`Self::push_text_run`] follows. The
1707    /// owner check belongs to the callers, which need it to derive `node_id`
1708    /// in the first place.
1709    fn push_collected_child(
1710        &mut self,
1711        parent: Option<NodeId>,
1712        node_id: NodeId,
1713        node: Node,
1714    ) -> Option<NodeId> {
1715        if self.children_collected.iter().any(|(id, _)| *id == node_id) {
1716            debug_assert!(
1717                false,
1718                "Teksilo bug: two accessibility children of widget {:?} derived \
1719                 the same node id {node_id:?}. Please file a bug report.",
1720                self.owner
1721            );
1722            // Paired with an `eprintln!` for the same reason `push_text_run`
1723            // has one: the `debug_assert!` compiles out, and a silent drop
1724            // here loses a whole subtree — a table row and every character in
1725            // it — with nothing to show for it in a shipped build.
1726            eprintln!(
1727                "Teksilo bug: two accessibility children of widget {:?} derived \
1728                 the same node id {node_id:?}; the second was dropped. Please \
1729                 file a bug report.",
1730                self.owner
1731            );
1732            return None;
1733        }
1734        self.children_collected.push((node_id, node));
1735        match parent {
1736            Some(parent_node) => {
1737                for (id, collected) in self.children_collected.iter_mut() {
1738                    if *id == parent_node {
1739                        collected.push_child(node_id);
1740                        return Some(node_id);
1741                    }
1742                }
1743                // Parent not found — attach to the widget's own node rather
1744                // than orphaning the child. Caller misused the API.
1745                self.inner.push_child(node_id);
1746            }
1747            None => self.inner.push_child(node_id),
1748        }
1749        Some(node_id)
1750    }
1751
1752    /// Push a `Role::Table` child carrying its dimensions, under `parent`
1753    /// when given and under the widget's own node otherwise.
1754    ///
1755    /// `element_id` must be the document's own durable table id, so the node
1756    /// keeps its `NodeId` across edits and a screen reader's cursor is not
1757    /// thrown out of the table by an unrelated change elsewhere.
1758    pub fn push_table_child(
1759        &mut self,
1760        parent: Option<NodeId>,
1761        element_id: u64,
1762        rows: usize,
1763        columns: usize,
1764    ) -> Option<NodeId> {
1765        let owner = self.owner?;
1766        let node_id = synthetic_node_id(owner, element_id, SyntheticKind::RichTextTable);
1767        let mut node = Node::new(Role::Table);
1768        node.set_row_count(rows);
1769        node.set_column_count(columns);
1770        self.push_collected_child(parent, node_id, node)
1771    }
1772
1773    /// Push a `Role::Row` child under a table pushed by
1774    /// [`Self::push_table_child`].
1775    ///
1776    /// **1-based** `row_index`, the ARIA `aria-rowindex` convention this
1777    /// builder's whole public surface uses — so the first row is 1, exactly as
1778    /// for [`Self::set_row_index`]. The conversion to the zero-based number
1779    /// AccessKit stores happens here, at the same boundary every other ordinal
1780    /// on this builder is converted at.
1781    pub fn push_table_row_child(
1782        &mut self,
1783        parent: NodeId,
1784        element_id: u64,
1785        row_index: usize,
1786    ) -> Option<NodeId> {
1787        let owner = self.owner?;
1788        let node_id = synthetic_node_id(owner, element_id, SyntheticKind::RichTextTableRow);
1789        let mut node = Node::new(Role::Row);
1790        node.set_row_index(to_accesskit_ordinal(row_index));
1791        self.push_collected_child(Some(parent), node_id, node)
1792    }
1793
1794    /// Push a `Role::Cell` child under a row pushed by
1795    /// [`Self::push_table_row_child`].
1796    ///
1797    /// **1-based** `row_index` / `column_index` (see
1798    /// [`Self::push_table_row_child`]); the spans are plain counts and are
1799    /// written only when they exceed 1, so an ordinary cell carries no
1800    /// span properties at all.
1801    pub fn push_table_cell_child(
1802        &mut self,
1803        parent: NodeId,
1804        element_id: u64,
1805        row_index: usize,
1806        column_index: usize,
1807        row_span: usize,
1808        column_span: usize,
1809    ) -> Option<NodeId> {
1810        let owner = self.owner?;
1811        let node_id = synthetic_node_id(owner, element_id, SyntheticKind::RichTextTableCell);
1812        let mut node = Node::new(Role::Cell);
1813        node.set_row_index(to_accesskit_ordinal(row_index));
1814        node.set_column_index(to_accesskit_ordinal(column_index));
1815        if row_span > 1 {
1816            node.set_row_span(row_span);
1817        }
1818        if column_span > 1 {
1819            node.set_column_span(column_span);
1820        }
1821        self.push_collected_child(Some(parent), node_id, node)
1822    }
1823
1824    /// Push the `Role::Label` text container inside a cell, and return it so
1825    /// the cell's text runs can be pushed under it.
1826    ///
1827    /// See [`SyntheticKind::RichTextCellText`] for why the runs may not hang
1828    /// off the `Role::Cell` directly.
1829    ///
1830    /// The container is deliberately given no value of its own: the text is
1831    /// carried by the runs beneath it, which is how every other text surface
1832    /// in the framework exposes its content, and duplicating it here would
1833    /// have a reader announce the cell twice.
1834    pub fn push_cell_text_child(&mut self, parent: NodeId, element_id: u64) -> Option<NodeId> {
1835        let owner = self.owner?;
1836        let node_id = synthetic_node_id(owner, element_id, SyntheticKind::RichTextCellText);
1837        let node = Node::new(Role::Label);
1838        self.push_collected_child(Some(parent), node_id, node)
1839    }
1840
1841    /// Push the `Role::Label` text container inside a structural block-level
1842    /// node, and return it so that block's text runs can be pushed under it.
1843    ///
1844    /// See [`SyntheticKind::RichTextBlockText`]. A heading keeps its own
1845    /// `Role::Heading` node — it is how a reader jumps through a document —
1846    /// a blockquote its `Role::Blockquote` and a list item its
1847    /// `Role::ListItem`; this sits between any of them and the runs so the
1848    /// text changes beneath still reach the platform.
1849    ///
1850    /// `element_id` is the **block's** id, so a heading nested in a blockquote
1851    /// still produces exactly one container.
1852    pub fn push_block_text_child(&mut self, parent: NodeId, element_id: u64) -> Option<NodeId> {
1853        let owner = self.owner?;
1854        let node_id = synthetic_node_id(owner, element_id, SyntheticKind::RichTextBlockText);
1855        let node = Node::new(Role::Label);
1856        self.push_collected_child(Some(parent), node_id, node)
1857    }
1858
1859    /// Push a `Role::Blockquote` child, under `parent` when given and under
1860    /// the widget's own node otherwise.
1861    ///
1862    /// `element_id` must be the document's own frame id, so the node keeps its
1863    /// `NodeId` across edits.
1864    pub fn push_blockquote_child(
1865        &mut self,
1866        parent: Option<NodeId>,
1867        element_id: u64,
1868    ) -> Option<NodeId> {
1869        let owner = self.owner?;
1870        let node_id = synthetic_node_id(owner, element_id, SyntheticKind::RichTextBlockquote);
1871        let node = Node::new(Role::Blockquote);
1872        self.push_collected_child(parent, node_id, node)
1873    }
1874
1875    /// Push a `Role::Heading` child at `level` (1 to 6), under `parent` when
1876    /// given and under the widget's own node otherwise.
1877    ///
1878    /// The node [`Self::push_paragraph_child`] pushes and
1879    /// [`Self::set_paragraph_as_heading`] makes a heading, same id and same
1880    /// level, for a heading inside another node: `push_paragraph_child` hangs
1881    /// every paragraph off the widget's own node, so a heading opening a
1882    /// blockquote was read after the quote's other text.
1883    pub fn push_heading_child(
1884        &mut self,
1885        parent: Option<NodeId>,
1886        element_id: u64,
1887        level: u8,
1888    ) -> Option<NodeId> {
1889        let owner = self.owner?;
1890        let node_id = synthetic_node_id(owner, element_id, SyntheticKind::Paragraph);
1891        let mut node = Node::new(Role::Heading);
1892        node.set_level(to_accesskit_ordinal((level as usize).clamp(1, 6)));
1893        self.push_collected_child(parent, node_id, node)
1894    }
1895
1896    /// Push a `Role::List` child of `size_of_set` items, under `parent` when
1897    /// given and under the widget's own node otherwise.
1898    ///
1899    /// `size_of_set` is the whole list's item count. Its items carry none
1900    /// ([`Self::push_list_item_child`]): the AT-SPI and Windows adapters read
1901    /// an item's set size from the nearest container that has one, and the
1902    /// macOS adapter reads neither a size nor a position. A list a paragraph
1903    /// interrupts is two list nodes with the one count, so the item after the
1904    /// paragraph is still "3 of 5".
1905    ///
1906    /// `element_id` must be durable across edits: the rich-text walk passes
1907    /// the block id of the list's first item.
1908    pub fn push_list_child(
1909        &mut self,
1910        parent: Option<NodeId>,
1911        element_id: u64,
1912        size_of_set: usize,
1913    ) -> Option<NodeId> {
1914        let owner = self.owner?;
1915        let node_id = synthetic_node_id(owner, element_id, SyntheticKind::RichTextList);
1916        let mut node = Node::new(Role::List);
1917        node.set_size_of_set(size_of_set);
1918        self.push_collected_child(parent, node_id, node)
1919    }
1920
1921    /// Push a `Role::ListItem` child under a list pushed by
1922    /// [`Self::push_list_child`].
1923    ///
1924    /// **1-based** `level` (1 for an item of a top-level list, 2 for an item
1925    /// of a list nested in one) and `position_in_set`, the builder's ordinal
1926    /// convention (see [`Self::push_table_row_child`]).
1927    pub fn push_list_item_child(
1928        &mut self,
1929        parent: NodeId,
1930        element_id: u64,
1931        level: usize,
1932        position_in_set: usize,
1933    ) -> Option<NodeId> {
1934        let owner = self.owner?;
1935        let node_id = synthetic_node_id(owner, element_id, SyntheticKind::RichTextListItem);
1936        let mut node = Node::new(Role::ListItem);
1937        node.set_level(to_accesskit_ordinal(level));
1938        node.set_position_in_set(to_accesskit_ordinal(position_in_set));
1939        self.push_collected_child(Some(parent), node_id, node)
1940    }
1941
1942    /// Push one `Role::TextRun` child, under `parent` when given and
1943    /// under the widget's own node otherwise.
1944    ///
1945    /// The single door every text run in the framework goes through.
1946    /// Returns `None` when the builder has no owner (so no id can be
1947    /// derived) or when the id it derived is already taken — a duplicate
1948    /// child id panics `accesskit_consumer`'s tree builder, so it is
1949    /// dropped here with a diagnostic instead.
1950    ///
1951    /// Prefer [`text_runs::push_text_runs`], which owns the chunking,
1952    /// the word segmentation, the hard-break rule and the line links;
1953    /// this is the primitive underneath it.
1954    pub fn push_text_run(&mut self, parent: Option<NodeId>, spec: TextRunSpec) -> Option<NodeId> {
1955        let owner = self.owner?;
1956        let node_id = synthetic_node_id(owner, spec.element_id, SyntheticKind::TextRun);
1957        if let Some((existing, _)) = self
1958            .children_collected
1959            .iter()
1960            .find(|(id, _)| *id == node_id)
1961        {
1962            debug_assert!(
1963                false,
1964                "Teksilo bug: two text runs of widget {owner:?} derived the same \
1965                 accessibility node id {existing:?}. Give each source its own \
1966                 `TextRunSource::id_seed`. Please file a bug report."
1967            );
1968            eprintln!(
1969                "Teksilo bug: two text runs of widget {owner:?} derived the same \
1970                 accessibility node id {existing:?}; the second was dropped. \
1971                 Please file a bug report."
1972            );
1973            return None;
1974        }
1975
1976        let TextRunSpec {
1977            value,
1978            character_lengths,
1979            word_starts,
1980            character_positions,
1981            character_widths,
1982            bounds,
1983            bounds_are_absolute,
1984            text_direction,
1985            attrs,
1986            ..
1987        } = spec;
1988
1989        debug_assert_eq!(
1990            character_lengths.iter().map(|n| *n as usize).sum::<usize>(),
1991            value.len(),
1992            "AccessKit requires the character lengths of a text run to sum to its value's byte length"
1993        );
1994        let mut node = Node::new(Role::TextRun);
1995        node.set_value(value);
1996        let expected = character_lengths.len();
1997        node.set_character_lengths(character_lengths);
1998        node.set_word_starts(word_starts);
1999        // All four or none: `Range::bounding_boxes()` throws away every box
2000        // it has already collected the moment one run is missing any of
2001        // them, so a half-populated run empties the geometry of every
2002        // range that touches it.
2003        if character_positions.len() == expected && character_widths.len() == expected {
2004            node.set_character_positions(character_positions);
2005            node.set_character_widths(character_widths);
2006        }
2007        node.set_text_direction(text_direction);
2008        if bounds_are_absolute {
2009            node.set_bounds(accesskit::Rect {
2010                x0: bounds.x as f64,
2011                y0: bounds.y as f64,
2012                x1: (bounds.x + bounds.width) as f64,
2013                y1: (bounds.y + bounds.height) as f64,
2014            });
2015        }
2016        apply_run_attrs(&mut node, attrs);
2017
2018        self.children_collected.push((node_id, node));
2019        if !bounds_are_absolute {
2020            self.child_local_bounds.push((node_id, bounds));
2021        }
2022
2023        match parent {
2024            Some(parent_node) => {
2025                for (id, node) in self.children_collected.iter_mut() {
2026                    if *id == parent_node {
2027                        node.push_child(node_id);
2028                        return Some(node_id);
2029                    }
2030                }
2031                // Parent not found — attach to the widget's own node
2032                // rather than orphaning the run. Caller misused the API.
2033                self.inner.push_child(node_id);
2034            }
2035            None => self.inner.push_child(node_id),
2036        }
2037        Some(node_id)
2038    }
2039
2040    /// Push a `Role::TextRun` child under `parent_node` (usually a
2041    /// paragraph NodeId returned from `push_paragraph_child`, but
2042    /// may also be the widget's own node for inline editors).
2043    ///
2044    /// `element_id` is the stable id of the underlying text-document
2045    /// inline element; combined with `parent_widget` and a
2046    /// disambiguator it produces a synthetic NodeId that survives
2047    /// edits. `fragment_offset` is the block-relative character
2048    /// offset of this run — used as the disambiguator so two
2049    /// highlight-split sub-runs sharing one source element don't
2050    /// collide.
2051    ///
2052    /// `character_lengths` must be the UTF-8 byte length of each
2053    /// character in `value`, per AccessKit's contract. Optional
2054    /// `word_starts`, `character_positions`, and `character_widths`
2055    /// populate the corresponding AccessKit properties.
2056    ///
2057    /// Returns the allocated synthetic `NodeId` so the caller can
2058    /// reference it later when attaching a `TextSelection` via
2059    /// `set_text_selection_to`.
2060    #[allow(clippy::too_many_arguments)]
2061    pub fn push_text_run_child(
2062        &mut self,
2063        parent_node: NodeId,
2064        element_id: u64,
2065        fragment_offset: usize,
2066        value: String,
2067        character_lengths: Vec<u8>,
2068        word_starts: Option<Vec<u8>>,
2069        character_positions: Option<Vec<f32>>,
2070        character_widths: Option<Vec<f32>>,
2071        attrs: TextRunAttributes,
2072    ) -> NodeId {
2073        let Some(owner) = self.owner else {
2074            debug_assert!(
2075                false,
2076                "push_text_run_child called on a builder with no owner — \
2077                 widgets must only call this from Widget::accessibility"
2078            );
2079            return NodeId(0);
2080        };
2081        // Give sub-runs of one source element (a highlight split, or a run
2082        // chunked to stay under the AccessKit word-start cap) distinct NodeIds.
2083        // A plain `element_id ^ (fragment_offset << 32)` would XOR the offset
2084        // into the very bits `element_id` already uses to encode the owning
2085        // block, so a chunk at offset 255 in block A could alias a whole-line run
2086        // in a block whose id is `A ^ 255`. Hashing the offset across all 64 bits
2087        // removes that structure; `fragment_offset == 0` (the whole-run common
2088        // case) stays a no-op, so those NodeIds are unchanged.
2089        let mixed_element = if fragment_offset == 0 {
2090            element_id
2091        } else {
2092            fnv_mix_u64(element_id, fragment_offset as u64, 0)
2093        };
2094        let node_id = synthetic_node_id(owner, mixed_element, SyntheticKind::TextRun);
2095        let mut node = Node::new(Role::TextRun);
2096        node.set_value(value);
2097        node.set_character_lengths(character_lengths);
2098        if let Some(ws) = word_starts {
2099            node.set_word_starts(ws);
2100        }
2101        if let Some(pos) = character_positions {
2102            node.set_character_positions(pos);
2103        }
2104        if let Some(widths) = character_widths {
2105            node.set_character_widths(widths);
2106        }
2107        // Text attributes (WCAG 1.3.1 / EN 301 549 11.5.2.9). AccessKit has no
2108        // bold flag, so an explicit weight wins, else bold => 700.
2109        if let Some(w) = attrs.font_weight {
2110            node.set_font_weight(w as f32);
2111        } else if attrs.bold {
2112            node.set_font_weight(700.0);
2113        }
2114        if attrs.italic {
2115            node.set_italic();
2116        }
2117        if attrs.underline {
2118            node.set_underline(default_text_decoration());
2119        }
2120        if attrs.strikethrough {
2121            node.set_strikethrough(default_text_decoration());
2122        }
2123        self.children_collected.push((node_id, node));
2124        // Attach the text-run to its parent paragraph's child list.
2125        // The parent must already be in `children_collected`.
2126        for (id, parent) in self.children_collected.iter_mut() {
2127            if *id == parent_node {
2128                parent.push_child(node_id);
2129                return node_id;
2130            }
2131        }
2132        // Parent not found — push as a direct child of the widget's
2133        // own node as a fallback. Caller misused the API.
2134        self.inner.push_child(node_id);
2135        node_id
2136    }
2137
2138    /// Push a single `Role::TextRun` child attached **directly** to the
2139    /// widget's own node (no intervening `Role::Paragraph`). This is the
2140    /// single-line text-input shape: `Role::TextInput` → one
2141    /// `Role::TextRun`.
2142    ///
2143    /// Required for screen-reader typing echo. accesskit_consumer's
2144    /// `supports_text_ranges()` returns `false` for a text input that
2145    /// only sets `character_lengths` on its *own* node — it needs a
2146    /// `Role::TextRun` child. Without it the macOS adapter never emits
2147    /// `AXSelectedTextChanged`, so VoiceOver reads the value once on
2148    /// focus but never echoes characters/words while typing. Emit this
2149    /// even when `value` / `character_lengths` are empty so
2150    /// `supports_text_ranges()` is already true before the first
2151    /// keystroke (the change-diff's *old* node must also support ranges
2152    /// for the notification to fire). Target the caret/selection at the
2153    /// returned `NodeId` via [`set_text_selection_to`](Self::set_text_selection_to).
2154    pub fn push_text_run_child_on_self(
2155        &mut self,
2156        element_id: u64,
2157        value: String,
2158        character_lengths: Vec<u8>,
2159        word_starts: Option<Vec<u8>>,
2160    ) -> NodeId {
2161        let Some(owner) = self.owner else {
2162            debug_assert!(
2163                false,
2164                "push_text_run_child_on_self called on a builder with no owner — \
2165                 widgets must only call this from Widget::accessibility"
2166            );
2167            return NodeId(0);
2168        };
2169        let node_id = synthetic_node_id(owner, element_id, SyntheticKind::TextRun);
2170        let mut node = Node::new(Role::TextRun);
2171        node.set_value(value);
2172        node.set_character_lengths(character_lengths);
2173        if let Some(ws) = word_starts {
2174            node.set_word_starts(ws);
2175        }
2176        self.children_collected.push((node_id, node));
2177        self.inner.push_child(node_id);
2178        node_id
2179    }
2180
2181    /// Declare a text selection that references TextRun children
2182    /// previously emitted via `push_text_run_child`. Both the
2183    /// anchor and the focus are expressed as
2184    /// `(NodeId, character_index)` pairs where the character index
2185    /// is an index into the target TextRun's `character_lengths`
2186    /// (NOT a document-absolute offset — per AccessKit's contract).
2187    pub fn set_text_selection_to(&mut self, anchor: (NodeId, usize), focus: (NodeId, usize)) {
2188        self.pending_explicit_selection = Some((
2189            TextPosition {
2190                node: anchor.0,
2191                character_index: anchor.1,
2192            },
2193            TextPosition {
2194                node: focus.0,
2195                character_index: focus.1,
2196            },
2197        ));
2198    }
2199}
2200
2201impl Default for AccessNodeBuilder {
2202    fn default() -> Self {
2203        Self::new()
2204    }
2205}
2206
2207/// Convert a WidgetId to an AccessKit NodeId.
2208///
2209/// This is the id the tree gives the widget's node, and the one every update
2210/// [`crate::WidgetTree::sync_accessibility`] returns carries. A platform
2211/// adapter can hold another for the same node: one that left the tree a
2212/// reader sees and came back is handed a new id on its way out, see
2213/// [`crate::WidgetTree::deliver_accessibility`].
2214pub fn widget_id_to_node_id(id: WidgetId) -> NodeId {
2215    use slotmap::Key;
2216    let key_data = id.data();
2217    let raw = key_data.as_ffi();
2218    NodeId(raw)
2219}
2220
2221/// Convert an AccessKit NodeId back to a WidgetId. Returns `None`
2222/// for synthetic NodeIds (widget-emitted child nodes like TextRuns);
2223/// callers that need to route an `ActionRequest` targeting a
2224/// synthetic NodeId must consult `WidgetTree::synthetic_parent_map`
2225/// to find the owning widget.
2226pub fn node_id_to_widget_id_maybe(node_id: NodeId) -> Option<WidgetId> {
2227    if is_synthetic(node_id) {
2228        return None;
2229    }
2230    use slotmap::KeyData;
2231    let key_data = KeyData::from_ffi(node_id.0);
2232    Some(key_data.into())
2233}
2234
2235/// Legacy infallible converter kept for existing call sites that
2236/// never encounter synthetic NodeIds. New code should prefer
2237/// [`node_id_to_widget_id_maybe`]. Panics in debug for synthetic
2238/// ids to catch misrouted calls early.
2239pub fn node_id_to_widget_id(node_id: NodeId) -> WidgetId {
2240    debug_assert!(
2241        !is_synthetic(node_id),
2242        "node_id_to_widget_id called on synthetic NodeId — use node_id_to_widget_id_maybe"
2243    );
2244    use slotmap::KeyData;
2245    let key_data = KeyData::from_ffi(node_id.0);
2246    key_data.into()
2247}
2248
2249/// The special root node ID for the accessibility tree.
2250pub fn root_node_id() -> NodeId {
2251    NodeId(0)
2252}
2253
2254/// Query result for accessibility information about a widget.
2255#[derive(Debug)]
2256pub struct AccessibilityInfo {
2257    role: Role,
2258    name: Option<String>,
2259    actions: Vec<Action>,
2260    toggled: Option<bool>,
2261    expanded: Option<bool>,
2262    selected: Option<bool>,
2263    disabled: bool,
2264    hidden: bool,
2265    published: bool,
2266    value: Option<String>,
2267    numeric_value: Option<f64>,
2268    min_numeric_value: Option<f64>,
2269    max_numeric_value: Option<f64>,
2270    custom_actions: Vec<String>,
2271}
2272
2273impl AccessibilityInfo {
2274    pub fn new(role: Role, name: Option<String>, actions: Vec<Action>) -> Self {
2275        Self {
2276            role,
2277            name,
2278            actions,
2279            toggled: None,
2280            expanded: None,
2281            selected: None,
2282            disabled: false,
2283            hidden: false,
2284            published: true,
2285            value: None,
2286            numeric_value: None,
2287            min_numeric_value: None,
2288            max_numeric_value: None,
2289            custom_actions: Vec::new(),
2290        }
2291    }
2292
2293    pub fn with_value(mut self, value: impl Into<String>) -> Self {
2294        self.value = Some(value.into());
2295        self
2296    }
2297
2298    pub fn with_numeric_value(mut self, value: f64) -> Self {
2299        self.numeric_value = Some(value);
2300        self
2301    }
2302
2303    /// Set the numeric range; either bound may be absent.
2304    pub fn with_numeric_range(mut self, min: Option<f64>, max: Option<f64>) -> Self {
2305        self.min_numeric_value = min;
2306        self.max_numeric_value = max;
2307        self
2308    }
2309
2310    /// Set the custom-action labels, in declaration order.
2311    pub fn with_custom_actions(mut self, labels: Vec<String>) -> Self {
2312        self.custom_actions = labels;
2313        self
2314    }
2315
2316    pub fn with_toggled(mut self, toggled: bool) -> Self {
2317        self.toggled = Some(toggled);
2318        self
2319    }
2320
2321    pub fn with_expanded(mut self, expanded: bool) -> Self {
2322        self.expanded = Some(expanded);
2323        self
2324    }
2325
2326    pub fn with_selected(mut self, selected: bool) -> Self {
2327        self.selected = Some(selected);
2328        self
2329    }
2330
2331    pub fn with_disabled(mut self, disabled: bool) -> Self {
2332        self.disabled = disabled;
2333        self
2334    }
2335
2336    pub fn with_hidden(mut self, hidden: bool) -> Self {
2337        self.hidden = hidden;
2338        self
2339    }
2340
2341    /// Record whether the node is in the published AT tree; see
2342    /// [`published`](Self::published). A node is published unless set
2343    /// otherwise.
2344    pub fn with_published(mut self, published: bool) -> Self {
2345        self.published = published;
2346        self
2347    }
2348
2349    pub fn role(&self) -> Role {
2350        self.role
2351    }
2352
2353    pub fn name(&self) -> Option<&str> {
2354        self.name.as_deref()
2355    }
2356
2357    pub fn actions(&self) -> &[Action] {
2358        &self.actions
2359    }
2360
2361    pub fn is_toggled(&self) -> bool {
2362        self.toggled.unwrap_or(false)
2363    }
2364
2365    pub fn is_expanded(&self) -> bool {
2366        self.expanded.unwrap_or(false)
2367    }
2368
2369    pub fn is_selected(&self) -> bool {
2370        self.selected.unwrap_or(false)
2371    }
2372
2373    pub fn is_disabled(&self) -> bool {
2374        self.disabled
2375    }
2376
2377    pub fn is_hidden(&self) -> bool {
2378        self.hidden
2379    }
2380
2381    /// Whether assistive technology can reach this node: `false` for a
2382    /// dormant node (a hidden `Switcher` page, a `visible_when(false)`
2383    /// branch), one under an ancestor whose subtree is excluded or merged,
2384    /// and one marked [hidden](Self::is_hidden).
2385    ///
2386    /// The other getters describe what the widget would say; this says
2387    /// whether it is being heard. A test that reads names across a tree
2388    /// filters on it.
2389    pub fn published(&self) -> bool {
2390        self.published && !self.hidden
2391    }
2392
2393    /// The node's value text (`Node::value`), if it has one.
2394    pub fn value(&self) -> Option<&str> {
2395        self.value.as_deref()
2396    }
2397
2398    /// The node's numeric value (a slider's position, a spin box's number).
2399    pub fn numeric_value(&self) -> Option<f64> {
2400        self.numeric_value
2401    }
2402
2403    /// The lower bound of the numeric range, if the node declares one.
2404    pub fn min_numeric_value(&self) -> Option<f64> {
2405        self.min_numeric_value
2406    }
2407
2408    /// The upper bound of the numeric range, if the node declares one.
2409    pub fn max_numeric_value(&self) -> Option<f64> {
2410        self.max_numeric_value
2411    }
2412
2413    /// The labels of the node's custom actions, in declaration order (the
2414    /// widget's own first, then any `access_custom_action` overrides).
2415    pub fn custom_actions(&self) -> &[String] {
2416        &self.custom_actions
2417    }
2418}
2419
2420#[cfg(test)]
2421mod tests {
2422    use super::*;
2423
2424    fn fake_widget(id: u64) -> WidgetId {
2425        slotmap::KeyData::from_ffi(id).into()
2426    }
2427
2428    #[test]
2429    fn widget_derived_node_id_has_bit_63_clear() {
2430        // A freshly-minted slotmap key (version 1, index 0) encodes
2431        // to a u64 with bit 63 clear. The top-bit namespace split
2432        // (synthetic NodeIds set bit 63, widget-derived NodeIds clear
2433        // it) only works if widget-derived NodeIds stay below bit 63.
2434        let wid = fake_widget(1);
2435        let nid = widget_id_to_node_id(wid);
2436        assert_eq!(
2437            nid.0 & SYNTHETIC_BIT,
2438            0,
2439            "widget NodeId must have bit 63 clear"
2440        );
2441        assert!(!is_synthetic(nid));
2442    }
2443
2444    #[test]
2445    fn synthetic_node_id_has_bit_63_set() {
2446        let wid = fake_widget(42);
2447        let nid = synthetic_node_id(wid, 17, SyntheticKind::TextRun);
2448        assert_eq!(nid.0 & SYNTHETIC_BIT, SYNTHETIC_BIT);
2449        assert!(is_synthetic(nid));
2450    }
2451
2452    #[test]
2453    fn link_runs_on_line_chains_runs_both_ways() {
2454        let mut b = AccessNodeBuilder::for_widget(fake_widget(1));
2455        let para = b.push_paragraph_child(1);
2456        let run = |b: &mut AccessNodeBuilder, off: usize| {
2457            b.push_text_run_child(
2458                para,
2459                10,
2460                off,
2461                "abc".to_string(),
2462                vec![1, 1, 1],
2463                None,
2464                None,
2465                None,
2466                TextRunAttributes::default(),
2467            )
2468        };
2469        let (r0, r1, r2) = (run(&mut b, 0), run(&mut b, 3), run(&mut b, 6));
2470        b.link_runs_on_line(&[r0, r1, r2]);
2471
2472        let (_id, _n, children, _local) = b.build(fake_widget(1));
2473        let node = |id| {
2474            children
2475                .iter()
2476                .find(|(i, _)| *i == id)
2477                .map(|(_, n)| n)
2478                .unwrap()
2479        };
2480        // First run: forward only. Middle: both. Last: back only.
2481        assert_eq!(node(r0).previous_on_line(), None);
2482        assert_eq!(node(r0).next_on_line(), Some(r1));
2483        assert_eq!(node(r1).previous_on_line(), Some(r0));
2484        assert_eq!(node(r1).next_on_line(), Some(r2));
2485        assert_eq!(node(r2).previous_on_line(), Some(r1));
2486        assert_eq!(node(r2).next_on_line(), None);
2487    }
2488
2489    /// A chunk at a non-zero offset in one element must not collide with a
2490    /// whole run in another element, even when the two element ids differ by
2491    /// exactly the low-byte XOR of the chunk offset — the aliasing the old
2492    /// `element_id ^ (offset << 32)` mix allowed.
2493    #[test]
2494    fn a_chunk_offset_does_not_alias_another_elements_run() {
2495        let mut b = AccessNodeBuilder::for_widget(fake_widget(1));
2496        let para = b.push_paragraph_child(1);
2497        // `synth_element_id` encodes the block id in bits 32-61.
2498        let elem_a: u64 = 0xABCD_u64 << 32;
2499        let elem_b: u64 = (0xABCD_u64 ^ 255) << 32;
2500        let run = |b: &mut AccessNodeBuilder, elem: u64, off: usize| {
2501            b.push_text_run_child(
2502                para,
2503                elem,
2504                off,
2505                "x".to_string(),
2506                vec![1],
2507                None,
2508                None,
2509                None,
2510                TextRunAttributes::default(),
2511            )
2512        };
2513        let a_chunk = run(&mut b, elem_a, 255); // offset-255 chunk in block A
2514        let b_whole = run(&mut b, elem_b, 0); // whole run in block B = A ^ 255
2515        assert_ne!(
2516            a_chunk, b_whole,
2517            "a chunk offset must not alias another block's run NodeId"
2518        );
2519    }
2520
2521    #[test]
2522    fn set_child_position_in_set_numbers_a_paragraph() {
2523        let mut b = AccessNodeBuilder::for_widget(fake_widget(1));
2524        let para = b.push_paragraph_child(5);
2525        assert!(b.set_child_position_in_set(para, 42, 200));
2526        assert!(
2527            !b.set_child_position_in_set(NodeId(999), 1, 1),
2528            "an unknown child is not found"
2529        );
2530
2531        let (_id, own, children, _local) = b.build(fake_widget(1));
2532        let p = children
2533            .iter()
2534            .find(|(i, _)| *i == para)
2535            .map(|(_, n)| n)
2536            .unwrap();
2537        // The caller says "line 42", the ARIA convention. AccessKit stores the
2538        // zero-based 41, and the Windows and AT-SPI adapters add the 1 back.
2539        assert_eq!(p.position_in_set(), Some(41));
2540        // The set size belongs on the container, which here is the widget's own
2541        // node: `size_of_set_from_container` walks up from the item's parent,
2542        // so a size written on the paragraph itself is read by no adapter.
2543        assert_eq!(p.size_of_set(), None);
2544        assert_eq!(own.size_of_set(), Some(200));
2545    }
2546
2547    /// The whole point of the 1-based public surface: a caller writes the
2548    /// number a person would say, and AccessKit gets the number it expects.
2549    #[test]
2550    fn every_aria_ordinal_is_converted_at_the_boundary() {
2551        let mut b = AccessNodeBuilder::for_widget(fake_widget(1));
2552        b.set_position_in_set(1);
2553        b.set_row_index(1);
2554        b.set_column_index(1);
2555        b.set_level(1);
2556        let (_id, n, _children, _local) = b.build(fake_widget(1));
2557        assert_eq!(n.position_in_set(), Some(0), "the first item is index 0");
2558        assert_eq!(n.row_index(), Some(0), "the header row is row 0");
2559        assert_eq!(n.column_index(), Some(0), "the leftmost column is column 0");
2560        assert_eq!(n.level(), Some(0), "a root item is level 0");
2561    }
2562
2563    /// A count has no base, so it must pass through untouched. Getting this
2564    /// wrong would turn "3 of 12" into "3 of 11".
2565    #[test]
2566    fn a_count_is_not_an_ordinal_and_is_not_converted() {
2567        let mut b = AccessNodeBuilder::for_widget(fake_widget(1));
2568        b.set_size_of_set(12);
2569        b.set_row_count(100);
2570        b.set_column_count(4);
2571        b.set_row_span(2);
2572        b.set_column_span(3);
2573        let (_id, n, _children, _local) = b.build(fake_widget(1));
2574        assert_eq!(n.size_of_set(), Some(12));
2575        assert_eq!(n.row_count(), Some(100));
2576        assert_eq!(n.column_count(), Some(4));
2577        assert_eq!(n.row_span(), Some(2));
2578        assert_eq!(n.column_span(), Some(3));
2579    }
2580
2581    /// A heading level is the same ARIA convention, and an H1 must be able to
2582    /// announce as level 1. Before the conversion the clamp floor of 1 made
2583    /// AccessKit level 0 unreachable, so no heading anywhere could.
2584    #[test]
2585    fn an_h1_is_accesskit_level_zero() {
2586        for (heading, expected) in [(1u8, 0usize), (2, 1), (6, 5)] {
2587            let mut b = AccessNodeBuilder::for_widget(fake_widget(1));
2588            let para = b.push_paragraph_child(7);
2589            assert!(b.set_paragraph_as_heading(para, heading));
2590            let (_id, _n, children, _local) = b.build(fake_widget(1));
2591            let p = children
2592                .iter()
2593                .find(|(i, _)| *i == para)
2594                .map(|(_, n)| n)
2595                .unwrap();
2596            assert_eq!(p.role(), Role::Heading);
2597            assert_eq!(p.level(), Some(expected), "h{heading}");
2598        }
2599    }
2600
2601    /// A heading pushed under a parent is the node `push_paragraph_child` and
2602    /// `set_paragraph_as_heading` make, same id and same level, in the parent.
2603    #[test]
2604    fn a_heading_under_a_parent_is_the_paragraph_heading_there() {
2605        let mut flat = AccessNodeBuilder::for_widget(fake_widget(1));
2606        let para = flat.push_paragraph_child(7);
2607        flat.set_paragraph_as_heading(para, 2);
2608        let (_id, _n, flat_children, _local) = flat.build(fake_widget(1));
2609
2610        let mut b = AccessNodeBuilder::for_widget(fake_widget(1));
2611        let quote = b.push_blockquote_child(None, 3).unwrap();
2612        let heading = b.push_heading_child(Some(quote), 7, 2).unwrap();
2613        assert_eq!(heading, para);
2614        let (_id, own, children, _local) = b.build(fake_widget(1));
2615        let node = |id| children.iter().find(|(i, _)| *i == id).map(|(_, n)| n);
2616        assert_eq!(own.children(), &[quote], "the heading is not the widget's");
2617        assert_eq!(node(quote).unwrap().children(), &[heading]);
2618        assert_eq!(node(heading), flat_children.first().map(|(_, n)| n));
2619    }
2620
2621    /// An item says "2 of 3, level 2" in the ARIA convention, and AccessKit
2622    /// gets its zero-based ordinals; the size is on the list, where every
2623    /// adapter looks for it.
2624    #[test]
2625    fn a_list_item_s_ordinals_are_converted_and_its_list_holds_the_size() {
2626        let mut b = AccessNodeBuilder::for_widget(fake_widget(1));
2627        let list = b.push_list_child(None, 4, 3).unwrap();
2628        let item = b.push_list_item_child(list, 5, 2, 2).unwrap();
2629        let (_id, own, children, _local) = b.build(fake_widget(1));
2630        let node = |id| {
2631            children
2632                .iter()
2633                .find(|(i, _)| *i == id)
2634                .map(|(_, n)| n)
2635                .unwrap()
2636        };
2637        assert_eq!(own.children(), &[list]);
2638        assert_eq!(node(list).role(), Role::List);
2639        assert_eq!(node(list).size_of_set(), Some(3));
2640        assert_eq!(node(list).children(), &[item]);
2641        assert_eq!(node(item).role(), Role::ListItem);
2642        assert_eq!(node(item).level(), Some(1));
2643        assert_eq!(node(item).position_in_set(), Some(1));
2644        assert_eq!(node(item).size_of_set(), None);
2645    }
2646
2647    #[test]
2648    fn synthetic_node_id_stable_across_calls() {
2649        // Same (widget, element, kind) produces identical NodeIds —
2650        // this stability is required for screen-reader focus to survive
2651        // accessibility rebuilds.
2652        let wid = fake_widget(42);
2653        let a = synthetic_node_id(wid, 17, SyntheticKind::TextRun);
2654        let b = synthetic_node_id(wid, 17, SyntheticKind::TextRun);
2655        assert_eq!(a, b);
2656    }
2657
2658    #[test]
2659    fn synthetic_node_id_differs_by_kind() {
2660        let wid = fake_widget(42);
2661        let p = synthetic_node_id(wid, 17, SyntheticKind::Paragraph);
2662        let r = synthetic_node_id(wid, 17, SyntheticKind::TextRun);
2663        assert_ne!(
2664            p, r,
2665            "paragraph and text-run kinds must produce distinct NodeIds"
2666        );
2667    }
2668
2669    #[test]
2670    fn synthetic_node_id_differs_by_element() {
2671        let wid = fake_widget(42);
2672        let a = synthetic_node_id(wid, 1, SyntheticKind::TextRun);
2673        let b = synthetic_node_id(wid, 2, SyntheticKind::TextRun);
2674        assert_ne!(a, b);
2675    }
2676
2677    #[test]
2678    fn node_id_to_widget_id_maybe_returns_none_for_synthetic() {
2679        let wid = fake_widget(42);
2680        let syn = synthetic_node_id(wid, 17, SyntheticKind::TextRun);
2681        assert!(node_id_to_widget_id_maybe(syn).is_none());
2682    }
2683
2684    #[test]
2685    fn node_id_to_widget_id_maybe_round_trips_widget_ids() {
2686        let wid = fake_widget(99);
2687        let nid = widget_id_to_node_id(wid);
2688        let back = node_id_to_widget_id_maybe(nid).unwrap();
2689        assert_eq!(wid, back);
2690    }
2691
2692    #[test]
2693    fn push_paragraph_child_and_text_run_child_emit_synthetic_nodes() {
2694        let owner = fake_widget(7);
2695        let mut builder = AccessNodeBuilder::for_widget(owner);
2696        builder.set_role(Role::MultilineTextInput);
2697
2698        let para = builder.push_paragraph_child(100);
2699        let run = builder.push_text_run_child(
2700            para,
2701            200,
2702            0,
2703            "hello".to_string(),
2704            vec![1, 1, 1, 1, 1],
2705            Some(vec![0]),
2706            None,
2707            None,
2708            TextRunAttributes::default(),
2709        );
2710        assert!(is_synthetic(para));
2711        assert!(is_synthetic(run));
2712
2713        let (_nid, _node, children, _local) = builder.build(owner);
2714        // Two emitted children: paragraph + text run.
2715        assert_eq!(children.len(), 2);
2716        assert!(children.iter().any(|(id, _)| *id == para));
2717        assert!(children.iter().any(|(id, _)| *id == run));
2718    }
2719
2720    #[test]
2721    fn text_run_attributes_reach_at_node() {
2722        // Audit G7 / EN 301 549 11.5.2.9: bold / italic / underline /
2723        // strikethrough formatting on a run is exposed on its TextRun node.
2724        let owner = fake_widget(9);
2725        let mut builder = AccessNodeBuilder::for_widget(owner);
2726        builder.set_role(Role::MultilineTextInput);
2727        let para = builder.push_paragraph_child(1);
2728        let run = builder.push_text_run_child(
2729            para,
2730            2,
2731            0,
2732            "ab".to_string(),
2733            vec![1, 1],
2734            None,
2735            None,
2736            None,
2737            TextRunAttributes {
2738                bold: true,
2739                italic: true,
2740                underline: true,
2741                strikethrough: true,
2742                ..Default::default()
2743            },
2744        );
2745        let (_nid, _node, children, _local) = builder.build(owner);
2746        let (_, run_node) = children
2747            .iter()
2748            .find(|(id, _)| *id == run)
2749            .expect("run node");
2750        assert_eq!(
2751            run_node.font_weight(),
2752            Some(700.0),
2753            "bold folds to font weight 700"
2754        );
2755        assert!(run_node.is_italic(), "italic flag set");
2756        assert!(run_node.underline().is_some(), "underline decoration set");
2757        assert!(
2758            run_node.strikethrough().is_some(),
2759            "strikethrough decoration set"
2760        );
2761
2762        // An explicit numeric weight wins over the bold flag.
2763        let owner2 = fake_widget(10);
2764        let mut b2 = AccessNodeBuilder::for_widget(owner2);
2765        b2.set_role(Role::MultilineTextInput);
2766        let p2 = b2.push_paragraph_child(1);
2767        let r2 = b2.push_text_run_child(
2768            p2,
2769            2,
2770            0,
2771            "x".to_string(),
2772            vec![1],
2773            None,
2774            None,
2775            None,
2776            TextRunAttributes {
2777                bold: true,
2778                font_weight: Some(300),
2779                ..Default::default()
2780            },
2781        );
2782        let (_, _, kids2, _local) = b2.build(owner2);
2783        let (_, r2n) = kids2.iter().find(|(id, _)| *id == r2).expect("run2 node");
2784        assert_eq!(
2785            r2n.font_weight(),
2786            Some(300.0),
2787            "explicit weight wins over bold"
2788        );
2789    }
2790
2791    #[test]
2792    fn set_text_selection_to_wins_over_self_selection() {
2793        let owner = fake_widget(3);
2794        let mut builder = AccessNodeBuilder::for_widget(owner);
2795        builder.set_role(Role::MultilineTextInput);
2796        // Emit a paragraph + run so set_text_selection_to has a
2797        // real synthetic NodeId to target.
2798        let para = builder.push_paragraph_child(1);
2799        let run = builder.push_text_run_child(
2800            para,
2801            2,
2802            0,
2803            "ab".to_string(),
2804            vec![1, 1],
2805            None,
2806            None,
2807            None,
2808            TextRunAttributes::default(),
2809        );
2810        // Both a self-targeted AND an explicit selection are
2811        // staged — the explicit one must win.
2812        builder.set_text_selection_on_self(0, 0);
2813        builder.set_text_selection_to((run, 0), (run, 2));
2814        let (_nid, node, _children, _local) = builder.build(owner);
2815        let sel = node.text_selection().expect("text selection set");
2816        assert_eq!(sel.focus.node, run);
2817        assert_eq!(sel.focus.character_index, 2);
2818    }
2819
2820    /// A `Status` holding its text only as a value has no name on any
2821    /// platform, so it announces nothing: the adapters read a value as the
2822    /// name for a `Role::Label` alone.
2823    #[test]
2824    fn a_status_holding_its_text_as_a_value_announces_no_name() {
2825        let mut status = Node::new(Role::Status);
2826        status.set_value("Enregistré".to_string());
2827        assert_eq!(announced_text(&status), None);
2828
2829        status.set_label("Prêt".to_string());
2830        assert_eq!(announced_text(&status), Some("Prêt"));
2831    }
2832
2833    /// A `Role::Label` is named by its value, and by nothing else.
2834    #[test]
2835    fn a_label_is_named_by_its_value_alone() {
2836        let mut label = Node::new(Role::Label);
2837        label.set_label("Titre".to_string());
2838        assert_eq!(announced_text(&label), None);
2839
2840        label.set_value("Enregistré".to_string());
2841        assert_eq!(announced_text(&label), Some("Enregistré"));
2842    }
2843}