1use crate::midi::PianoNote;
2use iced::{
3 Color, Event, Point, Rectangle, Renderer, Size, Theme, mouse,
4 widget::canvas::{Action as CanvasAction, Frame, Geometry, Path, Program},
5};
6use std::collections::HashSet;
7
8#[derive(Debug, Clone, PartialEq)]
9pub enum DrumMessage {
10 NoteSelected(usize),
11 ClearSelection,
12 NoteCreate {
13 start_sample: usize,
14 end_sample: usize,
15 pitch: u8,
16 repeat: bool,
17 },
18 NoteDelete(usize),
19 NoteMove {
20 note_index: usize,
21 delta_samples: i64,
22 },
23 AdjustVelocity {
24 note_index: usize,
25 delta: i8,
26 },
27 SelectRectStart {
28 position: Point,
29 },
30 SelectRectDrag {
31 position: Point,
32 },
33 SelectRectEnd,
34}
35
36#[derive(Default, Debug, Clone, Copy, PartialEq)]
37pub enum DraggingMode {
38 #[default]
39 None,
40 SelectingRect,
41 DraggingNote,
42 CreatingNote,
43}
44
45#[derive(Debug)]
46pub struct DrumRollInteraction {
47 pub notes: Vec<PianoNote>,
48 pub pixels_per_sample: f32,
49 pub zoom_x: f32,
50 pub drum_rows: Vec<u8>,
51 pub row_height: f32,
52 pub selecting_rect: Option<(Point, Point)>,
53 pub selected_notes: HashSet<usize>,
54 pub repeat_create: bool,
55}
56
57#[derive(Default, Debug)]
58pub struct DrumRollInteractionState {
59 pub dragging_mode: DraggingMode,
60 pub drag_start: Option<Point>,
61 pub drag_current: Option<Point>,
62 pub drag_note_index: Option<usize>,
63 pub hover_note_index: Option<usize>,
64 pub creating_dragged: bool,
65 pub creating_start_sample: Option<usize>,
66 pub creating_pitch: Option<u8>,
67}
68
69impl DrumRollInteraction {
70 pub fn new(
71 notes: Vec<PianoNote>,
72 pixels_per_sample: f32,
73 zoom_x: f32,
74 drum_rows: Vec<u8>,
75 row_height: f32,
76 selecting_rect: Option<(Point, Point)>,
77 selected_notes: HashSet<usize>,
78 ) -> Self {
79 Self {
80 notes,
81 pixels_per_sample,
82 zoom_x,
83 drum_rows,
84 row_height,
85 selecting_rect,
86 selected_notes,
87 repeat_create: false,
88 }
89 }
90
91 fn note_at_position(&self, position: Point, pps: f32, notes: &[PianoNote]) -> Option<usize> {
92 for (idx, note) in notes.iter().enumerate() {
93 let Some(row_idx) = self.drum_rows.iter().position(|&p| p == note.pitch) else {
94 continue;
95 };
96 let y = row_idx as f32 * self.row_height + 1.0;
97 let x = note.start_sample as f32 * pps;
98 let w = (note.length_samples as f32 * pps).max(2.0);
99 let h = (self.row_height - 2.0).max(2.0);
100 if position.x >= x && position.x <= x + w && position.y >= y && position.y <= y + h {
101 return Some(idx);
102 }
103 }
104 None
105 }
106
107 fn pitch_at_y(&self, y: f32) -> u8 {
108 let row_idx = (y / self.row_height)
109 .floor()
110 .clamp(0.0, (self.drum_rows.len().saturating_sub(1)) as f32)
111 as usize;
112 self.drum_rows.get(row_idx).copied().unwrap_or(60)
113 }
114
115 fn sample_at_x(&self, x: f32, pps: f32) -> usize {
116 (x / pps).max(0.0) as usize
117 }
118
119 fn local_position(bounds: Rectangle, position: Point) -> Point {
120 Point::new(position.x - bounds.x, position.y - bounds.y)
121 }
122}
123
124impl Program<DrumMessage> for DrumRollInteraction {
125 type State = DrumRollInteractionState;
126
127 fn update(
128 &self,
129 state: &mut Self::State,
130 event: &Event,
131 bounds: Rectangle,
132 cursor: mouse::Cursor,
133 ) -> Option<CanvasAction<DrumMessage>> {
134 let pps = (self.pixels_per_sample * self.zoom_x).max(0.0001);
135 let notes = &self.notes;
136
137 match event {
138 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
139 if let Some(position) = cursor.position_in(bounds) {
140 if let Some(note_idx) = self.note_at_position(position, pps, notes) {
141 state.drag_start = Some(position);
142 state.drag_current = Some(position);
143 state.drag_note_index = Some(note_idx);
144 state.dragging_mode = DraggingMode::DraggingNote;
145 return Some(
146 CanvasAction::publish(DrumMessage::NoteSelected(note_idx))
147 .and_capture(),
148 );
149 } else {
150 state.drag_start = Some(position);
151 state.drag_current = Some(position);
152 state.drag_note_index = None;
153 state.dragging_mode = DraggingMode::SelectingRect;
154 return Some(
155 CanvasAction::publish(DrumMessage::SelectRectStart { position })
156 .and_capture(),
157 );
158 }
159 }
160 }
161 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => {
162 if let Some(position) = cursor.position_in(bounds) {
163 state.drag_start = Some(position);
164 state.drag_current = Some(position);
165 state.drag_note_index = None;
166 state.creating_dragged = false;
167 state.creating_start_sample = Some(self.sample_at_x(position.x, pps));
168 state.creating_pitch = Some(self.pitch_at_y(position.y));
169 state.dragging_mode = DraggingMode::CreatingNote;
170 return Some(CanvasAction::request_redraw().and_capture());
171 }
172 }
173 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) => {
174 if let Some(position) = cursor.position_in(bounds)
175 && let Some(note_idx) = self.note_at_position(position, pps, notes)
176 {
177 return Some(
178 CanvasAction::publish(DrumMessage::NoteDelete(note_idx)).and_capture(),
179 );
180 }
181 }
182 Event::Mouse(mouse::Event::CursorMoved { position }) => {
183 let position = Self::local_position(bounds, *position);
184 if state.drag_start.is_some() {
185 state.drag_current = Some(position);
186 }
187 match state.dragging_mode {
188 DraggingMode::SelectingRect => {
189 return Some(CanvasAction::publish(DrumMessage::SelectRectDrag {
190 position,
191 }));
192 }
193 DraggingMode::DraggingNote => {
194 return Some(CanvasAction::request_redraw());
195 }
196 DraggingMode::CreatingNote => {
197 if let Some(drag_start) = state.drag_start
198 && (position.x - drag_start.x).hypot(position.y - drag_start.y) < 3.0
199 {
200 return Some(CanvasAction::request_redraw().and_capture());
201 }
202 state.creating_dragged = true;
203 return Some(CanvasAction::request_redraw().and_capture());
204 }
205 DraggingMode::None => {}
206 }
207 state.hover_note_index = cursor
208 .position_in(bounds)
209 .and_then(|position| self.note_at_position(position, pps, notes));
210 }
211 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
212 let mode = state.dragging_mode;
213
214 match mode {
215 DraggingMode::SelectingRect => {
216 state.drag_start = None;
217 state.drag_current = None;
218 state.drag_note_index = None;
219 state.dragging_mode = DraggingMode::None;
220 return Some(CanvasAction::publish(DrumMessage::SelectRectEnd));
221 }
222 DraggingMode::DraggingNote => {
223 if let (Some(drag_start), Some(note_idx)) =
224 (state.drag_start.take(), state.drag_note_index.take())
225 {
226 state.drag_current = None;
227 state.dragging_mode = DraggingMode::None;
228 if let Some(position) = cursor.position_in(bounds) {
229 let delta_x = position.x - drag_start.x;
230 let delta_samples = (delta_x / pps) as i64;
231 if delta_samples != 0 {
232 return Some(
233 CanvasAction::publish(DrumMessage::NoteMove {
234 note_index: note_idx,
235 delta_samples,
236 })
237 .and_capture(),
238 );
239 }
240 }
241 }
242 }
243 DraggingMode::CreatingNote => {}
244 DraggingMode::None => {}
245 }
246 }
247 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right)) => {
248 if state.dragging_mode == DraggingMode::CreatingNote {
249 let start = state.drag_start.take();
250 let current = state.drag_current.take();
251 let start_sample = state.creating_start_sample;
252 let pitch = state.creating_pitch;
253 state.drag_note_index = None;
254 state.creating_dragged = false;
255 state.creating_start_sample = None;
256 state.creating_pitch = None;
257 state.dragging_mode = DraggingMode::None;
258 if let Some(position) = cursor.position_in(bounds).or(current).or(start) {
259 let Some(start_sample) = start_sample else {
260 return Some(CanvasAction::request_redraw().and_capture());
261 };
262 let Some(pitch) = pitch else {
263 return Some(CanvasAction::request_redraw().and_capture());
264 };
265 let end_sample = self.sample_at_x(position.x, pps);
266 return Some(
267 CanvasAction::publish(DrumMessage::NoteCreate {
268 start_sample,
269 end_sample,
270 pitch,
271 repeat: self.repeat_create,
272 })
273 .and_capture(),
274 );
275 }
276 return Some(CanvasAction::request_redraw().and_capture());
277 }
278 }
279 Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
280 if let Some(position) = cursor.position_in(bounds) {
281 let raw = match delta {
282 mouse::ScrollDelta::Lines { y, .. } => *y,
283 mouse::ScrollDelta::Pixels { y, .. } => *y / 16.0,
284 };
285 let steps = raw.round() as i32;
286 if steps != 0
287 && let Some(note_idx) = self.note_at_position(position, pps, notes)
288 {
289 let delta = steps.clamp(-24, 24) as i8;
290 return Some(
291 CanvasAction::publish(DrumMessage::AdjustVelocity {
292 note_index: note_idx,
293 delta,
294 })
295 .and_capture(),
296 );
297 }
298 }
299 }
300 _ => {}
301 }
302 None
303 }
304
305 fn draw(
306 &self,
307 state: &Self::State,
308 renderer: &Renderer,
309 _theme: &Theme,
310 bounds: Rectangle,
311 cursor: mouse::Cursor,
312 ) -> Vec<Geometry> {
313 let mut frame = Frame::new(renderer, bounds.size());
314
315 if state.dragging_mode == DraggingMode::DraggingNote
316 && let (Some(drag_start), Some(cursor_pos)) =
317 (state.drag_start, cursor.position_in(bounds))
318 {
319 let pps = (self.pixels_per_sample * self.zoom_x).max(0.0001);
320 let delta_x = cursor_pos.x - drag_start.x;
321 for ¬e_idx in &self.selected_notes {
322 if let Some(note) = self.notes.get(note_idx)
323 && let Some(row_idx) = self.drum_rows.iter().position(|&p| p == note.pitch)
324 {
325 let x = note.start_sample as f32 * pps + delta_x;
326 let y = row_idx as f32 * self.row_height + 1.0;
327 let w = (note.length_samples as f32 * pps).max(2.0);
328 let h = (self.row_height - 2.0).max(2.0);
329 frame.fill(
330 &Path::rectangle(Point::new(x, y), Size::new(w, h)),
331 Color::from_rgba(0.9, 0.9, 0.95, 0.35),
332 );
333 }
334 }
335 }
336
337 if self.repeat_create
338 && state.dragging_mode == DraggingMode::CreatingNote
339 && let (Some(start), Some(current), Some(pitch)) =
340 (state.drag_start, state.drag_current, state.creating_pitch)
341 && let Some(row_idx) = self
342 .drum_rows
343 .iter()
344 .position(|&row_pitch| row_pitch == pitch)
345 {
346 let x0 = start.x.min(current.x).max(0.0);
347 let x1 = start.x.max(current.x).max(0.0);
348 let y = row_idx as f32 * self.row_height + 1.0;
349 let w = (x1 - x0).max(2.0);
350 let h = (self.row_height - 2.0).max(2.0);
351 let path = Path::rectangle(Point::new(x0, y), Size::new(w, h));
352 frame.fill(&path, Color::from_rgba(0.3, 0.55, 0.95, 0.28));
353 frame.stroke(
354 &path,
355 iced::widget::canvas::Stroke::default()
356 .with_color(Color::from_rgba(0.5, 0.75, 1.0, 0.9))
357 .with_width(1.5),
358 );
359 }
360
361 if let Some(note_idx) = state.hover_note_index
362 && let Some(note) = self.notes.get(note_idx)
363 && let Some(row_idx) = self.drum_rows.iter().position(|&p| p == note.pitch)
364 {
365 let pps = (self.pixels_per_sample * self.zoom_x).max(0.0001);
366 let x = note.start_sample as f32 * pps;
367 let y = row_idx as f32 * self.row_height + 1.0;
368 let w = (note.length_samples as f32 * pps).max(2.0);
369 let h = (self.row_height - 2.0).max(2.0);
370 frame.stroke(
371 &Path::rectangle(Point::new(x, y), Size::new(w, h)),
372 iced::widget::canvas::Stroke::default()
373 .with_color(Color::from_rgba(1.0, 1.0, 1.0, 0.6))
374 .with_width(1.5),
375 );
376 }
377
378 if let Some((start, end)) = self.selecting_rect {
379 let min_x = start.x.min(end.x);
380 let min_y = start.y.min(end.y);
381 let max_x = start.x.max(end.x);
382 let max_y = start.y.max(end.y);
383
384 let rect = Rectangle {
385 x: min_x,
386 y: min_y,
387 width: max_x - min_x,
388 height: max_y - min_y,
389 };
390
391 frame.fill(
392 &Path::rectangle(
393 Point::new(rect.x, rect.y),
394 Size::new(rect.width, rect.height),
395 ),
396 Color {
397 r: 0.3,
398 g: 0.5,
399 b: 0.8,
400 a: 0.2,
401 },
402 );
403 frame.stroke(
404 &Path::rectangle(
405 Point::new(rect.x, rect.y),
406 Size::new(rect.width, rect.height),
407 ),
408 iced::widget::canvas::Stroke::default()
409 .with_color(Color::from_rgb(0.4, 0.6, 0.9))
410 .with_width(1.5),
411 );
412 }
413
414 vec![frame.into_geometry()]
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421 use crate::midi::PianoNote;
422 use iced::widget::canvas::Program;
423 use iced::{Event, Point, Rectangle, Size, event, mouse};
424 use std::collections::HashSet;
425
426 fn action_message(action: CanvasAction<DrumMessage>) -> (Option<DrumMessage>, event::Status) {
427 let (message, _redraw, status) = action.into_inner();
428 (message, status)
429 }
430
431 fn drum_note(start_sample: usize, pitch: u8) -> PianoNote {
432 PianoNote {
433 start_sample,
434 length_samples: 20,
435 pitch,
436 velocity: 100,
437 channel: 0,
438 mpe: Default::default(),
439 }
440 }
441
442 #[test]
443 fn drum_roll_click_on_note_selects_and_starts_drag() {
444 let interaction = DrumRollInteraction::new(
445 vec![drum_note(10, 38)],
446 1.0,
447 1.0,
448 vec![36, 38],
449 20.0,
450 None,
451 HashSet::new(),
452 );
453 let mut state = DrumRollInteractionState::default();
454 let bounds = Rectangle::new(Point::ORIGIN, Size::new(200.0, 100.0));
455 let cursor = mouse::Cursor::Available(Point::new(15.0, 22.0));
456
457 let action = interaction
458 .update(
459 &mut state,
460 &Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
461 bounds,
462 cursor,
463 )
464 .expect("action");
465
466 let (message, status) = action_message(action);
467 assert_eq!(message, Some(DrumMessage::NoteSelected(0)));
468 assert_eq!(status, event::Status::Captured);
469 assert_eq!(state.dragging_mode, DraggingMode::DraggingNote);
470 assert_eq!(state.drag_note_index, Some(0));
471 }
472
473 #[test]
474 fn drum_roll_drag_release_publishes_move_with_delta() {
475 let interaction = DrumRollInteraction::new(
476 vec![drum_note(10, 38)],
477 1.0,
478 1.0,
479 vec![36, 38],
480 20.0,
481 None,
482 HashSet::new(),
483 );
484 let mut state = DrumRollInteractionState::default();
485 let bounds = Rectangle::new(Point::ORIGIN, Size::new(200.0, 100.0));
486 let press_cursor = mouse::Cursor::Available(Point::new(15.0, 22.0));
487 let release_cursor = mouse::Cursor::Available(Point::new(35.0, 22.0));
488
489 let _ = interaction.update(
490 &mut state,
491 &Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
492 bounds,
493 press_cursor,
494 );
495
496 let action = interaction
497 .update(
498 &mut state,
499 &Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
500 bounds,
501 release_cursor,
502 )
503 .expect("release action");
504
505 let (message, status) = action_message(action);
506 assert_eq!(
507 message,
508 Some(DrumMessage::NoteMove {
509 note_index: 0,
510 delta_samples: 20,
511 })
512 );
513 assert_eq!(status, event::Status::Captured);
514 }
515
516 #[test]
517 fn drum_roll_cursor_moved_while_dragging_requests_redraw() {
518 let interaction = DrumRollInteraction::new(
519 vec![drum_note(10, 38)],
520 1.0,
521 1.0,
522 vec![36, 38],
523 20.0,
524 None,
525 HashSet::new(),
526 );
527 let mut state = DrumRollInteractionState::default();
528 let bounds = Rectangle::new(Point::ORIGIN, Size::new(200.0, 100.0));
529 let press_cursor = mouse::Cursor::Available(Point::new(15.0, 22.0));
530 let drag_cursor = mouse::Cursor::Available(Point::new(35.0, 22.0));
531
532 let _ = interaction.update(
533 &mut state,
534 &Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
535 bounds,
536 press_cursor,
537 );
538
539 let action = interaction
540 .update(
541 &mut state,
542 &Event::Mouse(mouse::Event::CursorMoved {
543 position: Point::new(35.0, 22.0),
544 }),
545 bounds,
546 drag_cursor,
547 )
548 .expect("drag action");
549
550 let (message, _status) = action_message(action);
551 assert!(message.is_none());
552 }
553
554 #[test]
555 fn drum_roll_right_drag_publishes_paint_create_messages() {
556 let interaction = DrumRollInteraction::new(
557 Vec::new(),
558 1.0,
559 1.0,
560 vec![36, 38],
561 20.0,
562 None,
563 HashSet::new(),
564 );
565 let mut state = DrumRollInteractionState::default();
566 let bounds = Rectangle::new(Point::ORIGIN, Size::new(200.0, 100.0));
567 let press_cursor = mouse::Cursor::Available(Point::new(12.0, 2.0));
568 let drag_cursor = mouse::Cursor::Unavailable;
569
570 let press = interaction
571 .update(
572 &mut state,
573 &Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)),
574 bounds,
575 press_cursor,
576 )
577 .expect("press action");
578 let (message, status) = action_message(press);
579 assert_eq!(message, None);
580 assert_eq!(status, event::Status::Captured);
581 assert_eq!(state.dragging_mode, DraggingMode::CreatingNote);
582
583 let drag = interaction
584 .update(
585 &mut state,
586 &Event::Mouse(mouse::Event::CursorMoved {
587 position: Point::new(32.0, 22.0),
588 }),
589 bounds,
590 drag_cursor,
591 )
592 .expect("drag action");
593 let (message, _status) = action_message(drag);
594 assert_eq!(message, None);
595
596 let release_cursor = mouse::Cursor::Available(Point::new(32.0, 22.0));
597 let release = interaction
598 .update(
599 &mut state,
600 &Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right)),
601 bounds,
602 release_cursor,
603 )
604 .expect("release action");
605 let (message, status) = action_message(release);
606 assert_eq!(
607 message,
608 Some(DrumMessage::NoteCreate {
609 start_sample: 12,
610 end_sample: 32,
611 pitch: 36,
612 repeat: false,
613 })
614 );
615 assert_eq!(status, event::Status::Captured);
616 }
617
618 #[test]
619 fn drum_roll_right_click_creates_note_on_release() {
620 let interaction = DrumRollInteraction::new(
621 Vec::new(),
622 1.0,
623 1.0,
624 vec![36, 38],
625 20.0,
626 None,
627 HashSet::new(),
628 );
629 let mut state = DrumRollInteractionState::default();
630 let bounds = Rectangle::new(Point::ORIGIN, Size::new(200.0, 100.0));
631 let cursor = mouse::Cursor::Available(Point::new(12.0, 2.0));
632
633 let _ = interaction.update(
634 &mut state,
635 &Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)),
636 bounds,
637 cursor,
638 );
639
640 let release = interaction
641 .update(
642 &mut state,
643 &Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right)),
644 bounds,
645 cursor,
646 )
647 .expect("release action");
648 let (message, status) = action_message(release);
649 assert_eq!(
650 message,
651 Some(DrumMessage::NoteCreate {
652 start_sample: 12,
653 end_sample: 12,
654 pitch: 36,
655 repeat: false,
656 })
657 );
658 assert_eq!(status, event::Status::Captured);
659 assert_eq!(state.dragging_mode, DraggingMode::None);
660 }
661
662 #[test]
663 fn drum_roll_shift_right_drag_marks_create_as_repeat() {
664 let mut interaction = DrumRollInteraction::new(
665 Vec::new(),
666 1.0,
667 1.0,
668 vec![36, 38],
669 20.0,
670 None,
671 HashSet::new(),
672 );
673 interaction.repeat_create = true;
674 let mut state = DrumRollInteractionState::default();
675 let bounds = Rectangle::new(Point::ORIGIN, Size::new(200.0, 100.0));
676 let press_cursor = mouse::Cursor::Available(Point::new(12.0, 2.0));
677 let release_cursor = mouse::Cursor::Available(Point::new(32.0, 22.0));
678
679 let _ = interaction.update(
680 &mut state,
681 &Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)),
682 bounds,
683 press_cursor,
684 );
685
686 let release = interaction
687 .update(
688 &mut state,
689 &Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right)),
690 bounds,
691 release_cursor,
692 )
693 .expect("release action");
694 let (message, status) = action_message(release);
695 assert_eq!(
696 message,
697 Some(DrumMessage::NoteCreate {
698 start_sample: 12,
699 end_sample: 32,
700 pitch: 36,
701 repeat: true,
702 })
703 );
704 assert_eq!(status, event::Status::Captured);
705 }
706}