Skip to main content

x11_overlay/ui/
layout.rs

1use crate::graphics::{GraphicsContext, Rectangle};
2use crate::ui::Component;
3use anyhow::Result;
4
5/// Represents different layout strategies for arranging components
6#[derive(Debug, Clone, Copy, PartialEq)]
7pub enum LayoutType {
8    /// Fixed positioning - components maintain their exact positions
9    #[allow(dead_code)]
10    Fixed,
11    /// Horizontal flow - components arranged left to right
12    HorizontalFlow,
13    /// Vertical flow - components arranged top to bottom
14    VerticalFlow,
15    /// Grid layout - components arranged in rows and columns
16    Grid { columns: usize },
17}
18
19/// Alignment options for layout containers
20#[derive(Debug, Clone, Copy, PartialEq)]
21pub enum Alignment {
22    Start,
23    Center,
24    End,
25}
26
27/// Spacing configuration for layouts
28#[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
62/// A positioned component with layout metadata
63pub 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
94/// Layout container that manages the positioning and sizing of components
95pub 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        // Fixed layout keeps original component bounds
191        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            // Calculate vertical position
209            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            // Position item
223            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            // Calculate horizontal position
248            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            // Position item
262            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                // Calculate offset from component's original position to computed position
320                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 we have a cairo context, use translation to position the component
325                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                    // Fallback: render at original position (better than nothing)
332                    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; // Layout might need recalculation
350            }
351        }
352
353        if self.needs_layout {
354            let _ = self.layout(); // Ignore layout errors in update
355            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
372/// Simple layout builder for common layout patterns
373pub struct LayoutBuilder;
374
375impl LayoutBuilder {
376    /// Create a horizontal layout container
377    pub fn horizontal(bounds: Rectangle) -> LayoutContainer {
378        LayoutContainer::new(LayoutType::HorizontalFlow, bounds)
379    }
380
381    /// Create a vertical layout container
382    pub fn vertical(bounds: Rectangle) -> LayoutContainer {
383        LayoutContainer::new(LayoutType::VerticalFlow, bounds)
384    }
385
386    /// Create a grid layout container
387    pub fn grid(bounds: Rectangle, columns: usize) -> LayoutContainer {
388        LayoutContainer::new(LayoutType::Grid { columns }, bounds)
389    }
390
391    /// Create a fixed layout container
392    #[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    // Mock component for testing
403    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        // Add two components
492        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        // Layout should position components horizontally
499        container.layout().unwrap();
500
501        // First component should be at the start
502        assert_eq!(container.items[0].computed_bounds.x, 0);
503        assert_eq!(container.items[0].computed_bounds.y, 0);
504
505        // Second component should be positioned after first + gap
506        assert_eq!(container.items[1].computed_bounds.x, 50 + 10); // width of first + gap
507        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        // Add two components
518        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        // Layout should position components vertically
525        container.layout().unwrap();
526
527        // First component should be at the start
528        assert_eq!(container.items[0].computed_bounds.x, 0);
529        assert_eq!(container.items[0].computed_bounds.y, 0);
530
531        // Second component should be positioned after first + gap
532        assert_eq!(container.items[1].computed_bounds.x, 0);
533        assert_eq!(container.items[1].computed_bounds.y, 30 + 5); // height of first + gap
534    }
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); // 2 columns
540
541        // Add four components
542        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        // Cell dimensions should be 100x100 (200/2 columns, 200/2 rows)
550        let cell_width = 200 / 2;
551        let cell_height = 200 / 2;
552
553        // Check positioning of first component (top-left)
554        assert_eq!(container.items[0].computed_bounds.x, 0);
555        assert_eq!(container.items[0].computed_bounds.y, 0);
556
557        // Check positioning of second component (top-right)
558        assert_eq!(container.items[1].computed_bounds.x, cell_width as i16);
559        assert_eq!(container.items[1].computed_bounds.y, 0);
560
561        // Check positioning of third component (bottom-left)
562        assert_eq!(container.items[2].computed_bounds.x, 0);
563        assert_eq!(container.items[2].computed_bounds.y, cell_height as i16);
564
565        // Check positioning of fourth component (bottom-right)
566        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        // Add a component smaller than the container
576        let comp = MockComponent::new(0, 0, 50, 30);
577        container.add_component(Box::new(comp));
578
579        container.layout().unwrap();
580
581        // Component should be centered vertically (horizontal layout centers in cross-axis)
582        let expected_y = (100 - 30) / 2; // (container_height - component_height) / 2
583        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        // Component should be positioned considering padding
598        assert_eq!(container.items[0].computed_bounds.x, 10); // left padding
599        assert_eq!(container.items[0].computed_bounds.y, 10); // top padding
600    }
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        // Empty container should not be visible
608        assert!(!container.is_visible());
609
610        // Add a component
611        let comp = MockComponent::new(0, 0, 50, 30);
612        container.add_component(Box::new(comp));
613
614        // Now container should be visible
615        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        // Add a component
624        let comp = MockComponent::new(0, 0, 50, 30);
625        container.add_component(Box::new(comp));
626
627        // Update should return true if any component needs redraw
628        let needs_redraw = container.update(0.016);
629        assert!(needs_redraw); // MockComponent alternates its update return value
630    }
631}