tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
⎇ 168cd187519c0ad575d3ce63baee2f164c38d9d1
llimphi-ui/src/eventloop/a11y_rt.rs
History
Who touched what
raw
chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros
1// a11y_rt.rs — Integración con AccessKit en tiempo de ejecución.
2// Empuja el árbol de accesibilidad al SO y atiende las acciones del lector
3// de pantalla (focus, click). Separado para no contaminar el flujo de input
4// con detalles de la API de accesskit_winit.
5
6use super::super::*;
7use super::push_a11y_tree;
8
9impl<A: App> Runtime<A> {
10 /// Recibe un `accesskit_winit::Event` (ruteado vía `EventLoopProxy` como
11 /// `UserEvent::A11y(...)`) y reacciona:
12 /// - `InitialTreeRequested`: el lector pidió el árbol inicial → empujamos
13 /// uno desde `last_render` si lo hay, o pedimos un redraw que lo creará.
14 /// - `ActionRequested(req)`: el lector quiere ejecutar una acción sobre un
15 /// `NodeId`. v1 soporta `Action::Focus` (mueve `state.focused` + dispara
16 /// `App::on_focus`) y `Action::Click` (ejecuta el `on_click` del nodo).
17 /// - `AccessibilityDeactivated`: nada que hacer; el siguiente paint dejará
18 /// de construir trees (el `update_if_active` se autoinhibe).
19 pub(super) fn handle_a11y_event(&mut self, ev: accesskit_winit::Event) {
20 use accesskit_winit::WindowEvent as AkWinEvent;
21 let Some(state) = self.state.as_mut() else { return };
22 match ev.window_event {
23 AkWinEvent::InitialTreeRequested => {
24 // Si ya pintamos un frame, ese mounted sirve para el árbol
25 // inicial. Si no, forzamos un redraw — el path normal llamará
26 // a `push_a11y_tree::<A>` al final.
27 if state.last_render.is_some() {
28 push_a11y_tree::<A>(state);
29 } else {
30 state.window.request_redraw();
31 }
32 }
33 AkWinEvent::ActionRequested(req) => {
34 let Some(idx) = crate::a11y::mounted_idx_for(req.target_node) else {
35 return;
36 };
37 let Some(cache) = state.last_render.as_ref() else {
38 return;
39 };
40 let Some(node) = cache.mounted.nodes.get(idx) else {
41 return;
42 };
43 match req.action {
44 accesskit::Action::Focus => {
45 // Si el nodo es focusable, movemos el foco a su id
46 // opaco; si no, lo limpiamos. La app recibe la
47 // transición vía `App::on_focus`.
48 let new_focus = node.focusable;
49 state.focused = new_focus;
50 let model = state.model.as_ref().expect("model");
51 if let Some(msg) = A::on_focus(model, new_focus) {
52 let m = state.model.take().expect("model");
53 state.model = Some(super::vigia::aplicar_update::<A>(m, msg, &self.handle));
54 }
55 state.invalidar_render();
56 state.window.request_redraw();
57 }
58 accesskit::Action::Click => {
59 // Dos cosas que v1 no hacía:
60 //
61 // - Delegar al hijo clickeable cuando el rol está en el
62 // contenedor (fila de árbol/tabla). Lo resuelve
63 // `a11y::click_target`.
64 // - Atender `on_click_at`: la posición sintética SÍ
65 // existe — el centro del rect del nodo en el layout
66 // del frame, que es lo que produce un click "en el
67 // medio del control". Es la misma convención que usa
68 // `llimphi_test::query::NodeRef::click`.
69 let msg = crate::a11y::click_target(&cache.mounted, idx).and_then(|t| {
70 let destino = &cache.mounted.nodes[t];
71 match &destino.on_click_at {
72 Some(f) => cache
73 .computed
74 .get(destino.id)
75 .and_then(|r| f(r.w * 0.5, r.h * 0.5, r.w, r.h)),
76 None => destino.on_click.clone(),
77 }
78 });
79 if let Some(msg) = msg {
80 let m = state.model.take().expect("model");
81 state.model = Some(super::vigia::aplicar_update::<A>(m, msg, &self.handle));
82 state.invalidar_render();
83 state.window.request_redraw();
84 }
85 }
86 _ => {
87 // Otras acciones (Expand/Collapse/Increment/Decrement/
88 // SetValue/ScrollIntoView/etc.) se sumarán cuando un
89 // widget concreto lo pida — el modelo `SemanticsSpec`
90 // ya tiene los flags relevantes; solo falta cablear el
91 // efecto inverso (acción → mutación de Model).
92 }
93 }
94 }
95 AkWinEvent::AccessibilityDeactivated => {}
96 }
97 }
98}