tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual
1//! `llimphi-cable` — **el caño entre una app enjaulada y su anfitrión, en Linux.**
2//!
3//! W4 dejó resuelto qué cruza la frontera ([`llimphi_wire_escena`]), quién lo
4//! emite (`llimphi-jaula`) y quién lo pinta (`llimphi-reproductor`). Lo que
5//! faltaba para correr ese mismo cable entre **dos procesos de Linux** —y no
6//! entre una app `.wasm` y el kernel— es el transporte y la vuelta. Esto es eso,
7//! y nada más: enmarca, serializa y ordena.
8//!
9//! ## La forma
10//!
11//! ```text
12//! huésped (proceso propio) anfitrión (shuma / pata)
13//! ──────────────────────── ────────────────────────
14//! view + layout + geometría
15//! │ ¿cuánto mide esta cadena?
16//! ├──────── PedirMedida ──────────► parley, con SU typesetter
17//! ◄──────── Medida ───────────────┤ (12 bytes)
18//! │
19//! └──────── Cuadro(EscenaWire) ──► reproducir → píxeles
20//! │
21//! ◄──────── Puntero / Tamaño ────────┘
22//! ```
23//!
24//! ## El marco, y por qué un cuadro a medias se tira entero
25//!
26//! Cada mensaje va como **largo (`u32` LE) + postcard**. Un lector que se quede
27//! sin bytes a mitad de un marco no puede entregar «lo que llegó»: media escena
28//! es peor que ninguna —se pinta una pantalla que nunca existió— así que el
29//! marco incompleto es un error de lectura y el cuadro se descarta.
30//!
31//! Hay un **tope** ([`MAX_MARCO`]) y no es paranoia de manual: del otro lado hay
32//! un proceso que puede estar roto, y un `u32` de basura pediría alocar 4 GiB
33//! antes de darse cuenta.
34//!
35//! ## El orden, que es la única parte con astucia
36//!
37//! Medir es **síncrono** para el huésped: taffy no puede seguir sin la respuesta.
38//! Pero mientras espera, el anfitrión puede mandarle un clic o un redimensionado.
39//! Si el huésped leyera el primer mensaje que llega y lo tratara como la medida,
40//! interpretaría un evento como tres `f32` y el layout saldría cualquier cosa.
41//!
42//! Por eso [`Huesped::medir`] lee **hasta encontrar la medida** y **encola** todo
43//! lo demás, que se drena después con [`Huesped::eventos`]. Nada se pierde y
44//! nada se confunde. El test `un_evento_que_llega_mientras_se_espera_la_medida_no_se_pierde`
45//! lo fija, con brazo de control.
46
47#![forbid(unsafe_code)]
48
49pub mod vida;
50pub use vida::{Estado, Motivo, Reintentos};
51
52use std::collections::VecDeque;
53use std::io::{self, Read, Write};
54
55use llimphi_entrada::{ImeEvent, KeyEvent, WheelDelta};
56use llimphi_wire_escena::{EscenaWire, MedidaTexto, PeticionMedida};
57use serde::{Deserialize, Serialize};
58
59/// Tope de un marco, en bytes.
60///
61/// 64 MiB: una escena densa anda en decenas de KiB —los patrones de prueba dan
62/// entre 186 y 313 bytes—, así que el tope no es un límite de diseño sino la
63/// defensa contra un largo de basura. Del otro lado hay un proceso que puede
64/// estar roto, y sin esto un `u32` cualquiera pediría alocar 4 GiB.
65pub const MAX_MARCO: usize = 64 * 1024 * 1024;
66
67/// Lo que el huésped le manda al anfitrión.
68#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
69pub enum DelHuesped {
70 /// Un fotograma listo para pintar.
71 Cuadro(Box<EscenaWire>),
72 /// «¿Cuánto mide esta cadena?» — el huésped queda esperando la respuesta.
73 PedirMedida(Box<PeticionMedida>),
74}
75
76/// Lo que el anfitrión le manda al huésped.
77#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
78pub enum DelAnfitrion {
79 /// La respuesta a [`DelHuesped::PedirMedida`].
80 ///
81 /// Van los **12 bytes crudos** de [`MedidaTexto`] y no el struct: ese tipo no
82 /// implementa serde a propósito —su representación fija existe para que el
83 /// kernel la escriba sin alocar (`MedidaTexto::BYTES`)—, y agregarle serde
84 /// acá para comodidad de este caño le cambiaría el contrato al otro cable.
85 Medida([u8; MedidaTexto::BYTES]),
86 /// El puntero, **en coordenadas del huésped**.
87 ///
88 /// Dos leyes que se heredan del kernel y no se negocian
89 /// (`wasm/env/presentacion.rs`, `sys_puntero`):
90 ///
91 /// - **El huésped nunca conoce la posición absoluta.** Lo que viaja es
92 /// local a su tablado, ya traducido.
93 /// - **Sólo viajan los eventos que caen adentro.** Un clic sobre el chrome
94 /// del anfitrión, sobre otra pestaña o sobre la barra no llega. Lo decide
95 /// la geometría del anfitrión, no una comprobación del huésped: si fuera
96 /// una comprobación, sería opcional.
97 ///
98 /// Del lado de Linux las dos salen gratis: el compositor ya entrega las
99 /// coordenadas **locales al nodo** (`View::on_click_at`) y sólo llama al
100 /// handler del nodo que fue golpeado. Va en `f32` y no en los `u16` del
101 /// kernel porque en Llimphi las coordenadas son `f32` en todos lados —
102 /// redondear acá perdería el subpíxel del escalado fraccionario.
103 Puntero { x: f32, y: f32, apretado: bool },
104 /// Una tecla. El huésped la procesa con su propio `on_key`: el cable la
105 /// lleva, no la interpreta.
106 Tecla(KeyEvent),
107 /// Texto compuesto (acentos por dead key, CJK, emoji). Viaja aparte de
108 /// [`DelAnfitrion::Tecla`] porque es otro flujo —Enabled → Preedit* →
109 /// Commit— y aplanarlo perdería la preedición, que es lo que se pinta
110 /// mientras el usuario compone.
111 Ime(ImeEvent),
112 /// La rueda, en líneas lógicas. Convención CSS: positivo = hacia abajo.
113 Rueda(WheelDelta),
114 /// El puntero salió del tablado.
115 ///
116 /// Existe porque su ausencia es un defecto con nombre: sin esto, lo último
117 /// que el huésped sabe del puntero es dónde estaba cuando se fue, y deja
118 /// una fila resaltada para siempre. El anfitrión es el único que puede
119 /// saberlo — el huésped no ve los eventos de afuera, justamente.
120 PunteroSalio,
121 /// El tablado cambió de tamaño.
122 Tamano { ancho: f32, alto: f32 },
123 /// El anfitrión se va. El huésped debería salir en vez de quedarse pintando
124 /// contra un caño muerto.
125 Adios,
126 /// **Andá a este lugar.** La ruta como texto, porque el cable no conoce
127 /// `PathBuf` ni tiene por qué.
128 ///
129 /// Existe porque el anfitrión suele tener su propio navegador al lado del
130 /// tablado —en pata, el árbol de carpetas del sidebar— y sin esto ese árbol
131 /// es decoración: el clic mueve el estado del anfitrión y el huésped, que es
132 /// el que se ve, se queda donde estaba. Los otros eventos son de **entrada
133 /// cruda** (dónde está el dedo, qué tecla); éste es el único que dice una
134 /// intención, y va aparte justamente por eso: mandarlo como una tecla
135 /// sintética sería inventarle al huésped un atajo que no existe.
136 Ir(String),
137 /// **Montá este dispositivo.** El id del nodo de `DispositivosSource`, como
138 /// texto: un disco no es una carpeta, es una fuente sintética con su propio
139 /// espacio de ids, así que [`Self::Ir`] —que habla rutas POSIX— no lo puede
140 /// expresar.
141 ///
142 /// Existe por lo mismo que `Ir`: el anfitrión pinta al lado su propia
143 /// sección de Dispositivos, y sin esto esas filas son decoración.
144 AbrirDispositivo(String),
145 /// **Abrí esta colección.** El id del qutu (un ULID), como texto. Mismo
146 /// motivo que [`Self::AbrirDispositivo`]: una colección tampoco es una
147 /// carpeta.
148 AbrirColeccion(String),
149}
150
151/// Escribe un mensaje enmarcado.
152pub fn escribir<T: Serialize, W: Write>(destino: &mut W, msg: &T) -> io::Result<()> {
153 let cuerpo = postcard::to_allocvec(msg)
154 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
155 if cuerpo.len() > MAX_MARCO {
156 return Err(io::Error::new(
157 io::ErrorKind::InvalidData,
158 format!("marco de {} bytes, tope {MAX_MARCO}", cuerpo.len()),
159 ));
160 }
161 destino.write_all(&(cuerpo.len() as u32).to_le_bytes())?;
162 destino.write_all(&cuerpo)?;
163 destino.flush()
164}
165
166/// Lee un mensaje enmarcado. Bloquea hasta tenerlo **entero**.
167///
168/// Un marco incompleto es un error y no un mensaje a medias: ver la doc del
169/// módulo. `UnexpectedEof` limpio significa que el otro lado cerró.
170pub fn leer<T: for<'de> Deserialize<'de>, R: Read>(origen: &mut R) -> io::Result<T> {
171 let mut largo = [0u8; 4];
172 origen.read_exact(&mut largo)?;
173 let largo = u32::from_le_bytes(largo) as usize;
174 if largo > MAX_MARCO {
175 return Err(io::Error::new(
176 io::ErrorKind::InvalidData,
177 format!("marco de {largo} bytes, tope {MAX_MARCO}"),
178 ));
179 }
180 let mut cuerpo = vec![0u8; largo];
181 origen.read_exact(&mut cuerpo)?;
182 postcard::from_bytes(&cuerpo).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))
183}
184
185/// El extremo del **huésped**: manda cuadros, pide medidas, recibe eventos.
186///
187/// Genérico sobre el flujo para que los tests corran sobre un par de sockets sin
188/// procesos ni archivos — y para que mañana el caño pueda ser otra cosa sin
189/// tocar esta lógica.
190pub struct Huesped<S: Read + Write> {
191 flujo: S,
192 /// Lo que llegó mientras se esperaba una medida. Ver la doc del módulo.
193 pendientes: VecDeque<DelAnfitrion>,
194 /// Cuántas medidas se pidieron. Es el número que el SDD manda medir antes de
195 /// creerle nada al diseño: si un cuadro pide muchas, el trabajo no es
196 /// acelerar el viaje sino cachear.
197 medidas_pedidas: u64,
198}
199
200impl<S: Read + Write> Huesped<S> {
201 pub fn new(flujo: S) -> Self {
202 Self { flujo, pendientes: VecDeque::new(), medidas_pedidas: 0 }
203 }
204
205 /// Cuántas medidas pidió en toda su vida.
206 pub fn medidas_pedidas(&self) -> u64 {
207 self.medidas_pedidas
208 }
209
210 /// Manda un fotograma.
211 pub fn enviar_cuadro(&mut self, escena: &EscenaWire) -> io::Result<()> {
212 escribir(&mut self.flujo, &DelHuesped::Cuadro(Box::new(escena.clone())))
213 }
214
215 /// Pide una medida y **espera** la respuesta, encolando lo que llegue en el
216 /// medio. Ver «El orden» en la doc del módulo.
217 pub fn medir(&mut self, peticion: &PeticionMedida) -> io::Result<MedidaTexto> {
218 self.medidas_pedidas += 1;
219 escribir(&mut self.flujo, &DelHuesped::PedirMedida(Box::new(peticion.clone())))?;
220 loop {
221 match leer::<DelAnfitrion, _>(&mut self.flujo)? {
222 DelAnfitrion::Medida(b) => return Ok(MedidaTexto::de_bytes(&b)),
223 otro => self.pendientes.push_back(otro),
224 }
225 }
226 }
227
228 /// Los eventos que se encolaron mientras se esperaban medidas.
229 ///
230 /// **No lee del caño**: drena lo ya recibido. Leer acá bloquearía el bucle
231 /// del huésped esperando un evento que puede no venir nunca — un asiento
232 /// quieto no manda nada.
233 pub fn eventos(&mut self) -> impl Iterator<Item = DelAnfitrion> + '_ {
234 std::iter::from_fn(move || self.pendientes.pop_front())
235 }
236
237 /// Espera **un** evento del anfitrión, bloqueando. Para el arranque, donde el
238 /// huésped necesita saber su tamaño antes de componer el primer cuadro.
239 pub fn esperar_evento(&mut self) -> io::Result<DelAnfitrion> {
240 if let Some(e) = self.pendientes.pop_front() {
241 return Ok(e);
242 }
243 leer(&mut self.flujo)
244 }
245}
246
247/// El extremo del **anfitrión**: recibe cuadros y pedidos, contesta medidas.
248pub struct Anfitrion<S: Read + Write> {
249 flujo: S,
250}
251
252impl<S: Read + Write> Anfitrion<S> {
253 pub fn new(flujo: S) -> Self {
254 Self { flujo }
255 }
256
257 /// Lee lo próximo que mande el huésped. Bloquea.
258 pub fn recibir(&mut self) -> io::Result<DelHuesped> {
259 leer(&mut self.flujo)
260 }
261
262 /// Contesta una medida.
263 pub fn responder_medida(&mut self, m: MedidaTexto) -> io::Result<()> {
264 escribir(&mut self.flujo, &DelAnfitrion::Medida(m.a_bytes()))
265 }
266
267 /// Atiende **un** mensaje del huésped: guarda el cuadro o contesta la medida.
268 ///
269 /// `medir` es del llamador porque medir necesita un `Typesetter` y este crate
270 /// no dibuja ni mide — la misma línea que separa al reproductor del cable. Es
271 /// la simetría de `llimphi_jaula::medidor::medir_con` del otro lado: el
272 /// marshalling es común, el cruce lo pone quien lo tiene.
273 ///
274 /// **El que contesta tiene que ser el que pinta.** Si el anfitrión midiera
275 /// con un typesetter distinto del que va a shapear, el texto se mediría con
276 /// un juego de fuentes y se pintaría con otro — y eso no mueve el glifo,
277 /// mueve la caja de línea (`SDD-JAULA-LINUX.md`).
278 pub fn atender_uno<F>(&mut self, mut medir: F) -> io::Result<Atendido>
279 where
280 F: FnMut(&PeticionMedida) -> MedidaTexto,
281 {
282 match self.recibir()? {
283 DelHuesped::Cuadro(e) => Ok(Atendido::Cuadro(e)),
284 DelHuesped::PedirMedida(p) => {
285 let m = medir(&p);
286 self.responder_medida(m)?;
287 Ok(Atendido::Medida)
288 }
289 }
290 }
291
292 /// Manda un evento (puntero, tamaño, adiós).
293 pub fn enviar(&mut self, e: &DelAnfitrion) -> io::Result<()> {
294 escribir(&mut self.flujo, e)
295 }
296
297 /// Manda un clic **ya local al tablado**.
298 ///
299 /// Los `(x, y)` son los que `View::on_click_at` entrega: relativos al nodo.
300 /// El anfitrión no tiene que restar nada ni saber dónde está su cuerpo
301 /// dentro de la surface — que es justo la trampa que `SDD-IDE.md` anota para
302 /// la rueda («shuma hospedada en el cajón de pata no sabe el origen de su
303 /// cuerpo»): acá no aparece porque el compositor ya resolvió el origen.
304 pub fn enviar_clic(&mut self, x: f32, y: f32) -> io::Result<()> {
305 self.enviar(&DelAnfitrion::Puntero { x, y, apretado: true })
306 }
307
308 /// Manda un movimiento del puntero, local al tablado.
309 pub fn enviar_movimiento(&mut self, x: f32, y: f32) -> io::Result<()> {
310 self.enviar(&DelAnfitrion::Puntero { x, y, apretado: false })
311 }
312
313 /// Avisa que el puntero se fue. Ver [`DelAnfitrion::PunteroSalio`]: sin esto
314 /// el huésped deja una fila resaltada para siempre.
315 pub fn enviar_salida(&mut self) -> io::Result<()> {
316 self.enviar(&DelAnfitrion::PunteroSalio)
317 }
318
319 pub fn enviar_tecla(&mut self, e: KeyEvent) -> io::Result<()> {
320 self.enviar(&DelAnfitrion::Tecla(e))
321 }
322
323 pub fn enviar_ime(&mut self, e: ImeEvent) -> io::Result<()> {
324 self.enviar(&DelAnfitrion::Ime(e))
325 }
326
327 pub fn enviar_rueda(&mut self, d: WheelDelta) -> io::Result<()> {
328 self.enviar(&DelAnfitrion::Rueda(d))
329 }
330
331 /// Avisa el tamaño del tablado. Se manda **sólo cuando cambia**: el huésped
332 /// re-compone al recibirlo, y mandarlo por cuadro lo haría re-componer
333 /// siempre. Quien llama guarda el último — ver [`Tamanos`].
334 /// Le pide al huésped que se pare en `ruta`. Ver [`DelAnfitrion::Ir`].
335 pub fn enviar_ir(&mut self, ruta: &str) -> io::Result<()> {
336 self.enviar(&DelAnfitrion::Ir(ruta.to_string()))
337 }
338
339 /// Le pide al huésped que monte un dispositivo. Ver
340 /// [`DelAnfitrion::AbrirDispositivo`].
341 pub fn enviar_abrir_dispositivo(&mut self, id: &str) -> io::Result<()> {
342 self.enviar(&DelAnfitrion::AbrirDispositivo(id.to_string()))
343 }
344
345 /// Le pide al huésped que abra una colección. Ver
346 /// [`DelAnfitrion::AbrirColeccion`].
347 pub fn enviar_abrir_coleccion(&mut self, id: &str) -> io::Result<()> {
348 self.enviar(&DelAnfitrion::AbrirColeccion(id.to_string()))
349 }
350
351 pub fn enviar_tamano(&mut self, ancho: f32, alto: f32) -> io::Result<()> {
352 self.enviar(&DelAnfitrion::Tamano { ancho, alto })
353 }
354
355 /// Se despide. El huésped debería salir en vez de seguir pintando contra un
356 /// caño muerto.
357 pub fn despedirse(&mut self) -> io::Result<()> {
358 self.enviar(&DelAnfitrion::Adios)
359 }
360
361}
362
363/// Qué se atendió en una vuelta de [`Anfitrion::atender_uno`].
364#[derive(Debug)]
365pub enum Atendido {
366 /// Llegó un fotograma: el huésped terminó de componer.
367 Cuadro(Box<EscenaWire>),
368 /// Se contestó una medida. El huésped sigue componiendo.
369 Medida,
370}
371
372impl Anfitrion<std::os::unix::net::UnixStream> {
373 /// Atiende hasta que llegue un cuadro **o se acabe el plazo**.
374 ///
375 /// Es la respuesta práctica a la pregunta que abrió F4.bis: medir es síncrono
376 /// para el huésped, así que si el anfitrión contestara una medida por cuadro
377 /// de UI, un árbol con cincuenta etiquetas tardaría cincuenta cuadros en
378 /// aparecer. Acá se contestan **todas las que hagan falta** dentro de una
379 /// misma vuelta, y el primer fotograma sale enseguida.
380 ///
381 /// El plazo es lo que impide que eso se convierta en un cuelgue: un huésped
382 /// trabado —o uno malicioso que pide medidas para siempre— no puede quedarse
383 /// con el hilo que dibuja. Se vuelve con `Ok(None)` y el tablado sigue
384 /// mostrando lo último que tenía; a la vuelta siguiente se retoma donde
385 /// quedó, porque el socket no pierde nada.
386 ///
387 /// **`Ok(None)` no es un error y no se cuenta como muerte.** Distinguirlos es
388 /// justamente lo que evita matar a un huésped lento.
389 pub fn atender_hasta<F>(
390 &mut self,
391 plazo: core::time::Duration,
392 mut medir: F,
393 ) -> io::Result<Option<Box<EscenaWire>>>
394 where
395 F: FnMut(&PeticionMedida) -> MedidaTexto,
396 {
397 let hasta = std::time::Instant::now() + plazo;
398 loop {
399 let queda = hasta.saturating_duration_since(std::time::Instant::now());
400 if queda.is_zero() {
401 return Ok(None);
402 }
403 // El timeout se vuelve a poner en cada vuelta con lo que QUEDA: uno
404 // fijo por lectura dejaría que N medidas lentas sumaran N plazos.
405 self.flujo.set_read_timeout(Some(queda))?;
406 match self.atender_uno(&mut medir) {
407 Ok(Atendido::Cuadro(e)) => return Ok(Some(e)),
408 Ok(Atendido::Medida) => continue,
409 Err(e) if agotado(&e) => return Ok(None),
410 Err(e) => return Err(e),
411 }
412 }
413 }
414}
415
416/// ¿Este error es «se acabó el tiempo» y no «se rompió algo»?
417///
418/// Los dos nombres existen porque el sistema usa uno u otro según la
419/// configuración del socket, y tratar el plazo como una rotura mataría al
420/// huésped lento en vez de esperarlo.
421fn agotado(e: &io::Error) -> bool {
422 matches!(e.kind(), io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut)
423}
424
425/// Recuerda el último tamaño avisado, para no repetirlo.
426///
427/// Existe porque el anfitrión conoce su rectángulo **en cada pintada** y la
428/// tentación es avisarlo ahí: el huésped re-compone al recibir un tamaño, así
429/// que avisarlo por cuadro lo haría re-componer sesenta veces por segundo sin
430/// que nada haya cambiado. Comparar es de una línea; olvidarse cuesta el
431/// fotograma entero.
432#[derive(Debug, Clone, Copy, Default)]
433pub struct Tamanos {
434 ultimo: Option<(f32, f32)>,
435}
436
437impl Tamanos {
438 pub fn new() -> Self {
439 Self::default()
440 }
441
442 /// El último tamaño avisado, o `None` si **nunca se avisó ninguno**. Es el
443 /// dato que separa «el huésped no contesta» de «al huésped nadie le dijo de
444 /// qué tamaño es», que en pantalla se ven igual.
445 pub fn ultimo(&self) -> Option<(f32, f32)> {
446 self.ultimo
447 }
448
449 /// `true` si este tamaño hay que avisarlo (es el primero, o cambió).
450 pub fn cambio(&mut self, ancho: f32, alto: f32) -> bool {
451 // Comparación exacta a propósito: un layout que devuelve el mismo número
452 // devuelve EL MISMO número. Una tolerancia acá dejaría pasar cambios
453 // reales de subpíxel en pantallas con escalado fraccionario.
454 if self.ultimo == Some((ancho, alto)) {
455 return false;
456 }
457 self.ultimo = Some((ancho, alto));
458 true
459 }
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465 use std::os::unix::net::UnixStream;
466
467 fn peticion() -> PeticionMedida {
468 PeticionMedida {
469 cadena: "hola".into(),
470 estilo: Default::default(),
471 ancho_max: None,
472 }
473 }
474
475 fn medida(w: f32) -> MedidaTexto {
476 MedidaTexto { width: w, full_width: w, height: 10.0 }
477 }
478
479 /// El marco sobrevive el viaje: lo que entra es lo que sale.
480 #[test]
481 fn un_mensaje_va_y_vuelve_igual() {
482 let (a, b) = UnixStream::pair().unwrap();
483 let mut hue = Huesped::new(a);
484 let mut anf = Anfitrion::new(b);
485
486 hue.enviar_cuadro(&EscenaWire::new()).unwrap();
487 match anf.recibir().unwrap() {
488 DelHuesped::Cuadro(e) => assert_eq!(e.ordenes().len(), 0),
489 otro => panic!("llegó {otro:?}"),
490 }
491 }
492
493 /// **La razón de ser del encolado.** El anfitrión manda un evento justo
494 /// mientras el huésped espera una medida. Sin la cola, el huésped leería el
495 /// evento y lo interpretaría como la medida — tres `f32` sacados de un clic.
496 #[test]
497 fn un_evento_que_llega_mientras_se_espera_la_medida_no_se_pierde() {
498 let (a, b) = UnixStream::pair().unwrap();
499 let mut hue = Huesped::new(a);
500 let mut anf = Anfitrion::new(b);
501
502 // El anfitrión se adelanta: manda un evento ANTES de la respuesta.
503 anf.enviar(&DelAnfitrion::Puntero { x: 3.0, y: 4.0, apretado: true }).unwrap();
504 anf.responder_medida(medida(42.0)).unwrap();
505
506 let m = hue.medir(&peticion()).unwrap();
507 assert_eq!(m.width, 42.0, "la medida es LA medida, no el evento de antes");
508
509 let evs: Vec<_> = hue.eventos().collect();
510 assert_eq!(
511 evs,
512 vec![DelAnfitrion::Puntero { x: 3.0, y: 4.0, apretado: true }],
513 "y el evento sigue ahí, en orden"
514 );
515 }
516
517 /// Brazo de control del anterior: sin nada en el medio, la cola queda vacía.
518 /// Sin esto, un `eventos()` que siempre devolviera algo pasaría el test de
519 /// arriba igual.
520 #[test]
521 fn sin_eventos_en_el_medio_la_cola_queda_vacia() {
522 let (a, b) = UnixStream::pair().unwrap();
523 let mut hue = Huesped::new(a);
524 let mut anf = Anfitrion::new(b);
525
526 anf.responder_medida(medida(7.0)).unwrap();
527 assert_eq!(hue.medir(&peticion()).unwrap().width, 7.0);
528 assert_eq!(hue.eventos().count(), 0);
529 }
530
531 /// Varios eventos en el medio conservan su orden, y el segundo pedido de
532 /// medida no se come los que quedaron.
533 #[test]
534 fn dos_medidas_seguidas_conservan_los_eventos_de_las_dos() {
535 let (a, b) = UnixStream::pair().unwrap();
536 let mut hue = Huesped::new(a);
537 let mut anf = Anfitrion::new(b);
538
539 anf.enviar(&DelAnfitrion::Tamano { ancho: 100.0, alto: 50.0 }).unwrap();
540 anf.responder_medida(medida(1.0)).unwrap();
541 anf.enviar(&DelAnfitrion::Puntero { x: 1.0, y: 1.0, apretado: false }).unwrap();
542 anf.responder_medida(medida(2.0)).unwrap();
543
544 assert_eq!(hue.medir(&peticion()).unwrap().width, 1.0);
545 assert_eq!(hue.medir(&peticion()).unwrap().width, 2.0);
546 let evs: Vec<_> = hue.eventos().collect();
547 assert_eq!(evs.len(), 2, "los dos eventos, ninguno perdido");
548 assert!(matches!(evs[0], DelAnfitrion::Tamano { .. }), "y en orden de llegada");
549 assert!(matches!(evs[1], DelAnfitrion::Puntero { .. }));
550 }
551
552 /// `eventos()` **no lee del caño**: si leyera, el huésped se colgaría
553 /// esperando un evento que un asiento quieto nunca manda.
554 #[test]
555 fn drenar_eventos_no_bloquea_con_el_cano_vacio() {
556 let (a, _b) = UnixStream::pair().unwrap();
557 let mut hue = Huesped::new(a);
558 assert_eq!(hue.eventos().count(), 0);
559 }
560
561 /// El contador es el instrumento que el SDD manda mirar antes de creerle
562 /// nada al diseño.
563 #[test]
564 fn las_medidas_se_cuentan() {
565 let (a, b) = UnixStream::pair().unwrap();
566 let mut hue = Huesped::new(a);
567 let mut anf = Anfitrion::new(b);
568 assert_eq!(hue.medidas_pedidas(), 0);
569 anf.responder_medida(medida(1.0)).unwrap();
570 anf.responder_medida(medida(1.0)).unwrap();
571 hue.medir(&peticion()).unwrap();
572 hue.medir(&peticion()).unwrap();
573 assert_eq!(hue.medidas_pedidas(), 2);
574 }
575
576 /// Una tecla viaja **entera**: el carácter, el estado, el texto resultante y
577 /// los modificadores. Es lo que hace que un `Ctrl+S` adentro de la jaula sea
578 /// el mismo `Ctrl+S` que afuera.
579 #[test]
580 fn una_tecla_llega_con_todo_lo_que_lleva() {
581 use llimphi_entrada::{Key, KeyState, Modifiers, NamedKey};
582 let (a, b) = UnixStream::pair().unwrap();
583 let mut hue = Huesped::new(a);
584 let mut anf = Anfitrion::new(b);
585
586 let tecla = KeyEvent {
587 key: Key::Character("s".into()),
588 state: KeyState::Pressed,
589 text: Some("s".into()),
590 modifiers: Modifiers { ctrl: true, ..Default::default() },
591 repeat: false,
592 };
593 anf.enviar_tecla(tecla.clone()).unwrap();
594 match hue.esperar_evento().unwrap() {
595 DelAnfitrion::Tecla(t) => {
596 assert_eq!(t, tecla, "ni el texto ni los modificadores se pierden");
597 assert!(t.modifiers.ctrl, "y el Ctrl sigue puesto del otro lado");
598 }
599 otro => panic!("llegó {otro:?}"),
600 }
601
602 // Y una tecla con nombre, que es el otro brazo del enum.
603 let flecha = KeyEvent {
604 key: Key::Named(NamedKey::ArrowDown),
605 state: KeyState::Pressed,
606 text: None,
607 modifiers: Modifiers::default(),
608 repeat: true,
609 };
610 anf.enviar_tecla(flecha.clone()).unwrap();
611 match hue.esperar_evento().unwrap() {
612 DelAnfitrion::Tecla(t) => assert_eq!(t, flecha),
613 otro => panic!("llegó {otro:?}"),
614 }
615 }
616
617 /// El texto compuesto va por su propio carril: aplanarlo contra
618 /// [`DelAnfitrion::Tecla`] perdería la preedición, que es lo que se pinta
619 /// mientras el usuario todavía está componiendo.
620 #[test]
621 fn la_preedicion_del_ime_sobrevive_el_viaje() {
622 let (a, b) = UnixStream::pair().unwrap();
623 let mut hue = Huesped::new(a);
624 let mut anf = Anfitrion::new(b);
625
626 let pre = ImeEvent::Preedit { text: "á".into(), cursor: Some((0, 2)) };
627 anf.enviar_ime(pre.clone()).unwrap();
628 assert_eq!(hue.esperar_evento().unwrap(), DelAnfitrion::Ime(pre));
629
630 anf.enviar_ime(ImeEvent::Commit("á".into())).unwrap();
631 assert_eq!(
632 hue.esperar_evento().unwrap(),
633 DelAnfitrion::Ime(ImeEvent::Commit("á".into())),
634 "y el commit llega distinguible de la preedición"
635 );
636 }
637
638 /// El clic viaja con las coordenadas **tal como las da el compositor**:
639 /// locales al nodo. El anfitrión no resta nada.
640 #[test]
641 fn el_clic_viaja_local_y_apretado() {
642 let (a, b) = UnixStream::pair().unwrap();
643 let mut hue = Huesped::new(a);
644 let mut anf = Anfitrion::new(b);
645
646 anf.enviar_clic(12.5, 4.0).unwrap();
647 assert_eq!(
648 hue.esperar_evento().unwrap(),
649 DelAnfitrion::Puntero { x: 12.5, y: 4.0, apretado: true }
650 );
651 // Y el movimiento es el mismo evento sin apretar: un huésped que quiera
652 // hover no necesita otro canal.
653 anf.enviar_movimiento(1.0, 2.0).unwrap();
654 assert_eq!(
655 hue.esperar_evento().unwrap(),
656 DelAnfitrion::Puntero { x: 1.0, y: 2.0, apretado: false }
657 );
658 }
659
660 /// **La salida del puntero es un evento, no la ausencia de eventos.** Sin
661 /// esto, lo último que el huésped sabe es dónde estaba el puntero cuando se
662 /// fue, y deja una fila resaltada para siempre.
663 #[test]
664 fn la_salida_del_puntero_se_avisa() {
665 let (a, b) = UnixStream::pair().unwrap();
666 let mut hue = Huesped::new(a);
667 let mut anf = Anfitrion::new(b);
668 anf.enviar_salida().unwrap();
669 assert_eq!(hue.esperar_evento().unwrap(), DelAnfitrion::PunteroSalio);
670 }
671
672 /// El tamaño se avisa cuando cambia, y **sólo** cuando cambia: el huésped
673 /// re-compone al recibirlo.
674 #[test]
675 fn el_tamano_solo_se_avisa_cuando_cambia() {
676 let mut t = Tamanos::new();
677 assert!(t.cambio(100.0, 50.0), "el primero siempre se avisa");
678 assert!(!t.cambio(100.0, 50.0), "el mismo, no");
679 assert!(!t.cambio(100.0, 50.0));
680 assert!(t.cambio(100.0, 51.0), "un píxel de alto SÍ es un cambio");
681 assert!(t.cambio(99.5, 51.0), "y medio píxel de ancho también");
682 }
683
684 /// Los eventos de entrada también se encolan si llegan mientras se espera
685 /// una medida. Es el mismo mecanismo que ya estaba, con el tipo nuevo: si
686 /// una tecla se colara como medida, el layout saldría de un scancode.
687 #[test]
688 fn una_tecla_en_el_medio_de_una_medida_tampoco_se_pierde() {
689 use llimphi_entrada::{Key, KeyState, Modifiers};
690 let (a, b) = UnixStream::pair().unwrap();
691 let mut hue = Huesped::new(a);
692 let mut anf = Anfitrion::new(b);
693
694 anf.enviar_tecla(KeyEvent {
695 key: Key::Character("x".into()),
696 state: KeyState::Pressed,
697 text: Some("x".into()),
698 modifiers: Modifiers::default(),
699 repeat: false,
700 })
701 .unwrap();
702 anf.responder_medida(medida(5.0)).unwrap();
703
704 assert_eq!(hue.medir(&peticion()).unwrap().width, 5.0);
705 let evs: Vec<_> = hue.eventos().collect();
706 assert_eq!(evs.len(), 1);
707 assert!(matches!(evs[0], DelAnfitrion::Tecla(_)));
708 }
709
710 /// **El primer fotograma sale en una vuelta, no en cincuenta.** El huésped
711 /// pide tres medidas y recién después manda el cuadro; el anfitrión contesta
712 /// las tres y devuelve el cuadro en la misma llamada.
713 #[test]
714 fn se_contestan_todas_las_medidas_que_hagan_falta_antes_del_cuadro() {
715 let (a, b) = UnixStream::pair().unwrap();
716 let mut anf = Anfitrion::new(b);
717
718 // El huésped, en su hilo: mide tres veces y después compone.
719 let h = std::thread::spawn(move || {
720 let mut hue = Huesped::new(a);
721 for _ in 0..3 {
722 hue.medir(&peticion()).unwrap();
723 }
724 hue.enviar_cuadro(&EscenaWire::new()).unwrap();
725 hue.medidas_pedidas()
726 });
727
728 let mut medidas = 0;
729 let cuadro = anf
730 .atender_hasta(std::time::Duration::from_secs(5), |_p| {
731 medidas += 1;
732 medida(10.0)
733 })
734 .unwrap();
735
736 assert!(cuadro.is_some(), "el cuadro llegó en la MISMA vuelta");
737 assert_eq!(medidas, 3, "y se contestaron las tres medidas de camino");
738 assert_eq!(h.join().unwrap(), 3);
739 }
740
741 /// **Un huésped trabado no se queda con el hilo que dibuja.** Pide medidas
742 /// para siempre y nunca manda cuadro; el anfitrión vuelve con `None` al
743 /// vencer el plazo, sin error y sin matarlo.
744 #[test]
745 fn un_huesped_que_no_manda_cuadro_devuelve_el_hilo_al_vencer_el_plazo() {
746 let (a, b) = UnixStream::pair().unwrap();
747 let mut anf = Anfitrion::new(b);
748
749 let (fin_tx, fin_rx) = std::sync::mpsc::channel::<()>();
750 let h = std::thread::spawn(move || {
751 let mut hue = Huesped::new(a);
752 // Mide hasta que el test le diga que pare. Nunca compone.
753 while fin_rx.try_recv().is_err() {
754 if hue.medir(&peticion()).is_err() {
755 break;
756 }
757 }
758 });
759
760 let arranque = std::time::Instant::now();
761 let cuadro = anf
762 .atender_hasta(std::time::Duration::from_millis(150), |_p| medida(1.0))
763 .expect("vencer el plazo NO es un error");
764 assert!(cuadro.is_none(), "no hubo cuadro, y eso está bien");
765 assert!(
766 arranque.elapsed() < std::time::Duration::from_secs(2),
767 "y volvió cerca del plazo, no cuando al huésped se le dio la gana"
768 );
769
770 let _ = fin_tx.send(());
771 drop(anf);
772 let _ = h.join();
773 }
774
775 /// Con el caño quieto y nadie del otro lado, el plazo vence igual: no se
776 /// cuelga esperando a un huésped que no va a hablar.
777 #[test]
778 fn con_el_cano_mudo_el_plazo_vence_solo() {
779 let (a, b) = UnixStream::pair().unwrap();
780 let _vivo = a; // se sostiene: cerrarlo daría EOF, que es otra cosa
781 let mut anf = Anfitrion::new(b);
782 let r = anf.atender_hasta(std::time::Duration::from_millis(80), |_p| medida(1.0));
783 assert!(matches!(r, Ok(None)), "plazo vencido, no error: dio {r:?}");
784 }
785
786 /// **Brazo de control del anterior**: si el otro lado CIERRA, eso sí es una
787 /// muerte y tiene que distinguirse de un plazo vencido. Sin este test, un
788 /// `atender_hasta` que devolviera `Ok(None)` ante cualquier error dejaría al
789 /// anfitrión esperando para siempre a un huésped que ya no está.
790 #[test]
791 fn el_cierre_no_se_confunde_con_un_plazo_vencido() {
792 let (a, b) = UnixStream::pair().unwrap();
793 drop(a);
794 let mut anf = Anfitrion::new(b);
795 let e = anf
796 .atender_hasta(std::time::Duration::from_secs(5), |_p| medida(1.0))
797 .expect_err("el otro lado cerró: eso es una muerte");
798 assert_eq!(e.kind(), io::ErrorKind::UnexpectedEof);
799 }
800
801 /// `atender_uno` contesta la medida con lo que el llamador diga, y le pasa
802 /// la petición entera — no un resumen.
803 #[test]
804 fn atender_uno_contesta_con_lo_que_el_anfitrion_mide() {
805 let (a, b) = UnixStream::pair().unwrap();
806 let mut anf = Anfitrion::new(b);
807 let h = std::thread::spawn(move || {
808 let mut hue = Huesped::new(a);
809 hue.medir(&peticion()).unwrap()
810 });
811 let mut vista = None;
812 match anf.atender_uno(|p| {
813 vista = Some(p.cadena.clone());
814 medida(33.0)
815 }) {
816 Ok(Atendido::Medida) => {}
817 otro => panic!("esperaba una medida, dio {otro:?}"),
818 }
819 assert_eq!(vista.as_deref(), Some("hola"), "la petición llega entera");
820 assert_eq!(h.join().unwrap().width, 33.0);
821 }
822
823 /// Un largo de basura no puede hacer alocar 4 GiB.
824 #[test]
825 fn un_largo_imposible_es_error_y_no_una_alocacion() {
826 let mut basura: &[u8] = &u32::MAX.to_le_bytes();
827 let r = leer::<DelAnfitrion, _>(&mut basura);
828 let e = r.expect_err("un marco de 4 GiB tiene que rebotar");
829 assert_eq!(e.kind(), io::ErrorKind::InvalidData);
830 }
831
832 /// Un marco cortado a la mitad es un error, no medio mensaje.
833 #[test]
834 fn un_marco_incompleto_no_entrega_nada() {
835 let mut cortado: &[u8] = &[10, 0, 0, 0, 1, 2, 3];
836 let r = leer::<DelAnfitrion, _>(&mut cortado);
837 assert!(r.is_err(), "faltan bytes: no hay mensaje");
838 }
839
840 /// El otro lado cerró: se distingue de un marco roto.
841 #[test]
842 fn el_cierre_del_otro_lado_se_reconoce() {
843 let (a, b) = UnixStream::pair().unwrap();
844 drop(b);
845 let mut anf = Anfitrion::new(a);
846 let e = anf.recibir().expect_err("el otro lado cerró");
847 assert_eq!(e.kind(), io::ErrorKind::UnexpectedEof);
848 }
849}