1use 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
40pub(crate) const SEM_SCOPE: i32 = 0x5C09_E000;
44
45const SYN: u64 = 1 << 63;
47const SYN_MENUBAR: u64 = 1 << 40;
48const SYN_POPUP: u64 = 2 << 40;
49
50#[derive(Default)]
52pub(crate) struct S {
53 started: bool,
54 click: Option<(u8, u32, usize)>,
56 value: Option<(u8, u32, i32)>,
57 text: Option<(u8, u32, String)>,
58 select: Option<(u8, u32, i32, i32)>,
60 caret: [(u32, [i32; 4]); WIN_MAX],
63}
64
65impl S {
66 pub(crate) fn pending(&self) -> bool {
68 self.click.is_some()
69 }
70}
71
72struct Cand {
74 nd: Node,
75 layer: u8,
76 pos: usize,
78 container: bool,
79 parent: usize,
80 fixed: bool,
82 scope: usize,
84 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
111fn 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 K_BOX if !label.is_empty() => Role::Group,
136 K_BOX if s.max > 0 => Role::TabList,
137 _ => return None,
138 })
139}
140
141fn 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
154fn 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 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 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 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 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 if (q.scope != scope || q.scoped) && p != scope {
226 continue;
227 }
228 if q.nd.role == Role::TabList && cands[c].nd.role != Role::Tab {
230 continue;
231 }
232 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 if best == usize::MAX && scope != usize::MAX {
244 best = scope;
245 }
246 cands[c].parent = best;
247 }
248 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 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(); 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 self.a11y_select();
602 }
603 }
604 }
605 self.need = true;
606 }
607 }
608
609 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 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; return;
628 };
629 if !self.a11y_editing(win, id) {
630 return; }
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 self.need = true;
640 if !self.headless {
641 plat::wake();
642 }
643 }
644 self.uiw_use(cur);
645 }
646
647 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 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 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 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}