mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-07 04:06:45 +01:00
wip: p.44, e=135, removing E generic
This commit is contained in:
parent
a7998860b1
commit
260736f31d
20 changed files with 848 additions and 838 deletions
|
|
@ -1,10 +1,5 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait HasJack {
|
||||
fn jack (&self) -> &impl JackApi;
|
||||
}
|
||||
|
||||
pub trait JackApi {
|
||||
fn jack (&self) -> &Arc<RwLock<JackClient>>;
|
||||
fn transport (&self) -> &RwLock<Option<TransportState>>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait HasPlayer: HasJack {
|
||||
pub trait HasPlayer: JackApi {
|
||||
fn player (&self) -> &impl PlayerApi;
|
||||
fn player_mut (&mut self) -> &mut impl PlayerApi;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ submod! {
|
|||
tui_arranger
|
||||
tui_arranger_cmd
|
||||
tui_arranger_focus
|
||||
tui_arranger_jack
|
||||
tui_arranger_scene
|
||||
tui_arranger_select
|
||||
tui_arranger_status
|
||||
|
|
@ -29,6 +30,7 @@ submod! {
|
|||
//tui_plugin_vst2
|
||||
//tui_plugin_vst3
|
||||
tui_pool
|
||||
tui_pool_view
|
||||
//tui_sampler // TODO
|
||||
//tui_sampler_cmd
|
||||
tui_sequencer
|
||||
|
|
@ -37,61 +39,61 @@ submod! {
|
|||
tui_theme
|
||||
tui_transport
|
||||
tui_transport_cmd
|
||||
tui_transport_focus
|
||||
tui_transport_jack
|
||||
tui_transport_view
|
||||
}
|
||||
|
||||
pub struct AppView<E, A, C, S>
|
||||
where
|
||||
E: Engine,
|
||||
A: Widget<Engine = E> + Audio,
|
||||
C: Command<Self>,
|
||||
S: StatusBar,
|
||||
{
|
||||
pub app: A,
|
||||
pub cursor: (usize, usize),
|
||||
pub entered: bool,
|
||||
pub menu_bar: Option<MenuBar<E, Self, C>>,
|
||||
pub status_bar: Option<S>,
|
||||
pub history: Vec<C>,
|
||||
pub size: Measure<E>,
|
||||
}
|
||||
//pub struct AppView<E, A, C, S>
|
||||
//where
|
||||
//E: Engine,
|
||||
//A: Widget<Engine = E> + Audio,
|
||||
//C: Command<Self>,
|
||||
//S: StatusBar,
|
||||
//{
|
||||
//pub app: A,
|
||||
//pub cursor: (usize, usize),
|
||||
//pub entered: bool,
|
||||
//pub menu_bar: Option<MenuBar<E, Self, C>>,
|
||||
//pub status_bar: Option<S>,
|
||||
//pub history: Vec<C>,
|
||||
//pub size: Measure<E>,
|
||||
//}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AppViewCommand<T: Debug + Clone> {
|
||||
Focus(FocusCommand),
|
||||
Undo,
|
||||
Redo,
|
||||
App(T)
|
||||
}
|
||||
//#[derive(Debug, Clone)]
|
||||
//pub enum AppViewCommand<T: Debug + Clone> {
|
||||
//App(T)
|
||||
//}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub enum AppViewFocus<F: Debug + Copy + Clone + PartialEq> {
|
||||
Menu,
|
||||
Content(F),
|
||||
}
|
||||
//#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
//pub enum AppViewFocus<F: Debug + Copy + Clone + PartialEq> {
|
||||
//Menu,
|
||||
//Content(F),
|
||||
//}
|
||||
|
||||
impl<E, A, C, S> AppView<E, A, C, S>
|
||||
where
|
||||
E: Engine,
|
||||
A: Widget<Engine = E> + Audio,
|
||||
C: Command<Self>,
|
||||
S: StatusBar
|
||||
{
|
||||
pub fn new (
|
||||
app: A,
|
||||
menu_bar: Option<MenuBar<E, Self, C>>,
|
||||
status_bar: Option<S>,
|
||||
) -> Self {
|
||||
Self {
|
||||
app,
|
||||
cursor: (0, 0),
|
||||
entered: false,
|
||||
history: vec![],
|
||||
size: Measure::new(),
|
||||
menu_bar,
|
||||
status_bar,
|
||||
}
|
||||
}
|
||||
}
|
||||
//impl<E, A, C, S> AppView<E, A, C, S>
|
||||
//where
|
||||
//E: Engine,
|
||||
//A: Widget<Engine = E> + Audio,
|
||||
//C: Command<Self>,
|
||||
//S: StatusBar
|
||||
//{
|
||||
//pub fn new (
|
||||
//app: A,
|
||||
//menu_bar: Option<MenuBar<E, Self, C>>,
|
||||
//status_bar: Option<S>,
|
||||
//) -> Self {
|
||||
//Self {
|
||||
//app,
|
||||
//cursor: (0, 0),
|
||||
//entered: false,
|
||||
//history: vec![],
|
||||
//size: Measure::new(),
|
||||
//menu_bar,
|
||||
//status_bar,
|
||||
//}
|
||||
//}
|
||||
//}
|
||||
|
||||
impl<A, C, S> Content for AppView<Tui, A, C, S>
|
||||
where
|
||||
|
|
|
|||
|
|
@ -1,9 +1,38 @@
|
|||
use crate::*;
|
||||
|
||||
impl TryFrom<&Arc<RwLock<JackClient>>> for ArrangerApp<Tui> {
|
||||
/// Root view for standalone `tek_arranger`
|
||||
pub struct ArrangerTui {
|
||||
pub jack: Arc<RwLock<JackClient>>,
|
||||
pub transport: jack::Transport,
|
||||
pub playing: RwLock<Option<TransportState>>,
|
||||
pub started: RwLock<Option<(usize, usize)>>,
|
||||
pub current: Instant,
|
||||
pub quant: Quantize,
|
||||
pub sync: LaunchSync,
|
||||
pub metronome: bool,
|
||||
pub phrases: Vec<Arc<RwLock<Phrase>>>,
|
||||
pub phrase: usize,
|
||||
pub tracks: Vec<ArrangerTrack>,
|
||||
pub scenes: Vec<ArrangerScene>,
|
||||
pub name: Arc<RwLock<String>>,
|
||||
pub splits: [u16;2],
|
||||
pub selected: ArrangerSelection,
|
||||
pub mode: ArrangerMode,
|
||||
pub color: ItemColor,
|
||||
pub entered: bool,
|
||||
pub size: Measure<Tui>,
|
||||
pub note_buf: Vec<u8>,
|
||||
pub midi_buf: Vec<Vec<Vec<u8>>>,
|
||||
pub cursor: (usize, usize),
|
||||
pub menu_bar: Option<MenuBar<Tui, Self, ArrangerCommand>>,
|
||||
pub status_bar: Option<S>,
|
||||
pub history: Vec<C>,
|
||||
}
|
||||
|
||||
impl TryFrom<&Arc<RwLock<JackClient>>> for ArrangerTui {
|
||||
type Error = Box<dyn std::error::Error>;
|
||||
fn try_from (jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
|
||||
Ok(Self::new(ArrangerView {
|
||||
Ok(Self {
|
||||
name: Arc::new(RwLock::new(String::new())),
|
||||
phrases: vec![],
|
||||
phrase: 0,
|
||||
|
|
@ -25,87 +54,17 @@ impl TryFrom<&Arc<RwLock<JackClient>>> for ArrangerApp<Tui> {
|
|||
splits: [20, 20],
|
||||
note_buf: vec![],
|
||||
midi_buf: vec![],
|
||||
}.into(), None, None))
|
||||
cursor: (0, 0),
|
||||
entered: false,
|
||||
history: vec![],
|
||||
size: Measure::new(),
|
||||
menu_bar: None,
|
||||
status_bar: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub type ArrangerApp<E: Engine> = AppView<
|
||||
E,
|
||||
ArrangerView<E>,
|
||||
ArrangerAppCommand,
|
||||
ArrangerStatusBar
|
||||
>;
|
||||
|
||||
/// Root view for standalone `tek_arranger`
|
||||
pub struct ArrangerView<E: Engine> {
|
||||
pub(crate) jack: Arc<RwLock<JackClient>>,
|
||||
pub(crate) playing: RwLock<Option<TransportState>>,
|
||||
pub(crate) started: RwLock<Option<(usize, usize)>>,
|
||||
pub(crate) current: Instant,
|
||||
pub(crate) quant: Quantize,
|
||||
pub(crate) sync: LaunchSync,
|
||||
pub(crate) transport: jack::Transport,
|
||||
pub(crate) metronome: bool,
|
||||
pub(crate) phrases: Vec<Arc<RwLock<Phrase>>>,
|
||||
pub(crate) phrase: usize,
|
||||
pub(crate) tracks: Vec<ArrangerTrack>,
|
||||
pub(crate) scenes: Vec<ArrangerScene>,
|
||||
pub(crate) name: Arc<RwLock<String>>,
|
||||
pub(crate) splits: [u16;2],
|
||||
pub(crate) selected: ArrangerSelection,
|
||||
pub(crate) mode: ArrangerMode,
|
||||
pub(crate) color: ItemColor,
|
||||
pub(crate) entered: bool,
|
||||
pub(crate) size: Measure<E>,
|
||||
pub(crate) note_buf: Vec<u8>,
|
||||
pub(crate) midi_buf: Vec<Vec<Vec<u8>>>,
|
||||
}
|
||||
|
||||
impl HasJack for ArrangerView<Tui> {
|
||||
fn jack (&self) -> &Arc<RwLock<JackClient>> {
|
||||
&self.transport.jack()
|
||||
}
|
||||
}
|
||||
|
||||
impl Audio for ArrangerApp<Tui> {
|
||||
fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||
TracksAudio(
|
||||
&mut self.app.tracks,
|
||||
&mut self.app.note_buf,
|
||||
&mut self.app.midi_buf,
|
||||
Default::default(),
|
||||
).process(client, scope)
|
||||
}
|
||||
}
|
||||
|
||||
impl ClockApi for ArrangerView<Tui> {
|
||||
fn timebase (&self) -> &Arc<Timebase> {
|
||||
&self.current.timebase
|
||||
}
|
||||
fn quant (&self) -> &Quantize {
|
||||
&self.quant
|
||||
}
|
||||
fn sync (&self) -> &LaunchSync {
|
||||
&self.sync
|
||||
}
|
||||
}
|
||||
|
||||
impl PlayheadApi for ArrangerView<Tui> {
|
||||
fn current (&self) -> &Instant {
|
||||
&self.current
|
||||
}
|
||||
fn transport (&self) -> &jack::Transport {
|
||||
&self.transport
|
||||
}
|
||||
fn playing (&self) -> &RwLock<Option<TransportState>> {
|
||||
&self.playing
|
||||
}
|
||||
fn started (&self) -> &RwLock<Option<(usize, usize)>> {
|
||||
&self.started
|
||||
}
|
||||
}
|
||||
|
||||
impl HasPhrases for ArrangerView<Tui> {
|
||||
impl HasPhrases for ArrangerTui {
|
||||
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>> {
|
||||
&self.phrases
|
||||
}
|
||||
|
|
@ -115,7 +74,7 @@ impl HasPhrases for ArrangerView<Tui> {
|
|||
}
|
||||
|
||||
/// General methods for arranger
|
||||
impl ArrangerView<Tui> {
|
||||
impl ArrangerTui {
|
||||
pub fn selected_scene (&self) -> Option<&ArrangerScene> {
|
||||
self.selected.scene().map(|s|self.scenes().get(s)).flatten()
|
||||
}
|
||||
|
|
@ -196,33 +155,14 @@ impl ArrangerView<Tui> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> Audio for ArrangerView<E> {
|
||||
#[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||
if self.process(client, scope) == Control::Quit {
|
||||
return Control::Quit
|
||||
}
|
||||
// FIXME: one of these per playing track
|
||||
if let ArrangerSelection::Clip(t, s) = self.selected {
|
||||
let phrase = self.scenes().get(s).map(|scene|scene.clips.get(t));
|
||||
if let Some(Some(Some(phrase))) = phrase {
|
||||
if let Some(track) = self.tracks().get(t) {
|
||||
if let Some((ref started_at, Some(ref playing))) = track.player.phrase {
|
||||
let phrase = phrase.read().unwrap();
|
||||
if *playing.read().unwrap() == *phrase {
|
||||
let pulse = self.current().pulse.get();
|
||||
let start = started_at.pulse.get();
|
||||
let now = (pulse - start) % phrase.length as f64;
|
||||
self.editor.now.set(now);
|
||||
return Control::Continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.editor.now.set(0.);
|
||||
return Control::Continue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//pub fn track_next (&mut self, last_track: usize) {
|
||||
//use ArrangerSelection::*;
|
||||
//*self = match self {
|
||||
|
|
|
|||
|
|
@ -1,34 +1,34 @@
|
|||
use crate::*;
|
||||
|
||||
/// Handle top-level events in standalone arranger.
|
||||
impl Handle<Tui> for ArrangerApp<Tui> {
|
||||
impl Handle<Tui> for ArrangerTui {
|
||||
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
|
||||
ArrangerAppCommand::execute_with_state(self, i)
|
||||
ArrangerCommand::execute_with_state(self, i)
|
||||
}
|
||||
}
|
||||
|
||||
pub type ArrangerAppCommand = AppViewCommand<ArrangerViewCommand>;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ArrangerViewCommand {
|
||||
pub enum ArrangerCommand {
|
||||
Focus(FocusCommand),
|
||||
Undo,
|
||||
Redo,
|
||||
Clear,
|
||||
Clock(ClockCommand),
|
||||
Playhead(PlayheadCommand),
|
||||
Scene(ArrangerSceneCommand),
|
||||
Track(ArrangerTrackCommand),
|
||||
Clip(ArrangerClipCommand),
|
||||
Select(ArrangerSelection),
|
||||
Zoom(usize),
|
||||
Clock(ClockCommand),
|
||||
Playhead(PlayheadCommand),
|
||||
Phrases(PhrasePoolViewCommand),
|
||||
Editor(PhraseEditorCommand),
|
||||
EditPhrase(Option<Arc<RwLock<Phrase>>>),
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, ArrangerApp<Tui>> for ArrangerAppCommand {
|
||||
fn input_to_command (view: &ArrangerApp<Tui>, input: &TuiInput) -> Option<Self> {
|
||||
use AppViewFocus::*;
|
||||
impl InputToCommand<Tui, ArrangerTui> for ArrangerCommand {
|
||||
fn input_to_command (view: &ArrangerTui, input: &TuiInput) -> Option<Self> {
|
||||
use FocusCommand::*;
|
||||
use ArrangerViewCommand::*;
|
||||
use ArrangerCommand::*;
|
||||
Some(match input.event() {
|
||||
key!(KeyCode::Tab) => Self::Focus(Next),
|
||||
key!(Shift-KeyCode::Tab) => Self::Focus(Prev),
|
||||
|
|
@ -56,11 +56,11 @@ impl InputToCommand<Tui, ArrangerApp<Tui>> for ArrangerAppCommand {
|
|||
}
|
||||
},
|
||||
Content(ArrangerFocus::PhraseEditor) => Editor(
|
||||
PhraseEditorCommand::input_to_command(&view.app.editor, input)?
|
||||
PhraseEditorCommand::input_to_command(&view.editor, input)?
|
||||
),
|
||||
Content(ArrangerFocus::PhrasePool) => match input.event() {
|
||||
key!(KeyCode::Char('e')) => EditPhrase(
|
||||
Some(view.app.phrase().clone())
|
||||
Some(view.phrase().clone())
|
||||
),
|
||||
_ => Phrases(
|
||||
PhrasePoolViewCommand::input_to_command(view, input)?
|
||||
|
|
@ -76,28 +76,28 @@ impl InputToCommand<Tui, ArrangerApp<Tui>> for ArrangerAppCommand {
|
|||
_ => match input.event() {
|
||||
// FIXME: boundary conditions
|
||||
|
||||
key!(KeyCode::Up) => match view.app.selected {
|
||||
key!(KeyCode::Up) => match view.selected {
|
||||
Select::Mix => return None,
|
||||
Select::Track(t) => return None,
|
||||
Select::Scene(s) => Select(Select::Scene(s - 1)),
|
||||
Select::Clip(t, s) => Select(Select::Clip(t, s - 1)),
|
||||
},
|
||||
|
||||
key!(KeyCode::Down) => match view.app.selected {
|
||||
key!(KeyCode::Down) => match view.selected {
|
||||
Select::Mix => Select(Select::Scene(0)),
|
||||
Select::Track(t) => Select(Select::Clip(t, 0)),
|
||||
Select::Scene(s) => Select(Select::Scene(s + 1)),
|
||||
Select::Clip(t, s) => Select(Select::Clip(t, s + 1)),
|
||||
},
|
||||
|
||||
key!(KeyCode::Left) => match view.app.selected {
|
||||
key!(KeyCode::Left) => match view.selected {
|
||||
Select::Mix => return None,
|
||||
Select::Track(t) => Select(Select::Track(t - 1)),
|
||||
Select::Scene(s) => return None,
|
||||
Select::Clip(t, s) => Select(Select::Clip(t - 1, s)),
|
||||
},
|
||||
|
||||
key!(KeyCode::Right) => match view.app.selected {
|
||||
key!(KeyCode::Right) => match view.selected {
|
||||
Select::Mix => return None,
|
||||
Select::Track(t) => Select(Select::Track(t + 1)),
|
||||
Select::Scene(s) => Select(Select::Clip(0, s)),
|
||||
|
|
@ -114,42 +114,42 @@ impl InputToCommand<Tui, ArrangerApp<Tui>> for ArrangerAppCommand {
|
|||
|
||||
key!(KeyCode::Char('`')) => { todo!("toggle view mode") },
|
||||
|
||||
key!(KeyCode::Char(',')) => match view.app.selected {
|
||||
key!(KeyCode::Char(',')) => match view.selected {
|
||||
Select::Mix => Zoom(0),
|
||||
Select::Track(t) => Track(Track::Swap(t, t - 1)),
|
||||
Select::Scene(s) => Scene(Scene::Swap(s, s - 1)),
|
||||
Select::Clip(t, s) => Clip(Clip::Set(t, s, None)),
|
||||
},
|
||||
|
||||
key!(KeyCode::Char('.')) => match view.app.selected {
|
||||
key!(KeyCode::Char('.')) => match view.selected {
|
||||
Select::Mix => Zoom(0),
|
||||
Select::Track(t) => Track(Track::Swap(t, t + 1)),
|
||||
Select::Scene(s) => Scene(Scene::Swap(s, s + 1)),
|
||||
Select::Clip(t, s) => Clip(Clip::Set(t, s, None)),
|
||||
},
|
||||
|
||||
key!(KeyCode::Char('<')) => match view.app.selected {
|
||||
key!(KeyCode::Char('<')) => match view.selected {
|
||||
Select::Mix => Zoom(0),
|
||||
Select::Track(t) => Track(Track::Swap(t, t - 1)),
|
||||
Select::Scene(s) => Scene(Scene::Swap(s, s - 1)),
|
||||
Select::Clip(t, s) => Clip(Clip::Set(t, s, None)),
|
||||
},
|
||||
|
||||
key!(KeyCode::Char('>')) => match view.app.selected {
|
||||
key!(KeyCode::Char('>')) => match view.selected {
|
||||
Select::Mix => Zoom(0),
|
||||
Select::Track(t) => Track(Track::Swap(t, t + 1)),
|
||||
Select::Scene(s) => Scene(Scene::Swap(s, s + 1)),
|
||||
Select::Clip(t, s) => Clip(Clip::Set(t, s, None)),
|
||||
},
|
||||
|
||||
key!(KeyCode::Enter) => match view.app.selected {
|
||||
key!(KeyCode::Enter) => match view.selected {
|
||||
Select::Mix => return None,
|
||||
Select::Track(t) => return None,
|
||||
Select::Scene(s) => Scene(Scene::Play(s)),
|
||||
Select::Clip(t, s) => return None,
|
||||
},
|
||||
|
||||
key!(KeyCode::Delete) => match view.app.selected {
|
||||
key!(KeyCode::Delete) => match view.selected {
|
||||
Select::Mix => Clear,
|
||||
Select::Track(t) => Track(Track::Delete(t)),
|
||||
Select::Scene(s) => Scene(Scene::Delete(s)),
|
||||
|
|
@ -158,12 +158,12 @@ impl InputToCommand<Tui, ArrangerApp<Tui>> for ArrangerAppCommand {
|
|||
|
||||
key!(KeyCode::Char('c')) => Clip(Clip::RandomColor),
|
||||
|
||||
key!(KeyCode::Char('s')) => match view.app.selected {
|
||||
key!(KeyCode::Char('s')) => match view.selected {
|
||||
Select::Clip(t, s) => Clip(Clip::Set(t, s, None)),
|
||||
_ => return None,
|
||||
},
|
||||
|
||||
key!(KeyCode::Char('g')) => match view.app.selected {
|
||||
key!(KeyCode::Char('g')) => match view.selected {
|
||||
Select::Clip(t, s) => Clip(Clip::Get(t, s)),
|
||||
_ => return None,
|
||||
},
|
||||
|
|
@ -183,9 +183,8 @@ impl InputToCommand<Tui, ArrangerApp<Tui>> for ArrangerAppCommand {
|
|||
}
|
||||
}
|
||||
|
||||
impl Command<ArrangerApp<Tui>> for ArrangerAppCommand {
|
||||
fn execute (self, state: &mut ArrangerApp<Tui>) -> Perhaps<Self> {
|
||||
use AppViewCommand::*;
|
||||
impl Command<ArrangerTui> for ArrangerCommand {
|
||||
fn execute (self, state: &mut ArrangerTui) -> Perhaps<Self> {
|
||||
let undo = match self {
|
||||
Focus(cmd) => { delegate(cmd, Focus, state) },
|
||||
App(cmd) => { delegate(cmd, App, state) }
|
||||
|
|
@ -197,17 +196,17 @@ impl Command<ArrangerApp<Tui>> for ArrangerAppCommand {
|
|||
}
|
||||
}
|
||||
|
||||
impl Command<ArrangerApp<Tui>> for ArrangerViewCommand {
|
||||
fn execute (self, state: &mut ArrangerApp<Tui>) -> Perhaps<Self> {
|
||||
use ArrangerViewCommand::*;
|
||||
impl Command<ArrangerTui> for ArrangerCommand {
|
||||
fn execute (self, state: &mut ArrangerTui) -> Perhaps<Self> {
|
||||
use ArrangerCommand::*;
|
||||
match self {
|
||||
Scene(cmd) => { delegate(cmd, Scene, &mut state.app) },
|
||||
Track(cmd) => { delegate(cmd, Track, &mut state.app) },
|
||||
Clip(cmd) => { delegate(cmd, Clip, &mut state.app) },
|
||||
Phrases(cmd) => { delegate(cmd, Phrases, &mut state.app) },
|
||||
Editor(cmd) => { delegate(cmd, Editor, &mut state.app) },
|
||||
Clock(cmd) => { delegate(cmd, Clock, &mut state.app) },
|
||||
Playhead(cmd) => { delegate(cmd, Playhead, &mut state.app) },
|
||||
Scene(cmd) => { delegate(cmd, Scene, &mut state) },
|
||||
Track(cmd) => { delegate(cmd, Track, &mut state) },
|
||||
Clip(cmd) => { delegate(cmd, Clip, &mut state) },
|
||||
Phrases(cmd) => { delegate(cmd, Phrases, &mut state) },
|
||||
Editor(cmd) => { delegate(cmd, Editor, &mut state) },
|
||||
Clock(cmd) => { delegate(cmd, Clock, &mut state) },
|
||||
Playhead(cmd) => { delegate(cmd, Playhead, &mut state) },
|
||||
Zoom(zoom) => { todo!(); },
|
||||
Select(selected) => { state.selected = selected; Ok(None) },
|
||||
EditPhrase(phrase) => {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ pub enum ArrangerFocus {
|
|||
}
|
||||
|
||||
impl FocusEnter for ArrangerApp<Tui> {
|
||||
type Item = AppViewFocus<ArrangerFocus>;
|
||||
fn focus_enter (&mut self) {
|
||||
use AppViewFocus::*;
|
||||
use ArrangerFocus::*;
|
||||
|
|
|
|||
73
crates/tek_tui/src/tui_arranger_jack.rs
Normal file
73
crates/tek_tui/src/tui_arranger_jack.rs
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
use crate::*;
|
||||
|
||||
impl JackApi for ArrangerTui {
|
||||
fn jack (&self) -> &Arc<RwLock<JackClient>> {
|
||||
&self.jack
|
||||
}
|
||||
}
|
||||
|
||||
impl Audio for ArrangerTui {
|
||||
fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||
TracksAudio(
|
||||
&mut self.app.tracks,
|
||||
&mut self.app.note_buf,
|
||||
&mut self.app.midi_buf,
|
||||
Default::default(),
|
||||
).process(client, scope)
|
||||
}
|
||||
}
|
||||
|
||||
impl Audio for ArrangerTui {
|
||||
#[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||
if self.process(client, scope) == Control::Quit {
|
||||
return Control::Quit
|
||||
}
|
||||
// FIXME: one of these per playing track
|
||||
if let ArrangerSelection::Clip(t, s) = self.selected {
|
||||
let phrase = self.scenes().get(s).map(|scene|scene.clips.get(t));
|
||||
if let Some(Some(Some(phrase))) = phrase {
|
||||
if let Some(track) = self.tracks().get(t) {
|
||||
if let Some((ref started_at, Some(ref playing))) = track.player.phrase {
|
||||
let phrase = phrase.read().unwrap();
|
||||
if *playing.read().unwrap() == *phrase {
|
||||
let pulse = self.current().pulse.get();
|
||||
let start = started_at.pulse.get();
|
||||
let now = (pulse - start) % phrase.length as f64;
|
||||
self.editor.now.set(now);
|
||||
return Control::Continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.editor.now.set(0.);
|
||||
return Control::Continue
|
||||
}
|
||||
}
|
||||
|
||||
impl ClockApi for ArrangerTui {
|
||||
fn timebase (&self) -> &Arc<Timebase> {
|
||||
&self.current.timebase
|
||||
}
|
||||
fn quant (&self) -> &Quantize {
|
||||
&self.quant
|
||||
}
|
||||
fn sync (&self) -> &LaunchSync {
|
||||
&self.sync
|
||||
}
|
||||
}
|
||||
|
||||
impl PlayheadApi for ArrangerTui {
|
||||
fn current (&self) -> &Instant {
|
||||
&self.current
|
||||
}
|
||||
fn transport (&self) -> &jack::Transport {
|
||||
&self.transport
|
||||
}
|
||||
fn playing (&self) -> &RwLock<Option<TransportState>> {
|
||||
&self.playing
|
||||
}
|
||||
fn started (&self) -> &RwLock<Option<(usize, usize)>> {
|
||||
&self.started
|
||||
}
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ impl Content for ArrangerView<Tui> {
|
|||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
Split::up(
|
||||
1,
|
||||
widget(&TransportRef(self)),
|
||||
widget(&TransportView(self)),
|
||||
Split::down(
|
||||
self.splits[0],
|
||||
lay!(
|
||||
|
|
@ -45,20 +45,20 @@ impl Content for ArrangerView<Tui> {
|
|||
.grow_y(1)
|
||||
.border(Lozenge(Style::default()
|
||||
.bg(TuiTheme::border_bg())
|
||||
.fg(TuiTheme::border_fg(self.focused)))),
|
||||
.fg(TuiTheme::border_fg(self.focused() == ArrangerFocus::Arranger)))),
|
||||
widget(&self.size),
|
||||
widget(&format!("[{}] Arranger", if self.entered {
|
||||
"■"
|
||||
} else {
|
||||
" "
|
||||
}))
|
||||
.fg(TuiTheme::title_fg(self.focused))
|
||||
.fg(TuiTheme::title_fg(self.focused() == ArrangerFocus::Arranger))
|
||||
.push_x(1),
|
||||
),
|
||||
Split::right(
|
||||
self.splits[1],
|
||||
widget(&self.phrases),
|
||||
widget(&PhraseEditorRef(self)),
|
||||
widget(&PhraseView(self)),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -34,14 +34,14 @@ pub struct PhraseEditor<E: Engine> {
|
|||
impl Widget for PhraseEditor<Tui> {
|
||||
type Engine = Tui;
|
||||
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
PhraseEditorRef(&self, Default::default()).layout(to)
|
||||
PhraseView(&self, Default::default()).layout(to)
|
||||
}
|
||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||
PhraseEditorRef(&self, Default::default()).render(to)
|
||||
PhraseView(&self, Default::default()).render(to)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PhraseEditorRef<'a, T: PhraseEditorViewState>(pub &'a T);
|
||||
pub struct PhraseView<'a, T: PhraseEditorViewState>(pub &'a T);
|
||||
|
||||
pub trait PhraseEditorViewState: Send + Sync {
|
||||
fn focused (&self) -> bool;
|
||||
|
|
@ -89,7 +89,7 @@ impl PhraseEditorViewState for PhraseEditor<Tui> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, T: PhraseEditorViewState> Content for PhraseEditorRef<'a, T> {
|
||||
impl<'a, T: PhraseEditorViewState> Content for PhraseView<'a, T> {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
let phrase = self.0.phrase();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
use crate::*;
|
||||
|
||||
pub struct PhrasePoolView<E: Engine> {
|
||||
_engine: PhantomData<E>,
|
||||
pub struct PhrasesTui {
|
||||
/// Collection of phrases
|
||||
pub phrases: Vec<Arc<RwLock<Phrase>>>,
|
||||
/// Selected phrase
|
||||
|
|
@ -24,10 +23,9 @@ pub enum PhrasePoolMode {
|
|||
Length(usize, usize, PhraseLengthFocus),
|
||||
}
|
||||
|
||||
impl<E: Engine> PhrasePoolView<E> {
|
||||
impl PhrasesTui {
|
||||
pub fn new (phrases: Vec<Arc<RwLock<Phrase>>>) -> Self {
|
||||
Self {
|
||||
_engine: Default::default(),
|
||||
scroll: 0,
|
||||
phrase: 0,
|
||||
mode: None,
|
||||
|
|
@ -93,210 +91,8 @@ impl<E: Engine> PhrasePoolView<E> {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Display phrases always in order of appearance
|
||||
impl Content for PhrasePoolView<Tui> {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
let Self { focused, phrases, mode, .. } = self;
|
||||
let content = col!(
|
||||
(i, phrase) in phrases.iter().enumerate() => Layers::new(|add|{
|
||||
let Phrase { ref name, color, length, .. } = *phrase.read().unwrap();
|
||||
let mut length = PhraseLength::new(length, None);
|
||||
if let Some(PhrasePoolMode::Length(phrase, new_length, focus)) = mode {
|
||||
if *focused && i == *phrase {
|
||||
length.pulses = *new_length;
|
||||
length.focus = Some(*focus);
|
||||
}
|
||||
}
|
||||
let length = length.align_e().fill_x();
|
||||
let row1 = lay!(format!(" {i}").align_w().fill_x(), length).fill_x();
|
||||
let mut row2 = format!(" {name}");
|
||||
if let Some(PhrasePoolMode::Rename(phrase, _)) = mode {
|
||||
if *focused && i == *phrase { row2 = format!("{row2}▄"); }
|
||||
};
|
||||
let row2 = TuiStyle::bold(row2, true);
|
||||
add(&col!(row1, row2).fill_x().bg(color.base.rgb))?;
|
||||
Ok(if *focused && i == self.phrase { add(&CORNERS)?; })
|
||||
})
|
||||
);
|
||||
let border_color = if *focused {Color::Rgb(100, 110, 40)} else {Color::Rgb(70, 80, 50)};
|
||||
let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color));
|
||||
let content = content.fill_xy().bg(Color::Rgb(28, 35, 25)).border(border);
|
||||
let title_color = if *focused {Color::Rgb(150, 160, 90)} else {Color::Rgb(120, 130, 100)};
|
||||
let upper_left = format!("[{}] Phrases", if self.entered {"■"} else {" "});
|
||||
let upper_right = format!("({})", phrases.len());
|
||||
lay!(
|
||||
content,
|
||||
TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(),
|
||||
TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub enum PhrasePoolViewCommand {
|
||||
Select(usize),
|
||||
Edit(PhrasePoolCommand),
|
||||
Rename(PhraseRenameCommand),
|
||||
Length(PhraseLengthCommand),
|
||||
}
|
||||
|
||||
impl Handle<Tui> for PhrasePoolView<Tui> {
|
||||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||
PhrasePoolViewCommand::execute_with_state(self, from)
|
||||
}
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, PhrasePoolView<Tui>> for PhrasePoolViewCommand {
|
||||
fn input_to_command (state: &PhrasePoolView<Tui>, input: &TuiInput) -> Option<Self> {
|
||||
use PhrasePoolViewCommand as Cmd;
|
||||
use PhrasePoolCommand as Edit;
|
||||
use PhraseRenameCommand as Rename;
|
||||
use PhraseLengthCommand as Length;
|
||||
match input.event() {
|
||||
key!(KeyCode::Up) => Some(Cmd::Select(0)),
|
||||
key!(KeyCode::Down) => Some(Cmd::Select(0)),
|
||||
key!(KeyCode::Char(',')) => Some(Cmd::Edit(Edit::Swap(0, 0))),
|
||||
key!(KeyCode::Char('.')) => Some(Cmd::Edit(Edit::Swap(0, 0))),
|
||||
key!(KeyCode::Delete) => Some(Cmd::Edit(Edit::Delete(0))),
|
||||
key!(KeyCode::Char('a')) => Some(Cmd::Edit(Edit::Add(0))),
|
||||
key!(KeyCode::Char('i')) => Some(Cmd::Edit(Edit::Add(0))),
|
||||
key!(KeyCode::Char('d')) => Some(Cmd::Edit(Edit::Duplicate(0))),
|
||||
key!(KeyCode::Char('c')) => Some(Cmd::Edit(Edit::RandomColor(0))),
|
||||
key!(KeyCode::Char('n')) => Some(Cmd::Rename(Rename::Begin)),
|
||||
key!(KeyCode::Char('t')) => Some(Cmd::Length(Length::Begin)),
|
||||
_ => match state.mode {
|
||||
Some(PhrasePoolMode::Rename(..)) => {
|
||||
Rename::input_to_command(state, input).map(Cmd::Rename)
|
||||
},
|
||||
Some(PhrasePoolMode::Length(..)) => {
|
||||
Length::input_to_command(state, input).map(Cmd::Length)
|
||||
},
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> Command<PhrasePoolView<E>> for PhrasePoolViewCommand {
|
||||
fn execute (self, view: &mut PhrasePoolView<E>) -> Perhaps<Self> {
|
||||
use PhraseRenameCommand as Rename;
|
||||
use PhraseLengthCommand as Length;
|
||||
match self {
|
||||
Self::Select(phrase) => {
|
||||
view.phrase = phrase
|
||||
},
|
||||
Self::Edit(command) => {
|
||||
return Ok(command.execute(&mut view)?.map(Self::Edit))
|
||||
}
|
||||
Self::Rename(command) => match command {
|
||||
Rename::Begin => {
|
||||
view.mode = Some(PhrasePoolMode::Rename(
|
||||
view.phrase,
|
||||
view.phrases[view.phrase].read().unwrap().name.clone()
|
||||
))
|
||||
},
|
||||
_ => {
|
||||
return Ok(command.execute(view)?.map(Self::Rename))
|
||||
}
|
||||
},
|
||||
Self::Length(command) => match command {
|
||||
Length::Begin => {
|
||||
view.mode = Some(PhrasePoolMode::Length(
|
||||
view.phrase,
|
||||
view.phrases[view.phrase].read().unwrap().length,
|
||||
PhraseLengthFocus::Bar
|
||||
))
|
||||
},
|
||||
_ => {
|
||||
return Ok(command.execute(view)?.map(Self::Length))
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum PhraseLengthCommand {
|
||||
Begin,
|
||||
Next,
|
||||
Prev,
|
||||
Inc,
|
||||
Dec,
|
||||
Set(usize),
|
||||
Cancel,
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, PhrasePoolView<Tui>> for PhraseLengthCommand {
|
||||
fn input_to_command (view: &PhrasePoolView<Tui>, from: &TuiInput) -> Option<Self> {
|
||||
if let Some(PhrasePoolMode::Length(_, length, _)) = view.mode {
|
||||
Some(match from.event() {
|
||||
key!(KeyCode::Up) => Self::Inc,
|
||||
key!(KeyCode::Down) => Self::Dec,
|
||||
key!(KeyCode::Right) => Self::Next,
|
||||
key!(KeyCode::Left) => Self::Prev,
|
||||
key!(KeyCode::Enter) => Self::Set(length),
|
||||
key!(KeyCode::Esc) => Self::Cancel,
|
||||
_ => return None
|
||||
})
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> Command<PhrasePoolView<E>> for PhraseLengthCommand {
|
||||
fn execute (self, view: &mut PhrasePoolView<E>) -> Perhaps<Self> {
|
||||
use PhraseLengthFocus::*;
|
||||
use PhraseLengthCommand::*;
|
||||
if let Some(PhrasePoolMode::Length(phrase, ref mut length, ref mut focus)) = view.mode {
|
||||
match self {
|
||||
Self::Cancel => {
|
||||
view.mode = None;
|
||||
},
|
||||
Self::Prev => {
|
||||
focus.prev()
|
||||
},
|
||||
Self::Next => {
|
||||
focus.next()
|
||||
},
|
||||
Self::Inc => match focus {
|
||||
Bar => { *length += 4 * PPQ },
|
||||
Beat => { *length += PPQ },
|
||||
Tick => { *length += 1 },
|
||||
},
|
||||
Self::Dec => match focus {
|
||||
Bar => { *length = length.saturating_sub(4 * PPQ) },
|
||||
Beat => { *length = length.saturating_sub(PPQ) },
|
||||
Tick => { *length = length.saturating_sub(1) },
|
||||
},
|
||||
Self::Set(length) => {
|
||||
let mut phrase = view.phrases[phrase].write().unwrap();
|
||||
let old_length = phrase.length;
|
||||
phrase.length = length;
|
||||
view.mode = None;
|
||||
return Ok(Some(Self::Set(old_length)))
|
||||
},
|
||||
_ => unreachable!()
|
||||
}
|
||||
Ok(None)
|
||||
} else if self == Begin {
|
||||
view.mode = Some(PhrasePoolMode::Length(
|
||||
view.phrase,
|
||||
view.phrases[view.phrase].read().unwrap().length,
|
||||
PhraseLengthFocus::Bar
|
||||
));
|
||||
Ok(None)
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Displays and edits phrase length.
|
||||
pub struct PhraseLength<E: Engine> {
|
||||
_engine: PhantomData<E>,
|
||||
pub struct PhraseLength {
|
||||
/// Pulses per beat (quaver)
|
||||
pub ppq: usize,
|
||||
/// Beats per bar
|
||||
|
|
@ -331,7 +127,7 @@ impl<E: Engine> PhraseLength<E> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Content for PhraseLength<Tui> {
|
||||
impl Content for PhraseLength {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
Layers::new(move|add|{
|
||||
|
|
@ -392,68 +188,3 @@ impl PhraseLengthFocus {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum PhraseRenameCommand {
|
||||
Begin,
|
||||
Set(String),
|
||||
Confirm,
|
||||
Cancel,
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, PhrasePoolView<Tui>> for PhraseRenameCommand {
|
||||
fn input_to_command (view: &PhrasePoolView<Tui>, from: &TuiInput) -> Option<Self> {
|
||||
if let Some(PhrasePoolMode::Rename(_, ref old_name)) = view.mode {
|
||||
Some(match from.event() {
|
||||
key!(KeyCode::Char(c)) => {
|
||||
let mut new_name = old_name.clone();
|
||||
new_name.push(*c);
|
||||
Self::Set(new_name)
|
||||
},
|
||||
key!(KeyCode::Backspace) => {
|
||||
let mut new_name = old_name.clone();
|
||||
new_name.pop();
|
||||
Self::Set(new_name)
|
||||
},
|
||||
key!(KeyCode::Enter) => Self::Confirm,
|
||||
key!(KeyCode::Esc) => Self::Cancel,
|
||||
_ => return None
|
||||
})
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> Command<PhrasePoolView<E>> for PhraseRenameCommand {
|
||||
fn execute (self, view: &mut PhrasePoolView<E>) -> Perhaps<Self> {
|
||||
use PhraseRenameCommand::*;
|
||||
if let Some(PhrasePoolMode::Rename(phrase, ref mut old_name)) = view.mode {
|
||||
match self {
|
||||
Set(s) => {
|
||||
view.phrases[phrase].write().unwrap().name = s.into();
|
||||
return Ok(Some(Self::Set(old_name.clone())))
|
||||
},
|
||||
Confirm => {
|
||||
let old_name = old_name.clone();
|
||||
view.mode = None;
|
||||
return Ok(Some(Self::Set(old_name)))
|
||||
},
|
||||
Cancel => {
|
||||
let mut phrase = view.phrases[phrase].write().unwrap();
|
||||
phrase.name = old_name.clone();
|
||||
},
|
||||
_ => unreachable!()
|
||||
};
|
||||
Ok(None)
|
||||
} else if self == Begin {
|
||||
view.mode = Some(PhrasePoolMode::Rename(
|
||||
view.phrase,
|
||||
view.phrases[view.phrase].read().unwrap().name.clone()
|
||||
));
|
||||
Ok(None)
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
227
crates/tek_tui/src/tui_pool_cmd.rs
Normal file
227
crates/tek_tui/src/tui_pool_cmd.rs
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
use crate::*;
|
||||
|
||||
impl Handle<Tui> for PhrasePoolView<Tui> {
|
||||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||
PhrasePoolViewCommand::execute_with_state(self, from)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub enum PhrasePoolViewCommand {
|
||||
Select(usize),
|
||||
Edit(PhrasePoolCommand),
|
||||
Rename(PhraseRenameCommand),
|
||||
Length(PhraseLengthCommand),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum PhraseRenameCommand {
|
||||
Begin,
|
||||
Set(String),
|
||||
Confirm,
|
||||
Cancel,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum PhraseLengthCommand {
|
||||
Begin,
|
||||
Next,
|
||||
Prev,
|
||||
Inc,
|
||||
Dec,
|
||||
Set(usize),
|
||||
Cancel,
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, PhrasePoolView<Tui>> for PhrasePoolViewCommand {
|
||||
fn input_to_command (state: &PhrasePoolView<Tui>, input: &TuiInput) -> Option<Self> {
|
||||
use PhrasePoolViewCommand as Cmd;
|
||||
use PhrasePoolCommand as Edit;
|
||||
use PhraseRenameCommand as Rename;
|
||||
use PhraseLengthCommand as Length;
|
||||
match input.event() {
|
||||
key!(KeyCode::Up) => Some(Cmd::Select(0)),
|
||||
key!(KeyCode::Down) => Some(Cmd::Select(0)),
|
||||
key!(KeyCode::Char(',')) => Some(Cmd::Edit(Edit::Swap(0, 0))),
|
||||
key!(KeyCode::Char('.')) => Some(Cmd::Edit(Edit::Swap(0, 0))),
|
||||
key!(KeyCode::Delete) => Some(Cmd::Edit(Edit::Delete(0))),
|
||||
key!(KeyCode::Char('a')) => Some(Cmd::Edit(Edit::Add(0))),
|
||||
key!(KeyCode::Char('i')) => Some(Cmd::Edit(Edit::Add(0))),
|
||||
key!(KeyCode::Char('d')) => Some(Cmd::Edit(Edit::Duplicate(0))),
|
||||
key!(KeyCode::Char('c')) => Some(Cmd::Edit(Edit::RandomColor(0))),
|
||||
key!(KeyCode::Char('n')) => Some(Cmd::Rename(Rename::Begin)),
|
||||
key!(KeyCode::Char('t')) => Some(Cmd::Length(Length::Begin)),
|
||||
_ => match state.mode {
|
||||
Some(PhrasePoolMode::Rename(..)) => {
|
||||
Rename::input_to_command(state, input).map(Cmd::Rename)
|
||||
},
|
||||
Some(PhrasePoolMode::Length(..)) => {
|
||||
Length::input_to_command(state, input).map(Cmd::Length)
|
||||
},
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> Command<PhrasePoolView<E>> for PhrasePoolViewCommand {
|
||||
fn execute (self, view: &mut PhrasePoolView<E>) -> Perhaps<Self> {
|
||||
use PhraseRenameCommand as Rename;
|
||||
use PhraseLengthCommand as Length;
|
||||
match self {
|
||||
Self::Select(phrase) => {
|
||||
view.phrase = phrase
|
||||
},
|
||||
Self::Edit(command) => {
|
||||
return Ok(command.execute(&mut view)?.map(Self::Edit))
|
||||
}
|
||||
Self::Rename(command) => match command {
|
||||
Rename::Begin => {
|
||||
view.mode = Some(PhrasePoolMode::Rename(
|
||||
view.phrase,
|
||||
view.phrases[view.phrase].read().unwrap().name.clone()
|
||||
))
|
||||
},
|
||||
_ => {
|
||||
return Ok(command.execute(view)?.map(Self::Rename))
|
||||
}
|
||||
},
|
||||
Self::Length(command) => match command {
|
||||
Length::Begin => {
|
||||
view.mode = Some(PhrasePoolMode::Length(
|
||||
view.phrase,
|
||||
view.phrases[view.phrase].read().unwrap().length,
|
||||
PhraseLengthFocus::Bar
|
||||
))
|
||||
},
|
||||
_ => {
|
||||
return Ok(command.execute(view)?.map(Self::Length))
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, PhrasePoolView<Tui>> for PhraseLengthCommand {
|
||||
fn input_to_command (view: &PhrasePoolView<Tui>, from: &TuiInput) -> Option<Self> {
|
||||
if let Some(PhrasePoolMode::Length(_, length, _)) = view.mode {
|
||||
Some(match from.event() {
|
||||
key!(KeyCode::Up) => Self::Inc,
|
||||
key!(KeyCode::Down) => Self::Dec,
|
||||
key!(KeyCode::Right) => Self::Next,
|
||||
key!(KeyCode::Left) => Self::Prev,
|
||||
key!(KeyCode::Enter) => Self::Set(length),
|
||||
key!(KeyCode::Esc) => Self::Cancel,
|
||||
_ => return None
|
||||
})
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> Command<PhrasePoolView<E>> for PhraseLengthCommand {
|
||||
fn execute (self, view: &mut PhrasePoolView<E>) -> Perhaps<Self> {
|
||||
use PhraseLengthFocus::*;
|
||||
use PhraseLengthCommand::*;
|
||||
if let Some(PhrasePoolMode::Length(phrase, ref mut length, ref mut focus)) = view.mode {
|
||||
match self {
|
||||
Self::Cancel => {
|
||||
view.mode = None;
|
||||
},
|
||||
Self::Prev => {
|
||||
focus.prev()
|
||||
},
|
||||
Self::Next => {
|
||||
focus.next()
|
||||
},
|
||||
Self::Inc => match focus {
|
||||
Bar => { *length += 4 * PPQ },
|
||||
Beat => { *length += PPQ },
|
||||
Tick => { *length += 1 },
|
||||
},
|
||||
Self::Dec => match focus {
|
||||
Bar => { *length = length.saturating_sub(4 * PPQ) },
|
||||
Beat => { *length = length.saturating_sub(PPQ) },
|
||||
Tick => { *length = length.saturating_sub(1) },
|
||||
},
|
||||
Self::Set(length) => {
|
||||
let mut phrase = view.phrases[phrase].write().unwrap();
|
||||
let old_length = phrase.length;
|
||||
phrase.length = length;
|
||||
view.mode = None;
|
||||
return Ok(Some(Self::Set(old_length)))
|
||||
},
|
||||
_ => unreachable!()
|
||||
}
|
||||
Ok(None)
|
||||
} else if self == Begin {
|
||||
view.mode = Some(PhrasePoolMode::Length(
|
||||
view.phrase,
|
||||
view.phrases[view.phrase].read().unwrap().length,
|
||||
PhraseLengthFocus::Bar
|
||||
));
|
||||
Ok(None)
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, PhrasePoolView<Tui>> for PhraseRenameCommand {
|
||||
fn input_to_command (view: &PhrasePoolView<Tui>, from: &TuiInput) -> Option<Self> {
|
||||
if let Some(PhrasePoolMode::Rename(_, ref old_name)) = view.mode {
|
||||
Some(match from.event() {
|
||||
key!(KeyCode::Char(c)) => {
|
||||
let mut new_name = old_name.clone();
|
||||
new_name.push(*c);
|
||||
Self::Set(new_name)
|
||||
},
|
||||
key!(KeyCode::Backspace) => {
|
||||
let mut new_name = old_name.clone();
|
||||
new_name.pop();
|
||||
Self::Set(new_name)
|
||||
},
|
||||
key!(KeyCode::Enter) => Self::Confirm,
|
||||
key!(KeyCode::Esc) => Self::Cancel,
|
||||
_ => return None
|
||||
})
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> Command<PhrasePoolView<E>> for PhraseRenameCommand {
|
||||
fn execute (self, view: &mut PhrasePoolView<E>) -> Perhaps<Self> {
|
||||
use PhraseRenameCommand::*;
|
||||
if let Some(PhrasePoolMode::Rename(phrase, ref mut old_name)) = view.mode {
|
||||
match self {
|
||||
Set(s) => {
|
||||
view.phrases[phrase].write().unwrap().name = s.into();
|
||||
return Ok(Some(Self::Set(old_name.clone())))
|
||||
},
|
||||
Confirm => {
|
||||
let old_name = old_name.clone();
|
||||
view.mode = None;
|
||||
return Ok(Some(Self::Set(old_name)))
|
||||
},
|
||||
Cancel => {
|
||||
let mut phrase = view.phrases[phrase].write().unwrap();
|
||||
phrase.name = old_name.clone();
|
||||
},
|
||||
_ => unreachable!()
|
||||
};
|
||||
Ok(None)
|
||||
} else if self == Begin {
|
||||
view.mode = Some(PhrasePoolMode::Rename(
|
||||
view.phrase,
|
||||
view.phrases[view.phrase].read().unwrap().name.clone()
|
||||
));
|
||||
Ok(None)
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
51
crates/tek_tui/src/tui_pool_view.rs
Normal file
51
crates/tek_tui/src/tui_pool_view.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
use crate::*;
|
||||
|
||||
impl Widget for TransportTui {
|
||||
type Engine = Tui;
|
||||
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
TransportView(&self, Default::default()).layout(to)
|
||||
}
|
||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||
TransportView(&self, Default::default()).render(to)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Display phrases always in order of appearance
|
||||
impl Content for PhrasePoolView<Tui> {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
let Self { focused, phrases, mode, .. } = self;
|
||||
let content = col!(
|
||||
(i, phrase) in phrases.iter().enumerate() => Layers::new(|add|{
|
||||
let Phrase { ref name, color, length, .. } = *phrase.read().unwrap();
|
||||
let mut length = PhraseLength::new(length, None);
|
||||
if let Some(PhrasePoolMode::Length(phrase, new_length, focus)) = mode {
|
||||
if *focused && i == *phrase {
|
||||
length.pulses = *new_length;
|
||||
length.focus = Some(*focus);
|
||||
}
|
||||
}
|
||||
let length = length.align_e().fill_x();
|
||||
let row1 = lay!(format!(" {i}").align_w().fill_x(), length).fill_x();
|
||||
let mut row2 = format!(" {name}");
|
||||
if let Some(PhrasePoolMode::Rename(phrase, _)) = mode {
|
||||
if *focused && i == *phrase { row2 = format!("{row2}▄"); }
|
||||
};
|
||||
let row2 = TuiStyle::bold(row2, true);
|
||||
add(&col!(row1, row2).fill_x().bg(color.base.rgb))?;
|
||||
Ok(if *focused && i == self.phrase { add(&CORNERS)?; })
|
||||
})
|
||||
);
|
||||
let border_color = if *focused {Color::Rgb(100, 110, 40)} else {Color::Rgb(70, 80, 50)};
|
||||
let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color));
|
||||
let content = content.fill_xy().bg(Color::Rgb(28, 35, 25)).border(border);
|
||||
let title_color = if *focused {Color::Rgb(150, 160, 90)} else {Color::Rgb(120, 130, 100)};
|
||||
let upper_left = format!("[{}] Phrases", if self.entered {"■"} else {" "});
|
||||
let upper_right = format!("({})", phrases.len());
|
||||
lay!(
|
||||
content,
|
||||
TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(),
|
||||
TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,6 @@
|
|||
use crate::*;
|
||||
|
||||
pub type SequencerApp<E: Engine> = AppView<
|
||||
E,
|
||||
SequencerView<E>,
|
||||
SequencerViewCommand,
|
||||
SequencerStatusBar,
|
||||
>;
|
||||
|
||||
impl TryFrom<&Arc<RwLock<JackClient>>> for SequencerApp<Tui> {
|
||||
impl TryFrom<&Arc<RwLock<JackClient>>> for SequencerTui {
|
||||
type Error = Box<dyn std::error::Error>;
|
||||
fn try_from (jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
|
||||
let clock = Arc::new(Clock::from(Instant::default()));
|
||||
|
|
@ -25,14 +18,8 @@ impl TryFrom<&Arc<RwLock<JackClient>>> for SequencerApp<Tui> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Handle<Tui> for SequencerApp<Tui> {
|
||||
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
|
||||
SequencerCommand::execute_with_state(self, i)
|
||||
}
|
||||
}
|
||||
|
||||
/// Root view for standalone `tek_sequencer`.
|
||||
pub struct SequencerView<E: Engine> {
|
||||
pub struct SequencerTui {
|
||||
jack: Arc<RwLock<JackClient>>,
|
||||
playing: RwLock<Option<TransportState>>,
|
||||
started: RwLock<Option<(usize, usize)>>,
|
||||
|
|
@ -43,7 +30,6 @@ pub struct SequencerView<E: Engine> {
|
|||
metronome: bool,
|
||||
phrases: Vec<Arc<RwLock<Phrase>>>,
|
||||
view_phrase: usize,
|
||||
editor: PhraseEditor<E>,
|
||||
split: u16,
|
||||
/// Start time and phrase being played
|
||||
play_phrase: Option<(Instant, Option<Arc<RwLock<Phrase>>>)>,
|
||||
|
|
@ -90,7 +76,7 @@ pub enum SequencerStatusBar {
|
|||
PhraseEditor,
|
||||
}
|
||||
|
||||
impl Content for SequencerView<Tui> {
|
||||
impl Content for SequencerTui {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
col!(
|
||||
|
|
@ -103,7 +89,7 @@ impl Content for SequencerView<Tui> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> Audio for SequencerView<E> {
|
||||
impl Audio for SequencerTui {
|
||||
fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||
self.model.process(client, scope)
|
||||
}
|
||||
|
|
@ -127,11 +113,11 @@ impl Content for SequencerStatusBar {
|
|||
}
|
||||
}
|
||||
|
||||
impl HasFocus for SequencerApp<Tui> {
|
||||
impl HasFocus for SequencerTui {
|
||||
type Item = AppViewFocus<SequencerFocus>;
|
||||
}
|
||||
|
||||
impl FocusEnter for SequencerApp<Tui> {
|
||||
impl FocusEnter for SequencerTui {
|
||||
fn focus_enter (&mut self) {
|
||||
let focused = self.focused();
|
||||
if !self.entered {
|
||||
|
|
@ -154,7 +140,7 @@ impl FocusEnter for SequencerApp<Tui> {
|
|||
}
|
||||
}
|
||||
|
||||
impl FocusGrid for SequencerApp<Tui> {
|
||||
impl FocusGrid for SequencerTui {
|
||||
type Item = AppViewFocus<SequencerFocus>;
|
||||
fn focus_cursor (&self) -> (usize, usize) {
|
||||
self.cursor
|
||||
|
|
@ -176,13 +162,13 @@ impl FocusGrid for SequencerApp<Tui> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> HasJack for SequencerView<E> {
|
||||
impl JackApi for SequencerTui {
|
||||
fn jack (&self) -> &Arc<RwLock<JackClient>> {
|
||||
&self.jack
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> HasPhrases for SequencerView<E> {
|
||||
impl HasPhrases for SequencerTui {
|
||||
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>> {
|
||||
&self.phrases
|
||||
}
|
||||
|
|
@ -191,7 +177,7 @@ impl<E: Engine> HasPhrases for SequencerView<E> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> HasPhrase for SequencerView<E> {
|
||||
impl HasPhrase for SequencerTui {
|
||||
fn reset (&self) -> bool {
|
||||
self.reset
|
||||
}
|
||||
|
|
@ -212,7 +198,7 @@ impl<E: Engine> HasPhrase for SequencerView<E> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> MidiInputApi for SequencerView<E> {
|
||||
impl MidiInputApi for SequencerTui {
|
||||
fn midi_ins(&self) -> &Vec<Port<jack::MidiIn>> {
|
||||
todo!()
|
||||
}
|
||||
|
|
@ -242,7 +228,7 @@ impl<E: Engine> MidiInputApi for SequencerView<E> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> MidiOutputApi for SequencerView<E> {
|
||||
impl MidiOutputApi for SequencerTui {
|
||||
fn midi_outs (&self) -> &Vec<Port<jack::MidiOut>> {
|
||||
todo!()
|
||||
}
|
||||
|
|
@ -257,7 +243,7 @@ impl<E: Engine> MidiOutputApi for SequencerView<E> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> ClockApi for SequencerView<E> {
|
||||
impl ClockApi for SequencerTui {
|
||||
fn timebase (&self) -> &Arc<Timebase> {
|
||||
todo!()
|
||||
}
|
||||
|
|
@ -269,7 +255,7 @@ impl<E: Engine> ClockApi for SequencerView<E> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> PlayheadApi for SequencerView<E> {
|
||||
impl PlayheadApi for SequencerTui {
|
||||
fn current(&self) -> &Instant {
|
||||
todo!()
|
||||
}
|
||||
|
|
@ -284,9 +270,9 @@ impl<E: Engine> PlayheadApi for SequencerView<E> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> PlayerApi for SequencerView<E> {}
|
||||
impl PlayerApi for SequencerTui {}
|
||||
|
||||
impl TransportViewState for SequencerView<Tui> {
|
||||
impl TransportViewState for SequencerTui {
|
||||
fn focus (&self) -> TransportViewFocus {
|
||||
self.focus
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,28 @@
|
|||
use crate::*;
|
||||
|
||||
pub type SequencerCommand = AppViewCommand<SequencerViewCommand>;
|
||||
impl Handle<Tui> for SequencerTui {
|
||||
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
|
||||
SequencerCommand::execute_with_state(self, i)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum SequencerViewCommand {
|
||||
Transport(TransportCommand),
|
||||
pub enum SequencerCommand {
|
||||
Focus(FocusCommand),
|
||||
Undo,
|
||||
Redo,
|
||||
Clear,
|
||||
Clock(ClockCommand),
|
||||
Playhead(PlayheadCommand),
|
||||
Phrases(PhrasePoolViewCommand),
|
||||
Editor(PhraseEditorCommand),
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, SequencerApp<Tui>> for SequencerCommand {
|
||||
fn input_to_command (state: &SequencerApp<Tui>, input: &TuiInput) -> Option<Self> {
|
||||
impl InputToCommand<Tui, SequencerTui> for SequencerCommand {
|
||||
fn input_to_command (state: &SequencerTui, input: &TuiInput) -> Option<Self> {
|
||||
use AppViewFocus::*;
|
||||
use FocusCommand::*;
|
||||
use SequencerViewCommand::*;
|
||||
use SequencerCommand::*;
|
||||
match input.event() {
|
||||
key!(KeyCode::Tab) => Some(Self::Focus(Next)),
|
||||
key!(Shift-KeyCode::Tab) => Some(Self::Focus(Prev)),
|
||||
|
|
@ -39,8 +48,8 @@ impl InputToCommand<Tui, SequencerApp<Tui>> for SequencerCommand {
|
|||
}
|
||||
}
|
||||
|
||||
impl Command<SequencerApp<Tui>> for SequencerCommand {
|
||||
fn execute (self, state: &mut SequencerApp<Tui>) -> Perhaps<Self> {
|
||||
impl Command<SequencerTui> for SequencerCommand {
|
||||
fn execute (self, state: &mut SequencerTui) -> Perhaps<Self> {
|
||||
use AppViewCommand::*;
|
||||
match self {
|
||||
Focus(cmd) => delegate(cmd, Focus, state),
|
||||
|
|
@ -49,9 +58,9 @@ impl Command<SequencerApp<Tui>> for SequencerCommand {
|
|||
}
|
||||
}
|
||||
|
||||
impl Command<SequencerApp<Tui>> for SequencerViewCommand {
|
||||
fn execute (self, state: &mut SequencerApp<Tui>) -> Perhaps<Self> {
|
||||
use SequencerViewCommand::*;
|
||||
impl Command<SequencerTui> for SequencerCommand {
|
||||
fn execute (self, state: &mut SequencerTui) -> Perhaps<Self> {
|
||||
use SequencerCommand::*;
|
||||
match self {
|
||||
Phrases(cmd) => delegate(cmd, Phrases, &mut state.phrases),
|
||||
Editor(cmd) => delegate(cmd, Editor, &mut state.editor),
|
||||
|
|
@ -60,7 +69,7 @@ impl Command<SequencerApp<Tui>> for SequencerViewCommand {
|
|||
}
|
||||
}
|
||||
|
||||
impl TransportControl for SequencerApp<Tui> {
|
||||
impl TransportControl for SequencerTui {
|
||||
fn bpm (&self) -> &BeatsPerMinute {
|
||||
self.app.bpm()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,7 @@
|
|||
use crate::*;
|
||||
|
||||
/// Root type of application.
|
||||
pub type TransportApp<E: Engine> = AppView<
|
||||
E,
|
||||
TransportView<E>,
|
||||
AppViewCommand<TransportCommand>,
|
||||
TransportStatusBar
|
||||
>;
|
||||
|
||||
/// Create app state from JACK handle.
|
||||
impl TryFrom<&Arc<RwLock<JackClient>>> for TransportApp<Tui> {
|
||||
type Error = Box<dyn std::error::Error>;
|
||||
fn try_from (jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
|
||||
Ok(Self::new(TransportView {
|
||||
metronome: false,
|
||||
transport: jack.read().unwrap().transport(),
|
||||
jack: jack.clone(),
|
||||
focused: false,
|
||||
focus: TransportViewFocus::PlayPause,
|
||||
size: Measure::new(),
|
||||
}.into(), None, None))
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores and displays time-related info.
|
||||
pub struct TransportView<E: Engine> {
|
||||
_engine: PhantomData<E>,
|
||||
pub struct TransportTui {
|
||||
jack: Arc<RwLock<JackClient>>,
|
||||
/// Playback state
|
||||
playing: RwLock<Option<TransportState>>,
|
||||
|
|
@ -46,7 +22,22 @@ pub struct TransportView<E: Engine> {
|
|||
size: Measure<E>,
|
||||
}
|
||||
|
||||
impl<E: Engine> std::fmt::Debug for TransportView<E> {
|
||||
/// Create app state from JACK handle.
|
||||
impl TryFrom<&Arc<RwLock<JackClient>>> for TransportTui {
|
||||
type Error = Box<dyn std::error::Error>;
|
||||
fn try_from (jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
|
||||
Ok(Self {
|
||||
metronome: false,
|
||||
transport: jack.read().unwrap().transport(),
|
||||
jack: jack.clone(),
|
||||
focused: false,
|
||||
focus: TransportViewFocus::PlayPause,
|
||||
size: Measure::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for TransportTui {
|
||||
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
f.debug_struct("transport")
|
||||
.field("jack", &self.jack)
|
||||
|
|
@ -54,233 +45,3 @@ impl<E: Engine> std::fmt::Debug for TransportView<E> {
|
|||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Which item of the transport toolbar is focused
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum TransportViewFocus {
|
||||
Bpm,
|
||||
Sync,
|
||||
PlayPause,
|
||||
Clock,
|
||||
Quant,
|
||||
}
|
||||
|
||||
impl TransportViewFocus {
|
||||
pub fn next (&mut self) {
|
||||
*self = match self {
|
||||
Self::PlayPause => Self::Bpm,
|
||||
Self::Bpm => Self::Quant,
|
||||
Self::Quant => Self::Sync,
|
||||
Self::Sync => Self::Clock,
|
||||
Self::Clock => Self::PlayPause,
|
||||
}
|
||||
}
|
||||
pub fn prev (&mut self) {
|
||||
*self = match self {
|
||||
Self::PlayPause => Self::Clock,
|
||||
Self::Bpm => Self::PlayPause,
|
||||
Self::Quant => Self::Bpm,
|
||||
Self::Sync => Self::Quant,
|
||||
Self::Clock => Self::Sync,
|
||||
}
|
||||
}
|
||||
pub fn wrap <'a, W: Widget<Engine = Tui>> (
|
||||
self, parent_focus: bool, focus: Self, widget: &'a W
|
||||
) -> impl Widget<Engine = Tui> + 'a {
|
||||
let focused = parent_focus && focus == self;
|
||||
let corners = focused.then_some(CORNERS);
|
||||
let highlight = focused.then_some(Background(Color::Rgb(60, 70, 50)));
|
||||
lay!(corners, highlight, *widget)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct TransportStatusBar;
|
||||
|
||||
impl Widget for TransportView<Tui> {
|
||||
type Engine = Tui;
|
||||
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
TransportRef(&self, Default::default()).layout(to)
|
||||
}
|
||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||
TransportRef(&self, Default::default()).render(to)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TransportRef<'a, T: TransportViewState>(pub &'a T);
|
||||
|
||||
pub trait TransportViewState: Send + Sync {
|
||||
fn focus (&self) -> TransportViewFocus;
|
||||
fn focused (&self) -> bool;
|
||||
fn transport_state (&self) -> Option<TransportState>;
|
||||
fn bpm_value (&self) -> f64;
|
||||
fn sync_value (&self) -> f64;
|
||||
fn format_beat (&self) -> String;
|
||||
fn format_msu (&self) -> String;
|
||||
}
|
||||
|
||||
impl<E: Engine> TransportViewState for TransportView<E> {
|
||||
fn focus (&self) -> TransportViewFocus {
|
||||
self.focus
|
||||
}
|
||||
fn focused (&self) -> bool {
|
||||
self.focused
|
||||
}
|
||||
fn transport_state (&self) -> Option<TransportState> {
|
||||
*self.playing().read().unwrap()
|
||||
}
|
||||
fn bpm_value (&self) -> f64 {
|
||||
self.bpm().get()
|
||||
}
|
||||
fn sync_value (&self) -> f64 {
|
||||
self.sync().get()
|
||||
}
|
||||
fn format_beat (&self) -> String {
|
||||
self.current().format_beat()
|
||||
}
|
||||
fn format_msu (&self) -> String {
|
||||
self.current().usec.format_msu()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: TransportViewState> Content for TransportRef<'a, T> {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
let state = self.0;
|
||||
lay!(
|
||||
state.focus().wrap(state.focused(), TransportViewFocus::PlayPause, &Styled(
|
||||
None,
|
||||
match state.transport_state() {
|
||||
Some(TransportState::Rolling) => "▶ PLAYING",
|
||||
Some(TransportState::Starting) => "READY ...",
|
||||
Some(TransportState::Stopped) => "⏹ STOPPED",
|
||||
_ => unreachable!(),
|
||||
}
|
||||
).min_xy(11, 2).push_x(1)).align_x().fill_x(),
|
||||
|
||||
row!(
|
||||
state.focus().wrap(state.focused(), TransportViewFocus::Bpm, &Outset::X(1u16, {
|
||||
let bpm = state.bpm_value();
|
||||
row! { "BPM ", format!("{}.{:03}", bpm as usize, (bpm * 1000.0) % 1000.0) }
|
||||
})),
|
||||
//let quant = state.focus().wrap(state.focused(), TransportViewFocus::Quant, &Outset::X(1u16, row! {
|
||||
//"QUANT ", ppq_to_name(state.0.quant as usize)
|
||||
//})),
|
||||
state.focus().wrap(state.focused(), TransportViewFocus::Sync, &Outset::X(1u16, row! {
|
||||
"SYNC ", pulses_to_name(state.sync_value() as usize)
|
||||
}))
|
||||
).align_w().fill_x(),
|
||||
|
||||
state.focus().wrap(state.focused(), TransportViewFocus::Clock, &{
|
||||
let time1 = state.format_beat();
|
||||
let time2 = state.format_msu();
|
||||
row!("B" ,time1.as_str(), " T", time2.as_str()).outset_x(1)
|
||||
}).align_e().fill_x(),
|
||||
|
||||
).fill_x().bg(Color::Rgb(40, 50, 30))
|
||||
}
|
||||
}
|
||||
|
||||
impl Audio for TransportView<Tui> {
|
||||
fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||
PlayheadAudio(self).process(client, scope)
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusBar for TransportStatusBar {
|
||||
type State = ();
|
||||
fn hotkey_fg () -> Color {
|
||||
TuiTheme::hotkey_fg()
|
||||
}
|
||||
fn update (&mut self, state: &()) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Content for TransportStatusBar {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
todo!();
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
impl HasFocus for TransportApp<Tui> {
|
||||
type Item = AppViewFocus<TransportViewFocus>;
|
||||
}
|
||||
|
||||
impl FocusEnter for TransportApp<Tui> {
|
||||
fn focus_enter (&mut self) {
|
||||
self.entered = true;
|
||||
}
|
||||
fn focus_exit (&mut self) {
|
||||
self.entered = false;
|
||||
}
|
||||
fn focus_entered (&self) -> Option<Self::Item> {
|
||||
if self.entered {
|
||||
Some(self.focused())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusGrid for TransportApp<Tui> {
|
||||
type Item = AppViewFocus<TransportViewFocus>;
|
||||
fn focus_cursor (&self) -> (usize, usize) {
|
||||
self.cursor
|
||||
}
|
||||
fn focus_cursor_mut (&mut self) -> &mut (usize, usize) {
|
||||
&mut self.cursor
|
||||
}
|
||||
fn focus_layout (&self) -> &[&[Self::Item]] {
|
||||
use AppViewFocus::*;
|
||||
use TransportViewFocus::*;
|
||||
&[
|
||||
&[Menu],
|
||||
&[
|
||||
Content(Bpm),
|
||||
Content(Sync),
|
||||
Content(PlayPause),
|
||||
Content(Clock),
|
||||
Content(Quant),
|
||||
],
|
||||
]
|
||||
}
|
||||
fn focus_update (&mut self) {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> HasJack for TransportView<E> {
|
||||
fn jack (&self) -> &Arc<RwLock<JackClient>> {
|
||||
&self.jack
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> ClockApi for TransportView<E> {
|
||||
fn timebase (&self) -> &Arc<Timebase> {
|
||||
&self.current.timebase
|
||||
}
|
||||
fn quant (&self) -> &Quantize {
|
||||
&self.quant
|
||||
}
|
||||
fn sync (&self) -> &LaunchSync {
|
||||
&self.sync
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> PlayheadApi for TransportView<E> {
|
||||
fn current (&self) -> &Instant {
|
||||
&self.current
|
||||
}
|
||||
fn transport (&self) -> &jack::Transport {
|
||||
&self.transport
|
||||
}
|
||||
fn playing (&self) -> &RwLock<Option<TransportState>> {
|
||||
&self.playing
|
||||
}
|
||||
fn started (&self) -> &RwLock<Option<(usize, usize)>> {
|
||||
&self.started
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use crate::*;
|
||||
|
||||
/// Handle input.
|
||||
impl Handle<Tui> for TransportApp<Tui> {
|
||||
impl Handle<Tui> for TransportTui {
|
||||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||
AppViewCommand::<TransportCommand>::execute_with_state(self, from)
|
||||
}
|
||||
|
|
@ -9,15 +9,16 @@ impl Handle<Tui> for TransportApp<Tui> {
|
|||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum TransportCommand {
|
||||
Focus(FocusCommand),
|
||||
Clock(ClockCommand),
|
||||
Playhead(PlayheadCommand),
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, TransportApp<Tui>> for AppViewCommand<TransportCommand> {
|
||||
fn input_to_command (app: &TransportApp<Tui>, input: &TuiInput) -> Option<Self> {
|
||||
impl InputToCommand<Tui, TransportTui> for AppViewCommand<TransportCommand> {
|
||||
fn input_to_command (app: &TransportTui, input: &TuiInput) -> Option<Self> {
|
||||
use KeyCode::{Left, Right};
|
||||
use FocusCommand::{Prev, Next};
|
||||
use AppViewCommand::{Focus, App};
|
||||
use TransportCommand::{Focus, Clock, Playhead};
|
||||
Some(match input.event() {
|
||||
key!(Left) => Focus(Prev),
|
||||
key!(Right) => Focus(Next),
|
||||
|
|
@ -26,36 +27,6 @@ impl InputToCommand<Tui, TransportApp<Tui>> for AppViewCommand<TransportCommand>
|
|||
}
|
||||
}
|
||||
|
||||
pub trait TransportControl: FocusGrid<Item = AppViewFocus<TransportViewFocus>> {
|
||||
fn quant (&self) -> &Quantize;
|
||||
fn bpm (&self) -> &BeatsPerMinute;
|
||||
fn next_quant (&self) -> f64 {
|
||||
next_note_length(self.quant().get() as usize) as f64
|
||||
}
|
||||
fn prev_quant (&self) -> f64 {
|
||||
prev_note_length(self.quant().get() as usize) as f64
|
||||
}
|
||||
fn sync (&self) -> &LaunchSync;
|
||||
fn next_sync (&self) -> f64 {
|
||||
next_note_length(self.sync().get() as usize) as f64
|
||||
}
|
||||
fn prev_sync (&self) -> f64 {
|
||||
prev_note_length(self.sync().get() as usize) as f64
|
||||
}
|
||||
}
|
||||
|
||||
impl TransportControl for TransportApp<Tui> {
|
||||
fn bpm (&self) -> &BeatsPerMinute {
|
||||
self.app.bpm()
|
||||
}
|
||||
fn quant (&self) -> &Quantize {
|
||||
self.app.quant()
|
||||
}
|
||||
fn sync (&self) -> &LaunchSync {
|
||||
self.app.sync()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: TransportControl> InputToCommand<Tui, T> for TransportCommand {
|
||||
fn input_to_command (state: &T, input: &TuiInput) -> Option<Self> {
|
||||
use KeyCode::Char;
|
||||
|
|
@ -102,8 +73,38 @@ impl<T: TransportControl> InputToCommand<Tui, T> for TransportCommand {
|
|||
}
|
||||
}
|
||||
|
||||
impl Command<TransportApp<Tui>> for AppViewCommand<TransportCommand> {
|
||||
fn execute (self, state: &mut TransportApp<Tui>) -> Perhaps<Self> {
|
||||
pub trait TransportControl: FocusGrid<Item = AppViewFocus<TransportViewFocus>> {
|
||||
fn quant (&self) -> &Quantize;
|
||||
fn bpm (&self) -> &BeatsPerMinute;
|
||||
fn next_quant (&self) -> f64 {
|
||||
next_note_length(self.quant().get() as usize) as f64
|
||||
}
|
||||
fn prev_quant (&self) -> f64 {
|
||||
prev_note_length(self.quant().get() as usize) as f64
|
||||
}
|
||||
fn sync (&self) -> &LaunchSync;
|
||||
fn next_sync (&self) -> f64 {
|
||||
next_note_length(self.sync().get() as usize) as f64
|
||||
}
|
||||
fn prev_sync (&self) -> f64 {
|
||||
prev_note_length(self.sync().get() as usize) as f64
|
||||
}
|
||||
}
|
||||
|
||||
impl TransportControl for TransportTui {
|
||||
fn bpm (&self) -> &BeatsPerMinute {
|
||||
self.bpm()
|
||||
}
|
||||
fn quant (&self) -> &Quantize {
|
||||
self.quant()
|
||||
}
|
||||
fn sync (&self) -> &LaunchSync {
|
||||
self.sync()
|
||||
}
|
||||
}
|
||||
|
||||
impl Command<TransportTui> for AppViewCommand<TransportCommand> {
|
||||
fn execute (self, state: &mut TransportTui) -> Perhaps<Self> {
|
||||
use AppViewCommand::{Focus, App};
|
||||
use FocusCommand::{Next, Prev};
|
||||
Ok(Some(match self {
|
||||
|
|
|
|||
87
crates/tek_tui/src/tui_transport_focus.rs
Normal file
87
crates/tek_tui/src/tui_transport_focus.rs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
use crate::*;
|
||||
|
||||
/// Which item of the transport toolbar is focused
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum TransportViewFocus {
|
||||
Bpm,
|
||||
Sync,
|
||||
PlayPause,
|
||||
Clock,
|
||||
Quant,
|
||||
}
|
||||
|
||||
impl TransportViewFocus {
|
||||
pub fn next (&mut self) {
|
||||
*self = match self {
|
||||
Self::PlayPause => Self::Bpm,
|
||||
Self::Bpm => Self::Quant,
|
||||
Self::Quant => Self::Sync,
|
||||
Self::Sync => Self::Clock,
|
||||
Self::Clock => Self::PlayPause,
|
||||
}
|
||||
}
|
||||
pub fn prev (&mut self) {
|
||||
*self = match self {
|
||||
Self::PlayPause => Self::Clock,
|
||||
Self::Bpm => Self::PlayPause,
|
||||
Self::Quant => Self::Bpm,
|
||||
Self::Sync => Self::Quant,
|
||||
Self::Clock => Self::Sync,
|
||||
}
|
||||
}
|
||||
pub fn wrap <'a, W: Widget<Engine = Tui>> (
|
||||
self, parent_focus: bool, focus: Self, widget: &'a W
|
||||
) -> impl Widget<Engine = Tui> + 'a {
|
||||
let focused = parent_focus && focus == self;
|
||||
let corners = focused.then_some(CORNERS);
|
||||
let highlight = focused.then_some(Background(Color::Rgb(60, 70, 50)));
|
||||
lay!(corners, highlight, *widget)
|
||||
}
|
||||
}
|
||||
|
||||
impl HasFocus for TransportTui {
|
||||
type Item = AppViewFocus<TransportViewFocus>;
|
||||
}
|
||||
|
||||
impl FocusEnter for TransportTui {
|
||||
fn focus_enter (&mut self) {
|
||||
self.entered = true;
|
||||
}
|
||||
fn focus_exit (&mut self) {
|
||||
self.entered = false;
|
||||
}
|
||||
fn focus_entered (&self) -> Option<Self::Item> {
|
||||
if self.entered {
|
||||
Some(self.focused())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusGrid for TransportTui {
|
||||
type Item = AppViewFocus<TransportViewFocus>;
|
||||
fn focus_cursor (&self) -> (usize, usize) {
|
||||
self.cursor
|
||||
}
|
||||
fn focus_cursor_mut (&mut self) -> &mut (usize, usize) {
|
||||
&mut self.cursor
|
||||
}
|
||||
fn focus_layout (&self) -> &[&[Self::Item]] {
|
||||
use AppViewFocus::*;
|
||||
use TransportViewFocus::*;
|
||||
&[
|
||||
&[Menu],
|
||||
&[
|
||||
Content(Bpm),
|
||||
Content(Sync),
|
||||
Content(PlayPause),
|
||||
Content(Clock),
|
||||
Content(Quant),
|
||||
],
|
||||
]
|
||||
}
|
||||
fn focus_update (&mut self) {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
40
crates/tek_tui/src/tui_transport_jack.rs
Normal file
40
crates/tek_tui/src/tui_transport_jack.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
use crate::*;
|
||||
|
||||
impl Audio for TransportTui {
|
||||
fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||
PlayheadAudio(self).process(client, scope)
|
||||
}
|
||||
}
|
||||
|
||||
impl JackApi for TransportTui {
|
||||
fn jack (&self) -> &Arc<RwLock<JackClient>> {
|
||||
&self.jack
|
||||
}
|
||||
}
|
||||
|
||||
impl ClockApi for TransportTui {
|
||||
fn timebase (&self) -> &Arc<Timebase> {
|
||||
&self.current.timebase
|
||||
}
|
||||
fn quant (&self) -> &Quantize {
|
||||
&self.quant
|
||||
}
|
||||
fn sync (&self) -> &LaunchSync {
|
||||
&self.sync
|
||||
}
|
||||
}
|
||||
|
||||
impl PlayheadApi for TransportTui {
|
||||
fn current (&self) -> &Instant {
|
||||
&self.current
|
||||
}
|
||||
fn transport (&self) -> &jack::Transport {
|
||||
&self.transport
|
||||
}
|
||||
fn playing (&self) -> &RwLock<Option<TransportState>> {
|
||||
&self.playing
|
||||
}
|
||||
fn started (&self) -> &RwLock<Option<(usize, usize)>> {
|
||||
&self.started
|
||||
}
|
||||
}
|
||||
22
crates/tek_tui/src/tui_transport_status.rs
Normal file
22
crates/tek_tui/src/tui_transport_status.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct TransportStatusBar;
|
||||
|
||||
impl StatusBar for TransportStatusBar {
|
||||
type State = ();
|
||||
fn hotkey_fg () -> Color {
|
||||
TuiTheme::hotkey_fg()
|
||||
}
|
||||
fn update (&mut self, state: &()) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Content for TransportStatusBar {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
todo!();
|
||||
""
|
||||
}
|
||||
}
|
||||
85
crates/tek_tui/src/tui_transport_view.rs
Normal file
85
crates/tek_tui/src/tui_transport_view.rs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
use crate::*;
|
||||
|
||||
impl Widget for TransportTui {
|
||||
type Engine = Tui;
|
||||
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
TransportView(&self, Default::default()).layout(to)
|
||||
}
|
||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||
TransportView(&self, Default::default()).render(to)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TransportView<'a, T: TransportViewState>(pub &'a T);
|
||||
|
||||
pub trait TransportViewState: Send + Sync {
|
||||
fn focus (&self) -> TransportViewFocus;
|
||||
fn focused (&self) -> bool;
|
||||
fn transport_state (&self) -> Option<TransportState>;
|
||||
fn bpm_value (&self) -> f64;
|
||||
fn sync_value (&self) -> f64;
|
||||
fn format_beat (&self) -> String;
|
||||
fn format_msu (&self) -> String;
|
||||
}
|
||||
|
||||
impl TransportViewState for TransportTui {
|
||||
fn focus (&self) -> TransportViewFocus {
|
||||
self.focus
|
||||
}
|
||||
fn focused (&self) -> bool {
|
||||
self.focused
|
||||
}
|
||||
fn transport_state (&self) -> Option<TransportState> {
|
||||
*self.playing().read().unwrap()
|
||||
}
|
||||
fn bpm_value (&self) -> f64 {
|
||||
self.bpm().get()
|
||||
}
|
||||
fn sync_value (&self) -> f64 {
|
||||
self.sync().get()
|
||||
}
|
||||
fn format_beat (&self) -> String {
|
||||
self.current().format_beat()
|
||||
}
|
||||
fn format_msu (&self) -> String {
|
||||
self.current().usec.format_msu()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: TransportViewState> Content for TransportView<'a, T> {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
let state = self.0;
|
||||
lay!(
|
||||
state.focus().wrap(state.focused(), TransportViewFocus::PlayPause, &Styled(
|
||||
None,
|
||||
match state.transport_state() {
|
||||
Some(TransportState::Rolling) => "▶ PLAYING",
|
||||
Some(TransportState::Starting) => "READY ...",
|
||||
Some(TransportState::Stopped) => "⏹ STOPPED",
|
||||
_ => unreachable!(),
|
||||
}
|
||||
).min_xy(11, 2).push_x(1)).align_x().fill_x(),
|
||||
|
||||
row!(
|
||||
state.focus().wrap(state.focused(), TransportViewFocus::Bpm, &Outset::X(1u16, {
|
||||
let bpm = state.bpm_value();
|
||||
row! { "BPM ", format!("{}.{:03}", bpm as usize, (bpm * 1000.0) % 1000.0) }
|
||||
})),
|
||||
//let quant = state.focus().wrap(state.focused(), TransportViewFocus::Quant, &Outset::X(1u16, row! {
|
||||
//"QUANT ", ppq_to_name(state.0.quant as usize)
|
||||
//})),
|
||||
state.focus().wrap(state.focused(), TransportViewFocus::Sync, &Outset::X(1u16, row! {
|
||||
"SYNC ", pulses_to_name(state.sync_value() as usize)
|
||||
}))
|
||||
).align_w().fill_x(),
|
||||
|
||||
state.focus().wrap(state.focused(), TransportViewFocus::Clock, &{
|
||||
let time1 = state.format_beat();
|
||||
let time2 = state.format_msu();
|
||||
row!("B" ,time1.as_str(), " T", time2.as_str()).outset_x(1)
|
||||
}).align_e().fill_x(),
|
||||
|
||||
).fill_x().bg(Color::Rgb(40, 50, 30))
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue