tawasuyu

tawasuyu/llimphi

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

Rust 660Markdown 302TOML 157RON 312382 Definiciones

⎇ main widgets/tablado/tests/el_lazo_cerrado.rs Rust · 329 líneas · 13.0 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo (pin tawasuyu.git v0.3.0) b575c63f1 · Sergio ·

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}