tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 229TOML 101Markdown 82Shell 24052 Sutichaykuna

⎇ main llimphi-compositor/src/semantics.rs Rust · 290 siq'ikuna · 10.8 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros d251eb32b · Sergio ·

1//! Modelo de **semántica accesible** de un nodo. Es el dato que el runtime
2//! traduce a un árbol [AccessKit](https://accesskit.dev) por frame para
3//! alimentar lectores de pantalla (NVDA, VoiceOver, Orca, TalkBack) y otras
4//! ayudas técnicas — TTS, navegación por voz, switch control.
5//!
6//! Este módulo es **pura data**: define los tipos sin acoplarse al crate
7//! `accesskit`. La conversión a `accesskit::Node` vive en `llimphi-ui::a11y`
8//! (iter 2 del plan), donde el cableado del adapter winit ya importa la
9//! librería. Tener aquí solo el modelo permite:
10//!
11//! - Compilar el compositor con o sin la integración AccessKit habilitada.
12//! - Testear semántica a nivel "qué declaran los widgets" sin levantar un
13//!   adapter ni un lector real.
14//! - Mantener la API estable aunque cambien versiones de `accesskit`.
15//!
16//! ## Cuándo declarar semántica
17//!
18//! - **Siempre** en controles interactivos: botones, inputs, checkboxes, tabs,
19//!   ítems de menú, sliders. Sin rol declarado, el lector no sabe que el nodo
20//!   ES un botón aunque tenga `on_click`.
21//! - **Para texto significativo** que no es un botón: títulos (`Heading`),
22//!   etiquetas asociadas, valores (`Label` / `Static`). El text de un nodo se
23//!   lee igual aunque no tenga `semantics`, pero un rol explícito mejora la
24//!   navegación por rol de los lectores.
25//! - **Para grouping**: tabbar, dock, toolbars, listas — `Role::Group` o un
26//!   rol específico (`TabList`, `Menu`, `Toolbar`) ayuda a saltar bloques.
27//!
28//! ## Cuándo NO declarar
29//!
30//! Decorativo puro (un divider, un fondo con gradiente, una sombra) **no debe**
31//! declarar semántica — los lectores ya filtran texto vacío, pero un rol
32//! superfluo (`Role::Group` en cada `View` envoltorio) ensucia la navegación.
33
34// Preludio de `alloc`: bajo `no_std` estos tipos ya no vienen solos.
35#[allow(unused_imports)]
36use alloc::{boxed::Box, string::{String, ToString}, vec::Vec};
37
38use alloc::sync::Arc;
39
40/// Rol semántico del nodo. Los nombres y la granularidad siguen los roles de
41/// AccessKit / ARIA. Subset acotado: agregamos lo que falte cuando aparezca un
42/// caller real (regla del repo — no diseñamos para lo hipotético).
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
44pub enum Role {
45    /// Botón clickeable. El lector dice "botón <label>" + el flag `pressed`
46    /// para botones de toggle.
47    Button,
48    /// Campo de texto editable (single-line o multi-line). Combinable con
49    /// `value` (texto actual) y los flags `readonly`/`required`.
50    TextInput,
51    /// Título de sección (h1..h6 en HTML). El `value` puede llevar el nivel
52    /// como string ("1", "2", …) si la app lo necesita; v1 no lo distingue.
53    Heading,
54    /// Casilla de verificación. Combina con `checked`.
55    Checkbox,
56    /// Texto estático significativo (no interactivo, no título). Si solo es
57    /// decorativo, no declarar semántica.
58    Label,
59    /// Hipervínculo / acción que navega a otra ubicación.
60    Link,
61    /// Ítem de un menú (context-menu, menubar, dropdown).
62    MenuItem,
63    /// Pestaña de un tabbar / segmented control.
64    Tab,
65    /// Imagen significativa. El `label` actúa como alt-text.
66    Image,
67    /// Control deslizable continuo (volumen, brillo, range). Combinable con
68    /// `value` (string del valor actual) — los rangos numéricos se modelan
69    /// más fino en iter posteriores si hace falta.
70    Slider,
71    /// Agrupador genérico (toolbar, panel, sección). Sirve para que los
72    /// lectores ofrezcan "saltar al siguiente grupo".
73    Group,
74
75    // --- Contenedores de navegación (iter 3) ---
76    /// Barra de pestañas: el contenedor de los [`Role::Tab`].
77    TabList,
78    /// Barra de herramientas. Agrupa botones de acción.
79    Toolbar,
80    /// Barra de menú (contenedor de [`Role::Menu`] / [`Role::MenuItem`]).
81    MenuBar,
82    /// Menú desplegable o contextual (contenedor de [`Role::MenuItem`]).
83    Menu,
84
85    // --- Colecciones ---
86    /// Lista de ítems. Los hijos deberían ser [`Role::ListItem`].
87    List,
88    /// Ítem de una lista.
89    ListItem,
90    /// Árbol jerárquico. Los hijos deberían ser [`Role::TreeItem`].
91    Tree,
92    /// Fila de un árbol. Combina con el flag `expanded` cuando tiene hijos.
93    TreeItem,
94    /// Tabla de datos. Los hijos son [`Role::Row`], y los de éstos
95    /// [`Role::Cell`] / [`Role::ColumnHeader`].
96    Table,
97    /// Fila de una tabla.
98    Row,
99    /// Celda de una tabla.
100    Cell,
101    /// Celda de encabezado de columna.
102    ColumnHeader,
103
104    // --- Estado y notificación ---
105    /// Barra de progreso / medidor. El `value` lleva el texto que el lector
106    /// enuncia ("70%", "3 de 10").
107    ProgressBar,
108    /// Mensaje que aparece y reclama atención (toast, banner de error). Los
109    /// lectores lo anuncian sin que el usuario navegue hasta él.
110    Alert,
111    /// Región de estado no urgente (status bar). A diferencia de
112    /// [`Role::Alert`], se anuncia sin interrumpir.
113    Status,
114    /// Rótulo flotante de ayuda asociado a otro control.
115    Tooltip,
116
117    // --- Controles adicionales ---
118    /// Interruptor on/off. Distinto de [`Role::Checkbox`]: los lectores lo
119    /// enuncian "activado/desactivado" en vez de "marcado". Combina con
120    /// `checked`.
121    Switch,
122    /// Diálogo modal. Combina con `label` (el título) — el lector lo enuncia
123    /// al abrirse y confina la navegación adentro.
124    Dialog,
125    /// Campo de texto **multilínea** (text-area, editor). Los lectores cambian
126    /// el modo de navegación respecto de [`Role::TextInput`] de una línea.
127    MultilineTextInput,
128    /// Emulador de terminal. AccessKit tiene un rol propio: los lectores le
129    /// aplican el modo "salida en vivo" en vez de leer el buffer entero.
130    Terminal,
131    /// Divisor arrastrable entre dos paneles. Los lectores lo exponen como
132    /// control redimensionable en vez de como una línea decorativa.
133    Splitter,
134}
135
136/// Banderas booleanas del nodo accesible. Todas opcionales (`None` = no aplica,
137/// que es distinto de "aplica pero es false"). Mantienelas en None salvo que el
138/// widget realmente las exponga — los lectores diferencian "no es checkable" de
139/// "es checkable y no checked".
140#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
141pub struct SemanticsFlags {
142    /// Estado de un checkbox / radio / toggle button.
143    pub checked: Option<bool>,
144    /// Estado on/off de un botón de toggle (separado de `checked` porque ARIA
145    /// los distingue: un toggle `<button>` usa `aria-pressed`, una checkbox
146    /// `aria-checked`).
147    pub pressed: Option<bool>,
148    /// Para acordeones, menús, tree-rows que se expanden.
149    pub expanded: Option<bool>,
150    /// El control está deshabilitado (no responde a input).
151    pub disabled: Option<bool>,
152    /// Sólo lectura (típicamente input de texto que no se edita).
153    pub readonly: Option<bool>,
154    /// Campo requerido (formularios).
155    pub required: Option<bool>,
156}
157
158impl SemanticsFlags {
159    pub const EMPTY: Self = Self {
160        checked: None,
161        pressed: None,
162        expanded: None,
163        disabled: None,
164        readonly: None,
165        required: None,
166    };
167}
168
169/// Especificación semántica completa de un nodo. Lo que el runtime traduce a
170/// un `accesskit::Node` cada frame.
171///
172/// `label` es lo que el lector enuncia primero (el "nombre accesible"). Si el
173/// nodo ya tiene un `text` visible y significativo, puedes dejar `label = None`
174/// y el runtime usará ese texto como nombre — pero declararlo explícito es más
175/// robusto (e.g. un botón con sólo un ícono necesita label porque no hay texto
176/// visible).
177///
178/// `value` es el dato dinámico (texto del input, valor del slider). El lector
179/// suele leer label + value juntos: "Volumen, 70".
180///
181/// `description` es contexto adicional ("Disminuye el volumen del sistema").
182/// Los lectores lo leen tras una pausa o con un atajo distinto; úsalo para
183/// info que ayude PERO no sobreloadées (los usuarios de TTS perciben ruido
184/// más que falta de info).
185#[derive(Clone, Debug, Default, PartialEq)]
186pub struct SemanticsSpec {
187    pub role: Option<Role>,
188    pub label: Option<Arc<str>>,
189    pub description: Option<Arc<str>>,
190    pub value: Option<Arc<str>>,
191    pub flags: SemanticsFlags,
192}
193
194impl SemanticsSpec {
195    /// Especificación con sólo el rol fijado. Atajo común; los demás campos
196    /// quedan `None` y los flags vacíos.
197    pub fn role(role: Role) -> Self {
198        Self {
199            role: Some(role),
200            ..Self::default()
201        }
202    }
203
204    /// Pone `label` (consumiendo cualquier valor previo).
205    pub fn with_label(mut self, s: impl Into<Arc<str>>) -> Self {
206        self.label = Some(s.into());
207        self
208    }
209
210    /// Pone `description`.
211    pub fn with_description(mut self, s: impl Into<Arc<str>>) -> Self {
212        self.description = Some(s.into());
213        self
214    }
215
216    /// Pone `value`.
217    pub fn with_value(mut self, s: impl Into<Arc<str>>) -> Self {
218        self.value = Some(s.into());
219        self
220    }
221
222    /// Pone `flags.checked = Some(v)`.
223    pub fn with_checked(mut self, v: bool) -> Self {
224        self.flags.checked = Some(v);
225        self
226    }
227    pub fn with_pressed(mut self, v: bool) -> Self {
228        self.flags.pressed = Some(v);
229        self
230    }
231    pub fn with_expanded(mut self, v: bool) -> Self {
232        self.flags.expanded = Some(v);
233        self
234    }
235    pub fn with_disabled(mut self, v: bool) -> Self {
236        self.flags.disabled = Some(v);
237        self
238    }
239    pub fn with_readonly(mut self, v: bool) -> Self {
240        self.flags.readonly = Some(v);
241        self
242    }
243    pub fn with_required(mut self, v: bool) -> Self {
244        self.flags.required = Some(v);
245        self
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252
253    #[test]
254    fn default_es_todo_none_y_flags_empty() {
255        let s = SemanticsSpec::default();
256        assert!(s.role.is_none());
257        assert!(s.label.is_none());
258        assert!(s.value.is_none());
259        assert_eq!(s.flags, SemanticsFlags::EMPTY);
260    }
261
262    #[test]
263    fn role_builder_pone_solo_el_rol() {
264        let s = SemanticsSpec::role(Role::Button);
265        assert_eq!(s.role, Some(Role::Button));
266        assert!(s.label.is_none());
267        assert!(s.value.is_none());
268        assert_eq!(s.flags, SemanticsFlags::EMPTY);
269    }
270
271    #[test]
272    fn with_label_y_with_value_componen() {
273        let s = SemanticsSpec::role(Role::Slider)
274            .with_label("Volumen")
275            .with_value("70");
276        assert_eq!(s.role, Some(Role::Slider));
277        assert_eq!(s.label.as_deref(), Some("Volumen"));
278        assert_eq!(s.value.as_deref(), Some("70"));
279    }
280
281    #[test]
282    fn flags_con_with_son_independientes() {
283        let s = SemanticsSpec::role(Role::Checkbox)
284            .with_checked(true)
285            .with_required(true);
286        assert_eq!(s.flags.checked, Some(true));
287        assert_eq!(s.flags.required, Some(true));
288        assert!(s.flags.disabled.is_none(), "no setear flags no tocados");
289    }
290}