1use crate::chart::traits::Margins;
4use crate::error::{LayoutError, LayoutResult};
5use embedded_graphics::{prelude::*, primitives::Rectangle};
6
7#[derive(Debug, Clone)]
9pub struct ChartLayout {
10 pub total_area: Rectangle,
12 pub chart_area: Rectangle,
14 pub title_area: Option<Rectangle>,
16 pub legend_area: Option<Rectangle>,
18 pub x_axis_area: Option<Rectangle>,
20 pub y_axis_area: Option<Rectangle>,
22}
23
24impl ChartLayout {
25 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 pub fn with_margins(mut self, margins: Margins) -> Self {
39 self.chart_area = margins.apply_to(self.total_area);
40 self
41 }
42
43 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 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 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 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 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 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 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 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 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 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 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 pub fn chart_area(&self) -> Rectangle {
215 self.chart_area
216 }
217
218 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
229pub enum LegendPosition {
230 Top,
232 Right,
234 Bottom,
236 Left,
238}
239
240#[derive(Debug, Clone, Copy, PartialEq)]
242pub struct Viewport {
243 pub area: Rectangle,
245 pub zoom: f32,
247 pub offset: Point,
249}
250
251impl Viewport {
252 pub fn new(area: Rectangle) -> Self {
254 Self {
255 area,
256 zoom: 1.0,
257 offset: Point::zero(),
258 }
259 }
260
261 pub fn with_zoom(mut self, zoom: f32) -> Self {
263 self.zoom = zoom.clamp(0.1, 10.0); self
265 }
266
267 pub fn with_offset(mut self, offset: Point) -> Self {
269 self.offset = offset;
270 self
271 }
272
273 pub fn transform_point(&self, data_point: Point, data_bounds: Rectangle) -> Point {
275 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 let zoomed_x = norm_x * self.zoom;
290 let zoomed_y = norm_y * self.zoom;
291
292 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 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 pub fn visible_data_bounds(&self, full_data_bounds: Rectangle) -> Rectangle {
311 full_data_bounds
314 }
315}
316
317pub struct ComponentPositioning;
319
320impl ComponentPositioning {
321 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 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 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 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 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 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 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}