flatten workspace into 1 crate

This commit is contained in:
🪞👃🪞 2024-12-29 00:10:30 +01:00
parent 7c4e1e2166
commit d926422c67
147 changed files with 66 additions and 126 deletions

View file

@ -0,0 +1,339 @@
use crate::*;
use ClockCommand::{Play, Pause};
use KeyCode::{Char, Delete, Tab, Up, Down, Left, Right};
#[derive(Clone, Debug)] pub enum ArrangerCommand {
History(isize),
Color(ItemPalette),
Clock(ClockCommand),
Scene(ArrangerSceneCommand),
Track(ArrangerTrackCommand),
Clip(ArrangerClipCommand),
Select(ArrangerSelection),
Zoom(usize),
Phrases(PoolCommand),
Editor(PhraseCommand),
StopAll,
Clear,
}
#[derive(Clone, Debug)]
pub enum ArrangerTrackCommand {
Add,
Delete(usize),
Stop(usize),
Swap(usize, usize),
SetSize(usize),
SetZoom(usize),
SetColor(usize, ItemPalette),
}
#[derive(Clone, Debug)]
pub enum ArrangerSceneCommand {
Enqueue(usize),
Add,
Delete(usize),
Swap(usize, usize),
SetSize(usize),
SetZoom(usize),
SetColor(usize, ItemPalette),
}
#[derive(Clone, Debug)]
pub enum ArrangerClipCommand {
Get(usize, usize),
Put(usize, usize, Option<Arc<RwLock<MidiClip>>>),
Enqueue(usize, usize),
Edit(Option<Arc<RwLock<MidiClip>>>),
SetLoop(usize, usize, bool),
SetColor(usize, usize, ItemPalette),
}
input_to_command!(ArrangerCommand: <Tui>|state: ArrangerTui, input|match input.event() {
key_pat!(Char('u')) => Self::History(-1),
key_pat!(Char('U')) => Self::History(1),
// TODO: k: toggle on-screen keyboard
key_pat!(Ctrl-Char('k')) => { todo!("keyboard") },
// Transport: Play/pause
key_pat!(Char(' ')) =>
Self::Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }),
// Transport: Play from start or rewind to start
key_pat!(Shift-Char(' ')) =>
Self::Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }),
key_pat!(Char('e')) =>
Self::Editor(PhraseCommand::Show(Some(state.phrases.phrase().clone()))),
key_pat!(Ctrl-Left) =>
Self::Scene(ArrangerSceneCommand::Add),
key_pat!(Ctrl-Char('t')) =>
Self::Track(ArrangerTrackCommand::Add),
// Tab: Toggle visibility of phrase pool column
key_pat!(Tab) =>
Self::Phrases(PoolCommand::Show(!state.phrases.visible)),
_ => {
use ArrangerCommand as Cmd;
use ArrangerSelection as Selected;
use ArrangerSceneCommand as Scene;
use ArrangerTrackCommand as Track;
use ArrangerClipCommand as Clip;
let t_len = state.tracks.len();
let s_len = state.scenes.len();
match state.selected() {
Selected::Clip(t, s) => match input.event() {
key_pat!(Char('g')) => Some(Cmd::Phrases(PoolCommand::Select(0))),
key_pat!(Char('q')) => Some(Cmd::Clip(Clip::Enqueue(t, s))),
key_pat!(Char(',')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
key_pat!(Char('.')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
key_pat!(Char('<')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
key_pat!(Char('>')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
key_pat!(Char('p')) => Some(Cmd::Clip(Clip::Put(t, s, Some(state.phrases.phrase().clone())))),
key_pat!(Char('l')) => Some(Cmd::Clip(ArrangerClipCommand::SetLoop(t, s, false))),
key_pat!(Delete) => Some(Cmd::Clip(Clip::Put(t, s, None))),
key_pat!(Up) => Some(Cmd::Select(
if s > 0 { Selected::Clip(t, s - 1) } else { Selected::Track(t) })),
key_pat!(Down) => Some(Cmd::Select(
Selected::Clip(t, (s + 1).min(s_len.saturating_sub(1))))),
key_pat!(Left) => Some(Cmd::Select(
if t > 0 { Selected::Clip(t - 1, s) } else { Selected::Scene(s) })),
key_pat!(Right) => Some(Cmd::Select(
Selected::Clip((t + 1).min(t_len.saturating_sub(1)), s))),
_ => None
},
Selected::Scene(s) => match input.event() {
key_pat!(Char(',')) => Some(Cmd::Scene(Scene::Swap(s, s - 1))),
key_pat!(Char('.')) => Some(Cmd::Scene(Scene::Swap(s, s + 1))),
key_pat!(Char('<')) => Some(Cmd::Scene(Scene::Swap(s, s - 1))),
key_pat!(Char('>')) => Some(Cmd::Scene(Scene::Swap(s, s + 1))),
key_pat!(Char('q')) => Some(Cmd::Scene(Scene::Enqueue(s))),
key_pat!(Delete) => Some(Cmd::Scene(Scene::Delete(s))),
key_pat!(Char('c')) => Some(Cmd::Scene(Scene::SetColor(s, ItemPalette::random()))),
key_pat!(Up) => Some(
Cmd::Select(if s > 0 { Selected::Scene(s - 1) } else { Selected::Mix })),
key_pat!(Down) => Some(
Cmd::Select(Selected::Scene((s + 1).min(s_len.saturating_sub(1))))),
key_pat!(Left) =>
return None,
key_pat!(Right) => Some(
Cmd::Select(Selected::Clip(0, s))),
_ => None
},
Selected::Track(t) => match input.event() {
key_pat!(Char(',')) => Some(Cmd::Track(Track::Swap(t, t - 1))),
key_pat!(Char('.')) => Some(Cmd::Track(Track::Swap(t, t + 1))),
key_pat!(Char('<')) => Some(Cmd::Track(Track::Swap(t, t - 1))),
key_pat!(Char('>')) => Some(Cmd::Track(Track::Swap(t, t + 1))),
key_pat!(Delete) => Some(Cmd::Track(Track::Delete(t))),
key_pat!(Char('c')) => Some(Cmd::Track(Track::SetColor(t, ItemPalette::random()))),
key_pat!(Up) =>
return None,
key_pat!(Down) => Some(
Cmd::Select(Selected::Clip(t, 0))),
key_pat!(Left) => Some(
Cmd::Select(if t > 0 { Selected::Track(t - 1) } else { Selected::Mix })),
key_pat!(Right) => Some(
Cmd::Select(Selected::Track((t + 1).min(t_len.saturating_sub(1))))),
_ => None
},
Selected::Mix => match input.event() {
key_pat!(Delete) => Some(Cmd::Clear),
key_pat!(Char('0')) => Some(Cmd::StopAll),
key_pat!(Char('c')) => Some(Cmd::Color(ItemPalette::random())),
key_pat!(Up) =>
return None,
key_pat!(Down) => Some(
Cmd::Select(Selected::Scene(0))),
key_pat!(Left) =>
return None,
key_pat!(Right) => Some(
Cmd::Select(Selected::Track(0))),
_ => None
},
}
}.or_else(||if let Some(command) = PhraseCommand::input_to_command(&state.editor, input) {
Some(Self::Editor(command))
} else if let Some(command) = PoolCommand::input_to_command(&state.phrases, input) {
Some(Self::Phrases(command))
} else {
None
})?
});
fn to_arrangement_command (state: &ArrangerTui, input: &TuiInput) -> Option<ArrangerCommand> {
use ArrangerCommand as Cmd;
use ArrangerSelection as Selected;
use ArrangerSceneCommand as Scene;
use ArrangerTrackCommand as Track;
use ArrangerClipCommand as Clip;
let t_len = state.tracks.len();
let s_len = state.scenes.len();
match state.selected() {
Selected::Clip(t, s) => match input.event() {
key_pat!(Char('g')) => Some(Cmd::Phrases(PoolCommand::Select(0))),
key_pat!(Char('q')) => Some(Cmd::Clip(Clip::Enqueue(t, s))),
key_pat!(Char(',')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
key_pat!(Char('.')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
key_pat!(Char('<')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
key_pat!(Char('>')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
key_pat!(Char('p')) => Some(Cmd::Clip(Clip::Put(t, s, Some(state.phrases.phrase().clone())))),
key_pat!(Char('l')) => Some(Cmd::Clip(ArrangerClipCommand::SetLoop(t, s, false))),
key_pat!(Delete) => Some(Cmd::Clip(Clip::Put(t, s, None))),
key_pat!(Up) => Some(Cmd::Select(
if s > 0 { Selected::Clip(t, s - 1) } else { Selected::Track(t) })),
key_pat!(Down) => Some(Cmd::Select(
Selected::Clip(t, (s + 1).min(s_len.saturating_sub(1))))),
key_pat!(Left) => Some(Cmd::Select(
if t > 0 { Selected::Clip(t - 1, s) } else { Selected::Scene(s) })),
key_pat!(Right) => Some(Cmd::Select(
Selected::Clip((t + 1).min(t_len.saturating_sub(1)), s))),
_ => None
},
Selected::Scene(s) => match input.event() {
key_pat!(Char(',')) => Some(Cmd::Scene(Scene::Swap(s, s - 1))),
key_pat!(Char('.')) => Some(Cmd::Scene(Scene::Swap(s, s + 1))),
key_pat!(Char('<')) => Some(Cmd::Scene(Scene::Swap(s, s - 1))),
key_pat!(Char('>')) => Some(Cmd::Scene(Scene::Swap(s, s + 1))),
key_pat!(Char('q')) => Some(Cmd::Scene(Scene::Enqueue(s))),
key_pat!(Delete) => Some(Cmd::Scene(Scene::Delete(s))),
key_pat!(Char('c')) => Some(Cmd::Scene(Scene::SetColor(s, ItemPalette::random()))),
key_pat!(Up) => Some(
Cmd::Select(if s > 0 { Selected::Scene(s - 1) } else { Selected::Mix })),
key_pat!(Down) => Some(
Cmd::Select(Selected::Scene((s + 1).min(s_len.saturating_sub(1))))),
key_pat!(Left) =>
None,
key_pat!(Right) => Some(
Cmd::Select(Selected::Clip(0, s))),
_ => None
},
Selected::Track(t) => match input.event() {
key_pat!(Char(',')) => Some(Cmd::Track(Track::Swap(t, t - 1))),
key_pat!(Char('.')) => Some(Cmd::Track(Track::Swap(t, t + 1))),
key_pat!(Char('<')) => Some(Cmd::Track(Track::Swap(t, t - 1))),
key_pat!(Char('>')) => Some(Cmd::Track(Track::Swap(t, t + 1))),
key_pat!(Delete) => Some(Cmd::Track(Track::Delete(t))),
key_pat!(Char('c')) => Some(Cmd::Track(Track::SetColor(t, ItemPalette::random()))),
key_pat!(Up) =>
None,
key_pat!(Down) => Some(
Cmd::Select(Selected::Clip(t, 0))),
key_pat!(Left) => Some(
Cmd::Select(if t > 0 { Selected::Track(t - 1) } else { Selected::Mix })),
key_pat!(Right) => Some(
Cmd::Select(Selected::Track((t + 1).min(t_len.saturating_sub(1))))),
_ => None
},
Selected::Mix => match input.event() {
key_pat!(Delete) => Some(Cmd::Clear),
key_pat!(Char('0')) => Some(Cmd::StopAll),
key_pat!(Char('c')) => Some(Cmd::Color(ItemPalette::random())),
key_pat!(Up) =>
None,
key_pat!(Down) => Some(
Cmd::Select(Selected::Scene(0))),
key_pat!(Left) =>
None,
key_pat!(Right) => Some(
Cmd::Select(Selected::Track(0))),
_ => None
},
}
}
command!(|self: ArrangerCommand, state: ArrangerTui|match self {
Self::Scene(cmd) => cmd.execute(state)?.map(Self::Scene),
Self::Track(cmd) => cmd.execute(state)?.map(Self::Track),
Self::Clip(cmd) => cmd.execute(state)?.map(Self::Clip),
Self::Editor(cmd) => cmd.execute(&mut state.editor)?.map(Self::Editor),
Self::Clock(cmd) => cmd.execute(state)?.map(Self::Clock),
Self::Zoom(_) => { todo!(); },
Self::Select(selected) => {
*state.selected_mut() = selected;
None
},
Self::Color(palette) => {
let old = state.color;
state.color = palette;
Some(Self::Color(old))
},
Self::Phrases(cmd) => {
let mut default = |cmd: PoolCommand|{
cmd.execute(&mut state.phrases).map(|x|x.map(Self::Phrases))
};
match cmd {
// autoselect: automatically load selected phrase in editor
PoolCommand::Select(_) => {
let undo = default(cmd)?;
state.editor.set_phrase(Some(state.phrases.phrase()));
undo
},
// reload phrase in editor to update color
PoolCommand::Phrase(PhrasePoolCommand::SetColor(index, _)) => {
let undo = default(cmd)?;
state.editor.set_phrase(Some(state.phrases.phrase()));
undo
},
_ => default(cmd)?
}
},
Self::History(_) => { todo!() },
Self::StopAll => {
for track in 0..state.tracks.len() {
state.tracks[track].player.enqueue_next(None);
}
None
},
Self::Clear => { todo!() },
});
command!(|self: ArrangerTrackCommand, state: ArrangerTui|match self {
Self::SetColor(index, color) => {
let old = state.tracks[index].color;
state.tracks[index].color = color;
Some(Self::SetColor(index, old))
},
Self::Stop(track) => {
state.tracks[track].player.enqueue_next(None);
None
},
_ => None
});
command!(|self: ArrangerSceneCommand, state: ArrangerTui|match self {
Self::Delete(index) => {
state.scene_del(index);
None
},
Self::SetColor(index, color) => {
let old = state.scenes[index].color;
state.scenes[index].color = color;
Some(Self::SetColor(index, old))
},
Self::Enqueue(scene) => {
for track in 0..state.tracks.len() {
state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref());
}
None
},
_ => None
});
command!(|self: ArrangerClipCommand, state: ArrangerTui|match self {
Self::Get(track, scene) => { todo!() },
Self::Put(track, scene, phrase) => {
let old = state.scenes[scene].clips[track].clone();
state.scenes[scene].clips[track] = phrase;
Some(Self::Put(track, scene, old))
},
Self::Enqueue(track, scene) => {
state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref());
None
},
_ => None
});

View file

@ -0,0 +1 @@
// TODO

View file

@ -0,0 +1,26 @@
use crate::*;
/// Display mode of arranger
#[derive(Clone, PartialEq)]
pub enum ArrangerMode {
/// Tracks are columns
V(usize),
/// Tracks are rows
H,
}
render!(<Tui>|self: ArrangerMode|{});
/// Arranger display mode can be cycled
impl ArrangerMode {
/// Cycle arranger display mode
pub fn 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()))
}

View file

@ -0,0 +1,91 @@
use crate::*;
impl ArrangerTui {
pub 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.push(scene);
let index = self.scenes.len() - 1;
Ok(&mut self.scenes[index])
}
pub fn scene_del (&mut self, index: usize) {
todo!("delete scene");
}
fn scene_default_name (&self) -> String {
format!("S{:3>}", self.scenes.len() + 1)
}
pub fn selected_scene (&self) -> Option<&ArrangerScene> {
self.selected.scene().and_then(|s|self.scenes.get(s))
}
pub fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> {
self.selected.scene().and_then(|s|self.scenes.get_mut(s))
}
}
#[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<MidiClip>>>>,
/// Identifying color of scene
pub(crate) color: ItemPalette,
}
impl ArrangerScene {
pub fn name (&self) -> &Arc<RwLock<String>> {
&self.name
}
pub fn clips (&self) -> &Vec<Option<Arc<RwLock<MidiClip>>>> {
&self.clips
}
pub fn color (&self) -> ItemPalette {
self.color
}
pub fn ppqs (scenes: &[Self], factor: usize) -> Vec<(usize, usize)> {
let mut total = 0;
if factor == 0 {
scenes.iter().map(|scene|{
let pulses = scene.pulses().max(PPQ);
total += pulses;
(pulses, total - pulses)
}).collect()
} else {
(0..=scenes.len()).map(|i|{
(factor*PPQ, factor*PPQ*i)
}).collect()
}
}
pub fn longest_name (scenes: &[Self]) -> usize {
scenes.iter().map(|s|s.name().read().unwrap().len()).fold(0, usize::max)
}
/// Returns the pulse length of the longest phrase in the scene
pub fn pulses (&self) -> usize {
self.clips().iter().fold(0, |a, p|{
a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0))
})
}
/// Returns true if all phrases in the scene are
/// currently playing on the given collection of tracks.
pub fn is_playing (&self, tracks: &[ArrangerTrack]) -> bool {
self.clips().iter().any(|clip|clip.is_some()) && self.clips().iter().enumerate()
.all(|(track_index, clip)|match clip {
Some(clip) => tracks
.get(track_index)
.map(|track|{
if let Some((_, Some(phrase))) = track.player().play_phrase() {
*phrase.read().unwrap() == *clip.read().unwrap()
} else {
false
}
})
.unwrap_or(false),
None => true
})
}
pub fn clip (&self, index: usize) -> Option<&Arc<RwLock<MidiClip>>> {
match self.clips().get(index) { Some(Some(clip)) => Some(clip), _ => None }
}
}

