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