//! llimphi-layout — Física del Espacio. //! //! Wrapper sobre `taffy` que resuelve árboles flex/grid y devuelve //! coordenadas absolutas (no relativas al padre). El consumidor pasa el //! árbol a `compute(root, viewport)` y obtiene un [`ComputedLayout`] con //! un rect absoluto por nodo, listo para `llimphi-raster`. // `no_std` salvo bajo test. El layout es aritmética sobre un árbol: nunca tuvo // motivo para exigir sistema operativo, sólo la inercia de `std::collections`. // Es lo que permite que el mismo cálculo de cajas corra en Linux y en el kernel // de wawa (W1 de PLAN-LLIMPHI.md). #![cfg_attr(not(test), no_std)] extern crate alloc; use alloc::string::{String, ToString}; use hashbrown::HashMap; pub use taffy; pub use taffy::prelude::*; /// Errores del motor de layout. #[derive(Debug)] pub enum LayoutError { Taffy(String), } impl core::fmt::Display for LayoutError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::Taffy(s) => write!(f, "taffy: {s}"), } } } // `core::error::Error` es estable desde Rust 1.81 y es LA MISMA trait que // `std::error::Error` (std la reexporta), así que ningún consumidor nota el // cambio: quien haga `Box` con esto sigue compilando. impl core::error::Error for LayoutError {} /// Caja absoluta de un nodo (origen en la esquina superior izquierda del viewport). #[derive(Debug, Clone, Copy, PartialEq)] pub struct Rect { pub x: f32, pub y: f32, pub w: f32, pub h: f32, } /// Resultado de [`LayoutTree::compute`]: rect absoluto por nodo del árbol. #[derive(Debug, Default)] pub struct ComputedLayout { pub rects: HashMap, } impl ComputedLayout { pub fn get(&self, node: NodeId) -> Option { self.rects.get(&node).copied() } /// Vacía el mapa **conservando la capacidad**, para escribir el layout del /// próximo frame encima del bloque que ya está tomado. /// /// Es la contraparte de lo que el Bloque 21 hizo con el `Mounted`: sin /// esto, cada frame construye un `HashMap` de N entradas desde cero, y un /// `HashMap` que crece se rehashea **a saltos** (duplica la tabla, re-inserta /// todo). A 200k nodos eso medía 14 MB por frame — y era la mitad del «hilo /// abierto» del plan, donde un frame suelto aparecía con 1.807 asignaciones /// y 25 MB en la etapa de layout mientras el resto del lote iba en 465. /// /// Contrapeso, igual que en el mount: si el árbol se desploma (una tabla de /// 200k filas → una pantalla vacía), conservar la capacidad vieja sería /// quedarse con esos megas tomados para siempre. Se encoge sólo cuando sobra /// más de 4×, dejando 2× de margen, para que una UI que oscila alrededor de /// un tamaño no rebote entre encoger y volver a crecer. pub fn limpiar(&mut self) { let n = self.rects.len().max(1); if self.rects.capacity() > 4 * n { self.rects.shrink_to(2 * n); } self.rects.clear(); } } /// Árbol de layout. Encapsula la `TaffyTree` y la lógica de absolutización. pub struct LayoutTree { inner: TaffyTree<()>, } impl Default for LayoutTree { fn default() -> Self { Self::new() } } impl LayoutTree { pub fn new() -> Self { Self { inner: TaffyTree::new(), } } /// Vacía el árbol conservando la capacidad ya asignada. Permite /// reusar la misma `LayoutTree` entre frames sin re-allocar el /// slotmap interno de taffy: `clear()` + `mount` en vez de /// `LayoutTree::new()` por frame. Los `NodeId` emitidos antes de /// `clear()` quedan inválidos (el caller ya volcó lo que necesitaba /// a un `ComputedLayout`, que es dueño de sus rects). pub fn clear(&mut self) { self.inner.clear(); } /// Crea una hoja (nodo sin hijos). pub fn leaf(&mut self, style: Style) -> Result { self.inner .new_leaf(style) .map_err(|e| LayoutError::Taffy(e.to_string())) } /// Crea un nodo contenedor con hijos. pub fn node(&mut self, style: Style, children: &[NodeId]) -> Result { self.inner .new_with_children(style, children) .map_err(|e| LayoutError::Taffy(e.to_string())) } /// Calcula el layout para `root` con viewport `(w, h)` y devuelve rects absolutos. pub fn compute( &mut self, root: NodeId, viewport: (f32, f32), ) -> Result { self.inner .compute_layout( root, taffy::Size { width: AvailableSpace::Definite(viewport.0), height: AvailableSpace::Definite(viewport.1), }, ) .map_err(|e| LayoutError::Taffy(e.to_string()))?; let mut out = ComputedLayout::default(); flatten(&self.inner, root, 0.0, 0.0, &mut out.rects)?; Ok(out) } /// Como [`Self::compute`] pero pasando una función de medición por /// nodo. Taffy la invoca sobre las **hojas** que necesita dimensionar /// (texto que envuelve, contenido intrínseco) con el `NodeId`, las /// dimensiones ya conocidas y el espacio disponible; el caller devuelve /// el tamaño en px. Devolver `Size::ZERO` deja que el estilo decida (el /// comportamiento de [`Self::compute`] para hojas sin contenido). El /// `NodeId` permite al caller mantener su propio mapa nodo→contenido /// (p. ej. texto a shapear con parley) sin acoplar este crate a la capa /// de tipografía. pub fn compute_with_measure( &mut self, root: NodeId, viewport: (f32, f32), measure: F, ) -> Result where F: FnMut(NodeId, taffy::Size>, taffy::Size) -> taffy::Size, { self.compute_with_measure_reciclando(root, viewport, measure, None) } /// Igual que [`LayoutTree::compute_with_measure`], pero escribiendo el /// resultado **encima del `ComputedLayout` del frame anterior** en vez de /// pedir uno nuevo. /// /// El caller le entrega el mapa que ya no va a leer; acá se limpia /// conservando capacidad (ver [`ComputedLayout::limpiar`]) y se rellena. La /// fachada de arriba le pasa `None` — comportamiento histórico exacto, cero /// regresión para el resto de los callers. pub fn compute_with_measure_reciclando( &mut self, root: NodeId, viewport: (f32, f32), mut measure: F, reciclado: Option, ) -> Result where F: FnMut(NodeId, taffy::Size>, taffy::Size) -> taffy::Size, { self.inner .compute_layout_with_measure( root, taffy::Size { width: AvailableSpace::Definite(viewport.0), height: AvailableSpace::Definite(viewport.1), }, |known, available, node_id, _ctx, _style| { measure(node_id, known, available) }, ) .map_err(|e| LayoutError::Taffy(e.to_string()))?; let mut out = reciclado.unwrap_or_default(); out.limpiar(); flatten(&self.inner, root, 0.0, 0.0, &mut out.rects)?; Ok(out) } pub fn inner(&self) -> &TaffyTree<()> { &self.inner } pub fn inner_mut(&mut self) -> &mut TaffyTree<()> { &mut self.inner } } #[cfg(test)] mod cache_probe { //! Bloque 24 (probe): ¿taffy saltea subárboles limpios al recomputar sobre //! un árbol persistente (sin `clear()`)? De la respuesta depende todo el //! enfoque de layout retenido. Se mide contando llamadas a `measure`. use super::*; use std::cell::Cell; #[test] fn taffy_cachea_entre_computes() { let mut lt = LayoutTree::new(); let mut leaves = Vec::new(); for _ in 0..100 { leaves.push( lt.leaf(Style { size: Size { width: auto(), height: auto() }, ..Default::default() }) .unwrap(), ); } let root = lt .node( Style { flex_direction: FlexDirection::Column, size: Size { width: length(200.0), height: length(4000.0) }, ..Default::default() }, &leaves, ) .unwrap(); let count = Cell::new(0u32); let mut measure = |_nid: NodeId, _k: taffy::Size>, _a: taffy::Size| { count.set(count.get() + 1); taffy::Size { width: 10.0, height: 10.0 } }; lt.compute_with_measure(root, (200.0, 4000.0), &mut measure).unwrap(); let first = count.get(); // 2ª pasada SIN mutar ni clear. count.set(0); lt.compute_with_measure(root, (200.0, 4000.0), &mut measure).unwrap(); let second = count.get(); // 3ª: una sola hoja sucia. lt.inner_mut().mark_dirty(leaves[0]).unwrap(); count.set(0); lt.compute_with_measure(root, (200.0, 4000.0), &mut measure).unwrap(); let third = count.get(); println!("[probe] measures: 1ª={first} 2ª(sin mutar)={second} 3ª(1 hoja sucia)={third}"); assert!(first >= 100, "la 1ª pasada mide las hojas"); // Premisa del Bloque 24: la 2ª pasada cachea (mide << first) y una hoja // sucia sólo re-mide su cadena (third << first). assert!(second < first, "2ª pasada debe cachear (second={second} < first={first})"); assert!(third < first, "1 hoja sucia re-mide poco (third={third} < first={first})"); } } fn flatten( tree: &TaffyTree<()>, node: NodeId, ox: f32, oy: f32, out: &mut HashMap, ) -> Result<(), LayoutError> { let layout = tree .layout(node) .map_err(|e| LayoutError::Taffy(e.to_string()))?; let x = ox + layout.location.x; let y = oy + layout.location.y; out.insert( node, Rect { x, y, w: layout.size.width, h: layout.size.height, }, ); // `tree.children(node)` hace `self.children[node].clone()`: **una asignación // de heap por cada nodo con hijos, en cada frame**, sólo para recorrerlos y // tirarla. A 200k nodos en 448 columnas eso medía 448 de las 465 // asignaciones de la etapa de layout. Se recorre por índice, que lee el // mismo `Vec` de taffy sin copiarlo. let n = tree.child_count(node); for i in 0..n { let child = tree .child_at_index(node, i) .map_err(|e| LayoutError::Taffy(e.to_string()))?; flatten(tree, child, x, y, out)?; } Ok(()) }