Skip to main content

maolan_widgets/
clip.rs

1use crate::midi::{PITCH_MAX, PianoNote};
2use iced::{
3    Background, Border, Color, Element, Length, Point, Rectangle, Renderer, Theme, gradient, mouse,
4    widget::{
5        Space, Stack, canvas,
6        canvas::{Frame, Geometry, Path},
7        column, container, mouse_area, pin, text,
8    },
9};
10use std::{
11    cell::Cell,
12    hash::{Hash, Hasher},
13    path::PathBuf,
14    sync::Arc,
15};
16use wavers::Wav;
17
18pub type PeakPair = [f32; 2];
19pub type ClipPeaksData = Vec<Vec<PeakPair>>;
20pub type ClipPeaks = Arc<ClipPeaksData>;
21
22const CHECKPOINTS: usize = 16;
23const MAX_RENDER_COLUMNS: usize = 32_767;
24const RENDER_MARGIN_COLUMNS: usize = 2;
25const DEFAULT_RESIZE_HANDLE_WIDTH: f32 = 5.0;
26
27#[derive(Debug, Clone, Default)]
28pub struct AudioClipData {
29    pub name: String,
30    pub start: usize,
31    pub length: usize,
32    pub offset: usize,
33    pub muted: bool,
34    pub max_length_samples: usize,
35    pub peaks: ClipPeaks,
36    pub fade_enabled: bool,
37    pub fade_in_samples: usize,
38    pub fade_out_samples: usize,
39    pub grouped_clips: Vec<AudioClipData>,
40}
41
42impl AudioClipData {
43    pub fn is_group(&self) -> bool {
44        !self.grouped_clips.is_empty()
45    }
46}
47
48#[derive(Debug, Clone, Default)]
49pub struct MIDIClipData {
50    pub name: String,
51    pub start: usize,
52    pub length: usize,
53    pub offset: usize,
54    pub input_channel: usize,
55    pub muted: bool,
56    pub max_length_samples: usize,
57    pub grouped_clips: Vec<MIDIClipData>,
58}
59
60impl MIDIClipData {
61    pub fn is_group(&self) -> bool {
62        !self.grouped_clips.is_empty()
63    }
64}
65
66#[derive(Clone)]
67pub struct ClipEdgeMessages<Message> {
68    pub left_hover_enter: Message,
69    pub left_hover_exit: Message,
70    pub left_press: Message,
71    pub right_hover_enter: Message,
72    pub right_hover_exit: Message,
73    pub right_press: Message,
74}
75
76pub struct AudioClipInteraction<Message> {
77    pub on_select: Message,
78    pub on_open: Message,
79    pub on_drag: Option<Arc<dyn Fn(Point) -> Message + Send + Sync + 'static>>,
80    pub edges: ClipEdgeMessages<Message>,
81    pub fade_in_press: Option<Message>,
82    pub fade_out_press: Option<Message>,
83}
84
85pub struct MIDIClipInteraction<Message> {
86    pub on_select: Message,
87    pub on_open: Message,
88    pub on_drag: Option<Arc<dyn Fn(Point) -> Message + Send + Sync + 'static>>,
89    pub edges: ClipEdgeMessages<Message>,
90}
91
92fn clean_clip_name(name: &str) -> String {
93    let mut cleaned = name.to_string();
94    if let Some(stripped) = cleaned.strip_prefix("audio/") {
95        cleaned = stripped.to_string();
96    }
97    if let Some(stripped) = cleaned.strip_prefix("midi/") {
98        cleaned = stripped.to_string();
99    }
100    if let Some(stripped) = cleaned.strip_suffix(".wav") {
101        cleaned = stripped.to_string();
102    }
103    if let Some(stripped) = cleaned.strip_suffix(".midi") {
104        cleaned = stripped.to_string();
105    } else if let Some(stripped) = cleaned.strip_suffix(".mid") {
106        cleaned = stripped.to_string();
107    }
108    cleaned
109}
110
111fn trim_label_to_width(label: &str, width_px: f32) -> String {
112    let max_chars = ((width_px - 10.0) / 7.0).floor() as i32;
113    if max_chars <= 0 {
114        return String::new();
115    }
116    let max_chars = max_chars as usize;
117    if label.chars().count() <= max_chars {
118        return label.to_string();
119    }
120    label.chars().take(max_chars).collect()
121}
122
123fn clip_label_overlay<Message: 'static>(label: String) -> Element<'static, Message> {
124    container(
125        column![
126            Space::new().height(Length::FillPortion(1)),
127            text(label)
128                .size(12)
129                .width(Length::Fill)
130                .align_x(iced::alignment::Horizontal::Left),
131            Space::new().height(Length::FillPortion(1)),
132        ]
133        .width(Length::Fill)
134        .height(Length::Fill),
135    )
136    .width(Length::Fill)
137    .height(Length::Fill)
138    .padding([0, 5])
139    .into()
140}
141
142fn brighten(color: Color, amount: f32) -> Color {
143    Color {
144        r: (color.r + amount).min(1.0),
145        g: (color.g + amount).min(1.0),
146        b: (color.b + amount).min(1.0),
147        a: color.a,
148    }
149}
150
151fn darken(color: Color, amount: f32) -> Color {
152    Color {
153        r: (color.r - amount).max(0.0),
154        g: (color.g - amount).max(0.0),
155        b: (color.b - amount).max(0.0),
156        a: color.a,
157    }
158}
159
160fn clip_two_edge_gradient(
161    base: Color,
162    muted_alpha: f32,
163    normal_alpha: f32,
164    reverse: bool,
165) -> Background {
166    let alpha = normal_alpha;
167    let (edge, center) = if reverse {
168        (
169            Color {
170                a: alpha,
171                ..darken(base, 0.05)
172            },
173            Color {
174                a: alpha,
175                ..brighten(base, 0.06)
176            },
177        )
178    } else {
179        (
180            Color {
181                a: alpha,
182                ..brighten(base, 0.06)
183            },
184            Color {
185                a: alpha,
186                ..darken(base, 0.05)
187            },
188        )
189    };
190    let edge_muted = Color {
191        a: muted_alpha,
192        ..edge
193    };
194    let center_muted = Color {
195        a: muted_alpha,
196        ..center
197    };
198
199    let (top_bottom, middle) = if muted_alpha < normal_alpha {
200        (edge_muted, center_muted)
201    } else {
202        (edge, center)
203    };
204    Background::Gradient(
205        gradient::Linear::new(0.0)
206            .add_stop(0.0, top_bottom)
207            .add_stop(0.5, middle)
208            .add_stop(1.0, top_bottom)
209            .into(),
210    )
211}
212
213fn visible_fade_overlay_width(fade_samples: usize, pixels_per_sample: f32) -> f32 {
214    fade_samples as f32 * pixels_per_sample
215}
216
217fn should_draw_fade_overlay(fade_samples: usize, pixels_per_sample: f32) -> bool {
218    fade_samples as f32 * pixels_per_sample > 3.0
219}
220
221#[derive(Debug, Clone, Copy)]
222struct FadeBezierCanvas {
223    color: Color,
224    fade_out: bool,
225}
226
227impl<Message> canvas::Program<Message> for FadeBezierCanvas {
228    type State = ();
229
230    fn draw(
231        &self,
232        _state: &Self::State,
233        renderer: &Renderer,
234        _theme: &Theme,
235        bounds: Rectangle,
236        _cursor: mouse::Cursor,
237    ) -> Vec<Geometry> {
238        let mut frame = Frame::new(renderer, bounds.size());
239        let start = if self.fade_out {
240            Point::new(0.0, 0.0)
241        } else {
242            Point::new(0.0, bounds.height)
243        };
244        let end = if self.fade_out {
245            Point::new(bounds.width, bounds.height)
246        } else {
247            Point::new(bounds.width, 0.0)
248        };
249        let c1 = if self.fade_out {
250            Point::new(bounds.width * 0.2, 0.0)
251        } else {
252            Point::new(bounds.width * 0.2, bounds.height)
253        };
254        let c2 = if self.fade_out {
255            Point::new(bounds.width * 0.8, bounds.height)
256        } else {
257            Point::new(bounds.width * 0.8, 0.0)
258        };
259        let fill = Path::new(|builder| {
260            if self.fade_out {
261                builder.move_to(Point::new(0.0, 0.0));
262                builder.line_to(Point::new(bounds.width, 0.0));
263                builder.line_to(end);
264            } else {
265                builder.move_to(Point::new(0.0, 0.0));
266                builder.line_to(end);
267            }
268            builder.bezier_curve_to(c2, c1, start);
269            builder.line_to(Point::new(0.0, 0.0));
270        });
271        frame.fill(&fill, Color::from_rgba(0.0, 0.0, 0.0, 0.22));
272
273        let path = Path::new(|builder| {
274            builder.move_to(start);
275            builder.bezier_curve_to(c1, c2, end);
276        });
277        frame.stroke(
278            &path,
279            canvas::Stroke::default()
280                .with_width(1.0)
281                .with_color(self.color),
282        );
283        vec![frame.into_geometry()]
284    }
285}
286
287fn fade_bezier_overlay<Message: 'static>(
288    width: f32,
289    height: f32,
290    color: Color,
291    fade_out: bool,
292) -> Element<'static, Message> {
293    canvas(FadeBezierCanvas { color, fade_out })
294        .width(Length::Fixed(width.max(0.0)))
295        .height(Length::Fixed(height.max(0.0)))
296        .into()
297}
298
299#[derive(Default)]
300struct WaveformCanvasState {
301    cache: canvas::Cache,
302    last_hash: Cell<u64>,
303}
304
305#[derive(Clone)]
306struct WaveformCanvas {
307    peaks: ClipPeaks,
308    source_wav_path: Option<PathBuf>,
309    clip_offset: usize,
310    clip_length: usize,
311    max_length: usize,
312}
313
314impl WaveformCanvas {
315    fn shape_hash(&self, bounds: Rectangle) -> u64 {
316        let mut hasher = std::collections::hash_map::DefaultHasher::new();
317        bounds.width.to_bits().hash(&mut hasher);
318        bounds.height.to_bits().hash(&mut hasher);
319        self.clip_offset.hash(&mut hasher);
320        self.clip_length.hash(&mut hasher);
321        self.max_length.hash(&mut hasher);
322        self.peaks.len().hash(&mut hasher);
323        for channel in self.peaks.iter() {
324            channel.len().hash(&mut hasher);
325            if channel.is_empty() {
326                continue;
327            }
328            for i in 0..CHECKPOINTS {
329                let idx = (i * channel.len()) / CHECKPOINTS;
330                let sample = channel[idx.min(channel.len() - 1)];
331                sample[0].to_bits().hash(&mut hasher);
332                sample[1].to_bits().hash(&mut hasher);
333            }
334        }
335        hasher.finish()
336    }
337
338    fn aggregate_column_peak(
339        channel_peaks: &[[f32; 2]],
340        src_start: usize,
341        src_end: usize,
342    ) -> Option<(f32, f32)> {
343        if src_start >= src_end || src_end > channel_peaks.len() {
344            return None;
345        }
346        let mut min_val = 1.0_f32;
347        let mut max_val = -1.0_f32;
348        for pair in &channel_peaks[src_start..src_end] {
349            min_val = min_val.min(pair[0].clamp(-1.0, 1.0));
350            max_val = max_val.max(pair[1].clamp(-1.0, 1.0));
351        }
352        Some((min_val, max_val))
353    }
354
355    fn source_column_peaks(
356        source_wav_path: &PathBuf,
357        channel_count: usize,
358        source_start_sample: usize,
359        source_end_sample: usize,
360        total_columns: usize,
361    ) -> Option<Vec<Vec<[f32; 2]>>> {
362        if total_columns == 0 || source_end_sample <= source_start_sample || channel_count == 0 {
363            return None;
364        }
365        let mut wav = Wav::<f32>::from_path(source_wav_path).ok()?;
366        let wav_channels = wav.n_channels().max(1) as usize;
367        let use_channels = channel_count.min(wav_channels).max(1);
368        let total_frames = wav.n_samples() / wav_channels;
369        if source_start_sample >= total_frames {
370            return None;
371        }
372        let read_end = source_end_sample.min(total_frames);
373        let read_frames = read_end.saturating_sub(source_start_sample);
374        if read_frames == 0 {
375            return None;
376        }
377
378        wav.to_data().ok()?;
379        wav.seek_by_samples((source_start_sample.saturating_mul(wav_channels)) as u64)
380            .ok()?;
381        let chunk = wav
382            .read_samples(read_frames.saturating_mul(wav_channels))
383            .ok()?;
384        if chunk.is_empty() {
385            return None;
386        }
387
388        let mut out = vec![vec![[0.0_f32, 0.0_f32]; total_columns]; channel_count];
389        for col in 0..total_columns {
390            let frame_start = (col * read_frames) / total_columns;
391            let mut frame_end = ((col + 1) * read_frames) / total_columns;
392            if frame_end <= frame_start {
393                frame_end = (frame_start + 1).min(read_frames);
394            }
395            if frame_start >= frame_end {
396                continue;
397            }
398            for (ch, out_channel) in out.iter_mut().enumerate().take(use_channels) {
399                let mut min_val = 1.0_f32;
400                let mut max_val = -1.0_f32;
401                for frame_idx in frame_start..frame_end {
402                    let sample_idx = frame_idx.saturating_mul(wav_channels).saturating_add(ch);
403                    let s = chunk
404                        .get(sample_idx)
405                        .copied()
406                        .unwrap_or(0.0)
407                        .clamp(-1.0, 1.0);
408                    min_val = min_val.min(s);
409                    max_val = max_val.max(s);
410                }
411                out_channel[col] = [min_val, max_val];
412            }
413        }
414
415        Some(out)
416    }
417}
418
419impl<Message> canvas::Program<Message> for WaveformCanvas {
420    type State = WaveformCanvasState;
421
422    fn draw(
423        &self,
424        state: &Self::State,
425        renderer: &Renderer,
426        _theme: &Theme,
427        bounds: Rectangle,
428        _cursor: mouse::Cursor,
429    ) -> Vec<Geometry> {
430        if self.peaks.is_empty() || bounds.width <= 0.0 || bounds.height <= 0.0 {
431            return vec![];
432        }
433
434        let hash = self.shape_hash(bounds);
435        if state.last_hash.get() != hash {
436            state.cache.clear();
437            state.last_hash.set(hash);
438        }
439
440        let geom = state
441            .cache
442            .draw(renderer, bounds.size(), |frame: &mut Frame| {
443                let inner_w = bounds.width.max(4.0);
444                let inner_h = bounds.height.max(4.0);
445                let channel_count = self.peaks.len().max(1);
446                let channel_h = inner_h / channel_count as f32;
447                let waveform_fill = Color::from_rgba(0.86, 0.94, 1.0, 0.34);
448                let waveform_edge = Color::from_rgba(0.96, 0.98, 1.0, 0.62);
449                let zero_line = Color::from_rgba(0.74, 0.86, 1.0, 0.28);
450                let clip_color = Color::from_rgba(1.0, 0.42, 0.30, 0.78);
451                let clip_level = 0.90_f32;
452                let edge_shade = darken(waveform_fill, 0.08);
453
454                for (channel_idx, channel_peaks) in self.peaks.iter().enumerate() {
455                    if channel_peaks.is_empty() {
456                        continue;
457                    }
458                    let channel_top = channel_h * channel_idx as f32;
459                    let center_y = channel_top + channel_h * 0.5;
460                    let half_span = (channel_h * 0.45).max(1.0);
461                    let total_peaks = channel_peaks.len();
462                    let max_len = self.max_length.max(1);
463                    let start_idx = ((self.clip_offset * total_peaks) / max_len)
464                        .min(total_peaks.saturating_sub(1));
465                    let clip_end_sample = self
466                        .clip_offset
467                        .saturating_add(self.clip_length)
468                        .min(max_len);
469                    let mut end_idx = ((clip_end_sample * total_peaks) / max_len).min(total_peaks);
470                    if end_idx <= start_idx {
471                        end_idx = (start_idx + 1).min(total_peaks);
472                    }
473                    let visible_bins = end_idx.saturating_sub(start_idx).max(1);
474                    let visible_columns =
475                        inner_w.ceil().max(1.0).min(MAX_RENDER_COLUMNS as f32) as usize;
476                    let x_step = inner_w / visible_columns as f32;
477                    let margin_columns = RENDER_MARGIN_COLUMNS;
478                    let total_columns = visible_columns + (margin_columns * 2);
479                    let margin_bins = ((visible_bins * margin_columns) / visible_columns).max(1);
480                    let render_start_idx = start_idx.saturating_sub(margin_bins);
481                    let render_end_idx = end_idx.saturating_add(margin_bins).min(total_peaks);
482                    let render_bins = render_end_idx.saturating_sub(render_start_idx).max(1);
483                    let stored_samples_per_bin = max_len as f32 / total_peaks.max(1) as f32;
484                    let visible_source_samples =
485                        clip_end_sample.saturating_sub(self.clip_offset).max(1);
486                    let required_samples_per_column =
487                        visible_source_samples as f32 / visible_columns.max(1) as f32;
488                    let high_zoom_source_mode = required_samples_per_column < 1.0;
489                    let trace_mode = high_zoom_source_mode
490                        || required_samples_per_column <= 4.0
491                        || visible_bins <= visible_columns.saturating_mul(2);
492                    let use_source_columns = self.source_wav_path.is_some()
493                        && required_samples_per_column + f32::EPSILON < stored_samples_per_bin;
494                    let mut source_mode_columns = total_columns;
495                    let mut source_mode_margin = margin_columns;
496                    let mut source_mode_x_step = x_step;
497                    let mut source_mode_bin_w = x_step.max(1.0);
498                    let source_columns = if use_source_columns {
499                        let source_margin_samples = if high_zoom_source_mode {
500                            margin_columns
501                        } else {
502                            ((visible_source_samples * margin_columns) / visible_columns).max(1)
503                        };
504                        if high_zoom_source_mode {
505                            source_mode_columns =
506                                visible_source_samples + (source_margin_samples * 2);
507                            source_mode_margin = source_margin_samples;
508                            source_mode_x_step = inner_w / visible_source_samples.max(1) as f32;
509                            source_mode_bin_w = 1.0;
510                        }
511                        let source_start = self.clip_offset.saturating_sub(source_margin_samples);
512                        let source_end = clip_end_sample
513                            .saturating_add(source_margin_samples)
514                            .min(self.max_length.max(1));
515                        self.source_wav_path.as_ref().and_then(|path| {
516                            Self::source_column_peaks(
517                                path,
518                                self.peaks.len(),
519                                source_start,
520                                source_end,
521                                source_mode_columns,
522                            )
523                        })
524                    } else {
525                        None
526                    };
527
528                    frame.fill(
529                        &Path::rectangle(Point::new(0.0, center_y), iced::Size::new(inner_w, 1.0)),
530                        zero_line,
531                    );
532
533                    let draw_columns = if source_columns.is_some() {
534                        source_mode_columns
535                    } else {
536                        total_columns
537                    };
538                    if trace_mode {
539                        let trace = Path::new(|builder| {
540                            let mut started = false;
541                            for col in 0..draw_columns {
542                                let pair = if let Some(columns) = source_columns.as_ref() {
543                                    columns
544                                        .get(channel_idx)
545                                        .and_then(|ch| ch.get(col))
546                                        .copied()
547                                        .unwrap_or([0.0, 0.0])
548                                } else {
549                                    let src_start = render_start_idx
550                                        + ((col * render_bins) / draw_columns).min(render_bins);
551                                    let mut src_end = render_start_idx
552                                        + (((col + 1) * render_bins) / draw_columns)
553                                            .min(render_bins);
554                                    if src_end <= src_start {
555                                        src_end = (src_start + 1).min(total_peaks);
556                                    }
557                                    let pair = Self::aggregate_column_peak(
558                                        channel_peaks,
559                                        src_start,
560                                        src_end,
561                                    )
562                                    .unwrap_or((0.0, 0.0));
563                                    [pair.0, pair.1]
564                                };
565                                let sample = ((pair[0] + pair[1]) * 0.5).clamp(-1.0, 1.0);
566                                let x = if source_columns.is_some() {
567                                    (col as f32 - source_mode_margin as f32) * source_mode_x_step
568                                } else {
569                                    (col as f32 - margin_columns as f32) * x_step
570                                };
571                                let y = (center_y - (sample * half_span))
572                                    .clamp(channel_top, channel_top + channel_h);
573                                if !started {
574                                    builder.move_to(Point::new(x, y));
575                                    started = true;
576                                } else {
577                                    builder.line_to(Point::new(x, y));
578                                }
579                            }
580                        });
581                        frame.stroke(
582                            &trace,
583                            canvas::Stroke::default()
584                                .with_color(waveform_edge)
585                                .with_width(1.0),
586                        );
587                        continue;
588                    }
589
590                    for col in 0..draw_columns {
591                        let (min_val, max_val) = if let Some(columns) = source_columns.as_ref() {
592                            let pair = columns
593                                .get(channel_idx)
594                                .and_then(|ch| ch.get(col))
595                                .copied()
596                                .unwrap_or([0.0, 0.0]);
597                            (pair[0], pair[1])
598                        } else {
599                            let src_start = render_start_idx
600                                + ((col * render_bins) / total_columns).min(render_bins);
601                            let mut src_end = render_start_idx
602                                + (((col + 1) * render_bins) / total_columns).min(render_bins);
603                            if src_end <= src_start {
604                                src_end = (src_start + 1).min(total_peaks);
605                            }
606                            let Some(pair) =
607                                Self::aggregate_column_peak(channel_peaks, src_start, src_end)
608                            else {
609                                continue;
610                            };
611                            pair
612                        };
613                        let top = (center_y - (max_val * half_span))
614                            .clamp(channel_top, channel_top + channel_h);
615                        let bottom = (center_y - (min_val * half_span))
616                            .clamp(channel_top, channel_top + channel_h);
617                        let y = top.min(bottom);
618                        let h = (bottom - top).abs().max(1.0);
619                        let (x, bin_w) = if source_columns.is_some() {
620                            (
621                                (col as f32 - source_mode_margin as f32) * source_mode_x_step,
622                                source_mode_bin_w,
623                            )
624                        } else {
625                            (
626                                (col as f32 - margin_columns as f32) * x_step,
627                                x_step.max(1.0),
628                            )
629                        };
630
631                        frame.fill(
632                            &Path::rectangle(Point::new(x, y), iced::Size::new(bin_w, h)),
633                            waveform_fill,
634                        );
635                        let edge_h = (h * 0.2).clamp(1.0, 3.0);
636                        frame.fill(
637                            &Path::rectangle(Point::new(x, y), iced::Size::new(bin_w, edge_h)),
638                            edge_shade,
639                        );
640                        frame.fill(
641                            &Path::rectangle(
642                                Point::new(x, y + h - edge_h),
643                                iced::Size::new(bin_w, edge_h),
644                            ),
645                            edge_shade,
646                        );
647
648                        if h >= 3.0 {
649                            frame.fill(
650                                &Path::rectangle(Point::new(x, y), iced::Size::new(bin_w, 1.0)),
651                                waveform_edge,
652                            );
653                            frame.fill(
654                                &Path::rectangle(
655                                    Point::new(x, y + h - 1.0),
656                                    iced::Size::new(bin_w, 1.0),
657                                ),
658                                waveform_edge,
659                            );
660                        }
661
662                        if max_val >= clip_level {
663                            let clip_h = h.clamp(1.0, 3.0);
664                            frame.fill(
665                                &Path::rectangle(Point::new(x, y), iced::Size::new(bin_w, clip_h)),
666                                clip_color,
667                            );
668                        }
669                        if -min_val >= clip_level {
670                            let clip_h = h.clamp(1.0, 3.0);
671                            frame.fill(
672                                &Path::rectangle(
673                                    Point::new(x, y + h - clip_h),
674                                    iced::Size::new(bin_w, clip_h),
675                                ),
676                                clip_color,
677                            );
678                        }
679                    }
680                }
681            });
682        vec![geom]
683    }
684}
685
686#[derive(Default)]
687struct MidiClipNotesCanvasState {
688    cache: canvas::Cache,
689    last_hash: Cell<u64>,
690}
691
692#[derive(Clone)]
693struct MidiClipNotesCanvas {
694    notes: Arc<Vec<PianoNote>>,
695    clip_offset_samples: usize,
696    clip_visible_length_samples: usize,
697}
698
699impl MidiClipNotesCanvas {
700    fn shape_hash(&self, bounds: Rectangle) -> u64 {
701        let mut hasher = std::collections::hash_map::DefaultHasher::new();
702        bounds.width.to_bits().hash(&mut hasher);
703        bounds.height.to_bits().hash(&mut hasher);
704        self.clip_offset_samples.hash(&mut hasher);
705        self.clip_visible_length_samples.hash(&mut hasher);
706        self.notes.len().hash(&mut hasher);
707        if let Some(first) = self.notes.first() {
708            first.start_sample.hash(&mut hasher);
709            first.length_samples.hash(&mut hasher);
710            first.pitch.hash(&mut hasher);
711            first.velocity.hash(&mut hasher);
712        }
713        if let Some(last) = self.notes.last() {
714            last.start_sample.hash(&mut hasher);
715            last.length_samples.hash(&mut hasher);
716            last.pitch.hash(&mut hasher);
717            last.velocity.hash(&mut hasher);
718        }
719        hasher.finish()
720    }
721}
722
723impl<Message> canvas::Program<Message> for MidiClipNotesCanvas {
724    type State = MidiClipNotesCanvasState;
725
726    fn draw(
727        &self,
728        state: &Self::State,
729        renderer: &Renderer,
730        _theme: &Theme,
731        bounds: Rectangle,
732        _cursor: mouse::Cursor,
733    ) -> Vec<Geometry> {
734        if self.notes.is_empty() || bounds.width <= 0.0 || bounds.height <= 0.0 {
735            return vec![];
736        }
737
738        let hash = self.shape_hash(bounds);
739        if state.last_hash.get() != hash {
740            state.cache.clear();
741            state.last_hash.set(hash);
742        }
743
744        let geom = state
745            .cache
746            .draw(renderer, bounds.size(), |frame: &mut Frame| {
747                let inner_w = bounds.width.max(1.0);
748                let inner_h = bounds.height.max(1.0);
749                let visible_start = self.clip_offset_samples;
750                let visible_len = self.clip_visible_length_samples.max(1);
751                let visible_end = visible_start.saturating_add(visible_len);
752                let clip_len = visible_len as f32;
753                let pitch_span = f32::from(PITCH_MAX) + 1.0;
754                let note_color = Color::from_rgba(0.68, 0.92, 0.40, 0.82);
755                let note_edge = Color::from_rgba(0.86, 0.98, 0.62, 0.95);
756                let grid_major = Color::from_rgba(0.74, 0.95, 0.58, 0.14);
757                let grid_minor = Color::from_rgba(0.62, 0.86, 0.48, 0.07);
758                let horizon = Color::from_rgba(0.88, 0.98, 0.72, 0.22);
759
760                for step in 0..=16 {
761                    let x = (step as f32 / 16.0) * inner_w;
762                    let color = if step % 4 == 0 {
763                        grid_major
764                    } else {
765                        grid_minor
766                    };
767                    frame.stroke(
768                        &Path::line(Point::new(x, 0.0), Point::new(x, inner_h)),
769                        canvas::Stroke::default().with_color(color).with_width(1.0),
770                    );
771                }
772
773                for row in 0..=10 {
774                    let y = (row as f32 / 10.0) * inner_h;
775                    frame.stroke(
776                        &Path::line(Point::new(0.0, y), Point::new(inner_w, y)),
777                        canvas::Stroke::default()
778                            .with_color(if row % 2 == 0 { grid_minor } else { grid_major })
779                            .with_width(0.5),
780                    );
781                }
782                let horizon_y = inner_h * 0.84;
783                frame.stroke(
784                    &Path::line(Point::new(0.0, horizon_y), Point::new(inner_w, horizon_y)),
785                    canvas::Stroke::default()
786                        .with_color(horizon)
787                        .with_width(1.0),
788                );
789
790                for note in self.notes.iter() {
791                    let note_start = note.start_sample;
792                    let note_end = note.start_sample.saturating_add(note.length_samples.max(1));
793                    if note_end <= visible_start || note_start >= visible_end {
794                        continue;
795                    }
796                    let pitch = note.pitch.min(PITCH_MAX);
797                    let clipped_start = note_start.max(visible_start);
798                    let clipped_end = note_end.min(visible_end);
799                    let rel_start = clipped_start.saturating_sub(visible_start);
800                    let rel_len = clipped_end.saturating_sub(clipped_start).max(1);
801                    let x = (rel_start as f32 / clip_len) * inner_w;
802                    let w = ((rel_len as f32 / clip_len) * inner_w).max(1.0);
803                    let pitch_pos = (i16::from(PITCH_MAX) - i16::from(pitch)) as f32 / pitch_span;
804                    let y = pitch_pos * inner_h;
805                    let h = (inner_h / pitch_span).clamp(1.0, 8.0);
806                    let rect = Path::rectangle(Point::new(x, y), iced::Size::new(w, h));
807                    frame.fill(&rect, note_color);
808                    frame.stroke(
809                        &rect,
810                        canvas::Stroke::default()
811                            .with_color(note_edge)
812                            .with_width(0.5),
813                    );
814                }
815            });
816
817        vec![geom]
818    }
819}
820
821fn midi_clip_notes_overlay<Message: 'static>(
822    notes: Arc<Vec<PianoNote>>,
823    clip_offset_samples: usize,
824    clip_visible_length_samples: usize,
825) -> Element<'static, Message> {
826    canvas(MidiClipNotesCanvas {
827        notes,
828        clip_offset_samples,
829        clip_visible_length_samples,
830    })
831    .width(Length::Fill)
832    .height(Length::Fill)
833    .into()
834}
835
836fn audio_waveform_overlay<Message: 'static>(
837    peaks: ClipPeaks,
838    source_wav_path: Option<PathBuf>,
839    clip_offset: usize,
840    clip_length: usize,
841    max_length: usize,
842) -> Element<'static, Message> {
843    canvas(WaveformCanvas {
844        peaks,
845        source_wav_path,
846        clip_offset,
847        clip_length,
848        max_length,
849    })
850    .width(Length::Fill)
851    .height(Length::Fill)
852    .into()
853}
854
855fn resolve_audio_clip_path(session_root: Option<&PathBuf>, clip_name: &str) -> Option<PathBuf> {
856    let path = PathBuf::from(clip_name);
857    if path.is_absolute() {
858        Some(path)
859    } else {
860        session_root.map(|root| root.join(path))
861    }
862}
863
864fn grouped_audio_waveform_overlay<Message: 'static>(
865    clip: &AudioClipData,
866    session_root: Option<&PathBuf>,
867    pixels_per_sample: f32,
868    clip_height: f32,
869) -> Element<'static, Message> {
870    let mut stack = Stack::new();
871    for child in &clip.grouped_clips {
872        let child_width = (child.length as f32 * pixels_per_sample).max(12.0);
873        let child_overlay = if child.is_group() {
874            grouped_audio_waveform_overlay(child, session_root, pixels_per_sample, clip_height)
875        } else {
876            audio_waveform_overlay(
877                child.peaks.clone(),
878                resolve_audio_clip_path(session_root, &child.name),
879                child.offset,
880                child.length,
881                child.max_length_samples,
882            )
883        };
884        stack = stack.push(
885            pin(container(child_overlay)
886                .width(Length::Fixed(child_width))
887                .height(Length::Fixed(clip_height)))
888            .position(Point::new(child.start as f32 * pixels_per_sample, 0.0)),
889        );
890    }
891    container(stack)
892        .width(Length::Fill)
893        .height(Length::Fill)
894        .into()
895}
896
897#[derive(Clone, Copy)]
898enum AudioClipMode {
899    Widget,
900    Preview,
901}
902
903pub struct AudioClip<Message> {
904    clip: AudioClipData,
905    session_root: Option<PathBuf>,
906    pixels_per_sample: f32,
907    clip_width: f32,
908    clip_height: f32,
909    label: String,
910    is_selected: bool,
911    left_handle_hovered: bool,
912    right_handle_hovered: bool,
913    interaction: Option<AudioClipInteraction<Message>>,
914    background: Option<Background>,
915    border_color: Option<Color>,
916    radius: f32,
917    mode: AudioClipMode,
918    base_color: Color,
919    selected_base_color: Color,
920    border: Color,
921    selected_border: Color,
922    resize_handle_width: f32,
923}
924
925impl<Message> AudioClip<Message> {
926    pub fn clean_name(name: &str) -> String {
927        clean_clip_name(name)
928    }
929
930    pub fn label_for_width(label: &str, width_px: f32) -> String {
931        trim_label_to_width(label, width_px)
932    }
933
934    pub fn two_edge_gradient(
935        base: Color,
936        muted_alpha: f32,
937        normal_alpha: f32,
938        reverse: bool,
939    ) -> Background {
940        clip_two_edge_gradient(base, muted_alpha, normal_alpha, reverse)
941    }
942
943    pub fn waveform_overlay(
944        peaks: ClipPeaks,
945        source_wav_path: Option<PathBuf>,
946        clip_offset: usize,
947        clip_length: usize,
948        max_length: usize,
949    ) -> Element<'static, Message>
950    where
951        Message: 'static,
952    {
953        audio_waveform_overlay(peaks, source_wav_path, clip_offset, clip_length, max_length)
954    }
955}
956
957impl<Message: Clone + 'static> AudioClip<Message> {
958    pub fn new(clip: AudioClipData) -> Self {
959        Self {
960            clip,
961            session_root: None,
962            pixels_per_sample: 1.0,
963            clip_width: 12.0,
964            clip_height: 8.0,
965            label: String::new(),
966            is_selected: false,
967            left_handle_hovered: false,
968            right_handle_hovered: false,
969            interaction: None,
970            background: None,
971            border_color: None,
972            radius: 3.0,
973            mode: AudioClipMode::Widget,
974            base_color: Color::from_rgb8(68, 88, 132),
975            selected_base_color: Color::from_rgb8(96, 126, 186),
976            border: Color::from_rgb8(78, 93, 130),
977            selected_border: Color::from_rgb8(176, 218, 255),
978            resize_handle_width: DEFAULT_RESIZE_HANDLE_WIDTH,
979        }
980    }
981
982    pub fn with_colors(
983        mut self,
984        base_color: Color,
985        selected_base_color: Color,
986        border: Color,
987        selected_border: Color,
988    ) -> Self {
989        self.base_color = base_color;
990        self.selected_base_color = selected_base_color;
991        self.border = border;
992        self.selected_border = selected_border;
993        self
994    }
995
996    pub fn with_session_root(mut self, session_root: Option<&PathBuf>) -> Self {
997        self.session_root = session_root.cloned();
998        self
999    }
1000
1001    pub fn with_pixels_per_sample(mut self, pixels_per_sample: f32) -> Self {
1002        self.pixels_per_sample = pixels_per_sample;
1003        self
1004    }
1005
1006    pub fn with_size(mut self, clip_width: f32, clip_height: f32) -> Self {
1007        self.clip_width = clip_width;
1008        self.clip_height = clip_height;
1009        self
1010    }
1011
1012    pub fn with_label(mut self, label: String) -> Self {
1013        self.label = label;
1014        self
1015    }
1016
1017    pub fn selected(mut self, is_selected: bool) -> Self {
1018        self.is_selected = is_selected;
1019        self
1020    }
1021
1022    pub fn hovered_handles(mut self, left: bool, right: bool) -> Self {
1023        self.left_handle_hovered = left;
1024        self.right_handle_hovered = right;
1025        self
1026    }
1027
1028    pub fn interactive(mut self, interaction: AudioClipInteraction<Message>) -> Self {
1029        self.interaction = Some(interaction);
1030        self.mode = AudioClipMode::Widget;
1031        self
1032    }
1033
1034    pub fn preview(mut self, background: Background, border_color: Color) -> Self {
1035        self.background = Some(background);
1036        self.border_color = Some(border_color);
1037        self.mode = AudioClipMode::Preview;
1038        self
1039    }
1040
1041    pub fn into_element(self) -> Element<'static, Message> {
1042        match self.mode {
1043            AudioClipMode::Preview => {
1044                let preview_content = container(Stack::with_children(vec![
1045                    audio_waveform_overlay(
1046                        self.clip.peaks.clone(),
1047                        resolve_audio_clip_path(self.session_root.as_ref(), &self.clip.name),
1048                        self.clip.offset,
1049                        self.clip.length,
1050                        self.clip.max_length_samples,
1051                    ),
1052                    clip_label_overlay(self.label),
1053                ]))
1054                .width(Length::Fill)
1055                .height(Length::Fill)
1056                .padding(0)
1057                .style(move |_theme| container::Style {
1058                    background: self.background,
1059                    ..container::Style::default()
1060                });
1061                container(preview_content)
1062                    .width(Length::Fixed(self.clip_width))
1063                    .height(Length::Fixed(self.clip_height))
1064                    .style(move |_theme| container::Style {
1065                        background: None,
1066                        border: Border {
1067                            color: self.border_color.unwrap_or(Color::TRANSPARENT),
1068                            width: 2.0,
1069                            radius: self.radius.into(),
1070                        },
1071                        ..container::Style::default()
1072                    })
1073                    .into()
1074            }
1075            AudioClipMode::Widget => {
1076                let interaction = self.interaction.expect("audio clip interaction");
1077                let clip_muted = self.clip.muted;
1078                let left_edge_zone = mouse_area(
1079                    Space::new()
1080                        .width(Length::Fixed(self.resize_handle_width))
1081                        .height(Length::Fill),
1082                )
1083                .interaction(mouse::Interaction::ResizingColumn)
1084                .on_enter(interaction.edges.left_hover_enter.clone())
1085                .on_exit(interaction.edges.left_hover_exit.clone())
1086                .on_press(interaction.edges.left_press.clone());
1087                let right_edge_zone = mouse_area(
1088                    Space::new()
1089                        .width(Length::Fixed(self.resize_handle_width))
1090                        .height(Length::Fill),
1091                )
1092                .interaction(mouse::Interaction::ResizingColumn)
1093                .on_enter(interaction.edges.right_hover_enter.clone())
1094                .on_exit(interaction.edges.right_hover_exit.clone())
1095                .on_press(interaction.edges.right_press.clone());
1096
1097                let clip_content = container(Stack::with_children(vec![
1098                    if self.clip.is_group() {
1099                        grouped_audio_waveform_overlay(
1100                            &self.clip,
1101                            self.session_root.as_ref(),
1102                            self.pixels_per_sample,
1103                            self.clip_height,
1104                        )
1105                    } else {
1106                        audio_waveform_overlay(
1107                            self.clip.peaks.clone(),
1108                            resolve_audio_clip_path(self.session_root.as_ref(), &self.clip.name),
1109                            self.clip.offset,
1110                            self.clip.length,
1111                            self.clip.max_length_samples,
1112                        )
1113                    },
1114                    clip_label_overlay(self.label),
1115                ]))
1116                .width(Length::Fill)
1117                .height(Length::Fill)
1118                .padding(0)
1119                .style(move |_theme| {
1120                    let base = if self.is_selected {
1121                        self.selected_base_color
1122                    } else {
1123                        self.base_color
1124                    };
1125                    let (muted_alpha, normal_alpha) =
1126                        if clip_muted { (0.45, 0.45) } else { (1.0, 1.0) };
1127                    container::Style {
1128                        background: Some(clip_two_edge_gradient(
1129                            base,
1130                            muted_alpha,
1131                            normal_alpha,
1132                            true,
1133                        )),
1134                        ..container::Style::default()
1135                    }
1136                });
1137
1138                let clip_widget = container(Stack::with_children(vec![
1139                    clip_content.into(),
1140                    pin(left_edge_zone).position(Point::new(0.0, 0.0)).into(),
1141                    pin(right_edge_zone)
1142                        .position(Point::new(self.clip_width - self.resize_handle_width, 0.0))
1143                        .into(),
1144                ]))
1145                .width(Length::Fixed(self.clip_width))
1146                .height(Length::Fixed(self.clip_height))
1147                .style(move |_theme| container::Style {
1148                    background: None,
1149                    border: Border {
1150                        color: if self.is_selected {
1151                            self.selected_border
1152                        } else {
1153                            self.border
1154                        },
1155                        width: if self.is_selected { 2.0 } else { 1.0 },
1156                        radius: self.radius.into(),
1157                    },
1158                    ..container::Style::default()
1159                });
1160
1161                let clip_with_fades: Element<'static, Message> = if self.clip.fade_enabled {
1162                    let fade_in_width = visible_fade_overlay_width(
1163                        self.clip.fade_in_samples,
1164                        self.pixels_per_sample,
1165                    );
1166                    let fade_out_width = visible_fade_overlay_width(
1167                        self.clip.fade_out_samples,
1168                        self.pixels_per_sample,
1169                    );
1170                    let mut stack = Stack::new().push(clip_widget);
1171                    if should_draw_fade_overlay(self.clip.fade_in_samples, self.pixels_per_sample) {
1172                        if let Some(message) = interaction.fade_in_press.clone() {
1173                            let fade_in_handle = mouse_area(
1174                                container("")
1175                                    .width(Length::Fixed(6.0))
1176                                    .height(Length::Fixed(6.0))
1177                                    .style(|_theme| container::Style {
1178                                        background: Some(Background::Color(Color::from_rgba(
1179                                            1.0, 1.0, 1.0, 0.9,
1180                                        ))),
1181                                        border: Border {
1182                                            color: Color::from_rgba(0.3, 0.3, 0.3, 1.0),
1183                                            width: 1.0,
1184                                            radius: 3.0.into(),
1185                                        },
1186                                        ..container::Style::default()
1187                                    }),
1188                            )
1189                            .on_press(message);
1190                            stack = stack.push(
1191                                pin(fade_in_handle).position(Point::new(fade_in_width - 3.0, -3.0)),
1192                            );
1193                        }
1194                        stack = stack.push(
1195                            pin(fade_bezier_overlay(
1196                                fade_in_width,
1197                                self.clip_height,
1198                                Color::from_rgba(0.0, 0.0, 0.0, 0.3),
1199                                false,
1200                            ))
1201                            .position(Point::new(0.0, 0.0)),
1202                        );
1203                    }
1204                    if should_draw_fade_overlay(self.clip.fade_out_samples, self.pixels_per_sample)
1205                    {
1206                        if let Some(message) = interaction.fade_out_press.clone() {
1207                            let fade_out_handle = mouse_area(
1208                                container("")
1209                                    .width(Length::Fixed(6.0))
1210                                    .height(Length::Fixed(6.0))
1211                                    .style(|_theme| container::Style {
1212                                        background: Some(Background::Color(Color::from_rgba(
1213                                            1.0, 1.0, 1.0, 0.9,
1214                                        ))),
1215                                        border: Border {
1216                                            color: Color::from_rgba(0.3, 0.3, 0.3, 1.0),
1217                                            width: 1.0,
1218                                            radius: 3.0.into(),
1219                                        },
1220                                        ..container::Style::default()
1221                                    }),
1222                            )
1223                            .on_press(message);
1224                            stack = stack.push(pin(fade_out_handle).position(Point::new(
1225                                self.clip_width - fade_out_width - 3.0,
1226                                -3.0,
1227                            )));
1228                        }
1229                        stack = stack.push(
1230                            pin(fade_bezier_overlay(
1231                                fade_out_width,
1232                                self.clip_height,
1233                                Color::from_rgba(0.0, 0.0, 0.0, 0.3),
1234                                true,
1235                            ))
1236                            .position(Point::new(self.clip_width - fade_out_width, 0.0)),
1237                        );
1238                    }
1239                    stack.into()
1240                } else {
1241                    clip_widget.into()
1242                };
1243
1244                let base = mouse_area(clip_with_fades);
1245                let base = if self.left_handle_hovered || self.right_handle_hovered {
1246                    base.interaction(mouse::Interaction::ResizingColumn)
1247                } else {
1248                    base
1249                };
1250                let base = base
1251                    .on_press(interaction.on_select)
1252                    .on_double_click(interaction.on_open);
1253                if let Some(on_drag) = interaction.on_drag {
1254                    base.on_move(move |point| on_drag(point)).into()
1255                } else {
1256                    base.into()
1257                }
1258            }
1259        }
1260    }
1261}
1262
1263#[derive(Clone, Copy)]
1264enum MIDIClipMode {
1265    Widget,
1266    Preview,
1267}
1268
1269pub struct MIDIClip<Message> {
1270    clip: MIDIClipData,
1271    clip_width: f32,
1272    clip_height: f32,
1273    label: String,
1274    is_selected: bool,
1275    left_handle_hovered: bool,
1276    right_handle_hovered: bool,
1277    midi_notes: Option<Arc<Vec<PianoNote>>>,
1278    interaction: Option<MIDIClipInteraction<Message>>,
1279    background: Option<Background>,
1280    border_color: Option<Color>,
1281    radius: f32,
1282    mode: MIDIClipMode,
1283    base_color: Color,
1284    selected_base_color: Color,
1285    border: Color,
1286    selected_border: Color,
1287    resize_handle_width: f32,
1288}
1289
1290impl<Message> MIDIClip<Message> {
1291    pub fn clean_name(name: &str) -> String {
1292        clean_clip_name(name)
1293    }
1294
1295    pub fn label_for_width(label: &str, width_px: f32) -> String {
1296        trim_label_to_width(label, width_px)
1297    }
1298
1299    pub fn two_edge_gradient(
1300        base: Color,
1301        muted_alpha: f32,
1302        normal_alpha: f32,
1303        reverse: bool,
1304    ) -> Background {
1305        clip_two_edge_gradient(base, muted_alpha, normal_alpha, reverse)
1306    }
1307}
1308
1309impl<Message: Clone + 'static> MIDIClip<Message> {
1310    pub fn new(clip: MIDIClipData) -> Self {
1311        Self {
1312            clip,
1313            clip_width: 12.0,
1314            clip_height: 8.0,
1315            label: String::new(),
1316            is_selected: false,
1317            left_handle_hovered: false,
1318            right_handle_hovered: false,
1319            midi_notes: None,
1320            interaction: None,
1321            background: None,
1322            border_color: None,
1323            radius: 8.0,
1324            mode: MIDIClipMode::Widget,
1325            base_color: Color::from_rgb8(55, 90, 50),
1326            selected_base_color: Color::from_rgb8(84, 133, 72),
1327            border: Color::from_rgb8(148, 215, 118),
1328            selected_border: Color::from_rgb8(196, 255, 151),
1329            resize_handle_width: DEFAULT_RESIZE_HANDLE_WIDTH,
1330        }
1331    }
1332
1333    pub fn with_colors(
1334        mut self,
1335        base_color: Color,
1336        selected_base_color: Color,
1337        border: Color,
1338        selected_border: Color,
1339    ) -> Self {
1340        self.base_color = base_color;
1341        self.selected_base_color = selected_base_color;
1342        self.border = border;
1343        self.selected_border = selected_border;
1344        self
1345    }
1346
1347    pub fn with_size(mut self, clip_width: f32, clip_height: f32) -> Self {
1348        self.clip_width = clip_width;
1349        self.clip_height = clip_height;
1350        self
1351    }
1352
1353    pub fn with_label(mut self, label: String) -> Self {
1354        self.label = label;
1355        self
1356    }
1357
1358    pub fn selected(mut self, is_selected: bool) -> Self {
1359        self.is_selected = is_selected;
1360        self
1361    }
1362
1363    pub fn hovered_handles(mut self, left: bool, right: bool) -> Self {
1364        self.left_handle_hovered = left;
1365        self.right_handle_hovered = right;
1366        self
1367    }
1368
1369    pub fn with_notes(mut self, midi_notes: Option<Arc<Vec<PianoNote>>>) -> Self {
1370        self.midi_notes = midi_notes;
1371        self
1372    }
1373
1374    pub fn interactive(mut self, interaction: MIDIClipInteraction<Message>) -> Self {
1375        self.interaction = Some(interaction);
1376        self.mode = MIDIClipMode::Widget;
1377        self
1378    }
1379
1380    pub fn preview(mut self, background: Background, border_color: Color, radius: f32) -> Self {
1381        self.background = Some(background);
1382        self.border_color = Some(border_color);
1383        self.radius = radius;
1384        self.mode = MIDIClipMode::Preview;
1385        self
1386    }
1387
1388    pub fn into_element(self) -> Element<'static, Message> {
1389        match self.mode {
1390            MIDIClipMode::Preview => {
1391                let mut preview_layers = Vec::with_capacity(2);
1392                if let Some(notes) = self.midi_notes {
1393                    preview_layers.push(midi_clip_notes_overlay(
1394                        notes,
1395                        self.clip.offset,
1396                        self.clip.length.max(1),
1397                    ));
1398                }
1399                preview_layers.push(clip_label_overlay(self.label));
1400                let preview_content = container(Stack::with_children(preview_layers))
1401                    .width(Length::Fill)
1402                    .height(Length::Fill)
1403                    .padding(0)
1404                    .style(move |_theme| container::Style {
1405                        background: self.background,
1406                        ..container::Style::default()
1407                    });
1408                container(preview_content)
1409                    .width(Length::Fixed(self.clip_width))
1410                    .height(Length::Fixed(self.clip_height))
1411                    .style(move |_theme| container::Style {
1412                        background: None,
1413                        border: Border {
1414                            color: self.border_color.unwrap_or(Color::TRANSPARENT),
1415                            width: 2.0,
1416                            radius: self.radius.into(),
1417                        },
1418                        ..container::Style::default()
1419                    })
1420                    .into()
1421            }
1422            MIDIClipMode::Widget => {
1423                let interaction = self.interaction.expect("midi clip interaction");
1424                let left_edge_zone = mouse_area(
1425                    Space::new()
1426                        .width(Length::Fixed(self.resize_handle_width))
1427                        .height(Length::Fill),
1428                )
1429                .interaction(mouse::Interaction::ResizingColumn)
1430                .on_enter(interaction.edges.left_hover_enter.clone())
1431                .on_exit(interaction.edges.left_hover_exit.clone())
1432                .on_press(interaction.edges.left_press.clone());
1433                let right_edge_zone = mouse_area(
1434                    Space::new()
1435                        .width(Length::Fixed(self.resize_handle_width))
1436                        .height(Length::Fill),
1437                )
1438                .interaction(mouse::Interaction::ResizingColumn)
1439                .on_enter(interaction.edges.right_hover_enter.clone())
1440                .on_exit(interaction.edges.right_hover_exit.clone())
1441                .on_press(interaction.edges.right_press.clone());
1442
1443                let mut clip_layers = Vec::with_capacity(2);
1444                if let Some(notes) = self.midi_notes {
1445                    clip_layers.push(midi_clip_notes_overlay(
1446                        notes,
1447                        self.clip.offset,
1448                        self.clip.length.max(1),
1449                    ));
1450                }
1451                clip_layers.push(clip_label_overlay(self.label));
1452
1453                let clip_muted = self.clip.muted;
1454                let clip_widget = container(Stack::with_children(vec![
1455                    container(Stack::with_children(clip_layers))
1456                        .width(Length::Fill)
1457                        .height(Length::Fill)
1458                        .padding(0)
1459                        .style(move |_theme| {
1460                            let base = if self.is_selected {
1461                                self.selected_base_color
1462                            } else {
1463                                self.base_color
1464                            };
1465                            let (muted_alpha, normal_alpha) = if clip_muted {
1466                                (0.42, 0.42)
1467                            } else {
1468                                (0.92, 0.92)
1469                            };
1470                            container::Style {
1471                                background: Some(clip_two_edge_gradient(
1472                                    base,
1473                                    muted_alpha,
1474                                    normal_alpha,
1475                                    false,
1476                                )),
1477                                ..container::Style::default()
1478                            }
1479                        })
1480                        .into(),
1481                    pin(left_edge_zone).position(Point::new(0.0, 0.0)).into(),
1482                    pin(right_edge_zone)
1483                        .position(Point::new(self.clip_width - self.resize_handle_width, 0.0))
1484                        .into(),
1485                ]))
1486                .width(Length::Fixed(self.clip_width))
1487                .height(Length::Fixed(self.clip_height))
1488                .style(move |_theme| container::Style {
1489                    background: None,
1490                    border: Border {
1491                        color: if self.is_selected {
1492                            self.selected_border
1493                        } else {
1494                            self.border
1495                        },
1496                        width: if self.is_selected { 2.2 } else { 1.4 },
1497                        radius: self.radius.into(),
1498                    },
1499                    ..container::Style::default()
1500                });
1501
1502                let base = mouse_area(clip_widget);
1503                let base = if self.left_handle_hovered || self.right_handle_hovered {
1504                    base.interaction(mouse::Interaction::ResizingColumn)
1505                } else {
1506                    base
1507                };
1508                let base = base
1509                    .on_press(interaction.on_select)
1510                    .on_double_click(interaction.on_open);
1511                if let Some(on_drag) = interaction.on_drag {
1512                    base.on_move(move |point| on_drag(point)).into()
1513                } else {
1514                    base.into()
1515                }
1516            }
1517        }
1518    }
1519}
1520
1521#[cfg(test)]
1522mod tests {
1523    use super::{should_draw_fade_overlay, visible_fade_overlay_width};
1524
1525    #[test]
1526    fn visible_fade_overlay_width_grows_with_zoom_below_full_size() {
1527        let low_zoom = visible_fade_overlay_width(240, 0.01);
1528        let higher_zoom = visible_fade_overlay_width(240, 0.02);
1529
1530        assert!(higher_zoom > low_zoom);
1531        assert!((low_zoom - 2.4).abs() < 1.0e-5);
1532    }
1533
1534    #[test]
1535    fn visible_fade_overlay_width_matches_actual_size_once_large_enough() {
1536        let width = visible_fade_overlay_width(240, 0.1);
1537        assert_eq!(width, 24.0);
1538    }
1539
1540    #[test]
1541    fn should_draw_fade_overlay_hides_tiny_fades() {
1542        assert!(!should_draw_fade_overlay(240, 0.0125));
1543        assert!(should_draw_fade_overlay(240, 0.0126));
1544    }
1545}