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}