Skip to main content

ui/
a11y.rs

1// a11y -- the semantic records as the platform's accessibility tree (plat::a11y): after the
2// passes of a frame the records of every shown window become a snapshot that screen readers
3// read through AT-SPI2 (Linux), UI Automation (Windows), the browser's ARIA shadow tree or
4// Android's virtual views; their actions come back as requests that the next passes carry
5// out through the widgets' own behaviour.
6//
7//   a11y_tree() -> plat::a11y::Snapshot   the tree as assistive technology gets it
8// Runtime (runtime.rs): a11y_frame after the passes (publishes while plat::a11y::active();
9// starts the bridge with the first window frame and hands it i18n::tr for the names of the
10// roles: plat::a11y::set_translator), a11y_poll on E_WAKE (takes the requests).
11// Widgets: behave asks a11y_click(id) (a pending click, toggle, select or open acts like
12// Space on the focused widget: EV_ACT); the sliders ask a11y_value(id), the text boxes
13// a11y_text(id) for a new value (both public for widgets of an app's own).  A click no widget took in its passes becomes a pointer
14// click where the press reaches the record (a11y_aim), after the scroll views brought it into view; there is
15// none when the middle lies on something else.
16//
17// The tree: every record of a window becomes a node in the record order -- roles from the
18// kinds (K_*), states from SS_* (a check box with value 2 is mixed), a heading's level from its
19// value (K_HEADING: Node::value, clamped to 1 ..= 9).  Left out: title bars, empty texts,
20// records without area, boxes without a label (a box with tab records inside is the tab
21// strip).  Containers are lists, dialogs, titled boxes and tab strips: a node's parent is the
22// smallest one on its layer whose rectangle holds the node's centre -- inside a scoped record
23// (SEM_SCOPE: a window of a window area, desk.rs) only one it holds, else itself, and scoped
24// records hold nothing by their place (overlapping windows are siblings).  Menu bar entries
25// next to each other get a menu bar; the rows of a popup (menus, drop-downs) a menu or list that
26// hangs below the open record that opened it.  Names: the label; text boxes, drop-downs (their label
27// is the text / the choice), sliders, progress bars and lists without one take the text or
28// heading record right before them (to the left, or above and aligned).  The focus of a list
29// or tab strip is passed on to its selected row / tab.
30//
31// Rich records (section 19 of the guide; a web page's accessibility tree): the kinds K_TABLE ..=
32// K_FIGURE become nodes with a Sub (Node::role their nearest basic role); a record's SemAx (its
33// setters: id64, parent, name, desc, url, value_text, states, set_pos, level, cell, span, grid,
34// live, range) fills the further fields of its node.  A record with an id64 is keyed by it
35// (RICH | id), takes its name from nothing but its label / name, and every request for it --
36// focus, click, value, text, selection, scroll into view, expand / collapse, increment /
37// decrement, select items -- goes to the app (ui.a11y_request_take) instead of the widgets.  A
38// record with a parent hangs under the record of that id64 (else that widget id) of its window
39// in the same frame; a left out record passes its children on to its own parent, an unknown
40// parent or a loop leaves the record to its rectangle.
41//
42//   ui.announce(text, LIVE_*)       a text for the screen reader (the next frame publishes it)
43//   ui.a11y_request_take()          the next request for a record with an id64
44//   ui.a11y_active()                a client listens: publish what only it needs then
45
46use alloc::boxed::Box;
47use alloc::collections::{BTreeMap, VecDeque};
48use alloc::string::String;
49use alloc::vec::Vec;
50
51use plat::a11y::{self as ax, Act, Node, Op, Role, Snapshot, Sub, WinTree, NONE};
52
53use super::consts::*;
54use super::{Sem, SemAx, Ui};
55
56/// Sem::min of a record that holds the next Sem::max records of its window by their order, not
57/// by their place (a window inside a window area, desk.rs: its content stays inside it where the
58/// windows overlap)
59pub(crate) const SEM_SCOPE: i32 = 0x5C09_E000;
60
61// synthetic containers: keys outside the widget ids (bit 63, kind in bits 40..)
62const SYN: u64 = 1 << 63;
63const SYN_MENUBAR: u64 = 1 << 40;
64const SYN_POPUP: u64 = 2 << 40;
65// the keys of records with an id64: bit 62 and the id (below 2^62; widget keys are the
66// occurrence << 32 | the widget id, far below)
67pub(crate) const RICH: u64 = 1 << 62;
68pub(crate) const ID_MASK: u64 = RICH - 1;
69// at most this many requests wait for the app, announcements for the next frame
70const REQS_MAX: usize = 64;
71const ANNOUNCE_MAX: usize = 16;
72// the longest text of a record's SemAx (bytes; a longer one is cut at a character)
73const AX_TEXT_MAX: usize = 4096;
74
75/// A screen reader's request for a record with an id64 (`ui.a11y_request_take`).
76#[derive(Clone, Debug, PartialEq)]
77#[non_exhaustive]
78pub struct A11yRequest {
79    /// the id of the record's window: 0 the main window, else the id its window_begin got
80    pub win: u32,
81    /// the record's id64
82    pub id: u64,
83    pub action: A11yAction,
84}
85
86impl A11yRequest {
87    /// A request as `ui.a11y_request_take` hands it out (window id, the record's id64, the
88    /// action): for an app's tests of its own handling, the struct being `non_exhaustive`.
89    pub fn new(win: u32, id: u64, action: A11yAction) -> A11yRequest {
90        A11yRequest { win, id, action }
91    }
92}
93
94/// What a screen reader asks of a record (`A11yRequest::action`).
95#[derive(Clone, Debug, PartialEq)]
96#[non_exhaustive]
97pub enum A11yAction {
98    /// give it the keyboard focus
99    Focus,
100    /// its default action: press, follow, toggle, select, open
101    Click,
102    /// scroll it into view
103    ScrollIntoView,
104    /// open / close it (AS_COLLAPSED, SS_OPEN)
105    Expand,
106    Collapse,
107    /// one step up / down (spin buttons, sliders, scroll bars)
108    Increment,
109    Decrement,
110    /// a new number (sliders, spin buttons, scroll bars)
111    SetValue(f64),
112    /// a new text (text boxes)
113    SetText(String),
114    /// the selection of a text box: anchor, caret in characters (equal: the caret alone)
115    SelectText(u32, u32),
116    /// select it alone (a row, a tab, a tree item)
117    Select,
118    /// add it to / take it out of the selection (AS_MULTISELECT containers)
119    AddToSelection,
120    RemoveFromSelection,
121}
122
123/// the requests waiting for the widgets (shared by all windows)
124#[derive(Default)]
125pub(crate) struct S {
126    started: bool,
127    /// the requests for records with an id64, for the app (a11y_request_take)
128    reqs: VecDeque<A11yRequest>,
129    /// texts to say with the next publish: window, text, politeness (announce)
130    announce: Vec<(u8, String, u8)>,
131    /// (headless runs) the texts said, for the scripts' `a11y expect announced`
132    said: Vec<String>,
133    /// a click for (window, id) and which record of that id it is (the fallback click)
134    click: Option<(u8, u32, usize)>,
135    value: Option<(u8, u32, i32)>,
136    text: Option<(u8, u32, String)>,
137    /// a selection for (window, text box): anchor, caret in characters (waits for the focus)
138    select: Option<(u8, u32, i32, i32)>,
139    /// plat::a11y::active() as the app last heard of it (UIC_A11Y)
140    active: bool,
141    /// what the tree published last was made of (a11y_key; 0: none): an equal frame publishes
142    /// nothing
143    published: u64,
144    /// the caret of the focused text box per window: the pass that drew it, x, y, w, h (window
145    /// px; ime_pos reports it)
146    caret: [(u32, [i32; 4]); WIN_MAX],
147}
148
149impl S {
150    /// a click waits for its widget (behave asks a11y_click only then)
151    pub(crate) fn pending(&self) -> bool {
152        self.click.is_some()
153    }
154}
155
156// a node being built: its node, its layer, where it hangs
157struct Cand {
158    nd: Node,
159    layer: u8,
160    /// place among its siblings (record order; a synthetic container: its first member's)
161    pos: usize,
162    container: bool,
163    parent: usize,
164    /// members of a synthetic container (their parent is fixed)
165    fixed: bool,
166    /// the innermost scoped record (SEM_SCOPE) on its layer that holds it (usize::MAX: none)
167    scope: usize,
168    /// it is a scoped record itself (it holds only what its scope says)
169    scoped: bool,
170    /// its record's widget id, the parent it names (Sem::parent; 0: none)
171    wid: u32,
172    want: u64,
173}
174
175fn centre(r: &[i32; 4]) -> (i32, i32) {
176    (r[0] + r[2] / 2, r[1] + r[3] / 2)
177}
178
179fn holds(r: &[i32; 4], x: i32, y: i32) -> bool {
180    x >= r[0] && y >= r[1] && x < r[0] + r[2] && y < r[1] + r[3]
181}
182
183fn area(r: &[i32; 4]) -> i64 {
184    r[2].max(0) as i64 * r[3].max(0) as i64
185}
186
187fn union(a: [i32; 4], b: &[i32; 4]) -> [i32; 4] {
188    if a[2] <= 0 || a[3] <= 0 {
189        return *b;
190    }
191    let x0 = a[0].min(b[0]);
192    let y0 = a[1].min(b[1]);
193    let x1 = (a[0] + a[2]).max(b[0] + b[2]);
194    let y1 = (a[1] + a[3]).max(b[1] + b[3]);
195    [x0, y0, x1 - x0, y1 - y0]
196}
197
198// the Sub of a record's kind (Sub::None: a basic kind)
199fn sub_of(s: &Sem, named: bool) -> Sub {
200    match s.kind {
201        K_TABLE => Sub::Table,
202        K_ROW => Sub::Row,
203        K_CELL => Sub::Cell,
204        K_COLHEADER => Sub::ColumnHeader,
205        K_ROWHEADER => Sub::RowHeader,
206        K_LANDMARK => match s.value {
207            LM_BANNER => Sub::Banner,
208            LM_MAIN => Sub::Main,
209            LM_NAVIGATION => Sub::Navigation,
210            LM_COMPLEMENTARY => Sub::Complementary,
211            LM_CONTENTINFO => Sub::ContentInfo,
212            LM_SEARCH => Sub::Search,
213            LM_FORM => Sub::Form,
214            _ => Sub::Region,
215        },
216        K_TREE => Sub::Tree,
217        K_TREEITEM => Sub::TreeItem,
218        K_RADIOGROUP => Sub::RadioGroup,
219        K_TOOLBAR => Sub::Toolbar,
220        K_TOOLTIP if named => Sub::Tooltip,
221        K_SEPARATOR => Sub::Separator,
222        K_ALERT => Sub::Alert,
223        K_STATUS => Sub::Status,
224        K_LOG => Sub::Log,
225        K_METER => Sub::Meter,
226        K_SPIN => Sub::SpinButton,
227        K_SCROLLBAR => Sub::ScrollBar,
228        K_ARTICLE => Sub::Article,
229        K_FIGURE => Sub::Figure,
230        _ => Sub::None,
231    }
232}
233
234// the role of a record and its Sub (None: left out)
235fn role_of(s: &Sem) -> Option<(Role, Sub)> {
236    let label = s.label.as_deref().unwrap_or("");
237    let named = !label.is_empty() || s.ax.as_ref().and_then(|a| a.name.as_deref()).is_some_and(|n| !n.is_empty());
238    let sub = sub_of(s, named);
239    if let Some(r) = sub.role() {
240        return Some((r, sub));
241    }
242    if s.kind >= K_KIND_COUNT {
243        return None; // (a tooltip without a text)
244    }
245    Some((role_basic(s, named)?, Sub::None))
246}
247
248// the role of a record of a basic kind (None: left out)
249fn role_basic(s: &Sem, named: bool) -> Option<Role> {
250    Some(match s.kind {
251        K_TEXT if named => Role::Label,
252        K_HEADING if named => Role::Heading,
253        K_BUTTON | K_SWATCH => Role::Button,
254        K_CHECKBOX => Role::CheckBox,
255        K_RADIO => Role::Radio,
256        K_SWITCH => Role::Switch,
257        K_SLIDER => Role::Slider,
258        K_PROGRESS => Role::Progress,
259        K_TEXTBOX => Role::TextBox,
260        K_MENU => Role::Menu,
261        K_MENUITEM => Role::MenuItem,
262        K_LIST => Role::List,
263        K_LISTITEM => Role::ListItem,
264        K_TAB => Role::Tab,
265        K_CANVAS => Role::Canvas,
266        K_ICON | K_IMAGE => Role::Image,
267        K_COMBO => Role::Combo,
268        K_DIALOG => Role::Dialog,
269        K_LINK => Role::Link,
270        // a titled box (notifications, info bars); the tab strip's box (max = number of tabs)
271        K_BOX if named => Role::Group,
272        K_BOX if s.max > 0 => Role::TabList,
273        _ => return None,
274    })
275}
276
277// roles that need a name from the text before them when their label is no name
278fn borrows_name(role: Role) -> bool {
279    matches!(role, Role::TextBox | Role::Combo | Role::Slider | Role::Progress | Role::List)
280}
281
282fn focusable(role: Role) -> bool {
283    matches!(
284        role,
285        Role::Button | Role::CheckBox | Role::Radio | Role::Switch | Role::Slider | Role::TextBox | Role::Link | Role::List
286            | Role::ListItem | Role::Menu | Role::MenuItem | Role::TabList | Role::Tab | Role::Combo
287    )
288}
289
290// the characters before byte offset b of s
291fn chars_before(s: &str, b: i32) -> i32 {
292    let mut b = (b.max(0) as usize).min(s.len());
293    while !s.is_char_boundary(b) {
294        b -= 1;
295    }
296    s[..b].chars().count() as i32
297}
298
299impl Ui {
300    /// The accessibility tree of the last pass as assistive technology gets it (screen
301    /// readers through AT-SPI2 / UI Automation): every shown window with its nodes (roles,
302    /// names, states, values, rectangles), built from the semantic records (see sem()).
303    pub fn a11y_tree(&self) -> Snapshot {
304        let mut snap = Snapshot { app: String::from(self.desc.title), wins: Vec::new() };
305        for i in 0..WIN_MAX as u32 {
306            if i == 0 || self.uiw_drawable(i) {
307                snap.wins.push(self.a11y_win(i));
308            }
309        }
310        snap
311    }
312
313    // the tree of window i
314    fn a11y_win(&self, i: u32) -> WinTree {
315        let recs: Vec<&Sem> = self.sems.iter().filter(|s| s.win as u32 == i).collect();
316        // (room for every record and a few synthetic containers: a Cand is large, growing the Vec
317        // copied them again and again)
318        let mut cands: Vec<Cand> = Vec::with_capacity(recs.len() + 16);
319        let mut occ: BTreeMap<u32, u32> = BTreeMap::new();
320        // the scoped records open at a record: (the last record they hold, their candidate)
321        let mut scopes: Vec<(usize, usize)> = Vec::new();
322        // the id64s given so far; the left out records with an id64: their own parent
323        let mut rich_ids: BTreeMap<u64, usize> = BTreeMap::new();
324        let mut skip: BTreeMap<u64, u64> = BTreeMap::new();
325        for (k, s) in recs.iter().enumerate() {
326            while scopes.last().is_some_and(|e| e.0 < k) {
327                scopes.pop();
328            }
329            let n = occ.entry(s.id).or_insert(0);
330            *n += 1;
331            let (rid, want) = s.ax.as_deref().map_or((0, 0), |a| (a.id & ID_MASK, a.parent));
332            // (a second record of the same id64 is keyed as a widget's)
333            let rich = rid != 0 && !rich_ids.contains_key(&rid);
334            let key = if rich { RICH | rid } else { (*n as u64) << 32 | s.id as u64 };
335            // records without area are left out (those of a rich tree only with a negative size)
336            let empty = if rich { s.rect[2] < 0 || s.rect[3] < 0 } else { s.rect[2] <= 0 || s.rect[3] <= 0 };
337            let Some((role, sub)) = role_of(s).filter(|_| !empty) else {
338                if rich {
339                    skip.insert(rid, want);
340                }
341                continue;
342            };
343            let nd = self.a11y_node(s, role, sub, key, &recs, k);
344            let container = (matches!(role, Role::List | Role::Dialog | Role::Group | Role::TabList) && sub != Sub::Separator)
345                || matches!(sub, Sub::Cell | Sub::ColumnHeader | Sub::RowHeader | Sub::Alert | Sub::Status | Sub::Log);
346            let pos = cands.len();
347            let scope = scopes.iter().rev().find(|e| cands[e.1].layer == s.layer).map_or(usize::MAX, |e| e.1);
348            let scoped = container && s.min == SEM_SCOPE;
349            if rich {
350                rich_ids.insert(rid, pos);
351            }
352            cands.push(Cand { nd, layer: s.layer, pos, container, parent: usize::MAX, fixed: false, scope, scoped, wid: s.id, want });
353            if scoped && s.max > 0 {
354                scopes.push((k + s.max as usize, pos));
355            }
356        }
357        self.a11y_groups(&mut cands);
358        Self::a11y_parents(&mut cands, &rich_ids, &skip);
359        // parents: the smallest container on the same layer around the centre
360        let n = cands.len();
361        let conts: Vec<usize> = (0..n).filter(|&p| cands[p].container).collect();
362        for c in 0..n {
363            if cands[c].fixed {
364                continue;
365            }
366            let (x, y) = centre(&cands[c].nd.rect);
367            let a = area(&cands[c].nd.rect);
368            let mut best = usize::MAX;
369            let mut best_area = i64::MAX;
370            let scope = cands[c].scope;
371            for &p in &conts {
372                let q = &cands[p];
373                if p == c || q.layer != cands[c].layer || !holds(&q.nd.rect, x, y) {
374                    continue;
375                }
376                // inside a scoped record only what it holds (or itself) can be the parent; a
377                // scoped record holds nothing by its place (overlapping windows are siblings)
378                if (q.scope != scope || q.scoped) && p != scope {
379                    continue;
380                }
381                // tab strips hold tabs only
382                if q.nd.role == Role::TabList && cands[c].nd.role != Role::Tab {
383                    continue;
384                }
385                // a record out of view is not where its rectangle is; a record with an id64 (a
386                // page's) holds by its place only others with one (never the app's own widgets)
387                let rich = |nd: &Node| nd.key & SYN == 0 && nd.key & RICH != 0;
388                if q.nd.state & ax::S_OFFSCREEN != 0 || (rich(&q.nd) && !rich(&cands[c].nd)) {
389                    continue;
390                }
391                // never inside something smaller (equal: the earlier record is the outer one)
392                let qa = area(&q.nd.rect);
393                if qa < a || (qa == a && p > c) {
394                    continue;
395                }
396                if qa < best_area {
397                    best = p;
398                    best_area = qa;
399                }
400            }
401            // nothing inside a scoped record around it: the scoped record itself
402            if best == usize::MAX && scope != usize::MAX {
403                best = scope;
404            }
405            cands[c].parent = best;
406        }
407        // a loop (named parents against the rectangles): its record hangs at the top
408        for c in 0..n {
409            let mut p = cands[c].parent;
410            let mut steps = 0;
411            while p != usize::MAX && p != c && steps < n {
412                p = cands[p].parent;
413                steps += 1;
414            }
415            if p == c {
416                cands[c].parent = usize::MAX;
417            }
418        }
419        // tab strips without tabs are no nodes
420        let mut used = alloc::vec![false; n];
421        for c in &cands {
422            if c.parent < n {
423                used[c.parent] = true;
424            }
425        }
426        let keep: Vec<bool> = (0..n).map(|p| cands[p].nd.role != Role::TabList || used[p]).collect();
427        // the order: parents before their children, siblings in record order
428        let mut kids: Vec<Vec<usize>> = alloc::vec![Vec::new(); n + 1];
429        for c in 0..n {
430            if !keep[c] {
431                continue;
432            }
433            let p = cands[c].parent;
434            let p = if p != usize::MAX && keep[p] { p } else { n };
435            kids[p].push(c);
436        }
437        for k in kids.iter_mut() {
438            k.sort_by_key(|&c| cands[c].pos);
439        }
440        let mut order: Vec<(usize, u32)> = Vec::new(); // (candidate, parent node index)
441        let mut stack: Vec<(usize, u32)> = kids[n].iter().rev().map(|&c| (c, NONE)).collect();
442        let mut index_of = alloc::vec![NONE; n];
443        while let Some((c, parent)) = stack.pop() {
444            index_of[c] = order.len() as u32;
445            order.push((c, parent));
446            for &k in kids[c].iter().rev() {
447                stack.push((k, index_of[c]));
448            }
449        }
450        let mut nodes: Vec<Node> = Vec::with_capacity(order.len());
451        for &(c, parent) in &order {
452            let mut nd = core::mem::take(&mut cands[c].nd);
453            nd.parent = parent;
454            nodes.push(nd);
455        }
456        // the focus: the last focused node; a list's / tab strip's goes on to its selected child
457        let mut focus = nodes.iter().rposition(|nd| nd.state & ax::S_FOCUSED != 0).map_or(NONE, |k| k as u32);
458        if focus != NONE && matches!(nodes[focus as usize].role, Role::List | Role::TabList) {
459            if let Some(k) = (0..nodes.len()).find(|&k| nodes[k].parent == focus && nodes[k].state & ax::S_SELECTED != 0) {
460                nodes[focus as usize].state &= !ax::S_FOCUSED;
461                nodes[k].state |= ax::S_FOCUSED;
462                focus = k as u32;
463            }
464        }
465        let rec = &self.m.win.recs[i as usize];
466        let fb = self.win_fb(i);
467        let active = if self.headless { i == self.m.win.active } else { self.uiw_plat_state(i) & plat::WST_ACTIVE != 0 };
468        let mut w = WinTree {
469            win: i as u8,
470            pw: if self.headless { -1 } else { self.uiw_pw(i) },
471            title: String::from(if i == 0 { self.title() } else { self.uiw_title(i) }),
472            dialog: rec.flags & WF_DIALOG != 0,
473            active,
474            origin: [rec.sx, rec.sy],
475            size: [fb.w, fb.h],
476            nodes,
477            focus,
478            ..WinTree::default()
479        };
480        w.link();
481        w
482    }
483
484    // the node of record s (record k of the window's records)
485    fn a11y_node(&self, s: &Sem, role: Role, sub: Sub, key: u64, recs: &[&Sem], k: usize) -> Node {
486        let label = s.label.as_deref().unwrap_or("");
487        let mut nd = Node { key, role, sub, rect: s.rect, caret: -1, parent: NONE, ..Node::default() };
488        let rich = key & SYN == 0 && key & RICH != 0;
489        let a = s.ax.as_deref();
490        let st = s.state;
491        if st & SS_FOCUSED != 0 {
492            nd.state |= ax::S_FOCUSED;
493        }
494        if st & SS_DISABLED != 0 {
495            nd.state |= ax::S_DISABLED;
496        }
497        if st & SS_SELECTED != 0 && role != Role::Combo {
498            nd.state |= ax::S_SELECTED;
499        }
500        if st & SS_OPEN != 0 && role != Role::Dialog {
501            nd.state |= ax::S_EXPANDED;
502        }
503        if st & SS_CHECKED != 0 {
504            nd.state |= match role {
505                Role::Button => ax::S_PRESSED,
506                Role::CheckBox if s.value == 2 => ax::S_MIXED,
507                _ => ax::S_CHECKED,
508            };
509        }
510        if focusable(role) {
511            nd.state |= ax::S_FOCUSABLE;
512        }
513        if role == Role::TextBox {
514            // the kind of box (edit.rs: Sem::min)
515            let kind = (s.min as u32) << 24;
516            for (tb, st) in [(TB_MULTI, ax::S_MULTILINE), (TB_PASSWORD, ax::S_PROTECTED), (TB_READONLY, ax::S_READONLY)] {
517                if kind & tb != 0 {
518                    nd.state |= st;
519                }
520            }
521        }
522        match role {
523            Role::TextBox | Role::Combo => nd.text = String::from(label),
524            _ if s.kind == K_SWATCH => nd.name = alloc::format!("#{:06X}", s.value as u32 & 0xFF_FFFF),
525            _ => nd.name = String::from(label),
526        }
527        if role == Role::TextBox && st & SS_FOCUSED != 0 {
528            // (a rich box: the selection it gives in characters; a built-in one: bytes)
529            match a.and_then(|a| a.sel) {
530                Some([anchor, caret]) => {
531                    nd.caret = caret as i32;
532                    nd.anchor = anchor as i32;
533                }
534                None => {
535                    nd.caret = chars_before(label, s.value);
536                    nd.anchor = chars_before(label, s.max);
537                }
538            }
539            // the caret's rectangle when the box drew it in this pass (magnifiers)
540            let (pass, r) = self.m.a11y.caret[(s.win as usize) & (WIN_MAX - 1)];
541            if pass == self.frame && r[3] > 0 {
542                nd.caret_rect = r;
543            }
544        }
545        let named = a.and_then(|a| a.name.as_deref());
546        if borrows_name(role) && !rich && named.is_none() && (nd.name.is_empty() || matches!(role, Role::TextBox | Role::Combo)) {
547            nd.name = Self::a11y_label_before(recs, k).unwrap_or_default();
548        }
549        if let Some(n) = named {
550            nd.name = String::from(n);
551        }
552        match role {
553            Role::Slider => {
554                nd.value = s.value as f64;
555                nd.min = s.min.min(s.max) as f64;
556                nd.max = s.max as f64;
557                nd.step = 1.0;
558            }
559            Role::Progress if sub == Sub::Meter => {
560                nd.value = s.value as f64;
561                nd.min = s.min.min(s.max) as f64;
562                nd.max = s.max as f64;
563            }
564            Role::Progress => {
565                nd.value = if s.value < 0 { -1.0 } else { s.value as f64 };
566                nd.max = s.max as f64;
567            }
568            Role::Heading => nd.value = s.value.clamp(1, 9) as f64,
569            _ => {}
570        }
571        if let Some(a) = a {
572            Self::a11y_rich_node(&mut nd, a);
573        }
574        // live regions: as the record says, else as its kind says
575        nd.live = match a.map_or(0, |a| a.live) {
576            LIVE_POLITE => ax::LIVE_POLITE,
577            LIVE_ASSERTIVE => ax::LIVE_ASSERTIVE,
578            LIVE_OFF => 0,
579            _ => match s.kind {
580                K_ALERT => ax::LIVE_ASSERTIVE,
581                K_STATUS | K_LOG => ax::LIVE_POLITE,
582                _ => 0,
583            },
584        };
585        let readonly = rich && nd.state & ax::S_READONLY != 0;
586        if nd.state & ax::S_DISABLED == 0 {
587            if matches!(
588                role,
589                Role::Button | Role::Link | Role::CheckBox | Role::Radio | Role::Switch | Role::Tab | Role::ListItem | Role::Menu
590                    | Role::MenuItem | Role::Combo
591            ) || (rich && a.is_some_and(|a| a.states & AS_CLICKABLE != 0))
592            {
593                nd.actions |= ax::A_CLICK;
594            }
595            if nd.state & ax::S_FOCUSABLE != 0 {
596                nd.actions |= ax::A_FOCUS;
597            }
598            if role == Role::Slider && !readonly {
599                nd.actions |= ax::A_VALUE;
600            }
601            if role == Role::TextBox && !readonly {
602                nd.actions |= ax::A_TEXT;
603            }
604            // the further actions: only for records whose requests go to the app
605            if rich {
606                if nd.state & (ax::S_COLLAPSED | ax::S_EXPANDED) != 0 && role != Role::Dialog {
607                    nd.actions |= ax::A_EXPAND;
608                }
609                if role == Role::Slider && !readonly {
610                    nd.actions |= ax::A_STEP;
611                }
612                if matches!(role, Role::ListItem | Role::Tab) || nd.state & ax::S_SELECTABLE != 0 {
613                    nd.actions |= ax::A_SELECT;
614                }
615            }
616        }
617        if rich {
618            nd.actions |= ax::A_SCROLL; // (also a disabled one)
619        }
620        nd
621    }
622
623    // the further fields of a record's node from its SemAx: states, texts, places, a range with
624    // fractions
625    fn a11y_rich_node(nd: &mut Node, a: &SemAx) {
626        let map = [
627            (AS_REQUIRED, ax::S_REQUIRED),
628            (AS_INVALID, ax::S_INVALID),
629            (AS_BUSY, ax::S_BUSY),
630            (AS_HASPOPUP, ax::S_HASPOPUP),
631            (AS_VISITED, ax::S_VISITED),
632            (AS_MODAL, ax::S_MODAL),
633            (AS_MULTISELECT, ax::S_MULTISELECT),
634            (AS_OFFSCREEN, ax::S_OFFSCREEN),
635            (AS_FOCUSABLE, ax::S_FOCUSABLE),
636            (AS_SELECTABLE, ax::S_SELECTABLE),
637            (AS_READONLY, ax::S_READONLY),
638            (AS_MULTILINE, ax::S_MULTILINE),
639            (AS_PASSWORD, ax::S_PROTECTED),
640        ];
641        for (f, bit) in map {
642            if a.states & f != 0 {
643                nd.state |= bit;
644            }
645        }
646        if a.states & AS_COLLAPSED != 0 && nd.state & ax::S_EXPANDED == 0 {
647            nd.state |= ax::S_COLLAPSED;
648        }
649        if let Some(d) = &a.desc {
650            nd.desc = d.clone();
651        }
652        if let Some(u) = &a.url {
653            nd.url = u.clone();
654        }
655        nd.set = a.set;
656        if nd.role == Role::Heading {
657            if a.level > 0 {
658                nd.value = a.level.clamp(1, 9) as f64;
659            }
660        } else {
661            nd.item_level = a.level;
662        }
663        if a.cell[2] > 0 && a.cell[3] > 0 {
664            nd.cell = a.cell;
665        }
666        nd.grid = a.grid;
667        if matches!(nd.role, Role::Slider | Role::Progress) {
668            if let Some([v, lo, hi, step]) = a.range {
669                nd.value = v;
670                nd.min = lo.min(hi);
671                nd.max = hi;
672                nd.step = step.max(0.0);
673            }
674            if let Some(t) = &a.value_text {
675                nd.text = t.clone();
676            }
677        }
678    }
679
680    // the label of the text (or heading) record right before record k (records without a
681    // label in between, e.g. boxes, do not count): on its layer, to the left of it in the same
682    // row, or above it with the same left edge and at most a line apart
683    fn a11y_label_before(recs: &[&Sem], k: usize) -> Option<String> {
684        let me = recs[k];
685        let r = me.rect;
686        let t = recs[..k].iter().rev().find(|t| t.kind != K_BOX || t.label.is_some())?;
687        if !matches!(t.kind, K_TEXT | K_HEADING) || t.layer != me.layer {
688            return None;
689        }
690        let q = t.rect;
691        let rows = q[1] < r[1] + r[3] && r[1] < q[1] + q[3];
692        let left = rows && q[0] + q[2] <= r[0] + 4;
693        let above = (q[0] - r[0]).abs() <= 4 && q[1] + q[3] <= r[1] + 4 && r[1] - (q[1] + q[3]) <= r[3].max(q[3]);
694        if left || above {
695            return t.label.clone().filter(|l| !l.is_empty());
696        }
697        None
698    }
699
700    // the parents the records name (Sem::parent): the record with that id64 (rich_ids), else the
701    // first with that widget id; through left out records with an id64 (skip) to theirs; none
702    // found (or itself): its rectangle decides
703    fn a11y_parents(cands: &mut [Cand], rich_ids: &BTreeMap<u64, usize>, skip: &BTreeMap<u64, u64>) {
704        let mut wids: BTreeMap<u32, usize> = BTreeMap::new();
705        for (c, cand) in cands.iter().enumerate() {
706            if cand.wid != 0 {
707                wids.entry(cand.wid).or_insert(c);
708            }
709        }
710        for c in 0..cands.len() {
711            let mut want = cands[c].want;
712            let mut found = None;
713            for _ in 0..64 {
714                if want == 0 {
715                    break;
716                }
717                if let Some(&p) = rich_ids.get(&(want & ID_MASK)) {
718                    found = Some(p);
719                    break;
720                }
721                if let Some(&p) = u32::try_from(want).ok().and_then(|w| wids.get(&w)) {
722                    found = Some(p);
723                    break;
724                }
725                want = skip.get(&(want & ID_MASK)).copied().unwrap_or(0);
726            }
727            if let Some(p) = found.filter(|&p| p != c) {
728                cands[c].parent = p;
729                cands[c].fixed = true;
730            }
731        }
732    }
733
734    // the synthetic containers: menu bar entries next to each other get a menu bar; the rows
735    // of a popup (menu items, the rows of a drop-down) a menu / list below the record that is
736    // open (in record order: the n-th popup group belongs to the n-th open record)
737    fn a11y_groups(&self, cands: &mut Vec<Cand>) {
738        let n = cands.len();
739        let mut groups: Vec<(u64, Role, Vec<usize>)> = Vec::new();
740        let mut k = 0;
741        while k < n {
742            let r = cands[k].nd.role;
743            let popup = cands[k].layer as u32 >= L_POPUP && matches!(r, Role::MenuItem | Role::ListItem);
744            if r != Role::Menu && !popup {
745                k += 1;
746                continue;
747            }
748            let mut j = k + 1;
749            while j < n && cands[j].nd.role == r && cands[j].layer == cands[k].layer {
750                j += 1;
751            }
752            let id = cands[k].nd.key & 0xFFFF_FFFF;
753            let (kind, role) = if r == Role::Menu {
754                (SYN_MENUBAR, Role::MenuBar)
755            } else if r == Role::MenuItem {
756                (SYN_POPUP, Role::Menu)
757            } else {
758                (SYN_POPUP, Role::List)
759            };
760            groups.push((SYN | kind | id, role, (k..j).collect()));
761            k = j;
762        }
763        // the records that are open, in order (they opened the popups)
764        let opened: Vec<usize> =
765            (0..n).filter(|&c| cands[c].nd.state & ax::S_EXPANDED != 0 && !matches!(cands[c].nd.role, Role::Dialog | Role::Group)).collect();
766        let mut popups = 0;
767        for (key, role, members) in groups {
768            let at = cands.len();
769            let mut rect = [0; 4];
770            for &m in &members {
771                rect = union(rect, &cands[m].nd.rect);
772                cands[m].parent = at;
773                cands[m].fixed = true;
774            }
775            let layer = cands[members[0]].layer;
776            let mut g = Cand {
777                nd: Node { key, role, rect, caret: -1, parent: NONE, ..Node::default() },
778                layer,
779                pos: members[0],
780                container: false,
781                parent: usize::MAX,
782                fixed: false,
783                scope: cands[members[0]].scope,
784                scoped: false,
785                wid: 0,
786                want: 0,
787            };
788            if role != Role::MenuBar {
789                g.nd.state = ax::S_EXPANDED;
790                g.fixed = true;
791                if let Some(&o) = opened.get(popups) {
792                    g.parent = o;
793                }
794                popups += 1;
795            }
796            cands.push(g);
797        }
798    }
799
800    // ------------------------------------------------------------------ runtime hooks
801
802    /// (runtime) after the passes: a click no widget took becomes a pointer click; the tree
803    /// is published while assistive technology listens (the first window frame starts the
804    /// platform bridge)
805    pub(crate) fn a11y_frame(&mut self) {
806        if let Some(point) = self.a11y_click_record().and_then(|rec| self.a11y_aim(&rec)) {
807            let win = self.m.a11y.click.map_or(0, |c| c.0);
808            let pw = if self.headless { win as i32 } else { self.uiw_pw(win as u32) };
809            if pw >= 0 {
810                let ev = plat::Event { win: pw as u8, x: point.0, y: point.1, btn: plat::BTN_LEFT, ..Default::default() };
811                for typ in [plat::E_MOVE, plat::E_DOWN, plat::E_UP] {
812                    plat::evq_push(plat::Event { typ, ..ev });
813                }
814                plat::wake();
815            }
816        }
817        self.m.a11y.click = None;
818        self.m.a11y.value = None;
819        self.m.a11y.text = None;
820        self.a11y_select();
821        if !self.m.a11y.started {
822            self.m.a11y.started = true;
823            // the role names the bridges give, in the UI language (plat cannot ask ui)
824            plat::a11y::set_translator(crate::i18n::tr);
825            if !self.headless {
826                plat::a11y::start(self.desc.title);
827            }
828        }
829        let said = core::mem::take(&mut self.m.a11y.announce);
830        if self.headless {
831            // (no bridge: kept for the scripts)
832            let log = &mut self.m.a11y.said;
833            log.extend(said.into_iter().map(|e| e.1));
834            let over = log.len().saturating_sub(ANNOUNCE_MAX);
835            log.drain(..over);
836            return;
837        }
838        if plat::a11y::active() {
839            // the same records, windows and carets as the tree published last: the same tree (no
840            // change to send; the bridges answer from it) -- a page that does not move costs the
841            // key alone, not the tree
842            let key = self.a11y_key();
843            if key == self.m.a11y.published && said.is_empty() {
844                return;
845            }
846            for (w, text, p) in said {
847                plat::a11y::announce(w, &text, p);
848            }
849            plat::a11y::publish(self.a11y_tree());
850            self.m.a11y.published = key;
851        } else {
852            self.m.a11y.published = 0;
853        }
854    }
855
856    /// (benchmarks: plattest --test a11y-cost) the key a11y_frame compares with the tree published
857    /// last: equal keys, equal trees
858    #[doc(hidden)]
859    pub fn a11y_tree_key(&self) -> u64 {
860        self.a11y_key()
861    }
862
863    // a11y_win's inputs as a 64-bit key: the bridge's epoch, the shown windows (title, flags,
864    // place, size, keyboard, platform window), the carets drawn in this pass, every record
865    fn a11y_key(&self) -> u64 {
866        let mut h = Mix(plat::a11y::epoch() as u64 ^ 0x6A09_E667_F3BC_C908);
867        for i in 0..WIN_MAX as u32 {
868            if i != 0 && !self.uiw_drawable(i) {
869                continue;
870            }
871            let rec = &self.m.win.recs[i as usize];
872            let fb = self.win_fb(i);
873            h.u64(i as u64 | (rec.flags as u64) << 8 | (self.uiw_plat_state(i) as u64) << 32);
874            h.u64((rec.sx as u32 as u64) | (rec.sy as u32 as u64) << 32);
875            h.u64((fb.w as u32 as u64) | (fb.h as u32 as u64) << 32);
876            h.u64(if self.headless { self.m.win.active as u64 } else { self.uiw_pw(i) as u32 as u64 });
877            h.str(if i == 0 { self.title() } else { self.uiw_title(i) });
878            let (pass, r) = self.m.a11y.caret[i as usize & (WIN_MAX - 1)];
879            if pass == self.frame {
880                h.rect(&r);
881            }
882        }
883        for s in &self.sems {
884            h.u64(s.id as u64 | (s.kind as u64) << 32 | (s.state as u64) << 48);
885            h.rect(&s.rect);
886            h.u64(s.value as u32 as u64 | (s.max as u32 as u64) << 32);
887            h.u64(s.min as u32 as u64 | (s.layer as u64) << 32 | (s.depth as u64) << 40 | (s.win as u64) << 48);
888            h.opt(s.label.as_deref());
889            if let Some(a) = s.ax.as_deref() {
890                h.u64(a.id);
891                h.u64(a.parent);
892                for t in [&a.name, &a.desc, &a.url, &a.value_text] {
893                    h.opt(t.as_deref());
894                }
895                h.u64(a.states as u64 | (a.level as u64) << 32);
896                h.u64(a.set[0] as u64 | (a.set[1] as u64) << 32);
897                h.rect(&a.cell.map(|v| v as i32));
898                h.u64(a.grid[0] as u64 | (a.grid[1] as u64) << 32);
899                h.u64(a.live as u64);
900                if let Some(r) = a.range {
901                    for v in r {
902                        h.u64(v.to_bits());
903                    }
904                }
905                if let Some([x, y]) = a.sel {
906                    h.u64(x as u64 | (y as u64) << 32 | 1 << 63);
907                }
908            } else {
909                h.u64(u64::MAX);
910            }
911        }
912        h.0 | 1 // (0: nothing published)
913    }
914
915    // the record of the click waiting for a widget, as of the last pass
916    fn a11y_click_record(&self) -> Option<Sem> {
917        let (win, id, nth) = self.m.a11y.click?;
918        self.sems.iter().filter(|s| s.win == win && s.id == id).nth(nth - 1).cloned()
919    }
920
921    /// (runtime, after the passes) a click no widget took, its record's middle not on it: the
922    /// scroll views bring the record into view in further passes -> those are needed
923    pub(crate) fn a11y_reveal(&mut self) -> bool {
924        let Some(rec) = self.a11y_click_record().filter(|r| r.id != 0) else { return false };
925        match self.agent_aim(&rec, false) {
926            Err(miss) if miss.reason != "popup" => {
927                self.agent.reveal = Some((rec.win, rec.id));
928                true
929            }
930            _ => false,
931        }
932    }
933
934    // Where a click no widget took is pressed: where a click from outside would be (agent_aim),
935    // or in the middle of a record without a hit box of its own when the widget on top there
936    // draws it: that box lies inside the record (a link of a document, an app's own widget with
937    // another id), or holds it and is no control of its own (a row of a popup's list).  Else
938    // none: the middle lies on something else, and a press would go there.
939    fn a11y_aim(&self, rec: &Sem) -> Option<(i32, i32)> {
940        if rec.rect[2] <= 0 || rec.rect[3] <= 0 {
941            return None;
942        }
943        let miss = match self.agent_aim(rec, true) {
944            Ok(point) => return Some(point),
945            Err(miss) => miss,
946        };
947        let (x, y) = miss.point.filter(|_| miss.reason == "no_input" && miss.by != 0)?;
948        let p = self.uiw_parts(rec.win as u32)?;
949        // hit boxes are logical
950        let w = p.rt.fb.w;
951        let (lx, r) = if self.rtl { (w - 1 - x, [w - rec.rect[0] - rec.rect[2], rec.rect[1], rec.rect[2], rec.rect[3]]) } else { (x, rec.rect) };
952        let h = p.ctx.hits[p.ctx.hit_cur ^ 1].iter().rev().find(|h| h.id == miss.by && h.layer == rec.layer && lx >= h.x0 && lx < h.x1 && y >= h.y0 && y < h.y1)?;
953        let inside = h.x0 >= r[0] && h.y0 >= r[1] && h.x1 <= r[0] + r[2] && h.y1 <= r[1] + r[3];
954        let holds = h.x0 <= r[0] && h.y0 <= r[1] && h.x1 >= r[0] + r[2] && h.y1 >= r[1] + r[3];
955        let control = self.sems.iter().any(|s| s.id == miss.by && s.win == rec.win && !matches!(s.kind, K_BOX | K_LIST | K_CANVAS));
956        (inside || (holds && !control)).then_some((x, y))
957    }
958
959    /// (runtime, E_WAKE) take the actions of the assistive technology: a node of the last
960    /// pass that is enabled and not behind a modal dialog; the next passes carry them out
961    pub(crate) fn a11y_poll(&mut self) {
962        // a client came or left (the bridges wake this thread when it flips): the app hears it
963        let on = self.a11y_active();
964        if on != self.m.a11y.active {
965            self.m.a11y.active = on;
966            self.cmd_run(UIC_A11Y, on as u32);
967        }
968        // the further actions: offered by records with an id64 only, for the app
969        for o in plat::a11y::take_ops() {
970            let action = match o.op {
971                Op::ScrollIntoView => A11yAction::ScrollIntoView,
972                Op::Expand => A11yAction::Expand,
973                Op::Collapse => A11yAction::Collapse,
974                Op::Increment => A11yAction::Increment,
975                Op::Decrement => A11yAction::Decrement,
976                Op::SelectItem => A11yAction::Select,
977                Op::AddToSelection => A11yAction::AddToSelection,
978                Op::RemoveFromSelection => A11yAction::RemoveFromSelection,
979                _ => continue,
980            };
981            self.a11y_rich_request(o.win, o.key, action);
982        }
983        for r in plat::a11y::take() {
984            if r.key & SYN == 0 && r.key & RICH != 0 {
985                let action = match r.act {
986                    Act::Click => A11yAction::Click,
987                    Act::Focus => A11yAction::Focus,
988                    Act::Value(v) => A11yAction::SetValue(v),
989                    Act::Text(t) => A11yAction::SetText(t),
990                    Act::Select(a, c) => A11yAction::SelectText(a.max(0) as u32, c.max(0) as u32),
991                };
992                self.a11y_rich_request(r.win, r.key, action);
993                continue;
994            }
995            let win = r.win;
996            let id = r.key as u32;
997            let nth = (r.key >> 32) as usize;
998            if r.key & SYN != 0 || nth == 0 || win as usize >= WIN_MAX {
999                continue;
1000            }
1001            let Some(s) = self.sems.iter().filter(|s| s.win == win && s.id == id).nth(nth - 1) else { continue };
1002            let modal = self.uiw_parts(win as u32).map_or(0, |p| p.ctx.modal_prev);
1003            if s.state & SS_DISABLED != 0 || (s.layer as u32) < modal || (self.m.win.inert >> win) & 1 != 0 {
1004                continue;
1005            }
1006            match r.act {
1007                Act::Click => self.m.a11y.click = Some((win, id, nth)),
1008                Act::Focus => {
1009                    if win as u32 != self.uiw_cur {
1010                        self.uiw_activate(win as u32);
1011                    }
1012                    self.focus(id);
1013                }
1014                Act::Value(v) => self.m.a11y.value = Some((win, id, (if v < 0.0 { v - 0.5 } else { v + 0.5 }) as i32)),
1015                Act::Text(t) => self.m.a11y.text = Some((win, id, t)),
1016                Act::Select(a, c) => {
1017                    // a box without the focus gets it first (a11y_frame applies the selection then)
1018                    self.m.a11y.select = Some((win, id, a, c));
1019                    if s.kind == K_TEXTBOX && !self.a11y_editing(win, id) {
1020                        if win as u32 != self.uiw_cur {
1021                            self.uiw_activate(win as u32);
1022                        }
1023                        self.focus(id);
1024                    } else if !self.m.a11y.text.as_ref().is_some_and(|t| t.0 == win && t.1 == id) {
1025                        // (a new text waiting for the box: a11y_frame selects after the passes)
1026                        self.a11y_select();
1027                    }
1028                }
1029            }
1030            self.need = true;
1031        }
1032    }
1033
1034    // a request for the record of key (RICH | id64) of window win: queued for the app when that
1035    // record was in the last pass, enabled and not behind a modal dialog (scrolling into view
1036    // also when it is disabled)
1037    fn a11y_rich_request(&mut self, win: u8, key: u64, action: A11yAction) {
1038        if key & SYN != 0 || key & RICH == 0 || win as usize >= WIN_MAX {
1039            return;
1040        }
1041        let id = key & ID_MASK;
1042        let Some(s) = self.sems.iter().find(|s| s.win == win && s.ax.as_ref().is_some_and(|a| a.id & ID_MASK == id)) else { return };
1043        let modal = self.uiw_parts(win as u32).map_or(0, |p| p.ctx.modal_prev);
1044        let blocked = s.state & SS_DISABLED != 0 || (s.layer as u32) < modal || (self.m.win.inert >> win) & 1 != 0;
1045        if blocked && action != A11yAction::ScrollIntoView {
1046            return;
1047        }
1048        let id = s.ax.as_ref().map_or(id, |a| a.id);
1049        let win = self.uiw_rec_id(win as u32);
1050        let q = &mut self.m.a11y.reqs;
1051        if q.len() >= REQS_MAX {
1052            q.pop_front();
1053        }
1054        q.push_back(A11yRequest { win, id, action });
1055        self.need = true;
1056    }
1057
1058    /// (scripts) the texts ui.announce said in headless runs since the last call
1059    pub(crate) fn a11y_said_take(&mut self) -> Vec<String> {
1060        core::mem::take(&mut self.m.a11y.said)
1061    }
1062
1063    /// The next request of a screen reader for a record with an id64 (`Sem::id64`), oldest first:
1064    /// focus, click, scroll into view, expand / collapse, increment / decrement, a value, a text,
1065    /// a text selection, select items.  Take them in every frame and carry them out (a web view
1066    /// hands them to its engine); at most 64 wait.  Requests for the other records go to their
1067    /// widgets as before (EV_ACT, `a11y_value`, `a11y_text`).
1068    pub fn a11y_request_take(&mut self) -> Option<A11yRequest> {
1069        self.m.a11y.reqs.pop_front()
1070    }
1071
1072    /// Have the screen reader say `text`: `LIVE_POLITE` when the person is idle, `LIVE_ASSERTIVE`
1073    /// at once (a web page's live region announcement, "Saved").  It goes out with this frame's
1074    /// tree: UI Automation's notification event, AT-SPI2's announcement, the browser's live
1075    /// region.  Nothing is said (or kept) while no assistive technology listens; at most 16 per
1076    /// frame.
1077    pub fn announce(&mut self, text: &str, politeness: u32) {
1078        let q = &mut self.m.a11y.announce;
1079        if !text.is_empty() && q.len() < ANNOUNCE_MAX {
1080            let p = if politeness == LIVE_ASSERTIVE { ax::LIVE_ASSERTIVE } else { ax::LIVE_POLITE };
1081            q.push((self.uiw_cur as u8, cut(text, AX_TEXT_MAX), p));
1082        }
1083    }
1084
1085    /// A screen reader (or another client of the accessibility tree) listens: the library builds
1086    /// the tree from the records only then.  Records that only it needs -- the content of a page
1087    /// out of view (`AS_OFFSCREEN`) -- are best published only while this is true; it turns true
1088    /// with a frame of its own when a client comes, and the app gets the command `UIC_A11Y` (arg
1089    /// 1) then and when it leaves (arg 0; Windows: 10 s after its last question unless a screen
1090    /// reader listens), also while nothing else happens.  Always false in headless runs.
1091    pub fn a11y_active(&self) -> bool {
1092        !self.headless && plat::a11y::active()
1093    }
1094
1095    /// (behave) a pending click of the assistive technology for widget id of the live window
1096    pub(crate) fn a11y_click(&mut self, id: u32) -> bool {
1097        match self.m.a11y.click {
1098            Some((w, i, _)) if i == id && w as u32 == self.uiw_cur => {
1099                self.m.a11y.click = None;
1100                true
1101            }
1102            _ => false,
1103        }
1104    }
1105
1106    // a selection a screen reader asked for (Act::Select): once the text box has the focus its
1107    // anchor and caret move there (characters of its record's text -> bytes); the next passes
1108    // draw it
1109    fn a11y_select(&mut self) {
1110        let Some((win, id, a, c)) = self.m.a11y.select else { return };
1111        let Some(text) = self.sems.iter().find(|s| s.win == win && s.id == id && s.kind == K_TEXTBOX).and_then(|s| s.label.clone()) else {
1112            self.m.a11y.select = None; // the box is gone
1113            return;
1114        };
1115        if !self.a11y_editing(win, id) {
1116            return; // the focus arrives with the next pass
1117        }
1118        self.m.a11y.select = None;
1119        let byte = |k: i32| text.char_indices().nth(k.max(0) as usize).map_or(text.len(), |(b, _)| b);
1120        let (a, c) = (byte(a), byte(c));
1121        let cur = self.uiw_cur;
1122        self.uiw_use(win as u32);
1123        if self.textbox_select(id, &text, a, c) {
1124            // another frame (after the passes of a11y_frame the window loop waits for events)
1125            self.need = true;
1126            if !self.headless {
1127                plat::wake();
1128            }
1129        }
1130        self.uiw_use(cur);
1131    }
1132
1133    // text box id of window win holds the edit state (it has the focus)
1134    fn a11y_editing(&mut self, win: u8, id: u32) -> bool {
1135        let cur = self.uiw_cur;
1136        self.uiw_use(win as u32);
1137        let on = self.textbox_selection(id).is_some();
1138        self.uiw_use(cur);
1139        on
1140    }
1141
1142    /// (edit.rs ime_pos) the focused text box of the live window drew its caret at x, y (window
1143    /// px) with height h (h <= 0: no caret any more): the accessibility tree reports it, screen
1144    /// magnifiers follow it
1145    pub(crate) fn a11y_caret(&mut self, x: i32, y: i32, h: i32) {
1146        let w = (self.uiw_cur as usize) & (WIN_MAX - 1);
1147        self.m.a11y.caret[w] = if h > 0 { (self.frame, [x, y, self.hair().max(1), h]) } else { (0, [0; 4]) };
1148    }
1149
1150    /// A value a screen reader set for slider-like widget `id` (its record's value scale),
1151    /// taken once: the built-in sliders apply it themselves; a widget of its own applies it
1152    /// like keyboard input (clamped) in the pass it is built.
1153    pub fn a11y_value(&mut self, id: u32) -> Option<i32> {
1154        match self.m.a11y.value {
1155            Some((w, i, v)) if i == id && w as u32 == self.uiw_cur => {
1156                self.m.a11y.value = None;
1157                Some(v)
1158            }
1159            _ => None,
1160        }
1161    }
1162
1163    /// A text a screen reader set for text widget `id`, taken once (the built-in text boxes
1164    /// apply it themselves).
1165    pub fn a11y_text(&mut self, id: u32) -> Option<String> {
1166        match &self.m.a11y.text {
1167            Some((w, i, _)) if *i == id && *w as u32 == self.uiw_cur => self.m.a11y.text.take().map(|t| t.2),
1168            _ => None,
1169        }
1170    }
1171}
1172
1173// s cut to at most max bytes (at a character)
1174fn cut(s: &str, max: usize) -> String {
1175    let mut b = s.len().min(max);
1176    while !s.is_char_boundary(b) {
1177        b -= 1;
1178    }
1179    String::from(&s[..b])
1180}
1181
1182// a quick 64-bit mix of words (a11y_key; no defence against chosen input is needed there)
1183struct Mix(u64);
1184
1185impl Mix {
1186    fn u64(&mut self, v: u64) {
1187        self.0 = (self.0.rotate_left(5) ^ v).wrapping_mul(0x51_7CC1_B727_220A_95);
1188    }
1189    fn rect(&mut self, r: &[i32; 4]) {
1190        self.u64(r[0] as u32 as u64 | (r[1] as u32 as u64) << 32);
1191        self.u64(r[2] as u32 as u64 | (r[3] as u32 as u64) << 32);
1192    }
1193    fn str(&mut self, s: &str) {
1194        let b = s.as_bytes();
1195        self.u64(b.len() as u64);
1196        let mut c = b.chunks_exact(8);
1197        for w in &mut c {
1198            self.u64(u64::from_le_bytes([w[0], w[1], w[2], w[3], w[4], w[5], w[6], w[7]]));
1199        }
1200        let mut last = [0u8; 8];
1201        last[..c.remainder().len()].copy_from_slice(c.remainder());
1202        self.u64(u64::from_le_bytes(last));
1203    }
1204    fn opt(&mut self, s: Option<&str>) {
1205        match s {
1206            Some(s) => self.str(s),
1207            None => self.u64(0xA5A5_A5A5_A5A5_A5A5),
1208        }
1209    }
1210}
1211
1212// a text of a record's SemAx: None when empty, cut to AX_TEXT_MAX
1213fn ax_text(s: &str) -> Option<String> {
1214    (!s.is_empty()).then(|| cut(s, AX_TEXT_MAX))
1215}
1216
1217/// The setters of a record's further accessibility data (`Sem::ax`, section 19 of the guide):
1218/// each returns the record, so they chain after `sem` / `sem_rect`.  A web page's tree:
1219///
1220/// ```ignore
1221/// ui.sem_rect(K_CELL, 0, r, Some("12,50 €"), 0).id64(node).parent(row).cell(2, 1);
1222/// ```
1223impl Sem {
1224    fn ax_mut(&mut self) -> &mut SemAx {
1225        self.ax.get_or_insert_with(|| Box::new(SemAx::default()))
1226    }
1227
1228    /// Its own 64-bit id (below 2^62; a web page's node id).  The node keeps it from frame to
1229    /// frame, other records name it as their parent, and every request of a screen reader for it
1230    /// comes back to the app (`ui.a11y_request_take`) instead of going to a widget.
1231    pub fn id64(&mut self, id: u64) -> &mut Sem {
1232        self.ax_mut().id = id;
1233        self
1234    }
1235
1236    /// The record it hangs under: the one with this id64 (else this widget id) in the same frame
1237    /// and window -- the tree follows it, not the rectangles.  A parent that is left out (a box
1238    /// without a label) passes it on to its own parent; an unknown one leaves it to its rectangle.
1239    pub fn parent(&mut self, id: u64) -> &mut Sem {
1240        self.ax_mut().parent = id;
1241        self
1242    }
1243
1244    /// Its name when the label is something else: a text box's or drop-down's label is its text,
1245    /// so `.name("Suche")` names it.
1246    pub fn name(&mut self, name: &str) -> &mut Sem {
1247        self.ax_mut().name = ax_text(name);
1248        self
1249    }
1250
1251    /// A description beside the name (a hint; a page's aria-describedby).
1252    pub fn desc(&mut self, text: &str) -> &mut Sem {
1253        self.ax_mut().desc = ax_text(text);
1254        self
1255    }
1256
1257    /// A link's address.
1258    pub fn url(&mut self, url: &str) -> &mut Sem {
1259        self.ax_mut().url = ax_text(url);
1260        self
1261    }
1262
1263    /// The text a screen reader says for a slider's, meter's or spin button's value ("Montag").
1264    pub fn value_text(&mut self, text: &str) -> &mut Sem {
1265        self.ax_mut().value_text = ax_text(text);
1266        self
1267    }
1268
1269    /// Further states (`AS_*`: required, invalid, busy, has a popup, visited, modal,
1270    /// multi-select, collapsed, off screen, focusable, clickable, selectable, read-only,
1271    /// multi-line, password), added to those it has.
1272    pub fn states(&mut self, states: u32) -> &mut Sem {
1273        self.ax_mut().states |= states;
1274        self
1275    }
1276
1277    /// Its position in its set (1-based) and the set's size: list and tree items, tabs, rows.
1278    pub fn set_pos(&mut self, pos: u32, size: u32) -> &mut Sem {
1279        self.ax_mut().set = [pos, size];
1280        self
1281    }
1282
1283    /// The level of a heading (1 ..= 9, in place of the value), a tree item, list item or row
1284    /// (1 the top).
1285    pub fn level(&mut self, level: u32) -> &mut Sem {
1286        self.ax_mut().level = level;
1287        self
1288    }
1289
1290    /// A cell's or header's place in its table: row, column (0-based).
1291    pub fn cell(&mut self, row: u32, col: u32) -> &mut Sem {
1292        let a = self.ax_mut();
1293        a.cell = [row, col, a.cell[2].max(1), a.cell[3].max(1)];
1294        self
1295    }
1296
1297    /// The rows and columns a cell spans (1, 1 without).
1298    pub fn span(&mut self, rows: u32, cols: u32) -> &mut Sem {
1299        let a = self.ax_mut();
1300        a.cell[2] = rows.max(1);
1301        a.cell[3] = cols.max(1);
1302        self
1303    }
1304
1305    /// A table's number of rows and columns (without: counted from its cells).
1306    pub fn grid(&mut self, rows: u32, cols: u32) -> &mut Sem {
1307        self.ax_mut().grid = [rows, cols];
1308        self
1309    }
1310
1311    /// How it speaks as a live region: `LIVE_POLITE`, `LIVE_ASSERTIVE`, `LIVE_OFF` (0: as its kind
1312    /// says -- K_ALERT assertive, K_STATUS and K_LOG polite).
1313    pub fn live(&mut self, live: u32) -> &mut Sem {
1314        self.ax_mut().live = live;
1315        self
1316    }
1317
1318    /// A value with fractions in place of `value` / `min` / `max` (sliders, meters, spin buttons,
1319    /// scroll bars, progress bars; a value below min: unknown progress), and the step of a
1320    /// screen reader's increment.
1321    pub fn range(&mut self, value: f64, min: f64, max: f64, step: f64) -> &mut Sem {
1322        self.ax_mut().range = Some([value, min, max, step]);
1323        self
1324    }
1325
1326    /// A focused text box's selection in characters: anchor, caret (equal: the caret alone) -- a
1327    /// page's own field; the built-in text boxes report theirs themselves.
1328    pub fn text_sel(&mut self, anchor: u32, caret: u32) -> &mut Sem {
1329        self.ax_mut().sel = Some([anchor, caret]);
1330        self
1331    }
1332}