View file

@ -0,0 +1,60 @@
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 is_mix (&self) -> bool { matches!(self, Self::Mix) }
pub fn is_track (&self) -> bool { matches!(self, Self::Track(_)) }
pub fn is_scene (&self) -> bool { matches!(self, Self::Scene(_)) }
pub fn is_clip (&self) -> bool { matches!(self, Self::Clip(_, _)) }
pub fn description (
&self,
tracks: &[ArrangerTrack],
scenes: &[ArrangerScene],
) -> String {
format!("Selected: {}", match self {
Self::Mix => "Everything".to_string(),
Self::Track(t) => match tracks.get(*t) {
Some(track) => format!("T{t}: {}", &track.name.read().unwrap()),
None => "T??".into(),
},
Self::Scene(s) => match scenes.get(*s) {
Some(scene) => format!("S{s}: {}", &scene.name.read().unwrap()),
None => "S??".into(),
},
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 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
}
}
}

View file

@ -0,0 +1,111 @@
use crate::*;
impl ArrangerTui {
pub fn track_next_name (&self) -> String {
format!("T{}", self.tracks.len() + 1)
}
pub fn track_add (&mut self, name: Option<&str>, color: Option<ItemPalette>)
-> Usually<&mut ArrangerTrack>
{
let name = name.map_or_else(||self.track_next_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: MidiPlayer::from(&self.clock),
};
self.tracks.push(track);
let index = self.tracks.len() - 1;
Ok(&mut self.tracks[index])
}
pub fn track_del (&mut self, index: usize) {
self.tracks.remove(index);
for scene in self.scenes.iter_mut() {
scene.clips.remove(index);
}
}
}
#[derive(Debug)] pub struct ArrangerTrack {
/// Name of track
pub name: Arc<RwLock<String>>,
/// Preferred width of track column
pub width: usize,
/// Identifying color of track
pub color: ItemPalette,
/// MIDI player state
pub player: MidiPlayer,
}
has_clock!(|self:ArrangerTrack|self.player.clock());
has_player!(|self:ArrangerTrack|self.player);
impl ArrangerTrack {
pub fn widths (tracks: &[Self]) -> 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
}
pub fn with_widths (tracks: &[ArrangerTrack])
-> impl Iterator<Item = (usize, &ArrangerTrack, usize, usize)>
{
let mut x = 0;
tracks.iter().enumerate().map(move |(index, track)|{
let data = (index, track, x, x + track.width);
x += track.width;
data
})
}
/// Name of track
pub 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
pub fn color (&self) -> ItemPalette {
self.color
}
fn longest_name (tracks: &[Self]) -> usize {
tracks.iter().map(|s|s.name().read().unwrap().len()).fold(0, usize::max)
}
pub const MIN_WIDTH: usize = 6;
fn width_inc (&mut self) {
*self.width_mut() += 1;
}
fn width_dec (&mut self) {
if self.width() > Self::MIN_WIDTH {
*self.width_mut() -= 1;
}
}
}
/// Hosts the JACK callback for a collection of tracks
pub struct TracksAudio<'a>(
// Track collection
pub &'a mut [ArrangerTrack],
/// Note buffer
pub &'a mut Vec<u8>,
/// Note chunk buffer
pub &'a mut Vec<Vec<Vec<u8>>>,
);
impl Audio for TracksAudio<'_> {
#[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
let model = &mut self.0;
let note_buffer = &mut self.1;
let output_buffer = &mut self.2;
for track in model.iter_mut() {
if PlayerAudio(track.player_mut(), note_buffer, output_buffer).process(client, scope) == Control::Quit {
return Control::Quit
}
}
Control::Continue
}
}

