break down status bar, piano, arranger

This commit is contained in:
🪞👃🪞 2024-12-25 06:15:51 +01:00
parent eb5f451423
commit a43b7048ac
23 changed files with 355 additions and 347 deletions

View file

@ -1,51 +1,42 @@
use crate::*;
mod tui_input;
pub(crate) use tui_input::*;
pub use tui_input::TuiInput;
mod tui_input; pub(crate) use self::tui_input::*;
mod tui_output; pub(crate) use self::tui_output::*;
mod tui_output;
pub(crate) use tui_output::*;
pub use tui_output::TuiOutput;
pub use self::tui_input::TuiInput;
pub use self::tui_output::TuiOutput;
////////////////////////////////////////////////////////
mod tui_style;
mod tui_theme;
pub(crate) use tui_theme::*;
mod tui_border;
pub(crate) use tui_border::*;
mod tui_theme; pub(crate) use self::tui_theme::*;
mod tui_border; pub(crate) use self::tui_border::*;
////////////////////////////////////////////////////////
mod app_transport; pub(crate) use app_transport::*;
mod app_sequencer; pub(crate) use app_sequencer::*;
mod app_sampler; pub(crate) use app_sampler::*;
mod app_groovebox; pub(crate) use app_groovebox::*;
mod app_arranger; pub(crate) use app_arranger::*;
mod app_transport; pub(crate) use self::app_transport::*;
mod app_sequencer; pub(crate) use self::app_sequencer::*;
mod app_sampler; pub(crate) use self::app_sampler::*;
mod app_groovebox; pub(crate) use self::app_groovebox::*;
mod app_arranger; pub(crate) use self::app_arranger::*;
///////////////////////////////////////////////////////
mod arranger_command; pub(crate) use arranger_command::*;
mod arranger_scene; pub(crate) use arranger_scene::*;
mod arranger_select; pub(crate) use arranger_select::*;
mod arranger_track; pub(crate) use arranger_track::*;
mod arranger_mode_h;
mod arranger_mode_v; pub(crate) use arranger_mode_v::*;
mod arranger_command; pub(crate) use self::arranger_command::*;
mod arranger_scene; pub(crate) use self::arranger_scene::*;
mod arranger_select; pub(crate) use self::arranger_select::*;
mod arranger_track; pub(crate) use self::arranger_track::*;
mod arranger_mode; pub(crate) use self::arranger_mode::*;
mod arranger_v; pub(crate) use self::arranger_v::*;
mod arranger_h;
////////////////////////////////////////////////////////
mod pool; pub(crate) use pool::*;
mod phrase_editor; pub(crate) use phrase_editor::*;
mod status_bar; pub(crate) use status_bar::*;
mod file_browser; pub(crate) use file_browser::*;
mod piano_horizontal; pub(crate) use piano_horizontal::*;
mod port_select;
mod pool; pub(crate) use self::pool::*;
mod phrase_editor; pub(crate) use self::phrase_editor::*;
mod status; pub(crate) use self::status::*;
mod file_browser; pub(crate) use self::file_browser::*;
mod piano_h; pub(crate) use self::piano_h::*;
////////////////////////////////////////////////////////

View file

@ -151,31 +151,3 @@ has_clock!(|self: ArrangerTui|&self.clock);
has_phrases!(|self: ArrangerTui|self.phrases.phrases);
has_editor!(|self: ArrangerTui|self.editor);
handle!(<Tui>|self: ArrangerTui, input|ArrangerCommand::execute_with_state(self, input));
/// Display mode of arranger
#[derive(Clone, PartialEq)]
pub enum ArrangerMode {
/// Tracks are columns
V(usize),
/// Tracks are rows
H,
}
render!(<Tui>|self: ArrangerMode|{});
/// Arranger display mode can be cycled
impl ArrangerMode {
/// Cycle arranger display mode
pub fn to_next (&mut self) {
*self = match self {
Self::H => Self::V(1),
Self::V(1) => Self::V(2),
Self::V(2) => Self::V(2),
Self::V(0) => Self::H,
Self::V(_) => Self::V(0),
}
}
}
fn any_size <E: Engine> (_: E::Size) -> Perhaps<E::Size>{
Ok(Some([0.into(),0.into()].into()))
}

View file

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

View file

