[ui] pianoroll

This commit is contained in:
2026-04-20 13:58:18 +03:00
parent ad8910ea19
commit 67b37dedc6
3 changed files with 274 additions and 1 deletions

View File

@@ -1,6 +1,8 @@
use params::{ParamId, ParamStore}; use params::{ParamId, ParamStore};
use vizia::prelude::*; use vizia::prelude::*;
use crate::widgets::piano::PianoKeyboard;
#[derive(Clone, Copy, Debug, PartialEq, Data)] #[derive(Clone, Copy, Debug, PartialEq, Data)]
pub enum Panel { pub enum Panel {
Osc, Osc,
@@ -42,6 +44,8 @@ pub struct AppData {
pub host_bpm: f32, pub host_bpm: f32,
#[lens(ignore)] #[lens(ignore)]
pub store: ParamStore, pub store: ParamStore,
pub held_notes: Vec<u8>,
pub octave: i32,
} }
impl AppData { impl AppData {
@@ -57,6 +61,8 @@ impl AppData {
voice_count: 0, voice_count: 0,
cpu_load: 0.0, cpu_load: 0.0,
host_bpm: 120.0, host_bpm: 120.0,
held_notes: Vec::new(),
octave: 4,
} }
} }
} }
@@ -66,10 +72,15 @@ pub enum AppEvent {
SetParam(ParamId, f32), SetParam(ParamId, f32),
SetPanel(Panel), SetPanel(Panel),
UpdateMetrics { voices: u8, cpu: f32 }, UpdateMetrics { voices: u8, cpu: f32 },
NoteOn(u8, u8),
NoteOff(u8),
OctaveUp,
OctaveDown,
} }
impl Model for AppData { 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 { event.map(|e: &AppEvent, _| match e {
AppEvent::SetParam(id, val) => { AppEvent::SetParam(id, val) => {
let v = val.clamp(0.0, 1.0); let v = val.clamp(0.0, 1.0);
@@ -81,6 +92,17 @@ impl Model for AppData {
self.voice_count = *voices; self.voice_count = *voices;
self.cpu_load = *cpu; 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); crate::panels::macro_bar::build(cx);
}) })
.height(Stretch(1.0)); .height(Stretch(1.0));
PianoKeyboard::new(cx);
}) })
.background_color(Color::rgb(18, 18, 28)) .background_color(Color::rgb(18, 18, 28))
.width(Stretch(1.0)) .width(Stretch(1.0))

View File

@@ -1,3 +1,4 @@
pub mod env_display; pub mod env_display;
pub mod knob; pub mod knob;
pub mod wavetable_display; pub mod wavetable_display;
pub mod piano;

View File

@@ -0,0 +1,249 @@
use crate::app::{AppData, AppEvent};
use vizia::{prelude::*, vg};
fn key_semitone(code: Code) -> Option<i32> {
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<u8> {
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<u8> {
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<u8>,
base_octave: i32,
mouse_note: Option<u8>,
}
impl PianoKeyboard {
pub fn new(cx: &mut Context) -> Handle<Self> {
let init_octave = cx.data::<AppData>().map(|d| d.octave).unwrap_or(4);
let init_held = cx
.data::<AppData>()
.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,
);
}
}
}