redesign
This commit is contained in:
@@ -3,33 +3,23 @@ use vizia::prelude::*;
|
|||||||
|
|
||||||
use crate::widgets::piano::PianoKeyboard;
|
use crate::widgets::piano::PianoKeyboard;
|
||||||
|
|
||||||
|
pub const MOD_NSRC: usize = 19;
|
||||||
|
pub const MOD_NDST: usize = 11;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Data)]
|
#[derive(Clone, Copy, Debug, PartialEq, Data)]
|
||||||
pub enum Panel {
|
pub enum Panel {
|
||||||
Osc,
|
Voice,
|
||||||
Env,
|
Effects,
|
||||||
Lfo,
|
Matrix,
|
||||||
Filter,
|
|
||||||
Fx,
|
|
||||||
ModMatrix,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Panel {
|
impl Panel {
|
||||||
pub const ALL: &'static [Self] = &[
|
pub const ALL: &'static [Self] = &[Self::Voice, Self::Effects, Self::Matrix];
|
||||||
Self::Osc,
|
|
||||||
Self::Env,
|
|
||||||
Self::Lfo,
|
|
||||||
Self::Filter,
|
|
||||||
Self::Fx,
|
|
||||||
Self::ModMatrix,
|
|
||||||
];
|
|
||||||
pub fn label(self) -> &'static str {
|
pub fn label(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Osc => "OSC",
|
Self::Voice => "VOICE",
|
||||||
Self::Env => "ENV",
|
Self::Effects => "EFFECTS",
|
||||||
Self::Lfo => "LFO",
|
Self::Matrix => "MATRIX",
|
||||||
Self::Filter => "FLT",
|
|
||||||
Self::Fx => "FX",
|
|
||||||
Self::ModMatrix => "MOD",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,6 +36,11 @@ pub struct AppData {
|
|||||||
pub store: ParamStore,
|
pub store: ParamStore,
|
||||||
pub held_notes: Vec<u8>,
|
pub held_notes: Vec<u8>,
|
||||||
pub octave: i32,
|
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,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppData {
|
impl AppData {
|
||||||
@@ -56,13 +51,18 @@ impl AppData {
|
|||||||
Self {
|
Self {
|
||||||
params,
|
params,
|
||||||
store,
|
store,
|
||||||
active_panel: Panel::Osc,
|
active_panel: Panel::Voice,
|
||||||
preset_name: "Init".into(),
|
preset_name: "Init".into(),
|
||||||
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(),
|
held_notes: Vec::new(),
|
||||||
octave: 4,
|
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,13 +70,18 @@ impl AppData {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum AppEvent {
|
pub enum AppEvent {
|
||||||
SetParam(ParamId, f32),
|
SetParam(ParamId, f32),
|
||||||
|
SetParamRaw(ParamId, f32),
|
||||||
SetPanel(Panel),
|
SetPanel(Panel),
|
||||||
UpdateMetrics { voices: u8, cpu: f32 },
|
UpdateMetrics { voices: u8, cpu: f32 },
|
||||||
|
|
||||||
NoteOn(u8, u8),
|
NoteOn(u8, u8),
|
||||||
NoteOff(u8),
|
NoteOff(u8),
|
||||||
OctaveUp,
|
OctaveUp,
|
||||||
OctaveDown,
|
OctaveDown,
|
||||||
|
SetModDepth { src: usize, dst: usize, depth: f32 },
|
||||||
|
ToggleFx(usize),
|
||||||
|
SelectEnv(usize),
|
||||||
|
SelectLfo(usize),
|
||||||
|
SelectFx(usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Model for AppData {
|
impl Model for AppData {
|
||||||
@@ -87,12 +92,16 @@ impl Model for AppData {
|
|||||||
self.params[*id as usize] = v;
|
self.params[*id as usize] = v;
|
||||||
self.store.set(*id, 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::SetPanel(p) => self.active_panel = *p,
|
||||||
AppEvent::UpdateMetrics { voices, cpu } => {
|
AppEvent::UpdateMetrics { voices, cpu } => {
|
||||||
self.voice_count = *voices;
|
self.voice_count = *voices;
|
||||||
self.cpu_load = *cpu;
|
self.cpu_load = *cpu;
|
||||||
}
|
}
|
||||||
AppEvent::NoteOn(note, _vel) => {
|
AppEvent::NoteOn(note, _) => {
|
||||||
if !self.held_notes.contains(note) {
|
if !self.held_notes.contains(note) {
|
||||||
self.held_notes.push(*note);
|
self.held_notes.push(*note);
|
||||||
}
|
}
|
||||||
@@ -100,6 +109,20 @@ impl Model for AppData {
|
|||||||
AppEvent::NoteOff(note) => self.held_notes.retain(|n| n != note),
|
AppEvent::NoteOff(note) => self.held_notes.retain(|n| n != note),
|
||||||
AppEvent::OctaveUp => self.octave = (self.octave + 1).min(8),
|
AppEvent::OctaveUp => self.octave = (self.octave + 1).min(8),
|
||||||
AppEvent::OctaveDown => self.octave = (self.octave - 1).max(0),
|
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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AppEvent::SelectEnv(i) => self.active_env = *i,
|
||||||
|
AppEvent::SelectLfo(i) => self.active_lfo = *i,
|
||||||
|
AppEvent::SelectFx(i) => self.fx_selected = *i,
|
||||||
});
|
});
|
||||||
event.map(|we: &WindowEvent, _| {
|
event.map(|we: &WindowEvent, _| {
|
||||||
crate::widgets::piano::handle_kbd(cx, we, self.octave);
|
crate::widgets::piano::handle_kbd(cx, we, self.octave);
|
||||||
@@ -112,48 +135,26 @@ pub fn build_root(cx: &mut Context) {
|
|||||||
crate::panels::header::build(cx);
|
crate::panels::header::build(cx);
|
||||||
|
|
||||||
HStack::new(cx, |cx| {
|
HStack::new(cx, |cx| {
|
||||||
VStack::new(cx, |cx| {
|
crate::panels::macro_bar::build(cx);
|
||||||
for &p in Panel::ALL {
|
|
||||||
tab_button(cx, p);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.width(Pixels(56.0))
|
|
||||||
.height(Stretch(1.0))
|
|
||||||
.background_color(Color::rgb(14, 14, 26));
|
|
||||||
|
|
||||||
Binding::new(cx, AppData::active_panel, |cx, panel_lens| {
|
Binding::new(cx, AppData::active_panel, |cx, p| {
|
||||||
VStack::new(cx, |cx| match panel_lens.get(cx) {
|
VStack::new(cx, |cx| match p.get(cx) {
|
||||||
Panel::Osc => crate::panels::osc::build(cx),
|
Panel::Voice => crate::panels::voice::build(cx),
|
||||||
Panel::Env => crate::panels::env_panel::build(cx),
|
Panel::Effects => crate::panels::fx::build(cx),
|
||||||
Panel::Lfo => crate::panels::lfo::build(cx),
|
Panel::Matrix => crate::panels::mod_matrix::build(cx),
|
||||||
Panel::Filter => crate::panels::filter::build(cx),
|
|
||||||
Panel::Fx => crate::panels::fx::build(cx),
|
|
||||||
Panel::ModMatrix => crate::panels::mod_matrix::build(cx),
|
|
||||||
})
|
})
|
||||||
.class("panel")
|
|
||||||
.width(Stretch(1.0))
|
.width(Stretch(1.0))
|
||||||
.height(Stretch(1.0));
|
.height(Stretch(1.0))
|
||||||
|
.background_color(Color::rgb(12, 12, 20));
|
||||||
});
|
});
|
||||||
|
|
||||||
crate::panels::macro_bar::build(cx);
|
crate::panels::env_lfo_sidebar::build(cx);
|
||||||
})
|
})
|
||||||
.height(Stretch(1.0));
|
.height(Stretch(1.0));
|
||||||
|
|
||||||
PianoKeyboard::new(cx);
|
PianoKeyboard::new(cx);
|
||||||
})
|
})
|
||||||
.background_color(Color::rgb(18, 18, 28))
|
.background_color(Color::rgb(10, 10, 16))
|
||||||
.width(Stretch(1.0))
|
.width(Stretch(1.0))
|
||||||
.height(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 al = active_lens.get(cx) == p;
|
|
||||||
Label::new(cx, p.label())
|
|
||||||
.class("tab")
|
|
||||||
.checked(al)
|
|
||||||
.on_press(move |cx| cx.emit(AppEvent::SetPanel(p)))
|
|
||||||
.width(Stretch(1.0))
|
|
||||||
.height(Pixels(44.0))
|
|
||||||
.text_align(TextAlign::Center);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ fn main() {
|
|||||||
.expect("theme.css");
|
.expect("theme.css");
|
||||||
AppData::new(ParamStore::new()).build(cx);
|
AppData::new(ParamStore::new()).build(cx);
|
||||||
app::build_root(cx);
|
app::build_root(cx);
|
||||||
|
cx.focus();
|
||||||
})
|
})
|
||||||
.title("Tenko")
|
.title("Tenko")
|
||||||
.inner_size((1280, 760))
|
.inner_size((1280, 760))
|
||||||
|
|||||||
3
crates/ui/src/panels/advanced.rs
Normal file
3
crates/ui/src/panels/advanced.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
use vizia::prelude::*;
|
||||||
|
|
||||||
|
pub fn build(_cx: &mut Context) {}
|
||||||
169
crates/ui/src/panels/env_lfo_sidebar.rs
Normal file
169
crates/ui/src/panels/env_lfo_sidebar.rs
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
use crate::app::{AppData, AppEvent};
|
||||||
|
use crate::widgets::{
|
||||||
|
env_display::EnvDisplay, knob::labeled_knob, wavetable_display::WavetableDisplay,
|
||||||
|
};
|
||||||
|
use params::ParamId;
|
||||||
|
use vizia::prelude::*;
|
||||||
|
|
||||||
|
const ENV_KNOBS: [[ParamId; 6]; 3] = [
|
||||||
|
[
|
||||||
|
ParamId::Env1Delay,
|
||||||
|
ParamId::Env1Attack,
|
||||||
|
ParamId::Env1Hold,
|
||||||
|
ParamId::Env1Decay,
|
||||||
|
ParamId::Env1Sustain,
|
||||||
|
ParamId::Env1Release,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ParamId::Env2Delay,
|
||||||
|
ParamId::Env2Attack,
|
||||||
|
ParamId::Env2Hold,
|
||||||
|
ParamId::Env2Decay,
|
||||||
|
ParamId::Env2Sustain,
|
||||||
|
ParamId::Env2Release,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ParamId::Env3Delay,
|
||||||
|
ParamId::Env3Attack,
|
||||||
|
ParamId::Env3Hold,
|
||||||
|
ParamId::Env3Decay,
|
||||||
|
ParamId::Env3Sustain,
|
||||||
|
ParamId::Env3Release,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
const ENV_LABELS: [&str; 6] = ["DELAY", "ATTACK", "HOLD", "DECAY", "SUSTAIN", "RELEASE"];
|
||||||
|
|
||||||
|
const LFO_KNOBS: [[ParamId; 4]; 4] = [
|
||||||
|
[
|
||||||
|
ParamId::Lfo1Rate,
|
||||||
|
ParamId::Lfo1Phase,
|
||||||
|
ParamId::Lfo1Depth,
|
||||||
|
ParamId::Lfo1WavePos,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ParamId::Lfo2Rate,
|
||||||
|
ParamId::Lfo2Phase,
|
||||||
|
ParamId::Lfo2Depth,
|
||||||
|
ParamId::Lfo2WavePos,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ParamId::Lfo3Rate,
|
||||||
|
ParamId::Lfo3Phase,
|
||||||
|
ParamId::Lfo3Depth,
|
||||||
|
ParamId::Lfo3WavePos,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ParamId::Lfo4Rate,
|
||||||
|
ParamId::Lfo4Phase,
|
||||||
|
ParamId::Lfo4Depth,
|
||||||
|
ParamId::Lfo4WavePos,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
const SYNC_PARAMS: [ParamId; 4] = [
|
||||||
|
ParamId::Lfo1Sync,
|
||||||
|
ParamId::Lfo2Sync,
|
||||||
|
ParamId::Lfo3Sync,
|
||||||
|
ParamId::Lfo4Sync,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn build(cx: &mut Context) {
|
||||||
|
VStack::new(cx, |cx| {
|
||||||
|
env_section(cx);
|
||||||
|
Element::new(cx)
|
||||||
|
.height(Pixels(1.0))
|
||||||
|
.background_color(Color::from("#1e1e30"));
|
||||||
|
lfo_section(cx);
|
||||||
|
})
|
||||||
|
.width(Pixels(480.0))
|
||||||
|
.background_color(Color::from("#0e0e18"))
|
||||||
|
.height(Stretch(1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn env_section(cx: &mut Context) {
|
||||||
|
VStack::new(cx, |cx| {
|
||||||
|
Binding::new(cx, AppData::active_env, |cx, lens| {
|
||||||
|
let active = lens.get(cx);
|
||||||
|
HStack::new(cx, |cx| {
|
||||||
|
for i in 0..3usize {
|
||||||
|
Label::new(cx, format!("ENV {}", i + 1))
|
||||||
|
.class("env-tab")
|
||||||
|
.checked(active == i)
|
||||||
|
.on_press(move |cx| cx.emit(AppEvent::SelectEnv(i)));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.horizontal_gap(Pixels(4.0))
|
||||||
|
.bottom(Pixels(8.0));
|
||||||
|
});
|
||||||
|
|
||||||
|
Binding::new(cx, AppData::active_env, |cx, lens| {
|
||||||
|
let env_idx = lens.get(cx);
|
||||||
|
EnvDisplay::new(cx, env_idx)
|
||||||
|
.width(Stretch(1.0))
|
||||||
|
.height(Pixels(100.0));
|
||||||
|
|
||||||
|
HStack::new(cx, |cx| {
|
||||||
|
for &k in &ENV_KNOBS[env_idx] {
|
||||||
|
labeled_knob(cx, k);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.horizontal_gap(Pixels(4.0))
|
||||||
|
.top(Pixels(8.0))
|
||||||
|
.padding_left(Pixels(4.0));
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.padding(Pixels(12.0))
|
||||||
|
.height(Pixels(220.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lfo_section(cx: &mut Context) {
|
||||||
|
VStack::new(cx, |cx| {
|
||||||
|
Binding::new(cx, AppData::active_lfo, |cx, lens| {
|
||||||
|
let active = lens.get(cx);
|
||||||
|
HStack::new(cx, |cx| {
|
||||||
|
for i in 0..4usize {
|
||||||
|
Label::new(cx, format!("LFO {}", i + 1))
|
||||||
|
.class("lfo-tab")
|
||||||
|
.checked(active == i)
|
||||||
|
.on_press(move |cx| cx.emit(AppEvent::SelectLfo(i)));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.horizontal_gap(Pixels(4.0))
|
||||||
|
.bottom(Pixels(8.0));
|
||||||
|
});
|
||||||
|
|
||||||
|
Binding::new(cx, AppData::active_lfo, |cx, lens| {
|
||||||
|
let lfo_idx = lens.get(cx);
|
||||||
|
let knobs = &LFO_KNOBS[lfo_idx];
|
||||||
|
let sync_id = SYNC_PARAMS[lfo_idx];
|
||||||
|
|
||||||
|
WavetableDisplay::with_live_param(cx, knobs[3])
|
||||||
|
.width(Stretch(1.0))
|
||||||
|
.height(Pixels(90.0));
|
||||||
|
|
||||||
|
HStack::new(cx, |cx| {
|
||||||
|
for &k in knobs.iter() {
|
||||||
|
labeled_knob(cx, k);
|
||||||
|
}
|
||||||
|
|
||||||
|
Binding::new(cx, AppData::params.idx(sync_id as usize), move |cx, sl| {
|
||||||
|
let synced = sl.get(cx) > 0.5;
|
||||||
|
Label::new(cx, "SYNC")
|
||||||
|
.class("sync-toggle")
|
||||||
|
.checked(synced)
|
||||||
|
.on_press(move |cx| {
|
||||||
|
cx.emit(AppEvent::SetParam(sync_id, if synced { 0.0 } else { 1.0 }));
|
||||||
|
})
|
||||||
|
.top(Stretch(1.0))
|
||||||
|
.bottom(Stretch(1.0));
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.horizontal_gap(Pixels(4.0))
|
||||||
|
.top(Pixels(8.0))
|
||||||
|
.padding_left(Pixels(4.0));
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.padding(Pixels(12.0))
|
||||||
|
.height(Stretch(1.0));
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::widgets::knob::labeled_knob;
|
use crate::app::{AppData, AppEvent};
|
||||||
|
use crate::widgets::{filter_response::FilterResponseDisplay, knob::labeled_knob};
|
||||||
use params::ParamId;
|
use params::ParamId;
|
||||||
use vizia::prelude::*;
|
use vizia::prelude::*;
|
||||||
|
|
||||||
@@ -16,56 +17,98 @@ const FILTER_KNOBS: [[ParamId; 4]; 2] = [
|
|||||||
ParamId::Filter2Keytrack,
|
ParamId::Filter2Keytrack,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
const TYPES: [&str; 4] = ["LADDER", "SVF", "COMB", "FORMANT"];
|
const TYPE_PARAMS: [ParamId; 2] = [ParamId::Filter1Type, ParamId::Filter2Type];
|
||||||
|
const FILTER_TYPES: [&str; 4] = ["LADDER", "SVF", "COMB", "FORMANT"];
|
||||||
|
|
||||||
|
const ROUTING_ID: ParamId = ParamId::FilterRouting;
|
||||||
|
|
||||||
pub fn build(cx: &mut Context) {
|
pub fn build(cx: &mut Context) {
|
||||||
VStack::new(cx, |cx| {
|
VStack::new(cx, |cx| {
|
||||||
Label::new(cx, "FILTERS")
|
Binding::new(cx, AppData::params.idx(ROUTING_ID as usize), |cx, l| {
|
||||||
.class("section-title")
|
let serial = l.get(cx) < 0.5;
|
||||||
.bottom(Pixels(6.0));
|
HStack::new(cx, |cx| {
|
||||||
|
for (i, &lbl) in ["SERIAL", "PARALLEL"].iter().enumerate() {
|
||||||
|
let active = if i == 0 { serial } else { !serial };
|
||||||
|
Label::new(cx, lbl)
|
||||||
|
.class("filter-type-btn")
|
||||||
|
.checked(active)
|
||||||
|
.on_press(move |cx| {
|
||||||
|
cx.emit(AppEvent::SetParamRaw(ROUTING_ID, i as f32));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.horizontal_gap(Pixels(4.0))
|
||||||
|
.padding(Pixels(6.0));
|
||||||
|
});
|
||||||
|
|
||||||
HStack::new(cx, |cx| {
|
HStack::new(cx, |cx| {
|
||||||
for m in ["SERIAL", "PARALLEL"] {
|
for slot in 0..2usize {
|
||||||
Label::new(cx, m).class("tab").padding(Pixels(5.0));
|
filter_slot(cx, slot);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.bottom(Pixels(8.0));
|
.height(Pixels(160.0))
|
||||||
|
.horizontal_gap(Pixels(6.0))
|
||||||
|
.padding(Pixels(6.0));
|
||||||
|
})
|
||||||
|
.background_color(Color::from("#0e0e18"))
|
||||||
|
.border_width(Pixels(1.0))
|
||||||
|
.border_color(Color::from("#1e1e30"));
|
||||||
|
}
|
||||||
|
|
||||||
for (i, knobs) in FILTER_KNOBS.iter().enumerate() {
|
fn filter_slot(cx: &mut Context, slot: usize) {
|
||||||
HStack::new(cx, |cx| {
|
let type_id = TYPE_PARAMS[slot];
|
||||||
VStack::new(cx, |cx| {
|
VStack::new(cx, |cx| {
|
||||||
Label::new(cx, format!("FILTER {}", i + 1)).class("knob-label");
|
HStack::new(cx, |cx| {
|
||||||
|
Element::new(cx)
|
||||||
|
.class("osc-enable")
|
||||||
|
.top(Stretch(1.0))
|
||||||
|
.bottom(Stretch(1.0));
|
||||||
|
|
||||||
|
Label::new(cx, format!("FILTER {}", slot + 1))
|
||||||
|
.class("osc-label")
|
||||||
|
.left(Pixels(6.0))
|
||||||
|
.top(Stretch(1.0))
|
||||||
|
.bottom(Stretch(1.0));
|
||||||
|
|
||||||
|
Element::new(cx).width(Stretch(1.0));
|
||||||
|
|
||||||
|
Binding::new(
|
||||||
|
cx,
|
||||||
|
AppData::params.idx(type_id as usize),
|
||||||
|
move |cx, lens| {
|
||||||
|
let cur = lens.get(cx).round() as usize;
|
||||||
HStack::new(cx, |cx| {
|
HStack::new(cx, |cx| {
|
||||||
for t in TYPES {
|
for (i, &t) in FILTER_TYPES.iter().enumerate() {
|
||||||
Label::new(cx, t)
|
Label::new(cx, t)
|
||||||
.class("tab")
|
.class("filter-type-btn")
|
||||||
.padding(Pixels(3.0))
|
.checked(cur == i)
|
||||||
.height(Pixels(20.0));
|
.on_press(move |cx| {
|
||||||
|
cx.emit(AppEvent::SetParamRaw(type_id, i as f32));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.top(Pixels(4.0));
|
.horizontal_gap(Pixels(2.0));
|
||||||
Element::new(cx)
|
},
|
||||||
.width(Pixels(170.0))
|
);
|
||||||
.height(Pixels(52.0))
|
})
|
||||||
.background_color(Color::from("#12121c"))
|
.height(Pixels(22.0));
|
||||||
.corner_radius(Pixels(3.0))
|
|
||||||
.top(Pixels(4.0));
|
FilterResponseDisplay::new(cx, slot)
|
||||||
})
|
.width(Stretch(1.0))
|
||||||
.width(Pixels(178.0));
|
.height(Stretch(1.0));
|
||||||
HStack::new(cx, |cx| {
|
|
||||||
for &p in knobs.iter() {
|
HStack::new(cx, |cx| {
|
||||||
labeled_knob(cx, p);
|
for &p in FILTER_KNOBS[slot].iter() {
|
||||||
}
|
labeled_knob(cx, p);
|
||||||
})
|
}
|
||||||
.horizontal_gap(Pixels(2.0))
|
})
|
||||||
.left(Pixels(10.0));
|
.horizontal_gap(Pixels(4.0))
|
||||||
})
|
.height(Pixels(60.0))
|
||||||
.background_color(Color::from("#1a1a28"))
|
.padding_top(Pixels(4.0));
|
||||||
.corner_radius(Pixels(4.0))
|
|
||||||
.padding(Pixels(8.0))
|
|
||||||
.height(Pixels(120.0));
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.vertical_gap(Pixels(8.0))
|
.background_color(Color::from("#111119"))
|
||||||
.padding(Pixels(12.0));
|
.corner_radius(Pixels(4.0))
|
||||||
|
.padding(Pixels(8.0))
|
||||||
|
.width(Stretch(1.0))
|
||||||
|
.vertical_gap(Pixels(4.0));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::app::{AppData, AppEvent};
|
||||||
use crate::widgets::knob::labeled_knob;
|
use crate::widgets::knob::labeled_knob;
|
||||||
use params::ParamId;
|
use params::ParamId;
|
||||||
use vizia::prelude::*;
|
use vizia::prelude::*;
|
||||||
@@ -9,11 +10,11 @@ struct Slot {
|
|||||||
|
|
||||||
const CHAIN: &[Slot] = &[
|
const CHAIN: &[Slot] = &[
|
||||||
Slot {
|
Slot {
|
||||||
name: "DIST",
|
name: "Distortion",
|
||||||
params: &[ParamId::DistDrive],
|
params: &[ParamId::DistDrive],
|
||||||
},
|
},
|
||||||
Slot {
|
Slot {
|
||||||
name: "CHORUS",
|
name: "Chorus",
|
||||||
params: &[
|
params: &[
|
||||||
ParamId::ChorusRate,
|
ParamId::ChorusRate,
|
||||||
ParamId::ChorusDepth,
|
ParamId::ChorusDepth,
|
||||||
@@ -21,7 +22,7 @@ const CHAIN: &[Slot] = &[
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
Slot {
|
Slot {
|
||||||
name: "PHASER",
|
name: "Phaser",
|
||||||
params: &[
|
params: &[
|
||||||
ParamId::PhaserRate,
|
ParamId::PhaserRate,
|
||||||
ParamId::PhaserDepth,
|
ParamId::PhaserDepth,
|
||||||
@@ -29,7 +30,7 @@ const CHAIN: &[Slot] = &[
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
Slot {
|
Slot {
|
||||||
name: "REVERB",
|
name: "Reverb",
|
||||||
params: &[
|
params: &[
|
||||||
ParamId::ReverbSize,
|
ParamId::ReverbSize,
|
||||||
ParamId::ReverbDamping,
|
ParamId::ReverbDamping,
|
||||||
@@ -37,7 +38,7 @@ const CHAIN: &[Slot] = &[
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
Slot {
|
Slot {
|
||||||
name: "DELAY",
|
name: "Delay",
|
||||||
params: &[
|
params: &[
|
||||||
ParamId::DelayTime,
|
ParamId::DelayTime,
|
||||||
ParamId::DelayFeedback,
|
ParamId::DelayFeedback,
|
||||||
@@ -56,40 +57,95 @@ const CHAIN: &[Slot] = &[
|
|||||||
];
|
];
|
||||||
|
|
||||||
pub fn build(cx: &mut Context) {
|
pub fn build(cx: &mut Context) {
|
||||||
VStack::new(cx, |cx| {
|
HStack::new(cx, |cx| {
|
||||||
Label::new(cx, "FX CHAIN")
|
VStack::new(cx, |cx| {
|
||||||
.class("section-title")
|
Label::new(cx, "EFFECTS")
|
||||||
.bottom(Pixels(6.0));
|
.class("section-title")
|
||||||
for slot in CHAIN {
|
.padding(Pixels(10.0))
|
||||||
fx_row(cx, slot);
|
.padding_bottom(Pixels(4.0));
|
||||||
}
|
|
||||||
|
for (i, slot) in CHAIN.iter().enumerate() {
|
||||||
|
fx_list_item(cx, i, slot.name);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.width(Pixels(200.0))
|
||||||
|
.background_color(Color::from("#0e0e18"))
|
||||||
|
.border_width(Pixels(1.0))
|
||||||
|
.border_color(Color::from("#1e1e30"));
|
||||||
|
|
||||||
|
Binding::new(cx, AppData::fx_selected, |cx, sel_lens| {
|
||||||
|
let sel = sel_lens.get(cx);
|
||||||
|
let slot = &CHAIN[sel.min(CHAIN.len() - 1)];
|
||||||
|
fx_detail(cx, sel, slot);
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.vertical_gap(Pixels(5.0))
|
.width(Stretch(1.0))
|
||||||
.padding(Pixels(12.0));
|
.height(Stretch(1.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fx_row(cx: &mut Context, slot: &'static Slot) {
|
fn fx_list_item(cx: &mut Context, slot_idx: usize, name: &'static str) {
|
||||||
HStack::new(cx, |cx| {
|
Binding::new(cx, AppData::fx_selected, move |cx, sel_lens| {
|
||||||
Element::new(cx)
|
let selected = sel_lens.get(cx) == slot_idx;
|
||||||
.width(Pixels(10.0))
|
Binding::new(cx, AppData::fx_enabled.idx(slot_idx), move |cx, en_lens| {
|
||||||
.height(Pixels(10.0))
|
let enabled = en_lens.get(cx);
|
||||||
.background_color(Color::from("#ff7830"))
|
HStack::new(cx, |cx| {
|
||||||
.corner_radius(Pixels(5.0));
|
Label::new(cx, "")
|
||||||
Label::new(cx, slot.name)
|
.class("fx-bypass")
|
||||||
.color(Color::from("#d8d8e8"))
|
.checked(enabled)
|
||||||
.width(Pixels(55.0))
|
.top(Stretch(1.0))
|
||||||
.left(Pixels(8.0));
|
.bottom(Stretch(1.0))
|
||||||
|
.on_press(move |cx| cx.emit(AppEvent::ToggleFx(slot_idx)));
|
||||||
|
|
||||||
|
Label::new(cx, name)
|
||||||
|
.class("fx-item-name")
|
||||||
|
.checked(selected)
|
||||||
|
.left(Pixels(8.0))
|
||||||
|
.top(Stretch(1.0))
|
||||||
|
.bottom(Stretch(1.0))
|
||||||
|
.color(if selected {
|
||||||
|
Color::from("#ff8c00")
|
||||||
|
} else {
|
||||||
|
Color::from("#707090")
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.class("fx-list-item")
|
||||||
|
.checked(selected)
|
||||||
|
.height(Pixels(40.0))
|
||||||
|
.on_press(move |cx| cx.emit(AppEvent::SelectFx(slot_idx)));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fx_detail(cx: &mut Context, slot_idx: usize, slot: &'static Slot) {
|
||||||
|
VStack::new(cx, |cx| {
|
||||||
|
HStack::new(cx, |cx| {
|
||||||
|
Binding::new(cx, AppData::fx_enabled.idx(slot_idx), move |cx, en_lens| {
|
||||||
|
let enabled = en_lens.get(cx);
|
||||||
|
Label::new(cx, "")
|
||||||
|
.class("fx-bypass")
|
||||||
|
.checked(enabled)
|
||||||
|
.top(Stretch(1.0))
|
||||||
|
.bottom(Stretch(1.0))
|
||||||
|
.on_press(move |cx| cx.emit(AppEvent::ToggleFx(slot_idx)));
|
||||||
|
});
|
||||||
|
Label::new(cx, slot.name)
|
||||||
|
.class("section-title")
|
||||||
|
.left(Pixels(8.0))
|
||||||
|
.top(Stretch(1.0))
|
||||||
|
.bottom(Stretch(1.0));
|
||||||
|
})
|
||||||
|
.height(Pixels(36.0))
|
||||||
|
.padding(Pixels(8.0));
|
||||||
|
|
||||||
HStack::new(cx, |cx| {
|
HStack::new(cx, |cx| {
|
||||||
for &p in slot.params {
|
for &p in slot.params {
|
||||||
labeled_knob(cx, p);
|
labeled_knob(cx, p);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.horizontal_gap(Pixels(2.0));
|
.horizontal_gap(Pixels(12.0))
|
||||||
|
.padding(Pixels(16.0));
|
||||||
})
|
})
|
||||||
.background_color(Color::from("#1a1a28"))
|
.width(Stretch(1.0))
|
||||||
.corner_radius(Pixels(4.0))
|
.height(Stretch(1.0))
|
||||||
.padding(Pixels(8.0))
|
.background_color(Color::from("#0c0c14"));
|
||||||
.height(Pixels(68.0))
|
|
||||||
.padding_top(Stretch(1.0))
|
|
||||||
.padding_bottom(Stretch(1.0));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,33 @@
|
|||||||
use crate::app::AppData;
|
use crate::app::{AppData, AppEvent, Panel};
|
||||||
|
use crate::widgets::spectrum::SpectrumWidget;
|
||||||
use vizia::prelude::*;
|
use vizia::prelude::*;
|
||||||
|
|
||||||
pub fn build(cx: &mut Context) {
|
pub fn build(cx: &mut Context) {
|
||||||
HStack::new(cx, |cx| {
|
HStack::new(cx, |cx| {
|
||||||
Label::new(cx, AppData::preset_name)
|
Label::new(cx, "TENKO")
|
||||||
.font_size(13.0)
|
.color(Color::from("#ff8c00"))
|
||||||
.color(Color::from("#d8d8e8"))
|
.font_size(14.0)
|
||||||
.width(Pixels(180.0));
|
.font_weight(FontWeightKeyword::Bold)
|
||||||
|
.width(Pixels(72.0));
|
||||||
|
|
||||||
Element::new(cx).width(Stretch(1.0)).height(Stretch(1.0));
|
for &p in Panel::ALL {
|
||||||
|
Binding::new(cx, AppData::active_panel, move |cx, active_lens| {
|
||||||
|
let al = active_lens.get(cx) == p;
|
||||||
|
Label::new(cx, p.label())
|
||||||
|
.class("top-tab")
|
||||||
|
.checked(al)
|
||||||
|
.on_press(move |cx| cx.emit(AppEvent::SetPanel(p)))
|
||||||
|
.height(Stretch(1.0));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Element::new(cx).width(Stretch(1.0));
|
||||||
|
|
||||||
|
SpectrumWidget::new(cx, 80)
|
||||||
|
.width(Pixels(160.0))
|
||||||
|
.height(Pixels(22.0));
|
||||||
|
|
||||||
|
Element::new(cx).width(Pixels(12.0));
|
||||||
|
|
||||||
stat_pair(cx, "BPM", AppData::host_bpm.map(|b| format!("{b:.0}")));
|
stat_pair(cx, "BPM", AppData::host_bpm.map(|b| format!("{b:.0}")));
|
||||||
stat_pair(cx, "VOICES", AppData::voice_count.map(|v| format!("{v}")));
|
stat_pair(cx, "VOICES", AppData::voice_count.map(|v| format!("{v}")));
|
||||||
@@ -17,22 +36,31 @@ pub fn build(cx: &mut Context) {
|
|||||||
"CPU",
|
"CPU",
|
||||||
AppData::cpu_load.map(|c| format!("{:.0}%", c * 100.0)),
|
AppData::cpu_load.map(|c| format!("{:.0}%", c * 100.0)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Element::new(cx).width(Pixels(12.0));
|
||||||
|
|
||||||
|
Label::new(cx, AppData::preset_name)
|
||||||
|
.color(Color::from("#c0c0d8"))
|
||||||
|
.font_size(12.0)
|
||||||
|
.width(Pixels(140.0));
|
||||||
})
|
})
|
||||||
.height(Pixels(34.0))
|
.height(Pixels(36.0))
|
||||||
.background_color(Color::from("#0e0e1a"))
|
.background_color(Color::from("#09090f"))
|
||||||
.padding(Pixels(8.0))
|
.padding(Pixels(6.0))
|
||||||
.padding_top(Stretch(1.0))
|
.padding_top(Stretch(1.0))
|
||||||
.padding_bottom(Stretch(1.0))
|
.padding_bottom(Stretch(1.0))
|
||||||
.horizontal_gap(Pixels(16.0));
|
.horizontal_gap(Pixels(0.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stat_pair(cx: &mut Context, key: &'static str, val_lens: impl Lens<Target = String>) {
|
fn stat_pair(cx: &mut Context, key: &'static str, val_lens: impl Lens<Target = String>) {
|
||||||
HStack::new(cx, |cx| {
|
HStack::new(cx, |cx| {
|
||||||
Label::new(cx, key)
|
Label::new(cx, key)
|
||||||
.class("knob-label")
|
.class("knob-label")
|
||||||
.color(Color::from("#888899"));
|
.color(Color::from("#525270"));
|
||||||
Label::new(cx, val_lens)
|
Label::new(cx, val_lens)
|
||||||
.color(Color::from("#ff7830"))
|
.color(Color::from("#ff8c00"))
|
||||||
.left(Pixels(4.0));
|
.left(Pixels(3.0))
|
||||||
});
|
.font_size(10.0);
|
||||||
|
})
|
||||||
|
.padding(Pixels(4.0));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::app::AppEvent;
|
||||||
use crate::widgets::{knob::labeled_knob, wavetable_display::WavetableDisplay};
|
use crate::widgets::{knob::labeled_knob, wavetable_display::WavetableDisplay};
|
||||||
use params::ParamId;
|
use params::ParamId;
|
||||||
use vizia::prelude::*;
|
use vizia::prelude::*;
|
||||||
@@ -29,36 +30,20 @@ const LFO_KNOBS: [[ParamId; 4]; 4] = [
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const SYNC_PARAMS: [ParamId; 4] = [
|
||||||
|
ParamId::Lfo1Sync,
|
||||||
|
ParamId::Lfo2Sync,
|
||||||
|
ParamId::Lfo3Sync,
|
||||||
|
ParamId::Lfo4Sync,
|
||||||
|
];
|
||||||
|
|
||||||
pub fn build(cx: &mut Context) {
|
pub fn build(cx: &mut Context) {
|
||||||
VStack::new(cx, |cx| {
|
VStack::new(cx, |cx| {
|
||||||
Label::new(cx, "LFOs")
|
Label::new(cx, "LFOs")
|
||||||
.class("section-title")
|
.class("section-title")
|
||||||
.bottom(Pixels(6.0));
|
.bottom(Pixels(6.0));
|
||||||
for (i, knobs) in LFO_KNOBS.iter().enumerate() {
|
for (i, knobs) in LFO_KNOBS.iter().enumerate() {
|
||||||
HStack::new(cx, |cx| {
|
lfo_row(cx, i, knobs);
|
||||||
VStack::new(cx, |cx| {
|
|
||||||
Label::new(cx, format!("LFO {}", i + 1)).class("knob-label");
|
|
||||||
let sine: Vec<f32> = (0..96)
|
|
||||||
.map(|j| (j as f32 / 96.0 * std::f32::consts::TAU).sin())
|
|
||||||
.collect();
|
|
||||||
WavetableDisplay::new(cx, sine)
|
|
||||||
.width(Pixels(110.0))
|
|
||||||
.height(Pixels(52.0));
|
|
||||||
})
|
|
||||||
.width(Pixels(118.0));
|
|
||||||
HStack::new(cx, |cx| {
|
|
||||||
for &p in knobs.iter() {
|
|
||||||
labeled_knob(cx, p);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.horizontal_gap(Pixels(2.0))
|
|
||||||
.left(Pixels(10.0));
|
|
||||||
Label::new(cx, "SYNC").class("knob-label").left(Pixels(8.0));
|
|
||||||
})
|
|
||||||
.background_color(Color::from("#1a1a28"))
|
|
||||||
.corner_radius(Pixels(4.0))
|
|
||||||
.padding(Pixels(8.0))
|
|
||||||
.height(Pixels(84.0));
|
|
||||||
}
|
}
|
||||||
Button::new(cx, |cx| Label::new(cx, "+ LFO"))
|
Button::new(cx, |cx| Label::new(cx, "+ LFO"))
|
||||||
.class("add-btn")
|
.class("add-btn")
|
||||||
@@ -67,3 +52,43 @@ pub fn build(cx: &mut Context) {
|
|||||||
.vertical_gap(Pixels(8.0))
|
.vertical_gap(Pixels(8.0))
|
||||||
.padding(Pixels(12.0));
|
.padding(Pixels(12.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn lfo_row(cx: &mut Context, idx: usize, knobs: &'static [ParamId; 4]) {
|
||||||
|
let sync_id = SYNC_PARAMS[idx];
|
||||||
|
HStack::new(cx, |cx| {
|
||||||
|
VStack::new(cx, |cx| {
|
||||||
|
Label::new(cx, format!("LFO {}", idx + 1)).class("knob-label");
|
||||||
|
WavetableDisplay::with_live_param(cx, knobs[3])
|
||||||
|
.width(Pixels(110.0))
|
||||||
|
.height(Pixels(52.0));
|
||||||
|
})
|
||||||
|
.width(Pixels(118.0));
|
||||||
|
|
||||||
|
HStack::new(cx, |cx| {
|
||||||
|
for &p in knobs.iter() {
|
||||||
|
labeled_knob(cx, p);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.horizontal_gap(Pixels(2.0))
|
||||||
|
.left(Pixels(10.0));
|
||||||
|
|
||||||
|
Binding::new(
|
||||||
|
cx,
|
||||||
|
crate::app::AppData::params.idx(sync_id as usize),
|
||||||
|
move |cx, lens| {
|
||||||
|
let synced = lens.get(cx) > 0.5;
|
||||||
|
Label::new(cx, "SYNC")
|
||||||
|
.class("sync-toggle")
|
||||||
|
.checked(synced)
|
||||||
|
.on_press(move |cx| {
|
||||||
|
cx.emit(AppEvent::SetParam(sync_id, if synced { 0.0 } else { 1.0 }));
|
||||||
|
})
|
||||||
|
.left(Pixels(8.0));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.background_color(Color::from("#1a1a28"))
|
||||||
|
.corner_radius(Pixels(4.0))
|
||||||
|
.padding(Pixels(8.0))
|
||||||
|
.height(Pixels(84.0));
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,22 +2,19 @@ use crate::widgets::knob::labeled_knob;
|
|||||||
use params::ParamId;
|
use params::ParamId;
|
||||||
use vizia::prelude::*;
|
use vizia::prelude::*;
|
||||||
|
|
||||||
const MACROS: [ParamId; 8] = [
|
const MACROS: [ParamId; 4] = [
|
||||||
ParamId::Macro1,
|
ParamId::Macro1,
|
||||||
ParamId::Macro2,
|
ParamId::Macro2,
|
||||||
ParamId::Macro3,
|
ParamId::Macro3,
|
||||||
ParamId::Macro4,
|
ParamId::Macro4,
|
||||||
ParamId::Macro5,
|
|
||||||
ParamId::Macro6,
|
|
||||||
ParamId::Macro7,
|
|
||||||
ParamId::Macro8,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
pub fn build(cx: &mut Context) {
|
pub fn build(cx: &mut Context) {
|
||||||
VStack::new(cx, |cx| {
|
VStack::new(cx, |cx| {
|
||||||
Label::new(cx, "MACRO")
|
Label::new(cx, "MACRO")
|
||||||
.class("section-title")
|
.class("section-title")
|
||||||
.bottom(Pixels(6.0));
|
.padding_bottom(Pixels(6.0));
|
||||||
|
|
||||||
for &m in &MACROS {
|
for &m in &MACROS {
|
||||||
VStack::new(cx, |cx| {
|
VStack::new(cx, |cx| {
|
||||||
labeled_knob(cx, m);
|
labeled_knob(cx, m);
|
||||||
@@ -25,9 +22,26 @@ pub fn build(cx: &mut Context) {
|
|||||||
.class("macro-knob")
|
.class("macro-knob")
|
||||||
.bottom(Pixels(3.0));
|
.bottom(Pixels(3.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Element::new(cx).height(Stretch(1.0));
|
||||||
|
|
||||||
|
macro_wheel_label(cx, "PITCH\nWHL");
|
||||||
|
macro_wheel_label(cx, "MOD\nWHL");
|
||||||
})
|
})
|
||||||
.width(Pixels(68.0))
|
.width(Pixels(68.0))
|
||||||
.background_color(Color::from("#16162a"))
|
.background_color(Color::from("#0c0c14"))
|
||||||
.padding(Pixels(8.0))
|
.padding(Pixels(8.0))
|
||||||
.vertical_gap(Pixels(2.0));
|
.vertical_gap(Pixels(2.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn macro_wheel_label(cx: &mut Context, text: &'static str) {
|
||||||
|
Label::new(cx, text)
|
||||||
|
.class("knob-label")
|
||||||
|
.width(Stretch(1.0))
|
||||||
|
.height(Pixels(28.0))
|
||||||
|
.background_color(Color::from("#14141e"))
|
||||||
|
.corner_radius(Pixels(3.0))
|
||||||
|
.padding(Pixels(3.0))
|
||||||
|
.text_align(TextAlign::Center)
|
||||||
|
.bottom(Pixels(4.0));
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
pub mod advanced;
|
||||||
|
pub mod env_lfo_sidebar;
|
||||||
pub mod env_panel;
|
pub mod env_panel;
|
||||||
pub mod filter;
|
pub mod filter;
|
||||||
pub mod fx;
|
pub mod fx;
|
||||||
@@ -6,3 +8,4 @@ pub mod lfo;
|
|||||||
pub mod macro_bar;
|
pub mod macro_bar;
|
||||||
pub mod mod_matrix;
|
pub mod mod_matrix;
|
||||||
pub mod osc;
|
pub mod osc;
|
||||||
|
pub mod voice;
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
use crate::app::{AppData, AppEvent, MOD_NDST, MOD_NSRC};
|
||||||
use vizia::prelude::*;
|
use vizia::prelude::*;
|
||||||
|
use vizia::vg;
|
||||||
|
|
||||||
const SOURCES: &[&str] = &[
|
const SOURCES: [&str; MOD_NSRC] = [
|
||||||
"ENV1", "ENV2", "ENV3", "LFO1", "LFO2", "LFO3", "LFO4", "VEL", "NOTE", "MOD", "AFT", "M1",
|
"ENV1", "ENV2", "ENV3", "LFO1", "LFO2", "LFO3", "LFO4", "VEL", "NOTE", "MOD", "AFT", "M1",
|
||||||
"M2", "M3", "M4", "M5", "M6", "M7", "M8",
|
"M2", "M3", "M4", "M5", "M6", "M7", "M8",
|
||||||
];
|
];
|
||||||
const DESTS: &[&str] = &[
|
const DESTS: [&str; MOD_NDST] = [
|
||||||
"CUT1", "RES1", "CUT2", "RES2", "P1", "P2", "P3", "AMP", "PAN", "LR1", "LR2",
|
"CUT1", "RES1", "CUT2", "RES2", "P1", "P2", "P3", "AMP", "PAN", "LR1", "LR2",
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -12,33 +14,32 @@ pub fn build(cx: &mut Context) {
|
|||||||
VStack::new(cx, |cx| {
|
VStack::new(cx, |cx| {
|
||||||
Label::new(cx, "MOD MATRIX — 64 slots")
|
Label::new(cx, "MOD MATRIX — 64 slots")
|
||||||
.class("section-title")
|
.class("section-title")
|
||||||
.bottom(Pixels(6.0));
|
.bottom(Pixels(4.0));
|
||||||
Label::new(cx, "Click cell → depth · Ctrl+click → clear")
|
Label::new(cx, "Drag cell · Dbl-click to reset")
|
||||||
.class("knob-label")
|
.class("knob-label")
|
||||||
.bottom(Pixels(8.0));
|
.bottom(Pixels(8.0));
|
||||||
|
|
||||||
ScrollView::new(cx, |cx| {
|
ScrollView::new(cx, |cx| {
|
||||||
VStack::new(cx, |cx| {
|
VStack::new(cx, |cx| {
|
||||||
// column header
|
|
||||||
HStack::new(cx, |cx| {
|
HStack::new(cx, |cx| {
|
||||||
Element::new(cx).width(Pixels(48.0));
|
Element::new(cx).width(Pixels(50.0));
|
||||||
for &d in DESTS {
|
for &d in &DESTS {
|
||||||
Label::new(cx, d)
|
Label::new(cx, d)
|
||||||
.class("knob-label")
|
.class("knob-label")
|
||||||
.width(Pixels(38.0))
|
.width(Pixels(38.0))
|
||||||
.text_align(TextAlign::Center);
|
.text_align(TextAlign::Center);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.height(Pixels(20.0));
|
.height(Pixels(18.0));
|
||||||
|
|
||||||
for &src in SOURCES {
|
for (si, &src) in SOURCES.iter().enumerate() {
|
||||||
HStack::new(cx, |cx| {
|
HStack::new(cx, |cx| {
|
||||||
Label::new(cx, src).class("knob-label").width(Pixels(48.0));
|
Label::new(cx, src).class("knob-label").width(Pixels(50.0));
|
||||||
for _ in DESTS {
|
for di in 0..MOD_NDST {
|
||||||
mod_cell(cx);
|
ModCell::new(cx, si, di);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.height(Pixels(34.0))
|
.height(Pixels(32.0))
|
||||||
.padding_top(Stretch(1.0))
|
.padding_top(Stretch(1.0))
|
||||||
.padding_bottom(Stretch(1.0));
|
.padding_bottom(Stretch(1.0));
|
||||||
}
|
}
|
||||||
@@ -48,14 +49,101 @@ pub fn build(cx: &mut Context) {
|
|||||||
.padding(Pixels(12.0));
|
.padding(Pixels(12.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mod_cell(cx: &mut Context) {
|
struct ModCell {
|
||||||
Element::new(cx)
|
src: usize,
|
||||||
.width(Pixels(34.0))
|
dst: usize,
|
||||||
.height(Pixels(26.0))
|
depth: f32,
|
||||||
.background_color(Color::rgb(30, 30, 48))
|
drag_y: Option<f32>,
|
||||||
.border_color(Color::rgb(42, 42, 68))
|
drag_v: f32,
|
||||||
.border_width(Pixels(1.0))
|
}
|
||||||
.corner_radius(Pixels(3.0))
|
|
||||||
.left(Pixels(2.0))
|
impl ModCell {
|
||||||
.cursor(CursorIcon::Hand);
|
fn new(cx: &mut Context, src: usize, dst: usize) -> Handle<Self> {
|
||||||
|
let cell_idx = src * MOD_NDST + dst;
|
||||||
|
let init = cx
|
||||||
|
.data::<AppData>()
|
||||||
|
.map(|d| d.mod_depths[cell_idx])
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
Self {
|
||||||
|
src,
|
||||||
|
dst,
|
||||||
|
depth: init,
|
||||||
|
drag_y: None,
|
||||||
|
drag_v: 0.0,
|
||||||
|
}
|
||||||
|
.build(cx, |_| {})
|
||||||
|
.bind(AppData::mod_depths.idx(cell_idx), |h, l| {
|
||||||
|
let v = l.get(&h);
|
||||||
|
h.modify(|c: &mut ModCell| c.depth = v);
|
||||||
|
})
|
||||||
|
.class("mod-cell")
|
||||||
|
.width(Pixels(34.0))
|
||||||
|
.height(Pixels(24.0))
|
||||||
|
.left(Pixels(2.0))
|
||||||
|
.cursor(CursorIcon::NsResize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for ModCell {
|
||||||
|
fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
|
||||||
|
event.map(|we: &WindowEvent, _| match we {
|
||||||
|
WindowEvent::MouseDown(MouseButton::Left) => {
|
||||||
|
cx.capture();
|
||||||
|
self.drag_y = Some(cx.mouse().cursor_y);
|
||||||
|
self.drag_v = self.depth;
|
||||||
|
}
|
||||||
|
WindowEvent::MouseUp(MouseButton::Left) => {
|
||||||
|
cx.release();
|
||||||
|
self.drag_y = None;
|
||||||
|
}
|
||||||
|
WindowEvent::MouseMove(_, y) => {
|
||||||
|
if let Some(oy) = self.drag_y {
|
||||||
|
let new_v = (self.drag_v + (oy - y) * 0.006).clamp(-1.0, 1.0);
|
||||||
|
cx.emit(AppEvent::SetModDepth {
|
||||||
|
src: self.src,
|
||||||
|
dst: self.dst,
|
||||||
|
depth: new_v,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WindowEvent::MouseDoubleClick(MouseButton::Left) => {
|
||||||
|
cx.emit(AppEvent::SetModDepth {
|
||||||
|
src: self.src,
|
||||||
|
dst: self.dst,
|
||||||
|
depth: 0.0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&self, cx: &mut DrawContext, canvas: &Canvas) {
|
||||||
|
let b = cx.bounds();
|
||||||
|
let mut p = vg::Paint::default();
|
||||||
|
|
||||||
|
p.set_style(vg::PaintStyle::Fill);
|
||||||
|
p.set_color(vg::Color::from_argb(255, 26, 26, 44));
|
||||||
|
canvas.draw_round_rect(vg::Rect::from_xywh(b.x, b.y, b.w, b.h), 3.0, 3.0, &p);
|
||||||
|
|
||||||
|
if self.depth.abs() > 0.01 {
|
||||||
|
let fh = self.depth.abs() * b.h;
|
||||||
|
let (fy, color) = if self.depth > 0.0 {
|
||||||
|
(b.y + b.h - fh, vg::Color::from_argb(170, 255, 120, 48))
|
||||||
|
} else {
|
||||||
|
(b.y, vg::Color::from_argb(170, 80, 140, 255))
|
||||||
|
};
|
||||||
|
p.set_color(color);
|
||||||
|
canvas.draw_round_rect(
|
||||||
|
vg::Rect::from_xywh(b.x + 2.0, fy, b.w - 4.0, fh),
|
||||||
|
2.0,
|
||||||
|
2.0,
|
||||||
|
&p,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
p.set_style(vg::PaintStyle::Stroke);
|
||||||
|
p.set_stroke_width(1.0);
|
||||||
|
p.set_color(vg::Color::from_argb(255, 40, 40, 66));
|
||||||
|
canvas.draw_round_rect(vg::Rect::from_xywh(b.x, b.y, b.w, b.h), 3.0, 3.0, &p);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,40 +37,72 @@ const OSC_KNOBS: [[ParamId; 8]; 3] = [
|
|||||||
|
|
||||||
pub fn build(cx: &mut Context) {
|
pub fn build(cx: &mut Context) {
|
||||||
VStack::new(cx, |cx| {
|
VStack::new(cx, |cx| {
|
||||||
Label::new(cx, "OSCILLATORS")
|
|
||||||
.class("section-title")
|
|
||||||
.bottom(Pixels(6.0));
|
|
||||||
for (i, knobs) in OSC_KNOBS.iter().enumerate() {
|
for (i, knobs) in OSC_KNOBS.iter().enumerate() {
|
||||||
osc_row(cx, i + 1, knobs);
|
osc_strip(cx, i, knobs);
|
||||||
}
|
}
|
||||||
Button::new(cx, |cx| Label::new(cx, "+ OSC"))
|
|
||||||
.class("add-btn")
|
|
||||||
.top(Pixels(6.0));
|
|
||||||
})
|
})
|
||||||
.vertical_gap(Pixels(8.0))
|
.vertical_gap(Pixels(3.0))
|
||||||
.padding(Pixels(12.0));
|
.padding(Pixels(8.0))
|
||||||
|
.height(Stretch(1.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn osc_row(cx: &mut Context, idx: usize, knobs: &'static [ParamId; 8]) {
|
fn osc_strip(cx: &mut Context, idx: usize, knobs: &'static [ParamId; 8]) {
|
||||||
HStack::new(cx, |cx| {
|
HStack::new(cx, |cx| {
|
||||||
|
Element::new(cx)
|
||||||
|
.class("osc-enable")
|
||||||
|
.top(Stretch(1.0))
|
||||||
|
.bottom(Stretch(1.0));
|
||||||
|
|
||||||
|
Label::new(cx, format!("OSC\n{}", idx + 1))
|
||||||
|
.class("osc-label")
|
||||||
|
.width(Pixels(24.0))
|
||||||
|
.top(Stretch(1.0))
|
||||||
|
.bottom(Stretch(1.0));
|
||||||
|
|
||||||
VStack::new(cx, |cx| {
|
VStack::new(cx, |cx| {
|
||||||
Label::new(cx, format!("OSC {idx}")).class("knob-label");
|
labeled_knob(cx, knobs[2]);
|
||||||
let sine: Vec<f32> = (0..128)
|
labeled_knob(cx, knobs[3]);
|
||||||
.map(|i| (i as f32 / 128.0 * std::f32::consts::TAU).sin())
|
|
||||||
.collect();
|
|
||||||
WavetableDisplay::new(cx, sine);
|
|
||||||
})
|
})
|
||||||
.width(Pixels(158.0));
|
.width(Pixels(54.0))
|
||||||
HStack::new(cx, |cx| {
|
.top(Stretch(1.0))
|
||||||
for &p in knobs {
|
.bottom(Stretch(1.0));
|
||||||
labeled_knob(cx, p);
|
|
||||||
}
|
WavetableDisplay::with_live_param(cx, knobs[7])
|
||||||
|
.width(Stretch(1.0))
|
||||||
|
.height(Stretch(1.0));
|
||||||
|
|
||||||
|
VStack::new(cx, |cx| {
|
||||||
|
Label::new(cx, "UNISON")
|
||||||
|
.class("knob-label")
|
||||||
|
.bottom(Pixels(2.0));
|
||||||
|
HStack::new(cx, |cx| {
|
||||||
|
labeled_knob(cx, knobs[4]);
|
||||||
|
labeled_knob(cx, knobs[5]);
|
||||||
|
labeled_knob(cx, knobs[6]);
|
||||||
|
})
|
||||||
|
.horizontal_gap(Pixels(2.0));
|
||||||
})
|
})
|
||||||
.horizontal_gap(Pixels(2.0))
|
.width(Pixels(164.0))
|
||||||
.left(Pixels(10.0));
|
.top(Stretch(1.0))
|
||||||
|
.bottom(Stretch(1.0));
|
||||||
|
|
||||||
|
VStack::new(cx, |cx| {
|
||||||
|
Label::new(cx, "OUTPUT")
|
||||||
|
.class("knob-label")
|
||||||
|
.bottom(Pixels(2.0));
|
||||||
|
HStack::new(cx, |cx| {
|
||||||
|
labeled_knob(cx, knobs[0]);
|
||||||
|
labeled_knob(cx, knobs[1]);
|
||||||
|
})
|
||||||
|
.horizontal_gap(Pixels(2.0));
|
||||||
|
})
|
||||||
|
.width(Pixels(110.0))
|
||||||
|
.top(Stretch(1.0))
|
||||||
|
.bottom(Stretch(1.0));
|
||||||
})
|
})
|
||||||
.background_color(Color::from("#1a1a28"))
|
.class("osc-strip")
|
||||||
.corner_radius(Pixels(4.0))
|
|
||||||
.padding(Pixels(8.0))
|
.padding(Pixels(8.0))
|
||||||
.height(Pixels(98.0));
|
.padding_left(Pixels(10.0))
|
||||||
|
.height(Pixels(96.0))
|
||||||
|
.horizontal_gap(Pixels(10.0));
|
||||||
}
|
}
|
||||||
|
|||||||
10
crates/ui/src/panels/voice.rs
Normal file
10
crates/ui/src/panels/voice.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
use vizia::prelude::*;
|
||||||
|
|
||||||
|
pub fn build(cx: &mut Context) {
|
||||||
|
VStack::new(cx, |cx| {
|
||||||
|
crate::panels::osc::build(cx);
|
||||||
|
crate::panels::filter::build(cx);
|
||||||
|
})
|
||||||
|
.width(Stretch(1.0))
|
||||||
|
.height(Stretch(1.0));
|
||||||
|
}
|
||||||
@@ -3,47 +3,176 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
font-family: "Inter", "Segoe UI", sans-serif;
|
font-family: "Inter", "Segoe UI", "Liberation Sans", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
background-color: #1a1a28;
|
background-color: #14141e;
|
||||||
border-radius: 6px;
|
border-radius: 4px;
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab {
|
|
||||||
background-color: #22223a;
|
|
||||||
color: #888899;
|
|
||||||
padding: 6px 14px;
|
|
||||||
border-radius: 4px 4px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab.active {
|
|
||||||
background-color: #1a1a28;
|
|
||||||
color: #ff7830;
|
|
||||||
}
|
|
||||||
|
|
||||||
.knob-label {
|
|
||||||
color: #888899;
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
color: #ff7830;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.macro-knob {
|
|
||||||
background-color: #22223a;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-btn {
|
.section-title {
|
||||||
background-color: #22223a;
|
color: #ff8c00;
|
||||||
color: #888899;
|
font-size: 10px;
|
||||||
border-radius: 4px;
|
font-weight: 700;
|
||||||
padding: 4px 10px;
|
letter-spacing: 0.10em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knob-label {
|
||||||
|
color: #565672;
|
||||||
|
font-size: 9px;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-tab {
|
||||||
|
background-color: transparent;
|
||||||
|
color: #525270;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
padding: 0px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-tab:hover {
|
||||||
|
color: #b0b0cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-tab:checked {
|
||||||
|
color: #ff8c00;
|
||||||
|
border-bottom: 2px solid #ff8c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.env-tab,
|
||||||
|
.lfo-tab {
|
||||||
|
background-color: #1a1a28;
|
||||||
|
color: #525270;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.env-tab:checked,
|
||||||
|
.lfo-tab:checked {
|
||||||
|
background-color: #261a35;
|
||||||
|
color: #ff8c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.osc-strip {
|
||||||
|
background-color: #111119;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.osc-label {
|
||||||
|
color: #444460;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.osc-enable {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #ff8c00;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fx-list-item {
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 7px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fx-list-item:hover {
|
||||||
|
background-color: #1c1c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fx-list-item:checked {
|
||||||
|
background-color: #201830;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fx-item-name {
|
||||||
|
color: #707090;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fx-item-name:checked {
|
||||||
|
color: #ff8c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-type-btn {
|
||||||
|
background-color: #1e1e2e;
|
||||||
|
color: #525270;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 2px 5px;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-type-btn:checked {
|
||||||
|
background-color: #281640;
|
||||||
|
color: #c070ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-toggle {
|
||||||
|
background-color: #1e1e2e;
|
||||||
|
color: #525270;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-toggle:checked {
|
||||||
|
background-color: #162618;
|
||||||
|
color: #40c060;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fx-bypass {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #26263a;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fx-bypass:checked {
|
||||||
|
background-color: #ff8c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.macro-knob {
|
||||||
|
background-color: #1a1a28;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 6px 4px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn {
|
||||||
|
background-color: #1e1e2e;
|
||||||
|
color: #525270;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 3px 10px;
|
||||||
|
font-size: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn:hover {
|
||||||
|
color: #c0c0d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod-cell {
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: ns-resize;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,30 @@ impl EnvDisplay {
|
|||||||
release: g(ids[5]),
|
release: g(ids[5]),
|
||||||
}
|
}
|
||||||
.build(cx, |_| {})
|
.build(cx, |_| {})
|
||||||
|
.bind(crate::app::AppData::params.idx(ids[0] as usize), |h, l| {
|
||||||
|
let v = l.get(&h);
|
||||||
|
h.modify(|e: &mut EnvDisplay| e.delay = v);
|
||||||
|
})
|
||||||
|
.bind(crate::app::AppData::params.idx(ids[1] as usize), |h, l| {
|
||||||
|
let v = l.get(&h);
|
||||||
|
h.modify(|e: &mut EnvDisplay| e.attack = v);
|
||||||
|
})
|
||||||
|
.bind(crate::app::AppData::params.idx(ids[2] as usize), |h, l| {
|
||||||
|
let v = l.get(&h);
|
||||||
|
h.modify(|e: &mut EnvDisplay| e.hold = v);
|
||||||
|
})
|
||||||
|
.bind(crate::app::AppData::params.idx(ids[3] as usize), |h, l| {
|
||||||
|
let v = l.get(&h);
|
||||||
|
h.modify(|e: &mut EnvDisplay| e.decay = v);
|
||||||
|
})
|
||||||
|
.bind(crate::app::AppData::params.idx(ids[4] as usize), |h, l| {
|
||||||
|
let v = l.get(&h);
|
||||||
|
h.modify(|e: &mut EnvDisplay| e.sustain = v);
|
||||||
|
})
|
||||||
|
.bind(crate::app::AppData::params.idx(ids[5] as usize), |h, l| {
|
||||||
|
let v = l.get(&h);
|
||||||
|
h.modify(|e: &mut EnvDisplay| e.release = v);
|
||||||
|
})
|
||||||
.width(Pixels(210.0))
|
.width(Pixels(210.0))
|
||||||
.height(Pixels(72.0))
|
.height(Pixels(72.0))
|
||||||
}
|
}
|
||||||
@@ -66,6 +90,7 @@ impl View for EnvDisplay {
|
|||||||
fn draw(&self, cx: &mut DrawContext, canvas: &Canvas) {
|
fn draw(&self, cx: &mut DrawContext, canvas: &Canvas) {
|
||||||
let b = cx.bounds();
|
let b = cx.bounds();
|
||||||
let mut p = vg::Paint::default();
|
let mut p = vg::Paint::default();
|
||||||
|
|
||||||
p.set_color(vg::Color::from_argb(255, 18, 18, 30));
|
p.set_color(vg::Color::from_argb(255, 18, 18, 30));
|
||||||
canvas.draw_rect(vg::Rect::from_xywh(b.x, b.y, b.w, b.h), &p);
|
canvas.draw_rect(vg::Rect::from_xywh(b.x, b.y, b.w, b.h), &p);
|
||||||
|
|
||||||
|
|||||||
114
crates/ui/src/widgets/filter_response.rs
Normal file
114
crates/ui/src/widgets/filter_response.rs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
use params::ParamId;
|
||||||
|
use vizia::prelude::*;
|
||||||
|
use vizia::vg;
|
||||||
|
|
||||||
|
pub struct FilterResponseDisplay {
|
||||||
|
cutoff: f32,
|
||||||
|
resonance: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FilterResponseDisplay {
|
||||||
|
pub fn new(cx: &mut Context, filter_idx: usize) -> Handle<Self> {
|
||||||
|
let (cut_id, res_id) = if filter_idx == 0 {
|
||||||
|
(ParamId::Filter1Cutoff, ParamId::Filter1Resonance)
|
||||||
|
} else {
|
||||||
|
(ParamId::Filter2Cutoff, ParamId::Filter2Resonance)
|
||||||
|
};
|
||||||
|
let g = |id: ParamId| {
|
||||||
|
cx.data::<crate::app::AppData>()
|
||||||
|
.map(|d| d.params[id as usize])
|
||||||
|
.unwrap_or(id.default_value())
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
cutoff: g(cut_id),
|
||||||
|
resonance: g(res_id),
|
||||||
|
}
|
||||||
|
.build(cx, |_| {})
|
||||||
|
.bind(
|
||||||
|
crate::app::AppData::params.idx(cut_id as usize),
|
||||||
|
move |h, l| {
|
||||||
|
let v = l.get(&h);
|
||||||
|
h.modify(|d: &mut FilterResponseDisplay| d.cutoff = v);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.bind(
|
||||||
|
crate::app::AppData::params.idx(res_id as usize),
|
||||||
|
move |h, l| {
|
||||||
|
let v = l.get(&h);
|
||||||
|
h.modify(|d: &mut FilterResponseDisplay| d.resonance = v);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.width(Pixels(172.0))
|
||||||
|
.height(Pixels(54.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for FilterResponseDisplay {
|
||||||
|
fn draw(&self, cx: &mut DrawContext, canvas: &Canvas) {
|
||||||
|
let b = cx.bounds();
|
||||||
|
let mut p = vg::Paint::default();
|
||||||
|
|
||||||
|
p.set_color(vg::Color::from_argb(255, 10, 10, 20));
|
||||||
|
canvas.draw_rect(vg::Rect::from_xywh(b.x, b.y, b.w, b.h), &p);
|
||||||
|
|
||||||
|
p.set_style(vg::PaintStyle::Stroke);
|
||||||
|
p.set_stroke_width(0.5);
|
||||||
|
p.set_color(vg::Color::from_argb(35, 180, 180, 200));
|
||||||
|
{
|
||||||
|
let y0 = db_to_y(0.0, b.y, b.h);
|
||||||
|
let mut gl = vg::Path::new();
|
||||||
|
gl.move_to((b.x, y0));
|
||||||
|
gl.line_to((b.x + b.w, y0));
|
||||||
|
canvas.draw_path(&gl, &p);
|
||||||
|
}
|
||||||
|
|
||||||
|
const N: usize = 200;
|
||||||
|
let fc = 20.0f32 * (1000.0f32).powf(self.cutoff);
|
||||||
|
let q = 0.5 + self.resonance * 14.5;
|
||||||
|
|
||||||
|
let mut fill = vg::Path::new();
|
||||||
|
let mut stroke = vg::Path::new();
|
||||||
|
|
||||||
|
for i in 0..N {
|
||||||
|
let t = i as f32 / (N - 1) as f32;
|
||||||
|
let freq = 20.0f32 * (1000.0f32).powf(t);
|
||||||
|
let w = freq / fc;
|
||||||
|
let denom = ((1.0 - w * w).powi(2) + (w / q).powi(2)).sqrt().max(1e-9);
|
||||||
|
let db = 20.0 * (1.0 / denom).log10();
|
||||||
|
let x = b.x + t * b.w;
|
||||||
|
let y = db_to_y(db, b.y, b.h);
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
fill.move_to((x, b.y + b.h));
|
||||||
|
fill.line_to((x, y));
|
||||||
|
stroke.move_to((x, y));
|
||||||
|
} else {
|
||||||
|
fill.line_to((x, y));
|
||||||
|
stroke.line_to((x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fill.line_to((b.x + b.w, b.y + b.h));
|
||||||
|
|
||||||
|
let mut fp = vg::Paint::default();
|
||||||
|
fp.set_style(vg::PaintStyle::Fill);
|
||||||
|
fp.set_color(vg::Color::from_argb(28, 255, 120, 48));
|
||||||
|
fp.set_anti_alias(true);
|
||||||
|
canvas.draw_path(&fill, &fp);
|
||||||
|
|
||||||
|
p.set_style(vg::PaintStyle::Stroke);
|
||||||
|
p.set_stroke_width(1.5);
|
||||||
|
p.set_stroke_cap(vg::paint::Cap::Round);
|
||||||
|
p.set_stroke_join(vg::paint::Join::Round);
|
||||||
|
p.set_color(vg::Color::from_argb(255, 255, 120, 48));
|
||||||
|
p.set_anti_alias(true);
|
||||||
|
canvas.draw_path(&stroke, &p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn db_to_y(db: f32, top: f32, height: f32) -> f32 {
|
||||||
|
const DB_MIN: f32 = -42.0;
|
||||||
|
const DB_MAX: f32 = 14.0;
|
||||||
|
let t = 1.0 - (db.clamp(DB_MIN, DB_MAX) - DB_MIN) / (DB_MAX - DB_MIN);
|
||||||
|
top + t * height
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
pub mod env_display;
|
pub mod env_display;
|
||||||
|
pub mod filter_response;
|
||||||
pub mod knob;
|
pub mod knob;
|
||||||
pub mod wavetable_display;
|
|
||||||
pub mod piano;
|
pub mod piano;
|
||||||
|
pub mod spectrum;
|
||||||
|
pub mod wavetable_display;
|
||||||
|
|||||||
45
crates/ui/src/widgets/spectrum.rs
Normal file
45
crates/ui/src/widgets/spectrum.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
use vizia::prelude::*;
|
||||||
|
use vizia::vg;
|
||||||
|
|
||||||
|
pub struct SpectrumWidget {
|
||||||
|
pub magnitudes: Vec<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SpectrumWidget {
|
||||||
|
pub fn new(cx: &mut Context, bins: usize) -> Handle<Self> {
|
||||||
|
Self {
|
||||||
|
magnitudes: vec![0.0; bins],
|
||||||
|
}
|
||||||
|
.build(cx, |_| {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for SpectrumWidget {
|
||||||
|
fn draw(&self, cx: &mut DrawContext, canvas: &Canvas) {
|
||||||
|
let b = cx.bounds();
|
||||||
|
let mut p = vg::Paint::default();
|
||||||
|
|
||||||
|
p.set_color(vg::Color::from_argb(255, 10, 10, 20));
|
||||||
|
canvas.draw_rect(vg::Rect::from_xywh(b.x, b.y, b.w, b.h), &p);
|
||||||
|
|
||||||
|
let n = self.magnitudes.len();
|
||||||
|
if n == 0 || b.w < 1.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let bw = (b.w / n as f32).max(1.0);
|
||||||
|
p.set_style(vg::PaintStyle::Fill);
|
||||||
|
p.set_anti_alias(false);
|
||||||
|
|
||||||
|
for (i, &m) in self.magnitudes.iter().enumerate() {
|
||||||
|
let mh = m.clamp(0.0, 1.0) * b.h;
|
||||||
|
if mh < 0.5 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let x = b.x + i as f32 * bw;
|
||||||
|
let y = b.y + b.h - mh;
|
||||||
|
let alpha = ((m * 0.65 + 0.2) * 255.0).clamp(0.0, 255.0) as u8;
|
||||||
|
p.set_color(vg::Color::from_argb(alpha, 255, 120, 48));
|
||||||
|
canvas.draw_rect(vg::Rect::from_xywh(x, y, (bw - 1.0).max(1.0), mh), &p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use params::ParamId;
|
||||||
use vizia::prelude::*;
|
use vizia::prelude::*;
|
||||||
use vizia::vg;
|
use vizia::vg;
|
||||||
|
|
||||||
@@ -12,19 +13,39 @@ impl WavetableDisplay {
|
|||||||
.width(Pixels(150.0))
|
.width(Pixels(150.0))
|
||||||
.height(Pixels(68.0))
|
.height(Pixels(68.0))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_live_param(cx: &mut Context, wave_pos_param: ParamId) -> Handle<Self> {
|
||||||
|
let init = cx
|
||||||
|
.data::<crate::app::AppData>()
|
||||||
|
.map(|d| d.params[wave_pos_param as usize])
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
Self {
|
||||||
|
samples: gen_wave(init),
|
||||||
|
}
|
||||||
|
.build(cx, |_| {})
|
||||||
|
.bind(
|
||||||
|
crate::app::AppData::params.idx(wave_pos_param as usize),
|
||||||
|
|h, l| {
|
||||||
|
let v = l.get(&h);
|
||||||
|
h.modify(|d: &mut WavetableDisplay| d.samples = gen_wave(v));
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.width(Pixels(150.0))
|
||||||
|
.height(Pixels(68.0))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl View for WavetableDisplay {
|
impl View for WavetableDisplay {
|
||||||
fn draw(&self, cx: &mut DrawContext, canvas: &Canvas) {
|
fn draw(&self, cx: &mut DrawContext, canvas: &Canvas) {
|
||||||
let b = cx.bounds();
|
let b = cx.bounds();
|
||||||
|
|
||||||
let mut p = vg::Paint::default();
|
let mut p = vg::Paint::default();
|
||||||
|
|
||||||
p.set_color(vg::Color::from_argb(255, 18, 18, 30));
|
p.set_color(vg::Color::from_argb(255, 18, 18, 30));
|
||||||
canvas.draw_rect(vg::Rect::from_xywh(b.x, b.y, b.w, b.h), &p);
|
canvas.draw_rect(vg::Rect::from_xywh(b.x, b.y, b.w, b.h), &p);
|
||||||
|
|
||||||
p.set_color(vg::Color::from_argb(40, 255, 255, 255));
|
|
||||||
p.set_style(vg::PaintStyle::Stroke);
|
p.set_style(vg::PaintStyle::Stroke);
|
||||||
p.set_stroke_width(1.0);
|
p.set_stroke_width(1.0);
|
||||||
|
p.set_color(vg::Color::from_argb(30, 255, 255, 255));
|
||||||
let mid_y = b.y + b.h * 0.5;
|
let mid_y = b.y + b.h * 0.5;
|
||||||
let mut zl = vg::Path::new();
|
let mut zl = vg::Path::new();
|
||||||
zl.move_to((b.x, mid_y));
|
zl.move_to((b.x, mid_y));
|
||||||
@@ -35,7 +56,6 @@ impl View for WavetableDisplay {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let n = self.samples.len() as f32;
|
let n = self.samples.len() as f32;
|
||||||
|
|
||||||
let mut wave = vg::Path::new();
|
let mut wave = vg::Path::new();
|
||||||
for (i, &s) in self.samples.iter().enumerate() {
|
for (i, &s) in self.samples.iter().enumerate() {
|
||||||
let x = b.x + (i as f32 / n) * b.w;
|
let x = b.x + (i as f32 / n) * b.w;
|
||||||
@@ -48,7 +68,46 @@ impl View for WavetableDisplay {
|
|||||||
}
|
}
|
||||||
p.set_color(vg::Color::from_argb(255, 255, 120, 48));
|
p.set_color(vg::Color::from_argb(255, 255, 120, 48));
|
||||||
p.set_stroke_width(1.5);
|
p.set_stroke_width(1.5);
|
||||||
|
p.set_stroke_cap(vg::paint::Cap::Round);
|
||||||
p.set_anti_alias(true);
|
p.set_anti_alias(true);
|
||||||
canvas.draw_path(&wave, &p);
|
canvas.draw_path(&wave, &p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn gen_wave(wave_pos: f32) -> Vec<f32> {
|
||||||
|
const N: usize = 128;
|
||||||
|
let pos = wave_pos.clamp(0.0, 1.0) * 3.0;
|
||||||
|
let lo = pos as usize;
|
||||||
|
let hi = (lo + 1).min(3);
|
||||||
|
let morph = pos - lo as f32;
|
||||||
|
(0..N)
|
||||||
|
.map(|i| {
|
||||||
|
let ph = i as f32 / N as f32;
|
||||||
|
let a = table_sample(ph, lo);
|
||||||
|
let b = table_sample(ph, hi);
|
||||||
|
a + (b - a) * morph
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn table_sample(ph: f32, t: usize) -> f32 {
|
||||||
|
match t {
|
||||||
|
0 => (ph * std::f32::consts::TAU).sin(),
|
||||||
|
1 => {
|
||||||
|
if ph < 0.5 {
|
||||||
|
4.0 * ph - 1.0
|
||||||
|
} else {
|
||||||
|
3.0 - 4.0 * ph
|
||||||
|
}
|
||||||
|
}
|
||||||
|
2 => 2.0 * ph - 1.0,
|
||||||
|
_ => {
|
||||||
|
if ph < 0.5 {
|
||||||
|
1.0
|
||||||
|
} else {
|
||||||
|
-1.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user