Skip to main content

maolan_widgets/
waveform.rs

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}