[ui] pianoroll
This commit is contained in:
@@ -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<u8>,
|
||||
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))
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod env_display;
|
||||
pub mod knob;
|
||||
pub mod wavetable_display;
|
||||
pub mod piano;
|
||||
|
||||
249
crates/ui/src/widgets/piano.rs
Normal file
249
crates/ui/src/widgets/piano.rs
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user