Skip to main content

rlvgl_ui/
layout.rs

1// SPDX-License-Identifier: MIT
2//! Basic layout helpers for arranging [`Widget`](rlvgl_core::widget::Widget)
3//! instances from [`rlvgl-widgets`](rlvgl_widgets).
4//!
5//! Provides vertical and horizontal stacks, a simple grid, and a box wrapper.
6
7use alloc::{boxed::Box, vec::Vec};
8use rlvgl_core::{
9    event::Event,
10    renderer::Renderer,
11    widget::{Rect, Widget},
12};
13use rlvgl_widgets::container::Container;
14
15/// Container that positions children vertically.
16///
17/// Accepts any [`Widget`](rlvgl_core::widget::Widget) from
18/// [`rlvgl_widgets`](rlvgl_widgets) and arranges them top-to-bottom.
19pub struct VStack {
20    bounds: Rect,
21    spacing: i32,
22    children: Vec<Box<dyn Widget>>,
23    next_y: i32,
24}
25
26impl VStack {
27    /// Create an empty vertical stack with the given width.
28    pub fn new(width: i32) -> Self {
29        Self {
30            bounds: Rect {
31                x: 0,
32                y: 0,
33                width,
34                height: 0,
35            },
36            spacing: 0,
37            children: Vec::new(),
38            next_y: 0,
39        }
40    }
41
42    /// Set the spacing between stacked children.
43    pub fn spacing(mut self, spacing: i32) -> Self {
44        self.spacing = spacing;
45        self
46    }
47
48    /// Add a child of the given height, created by the supplied builder.
49    pub fn child<W, F>(mut self, height: i32, builder: F) -> Self
50    where
51        W: Widget + 'static,
52        F: FnOnce(Rect) -> W,
53    {
54        let rect = Rect {
55            x: 0,
56            y: self.next_y,
57            width: self.bounds.width,
58            height,
59        };
60        self.next_y += height + self.spacing;
61        self.bounds.height = self.next_y - self.spacing;
62        self.children.push(Box::new(builder(rect)));
63        self
64    }
65}
66
67impl Widget for VStack {
68    fn bounds(&self) -> Rect {
69        self.bounds
70    }
71
72    fn draw(&self, renderer: &mut dyn Renderer) {
73        for child in &self.children {
74            child.draw(renderer);
75        }
76    }
77
78    fn handle_event(&mut self, event: &Event) -> bool {
79        for child in &mut self.children {
80            if child.handle_event(event) {
81                return true;
82            }
83        }
84        false
85    }
86}
87
88/// Container that positions children horizontally.
89///
90/// Like [`VStack`](crate::layout::VStack), this operates on
91/// [`Widget`](rlvgl_core::widget::Widget) instances from
92/// [`rlvgl_widgets`](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#[cfg(test)]
283mod tests {
284    use super::*;
285    use rlvgl_widgets::label::Label;
286
287    #[test]
288    fn vstack_stacks_vertically() {
289        let stack = VStack::new(20)
290            .spacing(2)
291            .child(10, |r| Label::new("a", r))
292            .child(10, |r| Label::new("b", r));
293        assert_eq!(stack.bounds().height, 22);
294    }
295
296    #[test]
297    fn hstack_stacks_horizontally() {
298        let stack = HStack::new(10)
299            .spacing(1)
300            .child(5, |r| Label::new("a", r))
301            .child(5, |r| Label::new("b", r));
302        assert_eq!(stack.bounds().width, 11);
303    }
304
305    #[test]
306    fn grid_places_cells() {
307        let grid = Grid::new(2, 5, 5)
308            .spacing(1)
309            .child(|r| Label::new("a", r))
310            .child(|r| Label::new("b", r))
311            .child(|r| Label::new("c", r));
312        assert_eq!(grid.bounds().height, 11);
313        assert_eq!(grid.bounds().width, 11);
314    }
315}