mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 19:56:42 +01:00
refactor arranger
This commit is contained in:
parent
4ee9822213
commit
8472805142
10 changed files with 701 additions and 774 deletions
|
|
@ -23,6 +23,7 @@ impl ArrangerCli {
|
||||||
fn run (&self) -> Usually<()> {
|
fn run (&self) -> Usually<()> {
|
||||||
Tui::run(JackClient::new("tek_arranger")?.activate_with(|jack|{
|
Tui::run(JackClient::new("tek_arranger")?.activate_with(|jack|{
|
||||||
let mut app = ArrangerTui::try_from(jack)?;
|
let mut app = ArrangerTui::try_from(jack)?;
|
||||||
|
app.color = ItemPalette::random();
|
||||||
if let Some(name) = self.name.as_ref() {
|
if let Some(name) = self.name.as_ref() {
|
||||||
*app.name.write().unwrap() = name.clone();
|
*app.name.write().unwrap() = name.clone();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,15 @@ mod app_sampler; pub(crate) use app_sampler::*;
|
||||||
mod app_groovebox; pub(crate) use app_groovebox::*;
|
mod app_groovebox; pub(crate) use app_groovebox::*;
|
||||||
mod app_arranger; pub(crate) use app_arranger::*;
|
mod app_arranger; pub(crate) use app_arranger::*;
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
mod arranger_clip; pub(crate) use arranger_clip::*;
|
||||||
|
mod arranger_scene; pub(crate) use arranger_scene::*;
|
||||||
|
mod arranger_select; pub(crate) use arranger_select::*;
|
||||||
|
mod arranger_track; pub(crate) use arranger_track::*;
|
||||||
|
mod arranger_mode_h; pub(crate) use arranger_mode_h::*;
|
||||||
|
mod arranger_mode_v; pub(crate) use arranger_mode_v::*;
|
||||||
|
|
||||||
////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////
|
||||||
|
|
||||||
mod status_bar; pub(crate) use status_bar::*;
|
mod status_bar; pub(crate) use status_bar::*;
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ pub struct ArrangerTui {
|
||||||
pub selected: ArrangerSelection,
|
pub selected: ArrangerSelection,
|
||||||
pub mode: ArrangerMode,
|
pub mode: ArrangerMode,
|
||||||
pub color: ItemPalette,
|
pub color: ItemPalette,
|
||||||
pub entered: bool,
|
|
||||||
pub size: Measure<Tui>,
|
pub size: Measure<Tui>,
|
||||||
pub cursor: (usize, usize),
|
pub cursor: (usize, usize),
|
||||||
pub menu_bar: Option<MenuBar<Tui, Self, ArrangerCommand>>,
|
pub menu_bar: Option<MenuBar<Tui, Self, ArrangerCommand>>,
|
||||||
|
|
@ -20,7 +19,6 @@ pub struct ArrangerTui {
|
||||||
pub note_buf: Vec<u8>,
|
pub note_buf: Vec<u8>,
|
||||||
pub midi_buf: Vec<Vec<Vec<u8>>>,
|
pub midi_buf: Vec<Vec<Vec<u8>>>,
|
||||||
pub editor: PhraseEditorModel,
|
pub editor: PhraseEditorModel,
|
||||||
pub focus: FocusState<ArrangerFocus>,
|
|
||||||
pub perf: PerfModel,
|
pub perf: PerfModel,
|
||||||
}
|
}
|
||||||
from_jack!(|jack| ArrangerTui Self {
|
from_jack!(|jack| ArrangerTui Self {
|
||||||
|
|
@ -38,13 +36,11 @@ from_jack!(|jack| ArrangerTui Self {
|
||||||
size: Measure::new(),
|
size: Measure::new(),
|
||||||
cursor: (0, 0),
|
cursor: (0, 0),
|
||||||
splits: [16, 20],
|
splits: [16, 20],
|
||||||
entered: false,
|
|
||||||
menu_bar: None,
|
menu_bar: None,
|
||||||
status_bar: None,
|
status_bar: None,
|
||||||
midi_buf: vec![vec![];65536],
|
midi_buf: vec![vec![];65536],
|
||||||
note_buf: vec![],
|
note_buf: vec![],
|
||||||
perf: PerfModel::default(),
|
perf: PerfModel::default(),
|
||||||
focus: FocusState::Entered(ArrangerFocus::Transport(TransportFocus::PlayPause)),
|
|
||||||
});
|
});
|
||||||
has_clock!(|self: ArrangerTui|&self.clock);
|
has_clock!(|self: ArrangerTui|&self.clock);
|
||||||
has_phrases!(|self: ArrangerTui|self.phrases.phrases);
|
has_phrases!(|self: ArrangerTui|self.phrases.phrases);
|
||||||
|
|
@ -52,9 +48,8 @@ has_editor!(|self: ArrangerTui|self.editor);
|
||||||
handle!(<Tui>|self: ArrangerTui, input|ArrangerCommand::execute_with_state(self, input));
|
handle!(<Tui>|self: ArrangerTui, input|ArrangerCommand::execute_with_state(self, input));
|
||||||
render!(<Tui>|self: ArrangerTui|{
|
render!(<Tui>|self: ArrangerTui|{
|
||||||
let arranger = ||lay!(|add|{
|
let arranger = ||lay!(|add|{
|
||||||
add(&Fill::wh(Lozenge(Style::default()
|
let color = self.color;
|
||||||
.bg(TuiTheme::border_bg())
|
add(&Fill::wh(Lozenge(Style::default().bg(color.light.rgb).fg(color.darker.rgb))))?;
|
||||||
.fg(TuiTheme::border_fg(true)))))?;
|
|
||||||
add(&self.size)?;
|
add(&self.size)?;
|
||||||
match self.mode {
|
match self.mode {
|
||||||
ArrangerMode::H => todo!("horizontal arranger"),
|
ArrangerMode::H => todo!("horizontal arranger"),
|
||||||
|
|
@ -62,7 +57,7 @@ render!(<Tui>|self: ArrangerTui|{
|
||||||
Align::se(Fill::wh(Tui::pull_x(1, Tui::fg(TuiTheme::title_fg(true),
|
Align::se(Fill::wh(Tui::pull_x(1, Tui::fg(TuiTheme::title_fg(true),
|
||||||
format!("{}x{}", self.size.w(), self.size.h()))
|
format!("{}x{}", self.size.w(), self.size.h()))
|
||||||
))),
|
))),
|
||||||
Tui::bg(self.color.dark.rgb, lay!(![
|
Tui::bg(color.darkest.rgb, lay!(![
|
||||||
ArrangerVColumnSeparator::from(self),
|
ArrangerVColumnSeparator::from(self),
|
||||||
ArrangerVRowSeparator::from((self, factor)),
|
ArrangerVRowSeparator::from((self, factor)),
|
||||||
col!(![
|
col!(![
|
||||||
|
|
@ -118,7 +113,6 @@ audio!(|self: ArrangerTui, client, scope|{
|
||||||
return Control::Continue
|
return Control::Continue
|
||||||
});
|
});
|
||||||
#[derive(Clone, Debug)] pub enum ArrangerCommand {
|
#[derive(Clone, Debug)] pub enum ArrangerCommand {
|
||||||
Focus(FocusCommand<ArrangerFocus>),
|
|
||||||
Undo,
|
Undo,
|
||||||
Redo,
|
Redo,
|
||||||
Clear,
|
Clear,
|
||||||
|
|
@ -132,50 +126,27 @@ audio!(|self: ArrangerTui, client, scope|{
|
||||||
Phrases(PhrasesCommand),
|
Phrases(PhrasesCommand),
|
||||||
Editor(PhraseCommand),
|
Editor(PhraseCommand),
|
||||||
}
|
}
|
||||||
input_to_command!(ArrangerCommand: <Tui>|state:ArrangerTui,input|
|
input_to_command!(ArrangerCommand: <Tui>|state:ArrangerTui,input|to_arranger_command(state, input)?);
|
||||||
to_arranger_command(state, input)
|
|
||||||
.or_else(||to_focus_command(input).map(ArrangerCommand::Focus))?);
|
|
||||||
fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option<ArrangerCommand> {
|
fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option<ArrangerCommand> {
|
||||||
|
use ArrangerSelection::*;
|
||||||
use ArrangerCommand as Cmd;
|
use ArrangerCommand as Cmd;
|
||||||
use KeyCode::Char;
|
use KeyCode::Char;
|
||||||
if !state.entered() {
|
// WSAD navigation, Q launches, E edits, PgUp/Down pool, Arrows editor
|
||||||
return None
|
|
||||||
}
|
|
||||||
Some(match input.event() {
|
Some(match input.event() {
|
||||||
key_pat!(Char('e')) => Cmd::Editor(PhraseCommand::Show(Some(state.phrases.phrase().clone()))),
|
key_pat!(Char('e')) => Cmd::Editor(PhraseCommand::Show(Some(state.phrases.phrase().clone()))),
|
||||||
// WSAD navigation, Q launches, E edits, PgUp/Down pool, Arrows editor
|
key_pat!(Char('l')) => Cmd::Clip(ArrangerClipCommand::SetLoop(false)),
|
||||||
_ => match state.focused() {
|
key_pat!(Char('+')) => Cmd::Zoom(0), // TODO
|
||||||
ArrangerFocus::Transport(_) => {
|
key_pat!(Char('=')) => Cmd::Zoom(0), // TODO
|
||||||
match to_transport_command(state, input)? {
|
key_pat!(Char('_')) => Cmd::Zoom(0), // TODO
|
||||||
TransportCommand::Clock(command) => Cmd::Clock(command),
|
key_pat!(Char('-')) => Cmd::Zoom(0), // TODO
|
||||||
_ => return None,
|
key_pat!(Char('`')) => { todo!("toggle state mode") },
|
||||||
}
|
key_pat!(Ctrl-Char('a')) => Cmd::Scene(ArrangerSceneCommand::Add),
|
||||||
},
|
key_pat!(Ctrl-Char('t')) => Cmd::Track(ArrangerTrackCommand::Add),
|
||||||
ArrangerFocus::PhraseEditor => {
|
_ => match state.selected() {
|
||||||
Cmd::Editor(PhraseCommand::input_to_command(&state.editor, input)?)
|
Mix => to_arranger_mix_command(input)?,
|
||||||
},
|
Track(t) => to_arranger_track_command(input, t)?,
|
||||||
ArrangerFocus::Phrases => {
|
Scene(s) => to_arranger_scene_command(input, s)?,
|
||||||
Cmd::Phrases(PhrasesCommand::input_to_command(&state.phrases, input)?)
|
Clip(t, s) => to_arranger_clip_command(input, t, s)?,
|
||||||
},
|
|
||||||
ArrangerFocus::Arranger => {
|
|
||||||
use ArrangerSelection::*;
|
|
||||||
match input.event() {
|
|
||||||
key_pat!(Char('l')) => Cmd::Clip(ArrangerClipCommand::SetLoop(false)),
|
|
||||||
key_pat!(Char('+')) => Cmd::Zoom(0), // TODO
|
|
||||||
key_pat!(Char('=')) => Cmd::Zoom(0), // TODO
|
|
||||||
key_pat!(Char('_')) => Cmd::Zoom(0), // TODO
|
|
||||||
key_pat!(Char('-')) => Cmd::Zoom(0), // TODO
|
|
||||||
key_pat!(Char('`')) => { todo!("toggle state mode") },
|
|
||||||
key_pat!(Ctrl-Char('a')) => Cmd::Scene(ArrangerSceneCommand::Add),
|
|
||||||
key_pat!(Ctrl-Char('t')) => Cmd::Track(ArrangerTrackCommand::Add),
|
|
||||||
_ => match state.selected() {
|
|
||||||
Mix => to_arranger_mix_command(input)?,
|
|
||||||
Track(t) => to_arranger_track_command(input, t)?,
|
|
||||||
Scene(s) => to_arranger_scene_command(input, s)?,
|
|
||||||
Clip(t, s) => to_arranger_clip_command(input, t, s)?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -184,8 +155,8 @@ fn to_arranger_mix_command (input: &TuiInput) -> Option<ArrangerCommand> {
|
||||||
use ArrangerCommand as Cmd;
|
use ArrangerCommand as Cmd;
|
||||||
use ArrangerSelection as Select;
|
use ArrangerSelection as Select;
|
||||||
Some(match input.event() {
|
Some(match input.event() {
|
||||||
key_pat!(Down) => Cmd::Select(Select::Scene(0)),
|
key_pat!(Char('s')) => Cmd::Select(Select::Scene(0)),
|
||||||
key_pat!(Right) => Cmd::Select(Select::Track(0)),
|
key_pat!(Char('d')) => Cmd::Select(Select::Track(0)),
|
||||||
key_pat!(Char(',')) => Cmd::Zoom(0),
|
key_pat!(Char(',')) => Cmd::Zoom(0),
|
||||||
key_pat!(Char('.')) => Cmd::Zoom(0),
|
key_pat!(Char('.')) => Cmd::Zoom(0),
|
||||||
key_pat!(Char('<')) => Cmd::Zoom(0),
|
key_pat!(Char('<')) => Cmd::Zoom(0),
|
||||||
|
|
@ -195,68 +166,9 @@ fn to_arranger_mix_command (input: &TuiInput) -> Option<ArrangerCommand> {
|
||||||
_ => return None
|
_ => return None
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
fn to_arranger_track_command (input: &TuiInput, t: usize) -> Option<ArrangerCommand> {
|
|
||||||
use KeyCode::{Char, Down, Left, Right, Delete};
|
|
||||||
use ArrangerCommand as Cmd;
|
|
||||||
use ArrangerSelection as Select;
|
|
||||||
use ArrangerTrackCommand as Track;
|
|
||||||
Some(match input.event() {
|
|
||||||
key_pat!(Down) => Cmd::Select(Select::Clip(t, 0)),
|
|
||||||
key_pat!(Left) => Cmd::Select(if t > 0 { Select::Track(t - 1) } else { Select::Mix }),
|
|
||||||
key_pat!(Right) => Cmd::Select(Select::Track(t + 1)),
|
|
||||||
key_pat!(Char(',')) => Cmd::Track(Track::Swap(t, t - 1)),
|
|
||||||
key_pat!(Char('.')) => Cmd::Track(Track::Swap(t, t + 1)),
|
|
||||||
key_pat!(Char('<')) => Cmd::Track(Track::Swap(t, t - 1)),
|
|
||||||
key_pat!(Char('>')) => Cmd::Track(Track::Swap(t, t + 1)),
|
|
||||||
key_pat!(Delete) => Cmd::Track(Track::Delete(t)),
|
|
||||||
//key_pat!(Char('c')) => Cmd::Track(Track::Color(t, ItemPalette::random())),
|
|
||||||
_ => return None
|
|
||||||
})
|
|
||||||
}
|
|
||||||
fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option<ArrangerCommand> {
|
|
||||||
use KeyCode::{Char, Up, Down, Right, Enter, Delete};
|
|
||||||
use ArrangerCommand as Cmd;
|
|
||||||
use ArrangerSelection as Select;
|
|
||||||
use ArrangerSceneCommand as Scene;
|
|
||||||
Some(match input.event() {
|
|
||||||
key_pat!(Up) => Cmd::Select(if s > 0 { Select::Scene(s - 1) } else { Select::Mix }),
|
|
||||||
key_pat!(Down) => Cmd::Select(Select::Scene(s + 1)),
|
|
||||||
key_pat!(Right) => Cmd::Select(Select::Clip(0, s)),
|
|
||||||
key_pat!(Char(',')) => Cmd::Scene(Scene::Swap(s, s - 1)),
|
|
||||||
key_pat!(Char('.')) => Cmd::Scene(Scene::Swap(s, s + 1)),
|
|
||||||
key_pat!(Char('<')) => Cmd::Scene(Scene::Swap(s, s - 1)),
|
|
||||||
key_pat!(Char('>')) => Cmd::Scene(Scene::Swap(s, s + 1)),
|
|
||||||
key_pat!(Enter) => Cmd::Scene(Scene::Play(s)),
|
|
||||||
key_pat!(Delete) => Cmd::Scene(Scene::Delete(s)),
|
|
||||||
//key_pat!(Char('c')) => Cmd::Track(Scene::Color(s, ItemPalette::random())),
|
|
||||||
_ => return None
|
|
||||||
})
|
|
||||||
}
|
|
||||||
fn to_arranger_clip_command (input: &TuiInput, t: usize, s: usize) -> Option<ArrangerCommand> {
|
|
||||||
use KeyCode::{Char, Up, Down, Left, Right, Delete};
|
|
||||||
use ArrangerCommand as Cmd;
|
|
||||||
use ArrangerSelection as Select;
|
|
||||||
use ArrangerClipCommand as Clip;
|
|
||||||
Some(match input.event() {
|
|
||||||
key_pat!(Up) => Cmd::Select(if s > 0 { Select::Clip(t, s - 1) } else { Select::Track(t) }),
|
|
||||||
key_pat!(Down) => Cmd::Select(Select::Clip(t, s + 1)),
|
|
||||||
key_pat!(Left) => Cmd::Select(if t > 0 { Select::Clip(t - 1, s) } else { Select::Scene(s) }),
|
|
||||||
key_pat!(Right) => Cmd::Select(Select::Clip(t + 1, s)),
|
|
||||||
key_pat!(Char(',')) => Cmd::Clip(Clip::Set(t, s, None)),
|
|
||||||
key_pat!(Char('.')) => Cmd::Clip(Clip::Set(t, s, None)),
|
|
||||||
key_pat!(Char('<')) => Cmd::Clip(Clip::Set(t, s, None)),
|
|
||||||
key_pat!(Char('>')) => Cmd::Clip(Clip::Set(t, s, None)),
|
|
||||||
key_pat!(Delete) => Cmd::Clip(Clip::Set(t, s, None)),
|
|
||||||
//key_pat!(Char('c')) => Cmd::Clip(Clip::Color(t, s, ItemPalette::random())),
|
|
||||||
//key_pat!(Char('g')) => Cmd::Clip(Clip(Clip::Get(t, s))),
|
|
||||||
//key_pat!(Char('s')) => Cmd::Clip(Clip(Clip::Set(t, s))),
|
|
||||||
_ => return None
|
|
||||||
})
|
|
||||||
}
|
|
||||||
command!(|self:ArrangerCommand,state:ArrangerTui|{
|
command!(|self:ArrangerCommand,state:ArrangerTui|{
|
||||||
use ArrangerCommand::*;
|
use ArrangerCommand::*;
|
||||||
match self {
|
match self {
|
||||||
Focus(cmd) => cmd.execute(state)?.map(Focus),
|
|
||||||
Scene(cmd) => cmd.execute(state)?.map(Scene),
|
Scene(cmd) => cmd.execute(state)?.map(Scene),
|
||||||
Track(cmd) => cmd.execute(state)?.map(Track),
|
Track(cmd) => cmd.execute(state)?.map(Track),
|
||||||
Clip(cmd) => cmd.execute(state)?.map(Clip),
|
Clip(cmd) => cmd.execute(state)?.map(Clip),
|
||||||
|
|
@ -279,15 +191,43 @@ command!(|self:ArrangerCommand,state:ArrangerTui|{
|
||||||
command!(|self:ArrangerSceneCommand,_state:ArrangerTui|None);
|
command!(|self:ArrangerSceneCommand,_state:ArrangerTui|None);
|
||||||
command!(|self:ArrangerTrackCommand,_state:ArrangerTui|None);
|
command!(|self:ArrangerTrackCommand,_state:ArrangerTui|None);
|
||||||
command!(|self:ArrangerClipCommand, _state:ArrangerTui|None);
|
command!(|self:ArrangerClipCommand, _state:ArrangerTui|None);
|
||||||
pub trait ArrangerControl: TransportControl<ArrangerFocus> {
|
#[derive(Clone, Debug)]
|
||||||
fn selected (&self) -> ArrangerSelection;
|
pub enum ArrangerClipCommand {
|
||||||
fn selected_mut (&mut self) -> &mut ArrangerSelection;
|
Play,
|
||||||
fn selected_phrase (&self) -> Option<Arc<RwLock<Phrase>>>;
|
Get(usize, usize),
|
||||||
fn activate (&mut self) -> Usually<()>;
|
Set(usize, usize, Option<Arc<RwLock<Phrase>>>),
|
||||||
fn toggle_loop (&mut self);
|
Edit(Option<Arc<RwLock<Phrase>>>),
|
||||||
fn randomize_color (&mut self);
|
SetLoop(bool),
|
||||||
|
RandomColor,
|
||||||
}
|
}
|
||||||
impl ArrangerControl for ArrangerTui {
|
|
||||||
|
/// Display mode of arranger
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub enum ArrangerMode {
|
||||||
|
/// Tracks are columns
|
||||||
|
V(usize),
|
||||||
|
/// Tracks are rows
|
||||||
|
H,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Arranger display mode can be cycled
|
||||||
|
impl ArrangerMode {
|
||||||
|
/// Cycle arranger display mode
|
||||||
|
pub fn to_next (&mut self) {
|
||||||
|
*self = match self {
|
||||||
|
Self::H => Self::V(1),
|
||||||
|
Self::V(1) => Self::V(2),
|
||||||
|
Self::V(2) => Self::V(2),
|
||||||
|
Self::V(0) => Self::H,
|
||||||
|
Self::V(_) => Self::V(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn any_size <E: Engine> (_: E::Size) -> Perhaps<E::Size>{
|
||||||
|
Ok(Some([0.into(),0.into()].into()))
|
||||||
|
}
|
||||||
|
impl ArrangerTui {
|
||||||
fn selected (&self) -> ArrangerSelection {
|
fn selected (&self) -> ArrangerSelection {
|
||||||
self.selected
|
self.selected
|
||||||
}
|
}
|
||||||
|
|
@ -338,549 +278,6 @@ impl ArrangerControl for ArrangerTui {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl TransportControl<ArrangerFocus> for ArrangerTui {
|
|
||||||
fn transport_focused (&self) -> Option<TransportFocus> {
|
|
||||||
match self.focus.inner() {
|
|
||||||
ArrangerFocus::Transport(focus) => Some(focus),
|
|
||||||
_ => None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sections in the arranger app that may be focused
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub enum ArrangerFocus {
|
|
||||||
/// The transport (toolbar) is focused
|
|
||||||
Transport(TransportFocus),
|
|
||||||
/// The arrangement (grid) is focused
|
|
||||||
Arranger,
|
|
||||||
/// The phrase list (pool) is focused
|
|
||||||
Phrases,
|
|
||||||
/// The phrase editor (sequencer) is focused
|
|
||||||
PhraseEditor,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Into<Option<TransportFocus>> for ArrangerFocus {
|
|
||||||
fn into (self) -> Option<TransportFocus> {
|
|
||||||
if let Self::Transport(transport) = self {
|
|
||||||
Some(transport)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&ArrangerTui> for Option<TransportFocus> {
|
|
||||||
fn from (state: &ArrangerTui) -> Self {
|
|
||||||
match state.focus.inner() {
|
|
||||||
ArrangerFocus::Transport(focus) => Some(focus),
|
|
||||||
_ => None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl_focus!(ArrangerTui ArrangerFocus [
|
|
||||||
&[
|
|
||||||
Transport(TransportFocus::PlayPause),
|
|
||||||
Transport(TransportFocus::Bpm),
|
|
||||||
Transport(TransportFocus::Sync),
|
|
||||||
Transport(TransportFocus::Quant),
|
|
||||||
Transport(TransportFocus::Clock),
|
|
||||||
],
|
|
||||||
&[Arranger;5],
|
|
||||||
&[Phrases, Phrases, PhraseEditor, PhraseEditor, PhraseEditor],
|
|
||||||
]);
|
|
||||||
|
|
||||||
/// Status bar for arranger app
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
|
||||||
pub enum ArrangerStatus {
|
|
||||||
Transport,
|
|
||||||
ArrangerMix,
|
|
||||||
ArrangerTrack,
|
|
||||||
ArrangerScene,
|
|
||||||
ArrangerClip,
|
|
||||||
PhrasePool,
|
|
||||||
PhraseView,
|
|
||||||
PhraseEdit,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Display mode of arranger
|
|
||||||
#[derive(Clone, PartialEq)]
|
|
||||||
pub enum ArrangerMode {
|
|
||||||
/// Tracks are columns
|
|
||||||
V(usize),
|
|
||||||
/// Tracks are rows
|
|
||||||
H,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Arranger display mode can be cycled
|
|
||||||
impl ArrangerMode {
|
|
||||||
/// Cycle arranger display mode
|
|
||||||
pub fn to_next (&mut self) {
|
|
||||||
*self = match self {
|
|
||||||
Self::H => Self::V(1),
|
|
||||||
Self::V(1) => Self::V(2),
|
|
||||||
Self::V(2) => Self::V(2),
|
|
||||||
Self::V(0) => Self::H,
|
|
||||||
Self::V(_) => Self::V(0),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn track_widths (tracks: &[ArrangerTrack]) -> Vec<(usize, usize)> {
|
|
||||||
let mut widths = vec![];
|
|
||||||
let mut total = 0;
|
|
||||||
for track in tracks.iter() {
|
|
||||||
let width = track.width;
|
|
||||||
widths.push((width, total));
|
|
||||||
total += width;
|
|
||||||
}
|
|
||||||
widths.push((0, total));
|
|
||||||
widths
|
|
||||||
}
|
|
||||||
|
|
||||||
fn any_size <E: Engine> (_: E::Size) -> Perhaps<E::Size>{
|
|
||||||
Ok(Some([0.into(),0.into()].into()))
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ArrangerVColumnSeparator {
|
|
||||||
cols: Vec<(usize, usize)>,
|
|
||||||
scenes_w: u16,
|
|
||||||
sep_fg: Color,
|
|
||||||
}
|
|
||||||
impl From<&ArrangerTui> for ArrangerVColumnSeparator {
|
|
||||||
fn from (state: &ArrangerTui) -> Self {
|
|
||||||
Self {
|
|
||||||
cols: track_widths(state.tracks()),
|
|
||||||
scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16,
|
|
||||||
sep_fg: TuiTheme::separator_fg(false),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
render!(<Tui>|self: ArrangerVColumnSeparator|render(move|to: &mut TuiOutput|{
|
|
||||||
let style = Some(Style::default().fg(self.sep_fg));
|
|
||||||
Ok(for x in self.cols.iter().map(|col|col.1) {
|
|
||||||
let x = self.scenes_w + to.area().x() + x as u16;
|
|
||||||
for y in to.area().y()..to.area().y2() {
|
|
||||||
to.blit(&"▎", x, y, style);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}));
|
|
||||||
|
|
||||||
struct ArrangerVRowSeparator {
|
|
||||||
rows: Vec<(usize, usize)>,
|
|
||||||
sep_fg: Color,
|
|
||||||
}
|
|
||||||
impl From<(&ArrangerTui, usize)> for ArrangerVRowSeparator {
|
|
||||||
fn from ((state, factor): (&ArrangerTui, usize)) -> Self {
|
|
||||||
Self {
|
|
||||||
rows: ArrangerScene::ppqs(state.scenes(), factor),
|
|
||||||
sep_fg: TuiTheme::separator_fg(false),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render!(<Tui>|self: ArrangerVRowSeparator|render(move|to: &mut TuiOutput|{
|
|
||||||
Ok(for y in self.rows.iter().map(|row|row.1) {
|
|
||||||
let y = to.area().y() + (y / PPQ) as u16 + 1;
|
|
||||||
if y >= to.buffer.area.height { break }
|
|
||||||
for x in to.area().x()..to.area().x2().saturating_sub(2) {
|
|
||||||
if x < to.buffer.area.x && y < to.buffer.area.y {
|
|
||||||
let cell = to.buffer.get_mut(x, y);
|
|
||||||
cell.modifier = Modifier::UNDERLINED;
|
|
||||||
cell.underline_color = self.sep_fg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}));
|
|
||||||
|
|
||||||
struct ArrangerVCursor {
|
|
||||||
cols: Vec<(usize, usize)>,
|
|
||||||
rows: Vec<(usize, usize)>,
|
|
||||||
focused: bool,
|
|
||||||
selected: ArrangerSelection,
|
|
||||||
scenes_w: u16,
|
|
||||||
header_h: u16,
|
|
||||||
}
|
|
||||||
impl From<(&ArrangerTui, usize)> for ArrangerVCursor {
|
|
||||||
fn from ((state, factor): (&ArrangerTui, usize)) -> Self {
|
|
||||||
Self {
|
|
||||||
cols: track_widths(state.tracks()),
|
|
||||||
rows: ArrangerScene::ppqs(state.scenes(), factor),
|
|
||||||
focused: true,
|
|
||||||
selected: state.selected,
|
|
||||||
scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16,
|
|
||||||
header_h: 3,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
render!(<Tui>|self: ArrangerVCursor|render(move|to: &mut TuiOutput|{
|
|
||||||
let area = to.area();
|
|
||||||
let focused = self.focused;
|
|
||||||
let selected = self.selected;
|
|
||||||
let get_track_area = |t: usize| [
|
|
||||||
self.scenes_w + area.x() + self.cols[t].1 as u16, area.y(),
|
|
||||||
self.cols[t].0 as u16, area.h(),
|
|
||||||
];
|
|
||||||
let get_scene_area = |s: usize| [
|
|
||||||
area.x(), self.header_h + area.y() + (self.rows[s].1 / PPQ) as u16,
|
|
||||||
area.w(), (self.rows[s].0 / PPQ) as u16
|
|
||||||
];
|
|
||||||
let get_clip_area = |t: usize, s: usize| [
|
|
||||||
self.scenes_w + area.x() + self.cols[t].1 as u16,
|
|
||||||
self.header_h + area.y() + (self.rows[s].1/PPQ) as u16,
|
|
||||||
self.cols[t].0 as u16,
|
|
||||||
(self.rows[s].0 / PPQ) as u16
|
|
||||||
];
|
|
||||||
let mut track_area: Option<[u16;4]> = None;
|
|
||||||
let mut scene_area: Option<[u16;4]> = None;
|
|
||||||
let mut clip_area: Option<[u16;4]> = None;
|
|
||||||
let area = match selected {
|
|
||||||
ArrangerSelection::Mix => area,
|
|
||||||
ArrangerSelection::Track(t) => {
|
|
||||||
track_area = Some(get_track_area(t));
|
|
||||||
area
|
|
||||||
},
|
|
||||||
ArrangerSelection::Scene(s) => {
|
|
||||||
scene_area = Some(get_scene_area(s));
|
|
||||||
area
|
|
||||||
},
|
|
||||||
ArrangerSelection::Clip(t, s) => {
|
|
||||||
track_area = Some(get_track_area(t));
|
|
||||||
scene_area = Some(get_scene_area(s));
|
|
||||||
clip_area = Some(get_clip_area(t, s));
|
|
||||||
area
|
|
||||||
},
|
|
||||||
};
|
|
||||||
let bg = TuiTheme::border_bg();
|
|
||||||
if let Some([x, y, width, height]) = track_area {
|
|
||||||
to.fill_fg([x, y, 1, height], bg);
|
|
||||||
to.fill_fg([x + width, y, 1, height], bg);
|
|
||||||
}
|
|
||||||
if let Some([_, y, _, height]) = scene_area {
|
|
||||||
to.fill_ul([area.x(), y - 1, area.w(), 1], bg);
|
|
||||||
to.fill_ul([area.x(), y + height - 1, area.w(), 1], bg);
|
|
||||||
}
|
|
||||||
Ok(if focused {
|
|
||||||
to.render_in(if let Some(clip_area) = clip_area { clip_area }
|
|
||||||
else if let Some(track_area) = track_area { track_area.clip_h(self.header_h) }
|
|
||||||
else if let Some(scene_area) = scene_area { scene_area.clip_w(self.scenes_w) }
|
|
||||||
else { area.clip_w(self.scenes_w).clip_h(self.header_h) }, &CORNERS)?
|
|
||||||
})
|
|
||||||
}));
|
|
||||||
|
|
||||||
struct ArrangerVHeader<'a> {
|
|
||||||
tracks: &'a Vec<ArrangerTrack>,
|
|
||||||
cols: Vec<(usize, usize)>,
|
|
||||||
focused: bool,
|
|
||||||
selected: ArrangerSelection,
|
|
||||||
scenes_w: u16,
|
|
||||||
header_h: u16,
|
|
||||||
timebase: &'a Arc<Timebase>,
|
|
||||||
current: &'a Arc<Moment>,
|
|
||||||
}
|
|
||||||
impl<'a> From<&'a ArrangerTui> for ArrangerVHeader<'a> {
|
|
||||||
fn from (state: &'a ArrangerTui) -> Self {
|
|
||||||
Self {
|
|
||||||
tracks: &state.tracks,
|
|
||||||
cols: track_widths(state.tracks()),
|
|
||||||
focused: true,
|
|
||||||
selected: state.selected,
|
|
||||||
scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16,
|
|
||||||
header_h: 3,
|
|
||||||
timebase: state.clock().timebase(),
|
|
||||||
current: &state.clock().playhead,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
render!(<Tui>|self: ArrangerVHeader<'a>|row!(
|
|
||||||
(track, w) in self.tracks.iter().zip(self.cols.iter().map(|col|col.0)) => {
|
|
||||||
// name and width of track
|
|
||||||
let name = track.name().read().unwrap();
|
|
||||||
let max_w = w.saturating_sub(1).min(name.len()).max(2);
|
|
||||||
let name = format!("▎{}", &name[0..max_w]);
|
|
||||||
let name = Tui::bold(true, Tui::fg(track.color.lightest.rgb, name));
|
|
||||||
// beats elapsed
|
|
||||||
let elapsed = if let Some((_, Some(phrase))) = track.player.play_phrase().as_ref() {
|
|
||||||
let length = phrase.read().unwrap().length;
|
|
||||||
let elapsed = track.player.pulses_since_start().unwrap();
|
|
||||||
let elapsed = self.timebase.format_beats_1_short(
|
|
||||||
(elapsed as usize % length) as f64
|
|
||||||
);
|
|
||||||
format!("▎+{elapsed:>}")
|
|
||||||
} else {
|
|
||||||
String::from("▎")
|
|
||||||
};
|
|
||||||
// beats until switchover
|
|
||||||
let until_next = track.player.next_phrase().as_ref().map(|(t, _)|{
|
|
||||||
let target = t.pulse.get();
|
|
||||||
let current = self.current.pulse.get();
|
|
||||||
if target > current {
|
|
||||||
let remaining = target - current;
|
|
||||||
format!("▎-{:>}", self.timebase.format_beats_0_short(remaining))
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
}
|
|
||||||
}).unwrap_or(String::from("▎"));
|
|
||||||
let timer = col!([until_next, elapsed]);
|
|
||||||
// name of active MIDI input
|
|
||||||
let _input = format!("▎>{}", track.player.midi_ins().get(0)
|
|
||||||
.map(|port|port.short_name())
|
|
||||||
.transpose()?
|
|
||||||
.unwrap_or("(none)".into()));
|
|
||||||
// name of active MIDI output
|
|
||||||
let _output = format!("▎<{}", track.player.midi_outs().get(0)
|
|
||||||
.map(|port|port.short_name())
|
|
||||||
.transpose()?
|
|
||||||
.unwrap_or("(none)".into()));
|
|
||||||
Tui::push_x(self.scenes_w,
|
|
||||||
Tui::bg(track.color().base.rgb,
|
|
||||||
Tui::min_xy(w as u16, self.header_h,
|
|
||||||
col!([name, timer]))))
|
|
||||||
}
|
|
||||||
));
|
|
||||||
|
|
||||||
struct ArrangerVContent<'a> {
|
|
||||||
size: &'a Measure<Tui>,
|
|
||||||
scenes: &'a Vec<ArrangerScene>,
|
|
||||||
tracks: &'a Vec<ArrangerTrack>,
|
|
||||||
rows: Vec<(usize, usize)>,
|
|
||||||
cols: Vec<(usize, usize)>,
|
|
||||||
header_h: u16,
|
|
||||||
}
|
|
||||||
impl<'a> From<(&'a ArrangerTui, usize)> for ArrangerVContent<'a> {
|
|
||||||
fn from ((state, factor): (&'a ArrangerTui, usize)) -> Self {
|
|
||||||
Self {
|
|
||||||
size: &state.size,
|
|
||||||
scenes: &state.scenes,
|
|
||||||
tracks: &state.tracks,
|
|
||||||
rows: ArrangerScene::ppqs(state.scenes(), factor),
|
|
||||||
cols: track_widths(state.tracks()),
|
|
||||||
header_h: 3,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
render!(<Tui>|self: ArrangerVContent<'a>|Fixed::h(
|
|
||||||
(self.size.h() as u16).saturating_sub(self.header_h),
|
|
||||||
col!((scene, pulses) in self.scenes.iter().zip(self.rows.iter().map(|row|row.0)) => {
|
|
||||||
let height = 1.max((pulses / PPQ) as u16);
|
|
||||||
let playing = scene.is_playing(self.tracks);
|
|
||||||
Fixed::h(height, row!([
|
|
||||||
if playing { "▶ " } else { " " },
|
|
||||||
Tui::bold(true, scene.name.read().unwrap().as_str()),
|
|
||||||
row!((track, w) in self.cols.iter().map(|col|col.0).enumerate() => {
|
|
||||||
Fixed::wh(w as u16, height, Layers::new(move |add|{
|
|
||||||
let mut bg = TuiTheme::border_bg();
|
|
||||||
match (self.tracks.get(track), scene.clips.get(track)) {
|
|
||||||
(Some(track), Some(Some(phrase))) => {
|
|
||||||
let name = &(phrase as &Arc<RwLock<Phrase>>).read().unwrap().name;
|
|
||||||
let name = format!("{}", name);
|
|
||||||
let max_w = name.len().min((w as usize).saturating_sub(2));
|
|
||||||
let color = phrase.read().unwrap().color;
|
|
||||||
bg = color.dark.rgb;
|
|
||||||
if let Some((_, Some(ref playing))) = track.player.play_phrase() {
|
|
||||||
if *playing.read().unwrap() == *phrase.read().unwrap() {
|
|
||||||
bg = color.light.rgb
|
|
||||||
}
|
|
||||||
};
|
|
||||||
add(&Fixed::w(w as u16, Tui::push_x(1, &name.as_str()[0..max_w])))?;
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
};
|
|
||||||
//add(&Background(bg))
|
|
||||||
Ok(())
|
|
||||||
}))
|
|
||||||
})])
|
|
||||||
)
|
|
||||||
})
|
|
||||||
));
|
|
||||||
|
|
||||||
impl HasScenes<ArrangerScene> for ArrangerTui {
|
|
||||||
fn scenes (&self) -> &Vec<ArrangerScene> {
|
|
||||||
&self.scenes
|
|
||||||
}
|
|
||||||
fn scenes_mut (&mut self) -> &mut Vec<ArrangerScene> {
|
|
||||||
&mut self.scenes
|
|
||||||
}
|
|
||||||
fn scene_add (&mut self, name: Option<&str>, color: Option<ItemPalette>)
|
|
||||||
-> Usually<&mut ArrangerScene>
|
|
||||||
{
|
|
||||||
let name = name.map_or_else(||self.scene_default_name(), |x|x.to_string());
|
|
||||||
let scene = ArrangerScene {
|
|
||||||
name: Arc::new(name.into()),
|
|
||||||
clips: vec![None;self.tracks().len()],
|
|
||||||
color: color.unwrap_or_else(||ItemPalette::random()),
|
|
||||||
};
|
|
||||||
self.scenes_mut().push(scene);
|
|
||||||
let index = self.scenes().len() - 1;
|
|
||||||
Ok(&mut self.scenes_mut()[index])
|
|
||||||
}
|
|
||||||
fn selected_scene (&self) -> Option<&ArrangerScene> {
|
|
||||||
self.selected.scene().map(|s|self.scenes().get(s)).flatten()
|
|
||||||
}
|
|
||||||
fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> {
|
|
||||||
self.selected.scene().map(|s|self.scenes_mut().get_mut(s)).flatten()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone)] pub struct ArrangerScene {
|
|
||||||
/// Name of scene
|
|
||||||
pub(crate) name: Arc<RwLock<String>>,
|
|
||||||
/// Clips in scene, one per track
|
|
||||||
pub(crate) clips: Vec<Option<Arc<RwLock<Phrase>>>>,
|
|
||||||
/// Identifying color of scene
|
|
||||||
pub(crate) color: ItemPalette,
|
|
||||||
}
|
|
||||||
impl ArrangerSceneApi for ArrangerScene {
|
|
||||||
fn name (&self) -> &Arc<RwLock<String>> {
|
|
||||||
&self.name
|
|
||||||
}
|
|
||||||
fn clips (&self) -> &Vec<Option<Arc<RwLock<Phrase>>>> {
|
|
||||||
&self.clips
|
|
||||||
}
|
|
||||||
fn color (&self) -> ItemPalette {
|
|
||||||
self.color
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl HasTracks<ArrangerTrack> for ArrangerTui {
|
|
||||||
fn tracks (&self) -> &Vec<ArrangerTrack> {
|
|
||||||
&self.tracks
|
|
||||||
}
|
|
||||||
fn tracks_mut (&mut self) -> &mut Vec<ArrangerTrack> {
|
|
||||||
&mut self.tracks
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl ArrangerTracksApi<ArrangerTrack> for ArrangerTui {
|
|
||||||
fn track_add (&mut self, name: Option<&str>, color: Option<ItemPalette>)
|
|
||||||
-> Usually<&mut ArrangerTrack>
|
|
||||||
{
|
|
||||||
let name = name.map_or_else(||self.track_default_name(), |x|x.to_string());
|
|
||||||
let track = ArrangerTrack {
|
|
||||||
width: name.len() + 2,
|
|
||||||
name: Arc::new(name.into()),
|
|
||||||
color: color.unwrap_or_else(||ItemPalette::random()),
|
|
||||||
player: PhrasePlayerModel::from(&self.clock),
|
|
||||||
};
|
|
||||||
self.tracks_mut().push(track);
|
|
||||||
let index = self.tracks().len() - 1;
|
|
||||||
Ok(&mut self.tracks_mut()[index])
|
|
||||||
}
|
|
||||||
fn track_del (&mut self, index: usize) {
|
|
||||||
self.tracks_mut().remove(index);
|
|
||||||
for scene in self.scenes_mut().iter_mut() {
|
|
||||||
scene.clips.remove(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)] pub struct ArrangerTrack {
|
|
||||||
/// Name of track
|
|
||||||
pub(crate) name: Arc<RwLock<String>>,
|
|
||||||
/// Preferred width of track column
|
|
||||||
pub(crate) width: usize,
|
|
||||||
/// Identifying color of track
|
|
||||||
pub(crate) color: ItemPalette,
|
|
||||||
/// MIDI player state
|
|
||||||
pub(crate) player: PhrasePlayerModel,
|
|
||||||
}
|
|
||||||
has_clock!(|self:ArrangerTrack|self.player.clock());
|
|
||||||
has_player!(|self:ArrangerTrack|self.player);
|
|
||||||
impl ArrangerTrackApi for ArrangerTrack {
|
|
||||||
/// Name of track
|
|
||||||
fn name (&self) -> &Arc<RwLock<String>> {
|
|
||||||
&self.name
|
|
||||||
}
|
|
||||||
/// Preferred width of track column
|
|
||||||
fn width (&self) -> usize {
|
|
||||||
self.width
|
|
||||||
}
|
|
||||||
/// Preferred width of track column
|
|
||||||
fn width_mut (&mut self) -> &mut usize {
|
|
||||||
&mut self.width
|
|
||||||
}
|
|
||||||
/// Identifying color of track
|
|
||||||
fn color (&self) -> ItemPalette {
|
|
||||||
self.color
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[derive(PartialEq, Clone, Copy, Debug)]
|
|
||||||
/// Represents the current user selection in the arranger
|
|
||||||
pub enum ArrangerSelection {
|
|
||||||
/// The whole mix is selected
|
|
||||||
Mix,
|
|
||||||
/// A track is selected.
|
|
||||||
Track(usize),
|
|
||||||
/// A scene is selected.
|
|
||||||
Scene(usize),
|
|
||||||
/// A clip (track × scene) is selected.
|
|
||||||
Clip(usize, usize),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Focus identification methods
|
|
||||||
impl ArrangerSelection {
|
|
||||||
pub fn description <E: Engine> (
|
|
||||||
&self,
|
|
||||||
tracks: &Vec<ArrangerTrack>,
|
|
||||||
scenes: &Vec<ArrangerScene>,
|
|
||||||
) -> String {
|
|
||||||
format!("Selected: {}", match self {
|
|
||||||
Self::Mix => format!("Everything"),
|
|
||||||
Self::Track(t) => match tracks.get(*t) {
|
|
||||||
Some(track) => format!("T{t}: {}", &track.name.read().unwrap()),
|
|
||||||
None => format!("T??"),
|
|
||||||
},
|
|
||||||
Self::Scene(s) => match scenes.get(*s) {
|
|
||||||
Some(scene) => format!("S{s}: {}", &scene.name.read().unwrap()),
|
|
||||||
None => format!("S??"),
|
|
||||||
},
|
|
||||||
Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) {
|
|
||||||
(Some(_), Some(scene)) => match scene.clip(*t) {
|
|
||||||
Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name),
|
|
||||||
None => format!("T{t} S{s}: Empty")
|
|
||||||
},
|
|
||||||
_ => format!("T{t} S{s}: Empty"),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
pub fn is_mix (&self) -> bool {
|
|
||||||
match self { Self::Mix => true, _ => false }
|
|
||||||
}
|
|
||||||
pub fn is_track (&self) -> bool {
|
|
||||||
match self { Self::Track(_) => true, _ => false }
|
|
||||||
}
|
|
||||||
pub fn is_scene (&self) -> bool {
|
|
||||||
match self { Self::Scene(_) => true, _ => false }
|
|
||||||
}
|
|
||||||
pub fn is_clip (&self) -> bool {
|
|
||||||
match self { Self::Clip(_, _) => true, _ => false }
|
|
||||||
}
|
|
||||||
pub fn track (&self) -> Option<usize> {
|
|
||||||
use ArrangerSelection::*;
|
|
||||||
match self {
|
|
||||||
Clip(t, _) => Some(*t),
|
|
||||||
Track(t) => Some(*t),
|
|
||||||
_ => None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn scene (&self) -> Option<usize> {
|
|
||||||
use ArrangerSelection::*;
|
|
||||||
match self {
|
|
||||||
Clip(_, s) => Some(*s),
|
|
||||||
Scene(s) => Some(*s),
|
|
||||||
_ => None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub enum ArrangerClipCommand {
|
|
||||||
Play,
|
|
||||||
Get(usize, usize),
|
|
||||||
Set(usize, usize, Option<Arc<RwLock<Phrase>>>),
|
|
||||||
Edit(Option<Arc<RwLock<Phrase>>>),
|
|
||||||
SetLoop(bool),
|
|
||||||
RandomColor,
|
|
||||||
}
|
|
||||||
|
|
||||||
//impl<T: ArrangerApi> Command<T> for ArrangerClipCommand {
|
//impl<T: ArrangerApi> Command<T> for ArrangerClipCommand {
|
||||||
//fn execute (self, state: &mut T) -> Perhaps<Self> {
|
//fn execute (self, state: &mut T) -> Perhaps<Self> {
|
||||||
|
|
@ -890,12 +287,6 @@ pub enum ArrangerClipCommand {
|
||||||
//Ok(None)
|
//Ok(None)
|
||||||
//}
|
//}
|
||||||
//}
|
//}
|
||||||
|
|
||||||
pub fn arranger_content_horizontal (
|
|
||||||
view: &ArrangerTui,
|
|
||||||
) -> impl Render<Tui> + use<'_> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
//let focused = true;
|
//let focused = true;
|
||||||
//let _tracks = view.tracks();
|
//let _tracks = view.tracks();
|
||||||
//lay!(
|
//lay!(
|
||||||
|
|
|
||||||
23
crates/tek/src/tui/arranger_clip.rs
Normal file
23
crates/tek/src/tui/arranger_clip.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
pub fn to_arranger_clip_command (input: &TuiInput, t: usize, s: usize) -> Option<ArrangerCommand> {
|
||||||
|
use KeyCode::{Char, Up, Down, Left, Right, Delete};
|
||||||
|
use ArrangerCommand as Cmd;
|
||||||
|
use ArrangerSelection as Select;
|
||||||
|
use ArrangerClipCommand as Clip;
|
||||||
|
Some(match input.event() {
|
||||||
|
key_pat!(Char('w')) => Cmd::Select(if s > 0 { Select::Clip(t, s - 1) } else { Select::Track(t) }),
|
||||||
|
key_pat!(Char('s')) => Cmd::Select(Select::Clip(t, s + 1)),
|
||||||
|
key_pat!(Char('a')) => Cmd::Select(if t > 0 { Select::Clip(t - 1, s) } else { Select::Scene(s) }),
|
||||||
|
key_pat!(Char('d')) => Cmd::Select(Select::Clip(t + 1, s)),
|
||||||
|
key_pat!(Char(',')) => Cmd::Clip(Clip::Set(t, s, None)),
|
||||||
|
key_pat!(Char('.')) => Cmd::Clip(Clip::Set(t, s, None)),
|
||||||
|
key_pat!(Char('<')) => Cmd::Clip(Clip::Set(t, s, None)),
|
||||||
|
key_pat!(Char('>')) => Cmd::Clip(Clip::Set(t, s, None)),
|
||||||
|
key_pat!(Delete) => Cmd::Clip(Clip::Set(t, s, None)),
|
||||||
|
//key_pat!(Char('c')) => Cmd::Clip(Clip::Color(t, s, ItemPalette::random())),
|
||||||
|
//key_pat!(Char('g')) => Cmd::Clip(Clip(Clip::Get(t, s))),
|
||||||
|
//key_pat!(Char('s')) => Cmd::Clip(Clip(Clip::Set(t, s))),
|
||||||
|
_ => return None
|
||||||
|
})
|
||||||
|
}
|
||||||
0
crates/tek/src/tui/arranger_mode_h.rs
Normal file
0
crates/tek/src/tui/arranger_mode_h.rs
Normal file
274
crates/tek/src/tui/arranger_mode_v.rs
Normal file
274
crates/tek/src/tui/arranger_mode_v.rs
Normal file
|
|
@ -0,0 +1,274 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
pub struct ArrangerVColumnSeparator {
|
||||||
|
cols: Vec<(usize, usize)>,
|
||||||
|
scenes_w: u16,
|
||||||
|
sep_fg: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&ArrangerTui> for ArrangerVColumnSeparator {
|
||||||
|
fn from (state: &ArrangerTui) -> Self {
|
||||||
|
Self {
|
||||||
|
cols: track_widths(state.tracks()),
|
||||||
|
scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16,
|
||||||
|
sep_fg: TuiTheme::separator_fg(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render!(<Tui>|self: ArrangerVColumnSeparator|render(move|to: &mut TuiOutput|{
|
||||||
|
let style = Some(Style::default().fg(self.sep_fg));
|
||||||
|
Ok(for x in self.cols.iter().map(|col|col.1) {
|
||||||
|
let x = self.scenes_w + to.area().x() + x as u16;
|
||||||
|
for y in to.area().y()..to.area().y2() {
|
||||||
|
to.blit(&"▎", x, y, style);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
|
pub struct ArrangerVRowSeparator {
|
||||||
|
rows: Vec<(usize, usize)>,
|
||||||
|
sep_fg: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(&ArrangerTui, usize)> for ArrangerVRowSeparator {
|
||||||
|
fn from ((state, factor): (&ArrangerTui, usize)) -> Self {
|
||||||
|
Self {
|
||||||
|
rows: ArrangerScene::ppqs(state.scenes(), factor),
|
||||||
|
sep_fg: TuiTheme::separator_fg(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render!(<Tui>|self: ArrangerVRowSeparator|render(move|to: &mut TuiOutput|{
|
||||||
|
Ok(for y in self.rows.iter().map(|row|row.1) {
|
||||||
|
let y = to.area().y() + (y / PPQ) as u16 + 1;
|
||||||
|
if y >= to.buffer.area.height { break }
|
||||||
|
for x in to.area().x()..to.area().x2().saturating_sub(2) {
|
||||||
|
if x < to.buffer.area.x && y < to.buffer.area.y {
|
||||||
|
let cell = to.buffer.get_mut(x, y);
|
||||||
|
cell.modifier = Modifier::UNDERLINED;
|
||||||
|
cell.underline_color = self.sep_fg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
|
pub struct ArrangerVCursor {
|
||||||
|
cols: Vec<(usize, usize)>,
|
||||||
|
rows: Vec<(usize, usize)>,
|
||||||
|
focused: bool,
|
||||||
|
selected: ArrangerSelection,
|
||||||
|
scenes_w: u16,
|
||||||
|
header_h: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(&ArrangerTui, usize)> for ArrangerVCursor {
|
||||||
|
fn from ((state, factor): (&ArrangerTui, usize)) -> Self {
|
||||||
|
Self {
|
||||||
|
cols: track_widths(state.tracks()),
|
||||||
|
rows: ArrangerScene::ppqs(state.scenes(), factor),
|
||||||
|
focused: true,
|
||||||
|
selected: state.selected,
|
||||||
|
scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16,
|
||||||
|
header_h: 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render!(<Tui>|self: ArrangerVCursor|render(move|to: &mut TuiOutput|{
|
||||||
|
let area = to.area();
|
||||||
|
let focused = self.focused;
|
||||||
|
let selected = self.selected;
|
||||||
|
let get_track_area = |t: usize| [
|
||||||
|
self.scenes_w + area.x() + self.cols[t].1 as u16, area.y(),
|
||||||
|
self.cols[t].0 as u16, area.h(),
|
||||||
|
];
|
||||||
|
let get_scene_area = |s: usize| [
|
||||||
|
area.x(), self.header_h + area.y() + (self.rows[s].1 / PPQ) as u16,
|
||||||
|
area.w(), (self.rows[s].0 / PPQ) as u16
|
||||||
|
];
|
||||||
|
let get_clip_area = |t: usize, s: usize| [
|
||||||
|
self.scenes_w + area.x() + self.cols[t].1 as u16,
|
||||||
|
self.header_h + area.y() + (self.rows[s].1/PPQ) as u16,
|
||||||
|
self.cols[t].0 as u16,
|
||||||
|
(self.rows[s].0 / PPQ) as u16
|
||||||
|
];
|
||||||
|
let mut track_area: Option<[u16;4]> = None;
|
||||||
|
let mut scene_area: Option<[u16;4]> = None;
|
||||||
|
let mut clip_area: Option<[u16;4]> = None;
|
||||||
|
let area = match selected {
|
||||||
|
ArrangerSelection::Mix => area,
|
||||||
|
ArrangerSelection::Track(t) => {
|
||||||
|
track_area = Some(get_track_area(t));
|
||||||
|
area
|
||||||
|
},
|
||||||
|
ArrangerSelection::Scene(s) => {
|
||||||
|
scene_area = Some(get_scene_area(s));
|
||||||
|
area
|
||||||
|
},
|
||||||
|
ArrangerSelection::Clip(t, s) => {
|
||||||
|
track_area = Some(get_track_area(t));
|
||||||
|
scene_area = Some(get_scene_area(s));
|
||||||
|
clip_area = Some(get_clip_area(t, s));
|
||||||
|
area
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let bg = TuiTheme::border_bg();
|
||||||
|
if let Some([x, y, width, height]) = track_area {
|
||||||
|
to.fill_fg([x, y, 1, height], bg);
|
||||||
|
to.fill_fg([x + width, y, 1, height], bg);
|
||||||
|
}
|
||||||
|
if let Some([_, y, _, height]) = scene_area {
|
||||||
|
to.fill_ul([area.x(), y - 1, area.w(), 1], bg);
|
||||||
|
to.fill_ul([area.x(), y + height - 1, area.w(), 1], bg);
|
||||||
|
}
|
||||||
|
Ok(if focused {
|
||||||
|
to.render_in(if let Some(clip_area) = clip_area { clip_area }
|
||||||
|
else if let Some(track_area) = track_area { track_area.clip_h(self.header_h) }
|
||||||
|
else if let Some(scene_area) = scene_area { scene_area.clip_w(self.scenes_w) }
|
||||||
|
else { area.clip_w(self.scenes_w).clip_h(self.header_h) }, &CORNERS)?
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
|
pub struct ArrangerVHeader<'a> {
|
||||||
|
tracks: &'a Vec<ArrangerTrack>,
|
||||||
|
cols: Vec<(usize, usize)>,
|
||||||
|
focused: bool,
|
||||||
|
selected: ArrangerSelection,
|
||||||
|
scenes_w: u16,
|
||||||
|
header_h: u16,
|
||||||
|
timebase: &'a Arc<Timebase>,
|
||||||
|
current: &'a Arc<Moment>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a ArrangerTui> for ArrangerVHeader<'a> {
|
||||||
|
fn from (state: &'a ArrangerTui) -> Self {
|
||||||
|
Self {
|
||||||
|
tracks: &state.tracks,
|
||||||
|
cols: track_widths(state.tracks()),
|
||||||
|
focused: true,
|
||||||
|
selected: state.selected,
|
||||||
|
scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16,
|
||||||
|
header_h: 3,
|
||||||
|
timebase: state.clock().timebase(),
|
||||||
|
current: &state.clock().playhead,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render!(<Tui>|self: ArrangerVHeader<'a>|row!(
|
||||||
|
(track, w) in self.tracks.iter().zip(self.cols.iter().map(|col|col.0)) => {
|
||||||
|
// name and width of track
|
||||||
|
let name = track.name().read().unwrap();
|
||||||
|
let max_w = w.saturating_sub(1).min(name.len()).max(2);
|
||||||
|
let name = format!("▎{}", &name[0..max_w]);
|
||||||
|
let name = Tui::bold(true, Tui::fg(track.color.lightest.rgb, name));
|
||||||
|
// beats elapsed
|
||||||
|
let elapsed = if let Some((_, Some(phrase))) = track.player.play_phrase().as_ref() {
|
||||||
|
let length = phrase.read().unwrap().length;
|
||||||
|
let elapsed = track.player.pulses_since_start().unwrap();
|
||||||
|
let elapsed = self.timebase.format_beats_1_short(
|
||||||
|
(elapsed as usize % length) as f64
|
||||||
|
);
|
||||||
|
format!("▎+{elapsed:>}")
|
||||||
|
} else {
|
||||||
|
String::from("▎")
|
||||||
|
};
|
||||||
|
// beats until switchover
|
||||||
|
let until_next = track.player.next_phrase().as_ref().map(|(t, _)|{
|
||||||
|
let target = t.pulse.get();
|
||||||
|
let current = self.current.pulse.get();
|
||||||
|
if target > current {
|
||||||
|
let remaining = target - current;
|
||||||
|
format!("▎-{:>}", self.timebase.format_beats_0_short(remaining))
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
}).unwrap_or(String::from("▎"));
|
||||||
|
let timer = col!([until_next, elapsed]);
|
||||||
|
// name of active MIDI input
|
||||||
|
let _input = format!("▎>{}", track.player.midi_ins().get(0)
|
||||||
|
.map(|port|port.short_name())
|
||||||
|
.transpose()?
|
||||||
|
.unwrap_or("(none)".into()));
|
||||||
|
// name of active MIDI output
|
||||||
|
let _output = format!("▎<{}", track.player.midi_outs().get(0)
|
||||||
|
.map(|port|port.short_name())
|
||||||
|
.transpose()?
|
||||||
|
.unwrap_or("(none)".into()));
|
||||||
|
Tui::push_x(self.scenes_w,
|
||||||
|
Tui::bg(track.color().base.rgb,
|
||||||
|
Tui::min_xy(w as u16, self.header_h,
|
||||||
|
col!([name, timer]))))
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
pub struct ArrangerVContent<'a> {
|
||||||
|
size: &'a Measure<Tui>,
|
||||||
|
scenes: &'a Vec<ArrangerScene>,
|
||||||
|
tracks: &'a Vec<ArrangerTrack>,
|
||||||
|
rows: Vec<(usize, usize)>,
|
||||||
|
cols: Vec<(usize, usize)>,
|
||||||
|
header_h: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<(&'a ArrangerTui, usize)> for ArrangerVContent<'a> {
|
||||||
|
fn from ((state, factor): (&'a ArrangerTui, usize)) -> Self {
|
||||||
|
Self {
|
||||||
|
size: &state.size,
|
||||||
|
scenes: &state.scenes,
|
||||||
|
tracks: &state.tracks,
|
||||||
|
rows: ArrangerScene::ppqs(state.scenes(), factor),
|
||||||
|
cols: track_widths(state.tracks()),
|
||||||
|
header_h: 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render!(<Tui>|self: ArrangerVContent<'a>|Fixed::h(
|
||||||
|
(self.size.h() as u16).saturating_sub(self.header_h),
|
||||||
|
col!((scene, pulses) in self.scenes.iter().zip(self.rows.iter().map(|row|row.0)) => {
|
||||||
|
let height = 1.max((pulses / PPQ) as u16);
|
||||||
|
let playing = scene.is_playing(self.tracks);
|
||||||
|
Fixed::h(height, row!([
|
||||||
|
if playing { "▶ " } else { " " },
|
||||||
|
Tui::bold(true, scene.name.read().unwrap().as_str()),
|
||||||
|
row!((track, w) in self.cols.iter().map(|col|col.0).enumerate() => {
|
||||||
|
Fixed::wh(w as u16, height, Layers::new(move |add|{
|
||||||
|
let mut bg = TuiTheme::border_bg();
|
||||||
|
match (self.tracks.get(track), scene.clips.get(track)) {
|
||||||
|
(Some(track), Some(Some(phrase))) => {
|
||||||
|
let name = &(phrase as &Arc<RwLock<Phrase>>).read().unwrap().name;
|
||||||
|
let name = format!("{}", name);
|
||||||
|
let max_w = name.len().min((w as usize).saturating_sub(2));
|
||||||
|
let color = phrase.read().unwrap().color;
|
||||||
|
bg = color.dark.rgb;
|
||||||
|
if let Some((_, Some(ref playing))) = track.player.play_phrase() {
|
||||||
|
if *playing.read().unwrap() == *phrase.read().unwrap() {
|
||||||
|
bg = color.light.rgb
|
||||||
|
}
|
||||||
|
};
|
||||||
|
add(&Fixed::w(w as u16, Tui::push_x(1, &name.as_str()[0..max_w])))?;
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
//add(&Background(bg))
|
||||||
|
Ok(())
|
||||||
|
}))
|
||||||
|
})])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
));
|
||||||
|
|
||||||
|
fn track_widths (tracks: &[ArrangerTrack]) -> Vec<(usize, usize)> {
|
||||||
|
let mut widths = vec![];
|
||||||
|
let mut total = 0;
|
||||||
|
for track in tracks.iter() {
|
||||||
|
let width = track.width;
|
||||||
|
widths.push((width, total));
|
||||||
|
total += width;
|
||||||
|
}
|
||||||
|
widths.push((0, total));
|
||||||
|
widths
|
||||||
|
}
|
||||||
66
crates/tek/src/tui/arranger_scene.rs
Normal file
66
crates/tek/src/tui/arranger_scene.rs
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
use crate::*;
|
||||||
|
pub fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option<ArrangerCommand> {
|
||||||
|
use KeyCode::{Char, Up, Down, Right, Enter, Delete};
|
||||||
|
use ArrangerCommand as Cmd;
|
||||||
|
use ArrangerSelection as Select;
|
||||||
|
use ArrangerSceneCommand as Scene;
|
||||||
|
Some(match input.event() {
|
||||||
|
key_pat!(Char('w')) => Cmd::Select(if s > 0 { Select::Scene(s - 1) } else { Select::Mix }),
|
||||||
|
key_pat!(Char('s')) => Cmd::Select(Select::Scene(s + 1)),
|
||||||
|
key_pat!(Char('d')) => Cmd::Select(Select::Clip(0, s)),
|
||||||
|
key_pat!(Char(',')) => Cmd::Scene(Scene::Swap(s, s - 1)),
|
||||||
|
key_pat!(Char('.')) => Cmd::Scene(Scene::Swap(s, s + 1)),
|
||||||
|
key_pat!(Char('<')) => Cmd::Scene(Scene::Swap(s, s - 1)),
|
||||||
|
key_pat!(Char('>')) => Cmd::Scene(Scene::Swap(s, s + 1)),
|
||||||
|
key_pat!(Char('q')) => Cmd::Scene(Scene::Play(s)),
|
||||||
|
key_pat!(Delete) => Cmd::Scene(Scene::Delete(s)),
|
||||||
|
//key_pat!(Char('c')) => Cmd::Track(Scene::Color(s, ItemPalette::random())),
|
||||||
|
_ => return None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
impl HasScenes<ArrangerScene> for ArrangerTui {
|
||||||
|
fn scenes (&self) -> &Vec<ArrangerScene> {
|
||||||
|
&self.scenes
|
||||||
|
}
|
||||||
|
fn scenes_mut (&mut self) -> &mut Vec<ArrangerScene> {
|
||||||
|
&mut self.scenes
|
||||||
|
}
|
||||||
|
fn scene_add (&mut self, name: Option<&str>, color: Option<ItemPalette>)
|
||||||
|
-> Usually<&mut ArrangerScene>
|
||||||
|
{
|
||||||
|
let name = name.map_or_else(||self.scene_default_name(), |x|x.to_string());
|
||||||
|
let scene = ArrangerScene {
|
||||||
|
name: Arc::new(name.into()),
|
||||||
|
clips: vec![None;self.tracks().len()],
|
||||||
|
color: color.unwrap_or_else(||ItemPalette::random()),
|
||||||
|
};
|
||||||
|
self.scenes_mut().push(scene);
|
||||||
|
let index = self.scenes().len() - 1;
|
||||||
|
Ok(&mut self.scenes_mut()[index])
|
||||||
|
}
|
||||||
|
fn selected_scene (&self) -> Option<&ArrangerScene> {
|
||||||
|
self.selected.scene().map(|s|self.scenes().get(s)).flatten()
|
||||||
|
}
|
||||||
|
fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> {
|
||||||
|
self.selected.scene().map(|s|self.scenes_mut().get_mut(s)).flatten()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Default, Debug, Clone)] pub struct ArrangerScene {
|
||||||
|
/// Name of scene
|
||||||
|
pub(crate) name: Arc<RwLock<String>>,
|
||||||
|
/// Clips in scene, one per track
|
||||||
|
pub(crate) clips: Vec<Option<Arc<RwLock<Phrase>>>>,
|
||||||
|
/// Identifying color of scene
|
||||||
|
pub(crate) color: ItemPalette,
|
||||||
|
}
|
||||||
|
impl ArrangerSceneApi for ArrangerScene {
|
||||||
|
fn name (&self) -> &Arc<RwLock<String>> {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
fn clips (&self) -> &Vec<Option<Arc<RwLock<Phrase>>>> {
|
||||||
|
&self.clips
|
||||||
|
}
|
||||||
|
fn color (&self) -> ItemPalette {
|
||||||
|
self.color
|
||||||
|
}
|
||||||
|
}
|
||||||
70
crates/tek/src/tui/arranger_select.rs
Normal file
70
crates/tek/src/tui/arranger_select.rs
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
#[derive(PartialEq, Clone, Copy, Debug)]
|
||||||
|
/// Represents the current user selection in the arranger
|
||||||
|
pub enum ArrangerSelection {
|
||||||
|
/// The whole mix is selected
|
||||||
|
Mix,
|
||||||
|
/// A track is selected.
|
||||||
|
Track(usize),
|
||||||
|
/// A scene is selected.
|
||||||
|
Scene(usize),
|
||||||
|
/// A clip (track × scene) is selected.
|
||||||
|
Clip(usize, usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Focus identification methods
|
||||||
|
impl ArrangerSelection {
|
||||||
|
pub fn description <E: Engine> (
|
||||||
|
&self,
|
||||||
|
tracks: &Vec<ArrangerTrack>,
|
||||||
|
scenes: &Vec<ArrangerScene>,
|
||||||
|
) -> String {
|
||||||
|
format!("Selected: {}", match self {
|
||||||
|
Self::Mix => format!("Everything"),
|
||||||
|
Self::Track(t) => match tracks.get(*t) {
|
||||||
|
Some(track) => format!("T{t}: {}", &track.name.read().unwrap()),
|
||||||
|
None => format!("T??"),
|
||||||
|
},
|
||||||
|
Self::Scene(s) => match scenes.get(*s) {
|
||||||
|
Some(scene) => format!("S{s}: {}", &scene.name.read().unwrap()),
|
||||||
|
None => format!("S??"),
|
||||||
|
},
|
||||||
|
Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) {
|
||||||
|
(Some(_), Some(scene)) => match scene.clip(*t) {
|
||||||
|
Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name),
|
||||||
|
None => format!("T{t} S{s}: Empty")
|
||||||
|
},
|
||||||
|
_ => format!("T{t} S{s}: Empty"),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn is_mix (&self) -> bool {
|
||||||
|
match self { Self::Mix => true, _ => false }
|
||||||
|
}
|
||||||
|
pub fn is_track (&self) -> bool {
|
||||||
|
match self { Self::Track(_) => true, _ => false }
|
||||||
|
}
|
||||||
|
pub fn is_scene (&self) -> bool {
|
||||||
|
match self { Self::Scene(_) => true, _ => false }
|
||||||
|
}
|
||||||
|
pub fn is_clip (&self) -> bool {
|
||||||
|
match self { Self::Clip(_, _) => true, _ => false }
|
||||||
|
}
|
||||||
|
pub fn track (&self) -> Option<usize> {
|
||||||
|
use ArrangerSelection::*;
|
||||||
|
match self {
|
||||||
|
Clip(t, _) => Some(*t),
|
||||||
|
Track(t) => Some(*t),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn scene (&self) -> Option<usize> {
|
||||||
|
use ArrangerSelection::*;
|
||||||
|
match self {
|
||||||
|
Clip(_, s) => Some(*s),
|
||||||
|
Scene(s) => Some(*s),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
80
crates/tek/src/tui/arranger_track.rs
Normal file
80
crates/tek/src/tui/arranger_track.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
use crate::*;
|
||||||
|
pub fn to_arranger_track_command (input: &TuiInput, t: usize) -> Option<ArrangerCommand> {
|
||||||
|
use KeyCode::{Char, Down, Left, Right, Delete};
|
||||||
|
use ArrangerCommand as Cmd;
|
||||||
|
use ArrangerSelection as Select;
|
||||||
|
use ArrangerTrackCommand as Track;
|
||||||
|
Some(match input.event() {
|
||||||
|
key_pat!(Char('s')) => Cmd::Select(Select::Clip(t, 0)),
|
||||||
|
key_pat!(Char('a')) => Cmd::Select(if t > 0 { Select::Track(t - 1) } else { Select::Mix }),
|
||||||
|
key_pat!(Char('d')) => Cmd::Select(Select::Track(t + 1)),
|
||||||
|
key_pat!(Char(',')) => Cmd::Track(Track::Swap(t, t - 1)),
|
||||||
|
key_pat!(Char('.')) => Cmd::Track(Track::Swap(t, t + 1)),
|
||||||
|
key_pat!(Char('<')) => Cmd::Track(Track::Swap(t, t - 1)),
|
||||||
|
key_pat!(Char('>')) => Cmd::Track(Track::Swap(t, t + 1)),
|
||||||
|
key_pat!(Delete) => Cmd::Track(Track::Delete(t)),
|
||||||
|
//key_pat!(Char('c')) => Cmd::Track(Track::Color(t, ItemPalette::random())),
|
||||||
|
_ => return None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
impl HasTracks<ArrangerTrack> for ArrangerTui {
|
||||||
|
fn tracks (&self) -> &Vec<ArrangerTrack> {
|
||||||
|
&self.tracks
|
||||||
|
}
|
||||||
|
fn tracks_mut (&mut self) -> &mut Vec<ArrangerTrack> {
|
||||||
|
&mut self.tracks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ArrangerTracksApi<ArrangerTrack> for ArrangerTui {
|
||||||
|
fn track_add (&mut self, name: Option<&str>, color: Option<ItemPalette>)
|
||||||
|
-> Usually<&mut ArrangerTrack>
|
||||||
|
{
|
||||||
|
let name = name.map_or_else(||self.track_default_name(), |x|x.to_string());
|
||||||
|
let track = ArrangerTrack {
|
||||||
|
width: name.len() + 2,
|
||||||
|
name: Arc::new(name.into()),
|
||||||
|
color: color.unwrap_or_else(||ItemPalette::random()),
|
||||||
|
player: PhrasePlayerModel::from(&self.clock),
|
||||||
|
};
|
||||||
|
self.tracks_mut().push(track);
|
||||||
|
let index = self.tracks().len() - 1;
|
||||||
|
Ok(&mut self.tracks_mut()[index])
|
||||||
|
}
|
||||||
|
fn track_del (&mut self, index: usize) {
|
||||||
|
self.tracks_mut().remove(index);
|
||||||
|
for scene in self.scenes_mut().iter_mut() {
|
||||||
|
scene.clips.remove(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)] pub struct ArrangerTrack {
|
||||||
|
/// Name of track
|
||||||
|
pub(crate) name: Arc<RwLock<String>>,
|
||||||
|
/// Preferred width of track column
|
||||||
|
pub(crate) width: usize,
|
||||||
|
/// Identifying color of track
|
||||||
|
pub(crate) color: ItemPalette,
|
||||||
|
/// MIDI player state
|
||||||
|
pub(crate) player: PhrasePlayerModel,
|
||||||
|
}
|
||||||
|
has_clock!(|self:ArrangerTrack|self.player.clock());
|
||||||
|
has_player!(|self:ArrangerTrack|self.player);
|
||||||
|
impl ArrangerTrackApi for ArrangerTrack {
|
||||||
|
/// Name of track
|
||||||
|
fn name (&self) -> &Arc<RwLock<String>> {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
/// Preferred width of track column
|
||||||
|
fn width (&self) -> usize {
|
||||||
|
self.width
|
||||||
|
}
|
||||||
|
/// Preferred width of track column
|
||||||
|
fn width_mut (&mut self) -> &mut usize {
|
||||||
|
&mut self.width
|
||||||
|
}
|
||||||
|
/// Identifying color of track
|
||||||
|
fn color (&self) -> ItemPalette {
|
||||||
|
self.color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -86,112 +86,125 @@ render!(<Tui>|self: SequencerStatus|Fixed::h(2, lay!([
|
||||||
})),
|
})),
|
||||||
])));
|
])));
|
||||||
|
|
||||||
impl Bar for ArrangerStatus {
|
//impl Bar for ArrangerStatus {
|
||||||
type State = (ArrangerFocus, ArrangerSelection, bool);
|
//type State = (ArrangerFocus, ArrangerSelection, bool);
|
||||||
fn hotkey_fg () -> Color where Self: Sized {
|
//fn hotkey_fg () -> Color where Self: Sized {
|
||||||
TuiTheme::HOTKEY_FG
|
//TuiTheme::HOTKEY_FG
|
||||||
}
|
//}
|
||||||
fn update (&mut self, (focused, selected, entered): &Self::State) {
|
//fn update (&mut self, (focused, selected, entered): &Self::State) {
|
||||||
*self = match focused {
|
//*self = match focused {
|
||||||
//ArrangerFocus::Menu => { todo!() },
|
////ArrangerFocus::Menu => { todo!() },
|
||||||
ArrangerFocus::Transport(_) => ArrangerStatus::Transport,
|
//ArrangerFocus::Transport(_) => ArrangerStatus::Transport,
|
||||||
ArrangerFocus::Arranger => match selected {
|
//ArrangerFocus::Arranger => match selected {
|
||||||
ArrangerSelection::Mix => ArrangerStatus::ArrangerMix,
|
//ArrangerSelection::Mix => ArrangerStatus::ArrangerMix,
|
||||||
ArrangerSelection::Track(_) => ArrangerStatus::ArrangerTrack,
|
//ArrangerSelection::Track(_) => ArrangerStatus::ArrangerTrack,
|
||||||
ArrangerSelection::Scene(_) => ArrangerStatus::ArrangerScene,
|
//ArrangerSelection::Scene(_) => ArrangerStatus::ArrangerScene,
|
||||||
ArrangerSelection::Clip(_, _) => ArrangerStatus::ArrangerClip,
|
//ArrangerSelection::Clip(_, _) => ArrangerStatus::ArrangerClip,
|
||||||
},
|
//},
|
||||||
ArrangerFocus::Phrases => ArrangerStatus::PhrasePool,
|
//ArrangerFocus::Phrases => ArrangerStatus::PhrasePool,
|
||||||
ArrangerFocus::PhraseEditor => match entered {
|
//ArrangerFocus::PhraseEditor => match entered {
|
||||||
true => ArrangerStatus::PhraseEdit,
|
//true => ArrangerStatus::PhraseEdit,
|
||||||
false => ArrangerStatus::PhraseView,
|
//false => ArrangerStatus::PhraseView,
|
||||||
},
|
//},
|
||||||
}
|
//}
|
||||||
}
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
//render!(<Tui>|self: ArrangerStatus|{
|
||||||
|
|
||||||
|
//let label = match self {
|
||||||
|
//Self::Transport => "TRANSPORT",
|
||||||
|
//Self::ArrangerMix => "PROJECT",
|
||||||
|
//Self::ArrangerTrack => "TRACK",
|
||||||
|
//Self::ArrangerScene => "SCENE",
|
||||||
|
//Self::ArrangerClip => "CLIP",
|
||||||
|
//Self::PhrasePool => "SEQ LIST",
|
||||||
|
//Self::PhraseView => "VIEW SEQ",
|
||||||
|
//Self::PhraseEdit => "EDIT SEQ",
|
||||||
|
//};
|
||||||
|
|
||||||
|
//let status_bar_bg = TuiTheme::status_bar_bg();
|
||||||
|
|
||||||
|
//let mode_bg = TuiTheme::mode_bg();
|
||||||
|
//let mode_fg = TuiTheme::mode_fg();
|
||||||
|
//let mode = Tui::fg(mode_fg, Tui::bg(mode_bg, Tui::bold(true, format!(" {label} "))));
|
||||||
|
|
||||||
|
//let commands = match self {
|
||||||
|
//Self::ArrangerMix => Self::command(&[
|
||||||
|
//["", "c", "olor"],
|
||||||
|
//["", "<>", "resize"],
|
||||||
|
//["", "+-", "zoom"],
|
||||||
|
//["", "n", "ame/number"],
|
||||||
|
//["", "Enter", " stop all"],
|
||||||
|
//]),
|
||||||
|
//Self::ArrangerClip => Self::command(&[
|
||||||
|
//["", "g", "et"],
|
||||||
|
//["", "s", "et"],
|
||||||
|
//["", "a", "dd"],
|
||||||
|
//["", "i", "ns"],
|
||||||
|
//["", "d", "up"],
|
||||||
|
//["", "e", "dit"],
|
||||||
|
//["", "c", "olor"],
|
||||||
|
//["re", "n", "ame"],
|
||||||
|
//["", ",.", "select"],
|
||||||
|
//["", "Enter", " launch"],
|
||||||
|
//]),
|
||||||
|
//Self::ArrangerTrack => Self::command(&[
|
||||||
|
//["re", "n", "ame"],
|
||||||
|
//["", ",.", "resize"],
|
||||||
|
//["", "<>", "move"],
|
||||||
|
//["", "i", "nput"],
|
||||||
|
//["", "o", "utput"],
|
||||||
|
//["", "m", "ute"],
|
||||||
|
//["", "s", "olo"],
|
||||||
|
//["", "Del", "ete"],
|
||||||
|
//["", "Enter", " stop"],
|
||||||
|
//]),
|
||||||
|
//Self::ArrangerScene => Self::command(&[
|
||||||
|
//["re", "n", "ame"],
|
||||||
|
//["", "Del", "ete"],
|
||||||
|
//["", "Enter", " launch"],
|
||||||
|
//]),
|
||||||
|
//Self::PhrasePool => Self::command(&[
|
||||||
|
//["", "a", "ppend"],
|
||||||
|
//["", "i", "nsert"],
|
||||||
|
//["", "d", "uplicate"],
|
||||||
|
//["", "Del", "ete"],
|
||||||
|
//["", "c", "olor"],
|
||||||
|
//["re", "n", "ame"],
|
||||||
|
//["leng", "t", "h"],
|
||||||
|
//["", ",.", "move"],
|
||||||
|
//["", "+-", "resize view"],
|
||||||
|
//]),
|
||||||
|
//Self::PhraseView => Self::command(&[
|
||||||
|
//["", "enter", " edit"],
|
||||||
|
//["", "arrows/pgup/pgdn", " scroll"],
|
||||||
|
//["", "+=", "zoom"],
|
||||||
|
//]),
|
||||||
|
//Self::PhraseEdit => Self::command(&[
|
||||||
|
//["", "esc", " exit"],
|
||||||
|
//["", "a", "ppend"],
|
||||||
|
//["", "s", "et"],
|
||||||
|
//["", "][", "length"],
|
||||||
|
//["", "+-", "zoom"],
|
||||||
|
//]),
|
||||||
|
//_ => Self::command(&[])
|
||||||
|
//};
|
||||||
|
|
||||||
|
////let commands = commands.iter().reduce(String::new(), |s, (a, b, c)| format!("{s} {a}{b}{c}"));
|
||||||
|
//Tui::bg(status_bar_bg, Fill::w(row!([mode, commands])))
|
||||||
|
|
||||||
|
//});
|
||||||
|
|
||||||
|
/// Status bar for arranger app
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub enum ArrangerStatus {
|
||||||
|
Transport,
|
||||||
|
ArrangerMix,
|
||||||
|
ArrangerTrack,
|
||||||
|
ArrangerScene,
|
||||||
|
ArrangerClip,
|
||||||
|
PhrasePool,
|
||||||
|
PhraseView,
|
||||||
|
PhraseEdit,
|
||||||
}
|
}
|
||||||
|
|
||||||
render!(<Tui>|self: ArrangerStatus|{
|
|
||||||
|
|
||||||
let label = match self {
|
|
||||||
Self::Transport => "TRANSPORT",
|
|
||||||
Self::ArrangerMix => "PROJECT",
|
|
||||||
Self::ArrangerTrack => "TRACK",
|
|
||||||
Self::ArrangerScene => "SCENE",
|
|
||||||
Self::ArrangerClip => "CLIP",
|
|
||||||
Self::PhrasePool => "SEQ LIST",
|
|
||||||
Self::PhraseView => "VIEW SEQ",
|
|
||||||
Self::PhraseEdit => "EDIT SEQ",
|
|
||||||
};
|
|
||||||
|
|
||||||
let status_bar_bg = TuiTheme::status_bar_bg();
|
|
||||||
|
|
||||||
let mode_bg = TuiTheme::mode_bg();
|
|
||||||
let mode_fg = TuiTheme::mode_fg();
|
|
||||||
let mode = Tui::fg(mode_fg, Tui::bg(mode_bg, Tui::bold(true, format!(" {label} "))));
|
|
||||||
|
|
||||||
let commands = match self {
|
|
||||||
Self::ArrangerMix => Self::command(&[
|
|
||||||
["", "c", "olor"],
|
|
||||||
["", "<>", "resize"],
|
|
||||||
["", "+-", "zoom"],
|
|
||||||
["", "n", "ame/number"],
|
|
||||||
["", "Enter", " stop all"],
|
|
||||||
]),
|
|
||||||
Self::ArrangerClip => Self::command(&[
|
|
||||||
["", "g", "et"],
|
|
||||||
["", "s", "et"],
|
|
||||||
["", "a", "dd"],
|
|
||||||
["", "i", "ns"],
|
|
||||||
["", "d", "up"],
|
|
||||||
["", "e", "dit"],
|
|
||||||
["", "c", "olor"],
|
|
||||||
["re", "n", "ame"],
|
|
||||||
["", ",.", "select"],
|
|
||||||
["", "Enter", " launch"],
|
|
||||||
]),
|
|
||||||
Self::ArrangerTrack => Self::command(&[
|
|
||||||
["re", "n", "ame"],
|
|
||||||
["", ",.", "resize"],
|
|
||||||
["", "<>", "move"],
|
|
||||||
["", "i", "nput"],
|
|
||||||
["", "o", "utput"],
|
|
||||||
["", "m", "ute"],
|
|
||||||
["", "s", "olo"],
|
|
||||||
["", "Del", "ete"],
|
|
||||||
["", "Enter", " stop"],
|
|
||||||
]),
|
|
||||||
Self::ArrangerScene => Self::command(&[
|
|
||||||
["re", "n", "ame"],
|
|
||||||
["", "Del", "ete"],
|
|
||||||
["", "Enter", " launch"],
|
|
||||||
]),
|
|
||||||
Self::PhrasePool => Self::command(&[
|
|
||||||
["", "a", "ppend"],
|
|
||||||
["", "i", "nsert"],
|
|
||||||
["", "d", "uplicate"],
|
|
||||||
["", "Del", "ete"],
|
|
||||||
["", "c", "olor"],
|
|
||||||
["re", "n", "ame"],
|
|
||||||
["leng", "t", "h"],
|
|
||||||
["", ",.", "move"],
|
|
||||||
["", "+-", "resize view"],
|
|
||||||
]),
|
|
||||||
Self::PhraseView => Self::command(&[
|
|
||||||
["", "enter", " edit"],
|
|
||||||
["", "arrows/pgup/pgdn", " scroll"],
|
|
||||||
["", "+=", "zoom"],
|
|
||||||
]),
|
|
||||||
Self::PhraseEdit => Self::command(&[
|
|
||||||
["", "esc", " exit"],
|
|
||||||
["", "a", "ppend"],
|
|
||||||
["", "s", "et"],
|
|
||||||
["", "][", "length"],
|
|
||||||
["", "+-", "zoom"],
|
|
||||||
]),
|
|
||||||
_ => Self::command(&[])
|
|
||||||
};
|
|
||||||
|
|
||||||
//let commands = commands.iter().reduce(String::new(), |s, (a, b, c)| format!("{s} {a}{b}{c}"));
|
|
||||||
Tui::bg(status_bar_bg, Fill::w(row!([mode, commands])))
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue