egui_material3/
layoutgrid.rs1use crate::theme::get_global_color;
2use egui::{
3 epaint::CornerRadius,
4 Rect, Response, Sense, Ui, Vec2, Widget,
5};
6
7#[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 pub fn new() -> Self {
44 Self {
45 cells: Vec::new(),
46 columns: 12, gutter: 16.0, margin: 24.0, max_width: None,
50 debug_mode: false,
51 }
52 }
53
54 pub fn columns(mut self, columns: usize) -> Self {
56 self.columns = columns.max(1);
57 self
58 }
59
60 pub fn gutter(mut self, gutter: f32) -> Self {
62 self.gutter = gutter;
63 self
64 }
65
66 pub fn margin(mut self, margin: f32) -> Self {
68 self.margin = margin;
69 self
70 }
71
72 pub fn max_width(mut self, max_width: f32) -> Self {
74 self.max_width = Some(max_width);
75 self
76 }
77
78 pub fn debug_mode(mut self, debug: bool) -> Self {
80 self.debug_mode = debug;
81 self
82 }
83
84 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 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 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 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 for cell in cells {
180 if let Some(offset) = cell.offset {
182 current_column += offset;
183 }
184
185 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 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 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 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 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 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 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 let (_grid_response_rect, mut grid_response) = ui.allocate_at_least(
258 Vec2::new(effective_width, total_height),
259 Sense::hover()
260 );
261
262 for response in responses {
264 grid_response = grid_response.union(response);
265 }
266
267 grid_response
268 }
269}
270
271pub fn layout_grid() -> MaterialLayoutGrid<'static> {
273 MaterialLayoutGrid::new()
274}
275
276pub fn debug_layout_grid() -> MaterialLayoutGrid<'static> {
278 MaterialLayoutGrid::new().debug_mode(true)
279}