Add keyboard support to virtual piano
This commit is contained in:
parent
8f1934ab59
commit
98c2880b45
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
use super::{NodePath, PaneRenderer, SharedPaneState};
|
use super::{NodePath, PaneRenderer, SharedPaneState};
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
use std::collections::HashSet;
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
/// Virtual piano pane state
|
/// Virtual piano pane state
|
||||||
pub struct VirtualPianoPane {
|
pub struct VirtualPianoPane {
|
||||||
|
|
@ -21,6 +21,14 @@ pub struct VirtualPianoPane {
|
||||||
dragging_note: Option<u8>,
|
dragging_note: Option<u8>,
|
||||||
/// Octave offset for keyboard mapping (default: 0 = C4)
|
/// Octave offset for keyboard mapping (default: 0 = C4)
|
||||||
octave_offset: i8,
|
octave_offset: i8,
|
||||||
|
/// Tracks which computer keys are held and which MIDI notes they're playing
|
||||||
|
active_key_presses: HashMap<String, u8>,
|
||||||
|
/// Current velocity for keyboard input (adjustable with C/V keys)
|
||||||
|
keyboard_velocity: u8,
|
||||||
|
/// Base keyboard mapping (key string -> MIDI note, before octave offset)
|
||||||
|
keyboard_map: HashMap<String, u8>,
|
||||||
|
/// Reverse mapping for displaying labels (MIDI note -> key label)
|
||||||
|
note_to_key_map: HashMap<u8, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for VirtualPianoPane {
|
impl Default for VirtualPianoPane {
|
||||||
|
|
@ -31,6 +39,33 @@ impl Default for VirtualPianoPane {
|
||||||
|
|
||||||
impl VirtualPianoPane {
|
impl VirtualPianoPane {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
// Create keyboard mapping (C4-F5 range, MIDI notes 60-77)
|
||||||
|
let mut keyboard_map = HashMap::new();
|
||||||
|
keyboard_map.insert("a".to_string(), 60); // C4
|
||||||
|
keyboard_map.insert("w".to_string(), 61); // C#4
|
||||||
|
keyboard_map.insert("s".to_string(), 62); // D4
|
||||||
|
keyboard_map.insert("e".to_string(), 63); // D#4
|
||||||
|
keyboard_map.insert("d".to_string(), 64); // E4
|
||||||
|
keyboard_map.insert("f".to_string(), 65); // F4
|
||||||
|
keyboard_map.insert("t".to_string(), 66); // F#4
|
||||||
|
keyboard_map.insert("g".to_string(), 67); // G4
|
||||||
|
keyboard_map.insert("y".to_string(), 68); // G#4
|
||||||
|
keyboard_map.insert("h".to_string(), 69); // A4
|
||||||
|
keyboard_map.insert("u".to_string(), 70); // A#4
|
||||||
|
keyboard_map.insert("j".to_string(), 71); // B4
|
||||||
|
keyboard_map.insert("k".to_string(), 72); // C5
|
||||||
|
keyboard_map.insert("o".to_string(), 73); // C#5
|
||||||
|
keyboard_map.insert("l".to_string(), 74); // D5
|
||||||
|
keyboard_map.insert("p".to_string(), 75); // D#5
|
||||||
|
keyboard_map.insert(";".to_string(), 76); // E5
|
||||||
|
keyboard_map.insert("'".to_string(), 77); // F5
|
||||||
|
|
||||||
|
// Create reverse mapping for labels (note -> uppercase key)
|
||||||
|
let mut note_to_key_map = HashMap::new();
|
||||||
|
for (key, note) in &keyboard_map {
|
||||||
|
note_to_key_map.insert(*note, key.to_uppercase());
|
||||||
|
}
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
white_key_aspect_ratio: 6.0,
|
white_key_aspect_ratio: 6.0,
|
||||||
black_key_width_ratio: 0.6,
|
black_key_width_ratio: 0.6,
|
||||||
|
|
@ -38,6 +73,10 @@ impl VirtualPianoPane {
|
||||||
pressed_notes: HashSet::new(),
|
pressed_notes: HashSet::new(),
|
||||||
dragging_note: None,
|
dragging_note: None,
|
||||||
octave_offset: 0, // Center on C4 (MIDI note 60)
|
octave_offset: 0, // Center on C4 (MIDI note 60)
|
||||||
|
active_key_presses: HashMap::new(),
|
||||||
|
keyboard_velocity: 100, // Default MIDI velocity
|
||||||
|
keyboard_map,
|
||||||
|
note_to_key_map,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,6 +162,38 @@ impl VirtualPianoPane {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check black keys for interaction first (since they render on top and overlap white keys)
|
||||||
|
// This prevents white keys underneath from also receiving the click
|
||||||
|
let mut black_key_interacted = false;
|
||||||
|
let pointer_pos = ui.input(|i| i.pointer.hover_pos());
|
||||||
|
|
||||||
|
if let Some(pos) = pointer_pos {
|
||||||
|
for note in visible_start..=visible_end {
|
||||||
|
if !Self::is_black_key(note) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate black key rect
|
||||||
|
let mut white_keys_before = 0;
|
||||||
|
for n in visible_start..note {
|
||||||
|
if Self::is_white_key(n) {
|
||||||
|
white_keys_before += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = rect.min.x + offset_x + (white_keys_before as f32 * white_key_width) - (black_key_width / 2.0);
|
||||||
|
let key_rect = egui::Rect::from_min_size(
|
||||||
|
egui::pos2(x, rect.min.y),
|
||||||
|
egui::vec2(black_key_width, black_key_height),
|
||||||
|
);
|
||||||
|
|
||||||
|
if key_rect.contains(pos) {
|
||||||
|
black_key_interacted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Draw white keys first (so black keys render on top)
|
// Draw white keys first (so black keys render on top)
|
||||||
for note in visible_start..=visible_end {
|
for note in visible_start..=visible_end {
|
||||||
if !Self::is_white_key(note) {
|
if !Self::is_white_key(note) {
|
||||||
|
|
@ -136,8 +207,17 @@ impl VirtualPianoPane {
|
||||||
egui::vec2(white_key_width - 1.0, white_key_height),
|
egui::vec2(white_key_width - 1.0, white_key_height),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Visual feedback for pressed keys
|
// Handle interaction (skip if a black key is being interacted with)
|
||||||
let is_pressed = self.pressed_notes.contains(¬e);
|
let key_id = ui.id().with(("white_key", note));
|
||||||
|
let response = ui.interact(key_rect, key_id, egui::Sense::click_and_drag());
|
||||||
|
|
||||||
|
// Visual feedback for pressed keys (check both pressed_notes and current pointer state)
|
||||||
|
let pointer_over_key = ui.input(|i| {
|
||||||
|
i.pointer.hover_pos().map_or(false, |pos| key_rect.contains(pos))
|
||||||
|
});
|
||||||
|
let pointer_down = ui.input(|i| i.pointer.primary_down());
|
||||||
|
let is_pressed = self.pressed_notes.contains(¬e) ||
|
||||||
|
(!black_key_interacted && pointer_over_key && pointer_down);
|
||||||
let color = if is_pressed {
|
let color = if is_pressed {
|
||||||
egui::Color32::from_rgb(100, 150, 255) // Blue when pressed
|
egui::Color32::from_rgb(100, 150, 255) // Blue when pressed
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -152,40 +232,33 @@ impl VirtualPianoPane {
|
||||||
egui::StrokeKind::Middle,
|
egui::StrokeKind::Middle,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle interaction
|
if !black_key_interacted {
|
||||||
let key_id = ui.id().with(("white_key", note));
|
// Mouse down starts note (detect primary button pressed on this key)
|
||||||
let response = ui.interact(key_rect, key_id, egui::Sense::click_and_drag());
|
if pointer_over_key && ui.input(|i| i.pointer.primary_pressed()) {
|
||||||
|
|
||||||
// Check if pointer is currently over this key (works during drag too)
|
|
||||||
let pointer_over_key = ui.input(|i| {
|
|
||||||
i.pointer.hover_pos().map_or(false, |pos| key_rect.contains(pos))
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mouse down starts note (detect primary button pressed on this key)
|
|
||||||
if pointer_over_key && ui.input(|i| i.pointer.primary_pressed()) {
|
|
||||||
self.send_note_on(note, 100, shared);
|
|
||||||
self.dragging_note = Some(note);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mouse up stops note (detect primary button released)
|
|
||||||
if ui.input(|i| i.pointer.primary_released()) {
|
|
||||||
if self.dragging_note == Some(note) {
|
|
||||||
self.send_note_off(note, shared);
|
|
||||||
self.dragging_note = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dragging over a new key (pointer is down and over a different key)
|
|
||||||
if pointer_over_key && ui.input(|i| i.pointer.primary_down()) {
|
|
||||||
if self.dragging_note != Some(note) {
|
|
||||||
// Stop previous note
|
|
||||||
if let Some(prev_note) = self.dragging_note {
|
|
||||||
self.send_note_off(prev_note, shared);
|
|
||||||
}
|
|
||||||
// Start new note
|
|
||||||
self.send_note_on(note, 100, shared);
|
self.send_note_on(note, 100, shared);
|
||||||
self.dragging_note = Some(note);
|
self.dragging_note = Some(note);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mouse up stops note (detect primary button released)
|
||||||
|
if ui.input(|i| i.pointer.primary_released()) {
|
||||||
|
if self.dragging_note == Some(note) {
|
||||||
|
self.send_note_off(note, shared);
|
||||||
|
self.dragging_note = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dragging over a new key (pointer is down and over a different key)
|
||||||
|
if pointer_over_key && pointer_down {
|
||||||
|
if self.dragging_note != Some(note) {
|
||||||
|
// Stop previous note
|
||||||
|
if let Some(prev_note) = self.dragging_note {
|
||||||
|
self.send_note_off(prev_note, shared);
|
||||||
|
}
|
||||||
|
// Start new note
|
||||||
|
self.send_note_on(note, 100, shared);
|
||||||
|
self.dragging_note = Some(note);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -210,7 +283,17 @@ impl VirtualPianoPane {
|
||||||
egui::vec2(black_key_width, black_key_height),
|
egui::vec2(black_key_width, black_key_height),
|
||||||
);
|
);
|
||||||
|
|
||||||
let is_pressed = self.pressed_notes.contains(¬e);
|
// Handle interaction (same as white keys)
|
||||||
|
let key_id = ui.id().with(("black_key", note));
|
||||||
|
let response = ui.interact(key_rect, key_id, egui::Sense::click_and_drag());
|
||||||
|
|
||||||
|
// Visual feedback for pressed keys (check both pressed_notes and current pointer state)
|
||||||
|
let pointer_over_key = ui.input(|i| {
|
||||||
|
i.pointer.hover_pos().map_or(false, |pos| key_rect.contains(pos))
|
||||||
|
});
|
||||||
|
let pointer_down = ui.input(|i| i.pointer.primary_down());
|
||||||
|
let is_pressed = self.pressed_notes.contains(¬e) ||
|
||||||
|
(pointer_over_key && pointer_down);
|
||||||
let color = if is_pressed {
|
let color = if is_pressed {
|
||||||
egui::Color32::from_rgb(50, 100, 200) // Darker blue when pressed
|
egui::Color32::from_rgb(50, 100, 200) // Darker blue when pressed
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -219,15 +302,6 @@ impl VirtualPianoPane {
|
||||||
|
|
||||||
ui.painter().rect_filled(key_rect, 2.0, color);
|
ui.painter().rect_filled(key_rect, 2.0, color);
|
||||||
|
|
||||||
// Handle interaction (same as white keys)
|
|
||||||
let key_id = ui.id().with(("black_key", note));
|
|
||||||
let response = ui.interact(key_rect, key_id, egui::Sense::click_and_drag());
|
|
||||||
|
|
||||||
// Check if pointer is currently over this key (works during drag too)
|
|
||||||
let pointer_over_key = ui.input(|i| {
|
|
||||||
i.pointer.hover_pos().map_or(false, |pos| key_rect.contains(pos))
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mouse down starts note
|
// Mouse down starts note
|
||||||
if pointer_over_key && ui.input(|i| i.pointer.primary_pressed()) {
|
if pointer_over_key && ui.input(|i| i.pointer.primary_pressed()) {
|
||||||
self.send_note_on(note, 100, shared);
|
self.send_note_on(note, 100, shared);
|
||||||
|
|
@ -243,7 +317,7 @@ impl VirtualPianoPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dragging over a new key
|
// Dragging over a new key
|
||||||
if pointer_over_key && ui.input(|i| i.pointer.primary_down()) {
|
if pointer_over_key && pointer_down {
|
||||||
if self.dragging_note != Some(note) {
|
if self.dragging_note != Some(note) {
|
||||||
if let Some(prev_note) = self.dragging_note {
|
if let Some(prev_note) = self.dragging_note {
|
||||||
self.send_note_off(prev_note, shared);
|
self.send_note_off(prev_note, shared);
|
||||||
|
|
@ -282,21 +356,236 @@ impl VirtualPianoPane {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Process keyboard input for virtual piano
|
||||||
|
/// Returns true if the event was consumed
|
||||||
|
fn handle_keyboard_input(&mut self, ui: &mut egui::Ui, shared: &mut SharedPaneState) -> bool {
|
||||||
|
// Check if we have an active MIDI layer - don't process input if not
|
||||||
|
let has_active_midi_layer = if let Some(active_layer_id) = *shared.active_layer_id {
|
||||||
|
shared.layer_to_track_map.contains_key(&active_layer_id)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
if !has_active_midi_layer {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut consumed = false;
|
||||||
|
|
||||||
|
ui.input(|i| {
|
||||||
|
// Handle octave shift keys (Z/X)
|
||||||
|
if i.key_pressed(egui::Key::Z) {
|
||||||
|
if self.octave_offset > -2 {
|
||||||
|
self.octave_offset -= 1;
|
||||||
|
consumed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if i.key_pressed(egui::Key::X) {
|
||||||
|
if self.octave_offset < 2 {
|
||||||
|
self.octave_offset += 1;
|
||||||
|
consumed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle velocity adjustment (C/V)
|
||||||
|
if i.key_pressed(egui::Key::C) {
|
||||||
|
self.keyboard_velocity = self.keyboard_velocity.saturating_sub(10).max(1);
|
||||||
|
consumed = true;
|
||||||
|
}
|
||||||
|
if i.key_pressed(egui::Key::V) {
|
||||||
|
self.keyboard_velocity = self.keyboard_velocity.saturating_add(10).min(127);
|
||||||
|
consumed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process raw events for piano keys (need to track press/release separately)
|
||||||
|
for event in &i.events {
|
||||||
|
if let egui::Event::Key { key, pressed, repeat, .. } = event {
|
||||||
|
if *repeat {
|
||||||
|
continue; // Ignore key repeats
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert egui::Key to string representation
|
||||||
|
let key_str = match key {
|
||||||
|
egui::Key::A => "a",
|
||||||
|
egui::Key::S => "s",
|
||||||
|
egui::Key::D => "d",
|
||||||
|
egui::Key::F => "f",
|
||||||
|
egui::Key::G => "g",
|
||||||
|
egui::Key::H => "h",
|
||||||
|
egui::Key::J => "j",
|
||||||
|
egui::Key::K => "k",
|
||||||
|
egui::Key::L => "l",
|
||||||
|
egui::Key::W => "w",
|
||||||
|
egui::Key::E => "e",
|
||||||
|
egui::Key::T => "t",
|
||||||
|
egui::Key::Y => "y",
|
||||||
|
egui::Key::U => "u",
|
||||||
|
egui::Key::O => "o",
|
||||||
|
egui::Key::P => "p",
|
||||||
|
egui::Key::Semicolon => ";",
|
||||||
|
egui::Key::Quote => "'",
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(&base_note) = self.keyboard_map.get(key_str) {
|
||||||
|
if *pressed {
|
||||||
|
// Key down - start note
|
||||||
|
if !self.active_key_presses.contains_key(key_str) {
|
||||||
|
let note = (base_note as i32 + self.octave_offset as i32 * 12)
|
||||||
|
.clamp(0, 127) as u8;
|
||||||
|
self.active_key_presses.insert(key_str.to_string(), note);
|
||||||
|
self.send_note_on(note, self.keyboard_velocity, shared);
|
||||||
|
consumed = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Key up - stop note
|
||||||
|
if let Some(note) = self.active_key_presses.remove(key_str) {
|
||||||
|
self.send_note_off(note, shared);
|
||||||
|
consumed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
consumed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Release all keyboard-held notes (call when losing focus or switching tracks)
|
||||||
|
fn release_all_keyboard_notes(&mut self, shared: &mut SharedPaneState) {
|
||||||
|
let notes_to_release: Vec<u8> = self.active_key_presses.values().copied().collect();
|
||||||
|
for note in notes_to_release {
|
||||||
|
self.send_note_off(note, shared);
|
||||||
|
}
|
||||||
|
self.active_key_presses.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render keyboard letter labels on piano keys
|
||||||
|
fn render_key_labels(
|
||||||
|
&self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
rect: egui::Rect,
|
||||||
|
visible_start: u8,
|
||||||
|
visible_end: u8,
|
||||||
|
white_key_width: f32,
|
||||||
|
offset_x: f32,
|
||||||
|
) {
|
||||||
|
let white_key_height = rect.height();
|
||||||
|
let black_key_width = white_key_width * self.black_key_width_ratio;
|
||||||
|
let black_key_height = white_key_height * self.black_key_height_ratio;
|
||||||
|
|
||||||
|
// Render labels on white keys
|
||||||
|
for note in visible_start..=visible_end {
|
||||||
|
if !Self::is_white_key(note) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate base note (subtract octave offset to get unmapped note)
|
||||||
|
let base_note = (note as i32 - self.octave_offset as i32 * 12).clamp(0, 127) as u8;
|
||||||
|
|
||||||
|
// Check if this note has a keyboard mapping
|
||||||
|
if let Some(label) = self.note_to_key_map.get(&base_note) {
|
||||||
|
// Count white keys before this note for positioning
|
||||||
|
let mut white_keys_before = 0;
|
||||||
|
for n in visible_start..note {
|
||||||
|
if Self::is_white_key(n) {
|
||||||
|
white_keys_before += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = rect.min.x + offset_x + (white_keys_before as f32 * white_key_width);
|
||||||
|
let label_pos = egui::pos2(
|
||||||
|
x + white_key_width / 2.0,
|
||||||
|
rect.min.y + rect.height() - 30.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if key is currently pressed
|
||||||
|
let is_pressed = self.pressed_notes.contains(¬e);
|
||||||
|
let color = if is_pressed {
|
||||||
|
egui::Color32::BLACK
|
||||||
|
} else {
|
||||||
|
egui::Color32::from_gray(51) // #333333
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.painter().text(
|
||||||
|
label_pos,
|
||||||
|
egui::Align2::CENTER_CENTER,
|
||||||
|
label,
|
||||||
|
egui::FontId::proportional(16.0),
|
||||||
|
color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render labels on black keys
|
||||||
|
for note in visible_start..=visible_end {
|
||||||
|
if !Self::is_black_key(note) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let base_note = (note as i32 - self.octave_offset as i32 * 12).clamp(0, 127) as u8;
|
||||||
|
|
||||||
|
if let Some(label) = self.note_to_key_map.get(&base_note) {
|
||||||
|
// Count white keys before this note for positioning
|
||||||
|
let mut white_keys_before = 0;
|
||||||
|
for n in visible_start..note {
|
||||||
|
if Self::is_white_key(n) {
|
||||||
|
white_keys_before += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = rect.min.x + offset_x + (white_keys_before as f32 * white_key_width)
|
||||||
|
- (black_key_width / 2.0);
|
||||||
|
let label_pos = egui::pos2(
|
||||||
|
x + black_key_width / 2.0,
|
||||||
|
rect.min.y + black_key_height - 20.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
let is_pressed = self.pressed_notes.contains(¬e);
|
||||||
|
let color = if is_pressed {
|
||||||
|
egui::Color32::WHITE
|
||||||
|
} else {
|
||||||
|
egui::Color32::from_rgba_premultiplied(255, 255, 255, 178) // rgba(255,255,255,0.7)
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.painter().text(
|
||||||
|
label_pos,
|
||||||
|
egui::Align2::CENTER_CENTER,
|
||||||
|
label,
|
||||||
|
egui::FontId::proportional(14.0),
|
||||||
|
color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PaneRenderer for VirtualPianoPane {
|
impl PaneRenderer for VirtualPianoPane {
|
||||||
fn render_header(&mut self, ui: &mut egui::Ui, _shared: &mut SharedPaneState) -> bool {
|
fn render_header(&mut self, ui: &mut egui::Ui, _shared: &mut SharedPaneState) -> bool {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label("Octave Shift:");
|
ui.label("Octave Shift:");
|
||||||
if ui.button("-").clicked() && self.octave_offset > -3 {
|
if ui.button("-").clicked() && self.octave_offset > -2 {
|
||||||
self.octave_offset -= 1;
|
self.octave_offset -= 1;
|
||||||
}
|
}
|
||||||
let center_note = 60 + (self.octave_offset as i32 * 12);
|
let center_note = 60 + (self.octave_offset as i32 * 12);
|
||||||
let octave_name = format!("C{}", center_note / 12);
|
let octave_name = format!("C{}", center_note / 12);
|
||||||
ui.label(octave_name);
|
ui.label(octave_name);
|
||||||
if ui.button("+").clicked() && self.octave_offset < 3 {
|
if ui.button("+").clicked() && self.octave_offset < 2 {
|
||||||
self.octave_offset += 1;
|
self.octave_offset += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
ui.label("Velocity:");
|
||||||
|
if ui.button("-").clicked() {
|
||||||
|
self.keyboard_velocity = self.keyboard_velocity.saturating_sub(10).max(1);
|
||||||
|
}
|
||||||
|
ui.label(format!("{}", self.keyboard_velocity));
|
||||||
|
if ui.button("+").clicked() {
|
||||||
|
self.keyboard_velocity = self.keyboard_velocity.saturating_add(10).min(127);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
true // We rendered a header
|
true // We rendered a header
|
||||||
|
|
@ -317,6 +606,11 @@ impl PaneRenderer for VirtualPianoPane {
|
||||||
};
|
};
|
||||||
|
|
||||||
if !has_active_midi_layer {
|
if !has_active_midi_layer {
|
||||||
|
// Release any held notes before showing error message
|
||||||
|
if !self.active_key_presses.is_empty() {
|
||||||
|
self.release_all_keyboard_notes(shared);
|
||||||
|
}
|
||||||
|
|
||||||
// Show message if no active MIDI track
|
// Show message if no active MIDI track
|
||||||
ui.centered_and_justified(|ui| {
|
ui.centered_and_justified(|ui| {
|
||||||
ui.label("No MIDI track selected. Create a MIDI track to use the virtual piano.");
|
ui.label("No MIDI track selected. Create a MIDI track to use the virtual piano.");
|
||||||
|
|
@ -324,8 +618,23 @@ impl PaneRenderer for VirtualPianoPane {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Request keyboard focus to prevent tool shortcuts from firing
|
||||||
|
// This sets wants_keyboard_input() to true
|
||||||
|
let piano_id = ui.id().with("virtual_piano_keyboard");
|
||||||
|
ui.memory_mut(|m| m.request_focus(piano_id));
|
||||||
|
|
||||||
|
// Handle keyboard input FIRST
|
||||||
|
self.handle_keyboard_input(ui, shared);
|
||||||
|
|
||||||
|
// Calculate visible range (needed for both rendering and labels)
|
||||||
|
let (visible_start, visible_end, white_key_width, offset_x) =
|
||||||
|
self.calculate_visible_range(rect.width(), rect.height());
|
||||||
|
|
||||||
// Render the keyboard
|
// Render the keyboard
|
||||||
self.render_keyboard(ui, rect, shared);
|
self.render_keyboard(ui, rect, shared);
|
||||||
|
|
||||||
|
// Render keyboard labels on top
|
||||||
|
self.render_key_labels(ui, rect, visible_start, visible_end, white_key_width, offset_x);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue