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}