Skip to main content

maolan_widgets/
drum.rs

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