tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual
1use super::*;
2
3/// Holgura (px) que se le suma al ancho de envoltura del texto en el PAINT.
4///
5/// Taffy redondea cada borde de caja a entero de forma independiente, así que
6/// el ancho *usado* de un nodo puede quedar hasta ~1px por debajo del ancho
7/// natural de su contenido (el error de redondeo del borde izquierdo y el
8/// derecho se suman: cada uno ∈ [-0.5, 0.5] → pérdida ≤ 1.0). Para un nodo de
9/// texto encogido-a-contenido (un `<a>`/`<span>` inline que la medida
10/// dimensionó a `nat=86.438` pero taffy dejó en `86.000`), envolver el paint
11/// exactamente a ese ancho redondeado parte la última palabra a una segunda
12/// línea que la MEDIDA nunca reservó (queda desbordando hacia abajo). Sumar
13/// esta holgura hace que el paint tolere la pérdida de redondeo: el texto que
14/// cabía en una línea sigue en una línea. No afecta el wrapping real (un
15/// párrafo que de verdad excede su bloque por >1px sigue envolviendo).
16pub(crate) const TEXT_WRAP_ROUNDING_SLACK: f32 = 1.0;
17
18pub fn mount<Msg: Clone>(layout: &mut LayoutTree, v: View<Msg>) -> Mounted<Msg> {
19 let mut nodes = Vec::new();
20 let mut text_measures = hashbrown::HashMap::new();
21 let mut intrinsic_measures = hashbrown::HashMap::new();
22 let root = mount_recursive(
23 layout,
24 v,
25 &mut nodes,
26 &mut text_measures,
27 &mut intrinsic_measures,
28 &MountPlan::Fresh,
29 &mut Vec::new(),
30 );
31 Mounted { root, nodes, text_measures, intrinsic_measures }
32}
33
34/// Cómo obtiene su `NodeId` cada nodo montado (Bloque 24). `Fresh` crea nodos
35/// taffy nuevos (camino histórico). `Reuse` reusa los `NodeId` del frame anterior
36/// en el MISMO orden pre-orden (sólo válido si la **forma** del árbol no cambió —
37/// lo garantiza `mount_incremental`) y sólo llama `set_style` donde el estilo
38/// taffy cambió, dejando limpio el resto para que taffy cachee su layout (ver el
39/// probe `taffy_cachea_entre_computes` en llimphi-layout).
40pub(crate) enum MountPlan<'a> {
41 Fresh,
42 Reuse(&'a [NodeId]),
43}
44
45/// Mount en pre-orden directo sobre `out`: pusheamos el padre como
46/// placeholder (id real desconocido hasta crear el taffy node), recursamos
47/// hijos sobre el mismo `out`, y al volver completamos `id` + `subtree_end`.
48/// El `plan` decide si el `NodeId` es nuevo (`Fresh`) o reusado del frame
49/// anterior (`Reuse`, Bloque 24).
50/// `ids_scratch` es **una sola pila compartida por todo el recorrido**, no un
51/// buffer por nodo: cada nivel apila los ids de sus hijos al final, se los pasa
52/// a taffy como rebanada, y trunca al salir. Antes cada contenedor pedía su
53/// propio `Vec` — a 200k nodos en 448 columnas eran 448 asignaciones por frame,
54/// y las 448 últimas que le quedaban a la etapa de mount.
55pub(crate) fn mount_recursive<Msg: Clone>(
56 layout: &mut LayoutTree,
57 v: View<Msg>,
58 out: &mut Vec<MountedNode<Msg>>,
59 text_measures: &mut hashbrown::HashMap<NodeId, TextMeasure>,
60 intrinsic_measures: &mut hashbrown::HashMap<NodeId, IntrinsicSize>,
61 plan: &MountPlan<'_>,
62 ids_scratch: &mut Vec<NodeId>,
63) -> NodeId {
64 let View {
65 style,
66 text,
67 painter,
68 alpha,
69 semantics,
70 fill,
71 hover_fill,
72 radius,
73 clip,
74 over_layer,
75 on_click,
76 children,
77 rare,
78 } = v;
79 // Props raras (ver [`ViewRare`]): el nodo corriente no trae ninguna, así que
80 // `raro` es `None` y todos los campos de abajo quedan en su default — las
81 // mismas escrituras que hacía el literal plano antes del reparto.
82 let raro = rare.map(|caja| *caja);
83 let intrinsic = raro.as_ref().and_then(|r| r.intrinsic);
84 // Un nodo memo se monta como HOJA marcador (su contenido lo splica el
85 // runtime tras el layout); sus `children` estáticos se ignoran.
86 let children = if raro.as_ref().is_some_and(|r| r.memo.is_some()) {
87 Vec::new()
88 } else {
89 children
90 };
91 let parent_idx = out.len();
92 out.push(MountedNode {
93 id: NodeId::new(0), // placeholder, lo sobreescribimos abajo
94 fill,
95 hover_fill,
96 radius,
97 over_layer,
98 on_click,
99 clip,
100 underlay: None,
101 subtree_end: 0,
102 corner_radii: None,
103 shadow: None,
104 fill_gradient: None,
105 fill_gradient_superficie: false,
106 border: None,
107 text,
108 image: None,
109 image_fit: None,
110 mask_image: None,
111 mask_placement: None,
112 mask_extra: Vec::new(),
113 painter,
114 gpu_painter: None,
115 over_painter: None,
116 on_click_at: None,
117 on_click_screen: None,
118 on_right_click: None,
119 on_right_click_at: None,
120 on_right_click_screen: None,
121 on_middle_click: None,
122 drag: None,
123 drag_at: None,
124 drag_velocity: None,
125 drag_payload: None,
126 drag_carga: None,
127 on_drop: None,
128 drop_hover_fill: None,
129 on_drop_hover: None,
130 clip_inset: None,
131 clip_ellipse: None,
132 clip_polygon: None,
133 clip_path_svg: None,
134 clip_ref_inset: None,
135 on_pointer_enter: None,
136 on_pointer_leave: None,
137 on_pointer_move_at: None,
138 on_scroll: None,
139 on_scroll_take: None,
140 on_scale: None,
141 on_rotate: None,
142 on_double_tap: None,
143 on_double_tap_at: None,
144 double_tap_exclusive: None,
145 on_long_press: None,
146 on_long_press_at: None,
147 focusable: None,
148 text_select_key: None,
149 alpha,
150 anim: None,
151 animated_size: None,
152 semantics,
153 hero: None,
154 transform: None,
155 transform_rel: None,
156 transform_origin: None,
157 sticky: None,
158 tooltip: None,
159 cursor: None,
160 ripple: None,
161 is_layout_builder: false,
162 backdrop_blur: None,
163 filter: Vec::new(),
164 blend: None,
165 repaint_boundary: None,
166 memo_ref: None,
167 });
168 if let Some(r) = raro {
169 let n = &mut out[parent_idx];
170 n.corner_radii = r.corner_radii;
171 n.shadow = r.shadow;
172 n.fill_gradient = r.fill_gradient;
173 n.fill_gradient_superficie = r.fill_gradient_superficie;
174 n.border = r.border;
175 n.image = r.image;
176 n.image_fit = r.image_fit;
177 n.mask_image = r.mask_image;
178 n.mask_placement = r.mask_placement;
179 n.mask_extra = r.mask_extra;
180 n.gpu_painter = r.gpu_painter;
181 n.over_painter = r.over_painter;
182 n.on_click_at = r.on_click_at;
183 n.on_click_screen = r.on_click_screen;
184 n.on_right_click = r.on_right_click;
185 n.on_right_click_at = r.on_right_click_at;
186 n.on_right_click_screen = r.on_right_click_screen;
187 n.on_middle_click = r.on_middle_click;
188 n.drag = r.drag;
189 n.drag_at = r.drag_at;
190 n.drag_velocity = r.drag_velocity;
191 n.drag_payload = r.drag_payload;
192 n.drag_carga = r.drag_carga.clone();
193 n.on_drop = r.on_drop;
194 n.drop_hover_fill = r.drop_hover_fill;
195 n.on_drop_hover = r.on_drop_hover;
196 n.clip_inset = r.clip_inset;
197 n.clip_ellipse = r.clip_ellipse;
198 n.clip_polygon = r.clip_polygon;
199 n.clip_path_svg = r.clip_path_svg;
200 n.clip_ref_inset = r.clip_ref_inset;
201 n.on_pointer_enter = r.on_pointer_enter;
202 n.on_pointer_leave = r.on_pointer_leave;
203 n.on_pointer_move_at = r.on_pointer_move_at;
204 n.on_scroll = r.on_scroll;
205 n.on_scroll_take = r.on_scroll_take;
206 n.on_scale = r.on_scale;
207 n.on_rotate = r.on_rotate;
208 n.on_double_tap = r.on_double_tap;
209 n.on_double_tap_at = r.on_double_tap_at;
210 n.double_tap_exclusive = r.double_tap_exclusive;
211 n.on_long_press = r.on_long_press;
212 n.on_long_press_at = r.on_long_press_at;
213 n.focusable = r.focusable;
214 n.text_select_key = r.text_select_key;
215 n.anim = r.anim;
216 n.animated_size = r.animated_size;
217 n.hero = r.hero;
218 n.transform = r.transform;
219 n.transform_rel = r.transform_rel;
220 n.transform_origin = r.transform_origin;
221 n.sticky = r.sticky;
222 n.tooltip = r.tooltip;
223 n.cursor = r.cursor;
224 n.ripple = r.ripple;
225 n.is_layout_builder = r.layout_builder.is_some();
226 n.backdrop_blur = r.backdrop_blur;
227 n.filter = r.filter;
228 n.blend = r.blend;
229 n.repaint_boundary = r.repaint_boundary;
230 n.memo_ref = r.memo.as_ref().map(|m| m.key);
231 }
232 // Marca de pila: lo que este nodo apile queda de `base` para arriba, y se
233 // trunca al salir. Los hijos usan la MISMA pila más adelante, así que el
234 // recorrido entero no pide un solo bloque.
235 let base = ids_scratch.len();
236 for child in children {
237 let hijo = mount_recursive(
238 layout,
239 child,
240 out,
241 text_measures,
242 intrinsic_measures,
243 plan,
244 ids_scratch,
245 );
246 ids_scratch.push(hijo);
247 }
248 let sin_hijos = ids_scratch.len() == base;
249 let id = match plan {
250 MountPlan::Fresh => {
251 if sin_hijos {
252 layout.leaf(style).expect("layout leaf")
253 } else {
254 layout.node(style, &ids_scratch[base..]).expect("layout node")
255 }
256 }
257 MountPlan::Reuse(ids) => {
258 // `parent_idx` es el índice pre-orden de este nodo; con la forma
259 // validada (misma cantidad de hijos en cada posición) el reuso
260 // posicional es exacto. Sólo re-estilamos si el estilo cambió: así
261 // los nodos sin cambios quedan LIMPIOS y taffy cachea su layout.
262 let reused = ids[parent_idx];
263 let changed = layout
264 .inner()
265 .style(reused)
266 .map(|s| *s != style)
267 .unwrap_or(true);
268 if changed {
269 let _ = layout.inner_mut().set_style(reused, style);
270 }
271 reused
272 }
273 };
274 ids_scratch.truncate(base);
275 out[parent_idx].id = id;
276 out[parent_idx].subtree_end = out.len();
277 // Hoja de texto uniforme: registrá su contenido para que el runtime lo
278 // mida con parley. El texto multicolor (`runs`) lo dimensiona el caller
279 // (editor: un nodo por línea), así que no lo medimos aquí.
280 // Hoja de contenido REEMPLAZADO (una imagen): registrá su tamaño natural
281 // para que el runtime la mida como mide el texto. Ver `IntrinsicSize`.
282 if sin_hijos {
283 if let Some(nat) = intrinsic {
284 intrinsic_measures.insert(id, nat);
285 }
286 }
287 if sin_hijos {
288 if let Some(text) = out[parent_idx].text.as_ref() {
289 if text.runs.is_none() {
290 text_measures.insert(
291 id,
292 TextMeasure {
293 content: text.content.clone(),
294 size_px: text.size_px,
295 alignment: text.alignment,
296 italic: text.italic,
297 font_family: text.font_family.clone(),
298 line_height: text.line_height,
299 weight: text.weight,
300 max_lines: text.max_lines,
301 ellipsis: text.ellipsis,
302 underline: text.underline,
303 strikethrough: text.strikethrough,
304 spans: text.spans.clone(),
305 letter_spacing: text.letter_spacing,
306 word_spacing: text.word_spacing,
307 no_wrap: text.no_wrap,
308 overflow_wrap: text.overflow_wrap,
309 font_features: text.font_features.clone(),
310 font_variations: text.font_variations.clone(),
311 font_width: text.font_width,
312 },
313 );
314 }
315 }
316 }
317 id
318}
319