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}
25
26#[derive(Debug)]
27pub struct DrumRollInteraction {
28    pub notes: Vec<PianoNote>,
29    pub pixels_per_sample: f32,
30    pub zoom_x: f32,
31    pub drum_rows: Vec<u8>,
32    pub row_height: f32,
33}
34
35#[derive(Default, Debug)]
36pub struct DrumRollInteractionState {
37    pub drag_start: Option<Point>,
38    pub drag_note_index: Option<usize>,
39    pub hover_note_index: Option<usize>,
40}
41
42impl DrumRollInteraction {
43    pub fn new(
44        notes: Vec<PianoNote>,
45        pixels_per_sample: f32,
46        zoom_x: f32,
47        drum_rows: Vec<u8>,
48        row_height: f32,
49    ) -> Self {
50        Self {
51            notes,
52            pixels_per_sample,
53            zoom_x,
54            drum_rows,
55            row_height,
56        }
57    }
58
59    fn note_at_position(&self, position: Point, pps: f32, notes: &[PianoNote]) -> Option<usize> {
60        for (idx, note) in notes.iter().enumerate() {
61            let Some(row_idx) = self.drum_rows.iter().position(|&p| p == note.pitch) else {
62                continue;
63            };
64            let y = row_idx as f32 * self.row_height + 1.0;
65            let x = note.start_sample as f32 * pps;
66            let w = (note.length_samples as f32 * pps).max(2.0);
67            let h = (self.row_height - 2.0).max(2.0);
68            if position.x >= x && position.x <= x + w && position.y >= y && position.y <= y + h {
69                return Some(idx);
70            }
71        }
72        None
73    }
74
75    fn pitch_at_y(&self, y: f32) -> u8 {
76        let row_idx = (y / self.row_height)
77            .floor()
78            .clamp(0.0, (self.drum_rows.len().saturating_sub(1)) as f32)
79            as usize;
80        self.drum_rows.get(row_idx).copied().unwrap_or(60)
81    }
82
83    fn sample_at_x(&self, x: f32, pps: f32) -> usize {
84        (x / pps).max(0.0) as usize
85    }
86}
87
88impl Program<DrumMessage> for DrumRollInteraction {
89    type State = DrumRollInteractionState;
90
91    fn update(
92        &self,
93        state: &mut Self::State,
94        event: &Event,
95        bounds: Rectangle,
96        cursor: mouse::Cursor,
97    ) -> Option<CanvasAction<DrumMessage>> {
98        let pps = (self.pixels_per_sample * self.zoom_x).max(0.0001);
99        let notes = &self.notes;
100
101        match event {
102            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
103                if let Some(position) = cursor.position_in(bounds) {
104                    if let Some(note_idx) = self.note_at_position(position, pps, notes) {
105                        state.drag_start = Some(position);
106                        state.drag_note_index = Some(note_idx);
107                        return Some(
108                            CanvasAction::publish(DrumMessage::NoteSelected(note_idx))
109                                .and_capture(),
110                        );
111                    } else {
112                        state.drag_start = None;
113                        state.drag_note_index = None;
114                        return Some(
115                            CanvasAction::publish(DrumMessage::ClearSelection).and_capture(),
116                        );
117                    }
118                }
119            }
120            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => {
121                if let Some(position) = cursor.position_in(bounds) {
122                    let pitch = self.pitch_at_y(position.y);
123                    let start_sample = self.sample_at_x(position.x, pps);
124                    return Some(
125                        CanvasAction::publish(DrumMessage::NoteCreate {
126                            start_sample,
127                            pitch,
128                        })
129                        .and_capture(),
130                    );
131                }
132            }
133            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) => {
134                if let Some(position) = cursor.position_in(bounds)
135                    && let Some(note_idx) = self.note_at_position(position, pps, notes)
136                {
137                    return Some(
138                        CanvasAction::publish(DrumMessage::NoteDelete(note_idx)).and_capture(),
139                    );
140                }
141            }
142            Event::Mouse(mouse::Event::CursorMoved { .. }) => {
143                if let Some(position) = cursor.position_in(bounds) {
144                    if state.drag_start.is_some() && state.drag_note_index.is_some() {
145                        return Some(CanvasAction::request_redraw());
146                    }
147                    state.hover_note_index = self.note_at_position(position, pps, notes);
148                }
149            }
150            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
151                if let (Some(drag_start), Some(note_idx)) =
152                    (state.drag_start.take(), state.drag_note_index.take())
153                    && let Some(position) = cursor.position_in(bounds)
154                {
155                    let delta_x = position.x - drag_start.x;
156                    let delta_samples = (delta_x / pps) as i64;
157                    if delta_samples != 0 {
158                        return Some(
159                            CanvasAction::publish(DrumMessage::NoteMove {
160                                note_index: note_idx,
161                                delta_samples,
162                            })
163                            .and_capture(),
164                        );
165                    }
166                }
167            }
168            Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
169                if let Some(position) = cursor.position_in(bounds) {
170                    let raw = match delta {
171                        mouse::ScrollDelta::Lines { y, .. } => *y,
172                        mouse::ScrollDelta::Pixels { y, .. } => *y / 16.0,
173                    };
174                    let steps = raw.round() as i32;
175                    if steps != 0
176                        && let Some(note_idx) = self.note_at_position(position, pps, notes)
177                    {
178                        let delta = steps.clamp(-24, 24) as i8;
179                        return Some(
180                            CanvasAction::publish(DrumMessage::AdjustVelocity {
181                                note_index: note_idx,
182                                delta,
183                            })
184                            .and_capture(),
185                        );
186                    }
187                }
188            }
189            _ => {}
190        }
191        None
192    }
193
194    fn draw(
195        &self,
196        state: &Self::State,
197        renderer: &Renderer,
198        _theme: &Theme,
199        bounds: Rectangle,
200        cursor: mouse::Cursor,
201    ) -> Vec<Geometry> {
202        let mut frame = Frame::new(renderer, bounds.size());
203
204        if let (Some(drag_start), Some(note_idx)) = (state.drag_start, state.drag_note_index)
205            && let Some(note) = self.notes.get(note_idx)
206            && let Some(row_idx) = self.drum_rows.iter().position(|&p| p == note.pitch)
207        {
208            let cursor_pos = if let Some(pos) = cursor.position_in(bounds) {
209                pos
210            } else {
211                return vec![frame.into_geometry()];
212            };
213            let pps = (self.pixels_per_sample * self.zoom_x).max(0.0001);
214            let delta_x = cursor_pos.x - drag_start.x;
215            let x = note.start_sample as f32 * pps + delta_x;
216            let y = row_idx as f32 * self.row_height + 1.0;
217            let w = (note.length_samples as f32 * pps).max(2.0);
218            let h = (self.row_height - 2.0).max(2.0);
219            frame.fill(
220                &Path::rectangle(Point::new(x, y), Size::new(w, h)),
221                Color::from_rgba(0.9, 0.9, 0.95, 0.35),
222            );
223        }
224
225        if let Some(note_idx) = state.hover_note_index
226            && let Some(note) = self.notes.get(note_idx)
227            && let Some(row_idx) = self.drum_rows.iter().position(|&p| p == note.pitch)
228        {
229            let pps = (self.pixels_per_sample * self.zoom_x).max(0.0001);
230            let x = note.start_sample as f32 * pps;
231            let y = row_idx as f32 * self.row_height + 1.0;
232            let w = (note.length_samples as f32 * pps).max(2.0);
233            let h = (self.row_height - 2.0).max(2.0);
234            frame.stroke(
235                &Path::rectangle(Point::new(x, y), Size::new(w, h)),
236                iced::widget::canvas::Stroke::default()
237                    .with_color(Color::from_rgba(1.0, 1.0, 1.0, 0.6))
238                    .with_width(1.5),
239            );
240        }
241
242        vec![frame.into_geometry()]
243    }
244}