general unfuckeries

This commit is contained in:
🪞👃🪞 2024-12-12 10:39:04 +01:00
parent d0d187b5b6
commit a7ff74e27c
11 changed files with 210 additions and 317 deletions

View file

@ -1,9 +1,5 @@
use crate::*; use crate::*;
pub trait JackApi {
fn jack (&self) -> &Arc<RwLock<JackClient>>;
}
pub trait HasMidiIns { pub trait HasMidiIns {
fn midi_ins (&self) -> &Vec<Port<MidiIn>>; fn midi_ins (&self) -> &Vec<Port<MidiIn>>;
fn midi_ins_mut (&mut self) -> &mut Vec<Port<MidiIn>>; fn midi_ins_mut (&mut self) -> &mut Vec<Port<MidiIn>>;

View file

@ -16,7 +16,17 @@ pub trait HasPlayPhrase: HasClock {
fn next_phrase_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<Phrase>>>)>; fn next_phrase_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<Phrase>>>)>;
fn pulses_since_start (&self) -> Option<f64> { fn pulses_since_start (&self) -> Option<f64> {
if let Some((started, Some(_))) = self.play_phrase().as_ref() { if let Some((started, Some(_))) = self.play_phrase().as_ref() {
Some(self.clock().playhead.pulse.get() - started.pulse.get()) let elapsed = self.clock().playhead.pulse.get() - started.pulse.get();
Some(elapsed)
} else {
None
}
}
fn pulses_since_start_looped (&self) -> Option<f64> {
if let Some((started, Some(phrase))) = self.play_phrase().as_ref() {
let elapsed = self.clock().playhead.pulse.get() - started.pulse.get();
let elapsed = (elapsed as usize % phrase.read().unwrap().length) as f64;
Some(elapsed)
} else { } else {
None None
} }

View file

@ -1,7 +1,7 @@
use crate::*; use crate::*;
use jack::*; use jack::*;
#[derive(Debug)] #[derive(Debug, Clone)]
/// Event enum for JACK events. /// Event enum for JACK events.
pub enum JackEvent { pub enum JackEvent {
ThreadInit, ThreadInit,

View file

@ -61,12 +61,6 @@ pub struct ArrangerTui {
pub perf: PerfModel, pub perf: PerfModel,
} }
impl JackApi for ArrangerTui {
fn jack (&self) -> &Arc<RwLock<JackClient>> {
&self.jack
}
}
impl Audio for ArrangerTui { impl Audio for ArrangerTui {
#[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { #[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
// Start profiling cycle // Start profiling cycle

View file

@ -0,0 +1,2 @@
use crate::*;
use super::*;

View file

@ -64,12 +64,6 @@ pub struct SequencerTui {
pub perf: PerfModel, pub perf: PerfModel,
} }
impl JackApi for SequencerTui {
fn jack (&self) -> &Arc<RwLock<JackClient>> {
&self.jack
}
}
impl Audio for SequencerTui { impl Audio for SequencerTui {
fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
// Start profiling cycle // Start profiling cycle
@ -109,12 +103,12 @@ impl Audio for SequencerTui {
render!(|self: SequencerTui|lay!([self.size, Tui::split_up(false, 1, render!(|self: SequencerTui|lay!([self.size, Tui::split_up(false, 1,
Tui::fill_xy(SequencerStatusBar::from(self)), Tui::fill_xy(SequencerStatusBar::from(self)),
Tui::split_left(false, 20, Tui::split_right(false, 20,
Tui::fixed_x(20, Tui::split_down(false, 4, col!([ Tui::fixed_x(20, Tui::split_down(false, 4, col!([
PhraseSelector::play_phrase(&self.player, false, true), PhraseSelector::play_phrase(&self.player),
PhraseSelector::next_phrase(&self.player, false, true), PhraseSelector::next_phrase(&self.player),
]), Tui::split_up(false, 2, ]), Tui::split_up(false, 2,
PhraseSelector::edit_phrase(&self.editor.phrase, self.focused() == SequencerFocus::PhraseEditor, true), PhraseSelector::edit_phrase(&self.editor.phrase),
PhraseListView::from(self), PhraseListView::from(self),
))), ))),
col!([ col!([

View file

@ -43,12 +43,6 @@ impl HasClock for TransportTui {
} }
} }
impl JackApi for TransportTui {
fn jack (&self) -> &Arc<RwLock<JackClient>> {
&self.jack
}
}
impl Audio for TransportTui { impl Audio for TransportTui {
fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
ClockAudio(self).process(client, scope) ClockAudio(self).process(client, scope)
@ -120,55 +114,53 @@ impl<T: HasClock> From<(&T, Option<ItemPalette>, bool)> for TransportView {
} }
} }
struct TransportField<'a>(&'a str, &'a str);
render!(|self: TransportField<'a>|{
col!([
Tui::fg(Color::Rgb(200, 200, 200), self.0),
Tui::bold(true, Tui::fg(Color::Rgb(220, 220, 220), self.1)),
])
});
render!(|self: TransportView|{ render!(|self: TransportView|{
let bg = self.bg;
let border_style = Style::default() struct Field<'a>(&'a str, &'a str);
.bg(bg) render!(|self: Field<'a>|row!([
.fg(TuiTheme::border_fg(self.focused)); Tui::fg(Color::Rgb(200, 200, 200), self.0),
let play_bg = if self.started{Color::Rgb(0,128,0)}else{Color::Rgb(128,64,0)}; " ",
Tui::bg(bg, lay!(move|add|{ Tui::bold(true, Tui::fg(Color::Rgb(220, 220, 220), self.1)),
add(&Tui::fill_x(Tui::at_w(lay!([ ]));
//Lozenge(border_style),
Tui::outset_x(0, row!([ struct PlayPause(bool);
Tui::bg(play_bg, Tui::outset_x(1, Tui::fixed_x(9, col!(|add|{ render!(|self: PlayPause|Tui::bg(
if self.started { if self.0{Color::Rgb(0,128,0)}else{Color::Rgb(128,64,0)},
add(&col!([Tui::fg(Color::Rgb(0, 255, 0), "▶ PLAYING"), ""])) Tui::outset_x(1, Tui::fixed_x(9, col!(|add|{
} else { if self.0 {
add(&col!(["", Tui::fg(Color::Rgb(255, 128, 0), "⏹ STOPPED")])) add(&col!([Tui::fg(Color::Rgb(0, 255, 0), "▶ PLAYING"), ""]))
} } else {
})))), " ", add(&col!(["", Tui::fg(Color::Rgb(255, 128, 0), "⏹ STOPPED")]))
Tui::fixed_x(10, TransportField("Beat", }
self.beat.as_str())), " ", })))
Tui::fixed_x(8, TransportField("BPM ", ));
self.bpm.as_str())), " ",
Tui::fixed_x(8, TransportField("Second", Tui::bg(self.bg, Tui::fill_x(row!([
format!("{:.1}s", self.current_second).as_str())), " ", PlayPause(self.started), " ",
Tui::fixed_x(8, TransportField("Rate ", col!([
self.sr.as_str())), " ", Field("Beat", self.beat.as_str()),
Tui::fixed_x(8, TransportField("Sample", Field("BPM ", self.bpm.as_str()),
format!("{:.0}k", self.current_sample).as_str())), ]),
])) " ",
]))))?; col!([
//add(&Tui::fill_x(Tui::center_x(Tui::pull_x(2, row!([ Field("Time ", format!("{:.1}s", self.current_second).as_str()),
//<center> Field("Sample", format!("{:.0}k", self.current_sample).as_str()),
//])))))?; ]),
//add(&Tui::fill_x(Tui::at_e(lay!(move|add|{ ])))
////add(&Lozenge(border_style))?;
//add(&Tui::outset_x(1, row!([
//])))
//}))))
Ok(())
}))
}); });
impl HasFocus for TransportTui {
type Item = TransportFocus;
fn focused (&self) -> Self::Item {
self.focus.inner()
}
fn set_focused (&mut self, to: Self::Item) {
self.focus.set_inner(to)
}
}
/// Which item of the transport toolbar is focused /// Which item of the transport toolbar is focused
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TransportFocus { pub enum TransportFocus {
@ -207,29 +199,6 @@ impl FocusWrap<TransportFocus> for Option<TransportFocus> {
} }
} }
impl HasFocus for TransportTui {
type Item = TransportFocus;
/// Get the currently focused item.
fn focused (&self) -> Self::Item {
self.focus.inner()
}
/// Get the currently focused item.
fn set_focused (&mut self, to: Self::Item) {
self.focus.set_inner(to)
}
}
//impl_focus!(TransportTui TransportFocus [
////&[Menu],
//&[
//PlayPause,
//Bpm,
//Sync,
//Quant,
//Clock,
//],
//]);
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub struct TransportStatusBar; pub struct TransportStatusBar;

View file

@ -1,56 +1,62 @@
use crate::*; use crate::*;
use Ordering::Relaxed;
use ::crossterm::event::*;
pub struct TuiInput { pub struct TuiInput {
pub(crate) exited: Arc<AtomicBool>, pub(crate) exited: Arc<AtomicBool>,
pub(crate) event: TuiEvent, pub(crate) event: TuiEvent,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum TuiEvent { pub enum TuiEvent {
/// Terminal input /// Terminal input
Input(::crossterm::event::Event), Input(Event),
/// Update values but not the whole form. /// JACK notification
Update, _Jack(JackEvent)
/// Update the whole form.
Redraw,
/// Device gains focus
Focus,
/// Device loses focus
Blur,
// /// JACK notification
// Jack(JackEvent)
} }
impl Input<Tui> for TuiInput { impl Input<Tui> for TuiInput {
type Event = TuiEvent; type Event = TuiEvent;
fn event (&self) -> &TuiEvent { &self.event } fn event (&self) -> &TuiEvent {
fn is_done (&self) -> bool { self.exited.fetch_and(true, Ordering::Relaxed) } &self.event
fn done (&self) { self.exited.store(true, Ordering::Relaxed); } }
} fn is_done (&self) -> bool {
self.exited.fetch_and(true, Relaxed)
impl TuiInput { }
// TODO remove fn done (&self) {
pub fn handle_keymap <T> (&self, state: &mut T, keymap: &KeyMap<T>) -> Usually<bool> { self.exited.store(true, Relaxed);
match self.event() {
TuiEvent::Input(crossterm::event::Event::Key(event)) => {
for (code, modifiers, _, _, command) in keymap.iter() {
if *code == event.code && modifiers.bits() == event.modifiers.bits() {
return command(state)
}
}
},
_ => {}
};
Ok(false)
} }
} }
pub type KeyHandler<T> = &'static dyn Fn(&mut T)->Usually<bool>; /// Define key pattern in key match statement
#[macro_export] macro_rules! key {
pub type KeyBinding<T> = (KeyCode, KeyModifiers, &'static str, &'static str, KeyHandler<T>); (Ctrl-$code:pat) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code,
modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, state: KeyEventState::NONE
pub type KeyMap<T> = [KeyBinding<T>]; })) };
(Ctrl-$code:expr) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code,
modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, state: KeyEventState::NONE
})) };
(Alt-$code:pat) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code,
modifiers: KeyModifiers::ALT, kind: KeyEventKind::Press, state: KeyEventState::NONE
})) };
(Alt-$code:expr) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code,
modifiers: KeyModifiers::ALT, kind: KeyEventKind::Press, state: KeyEventState::NONE
})) };
(Shift-$code:pat) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code,
modifiers: KeyModifiers::SHIFT, kind: KeyEventKind::Press, state: KeyEventState::NONE
})) };
(Shift-$code:expr) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code,
modifiers: KeyModifiers::SHIFT, kind: KeyEventKind::Press, state: KeyEventState::NONE
})) };
($code:pat) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code,
modifiers: KeyModifiers::NONE, kind: KeyEventKind::Press, state: KeyEventState::NONE
})) };
($code:expr) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code,
modifiers: KeyModifiers::NONE, kind: KeyEventKind::Press, state: KeyEventState::NONE
})) };
}
/*
/// Define a key /// Define a key
pub const fn key (code: KeyCode) -> KeyEvent { pub const fn key (code: KeyCode) -> KeyEvent {
let modifiers = KeyModifiers::NONE; let modifiers = KeyModifiers::NONE;
@ -83,99 +89,31 @@ pub const fn shift (key: KeyEvent) -> KeyEvent {
} }
} }
/// Define a key in a keymap */
#[macro_export] macro_rules! map_key {
($k:ident $(($char:literal))?, $m:ident, $n: literal, $d: literal, $f: expr) => { /*
(KeyCode::$k $(($char))?, KeyModifiers::$m, $n, $d, &$f as &dyn Fn()->Usually<bool>)
impl TuiInput {
// TODO remove
pub fn handle_keymap <T> (&self, state: &mut T, keymap: &KeyMap<T>) -> Usually<bool> {
match self.event() {
TuiEvent::Input(Key(event)) => {
for (code, modifiers, _, _, command) in keymap.iter() {
if *code == event.code && modifiers.bits() == event.modifiers.bits() {
return command(state)
}
}
},
_ => {}
};
Ok(false)
} }
} }
/// Shorthand for key match statement pub type KeyHandler<T> = &'static dyn Fn(&mut T)->Usually<bool>;
#[macro_export] macro_rules! match_key {
($event:expr, {
$($key:pat=>$block:expr),* $(,)?
}) => {
match $event {
$(crossterm::event::Event::Key(crossterm::event::KeyEvent {
code: $key,
modifiers: crossterm::event::KeyModifiers::NONE,
kind: crossterm::event::KeyEventKind::Press,
state: crossterm::event::KeyEventState::NONE
}) => {
$block
})*
_ => Ok(None)
}
}
}
/// Define key pattern in key match statement pub type KeyBinding<T> = (KeyCode, KeyModifiers, &'static str, &'static str, KeyHandler<T>);
#[macro_export] macro_rules! key {
($code:pat) => {
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
code: $code,
modifiers: crossterm::event::KeyModifiers::NONE,
kind: crossterm::event::KeyEventKind::Press,
state: crossterm::event::KeyEventState::NONE
}))
};
(Ctrl-$code:pat) => {
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
code: $code,
modifiers: crossterm::event::KeyModifiers::CONTROL,
kind: crossterm::event::KeyEventKind::Press,
state: crossterm::event::KeyEventState::NONE
}))
};
(Alt-$code:pat) => {
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
code: $code,
modifiers: crossterm::event::KeyModifiers::ALT,
kind: crossterm::event::KeyEventKind::Press,
state: crossterm::event::KeyEventState::NONE
}))
};
(Shift-$code:pat) => {
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
code: $code,
modifiers: crossterm::event::KeyModifiers::SHIFT,
kind: crossterm::event::KeyEventKind::Press,
state: crossterm::event::KeyEventState::NONE
}))
}
}
#[macro_export] macro_rules! key_lit { pub type KeyMap<T> = [KeyBinding<T>];
($code:expr) => {
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent { */
code: $code,
modifiers: crossterm::event::KeyModifiers::NONE,
kind: crossterm::event::KeyEventKind::Press,
state: crossterm::event::KeyEventState::NONE
}))
};
(Ctrl-$code:expr) => {
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
code: $code,
modifiers: crossterm::event::KeyModifiers::CONTROL,
kind: crossterm::event::KeyEventKind::Press,
state: crossterm::event::KeyEventState::NONE
}))
};
(Alt-$code:expr) => {
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
code: $code,
modifiers: crossterm::event::KeyModifiers::ALT,
kind: crossterm::event::KeyEventKind::Press,
state: crossterm::event::KeyEventState::NONE
}))
};
(Shift-$code:expr) => {
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
code: $code,
modifiers: crossterm::event::KeyModifiers::SHIFT,
kind: crossterm::event::KeyEventKind::Press,
state: crossterm::event::KeyEventState::NONE
}))
}
}

