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