refactor(transport): make widgets focusable

This commit is contained in:
🪞👃🪞 2024-09-01 20:29:15 +03:00
parent 2106a7c044
commit b8ac83b019
9 changed files with 204 additions and 132 deletions

17
Cargo.lock generated
View file

@ -1302,12 +1302,6 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "microxdg"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdcd3dc4ca0d1a9b1b80576639e93911a3e1db25a31ab6e6ba33027429adde5e"
[[package]] [[package]]
name = "midly" name = "midly"
version = "0.5.3" version = "0.5.3"
@ -2505,17 +2499,6 @@ version = "0.12.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4873307b7c257eddcb50c9bedf158eb669578359fb28428bef438fec8e6ba7c2" checksum = "4873307b7c257eddcb50c9bedf158eb669578359fb28428bef438fec8e6ba7c2"
[[package]]
name = "tek"
version = "0.1.0"
dependencies = [
"clojure-reader",
"microxdg",
"tek_core",
"tek_mixer",
"tek_sequencer",
]
[[package]] [[package]]
name = "tek_core" name = "tek_core"
version = "0.1.0" version = "0.1.0"

View file

@ -1,3 +1,7 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = [ "crates/*" ] members = [
"crates/tek_core",
"crates/tek_mixer",
"crates/tek_sequencer",
]

View file

