embedded_charts/
layout.rs

1//! Layout management for chart components.
2
3use crate::chart::traits::Margins;
4use crate::error::{LayoutError, LayoutResult};
5use embedded_graphics::{prelude::*, primitives::Rectangle};
6
7/// Layout manager for chart components
8#[derive(Debug, Clone)]
9pub struct ChartLayout {
10    /// Total available area
11    pub total_area: Rectangle,
12    /// Chart drawing area (after margins)
13    pub chart_area: Rectangle,
14    /// Title area
15    pub title_area: Option<Rectangle>,
16    /// Legend area
17    pub legend_area: Option<Rectangle>,
18    /// X-axis area
19    pub x_axis_area: Option<Rectangle>,
20    /// Y-axis area
21    pub y_axis_area: Option<Rectangle>,
22}
23
24impl ChartLayout {
25    /// Create a new chart layout
26    pub fn new(total_area: Rectangle) -> Self {
27        Self {
28            total_area,
29            chart_area: total_area,
30            title_area: None,
31            legend_area: None,
32            x_axis_area: None,
33            y_axis_area: None,
34        }
35    }
36
37    /// Apply margins to the layout
38    pub fn with_margins(mut self, margins: Margins) -> Self {
39        self.chart_area = margins.apply_to(self.total_area);
40        self
41    }
42
43    /// Reserve space for a title at the top
44    pub fn with_title(mut self, height: u32) -> LayoutResult<Self> {
45        if height >= self.chart_area.size.height {
46            return Err(LayoutError::InsufficientSpace);
47        }
48
49        self.title_area = Some(Rectangle::new(
50            self.chart_area.top_left,
51            Size::new(self.chart_area.size.width, height),
52        ));
53
54        // Adjust chart area
55        self.chart_area = Rectangle::new(
56            Point::new(
57                self.chart_area.top_left.x,
58                self.chart_area.top_left.y + height as i32,
59            ),
60            Size::new(
61                self.chart_area.size.width,
62                self.chart_area.size.height - height,
63            ),
64        );
65
66        Ok(self)
67    }
68
69    /// Reserve space for a legend
70    pub fn with_legend(mut self, position: LegendPosition, size: Size) -> LayoutResult<Self> {
71        match position {
72            LegendPosition::Right => {
73                if size.width >= self.chart_area.size.width {
74                    return Err(LayoutError::InsufficientSpace);
75                }
76
77                self.legend_area = Some(Rectangle::new(
78                    Point::new(
79                        self.chart_area.top_left.x + self.chart_area.size.width as i32
80                            - size.width as i32,
81                        self.chart_area.top_left.y,
82                    ),
83                    size,
84                ));
85
86                // Adjust chart area
87                self.chart_area = Rectangle::new(
88                    self.chart_area.top_left,
89                    Size::new(
90                        self.chart_area.size.width - size.width,
91                        self.chart_area.size.height,
92                    ),
93                );
94            }
95            LegendPosition::Bottom => {
96                if size.height >= self.chart_area.size.height {
97                    return Err(LayoutError::InsufficientSpace);
98                }
99
100                self.legend_area = Some(Rectangle::new(
101                    Point::new(
102                        self.chart_area.top_left.x,
103                        self.chart_area.top_left.y + self.chart_area.size.height as i32
104                            - size.height as i32,
105                    ),
106                    size,
107                ));
108
109                // Adjust chart area
110                self.chart_area = Rectangle::new(
111                    self.chart_area.top_left,
112                    Size::new(
113                        self.chart_area.size.width,
114                        self.chart_area.size.height - size.height,
115                    ),
116                );
117            }
118            LegendPosition::Top => {
119                if size.height >= self.chart_area.size.height {
120                    return Err(LayoutError::InsufficientSpace);
121                }
122
123                self.legend_area = Some(Rectangle::new(self.chart_area.top_left, size));
124
125                // Adjust chart area
126                self.chart_area = Rectangle::new(
127                    Point::new(
128                        self.chart_area.top_left.x,
129                        self.chart_area.top_left.y + size.height as i32,
130                    ),
131                    Size::new(
132                        self.chart_area.size.width,
133                        self.chart_area.size.height - size.height,
134                    ),
135                );
136            }
137            LegendPosition::Left => {
138                if size.width >= self.chart_area.size.width {
139                    return Err(LayoutError::InsufficientSpace);
140                }
141
142                self.legend_area = Some(Rectangle::new(self.chart_area.top_left, size));
143
144                // Adjust chart area
145                self.chart_area = Rectangle::new(
146                    Point::new(
147                        self.chart_area.top_left.x + size.width as i32,
148                        self.chart_area.top_left.y,
149                    ),
150                    Size::new(
151                        self.chart_area.size.width - size.width,
152                        self.chart_area.size.height,
153                    ),
154                );
155            }
156        }
157
158        Ok(self)
159    }
160
161    /// Reserve space for X-axis
162    pub fn with_x_axis(mut self, height: u32) -> LayoutResult<Self> {
163        if height >= self.chart_area.size.height {
164            return Err(LayoutError::InsufficientSpace);
165        }
166
167        self.x_axis_area = Some(Rectangle::new(
168            Point::new(
169                self.chart_area.top_left.x,
170                self.chart_area.top_left.y + self.chart_area.size.height as i32 - height as i32,
171            ),
172            Size::new(self.chart_area.size.width, height),
173        ));
174
175        // Adjust chart area
176        self.chart_area = Rectangle::new(
177            self.chart_area.top_left,
178            Size::new(
179                self.chart_area.size.width,
180                self.chart_area.size.height - height,
181            ),
182        );
183
184        Ok(self)
185    }
186
187    /// Reserve space for Y-axis
188    pub fn with_y_axis(mut self, width: u32) -> LayoutResult<Self> {
189        if width >= self.chart_area.size.width {
190            return Err(LayoutError::InsufficientSpace);
191        }
192
193        self.y_axis_area = Some(Rectangle::new(
194            self.chart_area.top_left,
195            Size::new(width, self.chart_area.size.height),
196        ));
197
198        // Adjust chart area
199        self.chart_area = Rectangle::new(
200            Point::new(
201                self.chart_area.top_left.x + width as i32,
202                self.chart_area.top_left.y,
203            ),
204            Size::new(
205                self.chart_area.size.width - width,
206                self.chart_area.size.height,
207            ),
208        );
209
210        Ok(self)
211    }
212
213    /// Get the final chart drawing area
214    pub fn chart_area(&self) -> Rectangle {
215        self.chart_area
216    }
217
218    /// Validate that the layout has sufficient space
219    pub fn validate(&self) -> LayoutResult<()> {
220        if self.chart_area.size.width < 10 || self.chart_area.size.height < 10 {
221            return Err(LayoutError::InsufficientSpace);
222        }
223        Ok(())
224    }
225}
226
227/// Legend position options
228#[derive(Debug, Clone, Copy, PartialEq, Eq)]
229pub enum LegendPosition {
230    /// Legend on the top
231    Top,
232    /// Legend on the right
233    Right,
234    /// Legend on the bottom
235    Bottom,
236    /// Legend on the left
237    Left,
238}
239
240/// Viewport management for chart rendering
241#[derive(Debug, Clone, Copy, PartialEq)]
242pub struct Viewport {
243    /// The visible area
244    pub area: Rectangle,
245    /// Zoom level (1.0 = normal, >1.0 = zoomed in)
246    pub zoom: f32,
247    /// Pan offset
248    pub offset: Point,
249}
250
251impl Viewport {
252    /// Create a new viewport
253    pub fn new(area: Rectangle) -> Self {
254        Self {
255            area,
256            zoom: 1.0,
257            offset: Point::zero(),
258        }
259    }
260
261    /// Set the zoom level
262    pub fn with_zoom(mut self, zoom: f32) -> Self {
263        self.zoom = zoom.clamp(0.1, 10.0); // Clamp zoom to reasonable range
264        self
265    }
266
267    /// Set the pan offset
268    pub fn with_offset(mut self, offset: Point) -> Self {
269        self.offset = offset;
270        self
271    }
272
273    /// Transform a point from data coordinates to screen coordinates
274    pub fn transform_point(&self, data_point: Point, data_bounds: Rectangle) -> Point {
275        // Normalize to 0-1 range
276        let norm_x = if data_bounds.size.width > 0 {
277            (data_point.x - data_bounds.top_left.x) as f32 / data_bounds.size.width as f32
278        } else {
279            0.5
280        };
281
282        let norm_y = if data_bounds.size.height > 0 {
283            (data_point.y - data_bounds.top_left.y) as f32 / data_bounds.size.height as f32
284        } else {
285            0.5
286        };
287
288        // Apply zoom and offset
289        let zoomed_x = norm_x * self.zoom;
290        let zoomed_y = norm_y * self.zoom;
291
292        // Transform to screen coordinates
293        let screen_x =
294            self.area.top_left.x + (zoomed_x * self.area.size.width as f32) as i32 + self.offset.x;
295        let screen_y =
296            self.area.top_left.y + (zoomed_y * self.area.size.height as f32) as i32 + self.offset.y;
297
298        Point::new(screen_x, screen_y)
299    }
300
301    /// Check if a point is visible in the viewport
302    pub fn is_point_visible(&self, point: Point) -> bool {
303        point.x >= self.area.top_left.x
304            && point.x < self.area.top_left.x + self.area.size.width as i32
305            && point.y >= self.area.top_left.y
306            && point.y < self.area.top_left.y + self.area.size.height as i32
307    }
308
309    /// Get the visible data bounds for the current viewport
310    pub fn visible_data_bounds(&self, full_data_bounds: Rectangle) -> Rectangle {
311        // This is a simplified implementation
312        // In a full implementation, you'd calculate the actual visible bounds based on zoom and offset
313        full_data_bounds
314    }
315}
316
317/// Component positioning utilities
318pub struct ComponentPositioning;
319
320impl ComponentPositioning {
321    /// Center a component within a container
322    pub fn center_in_container(component_size: Size, container: Rectangle) -> Point {
323        let x =
324            container.top_left.x + (container.size.width as i32 - component_size.width as i32) / 2;
325        let y = container.top_left.y
326            + (container.size.height as i32 - component_size.height as i32) / 2;
327        Point::new(x, y)
328    }
329
330    /// Align a component to the top-left of a container
331    pub fn align_top_left(container: Rectangle, margin: u32) -> Point {
332        Point::new(
333            container.top_left.x + margin as i32,
334            container.top_left.y + margin as i32,
335        )
336    }
337
338    /// Align a component to the top-right of a container
339    pub fn align_top_right(component_size: Size, container: Rectangle, margin: u32) -> Point {
340        Point::new(
341            container.top_left.x + container.size.width as i32
342                - component_size.width as i32
343                - margin as i32,
344            container.top_left.y + margin as i32,
345        )
346    }
347
348    /// Align a component to the bottom-left of a container
349    pub fn align_bottom_left(component_size: Size, container: Rectangle, margin: u32) -> Point {
350        Point::new(
351            container.top_left.x + margin as i32,
352            container.top_left.y + container.size.height as i32
353                - component_size.height as i32
354                - margin as i32,
355        )
356    }
357
358    /// Align a component to the bottom-right of a container
359    pub fn align_bottom_right(component_size: Size, container: Rectangle, margin: u32) -> Point {
360        Point::new(
361            container.top_left.x + container.size.width as i32
362                - component_size.width as i32
363                - margin as i32,
364            container.top_left.y + container.size.height as i32
365                - component_size.height as i32
366                - margin as i32,
367        )
368    }
369
370    /// Distribute components evenly in a horizontal layout
371    pub fn distribute_horizontal(
372        component_sizes: &[Size],
373        container: Rectangle,
374        spacing: u32,
375    ) -> LayoutResult<heapless::Vec<Point, 16>> {
376        let mut positions = heapless::Vec::new();
377
378        if component_sizes.is_empty() {
379            return Ok(positions);
380        }
381
382        let total_width: u32 = component_sizes.iter().map(|s| s.width).sum();
383        let total_spacing = spacing * (component_sizes.len() as u32).saturating_sub(1);
384
385        if total_width + total_spacing > container.size.width {
386            return Err(LayoutError::InsufficientSpace);
387        }
388
389        let start_x =
390            container.top_left.x + (container.size.width - total_width - total_spacing) as i32 / 2;
391        let mut current_x = start_x;
392
393        for size in component_sizes {
394            let y = container.top_left.y + (container.size.height as i32 - size.height as i32) / 2;
395            positions
396                .push(Point::new(current_x, y))
397                .map_err(|_| LayoutError::InsufficientSpace)?;
398            current_x += size.width as i32 + spacing as i32;
399        }
400
401        Ok(positions)
402    }
403
404    /// Distribute components evenly in a vertical layout
405    pub fn distribute_vertical(
406        component_sizes: &[Size],
407        container: Rectangle,
408        spacing: u32,
409    ) -> LayoutResult<heapless::Vec<Point, 16>> {
410        let mut positions = heapless::Vec::new();
411
412        if component_sizes.is_empty() {
413            return Ok(positions);
414        }
415
416        let total_height: u32 = component_sizes.iter().map(|s| s.height).sum();
417        let total_spacing = spacing * (component_sizes.len() as u32).saturating_sub(1);
418
419        if total_height + total_spacing > container.size.height {
420            return Err(LayoutError::InsufficientSpace);
421        }
422
423        let start_y = container.top_left.y
424            + (container.size.height - total_height - total_spacing) as i32 / 2;
425        let mut current_y = start_y;
426
427        for size in component_sizes {
428            let x = container.top_left.x + (container.size.width as i32 - size.width as i32) / 2;
429            positions
430                .push(Point::new(x, current_y))
431                .map_err(|_| LayoutError::InsufficientSpace)?;
432            current_y += size.height as i32 + spacing as i32;
433        }
434
435        Ok(positions)
436    }
437}
438
439#[cfg(test)]
440mod tests {
441    use super::*;
442
443    #[test]
444    fn test_chart_layout_creation() {
445        let area = Rectangle::new(Point::zero(), Size::new(400, 300));
446        let layout = ChartLayout::new(area);
447
448        assert_eq!(layout.total_area, area);
449        assert_eq!(layout.chart_area, area);
450        assert!(layout.title_area.is_none());
451    }
452
453    #[test]
454    fn test_layout_with_margins() {
455        let area = Rectangle::new(Point::zero(), Size::new(400, 300));
456        let margins = Margins::all(20);
457        let layout = ChartLayout::new(area).with_margins(margins);
458
459        assert_eq!(layout.chart_area.top_left, Point::new(20, 20));
460        assert_eq!(layout.chart_area.size, Size::new(360, 260));
461    }
462
463    #[test]
464    fn test_layout_with_title() {
465        let area = Rectangle::new(Point::zero(), Size::new(400, 300));
466        let layout = ChartLayout::new(area).with_title(30).unwrap();
467
468        assert!(layout.title_area.is_some());
469        let title_area = layout.title_area.unwrap();
470        assert_eq!(title_area.size.height, 30);
471        assert_eq!(layout.chart_area.size.height, 270);
472    }
473
474    #[test]
475    fn test_viewport_creation() {
476        let area = Rectangle::new(Point::zero(), Size::new(200, 150));
477        let viewport = Viewport::new(area);
478
479        assert_eq!(viewport.area, area);
480        assert_eq!(viewport.zoom, 1.0);
481        assert_eq!(viewport.offset, Point::zero());
482    }
483
484    #[test]
485    fn test_viewport_with_zoom() {
486        let area = Rectangle::new(Point::zero(), Size::new(200, 150));
487        let viewport = Viewport::new(area).with_zoom(2.0);
488
489        assert_eq!(viewport.zoom, 2.0);
490    }
491
492    #[test]
493    fn test_component_positioning_center() {
494        let container = Rectangle::new(Point::new(10, 10), Size::new(100, 80));
495        let component_size = Size::new(20, 10);
496
497        let position = ComponentPositioning::center_in_container(component_size, container);
498        assert_eq!(position, Point::new(50, 45));
499    }
500
501    #[test]
502    fn test_component_positioning_corners() {
503        let container = Rectangle::new(Point::new(0, 0), Size::new(100, 80));
504        let component_size = Size::new(20, 10);
505        let margin = 5;
506
507        let top_left = ComponentPositioning::align_top_left(container, margin);
508        assert_eq!(top_left, Point::new(5, 5));
509
510        let top_right = ComponentPositioning::align_top_right(component_size, container, margin);
511        assert_eq!(top_right, Point::new(75, 5));
512
513        let bottom_left =
514            ComponentPositioning::align_bottom_left(component_size, container, margin);
515        assert_eq!(bottom_left, Point::new(5, 65));
516
517        let bottom_right =
518            ComponentPositioning::align_bottom_right(component_size, container, margin);
519        assert_eq!(bottom_right, Point::new(75, 65));
520    }
521}