@ -0,0 +1,29 @@
use crate::*;
/// Display mode of arranger
#[derive(Clone, PartialEq)]
pub enum ArrangerMode {
/// Tracks are columns
V(usize),
/// Tracks are rows
H,
}
render!(<Tui>|self: ArrangerMode|{});
/// Arranger display mode can be cycled
impl ArrangerMode {
/// Cycle arranger display mode
pub fn to_next (&mut self) {
*self = match self {
Self::H => Self::V(1),
Self::V(1) => Self::V(2),
Self::V(2) => Self::V(2),
Self::V(0) => Self::H,
Self::V(_) => Self::V(0),
}
}
}
fn any_size <E: Engine> (_: E::Size) -> Perhaps<E::Size>{
Ok(Some([0.into(),0.into()].into()))
}

View file

@ -1,26 +1,13 @@
use crate::*;
use super::*;
fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16) -> impl Iterator<Item=(usize, u16, usize)> {
(note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n))
}
mod piano_h_cursor; use self::piano_h_cursor::*;
mod piano_h_keys; use self::piano_h_keys::*;
mod piano_h_notes; use self::piano_h_notes::*;
mod piano_h_time; use self::piano_h_time::*;
fn to_key (note: usize) -> &'static str {
match note % 12 {
11 => "████▌",
10 => " ",
9 => "████▌",
8 => " ",
7 => "████▌",
6 => " ",
5 => "████▌",
4 => "████▌",
3 => " ",
2 => "████▌",
1 => " ",
0 => "████▌",
_ => unreachable!(),
}
pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16) -> impl Iterator<Item=(usize, u16, usize)> {
(note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n))
}
/// A phrase, rendered as a horizontal piano roll.
@ -76,133 +63,6 @@ render!(<Tui>|self: PianoHorizontal|{
)))
});
pub struct PianoHorizontalTimeline<'a>(&'a PianoHorizontal);
render!(<Tui>|self: PianoHorizontalTimeline<'a>|render(|to|{
let [x, y, w, h] = to.area();
let style = Some(Style::default().dim());
let length = self.0.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);
for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) {
let t = area_x as usize * self.0.time_zoom().get();
if t < length {
to.blit(&"|", screen_x, y, style);
}
}
Ok(())
}));
//Tui::fg_bg(
//self.0.color.lightest.rgb,
//self.0.color.darkest.rgb,
//format!("{}*{}", self.0.time_start(), self.0.time_zoom()).as_str()
//));
pub struct PianoHorizontalKeys<'a>(&'a PianoHorizontal);
render!(<Tui>|self: PianoHorizontalKeys<'a>|render(|to|Ok({
let color = self.0.color;
let note_lo = self.0.note_lo().get();
let note_hi = self.0.note_hi();
let note_point = self.0.note_point();
let [x, y0, w, h] = to.area().xywh();
let key_style = Some(Style::default().fg(Color::Rgb(192, 192, 192)).bg(Color::Rgb(0, 0, 0)));
let off_style = Some(Style::default().fg(TuiTheme::g(160)));
let on_style = Some(Style::default().fg(TuiTheme::g(255)).bg(color.light.rgb).bold());
for (area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
to.blit(&to_key(note), x, screen_y, key_style);
if note > 127 {
continue
}
if note == note_point {
to.blit(&format!("{:<5}", to_note_name(note)), x, screen_y, on_style)
} else {
to.blit(&to_note_name(note), x, screen_y, off_style)
};
}
//let debug = false;
//if debug {
//to.blit(&format!("XYU"), x, y0, None);
//to.blit(&format!("x={x}"), x, y0+1, None);
//to.blit(&format!("y0={y0}"), x, y0+2, None);
//to.blit(&format!("w={w}"), x, y0+3, None);
//to.blit(&format!("h={h}"), x, y0+4, None);
//to.blit(&format!("note_lo={note_lo}"), x, y0+5, None);
//to.blit(&format!("note_hi={note_hi}"), x, y0+6, None);
//}
})));
pub struct PianoHorizontalCursor<'a>(&'a PianoHorizontal);
render!(<Tui>|self: PianoHorizontalCursor<'a>|render(|to|Ok({
let style = Some(Style::default().fg(self.0.color.lightest.rgb));
let note_hi = self.0.note_hi();
let note_len = self.0.note_len();
let note_lo = self.0.note_lo().get();
let note_point = self.0.note_point();
let time_point = self.0.time_point();
let time_start = self.0.time_start().get();
let time_zoom = self.0.time_zoom().get();
let [x0, y0, w, _] = to.area().xywh();
for (area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
if note == note_point {
for x in 0..w {
let screen_x = x0 + x as u16;
let time_1 = time_start + x as usize * time_zoom;
let time_2 = time_1 + time_zoom;
if time_1 <= time_point && time_point < time_2 {
to.blit(&"", screen_x, screen_y, style);
let tail = note_len as u16 / time_zoom as u16;
for x_tail in (screen_x + 1)..(screen_x + tail) {
to.blit(&"", x_tail, screen_y, style);
}
break
}
}
break
}
}
})));
pub struct PianoHorizontalNotes<'a>(&'a PianoHorizontal);
render!(<Tui>|self: PianoHorizontalNotes<'a>|render(|to|Ok({
let time_start = self.0.time_start().get();
let note_lo = self.0.note_lo().get();
let note_hi = self.0.note_hi();
let note_point = self.0.note_point();
let source = &self.0.buffer;
let [x0, y0, w, h] = to.area().xywh();
if h as usize != self.0.note_axis().get() {
panic!("area height mismatch");
}
for (area_x, screen_x) in (x0..x0+w).enumerate() {
for (area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
//if area_x % 10 == 0 {
//to.blit(&format!("{area_y} {note}"), screen_x, screen_y, None);
//}
let source_x = time_start + area_x;
let source_y = note_hi - area_y;
////// TODO: enable loop rollover:
//////let source_x = (time_start + area_x) % source.width.max(1);
//////let source_y = (note_hi - area_y) % source.height.max(1);
let is_in_x = source_x < source.width;
let is_in_y = source_y < source.height;
if is_in_x && is_in_y {
if let Some(source_cell) = source.get(source_x, source_y) {
*to.buffer.get_mut(screen_x, screen_y) = source_cell.clone();
}
}
}
}
//let debug = true;
//if debug {
//let x0=20+x0;
//to.blit(&format!("KYP "), x0, y0, None);
//to.blit(&format!("x0={x0} "), x0, y0+1, None);
//to.blit(&format!("y0={y0} "), x0, y0+2, None);
//to.blit(&format!("note_lo={note_lo}, {} ", to_note_name(note_lo)), x0, y0+3, None);
//to.blit(&format!("note_hi={note_hi}, {} ", to_note_name(note_hi)), x0, y0+4, None);
//to.blit(&format!("note_point={note_point}, {} ", to_note_name(note_point)), x0, y0+5, None);
//to.blit(&format!("time_start={time_start} "), x0, y0+6, None);
//return Ok(());
//}
})));
impl PianoHorizontal {
/// Draw the piano roll foreground using full blocks on note on and half blocks on legato: █▄ █▄ █▄
fn draw_bg (buf: &mut BigBuffer, phrase: &Phrase, zoom: usize, note_len: usize) {

View file

@ -0,0 +1,33 @@
use crate::*;
use super::note_y_iter;
pub struct PianoHorizontalCursor<'a>(pub(crate) &'a PianoHorizontal);
render!(<Tui>|self: PianoHorizontalCursor<'a>|render(|to|Ok({
let style = Some(Style::default().fg(self.0.color.lightest.rgb));
let note_hi = self.0.note_hi();
let note_len = self.0.note_len();
let note_lo = self.0.note_lo().get();
let note_point = self.0.note_point();
let time_point = self.0.time_point();
let time_start = self.0.time_start().get();
let time_zoom = self.0.time_zoom().get();
let [x0, y0, w, _] = to.area().xywh();
for (area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
if note == note_point {
for x in 0..w {
let screen_x = x0 + x as u16;
let time_1 = time_start + x as usize * time_zoom;
let time_2 = time_1 + time_zoom;
if time_1 <= time_point && time_point < time_2 {
to.blit(&"", screen_x, screen_y, style);
let tail = note_len as u16 / time_zoom as u16;
for x_tail in (screen_x + 1)..(screen_x + tail) {
to.blit(&"", x_tail, screen_y, style);
}
break
}
}
break
}
}
})));

