//! `llimphi-cable` — **el caño entre una app enjaulada y su anfitrión, en Linux.** //! //! W4 dejó resuelto qué cruza la frontera ([`llimphi_wire_escena`]), quién lo //! emite (`llimphi-jaula`) y quién lo pinta (`llimphi-reproductor`). Lo que //! faltaba para correr ese mismo cable entre **dos procesos de Linux** —y no //! entre una app `.wasm` y el kernel— es el transporte y la vuelta. Esto es eso, //! y nada más: enmarca, serializa y ordena. //! //! ## La forma //! //! ```text //! huésped (proceso propio) anfitrión (shuma / pata) //! ──────────────────────── ──────────────────────── //! view + layout + geometría //! │ ¿cuánto mide esta cadena? //! ├──────── PedirMedida ──────────► parley, con SU typesetter //! ◄──────── Medida ───────────────┤ (12 bytes) //! │ //! └──────── Cuadro(EscenaWire) ──► reproducir → píxeles //! │ //! ◄──────── Puntero / Tamaño ────────┘ //! ``` //! //! ## El marco, y por qué un cuadro a medias se tira entero //! //! Cada mensaje va como **largo (`u32` LE) + postcard**. Un lector que se quede //! sin bytes a mitad de un marco no puede entregar «lo que llegó»: media escena //! es peor que ninguna —se pinta una pantalla que nunca existió— así que el //! marco incompleto es un error de lectura y el cuadro se descarta. //! //! Hay un **tope** ([`MAX_MARCO`]) y no es paranoia de manual: del otro lado hay //! un proceso que puede estar roto, y un `u32` de basura pediría alocar 4 GiB //! antes de darse cuenta. //! //! ## El orden, que es la única parte con astucia //! //! Medir es **síncrono** para el huésped: taffy no puede seguir sin la respuesta. //! Pero mientras espera, el anfitrión puede mandarle un clic o un redimensionado. //! Si el huésped leyera el primer mensaje que llega y lo tratara como la medida, //! interpretaría un evento como tres `f32` y el layout saldría cualquier cosa. //! //! Por eso [`Huesped::medir`] lee **hasta encontrar la medida** y **encola** todo //! lo demás, que se drena después con [`Huesped::eventos`]. Nada se pierde y //! nada se confunde. El test `un_evento_que_llega_mientras_se_espera_la_medida_no_se_pierde` //! lo fija, con brazo de control. #![forbid(unsafe_code)] pub mod vida; pub use vida::{Estado, Motivo, Reintentos}; use std::collections::VecDeque; use std::io::{self, Read, Write}; use llimphi_entrada::{ImeEvent, KeyEvent, WheelDelta}; use llimphi_wire_escena::{EscenaWire, MedidaTexto, PeticionMedida}; use serde::{Deserialize, Serialize}; /// Tope de un marco, en bytes. /// /// 64 MiB: una escena densa anda en decenas de KiB —los patrones de prueba dan /// entre 186 y 313 bytes—, así que el tope no es un límite de diseño sino la /// defensa contra un largo de basura. Del otro lado hay un proceso que puede /// estar roto, y sin esto un `u32` cualquiera pediría alocar 4 GiB. pub const MAX_MARCO: usize = 64 * 1024 * 1024; /// Lo que el huésped le manda al anfitrión. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum DelHuesped { /// Un fotograma listo para pintar. Cuadro(Box), /// «¿Cuánto mide esta cadena?» — el huésped queda esperando la respuesta. PedirMedida(Box), } /// Lo que el anfitrión le manda al huésped. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum DelAnfitrion { /// La respuesta a [`DelHuesped::PedirMedida`]. /// /// Van los **12 bytes crudos** de [`MedidaTexto`] y no el struct: ese tipo no /// implementa serde a propósito —su representación fija existe para que el /// kernel la escriba sin alocar (`MedidaTexto::BYTES`)—, y agregarle serde /// acá para comodidad de este caño le cambiaría el contrato al otro cable. Medida([u8; MedidaTexto::BYTES]), /// El puntero, **en coordenadas del huésped**. /// /// Dos leyes que se heredan del kernel y no se negocian /// (`wasm/env/presentacion.rs`, `sys_puntero`): /// /// - **El huésped nunca conoce la posición absoluta.** Lo que viaja es /// local a su tablado, ya traducido. /// - **Sólo viajan los eventos que caen adentro.** Un clic sobre el chrome /// del anfitrión, sobre otra pestaña o sobre la barra no llega. Lo decide /// la geometría del anfitrión, no una comprobación del huésped: si fuera /// una comprobación, sería opcional. /// /// Del lado de Linux las dos salen gratis: el compositor ya entrega las /// coordenadas **locales al nodo** (`View::on_click_at`) y sólo llama al /// handler del nodo que fue golpeado. Va en `f32` y no en los `u16` del /// kernel porque en Llimphi las coordenadas son `f32` en todos lados — /// redondear acá perdería el subpíxel del escalado fraccionario. Puntero { x: f32, y: f32, apretado: bool }, /// Una tecla. El huésped la procesa con su propio `on_key`: el cable la /// lleva, no la interpreta. Tecla(KeyEvent), /// Texto compuesto (acentos por dead key, CJK, emoji). Viaja aparte de /// [`DelAnfitrion::Tecla`] porque es otro flujo —Enabled → Preedit* → /// Commit— y aplanarlo perdería la preedición, que es lo que se pinta /// mientras el usuario compone. Ime(ImeEvent), /// La rueda, en líneas lógicas. Convención CSS: positivo = hacia abajo. Rueda(WheelDelta), /// El puntero salió del tablado. /// /// Existe porque su ausencia es un defecto con nombre: sin esto, lo último /// que el huésped sabe del puntero es dónde estaba cuando se fue, y deja /// una fila resaltada para siempre. El anfitrión es el único que puede /// saberlo — el huésped no ve los eventos de afuera, justamente. PunteroSalio, /// El tablado cambió de tamaño. Tamano { ancho: f32, alto: f32 }, /// El anfitrión se va. El huésped debería salir en vez de quedarse pintando /// contra un caño muerto. Adios, /// **Andá a este lugar.** La ruta como texto, porque el cable no conoce /// `PathBuf` ni tiene por qué. /// /// Existe porque el anfitrión suele tener su propio navegador al lado del /// tablado —en pata, el árbol de carpetas del sidebar— y sin esto ese árbol /// es decoración: el clic mueve el estado del anfitrión y el huésped, que es /// el que se ve, se queda donde estaba. Los otros eventos son de **entrada /// cruda** (dónde está el dedo, qué tecla); éste es el único que dice una /// intención, y va aparte justamente por eso: mandarlo como una tecla /// sintética sería inventarle al huésped un atajo que no existe. Ir(String), /// **Montá este dispositivo.** El id del nodo de `DispositivosSource`, como /// texto: un disco no es una carpeta, es una fuente sintética con su propio /// espacio de ids, así que [`Self::Ir`] —que habla rutas POSIX— no lo puede /// expresar. /// /// Existe por lo mismo que `Ir`: el anfitrión pinta al lado su propia /// sección de Dispositivos, y sin esto esas filas son decoración. AbrirDispositivo(String), /// **Abrí esta colección.** El id del qutu (un ULID), como texto. Mismo /// motivo que [`Self::AbrirDispositivo`]: una colección tampoco es una /// carpeta. AbrirColeccion(String), } /// Escribe un mensaje enmarcado. pub fn escribir(destino: &mut W, msg: &T) -> io::Result<()> { let cuerpo = postcard::to_allocvec(msg) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; if cuerpo.len() > MAX_MARCO { return Err(io::Error::new( io::ErrorKind::InvalidData, format!("marco de {} bytes, tope {MAX_MARCO}", cuerpo.len()), )); } destino.write_all(&(cuerpo.len() as u32).to_le_bytes())?; destino.write_all(&cuerpo)?; destino.flush() } /// Lee un mensaje enmarcado. Bloquea hasta tenerlo **entero**. /// /// Un marco incompleto es un error y no un mensaje a medias: ver la doc del /// módulo. `UnexpectedEof` limpio significa que el otro lado cerró. pub fn leer Deserialize<'de>, R: Read>(origen: &mut R) -> io::Result { let mut largo = [0u8; 4]; origen.read_exact(&mut largo)?; let largo = u32::from_le_bytes(largo) as usize; if largo > MAX_MARCO { return Err(io::Error::new( io::ErrorKind::InvalidData, format!("marco de {largo} bytes, tope {MAX_MARCO}"), )); } let mut cuerpo = vec![0u8; largo]; origen.read_exact(&mut cuerpo)?; postcard::from_bytes(&cuerpo).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string())) } /// El extremo del **huésped**: manda cuadros, pide medidas, recibe eventos. /// /// Genérico sobre el flujo para que los tests corran sobre un par de sockets sin /// procesos ni archivos — y para que mañana el caño pueda ser otra cosa sin /// tocar esta lógica. pub struct Huesped { flujo: S, /// Lo que llegó mientras se esperaba una medida. Ver la doc del módulo. pendientes: VecDeque, /// Cuántas medidas se pidieron. Es el número que el SDD manda medir antes de /// creerle nada al diseño: si un cuadro pide muchas, el trabajo no es /// acelerar el viaje sino cachear. medidas_pedidas: u64, } impl Huesped { pub fn new(flujo: S) -> Self { Self { flujo, pendientes: VecDeque::new(), medidas_pedidas: 0 } } /// Cuántas medidas pidió en toda su vida. pub fn medidas_pedidas(&self) -> u64 { self.medidas_pedidas } /// Manda un fotograma. pub fn enviar_cuadro(&mut self, escena: &EscenaWire) -> io::Result<()> { escribir(&mut self.flujo, &DelHuesped::Cuadro(Box::new(escena.clone()))) } /// Pide una medida y **espera** la respuesta, encolando lo que llegue en el /// medio. Ver «El orden» en la doc del módulo. pub fn medir(&mut self, peticion: &PeticionMedida) -> io::Result { self.medidas_pedidas += 1; escribir(&mut self.flujo, &DelHuesped::PedirMedida(Box::new(peticion.clone())))?; loop { match leer::(&mut self.flujo)? { DelAnfitrion::Medida(b) => return Ok(MedidaTexto::de_bytes(&b)), otro => self.pendientes.push_back(otro), } } } /// Los eventos que se encolaron mientras se esperaban medidas. /// /// **No lee del caño**: drena lo ya recibido. Leer acá bloquearía el bucle /// del huésped esperando un evento que puede no venir nunca — un asiento /// quieto no manda nada. pub fn eventos(&mut self) -> impl Iterator + '_ { std::iter::from_fn(move || self.pendientes.pop_front()) } /// Espera **un** evento del anfitrión, bloqueando. Para el arranque, donde el /// huésped necesita saber su tamaño antes de componer el primer cuadro. pub fn esperar_evento(&mut self) -> io::Result { if let Some(e) = self.pendientes.pop_front() { return Ok(e); } leer(&mut self.flujo) } } /// El extremo del **anfitrión**: recibe cuadros y pedidos, contesta medidas. pub struct Anfitrion { flujo: S, } impl Anfitrion { pub fn new(flujo: S) -> Self { Self { flujo } } /// Lee lo próximo que mande el huésped. Bloquea. pub fn recibir(&mut self) -> io::Result { leer(&mut self.flujo) } /// Contesta una medida. pub fn responder_medida(&mut self, m: MedidaTexto) -> io::Result<()> { escribir(&mut self.flujo, &DelAnfitrion::Medida(m.a_bytes())) } /// Atiende **un** mensaje del huésped: guarda el cuadro o contesta la medida. /// /// `medir` es del llamador porque medir necesita un `Typesetter` y este crate /// no dibuja ni mide — la misma línea que separa al reproductor del cable. Es /// la simetría de `llimphi_jaula::medidor::medir_con` del otro lado: el /// marshalling es común, el cruce lo pone quien lo tiene. /// /// **El que contesta tiene que ser el que pinta.** Si el anfitrión midiera /// con un typesetter distinto del que va a shapear, el texto se mediría con /// un juego de fuentes y se pintaría con otro — y eso no mueve el glifo, /// mueve la caja de línea (`SDD-JAULA-LINUX.md`). pub fn atender_uno(&mut self, mut medir: F) -> io::Result where F: FnMut(&PeticionMedida) -> MedidaTexto, { match self.recibir()? { DelHuesped::Cuadro(e) => Ok(Atendido::Cuadro(e)), DelHuesped::PedirMedida(p) => { let m = medir(&p); self.responder_medida(m)?; Ok(Atendido::Medida) } } } /// Manda un evento (puntero, tamaño, adiós). pub fn enviar(&mut self, e: &DelAnfitrion) -> io::Result<()> { escribir(&mut self.flujo, e) } /// Manda un clic **ya local al tablado**. /// /// Los `(x, y)` son los que `View::on_click_at` entrega: relativos al nodo. /// El anfitrión no tiene que restar nada ni saber dónde está su cuerpo /// dentro de la surface — que es justo la trampa que `SDD-IDE.md` anota para /// la rueda («shuma hospedada en el cajón de pata no sabe el origen de su /// cuerpo»): acá no aparece porque el compositor ya resolvió el origen. pub fn enviar_clic(&mut self, x: f32, y: f32) -> io::Result<()> { self.enviar(&DelAnfitrion::Puntero { x, y, apretado: true }) } /// Manda un movimiento del puntero, local al tablado. pub fn enviar_movimiento(&mut self, x: f32, y: f32) -> io::Result<()> { self.enviar(&DelAnfitrion::Puntero { x, y, apretado: false }) } /// Avisa que el puntero se fue. Ver [`DelAnfitrion::PunteroSalio`]: sin esto /// el huésped deja una fila resaltada para siempre. pub fn enviar_salida(&mut self) -> io::Result<()> { self.enviar(&DelAnfitrion::PunteroSalio) } pub fn enviar_tecla(&mut self, e: KeyEvent) -> io::Result<()> { self.enviar(&DelAnfitrion::Tecla(e)) } pub fn enviar_ime(&mut self, e: ImeEvent) -> io::Result<()> { self.enviar(&DelAnfitrion::Ime(e)) } pub fn enviar_rueda(&mut self, d: WheelDelta) -> io::Result<()> { self.enviar(&DelAnfitrion::Rueda(d)) } /// Avisa el tamaño del tablado. Se manda **sólo cuando cambia**: el huésped /// re-compone al recibirlo, y mandarlo por cuadro lo haría re-componer /// siempre. Quien llama guarda el último — ver [`Tamanos`]. /// Le pide al huésped que se pare en `ruta`. Ver [`DelAnfitrion::Ir`]. pub fn enviar_ir(&mut self, ruta: &str) -> io::Result<()> { self.enviar(&DelAnfitrion::Ir(ruta.to_string())) } /// Le pide al huésped que monte un dispositivo. Ver /// [`DelAnfitrion::AbrirDispositivo`]. pub fn enviar_abrir_dispositivo(&mut self, id: &str) -> io::Result<()> { self.enviar(&DelAnfitrion::AbrirDispositivo(id.to_string())) } /// Le pide al huésped que abra una colección. Ver /// [`DelAnfitrion::AbrirColeccion`]. pub fn enviar_abrir_coleccion(&mut self, id: &str) -> io::Result<()> { self.enviar(&DelAnfitrion::AbrirColeccion(id.to_string())) } pub fn enviar_tamano(&mut self, ancho: f32, alto: f32) -> io::Result<()> { self.enviar(&DelAnfitrion::Tamano { ancho, alto }) } /// Se despide. El huésped debería salir en vez de seguir pintando contra un /// caño muerto. pub fn despedirse(&mut self) -> io::Result<()> { self.enviar(&DelAnfitrion::Adios) } } /// Qué se atendió en una vuelta de [`Anfitrion::atender_uno`]. #[derive(Debug)] pub enum Atendido { /// Llegó un fotograma: el huésped terminó de componer. Cuadro(Box), /// Se contestó una medida. El huésped sigue componiendo. Medida, } impl Anfitrion { /// Atiende hasta que llegue un cuadro **o se acabe el plazo**. /// /// Es la respuesta práctica a la pregunta que abrió F4.bis: medir es síncrono /// para el huésped, así que si el anfitrión contestara una medida por cuadro /// de UI, un árbol con cincuenta etiquetas tardaría cincuenta cuadros en /// aparecer. Acá se contestan **todas las que hagan falta** dentro de una /// misma vuelta, y el primer fotograma sale enseguida. /// /// El plazo es lo que impide que eso se convierta en un cuelgue: un huésped /// trabado —o uno malicioso que pide medidas para siempre— no puede quedarse /// con el hilo que dibuja. Se vuelve con `Ok(None)` y el tablado sigue /// mostrando lo último que tenía; a la vuelta siguiente se retoma donde /// quedó, porque el socket no pierde nada. /// /// **`Ok(None)` no es un error y no se cuenta como muerte.** Distinguirlos es /// justamente lo que evita matar a un huésped lento. pub fn atender_hasta( &mut self, plazo: core::time::Duration, mut medir: F, ) -> io::Result>> where F: FnMut(&PeticionMedida) -> MedidaTexto, { let hasta = std::time::Instant::now() + plazo; loop { let queda = hasta.saturating_duration_since(std::time::Instant::now()); if queda.is_zero() { return Ok(None); } // El timeout se vuelve a poner en cada vuelta con lo que QUEDA: uno // fijo por lectura dejaría que N medidas lentas sumaran N plazos. self.flujo.set_read_timeout(Some(queda))?; match self.atender_uno(&mut medir) { Ok(Atendido::Cuadro(e)) => return Ok(Some(e)), Ok(Atendido::Medida) => continue, Err(e) if agotado(&e) => return Ok(None), Err(e) => return Err(e), } } } } /// ¿Este error es «se acabó el tiempo» y no «se rompió algo»? /// /// Los dos nombres existen porque el sistema usa uno u otro según la /// configuración del socket, y tratar el plazo como una rotura mataría al /// huésped lento en vez de esperarlo. fn agotado(e: &io::Error) -> bool { matches!(e.kind(), io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut) } /// Recuerda el último tamaño avisado, para no repetirlo. /// /// Existe porque el anfitrión conoce su rectángulo **en cada pintada** y la /// tentación es avisarlo ahí: el huésped re-compone al recibir un tamaño, así /// que avisarlo por cuadro lo haría re-componer sesenta veces por segundo sin /// que nada haya cambiado. Comparar es de una línea; olvidarse cuesta el /// fotograma entero. #[derive(Debug, Clone, Copy, Default)] pub struct Tamanos { ultimo: Option<(f32, f32)>, } impl Tamanos { pub fn new() -> Self { Self::default() } /// El último tamaño avisado, o `None` si **nunca se avisó ninguno**. Es el /// dato que separa «el huésped no contesta» de «al huésped nadie le dijo de /// qué tamaño es», que en pantalla se ven igual. pub fn ultimo(&self) -> Option<(f32, f32)> { self.ultimo } /// `true` si este tamaño hay que avisarlo (es el primero, o cambió). pub fn cambio(&mut self, ancho: f32, alto: f32) -> bool { // Comparación exacta a propósito: un layout que devuelve el mismo número // devuelve EL MISMO número. Una tolerancia acá dejaría pasar cambios // reales de subpíxel en pantallas con escalado fraccionario. if self.ultimo == Some((ancho, alto)) { return false; } self.ultimo = Some((ancho, alto)); true } } #[cfg(test)] mod tests { use super::*; use std::os::unix::net::UnixStream; fn peticion() -> PeticionMedida { PeticionMedida { cadena: "hola".into(), estilo: Default::default(), ancho_max: None, } } fn medida(w: f32) -> MedidaTexto { MedidaTexto { width: w, full_width: w, height: 10.0 } } /// El marco sobrevive el viaje: lo que entra es lo que sale. #[test] fn un_mensaje_va_y_vuelve_igual() { let (a, b) = UnixStream::pair().unwrap(); let mut hue = Huesped::new(a); let mut anf = Anfitrion::new(b); hue.enviar_cuadro(&EscenaWire::new()).unwrap(); match anf.recibir().unwrap() { DelHuesped::Cuadro(e) => assert_eq!(e.ordenes().len(), 0), otro => panic!("llegó {otro:?}"), } } /// **La razón de ser del encolado.** El anfitrión manda un evento justo /// mientras el huésped espera una medida. Sin la cola, el huésped leería el /// evento y lo interpretaría como la medida — tres `f32` sacados de un clic. #[test] fn un_evento_que_llega_mientras_se_espera_la_medida_no_se_pierde() { let (a, b) = UnixStream::pair().unwrap(); let mut hue = Huesped::new(a); let mut anf = Anfitrion::new(b); // El anfitrión se adelanta: manda un evento ANTES de la respuesta. anf.enviar(&DelAnfitrion::Puntero { x: 3.0, y: 4.0, apretado: true }).unwrap(); anf.responder_medida(medida(42.0)).unwrap(); let m = hue.medir(&peticion()).unwrap(); assert_eq!(m.width, 42.0, "la medida es LA medida, no el evento de antes"); let evs: Vec<_> = hue.eventos().collect(); assert_eq!( evs, vec![DelAnfitrion::Puntero { x: 3.0, y: 4.0, apretado: true }], "y el evento sigue ahí, en orden" ); } /// Brazo de control del anterior: sin nada en el medio, la cola queda vacía. /// Sin esto, un `eventos()` que siempre devolviera algo pasaría el test de /// arriba igual. #[test] fn sin_eventos_en_el_medio_la_cola_queda_vacia() { let (a, b) = UnixStream::pair().unwrap(); let mut hue = Huesped::new(a); let mut anf = Anfitrion::new(b); anf.responder_medida(medida(7.0)).unwrap(); assert_eq!(hue.medir(&peticion()).unwrap().width, 7.0); assert_eq!(hue.eventos().count(), 0); } /// Varios eventos en el medio conservan su orden, y el segundo pedido de /// medida no se come los que quedaron. #[test] fn dos_medidas_seguidas_conservan_los_eventos_de_las_dos() { let (a, b) = UnixStream::pair().unwrap(); let mut hue = Huesped::new(a); let mut anf = Anfitrion::new(b); anf.enviar(&DelAnfitrion::Tamano { ancho: 100.0, alto: 50.0 }).unwrap(); anf.responder_medida(medida(1.0)).unwrap(); anf.enviar(&DelAnfitrion::Puntero { x: 1.0, y: 1.0, apretado: false }).unwrap(); anf.responder_medida(medida(2.0)).unwrap(); assert_eq!(hue.medir(&peticion()).unwrap().width, 1.0); assert_eq!(hue.medir(&peticion()).unwrap().width, 2.0); let evs: Vec<_> = hue.eventos().collect(); assert_eq!(evs.len(), 2, "los dos eventos, ninguno perdido"); assert!(matches!(evs[0], DelAnfitrion::Tamano { .. }), "y en orden de llegada"); assert!(matches!(evs[1], DelAnfitrion::Puntero { .. })); } /// `eventos()` **no lee del caño**: si leyera, el huésped se colgaría /// esperando un evento que un asiento quieto nunca manda. #[test] fn drenar_eventos_no_bloquea_con_el_cano_vacio() { let (a, _b) = UnixStream::pair().unwrap(); let mut hue = Huesped::new(a); assert_eq!(hue.eventos().count(), 0); } /// El contador es el instrumento que el SDD manda mirar antes de creerle /// nada al diseño. #[test] fn las_medidas_se_cuentan() { let (a, b) = UnixStream::pair().unwrap(); let mut hue = Huesped::new(a); let mut anf = Anfitrion::new(b); assert_eq!(hue.medidas_pedidas(), 0); anf.responder_medida(medida(1.0)).unwrap(); anf.responder_medida(medida(1.0)).unwrap(); hue.medir(&peticion()).unwrap(); hue.medir(&peticion()).unwrap(); assert_eq!(hue.medidas_pedidas(), 2); } /// Una tecla viaja **entera**: el carácter, el estado, el texto resultante y /// los modificadores. Es lo que hace que un `Ctrl+S` adentro de la jaula sea /// el mismo `Ctrl+S` que afuera. #[test] fn una_tecla_llega_con_todo_lo_que_lleva() { use llimphi_entrada::{Key, KeyState, Modifiers, NamedKey}; let (a, b) = UnixStream::pair().unwrap(); let mut hue = Huesped::new(a); let mut anf = Anfitrion::new(b); let tecla = KeyEvent { key: Key::Character("s".into()), state: KeyState::Pressed, text: Some("s".into()), modifiers: Modifiers { ctrl: true, ..Default::default() }, repeat: false, }; anf.enviar_tecla(tecla.clone()).unwrap(); match hue.esperar_evento().unwrap() { DelAnfitrion::Tecla(t) => { assert_eq!(t, tecla, "ni el texto ni los modificadores se pierden"); assert!(t.modifiers.ctrl, "y el Ctrl sigue puesto del otro lado"); } otro => panic!("llegó {otro:?}"), } // Y una tecla con nombre, que es el otro brazo del enum. let flecha = KeyEvent { key: Key::Named(NamedKey::ArrowDown), state: KeyState::Pressed, text: None, modifiers: Modifiers::default(), repeat: true, }; anf.enviar_tecla(flecha.clone()).unwrap(); match hue.esperar_evento().unwrap() { DelAnfitrion::Tecla(t) => assert_eq!(t, flecha), otro => panic!("llegó {otro:?}"), } } /// El texto compuesto va por su propio carril: aplanarlo contra /// [`DelAnfitrion::Tecla`] perdería la preedición, que es lo que se pinta /// mientras el usuario todavía está componiendo. #[test] fn la_preedicion_del_ime_sobrevive_el_viaje() { let (a, b) = UnixStream::pair().unwrap(); let mut hue = Huesped::new(a); let mut anf = Anfitrion::new(b); let pre = ImeEvent::Preedit { text: "á".into(), cursor: Some((0, 2)) }; anf.enviar_ime(pre.clone()).unwrap(); assert_eq!(hue.esperar_evento().unwrap(), DelAnfitrion::Ime(pre)); anf.enviar_ime(ImeEvent::Commit("á".into())).unwrap(); assert_eq!( hue.esperar_evento().unwrap(), DelAnfitrion::Ime(ImeEvent::Commit("á".into())), "y el commit llega distinguible de la preedición" ); } /// El clic viaja con las coordenadas **tal como las da el compositor**: /// locales al nodo. El anfitrión no resta nada. #[test] fn el_clic_viaja_local_y_apretado() { let (a, b) = UnixStream::pair().unwrap(); let mut hue = Huesped::new(a); let mut anf = Anfitrion::new(b); anf.enviar_clic(12.5, 4.0).unwrap(); assert_eq!( hue.esperar_evento().unwrap(), DelAnfitrion::Puntero { x: 12.5, y: 4.0, apretado: true } ); // Y el movimiento es el mismo evento sin apretar: un huésped que quiera // hover no necesita otro canal. anf.enviar_movimiento(1.0, 2.0).unwrap(); assert_eq!( hue.esperar_evento().unwrap(), DelAnfitrion::Puntero { x: 1.0, y: 2.0, apretado: false } ); } /// **La salida del puntero es un evento, no la ausencia de eventos.** Sin /// esto, lo último que el huésped sabe es dónde estaba el puntero cuando se /// fue, y deja una fila resaltada para siempre. #[test] fn la_salida_del_puntero_se_avisa() { let (a, b) = UnixStream::pair().unwrap(); let mut hue = Huesped::new(a); let mut anf = Anfitrion::new(b); anf.enviar_salida().unwrap(); assert_eq!(hue.esperar_evento().unwrap(), DelAnfitrion::PunteroSalio); } /// El tamaño se avisa cuando cambia, y **sólo** cuando cambia: el huésped /// re-compone al recibirlo. #[test] fn el_tamano_solo_se_avisa_cuando_cambia() { let mut t = Tamanos::new(); assert!(t.cambio(100.0, 50.0), "el primero siempre se avisa"); assert!(!t.cambio(100.0, 50.0), "el mismo, no"); assert!(!t.cambio(100.0, 50.0)); assert!(t.cambio(100.0, 51.0), "un píxel de alto SÍ es un cambio"); assert!(t.cambio(99.5, 51.0), "y medio píxel de ancho también"); } /// Los eventos de entrada también se encolan si llegan mientras se espera /// una medida. Es el mismo mecanismo que ya estaba, con el tipo nuevo: si /// una tecla se colara como medida, el layout saldría de un scancode. #[test] fn una_tecla_en_el_medio_de_una_medida_tampoco_se_pierde() { use llimphi_entrada::{Key, KeyState, Modifiers}; let (a, b) = UnixStream::pair().unwrap(); let mut hue = Huesped::new(a); let mut anf = Anfitrion::new(b); anf.enviar_tecla(KeyEvent { key: Key::Character("x".into()), state: KeyState::Pressed, text: Some("x".into()), modifiers: Modifiers::default(), repeat: false, }) .unwrap(); anf.responder_medida(medida(5.0)).unwrap(); assert_eq!(hue.medir(&peticion()).unwrap().width, 5.0); let evs: Vec<_> = hue.eventos().collect(); assert_eq!(evs.len(), 1); assert!(matches!(evs[0], DelAnfitrion::Tecla(_))); } /// **El primer fotograma sale en una vuelta, no en cincuenta.** El huésped /// pide tres medidas y recién después manda el cuadro; el anfitrión contesta /// las tres y devuelve el cuadro en la misma llamada. #[test] fn se_contestan_todas_las_medidas_que_hagan_falta_antes_del_cuadro() { let (a, b) = UnixStream::pair().unwrap(); let mut anf = Anfitrion::new(b); // El huésped, en su hilo: mide tres veces y después compone. let h = std::thread::spawn(move || { let mut hue = Huesped::new(a); for _ in 0..3 { hue.medir(&peticion()).unwrap(); } hue.enviar_cuadro(&EscenaWire::new()).unwrap(); hue.medidas_pedidas() }); let mut medidas = 0; let cuadro = anf .atender_hasta(std::time::Duration::from_secs(5), |_p| { medidas += 1; medida(10.0) }) .unwrap(); assert!(cuadro.is_some(), "el cuadro llegó en la MISMA vuelta"); assert_eq!(medidas, 3, "y se contestaron las tres medidas de camino"); assert_eq!(h.join().unwrap(), 3); } /// **Un huésped trabado no se queda con el hilo que dibuja.** Pide medidas /// para siempre y nunca manda cuadro; el anfitrión vuelve con `None` al /// vencer el plazo, sin error y sin matarlo. #[test] fn un_huesped_que_no_manda_cuadro_devuelve_el_hilo_al_vencer_el_plazo() { let (a, b) = UnixStream::pair().unwrap(); let mut anf = Anfitrion::new(b); let (fin_tx, fin_rx) = std::sync::mpsc::channel::<()>(); let h = std::thread::spawn(move || { let mut hue = Huesped::new(a); // Mide hasta que el test le diga que pare. Nunca compone. while fin_rx.try_recv().is_err() { if hue.medir(&peticion()).is_err() { break; } } }); let arranque = std::time::Instant::now(); let cuadro = anf .atender_hasta(std::time::Duration::from_millis(150), |_p| medida(1.0)) .expect("vencer el plazo NO es un error"); assert!(cuadro.is_none(), "no hubo cuadro, y eso está bien"); assert!( arranque.elapsed() < std::time::Duration::from_secs(2), "y volvió cerca del plazo, no cuando al huésped se le dio la gana" ); let _ = fin_tx.send(()); drop(anf); let _ = h.join(); } /// Con el caño quieto y nadie del otro lado, el plazo vence igual: no se /// cuelga esperando a un huésped que no va a hablar. #[test] fn con_el_cano_mudo_el_plazo_vence_solo() { let (a, b) = UnixStream::pair().unwrap(); let _vivo = a; // se sostiene: cerrarlo daría EOF, que es otra cosa let mut anf = Anfitrion::new(b); let r = anf.atender_hasta(std::time::Duration::from_millis(80), |_p| medida(1.0)); assert!(matches!(r, Ok(None)), "plazo vencido, no error: dio {r:?}"); } /// **Brazo de control del anterior**: si el otro lado CIERRA, eso sí es una /// muerte y tiene que distinguirse de un plazo vencido. Sin este test, un /// `atender_hasta` que devolviera `Ok(None)` ante cualquier error dejaría al /// anfitrión esperando para siempre a un huésped que ya no está. #[test] fn el_cierre_no_se_confunde_con_un_plazo_vencido() { let (a, b) = UnixStream::pair().unwrap(); drop(a); let mut anf = Anfitrion::new(b); let e = anf .atender_hasta(std::time::Duration::from_secs(5), |_p| medida(1.0)) .expect_err("el otro lado cerró: eso es una muerte"); assert_eq!(e.kind(), io::ErrorKind::UnexpectedEof); } /// `atender_uno` contesta la medida con lo que el llamador diga, y le pasa /// la petición entera — no un resumen. #[test] fn atender_uno_contesta_con_lo_que_el_anfitrion_mide() { let (a, b) = UnixStream::pair().unwrap(); let mut anf = Anfitrion::new(b); let h = std::thread::spawn(move || { let mut hue = Huesped::new(a); hue.medir(&peticion()).unwrap() }); let mut vista = None; match anf.atender_uno(|p| { vista = Some(p.cadena.clone()); medida(33.0) }) { Ok(Atendido::Medida) => {} otro => panic!("esperaba una medida, dio {otro:?}"), } assert_eq!(vista.as_deref(), Some("hola"), "la petición llega entera"); assert_eq!(h.join().unwrap().width, 33.0); } /// Un largo de basura no puede hacer alocar 4 GiB. #[test] fn un_largo_imposible_es_error_y_no_una_alocacion() { let mut basura: &[u8] = &u32::MAX.to_le_bytes(); let r = leer::(&mut basura); let e = r.expect_err("un marco de 4 GiB tiene que rebotar"); assert_eq!(e.kind(), io::ErrorKind::InvalidData); } /// Un marco cortado a la mitad es un error, no medio mensaje. #[test] fn un_marco_incompleto_no_entrega_nada() { let mut cortado: &[u8] = &[10, 0, 0, 0, 1, 2, 3]; let r = leer::(&mut cortado); assert!(r.is_err(), "faltan bytes: no hay mensaje"); } /// El otro lado cerró: se distingue de un marco roto. #[test] fn el_cierre_del_otro_lado_se_reconoce() { let (a, b) = UnixStream::pair().unwrap(); drop(b); let mut anf = Anfitrion::new(a); let e = anf.recibir().expect_err("el otro lado cerró"); assert_eq!(e.kind(), io::ErrorKind::UnexpectedEof); } }