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}