truce_iced/widgets/
meter.rs1use 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
17pub 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 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
85struct 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 #[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 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 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 None
148 }
149}