View file

@ -0,0 +1,53 @@
use crate::*;
use super::note_y_iter;
pub struct PianoHorizontalKeys<'a>(pub(crate) &'a PianoHorizontal);
render!(<Tui>|self: PianoHorizontalKeys<'a>|render(|to|Ok({
let color = self.0.color;
let note_lo = self.0.note_lo().get();
let note_hi = self.0.note_hi();
let note_point = self.0.note_point();
let [x, y0, w, h] = to.area().xywh();
let key_style = Some(Style::default().fg(Color::Rgb(192, 192, 192)).bg(Color::Rgb(0, 0, 0)));
let off_style = Some(Style::default().fg(TuiTheme::g(160)));
let on_style = Some(Style::default().fg(TuiTheme::g(255)).bg(color.light.rgb).bold());
for (area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
to.blit(&to_key(note), x, screen_y, key_style);
if note > 127 {
continue
}
if note == note_point {
to.blit(&format!("{:<5}", to_note_name(note)), x, screen_y, on_style)
} else {
to.blit(&to_note_name(note), x, screen_y, off_style)
};
}
//let debug = false;
//if debug {
//to.blit(&format!("XYU"), x, y0, None);
//to.blit(&format!("x={x}"), x, y0+1, None);
//to.blit(&format!("y0={y0}"), x, y0+2, None);
//to.blit(&format!("w={w}"), x, y0+3, None);
//to.blit(&format!("h={h}"), x, y0+4, None);
//to.blit(&format!("note_lo={note_lo}"), x, y0+5, None);
//to.blit(&format!("note_hi={note_hi}"), x, y0+6, None);
//}
})));
fn to_key (note: usize) -> &'static str {
match note % 12 {
11 => "████▌",
10 => " ",
9 => "████▌",
8 => " ",
7 => "████▌",
6 => " ",
5 => "████▌",
4 => "████▌",
3 => " ",
2 => "████▌",
1 => " ",
0 => "████▌",
_ => unreachable!(),
}
}

