Skip to main content

maolan_widgets/
clip.rs

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