1use crate::Theme;
6use egui::{Color32, Pos2, Rect, Sense, Stroke, Ui, Vec2};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum GridSpan {
11 Single,
13 Wide,
15 Tall,
17 Large,
19}
20
21impl GridSpan {
22 const fn columns(self) -> usize {
23 match self {
24 Self::Single | Self::Tall => 1,
25 Self::Wide | Self::Large => 2,
26 }
27 }
28
29 const fn rows(self) -> usize {
30 match self {
31 Self::Single | Self::Wide => 1,
32 Self::Tall | Self::Large => 2,
33 }
34 }
35}
36
37pub struct BentoGrid {
57 columns: usize,
58 cell_size: f32,
59 gap: f32,
60 corner_radius: f32,
61 padding: f32,
62}
63
64impl Default for BentoGrid {
65 fn default() -> Self {
66 Self::new()
67 }
68}
69
70impl BentoGrid {
71 #[must_use]
73 pub const fn new() -> Self {
74 Self {
75 columns: 3,
76 cell_size: 120.0,
77 gap: 12.0,
78 corner_radius: 12.0,
79 padding: 16.0,
80 }
81 }
82
83 #[must_use]
85 pub fn columns(mut self, columns: usize) -> Self {
86 self.columns = columns.max(1);
87 self
88 }
89
90 #[must_use]
92 pub const fn cell_size(mut self, size: f32) -> Self {
93 self.cell_size = size.max(50.0);
94 self
95 }
96
97 #[must_use]
99 pub const fn gap(mut self, gap: f32) -> Self {
100 self.gap = gap;
101 self
102 }
103
104 #[must_use]
106 pub const fn corner_radius(mut self, radius: f32) -> Self {
107 self.corner_radius = radius;
108 self
109 }
110
111 #[must_use]
113 pub const fn padding(mut self, padding: f32) -> Self {
114 self.padding = padding;
115 self
116 }
117
118 pub fn show<R>(self, ui: &mut Ui, content: impl FnOnce(&mut GridBuilder) -> R) -> R {
120 let theme = ui.ctx().data(|d| {
121 d.get_temp::<Theme>(egui::Id::new("armas_theme"))
122 .unwrap_or_else(Theme::dark)
123 });
124
125 ui.vertical(|ui| {
126 let start_pos = ui.cursor().min;
128
129 let mut builder = GridBuilder {
130 ui,
131 theme: &theme,
132 columns: self.columns,
133 cell_size: self.cell_size,
134 gap: self.gap,
135 corner_radius: self.corner_radius,
136 padding: self.padding,
137 grid_start_pos: start_pos,
138 current_col: 0,
139 current_row: 0,
140 occupied: Vec::new(),
141 };
142
143 let result = content(&mut builder);
144
145 let max_row = builder.occupied.len();
147 let total_height = if max_row > 0 {
148 max_row as f32 * self.cell_size + (max_row - 1) as f32 * self.gap
149 } else {
150 0.0
151 };
152 let grid_width =
153 self.columns as f32 * self.cell_size + (self.columns - 1) as f32 * self.gap;
154 ui.allocate_space(Vec2::new(grid_width, total_height));
155
156 result
157 })
158 .inner
159 }
160}
161
162pub struct GridBuilder<'a> {
163 ui: &'a mut Ui,
164 theme: &'a Theme,
165 columns: usize,
166 cell_size: f32,
167 gap: f32,
168 corner_radius: f32,
169 padding: f32,
170 grid_start_pos: Pos2,
171 current_col: usize,
172 current_row: usize,
173 occupied: Vec<Vec<usize>>,
175}
176
177impl GridBuilder<'_> {
178 fn is_occupied(&self, row: usize, col: usize) -> bool {
180 if row >= self.occupied.len() {
181 return false;
182 }
183 if col >= self.occupied[row].len() {
184 return false;
185 }
186 self.occupied[row][col] > 0
187 }
188
189 fn mark_occupied(&mut self, row: usize, col: usize, cols: usize, rows: usize) {
191 while self.occupied.len() <= row + rows {
193 self.occupied.push(vec![0; self.columns]);
194 }
195
196 for r in row..row + rows {
198 for c in col..col + cols {
199 if c < self.columns {
200 self.occupied[r][c] = rows;
201 }
202 }
203 }
204 }
205
206 fn find_next_position(&mut self, cols: usize, rows: usize) {
208 loop {
209 let mut can_fit = self.current_col + cols <= self.columns;
211
212 if can_fit {
213 for r in 0..rows {
215 for c in 0..cols {
216 if self.is_occupied(self.current_row + r, self.current_col + c) {
217 can_fit = false;
218 break;
219 }
220 }
221 if !can_fit {
222 break;
223 }
224 }
225 }
226
227 if can_fit {
228 return;
229 }
230
231 self.current_col += 1;
233 if self.current_col >= self.columns {
234 self.current_col = 0;
235 self.current_row += 1;
236 }
237 }
238 }
239
240 pub fn item<R>(&mut self, span: GridSpan, content: impl FnOnce(&mut Ui) -> R) -> R {
241 self.item_with_style(span, None, None, content)
242 }
243
244 pub fn item_with_background<R>(
245 &mut self,
246 span: GridSpan,
247 background: Color32,
248 content: impl FnOnce(&mut Ui) -> R,
249 ) -> R {
250 self.item_with_style(span, Some(background), None, content)
251 }
252
253 pub fn item_with_style<R>(
254 &mut self,
255 span: GridSpan,
256 background: Option<Color32>,
257 border: Option<Color32>,
258 content: impl FnOnce(&mut Ui) -> R,
259 ) -> R {
260 let cols = span.columns();
261 let rows = span.rows();
262
263 self.find_next_position(cols, rows);
265
266 let x = self.grid_start_pos.x + self.current_col as f32 * (self.cell_size + self.gap);
268 let y = self.grid_start_pos.y + self.current_row as f32 * (self.cell_size + self.gap);
269
270 let width = cols as f32 * self.cell_size + (cols - 1) as f32 * self.gap;
271 let height = rows as f32 * self.cell_size + (rows - 1) as f32 * self.gap;
272
273 let rect = Rect::from_min_size(Pos2::new(x, y), Vec2::new(width, height));
274
275 let painter = self.ui.painter();
277 let bg_color = background.unwrap_or_else(|| self.theme.card());
278 let border_color = border.or_else(|| Some(self.theme.border()));
279
280 painter.rect_filled(rect, self.corner_radius, bg_color);
281
282 if let Some(border) = border_color {
283 painter.rect_stroke(
284 rect,
285 self.corner_radius,
286 Stroke::new(1.0, border),
287 egui::StrokeKind::Outside,
288 );
289 }
290
291 let content_rect = rect.shrink(self.padding);
293 let result = self
294 .ui
295 .scope_builder(egui::UiBuilder::new().max_rect(content_rect), |ui| {
296 content(ui)
297 })
298 .inner;
299
300 let _response = self.ui.interact(
302 rect,
303 self.ui.id().with((self.current_col, self.current_row)),
304 Sense::hover(),
305 );
306
307 self.mark_occupied(self.current_row, self.current_col, cols, rows);
309
310 self.current_col += cols;
312 if self.current_col >= self.columns {
313 self.current_col = 0;
314 self.current_row += 1;
315 }
316
317 result
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 #[test]
326 fn test_grid_span() {
327 assert_eq!(GridSpan::Single.columns(), 1);
328 assert_eq!(GridSpan::Single.rows(), 1);
329 assert_eq!(GridSpan::Wide.columns(), 2);
330 assert_eq!(GridSpan::Wide.rows(), 1);
331 assert_eq!(GridSpan::Tall.columns(), 1);
332 assert_eq!(GridSpan::Tall.rows(), 2);
333 assert_eq!(GridSpan::Large.columns(), 2);
334 assert_eq!(GridSpan::Large.rows(), 2);
335 }
336
337 #[test]
338 fn test_bento_grid_creation() {
339 let grid = BentoGrid::new().columns(3).cell_size(100.0);
340 assert_eq!(grid.columns, 3);
341 assert_eq!(grid.cell_size, 100.0);
342 }
343
344 #[test]
345 fn test_bento_grid_config() {
346 let grid = BentoGrid::new().gap(16.0).corner_radius(8.0);
347 assert_eq!(grid.gap, 16.0);
348 assert_eq!(grid.corner_radius, 8.0);
349 }
350}