From 67b37dedc66ea6449394e0ee17ceaa94e0de6efc Mon Sep 17 00:00:00 2001 From: deadYokai Date: Mon, 20 Apr 2026 13:58:18 +0300 Subject: [PATCH] [ui] pianoroll --- crates/ui/src/app.rs | 25 +++- crates/ui/src/widgets/mod.rs | 1 + crates/ui/src/widgets/piano.rs | 249 +++++++++++++++++++++++++++++++++ 3 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 crates/ui/src/widgets/piano.rs diff --git a/crates/ui/src/app.rs b/crates/ui/src/app.rs index ef86367..02c0cdd 100644 --- a/crates/ui/src/app.rs +++ b/crates/ui/src/app.rs @@ -1,6 +1,8 @@ use params::{ParamId, ParamStore}; use vizia::prelude::*; +use crate::widgets::piano::PianoKeyboard; + #[derive(Clone, Copy, Debug, PartialEq, Data)] pub enum Panel { Osc, @@ -42,6 +44,8 @@ pub struct AppData { pub host_bpm: f32, #[lens(ignore)] pub store: ParamStore, + pub held_notes: Vec, + pub octave: i32, } impl AppData { @@ -57,6 +61,8 @@ impl AppData { voice_count: 0, cpu_load: 0.0, host_bpm: 120.0, + held_notes: Vec::new(), + octave: 4, } } } @@ -66,10 +72,15 @@ pub enum AppEvent { SetParam(ParamId, f32), SetPanel(Panel), UpdateMetrics { voices: u8, cpu: f32 }, + + NoteOn(u8, u8), + NoteOff(u8), + OctaveUp, + OctaveDown, } impl Model for AppData { - fn event(&mut self, _cx: &mut EventContext, event: &mut Event) { + fn event(&mut self, cx: &mut EventContext, event: &mut Event) { event.map(|e: &AppEvent, _| match e { AppEvent::SetParam(id, val) => { let v = val.clamp(0.0, 1.0); @@ -81,6 +92,17 @@ impl Model for AppData { self.voice_count = *voices; self.cpu_load = *cpu; } + AppEvent::NoteOn(note, _vel) => { + if !self.held_notes.contains(note) { + self.held_notes.push(*note); + } + } + AppEvent::NoteOff(note) => self.held_notes.retain(|n| n != note), + AppEvent::OctaveUp => self.octave = (self.octave + 1).min(8), + AppEvent::OctaveDown => self.octave = (self.octave - 1).max(0), + }); + event.map(|we: &WindowEvent, _| { + crate::widgets::piano::handle_kbd(cx, we, self.octave); }); } } @@ -116,6 +138,7 @@ pub fn build_root(cx: &mut Context) { crate::panels::macro_bar::build(cx); }) .height(Stretch(1.0)); + PianoKeyboard::new(cx); }) .background_color(Color::rgb(18, 18, 28)) .width(Stretch(1.0)) diff --git a/crates/ui/src/widgets/mod.rs b/crates/ui/src/widgets/mod.rs index a2d15b9..630438f 100644 --- a/crates/ui/src/widgets/mod.rs +++ b/crates/ui/src/widgets/mod.rs @@ -1,3 +1,4 @@ pub mod env_display; pub mod knob; pub mod wavetable_display; +pub mod piano; diff --git a/crates/ui/src/widgets/piano.rs b/crates/ui/src/widgets/piano.rs new file mode 100644 index 0000000..71c40c7 --- /dev/null +++ b/crates/ui/src/widgets/piano.rs @@ -0,0 +1,249 @@ +use crate::app::{AppData, AppEvent}; +use vizia::{prelude::*, vg}; +fn key_semitone(code: Code) -> Option { + Some(match code { + Code::KeyA => 0, // C + Code::KeyW => 1, // C# + Code::KeyS => 2, // D + Code::KeyE => 3, // D# + Code::KeyD => 4, // E + Code::KeyF => 5, // F + Code::KeyT => 6, // F# + Code::KeyG => 7, // G + Code::KeyY => 8, // G# + Code::KeyH => 9, // A + Code::KeyU => 10, // A# + Code::KeyJ => 11, // B + Code::KeyK => 12, // C+1 + Code::KeyO => 13, // C#+1 + Code::KeyL => 14, // D+1 + Code::KeyP => 15, // D#+1 + Code::Semicolon => 16, // E+1 + _ => return None, + }) +} + +pub fn semitone_to_note(semitone: i32, octave: i32) -> Option { + let n = (octave + 1) * 12 + semitone; + (0..=127).contains(&n).then_some(n as u8) +} + +pub fn handle_kbd(cx: &mut EventContext, we: &WindowEvent, octave: i32) { + match we { + WindowEvent::KeyDown(code, _) => { + match code { + Code::KeyZ => { + cx.emit(AppEvent::OctaveDown); + return; + } + Code::KeyX => { + cx.emit(AppEvent::OctaveUp); + return; + } + _ => {} + } + if let Some(note) = key_semitone(*code).and_then(|s| semitone_to_note(s, octave)) { + cx.emit(AppEvent::NoteOn(note, 100)); + } + } + WindowEvent::KeyUp(code, _) => { + if let Some(note) = key_semitone(*code).and_then(|s| semitone_to_note(s, octave)) { + cx.emit(AppEvent::NoteOff(note)); + } + } + _ => {} + } +} + +const WW: f32 = 28.0; +const WH: f32 = 94.0; +const BW: f32 = 17.0; +const BH: f32 = 58.0; +const OCT: i32 = 3; +const WHITE: [i32; 7] = [0, 2, 4, 5, 7, 9, 11]; +const BLACK: [i32; 5] = [1, 3, 6, 8, 10]; + +const BX: [f32; 5] = [ + WW * 1.0 - BW * 0.5, // C# + WW * 2.0 - BW * 0.5, // D# + WW * 4.0 - BW * 0.5, // F# + WW * 5.0 - BW * 0.5, // G# + WW * 6.0 - BW * 0.5, // A# +]; + +fn note(oct_off: i32, semi: i32, base: i32) -> u8 { + ((base + oct_off + 1) * 12 + semi).clamp(0, 127) as u8 +} + +fn hit_test(lx: f32, ly: f32, base: i32) -> Option { + for oct in 0..OCT { + let ox = oct as f32 * WW * 7.0; + for (i, &semi) in BLACK.iter().enumerate() { + let kx = ox + BX[i]; + if ly < BH && lx >= kx && lx < kx + BW { + return Some(note(oct, semi, base)); + } + } + } + for oct in 0..OCT { + let ox = oct as f32 * WW * 7.0; + for (i, &semi) in WHITE.iter().enumerate() { + let kx = ox + i as f32 * WW; + if lx >= kx && lx < kx + WW { + return Some(note(oct, semi, base)); + } + } + } + None +} + +pub struct PianoKeyboard { + held: Vec, + base_octave: i32, + mouse_note: Option, +} + +impl PianoKeyboard { + pub fn new(cx: &mut Context) -> Handle { + let init_octave = cx.data::().map(|d| d.octave).unwrap_or(4); + let init_held = cx + .data::() + .map(|d| d.held_notes.clone()) + .unwrap_or_default(); + + Self { + held: init_held, + base_octave: init_octave, + mouse_note: None, + } + .build(cx, |_| {}) + .bind(AppData::held_notes, |h, lens| { + let held = lens.get(&h).clone(); + h.modify(|k: &mut PianoKeyboard| k.held = held); + }) + .bind(AppData::octave, |h, lens| { + let oct = lens.get(&h); + h.modify(|k: &mut PianoKeyboard| k.base_octave = oct); + }) + .height(Pixels(WH + 6.0)) + .width(Stretch(1.0)) + .background_color(Color::rgb(0x10, 0x10, 0x18)) + .focusable(true) + } +} + +impl View for PianoKeyboard { + fn element(&self) -> Option<&'static str> { + Some("piano-keyboard") + } + + fn event(&mut self, cx: &mut EventContext, event: &mut Event) { + event.map(|we: &WindowEvent, _| match we { + WindowEvent::MouseDown(MouseButton::Left) => { + let b = cx.bounds(); + let lx = cx.mouse().cursor_x - b.x; + let ly = cx.mouse().cursor_y - b.y; + if let Some(n) = hit_test(lx, ly, self.base_octave) { + self.mouse_note = Some(n); + cx.emit(AppEvent::NoteOn(n, 100)); + } + cx.capture(); + cx.set_active(true); + } + WindowEvent::MouseUp(MouseButton::Left) => { + if let Some(n) = self.mouse_note.take() { + cx.emit(AppEvent::NoteOff(n)); + } + cx.release(); + cx.set_active(false); + } + WindowEvent::MouseMove(mx, my) => { + if cx.mouse().left.state == MouseButtonState::Pressed { + let b = cx.bounds(); + let lx = mx - b.x; + let ly = my - b.y; + let new = hit_test(lx, ly, self.base_octave); + if new != self.mouse_note { + if let Some(n) = self.mouse_note.take() { + cx.emit(AppEvent::NoteOff(n)); + } + if let Some(n) = new { + self.mouse_note = Some(n); + cx.emit(AppEvent::NoteOn(n, 100)); + } + } + } + } + _ => {} + }); + } + + fn draw(&self, cx: &mut DrawContext, canvas: &Canvas) { + let b = cx.bounds(); + let ox = b.x; + let oy = b.y + 3.0; + + let mut p = vg::Paint::default(); + p.set_anti_alias(true); + + for oct in 0..OCT { + for (i, &semi) in WHITE.iter().enumerate() { + let n = note(oct, semi, self.base_octave); + let kx = ox + oct as f32 * WW * 7.0 + i as f32 * WW; + let r = vg::Rect::from_xywh(kx + 1.0, oy, WW - 2.0, WH); + + p.set_style(vg::PaintStyle::Fill); + p.set_color(if self.held.contains(&n) { + vg::Color::from_rgb(140, 90, 246) + } else { + vg::Color::from_rgb(232, 232, 240) + }); + canvas.draw_round_rect(r, 2.0, 2.0, &p); + + p.set_style(vg::PaintStyle::Stroke); + p.set_stroke_width(1.0); + p.set_color(vg::Color::from_rgb(60, 60, 80)); + canvas.draw_round_rect(r, 2.0, 2.0, &p); + } + } + + for oct in 0..OCT { + for (i, &semi) in BLACK.iter().enumerate() { + let n = note(oct, semi, self.base_octave); + let kx = ox + oct as f32 * WW * 7.0 + BX[i]; + let r = vg::Rect::from_xywh(kx, oy, BW, BH); + + p.set_style(vg::PaintStyle::Fill); + p.set_color(if self.held.contains(&n) { + vg::Color::from_rgb(100, 40, 200) + } else { + vg::Color::from_rgb(22, 22, 32) + }); + canvas.draw_round_rect(r, 2.0, 2.0, &p); + + p.set_color(vg::Color::from_argb(20, 255, 255, 255)); + canvas.draw_round_rect( + vg::Rect::from_xywh(kx + 2.0, oy + 3.0, BW - 4.0, 5.0), + 1.0, + 1.0, + &p, + ); + } + } + + let mut tick = vg::Paint::default(); + tick.set_anti_alias(false); + tick.set_color(vg::Color::from_argb(140, 120, 120, 180)); + tick.set_style(vg::PaintStyle::Fill); + + for oct in 0..OCT { + let kx = ox + oct as f32 * WW * 7.0; + canvas.draw_round_rect( + vg::Rect::from_xywh(kx + WW * 0.5 - 2.0, oy + WH - 6.0, 4.0, 4.0), + 1.0, + 1.0, + &tick, + ); + } + } +}