mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-05-09 08:40:13 +02:00
2114 lines
83 KiB
Rust
2114 lines
83 KiB
Rust
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
//#[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>>);
|
|
|
|
//impl Engine for TestEngine {
|
|
//type Unit = u16;
|
|
//type Size = [Self::Unit;2];
|
|
//type Area = [Self::Unit;4];
|
|
//type Input = Self;
|
|
//type Handled = bool;
|
|
//fn exited (&self) -> bool {
|
|
//true
|
|
//}
|
|
//}
|
|
|
|
//#[derive(Copy, Clone)]
|
|
//struct TestArea(u16, u16);
|
|
|
|
//impl Render<TestEngine> for TestArea {
|
|
//fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
|
//Ok(Some([to[0], to[1], self.0, self.1]))
|
|
//}
|
|
//fn render (&self, to: &mut TestEngine) -> Perhaps<[u16;4]> {
|
|
//if let Some(layout) = self.layout(to.area())? {
|
|
//for y in layout.y()..layout.y()+layout.h()-1 {
|
|
//for x in layout.x()..layout.x()+layout.w()-1 {
|
|
//to.1[y as usize][x as usize] = '*';
|
|
//}
|
|
//}
|
|
//Ok(Some(layout))
|
|
//} else {
|
|
//Ok(None)
|
|
//}
|
|
//}
|
|
//}
|
|
|
|
//#[test]
|
|
//fn test_plus_minus () -> Usually<()> {
|
|
//let area = [0, 0, 10, 10];
|
|
//let engine = TestEngine(area, vec![vec![' ';10];10]);
|
|
//let test = TestArea(4, 4);
|
|
//assert_eq!(test.layout(area)?, Some([0, 0, 4, 4]));
|
|
//assert_eq!(Push::X(1, test).layout(area)?, Some([1, 0, 4, 4]));
|
|
//Ok(())
|
|
//}
|
|
|
|
//#[test]
|
|
//fn test_outset_align () -> Usually<()> {
|
|
//let area = [0, 0, 10, 10];
|
|
//let engine = TestEngine(area, vec![vec![' ';10];10]);
|
|
//let test = TestArea(4, 4);
|
|
//assert_eq!(test.layout(area)?, Some([0, 0, 4, 4]));
|
|
//assert_eq!(Margin::X(1, test).layout(area)?, Some([0, 0, 6, 4]));
|
|
//assert_eq!(Align::X(test).layout(area)?, Some([3, 0, 4, 4]));
|
|
//assert_eq!(Align::X(Margin::X(1, test)).layout(area)?, Some([2, 0, 6, 4]));
|
|
//assert_eq!(Margin::X(1, Align::X(test)).layout(area)?, Some([2, 0, 6, 4]));
|
|
//Ok(())
|
|
//}
|
|
|
|
////#[test]
|
|
////fn test_misc () -> Usually<()> {
|
|
////let area: [u16;4] = [0, 0, 10, 10];
|
|
////let test = TestArea(4, 4);
|
|
////assert_eq!(test.layout(area)?,
|
|
////Some([0, 0, 4, 4]));
|
|
////assert_eq!(Align::Center(test).layout(area)?,
|
|
////Some([3, 3, 4, 4]));
|
|
////assert_eq!(Align::Center(Stack::down(|add|{
|
|
////add(&test)?;
|
|
////add(&test)
|
|
////})).layout(area)?,
|
|
////Some([3, 1, 4, 8]));
|
|
////assert_eq!(Align::Center(Stack::down(|add|{
|
|
////add(&Margin::XY(2, 2, test))?;
|
|
////add(&test)
|
|
////})).layout(area)?,
|
|
////Some([2, 0, 6, 10]));
|
|
////assert_eq!(Align::Center(Stack::down(|add|{
|
|
////add(&Margin::XY(2, 2, test))?;
|
|
////add(&Padding::XY(2, 2, test))
|
|
////})).layout(area)?,
|
|
////Some([2, 1, 6, 8]));
|
|
////assert_eq!(Stack::down(|add|{
|
|
////add(&Margin::XY(2, 2, test))?;
|
|
////add(&Padding::XY(2, 2, test))
|
|
////}).layout(area)?,
|
|
////Some([0, 0, 6, 8]));
|
|
////assert_eq!(Stack::right(|add|{
|
|
////add(&Stack::down(|add|{
|
|
////add(&Margin::XY(2, 2, test))?;
|
|
////add(&Padding::XY(2, 2, test))
|
|
////}))?;
|
|
////add(&Align::Center(TestArea(2 ,2)))
|
|
////}).layout(area)?,
|
|
////Some([0, 0, 8, 8]));
|
|
////Ok(())
|
|
////}
|
|
|
|
////#[test]
|
|
////fn test_offset () -> Usually<()> {
|
|
////let area: [u16;4] = [50, 50, 100, 100];
|
|
////let test = TestArea(3, 3);
|
|
////assert_eq!(Push::X(1, test).layout(area)?, Some([51, 50, 3, 3]));
|
|
////assert_eq!(Push::Y(1, test).layout(area)?, Some([50, 51, 3, 3]));
|
|
////assert_eq!(Push::XY(1, 1, test).layout(area)?, Some([51, 51, 3, 3]));
|
|
////Ok(())
|
|
////}
|
|
|
|
////#[test]
|
|
////fn test_outset () -> Usually<()> {
|
|
////let area: [u16;4] = [50, 50, 100, 100];
|
|
////let test = TestArea(3, 3);
|
|
////assert_eq!(Margin::X(1, test).layout(area)?, Some([49, 50, 5, 3]));
|
|
////assert_eq!(Margin::Y(1, test).layout(area)?, Some([50, 49, 3, 5]));
|
|
////assert_eq!(Margin::XY(1, 1, test).layout(area)?, Some([49, 49, 5, 5]));
|
|
////Ok(())
|
|
////}
|
|
|
|
////#[test]
|
|
////fn test_padding () -> Usually<()> {
|
|
////let area: [u16;4] = [50, 50, 100, 100];
|
|
////let test = TestArea(3, 3);
|
|
////assert_eq!(Padding::X(1, test).layout(area)?, Some([51, 50, 1, 3]));
|
|
////assert_eq!(Padding::Y(1, test).layout(area)?, Some([50, 51, 3, 1]));
|
|
////assert_eq!(Padding::XY(1, 1, test).layout(area)?, Some([51, 51, 1, 1]));
|
|
////Ok(())
|
|
////}
|
|
|
|
////#[test]
|
|
////fn test_stuff () -> Usually<()> {
|
|
////let area: [u16;4] = [0, 0, 100, 100];
|
|
////assert_eq!("1".layout(area)?,
|
|
////Some([0, 0, 1, 1]));
|
|
////assert_eq!("333".layout(area)?,
|
|
////Some([0, 0, 3, 1]));
|
|
////assert_eq!(Layers::new(|add|{add(&"1")?;add(&"333")}).layout(area)?,
|
|
////Some([0, 0, 3, 1]));
|
|
////assert_eq!(Stack::down(|add|{add(&"1")?;add(&"333")}).layout(area)?,
|
|
////Some([0, 0, 3, 2]));
|
|
////assert_eq!(Stack::right(|add|{add(&"1")?;add(&"333")}).layout(area)?,
|
|
////Some([0, 0, 4, 1]));
|
|
////assert_eq!(Stack::down(|add|{
|
|
////add(&Stack::right(|add|{add(&"1")?;add(&"333")}))?;
|
|
////add(&"55555")
|
|
////}).layout(area)?,
|
|
////Some([0, 0, 5, 2]));
|
|
////let area: [u16;4] = [1, 1, 100, 100];
|
|
////assert_eq!(Margin::X(1, Stack::right(|add|{add(&"1")?;add(&"333")})).layout(area)?,
|
|
////Some([0, 1, 6, 1]));
|
|
////assert_eq!(Margin::Y(1, Stack::right(|add|{add(&"1")?;add(&"333")})).layout(area)?,
|
|
////Some([1, 0, 4, 3]));
|
|
////assert_eq!(Margin::XY(1, 1, Stack::right(|add|{add(&"1")?;add(&"333")})).layout(area)?,
|
|
////Some([0, 0, 6, 3]));
|
|
////assert_eq!(Stack::down(|add|{
|
|
////add(&Margin::XY(1, 1, "1"))?;
|
|
////add(&Margin::XY(1, 1, "333"))
|
|
////}).layout(area)?,
|
|
////Some([1, 1, 5, 6]));
|
|
////let area: [u16;4] = [1, 1, 95, 100];
|
|
////assert_eq!(Align::Center(Stack::down(|add|{
|
|
////add(&Margin::XY(1, 1, "1"))?;
|
|
////add(&Margin::XY(1, 1, "333"))
|
|
////})).layout(area)?,
|
|
////Some([46, 48, 5, 6]));
|
|
////assert_eq!(Align::Center(Stack::down(|add|{
|
|
////add(&Layers::new(|add|{
|
|
//////add(&Margin::XY(1, 1, Background(Color::Rgb(0,128,0))))?;
|
|
////add(&Margin::XY(1, 1, "1"))?;
|
|
////add(&Margin::XY(1, 1, "333"))?;
|
|
//////add(&Background(Color::Rgb(0,128,0)))?;
|
|
////Ok(())
|
|
////}))?;
|
|
////add(&Layers::new(|add|{
|
|
//////add(&Margin::XY(1, 1, Background(Color::Rgb(0,0,128))))?;
|
|
////add(&Margin::XY(1, 1, "555"))?;
|
|
////add(&Margin::XY(1, 1, "777777"))?;
|
|
//////add(&Background(Color::Rgb(0,0,128)))?;
|
|
////Ok(())
|
|
////}))
|
|
////})).layout(area)?,
|
|
////Some([46, 48, 5, 6]));
|
|
////Ok(())
|
|
////}
|
|
|
|
//#[derive(Default)] pub struct Sequencer {
|
|
//pub jack: Arc<RwLock<JackConnection>>,
|
|
//pub compact: bool,
|
|
//pub editor: MidiEditor,
|
|
//pub midi_buf: Vec<Vec<Vec<u8>>>,
|
|
//pub note_buf: Vec<u8>,
|
|
//pub perf: PerfModel,
|
|
//pub player: MidiPlayer,
|
|
//pub pool: MidiPool,
|
|
//pub selectors: bool,
|
|
//pub size: Measure<TuiOut>,
|
|
//pub status: bool,
|
|
//pub transport: bool,
|
|
//}
|
|
//has_size!(<TuiOut>|self:Sequencer|&self.size);
|
|
//has_clock!(|self:Sequencer|&self.player.clock);
|
|
//has_clips!(|self:Sequencer|self.pool.clips);
|
|
//has_editor!(|self:Sequencer|self.editor);
|
|
//has_player!(|self:Sequencer|self.player);
|
|
|
|
//#[derive(Default)] pub struct Groovebox {
|
|
//pub jack: Arc<RwLock<JackConnection>>,
|
|
//pub compact: bool,
|
|
//pub editor: MidiEditor,
|
|
//pub midi_buf: Vec<Vec<Vec<u8>>>,
|
|
//pub note_buf: Vec<u8>,
|
|
//pub perf: PerfModel,
|
|
//pub player: MidiPlayer,
|
|
//pub pool: MidiPool,
|
|
//pub sampler: Sampler,
|
|
//pub size: Measure<TuiOut>,
|
|
//pub status: bool,
|
|
//}
|
|
//has_clock!(|self: Groovebox|self.player.clock());
|
|
|
|
//#[derive(Default)] pub struct Arranger {
|
|
//pub clock: Clock,
|
|
//pub color: ItemPalette,
|
|
//pub compact: bool,
|
|
//pub editing: AtomicBool,
|
|
//pub editor: MidiEditor,
|
|
//pub jack: Arc<RwLock<JackConnection>>,
|
|
//pub midi_buf: Vec<Vec<Vec<u8>>>,
|
|
//pub midi_ins: Vec<JackPort<MidiIn>>,
|
|
//pub midi_outs: Vec<JackPort<MidiOut>>,
|
|
//pub note_buf: Vec<u8>,
|
|
//pub perf: PerfModel,
|
|
//pub pool: MidiPool,
|
|
//pub scenes: Vec<ArrangerScene>,
|
|
//pub selected: ArrangerSelection,
|
|
//pub size: Measure<TuiOut>,
|
|
//pub splits: [u16;2],
|
|
//pub tracks: Vec<ArrangerTrack>,
|
|
//}
|
|
//has_clock!(|self: Arranger|&self.clock);
|
|
//has_clips!(|self: Arranger|self.pool.clips);
|
|
//has_editor!(|self: Arranger|self.editor);
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
//render!(TuiOut: (self: Sequencer) => self.size.of(EdnView::from_source(self, Self::EDN)));
|
|
//impl EdnViewData<TuiOut> for &Sequencer {
|
|
//fn get_content <'a> (&'a self, item: EdnItem<&'a str>) -> RenderBox<'a, TuiOut> {
|
|
//use EdnItem::*;
|
|
//match item {
|
|
//Nil => Box::new(()),
|
|
//Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())),
|
|
//Sym(":editor") => (&self.editor).boxed(),
|
|
//Sym(":pool") => self.pool_view().boxed(),
|
|
//Sym(":status") => self.status_view().boxed(),
|
|
//Sym(":toolbar") => self.toolbar_view().boxed(),
|
|
//_ => panic!("no content for {item:?}")
|
|
//}
|
|
//}
|
|
//}
|
|
//impl Sequencer {
|
|
//const EDN: &'static str = include_str!("../edn/sequencer.edn");
|
|
//fn toolbar_view (&self) -> impl Content<TuiOut> + use<'_> {
|
|
//Fill::x(Fixed::y(2, Align::x(ClockView::new(true, &self.player.clock))))
|
|
//}
|
|
//fn status_view (&self) -> impl Content<TuiOut> + use<'_> {
|
|
//Bsp::e(
|
|
//When(self.selectors, Bsp::e(
|
|
//self.player.play_status(),
|
|
//self.player.next_status(),
|
|
//)),
|
|
//Bsp::e(
|
|
//self.editor.clip_status(),
|
|
//self.editor.edit_status(),
|
|
//)
|
|
//)
|
|
//}
|
|
//fn pool_view (&self) -> impl Content<TuiOut> + use<'_> {
|
|
//let w = self.size.w();
|
|
//let clip_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
|
|
//let pool_w = if self.pool.visible { clip_w } else { 0 };
|
|
//let pool = Pull::y(1, Fill::y(Align::e(PoolView(self.pool.visible, &self.pool))));
|
|
//Fixed::x(pool_w, Align::e(Fill::y(PoolView(self.compact, &self.pool))))
|
|
//}
|
|
//fn help () -> impl Content<TuiOut> {
|
|
//let single = |binding, command|row!(" ", col!(
|
|
//Tui::fg(TuiTheme::yellow(), binding),
|
|
//command
|
|
//));
|
|
//let double = |(b1, c1), (b2, c2)|col!(
|
|
//row!(" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1,),
|
|
//row!(" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2,),
|
|
//);
|
|
//Tui::fg_bg(TuiTheme::g(255), TuiTheme::g(50), row!(
|
|
//single("SPACE", "play/pause"),
|
|
//double(("▲▼▶◀", "cursor"), ("Ctrl", "scroll"), ),
|
|
//double(("a", "append"), ("s", "set note"),),
|
|
//double((",.", "length"), ("<>", "triplet"), ),
|
|
//double(("[]", "clip"), ("{}", "order"), ),
|
|
//double(("q", "enqueue"), ("e", "edit"), ),
|
|
//double(("c", "color"), ("", ""),),
|
|
//))
|
|
//}
|
|
//}
|
|
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//render!(TuiOut: (self: Groovebox) => self.size.of(EdnView::from_source(self, Self::EDN)));
|
|
//impl EdnViewData<TuiOut> for &Groovebox {
|
|
//fn get_content <'a> (&'a self, item: EdnItem<&'a str>) -> RenderBox<'a, TuiOut> {
|
|
//use EdnItem::*;
|
|
//match item {
|
|
//Nil => Box::new(()),
|
|
//Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())),
|
|
//Sym(":editor") => (&self.editor).boxed(),
|
|
//Sym(":pool") => self.pool().boxed(),
|
|
//Sym(":status") => self.status().boxed(),
|
|
//Sym(":toolbar") => self.toolbar().boxed(),
|
|
//Sym(":sampler") => self.sampler().boxed(),
|
|
//Sym(":sample") => self.sample().boxed(),
|
|
//_ => panic!("no content for {item:?}")
|
|
//}
|
|
//}
|
|
//fn get_unit (&self, item: EdnItem<&str>) -> u16 {
|
|
//use EdnItem::*;
|
|
//match item.to_str() {
|
|
//":sample-h" => if self.compact { 0 } else { 5 },
|
|
//":samples-w" => if self.compact { 4 } else { 11 },
|
|
//":samples-y" => if self.compact { 1 } else { 0 },
|
|
//":pool-w" => if self.compact { 5 } else {
|
|
//let w = self.size.w();
|
|
//if w > 60 { 20 } else if w > 40 { 15 } else { 10 }
|
|
//},
|
|
//_ => 0
|
|
//}
|
|
//}
|
|
//}
|
|
//impl Groovebox {
|
|
//const EDN: &'static str = include_str!("../edn/groovebox.edn");
|
|
//fn toolbar (&self) -> impl Content<TuiOut> + use<'_> {
|
|
//Fill::x(Fixed::y(2, lay!(
|
|
//Fill::x(Align::w(Meter("L/", self.sampler.input_meter[0]))),
|
|
//Fill::x(Align::e(Meter("R/", self.sampler.input_meter[1]))),
|
|
//Align::x(ClockView::new(true, &self.player.clock)),
|
|
//)))
|
|
//}
|
|
//fn status (&self) -> impl Content<TuiOut> + use<'_> {
|
|
//row!(
|
|
//self.player.play_status(),
|
|
//self.player.next_status(),
|
|
//self.editor.clip_status(),
|
|
//self.editor.edit_status(),
|
|
//)
|
|
//}
|
|
//fn sample (&self) -> impl Content<TuiOut> + use<'_> {
|
|
//let note_pt = self.editor.note_point();
|
|
//let sample_h = if self.compact { 0 } else { 5 };
|
|
//Max::y(sample_h, Fill::xy(
|
|
//Bsp::a(
|
|
//Fill::x(Align::w(Fixed::y(1, self.sampler.status(note_pt)))),
|
|
//self.sampler.viewer(note_pt))))
|
|
//}
|
|
//fn pool (&self) -> impl Content<TuiOut> + use<'_> {
|
|
//let w = self.size.w();
|
|
//let pool_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
|
|
//Fixed::x(if self.compact { 5 } else { pool_w },
|
|
//PoolView(self.compact, &self.pool))
|
|
//}
|
|
//fn sampler (&self) -> impl Content<TuiOut> + use<'_> {
|
|
//let note_pt = self.editor.note_point();
|
|
//let sampler_w = if self.compact { 4 } else { 40 };
|
|
//let sampler_y = if self.compact { 1 } else { 0 };
|
|
//Fixed::x(sampler_w, Push::y(sampler_y, Fill::y(self.sampler.list(self.compact, &self.editor))))
|
|
//}
|
|
//}
|
|
|
|
///// Status bar for sequencer app
|
|
//#[derive(Clone)]
|
|
//pub struct GrooveboxStatus {
|
|
//pub(crate) width: usize,
|
|
//pub(crate) cpu: Option<String>,
|
|
//pub(crate) size: String,
|
|
//pub(crate) playing: bool,
|
|
//}
|
|
//from!(|state: &Groovebox|GrooveboxStatus = {
|
|
//let samples = state.clock().chunk.load(Relaxed);
|
|
//let rate = state.clock().timebase.sr.get();
|
|
//let buffer = samples as f64 / rate;
|
|
//let width = state.size.w();
|
|
//Self {
|
|
//width,
|
|
//playing: state.clock().is_rolling(),
|
|
//cpu: state.perf.percentage().map(|cpu|format!("│{cpu:.01}%")),
|
|
//size: format!("{}x{}│", width, state.size.h()),
|
|
//}
|
|
//});
|
|
//render!(TuiOut: (self: GrooveboxStatus) => Fixed::y(2, lay!(
|
|
//Self::help(),
|
|
//Fill::xy(Align::se(Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), self.stats()))),
|
|
//)));
|
|
//impl GrooveboxStatus {
|
|
//fn help () -> impl Content<TuiOut> {
|
|
//let single = |binding, command|row!(" ", col!(
|
|
//Tui::fg(TuiTheme::yellow(), binding),
|
|
//command
|
|
//));
|
|
//let double = |(b1, c1), (b2, c2)|col!(
|
|
//row!(" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1,),
|
|
//row!(" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2,),
|
|
//);
|
|
//Tui::fg_bg(TuiTheme::g(255), TuiTheme::g(50), row!(
|
|
//single("SPACE", "play/pause"),
|
|
//double(("▲▼▶◀", "cursor"), ("Ctrl", "scroll"), ),
|
|
//double(("a", "append"), ("s", "set note"),),
|
|
//double((",.", "length"), ("<>", "triplet"), ),
|
|
//double(("[]", "phrase"), ("{}", "order"), ),
|
|
//double(("q", "enqueue"), ("e", "edit"), ),
|
|
//double(("c", "color"), ("", ""),),
|
|
//))
|
|
//}
|
|
//fn stats (&self) -> impl Content<TuiOut> + use<'_> {
|
|
//row!(&self.cpu, &self.size)
|
|
//}
|
|
//}
|
|
//macro_rules! edn_context {
|
|
//($Struct:ident |$l:lifetime, $state:ident| {
|
|
//$($key:literal = $field:ident: $Type:ty => $expr:expr,)*
|
|
//}) => {
|
|
|
|
//#[derive(Default)]
|
|
//pub struct EdnView<$l> { $($field: Option<$Type>),* }
|
|
|
|
//impl<$l> EdnView<$l> {
|
|
//pub fn parse <'e> (edn: &[Edn<'e>]) -> impl Fn(&$Struct) + use<'e> {
|
|
//let imports = Self::imports_all(edn);
|
|
//move |state| {
|
|
//let mut context = EdnView::default();
|
|
//for import in imports.iter() {
|
|
//context.import(state, import)
|
|
//}
|
|
//}
|
|
//}
|
|
//fn imports_all <'e> (edn: &[Edn<'e>]) -> Vec<&'e str> {
|
|
//let mut imports = vec![];
|
|
//for edn in edn.iter() {
|
|
//for import in Self::imports_one(edn) {
|
|
//imports.push(import);
|
|
//}
|
|
//}
|
|
//imports
|
|
//}
|
|
//fn imports_one <'e> (edn: &Edn<'e>) -> Vec<&'e str> {
|
|
//match edn {
|
|
//Edn::Symbol(import) => vec![import],
|
|
//Edn::List(edn) => Self::imports_all(edn.as_slice()),
|
|
//_ => vec![],
|
|
//}
|
|
//}
|
|
//pub fn import (&mut self, $state: &$l$Struct, key: &str) {
|
|
//match key {
|
|
//$($key => self.$field = Some($expr),)*
|
|
//_ => {}
|
|
//}
|
|
//}
|
|
//}
|
|
//}
|
|
//}
|
|
|
|
////impl Groovebox {
|
|
////fn status (&self) -> impl Content<TuiOut> + use<'_> {
|
|
////let note_pt = self.editor.note_point();
|
|
////Align::w(Fixed::y(1, ))
|
|
////}
|
|
////fn pool (&self) -> impl Content<TuiOut> + use<'_> {
|
|
////let w = self.size.w();
|
|
////let pool_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
|
|
////Fixed::x(if self.compact { 5 } else { pool_w },
|
|
////)
|
|
////}
|
|
////fn sampler (&self) -> impl Content<TuiOut> + use<'_> {
|
|
////let sampler_w = if self.compact { 4 } else { 11 };
|
|
////let sampler_y = if self.compact { 1 } else { 0 };
|
|
////Fixed::x(sampler_w, Push::y(sampler_y, Fill::y(
|
|
////SampleList::new(self.compact, &self.sampler, &self.editor))))
|
|
////}
|
|
////}
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//render!(TuiOut: (self: Arranger) => self.size.of(EdnView::from_source(self, Self::EDN)));
|
|
//impl EdnViewData<TuiOut> for &Arranger {
|
|
//fn get_content <'a> (&'a self, item: EdnItem<&'a str>) -> RenderBox<'a, TuiOut> {
|
|
//use EdnItem::*;
|
|
//let tracks_w = self.tracks_with_sizes().last().unwrap().3 as u16;
|
|
//match item {
|
|
//Nil => Box::new(()),
|
|
//Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())),
|
|
//Sym(":editor") => (&self.editor).boxed(),
|
|
//Sym(":pool") => self.pool().boxed(),
|
|
//Sym(":status") => self.status().boxed(),
|
|
//Sym(":toolbar") => self.toolbar().boxed(),
|
|
//Sym(":tracks") => self.track_row(tracks_w).boxed(),
|
|
//Sym(":scenes") => self.scene_row(tracks_w).boxed(),
|
|
//Sym(":inputs") => self.input_row(tracks_w).boxed(),
|
|
//Sym(":outputs") => self.output_row(tracks_w).boxed(),
|
|
//_ => panic!("no content for {item:?}")
|
|
//}
|
|
//}
|
|
//}
|
|
//impl Arranger {
|
|
//const EDN: &'static str = include_str!("../edn/arranger.edn");
|
|
//pub const LEFT_SEP: char = '▎';
|
|
|
|
//fn toolbar (&self) -> impl Content<TuiOut> + use<'_> {
|
|
//Fill::x(Fixed::y(2, Align::x(ClockView::new(true, &self.clock))))
|
|
//}
|
|
//fn pool (&self) -> impl Content<TuiOut> + use<'_> {
|
|
//Align::e(Fixed::x(self.sidebar_w(), PoolView(self.compact, &self.pool)))
|
|
//}
|
|
//fn status (&self) -> impl Content<TuiOut> + use<'_> {
|
|
//Bsp::e(self.editor.clip_status(), self.editor.edit_status())
|
|
//}
|
|
//fn is_editing (&self) -> bool {
|
|
// !self.pool.visible
|
|
//}
|
|
//fn sidebar_w (&self) -> u16 {
|
|
//let w = self.size.w();
|
|
//let w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
|
|
//let w = if self.pool.visible { w } else { 8 };
|
|
//w
|
|
//}
|
|
//fn editor_w (&self) -> usize {
|
|
////self.editor.note_len() / self.editor.note_zoom().get()
|
|
//(5 + (self.editor.time_len().get() / self.editor.time_zoom().get()))
|
|
//.min(self.size.w().saturating_sub(20))
|
|
//.max(16)
|
|
////self.editor.time_axis().get().max(16)
|
|
////50
|
|
//}
|
|
//pub fn scenes_with_sizes (&self, h: usize)
|
|
//-> impl Iterator<Item = (usize, &ArrangerScene, usize, usize)>
|
|
//{
|
|
//let mut y = 0;
|
|
//let editing = self.is_editing();
|
|
//let (selected_track, selected_scene) = match self.selected {
|
|
//ArrangerSelection::Clip(t, s) => (Some(t), Some(s)),
|
|
//_ => (None, None)
|
|
//};
|
|
//self.scenes.iter().enumerate().map(move|(s, scene)|{
|
|
//let active = editing && selected_track.is_some() && selected_scene == Some(s);
|
|
//let height = if active { 15 } else { h };
|
|
//let data = (s, scene, y, y + height);
|
|
//y += height;
|
|
//data
|
|
//})
|
|
//}
|
|
//pub fn tracks_with_sizes (&self)
|
|
//-> impl Iterator<Item = (usize, &ArrangerTrack, usize, usize)>
|
|
//{
|
|
//tracks_with_sizes(self.tracks.iter(), match self.selected {
|
|
//ArrangerSelection::Track(t) if self.is_editing() => Some(t),
|
|
//ArrangerSelection::Clip(t, _) if self.is_editing() => Some(t),
|
|
//_ => None
|
|
//}, self.editor_w())
|
|
//}
|
|
|
|
//fn play_row (&self, tracks_w: u16) -> impl Content<TuiOut> + '_ {
|
|
//let h = 2;
|
|
//Fixed::y(h, Bsp::e(
|
|
//Fixed::xy(self.sidebar_w() as u16, h, self.play_header()),
|
|
//Fill::x(Align::c(Fixed::xy(tracks_w, h, self.play_cells())))
|
|
//))
|
|
//}
|
|
//fn play_header (&self) -> BoxThunk<TuiOut> {
|
|
//(||Tui::bold(true, Tui::fg(TuiTheme::g(128), "Playing")).boxed()).into()
|
|
//}
|
|
//fn play_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
|
//(move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| {
|
|
////let color = track.color;
|
|
//let color: ItemPalette = track.color.dark.into();
|
|
//let timebase = self.clock().timebase();
|
|
//let value = Tui::fg_bg(color.lightest.rgb, color.base.rgb,
|
|
//if let Some((_, Some(clip))) = track.player.play_clip().as_ref() {
|
|
//let length = clip.read().unwrap().length;
|
|
//let elapsed = track.player.pulses_since_start().unwrap() as usize;
|
|
//format!("+{:>}", timebase.format_beats_1_short((elapsed % length) as f64))
|
|
//} else {
|
|
//String::new()
|
|
//});
|
|
//let cell = Bsp::s(value, phat_hi(color.dark.rgb, color.darker.rgb));
|
|
//Tui::bg(color.base.rgb, map_east(x1 as u16, (x2 - x1) as u16, cell))
|
|
//})).boxed()).into()
|
|
//}
|
|
|
|
//fn next_row (&self, tracks_w: u16) -> impl Content<TuiOut> + '_ {
|
|
//let h = 2;
|
|
//Fixed::y(h, Bsp::e(
|
|
//Fixed::xy(self.sidebar_w() as u16, h, self.next_header()),
|
|
//Fill::x(Align::c(Fixed::xy(tracks_w, h, self.next_cells())))
|
|
//))
|
|
//}
|
|
//fn next_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
|
//(||Tui::bold(true, Tui::fg(TuiTheme::g(128), "Next")).boxed()).into()
|
|
//}
|
|
//fn next_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
|
//(move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| {
|
|
//let color: ItemPalette = track.color;
|
|
//let color: ItemPalette = track.color.dark.into();
|
|
//let current = &self.clock().playhead;
|
|
//let timebase = ¤t.timebase;
|
|
//let cell = Self::cell(color, Tui::bold(true, {
|
|
//let mut result = String::new();
|
|
//if let Some((t, _)) = track.player.next_clip().as_ref() {
|
|
//let target = t.pulse.get();
|
|
//let current = current.pulse.get();
|
|
//if target > current {
|
|
//result = format!("-{:>}", timebase.format_beats_0_short(target - current))
|
|
//}
|
|
//}
|
|
//result
|
|
//}));
|
|
//let cell = Tui::fg_bg(color.lightest.rgb, color.base.rgb, cell);
|
|
//let cell = Bsp::s(cell, phat_hi(color.dark.rgb, color.darker.rgb));
|
|
//Tui::bg(color.base.rgb, map_east(x1 as u16, (x2 - x1) as u16, cell))
|
|
//})).boxed()).into()
|
|
//}
|
|
///// beats until switchover
|
|
//fn cell_until_next (track: &ArrangerTrack, current: &Arc<Moment>)
|
|
//-> Option<impl Content<TuiOut>>
|
|
//{
|
|
//let timebase = ¤t.timebase;
|
|
//let mut result = String::new();
|
|
//if let Some((t, _)) = track.player.next_clip().as_ref() {
|
|
//let target = t.pulse.get();
|
|
//let current = current.pulse.get();
|
|
//if target > current {
|
|
//result = format!("-{:>}", timebase.format_beats_0_short(target - current))
|
|
//}
|
|
//}
|
|
//Some(result)
|
|
//}
|
|
|
|
//fn track_row (&self, tracks_w: u16) -> impl Content<TuiOut> + '_ {
|
|
//let h = 3;
|
|
//let border = |x|x;//Rugged(Style::default().fg(Color::Rgb(0,0,0)).bg(Color::Reset)).enclose2(x);
|
|
//Fixed::y(h, Bsp::e(
|
|
//Fixed::xy(self.sidebar_w() as u16, h, self.track_header()),
|
|
//Fill::x(Align::c(Fixed::xy(tracks_w, h, border(self.track_cells()))))
|
|
//))
|
|
//}
|
|
//fn track_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
|
//(||Tui::bold(true, Bsp::s(
|
|
//row!(
|
|
//Tui::fg(TuiTheme::g(128), "add "),
|
|
//Tui::fg(TuiTheme::orange(), "t"),
|
|
//Tui::fg(TuiTheme::g(128), "rack"),
|
|
//),
|
|
//row!(
|
|
//Tui::fg(TuiTheme::orange(), "a"),
|
|
//Tui::fg(TuiTheme::g(128), "dd scene"),
|
|
//),
|
|
//).boxed())).into()
|
|
//}
|
|
//fn track_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
|
//let iter = ||self.tracks_with_sizes();
|
|
//(move||Align::x(Map::new(iter, move|(_, track, x1, x2), i| {
|
|
//let name = Push::x(1, &track.name);
|
|
//let color = track.color;
|
|
//let fg = color.lightest.rgb;
|
|
//let bg = color.base.rgb;
|
|
//let active = self.selected.track() == Some(i);
|
|
//let bfg = if active { Color::Rgb(255,255,255) } else { Color::Rgb(0,0,0) };
|
|
//let border = Style::default().fg(bfg).bg(bg);
|
|
//Tui::bg(bg, map_east(x1 as u16, (x2 - x1) as u16,
|
|
//Outer(border).enclose(Tui::fg_bg(fg, bg, Tui::bold(true, Fill::x(Align::x(name)))))
|
|
//))
|
|
//})).boxed()).into()
|
|
//}
|
|
|
|
//fn input_row (&self, tracks_w: u16) -> impl Content<TuiOut> + '_ {
|
|
//let h = 2 + self.midi_ins[0].connect.len() as u16;
|
|
//Fixed::y(h, Bsp::e(
|
|
//Fixed::xy(self.sidebar_w() as u16, h, self.input_header()),
|
|
//Fill::x(Align::c(Fixed::xy(tracks_w, h, self.input_cells())))
|
|
//))
|
|
//}
|
|
//fn input_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
|
//(||Bsp::s(
|
|
//Tui::bold(true, row!(
|
|
//Tui::fg(TuiTheme::g(128), "midi "),
|
|
//Tui::fg(TuiTheme::orange(), "I"),
|
|
//Tui::fg(TuiTheme::g(128), "ns"),
|
|
//)),
|
|
//Bsp::s(
|
|
//Fill::x(Tui::bold(true, Tui::fg_bg(TuiTheme::g(224), TuiTheme::g(64),
|
|
//Align::w(&self.midi_ins[0].name)))),
|
|
//self.midi_ins.get(0)
|
|
//.and_then(|midi_in|midi_in.connect.get(0))
|
|
//.map(|connect|Fill::x(Align::w(
|
|
//Tui::bold(false, Tui::fg_bg(TuiTheme::g(224), TuiTheme::g(64), connect.info())))))
|
|
//)
|
|
//).boxed()).into()
|
|
//}
|
|
//fn input_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
|
//(move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| {
|
|
//let w = (x2 - x1) as u16;
|
|
//let color: ItemPalette = track.color.dark.into();
|
|
//map_east(x1 as u16, w, Fixed::x(w, Self::cell(color, Bsp::n(
|
|
//Self::rec_mon(color.base.rgb, false, false),
|
|
//phat_hi(color.base.rgb, color.dark.rgb)
|
|
//))))
|
|
//})).boxed()).into()
|
|
//}
|
|
//fn rec_mon (bg: Color, rec: bool, mon: bool) -> impl Content<TuiOut> {
|
|
//row!(
|
|
//Tui::fg_bg(if rec { Color::Red } else { bg }, bg, "▐"),
|
|
//Tui::fg_bg(if rec { Color::White } else { Color::Rgb(0,0,0) }, bg, "REC"),
|
|
//Tui::fg_bg(if rec { Color::White } else { bg }, bg, "▐"),
|
|
//Tui::fg_bg(if mon { Color::White } else { Color::Rgb(0,0,0) }, bg, "MON"),
|
|
//Tui::fg_bg(if mon { Color::White } else { bg }, bg, "▌"),
|
|
//)
|
|
//}
|
|
|
|
//fn output_row (&self, tracks_w: u16) -> impl Content<TuiOut> + '_ {
|
|
//let h = 2 + self.midi_outs[0].connect.len() as u16;
|
|
//Fixed::y(h, Bsp::e(
|
|
//Fixed::xy(self.sidebar_w() as u16, h, self.output_header()),
|
|
//Fill::x(Align::c(Fixed::xy(tracks_w, h, self.output_cells())))
|
|
//))
|
|
//}
|
|
//fn output_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
|
//(||Bsp::s(
|
|
//Tui::bold(true, row!(
|
|
//Tui::fg(TuiTheme::g(128), "midi "),
|
|
//Tui::fg(TuiTheme::orange(), "O"),
|
|
//Tui::fg(TuiTheme::g(128), "uts"),
|
|
//)),
|
|
//Bsp::s(
|
|
//Fill::x(Tui::bold(true, Tui::fg_bg(TuiTheme::g(224), TuiTheme::g(64),
|
|
//Align::w(&self.midi_outs[0].name)))),
|
|
//self.midi_outs.get(0)
|
|
//.and_then(|midi_out|midi_out.connect.get(0))
|
|
//.map(|connect|Fill::x(Align::w(
|
|
//Tui::bold(false, Tui::fg_bg(TuiTheme::g(224), TuiTheme::g(64), connect.info())))))
|
|
//),
|
|
//).boxed()).into()
|
|
//}
|
|
//fn output_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
|
//(move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| {
|
|
//let w = (x2 - x1) as u16;
|
|
//let color: ItemPalette = track.color.dark.into();
|
|
//map_east(x1 as u16, w, Fixed::x(w, Self::cell(color, Bsp::n(
|
|
//Self::mute_solo(color.base.rgb, false, false),
|
|
//phat_hi(color.dark.rgb, color.darker.rgb)
|
|
//))))
|
|
//})).boxed()).into()
|
|
//}
|
|
//fn mute_solo (bg: Color, mute: bool, solo: bool) -> impl Content<TuiOut> {
|
|
//row!(
|
|
//Tui::fg_bg(if mute { Color::White } else { Color::Rgb(0,0,0) }, bg, "MUTE"),
|
|
//Tui::fg_bg(if mute { Color::White } else { bg }, bg, "▐"),
|
|
//Tui::fg_bg(if solo { Color::White } else { Color::Rgb(0,0,0) }, bg, "SOLO"),
|
|
//)
|
|
//}
|
|
|
|
//fn scene_row (&self, tracks_w: u16) -> impl Content<TuiOut> + '_ {
|
|
//let h = (self.size.h() as u16).saturating_sub(8).max(8);
|
|
//let border = |x|x;//Skinny(Style::default().fg(Color::Rgb(0,0,0)).bg(Color::Reset)).enclose2(x);
|
|
//Bsp::e(
|
|
//Tui::bg(Color::Reset, Fixed::xy(self.sidebar_w() as u16, h, self.scene_headers())),
|
|
//Tui::bg(Color::Reset, Fill::x(Align::c(Fixed::xy(tracks_w, h, border(self.scene_cells())))))
|
|
//)
|
|
//}
|
|
//fn scene_headers <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
|
//(||{
|
|
//let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0))));
|
|
//let selected = self.selected.scene();
|
|
//Fill::y(Align::c(Map::new(||self.scenes_with_sizes(2), move|(_, scene, y1, y2), i| {
|
|
//let h = (y2 - y1) as u16;
|
|
//let name = format!("🭬{}", &scene.name);
|
|
//let color = scene.color;
|
|
//let active = selected == Some(i);
|
|
//let mid = if active { color.light } else { color.base };
|
|
//let top = Some(last_color.read().unwrap().base.rgb);
|
|
//let cell = phat_sel_3(
|
|
//active,
|
|
//Tui::bold(true, name.clone()),
|
|
//Tui::bold(true, name),
|
|
//top,
|
|
//mid.rgb,
|
|
//Color::Rgb(0, 0, 0)
|
|
//);
|
|
//*last_color.write().unwrap() = color;
|
|
//map_south(y1 as u16, h + 1, Fixed::y(h + 1, cell))
|
|
//}))).boxed()
|
|
//}).into()
|
|
//}
|
|
//fn scene_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
|
//let editing = self.is_editing();
|
|
//let tracks = move||self.tracks_with_sizes();
|
|
//let scenes = ||self.scenes_with_sizes(2);
|
|
//let selected_track = self.selected.track();
|
|
//let selected_scene = self.selected.scene();
|
|
//(move||Fill::y(Align::c(Map::new(tracks, move|(_, track, x1, x2), t| {
|
|
//let w = (x2 - x1) as u16;
|
|
//let color: ItemPalette = track.color.dark.into();
|
|
//let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0))));
|
|
//let cells = Map::new(scenes, move|(_, scene, y1, y2), s| {
|
|
//let h = (y2 - y1) as u16;
|
|
//let color = scene.color;
|
|
//let (name, fg, bg) = if let Some(c) = &scene.clips[t] {
|
|
//let c = c.read().unwrap();
|
|
//(c.name.to_string(), c.color.lightest.rgb, c.color.base.rgb)
|
|
//} else {
|
|
//("⏹ ".to_string(), TuiTheme::g(64), TuiTheme::g(32))
|
|
//};
|
|
//let last = last_color.read().unwrap().clone();
|
|
//let active = editing && selected_scene == Some(s) && selected_track == Some(t);
|
|
//let editor = Thunk::new(||&self.editor);
|
|
//let cell = Thunk::new(move||phat_sel_3(
|
|
//selected_track == Some(t) && selected_scene == Some(s),
|
|
//Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))),
|
|
//Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))),
|
|
//if selected_track == Some(t) && selected_scene.map(|s|s+1) == Some(s) {
|
|
//None
|
|
//} else {
|
|
//Some(bg.into())
|
|
//},
|
|
//bg.into(),
|
|
//bg.into(),
|
|
//));
|
|
//let cell = Either(active, editor, cell);
|
|
//*last_color.write().unwrap() = bg.into();
|
|
//map_south(
|
|
//y1 as u16,
|
|
//h + 1,
|
|
//Fill::x(Fixed::y(h + 1, cell))
|
|
//)
|
|
//});
|
|
//let column = Fixed::x(w, Tui::bg(Color::Reset, Align::y(cells)).boxed());
|
|
//Fixed::x(w, map_east(x1 as u16, w, column))
|
|
//}))).boxed()).into()
|
|
//}
|
|
//fn cell_clip <'a> (
|
|
//scene: &'a ArrangerScene, index: usize, track: &'a ArrangerTrack, w: u16, h: u16
|
|
//) -> impl Content<TuiOut> + use<'a> {
|
|
//scene.clips.get(index).map(|clip|clip.as_ref().map(|clip|{
|
|
//let clip = clip.read().unwrap();
|
|
//let mut bg = TuiTheme::border_bg();
|
|
//let name = clip.name.to_string();
|
|
//let max_w = name.len().min((w as usize).saturating_sub(2));
|
|
//let color = clip.color;
|
|
//bg = color.dark.rgb;
|
|
//if let Some((_, Some(ref playing))) = track.player.play_clip() {
|
|
//if *playing.read().unwrap() == *clip {
|
|
//bg = color.light.rgb
|
|
//}
|
|
//};
|
|
//Fixed::xy(w, h, &Tui::bg(bg, Push::x(1, Fixed::x(w, &name.as_str()[0..max_w]))));
|
|
//}))
|
|
//}
|
|
|
|
//fn track_column_separators <'a> (&'a self) -> impl Content<TuiOut> + 'a {
|
|
//let scenes_w = 16;//.max(SCENES_W_OFFSET + ArrangerScene::longest_name(&self.scenes) as u16);
|
|
//let fg = Color::Rgb(64,64,64);
|
|
//Map::new(move||self.tracks_with_sizes(), move|(_n, _track, x1, x2), _i|{
|
|
//Push::x(scenes_w, map_east(x1 as u16, (x2 - x1) as u16,
|
|
//Fixed::x((x2 - x1) as u16, Tui::fg(fg, RepeatV(&"·")))))
|
|
//})
|
|
//}
|
|
|
|
//pub fn track_widths (tracks: &[ArrangerTrack]) -> Vec<(usize, usize)> {
|
|
//let mut widths = vec![];
|
|
//let mut total = 0;
|
|
//for track in tracks.iter() {
|
|
//let width = track.width;
|
|
//widths.push((width, total));
|
|
//total += width;
|
|
//}
|
|
//widths.push((0, total));
|
|
//widths
|
|
//}
|
|
|
|
//fn scene_row_sep <'a> (&'a self) -> impl Content<TuiOut> + 'a {
|
|
//let fg = Color::Rgb(255,255,255);
|
|
//Map::new(move||self.scenes_with_sizes(1), |_, _|"")
|
|
////Map(||rows.iter(), |(_n, _scene, y1, _y2), _i| {
|
|
////let y = to.area().y() + (y / PPQ) as u16 + 1;
|
|
////if y >= to.buffer.area.height { break }
|
|
////for x in to.area().x()..to.area().x2().saturating_sub(2) {
|
|
//////if x < to.buffer.area.x && y < to.buffer.area.y {
|
|
////if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((x, y))) {
|
|
////cell.modifier = Modifier::UNDERLINED;
|
|
////cell.underline_color = fg;
|
|
////}
|
|
//////}
|
|
////}
|
|
////})
|
|
//}
|
|
|
|
//pub fn scene_heights (scenes: &[ArrangerScene], factor: usize) -> Vec<(usize, usize)> {
|
|
//let mut total = 0;
|
|
//if factor == 0 {
|
|
//scenes.iter().map(|scene|{
|
|
//let pulses = scene.pulses().max(PPQ);
|
|
//total += pulses;
|
|
//(pulses, total - pulses)
|
|
//}).collect()
|
|
//} else {
|
|
//(0..=scenes.len()).map(|i|{
|
|
//(factor*PPQ, factor*PPQ*i)
|
|
//}).collect()
|
|
//}
|
|
//}
|
|
//fn cursor (&self) -> impl Content<TuiOut> + '_ {
|
|
//let color = self.color;
|
|
//let bg = color.lighter.rgb;//Color::Rgb(0, 255, 0);
|
|
//let selected = self.selected();
|
|
//let cols = Arranger::track_widths(&self.tracks);
|
|
//let rows = Arranger::scene_heights(&self.scenes, 1);
|
|
//let scenes_w = 16.max(SCENES_W_OFFSET + ArrangerScene::longest_name(&self.scenes) as u16);
|
|
//let focused = true;
|
|
//let reticle = Reticle(Style {
|
|
//fg: Some(self.color.lighter.rgb),
|
|
//bg: None,
|
|
//underline_color: None,
|
|
//add_modifier: Modifier::empty(),
|
|
//sub_modifier: Modifier::DIM
|
|
//});
|
|
//RenderThunk::new(move|to: &mut TuiOut|{
|
|
//let area = to.area();
|
|
//let [x, y, w, h] = area.xywh();
|
|
//let mut track_area: Option<[u16;4]> = match selected {
|
|
//ArrangerSelection::Track(t) | ArrangerSelection::Clip(t, _) => Some([
|
|
//x + scenes_w + cols[t].1 as u16, y,
|
|
//cols[t].0 as u16, h,
|
|
//]),
|
|
//_ => None
|
|
//};
|
|
//let mut scene_area: Option<[u16;4]> = match selected {
|
|
//ArrangerSelection::Scene(s) | ArrangerSelection::Clip(_, s) => Some([
|
|
//x, y + HEADER_H + (rows[s].1 / PPQ) as u16,
|
|
//w, (rows[s].0 / PPQ) as u16
|
|
//]),
|
|
//_ => None
|
|
//};
|
|
//let mut clip_area: Option<[u16;4]> = match selected {
|
|
//ArrangerSelection::Clip(t, s) => Some([
|
|
//(scenes_w + x + cols[t].1 as u16).saturating_sub(1),
|
|
//HEADER_H + y + (rows[s].1/PPQ) as u16,
|
|
//cols[t].0 as u16 + 2,
|
|
//(rows[s].0 / PPQ) as u16
|
|
//]),
|
|
//_ => None
|
|
//};
|
|
//if let Some([x, y, width, height]) = track_area {
|
|
//to.fill_fg([x, y, 1, height], bg);
|
|
//to.fill_fg([x + width, y, 1, height], bg);
|
|
//}
|
|
//if let Some([_, y, _, height]) = scene_area {
|
|
//to.fill_ul([x, y - 1, w, 1], bg);
|
|
//to.fill_ul([x, y + height - 1, w, 1], bg);
|
|
//}
|
|
//if focused {
|
|
//to.place(if let Some(clip_area) = clip_area {
|
|
//clip_area
|
|
//} else if let Some(track_area) = track_area {
|
|
//track_area.clip_h(HEADER_H)
|
|
//} else if let Some(scene_area) = scene_area {
|
|
//scene_area.clip_w(scenes_w)
|
|
//} else {
|
|
//area.clip_w(scenes_w).clip_h(HEADER_H)
|
|
//}, &reticle)
|
|
//};
|
|
//})
|
|
//}
|
|
|
|
///// A 1-row cell.
|
|
//fn cell <T: Content<TuiOut>> (color: ItemPalette, field: T) -> impl Content<TuiOut> {
|
|
//Tui::fg_bg(color.lightest.rgb, color.base.rgb, Fixed::y(1, field))
|
|
//}
|
|
//}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//handle!(TuiIn: |self: Sequencer, input|SequencerCommand::execute_with_state(self, input.event()));
|
|
//handle!(TuiIn: |self: Groovebox, input|GrooveboxCommand::execute_with_state(self, input.event()));
|
|
//handle!(TuiIn: |self: Arranger, input|ArrangerCommand::execute_with_state(self, input.event()));
|
|
//use SequencerCommand as SeqCmd;
|
|
//use GrooveboxCommand as GrvCmd;
|
|
//use ArrangerCommand as ArrCmd;
|
|
//#[derive(Clone, Debug)] pub enum SequencerCommand {
|
|
//Compact(bool),
|
|
//History(isize),
|
|
//Clock(ClockCommand),
|
|
//Pool(PoolCommand),
|
|
//Editor(MidiEditCommand),
|
|
//Enqueue(Option<Arc<RwLock<MidiClip>>>),
|
|
//}
|
|
//#[derive(Clone, Debug)] pub enum GrooveboxCommand {
|
|
//Compact(bool),
|
|
//History(isize),
|
|
//Clock(ClockCommand),
|
|
//Pool(PoolCommand),
|
|
//Editor(MidiEditCommand),
|
|
//Enqueue(Option<Arc<RwLock<MidiClip>>>),
|
|
//Sampler(SamplerCommand),
|
|
//}
|
|
//#[derive(Clone, Debug)] pub enum ArrangerCommand {
|
|
//History(isize),
|
|
//Color(ItemPalette),
|
|
//Clock(ClockCommand),
|
|
//Scene(SceneCommand),
|
|
//Track(TrackCommand),
|
|
//Clip(ClipCommand),
|
|
//Select(ArrangerSelection),
|
|
//Zoom(usize),
|
|
//Pool(PoolCommand),
|
|
//Editor(MidiEditCommand),
|
|
//StopAll,
|
|
//Clear,
|
|
//}
|
|
|
|
//command!(|self: SequencerCommand, state: Sequencer|match self {
|
|
//Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?,
|
|
//Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?,
|
|
//Self::Enqueue(clip) => { state.player.enqueue_next(clip.as_ref()); None },
|
|
//Self::History(delta) => { todo!("undo/redo") },
|
|
|
|
//Self::Pool(cmd) => match cmd {
|
|
//// autoselect: automatically load selected clip in editor
|
|
//PoolCommand::Select(_) => {
|
|
//let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
|
//state.editor.set_clip(state.pool.clip().as_ref());
|
|
//undo
|
|
//},
|
|
//// update color in all places simultaneously
|
|
//PoolCommand::Clip(PoolCmd::SetColor(index, _)) => {
|
|
//let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
|
//state.editor.set_clip(state.pool.clip().as_ref());
|
|
//undo
|
|
//},
|
|
//_ => cmd.delegate(&mut state.pool, Self::Pool)?
|
|
//},
|
|
//Self::Compact(compact) => if state.compact != compact {
|
|
//state.compact = compact;
|
|
//Some(Self::Compact(!compact))
|
|
//} else {
|
|
//None
|
|
//},
|
|
//});
|
|
//command!(|self: GrooveboxCommand, state: Groovebox|match self {
|
|
//Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?,
|
|
//Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?,
|
|
//Self::Enqueue(clip) => { state.player.enqueue_next(clip.as_ref()); None },
|
|
//Self::History(delta) => { todo!("undo/redo") },
|
|
//Self::Sampler(cmd) => cmd.delegate(&mut state.sampler, Self::Sampler)?,
|
|
|
|
//Self::Pool(cmd) => match cmd {
|
|
//// autoselect: automatically load selected clip in editor
|
|
//PoolCommand::Select(_) => {
|
|
//let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
|
//state.editor.set_clip(state.pool.clip().as_ref());
|
|
//undo
|
|
//},
|
|
//// update color in all places simultaneously
|
|
//PoolCommand::Clip(PoolCmd::SetColor(index, _)) => {
|
|
//let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
|
//state.editor.set_clip(state.pool.clip().as_ref());
|
|
//undo
|
|
//},
|
|
//_ => cmd.delegate(&mut state.pool, Self::Pool)?
|
|
//},
|
|
//Self::Compact(compact) => if state.compact != compact {
|
|
//state.compact = compact;
|
|
//Some(Self::Compact(!compact))
|
|
//} else {
|
|
//None
|
|
//},
|
|
//});
|
|
//command!(|self: ArrangerCommand, state: Arranger|match self {
|
|
//Self::Clear => { todo!() },
|
|
//Self::Clip(cmd) => cmd.delegate(state, Self::Clip)?,
|
|
//Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?,
|
|
//Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?,
|
|
//Self::History(_) => { todo!() },
|
|
//Self::Scene(cmd) => cmd.delegate(state, Self::Scene)?,
|
|
//Self::Select(s) => { state.selected = s; None },
|
|
//Self::Track(cmd) => cmd.delegate(state, Self::Track)?,
|
|
//Self::Zoom(_) => { todo!(); },
|
|
|
|
//Self::StopAll => {
|
|
//for track in 0..state.tracks.len() { state.tracks[track].player.enqueue_next(None); }
|
|
//None
|
|
//},
|
|
//Self::Color(palette) => {
|
|
//let old = state.color;
|
|
//state.color = palette;
|
|
//Some(Self::Color(old))
|
|
//},
|
|
//Self::Pool(cmd) => {
|
|
//match cmd {
|
|
//// autoselect: automatically load selected clip in editor
|
|
//PoolCommand::Select(_) => {
|
|
//let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
|
//state.editor.set_clip(state.pool.clip().as_ref());
|
|
//undo
|
|
//},
|
|
//// reload clip in editor to update color
|
|
//PoolCommand::Clip(PoolClipCommand::SetColor(index, _)) => {
|
|
//let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
|
//state.editor.set_clip(state.pool.clip().as_ref());
|
|
//undo
|
|
//},
|
|
//_ => cmd.delegate(&mut state.pool, Self::Pool)?
|
|
//}
|
|
//},
|
|
//});
|
|
//command!(|self: SceneCommand, state: Arranger|match self {
|
|
//Self::Add => { state.scene_add(None, None)?; None }
|
|
//Self::Del(index) => { state.scene_del(index); None },
|
|
//Self::SetColor(index, color) => {
|
|
//let old = state.scenes[index].color;
|
|
//state.scenes[index].color = color;
|
|
//Some(Self::SetColor(index, old))
|
|
//},
|
|
//Self::Enqueue(scene) => {
|
|
//for track in 0..state.tracks.len() {
|
|
//state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref());
|
|
//}
|
|
//None
|
|
//},
|
|
//_ => None
|
|
//});
|
|
//command!(|self: TrackCommand, state: Arranger|match self {
|
|
//Self::Add => { state.track_add(None, None)?; None },
|
|
//Self::Del(index) => { state.track_del(index); None },
|
|
//Self::Stop(track) => { state.tracks[track].player.enqueue_next(None); None },
|
|
//Self::SetColor(index, color) => {
|
|
//let old = state.tracks[index].color;
|
|
//state.tracks[index].color = color;
|
|
//Some(Self::SetColor(index, old))
|
|
//},
|
|
//_ => None
|
|
//});
|
|
//command!(|self: ClipCommand, state: Arranger|match self {
|
|
//Self::Get(track, scene) => { todo!() },
|
|
//Self::Put(track, scene, clip) => {
|
|
//let old = state.scenes[scene].clips[track].clone();
|
|
//state.scenes[scene].clips[track] = clip;
|
|
//Some(Self::Put(track, scene, old))
|
|
//},
|
|
//Self::Enqueue(track, scene) => {
|
|
//state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref());
|
|
//None
|
|
//},
|
|
//_ => None
|
|
//});
|
|
//keymap!(KEYS_SEQUENCER = |state: Sequencer, input: Event| SequencerCommand {
|
|
//// TODO: k: toggle on-screen keyboard
|
|
//ctrl(key(Char('k'))) => { todo!("keyboard") },
|
|
//// Transport: Play/pause
|
|
//key(Char(' ')) => SeqCmd::Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }),
|
|
//// Transport: Play from start or rewind to start
|
|
//shift(key(Char(' '))) => SeqCmd::Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }),
|
|
//// u: undo
|
|
//key(Char('u')) => SeqCmd::History(-1),
|
|
//// Shift-U: redo
|
|
//key(Char('U')) => SeqCmd::History( 1),
|
|
//// Tab: Toggle compact mode
|
|
//key(Tab) => SeqCmd::Compact(!state.compact),
|
|
//// q: Enqueue currently edited clip
|
|
//key(Char('q')) => SeqCmd::Enqueue(state.pool.clip().clone()),
|
|
//// 0: Enqueue clip 0 (stop all)
|
|
//key(Char('0')) => SeqCmd::Enqueue(Some(state.clips()[0].clone())),
|
|
//// e: Toggle between editing currently playing or other clip
|
|
////key(Char('e')) => if let Some((_, Some(playing))) = state.player.play_clip() {
|
|
////let editing = state.editor.clip().as_ref().map(|p|p.read().unwrap().clone());
|
|
////let selected = state.pool.clip().clone();
|
|
////SeqCmd::Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing {
|
|
////selected
|
|
////} else {
|
|
////playing.clone()
|
|
////})))
|
|
////} else {
|
|
////return None
|
|
////}
|
|
//}, if let Some(command) = MidiEditCommand::input_to_command(&state.editor, input) {
|
|
//SeqCmd::Editor(command)
|
|
//} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) {
|
|
//SeqCmd::Pool(command)
|
|
//} else {
|
|
//return None
|
|
//});
|
|
//keymap!(<'a> KEYS_GROOVEBOX = |state: Groovebox, input: Event| GrooveboxCommand {
|
|
//// Tab: Toggle compact mode
|
|
//key(Tab) => GrvCmd::Compact(!state.compact),
|
|
//// q: Enqueue currently edited clip
|
|
//key(Char('q')) => GrvCmd::Enqueue(state.pool.clip().clone()),
|
|
//// 0: Enqueue clip 0 (stop all)
|
|
//key(Char('0')) => GrvCmd::Enqueue(Some(state.pool.clips()[0].clone())),
|
|
//// TODO: k: toggle on-screen keyboard
|
|
//ctrl(key(Char('k'))) => todo!("keyboard"),
|
|
//// Transport: Play from start or rewind to start
|
|
//ctrl(key(Char(' '))) => GrvCmd::Clock(
|
|
//if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }
|
|
//),
|
|
//// Shift-R: toggle recording
|
|
//shift(key(Char('R'))) => GrvCmd::Sampler(if state.sampler.recording.is_some() {
|
|
//SmplCmd::RecordFinish
|
|
//} else {
|
|
//SmplCmd::RecordBegin(u7::from(state.editor.note_point() as u8))
|
|
//}),
|
|
//// Shift-Del: delete sample
|
|
//shift(key(Delete)) => GrvCmd::Sampler(
|
|
//SmplCmd::SetSample(u7::from(state.editor.note_point() as u8), None)
|
|
//),
|
|
//// e: Toggle between editing currently playing or other clip
|
|
////shift(key(Char('e'))) => if let Some((_, Some(playing))) = state.player.play_clip() {
|
|
////let editing = state.editor.clip().as_ref().map(|p|p.read().unwrap().clone());
|
|
////let selected = state.pool.clip().clone().map(|s|s.read().unwrap().clone());
|
|
////GrvCmd::Editor(Show(if selected != editing {
|
|
////selected
|
|
////} else {
|
|
////Some(playing.clone())
|
|
////}))
|
|
////} else {
|
|
////return None
|
|
////},
|
|
//}, if let Some(command) = MidiEditCommand::input_to_command(&state.editor, input) {
|
|
//GrvCmd::Editor(command)
|
|
//} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) {
|
|
//GrvCmd::Pool(command)
|
|
//} else {
|
|
//return None
|
|
//});
|
|
//keymap!(KEYS_ARRANGER = |state: Arranger, input: Event| ArrangerCommand {
|
|
//key(Char('u')) => ArrCmd::History(-1),
|
|
//key(Char('U')) => ArrCmd::History(1),
|
|
//// TODO: k: toggle on-screen keyboard
|
|
//ctrl(key(Char('k'))) => { todo!("keyboard") },
|
|
//// Transport: Play/pause
|
|
//key(Char(' ')) => ArrCmd::Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }),
|
|
//// Transport: Play from start or rewind to start
|
|
//shift(key(Char(' '))) => ArrCmd::Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }),
|
|
//key(Char('e')) => ArrCmd::Editor(MidiEditCommand::Show(state.pool.clip().clone())),
|
|
//ctrl(key(Char('a'))) => ArrCmd::Scene(SceneCommand::Add),
|
|
//ctrl(key(Char('A'))) => return None,//ArrCmd::Scene(SceneCommand::Add),
|
|
//ctrl(key(Char('t'))) => ArrCmd::Track(TrackCommand::Add),
|
|
//// Tab: Toggle visibility of clip pool column
|
|
//key(Tab) => ArrCmd::Pool(PoolCommand::Show(!state.pool.visible)),
|
|
//}, {
|
|
//use ArrangerSelection as Selected;
|
|
//use SceneCommand as Scene;
|
|
//use TrackCommand as Track;
|
|
//use ClipCommand as Clip;
|
|
//let t_len = state.tracks.len();
|
|
//let s_len = state.scenes.len();
|
|
//match state.selected {
|
|
//Selected::Clip(t, s) => clip_keymap(state, input, t, s),
|
|
//Selected::Scene(s) => scene_keymap(state, input, s),
|
|
//Selected::Track(t) => track_keymap(state, input, t),
|
|
//Selected::Mix => match input {
|
|
|
|
//kpat!(Delete) => Some(ArrCmd::Clear),
|
|
//kpat!(Char('0')) => Some(ArrCmd::StopAll),
|
|
//kpat!(Char('c')) => Some(ArrCmd::Color(ItemPalette::random())),
|
|
|
|
//kpat!(Up) => return None,
|
|
//kpat!(Down) => Some(ArrCmd::Select(Selected::Scene(0))),
|
|
//kpat!(Left) => return None,
|
|
//kpat!(Right) => Some(ArrCmd::Select(Selected::Track(0))),
|
|
|
|
//_ => None
|
|
//},
|
|
//}
|
|
//}.or_else(||if let Some(command) = MidiEditCommand::input_to_command(&state.editor, input) {
|
|
//Some(ArrCmd::Editor(command))
|
|
//} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) {
|
|
//Some(ArrCmd::Pool(command))
|
|
//} else {
|
|
//None
|
|
//})?);
|
|
|
|
//fn clip_keymap (state: &Arranger, input: &Event, t: usize, s: usize) -> Option<ArrangerCommand> {
|
|
//use ArrangerSelection as Selected;
|
|
//use SceneCommand as Scene;
|
|
//use TrackCommand as Track;
|
|
//use ClipCommand as Clip;
|
|
//let t_len = state.tracks.len();
|
|
//let s_len = state.scenes.len();
|
|
//Some(match input {
|
|
|
|
//kpat!(Char('g')) => ArrCmd::Pool(PoolCommand::Select(0)),
|
|
//kpat!(Char('q')) => ArrCmd::Clip(Clip::Enqueue(t, s)),
|
|
//kpat!(Char('l')) => ArrCmd::Clip(Clip::SetLoop(t, s, false)),
|
|
|
|
//kpat!(Enter) => if state.scenes[s].clips[t].is_none() {
|
|
//// FIXME: get this clip from the pool (autoregister via intmut)
|
|
//let (_, clip) = state.add_clip();
|
|
//ArrCmd::Clip(Clip::Put(t, s, Some(clip)))
|
|
//} else {
|
|
//return None
|
|
//},
|
|
//kpat!(Delete) => ArrCmd::Clip(Clip::Put(t, s, None)),
|
|
//kpat!(Char('p')) => ArrCmd::Clip(Clip::Put(t, s, state.pool.clip().clone())),
|
|
//kpat!(Char(',')) => ArrCmd::Clip(Clip::Put(t, s, None)),
|
|
//kpat!(Char('.')) => ArrCmd::Clip(Clip::Put(t, s, None)),
|
|
//kpat!(Char('<')) => ArrCmd::Clip(Clip::Put(t, s, None)),
|
|
//kpat!(Char('>')) => ArrCmd::Clip(Clip::Put(t, s, None)),
|
|
|
|
//kpat!(Up) => ArrCmd::Select(if s > 0 { Selected::Clip(t, s - 1) } else { Selected::Track(t) }),
|
|
//kpat!(Down) => ArrCmd::Select(Selected::Clip(t, (s + 1).min(s_len.saturating_sub(1)))),
|
|
//kpat!(Left) => ArrCmd::Select(if t > 0 { Selected::Clip(t - 1, s) } else { Selected::Scene(s) }),
|
|
//kpat!(Right) => ArrCmd::Select(Selected::Clip((t + 1).min(t_len.saturating_sub(1)), s)),
|
|
|
|
//_ => return None
|
|
//})
|
|
//}
|
|
//fn scene_keymap (state: &Arranger, input: &Event, s: usize) -> Option<ArrangerCommand> {
|
|
//use ArrangerSelection as Selected;
|
|
//use SceneCommand as Scene;
|
|
//use TrackCommand as Track;
|
|
//use ClipCommand as Clip;
|
|
//let t_len = state.tracks.len();
|
|
//let s_len = state.scenes.len();
|
|
//Some(match input {
|
|
|
|
//kpat!(Char(',')) => ArrCmd::Scene(Scene::Swap(s, s - 1)),
|
|
//kpat!(Char('.')) => ArrCmd::Scene(Scene::Swap(s, s + 1)),
|
|
//kpat!(Char('<')) => ArrCmd::Scene(Scene::Swap(s, s - 1)),
|
|
//kpat!(Char('>')) => ArrCmd::Scene(Scene::Swap(s, s + 1)),
|
|
//kpat!(Char('q')) => ArrCmd::Scene(Scene::Enqueue(s)),
|
|
//kpat!(Delete) => ArrCmd::Scene(Scene::Del(s)),
|
|
//kpat!(Char('c')) => ArrCmd::Scene(Scene::SetColor(s, ItemPalette::random())),
|
|
|
|
//kpat!(Up) => ArrCmd::Select(if s > 0 { Selected::Scene(s - 1) } else { Selected::Mix }),
|
|
//kpat!(Down) => ArrCmd::Select(Selected::Scene((s + 1).min(s_len.saturating_sub(1)))),
|
|
//kpat!(Left) => return None,
|
|
//kpat!(Right) => ArrCmd::Select(Selected::Clip(0, s)),
|
|
|
|
//_ => return None
|
|
//})
|
|
//}
|
|
//fn track_keymap (state: &Arranger, input: &Event, t: usize) -> Option<ArrangerCommand> {
|
|
//use ArrangerSelection as Selected;
|
|
//use SceneCommand as Scene;
|
|
//use TrackCommand as Track;
|
|
//use ClipCommand as Clip;
|
|
//let t_len = state.tracks.len();
|
|
//let s_len = state.scenes.len();
|
|
//Some(match input {
|
|
|
|
//kpat!(Char(',')) => ArrCmd::Track(Track::Swap(t, t - 1)),
|
|
//kpat!(Char('.')) => ArrCmd::Track(Track::Swap(t, t + 1)),
|
|
//kpat!(Char('<')) => ArrCmd::Track(Track::Swap(t, t - 1)),
|
|
//kpat!(Char('>')) => ArrCmd::Track(Track::Swap(t, t + 1)),
|
|
//kpat!(Delete) => ArrCmd::Track(Track::Del(t)),
|
|
//kpat!(Char('c')) => ArrCmd::Track(Track::SetColor(t, ItemPalette::random())),
|
|
|
|
//kpat!(Up) => return None,
|
|
//kpat!(Down) => ArrCmd::Select(Selected::Clip(t, 0)),
|
|
//kpat!(Left) => ArrCmd::Select(if t > 0 { Selected::Track(t - 1) } else { Selected::Mix }),
|
|
//kpat!(Right) => ArrCmd::Select(Selected::Track((t + 1).min(t_len.saturating_sub(1)))),
|
|
|
|
//_ => return None
|
|
//})
|
|
//}
|
|
//impl HasJack for Arranger {
|
|
//fn jack (&self) -> &Arc<RwLock<JackConnection>> { &self.jack }
|
|
//}
|
|
|
|
//audio!(|self: Sequencer, client, scope|{
|
|
//// Start profiling cycle
|
|
//let t0 = self.perf.get_t0();
|
|
|
|
//// Update transport clock
|
|
//if Control::Quit == ClockAudio(self).process(client, scope) {
|
|
//return Control::Quit
|
|
//}
|
|
//// Update MIDI sequencer
|
|
//if Control::Quit == PlayerAudio(
|
|
//&mut self.player, &mut self.note_buf, &mut self.midi_buf
|
|
//).process(client, scope) {
|
|
//return Control::Quit
|
|
//}
|
|
|
|
//// End profiling cycle
|
|
//self.perf.update(t0, scope);
|
|
|
|
//Control::Continue
|
|
//});
|
|
|
|
//audio!(|self: Groovebox, client, scope|{
|
|
//// Start profiling cycle
|
|
//let t0 = self.perf.get_t0();
|
|
|
|
//// Update transport clock
|
|
//if Control::Quit == ClockAudio(&mut self.player).process(client, scope) {
|
|
//return Control::Quit
|
|
//}
|
|
|
|
//// Update MIDI sequencer
|
|
//if Control::Quit == PlayerAudio(
|
|
//&mut self.player, &mut self.note_buf, &mut self.midi_buf
|
|
//).process(client, scope) {
|
|
//return Control::Quit
|
|
//}
|
|
|
|
//// Update sampler
|
|
//if Control::Quit == SamplerAudio(&mut self.sampler).process(client, scope) {
|
|
//return Control::Quit
|
|
//}
|
|
|
|
//// TODO move these to editor and sampler:
|
|
//for RawMidi { time, bytes } in self.player.midi_ins[0].port.iter(scope) {
|
|
//if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() {
|
|
//match message {
|
|
//MidiMessage::NoteOn { ref key, .. } => {
|
|
//self.editor.set_note_point(key.as_int() as usize);
|
|
//},
|
|
//MidiMessage::Controller { controller, value } => {
|
|
//if let Some(sample) = &self.sampler.mapped[self.editor.note_point()] {
|
|
//sample.write().unwrap().handle_cc(controller, value)
|
|
//}
|
|
//}
|
|
//_ => {}
|
|
//}
|
|
//}
|
|
//}
|
|
|
|
//// End profiling cycle
|
|
//self.perf.update(t0, scope);
|
|
|
|
//Control::Continue
|
|
//});
|
|
|
|
//audio!(|self: Arranger, client, scope|{
|
|
//// Start profiling cycle
|
|
//let t0 = self.perf.get_t0();
|
|
|
|
//// Update transport clock
|
|
//if Control::Quit == ClockAudio(self).process(client, scope) {
|
|
//return Control::Quit
|
|
//}
|
|
|
|
////// Update MIDI sequencers
|
|
////let tracks = &mut self.tracks;
|
|
////let note_buf = &mut self.note_buf;
|
|
////let midi_buf = &mut self.midi_buf;
|
|
////if Control::Quit == TracksAudio(tracks, note_buf, midi_buf).process(client, scope) {
|
|
////return Control::Quit
|
|
////}
|
|
|
|
//// FIXME: one of these per playing track
|
|
////self.now.set(0.);
|
|
////if let ArrangerSelection::Clip(t, s) = self.selected {
|
|
////let clip = self.scenes.get(s).map(|scene|scene.clips.get(t));
|
|
////if let Some(Some(Some(clip))) = clip {
|
|
////if let Some(track) = self.tracks().get(t) {
|
|
////if let Some((ref started_at, Some(ref playing))) = track.player.play_clip {
|
|
////let clip = clip.read().unwrap();
|
|
////if *playing.read().unwrap() == *clip {
|
|
////let pulse = self.current().pulse.get();
|
|
////let start = started_at.pulse.get();
|
|
////let now = (pulse - start) % clip.length as f64;
|
|
////self.now.set(now);
|
|
////}
|
|
////}
|
|
////}
|
|
////}
|
|
////}
|
|
|
|
//// End profiling cycle
|
|
//self.perf.update(t0, scope);
|
|
//return Control::Continue
|
|
//});
|
|
//fn get_device_mut (&self, i: usize) -> Option<RwLockWriteGuard<Box<dyn AudioComponent<E>>>> {
|
|
//self.devices.get(i).map(|d|d.state.write().unwrap())
|
|
//}
|
|
//pub fn device_mut (&self) -> Option<RwLockWriteGuard<Box<dyn AudioComponent<E>>>> {
|
|
//self.get_device_mut(self.device)
|
|
//}
|
|
///// Add a device to the end of the chain.
|
|
//pub fn append_device (&mut self, device: JackDevice<E>) -> Usually<&mut JackDevice<E>> {
|
|
//self.devices.push(device);
|
|
//let index = self.devices.len() - 1;
|
|
//Ok(&mut self.devices[index])
|
|
//}
|
|
//pub fn add_device (&mut self, device: JackDevice<E>) {
|
|
//self.devices.push(device);
|
|
//}
|
|
//pub fn connect_first_device (&self) -> Usually<()> {
|
|
//if let (Some(port), Some(device)) = (&self.midi_out, self.devices.get(0)) {
|
|
//device.client.as_client().connect_ports(&port, &device.midi_ins()?[0])?;
|
|
//}
|
|
//Ok(())
|
|
//}
|
|
//pub fn connect_last_device (&self, app: &Track) -> Usually<()> {
|
|
//Ok(match self.devices.get(self.devices.len().saturating_sub(1)) {
|
|
//Some(device) => {
|
|
//app.audio_out(0).map(|left|device.connect_audio_out(0, &left)).transpose()?;
|
|
//app.audio_out(1).map(|right|device.connect_audio_out(1, &right)).transpose()?;
|
|
//()
|
|
//},
|
|
//None => ()
|
|
//})
|
|
//}
|
|
use crate::*;
|
|
impl MixerTrack {
|
|
pub fn new (name: &str) -> Usually<Self> {
|
|
Ok(Self {
|
|
name: name.to_string().into(),
|
|
audio_ins: vec![],
|
|
audio_outs: vec![],
|
|
devices: vec![],
|
|
//ports: JackPorts::default(),
|
|
//devices: vec![],
|
|
//device: 0,
|
|
})
|
|
}
|
|
}
|
|
|
|
pub struct TrackView<'a> {
|
|
pub chain: Option<&'a MixerTrack>,
|
|
pub direction: Direction,
|
|
pub focused: bool,
|
|
pub entered: bool,
|
|
}
|
|
|
|
impl<'a> Content<TuiOut> for TrackView<'a> {
|
|
fn render (&self, to: &mut TuiOut) {
|
|
todo!();
|
|
//let mut area = to.area();
|
|
//if let Some(chain) = self.chain {
|
|
//match self.direction {
|
|
//Direction::Down => area.width = area.width.min(40),
|
|
//Direction::Right => area.width = area.width.min(10),
|
|
//_ => { unimplemented!() },
|
|
//}
|
|
//to.fill_bg(to.area(), Nord::bg_lo(self.focused, self.entered));
|
|
//let mut split = Stack::new(self.direction);
|
|
//for device in chain.devices.as_slice().iter() {
|
|
//split = split.add_ref(device);
|
|
//}
|
|
//let (area, areas) = split.render_areas(to)?;
|
|
//if self.focused && self.entered && areas.len() > 0 {
|
|
//Corners(Style::default().green().not_dim()).draw(to.with_rect(areas[0]))?;
|
|
//}
|
|
//Ok(Some(area))
|
|
//} else {
|
|
//let [x, y, width, height] = area;
|
|
//let label = "No chain selected";
|
|
//let x = x + (width - label.len() as u16) / 2;
|
|
//let y = y + height / 2;
|
|
//to.blit(&label, x, y, Some(Style::default().dim().bold()))?;
|
|
//Ok(Some(area))
|
|
//}
|
|
}
|
|
}
|
|
|
|
//impl Content<TuiOut> for Mixer<Tui> {
|
|
//fn content (&self) -> impl Content<TuiOut> {
|
|
//Stack::right(|add| {
|
|
//for channel in self.tracks.iter() {
|
|
//add(channel)?;
|
|
//}
|
|
//Ok(())
|
|
//})
|
|
//}
|
|
//}
|
|
|
|
//impl Content<TuiOut> for Track<Tui> {
|
|
//fn content (&self) -> impl Content<TuiOut> {
|
|
//TrackView {
|
|
//chain: Some(&self),
|
|
//direction: tek_core::Direction::Right,
|
|
//focused: true,
|
|
//entered: true,
|
|
////pub channels: u8,
|
|
////pub input_ports: Vec<Port<AudioIn>>,
|
|
////pub pre_gain_meter: f64,
|
|
////pub gain: f64,
|
|
////pub insert_ports: Vec<Port<AudioOut>>,
|
|
////pub return_ports: Vec<Port<AudioIn>>,
|
|
////pub post_gain_meter: f64,
|
|
////pub post_insert_meter: f64,
|
|
////pub level: f64,
|
|
////pub pan: f64,
|
|
////pub output_ports: Vec<Port<AudioOut>>,
|
|
////pub post_fader_meter: f64,
|
|
////pub route: String,
|
|
//}
|
|
//}
|
|
//}
|
|
|
|
handle!(TuiIn: |self: Mixer, engine|{
|
|
if let crossterm::event::Event::Key(event) = engine.event() {
|
|
|
|
match event.code {
|
|
//KeyCode::Char('c') => {
|
|
//if event.modifiers == KeyModifiers::CONTROL {
|
|
//self.exit();
|
|
//}
|
|
//},
|
|
KeyCode::Down => {
|
|
self.selected_track = (self.selected_track + 1) % self.tracks.len();
|
|
println!("{}", self.selected_track);
|
|
return Ok(Some(true))
|
|
},
|
|
KeyCode::Up => {
|
|
if self.selected_track == 0 {
|
|
self.selected_track = self.tracks.len() - 1;
|
|
} else {
|
|
self.selected_track -= 1;
|
|
}
|
|
println!("{}", self.selected_track);
|
|
return Ok(Some(true))
|
|
},
|
|
KeyCode::Left => {
|
|
if self.selected_column == 0 {
|
|
self.selected_column = 6
|
|
} else {
|
|
self.selected_column -= 1;
|
|
}
|
|
return Ok(Some(true))
|
|
},
|
|
KeyCode::Right => {
|
|
if self.selected_column == 6 {
|
|
self.selected_column = 0
|
|
} else {
|
|
self.selected_column += 1;
|
|
}
|
|
return Ok(Some(true))
|
|
},
|
|
_ => {
|
|
println!("\n{event:?}");
|
|
}
|
|
}
|
|
|
|
}
|
|
Ok(None)
|
|
});
|
|
|
|
handle!(TuiIn: |self:MixerTrack,from|{
|
|
match from.event() {
|
|
//, NONE, "chain_cursor_up", "move cursor up", || {
|
|
kpat!(KeyCode::Up) => {
|
|
Ok(Some(true))
|
|
},
|
|
// , NONE, "chain_cursor_down", "move cursor down", || {
|
|
kpat!(KeyCode::Down) => {
|
|
Ok(Some(true))
|
|
},
|
|
// Left, NONE, "chain_cursor_left", "move cursor left", || {
|
|
kpat!(KeyCode::Left) => {
|
|
//if let Some(track) = app.arranger.track_mut() {
|
|
//track.device = track.device.saturating_sub(1);
|
|
//return Ok(true)
|
|
//}
|
|
Ok(Some(true))
|
|
},
|
|
// , NONE, "chain_cursor_right", "move cursor right", || {
|
|
kpat!(KeyCode::Right) => {
|
|
//if let Some(track) = app.arranger.track_mut() {
|
|
//track.device = (track.device + 1).min(track.devices.len().saturating_sub(1));
|
|
//return Ok(true)
|
|
//}
|
|
Ok(Some(true))
|
|
},
|
|
// , NONE, "chain_mode_switch", "switch the display mode", || {
|
|
kpat!(KeyCode::Char('`')) => {
|
|
//app.chain_mode = !app.chain_mode;
|
|
Ok(Some(true))
|
|
},
|
|
_ => Ok(None)
|
|
}
|
|
});
|
|
|
|
pub enum MixerTrackCommand {}
|
|
|
|
//impl MixerTrackDevice for LV2Plugin {}
|
|
|
|
pub trait MixerTrackDevice: Debug + Send + Sync {
|
|
fn boxed (self) -> Box<dyn MixerTrackDevice> where Self: Sized + 'static {
|
|
Box::new(self)
|
|
}
|
|
}
|
|
|
|
impl MixerTrackDevice for Sampler {}
|
|
|
|
impl MixerTrackDevice for Plugin {}
|
|
|
|
const SYM_NAME: &str = ":name";
|
|
const SYM_GAIN: &str = ":gain";
|
|
const SYM_SAMPLER: &str = "sampler";
|
|
const SYM_LV2: &str = "lv2";
|
|
|
|
from_edn!("mixer/track" => |jack: &Arc<RwLock<JackConnection>>, args| -> MixerTrack {
|
|
let mut _gain = 0.0f64;
|
|
let mut track = MixerTrack {
|
|
name: "".into(),
|
|
audio_ins: vec![],
|
|
audio_outs: vec![],
|
|
devices: vec![],
|
|
};
|
|
edn!(edn in args {
|
|
Edn::Map(map) => {
|
|
if let Some(Edn::Str(n)) = map.get(&Edn::Key(SYM_NAME)) {
|
|
track.name = n.to_string();
|
|
}
|
|
if let Some(Edn::Double(g)) = map.get(&Edn::Key(SYM_GAIN)) {
|
|
_gain = f64::from(*g);
|
|
}
|
|
},
|
|
Edn::List(args) => match args.first() {
|
|
// Add a sampler device to the track
|
|
Some(Edn::Symbol(SYM_SAMPLER)) => {
|
|
track.devices.push(
|
|
Box::new(Sampler::from_edn(jack, &args[1..])?) as Box<dyn MixerTrackDevice>
|
|
);
|
|
panic!(
|
|
"unsupported in track {}: {:?}; tek_mixer not compiled with feature \"sampler\"",
|
|
&track.name,
|
|
args.first().unwrap()
|
|
)
|
|
},
|
|
// Add a LV2 plugin to the track.
|
|
Some(Edn::Symbol(SYM_LV2)) => {
|
|
track.devices.push(
|
|
Box::new(Plugin::from_edn(jack, &args[1..])?) as Box<dyn MixerTrackDevice>
|
|
);
|
|
panic!(
|
|
"unsupported in track {}: {:?}; tek_mixer not compiled with feature \"plugin\"",
|
|
&track.name,
|
|
args.first().unwrap()
|
|
)
|
|
},
|
|
None =>
|
|
panic!("empty list track {}", &track.name),
|
|
_ =>
|
|
panic!("unexpected in track {}: {:?}", &track.name, args.first().unwrap())
|
|
},
|
|
_ => {}
|
|
});
|
|
Ok(track)
|
|
});
|
|
|
|
//impl ArrangerScene {
|
|
|
|
////TODO
|
|
////pub fn from_edn <'a, 'e> (args: &[Edn<'e>]) -> Usually<Self> {
|
|
////let mut name = None;
|
|
////let mut clips = vec![];
|
|
////edn!(edn in args {
|
|
////Edn::Map(map) => {
|
|
////let key = map.get(&Edn::Key(":name"));
|
|
////if let Some(Edn::Str(n)) = key {
|
|
////name = Some(*n);
|
|
////} else {
|
|
////panic!("unexpected key in scene '{name:?}': {key:?}")
|
|
////}
|
|
////},
|
|
////Edn::Symbol("_") => {
|
|
////clips.push(None);
|
|
////},
|
|
////Edn::Int(i) => {
|
|
////clips.push(Some(*i as usize));
|
|
////},
|
|
////_ => panic!("unexpected in scene '{name:?}': {edn:?}")
|
|
////});
|
|
////Ok(ArrangerScene {
|
|
////name: Arc::new(name.unwrap_or("").to_string().into()),
|
|
////color: ItemColor::random(),
|
|
////clips,
|
|
////})
|
|
////}
|
|
//}
|
|
|
|
// TODO:
|
|
//keymap!(TRANSPORT_BPM_KEYS = |state: Clock, input: Event| ClockCommand {
|
|
//key(Char(',')) => SetBpm(state.bpm().get() - 1.0),
|
|
//key(Char('.')) => SetBpm(state.bpm().get() + 1.0),
|
|
//key(Char('<')) => SetBpm(state.bpm().get() - 0.001),
|
|
//key(Char('>')) => SetBpm(state.bpm().get() + 0.001),
|
|
//});
|
|
//keymap!(TRANSPORT_QUANT_KEYS = |state: Clock, input: Event| ClockCommand {
|
|
//key(Char(',')) => SetQuant(state.quant.prev()),
|
|
//key(Char('.')) => SetQuant(state.quant.next()),
|
|
//key(Char('<')) => SetQuant(state.quant.prev()),
|
|
//key(Char('>')) => SetQuant(state.quant.next()),
|
|
//});
|
|
//keymap!(TRANSPORT_SYNC_KEYS = |sync: Clock, input: Event | ClockCommand {
|
|
//key(Char(',')) => SetSync(state.sync.prev()),
|
|
//key(Char('.')) => SetSync(state.sync.next()),
|
|
//key(Char('<')) => SetSync(state.sync.prev()),
|
|
//key(Char('>')) => SetSync(state.sync.next()),
|
|
//});
|
|
//keymap!(TRANSPORT_SEEK_KEYS = |state: Clock, input: Event| ClockCommand {
|
|
//key(Char(',')) => todo!("transport seek bar"),
|
|
//key(Char('.')) => todo!("transport seek bar"),
|
|
//key(Char('<')) => todo!("transport seek beat"),
|
|
//key(Char('>')) => todo!("transport seek beat"),
|
|
//});
|
|
|
|
//#[derive(Debug)]
|
|
//pub struct MIDIPlayer {
|
|
///// Global timebase
|
|
//pub clock: Arc<Clock>,
|
|
///// Start time and clip being played
|
|
//pub play_clip: Option<(Moment, Option<Arc<RwLock<Clip>>>)>,
|
|
///// Start time and next clip
|
|
//pub next_clip: Option<(Moment, Option<Arc<RwLock<Clip>>>)>,
|
|
///// Play input through output.
|
|
//pub monitoring: bool,
|
|
///// Write input to sequence.
|
|
//pub recording: bool,
|
|
///// Overdub input to sequence.
|
|
//pub overdub: bool,
|
|
///// Send all notes off
|
|
//pub reset: bool, // TODO?: after Some(nframes)
|
|
///// Record from MIDI ports to current sequence.
|
|
//pub midi_inputs: Vec<Port<MidiIn>>,
|
|
///// Play from current sequence to MIDI ports
|
|
//pub midi_outputs: Vec<Port<MidiOut>>,
|
|
///// MIDI output buffer
|
|
//pub midi_note: Vec<u8>,
|
|
///// MIDI output buffer
|
|
//pub midi_chunk: Vec<Vec<Vec<u8>>>,
|
|
///// Notes currently held at input
|
|
//pub notes_in: Arc<RwLock<[bool; 128]>>,
|
|
///// Notes currently held at output
|
|
//pub notes_out: Arc<RwLock<[bool; 128]>>,
|
|
//}
|
|
|
|
///// Methods used primarily by the process callback
|
|
//impl MIDIPlayer {
|
|
//pub fn new (
|
|
//jack: &Arc<RwLock<JackConnection>>,
|
|
//clock: &Arc<Clock>,
|
|
//name: &str
|
|
//) -> Usually<Self> {
|
|
//let jack = jack.read().unwrap();
|
|
//Ok(Self {
|
|
//clock: clock.clone(),
|
|
//clip: None,
|
|
//next_clip: None,
|
|
//notes_in: Arc::new(RwLock::new([false;128])),
|
|
//notes_out: Arc::new(RwLock::new([false;128])),
|
|
//monitoring: false,
|
|
//recording: false,
|
|
//overdub: true,
|
|
//reset: true,
|
|
//midi_note: Vec::with_capacity(8),
|
|
//midi_chunk: vec![Vec::with_capacity(16);16384],
|
|
//midi_outputs: vec![
|
|
//jack.client().register_port(format!("{name}_out0").as_str(), MidiOut::default())?
|
|
//],
|
|
//midi_inputs: vec![
|
|
//jack.client().register_port(format!("{name}_in0").as_str(), MidiIn::default())?
|
|
//],
|
|
//})
|
|
//}
|
|
//}
|
|
use std::sync::{Arc, RwLock};
|
|
use std::collections::BTreeMap;
|
|
pub use clojure_reader::edn::Edn;
|
|
|
|
//#[derive(Debug, Copy, Clone, Default, PartialEq)]
|
|
//pub struct Items<'a>(&'a [Item<'a>]);
|
|
//impl<'a> Items<'a> {
|
|
//fn iter (&'a self) -> ItemsIterator<'a> {
|
|
//ItemsIterator(0, self.0)
|
|
//}
|
|
//}
|
|
|
|
//pub struct ItemsIterator<'a>(usize, &'a [Item<'a>]);
|
|
//impl<'a> Iterator for ItemsIterator<'a> {
|
|
//type Item = &'a Item<'a>;
|
|
//fn next (&mut self) -> Option<Self::Item> {
|
|
//let item = self.1.get(self.0);
|
|
//self.0 += 1;
|
|
//item
|
|
//}
|
|
//}
|
|
|
|
/*
|
|
|
|
nice but doesn't work without compile time slice concat
|
|
(which i guess could be implemeted using an unsafe linked list?)
|
|
never done that one before im ny life, might try
|
|
|
|
use konst::slice_concat;
|
|
|
|
const fn read <'a> (
|
|
chars: impl Iterator<Item = char>
|
|
) -> Result<Range<'a>, ParseError> {
|
|
use Range::*;
|
|
let mut state = Range::Nil;
|
|
let mut tokens: &[Range<'a>] = &[];
|
|
while let Some(c) = chars.next() {
|
|
state = match state {
|
|
// must begin expression
|
|
Nil => match c {
|
|
' ' => Nil,
|
|
'(' => Exp(&[]),
|
|
':' => Sym(&[]),
|
|
'1'..'9' => Num(digit(c)),
|
|
'a'..'z' => Key(&[&[c]]),
|
|
_ => return Err(ParseError::Unexpected(c))
|
|
},
|
|
Num(b) => match c {
|
|
' ' => return Ok(Num(digit(c))),
|
|
'1'..'9' => Num(b*10+digit(c)),
|
|
_ => return Err(ParseError::Unexpected(c))
|
|
}
|
|
Sym([]) => match c {
|
|
'a'..'z' => Sym(&[c]),
|
|
_ => return Err(ParseError::Unexpected(c))
|
|
},
|
|
Sym([b @ ..]) => match c {
|
|
' ' => return Ok(Sym(&b)),
|
|
'a'..'z' | '0'..'9' | '-' => Sym(&[..b, c]),
|
|
_ => return Err(ParseError::Unexpected(c))
|
|
}
|
|
Key([[b @ ..]]) => match c {
|
|
' ' => return Ok(Key(&[&b])),
|
|
'/' => Key(&[&b, &[]]),
|
|
'a'..'z' | '0'..'9' | '-' => Key(&[&[..b, c], &[]]),
|
|
_ => return Err(ParseError::Unexpected(c))
|
|
}
|
|
Key([s @ .., []]) => match c {
|
|
'a'..'z' => Key(&[..s, &[c]]),
|
|
_ => return Err(ParseError::Unexpected(c))
|
|
}
|
|
Key([s @ .., [b @ ..]]) => match c {
|
|
'/' => Key([..s, &b, &[]]),
|
|
'a'..'z' | '0'..'9' | '-' => Key(&[..s, &[..b, c]]),
|
|
_ => return Err(ParseError::Unexpected(c))
|
|
}
|
|
// expression must begin with key or symbol
|
|
Exp([]) => match c {
|
|
' ' => Exp(&[]),
|
|
')' => return Err(ParseError::Empty),
|
|
':' => Exp(&[Sym(&[':'])]),
|
|
c => Exp(&[Key(&[&[c]])]),
|
|
},
|
|
|
|
// expression can't begin with number
|
|
Exp([Num(num)]) => return Err(ParseError::Unexpected(c)),
|
|
|
|
// symbol begins with : and lowercase a-z
|
|
Exp([Sym([':'])]) => match c {
|
|
'a'..'z' => Exp(&[Sym(&[':', c])]),
|
|
_ => return Err(ParseError::Unexpected(c)),
|
|
},
|
|
|
|
// any other char is part of symbol until space or )
|
|
Exp([Sym([':', b @ ..])]) => match c {
|
|
')' => { tokens = &[..tokens, Exp(&[Sym(&[":", ..b])])]; Nil },
|
|
' ' => Exp(&[Sym(&[':', ..b]), Nil]),
|
|
c => Exp(&[Sym(&[':', ..b, c])]),
|
|
},
|
|
|
|
// key begins with lowercase a-z
|
|
Exp([Key([])]) => match c {
|
|
'a'..'z' => Exp([Key([[c]])]),
|
|
_ => return Err(ParseError::Unexpected(c)),
|
|
},
|
|
|
|
// any other char is part of key until slash space or )
|
|
Exp([Key([[b @ ..]])]) => match c {
|
|
'/' => Exp(&[Key(&[[..b], []])]),
|
|
' ' => Exp(&[Key(&[[..b]]), Nil]),
|
|
')' => { tokens = &[..tokens, Exp(&[Sym(&[":", ..b])])]; Nil },
|
|
c => Exp(&[Key(&[[..b, c]])])
|
|
}
|
|
|
|
// slash adds new section to key
|
|
Exp([Key([b @ .., []])]) => match c {
|
|
'/' => Exp(&[Key(&[[..b], []])]),
|
|
' ' => Exp(&[Key(&[[..b]]), Nil]),
|
|
')' => { tokens = &[..tokens, Exp(&[Sym(&[":", ..b])])]; Nil },
|
|
c => Exp(&[Key(&[[..b, c]])])
|
|
}
|
|
|
|
}
|
|
}
|
|
Ok(state)
|
|
}
|
|
*/
|
|
|
|
|
|
/// EDN parsing helper.
|
|
#[macro_export] macro_rules! edn {
|
|
($edn:ident { $($pat:pat => $expr:expr),* $(,)? }) => {
|
|
match $edn { $($pat => $expr),* }
|
|
};
|
|
($edn:ident in $args:ident { $($pat:pat => $expr:expr),* $(,)? }) => {
|
|
for $edn in $args {
|
|
edn!($edn { $($pat => $expr),* })
|
|
}
|
|
};
|
|
}
|
|
|
|
pub trait FromEdn<C>: Sized {
|
|
const ID: &'static str;
|
|
fn from_edn (context: C, expr: &[Edn<'_>]) ->
|
|
std::result::Result<Self, Box<dyn std::error::Error>>;
|
|
}
|
|
|
|
/// Implements the [FromEdn] trait.
|
|
#[macro_export] macro_rules! from_edn {
|
|
($id:expr => |$context:tt:$Context:ty, $args:ident| -> $T:ty $body:block) => {
|
|
impl FromEdn<$Context> for $T {
|
|
const ID: &'static str = $id;
|
|
fn from_edn <'e> ($context: $Context, $args: &[Edn<'e>]) -> Usually<Self> {
|
|
$body
|
|
}
|
|
}
|
|
}
|
|
}
|
|
use crate::*;
|
|
use std::sync::Arc;
|
|
pub enum EdnIterator {
|
|
Nil,
|
|
Sym(Arc<str>),
|
|
Exp(Vec<EdnIterator>)
|
|
}
|
|
impl EdnIterator {
|
|
pub fn new (item: &EdnItem) -> Self {
|
|
use EdnItem::*;
|
|
match item {
|
|
Sym(t) => Self::Sym(t.clone()),
|
|
Exp(i) => Self::Exp(i.iter().map(EdnIterator::new).collect()),
|
|
_ => Self::Nil,
|
|
}
|
|
}
|
|
}
|
|
impl Iterator for EdnIterator {
|
|
type Item = EdnItem;
|
|
fn next (&mut self) -> Option<Self::Item> {
|
|
use EdnIterator::*;
|
|
match self {
|
|
Sym(t) => {
|
|
let t = *t;
|
|
*self = Nil;
|
|
Some(Sym(t))
|
|
},
|
|
Exp(v) => match v.as_mut_slice() {
|
|
[a] => if let Some(next) = a.next() {
|
|
Some(next)
|
|
} else {
|
|
*self = Exp(v.split_off(1));
|
|
self.next()
|
|
},
|
|
_ => {
|
|
*self = Nil;
|
|
None
|
|
}
|
|
},
|
|
_ => {
|
|
*self = Nil;
|
|
None
|
|
}
|
|
}
|
|
}
|
|
}
|