mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-09 13:16:44 +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::*;
|
use crate::*;
|
||||||
|
|
||||||
pub trait HasJack {
|
|
||||||
fn jack (&self) -> &impl JackApi;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait JackApi {
|
pub trait JackApi {
|
||||||
fn jack (&self) -> &Arc<RwLock<JackClient>>;
|
fn jack (&self) -> &Arc<RwLock<JackClient>>;
|
||||||
fn transport (&self) -> &RwLock<Option<TransportState>>;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
pub trait HasPlayer: HasJack {
|
pub trait HasPlayer: JackApi {
|
||||||
fn player (&self) -> &impl PlayerApi;
|
fn player (&self) -> &impl PlayerApi;
|
||||||
fn player_mut (&mut self) -> &mut impl PlayerApi;
|
fn player_mut (&mut self) -> &mut impl PlayerApi;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ submod! {
|
||||||
tui_arranger
|
tui_arranger
|
||||||
tui_arranger_cmd
|
tui_arranger_cmd
|
||||||
tui_arranger_focus
|
tui_arranger_focus
|
||||||
|
tui_arranger_jack
|
||||||
tui_arranger_scene
|
tui_arranger_scene
|
||||||
tui_arranger_select
|
tui_arranger_select
|
||||||
tui_arranger_status
|
tui_arranger_status
|
||||||
|
|
@ -29,6 +30,7 @@ submod! {
|
||||||
//tui_plugin_vst2
|
//tui_plugin_vst2
|
||||||
//tui_plugin_vst3
|
//tui_plugin_vst3
|
||||||
tui_pool
|
tui_pool
|
||||||
|
tui_pool_view
|
||||||
//tui_sampler // TODO
|
//tui_sampler // TODO
|
||||||
//tui_sampler_cmd
|
//tui_sampler_cmd
|
||||||
tui_sequencer
|
tui_sequencer
|
||||||
|
|
@ -37,61 +39,61 @@ submod! {
|
||||||
tui_theme
|
tui_theme
|
||||||
tui_transport
|
tui_transport
|
||||||
tui_transport_cmd
|
tui_transport_cmd
|
||||||
|
tui_transport_focus
|
||||||
|
tui_transport_jack
|
||||||
|
tui_transport_view
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AppView<E, A, C, S>
|
//pub struct AppView<E, A, C, S>
|
||||||
where
|
//where
|
||||||
E: Engine,
|
//E: Engine,
|
||||||
A: Widget<Engine = E> + Audio,
|
//A: Widget<Engine = E> + Audio,
|
||||||
C: Command<Self>,
|
//C: Command<Self>,
|
||||||
S: StatusBar,
|
//S: StatusBar,
|
||||||
{
|
//{
|
||||||
pub app: A,
|
//pub app: A,
|
||||||
pub cursor: (usize, usize),
|
//pub cursor: (usize, usize),
|
||||||
pub entered: bool,
|
//pub entered: bool,
|
||||||
pub menu_bar: Option<MenuBar<E, Self, C>>,
|
//pub menu_bar: Option<MenuBar<E, Self, C>>,
|
||||||
pub status_bar: Option<S>,
|
//pub status_bar: Option<S>,
|
||||||
pub history: Vec<C>,
|
//pub history: Vec<C>,
|
||||||
pub size: Measure<E>,
|
//pub size: Measure<E>,
|
||||||
}
|
//}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
//#[derive(Debug, Clone)]
|
||||||
pub enum AppViewCommand<T: Debug + Clone> {
|
//pub enum AppViewCommand<T: Debug + Clone> {
|
||||||
Focus(FocusCommand),
|
//App(T)
|
||||||
Undo,
|
//}
|
||||||
Redo,
|
|
||||||
App(T)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
//#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
pub enum AppViewFocus<F: Debug + Copy + Clone + PartialEq> {
|
//pub enum AppViewFocus<F: Debug + Copy + Clone + PartialEq> {
|
||||||
Menu,
|
//Menu,
|
||||||
Content(F),
|
//Content(F),
|
||||||
}
|
//}
|
||||||
|
|
||||||
impl<E, A, C, S> AppView<E, A, C, S>
|
//impl<E, A, C, S> AppView<E, A, C, S>
|
||||||
where
|
//where
|
||||||
E: Engine,
|
//E: Engine,
|
||||||
A: Widget<Engine = E> + Audio,
|
//A: Widget<Engine = E> + Audio,
|
||||||
C: Command<Self>,
|
//C: Command<Self>,
|
||||||
S: StatusBar
|
//S: StatusBar
|
||||||
{
|
//{
|
||||||
pub fn new (
|
//pub fn new (
|
||||||
app: A,
|
//app: A,
|
||||||
menu_bar: Option<MenuBar<E, Self, C>>,
|
//menu_bar: Option<MenuBar<E, Self, C>>,
|
||||||
status_bar: Option<S>,
|
//status_bar: Option<S>,
|
||||||
) -> Self {
|
//) -> Self {
|
||||||
Self {
|
//Self {
|
||||||
app,
|
//app,
|
||||||
cursor: (0, 0),
|
//cursor: (0, 0),
|
||||||
entered: false,
|
//entered: false,
|
||||||
history: vec![],
|
//history: vec![],
|
||||||
size: Measure::new(),
|
//size: Measure::new(),
|
||||||
menu_bar,
|
//menu_bar,
|
||||||
status_bar,
|
//status_bar,
|
||||||
}
|
//}
|
||||||
}
|
//}
|
||||||
}
|
//}
|
||||||
|
|
||||||
impl<A, C, S> Content for AppView<Tui, A, C, S>
|
impl<A, C, S> Content for AppView<Tui, A, C, S>
|
||||||
where
|
where
|
||||||
|
|
|
||||||
|
|
@ -1,111 +1,70 @@
|
||||||
use crate::*;
|
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>;
|
type Error = Box<dyn std::error::Error>;
|
||||||
fn try_from (jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
|
fn try_from (jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
|
||||||
Ok(Self::new(ArrangerView {
|
Ok(Self {
|
||||||
name: Arc::new(RwLock::new(String::new())),
|
name: Arc::new(RwLock::new(String::new())),
|
||||||
phrases: vec![],
|
phrases: vec![],
|
||||||
phrase: 0,
|
phrase: 0,
|
||||||
scenes: vec![],
|
scenes: vec![],
|
||||||
tracks: vec![],
|
tracks: vec![],
|
||||||
metronome: false,
|
metronome: false,
|
||||||
playing: None.into(),
|
playing: None.into(),
|
||||||
started: None.into(),
|
started: None.into(),
|
||||||
transport: jack.read().unwrap().transport(),
|
transport: jack.read().unwrap().transport(),
|
||||||
current: Instant::default(),
|
current: Instant::default(),
|
||||||
jack: jack.clone(),
|
jack: jack.clone(),
|
||||||
selected: ArrangerSelection::Clip(0, 0),
|
selected: ArrangerSelection::Clip(0, 0),
|
||||||
mode: ArrangerMode::Vertical(2),
|
mode: ArrangerMode::Vertical(2),
|
||||||
color: Color::Rgb(28, 35, 25).into(),
|
color: Color::Rgb(28, 35, 25).into(),
|
||||||
size: Measure::new(),
|
size: Measure::new(),
|
||||||
entered: false,
|
entered: false,
|
||||||
quant: Default::default(),
|
quant: Default::default(),
|
||||||
sync: Default::default(),
|
sync: Default::default(),
|
||||||
splits: [20, 20],
|
splits: [20, 20],
|
||||||
note_buf: vec![],
|
note_buf: vec![],
|
||||||
midi_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<
|
impl HasPhrases for ArrangerTui {
|
||||||
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> {
|
|
||||||
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>> {
|
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>> {
|
||||||
&self.phrases
|
&self.phrases
|
||||||
}
|
}
|
||||||
|
|
@ -115,7 +74,7 @@ impl HasPhrases for ArrangerView<Tui> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// General methods for arranger
|
/// General methods for arranger
|
||||||
impl ArrangerView<Tui> {
|
impl ArrangerTui {
|
||||||
pub fn selected_scene (&self) -> Option<&ArrangerScene> {
|
pub fn selected_scene (&self) -> Option<&ArrangerScene> {
|
||||||
self.selected.scene().map(|s|self.scenes().get(s)).flatten()
|
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) {
|
//pub fn track_next (&mut self, last_track: usize) {
|
||||||
//use ArrangerSelection::*;
|
//use ArrangerSelection::*;
|
||||||
//*self = match self {
|
//*self = match self {
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,34 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
/// Handle top-level events in standalone arranger.
|
/// 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> {
|
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)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum ArrangerViewCommand {
|
pub enum ArrangerCommand {
|
||||||
|
Focus(FocusCommand),
|
||||||
|
Undo,
|
||||||
|
Redo,
|
||||||
Clear,
|
Clear,
|
||||||
|
Clock(ClockCommand),
|
||||||
|
Playhead(PlayheadCommand),
|
||||||
Scene(ArrangerSceneCommand),
|
Scene(ArrangerSceneCommand),
|
||||||
Track(ArrangerTrackCommand),
|
Track(ArrangerTrackCommand),
|
||||||
Clip(ArrangerClipCommand),
|
Clip(ArrangerClipCommand),
|
||||||
Select(ArrangerSelection),
|
Select(ArrangerSelection),
|
||||||
Zoom(usize),
|
Zoom(usize),
|
||||||
Clock(ClockCommand),
|
|
||||||
Playhead(PlayheadCommand),
|
|
||||||
Phrases(PhrasePoolViewCommand),
|
Phrases(PhrasePoolViewCommand),
|
||||||
Editor(PhraseEditorCommand),
|
Editor(PhraseEditorCommand),
|
||||||
EditPhrase(Option<Arc<RwLock<Phrase>>>),
|
EditPhrase(Option<Arc<RwLock<Phrase>>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InputToCommand<Tui, ArrangerApp<Tui>> for ArrangerAppCommand {
|
impl InputToCommand<Tui, ArrangerTui> for ArrangerCommand {
|
||||||
fn input_to_command (view: &ArrangerApp<Tui>, input: &TuiInput) -> Option<Self> {
|
fn input_to_command (view: &ArrangerTui, input: &TuiInput) -> Option<Self> {
|
||||||
use AppViewFocus::*;
|
|
||||||
use FocusCommand::*;
|
use FocusCommand::*;
|
||||||
use ArrangerViewCommand::*;
|
use ArrangerCommand::*;
|
||||||
Some(match input.event() {
|
Some(match input.event() {
|
||||||
key!(KeyCode::Tab) => Self::Focus(Next),
|
key!(KeyCode::Tab) => Self::Focus(Next),
|
||||||
key!(Shift-KeyCode::Tab) => Self::Focus(Prev),
|
key!(Shift-KeyCode::Tab) => Self::Focus(Prev),
|
||||||
|
|
@ -56,11 +56,11 @@ impl InputToCommand<Tui, ArrangerApp<Tui>> for ArrangerAppCommand {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Content(ArrangerFocus::PhraseEditor) => Editor(
|
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() {
|
Content(ArrangerFocus::PhrasePool) => match input.event() {
|
||||||
key!(KeyCode::Char('e')) => EditPhrase(
|
key!(KeyCode::Char('e')) => EditPhrase(
|
||||||
Some(view.app.phrase().clone())
|
Some(view.phrase().clone())
|
||||||
),
|
),
|
||||||
_ => Phrases(
|
_ => Phrases(
|
||||||
PhrasePoolViewCommand::input_to_command(view, input)?
|
PhrasePoolViewCommand::input_to_command(view, input)?
|
||||||
|
|
@ -76,28 +76,28 @@ impl InputToCommand<Tui, ArrangerApp<Tui>> for ArrangerAppCommand {
|
||||||
_ => match input.event() {
|
_ => match input.event() {
|
||||||
// FIXME: boundary conditions
|
// FIXME: boundary conditions
|
||||||
|
|
||||||
key!(KeyCode::Up) => match view.app.selected {
|
key!(KeyCode::Up) => match view.selected {
|
||||||
Select::Mix => return None,
|
Select::Mix => return None,
|
||||||
Select::Track(t) => return None,
|
Select::Track(t) => return None,
|
||||||
Select::Scene(s) => Select(Select::Scene(s - 1)),
|
Select::Scene(s) => Select(Select::Scene(s - 1)),
|
||||||
Select::Clip(t, s) => Select(Select::Clip(t, 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::Mix => Select(Select::Scene(0)),
|
||||||
Select::Track(t) => Select(Select::Clip(t, 0)),
|
Select::Track(t) => Select(Select::Clip(t, 0)),
|
||||||
Select::Scene(s) => Select(Select::Scene(s + 1)),
|
Select::Scene(s) => Select(Select::Scene(s + 1)),
|
||||||
Select::Clip(t, s) => Select(Select::Clip(t, 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::Mix => return None,
|
||||||
Select::Track(t) => Select(Select::Track(t - 1)),
|
Select::Track(t) => Select(Select::Track(t - 1)),
|
||||||
Select::Scene(s) => return None,
|
Select::Scene(s) => return None,
|
||||||
Select::Clip(t, s) => Select(Select::Clip(t - 1, s)),
|
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::Mix => return None,
|
||||||
Select::Track(t) => Select(Select::Track(t + 1)),
|
Select::Track(t) => Select(Select::Track(t + 1)),
|
||||||
Select::Scene(s) => Select(Select::Clip(0, s)),
|
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('`')) => { todo!("toggle view mode") },
|
||||||
|
|
||||||
key!(KeyCode::Char(',')) => match view.app.selected {
|
key!(KeyCode::Char(',')) => match view.selected {
|
||||||
Select::Mix => Zoom(0),
|
Select::Mix => Zoom(0),
|
||||||
Select::Track(t) => Track(Track::Swap(t, t - 1)),
|
Select::Track(t) => Track(Track::Swap(t, t - 1)),
|
||||||
Select::Scene(s) => Scene(Scene::Swap(s, s - 1)),
|
Select::Scene(s) => Scene(Scene::Swap(s, s - 1)),
|
||||||
Select::Clip(t, s) => Clip(Clip::Set(t, s, None)),
|
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::Mix => Zoom(0),
|
||||||
Select::Track(t) => Track(Track::Swap(t, t + 1)),
|
Select::Track(t) => Track(Track::Swap(t, t + 1)),
|
||||||
Select::Scene(s) => Scene(Scene::Swap(s, s + 1)),
|
Select::Scene(s) => Scene(Scene::Swap(s, s + 1)),
|
||||||
Select::Clip(t, s) => Clip(Clip::Set(t, s, None)),
|
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::Mix => Zoom(0),
|
||||||
Select::Track(t) => Track(Track::Swap(t, t - 1)),
|
Select::Track(t) => Track(Track::Swap(t, t - 1)),
|
||||||
Select::Scene(s) => Scene(Scene::Swap(s, s - 1)),
|
Select::Scene(s) => Scene(Scene::Swap(s, s - 1)),
|
||||||
Select::Clip(t, s) => Clip(Clip::Set(t, s, None)),
|
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::Mix => Zoom(0),
|
||||||
Select::Track(t) => Track(Track::Swap(t, t + 1)),
|
Select::Track(t) => Track(Track::Swap(t, t + 1)),
|
||||||
Select::Scene(s) => Scene(Scene::Swap(s, s + 1)),
|
Select::Scene(s) => Scene(Scene::Swap(s, s + 1)),
|
||||||
Select::Clip(t, s) => Clip(Clip::Set(t, s, None)),
|
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::Mix => return None,
|
||||||
Select::Track(t) => return None,
|
Select::Track(t) => return None,
|
||||||
Select::Scene(s) => Scene(Scene::Play(s)),
|
Select::Scene(s) => Scene(Scene::Play(s)),
|
||||||
Select::Clip(t, s) => return None,
|
Select::Clip(t, s) => return None,
|
||||||
},
|
},
|
||||||
|
|
||||||
key!(KeyCode::Delete) => match view.app.selected {
|
key!(KeyCode::Delete) => match view.selected {
|
||||||
Select::Mix => Clear,
|
Select::Mix => Clear,
|
||||||
Select::Track(t) => Track(Track::Delete(t)),
|
Select::Track(t) => Track(Track::Delete(t)),
|
||||||
Select::Scene(s) => Scene(Scene::Delete(s)),
|
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('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)),
|
Select::Clip(t, s) => Clip(Clip::Set(t, s, None)),
|
||||||
_ => return 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)),
|
Select::Clip(t, s) => Clip(Clip::Get(t, s)),
|
||||||
_ => return None,
|
_ => return None,
|
||||||
},
|
},
|
||||||
|
|
@ -183,9 +183,8 @@ impl InputToCommand<Tui, ArrangerApp<Tui>> for ArrangerAppCommand {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Command<ArrangerApp<Tui>> for ArrangerAppCommand {
|
impl Command<ArrangerTui> for ArrangerCommand {
|
||||||
fn execute (self, state: &mut ArrangerApp<Tui>) -> Perhaps<Self> {
|
fn execute (self, state: &mut ArrangerTui) -> Perhaps<Self> {
|
||||||
use AppViewCommand::*;
|
|
||||||
let undo = match self {
|
let undo = match self {
|
||||||
Focus(cmd) => { delegate(cmd, Focus, state) },
|
Focus(cmd) => { delegate(cmd, Focus, state) },
|
||||||
App(cmd) => { delegate(cmd, App, state) }
|
App(cmd) => { delegate(cmd, App, state) }
|
||||||
|
|
@ -197,17 +196,17 @@ impl Command<ArrangerApp<Tui>> for ArrangerAppCommand {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Command<ArrangerApp<Tui>> for ArrangerViewCommand {
|
impl Command<ArrangerTui> for ArrangerCommand {
|
||||||
fn execute (self, state: &mut ArrangerApp<Tui>) -> Perhaps<Self> {
|
fn execute (self, state: &mut ArrangerTui) -> Perhaps<Self> {
|
||||||
use ArrangerViewCommand::*;
|
use ArrangerCommand::*;
|
||||||
match self {
|
match self {
|
||||||
Scene(cmd) => { delegate(cmd, Scene, &mut state.app) },
|
Scene(cmd) => { delegate(cmd, Scene, &mut state) },
|
||||||
Track(cmd) => { delegate(cmd, Track, &mut state.app) },
|
Track(cmd) => { delegate(cmd, Track, &mut state) },
|
||||||
Clip(cmd) => { delegate(cmd, Clip, &mut state.app) },
|
Clip(cmd) => { delegate(cmd, Clip, &mut state) },
|
||||||
Phrases(cmd) => { delegate(cmd, Phrases, &mut state.app) },
|
Phrases(cmd) => { delegate(cmd, Phrases, &mut state) },
|
||||||
Editor(cmd) => { delegate(cmd, Editor, &mut state.app) },
|
Editor(cmd) => { delegate(cmd, Editor, &mut state) },
|
||||||
Clock(cmd) => { delegate(cmd, Clock, &mut state.app) },
|
Clock(cmd) => { delegate(cmd, Clock, &mut state) },
|
||||||
Playhead(cmd) => { delegate(cmd, Playhead, &mut state.app) },
|
Playhead(cmd) => { delegate(cmd, Playhead, &mut state) },
|
||||||
Zoom(zoom) => { todo!(); },
|
Zoom(zoom) => { todo!(); },
|
||||||
Select(selected) => { state.selected = selected; Ok(None) },
|
Select(selected) => { state.selected = selected; Ok(None) },
|
||||||
EditPhrase(phrase) => {
|
EditPhrase(phrase) => {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ pub enum ArrangerFocus {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FocusEnter for ArrangerApp<Tui> {
|
impl FocusEnter for ArrangerApp<Tui> {
|
||||||
|
type Item = AppViewFocus<ArrangerFocus>;
|
||||||
fn focus_enter (&mut self) {
|
fn focus_enter (&mut self) {
|
||||||
use AppViewFocus::*;
|
use AppViewFocus::*;
|
||||||
use ArrangerFocus::*;
|
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> {
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||||
Split::up(
|
Split::up(
|
||||||
1,
|
1,
|
||||||
widget(&TransportRef(self)),
|
widget(&TransportView(self)),
|
||||||
Split::down(
|
Split::down(
|
||||||
self.splits[0],
|
self.splits[0],
|
||||||
lay!(
|
lay!(
|
||||||
|
|
@ -45,20 +45,20 @@ impl Content for ArrangerView<Tui> {
|
||||||
.grow_y(1)
|
.grow_y(1)
|
||||||
.border(Lozenge(Style::default()
|
.border(Lozenge(Style::default()
|
||||||
.bg(TuiTheme::border_bg())
|
.bg(TuiTheme::border_bg())
|
||||||
.fg(TuiTheme::border_fg(self.focused)))),
|
.fg(TuiTheme::border_fg(self.focused() == ArrangerFocus::Arranger)))),
|
||||||
widget(&self.size),
|
widget(&self.size),
|
||||||
widget(&format!("[{}] Arranger", if self.entered {
|
widget(&format!("[{}] Arranger", if self.entered {
|
||||||
"■"
|
"■"
|
||||||
} else {
|
} else {
|
||||||
" "
|
" "
|
||||||
}))
|
}))
|
||||||
.fg(TuiTheme::title_fg(self.focused))
|
.fg(TuiTheme::title_fg(self.focused() == ArrangerFocus::Arranger))
|
||||||
.push_x(1),
|
.push_x(1),
|
||||||
),
|
),
|
||||||
Split::right(
|
Split::right(
|
||||||
self.splits[1],
|
self.splits[1],
|
||||||
widget(&self.phrases),
|
widget(&self.phrases),
|
||||||
widget(&PhraseEditorRef(self)),
|
widget(&PhraseView(self)),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -34,14 +34,14 @@ pub struct PhraseEditor<E: Engine> {
|
||||||
impl Widget for PhraseEditor<Tui> {
|
impl Widget for PhraseEditor<Tui> {
|
||||||
type Engine = Tui;
|
type Engine = Tui;
|
||||||
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
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<()> {
|
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 {
|
pub trait PhraseEditorViewState: Send + Sync {
|
||||||
fn focused (&self) -> bool;
|
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;
|
type Engine = Tui;
|
||||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||||
let phrase = self.0.phrase();
|
let phrase = self.0.phrase();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
pub struct PhrasePoolView<E: Engine> {
|
pub struct PhrasesTui {
|
||||||
_engine: PhantomData<E>,
|
|
||||||
/// Collection of phrases
|
/// Collection of phrases
|
||||||
pub phrases: Vec<Arc<RwLock<Phrase>>>,
|
pub phrases: Vec<Arc<RwLock<Phrase>>>,
|
||||||
/// Selected phrase
|
/// Selected phrase
|
||||||
|
|
@ -24,10 +23,9 @@ pub enum PhrasePoolMode {
|
||||||
Length(usize, usize, PhraseLengthFocus),
|
Length(usize, usize, PhraseLengthFocus),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E: Engine> PhrasePoolView<E> {
|
impl PhrasesTui {
|
||||||
pub fn new (phrases: Vec<Arc<RwLock<Phrase>>>) -> Self {
|
pub fn new (phrases: Vec<Arc<RwLock<Phrase>>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
_engine: Default::default(),
|
|
||||||
scroll: 0,
|
scroll: 0,
|
||||||
phrase: 0,
|
phrase: 0,
|
||||||
mode: None,
|
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.
|
/// Displays and edits phrase length.
|
||||||
pub struct PhraseLength<E: Engine> {
|
pub struct PhraseLength {
|
||||||
_engine: PhantomData<E>,
|
|
||||||
/// Pulses per beat (quaver)
|
/// Pulses per beat (quaver)
|
||||||
pub ppq: usize,
|
pub ppq: usize,
|
||||||
/// Beats per bar
|
/// Beats per bar
|
||||||
|
|
@ -331,7 +127,7 @@ impl<E: Engine> PhraseLength<E> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Content for PhraseLength<Tui> {
|
impl Content for PhraseLength {
|
||||||
type Engine = Tui;
|
type Engine = Tui;
|
||||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||||
Layers::new(move|add|{
|
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::*;
|
use crate::*;
|
||||||
|
|
||||||
pub type SequencerApp<E: Engine> = AppView<
|
impl TryFrom<&Arc<RwLock<JackClient>>> for SequencerTui {
|
||||||
E,
|
|
||||||
SequencerView<E>,
|
|
||||||
SequencerViewCommand,
|
|
||||||
SequencerStatusBar,
|
|
||||||
>;
|
|
||||||
|
|
||||||
impl TryFrom<&Arc<RwLock<JackClient>>> for SequencerApp<Tui> {
|
|
||||||
type Error = Box<dyn std::error::Error>;
|
type Error = Box<dyn std::error::Error>;
|
||||||
fn try_from (jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
|
fn try_from (jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
|
||||||
let clock = Arc::new(Clock::from(Instant::default()));
|
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`.
|
/// Root view for standalone `tek_sequencer`.
|
||||||
pub struct SequencerView<E: Engine> {
|
pub struct SequencerTui {
|
||||||
jack: Arc<RwLock<JackClient>>,
|
jack: Arc<RwLock<JackClient>>,
|
||||||
playing: RwLock<Option<TransportState>>,
|
playing: RwLock<Option<TransportState>>,
|
||||||
started: RwLock<Option<(usize, usize)>>,
|
started: RwLock<Option<(usize, usize)>>,
|
||||||
|
|
@ -43,7 +30,6 @@ pub struct SequencerView<E: Engine> {
|
||||||
metronome: bool,
|
metronome: bool,
|
||||||
phrases: Vec<Arc<RwLock<Phrase>>>,
|
phrases: Vec<Arc<RwLock<Phrase>>>,
|
||||||
view_phrase: usize,
|
view_phrase: usize,
|
||||||
editor: PhraseEditor<E>,
|
|
||||||
split: u16,
|
split: u16,
|
||||||
/// Start time and phrase being played
|
/// Start time and phrase being played
|
||||||
play_phrase: Option<(Instant, Option<Arc<RwLock<Phrase>>>)>,
|
play_phrase: Option<(Instant, Option<Arc<RwLock<Phrase>>>)>,
|
||||||
|
|
@ -90,7 +76,7 @@ pub enum SequencerStatusBar {
|
||||||
PhraseEditor,
|
PhraseEditor,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Content for SequencerView<Tui> {
|
impl Content for SequencerTui {
|
||||||
type Engine = Tui;
|
type Engine = Tui;
|
||||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||||
col!(
|
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 {
|
fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||||
self.model.process(client, scope)
|
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>;
|
type Item = AppViewFocus<SequencerFocus>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FocusEnter for SequencerApp<Tui> {
|
impl FocusEnter for SequencerTui {
|
||||||
fn focus_enter (&mut self) {
|
fn focus_enter (&mut self) {
|
||||||
let focused = self.focused();
|
let focused = self.focused();
|
||||||
if !self.entered {
|
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>;
|
type Item = AppViewFocus<SequencerFocus>;
|
||||||
fn focus_cursor (&self) -> (usize, usize) {
|
fn focus_cursor (&self) -> (usize, usize) {
|
||||||
self.cursor
|
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>> {
|
fn jack (&self) -> &Arc<RwLock<JackClient>> {
|
||||||
&self.jack
|
&self.jack
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E: Engine> HasPhrases for SequencerView<E> {
|
impl HasPhrases for SequencerTui {
|
||||||
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>> {
|
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>> {
|
||||||
&self.phrases
|
&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 {
|
fn reset (&self) -> bool {
|
||||||
self.reset
|
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>> {
|
fn midi_ins(&self) -> &Vec<Port<jack::MidiIn>> {
|
||||||
todo!()
|
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>> {
|
fn midi_outs (&self) -> &Vec<Port<jack::MidiOut>> {
|
||||||
todo!()
|
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> {
|
fn timebase (&self) -> &Arc<Timebase> {
|
||||||
todo!()
|
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 {
|
fn current(&self) -> &Instant {
|
||||||
todo!()
|
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 {
|
fn focus (&self) -> TransportViewFocus {
|
||||||
self.focus
|
self.focus
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,28 @@
|
||||||
use crate::*;
|
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)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub enum SequencerViewCommand {
|
pub enum SequencerCommand {
|
||||||
Transport(TransportCommand),
|
Focus(FocusCommand),
|
||||||
|
Undo,
|
||||||
|
Redo,
|
||||||
|
Clear,
|
||||||
|
Clock(ClockCommand),
|
||||||
|
Playhead(PlayheadCommand),
|
||||||
Phrases(PhrasePoolViewCommand),
|
Phrases(PhrasePoolViewCommand),
|
||||||
Editor(PhraseEditorCommand),
|
Editor(PhraseEditorCommand),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InputToCommand<Tui, SequencerApp<Tui>> for SequencerCommand {
|
impl InputToCommand<Tui, SequencerTui> for SequencerCommand {
|
||||||
fn input_to_command (state: &SequencerApp<Tui>, input: &TuiInput) -> Option<Self> {
|
fn input_to_command (state: &SequencerTui, input: &TuiInput) -> Option<Self> {
|
||||||
use AppViewFocus::*;
|
use AppViewFocus::*;
|
||||||
use FocusCommand::*;
|
use FocusCommand::*;
|
||||||
use SequencerViewCommand::*;
|
use SequencerCommand::*;
|
||||||
match input.event() {
|
match input.event() {
|
||||||
key!(KeyCode::Tab) => Some(Self::Focus(Next)),
|
key!(KeyCode::Tab) => Some(Self::Focus(Next)),
|
||||||
key!(Shift-KeyCode::Tab) => Some(Self::Focus(Prev)),
|
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 {
|
impl Command<SequencerTui> for SequencerCommand {
|
||||||
fn execute (self, state: &mut SequencerApp<Tui>) -> Perhaps<Self> {
|
fn execute (self, state: &mut SequencerTui) -> Perhaps<Self> {
|
||||||
use AppViewCommand::*;
|
use AppViewCommand::*;
|
||||||
match self {
|
match self {
|
||||||
Focus(cmd) => delegate(cmd, Focus, state),
|
Focus(cmd) => delegate(cmd, Focus, state),
|
||||||
|
|
@ -49,9 +58,9 @@ impl Command<SequencerApp<Tui>> for SequencerCommand {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Command<SequencerApp<Tui>> for SequencerViewCommand {
|
impl Command<SequencerTui> for SequencerCommand {
|
||||||
fn execute (self, state: &mut SequencerApp<Tui>) -> Perhaps<Self> {
|
fn execute (self, state: &mut SequencerTui) -> Perhaps<Self> {
|
||||||
use SequencerViewCommand::*;
|
use SequencerCommand::*;
|
||||||
match self {
|
match self {
|
||||||
Phrases(cmd) => delegate(cmd, Phrases, &mut state.phrases),
|
Phrases(cmd) => delegate(cmd, Phrases, &mut state.phrases),
|
||||||
Editor(cmd) => delegate(cmd, Editor, &mut state.editor),
|
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 {
|
fn bpm (&self) -> &BeatsPerMinute {
|
||||||
self.app.bpm()
|
self.app.bpm()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,7 @@
|
||||||
use crate::*;
|
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.
|
/// Stores and displays time-related info.
|
||||||
pub struct TransportView<E: Engine> {
|
pub struct TransportTui {
|
||||||
_engine: PhantomData<E>,
|
|
||||||
jack: Arc<RwLock<JackClient>>,
|
jack: Arc<RwLock<JackClient>>,
|
||||||
/// Playback state
|
/// Playback state
|
||||||
playing: RwLock<Option<TransportState>>,
|
playing: RwLock<Option<TransportState>>,
|
||||||
|
|
@ -46,7 +22,22 @@ pub struct TransportView<E: Engine> {
|
||||||
size: Measure<E>,
|
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> {
|
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||||
f.debug_struct("transport")
|
f.debug_struct("transport")
|
||||||
.field("jack", &self.jack)
|
.field("jack", &self.jack)
|
||||||
|
|
@ -54,233 +45,3 @@ impl<E: Engine> std::fmt::Debug for TransportView<E> {
|
||||||
.finish()
|
.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::*;
|
use crate::*;
|
||||||
|
|
||||||
/// Handle input.
|
/// Handle input.
|
||||||
impl Handle<Tui> for TransportApp<Tui> {
|
impl Handle<Tui> for TransportTui {
|
||||||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||||
AppViewCommand::<TransportCommand>::execute_with_state(self, from)
|
AppViewCommand::<TransportCommand>::execute_with_state(self, from)
|
||||||
}
|
}
|
||||||
|
|
@ -9,15 +9,16 @@ impl Handle<Tui> for TransportApp<Tui> {
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub enum TransportCommand {
|
pub enum TransportCommand {
|
||||||
|
Focus(FocusCommand),
|
||||||
Clock(ClockCommand),
|
Clock(ClockCommand),
|
||||||
Playhead(PlayheadCommand),
|
Playhead(PlayheadCommand),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InputToCommand<Tui, TransportApp<Tui>> for AppViewCommand<TransportCommand> {
|
impl InputToCommand<Tui, TransportTui> for AppViewCommand<TransportCommand> {
|
||||||
fn input_to_command (app: &TransportApp<Tui>, input: &TuiInput) -> Option<Self> {
|
fn input_to_command (app: &TransportTui, input: &TuiInput) -> Option<Self> {
|
||||||
use KeyCode::{Left, Right};
|
use KeyCode::{Left, Right};
|
||||||
use FocusCommand::{Prev, Next};
|
use FocusCommand::{Prev, Next};
|
||||||
use AppViewCommand::{Focus, App};
|
use TransportCommand::{Focus, Clock, Playhead};
|
||||||
Some(match input.event() {
|
Some(match input.event() {
|
||||||
key!(Left) => Focus(Prev),
|
key!(Left) => Focus(Prev),
|
||||||
key!(Right) => Focus(Next),
|
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 {
|
impl<T: TransportControl> InputToCommand<Tui, T> for TransportCommand {
|
||||||
fn input_to_command (state: &T, input: &TuiInput) -> Option<Self> {
|
fn input_to_command (state: &T, input: &TuiInput) -> Option<Self> {
|
||||||
use KeyCode::Char;
|
use KeyCode::Char;
|
||||||
|
|
@ -102,8 +73,38 @@ impl<T: TransportControl> InputToCommand<Tui, T> for TransportCommand {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Command<TransportApp<Tui>> for AppViewCommand<TransportCommand> {
|
pub trait TransportControl: FocusGrid<Item = AppViewFocus<TransportViewFocus>> {
|
||||||
fn execute (self, state: &mut TransportApp<Tui>) -> Perhaps<Self> {
|
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 AppViewCommand::{Focus, App};
|
||||||
use FocusCommand::{Next, Prev};
|
use FocusCommand::{Next, Prev};
|
||||||
Ok(Some(match self {
|
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