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/// Container that positions children vertically.
17///
18/// Accepts any [`Widget`] from [`rlvgl_widgets`] and arranges them
19/// top-to-bottom.
20pub struct VStack {
21    bounds: Rect,
22    spacing: i32,
23    children: Vec<Box<dyn Widget>>,
24    next_y: i32,
25}
26
27impl VStack {
28    /// Create an empty vertical stack with the given width.
29    pub fn new(width: i32) -> Self {
30        Self {
31            bounds: Rect {
32                x: 0,
33                y: 0,
34                width,
35                height: 0,
36            },
37            spacing: 0,
38            children: Vec::new(),
39            next_y: 0,
40        }
41    }
42
43    /// Set the spacing between stacked children.
44    pub fn spacing(mut self, spacing: i32) -> Self {
45        self.spacing = spacing;
46        self
47    }
48
49    /// Add a child of the given height, created by the supplied builder.
50    pub fn child<W, F>(mut self, height: i32, builder: F) -> Self
51    where
52        W: Widget + 'static,
53        F: FnOnce(Rect) -> W,
54    {
55        let rect = Rect {
56            x: 0,
57            y: self.next_y,
58            width: self.bounds.width,
59            height,
60        };
61        self.next_y += height + self.spacing;
62        self.bounds.height = self.next_y - self.spacing;
63        self.children.push(Box::new(builder(rect)));
64        self
65    }
66}
67
68impl Widget for VStack {
69    fn bounds(&self) -> Rect {
70        self.bounds
71    }
72
73    fn draw(&self, renderer: &mut dyn Renderer) {
74        for child in &self.children {
75            child.draw(renderer);
76        }
77    }
78
79    fn handle_event(&mut self, event: &Event) -> bool {
80        for child in &mut self.children {
81            if child.handle_event(event) {
82                return true;
83            }
84        }
85        false
86    }
87}
88
89/// Container that positions children horizontally.
90///
91/// Like [`VStack`], this operates on [`Widget`] instances from
92/// [`rlvgl_widgets`].
93pub struct HStack {
94    bounds: Rect,
95    spacing: i32,
96    children: Vec<Box<dyn Widget>>,
97    next_x: i32,
98}
99
100impl HStack {
101    /// Create an empty horizontal stack with the given height.
102    pub fn new(height: i32) -> Self {
103        Self {
104            bounds: Rect {
105                x: 0,
106                y: 0,
107                width: 0,
108                height,
109            },
110            spacing: 0,
111            children: Vec::new(),
112            next_x: 0,
113        }
114    }
115
116    /// Set the spacing between stacked children.
117    pub fn spacing(mut self, spacing: i32) -> Self {
118        self.spacing = spacing;
119        self
120    }
121
122    /// Add a child of the given width, created by the supplied builder.
123    pub fn child<W, F>(mut self, width: i32, builder: F) -> Self
124    where
125        W: Widget + 'static,
126        F: FnOnce(Rect) -> W,
127    {
128        let rect = Rect {
129            x: self.next_x,
130            y: 0,
131            width,
132            height: self.bounds.height,
133        };
134        self.next_x += width + self.spacing;
135        self.bounds.width = self.next_x - self.spacing;
136        self.children.push(Box::new(builder(rect)));
137        self
138    }
139}
140
141impl Widget for HStack {
142    fn bounds(&self) -> Rect {
143        self.bounds
144    }
145
146    fn draw(&self, renderer: &mut dyn Renderer) {
147        for child in &self.children {
148            child.draw(renderer);
149        }
150    }
151
152    fn handle_event(&mut self, event: &Event) -> bool {
153        for child in &mut self.children {
154            if child.handle_event(event) {
155                return true;
156            }
157        }
158        false
159    }
160}
161
162/// Simple grid container placing widgets in fixed-size cells.
163pub struct Grid {
164    bounds: Rect,
165    cols: i32,
166    cell_w: i32,
167    cell_h: i32,
168    spacing: i32,
169    children: Vec<Box<dyn Widget>>,
170    next: i32,
171}
172
173impl Grid {
174    /// Create a new grid with the given cell size and column count.
175    pub fn new(cols: i32, cell_w: i32, cell_h: i32) -> Self {
176        Self {
177            bounds: Rect {
178                x: 0,
179                y: 0,
180                width: 0,
181                height: 0,
182            },
183            cols,
184            cell_w,
185            cell_h,
186            spacing: 0,
187            children: Vec::new(),
188            next: 0,
189        }
190    }
191
192    /// Set the spacing between grid cells.
193    pub fn spacing(mut self, spacing: i32) -> Self {
194        self.spacing = spacing;
195        self
196    }
197
198    /// Add a child placed in the next grid cell.
199    pub fn child<W, F>(mut self, builder: F) -> Self
200    where
201        W: Widget + 'static,
202        F: FnOnce(Rect) -> W,
203    {
204        let col = self.next % self.cols;
205        let row = self.next / self.cols;
206        let x = col * (self.cell_w + self.spacing);
207        let y = row * (self.cell_h + self.spacing);
208        let rect = Rect {
209            x,
210            y,
211            width: self.cell_w,
212            height: self.cell_h,
213        };
214        self.children.push(Box::new(builder(rect)));
215        self.next += 1;
216        let w = x + self.cell_w;
217        let h = y + self.cell_h;
218        if w > self.bounds.width {
219            self.bounds.width = w;
220        }
221        if h > self.bounds.height {
222            self.bounds.height = h;
223        }
224        self
225    }
226}
227
228impl Widget for Grid {
229    fn bounds(&self) -> Rect {
230        self.bounds
231    }
232
233    fn draw(&self, renderer: &mut dyn Renderer) {
234        for child in &self.children {
235            child.draw(renderer);
236        }
237    }
238
239    fn handle_event(&mut self, event: &Event) -> bool {
240        for child in &mut self.children {
241            if child.handle_event(event) {
242                return true;
243            }
244        }
245        false
246    }
247}
248
249/// Generic container box that wraps the base `Container` widget.
250pub struct BoxLayout {
251    inner: Container,
252}
253
254impl BoxLayout {
255    /// Create a new box with the provided bounds.
256    pub fn new(bounds: Rect) -> Self {
257        Self {
258            inner: Container::new(bounds),
259        }
260    }
261
262    /// Mutable access to the inner style.
263    pub fn style_mut(&mut self) -> &mut rlvgl_core::style::Style {
264        &mut self.inner.style
265    }
266}
267
268impl Widget for BoxLayout {
269    fn bounds(&self) -> Rect {
270        self.inner.bounds()
271    }
272
273    fn draw(&self, renderer: &mut dyn Renderer) {
274        self.inner.draw(renderer);
275    }
276
277    fn handle_event(&mut self, event: &Event) -> bool {
278        self.inner.handle_event(event)
279    }
280}
281
282/// Pure-geometry grid calculator for manual widget layouts.
283///
284/// Unlike [`Grid`], this does not own widgets — it only computes
285/// cell [`Rect`]s from `(row, col)` indices, making it suitable for
286/// custom widgets that do their own drawing and hit-testing.
287///
288/// ```
289/// # use rlvgl_core::widget::Rect;
290/// # use rlvgl_ui::layout::GridCalc;
291/// let g = GridCalc::new(10, 20, 2, 100, 40).gap(4, 2);
292/// let r = g.cell(1, 0);
293/// assert_eq!(r, Rect { x: 10, y: 62, width: 100, height: 40 });
294/// ```
295pub struct GridCalc {
296    /// Top-left origin X.
297    pub x: i32,
298    /// Top-left origin Y.
299    pub y: i32,
300    /// Number of columns.
301    pub cols: usize,
302    /// Width of each cell.
303    pub col_w: i32,
304    /// Height of each cell.
305    pub row_h: i32,
306    /// Horizontal gap between columns.
307    pub col_gap: i32,
308    /// Vertical gap between rows.
309    pub row_gap: i32,
310}
311
312impl GridCalc {
313    /// Create a grid calculator with the given origin, column count, and cell size.
314    pub const fn new(x: i32, y: i32, cols: usize, col_w: i32, row_h: i32) -> Self {
315        Self {
316            x,
317            y,
318            cols,
319            col_w,
320            row_h,
321            col_gap: 0,
322            row_gap: 0,
323        }
324    }
325
326    /// Set inter-cell gaps.
327    pub const fn gap(mut self, col_gap: i32, row_gap: i32) -> Self {
328        self.col_gap = col_gap;
329        self.row_gap = row_gap;
330        self
331    }
332
333    /// Return the `Rect` for the cell at `(row, col)`.
334    pub const fn cell(&self, row: usize, col: usize) -> Rect {
335        Rect {
336            x: self.x + col as i32 * (self.col_w + self.col_gap),
337            y: self.y + row as i32 * (self.row_h + self.row_gap),
338            width: self.col_w,
339            height: self.row_h,
340        }
341    }
342
343    /// Return a `Rect` spanning all columns for the given `row`.
344    pub const fn row_span(&self, row: usize) -> Rect {
345        let total_w = if self.cols == 0 {
346            0
347        } else {
348            self.cols as i32 * self.col_w + (self.cols as i32 - 1) * self.col_gap
349        };
350        Rect {
351            x: self.x,
352            y: self.y + row as i32 * (self.row_h + self.row_gap),
353            width: total_w,
354            height: self.row_h,
355        }
356    }
357
358    /// Total width of the grid (all columns + gaps).
359    pub const fn total_width(&self) -> i32 {
360        if self.cols == 0 {
361            0
362        } else {
363            self.cols as i32 * self.col_w + (self.cols as i32 - 1) * self.col_gap
364        }
365    }
366
367    /// Total height of the grid for the given number of rows.
368    pub const fn total_height(&self, rows: usize) -> i32 {
369        if rows == 0 {
370            0
371        } else {
372            rows as i32 * self.row_h + (rows as i32 - 1) * self.row_gap
373        }
374    }
375}
376
377#[cfg(test)]
378mod tests {
379    use super::*;
380    use rlvgl_widgets::label::Label;
381
382    #[test]
383    fn vstack_stacks_vertically() {
384        let stack = VStack::new(20)
385            .spacing(2)
386            .child(10, |r| Label::new("a", r))
387            .child(10, |r| Label::new("b", r));
388        assert_eq!(stack.bounds().height, 22);
389    }
390
391    #[test]
392    fn hstack_stacks_horizontally() {
393        let stack = HStack::new(10)
394            .spacing(1)
395            .child(5, |r| Label::new("a", r))
396            .child(5, |r| Label::new("b", r));
397        assert_eq!(stack.bounds().width, 11);
398    }
399
400    #[test]
401    fn grid_places_cells() {
402        let grid = Grid::new(2, 5, 5)
403            .spacing(1)
404            .child(|r| Label::new("a", r))
405            .child(|r| Label::new("b", r))
406            .child(|r| Label::new("c", r));
407        assert_eq!(grid.bounds().height, 11);
408        assert_eq!(grid.bounds().width, 11);
409    }
410
411    #[test]
412    fn grid_calc_cell() {
413        let g = GridCalc::new(10, 20, 2, 100, 40).gap(4, 2);
414        let r = g.cell(0, 0);
415        assert_eq!(
416            r,
417            Rect {
418                x: 10,
419                y: 20,
420                width: 100,
421                height: 40
422            }
423        );
424        let r = g.cell(1, 1);
425        assert_eq!(
426            r,
427            Rect {
428                x: 114,
429                y: 62,
430                width: 100,
431                height: 40
432            }
433        );
434    }
435
436    #[test]
437    fn grid_calc_row_span() {
438        let g = GridCalc::new(0, 0, 3, 50, 30).gap(10, 5);
439        let r = g.row_span(0);
440        assert_eq!(
441            r,
442            Rect {
443                x: 0,
444                y: 0,
445                width: 170,
446                height: 30
447            }
448        );
449        assert_eq!(g.total_width(), 170);
450        assert_eq!(g.total_height(2), 65);
451    }
452}