egui_material3/
layoutgrid.rs

1use crate::theme::get_global_color;
2use egui::{
3    epaint::CornerRadius,
4    Rect, Response, Sense, Ui, Vec2, Widget,
5};
6
7/// Material Design layout grid component.
8///
9/// Layout grids provide structure and organize content across multiple screen sizes.
10/// They help create consistent layouts following Material Design principles.
11///
12/// ```
13/// # egui::__run_test_ui(|ui| {
14/// let grid = MaterialLayoutGrid::new()
15///     .columns(12)
16///     .gutter(16.0)
17///     .margin(24.0)
18///     .cell(4, |ui| { ui.label("Column 1-4"); })
19///     .cell(4, |ui| { ui.label("Column 5-8"); })
20///     .cell(4, |ui| { ui.label("Column 9-12"); });
21///
22/// ui.add(grid);
23/// # });
24/// ```
25#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
26pub struct MaterialLayoutGrid<'a> {
27    cells: Vec<GridCell<'a>>,
28    columns: usize,
29    gutter: f32,
30    margin: f32,
31    max_width: Option<f32>,
32    debug_mode: bool,
33}
34
35struct GridCell<'a> {
36    span: usize,
37    content: Box<dyn FnOnce(&mut Ui) -> Response + 'a>,
38    offset: Option<usize>,
39}
40
41impl<'a> MaterialLayoutGrid<'a> {
42    /// Create a new layout grid.
43    pub fn new() -> Self {
44        Self {
45            cells: Vec::new(),
46            columns: 12, // Standard 12-column grid
47            gutter: 16.0, // Standard gutter size
48            margin: 24.0, // Standard margin
49            max_width: None,
50            debug_mode: false,
51        }
52    }
53
54    /// Set the number of columns.
55    pub fn columns(mut self, columns: usize) -> Self {
56        self.columns = columns.max(1);
57        self
58    }
59
60    /// Set the gutter size (space between columns).
61    pub fn gutter(mut self, gutter: f32) -> Self {
62        self.gutter = gutter;
63        self
64    }
65
66    /// Set the margin (space around the grid).
67    pub fn margin(mut self, margin: f32) -> Self {
68        self.margin = margin;
69        self
70    }
71
72    /// Set maximum width of the grid.
73    pub fn max_width(mut self, max_width: f32) -> Self {
74        self.max_width = Some(max_width);
75        self
76    }
77
78    /// Enable debug mode to visualize grid structure.
79    pub fn debug_mode(mut self, debug: bool) -> Self {
80        self.debug_mode = debug;
81        self
82    }
83
84    /// Add a cell that spans the specified number of columns.
85    pub fn cell<F>(mut self, span: usize, content: F) -> Self 
86    where
87        F: FnOnce(&mut Ui) + 'a,
88    {
89        self.cells.push(GridCell {
90            span: span.clamp(1, self.columns),
91            content: Box::new(move |ui| {
92                content(ui);
93                ui.allocate_response(Vec2::ZERO, Sense::hover())
94            }),
95            offset: None,
96        });
97        self
98    }
99
100    /// Add a cell with an offset (empty columns before this cell).
101    pub fn cell_with_offset<F>(mut self, span: usize, offset: usize, content: F) -> Self 
102    where
103        F: FnOnce(&mut Ui) + 'a,
104    {
105        self.cells.push(GridCell {
106            span: span.clamp(1, self.columns),
107            content: Box::new(move |ui| {
108                content(ui);
109                ui.allocate_response(Vec2::ZERO, Sense::hover())
110            }),
111            offset: Some(offset),
112        });
113        self
114    }
115
116    /// Add an empty cell (spacer).
117    pub fn spacer(mut self, span: usize) -> Self {
118        self.cells.push(GridCell {
119            span: span.clamp(1, self.columns),
120            content: Box::new(|ui| ui.allocate_response(Vec2::ZERO, Sense::hover())),
121            offset: None,
122        });
123        self
124    }
125
126    fn calculate_column_width(&self, available_width: f32) -> f32 {
127        let effective_width = if let Some(max_width) = self.max_width {
128            available_width.min(max_width)
129        } else {
130            available_width
131        };
132        
133        let total_gutter_width = (self.columns - 1) as f32 * self.gutter;
134        let content_width = effective_width - 2.0 * self.margin - total_gutter_width;
135        content_width / self.columns as f32
136    }
137}
138
139impl<'a> Default for MaterialLayoutGrid<'a> {
140    fn default() -> Self {
141        Self::new()
142    }
143}
144
145impl Widget for MaterialLayoutGrid<'_> {
146    fn ui(self, ui: &mut Ui) -> Response {
147        let available_width = ui.available_width();
148        let column_width = self.calculate_column_width(available_width);
149        
150        let MaterialLayoutGrid {
151            cells,
152            columns,
153            gutter,
154            margin,
155            max_width,
156            debug_mode,
157        } = self;
158
159        if cells.is_empty() {
160            return ui.allocate_response(Vec2::ZERO, Sense::hover());
161        }
162
163        let effective_width = if let Some(max_width) = max_width {
164            available_width.min(max_width)
165        } else {
166            available_width
167        };
168        
169        // Start layout
170        let start_pos = ui.next_widget_position();
171        let mut current_row_y = start_pos.y + margin;
172        let mut current_column = 0;
173        let mut row_height: f32 = 0.0;
174        let mut max_y = current_row_y;
175        
176        let mut responses = Vec::new();
177
178        // Process each cell
179        for cell in cells {
180            // Handle offset
181            if let Some(offset) = cell.offset {
182                current_column += offset;
183            }
184            
185            // Check if we need to wrap to next row
186            if current_column + cell.span > columns {
187                current_row_y = max_y + gutter;
188                current_column = 0;
189                row_height = 0.0;
190            }
191            
192            // Calculate cell position and size
193            let cell_x = start_pos.x + margin + current_column as f32 * (column_width + gutter);
194            let cell_width = cell.span as f32 * column_width + (cell.span - 1) as f32 * gutter;
195            
196            // Create a constrained UI for this cell
197            let cell_rect = Rect::from_min_size(
198                egui::pos2(cell_x, current_row_y),
199                Vec2::new(cell_width, ui.available_height())
200            );
201            
202            let cell_response = ui.scope_builder(
203                egui::UiBuilder::new().max_rect(cell_rect),
204                |ui| {
205                    // Debug visualization
206                    if debug_mode {
207                        let debug_color = get_global_color("primary").linear_multiply(0.12);
208                        ui.painter().rect_filled(
209                            cell_rect,
210                            CornerRadius::from(2.0),
211                            debug_color
212                        );
213                    }
214                    
215                    (cell.content)(ui)
216                }
217            );
218            
219            let cell_height = cell_response.response.rect.height();
220            row_height = row_height.max(cell_height);
221            max_y = max_y.max(current_row_y + row_height);
222            
223            responses.push(cell_response.response);
224            current_column += cell.span;
225        }
226        
227        // Calculate total grid size
228        let total_height = max_y - start_pos.y + margin;
229        let grid_rect = Rect::from_min_size(start_pos, Vec2::new(effective_width, total_height));
230        
231        // Debug: Draw grid outline
232        if debug_mode {
233            let outline_color = get_global_color("primary");
234            ui.painter().rect_stroke(
235                grid_rect,
236                CornerRadius::from(4.0),
237                egui::epaint::Stroke::new(1.0, outline_color),
238                egui::epaint::StrokeKind::Outside
239            );
240            
241            // Draw column guides
242            for i in 0..=columns {
243                let x = start_pos.x + margin + i as f32 * (column_width + gutter) - gutter / 2.0;
244                if i > 0 && i < columns {
245                    ui.painter().line_segment(
246                        [
247                            egui::pos2(x, start_pos.y + margin),
248                            egui::pos2(x, max_y)
249                        ],
250                        egui::epaint::Stroke::new(0.5, get_global_color("outlineVariant"))
251                    );
252                }
253            }
254        }
255        
256        // Allocate the full grid space
257        let (_grid_response_rect, mut grid_response) = ui.allocate_at_least(
258            Vec2::new(effective_width, total_height),
259            Sense::hover()
260        );
261        
262        // Union all cell responses
263        for response in responses {
264            grid_response = grid_response.union(response);
265        }
266        
267        grid_response
268    }
269}
270
271/// Convenience function to create a new layout grid.
272pub fn layout_grid() -> MaterialLayoutGrid<'static> {
273    MaterialLayoutGrid::new()
274}
275
276/// Convenience function to create a layout grid with debug mode enabled.
277pub fn debug_layout_grid() -> MaterialLayoutGrid<'static> {
278    MaterialLayoutGrid::new().debug_mode(true)
279}