Skip to main content

maolan_widgets/
clip.rs

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