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