tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
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}