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::*;
pub trait JackApi {
fn jack (&self) -> &Arc<RwLock<JackClient>>;
}
pub trait HasMidiIns {
fn midi_ins (&self) -> &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 pulses_since_start (&self) -> Option<f64> {
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 {
None
}

View file

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

View file

@ -61,12 +61,6 @@ pub struct ArrangerTui {
pub perf: PerfModel,
}
impl JackApi for ArrangerTui {
fn jack (&self) -> &Arc<RwLock<JackClient>> {
&self.jack
}
}
impl Audio for ArrangerTui {
#[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
// 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,
}
impl JackApi for SequencerTui {
fn jack (&self) -> &Arc<RwLock<JackClient>> {
&self.jack
}
}
impl Audio for SequencerTui {
fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
// Start profiling cycle
@ -109,12 +103,12 @@ impl Audio for SequencerTui {
render!(|self: SequencerTui|lay!([self.size, Tui::split_up(false, 1,
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!([
PhraseSelector::play_phrase(&self.player, false, true),
PhraseSelector::next_phrase(&self.player, false, true),
PhraseSelector::play_phrase(&self.player),
PhraseSelector::next_phrase(&self.player),
]), 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),
))),
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 {
fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
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|{
let bg = self.bg;
let border_style = Style::default()
.bg(bg)
.fg(TuiTheme::border_fg(self.focused));
let play_bg = if self.started{Color::Rgb(0,128,0)}else{Color::Rgb(128,64,0)};
Tui::bg(bg, lay!(move|add|{
add(&Tui::fill_x(Tui::at_w(lay!([
//Lozenge(border_style),
Tui::outset_x(0, row!([
Tui::bg(play_bg, Tui::outset_x(1, Tui::fixed_x(9, col!(|add|{
if self.started {
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",
format!("{:.1}s", self.current_second).as_str())), " ",
Tui::fixed_x(8, TransportField("Rate ",
self.sr.as_str())), " ",
Tui::fixed_x(8, TransportField("Sample",
format!("{:.0}k", self.current_sample).as_str())),
]))
]))))?;
//add(&Tui::fill_x(Tui::center_x(Tui::pull_x(2, row!([
//<center>
//])))))?;
//add(&Tui::fill_x(Tui::at_e(lay!(move|add|{
////add(&Lozenge(border_style))?;
//add(&Tui::outset_x(1, row!([
//])))
//}))))
Ok(())
}))
struct Field<'a>(&'a str, &'a str);
render!(|self: Field<'a>|row!([
Tui::fg(Color::Rgb(200, 200, 200), self.0),
" ",
Tui::bold(true, Tui::fg(Color::Rgb(220, 220, 220), self.1)),
]));
struct PlayPause(bool);
render!(|self: PlayPause|Tui::bg(
if self.0{Color::Rgb(0,128,0)}else{Color::Rgb(128,64,0)},
Tui::outset_x(1, Tui::fixed_x(9, col!(|add|{
if self.0 {
add(&col!([Tui::fg(Color::Rgb(0, 255, 0), "▶ PLAYING"), ""]))
} else {
add(&col!(["", Tui::fg(Color::Rgb(255, 128, 0), "⏹ STOPPED")]))
}
})))
));
Tui::bg(self.bg, Tui::fill_x(row!([
PlayPause(self.started), " ",
col!([
Field("Beat", self.beat.as_str()),
Field("BPM ", self.bpm.as_str()),
]),
" ",
col!([
Field("Time ", format!("{:.1}s", self.current_second).as_str()),
Field("Sample", format!("{:.0}k", self.current_sample).as_str()),
]),
])))
});
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
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
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)]
pub struct TransportStatusBar;

View file

@ -1,56 +1,62 @@
use crate::*;
use Ordering::Relaxed;
use ::crossterm::event::*;
pub struct TuiInput {
pub(crate) exited: Arc<AtomicBool>,
pub(crate) event: TuiEvent,
pub(crate) event: TuiEvent,
}
#[derive(Debug, Clone)]
pub enum TuiEvent {
/// Terminal input
Input(::crossterm::event::Event),
/// Update values but not the whole form.
Update,
/// Update the whole form.
Redraw,
/// Device gains focus
Focus,
/// Device loses focus
Blur,
// /// JACK notification
// Jack(JackEvent)
Input(Event),
/// JACK notification
_Jack(JackEvent)
}
impl Input<Tui> for TuiInput {
type Event = TuiEvent;
fn event (&self) -> &TuiEvent { &self.event }
fn is_done (&self) -> bool { self.exited.fetch_and(true, Ordering::Relaxed) }
fn done (&self) { self.exited.store(true, Ordering::Relaxed); }
}
impl TuiInput {
// TODO remove
pub fn handle_keymap <T> (&self, state: &mut T, keymap: &KeyMap<T>) -> Usually<bool> {
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)
fn event (&self) -> &TuiEvent {
&self.event
}
fn is_done (&self) -> bool {
self.exited.fetch_and(true, Relaxed)
}
fn done (&self) {
self.exited.store(true, Relaxed);
}
}
pub type KeyHandler<T> = &'static dyn Fn(&mut T)->Usually<bool>;
pub type KeyBinding<T> = (KeyCode, KeyModifiers, &'static str, &'static str, KeyHandler<T>);
pub type KeyMap<T> = [KeyBinding<T>];
/// Define key pattern in key match statement
#[macro_export] macro_rules! key {
(Ctrl-$code:pat) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code,
modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, state: KeyEventState::NONE
})) };
(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
pub const fn key (code: KeyCode) -> KeyEvent {
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
#[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)
}
}
}
pub type KeyHandler<T> = &'static dyn Fn(&mut T)->Usually<bool>;
/// Define key pattern in key match statement
#[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
}))
}
}
pub type KeyBinding<T> = (KeyCode, KeyModifiers, &'static str, &'static str, KeyHandler<T>);
#[macro_export] macro_rules! key_lit {
($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
}))
}
}
pub type KeyMap<T> = [KeyBinding<T>];
*/

