embedded_charts/dashboard/
grid.rs

1//! Grid-based layout system for dashboards
2
3use embedded_graphics::{prelude::*, primitives::Rectangle};
4use heapless::Vec;
5
6/// Position in a grid layout
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub struct GridPosition {
9    /// Row index (0-based)
10    pub row: u8,
11    /// Column index (0-based)
12    pub col: u8,
13    /// Number of rows this panel spans
14    pub row_span: u8,
15    /// Number of columns this panel spans  
16    pub col_span: u8,
17}
18
19impl GridPosition {
20    /// Create a new grid position with single cell
21    pub fn new(row: u8, col: u8) -> Self {
22        Self {
23            row,
24            col,
25            row_span: 1,
26            col_span: 1,
27        }
28    }
29
30    /// Create a position that spans multiple cells
31    pub fn with_span(row: u8, col: u8, row_span: u8, col_span: u8) -> Self {
32        Self {
33            row,
34            col,
35            row_span: row_span.max(1),
36            col_span: col_span.max(1),
37        }
38    }
39}
40
41/// Grid-based layout for arranging charts
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub struct GridLayout {
44    /// Number of rows in the grid
45    pub rows: u8,
46    /// Number of columns in the grid
47    pub cols: u8,
48}
49
50impl GridLayout {
51    /// Create a new grid layout
52    pub fn new(rows: u8, cols: u8) -> Self {
53        Self {
54            rows: rows.max(1),
55            cols: cols.max(1),
56        }
57    }
58
59    /// Calculate the viewport for a specific grid position
60    pub fn calculate_cell_viewport(
61        &self,
62        total_viewport: Rectangle,
63        position: GridPosition,
64        spacing: u32,
65    ) -> Rectangle {
66        let total_width = total_viewport.size.width;
67        let total_height = total_viewport.size.height;
68
69        // Calculate spacing requirements
70        let h_spacing = spacing * (self.cols as u32 - 1);
71        let v_spacing = spacing * (self.rows as u32 - 1);
72
73        // Calculate cell dimensions
74        let cell_width = (total_width.saturating_sub(h_spacing)) / self.cols as u32;
75        let cell_height = (total_height.saturating_sub(v_spacing)) / self.rows as u32;
76
77        // Calculate position
78        let x = total_viewport.top_left.x
79            + (position.col as i32 * (cell_width as i32 + spacing as i32));
80        let y = total_viewport.top_left.y
81            + (position.row as i32 * (cell_height as i32 + spacing as i32));
82
83        // Calculate size with span
84        let width = (cell_width * position.col_span as u32)
85            + (spacing * position.col_span.saturating_sub(1) as u32);
86        let height = (cell_height * position.row_span as u32)
87            + (spacing * position.row_span.saturating_sub(1) as u32);
88
89        Rectangle::new(Point::new(x, y), Size::new(width, height))
90    }
91
92    /// Calculate viewports for all panels in order
93    pub fn calculate_viewports<const N: usize>(
94        &self,
95        total_viewport: Rectangle,
96        positions: &[GridPosition],
97        spacing: u32,
98    ) -> crate::error::ChartResult<Vec<Rectangle, N>> {
99        let mut viewports = Vec::new();
100
101        for position in positions {
102            let viewport = self.calculate_cell_viewport(total_viewport, *position, spacing);
103            viewports
104                .push(viewport)
105                .map_err(|_| crate::error::ChartError::MemoryFull)?;
106        }
107
108        Ok(viewports)
109    }
110}
111
112impl Default for GridLayout {
113    fn default() -> Self {
114        Self::new(2, 2)
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn test_grid_position() {
124        let pos = GridPosition::new(1, 2);
125        assert_eq!(pos.row, 1);
126        assert_eq!(pos.col, 2);
127        assert_eq!(pos.row_span, 1);
128        assert_eq!(pos.col_span, 1);
129
130        let span_pos = GridPosition::with_span(0, 0, 2, 3);
131        assert_eq!(span_pos.row_span, 2);
132        assert_eq!(span_pos.col_span, 3);
133    }
134
135    #[test]
136    fn test_grid_layout_2x2() {
137        let layout = GridLayout::new(2, 2);
138        let viewport = Rectangle::new(Point::new(0, 0), Size::new(200, 200));
139
140        // Test top-left cell
141        let cell = layout.calculate_cell_viewport(
142            viewport,
143            GridPosition::new(0, 0),
144            10, // spacing
145        );
146        assert_eq!(cell.top_left, Point::new(0, 0));
147        assert_eq!(cell.size, Size::new(95, 95)); // (200 - 10) / 2 = 95
148
149        // Test bottom-right cell
150        let cell = layout.calculate_cell_viewport(viewport, GridPosition::new(1, 1), 10);
151        assert_eq!(cell.top_left, Point::new(105, 105)); // 95 + 10 = 105
152        assert_eq!(cell.size, Size::new(95, 95));
153    }
154
155    #[test]
156    fn test_grid_layout_with_span() {
157        let layout = GridLayout::new(3, 3);
158        let viewport = Rectangle::new(Point::new(0, 0), Size::new(320, 320));
159
160        // Test 2x2 span starting at (0,0)
161        let cell =
162            layout.calculate_cell_viewport(viewport, GridPosition::with_span(0, 0, 2, 2), 10);
163        assert_eq!(cell.top_left, Point::new(0, 0));
164        assert_eq!(cell.size, Size::new(210, 210)); // 2*100 + 10 = 210
165    }
166}