Skip to main content

rlvgl_ui/
layout.rs

1// SPDX-License-Identifier: MIT
2//! Basic layout helpers for arranging [`Widget`] instances from
3//! [`rlvgl_widgets`].
4//!
5//! Provides vertical and horizontal stacks, a simple grid, a box wrapper,
6//! and a lightweight [`GridCalc`] geometry calculator.
7
8use alloc::{boxed::Box, vec::Vec};
9use rlvgl_core::{
10    event::Event,
11    renderer::Renderer,
12    widget::{Rect, Widget},
13};
14use rlvgl_widgets::container::Container;
15
16/// Construct a [`Rect`] from `(x, y, width, height)`.
17///
18/// This keeps application code terse when building UI trees:
19///
20/// ```
21/// # use rlvgl_core::widget::Rect;
22/// # use rlvgl_ui::rect;
23/// assert_eq!(rect(1, 2, 3, 4), Rect { x: 1, y: 2, width: 3, height: 4 });
24/// ```
25pub const fn rect(x: i32, y: i32, width: i32, height: i32) -> Rect {
26    Rect {
27        x,
28        y,
29        width,
30        height,
31    }
32}
33
34/// Construct a [`Rect`] at origin `(0, 0)`.
35pub const fn origin_rect(width: i32, height: i32) -> Rect {
36    rect(0, 0, width, height)
37}
38
39/// Fluent geometry helpers for [`Rect`].
40pub trait RectProps: Sized {
41    /// Set the top-left origin.
42    fn at(self, x: i32, y: i32) -> Rect;
43
44    /// Set the X coordinate.
45    fn x(self, x: i32) -> Rect;
46
47    /// Set the Y coordinate.
48    fn y(self, y: i32) -> Rect;
49
50    /// Set width and height.
51    fn size(self, width: i32, height: i32) -> Rect;
52
53    /// Set the width.
54    fn width(self, width: i32) -> Rect;
55
56    /// Set the height.
57    fn height(self, height: i32) -> Rect;
58
59    /// Translate the rectangle by `(dx, dy)`.
60    fn translate(self, dx: i32, dy: i32) -> Rect;
61
62    /// Inset all sides by `amount`.
63    fn inset(self, amount: i32) -> Rect;
64
65    /// Inset horizontal and vertical sides separately.
66    fn inset_xy(self, x: i32, y: i32) -> Rect;
67
68    /// Clamp the rectangle width to at least `width`.
69    fn min_width(self, width: i32) -> Rect;
70
71    /// Clamp the rectangle height to at least `height`.
72    fn min_height(self, height: i32) -> Rect;
73
74    /// Clamp width and height to at least the provided values.
75    fn min_size(self, width: i32, height: i32) -> Rect;
76}
77
78impl RectProps for Rect {
79    fn at(mut self, x: i32, y: i32) -> Rect {
80        self.x = x;
81        self.y = y;
82        self
83    }
84
85    fn x(mut self, x: i32) -> Rect {
86        self.x = x;
87        self
88    }
89
90    fn y(mut self, y: i32) -> Rect {
91        self.y = y;
92        self
93    }
94
95    fn size(mut self, width: i32, height: i32) -> Rect {
96        self.width = width;
97        self.height = height;
98        self
99    }
100
101    fn width(mut self, width: i32) -> Rect {
102        self.width = width;
103        self
104    }
105
106    fn height(mut self, height: i32) -> Rect {
107        self.height = height;
108        self
109    }
110
111    fn translate(mut self, dx: i32, dy: i32) -> Rect {
112        self.x += dx;
113        self.y += dy;
114        self
115    }
116
117    fn inset(self, amount: i32) -> Rect {
118        self.inset_xy(amount, amount)
119    }
120
121    fn inset_xy(mut self, x: i32, y: i32) -> Rect {
122        self.x += x;
123        self.y += y;
124        self.width = (self.width - x * 2).max(0);
125        self.height = (self.height - y * 2).max(0);
126        self
127    }
128
129    fn min_width(mut self, width: i32) -> Rect {
130        self.width = self.width.max(width);
131        self
132    }
133
134    fn min_height(mut self, height: i32) -> Rect {
135        self.height = self.height.max(height);
136        self
137    }
138
139    fn min_size(self, width: i32, height: i32) -> Rect {
140        self.min_width(width).min_height(height)
141    }
142}
143
144/// Container that positions children vertically.
145///
146/// Accepts any [`Widget`] from [`rlvgl_widgets`] and arranges them
147/// top-to-bottom.
148pub struct VStack {
149    bounds: Rect,
150    spacing: i32,
151    children: Vec<Box<dyn Widget>>,
152    next_y: i32,
153}
154
155impl VStack {
156    /// Create an empty vertical stack with the given width.
157    pub fn new(width: i32) -> Self {
158        Self {
159            bounds: Rect {
160                x: 0,
161                y: 0,
162                width,
163                height: 0,
164            },
165            spacing: 0,
166            children: Vec::new(),
167            next_y: 0,
168        }
169    }
170
171    /// Set the spacing between stacked children.
172    pub fn spacing(mut self, spacing: i32) -> Self {
173        self.spacing = spacing;
174        self
175    }
176
177    /// Set the gap between stacked children.
178    pub fn gap(self, gap: i32) -> Self {
179        self.spacing(gap)
180    }
181
182    /// Add a child of the given height, created by the supplied builder.
183    pub fn child<W, F>(mut self, height: i32, builder: F) -> Self
184    where
185        W: Widget + 'static,
186        F: FnOnce(Rect) -> W,
187    {
188        let rect = Rect {
189            x: 0,
190            y: self.next_y,
191            width: self.bounds.width,
192            height,
193        };
194        self.next_y += height + self.spacing;
195        self.bounds.height = self.next_y - self.spacing;
196        self.children.push(Box::new(builder(rect)));
197        self
198    }
199}
200
201impl Widget for VStack {
202    fn bounds(&self) -> Rect {
203        self.bounds
204    }
205
206    fn draw(&self, renderer: &mut dyn Renderer) {
207        for child in &self.children {
208            child.draw(renderer);
209        }
210    }
211
212    fn handle_event(&mut self, event: &Event) -> bool {
213        for child in &mut self.children {
214            if child.handle_event(event) {
215                return true;
216            }
217        }
218        false
219    }
220}
221
222/// Container that positions children horizontally.
223///
224/// Like [`VStack`], this operates on [`Widget`] instances from
225/// [`rlvgl_widgets`].
226pub struct HStack {
227    bounds: Rect,
228    spacing: i32,
229    children: Vec<Box<dyn Widget>>,
230    next_x: i32,
231}
232
233impl HStack {
234    /// Create an empty horizontal stack with the given height.
235    pub fn new(height: i32) -> Self {
236        Self {
237            bounds: Rect {
238                x: 0,
239                y: 0,
240                width: 0,
241                height,
242            },
243            spacing: 0,
244            children: Vec::new(),
245            next_x: 0,
246        }
247    }
248
249    /// Set the spacing between stacked children.
250    pub fn spacing(mut self, spacing: i32) -> Self {
251        self.spacing = spacing;
252        self
253    }
254
255    /// Set the gap between stacked children.
256    pub fn gap(self, gap: i32) -> Self {
257        self.spacing(gap)
258    }
259
260    /// Add a child of the given width, created by the supplied builder.
261    pub fn child<W, F>(mut self, width: i32, builder: F) -> Self
262    where
263        W: Widget + 'static,
264        F: FnOnce(Rect) -> W,
265    {
266        let rect = Rect {
267            x: self.next_x,
268            y: 0,
269            width,
270            height: self.bounds.height,
271        };
272        self.next_x += width + self.spacing;
273        self.bounds.width = self.next_x - self.spacing;
274        self.children.push(Box::new(builder(rect)));
275        self
276    }
277}
278
279impl Widget for HStack {
280    fn bounds(&self) -> Rect {
281        self.bounds
282    }
283
284    fn draw(&self, renderer: &mut dyn Renderer) {
285        for child in &self.children {
286            child.draw(renderer);
287        }
288    }
289
290    fn handle_event(&mut self, event: &Event) -> bool {
291        for child in &mut self.children {
292            if child.handle_event(event) {
293                return true;
294            }
295        }
296        false
297    }
298}
299
300/// Simple grid container placing widgets in fixed-size cells.
301pub struct Grid {
302    bounds: Rect,
303    cols: i32,
304    cell_w: i32,
305    cell_h: i32,
306    spacing: i32,
307    children: Vec<Box<dyn Widget>>,
308    next: i32,
309}
310
311impl Grid {
312    /// Create a new grid with the given cell size and column count.
313    pub fn new(cols: i32, cell_w: i32, cell_h: i32) -> Self {
314        Self {
315            bounds: Rect {
316                x: 0,
317                y: 0,
318                width: 0,
319                height: 0,
320            },
321            cols,
322            cell_w,
323            cell_h,
324            spacing: 0,
325            children: Vec::new(),
326            next: 0,
327        }
328    }
329
330    /// Set the spacing between grid cells.
331    pub fn spacing(mut self, spacing: i32) -> Self {
332        self.spacing = spacing;
333        self
334    }
335
336    /// Set the gap between grid cells.
337    pub fn gap(self, gap: i32) -> Self {
338        self.spacing(gap)
339    }
340
341    /// Add a child placed in the next grid cell.
342    pub fn child<W, F>(mut self, builder: F) -> Self
343    where
344        W: Widget + 'static,
345        F: FnOnce(Rect) -> W,
346    {
347        // Guard a misconfigured zero/negative column count: degenerate to a
348        // single column instead of panicking on `% 0` / `/ 0`. Mirrors the
349        // `cols == 0` tolerance in `GridCalc`.
350        let cols = self.cols.max(1);
351        let col = self.next % cols;
352        let row = self.next / cols;
353        let x = col * (self.cell_w + self.spacing);
354        let y = row * (self.cell_h + self.spacing);
355        let rect = Rect {
356            x,
357            y,
358            width: self.cell_w,
359            height: self.cell_h,
360        };
361        self.children.push(Box::new(builder(rect)));
362        self.next += 1;
363        let w = x + self.cell_w;
364        let h = y + self.cell_h;
365        if w > self.bounds.width {
366            self.bounds.width = w;
367        }
368        if h > self.bounds.height {
369            self.bounds.height = h;
370        }
371        self
372    }
373}
374
375impl Widget for Grid {
376    fn bounds(&self) -> Rect {
377        self.bounds
378    }
379
380    fn draw(&self, renderer: &mut dyn Renderer) {
381        for child in &self.children {
382            child.draw(renderer);
383        }
384    }
385
386    fn handle_event(&mut self, event: &Event) -> bool {
387        for child in &mut self.children {
388            if child.handle_event(event) {
389                return true;
390            }
391        }
392        false
393    }
394}
395
396/// Generic container box that wraps the base `Container` widget.
397pub struct BoxLayout {
398    inner: Container,
399}
400
401impl BoxLayout {
402    /// Create a new box with the provided bounds.
403    pub fn new(bounds: Rect) -> Self {
404        Self {
405            inner: Container::new(bounds),
406        }
407    }
408
409    /// Mutable access to the inner style.
410    pub fn style_mut(&mut self) -> &mut rlvgl_core::style::Style {
411        &mut self.inner.style
412    }
413}
414
415impl Widget for BoxLayout {
416    fn bounds(&self) -> Rect {
417        self.inner.bounds()
418    }
419
420    fn draw(&self, renderer: &mut dyn Renderer) {
421        self.inner.draw(renderer);
422    }
423
424    fn handle_event(&mut self, event: &Event) -> bool {
425        self.inner.handle_event(event)
426    }
427}
428
429/// Pure-geometry grid calculator for manual widget layouts.
430///
431/// Unlike [`Grid`], this does not own widgets — it only computes
432/// cell [`Rect`]s from `(row, col)` indices, making it suitable for
433/// custom widgets that do their own drawing and hit-testing.
434///
435/// ```
436/// # use rlvgl_core::widget::Rect;
437/// # use rlvgl_ui::layout::GridCalc;
438/// let g = GridCalc::new(10, 20, 2, 100, 40).gap(4, 2);
439/// let r = g.cell(1, 0);
440/// assert_eq!(r, Rect { x: 10, y: 62, width: 100, height: 40 });
441/// ```
442pub struct GridCalc {
443    /// Top-left origin X.
444    pub x: i32,
445    /// Top-left origin Y.
446    pub y: i32,
447    /// Number of columns.
448    pub cols: usize,
449    /// Width of each cell.
450    pub col_w: i32,
451    /// Height of each cell.
452    pub row_h: i32,
453    /// Horizontal gap between columns.
454    pub col_gap: i32,
455    /// Vertical gap between rows.
456    pub row_gap: i32,
457}
458
459impl GridCalc {
460    /// Create a grid calculator with the given origin, column count, and cell size.
461    pub const fn new(x: i32, y: i32, cols: usize, col_w: i32, row_h: i32) -> Self {
462        Self {
463            x,
464            y,
465            cols,
466            col_w,
467            row_h,
468            col_gap: 0,
469            row_gap: 0,
470        }
471    }
472
473    /// Set inter-cell gaps.
474    pub const fn gap(mut self, col_gap: i32, row_gap: i32) -> Self {
475        self.col_gap = col_gap;
476        self.row_gap = row_gap;
477        self
478    }
479
480    /// Return the `Rect` for the cell at `(row, col)`.
481    pub const fn cell(&self, row: usize, col: usize) -> Rect {
482        Rect {
483            x: self.x + col as i32 * (self.col_w + self.col_gap),
484            y: self.y + row as i32 * (self.row_h + self.row_gap),
485            width: self.col_w,
486            height: self.row_h,
487        }
488    }
489
490    /// Return a `Rect` spanning all columns for the given `row`.
491    pub const fn row_span(&self, row: usize) -> Rect {
492        let total_w = if self.cols == 0 {
493            0
494        } else {
495            self.cols as i32 * self.col_w + (self.cols as i32 - 1) * self.col_gap
496        };
497        Rect {
498            x: self.x,
499            y: self.y + row as i32 * (self.row_h + self.row_gap),
500            width: total_w,
501            height: self.row_h,
502        }
503    }
504
505    /// Total width of the grid (all columns + gaps).
506    pub const fn total_width(&self) -> i32 {
507        if self.cols == 0 {
508            0
509        } else {
510            self.cols as i32 * self.col_w + (self.cols as i32 - 1) * self.col_gap
511        }
512    }
513
514    /// Total height of the grid for the given number of rows.
515    pub const fn total_height(&self, rows: usize) -> i32 {
516        if rows == 0 {
517            0
518        } else {
519            rows as i32 * self.row_h + (rows as i32 - 1) * self.row_gap
520        }
521    }
522}
523
524#[cfg(test)]
525mod tests {
526    use super::*;
527    use rlvgl_widgets::label::Label;
528
529    #[test]
530    fn rect_helpers_construct_and_transform_rects() {
531        let r = origin_rect(10, 20)
532            .at(2, 3)
533            .width(30)
534            .height(40)
535            .translate(5, -1)
536            .inset(2)
537            .min_size(40, 50);
538
539        assert_eq!(
540            r,
541            Rect {
542                x: 9,
543                y: 4,
544                width: 40,
545                height: 50
546            }
547        );
548    }
549
550    #[test]
551    fn rect_inset_clamps_to_zero_size() {
552        assert_eq!(rect(0, 0, 3, 3).inset(4).width, 0);
553        assert_eq!(rect(0, 0, 3, 3).inset(4).height, 0);
554    }
555
556    #[test]
557    fn vstack_stacks_vertically() {
558        let stack = VStack::new(20)
559            .spacing(2)
560            .child(10, |r| Label::new("a", r))
561            .child(10, |r| Label::new("b", r));
562        assert_eq!(stack.bounds().height, 22);
563    }
564
565    #[test]
566    fn hstack_stacks_horizontally() {
567        let stack = HStack::new(10)
568            .spacing(1)
569            .child(5, |r| Label::new("a", r))
570            .child(5, |r| Label::new("b", r));
571        assert_eq!(stack.bounds().width, 11);
572    }
573
574    #[test]
575    fn grid_places_cells() {
576        let grid = Grid::new(2, 5, 5)
577            .spacing(1)
578            .child(|r| Label::new("a", r))
579            .child(|r| Label::new("b", r))
580            .child(|r| Label::new("c", r));
581        assert_eq!(grid.bounds().height, 11);
582        assert_eq!(grid.bounds().width, 11);
583    }
584
585    #[test]
586    fn grid_zero_cols_does_not_panic() {
587        // Regression: `Grid::child` divided/modulo'd by `self.cols`, panicking
588        // on a zero-column grid. It now degenerates to a single column.
589        let grid = Grid::new(0, 5, 5)
590            .child(|r| Label::new("a", r))
591            .child(|r| Label::new("b", r));
592        assert_eq!(grid.bounds().height, 10);
593    }
594
595    #[test]
596    fn grid_calc_cell() {
597        let g = GridCalc::new(10, 20, 2, 100, 40).gap(4, 2);
598        let r = g.cell(0, 0);
599        assert_eq!(
600            r,
601            Rect {
602                x: 10,
603                y: 20,
604                width: 100,
605                height: 40
606            }
607        );
608        let r = g.cell(1, 1);
609        assert_eq!(
610            r,
611            Rect {
612                x: 114,
613                y: 62,
614                width: 100,
615                height: 40
616            }
617        );
618    }
619
620    #[test]
621    fn grid_calc_row_span() {
622        let g = GridCalc::new(0, 0, 3, 50, 30).gap(10, 5);
623        let r = g.row_span(0);
624        assert_eq!(
625            r,
626            Rect {
627                x: 0,
628                y: 0,
629                width: 170,
630                height: 30
631            }
632        );
633        assert_eq!(g.total_width(), 170);
634        assert_eq!(g.total_height(2), 65);
635    }
636}