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