tawasuyu

tawasuyu/llimphi

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

Rust 229TOML 101Markdown 82Shell 24052 Sutichaykuna

⎇ main llimphi-layout/tests/grid_min_content.rs Rust · 244 siq'ikuna · 10.6 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros d251eb32b · Sergio ·

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}