Skip to main content

rustmotion_components/
lib.rs

1pub mod box_builder;
2pub mod intrinsic;
3pub mod legacy_dispatch;
4
5pub mod arrow;
6pub mod audio_spectrum;
7pub mod avatar;
8pub mod avatar_group;
9pub mod badge;
10pub mod callout;
11pub mod caption;
12pub mod card;
13pub mod chart;
14pub mod codeblock;
15pub mod comparison;
16pub mod connector;
17pub mod container;
18pub mod countdown;
19pub mod counter;
20pub mod cursor;
21pub mod divider;
22pub mod dot_map;
23pub mod flex;
24pub mod gauge;
25pub mod gif;
26pub mod gradient_text;
27pub mod grid;
28pub mod heatmap;
29pub mod icon;
30pub mod image;
31pub mod kbd;
32pub mod line;
33pub mod list;
34pub mod lottie;
35pub mod marquee;
36pub mod mockup;
37pub mod notification;
38pub mod number_wheel;
39pub mod particle;
40pub mod pill_nav;
41pub mod pointer;
42pub mod positioned;
43pub mod progress;
44pub mod qrcode;
45pub mod rating;
46pub mod rich_text;
47pub mod shape;
48pub mod skeleton;
49pub mod slider;
50pub mod sparkline;
51pub mod stat;
52pub mod stepper;
53pub mod success_check;
54pub mod svg;
55pub mod switch;
56pub mod table;
57pub mod tag_cloud;
58pub mod terminal;
59pub mod text;
60pub mod timeline;
61pub mod tooltip;
62pub mod treemap;
63pub mod video;
64pub mod waveform;
65pub mod world_bitmap;
66
67use schemars::JsonSchema;
68use serde::{Deserialize, Serialize};
69
70use rustmotion_core::traits::{Animatable, Painter, Styled, Timed};
71
72pub use arrow::Arrow;
73pub use audio_spectrum::AudioSpectrum;
74pub use avatar::Avatar;
75pub use avatar_group::AvatarGroup;
76pub use badge::Badge;
77pub use callout::Callout;
78pub use caption::Caption;
79pub use card::Card;
80pub use chart::Chart;
81pub use codeblock::Codeblock;
82pub use comparison::Comparison;
83pub use connector::Connector;
84pub use container::ContainerComponent;
85pub use countdown::Countdown;
86pub use counter::Counter;
87pub use cursor::Cursor;
88pub use divider::Divider;
89pub use dot_map::DotMap;
90pub use flex::Flex;
91pub use gauge::Gauge;
92pub use gif::Gif;
93pub use gradient_text::GradientText;
94pub use grid::Grid;
95pub use heatmap::Heatmap;
96pub use icon::Icon;
97pub use image::Image;
98pub use kbd::Kbd;
99pub use line::Line;
100pub use list::List;
101pub use lottie::Lottie;
102pub use marquee::Marquee;
103pub use mockup::Mockup;
104pub use notification::Notification;
105pub use number_wheel::NumberWheel;
106pub use particle::Particle;
107pub use pill_nav::PillNav;
108pub use pointer::Pointer;
109pub use positioned::Positioned;
110pub use progress::Progress;
111pub use qrcode::QrCode;
112pub use rating::Rating;
113pub use rich_text::RichText;
114pub use shape::Shape;
115pub use skeleton::Skeleton;
116pub use slider::Slider;
117pub use sparkline::Sparkline;
118pub use stat::Stat;
119pub use stepper::Stepper;
120pub use success_check::SuccessCheck;
121pub use svg::Svg;
122pub use switch::Switch;
123pub use table::Table;
124pub use tag_cloud::TagCloud;
125pub use terminal::Terminal;
126pub use text::Text;
127pub use timeline::Timeline;
128pub use tooltip::Tooltip;
129pub use treemap::Treemap;
130pub use video::Video;
131pub use waveform::Waveform;
132
133// --- Position mode ---
134
135/// Constat #8: `PositionMode::Named(String)` accepts any string, but
136/// [`ChildComponent::absolute_position`] only ever treats the literal
137/// `"absolute"` specially — every other value (including the CSS-legitimate
138/// `"relative"`/`"static"`, which an LLM reasoning in CSS terms naturally
139/// reaches for) silently drops `x`/`y`: the component is taken out of flow
140/// (`is_flow()` is false for any `Some(position)`) but never receives an
141/// absolute offset either, since only `"absolute"` is matched. `x`/`y` are
142/// top-level sibling fields on `ChildComponent`, not on `PositionMode`
143/// itself, so this can't detect *whether* they were actually set — only
144/// that, if they were, they are about to be silently ignored. `"absolute"`
145/// stays completely silent (the common, correct case); anything else warns.
146pub fn is_recognized_position_name(s: &str) -> bool {
147    s == "absolute"
148}
149
150#[derive(Debug, Clone, Serialize, JsonSchema)]
151#[serde(untagged)]
152pub enum PositionMode {
153    Absolute { x: f32, y: f32 },
154    Named(String),
155}
156
157impl<'de> Deserialize<'de> for PositionMode {
158    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
159    where
160        D: serde::Deserializer<'de>,
161    {
162        #[derive(Deserialize)]
163        #[serde(untagged)]
164        enum Raw {
165            Absolute { x: f32, y: f32 },
166            Named(String),
167        }
168        Ok(match Raw::deserialize(deserializer)? {
169            Raw::Absolute { x, y } => PositionMode::Absolute { x, y },
170            Raw::Named(s) => {
171                if !is_recognized_position_name(&s) && warn_once_for(&s) {
172                    eprintln!(
173                        "Warning: position: \"{s}\" is not \"absolute\" — this component-level \
174                         `position` shorthand only honours the literal \"absolute\" (paired with \
175                         `x`/`y`); any other value, including CSS-legitimate ones like \
176                         \"relative\"/\"static\", is accepted but silently drops `x`/`y` instead \
177                         of positioning the element (it still removes the component from flex \
178                         flow). Use `style.position` for real CSS relative/static semantics."
179                    );
180                }
181                PositionMode::Named(s)
182            }
183        })
184    }
185}
186
187/// True the first time this exact `position` value is seen, false afterwards.
188///
189/// `render_scene_frame` calls `prepare_scene` — and therefore re-runs this
190/// `Deserialize` over the whole scene tree — once **per frame**. An unguarded
191/// warning here would print the same line once per offending component per
192/// frame: over a thousand times on a 1200-frame render, drowning out anything
193/// else on stderr. Keyed by the value rather than a plain `Once` so a scenario
194/// with several distinct bad values still hears about each of them.
195pub(crate) fn warn_once_for(value: &str) -> bool {
196    use std::collections::HashSet;
197    use std::sync::{Mutex, OnceLock};
198    static SEEN: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
199    SEEN.get_or_init(Default::default)
200        .lock()
201        .map(|mut seen| seen.insert(value.to_owned()))
202        .unwrap_or(false)
203}
204
205impl Default for PositionMode {
206    fn default() -> Self {
207        Self::Absolute { x: 0.0, y: 0.0 }
208    }
209}
210
211#[cfg(test)]
212mod position_mode_tests {
213    use super::*;
214
215    // ---- constat #8 (RED first) ----
216
217    #[test]
218    fn absolute_is_recognized() {
219        assert!(is_recognized_position_name("absolute"));
220    }
221
222    #[test]
223    fn relative_and_static_and_typos_are_not_recognized() {
224        for s in ["relative", "static", "fixed", "sticky", "Absolute", "abs"] {
225            assert!(
226                !is_recognized_position_name(s),
227                "'{s}' must not be treated as the recognised \"absolute\" value"
228            );
229        }
230    }
231
232    #[test]
233    fn absolute_object_form_still_carries_x_y() {
234        let json =
235            r#"{ "position": { "x": 10.0, "y": 20.0 }, "type": "shape", "shape": "circle" }"#;
236        let child: ChildComponent = serde_json::from_str(json).unwrap();
237        assert_eq!(child.absolute_position(), Some((10.0, 20.0)));
238    }
239
240    #[test]
241    fn absolute_string_form_with_sibling_x_y_still_carries_them() {
242        let json =
243            r#"{ "position": "absolute", "x": 5.0, "y": 7.0, "type": "shape", "shape": "circle" }"#;
244        let child: ChildComponent = serde_json::from_str(json).unwrap();
245        assert_eq!(child.absolute_position(), Some((5.0, 7.0)));
246    }
247
248    #[test]
249    fn relative_still_parses_but_drops_x_y_and_the_helper_flags_it() {
250        // The legitimate-CSS trap named in constat #8: an LLM writes
251        // `"position": "relative"` (valid CSS) with `x`/`y` alongside it,
252        // expecting a positioned element. The parse must not fail — this is
253        // legitimate JSON per the schema's own untagged catch-all — but the
254        // coordinates are provably dropped (`absolute_position()` is
255        // `None`), and `is_recognized_position_name` is the named,
256        // independently testable signal the warning path uses to detect
257        // this instead of staying silent.
258        let json =
259            r#"{ "position": "relative", "x": 5.0, "y": 7.0, "type": "shape", "shape": "circle" }"#;
260        let child: ChildComponent = serde_json::from_str(json).unwrap();
261        assert!(
262            !is_recognized_position_name("relative"),
263            "this is exactly the case the warning fires for"
264        );
265        assert_eq!(
266            child.absolute_position(),
267            None,
268            "x/y are indeed dropped for a non-\"absolute\" position — this is the silent \
269             behaviour being made loud, not a new regression"
270        );
271        // The component is still taken out of flow, same as before.
272        assert!(!child.is_flow());
273    }
274
275    /// `prepare_scene` re-runs this `Deserialize` over the whole scene tree
276    /// once per frame, so the warning must be deduplicated or a 1200-frame
277    /// render prints it 1200 times. Distinct values still each get a line.
278    #[test]
279    fn the_warning_fires_once_per_distinct_value_not_once_per_frame() {
280        let value = "position-value-used-only-by-this-test";
281        assert!(warn_once_for(value), "first sighting must warn");
282        for _ in 0..1000 {
283            assert!(
284                !warn_once_for(value),
285                "re-parsing the same value must stay silent"
286            );
287        }
288        assert!(
289            warn_once_for("a-different-position-value-for-this-test"),
290            "a different bad value must still get its own warning"
291        );
292    }
293
294    #[test]
295    fn no_position_set_is_a_normal_flow_child() {
296        let json = r#"{ "type": "shape", "shape": "circle" }"#;
297        let child: ChildComponent = serde_json::from_str(json).unwrap();
298        assert!(child.is_flow());
299        assert_eq!(child.absolute_position(), None);
300    }
301}
302
303// --- Child wrapper ---
304
305#[derive(Debug, Serialize, Deserialize, JsonSchema)]
306pub struct ChildComponent {
307    #[serde(flatten)]
308    pub component: Component,
309    #[serde(default)]
310    pub position: Option<PositionMode>,
311    #[serde(default)]
312    pub x: Option<f32>,
313    #[serde(default)]
314    pub y: Option<f32>,
315    #[serde(default, rename = "z-index")]
316    pub z_index: Option<i32>,
317    /// Declares that this component's job is to extend past the frame edge
318    /// (e.g. a radial glow used as a base layer). Top-level field, not a
319    /// `style` property — `CssStyle` is `deny_unknown_fields` and belongs to
320    /// no one this wave. Defaults to `false`: no existing scenario changes
321    /// behaviour. Exempts only `viewport_overflow` and `animated_text_overflow`
322    /// (see `crates/rustmotion/src/cli/commands/geometry.rs`); it does NOT
323    /// exempt `content_overflows_box` — content larger than its own box stays
324    /// a reported defect regardless of `bleed`. Applies to this component
325    /// only: a bled container does not suppress checks on its children, since
326    /// each child is its own `ChildComponent` with its own `bleed` flag.
327    #[serde(default)]
328    pub bleed: bool,
329}
330
331impl ChildComponent {
332    pub fn is_flow(&self) -> bool {
333        self.position.is_none()
334    }
335
336    pub fn is_decorative(&self) -> bool {
337        matches!(self.component, Component::Particle(_))
338    }
339
340    pub fn absolute_position(&self) -> Option<(f32, f32)> {
341        match &self.position {
342            Some(PositionMode::Absolute { x, y }) => Some((*x, *y)),
343            Some(PositionMode::Named(s)) if s == "absolute" => {
344                Some((self.x.unwrap_or(0.0), self.y.unwrap_or(0.0)))
345            }
346            _ => None,
347        }
348    }
349}
350
351// --- Component enum ---
352
353#[derive(Debug, Serialize, Deserialize, JsonSchema)]
354#[serde(tag = "type", rename_all = "snake_case")]
355pub enum Component {
356    AudioSpectrum(AudioSpectrum),
357    Text(Text),
358    Shape(Shape),
359    Image(Image),
360    Icon(Icon),
361    Svg(Svg),
362    Video(Video),
363    Gif(Gif),
364    Counter(Counter),
365    Cursor(Cursor),
366    Caption(Caption),
367    Codeblock(Codeblock),
368    Connector(Connector),
369    Avatar(Avatar),
370    AvatarGroup(AvatarGroup),
371    Arrow(Arrow),
372    Badge(Badge),
373    Callout(Callout),
374    Chart(Chart),
375    Comparison(Comparison),
376    Countdown(Countdown),
377    Divider(Divider),
378    DotMap(DotMap),
379    Gauge(Gauge),
380    GradientText(GradientText),
381    Heatmap(Heatmap),
382    Kbd(Kbd),
383    Line(Line),
384    List(List),
385    Lottie(Lottie),
386    Marquee(Marquee),
387    Mockup(Mockup),
388    Notification(Notification),
389    Particle(Particle),
390    PillNav(PillNav),
391    #[serde(alias = "progress_bar")]
392    Progress(Progress),
393    QrCode(QrCode),
394    NumberWheel(NumberWheel),
395    SuccessCheck(SuccessCheck),
396    Pointer(Pointer),
397    Rating(Rating),
398    Skeleton(Skeleton),
399    Slider(Slider),
400    Sparkline(Sparkline),
401    Stat(Stat),
402    Stepper(Stepper),
403    Switch(Switch),
404    RichText(RichText),
405    Table(Table),
406    TagCloud(TagCloud),
407    Terminal(Terminal),
408    Timeline(Timeline),
409    Tooltip(Tooltip),
410    Treemap(Treemap),
411    Positioned(Positioned),
412    Flex(Flex),
413    Grid(Grid),
414    Card(Card),
415    #[serde(rename = "div", alias = "container")]
416    Container(ContainerComponent),
417    Waveform(Waveform),
418}
419
420// --- Dispatch helpers ---
421
422impl Component {
423    pub fn as_animatable(&self) -> Option<&dyn Animatable> {
424        match self {
425            Component::AudioSpectrum(c) => Some(c),
426            Component::Waveform(c) => Some(c),
427            Component::Text(c) => Some(c),
428            Component::Shape(c) => Some(c),
429            Component::Image(c) => Some(c),
430            Component::Icon(c) => Some(c),
431            Component::Svg(c) => Some(c),
432            Component::Video(c) => Some(c),
433            Component::Gif(c) => Some(c),
434            Component::Counter(c) => Some(c),
435            Component::Cursor(c) => Some(c),
436            Component::Caption(c) => Some(c),
437            Component::Codeblock(c) => Some(c),
438            Component::Avatar(c) => Some(c),
439            Component::AvatarGroup(c) => Some(c),
440            Component::Arrow(c) => Some(c),
441            Component::Connector(c) => Some(c),
442            Component::Badge(c) => Some(c),
443            Component::Callout(c) => Some(c),
444            Component::Chart(c) => Some(c),
445            Component::Comparison(c) => Some(c),
446            Component::Countdown(c) => Some(c),
447            Component::Divider(c) => Some(c),
448            Component::DotMap(c) => Some(c),
449            Component::Gauge(c) => Some(c),
450            Component::GradientText(c) => Some(c),
451            Component::Heatmap(c) => Some(c),
452            Component::Kbd(c) => Some(c),
453            Component::Line(c) => Some(c),
454            Component::List(c) => Some(c),
455            Component::Lottie(c) => Some(c),
456            Component::Marquee(c) => Some(c),
457            Component::Mockup(c) => Some(c),
458            Component::Notification(c) => Some(c),
459            Component::Particle(c) => Some(c),
460            Component::PillNav(c) => Some(c),
461            Component::Progress(c) => Some(c),
462            Component::QrCode(c) => Some(c),
463            Component::NumberWheel(c) => Some(c),
464            Component::SuccessCheck(c) => Some(c),
465            Component::Pointer(c) => Some(c),
466            Component::Rating(c) => Some(c),
467            Component::Skeleton(c) => Some(c),
468            Component::Slider(c) => Some(c),
469            Component::Sparkline(c) => Some(c),
470            Component::Stat(c) => Some(c),
471            Component::Stepper(c) => Some(c),
472            Component::Switch(c) => Some(c),
473            Component::RichText(c) => Some(c),
474            Component::Table(c) => Some(c),
475            Component::TagCloud(c) => Some(c),
476            Component::Terminal(c) => Some(c),
477            Component::Timeline(c) => Some(c),
478            Component::Tooltip(c) => Some(c),
479            Component::Treemap(c) => Some(c),
480            Component::Flex(c) => Some(c),
481            Component::Grid(c) => Some(c),
482            Component::Card(c) => Some(c),
483            Component::Container(c) => Some(c),
484            Component::Positioned(c) => Some(c),
485        }
486    }
487
488    pub fn as_timed(&self) -> Option<&dyn Timed> {
489        match self {
490            Component::AudioSpectrum(c) => Some(c),
491            Component::Waveform(c) => Some(c),
492            Component::Text(c) => Some(c),
493            Component::Shape(c) => Some(c),
494            Component::Image(c) => Some(c),
495            Component::Icon(c) => Some(c),
496            Component::Svg(c) => Some(c),
497            Component::Video(c) => Some(c),
498            Component::Gif(c) => Some(c),
499            Component::Counter(c) => Some(c),
500            Component::Cursor(c) => Some(c),
501            Component::Codeblock(c) => Some(c),
502            Component::Avatar(c) => Some(c),
503            Component::AvatarGroup(c) => Some(c),
504            Component::Arrow(c) => Some(c),
505            Component::Connector(c) => Some(c),
506            Component::Badge(c) => Some(c),
507            Component::Callout(c) => Some(c),
508            Component::Chart(c) => Some(c),
509            Component::Comparison(c) => Some(c),
510            Component::Countdown(c) => Some(c),
511            Component::Divider(c) => Some(c),
512            Component::DotMap(c) => Some(c),
513            Component::Gauge(c) => Some(c),
514            Component::GradientText(c) => Some(c),
515            Component::Heatmap(c) => Some(c),
516            Component::Kbd(c) => Some(c),
517            Component::Line(c) => Some(c),
518            Component::List(c) => Some(c),
519            Component::Lottie(c) => Some(c),
520            Component::Marquee(c) => Some(c),
521            Component::Mockup(c) => Some(c),
522            Component::Notification(c) => Some(c),
523            Component::Particle(c) => Some(c),
524            Component::PillNav(c) => Some(c),
525            Component::Progress(c) => Some(c),
526            Component::QrCode(c) => Some(c),
527            Component::NumberWheel(c) => Some(c),
528            Component::SuccessCheck(c) => Some(c),
529            Component::Pointer(c) => Some(c),
530            Component::Rating(c) => Some(c),
531            Component::Skeleton(c) => Some(c),
532            Component::Slider(c) => Some(c),
533            Component::Sparkline(c) => Some(c),
534            Component::Stat(c) => Some(c),
535            Component::Stepper(c) => Some(c),
536            Component::Switch(c) => Some(c),
537            Component::RichText(c) => Some(c),
538            Component::Table(c) => Some(c),
539            Component::TagCloud(c) => Some(c),
540            Component::Terminal(c) => Some(c),
541            Component::Timeline(c) => Some(c),
542            Component::Tooltip(c) => Some(c),
543            Component::Treemap(c) => Some(c),
544            Component::Flex(c) => Some(c),
545            Component::Grid(c) => Some(c),
546            Component::Card(c) => Some(c),
547            Component::Container(c) => Some(c),
548            Component::Caption(c) => Some(c),
549            Component::Positioned(c) => Some(c),
550        }
551    }
552
553    pub fn as_styled(&self) -> &dyn Styled {
554        match self {
555            Component::AudioSpectrum(c) => c,
556            Component::Waveform(c) => c,
557            Component::Text(c) => c,
558            Component::Shape(c) => c,
559            Component::Image(c) => c,
560            Component::Icon(c) => c,
561            Component::Svg(c) => c,
562            Component::Video(c) => c,
563            Component::Gif(c) => c,
564            Component::Counter(c) => c,
565            Component::Cursor(c) => c,
566            Component::Caption(c) => c,
567            Component::Codeblock(c) => c,
568            Component::Avatar(c) => c,
569            Component::AvatarGroup(c) => c,
570            Component::Arrow(c) => c,
571            Component::Connector(c) => c,
572            Component::Badge(c) => c,
573            Component::Callout(c) => c,
574            Component::Chart(c) => c,
575            Component::Comparison(c) => c,
576            Component::Countdown(c) => c,
577            Component::Divider(c) => c,
578            Component::DotMap(c) => c,
579            Component::Gauge(c) => c,
580            Component::GradientText(c) => c,
581            Component::Heatmap(c) => c,
582            Component::Kbd(c) => c,
583            Component::Line(c) => c,
584            Component::List(c) => c,
585            Component::Lottie(c) => c,
586            Component::Marquee(c) => c,
587            Component::Mockup(c) => c,
588            Component::Notification(c) => c,
589            Component::Particle(c) => c,
590            Component::PillNav(c) => c,
591            Component::Progress(c) => c,
592            Component::QrCode(c) => c,
593            Component::NumberWheel(c) => c,
594            Component::SuccessCheck(c) => c,
595            Component::Pointer(c) => c,
596            Component::Rating(c) => c,
597            Component::Skeleton(c) => c,
598            Component::Slider(c) => c,
599            Component::Sparkline(c) => c,
600            Component::Stat(c) => c,
601            Component::Stepper(c) => c,
602            Component::Switch(c) => c,
603            Component::RichText(c) => c,
604            Component::Table(c) => c,
605            Component::TagCloud(c) => c,
606            Component::Terminal(c) => c,
607            Component::Timeline(c) => c,
608            Component::Tooltip(c) => c,
609            Component::Treemap(c) => c,
610            Component::Positioned(c) => c,
611            Component::Flex(c) => c,
612            Component::Grid(c) => c,
613            Component::Card(c) => c,
614            Component::Container(c) => c,
615        }
616    }
617
618    /// Returns the Painter trait. All 51 components are migrated to the new
619    /// pipeline; the dispatcher always uses Painter::paint_content.
620    pub fn as_painter(&self) -> Option<&dyn Painter> {
621        match self {
622            Component::AudioSpectrum(c) => Some(c),
623            Component::Waveform(c) => Some(c),
624            Component::Card(c) => Some(c),
625            Component::Container(c) => Some(c),
626            Component::Flex(c) => Some(c),
627            Component::Grid(c) => Some(c),
628            Component::Positioned(c) => Some(c),
629            Component::Divider(c) => Some(c),
630            Component::Shape(c) => Some(c),
631            Component::Image(c) => Some(c),
632            Component::Icon(c) => Some(c),
633            Component::Svg(c) => Some(c),
634            Component::QrCode(c) => Some(c),
635            Component::Gif(c) => Some(c),
636            Component::Video(c) => Some(c),
637            Component::Lottie(c) => Some(c),
638            Component::Cursor(c) => Some(c),
639            Component::Particle(c) => Some(c),
640            Component::Mockup(c) => Some(c),
641            Component::Text(c) => Some(c),
642            Component::Caption(c) => Some(c),
643            Component::Badge(c) => Some(c),
644            Component::Kbd(c) => Some(c),
645            Component::Callout(c) => Some(c),
646            Component::Marquee(c) => Some(c),
647            Component::TagCloud(c) => Some(c),
648            Component::GradientText(c) => Some(c),
649            Component::RichText(c) => Some(c),
650            Component::Switch(c) => Some(c),
651            Component::Slider(c) => Some(c),
652            Component::NumberWheel(c) => Some(c),
653            Component::SuccessCheck(c) => Some(c),
654            Component::Pointer(c) => Some(c),
655            Component::Rating(c) => Some(c),
656            Component::Stepper(c) => Some(c),
657            Component::Comparison(c) => Some(c),
658            Component::Notification(c) => Some(c),
659            Component::Tooltip(c) => Some(c),
660            Component::PillNav(c) => Some(c),
661            Component::List(c) => Some(c),
662            Component::Skeleton(c) => Some(c),
663            Component::Avatar(c) => Some(c),
664            Component::AvatarGroup(c) => Some(c),
665            Component::Timeline(c) => Some(c),
666            Component::Progress(c) => Some(c),
667            Component::Counter(c) => Some(c),
668            Component::Countdown(c) => Some(c),
669            Component::Gauge(c) => Some(c),
670            Component::Sparkline(c) => Some(c),
671            Component::Stat(c) => Some(c),
672            Component::Heatmap(c) => Some(c),
673            Component::Treemap(c) => Some(c),
674            Component::DotMap(c) => Some(c),
675            Component::Table(c) => Some(c),
676            Component::Codeblock(c) => Some(c),
677            Component::Terminal(c) => Some(c),
678            Component::Chart(c) => Some(c),
679            Component::Line(c) => Some(c),
680            Component::Arrow(c) => Some(c),
681            Component::Connector(c) => Some(c),
682        }
683    }
684}