Skip to main content

llimphi_layout/
lib.rs

1//! llimphi-layout — Física del Espacio.
2//!
3//! Wrapper sobre `taffy` que resuelve árboles flex/grid y devuelve
4//! coordenadas absolutas (no relativas al padre). El consumidor pasa el
5//! árbol a `compute(root, viewport)` y obtiene un [`ComputedLayout`] con
6//! un rect absoluto por nodo, listo para `llimphi-raster`.
7
8use std::collections::HashMap;
9
10pub use taffy;
11pub use taffy::prelude::*;
12
13/// Errores del motor de layout.
14#[derive(Debug)]
15pub enum LayoutError {
16    Taffy(String),
17}
18
19impl std::fmt::Display for LayoutError {
20    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21        match self {
22            Self::Taffy(s) => write!(f, "taffy: {s}"),
23        }
24    }
25}
26
27impl std::error::Error for LayoutError {}
28
29/// Caja absoluta de un nodo (origen en la esquina superior izquierda del viewport).
30#[derive(Debug, Clone, Copy, PartialEq)]
31pub struct Rect {
32    pub x: f32,
33    pub y: f32,
34    pub w: f32,
35    pub h: f32,
36}
37
38/// Resultado de [`LayoutTree::compute`]: rect absoluto por nodo del árbol.
39#[derive(Debug, Default)]
40pub struct ComputedLayout {
41    pub rects: HashMap<NodeId, Rect>,
42}
43
44impl ComputedLayout {
45    pub fn get(&self, node: NodeId) -> Option<Rect> {
46        self.rects.get(&node).copied()
47    }
48}
49
50/// Árbol de layout. Encapsula la `TaffyTree` y la lógica de absolutización.
51pub struct LayoutTree {
52    inner: TaffyTree<()>,
53}
54
55impl Default for LayoutTree {
56    fn default() -> Self {
57        Self::new()
58    }
59}
60
61impl LayoutTree {
62    pub fn new() -> Self {
63        Self {
64            inner: TaffyTree::new(),
65        }
66    }
67
68    /// Vacía el árbol conservando la capacidad ya asignada. Permite
69    /// reusar la misma `LayoutTree` entre frames sin re-allocar el
70    /// slotmap interno de taffy: `clear()` + `mount` en vez de
71    /// `LayoutTree::new()` por frame. Los `NodeId` emitidos antes de
72    /// `clear()` quedan inválidos (el caller ya volcó lo que necesitaba
73    /// a un `ComputedLayout`, que es dueño de sus rects).
74    pub fn clear(&mut self) {
75        self.inner.clear();
76    }
77
78    /// Crea una hoja (nodo sin hijos).
79    pub fn leaf(&mut self, style: Style) -> Result<NodeId, LayoutError> {
80        self.inner
81            .new_leaf(style)
82            .map_err(|e| LayoutError::Taffy(e.to_string()))
83    }
84
85    /// Crea un nodo contenedor con hijos.
86    pub fn node(&mut self, style: Style, children: &[NodeId]) -> Result<NodeId, LayoutError> {
87        self.inner
88            .new_with_children(style, children)
89            .map_err(|e| LayoutError::Taffy(e.to_string()))
90    }
91
92    /// Calcula el layout para `root` con viewport `(w, h)` y devuelve rects absolutos.
93    pub fn compute(
94        &mut self,
95        root: NodeId,
96        viewport: (f32, f32),
97    ) -> Result<ComputedLayout, LayoutError> {
98        self.inner
99            .compute_layout(
100                root,
101                taffy::Size {
102                    width: AvailableSpace::Definite(viewport.0),
103                    height: AvailableSpace::Definite(viewport.1),
104                },
105            )
106            .map_err(|e| LayoutError::Taffy(e.to_string()))?;
107        let mut out = ComputedLayout::default();
108        flatten(&self.inner, root, 0.0, 0.0, &mut out.rects)?;
109        Ok(out)
110    }
111
112    /// Como [`Self::compute`] pero pasando una función de medición por
113    /// nodo. Taffy la invoca sobre las **hojas** que necesita dimensionar
114    /// (texto que envuelve, contenido intrínseco) con el `NodeId`, las
115    /// dimensiones ya conocidas y el espacio disponible; el caller devuelve
116    /// el tamaño en px. Devolver `Size::ZERO` deja que el estilo decida (el
117    /// comportamiento de [`Self::compute`] para hojas sin contenido). El
118    /// `NodeId` permite al caller mantener su propio mapa nodo→contenido
119    /// (p. ej. texto a shapear con parley) sin acoplar este crate a la capa
120    /// de tipografía.
121    pub fn compute_with_measure<F>(
122        &mut self,
123        root: NodeId,
124        viewport: (f32, f32),
125        mut measure: F,
126    ) -> Result<ComputedLayout, LayoutError>
127    where
128        F: FnMut(NodeId, taffy::Size<Option<f32>>, taffy::Size<AvailableSpace>) -> taffy::Size<f32>,
129    {
130        self.inner
131            .compute_layout_with_measure(
132                root,
133                taffy::Size {
134                    width: AvailableSpace::Definite(viewport.0),
135                    height: AvailableSpace::Definite(viewport.1),
136                },
137                |known, available, node_id, _ctx, _style| {
138                    measure(node_id, known, available)
139                },
140            )
141            .map_err(|e| LayoutError::Taffy(e.to_string()))?;
142        let mut out = ComputedLayout::default();
143        flatten(&self.inner, root, 0.0, 0.0, &mut out.rects)?;
144        Ok(out)
145    }
146
147    pub fn inner(&self) -> &TaffyTree<()> {
148        &self.inner
149    }
150
151    pub fn inner_mut(&mut self) -> &mut TaffyTree<()> {
152        &mut self.inner
153    }
154}
155
156fn flatten(
157    tree: &TaffyTree<()>,
158    node: NodeId,
159    ox: f32,
160    oy: f32,
161    out: &mut HashMap<NodeId, Rect>,
162) -> Result<(), LayoutError> {
163    let layout = tree
164        .layout(node)
165        .map_err(|e| LayoutError::Taffy(e.to_string()))?;
166    let x = ox + layout.location.x;
167    let y = oy + layout.location.y;
168    out.insert(
169        node,
170        Rect {
171            x,
172            y,
173            w: layout.size.width,
174            h: layout.size.height,
175        },
176    );
177    let children = tree
178        .children(node)
179        .map_err(|e| LayoutError::Taffy(e.to_string()))?;
180    for child in children {
181        flatten(tree, child, x, y, out)?;
182    }
183    Ok(())
184}