Skip to main content

agpu/layout/
mod.rs

1//! Layout engine for agpu — composable layout algorithms.
2//!
3//! Provides [`Layout`] trait and concrete implementations for flexbox,
4//! grid, constraint-based, scroll container, and responsive layouts.
5
6mod constraint;
7mod flex;
8mod grid;
9mod responsive;
10mod scroll;
11
12pub use constraint::{ConstraintLayout, ConstraintRule, MinMax};
13pub use flex::{Align, CrossAlign, FlexItem, FlexLayout, FlexWrap, JustifyContent};
14pub use grid::{GridItem, GridLayout, GridSpan};
15pub use responsive::{Breakpoint, ResponsiveLayout};
16pub use scroll::{ScrollContainer, ScrollDirection};
17
18use crate::core::Rect;
19
20/// Size constraints passed to layout algorithms.
21#[derive(Debug, Clone, Copy)]
22pub struct SizeConstraints {
23    pub min_width: f32,
24    pub min_height: f32,
25    pub max_width: f32,
26    pub max_height: f32,
27}
28
29impl SizeConstraints {
30    /// Unconstrained — no minimum, infinite maximum.
31    pub fn unconstrained() -> Self {
32        Self {
33            min_width: 0.0,
34            min_height: 0.0,
35            max_width: f32::INFINITY,
36            max_height: f32::INFINITY,
37        }
38    }
39
40    /// Fixed-size constraint.
41    pub fn fixed(width: f32, height: f32) -> Self {
42        Self {
43            min_width: width,
44            min_height: height,
45            max_width: width,
46            max_height: height,
47        }
48    }
49
50    /// Constrain to the given available area.
51    pub fn from_rect(area: Rect) -> Self {
52        Self {
53            min_width: 0.0,
54            min_height: 0.0,
55            max_width: area.width,
56            max_height: area.height,
57        }
58    }
59
60    /// Clamp a size to these constraints.
61    pub fn clamp(&self, width: f32, height: f32) -> (f32, f32) {
62        (
63            width.clamp(self.min_width, self.max_width),
64            height.clamp(self.min_height, self.max_height),
65        )
66    }
67}
68
69impl Default for SizeConstraints {
70    fn default() -> Self {
71        Self::unconstrained()
72    }
73}
74
75/// A node's desired size as reported by [`Layout::measure`].
76#[derive(Debug, Clone, Copy, Default)]
77pub struct DesiredSize {
78    pub width: f32,
79    pub height: f32,
80}
81
82impl DesiredSize {
83    pub fn new(width: f32, height: f32) -> Self {
84        Self { width, height }
85    }
86
87    pub fn zero() -> Self {
88        Self {
89            width: 0.0,
90            height: 0.0,
91        }
92    }
93}
94
95/// Trait implemented by all layout algorithms.
96///
97/// A layout receives a set of child sizes and an available area, then
98/// computes the final [`Rect`] for each child.
99pub trait Layout {
100    /// Compute the desired size of this layout given child sizes and constraints.
101    fn measure(&self, children: &[DesiredSize], constraints: SizeConstraints) -> DesiredSize;
102
103    /// Arrange children within the given area, returning a positioned rect per child.
104    fn arrange(&self, children: &[DesiredSize], area: Rect) -> Vec<Rect>;
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn size_constraints_unconstrained() {
113        let c = SizeConstraints::unconstrained();
114        assert_eq!(c.min_width, 0.0);
115        assert!(c.max_width.is_infinite());
116    }
117
118    #[test]
119    fn size_constraints_fixed() {
120        let c = SizeConstraints::fixed(100.0, 50.0);
121        assert_eq!(c.min_width, 100.0);
122        assert_eq!(c.max_width, 100.0);
123        assert_eq!(c.min_height, 50.0);
124        assert_eq!(c.max_height, 50.0);
125    }
126
127    #[test]
128    fn size_constraints_clamp() {
129        let c = SizeConstraints {
130            min_width: 10.0,
131            min_height: 10.0,
132            max_width: 100.0,
133            max_height: 100.0,
134        };
135        assert_eq!(c.clamp(5.0, 200.0), (10.0, 100.0));
136        assert_eq!(c.clamp(50.0, 50.0), (50.0, 50.0));
137    }
138
139    #[test]
140    fn desired_size_zero() {
141        let s = DesiredSize::zero();
142        assert_eq!(s.width, 0.0);
143        assert_eq!(s.height, 0.0);
144    }
145
146    #[test]
147    fn size_constraints_from_rect() {
148        let r = Rect::new(10.0, 20.0, 300.0, 400.0);
149        let c = SizeConstraints::from_rect(r);
150        assert_eq!(c.max_width, 300.0);
151        assert_eq!(c.max_height, 400.0);
152    }
153
154    // ── Flex layout tests ───────────────────────────────────────────
155
156    #[test]
157    fn flex_row_even_distribution() {
158        let flex = FlexLayout::row();
159        let children = vec![
160            DesiredSize::new(50.0, 30.0),
161            DesiredSize::new(50.0, 30.0),
162            DesiredSize::new(50.0, 30.0),
163        ];
164        let area = Rect::new(0.0, 0.0, 300.0, 100.0);
165        let rects = flex.arrange(&children, area);
166        assert_eq!(rects.len(), 3);
167        assert!((rects[0].x - 0.0).abs() < 0.01);
168        assert!((rects[1].x - 50.0).abs() < 0.01);
169        assert!((rects[2].x - 100.0).abs() < 0.01);
170    }
171
172    #[test]
173    fn flex_column_stacks_vertically() {
174        let flex = FlexLayout::column();
175        let children = vec![DesiredSize::new(100.0, 40.0), DesiredSize::new(100.0, 60.0)];
176        let area = Rect::new(0.0, 0.0, 200.0, 200.0);
177        let rects = flex.arrange(&children, area);
178        assert_eq!(rects.len(), 2);
179        assert!((rects[0].y - 0.0).abs() < 0.01);
180        assert!((rects[1].y - 40.0).abs() < 0.01);
181    }
182
183    #[test]
184    fn flex_spacing() {
185        let flex = FlexLayout::row().spacing(10.0);
186        let children = vec![DesiredSize::new(50.0, 30.0), DesiredSize::new(50.0, 30.0)];
187        let area = Rect::new(0.0, 0.0, 200.0, 100.0);
188        let rects = flex.arrange(&children, area);
189        assert!((rects[1].x - 60.0).abs() < 0.01); // 50 + 10 spacing
190    }
191
192    #[test]
193    fn flex_measure_row() {
194        let flex = FlexLayout::row().spacing(5.0);
195        let children = vec![DesiredSize::new(40.0, 20.0), DesiredSize::new(60.0, 30.0)];
196        let size = flex.measure(&children, SizeConstraints::unconstrained());
197        assert!((size.width - 105.0).abs() < 0.01); // 40 + 5 + 60
198        assert!((size.height - 30.0).abs() < 0.01); // max height
199    }
200
201    #[test]
202    fn flex_wrap_wraps_to_next_line() {
203        let flex = FlexLayout::row().wrap(FlexWrap::Wrap);
204        let children = vec![
205            DesiredSize::new(60.0, 30.0),
206            DesiredSize::new(60.0, 30.0),
207            DesiredSize::new(60.0, 30.0),
208        ];
209        let area = Rect::new(0.0, 0.0, 130.0, 200.0);
210        let rects = flex.arrange(&children, area);
211        // First two fit in row (60+60=120 <= 130), third wraps
212        assert!((rects[2].y - 30.0).abs() < 0.01);
213    }
214
215    // ── Grid layout tests ───────────────────────────────────────────
216
217    #[test]
218    fn grid_basic_layout() {
219        let grid = GridLayout::new(2, 2);
220        let children = vec![
221            DesiredSize::new(50.0, 50.0),
222            DesiredSize::new(50.0, 50.0),
223            DesiredSize::new(50.0, 50.0),
224            DesiredSize::new(50.0, 50.0),
225        ];
226        let area = Rect::new(0.0, 0.0, 200.0, 200.0);
227        let rects = grid.arrange(&children, area);
228        assert_eq!(rects.len(), 4);
229        assert!((rects[0].width - 100.0).abs() < 0.01);
230        assert!((rects[0].height - 100.0).abs() < 0.01);
231    }
232
233    #[test]
234    fn grid_measure() {
235        let grid = GridLayout::new(3, 2).gap(4.0);
236        let children = vec![
237            DesiredSize::new(30.0, 20.0),
238            DesiredSize::new(30.0, 20.0),
239            DesiredSize::new(30.0, 20.0),
240            DesiredSize::new(30.0, 20.0),
241            DesiredSize::new(30.0, 20.0),
242            DesiredSize::new(30.0, 20.0),
243        ];
244        let size = grid.measure(&children, SizeConstraints::unconstrained());
245        assert!((size.width - 98.0).abs() < 0.01); // 3*30 + 2*4
246        assert!((size.height - 44.0).abs() < 0.01); // 2*20 + 1*4
247    }
248
249    #[test]
250    fn grid_with_spans() {
251        let grid = GridLayout::new(3, 2)
252            .item(GridItem::new(0).col_span(2))
253            .item(GridItem::new(1));
254        let children = vec![DesiredSize::new(50.0, 30.0), DesiredSize::new(50.0, 30.0)];
255        let area = Rect::new(0.0, 0.0, 300.0, 200.0);
256        let rects = grid.arrange(&children, area);
257        // First item spans 2 columns: width should be ~200
258        assert!((rects[0].width - 200.0).abs() < 0.01);
259    }
260
261    // ── Constraint layout tests ─────────────────────────────────────
262
263    #[test]
264    fn constraint_layout_min_max() {
265        let layout =
266            ConstraintLayout::new().rule(ConstraintRule::new(0).width(MinMax::new(50.0, 200.0)));
267        let children = vec![DesiredSize::new(300.0, 40.0)];
268        let area = Rect::new(0.0, 0.0, 400.0, 400.0);
269        let rects = layout.arrange(&children, area);
270        assert!((rects[0].width - 200.0).abs() < 0.01); // clamped to max
271    }
272
273    #[test]
274    fn constraint_layout_unconstrained_children() {
275        let layout = ConstraintLayout::new();
276        let children = vec![DesiredSize::new(80.0, 60.0)];
277        let area = Rect::new(10.0, 20.0, 400.0, 300.0);
278        let rects = layout.arrange(&children, area);
279        assert!((rects[0].x - 10.0).abs() < 0.01);
280        assert!((rects[0].y - 20.0).abs() < 0.01);
281        assert!((rects[0].width - 80.0).abs() < 0.01);
282    }
283
284    // ── Scroll container tests ──────────────────────────────────────
285
286    #[test]
287    fn scroll_container_clips_content() {
288        let scroll = ScrollContainer::new(ScrollDirection::Vertical);
289        let children = vec![DesiredSize::new(100.0, 500.0)];
290        let area = Rect::new(0.0, 0.0, 100.0, 200.0);
291        let rects = scroll.arrange(&children, area);
292        assert!((rects[0].height - 500.0).abs() < 0.01); // content keeps full height
293    }
294
295    #[test]
296    fn scroll_with_offset() {
297        let scroll = ScrollContainer::new(ScrollDirection::Vertical).offset(50.0);
298        let children = vec![DesiredSize::new(100.0, 500.0)];
299        let area = Rect::new(0.0, 0.0, 100.0, 200.0);
300        let rects = scroll.arrange(&children, area);
301        assert!((rects[0].y - (-50.0)).abs() < 0.01); // shifted up
302    }
303
304    #[test]
305    fn scroll_measure_passes_through() {
306        let scroll = ScrollContainer::new(ScrollDirection::Both);
307        let children = vec![DesiredSize::new(800.0, 600.0)];
308        let size = scroll.measure(&children, SizeConstraints::unconstrained());
309        assert!((size.width - 800.0).abs() < 0.01);
310        assert!((size.height - 600.0).abs() < 0.01);
311    }
312
313    // ── Responsive layout tests ─────────────────────────────────────
314
315    #[test]
316    fn responsive_selects_correct_breakpoint() {
317        let layout = ResponsiveLayout::new()
318            .breakpoint(Breakpoint::new(0.0, FlexLayout::column()))
319            .breakpoint(Breakpoint::new(600.0, FlexLayout::row()));
320        let children = vec![DesiredSize::new(50.0, 30.0), DesiredSize::new(50.0, 30.0)];
321        // Wide area → row layout
322        let wide = Rect::new(0.0, 0.0, 800.0, 400.0);
323        let rects = layout.arrange(&children, wide);
324        assert!((rects[1].x - 50.0).abs() < 0.01); // row: side by side
325
326        // Narrow area → column layout
327        let narrow = Rect::new(0.0, 0.0, 400.0, 400.0);
328        let rects = layout.arrange(&children, narrow);
329        assert!((rects[1].y - 30.0).abs() < 0.01); // column: stacked
330    }
331
332    #[test]
333    fn responsive_measure_uses_widest() {
334        let layout = ResponsiveLayout::new().breakpoint(Breakpoint::new(0.0, FlexLayout::column()));
335        let children = vec![DesiredSize::new(50.0, 30.0)];
336        let size = layout.measure(&children, SizeConstraints::unconstrained());
337        assert!((size.width - 50.0).abs() < 0.01);
338    }
339}