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}