View file

@ -4,6 +4,7 @@ use PhraseLengthFocus::*;
use PhraseLengthCommand::*; use PhraseLengthCommand::*;
/// Displays and edits phrase length. /// Displays and edits phrase length.
#[derive(Clone)]
pub struct PhraseLength { pub struct PhraseLength {
/// Pulses per beat (quaver) /// Pulses per beat (quaver)
pub ppq: usize, pub ppq: usize,

View file

@ -110,50 +110,46 @@ impl<'a, T: HasPhraseList> From<&'a T> for PhraseListView<'a> {
// TODO: Display phrases always in order of appearance // TODO: Display phrases always in order of appearance
render!(|self: PhraseListView<'a>|{ render!(|self: PhraseListView<'a>|{
let Self { title, focused, entered, phrases, index, mode } = self; let Self { title, focused, entered, phrases, index, mode } = self;
let bg = if *focused {TuiTheme::g(32)} else {TuiTheme::null()}; let bg = TuiTheme::g(32);
let border_bg = if *focused {TuiTheme::bg()} else {TuiTheme::null()}; let title_color = TuiTheme::ti1();
let border_color = if *entered {TuiTheme::bo1()} else {TuiTheme::bo2()};
let title_color = if *focused {TuiTheme::ti1()} else {TuiTheme::ti2()};
let upper_left = format!("{title}"); let upper_left = format!("{title}");
let upper_right = format!("({})", phrases.len()); let upper_right = format!("({})", phrases.len());
Tui::bg(bg, lay!(move|add|{ Tui::bg(bg, lay!(move|add|{
//add(&Lozenge(Style::default().bg(border_bg).fg(border_color)))?; //add(&Lozenge(Style::default().bg(border_bg).fg(border_color)))?;
add(&Tui::inset_xy(0, 1, Tui::fill_xy(col!(move|add|match mode { add(&Tui::inset_xy(0, 1, Tui::fill_xy(col!(move|add|match mode {
Some(PhrasesMode::Import(_, ref browser)) => { Some(PhrasesMode::Import(_, ref file_picker)) => add(file_picker),
add(browser) Some(PhrasesMode::Export(_, ref file_picker)) => add(file_picker),
}, _ => Ok(for (i, phrase) in phrases.iter().enumerate() {
Some(PhrasesMode::Export(_, ref browser)) => { add(&lay!(|add|{
add(browser) let Phrase { ref name, color, length, .. } = *phrase.read().unwrap();
}, let mut length = PhraseLength::new(length, None);
_ => { if let Some(PhrasesMode::Length(phrase, new_length, focus)) = mode {
for (i, phrase) in phrases.iter().enumerate() { if *focused && i == *phrase {
add(&lay!(|add|{ length.pulses = *new_length;
let Phrase { ref name, color, length, .. } = *phrase.read().unwrap(); length.focus = Some(*focus);
let mut length = PhraseLength::new(length, None);
if let Some(PhrasesMode::Length(phrase, new_length, focus)) = mode {
if *focused && i == *phrase {
length.pulses = *new_length;
length.focus = Some(*focus);
}
} }
let length = Tui::fill_x(Tui::at_e(length)); }
let row1 = Tui::fill_x(lay!([Tui::fill_x(Tui::at_w(format!(" {i}"))), length])); add(&Tui::bg(color.base.rgb, Tui::fill_x(col!([
let mut row2 = format!(" {name}"); Tui::fill_x(lay!(|add|{
if let Some(PhrasesMode::Rename(phrase, _)) = mode { add(&Tui::fill_x(Tui::at_w(format!(" {i}"))))?;
if *focused && i == *phrase { add(&Tui::fill_x(Tui::at_e(Tui::pull_x(1, length.clone()))))
row2 = format!("{row2}"); })),
} Tui::bold(true, {
}; let mut row2 = format!(" {name}");
let row2 = Tui::bold(true, row2); if let Some(PhrasesMode::Rename(phrase, _)) = mode {
add(&Tui::bg(color.base.rgb, Tui::fill_x(col!([row1, row2]))))?; if *focused && i == *phrase {
if *entered && i == *index { row2 = format!("{row2}");
add(&CORNERS)?; }
} };
Ok(()) row2
}))?; }),
} ]))))?;
Ok(()) if *entered && i == *index {
} add(&CORNERS)?;
}
Ok(())
}))?;
})
}))))?; }))))?;
add(&Tui::fill_xy(Tui::at_nw(Tui::push_x(1, Tui::fg(title_color, upper_left.to_string())))))?; add(&Tui::fill_xy(Tui::at_nw(Tui::push_x(1, Tui::fg(title_color, upper_left.to_string())))))?;
add(&Tui::fill_xy(Tui::at_ne(Tui::pull_x(1, Tui::fg(title_color, upper_right.to_string()))))) add(&Tui::fill_xy(Tui::at_ne(Tui::pull_x(1, Tui::fg(title_color, upper_right.to_string())))))

View file

@ -3,79 +3,72 @@ use crate::*;
pub struct PhraseSelector { pub struct PhraseSelector {
pub(crate) title: &'static str, pub(crate) title: &'static str,
pub(crate) phrase: Option<(Moment, Option<Arc<RwLock<Phrase>>>)>, pub(crate) phrase: Option<(Moment, Option<Arc<RwLock<Phrase>>>)>,
pub(crate) focused: bool, pub(crate) time: String,
pub(crate) entered: bool,
} }
// TODO: Display phrases always in order of appearance // TODO: Display phrases always in order of appearance
render!(|self: PhraseSelector|{ render!(|self: PhraseSelector|{
let Self { title, phrase, focused, entered } = self; let Self { title, phrase, time } = self;
let border_bg = if *focused {TuiTheme::border_bg()} else {TuiTheme::null()}; let border_bg = TuiTheme::border_bg();
let border_color = if *focused {TuiTheme::bo1()} else {TuiTheme::bo2()}; let border_color = TuiTheme::bo1();
let border = Lozenge(Style::default().bg(border_bg).fg(border_color)); let border = Lozenge(Style::default().bg(border_bg).fg(border_color));
let title_color = if phrase.is_some() { let title_color = TuiTheme::g(200);
TuiTheme::g(200)
} else if *focused {
TuiTheme::ti1()
} else {
TuiTheme::ti2()
};
Tui::fixed_y(2, lay!(move|add|{ Tui::fixed_y(2, lay!(move|add|{
if phrase.is_none() { //if phrase.is_none() {
add(&Tui::fill_x(border))?; //add(&Tui::fill_x(border))?;
} //}
add(&Tui::push_x(1, Tui::fg(title_color, *title)))?; add(&Tui::push_x(1, Tui::fg(title_color, *title)))?;
add(&Tui::push_y(0, Tui::fill_xy(Layers::new(move|add|{ if let Some((_started, Some(phrase))) = phrase {
if let Some((instant, Some(phrase))) = phrase { let Phrase { ref name, color, .. } = *phrase.read().unwrap();
let Phrase { ref name, color, length, .. } = *phrase.read().unwrap(); add(&Tui::bg(color.base.rgb, Tui::fill_x(col!([
add(&Tui::pull_y(0, Tui::inset_x(0, Tui::bg(color.base.rgb, Tui::fill_x(col!([ Tui::fill_x(lay!([
Tui::fill_x(lay!([ Tui::fill_x(Tui::at_w(Tui::fg(TuiTheme::g(255), format!(" ")))),
Tui::fill_x(Tui::at_w(Tui::fg(TuiTheme::g(255), format!(" ")))), Tui::inset_x(1, Tui::fill_x(Tui::at_e(Tui::fg(TuiTheme::g(255), time))))
Tui::fill_x(Tui::at_e(Tui::fg(TuiTheme::g(255), PhraseLength::new(length, None)))) ])),
])), Tui::bold(true, Tui::fg(TuiTheme::g(255), format!(" {name}")))
Tui::bold(true, Tui::fg(TuiTheme::g(255), format!(" {name}"))) ]))))?;
]))))))?; }
} Ok(())
Ok(())
}))))
})) }))
}); });
impl PhraseSelector { impl PhraseSelector {
pub fn play_phrase <T: HasPlayPhrase> ( // beats elapsed
state: &T, pub fn play_phrase <T: HasPlayPhrase + HasClock> (state: &T) -> Self {
focused: bool, let phrase = state.play_phrase().clone();
entered: bool,
) -> Self {
Self { Self {
focused, title: "Now:",
entered: focused && entered, time: if let Some(elapsed) = state.pulses_since_start_looped() {
phrase: state.play_phrase().clone(), format!("+{:>}", state.clock().timebase.format_beats_0(elapsed))
title: "Now:", } else {
String::from("")
},
phrase,
} }
} }
pub fn next_phrase <T: HasPlayPhrase> ( // beats until switchover
state: &T, pub fn next_phrase <T: HasPlayPhrase> (state: &T) -> Self {
focused: bool, let phrase = state.next_phrase().clone();
entered: bool,
) -> Self {
Self { Self {
focused, title: "Next:",
entered: focused && entered, time: phrase.as_ref().map(|(t, _)|{
phrase: state.next_phrase().clone(), let target = t.pulse.get();
title: "Next:", let current = state.clock().playhead.pulse.get();
if target > current {
let remaining = target - current;
format!("-{:>}", state.clock().timebase.format_beats_0(remaining))
} else {
String::new()
}
}).unwrap_or(String::from("")),
phrase,
} }
} }
pub fn edit_phrase ( pub fn edit_phrase (phrase: &Option<Arc<RwLock<Phrase>>>) -> Self {
phrase: &Option<Arc<RwLock<Phrase>>>, let phrase = phrase.clone();
focused: bool,
entered: bool,
) -> Self {
Self { Self {
focused, title: "Edit:",
entered: focused && entered, time: phrase.as_ref().map(|p|format!("{}", p.read().unwrap().length)).unwrap_or(String::new()),
phrase: Some((Moment::default(), phrase.clone())), phrase: Some((Moment::default(), phrase)),
title: "Edit:",
} }
} }
} }