tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros
1// =============================================================================
2// pruebas_wire :: que el encoder produzca una escena que sirva (W4)
3// -----------------------------------------------------------------------------
4// Que esto COMPILE ya prueba lo principal —`render.rs` no cambió una línea y sin
5// embargo emite hacia el IR de escena— pero compilar no es suficiente, y en este
6// crate menos que en ningún otro: con un medidor que devuelva ceros el layout se
7// colapsa entero y la escena sale «bien» con todas las cajas en el origen.
8//
9// Así que se certifica lo que puede salir mal en silencio:
10//
11// 1. Que el texto viaje SIN SHAPEAR (es W4 entero).
12// 2. Que se haya usado el medidor de verdad y no la estimación de emergencia.
13// 3. Que la posición que emite el paint sea la que el layout calculó.
14// 4. Que un subárbol rotado conserve su matriz en vez de aplanarse.
15// =============================================================================
16
17use alloc::vec;
18
19use llimphi_layout::taffy::prelude::*;
20use llimphi_layout::Style;
21use llimphi_text::peniko::Color;
22use llimphi_text::wire::{Medida, Peticion};
23use llimphi_wire_escena::{EscenaWire, OrdenWire, UbicacionWire};
24
25use crate::{Jaula, View};
26
27/// Ancho por carácter del medidor de prueba. Un número redondo y feo a
28/// propósito: si algún cálculo lo perdiera, la diferencia sería visible.
29const ANCHO_GLIFO: f32 = 10.0;
30
31#[derive(Clone, Debug, PartialEq)]
32enum Msg {
33 Nada,
34}
35
36/// Un medidor determinista que ocupa el lugar del `sys_` de wawa.
37///
38/// En producción esto pregunta del otro lado de la frontera y allá contesta
39/// parley. Acá contesta una fórmula, porque lo que este test certifica no es la
40/// fidelidad tipográfica —eso lo prueba el reproductor, que shapea de verdad—
41/// sino que el número que devuelve el medidor sea el que termina gobernando el
42/// layout y la posición emitida.
43fn medidor(p: &Peticion<'_>) -> Medida {
44 let chars = p.cadena.chars().count() as f32;
45 let ancho = chars * ANCHO_GLIFO;
46 Medida {
47 width: ancho,
48 full_width: ancho,
49 height: p.estilo.tamano_px * p.estilo.alto_linea,
50 }
51}
52
53fn pantalla(w: f32, h: f32) -> Style {
54 Style {
55 size: Size {
56 width: length(w),
57 height: length(h),
58 },
59 ..Default::default()
60 }
61}
62
63/// Corre un fotograma entero y devuelve la escena lista para el cable.
64fn componer(vista: View<Msg>, viewport: (f32, f32)) -> (EscenaWire, usize) {
65 let mut jaula = Jaula::new(viewport.0, viewport.1, medidor);
66 let _montado = jaula.fotograma(vista);
67 (jaula.escena().clone(), jaula.medidas_estimadas())
68}
69
70#[test]
71fn el_paint_emite_el_texto_sin_shapear() {
72 let vista: View<Msg> = View::new(pantalla(200.0, 60.0))
73 .fill(Color::from_rgba8(10, 10, 20, 255))
74 .children(vec![View::new(Style::default()).text(
75 "hola",
76 14.0,
77 Color::from_rgba8(255, 255, 255, 255),
78 )]);
79
80 let (escena, estimadas) = componer(vista, (200.0, 60.0));
81
82 // Que NO se haya estimado es tan importante como que haya texto: una
83 // estimación silenciosa querría decir que el medidor no se enchufó y que
84 // todo el layout de este test es ficción.
85 assert_eq!(
86 estimadas, 0,
87 "se midió por estimación — el medidor no se usó"
88 );
89
90 assert_eq!(escena.textos(), 1, "tiene que haber exactamente un texto");
91 let texto = escena
92 .ordenes()
93 .iter()
94 .find_map(|o| match o {
95 OrdenWire::Texto(t) => Some(t),
96 _ => None,
97 })
98 .expect("la orden de texto");
99
100 assert_eq!(texto.cadena, "hola");
101 assert_eq!(texto.color, [255, 255, 255, 255]);
102 // El estilo se internó y se puede resolver.
103 let estilo = escena.estilo(texto.estilo).expect("estilo en tabla");
104 assert_eq!(estilo.tamano_px, 14.0);
105
106 // Y el fondo se emitió como relleno, o sea que el resto del paint también
107 // pasó por el cable y no sólo el texto.
108 assert!(
109 escena
110 .ordenes()
111 .iter()
112 .any(|o| matches!(o, OrdenWire::Rellenar { .. })),
113 "falta el relleno del fondo"
114 );
115}
116
117#[test]
118fn la_medida_del_medidor_gobierna_el_layout_y_la_posicion() {
119 // Dos etiquetas en columna: la segunda tiene que caer al alto de la primera,
120 // que es el número que devolvió el medidor. Si el layout ignorara la medida
121 // —o si el paint emitiera coordenadas crudas— las dos se pisarían en y=0.
122 let alto_linea = 14.0 * 1.2;
123 let vista: View<Msg> = View::new(Style {
124 size: Size {
125 width: length(300.0),
126 height: length(100.0),
127 },
128 flex_direction: FlexDirection::Column,
129 ..Default::default()
130 })
131 .children(vec![
132 View::new(Style::default()).text("arriba", 14.0, Color::BLACK),
133 View::new(Style::default()).text("abajo", 14.0, Color::BLACK),
134 ]);
135
136 let (escena, estimadas) = componer(vista, (300.0, 100.0));
137 assert_eq!(estimadas, 0);
138
139 let ys: alloc::vec::Vec<f32> = escena
140 .ordenes()
141 .iter()
142 .filter_map(|o| match o {
143 OrdenWire::Texto(t) => Some(t.xf.traslacion().1),
144 _ => None,
145 })
146 .collect();
147
148 assert_eq!(ys.len(), 2, "dos textos");
149
150 // Las comparaciones van con holgura de un píxel, y la razón no es pereza:
151 // taffy REDONDEA el rect final a píxeles enteros, así que una línea de 16,8
152 // px queda en una caja de 17, y el centrado vertical del paint la baja 0,1.
153 // Esos 0,1 son comportamiento correcto —el mismo que produce el backend de
154 // vello— y exigir el cero exacto sería certificar un bug.
155 assert!(ys[0] < 1.0, "el primero arriba de todo, cayó a {}", ys[0]);
156 assert!(
157 (ys[1] - alto_linea).abs() <= 1.0,
158 "el segundo tenía que caer a ~{alto_linea}, cayó a {}",
159 ys[1]
160 );
161 // Lo que de verdad importa: el segundo NO se pisa con el primero. Si el
162 // layout hubiera ignorado la medida, los dos estarían en cero.
163 assert!(
164 ys[1] - ys[0] > alto_linea * 0.9,
165 "los dos textos se pisaron: {} y {}",
166 ys[0],
167 ys[1]
168 );
169}
170
171#[test]
172fn un_subarbol_rotado_conserva_su_matriz() {
173 // El caso que motivó que la ubicación del texto sea `UbicacionWire` y no un
174 // punto. Con un punto, esto se pintaría derecho y nada avisaría.
175 let vista: View<Msg> = View::new(pantalla(200.0, 200.0)).children(vec![View::new(
176 Style::default(),
177 )
178 .transform(llimphi_text::peniko::kurbo::Affine::rotate(0.4))
179 .children(vec![View::new(Style::default()).text(
180 "girado",
181 14.0,
182 Color::BLACK,
183 )])]);
184
185 let (escena, _) = componer(vista, (200.0, 200.0));
186 let texto = escena
187 .ordenes()
188 .iter()
189 .find_map(|o| match o {
190 OrdenWire::Texto(t) => Some(t),
191 _ => None,
192 })
193 .expect("la orden de texto");
194
195 assert!(
196 matches!(texto.xf, UbicacionWire::Afin(_)),
197 "un texto bajo una rotación tiene que viajar con la matriz entera, vino {:?}",
198 texto.xf
199 );
200}
201
202#[test]
203fn sin_medidor_se_estima_y_se_delata() {
204 // La red de seguridad: si alguien olvida enchufar el medidor, el layout es
205 // aproximado — pero el contador lo dice en vez de que la pantalla salga rara
206 // sin explicación.
207 let mut ts = llimphi_text::wire::Typesetter::new();
208 let ancho = llimphi_text::text_width_with(&mut ts, "hola", 14.0);
209 assert!(ancho > 0.0, "la estimación no puede devolver cero");
210 assert_eq!(ts.medidas_estimadas(), 1, "y tiene que quedar contada");
211}