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