View file

@ -0,0 +1,23 @@
use crate::*;
mod v_clips; pub(crate) use self::v_clips::*;
mod v_cursor; pub(crate) use self::v_cursor::*;
mod v_head; pub(crate) use self::v_head::*;
mod v_io; pub(crate) use self::v_io::*;
mod v_sep; pub(crate) use self::v_sep::*;
const HEADER_H: u16 = 5;
const SCENES_W_OFFSET: u16 = 3;
impl ArrangerTui {
pub fn render_mode_v (state: &ArrangerTui, factor: usize) -> impl Render<Tui> + use<'_> {
lay!([
ArrangerVColSep::from(state),
ArrangerVRowSep::from((state, factor)),
col!([
ArrangerVHead::from(state),
ArrangerVIns::from(state),
ArrangerVClips::from((state, factor)),
ArrangerVOuts::from(state),
]),
ArrangerVCursor::from((state, factor)),
])
}
}

View file

@ -0,0 +1,62 @@
use crate::*;
use super::*;
pub struct ArrangerVClips<'a> {
size: &'a Measure<Tui>,
scenes: &'a Vec<ArrangerScene>,
tracks: &'a Vec<ArrangerTrack>,
rows: Vec<(usize, usize)>,
}
from!(<'a>|args:(&'a ArrangerTui, usize)|ArrangerVClips<'a> = Self {
size: &args.0.size,
scenes: &args.0.scenes,
tracks: &args.0.tracks,
rows: ArrangerScene::ppqs(&args.0.scenes, args.1),
});
render!(<Tui>|self: ArrangerVClips<'a>|Fill::wh(
col!((scene, pulses) in self.scenes.iter().zip(self.rows.iter().map(|row|row.0)) => {
Self::format_scene(self.tracks, scene, pulses)
})
));
impl<'a> ArrangerVClips<'a> {
fn format_scene (
tracks: &'a [ArrangerTrack], scene: &'a ArrangerScene, pulses: usize
) -> impl Render<Tui> + use<'a> {
let height = 1.max((pulses / PPQ) as u16);
let playing = scene.is_playing(tracks);
Fixed::h(height, row!([
Tui::bg(scene.color.base.rgb,
if playing { "" } else { " " }),
Tui::fg_bg(scene.color.lightest.rgb, scene.color.base.rgb,
Tui::grow_x(1, Tui::bold(true, scene.name.read().unwrap().as_str()))),
row!((index, track, x1, x2) in ArrangerTrack::with_widths(tracks) => {
Self::format_clip(scene, index, track, (x2 - x1) as u16, height)
})])
)
}
fn format_clip (
scene: &'a ArrangerScene, index: usize, track: &'a ArrangerTrack, w: u16, h: u16
) -> impl Render<Tui> + use<'a> {
Fixed::wh(w, h, Layers::new(move |add|{
if let Some(Some(phrase)) = scene.clips.get(index) {
let mut bg = TuiTheme::border_bg();
let name = &(phrase as &Arc<RwLock<MidiClip>>).read().unwrap().name.to_string();
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::bg(bg,
Tui::push_x(1, Fixed::w(w, &name.as_str()[0..max_w])))
)?;
}
Ok(())
}))
}
}

