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.saturating_add(source_margin_samples).min(
521 if self.source_length > 0 {
522 self.source_length
523 } else {
524 self.max_length
525 }
526 .max(1),
527 );
528 self.source_wav_path.as_ref().and_then(|path| {
529 Self::source_column_peaks(
530 path,
531 self.peaks.len(),
532 source_start,
533 source_end,
534 source_mode_columns,
535 )
536 })
537 } else {
538 None
539 };
540
541 frame.fill(
542 &Path::rectangle(Point::new(0.0, center_y), iced::Size::new(inner_w, 1.0)),
543 zero_line,
544 );
545
546 let draw_columns = if source_columns.is_some() {
547 source_mode_columns
548 } else {
549 total_columns
550 };
551 if trace_mode {
552 let trace = Path::new(|builder| {
553 let mut started = false;
554 for col in 0..draw_columns {
555 let pair = if let Some(columns) = source_columns.as_ref() {
556 columns
557 .get(channel_idx)
558 .and_then(|ch| ch.get(col))
559 .copied()
560 .unwrap_or([0.0, 0.0])
561 } else {
562 let src_start = render_start_idx
563 + ((col * render_bins) / draw_columns).min(render_bins);
564 let mut src_end = render_start_idx
565 + (((col + 1) * render_bins) / draw_columns)
566 .min(render_bins);
567 if src_end <= src_start {
568 src_end = (src_start + 1).min(total_peaks);
569 }
570 let pair = Self::aggregate_column_peak(
571 channel_peaks,
572 src_start,
573 src_end,
574 )
575 .unwrap_or((0.0, 0.0));
576 [pair.0, pair.1]
577 };
578 let sample = ((pair[0] + pair[1]) * 0.5).clamp(-1.0, 1.0);
579 let x = if source_columns.is_some() {
580 (col as f32 - source_mode_margin as f32) * source_mode_x_step
581 } else {
582 (col as f32 - margin_columns as f32) * x_step
583 };
584 let y = (center_y - (sample * half_span))
585 .clamp(channel_top, channel_top + channel_h);
586 if !started {
587 builder.move_to(Point::new(x, y));
588 started = true;
589 } else {
590 builder.line_to(Point::new(x, y));
591 }
592 }
593 });
594 frame.stroke(
595 &trace,
596 canvas::Stroke::default()
597 .with_color(waveform_edge)
598 .with_width(1.0),
599 );
600 continue;
601 }
602
603 for col in 0..draw_columns {
604 let (min_val, max_val) = if let Some(columns) = source_columns.as_ref() {
605 let pair = columns
606 .get(channel_idx)
607 .and_then(|ch| ch.get(col))
608 .copied()
609 .unwrap_or([0.0, 0.0]);
610 (pair[0], pair[1])
611 } else {
612 let src_start = render_start_idx
613 + ((col * render_bins) / total_columns).min(render_bins);
614 let mut src_end = render_start_idx
615 + (((col + 1) * render_bins) / total_columns).min(render_bins);
616 if src_end <= src_start {
617 src_end = (src_start + 1).min(total_peaks);
618 }
619 let Some(pair) =
620 Self::aggregate_column_peak(channel_peaks, src_start, src_end)
621 else {
622 continue;
623 };
624 pair
625 };
626 let top = (center_y - (max_val * half_span))
627 .clamp(channel_top, channel_top + channel_h);
628 let bottom = (center_y - (min_val * half_span))
629 .clamp(channel_top, channel_top + channel_h);
630 let y = top.min(bottom);
631 let h = (bottom - top).abs().max(1.0);
632 let (x, bin_w) = if source_columns.is_some() {
633 (
634 (col as f32 - source_mode_margin as f32) * source_mode_x_step,
635 source_mode_bin_w,
636 )
637 } else {
638 (
639 (col as f32 - margin_columns as f32) * x_step,
640 x_step.max(1.0),
641 )
642 };
643
644 frame.fill(
645 &Path::rectangle(Point::new(x, y), iced::Size::new(bin_w, h)),
646 waveform_fill,
647 );
648 let edge_h = (h * 0.2).clamp(1.0, 3.0);
649 frame.fill(
650 &Path::rectangle(Point::new(x, y), iced::Size::new(bin_w, edge_h)),
651 edge_shade,
652 );
653 frame.fill(
654 &Path::rectangle(
655 Point::new(x, y + h - edge_h),
656 iced::Size::new(bin_w, edge_h),
657 ),
658 edge_shade,
659 );
660
661 if h >= 3.0 {
662 frame.fill(
663 &Path::rectangle(Point::new(x, y), iced::Size::new(bin_w, 1.0)),
664 waveform_edge,
665 );
666 frame.fill(
667 &Path::rectangle(
668 Point::new(x, y + h - 1.0),
669 iced::Size::new(bin_w, 1.0),
670 ),
671 waveform_edge,
672 );
673 }
674
675 if max_val >= clip_level {
676 let clip_h = h.clamp(1.0, 3.0);
677 frame.fill(
678 &Path::rectangle(Point::new(x, y), iced::Size::new(bin_w, clip_h)),
679 clip_color,
680 );
681 }
682 if -min_val >= clip_level {
683 let clip_h = h.clamp(1.0, 3.0);
684 frame.fill(
685 &Path::rectangle(
686 Point::new(x, y + h - clip_h),
687 iced::Size::new(bin_w, clip_h),
688 ),
689 clip_color,
690 );
691 }
692 }
693 }
694 });
695 vec![geom]
696 }
697}
698
699#[derive(Default)]
700struct MidiClipNotesCanvasState {
701 cache: canvas::Cache,
702 last_hash: Cell<u64>,
703}
704
705#[derive(Clone)]
706struct MidiClipNotesCanvas {
707 notes: Arc<Vec<PianoNote>>,
708 clip_offset_samples: usize,
709 clip_visible_length_samples: usize,
710}
711
712impl MidiClipNotesCanvas {
713 fn shape_hash(&self, bounds: Rectangle) -> u64 {
714 let mut hasher = std::collections::hash_map::DefaultHasher::new();
715 bounds.width.to_bits().hash(&mut hasher);
716 bounds.height.to_bits().hash(&mut hasher);
717 self.clip_offset_samples.hash(&mut hasher);
718 self.clip_visible_length_samples.hash(&mut hasher);
719 self.notes.len().hash(&mut hasher);
720 if let Some(first) = self.notes.first() {
721 first.start_sample.hash(&mut hasher);
722 first.length_samples.hash(&mut hasher);
723 first.pitch.hash(&mut hasher);
724 first.velocity.hash(&mut hasher);
725 }
726 if let Some(last) = self.notes.last() {
727 last.start_sample.hash(&mut hasher);
728 last.length_samples.hash(&mut hasher);
729 last.pitch.hash(&mut hasher);
730 last.velocity.hash(&mut hasher);
731 }
732 hasher.finish()
733 }
734}
735
736impl<Message> canvas::Program<Message> for MidiClipNotesCanvas {
737 type State = MidiClipNotesCanvasState;
738
739 fn draw(
740 &self,
741 state: &Self::State,
742 renderer: &Renderer,
743 _theme: &Theme,
744 bounds: Rectangle,
745 _cursor: mouse::Cursor,
746 ) -> Vec<Geometry> {
747 if self.notes.is_empty() || bounds.width <= 0.0 || bounds.height <= 0.0 {
748 return vec![];
749 }
750
751 let hash = self.shape_hash(bounds);
752 if state.last_hash.get() != hash {
753 state.cache.clear();
754 state.last_hash.set(hash);
755 }
756
757 let geom = state
758 .cache
759 .draw(renderer, bounds.size(), |frame: &mut Frame| {
760 let inner_w = bounds.width.max(1.0);
761 let inner_h = bounds.height.max(1.0);
762 let visible_start = self.clip_offset_samples;
763 let visible_len = self.clip_visible_length_samples.max(1);
764 let visible_end = visible_start.saturating_add(visible_len);
765 let clip_len = visible_len as f32;
766 let pitch_span = f32::from(PITCH_MAX) + 1.0;
767 let note_color = Color::from_rgba(0.68, 0.92, 0.40, 0.82);
768 let note_edge = Color::from_rgba(0.86, 0.98, 0.62, 0.95);
769 let grid_major = Color::from_rgba(0.74, 0.95, 0.58, 0.14);
770 let grid_minor = Color::from_rgba(0.62, 0.86, 0.48, 0.07);
771 let horizon = Color::from_rgba(0.88, 0.98, 0.72, 0.22);
772
773 for step in 0..=16 {
774 let x = (step as f32 / 16.0) * inner_w;
775 let color = if step % 4 == 0 {
776 grid_major
777 } else {
778 grid_minor
779 };
780 frame.stroke(
781 &Path::line(Point::new(x, 0.0), Point::new(x, inner_h)),
782 canvas::Stroke::default().with_color(color).with_width(1.0),
783 );
784 }
785
786 for row in 0..=10 {
787 let y = (row as f32 / 10.0) * inner_h;
788 frame.stroke(
789 &Path::line(Point::new(0.0, y), Point::new(inner_w, y)),
790 canvas::Stroke::default()
791 .with_color(if row % 2 == 0 { grid_minor } else { grid_major })
792 .with_width(0.5),
793 );
794 }
795 let horizon_y = inner_h * 0.84;
796 frame.stroke(
797 &Path::line(Point::new(0.0, horizon_y), Point::new(inner_w, horizon_y)),
798 canvas::Stroke::default()
799 .with_color(horizon)
800 .with_width(1.0),
801 );
802
803 for note in self.notes.iter() {
804 let note_start = note.start_sample;
805 let note_end = note.start_sample.saturating_add(note.length_samples.max(1));
806 if note_end <= visible_start || note_start >= visible_end {
807 continue;
808 }
809 let pitch = note.pitch.min(PITCH_MAX);
810 let clipped_start = note_start.max(visible_start);
811 let clipped_end = note_end.min(visible_end);
812 let rel_start = clipped_start.saturating_sub(visible_start);
813 let rel_len = clipped_end.saturating_sub(clipped_start).max(1);
814 let x = (rel_start as f32 / clip_len) * inner_w;
815 let w = ((rel_len as f32 / clip_len) * inner_w).max(1.0);
816 let pitch_pos = (i16::from(PITCH_MAX) - i16::from(pitch)) as f32 / pitch_span;
817 let y = pitch_pos * inner_h;
818 let h = (inner_h / pitch_span).clamp(1.0, 8.0);
819 let rect = Path::rectangle(Point::new(x, y), iced::Size::new(w, h));
820 frame.fill(&rect, note_color);
821 frame.stroke(
822 &rect,
823 canvas::Stroke::default()
824 .with_color(note_edge)
825 .with_width(0.5),
826 );
827 }
828 });
829
830 vec![geom]
831 }
832}
833
834fn midi_clip_notes_overlay<Message: 'static>(
835 notes: Arc<Vec<PianoNote>>,
836 clip_offset_samples: usize,
837 clip_visible_length_samples: usize,
838) -> Element<'static, Message> {
839 canvas(MidiClipNotesCanvas {
840 notes,
841 clip_offset_samples,
842 clip_visible_length_samples,
843 })
844 .width(Length::Fill)
845 .height(Length::Fill)
846 .into()
847}
848
849fn audio_waveform_overlay<Message: 'static>(
850 peaks: ClipPeaks,
851 source_wav_path: Option<PathBuf>,
852 clip_offset: usize,
853 clip_length: usize,
854 max_length: usize,
855 source_length: usize,
856) -> Element<'static, Message> {
857 canvas(WaveformCanvas {
858 peaks,
859 source_wav_path,
860 clip_offset,
861 clip_length,
862 max_length,
863 source_length,
864 })
865 .width(Length::Fill)
866 .height(Length::Fill)
867 .into()
868}
869
870fn resolve_audio_clip_path(session_root: Option<&PathBuf>, clip_name: &str) -> Option<PathBuf> {
871 let path = PathBuf::from(clip_name);
872 if path.is_absolute() {
873 Some(path)
874 } else {
875 session_root.map(|root| root.join(path))
876 }
877}
878
879fn grouped_audio_waveform_overlay<Message: 'static>(
880 clip: &AudioClipData,
881 session_root: Option<&PathBuf>,
882 pixels_per_sample: f32,
883 clip_height: f32,
884) -> Element<'static, Message> {
885 let mut stack = Stack::new();
886 for child in &clip.grouped_clips {
887 let child_width = (child.length as f32 * pixels_per_sample).max(12.0);
888 let child_overlay = if child.is_group() {
889 grouped_audio_waveform_overlay(child, session_root, pixels_per_sample, clip_height)
890 } else {
891 audio_waveform_overlay(
892 child.peaks.clone(),
893 resolve_audio_clip_path(session_root, &child.name),
894 child.offset,
895 child.length,
896 child.max_length_samples,
897 child.source_length_samples,
898 )
899 };
900 stack = stack.push(
901 pin(container(child_overlay)
902 .width(Length::Fixed(child_width))
903 .height(Length::Fixed(clip_height)))
904 .position(Point::new(child.start as f32 * pixels_per_sample, 0.0)),
905 );
906 }
907 container(stack)
908 .width(Length::Fill)
909 .height(Length::Fill)
910 .into()
911}
912
913#[derive(Clone, Copy)]
914enum AudioClipMode {
915 Widget,
916 Preview,
917}
918
919pub struct AudioClip<Message> {
920 clip: AudioClipData,
921 session_root: Option<PathBuf>,
922 pixels_per_sample: f32,
923 clip_width: f32,
924 clip_height: f32,
925 label: String,
926 is_selected: bool,
927 left_handle_hovered: bool,
928 right_handle_hovered: bool,
929 interaction: Option<AudioClipInteraction<Message>>,
930 background: Option<Background>,
931 border_color: Option<Color>,
932 radius: f32,
933 mode: AudioClipMode,
934 base_color: Color,
935 selected_base_color: Color,
936 border: Color,
937 selected_border: Color,
938 resize_handle_width: f32,
939}
940
941impl<Message> AudioClip<Message> {
942 pub fn clean_name(name: &str) -> String {
943 clean_clip_name(name)
944 }
945
946 pub fn label_for_width(label: &str, width_px: f32) -> String {
947 trim_label_to_width(label, width_px)
948 }
949
950 pub fn two_edge_gradient(
951 base: Color,
952 muted_alpha: f32,
953 normal_alpha: f32,
954 reverse: bool,
955 ) -> Background {
956 clip_two_edge_gradient(base, muted_alpha, normal_alpha, reverse)
957 }
958
959 pub fn waveform_overlay(
960 peaks: ClipPeaks,
961 source_wav_path: Option<PathBuf>,
962 clip_offset: usize,
963 clip_length: usize,
964 max_length: usize,
965 source_length: usize,
966 ) -> Element<'static, Message>
967 where
968 Message: 'static,
969 {
970 audio_waveform_overlay(
971 peaks,
972 source_wav_path,
973 clip_offset,
974 clip_length,
975 max_length,
976 source_length,
977 )
978 }
979}
980
981impl<Message: Clone + 'static> AudioClip<Message> {
982 pub fn new(clip: AudioClipData) -> Self {
983 Self {
984 clip,
985 session_root: None,
986 pixels_per_sample: 1.0,
987 clip_width: 12.0,
988 clip_height: 8.0,
989 label: String::new(),
990 is_selected: false,
991 left_handle_hovered: false,
992 right_handle_hovered: false,
993 interaction: None,
994 background: None,
995 border_color: None,
996 radius: 8.0,
997 mode: AudioClipMode::Widget,
998 base_color: Color::from_rgb8(68, 88, 132),
999 selected_base_color: Color::from_rgb8(96, 126, 186),
1000 border: Color::from_rgb8(78, 93, 130),
1001 selected_border: Color::from_rgb8(176, 218, 255),
1002 resize_handle_width: DEFAULT_RESIZE_HANDLE_WIDTH,
1003 }
1004 }
1005
1006 pub fn with_colors(
1007 mut self,
1008 base_color: Color,
1009 selected_base_color: Color,
1010 border: Color,
1011 selected_border: Color,
1012 ) -> Self {
1013 self.base_color = base_color;
1014 self.selected_base_color = selected_base_color;
1015 self.border = border;
1016 self.selected_border = selected_border;
1017 self
1018 }
1019
1020 pub fn with_session_root(mut self, session_root: Option<&PathBuf>) -> Self {
1021 self.session_root = session_root.cloned();
1022 self
1023 }
1024
1025 pub fn with_pixels_per_sample(mut self, pixels_per_sample: f32) -> Self {
1026 self.pixels_per_sample = pixels_per_sample;
1027 self
1028 }
1029
1030 pub fn with_size(mut self, clip_width: f32, clip_height: f32) -> Self {
1031 self.clip_width = clip_width;
1032 self.clip_height = clip_height;
1033 self
1034 }
1035
1036 pub fn with_label(mut self, label: String) -> Self {
1037 self.label = label;
1038 self
1039 }
1040
1041 pub fn selected(mut self, is_selected: bool) -> Self {
1042 self.is_selected = is_selected;
1043 self
1044 }
1045
1046 pub fn hovered_handles(mut self, left: bool, right: bool) -> Self {
1047 self.left_handle_hovered = left;
1048 self.right_handle_hovered = right;
1049 self
1050 }
1051
1052 pub fn interactive(mut self, interaction: AudioClipInteraction<Message>) -> Self {
1053 self.interaction = Some(interaction);
1054 self.mode = AudioClipMode::Widget;
1055 self
1056 }
1057
1058 pub fn preview(mut self, background: Background, border_color: Color) -> Self {
1059 self.background = Some(background);
1060 self.border_color = Some(border_color);
1061 self.mode = AudioClipMode::Preview;
1062 self
1063 }
1064
1065 pub fn into_element(self) -> Element<'static, Message> {
1066 match self.mode {
1067 AudioClipMode::Preview => {
1068 let preview_content = container(Stack::with_children(vec![
1069 audio_waveform_overlay(
1070 self.clip.peaks.clone(),
1071 resolve_audio_clip_path(self.session_root.as_ref(), &self.clip.name),
1072 self.clip.offset,
1073 self.clip.length,
1074 self.clip.max_length_samples,
1075 self.clip.source_length_samples,
1076 ),
1077 clip_label_overlay(self.label),
1078 ]))
1079 .width(Length::Fill)
1080 .height(Length::Fill)
1081 .padding(0)
1082 .style(move |_theme| container::Style {
1083 background: self.background,
1084 ..container::Style::default()
1085 });
1086 container(preview_content)
1087 .width(Length::Fixed(self.clip_width))
1088 .height(Length::Fixed(self.clip_height))
1089 .style(move |_theme| container::Style {
1090 background: None,
1091 border: Border {
1092 color: self.border_color.unwrap_or(Color::TRANSPARENT),
1093 width: 2.0,
1094 radius: self.radius.into(),
1095 },
1096 ..container::Style::default()
1097 })
1098 .into()
1099 }
1100 AudioClipMode::Widget => {
1101 let interaction = self.interaction.expect("audio clip interaction");
1102 let clip_muted = self.clip.muted;
1103 let left_edge_zone = mouse_area(
1104 Space::new()
1105 .width(Length::Fixed(self.resize_handle_width))
1106 .height(Length::Fill),
1107 )
1108 .interaction(mouse::Interaction::ResizingColumn)
1109 .on_enter(interaction.edges.left_hover_enter.clone())
1110 .on_exit(interaction.edges.left_hover_exit.clone())
1111 .on_press(interaction.edges.left_press.clone());
1112 let right_edge_zone = mouse_area(
1113 Space::new()
1114 .width(Length::Fixed(self.resize_handle_width))
1115 .height(Length::Fill),
1116 )
1117 .interaction(mouse::Interaction::ResizingColumn)
1118 .on_enter(interaction.edges.right_hover_enter.clone())
1119 .on_exit(interaction.edges.right_hover_exit.clone())
1120 .on_press(interaction.edges.right_press.clone());
1121
1122 let clip_content = container(Stack::with_children(vec![
1123 if self.clip.is_group() {
1124 grouped_audio_waveform_overlay(
1125 &self.clip,
1126 self.session_root.as_ref(),
1127 self.pixels_per_sample,
1128 self.clip_height,
1129 )
1130 } else {
1131 audio_waveform_overlay(
1132 self.clip.peaks.clone(),
1133 resolve_audio_clip_path(self.session_root.as_ref(), &self.clip.name),
1134 self.clip.offset,
1135 self.clip.length,
1136 self.clip.max_length_samples,
1137 self.clip.source_length_samples,
1138 )
1139 },
1140 clip_label_overlay(self.label),
1141 ]))
1142 .width(Length::Fill)
1143 .height(Length::Fill)
1144 .padding(0)
1145 .style(move |_theme| {
1146 let base = if self.is_selected {
1147 self.selected_base_color
1148 } else {
1149 self.base_color
1150 };
1151 let (muted_alpha, normal_alpha) =
1152 if clip_muted { (0.45, 0.45) } else { (1.0, 1.0) };
1153 container::Style {
1154 background: Some(clip_two_edge_gradient(
1155 base,
1156 muted_alpha,
1157 normal_alpha,
1158 true,
1159 )),
1160 border: Border {
1161 radius: 8.0.into(),
1162 ..Default::default()
1163 },
1164 ..container::Style::default()
1165 }
1166 });
1167
1168 let clip_widget = container(Stack::with_children(vec![
1169 clip_content.into(),
1170 pin(left_edge_zone).position(Point::new(0.0, 0.0)).into(),
1171 pin(right_edge_zone)
1172 .position(Point::new(self.clip_width - self.resize_handle_width, 0.0))
1173 .into(),
1174 ]))
1175 .width(Length::Fixed(self.clip_width))
1176 .height(Length::Fixed(self.clip_height))
1177 .style(move |_theme| container::Style {
1178 background: None,
1179 border: Border {
1180 color: if self.is_selected {
1181 self.selected_border
1182 } else {
1183 self.border
1184 },
1185 width: if self.is_selected { 2.0 } else { 1.0 },
1186 radius: 8.0.into(),
1187 },
1188 ..container::Style::default()
1189 });
1190
1191 let clip_with_fades: Element<'static, Message> = if self.clip.fade_enabled {
1192 let fade_in_width = visible_fade_overlay_width(
1193 self.clip.fade_in_samples,
1194 self.pixels_per_sample,
1195 );
1196 let fade_out_width = visible_fade_overlay_width(
1197 self.clip.fade_out_samples,
1198 self.pixels_per_sample,
1199 );
1200 let mut stack = Stack::new().push(clip_widget);
1201 if should_draw_fade_overlay(self.clip.fade_in_samples, self.pixels_per_sample) {
1202 if let Some(message) = interaction.fade_in_press.clone() {
1203 let fade_in_handle = mouse_area(
1204 container("")
1205 .width(Length::Fixed(6.0))
1206 .height(Length::Fixed(6.0))
1207 .style(|_theme| container::Style {
1208 background: Some(Background::Color(Color::from_rgba(
1209 1.0, 1.0, 1.0, 0.9,
1210 ))),
1211 border: Border {
1212 color: Color::from_rgba(0.3, 0.3, 0.3, 1.0),
1213 width: 1.0,
1214 radius: 8.0.into(),
1215 },
1216 ..container::Style::default()
1217 }),
1218 )
1219 .on_press(message);
1220 stack = stack.push(
1221 pin(fade_in_handle).position(Point::new(fade_in_width - 3.0, -3.0)),
1222 );
1223 }
1224 stack = stack.push(
1225 pin(fade_bezier_overlay(
1226 fade_in_width,
1227 self.clip_height,
1228 Color::from_rgba(0.0, 0.0, 0.0, 0.3),
1229 false,
1230 ))
1231 .position(Point::new(0.0, 0.0)),
1232 );
1233 }
1234 if should_draw_fade_overlay(self.clip.fade_out_samples, self.pixels_per_sample)
1235 {
1236 if let Some(message) = interaction.fade_out_press.clone() {
1237 let fade_out_handle = mouse_area(
1238 container("")
1239 .width(Length::Fixed(6.0))
1240 .height(Length::Fixed(6.0))
1241 .style(|_theme| container::Style {
1242 background: Some(Background::Color(Color::from_rgba(
1243 1.0, 1.0, 1.0, 0.9,
1244 ))),
1245 border: Border {
1246 color: Color::from_rgba(0.3, 0.3, 0.3, 1.0),
1247 width: 1.0,
1248 radius: 8.0.into(),
1249 },
1250 ..container::Style::default()
1251 }),
1252 )
1253 .on_press(message);
1254 stack = stack.push(pin(fade_out_handle).position(Point::new(
1255 self.clip_width - fade_out_width - 3.0,
1256 -3.0,
1257 )));
1258 }
1259 stack = stack.push(
1260 pin(fade_bezier_overlay(
1261 fade_out_width,
1262 self.clip_height,
1263 Color::from_rgba(0.0, 0.0, 0.0, 0.3),
1264 true,
1265 ))
1266 .position(Point::new(self.clip_width - fade_out_width, 0.0)),
1267 );
1268 }
1269 stack.into()
1270 } else {
1271 clip_widget.into()
1272 };
1273
1274 let base = mouse_area(clip_with_fades);
1275 let base = if self.left_handle_hovered || self.right_handle_hovered {
1276 base.interaction(mouse::Interaction::ResizingColumn)
1277 } else {
1278 base
1279 };
1280 let base = base
1281 .on_press(interaction.on_select)
1282 .on_double_click(interaction.on_open);
1283 if let Some(on_drag) = interaction.on_drag {
1284 base.on_move(move |point| on_drag(point)).into()
1285 } else {
1286 base.into()
1287 }
1288 }
1289 }
1290 }
1291}
1292
1293#[derive(Clone, Copy)]
1294enum MIDIClipMode {
1295 Widget,
1296 Preview,
1297}
1298
1299pub struct MIDIClip<Message> {
1300 clip: MIDIClipData,
1301 clip_width: f32,
1302 clip_height: f32,
1303 label: String,
1304 is_selected: bool,
1305 left_handle_hovered: bool,
1306 right_handle_hovered: bool,
1307 midi_notes: Option<Arc<Vec<PianoNote>>>,
1308 interaction: Option<MIDIClipInteraction<Message>>,
1309 background: Option<Background>,
1310 border_color: Option<Color>,
1311 radius: f32,
1312 mode: MIDIClipMode,
1313 base_color: Color,
1314 selected_base_color: Color,
1315 border: Color,
1316 selected_border: Color,
1317 resize_handle_width: f32,
1318}
1319
1320impl<Message> MIDIClip<Message> {
1321 pub fn clean_name(name: &str) -> String {
1322 clean_clip_name(name)
1323 }
1324
1325 pub fn label_for_width(label: &str, width_px: f32) -> String {
1326 trim_label_to_width(label, width_px)
1327 }
1328
1329 pub fn two_edge_gradient(
1330 base: Color,
1331 muted_alpha: f32,
1332 normal_alpha: f32,
1333 reverse: bool,
1334 ) -> Background {
1335 clip_two_edge_gradient(base, muted_alpha, normal_alpha, reverse)
1336 }
1337}
1338
1339impl<Message: Clone + 'static> MIDIClip<Message> {
1340 pub fn new(clip: MIDIClipData) -> Self {
1341 Self {
1342 clip,
1343 clip_width: 12.0,
1344 clip_height: 8.0,
1345 label: String::new(),
1346 is_selected: false,
1347 left_handle_hovered: false,
1348 right_handle_hovered: false,
1349 midi_notes: None,
1350 interaction: None,
1351 background: None,
1352 border_color: None,
1353 radius: 8.0,
1354 mode: MIDIClipMode::Widget,
1355 base_color: Color::from_rgb8(55, 90, 50),
1356 selected_base_color: Color::from_rgb8(84, 133, 72),
1357 border: Color::from_rgb8(148, 215, 118),
1358 selected_border: Color::from_rgb8(196, 255, 151),
1359 resize_handle_width: DEFAULT_RESIZE_HANDLE_WIDTH,
1360 }
1361 }
1362
1363 pub fn with_colors(
1364 mut self,
1365 base_color: Color,
1366 selected_base_color: Color,
1367 border: Color,
1368 selected_border: Color,
1369 ) -> Self {
1370 self.base_color = base_color;
1371 self.selected_base_color = selected_base_color;
1372 self.border = border;
1373 self.selected_border = selected_border;
1374 self
1375 }
1376
1377 pub fn with_size(mut self, clip_width: f32, clip_height: f32) -> Self {
1378 self.clip_width = clip_width;
1379 self.clip_height = clip_height;
1380 self
1381 }
1382
1383 pub fn with_label(mut self, label: String) -> Self {
1384 self.label = label;
1385 self
1386 }
1387
1388 pub fn selected(mut self, is_selected: bool) -> Self {
1389 self.is_selected = is_selected;
1390 self
1391 }
1392
1393 pub fn hovered_handles(mut self, left: bool, right: bool) -> Self {
1394 self.left_handle_hovered = left;
1395 self.right_handle_hovered = right;
1396 self
1397 }
1398
1399 pub fn with_notes(mut self, midi_notes: Option<Arc<Vec<PianoNote>>>) -> Self {
1400 self.midi_notes = midi_notes;
1401 self
1402 }
1403
1404 pub fn interactive(mut self, interaction: MIDIClipInteraction<Message>) -> Self {
1405 self.interaction = Some(interaction);
1406 self.mode = MIDIClipMode::Widget;
1407 self
1408 }
1409
1410 pub fn preview(mut self, background: Background, border_color: Color, radius: f32) -> Self {
1411 self.background = Some(background);
1412 self.border_color = Some(border_color);
1413 self.radius = radius;
1414 self.mode = MIDIClipMode::Preview;
1415 self
1416 }
1417
1418 pub fn into_element(self) -> Element<'static, Message> {
1419 match self.mode {
1420 MIDIClipMode::Preview => {
1421 let mut preview_layers = Vec::with_capacity(2);
1422 if let Some(notes) = self.midi_notes {
1423 preview_layers.push(midi_clip_notes_overlay(
1424 notes,
1425 self.clip.offset,
1426 self.clip.length.max(1),
1427 ));
1428 }
1429 preview_layers.push(clip_label_overlay(self.label));
1430 let preview_content = container(Stack::with_children(preview_layers))
1431 .width(Length::Fill)
1432 .height(Length::Fill)
1433 .padding(0)
1434 .style(move |_theme| container::Style {
1435 background: self.background,
1436 ..container::Style::default()
1437 });
1438 container(preview_content)
1439 .width(Length::Fixed(self.clip_width))
1440 .height(Length::Fixed(self.clip_height))
1441 .style(move |_theme| container::Style {
1442 background: None,
1443 border: Border {
1444 color: self.border_color.unwrap_or(Color::TRANSPARENT),
1445 width: 2.0,
1446 radius: self.radius.into(),
1447 },
1448 ..container::Style::default()
1449 })
1450 .into()
1451 }
1452 MIDIClipMode::Widget => {
1453 let interaction = self.interaction.expect("midi clip interaction");
1454 let left_edge_zone = mouse_area(
1455 Space::new()
1456 .width(Length::Fixed(self.resize_handle_width))
1457 .height(Length::Fill),
1458 )
1459 .interaction(mouse::Interaction::ResizingColumn)
1460 .on_enter(interaction.edges.left_hover_enter.clone())
1461 .on_exit(interaction.edges.left_hover_exit.clone())
1462 .on_press(interaction.edges.left_press.clone());
1463 let right_edge_zone = mouse_area(
1464 Space::new()
1465 .width(Length::Fixed(self.resize_handle_width))
1466 .height(Length::Fill),
1467 )
1468 .interaction(mouse::Interaction::ResizingColumn)
1469 .on_enter(interaction.edges.right_hover_enter.clone())
1470 .on_exit(interaction.edges.right_hover_exit.clone())
1471 .on_press(interaction.edges.right_press.clone());
1472
1473 let mut clip_layers = Vec::with_capacity(2);
1474 if let Some(notes) = self.midi_notes {
1475 clip_layers.push(midi_clip_notes_overlay(
1476 notes,
1477 self.clip.offset,
1478 self.clip.length.max(1),
1479 ));
1480 }
1481 clip_layers.push(clip_label_overlay(self.label));
1482
1483 let clip_muted = self.clip.muted;
1484 let clip_widget = container(Stack::with_children(vec![
1485 container(Stack::with_children(clip_layers))
1486 .width(Length::Fill)
1487 .height(Length::Fill)
1488 .padding(0)
1489 .style(move |_theme| {
1490 let base = if self.is_selected {
1491 self.selected_base_color
1492 } else {
1493 self.base_color
1494 };
1495 let (muted_alpha, normal_alpha) = if clip_muted {
1496 (0.42, 0.42)
1497 } else {
1498 (0.92, 0.92)
1499 };
1500 container::Style {
1501 background: Some(clip_two_edge_gradient(
1502 base,
1503 muted_alpha,
1504 normal_alpha,
1505 false,
1506 )),
1507 border: Border {
1508 radius: 8.0.into(),
1509 ..Default::default()
1510 },
1511 ..container::Style::default()
1512 }
1513 })
1514 .into(),
1515 pin(left_edge_zone).position(Point::new(0.0, 0.0)).into(),
1516 pin(right_edge_zone)
1517 .position(Point::new(self.clip_width - self.resize_handle_width, 0.0))
1518 .into(),
1519 ]))
1520 .width(Length::Fixed(self.clip_width))
1521 .height(Length::Fixed(self.clip_height))
1522 .style(move |_theme| container::Style {
1523 background: None,
1524 border: Border {
1525 color: if self.is_selected {
1526 self.selected_border
1527 } else {
1528 self.border
1529 },
1530 width: if self.is_selected { 2.2 } else { 1.4 },
1531 radius: 8.0.into(),
1532 },
1533 ..container::Style::default()
1534 });
1535
1536 let base = mouse_area(clip_widget);
1537 let base = if self.left_handle_hovered || self.right_handle_hovered {
1538 base.interaction(mouse::Interaction::ResizingColumn)
1539 } else {
1540 base
1541 };
1542 let base = base
1543 .on_press(interaction.on_select)
1544 .on_double_click(interaction.on_open);
1545 if let Some(on_drag) = interaction.on_drag {
1546 base.on_move(move |point| on_drag(point)).into()
1547 } else {
1548 base.into()
1549 }
1550 }
1551 }
1552 }
1553}
1554
1555#[cfg(test)]
1556mod tests {
1557 use super::{should_draw_fade_overlay, visible_fade_overlay_width};
1558
1559 #[test]
1560 fn visible_fade_overlay_width_grows_with_zoom_below_full_size() {
1561 let low_zoom = visible_fade_overlay_width(240, 0.01);
1562 let higher_zoom = visible_fade_overlay_width(240, 0.02);
1563
1564 assert!(higher_zoom > low_zoom);
1565 assert!((low_zoom - 2.4).abs() < 1.0e-5);
1566 }
1567
1568 #[test]
1569 fn visible_fade_overlay_width_matches_actual_size_once_large_enough() {
1570 let width = visible_fade_overlay_width(240, 0.1);
1571 assert_eq!(width, 24.0);
1572 }
1573
1574 #[test]
1575 fn should_draw_fade_overlay_hides_tiny_fades() {
1576 assert!(!should_draw_fade_overlay(240, 0.0125));
1577 assert!(should_draw_fade_overlay(240, 0.0126));
1578 }
1579}