Skip to main content

truce_iced/widgets/
meter.rs

1//! Level meter widget rendered via iced Canvas (display-only).
2
3use std::fmt::Debug;
4use std::marker::PhantomData;
5
6use iced::widget::Canvas;
7use iced::widget::canvas::{self, Frame, Geometry, Path};
8use iced::{Element, Length, Point, Rectangle, Renderer, Size, Theme, mouse};
9
10use truce_core::meter_display;
11use truce_params::Params;
12
13use crate::param_cache::ParamCache;
14use crate::param_message::Message;
15use crate::theme;
16
17/// Builder for a multi-channel level meter.
18pub struct MeterWidget<'a, M> {
19    values: Vec<f32>,
20    label: Option<&'a str>,
21    width: f32,
22    height: f32,
23    font: iced::Font,
24    _phantom: PhantomData<M>,
25}
26
27impl<'a, M: Clone + Debug + 'static> MeterWidget<'a, M> {
28    #[must_use]
29    pub fn new(ids: &[u32], params: &'a ParamCache<impl Params>) -> Self {
30        let values: Vec<f32> = ids.iter().map(|&id| params.meter(id)).collect();
31        Self {
32            values,
33            label: None,
34            width: 16.0,
35            height: 80.0,
36            font: params.font(),
37            _phantom: PhantomData,
38        }
39    }
40
41    #[must_use]
42    pub fn label(mut self, label: &'a str) -> Self {
43        self.label = Some(label);
44        self
45    }
46
47    #[must_use]
48    pub fn size(mut self, width: f32, height: f32) -> Self {
49        self.width = width;
50        self.height = height;
51        self
52    }
53
54    #[must_use]
55    pub fn font(mut self, font: iced::Font) -> Self {
56        self.font = font;
57        self
58    }
59
60    #[must_use]
61    pub fn into_element(self) -> Element<'a, Message<M>> {
62        let total_h = self.height;
63        // `label` and `font` are accepted on the builder for API symmetry
64        // with knob/dropdown but the meter currently renders bars only -
65        // text labels are drawn by the surrounding layout, not the canvas.
66        let _ = (self.label, self.font);
67        let program = MeterProgram {
68            values: self.values,
69            meter_height: self.height,
70        };
71
72        Canvas::new(program)
73            .width(Length::Fixed(self.width))
74            .height(Length::Fixed(total_h))
75            .into()
76    }
77}
78
79impl<'a, M: Clone + Debug + 'static> From<MeterWidget<'a, M>> for Element<'a, Message<M>> {
80    fn from(m: MeterWidget<'a, M>) -> Self {
81        m.into_element()
82    }
83}
84
85// Canvas program
86
87struct MeterProgram {
88    values: Vec<f32>,
89    meter_height: f32,
90}
91
92impl<M: Clone + Debug + 'static> canvas::Program<Message<M>> for MeterProgram {
93    type State = ();
94
95    // `usize as f32` for channel-count layout; channel counts are
96    // tiny (typically <= 64).
97    #[allow(clippy::cast_precision_loss)]
98    fn draw(
99        &self,
100        _state: &Self::State,
101        renderer: &Renderer,
102        _theme: &Theme,
103        bounds: Rectangle,
104        _cursor: mouse::Cursor,
105    ) -> Vec<Geometry> {
106        let mut frame = Frame::new(renderer, bounds.size());
107        let channels = self.values.len().max(1);
108        let bar_gap = 2.0;
109        let total_gap = bar_gap * (channels as f32 - 1.0).max(0.0);
110        let bar_w = ((bounds.width - total_gap) / channels as f32).max(4.0);
111
112        for (i, &value) in self.values.iter().enumerate() {
113            let x = i as f32 * (bar_w + bar_gap);
114            let display = meter_display(value);
115            let fill_h = (display * self.meter_height).clamp(0.0, self.meter_height);
116
117            // Background
118            let bg = Path::rectangle(Point::new(x, 0.0), Size::new(bar_w, self.meter_height));
119            frame.fill(&bg, iced::Color::from_rgb(0.165, 0.165, 0.188));
120
121            // Fill (blue, red when clipping)
122            if fill_h > 0.0 {
123                let color = if display > 0.95 {
124                    theme::METER_CLIP
125                } else {
126                    theme::KNOB_FILL
127                };
128                let bar = Path::rectangle(
129                    Point::new(x, self.meter_height - fill_h),
130                    Size::new(bar_w, fill_h),
131                );
132                frame.fill(&bar, color);
133            }
134        }
135
136        vec![frame.into_geometry()]
137    }
138
139    fn update(
140        &self,
141        _state: &mut Self::State,
142        _event: &canvas::Event,
143        _bounds: Rectangle,
144        _cursor: mouse::Cursor,
145    ) -> Option<canvas::Action<Message<M>>> {
146        // Display-only, no interaction
147        None
148    }
149}