Skip to main content

teksilo_preview/
catalog.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! `WidgetCatalog` trait — the user-facing trait that widget authors
5//! implement to register a widget for previewing.
6//!
7//! Two traits coexist by design:
8//!
9//! - [`WidgetCatalog`] — static-method trait. Widget authors implement
10//!   this on their widget type. The methods describe the widget's id,
11//!   group, display name, variants, knobs, and the build closure that
12//!   constructs an instance from a `KnobValues`.
13//!
14//! - [`CatalogEntry`] — object-safe erased trait. The `inventory` plugin
15//!   registry collects `&'static dyn CatalogEntry`. Each entry forwards
16//!   to the corresponding `WidgetCatalog` static methods. Authors do not
17//!   implement this directly — the `register_widget_catalog!` macro
18//!   generates a small zero-sized type that implements it and submits it
19//!   to the inventory.
20
21use crate::knob::{KnobSpec, KnobValues};
22use crate::source_loc::SourceLoc;
23use crate::variant::PreviewVariant;
24use teksilo_core::widget::Widget;
25use teksilo_core::widget_id::WidgetId;
26
27/// How a catalog widget accepts children. Drives the designer's outline
28/// rendering and the runtime [`WidgetCatalog::build_with_children`]
29/// factory. Default is [`WidgetCategory::Leaf`].
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum WidgetCategory {
32    /// No children — a leaf control (Button, TextWidget, Slider, …).
33    Leaf,
34    /// Ordered bare children (VStack, HStack, ZStack, Grid, Padding, …).
35    ContainerA,
36    /// Named slots (Card → header/content/footer; Dialog; TabWidget; …).
37    ContainerB,
38}
39
40/// One child handed to [`WidgetCatalog::build_with_children`]: a
41/// pre-registered widget id plus, for [`WidgetCategory::ContainerB`]
42/// parents, the name of the slot it fills (`None` for the ordered
43/// children of a [`WidgetCategory::ContainerA`] parent).
44#[derive(Debug, Clone)]
45pub struct SlottedChild {
46    /// The named slot this child fills (`ContainerB`), or `None` for an
47    /// ordered bare child (`ContainerA`).
48    pub slot: Option<String>,
49    /// The pre-registered child widget, already inserted in the arena.
50    pub id: WidgetId,
51}
52
53/// Static-method trait implemented by widget authors.
54pub trait WidgetCatalog: 'static {
55    /// Stable, ASCII id (e.g. `"button"`, `"tag_chip"`). Used in
56    /// CLI args, navigator URLs, and persistence keys.
57    fn id() -> &'static str;
58
59    /// Group label for navigator organisation
60    /// (`"Controls"`, `"Containers"`, `"Composites"`, …).
61    fn group() -> &'static str;
62
63    /// Human-readable label shown in the navigator.
64    fn display_name() -> &'static str;
65
66    /// Named variants. At least one is required — typically a "default".
67    fn variants() -> Vec<PreviewVariant>;
68
69    /// Optional knob declarations. Empty by default — composite
70    /// widgets that build via `Scenario` variants leave this empty.
71    fn knobs() -> KnobSpec {
72        KnobSpec::empty()
73    }
74
75    /// Construct a fresh widget instance for the named variant, given
76    /// runtime knob values. The implementation typically dispatches on
77    /// the variant name to handle `Scenario` paths and otherwise builds
78    /// via the knob values for `Knobs` variants.
79    fn build(variant: &str, knobs: &KnobValues) -> Box<dyn Widget>;
80
81    /// An icon widget for the navigator palette and the designer's
82    /// outline tree. `None` (the default) leaves the consumer to
83    /// substitute a generic fallback. Returns a `Box<dyn Widget>` (not
84    /// an `IconWidget`) so this crate stays free of any widgets-crate
85    /// dependency.
86    fn icon() -> Option<Box<dyn Widget>> {
87        None
88    }
89
90    /// How this widget accepts children. The default,
91    /// [`WidgetCategory::Leaf`], is correct for every control; container
92    /// widgets override it.
93    fn category() -> WidgetCategory {
94        WidgetCategory::Leaf
95    }
96
97    /// Named slots for a [`WidgetCategory::ContainerB`] widget (e.g.
98    /// `Card` → `["header", "content", "footer"]`). Empty for `Leaf` and
99    /// `ContainerA`.
100    fn slots() -> &'static [&'static str] {
101        &[]
102    }
103
104    /// Build with pre-registered children injected. `ContainerA` folds
105    /// `children` as ordered bare children; `ContainerB` routes each by
106    /// its `slot` name; `Leaf` ignores them. The default ignores
107    /// `children` and delegates to [`build`](Self::build), so non-container
108    /// widgets need no override.
109    fn build_with_children(
110        variant: &str,
111        knobs: &KnobValues,
112        children: Vec<SlottedChild>,
113    ) -> Box<dyn Widget> {
114        let _ = children;
115        Self::build(variant, knobs)
116    }
117}
118
119/// Object-safe trait collected by `inventory`. Each implementor is a
120/// zero-sized shim generated by `register_widget_catalog!` that
121/// forwards to the corresponding `WidgetCatalog` impl.
122pub trait CatalogEntry: Sync {
123    fn id(&self) -> &'static str;
124    fn group(&self) -> &'static str;
125    fn display_name(&self) -> &'static str;
126    fn source(&self) -> SourceLoc;
127    fn variants(&self) -> Vec<PreviewVariant>;
128    fn knobs(&self) -> KnobSpec;
129    fn build(&self, variant: &str, knobs: &KnobValues) -> Box<dyn Widget>;
130
131    fn icon(&self) -> Option<Box<dyn Widget>> {
132        None
133    }
134    fn category(&self) -> WidgetCategory {
135        WidgetCategory::Leaf
136    }
137    fn slots(&self) -> &'static [&'static str] {
138        &[]
139    }
140    fn build_with_children(
141        &self,
142        variant: &str,
143        knobs: &KnobValues,
144        children: Vec<SlottedChild>,
145    ) -> Box<dyn Widget> {
146        let _ = children;
147        self.build(variant, knobs)
148    }
149}
150
151inventory::collect!(&'static dyn CatalogEntry);
152
153/// Register a `WidgetCatalog` impl with the global inventory.
154///
155/// Expand at module scope, alongside (or near) the `impl WidgetCatalog
156/// for X` block:
157///
158/// ```ignore
159/// impl WidgetCatalog for Button { /* ... */ }
160/// teksilo_preview::register_widget_catalog!(Button);
161/// ```
162///
163/// The macro captures `file!()` and `line!()` at the call site, so
164/// `entry.source()` returns the path of the file the macro expanded in
165/// — used by the previewer's `--file=PATH` resolution.
166/// Register a `WidgetCatalog` impl. Captures the file and line of the
167/// macro call site as the entry's source location — used by
168/// `previewer --file=PATH` resolution.
169#[macro_export]
170macro_rules! register_widget_catalog {
171    ($t:ty) => {
172        $crate::__register_widget_catalog_with!($t, file!(), line!());
173    };
174}
175
176/// Register a `WidgetCatalog` impl with an explicit source file path.
177/// Useful when several catalog impls live in a single shared
178/// `preview_catalog.rs` module — each call declares the source file
179/// that the user would open in their editor to find the widget
180/// itself, so that `previewer --file=<that path>` resolves to the
181/// right entry. The line value is set to 1 since the call site does
182/// not correspond to the widget's own location.
183#[macro_export]
184macro_rules! register_widget_catalog_at {
185    ($file:literal, $t:ty) => {
186        $crate::__register_widget_catalog_with!($t, $file, 1u32);
187    };
188}
189
190#[doc(hidden)]
191#[macro_export]
192macro_rules! __register_widget_catalog_with {
193    ($t:ty, $file:expr, $line:expr) => {
194        const _: () = {
195            #[allow(non_camel_case_types)]
196            struct __Entry;
197            impl $crate::CatalogEntry for __Entry {
198                fn id(&self) -> &'static str {
199                    <$t as $crate::WidgetCatalog>::id()
200                }
201                fn group(&self) -> &'static str {
202                    <$t as $crate::WidgetCatalog>::group()
203                }
204                fn display_name(&self) -> &'static str {
205                    <$t as $crate::WidgetCatalog>::display_name()
206                }
207                fn source(&self) -> $crate::SourceLoc {
208                    $crate::SourceLoc::new($file, $line)
209                }
210                fn variants(&self) -> ::std::vec::Vec<$crate::PreviewVariant> {
211                    <$t as $crate::WidgetCatalog>::variants()
212                }
213                fn knobs(&self) -> $crate::KnobSpec {
214                    <$t as $crate::WidgetCatalog>::knobs()
215                }
216                fn build(
217                    &self,
218                    variant: &str,
219                    knobs: &$crate::KnobValues,
220                ) -> ::std::boxed::Box<dyn $crate::__widget::Widget> {
221                    <$t as $crate::WidgetCatalog>::build(variant, knobs)
222                }
223                fn icon(
224                    &self,
225                ) -> ::std::option::Option<::std::boxed::Box<dyn $crate::__widget::Widget>>
226                {
227                    <$t as $crate::WidgetCatalog>::icon()
228                }
229                fn category(&self) -> $crate::WidgetCategory {
230                    <$t as $crate::WidgetCatalog>::category()
231                }
232                fn slots(&self) -> &'static [&'static str] {
233                    <$t as $crate::WidgetCatalog>::slots()
234                }
235                fn build_with_children(
236                    &self,
237                    variant: &str,
238                    knobs: &$crate::KnobValues,
239                    children: ::std::vec::Vec<$crate::SlottedChild>,
240                ) -> ::std::boxed::Box<dyn $crate::__widget::Widget> {
241                    <$t as $crate::WidgetCatalog>::build_with_children(variant, knobs, children)
242                }
243            }
244            static __ENTRY: __Entry = __Entry;
245            $crate::__inventory::submit! {
246                &__ENTRY as &'static dyn $crate::CatalogEntry
247            }
248        };
249    };
250}