Skip to main content

hblank_core/
control.rs

1#![allow(
2    clippy::cast_lossless,
3    clippy::cast_possible_truncation,
4    clippy::cast_precision_loss,
5    clippy::cast_sign_loss,
6    clippy::float_cmp
7)]
8
9use std::any::Any;
10
11use serde::{Deserialize, Serialize};
12use thiserror::Error;
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum TextMode {
16    SingleLine,
17    Multiline,
18}
19
20#[derive(Clone, Copy, Debug, PartialEq)]
21pub struct NumberConstraints {
22    pub min: Option<f64>,
23    pub max: Option<f64>,
24    pub step: f64,
25}
26
27impl Default for NumberConstraints {
28    fn default() -> Self {
29        Self {
30            min: None,
31            max: None,
32            step: 1.0,
33        }
34    }
35}
36
37#[derive(Clone, Copy, Debug, PartialEq)]
38pub enum ControlKind {
39    Boolean,
40    Text { mode: TextMode },
41    Number { constraints: NumberConstraints },
42    Enum { options: &'static [&'static str] },
43}
44
45impl ControlKind {
46    #[must_use]
47    pub const fn name(self) -> &'static str {
48        match self {
49            Self::Boolean => "boolean",
50            Self::Text { .. } => "text",
51            Self::Number { .. } => "number",
52            Self::Enum { .. } => "enum",
53        }
54    }
55
56    #[doc(hidden)]
57    #[must_use]
58    pub const fn multiline(self) -> Self {
59        match self {
60            Self::Text { .. } => Self::Text {
61                mode: TextMode::Multiline,
62            },
63            _ => panic!("multiline is only valid for text controls"),
64        }
65    }
66
67    #[doc(hidden)]
68    #[must_use]
69    pub const fn constrained(self, constraints: NumberConstraints) -> Self {
70        match self {
71            Self::Number { .. } => Self::Number { constraints },
72            _ => panic!("min, max, and step are only valid for numeric controls"),
73        }
74    }
75}
76
77#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
78pub enum ControlValue {
79    Boolean(bool),
80    Text(String),
81    Number(f64),
82    Enum(String),
83}
84
85impl ControlValue {
86    #[must_use]
87    pub const fn kind_name(&self) -> &'static str {
88        match self {
89            Self::Boolean(_) => "boolean",
90            Self::Text(_) => "text",
91            Self::Number(_) => "number",
92            Self::Enum(_) => "enum",
93        }
94    }
95}
96
97#[derive(Clone, Copy, Debug, PartialEq)]
98pub struct ControlDefinition {
99    pub id: &'static str,
100    pub label: &'static str,
101    pub docs: &'static str,
102    pub kind: ControlKind,
103}
104
105impl ControlDefinition {
106    /// Validates metadata-level constraints before a typed field accepts a value.
107    ///
108    /// # Errors
109    /// Returns an error when a numeric value violates configured bounds or step.
110    pub fn validate(&self, value: &ControlValue) -> Result<(), ControlError> {
111        let (ControlKind::Number { constraints }, ControlValue::Number(value)) = (self.kind, value)
112        else {
113            return Ok(());
114        };
115        if let Some(min) = constraints.min
116            && *value < min
117        {
118            return Err(ControlError::BelowMinimum {
119                control: self.id,
120                min,
121                value: *value,
122            });
123        }
124        if let Some(max) = constraints.max
125            && *value > max
126        {
127            return Err(ControlError::AboveMaximum {
128                control: self.id,
129                max,
130                value: *value,
131            });
132        }
133        let origin = constraints.min.unwrap_or(0.0);
134        let quotient = (*value - origin) / constraints.step;
135        let tolerance = f64::EPSILON * 16.0 * quotient.abs().max(1.0);
136        if (quotient - quotient.round()).abs() > tolerance {
137            return Err(ControlError::StepMismatch {
138                control: self.id,
139                step: constraints.step,
140                value: *value,
141            });
142        }
143        Ok(())
144    }
145}
146
147#[derive(Debug, Error, PartialEq)]
148pub enum ControlError {
149    #[error("unknown control '{0}'")]
150    UnknownControl(String),
151    #[error("control '{control}' expects {expected}, received {actual}")]
152    TypeMismatch {
153        control: &'static str,
154        expected: &'static str,
155        actual: &'static str,
156    },
157    #[error("{value} is not a valid value for numeric control '{control}'")]
158    InvalidNumber { control: &'static str, value: f64 },
159    #[error("{value} is below the minimum {min} for numeric control '{control}'")]
160    BelowMinimum {
161        control: &'static str,
162        min: f64,
163        value: f64,
164    },
165    #[error("{value} is above the maximum {max} for numeric control '{control}'")]
166    AboveMaximum {
167        control: &'static str,
168        max: f64,
169        value: f64,
170    },
171    #[error("{value} does not align to step {step} for numeric control '{control}'")]
172    StepMismatch {
173        control: &'static str,
174        step: f64,
175        value: f64,
176    },
177    #[error("'{value}' is not a valid option for control '{control}'")]
178    InvalidOption {
179        control: &'static str,
180        value: String,
181    },
182}
183
184pub trait HblankProps: Any + Send {
185    fn definitions(&self) -> &'static [ControlDefinition];
186    fn control_value(&self, id: &str) -> Option<ControlValue>;
187    /// Replaces one control value by its stable field identifier.
188    ///
189    /// # Errors
190    /// Returns an error when the identifier, value kind, number, or enum option is invalid.
191    fn set_control(&mut self, id: &str, value: ControlValue) -> Result<(), ControlError>;
192    fn clone_box(&self) -> Box<dyn HblankProps>;
193    fn as_any(&self) -> &dyn Any;
194}
195
196impl Clone for Box<dyn HblankProps> {
197    fn clone(&self) -> Self {
198        self.clone_box()
199    }
200}
201
202/// Maps a project domain type onto one of Hblank's built-in control value types.
203///
204/// The adapter keeps domain conversion in project code while Hblank owns editor UI,
205/// validation, reset behavior, and session serialization.
206pub trait HblankControlAdapter<T> {
207    type Value: ControlField;
208
209    fn to_control(value: &T) -> Self::Value;
210    fn apply_control(value: &mut T, control: Self::Value);
211}
212
213#[doc(hidden)]
214pub trait ControlField: Sized {
215    const KIND: ControlKind;
216
217    fn to_control_value(&self) -> ControlValue;
218    fn set_control_value(
219        &mut self,
220        control: &'static str,
221        value: ControlValue,
222    ) -> Result<(), ControlError>;
223}
224
225impl ControlField for bool {
226    const KIND: ControlKind = ControlKind::Boolean;
227
228    fn to_control_value(&self) -> ControlValue {
229        ControlValue::Boolean(*self)
230    }
231
232    fn set_control_value(
233        &mut self,
234        control: &'static str,
235        value: ControlValue,
236    ) -> Result<(), ControlError> {
237        let ControlValue::Boolean(value) = value else {
238            return Err(ControlError::TypeMismatch {
239                control,
240                expected: Self::KIND.name(),
241                actual: value.kind_name(),
242            });
243        };
244        *self = value;
245        Ok(())
246    }
247}
248
249impl ControlField for String {
250    const KIND: ControlKind = ControlKind::Text {
251        mode: TextMode::SingleLine,
252    };
253
254    fn to_control_value(&self) -> ControlValue {
255        ControlValue::Text(self.clone())
256    }
257
258    fn set_control_value(
259        &mut self,
260        control: &'static str,
261        value: ControlValue,
262    ) -> Result<(), ControlError> {
263        let ControlValue::Text(value) = value else {
264            return Err(ControlError::TypeMismatch {
265                control,
266                expected: Self::KIND.name(),
267                actual: value.kind_name(),
268            });
269        };
270        *self = value;
271        Ok(())
272    }
273}
274
275macro_rules! numeric_control {
276    ($($type:ty),+ $(,)?) => {
277        $(
278            impl ControlField for $type {
279                const KIND: ControlKind = ControlKind::Number {
280                    constraints: NumberConstraints {
281                        min: None,
282                        max: None,
283                        step: 1.0,
284                    },
285                };
286
287                fn to_control_value(&self) -> ControlValue {
288                    ControlValue::Number(*self as f64)
289                }
290
291                fn set_control_value(
292                    &mut self,
293                    control: &'static str,
294                    value: ControlValue,
295                ) -> Result<(), ControlError> {
296                    let ControlValue::Number(value) = value else {
297                        return Err(ControlError::TypeMismatch {
298                            control,
299                            expected: Self::KIND.name(),
300                            actual: value.kind_name(),
301                        });
302                    };
303                    if !value.is_finite()
304                        || value < <$type>::MIN as f64
305                        || value > <$type>::MAX as f64
306                        || (value as $type) as f64 != value
307                    {
308                        return Err(ControlError::InvalidNumber { control, value });
309                    }
310                    *self = value as $type;
311                    Ok(())
312                }
313            }
314        )+
315    };
316}
317
318numeric_control!(i8, i16, i32, i64, isize, u8, u16, u32, u64, usize);
319
320macro_rules! floating_control {
321    ($($type:ty),+ $(,)?) => {
322        $(
323            impl ControlField for $type {
324                const KIND: ControlKind = ControlKind::Number {
325                    constraints: NumberConstraints {
326                        min: None,
327                        max: None,
328                        step: 1.0,
329                    },
330                };
331
332                fn to_control_value(&self) -> ControlValue {
333                    ControlValue::Number(f64::from(*self))
334                }
335
336                fn set_control_value(
337                    &mut self,
338                    control: &'static str,
339                    value: ControlValue,
340                ) -> Result<(), ControlError> {
341                    let ControlValue::Number(value) = value else {
342                        return Err(ControlError::TypeMismatch {
343                            control,
344                            expected: Self::KIND.name(),
345                            actual: value.kind_name(),
346                        });
347                    };
348                    if !value.is_finite() || value < <$type>::MIN as f64 || value > <$type>::MAX as f64 {
349                        return Err(ControlError::InvalidNumber { control, value });
350                    }
351                    *self = value as $type;
352                    Ok(())
353                }
354            }
355        )+
356    };
357}
358
359floating_control!(f32, f64);
360
361pub trait HblankEnum: Clone + Send + 'static {
362    const VARIANTS: &'static [&'static str];
363
364    fn variant_name(&self) -> &'static str;
365    fn from_variant_name(value: &str) -> Option<Self>;
366}
367
368impl<T: HblankEnum> ControlField for T {
369    const KIND: ControlKind = ControlKind::Enum {
370        options: T::VARIANTS,
371    };
372
373    fn to_control_value(&self) -> ControlValue {
374        ControlValue::Enum(self.variant_name().to_owned())
375    }
376
377    fn set_control_value(
378        &mut self,
379        control: &'static str,
380        value: ControlValue,
381    ) -> Result<(), ControlError> {
382        let ControlValue::Enum(value) = value else {
383            return Err(ControlError::TypeMismatch {
384                control,
385                expected: Self::KIND.name(),
386                actual: value.kind_name(),
387            });
388        };
389        let Some(next) = T::from_variant_name(&value) else {
390            return Err(ControlError::InvalidOption { control, value });
391        };
392        *self = next;
393        Ok(())
394    }
395}