Skip to main content

fret_ui_kit/node_graph/
presets.rs

1//! Node-graph theme preset families (v1).
2//!
3//! The schema is designed to be JSON-serializable and paint-only: switching presets should not
4//! require rebuilding derived geometry in the node graph widget.
5
6use std::collections::HashMap;
7
8use fret_core::scene::DashPatternV1;
9use fret_core::window::ColorScheme;
10use fret_core::{Color, Px};
11use fret_ui::ThemeSnapshot;
12use serde::Deserialize;
13
14/// Parse a `node_graph_theme_presets.v1` JSON document.
15///
16/// This is intentionally a thin wrapper around `serde_json::from_str` so higher-level crates can
17/// share a consistent entry-point for preset loading without depending on the caller's `serde`
18/// glue.
19pub fn parse_node_graph_theme_presets_v1(
20    raw: &str,
21) -> Result<NodeGraphThemePresetsV1, serde_json::Error> {
22    serde_json::from_str(raw)
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26pub enum NodeGraphPresetFamily {
27    WorkflowClean,
28    SchematicContrast,
29    GraphDark,
30}
31
32impl NodeGraphPresetFamily {
33    pub fn all() -> [Self; 3] {
34        [
35            Self::WorkflowClean,
36            Self::SchematicContrast,
37            Self::GraphDark,
38        ]
39    }
40
41    pub fn display_name(self) -> &'static str {
42        match self {
43            Self::WorkflowClean => "WorkflowClean",
44            Self::SchematicContrast => "SchematicContrast",
45            Self::GraphDark => "GraphDark",
46        }
47    }
48
49    pub fn preset_id(self) -> &'static str {
50        match self {
51            Self::WorkflowClean => "workflow_clean",
52            Self::SchematicContrast => "schematic_contrast",
53            Self::GraphDark => "graph_dark",
54        }
55    }
56}
57
58/// Derive in-tree preset families from an application theme snapshot.
59///
60/// This function is pure and intended to be used by higher-level editors to build paint-only
61/// presets without hard-coding palette values into the node-graph integration crate.
62pub fn theme_derived_presets(theme: &ThemeSnapshot) -> NodeGraphThemePresetsV1 {
63    NodeGraphThemePresetsV1 {
64        schema_version: "node_graph_theme_presets.v1".to_string(),
65        notes: "derived from ThemeSnapshot (with opt-out for GraphDark on light themes)"
66            .to_string(),
67        presets: vec![
68            theme_derived_preset(theme, NodeGraphPresetFamily::WorkflowClean),
69            theme_derived_preset(theme, NodeGraphPresetFamily::SchematicContrast),
70            theme_derived_preset(theme, NodeGraphPresetFamily::GraphDark),
71        ],
72    }
73}
74
75fn theme_derived_preset(
76    theme: &ThemeSnapshot,
77    family: NodeGraphPresetFamily,
78) -> NodeGraphThemePresetV1 {
79    fn alpha(mut c: Color, a: f32) -> Color {
80        c.a = a;
81        c
82    }
83
84    fn mix(a: Color, b: Color, t: f32) -> Color {
85        let t = t.clamp(0.0, 1.0);
86        Color {
87            r: a.r + (b.r - a.r) * t,
88            g: a.g + (b.g - a.g) * t,
89            b: a.b + (b.b - a.b) * t,
90            a: a.a + (b.a - a.a) * t,
91        }
92    }
93
94    fn tint(base: Color, accent: Color, amount: f32) -> Color {
95        let mut out = mix(base, accent, amount);
96        out.a = 1.0;
97        out
98    }
99
100    let scheme_is_dark = theme.color_scheme == Some(ColorScheme::Dark);
101
102    let background = theme.color_token("background");
103    let foreground = theme.color_token("foreground");
104    let border = theme.color_token("border");
105    let ring = theme.color_token("ring");
106    let card = theme.color_token("card");
107    let card_foreground = theme.color_token("card-foreground");
108    let muted_foreground = theme.color_token("muted-foreground");
109    let accent = theme.color_token("accent");
110    let primary = theme.color_token("primary");
111    let destructive = theme.color_token("destructive");
112
113    let chart_1 = theme.color_token("chart-1");
114    let chart_2 = theme.color_token("chart-2");
115    let chart_3 = theme.color_token("chart-3");
116    let chart_4 = theme.color_token("chart-4");
117    let chart_5 = theme.color_token("chart-5");
118
119    let kind_colors = [
120        ("source", chart_1),
121        ("compute", chart_2),
122        ("condition", chart_3),
123        ("output", chart_4),
124        ("utility", chart_5),
125        ("preview", destructive),
126    ];
127
128    let (canvas_bg, grid_minor, grid_major, node_bg, node_border, node_border_selected, title_text) =
129        match family {
130            NodeGraphPresetFamily::WorkflowClean => (
131                background,
132                alpha(border, 0.50),
133                alpha(border, 0.80),
134                card,
135                alpha(border, 1.0),
136                alpha(ring, 1.0),
137                card_foreground,
138            ),
139            NodeGraphPresetFamily::SchematicContrast => (
140                background,
141                alpha(border, 0.90),
142                alpha(border, 1.0),
143                card,
144                alpha(foreground, 1.0),
145                alpha(foreground, 1.0),
146                theme.color_token("primary-foreground"),
147            ),
148            NodeGraphPresetFamily::GraphDark => {
149                if scheme_is_dark {
150                    (
151                        background,
152                        alpha(border, 0.35),
153                        alpha(border, 0.70),
154                        tint(card, border, 0.20),
155                        alpha(border, 1.0),
156                        alpha(ring, 1.0),
157                        card_foreground,
158                    )
159                } else {
160                    // Opt-out: keep GraphDark as an explicit style family, but avoid forcing dark
161                    // palette on a light theme snapshot.
162                    (
163                        background,
164                        alpha(border, 0.50),
165                        alpha(border, 0.80),
166                        card,
167                        alpha(border, 1.0),
168                        alpha(ring, 1.0),
169                        card_foreground,
170                    )
171                }
172            }
173        };
174
175    let header_default = match family {
176        NodeGraphPresetFamily::WorkflowClean => tint(card, border, 0.10),
177        NodeGraphPresetFamily::SchematicContrast => alpha(theme.color_token("secondary"), 1.0),
178        NodeGraphPresetFamily::GraphDark => tint(node_bg, border, 0.20),
179    };
180
181    let mut header_by_kind: HashMap<String, RgbaV1> = HashMap::new();
182    for (k, c) in kind_colors {
183        let header = match family {
184            NodeGraphPresetFamily::WorkflowClean => tint(card, c, 0.22),
185            NodeGraphPresetFamily::SchematicContrast => alpha(c, 1.0),
186            NodeGraphPresetFamily::GraphDark => {
187                if scheme_is_dark {
188                    tint(node_bg, c, 0.35)
189                } else {
190                    tint(card, c, 0.22)
191                }
192            }
193        };
194        header_by_kind.insert(k.to_string(), header.into());
195    }
196
197    let (ring_sel, ring_focus) = match family {
198        NodeGraphPresetFamily::WorkflowClean => (
199            NodeRingTokensV1 {
200                color: alpha(primary, 0.40).into(),
201                width_px: 3.0,
202                pad_px: 2.0,
203            },
204            NodeRingTokensV1 {
205                color: alpha(primary, 0.60).into(),
206                width_px: 2.0,
207                pad_px: 1.0,
208            },
209        ),
210        NodeGraphPresetFamily::SchematicContrast => (
211            NodeRingTokensV1 {
212                color: alpha(theme.color_token("chart-4"), 1.0).into(),
213                width_px: 4.0,
214                pad_px: 0.0,
215            },
216            NodeRingTokensV1 {
217                color: alpha(theme.color_token("chart-5"), 1.0).into(),
218                width_px: 4.0,
219                pad_px: 0.0,
220            },
221        ),
222        NodeGraphPresetFamily::GraphDark => (
223            NodeRingTokensV1 {
224                color: alpha(ring, 1.0).into(),
225                width_px: 3.0,
226                pad_px: 2.0,
227            },
228            NodeRingTokensV1 {
229                color: alpha(accent, 1.0).into(),
230                width_px: 3.0,
231                pad_px: 2.0,
232            },
233        ),
234    };
235
236    let (hover, invalid, convertible) = match family {
237        NodeGraphPresetFamily::WorkflowClean => (
238            alpha(theme.color_token("chart-1"), 1.0),
239            alpha(destructive, 1.0),
240            alpha(theme.color_token("chart-1"), 1.0),
241        ),
242        NodeGraphPresetFamily::SchematicContrast => (
243            alpha(foreground, 1.0),
244            alpha(destructive, 1.0),
245            alpha(theme.color_token("chart-1"), 1.0),
246        ),
247        NodeGraphPresetFamily::GraphDark => (
248            alpha(theme.color_token("chart-1"), 1.0),
249            alpha(destructive, 1.0),
250            alpha(theme.color_token("chart-1"), 1.0),
251        ),
252    };
253
254    let port_data = match family {
255        NodeGraphPresetFamily::WorkflowClean => PortTokensV1 {
256            fill: alpha(muted_foreground, 0.85).into(),
257            stroke: alpha(muted_foreground, 1.0).into(),
258            stroke_width_px: 1.0,
259            inner_scale: 1.0,
260            shape: PortShapeKindV1::Circle,
261        },
262        NodeGraphPresetFamily::SchematicContrast => PortTokensV1 {
263            fill: alpha(theme.color_token("chart-1"), 1.0).into(),
264            stroke: alpha(foreground, 1.0).into(),
265            stroke_width_px: 2.0,
266            inner_scale: 1.0,
267            shape: PortShapeKindV1::Circle,
268        },
269        NodeGraphPresetFamily::GraphDark => PortTokensV1 {
270            fill: alpha(theme.color_token("chart-2"), 1.0).into(),
271            stroke: alpha(theme.color_token("chart-2"), 1.0).into(),
272            stroke_width_px: 1.5,
273            inner_scale: 1.0,
274            shape: PortShapeKindV1::Circle,
275        },
276    };
277
278    let port_exec = match family {
279        NodeGraphPresetFamily::WorkflowClean => PortTokensV1 {
280            fill: alpha(card, 1.0).into(),
281            stroke: alpha(foreground, 0.85).into(),
282            stroke_width_px: 1.5,
283            inner_scale: 0.0,
284            shape: PortShapeKindV1::Circle,
285        },
286        NodeGraphPresetFamily::SchematicContrast => PortTokensV1 {
287            fill: alpha(theme.color_token("chart-3"), 1.0).into(),
288            stroke: alpha(foreground, 1.0).into(),
289            stroke_width_px: 2.0,
290            inner_scale: 0.0,
291            shape: PortShapeKindV1::Circle,
292        },
293        NodeGraphPresetFamily::GraphDark => PortTokensV1 {
294            fill: alpha(destructive, 0.65).into(),
295            stroke: alpha(destructive, 1.0).into(),
296            stroke_width_px: 1.5,
297            inner_scale: 0.0,
298            shape: PortShapeKindV1::Circle,
299        },
300    };
301
302    let port_preview = PortTokensV1 {
303        fill: header_by_kind
304            .get("preview")
305            .copied()
306            .unwrap_or_else(|| alpha(destructive, 1.0).into()),
307        stroke: alpha(destructive, 1.0).into(),
308        stroke_width_px: 1.0,
309        inner_scale: 0.5,
310        shape: PortShapeKindV1::Circle,
311    };
312
313    let (wire_data, wire_exec, wire_preview) = match family {
314        NodeGraphPresetFamily::WorkflowClean => (
315            alpha(muted_foreground, 1.0),
316            alpha(theme.color_token("secondary-foreground"), 1.0),
317            alpha(border, 1.0),
318        ),
319        NodeGraphPresetFamily::SchematicContrast => (
320            alpha(theme.color_token("chart-1"), 1.0),
321            alpha(theme.color_token("chart-3"), 1.0),
322            alpha(theme.color_token("chart-4"), 1.0),
323        ),
324        NodeGraphPresetFamily::GraphDark => (
325            alpha(theme.color_token("chart-2"), 1.0),
326            alpha(destructive, 1.0),
327            alpha(theme.color_token("chart-4"), 1.0),
328        ),
329    };
330
331    let (highlight_sel, highlight_hov) = match family {
332        NodeGraphPresetFamily::WorkflowClean => (
333            WireHighlightTokensV1 {
334                width_mul: 0.65,
335                alpha_mul: 0.80,
336                color: None,
337            },
338            WireHighlightTokensV1 {
339                width_mul: 0.70,
340                alpha_mul: 0.95,
341                color: None,
342            },
343        ),
344        NodeGraphPresetFamily::SchematicContrast => (
345            WireHighlightTokensV1 {
346                width_mul: 0.70,
347                alpha_mul: 0.90,
348                color: None,
349            },
350            WireHighlightTokensV1 {
351                width_mul: 0.75,
352                alpha_mul: 1.0,
353                color: None,
354            },
355        ),
356        NodeGraphPresetFamily::GraphDark => (
357            WireHighlightTokensV1 {
358                width_mul: 0.65,
359                alpha_mul: 0.85,
360                color: Some(alpha(convertible, 1.0).into()),
361            },
362            WireHighlightTokensV1 {
363                width_mul: 0.70,
364                alpha_mul: 0.95,
365                color: Some(alpha(hover, 1.0).into()),
366            },
367        ),
368    };
369
370    NodeGraphThemePresetV1 {
371        id: family.preset_id().to_string(),
372        display_name: family.display_name().to_string(),
373        intent: match family {
374            NodeGraphPresetFamily::WorkflowClean => "theme-derived, clean, minimal".to_string(),
375            NodeGraphPresetFamily::SchematicContrast => "theme-derived, high contrast".to_string(),
376            NodeGraphPresetFamily::GraphDark => {
377                if scheme_is_dark {
378                    "theme-derived, dark with neon accents".to_string()
379                } else {
380                    "theme-derived, dark family (opted-out on light theme)".to_string()
381                }
382            }
383        },
384        paint_only_tokens: PaintOnlyTokensV1 {
385            canvas: CanvasTokensV1 {
386                background: canvas_bg.into(),
387            },
388            grid: GridTokensV1 {
389                minor_color: grid_minor.into(),
390                major_color: grid_major.into(),
391            },
392            text: TextTokensV1 {
393                primary: title_text.into(),
394                muted: muted_foreground.into(),
395            },
396            node: NodeTokensV1 {
397                body_background: node_bg.into(),
398                border: node_border.into(),
399                border_selected: node_border_selected.into(),
400                header_background_default: header_default.into(),
401                header_by_kind,
402                title_text: title_text.into(),
403                ring_selected: ring_sel,
404                ring_focused: ring_focus,
405            },
406            port: PortThemeTokensV1 {
407                by_port_kind: PortKindTokensV1 {
408                    data: port_data,
409                    exec: port_exec,
410                    preview: port_preview,
411                },
412            },
413            wire: WireTokensV1 {
414                data_color: wire_data.into(),
415                exec_color: wire_exec.into(),
416                preview_color: wire_preview.into(),
417                dash_preview: DashPatternTokensV1 {
418                    dash_px: 4.0,
419                    gap_px: 4.0,
420                    phase_px: 0.0,
421                },
422                dash_invalid: DashPatternTokensV1 {
423                    dash_px: 6.0,
424                    gap_px: 3.0,
425                    phase_px: 0.0,
426                },
427                dash_emphasis: DashPatternTokensV1 {
428                    dash_px: 2.0,
429                    gap_px: 2.0,
430                    phase_px: 0.0,
431                },
432                highlight_selected: Some(highlight_sel),
433                highlight_hovered: Some(highlight_hov),
434                marker_exec_end: Some(EdgeMarkerTokensV1 {
435                    kind: EdgeMarkerKindTokensV1::Arrow,
436                    size_px: 12.0,
437                }),
438                marker_exec_start: Some(EdgeMarkerTokensV1 {
439                    kind: EdgeMarkerKindTokensV1::Arrow,
440                    size_px: 8.0,
441                }),
442                marker_data_end: None,
443                marker_data_start: None,
444                marker_size_mul_selected: Some(1.15),
445                marker_size_mul_hovered: Some(1.25),
446            },
447            states: StateTokensV1 {
448                hover: StateColorV1 {
449                    color: hover.into(),
450                },
451                invalid: StateColorV1 {
452                    color: invalid.into(),
453                },
454                convertible: StateColorV1 {
455                    color: convertible.into(),
456                },
457                disabled: DisabledStateV1 { alpha_mul: 0.5 },
458            },
459        },
460        layout_tokens: None,
461        interaction_state_matrix: serde_json::Value::Null,
462        example_compositions: serde_json::Value::Null,
463        a11y_notes: serde_json::Value::Null,
464    }
465}
466
467#[derive(Debug, Clone, Copy, Deserialize)]
468pub struct RgbaV1 {
469    pub r: f32,
470    pub g: f32,
471    pub b: f32,
472    pub a: f32,
473}
474
475impl From<RgbaV1> for Color {
476    fn from(v: RgbaV1) -> Self {
477        Color {
478            r: v.r,
479            g: v.g,
480            b: v.b,
481            a: v.a,
482        }
483    }
484}
485
486impl From<Color> for RgbaV1 {
487    fn from(v: Color) -> Self {
488        RgbaV1 {
489            r: v.r,
490            g: v.g,
491            b: v.b,
492            a: v.a,
493        }
494    }
495}
496
497#[derive(Debug, Clone, Copy, Deserialize)]
498pub struct DashPatternTokensV1 {
499    pub dash_px: f32,
500    pub gap_px: f32,
501    pub phase_px: f32,
502}
503
504impl DashPatternTokensV1 {
505    pub fn into_dash(self) -> DashPatternV1 {
506        DashPatternV1::new(Px(self.dash_px), Px(self.gap_px), Px(self.phase_px))
507    }
508}
509
510#[derive(Debug, Clone, Copy, Deserialize)]
511pub struct NodeRingTokensV1 {
512    pub color: RgbaV1,
513    pub width_px: f32,
514    pub pad_px: f32,
515}
516
517#[derive(Debug, Clone, Deserialize)]
518pub struct NodeGraphThemePresetsV1 {
519    #[allow(dead_code)]
520    pub schema_version: String,
521    #[allow(dead_code)]
522    pub notes: String,
523    pub presets: Vec<NodeGraphThemePresetV1>,
524}
525
526#[derive(Debug, Clone, Deserialize)]
527pub struct NodeGraphThemePresetV1 {
528    pub id: String,
529    #[allow(dead_code)]
530    pub display_name: String,
531    #[allow(dead_code)]
532    pub intent: String,
533    pub paint_only_tokens: PaintOnlyTokensV1,
534    #[serde(default)]
535    pub layout_tokens: Option<LayoutTokensV1>,
536    #[serde(default)]
537    #[allow(dead_code)]
538    pub interaction_state_matrix: serde_json::Value,
539    #[serde(default)]
540    #[allow(dead_code)]
541    pub example_compositions: serde_json::Value,
542    #[serde(default)]
543    #[allow(dead_code)]
544    pub a11y_notes: serde_json::Value,
545}
546
547#[derive(Debug, Clone, Deserialize)]
548pub struct LayoutTokensV1 {
549    #[allow(dead_code)]
550    pub optional: bool,
551    #[serde(default)]
552    pub grid_minor_width_px: Option<f32>,
553    #[serde(default)]
554    #[allow(dead_code)]
555    pub grid_major_width_px: Option<f32>,
556    #[allow(dead_code)]
557    pub node_corner_radius_px: Option<f32>,
558    #[allow(dead_code)]
559    pub node_header_height_px: Option<f32>,
560    #[allow(dead_code)]
561    pub pin_radius_px: Option<f32>,
562    #[allow(dead_code)]
563    pub wire_width_px: Option<f32>,
564}
565
566#[derive(Debug, Clone, Deserialize)]
567pub struct PaintOnlyTokensV1 {
568    pub canvas: CanvasTokensV1,
569    pub grid: GridTokensV1,
570    #[allow(dead_code)]
571    pub text: TextTokensV1,
572    pub node: NodeTokensV1,
573    pub port: PortThemeTokensV1,
574    pub wire: WireTokensV1,
575    pub states: StateTokensV1,
576}
577
578#[derive(Debug, Clone, Copy, Deserialize)]
579pub struct CanvasTokensV1 {
580    pub background: RgbaV1,
581}
582
583#[derive(Debug, Clone, Copy, Deserialize)]
584pub struct GridTokensV1 {
585    pub minor_color: RgbaV1,
586    pub major_color: RgbaV1,
587}
588
589#[derive(Debug, Clone, Copy, Deserialize)]
590pub struct TextTokensV1 {
591    #[allow(dead_code)]
592    pub primary: RgbaV1,
593    #[allow(dead_code)]
594    pub muted: RgbaV1,
595}
596
597#[derive(Debug, Clone, Deserialize)]
598pub struct NodeTokensV1 {
599    pub body_background: RgbaV1,
600    pub border: RgbaV1,
601    pub border_selected: RgbaV1,
602    pub header_background_default: RgbaV1,
603    pub header_by_kind: HashMap<String, RgbaV1>,
604    pub title_text: RgbaV1,
605    pub ring_selected: NodeRingTokensV1,
606    pub ring_focused: NodeRingTokensV1,
607}
608
609#[derive(Debug, Clone, Deserialize)]
610pub struct PortThemeTokensV1 {
611    pub by_port_kind: PortKindTokensV1,
612}
613
614#[derive(Debug, Clone, Deserialize)]
615pub struct PortKindTokensV1 {
616    pub data: PortTokensV1,
617    pub exec: PortTokensV1,
618    #[allow(dead_code)]
619    pub preview: PortTokensV1,
620}
621
622#[derive(Debug, Clone, Copy, Deserialize)]
623pub struct PortTokensV1 {
624    pub fill: RgbaV1,
625    pub stroke: RgbaV1,
626    pub stroke_width_px: f32,
627    pub inner_scale: f32,
628    pub shape: PortShapeKindV1,
629}
630
631#[derive(Debug, Clone, Copy, PartialEq, Eq)]
632pub enum PortShapeKindV1 {
633    Circle,
634    Diamond,
635    Triangle,
636}
637
638impl<'de> Deserialize<'de> for PortShapeKindV1 {
639    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
640    where
641        D: serde::Deserializer<'de>,
642    {
643        let s = String::deserialize(deserializer)?;
644        match s.as_str() {
645            "Circle" => Ok(PortShapeKindV1::Circle),
646            "Diamond" => Ok(PortShapeKindV1::Diamond),
647            "Triangle" => Ok(PortShapeKindV1::Triangle),
648            _ => Ok(PortShapeKindV1::Circle),
649        }
650    }
651}
652
653#[derive(Debug, Clone, Deserialize)]
654pub struct WireTokensV1 {
655    pub data_color: RgbaV1,
656    pub exec_color: RgbaV1,
657    pub preview_color: RgbaV1,
658    pub dash_preview: DashPatternTokensV1,
659    pub dash_invalid: DashPatternTokensV1,
660    pub dash_emphasis: DashPatternTokensV1,
661    #[serde(default)]
662    pub highlight_selected: Option<WireHighlightTokensV1>,
663    #[serde(default)]
664    pub highlight_hovered: Option<WireHighlightTokensV1>,
665    #[serde(default)]
666    pub marker_exec_end: Option<EdgeMarkerTokensV1>,
667    #[serde(default)]
668    pub marker_exec_start: Option<EdgeMarkerTokensV1>,
669    #[serde(default)]
670    pub marker_data_end: Option<EdgeMarkerTokensV1>,
671    #[serde(default)]
672    pub marker_data_start: Option<EdgeMarkerTokensV1>,
673    #[serde(default)]
674    pub marker_size_mul_selected: Option<f32>,
675    #[serde(default)]
676    pub marker_size_mul_hovered: Option<f32>,
677}
678
679#[derive(Debug, Clone, Copy, Deserialize)]
680pub struct EdgeMarkerTokensV1 {
681    pub kind: EdgeMarkerKindTokensV1,
682    pub size_px: f32,
683}
684
685#[derive(Debug, Clone, Copy, PartialEq, Eq)]
686pub enum EdgeMarkerKindTokensV1 {
687    Arrow,
688}
689
690impl<'de> Deserialize<'de> for EdgeMarkerKindTokensV1 {
691    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
692    where
693        D: serde::Deserializer<'de>,
694    {
695        let s = String::deserialize(deserializer)?;
696        match s.as_str() {
697            "Arrow" => Ok(EdgeMarkerKindTokensV1::Arrow),
698            _ => Ok(EdgeMarkerKindTokensV1::Arrow),
699        }
700    }
701}
702
703#[derive(Debug, Clone, Copy, Deserialize)]
704pub struct WireHighlightTokensV1 {
705    pub width_mul: f32,
706    pub alpha_mul: f32,
707    #[serde(default)]
708    pub color: Option<RgbaV1>,
709}
710
711#[derive(Debug, Clone, Deserialize)]
712pub struct StateTokensV1 {
713    pub hover: StateColorV1,
714    pub invalid: StateColorV1,
715    pub convertible: StateColorV1,
716    #[allow(dead_code)]
717    pub disabled: DisabledStateV1,
718}
719
720#[derive(Debug, Clone, Copy, Deserialize)]
721pub struct StateColorV1 {
722    pub color: RgbaV1,
723}
724
725#[derive(Debug, Clone, Copy, Deserialize)]
726pub struct DisabledStateV1 {
727    #[allow(dead_code)]
728    pub alpha_mul: f32,
729}