@ -1,5 +1,6 @@
use crate::*; use crate::*;
/// A component that may contain [Focusable] components.
pub trait Focus <const N: usize>: Render + Handle { pub trait Focus <const N: usize>: Render + Handle {
fn focus (&self) -> usize; fn focus (&self) -> usize;
fn focus_mut (&mut self) -> &mut usize; fn focus_mut (&mut self) -> &mut usize;
@ -26,11 +27,27 @@ pub trait Focus <const N: usize>: Render + Handle {
} }
} }
/// A component that may be focused.
pub trait Focusable: Render + Handle { pub trait Focusable: Render + Handle {
fn is_focused (&self) -> bool; fn is_focused (&self) -> bool;
fn set_focused (&mut self, focused: bool); fn set_focused (&mut self, focused: bool);
} }
impl<T: Focusable> Focusable for Option<T> {
fn is_focused (&self) -> bool {
match self {
Some(focusable) => focusable.is_focused(),
None => false
}
}
fn set_focused (&mut self, focused: bool) {
if let Some(focusable) = self {
focusable.set_focused(focused)
}
}
}
/// Implement the [Focus] trait for a component.
#[macro_export] macro_rules! focus { #[macro_export] macro_rules! focus {
($struct:ident ($focus:ident) : $count:expr => [ ($struct:ident ($focus:ident) : $count:expr => [
$($focusable:ident),* $($focusable:ident),*
@ -47,7 +64,7 @@ pub trait Focusable: Render + Handle {
$(&self.$focusable as &dyn Focusable,)* $(&self.$focusable as &dyn Focusable,)*
] ]
} }
fn focusable_mut (&mut self) -> [&mut dyn Focusable;2] { fn focusable_mut (&mut self) -> [&mut dyn Focusable;$count] {
[ [
$(&mut self.$focusable as &mut dyn Focusable,)* $(&mut self.$focusable as &mut dyn Focusable,)*
] ]
@ -56,7 +73,11 @@ pub trait Focusable: Render + Handle {
} }
} }
/// Implement the [Focusable] trait for a component.
#[macro_export] macro_rules! focusable { #[macro_export] macro_rules! focusable {
($struct:ident) => {
focusable!($struct (focused));
};
($struct:ident ($focused:ident)) => { ($struct:ident ($focused:ident)) => {
impl Focusable for $struct { impl Focusable for $struct {
fn is_focused (&self) -> bool { fn is_focused (&self) -> bool {

View file

@ -15,8 +15,12 @@ pub struct Arranger {
pub mode: ArrangerViewMode, pub mode: ArrangerViewMode,
/// Slot for modal dialog displayed on top of app. /// Slot for modal dialog displayed on top of app.
pub modal: Option<Box<dyn ExitableComponent>>, pub modal: Option<Box<dyn ExitableComponent>>,
/// Whether the arranger is currently focused
pub focused: bool
} }
focusable!(Arranger (focused));
impl Arranger { impl Arranger {
pub fn new (name: &str) -> Self { pub fn new (name: &str) -> Self {
Self { Self {
@ -25,7 +29,8 @@ impl Arranger {
selected: ArrangerFocus::Clip(0, 0), selected: ArrangerFocus::Clip(0, 0),
scenes: vec![], scenes: vec![],
tracks: vec![], tracks: vec![],
modal: None modal: None,
focused: false
} }
} }
pub fn activate (&mut self) { pub fn activate (&mut self) {

View file

@ -95,11 +95,25 @@ impl Render for ArrangerStandalone {
impl Handle for ArrangerStandalone { impl Handle for ArrangerStandalone {
fn handle (&mut self, e: &AppEvent) -> Usually<bool> { fn handle (&mut self, e: &AppEvent) -> Usually<bool> {
match e { match e {
AppEvent::Input(Event::Key(KeyEvent { code: KeyCode::Tab, .. })) => { AppEvent::Input(Event::Key(KeyEvent {
code: KeyCode::Char(' '), ..
})) => {
if let Some(ref mut transport) = self.transport {
transport.toggle_play()?;
Ok(true)
} else {
Ok(false)
}
},
AppEvent::Input(Event::Key(KeyEvent {
code: KeyCode::Tab, ..
})) => {
self.focus_next(); self.focus_next();
Ok(true) Ok(true)
}, },
AppEvent::Input(Event::Key(KeyEvent { code: KeyCode::BackTab, .. })) => { AppEvent::Input(Event::Key(KeyEvent {
code: KeyCode::BackTab, ..
})) => {
self.focus_prev(); self.focus_prev();
Ok(true) Ok(true)
}, },

View file

@ -4,9 +4,6 @@ use crate::*;
pub struct TransportToolbar { pub struct TransportToolbar {
/// Enable metronome? /// Enable metronome?
pub metronome: bool, pub metronome: bool,
pub focused: bool,
pub entered: bool,
pub selected: TransportFocus,
/// Current sample rate, tempo, and PPQ. /// Current sample rate, tempo, and PPQ.
pub timebase: Arc<Timebase>, pub timebase: Arc<Timebase>,
/// JACK client handle (needs to not be dropped for standalone mode to work). /// JACK client handle (needs to not be dropped for standalone mode to work).
@ -14,28 +11,66 @@ pub struct TransportToolbar {
/// JACK transport handle. /// JACK transport handle.
pub transport: Option<Transport>, pub transport: Option<Transport>,
/// Quantization factor /// Quantization factor
pub quant: usize,
/// Global sync quant
pub sync: usize,
/// Current transport state
pub playing: Option<TransportState>,
/// Current position according to transport
pub playhead: usize,
/// Global frame and usec at which playback started /// Global frame and usec at which playback started
pub started: Option<(usize, usize)>, pub started: Option<(usize, usize)>,
pub focused: bool,
pub focus: usize,
pub playing: TransportPlayPauseButton,
pub bpm: TransportBPM,
pub quant: TransportQuantize,
pub sync: TransportSync,
pub clock: TransportClock,
} }
focusable!(TransportToolbar);
focus!(TransportToolbar (focus) : 5 => [
playing, bpm, quant, sync, clock
]);
process!(TransportToolbar |self, _client, scope| { process!(TransportToolbar |self, _client, scope| {
self.update(&scope); self.update(&scope);
Control::Continue Control::Continue
}); });
pub struct TransportPlayPauseButton {
pub value: Option<TransportState>,
pub focused: bool
}
focusable!(TransportPlayPauseButton);
pub struct TransportBPM {
pub value: f64,
pub focused: bool
}
focusable!(TransportBPM);
pub struct TransportQuantize {
pub value: usize,
pub focused: bool
}
focusable!(TransportQuantize);
pub struct TransportSync {
pub value: usize,
pub focused: bool
}
focusable!(TransportSync);
pub struct TransportClock {
pub frame: usize,
pub pulse: usize,
pub ppq: usize,
pub usecs: usize,
pub focused: bool,
}
focusable!(TransportClock);
impl TransportToolbar { impl TransportToolbar {
pub fn standalone () -> Usually<Arc<RwLock<Self>>> { pub fn standalone () -> Usually<Arc<RwLock<Self>>> {
let mut transport = Self::new(None); let mut transport = Self::new(None);
transport.focused = true; transport.focused = true;
transport.entered = true;
let jack = JackClient::Inactive( let jack = JackClient::Inactive(
Client::new("tek_transport", ClientOptions::NO_START_SERVER)?.0 Client::new("tek_transport", ClientOptions::NO_START_SERVER)?.0
); );
@ -55,24 +90,44 @@ impl TransportToolbar {
pub fn new (transport: Option<Transport>) -> Self { pub fn new (transport: Option<Transport>) -> Self {
let timebase = Arc::new(Timebase::default()); let timebase = Arc::new(Timebase::default());
Self { Self {
selected: TransportFocus::BPM,
metronome: false, metronome: false,
focused: false, focused: false,
entered: false, focus: 0,
playhead: 0,
playing: Some(TransportState::Stopped),
started: None, started: None,
quant: 24,
sync: timebase.ppq() as usize * 4,
jack: None, jack: None,
transport, transport,
playing: TransportPlayPauseButton {
value: Some(TransportState::Stopped),
focused: true
},
bpm: TransportBPM {
value: timebase.bpm(),
focused: false
},
quant: TransportQuantize {
value: 24,
focused: false
},
sync: TransportSync {
value: timebase.ppq() as usize * 4,
focused: false
},
clock: TransportClock {
frame: 0,
pulse: 0,
ppq: 0,
usecs: 0,
focused: false
},
timebase, timebase,
} }
} }
pub fn toggle_play (&mut self) -> Usually<()> { pub fn toggle_play (&mut self) -> Usually<()> {
let transport = self.transport.as_ref().unwrap(); let transport = self.transport.as_ref().unwrap();
self.playing = match self.playing.expect("1st frame has not been processed yet") { self.playing.value = match self.playing.value.expect("1st frame has not been processed yet") {
TransportState::Stopped => { TransportState::Stopped => {
transport.start()?; transport.start()?;
Some(TransportState::Starting) Some(TransportState::Starting)
@ -95,9 +150,9 @@ impl TransportToolbar {
} = scope.cycle_times().unwrap(); } = scope.cycle_times().unwrap();
let chunk_size = scope.n_frames() as usize; let chunk_size = scope.n_frames() as usize;
let transport = self.transport.as_ref().unwrap().query().unwrap(); let transport = self.transport.as_ref().unwrap().query().unwrap();
self.playhead = transport.pos.frame() as usize; self.clock.frame = transport.pos.frame() as usize;
let mut reset = false; let mut reset = false;
if self.playing != Some(transport.state) { if self.playing.value != Some(transport.state) {
match transport.state { match transport.state {
TransportState::Rolling => { TransportState::Rolling => {
self.started = Some(( self.started = Some((
@ -112,7 +167,7 @@ impl TransportToolbar {
_ => {} _ => {}
} }
} }
self.playing = Some(transport.state); self.playing.value = Some(transport.state);
( (
reset, reset,
current_frames as usize, current_frames as usize,
@ -132,11 +187,11 @@ impl TransportToolbar {
} }
pub fn pulse (&self) -> usize { pub fn pulse (&self) -> usize {
self.timebase.frame_to_pulse(self.playhead as f64) as usize self.timebase.frame_to_pulse(self.clock.frame as f64) as usize
} }
pub fn usecs (&self) -> usize { pub fn usecs (&self) -> usize {
self.timebase.frame_to_usec(self.playhead as f64) as usize self.timebase.frame_to_usec(self.clock.frame as f64) as usize
} }
} }

View file

@ -20,4 +20,3 @@ impl TransportFocus {
} }
} }
} }

View file

@ -1,51 +1,55 @@
use crate::*; use crate::*;
handle!{ handle!(TransportToolbar |self, e| {
TransportToolbar |self, e| { match e {
handle_keymap(self, e, KEYMAP_TRANSPORT) AppEvent::Input(Event::Key(KeyEvent { code: KeyCode::Right, .. })) => {
self.focus_next();
Ok(true)
},
AppEvent::Input(Event::Key(KeyEvent { code: KeyCode::Left, .. })) => {
self.focus_prev();
Ok(true)
},
AppEvent::Input(Event::Key(KeyEvent { code: KeyCode::Char(' '), .. })) => {
self.toggle_play();
Ok(true)
},
_ => self.focused_mut().handle(e)
} }
} });
/// Key bindings for transport toolbar. handle!(TransportPlayPauseButton |self, _e| Ok(false));
pub const KEYMAP_TRANSPORT: &'static [KeyBinding<TransportToolbar>] = keymap!(TransportToolbar {
[Left, NONE, "transport_prev", "select previous control", |transport: &mut TransportToolbar| Ok({ handle!(TransportBPM |self, _e| {
transport.selected.prev(); //TransportFocus::BPM => {
true //transport.timebase.bpm.fetch_add(1.0, Ordering::Relaxed);
})], //},
[Right, NONE, "transport_next", "select next control", |transport: &mut TransportToolbar| Ok({ //TransportFocus::BPM => {
transport.selected.next(); //transport.timebase.bpm.fetch_sub(1.0, Ordering::Relaxed);
true //},
})], Ok(false)
[Char('.'), NONE, "transport_increment", "increment value at cursor", |transport: &mut TransportToolbar| { });
match transport.selected {
TransportFocus::BPM => { handle!(TransportQuantize |self, _e| {
transport.timebase.bpm.fetch_add(1.0, Ordering::Relaxed); //TransportFocus::Quant => {
}, //transport.quant.value = next_note_length(transport.quant)
TransportFocus::Quant => { //},
transport.quant = next_note_length(transport.quant) //TransportFocus::Quant => {
}, //transport.quant.value = prev_note_length(transport.quant);
TransportFocus::Sync => { //},
transport.sync = next_note_length(transport.sync) Ok(false)
}, });
};
Ok(true) handle!(TransportSync |self, _e| {
}], //TransportFocus::Sync => {
[Char(','), NONE, "transport_decrement", "decrement value at cursor", |transport: &mut TransportToolbar| { //transport.sync.value = next_note_length(transport.sync)
match transport.selected { //},
TransportFocus::BPM => { //TransportFocus::Sync => {
transport.timebase.bpm.fetch_sub(1.0, Ordering::Relaxed); //transport.sync.value = prev_note_length(transport.sync);
}, //},
TransportFocus::Quant => { Ok(false)
transport.quant = prev_note_length(transport.quant); });
},
TransportFocus::Sync => { handle!(TransportClock |self, _e| {
transport.sync = prev_note_length(transport.sync); Ok(false)
},
};
Ok(true)
}],
[Char(' '), NONE, "transport_play_toggle", "play or pause", |transport: &mut TransportToolbar| {
transport.toggle_play()?;
Ok(true)
}],
}); });

View file

@ -5,92 +5,79 @@ const CORNERS: Corners = Corners(NOT_DIM_GREEN);
render!(TransportToolbar |self, buf, area| { render!(TransportToolbar |self, buf, area| {
let mut area = area; let mut area = area;
area.height = 2; area.height = 2;
let Self { quant, sync, focused, entered, .. } = self; fill_bg(buf, area, Nord::BG0);
fill_bg(buf, area, Nord::bg_lo(*focused, *entered));
let active = self.focused && self.entered;
Split::right() Split::right()
.add(TransportPlayPauseButton(self.playing)) .add_ref(&self.playing)
.add(TransportBPM(self.bpm(), active && self.selected == TransportFocus::BPM)) .add_ref(&self.bpm)
.add(TransportQuantize(*quant, active && self.selected == TransportFocus::Quant)) .add_ref(&self.quant)
.add(TransportSync(*sync, active && self.selected == TransportFocus::Sync)) .add_ref(&self.sync)
.add(TransportClock(self.pulse(), self.ppq(), self.usecs())) .add_ref(&self.clock)
.render(buf, area) .render(buf, area)
}); });
#[derive(Copy, Clone)]
struct TransportPlayPauseButton(Option<TransportState>);
render!(TransportPlayPauseButton |self, buf, area| { render!(TransportPlayPauseButton |self, buf, area| {
let Rect { x, y, .. } = area; let Rect { x, y, .. } = area;
let style = Some(match self.0 { let Self { value, focused } = self;
let style = Some(match value {
Some(TransportState::Stopped) => GRAY_DIM.bold(), Some(TransportState::Stopped) => GRAY_DIM.bold(),
Some(TransportState::Starting) => GRAY_NOT_DIM_BOLD, Some(TransportState::Starting) => GRAY_NOT_DIM_BOLD,
Some(TransportState::Rolling) => WHITE_NOT_DIM_BOLD, Some(TransportState::Rolling) => WHITE_NOT_DIM_BOLD,
_ => unreachable!(), _ => unreachable!(),
}); });
let label = match self.0 { let label = match value {
Some(TransportState::Rolling) => "▶ PLAYING", Some(TransportState::Rolling) => "▶ PLAYING",
Some(TransportState::Starting) => "READY ...", Some(TransportState::Starting) => "READY ...",
Some(TransportState::Stopped) => "⏹ STOPPED", Some(TransportState::Stopped) => "⏹ STOPPED",
_ => unreachable!(), _ => unreachable!(),
}; };
let mut result = label.blit(buf, x + 1, y, style)?; let mut area = label.blit(buf, x + 1, y, style)?;
result.width = result.width + 1; area.width = area.width + 1;
Ok(result) if *focused {
CORNERS.draw(buf, Rect { x: area.x - 1, ..area })?;
}
Ok(area)
}); });
#[derive(Copy, Clone)]
struct TransportBPM(usize, bool);
render!(TransportBPM |self, buf, area| { render!(TransportBPM |self, buf, area| {
let Rect { x, y, .. } = area; let Rect { x, y, .. } = area;
let Self(bpm, highlight) = self; let Self { value, focused } = self;
"BPM".blit(buf, x, y, Some(NOT_DIM))?; "BPM".blit(buf, x, y, Some(NOT_DIM))?;
let width = format!("{}.{:03}", bpm, bpm * 1000 % 1000).blit(buf, x, y + 1, Some(NOT_DIM_BOLD))?.width; let width = format!("{}.{:03}", value, (value * 1000.0) % 1000.0).blit(buf, x, y + 1, Some(NOT_DIM_BOLD))?.width;
let area = Rect { x, y, width: (width + 2).max(10), height: 2 }; let area = Rect { x, y, width: (width + 2).max(10), height: 2 };
if *highlight { if *focused {
CORNERS.draw(buf, Rect { x: area.x - 1, ..area })?; CORNERS.draw(buf, Rect { x: area.x - 1, ..area })?;
} }
Ok(area) Ok(area)
}); });
#[derive(Copy, Clone)]
struct TransportQuantize(usize, bool);
render!(TransportQuantize |self, buf, area| { render!(TransportQuantize |self, buf, area| {
let Rect { x, y, .. } = area; let Rect { x, y, .. } = area;
let Self(quant, highlight) = self; let Self { value, focused } = self;
"QUANT".blit(buf, x, y, Some(NOT_DIM))?; "QUANT".blit(buf, x, y, Some(NOT_DIM))?;
let width = ppq_to_name(*quant as usize).blit(buf, x, y + 1, Some(NOT_DIM_BOLD))?.width; let width = ppq_to_name(*value as usize).blit(buf, x, y + 1, Some(NOT_DIM_BOLD))?.width;
let area = Rect { x, y, width: (width + 2).max(10), height: 2 }; let area = Rect { x, y, width: (width + 2).max(10), height: 2 };
if *highlight { if *focused {
CORNERS.draw(buf, Rect { x: area.x - 1, ..area })?; CORNERS.draw(buf, Rect { x: area.x - 1, ..area })?;
} }
Ok(area) Ok(area)
}); });
#[derive(Copy, Clone)]
struct TransportSync(usize, bool);
render!(TransportSync |self, buf, area| { render!(TransportSync |self, buf, area| {
let Rect { x, y, .. } = area; let Rect { x, y, .. } = area;
let Self(sync, highlight) = self; let Self { value, focused } = self;
"SYNC".blit(buf, x, y, Some(NOT_DIM))?; "SYNC".blit(buf, x, y, Some(NOT_DIM))?;
let width = ppq_to_name(*sync as usize).blit(buf, x, y + 1, Some(NOT_DIM_BOLD))?.width; let width = ppq_to_name(*value as usize).blit(buf, x, y + 1, Some(NOT_DIM_BOLD))?.width;
let area = Rect { x, y, width: (width + 2).max(10), height: 2 }; let area = Rect { x, y, width: (width + 2).max(10), height: 2 };
if *highlight { if *focused {
CORNERS.draw(buf, Rect { x: area.x - 1, ..area })?; CORNERS.draw(buf, Rect { x: area.x - 1, ..area })?;
} }
Ok(area) Ok(area)
}); });
#[derive(Copy, Clone)]
struct TransportClock(usize, usize, usize);
render!(TransportClock |self, buf, area| { render!(TransportClock |self, buf, area| {
let Rect { x, y, width, .. } = area; let Rect { x, y, width, .. } = area;
let Self(pulse, ppq, usecs) = self; let Self { frame, pulse, ppq, usecs, focused } = self;
let (beats, pulses) = (pulse / ppq, pulse % ppq); let (beats, pulses) = if *ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) };
let (bars, beats) = ((beats / 4) + 1, (beats % 4) + 1); let (bars, beats) = ((beats / 4) + 1, (beats % 4) + 1);
let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000); let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000);
let (minutes, seconds) = (seconds / 60, seconds % 60); let (minutes, seconds) = (seconds / 60, seconds % 60);