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}