wip: p.44, e=135, removing E generic

This commit is contained in:
🪞👃🪞 2024-11-16 22:05:34 +01:00
parent a7998860b1
commit 260736f31d
20 changed files with 848 additions and 838 deletions

View file

@ -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>>;
}

View file

@ -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;
}

View file

@ -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

View file

@ -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 {

View file

@ -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) => {

View file

@ -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::*;

View 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
}
}

View file

@ -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)),
)
)
)

View file

@ -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();

View file

@ -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!()
}
}
}

View 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!()
}
}
}

View 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(),
)
}
}

View file

@ -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
}

View file

@ -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()
}

View file

@ -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
}
}

View file

@ -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 {

View 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
}
}

View 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
}
}

View 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!();
""
}
}

View 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))
}
}