1use super::theme::DashboardTheme;
8use crate::error::PdfError;
9use crate::graphics::Point;
10use crate::page::Page;
11
12pub trait DashboardComponent: std::fmt::Debug + DashboardComponentClone {
14 fn render(
16 &self,
17 page: &mut Page,
18 position: ComponentPosition,
19 theme: &DashboardTheme,
20 ) -> Result<(), PdfError>;
21
22 fn get_span(&self) -> ComponentSpan;
24
25 fn set_span(&mut self, span: ComponentSpan);
27
28 fn preferred_height(&self, available_width: f64) -> f64;
30
31 fn minimum_width(&self) -> f64 {
33 50.0 }
35
36 fn estimated_render_time_ms(&self) -> u32 {
38 10 }
40
41 fn estimated_memory_mb(&self) -> f64 {
43 0.1 }
45
46 fn complexity_score(&self) -> u8 {
48 25 }
50
51 fn component_type(&self) -> &'static str;
53
54 fn validate(&self) -> Result<(), PdfError> {
56 if self.get_span().columns < 1 || self.get_span().columns > 12 {
58 return Err(PdfError::InvalidOperation(format!(
59 "Invalid span: {}. Must be 1-12",
60 self.get_span().columns
61 )));
62 }
63 Ok(())
64 }
65}
66
67pub trait DashboardComponentClone {
69 fn clone_box(&self) -> Box<dyn DashboardComponent>;
70}
71
72impl<T> DashboardComponentClone for T
73where
74 T: 'static + DashboardComponent + Clone,
75{
76 fn clone_box(&self) -> Box<dyn DashboardComponent> {
77 Box::new(self.clone())
78 }
79}
80
81impl Clone for Box<dyn DashboardComponent> {
82 fn clone(&self) -> Box<dyn DashboardComponent> {
83 self.clone_box()
84 }
85}
86
87#[derive(Debug, Clone, Copy)]
89pub struct ComponentPosition {
90 pub x: f64,
92 pub y: f64,
94 pub width: f64,
96 pub height: f64,
98}
99
100impl ComponentPosition {
101 pub fn new(x: f64, y: f64, width: f64, height: f64) -> Self {
103 Self {
104 x,
105 y,
106 width,
107 height,
108 }
109 }
110
111 pub fn center(&self) -> Point {
113 Point::new(self.x + self.width / 2.0, self.y + self.height / 2.0)
114 }
115
116 pub fn top_left(&self) -> Point {
118 Point::new(self.x, self.y + self.height)
119 }
120
121 pub fn bottom_right(&self) -> Point {
123 Point::new(self.x + self.width, self.y)
124 }
125
126 pub fn with_padding(&self, padding: f64) -> Self {
128 Self {
129 x: self.x + padding,
130 y: self.y + padding,
131 width: self.width - 2.0 * padding,
132 height: self.height - 2.0 * padding,
133 }
134 }
135
136 pub fn contains(&self, point: Point) -> bool {
138 point.x >= self.x
139 && point.x <= self.x + self.width
140 && point.y >= self.y
141 && point.y <= self.y + self.height
142 }
143
144 pub fn aspect_ratio(&self) -> f64 {
146 if self.height > 0.0 {
147 self.width / self.height
148 } else {
149 1.0
150 }
151 }
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub struct ComponentSpan {
157 pub columns: u8,
159 pub rows: Option<u8>,
161}
162
163impl ComponentSpan {
164 pub fn new(columns: u8) -> Self {
166 Self {
167 columns: columns.clamp(1, 12),
168 rows: None,
169 }
170 }
171
172 pub fn with_rows(columns: u8, rows: u8) -> Self {
174 Self {
175 columns: columns.clamp(1, 12),
176 rows: Some(rows.max(1)),
177 }
178 }
179
180 pub fn as_fraction(&self) -> f64 {
182 self.columns as f64 / 12.0
183 }
184
185 pub fn is_full_width(&self) -> bool {
187 self.columns == 12
188 }
189
190 pub fn is_half_width(&self) -> bool {
192 self.columns == 6
193 }
194
195 pub fn is_quarter_width(&self) -> bool {
197 self.columns == 3
198 }
199}
200
201impl From<u8> for ComponentSpan {
202 fn from(columns: u8) -> Self {
203 Self::new(columns)
204 }
205}
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq)]
209pub enum ComponentAlignment {
210 Start,
212 Center,
214 End,
216 Stretch,
218}
219
220impl Default for ComponentAlignment {
221 fn default() -> Self {
222 Self::Stretch
223 }
224}
225
226#[derive(Debug, Clone, Copy)]
228pub struct ComponentMargin {
229 pub top: f64,
231 pub right: f64,
233 pub bottom: f64,
235 pub left: f64,
237}
238
239impl ComponentMargin {
240 pub fn uniform(margin: f64) -> Self {
242 Self {
243 top: margin,
244 right: margin,
245 bottom: margin,
246 left: margin,
247 }
248 }
249
250 pub fn symmetric(vertical: f64, horizontal: f64) -> Self {
252 Self {
253 top: vertical,
254 right: horizontal,
255 bottom: vertical,
256 left: horizontal,
257 }
258 }
259
260 pub fn new(top: f64, right: f64, bottom: f64, left: f64) -> Self {
262 Self {
263 top,
264 right,
265 bottom,
266 left,
267 }
268 }
269
270 pub fn horizontal(&self) -> f64 {
272 self.left + self.right
273 }
274
275 pub fn vertical(&self) -> f64 {
277 self.top + self.bottom
278 }
279}
280
281impl Default for ComponentMargin {
282 fn default() -> Self {
283 Self::uniform(8.0) }
285}
286
287#[derive(Debug, Clone)]
289pub struct ComponentConfig {
290 pub span: ComponentSpan,
292 pub alignment: ComponentAlignment,
294 pub margin: ComponentMargin,
296 pub id: Option<String>,
298 pub visible: bool,
300 pub classes: Vec<String>,
302}
303
304impl ComponentConfig {
305 pub fn new(span: ComponentSpan) -> Self {
307 Self {
308 span,
309 alignment: ComponentAlignment::default(),
310 margin: ComponentMargin::default(),
311 id: None,
312 visible: true,
313 classes: Vec::new(),
314 }
315 }
316
317 pub fn with_alignment(mut self, alignment: ComponentAlignment) -> Self {
319 self.alignment = alignment;
320 self
321 }
322
323 pub fn with_margin(mut self, margin: ComponentMargin) -> Self {
325 self.margin = margin;
326 self
327 }
328
329 pub fn with_id(mut self, id: String) -> Self {
331 self.id = Some(id);
332 self
333 }
334
335 pub fn with_class(mut self, class: String) -> Self {
337 self.classes.push(class);
338 self
339 }
340
341 pub fn with_visibility(mut self, visible: bool) -> Self {
343 self.visible = visible;
344 self
345 }
346}
347
348impl Default for ComponentConfig {
349 fn default() -> Self {
350 Self::new(ComponentSpan::new(12)) }
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357
358 #[test]
359 fn test_component_span() {
360 let span = ComponentSpan::new(6);
361 assert_eq!(span.columns, 6);
362 assert_eq!(span.as_fraction(), 0.5);
363 assert!(span.is_half_width());
364 assert!(!span.is_full_width());
365 }
366
367 #[test]
368 fn test_component_span_bounds() {
369 let span_too_large = ComponentSpan::new(15);
370 assert_eq!(span_too_large.columns, 12);
371
372 let span_too_small = ComponentSpan::new(0);
373 assert_eq!(span_too_small.columns, 1);
374 }
375
376 #[test]
377 fn test_component_position() {
378 let pos = ComponentPosition::new(100.0, 200.0, 300.0, 400.0);
379 let center = pos.center();
380
381 assert_eq!(center.x, 250.0);
382 assert_eq!(center.y, 400.0);
383 assert_eq!(pos.aspect_ratio(), 0.75);
384 }
385
386 #[test]
387 fn test_component_margin() {
388 let margin = ComponentMargin::uniform(10.0);
389 assert_eq!(margin.horizontal(), 20.0);
390 assert_eq!(margin.vertical(), 20.0);
391
392 let asymmetric = ComponentMargin::symmetric(5.0, 8.0);
393 assert_eq!(asymmetric.vertical(), 10.0);
394 assert_eq!(asymmetric.horizontal(), 16.0);
395 }
396
397 #[test]
398 fn test_component_config() {
399 let config = ComponentConfig::new(ComponentSpan::new(6))
400 .with_id("test-component".to_string())
401 .with_alignment(ComponentAlignment::Center)
402 .with_class("highlight".to_string());
403
404 assert_eq!(config.span.columns, 6);
405 assert_eq!(config.id, Some("test-component".to_string()));
406 assert_eq!(config.alignment, ComponentAlignment::Center);
407 assert!(config.classes.contains(&"highlight".to_string()));
408 }
409
410 #[test]
411 fn test_component_position_top_left() {
412 let pos = ComponentPosition::new(100.0, 200.0, 300.0, 400.0);
413 let top_left = pos.top_left();
414 assert_eq!(top_left.x, 100.0);
415 assert_eq!(top_left.y, 600.0); }
417
418 #[test]
419 fn test_component_position_bottom_right() {
420 let pos = ComponentPosition::new(100.0, 200.0, 300.0, 400.0);
421 let bottom_right = pos.bottom_right();
422 assert_eq!(bottom_right.x, 400.0); assert_eq!(bottom_right.y, 200.0);
424 }
425
426 #[test]
427 fn test_component_position_with_padding() {
428 let pos = ComponentPosition::new(100.0, 200.0, 300.0, 400.0);
429 let padded = pos.with_padding(10.0);
430
431 assert_eq!(padded.x, 110.0);
432 assert_eq!(padded.y, 210.0);
433 assert_eq!(padded.width, 280.0); assert_eq!(padded.height, 380.0); }
436
437 #[test]
438 fn test_component_position_contains() {
439 let pos = ComponentPosition::new(100.0, 200.0, 300.0, 400.0);
440
441 assert!(pos.contains(Point::new(200.0, 300.0)));
443
444 assert!(pos.contains(Point::new(100.0, 200.0))); assert!(pos.contains(Point::new(400.0, 600.0))); assert!(!pos.contains(Point::new(50.0, 300.0))); assert!(!pos.contains(Point::new(500.0, 300.0))); assert!(!pos.contains(Point::new(200.0, 100.0))); assert!(!pos.contains(Point::new(200.0, 700.0))); }
454
455 #[test]
456 fn test_component_position_aspect_ratio_zero_height() {
457 let pos = ComponentPosition::new(100.0, 200.0, 300.0, 0.0);
458 assert_eq!(pos.aspect_ratio(), 1.0); }
460
461 #[test]
462 fn test_component_span_with_rows() {
463 let span = ComponentSpan::with_rows(6, 2);
464 assert_eq!(span.columns, 6);
465 assert_eq!(span.rows, Some(2));
466
467 let span_clamped = ComponentSpan::with_rows(15, 0);
469 assert_eq!(span_clamped.columns, 12);
470 assert_eq!(span_clamped.rows, Some(1));
471 }
472
473 #[test]
474 fn test_component_span_is_quarter_width() {
475 let span = ComponentSpan::new(3);
476 assert!(span.is_quarter_width());
477 assert!(!span.is_half_width());
478 assert!(!span.is_full_width());
479 }
480
481 #[test]
482 fn test_component_span_from_u8() {
483 let span: ComponentSpan = 4u8.into();
484 assert_eq!(span.columns, 4);
485 assert!(span.rows.is_none());
486 }
487
488 #[test]
489 fn test_component_alignment_debug() {
490 let alignments = vec![
491 ComponentAlignment::Start,
492 ComponentAlignment::Center,
493 ComponentAlignment::End,
494 ComponentAlignment::Stretch,
495 ];
496
497 for alignment in alignments {
498 let debug_str = format!("{:?}", alignment);
499 assert!(!debug_str.is_empty());
500 }
501 }
502
503 #[test]
504 fn test_component_alignment_default() {
505 let default = ComponentAlignment::default();
506 assert_eq!(default, ComponentAlignment::Stretch);
507 }
508
509 #[test]
510 fn test_component_margin_new() {
511 let margin = ComponentMargin::new(1.0, 2.0, 3.0, 4.0);
512 assert_eq!(margin.top, 1.0);
513 assert_eq!(margin.right, 2.0);
514 assert_eq!(margin.bottom, 3.0);
515 assert_eq!(margin.left, 4.0);
516 }
517
518 #[test]
519 fn test_component_margin_default() {
520 let default = ComponentMargin::default();
521 assert_eq!(default.top, 8.0);
522 assert_eq!(default.right, 8.0);
523 assert_eq!(default.bottom, 8.0);
524 assert_eq!(default.left, 8.0);
525 }
526
527 #[test]
528 fn test_component_config_default() {
529 let default = ComponentConfig::default();
530 assert_eq!(default.span.columns, 12);
531 assert_eq!(default.alignment, ComponentAlignment::Stretch);
532 assert!(default.visible);
533 assert!(default.classes.is_empty());
534 assert!(default.id.is_none());
535 }
536
537 #[test]
538 fn test_component_config_with_margin() {
539 let config =
540 ComponentConfig::new(ComponentSpan::new(6)).with_margin(ComponentMargin::uniform(16.0));
541
542 assert_eq!(config.margin.top, 16.0);
543 assert_eq!(config.margin.horizontal(), 32.0);
544 }
545
546 #[test]
547 fn test_component_config_with_visibility() {
548 let config = ComponentConfig::new(ComponentSpan::new(6)).with_visibility(false);
549
550 assert!(!config.visible);
551 }
552
553 #[test]
554 fn test_component_config_clone() {
555 let config = ComponentConfig::new(ComponentSpan::new(6))
556 .with_id("test".to_string())
557 .with_class("class1".to_string());
558
559 let cloned = config.clone();
560 assert_eq!(config.span, cloned.span);
561 assert_eq!(config.id, cloned.id);
562 assert_eq!(config.classes.len(), cloned.classes.len());
563 }
564
565 #[test]
566 fn test_component_position_clone_copy() {
567 let pos = ComponentPosition::new(10.0, 20.0, 30.0, 40.0);
568 let cloned = pos.clone();
569 let copied = pos;
570
571 assert_eq!(pos.x, cloned.x);
572 assert_eq!(pos.y, copied.y);
573 }
574
575 #[test]
576 fn test_component_span_equality() {
577 let span1 = ComponentSpan::new(6);
578 let span2 = ComponentSpan::new(6);
579 let span3 = ComponentSpan::new(8);
580
581 assert_eq!(span1, span2);
582 assert_ne!(span1, span3);
583 }
584
585 #[test]
586 fn test_component_margin_clone_copy() {
587 let margin = ComponentMargin::new(1.0, 2.0, 3.0, 4.0);
588 let cloned = margin.clone();
589 let copied = margin;
590
591 assert_eq!(margin.top, cloned.top);
592 assert_eq!(margin.left, copied.left);
593 }
594}