View file

@ -0,0 +1,46 @@
use crate::*;
use super::note_y_iter;
pub struct PianoHorizontalNotes<'a>(pub(crate) &'a PianoHorizontal);
render!(<Tui>|self: PianoHorizontalNotes<'a>|render(|to|Ok({
let time_start = self.0.time_start().get();
let note_lo = self.0.note_lo().get();
let note_hi = self.0.note_hi();
let note_point = self.0.note_point();
let source = &self.0.buffer;
let [x0, y0, w, h] = to.area().xywh();
if h as usize != self.0.note_axis().get() {
panic!("area height mismatch");
}
for (area_x, screen_x) in (x0..x0+w).enumerate() {
for (area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
//if area_x % 10 == 0 {
//to.blit(&format!("{area_y} {note}"), screen_x, screen_y, None);
//}
let source_x = time_start + area_x;
let source_y = note_hi - area_y;
////// TODO: enable loop rollover:
//////let source_x = (time_start + area_x) % source.width.max(1);
//////let source_y = (note_hi - area_y) % source.height.max(1);
let is_in_x = source_x < source.width;
let is_in_y = source_y < source.height;
if is_in_x && is_in_y {
if let Some(source_cell) = source.get(source_x, source_y) {
*to.buffer.get_mut(screen_x, screen_y) = source_cell.clone();
}
}
}
}
//let debug = true;
//if debug {
//let x0=20+x0;
//to.blit(&format!("KYP "), x0, y0, None);
//to.blit(&format!("x0={x0} "), x0, y0+1, None);
//to.blit(&format!("y0={y0} "), x0, y0+2, None);
//to.blit(&format!("note_lo={note_lo}, {} ", to_note_name(note_lo)), x0, y0+3, None);
//to.blit(&format!("note_hi={note_hi}, {} ", to_note_name(note_hi)), x0, y0+4, None);
//to.blit(&format!("note_point={note_point}, {} ", to_note_name(note_point)), x0, y0+5, None);
//to.blit(&format!("time_start={time_start} "), x0, y0+6, None);
//return Ok(());
//}
})));

View file

@ -0,0 +1,21 @@
use crate::*;
use super::note_y_iter;
pub struct PianoHorizontalTimeline<'a>(pub(crate) &'a PianoHorizontal);
render!(<Tui>|self: PianoHorizontalTimeline<'a>|render(|to|{
let [x, y, w, h] = to.area();
let style = Some(Style::default().dim());
let length = self.0.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);
for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) {
let t = area_x as usize * self.0.time_zoom().get();
if t < length {
to.blit(&"|", screen_x, y, style);
}
}
Ok(())
}));
//Tui::fg_bg(
//self.0.color.lightest.rgb,
//self.0.color.darkest.rgb,
//format!("{}*{}", self.0.time_start(), self.0.time_zoom()).as_str()
//));

View file

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

