mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 03:36:41 +01:00
wip: big flat
This commit is contained in:
parent
8cbe621b07
commit
4a3de618d0
20 changed files with 305 additions and 336 deletions
|
|
@ -22,7 +22,7 @@ pub struct ArrangerTui {
|
|||
pub size: Measure<Tui>,
|
||||
pub note_buf: Vec<u8>,
|
||||
pub midi_buf: Vec<Vec<Vec<u8>>>,
|
||||
pub editor: MidiEditorModel,
|
||||
pub editor: MidiEditor,
|
||||
pub perf: PerfModel,
|
||||
}
|
||||
impl ArrangerTui {
|
||||
|
|
@ -99,27 +99,29 @@ impl ArrangerTui {
|
|||
}
|
||||
}
|
||||
}
|
||||
render!(<Tui>|self: ArrangerTui|{
|
||||
let play = PlayPause(self.clock.is_rolling());
|
||||
let transport = TransportView::new(self, Some(ItemPalette::from(TuiTheme::g(96))), true);
|
||||
let with_transport = |x|col!([row!(![&play, &transport]), &x]);
|
||||
let pool_size = if self.phrases.visible { self.splits[1] } else { 0 };
|
||||
let with_pool = |x|Split::w(false, pool_size, PoolView(&self.phrases), x);
|
||||
let status = ArrangerStatus::from(self);
|
||||
let with_editbar = |x|Split::n(false, 1, MidiEditStatus(&self.editor), x);
|
||||
let with_status = |x|Split::n(false, 2, status, x);
|
||||
let with_size = |x|lay!([&self.size, x]);
|
||||
let arranger = ||lay!(|add|{
|
||||
let color = self.color;
|
||||
add(&Fill::wh(Tui::bg(color.darkest.rgb, ())))?;
|
||||
add(&Fill::wh(Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb))))?;
|
||||
add(&Self::render_mode(self))
|
||||
});
|
||||
with_size(with_status(with_editbar(with_pool(with_transport(col!([
|
||||
Fill::w(Fixed::h(20, arranger())),
|
||||
Fill::wh(&self.editor),
|
||||
]))))))
|
||||
});
|
||||
impl Render<Tui> for ArrangerTui {
|
||||
fn content (&self) -> Option<impl Render<Tui>> {
|
||||
let play = PlayPause(self.clock.is_rolling());
|
||||
let transport = TransportView::new(self, Some(ItemPalette::from(TuiTheme::g(96))), true);
|
||||
let with_transport = |x|col!([row!(![&play, &transport]), &x]);
|
||||
let pool_size = if self.phrases.visible { self.splits[1] } else { 0 };
|
||||
let with_pool = |x|Split::w(false, pool_size, PoolView(&self.phrases), x);
|
||||
let status = ArrangerStatus::from(self);
|
||||
let with_editbar = |x|Split::n(false, 1, MidiEditStatus(&self.editor), x);
|
||||
let with_status = |x|Split::n(false, 2, status, x);
|
||||
let with_size = |x|lay!([&self.size, x]);
|
||||
let arranger = ||lay!(|add|{
|
||||
let color = self.color;
|
||||
add(&Fill::wh(Tui::bg(color.darkest.rgb, ())))?;
|
||||
add(&Fill::wh(Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb))))?;
|
||||
add(&Self::render_mode(self))
|
||||
});
|
||||
Some(with_size(with_status(with_editbar(with_pool(with_transport(col!([
|
||||
Fill::w(Fixed::h(20, arranger())),
|
||||
Fill::wh(&self.editor),
|
||||
])))))))
|
||||
}
|
||||
}
|
||||
audio!(|self: ArrangerTui, client, scope|{
|
||||
// Start profiling cycle
|
||||
let t0 = self.perf.get_t0();
|
||||
|
|
|
|||
69
src/core.rs
69
src/core.rs
|
|
@ -1,69 +0,0 @@
|
|||
pub(crate) use std::error::Error;
|
||||
|
||||
pub(crate) mod color;
|
||||
pub(crate) use color::*;
|
||||
pub use color::*;
|
||||
|
||||
pub(crate) mod command; pub(crate) use command::*;
|
||||
pub(crate) mod engine; pub(crate) use engine::*;
|
||||
pub(crate) mod focus; pub(crate) use focus::*;
|
||||
pub(crate) mod input; pub(crate) use input::*;
|
||||
pub(crate) mod output; pub(crate) use output::*;
|
||||
|
||||
pub(crate) use std::sync::atomic::{Ordering, AtomicBool, AtomicUsize};
|
||||
pub(crate) use Ordering::Relaxed;
|
||||
|
||||
pub use self::{
|
||||
engine::Engine,
|
||||
input::Handle,
|
||||
output::Render
|
||||
};
|
||||
|
||||
/// Standard result type.
|
||||
pub type Usually<T> = Result<T, Box<dyn Error>>;
|
||||
|
||||
/// Standard optional result type.
|
||||
pub type Perhaps<T> = Result<Option<T>, Box<dyn Error>>;
|
||||
|
||||
/// Define test modules.
|
||||
#[macro_export] macro_rules! testmod {
|
||||
($($name:ident)*) => { $(#[cfg(test)] mod $name;)* };
|
||||
}
|
||||
|
||||
/// Prototypal case of implementor macro.
|
||||
/// Saves 4loc per data pats.
|
||||
#[macro_export] macro_rules! from {
|
||||
($(<$($lt:lifetime),+>)?|$state:ident:$Source:ty|$Target:ty=$cb:expr) => {
|
||||
impl $(<$($lt),+>)? From<$Source> for $Target {
|
||||
fn from ($state:$Source) -> Self { $cb }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub trait Gettable<T> {
|
||||
/// Returns current value
|
||||
fn get (&self) -> T;
|
||||
}
|
||||
|
||||
pub trait Mutable<T>: Gettable<T> {
|
||||
/// Sets new value, returns old
|
||||
fn set (&mut self, value: T) -> T;
|
||||
}
|
||||
|
||||
pub trait InteriorMutable<T>: Gettable<T> {
|
||||
/// Sets new value, returns old
|
||||
fn set (&self, value: T) -> T;
|
||||
}
|
||||
|
||||
impl Gettable<bool> for AtomicBool {
|
||||
fn get (&self) -> bool { self.load(Relaxed) }
|
||||
}
|
||||
impl InteriorMutable<bool> for AtomicBool {
|
||||
fn set (&self, value: bool) -> bool { self.swap(value, Relaxed) }
|
||||
}
|
||||
impl Gettable<usize> for AtomicUsize {
|
||||
fn get (&self) -> usize { self.load(Relaxed) }
|
||||
}
|
||||
impl InteriorMutable<usize> for AtomicUsize {
|
||||
fn set (&self, value: usize) -> usize { self.swap(value, Relaxed) }
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
#[cfg(test)] mod test_focus {
|
||||
use super::focus::*;
|
||||
#[test] fn test_focus () {
|
||||
|
||||
struct FocusTest {
|
||||
focused: char,
|
||||
cursor: (usize, usize)
|
||||
}
|
||||
|
||||
impl HasFocus for FocusTest {
|
||||
type Item = char;
|
||||
fn focused (&self) -> Self::Item {
|
||||
self.focused
|
||||
}
|
||||
fn set_focused (&mut self, to: Self::Item) {
|
||||
self.focused = to
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusGrid for FocusTest {
|
||||
fn focus_cursor (&self) -> (usize, usize) {
|
||||
self.cursor
|
||||
}
|
||||
fn focus_cursor_mut (&mut self) -> &mut (usize, usize) {
|
||||
&mut self.cursor
|
||||
}
|
||||
fn focus_layout (&self) -> &[&[Self::Item]] {
|
||||
&[
|
||||
&['a', 'a', 'a', 'b', 'b', 'd'],
|
||||
&['a', 'a', 'a', 'b', 'b', 'd'],
|
||||
&['a', 'a', 'a', 'c', 'c', 'd'],
|
||||
&['a', 'a', 'a', 'c', 'c', 'd'],
|
||||
&['e', 'e', 'e', 'e', 'e', 'e'],
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
let mut tester = FocusTest { focused: 'a', cursor: (0, 0) };
|
||||
|
||||
tester.focus_right();
|
||||
assert_eq!(tester.cursor.0, 3);
|
||||
assert_eq!(tester.focused, 'b');
|
||||
|
||||
tester.focus_down();
|
||||
assert_eq!(tester.cursor.1, 2);
|
||||
assert_eq!(tester.focused, 'c');
|
||||
|
||||
}
|
||||
}
|
||||
34
src/event.rs
Normal file
34
src/event.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
use crate::*;
|
||||
|
||||
pub struct EventMap<'a, const N: usize, E, T, U>(
|
||||
pub [(E, &'a dyn Fn(T) -> U); N],
|
||||
pub Option<&'a dyn Fn(T) -> U>,
|
||||
);
|
||||
|
||||
impl<'a, const N: usize, E: PartialEq, T, U> EventMap<'a, N, E, T, U> {
|
||||
pub fn handle (&self, context: T, event: &E) -> Option<U> {
|
||||
for (binding, handler) in self.0.iter() {
|
||||
if event == binding {
|
||||
return Some(handler(context))
|
||||
}
|
||||
}
|
||||
return None
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! event_map {
|
||||
($events:expr) => {
|
||||
EventMap($events, None)
|
||||
};
|
||||
($events:expr, $default: expr) => {
|
||||
EventMap($events, $default)
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! event_map_input_to_command {
|
||||
($Engine:ty: $Model:ty: $Command:ty: $EventMap:expr) => {
|
||||
input_to_command!($Command: <$Engine>|state: $Model, input|{
|
||||
event_map!($EventMap).handle(state, input.event())?
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ use crate::*;
|
|||
use super::*;
|
||||
use KeyCode::{Char, Delete, Tab, Up, Down, Left, Right};
|
||||
use ClockCommand::{Play, Pause};
|
||||
use GrooveboxCommand::*;
|
||||
use GrooveboxCommand as Cmd;
|
||||
use PhraseCommand::*;
|
||||
use PhrasePoolCommand::*;
|
||||
|
||||
|
|
@ -11,7 +11,7 @@ pub struct Groovebox {
|
|||
|
||||
pub player: MidiPlayer,
|
||||
pub pool: PoolModel,
|
||||
pub editor: MidiEditorModel,
|
||||
pub editor: MidiEditor,
|
||||
pub sampler: Sampler,
|
||||
|
||||
pub size: Measure<Tui>,
|
||||
|
|
@ -49,7 +49,7 @@ impl Groovebox {
|
|||
)));
|
||||
player.play_phrase = Some((Moment::zero(&player.clock.timebase), Some(phrase.clone())));
|
||||
let pool = crate::pool::PoolModel::from(&phrase);
|
||||
let editor = crate::midi::MidiEditorModel::from(&phrase);
|
||||
let editor = crate::midi::MidiEditor::from(&phrase);
|
||||
Ok(Self {
|
||||
_jack: jack.clone(),
|
||||
player,
|
||||
|
|
@ -142,25 +142,29 @@ render!(<Tui>|self:Groovebox|{
|
|||
});
|
||||
|
||||
struct EditStatus<'a, T: Render<Tui>>(&'a Sampler, &'a MidiEditor, usize, T);
|
||||
render!(<Tui>|self: EditStatus<'a, T: Render<Tui>>|Split::n(false, 9, col!(![
|
||||
row!(|add|{
|
||||
if let Some(sample) = &self.0.mapped[self.2] {
|
||||
add(&format!("Sample {}", sample.read().unwrap().end))?;
|
||||
}
|
||||
add(&MidiEditStatus(&self.1))?;
|
||||
Ok(())
|
||||
}),
|
||||
lay!([
|
||||
Outer(Style::default().fg(TuiTheme::g(128))),
|
||||
Fill::w(Fixed::h(8, if let Some((_, sample)) = &self.0.recording {
|
||||
SampleViewer(Some(sample.clone()))
|
||||
} else if let Some(sample) = &self.0.mapped[self.2] {
|
||||
SampleViewer(Some(sample.clone()))
|
||||
} else {
|
||||
SampleViewer(None)
|
||||
})),
|
||||
]),
|
||||
]), self.3));
|
||||
impl<'a, T: Render<Tui>> Render<Tui> for EditStatus<'a, T> {
|
||||
fn content (&self) -> impl Render<Tui> {
|
||||
Split::n(false, 9, col!([
|
||||
row!(|add|{
|
||||
if let Some(sample) = &self.0.mapped[self.2] {
|
||||
add(&format!("Sample {}", sample.read().unwrap().end))?;
|
||||
}
|
||||
add(&MidiEditStatus(&self.1))?;
|
||||
Ok(())
|
||||
}),
|
||||
lay!([
|
||||
Outer(Style::default().fg(TuiTheme::g(128))),
|
||||
Fill::w(Fixed::h(8, if let Some((_, sample)) = &self.0.recording {
|
||||
SampleViewer(Some(sample.clone()))
|
||||
} else if let Some(sample) = &self.0.mapped[self.2] {
|
||||
SampleViewer(Some(sample.clone()))
|
||||
} else {
|
||||
SampleViewer(None)
|
||||
})),
|
||||
]),
|
||||
]), &self.3)
|
||||
}
|
||||
}
|
||||
|
||||
struct GrooveboxSamples<'a>(&'a Groovebox);
|
||||
render!(<Tui>|self: GrooveboxSamples<'a>|{
|
||||
|
|
@ -204,19 +208,19 @@ input_to_command!(GrooveboxCommand: <Tui>|state: Groovebox, input|match input.ev
|
|||
},
|
||||
|
||||
// Transport: Play from start or rewind to start
|
||||
key_pat!(Char(' ')) => Clock(
|
||||
key_pat!(Char(' ')) => Cmd::Clock(
|
||||
if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }
|
||||
),
|
||||
|
||||
// Tab: Toggle visibility of phrase pool column
|
||||
key_pat!(Tab) => Pool(PoolCommand::Show(!state.pool.visible)),
|
||||
key_pat!(Tab) => Cmd::Pool(PoolCommand::Show(!state.pool.visible)),
|
||||
|
||||
// q: Enqueue currently edited phrase
|
||||
key_pat!(Char('q')) => Enqueue(Some(state.pool.phrase().clone())),
|
||||
key_pat!(Char('q')) => Cmd::Enqueue(Some(state.pool.phrase().clone())),
|
||||
// 0: Enqueue phrase 0 (stop all)
|
||||
key_pat!(Char('0')) => Enqueue(Some(state.pool.phrases()[0].clone())),
|
||||
key_pat!(Char('0')) => Cmd::Enqueue(Some(state.pool.phrases()[0].clone())),
|
||||
|
||||
key_pat!(Shift-Char('R')) => Sampler(if state.sampler.recording.is_some() {
|
||||
key_pat!(Shift-Char('R')) => Cmd::Sampler(if state.sampler.recording.is_some() {
|
||||
SamplerCommand::RecordFinish
|
||||
} else {
|
||||
SamplerCommand::RecordBegin(u7::from(state.editor.note_point() as u8))
|
||||
|
|
@ -226,7 +230,7 @@ input_to_command!(GrooveboxCommand: <Tui>|state: Groovebox, input|match input.ev
|
|||
key_pat!(Char('e')) => if let Some((_, Some(playing))) = state.player.play_phrase() {
|
||||
let editing = state.editor.phrase().as_ref().map(|p|p.read().unwrap().clone());
|
||||
let selected = state.pool.phrase().clone();
|
||||
Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing {
|
||||
Cmd::Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing {
|
||||
selected
|
||||
} else {
|
||||
playing.clone()
|
||||
|
|
@ -238,9 +242,9 @@ input_to_command!(GrooveboxCommand: <Tui>|state: Groovebox, input|match input.ev
|
|||
// For the rest, use the default keybindings of the components.
|
||||
// The ones defined above supersede them.
|
||||
_ => if let Some(command) = PhraseCommand::input_to_command(&state.editor, input) {
|
||||
Editor(command)
|
||||
Cmd::Editor(command)
|
||||
} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) {
|
||||
Pool(command)
|
||||
Cmd::Pool(command)
|
||||
} else {
|
||||
return None
|
||||
}
|
||||
|
|
@ -250,7 +254,7 @@ command!(|self: GrooveboxCommand, state: Groovebox|match self {
|
|||
Self::Pool(cmd) => {
|
||||
let mut default = |cmd: PoolCommand|cmd
|
||||
.execute(&mut state.pool)
|
||||
.map(|x|x.map(Pool));
|
||||
.map(|x|x.map(Self::Pool));
|
||||
match cmd {
|
||||
// autoselect: automatically load selected phrase in editor
|
||||
PoolCommand::Select(_) => {
|
||||
|
|
@ -268,13 +272,13 @@ command!(|self: GrooveboxCommand, state: Groovebox|match self {
|
|||
}
|
||||
},
|
||||
Self::Editor(cmd) => {
|
||||
let default = ||cmd.execute(&mut state.editor).map(|x|x.map(Editor));
|
||||
let default = ||cmd.execute(&mut state.editor).map(|x|x.map(Self::Editor));
|
||||
match cmd {
|
||||
_ => default()?
|
||||
}
|
||||
},
|
||||
Self::Clock(cmd) => cmd.execute(state)?.map(Clock),
|
||||
Self::Sampler(cmd) => cmd.execute(&mut state.sampler)?.map(Sampler),
|
||||
Self::Clock(cmd) => cmd.execute(state)?.map(Self::Clock),
|
||||
Self::Sampler(cmd) => cmd.execute(&mut state.sampler)?.map(Self::Sampler),
|
||||
Self::Enqueue(phrase) => {
|
||||
state.player.enqueue_next(phrase.as_ref());
|
||||
None
|
||||
|
|
|
|||
155
src/lib.rs
155
src/lib.rs
|
|
@ -1,69 +1,50 @@
|
|||
#![allow(unused)]
|
||||
#![allow(clippy::unit_arg)]
|
||||
|
||||
pub mod core; pub use self::core::*;
|
||||
|
||||
pub mod time; pub(crate) use self::time::*;
|
||||
pub use self::time::HasClock;
|
||||
|
||||
pub mod space; pub(crate) use self::space::*;
|
||||
pub use self::space::Measure;
|
||||
|
||||
pub mod tui; pub(crate) use self::tui::*;
|
||||
pub use tui::*;
|
||||
|
||||
pub mod jack; pub(crate) use self::jack::*;
|
||||
pub use self::jack::*;
|
||||
|
||||
pub mod midi; pub(crate) use self::midi::*;
|
||||
|
||||
pub mod meter; pub(crate) use self::meter::*;
|
||||
|
||||
pub mod piano_h; pub(crate) use self::piano_h::*;
|
||||
|
||||
pub mod transport; pub(crate) use self::transport::*;
|
||||
pub use self::transport::TransportTui;
|
||||
|
||||
pub mod sequencer; pub(crate) use self::sequencer::*;
|
||||
pub use self::sequencer::SequencerTui;
|
||||
|
||||
pub mod arranger; pub(crate) use self::arranger::*;
|
||||
pub use self::arranger::ArrangerTui;
|
||||
|
||||
pub mod sampler; pub(crate) use self::sampler::*;
|
||||
pub use self::sampler::{SamplerTui, Sampler, Sample, Voice};
|
||||
|
||||
pub mod mixer; pub(crate) use self::mixer::*;
|
||||
pub use self::mixer::{Mixer, MixerTrack, MixerTrackDevice};
|
||||
|
||||
pub mod plugin; pub(crate) use self::plugin::*;
|
||||
pub use self::plugin::*;
|
||||
|
||||
pub mod groovebox; pub(crate) use self::groovebox::*;
|
||||
pub use self::groovebox::Groovebox;
|
||||
|
||||
pub mod pool; pub(crate) use self::pool::*;
|
||||
pub use self::pool::PoolModel;
|
||||
|
||||
pub mod status; pub(crate) use self::status::*;
|
||||
|
||||
pub use ::better_panic; pub(crate) use better_panic::{Settings, Verbosity};
|
||||
|
||||
pub use ::atomic_float; pub(crate) use atomic_float::*;
|
||||
|
||||
pub(crate) use std::sync::{Arc, Mutex, RwLock};
|
||||
pub(crate) use std::sync::atomic::Ordering;
|
||||
pub(crate) use std::sync::atomic::{AtomicBool, AtomicUsize};
|
||||
pub(crate) use std::collections::BTreeMap;
|
||||
pub(crate) use std::marker::PhantomData;
|
||||
pub(crate) use std::thread::{spawn, JoinHandle};
|
||||
pub(crate) use std::path::PathBuf;
|
||||
pub(crate) use std::ffi::OsString;
|
||||
pub(crate) use std::time::Duration;
|
||||
pub(crate) use std::io::{Stdout, stdout};
|
||||
pub(crate) use std::ops::{Add, Sub, Mul, Div, Rem};
|
||||
pub(crate) use std::cmp::{Ord, Eq, PartialEq};
|
||||
pub(crate) use std::collections::BTreeMap;
|
||||
pub(crate) use std::error::Error;
|
||||
pub(crate) use std::ffi::OsString;
|
||||
pub(crate) use std::fmt::{Debug, Display, Formatter};
|
||||
pub(crate) use std::io::{Stdout, stdout};
|
||||
pub(crate) use std::marker::PhantomData;
|
||||
pub(crate) use std::ops::{Add, Sub, Mul, Div, Rem};
|
||||
pub(crate) use std::path::PathBuf;
|
||||
pub(crate) use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::{self, *}};
|
||||
pub(crate) use std::sync::{Arc, Mutex, RwLock};
|
||||
pub(crate) use std::thread::{spawn, JoinHandle};
|
||||
pub(crate) use std::time::Duration;
|
||||
|
||||
pub mod arranger; pub use self::arranger::*;
|
||||
pub mod color; pub use self::color::*;
|
||||
pub mod command; pub use self::command::*;
|
||||
pub mod engine; pub use self::engine::*;
|
||||
pub mod event; pub use self::event::*;
|
||||
pub mod file; pub use self::file::*;
|
||||
pub mod focus; pub use self::focus::*;
|
||||
pub mod groovebox; pub use self::groovebox::*;
|
||||
pub mod input; pub use self::input::*;
|
||||
pub mod jack; pub use self::jack::*;
|
||||
pub mod meter; pub use self::meter::*;
|
||||
pub mod midi; pub use self::midi::*;
|
||||
pub mod mixer; pub use self::mixer::*;
|
||||
pub mod output; pub use self::output::*;
|
||||
pub mod piano_h; pub use self::piano_h::*;
|
||||
pub mod plugin; pub use self::plugin::*;
|
||||
pub mod pool; pub use self::pool::*;
|
||||
pub mod sampler; pub use self::sampler::*;
|
||||
pub mod sequencer; pub use self::sequencer::*;
|
||||
pub mod space; pub use self::space::*;
|
||||
pub mod status; pub use self::status::*;
|
||||
pub mod time; pub use self::time::*;
|
||||
pub mod transport; pub use self::transport::*;
|
||||
pub mod tui; pub use self::tui::*;
|
||||
|
||||
pub use ::better_panic;
|
||||
pub(crate) use better_panic::{Settings, Verbosity};
|
||||
|
||||
pub use ::atomic_float;
|
||||
pub(crate) use atomic_float::*;
|
||||
|
||||
pub use ::crossterm;
|
||||
pub(crate) use crossterm::{ExecutableCommand};
|
||||
|
|
@ -93,3 +74,55 @@ pub(crate) use ::palette::{
|
|||
};
|
||||
|
||||
testmod! { test }
|
||||
|
||||
/// Standard result type.
|
||||
pub type Usually<T> = Result<T, Box<dyn Error>>;
|
||||
|
||||
/// Standard optional result type.
|
||||
pub type Perhaps<T> = Result<Option<T>, Box<dyn Error>>;
|
||||
|
||||
/// Define test modules.
|
||||
#[macro_export] macro_rules! testmod {
|
||||
($($name:ident)*) => { $(#[cfg(test)] mod $name;)* };
|
||||
}
|
||||
|
||||
/// Prototypal case of implementor macro.
|
||||
/// Saves 4loc per data pats.
|
||||
#[macro_export] macro_rules! from {
|
||||
($(<$($lt:lifetime),+>)?|$state:ident:$Source:ty|$Target:ty=$cb:expr) => {
|
||||
impl $(<$($lt),+>)? From<$Source> for $Target {
|
||||
fn from ($state:$Source) -> Self { $cb }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub trait Gettable<T> {
|
||||
/// Returns current value
|
||||
fn get (&self) -> T;
|
||||
}
|
||||
|
||||
pub trait Mutable<T>: Gettable<T> {
|
||||
/// Sets new value, returns old
|
||||
fn set (&mut self, value: T) -> T;
|
||||
}
|
||||
|
||||
pub trait InteriorMutable<T>: Gettable<T> {
|
||||
/// Sets new value, returns old
|
||||
fn set (&self, value: T) -> T;
|
||||
}
|
||||
|
||||
impl Gettable<bool> for AtomicBool {
|
||||
fn get (&self) -> bool { self.load(Relaxed) }
|
||||
}
|
||||
|
||||
impl InteriorMutable<bool> for AtomicBool {
|
||||
fn set (&self, value: bool) -> bool { self.swap(value, Relaxed) }
|
||||
}
|
||||
|
||||
impl Gettable<usize> for AtomicUsize {
|
||||
fn get (&self) -> usize { self.load(Relaxed) }
|
||||
}
|
||||
|
||||
impl InteriorMutable<usize> for AtomicUsize {
|
||||
fn set (&self, value: usize) -> usize { self.swap(value, Relaxed) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ use KeyCode::{Char, Up, Down, Left, Right, Enter};
|
|||
use PhraseCommand::*;
|
||||
|
||||
pub trait HasEditor {
|
||||
fn editor (&self) -> &MidiEditorModel;
|
||||
fn editor (&self) -> &MidiEditor;
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! has_editor {
|
||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasEditor for $Struct $(<$($L),*$($T),*>)? {
|
||||
fn editor (&$self) -> &MidiEditorModel { &$cb }
|
||||
fn editor (&$self) -> &MidiEditor { &$cb }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -29,8 +29,8 @@ pub enum PhraseCommand {
|
|||
Show(Option<Arc<RwLock<MidiClip>>>),
|
||||
}
|
||||
|
||||
event_map_input_to_command!(Tui: MidiEditorModel: PhraseCommand: MidiEditorModel::KEYS);
|
||||
impl MidiEditorModel {
|
||||
event_map_input_to_command!(Tui: MidiEditor: PhraseCommand: MidiEditor::KEYS);
|
||||
impl MidiEditor {
|
||||
const KEYS: KeyMapping<31, Self> = [
|
||||
(kexp!(Ctrl-Alt-Up), &|s: &Self|SetNoteScroll(s.note_point() + 3)),
|
||||
(kexp!(Ctrl-Alt-Down), &|s: &Self|SetNoteScroll(s.note_point().saturating_sub(3))),
|
||||
|
|
@ -71,8 +71,8 @@ impl MidiEditorModel {
|
|||
}
|
||||
}
|
||||
|
||||
impl Command<MidiEditorModel> for PhraseCommand {
|
||||
fn execute (self, state: &mut MidiEditorModel) -> Perhaps<Self> {
|
||||
impl Command<MidiEditor> for PhraseCommand {
|
||||
fn execute (self, state: &mut MidiEditor) -> Perhaps<Self> {
|
||||
use PhraseCommand::*;
|
||||
match self {
|
||||
Show(phrase) => { state.set_phrase(phrase.as_ref()); },
|
||||
|
|
@ -92,13 +92,13 @@ impl Command<MidiEditorModel> for PhraseCommand {
|
|||
}
|
||||
|
||||
/// Contains state for viewing and editing a phrase
|
||||
pub struct MidiEditorModel {
|
||||
pub struct MidiEditor {
|
||||
/// Renders the phrase
|
||||
pub mode: Box<dyn PhraseViewMode>,
|
||||
pub size: Measure<Tui>
|
||||
}
|
||||
|
||||
impl Default for MidiEditorModel {
|
||||
impl Default for MidiEditor {
|
||||
fn default () -> Self {
|
||||
let mut mode = Box::new(PianoHorizontal::new(None));
|
||||
mode.redraw();
|
||||
|
|
@ -106,13 +106,13 @@ impl Default for MidiEditorModel {
|
|||
}
|
||||
}
|
||||
|
||||
has_size!(<Tui>|self:MidiEditorModel|&self.size);
|
||||
render!(<Tui>|self: MidiEditorModel|{
|
||||
has_size!(<Tui>|self: MidiEditor|&self.size);
|
||||
render!(<Tui>|self: MidiEditor|{
|
||||
self.autoscroll();
|
||||
self.autozoom();
|
||||
&self.mode
|
||||
});
|
||||
//render!(<Tui>|self: MidiEditorModel|lay!(|add|{add(&self.size)?;add(self.mode)}));//bollocks
|
||||
//render!(<Tui>|self: MidiEditor|lay!(|add|{add(&self.size)?;add(self.mode)}));//bollocks
|
||||
|
||||
pub trait PhraseViewMode: Render<Tui> + HasSize<Tui> + MidiRange + MidiPoint + Debug + Send + Sync {
|
||||
fn buffer_size (&self, phrase: &MidiClip) -> (usize, usize);
|
||||
|
|
@ -125,9 +125,9 @@ pub trait PhraseViewMode: Render<Tui> + HasSize<Tui> + MidiRange + MidiPoint + D
|
|||
}
|
||||
}
|
||||
|
||||
impl MidiView<Tui> for MidiEditorModel {}
|
||||
impl MidiView<Tui> for MidiEditor {}
|
||||
|
||||
impl TimeRange for MidiEditorModel {
|
||||
impl TimeRange for MidiEditor {
|
||||
fn time_len (&self) -> &AtomicUsize { self.mode.time_len() }
|
||||
fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() }
|
||||
fn time_lock (&self) -> &AtomicBool { self.mode.time_lock() }
|
||||
|
|
@ -135,24 +135,24 @@ impl TimeRange for MidiEditorModel {
|
|||
fn time_axis (&self) -> &AtomicUsize { self.mode.time_axis() }
|
||||
}
|
||||
|
||||
impl NoteRange for MidiEditorModel {
|
||||
impl NoteRange for MidiEditor {
|
||||
fn note_lo (&self) -> &AtomicUsize { self.mode.note_lo() }
|
||||
fn note_axis (&self) -> &AtomicUsize { self.mode.note_axis() }
|
||||
}
|
||||
|
||||
impl NotePoint for MidiEditorModel {
|
||||
impl NotePoint for MidiEditor {
|
||||
fn note_len (&self) -> usize { self.mode.note_len() }
|
||||
fn set_note_len (&self, x: usize) { self.mode.set_note_len(x) }
|
||||
fn note_point (&self) -> usize { self.mode.note_point() }
|
||||
fn set_note_point (&self, x: usize) { self.mode.set_note_point(x) }
|
||||
}
|
||||
|
||||
impl TimePoint for MidiEditorModel {
|
||||
impl TimePoint for MidiEditor {
|
||||
fn time_point (&self) -> usize { self.mode.time_point() }
|
||||
fn set_time_point (&self, x: usize) { self.mode.set_time_point(x) }
|
||||
}
|
||||
|
||||
impl PhraseViewMode for MidiEditorModel {
|
||||
impl PhraseViewMode for MidiEditor {
|
||||
fn buffer_size (&self, phrase: &MidiClip) -> (usize, usize) {
|
||||
self.mode.buffer_size(phrase)
|
||||
}
|
||||
|
|
@ -170,7 +170,7 @@ impl PhraseViewMode for MidiEditorModel {
|
|||
}
|
||||
}
|
||||
|
||||
impl MidiEditorModel {
|
||||
impl MidiEditor {
|
||||
/// Put note at current position
|
||||
pub fn put_note (&mut self, advance: bool) {
|
||||
let mut redraw = false;
|
||||
|
|
@ -203,22 +203,22 @@ impl MidiEditorModel {
|
|||
}
|
||||
}
|
||||
|
||||
from!(|phrase: &Arc<RwLock<MidiClip>>|MidiEditorModel = {
|
||||
from!(|phrase: &Arc<RwLock<MidiClip>>|MidiEditor = {
|
||||
let mut model = Self::from(Some(phrase.clone()));
|
||||
model.redraw();
|
||||
model
|
||||
});
|
||||
|
||||
from!(|phrase: Option<Arc<RwLock<MidiClip>>>|MidiEditorModel = {
|
||||
from!(|phrase: Option<Arc<RwLock<MidiClip>>>|MidiEditor = {
|
||||
let mut model = Self::default();
|
||||
*model.phrase_mut() = phrase;
|
||||
model.redraw();
|
||||
model
|
||||
});
|
||||
|
||||
impl std::fmt::Debug for MidiEditorModel {
|
||||
impl std::fmt::Debug for MidiEditor {
|
||||
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
f.debug_struct("MidiEditorModel")
|
||||
f.debug_struct("MidiEditor")
|
||||
.field("mode", &self.mode)
|
||||
.finish()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,24 @@
|
|||
use crate::*;
|
||||
|
||||
/// Ad-hoc widget with custom rendering.
|
||||
pub fn render <E, F> (render: F) -> impl Render<E>
|
||||
where E: Engine, F: Fn(&mut E::Output)->Usually<()>+Send+Sync
|
||||
{
|
||||
Widget::new(|_|Ok(Some([0.into(),0.into()].into())), render)
|
||||
}
|
||||
|
||||
/// Cast to dynamic pointer
|
||||
pub fn widget <E, T> (w: &T) -> &dyn Render<E>
|
||||
where E: Engine, T: Render<E>
|
||||
{
|
||||
w as &dyn Render<E>
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! render {
|
||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*E$(,$T:ident$(:$U:path)?)*$(,)?>)?|$cb:expr) => {
|
||||
impl<E: Engine, $($($L),*$($T $(: $U)?),*)?> Render<E> for $Struct $(<$($L,)* E, $($T),*>)? {
|
||||
fn min_size (&$self, to: <E as Engine>::Size) -> Perhaps<<E as Engine>::Size> {
|
||||
$cb.min_size(to)
|
||||
}
|
||||
fn render (&$self, to: &mut <E as Engine>::Output) -> Usually<()> {
|
||||
$cb.render(to)
|
||||
fn content (&$self) -> Option<impl Render<$E>> {
|
||||
Some($cb)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -22,11 +33,8 @@ use crate::*;
|
|||
$($($L),+)?
|
||||
$($($T),+)?
|
||||
>)? {
|
||||
fn min_size (&$self, to: <$E as Engine>::Size) -> Perhaps<<$E as Engine>::Size> {
|
||||
$cb.min_size(to)
|
||||
}
|
||||
fn render (&$self, to: &mut <$E as Engine>::Output) -> Usually<()> {
|
||||
$cb.render(to)
|
||||
fn content (&$self) -> Option<impl Render<$E>> {
|
||||
Some($cb)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -42,19 +50,28 @@ pub trait Output<E: Engine> {
|
|||
fn render_in (&mut self, area: E::Area, widget: &dyn Render<E>) -> Usually<()>;
|
||||
}
|
||||
|
||||
/// Cast to dynamic pointer
|
||||
pub fn widget <E: Engine, T: Render<E>> (w: &T) -> &dyn Render<E> {
|
||||
w as &dyn Render<E>
|
||||
}
|
||||
|
||||
/// A renderable component
|
||||
pub trait Render<E: Engine>: Send + Sync {
|
||||
pub trait Render<E: Engine>: Send + Sync where (): Render<E> {
|
||||
fn content (&self) -> Option<impl Render<E>> where Self: Sized {
|
||||
None::<()>
|
||||
}
|
||||
/// Minimum size to use
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
Ok(Some(to))
|
||||
self.content().map(|content|content.min_size(to)).unwrap_or(Ok(None))
|
||||
}
|
||||
/// Draw to output render target
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()>;
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
self.content().map(|content|content.render(to)).unwrap_or(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> Render<E> for () {
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
Ok(None)
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine, R: Render<E>> Render<E> for &R {
|
||||
|
|
@ -12,7 +12,7 @@ pub struct SequencerTui {
|
|||
pub clock: ClockModel,
|
||||
pub phrases: PoolModel,
|
||||
pub player: MidiPlayer,
|
||||
pub editor: MidiEditorModel,
|
||||
pub editor: MidiEditor,
|
||||
pub size: Measure<Tui>,
|
||||
pub status: bool,
|
||||
pub note_buf: Vec<u8>,
|
||||
|
|
@ -30,7 +30,7 @@ from_jack!(|jack|SequencerTui {
|
|||
transport: true,
|
||||
selectors: true,
|
||||
phrases: PoolModel::from(&phrase),
|
||||
editor: MidiEditorModel::from(&phrase),
|
||||
editor: MidiEditor::from(&phrase),
|
||||
player: MidiPlayer::from((&clock, &phrase)),
|
||||
size: Measure::new(),
|
||||
midi_buf: vec![vec![];65536],
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::*;
|
||||
|
||||
pub struct MidiEditStatus<'a>(pub &'a MidiEditorModel);
|
||||
pub struct MidiEditStatus<'a>(pub &'a MidiEditor);
|
||||
render!(<Tui>|self:MidiEditStatus<'a>|{
|
||||
|
||||
let (color, name, length, looped) = if let Some(phrase) = self.0.phrase().as_ref().map(|p|p.read().unwrap()) {
|
||||
|
|
|
|||
49
src/test.rs
49
src/test.rs
|
|
@ -1,3 +1,52 @@
|
|||
//#[cfg(test)] mod test_focus {
|
||||
//use super::focus::*;
|
||||
//#[test] fn test_focus () {
|
||||
|
||||
//struct FocusTest {
|
||||
//focused: char,
|
||||
//cursor: (usize, usize)
|
||||
//}
|
||||
|
||||
//impl HasFocus for FocusTest {
|
||||
//type Item = char;
|
||||
//fn focused (&self) -> Self::Item {
|
||||
//self.focused
|
||||
//}
|
||||
//fn set_focused (&mut self, to: Self::Item) {
|
||||
//self.focused = to
|
||||
//}
|
||||
//}
|
||||
|
||||
//impl FocusGrid for FocusTest {
|
||||
//fn focus_cursor (&self) -> (usize, usize) {
|
||||
//self.cursor
|
||||
//}
|
||||
//fn focus_cursor_mut (&mut self) -> &mut (usize, usize) {
|
||||
//&mut self.cursor
|
||||
//}
|
||||
//fn focus_layout (&self) -> &[&[Self::Item]] {
|
||||
//&[
|
||||
//&['a', 'a', 'a', 'b', 'b', 'd'],
|
||||
//&['a', 'a', 'a', 'b', 'b', 'd'],
|
||||
//&['a', 'a', 'a', 'c', 'c', 'd'],
|
||||
//&['a', 'a', 'a', 'c', 'c', 'd'],
|
||||
//&['e', 'e', 'e', 'e', 'e', 'e'],
|
||||
//]
|
||||
//}
|
||||
//}
|
||||
|
||||
//let mut tester = FocusTest { focused: 'a', cursor: (0, 0) };
|
||||
|
||||
//tester.focus_right();
|
||||
//assert_eq!(tester.cursor.0, 3);
|
||||
//assert_eq!(tester.focused, 'b');
|
||||
|
||||
//tester.focus_down();
|
||||
//assert_eq!(tester.cursor.1, 2);
|
||||
//assert_eq!(tester.focused, 'c');
|
||||
|
||||
//}
|
||||
//}
|
||||
//use crate::*;
|
||||
|
||||
//struct TestEngine([u16;4], Vec<Vec<char>>);
|
||||
|
|
|
|||
22
src/tui.rs
22
src/tui.rs
|
|
@ -1,27 +1,17 @@
|
|||
use crate::*;
|
||||
|
||||
mod tui_input; pub(crate) use self::tui_input::*;
|
||||
mod tui_output; pub(crate) use self::tui_output::*;
|
||||
|
||||
mod tui_input; pub(crate) use self::tui_input::*;
|
||||
pub use self::tui_input::TuiInput;
|
||||
|
||||
mod tui_output; pub(crate) use self::tui_output::*;
|
||||
pub use self::tui_output::TuiOutput;
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
|
||||
mod tui_style;
|
||||
|
||||
mod tui_theme; pub(crate) use self::tui_theme::*;
|
||||
|
||||
mod tui_border; pub(crate) use self::tui_border::*;
|
||||
|
||||
///////////////////////////////////////////////////////
|
||||
|
||||
pub mod file_browser; pub(crate) use self::file_browser::*;
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
|
||||
pub fn render <F: Fn(&mut TuiOutput)->Usually<()>+Send+Sync> (render: F) -> impl Render<Tui> {
|
||||
Widget::new(|_|Ok(Some([0u16,0u16])), render)
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
|
||||
pub struct Tui {
|
||||
|
|
@ -31,7 +21,7 @@ pub struct Tui {
|
|||
pub area: [u16;4], // FIXME auto resize
|
||||
}
|
||||
|
||||
impl crate::core::Engine for Tui {
|
||||
impl Engine for Tui {
|
||||
type Unit = u16;
|
||||
type Size = [Self::Unit;2];
|
||||
type Area = [Self::Unit;4];
|
||||
|
|
|
|||
|
|
@ -59,39 +59,6 @@ impl Input<Tui> for TuiInput {
|
|||
};
|
||||
}
|
||||
|
||||
pub struct EventMap<'a, const N: usize, E, T, U>(
|
||||
pub [(E, &'a dyn Fn(T) -> U); N],
|
||||
pub Option<&'a dyn Fn(T) -> U>,
|
||||
);
|
||||
|
||||
impl<'a, const N: usize, E: PartialEq, T, U> EventMap<'a, N, E, T, U> {
|
||||
pub fn handle (&self, context: T, event: &E) -> Option<U> {
|
||||
for (binding, handler) in self.0.iter() {
|
||||
if event == binding {
|
||||
return Some(handler(context))
|
||||
}
|
||||
}
|
||||
return None
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! event_map {
|
||||
($events:expr) => {
|
||||
EventMap($events, None)
|
||||
};
|
||||
($events:expr, $default: expr) => {
|
||||
EventMap($events, $default)
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! event_map_input_to_command {
|
||||
($Engine:ty: $Model:ty: $Command:ty: $EventMap:expr) => {
|
||||
input_to_command!($Command: <$Engine>|state: $Model, input|{
|
||||
event_map!($EventMap).handle(state, input.event())?
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type KeyMapping<const N: usize, T> = [(TuiEvent, &'static dyn Fn(&T)->PhraseCommand);N];
|
||||
|
||||
#[macro_export] macro_rules! kexp {
|
||||
|
|
|
|||
|
|
@ -121,15 +121,6 @@ pub fn half_block (lower: bool, upper: bool) -> Option<char> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Render<Tui> for () {
|
||||
fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
Ok(None)
|
||||
}
|
||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Render<Tui> for &str {
|
||||
fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
// TODO: line breaks
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue