Files
tenko/crates/ui/src/app.rs
2026-04-21 00:51:47 +03:00

194 lines
5.5 KiB
Rust

use crossbeam_channel::Sender;
use params::{ParamId, ParamStore};
use vizia::prelude::*;
use crate::widgets::piano::PianoKeyboard;
pub const MOD_NSRC: usize = 19;
pub const MOD_NDST: usize = 11;
#[derive(Clone, Copy, Debug)]
pub enum MidiMsg {
NoteOn(u8, u8),
NoteOff(u8),
AllNotesOff,
}
#[derive(Clone, Copy, Debug, PartialEq, Data)]
pub enum Panel {
Voice,
Effects,
Matrix,
}
impl Panel {
pub const ALL: &'static [Self] = &[Self::Voice, Self::Effects, Self::Matrix];
pub fn label(self) -> &'static str {
match self {
Self::Voice => "VOICE",
Self::Effects => "EFFECTS",
Self::Matrix => "MATRIX",
}
}
}
#[derive(Lens, Clone)]
pub struct AppData {
pub params: Vec<f32>,
pub active_panel: Panel,
pub preset_name: String,
pub voice_count: u8,
pub cpu_load: f32,
pub host_bpm: f32,
#[lens(ignore)]
pub store: ParamStore,
pub held_notes: Vec<u8>,
pub octave: i32,
pub mod_depths: Vec<f32>,
pub fx_enabled: Vec<bool>,
pub active_env: usize,
pub active_lfo: usize,
pub fx_selected: usize,
#[lens(ignore)]
pub midi_tx: Sender<MidiMsg>,
}
impl AppData {
pub fn new(store: ParamStore, midi_tx: Sender<MidiMsg>) -> Self {
let params = (0..ParamId::COUNT)
.map(|i| store.get(unsafe { std::mem::transmute::<usize, ParamId>(i) }))
.collect();
Self {
params,
store,
active_panel: Panel::Voice,
preset_name: "Init".into(),
voice_count: 0,
cpu_load: 0.0,
host_bpm: 120.0,
held_notes: Vec::new(),
octave: 4,
mod_depths: vec![0.0; MOD_NSRC * MOD_NDST],
fx_enabled: vec![true; 6],
active_env: 0,
active_lfo: 0,
fx_selected: 0,
midi_tx,
}
}
}
#[derive(Debug)]
pub enum AppEvent {
SetParam(ParamId, f32),
SetParamRaw(ParamId, f32),
SetPanel(Panel),
UpdateMetrics { voices: u8, cpu: f32 },
NoteOn(u8, u8),
NoteOff(u8),
OctaveUp,
OctaveDown,
SetModDepth { src: usize, dst: usize, depth: f32 },
ToggleFx(usize),
SelectEnv(usize),
SelectLfo(usize),
SelectFx(usize),
}
impl Model for AppData {
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);
self.params[*id as usize] = v;
self.store.set(*id, v);
}
AppEvent::SetParamRaw(id, val) => {
self.params[*id as usize] = *val;
self.store.set(*id, *val);
}
AppEvent::SetPanel(p) => self.active_panel = *p,
AppEvent::SelectEnv(i) => self.active_env = *i,
AppEvent::SelectLfo(i) => self.active_lfo = *i,
AppEvent::SelectFx(i) => self.fx_selected = *i,
AppEvent::UpdateMetrics { voices, cpu } => {
self.voice_count = *voices;
self.cpu_load = *cpu;
}
AppEvent::NoteOn(note, vel) => {
if !self.held_notes.contains(note) {
self.held_notes.push(*note);
}
let _ = self.midi_tx.try_send(MidiMsg::NoteOn(*note, *vel));
}
AppEvent::NoteOff(note) => {
self.held_notes.retain(|n| n != note);
let _ = self.midi_tx.try_send(MidiMsg::NoteOff(*note));
}
AppEvent::OctaveUp => self.octave = (self.octave + 1).min(8),
AppEvent::OctaveDown => self.octave = (self.octave - 1).max(0),
AppEvent::SetModDepth { src, dst, depth } => {
let idx = src * MOD_NDST + dst;
if idx < self.mod_depths.len() {
self.mod_depths[idx] = depth.clamp(-1.0, 1.0);
}
}
AppEvent::ToggleFx(s) => {
if *s < self.fx_enabled.len() {
self.fx_enabled[*s] = !self.fx_enabled[*s];
}
}
});
event.map(|we: &WindowEvent, _| {
crate::widgets::piano::handle_kbd(cx, we, self.octave);
});
}
}
pub fn build_root(cx: &mut Context) {
VStack::new(cx, |cx| {
crate::panels::header::build(cx);
HStack::new(cx, |cx| {
crate::panels::macro_bar::build(cx);
Binding::new(cx, AppData::active_panel, |cx, p| {
VStack::new(cx, |cx| match p.get(cx) {
Panel::Voice => crate::panels::voice::build(cx),
Panel::Effects => crate::panels::fx::build(cx),
Panel::Matrix => crate::panels::mod_matrix::build(cx),
})
.width(Stretch(1.0))
.height(Stretch(1.0))
.background_color(Color::rgb(12, 12, 20));
});
crate::panels::env_lfo_sidebar::build(cx);
})
.height(Stretch(1.0));
PianoKeyboard::new(cx);
})
.background_color(Color::rgb(10, 10, 16))
.width(Stretch(1.0))
.height(Stretch(1.0));
}
fn tab_button(cx: &mut Context, p: Panel) {
Binding::new(cx, AppData::active_panel, move |cx, active_lens| {
let active = active_lens.get(cx) == p;
Label::new(cx, p.label())
.class("top-tab")
.checked(active)
.on_press(move |cx| cx.emit(AppEvent::SetPanel(p)))
.height(Stretch(1.0));
});
}