//! Aislamiento del desborde de svelte.dev (salud_web +366px): un ítem de grid //! con `width: percent(1.0)` ENVENENA el track sizing de taffy 0.9.2 — el //! porcentaje se resuelve contra el CONTENEDOR (el área del ítem todavía no //! existe) y el ítem aporta el ancho entero del grid como contribución, así //! que su track se come todo el espacio y las demás columnas colapsan a 0. //! En CSS, un porcentaje en pleno sizing intrínseco se comporta como `auto`. //! //! El wire lo esquiva en `puriy-render::box_style`: ítem de grid con //! `width:100%` de autor → `auto` (equivale al stretch default del ítem). //! //! Los tests SIN porcentaje demuestran que el resto (placement auto, áreas //! nombradas, spans, fr tracks, medición min/max-content) anda bien — no //! tocar esos caminos para "arreglar" esto. use llimphi_layout::taffy::{self, prelude::*, AvailableSpace}; /// Medidor de "texto": min-content = palabra más ancha, max-content = línea /// entera; el alto crece al encoger el ancho (aprox). fn measure_texto( min_w: f32, max_w: f32, known: taffy::Size>, available: taffy::Size, ) -> taffy::Size { let alto = 19.0; let w = known.width.unwrap_or(match available.width { AvailableSpace::Definite(w) => w.clamp(min_w, max_w), AvailableSpace::MaxContent => max_w, AvailableSpace::MinContent => min_w, }); let lineas = (max_w / w.max(1.0)).ceil(); taffy::Size { width: w, height: alto * lineas } } fn medir(tree: &mut TaffyTree<(f32, f32)>, raiz: NodeId, ancho: AvailableSpace) { tree.compute_layout_with_measure( raiz, taffy::Size { width: ancho, height: AvailableSpace::MaxContent }, |known, avail, _n, ctx, _st| match ctx { Some(&mut (mn, mx)) => measure_texto(mn, mx, known, avail), None => taffy::Size::ZERO, }, ) .unwrap(); } fn areas_svelte() -> Vec> { // "definition definition" / "blurb screenshots" — líneas 1-based, fin // EXCLUSIVO (la línea después de la última celda), como el wire. vec![ taffy::GridTemplateArea { name: "definition".to_string(), row_start: 1, row_end: 2, column_start: 1, column_end: 3 }, taffy::GridTemplateArea { name: "blurb".to_string(), row_start: 2, row_end: 3, column_start: 1, column_end: 2 }, taffy::GridTemplateArea { name: "screenshots".to_string(), row_start: 2, row_end: 3, column_start: 2, column_end: 3 }, ] } fn estilo_grid() -> Style { Style { display: Display::Grid, grid_template_columns: vec![fr(1.0), fr(1.0)], gap: taffy::Size { width: length(64.0), height: length(0.0) }, size: taffy::Size { width: length(960.0), height: auto() }, ..Default::default() } } #[test] fn grid_fr_min_content_no_es_max_content() { // Grid `1fr 1fr` medido bajo MinContent: el ancho es la suma de los // min-content de los ítems (+gap), no la de los max-content. let mut tree: TaffyTree<(f32, f32)> = TaffyTree::new(); let a = tree.new_leaf_with_context(Style::default(), (212.0, 380.0)).unwrap(); let b = tree.new_leaf_with_context(Style::default(), (55.0, 635.0)).unwrap(); let grid = tree .new_with_children( Style { display: Display::Grid, grid_template_columns: vec![fr(1.0), fr(1.0)], gap: taffy::Size { width: length(64.0), height: length(0.0) }, ..Default::default() }, &[a, b], ) .unwrap(); medir(&mut tree, grid, AvailableSpace::MinContent); let w = tree.layout(grid).unwrap().size.width; // 212 + 55 + 64 = 331. assert!(w < 400.0, "min-content del grid debe dar ~331, dio {w}"); } #[test] fn grid_fr_definite_item_que_abarca_no_desborda() { // Ítem que abarca ambas columnas (líneas 1..3) con max-content ENORME: // con available definido, ni el grid ni el ítem exceden el contenedor. let mut tree: TaffyTree<(f32, f32)> = TaffyTree::new(); let abarca = tree .new_leaf_with_context( Style { grid_column: taffy::geometry::Line { start: line(1), end: line(3) }, ..Default::default() }, (212.0, 1249.0), ) .unwrap(); let b = tree.new_leaf_with_context(Style::default(), (55.0, 635.0)).unwrap(); let c = tree.new_leaf_with_context(Style::default(), (10.0, 10.0)).unwrap(); let grid = tree.new_with_children(estilo_grid(), &[abarca, b, c]).unwrap(); medir(&mut tree, grid, AvailableSpace::Definite(960.0)); let w_grid = tree.layout(grid).unwrap().size.width; let w_abarca = tree.layout(abarca).unwrap().size.width; assert!(w_grid <= 961.0, "grid {w_grid} > 960"); assert!(w_abarca <= w_grid + 1.0, "el ítem que abarca ({w_abarca}) desborda el grid ({w_grid})"); } #[test] fn grid_areas_nombradas_sin_percent_posicionan_bien() { // Placement por NamedLine contra áreas: posiciones y anchos correctos // mientras ningún ítem tenga width porcentual. let nombrado = |n: &str| taffy::geometry::Line { start: GridPlacement::::NamedLine(n.to_string(), 1), end: GridPlacement::::NamedLine(n.to_string(), 1), }; let mut tree: TaffyTree<(f32, f32)> = TaffyTree::new(); let def = tree .new_leaf_with_context( Style { grid_row: nombrado("definition"), grid_column: nombrado("definition"), ..Default::default() }, (212.0, 1249.0), ) .unwrap(); let blurb = tree .new_leaf_with_context( Style { grid_row: nombrado("blurb"), grid_column: nombrado("blurb"), ..Default::default() }, (55.0, 635.0), ) .unwrap(); let shots = tree .new_leaf_with_context( Style { grid_row: nombrado("screenshots"), grid_column: nombrado("screenshots"), ..Default::default() }, (40.0, 40.0), ) .unwrap(); let mut estilo = estilo_grid(); estilo.grid_template_areas = areas_svelte(); let grid = tree.new_with_children(estilo, &[def, blurb, shots]).unwrap(); medir(&mut tree, grid, AvailableSpace::Definite(960.0)); let ld = tree.layout(def).unwrap(); let lb = tree.layout(blurb).unwrap(); let ls = tree.layout(shots).unwrap(); assert!(ld.size.width <= 961.0, "definition mide {} > 960", ld.size.width); assert!(lb.location.x < 1.0, "blurb debe arrancar en col 1, x={}", lb.location.x); assert!(ls.location.x + ls.size.width <= 961.0, "screenshots se sale: x={} w={}", ls.location.x, ls.size.width); assert!(lb.location.y > 1.0 && ls.location.y > 1.0, "blurb/screenshots van en la fila 2"); } /// EL BUG UPSTREAM (taffy 0.9.2): mismo grid pero el primer ítem lleva /// `width: percent(1.0)` — el % se resuelve contra el CONTENEDOR en pleno /// track sizing, el track 1 se come los 960 y el 2º ítem cae en x=1024, /// fuera del grid. Es el desborde de svelte.dev. El wire lo esquiva mapeando /// `width:100%` de un ítem de grid a `auto` (`box_style`). Si taffy lo /// arregla aguas arriba, des-ignorar y convertir en regresión. /// Con taffy 0.12 (bump 2026-07-27) este bug quedó ARREGLADO upstream y el /// test corre como REGRESIÓN: si vuelve a fallar, el track sizing volvió a /// resolver el % del ítem contra el contenedor (era el desborde de svelte). #[test] fn grid_item_percent_envenena_track_sizing() { let mut tree: TaffyTree<(f32, f32)> = TaffyTree::new(); let a = tree .new_leaf_with_context( Style { size: taffy::Size { width: percent(1.0), height: auto() }, ..Default::default() }, (212.0, 380.0), ) .unwrap(); let b = tree.new_leaf_with_context(Style::default(), (55.0, 635.0)).unwrap(); let c = tree.new_leaf_with_context(Style::default(), (0.0, 0.0)).unwrap(); let grid = tree.new_with_children(estilo_grid(), &[a, b, c]).unwrap(); medir(&mut tree, grid, AvailableSpace::Definite(960.0)); let lb = tree.layout(b).unwrap(); assert!( lb.location.x + lb.size.width <= 961.0, "b se sale del grid: x={} w={} (track 1 se comió el contenedor)", lb.location.x, lb.size.width ); } /// Flexbox §9.3: un contenedor COLUMN + Wrap con altura INDEFINIDA tiene la /// main size infinita → los ítems van todos en UNA línea (columna). taffy /// 0.9.2 envuelve contra la altura resuelta por contenido y manda el 2º ítem /// a una columna fantasma a la derecha (go.dev: `flex-flow:column wrap` del /// CSS mobile-first tiraba media sección a x=952, fuera del contenedor). El /// wire lo esquiva (`box_style`: column+wrap sin altura → NoWrap). #[test] #[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)"] fn column_wrap_altura_auto_no_envuelve() { let mut tree: TaffyTree<(f32, f32)> = TaffyTree::new(); let a = tree .new_leaf_with_context( Style { flex_basis: percent(1.0), flex_shrink: 0.0, ..Default::default() }, (200.0, 200.0), ) .unwrap(); let b = tree.new_leaf_with_context(Style::default(), (200.0, 200.0)).unwrap(); let cont = tree .new_with_children( Style { display: Display::Flex, flex_direction: FlexDirection::Column, flex_wrap: FlexWrap::Wrap, size: taffy::Size { width: length(976.0), height: auto() }, ..Default::default() }, &[a, b], ) .unwrap(); // El contenedor va DENTRO de otra columna (como en la página real): el // pase de medición resuelve su altura por contenido y el arrange corre // con esa altura ya conocida — ahí es donde taffy envuelve. let padre = tree .new_with_children( Style { display: Display::Flex, flex_direction: FlexDirection::Column, size: taffy::Size { width: length(976.0), height: auto() }, ..Default::default() }, &[cont], ) .unwrap(); tree.compute_layout_with_measure( padre, taffy::Size { width: AvailableSpace::Definite(976.0), height: AvailableSpace::MaxContent }, |known, avail, _n, ctx, _st| match ctx { Some(&mut (mn, mx)) => measure_texto(mn, mx, known, avail), None => taffy::Size::ZERO, }, ) .unwrap(); let lb = tree.layout(b).unwrap(); assert!( lb.location.x < 1.0, "el 2º ítem debe APILARSE (columna única), no irse a x={}", lb.location.x ); }