[ui] pianoroll
This commit is contained in:
@@ -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))
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
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