1use std::collections::HashMap;
18
19use teksilo_core::signal::Signal;
20use teksilo_tokens::{BorderRole, SurfaceRole, TextRole, TextStyleRole};
21
22#[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 Choice {
68 options: Vec<&'static str>,
69 default: usize,
70 },
71 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#[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 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 pub ctor_position: Option<usize>,
163 pub kind: KnobKind,
164}
165
166#[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 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 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 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 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 if decl.group.is_none() {
462 decl.group = Some(group);
463 }
464 self = self.push(decl);
466 }
467 self
468 }
469}
470
471#[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#[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#[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
608fn 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 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 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 (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 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 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 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 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 let ov = KnobOverrides::new().choice("variant", 1);
1032 let _ = KnobValues::from_spec(&spec, Some(&ov));
1033 }
1034}