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