View file

@ -1,7 +0,0 @@
use crate::*;
pub struct PortSelector {
pub(crate) title: &'static str,
}
render!(<Tui>|self: PortSelector|{});

View file

@ -0,0 +1,3 @@
mod status_arranger; pub(crate) use self::status_arranger::*;
mod status_edit; pub(crate) use self::status_edit::*;
mod status_sequencer; pub(crate) use self::status_sequencer::*;

View file

@ -0,0 +1,55 @@
use crate::*;
use super::*;
/// Status bar for arranger app
#[derive(Clone)]
pub struct ArrangerStatus {
pub(crate) width: usize,
pub(crate) cpu: Option<String>,
pub(crate) size: String,
pub(crate) res: String,
pub(crate) playing: bool,
}
from!(|state:&ArrangerTui|ArrangerStatus = {
let samples = state.clock.chunk.load(Relaxed);
let rate = state.clock.timebase.sr.get() as f64;
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()),
res: format!("│{}s│{:.1}kHz│{:.1}ms│", samples, rate / 1000., buffer * 1000.),
}
});
render!(<Tui>|self: ArrangerStatus|Fixed::h(2, lay!([
Self::help(),
Fill::wh(Align::se(Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), self.stats()))),
])));
impl ArrangerStatus {
fn help () -> impl Render<Tui> {
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"),
single(" Ctrl", " scroll"),
single(" ▲▼▶◀", " cell"),
double(("p", "put"), ("g", "get")),
double(("q", "enqueue"), ("e", "edit")),
single(" wsad", " note"),
double(("a", "append"), ("s", "set"),),
double((",.", "length"), ("<>", "triplet"),),
double(("[]", "phrase"), ("{}", "order"),),
]))
}
fn stats <'a> (&'a self) -> impl Render<Tui> + use<'a> {
row!([&self.cpu, &self.res, &self.size])
}
}

View file

@ -0,0 +1,31 @@
use crate::*;
use super::*;
pub struct PhraseEditStatus<'a>(pub &'a PhraseEditorModel);
render!(<Tui>|self:PhraseEditStatus<'a>|{
let (color, name, length, looped) = if let Some(phrase) = self.0.phrase().as_ref().map(|p|p.read().unwrap()) {
(phrase.color, phrase.name.clone(), phrase.length, phrase.looped)
} else {
(ItemPalette::from(TuiTheme::g(64)), String::new(), 0, false)
};
let bg = color.darkest.rgb;
let fg = color.lightest.rgb;
let field = move|x, y|row!([
Tui::fg_bg(color.lighter.rgb, color.darker.rgb, Tui::bold(true, x)),
Tui::fg_bg(color.light.rgb, color.darker.rgb, Tui::bold(true, "")),
Fill::w(Tui::fg_bg(color.lightest.rgb, color.dark.rgb, &y)),
]);
Fill::w(Tui::fg_bg(fg, bg, row!([
Fixed::wh(26, 3, col!(![
field(" Edit", format!("{name}")),
field(" Length", format!("{length}")),
field(" Loop", format!("{looped}"))])),
Fixed::wh(30, 3, col!(![
field(" Time", format!("{}/{}-{} ({}*{}) {}",
self.0.time_point(), self.0.time_start().get(), self.0.time_end(),
self.0.time_axis().get(), self.0.time_zoom().get(),
if self.0.time_lock().get() { "[lock]" } else { " " })),
field(" Note", format!("{} ({}) {} | {}-{} ({})",
self.0.note_point(), to_note_name(self.0.note_point()), self.0.note_len(),
to_note_name(self.0.note_lo().get()), to_note_name(self.0.note_hi()),
self.0.note_axis().get()))]))])))});

View file

@ -0,0 +1,53 @@
use crate::*;
use super::*;
/// Status bar for sequencer app
#[derive(Clone)]
pub struct SequencerStatus {
pub(crate) width: usize,
pub(crate) cpu: Option<String>,
pub(crate) size: String,
pub(crate) res: String,
pub(crate) playing: bool,
}
from!(|state:&SequencerTui|SequencerStatus = {
let samples = state.clock.chunk.load(Relaxed);
let rate = state.clock.timebase.sr.get() as f64;
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()),
res: format!("│{}s│{:.1}kHz│{:.1}ms│", samples, rate / 1000., buffer * 1000.),
}
});
render!(<Tui>|self: SequencerStatus|Fixed::h(2, lay!([
Self::help(),
Fill::wh(Align::se(Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), self.stats()))),
])));
impl SequencerStatus {
fn help () -> impl Render<Tui> {
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 <'a> (&'a self) -> impl Render<Tui> + use<'a> {
row!([&self.cpu, &self.res, &self.size])
}
}

