ailloli_ui_runtime/layout/
layout_node.rs

1//! Layout-node contract implemented by retained element kinds.
2
3use ailloli_ui_core::{Constraints, EdgeInsets, Offset, Rect, Size};
4
5use super::layout_ctx::LayoutCtx;
6use super::layout_result::{ChildLayout, LayoutResult};
7use crate::scene::{PaintCtx, Painter};
8
9/// Legacy immediate layout/paint contract used by experimental [`LayoutNode`] code.
10///
11/// This is distinct from [`crate::component::Widget`], which participates in
12/// the retained element tree. Implementations receive logical-pixel geometry;
13/// they are responsible for honoring constraints and for not retaining either
14/// borrowed context.
15///
16/// # Examples
17///
18/// ```
19/// use ailloli_ui_core::{Constraints, Size};
20/// use ailloli_ui_runtime::layout::{LayoutCtx, Widget};
21/// use ailloli_ui_runtime::scene::{PaintCtx, Painter};
22///
23/// struct Fixed;
24/// impl Widget for Fixed {
25///     fn layout(&mut self, _ctx: &LayoutCtx<'_>, constraints: Constraints) -> Size {
26///         constraints.constrain(Size::new(12.0, 8.0))
27///     }
28///     fn paint(&self, _ctx: &mut PaintCtx<'_>, _out: &mut dyn Painter) {}
29/// }
30/// ```
31pub trait Widget {
32    /// Measures the widget within `constraints`, returning logical pixels.
33    ///
34    /// Implementations should return a size accepted by
35    /// [`Constraints::constrain`]; the trait itself does not enforce that
36    /// invariant.
37    ///
38    /// # Examples
39    ///
40    /// ```
41    /// use ailloli_ui_core::{Constraints, Scale, Size};
42    /// use ailloli_ui_runtime::layout::{LayoutCtx, Widget};
43    /// use ailloli_ui_runtime::scene::{PaintCtx, Painter};
44    ///
45    /// struct Fixed;
46    /// impl Widget for Fixed {
47    ///     fn layout(&mut self, _ctx: &LayoutCtx<'_>, c: Constraints) -> Size {
48    ///         c.constrain(Size::new(30.0, 10.0))
49    ///     }
50    ///     fn paint(&self, _ctx: &mut PaintCtx<'_>, _out: &mut dyn Painter) {}
51    /// }
52    /// let mut widget = Fixed;
53    /// let ctx = LayoutCtx::new(Scale::new(1.0));
54    /// assert_eq!(widget.layout(&ctx, Constraints::loose(20.0, 20.0)), Size::new(20.0, 10.0));
55    /// ```
56    fn layout(&mut self, ctx: &LayoutCtx<'_>, constraints: Constraints) -> Size;
57
58    /// Emits this widget's draw commands through `out`.
59    ///
60    /// The method has no implicit clip or clearing behavior. A widget that emits
61    /// no commands leaves the painter unchanged.
62    ///
63    /// # Examples
64    ///
65    /// ```
66    /// use ailloli_ui_core::{Constraints, Size};
67    /// use ailloli_ui_runtime::layout::{LayoutCtx, Widget};
68    /// use ailloli_ui_runtime::scene::{PaintCtx, Painter};
69    ///
70    /// struct Invisible;
71    /// impl Widget for Invisible {
72    ///     fn layout(&mut self, _ctx: &LayoutCtx<'_>, _c: Constraints) -> Size { Size::default() }
73    ///     fn paint(&self, _ctx: &mut PaintCtx<'_>, _out: &mut dyn Painter) {}
74    /// }
75    /// let widget = Invisible;
76    /// let mut ctx = PaintCtx::new();
77    /// let mut out = PaintCtx::new();
78    /// widget.paint(&mut ctx, &mut out);
79    /// assert_eq!(out.layers[0].cmds.len(), 0);
80    /// ```
81    fn paint(&self, ctx: &mut PaintCtx<'_>, out: &mut dyn Painter);
82}
83
84/// Retained layout tree node (experimental; coexists with [`Widget`] trait layout).
85///
86/// This is not a compatibility shim; it is intended as the future retained layout engine base.
87/// Geometry is in logical pixels and values are not validated for finiteness.
88/// `ScrollY::scroll_y` is currently retained but deliberately has no effect on
89/// child offsets; callers must not yet rely on this node for visible scrolling.
90///
91/// # Examples
92///
93/// ```
94/// use ailloli_ui_core::{Constraints, EdgeInsets, Scale, Size};
95/// use ailloli_ui_runtime::layout::{LayoutCtx, LayoutNode};
96///
97/// let mut node = LayoutNode::Padding {
98///     padding: EdgeInsets::all(2.0),
99///     child: Box::new(LayoutNode::Leaf),
100/// };
101/// let result = node.layout(&LayoutCtx::new(Scale::new(1.0)), Constraints::tight(20.0, 10.0));
102/// assert_eq!(result.size, Size::new(20.0, 10.0));
103/// assert_eq!(result.children[0].offset.x, 2.0);
104/// ```
105#[derive(Debug, Clone)]
106pub enum LayoutNode {
107    /// Fills the stored maximum constraints and has no children.
108    Leaf,
109    /// Insets one child by per-side logical pixels.
110    Padding {
111        /// Insets subtracted from child constraints and added around its size.
112        padding: EdgeInsets,
113        /// Sole retained child.
114        child: Box<LayoutNode>,
115    },
116    /// Overlays every child at offset `(0, 0)` and fills the maximum constraints.
117    Stack {
118        /// Children painted in stored order.
119        children: Vec<LayoutNode>,
120    },
121    /// Places children top-to-bottom, giving each the full maximum height.
122    Column {
123        /// Children laid out in stored order; heights are summed then constrained.
124        children: Vec<LayoutNode>,
125    },
126    /// Replaces one child's bounds with a rectangular clip matching its size.
127    Clip {
128        /// Sole clipped child.
129        child: Box<LayoutNode>,
130    },
131    /// Measures a vertically unbounded child inside a fixed viewport.
132    ScrollY {
133        /// Requested vertical offset in logical pixels; currently stored but ignored.
134        scroll_y: f32,
135        /// Content measured with infinite maximum height.
136        child: Box<LayoutNode>,
137    },
138}
139
140/// Provides the operations defined for LayoutNode.
141impl LayoutNode {
142    /// Recursively lays out this experimental node tree.
143    ///
144    /// `Leaf` and `Stack` use the raw maximum constraints, so an unbounded axis
145    /// can produce an infinite size. `Column` does not reduce remaining height
146    /// between children. `Padding` preserves only child geometry, while child
147    /// clips, overlays, and artifacts are not propagated into its parent result.
148    /// `ScrollY` clips to the viewport but currently ignores its `scroll_y`
149    /// value and does not translate child geometry.
150    ///
151    /// # Panics
152    ///
153    /// Constraint operations may panic when normalized bounds contain NaN.
154    ///
155    /// # Examples
156    ///
157    /// ```
158    /// use ailloli_ui_core::{Constraints, Scale, Size};
159    /// use ailloli_ui_runtime::layout::{LayoutCtx, LayoutNode};
160    ///
161    /// let mut node = LayoutNode::Column {
162    ///     children: vec![LayoutNode::Leaf, LayoutNode::Leaf],
163    /// };
164    /// let result = node.layout(&LayoutCtx::new(Scale::new(1.0)), Constraints::tight(10.0, 5.0));
165    /// assert_eq!(result.size, Size::new(10.0, 5.0));
166    /// assert_eq!(result.children[1].offset.y, 5.0);
167    /// ```
168    pub fn layout(&mut self, _ctx: &LayoutCtx<'_>, constraints: Constraints) -> LayoutResult {
169        match self {
170            LayoutNode::Leaf => {
171                let size = constraints.max_size();
172                LayoutResult {
173                    size,
174                    children: Vec::new(),
175                    paint_bounds: Rect::new(0.0, 0.0, size.w, size.h),
176                    visual_bounds: Rect::new(0.0, 0.0, size.w, size.h),
177                    overlay_hit_bounds: Vec::new(),
178                    clip: None,
179                    is_window_root_clip: false,
180                    artifact: None,
181                }
182            }
183            LayoutNode::Padding { padding, child } => {
184                let inner = constraints.deflate(*padding);
185                let mut child_res = child.layout(_ctx, inner);
186                let child_offset = Offset::new(padding.left, padding.top);
187                for c in &mut child_res.children {
188                    c.offset =
189                        Offset::new(c.offset.x + child_offset.x, c.offset.y + child_offset.y);
190                    c.paint_bounds = c.paint_bounds.translate(child_offset);
191                    c.visual_bounds = c.visual_bounds.translate(child_offset);
192                }
193                child_res.paint_bounds = child_res.paint_bounds.translate(child_offset);
194                child_res.visual_bounds = child_res.visual_bounds.translate(child_offset);
195
196                let size = constraints.constrain(Size::new(
197                    child_res.size.w + padding.left + padding.right,
198                    child_res.size.h + padding.top + padding.bottom,
199                ));
200
201                LayoutResult {
202                    size,
203                    children: vec![ChildLayout {
204                        offset: child_offset,
205                        size: child_res.size,
206                        paint_bounds: child_res.paint_bounds,
207                        visual_bounds: child_res.visual_bounds,
208                    }],
209                    paint_bounds: Rect::new(0.0, 0.0, size.w, size.h),
210                    visual_bounds: Rect::new(0.0, 0.0, size.w, size.h),
211                    overlay_hit_bounds: Vec::new(),
212                    clip: None,
213                    is_window_root_clip: false,
214                    artifact: None,
215                }
216            }
217            LayoutNode::Stack { children } => {
218                let size = constraints.max_size();
219                let mut out_children = Vec::with_capacity(children.len());
220                for child in children.iter_mut() {
221                    let child_res = child.layout(_ctx, Constraints::loose(size.w, size.h));
222                    out_children.push(ChildLayout {
223                        offset: Offset::default(),
224                        size: child_res.size,
225                        paint_bounds: child_res.paint_bounds,
226                        visual_bounds: child_res.visual_bounds,
227                    });
228                }
229                LayoutResult {
230                    size,
231                    children: out_children,
232                    paint_bounds: Rect::new(0.0, 0.0, size.w, size.h),
233                    visual_bounds: Rect::new(0.0, 0.0, size.w, size.h),
234                    overlay_hit_bounds: Vec::new(),
235                    clip: None,
236                    is_window_root_clip: false,
237                    artifact: None,
238                }
239            }
240            LayoutNode::Column { children } => {
241                let max = constraints.max_size();
242                let mut y = 0.0;
243                let mut max_w: f32 = 0.0;
244                let mut out_children = Vec::with_capacity(children.len());
245                for child in children.iter_mut() {
246                    let child_res = child.layout(_ctx, Constraints::loose(max.w, max.h.max(0.0)));
247                    out_children.push(ChildLayout {
248                        offset: Offset::new(0.0, y),
249                        size: child_res.size,
250                        paint_bounds: child_res.paint_bounds.translate(Offset::new(0.0, y)),
251                        visual_bounds: child_res.visual_bounds.translate(Offset::new(0.0, y)),
252                    });
253                    y += child_res.size.h;
254                    max_w = max_w.max(child_res.size.w);
255                }
256                let size = constraints.constrain(Size::new(max_w, y));
257                LayoutResult {
258                    size,
259                    children: out_children,
260                    paint_bounds: Rect::new(0.0, 0.0, size.w, size.h),
261                    visual_bounds: Rect::new(0.0, 0.0, size.w, size.h),
262                    overlay_hit_bounds: Vec::new(),
263                    clip: None,
264                    is_window_root_clip: false,
265                    artifact: None,
266                }
267            }
268            LayoutNode::Clip { child } => {
269                let mut child_res = child.layout(_ctx, constraints);
270                let size = child_res.size;
271                let bounds = Rect::new(0.0, 0.0, size.w, size.h);
272                child_res.clip = Some(ailloli_ui_core::ClipShape::Rect(bounds));
273                child_res.paint_bounds = bounds;
274                child_res.visual_bounds = bounds;
275                child_res
276            }
277            LayoutNode::ScrollY { scroll_y: _, child } => {
278                let viewport = constraints.max_size();
279                let mut child_res =
280                    child.layout(_ctx, Constraints::loose(viewport.w, f32::INFINITY));
281                let size = viewport;
282                child_res.size = size;
283                child_res.paint_bounds = Rect::new(0.0, 0.0, size.w, size.h);
284                child_res.visual_bounds = child_res.paint_bounds;
285                child_res.clip = Some(ailloli_ui_core::ClipShape::Rect(child_res.paint_bounds));
286                child_res
287            }
288        }
289    }
290}
291
292#[cfg(test)]
293/// Tests implementation details.
294mod tests {
295    use super::*;
296
297    #[test]
298    /// Verifies that leaf respects constraints.
299    fn leaf_respects_constraints() {
300        let mut node = LayoutNode::Leaf;
301        let c = Constraints::tight(10.0, 20.0);
302        let res = node.layout(&LayoutCtx::new(ailloli_ui_core::Scale { dpr: 1.0 }), c);
303        assert_eq!(res.size, Size::new(10.0, 20.0));
304        assert_eq!(c.constrain(res.size), res.size);
305    }
306}