1use crate::midi::{MIDI_NOTE_COUNT, NOTES_PER_OCTAVE, WHITE_KEY_HEIGHT, WHITE_KEYS_PER_OCTAVE};
2use iced::{
3 Background, Color, Event, Point, Rectangle, Renderer, Size, Theme, gradient, mouse,
4 widget::canvas::{self, Action as CanvasAction, Frame, Geometry, Path, Program},
5};
6use std::collections::{HashMap, HashSet};
7
8pub fn is_black_key(pitch: u8) -> bool {
9 matches!(pitch % 12, 1 | 3 | 6 | 8 | 10)
10}
11
12pub fn note_color(velocity: u8, channel: u8) -> Color {
13 let t = (velocity as f32 / 127.0).clamp(0.0, 1.0);
14 let c = (channel as f32 / 15.0).clamp(0.0, 1.0);
15 Color {
16 r: 0.25 + 0.45 * t,
17 g: 0.35 + 0.4 * (1.0 - c),
18 b: 0.65 + 0.3 * c,
19 a: 0.9,
20 }
21}
22
23pub fn brighten(color: Color, amount: f32) -> Color {
24 Color {
25 r: (color.r + amount).min(1.0),
26 g: (color.g + amount).min(1.0),
27 b: (color.b + amount).min(1.0),
28 a: color.a,
29 }
30}
31
32pub fn darken(color: Color, amount: f32) -> Color {
33 Color {
34 r: (color.r - amount).max(0.0),
35 g: (color.g - amount).max(0.0),
36 b: (color.b - amount).max(0.0),
37 a: color.a,
38 }
39}
40
41pub fn note_two_edge_gradient(base: Color) -> Background {
42 let edge = brighten(base, 0.08);
43 let middle = darken(base, 0.08);
44 Background::Gradient(
45 gradient::Linear::new(0.0)
46 .add_stop(0.0, edge)
47 .add_stop(0.5, middle)
48 .add_stop(1.0, edge)
49 .into(),
50 )
51}
52
53pub fn octave_note_count(octave: u8) -> u8 {
54 let start = usize::from(octave) * NOTES_PER_OCTAVE;
55 if start >= MIDI_NOTE_COUNT {
56 0
57 } else {
58 (MIDI_NOTE_COUNT - start).min(NOTES_PER_OCTAVE) as u8
59 }
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
64pub enum Orientation {
65 #[default]
67 Degree0,
68 Degree90,
70 Degree180,
72 Degree270,
74}
75
76const BLACK_KEY_DEPTH_RATIO: f32 = 0.5;
81
82const BLACK_KEY_LENGTH_RATIO: f32 = 0.6;
84
85fn velocity_from_v(v: f32, depth: f32) -> u8 {
86 let normalized = (v / depth).clamp(0.0, 1.0);
87 (normalized * 126.0).round() as u8 + 1
88}
89
90pub fn note_at_in_range(
96 cursor: Point,
97 bounds: Rectangle,
98 orientation: Orientation,
99 note_count: u8,
100) -> Option<(u8, u8)> {
101 if note_count == 0 {
102 return None;
103 }
104 if orientation == Orientation::Degree270 {
105 return note_at_in_range_degree270(cursor, bounds, note_count);
106 }
107 let natural_cursor = to_natural_cursor(orientation, bounds, cursor);
108 let natural = natural_bounds(orientation, bounds);
109 let height = natural.height;
110 let width = natural.width;
111 let white_notes: &[u8] = if note_count >= 12 {
112 &[0, 2, 4, 5, 7, 9, 11]
113 } else {
114 &[0, 2, 4, 5, 7]
115 };
116 let white_key_count = white_notes.iter().take_while(|&n| *n < note_count).count() as f32;
117 if white_key_count == 0.0 {
118 return None;
119 }
120 let white_key_size = height / white_key_count;
121 let black_key_width = width * BLACK_KEY_DEPTH_RATIO;
122 let black_key_length = white_key_size * BLACK_KEY_LENGTH_RATIO;
123 let u = natural_cursor.x.clamp(0.0, height);
124 let v = natural_cursor.y;
125
126 let (black_offsets, black_notes): (&[u8], &[u8]) = if note_count >= 12 {
127 (&[1, 2, 4, 5, 6], &[1, 3, 6, 8, 10])
128 } else {
129 (&[1, 2, 4], &[1, 3, 6])
130 };
131
132 if v <= black_key_width {
133 for (idx, ¬e_id) in black_notes
134 .iter()
135 .enumerate()
136 .take_while(|&(_, n)| *n < note_count)
137 {
138 let center = black_offsets[idx] as f32 * white_key_size;
139 if (u - center).abs() <= black_key_length * 0.5 {
140 let velocity = velocity_from_v(v, black_key_width);
141 return Some((note_id, velocity));
142 }
143 }
144 }
145
146 let index = (u / white_key_size)
147 .clamp(0.0, white_key_count - 1.0)
148 .floor() as usize;
149 let note_id = white_notes.get(index).copied()?;
150 let velocity = velocity_from_v(v, width);
151 Some((note_id, velocity))
152}
153
154fn note_at_in_range_degree270(
155 cursor: Point,
156 bounds: Rectangle,
157 note_count: u8,
158) -> Option<(u8, u8)> {
159 if note_count < NOTES_PER_OCTAVE as u8 {
160 let white_note_ids = [0_u8, 2, 4, 5, 7];
161 let black_key_offsets = [1_u8, 2, 4];
162 let black_note_ids = [1_u8, 3, 6];
163 let white_key_height = bounds.height / white_note_ids.len() as f32;
164 let black_key_width = bounds.width * BLACK_KEY_DEPTH_RATIO;
165 let black_key_height = white_key_height * BLACK_KEY_LENGTH_RATIO;
166
167 if cursor.x <= black_key_width {
168 for (idx, offset) in black_key_offsets.iter().enumerate() {
169 let note_id = black_note_ids[idx];
170 if note_id >= note_count {
171 continue;
172 }
173 let y_pos_black = bounds.height
174 - (f32::from(*offset) * white_key_height)
175 - (black_key_height * 0.5);
176 if cursor.y >= y_pos_black && cursor.y <= y_pos_black + black_key_height {
177 let velocity = velocity_from_v(cursor.x, black_key_width);
178 return Some((note_id, velocity));
179 }
180 }
181 }
182
183 for (i, note_id) in white_note_ids.iter().enumerate() {
184 let y_pos = bounds.height - ((i + 1) as f32 * white_key_height);
185 if cursor.y >= y_pos && cursor.y <= y_pos + white_key_height {
186 let velocity = velocity_from_v(cursor.x, bounds.width);
187 return Some((*note_id, velocity));
188 }
189 }
190 return None;
191 }
192 let white_key_height = bounds.height / 7.0;
193 let black_key_offsets = [1, 2, 4, 5, 6];
194 let black_note_ids = [1, 3, 6, 8, 10];
195 let black_key_width = bounds.width * BLACK_KEY_DEPTH_RATIO;
196 let black_key_height = white_key_height * BLACK_KEY_LENGTH_RATIO;
197
198 if cursor.x <= black_key_width {
199 for (idx, offset) in black_key_offsets.iter().enumerate() {
200 let y_pos_black =
201 bounds.height - (*offset as f32 * white_key_height) - (black_key_height * 0.5);
202 if cursor.y >= y_pos_black && cursor.y <= y_pos_black + black_key_height {
203 let velocity = velocity_from_v(cursor.x, black_key_width);
204 return Some((black_note_ids[idx], velocity));
205 }
206 }
207 }
208
209 for i in 0..7 {
210 let note_id = match i {
211 0 => 0,
212 1 => 2,
213 2 => 4,
214 3 => 5,
215 4 => 7,
216 5 => 9,
217 6 => 11,
218 _ => 0,
219 };
220 let y_pos = bounds.height - ((i + 1) as f32 * white_key_height);
221 if cursor.y >= y_pos && cursor.y <= y_pos + white_key_height {
222 let velocity = velocity_from_v(cursor.x, bounds.width);
223 return Some((note_id, velocity));
224 }
225 }
226 None
227}
228
229pub fn map_point(orientation: Orientation, bounds: Rectangle, u: f32, v: f32) -> Point {
235 let x = bounds.x;
236 let y = bounds.y;
237 let w = bounds.width;
238 let h = bounds.height;
239 match orientation {
240 Orientation::Degree0 => Point::new(x + u, y + v),
241 Orientation::Degree90 => Point::new(x + w - v, y + u),
242 Orientation::Degree180 => Point::new(x + u, y + h - v),
243 Orientation::Degree270 => Point::new(x + v, y + h - u),
244 }
245}
246
247fn map_rect(
250 orientation: Orientation,
251 bounds: Rectangle,
252 u_low: f32,
253 u_high: f32,
254 v_low: f32,
255 v_high: f32,
256) -> (Point, Size) {
257 let x = bounds.x;
258 let y = bounds.y;
259 match orientation {
260 Orientation::Degree0 => (
261 Point::new(x + u_low, y + v_low),
262 Size::new(u_high - u_low, v_high - v_low),
263 ),
264 Orientation::Degree90 => (
265 Point::new(x + bounds.width - v_high, y + u_low),
266 Size::new(v_high - v_low, u_high - u_low),
267 ),
268 Orientation::Degree180 => (
269 Point::new(x + u_low, y + bounds.height - v_high),
270 Size::new(u_high - u_low, v_high - v_low),
271 ),
272 Orientation::Degree270 => (
273 Point::new(x + v_low, y + bounds.height - u_high),
274 Size::new(v_high - v_low, u_high - u_low),
275 ),
276 }
277}
278
279fn natural_bounds(orientation: Orientation, bounds: Rectangle) -> Rectangle {
281 match orientation {
282 Orientation::Degree90 | Orientation::Degree270 => bounds,
283 Orientation::Degree0 | Orientation::Degree180 => Rectangle {
284 x: bounds.x,
285 y: bounds.y,
286 width: bounds.height,
287 height: bounds.width,
288 },
289 }
290}
291
292fn to_natural_cursor(orientation: Orientation, bounds: Rectangle, cursor: Point) -> Point {
294 let w = bounds.width;
295 let h = bounds.height;
296 match orientation {
297 Orientation::Degree0 => Point::new(cursor.x, cursor.y),
298 Orientation::Degree90 => Point::new(cursor.y, w - cursor.x),
299 Orientation::Degree180 => Point::new(cursor.x, h - cursor.y),
300 Orientation::Degree270 => Point::new(h - cursor.y, cursor.x),
301 }
302}
303
304fn text_align_x(orientation: Orientation) -> iced::advanced::text::Alignment {
306 use iced::advanced::text::Alignment;
307 match orientation {
308 Orientation::Degree0 => Alignment::Center,
309 Orientation::Degree90 => Alignment::Left,
310 Orientation::Degree180 => Alignment::Center,
311 Orientation::Degree270 => Alignment::Right,
312 }
313}
314
315fn text_align_y(orientation: Orientation) -> iced::alignment::Vertical {
316 use iced::alignment::Vertical;
317 match orientation {
318 Orientation::Degree0 => Vertical::Top,
319 Orientation::Degree90 => Vertical::Center,
320 Orientation::Degree180 => Vertical::Bottom,
321 Orientation::Degree270 => Vertical::Center,
322 }
323}
324
325fn draw_keyboard_range_into(
326 frame: &mut Frame<Renderer>,
327 bounds: Rectangle,
328 pressed_notes: &HashSet<u8>,
329 octave: u8,
330 midnam_note_names: &HashMap<u8, String>,
331 orientation: Orientation,
332 note_count: u8,
333) {
334 if note_count == 0 {
335 return;
336 }
337 let natural = natural_bounds(orientation, bounds);
338 let height = natural.height;
339 let width = natural.width;
340 let white_notes: &[u8] = if note_count >= 12 {
341 &[0, 2, 4, 5, 7, 9, 11]
342 } else {
343 &[0, 2, 4, 5, 7]
344 };
345 let white_key_count = white_notes.iter().take_while(|&&n| n < note_count).count() as f32;
346 if white_key_count == 0.0 {
347 return;
348 }
349 let white_key_size = height / white_key_count;
350 let black_key_width = width * BLACK_KEY_DEPTH_RATIO;
351 let black_key_length = white_key_size * BLACK_KEY_LENGTH_RATIO;
352 let stroke = canvas::Stroke::default().with_width(1.0);
353 let h_align = text_align_x(orientation);
354 let v_align = text_align_y(orientation);
355
356 for (i, ¬e_id) in white_notes
358 .iter()
359 .enumerate()
360 .take_while(|&(_, n)| *n < note_count)
361 {
362 let midi_note = octave * 12 + note_id;
363 let is_pressed = pressed_notes.contains(¬e_id);
364 let color = if is_pressed {
365 Color::from_rgb(0.0, 0.5, 1.0)
366 } else {
367 Color::WHITE
368 };
369 let u_low = i as f32 * white_key_size;
370 let u_high = u_low + white_key_size - 1.0;
371
372 let (origin, size) = map_rect(orientation, bounds, u_low, u_high, 0.0, width);
373 let rect = Path::rectangle(origin, size);
374 frame.fill(&rect, color);
375 frame.stroke(&rect, stroke);
376
377 if let Some(note_name) = midnam_note_names.get(&midi_note) {
378 use iced::widget::canvas::Text;
379 let text_pos = map_point(orientation, bounds, (u_low + u_high) * 0.5, width - 4.0);
380 frame.fill_text(Text {
381 content: note_name.clone(),
382 position: text_pos,
383 color: Color::BLACK,
384 size: 10.0.into(),
385 align_x: h_align,
386 align_y: v_align,
387 ..Text::default()
388 });
389 }
390 }
391
392 let (black_offsets, black_notes): (&[u8], &[u8]) = if note_count >= 12 {
393 (&[1, 2, 4, 5, 6], &[1, 3, 6, 8, 10])
394 } else {
395 (&[1, 2, 4], &[1, 3, 6])
396 };
397
398 for (idx, ¬e_id) in black_notes
399 .iter()
400 .enumerate()
401 .take_while(|&(_, n)| *n < note_count)
402 {
403 let is_pressed = pressed_notes.contains(¬e_id);
404 let color = if is_pressed {
405 Color::from_rgb(0.0, 0.4, 0.8)
406 } else {
407 Color::BLACK
408 };
409 let center = black_offsets[idx] as f32 * white_key_size;
410 let (origin, size) = map_rect(
411 orientation,
412 bounds,
413 center - black_key_length * 0.5,
414 center + black_key_length * 0.5,
415 0.0,
416 black_key_width,
417 );
418 let rect = Path::rectangle(origin, size);
419 frame.fill(&rect, color);
420 }
421}
422
423pub fn draw_octave_into(
424 frame: &mut Frame<Renderer>,
425 bounds: Rectangle,
426 pressed_notes: &HashSet<u8>,
427 octave: u8,
428 midnam_note_names: &HashMap<u8, String>,
429 orientation: Orientation,
430) {
431 draw_keyboard_range_into(
432 frame,
433 bounds,
434 pressed_notes,
435 octave,
436 midnam_note_names,
437 orientation,
438 12,
439 );
440}
441
442pub fn draw_partial_octave_into(
443 frame: &mut Frame<Renderer>,
444 bounds: Rectangle,
445 pressed_notes: &HashSet<u8>,
446 octave: u8,
447 midnam_note_names: &HashMap<u8, String>,
448 orientation: Orientation,
449) {
450 draw_keyboard_range_into(
451 frame,
452 bounds,
453 pressed_notes,
454 octave,
455 midnam_note_names,
456 orientation,
457 octave_note_count(octave),
458 );
459}
460
461fn draw_octave_degree270(
462 renderer: &Renderer,
463 bounds: Rectangle,
464 pressed_notes: &HashSet<u8>,
465 octave: u8,
466 midnam_note_names: &HashMap<u8, String>,
467) -> Vec<canvas::Geometry> {
468 let mut frame = Frame::new(renderer, bounds.size());
469 let white_key_height = bounds.height / 7.0;
470
471 for i in 0..7 {
472 let note_id = match i {
473 0 => 0,
474 1 => 2,
475 2 => 4,
476 3 => 5,
477 4 => 7,
478 5 => 9,
479 6 => 11,
480 _ => 0,
481 };
482 let midi_note = octave * 12 + note_id;
483 let is_pressed = pressed_notes.contains(¬e_id);
484 let y_pos = bounds.height - ((i + 1) as f32 * white_key_height);
485 let rect = Path::rectangle(
486 Point::new(0.0, y_pos),
487 Size::new(bounds.width, white_key_height - 1.0),
488 );
489
490 frame.fill(
491 &rect,
492 if is_pressed {
493 Color::from_rgb(0.0, 0.5, 1.0)
494 } else {
495 Color::WHITE
496 },
497 );
498 frame.stroke(&rect, canvas::Stroke::default().with_width(1.0));
499
500 if let Some(note_name) = midnam_note_names.get(&midi_note) {
501 use iced::widget::canvas::Text;
502 frame.fill_text(Text {
503 content: note_name.clone(),
504 position: Point::new(bounds.width - 25.0, y_pos + white_key_height * 0.5 - 6.0),
505 color: Color::BLACK,
506 size: 10.0.into(),
507 ..Text::default()
508 });
509 }
510 }
511
512 let black_key_offsets = [1, 2, 4, 5, 6];
513 let black_note_ids = [1, 3, 6, 8, 10];
514 let black_key_width = bounds.width * BLACK_KEY_DEPTH_RATIO;
515 let black_key_height = white_key_height * BLACK_KEY_LENGTH_RATIO;
516
517 for (idx, offset) in black_key_offsets.iter().enumerate() {
518 let note_id = black_note_ids[idx];
519 let is_pressed = pressed_notes.contains(¬e_id);
520 let y_pos_black =
521 bounds.height - (*offset as f32 * white_key_height) - (black_key_height * 0.5);
522 let rect = Path::rectangle(
523 Point::new(0.0, y_pos_black),
524 Size::new(black_key_width, black_key_height),
525 );
526
527 frame.fill(
528 &rect,
529 if is_pressed {
530 Color::from_rgb(0.0, 0.4, 0.8)
531 } else {
532 Color::BLACK
533 },
534 );
535 }
536
537 vec![frame.into_geometry()]
538}
539
540fn draw_partial_octave_degree270(
541 renderer: &Renderer,
542 bounds: Rectangle,
543 pressed_notes: &HashSet<u8>,
544 octave: u8,
545 midnam_note_names: &HashMap<u8, String>,
546 note_count: u8,
547) -> Vec<canvas::Geometry> {
548 let mut frame = Frame::new(renderer, bounds.size());
549 let white_note_ids = [0_u8, 2, 4, 5, 7];
550 let black_key_offsets = [1_u8, 2, 4];
551 let black_note_ids = [1_u8, 3, 6];
552 let white_key_height = bounds.height / white_note_ids.len() as f32;
553 let black_key_height = white_key_height * BLACK_KEY_LENGTH_RATIO;
554 let black_key_width = bounds.width * BLACK_KEY_DEPTH_RATIO;
555
556 for (i, note_id) in white_note_ids.iter().enumerate() {
557 let midi_note = octave * 12 + *note_id;
558 let is_pressed = pressed_notes.contains(note_id);
559 let y_pos = bounds.height - ((i + 1) as f32 * white_key_height);
560 let rect = Path::rectangle(
561 Point::new(0.0, y_pos),
562 Size::new(bounds.width, white_key_height - 1.0),
563 );
564 frame.fill(
565 &rect,
566 if is_pressed {
567 Color::from_rgb(0.0, 0.5, 1.0)
568 } else {
569 Color::WHITE
570 },
571 );
572 frame.stroke(&rect, canvas::Stroke::default().with_width(1.0));
573 if let Some(note_name) = midnam_note_names.get(&midi_note) {
574 use iced::widget::canvas::Text;
575 frame.fill_text(Text {
576 content: note_name.clone(),
577 position: Point::new(bounds.width - 25.0, y_pos + white_key_height * 0.5 - 6.0),
578 color: Color::BLACK,
579 size: 10.0.into(),
580 ..Text::default()
581 });
582 }
583 }
584
585 for (idx, offset) in black_key_offsets.iter().enumerate() {
586 let note_id = black_note_ids[idx];
587 if note_id >= note_count {
588 continue;
589 }
590 let is_pressed = pressed_notes.contains(¬e_id);
591 let y_pos_black =
592 bounds.height - (f32::from(*offset) * white_key_height) - (black_key_height * 0.5);
593 let rect = Path::rectangle(
594 Point::new(0.0, y_pos_black),
595 Size::new(black_key_width, black_key_height),
596 );
597 frame.fill(
598 &rect,
599 if is_pressed {
600 Color::from_rgb(0.0, 0.4, 0.8)
601 } else {
602 Color::BLACK
603 },
604 );
605 }
606
607 vec![frame.into_geometry()]
608}
609
610pub fn draw_octave(
611 renderer: &Renderer,
612 bounds: Rectangle,
613 pressed_notes: &HashSet<u8>,
614 octave: u8,
615 midnam_note_names: &HashMap<u8, String>,
616 orientation: Orientation,
617) -> Vec<canvas::Geometry> {
618 if orientation == Orientation::Degree270 {
619 return draw_octave_degree270(renderer, bounds, pressed_notes, octave, midnam_note_names);
620 }
621 let mut frame = Frame::new(renderer, bounds.size());
622 let local_bounds = Rectangle {
623 x: 0.0,
624 y: 0.0,
625 width: bounds.width,
626 height: bounds.height,
627 };
628 draw_octave_into(
629 &mut frame,
630 local_bounds,
631 pressed_notes,
632 octave,
633 midnam_note_names,
634 orientation,
635 );
636 vec![frame.into_geometry()]
637}
638
639pub fn draw_partial_octave(
640 renderer: &Renderer,
641 bounds: Rectangle,
642 pressed_notes: &HashSet<u8>,
643 octave: u8,
644 midnam_note_names: &HashMap<u8, String>,
645 orientation: Orientation,
646) -> Vec<canvas::Geometry> {
647 if orientation == Orientation::Degree270 {
648 return draw_partial_octave_degree270(
649 renderer,
650 bounds,
651 pressed_notes,
652 octave,
653 midnam_note_names,
654 octave_note_count(octave),
655 );
656 }
657 let mut frame = Frame::new(renderer, bounds.size());
658 let local_bounds = Rectangle {
659 x: 0.0,
660 y: 0.0,
661 width: bounds.width,
662 height: bounds.height,
663 };
664 draw_partial_octave_into(
665 &mut frame,
666 local_bounds,
667 pressed_notes,
668 octave,
669 midnam_note_names,
670 orientation,
671 );
672 vec![frame.into_geometry()]
673}
674
675#[derive(Debug, Clone)]
676pub struct OctaveKeyboard<Message, Press, Release>
677where
678 Press: Fn(u8, u8) -> Message + Clone,
679 Release: Fn(u8) -> Message + Clone,
680{
681 pub octave: u8,
682 pub note_count: u8,
683 pub midnam_note_names: HashMap<u8, String>,
684 pub orientation: Orientation,
685 on_press: Press,
687 on_release: Release,
688}
689
690impl<Message, Press, Release> OctaveKeyboard<Message, Press, Release>
691where
692 Press: Fn(u8, u8) -> Message + Clone,
693 Release: Fn(u8) -> Message + Clone,
694{
695 pub fn new(
696 octave: u8,
697 midnam_note_names: HashMap<u8, String>,
698 on_press: Press,
699 on_release: Release,
700 ) -> Self {
701 Self {
702 octave,
703 note_count: octave_note_count(octave),
704 midnam_note_names,
705 orientation: Orientation::Degree0,
706 on_press,
707 on_release,
708 }
709 }
710
711 pub fn with_orientation(mut self, orientation: Orientation) -> Self {
712 self.orientation = orientation;
713 self
714 }
715
716 fn note_at(&self, cursor: Point, bounds: Rectangle) -> Option<(u8, u8)> {
717 note_at_in_range(cursor, bounds, self.orientation, self.note_count)
718 }
719
720 fn midi_note(&self, note_class: u8) -> u8 {
721 (usize::from(self.octave) * 12 + usize::from(note_class)) as u8
722 }
723}
724
725#[derive(Default, Debug)]
726pub struct OctaveKeyboardState {
727 pub pressed_notes: HashSet<u8>,
728 pub active_note_class: Option<u8>,
729}
730
731impl<Message, Press, Release> Program<Message> for OctaveKeyboard<Message, Press, Release>
732where
733 Message: 'static,
734 Press: Fn(u8, u8) -> Message + Clone,
735 Release: Fn(u8) -> Message + Clone,
736{
737 type State = OctaveKeyboardState;
738
739 fn update(
740 &self,
741 state: &mut Self::State,
742 event: &Event,
743 bounds: Rectangle,
744 cursor: mouse::Cursor,
745 ) -> Option<CanvasAction<Message>> {
746 match event {
747 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
748 if let Some(position) = cursor.position_in(bounds)
749 && let Some((note_class, velocity)) = self.note_at(position, bounds)
750 {
751 state.active_note_class = Some(note_class);
752 state.pressed_notes.clear();
753 state.pressed_notes.insert(note_class);
754 return Some(
755 CanvasAction::publish((self.on_press.clone())(
756 self.midi_note(note_class),
757 velocity,
758 ))
759 .and_capture(),
760 );
761 }
762 }
763 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
764 if let Some(note_class) = state.active_note_class.take() {
765 state.pressed_notes.clear();
766 return Some(CanvasAction::publish((self.on_release.clone())(
767 self.midi_note(note_class),
768 )));
769 }
770 }
771 _ => {}
772 }
773 None
774 }
775
776 fn draw(
777 &self,
778 state: &Self::State,
779 renderer: &Renderer,
780 _theme: &Theme,
781 bounds: Rectangle,
782 _cursor: mouse::Cursor,
783 ) -> Vec<Geometry> {
784 if self.note_count == NOTES_PER_OCTAVE as u8 {
785 draw_octave(
786 renderer,
787 bounds,
788 &state.pressed_notes,
789 self.octave,
790 &self.midnam_note_names,
791 self.orientation,
792 )
793 } else {
794 draw_partial_octave(
795 renderer,
796 bounds,
797 &state.pressed_notes,
798 self.octave,
799 &self.midnam_note_names,
800 self.orientation,
801 )
802 }
803 }
804}
805
806pub fn row_height(zoom_y: f32) -> f32 {
807 ((WHITE_KEY_HEIGHT * WHITE_KEYS_PER_OCTAVE as f32 / NOTES_PER_OCTAVE as f32) * zoom_y).max(1.0)
808}
809
810#[cfg(test)]
811mod tests {
812 use super::*;
813 use iced::widget::canvas::Program;
814 use iced::{Point, Rectangle, Size, event, mouse};
815
816 #[derive(Debug, Clone, PartialEq, Eq)]
817 enum TestMessage {
818 Pressed(u8, u8),
819 Released(u8),
820 }
821
822 fn action_message(action: CanvasAction<TestMessage>) -> (Option<TestMessage>, event::Status) {
823 let (message, _redraw, status) = action.into_inner();
824 (message, status)
825 }
826
827 #[test]
828 fn octave_keyboard_update_publishes_pressed_and_released_notes() {
829 let keyboard = OctaveKeyboard::new(
830 4,
831 HashMap::new(),
832 TestMessage::Pressed,
833 TestMessage::Released,
834 )
835 .with_orientation(Orientation::Degree270);
836 let bounds = Rectangle::new(Point::ORIGIN, Size::new(20.0, 70.0));
837 let press_cursor = mouse::Cursor::Available(Point::new(15.0, 65.0));
838 let release_cursor = mouse::Cursor::Available(Point::new(15.0, 65.0));
839 let mut state = OctaveKeyboardState::default();
840
841 let press = keyboard
842 .update(
843 &mut state,
844 &Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
845 bounds,
846 press_cursor,
847 )
848 .expect("press action");
849 let (message, status) = action_message(press);
850 assert_eq!(message, Some(TestMessage::Pressed(48, 96)));
851 assert_eq!(status, event::Status::Captured);
852 assert_eq!(state.active_note_class, Some(0));
853 assert!(state.pressed_notes.contains(&0));
854
855 let release = keyboard
856 .update(
857 &mut state,
858 &Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
859 bounds,
860 release_cursor,
861 )
862 .expect("release action");
863 let (message, status) = action_message(release);
864 assert_eq!(message, Some(TestMessage::Released(48)));
865 assert_eq!(status, event::Status::Ignored);
866 assert!(state.pressed_notes.is_empty());
867 }
868
869 #[test]
870 fn partial_octave_keyboard_maps_top_note_to_midi_127() {
871 let keyboard = OctaveKeyboard::new(
872 10,
873 HashMap::new(),
874 TestMessage::Pressed,
875 TestMessage::Released,
876 )
877 .with_orientation(Orientation::Degree270);
878 let bounds = Rectangle::new(Point::ORIGIN, Size::new(20.0, 80.0));
879 let cursor = mouse::Cursor::Available(Point::new(15.0, 5.0));
880 let mut state = OctaveKeyboardState::default();
881
882 let press = keyboard
883 .update(
884 &mut state,
885 &Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
886 bounds,
887 cursor,
888 )
889 .expect("press action");
890 let (message, status) = action_message(press);
891
892 assert_eq!(message, Some(TestMessage::Pressed(127, 96)));
893 assert_eq!(status, event::Status::Captured);
894 assert_eq!(state.active_note_class, Some(7));
895 }
896}