View file

@ -1,134 +0,0 @@
use crate::*;
pub struct PhraseEditStatus<'a>(pub &'a PhraseEditorModel);
render!(<Tui>|self:PhraseEditStatus<'a>|{
let (color, name, length, looped) = if let Some(phrase) = self.0.phrase().as_ref().map(|p|p.read().unwrap()) {
(phrase.color, phrase.name.clone(), phrase.length, phrase.looped)
} else {
(ItemPalette::from(TuiTheme::g(64)), String::new(), 0, false)
};
let bg = color.darkest.rgb;
let fg = color.lightest.rgb;
let field = move|x, y|row!([
Tui::fg_bg(color.lighter.rgb, color.darker.rgb, Tui::bold(true, x)),
Tui::fg_bg(color.light.rgb, color.darker.rgb, Tui::bold(true, "")),
Fill::w(Tui::fg_bg(color.lightest.rgb, color.dark.rgb, &y)),
]);
Fill::w(Tui::fg_bg(fg, bg, row!([
Fixed::wh(26, 3, col!(![
field(" Edit", format!("{name}")),
field(" Length", format!("{length}")),
field(" Loop", format!("{looped}"))])),
Fixed::wh(30, 3, col!(![
field(" Time", format!("{}/{}-{} ({}*{}) {}",
self.0.time_point(), self.0.time_start().get(), self.0.time_end(),
self.0.time_axis().get(), self.0.time_zoom().get(),
if self.0.time_lock().get() { "[lock]" } else { " " })),
field(" Note", format!("{} ({}) {} | {}-{} ({})",
self.0.note_point(), to_note_name(self.0.note_point()), self.0.note_len(),
to_note_name(self.0.note_lo().get()), to_note_name(self.0.note_hi()),
self.0.note_axis().get()))]))])))});
/// Status bar for sequencer app
#[derive(Clone)]
pub struct SequencerStatus {
pub(crate) width: usize,
pub(crate) cpu: Option<String>,
pub(crate) size: String,
pub(crate) res: String,
pub(crate) playing: bool,
}
from!(|state:&SequencerTui|SequencerStatus = {
let samples = state.clock.chunk.load(Relaxed);
let rate = state.clock.timebase.sr.get() as f64;
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()),
res: format!("│{}s│{:.1}kHz│{:.1}ms│", samples, rate / 1000., buffer * 1000.),
}
});
render!(<Tui>|self: SequencerStatus|Fixed::h(2, lay!([
Self::help(),
Fill::wh(Align::se(Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), self.stats()))),
])));
impl SequencerStatus {
fn help () -> impl Render<Tui> {
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 <'a> (&'a self) -> impl Render<Tui> + use<'a> {
row!([&self.cpu, &self.res, &self.size])
}
}
/// Status bar for arranger app
#[derive(Clone)]
pub struct ArrangerStatus {
pub(crate) width: usize,
pub(crate) cpu: Option<String>,
pub(crate) size: String,
pub(crate) res: String,
pub(crate) playing: bool,
}
from!(|state:&ArrangerTui|ArrangerStatus = {
let samples = state.clock.chunk.load(Relaxed);
let rate = state.clock.timebase.sr.get() as f64;
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()),
res: format!("│{}s│{:.1}kHz│{:.1}ms│", samples, rate / 1000., buffer * 1000.),
}
});
render!(<Tui>|self: ArrangerStatus|Fixed::h(2, lay!([
Self::help(),
Fill::wh(Align::se(Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), self.stats()))),
])));
impl ArrangerStatus {
fn help () -> impl Render<Tui> {
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"),
single(" Ctrl", " scroll"),
single(" ▲▼▶◀", " cell"),
double(("p", "put"), ("g", "get")),
double(("q", "enqueue"), ("e", "edit")),
single(" wsad", " note"),
double(("a", "append"), ("s", "set"),),
double((",.", "length"), ("<>", "triplet"),),
double(("[]", "phrase"), ("{}", "order"),),
]))
}
fn stats <'a> (&'a self) -> impl Render<Tui> + use<'a> {
row!([&self.cpu, &self.res, &self.size])
}
}