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