styx_core/
controls.rs

1/// Strongly typed control identifier.
2///
3/// # Example
4/// ```rust
5/// use styx_core::prelude::ControlId;
6///
7/// let id = ControlId(42);
8/// assert_eq!(id.0, 42);
9/// ```
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
13#[cfg_attr(feature = "schema", schema(value_type = u32))]
14pub struct ControlId(pub u32);
15
16/// Access permissions for a control.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
20pub enum Access {
21    ReadOnly,
22    ReadWrite,
23}
24
25/// Simplified control metadata.
26///
27/// # Example
28/// ```rust
29/// use styx_core::prelude::{Access, ControlId, ControlKind, ControlMeta, ControlValue};
30///
31/// let meta = ControlMeta {
32///     id: ControlId(1),
33///     name: "gain".into(),
34///     kind: ControlKind::Uint,
35///     access: Access::ReadWrite,
36///     min: ControlValue::Uint(0),
37///     max: ControlValue::Uint(255),
38///     default: ControlValue::Uint(16),
39///     step: Some(ControlValue::Uint(1)),
40///     menu: None,
41/// };
42/// assert!(meta.validate(&ControlValue::Uint(32)));
43/// ```
44#[derive(Debug, Clone)]
45#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
46#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
47pub struct ControlMeta {
48    /// Stable identifier.
49    pub id: ControlId,
50    /// Human-readable name.
51    pub name: String,
52    /// Kind of control/value type.
53    pub kind: ControlKind,
54    /// Access permissions.
55    pub access: Access,
56    /// Minimum accepted value.
57    pub min: ControlValue,
58    /// Maximum accepted value.
59    pub max: ControlValue,
60    /// Default value.
61    pub default: ControlValue,
62    /// Optional step size for ranged controls.
63    #[cfg_attr(feature = "serde", serde(default))]
64    pub step: Option<ControlValue>,
65    /// Optional enumerated menu entries (for menu controls).
66    #[cfg_attr(feature = "serde", serde(default))]
67    pub menu: Option<Vec<String>>,
68}
69
70/// Control kind/type metadata.
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
73#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
74pub enum ControlKind {
75    None,
76    Bool,
77    Int,
78    Uint,
79    Float,
80    Menu,
81    IntMenu,
82    Unknown,
83}
84
85/// Control value variants with minimal footprint.
86///
87/// # Example
88/// ```rust
89/// use styx_core::prelude::ControlValue;
90///
91/// let v = ControlValue::Bool(true);
92/// assert_eq!(v, ControlValue::Bool(true));
93/// ```
94#[derive(Debug, Clone, PartialEq)]
95#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
96pub enum ControlValue {
97    /// No value.
98    None,
99    /// Boolean value.
100    Bool(bool),
101    /// Signed integer.
102    Int(i32),
103    /// Unsigned integer.
104    Uint(u32),
105    /// Floating-point value.
106    Float(f32),
107}
108
109#[cfg(feature = "serde")]
110impl serde::Serialize for ControlValue {
111    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
112    where
113        S: serde::Serializer,
114    {
115        if serializer.is_human_readable() {
116            #[derive(serde::Serialize)]
117            #[serde(tag = "kind", content = "value", rename_all = "snake_case")]
118            enum Human {
119                None,
120                Bool(bool),
121                Int(i32),
122                Uint(u32),
123                Float(f32),
124            }
125            let h = match self {
126                ControlValue::None => Human::None,
127                ControlValue::Bool(v) => Human::Bool(*v),
128                ControlValue::Int(v) => Human::Int(*v),
129                ControlValue::Uint(v) => Human::Uint(*v),
130                ControlValue::Float(v) => Human::Float(*v),
131            };
132            h.serialize(serializer)
133        } else {
134            #[derive(serde::Serialize)]
135            enum Binary {
136                None,
137                Bool(bool),
138                Int(i32),
139                Uint(u32),
140                Float(f32),
141            }
142            let b = match self {
143                ControlValue::None => Binary::None,
144                ControlValue::Bool(v) => Binary::Bool(*v),
145                ControlValue::Int(v) => Binary::Int(*v),
146                ControlValue::Uint(v) => Binary::Uint(*v),
147                ControlValue::Float(v) => Binary::Float(*v),
148            };
149            b.serialize(serializer)
150        }
151    }
152}
153
154#[cfg(feature = "serde")]
155impl<'de> serde::Deserialize<'de> for ControlValue {
156    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
157    where
158        D: serde::Deserializer<'de>,
159    {
160        if deserializer.is_human_readable() {
161            #[derive(serde::Deserialize)]
162            #[serde(tag = "kind", content = "value", rename_all = "snake_case")]
163            enum Human {
164                None,
165                Bool(bool),
166                Int(i32),
167                Uint(u32),
168                Float(f32),
169            }
170            let h = Human::deserialize(deserializer)?;
171            Ok(match h {
172                Human::None => ControlValue::None,
173                Human::Bool(v) => ControlValue::Bool(v),
174                Human::Int(v) => ControlValue::Int(v),
175                Human::Uint(v) => ControlValue::Uint(v),
176                Human::Float(v) => ControlValue::Float(v),
177            })
178        } else {
179            #[derive(serde::Deserialize)]
180            enum Binary {
181                None,
182                Bool(bool),
183                Int(i32),
184                Uint(u32),
185                Float(f32),
186            }
187            let b = Binary::deserialize(deserializer)?;
188            Ok(match b {
189                Binary::None => ControlValue::None,
190                Binary::Bool(v) => ControlValue::Bool(v),
191                Binary::Int(v) => ControlValue::Int(v),
192                Binary::Uint(v) => ControlValue::Uint(v),
193                Binary::Float(v) => ControlValue::Float(v),
194            })
195        }
196    }
197}
198
199impl ControlMeta {
200    /// Basic range validation respecting the variant.
201    ///
202    /// # Example
203    /// ```rust
204    /// use styx_core::prelude::{Access, ControlId, ControlKind, ControlMeta, ControlValue};
205    ///
206    /// let meta = ControlMeta {
207    ///     id: ControlId(1),
208    ///     name: "brightness".into(),
209    ///     kind: ControlKind::Int,
210    ///     access: Access::ReadWrite,
211    ///     min: ControlValue::Int(-10),
212    ///     max: ControlValue::Int(10),
213    ///     default: ControlValue::Int(0),
214    ///     step: Some(ControlValue::Int(1)),
215    ///     menu: None,
216    /// };
217    /// assert!(meta.validate(&ControlValue::Int(5)));
218    /// ```
219    pub fn validate(&self, candidate: &ControlValue) -> bool {
220        if let Some(menu) = &self.menu {
221            // For menus, only None/Uint within menu length or Int for IntMenu.
222            if let ControlValue::Uint(idx) = candidate {
223                return (*idx as usize) < menu.len();
224            }
225            if let ControlValue::Int(idx) = candidate {
226                return (*idx >= 0) && ((*idx as usize) < menu.len());
227            }
228        }
229
230        match (candidate, &self.min, &self.max) {
231            (ControlValue::Bool(v), ControlValue::Bool(min), ControlValue::Bool(max)) => {
232                let v = *v as u8;
233                let min = *min as u8;
234                let max = *max as u8;
235                v >= min && v <= max
236            }
237            (ControlValue::Int(v), ControlValue::Int(min), ControlValue::Int(max)) => {
238                let within = v >= min && v <= max;
239                if !within {
240                    return false;
241                }
242                if let Some(ControlValue::Int(step)) = &self.step
243                    && *step > 0
244                {
245                    return ((v - min) % step) == 0;
246                }
247                true
248            }
249            (ControlValue::Uint(v), ControlValue::Uint(min), ControlValue::Uint(max)) => {
250                let within = v >= min && v <= max;
251                if !within {
252                    return false;
253                }
254                if let Some(ControlValue::Uint(step)) = &self.step
255                    && *step > 0
256                {
257                    return ((v - min) % step) == 0;
258                }
259                true
260            }
261            (ControlValue::Float(v), ControlValue::Float(min), ControlValue::Float(max)) => {
262                v >= min && v <= max
263            }
264            (ControlValue::None, ControlValue::None, ControlValue::None) => true,
265            _ => false,
266        }
267    }
268}