tek/crates/tek/src/tui/app_arranger.rs

1349 lines
48 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use crate::*;
use crate::api::ArrangerTrackCommand;
use crate::api::ArrangerSceneCommand;
use crate::api::ArrangerClipCommand;
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 {
jack: jack.clone(),
clock: ClockModel::from(jack),
phrases: PhraseListModel::default(),
editor: PhraseEditorModel::default(),
selected: ArrangerSelection::Clip(0, 0),
scenes: vec![],
tracks: vec![],
color: TuiTheme::bg().into(),
history: vec![],
mode: ArrangerMode::Vertical(2),
name: Arc::new(RwLock::new(String::new())),
size: Measure::new(),
cursor: (0, 0),
splits: [20, 20],
entered: false,
menu_bar: None,
status_bar: None,
midi_buf: vec![vec![];65536],
note_buf: vec![],
perf: PerfModel::default(),
focus: FocusState::Entered(ArrangerFocus::Transport(TransportFocus::PlayPause)),
})
}
}
/// Root view for standalone `tek_arranger`
pub struct ArrangerTui {
pub jack: Arc<RwLock<JackClient>>,
pub clock: ClockModel,
pub phrases: PhraseListModel,
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 cursor: (usize, usize),
pub menu_bar: Option<MenuBar<Tui, Self, ArrangerCommand>>,
pub status_bar: Option<ArrangerStatus>,
pub history: Vec<ArrangerCommand>,
pub note_buf: Vec<u8>,
pub midi_buf: Vec<Vec<Vec<u8>>>,
pub editor: PhraseEditorModel,
pub focus: FocusState<ArrangerFocus>,
pub perf: PerfModel,
}
impl Handle<Tui> for ArrangerTui {
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
ArrangerCommand::execute_with_state(self, i)
}
}
#[derive(Clone, Debug)]
pub enum ArrangerCommand {
Focus(FocusCommand<ArrangerFocus>),
Undo,
Redo,
Clear,
Color(ItemColor),
Clock(ClockCommand),
Scene(ArrangerSceneCommand),
Track(ArrangerTrackCommand),
Clip(ArrangerClipCommand),
Select(ArrangerSelection),
Zoom(usize),
Phrases(PhrasesCommand),
Editor(PhraseCommand),
}
impl Command<ArrangerTui> for ArrangerCommand {
fn execute (self, state: &mut ArrangerTui) -> Perhaps<Self> {
use ArrangerCommand::*;
Ok(match self {
Focus(cmd) => cmd.execute(state)?.map(Focus),
Scene(cmd) => cmd.execute(state)?.map(Scene),
Track(cmd) => cmd.execute(state)?.map(Track),
Clip(cmd) => cmd.execute(state)?.map(Clip),
Phrases(cmd) => cmd.execute(&mut state.phrases)?.map(Phrases),
Editor(cmd) => cmd.execute(&mut state.editor)?.map(Editor),
Clock(cmd) => cmd.execute(state)?.map(Clock),
Zoom(_) => { todo!(); },
Select(selected) => {
*state.selected_mut() = selected;
None
},
_ => { todo!() }
})
}
}
impl Command<ArrangerTui> for ArrangerSceneCommand {
fn execute (self, _state: &mut ArrangerTui) -> Perhaps<Self> {
//todo!();
Ok(None)
}
}
impl Command<ArrangerTui> for ArrangerTrackCommand {
fn execute (self, _state: &mut ArrangerTui) -> Perhaps<Self> {
//todo!();
Ok(None)
}
}
impl Command<ArrangerTui> for ArrangerClipCommand {
fn execute (self, _state: &mut ArrangerTui) -> Perhaps<Self> {
//todo!();
Ok(None)
}
}
pub trait ArrangerControl: TransportControl<ArrangerFocus> {
fn selected (&self) -> ArrangerSelection;
fn selected_mut (&mut self) -> &mut ArrangerSelection;
fn activate (&mut self) -> Usually<()>;
fn selected_phrase (&self) -> Option<Arc<RwLock<Phrase>>>;
fn toggle_loop (&mut self);
fn randomize_color (&mut self);
}
impl ArrangerControl for ArrangerTui {
fn selected (&self) -> ArrangerSelection {
self.selected
}
fn selected_mut (&mut self) -> &mut ArrangerSelection {
&mut self.selected
}
fn activate (&mut self) -> Usually<()> {
if let ArrangerSelection::Scene(s) = self.selected {
for (t, track) in self.tracks.iter_mut().enumerate() {
let phrase = self.scenes[s].clips[t].clone();
if track.player.play_phrase.is_some() || phrase.is_some() {
track.player.enqueue_next(phrase.as_ref());
}
}
if self.clock().is_stopped() {
self.clock().play_from(Some(0))?;
}
} else if let ArrangerSelection::Clip(t, s) = self.selected {
let phrase = self.scenes()[s].clips[t].clone();
self.tracks_mut()[t].player.enqueue_next(phrase.as_ref());
};
Ok(())
}
fn selected_phrase (&self) -> Option<Arc<RwLock<Phrase>>> {
self.selected_scene()?.clips.get(self.selected.track()?)?.clone()
}
fn toggle_loop (&mut self) {
if let Some(phrase) = self.selected_phrase() {
phrase.write().unwrap().toggle_loop()
}
}
fn randomize_color (&mut self) {
match self.selected {
ArrangerSelection::Mix => {
self.color = ItemColor::random_dark()
},
ArrangerSelection::Track(t) => {
self.tracks_mut()[t].color = ItemColor::random()
},
ArrangerSelection::Scene(s) => {
self.scenes_mut()[s].color = ItemColor::random()
},
ArrangerSelection::Clip(t, s) => {
if let Some(phrase) = &self.scenes_mut()[s].clips[t] {
phrase.write().unwrap().color = ItemPalette::random();
}
}
}
}
}
impl InputToCommand<Tui, ArrangerTui> for ArrangerCommand {
fn input_to_command (state: &ArrangerTui, input: &TuiInput) -> Option<Self> {
to_arranger_command(state, input)
.or_else(||to_focus_command(input).map(ArrangerCommand::Focus))
}
}
fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option<ArrangerCommand> {
use ArrangerCommand as Cmd;
use KeyCode::Char;
if !state.entered() {
return None
}
Some(match input.event() {
key_pat!(Char('e')) => Cmd::Editor(PhraseCommand::Show(Some(
state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone()
))),
// WSAD navigation, Q launches, E edits, PgUp/Down pool, Arrows editor
_ => match state.focused() {
ArrangerFocus::Transport(_) => {
match to_transport_command(state, input)? {
TransportCommand::Clock(command) => Cmd::Clock(command),
_ => return None,
}
},
ArrangerFocus::PhraseEditor => {
Cmd::Editor(PhraseCommand::input_to_command(&state.editor, input)?)
},
ArrangerFocus::Phrases => {
Cmd::Phrases(PhrasesCommand::input_to_command(&state.phrases, input)?)
},
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)?,
}
}
}
}
})
}
fn to_arranger_mix_command (input: &TuiInput) -> Option<ArrangerCommand> {
use KeyCode::{Char, Down, Right, Delete};
use ArrangerCommand as Cmd;
use ArrangerSelection as Select;
Some(match input.event() {
key_pat!(Down) => Cmd::Select(Select::Scene(0)),
key_pat!(Right) => 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!(Delete) => Cmd::Clear,
key_pat!(Char('c')) => Cmd::Color(ItemColor::random()),
_ => 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, ItemColor::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, ItemColor::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, ItemColor::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
})
}
impl TransportControl<ArrangerFocus> for ArrangerTui {
fn transport_focused (&self) -> Option<TransportFocus> {
match self.focus.inner() {
ArrangerFocus::Transport(focus) => Some(focus),
_ => None
}
}
}
impl Audio for ArrangerTui {
#[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
// Start profiling cycle
let t0 = self.perf.get_t0();
// Update transport clock
if ClockAudio(self).process(client, scope) == Control::Quit {
return Control::Quit
}
// Update MIDI sequencers
if TracksAudio(
&mut self.tracks,
&mut self.note_buf,
&mut self.midi_buf,
Default::default(),
).process(client, scope) == Control::Quit {
return Control::Quit
}
// FIXME: one of these per playing track
//self.now.set(0.);
//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.play_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.now.set(now);
//}
//}
//}
//}
//}
// End profiling cycle
self.perf.update(t0, scope);
return Control::Continue
}
}
// Layout for standalone arranger app.
render!(|self: ArrangerTui|{
let arranger_focused = self.arranger_focused();
let border = Lozenge(Style::default().bg(TuiTheme::border_bg()).fg(TuiTheme::border_fg(arranger_focused)));
let transport_focused = if let ArrangerFocus::Transport(_) = self.focus.inner() {
true
} else {
false
};
col!([
TransportView::from((self, None, transport_focused)),
col!([
Tui::fixed_y(self.splits[0], lay!([
border.wrap(Tui::grow_y(1, Layers::new(move |add|{
match self.mode {
ArrangerMode::Horizontal =>
add(&arranger_content_horizontal(self))?,
ArrangerMode::Vertical(factor) =>
add(&arranger_content_vertical(self, factor))?
};
add(&self.size)
}))),
self.size,
Tui::push_x(1, Tui::fg(
TuiTheme::title_fg(arranger_focused),
format!("[{}] Arranger", if self.entered {
""
} else {
" "
})
))
])),
Split::right(
false,
self.splits[1],
PhraseListView::from(self),
&self.editor,
)
])
])
});
impl HasClock for ArrangerTui {
fn clock (&self) -> &ClockModel {
&self.clock
}
}
impl HasClock for ArrangerTrack {
fn clock (&self) -> &ClockModel {
&self.player.clock()
}
}
impl HasPhrases for ArrangerTui {
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>> {
&self.phrases.phrases
}
fn phrases_mut (&mut self) -> &mut Vec<Arc<RwLock<Phrase>>> {
&mut self.phrases.phrases
}
}
impl HasPhraseList for ArrangerTui {
fn phrases_focused (&self) -> bool {
self.focused() == ArrangerFocus::Phrases
}
fn phrases_entered (&self) -> bool {
self.entered() && self.phrases_focused()
}
fn phrases_mode (&self) -> &Option<PhraseListMode> {
&self.phrases.mode
}
fn phrase_index (&self) -> usize {
self.phrases.phrase.load(Ordering::Relaxed)
}
}
impl HasEditor for ArrangerTui {
fn editor (&self) -> &PhraseEditorModel {
&self.editor
}
fn editor_focused (&self) -> bool {
self.focused() == ArrangerFocus::PhraseEditor
}
fn editor_entered (&self) -> bool {
self.entered() && self.editor_focused()
}
}
/// 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 [
//&[
//Menu,
//Menu,
//Menu,
//Menu,
//Menu,
//],
&[
Transport(TransportFocus::PlayPause),
Transport(TransportFocus::Bpm),
Transport(TransportFocus::Sync),
Transport(TransportFocus::Quant),
Transport(TransportFocus::Clock),
], &[
Arranger,
Arranger,
Arranger,
Arranger,
Arranger,
], &[
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,
}
impl StatusBar for ArrangerStatus {
type State = (ArrangerFocus, ArrangerSelection, bool);
fn hotkey_fg () -> Color where Self: Sized {
TuiTheme::HOTKEY_FG
}
fn update (&mut self, (focused, selected, entered): &Self::State) {
*self = match focused {
//ArrangerFocus::Menu => { todo!() },
ArrangerFocus::Transport(_) => ArrangerStatus::Transport,
ArrangerFocus::Arranger => match selected {
ArrangerSelection::Mix => ArrangerStatus::ArrangerMix,
ArrangerSelection::Track(_) => ArrangerStatus::ArrangerTrack,
ArrangerSelection::Scene(_) => ArrangerStatus::ArrangerScene,
ArrangerSelection::Clip(_, _) => ArrangerStatus::ArrangerClip,
},
ArrangerFocus::Phrases => ArrangerStatus::PhrasePool,
ArrangerFocus::PhraseEditor => match entered {
true => ArrangerStatus::PhraseEdit,
false => ArrangerStatus::PhraseView,
},
}
}
}
render!(|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, Tui::fill_x(row!([mode, commands])))
});
/// Display mode of arranger
#[derive(Clone, PartialEq)]
pub enum ArrangerMode {
/// Tracks are rows
Horizontal,
/// Tracks are columns
Vertical(usize),
}
/// Arranger display mode can be cycled
impl ArrangerMode {
/// Cycle arranger display mode
pub fn to_next (&mut self) {
*self = match self {
Self::Horizontal => Self::Vertical(1),
Self::Vertical(1) => Self::Vertical(2),
Self::Vertical(2) => Self::Vertical(2),
Self::Vertical(0) => Self::Horizontal,
Self::Vertical(_) => Self::Vertical(0),
}
}
}
pub trait ArrangerViewState {
fn arranger_focused (&self) -> bool;
}
impl ArrangerViewState for ArrangerTui {
fn arranger_focused (&self) -> bool {
self.focused() == ArrangerFocus::Arranger
}
}
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()))
}
pub fn arranger_content_vertical (
view: &ArrangerTui,
factor: usize
) -> impl Render<Tui> + use<'_> {
lay!([
Tui::at_se(Tui::fill_xy(Tui::pull_x(1, Tui::fg(TuiTheme::title_fg(view.arranger_focused()),
format!("{}x{}", view.size.w(), view.size.h()))
))),
Tui::bg(view.color.rgb, lay!(![
ArrangerVerticalColumnSeparator::from(view),
ArrangerVerticalRowSeparator::from((view, factor)),
col!(![
ArrangerVerticalHeader::from(view),
ArrangerVerticalContent::from((view, factor)),
]),
ArrangerVerticalCursor::from((view, factor)),
])),
])
}
struct ArrangerVerticalColumnSeparator {
cols: Vec<(usize, usize)>,
scenes_w: u16,
sep_fg: Color,
}
impl From<&ArrangerTui> for ArrangerVerticalColumnSeparator {
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!(|self: ArrangerVerticalColumnSeparator|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 ArrangerVerticalRowSeparator {
rows: Vec<(usize, usize)>,
sep_fg: Color,
}
impl From<(&ArrangerTui, usize)> for ArrangerVerticalRowSeparator {
fn from ((state, factor): (&ArrangerTui, usize)) -> Self {
Self {
rows: ArrangerScene::ppqs(state.scenes(), factor),
sep_fg: TuiTheme::separator_fg(false),
}
}
}
render!(|self: ArrangerVerticalRowSeparator|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 ArrangerVerticalCursor {
cols: Vec<(usize, usize)>,
rows: Vec<(usize, usize)>,
focused: bool,
selected: ArrangerSelection,
scenes_w: u16,
header_h: u16,
}
impl From<(&ArrangerTui, usize)> for ArrangerVerticalCursor {
fn from ((state, factor): (&ArrangerTui, usize)) -> Self {
Self {
cols: track_widths(state.tracks()),
rows: ArrangerScene::ppqs(state.scenes(), factor),
focused: state.arranger_focused(),
selected: state.selected,
scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16,
header_h: 3,
}
}
}
render!(|self: ArrangerVerticalCursor|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 ArrangerVerticalHeader<'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 ArrangerVerticalHeader<'a> {
fn from (state: &'a ArrangerTui) -> Self {
Self {
tracks: &state.tracks,
cols: track_widths(state.tracks()),
focused: state.arranger_focused(),
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!(|self: ArrangerVerticalHeader<'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, 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().rgb,
Tui::min_xy(w as u16, self.header_h,
col!([name, timer]))))
}
));
struct ArrangerVerticalContent<'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 ArrangerVerticalContent<'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!(|self: ArrangerVerticalContent<'a>|Tui::fixed_y(
(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);
Tui::fixed_y(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() => {
Tui::fixed_xy(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(&Tui::fixed_x(w as u16, Tui::push_x(1, &name.as_str()[0..max_w])))?;
},
_ => {}
};
//add(&Background(bg))
Ok(())
}))
})])
)
})
));
pub fn arranger_content_horizontal (
view: &ArrangerTui,
) -> impl Render<Tui> + use<'_> {
todo!()
}
//let focused = view.arranger_focused();
//let _tracks = view.tracks();
//lay!(
//focused.then_some(Background(TuiTheme::border_bg())),
//row!(
//// name
//Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{
//todo!()
////let Self(tracks, selected) = self;
////let yellow = Some(Style::default().yellow().bold().not_dim());
////let white = Some(Style::default().white().bold().not_dim());
////let area = to.area();
////let area = [area.x(), area.y(), 3 + 5.max(track_name_max_len(tracks)) as u16, area.h()];
////let offset = 0; // track scroll offset
////for y in 0..area.h() {
////if y == 0 {
////to.blit(&"Mixer", area.x() + 1, area.y() + y, Some(DIM))?;
////} else if y % 2 == 0 {
////let index = (y as usize - 2) / 2 + offset;
////if let Some(track) = tracks.get(index) {
////let selected = selected.track() == Some(index);
////let style = if selected { yellow } else { white };
////to.blit(&format!(" {index:>02} "), area.x(), area.y() + y, style)?;
////to.blit(&*track.name.read().unwrap(), area.x() + 4, area.y() + y, style)?;
////}
////}
////}
////Ok(Some(area))
//}),
//// monitor
//Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{
//todo!()
////let Self(tracks) = self;
////let mut area = to.area();
////let on = Some(Style::default().not_dim().green().bold());
////let off = Some(DIM);
////area.x += 1;
////for y in 0..area.h() {
////if y == 0 {
//////" MON ".blit(to.buffer, area.x, area.y + y, style2)?;
////} else if y % 2 == 0 {
////let index = (y as usize - 2) / 2;
////if let Some(track) = tracks.get(index) {
////let style = if track.monitoring { on } else { off };
////to.blit(&" MON ", area.x(), area.y() + y, style)?;
////} else {
////area.height = y;
////break
////}
////}
////}
////area.width = 4;
////Ok(Some(area))
//}),
//// record
//Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{
//todo!()
////let Self(tracks) = self;
////let mut area = to.area();
////let on = Some(Style::default().not_dim().red().bold());
////let off = Some(Style::default().dim());
////area.x += 1;
////for y in 0..area.h() {
////if y == 0 {
//////" REC ".blit(to.buffer, area.x, area.y + y, style2)?;
////} else if y % 2 == 0 {
////let index = (y as usize - 2) / 2;
////if let Some(track) = tracks.get(index) {
////let style = if track.recording { on } else { off };
////to.blit(&" REC ", area.x(), area.y() + y, style)?;
////} else {
////area.height = y;
////break
////}
////}
////}
////area.width = 4;
////Ok(Some(area))
//}),
//// overdub
//Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{
//todo!()
////let Self(tracks) = self;
////let mut area = to.area();
////let on = Some(Style::default().not_dim().yellow().bold());
////let off = Some(Style::default().dim());
////area.x = area.x + 1;
////for y in 0..area.h() {
////if y == 0 {
//////" OVR ".blit(to.buffer, area.x, area.y + y, style2)?;
////} else if y % 2 == 0 {
////let index = (y as usize - 2) / 2;
////if let Some(track) = tracks.get(index) {
////to.blit(&" OVR ", area.x(), area.y() + y, if track.overdub {
////on
////} else {
////off
////})?;
////} else {
////area.height = y;
////break
////}
////}
////}
////area.width = 4;
////Ok(Some(area))
//}),
//// erase
//Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{
//todo!()
////let Self(tracks) = self;
////let mut area = to.area();
////let off = Some(Style::default().dim());
////area.x = area.x + 1;
////for y in 0..area.h() {
////if y == 0 {
//////" DEL ".blit(to.buffer, area.x, area.y + y, style2)?;
////} else if y % 2 == 0 {
////let index = (y as usize - 2) / 2;
////if let Some(_) = tracks.get(index) {
////to.blit(&" DEL ", area.x(), area.y() + y, off)?;
////} else {
////area.height = y;
////break
////}
////}
////}
////area.width = 4;
////Ok(Some(area))
//}),
//// gain
//Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{
//todo!()
////let Self(tracks) = self;
////let mut area = to.area();
////let off = Some(Style::default().dim());
////area.x = area.x() + 1;
////for y in 0..area.h() {
////if y == 0 {
//////" GAIN ".blit(to.buffer, area.x, area.y + y, style2)?;
////} else if y % 2 == 0 {
////let index = (y as usize - 2) / 2;
////if let Some(_) = tracks.get(index) {
////to.blit(&" +0.0 ", area.x(), area.y() + y, off)?;
////} else {
////area.height = y;
////break
////}
////}
////}
////area.width = 7;
////Ok(Some(area))
//}),
//// scenes
//Widget::new(|_|{todo!()}, |to: &mut TuiOutput|{
//let [x, y, _, height] = to.area();
//let mut x2 = 0;
//Ok(for (scene_index, scene) in view.scenes().iter().enumerate() {
//let active_scene = view.selected.scene() == Some(scene_index);
//let sep = Some(if active_scene {
//Style::default().yellow().not_dim()
//} else {
//Style::default().dim()
//});
//for y in y+1..y+height {
//to.blit(&"│", x + x2, y, sep);
//}
//let name = scene.name.read().unwrap();
//let mut x3 = name.len() as u16;
//to.blit(&*name, x + x2, y, sep);
//for (i, clip) in scene.clips.iter().enumerate() {
//let active_track = view.selected.track() == Some(i);
//if let Some(clip) = clip {
//let y2 = y + 2 + i as u16 * 2;
//let label = format!("{}", clip.read().unwrap().name);
//to.blit(&label, x + x2, y2, Some(if active_track && active_scene {
//Style::default().not_dim().yellow().bold()
//} else {
//Style::default().not_dim()
//}));
//x3 = x3.max(label.len() as u16)
//}
//}
//x2 = x2 + x3 + 1;
//})
//}),
//)
//)
//}
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<ItemColor>)
-> 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(||ItemColor::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: ItemColor,
}
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) -> ItemColor {
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<ItemColor>)
-> 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(||ItemColor::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: ItemColor,
/// MIDI player state
pub(crate) player: PhrasePlayerModel,
}
impl HasPlayer for ArrangerTrack {
fn player (&self) -> &impl MidiPlayerApi {
&self.player
}
fn player_mut (&mut self) -> &mut impl MidiPlayerApi {
&mut 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) -> ItemColor {
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
}
}
}