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