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