View file

@ -0,0 +1,81 @@
use crate::*;
use super::*;
pub struct ArrangerVCursor {
cols: Vec<(usize, usize)>,
rows: Vec<(usize, usize)>,
color: ItemPalette,
reticle: Reticle,
selected: ArrangerSelection,
scenes_w: u16,
}
from!(|args:(&ArrangerTui, usize)|ArrangerVCursor = Self {
cols: ArrangerTrack::widths(&args.0.tracks),
rows: ArrangerScene::ppqs(&args.0.scenes, args.1),
selected: args.0.selected(),
scenes_w: SCENES_W_OFFSET + ArrangerScene::longest_name(&args.0.scenes) as u16,
color: args.0.color,
reticle: Reticle(Style {
fg: Some(args.0.color.lighter.rgb),
bg: None,
underline_color: None,
add_modifier: Modifier::empty(),
sub_modifier: Modifier::DIM
}),
});
render!(<Tui>|self: ArrangerVCursor|render(move|to: &mut TuiOutput|{
let area = to.area();
let focused = true;
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(), 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).saturating_sub(1),
HEADER_H + area.y() + (self.rows[s].1/PPQ) as u16,
self.cols[t].0 as u16 + 2,
(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 = self.color.lighter.rgb;//Color::Rgb(0, 255, 0);
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(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(HEADER_H) }, &self.reticle)?
})
}));

