mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 11:46:41 +01:00
refactor(transport): make widgets focusable
This commit is contained in:
parent
2106a7c044
commit
b8ac83b019
9 changed files with 204 additions and 132 deletions
17
Cargo.lock
generated
17
Cargo.lock
generated
|
|
@ -1302,12 +1302,6 @@ dependencies = [
|
|||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "microxdg"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdcd3dc4ca0d1a9b1b80576639e93911a3e1db25a31ab6e6ba33027429adde5e"
|
||||
|
||||
[[package]]
|
||||
name = "midly"
|
||||
version = "0.5.3"
|
||||
|
|
@ -2505,17 +2499,6 @@ version = "0.12.15"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4873307b7c257eddcb50c9bedf158eb669578359fb28428bef438fec8e6ba7c2"
|
||||
|
||||
[[package]]
|
||||
name = "tek"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clojure-reader",
|
||||
"microxdg",
|
||||
"tek_core",
|
||||
"tek_mixer",
|
||||
"tek_sequencer",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tek_core"
|
||||
version = "0.1.0"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = [ "crates/*" ]
|
||||
members = [
|
||||
"crates/tek_core",
|
||||
"crates/tek_mixer",
|
||||
"crates/tek_sequencer",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use crate::*;
|
||||
|
||||
/// A component that may contain [Focusable] components.
|
||||
pub trait Focus <const N: usize>: Render + Handle {
|
||||
fn focus (&self) -> 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 {
|
||||
fn is_focused (&self) -> 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 {
|
||||
($struct:ident ($focus:ident) : $count:expr => [
|
||||
$($focusable:ident),*
|
||||
|
|
@ -47,7 +64,7 @@ pub trait Focusable: Render + Handle {
|
|||
$(&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,)*
|
||||
]
|
||||
|
|
@ -56,7 +73,11 @@ pub trait Focusable: Render + Handle {
|
|||
}
|
||||
}
|
||||
|
||||
/// Implement the [Focusable] trait for a component.
|
||||
#[macro_export] macro_rules! focusable {
|
||||
($struct:ident) => {
|
||||
focusable!($struct (focused));
|
||||
};
|
||||
($struct:ident ($focused:ident)) => {
|
||||
impl Focusable for $struct {
|
||||
fn is_focused (&self) -> bool {
|
||||
|
|
|
|||
|
|
@ -15,8 +15,12 @@ pub struct Arranger {
|
|||
pub mode: ArrangerViewMode,
|
||||
/// Slot for modal dialog displayed on top of app.
|
||||
pub modal: Option<Box<dyn ExitableComponent>>,
|
||||
/// Whether the arranger is currently focused
|
||||
pub focused: bool
|
||||
}
|
||||
|
||||
focusable!(Arranger (focused));
|
||||
|
||||
impl Arranger {
|
||||
pub fn new (name: &str) -> Self {
|
||||
Self {
|
||||
|
|
@ -25,7 +29,8 @@ impl Arranger {
|
|||
selected: ArrangerFocus::Clip(0, 0),
|
||||
scenes: vec![],
|
||||
tracks: vec![],
|
||||
modal: None
|
||||
modal: None,
|
||||
focused: false
|
||||
}
|
||||
}
|
||||
pub fn activate (&mut self) {
|
||||
|
|
|
|||
|
|
@ -95,11 +95,25 @@ impl Render for ArrangerStandalone {
|
|||
impl Handle for ArrangerStandalone {
|
||||
fn handle (&mut self, e: &AppEvent) -> Usually<bool> {
|
||||
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();
|
||||
Ok(true)
|
||||
},
|
||||
AppEvent::Input(Event::Key(KeyEvent { code: KeyCode::BackTab, .. })) => {
|
||||
AppEvent::Input(Event::Key(KeyEvent {
|
||||
code: KeyCode::BackTab, ..
|
||||
})) => {
|
||||
self.focus_prev();
|
||||
Ok(true)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,9 +4,6 @@ use crate::*;
|
|||
pub struct TransportToolbar {
|
||||
/// Enable metronome?
|
||||
pub metronome: bool,
|
||||
pub focused: bool,
|
||||
pub entered: bool,
|
||||
pub selected: TransportFocus,
|
||||
/// Current sample rate, tempo, and PPQ.
|
||||
pub timebase: Arc<Timebase>,
|
||||
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
||||
|
|
@ -14,28 +11,66 @@ pub struct TransportToolbar {
|
|||
/// JACK transport handle.
|
||||
pub transport: Option<Transport>,
|
||||
/// 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
|
||||
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| {
|
||||
self.update(&scope);
|
||||
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 {
|
||||
|
||||
pub fn standalone () -> Usually<Arc<RwLock<Self>>> {
|
||||
let mut transport = Self::new(None);
|
||||
transport.focused = true;
|
||||
transport.entered = true;
|
||||
let jack = JackClient::Inactive(
|
||||
Client::new("tek_transport", ClientOptions::NO_START_SERVER)?.0
|
||||
);
|
||||
|
|
@ -55,24 +90,44 @@ impl TransportToolbar {
|
|||
pub fn new (transport: Option<Transport>) -> Self {
|
||||
let timebase = Arc::new(Timebase::default());
|
||||
Self {
|
||||
selected: TransportFocus::BPM,
|
||||
metronome: false,
|
||||
focused: false,
|
||||
entered: false,
|
||||
playhead: 0,
|
||||
playing: Some(TransportState::Stopped),
|
||||
focus: 0,
|
||||
started: None,
|
||||
quant: 24,
|
||||
sync: timebase.ppq() as usize * 4,
|
||||
jack: None,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_play (&mut self) -> Usually<()> {
|
||||
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 => {
|
||||
transport.start()?;
|
||||
Some(TransportState::Starting)
|
||||
|
|
@ -95,9 +150,9 @@ impl TransportToolbar {
|
|||
} = scope.cycle_times().unwrap();
|
||||
let chunk_size = scope.n_frames() as usize;
|
||||
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;
|
||||
if self.playing != Some(transport.state) {
|
||||
if self.playing.value != Some(transport.state) {
|
||||
match transport.state {
|
||||
TransportState::Rolling => {
|
||||
self.started = Some((
|
||||
|
|
@ -112,7 +167,7 @@ impl TransportToolbar {
|
|||
_ => {}
|
||||
}
|
||||
}
|
||||
self.playing = Some(transport.state);
|
||||
self.playing.value = Some(transport.state);
|
||||
(
|
||||
reset,
|
||||
current_frames as usize,
|
||||
|
|
@ -132,11 +187,11 @@ impl TransportToolbar {
|
|||
}
|
||||
|
||||
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 {
|
||||
self.timebase.frame_to_usec(self.playhead as f64) as usize
|
||||
self.timebase.frame_to_usec(self.clock.frame as f64) as usize
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,4 +20,3 @@ impl TransportFocus {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,51 +1,55 @@
|
|||
use crate::*;
|
||||
|
||||
handle!{
|
||||
TransportToolbar |self, e| {
|
||||
handle_keymap(self, e, KEYMAP_TRANSPORT)
|
||||
handle!(TransportToolbar |self, e| {
|
||||
match e {
|
||||
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.
|
||||
pub const KEYMAP_TRANSPORT: &'static [KeyBinding<TransportToolbar>] = keymap!(TransportToolbar {
|
||||
[Left, NONE, "transport_prev", "select previous control", |transport: &mut TransportToolbar| Ok({
|
||||
transport.selected.prev();
|
||||
true
|
||||
})],
|
||||
[Right, NONE, "transport_next", "select next control", |transport: &mut TransportToolbar| Ok({
|
||||
transport.selected.next();
|
||||
true
|
||||
})],
|
||||
[Char('.'), NONE, "transport_increment", "increment value at cursor", |transport: &mut TransportToolbar| {
|
||||
match transport.selected {
|
||||
TransportFocus::BPM => {
|
||||
transport.timebase.bpm.fetch_add(1.0, Ordering::Relaxed);
|
||||
},
|
||||
TransportFocus::Quant => {
|
||||
transport.quant = next_note_length(transport.quant)
|
||||
},
|
||||
TransportFocus::Sync => {
|
||||
transport.sync = next_note_length(transport.sync)
|
||||
},
|
||||
};
|
||||
Ok(true)
|
||||
}],
|
||||
[Char(','), NONE, "transport_decrement", "decrement value at cursor", |transport: &mut TransportToolbar| {
|
||||
match transport.selected {
|
||||
TransportFocus::BPM => {
|
||||
transport.timebase.bpm.fetch_sub(1.0, Ordering::Relaxed);
|
||||
},
|
||||
TransportFocus::Quant => {
|
||||
transport.quant = prev_note_length(transport.quant);
|
||||
},
|
||||
TransportFocus::Sync => {
|
||||
transport.sync = prev_note_length(transport.sync);
|
||||
},
|
||||
};
|
||||
Ok(true)
|
||||
}],
|
||||
[Char(' '), NONE, "transport_play_toggle", "play or pause", |transport: &mut TransportToolbar| {
|
||||
transport.toggle_play()?;
|
||||
Ok(true)
|
||||
}],
|
||||
});
|
||||
|
||||
handle!(TransportPlayPauseButton |self, _e| Ok(false));
|
||||
|
||||
handle!(TransportBPM |self, _e| {
|
||||
//TransportFocus::BPM => {
|
||||
//transport.timebase.bpm.fetch_add(1.0, Ordering::Relaxed);
|
||||
//},
|
||||
//TransportFocus::BPM => {
|
||||
//transport.timebase.bpm.fetch_sub(1.0, Ordering::Relaxed);
|
||||
//},
|
||||
Ok(false)
|
||||
});
|
||||
|
||||
handle!(TransportQuantize |self, _e| {
|
||||
//TransportFocus::Quant => {
|
||||
//transport.quant.value = next_note_length(transport.quant)
|
||||
//},
|
||||
//TransportFocus::Quant => {
|
||||
//transport.quant.value = prev_note_length(transport.quant);
|
||||
//},
|
||||
Ok(false)
|
||||
});
|
||||
|
||||
handle!(TransportSync |self, _e| {
|
||||
//TransportFocus::Sync => {
|
||||
//transport.sync.value = next_note_length(transport.sync)
|
||||
//},
|
||||
//TransportFocus::Sync => {
|
||||
//transport.sync.value = prev_note_length(transport.sync);
|
||||
//},
|
||||
Ok(false)
|
||||
});
|
||||
|
||||
handle!(TransportClock |self, _e| {
|
||||
Ok(false)
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,92 +5,79 @@ const CORNERS: Corners = Corners(NOT_DIM_GREEN);
|
|||
render!(TransportToolbar |self, buf, area| {
|
||||
let mut area = area;
|
||||
area.height = 2;
|
||||
let Self { quant, sync, focused, entered, .. } = self;
|
||||
fill_bg(buf, area, Nord::bg_lo(*focused, *entered));
|
||||
let active = self.focused && self.entered;
|
||||
fill_bg(buf, area, Nord::BG0);
|
||||
Split::right()
|
||||
.add(TransportPlayPauseButton(self.playing))
|
||||
.add(TransportBPM(self.bpm(), active && self.selected == TransportFocus::BPM))
|
||||
.add(TransportQuantize(*quant, active && self.selected == TransportFocus::Quant))
|
||||
.add(TransportSync(*sync, active && self.selected == TransportFocus::Sync))
|
||||
.add(TransportClock(self.pulse(), self.ppq(), self.usecs()))
|
||||
.add_ref(&self.playing)
|
||||
.add_ref(&self.bpm)
|
||||
.add_ref(&self.quant)
|
||||
.add_ref(&self.sync)
|
||||
.add_ref(&self.clock)
|
||||
.render(buf, area)
|
||||
});
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct TransportPlayPauseButton(Option<TransportState>);
|
||||
|
||||
render!(TransportPlayPauseButton |self, buf, 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::Starting) => GRAY_NOT_DIM_BOLD,
|
||||
Some(TransportState::Rolling) => WHITE_NOT_DIM_BOLD,
|
||||
_ => unreachable!(),
|
||||
});
|
||||
let label = match self.0 {
|
||||
let label = match value {
|
||||
Some(TransportState::Rolling) => "▶ PLAYING",
|
||||
Some(TransportState::Starting) => "READY ...",
|
||||
Some(TransportState::Stopped) => "⏹ STOPPED",
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let mut result = label.blit(buf, x + 1, y, style)?;
|
||||
result.width = result.width + 1;
|
||||
Ok(result)
|
||||
let mut area = label.blit(buf, x + 1, y, style)?;
|
||||
area.width = area.width + 1;
|
||||
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| {
|
||||
let Rect { x, y, .. } = area;
|
||||
let Self(bpm, highlight) = self;
|
||||
let Self { value, focused } = self;
|
||||
"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 };
|
||||
if *highlight {
|
||||
if *focused {
|
||||
CORNERS.draw(buf, Rect { x: area.x - 1, ..area })?;
|
||||
}
|
||||
Ok(area)
|
||||
});
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct TransportQuantize(usize, bool);
|
||||
|
||||
render!(TransportQuantize |self, buf, area| {
|
||||
let Rect { x, y, .. } = area;
|
||||
let Self(quant, highlight) = self;
|
||||
let Self { value, focused } = self;
|
||||
"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 };
|
||||
if *highlight {
|
||||
if *focused {
|
||||
CORNERS.draw(buf, Rect { x: area.x - 1, ..area })?;
|
||||
}
|
||||
Ok(area)
|
||||
});
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct TransportSync(usize, bool);
|
||||
|
||||
render!(TransportSync |self, buf, area| {
|
||||
let Rect { x, y, .. } = area;
|
||||
let Self(sync, highlight) = self;
|
||||
let Self { value, focused } = self;
|
||||
"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 };
|
||||
if *highlight {
|
||||
if *focused {
|
||||
CORNERS.draw(buf, Rect { x: area.x - 1, ..area })?;
|
||||
}
|
||||
Ok(area)
|
||||
});
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct TransportClock(usize, usize, usize);
|
||||
|
||||
render!(TransportClock |self, buf, area| {
|
||||
let Rect { x, y, width, .. } = area;
|
||||
let Self(pulse, ppq, usecs) = self;
|
||||
let (beats, pulses) = (pulse / ppq, pulse % ppq);
|
||||
let Self { frame, pulse, ppq, usecs, focused } = self;
|
||||
let (beats, pulses) = if *ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) };
|
||||
let (bars, beats) = ((beats / 4) + 1, (beats % 4) + 1);
|
||||
let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000);
|
||||
let (minutes, seconds) = (seconds / 60, seconds % 60);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue