1use iced::{
2 Color, Element, Event, Length, Point, Rectangle, Renderer, Theme, mouse,
3 widget::{
4 canvas,
5 canvas::{Action as CanvasAction, Frame, Geometry, Path, Program, Stroke},
6 },
7};
8use std::time::Instant;
9
10use crate::consts::DOUBLE_CLICK;
11
12pub struct SampleWaveform<'a, Message> {
13 channels: Vec<&'a [f32]>,
14 peak: f32,
15 empty_label: &'static str,
16 playhead_ratio: Option<f32>,
17 selection_ratio: Option<(f32, f32)>,
18 markers: Vec<(usize, String)>,
19 on_click: Option<Box<dyn Fn(f32) -> Message + 'a>>,
20 on_double_click: Option<Box<dyn Fn(f32) -> Message + 'a>>,
21 on_selection_start: Option<Box<dyn Fn(f32) -> Message + 'a>>,
22 on_selection_drag: Option<Box<dyn Fn(f32) -> Message + 'a>>,
23 on_selection_finish: Option<Box<dyn Fn(f32) -> Message + 'a>>,
24 on_right_click: Option<Box<dyn Fn(f32) -> Message + 'a>>,
25 on_middle_click: Option<Box<dyn Fn(f32) -> Message + 'a>>,
26 on_middle_click_away: Option<Box<dyn Fn(f32) -> Message + 'a>>,
27}
28
29impl<'a, Message> SampleWaveform<'a, Message> {
30 pub fn new(channels: impl IntoIterator<Item = &'a [f32]>, peak: f32) -> Self {
31 Self {
32 channels: channels.into_iter().collect(),
33 peak,
34 empty_label: "No sample loaded",
35 playhead_ratio: None,
36 selection_ratio: None,
37 markers: Vec::new(),
38 on_click: None,
39 on_double_click: None,
40 on_selection_start: None,
41 on_selection_drag: None,
42 on_selection_finish: None,
43 on_right_click: None,
44 on_middle_click: None,
45 on_middle_click_away: None,
46 }
47 }
48
49 pub fn playhead_ratio(mut self, playhead_ratio: Option<f32>) -> Self {
50 self.playhead_ratio = playhead_ratio.map(|ratio| ratio.clamp(0.0, 1.0));
51 self
52 }
53
54 pub fn selection_ratio(mut self, selection_ratio: Option<(f32, f32)>) -> Self {
55 self.selection_ratio = selection_ratio.map(|(start, end)| {
56 let start = start.clamp(0.0, 1.0);
57 let end = end.clamp(0.0, 1.0);
58 (start.min(end), start.max(end))
59 });
60 self
61 }
62
63 pub fn on_selection_start(mut self, on_selection_start: impl Fn(f32) -> Message + 'a) -> Self {
64 self.on_selection_start = Some(Box::new(on_selection_start));
65 self
66 }
67
68 pub fn on_selection_drag(mut self, on_selection_drag: impl Fn(f32) -> Message + 'a) -> Self {
69 self.on_selection_drag = Some(Box::new(on_selection_drag));
70 self
71 }
72
73 pub fn on_selection_finish(
74 mut self,
75 on_selection_finish: impl Fn(f32) -> Message + 'a,
76 ) -> Self {
77 self.on_selection_finish = Some(Box::new(on_selection_finish));
78 self
79 }
80
81 pub fn markers(mut self, markers: impl IntoIterator<Item = (usize, String)>) -> Self {
82 self.markers = markers.into_iter().collect();
83 self.markers.sort_unstable_by_key(|(sample, _)| *sample);
84 self
85 }
86
87 pub fn on_click(mut self, on_click: impl Fn(f32) -> Message + 'a) -> Self {
88 self.on_click = Some(Box::new(on_click));
89 self
90 }
91
92 pub fn on_double_click(mut self, on_double_click: impl Fn(f32) -> Message + 'a) -> Self {
93 self.on_double_click = Some(Box::new(on_double_click));
94 self
95 }
96
97 pub fn on_right_click(mut self, on_right_click: impl Fn(f32) -> Message + 'a) -> Self {
98 self.on_right_click = Some(Box::new(on_right_click));
99 self
100 }
101
102 pub fn on_middle_click(mut self, on_middle_click: impl Fn(f32) -> Message + 'a) -> Self {
103 self.on_middle_click = Some(Box::new(on_middle_click));
104 self
105 }
106
107 pub fn on_middle_click_away(
108 mut self,
109 on_middle_click_away: impl Fn(f32) -> Message + 'a,
110 ) -> Self {
111 self.on_middle_click_away = Some(Box::new(on_middle_click_away));
112 self
113 }
114
115 pub fn empty_label(mut self, label: &'static str) -> Self {
116 self.empty_label = label;
117 self
118 }
119
120 pub fn view(self) -> Element<'a, Message>
121 where
122 Message: 'a,
123 {
124 canvas(self).width(Length::Fill).height(Length::Fill).into()
125 }
126
127 fn frame_count(&self) -> usize {
128 self.channels
129 .iter()
130 .map(|channel| channel.len())
131 .min()
132 .unwrap_or(0)
133 }
134
135 fn near_marker(&self, ratio: f32, width: f32) -> bool {
136 if self.markers.is_empty() {
137 return false;
138 }
139 let frames = self.frame_count();
140 if frames == 0 {
141 return false;
142 }
143 let x = width * ratio;
144 self.markers
145 .iter()
146 .map(|(sample, _)| width * (*sample as f32 / frames as f32))
147 .map(|marker_x| (marker_x - x).abs())
148 .any(|distance| distance <= 8.0)
149 }
150}
151
152#[derive(Debug, Default)]
153pub struct SampleWaveformState {
154 selecting: bool,
155 drag_start: Option<Point>,
156 last_click_at: Option<Instant>,
157}
158
159impl<Message> Program<Message> for SampleWaveform<'_, Message> {
160 type State = SampleWaveformState;
161
162 fn update(
163 &self,
164 state: &mut Self::State,
165 event: &Event,
166 bounds: Rectangle,
167 cursor: mouse::Cursor,
168 ) -> Option<CanvasAction<Message>> {
169 match event {
170 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
171 let ratio = cursor_ratio(cursor, bounds)?;
172 let position = cursor.position_in(bounds)?;
173 let now = Instant::now();
174 let is_double_click = state
175 .last_click_at
176 .is_some_and(|last| now.duration_since(last) <= DOUBLE_CLICK);
177 state.last_click_at = Some(now);
178
179 if is_double_click {
180 state.drag_start = None;
181 state.selecting = false;
182 return self
183 .on_double_click
184 .as_ref()
185 .map(|message| CanvasAction::publish(message(ratio)).and_capture());
186 }
187
188 state.drag_start = Some(position);
189 state.selecting = false;
190 None
191 }
192 Event::Mouse(mouse::Event::CursorMoved { .. }) => {
193 let position = cursor.position_in(bounds)?;
194 if let Some(drag_start) = state.drag_start
195 && !state.selecting
196 && (position.x - drag_start.x).abs() > 2.0
197 {
198 state.selecting = true;
199 let ratio = cursor_ratio(cursor, bounds)?;
200 return self
201 .on_selection_start
202 .as_ref()
203 .map(|message| CanvasAction::publish(message(ratio)).and_capture());
204 }
205 if state.selecting {
206 let ratio = cursor_ratio(cursor, bounds)?;
207 return self
208 .on_selection_drag
209 .as_ref()
210 .map(|message| CanvasAction::publish(message(ratio)).and_capture());
211 }
212 None
213 }
214 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
215 let ratio = cursor_ratio(cursor, bounds)?;
216 if state.selecting {
217 state.selecting = false;
218 state.drag_start = None;
219 return self
220 .on_selection_finish
221 .as_ref()
222 .map(|message| CanvasAction::publish(message(ratio)).and_capture());
223 }
224 if state.drag_start.take().is_some() {
225 return self
226 .on_click
227 .as_ref()
228 .map(|message| CanvasAction::publish(message(ratio)).and_capture());
229 }
230 None
231 }
232 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => {
233 let ratio = cursor_ratio(cursor, bounds)?;
234 self.on_right_click
235 .as_ref()
236 .map(|message| CanvasAction::publish(message(ratio)).and_capture())
237 }
238 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) => {
239 let ratio = cursor_ratio(cursor, bounds)?;
240 if self.near_marker(ratio, bounds.width) {
241 self.on_middle_click
242 .as_ref()
243 .map(|message| CanvasAction::publish(message(ratio)).and_capture())
244 } else {
245 self.on_middle_click_away
246 .as_ref()
247 .map(|message| CanvasAction::publish(message(ratio)).and_capture())
248 }
249 }
250 _ => None,
251 }
252 }
253
254 fn draw(
255 &self,
256 _state: &Self::State,
257 renderer: &Renderer,
258 _theme: &Theme,
259 bounds: Rectangle,
260 _cursor: iced::mouse::Cursor,
261 ) -> Vec<Geometry> {
262 let mut frame = Frame::new(renderer, bounds.size());
263
264 let background = Path::rectangle(Point::ORIGIN, bounds.size());
265 frame.fill(&background, Color::from_rgb(0.075, 0.078, 0.095));
266
267 let frames = self.frame_count();
268 if frames == 0 {
269 draw_empty_label(&mut frame, self.empty_label);
270 return vec![frame.into_geometry()];
271 }
272
273 draw_waveform(&mut frame, bounds, &self.channels, frames, self.peak);
274 if let Some(selection) = self.selection_ratio {
275 draw_selection(&mut frame, bounds, selection);
276 }
277 if let Some(ratio) = self.playhead_ratio {
278 draw_playhead(&mut frame, bounds, ratio);
279 }
280 draw_markers(&mut frame, bounds, frames, &self.markers);
281
282 vec![frame.into_geometry()]
283 }
284}
285
286fn cursor_ratio(cursor: mouse::Cursor, bounds: Rectangle) -> Option<f32> {
287 let position = cursor.position_in(bounds)?;
288 Some((position.x / bounds.width.max(1.0)).clamp(0.0, 1.0))
289}
290
291fn draw_empty_label(frame: &mut Frame, label: &'static str) {
292 frame.fill_text(canvas::Text {
293 content: String::from(label),
294 position: Point::new(8.0, 18.0),
295 color: Color::from_rgb(0.50, 0.52, 0.58),
296 size: iced::Pixels(12.0),
297 ..canvas::Text::default()
298 });
299}
300
301fn draw_waveform(
302 frame: &mut Frame,
303 bounds: Rectangle,
304 channels: &[&[f32]],
305 frames: usize,
306 peak: f32,
307) {
308 let width = bounds.width.max(1.0) as usize;
309 let height = bounds.height.max(1.0);
310 let channel_count = channels.len().max(1);
311 let lane_height = height / channel_count as f32;
312 let peak = peak.max(1e-10);
313 let samples_per_pixel = frames as f32 / width as f32;
314
315 for (channel_index, channel) in channels.iter().enumerate() {
316 let top = channel_index as f32 * lane_height;
317 let center = top + lane_height / 2.0;
318 let scale = (lane_height / 2.0 - 2.0).max(1.0);
319 let mut top_points = Vec::with_capacity(width);
320 let mut bottom_points = Vec::with_capacity(width);
321 for x in 0..width {
322 let start = ((x as f32 * samples_per_pixel) as usize).min(frames);
323 let end = (((x + 1) as f32 * samples_per_pixel) as usize).min(frames);
324
325 let mut min_sample = 0.0f32;
326 let mut max_sample = 0.0f32;
327 if start < end {
328 for &sample in &channel[start..end] {
329 min_sample = min_sample.min(sample);
330 max_sample = max_sample.max(sample);
331 }
332 }
333
334 let y_top = (center - (max_sample / peak) * scale).clamp(top, top + lane_height);
335 let y_bottom = (center - (min_sample / peak) * scale).clamp(top, top + lane_height);
336 top_points.push(Point::new(x as f32, y_top));
337 bottom_points.push(Point::new(x as f32, y_bottom));
338 }
339
340 let waveform_path = Path::new(|builder| {
341 if let Some(first) = top_points.first() {
342 builder.move_to(*first);
343 for point in &top_points[1..] {
344 builder.line_to(*point);
345 }
346 for point in bottom_points.iter().rev() {
347 builder.line_to(*point);
348 }
349 builder.close();
350 }
351 });
352
353 frame.fill(&waveform_path, Color::from_rgba(0.35, 0.55, 0.85, 0.35));
354 frame.stroke(
355 &waveform_path,
356 Stroke::default()
357 .with_color(Color::from_rgb(0.45, 0.72, 1.0))
358 .with_width(1.0),
359 );
360
361 if channel_index > 0 {
362 let separator = Path::line(Point::new(0.0, top), Point::new(width as f32, top));
363 frame.stroke(
364 &separator,
365 Stroke::default()
366 .with_color(Color::from_rgb(0.12, 0.13, 0.16))
367 .with_width(1.0),
368 );
369 }
370 }
371}
372
373fn draw_playhead(frame: &mut Frame, bounds: Rectangle, ratio: f32) {
374 let x = (bounds.width * ratio.clamp(0.0, 1.0)).clamp(0.0, bounds.width);
375 let path = Path::line(Point::new(x, 0.0), Point::new(x, bounds.height));
376 frame.stroke(
377 &path,
378 Stroke::default()
379 .with_color(Color::from_rgb(1.0, 0.82, 0.25))
380 .with_width(2.0),
381 );
382}
383
384fn draw_selection(frame: &mut Frame, bounds: Rectangle, selection: (f32, f32)) {
385 let x0 = bounds.width * selection.0.clamp(0.0, 1.0);
386 let x1 = bounds.width * selection.1.clamp(0.0, 1.0);
387 let width = (x1 - x0).abs().max(1.0);
388 let path = Path::rectangle(
389 Point::new(x0.min(x1), 0.0),
390 iced::Size::new(width, bounds.height),
391 );
392 frame.fill(&path, Color::from_rgba(1.0, 0.82, 0.25, 0.22));
393 frame.stroke(
394 &path,
395 Stroke::default()
396 .with_color(Color::from_rgba(1.0, 0.82, 0.25, 0.70))
397 .with_width(1.0),
398 );
399}
400
401fn draw_markers(frame: &mut Frame, bounds: Rectangle, frames: usize, markers: &[(usize, String)]) {
402 if frames == 0 || markers.is_empty() {
403 return;
404 }
405 let marker_color = Color::from_rgba(0.96, 0.72, 0.18, 0.95);
406 let marker_border = Color::from_rgba(0.2, 0.16, 0.04, 0.95);
407 let label_bg = Color::from_rgba(0.28, 0.20, 0.06, 0.92);
408 let label_border = Color::from_rgba(0.78, 0.62, 0.18, 0.85);
409 let text_color = Color::from_rgba(0.98, 0.92, 0.72, 0.96);
410
411 for (sample, name) in markers {
412 let ratio = (*sample as f32 / frames as f32).clamp(0.0, 1.0);
413 let x = bounds.width * ratio;
414
415 frame.stroke(
416 &Path::line(Point::new(x, 3.0), Point::new(x, bounds.height - 3.0)),
417 Stroke::default().with_width(2.0).with_color(marker_color),
418 );
419
420 let handle_size = 6.0;
421 frame.fill(
422 &Path::rectangle(
423 Point::new(x - handle_size / 2.0, 0.0),
424 iced::Size::new(handle_size, handle_size),
425 ),
426 marker_color,
427 );
428 frame.stroke(
429 &Path::rectangle(
430 Point::new(x - handle_size / 2.0, 0.0),
431 iced::Size::new(handle_size, handle_size),
432 ),
433 Stroke::default().with_width(1.0).with_color(marker_border),
434 );
435
436 let trimmed = name.trim();
437 if !trimmed.is_empty() {
438 let text_x = x + 5.0;
439 let text_y = 1.0;
440 let approx_width = trimmed.len() as f32 * 5.5 + 8.0;
441 frame.fill(
442 &Path::rectangle(
443 Point::new(text_x, text_y),
444 iced::Size::new(approx_width, 12.0),
445 ),
446 label_bg,
447 );
448 frame.stroke(
449 &Path::rectangle(
450 Point::new(text_x, text_y),
451 iced::Size::new(approx_width, 12.0),
452 ),
453 Stroke::default().with_width(1.0).with_color(label_border),
454 );
455 frame.fill_text(canvas::Text {
456 content: trimmed.to_string(),
457 position: Point::new(text_x + 3.0, text_y + 1.0),
458 color: text_color,
459 size: iced::Pixels(9.0),
460 ..Default::default()
461 });
462 }
463 }
464}