Skip to main content

egui_cha_ds/atoms/midi/
midi_keyboard.rs

1//! MidiKeyboard - Piano keyboard display with note visualization
2//!
3//! A piano keyboard for displaying MIDI input, note triggers, and velocity.
4//!
5//! # Example
6//! ```ignore
7//! MidiKeyboard::new()
8//!     .octaves(2)
9//!     .start_octave(3)
10//!     .active_notes(&model.pressed_notes)  // Vec<(note, velocity)>
11//!     .show_with(ctx, |event| match event {
12//!         KeyboardEvent::NoteOn(note, vel) => Msg::NoteOn(note, vel),
13//!         KeyboardEvent::NoteOff(note) => Msg::NoteOff(note),
14//!     });
15//! ```
16
17use crate::Theme;
18use egui::{Color32, Rect, Sense, Stroke, Ui, Vec2};
19use egui_cha::ViewCtx;
20
21/// Keyboard events
22#[derive(Clone, Copy, Debug, PartialEq)]
23pub enum KeyboardEvent {
24    /// Note on (note number 0-127, velocity 0-127)
25    NoteOn(u8, u8),
26    /// Note off (note number)
27    NoteOff(u8),
28}
29
30/// Active note with velocity
31#[derive(Clone, Copy, Debug, PartialEq)]
32pub struct ActiveNote {
33    /// MIDI note number (0-127)
34    pub note: u8,
35    /// Velocity (0-127)
36    pub velocity: u8,
37    /// Optional color override
38    pub color: Option<Color32>,
39}
40
41impl ActiveNote {
42    /// Create from note and velocity
43    pub fn new(note: u8, velocity: u8) -> Self {
44        Self {
45            note,
46            velocity,
47            color: None,
48        }
49    }
50
51    /// With custom color
52    pub fn with_color(mut self, color: Color32) -> Self {
53        self.color = Some(color);
54        self
55    }
56}
57
58impl From<(u8, u8)> for ActiveNote {
59    fn from((note, velocity): (u8, u8)) -> Self {
60        Self::new(note, velocity)
61    }
62}
63
64/// Piano keyboard display
65pub struct MidiKeyboard<'a> {
66    octaves: u8,
67    start_octave: i8,
68    active_notes: &'a [ActiveNote],
69    white_key_width: f32,
70    white_key_height: f32,
71    show_labels: bool,
72    show_velocity: bool,
73    clickable: bool,
74    highlight_color: Option<Color32>,
75}
76
77impl<'a> MidiKeyboard<'a> {
78    /// Create a new keyboard
79    pub fn new() -> Self {
80        Self {
81            octaves: 2,
82            start_octave: 4,
83            active_notes: &[],
84            white_key_width: 24.0,
85            white_key_height: 80.0,
86            show_labels: true,
87            show_velocity: true,
88            clickable: true,
89            highlight_color: None,
90        }
91    }
92
93    /// Set number of octaves to display
94    pub fn octaves(mut self, octaves: u8) -> Self {
95        self.octaves = octaves.clamp(1, 10);
96        self
97    }
98
99    /// Set starting octave (-2 to 8)
100    pub fn start_octave(mut self, octave: i8) -> Self {
101        self.start_octave = octave.clamp(-2, 8);
102        self
103    }
104
105    /// Set active (pressed) notes
106    pub fn active_notes(mut self, notes: &'a [ActiveNote]) -> Self {
107        self.active_notes = notes;
108        self
109    }
110
111    /// Set key size
112    pub fn key_size(mut self, width: f32, height: f32) -> Self {
113        self.white_key_width = width;
114        self.white_key_height = height;
115        self
116    }
117
118    /// Show/hide note labels (C4, D4, etc.)
119    pub fn show_labels(mut self, show: bool) -> Self {
120        self.show_labels = show;
121        self
122    }
123
124    /// Show/hide velocity indicators
125    pub fn show_velocity(mut self, show: bool) -> Self {
126        self.show_velocity = show;
127        self
128    }
129
130    /// Enable/disable click interaction
131    pub fn clickable(mut self, clickable: bool) -> Self {
132        self.clickable = clickable;
133        self
134    }
135
136    /// Set highlight color for active notes
137    pub fn highlight_color(mut self, color: Color32) -> Self {
138        self.highlight_color = Some(color);
139        self
140    }
141
142    /// TEA-style: Show keyboard and emit events
143    pub fn show_with<Msg>(
144        self,
145        ctx: &mut ViewCtx<'_, Msg>,
146        on_event: impl Fn(KeyboardEvent) -> Msg,
147    ) {
148        if let Some(event) = self.render(ctx.ui) {
149            ctx.emit(on_event(event));
150        }
151    }
152
153    /// Show keyboard, returns event if any
154    pub fn show(self, ui: &mut Ui) -> Option<KeyboardEvent> {
155        self.render(ui)
156    }
157
158    fn render(self, ui: &mut Ui) -> Option<KeyboardEvent> {
159        let theme = Theme::current(ui.ctx());
160        let mut event = None;
161
162        // 7 white keys per octave
163        let white_keys_per_octave = 7;
164        let total_white_keys = self.octaves as usize * white_keys_per_octave;
165        let total_width = total_white_keys as f32 * self.white_key_width;
166
167        let black_key_width = self.white_key_width * 0.6;
168        let black_key_height = self.white_key_height * 0.6;
169
170        let (rect, _) = ui.allocate_exact_size(
171            Vec2::new(total_width, self.white_key_height),
172            Sense::hover(),
173        );
174
175        if !ui.is_rect_visible(rect) {
176            return None;
177        }
178
179        // Key patterns: C D E F G A B (white), C# D# F# G# A# (black)
180        // Black key positions relative to white keys: after C, D, F, G, A
181        let black_key_positions = [0, 1, 3, 4, 5]; // indices where black keys appear (after these white keys)
182
183        // First pass: collect interaction info
184        struct KeyInfo {
185            rect: Rect,
186            note: u8,
187            is_black: bool,
188            is_active: bool,
189            velocity: u8,
190            color: Option<Color32>,
191        }
192
193        let mut keys: Vec<KeyInfo> = Vec::new();
194
195        // Generate all keys
196        for octave in 0..self.octaves {
197            let octave_offset = octave as usize * white_keys_per_octave;
198            let midi_octave = (self.start_octave + octave as i8) as i32;
199
200            // White keys
201            for (i, note_in_octave) in [0, 2, 4, 5, 7, 9, 11].iter().enumerate() {
202                let x = rect.min.x + (octave_offset + i) as f32 * self.white_key_width;
203                let key_rect = Rect::from_min_size(
204                    egui::pos2(x, rect.min.y),
205                    Vec2::new(self.white_key_width, self.white_key_height),
206                );
207
208                let note = ((midi_octave + 1) * 12 + *note_in_octave as i32) as u8;
209                let active_note = self.active_notes.iter().find(|n| n.note == note);
210
211                keys.push(KeyInfo {
212                    rect: key_rect,
213                    note,
214                    is_black: false,
215                    is_active: active_note.is_some(),
216                    velocity: active_note.map(|n| n.velocity).unwrap_or(0),
217                    color: active_note.and_then(|n| n.color),
218                });
219            }
220
221            // Black keys
222            for (i, &white_idx) in black_key_positions.iter().enumerate() {
223                let note_in_octave = match i {
224                    0 => 1,  // C#
225                    1 => 3,  // D#
226                    2 => 6,  // F#
227                    3 => 8,  // G#
228                    4 => 10, // A#
229                    _ => continue,
230                };
231
232                let x = rect.min.x
233                    + (octave_offset + white_idx) as f32 * self.white_key_width
234                    + self.white_key_width
235                    - black_key_width / 2.0;
236
237                let key_rect = Rect::from_min_size(
238                    egui::pos2(x, rect.min.y),
239                    Vec2::new(black_key_width, black_key_height),
240                );
241
242                let note = ((midi_octave + 1) * 12 + note_in_octave) as u8;
243                let active_note = self.active_notes.iter().find(|n| n.note == note);
244
245                keys.push(KeyInfo {
246                    rect: key_rect,
247                    note,
248                    is_black: true,
249                    is_active: active_note.is_some(),
250                    velocity: active_note.map(|n| n.velocity).unwrap_or(0),
251                    color: active_note.and_then(|n| n.color),
252                });
253            }
254        }
255
256        // Handle interactions (check black keys first as they're on top)
257        if self.clickable {
258            // Sort by is_black (black keys checked first)
259            let mut interaction_order: Vec<usize> = (0..keys.len()).collect();
260            interaction_order.sort_by(|&a, &b| keys[b].is_black.cmp(&keys[a].is_black));
261
262            for &idx in &interaction_order {
263                let key = &keys[idx];
264                let response = ui.allocate_rect(key.rect, Sense::click());
265
266                if response.clicked() {
267                    if key.is_active {
268                        event = Some(KeyboardEvent::NoteOff(key.note));
269                    } else {
270                        event = Some(KeyboardEvent::NoteOn(key.note, 100));
271                    }
272                    break;
273                }
274            }
275        }
276
277        // Second pass: draw all keys
278        let painter = ui.painter();
279        let highlight = self.highlight_color.unwrap_or(theme.primary);
280
281        // Draw white keys first
282        for key in keys.iter().filter(|k| !k.is_black) {
283            let bg_color = if key.is_active {
284                let vel_factor = key.velocity as f32 / 127.0;
285                let c = key.color.unwrap_or(highlight);
286                Color32::from_rgba_unmultiplied(
287                    c.r(),
288                    c.g(),
289                    c.b(),
290                    (100.0 + vel_factor * 155.0) as u8,
291                )
292            } else {
293                Color32::from_rgb(250, 250, 250)
294            };
295
296            painter.rect_filled(key.rect, 2.0, bg_color);
297            painter.rect_stroke(
298                key.rect,
299                2.0,
300                Stroke::new(1.0, theme.border),
301                egui::StrokeKind::Inside,
302            );
303
304            // Note label
305            if self.show_labels {
306                let note_name = Self::note_name(key.note);
307                if note_name.starts_with('C') && !note_name.contains('#') {
308                    painter.text(
309                        egui::pos2(key.rect.center().x, key.rect.max.y - 12.0),
310                        egui::Align2::CENTER_CENTER,
311                        note_name,
312                        egui::FontId::proportional(theme.font_size_xs * 0.8),
313                        theme.text_muted,
314                    );
315                }
316            }
317
318            // Velocity bar
319            if self.show_velocity && key.is_active {
320                let vel_height = (key.velocity as f32 / 127.0) * 20.0;
321                let vel_rect = Rect::from_min_size(
322                    egui::pos2(key.rect.min.x + 2.0, key.rect.max.y - vel_height - 2.0),
323                    Vec2::new(key.rect.width() - 4.0, vel_height),
324                );
325                let vel_color = key.color.unwrap_or(highlight);
326                painter.rect_filled(vel_rect, 1.0, vel_color);
327            }
328        }
329
330        // Draw black keys on top
331        for key in keys.iter().filter(|k| k.is_black) {
332            let bg_color = if key.is_active {
333                let vel_factor = key.velocity as f32 / 127.0;
334                let c = key.color.unwrap_or(highlight);
335                Color32::from_rgba_unmultiplied(
336                    c.r(),
337                    c.g(),
338                    c.b(),
339                    (150.0 + vel_factor * 105.0) as u8,
340                )
341            } else {
342                Color32::from_rgb(30, 30, 35)
343            };
344
345            painter.rect_filled(key.rect, 2.0, bg_color);
346
347            // Velocity indicator for black keys
348            if self.show_velocity && key.is_active {
349                let vel_height = (key.velocity as f32 / 127.0) * 15.0;
350                let vel_rect = Rect::from_min_size(
351                    egui::pos2(key.rect.min.x + 2.0, key.rect.max.y - vel_height - 2.0),
352                    Vec2::new(key.rect.width() - 4.0, vel_height),
353                );
354                let vel_color = key.color.unwrap_or(highlight);
355                painter.rect_filled(vel_rect, 1.0, vel_color);
356            }
357        }
358
359        event
360    }
361
362    fn note_name(note: u8) -> String {
363        let names = [
364            "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
365        ];
366        let octave = (note as i32 / 12) - 1;
367        let name_idx = (note % 12) as usize;
368        format!("{}{}", names[name_idx], octave)
369    }
370}
371
372impl Default for MidiKeyboard<'_> {
373    fn default() -> Self {
374        Self::new()
375    }
376}