Skip to main content

concinnity_world/schema/
option_select.rs

1//! Settings-row option-select schema.
2
3/// A settings row that cycles through a fixed set of values on click.
4///
5/// `OptionSelect` is a build-time shorthand for one row of a settings menu: a
6/// left-aligned name, a right-aligned current value, and a clickable region
7/// that advances the value. It expands into a [TextLabel](#textlabel) for the
8/// name, a `TextLabel` for the value, and a [HitRegion](#hitregion) that fires a
9/// `"setting:<setting>:next"` action.
10///
11/// The `setting` field names an engine setting the runtime knows how to read,
12/// cycle, and apply (e.g. `"vsync"`); its option list lives in the engine, not
13/// here. The value label shows a placeholder at build time and is corrected to
14/// the live value when the world starts.
15///
16/// Generated names are prefixed with this asset's `name` (`<name>_label`,
17/// `<name>_value`, `<name>_btn`), so they never clash with hand-authored assets.
18///
19/// ```rust
20/// # use concinnity_world::registry::build_only::OptionSelect;
21/// OptionSelect {
22///     setting: "vsync".into(),
23///     label: "Vsync".into(),
24///     ..Default::default()
25/// };
26/// ```
27#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
28#[serde(default)]
29pub struct OptionSelect {
30    /// Engine setting this row controls (e.g. `"vsync"`). Must be a setting the
31    /// runtime recognises; an unknown key renders but does nothing on click.
32    pub setting: String,
33    /// Display name shown at the left of the row.
34    pub label: String,
35    /// Left edge of the row in window pixels.
36    pub x: f32,
37    /// Top edge of the row in window pixels.
38    pub y: f32,
39    /// Row width in window pixels (name sits at the left, value at the right).
40    pub width: f32,
41    /// Row height in window pixels (the clickable region's height).
42    pub height: f32,
43    /// [Font](#font) for the row text. Empty uses the built-in font.
44    pub font: String,
45    /// Pixel size of the row text when it uses the built-in font (that is, when
46    /// `font` is empty). Ignored when `font` names a [Font](#font), which
47    /// carries its own size.
48    pub font_px: f32,
49    /// Linear-space RGB color of the name text.
50    pub text_color: [f32; 3],
51    /// Linear-space RGB color of the value text.
52    pub value_color: [f32; 3],
53    /// Scale applied to the row text.
54    pub text_scale: f32,
55    /// RGB color of the value text while the row is hovered.
56    pub hover_color: [f32; 3],
57    /// Scale of the value text while the row is hovered.
58    pub hover_scale: f32,
59    /// Width in pixels of the `<` previous-value click region. The `>`
60    /// next-value region spans the rest of the row's right half (the value
61    /// sits inside it), so a click on the value advances to the next option.
62    pub stepper_width: f32,
63}
64
65impl Default for OptionSelect {
66    fn default() -> Self {
67        Self {
68            setting: String::new(),
69            label: String::new(),
70            x: 0.0,
71            y: 0.0,
72            width: 360.0,
73            height: 48.0,
74            font: String::new(),
75            font_px: 48.0,
76            text_color: [0.85, 0.85, 0.85],
77            value_color: [0.85, 0.85, 0.85],
78            text_scale: 1.0,
79            hover_color: [1.0, 0.85, 0.3],
80            hover_scale: 1.08,
81            stepper_width: 40.0,
82        }
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn a_blank_row_is_a_settings_sized_control_bound_to_nothing() {
92        let o = OptionSelect::default();
93        assert!(o.setting.is_empty());
94        assert!(o.label.is_empty());
95        assert_eq!((o.width, o.height), (360.0, 48.0));
96        assert_eq!(o.stepper_width, 40.0);
97        assert_eq!(o.text_scale, 1.0);
98        assert_eq!(o.hover_scale, 1.08);
99        assert_eq!(o.hover_color, [1.0, 0.85, 0.3]);
100    }
101
102    #[test]
103    fn an_authored_row_parses_and_round_trips_through_postcard() {
104        let o: OptionSelect = serde_json::from_str(
105            r#"{"setting":"quality","label":"Quality","x":40,"y":120,"font":"body",
106                "font_px":32,"value_color":[1,1,1],"stepper_width":56}"#,
107        )
108        .unwrap();
109        assert_eq!(o.setting, "quality");
110        assert_eq!((o.x, o.y), (40.0, 120.0));
111
112        let bytes = postcard::to_allocvec(&o).unwrap();
113        let back: OptionSelect = postcard::from_bytes(&bytes).unwrap();
114        assert_eq!(back.label, "Quality");
115        assert_eq!(back.font, "body");
116        assert_eq!(back.font_px, 32.0);
117        assert_eq!(back.value_color, [1.0, 1.0, 1.0]);
118        assert_eq!(back.stepper_width, 56.0);
119        // Unmentioned styling keeps the schema defaults.
120        assert_eq!(back.text_color, [0.85, 0.85, 0.85]);
121    }
122}