1use 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
56pub(crate) const SEM_SCOPE: i32 = 0x5C09_E000;
60
61const SYN: u64 = 1 << 63;
63const SYN_MENUBAR: u64 = 1 << 40;
64const SYN_POPUP: u64 = 2 << 40;
65pub(crate) const RICH: u64 = 1 << 62;
68pub(crate) const ID_MASK: u64 = RICH - 1;
69const REQS_MAX: usize = 64;
71const ANNOUNCE_MAX: usize = 16;
72const AX_TEXT_MAX: usize = 4096;
74
75#[derive(Clone, Debug, PartialEq)]
77#[non_exhaustive]
78pub struct A11yRequest {
79 pub win: u32,
81 pub id: u64,
83 pub action: A11yAction,
84}
85
86impl A11yRequest {
87 pub fn new(win: u32, id: u64, action: A11yAction) -> A11yRequest {
90 A11yRequest { win, id, action }
91 }
92}
93
94#[derive(Clone, Debug, PartialEq)]
96#[non_exhaustive]
97pub enum A11yAction {
98 Focus,
100 Click,
102 ScrollIntoView,
104 Expand,
106 Collapse,
107 Increment,
109 Decrement,
110 SetValue(f64),
112 SetText(String),
114 SelectText(u32, u32),
116 Select,
118 AddToSelection,
120 RemoveFromSelection,
121}
122
123#[derive(Default)]
125pub(crate) struct S {
126 started: bool,
127 reqs: VecDeque<A11yRequest>,
129 announce: Vec<(u8, String, u8)>,
131 said: Vec<String>,
133 click: Option<(u8, u32, usize)>,
135 value: Option<(u8, u32, i32)>,
136 text: Option<(u8, u32, String)>,
137 select: Option<(u8, u32, i32, i32)>,
139 active: bool,
141 published: u64,
144 caret: [(u32, [i32; 4]); WIN_MAX],
147}
148
149impl S {
150 pub(crate) fn pending(&self) -> bool {
152 self.click.is_some()
153 }
154}
155
156struct Cand {
158 nd: Node,
159 layer: u8,
160 pos: usize,
162 container: bool,
163 parent: usize,
164 fixed: bool,
166 scope: usize,
168 scoped: bool,
170 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
198fn 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
234fn 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; }
245 Some((role_basic(s, named)?, Sub::None))
246}
247
248fn 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 K_BOX if named => Role::Group,
272 K_BOX if s.max > 0 => Role::TabList,
273 _ => return None,
274 })
275}
276
277fn 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
290fn 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 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 fn a11y_win(&self, i: u32) -> WinTree {
315 let recs: Vec<&Sem> = self.sems.iter().filter(|s| s.win as u32 == i).collect();
316 let mut cands: Vec<Cand> = Vec::with_capacity(recs.len() + 16);
319 let mut occ: BTreeMap<u32, u32> = BTreeMap::new();
320 let mut scopes: Vec<(usize, usize)> = Vec::new();
322 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 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 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 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 if (q.scope != scope || q.scoped) && p != scope {
379 continue;
380 }
381 if q.nd.role == Role::TabList && cands[c].nd.role != Role::Tab {
383 continue;
384 }
385 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 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 if best == usize::MAX && scope != usize::MAX {
403 best = scope;
404 }
405 cands[c].parent = best;
406 }
407 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 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 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(); 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 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 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 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 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 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 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 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; }
620 nd
621 }
622
623 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 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 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 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 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 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 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 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 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 #[doc(hidden)]
859 pub fn a11y_tree_key(&self) -> u64 {
860 self.a11y_key()
861 }
862
863 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 }
914
915 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 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 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 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 pub(crate) fn a11y_poll(&mut self) {
962 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 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 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 self.a11y_select();
1027 }
1028 }
1029 }
1030 self.need = true;
1031 }
1032 }
1033
1034 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 pub(crate) fn a11y_said_take(&mut self) -> Vec<String> {
1060 core::mem::take(&mut self.m.a11y.said)
1061 }
1062
1063 pub fn a11y_request_take(&mut self) -> Option<A11yRequest> {
1069 self.m.a11y.reqs.pop_front()
1070 }
1071
1072 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 pub fn a11y_active(&self) -> bool {
1092 !self.headless && plat::a11y::active()
1093 }
1094
1095 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 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; return;
1114 };
1115 if !self.a11y_editing(win, id) {
1116 return; }
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 self.need = true;
1126 if !self.headless {
1127 plat::wake();
1128 }
1129 }
1130 self.uiw_use(cur);
1131 }
1132
1133 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 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 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 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
1173fn 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
1182struct 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
1212fn ax_text(s: &str) -> Option<String> {
1214 (!s.is_empty()).then(|| cut(s, AX_TEXT_MAX))
1215}
1216
1217impl Sem {
1224 fn ax_mut(&mut self) -> &mut SemAx {
1225 self.ax.get_or_insert_with(|| Box::new(SemAx::default()))
1226 }
1227
1228 pub fn id64(&mut self, id: u64) -> &mut Sem {
1232 self.ax_mut().id = id;
1233 self
1234 }
1235
1236 pub fn parent(&mut self, id: u64) -> &mut Sem {
1240 self.ax_mut().parent = id;
1241 self
1242 }
1243
1244 pub fn name(&mut self, name: &str) -> &mut Sem {
1247 self.ax_mut().name = ax_text(name);
1248 self
1249 }
1250
1251 pub fn desc(&mut self, text: &str) -> &mut Sem {
1253 self.ax_mut().desc = ax_text(text);
1254 self
1255 }
1256
1257 pub fn url(&mut self, url: &str) -> &mut Sem {
1259 self.ax_mut().url = ax_text(url);
1260 self
1261 }
1262
1263 pub fn value_text(&mut self, text: &str) -> &mut Sem {
1265 self.ax_mut().value_text = ax_text(text);
1266 self
1267 }
1268
1269 pub fn states(&mut self, states: u32) -> &mut Sem {
1273 self.ax_mut().states |= states;
1274 self
1275 }
1276
1277 pub fn set_pos(&mut self, pos: u32, size: u32) -> &mut Sem {
1279 self.ax_mut().set = [pos, size];
1280 self
1281 }
1282
1283 pub fn level(&mut self, level: u32) -> &mut Sem {
1286 self.ax_mut().level = level;
1287 self
1288 }
1289
1290 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 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 pub fn grid(&mut self, rows: u32, cols: u32) -> &mut Sem {
1307 self.ax_mut().grid = [rows, cols];
1308 self
1309 }
1310
1311 pub fn live(&mut self, live: u32) -> &mut Sem {
1314 self.ax_mut().live = live;
1315 self
1316 }
1317
1318 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 pub fn text_sel(&mut self, anchor: u32, caret: u32) -> &mut Sem {
1329 self.ax_mut().sel = Some([anchor, caret]);
1330 self
1331 }
1332}