View file

@ -0,0 +1,83 @@
use crate::*;
use super::*;
pub struct ArrangerVHead<'a> {
scenes_w: u16,
timebase: &'a Arc<Timebase>,
current: &'a Arc<Moment>,
tracks: &'a [ArrangerTrack],
}
from!(<'a>|state: &'a ArrangerTui|ArrangerVHead<'a> = Self { // A
tracks: &state.tracks,
timebase: state.clock().timebase(),
current: &state.clock().playhead,
scenes_w: SCENES_W_OFFSET + ArrangerScene::longest_name(&state.scenes) as u16,
});
render!(<Tui>|self: ArrangerVHead<'a>|Tui::push_x(self.scenes_w, row!(
(_, track, x1, x2) in ArrangerTrack::with_widths(self.tracks) => {
let (w, h) = (ArrangerTrack::MIN_WIDTH.max(x2 - x1), HEADER_H);
let color = track.color();
fn row <T: Render<Tui>> (color: ItemPalette, field: &T) -> impl Render<Tui> + use<'_, T> {
row!([
Tui::fg(color.light.rgb, ""),
Tui::fg(color.lightest.rgb, field)
])
}
Tui::bg(color.base.rgb, Tui::min_xy(w as u16, h, Fixed::wh(w as u16, 5, col!([
row(color, &Self::format_name(track, w)),
row(color, &Self::format_input(track)?),
row(color, &Self::format_output(track)?),
row(color, &Self::format_elapsed(track, self.timebase)),
row(color, &Self::format_until_next(track, self.current)),
]))))
}
)));
impl ArrangerVHead<'_> {
/// name and width of track
fn format_name (track: &ArrangerTrack, _w: usize) -> impl Render<Tui> {
let name = track.name().read().unwrap().clone();
Tui::bold(true, Tui::fg(track.color.lightest.rgb, name))
}
/// input port
fn format_input (track: &ArrangerTrack) -> Usually<impl Render<Tui>> {
Ok(format!(">{}", track.player.midi_ins().first().map(|port|port.short_name())
.transpose()?.unwrap_or("?".into())))
}
/// output port
fn format_output (track: &ArrangerTrack) -> Usually<impl Render<Tui>> {
Ok(format!("<{}", track.player.midi_outs().first().map(|port|port.short_name())
.transpose()?.unwrap_or("?".into())))
}
/// beats elapsed
fn format_elapsed (track: &ArrangerTrack, timebase: &Arc<Timebase>) -> impl Render<Tui> {
let mut result = String::new();
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 = timebase.format_beats_1_short(
(elapsed as usize % length) as f64
);
result = format!("+{elapsed:>}")
}
result
}
/// beats until switchover
fn format_until_next (track: &ArrangerTrack, current: &Arc<Moment>)
-> Option<impl Render<Tui>>
{
let timebase = &current.timebase;
let mut result = String::new();
if let Some((t, _)) = track.player.next_phrase().as_ref() {
let target = t.pulse.get();
let current = current.pulse.get();
if target > current {
let remaining = target - current;
result = format!("-{:>}", timebase.format_beats_0_short(remaining))
}
}
Some(result)
}
}

