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}