Skip to main content

henad_core/
params.rs

1//! Parameter descriptors, the values they describe, and the [`ParamStore`] a running state keeps the
2//! values in.
3
4/// A parameter a model declares.
5#[derive(Debug, Clone)]
6pub struct ParamDescriptor {
7    /// Stable name that `--set` and a spec file match on, and that results record.
8    pub id: &'static str,
9    /// Name the Parameters panel shows.
10    pub label: &'static str,
11    /// Type, bounds and default.
12    pub kind: ParamKind,
13    /// Whether an edit reaches the running simulation or waits for a rebuild.
14    pub apply: ParamApply,
15    /// Display format of the value.
16    pub format: ParamFormat,
17}
18
19/// Display format of a parameter value. The stored value is the same either way.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21pub enum ParamFormat {
22    /// The value as stored.
23    #[default]
24    Plain,
25    /// A fraction in `0..=1` displayed as a percentage, so `0.025` shows as `2.5%`.
26    Percent,
27}
28
29/// Point at which an edit to a parameter reaches the simulation.
30///
31/// A state's `set_param` rejects an edit to a parameter declared `OnReload`, and the UI reads the same
32/// flag to say so before anything is sent.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
34pub enum ParamApply {
35    /// The running state picks the new value up on its next tick.
36    #[default]
37    Live,
38    /// The state reads the value only while it is built, and an edit needs a rebuild.
39    OnReload,
40}
41
42impl ParamDescriptor {
43    /// Marks the parameter [`ParamApply::OnReload`]. A parameter is live by default.
44    pub fn on_reload(mut self) -> Self {
45        self.apply = ParamApply::OnReload;
46        self
47    }
48
49    /// Marks the parameter as a fraction displayed as a percentage, [`ParamFormat::Percent`].
50    pub fn percent(mut self) -> Self {
51        self.format = ParamFormat::Percent;
52        self
53    }
54
55    /// Returns whether an edit reaches a running state.
56    pub fn is_live(&self) -> bool {
57        self.apply == ParamApply::Live
58    }
59}
60
61/// Type of a parameter, with its bounds and default.
62#[derive(Debug, Clone)]
63pub enum ParamKind {
64    /// A float, drawn as a slider.
65    F32 {
66        /// Lowest value.
67        min: f32,
68        /// Highest value.
69        max: f32,
70        /// Default value.
71        default: f32,
72        /// Slider step, or `None` for a continuous slider.
73        step: Option<f32>,
74    },
75    /// An unsigned integer, drawn as a slider.
76    U32 {
77        /// Lowest value.
78        min: u32,
79        /// Highest value.
80        max: u32,
81        /// Default value.
82        default: u32,
83    },
84    /// A switch, drawn as a checkbox.
85    Bool {
86        /// Default value.
87        default: bool,
88    },
89    /// One of a list of named options, drawn as a dropdown.
90    Choice {
91        /// Option names, in index order.
92        options: &'static [&'static str],
93        /// Index of the default option.
94        default: usize,
95    },
96}
97
98/// Value of one parameter.
99#[derive(Debug, Clone, PartialEq)]
100pub enum ParamValue {
101    /// Value of an `F32` parameter.
102    F32(f32),
103    /// Value of a `U32` parameter.
104    U32(u32),
105    /// Value of a `Bool` parameter.
106    Bool(bool),
107    /// Index of the option chosen for a `Choice` parameter.
108    Choice(usize),
109}
110
111// The set of conversions is closed. An unsuffixed literal infers `u32` or `f32` only while exactly one integer and
112// one float conversion exist, and a fourth conversion such as `From<usize>` stops `set("grid_width", 256)` compiling.
113impl From<f32> for ParamValue {
114    fn from(value: f32) -> Self {
115        Self::F32(value)
116    }
117}
118
119impl From<u32> for ParamValue {
120    fn from(value: u32) -> Self {
121        Self::U32(value)
122    }
123}
124
125impl From<bool> for ParamValue {
126    fn from(value: bool) -> Self {
127        Self::Bool(value)
128    }
129}
130
131impl ParamKind {
132    /// Returns the default as a value.
133    pub fn default_value(&self) -> ParamValue {
134        match *self {
135            Self::F32 { default, .. } => ParamValue::F32(default),
136            Self::U32 { default, .. } => ParamValue::U32(default),
137            Self::Bool { default } => ParamValue::Bool(default),
138            Self::Choice { default, .. } => ParamValue::Choice(default),
139        }
140    }
141}
142
143/// The values a running state holds, with the live/reload decision cached from the descriptors.
144///
145/// The decision is cached, so `set_param` can reject a reload-only index without rebuilding the
146/// descriptor list every time a slider moves.
147#[derive(Debug)]
148pub struct ParamStore {
149    values: Vec<ParamValue>,
150    live: Vec<bool>,
151}
152
153impl ParamStore {
154    /// Creates a store holding `values`, with the apply mode of each descriptor in `descriptors`.
155    pub fn new(descriptors: &[ParamDescriptor], values: &[ParamValue]) -> Self {
156        Self {
157            values: values.to_vec(),
158            live: descriptors.iter().map(ParamDescriptor::is_live).collect(),
159        }
160    }
161
162    /// Current values, in descriptor order.
163    pub fn values(&self) -> &[ParamValue] {
164        &self.values
165    }
166
167    /// Sets parameter `index` to `value` if the parameter is live, and returns whether the value was set.
168    pub fn set(&mut self, index: usize, value: &ParamValue) -> bool {
169        if self.live.get(index) == Some(&true) && index < self.values.len() {
170            self.values[index] = value.clone();
171            true
172        } else {
173            false
174        }
175    }
176}
177
178/// Declares a model's parameters and their indices in one place.
179///
180/// The index is the declaration's position, so it is derived rather than written down. Invoke it at
181/// module scope, next to the impl that forwards `param_descriptors` to `descriptors`.
182///
183/// ```ignore
184/// params! {
185///     /// Reason for this default, if it needs saying.
186///     const DENSITY = f32_param("density", "Initial Density", 0.3, 0.0, 1.0, Some(0.01));
187/// }
188/// ```
189#[macro_export]
190macro_rules! params {
191    ($($(#[$meta:meta])* $vis:vis const $name:ident = $descriptor:expr;)+) => {
192        $crate::__indices!(0usize, $([$(#[$meta])* $vis $name],)+);
193
194        /// This model's own parameters, in index order.
195        fn descriptors() -> ::std::vec::Vec<$crate::__macro_support::ParamDescriptor> {
196            ::std::vec![$($descriptor),+]
197        }
198    };
199}
200
201/// Assigns each name its position, for [`params`] and [`crate::buffers`].
202#[doc(hidden)]
203#[macro_export]
204macro_rules! __indices {
205    ($i:expr,) => {};
206    ($i:expr, [$(#[$meta:meta])* $vis:vis $first:ident], $($rest:tt,)*) => {
207        $(#[$meta])*
208        $vis const $first: usize = $i;
209        $crate::__indices!($i + 1usize, $($rest,)*);
210    };
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    fn descriptors() -> Vec<ParamDescriptor> {
218        vec![
219            ParamDescriptor {
220                id: "live",
221                label: "Live",
222                kind: ParamKind::F32 {
223                    min: 0.0,
224                    max: 1.0,
225                    default: 0.5,
226                    step: None,
227                },
228                apply: ParamApply::Live,
229                format: ParamFormat::Plain,
230            },
231            ParamDescriptor {
232                id: "reload",
233                label: "Reload",
234                kind: ParamKind::U32 {
235                    min: 0,
236                    max: 10,
237                    default: 1,
238                },
239                apply: ParamApply::OnReload,
240                format: ParamFormat::Plain,
241            },
242        ]
243    }
244
245    #[test]
246    fn store_accepts_live_edits_and_rejects_reload_ones() {
247        let descs = descriptors();
248        let mut store = ParamStore::new(&descs, &[ParamValue::F32(0.5), ParamValue::U32(1)]);
249
250        assert!(store.set(0, &ParamValue::F32(0.9)));
251        assert_eq!(store.values()[0], ParamValue::F32(0.9));
252
253        assert!(!store.set(1, &ParamValue::U32(7)));
254        assert_eq!(store.values()[1], ParamValue::U32(1), "a rejected edit must not land");
255
256        assert!(!store.set(9, &ParamValue::F32(0.0)), "out of range index");
257    }
258
259    /// The macro has to expand in function scope as well as module scope (C-ANYWHERE), and an
260    /// entry has to take attributes (C-MACRO-ATTR).
261    #[test]
262    fn params_macro_expands_in_function_scope() {
263        crate::params! {
264            const FIRST = crate::helpers::f32_param("first", "First", 0.0, 0.0, 1.0, None);
265            /// An entry can carry a doc comment.
266            const SECOND = crate::helpers::f32_param("second", "Second", 1.0, 0.0, 1.0, None);
267        }
268        assert_eq!((FIRST, SECOND), (0, 1), "indices follow declaration order");
269        assert_eq!(descriptors().len(), 2);
270    }
271
272    #[test]
273    fn param_kind_defaults() {
274        let f = ParamKind::F32 {
275            min: 0.0,
276            max: 1.0,
277            default: 0.5,
278            step: None,
279        };
280        assert_eq!(f.default_value(), ParamValue::F32(0.5));
281
282        let u = ParamKind::U32 {
283            min: 0,
284            max: 100,
285            default: 42,
286        };
287        assert_eq!(u.default_value(), ParamValue::U32(42));
288
289        let b = ParamKind::Bool { default: true };
290        assert_eq!(b.default_value(), ParamValue::Bool(true));
291
292        let c = ParamKind::Choice {
293            options: &["a", "b"],
294            default: 1,
295        };
296        assert_eq!(c.default_value(), ParamValue::Choice(1));
297    }
298}