use crate::chart::traits::Margins;
use crate::error::{LayoutError, LayoutResult};
use embedded_graphics::{prelude::*, primitives::Rectangle};
#[derive(Debug, Clone)]
pub struct ChartLayout {
pub total_area: Rectangle,
pub chart_area: Rectangle,
pub title_area: Option<Rectangle>,
pub legend_area: Option<Rectangle>,
pub x_axis_area: Option<Rectangle>,
pub y_axis_area: Option<Rectangle>,
}
impl ChartLayout {
pub fn new(total_area: Rectangle) -> Self {
Self {
total_area,
chart_area: total_area,
title_area: None,
legend_area: None,
x_axis_area: None,
y_axis_area: None,
}
}
pub fn with_margins(mut self, margins: Margins) -> Self {
self.chart_area = margins.apply_to(self.total_area);
self
}
pub fn with_title(mut self, height: u32) -> LayoutResult<Self> {
if height >= self.chart_area.size.height {
return Err(LayoutError::InsufficientSpace);
}
self.title_area = Some(Rectangle::new(
self.chart_area.top_left,
Size::new(self.chart_area.size.width, height),
));
self.chart_area = Rectangle::new(
Point::new(
self.chart_area.top_left.x,
self.chart_area.top_left.y + height as i32,
),
Size::new(
self.chart_area.size.width,
self.chart_area.size.height - height,
),
);
Ok(self)
}
pub fn with_legend(mut self, position: LegendPosition, size: Size) -> LayoutResult<Self> {
match position {
LegendPosition::Right => {
if size.width >= self.chart_area.size.width {
return Err(LayoutError::InsufficientSpace);
}
self.legend_area = Some(Rectangle::new(
Point::new(
self.chart_area.top_left.x + self.chart_area.size.width as i32
- size.width as i32,
self.chart_area.top_left.y,
),
size,
));
self.chart_area = Rectangle::new(
self.chart_area.top_left,
Size::new(
self.chart_area.size.width - size.width,
self.chart_area.size.height,
),
);
}
LegendPosition::Bottom => {
if size.height >= self.chart_area.size.height {
return Err(LayoutError::InsufficientSpace);
}
self.legend_area = Some(Rectangle::new(
Point::new(
self.chart_area.top_left.x,
self.chart_area.top_left.y + self.chart_area.size.height as i32
- size.height as i32,
),
size,
));
self.chart_area = Rectangle::new(
self.chart_area.top_left,
Size::new(
self.chart_area.size.width,
self.chart_area.size.height - size.height,
),
);
}
LegendPosition::Top => {
if size.height >= self.chart_area.size.height {
return Err(LayoutError::InsufficientSpace);
}
self.legend_area = Some(Rectangle::new(self.chart_area.top_left, size));
self.chart_area = Rectangle::new(
Point::new(
self.chart_area.top_left.x,
self.chart_area.top_left.y + size.height as i32,
),
Size::new(
self.chart_area.size.width,
self.chart_area.size.height - size.height,
),
);
}
LegendPosition::Left => {
if size.width >= self.chart_area.size.width {
return Err(LayoutError::InsufficientSpace);
}
self.legend_area = Some(Rectangle::new(self.chart_area.top_left, size));
self.chart_area = Rectangle::new(
Point::new(
self.chart_area.top_left.x + size.width as i32,
self.chart_area.top_left.y,
),
Size::new(
self.chart_area.size.width - size.width,
self.chart_area.size.height,
),
);
}
}
Ok(self)
}
pub fn with_x_axis(mut self, height: u32) -> LayoutResult<Self> {
if height >= self.chart_area.size.height {
return Err(LayoutError::InsufficientSpace);
}
self.x_axis_area = Some(Rectangle::new(
Point::new(
self.chart_area.top_left.x,
self.chart_area.top_left.y + self.chart_area.size.height as i32 - height as i32,
),
Size::new(self.chart_area.size.width, height),
));
self.chart_area = Rectangle::new(
self.chart_area.top_left,
Size::new(
self.chart_area.size.width,
self.chart_area.size.height - height,
),
);
Ok(self)
}
pub fn with_y_axis(mut self, width: u32) -> LayoutResult<Self> {
if width >= self.chart_area.size.width {
return Err(LayoutError::InsufficientSpace);
}
self.y_axis_area = Some(Rectangle::new(
self.chart_area.top_left,
Size::new(width, self.chart_area.size.height),
));
self.chart_area = Rectangle::new(
Point::new(
self.chart_area.top_left.x + width as i32,
self.chart_area.top_left.y,
),
Size::new(
self.chart_area.size.width - width,
self.chart_area.size.height,
),
);
Ok(self)
}
pub fn chart_area(&self) -> Rectangle {
self.chart_area
}
pub fn validate(&self) -> LayoutResult<()> {
if self.chart_area.size.width < 10 || self.chart_area.size.height < 10 {
return Err(LayoutError::InsufficientSpace);
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LegendPosition {
Top,
Right,
Bottom,
Left,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Viewport {
pub area: Rectangle,
pub zoom: f32,
pub offset: Point,
}
impl Viewport {
pub fn new(area: Rectangle) -> Self {
Self {
area,
zoom: 1.0,
offset: Point::zero(),
}
}
pub fn with_zoom(mut self, zoom: f32) -> Self {
self.zoom = zoom.clamp(0.1, 10.0); self
}
pub fn with_offset(mut self, offset: Point) -> Self {
self.offset = offset;
self
}
pub fn transform_point(&self, data_point: Point, data_bounds: Rectangle) -> Point {
let norm_x = if data_bounds.size.width > 0 {
(data_point.x - data_bounds.top_left.x) as f32 / data_bounds.size.width as f32
} else {
0.5
};
let norm_y = if data_bounds.size.height > 0 {
(data_point.y - data_bounds.top_left.y) as f32 / data_bounds.size.height as f32
} else {
0.5
};
let zoomed_x = norm_x * self.zoom;
let zoomed_y = norm_y * self.zoom;
let screen_x =
self.area.top_left.x + (zoomed_x * self.area.size.width as f32) as i32 + self.offset.x;
let screen_y =
self.area.top_left.y + (zoomed_y * self.area.size.height as f32) as i32 + self.offset.y;
Point::new(screen_x, screen_y)
}
pub fn is_point_visible(&self, point: Point) -> bool {
point.x >= self.area.top_left.x
&& point.x < self.area.top_left.x + self.area.size.width as i32
&& point.y >= self.area.top_left.y
&& point.y < self.area.top_left.y + self.area.size.height as i32
}
pub fn visible_data_bounds(&self, full_data_bounds: Rectangle) -> Rectangle {
full_data_bounds
}
}
pub struct ComponentPositioning;
impl ComponentPositioning {
pub fn center_in_container(component_size: Size, container: Rectangle) -> Point {
let x =
container.top_left.x + (container.size.width as i32 - component_size.width as i32) / 2;
let y = container.top_left.y
+ (container.size.height as i32 - component_size.height as i32) / 2;
Point::new(x, y)
}
pub fn align_top_left(container: Rectangle, margin: u32) -> Point {
Point::new(
container.top_left.x + margin as i32,
container.top_left.y + margin as i32,
)
}
pub fn align_top_right(component_size: Size, container: Rectangle, margin: u32) -> Point {
Point::new(
container.top_left.x + container.size.width as i32
- component_size.width as i32
- margin as i32,
container.top_left.y + margin as i32,
)
}
pub fn align_bottom_left(component_size: Size, container: Rectangle, margin: u32) -> Point {
Point::new(
container.top_left.x + margin as i32,
container.top_left.y + container.size.height as i32
- component_size.height as i32
- margin as i32,
)
}
pub fn align_bottom_right(component_size: Size, container: Rectangle, margin: u32) -> Point {
Point::new(
container.top_left.x + container.size.width as i32
- component_size.width as i32
- margin as i32,
container.top_left.y + container.size.height as i32
- component_size.height as i32
- margin as i32,
)
}
pub fn distribute_horizontal(
component_sizes: &[Size],
container: Rectangle,
spacing: u32,
) -> LayoutResult<heapless::Vec<Point, 16>> {
let mut positions = heapless::Vec::new();
if component_sizes.is_empty() {
return Ok(positions);
}
let total_width: u32 = component_sizes.iter().map(|s| s.width).sum();
let total_spacing = spacing * (component_sizes.len() as u32).saturating_sub(1);
if total_width + total_spacing > container.size.width {
return Err(LayoutError::InsufficientSpace);
}
let start_x =
container.top_left.x + (container.size.width - total_width - total_spacing) as i32 / 2;
let mut current_x = start_x;
for size in component_sizes {
let y = container.top_left.y + (container.size.height as i32 - size.height as i32) / 2;
positions
.push(Point::new(current_x, y))
.map_err(|_| LayoutError::InsufficientSpace)?;
current_x += size.width as i32 + spacing as i32;
}
Ok(positions)
}
pub fn distribute_vertical(
component_sizes: &[Size],
container: Rectangle,
spacing: u32,
) -> LayoutResult<heapless::Vec<Point, 16>> {
let mut positions = heapless::Vec::new();
if component_sizes.is_empty() {
return Ok(positions);
}
let total_height: u32 = component_sizes.iter().map(|s| s.height).sum();
let total_spacing = spacing * (component_sizes.len() as u32).saturating_sub(1);
if total_height + total_spacing > container.size.height {
return Err(LayoutError::InsufficientSpace);
}
let start_y = container.top_left.y
+ (container.size.height - total_height - total_spacing) as i32 / 2;
let mut current_y = start_y;
for size in component_sizes {
let x = container.top_left.x + (container.size.width as i32 - size.width as i32) / 2;
positions
.push(Point::new(x, current_y))
.map_err(|_| LayoutError::InsufficientSpace)?;
current_y += size.height as i32 + spacing as i32;
}
Ok(positions)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chart_layout_creation() {
let area = Rectangle::new(Point::zero(), Size::new(400, 300));
let layout = ChartLayout::new(area);
assert_eq!(layout.total_area, area);
assert_eq!(layout.chart_area, area);
assert!(layout.title_area.is_none());
}
#[test]
fn test_layout_with_margins() {
let area = Rectangle::new(Point::zero(), Size::new(400, 300));
let margins = Margins::all(20);
let layout = ChartLayout::new(area).with_margins(margins);
assert_eq!(layout.chart_area.top_left, Point::new(20, 20));
assert_eq!(layout.chart_area.size, Size::new(360, 260));
}
#[test]
fn test_layout_with_title() {
let area = Rectangle::new(Point::zero(), Size::new(400, 300));
let layout = ChartLayout::new(area).with_title(30).unwrap();
assert!(layout.title_area.is_some());
let title_area = layout.title_area.unwrap();
assert_eq!(title_area.size.height, 30);
assert_eq!(layout.chart_area.size.height, 270);
}
#[test]
fn test_viewport_creation() {
let area = Rectangle::new(Point::zero(), Size::new(200, 150));
let viewport = Viewport::new(area);
assert_eq!(viewport.area, area);
assert_eq!(viewport.zoom, 1.0);
assert_eq!(viewport.offset, Point::zero());
}
#[test]
fn test_viewport_with_zoom() {
let area = Rectangle::new(Point::zero(), Size::new(200, 150));
let viewport = Viewport::new(area).with_zoom(2.0);
assert_eq!(viewport.zoom, 2.0);
}
#[test]
fn test_component_positioning_center() {
let container = Rectangle::new(Point::new(10, 10), Size::new(100, 80));
let component_size = Size::new(20, 10);
let position = ComponentPositioning::center_in_container(component_size, container);
assert_eq!(position, Point::new(50, 45));
}
#[test]
fn test_component_positioning_corners() {
let container = Rectangle::new(Point::new(0, 0), Size::new(100, 80));
let component_size = Size::new(20, 10);
let margin = 5;
let top_left = ComponentPositioning::align_top_left(container, margin);
assert_eq!(top_left, Point::new(5, 5));
let top_right = ComponentPositioning::align_top_right(component_size, container, margin);
assert_eq!(top_right, Point::new(75, 5));
let bottom_left =
ComponentPositioning::align_bottom_left(component_size, container, margin);
assert_eq!(bottom_left, Point::new(5, 65));
let bottom_right =
ComponentPositioning::align_bottom_right(component_size, container, margin);
assert_eq!(bottom_right, Point::new(75, 65));
}
}