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}