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//! Aislamiento del desborde de svelte.dev (salud_web +366px): un ítem de grid
2//! con `width: percent(1.0)` ENVENENA el track sizing de taffy 0.9.2 — el
3//! porcentaje se resuelve contra el CONTENEDOR (el área del ítem todavía no
4//! existe) y el ítem aporta el ancho entero del grid como contribución, así
5//! que su track se come todo el espacio y las demás columnas colapsan a 0.
6//! En CSS, un porcentaje en pleno sizing intrínseco se comporta como `auto`.
7//!
8//! El wire lo esquiva en `puriy-render::box_style`: ítem de grid con
9//! `width:100%` de autor → `auto` (equivale al stretch default del ítem).
10//!
11//! Los tests SIN porcentaje demuestran que el resto (placement auto, áreas
12//! nombradas, spans, fr tracks, medición min/max-content) anda bien — no
13//! tocar esos caminos para "arreglar" esto.
14
15use llimphi_layout::taffy::{self, prelude::*, AvailableSpace};
16
17/// Medidor de "texto": min-content = palabra más ancha, max-content = línea
18/// entera; el alto crece al encoger el ancho (aprox).
19fn measure_texto(
20 min_w: f32,
21 max_w: f32,
22 known: taffy::Size<Option<f32>>,
23 available: taffy::Size<AvailableSpace>,
24) -> taffy::Size<f32> {
25 let alto = 19.0;
26 let w = known.width.unwrap_or(match available.width {
27 AvailableSpace::Definite(w) => w.clamp(min_w, max_w),
28 AvailableSpace::MaxContent => max_w,
29 AvailableSpace::MinContent => min_w,
30 });
31 let lineas = (max_w / w.max(1.0)).ceil();
32 taffy::Size { width: w, height: alto * lineas }
33}
34
35fn medir(tree: &mut TaffyTree<(f32, f32)>, raiz: NodeId, ancho: AvailableSpace) {
36 tree.compute_layout_with_measure(
37 raiz,
38 taffy::Size { width: ancho, height: AvailableSpace::MaxContent },
39 |known, avail, _n, ctx, _st| match ctx {
40 Some(&mut (mn, mx)) => measure_texto(mn, mx, known, avail),
41 None => taffy::Size::ZERO,
42 },
43 )
44 .unwrap();
45}
46
47fn areas_svelte() -> Vec<taffy::GridTemplateArea<String>> {
48 // "definition definition" / "blurb screenshots" — líneas 1-based, fin
49 // EXCLUSIVO (la línea después de la última celda), como el wire.
50 vec![
51 taffy::GridTemplateArea { name: "definition".to_string(), row_start: 1, row_end: 2, column_start: 1, column_end: 3 },
52 taffy::GridTemplateArea { name: "blurb".to_string(), row_start: 2, row_end: 3, column_start: 1, column_end: 2 },
53 taffy::GridTemplateArea { name: "screenshots".to_string(), row_start: 2, row_end: 3, column_start: 2, column_end: 3 },
54 ]
55}
56
57fn estilo_grid() -> Style {
58 Style {
59 display: Display::Grid,
60 grid_template_columns: vec![fr(1.0), fr(1.0)],
61 gap: taffy::Size { width: length(64.0), height: length(0.0) },
62 size: taffy::Size { width: length(960.0), height: auto() },
63 ..Default::default()
64 }
65}
66
67#[test]
68fn grid_fr_min_content_no_es_max_content() {
69 // Grid `1fr 1fr` medido bajo MinContent: el ancho es la suma de los
70 // min-content de los ítems (+gap), no la de los max-content.
71 let mut tree: TaffyTree<(f32, f32)> = TaffyTree::new();
72 let a = tree.new_leaf_with_context(Style::default(), (212.0, 380.0)).unwrap();
73 let b = tree.new_leaf_with_context(Style::default(), (55.0, 635.0)).unwrap();
74 let grid = tree
75 .new_with_children(
76 Style {
77 display: Display::Grid,
78 grid_template_columns: vec![fr(1.0), fr(1.0)],
79 gap: taffy::Size { width: length(64.0), height: length(0.0) },
80 ..Default::default()
81 },
82 &[a, b],
83 )
84 .unwrap();
85 medir(&mut tree, grid, AvailableSpace::MinContent);
86 let w = tree.layout(grid).unwrap().size.width;
87 // 212 + 55 + 64 = 331.
88 assert!(w < 400.0, "min-content del grid debe dar ~331, dio {w}");
89}
90
91#[test]
92fn grid_fr_definite_item_que_abarca_no_desborda() {
93 // Ítem que abarca ambas columnas (líneas 1..3) con max-content ENORME:
94 // con available definido, ni el grid ni el ítem exceden el contenedor.
95 let mut tree: TaffyTree<(f32, f32)> = TaffyTree::new();
96 let abarca = tree
97 .new_leaf_with_context(
98 Style {
99 grid_column: taffy::geometry::Line { start: line(1), end: line(3) },
100 ..Default::default()
101 },
102 (212.0, 1249.0),
103 )
104 .unwrap();
105 let b = tree.new_leaf_with_context(Style::default(), (55.0, 635.0)).unwrap();
106 let c = tree.new_leaf_with_context(Style::default(), (10.0, 10.0)).unwrap();
107 let grid = tree.new_with_children(estilo_grid(), &[abarca, b, c]).unwrap();
108 medir(&mut tree, grid, AvailableSpace::Definite(960.0));
109 let w_grid = tree.layout(grid).unwrap().size.width;
110 let w_abarca = tree.layout(abarca).unwrap().size.width;
111 assert!(w_grid <= 961.0, "grid {w_grid} > 960");
112 assert!(w_abarca <= w_grid + 1.0, "el ítem que abarca ({w_abarca}) desborda el grid ({w_grid})");
113}
114
115#[test]
116fn grid_areas_nombradas_sin_percent_posicionan_bien() {
117 // Placement por NamedLine contra áreas: posiciones y anchos correctos
118 // mientras ningún ítem tenga width porcentual.
119 let nombrado = |n: &str| taffy::geometry::Line {
120 start: GridPlacement::<String>::NamedLine(n.to_string(), 1),
121 end: GridPlacement::<String>::NamedLine(n.to_string(), 1),
122 };
123 let mut tree: TaffyTree<(f32, f32)> = TaffyTree::new();
124 let def = tree
125 .new_leaf_with_context(
126 Style { grid_row: nombrado("definition"), grid_column: nombrado("definition"), ..Default::default() },
127 (212.0, 1249.0),
128 )
129 .unwrap();
130 let blurb = tree
131 .new_leaf_with_context(
132 Style { grid_row: nombrado("blurb"), grid_column: nombrado("blurb"), ..Default::default() },
133 (55.0, 635.0),
134 )
135 .unwrap();
136 let shots = tree
137 .new_leaf_with_context(
138 Style { grid_row: nombrado("screenshots"), grid_column: nombrado("screenshots"), ..Default::default() },
139 (40.0, 40.0),
140 )
141 .unwrap();
142 let mut estilo = estilo_grid();
143 estilo.grid_template_areas = areas_svelte();
144 let grid = tree.new_with_children(estilo, &[def, blurb, shots]).unwrap();
145 medir(&mut tree, grid, AvailableSpace::Definite(960.0));
146 let ld = tree.layout(def).unwrap();
147 let lb = tree.layout(blurb).unwrap();
148 let ls = tree.layout(shots).unwrap();
149 assert!(ld.size.width <= 961.0, "definition mide {} > 960", ld.size.width);
150 assert!(lb.location.x < 1.0, "blurb debe arrancar en col 1, x={}", lb.location.x);
151 assert!(ls.location.x + ls.size.width <= 961.0, "screenshots se sale: x={} w={}", ls.location.x, ls.size.width);
152 assert!(lb.location.y > 1.0 && ls.location.y > 1.0, "blurb/screenshots van en la fila 2");
153}
154
155/// EL BUG UPSTREAM (taffy 0.9.2): mismo grid pero el primer ítem lleva
156/// `width: percent(1.0)` — el % se resuelve contra el CONTENEDOR en pleno
157/// track sizing, el track 1 se come los 960 y el 2º ítem cae en x=1024,
158/// fuera del grid. Es el desborde de svelte.dev. El wire lo esquiva mapeando
159/// `width:100%` de un ítem de grid a `auto` (`box_style`). Si taffy lo
160/// arregla aguas arriba, des-ignorar y convertir en regresión.
161/// Con taffy 0.12 (bump 2026-07-27) este bug quedó ARREGLADO upstream y el
162/// test corre como REGRESIÓN: si vuelve a fallar, el track sizing volvió a
163/// resolver el % del ítem contra el contenedor (era el desborde de svelte).
164#[test]
165fn grid_item_percent_envenena_track_sizing() {
166 let mut tree: TaffyTree<(f32, f32)> = TaffyTree::new();
167 let a = tree
168 .new_leaf_with_context(
169 Style { size: taffy::Size { width: percent(1.0), height: auto() }, ..Default::default() },
170 (212.0, 380.0),
171 )
172 .unwrap();
173 let b = tree.new_leaf_with_context(Style::default(), (55.0, 635.0)).unwrap();
174 let c = tree.new_leaf_with_context(Style::default(), (0.0, 0.0)).unwrap();
175 let grid = tree.new_with_children(estilo_grid(), &[a, b, c]).unwrap();
176 medir(&mut tree, grid, AvailableSpace::Definite(960.0));
177 let lb = tree.layout(b).unwrap();
178 assert!(
179 lb.location.x + lb.size.width <= 961.0,
180 "b se sale del grid: x={} w={} (track 1 se comió el contenedor)",
181 lb.location.x,
182 lb.size.width
183 );
184}
185
186/// Flexbox §9.3: un contenedor COLUMN + Wrap con altura INDEFINIDA tiene la
187/// main size infinita → los ítems van todos en UNA línea (columna). taffy
188/// 0.9.2 envuelve contra la altura resuelta por contenido y manda el 2º ítem
189/// a una columna fantasma a la derecha (go.dev: `flex-flow:column wrap` del
190/// CSS mobile-first tiraba media sección a x=952, fuera del contenedor). El
191/// wire lo esquiva (`box_style`: column+wrap sin altura → NoWrap).
192#[test]
193#[ignore = "bug UPSTREAM taffy: column+wrap con altura auto envuelve contra la altura resuelta — vivo también en 0.12.2, reportado como DioxusLabs/taffy#999 (repro mínimo ahí: necesita anidamiento + flex-basis:100% + measure fn)"]
194fn column_wrap_altura_auto_no_envuelve() {
195 let mut tree: TaffyTree<(f32, f32)> = TaffyTree::new();
196 let a = tree
197 .new_leaf_with_context(
198 Style { flex_basis: percent(1.0), flex_shrink: 0.0, ..Default::default() },
199 (200.0, 200.0),
200 )
201 .unwrap();
202 let b = tree.new_leaf_with_context(Style::default(), (200.0, 200.0)).unwrap();
203 let cont = tree
204 .new_with_children(
205 Style {
206 display: Display::Flex,
207 flex_direction: FlexDirection::Column,
208 flex_wrap: FlexWrap::Wrap,
209 size: taffy::Size { width: length(976.0), height: auto() },
210 ..Default::default()
211 },
212 &[a, b],
213 )
214 .unwrap();
215 // El contenedor va DENTRO de otra columna (como en la página real): el
216 // pase de medición resuelve su altura por contenido y el arrange corre
217 // con esa altura ya conocida — ahí es donde taffy envuelve.
218 let padre = tree
219 .new_with_children(
220 Style {
221 display: Display::Flex,
222 flex_direction: FlexDirection::Column,
223 size: taffy::Size { width: length(976.0), height: auto() },
224 ..Default::default()
225 },
226 &[cont],
227 )
228 .unwrap();
229 tree.compute_layout_with_measure(
230 padre,
231 taffy::Size { width: AvailableSpace::Definite(976.0), height: AvailableSpace::MaxContent },
232 |known, avail, _n, ctx, _st| match ctx {
233 Some(&mut (mn, mx)) => measure_texto(mn, mx, known, avail),
234 None => taffy::Size::ZERO,
235 },
236 )
237 .unwrap();
238 let lb = tree.layout(b).unwrap();
239 assert!(
240 lb.location.x < 1.0,
241 "el 2º ítem debe APILARSE (columna única), no irse a x={}",
242 lb.location.x
243 );
244}