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
31use alloc::collections::BTreeMap;
32use alloc::string::String;
33use alloc::vec::Vec;
34
35use plat::a11y::{self as ax, Act, Node, Role, Snapshot, WinTree, NONE};
36
37use super::consts::*;
38use super::{Sem, Ui};
39
40/// Sem::min of a record that holds the next Sem::max records of its window by their order, not
41/// by their place (a window inside a window area, desk.rs: its content stays inside it where the
42/// windows overlap)
43pub(crate) const SEM_SCOPE: i32 = 0x5C09_E000;
44
45// synthetic containers: keys outside the widget ids (bit 63, kind in bits 40..)
46const SYN: u64 = 1 << 63;
47const SYN_MENUBAR: u64 = 1 << 40;
48const SYN_POPUP: u64 = 2 << 40;
49
50/// the requests waiting for the widgets (shared by all windows)
51#[derive(Default)]
52pub(crate) struct S {
53    started: bool,
54    /// a click for (window, id) and which record of that id it is (the fallback click)
55    click: Option<(u8, u32, usize)>,
56    value: Option<(u8, u32, i32)>,
57    text: Option<(u8, u32, String)>,
58    /// a selection for (window, text box): anchor, caret in characters (waits for the focus)
59    select: Option<(u8, u32, i32, i32)>,
60    /// the caret of the focused text box per window: the pass that drew it, x, y, w, h (window
61    /// px; ime_pos reports it)
62    caret: [(u32, [i32; 4]); WIN_MAX],
63}
64
65impl S {
66    /// a click waits for its widget (behave asks a11y_click only then)
67    pub(crate) fn pending(&self) -> bool {
68        self.click.is_some()
69    }
70}
71
72// a node being built: its node, its layer, where it hangs
73struct Cand {
74    nd: Node,
75    layer: u8,
76    /// place among its siblings (record order; a synthetic container: its first member's)
77    pos: usize,
78    container: bool,
79    parent: usize,
80    /// members of a synthetic container (their parent is fixed)
81    fixed: bool,
82    /// the innermost scoped record (SEM_SCOPE) on its layer that holds it (usize::MAX: none)
83    scope: usize,
84    /// it is a scoped record itself (it holds only what its scope says)
85    scoped: bool,
86}
87
88fn centre(r: &[i32; 4]) -> (i32, i32) {
89    (r[0] + r[2] / 2, r[1] + r[3] / 2)
90}
91
92fn holds(r: &[i32; 4], x: i32, y: i32) -> bool {
93    x >= r[0] && y >= r[1] && x < r[0] + r[2] && y < r[1] + r[3]
94}
95
96fn area(r: &[i32; 4]) -> i64 {
97    r[2].max(0) as i64 * r[3].max(0) as i64
98}
99
100fn union(a: [i32; 4], b: &[i32; 4]) -> [i32; 4] {
101    if a[2] <= 0 || a[3] <= 0 {
102        return *b;
103    }
104    let x0 = a[0].min(b[0]);
105    let y0 = a[1].min(b[1]);
106    let x1 = (a[0] + a[2]).max(b[0] + b[2]);
107    let y1 = (a[1] + a[3]).max(b[1] + b[3]);
108    [x0, y0, x1 - x0, y1 - y0]
109}
110
111// the role of a record (None: left out)
112fn role_of(s: &Sem) -> Option<Role> {
113    let label = s.label.as_deref().unwrap_or("");
114    Some(match s.kind {
115        K_TEXT if !label.is_empty() => Role::Label,
116        K_HEADING if !label.is_empty() => Role::Heading,
117        K_BUTTON | K_SWATCH => Role::Button,
118        K_CHECKBOX => Role::CheckBox,
119        K_RADIO => Role::Radio,
120        K_SWITCH => Role::Switch,
121        K_SLIDER => Role::Slider,
122        K_PROGRESS => Role::Progress,
123        K_TEXTBOX => Role::TextBox,
124        K_MENU => Role::Menu,
125        K_MENUITEM => Role::MenuItem,
126        K_LIST => Role::List,
127        K_LISTITEM => Role::ListItem,
128        K_TAB => Role::Tab,
129        K_CANVAS => Role::Canvas,
130        K_ICON | K_IMAGE => Role::Image,
131        K_COMBO => Role::Combo,
132        K_DIALOG => Role::Dialog,
133        K_LINK => Role::Link,
134        // a titled box (notifications, info bars); the tab strip's box (max = number of tabs)
135        K_BOX if !label.is_empty() => Role::Group,
136        K_BOX if s.max > 0 => Role::TabList,
137        _ => return None,
138    })
139}
140
141// roles that need a name from the text before them when their label is no name
142fn borrows_name(role: Role) -> bool {
143    matches!(role, Role::TextBox | Role::Combo | Role::Slider | Role::Progress | Role::List)
144}
145
146fn focusable(role: Role) -> bool {
147    matches!(
148        role,
149        Role::Button | Role::CheckBox | Role::Radio | Role::Switch | Role::Slider | Role::TextBox | Role::Link | Role::List
150            | Role::ListItem | Role::Menu | Role::MenuItem | Role::TabList | Role::Tab | Role::Combo
151    )
152}
153
154// the characters before byte offset b of s
155fn chars_before(s: &str, b: i32) -> i32 {
156    let mut b = (b.max(0) as usize).min(s.len());
157    while !s.is_char_boundary(b) {
158        b -= 1;
159    }
160    s[..b].chars().count() as i32
161}
162
163impl Ui {
164    /// The accessibility tree of the last pass as assistive technology gets it (screen
165    /// readers through AT-SPI2 / UI Automation): every shown window with its nodes (roles,
166    /// names, states, values, rectangles), built from the semantic records (see sem()).
167    pub fn a11y_tree(&self) -> Snapshot {
168        let mut snap = Snapshot { app: String::from(self.desc.title), wins: Vec::new() };
169        for i in 0..WIN_MAX as u32 {
170            if i == 0 || self.uiw_drawable(i) {
171                snap.wins.push(self.a11y_win(i));
172            }
173        }
174        snap
175    }
176
177    // the tree of window i
178    fn a11y_win(&self, i: u32) -> WinTree {
179        let recs: Vec<&Sem> = self.sems.iter().filter(|s| s.win as u32 == i).collect();
180        let mut cands: Vec<Cand> = Vec::new();
181        let mut occ: BTreeMap<u32, u32> = BTreeMap::new();
182        // the scoped records open at a record: (the last record they hold, their candidate)
183        let mut scopes: Vec<(usize, usize)> = Vec::new();
184        for (k, s) in recs.iter().enumerate() {
185            while scopes.last().is_some_and(|e| e.0 < k) {
186                scopes.pop();
187            }
188            let n = occ.entry(s.id).or_insert(0);
189            *n += 1;
190            let key = (*n as u64) << 32 | s.id as u64;
191            let Some(role) = role_of(s) else { continue };
192            if s.rect[2] <= 0 || s.rect[3] <= 0 {
193                continue;
194            }
195            let nd = self.a11y_node(s, role, key, &recs, k);
196            let container = matches!(role, Role::List | Role::Dialog | Role::Group | Role::TabList);
197            let pos = cands.len();
198            let scope = scopes.iter().rev().find(|e| cands[e.1].layer == s.layer).map_or(usize::MAX, |e| e.1);
199            let scoped = container && s.min == SEM_SCOPE;
200            cands.push(Cand { nd, layer: s.layer, pos, container, parent: usize::MAX, fixed: false, scope, scoped });
201            if scoped && s.max > 0 {
202                scopes.push((k + s.max as usize, pos));
203            }
204        }
205        self.a11y_groups(&mut cands);
206        // parents: the smallest container on the same layer around the centre
207        let n = cands.len();
208        let conts: Vec<usize> = (0..n).filter(|&p| cands[p].container).collect();
209        for c in 0..n {
210            if cands[c].fixed {
211                continue;
212            }
213            let (x, y) = centre(&cands[c].nd.rect);
214            let a = area(&cands[c].nd.rect);
215            let mut best = usize::MAX;
216            let mut best_area = i64::MAX;
217            let scope = cands[c].scope;
218            for &p in &conts {
219                let q = &cands[p];
220                if p == c || q.layer != cands[c].layer || !holds(&q.nd.rect, x, y) {
221                    continue;
222                }
223                // inside a scoped record only what it holds (or itself) can be the parent; a
224                // scoped record holds nothing by its place (overlapping windows are siblings)
225                if (q.scope != scope || q.scoped) && p != scope {
226                    continue;
227                }
228                // tab strips hold tabs only
229                if q.nd.role == Role::TabList && cands[c].nd.role != Role::Tab {
230                    continue;
231                }
232                // never inside something smaller (equal: the earlier record is the outer one)
233                let qa = area(&q.nd.rect);
234                if qa < a || (qa == a && p > c) {
235                    continue;
236                }
237                if qa < best_area {
238                    best = p;
239                    best_area = qa;
240                }
241            }
242            // nothing inside a scoped record around it: the scoped record itself
243            if best == usize::MAX && scope != usize::MAX {
244                best = scope;
245            }
246            cands[c].parent = best;
247        }
248        // tab strips without tabs are no nodes
249        let mut used = alloc::vec![false; n];
250        for c in &cands {
251            if c.parent < n {
252                used[c.parent] = true;
253            }
254        }
255        let keep: Vec<bool> = (0..n).map(|p| cands[p].nd.role != Role::TabList || used[p]).collect();
256        // the order: parents before their children, siblings in record order
257        let mut kids: Vec<Vec<usize>> = alloc::vec![Vec::new(); n + 1];
258        for c in 0..n {
259            if !keep[c] {
260                continue;
261            }
262            let p = cands[c].parent;
263            let p = if p != usize::MAX && keep[p] { p } else { n };
264            kids[p].push(c);
265        }
266        for k in kids.iter_mut() {
267            k.sort_by_key(|&c| cands[c].pos);
268        }
269        let mut order: Vec<(usize, u32)> = Vec::new(); // (candidate, parent node index)
270        let mut stack: Vec<(usize, u32)> = kids[n].iter().rev().map(|&c| (c, NONE)).collect();
271        let mut index_of = alloc::vec![NONE; n];
272        while let Some((c, parent)) = stack.pop() {
273            index_of[c] = order.len() as u32;
274            order.push((c, parent));
275            for &k in kids[c].iter().rev() {
276                stack.push((k, index_of[c]));
277            }
278        }
279        let mut nodes: Vec<Node> = Vec::with_capacity(order.len());
280        for &(c, parent) in &order {
281            let mut nd = core::mem::take(&mut cands[c].nd);
282            nd.parent = parent;
283            nodes.push(nd);
284        }
285        // the focus: the last focused node; a list's / tab strip's goes on to its selected child
286        let mut focus = nodes.iter().rposition(|nd| nd.state & ax::S_FOCUSED != 0).map_or(NONE, |k| k as u32);
287        if focus != NONE && matches!(nodes[focus as usize].role, Role::List | Role::TabList) {
288            if let Some(k) = (0..nodes.len()).find(|&k| nodes[k].parent == focus && nodes[k].state & ax::S_SELECTED != 0) {
289                nodes[focus as usize].state &= !ax::S_FOCUSED;
290                nodes[k].state |= ax::S_FOCUSED;
291                focus = k as u32;
292            }
293        }
294        let rec = &self.m.win.recs[i as usize];
295        let fb = self.win_fb(i);
296        let active = if self.headless { i == self.m.win.active } else { self.uiw_plat_state(i) & plat::WST_ACTIVE != 0 };
297        let mut w = WinTree {
298            win: i as u8,
299            pw: if self.headless { -1 } else { self.uiw_pw(i) },
300            title: String::from(if i == 0 { self.title() } else { self.uiw_title(i) }),
301            dialog: rec.flags & WF_DIALOG != 0,
302            active,
303            origin: [rec.sx, rec.sy],
304            size: [fb.w, fb.h],
305            nodes,
306            focus,
307            ..WinTree::default()
308        };
309        w.link();
310        w
311    }
312
313    // the node of record s (record k of the window's records)
314    fn a11y_node(&self, s: &Sem, role: Role, key: u64, recs: &[&Sem], k: usize) -> Node {
315        let label = s.label.as_deref().unwrap_or("");
316        let mut nd = Node { key, role, rect: s.rect, caret: -1, parent: NONE, ..Node::default() };
317        let st = s.state;
318        if st & SS_FOCUSED != 0 {
319            nd.state |= ax::S_FOCUSED;
320        }
321        if st & SS_DISABLED != 0 {
322            nd.state |= ax::S_DISABLED;
323        }
324        if st & SS_SELECTED != 0 && role != Role::Combo {
325            nd.state |= ax::S_SELECTED;
326        }
327        if st & SS_OPEN != 0 && role != Role::Dialog {
328            nd.state |= ax::S_EXPANDED;
329        }
330        if st & SS_CHECKED != 0 {
331            nd.state |= match role {
332                Role::Button => ax::S_PRESSED,
333                Role::CheckBox if s.value == 2 => ax::S_MIXED,
334                _ => ax::S_CHECKED,
335            };
336        }
337        if focusable(role) {
338            nd.state |= ax::S_FOCUSABLE;
339        }
340        if role == Role::TextBox {
341            // the kind of box (edit.rs: Sem::min)
342            let kind = (s.min as u32) << 24;
343            for (tb, st) in [(TB_MULTI, ax::S_MULTILINE), (TB_PASSWORD, ax::S_PROTECTED), (TB_READONLY, ax::S_READONLY)] {
344                if kind & tb != 0 {
345                    nd.state |= st;
346                }
347            }
348        }
349        match role {
350            Role::TextBox | Role::Combo => nd.text = String::from(label),
351            _ if s.kind == K_SWATCH => nd.name = alloc::format!("#{:06X}", s.value as u32 & 0xFF_FFFF),
352            _ => nd.name = String::from(label),
353        }
354        if role == Role::TextBox && st & SS_FOCUSED != 0 {
355            nd.caret = chars_before(label, s.value);
356            nd.anchor = chars_before(label, s.max);
357            // the caret's rectangle when the box drew it in this pass (magnifiers)
358            let (pass, r) = self.m.a11y.caret[(s.win as usize) & (WIN_MAX - 1)];
359            if pass == self.frame && r[3] > 0 {
360                nd.caret_rect = r;
361            }
362        }
363        if borrows_name(role) && (nd.name.is_empty() || matches!(role, Role::TextBox | Role::Combo)) {
364            nd.name = Self::a11y_label_before(recs, k).unwrap_or_default();
365        }
366        match role {
367            Role::Slider => {
368                nd.value = s.value as f64;
369                nd.min = s.min.min(s.max) as f64;
370                nd.max = s.max as f64;
371                nd.step = 1.0;
372            }
373            Role::Progress => {
374                nd.value = if s.value < 0 { -1.0 } else { s.value as f64 };
375                nd.max = s.max as f64;
376            }
377            Role::Heading => nd.value = s.value.clamp(1, 9) as f64,
378            _ => {}
379        }
380        if nd.state & ax::S_DISABLED == 0 {
381            if matches!(
382                role,
383                Role::Button | Role::Link | Role::CheckBox | Role::Radio | Role::Switch | Role::Tab | Role::ListItem | Role::Menu
384                    | Role::MenuItem | Role::Combo
385            ) {
386                nd.actions |= ax::A_CLICK;
387            }
388            if focusable(role) {
389                nd.actions |= ax::A_FOCUS;
390            }
391            if role == Role::Slider {
392                nd.actions |= ax::A_VALUE;
393            }
394            if role == Role::TextBox {
395                nd.actions |= ax::A_TEXT;
396            }
397        }
398        nd
399    }
400
401    // the label of the text (or heading) record right before record k (records without a
402    // label in between, e.g. boxes, do not count): on its layer, to the left of it in the same
403    // row, or above it with the same left edge and at most a line apart
404    fn a11y_label_before(recs: &[&Sem], k: usize) -> Option<String> {
405        let me = recs[k];
406        let r = me.rect;
407        let t = recs[..k].iter().rev().find(|t| t.kind != K_BOX || t.label.is_some())?;
408        if !matches!(t.kind, K_TEXT | K_HEADING) || t.layer != me.layer {
409            return None;
410        }
411        let q = t.rect;
412        let rows = q[1] < r[1] + r[3] && r[1] < q[1] + q[3];
413        let left = rows && q[0] + q[2] <= r[0] + 4;
414        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]);
415        if left || above {
416            return t.label.clone().filter(|l| !l.is_empty());
417        }
418        None
419    }
420
421    // the synthetic containers: menu bar entries next to each other get a menu bar; the rows
422    // of a popup (menu items, the rows of a drop-down) a menu / list below the record that is
423    // open (in record order: the n-th popup group belongs to the n-th open record)
424    fn a11y_groups(&self, cands: &mut Vec<Cand>) {
425        let n = cands.len();
426        let mut groups: Vec<(u64, Role, Vec<usize>)> = Vec::new();
427        let mut k = 0;
428        while k < n {
429            let r = cands[k].nd.role;
430            let popup = cands[k].layer as u32 >= L_POPUP && matches!(r, Role::MenuItem | Role::ListItem);
431            if r != Role::Menu && !popup {
432                k += 1;
433                continue;
434            }
435            let mut j = k + 1;
436            while j < n && cands[j].nd.role == r && cands[j].layer == cands[k].layer {
437                j += 1;
438            }
439            let id = cands[k].nd.key & 0xFFFF_FFFF;
440            let (kind, role) = if r == Role::Menu {
441                (SYN_MENUBAR, Role::MenuBar)
442            } else if r == Role::MenuItem {
443                (SYN_POPUP, Role::Menu)
444            } else {
445                (SYN_POPUP, Role::List)
446            };
447            groups.push((SYN | kind | id, role, (k..j).collect()));
448            k = j;
449        }
450        // the records that are open, in order (they opened the popups)
451        let opened: Vec<usize> =
452            (0..n).filter(|&c| cands[c].nd.state & ax::S_EXPANDED != 0 && !matches!(cands[c].nd.role, Role::Dialog | Role::Group)).collect();
453        let mut popups = 0;
454        for (key, role, members) in groups {
455            let at = cands.len();
456            let mut rect = [0; 4];
457            for &m in &members {
458                rect = union(rect, &cands[m].nd.rect);
459                cands[m].parent = at;
460                cands[m].fixed = true;
461            }
462            let layer = cands[members[0]].layer;
463            let mut g = Cand {
464                nd: Node { key, role, rect, caret: -1, parent: NONE, ..Node::default() },
465                layer,
466                pos: members[0],
467                container: false,
468                parent: usize::MAX,
469                fixed: false,
470                scope: cands[members[0]].scope,
471                scoped: false,
472            };
473            if role != Role::MenuBar {
474                g.nd.state = ax::S_EXPANDED;
475                g.fixed = true;
476                if let Some(&o) = opened.get(popups) {
477                    g.parent = o;
478                }
479                popups += 1;
480            }
481            cands.push(g);
482        }
483    }
484
485    // ------------------------------------------------------------------ runtime hooks
486
487    /// (runtime) after the passes: a click no widget took becomes a pointer click; the tree
488    /// is published while assistive technology listens (the first window frame starts the
489    /// platform bridge)
490    pub(crate) fn a11y_frame(&mut self) {
491        if let Some(point) = self.a11y_click_record().and_then(|rec| self.a11y_aim(&rec)) {
492            let win = self.m.a11y.click.map_or(0, |c| c.0);
493            let pw = if self.headless { win as i32 } else { self.uiw_pw(win as u32) };
494            if pw >= 0 {
495                let ev = plat::Event { win: pw as u8, x: point.0, y: point.1, btn: plat::BTN_LEFT, ..Default::default() };
496                for typ in [plat::E_MOVE, plat::E_DOWN, plat::E_UP] {
497                    plat::evq_push(plat::Event { typ, ..ev });
498                }
499                plat::wake();
500            }
501        }
502        self.m.a11y.click = None;
503        self.m.a11y.value = None;
504        self.m.a11y.text = None;
505        self.a11y_select();
506        if !self.m.a11y.started {
507            self.m.a11y.started = true;
508            // the role names the bridges give, in the UI language (plat cannot ask ui)
509            plat::a11y::set_translator(crate::i18n::tr);
510            if !self.headless {
511                plat::a11y::start(self.desc.title);
512            }
513        }
514        if self.headless {
515            return;
516        }
517        if plat::a11y::active() {
518            plat::a11y::publish(self.a11y_tree());
519        }
520    }
521
522    // the record of the click waiting for a widget, as of the last pass
523    fn a11y_click_record(&self) -> Option<Sem> {
524        let (win, id, nth) = self.m.a11y.click?;
525        self.sems.iter().filter(|s| s.win == win && s.id == id).nth(nth - 1).cloned()
526    }
527
528    /// (runtime, after the passes) a click no widget took, its record's middle not on it: the
529    /// scroll views bring the record into view in further passes -> those are needed
530    pub(crate) fn a11y_reveal(&mut self) -> bool {
531        let Some(rec) = self.a11y_click_record().filter(|r| r.id != 0) else { return false };
532        match self.agent_aim(&rec, false) {
533            Err(miss) if miss.reason != "popup" => {
534                self.agent.reveal = Some((rec.win, rec.id));
535                true
536            }
537            _ => false,
538        }
539    }
540
541    // Where a click no widget took is pressed: where a click from outside would be (agent_aim),
542    // or in the middle of a record without a hit box of its own when the widget on top there
543    // draws it: that box lies inside the record (a link of a document, an app's own widget with
544    // another id), or holds it and is no control of its own (a row of a popup's list).  Else
545    // none: the middle lies on something else, and a press would go there.
546    fn a11y_aim(&self, rec: &Sem) -> Option<(i32, i32)> {
547        if rec.rect[2] <= 0 || rec.rect[3] <= 0 {
548            return None;
549        }
550        let miss = match self.agent_aim(rec, true) {
551            Ok(point) => return Some(point),
552            Err(miss) => miss,
553        };
554        let (x, y) = miss.point.filter(|_| miss.reason == "no_input" && miss.by != 0)?;
555        let p = self.uiw_parts(rec.win as u32)?;
556        // hit boxes are logical
557        let w = p.rt.fb.w;
558        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) };
559        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)?;
560        let inside = h.x0 >= r[0] && h.y0 >= r[1] && h.x1 <= r[0] + r[2] && h.y1 <= r[1] + r[3];
561        let holds = h.x0 <= r[0] && h.y0 <= r[1] && h.x1 >= r[0] + r[2] && h.y1 >= r[1] + r[3];
562        let control = self.sems.iter().any(|s| s.id == miss.by && s.win == rec.win && !matches!(s.kind, K_BOX | K_LIST | K_CANVAS));
563        (inside || (holds && !control)).then_some((x, y))
564    }
565
566    /// (runtime, E_WAKE) take the actions of the assistive technology: a node of the last
567    /// pass that is enabled and not behind a modal dialog; the next passes carry them out
568    pub(crate) fn a11y_poll(&mut self) {
569        for r in plat::a11y::take() {
570            let win = r.win;
571            let id = r.key as u32;
572            let nth = (r.key >> 32) as usize;
573            if r.key & SYN != 0 || nth == 0 || win as usize >= WIN_MAX {
574                continue;
575            }
576            let Some(s) = self.sems.iter().filter(|s| s.win == win && s.id == id).nth(nth - 1) else { continue };
577            let modal = self.uiw_parts(win as u32).map_or(0, |p| p.ctx.modal_prev);
578            if s.state & SS_DISABLED != 0 || (s.layer as u32) < modal || (self.m.win.inert >> win) & 1 != 0 {
579                continue;
580            }
581            match r.act {
582                Act::Click => self.m.a11y.click = Some((win, id, nth)),
583                Act::Focus => {
584                    if win as u32 != self.uiw_cur {
585                        self.uiw_activate(win as u32);
586                    }
587                    self.focus(id);
588                }
589                Act::Value(v) => self.m.a11y.value = Some((win, id, (if v < 0.0 { v - 0.5 } else { v + 0.5 }) as i32)),
590                Act::Text(t) => self.m.a11y.text = Some((win, id, t)),
591                Act::Select(a, c) => {
592                    // a box without the focus gets it first (a11y_frame applies the selection then)
593                    self.m.a11y.select = Some((win, id, a, c));
594                    if s.kind == K_TEXTBOX && !self.a11y_editing(win, id) {
595                        if win as u32 != self.uiw_cur {
596                            self.uiw_activate(win as u32);
597                        }
598                        self.focus(id);
599                    } else if !self.m.a11y.text.as_ref().is_some_and(|t| t.0 == win && t.1 == id) {
600                        // (a new text waiting for the box: a11y_frame selects after the passes)
601                        self.a11y_select();
602                    }
603                }
604            }
605            self.need = true;
606        }
607    }
608
609    /// (behave) a pending click of the assistive technology for widget id of the live window
610    pub(crate) fn a11y_click(&mut self, id: u32) -> bool {
611        match self.m.a11y.click {
612            Some((w, i, _)) if i == id && w as u32 == self.uiw_cur => {
613                self.m.a11y.click = None;
614                true
615            }
616            _ => false,
617        }
618    }
619
620    // a selection a screen reader asked for (Act::Select): once the text box has the focus its
621    // anchor and caret move there (characters of its record's text -> bytes); the next passes
622    // draw it
623    fn a11y_select(&mut self) {
624        let Some((win, id, a, c)) = self.m.a11y.select else { return };
625        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 {
626            self.m.a11y.select = None; // the box is gone
627            return;
628        };
629        if !self.a11y_editing(win, id) {
630            return; // the focus arrives with the next pass
631        }
632        self.m.a11y.select = None;
633        let byte = |k: i32| text.char_indices().nth(k.max(0) as usize).map_or(text.len(), |(b, _)| b);
634        let (a, c) = (byte(a), byte(c));
635        let cur = self.uiw_cur;
636        self.uiw_use(win as u32);
637        if self.textbox_select(id, &text, a, c) {
638            // another frame (after the passes of a11y_frame the window loop waits for events)
639            self.need = true;
640            if !self.headless {
641                plat::wake();
642            }
643        }
644        self.uiw_use(cur);
645    }
646
647    // text box id of window win holds the edit state (it has the focus)
648    fn a11y_editing(&mut self, win: u8, id: u32) -> bool {
649        let cur = self.uiw_cur;
650        self.uiw_use(win as u32);
651        let on = self.textbox_selection(id).is_some();
652        self.uiw_use(cur);
653        on
654    }
655
656    /// (edit.rs ime_pos) the focused text box of the live window drew its caret at x, y (window
657    /// px) with height h (h <= 0: no caret any more): the accessibility tree reports it, screen
658    /// magnifiers follow it
659    pub(crate) fn a11y_caret(&mut self, x: i32, y: i32, h: i32) {
660        let w = (self.uiw_cur as usize) & (WIN_MAX - 1);
661        self.m.a11y.caret[w] = if h > 0 { (self.frame, [x, y, self.hair().max(1), h]) } else { (0, [0; 4]) };
662    }
663
664    /// A value a screen reader set for slider-like widget `id` (its record's value scale),
665    /// taken once: the built-in sliders apply it themselves; a widget of its own applies it
666    /// like keyboard input (clamped) in the pass it is built.
667    pub fn a11y_value(&mut self, id: u32) -> Option<i32> {
668        match self.m.a11y.value {
669            Some((w, i, v)) if i == id && w as u32 == self.uiw_cur => {
670                self.m.a11y.value = None;
671                Some(v)
672            }
673            _ => None,
674        }
675    }
676
677    /// A text a screen reader set for text widget `id`, taken once (the built-in text boxes
678    /// apply it themselves).
679    pub fn a11y_text(&mut self, id: u32) -> Option<String> {
680        match &self.m.a11y.text {
681            Some((w, i, _)) if *i == id && *w as u32 == self.uiw_cur => self.m.a11y.text.take().map(|t| t.2),
682            _ => None,
683        }
684    }
685}