tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo (pin tawasuyu.git v0.3.0)
1//! **El lazo entero, con un proceso de verdad y sin ventana.**
2//!
3//! Las piezas estaban probadas por separado: el dibujo bit a bit, el marco del
4//! cable, las coordenadas locales, la vida del huésped. Ninguna de esas pruebas
5//! contesta la pregunta que importa — *si se juntan, ¿pasa algo?* — y esa es
6//! justo la clase de hueco donde este repo ya se cortó: cada mitad probada, la
7//! costura sin cubrir.
8//!
9//! Acá corre el huésped **real** (`llimphi-jaula`, ejemplo `huesped_demo`), se
10//! le contestan las medidas con el `Typesetter` de verdad, su escena se pinta en
11//! un tablado montado, y se mide qué salió.
12//!
13//! **Sin ventana**: esta máquina es ciega, y encima una ventana no probaría más.
14//! Lo que hay que saber —¿cruzó la escena? ¿se shapeó el texto? ¿el clic volvió?—
15//! son números, y los números se leen mejor que un pantallazo (regla 8).
16//!
17//! ```bash
18//! ./scripts/check-jaula-linux.sh
19//! ```
20//!
21//! Sin el binario del huésped el test **se saltea con el comando para
22//! generarlo**, en vez de fallar: vive en otro workspace a propósito —los
23//! backends de `llimphi-text` son excluyentes— y `cargo test` de acá no lo puede
24//! construir.
25
26use std::time::Duration;
27
28use llimphi_cable::vida::lanzar;
29use llimphi_cable::Anfitrion;
30use llimphi_compositor::llimphi_layout::taffy::prelude::{length, Rect as TRect, Size, Style};
31use llimphi_compositor::llimphi_layout::{ComputedLayout, LayoutTree};
32use llimphi_compositor::{hit_test_click, mount, paint, View};
33use llimphi_text::{Lienzo, Typesetter};
34use llimphi_widget_tablado::Tablado;
35
36const ANCHO: f32 = 400.0;
37const ALTO: f32 = 300.0;
38/// El tablado va **corrido**, para que las coordenadas locales no coincidan con
39/// las absolutas por casualidad.
40const ORIGEN: (f32, f32) = (30.0, 20.0);
41const CAJA: (f32, f32) = (320.0, 240.0);
42
43/// Dónde quedó el huésped. Vive en el `target` de SU workspace: está excluido
44/// del principal porque enciende `llimphi-text/wire`, excluyente con `vello`.
45fn binario_del_huesped() -> Option<std::path::PathBuf> {
46 // `CARGO_MANIFEST_DIR` es …/02_ruway/llimphi/widgets/tablado, así que la raíz
47 // del repo está CUATRO arriba. Estuvo en tres y el test se salteaba **en
48 // silencio informando `ok`** — la falla exacta contra la que existe
49 // `exigido()`.
50 let raiz = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
51 .ancestors()
52 .nth(4)?
53 .to_path_buf();
54 let p = raiz.join("02_ruway/llimphi/llimphi-jaula/target/debug/examples/huesped_demo");
55 p.exists().then_some(p)
56}
57
58/// ¿Alguien exige que este test corra de verdad?
59///
60/// Lo pone `scripts/check-jaula-linux.sh`, que compila el huésped justo antes.
61/// Existe porque saltearse **informando `ok`** es la peor manera de fallar: pasó
62/// acá mismo, con la ruta mal por un nivel, y el script imprimió que todo estaba
63/// bien. Un instrumento que no se puede encender no existe; uno que se apaga solo
64/// y dice que está prendido es peor.
65fn exigido() -> bool {
66 std::env::var_os("LLIMPHI_JAULA_REQUIRE").is_some()
67}
68
69struct Anfitriona {
70 tablado: Tablado,
71 anfitrion: Anfitrion<std::os::unix::net::UnixStream>,
72 hijo: std::process::Child,
73 typesetter: Typesetter,
74 medidas: u32,
75}
76
77impl Anfitriona {
78 fn abrir(binario: &std::path::Path) -> Self {
79 let (hijo, flujo) = lanzar("prueba-lazo", |ruta| {
80 let mut c = std::process::Command::new(binario);
81 c.arg(ruta);
82 c
83 })
84 .expect("lanzar el huésped");
85 Self {
86 tablado: Tablado::nuevo(),
87 anfitrion: Anfitrion::new(flujo),
88 hijo,
89 typesetter: Typesetter::new(),
90 medidas: 0,
91 }
92 }
93
94 /// Una vuelta: atiende medidas hasta que llegue el cuadro y lo guarda.
95 fn tomar_cuadro(&mut self) {
96 let ts = &mut self.typesetter;
97 let medidas = &mut self.medidas;
98 let cuadro = self
99 .anfitrion
100 .atender_hasta(Duration::from_secs(10), |p| {
101 *medidas += 1;
102 // **El que contesta es el que pinta**: el mismo typesetter que va
103 // a shapear. Medir con otro movería la caja de línea.
104 llimphi_reproductor::medir_texto(ts, &p.cadena, &p.estilo, p.ancho_max)
105 })
106 .expect("atender al huésped");
107 self.tablado.poner_cuadro(*cuadro.expect("el huésped tiene que mandar su cuadro"));
108 }
109
110 /// Monta el árbol del anfitrión con el tablado adentro y lo pinta de verdad.
111 /// Devuelve el `Mounted` para poder hacer hit-test como el bucle de eventos.
112 fn pintar(&self) -> (llimphi_compositor::Mounted<(f32, f32)>, ComputedLayout) {
113 let hijo = self
114 .tablado
115 .vista::<(f32, f32)>(Style {
116 size: Size { width: length(CAJA.0), height: length(CAJA.1) },
117 ..Default::default()
118 })
119 .on_click_at(|x, y, _w, _h| Some((x, y)));
120 let raiz = View::new(Style {
121 size: Size { width: length(ANCHO), height: length(ALTO) },
122 padding: TRect {
123 left: length(ORIGEN.0),
124 right: length(0.0),
125 top: length(ORIGEN.1),
126 bottom: length(0.0),
127 },
128 ..Default::default()
129 })
130 .children(vec![hijo]);
131
132 let mut arbol = LayoutTree::new();
133 let montado = mount(&mut arbol, raiz);
134 let computado = arbol.compute(montado.root, (ANCHO, ALTO)).expect("layout");
135 let mut lienzo = Lienzo::new();
136 let mut ts = Typesetter::new();
137 paint(&mut lienzo, &montado, &computado, &mut ts, None, None);
138 (montado, computado)
139 }
140}
141
142impl Drop for Anfitriona {
143 fn drop(&mut self) {
144 let _ = self.hijo.kill();
145 let _ = self.hijo.wait();
146 }
147}
148
149#[test]
150fn una_app_de_otro_proceso_cruza_entera_y_contesta_los_clics() {
151 let Some(binario) = binario_del_huesped() else {
152 assert!(
153 !exigido(),
154 "el huésped tendría que estar compilado y no está: el lazo NO se probó"
155 );
156 eprintln!("salteado: falta el huésped. Generalo con\n ./scripts/check-jaula-linux.sh");
157 return;
158 };
159 eprintln!("lazo · huésped: {}", binario.display());
160
161 let mut a = Anfitriona::abrir(&binario);
162
163 // ── 1) La escena del huésped cruza y se pinta ────────────────────────
164 a.tomar_cuadro();
165 let (montado, computado) = a.pintar();
166
167 let c = a.tablado.contadores();
168 assert!(c.ordenes() > 0, "el huésped compuso algo y se ejecutó de este lado");
169 assert!(
170 c.textos() > 0,
171 "y hay texto SHAPEADO acá: es lo que prueba que el reparto de W4 funciona \
172 — el huésped mandó cadenas, no glifos"
173 );
174 assert_eq!(c.salteadas(), 0, "nada vino mal formado");
175 assert_eq!(c.cuadros_pintados(), 1);
176 assert_eq!(c.pintadas_sin_escena(), 0, "hubo escena desde el primer cuadro");
177
178 // La evidencia, por texto: es lo que se mira cuando alguien pregunta si esto
179 // anda, y lo que delata un cable a medio conectar mucho antes que un
180 // pantallazo (un tablado gris se ve igual que uno con una escena vacía).
181 eprintln!(
182 "lazo · {} órdenes · {} textos shapeados acá · {} medidas preguntadas · {} salteadas",
183 c.ordenes(),
184 c.textos(),
185 a.medidas,
186 c.salteadas()
187 );
188
189 // ── 2) El medidor viajó de verdad ────────────────────────────────────
190 assert!(
191 a.medidas > 0,
192 "el huésped NO shapea: si no pidió ni una medida, o no hay texto o el \
193 cable de medidas está roto"
194 );
195
196 // ── 3) Un clic vuelve, en coordenadas del huésped ────────────────────
197 // Se golpea una fila de la lista: absoluto (60, 90) cae adentro del tablado,
198 // que empieza en (30, 20).
199 let (abs_x, abs_y) = (60.0, 90.0);
200 let i = hit_test_click(&montado, &computado, abs_x, abs_y).expect("el clic pega en el tablado");
201 let nodo = &montado.nodes[i];
202 let r = computado.get(nodo.id).expect("rect del nodo");
203 let local = (nodo.on_click_at.as_ref().expect("el tablado escucha clics"))(
204 abs_x - r.x,
205 abs_y - r.y,
206 r.w,
207 r.h,
208 )
209 .expect("el handler devuelve las coordenadas locales");
210 assert_eq!(local, (30.0, 70.0), "el huésped recibe SU (30,70), no el (60,90) de la ventana");
211
212 a.anfitrion.enviar_clic(local.0, local.1).expect("mandar el clic");
213
214 // ── 4) El huésped reaccionó: el cuadro siguiente es DISTINTO ─────────
215 // Es la prueba de que el evento llegó y se aplicó, y no que simplemente se
216 // mandó. La demo resalta la fila elegida y sube un contador visible, así que
217 // el conteo de órdenes o el de textos tiene que moverse… o, si el layout no
218 // cambia de forma, al menos tiene que haber un cuadro nuevo.
219 let ordenes_antes = c.ordenes();
220 a.tomar_cuadro();
221 let _ = a.pintar();
222 assert_eq!(a.tablado.contadores().cuadros_pintados(), 2, "llegó y se pintó otro cuadro");
223 eprintln!(
224 "lazo · tras el clic: {} órdenes · {} medidas acumuladas (el caché del huésped se nota acá)",
225 a.tablado.contadores().ordenes(),
226 a.medidas
227 );
228 assert!(
229 a.tablado.contadores().ordenes() > 0,
230 "y el cuadro nuevo también trae órdenes (no se vació al reaccionar)"
231 );
232 let _ = ordenes_antes;
233
234 // ── 5) La muerte se ve, y borra el cuadro ────────────────────────────
235 a.anfitrion.despedirse().expect("decirle que se vaya");
236 a.hijo.wait().expect("esperar al huésped");
237 a.tablado.olvidar();
238 assert!(!a.tablado.hay_cuadro(), "un proceso muerto no deja su interfaz pegada");
239 let (_, _) = a.pintar();
240 assert_eq!(
241 a.tablado.contadores().pintadas_sin_escena(),
242 1,
243 "y pintar sin huésped se cuenta aparte de pintar una escena vacía"
244 );
245}
246
247/// **El editor de verdad, enjaulado.**
248///
249/// El otro test usa el huésped mínimo, que ejercita la frontera. Éste corre
250/// `nada` entero —árbol de archivos, pestañas, editor, barra de estado— adentro
251/// de la jaula, que es el objetivo de todo el SDD. Lo que se mide es lo mismo,
252/// pero los números son de una app real: **cuántas órdenes tiene una interfaz de
253/// verdad y cuántas medidas cuesta**.
254///
255/// Es lo que convierte «el mecanismo anda» en «la app anda».
256#[test]
257fn nada_entero_corre_enjaulado_y_su_interfaz_cruza() {
258 let raiz = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
259 .ancestors()
260 .nth(4)
261 .expect("raíz del repo")
262 .to_path_buf();
263 let binario = raiz.join("02_ruway/llimphi/llimphi-jaula/target/debug/examples/nada_enjaulado");
264 if !binario.exists() {
265 assert!(!exigido(), "falta `nada` enjaulado y se está exigiendo el lazo");
266 eprintln!("salteado: falta nada_enjaulado. Generalo con\n ./scripts/check-jaula-linux.sh");
267 return;
268 }
269
270 let (hijo, flujo) = lanzar("nada-enjaulado", |ruta| {
271 let mut c = std::process::Command::new(&binario);
272 // Se le abre el propio crate del tablado: un árbol chico y real.
273 c.arg(ruta).arg(env!("CARGO_MANIFEST_DIR"));
274 c
275 })
276 .expect("lanzar nada enjaulado");
277
278 let mut a = Anfitriona {
279 tablado: Tablado::nuevo(),
280 anfitrion: Anfitrion::new(flujo),
281 hijo,
282 typesetter: Typesetter::new(),
283 medidas: 0,
284 };
285
286 a.tomar_cuadro();
287 let _ = a.pintar();
288 let c = a.tablado.contadores();
289
290 eprintln!(
291 "nada enjaulado · {} órdenes · {} textos shapeados acá · {} medidas · {} salteadas",
292 c.ordenes(),
293 c.textos(),
294 a.medidas,
295 c.salteadas()
296 );
297
298 assert!(c.ordenes() > 0, "el editor compuso su interfaz y cruzó entera");
299 assert!(
300 c.textos() > 0,
301 "y trae TEXTO: el árbol de archivos y la barra de estado son cadenas que \
302 se shapean de este lado"
303 );
304 assert_eq!(c.salteadas(), 0, "nada vino mal formado");
305 assert!(a.medidas > 0, "y el editor preguntó sus medidas: no shapea del otro lado");
306}
307
308/// **Brazo de control.** Sin huésped no hay nada que pintar, y eso se distingue
309/// de un huésped que manda una escena vacía. Sin este test, un tablado que
310/// ignorara la escena pasaría el de arriba si los contadores se movieran por
311/// cualquier otro motivo.
312#[test]
313fn sin_huesped_no_hay_ordenes_pero_si_intento_de_pintar() {
314 let t = Tablado::nuevo();
315 let vista = t.vista::<()>(Style {
316 size: Size { width: length(CAJA.0), height: length(CAJA.1) },
317 ..Default::default()
318 });
319 let mut arbol = LayoutTree::new();
320 let montado = mount(&mut arbol, vista);
321 let computado = arbol.compute(montado.root, (ANCHO, ALTO)).expect("layout");
322 let mut lienzo = Lienzo::new();
323 let mut ts = Typesetter::new();
324 paint(&mut lienzo, &montado, &computado, &mut ts, None, None);
325
326 assert_eq!(t.contadores().ordenes(), 0);
327 assert_eq!(t.contadores().cuadros_pintados(), 0);
328 assert_eq!(t.contadores().pintadas_sin_escena(), 1);
329}