Skip to main content

truce_iced/
param_cache.rs

1//! Cached parameter state for iced widgets.
2//!
3//! `ParamCache` reads parameter values from the atomic `Params` store
4//! once per tick (~60fps) and caches them as plain values that iced
5//! widgets can read without atomic loads on every frame. The cache is
6//! polled from `IcedProgram::update(Message::Tick)` against the
7//! `PluginContext` the editor was opened with.
8
9use std::collections::HashMap;
10use std::sync::Arc;
11
12use truce_core::editor::{PluginContext, PluginContextReadF64};
13use truce_params::Params;
14
15/// Cached parameter values for iced widget consumption.
16///
17/// Distinct from `PluginContext<P>`: that is the host-plugin protocol
18/// surface (live atomic reads, host gestures); this is a per-tick
19/// snapshot used inside `Canvas::draw` closures where iced doesn't
20/// allow side effects.
21pub struct ParamCache<P: Params + ?Sized> {
22    params: Arc<P>,
23    /// Param IDs (cached at construction so each `sync` doesn't reallocate
24    /// `Vec<ParamInfo>`). The set is fixed for the lifetime of the editor -
25    /// `param_infos()` returns the same list every call.
26    ids: Vec<u32>,
27    /// Cached normalized values, indexed by param ID.
28    values: HashMap<u32, f64>,
29    /// Cached formatted display strings.
30    labels: HashMap<u32, String>,
31    /// Meter values (0.0–1.0).
32    meters: HashMap<u32, f32>,
33    /// Font for canvas-drawn widget labels. Set via the editor's `with_font()`.
34    font: iced::Font,
35}
36
37impl<P: Params + ?Sized> ParamCache<P> {
38    /// Create a new `ParamCache`, populating initial values from the params.
39    pub fn new(params: Arc<P>) -> Self {
40        let infos = params.param_infos();
41        let ids: Vec<u32> = infos.iter().map(|i| i.id).collect();
42        let mut values = HashMap::with_capacity(ids.len());
43        let mut labels = HashMap::with_capacity(ids.len());
44        for info in &infos {
45            if let Some(v) = params.get_normalized(info.id) {
46                values.insert(info.id, v);
47            }
48            let plain = params.get_plain(info.id).unwrap_or(0.0);
49            if let Some(label) = params.format_value(info.id, plain) {
50                labels.insert(info.id, label);
51            }
52        }
53        Self {
54            params,
55            ids,
56            values,
57            labels,
58            meters: HashMap::new(),
59            font: iced::Font::DEFAULT,
60        }
61    }
62
63    /// Read a param's normalized value (0.0–1.0).
64    pub fn get(&self, id: impl Into<u32>) -> f64 {
65        self.values.get(&id.into()).copied().unwrap_or(0.0)
66    }
67
68    /// Read a param's plain value.
69    pub fn get_plain(&self, id: impl Into<u32>) -> f64 {
70        self.params.get_plain(id.into()).unwrap_or(0.0)
71    }
72
73    /// Read a param's formatted display string.
74    pub fn label(&self, id: impl Into<u32>) -> &str {
75        self.labels
76            .get(&id.into())
77            .map_or("", std::string::String::as_str)
78    }
79
80    /// Read a meter value (0.0–1.0).
81    pub fn meter(&self, id: impl Into<u32>) -> f32 {
82        self.meters.get(&id.into()).copied().unwrap_or(0.0)
83    }
84
85    /// The font set via the editor's `with_font()`, or `Font::DEFAULT`.
86    #[must_use]
87    pub fn font(&self) -> iced::Font {
88        self.font
89    }
90
91    /// Set the font (called by the editor runtime).
92    pub fn set_font(&mut self, font: iced::Font) {
93        self.font = font;
94    }
95
96    /// Access the underlying params (for info lookups).
97    #[must_use]
98    pub fn params(&self) -> &P {
99        &self.params
100    }
101
102    /// Poll all params from the editor context, return IDs that changed.
103    pub(crate) fn sync<Q: ?Sized>(&mut self, ctx: &PluginContext<Q>) -> Vec<u32> {
104        let mut changed = Vec::new();
105        for &id in &self.ids {
106            let new_val = ctx.get_param(id);
107            let old_val = self.values.get(&id).copied().unwrap_or(-1.0);
108            if (new_val - old_val).abs() > 1e-10 {
109                self.values.insert(id, new_val);
110                // Reuse the existing label slot's capacity instead of
111                // dropping it on every change. `entry().or_default()`
112                // returns the slot's `&mut String` (or inserts an
113                // empty one); `format_param_into` clears + writes.
114                // The bridge's default impl still allocates a
115                // temporary internally, but bridges can override for
116                // a fully alloc-free path. Either way the cache's
117                // own storage no longer churns.
118                let slot = self.labels.entry(id).or_default();
119                ctx.format_param_into(id, slot);
120                changed.push(id);
121            }
122        }
123        changed
124    }
125
126    /// Poll meter values from the editor context.
127    pub(crate) fn sync_meters<Q: ?Sized>(&mut self, ctx: &PluginContext<Q>, meter_ids: &[u32]) {
128        for &id in meter_ids {
129            self.meters.insert(id, ctx.get_meter(id));
130        }
131    }
132}