use super::*; /// Holgura (px) que se le suma al ancho de envoltura del texto en el PAINT. /// /// Taffy redondea cada borde de caja a entero de forma independiente, así que /// el ancho *usado* de un nodo puede quedar hasta ~1px por debajo del ancho /// natural de su contenido (el error de redondeo del borde izquierdo y el /// derecho se suman: cada uno ∈ [-0.5, 0.5] → pérdida ≤ 1.0). Para un nodo de /// texto encogido-a-contenido (un ``/`` inline que la medida /// dimensionó a `nat=86.438` pero taffy dejó en `86.000`), envolver el paint /// exactamente a ese ancho redondeado parte la última palabra a una segunda /// línea que la MEDIDA nunca reservó (queda desbordando hacia abajo). Sumar /// esta holgura hace que el paint tolere la pérdida de redondeo: el texto que /// cabía en una línea sigue en una línea. No afecta el wrapping real (un /// párrafo que de verdad excede su bloque por >1px sigue envolviendo). pub(crate) const TEXT_WRAP_ROUNDING_SLACK: f32 = 1.0; pub fn mount(layout: &mut LayoutTree, v: View) -> Mounted { let mut nodes = Vec::new(); let mut text_measures = hashbrown::HashMap::new(); let mut intrinsic_measures = hashbrown::HashMap::new(); let root = mount_recursive( layout, v, &mut nodes, &mut text_measures, &mut intrinsic_measures, &MountPlan::Fresh, &mut Vec::new(), ); Mounted { root, nodes, text_measures, intrinsic_measures } } /// Cómo obtiene su `NodeId` cada nodo montado (Bloque 24). `Fresh` crea nodos /// taffy nuevos (camino histórico). `Reuse` reusa los `NodeId` del frame anterior /// en el MISMO orden pre-orden (sólo válido si la **forma** del árbol no cambió — /// lo garantiza `mount_incremental`) y sólo llama `set_style` donde el estilo /// taffy cambió, dejando limpio el resto para que taffy cachee su layout (ver el /// probe `taffy_cachea_entre_computes` en llimphi-layout). pub(crate) enum MountPlan<'a> { Fresh, Reuse(&'a [NodeId]), } /// Mount en pre-orden directo sobre `out`: pusheamos el padre como /// placeholder (id real desconocido hasta crear el taffy node), recursamos /// hijos sobre el mismo `out`, y al volver completamos `id` + `subtree_end`. /// El `plan` decide si el `NodeId` es nuevo (`Fresh`) o reusado del frame /// anterior (`Reuse`, Bloque 24). /// `ids_scratch` es **una sola pila compartida por todo el recorrido**, no un /// buffer por nodo: cada nivel apila los ids de sus hijos al final, se los pasa /// a taffy como rebanada, y trunca al salir. Antes cada contenedor pedía su /// propio `Vec` — a 200k nodos en 448 columnas eran 448 asignaciones por frame, /// y las 448 últimas que le quedaban a la etapa de mount. pub(crate) fn mount_recursive( layout: &mut LayoutTree, v: View, out: &mut Vec>, text_measures: &mut hashbrown::HashMap, intrinsic_measures: &mut hashbrown::HashMap, plan: &MountPlan<'_>, ids_scratch: &mut Vec, ) -> NodeId { let View { style, text, painter, alpha, semantics, fill, hover_fill, radius, clip, over_layer, on_click, children, rare, } = v; // Props raras (ver [`ViewRare`]): el nodo corriente no trae ninguna, así que // `raro` es `None` y todos los campos de abajo quedan en su default — las // mismas escrituras que hacía el literal plano antes del reparto. let raro = rare.map(|caja| *caja); let intrinsic = raro.as_ref().and_then(|r| r.intrinsic); // Un nodo memo se monta como HOJA marcador (su contenido lo splica el // runtime tras el layout); sus `children` estáticos se ignoran. let children = if raro.as_ref().is_some_and(|r| r.memo.is_some()) { Vec::new() } else { children }; let parent_idx = out.len(); out.push(MountedNode { id: NodeId::new(0), // placeholder, lo sobreescribimos abajo fill, hover_fill, radius, over_layer, on_click, clip, underlay: None, subtree_end: 0, corner_radii: None, shadow: None, fill_gradient: None, fill_gradient_superficie: false, border: None, text, image: None, image_fit: None, mask_image: None, mask_placement: None, mask_extra: Vec::new(), painter, gpu_painter: None, over_painter: None, on_click_at: None, on_click_screen: None, on_right_click: None, on_right_click_at: None, on_right_click_screen: None, on_middle_click: None, drag: None, drag_at: None, drag_velocity: None, drag_payload: None, drag_carga: None, on_drop: None, drop_hover_fill: None, on_drop_hover: None, clip_inset: None, clip_ellipse: None, clip_polygon: None, clip_path_svg: None, clip_ref_inset: None, on_pointer_enter: None, on_pointer_leave: None, on_pointer_move_at: None, on_scroll: None, on_scroll_take: None, on_scale: None, on_rotate: None, on_double_tap: None, on_double_tap_at: None, double_tap_exclusive: None, on_long_press: None, on_long_press_at: None, focusable: None, text_select_key: None, alpha, anim: None, animated_size: None, semantics, hero: None, transform: None, transform_rel: None, transform_origin: None, sticky: None, tooltip: None, cursor: None, ripple: None, is_layout_builder: false, backdrop_blur: None, filter: Vec::new(), blend: None, repaint_boundary: None, memo_ref: None, }); if let Some(r) = raro { let n = &mut out[parent_idx]; n.corner_radii = r.corner_radii; n.shadow = r.shadow; n.fill_gradient = r.fill_gradient; n.fill_gradient_superficie = r.fill_gradient_superficie; n.border = r.border; n.image = r.image; n.image_fit = r.image_fit; n.mask_image = r.mask_image; n.mask_placement = r.mask_placement; n.mask_extra = r.mask_extra; n.gpu_painter = r.gpu_painter; n.over_painter = r.over_painter; n.on_click_at = r.on_click_at; n.on_click_screen = r.on_click_screen; n.on_right_click = r.on_right_click; n.on_right_click_at = r.on_right_click_at; n.on_right_click_screen = r.on_right_click_screen; n.on_middle_click = r.on_middle_click; n.drag = r.drag; n.drag_at = r.drag_at; n.drag_velocity = r.drag_velocity; n.drag_payload = r.drag_payload; n.drag_carga = r.drag_carga.clone(); n.on_drop = r.on_drop; n.drop_hover_fill = r.drop_hover_fill; n.on_drop_hover = r.on_drop_hover; n.clip_inset = r.clip_inset; n.clip_ellipse = r.clip_ellipse; n.clip_polygon = r.clip_polygon; n.clip_path_svg = r.clip_path_svg; n.clip_ref_inset = r.clip_ref_inset; n.on_pointer_enter = r.on_pointer_enter; n.on_pointer_leave = r.on_pointer_leave; n.on_pointer_move_at = r.on_pointer_move_at; n.on_scroll = r.on_scroll; n.on_scroll_take = r.on_scroll_take; n.on_scale = r.on_scale; n.on_rotate = r.on_rotate; n.on_double_tap = r.on_double_tap; n.on_double_tap_at = r.on_double_tap_at; n.double_tap_exclusive = r.double_tap_exclusive; n.on_long_press = r.on_long_press; n.on_long_press_at = r.on_long_press_at; n.focusable = r.focusable; n.text_select_key = r.text_select_key; n.anim = r.anim; n.animated_size = r.animated_size; n.hero = r.hero; n.transform = r.transform; n.transform_rel = r.transform_rel; n.transform_origin = r.transform_origin; n.sticky = r.sticky; n.tooltip = r.tooltip; n.cursor = r.cursor; n.ripple = r.ripple; n.is_layout_builder = r.layout_builder.is_some(); n.backdrop_blur = r.backdrop_blur; n.filter = r.filter; n.blend = r.blend; n.repaint_boundary = r.repaint_boundary; n.memo_ref = r.memo.as_ref().map(|m| m.key); } // Marca de pila: lo que este nodo apile queda de `base` para arriba, y se // trunca al salir. Los hijos usan la MISMA pila más adelante, así que el // recorrido entero no pide un solo bloque. let base = ids_scratch.len(); for child in children { let hijo = mount_recursive( layout, child, out, text_measures, intrinsic_measures, plan, ids_scratch, ); ids_scratch.push(hijo); } let sin_hijos = ids_scratch.len() == base; let id = match plan { MountPlan::Fresh => { if sin_hijos { layout.leaf(style).expect("layout leaf") } else { layout.node(style, &ids_scratch[base..]).expect("layout node") } } MountPlan::Reuse(ids) => { // `parent_idx` es el índice pre-orden de este nodo; con la forma // validada (misma cantidad de hijos en cada posición) el reuso // posicional es exacto. Sólo re-estilamos si el estilo cambió: así // los nodos sin cambios quedan LIMPIOS y taffy cachea su layout. let reused = ids[parent_idx]; let changed = layout .inner() .style(reused) .map(|s| *s != style) .unwrap_or(true); if changed { let _ = layout.inner_mut().set_style(reused, style); } reused } }; ids_scratch.truncate(base); out[parent_idx].id = id; out[parent_idx].subtree_end = out.len(); // Hoja de texto uniforme: registrá su contenido para que el runtime lo // mida con parley. El texto multicolor (`runs`) lo dimensiona el caller // (editor: un nodo por línea), así que no lo medimos aquí. // Hoja de contenido REEMPLAZADO (una imagen): registrá su tamaño natural // para que el runtime la mida como mide el texto. Ver `IntrinsicSize`. if sin_hijos { if let Some(nat) = intrinsic { intrinsic_measures.insert(id, nat); } } if sin_hijos { if let Some(text) = out[parent_idx].text.as_ref() { if text.runs.is_none() { text_measures.insert( id, TextMeasure { content: text.content.clone(), size_px: text.size_px, alignment: text.alignment, italic: text.italic, font_family: text.font_family.clone(), line_height: text.line_height, weight: text.weight, max_lines: text.max_lines, ellipsis: text.ellipsis, underline: text.underline, strikethrough: text.strikethrough, spans: text.spans.clone(), letter_spacing: text.letter_spacing, word_spacing: text.word_spacing, no_wrap: text.no_wrap, overflow_wrap: text.overflow_wrap, font_features: text.font_features.clone(), font_variations: text.font_variations.clone(), font_width: text.font_width, }, ); } } } id }