mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-07 04:06:45 +01:00
Phrase -> Clip in all remaining places
This commit is contained in:
parent
06b643e2b1
commit
bb52555183
15 changed files with 916 additions and 936 deletions
|
|
@ -9,7 +9,7 @@ pub trait HasMidiIns {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait MidiRecordApi: HasClock + HasPlayPhrase + HasMidiIns {
|
pub trait MidiRecordApi: HasClock + HasPlayClip + HasMidiIns {
|
||||||
fn notes_in (&self) -> &Arc<RwLock<[bool;128]>>;
|
fn notes_in (&self) -> &Arc<RwLock<[bool;128]>>;
|
||||||
|
|
||||||
fn recording (&self) -> bool;
|
fn recording (&self) -> bool;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
pub trait HasPlayPhrase: HasClock {
|
pub trait HasPlayClip: HasClock {
|
||||||
fn reset (&self) -> bool;
|
fn reset (&self) -> bool;
|
||||||
fn reset_mut (&mut self) -> &mut bool;
|
fn reset_mut (&mut self) -> &mut bool;
|
||||||
fn play_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
fn play_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ pub trait HasMidiOuts {
|
||||||
fn midi_note (&mut self) -> &mut Vec<u8>;
|
fn midi_note (&mut self) -> &mut Vec<u8>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait MidiPlaybackApi: HasPlayPhrase + HasClock + HasMidiOuts {
|
pub trait MidiPlaybackApi: HasPlayClip + HasClock + HasMidiOuts {
|
||||||
|
|
||||||
fn notes_out (&self) -> &Arc<RwLock<[bool;128]>>;
|
fn notes_out (&self) -> &Arc<RwLock<[bool;128]>>;
|
||||||
|
|
||||||
|
|
@ -117,7 +117,7 @@ pub trait MidiPlaybackApi: HasPlayPhrase + HasClock + HasMidiOuts {
|
||||||
) {
|
) {
|
||||||
// Source clip from which the MIDI events will be taken.
|
// Source clip from which the MIDI events will be taken.
|
||||||
let clip = clip.read().unwrap();
|
let clip = clip.read().unwrap();
|
||||||
// Phrase with zero length is not processed
|
// Clip with zero length is not processed
|
||||||
if clip.length > 0 {
|
if clip.length > 0 {
|
||||||
// Current pulse index in source clip
|
// Current pulse index in source clip
|
||||||
let pulse = pulse % clip.length;
|
let pulse = pulse % clip.length;
|
||||||
|
|
|
||||||
|
|
@ -186,7 +186,7 @@ impl MidiPlaybackApi for MidiPlayer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HasPlayPhrase for MidiPlayer {
|
impl HasPlayClip for MidiPlayer {
|
||||||
fn reset (&self) -> bool {
|
fn reset (&self) -> bool {
|
||||||
self.reset
|
self.reset
|
||||||
}
|
}
|
||||||
|
|
@ -212,9 +212,9 @@ impl HasPlayPhrase for MidiPlayer {
|
||||||
///// Global timebase
|
///// Global timebase
|
||||||
//pub clock: Arc<Clock>,
|
//pub clock: Arc<Clock>,
|
||||||
///// Start time and clip being played
|
///// Start time and clip being played
|
||||||
//pub play_clip: Option<(Moment, Option<Arc<RwLock<Phrase>>>)>,
|
//pub play_clip: Option<(Moment, Option<Arc<RwLock<Clip>>>)>,
|
||||||
///// Start time and next clip
|
///// Start time and next clip
|
||||||
//pub next_clip: Option<(Moment, Option<Arc<RwLock<Phrase>>>)>,
|
//pub next_clip: Option<(Moment, Option<Arc<RwLock<Clip>>>)>,
|
||||||
///// Play input through output.
|
///// Play input through output.
|
||||||
//pub monitoring: bool,
|
//pub monitoring: bool,
|
||||||
///// Write input to sequence.
|
///// Write input to sequence.
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ render!(TuiOut: (self: ClipSelected) =>
|
||||||
impl ClipSelected {
|
impl ClipSelected {
|
||||||
|
|
||||||
/// Shows currently playing clip with beats elapsed
|
/// Shows currently playing clip with beats elapsed
|
||||||
pub fn play_clip <T: HasPlayPhrase + HasClock> (state: &T) -> Self {
|
pub fn play_clip <T: HasPlayClip + HasClock> (state: &T) -> Self {
|
||||||
let (name, color) = if let Some((_, Some(clip))) = state.play_clip() {
|
let (name, color) = if let Some((_, Some(clip))) = state.play_clip() {
|
||||||
let MidiClip { ref name, color, .. } = *clip.read().unwrap();
|
let MidiClip { ref name, color, .. } = *clip.read().unwrap();
|
||||||
(name.clone().into(), color)
|
(name.clone().into(), color)
|
||||||
|
|
@ -33,7 +33,7 @@ impl ClipSelected {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows next clip with beats remaining until switchover
|
/// Shows next clip with beats remaining until switchover
|
||||||
pub fn next_clip <T: HasPlayPhrase> (state: &T) -> Self {
|
pub fn next_clip <T: HasPlayClip> (state: &T) -> Self {
|
||||||
let mut time: Arc<str> = String::from("--.-.--").into();
|
let mut time: Arc<str> = String::from("--.-.--").into();
|
||||||
let mut name: Arc<str> = String::from("").into();
|
let mut name: Arc<str> = String::from("").into();
|
||||||
let mut color = ItemPalette::from(TuiTheme::g(64));
|
let mut color = ItemPalette::from(TuiTheme::g(64));
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
use tek::*;
|
use tek::*;
|
||||||
|
|
||||||
struct ExamplePhrases(Vec<Arc<RwLock<Phrase>>>);
|
struct ExampleClips(Vec<Arc<RwLock<Clip>>>);
|
||||||
|
|
||||||
impl HasClips for ExamplePhrases {
|
impl HasClips for ExampleClips {
|
||||||
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>> {
|
fn phrases (&self) -> &Vec<Arc<RwLock<Clip>>> {
|
||||||
&self.0
|
&self.0
|
||||||
}
|
}
|
||||||
fn phrases_mut (&mut self) -> &mut Vec<Arc<RwLock<Phrase>>> {
|
fn phrases_mut (&mut self) -> &mut Vec<Arc<RwLock<Clip>>> {
|
||||||
&mut self.0
|
&mut self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main () -> Usually<()> {
|
fn main () -> Usually<()> {
|
||||||
let mut phrases = ExamplePhrases(vec![]);
|
let mut phrases = ExampleClips(vec![]);
|
||||||
MidiPoolCommand::Import(0, String::from("./example.mid")).execute(&mut phrases)?;
|
MidiPoolCommand::Import(0, String::from("./example.mid")).execute(&mut phrases)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,560 +1,6 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
use ClockCommand::{Play, Pause};
|
use ClockCommand::{Play, Pause};
|
||||||
use self::ArrangerCommand as Cmd;
|
use self::ArrangerCommand as Cmd;
|
||||||
/// Root view for standalone `tek_arranger`
|
|
||||||
pub struct Arranger {
|
|
||||||
pub jack: Arc<RwLock<JackConnection>>,
|
|
||||||
pub midi_ins: Vec<JackPort<MidiIn>>,
|
|
||||||
pub midi_outs: Vec<JackPort<MidiOut>>,
|
|
||||||
pub clock: Clock,
|
|
||||||
pub pool: PoolModel,
|
|
||||||
pub tracks: Vec<ArrangerTrack>,
|
|
||||||
pub scenes: Vec<ArrangerScene>,
|
|
||||||
pub splits: [u16;2],
|
|
||||||
pub selected: ArrangerSelection,
|
|
||||||
pub color: ItemPalette,
|
|
||||||
pub size: Measure<TuiOut>,
|
|
||||||
pub note_buf: Vec<u8>,
|
|
||||||
pub midi_buf: Vec<Vec<Vec<u8>>>,
|
|
||||||
pub editor: MidiEditor,
|
|
||||||
pub editing: AtomicBool,
|
|
||||||
pub perf: PerfModel,
|
|
||||||
pub compact: bool,
|
|
||||||
}
|
|
||||||
render!(TuiOut: (self: Arranger) => self.size.of(EdnView::from_source(self, Self::EDN)));
|
|
||||||
impl EdnViewData<TuiOut> for &Arranger {
|
|
||||||
fn get_content <'a> (&'a self, item: EdnItem<&'a str>) -> RenderBox<'a, TuiOut> {
|
|
||||||
use EdnItem::*;
|
|
||||||
let tracks_w = self.tracks_with_sizes().last().unwrap().3 as u16;
|
|
||||||
match item {
|
|
||||||
Nil => Box::new(()),
|
|
||||||
Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())),
|
|
||||||
Sym(":editor") => (&self.editor).boxed(),
|
|
||||||
Sym(":pool") => self.pool().boxed(),
|
|
||||||
Sym(":status") => self.status().boxed(),
|
|
||||||
Sym(":toolbar") => self.toolbar().boxed(),
|
|
||||||
Sym(":tracks") => self.track_row(tracks_w).boxed(),
|
|
||||||
Sym(":scenes") => self.scene_row(tracks_w).boxed(),
|
|
||||||
Sym(":inputs") => self.input_row(tracks_w).boxed(),
|
|
||||||
Sym(":outputs") => self.output_row(tracks_w).boxed(),
|
|
||||||
_ => panic!("no content for {item:?}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Arranger {
|
|
||||||
const EDN: &'static str = include_str!("arranger.edn");
|
|
||||||
pub const LEFT_SEP: char = '▎';
|
|
||||||
pub const TRACK_MIN_WIDTH: usize = 9;
|
|
||||||
|
|
||||||
fn toolbar (&self) -> impl Content<TuiOut> + use<'_> {
|
|
||||||
Fill::x(Fixed::y(2, Align::x(TransportView::new(true, &self.clock))))
|
|
||||||
}
|
|
||||||
fn pool (&self) -> impl Content<TuiOut> + use<'_> {
|
|
||||||
Align::e(Fixed::x(self.sidebar_w(), PoolView(self.compact, &self.pool)))
|
|
||||||
}
|
|
||||||
fn status (&self) -> impl Content<TuiOut> + use<'_> {
|
|
||||||
Bsp::e(self.editor.clip_status(), self.editor.edit_status())
|
|
||||||
}
|
|
||||||
fn is_editing (&self) -> bool {
|
|
||||||
!self.pool.visible
|
|
||||||
}
|
|
||||||
fn sidebar_w (&self) -> u16 {
|
|
||||||
let w = self.size.w();
|
|
||||||
let w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
|
|
||||||
let w = if self.pool.visible { w } else { 8 };
|
|
||||||
w
|
|
||||||
}
|
|
||||||
fn editor_w (&self) -> usize {
|
|
||||||
//self.editor.note_len() / self.editor.note_zoom().get()
|
|
||||||
(5 + (self.editor.time_len().get() / self.editor.time_zoom().get()))
|
|
||||||
.min(self.size.w().saturating_sub(20))
|
|
||||||
.max(16)
|
|
||||||
//self.editor.time_axis().get().max(16)
|
|
||||||
//50
|
|
||||||
}
|
|
||||||
pub fn scenes_with_sizes (&self, h: usize)
|
|
||||||
-> impl Iterator<Item = (usize, &ArrangerScene, usize, usize)>
|
|
||||||
{
|
|
||||||
let mut y = 0;
|
|
||||||
let editing = self.is_editing();
|
|
||||||
let (selected_track, selected_scene) = match self.selected {
|
|
||||||
ArrangerSelection::Clip(t, s) => (Some(t), Some(s)),
|
|
||||||
_ => (None, None)
|
|
||||||
};
|
|
||||||
self.scenes.iter().enumerate().map(move|(s, scene)|{
|
|
||||||
let active = editing && selected_track.is_some() && selected_scene == Some(s);
|
|
||||||
let height = if active { 15 } else { h };
|
|
||||||
let data = (s, scene, y, y + height);
|
|
||||||
y += height;
|
|
||||||
data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
pub fn tracks_with_sizes (&self)
|
|
||||||
-> impl Iterator<Item = (usize, &ArrangerTrack, usize, usize)>
|
|
||||||
{
|
|
||||||
tracks_with_sizes(self.tracks.iter(), match self.selected {
|
|
||||||
ArrangerSelection::Track(t) if self.is_editing() => Some(t),
|
|
||||||
ArrangerSelection::Clip(t, _) if self.is_editing() => Some(t),
|
|
||||||
_ => None
|
|
||||||
}, self.editor_w())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn play_row (&self, tracks_w: u16) -> impl Content<TuiOut> + '_ {
|
|
||||||
let h = 2;
|
|
||||||
Fixed::y(h, Bsp::e(
|
|
||||||
Fixed::xy(self.sidebar_w() as u16, h, self.play_header()),
|
|
||||||
Fill::x(Align::c(Fixed::xy(tracks_w, h, self.play_cells())))
|
|
||||||
))
|
|
||||||
}
|
|
||||||
fn play_header (&self) -> BoxThunk<TuiOut> {
|
|
||||||
(||Tui::bold(true, Tui::fg(TuiTheme::g(128), "Playing")).boxed()).into()
|
|
||||||
}
|
|
||||||
fn play_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
|
||||||
(move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| {
|
|
||||||
//let color = track.color;
|
|
||||||
let color: ItemPalette = track.color.dark.into();
|
|
||||||
let timebase = self.clock().timebase();
|
|
||||||
let value = Tui::fg_bg(color.lightest.rgb, color.base.rgb,
|
|
||||||
if let Some((_, Some(clip))) = track.player.play_clip().as_ref() {
|
|
||||||
let length = clip.read().unwrap().length;
|
|
||||||
let elapsed = track.player.pulses_since_start().unwrap() as usize;
|
|
||||||
format!("+{:>}", timebase.format_beats_1_short((elapsed % length) as f64))
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
});
|
|
||||||
let cell = Bsp::s(value, phat_hi(color.dark.rgb, color.darker.rgb));
|
|
||||||
Tui::bg(color.base.rgb, map_east(x1 as u16, (x2 - x1) as u16, cell))
|
|
||||||
})).boxed()).into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn next_row (&self, tracks_w: u16) -> impl Content<TuiOut> + '_ {
|
|
||||||
let h = 2;
|
|
||||||
Fixed::y(h, Bsp::e(
|
|
||||||
Fixed::xy(self.sidebar_w() as u16, h, self.next_header()),
|
|
||||||
Fill::x(Align::c(Fixed::xy(tracks_w, h, self.next_cells())))
|
|
||||||
))
|
|
||||||
}
|
|
||||||
fn next_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
|
||||||
(||Tui::bold(true, Tui::fg(TuiTheme::g(128), "Next")).boxed()).into()
|
|
||||||
}
|
|
||||||
fn next_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
|
||||||
(move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| {
|
|
||||||
let color: ItemPalette = track.color;
|
|
||||||
let color: ItemPalette = track.color.dark.into();
|
|
||||||
let current = &self.clock().playhead;
|
|
||||||
let timebase = ¤t.timebase;
|
|
||||||
let cell = Self::cell(color, Tui::bold(true, {
|
|
||||||
let mut result = String::new();
|
|
||||||
if let Some((t, _)) = track.player.next_clip().as_ref() {
|
|
||||||
let target = t.pulse.get();
|
|
||||||
let current = current.pulse.get();
|
|
||||||
if target > current {
|
|
||||||
result = format!("-{:>}", timebase.format_beats_0_short(target - current))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}));
|
|
||||||
let cell = Tui::fg_bg(color.lightest.rgb, color.base.rgb, cell);
|
|
||||||
let cell = Bsp::s(cell, phat_hi(color.dark.rgb, color.darker.rgb));
|
|
||||||
Tui::bg(color.base.rgb, map_east(x1 as u16, (x2 - x1) as u16, cell))
|
|
||||||
})).boxed()).into()
|
|
||||||
}
|
|
||||||
/// beats until switchover
|
|
||||||
fn cell_until_next (track: &ArrangerTrack, current: &Arc<Moment>)
|
|
||||||
-> Option<impl Content<TuiOut>>
|
|
||||||
{
|
|
||||||
let timebase = ¤t.timebase;
|
|
||||||
let mut result = String::new();
|
|
||||||
if let Some((t, _)) = track.player.next_clip().as_ref() {
|
|
||||||
let target = t.pulse.get();
|
|
||||||
let current = current.pulse.get();
|
|
||||||
if target > current {
|
|
||||||
result = format!("-{:>}", timebase.format_beats_0_short(target - current))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn track_row (&self, tracks_w: u16) -> impl Content<TuiOut> + '_ {
|
|
||||||
let h = 3;
|
|
||||||
let border = |x|x;//Rugged(Style::default().fg(Color::Rgb(0,0,0)).bg(Color::Reset)).enclose2(x);
|
|
||||||
Fixed::y(h, Bsp::e(
|
|
||||||
Fixed::xy(self.sidebar_w() as u16, h, self.track_header()),
|
|
||||||
Fill::x(Align::c(Fixed::xy(tracks_w, h, border(self.track_cells()))))
|
|
||||||
))
|
|
||||||
}
|
|
||||||
fn track_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
|
||||||
(||Tui::bold(true, Bsp::s(
|
|
||||||
row!(
|
|
||||||
Tui::fg(TuiTheme::g(128), "add "),
|
|
||||||
Tui::fg(TuiTheme::orange(), "t"),
|
|
||||||
Tui::fg(TuiTheme::g(128), "rack"),
|
|
||||||
),
|
|
||||||
row!(
|
|
||||||
Tui::fg(TuiTheme::orange(), "a"),
|
|
||||||
Tui::fg(TuiTheme::g(128), "dd scene"),
|
|
||||||
),
|
|
||||||
).boxed())).into()
|
|
||||||
}
|
|
||||||
fn track_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
|
||||||
let iter = ||self.tracks_with_sizes();
|
|
||||||
(move||Align::x(Map::new(iter, move|(_, track, x1, x2), i| {
|
|
||||||
let name = Push::x(1, &track.name);
|
|
||||||
let color = track.color;
|
|
||||||
let fg = color.lightest.rgb;
|
|
||||||
let bg = color.base.rgb;
|
|
||||||
let active = self.selected.track() == Some(i);
|
|
||||||
let bfg = if active { Color::Rgb(255,255,255) } else { Color::Rgb(0,0,0) };
|
|
||||||
let border = Style::default().fg(bfg).bg(bg);
|
|
||||||
Tui::bg(bg, map_east(x1 as u16, (x2 - x1) as u16,
|
|
||||||
Outer(border).enclose(Tui::fg_bg(fg, bg, Tui::bold(true, Fill::x(Align::x(name)))))
|
|
||||||
))
|
|
||||||
})).boxed()).into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn input_row (&self, tracks_w: u16) -> impl Content<TuiOut> + '_ {
|
|
||||||
let h = 2 + self.midi_ins[0].connect.len() as u16;
|
|
||||||
Fixed::y(h, Bsp::e(
|
|
||||||
Fixed::xy(self.sidebar_w() as u16, h, self.input_header()),
|
|
||||||
Fill::x(Align::c(Fixed::xy(tracks_w, h, self.input_cells())))
|
|
||||||
))
|
|
||||||
}
|
|
||||||
fn input_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
|
||||||
(||Bsp::s(
|
|
||||||
Tui::bold(true, row!(
|
|
||||||
Tui::fg(TuiTheme::g(128), "midi "),
|
|
||||||
Tui::fg(TuiTheme::orange(), "I"),
|
|
||||||
Tui::fg(TuiTheme::g(128), "ns"),
|
|
||||||
)),
|
|
||||||
Bsp::s(
|
|
||||||
Fill::x(Tui::bold(true, Tui::fg_bg(TuiTheme::g(224), TuiTheme::g(64),
|
|
||||||
Align::w(&self.midi_ins[0].name)))),
|
|
||||||
self.midi_ins.get(0)
|
|
||||||
.and_then(|midi_in|midi_in.connect.get(0))
|
|
||||||
.map(|connect|Fill::x(Align::w(
|
|
||||||
Tui::bold(false, Tui::fg_bg(TuiTheme::g(224), TuiTheme::g(64), connect.info())))))
|
|
||||||
)
|
|
||||||
).boxed()).into()
|
|
||||||
}
|
|
||||||
fn input_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
|
||||||
(move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| {
|
|
||||||
let w = (x2 - x1) as u16;
|
|
||||||
let color: ItemPalette = track.color.dark.into();
|
|
||||||
map_east(x1 as u16, w, Fixed::x(w, Self::cell(color, Bsp::n(
|
|
||||||
Self::rec_mon(color.base.rgb, false, false),
|
|
||||||
phat_hi(color.base.rgb, color.dark.rgb)
|
|
||||||
))))
|
|
||||||
})).boxed()).into()
|
|
||||||
}
|
|
||||||
fn rec_mon (bg: Color, rec: bool, mon: bool) -> impl Content<TuiOut> {
|
|
||||||
row!(
|
|
||||||
Tui::fg_bg(if rec { Color::Red } else { bg }, bg, "▐"),
|
|
||||||
Tui::fg_bg(if rec { Color::White } else { Color::Rgb(0,0,0) }, bg, "REC"),
|
|
||||||
Tui::fg_bg(if rec { Color::White } else { bg }, bg, "▐"),
|
|
||||||
Tui::fg_bg(if mon { Color::White } else { Color::Rgb(0,0,0) }, bg, "MON"),
|
|
||||||
Tui::fg_bg(if mon { Color::White } else { bg }, bg, "▌"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn output_row (&self, tracks_w: u16) -> impl Content<TuiOut> + '_ {
|
|
||||||
let h = 2 + self.midi_outs[0].connect.len() as u16;
|
|
||||||
Fixed::y(h, Bsp::e(
|
|
||||||
Fixed::xy(self.sidebar_w() as u16, h, self.output_header()),
|
|
||||||
Fill::x(Align::c(Fixed::xy(tracks_w, h, self.output_cells())))
|
|
||||||
))
|
|
||||||
}
|
|
||||||
fn output_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
|
||||||
(||Bsp::s(
|
|
||||||
Tui::bold(true, row!(
|
|
||||||
Tui::fg(TuiTheme::g(128), "midi "),
|
|
||||||
Tui::fg(TuiTheme::orange(), "O"),
|
|
||||||
Tui::fg(TuiTheme::g(128), "uts"),
|
|
||||||
)),
|
|
||||||
Bsp::s(
|
|
||||||
Fill::x(Tui::bold(true, Tui::fg_bg(TuiTheme::g(224), TuiTheme::g(64),
|
|
||||||
Align::w(&self.midi_outs[0].name)))),
|
|
||||||
self.midi_outs.get(0)
|
|
||||||
.and_then(|midi_out|midi_out.connect.get(0))
|
|
||||||
.map(|connect|Fill::x(Align::w(
|
|
||||||
Tui::bold(false, Tui::fg_bg(TuiTheme::g(224), TuiTheme::g(64), connect.info())))))
|
|
||||||
),
|
|
||||||
).boxed()).into()
|
|
||||||
}
|
|
||||||
fn output_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
|
||||||
(move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| {
|
|
||||||
let w = (x2 - x1) as u16;
|
|
||||||
let color: ItemPalette = track.color.dark.into();
|
|
||||||
map_east(x1 as u16, w, Fixed::x(w, Self::cell(color, Bsp::n(
|
|
||||||
Self::mute_solo(color.base.rgb, false, false),
|
|
||||||
phat_hi(color.dark.rgb, color.darker.rgb)
|
|
||||||
))))
|
|
||||||
})).boxed()).into()
|
|
||||||
}
|
|
||||||
fn mute_solo (bg: Color, mute: bool, solo: bool) -> impl Content<TuiOut> {
|
|
||||||
row!(
|
|
||||||
Tui::fg_bg(if mute { Color::White } else { Color::Rgb(0,0,0) }, bg, "MUTE"),
|
|
||||||
Tui::fg_bg(if mute { Color::White } else { bg }, bg, "▐"),
|
|
||||||
Tui::fg_bg(if solo { Color::White } else { Color::Rgb(0,0,0) }, bg, "SOLO"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn scene_row (&self, tracks_w: u16) -> impl Content<TuiOut> + '_ {
|
|
||||||
let h = (self.size.h() as u16).saturating_sub(8).max(8);
|
|
||||||
let border = |x|x;//Skinny(Style::default().fg(Color::Rgb(0,0,0)).bg(Color::Reset)).enclose2(x);
|
|
||||||
Bsp::e(
|
|
||||||
Tui::bg(Color::Reset, Fixed::xy(self.sidebar_w() as u16, h, self.scene_headers())),
|
|
||||||
Tui::bg(Color::Reset, Fill::x(Align::c(Fixed::xy(tracks_w, h, border(self.scene_cells())))))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
fn scene_headers <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
|
||||||
(||{
|
|
||||||
let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0))));
|
|
||||||
let selected = self.selected.scene();
|
|
||||||
Fill::y(Align::c(Map::new(||self.scenes_with_sizes(2), move|(_, scene, y1, y2), i| {
|
|
||||||
let h = (y2 - y1) as u16;
|
|
||||||
let name = format!("🭬{}", &scene.name);
|
|
||||||
let color = scene.color;
|
|
||||||
let active = selected == Some(i);
|
|
||||||
let mid = if active { color.light } else { color.base };
|
|
||||||
let top = Some(last_color.read().unwrap().base.rgb);
|
|
||||||
let cell = phat_sel_3(
|
|
||||||
active,
|
|
||||||
Tui::bold(true, name.clone()),
|
|
||||||
Tui::bold(true, name),
|
|
||||||
top,
|
|
||||||
mid.rgb,
|
|
||||||
Color::Rgb(0, 0, 0)
|
|
||||||
);
|
|
||||||
*last_color.write().unwrap() = color;
|
|
||||||
map_south(y1 as u16, h + 1, Fixed::y(h + 1, cell))
|
|
||||||
}))).boxed()
|
|
||||||
}).into()
|
|
||||||
}
|
|
||||||
fn scene_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
|
||||||
let editing = self.is_editing();
|
|
||||||
let tracks = move||self.tracks_with_sizes();
|
|
||||||
let scenes = ||self.scenes_with_sizes(2);
|
|
||||||
let selected_track = self.selected.track();
|
|
||||||
let selected_scene = self.selected.scene();
|
|
||||||
(move||Fill::y(Align::c(Map::new(tracks, move|(_, track, x1, x2), t| {
|
|
||||||
let w = (x2 - x1) as u16;
|
|
||||||
let color: ItemPalette = track.color.dark.into();
|
|
||||||
let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0))));
|
|
||||||
let cells = Map::new(scenes, move|(_, scene, y1, y2), s| {
|
|
||||||
let h = (y2 - y1) as u16;
|
|
||||||
let color = scene.color;
|
|
||||||
let (name, fg, bg) = if let Some(c) = &scene.clips[t] {
|
|
||||||
let c = c.read().unwrap();
|
|
||||||
(c.name.to_string(), c.color.lightest.rgb, c.color.base.rgb)
|
|
||||||
} else {
|
|
||||||
("⏹ ".to_string(), TuiTheme::g(64), TuiTheme::g(32))
|
|
||||||
};
|
|
||||||
let last = last_color.read().unwrap().clone();
|
|
||||||
let active = editing && selected_scene == Some(s) && selected_track == Some(t);
|
|
||||||
let editor = Thunk::new(||&self.editor);
|
|
||||||
let cell = Thunk::new(move||phat_sel_3(
|
|
||||||
selected_track == Some(t) && selected_scene == Some(s),
|
|
||||||
Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))),
|
|
||||||
Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))),
|
|
||||||
if selected_track == Some(t) && selected_scene.map(|s|s+1) == Some(s) {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(bg.into())
|
|
||||||
},
|
|
||||||
bg.into(),
|
|
||||||
bg.into(),
|
|
||||||
));
|
|
||||||
let cell = Either(active, editor, cell);
|
|
||||||
*last_color.write().unwrap() = bg.into();
|
|
||||||
map_south(
|
|
||||||
y1 as u16,
|
|
||||||
h + 1,
|
|
||||||
Fill::x(Fixed::y(h + 1, cell))
|
|
||||||
)
|
|
||||||
});
|
|
||||||
let column = Fixed::x(w, Tui::bg(Color::Reset, Align::y(cells)).boxed());
|
|
||||||
Fixed::x(w, map_east(x1 as u16, w, column))
|
|
||||||
}))).boxed()).into()
|
|
||||||
}
|
|
||||||
fn cell_clip <'a> (
|
|
||||||
scene: &'a ArrangerScene, index: usize, track: &'a ArrangerTrack, w: u16, h: u16
|
|
||||||
) -> impl Content<TuiOut> + use<'a> {
|
|
||||||
scene.clips.get(index).map(|clip|clip.as_ref().map(|clip|{
|
|
||||||
let clip = clip.read().unwrap();
|
|
||||||
let mut bg = TuiTheme::border_bg();
|
|
||||||
let name = clip.name.to_string();
|
|
||||||
let max_w = name.len().min((w as usize).saturating_sub(2));
|
|
||||||
let color = clip.color;
|
|
||||||
bg = color.dark.rgb;
|
|
||||||
if let Some((_, Some(ref playing))) = track.player.play_clip() {
|
|
||||||
if *playing.read().unwrap() == *clip {
|
|
||||||
bg = color.light.rgb
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Fixed::xy(w, h, &Tui::bg(bg, Push::x(1, Fixed::x(w, &name.as_str()[0..max_w]))));
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn track_column_separators <'a> (&'a self) -> impl Content<TuiOut> + 'a {
|
|
||||||
let scenes_w = 16;//.max(SCENES_W_OFFSET + ArrangerScene::longest_name(&self.scenes) as u16);
|
|
||||||
let fg = Color::Rgb(64,64,64);
|
|
||||||
Map::new(move||self.tracks_with_sizes(), move|(_n, _track, x1, x2), _i|{
|
|
||||||
Push::x(scenes_w, map_east(x1 as u16, (x2 - x1) as u16,
|
|
||||||
Fixed::x((x2 - x1) as u16, Tui::fg(fg, RepeatV(&"·")))))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub 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 scene_row_sep <'a> (&'a self) -> impl Content<TuiOut> + 'a {
|
|
||||||
let fg = Color::Rgb(255,255,255);
|
|
||||||
Map::new(move||self.scenes_with_sizes(1), |_, _|"")
|
|
||||||
//Map(||rows.iter(), |(_n, _scene, y1, _y2), _i| {
|
|
||||||
//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 {
|
|
||||||
//if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((x, y))) {
|
|
||||||
//cell.modifier = Modifier::UNDERLINED;
|
|
||||||
//cell.underline_color = fg;
|
|
||||||
//}
|
|
||||||
////}
|
|
||||||
//}
|
|
||||||
//})
|
|
||||||
}
|
|
||||||
|
|
||||||
//pub fn scene_heights (scenes: &[ArrangerScene], 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()
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//fn cursor (&self) -> impl Content<TuiOut> + '_ {
|
|
||||||
//let color = self.color;
|
|
||||||
//let bg = color.lighter.rgb;//Color::Rgb(0, 255, 0);
|
|
||||||
//let selected = self.selected();
|
|
||||||
//let cols = Arranger::track_widths(&self.tracks);
|
|
||||||
//let rows = Arranger::scene_heights(&self.scenes, 1);
|
|
||||||
//let scenes_w = 16.max(SCENES_W_OFFSET + ArrangerScene::longest_name(&self.scenes) as u16);
|
|
||||||
//let focused = true;
|
|
||||||
//let reticle = Reticle(Style {
|
|
||||||
//fg: Some(self.color.lighter.rgb),
|
|
||||||
//bg: None,
|
|
||||||
//underline_color: None,
|
|
||||||
//add_modifier: Modifier::empty(),
|
|
||||||
//sub_modifier: Modifier::DIM
|
|
||||||
//});
|
|
||||||
//RenderThunk::new(move|to: &mut TuiOut|{
|
|
||||||
//let area = to.area();
|
|
||||||
//let [x, y, w, h] = area.xywh();
|
|
||||||
//let mut track_area: Option<[u16;4]> = match selected {
|
|
||||||
//ArrangerSelection::Track(t) | ArrangerSelection::Clip(t, _) => Some([
|
|
||||||
//x + scenes_w + cols[t].1 as u16, y,
|
|
||||||
//cols[t].0 as u16, h,
|
|
||||||
//]),
|
|
||||||
//_ => None
|
|
||||||
//};
|
|
||||||
//let mut scene_area: Option<[u16;4]> = match selected {
|
|
||||||
//ArrangerSelection::Scene(s) | ArrangerSelection::Clip(_, s) => Some([
|
|
||||||
//x, y + HEADER_H + (rows[s].1 / PPQ) as u16,
|
|
||||||
//w, (rows[s].0 / PPQ) as u16
|
|
||||||
//]),
|
|
||||||
//_ => None
|
|
||||||
//};
|
|
||||||
//let mut clip_area: Option<[u16;4]> = match selected {
|
|
||||||
//ArrangerSelection::Clip(t, s) => Some([
|
|
||||||
//(scenes_w + x + cols[t].1 as u16).saturating_sub(1),
|
|
||||||
//HEADER_H + y + (rows[s].1/PPQ) as u16,
|
|
||||||
//cols[t].0 as u16 + 2,
|
|
||||||
//(rows[s].0 / PPQ) as u16
|
|
||||||
//]),
|
|
||||||
//_ => None
|
|
||||||
//};
|
|
||||||
//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([x, y - 1, w, 1], bg);
|
|
||||||
//to.fill_ul([x, y + height - 1, w, 1], bg);
|
|
||||||
//}
|
|
||||||
//if focused {
|
|
||||||
//to.place(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(scenes_w)
|
|
||||||
//} else {
|
|
||||||
//area.clip_w(scenes_w).clip_h(HEADER_H)
|
|
||||||
//}, &reticle)
|
|
||||||
//};
|
|
||||||
//})
|
|
||||||
//}
|
|
||||||
|
|
||||||
/// A 1-row cell.
|
|
||||||
fn cell <T: Content<TuiOut>> (color: ItemPalette, field: T) -> impl Content<TuiOut> {
|
|
||||||
Tui::fg_bg(color.lightest.rgb, color.base.rgb, Fixed::y(1, field))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// A phat line
|
|
||||||
pub fn phat_lo (fg: Color, bg: Color) -> impl Content<TuiOut> {
|
|
||||||
Fixed::y(1, Tui::fg_bg(fg, bg, RepeatH(&"▄")))
|
|
||||||
}
|
|
||||||
/// A phat line
|
|
||||||
pub fn phat_hi (fg: Color, bg: Color) -> impl Content<TuiOut> {
|
|
||||||
Fixed::y(1, Tui::fg_bg(fg, bg, RepeatH(&"▀")))
|
|
||||||
}
|
|
||||||
/// A cell that is 3-row on its own, but stacks, giving (N+1)*2 rows per N cells.
|
|
||||||
pub fn phat_cell <T: Content<TuiOut>> (
|
|
||||||
color: ItemPalette, last: ItemPalette, field: T
|
|
||||||
) -> impl Content<TuiOut> {
|
|
||||||
Bsp::s(phat_lo(color.base.rgb, last.base.rgb),
|
|
||||||
Bsp::n(phat_hi(color.base.rgb, last.base.rgb),
|
|
||||||
Fixed::y(1, Fill::x(Tui::fg_bg(color.lightest.rgb, color.base.rgb, field))),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
pub fn phat_cell_3 <T: Content<TuiOut>> (
|
|
||||||
field: T, top: Color, middle: Color, bottom: Color
|
|
||||||
) -> impl Content<TuiOut> {
|
|
||||||
Bsp::s(phat_lo(middle, top),
|
|
||||||
Bsp::n(phat_hi(middle, bottom),
|
|
||||||
Fill::y(Fill::x(Tui::bg(middle, field))),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
pub fn phat_sel_3 <T: Content<TuiOut>> (
|
|
||||||
selected: bool, field_1: T, field_2: T, top: Option<Color>, middle: Color, bottom: Color
|
|
||||||
) -> impl Content<TuiOut> {
|
|
||||||
let border = Style::default().fg(Color::Rgb(255,255,255)).bg(middle);
|
|
||||||
Either(selected,
|
|
||||||
Tui::bg(middle, Outer(border).enclose(Align::w(Bsp::s("", Bsp::n("", Fill::y(field_1)))))),
|
|
||||||
Bsp::s(Fixed::y(1, top.map(|top|phat_lo(middle, top))),
|
|
||||||
Bsp::n(Fixed::y(1, phat_hi(middle, bottom)),
|
|
||||||
Fill::xy(Tui::bg(middle, field_2)),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
has_clock!(|self: Arranger|&self.clock);
|
has_clock!(|self: Arranger|&self.clock);
|
||||||
has_clips!(|self: Arranger|self.pool.clips);
|
has_clips!(|self: Arranger|self.pool.clips);
|
||||||
has_editor!(|self: Arranger|self.editor);
|
has_editor!(|self: Arranger|self.editor);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use KeyCode::{Tab, Char};
|
||||||
use SequencerCommand as SeqCmd;
|
use SequencerCommand as SeqCmd;
|
||||||
use GrooveboxCommand as GrvCmd;
|
use GrooveboxCommand as GrvCmd;
|
||||||
use ArrangerCommand as ArrCmd;
|
use ArrangerCommand as ArrCmd;
|
||||||
|
use SamplerCommand as SmplCmd;
|
||||||
use MidiEditCommand as EditCmd;
|
use MidiEditCommand as EditCmd;
|
||||||
use MidiPoolCommand as PoolCmd;
|
use MidiPoolCommand as PoolCmd;
|
||||||
|
|
||||||
|
|
@ -88,10 +89,11 @@ handle!(TuiIn: |self: Arranger, input|ArrangerCommand::execute_with_state(self,
|
||||||
}
|
}
|
||||||
|
|
||||||
command!(|self: SequencerCommand, state: Sequencer|match self {
|
command!(|self: SequencerCommand, state: Sequencer|match self {
|
||||||
Self::Enqueue(clip) => {
|
Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?,
|
||||||
state.player.enqueue_next(clip.as_ref());
|
Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?,
|
||||||
None
|
Self::Enqueue(clip) => { state.player.enqueue_next(clip.as_ref()); None },
|
||||||
},
|
Self::History(delta) => { todo!("undo/redo") },
|
||||||
|
|
||||||
Self::Pool(cmd) => match cmd {
|
Self::Pool(cmd) => match cmd {
|
||||||
// autoselect: automatically load selected clip in editor
|
// autoselect: automatically load selected clip in editor
|
||||||
PoolCommand::Select(_) => {
|
PoolCommand::Select(_) => {
|
||||||
|
|
@ -100,18 +102,13 @@ command!(|self: SequencerCommand, state: Sequencer|match self {
|
||||||
undo
|
undo
|
||||||
},
|
},
|
||||||
// update color in all places simultaneously
|
// update color in all places simultaneously
|
||||||
PoolCommand::Phrase(PoolCmd::SetColor(index, _)) => {
|
PoolCommand::Clip(PoolCmd::SetColor(index, _)) => {
|
||||||
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
||||||
state.editor.set_clip(state.pool.clip().as_ref());
|
state.editor.set_clip(state.pool.clip().as_ref());
|
||||||
undo
|
undo
|
||||||
},
|
},
|
||||||
_ => cmd.delegate(&mut state.pool, Self::Pool)?
|
_ => cmd.delegate(&mut state.pool, Self::Pool)?
|
||||||
},
|
},
|
||||||
Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?,
|
|
||||||
Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?,
|
|
||||||
Self::History(delta) => {
|
|
||||||
todo!("undo/redo")
|
|
||||||
},
|
|
||||||
Self::Compact(compact) => if state.compact != compact {
|
Self::Compact(compact) => if state.compact != compact {
|
||||||
state.compact = compact;
|
state.compact = compact;
|
||||||
Some(Self::Compact(!compact))
|
Some(Self::Compact(!compact))
|
||||||
|
|
@ -120,10 +117,12 @@ command!(|self: SequencerCommand, state: Sequencer|match self {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
command!(|self: GrooveboxCommand, state: Groovebox|match self {
|
command!(|self: GrooveboxCommand, state: Groovebox|match self {
|
||||||
Self::Enqueue(clip) => {
|
Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?,
|
||||||
state.player.enqueue_next(clip.as_ref());
|
Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?,
|
||||||
None
|
Self::Enqueue(clip) => { state.player.enqueue_next(clip.as_ref()); None },
|
||||||
},
|
Self::History(delta) => { todo!("undo/redo") },
|
||||||
|
Self::Sampler(cmd) => cmd.delegate(&mut state.sampler, Self::Sampler)?,
|
||||||
|
|
||||||
Self::Pool(cmd) => match cmd {
|
Self::Pool(cmd) => match cmd {
|
||||||
// autoselect: automatically load selected clip in editor
|
// autoselect: automatically load selected clip in editor
|
||||||
PoolCommand::Select(_) => {
|
PoolCommand::Select(_) => {
|
||||||
|
|
@ -132,17 +131,13 @@ command!(|self: GrooveboxCommand, state: Groovebox|match self {
|
||||||
undo
|
undo
|
||||||
},
|
},
|
||||||
// update color in all places simultaneously
|
// update color in all places simultaneously
|
||||||
PoolCommand::Phrase(PoolCmd::SetColor(index, _)) => {
|
PoolCommand::Clip(PoolCmd::SetColor(index, _)) => {
|
||||||
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
||||||
state.editor.set_clip(state.pool.clip().as_ref());
|
state.editor.set_clip(state.pool.clip().as_ref());
|
||||||
undo
|
undo
|
||||||
},
|
},
|
||||||
_ => cmd.delegate(&mut state.pool, Self::Pool)?
|
_ => cmd.delegate(&mut state.pool, Self::Pool)?
|
||||||
},
|
},
|
||||||
Self::Sampler(cmd) => cmd.delegate(&mut state.sampler, Self::Sampler)?,
|
|
||||||
Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?,
|
|
||||||
Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?,
|
|
||||||
Self::History(delta) => { todo!("undo/redo") },
|
|
||||||
Self::Compact(compact) => if state.compact != compact {
|
Self::Compact(compact) => if state.compact != compact {
|
||||||
state.compact = compact;
|
state.compact = compact;
|
||||||
Some(Self::Compact(!compact))
|
Some(Self::Compact(!compact))
|
||||||
|
|
@ -151,25 +146,26 @@ command!(|self: GrooveboxCommand, state: Groovebox|match self {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
command!(|self: ArrangerCommand, state: Arranger|match self {
|
command!(|self: ArrangerCommand, state: Arranger|match self {
|
||||||
Self::Clear => { todo!() },
|
Self::Clear => { todo!() },
|
||||||
Self::History(_) => { todo!() },
|
Self::Clip(cmd) => cmd.delegate(state, Self::Clip)?,
|
||||||
Self::Zoom(_) => { todo!(); },
|
Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?,
|
||||||
Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?,
|
Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?,
|
||||||
Self::Clip(cmd) => cmd.delegate(state, Self::Clip)?,
|
Self::History(_) => { todo!() },
|
||||||
Self::Scene(cmd) => cmd.delegate(state, Self::Scene)?,
|
Self::Scene(cmd) => cmd.delegate(state, Self::Scene)?,
|
||||||
Self::Track(cmd) => cmd.delegate(state, Self::Track)?,
|
Self::Select(s) => { state.selected = s; None },
|
||||||
Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?,
|
Self::Track(cmd) => cmd.delegate(state, Self::Track)?,
|
||||||
Self::Select(selected) => { state.selected = selected; None },
|
Self::Zoom(_) => { todo!(); },
|
||||||
Self::StopAll => {
|
|
||||||
|
Self::StopAll => {
|
||||||
for track in 0..state.tracks.len() { state.tracks[track].player.enqueue_next(None); }
|
for track in 0..state.tracks.len() { state.tracks[track].player.enqueue_next(None); }
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
Self::Color(palette) => {
|
Self::Color(palette) => {
|
||||||
let old = state.color;
|
let old = state.color;
|
||||||
state.color = palette;
|
state.color = palette;
|
||||||
Some(Self::Color(old))
|
Some(Self::Color(old))
|
||||||
},
|
},
|
||||||
Self::Pool(cmd) => {
|
Self::Pool(cmd) => {
|
||||||
match cmd {
|
match cmd {
|
||||||
// autoselect: automatically load selected clip in editor
|
// autoselect: automatically load selected clip in editor
|
||||||
PoolCommand::Select(_) => {
|
PoolCommand::Select(_) => {
|
||||||
|
|
@ -178,7 +174,7 @@ command!(|self: ArrangerCommand, state: Arranger|match self {
|
||||||
undo
|
undo
|
||||||
},
|
},
|
||||||
// reload clip in editor to update color
|
// reload clip in editor to update color
|
||||||
PoolCommand::Phrase(MidiPoolCommand::SetColor(index, _)) => {
|
PoolCommand::Clip(MidiPoolCommand::SetColor(index, _)) => {
|
||||||
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
||||||
state.editor.set_clip(state.pool.clip().as_ref());
|
state.editor.set_clip(state.pool.clip().as_ref());
|
||||||
undo
|
undo
|
||||||
|
|
@ -188,14 +184,8 @@ command!(|self: ArrangerCommand, state: Arranger|match self {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
command!(|self: ArrangerSceneCommand, state: Arranger|match self {
|
command!(|self: ArrangerSceneCommand, state: Arranger|match self {
|
||||||
Self::Add => {
|
Self::Add => { state.scene_add(None, None)?; None }
|
||||||
state.scene_add(None, None)?;
|
Self::Delete(index) => { state.scene_del(index); None },
|
||||||
None
|
|
||||||
}
|
|
||||||
Self::Delete(index) => {
|
|
||||||
state.scene_del(index);
|
|
||||||
None
|
|
||||||
},
|
|
||||||
Self::SetColor(index, color) => {
|
Self::SetColor(index, color) => {
|
||||||
let old = state.scenes[index].color;
|
let old = state.scenes[index].color;
|
||||||
state.scenes[index].color = color;
|
state.scenes[index].color = color;
|
||||||
|
|
@ -210,19 +200,14 @@ command!(|self: ArrangerSceneCommand, state: Arranger|match self {
|
||||||
_ => None
|
_ => None
|
||||||
});
|
});
|
||||||
command!(|self: ArrangerTrackCommand, state: Arranger|match self {
|
command!(|self: ArrangerTrackCommand, state: Arranger|match self {
|
||||||
Self::Add => {
|
Self::Add => { state.track_add(None, None)?; None },
|
||||||
state.track_add(None, None)?;
|
Self::Delete(index) => { state.track_del(index); None },
|
||||||
None
|
Self::Stop(track) => { state.tracks[track].player.enqueue_next(None); None },
|
||||||
},
|
|
||||||
Self::SetColor(index, color) => {
|
Self::SetColor(index, color) => {
|
||||||
let old = state.tracks[index].color;
|
let old = state.tracks[index].color;
|
||||||
state.tracks[index].color = color;
|
state.tracks[index].color = color;
|
||||||
Some(Self::SetColor(index, old))
|
Some(Self::SetColor(index, old))
|
||||||
},
|
},
|
||||||
Self::Stop(track) => {
|
|
||||||
state.tracks[track].player.enqueue_next(None);
|
|
||||||
None
|
|
||||||
},
|
|
||||||
_ => None
|
_ => None
|
||||||
});
|
});
|
||||||
command!(|self: ArrangerClipCommand, state: Arranger|match self {
|
command!(|self: ArrangerClipCommand, state: Arranger|match self {
|
||||||
|
|
@ -289,13 +274,13 @@ keymap!(<'a> KEYS_GROOVEBOX = |state: Groovebox, input: Event| GrooveboxCommand
|
||||||
),
|
),
|
||||||
// Shift-R: toggle recording
|
// Shift-R: toggle recording
|
||||||
shift(key(Char('R'))) => GrvCmd::Sampler(if state.sampler.recording.is_some() {
|
shift(key(Char('R'))) => GrvCmd::Sampler(if state.sampler.recording.is_some() {
|
||||||
SamplerCommand::RecordFinish
|
SmplCmd::RecordFinish
|
||||||
} else {
|
} else {
|
||||||
SamplerCommand::RecordBegin(u7::from(state.editor.note_point() as u8))
|
SmplCmd::RecordBegin(u7::from(state.editor.note_point() as u8))
|
||||||
}),
|
}),
|
||||||
// Shift-Del: delete sample
|
// Shift-Del: delete sample
|
||||||
shift(key(Delete)) => GrvCmd::Sampler(
|
shift(key(Delete)) => GrvCmd::Sampler(
|
||||||
SamplerCommand::SetSample(u7::from(state.editor.note_point() as u8), None)
|
SmplCmd::SetSample(u7::from(state.editor.note_point() as u8), None)
|
||||||
),
|
),
|
||||||
// e: Toggle between editing currently playing or other clip
|
// e: Toggle between editing currently playing or other clip
|
||||||
//shift(key(Char('e'))) => if let Some((_, Some(playing))) = state.player.play_clip() {
|
//shift(key(Char('e'))) => if let Some((_, Some(playing))) = state.player.play_clip() {
|
||||||
|
|
@ -349,9 +334,9 @@ keymap!(KEYS_ARRANGER = |state: Arranger, input: Event| ArrangerCommand {
|
||||||
kpat!(Char('c')) => Some(ArrCmd::Color(ItemPalette::random())),
|
kpat!(Char('c')) => Some(ArrCmd::Color(ItemPalette::random())),
|
||||||
|
|
||||||
kpat!(Up) => return None,
|
kpat!(Up) => return None,
|
||||||
kpat!(Down) => Some( ArrCmd::Select(Selected::Scene(0))),
|
kpat!(Down) => Some(ArrCmd::Select(Selected::Scene(0))),
|
||||||
kpat!(Left) => return None,
|
kpat!(Left) => return None,
|
||||||
kpat!(Right) => Some( ArrCmd::Select(Selected::Track(0))),
|
kpat!(Right) => Some(ArrCmd::Select(Selected::Track(0))),
|
||||||
|
|
||||||
_ => None
|
_ => None
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -2,202 +2,4 @@ use crate::*;
|
||||||
use super::*;
|
use super::*;
|
||||||
use self::GrooveboxCommand as Cmd;
|
use self::GrooveboxCommand as Cmd;
|
||||||
use EdnItem::*;
|
use EdnItem::*;
|
||||||
use ClockCommand::{Play, Pause};
|
|
||||||
use MidiEditCommand::*;
|
|
||||||
use MidiPoolCommand::*;
|
|
||||||
use KeyCode::{Char, Delete, Tab, Up, Down, Left, Right};
|
|
||||||
use std::marker::ConstParamTy;
|
use std::marker::ConstParamTy;
|
||||||
pub struct Groovebox {
|
|
||||||
pub _jack: Arc<RwLock<JackConnection>>,
|
|
||||||
pub player: MidiPlayer,
|
|
||||||
pub pool: PoolModel,
|
|
||||||
pub editor: MidiEditor,
|
|
||||||
pub sampler: Sampler,
|
|
||||||
|
|
||||||
pub compact: bool,
|
|
||||||
pub size: Measure<TuiOut>,
|
|
||||||
pub status: bool,
|
|
||||||
pub note_buf: Vec<u8>,
|
|
||||||
pub midi_buf: Vec<Vec<Vec<u8>>>,
|
|
||||||
pub perf: PerfModel,
|
|
||||||
}
|
|
||||||
render!(TuiOut: (self: Groovebox) => self.size.of(EdnView::from_source(self, Self::EDN)));
|
|
||||||
impl EdnViewData<TuiOut> for &Groovebox {
|
|
||||||
fn get_content <'a> (&'a self, item: EdnItem<&'a str>) -> RenderBox<'a, TuiOut> {
|
|
||||||
use EdnItem::*;
|
|
||||||
match item {
|
|
||||||
Nil => Box::new(()),
|
|
||||||
Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())),
|
|
||||||
Sym(":editor") => (&self.editor).boxed(),
|
|
||||||
Sym(":pool") => self.pool().boxed(),
|
|
||||||
Sym(":status") => self.status().boxed(),
|
|
||||||
Sym(":toolbar") => self.toolbar().boxed(),
|
|
||||||
Sym(":sampler") => self.sampler().boxed(),
|
|
||||||
Sym(":sample") => self.sample().boxed(),
|
|
||||||
_ => panic!("no content for {item:?}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn get_unit (&self, item: EdnItem<&str>) -> u16 {
|
|
||||||
use EdnItem::*;
|
|
||||||
match item.to_str() {
|
|
||||||
":sample-h" => if self.compact { 0 } else { 5 },
|
|
||||||
":samples-w" => if self.compact { 4 } else { 11 },
|
|
||||||
":samples-y" => if self.compact { 1 } else { 0 },
|
|
||||||
":pool-w" => if self.compact { 5 } else {
|
|
||||||
let w = self.size.w();
|
|
||||||
if w > 60 { 20 } else if w > 40 { 15 } else { 10 }
|
|
||||||
},
|
|
||||||
_ => 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Groovebox {
|
|
||||||
const EDN: &'static str = include_str!("groovebox.edn");
|
|
||||||
fn toolbar (&self) -> impl Content<TuiOut> + use<'_> {
|
|
||||||
Fill::x(Fixed::y(2, lay!(
|
|
||||||
Fill::x(Align::w(Meter("L/", self.sampler.input_meter[0]))),
|
|
||||||
Fill::x(Align::e(Meter("R/", self.sampler.input_meter[1]))),
|
|
||||||
Align::x(TransportView::new(true, &self.player.clock)),
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
fn status (&self) -> impl Content<TuiOut> + use<'_> {
|
|
||||||
row!(
|
|
||||||
self.player.play_status(),
|
|
||||||
self.player.next_status(),
|
|
||||||
self.editor.clip_status(),
|
|
||||||
self.editor.edit_status(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
fn sample (&self) -> impl Content<TuiOut> + use<'_> {
|
|
||||||
let note_pt = self.editor.note_point();
|
|
||||||
let sample_h = if self.compact { 0 } else { 5 };
|
|
||||||
Max::y(sample_h, Fill::xy(
|
|
||||||
Bsp::a(
|
|
||||||
Fill::x(Align::w(Fixed::y(1, self.sampler.status(note_pt)))),
|
|
||||||
self.sampler.viewer(note_pt))))
|
|
||||||
}
|
|
||||||
fn pool (&self) -> impl Content<TuiOut> + use<'_> {
|
|
||||||
let w = self.size.w();
|
|
||||||
let pool_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
|
|
||||||
Fixed::x(if self.compact { 5 } else { pool_w },
|
|
||||||
PoolView(self.compact, &self.pool))
|
|
||||||
}
|
|
||||||
fn sampler (&self) -> impl Content<TuiOut> + use<'_> {
|
|
||||||
let note_pt = self.editor.note_point();
|
|
||||||
let sampler_w = if self.compact { 4 } else { 40 };
|
|
||||||
let sampler_y = if self.compact { 1 } else { 0 };
|
|
||||||
Fixed::x(sampler_w, Push::y(sampler_y, Fill::y(self.sampler.list(self.compact, &self.editor))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
has_clock!(|self: Groovebox|self.player.clock());
|
|
||||||
|
|
||||||
///// Status bar for sequencer app
|
|
||||||
//#[derive(Clone)]
|
|
||||||
//pub struct GrooveboxStatus {
|
|
||||||
//pub(crate) width: usize,
|
|
||||||
//pub(crate) cpu: Option<String>,
|
|
||||||
//pub(crate) size: String,
|
|
||||||
//pub(crate) playing: bool,
|
|
||||||
//}
|
|
||||||
//from!(|state: &Groovebox|GrooveboxStatus = {
|
|
||||||
//let samples = state.clock().chunk.load(Relaxed);
|
|
||||||
//let rate = state.clock().timebase.sr.get();
|
|
||||||
//let buffer = samples as f64 / rate;
|
|
||||||
//let width = state.size.w();
|
|
||||||
//Self {
|
|
||||||
//width,
|
|
||||||
//playing: state.clock().is_rolling(),
|
|
||||||
//cpu: state.perf.percentage().map(|cpu|format!("│{cpu:.01}%")),
|
|
||||||
//size: format!("{}x{}│", width, state.size.h()),
|
|
||||||
//}
|
|
||||||
//});
|
|
||||||
//render!(TuiOut: (self: GrooveboxStatus) => Fixed::y(2, lay!(
|
|
||||||
//Self::help(),
|
|
||||||
//Fill::xy(Align::se(Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), self.stats()))),
|
|
||||||
//)));
|
|
||||||
//impl GrooveboxStatus {
|
|
||||||
//fn help () -> impl Content<TuiOut> {
|
|
||||||
//let single = |binding, command|row!(" ", col!(
|
|
||||||
//Tui::fg(TuiTheme::yellow(), binding),
|
|
||||||
//command
|
|
||||||
//));
|
|
||||||
//let double = |(b1, c1), (b2, c2)|col!(
|
|
||||||
//row!(" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1,),
|
|
||||||
//row!(" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2,),
|
|
||||||
//);
|
|
||||||
//Tui::fg_bg(TuiTheme::g(255), TuiTheme::g(50), row!(
|
|
||||||
//single("SPACE", "play/pause"),
|
|
||||||
//double(("▲▼▶◀", "cursor"), ("Ctrl", "scroll"), ),
|
|
||||||
//double(("a", "append"), ("s", "set note"),),
|
|
||||||
//double((",.", "length"), ("<>", "triplet"), ),
|
|
||||||
//double(("[]", "phrase"), ("{}", "order"), ),
|
|
||||||
//double(("q", "enqueue"), ("e", "edit"), ),
|
|
||||||
//double(("c", "color"), ("", ""),),
|
|
||||||
//))
|
|
||||||
//}
|
|
||||||
//fn stats (&self) -> impl Content<TuiOut> + use<'_> {
|
|
||||||
//row!(&self.cpu, &self.size)
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//macro_rules! edn_context {
|
|
||||||
//($Struct:ident |$l:lifetime, $state:ident| {
|
|
||||||
//$($key:literal = $field:ident: $Type:ty => $expr:expr,)*
|
|
||||||
//}) => {
|
|
||||||
|
|
||||||
//#[derive(Default)]
|
|
||||||
//pub struct EdnView<$l> { $($field: Option<$Type>),* }
|
|
||||||
|
|
||||||
//impl<$l> EdnView<$l> {
|
|
||||||
//pub fn parse <'e> (edn: &[Edn<'e>]) -> impl Fn(&$Struct) + use<'e> {
|
|
||||||
//let imports = Self::imports_all(edn);
|
|
||||||
//move |state| {
|
|
||||||
//let mut context = EdnView::default();
|
|
||||||
//for import in imports.iter() {
|
|
||||||
//context.import(state, import)
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//fn imports_all <'e> (edn: &[Edn<'e>]) -> Vec<&'e str> {
|
|
||||||
//let mut imports = vec![];
|
|
||||||
//for edn in edn.iter() {
|
|
||||||
//for import in Self::imports_one(edn) {
|
|
||||||
//imports.push(import);
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//imports
|
|
||||||
//}
|
|
||||||
//fn imports_one <'e> (edn: &Edn<'e>) -> Vec<&'e str> {
|
|
||||||
//match edn {
|
|
||||||
//Edn::Symbol(import) => vec![import],
|
|
||||||
//Edn::List(edn) => Self::imports_all(edn.as_slice()),
|
|
||||||
//_ => vec![],
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//pub fn import (&mut self, $state: &$l$Struct, key: &str) {
|
|
||||||
//match key {
|
|
||||||
//$($key => self.$field = Some($expr),)*
|
|
||||||
//_ => {}
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
|
|
||||||
////impl Groovebox {
|
|
||||||
////fn status (&self) -> impl Content<TuiOut> + use<'_> {
|
|
||||||
////let note_pt = self.editor.note_point();
|
|
||||||
////Align::w(Fixed::y(1, ))
|
|
||||||
////}
|
|
||||||
////fn pool (&self) -> impl Content<TuiOut> + use<'_> {
|
|
||||||
////let w = self.size.w();
|
|
||||||
////let pool_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
|
|
||||||
////Fixed::x(if self.compact { 5 } else { pool_w },
|
|
||||||
////)
|
|
||||||
////}
|
|
||||||
////fn sampler (&self) -> impl Content<TuiOut> + use<'_> {
|
|
||||||
////let sampler_w = if self.compact { 4 } else { 11 };
|
|
||||||
////let sampler_y = if self.compact { 1 } else { 0 };
|
|
||||||
////Fixed::x(sampler_w, Push::y(sampler_y, Fill::y(
|
|
||||||
////SampleList::new(self.compact, &self.sampler, &self.editor))))
|
|
||||||
////}
|
|
||||||
////}
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ pub type Usually<T> = std::result::Result<T, Box<dyn Error>>;
|
||||||
/// Standard optional result type.
|
/// Standard optional result type.
|
||||||
pub type Perhaps<T> = std::result::Result<Option<T>, Box<dyn Error>>;
|
pub type Perhaps<T> = std::result::Result<Option<T>, Box<dyn Error>>;
|
||||||
|
|
||||||
pub mod app; pub use self::app::*;
|
pub mod model; pub use self::model::*;
|
||||||
pub mod view; pub use self::view::*;
|
pub mod view; pub use self::view::*;
|
||||||
pub mod control; pub use self::control::*;
|
pub mod control; pub use self::control::*;
|
||||||
pub mod audio; pub use self::audio::*;
|
pub mod audio; pub use self::audio::*;
|
||||||
|
|
|
||||||
|
|
@ -87,3 +87,49 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub struct Sequencer {
|
||||||
|
pub jack: Arc<RwLock<JackConnection>>,
|
||||||
|
pub compact: bool,
|
||||||
|
pub editor: MidiEditor,
|
||||||
|
pub midi_buf: Vec<Vec<Vec<u8>>>,
|
||||||
|
pub note_buf: Vec<u8>,
|
||||||
|
pub perf: PerfModel,
|
||||||
|
pub player: MidiPlayer,
|
||||||
|
pub pool: PoolModel,
|
||||||
|
pub selectors: bool,
|
||||||
|
pub size: Measure<TuiOut>,
|
||||||
|
pub status: bool,
|
||||||
|
pub transport: bool,
|
||||||
|
}
|
||||||
|
pub struct Groovebox {
|
||||||
|
pub jack: Arc<RwLock<JackConnection>>,
|
||||||
|
pub compact: bool,
|
||||||
|
pub editor: MidiEditor,
|
||||||
|
pub midi_buf: Vec<Vec<Vec<u8>>>,
|
||||||
|
pub note_buf: Vec<u8>,
|
||||||
|
pub perf: PerfModel,
|
||||||
|
pub player: MidiPlayer,
|
||||||
|
pub pool: PoolModel,
|
||||||
|
pub sampler: Sampler,
|
||||||
|
pub size: Measure<TuiOut>,
|
||||||
|
pub status: bool,
|
||||||
|
}
|
||||||
|
pub struct Arranger {
|
||||||
|
pub clock: Clock,
|
||||||
|
pub color: ItemPalette,
|
||||||
|
pub compact: bool,
|
||||||
|
pub editing: AtomicBool,
|
||||||
|
pub editor: MidiEditor,
|
||||||
|
pub jack: Arc<RwLock<JackConnection>>,
|
||||||
|
pub midi_buf: Vec<Vec<Vec<u8>>>,
|
||||||
|
pub midi_ins: Vec<JackPort<MidiIn>>,
|
||||||
|
pub midi_outs: Vec<JackPort<MidiOut>>,
|
||||||
|
pub note_buf: Vec<u8>,
|
||||||
|
pub perf: PerfModel,
|
||||||
|
pub pool: PoolModel,
|
||||||
|
pub scenes: Vec<ArrangerScene>,
|
||||||
|
pub selected: ArrangerSelection,
|
||||||
|
pub size: Measure<TuiOut>,
|
||||||
|
pub splits: [u16;2],
|
||||||
|
pub tracks: Vec<ArrangerTrack>,
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
use super::*;
|
use super::*;
|
||||||
use PhraseLengthFocus::*;
|
use ClipLengthFocus::*;
|
||||||
use PhraseLengthCommand::*;
|
use ClipLengthCommand::*;
|
||||||
use KeyCode::{Up, Down, Left, Right, Enter, Esc};
|
use KeyCode::{Up, Down, Left, Right, Enter, Esc};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
@ -71,7 +71,7 @@ pub enum PoolMode {
|
||||||
/// Renaming a pattern
|
/// Renaming a pattern
|
||||||
Rename(usize, Arc<str>),
|
Rename(usize, Arc<str>),
|
||||||
/// Editing the length of a pattern
|
/// Editing the length of a pattern
|
||||||
Length(usize, usize, PhraseLengthFocus),
|
Length(usize, usize, ClipLengthFocus),
|
||||||
/// Load clip from disk
|
/// Load clip from disk
|
||||||
Import(usize, FileBrowser),
|
Import(usize, FileBrowser),
|
||||||
/// Save clip to disk
|
/// Save clip to disk
|
||||||
|
|
@ -82,13 +82,13 @@ pub enum PoolMode {
|
||||||
pub enum PoolCommand {
|
pub enum PoolCommand {
|
||||||
Show(bool),
|
Show(bool),
|
||||||
/// Update the contents of the clip pool
|
/// Update the contents of the clip pool
|
||||||
Phrase(MidiPoolCommand),
|
Clip(MidiPoolCommand),
|
||||||
/// Select a clip from the clip pool
|
/// Select a clip from the clip pool
|
||||||
Select(usize),
|
Select(usize),
|
||||||
/// Rename a clip
|
/// Rename a clip
|
||||||
Rename(PhraseRenameCommand),
|
Rename(ClipRenameCommand),
|
||||||
/// Change the length of a clip
|
/// Change the length of a clip
|
||||||
Length(PhraseLengthCommand),
|
Length(ClipLengthCommand),
|
||||||
/// Import from file
|
/// Import from file
|
||||||
Import(FileBrowserCommand),
|
Import(FileBrowserCommand),
|
||||||
/// Export to file
|
/// Export to file
|
||||||
|
|
@ -103,17 +103,17 @@ command!(|self:PoolCommand, state: PoolModel|{
|
||||||
Some(Self::Show(!visible))
|
Some(Self::Show(!visible))
|
||||||
}
|
}
|
||||||
Rename(command) => match command {
|
Rename(command) => match command {
|
||||||
PhraseRenameCommand::Begin => {
|
ClipRenameCommand::Begin => {
|
||||||
let length = state.clips()[state.clip_index()].read().unwrap().length;
|
let length = state.clips()[state.clip_index()].read().unwrap().length;
|
||||||
*state.clips_mode_mut() = Some(
|
*state.clips_mode_mut() = Some(
|
||||||
PoolMode::Length(state.clip_index(), length, PhraseLengthFocus::Bar)
|
PoolMode::Length(state.clip_index(), length, ClipLengthFocus::Bar)
|
||||||
);
|
);
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
_ => command.execute(state)?.map(Rename)
|
_ => command.execute(state)?.map(Rename)
|
||||||
},
|
},
|
||||||
Length(command) => match command {
|
Length(command) => match command {
|
||||||
PhraseLengthCommand::Begin => {
|
ClipLengthCommand::Begin => {
|
||||||
let name = state.clips()[state.clip_index()].read().unwrap().name.clone();
|
let name = state.clips()[state.clip_index()].read().unwrap().name.clone();
|
||||||
*state.clips_mode_mut() = Some(
|
*state.clips_mode_mut() = Some(
|
||||||
PoolMode::Rename(state.clip_index(), name)
|
PoolMode::Rename(state.clip_index(), name)
|
||||||
|
|
@ -144,13 +144,13 @@ command!(|self:PoolCommand, state: PoolModel|{
|
||||||
state.set_clip_index(clip);
|
state.set_clip_index(clip);
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
Phrase(command) => command.execute(state)?.map(Phrase),
|
Clip(command) => command.execute(state)?.map(Clip),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
input_to_command!(PoolCommand: |state: PoolModel, input: Event|match state.clips_mode() {
|
input_to_command!(PoolCommand: |state: PoolModel, input: Event|match state.clips_mode() {
|
||||||
Some(PoolMode::Rename(..)) => Self::Rename(PhraseRenameCommand::input_to_command(state, input)?),
|
Some(PoolMode::Rename(..)) => Self::Rename(ClipRenameCommand::input_to_command(state, input)?),
|
||||||
Some(PoolMode::Length(..)) => Self::Length(PhraseLengthCommand::input_to_command(state, input)?),
|
Some(PoolMode::Length(..)) => Self::Length(ClipLengthCommand::input_to_command(state, input)?),
|
||||||
Some(PoolMode::Import(..)) => Self::Import(FileBrowserCommand::input_to_command(state, input)?),
|
Some(PoolMode::Import(..)) => Self::Import(FileBrowserCommand::input_to_command(state, input)?),
|
||||||
Some(PoolMode::Export(..)) => Self::Export(FileBrowserCommand::input_to_command(state, input)?),
|
Some(PoolMode::Export(..)) => Self::Export(FileBrowserCommand::input_to_command(state, input)?),
|
||||||
_ => to_clips_command(state, input)?
|
_ => to_clips_command(state, input)?
|
||||||
|
|
@ -162,11 +162,11 @@ fn to_clips_command (state: &PoolModel, input: &Event) -> Option<PoolCommand> {
|
||||||
let index = state.clip_index();
|
let index = state.clip_index();
|
||||||
let count = state.clips().len();
|
let count = state.clips().len();
|
||||||
Some(match input {
|
Some(match input {
|
||||||
kpat!(Char('n')) => Cmd::Rename(PhraseRenameCommand::Begin),
|
kpat!(Char('n')) => Cmd::Rename(ClipRenameCommand::Begin),
|
||||||
kpat!(Char('t')) => Cmd::Length(PhraseLengthCommand::Begin),
|
kpat!(Char('t')) => Cmd::Length(ClipLengthCommand::Begin),
|
||||||
kpat!(Char('m')) => Cmd::Import(FileBrowserCommand::Begin),
|
kpat!(Char('m')) => Cmd::Import(FileBrowserCommand::Begin),
|
||||||
kpat!(Char('x')) => Cmd::Export(FileBrowserCommand::Begin),
|
kpat!(Char('x')) => Cmd::Export(FileBrowserCommand::Begin),
|
||||||
kpat!(Char('c')) => Cmd::Phrase(MidiPoolCommand::SetColor(index, ItemColor::random())),
|
kpat!(Char('c')) => Cmd::Clip(MidiPoolCommand::SetColor(index, ItemColor::random())),
|
||||||
kpat!(Char('[')) | kpat!(Up) => Cmd::Select(
|
kpat!(Char('[')) | kpat!(Up) => Cmd::Select(
|
||||||
index.overflowing_sub(1).0.min(state.clips().len() - 1)
|
index.overflowing_sub(1).0.min(state.clips().len() - 1)
|
||||||
),
|
),
|
||||||
|
|
@ -175,32 +175,32 @@ fn to_clips_command (state: &PoolModel, input: &Event) -> Option<PoolCommand> {
|
||||||
),
|
),
|
||||||
kpat!(Char('<')) => if index > 1 {
|
kpat!(Char('<')) => if index > 1 {
|
||||||
state.set_clip_index(state.clip_index().saturating_sub(1));
|
state.set_clip_index(state.clip_index().saturating_sub(1));
|
||||||
Cmd::Phrase(MidiPoolCommand::Swap(index - 1, index))
|
Cmd::Clip(MidiPoolCommand::Swap(index - 1, index))
|
||||||
} else {
|
} else {
|
||||||
return None
|
return None
|
||||||
},
|
},
|
||||||
kpat!(Char('>')) => if index < count.saturating_sub(1) {
|
kpat!(Char('>')) => if index < count.saturating_sub(1) {
|
||||||
state.set_clip_index(state.clip_index() + 1);
|
state.set_clip_index(state.clip_index() + 1);
|
||||||
Cmd::Phrase(MidiPoolCommand::Swap(index + 1, index))
|
Cmd::Clip(MidiPoolCommand::Swap(index + 1, index))
|
||||||
} else {
|
} else {
|
||||||
return None
|
return None
|
||||||
},
|
},
|
||||||
kpat!(Delete) => if index > 0 {
|
kpat!(Delete) => if index > 0 {
|
||||||
state.set_clip_index(index.min(count.saturating_sub(1)));
|
state.set_clip_index(index.min(count.saturating_sub(1)));
|
||||||
Cmd::Phrase(MidiPoolCommand::Delete(index))
|
Cmd::Clip(MidiPoolCommand::Delete(index))
|
||||||
} else {
|
} else {
|
||||||
return None
|
return None
|
||||||
},
|
},
|
||||||
kpat!(Char('a')) | kpat!(Shift-Char('A')) => Cmd::Phrase(MidiPoolCommand::Add(count, MidiClip::new(
|
kpat!(Char('a')) | kpat!(Shift-Char('A')) => Cmd::Clip(MidiPoolCommand::Add(count, MidiClip::new(
|
||||||
"Clip", true, 4 * PPQ, None, Some(ItemPalette::random())
|
"Clip", true, 4 * PPQ, None, Some(ItemPalette::random())
|
||||||
))),
|
))),
|
||||||
kpat!(Char('i')) => Cmd::Phrase(MidiPoolCommand::Add(index + 1, MidiClip::new(
|
kpat!(Char('i')) => Cmd::Clip(MidiPoolCommand::Add(index + 1, MidiClip::new(
|
||||||
"Clip", true, 4 * PPQ, None, Some(ItemPalette::random())
|
"Clip", true, 4 * PPQ, None, Some(ItemPalette::random())
|
||||||
))),
|
))),
|
||||||
kpat!(Char('d')) | kpat!(Shift-Char('D')) => {
|
kpat!(Char('d')) | kpat!(Shift-Char('D')) => {
|
||||||
let mut clip = state.clips()[index].read().unwrap().duplicate();
|
let mut clip = state.clips()[index].read().unwrap().duplicate();
|
||||||
clip.color = ItemPalette::random_near(clip.color, 0.25);
|
clip.color = ItemPalette::random_near(clip.color, 0.25);
|
||||||
Cmd::Phrase(MidiPoolCommand::Add(index + 1, clip))
|
Cmd::Clip(MidiPoolCommand::Add(index + 1, clip))
|
||||||
},
|
},
|
||||||
_ => return None
|
_ => return None
|
||||||
})
|
})
|
||||||
|
|
@ -295,7 +295,7 @@ input_to_command!(FileBrowserCommand: |state: PoolModel, input: Event|{
|
||||||
|
|
||||||
/// Displays and edits clip length.
|
/// Displays and edits clip length.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct PhraseLength {
|
pub struct ClipLength {
|
||||||
/// Pulses per beat (quaver)
|
/// Pulses per beat (quaver)
|
||||||
pub ppq: usize,
|
pub ppq: usize,
|
||||||
/// Beats per bar
|
/// Beats per bar
|
||||||
|
|
@ -303,11 +303,11 @@ pub struct PhraseLength {
|
||||||
/// Length of clip in pulses
|
/// Length of clip in pulses
|
||||||
pub pulses: usize,
|
pub pulses: usize,
|
||||||
/// Selected subdivision
|
/// Selected subdivision
|
||||||
pub focus: Option<PhraseLengthFocus>,
|
pub focus: Option<ClipLengthFocus>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PhraseLength {
|
impl ClipLength {
|
||||||
pub fn new (pulses: usize, focus: Option<PhraseLengthFocus>) -> Self {
|
pub fn new (pulses: usize, focus: Option<ClipLengthFocus>) -> Self {
|
||||||
Self { ppq: PPQ, bpb: 4, pulses, focus }
|
Self { ppq: PPQ, bpb: 4, pulses, focus }
|
||||||
}
|
}
|
||||||
pub fn bars (&self) -> usize {
|
pub fn bars (&self) -> usize {
|
||||||
|
|
@ -330,9 +330,9 @@ impl PhraseLength {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Focused field of `PhraseLength`
|
/// Focused field of `ClipLength`
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub enum PhraseLengthFocus {
|
pub enum ClipLengthFocus {
|
||||||
/// Editing the number of bars
|
/// Editing the number of bars
|
||||||
Bar,
|
Bar,
|
||||||
/// Editing the number of beats
|
/// Editing the number of beats
|
||||||
|
|
@ -341,7 +341,7 @@ pub enum PhraseLengthFocus {
|
||||||
Tick,
|
Tick,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PhraseLengthFocus {
|
impl ClipLengthFocus {
|
||||||
pub fn next (&mut self) {
|
pub fn next (&mut self) {
|
||||||
*self = match self {
|
*self = match self {
|
||||||
Self::Bar => Self::Beat,
|
Self::Bar => Self::Beat,
|
||||||
|
|
@ -358,24 +358,24 @@ impl PhraseLengthFocus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render!(TuiOut: (self: PhraseLength) => {
|
render!(TuiOut: (self: ClipLength) => {
|
||||||
let bars = ||self.bars_string();
|
let bars = ||self.bars_string();
|
||||||
let beats = ||self.beats_string();
|
let beats = ||self.beats_string();
|
||||||
let ticks = ||self.ticks_string();
|
let ticks = ||self.ticks_string();
|
||||||
match self.focus {
|
match self.focus {
|
||||||
None =>
|
None =>
|
||||||
row!(" ", bars(), ".", beats(), ".", ticks()),
|
row!(" ", bars(), ".", beats(), ".", ticks()),
|
||||||
Some(PhraseLengthFocus::Bar) =>
|
Some(ClipLengthFocus::Bar) =>
|
||||||
row!("[", bars(), "]", beats(), ".", ticks()),
|
row!("[", bars(), "]", beats(), ".", ticks()),
|
||||||
Some(PhraseLengthFocus::Beat) =>
|
Some(ClipLengthFocus::Beat) =>
|
||||||
row!(" ", bars(), "[", beats(), "]", ticks()),
|
row!(" ", bars(), "[", beats(), "]", ticks()),
|
||||||
Some(PhraseLengthFocus::Tick) =>
|
Some(ClipLengthFocus::Tick) =>
|
||||||
row!(" ", bars(), ".", beats(), "[", ticks()),
|
row!(" ", bars(), ".", beats(), "[", ticks()),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
pub enum PhraseLengthCommand {
|
pub enum ClipLengthCommand {
|
||||||
Begin,
|
Begin,
|
||||||
Cancel,
|
Cancel,
|
||||||
Set(usize),
|
Set(usize),
|
||||||
|
|
@ -385,7 +385,7 @@ pub enum PhraseLengthCommand {
|
||||||
Dec,
|
Dec,
|
||||||
}
|
}
|
||||||
|
|
||||||
command!(|self:PhraseLengthCommand,state:PoolModel|{
|
command!(|self:ClipLengthCommand,state:PoolModel|{
|
||||||
match state.clips_mode_mut().clone() {
|
match state.clips_mode_mut().clone() {
|
||||||
Some(PoolMode::Length(clip, ref mut length, ref mut focus)) => match self {
|
Some(PoolMode::Length(clip, ref mut length, ref mut focus)) => match self {
|
||||||
Cancel => { *state.clips_mode_mut() = None; },
|
Cancel => { *state.clips_mode_mut() = None; },
|
||||||
|
|
@ -418,7 +418,7 @@ command!(|self:PhraseLengthCommand,state:PoolModel|{
|
||||||
None
|
None
|
||||||
});
|
});
|
||||||
|
|
||||||
input_to_command!(PhraseLengthCommand: |state: PoolModel, input: Event|{
|
input_to_command!(ClipLengthCommand: |state: PoolModel, input: Event|{
|
||||||
if let Some(PoolMode::Length(_, length, _)) = state.clips_mode() {
|
if let Some(PoolMode::Length(_, length, _)) = state.clips_mode() {
|
||||||
match input {
|
match input {
|
||||||
kpat!(Up) => Self::Inc,
|
kpat!(Up) => Self::Inc,
|
||||||
|
|
@ -437,16 +437,16 @@ use crate::*;
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub enum PhraseRenameCommand {
|
pub enum ClipRenameCommand {
|
||||||
Begin,
|
Begin,
|
||||||
Cancel,
|
Cancel,
|
||||||
Confirm,
|
Confirm,
|
||||||
Set(Arc<str>),
|
Set(Arc<str>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Command<PoolModel> for PhraseRenameCommand {
|
impl Command<PoolModel> for ClipRenameCommand {
|
||||||
fn execute (self, state: &mut PoolModel) -> Perhaps<Self> {
|
fn execute (self, state: &mut PoolModel) -> Perhaps<Self> {
|
||||||
use PhraseRenameCommand::*;
|
use ClipRenameCommand::*;
|
||||||
match state.clips_mode_mut().clone() {
|
match state.clips_mode_mut().clone() {
|
||||||
Some(PoolMode::Rename(clip, ref mut old_name)) => match self {
|
Some(PoolMode::Rename(clip, ref mut old_name)) => match self {
|
||||||
Set(s) => {
|
Set(s) => {
|
||||||
|
|
@ -469,7 +469,7 @@ impl Command<PoolModel> for PhraseRenameCommand {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InputToCommand<Event, PoolModel> for PhraseRenameCommand {
|
impl InputToCommand<Event, PoolModel> for ClipRenameCommand {
|
||||||
fn input_to_command (state: &PoolModel, input: &Event) -> Option<Self> {
|
fn input_to_command (state: &PoolModel, input: &Event) -> Option<Self> {
|
||||||
use KeyCode::{Char, Backspace, Enter, Esc};
|
use KeyCode::{Char, Backspace, Enter, Esc};
|
||||||
if let Some(PoolMode::Rename(_, ref old_name)) = state.clips_mode() {
|
if let Some(PoolMode::Rename(_, ref old_name)) = state.clips_mode() {
|
||||||
|
|
|
||||||
|
|
@ -4,83 +4,6 @@ use KeyCode::{Tab, Char};
|
||||||
use SequencerCommand as Cmd;
|
use SequencerCommand as Cmd;
|
||||||
use MidiEditCommand::*;
|
use MidiEditCommand::*;
|
||||||
use MidiPoolCommand::*;
|
use MidiPoolCommand::*;
|
||||||
render!(TuiOut: (self: Sequencer) => self.size.of(EdnView::from_source(self, Self::EDN)));
|
|
||||||
/// Root view for standalone `tek_sequencer`.
|
|
||||||
pub struct Sequencer {
|
|
||||||
pub _jack: Arc<RwLock<JackConnection>>,
|
|
||||||
|
|
||||||
pub pool: PoolModel,
|
|
||||||
pub editor: MidiEditor,
|
|
||||||
pub player: MidiPlayer,
|
|
||||||
|
|
||||||
pub transport: bool,
|
|
||||||
pub selectors: bool,
|
|
||||||
pub compact: bool,
|
|
||||||
|
|
||||||
pub size: Measure<TuiOut>,
|
|
||||||
pub status: bool,
|
|
||||||
pub note_buf: Vec<u8>,
|
|
||||||
pub midi_buf: Vec<Vec<Vec<u8>>>,
|
|
||||||
pub perf: PerfModel,
|
|
||||||
}
|
|
||||||
impl EdnViewData<TuiOut> for &Sequencer {
|
|
||||||
fn get_content <'a> (&'a self, item: EdnItem<&'a str>) -> RenderBox<'a, TuiOut> {
|
|
||||||
use EdnItem::*;
|
|
||||||
match item {
|
|
||||||
Nil => Box::new(()),
|
|
||||||
Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())),
|
|
||||||
Sym(":editor") => (&self.editor).boxed(),
|
|
||||||
Sym(":pool") => self.pool_view().boxed(),
|
|
||||||
Sym(":status") => self.status_view().boxed(),
|
|
||||||
Sym(":toolbar") => self.toolbar_view().boxed(),
|
|
||||||
_ => panic!("no content for {item:?}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Sequencer {
|
|
||||||
const EDN: &'static str = include_str!("sequencer.edn");
|
|
||||||
fn toolbar_view (&self) -> impl Content<TuiOut> + use<'_> {
|
|
||||||
Fill::x(Fixed::y(2, Align::x(TransportView::new(true, &self.player.clock))))
|
|
||||||
}
|
|
||||||
fn status_view (&self) -> impl Content<TuiOut> + use<'_> {
|
|
||||||
Bsp::e(
|
|
||||||
When(self.selectors, Bsp::e(
|
|
||||||
self.player.play_status(),
|
|
||||||
self.player.next_status(),
|
|
||||||
)),
|
|
||||||
Bsp::e(
|
|
||||||
self.editor.clip_status(),
|
|
||||||
self.editor.edit_status(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
fn pool_view (&self) -> impl Content<TuiOut> + use<'_> {
|
|
||||||
let w = self.size.w();
|
|
||||||
let clip_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
|
|
||||||
let pool_w = if self.pool.visible { clip_w } else { 0 };
|
|
||||||
let pool = Pull::y(1, Fill::y(Align::e(PoolView(self.pool.visible, &self.pool))));
|
|
||||||
Fixed::x(pool_w, Align::e(Fill::y(PoolView(self.compact, &self.pool))))
|
|
||||||
}
|
|
||||||
fn help () -> impl Content<TuiOut> {
|
|
||||||
let single = |binding, command|row!(" ", col!(
|
|
||||||
Tui::fg(TuiTheme::yellow(), binding),
|
|
||||||
command
|
|
||||||
));
|
|
||||||
let double = |(b1, c1), (b2, c2)|col!(
|
|
||||||
row!(" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1,),
|
|
||||||
row!(" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2,),
|
|
||||||
);
|
|
||||||
Tui::fg_bg(TuiTheme::g(255), TuiTheme::g(50), row!(
|
|
||||||
single("SPACE", "play/pause"),
|
|
||||||
double(("▲▼▶◀", "cursor"), ("Ctrl", "scroll"), ),
|
|
||||||
double(("a", "append"), ("s", "set note"),),
|
|
||||||
double((",.", "length"), ("<>", "triplet"), ),
|
|
||||||
double(("[]", "clip"), ("{}", "order"), ),
|
|
||||||
double(("q", "enqueue"), ("e", "edit"), ),
|
|
||||||
double(("c", "color"), ("", ""),),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
has_size!(<TuiOut>|self:Sequencer|&self.size);
|
has_size!(<TuiOut>|self:Sequencer|&self.size);
|
||||||
has_clock!(|self:Sequencer|&self.player.clock);
|
has_clock!(|self:Sequencer|&self.player.clock);
|
||||||
has_clips!(|self:Sequencer|self.pool.clips);
|
has_clips!(|self:Sequencer|self.pool.clips);
|
||||||
|
|
|
||||||
737
tek/src/view.rs
737
tek/src/view.rs
|
|
@ -336,3 +336,740 @@ fn mute_solo (bg: Color, mute: bool, solo: bool) -> impl Content<TuiOut> {
|
||||||
fn cell <T: Content<TuiOut>> (color: ItemPalette, field: T) -> impl Content<TuiOut> {
|
fn cell <T: Content<TuiOut>> (color: ItemPalette, field: T) -> impl Content<TuiOut> {
|
||||||
Tui::fg_bg(color.lightest.rgb, color.base.rgb, Fixed::y(1, field))
|
Tui::fg_bg(color.lightest.rgb, color.base.rgb, Fixed::y(1, field))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
render!(TuiOut: (self: Sequencer) => self.size.of(EdnView::from_source(self, Self::EDN)));
|
||||||
|
impl EdnViewData<TuiOut> for &Sequencer {
|
||||||
|
fn get_content <'a> (&'a self, item: EdnItem<&'a str>) -> RenderBox<'a, TuiOut> {
|
||||||
|
use EdnItem::*;
|
||||||
|
match item {
|
||||||
|
Nil => Box::new(()),
|
||||||
|
Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())),
|
||||||
|
Sym(":editor") => (&self.editor).boxed(),
|
||||||
|
Sym(":pool") => self.pool_view().boxed(),
|
||||||
|
Sym(":status") => self.status_view().boxed(),
|
||||||
|
Sym(":toolbar") => self.toolbar_view().boxed(),
|
||||||
|
_ => panic!("no content for {item:?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Sequencer {
|
||||||
|
const EDN: &'static str = include_str!("sequencer.edn");
|
||||||
|
fn toolbar_view (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
Fill::x(Fixed::y(2, Align::x(TransportView::new(true, &self.player.clock))))
|
||||||
|
}
|
||||||
|
fn status_view (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
Bsp::e(
|
||||||
|
When(self.selectors, Bsp::e(
|
||||||
|
self.player.play_status(),
|
||||||
|
self.player.next_status(),
|
||||||
|
)),
|
||||||
|
Bsp::e(
|
||||||
|
self.editor.clip_status(),
|
||||||
|
self.editor.edit_status(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn pool_view (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
let w = self.size.w();
|
||||||
|
let clip_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
|
||||||
|
let pool_w = if self.pool.visible { clip_w } else { 0 };
|
||||||
|
let pool = Pull::y(1, Fill::y(Align::e(PoolView(self.pool.visible, &self.pool))));
|
||||||
|
Fixed::x(pool_w, Align::e(Fill::y(PoolView(self.compact, &self.pool))))
|
||||||
|
}
|
||||||
|
fn help () -> impl Content<TuiOut> {
|
||||||
|
let single = |binding, command|row!(" ", col!(
|
||||||
|
Tui::fg(TuiTheme::yellow(), binding),
|
||||||
|
command
|
||||||
|
));
|
||||||
|
let double = |(b1, c1), (b2, c2)|col!(
|
||||||
|
row!(" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1,),
|
||||||
|
row!(" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2,),
|
||||||
|
);
|
||||||
|
Tui::fg_bg(TuiTheme::g(255), TuiTheme::g(50), row!(
|
||||||
|
single("SPACE", "play/pause"),
|
||||||
|
double(("▲▼▶◀", "cursor"), ("Ctrl", "scroll"), ),
|
||||||
|
double(("a", "append"), ("s", "set note"),),
|
||||||
|
double((",.", "length"), ("<>", "triplet"), ),
|
||||||
|
double(("[]", "clip"), ("{}", "order"), ),
|
||||||
|
double(("q", "enqueue"), ("e", "edit"), ),
|
||||||
|
double(("c", "color"), ("", ""),),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
render!(TuiOut: (self: Groovebox) => self.size.of(EdnView::from_source(self, Self::EDN)));
|
||||||
|
impl EdnViewData<TuiOut> for &Groovebox {
|
||||||
|
fn get_content <'a> (&'a self, item: EdnItem<&'a str>) -> RenderBox<'a, TuiOut> {
|
||||||
|
use EdnItem::*;
|
||||||
|
match item {
|
||||||
|
Nil => Box::new(()),
|
||||||
|
Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())),
|
||||||
|
Sym(":editor") => (&self.editor).boxed(),
|
||||||
|
Sym(":pool") => self.pool().boxed(),
|
||||||
|
Sym(":status") => self.status().boxed(),
|
||||||
|
Sym(":toolbar") => self.toolbar().boxed(),
|
||||||
|
Sym(":sampler") => self.sampler().boxed(),
|
||||||
|
Sym(":sample") => self.sample().boxed(),
|
||||||
|
_ => panic!("no content for {item:?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn get_unit (&self, item: EdnItem<&str>) -> u16 {
|
||||||
|
use EdnItem::*;
|
||||||
|
match item.to_str() {
|
||||||
|
":sample-h" => if self.compact { 0 } else { 5 },
|
||||||
|
":samples-w" => if self.compact { 4 } else { 11 },
|
||||||
|
":samples-y" => if self.compact { 1 } else { 0 },
|
||||||
|
":pool-w" => if self.compact { 5 } else {
|
||||||
|
let w = self.size.w();
|
||||||
|
if w > 60 { 20 } else if w > 40 { 15 } else { 10 }
|
||||||
|
},
|
||||||
|
_ => 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Groovebox {
|
||||||
|
const EDN: &'static str = include_str!("groovebox.edn");
|
||||||
|
fn toolbar (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
Fill::x(Fixed::y(2, lay!(
|
||||||
|
Fill::x(Align::w(Meter("L/", self.sampler.input_meter[0]))),
|
||||||
|
Fill::x(Align::e(Meter("R/", self.sampler.input_meter[1]))),
|
||||||
|
Align::x(TransportView::new(true, &self.player.clock)),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
fn status (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
row!(
|
||||||
|
self.player.play_status(),
|
||||||
|
self.player.next_status(),
|
||||||
|
self.editor.clip_status(),
|
||||||
|
self.editor.edit_status(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn sample (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
let note_pt = self.editor.note_point();
|
||||||
|
let sample_h = if self.compact { 0 } else { 5 };
|
||||||
|
Max::y(sample_h, Fill::xy(
|
||||||
|
Bsp::a(
|
||||||
|
Fill::x(Align::w(Fixed::y(1, self.sampler.status(note_pt)))),
|
||||||
|
self.sampler.viewer(note_pt))))
|
||||||
|
}
|
||||||
|
fn pool (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
let w = self.size.w();
|
||||||
|
let pool_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
|
||||||
|
Fixed::x(if self.compact { 5 } else { pool_w },
|
||||||
|
PoolView(self.compact, &self.pool))
|
||||||
|
}
|
||||||
|
fn sampler (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
let note_pt = self.editor.note_point();
|
||||||
|
let sampler_w = if self.compact { 4 } else { 40 };
|
||||||
|
let sampler_y = if self.compact { 1 } else { 0 };
|
||||||
|
Fixed::x(sampler_w, Push::y(sampler_y, Fill::y(self.sampler.list(self.compact, &self.editor))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
has_clock!(|self: Groovebox|self.player.clock());
|
||||||
|
|
||||||
|
///// Status bar for sequencer app
|
||||||
|
//#[derive(Clone)]
|
||||||
|
//pub struct GrooveboxStatus {
|
||||||
|
//pub(crate) width: usize,
|
||||||
|
//pub(crate) cpu: Option<String>,
|
||||||
|
//pub(crate) size: String,
|
||||||
|
//pub(crate) playing: bool,
|
||||||
|
//}
|
||||||
|
//from!(|state: &Groovebox|GrooveboxStatus = {
|
||||||
|
//let samples = state.clock().chunk.load(Relaxed);
|
||||||
|
//let rate = state.clock().timebase.sr.get();
|
||||||
|
//let buffer = samples as f64 / rate;
|
||||||
|
//let width = state.size.w();
|
||||||
|
//Self {
|
||||||
|
//width,
|
||||||
|
//playing: state.clock().is_rolling(),
|
||||||
|
//cpu: state.perf.percentage().map(|cpu|format!("│{cpu:.01}%")),
|
||||||
|
//size: format!("{}x{}│", width, state.size.h()),
|
||||||
|
//}
|
||||||
|
//});
|
||||||
|
//render!(TuiOut: (self: GrooveboxStatus) => Fixed::y(2, lay!(
|
||||||
|
//Self::help(),
|
||||||
|
//Fill::xy(Align::se(Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), self.stats()))),
|
||||||
|
//)));
|
||||||
|
//impl GrooveboxStatus {
|
||||||
|
//fn help () -> impl Content<TuiOut> {
|
||||||
|
//let single = |binding, command|row!(" ", col!(
|
||||||
|
//Tui::fg(TuiTheme::yellow(), binding),
|
||||||
|
//command
|
||||||
|
//));
|
||||||
|
//let double = |(b1, c1), (b2, c2)|col!(
|
||||||
|
//row!(" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1,),
|
||||||
|
//row!(" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2,),
|
||||||
|
//);
|
||||||
|
//Tui::fg_bg(TuiTheme::g(255), TuiTheme::g(50), row!(
|
||||||
|
//single("SPACE", "play/pause"),
|
||||||
|
//double(("▲▼▶◀", "cursor"), ("Ctrl", "scroll"), ),
|
||||||
|
//double(("a", "append"), ("s", "set note"),),
|
||||||
|
//double((",.", "length"), ("<>", "triplet"), ),
|
||||||
|
//double(("[]", "phrase"), ("{}", "order"), ),
|
||||||
|
//double(("q", "enqueue"), ("e", "edit"), ),
|
||||||
|
//double(("c", "color"), ("", ""),),
|
||||||
|
//))
|
||||||
|
//}
|
||||||
|
//fn stats (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
//row!(&self.cpu, &self.size)
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//macro_rules! edn_context {
|
||||||
|
//($Struct:ident |$l:lifetime, $state:ident| {
|
||||||
|
//$($key:literal = $field:ident: $Type:ty => $expr:expr,)*
|
||||||
|
//}) => {
|
||||||
|
|
||||||
|
//#[derive(Default)]
|
||||||
|
//pub struct EdnView<$l> { $($field: Option<$Type>),* }
|
||||||
|
|
||||||
|
//impl<$l> EdnView<$l> {
|
||||||
|
//pub fn parse <'e> (edn: &[Edn<'e>]) -> impl Fn(&$Struct) + use<'e> {
|
||||||
|
//let imports = Self::imports_all(edn);
|
||||||
|
//move |state| {
|
||||||
|
//let mut context = EdnView::default();
|
||||||
|
//for import in imports.iter() {
|
||||||
|
//context.import(state, import)
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//fn imports_all <'e> (edn: &[Edn<'e>]) -> Vec<&'e str> {
|
||||||
|
//let mut imports = vec![];
|
||||||
|
//for edn in edn.iter() {
|
||||||
|
//for import in Self::imports_one(edn) {
|
||||||
|
//imports.push(import);
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//imports
|
||||||
|
//}
|
||||||
|
//fn imports_one <'e> (edn: &Edn<'e>) -> Vec<&'e str> {
|
||||||
|
//match edn {
|
||||||
|
//Edn::Symbol(import) => vec![import],
|
||||||
|
//Edn::List(edn) => Self::imports_all(edn.as_slice()),
|
||||||
|
//_ => vec![],
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//pub fn import (&mut self, $state: &$l$Struct, key: &str) {
|
||||||
|
//match key {
|
||||||
|
//$($key => self.$field = Some($expr),)*
|
||||||
|
//_ => {}
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
////impl Groovebox {
|
||||||
|
////fn status (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
////let note_pt = self.editor.note_point();
|
||||||
|
////Align::w(Fixed::y(1, ))
|
||||||
|
////}
|
||||||
|
////fn pool (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
////let w = self.size.w();
|
||||||
|
////let pool_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
|
||||||
|
////Fixed::x(if self.compact { 5 } else { pool_w },
|
||||||
|
////)
|
||||||
|
////}
|
||||||
|
////fn sampler (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
////let sampler_w = if self.compact { 4 } else { 11 };
|
||||||
|
////let sampler_y = if self.compact { 1 } else { 0 };
|
||||||
|
////Fixed::x(sampler_w, Push::y(sampler_y, Fill::y(
|
||||||
|
////SampleList::new(self.compact, &self.sampler, &self.editor))))
|
||||||
|
////}
|
||||||
|
////}
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
render!(TuiOut: (self: Arranger) => self.size.of(EdnView::from_source(self, Self::EDN)));
|
||||||
|
impl EdnViewData<TuiOut> for &Arranger {
|
||||||
|
fn get_content <'a> (&'a self, item: EdnItem<&'a str>) -> RenderBox<'a, TuiOut> {
|
||||||
|
use EdnItem::*;
|
||||||
|
let tracks_w = self.tracks_with_sizes().last().unwrap().3 as u16;
|
||||||
|
match item {
|
||||||
|
Nil => Box::new(()),
|
||||||
|
Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())),
|
||||||
|
Sym(":editor") => (&self.editor).boxed(),
|
||||||
|
Sym(":pool") => self.pool().boxed(),
|
||||||
|
Sym(":status") => self.status().boxed(),
|
||||||
|
Sym(":toolbar") => self.toolbar().boxed(),
|
||||||
|
Sym(":tracks") => self.track_row(tracks_w).boxed(),
|
||||||
|
Sym(":scenes") => self.scene_row(tracks_w).boxed(),
|
||||||
|
Sym(":inputs") => self.input_row(tracks_w).boxed(),
|
||||||
|
Sym(":outputs") => self.output_row(tracks_w).boxed(),
|
||||||
|
_ => panic!("no content for {item:?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Arranger {
|
||||||
|
const EDN: &'static str = include_str!("arranger.edn");
|
||||||
|
pub const LEFT_SEP: char = '▎';
|
||||||
|
pub const TRACK_MIN_WIDTH: usize = 9;
|
||||||
|
|
||||||
|
fn toolbar (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
Fill::x(Fixed::y(2, Align::x(TransportView::new(true, &self.clock))))
|
||||||
|
}
|
||||||
|
fn pool (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
Align::e(Fixed::x(self.sidebar_w(), PoolView(self.compact, &self.pool)))
|
||||||
|
}
|
||||||
|
fn status (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
Bsp::e(self.editor.clip_status(), self.editor.edit_status())
|
||||||
|
}
|
||||||
|
fn is_editing (&self) -> bool {
|
||||||
|
!self.pool.visible
|
||||||
|
}
|
||||||
|
fn sidebar_w (&self) -> u16 {
|
||||||
|
let w = self.size.w();
|
||||||
|
let w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
|
||||||
|
let w = if self.pool.visible { w } else { 8 };
|
||||||
|
w
|
||||||
|
}
|
||||||
|
fn editor_w (&self) -> usize {
|
||||||
|
//self.editor.note_len() / self.editor.note_zoom().get()
|
||||||
|
(5 + (self.editor.time_len().get() / self.editor.time_zoom().get()))
|
||||||
|
.min(self.size.w().saturating_sub(20))
|
||||||
|
.max(16)
|
||||||
|
//self.editor.time_axis().get().max(16)
|
||||||
|
//50
|
||||||
|
}
|
||||||
|
pub fn scenes_with_sizes (&self, h: usize)
|
||||||
|
-> impl Iterator<Item = (usize, &ArrangerScene, usize, usize)>
|
||||||
|
{
|
||||||
|
let mut y = 0;
|
||||||
|
let editing = self.is_editing();
|
||||||
|
let (selected_track, selected_scene) = match self.selected {
|
||||||
|
ArrangerSelection::Clip(t, s) => (Some(t), Some(s)),
|
||||||
|
_ => (None, None)
|
||||||
|
};
|
||||||
|
self.scenes.iter().enumerate().map(move|(s, scene)|{
|
||||||
|
let active = editing && selected_track.is_some() && selected_scene == Some(s);
|
||||||
|
let height = if active { 15 } else { h };
|
||||||
|
let data = (s, scene, y, y + height);
|
||||||
|
y += height;
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn tracks_with_sizes (&self)
|
||||||
|
-> impl Iterator<Item = (usize, &ArrangerTrack, usize, usize)>
|
||||||
|
{
|
||||||
|
tracks_with_sizes(self.tracks.iter(), match self.selected {
|
||||||
|
ArrangerSelection::Track(t) if self.is_editing() => Some(t),
|
||||||
|
ArrangerSelection::Clip(t, _) if self.is_editing() => Some(t),
|
||||||
|
_ => None
|
||||||
|
}, self.editor_w())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn play_row (&self, tracks_w: u16) -> impl Content<TuiOut> + '_ {
|
||||||
|
let h = 2;
|
||||||
|
Fixed::y(h, Bsp::e(
|
||||||
|
Fixed::xy(self.sidebar_w() as u16, h, self.play_header()),
|
||||||
|
Fill::x(Align::c(Fixed::xy(tracks_w, h, self.play_cells())))
|
||||||
|
))
|
||||||
|
}
|
||||||
|
fn play_header (&self) -> BoxThunk<TuiOut> {
|
||||||
|
(||Tui::bold(true, Tui::fg(TuiTheme::g(128), "Playing")).boxed()).into()
|
||||||
|
}
|
||||||
|
fn play_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
||||||
|
(move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| {
|
||||||
|
//let color = track.color;
|
||||||
|
let color: ItemPalette = track.color.dark.into();
|
||||||
|
let timebase = self.clock().timebase();
|
||||||
|
let value = Tui::fg_bg(color.lightest.rgb, color.base.rgb,
|
||||||
|
if let Some((_, Some(clip))) = track.player.play_clip().as_ref() {
|
||||||
|
let length = clip.read().unwrap().length;
|
||||||
|
let elapsed = track.player.pulses_since_start().unwrap() as usize;
|
||||||
|
format!("+{:>}", timebase.format_beats_1_short((elapsed % length) as f64))
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
});
|
||||||
|
let cell = Bsp::s(value, phat_hi(color.dark.rgb, color.darker.rgb));
|
||||||
|
Tui::bg(color.base.rgb, map_east(x1 as u16, (x2 - x1) as u16, cell))
|
||||||
|
})).boxed()).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_row (&self, tracks_w: u16) -> impl Content<TuiOut> + '_ {
|
||||||
|
let h = 2;
|
||||||
|
Fixed::y(h, Bsp::e(
|
||||||
|
Fixed::xy(self.sidebar_w() as u16, h, self.next_header()),
|
||||||
|
Fill::x(Align::c(Fixed::xy(tracks_w, h, self.next_cells())))
|
||||||
|
))
|
||||||
|
}
|
||||||
|
fn next_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
||||||
|
(||Tui::bold(true, Tui::fg(TuiTheme::g(128), "Next")).boxed()).into()
|
||||||
|
}
|
||||||
|
fn next_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
||||||
|
(move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| {
|
||||||
|
let color: ItemPalette = track.color;
|
||||||
|
let color: ItemPalette = track.color.dark.into();
|
||||||
|
let current = &self.clock().playhead;
|
||||||
|
let timebase = ¤t.timebase;
|
||||||
|
let cell = Self::cell(color, Tui::bold(true, {
|
||||||
|
let mut result = String::new();
|
||||||
|
if let Some((t, _)) = track.player.next_clip().as_ref() {
|
||||||
|
let target = t.pulse.get();
|
||||||
|
let current = current.pulse.get();
|
||||||
|
if target > current {
|
||||||
|
result = format!("-{:>}", timebase.format_beats_0_short(target - current))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}));
|
||||||
|
let cell = Tui::fg_bg(color.lightest.rgb, color.base.rgb, cell);
|
||||||
|
let cell = Bsp::s(cell, phat_hi(color.dark.rgb, color.darker.rgb));
|
||||||
|
Tui::bg(color.base.rgb, map_east(x1 as u16, (x2 - x1) as u16, cell))
|
||||||
|
})).boxed()).into()
|
||||||
|
}
|
||||||
|
/// beats until switchover
|
||||||
|
fn cell_until_next (track: &ArrangerTrack, current: &Arc<Moment>)
|
||||||
|
-> Option<impl Content<TuiOut>>
|
||||||
|
{
|
||||||
|
let timebase = ¤t.timebase;
|
||||||
|
let mut result = String::new();
|
||||||
|
if let Some((t, _)) = track.player.next_clip().as_ref() {
|
||||||
|
let target = t.pulse.get();
|
||||||
|
let current = current.pulse.get();
|
||||||
|
if target > current {
|
||||||
|
result = format!("-{:>}", timebase.format_beats_0_short(target - current))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn track_row (&self, tracks_w: u16) -> impl Content<TuiOut> + '_ {
|
||||||
|
let h = 3;
|
||||||
|
let border = |x|x;//Rugged(Style::default().fg(Color::Rgb(0,0,0)).bg(Color::Reset)).enclose2(x);
|
||||||
|
Fixed::y(h, Bsp::e(
|
||||||
|
Fixed::xy(self.sidebar_w() as u16, h, self.track_header()),
|
||||||
|
Fill::x(Align::c(Fixed::xy(tracks_w, h, border(self.track_cells()))))
|
||||||
|
))
|
||||||
|
}
|
||||||
|
fn track_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
||||||
|
(||Tui::bold(true, Bsp::s(
|
||||||
|
row!(
|
||||||
|
Tui::fg(TuiTheme::g(128), "add "),
|
||||||
|
Tui::fg(TuiTheme::orange(), "t"),
|
||||||
|
Tui::fg(TuiTheme::g(128), "rack"),
|
||||||
|
),
|
||||||
|
row!(
|
||||||
|
Tui::fg(TuiTheme::orange(), "a"),
|
||||||
|
Tui::fg(TuiTheme::g(128), "dd scene"),
|
||||||
|
),
|
||||||
|
).boxed())).into()
|
||||||
|
}
|
||||||
|
fn track_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
||||||
|
let iter = ||self.tracks_with_sizes();
|
||||||
|
(move||Align::x(Map::new(iter, move|(_, track, x1, x2), i| {
|
||||||
|
let name = Push::x(1, &track.name);
|
||||||
|
let color = track.color;
|
||||||
|
let fg = color.lightest.rgb;
|
||||||
|
let bg = color.base.rgb;
|
||||||
|
let active = self.selected.track() == Some(i);
|
||||||
|
let bfg = if active { Color::Rgb(255,255,255) } else { Color::Rgb(0,0,0) };
|
||||||
|
let border = Style::default().fg(bfg).bg(bg);
|
||||||
|
Tui::bg(bg, map_east(x1 as u16, (x2 - x1) as u16,
|
||||||
|
Outer(border).enclose(Tui::fg_bg(fg, bg, Tui::bold(true, Fill::x(Align::x(name)))))
|
||||||
|
))
|
||||||
|
})).boxed()).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn input_row (&self, tracks_w: u16) -> impl Content<TuiOut> + '_ {
|
||||||
|
let h = 2 + self.midi_ins[0].connect.len() as u16;
|
||||||
|
Fixed::y(h, Bsp::e(
|
||||||
|
Fixed::xy(self.sidebar_w() as u16, h, self.input_header()),
|
||||||
|
Fill::x(Align::c(Fixed::xy(tracks_w, h, self.input_cells())))
|
||||||
|
))
|
||||||
|
}
|
||||||
|
fn input_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
||||||
|
(||Bsp::s(
|
||||||
|
Tui::bold(true, row!(
|
||||||
|
Tui::fg(TuiTheme::g(128), "midi "),
|
||||||
|
Tui::fg(TuiTheme::orange(), "I"),
|
||||||
|
Tui::fg(TuiTheme::g(128), "ns"),
|
||||||
|
)),
|
||||||
|
Bsp::s(
|
||||||
|
Fill::x(Tui::bold(true, Tui::fg_bg(TuiTheme::g(224), TuiTheme::g(64),
|
||||||
|
Align::w(&self.midi_ins[0].name)))),
|
||||||
|
self.midi_ins.get(0)
|
||||||
|
.and_then(|midi_in|midi_in.connect.get(0))
|
||||||
|
.map(|connect|Fill::x(Align::w(
|
||||||
|
Tui::bold(false, Tui::fg_bg(TuiTheme::g(224), TuiTheme::g(64), connect.info())))))
|
||||||
|
)
|
||||||
|
).boxed()).into()
|
||||||
|
}
|
||||||
|
fn input_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
||||||
|
(move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| {
|
||||||
|
let w = (x2 - x1) as u16;
|
||||||
|
let color: ItemPalette = track.color.dark.into();
|
||||||
|
map_east(x1 as u16, w, Fixed::x(w, Self::cell(color, Bsp::n(
|
||||||
|
Self::rec_mon(color.base.rgb, false, false),
|
||||||
|
phat_hi(color.base.rgb, color.dark.rgb)
|
||||||
|
))))
|
||||||
|
})).boxed()).into()
|
||||||
|
}
|
||||||
|
fn rec_mon (bg: Color, rec: bool, mon: bool) -> impl Content<TuiOut> {
|
||||||
|
row!(
|
||||||
|
Tui::fg_bg(if rec { Color::Red } else { bg }, bg, "▐"),
|
||||||
|
Tui::fg_bg(if rec { Color::White } else { Color::Rgb(0,0,0) }, bg, "REC"),
|
||||||
|
Tui::fg_bg(if rec { Color::White } else { bg }, bg, "▐"),
|
||||||
|
Tui::fg_bg(if mon { Color::White } else { Color::Rgb(0,0,0) }, bg, "MON"),
|
||||||
|
Tui::fg_bg(if mon { Color::White } else { bg }, bg, "▌"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn output_row (&self, tracks_w: u16) -> impl Content<TuiOut> + '_ {
|
||||||
|
let h = 2 + self.midi_outs[0].connect.len() as u16;
|
||||||
|
Fixed::y(h, Bsp::e(
|
||||||
|
Fixed::xy(self.sidebar_w() as u16, h, self.output_header()),
|
||||||
|
Fill::x(Align::c(Fixed::xy(tracks_w, h, self.output_cells())))
|
||||||
|
))
|
||||||
|
}
|
||||||
|
fn output_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
||||||
|
(||Bsp::s(
|
||||||
|
Tui::bold(true, row!(
|
||||||
|
Tui::fg(TuiTheme::g(128), "midi "),
|
||||||
|
Tui::fg(TuiTheme::orange(), "O"),
|
||||||
|
Tui::fg(TuiTheme::g(128), "uts"),
|
||||||
|
)),
|
||||||
|
Bsp::s(
|
||||||
|
Fill::x(Tui::bold(true, Tui::fg_bg(TuiTheme::g(224), TuiTheme::g(64),
|
||||||
|
Align::w(&self.midi_outs[0].name)))),
|
||||||
|
self.midi_outs.get(0)
|
||||||
|
.and_then(|midi_out|midi_out.connect.get(0))
|
||||||
|
.map(|connect|Fill::x(Align::w(
|
||||||
|
Tui::bold(false, Tui::fg_bg(TuiTheme::g(224), TuiTheme::g(64), connect.info())))))
|
||||||
|
),
|
||||||
|
).boxed()).into()
|
||||||
|
}
|
||||||
|
fn output_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
||||||
|
(move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| {
|
||||||
|
let w = (x2 - x1) as u16;
|
||||||
|
let color: ItemPalette = track.color.dark.into();
|
||||||
|
map_east(x1 as u16, w, Fixed::x(w, Self::cell(color, Bsp::n(
|
||||||
|
Self::mute_solo(color.base.rgb, false, false),
|
||||||
|
phat_hi(color.dark.rgb, color.darker.rgb)
|
||||||
|
))))
|
||||||
|
})).boxed()).into()
|
||||||
|
}
|
||||||
|
fn mute_solo (bg: Color, mute: bool, solo: bool) -> impl Content<TuiOut> {
|
||||||
|
row!(
|
||||||
|
Tui::fg_bg(if mute { Color::White } else { Color::Rgb(0,0,0) }, bg, "MUTE"),
|
||||||
|
Tui::fg_bg(if mute { Color::White } else { bg }, bg, "▐"),
|
||||||
|
Tui::fg_bg(if solo { Color::White } else { Color::Rgb(0,0,0) }, bg, "SOLO"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scene_row (&self, tracks_w: u16) -> impl Content<TuiOut> + '_ {
|
||||||
|
let h = (self.size.h() as u16).saturating_sub(8).max(8);
|
||||||
|
let border = |x|x;//Skinny(Style::default().fg(Color::Rgb(0,0,0)).bg(Color::Reset)).enclose2(x);
|
||||||
|
Bsp::e(
|
||||||
|
Tui::bg(Color::Reset, Fixed::xy(self.sidebar_w() as u16, h, self.scene_headers())),
|
||||||
|
Tui::bg(Color::Reset, Fill::x(Align::c(Fixed::xy(tracks_w, h, border(self.scene_cells())))))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn scene_headers <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
||||||
|
(||{
|
||||||
|
let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0))));
|
||||||
|
let selected = self.selected.scene();
|
||||||
|
Fill::y(Align::c(Map::new(||self.scenes_with_sizes(2), move|(_, scene, y1, y2), i| {
|
||||||
|
let h = (y2 - y1) as u16;
|
||||||
|
let name = format!("🭬{}", &scene.name);
|
||||||
|
let color = scene.color;
|
||||||
|
let active = selected == Some(i);
|
||||||
|
let mid = if active { color.light } else { color.base };
|
||||||
|
let top = Some(last_color.read().unwrap().base.rgb);
|
||||||
|
let cell = phat_sel_3(
|
||||||
|
active,
|
||||||
|
Tui::bold(true, name.clone()),
|
||||||
|
Tui::bold(true, name),
|
||||||
|
top,
|
||||||
|
mid.rgb,
|
||||||
|
Color::Rgb(0, 0, 0)
|
||||||
|
);
|
||||||
|
*last_color.write().unwrap() = color;
|
||||||
|
map_south(y1 as u16, h + 1, Fixed::y(h + 1, cell))
|
||||||
|
}))).boxed()
|
||||||
|
}).into()
|
||||||
|
}
|
||||||
|
fn scene_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
||||||
|
let editing = self.is_editing();
|
||||||
|
let tracks = move||self.tracks_with_sizes();
|
||||||
|
let scenes = ||self.scenes_with_sizes(2);
|
||||||
|
let selected_track = self.selected.track();
|
||||||
|
let selected_scene = self.selected.scene();
|
||||||
|
(move||Fill::y(Align::c(Map::new(tracks, move|(_, track, x1, x2), t| {
|
||||||
|
let w = (x2 - x1) as u16;
|
||||||
|
let color: ItemPalette = track.color.dark.into();
|
||||||
|
let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0))));
|
||||||
|
let cells = Map::new(scenes, move|(_, scene, y1, y2), s| {
|
||||||
|
let h = (y2 - y1) as u16;
|
||||||
|
let color = scene.color;
|
||||||
|
let (name, fg, bg) = if let Some(c) = &scene.clips[t] {
|
||||||
|
let c = c.read().unwrap();
|
||||||
|
(c.name.to_string(), c.color.lightest.rgb, c.color.base.rgb)
|
||||||
|
} else {
|
||||||
|
("⏹ ".to_string(), TuiTheme::g(64), TuiTheme::g(32))
|
||||||
|
};
|
||||||
|
let last = last_color.read().unwrap().clone();
|
||||||
|
let active = editing && selected_scene == Some(s) && selected_track == Some(t);
|
||||||
|
let editor = Thunk::new(||&self.editor);
|
||||||
|
let cell = Thunk::new(move||phat_sel_3(
|
||||||
|
selected_track == Some(t) && selected_scene == Some(s),
|
||||||
|
Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))),
|
||||||
|
Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))),
|
||||||
|
if selected_track == Some(t) && selected_scene.map(|s|s+1) == Some(s) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(bg.into())
|
||||||
|
},
|
||||||
|
bg.into(),
|
||||||
|
bg.into(),
|
||||||
|
));
|
||||||
|
let cell = Either(active, editor, cell);
|
||||||
|
*last_color.write().unwrap() = bg.into();
|
||||||
|
map_south(
|
||||||
|
y1 as u16,
|
||||||
|
h + 1,
|
||||||
|
Fill::x(Fixed::y(h + 1, cell))
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let column = Fixed::x(w, Tui::bg(Color::Reset, Align::y(cells)).boxed());
|
||||||
|
Fixed::x(w, map_east(x1 as u16, w, column))
|
||||||
|
}))).boxed()).into()
|
||||||
|
}
|
||||||
|
fn cell_clip <'a> (
|
||||||
|
scene: &'a ArrangerScene, index: usize, track: &'a ArrangerTrack, w: u16, h: u16
|
||||||
|
) -> impl Content<TuiOut> + use<'a> {
|
||||||
|
scene.clips.get(index).map(|clip|clip.as_ref().map(|clip|{
|
||||||
|
let clip = clip.read().unwrap();
|
||||||
|
let mut bg = TuiTheme::border_bg();
|
||||||
|
let name = clip.name.to_string();
|
||||||
|
let max_w = name.len().min((w as usize).saturating_sub(2));
|
||||||
|
let color = clip.color;
|
||||||
|
bg = color.dark.rgb;
|
||||||
|
if let Some((_, Some(ref playing))) = track.player.play_clip() {
|
||||||
|
if *playing.read().unwrap() == *clip {
|
||||||
|
bg = color.light.rgb
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Fixed::xy(w, h, &Tui::bg(bg, Push::x(1, Fixed::x(w, &name.as_str()[0..max_w]))));
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn track_column_separators <'a> (&'a self) -> impl Content<TuiOut> + 'a {
|
||||||
|
let scenes_w = 16;//.max(SCENES_W_OFFSET + ArrangerScene::longest_name(&self.scenes) as u16);
|
||||||
|
let fg = Color::Rgb(64,64,64);
|
||||||
|
Map::new(move||self.tracks_with_sizes(), move|(_n, _track, x1, x2), _i|{
|
||||||
|
Push::x(scenes_w, map_east(x1 as u16, (x2 - x1) as u16,
|
||||||
|
Fixed::x((x2 - x1) as u16, Tui::fg(fg, RepeatV(&"·")))))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub 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 scene_row_sep <'a> (&'a self) -> impl Content<TuiOut> + 'a {
|
||||||
|
let fg = Color::Rgb(255,255,255);
|
||||||
|
Map::new(move||self.scenes_with_sizes(1), |_, _|"")
|
||||||
|
//Map(||rows.iter(), |(_n, _scene, y1, _y2), _i| {
|
||||||
|
//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 {
|
||||||
|
//if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((x, y))) {
|
||||||
|
//cell.modifier = Modifier::UNDERLINED;
|
||||||
|
//cell.underline_color = fg;
|
||||||
|
//}
|
||||||
|
////}
|
||||||
|
//}
|
||||||
|
//})
|
||||||
|
}
|
||||||
|
|
||||||
|
//pub fn scene_heights (scenes: &[ArrangerScene], 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()
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//fn cursor (&self) -> impl Content<TuiOut> + '_ {
|
||||||
|
//let color = self.color;
|
||||||
|
//let bg = color.lighter.rgb;//Color::Rgb(0, 255, 0);
|
||||||
|
//let selected = self.selected();
|
||||||
|
//let cols = Arranger::track_widths(&self.tracks);
|
||||||
|
//let rows = Arranger::scene_heights(&self.scenes, 1);
|
||||||
|
//let scenes_w = 16.max(SCENES_W_OFFSET + ArrangerScene::longest_name(&self.scenes) as u16);
|
||||||
|
//let focused = true;
|
||||||
|
//let reticle = Reticle(Style {
|
||||||
|
//fg: Some(self.color.lighter.rgb),
|
||||||
|
//bg: None,
|
||||||
|
//underline_color: None,
|
||||||
|
//add_modifier: Modifier::empty(),
|
||||||
|
//sub_modifier: Modifier::DIM
|
||||||
|
//});
|
||||||
|
//RenderThunk::new(move|to: &mut TuiOut|{
|
||||||
|
//let area = to.area();
|
||||||
|
//let [x, y, w, h] = area.xywh();
|
||||||
|
//let mut track_area: Option<[u16;4]> = match selected {
|
||||||
|
//ArrangerSelection::Track(t) | ArrangerSelection::Clip(t, _) => Some([
|
||||||
|
//x + scenes_w + cols[t].1 as u16, y,
|
||||||
|
//cols[t].0 as u16, h,
|
||||||
|
//]),
|
||||||
|
//_ => None
|
||||||
|
//};
|
||||||
|
//let mut scene_area: Option<[u16;4]> = match selected {
|
||||||
|
//ArrangerSelection::Scene(s) | ArrangerSelection::Clip(_, s) => Some([
|
||||||
|
//x, y + HEADER_H + (rows[s].1 / PPQ) as u16,
|
||||||
|
//w, (rows[s].0 / PPQ) as u16
|
||||||
|
//]),
|
||||||
|
//_ => None
|
||||||
|
//};
|
||||||
|
//let mut clip_area: Option<[u16;4]> = match selected {
|
||||||
|
//ArrangerSelection::Clip(t, s) => Some([
|
||||||
|
//(scenes_w + x + cols[t].1 as u16).saturating_sub(1),
|
||||||
|
//HEADER_H + y + (rows[s].1/PPQ) as u16,
|
||||||
|
//cols[t].0 as u16 + 2,
|
||||||
|
//(rows[s].0 / PPQ) as u16
|
||||||
|
//]),
|
||||||
|
//_ => None
|
||||||
|
//};
|
||||||
|
//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([x, y - 1, w, 1], bg);
|
||||||
|
//to.fill_ul([x, y + height - 1, w, 1], bg);
|
||||||
|
//}
|
||||||
|
//if focused {
|
||||||
|
//to.place(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(scenes_w)
|
||||||
|
//} else {
|
||||||
|
//area.clip_w(scenes_w).clip_h(HEADER_H)
|
||||||
|
//}, &reticle)
|
||||||
|
//};
|
||||||
|
//})
|
||||||
|
//}
|
||||||
|
|
||||||
|
/// A 1-row cell.
|
||||||
|
fn cell <T: Content<TuiOut>> (color: ItemPalette, field: T) -> impl Content<TuiOut> {
|
||||||
|
Tui::fg_bg(color.lightest.rgb, color.base.rgb, Fixed::y(1, field))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,3 +80,44 @@ impl Content<TuiOut> for RepeatH<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A phat line
|
||||||
|
pub fn phat_lo (fg: Color, bg: Color) -> impl Content<TuiOut> {
|
||||||
|
Fixed::y(1, Tui::fg_bg(fg, bg, RepeatH(&"▄")))
|
||||||
|
}
|
||||||
|
/// A phat line
|
||||||
|
pub fn phat_hi (fg: Color, bg: Color) -> impl Content<TuiOut> {
|
||||||
|
Fixed::y(1, Tui::fg_bg(fg, bg, RepeatH(&"▀")))
|
||||||
|
}
|
||||||
|
/// A cell that is 3-row on its own, but stacks, giving (N+1)*2 rows per N cells.
|
||||||
|
pub fn phat_cell <T: Content<TuiOut>> (
|
||||||
|
color: ItemPalette, last: ItemPalette, field: T
|
||||||
|
) -> impl Content<TuiOut> {
|
||||||
|
Bsp::s(phat_lo(color.base.rgb, last.base.rgb),
|
||||||
|
Bsp::n(phat_hi(color.base.rgb, last.base.rgb),
|
||||||
|
Fixed::y(1, Fill::x(Tui::fg_bg(color.lightest.rgb, color.base.rgb, field))),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
pub fn phat_cell_3 <T: Content<TuiOut>> (
|
||||||
|
field: T, top: Color, middle: Color, bottom: Color
|
||||||
|
) -> impl Content<TuiOut> {
|
||||||
|
Bsp::s(phat_lo(middle, top),
|
||||||
|
Bsp::n(phat_hi(middle, bottom),
|
||||||
|
Fill::y(Fill::x(Tui::bg(middle, field))),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
pub fn phat_sel_3 <T: Content<TuiOut>> (
|
||||||
|
selected: bool, field_1: T, field_2: T, top: Option<Color>, middle: Color, bottom: Color
|
||||||
|
) -> impl Content<TuiOut> {
|
||||||
|
let border = Style::default().fg(Color::Rgb(255,255,255)).bg(middle);
|
||||||
|
Either(selected,
|
||||||
|
Tui::bg(middle, Outer(border).enclose(Align::w(Bsp::s("", Bsp::n("", Fill::y(field_1)))))),
|
||||||
|
Bsp::s(Fixed::y(1, top.map(|top|phat_lo(middle, top))),
|
||||||
|
Bsp::n(Fixed::y(1, phat_hi(middle, bottom)),
|
||||||
|
Fill::xy(Tui::bg(middle, field_2)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue