#![no_std] #![forbid(unsafe_code)] extern crate alloc; // ============================================================================= // llimphi-entrada :: el modelo de entrada, como dato (W4.7 de PLAN-LLIMPHI) // ----------------------------------------------------------------------------- // Hasta acá los tipos de entrada —`KeyEvent`, `ImeEvent`, `Modifiers`— vivían // en `llimphi-ui`, y `Key`/`NamedKey` eran directamente los de winit. La // consecuencia medida: los cinco widgets del editor (`text-editor`, // `text-input`, `text-area`, `edit-menu`, `gallery`) no podían cruzar a una // jaula WASM ni al kernel, porque procesar UNA TECLA arrastraba el windowing // de escritorio entero. // // El error de diseño era de dirección: el input no es del shell, es del // MODELO. Un `KeyEvent` es un dato — quién lo produjo (winit, el canal de // scancodes del kernel, un test) es asunto del sustrato, igual que quién // presenta el framebuffer. Este módulo lo pone donde el resto del modelo ya // vive, y cada shell CONVIERTE en su frontera: // // winit ──convierte──► input::KeyEvent ◄──convierte── sys_get_scancode // (llimphi-ui) (la jaula, W7) // // ## `NamedKey`: el subconjunto REAL, no el catálogo de winit // // winit modela cientos de teclas (media keys, teclado japonés, botones de // navegador). Se contó qué usa el repo entero: **31 variantes** — navegación, // edición, F1-F12 y modificadores — y puriy, que traduce teclas a nombres DOM // para el navegador, usa exactamente las mismas. Eso es la interfaz real, y es // lo que este enum nombra, con los MISMOS nombres que winit para que ni un // widget ni una app cambien un `match`. Lo no mapeado cae a `Unidentified`, // que todo `match` existente ya absorbe en su brazo `_`. // ============================================================================= use alloc::string::String; // Cada tipo lleva el derive de serde detrás de la feature. Va acá y no en un // espejo del otro lado del cable porque `NamedKey` tiene 31 variantes: una copia // se desincroniza en la primera tecla que alguien agregue, y el síntoma sería una // tecla que funciona suelta y no enjaulada. /// Una tecla con nombre. Espeja el subconjunto de `winit::keyboard::NamedKey` /// que el repo usa — mismos nombres, así el código existente no cambia. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum NamedKey { Enter, Escape, Backspace, Tab, Space, Delete, Insert, Home, End, PageUp, PageDown, ArrowUp, ArrowDown, ArrowLeft, ArrowRight, Shift, Control, Alt, Meta, CapsLock, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, /// Cualquier tecla con nombre que el modelo no distingue. Existe para que /// la conversión desde un sustrato rico (winit) sea total sin arrastrar su /// catálogo: una media key llega como esto y el brazo `_` de todo `match` /// existente la absorbe, que es lo que ya pasaba. Unidentified, } /// Una tecla: con nombre, o el carácter que produce. /// /// Espeja la forma de `winit::keyboard::Key` en sus dos variantes reales — el /// repo no usa `Dead` ni `Unidentified(NativeKey)` (verificado), así que no /// existen acá. `Character` lleva `String` y no el `SmolStr` de winit: mismo /// `.as_str()`, mismas comparaciones con `==`, sin la dependencia. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Key { /// Tecla con nombre (Enter, flechas, F5…). Named(NamedKey), /// Tecla que produce texto: el carácter ya con layout aplicado. Character(String), } /// Estado de una tecla en un evento. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum KeyState { Pressed, Released, } /// Modificadores activos en el momento del evento. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct Modifiers { pub shift: bool, pub ctrl: bool, pub alt: bool, pub meta: bool, } /// Evento de teclado normalizado. /// /// La MISMA forma que tenía en `llimphi-ui`, movida acá sin un campo de /// diferencia: era un dato puro desde el principio — sólo estaba mal vecindado. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, PartialEq)] pub struct KeyEvent { pub key: Key, pub state: KeyState, /// Texto resultante (con modifiers e IME aplicados). Útil para inserción /// directa; `None` para teclas que no producen texto (flechas, etc.). pub text: Option, pub modifiers: Modifiers, pub repeat: bool, } /// Evento de IME normalizado. Ver `App::on_ime` en el shell para el flujo /// Enabled → Preedit* → Commit/Disabled. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, PartialEq, Eq)] pub enum ImeEvent { /// El IME se activó para esta ventana. Enabled, /// Texto en composición (aún no confirmado). `cursor` es el rango /// `(inicio, fin)` en bytes a resaltar dentro de `text`, si el IME lo /// reporta. Cada `Preedit` reemplaza al anterior; uno con `text` /// vacío cierra la preedición sin confirmar. Preedit { text: String, cursor: Option<(usize, usize)>, }, /// Texto confirmado: insertarlo como si se hubiera tecleado. Commit(String), /// El IME se desactivó (perder foco, cambiar de método). Disabled, } /// Delta de rueda en "líneas" lógicas (normalizado a través de backends). /// Convención CSS: positivo = scroll **hacia abajo** (contenido sube). /// `x` similar para scroll horizontal (touchpads, ratones de 2 ejes). #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, Default, PartialEq)] pub struct WheelDelta { pub x: f32, pub y: f32, } // ============================================================================= // El SENTIDO de la rueda, y por qué va por clase de dispositivo // ----------------------------------------------------------------------------- // «Scroll natural» fue siempre UN interruptor para toda la máquina, y esa es la // forma equivocada: la rueda de un ratón y dos dedos sobre un pad no son el // mismo gesto. En el pad el contenido *se agarra* y sigue al dedo —invertido se // siente como arrastrar un papel—; en la rueda no hay papel que agarrar, y la // misma inversión se siente al revés. Quien usa las dos cosas en la misma // máquina (portátil con ratón enchufado) quiere una en natural y la otra no, y // con un solo interruptor tiene que elegir cuál de los dos sufre. // // Por eso son DOS sentidos, no uno, y por eso la clase de dispositivo viaja con // el delta ([`FuenteRueda`]) hasta la frontera que lo convierte. Cada sustrato // sabe distinguirlas con lo que ya le llega: // // winit LineDelta = ratón · PixelDelta = touchpad // wayland discrete≠0 = ratón · sólo `absolute` = touchpad // // La política es un GLOBAL de proceso a propósito: la decide la sesión (el // panel la escribe, el runtime la siembra al arrancar) y la consultan las dos // o tres fronteras que fabrican un `WheelDelta`. Pasarla por parámetro obligaría // a que cada `App` la conociera, y una `App` no tiene por qué saber si el humano // invierte su pad. // ============================================================================= use core::sync::atomic::{AtomicU8, Ordering}; /// De qué clase de dispositivo viene una rueda. La distinción no es cosmética: /// es la única forma de que ratón y touchpad puedan ir en sentidos distintos. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FuenteRueda { /// Rueda física de un ratón: clicks discretos. Raton, /// Dos dedos sobre un touchpad (o una rueda de precisión, que entrega /// píxeles y no clicks). Touchpad, } /// Sentido de la rueda para UNA clase de dispositivo. /// /// El punto de partida (`Clasico`) es la convención que ya normaliza Llimphi: /// positivo = scroll hacia abajo = el contenido sube. `Natural` invierte los dos /// ejes, que es exactamente lo que hace `natural_scroll` de libinput. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum SentidoRueda { /// **No reverso**: el delta pasa tal cual llega. #[default] Clasico, /// **Reverso**: el contenido sigue al dedo/rueda (se invierten `x` e `y`). Natural, } impl SentidoRueda { /// Id estable para config y para el panel. pub const fn as_str(self) -> &'static str { match self { Self::Clasico => "clasico", Self::Natural => "natural", } } /// Lee un id de config. Acepta los sinónimos con los que el humano lo /// nombra (`"reverso"`, `"invertido"`, `"no-reverso"`) para que un archivo /// escrito a mano no caiga en silencio al default. pub fn desde_id(s: &str) -> Option { match s { "natural" | "reverso" | "invertido" | "invertida" => Some(Self::Natural), "clasico" | "clásico" | "no-reverso" | "no_reverso" | "directo" => Some(Self::Clasico), _ => None, } } /// `true` si invierte (equivalente al `natural_scroll` de libinput). pub const fn es_natural(self) -> bool { matches!(self, Self::Natural) } /// Desde el booleano histórico (`natural_scroll`). pub const fn desde_natural(natural: bool) -> Self { if natural { Self::Natural } else { Self::Clasico } } } /// Los dos sentidos de la sesión: uno por clase de dispositivo. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct PoliticaRueda { /// Sentido de la rueda del ratón. pub raton: SentidoRueda, /// Sentido del deslizamiento de dos dedos en el touchpad. pub touchpad: SentidoRueda, } impl PoliticaRueda { /// El sentido que le toca a `fuente`. pub const fn sentido(&self, fuente: FuenteRueda) -> SentidoRueda { match fuente { FuenteRueda::Raton => self.raton, FuenteRueda::Touchpad => self.touchpad, } } /// Aplica el sentido al delta ya normalizado a la convención de Llimphi. pub fn aplicar(&self, d: WheelDelta, fuente: FuenteRueda) -> WheelDelta { if self.sentido(fuente).es_natural() { WheelDelta { x: -d.x, y: -d.y } } else { d } } } // El global de proceso. Dos bytes: `0` = clásico, `1` = natural — el mismo // mapeo que `SentidoRueda as u8`, pero explícito para que agregar una variante // no cambie en silencio lo que hay guardado. static RUEDA_RATON: AtomicU8 = AtomicU8::new(0); static RUEDA_TOUCHPAD: AtomicU8 = AtomicU8::new(0); const fn codigo(s: SentidoRueda) -> u8 { match s { SentidoRueda::Clasico => 0, SentidoRueda::Natural => 1, } } const fn desde_codigo(c: u8) -> SentidoRueda { if c == 1 { SentidoRueda::Natural } else { SentidoRueda::Clasico } } /// La política vigente en este proceso. Default: clásico en las dos clases — /// o sea, el delta pasa tal cual, que es lo que Llimphi hizo siempre. pub fn politica_rueda() -> PoliticaRueda { PoliticaRueda { raton: desde_codigo(RUEDA_RATON.load(Ordering::Relaxed)), touchpad: desde_codigo(RUEDA_TOUCHPAD.load(Ordering::Relaxed)), } } /// Fija la política del proceso. La llama el runtime al arrancar (sembrada del /// bus de config) y cada vez que el panel la cambia en caliente. pub fn fijar_politica_rueda(p: PoliticaRueda) { RUEDA_RATON.store(codigo(p.raton), Ordering::Relaxed); RUEDA_TOUCHPAD.store(codigo(p.touchpad), Ordering::Relaxed); } // ============================================================================= // navegación por teclado — EL atajo, uno solo para toda la suite // ----------------------------------------------------------------------------- // Cada app Llimphi tiene las mismas dos escalas: los **dientes** (el rail: las // secciones grandes) y los **tabs** (las sub-pestañas del canvas). Hasta acá se // navegaban las dos sólo con el mouse, y cada app que quisiera teclado iba a // elegir su propia tecla — que es como se termina con tres atajos distintos // para el mismo gesto en tres apps de la MISMA suite (ya pasó con los verbos de // los CLIs: regla 7). // // Por qué estas teclas y no otras: // // * El compositor (`mirada_brain::action::default_keymap`) se reserva `Super+*`, // `Alt+Tab`, `Ctrl+Alt+*`, `Ctrl+Print` y `F12`. `Ctrl+Tab`, `Ctrl+PageUp/Down` // y `Ctrl+` están **libres**: nunca los agarra el escritorio, así que // llegan enteros a la app enfocada. // * `Ctrl+Tab` para tabs es el atajo que ya tiene todo el mundo en las manos // (navegador, editor, terminal), y `Ctrl+PageUp/PageDown` es el otro clásico // de «cambiar de sección» (GTK notebook, Firefox, LibreOffice). Al ser teclas // distintas, las dos escalas nunca compiten por el mismo gesto. // * Los **dígitos van a los dientes** y no a los tabs: el diente es la // navegación primaria (es la lista de secciones de la app), y los tabs son // pocos y locales. `Ctrl+0` va al último, como el `Ctrl+9` del navegador. // // Queda afuera **pata**: su barra es layer-shell y con el cajón cerrado corre // con `KeyboardInteractivity::None`, así que un atajo con Ctrl no le llega // nunca — sus teclas viven en el compositor (es la razón por la que `F12` está // en el keymap del escritorio y no en la barra). // ============================================================================= /// Un gesto de navegación de la suite. Ver [`navegacion`]. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Navegacion { /// `Ctrl+PageDown` — el diente siguiente (cíclico, lo decide la app). DienteSiguiente, /// `Ctrl+PageUp` — el diente anterior. DienteAnterior, /// `Ctrl+1`…`Ctrl+9` — el diente N (base 0 acá: `Ctrl+1` → `Diente(0)`). /// `Ctrl+0` pide **el último**, y por eso viaja como [`Navegacion::DienteUltimo`]. Diente(usize), /// `Ctrl+0` — el último diente. DienteUltimo, /// `Ctrl+Tab` — el tab siguiente del canvas. TabSiguiente, /// `Ctrl+Shift+Tab` — el tab anterior. TabAnterior, } /// A qué diente lleva un gesto, dado el actual y cuántos hay. `None` si el /// gesto es de **tabs** (esos los resuelve la app: el número de tabs depende de /// qué diente esté abierto) o si no hay ningún diente. /// /// Existe para que la aritmética no se copie una vez por app. Es cíclica a /// propósito —del último se pasa al primero— y `Ctrl+` **satura** en vez /// de no hacer nada: con seis dientes, `Ctrl+9` lleva al sexto. Un atajo que no /// hace nada se lee como que el teclado no anda; uno que lleva al último se lee /// como lo que es. pub fn destino_diente(nav: Navegacion, actual: usize, n: usize) -> Option { if n == 0 { return None; } Some(match nav { Navegacion::DienteSiguiente => (actual + 1) % n, Navegacion::DienteAnterior => (actual + n - 1) % n, Navegacion::Diente(i) => i.min(n - 1), Navegacion::DienteUltimo => n - 1, Navegacion::TabSiguiente | Navegacion::TabAnterior => return None, }) } /// Lee un evento de teclado como un gesto de navegación de la suite/// Lee un evento de teclado como un gesto de navegación de la suite, o `None` /// si no lo es. /// /// Sólo mira el **Pressed**: un atajo se ejecuta una vez, y con el `Released` /// haría dos saltos por pulsación. Y exige que **no** haya `alt` ni `meta` /// puestos: `Ctrl+Alt+…` es territorio del compositor, y tomarlo acá le robaría /// el atajo al escritorio en la app enfocada. pub fn navegacion(ev: &KeyEvent) -> Option { if ev.state != KeyState::Pressed || !ev.modifiers.ctrl || ev.modifiers.alt || ev.modifiers.meta { return None; } match &ev.key { Key::Named(NamedKey::Tab) => Some(if ev.modifiers.shift { Navegacion::TabAnterior } else { Navegacion::TabSiguiente }), Key::Named(NamedKey::PageDown) => Some(Navegacion::DienteSiguiente), Key::Named(NamedKey::PageUp) => Some(Navegacion::DienteAnterior), // Los dígitos no llevan shift: `Ctrl+Shift+1` es otro gesto (y en varios // teclados ni siquiera produce un dígito). Key::Character(c) if !ev.modifiers.shift => match c.as_str() { "0" => Some(Navegacion::DienteUltimo), d if d.len() == 1 => { let n = d.as_bytes()[0]; (n.is_ascii_digit()).then(|| Navegacion::Diente((n - b'1') as usize)) } _ => None, }, _ => None, } } #[cfg(test)] mod tests_rueda { use super::*; #[test] fn clasico_no_toca_el_delta() { let p = PoliticaRueda::default(); let d = WheelDelta { x: 1.0, y: -3.0 }; assert_eq!(p.aplicar(d, FuenteRueda::Raton), d); assert_eq!(p.aplicar(d, FuenteRueda::Touchpad), d); } #[test] fn cada_clase_va_por_su_lado() { // El caso que motiva todo esto: portátil con ratón enchufado, pad en // natural y rueda en clásico. Un solo interruptor no puede expresarlo. let p = PoliticaRueda { raton: SentidoRueda::Clasico, touchpad: SentidoRueda::Natural, }; let d = WheelDelta { x: 2.0, y: 3.0 }; assert_eq!(p.aplicar(d, FuenteRueda::Raton), d); assert_eq!( p.aplicar(d, FuenteRueda::Touchpad), WheelDelta { x: -2.0, y: -3.0 } ); } #[test] fn el_global_arranca_neutro_y_se_puede_fijar() { assert_eq!(politica_rueda(), PoliticaRueda::default()); let p = PoliticaRueda { raton: SentidoRueda::Natural, touchpad: SentidoRueda::Clasico, }; fijar_politica_rueda(p); assert_eq!(politica_rueda(), p); fijar_politica_rueda(PoliticaRueda::default()); } #[test] fn los_ids_van_y_vuelven() { for s in [SentidoRueda::Clasico, SentidoRueda::Natural] { assert_eq!(SentidoRueda::desde_id(s.as_str()), Some(s)); } assert_eq!(SentidoRueda::desde_id("reverso"), Some(SentidoRueda::Natural)); assert_eq!(SentidoRueda::desde_id("no-reverso"), Some(SentidoRueda::Clasico)); assert_eq!(SentidoRueda::desde_id("nada"), None); } } #[cfg(test)] mod navegacion_tests { use super::*; fn ev(key: Key, ctrl: bool, shift: bool) -> KeyEvent { KeyEvent { key, state: KeyState::Pressed, text: None, modifiers: Modifiers { shift, ctrl, alt: false, meta: false }, repeat: false, } } #[test] fn las_dos_escalas_no_comparten_tecla() { assert_eq!( navegacion(&ev(Key::Named(NamedKey::Tab), true, false)), Some(Navegacion::TabSiguiente) ); assert_eq!( navegacion(&ev(Key::Named(NamedKey::Tab), true, true)), Some(Navegacion::TabAnterior) ); assert_eq!( navegacion(&ev(Key::Named(NamedKey::PageDown), true, false)), Some(Navegacion::DienteSiguiente) ); assert_eq!( navegacion(&ev(Key::Named(NamedKey::PageUp), true, false)), Some(Navegacion::DienteAnterior) ); } #[test] fn los_digitos_van_a_los_dientes_y_el_cero_al_ultimo() { assert_eq!( navegacion(&ev(Key::Character("1".into()), true, false)), Some(Navegacion::Diente(0)) ); assert_eq!( navegacion(&ev(Key::Character("9".into()), true, false)), Some(Navegacion::Diente(8)) ); assert_eq!( navegacion(&ev(Key::Character("0".into()), true, false)), Some(Navegacion::DienteUltimo) ); } /// Sin Ctrl no es un atajo: es tecleo. Un `Tab` pelado tiene que seguir /// llegando al foco de los campos, y una `1` al campo de texto. #[test] fn sin_ctrl_no_hay_navegacion() { assert_eq!(navegacion(&ev(Key::Named(NamedKey::Tab), false, false)), None); assert_eq!(navegacion(&ev(Key::Character("1".into()), false, false)), None); } /// `Ctrl+Alt+…` y `Super+…` son del compositor: no se los robamos. #[test] fn los_atajos_del_escritorio_no_se_tocan() { let mut e = ev(Key::Named(NamedKey::PageDown), true, false); e.modifiers.alt = true; assert_eq!(navegacion(&e), None); let mut e = ev(Key::Named(NamedKey::Tab), true, false); e.modifiers.meta = true; assert_eq!(navegacion(&e), None); } /// El `Released` no navega: con él cada pulsación saltaría dos dientes. #[test] fn solo_el_pressed_navega() { let mut e = ev(Key::Named(NamedKey::PageDown), true, false); e.state = KeyState::Released; assert_eq!(navegacion(&e), None); } /// La aritmética de los dientes, una sola vez para toda la suite: cíclica en /// los extremos y saturante en los dígitos. #[test] fn el_destino_es_ciclico_y_el_digito_satura() { use Navegacion::*; assert_eq!(destino_diente(DienteSiguiente, 4, 5), Some(0), "del último al primero"); assert_eq!(destino_diente(DienteAnterior, 0, 5), Some(4), "del primero al último"); assert_eq!(destino_diente(Diente(2), 0, 5), Some(2)); assert_eq!(destino_diente(Diente(8), 0, 5), Some(4), "Ctrl+9 con cinco → el quinto"); assert_eq!(destino_diente(DienteUltimo, 0, 5), Some(4)); // Los gestos de tab no son de diente: los resuelve la app. assert_eq!(destino_diente(TabSiguiente, 0, 5), None); // Sin dientes no hay a dónde ir (y no se divide por cero). assert_eq!(destino_diente(DienteSiguiente, 0, 0), None); } }