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