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