use super::theme::DashboardTheme;
use crate::error::PdfError;
use crate::graphics::Point;
use crate::page::Page;
pub trait DashboardComponent: std::fmt::Debug + DashboardComponentClone {
fn render(
&self,
page: &mut Page,
position: ComponentPosition,
theme: &DashboardTheme,
) -> Result<(), PdfError>;
fn get_span(&self) -> ComponentSpan;
fn set_span(&mut self, span: ComponentSpan);
fn preferred_height(&self, available_width: f64) -> f64;
fn minimum_width(&self) -> f64 {
50.0 }
fn estimated_render_time_ms(&self) -> u32 {
10 }
fn estimated_memory_mb(&self) -> f64 {
0.1 }
fn complexity_score(&self) -> u8 {
25 }
fn component_type(&self) -> &'static str;
fn validate(&self) -> Result<(), PdfError> {
if self.get_span().columns < 1 || self.get_span().columns > 12 {
return Err(PdfError::InvalidOperation(format!(
"Invalid span: {}. Must be 1-12",
self.get_span().columns
)));
}
Ok(())
}
}
pub trait DashboardComponentClone {
fn clone_box(&self) -> Box<dyn DashboardComponent>;
}
impl<T> DashboardComponentClone for T
where
T: 'static + DashboardComponent + Clone,
{
fn clone_box(&self) -> Box<dyn DashboardComponent> {
Box::new(self.clone())
}
}
impl Clone for Box<dyn DashboardComponent> {
fn clone(&self) -> Box<dyn DashboardComponent> {
self.clone_box()
}
}
#[derive(Debug, Clone, Copy)]
pub struct ComponentPosition {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
}
impl ComponentPosition {
pub fn new(x: f64, y: f64, width: f64, height: f64) -> Self {
Self {
x,
y,
width,
height,
}
}
pub fn center(&self) -> Point {
Point::new(self.x + self.width / 2.0, self.y + self.height / 2.0)
}
pub fn top_left(&self) -> Point {
Point::new(self.x, self.y + self.height)
}
pub fn bottom_right(&self) -> Point {
Point::new(self.x + self.width, self.y)
}
pub fn with_padding(&self, padding: f64) -> Self {
Self {
x: self.x + padding,
y: self.y + padding,
width: self.width - 2.0 * padding,
height: self.height - 2.0 * padding,
}
}
pub fn contains(&self, point: Point) -> bool {
point.x >= self.x
&& point.x <= self.x + self.width
&& point.y >= self.y
&& point.y <= self.y + self.height
}
pub fn aspect_ratio(&self) -> f64 {
if self.height > 0.0 {
self.width / self.height
} else {
1.0
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ComponentSpan {
pub columns: u8,
pub rows: Option<u8>,
}
impl ComponentSpan {
pub fn new(columns: u8) -> Self {
Self {
columns: columns.clamp(1, 12),
rows: None,
}
}
pub fn with_rows(columns: u8, rows: u8) -> Self {
Self {
columns: columns.clamp(1, 12),
rows: Some(rows.max(1)),
}
}
pub fn as_fraction(&self) -> f64 {
self.columns as f64 / 12.0
}
pub fn is_full_width(&self) -> bool {
self.columns == 12
}
pub fn is_half_width(&self) -> bool {
self.columns == 6
}
pub fn is_quarter_width(&self) -> bool {
self.columns == 3
}
}
impl From<u8> for ComponentSpan {
fn from(columns: u8) -> Self {
Self::new(columns)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ComponentAlignment {
Start,
Center,
End,
Stretch,
}
impl Default for ComponentAlignment {
fn default() -> Self {
Self::Stretch
}
}
#[derive(Debug, Clone, Copy)]
pub struct ComponentMargin {
pub top: f64,
pub right: f64,
pub bottom: f64,
pub left: f64,
}
impl ComponentMargin {
pub fn uniform(margin: f64) -> Self {
Self {
top: margin,
right: margin,
bottom: margin,
left: margin,
}
}
pub fn symmetric(vertical: f64, horizontal: f64) -> Self {
Self {
top: vertical,
right: horizontal,
bottom: vertical,
left: horizontal,
}
}
pub fn new(top: f64, right: f64, bottom: f64, left: f64) -> Self {
Self {
top,
right,
bottom,
left,
}
}
pub fn horizontal(&self) -> f64 {
self.left + self.right
}
pub fn vertical(&self) -> f64 {
self.top + self.bottom
}
}
impl Default for ComponentMargin {
fn default() -> Self {
Self::uniform(8.0) }
}
#[derive(Debug, Clone)]
pub struct ComponentConfig {
pub span: ComponentSpan,
pub alignment: ComponentAlignment,
pub margin: ComponentMargin,
pub id: Option<String>,
pub visible: bool,
pub classes: Vec<String>,
}
impl ComponentConfig {
pub fn new(span: ComponentSpan) -> Self {
Self {
span,
alignment: ComponentAlignment::default(),
margin: ComponentMargin::default(),
id: None,
visible: true,
classes: Vec::new(),
}
}
pub fn with_alignment(mut self, alignment: ComponentAlignment) -> Self {
self.alignment = alignment;
self
}
pub fn with_margin(mut self, margin: ComponentMargin) -> Self {
self.margin = margin;
self
}
pub fn with_id(mut self, id: String) -> Self {
self.id = Some(id);
self
}
pub fn with_class(mut self, class: String) -> Self {
self.classes.push(class);
self
}
pub fn with_visibility(mut self, visible: bool) -> Self {
self.visible = visible;
self
}
}
impl Default for ComponentConfig {
fn default() -> Self {
Self::new(ComponentSpan::new(12)) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_component_span() {
let span = ComponentSpan::new(6);
assert_eq!(span.columns, 6);
assert_eq!(span.as_fraction(), 0.5);
assert!(span.is_half_width());
assert!(!span.is_full_width());
}
#[test]
fn test_component_span_bounds() {
let span_too_large = ComponentSpan::new(15);
assert_eq!(span_too_large.columns, 12);
let span_too_small = ComponentSpan::new(0);
assert_eq!(span_too_small.columns, 1);
}
#[test]
fn test_component_position() {
let pos = ComponentPosition::new(100.0, 200.0, 300.0, 400.0);
let center = pos.center();
assert_eq!(center.x, 250.0);
assert_eq!(center.y, 400.0);
assert_eq!(pos.aspect_ratio(), 0.75);
}
#[test]
fn test_component_margin() {
let margin = ComponentMargin::uniform(10.0);
assert_eq!(margin.horizontal(), 20.0);
assert_eq!(margin.vertical(), 20.0);
let asymmetric = ComponentMargin::symmetric(5.0, 8.0);
assert_eq!(asymmetric.vertical(), 10.0);
assert_eq!(asymmetric.horizontal(), 16.0);
}
#[test]
fn test_component_config() {
let config = ComponentConfig::new(ComponentSpan::new(6))
.with_id("test-component".to_string())
.with_alignment(ComponentAlignment::Center)
.with_class("highlight".to_string());
assert_eq!(config.span.columns, 6);
assert_eq!(config.id, Some("test-component".to_string()));
assert_eq!(config.alignment, ComponentAlignment::Center);
assert!(config.classes.contains(&"highlight".to_string()));
}
#[test]
fn test_component_position_top_left() {
let pos = ComponentPosition::new(100.0, 200.0, 300.0, 400.0);
let top_left = pos.top_left();
assert_eq!(top_left.x, 100.0);
assert_eq!(top_left.y, 600.0); }
#[test]
fn test_component_position_bottom_right() {
let pos = ComponentPosition::new(100.0, 200.0, 300.0, 400.0);
let bottom_right = pos.bottom_right();
assert_eq!(bottom_right.x, 400.0); assert_eq!(bottom_right.y, 200.0);
}
#[test]
fn test_component_position_with_padding() {
let pos = ComponentPosition::new(100.0, 200.0, 300.0, 400.0);
let padded = pos.with_padding(10.0);
assert_eq!(padded.x, 110.0);
assert_eq!(padded.y, 210.0);
assert_eq!(padded.width, 280.0); assert_eq!(padded.height, 380.0); }
#[test]
fn test_component_position_contains() {
let pos = ComponentPosition::new(100.0, 200.0, 300.0, 400.0);
assert!(pos.contains(Point::new(200.0, 300.0)));
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))); }
#[test]
fn test_component_position_aspect_ratio_zero_height() {
let pos = ComponentPosition::new(100.0, 200.0, 300.0, 0.0);
assert_eq!(pos.aspect_ratio(), 1.0); }
#[test]
fn test_component_span_with_rows() {
let span = ComponentSpan::with_rows(6, 2);
assert_eq!(span.columns, 6);
assert_eq!(span.rows, Some(2));
let span_clamped = ComponentSpan::with_rows(15, 0);
assert_eq!(span_clamped.columns, 12);
assert_eq!(span_clamped.rows, Some(1));
}
#[test]
fn test_component_span_is_quarter_width() {
let span = ComponentSpan::new(3);
assert!(span.is_quarter_width());
assert!(!span.is_half_width());
assert!(!span.is_full_width());
}
#[test]
fn test_component_span_from_u8() {
let span: ComponentSpan = 4u8.into();
assert_eq!(span.columns, 4);
assert!(span.rows.is_none());
}
#[test]
fn test_component_alignment_debug() {
let alignments = vec![
ComponentAlignment::Start,
ComponentAlignment::Center,
ComponentAlignment::End,
ComponentAlignment::Stretch,
];
for alignment in alignments {
let debug_str = format!("{:?}", alignment);
assert!(!debug_str.is_empty());
}
}
#[test]
fn test_component_alignment_default() {
let default = ComponentAlignment::default();
assert_eq!(default, ComponentAlignment::Stretch);
}
#[test]
fn test_component_margin_new() {
let margin = ComponentMargin::new(1.0, 2.0, 3.0, 4.0);
assert_eq!(margin.top, 1.0);
assert_eq!(margin.right, 2.0);
assert_eq!(margin.bottom, 3.0);
assert_eq!(margin.left, 4.0);
}
#[test]
fn test_component_margin_default() {
let default = ComponentMargin::default();
assert_eq!(default.top, 8.0);
assert_eq!(default.right, 8.0);
assert_eq!(default.bottom, 8.0);
assert_eq!(default.left, 8.0);
}
#[test]
fn test_component_config_default() {
let default = ComponentConfig::default();
assert_eq!(default.span.columns, 12);
assert_eq!(default.alignment, ComponentAlignment::Stretch);
assert!(default.visible);
assert!(default.classes.is_empty());
assert!(default.id.is_none());
}
#[test]
fn test_component_config_with_margin() {
let config =
ComponentConfig::new(ComponentSpan::new(6)).with_margin(ComponentMargin::uniform(16.0));
assert_eq!(config.margin.top, 16.0);
assert_eq!(config.margin.horizontal(), 32.0);
}
#[test]
fn test_component_config_with_visibility() {
let config = ComponentConfig::new(ComponentSpan::new(6)).with_visibility(false);
assert!(!config.visible);
}
#[test]
fn test_component_config_clone() {
let config = ComponentConfig::new(ComponentSpan::new(6))
.with_id("test".to_string())
.with_class("class1".to_string());
let cloned = config.clone();
assert_eq!(config.span, cloned.span);
assert_eq!(config.id, cloned.id);
assert_eq!(config.classes.len(), cloned.classes.len());
}
#[test]
fn test_component_position_clone_copy() {
let pos = ComponentPosition::new(10.0, 20.0, 30.0, 40.0);
let cloned = pos.clone();
let copied = pos;
assert_eq!(pos.x, cloned.x);
assert_eq!(pos.y, copied.y);
}
#[test]
fn test_component_span_equality() {
let span1 = ComponentSpan::new(6);
let span2 = ComponentSpan::new(6);
let span3 = ComponentSpan::new(8);
assert_eq!(span1, span2);
assert_ne!(span1, span3);
}
#[test]
fn test_component_margin_clone_copy() {
let margin = ComponentMargin::new(1.0, 2.0, 3.0, 4.0);
let cloned = margin.clone();
let copied = margin;
assert_eq!(margin.top, cloned.top);
assert_eq!(margin.left, copied.left);
}
}