tawasuyu

tawasuyu/llimphi

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

Rust 238TOML 102Markdown 83Shell 24178 Sutichaykuna

⎇ main llimphi-compositor/tests/text_measure.rs Rust · 207 siq'ikuna · 7.8 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: refresco desde el monorepo — 27 crates nuevos (anim, lottie, svg, mesh, image, layer, test, wire-view, widgets rag-sidebar/router/lazy-list/rive-button) y sync de los 95 existentes 93253cc22 · Sergio ·

1//! Verifica que un párrafo largo, dentro de un bloque angosto, reserva el
2//! alto de **varias líneas** (no se aplasta en una). Es el regresor del bug
3//! "textos aplastados" de puriy: sin medición con parley, taffy le daba a la
4//! hoja de texto una sola línea de alto y las líneas envueltas se solapaban.
5
6use llimphi_compositor::{measure_text_node, mount, View};
7use llimphi_layout::taffy::prelude::*;
8use llimphi_layout::taffy::Size as TSize;
9use llimphi_layout::LayoutTree;
10
11#[derive(Clone)]
12enum Msg {}
13
14#[test]
15fn parrafo_largo_reserva_varias_lineas() {
16    // Bloque de 200px de ancho con un párrafo que claramente excede una línea.
17    let texto = "Lorem ipsum dolor sit amet consectetur adipiscing elit sed do \
18                 eiusmod tempor incididunt ut labore et dolore magna aliqua ut \
19                 enim ad minim veniam quis nostrud exercitation ullamco laboris.";
20    let block: View<Msg> = View::new(Style {
21        size: TSize { width: length(200.0_f32), height: auto() },
22        flex_direction: FlexDirection::Row,
23        flex_wrap: FlexWrap::Wrap,
24        ..Default::default()
25    })
26    .children(vec![View::new(Style {
27        size: TSize { width: auto(), height: auto() },
28        flex_shrink: 1.0,
29        ..Default::default()
30    })
31    .text_aligned(texto, 16.0_f32, vello::peniko::Color::BLACK, llimphi_text::Alignment::Start)]);
32
33    let mut layout = LayoutTree::new();
34    let mounted = mount(&mut layout, block);
35    let mut ts = llimphi_text::Typesetter::new();
36    let tmap = &mounted.text_measures;
37    assert_eq!(tmap.len(), 1, "debería haber exactamente una hoja de texto");
38
39    let computed = layout
40        .compute_with_measure(mounted.root, (800.0, 600.0), |nid, known, avail| match tmap.get(&nid)
41        {
42            Some(tm) => measure_text_node(&mut ts, tm, known, avail),
43            None => TSize::ZERO,
44        })
45        .expect("layout");
46
47    // El nodo de texto es el segundo en orden DFS (root, luego la hoja).
48    let leaf_id = mounted.nodes[1].id;
49    let rect = computed.get(leaf_id).expect("rect de la hoja");
50    // A 16px y ~1.2 de interlínea, una línea ≈ 19px. Con ~150px de texto en
51    // 200px de ancho deberían ser >= 4 líneas → bastante más de una.
52    assert!(
53        rect.h > 40.0,
54        "el párrafo se aplastó: alto={} (esperaba varias líneas)",
55        rect.h
56    );
57    assert!(rect.w <= 200.0 + 1.0, "no debería exceder el ancho del bloque");
58}
59
60#[test]
61fn no_wrap_mide_una_sola_linea_fase_7_1253() {
62    use llimphi_layout::taffy::AvailableSpace;
63    // Mismo texto largo, mismo ancho disponible (angosto): con `no_wrap` se
64    // mide en una sola línea (ancho completo, ignora el available); sin él,
65    // envuelve (más alto, ancho acotado al disponible).
66    let texto = "una linea larga que normalmente envuelve en varios renglones \
67                 cuando el ancho disponible es angosto de verdad";
68    let mk = |no_wrap: bool| llimphi_compositor::TextMeasure {
69        content: texto.to_string(),
70        size_px: 16.0,
71        alignment: llimphi_text::Alignment::Start,
72        italic: false,
73        font_family: None,
74        line_height: 1.2,
75        weight: 400.0,
76        max_lines: None,
77        ellipsis: false,
78        underline: false,
79        strikethrough: false,
80        spans: None,
81        letter_spacing: 0.0,
82        word_spacing: 0.0,
83        no_wrap,
84        overflow_wrap: false,
85        font_features: String::new(),
86        font_variations: String::new(),
87        font_width: 100.0,
88    };
89    let mut ts = llimphi_text::Typesetter::new();
90    let known = TSize { width: None, height: None };
91    let avail = TSize {
92        width: AvailableSpace::Definite(160.0),
93        height: AvailableSpace::MaxContent,
94    };
95    let env = measure_text_node(&mut ts, &mk(false), known, avail);
96    let nw = measure_text_node(&mut ts, &mk(true), known, avail);
97    // Envuelto: ancho acotado al disponible y alto de varias líneas.
98    assert!(env.width <= 160.0 + 1.0, "wrap acota el ancho: {}", env.width);
99    assert!(env.height > 40.0, "wrap reserva varias líneas: {}", env.height);
100    // no_wrap: una sola línea → mucho más ancho que el disponible y bajo.
101    assert!(nw.width > 160.0, "no_wrap mide ancho completo: {}", nw.width);
102    assert!(
103        nw.height < env.height,
104        "no_wrap es una línea (más bajo que el envuelto): nw={} env={}",
105        nw.height,
106        env.height
107    );
108}
109
110#[test]
111fn line_height_mayor_reserva_mas_alto() {
112    let texto = "una línea de texto que envuelve en dos o tres renglones según \
113                 el ancho disponible para el bloque contenedor angosto";
114    let medir = |lh: f32| -> f32 {
115        let mut ts = llimphi_text::Typesetter::new();
116        let tm = llimphi_compositor::TextMeasure {
117            content: texto.to_string(),
118            size_px: 16.0,
119            alignment: llimphi_text::Alignment::Start,
120            italic: false,
121            font_family: None,
122            line_height: lh,
123            weight: 400.0,
124            max_lines: None,
125            ellipsis: false,
126            underline: false,
127            strikethrough: false,
128            spans: None,
129            letter_spacing: 0.0,
130            word_spacing: 0.0,
131            no_wrap: false,
132            overflow_wrap: false,
133            font_features: String::new(),
134            font_variations: String::new(),
135            font_width: 100.0,
136        };
137        let known = TSize { width: Some(180.0_f32), height: None };
138        let avail = TSize {
139            width: AvailableSpace::Definite(180.0),
140            height: AvailableSpace::MaxContent,
141        };
142        measure_text_node(&mut ts, &tm, known, avail).height
143    };
144    let compacto = medir(1.0);
145    let comodo = medir(2.0);
146    assert!(
147        comodo > compacto * 1.5,
148        "line-height: 2 debería reservar bastante más alto que 1.0 (got {compacto} vs {comodo})"
149    );
150}
151
152#[test]
153fn overflow_wrap_parte_la_palabra_larga_fase_7_1254() {
154    use llimphi_layout::taffy::AvailableSpace;
155    // Una sola palabra sin espacios, más ancha que la caja angosta. Sin
156    // `overflow-wrap` parley la deja desbordar (mide más ancho que la caja);
157    // con `overflow-wrap` la parte para que entre (ancho acotado, varias líneas
158    // ⇒ más alto). Es el regresor directo de la Fase 7.1254.
159    let palabrota = "supercalifragilisticoexpialidosoineluctableantidisestablishmentariano";
160    let mk = |overflow_wrap: bool| llimphi_compositor::TextMeasure {
161        content: palabrota.to_string(),
162        size_px: 16.0,
163        alignment: llimphi_text::Alignment::Start,
164        italic: false,
165        font_family: None,
166        line_height: 1.2,
167        weight: 400.0,
168        max_lines: None,
169        ellipsis: false,
170        underline: false,
171        strikethrough: false,
172        spans: None,
173        letter_spacing: 0.0,
174        word_spacing: 0.0,
175        no_wrap: false,
176        overflow_wrap,
177        font_features: String::new(),
178        font_variations: String::new(),
179        font_width: 100.0,
180    };
181    let mut ts = llimphi_text::Typesetter::new();
182    let known = TSize { width: None, height: None };
183    let avail = TSize {
184        width: AvailableSpace::Definite(80.0),
185        height: AvailableSpace::MaxContent,
186    };
187    let normal = measure_text_node(&mut ts, &mk(false), known, avail);
188    let roto = measure_text_node(&mut ts, &mk(true), known, avail);
189    // Sin overflow-wrap: la palabra desborda → ancho mayor que la caja.
190    assert!(
191        normal.width > 80.0,
192        "sin overflow-wrap la palabra desborda: {}",
193        normal.width
194    );
195    // Con overflow-wrap: se parte → ancho acotado a la caja y más alto.
196    assert!(
197        roto.width <= 80.0 + 1.0,
198        "overflow-wrap acota el ancho a la caja: {}",
199        roto.width
200    );
201    assert!(
202        roto.height > normal.height,
203        "overflow-wrap parte en varias líneas (más alto): roto={} normal={}",
204        roto.height,
205        normal.height
206    );
207}