presentar_widgets/
model_card.rs

1//! `ModelCard` widget for displaying ML model metadata.
2
3use presentar_core::{
4    widget::{AccessibleRole, LayoutResult, TextStyle},
5    Canvas, Color, Constraints, Point, Rect, Size, TypeId, Widget,
6};
7use serde::{Deserialize, Serialize};
8use std::any::Any;
9use std::collections::HashMap;
10
11/// Model status indicator.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
13pub enum ModelStatus {
14    /// Draft/in development
15    #[default]
16    Draft,
17    /// Under review
18    Review,
19    /// Published/production ready
20    Published,
21    /// Deprecated
22    Deprecated,
23    /// Archived
24    Archived,
25}
26
27impl ModelStatus {
28    /// Get display color for status.
29    #[must_use]
30    pub fn color(&self) -> Color {
31        match self {
32            Self::Draft => Color::new(0.6, 0.6, 0.6, 1.0),
33            Self::Review => Color::new(0.9, 0.7, 0.1, 1.0),
34            Self::Published => Color::new(0.2, 0.7, 0.3, 1.0),
35            Self::Deprecated => Color::new(0.9, 0.5, 0.1, 1.0),
36            Self::Archived => Color::new(0.5, 0.5, 0.5, 1.0),
37        }
38    }
39
40    /// Get status label.
41    #[must_use]
42    pub const fn label(&self) -> &'static str {
43        match self {
44            Self::Draft => "Draft",
45            Self::Review => "Review",
46            Self::Published => "Published",
47            Self::Deprecated => "Deprecated",
48            Self::Archived => "Archived",
49        }
50    }
51}
52
53/// Model metric (e.g., accuracy, F1 score).
54#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
55pub struct ModelMetric {
56    /// Metric name
57    pub name: String,
58    /// Metric value
59    pub value: f64,
60    /// Optional unit
61    pub unit: Option<String>,
62    /// Higher is better
63    pub higher_is_better: bool,
64}
65
66impl ModelMetric {
67    /// Create a new metric.
68    #[must_use]
69    pub fn new(name: impl Into<String>, value: f64) -> Self {
70        Self {
71            name: name.into(),
72            value,
73            unit: None,
74            higher_is_better: true,
75        }
76    }
77
78    /// Set unit.
79    #[must_use]
80    pub fn unit(mut self, unit: impl Into<String>) -> Self {
81        self.unit = Some(unit.into());
82        self
83    }
84
85    /// Set lower is better.
86    #[must_use]
87    pub const fn lower_is_better(mut self) -> Self {
88        self.higher_is_better = false;
89        self
90    }
91
92    /// Format the value for display.
93    #[must_use]
94    pub fn formatted_value(&self) -> String {
95        if let Some(ref unit) = self.unit {
96            format!("{:.2}{}", self.value, unit)
97        } else if self.value.abs() < 1.0 {
98            format!("{:.2}%", self.value * 100.0)
99        } else {
100            format!("{:.2}", self.value)
101        }
102    }
103}
104
105/// `ModelCard` widget for displaying ML model metadata.
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct ModelCard {
108    /// Model name
109    name: String,
110    /// Model version
111    version: String,
112    /// Model description
113    description: Option<String>,
114    /// Model status
115    status: ModelStatus,
116    /// Model type/framework (e.g., `PyTorch`, `TensorFlow`)
117    framework: Option<String>,
118    /// Model task (e.g., "classification", "regression")
119    task: Option<String>,
120    /// Performance metrics
121    metrics: Vec<ModelMetric>,
122    /// Parameter count
123    parameters: Option<u64>,
124    /// Training dataset name
125    dataset: Option<String>,
126    /// Author/owner
127    author: Option<String>,
128    /// Tags
129    tags: Vec<String>,
130    /// Custom metadata
131    metadata: HashMap<String, String>,
132    /// Card width
133    width: Option<f32>,
134    /// Card height
135    height: Option<f32>,
136    /// Background color
137    background: Color,
138    /// Border color
139    border_color: Color,
140    /// Corner radius
141    corner_radius: f32,
142    /// Show metrics chart
143    show_metrics_chart: bool,
144    /// Accessible name
145    accessible_name_value: Option<String>,
146    /// Test ID
147    test_id_value: Option<String>,
148    /// Cached bounds
149    #[serde(skip)]
150    bounds: Rect,
151}
152
153impl Default for ModelCard {
154    fn default() -> Self {
155        Self {
156            name: String::new(),
157            version: String::from("1.0.0"),
158            description: None,
159            status: ModelStatus::Draft,
160            framework: None,
161            task: None,
162            metrics: Vec::new(),
163            parameters: None,
164            dataset: None,
165            author: None,
166            tags: Vec::new(),
167            metadata: HashMap::new(),
168            width: None,
169            height: None,
170            background: Color::WHITE,
171            border_color: Color::new(0.9, 0.9, 0.9, 1.0),
172            corner_radius: 8.0,
173            show_metrics_chart: true,
174            accessible_name_value: None,
175            test_id_value: None,
176            bounds: Rect::default(),
177        }
178    }
179}
180
181impl ModelCard {
182    /// Create a new model card.
183    #[must_use]
184    pub fn new(name: impl Into<String>) -> Self {
185        Self {
186            name: name.into(),
187            ..Self::default()
188        }
189    }
190
191    /// Set model name.
192    #[must_use]
193    pub fn name(mut self, name: impl Into<String>) -> Self {
194        self.name = name.into();
195        self
196    }
197
198    /// Set version.
199    #[must_use]
200    pub fn version(mut self, version: impl Into<String>) -> Self {
201        self.version = version.into();
202        self
203    }
204
205    /// Set description.
206    #[must_use]
207    pub fn description(mut self, desc: impl Into<String>) -> Self {
208        self.description = Some(desc.into());
209        self
210    }
211
212    /// Set status.
213    #[must_use]
214    pub const fn status(mut self, status: ModelStatus) -> Self {
215        self.status = status;
216        self
217    }
218
219    /// Set framework.
220    #[must_use]
221    pub fn framework(mut self, framework: impl Into<String>) -> Self {
222        self.framework = Some(framework.into());
223        self
224    }
225
226    /// Set task.
227    #[must_use]
228    pub fn task(mut self, task: impl Into<String>) -> Self {
229        self.task = Some(task.into());
230        self
231    }
232
233    /// Add a metric.
234    #[must_use]
235    pub fn metric(mut self, metric: ModelMetric) -> Self {
236        self.metrics.push(metric);
237        self
238    }
239
240    /// Add multiple metrics.
241    #[must_use]
242    pub fn metrics(mut self, metrics: impl IntoIterator<Item = ModelMetric>) -> Self {
243        self.metrics.extend(metrics);
244        self
245    }
246
247    /// Set parameter count.
248    #[must_use]
249    pub const fn parameters(mut self, count: u64) -> Self {
250        self.parameters = Some(count);
251        self
252    }
253
254    /// Set dataset.
255    #[must_use]
256    pub fn dataset(mut self, dataset: impl Into<String>) -> Self {
257        self.dataset = Some(dataset.into());
258        self
259    }
260
261    /// Set author.
262    #[must_use]
263    pub fn author(mut self, author: impl Into<String>) -> Self {
264        self.author = Some(author.into());
265        self
266    }
267
268    /// Add a tag.
269    #[must_use]
270    pub fn tag(mut self, tag: impl Into<String>) -> Self {
271        self.tags.push(tag.into());
272        self
273    }
274
275    /// Add multiple tags.
276    #[must_use]
277    pub fn tags(mut self, tags: impl IntoIterator<Item = impl Into<String>>) -> Self {
278        self.tags.extend(tags.into_iter().map(Into::into));
279        self
280    }
281
282    /// Add custom metadata.
283    #[must_use]
284    pub fn metadata_entry(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
285        self.metadata.insert(key.into(), value.into());
286        self
287    }
288
289    /// Set width.
290    #[must_use]
291    pub fn width(mut self, width: f32) -> Self {
292        self.width = Some(width.max(200.0));
293        self
294    }
295
296    /// Set height.
297    #[must_use]
298    pub fn height(mut self, height: f32) -> Self {
299        self.height = Some(height.max(150.0));
300        self
301    }
302
303    /// Set background color.
304    #[must_use]
305    pub const fn background(mut self, color: Color) -> Self {
306        self.background = color;
307        self
308    }
309
310    /// Set border color.
311    #[must_use]
312    pub const fn border_color(mut self, color: Color) -> Self {
313        self.border_color = color;
314        self
315    }
316
317    /// Set corner radius.
318    #[must_use]
319    pub fn corner_radius(mut self, radius: f32) -> Self {
320        self.corner_radius = radius.max(0.0);
321        self
322    }
323
324    /// Set whether to show metrics chart.
325    #[must_use]
326    pub const fn show_metrics_chart(mut self, show: bool) -> Self {
327        self.show_metrics_chart = show;
328        self
329    }
330
331    /// Set accessible name.
332    #[must_use]
333    pub fn accessible_name(mut self, name: impl Into<String>) -> Self {
334        self.accessible_name_value = Some(name.into());
335        self
336    }
337
338    /// Set test ID.
339    #[must_use]
340    pub fn test_id(mut self, id: impl Into<String>) -> Self {
341        self.test_id_value = Some(id.into());
342        self
343    }
344
345    // Getters
346
347    /// Get model name.
348    #[must_use]
349    pub fn get_name(&self) -> &str {
350        &self.name
351    }
352
353    /// Get version.
354    #[must_use]
355    pub fn get_version(&self) -> &str {
356        &self.version
357    }
358
359    /// Get description.
360    #[must_use]
361    pub fn get_description(&self) -> Option<&str> {
362        self.description.as_deref()
363    }
364
365    /// Get status.
366    #[must_use]
367    pub const fn get_status(&self) -> ModelStatus {
368        self.status
369    }
370
371    /// Get framework.
372    #[must_use]
373    pub fn get_framework(&self) -> Option<&str> {
374        self.framework.as_deref()
375    }
376
377    /// Get task.
378    #[must_use]
379    pub fn get_task(&self) -> Option<&str> {
380        self.task.as_deref()
381    }
382
383    /// Get metrics.
384    #[must_use]
385    pub fn get_metrics(&self) -> &[ModelMetric] {
386        &self.metrics
387    }
388
389    /// Get parameter count.
390    #[must_use]
391    pub const fn get_parameters(&self) -> Option<u64> {
392        self.parameters
393    }
394
395    /// Get dataset.
396    #[must_use]
397    pub fn get_dataset(&self) -> Option<&str> {
398        self.dataset.as_deref()
399    }
400
401    /// Get author.
402    #[must_use]
403    pub fn get_author(&self) -> Option<&str> {
404        self.author.as_deref()
405    }
406
407    /// Get tags.
408    #[must_use]
409    pub fn get_tags(&self) -> &[String] {
410        &self.tags
411    }
412
413    /// Get custom metadata.
414    #[must_use]
415    pub fn get_metadata(&self, key: &str) -> Option<&str> {
416        self.metadata.get(key).map(String::as_str)
417    }
418
419    /// Check if model has metrics.
420    #[must_use]
421    pub fn has_metrics(&self) -> bool {
422        !self.metrics.is_empty()
423    }
424
425    /// Format parameter count for display.
426    #[must_use]
427    pub fn formatted_parameters(&self) -> Option<String> {
428        self.parameters.map(|p| {
429            if p >= 1_000_000_000 {
430                format!("{:.1}B", p as f64 / 1_000_000_000.0)
431            } else if p >= 1_000_000 {
432                format!("{:.1}M", p as f64 / 1_000_000.0)
433            } else if p >= 1_000 {
434                format!("{:.1}K", p as f64 / 1_000.0)
435            } else {
436                format!("{p}")
437            }
438        })
439    }
440}
441
442impl Widget for ModelCard {
443    fn type_id(&self) -> TypeId {
444        TypeId::of::<Self>()
445    }
446
447    fn measure(&self, constraints: Constraints) -> Size {
448        let width = self.width.unwrap_or(320.0);
449        let height = self.height.unwrap_or(200.0);
450        constraints.constrain(Size::new(width, height))
451    }
452
453    fn layout(&mut self, bounds: Rect) -> LayoutResult {
454        self.bounds = bounds;
455        LayoutResult {
456            size: bounds.size(),
457        }
458    }
459
460    #[allow(clippy::too_many_lines)]
461    fn paint(&self, canvas: &mut dyn Canvas) {
462        let padding = 16.0;
463
464        // Background
465        canvas.fill_rect(self.bounds, self.background);
466
467        // Border
468        canvas.stroke_rect(self.bounds, self.border_color, 1.0);
469
470        // Status badge
471        let status_color = self.status.color();
472        let badge_rect = Rect::new(
473            self.bounds.x + self.bounds.width - 80.0,
474            self.bounds.y + padding,
475            70.0,
476            22.0,
477        );
478        canvas.fill_rect(badge_rect, status_color);
479
480        let badge_style = TextStyle {
481            size: 10.0,
482            color: Color::WHITE,
483            ..TextStyle::default()
484        };
485        canvas.draw_text(
486            self.status.label(),
487            Point::new(badge_rect.x + 10.0, badge_rect.y + 15.0),
488            &badge_style,
489        );
490
491        // Title
492        let title_style = TextStyle {
493            size: 18.0,
494            color: Color::new(0.1, 0.1, 0.1, 1.0),
495            ..TextStyle::default()
496        };
497        canvas.draw_text(
498            &self.name,
499            Point::new(self.bounds.x + padding, self.bounds.y + padding + 16.0),
500            &title_style,
501        );
502
503        // Version
504        let version_style = TextStyle {
505            size: 12.0,
506            color: Color::new(0.5, 0.5, 0.5, 1.0),
507            ..TextStyle::default()
508        };
509        canvas.draw_text(
510            &format!("v{}", self.version),
511            Point::new(self.bounds.x + padding, self.bounds.y + padding + 36.0),
512            &version_style,
513        );
514
515        // Description (if any)
516        let mut y_offset = padding + 50.0;
517        if let Some(ref desc) = self.description {
518            let desc_style = TextStyle {
519                size: 12.0,
520                color: Color::new(0.3, 0.3, 0.3, 1.0),
521                ..TextStyle::default()
522            };
523            canvas.draw_text(
524                desc,
525                Point::new(self.bounds.x + padding, self.bounds.y + y_offset + 12.0),
526                &desc_style,
527            );
528            y_offset += 24.0;
529        }
530
531        // Framework and task
532        if self.framework.is_some() || self.task.is_some() {
533            let info_style = TextStyle {
534                size: 11.0,
535                color: Color::new(0.4, 0.4, 0.4, 1.0),
536                ..TextStyle::default()
537            };
538            let info_text = match (&self.framework, &self.task) {
539                (Some(f), Some(t)) => format!("{f} • {t}"),
540                (Some(f), None) => f.clone(),
541                (None, Some(t)) => t.clone(),
542                (None, None) => String::new(),
543            };
544            canvas.draw_text(
545                &info_text,
546                Point::new(self.bounds.x + padding, self.bounds.y + y_offset + 12.0),
547                &info_style,
548            );
549            y_offset += 20.0;
550        }
551
552        // Parameters
553        if let Some(params) = self.formatted_parameters() {
554            let params_style = TextStyle {
555                size: 11.0,
556                color: Color::new(0.4, 0.4, 0.4, 1.0),
557                ..TextStyle::default()
558            };
559            canvas.draw_text(
560                &format!("Parameters: {params}"),
561                Point::new(self.bounds.x + padding, self.bounds.y + y_offset + 12.0),
562                &params_style,
563            );
564            y_offset += 18.0;
565        }
566
567        // Metrics
568        if self.show_metrics_chart && !self.metrics.is_empty() {
569            let metric_style = TextStyle {
570                size: 11.0,
571                color: Color::new(0.2, 0.2, 0.2, 1.0),
572                ..TextStyle::default()
573            };
574            let value_style = TextStyle {
575                size: 14.0,
576                color: Color::new(0.2, 0.47, 0.96, 1.0),
577                ..TextStyle::default()
578            };
579
580            let metric_width =
581                (self.bounds.width - padding * 2.0) / self.metrics.len().min(4) as f32;
582            for (i, metric) in self.metrics.iter().take(4).enumerate() {
583                let mx = (i as f32).mul_add(metric_width, self.bounds.x + padding);
584                canvas.draw_text(
585                    &metric.name,
586                    Point::new(mx, self.bounds.y + y_offset + 12.0),
587                    &metric_style,
588                );
589                canvas.draw_text(
590                    &metric.formatted_value(),
591                    Point::new(mx, self.bounds.y + y_offset + 28.0),
592                    &value_style,
593                );
594            }
595            y_offset += 40.0;
596        }
597
598        // Tags
599        if !self.tags.is_empty() {
600            let tag_style = TextStyle {
601                size: 10.0,
602                color: Color::new(0.3, 0.3, 0.3, 1.0),
603                ..TextStyle::default()
604            };
605            let tag_bg = Color::new(0.95, 0.95, 0.95, 1.0);
606
607            let mut tx = self.bounds.x + padding;
608            for tag in self.tags.iter().take(5) {
609                let tag_width = (tag.len() as f32).mul_add(6.0, 12.0);
610                canvas.fill_rect(
611                    Rect::new(tx, self.bounds.y + y_offset, tag_width, 18.0),
612                    tag_bg,
613                );
614                canvas.draw_text(
615                    tag,
616                    Point::new(tx + 6.0, self.bounds.y + y_offset + 13.0),
617                    &tag_style,
618                );
619                tx += tag_width + 6.0;
620            }
621        }
622    }
623
624    fn event(&mut self, _event: &presentar_core::Event) -> Option<Box<dyn Any + Send>> {
625        None
626    }
627
628    fn children(&self) -> &[Box<dyn Widget>] {
629        &[]
630    }
631
632    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
633        &mut []
634    }
635
636    fn is_interactive(&self) -> bool {
637        false
638    }
639
640    fn is_focusable(&self) -> bool {
641        false
642    }
643
644    fn accessible_name(&self) -> Option<&str> {
645        self.accessible_name_value.as_deref().or(Some(&self.name))
646    }
647
648    fn accessible_role(&self) -> AccessibleRole {
649        AccessibleRole::Generic
650    }
651
652    fn test_id(&self) -> Option<&str> {
653        self.test_id_value.as_deref()
654    }
655}
656
657#[cfg(test)]
658mod tests {
659    use super::*;
660
661    // ===== ModelStatus Tests =====
662
663    #[test]
664    fn test_model_status_default() {
665        assert_eq!(ModelStatus::default(), ModelStatus::Draft);
666    }
667
668    #[test]
669    fn test_model_status_color() {
670        let published = ModelStatus::Published;
671        let color = published.color();
672        assert!(color.g > color.r); // Green-ish
673    }
674
675    #[test]
676    fn test_model_status_label() {
677        assert_eq!(ModelStatus::Draft.label(), "Draft");
678        assert_eq!(ModelStatus::Review.label(), "Review");
679        assert_eq!(ModelStatus::Published.label(), "Published");
680        assert_eq!(ModelStatus::Deprecated.label(), "Deprecated");
681        assert_eq!(ModelStatus::Archived.label(), "Archived");
682    }
683
684    // ===== ModelMetric Tests =====
685
686    #[test]
687    fn test_model_metric_new() {
688        let metric = ModelMetric::new("Accuracy", 0.95);
689        assert_eq!(metric.name, "Accuracy");
690        assert_eq!(metric.value, 0.95);
691        assert!(metric.unit.is_none());
692        assert!(metric.higher_is_better);
693    }
694
695    #[test]
696    fn test_model_metric_unit() {
697        let metric = ModelMetric::new("Latency", 45.0).unit("ms");
698        assert_eq!(metric.unit, Some("ms".to_string()));
699    }
700
701    #[test]
702    fn test_model_metric_lower_is_better() {
703        let metric = ModelMetric::new("Loss", 0.05).lower_is_better();
704        assert!(!metric.higher_is_better);
705    }
706
707    #[test]
708    fn test_model_metric_formatted_value_percentage() {
709        let metric = ModelMetric::new("Accuracy", 0.95);
710        assert_eq!(metric.formatted_value(), "95.00%");
711    }
712
713    #[test]
714    fn test_model_metric_formatted_value_with_unit() {
715        let metric = ModelMetric::new("Latency", 45.0).unit("ms");
716        assert_eq!(metric.formatted_value(), "45.00ms");
717    }
718
719    #[test]
720    fn test_model_metric_formatted_value_large() {
721        let metric = ModelMetric::new("Score", 1234.5);
722        assert_eq!(metric.formatted_value(), "1234.50");
723    }
724
725    // ===== ModelCard Construction Tests =====
726
727    #[test]
728    fn test_model_card_new() {
729        let card = ModelCard::new("GPT-4");
730        assert_eq!(card.get_name(), "GPT-4");
731        assert_eq!(card.get_version(), "1.0.0");
732        assert_eq!(card.get_status(), ModelStatus::Draft);
733    }
734
735    #[test]
736    fn test_model_card_default() {
737        let card = ModelCard::default();
738        assert!(card.name.is_empty());
739        assert_eq!(card.version, "1.0.0");
740    }
741
742    #[test]
743    fn test_model_card_builder() {
744        let card = ModelCard::new("ResNet-50")
745            .version("2.1.0")
746            .description("Image classification model")
747            .status(ModelStatus::Published)
748            .framework("PyTorch")
749            .task("classification")
750            .metric(ModelMetric::new("Top-1 Accuracy", 0.761))
751            .metric(ModelMetric::new("Top-5 Accuracy", 0.929))
752            .parameters(25_600_000)
753            .dataset("ImageNet")
754            .author("Deep Learning Team")
755            .tag("vision")
756            .tag("classification")
757            .metadata_entry("license", "Apache-2.0")
758            .width(400.0)
759            .height(300.0)
760            .background(Color::WHITE)
761            .border_color(Color::new(0.8, 0.8, 0.8, 1.0))
762            .corner_radius(12.0)
763            .show_metrics_chart(true)
764            .accessible_name("ResNet-50 model card")
765            .test_id("resnet-card");
766
767        assert_eq!(card.get_name(), "ResNet-50");
768        assert_eq!(card.get_version(), "2.1.0");
769        assert_eq!(card.get_description(), Some("Image classification model"));
770        assert_eq!(card.get_status(), ModelStatus::Published);
771        assert_eq!(card.get_framework(), Some("PyTorch"));
772        assert_eq!(card.get_task(), Some("classification"));
773        assert_eq!(card.get_metrics().len(), 2);
774        assert_eq!(card.get_parameters(), Some(25_600_000));
775        assert_eq!(card.get_dataset(), Some("ImageNet"));
776        assert_eq!(card.get_author(), Some("Deep Learning Team"));
777        assert_eq!(card.get_tags().len(), 2);
778        assert_eq!(card.get_metadata("license"), Some("Apache-2.0"));
779        assert_eq!(Widget::accessible_name(&card), Some("ResNet-50 model card"));
780        assert_eq!(Widget::test_id(&card), Some("resnet-card"));
781    }
782
783    #[test]
784    fn test_model_card_metrics() {
785        let metrics = vec![
786            ModelMetric::new("Accuracy", 0.95),
787            ModelMetric::new("F1", 0.92),
788        ];
789        let card = ModelCard::new("Model").metrics(metrics);
790        assert_eq!(card.get_metrics().len(), 2);
791        assert!(card.has_metrics());
792    }
793
794    #[test]
795    fn test_model_card_tags() {
796        let card = ModelCard::new("Model").tags(["nlp", "transformer", "bert"]);
797        assert_eq!(card.get_tags().len(), 3);
798    }
799
800    // ===== Formatted Parameters Tests =====
801
802    #[test]
803    fn test_formatted_parameters_none() {
804        let card = ModelCard::new("Model");
805        assert!(card.formatted_parameters().is_none());
806    }
807
808    #[test]
809    fn test_formatted_parameters_small() {
810        let card = ModelCard::new("Model").parameters(500);
811        assert_eq!(card.formatted_parameters(), Some("500".to_string()));
812    }
813
814    #[test]
815    fn test_formatted_parameters_thousands() {
816        let card = ModelCard::new("Model").parameters(25_000);
817        assert_eq!(card.formatted_parameters(), Some("25.0K".to_string()));
818    }
819
820    #[test]
821    fn test_formatted_parameters_millions() {
822        let card = ModelCard::new("Model").parameters(125_000_000);
823        assert_eq!(card.formatted_parameters(), Some("125.0M".to_string()));
824    }
825
826    #[test]
827    fn test_formatted_parameters_billions() {
828        let card = ModelCard::new("Model").parameters(175_000_000_000);
829        assert_eq!(card.formatted_parameters(), Some("175.0B".to_string()));
830    }
831
832    // ===== Dimension Tests =====
833
834    #[test]
835    fn test_model_card_width_min() {
836        let card = ModelCard::new("Model").width(100.0);
837        assert_eq!(card.width, Some(200.0));
838    }
839
840    #[test]
841    fn test_model_card_height_min() {
842        let card = ModelCard::new("Model").height(50.0);
843        assert_eq!(card.height, Some(150.0));
844    }
845
846    #[test]
847    fn test_model_card_corner_radius_min() {
848        let card = ModelCard::new("Model").corner_radius(-5.0);
849        assert_eq!(card.corner_radius, 0.0);
850    }
851
852    // ===== Widget Trait Tests =====
853
854    #[test]
855    fn test_model_card_type_id() {
856        let card = ModelCard::new("Model");
857        assert_eq!(Widget::type_id(&card), TypeId::of::<ModelCard>());
858    }
859
860    #[test]
861    fn test_model_card_measure_default() {
862        let card = ModelCard::new("Model");
863        let size = card.measure(Constraints::loose(Size::new(1000.0, 1000.0)));
864        assert_eq!(size.width, 320.0);
865        assert_eq!(size.height, 200.0);
866    }
867
868    #[test]
869    fn test_model_card_measure_custom() {
870        let card = ModelCard::new("Model").width(400.0).height(250.0);
871        let size = card.measure(Constraints::loose(Size::new(1000.0, 1000.0)));
872        assert_eq!(size.width, 400.0);
873        assert_eq!(size.height, 250.0);
874    }
875
876    #[test]
877    fn test_model_card_layout() {
878        let mut card = ModelCard::new("Model");
879        let bounds = Rect::new(10.0, 20.0, 320.0, 200.0);
880        let result = card.layout(bounds);
881        assert_eq!(result.size, Size::new(320.0, 200.0));
882        assert_eq!(card.bounds, bounds);
883    }
884
885    #[test]
886    fn test_model_card_children() {
887        let card = ModelCard::new("Model");
888        assert!(card.children().is_empty());
889    }
890
891    #[test]
892    fn test_model_card_is_interactive() {
893        let card = ModelCard::new("Model");
894        assert!(!card.is_interactive());
895    }
896
897    #[test]
898    fn test_model_card_is_focusable() {
899        let card = ModelCard::new("Model");
900        assert!(!card.is_focusable());
901    }
902
903    #[test]
904    fn test_model_card_accessible_role() {
905        let card = ModelCard::new("Model");
906        assert_eq!(card.accessible_role(), AccessibleRole::Generic);
907    }
908
909    #[test]
910    fn test_model_card_accessible_name_from_name() {
911        let card = ModelCard::new("GPT-4");
912        assert_eq!(Widget::accessible_name(&card), Some("GPT-4"));
913    }
914
915    #[test]
916    fn test_model_card_accessible_name_explicit() {
917        let card = ModelCard::new("GPT-4").accessible_name("Language model card");
918        assert_eq!(Widget::accessible_name(&card), Some("Language model card"));
919    }
920
921    #[test]
922    fn test_model_card_test_id() {
923        let card = ModelCard::new("Model").test_id("model-card");
924        assert_eq!(Widget::test_id(&card), Some("model-card"));
925    }
926
927    // ===== Has Metrics Tests =====
928
929    #[test]
930    fn test_model_card_has_metrics_false() {
931        let card = ModelCard::new("Model");
932        assert!(!card.has_metrics());
933    }
934
935    #[test]
936    fn test_model_card_has_metrics_true() {
937        let card = ModelCard::new("Model").metric(ModelMetric::new("Acc", 0.9));
938        assert!(card.has_metrics());
939    }
940
941    // =========================================================================
942    // Additional Coverage Tests
943    // =========================================================================
944
945    #[test]
946    fn test_model_status_color_all_variants() {
947        let _ = ModelStatus::Draft.color();
948        let _ = ModelStatus::Review.color();
949        let _ = ModelStatus::Published.color();
950        let _ = ModelStatus::Deprecated.color();
951        let _ = ModelStatus::Archived.color();
952    }
953
954    #[test]
955    fn test_model_card_children_mut() {
956        let mut card = ModelCard::new("Model");
957        assert!(card.children_mut().is_empty());
958    }
959
960    #[test]
961    fn test_model_card_event_returns_none() {
962        let mut card = ModelCard::new("Model");
963        let result = card.event(&presentar_core::Event::KeyDown {
964            key: presentar_core::Key::Down,
965        });
966        assert!(result.is_none());
967    }
968
969    #[test]
970    fn test_model_card_test_id_none() {
971        let card = ModelCard::new("Model");
972        assert!(Widget::test_id(&card).is_none());
973    }
974
975    #[test]
976    fn test_model_card_bounds() {
977        let mut card = ModelCard::new("Model");
978        card.layout(Rect::new(5.0, 10.0, 320.0, 200.0));
979        assert_eq!(card.bounds.x, 5.0);
980        assert_eq!(card.bounds.y, 10.0);
981    }
982
983    #[test]
984    fn test_model_metric_eq() {
985        let m1 = ModelMetric::new("Acc", 0.95);
986        let m2 = ModelMetric::new("Acc", 0.95);
987        assert_eq!(m1.name, m2.name);
988        assert_eq!(m1.value, m2.value);
989    }
990
991    #[test]
992    fn test_model_card_name_setter() {
993        let card = ModelCard::new("Initial").name("Changed");
994        assert_eq!(card.get_name(), "Changed");
995    }
996}