1use super::{ComponentPosition, ComponentSpan, DashboardComponent, DashboardConfig};
8use crate::error::PdfError;
9use std::collections::HashMap;
10
11#[derive(Debug, Clone)]
13pub struct DashboardLayout {
14 config: DashboardConfig,
16 grid: GridSystem,
18 position_cache: HashMap<String, ComponentPosition>,
20}
21
22impl DashboardLayout {
23 pub fn new(config: DashboardConfig) -> Self {
25 let grid = GridSystem::new(12, config.column_gutter, config.row_gutter);
26
27 Self {
28 config,
29 grid,
30 position_cache: HashMap::new(),
31 }
32 }
33
34 pub fn calculate_content_area(
36 &self,
37 page_bounds: (f64, f64, f64, f64),
38 ) -> (f64, f64, f64, f64) {
39 let (page_x, page_y, page_width, page_height) = page_bounds;
40 let (margin_top, margin_right, margin_bottom, margin_left) = self.config.margins;
41
42 let mut content_x = page_x + margin_left;
44 let content_y = page_y + margin_top;
45 let mut content_width = page_width - margin_left - margin_right;
46 let content_height = page_height
47 - margin_top
48 - margin_bottom
49 - self.config.header_height
50 - self.config.footer_height;
51
52 if self.config.max_content_width > 0.0 && content_width > self.config.max_content_width {
54 content_width = self.config.max_content_width;
55
56 if self.config.center_content {
58 content_x = page_x + (page_width - content_width) / 2.0;
59 }
60 }
61
62 (content_x, content_y, content_width, content_height)
63 }
64
65 pub fn calculate_positions(
67 &self,
68 components: &[Box<dyn DashboardComponent>],
69 content_area: (f64, f64, f64, f64),
70 ) -> Result<Vec<ComponentPosition>, PdfError> {
71 let (content_x, content_y, content_width, content_height) = content_area;
72
73 let layout_y = content_y + content_height - self.config.header_height;
75 let layout_height = content_height - self.config.header_height;
76
77 self.grid.layout_components(
79 components,
80 content_x,
81 layout_y,
82 content_width,
83 layout_height,
84 self.config.default_component_height,
85 )
86 }
87
88 pub fn get_stats(&self, components: &[Box<dyn DashboardComponent>]) -> LayoutStats {
90 let total_components = components.len();
91 let rows_used = self.estimate_rows_needed(components);
92 let column_utilization = self.calculate_column_utilization(components);
93
94 LayoutStats {
95 total_components,
96 rows_used,
97 column_utilization,
98 has_overflow: column_utilization > 1.0,
99 }
100 }
101
102 fn estimate_rows_needed(&self, components: &[Box<dyn DashboardComponent>]) -> usize {
104 let mut current_row_span = 0;
105 let mut rows = 0;
106
107 for component in components {
108 let span = component.get_span().columns;
109
110 if current_row_span + span > 12 {
111 rows += 1;
112 current_row_span = span;
113 } else {
114 current_row_span += span;
115 if current_row_span == 12 {
116 rows += 1;
117 current_row_span = 0;
118 }
119 }
120 }
121
122 if current_row_span > 0 {
123 rows += 1;
124 }
125
126 rows.max(1)
127 }
128
129 fn calculate_column_utilization(&self, components: &[Box<dyn DashboardComponent>]) -> f64 {
131 if components.is_empty() {
132 return 0.0;
133 }
134
135 let total_span: u32 = components.iter().map(|c| c.get_span().columns as u32).sum();
136
137 let estimated_rows = self.estimate_rows_needed(components) as u32;
138 let available_columns = estimated_rows * 12;
139
140 if available_columns > 0 {
141 total_span as f64 / available_columns as f64
142 } else {
143 1.0
144 }
145 }
146}
147
148#[derive(Debug, Clone)]
150pub struct GridSystem {
151 columns: u8,
153 column_gutter: f64,
155 row_gutter: f64,
157}
158
159impl GridSystem {
160 pub fn new(columns: u8, column_gutter: f64, row_gutter: f64) -> Self {
162 Self {
163 columns,
164 column_gutter,
165 row_gutter,
166 }
167 }
168
169 pub fn layout_components(
171 &self,
172 components: &[Box<dyn DashboardComponent>],
173 start_x: f64,
174 start_y: f64,
175 total_width: f64,
176 total_height: f64,
177 default_height: f64,
178 ) -> Result<Vec<ComponentPosition>, PdfError> {
179 let mut positions = Vec::new();
180
181 let mut current_y = start_y;
183 let mut row_start = 0;
184
185 let total_gutter_width = (self.columns as f64 - 1.0) * self.column_gutter;
187 let available_width = total_width - total_gutter_width;
188 let column_width = available_width / self.columns as f64;
189
190 let adjusted_height = (default_height * 0.6).max(120.0); while row_start < components.len() {
194 let row_end = self.find_row_end(components, row_start);
196 let row_components = &components[row_start..row_end];
197
198 let row_height = adjusted_height;
200
201 if current_y - row_height < start_y - total_height {
203 tracing::warn!(
204 "Dashboard components exceed available height, stopping at row {}",
205 positions.len() / row_components.len()
206 );
207 break;
208 }
209
210 let mut current_x = start_x;
212
213 for component in row_components {
214 let span = component.get_span();
215 let component_width = column_width * span.columns as f64
216 + self.column_gutter * (span.columns as f64 - 1.0);
217
218 positions.push(ComponentPosition::new(
220 current_x,
221 current_y - row_height,
222 component_width,
223 row_height,
224 ));
225
226 current_x += component_width + self.column_gutter;
227 }
228
229 current_y -= row_height + self.row_gutter;
231 row_start = row_end;
232 }
233
234 Ok(positions)
235 }
236
237 fn find_row_end(&self, components: &[Box<dyn DashboardComponent>], start: usize) -> usize {
239 let mut current_span = 0;
240 let mut end = start;
241
242 for (i, component) in components[start..].iter().enumerate() {
243 let span = component.get_span().columns;
244
245 if current_span + span > self.columns {
246 break;
247 }
248
249 current_span += span;
250 end = start + i + 1;
251
252 if current_span == self.columns {
253 break;
254 }
255 }
256
257 end.max(start + 1) }
259
260 fn calculate_row_height(
262 &self,
263 components: &[Box<dyn DashboardComponent>],
264 column_width: f64,
265 default_height: f64,
266 ) -> f64 {
267 components
268 .iter()
269 .map(|component| {
270 let span = component.get_span();
271 let available_width = column_width * span.columns as f64;
272 component.preferred_height(available_width)
273 })
274 .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
275 .unwrap_or(default_height)
276 }
277}
278
279#[derive(Debug, Clone)]
281pub struct LayoutManager {
282 state: LayoutState,
284 breakpoints: HashMap<String, f64>,
286}
287
288impl LayoutManager {
289 pub fn new() -> Self {
291 Self {
292 state: LayoutState::default(),
293 breakpoints: HashMap::new(),
294 }
295 }
296
297 pub fn add_breakpoint<T: Into<String>>(&mut self, name: T, width: f64) {
299 self.breakpoints.insert(name.into(), width);
300 }
301
302 pub fn get_current_breakpoint(&self, width: f64) -> String {
304 let mut best_match = "default".to_string();
305 let mut best_width = 0.0;
306
307 for (name, breakpoint_width) in &self.breakpoints {
308 if width >= *breakpoint_width && *breakpoint_width > best_width {
309 best_match = name.clone();
310 best_width = *breakpoint_width;
311 }
312 }
313
314 best_match
315 }
316
317 pub fn optimize_layout(
319 &self,
320 components: &mut [Box<dyn DashboardComponent>],
321 available_width: f64,
322 ) -> Result<(), PdfError> {
323 let breakpoint = self.get_current_breakpoint(available_width);
324
325 match breakpoint.as_str() {
327 "small" => self.apply_mobile_layout(components)?,
328 "medium" => self.apply_tablet_layout(components)?,
329 _ => {} }
331
332 Ok(())
333 }
334
335 fn apply_mobile_layout(
337 &self,
338 components: &mut [Box<dyn DashboardComponent>],
339 ) -> Result<(), PdfError> {
340 for component in components.iter_mut() {
341 component.set_span(ComponentSpan::new(12));
343 }
344 Ok(())
345 }
346
347 fn apply_tablet_layout(
349 &self,
350 components: &mut [Box<dyn DashboardComponent>],
351 ) -> Result<(), PdfError> {
352 for component in components.iter_mut() {
353 let current_span = component.get_span().columns;
354
355 let new_span = match current_span {
357 1..=3 => 6, 4..=6 => 6, 7..=12 => 12, _ => current_span,
361 };
362
363 component.set_span(ComponentSpan::new(new_span));
364 }
365 Ok(())
366 }
367}
368
369impl Default for LayoutManager {
370 fn default() -> Self {
371 Self::new()
372 }
373}
374
375#[derive(Debug, Clone)]
377pub struct LayoutState {
378 pub current_row: usize,
380 pub current_column: u8,
382 pub total_rows: usize,
384}
385
386impl Default for LayoutState {
387 fn default() -> Self {
388 Self {
389 current_row: 0,
390 current_column: 0,
391 total_rows: 0,
392 }
393 }
394}
395
396#[derive(Debug, Clone, Copy, PartialEq, Eq)]
398pub struct GridPosition {
399 pub row: usize,
401 pub column_start: u8,
403 pub column_span: u8,
405 pub row_span: u8,
407}
408
409impl GridPosition {
410 pub fn new(row: usize, column_start: u8, column_span: u8) -> Self {
412 Self {
413 row,
414 column_start,
415 column_span,
416 row_span: 1,
417 }
418 }
419
420 pub fn with_row_span(mut self, row_span: u8) -> Self {
422 self.row_span = row_span;
423 self
424 }
425
426 pub fn column_end(&self) -> u8 {
428 self.column_start + self.column_span
429 }
430
431 pub fn overlaps(&self, other: &GridPosition) -> bool {
433 self.row < other.row + other.row_span as usize
434 && other.row < self.row + self.row_span as usize
435 && self.column_start < other.column_end()
436 && other.column_start < self.column_end()
437 }
438}
439
440#[derive(Debug, Clone)]
442pub struct LayoutStats {
443 pub total_components: usize,
445 pub rows_used: usize,
447 pub column_utilization: f64,
449 pub has_overflow: bool,
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456
457 #[test]
458 fn test_grid_system() {
459 let grid = GridSystem::new(12, 15.0, 20.0);
460 assert_eq!(grid.columns, 12);
461 assert_eq!(grid.column_gutter, 15.0);
462 assert_eq!(grid.row_gutter, 20.0);
463 }
464
465 #[test]
466 fn test_grid_position() {
467 let pos1 = GridPosition::new(0, 0, 6);
468 let pos2 = GridPosition::new(0, 6, 6);
469 let pos3 = GridPosition::new(0, 3, 6);
470
471 assert!(!pos1.overlaps(&pos2));
472 assert!(pos1.overlaps(&pos3));
473 assert_eq!(pos1.column_end(), 6);
474 }
475
476 #[test]
477 fn test_layout_manager_breakpoints() {
478 let mut manager = LayoutManager::new();
479 manager.add_breakpoint("small", 400.0);
480 manager.add_breakpoint("medium", 768.0);
481 manager.add_breakpoint("large", 1024.0);
482
483 assert_eq!(manager.get_current_breakpoint(300.0), "default");
484 assert_eq!(manager.get_current_breakpoint(500.0), "small");
485 assert_eq!(manager.get_current_breakpoint(800.0), "medium");
486 assert_eq!(manager.get_current_breakpoint(1200.0), "large");
487 }
488
489 #[test]
490 fn test_dashboard_layout_content_area() {
491 let config = DashboardConfig::default();
492 let layout = DashboardLayout::new(config);
493
494 let page_bounds = (0.0, 0.0, 800.0, 600.0);
495 let content_area = layout.calculate_content_area(page_bounds);
496
497 assert_eq!(content_area.0, 30.0); assert!(content_area.2 < 800.0); assert!(content_area.3 < 600.0); }
502
503 #[test]
504 fn test_grid_position_with_row_span() {
505 let pos = GridPosition::new(0, 0, 6).with_row_span(2);
506 assert_eq!(pos.row, 0);
507 assert_eq!(pos.column_start, 0);
508 assert_eq!(pos.column_span, 6);
509 assert_eq!(pos.row_span, 2);
510 }
511
512 #[test]
513 fn test_grid_position_column_end() {
514 let pos = GridPosition::new(0, 3, 5);
515 assert_eq!(pos.column_end(), 8);
516 }
517
518 #[test]
519 fn test_grid_position_overlaps_same_row() {
520 let pos1 = GridPosition::new(0, 0, 4);
521 let pos2 = GridPosition::new(0, 2, 4);
522 assert!(pos1.overlaps(&pos2));
523 assert!(pos2.overlaps(&pos1));
524 }
525
526 #[test]
527 fn test_grid_position_overlaps_with_row_span() {
528 let pos1 = GridPosition::new(0, 0, 6).with_row_span(2);
529 let pos2 = GridPosition::new(1, 0, 6);
530 assert!(pos1.overlaps(&pos2));
531 }
532
533 #[test]
534 fn test_grid_position_no_overlap_different_rows() {
535 let pos1 = GridPosition::new(0, 0, 6);
536 let pos2 = GridPosition::new(2, 0, 6);
537 assert!(!pos1.overlaps(&pos2));
538 }
539
540 #[test]
541 fn test_grid_position_equality() {
542 let pos1 = GridPosition::new(1, 2, 3);
543 let pos2 = GridPosition::new(1, 2, 3);
544 let pos3 = GridPosition::new(1, 2, 4);
545 assert_eq!(pos1, pos2);
546 assert_ne!(pos1, pos3);
547 }
548
549 #[test]
550 fn test_layout_state_default() {
551 let state = LayoutState::default();
552 assert_eq!(state.current_row, 0);
553 assert_eq!(state.current_column, 0);
554 assert_eq!(state.total_rows, 0);
555 }
556
557 #[test]
558 fn test_layout_state_clone() {
559 let state = LayoutState {
560 current_row: 2,
561 current_column: 5,
562 total_rows: 3,
563 };
564 let cloned = state.clone();
565 assert_eq!(state.current_row, cloned.current_row);
566 assert_eq!(state.current_column, cloned.current_column);
567 assert_eq!(state.total_rows, cloned.total_rows);
568 }
569
570 #[test]
571 fn test_layout_manager_default() {
572 let manager = LayoutManager::default();
573 assert_eq!(manager.get_current_breakpoint(500.0), "default");
574 }
575
576 #[test]
577 fn test_layout_manager_no_breakpoints() {
578 let manager = LayoutManager::new();
579 assert_eq!(manager.get_current_breakpoint(0.0), "default");
580 assert_eq!(manager.get_current_breakpoint(10000.0), "default");
581 }
582
583 #[test]
584 fn test_layout_stats_debug() {
585 let stats = LayoutStats {
586 total_components: 5,
587 rows_used: 2,
588 column_utilization: 0.8,
589 has_overflow: false,
590 };
591 let debug_str = format!("{:?}", stats);
592 assert!(debug_str.contains("LayoutStats"));
593 assert!(debug_str.contains("5"));
594 }
595
596 #[test]
597 fn test_layout_stats_clone() {
598 let stats = LayoutStats {
599 total_components: 3,
600 rows_used: 1,
601 column_utilization: 0.5,
602 has_overflow: true,
603 };
604 let cloned = stats.clone();
605 assert_eq!(stats.total_components, cloned.total_components);
606 assert_eq!(stats.rows_used, cloned.rows_used);
607 assert_eq!(stats.column_utilization, cloned.column_utilization);
608 assert_eq!(stats.has_overflow, cloned.has_overflow);
609 }
610
611 #[test]
612 fn test_grid_system_clone() {
613 let grid = GridSystem::new(12, 10.0, 15.0);
614 let cloned = grid.clone();
615 assert_eq!(grid.columns, cloned.columns);
616 assert_eq!(grid.column_gutter, cloned.column_gutter);
617 assert_eq!(grid.row_gutter, cloned.row_gutter);
618 }
619
620 #[test]
621 fn test_grid_system_debug() {
622 let grid = GridSystem::new(8, 5.0, 10.0);
623 let debug_str = format!("{:?}", grid);
624 assert!(debug_str.contains("GridSystem"));
625 assert!(debug_str.contains("8"));
626 }
627
628 #[test]
629 fn test_layout_manager_clone() {
630 let mut manager = LayoutManager::new();
631 manager.add_breakpoint("test", 500.0);
632 let cloned = manager.clone();
633 assert_eq!(
634 manager.get_current_breakpoint(600.0),
635 cloned.get_current_breakpoint(600.0)
636 );
637 }
638
639 #[test]
640 fn test_dashboard_layout_clone() {
641 let config = DashboardConfig::default();
642 let layout = DashboardLayout::new(config);
643 let cloned = layout.clone();
644 let page_bounds = (0.0, 0.0, 800.0, 600.0);
645 assert_eq!(
646 layout.calculate_content_area(page_bounds),
647 cloned.calculate_content_area(page_bounds)
648 );
649 }
650}