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};
6
7#[derive(Debug, Clone)]
8pub enum DrumMessage {
9    NoteSelected(usize),
10    ClearSelection,
11    NoteCreate {
12        start_sample: usize,
13        pitch: u8,
14    },
15    NoteDelete(usize),
16    NoteMove {
17        note_index: usize,
18        delta_samples: i64,
19    },
20    AdjustVelocity {
21        note_index: usize,
22        delta: i8,
23    },
24    SelectRectStart {
25        position: Point,
26    },
27    SelectRectDrag {
28        position: Point,
29    },
30    SelectRectEnd,
31}
32
33#[derive(Default, Debug, Clone, Copy, PartialEq)]
34pub enum DraggingMode {
35    #[default]
36    None,
37    SelectingRect,
38    DraggingNote,
39}
40
41#[derive(Debug)]
42pub struct DrumRollInteraction {
43    pub notes: Vec<PianoNote>,
44    pub pixels_per_sample: f32,
45    pub zoom_x: f32,
46    pub drum_rows: Vec<u8>,
47    pub row_height: f32,
48    pub selecting_rect: Option<(Point, Point)>,
49}
50
51#[derive(Default, Debug)]
52pub struct DrumRollInteractionState {
53    pub dragging_mode: DraggingMode,
54    pub drag_start: Option<Point>,
55    pub drag_note_index: Option<usize>,
56    pub hover_note_index: Option<usize>,
57}
58
59impl DrumRollInteraction {
60    pub fn new(
61        notes: Vec<PianoNote>,
62        pixels_per_sample: f32,
63        zoom_x: f32,
64        drum_rows: Vec<u8>,
65        row_height: f32,
66        selecting_rect: Option<(Point, Point)>,
67    ) -> Self {
68        Self {
69            notes,
70            pixels_per_sample,
71            zoom_x,
72            drum_rows,
73            row_height,
74            selecting_rect,
75        }
76    }
77
78    fn note_at_position(&self, position: Point, pps: f32, notes: &[PianoNote]) -> Option<usize> {
79        for (idx, note) in notes.iter().enumerate() {
80            let Some(row_idx) = self.drum_rows.iter().position(|&p| p == note.pitch) else {
81                continue;
82            };
83            let y = row_idx as f32 * self.row_height + 1.0;
84            let x = note.start_sample as f32 * pps;
85            let w = (note.length_samples as f32 * pps).max(2.0);
86            let h = (self.row_height - 2.0).max(2.0);
87            if position.x >= x && position.x <= x + w && position.y >= y && position.y <= y + h {
88                return Some(idx);
89            }
90        }
91        None
92    }
93
94    fn pitch_at_y(&self, y: f32) -> u8 {
95        let row_idx = (y / self.row_height)
96            .floor()
97            .clamp(0.0, (self.drum_rows.len().saturating_sub(1)) as f32)
98            as usize;
99        self.drum_rows.get(row_idx).copied().unwrap_or(60)
100    }
101
102    fn sample_at_x(&self, x: f32, pps: f32) -> usize {
103        (x / pps).max(0.0) as usize
104    }
105}
106
107impl Program<DrumMessage> for DrumRollInteraction {
108    type State = DrumRollInteractionState;
109
110    fn update(
111        &self,
112        state: &mut Self::State,
113        event: &Event,
114        bounds: Rectangle,
115        cursor: mouse::Cursor,
116    ) -> Option<CanvasAction<DrumMessage>> {
117        let pps = (self.pixels_per_sample * self.zoom_x).max(0.0001);
118        let notes = &self.notes;
119
120        match event {
121            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
122                if let Some(position) = cursor.position_in(bounds) {
123                    if let Some(note_idx) = self.note_at_position(position, pps, notes) {
124                        state.drag_start = Some(position);
125                        state.drag_note_index = Some(note_idx);
126                        state.dragging_mode = DraggingMode::DraggingNote;
127                        return Some(
128                            CanvasAction::publish(DrumMessage::NoteSelected(note_idx))
129                                .and_capture(),
130                        );
131                    } else {
132                        state.drag_start = Some(position);
133                        state.drag_note_index = None;
134                        state.dragging_mode = DraggingMode::SelectingRect;
135                        return Some(
136                            CanvasAction::publish(DrumMessage::SelectRectStart { position })
137                                .and_capture(),
138                        );
139                    }
140                }
141            }
142            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => {
143                if let Some(position) = cursor.position_in(bounds) {
144                    let pitch = self.pitch_at_y(position.y);
145                    let start_sample = self.sample_at_x(position.x, pps);
146                    return Some(
147                        CanvasAction::publish(DrumMessage::NoteCreate {
148                            start_sample,
149                            pitch,
150                        })
151                        .and_capture(),
152                    );
153                }
154            }
155            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) => {
156                if let Some(position) = cursor.position_in(bounds)
157                    && let Some(note_idx) = self.note_at_position(position, pps, notes)
158                {
159                    return Some(
160                        CanvasAction::publish(DrumMessage::NoteDelete(note_idx)).and_capture(),
161                    );
162                }
163            }
164            Event::Mouse(mouse::Event::CursorMoved { .. }) => {
165                if let Some(position) = cursor.position_in(bounds) {
166                    match state.dragging_mode {
167                        DraggingMode::SelectingRect => {
168                            return Some(CanvasAction::publish(DrumMessage::SelectRectDrag {
169                                position,
170                            }));
171                        }
172                        DraggingMode::DraggingNote => {
173                            return Some(CanvasAction::request_redraw());
174                        }
175                        DraggingMode::None => {}
176                    }
177                    state.hover_note_index = self.note_at_position(position, pps, notes);
178                }
179            }
180            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
181                let mode = state.dragging_mode;
182
183                match mode {
184                    DraggingMode::SelectingRect => {
185                        state.drag_start = None;
186                        state.drag_note_index = None;
187                        state.dragging_mode = DraggingMode::None;
188                        return Some(CanvasAction::publish(DrumMessage::SelectRectEnd));
189                    }
190                    DraggingMode::DraggingNote => {
191                        if let (Some(drag_start), Some(note_idx)) =
192                            (state.drag_start.take(), state.drag_note_index.take())
193                        {
194                            state.dragging_mode = DraggingMode::None;
195                            if let Some(position) = cursor.position_in(bounds) {
196                                let delta_x = position.x - drag_start.x;
197                                let delta_samples = (delta_x / pps) as i64;
198                                if delta_samples != 0 {
199                                    return Some(
200                                        CanvasAction::publish(DrumMessage::NoteMove {
201                                            note_index: note_idx,
202                                            delta_samples,
203                                        })
204                                        .and_capture(),
205                                    );
206                                }
207                            }
208                        }
209                    }
210                    DraggingMode::None => {}
211                }
212            }
213            Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
214                if let Some(position) = cursor.position_in(bounds) {
215                    let raw = match delta {
216                        mouse::ScrollDelta::Lines { y, .. } => *y,
217                        mouse::ScrollDelta::Pixels { y, .. } => *y / 16.0,
218                    };
219                    let steps = raw.round() as i32;
220                    if steps != 0
221                        && let Some(note_idx) = self.note_at_position(position, pps, notes)
222                    {
223                        let delta = steps.clamp(-24, 24) as i8;
224                        return Some(
225                            CanvasAction::publish(DrumMessage::AdjustVelocity {
226                                note_index: note_idx,
227                                delta,
228                            })
229                            .and_capture(),
230                        );
231                    }
232                }
233            }
234            _ => {}
235        }
236        None
237    }
238
239    fn draw(
240        &self,
241        state: &Self::State,
242        renderer: &Renderer,
243        _theme: &Theme,
244        bounds: Rectangle,
245        cursor: mouse::Cursor,
246    ) -> Vec<Geometry> {
247        let mut frame = Frame::new(renderer, bounds.size());
248
249        if let (Some(drag_start), Some(note_idx)) = (state.drag_start, state.drag_note_index)
250            && state.dragging_mode == DraggingMode::DraggingNote
251            && let Some(note) = self.notes.get(note_idx)
252            && let Some(row_idx) = self.drum_rows.iter().position(|&p| p == note.pitch)
253        {
254            let cursor_pos = if let Some(pos) = cursor.position_in(bounds) {
255                pos
256            } else {
257                return vec![frame.into_geometry()];
258            };
259            let pps = (self.pixels_per_sample * self.zoom_x).max(0.0001);
260            let delta_x = cursor_pos.x - drag_start.x;
261            let x = note.start_sample as f32 * pps + delta_x;
262            let y = row_idx as f32 * self.row_height + 1.0;
263            let w = (note.length_samples as f32 * pps).max(2.0);
264            let h = (self.row_height - 2.0).max(2.0);
265            frame.fill(
266                &Path::rectangle(Point::new(x, y), Size::new(w, h)),
267                Color::from_rgba(0.9, 0.9, 0.95, 0.35),
268            );
269        }
270
271        if let Some(note_idx) = state.hover_note_index
272            && let Some(note) = self.notes.get(note_idx)
273            && let Some(row_idx) = self.drum_rows.iter().position(|&p| p == note.pitch)
274        {
275            let pps = (self.pixels_per_sample * self.zoom_x).max(0.0001);
276            let x = note.start_sample as f32 * pps;
277            let y = row_idx as f32 * self.row_height + 1.0;
278            let w = (note.length_samples as f32 * pps).max(2.0);
279            let h = (self.row_height - 2.0).max(2.0);
280            frame.stroke(
281                &Path::rectangle(Point::new(x, y), Size::new(w, h)),
282                iced::widget::canvas::Stroke::default()
283                    .with_color(Color::from_rgba(1.0, 1.0, 1.0, 0.6))
284                    .with_width(1.5),
285            );
286        }
287
288        if let Some((start, end)) = self.selecting_rect {
289            let min_x = start.x.min(end.x);
290            let min_y = start.y.min(end.y);
291            let max_x = start.x.max(end.x);
292            let max_y = start.y.max(end.y);
293
294            let rect = Rectangle {
295                x: min_x,
296                y: min_y,
297                width: max_x - min_x,
298                height: max_y - min_y,
299            };
300
301            frame.fill(
302                &Path::rectangle(
303                    Point::new(rect.x, rect.y),
304                    Size::new(rect.width, rect.height),
305                ),
306                Color {
307                    r: 0.3,
308                    g: 0.5,
309                    b: 0.8,
310                    a: 0.2,
311                },
312            );
313            frame.stroke(
314                &Path::rectangle(
315                    Point::new(rect.x, rect.y),
316                    Size::new(rect.width, rect.height),
317                ),
318                iced::widget::canvas::Stroke::default()
319                    .with_color(Color::from_rgb(0.4, 0.6, 0.9))
320                    .with_width(1.5),
321            );
322        }
323
324        vec![frame.into_geometry()]
325    }
326}