1use std::fmt::Debug;
4use std::marker::PhantomData;
5
6use iced::widget::Canvas;
7use iced::widget::canvas::{self, Event, Frame, Geometry, Path, Stroke, Text};
8use iced::{Color, Element, Length, Point, Rectangle, Renderer, Theme, alignment, mouse};
9
10use crate::param_cache::ParamCache;
11use crate::param_message::{Message, ParamMessage};
12use crate::theme;
13use truce_core::Float;
14use truce_params::Params;
15
16const TRACK_HEIGHT: f32 = 4.0;
17const THUMB_RADIUS: f32 = 6.0;
18
19pub struct SliderWidget<'a, M> {
21 id: u32,
22 value: f64,
23 display: String,
24 label: Option<&'a str>,
25 width: f32,
26 font: iced::Font,
27 _phantom: PhantomData<M>,
28}
29
30impl<'a, M: Clone + Debug + 'static> SliderWidget<'a, M> {
31 pub fn new(id: impl Into<u32>, params: &'a ParamCache<impl Params>) -> Self {
32 let id = id.into();
33 Self {
34 id,
35 value: params.get(id),
36 display: params.label(id).to_string(),
37 label: None,
38 width: 120.0,
39 font: params.font(),
40 _phantom: PhantomData,
41 }
42 }
43
44 #[must_use]
45 pub fn label(mut self, label: &'a str) -> Self {
46 self.label = Some(label);
47 self
48 }
49
50 #[must_use]
51 pub fn width(mut self, width: f32) -> Self {
52 self.width = width;
53 self
54 }
55
56 #[must_use]
57 pub fn font(mut self, font: iced::Font) -> Self {
58 self.font = font;
59 self
60 }
61
62 #[must_use]
63 pub fn into_element(self) -> Element<'a, Message<M>> {
64 let total_h = THUMB_RADIUS * 2.0 + 30.0;
65 let program = SliderProgram {
66 id: self.id,
67 value: f32::from_f64(self.value),
68 display: self.display,
69 label: self.label.unwrap_or("").to_string(),
70 font: self.font,
71 };
72
73 Canvas::new(program)
74 .width(Length::Fixed(self.width))
75 .height(Length::Fixed(total_h))
76 .into()
77 }
78}
79
80impl<'a, M: Clone + Debug + 'static> From<SliderWidget<'a, M>> for Element<'a, Message<M>> {
81 fn from(s: SliderWidget<'a, M>) -> Self {
82 s.into_element()
83 }
84}
85
86struct SliderProgram {
89 id: u32,
90 value: f32,
91 display: String,
92 label: String,
93 font: iced::Font,
94}
95
96#[derive(Default)]
97struct SliderState {
98 dragging: bool,
99 start_value: f32,
100 start_x: f32,
101}
102
103impl<M: Clone + Debug + 'static> canvas::Program<Message<M>> for SliderProgram {
104 type State = SliderState;
105
106 fn draw(
107 &self,
108 _state: &Self::State,
109 renderer: &Renderer,
110 _theme: &Theme,
111 bounds: Rectangle,
112 _cursor: mouse::Cursor,
113 ) -> Vec<Geometry> {
114 let mut frame = Frame::new(renderer, bounds.size());
115
116 let margin = THUMB_RADIUS;
117 let track_y = THUMB_RADIUS;
118 let track_left = margin;
119 let track_right = bounds.width - margin;
120 let track_width = track_right - track_left;
121
122 let track_bg = Path::line(
124 Point::new(track_left, track_y),
125 Point::new(track_right, track_y),
126 );
127 frame.stroke(
128 &track_bg,
129 Stroke::default()
130 .with_color(theme::KNOB_TRACK)
131 .with_width(TRACK_HEIGHT)
132 .with_line_cap(iced::widget::canvas::LineCap::Round),
133 );
134
135 let fill_x = track_left + self.value * track_width;
137 if self.value > 0.001 {
138 let track_fill =
139 Path::line(Point::new(track_left, track_y), Point::new(fill_x, track_y));
140 frame.stroke(
141 &track_fill,
142 Stroke::default()
143 .with_color(theme::KNOB_FILL)
144 .with_width(TRACK_HEIGHT)
145 .with_line_cap(iced::widget::canvas::LineCap::Round),
146 );
147 }
148
149 let thumb = Path::circle(Point::new(fill_x, track_y), THUMB_RADIUS);
151 frame.fill(&thumb, theme::KNOB_POINTER);
152
153 let text_y = THUMB_RADIUS * 2.0 + 4.0;
155 let cx = bounds.width / 2.0;
156 frame.fill_text(Text {
157 content: self.display.clone(),
158 position: Point::new(cx, text_y),
159 color: Color::from_rgb(0.90, 0.90, 0.92),
160 size: iced::Pixels(11.0),
161 align_x: alignment::Horizontal::Center.into(),
162 align_y: alignment::Vertical::Top,
163 font: self.font,
164 ..Text::default()
165 });
166
167 if !self.label.is_empty() {
169 let label_y = text_y + 14.0;
170 frame.fill_text(Text {
171 content: self.label.clone(),
172 position: Point::new(cx, label_y),
173 color: theme::TEXT_DIM,
174 size: iced::Pixels(10.0),
175 align_x: alignment::Horizontal::Center.into(),
176 align_y: alignment::Vertical::Top,
177 font: self.font,
178 ..Text::default()
179 });
180 }
181
182 vec![frame.into_geometry()]
183 }
184
185 fn update(
186 &self,
187 state: &mut Self::State,
188 event: &Event,
189 bounds: Rectangle,
190 cursor: mouse::Cursor,
191 ) -> Option<canvas::Action<Message<M>>> {
192 match event {
193 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
194 if let Some(pos) = cursor.position_in(bounds) {
195 let track_top = 0.0;
196 let track_bottom = THUMB_RADIUS * 2.0;
197 if pos.y >= track_top && pos.y <= track_bottom {
198 state.dragging = true;
199 state.start_value = self.value;
200 state.start_x = pos.x;
201 return Some(
202 canvas::Action::publish(Message::Param(ParamMessage::BeginEdit(
203 self.id,
204 )))
205 .and_capture(),
206 );
207 }
208 }
209 }
210 Event::Mouse(mouse::Event::CursorMoved { .. }) if state.dragging => {
211 if let Some(pos) = cursor.position() {
212 let current_x = pos.x - bounds.x;
213 let track_width = bounds.width - THUMB_RADIUS * 2.0;
214 let delta = (current_x - state.start_x) / track_width;
215 let new_value = (state.start_value + delta).clamp(0.0, 1.0);
216 return Some(
217 canvas::Action::publish(Message::Param(ParamMessage::SetNormalized(
218 self.id,
219 f64::from(new_value),
220 )))
221 .and_capture(),
222 );
223 }
224 }
225 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) if state.dragging => {
226 state.dragging = false;
227 return Some(
228 canvas::Action::publish(Message::Param(ParamMessage::EndEdit(self.id)))
229 .and_capture(),
230 );
231 }
232 _ => {}
233 }
234
235 None
236 }
237
238 fn mouse_interaction(
239 &self,
240 state: &Self::State,
241 bounds: Rectangle,
242 cursor: mouse::Cursor,
243 ) -> mouse::Interaction {
244 if state.dragging {
245 return mouse::Interaction::Grabbing;
246 }
247 if let Some(pos) = cursor.position_in(bounds)
248 && pos.y <= THUMB_RADIUS * 2.0
249 {
250 return mouse::Interaction::Grab;
251 }
252 mouse::Interaction::default()
253 }
254}