1use iced::{
2 Color, Element, Event, Length, Point, Rectangle, Renderer, Theme, keyboard, 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
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum SelectionEdge {
14 Start,
15 End,
16}
17
18type RatioMessageFn<'a, Message> = Box<dyn Fn(f32) -> Message + 'a>;
19type RatioPairMessageFn<'a, Message> = Box<dyn Fn((f32, f32)) -> Message + 'a>;
20
21pub struct SampleWaveform<'a, Message> {
22 channels: Vec<&'a [f32]>,
23 peak: f32,
24 empty_label: &'static str,
25 playhead_ratio: Option<f32>,
26 selection_ratio: Option<(f32, f32)>,
27 xfade_ratio: Option<(f32, f32)>,
28 markers: Vec<(usize, String)>,
29 on_click: Option<RatioMessageFn<'a, Message>>,
30 on_double_click: Option<RatioMessageFn<'a, Message>>,
31 on_selection_start: Option<RatioMessageFn<'a, Message>>,
32 on_selection_drag: Option<RatioMessageFn<'a, Message>>,
33 on_selection_finish: Option<RatioMessageFn<'a, Message>>,
34 on_xfade: Option<RatioPairMessageFn<'a, Message>>,
35 on_right_click: Option<RatioMessageFn<'a, Message>>,
36 on_middle_click: Option<RatioMessageFn<'a, Message>>,
37 on_middle_click_away: Option<RatioMessageFn<'a, Message>>,
38}
39
40impl<'a, Message> SampleWaveform<'a, Message> {
41 pub fn new(channels: impl IntoIterator<Item = &'a [f32]>, peak: f32) -> Self {
42 Self {
43 channels: channels.into_iter().collect(),
44 peak,
45 empty_label: "No sample loaded",
46 playhead_ratio: None,
47 selection_ratio: None,
48 xfade_ratio: None,
49 markers: Vec::new(),
50 on_click: None,
51 on_double_click: None,
52 on_selection_start: None,
53 on_selection_drag: None,
54 on_selection_finish: None,
55 on_xfade: None,
56 on_right_click: None,
57 on_middle_click: None,
58 on_middle_click_away: None,
59 }
60 }
61
62 pub fn playhead_ratio(mut self, playhead_ratio: Option<f32>) -> Self {
63 self.playhead_ratio = playhead_ratio.map(|ratio| ratio.clamp(0.0, 1.0));
64 self
65 }
66
67 pub fn selection_ratio(mut self, selection_ratio: Option<(f32, f32)>) -> Self {
68 self.selection_ratio = selection_ratio.map(|(start, end)| {
69 let start = start.clamp(0.0, 1.0);
70 let end = end.clamp(0.0, 1.0);
71 (start.min(end), start.max(end))
72 });
73 self
74 }
75
76 pub fn xfade_ratio(mut self, xfade_ratio: Option<(f32, f32)>) -> Self {
77 self.xfade_ratio = xfade_ratio.map(|(start, end)| {
78 let start = start.clamp(0.0, 1.0);
79 let end = end.clamp(0.0, 1.0);
80 (start.min(end), start.max(end))
81 });
82 self
83 }
84
85 pub fn on_xfade(mut self, on_xfade: impl Fn((f32, f32)) -> Message + 'a) -> Self {
86 self.on_xfade = Some(Box::new(on_xfade));
87 self
88 }
89
90 pub fn on_selection_start(mut self, on_selection_start: impl Fn(f32) -> Message + 'a) -> Self {
91 self.on_selection_start = Some(Box::new(on_selection_start));
92 self
93 }
94
95 pub fn on_selection_drag(mut self, on_selection_drag: impl Fn(f32) -> Message + 'a) -> Self {
96 self.on_selection_drag = Some(Box::new(on_selection_drag));
97 self
98 }
99
100 pub fn on_selection_finish(
101 mut self,
102 on_selection_finish: impl Fn(f32) -> Message + 'a,
103 ) -> Self {
104 self.on_selection_finish = Some(Box::new(on_selection_finish));
105 self
106 }
107
108 pub fn markers(mut self, markers: impl IntoIterator<Item = (usize, String)>) -> Self {
109 self.markers = markers.into_iter().collect();
110 self.markers.sort_unstable_by_key(|(sample, _)| *sample);
111 self
112 }
113
114 pub fn on_click(mut self, on_click: impl Fn(f32) -> Message + 'a) -> Self {
115 self.on_click = Some(Box::new(on_click));
116 self
117 }
118
119 pub fn on_double_click(mut self, on_double_click: impl Fn(f32) -> Message + 'a) -> Self {
120 self.on_double_click = Some(Box::new(on_double_click));
121 self
122 }
123
124 pub fn on_right_click(mut self, on_right_click: impl Fn(f32) -> Message + 'a) -> Self {
125 self.on_right_click = Some(Box::new(on_right_click));
126 self
127 }
128
129 pub fn on_middle_click(mut self, on_middle_click: impl Fn(f32) -> Message + 'a) -> Self {
130 self.on_middle_click = Some(Box::new(on_middle_click));
131 self
132 }
133
134 pub fn on_middle_click_away(
135 mut self,
136 on_middle_click_away: impl Fn(f32) -> Message + 'a,
137 ) -> Self {
138 self.on_middle_click_away = Some(Box::new(on_middle_click_away));
139 self
140 }
141
142 pub fn empty_label(mut self, label: &'static str) -> Self {
143 self.empty_label = label;
144 self
145 }
146
147 pub fn view(self) -> Element<'a, Message>
148 where
149 Message: 'a,
150 {
151 canvas(self).width(Length::Fill).height(Length::Fill).into()
152 }
153
154 fn frame_count(&self) -> usize {
155 self.channels
156 .iter()
157 .map(|channel| channel.len())
158 .min()
159 .unwrap_or(0)
160 }
161
162 fn near_marker(&self, ratio: f32, width: f32) -> bool {
163 if self.markers.is_empty() {
164 return false;
165 }
166 let frames = self.frame_count();
167 if frames == 0 {
168 return false;
169 }
170 let x = width * ratio;
171 self.markers
172 .iter()
173 .map(|(sample, _)| width * (*sample as f32 / frames as f32))
174 .map(|marker_x| (marker_x - x).abs())
175 .any(|distance| distance <= 8.0)
176 }
177
178 fn selection_edge_at(&self, ratio: f32, width: f32) -> Option<SelectionEdge> {
179 let (start, end) = self.selection_ratio?;
180 let x = width * ratio;
181 let start_distance = (width * start - x).abs();
182 let end_distance = (width * end - x).abs();
183 const EDGE_TOLERANCE: f32 = 6.0;
184 match (
185 start_distance <= EDGE_TOLERANCE,
186 end_distance <= EDGE_TOLERANCE,
187 ) {
188 (true, true) => {
189 if start_distance <= end_distance {
190 Some(SelectionEdge::Start)
191 } else {
192 Some(SelectionEdge::End)
193 }
194 }
195 (true, false) => Some(SelectionEdge::Start),
196 (false, true) => Some(SelectionEdge::End),
197 (false, false) => None,
198 }
199 }
200
201 fn publish_xfade(&self, edge: SelectionEdge, ratio: f32) -> Option<CanvasAction<Message>> {
202 let selection = self.selection_ratio?;
203 let region = xfade_inner_region(selection, edge, ratio);
204 self.on_xfade
205 .as_ref()
206 .map(|message| CanvasAction::publish(message(region)).and_capture())
207 }
208}
209
210fn xfade_inner_region(selection: (f32, f32), edge: SelectionEdge, ratio: f32) -> (f32, f32) {
215 let (sel_start, sel_end) = selection;
216 let (inner_start, inner_end) = match edge {
217 SelectionEdge::Start => (ratio, sel_end - (ratio - sel_start)),
218 SelectionEdge::End => (sel_start + (sel_end - ratio), ratio),
219 };
220 let inner_start = inner_start.clamp(sel_start, sel_end);
221 let inner_end = inner_end.clamp(sel_start, sel_end);
222 if inner_start > inner_end {
223 let mid = (sel_start + sel_end) / 2.0;
224 (mid, mid)
225 } else {
226 (inner_start, inner_end)
227 }
228}
229
230#[derive(Debug, Default)]
231pub struct SampleWaveformState {
232 selecting: bool,
233 drag_start: Option<Point>,
234 last_click_at: Option<Instant>,
235 modifiers: keyboard::Modifiers,
236 xfade_edge: Option<SelectionEdge>,
237}
238
239impl<Message> Program<Message> for SampleWaveform<'_, Message> {
240 type State = SampleWaveformState;
241
242 fn update(
243 &self,
244 state: &mut Self::State,
245 event: &Event,
246 bounds: Rectangle,
247 cursor: mouse::Cursor,
248 ) -> Option<CanvasAction<Message>> {
249 match event {
250 Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
251 state.modifiers = *modifiers;
252 None
253 }
254 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
255 let ratio = cursor_ratio(cursor, bounds)?;
256 let position = cursor.position_in(bounds)?;
257 let now = Instant::now();
258 let is_double_click = state
259 .last_click_at
260 .is_some_and(|last| now.duration_since(last) <= DOUBLE_CLICK);
261 state.last_click_at = Some(now);
262
263 if is_double_click {
264 state.drag_start = None;
265 state.selecting = false;
266 state.xfade_edge = None;
267 return self
268 .on_double_click
269 .as_ref()
270 .map(|message| CanvasAction::publish(message(ratio)).and_capture());
271 }
272
273 if state.modifiers.control()
274 && let Some(edge) = self.selection_edge_at(ratio, bounds.width)
275 {
276 state.drag_start = None;
277 state.selecting = false;
278 state.xfade_edge = Some(edge);
279 return self.publish_xfade(edge, ratio);
280 }
281
282 state.drag_start = Some(position);
283 state.selecting = false;
284 state.xfade_edge = None;
285 None
286 }
287 Event::Mouse(mouse::Event::CursorMoved { .. }) => {
288 let position = cursor.position_in(bounds)?;
289 if let Some(edge) = state.xfade_edge {
290 let ratio = cursor_ratio(cursor, bounds)?;
291 return self.publish_xfade(edge, ratio);
292 }
293 if let Some(drag_start) = state.drag_start
294 && !state.selecting
295 && (position.x - drag_start.x).abs() > 2.0
296 {
297 state.selecting = true;
298 let ratio = cursor_ratio(cursor, bounds)?;
299 return self
300 .on_selection_start
301 .as_ref()
302 .map(|message| CanvasAction::publish(message(ratio)).and_capture());
303 }
304 if state.selecting {
305 let ratio = cursor_ratio(cursor, bounds)?;
306 return self
307 .on_selection_drag
308 .as_ref()
309 .map(|message| CanvasAction::publish(message(ratio)).and_capture());
310 }
311 None
312 }
313 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
314 let ratio = cursor_ratio(cursor, bounds)?;
315 if let Some(edge) = state.xfade_edge.take() {
316 return self.publish_xfade(edge, ratio);
317 }
318 if state.selecting {
319 state.selecting = false;
320 state.drag_start = None;
321 return self
322 .on_selection_finish
323 .as_ref()
324 .map(|message| CanvasAction::publish(message(ratio)).and_capture());
325 }
326 if state.drag_start.take().is_some() {
327 return self
328 .on_click
329 .as_ref()
330 .map(|message| CanvasAction::publish(message(ratio)).and_capture());
331 }
332 None
333 }
334 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => {
335 let ratio = cursor_ratio(cursor, bounds)?;
336 self.on_right_click
337 .as_ref()
338 .map(|message| CanvasAction::publish(message(ratio)).and_capture())
339 }
340 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) => {
341 let ratio = cursor_ratio(cursor, bounds)?;
342 if self.near_marker(ratio, bounds.width) {
343 self.on_middle_click
344 .as_ref()
345 .map(|message| CanvasAction::publish(message(ratio)).and_capture())
346 } else {
347 self.on_middle_click_away
348 .as_ref()
349 .map(|message| CanvasAction::publish(message(ratio)).and_capture())
350 }
351 }
352 _ => None,
353 }
354 }
355
356 fn draw(
357 &self,
358 _state: &Self::State,
359 renderer: &Renderer,
360 _theme: &Theme,
361 bounds: Rectangle,
362 _cursor: iced::mouse::Cursor,
363 ) -> Vec<Geometry> {
364 let mut frame = Frame::new(renderer, bounds.size());
365
366 let background = Path::rectangle(Point::ORIGIN, bounds.size());
367 frame.fill(&background, Color::from_rgb(0.075, 0.078, 0.095));
368
369 let frames = self.frame_count();
370 if frames == 0 {
371 draw_empty_label(&mut frame, self.empty_label);
372 return vec![frame.into_geometry()];
373 }
374
375 draw_waveform(&mut frame, bounds, &self.channels, frames, self.peak);
376 if let Some(selection) = self.selection_ratio {
377 draw_selection(&mut frame, bounds, selection);
378 }
379 if let Some(xfade) = self.xfade_ratio {
380 draw_xfade(&mut frame, bounds, xfade);
381 }
382 if let Some(ratio) = self.playhead_ratio {
383 draw_playhead(&mut frame, bounds, ratio);
384 }
385 draw_markers(&mut frame, bounds, frames, &self.markers);
386
387 vec![frame.into_geometry()]
388 }
389}
390
391fn cursor_ratio(cursor: mouse::Cursor, bounds: Rectangle) -> Option<f32> {
392 let position = cursor.position_in(bounds)?;
393 Some((position.x / bounds.width.max(1.0)).clamp(0.0, 1.0))
394}
395
396fn draw_empty_label(frame: &mut Frame, label: &'static str) {
397 frame.fill_text(canvas::Text {
398 content: String::from(label),
399 position: Point::new(8.0, 18.0),
400 color: Color::from_rgb(0.50, 0.52, 0.58),
401 size: iced::Pixels(12.0),
402 ..canvas::Text::default()
403 });
404}
405
406fn draw_waveform(
407 frame: &mut Frame,
408 bounds: Rectangle,
409 channels: &[&[f32]],
410 frames: usize,
411 peak: f32,
412) {
413 let width = bounds.width.max(1.0) as usize;
414 let height = bounds.height.max(1.0);
415 let channel_count = channels.len().max(1);
416 let lane_height = height / channel_count as f32;
417 let peak = peak.max(1e-10);
418 let samples_per_pixel = frames as f32 / width as f32;
419
420 for (channel_index, channel) in channels.iter().enumerate() {
421 let top = channel_index as f32 * lane_height;
422 let center = top + lane_height / 2.0;
423 let scale = (lane_height / 2.0 - 2.0).max(1.0);
424 let mut top_points = Vec::with_capacity(width);
425 let mut bottom_points = Vec::with_capacity(width);
426 for x in 0..width {
427 let start = ((x as f32 * samples_per_pixel) as usize).min(frames);
428 let end = (((x + 1) as f32 * samples_per_pixel) as usize).min(frames);
429
430 let mut min_sample = 0.0f32;
431 let mut max_sample = 0.0f32;
432 if start < end {
433 for &sample in &channel[start..end] {
434 min_sample = min_sample.min(sample);
435 max_sample = max_sample.max(sample);
436 }
437 }
438
439 let y_top = (center - (max_sample / peak) * scale).clamp(top, top + lane_height);
440 let y_bottom = (center - (min_sample / peak) * scale).clamp(top, top + lane_height);
441 top_points.push(Point::new(x as f32, y_top));
442 bottom_points.push(Point::new(x as f32, y_bottom));
443 }
444
445 let waveform_path = Path::new(|builder| {
446 if let Some(first) = top_points.first() {
447 builder.move_to(*first);
448 for point in &top_points[1..] {
449 builder.line_to(*point);
450 }
451 for point in bottom_points.iter().rev() {
452 builder.line_to(*point);
453 }
454 builder.close();
455 }
456 });
457
458 frame.fill(&waveform_path, Color::from_rgba(0.35, 0.55, 0.85, 0.35));
459 frame.stroke(
460 &waveform_path,
461 Stroke::default()
462 .with_color(Color::from_rgb(0.45, 0.72, 1.0))
463 .with_width(1.0),
464 );
465
466 if channel_index > 0 {
467 let separator = Path::line(Point::new(0.0, top), Point::new(width as f32, top));
468 frame.stroke(
469 &separator,
470 Stroke::default()
471 .with_color(Color::from_rgb(0.12, 0.13, 0.16))
472 .with_width(1.0),
473 );
474 }
475 }
476}
477
478fn draw_playhead(frame: &mut Frame, bounds: Rectangle, ratio: f32) {
479 let x = (bounds.width * ratio.clamp(0.0, 1.0)).clamp(0.0, bounds.width);
480 let path = Path::line(Point::new(x, 0.0), Point::new(x, bounds.height));
481 frame.stroke(
482 &path,
483 Stroke::default()
484 .with_color(Color::from_rgb(1.0, 0.82, 0.25))
485 .with_width(2.0),
486 );
487}
488
489fn draw_selection(frame: &mut Frame, bounds: Rectangle, selection: (f32, f32)) {
490 let x0 = bounds.width * selection.0.clamp(0.0, 1.0);
491 let x1 = bounds.width * selection.1.clamp(0.0, 1.0);
492 let width = (x1 - x0).abs().max(1.0);
493 let path = Path::rectangle(
494 Point::new(x0.min(x1), 0.0),
495 iced::Size::new(width, bounds.height),
496 );
497 frame.fill(&path, Color::from_rgba(1.0, 0.82, 0.25, 0.22));
498 frame.stroke(
499 &path,
500 Stroke::default()
501 .with_color(Color::from_rgba(1.0, 0.82, 0.25, 0.70))
502 .with_width(1.0),
503 );
504}
505
506fn draw_xfade(frame: &mut Frame, bounds: Rectangle, region: (f32, f32)) {
507 let x0 = bounds.width * region.0.clamp(0.0, 1.0);
508 let x1 = bounds.width * region.1.clamp(0.0, 1.0);
509 if x1 - x0 < 1.0 {
510 return;
511 }
512 let path = Path::rectangle(Point::new(x0, 0.0), iced::Size::new(x1 - x0, bounds.height));
513 frame.fill(&path, Color::from_rgba(0.35, 0.85, 0.55, 0.16));
514 frame.stroke(
515 &path,
516 Stroke::default()
517 .with_color(Color::from_rgba(0.35, 0.85, 0.55, 0.65))
518 .with_width(1.0),
519 );
520}
521
522fn draw_markers(frame: &mut Frame, bounds: Rectangle, frames: usize, markers: &[(usize, String)]) {
523 if frames == 0 || markers.is_empty() {
524 return;
525 }
526 let marker_color = Color::from_rgba(0.96, 0.72, 0.18, 0.95);
527 let marker_border = Color::from_rgba(0.2, 0.16, 0.04, 0.95);
528 let label_bg = Color::from_rgba(0.28, 0.20, 0.06, 0.92);
529 let label_border = Color::from_rgba(0.78, 0.62, 0.18, 0.85);
530 let text_color = Color::from_rgba(0.98, 0.92, 0.72, 0.96);
531
532 for (sample, name) in markers {
533 let ratio = (*sample as f32 / frames as f32).clamp(0.0, 1.0);
534 let x = bounds.width * ratio;
535
536 frame.stroke(
537 &Path::line(Point::new(x, 3.0), Point::new(x, bounds.height - 3.0)),
538 Stroke::default().with_width(2.0).with_color(marker_color),
539 );
540
541 let handle_size = 6.0;
542 frame.fill(
543 &Path::rectangle(
544 Point::new(x - handle_size / 2.0, 0.0),
545 iced::Size::new(handle_size, handle_size),
546 ),
547 marker_color,
548 );
549 frame.stroke(
550 &Path::rectangle(
551 Point::new(x - handle_size / 2.0, 0.0),
552 iced::Size::new(handle_size, handle_size),
553 ),
554 Stroke::default().with_width(1.0).with_color(marker_border),
555 );
556
557 let trimmed = name.trim();
558 if !trimmed.is_empty() {
559 let text_x = x + 5.0;
560 let text_y = 1.0;
561 let approx_width = trimmed.len() as f32 * 5.5 + 8.0;
562 frame.fill(
563 &Path::rectangle(
564 Point::new(text_x, text_y),
565 iced::Size::new(approx_width, 12.0),
566 ),
567 label_bg,
568 );
569 frame.stroke(
570 &Path::rectangle(
571 Point::new(text_x, text_y),
572 iced::Size::new(approx_width, 12.0),
573 ),
574 Stroke::default().with_width(1.0).with_color(label_border),
575 );
576 frame.fill_text(canvas::Text {
577 content: trimmed.to_string(),
578 position: Point::new(text_x + 3.0, text_y + 1.0),
579 color: text_color,
580 size: iced::Pixels(9.0),
581 ..Default::default()
582 });
583 }
584 }
585}
586
587#[cfg(test)]
588mod tests {
589 use super::{SelectionEdge, xfade_inner_region};
590
591 #[test]
592 fn xfade_inner_region_from_start_edge_is_symmetric() {
593 let (start, end) = xfade_inner_region((0.2, 0.8), SelectionEdge::Start, 0.3);
594 assert!((start - 0.3).abs() < 1e-6);
595 assert!((end - 0.7).abs() < 1e-6);
596 }
597
598 #[test]
599 fn xfade_inner_region_from_end_edge_is_symmetric() {
600 let (start, end) = xfade_inner_region((0.2, 0.8), SelectionEdge::End, 0.7);
601 assert!((start - 0.3).abs() < 1e-6);
602 assert!((end - 0.7).abs() < 1e-6);
603 }
604
605 #[test]
606 fn xfade_inner_region_clamps_inside_selection() {
607 let (start, end) = xfade_inner_region((0.2, 0.8), SelectionEdge::Start, 0.05);
608 assert!((start - 0.2).abs() < 1e-6);
609 assert!((end - 0.8).abs() < 1e-6);
610 let (start, end) = xfade_inner_region((0.2, 0.8), SelectionEdge::End, 0.95);
611 assert!((start - 0.2).abs() < 1e-6);
612 assert!((end - 0.8).abs() < 1e-6);
613 }
614
615 #[test]
616 fn xfade_inner_region_collapses_to_middle_when_edges_cross() {
617 let (start, end) = xfade_inner_region((0.2, 0.8), SelectionEdge::Start, 0.6);
618 assert!((start - 0.5).abs() < 1e-6);
619 assert!((end - 0.5).abs() < 1e-6);
620 }
621}