1use crate::Theme;
18use egui::{Color32, Rect, Sense, Stroke, Ui, Vec2};
19use egui_cha::ViewCtx;
20
21#[derive(Clone, Copy, Debug, PartialEq)]
23pub enum KeyboardEvent {
24 NoteOn(u8, u8),
26 NoteOff(u8),
28}
29
30#[derive(Clone, Copy, Debug, PartialEq)]
32pub struct ActiveNote {
33 pub note: u8,
35 pub velocity: u8,
37 pub color: Option<Color32>,
39}
40
41impl ActiveNote {
42 pub fn new(note: u8, velocity: u8) -> Self {
44 Self {
45 note,
46 velocity,
47 color: None,
48 }
49 }
50
51 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
64pub 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 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 pub fn octaves(mut self, octaves: u8) -> Self {
95 self.octaves = octaves.clamp(1, 10);
96 self
97 }
98
99 pub fn start_octave(mut self, octave: i8) -> Self {
101 self.start_octave = octave.clamp(-2, 8);
102 self
103 }
104
105 pub fn active_notes(mut self, notes: &'a [ActiveNote]) -> Self {
107 self.active_notes = notes;
108 self
109 }
110
111 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 pub fn show_labels(mut self, show: bool) -> Self {
120 self.show_labels = show;
121 self
122 }
123
124 pub fn show_velocity(mut self, show: bool) -> Self {
126 self.show_velocity = show;
127 self
128 }
129
130 pub fn clickable(mut self, clickable: bool) -> Self {
132 self.clickable = clickable;
133 self
134 }
135
136 pub fn highlight_color(mut self, color: Color32) -> Self {
138 self.highlight_color = Some(color);
139 self
140 }
141
142 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 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 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 let black_key_positions = [0, 1, 3, 4, 5]; 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 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 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 for (i, &white_idx) in black_key_positions.iter().enumerate() {
223 let note_in_octave = match i {
224 0 => 1, 1 => 3, 2 => 6, 3 => 8, 4 => 10, _ => 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 if self.clickable {
258 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 let painter = ui.painter();
279 let highlight = self.highlight_color.unwrap_or(theme.primary);
280
281 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 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 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 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 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}