mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 19:56:42 +01:00
1349 lines
48 KiB
Rust
1349 lines
48 KiB
Rust
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
|
||
}
|
||
}
|
||
}
|