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}