View file

@ -4,6 +4,7 @@ use PhraseLengthFocus::*;
use PhraseLengthCommand::*;
/// Displays and edits phrase length.
#[derive(Clone)]
pub struct PhraseLength {
/// Pulses per beat (quaver)
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
render!(|self: PhraseListView<'a>|{
let Self { title, focused, entered, phrases, index, mode } = self;
let bg = if *focused {TuiTheme::g(32)} else {TuiTheme::null()};
let border_bg = if *focused {TuiTheme::bg()} else {TuiTheme::null()};
let border_color = if *entered {TuiTheme::bo1()} else {TuiTheme::bo2()};
let title_color = if *focused {TuiTheme::ti1()} else {TuiTheme::ti2()};
let bg = TuiTheme::g(32);
let title_color = TuiTheme::ti1();
let upper_left = format!("{title}");
let upper_right = format!("({})", phrases.len());
Tui::bg(bg, lay!(move|add|{
//add(&Lozenge(Style::default().bg(border_bg).fg(border_color)))?;
add(&Tui::inset_xy(0, 1, Tui::fill_xy(col!(move|add|match mode {
Some(PhrasesMode::Import(_, ref browser)) => {
add(browser)
},
Some(PhrasesMode::Export(_, ref browser)) => {
add(browser)
},
_ => {
for (i, phrase) in phrases.iter().enumerate() {
add(&lay!(|add|{
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 {
if *focused && i == *phrase {
length.pulses = *new_length;
length.focus = Some(*focus);
}
Some(PhrasesMode::Import(_, ref file_picker)) => add(file_picker),
Some(PhrasesMode::Export(_, ref file_picker)) => add(file_picker),
_ => Ok(for (i, phrase) in phrases.iter().enumerate() {
add(&lay!(|add|{
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 {
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]));
let mut row2 = format!(" {name}");
if let Some(PhrasesMode::Rename(phrase, _)) = mode {
if *focused && i == *phrase {
row2 = format!("{row2}");
}
};
let row2 = Tui::bold(true, row2);
add(&Tui::bg(color.base.rgb, Tui::fill_x(col!([row1, row2]))))?;
if *entered && i == *index {
add(&CORNERS)?;
}
Ok(())
}))?;
}
Ok(())
}
}
add(&Tui::bg(color.base.rgb, Tui::fill_x(col!([
Tui::fill_x(lay!(|add|{
add(&Tui::fill_x(Tui::at_w(format!(" {i}"))))?;
add(&Tui::fill_x(Tui::at_e(Tui::pull_x(1, length.clone()))))
})),
Tui::bold(true, {
let mut row2 = format!(" {name}");
if let Some(PhrasesMode::Rename(phrase, _)) = mode {
if *focused && i == *phrase {
row2 = format!("{row2}");
}
};
row2
}),
]))))?;
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_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(crate) title: &'static str,
pub(crate) phrase: Option<(Moment, Option<Arc<RwLock<Phrase>>>)>,
pub(crate) focused: bool,
pub(crate) entered: bool,
pub(crate) time: String,
}
// TODO: Display phrases always in order of appearance
render!(|self: PhraseSelector|{
let Self { title, phrase, focused, entered } = self;
let border_bg = if *focused {TuiTheme::border_bg()} else {TuiTheme::null()};
let border_color = if *focused {TuiTheme::bo1()} else {TuiTheme::bo2()};
let Self { title, phrase, time } = self;
let border_bg = TuiTheme::border_bg();
let border_color = TuiTheme::bo1();
let border = Lozenge(Style::default().bg(border_bg).fg(border_color));
let title_color = if phrase.is_some() {
TuiTheme::g(200)
} else if *focused {
TuiTheme::ti1()
} else {
TuiTheme::ti2()
};
let title_color = TuiTheme::g(200);
Tui::fixed_y(2, lay!(move|add|{
if phrase.is_none() {
add(&Tui::fill_x(border))?;
}
//if phrase.is_none() {
//add(&Tui::fill_x(border))?;
//}
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((instant, Some(phrase))) = phrase {
let Phrase { ref name, color, length, .. } = *phrase.read().unwrap();
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(Tui::at_w(Tui::fg(TuiTheme::g(255), format!(" ")))),
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}")))
]))))))?;
}
Ok(())
}))))
if let Some((_started, Some(phrase))) = phrase {
let Phrase { ref name, color, .. } = *phrase.read().unwrap();
add(&Tui::bg(color.base.rgb, Tui::fill_x(col!([
Tui::fill_x(lay!([
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::bold(true, Tui::fg(TuiTheme::g(255), format!(" {name}")))
]))))?;
}
Ok(())
}))
});
impl PhraseSelector {
pub fn play_phrase <T: HasPlayPhrase> (
state: &T,
focused: bool,
entered: bool,
) -> Self {
// beats elapsed
pub fn play_phrase <T: HasPlayPhrase + HasClock> (state: &T) -> Self {
let phrase = state.play_phrase().clone();
Self {
focused,
entered: focused && entered,
phrase: state.play_phrase().clone(),
title: "Now:",
title: "Now:",
time: if let Some(elapsed) = state.pulses_since_start_looped() {
format!("+{:>}", state.clock().timebase.format_beats_0(elapsed))
} else {
String::from("")
},
phrase,
}
}
pub fn next_phrase <T: HasPlayPhrase> (
state: &T,
focused: bool,
entered: bool,
) -> Self {
// beats until switchover
pub fn next_phrase <T: HasPlayPhrase> (state: &T) -> Self {
let phrase = state.next_phrase().clone();
Self {
focused,
entered: focused && entered,
phrase: state.next_phrase().clone(),
title: "Next:",
title: "Next:",
time: phrase.as_ref().map(|(t, _)|{
let target = t.pulse.get();
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 (
phrase: &Option<Arc<RwLock<Phrase>>>,
focused: bool,
entered: bool,
) -> Self {
pub fn edit_phrase (phrase: &Option<Arc<RwLock<Phrase>>>) -> Self {
let phrase = phrase.clone();
Self {
focused,
entered: focused && entered,
phrase: Some((Moment::default(), phrase.clone())),
title: "Edit:",
title: "Edit:",
time: phrase.as_ref().map(|p|format!("{}", p.read().unwrap().length)).unwrap_or(String::new()),
phrase: Some((Moment::default(), phrase)),
}
}
}