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: 3.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 ..container::Style::default()
1135 }
1136 });
1137
1138 let clip_widget = container(Stack::with_children(vec![
1139 clip_content.into(),
1140 pin(left_edge_zone).position(Point::new(0.0, 0.0)).into(),
1141 pin(right_edge_zone)
1142 .position(Point::new(self.clip_width - self.resize_handle_width, 0.0))
1143 .into(),
1144 ]))
1145 .width(Length::Fixed(self.clip_width))
1146 .height(Length::Fixed(self.clip_height))
1147 .style(move |_theme| container::Style {
1148 background: None,
1149 border: Border {
1150 color: if self.is_selected {
1151 self.selected_border
1152 } else {
1153 self.border
1154 },
1155 width: if self.is_selected { 2.0 } else { 1.0 },
1156 radius: self.radius.into(),
1157 },
1158 ..container::Style::default()
1159 });
1160
1161 let clip_with_fades: Element<'static, Message> = if self.clip.fade_enabled {
1162 let fade_in_width = visible_fade_overlay_width(
1163 self.clip.fade_in_samples,
1164 self.pixels_per_sample,
1165 );
1166 let fade_out_width = visible_fade_overlay_width(
1167 self.clip.fade_out_samples,
1168 self.pixels_per_sample,
1169 );
1170 let mut stack = Stack::new().push(clip_widget);
1171 if should_draw_fade_overlay(self.clip.fade_in_samples, self.pixels_per_sample) {
1172 if let Some(message) = interaction.fade_in_press.clone() {
1173 let fade_in_handle = mouse_area(
1174 container("")
1175 .width(Length::Fixed(6.0))
1176 .height(Length::Fixed(6.0))
1177 .style(|_theme| container::Style {
1178 background: Some(Background::Color(Color::from_rgba(
1179 1.0, 1.0, 1.0, 0.9,
1180 ))),
1181 border: Border {
1182 color: Color::from_rgba(0.3, 0.3, 0.3, 1.0),
1183 width: 1.0,
1184 radius: 3.0.into(),
1185 },
1186 ..container::Style::default()
1187 }),
1188 )
1189 .on_press(message);
1190 stack = stack.push(
1191 pin(fade_in_handle).position(Point::new(fade_in_width - 3.0, -3.0)),
1192 );
1193 }
1194 stack = stack.push(
1195 pin(fade_bezier_overlay(
1196 fade_in_width,
1197 self.clip_height,
1198 Color::from_rgba(0.0, 0.0, 0.0, 0.3),
1199 false,
1200 ))
1201 .position(Point::new(0.0, 0.0)),
1202 );
1203 }
1204 if should_draw_fade_overlay(self.clip.fade_out_samples, self.pixels_per_sample)
1205 {
1206 if let Some(message) = interaction.fade_out_press.clone() {
1207 let fade_out_handle = mouse_area(
1208 container("")
1209 .width(Length::Fixed(6.0))
1210 .height(Length::Fixed(6.0))
1211 .style(|_theme| container::Style {
1212 background: Some(Background::Color(Color::from_rgba(
1213 1.0, 1.0, 1.0, 0.9,
1214 ))),
1215 border: Border {
1216 color: Color::from_rgba(0.3, 0.3, 0.3, 1.0),
1217 width: 1.0,
1218 radius: 3.0.into(),
1219 },
1220 ..container::Style::default()
1221 }),
1222 )
1223 .on_press(message);
1224 stack = stack.push(pin(fade_out_handle).position(Point::new(
1225 self.clip_width - fade_out_width - 3.0,
1226 -3.0,
1227 )));
1228 }
1229 stack = stack.push(
1230 pin(fade_bezier_overlay(
1231 fade_out_width,
1232 self.clip_height,
1233 Color::from_rgba(0.0, 0.0, 0.0, 0.3),
1234 true,
1235 ))
1236 .position(Point::new(self.clip_width - fade_out_width, 0.0)),
1237 );
1238 }
1239 stack.into()
1240 } else {
1241 clip_widget.into()
1242 };
1243
1244 let base = mouse_area(clip_with_fades);
1245 let base = if self.left_handle_hovered || self.right_handle_hovered {
1246 base.interaction(mouse::Interaction::ResizingColumn)
1247 } else {
1248 base
1249 };
1250 let base = base
1251 .on_press(interaction.on_select)
1252 .on_double_click(interaction.on_open);
1253 if let Some(on_drag) = interaction.on_drag {
1254 base.on_move(move |point| on_drag(point)).into()
1255 } else {
1256 base.into()
1257 }
1258 }
1259 }
1260 }
1261}
1262
1263#[derive(Clone, Copy)]
1264enum MIDIClipMode {
1265 Widget,
1266 Preview,
1267}
1268
1269pub struct MIDIClip<Message> {
1270 clip: MIDIClipData,
1271 clip_width: f32,
1272 clip_height: f32,
1273 label: String,
1274 is_selected: bool,
1275 left_handle_hovered: bool,
1276 right_handle_hovered: bool,
1277 midi_notes: Option<Arc<Vec<PianoNote>>>,
1278 interaction: Option<MIDIClipInteraction<Message>>,
1279 background: Option<Background>,
1280 border_color: Option<Color>,
1281 radius: f32,
1282 mode: MIDIClipMode,
1283 base_color: Color,
1284 selected_base_color: Color,
1285 border: Color,
1286 selected_border: Color,
1287 resize_handle_width: f32,
1288}
1289
1290impl<Message> MIDIClip<Message> {
1291 pub fn clean_name(name: &str) -> String {
1292 clean_clip_name(name)
1293 }
1294
1295 pub fn label_for_width(label: &str, width_px: f32) -> String {
1296 trim_label_to_width(label, width_px)
1297 }
1298
1299 pub fn two_edge_gradient(
1300 base: Color,
1301 muted_alpha: f32,
1302 normal_alpha: f32,
1303 reverse: bool,
1304 ) -> Background {
1305 clip_two_edge_gradient(base, muted_alpha, normal_alpha, reverse)
1306 }
1307}
1308
1309impl<Message: Clone + 'static> MIDIClip<Message> {
1310 pub fn new(clip: MIDIClipData) -> Self {
1311 Self {
1312 clip,
1313 clip_width: 12.0,
1314 clip_height: 8.0,
1315 label: String::new(),
1316 is_selected: false,
1317 left_handle_hovered: false,
1318 right_handle_hovered: false,
1319 midi_notes: None,
1320 interaction: None,
1321 background: None,
1322 border_color: None,
1323 radius: 8.0,
1324 mode: MIDIClipMode::Widget,
1325 base_color: Color::from_rgb8(55, 90, 50),
1326 selected_base_color: Color::from_rgb8(84, 133, 72),
1327 border: Color::from_rgb8(148, 215, 118),
1328 selected_border: Color::from_rgb8(196, 255, 151),
1329 resize_handle_width: DEFAULT_RESIZE_HANDLE_WIDTH,
1330 }
1331 }
1332
1333 pub fn with_colors(
1334 mut self,
1335 base_color: Color,
1336 selected_base_color: Color,
1337 border: Color,
1338 selected_border: Color,
1339 ) -> Self {
1340 self.base_color = base_color;
1341 self.selected_base_color = selected_base_color;
1342 self.border = border;
1343 self.selected_border = selected_border;
1344 self
1345 }
1346
1347 pub fn with_size(mut self, clip_width: f32, clip_height: f32) -> Self {
1348 self.clip_width = clip_width;
1349 self.clip_height = clip_height;
1350 self
1351 }
1352
1353 pub fn with_label(mut self, label: String) -> Self {
1354 self.label = label;
1355 self
1356 }
1357
1358 pub fn selected(mut self, is_selected: bool) -> Self {
1359 self.is_selected = is_selected;
1360 self
1361 }
1362
1363 pub fn hovered_handles(mut self, left: bool, right: bool) -> Self {
1364 self.left_handle_hovered = left;
1365 self.right_handle_hovered = right;
1366 self
1367 }
1368
1369 pub fn with_notes(mut self, midi_notes: Option<Arc<Vec<PianoNote>>>) -> Self {
1370 self.midi_notes = midi_notes;
1371 self
1372 }
1373
1374 pub fn interactive(mut self, interaction: MIDIClipInteraction<Message>) -> Self {
1375 self.interaction = Some(interaction);
1376 self.mode = MIDIClipMode::Widget;
1377 self
1378 }
1379
1380 pub fn preview(mut self, background: Background, border_color: Color, radius: f32) -> Self {
1381 self.background = Some(background);
1382 self.border_color = Some(border_color);
1383 self.radius = radius;
1384 self.mode = MIDIClipMode::Preview;
1385 self
1386 }
1387
1388 pub fn into_element(self) -> Element<'static, Message> {
1389 match self.mode {
1390 MIDIClipMode::Preview => {
1391 let mut preview_layers = Vec::with_capacity(2);
1392 if let Some(notes) = self.midi_notes {
1393 preview_layers.push(midi_clip_notes_overlay(
1394 notes,
1395 self.clip.offset,
1396 self.clip.length.max(1),
1397 ));
1398 }
1399 preview_layers.push(clip_label_overlay(self.label));
1400 let preview_content = container(Stack::with_children(preview_layers))
1401 .width(Length::Fill)
1402 .height(Length::Fill)
1403 .padding(0)
1404 .style(move |_theme| container::Style {
1405 background: self.background,
1406 ..container::Style::default()
1407 });
1408 container(preview_content)
1409 .width(Length::Fixed(self.clip_width))
1410 .height(Length::Fixed(self.clip_height))
1411 .style(move |_theme| container::Style {
1412 background: None,
1413 border: Border {
1414 color: self.border_color.unwrap_or(Color::TRANSPARENT),
1415 width: 2.0,
1416 radius: self.radius.into(),
1417 },
1418 ..container::Style::default()
1419 })
1420 .into()
1421 }
1422 MIDIClipMode::Widget => {
1423 let interaction = self.interaction.expect("midi clip interaction");
1424 let left_edge_zone = mouse_area(
1425 Space::new()
1426 .width(Length::Fixed(self.resize_handle_width))
1427 .height(Length::Fill),
1428 )
1429 .interaction(mouse::Interaction::ResizingColumn)
1430 .on_enter(interaction.edges.left_hover_enter.clone())
1431 .on_exit(interaction.edges.left_hover_exit.clone())
1432 .on_press(interaction.edges.left_press.clone());
1433 let right_edge_zone = mouse_area(
1434 Space::new()
1435 .width(Length::Fixed(self.resize_handle_width))
1436 .height(Length::Fill),
1437 )
1438 .interaction(mouse::Interaction::ResizingColumn)
1439 .on_enter(interaction.edges.right_hover_enter.clone())
1440 .on_exit(interaction.edges.right_hover_exit.clone())
1441 .on_press(interaction.edges.right_press.clone());
1442
1443 let mut clip_layers = Vec::with_capacity(2);
1444 if let Some(notes) = self.midi_notes {
1445 clip_layers.push(midi_clip_notes_overlay(
1446 notes,
1447 self.clip.offset,
1448 self.clip.length.max(1),
1449 ));
1450 }
1451 clip_layers.push(clip_label_overlay(self.label));
1452
1453 let clip_muted = self.clip.muted;
1454 let clip_widget = container(Stack::with_children(vec![
1455 container(Stack::with_children(clip_layers))
1456 .width(Length::Fill)
1457 .height(Length::Fill)
1458 .padding(0)
1459 .style(move |_theme| {
1460 let base = if self.is_selected {
1461 self.selected_base_color
1462 } else {
1463 self.base_color
1464 };
1465 let (muted_alpha, normal_alpha) = if clip_muted {
1466 (0.42, 0.42)
1467 } else {
1468 (0.92, 0.92)
1469 };
1470 container::Style {
1471 background: Some(clip_two_edge_gradient(
1472 base,
1473 muted_alpha,
1474 normal_alpha,
1475 false,
1476 )),
1477 ..container::Style::default()
1478 }
1479 })
1480 .into(),
1481 pin(left_edge_zone).position(Point::new(0.0, 0.0)).into(),
1482 pin(right_edge_zone)
1483 .position(Point::new(self.clip_width - self.resize_handle_width, 0.0))
1484 .into(),
1485 ]))
1486 .width(Length::Fixed(self.clip_width))
1487 .height(Length::Fixed(self.clip_height))
1488 .style(move |_theme| container::Style {
1489 background: None,
1490 border: Border {
1491 color: if self.is_selected {
1492 self.selected_border
1493 } else {
1494 self.border
1495 },
1496 width: if self.is_selected { 2.2 } else { 1.4 },
1497 radius: self.radius.into(),
1498 },
1499 ..container::Style::default()
1500 });
1501
1502 let base = mouse_area(clip_widget);
1503 let base = if self.left_handle_hovered || self.right_handle_hovered {
1504 base.interaction(mouse::Interaction::ResizingColumn)
1505 } else {
1506 base
1507 };
1508 let base = base
1509 .on_press(interaction.on_select)
1510 .on_double_click(interaction.on_open);
1511 if let Some(on_drag) = interaction.on_drag {
1512 base.on_move(move |point| on_drag(point)).into()
1513 } else {
1514 base.into()
1515 }
1516 }
1517 }
1518 }
1519}
1520
1521#[cfg(test)]
1522mod tests {
1523 use super::{should_draw_fade_overlay, visible_fade_overlay_width};
1524
1525 #[test]
1526 fn visible_fade_overlay_width_grows_with_zoom_below_full_size() {
1527 let low_zoom = visible_fade_overlay_width(240, 0.01);
1528 let higher_zoom = visible_fade_overlay_width(240, 0.02);
1529
1530 assert!(higher_zoom > low_zoom);
1531 assert!((low_zoom - 2.4).abs() < 1.0e-5);
1532 }
1533
1534 #[test]
1535 fn visible_fade_overlay_width_matches_actual_size_once_large_enough() {
1536 let width = visible_fade_overlay_width(240, 0.1);
1537 assert_eq!(width, 24.0);
1538 }
1539
1540 #[test]
1541 fn should_draw_fade_overlay_hides_tiny_fades() {
1542 assert!(!should_draw_fade_overlay(240, 0.0125));
1543 assert!(should_draw_fade_overlay(240, 0.0126));
1544 }
1545}