tawasuyu

tawasuyu/llimphi

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

Rust 229TOML 101Markdown 82Shell 24052 Sutichaykuna

⎇ main llimphi-compositor/src/render/montaje.rs Rust · 319 siq'ikuna · 11.7 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual d8edd9a9c · Sergio ·

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