feat: llimphi standalone — framework UI soberano extraído del monorepo

Motor gráfico Llimphi como workspace independiente: bucle Elm
(input→update→view→layout→raster→present) sobre wgpu+vello+taffy+parley.
Núcleo (hal/raster/layout/text/ui/theme/surface/motion/icons) + ~40 widgets
+ módulos, sin dependencias al resto del monorepo. cargo check --workspace
pasa (64 crates). Puerta de entrada: cargo run -p llimphi-ui --example counter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-04 04:23:42 +00:00
commit e65e9cc623
286 changed files with 46136 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
//! Verifica que un párrafo largo, dentro de un bloque angosto, reserva el
//! alto de **varias líneas** (no se aplasta en una). Es el regresor del bug
//! "textos aplastados" de puriy: sin medición con parley, taffy le daba a la
//! hoja de texto una sola línea de alto y las líneas envueltas se solapaban.
use llimphi_compositor::{measure_text_node, mount, View};
use llimphi_layout::taffy::prelude::*;
use llimphi_layout::taffy::Size as TSize;
use llimphi_layout::LayoutTree;
#[derive(Clone)]
enum Msg {}
#[test]
fn parrafo_largo_reserva_varias_lineas() {
// Bloque de 200px de ancho con un párrafo que claramente excede una línea.
let texto = "Lorem ipsum dolor sit amet consectetur adipiscing elit sed do \
eiusmod tempor incididunt ut labore et dolore magna aliqua ut \
enim ad minim veniam quis nostrud exercitation ullamco laboris.";
let block: View<Msg> = View::new(Style {
size: TSize { width: length(200.0_f32), height: auto() },
flex_direction: FlexDirection::Row,
flex_wrap: FlexWrap::Wrap,
..Default::default()
})
.children(vec![View::new(Style {
size: TSize { width: auto(), height: auto() },
flex_shrink: 1.0,
..Default::default()
})
.text_aligned(texto, 16.0_f32, vello::peniko::Color::BLACK, llimphi_text::Alignment::Start)]);
let mut layout = LayoutTree::new();
let mounted = mount(&mut layout, block);
let mut ts = llimphi_text::Typesetter::new();
let tmap = &mounted.text_measures;
assert_eq!(tmap.len(), 1, "debería haber exactamente una hoja de texto");
let computed = layout
.compute_with_measure(mounted.root, (800.0, 600.0), |nid, known, avail| match tmap.get(&nid)
{
Some(tm) => measure_text_node(&mut ts, tm, known, avail),
None => TSize::ZERO,
})
.expect("layout");
// El nodo de texto es el segundo en orden DFS (root, luego la hoja).
let leaf_id = mounted.nodes[1].id;
let rect = computed.get(leaf_id).expect("rect de la hoja");
// A 16px y ~1.2 de interlínea, una línea ≈ 19px. Con ~150px de texto en
// 200px de ancho deberían ser >= 4 líneas → bastante más de una.
assert!(
rect.h > 40.0,
"el párrafo se aplastó: alto={} (esperaba varias líneas)",
rect.h
);
assert!(rect.w <= 200.0 + 1.0, "no debería exceder el ancho del bloque");
}
#[test]
fn line_height_mayor_reserva_mas_alto() {
let texto = "una línea de texto que envuelve en dos o tres renglones según \
el ancho disponible para el bloque contenedor angosto";
let medir = |lh: f32| -> f32 {
let mut ts = llimphi_text::Typesetter::new();
let tm = llimphi_compositor::TextMeasure {
content: texto.to_string(),
size_px: 16.0,
alignment: llimphi_text::Alignment::Start,
italic: false,
font_family: None,
line_height: lh,
};
let known = TSize { width: Some(180.0_f32), height: None };
let avail = TSize {
width: AvailableSpace::Definite(180.0),
height: AvailableSpace::MaxContent,
};
measure_text_node(&mut ts, &tm, known, avail).height
};
let compacto = medir(1.0);
let comodo = medir(2.0);
assert!(
comodo > compacto * 1.5,
"line-height: 2 debería reservar bastante más alto que 1.0 (got {compacto} vs {comodo})"
);
}