Skip to main content

teksilo_preview/
knob.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Knob specs and knob value containers.
5//!
6//! `KnobSpec` is a static, declarative description of which properties
7//! a widget exposes for live tweaking — authored in `WidgetCatalog::knobs()`.
8//! `KnobValues` is the runtime container holding one typed `Signal<T>` per
9//! declared knob; the previewer constructs it from a spec and threads
10//! signals into the displayed widget via `Prop::Bound`.
11//!
12//! Each knob `id` is a stable, ASCII string used both for spec lookup
13//! and for variant override application. Accessors panic on a typo or
14//! kind mismatch — this is developer-facing tooling, panic on misuse
15//! is the right policy.
16
17use std::collections::HashMap;
18
19use teksilo_core::signal::Signal;
20use teksilo_tokens::{BorderRole, SurfaceRole, TextRole, TextStyleRole};
21
22// ---------------------------------------------------------------------------
23// Spec — declarative description authored by the widget
24// ---------------------------------------------------------------------------
25
26/// A typed knob declaration. Each kind maps 1:1 to a `Signal<T>` in
27/// `KnobValues` and to one row in the inspector's auto-generated form.
28#[derive(Debug, Clone)]
29pub enum KnobKind {
30    Bool {
31        default: bool,
32    },
33    OptBool {
34        default: Option<bool>,
35    },
36    I32 {
37        default: i32,
38        min: i32,
39        max: i32,
40        step: i32,
41    },
42    OptI32 {
43        default: Option<i32>,
44        min: i32,
45        max: i32,
46        step: i32,
47    },
48    F32 {
49        default: f32,
50        min: f32,
51        max: f32,
52        step: f32,
53    },
54    OptF32 {
55        default: Option<f32>,
56        min: f32,
57        max: f32,
58        step: f32,
59    },
60    Text {
61        default: String,
62    },
63    OptText {
64        default: Option<String>,
65    },
66    /// Position-based selection over a fixed list of labels.
67    Choice {
68        options: Vec<&'static str>,
69        default: usize,
70    },
71    /// A Rust enum property: like `Choice`, but carries the enum's path and
72    /// variant idents so a design tool can render a dropdown and emit
73    /// `enum_path::variant`. Stored at runtime as a `usize` index (like Choice).
74    Enum {
75        enum_path: &'static str,
76        variants: Vec<&'static str>,
77        default: usize,
78    },
79    TextRole {
80        default: TextRole,
81    },
82    SurfaceRole {
83        default: SurfaceRole,
84    },
85    BorderRole {
86        default: BorderRole,
87    },
88    TextStyle {
89        default: TextStyleRole,
90    },
91}
92
93/// Resolved dropdown metadata for an enum-typed knob — the Rust enum path, its
94/// variant idents (in declaration order), and the default's index. Lets a
95/// design tool render a dropdown and emit `enum_path::variant`.
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct EnumInfo {
98    pub enum_path: &'static str,
99    pub variants: Vec<&'static str>,
100    pub default: usize,
101}
102
103impl KnobKind {
104    /// For enum-typed knobs (`Enum` and the four role kinds), the data needed to
105    /// render a dropdown + emit `Path::Variant`; `None` for scalar / text /
106    /// label-only `Choice` kinds. The role variant lists come from
107    /// `teksilo_tokens`, so they never drift from the actual enums.
108    pub fn enum_info(&self) -> Option<EnumInfo> {
109        fn role(
110            path: &'static str,
111            names: &'static [&'static str],
112            default_dbg: String,
113        ) -> EnumInfo {
114            EnumInfo {
115                enum_path: path,
116                variants: names.to_vec(),
117                default: names.iter().position(|n| **n == default_dbg).unwrap_or(0),
118            }
119        }
120        match self {
121            KnobKind::Enum {
122                enum_path,
123                variants,
124                default,
125            } => Some(EnumInfo {
126                enum_path,
127                variants: variants.clone(),
128                default: *default,
129            }),
130            KnobKind::TextRole { default } => Some(role(
131                "TextRole",
132                TextRole::variant_names(),
133                format!("{default:?}"),
134            )),
135            KnobKind::SurfaceRole { default } => Some(role(
136                "SurfaceRole",
137                SurfaceRole::variant_names(),
138                format!("{default:?}"),
139            )),
140            KnobKind::BorderRole { default } => Some(role(
141                "BorderRole",
142                BorderRole::variant_names(),
143                format!("{default:?}"),
144            )),
145            KnobKind::TextStyle { default } => Some(role(
146                "TextStyleRole",
147                TextStyleRole::variant_names(),
148                format!("{default:?}"),
149            )),
150            _ => None,
151        }
152    }
153}
154
155#[derive(Debug, Clone)]
156pub struct KnobDecl {
157    pub id: &'static str,
158    pub label: &'static str,
159    pub group: Option<&'static str>,
160    /// `Some(i)` when this knob is constructor argument `i` (`Slider::new(v, …)`)
161    /// rather than a named builder property — a design tool emits it positionally.
162    pub ctor_position: Option<usize>,
163    pub kind: KnobKind,
164}
165
166/// Ordered list of knob declarations. Order is preserved by the
167/// inspector when rendering the form.
168#[derive(Debug, Clone, Default)]
169pub struct KnobSpec {
170    decls: Vec<KnobDecl>,
171}
172
173impl KnobSpec {
174    pub fn new() -> Self {
175        Self::default()
176    }
177
178    pub fn empty() -> Self {
179        Self::default()
180    }
181
182    pub fn declarations(&self) -> &[KnobDecl] {
183        &self.decls
184    }
185
186    /// Look up a knob declaration by id.
187    pub fn get(&self, id: &str) -> Option<&KnobDecl> {
188        self.decls.iter().find(|d| d.id == id)
189    }
190
191    fn push(mut self, decl: KnobDecl) -> Self {
192        debug_assert!(
193            !self.decls.iter().any(|d| d.id == decl.id),
194            "duplicate knob id: {}",
195            decl.id
196        );
197        self.decls.push(decl);
198        self
199    }
200
201    pub fn bool_(self, id: &'static str, label: &'static str, default: bool) -> Self {
202        self.push(KnobDecl {
203            id,
204            label,
205            group: None,
206            ctor_position: None,
207            kind: KnobKind::Bool { default },
208        })
209    }
210
211    pub fn opt_bool(self, id: &'static str, label: &'static str, default: Option<bool>) -> Self {
212        self.push(KnobDecl {
213            id,
214            label,
215            group: None,
216            ctor_position: None,
217            kind: KnobKind::OptBool { default },
218        })
219    }
220
221    pub fn i32_(
222        self,
223        id: &'static str,
224        label: &'static str,
225        default: i32,
226        min: i32,
227        max: i32,
228    ) -> Self {
229        self.push(KnobDecl {
230            id,
231            label,
232            group: None,
233            ctor_position: None,
234            kind: KnobKind::I32 {
235                default,
236                min,
237                max,
238                step: 1,
239            },
240        })
241    }
242
243    pub fn f32_(
244        self,
245        id: &'static str,
246        label: &'static str,
247        default: f32,
248        min: f32,
249        max: f32,
250    ) -> Self {
251        self.push(KnobDecl {
252            id,
253            label,
254            group: None,
255            ctor_position: None,
256            kind: KnobKind::F32 {
257                default,
258                min,
259                max,
260                step: ((max - min) / 100.0).max(0.01),
261            },
262        })
263    }
264
265    pub fn f32_step(
266        self,
267        id: &'static str,
268        label: &'static str,
269        default: f32,
270        min: f32,
271        max: f32,
272        step: f32,
273    ) -> Self {
274        self.push(KnobDecl {
275            id,
276            label,
277            group: None,
278            ctor_position: None,
279            kind: KnobKind::F32 {
280                default,
281                min,
282                max,
283                step,
284            },
285        })
286    }
287
288    pub fn opt_i32(
289        self,
290        id: &'static str,
291        label: &'static str,
292        default: Option<i32>,
293        min: i32,
294        max: i32,
295    ) -> Self {
296        self.push(KnobDecl {
297            id,
298            label,
299            group: None,
300            ctor_position: None,
301            kind: KnobKind::OptI32 {
302                default,
303                min,
304                max,
305                step: 1,
306            },
307        })
308    }
309
310    pub fn opt_f32(
311        self,
312        id: &'static str,
313        label: &'static str,
314        default: Option<f32>,
315        min: f32,
316        max: f32,
317    ) -> Self {
318        self.push(KnobDecl {
319            id,
320            label,
321            group: None,
322            ctor_position: None,
323            kind: KnobKind::OptF32 {
324                default,
325                min,
326                max,
327                step: ((max - min) / 100.0).max(0.01),
328            },
329        })
330    }
331
332    pub fn text(self, id: &'static str, label: &'static str, default: &str) -> Self {
333        self.push(KnobDecl {
334            id,
335            label,
336            group: None,
337            ctor_position: None,
338            kind: KnobKind::Text {
339                default: default.to_string(),
340            },
341        })
342    }
343
344    pub fn opt_text(self, id: &'static str, label: &'static str, default: Option<&str>) -> Self {
345        self.push(KnobDecl {
346            id,
347            label,
348            group: None,
349            ctor_position: None,
350            kind: KnobKind::OptText {
351                default: default.map(|s| s.to_string()),
352            },
353        })
354    }
355
356    pub fn choice(
357        self,
358        id: &'static str,
359        label: &'static str,
360        options: &[&'static str],
361        default: usize,
362    ) -> Self {
363        debug_assert!(default < options.len(), "default index out of range");
364        self.push(KnobDecl {
365            id,
366            label,
367            group: None,
368            ctor_position: None,
369            kind: KnobKind::Choice {
370                options: options.to_vec(),
371                default,
372            },
373        })
374    }
375
376    /// A Rust enum property (e.g. `ButtonVariant`): `variants` are the Rust
377    /// idents in declaration order, `default` their index. Unlike `choice`, a
378    /// design tool can emit `enum_path::variant` and offer a typed dropdown.
379    pub fn enum_(
380        self,
381        id: &'static str,
382        label: &'static str,
383        enum_path: &'static str,
384        variants: &[&'static str],
385        default: usize,
386    ) -> Self {
387        debug_assert!(default < variants.len(), "default index out of range");
388        self.push(KnobDecl {
389            id,
390            label,
391            group: None,
392            ctor_position: None,
393            kind: KnobKind::Enum {
394                enum_path,
395                variants: variants.to_vec(),
396                default,
397            },
398        })
399    }
400
401    /// Mark the most-recently-added knob as constructor argument `pos`
402    /// (`Slider::new(value, min, max)` → `.f32_("min", …).ctor(1)`), so a design
403    /// tool emits it positionally rather than as a named property.
404    pub fn ctor(mut self, pos: usize) -> Self {
405        if let Some(last) = self.decls.last_mut() {
406            last.ctor_position = Some(pos);
407        }
408        self
409    }
410
411    pub fn text_role(self, id: &'static str, label: &'static str, default: TextRole) -> Self {
412        self.push(KnobDecl {
413            id,
414            label,
415            group: None,
416            ctor_position: None,
417            kind: KnobKind::TextRole { default },
418        })
419    }
420
421    pub fn surface_role(self, id: &'static str, label: &'static str, default: SurfaceRole) -> Self {
422        self.push(KnobDecl {
423            id,
424            label,
425            group: None,
426            ctor_position: None,
427            kind: KnobKind::SurfaceRole { default },
428        })
429    }
430
431    pub fn border_role(self, id: &'static str, label: &'static str, default: BorderRole) -> Self {
432        self.push(KnobDecl {
433            id,
434            label,
435            group: None,
436            ctor_position: None,
437            kind: KnobKind::BorderRole { default },
438        })
439    }
440
441    pub fn text_style(self, id: &'static str, label: &'static str, default: TextStyleRole) -> Self {
442        self.push(KnobDecl {
443            id,
444            label,
445            group: None,
446            ctor_position: None,
447            kind: KnobKind::TextStyle { default },
448        })
449    }
450
451    /// Mark every knob added inside `f` as belonging to `group`. Used
452    /// for inspector-side organisation: composite widgets group knobs
453    /// per logical sub-component (`add_button`, `search`, …).
454    pub fn group<F>(mut self, group: &'static str, f: F) -> Self
455    where
456        F: FnOnce(KnobSpec) -> KnobSpec,
457    {
458        let nested = f(KnobSpec::default());
459        for mut decl in nested.decls {
460            // Don't clobber a group a nested `group(...)` already set.
461            if decl.group.is_none() {
462                decl.group = Some(group);
463            }
464            // Route through `push` so the duplicate-id guard applies uniformly.
465            self = self.push(decl);
466        }
467        self
468    }
469}
470
471// ---------------------------------------------------------------------------
472// Variant overrides — per-variant preset values
473// ---------------------------------------------------------------------------
474
475/// Concrete value for a knob, used by `PreviewVariant::Knobs` to override
476/// the spec's defaults for a specific named variant.
477#[derive(Debug, Clone)]
478pub enum KnobValue {
479    Bool(bool),
480    OptBool(Option<bool>),
481    I32(i32),
482    OptI32(Option<i32>),
483    F32(f32),
484    OptF32(Option<f32>),
485    Text(String),
486    OptText(Option<String>),
487    Choice(usize),
488    Enum(usize),
489    TextRole(TextRole),
490    SurfaceRole(SurfaceRole),
491    BorderRole(BorderRole),
492    TextStyle(TextStyleRole),
493}
494
495/// Variant override map: knob id → preset value. Built incrementally
496/// with `KnobOverrides::new().bool_("disabled", true)…` and supplied
497/// to `PreviewVariant::knobs(...)`.
498#[derive(Debug, Clone, Default)]
499pub struct KnobOverrides {
500    map: HashMap<&'static str, KnobValue>,
501}
502
503impl KnobOverrides {
504    pub fn new() -> Self {
505        Self::default()
506    }
507
508    pub fn get(&self, id: &str) -> Option<&KnobValue> {
509        self.map.get(id)
510    }
511
512    pub fn iter(&self) -> impl Iterator<Item = (&&'static str, &KnobValue)> {
513        self.map.iter()
514    }
515
516    pub fn bool_(mut self, id: &'static str, value: bool) -> Self {
517        self.map.insert(id, KnobValue::Bool(value));
518        self
519    }
520
521    pub fn opt_bool(mut self, id: &'static str, value: Option<bool>) -> Self {
522        self.map.insert(id, KnobValue::OptBool(value));
523        self
524    }
525
526    pub fn i32_(mut self, id: &'static str, value: i32) -> Self {
527        self.map.insert(id, KnobValue::I32(value));
528        self
529    }
530
531    pub fn f32_(mut self, id: &'static str, value: f32) -> Self {
532        self.map.insert(id, KnobValue::F32(value));
533        self
534    }
535
536    pub fn text(mut self, id: &'static str, value: impl Into<String>) -> Self {
537        self.map.insert(id, KnobValue::Text(value.into()));
538        self
539    }
540
541    pub fn opt_text(mut self, id: &'static str, value: Option<&str>) -> Self {
542        self.map
543            .insert(id, KnobValue::OptText(value.map(|s| s.to_string())));
544        self
545    }
546
547    pub fn choice(mut self, id: &'static str, value: usize) -> Self {
548        self.map.insert(id, KnobValue::Choice(value));
549        self
550    }
551
552    pub fn enum_(mut self, id: &'static str, value: usize) -> Self {
553        self.map.insert(id, KnobValue::Enum(value));
554        self
555    }
556
557    pub fn text_role(mut self, id: &'static str, value: TextRole) -> Self {
558        self.map.insert(id, KnobValue::TextRole(value));
559        self
560    }
561
562    pub fn surface_role(mut self, id: &'static str, value: SurfaceRole) -> Self {
563        self.map.insert(id, KnobValue::SurfaceRole(value));
564        self
565    }
566
567    pub fn border_role(mut self, id: &'static str, value: BorderRole) -> Self {
568        self.map.insert(id, KnobValue::BorderRole(value));
569        self
570    }
571
572    pub fn text_style(mut self, id: &'static str, value: TextStyleRole) -> Self {
573        self.map.insert(id, KnobValue::TextStyle(value));
574        self
575    }
576}
577
578// ---------------------------------------------------------------------------
579// Runtime — typed signals constructed from a spec
580// ---------------------------------------------------------------------------
581
582/// Runtime container of one `Signal<T>` per declared knob, populated
583/// from a `KnobSpec` and an optional set of variant overrides.
584///
585/// Construct once per (widget, variant) selection in the previewer; pass
586/// by reference to `WidgetCatalog::build`. The widget reads each knob's
587/// signal via the typed accessor methods (`bool_("disabled")`, …) and
588/// threads it into the constructed widget through `Prop::Bound`.
589///
590/// All accessors return a `Signal<T>` clone. Cheap — `Signal<T>` is `Rc`-backed.
591#[derive(Debug, Clone)]
592pub struct KnobValues {
593    bools: HashMap<&'static str, Signal<bool>>,
594    opt_bools: HashMap<&'static str, Signal<Option<bool>>>,
595    i32s: HashMap<&'static str, Signal<i32>>,
596    opt_i32s: HashMap<&'static str, Signal<Option<i32>>>,
597    f32s: HashMap<&'static str, Signal<f32>>,
598    opt_f32s: HashMap<&'static str, Signal<Option<f32>>>,
599    texts: HashMap<&'static str, Signal<String>>,
600    opt_texts: HashMap<&'static str, Signal<Option<String>>>,
601    choices: HashMap<&'static str, Signal<usize>>,
602    text_roles: HashMap<&'static str, Signal<TextRole>>,
603    surface_roles: HashMap<&'static str, Signal<SurfaceRole>>,
604    border_roles: HashMap<&'static str, Signal<BorderRole>>,
605    text_styles: HashMap<&'static str, Signal<TextStyleRole>>,
606}
607
608/// Whether an override `value` is the right `KnobValue` variant for a knob of
609/// `kind` (so `from_spec` can reject a mistyped override instead of dropping it).
610fn override_matches_kind(kind: &KnobKind, value: &KnobValue) -> bool {
611    matches!(
612        (kind, value),
613        (KnobKind::Bool { .. }, KnobValue::Bool(_))
614            | (KnobKind::OptBool { .. }, KnobValue::OptBool(_))
615            | (KnobKind::I32 { .. }, KnobValue::I32(_))
616            | (KnobKind::OptI32 { .. }, KnobValue::OptI32(_))
617            | (KnobKind::F32 { .. }, KnobValue::F32(_))
618            | (KnobKind::OptF32 { .. }, KnobValue::OptF32(_))
619            | (KnobKind::Text { .. }, KnobValue::Text(_))
620            | (KnobKind::OptText { .. }, KnobValue::OptText(_))
621            | (KnobKind::Choice { .. }, KnobValue::Choice(_))
622            | (KnobKind::Enum { .. }, KnobValue::Enum(_))
623            | (KnobKind::TextRole { .. }, KnobValue::TextRole(_))
624            | (KnobKind::SurfaceRole { .. }, KnobValue::SurfaceRole(_))
625            | (KnobKind::BorderRole { .. }, KnobValue::BorderRole(_))
626            | (KnobKind::TextStyle { .. }, KnobValue::TextStyle(_))
627    )
628}
629
630impl KnobValues {
631    /// Build a fresh runtime view for a spec, applying optional
632    /// variant overrides on top of each knob's declared default.
633    pub fn from_spec(spec: &KnobSpec, overrides: Option<&KnobOverrides>) -> Self {
634        let mut values = KnobValues {
635            bools: HashMap::new(),
636            opt_bools: HashMap::new(),
637            i32s: HashMap::new(),
638            opt_i32s: HashMap::new(),
639            f32s: HashMap::new(),
640            opt_f32s: HashMap::new(),
641            texts: HashMap::new(),
642            opt_texts: HashMap::new(),
643            choices: HashMap::new(),
644            text_roles: HashMap::new(),
645            surface_roles: HashMap::new(),
646            border_roles: HashMap::new(),
647            text_styles: HashMap::new(),
648        };
649        for decl in &spec.decls {
650            let ov = overrides.and_then(|o| o.get(decl.id));
651            // Catch an override whose value type doesn't match the knob's
652            // declared kind — it would otherwise fall through to the default
653            // arm and be silently dropped. Dev tooling: panic on misuse.
654            debug_assert!(
655                ov.is_none_or(|v| override_matches_kind(&decl.kind, v)),
656                "knob '{}' override type does not match its declared kind",
657                decl.id
658            );
659            match (&decl.kind, ov) {
660                (KnobKind::Bool { default: _ }, Some(KnobValue::Bool(v))) => {
661                    values.bools.insert(decl.id, Signal::new(*v));
662                }
663                (KnobKind::Bool { default }, _) => {
664                    values.bools.insert(decl.id, Signal::new(*default));
665                }
666                (KnobKind::OptBool { default: _ }, Some(KnobValue::OptBool(v))) => {
667                    values.opt_bools.insert(decl.id, Signal::new(*v));
668                }
669                (KnobKind::OptBool { default }, _) => {
670                    values.opt_bools.insert(decl.id, Signal::new(*default));
671                }
672                (KnobKind::I32 { .. }, Some(KnobValue::I32(v))) => {
673                    values.i32s.insert(decl.id, Signal::new(*v));
674                }
675                (KnobKind::I32 { default, .. }, _) => {
676                    values.i32s.insert(decl.id, Signal::new(*default));
677                }
678                (KnobKind::OptI32 { .. }, Some(KnobValue::OptI32(v))) => {
679                    values.opt_i32s.insert(decl.id, Signal::new(*v));
680                }
681                (KnobKind::OptI32 { default, .. }, _) => {
682                    values.opt_i32s.insert(decl.id, Signal::new(*default));
683                }
684                (KnobKind::F32 { .. }, Some(KnobValue::F32(v))) => {
685                    values.f32s.insert(decl.id, Signal::new(*v));
686                }
687                (KnobKind::F32 { default, .. }, _) => {
688                    values.f32s.insert(decl.id, Signal::new(*default));
689                }
690                (KnobKind::OptF32 { .. }, Some(KnobValue::OptF32(v))) => {
691                    values.opt_f32s.insert(decl.id, Signal::new(*v));
692                }
693                (KnobKind::OptF32 { default, .. }, _) => {
694                    values.opt_f32s.insert(decl.id, Signal::new(*default));
695                }
696                (KnobKind::Text { default: _ }, Some(KnobValue::Text(v))) => {
697                    values.texts.insert(decl.id, Signal::new(v.clone()));
698                }
699                (KnobKind::Text { default }, _) => {
700                    values.texts.insert(decl.id, Signal::new(default.clone()));
701                }
702                (KnobKind::OptText { default: _ }, Some(KnobValue::OptText(v))) => {
703                    values.opt_texts.insert(decl.id, Signal::new(v.clone()));
704                }
705                (KnobKind::OptText { default }, _) => {
706                    values
707                        .opt_texts
708                        .insert(decl.id, Signal::new(default.clone()));
709                }
710                (KnobKind::Choice { .. }, Some(KnobValue::Choice(v))) => {
711                    values.choices.insert(decl.id, Signal::new(*v));
712                }
713                (KnobKind::Choice { default, .. }, _) => {
714                    values.choices.insert(decl.id, Signal::new(*default));
715                }
716                // Enum knobs share the Choice storage (a usize index).
717                (KnobKind::Enum { .. }, Some(KnobValue::Enum(v))) => {
718                    values.choices.insert(decl.id, Signal::new(*v));
719                }
720                (KnobKind::Enum { default, .. }, _) => {
721                    values.choices.insert(decl.id, Signal::new(*default));
722                }
723                (KnobKind::TextRole { default: _ }, Some(KnobValue::TextRole(v))) => {
724                    values.text_roles.insert(decl.id, Signal::new(*v));
725                }
726                (KnobKind::TextRole { default }, _) => {
727                    values.text_roles.insert(decl.id, Signal::new(*default));
728                }
729                (KnobKind::SurfaceRole { default: _ }, Some(KnobValue::SurfaceRole(v))) => {
730                    values.surface_roles.insert(decl.id, Signal::new(*v));
731                }
732                (KnobKind::SurfaceRole { default }, _) => {
733                    values.surface_roles.insert(decl.id, Signal::new(*default));
734                }
735                (KnobKind::BorderRole { default: _ }, Some(KnobValue::BorderRole(v))) => {
736                    values.border_roles.insert(decl.id, Signal::new(*v));
737                }
738                (KnobKind::BorderRole { default }, _) => {
739                    values.border_roles.insert(decl.id, Signal::new(*default));
740                }
741                (KnobKind::TextStyle { default: _ }, Some(KnobValue::TextStyle(v))) => {
742                    values.text_styles.insert(decl.id, Signal::new(*v));
743                }
744                (KnobKind::TextStyle { default }, _) => {
745                    values.text_styles.insert(decl.id, Signal::new(*default));
746                }
747            }
748        }
749        values
750    }
751
752    pub fn bool_(&self, id: &str) -> Signal<bool> {
753        self.bools
754            .get(id)
755            .cloned()
756            .unwrap_or_else(|| panic!("knob '{}' is not declared as Bool", id))
757    }
758
759    pub fn opt_bool(&self, id: &str) -> Signal<Option<bool>> {
760        self.opt_bools
761            .get(id)
762            .cloned()
763            .unwrap_or_else(|| panic!("knob '{}' is not declared as OptBool", id))
764    }
765
766    pub fn i32_(&self, id: &str) -> Signal<i32> {
767        self.i32s
768            .get(id)
769            .cloned()
770            .unwrap_or_else(|| panic!("knob '{}' is not declared as I32", id))
771    }
772
773    pub fn opt_i32(&self, id: &str) -> Signal<Option<i32>> {
774        self.opt_i32s
775            .get(id)
776            .cloned()
777            .unwrap_or_else(|| panic!("knob '{}' is not declared as OptI32", id))
778    }
779
780    pub fn f32_(&self, id: &str) -> Signal<f32> {
781        self.f32s
782            .get(id)
783            .cloned()
784            .unwrap_or_else(|| panic!("knob '{}' is not declared as F32", id))
785    }
786
787    pub fn opt_f32(&self, id: &str) -> Signal<Option<f32>> {
788        self.opt_f32s
789            .get(id)
790            .cloned()
791            .unwrap_or_else(|| panic!("knob '{}' is not declared as OptF32", id))
792    }
793
794    pub fn text(&self, id: &str) -> Signal<String> {
795        self.texts
796            .get(id)
797            .cloned()
798            .unwrap_or_else(|| panic!("knob '{}' is not declared as Text", id))
799    }
800
801    pub fn opt_text(&self, id: &str) -> Signal<Option<String>> {
802        self.opt_texts
803            .get(id)
804            .cloned()
805            .unwrap_or_else(|| panic!("knob '{}' is not declared as OptText", id))
806    }
807
808    pub fn choice(&self, id: &str) -> Signal<usize> {
809        self.choices
810            .get(id)
811            .cloned()
812            .unwrap_or_else(|| panic!("knob '{}' is not declared as Choice", id))
813    }
814
815    /// Enum knobs share the Choice storage (a `usize` index); alias of
816    /// [`choice`](Self::choice) that reads clearer at enum build sites.
817    pub fn enum_(&self, id: &str) -> Signal<usize> {
818        self.choice(id)
819    }
820
821    pub fn text_role(&self, id: &str) -> Signal<TextRole> {
822        self.text_roles
823            .get(id)
824            .cloned()
825            .unwrap_or_else(|| panic!("knob '{}' is not declared as TextRole", id))
826    }
827
828    pub fn surface_role(&self, id: &str) -> Signal<SurfaceRole> {
829        self.surface_roles
830            .get(id)
831            .cloned()
832            .unwrap_or_else(|| panic!("knob '{}' is not declared as SurfaceRole", id))
833    }
834
835    pub fn border_role(&self, id: &str) -> Signal<BorderRole> {
836        self.border_roles
837            .get(id)
838            .cloned()
839            .unwrap_or_else(|| panic!("knob '{}' is not declared as BorderRole", id))
840    }
841
842    pub fn text_style(&self, id: &str) -> Signal<TextStyleRole> {
843        self.text_styles
844            .get(id)
845            .cloned()
846            .unwrap_or_else(|| panic!("knob '{}' is not declared as TextStyle", id))
847    }
848
849    /// Bind every knob signal to `widget_id` at the given level via
850    /// the supplied registry. Used by the canvas to force a rebuild
851    /// whenever any knob mutates — many widgets read knob values
852    /// once at construction time (no `Prop::Bound` for every property)
853    /// so the catch-all is a rebuild on each change.
854    pub fn bind_all(
855        &self,
856        widget_id: crate::__widget_id::WidgetId,
857        registry: &teksilo_core::binding::BindingRegistry,
858        level: teksilo_core::binding::BindingLevel,
859    ) {
860        for sig in self.bools.values() {
861            sig.bind_to(widget_id, registry, level);
862        }
863        for sig in self.opt_bools.values() {
864            sig.bind_to(widget_id, registry, level);
865        }
866        for sig in self.i32s.values() {
867            sig.bind_to(widget_id, registry, level);
868        }
869        for sig in self.opt_i32s.values() {
870            sig.bind_to(widget_id, registry, level);
871        }
872        for sig in self.f32s.values() {
873            sig.bind_to(widget_id, registry, level);
874        }
875        for sig in self.opt_f32s.values() {
876            sig.bind_to(widget_id, registry, level);
877        }
878        for sig in self.texts.values() {
879            sig.bind_to(widget_id, registry, level);
880        }
881        for sig in self.opt_texts.values() {
882            sig.bind_to(widget_id, registry, level);
883        }
884        for sig in self.choices.values() {
885            sig.bind_to(widget_id, registry, level);
886        }
887        for sig in self.text_roles.values() {
888            sig.bind_to(widget_id, registry, level);
889        }
890        for sig in self.surface_roles.values() {
891            sig.bind_to(widget_id, registry, level);
892        }
893        for sig in self.border_roles.values() {
894            sig.bind_to(widget_id, registry, level);
895        }
896        for sig in self.text_styles.values() {
897            sig.bind_to(widget_id, registry, level);
898        }
899    }
900}
901
902#[cfg(test)]
903mod tests {
904    use super::*;
905
906    #[test]
907    fn knob_spec_records_decls_in_insertion_order() {
908        let spec = KnobSpec::new()
909            .text("label", "Label", "Click")
910            .bool_("disabled", "Disabled", false)
911            .choice("role", "Role", &["Primary", "Secondary"], 0);
912        let ids: Vec<_> = spec.declarations().iter().map(|d| d.id).collect();
913        assert_eq!(ids, vec!["label", "disabled", "role"]);
914    }
915
916    #[test]
917    fn knob_values_from_spec_uses_defaults_when_no_override() {
918        let spec = KnobSpec::new()
919            .bool_("disabled", "Disabled", true)
920            .text("label", "Label", "Hello")
921            .choice("role", "Role", &["A", "B", "C"], 1);
922        let values = KnobValues::from_spec(&spec, None);
923        assert!(values.bool_("disabled").get());
924        assert_eq!(values.text("label").get(), "Hello");
925        assert_eq!(values.choice("role").get(), 1);
926    }
927
928    #[test]
929    fn knob_values_applies_overrides() {
930        let spec = KnobSpec::new()
931            .bool_("disabled", "Disabled", false)
932            .text("label", "Label", "Default");
933        let overrides = KnobOverrides::new()
934            .bool_("disabled", true)
935            .text("label", "Overridden");
936        let values = KnobValues::from_spec(&spec, Some(&overrides));
937        assert!(values.bool_("disabled").get());
938        assert_eq!(values.text("label").get(), "Overridden");
939    }
940
941    #[test]
942    fn knob_values_kind_mismatch_panics() {
943        let spec = KnobSpec::new().bool_("flag", "Flag", false);
944        let values = KnobValues::from_spec(&spec, None);
945        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
946            values.text("flag");
947        }));
948        assert!(result.is_err());
949    }
950
951    #[test]
952    fn knob_spec_groups_propagate_to_decls() {
953        let spec = KnobSpec::new()
954            .bool_("global", "Global", false)
955            .group("search", |g| {
956                g.bool_("visible", "Visible", true)
957                    .text("placeholder", "Placeholder", "Search…")
958            });
959        let decls = spec.declarations();
960        assert_eq!(decls[0].group, None);
961        assert_eq!(decls[1].group, Some("search"));
962        assert_eq!(decls[2].group, Some("search"));
963    }
964
965    #[test]
966    fn enum_info_carries_path_variants_and_default() {
967        let spec = KnobSpec::new().enum_(
968            "variant",
969            "Variant",
970            "ButtonVariant",
971            &["Filled", "Tinted", "Plain"],
972            2,
973        );
974        let info = spec.get("variant").unwrap().kind.enum_info().unwrap();
975        assert_eq!(info.enum_path, "ButtonVariant");
976        assert_eq!(info.variants, vec!["Filled", "Tinted", "Plain"]);
977        assert_eq!(info.default, 2);
978    }
979
980    #[test]
981    fn enum_info_for_role_kinds_uses_token_variant_names() {
982        let spec = KnobSpec::new().text_role("color", "Color", TextRole::Secondary);
983        let info = spec.get("color").unwrap().kind.enum_info().unwrap();
984        assert_eq!(info.enum_path, "TextRole");
985        assert_eq!(info.variants, TextRole::variant_names().to_vec());
986        assert_eq!(info.variants[info.default], "Secondary");
987        // A scalar kind has no enum_info.
988        assert!(
989            KnobSpec::new()
990                .bool_("x", "X", false)
991                .get("x")
992                .unwrap()
993                .kind
994                .enum_info()
995                .is_none()
996        );
997    }
998
999    #[test]
1000    fn ctor_marks_only_the_last_decl() {
1001        let spec = KnobSpec::new()
1002            .text("label", "Label", "Go")
1003            .ctor(0)
1004            .bool_("enabled", "Enabled", true);
1005        assert_eq!(spec.get("label").unwrap().ctor_position, Some(0));
1006        assert_eq!(spec.get("enabled").unwrap().ctor_position, None);
1007    }
1008
1009    #[test]
1010    fn enum_knob_resolves_to_its_default_index() {
1011        let spec =
1012            KnobSpec::new().enum_("variant", "Variant", "ButtonVariant", &["A", "B", "C"], 1);
1013        let values = KnobValues::from_spec(&spec, None);
1014        assert_eq!(values.enum_("variant").get(), 1);
1015        // An override moves it.
1016        let ov = KnobOverrides::new().enum_("variant", 2);
1017        assert_eq!(
1018            KnobValues::from_spec(&spec, Some(&ov))
1019                .enum_("variant")
1020                .get(),
1021            2
1022        );
1023    }
1024
1025    #[test]
1026    #[should_panic(expected = "does not match its declared kind")]
1027    fn from_spec_rejects_a_mistyped_override() {
1028        let spec = KnobSpec::new().enum_("variant", "Variant", "ButtonVariant", &["A", "B"], 0);
1029        // A `choice` override on an Enum knob is the wrong KnobValue variant —
1030        // previously dropped silently, now caught (dev tooling: panic on misuse).
1031        let ov = KnobOverrides::new().choice("variant", 1);
1032        let _ = KnobValues::from_spec(&spec, Some(&ov));
1033    }
1034}