tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
⎇ 92dd0e5bfb2ca4456006da99437f4100e156d856
llimphi-compositor/tests/text_measure.rs
Historia
Quién tocó qué
crudo
refresh: stack al día (vello 0.7 / wgpu 27 / parley 0.6) + motor 3D voxel
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 };
86 let mut ts = llimphi_text::Typesetter::new();
87 let known = TSize { width: None, height: None };
88 let avail = TSize {
89 width: AvailableSpace::Definite(160.0),
90 height: AvailableSpace::MaxContent,
91 };
92 let env = measure_text_node(&mut ts, &mk(false), known, avail);
93 let nw = measure_text_node(&mut ts, &mk(true), known, avail);
94 // Envuelto: ancho acotado al disponible y alto de varias líneas.
95 assert!(env.width <= 160.0 + 1.0, "wrap acota el ancho: {}", env.width);
96 assert!(env.height > 40.0, "wrap reserva varias líneas: {}", env.height);
97 // no_wrap: una sola línea → mucho más ancho que el disponible y bajo.
98 assert!(nw.width > 160.0, "no_wrap mide ancho completo: {}", nw.width);
99 assert!(
100 nw.height < env.height,
101 "no_wrap es una línea (más bajo que el envuelto): nw={} env={}",
102 nw.height,
103 env.height
104 );
105}
106
107#[test]
108fn line_height_mayor_reserva_mas_alto() {
109 let texto = "una línea de texto que envuelve en dos o tres renglones según \
110 el ancho disponible para el bloque contenedor angosto";
111 let medir = |lh: f32| -> f32 {
112 let mut ts = llimphi_text::Typesetter::new();
113 let tm = llimphi_compositor::TextMeasure {
114 content: texto.to_string(),
115 size_px: 16.0,
116 alignment: llimphi_text::Alignment::Start,
117 italic: false,
118 font_family: None,
119 line_height: lh,
120 weight: 400.0,
121 max_lines: None,
122 ellipsis: false,
123 underline: false,
124 strikethrough: false,
125 spans: None,
126 letter_spacing: 0.0,
127 word_spacing: 0.0,
128 no_wrap: false,
129 overflow_wrap: false,
130 };
131 let known = TSize { width: Some(180.0_f32), height: None };
132 let avail = TSize {
133 width: AvailableSpace::Definite(180.0),
134 height: AvailableSpace::MaxContent,
135 };
136 measure_text_node(&mut ts, &tm, known, avail).height
137 };
138 let compacto = medir(1.0);
139 let comodo = medir(2.0);
140 assert!(
141 comodo > compacto * 1.5,
142 "line-height: 2 debería reservar bastante más alto que 1.0 (got {compacto} vs {comodo})"
143 );
144}
145
146#[test]
147fn overflow_wrap_parte_la_palabra_larga_fase_7_1254() {
148 use llimphi_layout::taffy::AvailableSpace;
149 // Una sola palabra sin espacios, más ancha que la caja angosta. Sin
150 // `overflow-wrap` parley la deja desbordar (mide más ancho que la caja);
151 // con `overflow-wrap` la parte para que entre (ancho acotado, varias líneas
152 // ⇒ más alto). Es el regresor directo de la Fase 7.1254.
153 let palabrota = "supercalifragilisticoexpialidosoineluctableantidisestablishmentariano";
154 let mk = |overflow_wrap: bool| llimphi_compositor::TextMeasure {
155 content: palabrota.to_string(),
156 size_px: 16.0,
157 alignment: llimphi_text::Alignment::Start,
158 italic: false,
159 font_family: None,
160 line_height: 1.2,
161 weight: 400.0,
162 max_lines: None,
163 ellipsis: false,
164 underline: false,
165 strikethrough: false,
166 spans: None,
167 letter_spacing: 0.0,
168 word_spacing: 0.0,
169 no_wrap: false,
170 overflow_wrap,
171 };
172 let mut ts = llimphi_text::Typesetter::new();
173 let known = TSize { width: None, height: None };
174 let avail = TSize {
175 width: AvailableSpace::Definite(80.0),
176 height: AvailableSpace::MaxContent,
177 };
178 let normal = measure_text_node(&mut ts, &mk(false), known, avail);
179 let roto = measure_text_node(&mut ts, &mk(true), known, avail);
180 // Sin overflow-wrap: la palabra desborda → ancho mayor que la caja.
181 assert!(
182 normal.width > 80.0,
183 "sin overflow-wrap la palabra desborda: {}",
184 normal.width
185 );
186 // Con overflow-wrap: se parte → ancho acotado a la caja y más alto.
187 assert!(
188 roto.width <= 80.0 + 1.0,
189 "overflow-wrap acota el ancho a la caja: {}",
190 roto.width
191 );
192 assert!(
193 roto.height > normal.height,
194 "overflow-wrap parte en varias líneas (más alto): roto={} normal={}",
195 roto.height,
196 normal.height
197 );
198}