1use crate::graphics::{GraphicsContext, Rectangle};
2use crate::ui::Component;
3use anyhow::Result;
4
5#[derive(Debug, Clone, Copy, PartialEq)]
7pub enum LayoutType {
8 #[allow(dead_code)]
10 Fixed,
11 HorizontalFlow,
13 VerticalFlow,
15 Grid { columns: usize },
17}
18
19#[derive(Debug, Clone, Copy, PartialEq)]
21pub enum Alignment {
22 Start,
23 Center,
24 End,
25}
26
27#[derive(Debug, Clone, Copy)]
29pub struct Spacing {
30 pub top: i16,
31 pub right: i16,
32 pub bottom: i16,
33 pub left: i16,
34}
35
36impl Spacing {
37 pub fn new(top: i16, right: i16, bottom: i16, left: i16) -> Self {
38 Self {
39 top,
40 right,
41 bottom,
42 left,
43 }
44 }
45
46 pub fn uniform(spacing: i16) -> Self {
47 Self::new(spacing, spacing, spacing, spacing)
48 }
49
50 #[allow(dead_code)]
51 pub fn horizontal_vertical(horizontal: i16, vertical: i16) -> Self {
52 Self::new(vertical, horizontal, vertical, horizontal)
53 }
54}
55
56impl Default for Spacing {
57 fn default() -> Self {
58 Self::uniform(0)
59 }
60}
61
62pub struct LayoutItem {
64 pub component: Box<dyn Component>,
65 pub computed_bounds: Rectangle,
66 pub margin: Spacing,
67 pub visible: bool,
68}
69
70impl LayoutItem {
71 pub fn new(component: Box<dyn Component>) -> Self {
72 let bounds = component.bounds();
73 Self {
74 component,
75 computed_bounds: bounds,
76 margin: Spacing::default(),
77 visible: true,
78 }
79 }
80
81 #[allow(dead_code)]
82 pub fn with_margin(mut self, margin: Spacing) -> Self {
83 self.margin = margin;
84 self
85 }
86
87 #[allow(dead_code)]
88 pub fn set_visible(mut self, visible: bool) -> Self {
89 self.visible = visible;
90 self
91 }
92}
93
94pub struct LayoutContainer {
96 layout_type: LayoutType,
97 bounds: Rectangle,
98 items: Vec<LayoutItem>,
99 gap: i16,
100 alignment: Alignment,
101 padding: Spacing,
102 needs_layout: bool,
103}
104
105impl LayoutContainer {
106 pub fn new(layout_type: LayoutType, bounds: Rectangle) -> Self {
107 Self {
108 layout_type,
109 bounds,
110 items: Vec::new(),
111 gap: 0,
112 alignment: Alignment::Start,
113 padding: Spacing::default(),
114 needs_layout: true,
115 }
116 }
117
118 pub fn with_gap(mut self, gap: i16) -> Self {
119 self.gap = gap;
120 self.needs_layout = true;
121 self
122 }
123
124 pub fn with_alignment(mut self, alignment: Alignment) -> Self {
125 self.alignment = alignment;
126 self.needs_layout = true;
127 self
128 }
129
130 pub fn with_padding(mut self, padding: Spacing) -> Self {
131 self.padding = padding;
132 self.needs_layout = true;
133 self
134 }
135
136 pub fn add_item(&mut self, item: LayoutItem) {
137 self.items.push(item);
138 self.needs_layout = true;
139 }
140
141 pub fn add_component(&mut self, component: Box<dyn Component>) {
142 self.add_item(LayoutItem::new(component));
143 }
144
145 #[allow(dead_code)]
146 pub fn set_bounds(&mut self, bounds: Rectangle) {
147 self.bounds = bounds;
148 self.needs_layout = true;
149 }
150
151 pub fn layout(&mut self) -> Result<()> {
152 if !self.needs_layout {
153 return Ok(());
154 }
155
156 let content_bounds = self.calculate_content_bounds();
157
158 match self.layout_type {
159 LayoutType::Fixed => self.layout_fixed(),
160 LayoutType::HorizontalFlow => self.layout_horizontal_flow(content_bounds),
161 LayoutType::VerticalFlow => self.layout_vertical_flow(content_bounds),
162 LayoutType::Grid { columns } => self.layout_grid(content_bounds, columns),
163 }?;
164
165 self.needs_layout = false;
166 Ok(())
167 }
168
169 fn calculate_content_bounds(&self) -> Rectangle {
170 let x = self.bounds.x + self.padding.left;
171 let y = self.bounds.y + self.padding.top;
172 let width = self
173 .bounds
174 .width
175 .saturating_sub((self.padding.left + self.padding.right) as u16);
176 let height = self
177 .bounds
178 .height
179 .saturating_sub((self.padding.top + self.padding.bottom) as u16);
180
181 Rectangle {
182 x,
183 y,
184 width,
185 height,
186 }
187 }
188
189 fn layout_fixed(&mut self) -> Result<()> {
190 for item in &mut self.items {
192 item.computed_bounds = item.component.bounds();
193 }
194 Ok(())
195 }
196
197 fn layout_horizontal_flow(&mut self, content_bounds: Rectangle) -> Result<()> {
198 let mut current_x = content_bounds.x;
199 let alignment = self.alignment;
200
201 for item in &mut self.items {
202 if !item.visible {
203 continue;
204 }
205
206 let original_bounds = item.component.bounds();
207
208 let y = match alignment {
210 Alignment::Start => content_bounds.y + item.margin.top,
211 Alignment::Center => {
212 content_bounds.y
213 + (content_bounds.height as i16 - original_bounds.height as i16) / 2
214 + item.margin.top
215 }
216 Alignment::End => {
217 content_bounds.y + content_bounds.height as i16 - original_bounds.height as i16
218 + item.margin.top
219 }
220 };
221
222 item.computed_bounds = Rectangle {
224 x: current_x + item.margin.left,
225 y,
226 width: original_bounds.width,
227 height: original_bounds.height,
228 };
229
230 current_x +=
231 original_bounds.width as i16 + item.margin.left + item.margin.right + self.gap;
232 }
233 Ok(())
234 }
235
236 fn layout_vertical_flow(&mut self, content_bounds: Rectangle) -> Result<()> {
237 let mut current_y = content_bounds.y;
238 let alignment = self.alignment;
239
240 for item in &mut self.items {
241 if !item.visible {
242 continue;
243 }
244
245 let original_bounds = item.component.bounds();
246
247 let x = match alignment {
249 Alignment::Start => content_bounds.x + item.margin.left,
250 Alignment::Center => {
251 content_bounds.x
252 + (content_bounds.width as i16 - original_bounds.width as i16) / 2
253 + item.margin.left
254 }
255 Alignment::End => {
256 content_bounds.x + content_bounds.width as i16 - original_bounds.width as i16
257 + item.margin.left
258 }
259 };
260
261 item.computed_bounds = Rectangle {
263 x,
264 y: current_y + item.margin.top,
265 width: original_bounds.width,
266 height: original_bounds.height,
267 };
268
269 current_y +=
270 original_bounds.height as i16 + item.margin.top + item.margin.bottom + self.gap;
271 }
272 Ok(())
273 }
274
275 fn layout_grid(&mut self, content_bounds: Rectangle, columns: usize) -> Result<()> {
276 if columns == 0 {
277 return Ok(());
278 }
279
280 let visible_items: Vec<&mut LayoutItem> =
281 self.items.iter_mut().filter(|item| item.visible).collect();
282
283 if visible_items.is_empty() {
284 return Ok(());
285 }
286
287 let rows_needed = visible_items.len().div_ceil(columns);
288 let cell_width = content_bounds.width / columns as u16;
289 let cell_height = if rows_needed > 0 {
290 content_bounds.height / rows_needed as u16
291 } else {
292 content_bounds.height
293 };
294
295 for (index, item) in visible_items.into_iter().enumerate() {
296 let col = index % columns;
297 let row = index / columns;
298
299 let cell_x = content_bounds.x + (col as u16 * cell_width) as i16;
300 let cell_y = content_bounds.y + (row as u16 * cell_height) as i16;
301
302 let original_bounds = item.component.bounds();
303
304 item.computed_bounds = Rectangle {
305 x: cell_x + item.margin.left,
306 y: cell_y + item.margin.top,
307 width: cell_width.min(original_bounds.width),
308 height: cell_height.min(original_bounds.height),
309 };
310 }
311 Ok(())
312 }
313}
314
315impl Component for LayoutContainer {
316 fn render(&self, graphics: &mut GraphicsContext) -> Result<()> {
317 for item in &self.items {
318 if item.visible && item.component.is_visible() {
319 let component_bounds = item.component.bounds();
321 let offset_x = item.computed_bounds.x - component_bounds.x;
322 let offset_y = item.computed_bounds.y - component_bounds.y;
323
324 if let Ok(Some(cairo_ctx)) = graphics.get_cairo_context() {
326 cairo_ctx.save().unwrap();
327 cairo_ctx.translate(offset_x as f64, offset_y as f64);
328 item.component.render(graphics)?;
329 cairo_ctx.restore().unwrap();
330 } else {
331 item.component.render(graphics)?;
333 }
334 }
335 }
336 Ok(())
337 }
338
339 fn bounds(&self) -> Rectangle {
340 self.bounds
341 }
342
343 fn update(&mut self, delta_time: f64) -> bool {
344 let mut needs_redraw = false;
345
346 for item in &mut self.items {
347 if item.component.update(delta_time) {
348 needs_redraw = true;
349 self.needs_layout = true; }
351 }
352
353 if self.needs_layout {
354 let _ = self.layout(); needs_redraw = true;
356 }
357
358 needs_redraw
359 }
360
361 fn is_visible(&self) -> bool {
362 self.items
363 .iter()
364 .any(|item| item.visible && item.component.is_visible())
365 }
366
367 fn should_remove(&self) -> bool {
368 self.items.iter().all(|item| item.component.should_remove())
369 }
370}
371
372pub struct LayoutBuilder;
374
375impl LayoutBuilder {
376 pub fn horizontal(bounds: Rectangle) -> LayoutContainer {
378 LayoutContainer::new(LayoutType::HorizontalFlow, bounds)
379 }
380
381 pub fn vertical(bounds: Rectangle) -> LayoutContainer {
383 LayoutContainer::new(LayoutType::VerticalFlow, bounds)
384 }
385
386 pub fn grid(bounds: Rectangle, columns: usize) -> LayoutContainer {
388 LayoutContainer::new(LayoutType::Grid { columns }, bounds)
389 }
390
391 #[allow(dead_code)]
393 pub fn fixed(bounds: Rectangle) -> LayoutContainer {
394 LayoutContainer::new(LayoutType::Fixed, bounds)
395 }
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401
402 struct MockComponent {
404 bounds: Rectangle,
405 updated: bool,
406 }
407
408 impl MockComponent {
409 fn new(x: i16, y: i16, width: u16, height: u16) -> Self {
410 Self {
411 bounds: Rectangle::new(x, y, width, height),
412 updated: false,
413 }
414 }
415 }
416
417 impl Component for MockComponent {
418 fn render(&self, _graphics: &mut crate::graphics::GraphicsContext) -> anyhow::Result<()> {
419 Ok(())
420 }
421
422 fn bounds(&self) -> Rectangle {
423 self.bounds
424 }
425
426 fn update(&mut self, _delta_time: f64) -> bool {
427 self.updated = !self.updated;
428 self.updated
429 }
430 }
431
432 #[test]
433 fn test_spacing_creation() {
434 let spacing = Spacing::new(10, 20, 30, 40);
435 assert_eq!(spacing.top, 10);
436 assert_eq!(spacing.right, 20);
437 assert_eq!(spacing.bottom, 30);
438 assert_eq!(spacing.left, 40);
439
440 let uniform = Spacing::uniform(15);
441 assert_eq!(uniform.top, 15);
442 assert_eq!(uniform.right, 15);
443 assert_eq!(uniform.bottom, 15);
444 assert_eq!(uniform.left, 15);
445 }
446
447 #[test]
448 fn test_layout_item_creation() {
449 let component = MockComponent::new(10, 20, 100, 50);
450 let item = LayoutItem::new(Box::new(component));
451
452 assert_eq!(item.computed_bounds.x, 10);
453 assert_eq!(item.computed_bounds.y, 20);
454 assert_eq!(item.computed_bounds.width, 100);
455 assert_eq!(item.computed_bounds.height, 50);
456 assert!(item.visible);
457 }
458
459 #[test]
460 fn test_layout_container_creation() {
461 let bounds = Rectangle::new(0, 0, 800, 600);
462 let container = LayoutContainer::new(LayoutType::HorizontalFlow, bounds);
463
464 assert_eq!(container.bounds().x, 0);
465 assert_eq!(container.bounds().y, 0);
466 assert_eq!(container.bounds().width, 800);
467 assert_eq!(container.bounds().height, 600);
468 }
469
470 #[test]
471 fn test_layout_builder() {
472 let bounds = Rectangle::new(0, 0, 400, 300);
473
474 let horizontal = LayoutBuilder::horizontal(bounds);
475 matches!(horizontal.layout_type, LayoutType::HorizontalFlow);
476
477 let vertical = LayoutBuilder::vertical(bounds);
478 matches!(vertical.layout_type, LayoutType::VerticalFlow);
479
480 let grid = LayoutBuilder::grid(bounds, 3);
481 matches!(grid.layout_type, LayoutType::Grid { columns: 3 });
482 }
483
484 #[test]
485 fn test_horizontal_layout() {
486 let bounds = Rectangle::new(0, 0, 400, 100);
487 let mut container = LayoutBuilder::horizontal(bounds)
488 .with_gap(10)
489 .with_alignment(Alignment::Start);
490
491 let comp1 = MockComponent::new(0, 0, 50, 30);
493 let comp2 = MockComponent::new(0, 0, 80, 40);
494
495 container.add_component(Box::new(comp1));
496 container.add_component(Box::new(comp2));
497
498 container.layout().unwrap();
500
501 assert_eq!(container.items[0].computed_bounds.x, 0);
503 assert_eq!(container.items[0].computed_bounds.y, 0);
504
505 assert_eq!(container.items[1].computed_bounds.x, 50 + 10); assert_eq!(container.items[1].computed_bounds.y, 0);
508 }
509
510 #[test]
511 fn test_vertical_layout() {
512 let bounds = Rectangle::new(0, 0, 100, 400);
513 let mut container = LayoutBuilder::vertical(bounds)
514 .with_gap(5)
515 .with_alignment(Alignment::Start);
516
517 let comp1 = MockComponent::new(0, 0, 50, 30);
519 let comp2 = MockComponent::new(0, 0, 60, 40);
520
521 container.add_component(Box::new(comp1));
522 container.add_component(Box::new(comp2));
523
524 container.layout().unwrap();
526
527 assert_eq!(container.items[0].computed_bounds.x, 0);
529 assert_eq!(container.items[0].computed_bounds.y, 0);
530
531 assert_eq!(container.items[1].computed_bounds.x, 0);
533 assert_eq!(container.items[1].computed_bounds.y, 30 + 5); }
535
536 #[test]
537 fn test_grid_layout() {
538 let bounds = Rectangle::new(0, 0, 200, 200);
539 let mut container = LayoutBuilder::grid(bounds, 2); for _i in 0..4 {
543 let comp = MockComponent::new(0, 0, 40, 30);
544 container.add_component(Box::new(comp));
545 }
546
547 container.layout().unwrap();
548
549 let cell_width = 200 / 2;
551 let cell_height = 200 / 2;
552
553 assert_eq!(container.items[0].computed_bounds.x, 0);
555 assert_eq!(container.items[0].computed_bounds.y, 0);
556
557 assert_eq!(container.items[1].computed_bounds.x, cell_width as i16);
559 assert_eq!(container.items[1].computed_bounds.y, 0);
560
561 assert_eq!(container.items[2].computed_bounds.x, 0);
563 assert_eq!(container.items[2].computed_bounds.y, cell_height as i16);
564
565 assert_eq!(container.items[3].computed_bounds.x, cell_width as i16);
567 assert_eq!(container.items[3].computed_bounds.y, cell_height as i16);
568 }
569
570 #[test]
571 fn test_alignment_center() {
572 let bounds = Rectangle::new(0, 0, 200, 100);
573 let mut container = LayoutBuilder::horizontal(bounds).with_alignment(Alignment::Center);
574
575 let comp = MockComponent::new(0, 0, 50, 30);
577 container.add_component(Box::new(comp));
578
579 container.layout().unwrap();
580
581 let expected_y = (100 - 30) / 2; assert_eq!(container.items[0].computed_bounds.y, expected_y as i16);
584 }
585
586 #[test]
587 fn test_padding() {
588 let bounds = Rectangle::new(0, 0, 200, 100);
589 let padding = Spacing::uniform(10);
590 let mut container = LayoutBuilder::horizontal(bounds).with_padding(padding);
591
592 let comp = MockComponent::new(0, 0, 50, 30);
593 container.add_component(Box::new(comp));
594
595 container.layout().unwrap();
596
597 assert_eq!(container.items[0].computed_bounds.x, 10); assert_eq!(container.items[0].computed_bounds.y, 10); }
601
602 #[test]
603 fn test_container_visibility() {
604 let bounds = Rectangle::new(0, 0, 100, 100);
605 let mut container = LayoutContainer::new(LayoutType::Fixed, bounds);
606
607 assert!(!container.is_visible());
609
610 let comp = MockComponent::new(0, 0, 50, 30);
612 container.add_component(Box::new(comp));
613
614 assert!(container.is_visible());
616 }
617
618 #[test]
619 fn test_container_update() {
620 let bounds = Rectangle::new(0, 0, 100, 100);
621 let mut container = LayoutContainer::new(LayoutType::Fixed, bounds);
622
623 let comp = MockComponent::new(0, 0, 50, 30);
625 container.add_component(Box::new(comp));
626
627 let needs_redraw = container.update(0.016);
629 assert!(needs_redraw); }
631}