View file

@ -0,0 +1,26 @@
use crate::*;
use super::*;
pub struct ArrangerVIns<'a> {
size: &'a Measure<Tui>,
tracks: &'a Vec<ArrangerTrack>,
}
from!(<'a>|args: &'a ArrangerTui|ArrangerVIns<'a> = Self {
size: &args.size,
tracks: &args.tracks,
});
render!(<Tui>|self: ArrangerVIns<'a>|());
pub struct ArrangerVOuts<'a> {
size: &'a Measure<Tui>,
tracks: &'a Vec<ArrangerTrack>,
}
from!(<'a>|args: &'a ArrangerTui|ArrangerVOuts<'a> = Self {
size: &args.size,
tracks: &args.tracks,
});
render!(<Tui>|self: ArrangerVOuts<'a>|());

View file

@ -0,0 +1,48 @@
use crate::*;
use super::*;
pub struct ArrangerVColSep {
fg: Color,
cols: Vec<(usize, usize)>,
scenes_w: u16
}
from!(|state:&ArrangerTui|ArrangerVColSep = Self {
fg: TuiTheme::separator_fg(false),
cols: ArrangerTrack::widths(&state.tracks),
scenes_w: SCENES_W_OFFSET + ArrangerScene::longest_name(&state.scenes) as u16,
});
render!(<Tui>|self: ArrangerVColSep|render(move|to: &mut TuiOutput|{
let style = Some(Style::default().fg(self.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 ArrangerVRowSep {
fg: Color,
rows: Vec<(usize, usize)>,
}
from!(|args:(&ArrangerTui, usize)|ArrangerVRowSep = Self {
fg: TuiTheme::separator_fg(false),
rows: ArrangerScene::ppqs(&args.0.scenes, args.1),
});
render!(<Tui>|self: ArrangerVRowSep|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.fg;
}
}
})
}));