diff --git a/Cargo.lock b/Cargo.lock index d1b3d2d..877cb78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -402,25 +402,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crossbeam-deque" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -543,22 +524,11 @@ name = "dizzle" version = "0.1.0" dependencies = [ "const_panic", - "dizzle_proc", "itertools 0.14.0", "konst", - "peg", "thiserror 2.0.18", ] -[[package]] -name = "dizzle_proc" -version = "0.1.0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "dlib" version = "0.5.2" @@ -1037,15 +1007,6 @@ dependencies = [ "libc", ] -[[package]] -name = "midly" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "207d755f4cb882d20c4da58d707ca9130a0c9bc5061f657a4f299b8e36362b7a" -dependencies = [ - "rayon", -] - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1419,33 +1380,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" -[[package]] -name = "peg" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aad070be5b63aa72103f2fcdd70a83adbd5e90112ce5b574171ff1c65501773" -dependencies = [ - "peg-macros", - "peg-runtime", -] - -[[package]] -name = "peg-macros" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd8ef6825cae95355031ae26a99b616a2a21f22ba2de0197c43dfb05acbe7ee" -dependencies = [ - "peg-runtime", - "proc-macro2", - "quote", -] - -[[package]] -name = "peg-runtime" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7011d97b484a5ebdc4b1fdb3b12d5e4bbbea56e9d22b688f2e79e04b65a7d8a6" - [[package]] name = "percent-encoding" version = "2.3.2" @@ -1639,9 +1573,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.46" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -1756,26 +1690,6 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" -[[package]] -name = "rayon" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - [[package]] name = "redox_syscall" version = "0.4.1" @@ -2087,9 +2001,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.119" +version = "2.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" dependencies = [ "proc-macro2", "quote", @@ -2120,7 +2034,6 @@ dependencies = [ "crossterm 0.29.0", "dizzle", "jack", - "midly", "palette", "proptest", "proptest-derive", diff --git a/Cargo.toml b/Cargo.toml index 588f2a3..8657a68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,12 +5,11 @@ version = "0.15.0" description = "UI metaframework." [features] -default = ["lang", "sing", "midi", "draw", "play", "term", "text", "time", "rand", "okhsl"] +default = ["lang", "sing", "draw", "play", "term", "text", "time", "rand", "okhsl"] bumpalo = ["dep:bumpalo"] draw = [] gui = ["draw", "dep:winit"] lang = ["dep:dizzle"] -midi = ["dep:midly"] okhsl = ["dep:palette"] play = [] rand = ["dep:rand"] @@ -28,7 +27,6 @@ bumpalo = { optional = true, version = "3.19.0" } crossterm = { optional = true, version = "0.29.0" } dizzle = { optional = true, path = "./dizzle" } jack = { optional = true, path = "./rust-jack" } -midly = { optional = true, version = "0.5" } palette = { optional = true, version = "0.7.6", features = [ "random" ] } quanta = { optional = true, version = "0.12.3" } rand = { optional = true, version = "0.8.5" } diff --git a/Justfile b/Justfile index b233be1..8d7c790 100644 --- a/Justfile +++ b/Justfile @@ -30,10 +30,7 @@ doc: cargo doc example-tui-00: - cargo run --example mode_0 + cargo run --example tui_00 example-tui-01: - cargo run --example mode_1 - -example-tui-02: - cargo run --example mode_2 + cargo run --example tui_01 diff --git a/dizzle b/dizzle index 4424ef7..0050385 160000 --- a/dizzle +++ b/dizzle @@ -1 +1 @@ -Subproject commit 4424ef7fc3fd8bfbea1fb55b4922f7a8cfd2a8ef +Subproject commit 0050385e63ea5d3ab9be7c495dae5fc635396cf0 diff --git a/examples/mode_00.rs b/examples/mode_00.rs deleted file mode 100644 index 9ab858c..0000000 --- a/examples/mode_00.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Mode 0: Direct draw -use ::{std::sync::{Arc, RwLock}, ratatui::style::Color, tengri::*}; -fn main () {} diff --git a/examples/mode_01.rs b/examples/mode_01.rs deleted file mode 100644 index 18a9dae..0000000 --- a/examples/mode_01.rs +++ /dev/null @@ -1,153 +0,0 @@ -//! Mode 01 -use ::std::sync::{Arc, RwLock}; -use ::crossterm::event::{Event::*, KeyEvent, KeyCode::*}; -use ::ratatui::style::Color; -use ::tengri::{*, lang::*}; -tui_app!(State { - /** Command history (undo/redo). */ - history: Vec, - /** User-controllable value. */ - cursor: usize, - /** Rendered window size. */ - size: Sizer, -}); -tui_keys!(self: State, input { - Ok(if let Key(KeyEvent { code, .. }) = input.0 { - match code { - Up | Right => { self.next()?.map(|x|self.history.push(x)); }, - Down | Left => { self.prev()?.map(|x|self.history.push(x)); }, - _ => {} - } - }) -}); -tui_view!(self: State { - let index = self.cursor + 1; - let wh = (self.size.w(), self.size.h()); - let src = VIEWS.get(self.cursor).unwrap_or(&""); - let heading = format!("State {}/{} in {:?}", index, VIEWS.len(), &wh); - let title = bg(Color::Rgb(60, 10, 10), heading.align_n().push_y(1)); - let code = bg(Color::Rgb(10, 60, 10), format!("{}", src).align_n().push_y(2)); - let widget = thunk(move|to: &mut Tui|self.interpret(to, &src)); - self.size.of(south(title, north(code, widget))) -}); -impl Interpret for State { - fn interpret_expr (&self, to: &mut Tui, expr: &impl Language) -> Usually { - let expr = expr.expr()?; - match expr.head()? { - Some("g") if let Some(tail) = expr.tail()? => { - Color::new_g(tail.head()?, try_to_u8) - }, - Some("rgb") if let Some(tail) = expr.tail()? => { - Color::new_rgb(tail.head()?) - }, - _ => Err(format!("not a color").into()) - } - } -} -impl Interpret>> for State { - fn interpret_word (&self, to: &mut Tui, sym: &impl Language) -> Perhaps> { - match sym.src()? { - Some(":foo") => "foo".draw(to), - Some(":bar") => "bar".draw(to), - Some(":foobar") => "FOOBAR".draw(to), - _ => todo!() - } - } - fn interpret_expr (&self, to: &mut Tui, src: &impl Expression) -> Perhaps> { - Ok(Some(if let Some(area) = eval_view(self, to, src)? { - area - } else if let Some(area) = eval_view_tui(self, to, src)? { - area - } else { - return Err(format!("App::interpret_expr: unexpected: {src:?}").into()) - })) - } -} -impl State { - fn next (&mut self) -> Perhaps { - self.cursor = (self.cursor + 1) % VIEWS.len(); - Ok(Some(Action::Prev)) - } - fn prev (&mut self) -> Perhaps { - self.cursor = if self.cursor > 0 { self.cursor - 1 } else { VIEWS.len() - 1 }; - Ok(Some(Action::Next)) - } -} -#[derive(Debug)] -enum Action { - /** Increment cursor */ Next, - /** Decrement cursor */ Prev, -} -impl Action { - fn eval (&self, state: &mut State) -> Perhaps { - use Action::*; - match self { Next => state.next(), Prev => state.prev(), } - } -} -const VIEWS: &'static [&'static str] = &[ - stringify! { :foobar }, - stringify! { (bg (g 8) :foobar) }, - stringify! { (fill/xy :foobar) }, - stringify! { (bsp/s :foo :bar) }, - stringify! { (fixed/xy 20 10 :foobar) }, - stringify! { (bsp/s (fixed/xy 5 6 :foo) (fixed/xy 7 8 :bar)) }, - stringify! { (bsp/e (fixed/xy 5 6 :foo) (fixed/xy 7 8 :bar)) }, - stringify! { (bsp/n (fixed/xy 5 6 :foo) (fixed/xy 7 8 :bar)) }, - stringify! { (bsp/w (fixed/xy 5 6 :foo) (fixed/xy 7 8 :bar)) }, - stringify! { (bsp/a (fixed/xy 5 6 :foo) (fixed/xy 7 8 :bar)) }, - stringify! { (bsp/b (fixed/xy 5 6 :foo) (fixed/xy 7 8 :bar)) }, - stringify! { - (bsp/s - (bsp/e (align/nw (fixed/xy 5 3 :foo)) - (bsp/e (align/n (fixed/xy 5 3 :foo)) - (align/ne (fixed/xy 5 3 :foo)))) - (bsp/s - (bsp/e (align/w (fixed/xy 5 3 :foo)) - (bsp/e (align/c (fixed/xy 5 3 :foo)) - (align/e (fixed/xy 5 3 :foo)))) - (bsp/e (align/sw (fixed/xy 5 3 :foo)) - (bsp/e (align/s (fixed/xy 5 3 :foo)) - (align/se (fixed/xy 5 3 :foo)))))) - }, - stringify! { - (bsp/s - (bsp/e (fixed/xy 8 5 (align/nw :foo)) - (bsp/e (fixed/xy 8 5 (align/n :foo)) - (fixed/xy 8 5 (align/ne :foo)))) - (bsp/s - (bsp/e (fixed/xy 8 5 (align/w :foo)) - (bsp/e (fixed/xy 8 5 (align/c :foo)) - (fixed/xy 8 5 (align/e :foo)))) - (bsp/e (fixed/xy 8 5 (align/sw :foo)) - (bsp/e (fixed/xy 8 5 (align/s :foo)) - (fixed/xy 8 5 (align/se :foo)))))) - }, - stringify! { - (bsp/s - (bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/nw :foo))) - (bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/n :foo))) - (grow/xy 1 1 (fixed/xy 8 5 (align/ne :foo))))) - (bsp/s - (bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/w :foo))) - (bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/c :foo))) - (grow/xy 1 1 (fixed/xy 8 5 (align/e :foo))))) - (bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/sw :foo))) - (bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/s :foo))) - (grow/xy 1 1 (fixed/xy 8 5 (align/se :foo))))))) - }, - stringify! { :map-e }, - stringify! { (align/c :map-e) }, - stringify! { :map-s }, - stringify! { (align/c :map-s) }, - stringify! { - (align/c (bg/behind :bg0 (margin/xy 1 1 (col - (bg/behind :bg1 (border/around :border1 (margin/xy 2 1 :label1))) - (bg/behind :bg2 (border/around :border2 (margin/xy 4 2 :label2))) - (bg/behind :bg3 (border/around :border3 (margin/xy 6 3 :label3))))))) - }, -]; -//handle!(TuiIn: |self: State, input|Action::from(input).eval(self).map(|_|None)); - //view!(State: Tui: [ evaluate_output_expression, evaluate_output_expression_tui ]); - //draw!(State: Tui: [ draw_example ]); -//impl_from!(Action: |input: &TuiIn| todo!()); -//fn draw_example (state: &State, to: &mut Tui) {} diff --git a/examples/tui_00.rs b/examples/tui_00.rs new file mode 100644 index 0000000..8aedd9f --- /dev/null +++ b/examples/tui_00.rs @@ -0,0 +1,163 @@ +use ::{ + std::{io::stdout, sync::{Arc, RwLock}}, + ratatui::style::Color, + tengri::{*, lang::*}, +}; + +tui_main!(State { + cursor: 10, + ..Default::default() +}); + +tui_keys!(|self: State, input| { + Ok(()) +}); + +tui_view!(|self: State| { + let index = self.cursor + 1; + let wh = (self.size.w(), self.size.h()); + let src = VIEWS.get(self.cursor).unwrap_or(&""); + let heading = format!("State {}/{} in {:?}", index, VIEWS.len(), &wh); + let title = bg(Color::Rgb(60, 10, 10), heading.align_n().push_y(1)); + let code = bg(Color::Rgb(10, 60, 10), format!("{}", src).align_n().push_y(2)); + //let content = ;//();//bg(Color::Rgb(10, 10, 60), View(self, CstIter::new(src))); + let widget = thunk(move|to: &mut Tui|self.interpret(to, &src).map(Some)); + self.size.of(south(title, north(code, widget))) +}); + +#[derive(Debug, Default)] +struct State { + /** Command history (undo/redo). */ + history: Vec, + /** User-controllable value. */ + cursor: usize, + /** Rendered window size. */ + size: Sizer, +} + +impl Interpret> for State { +} + +#[derive(Debug)] enum Action { + /** Increment cursor */ + Next, + /** Decrement cursor */ + Prev +} + +//impl_from!(Action: |input: &TuiIn| todo!()); + +fn draw_example (state: &State, to: &mut Tui) { +} + +impl Action { + + const BINDS: &'static str = stringify! { + (@left prev) + (@right next) + }; + + fn eval (&self, state: &mut State) -> Perhaps { + use Action::*; + match self { Next => Self::next(state), Prev => Self::prev(state), } + } + + fn next (state: &mut State) -> Perhaps { + state.cursor = (state.cursor + 1) % VIEWS.len(); + Ok(Some(Self::Prev)) + } + + fn prev (state: &mut State) -> Perhaps { + state.cursor = if state.cursor > 0 { state.cursor - 1 } else { VIEWS.len() - 1 }; + Ok(Some(Self::Next)) + } + +} + +const VIEWS: &'static [&'static str] = &[ + + stringify! { :hello-world }, + + stringify! { (fill/xy :hello-world) }, + + stringify! { (bsp/s :hello :world) }, + + stringify! { (fixed/xy 20 10 :hello-world) }, + + stringify! { (bsp/s (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world)) }, + + stringify! { (bsp/e (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world)) }, + + stringify! { (bsp/n (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world)) }, + + stringify! { (bsp/w (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world)) }, + + stringify! { (bsp/a (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world)) }, + + stringify! { (bsp/b (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world)) }, + + stringify! { + (bsp/s + (bsp/e (align/nw (fixed/xy 5 3 :hello)) + (bsp/e (align/n (fixed/xy 5 3 :hello)) + (align/ne (fixed/xy 5 3 :hello)))) + (bsp/s + (bsp/e (align/w (fixed/xy 5 3 :hello)) + (bsp/e (align/c (fixed/xy 5 3 :hello)) + (align/e (fixed/xy 5 3 :hello)))) + (bsp/e (align/sw (fixed/xy 5 3 :hello)) + (bsp/e (align/s (fixed/xy 5 3 :hello)) + (align/se (fixed/xy 5 3 :hello)))))) + }, + + stringify! { + (bsp/s + (bsp/e (fixed/xy 8 5 (align/nw :hello)) + (bsp/e (fixed/xy 8 5 (align/n :hello)) + (fixed/xy 8 5 (align/ne :hello)))) + (bsp/s + (bsp/e (fixed/xy 8 5 (align/w :hello)) + (bsp/e (fixed/xy 8 5 (align/c :hello)) + (fixed/xy 8 5 (align/e :hello)))) + (bsp/e (fixed/xy 8 5 (align/sw :hello)) + (bsp/e (fixed/xy 8 5 (align/s :hello)) + (fixed/xy 8 5 (align/se :hello)))))) + }, + + stringify! { + (bsp/s + (bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/nw :hello))) + (bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/n :hello))) + (grow/xy 1 1 (fixed/xy 8 5 (align/ne :hello))))) + (bsp/s + (bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/w :hello))) + (bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/c :hello))) + (grow/xy 1 1 (fixed/xy 8 5 (align/e :hello))))) + (bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/sw :hello))) + (bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/s :hello))) + (grow/xy 1 1 (fixed/xy 8 5 (align/se :hello))))))) + }, + + stringify! { :map-e }, + + stringify! { (align/c :map-e) }, + + stringify! { :map-s }, + + stringify! { (align/c :map-s) }, + + stringify! { + (align/c (bg/behind :bg0 (margin/xy 1 1 (col + (bg/behind :bg1 (border/around :border1 (margin/xy 2 1 :label1))) + (bg/behind :bg2 (border/around :border2 (margin/xy 4 2 :label2))) + (bg/behind :bg3 (border/around :border3 (margin/xy 6 3 :label3))))))) + }, + +]; + +//namespace!(State: bool {}); +//namespace!(State: u16 {}); +//namespace!(State: Color {}); + //handle!(TuiIn: |self: State, input|Action::from(input).eval(self).map(|_|None)); + //view!(State: Tui: [ evaluate_output_expression, evaluate_output_expression_tui ]); + //draw!(State: Tui: [ draw_example ]); diff --git a/examples/mode_02.rs b/examples/tui_01.rs similarity index 99% rename from examples/mode_02.rs rename to examples/tui_01.rs index 40fba02..9fa7a93 100644 --- a/examples/mode_02.rs +++ b/examples/tui_01.rs @@ -1,4 +1,3 @@ -//! Mode 02 use ::{std::sync::{Arc, RwLock}, ratatui::style::Color, tengri::*}; fn main () {} diff --git a/proc/src/lib.rs b/proc/src/lib.rs index 1c31c78..7ae7699 100644 --- a/proc/src/lib.rs +++ b/proc/src/lib.rs @@ -50,6 +50,26 @@ pub fn view (meta: TokenStream, item: TokenStream) -> TokenStream { )) } +pub(crate) fn write (t: T) -> TokenStream { + let mut out = TokenStream2::new(); + t.to_tokens(&mut out); + out.into() +} + +pub(crate) fn write_quote (quote: TokenStream2) -> TokenStream2 { + let mut out = TokenStream2::new(); + for token in quote { + out.append(token); + } + out +} + +pub(crate) fn write_quote_to (out: &mut TokenStream2, quote: TokenStream2) { + for token in quote { + out.append(token); + } +} + #[cfg(test)] #[test] fn test_proc_view () { let x: crate::proc_view::ViewMeta = pq! { SomeOut }; let output: Ident = pq! { SomeOut }; diff --git a/shell.nix b/shell.nix index f3e43dc..6f44581 100644 --- a/shell.nix +++ b/shell.nix @@ -1,14 +1,13 @@ #!/usr/bin/env nix-shell {pkgs?import{}}:let - name = "tengri"; - stdenv = pkgs.clang19Stdenv; + name = "tengri"; + stdenv = pkgs.clang19Stdenv; + LIBCLANG_PATH = "${pkgs.libclang.lib.outPath}/lib"; + LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath []; in pkgs.mkShell.override { inherit stdenv; } { - LIBCLANG_PATH = "${pkgs.libclang.lib.outPath}/lib"; - LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ - pkgs.pipewire.jack - ]; + inherit name LIBCLANG_PATH LD_LIBRARY_PATH; buildInputs = [ pkgs.libclang pkgs.jack2 diff --git a/src/draw.rs b/src/draw.rs index 6a7bc5b..a672576 100644 --- a/src/draw.rs +++ b/src/draw.rs @@ -7,22 +7,13 @@ use crate::*; /// struct TestOut { w: u16, h: u16 }; /// impl Wide for TestOut {} /// impl Tall for TestOut {} -/// impl Xy for TestOut { -/// fn x (&self) -> u16 { 0 } -/// fn y (&self) -> u16 { 0 } -/// } +/// impl Xy for TestOut { fn x (&self) -> u16 { 0 } fn y (&self) -> u16 { 0 } } /// impl Screen for TestOut { /// type Unit = u16; -/// fn show (&mut self, _: impl Draw) -> Perhaps> -/// { println!("placed"); Ok(None) } -/// fn area (&self) -> XYWH -/// { Default::default() } -/// fn clip ( -/// &mut self, -/// area: impl Into>>, -/// draw: impl FnOnce(&mut Self)->T -/// ) -> T -/// { draw(self } +/// fn show > (&mut self, _: D) -> Drawn { +/// println!("placed"); +/// Ok(None) +/// } /// } /// /// impl_draw!(|self: String, to: TestOut|{ @@ -94,11 +85,6 @@ pub trait Draw { } } -/// The opposite of [thunk]? -pub fn draw > (item: T) -> impl FnOnce(&mut S) -> Perhaps> { - move|to: &mut S|item.draw(to) -} - pub type Drawn = Perhaps>; impl Draw for () { diff --git a/src/draw/layout.rs b/src/draw/layout.rs index 0561f2a..366eadf 100644 --- a/src/draw/layout.rs +++ b/src/draw/layout.rs @@ -1,7 +1,5 @@ use crate::*; -impl> Layout for T {} - pub trait Layout: Draw + Sized { fn full_w (self) -> impl Draw { Full::W(self) @@ -13,15 +11,12 @@ pub trait Layout: Draw + Sized { Full::WH(self) } - /// (bsp/e (exact/w 10 "Hello") "World") fn exact_w >> (self, x: N) -> impl Draw { Exact::W(self, x.into()) } - /// (bsp/s (exact/h 10 "Hello") "World") fn exact_h >> (self, y: N) -> impl Draw { Exact::H(self, y.into()) } - /// (exact/wh 10 2 "Hello World") fn exact_wh >> (self, x: N, y: N) -> impl Draw { Exact::WH(self, x.into(), y.into()) } @@ -60,7 +55,7 @@ pub trait Layout: Draw + Sized { Pull::X(self, x.into()) } fn pull_y >> (self, y: N) -> impl Draw { - Pull::Y(self, y.into()) + Pull::X(self, y.into()) } fn pull_xy >> (self, x: N, y: N) -> impl Draw { Pull::XY(self, x.into(), y.into()) @@ -70,7 +65,7 @@ pub trait Layout: Draw + Sized { Push::X(self, x.into()) } fn push_y >> (self, y: N) -> impl Draw { - Push::Y(self, y.into()) + Push::X(self, y.into()) } fn push_xy >> (self, x: N, y: N) -> impl Draw { Push::XY(self, x.into(), y.into()) @@ -155,6 +150,8 @@ pub trait Layout: Draw + Sized { } } +impl> Layout for T {} + /// Use whole drawing area along one or both axes. /// /// ``` @@ -282,24 +279,8 @@ pub enum Max, X: Into>> { WH(I, X, X), } -impl_draw!(, X: Into>,>|self: Max, to: T|{ - let area: XYWH = to.area(); - let (item, area) = match self { - Self::W(item, max_w) => (item, XYWH( - area.0, area.1, max_w.into().map(|max|max.min(area.2)).unwrap_or(area.2), - area.3 - )), - Self::H(item, max_h) => (item, XYWH( - area.0, area.1, area.2, - max_h.into().map(|max|max.min(area.3)).unwrap_or(area.3) - )), - Self::WH(item, max_w, max_h) => (item, XYWH( - area.0, area.1, max_w.into().map(|max|max.min(area.2)).unwrap_or(area.2), - max_h.into().map(|max|max.min(area.3)).unwrap_or(area.3), - )), - _ => return Ok(None) - }; - to.clip(area, draw(item)) +impl_draw!(, X: Into>,>|self: Max, _to: T|{ + todo!() }); /// Set size of of drawing area. @@ -318,17 +299,19 @@ pub enum Exact, X: Into>> { } impl_draw!(, X: Into>,>|self: Exact, to: T|{ - let area: XYWH = to.area(); - let (item, area) = match self { - Self::W(item, w1) => - (item, XYWH(area.0, area.1, w1.into().unwrap_or(area.2), area.3)), - Self::H(item, h1) => - (item, XYWH(area.0, area.1, area.2, h1.into().unwrap_or(area.3))), - Self::WH(item, w1, h1) => - (item, XYWH(area.0, area.1, w1.into().unwrap_or(area.2), h1.into().unwrap_or(area.3))), - _ => return Ok(None) - }; - to.clip(area, |to|item.draw(to)) + match self { + Self::__(_) => unreachable!(), + Self::W(item, w1) if let Some(XYWH(x, y, w, h)) = item.layout(to.area())? => { + to.clip(XYWH(x, y, w1.into().unwrap_or(w), h), |to|item.draw(to)) + }, + Self::H(item, h1) if let Some(XYWH(x, y, w, h)) = item.layout(to.area())? => { + to.clip(XYWH(x, y, w, h1.into().unwrap_or(h)), |to|item.draw(to)) + }, + Self::WH(item, w1, h1) if let Some(XYWH(x, y, w, h)) = item.layout(to.area())? => { + to.clip(XYWH(x, y, w1.into().unwrap_or(w), h1.into().unwrap_or(h)), |to|item.draw(to)) + }, + _ => Ok(None) + } }); /// Define inner drawing area. diff --git a/src/draw/split.rs b/src/draw/split.rs index 8408f57..1cee49c 100644 --- a/src/draw/split.rs +++ b/src/draw/split.rs @@ -37,23 +37,6 @@ pub const fn below , B: Draw> (a: A, b: B) -> impl Draw impl Split { - /// ``` - /// use tengri::*; - /// let _ = Split::Above.stack("", ""); - /// let _ = Split::Below.stack("", ""); - /// let _ = Split::North.stack("", ""); - /// let _ = Split::South.stack("", ""); - /// let _ = Split::East.stack("", ""); - /// let _ = Split::West.stack("", ""); - /// ``` - pub const fn stack , B: Draw> (&self, a: A, b: B) -> impl Draw { - thunk(move|to: &mut S|{ - let (area_a, area_b) = stack_areas(self, to.area(), &a, &b)?; - let (drawn_a, drawn_b) = draw_stacks(self, to, a, area_a, None, b, area_b, None)?; - Ok(stack_drawn(self, drawn_a, drawn_b)) - }) - } - /// ``` /// use tengri::*; /// let _ = Split::Above.half("", ""); @@ -65,10 +48,21 @@ impl Split { /// ``` pub const fn half , B: Draw> (&self, a: A, b: B) -> impl Draw { thunk(move|to: &mut S|{ - let (area_a, area_b) = to.xywh().split_half(self); + let (area_a, area_b) = to.xywh().split_half(self); let (origin_a, origin_b) = self.origins(); - let (drawn_a, drawn_b) = draw_stacks(self, to, a, area_a, origin_a, b, area_b, origin_b)?; - Ok(stack_drawn(self, drawn_a, drawn_b)) + let a = a.align(origin_a); + let b = b.align(origin_b); + match self { + Self::Below => { + to.show(area(area_b, b))?; + to.show(area(area_b, a))?; + }, + _ => { + to.show(area(area_a, a))?; + to.show(area(area_b, b))?; + } + } + Ok(Some(to.xywh())) // FIXME: compute and return actually used area }) } @@ -95,8 +89,8 @@ impl Split { Self::East => (E, W), Self::North => (N, S), Self::West => (W, E), - Self::Above => (C, C), - Self::Below => (C, C), + Self::Above => (C, C), + Self::Below => (C, C), } } @@ -117,120 +111,6 @@ impl Split { } -fn draw_stacks ( - split: &Split, - to: &mut S, - a: impl Draw, - area_a: impl Into>>, - origin_a: impl Into>, - b: impl Draw, - area_b: impl Into>>, - origin_b: impl Into>, -) -> Usually<(Option>, Option>)> { - Ok(match split { - Split::Below => { - let drawn_b = to.clip(area_b.into(), |to|b.align(origin_b.into()).draw(to))?; - let drawn_a = to.clip(area_a.into(), |to|a.align(origin_a.into()).draw(to))?; - (drawn_a, drawn_b) - }, - _ => { - let drawn_a = to.clip(area_a.into(), |to|a.align(origin_a.into()).draw(to))?; - let drawn_b = to.clip(area_b.into(), |to|b.align(origin_b.into()).draw(to))?; - (drawn_a, drawn_b) - } - }) -} - -fn stack_areas ( - split: &Split, - area: XYWH, - a: &impl Draw, - b: &impl Draw, -) -> Usually<(Option>, Option>)> { - let area_a = a.layout(area)?; - Ok(match split { - Split::South => ( - area_a, - if let Some(used) = area_a { - b.layout(XYWH( - area.x(), area.y() + used.h(), area.w(), area.h().minus(used.h()) - ))? - } else { - None - } - ), - Split::East => ( - area_a, - if let Some(used) = area_a { - b.layout(XYWH( - area.x() + used.w(), area.y(), area.w().minus(used.w()), area.h() - ))? - } else { - None - } - ), - Split::North => ( - if let Some(used) = area_a { - Some(XYWH(used.x(), area.y() + used.h(), used.w(), used.h())) - } else { - None - }, - if let Some(used) = area_a { - b.layout(XYWH( - area.x(), area.y(), area.w(), area.h().minus(used.h()) - ))? - } else { - b.layout(area)?.map(|area_b|XYWH( - area_b.x(), area_b.y() + area_b.h(), area_b.w(), area_b.h() - )) - } - ), - Split::West => ( - if let Some(used) = area_a { - Some(XYWH(area.x() + used.w(), used.y(), used.w(), used.h())) - } else { - None - }, - if let Some(used) = area_a { - b.layout(XYWH( - area.x(), area.y(), area.w().minus(used.w()), area.h() - ))? - } else { - b.layout(area)?.map(|area_b|XYWH( - area_b.x() + area_b.w(), area_b.y(), area_b.w(), area_b.h() - )) - } - ), - Split::Above | Split::Below => ( - area_a, - b.layout(area)?, - ), - }) -} - -fn stack_drawn ( - split: &Split, - drawn_a: Option>, - drawn_b: Option>, -) -> Option> { - if let (Some(XYWH(xa, ya, wa, ha)), Some(XYWH(xb, yb, wb, hb))) = (drawn_a, drawn_b) { - match split { - Split::South => Some(XYWH(xa.min(xb), ya, wa.max(wb), ha + hb)), - Split::East => Some(XYWH(xa, ya.min(yb), wa + wb, ha.max(hb))), - Split::North => Some(XYWH(xa.min(xb), yb, wa.max(wb), ha + hb)), - Split::West => Some(XYWH(xb, ya.min(yb), wa + wb, ha.max(hb))), - Split::Above | Split::Below => - Some(XYWH(xa.min(xb), ya.min(yb), wa.max(wb), ha.max(hb))), - } - } else if let Some(a) = drawn_a { - Some(a) - } else if let Some(b) = drawn_b { - Some(b) - } else { - None - } -} - #[macro_export] macro_rules! north { ($head:expr $(,)?) => { $head }; ($head:expr, $($tail:expr),* $(,)?) => { north($head, north!($($tail,)*)) }; diff --git a/src/eval.rs b/src/eval.rs index 35ec582..67f565e 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -128,57 +128,44 @@ pub fn eval_view <'a, O: Screen + 'a, S> ( let arg2 = tail1.head(); // First `frags.next()` calls returns the namespace. match frags.next() { - Some("when") => when( state.namespace(arg0?)?.unwrap(), thunk(move|output: &mut O|{state.interpret(output, &arg1)}) ).draw(output), - Some("either") => either( state.namespace(arg0?)?.unwrap(), thunk(move|output: &mut O|{state.interpret(output, &arg1)}), thunk(move|output: &mut O|{state.interpret(output, &arg2)}), ).draw(output), - Some("bsp") => eval_enum!("bsp", output, state, frags.next(), arg0, Split { "n" => North, "s" => South, "e" => East, "w" => West, "a" => Above, "b" => Below - }).stack( + }).half( thunk(move|output: &mut O|{state.interpret(output, &arg0)}), thunk(move|output: &mut O|{state.interpret(output, &arg1)}), ).draw(output), - Some("align") => thunk(move|output: &mut O|{ state.interpret(output, &arg0) }).align(eval_enum!("align", output, state, frags.next(), arg0, Azimuth { - "c" => C, "x" => X, "y" => Y, - "n" => N, "s" => S, "e" => E, "w" => W, - "nw" => NW, "sw" => SW, "ne" => NE, "se" => SE, + "c" => C, "n" => N, "s" => S, "e" => E, "w" => W, "x" => X, "y" => Y })).draw(output), - Some("exact") => eval_xy!( "exact" => expr, head, output, state, frags.next(), exact_wh, exact_w, exact_h, arg0, arg1, arg2, ), - Some("fixed") => eval_xy!( "fixed" => expr, head, output, state, frags.next(), exact_wh, exact_w, exact_h, arg0, arg1, arg2, ), - Some("min") => eval_xy!( "min" => expr, head, output, state, frags.next(), min_wh, min_w, min_h, arg0, arg1, arg2, ), - Some("max") => eval_xy!( "max" => expr, head, output, state, frags.next(), max_wh, max_w, max_h, arg0, arg1, arg2, ), - Some("push") => eval_xy!( "push" => expr, head, output, state, frags.next(), push_xy, push_x, push_y, arg0, arg1, arg2, ), - Some("fill") => eval_xy!( "fill" => expr, head, output, state, frags.next(), full_wh, full_w, full_h, arg0, ), - _ => return Ok(None) } @@ -189,14 +176,27 @@ pub fn eval_view <'a, O: Screen + 'a, S> ( /// ``` /// use tengri::{*, lang::*, ratatui::prelude::Color}; /// -/// #[namespace(bool)] -/// #[namespace(u8 try_to_u8)] -/// #[namespace(u16 try_to_u16)] -/// #[namespace(Color try_to_color)] +/// #[namespace(bool {})] +/// #[namespace(u8: try_to_u8)] +/// #[namespace(u16: try_to_u16)] +/// #[namespace(Color: try_to_color)] /// #[interpret(Tui -> Option>: try_eval_tui)] /// struct State; /// tengri::lang::primitive!(u8: try_to_u8); /// tengri::lang::primitive!(u16: try_to_u16); +/// tengri::lang::namespace!(State: bool { +/// }); +/// tengri::lang::namespace!(State: u8 { +/// literal = |x|try_to_u8(x); +/// }); +/// tengri::lang::namespace!(State: u16 { +/// literal = |x|try_to_u16(x); +/// }); +/// tengri::lang::namespace!(State: Color { +/// expression = |_state| { +/// "g" (x: u8) => { ItemTheme::G[x as usize].base.term } +/// }; +/// }); /// tengri::lang::interpret!(|self: State, context: Tui, lang|->Option>{ /// expression = { /// "text" (...rest) => { todo!() } @@ -228,7 +228,6 @@ pub fn eval_view_tui <'a, S> ( + for<'b>Namespace<'b, u16> + for<'b>Namespace<'b, Color> { - use crate::term::*; // See `tengri::eval_view` let head = expr.head()?; let mut frags = head.src()?.unwrap_or_default().split("/"); @@ -237,6 +236,7 @@ pub fn eval_view_tui <'a, S> ( let tail0 = args.tail(); let arg1 = tail0.head(); match frags.next() { + Some("text") => { if let Some(src) = args?.src()? { output.show(src) diff --git a/src/exit.rs b/src/exit.rs index 8d96419..787e3e5 100644 --- a/src/exit.rs +++ b/src/exit.rs @@ -1,5 +1,5 @@ -use crate::*; use std::sync::{Arc, atomic::AtomicBool}; +use crate::Usually; #[derive(Clone)] pub struct Exit(Arc); @@ -7,14 +7,6 @@ impl Exit { pub fn run (run: impl FnOnce(Self)->Usually) -> Usually { run(Self(Arc::new(AtomicBool::new(false)))) } - pub fn is (event: &Event) -> bool { - matches!(event, Event::Key(KeyEvent { - modifiers: KeyModifiers::CONTROL, - code: KeyCode::Char('c'), - kind: KeyEventKind::Press, - state: KeyEventState::NONE - })) - } } impl AsRef> for Exit { diff --git a/src/lib.rs b/src/lib.rs index 155a53f..f42030b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,7 +14,6 @@ pub extern crate palette; pub extern crate better_panic; pub extern crate unicode_width; #[cfg(feature = "sing")] pub extern crate jack; -#[cfg(feature = "midi")] pub extern crate midly; #[cfg(feature = "term")] pub extern crate ratatui; #[cfg(feature = "term")] pub extern crate crossterm; #[cfg(feature = "lang")] pub extern crate dizzle as lang; @@ -27,6 +26,7 @@ pub(crate) use ::{ std::ops::{Add, Sub, Mul, Div}, std::sync::{Arc, RwLock}, std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::*}, + std::error::Error, std::marker::PhantomData }; @@ -34,7 +34,7 @@ macro_rules! features { ($($feature:literal: [ $($module:ident),* ]),*) => { $( $( - #[cfg(feature = $feature)] pub mod $module; + #[cfg(feature = $feature)] mod $module; #[cfg(feature = $feature)] pub use $module::*; )* )* diff --git a/src/sing.rs b/src/sing.rs index c06a1e5..ce776a6 100644 --- a/src/sing.rs +++ b/src/sing.rs @@ -1,205 +1,9 @@ -use crate::{*, time::PerfModel}; pub use ::jack::{*, contrib::{*, ClosureProcessHandler}}; -pub use ::midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*}; -use ConnectName::*; -use ConnectScope::*; -use ConnectStatus::*; -use JackState::*; +use crate::{*, time::PerfModel}; -/// Wraps [JackState], and through it [jack::Client] when connected. -/// -/// ``` -/// let jack = tengri::Jack::default(); -/// ``` -#[derive(Clone, Debug, Default)] pub struct Jack<'j> ( - pub(crate) Arc>> -); - -/// This is a connection which may be [Inactive], [Activating], or [Active]. -/// In the [Active] and [Inactive] states, [JackState::client] returns a -/// [jack::Client], which you can use to talk to the JACK API. -/// -/// ``` -/// let state = tengri::JackState::default(); -/// ``` -#[derive(Debug, Default)] pub enum JackState<'j> { - /// Unused - #[default] Inert, - /// Before activation. - Inactive(Client), - /// During activation. - Activating, - /// After activation. Must not be dropped for JACK thread to persist. - Active(DynamicAsyncClient<'j>), -} - -/// Implement [Jack] constructor and methods -impl<'j> Jack<'j> { - /// Register new [Client] and wrap it for shared use. - pub fn new_run + Audio + Send + Sync + 'static> ( - name: impl AsRef, - init: impl FnOnce(Jack<'j>)->Usually - ) -> Usually>> { - Jack::new(name)?.run(init) - } - pub fn new (name: impl AsRef) -> Usually { - let client = Client::new(name.as_ref(), ClientOptions::NO_START_SERVER)?.0; - Ok(Jack(Arc::new(RwLock::new(JackState::Inactive(client))))) - } - /// Run something with the client. - pub fn with_client (&self, op: impl FnOnce(&Client)->T) -> T { - match &*self.0.read().unwrap() { - Inert => panic!("jack client not activated"), - Inactive(client) => op(client), - Activating => panic!("jack client has not finished activation"), - Active(client) => op(client.as_client()), - } - } - pub fn run + Audio + Send + Sync + 'static> - (self, init: impl FnOnce(Self)->Usually) -> Usually>> - { - let client_state = self.0.clone(); - let app: Arc> = Arc::new(RwLock::new(init(self)?)); - let mut state = Activating; - std::mem::swap(&mut*client_state.write().unwrap(), &mut state); - if let Inactive(client) = state { - // This is the misc notifications handler. It's a struct that wraps a [Box] - // which performs type erasure on a callback that takes [JackEvent], which is - // one of the available misc notifications. - let notify = JackNotify(Box::new({ - let app = app.clone(); - move|event|(&mut*app.write().unwrap()).handle(event) - }) as BoxedJackEventHandler); - // This is the main processing handler. It's a struct that wraps a [Box] - // which performs type erasure on a callback that takes [Client] and [ProcessScope] - // and passes them down to the `app`'s `process` callback, which in turn - // implements audio and MIDI input and output on a realtime basis. - let process = ::jack::contrib::ClosureProcessHandler::new(Box::new({ - let app = app.clone(); - move|c: &_, s: &_|if let Ok(mut app) = app.write() { - app.process(c, s) - } else { - Control::Quit - } - }) as BoxedAudioHandler); - // Launch a client with the two handlers. - *client_state.write().unwrap() = Active( - client.activate_async(notify, process)? - ); - } else { - unreachable!(); - } - Ok(app) - } -} - -impl<'j> HasJack<'j> for Jack<'j> { - fn jack (&self) -> &Jack<'j> { - self - } -} - -impl<'j> HasJack<'j> for &Jack<'j> { - fn jack (&self) -> &Jack<'j> { - self - } -} - -impl<'j, T: HasJack<'j>> HasJack<'j> for Arc { - fn jack (&self) -> &Jack<'j> { - (&**self).jack() - } -} - -/// Event enum for JACK events. -/// -/// ``` -/// let event = tengri::JackEvent::XRun; // kerpop -/// ``` -#[derive(Debug, Clone, PartialEq)] pub enum JackEvent { - ThreadInit, - Shutdown(ClientStatus, Arc), - Freewheel(bool), - SampleRate(Frames), - ClientRegistration(Arc, bool), - PortRegistration(PortId, bool), - PortRename(PortId, Arc, Arc), - PortsConnected(PortId, PortId, bool), - GraphReorder, - XRun, -} - -/// Generic notification handler that emits [JackEvent] -/// -/// ``` -/// let notify = tengri::JackNotify(|_|{}); -/// ``` -pub struct JackNotify(pub T); - -/// Notification handler wrapper for [BoxedJackEventHandler]. -pub type DynamicNotifications<'j> = - JackNotify>; - -/// Boxed [JackEvent] callback. -pub type BoxedJackEventHandler<'j> = - Box; - -impl NotificationHandler for JackNotify { - fn thread_init(&self, _: &Client) { - self.0(JackEvent::ThreadInit); - } - unsafe fn shutdown(&mut self, status: ClientStatus, reason: &str) { - self.0(JackEvent::Shutdown(status, reason.into())); - } - fn freewheel(&mut self, _: &Client, enabled: bool) { - self.0(JackEvent::Freewheel(enabled)); - } - fn sample_rate(&mut self, _: &Client, frames: Frames) -> Control { - self.0(JackEvent::SampleRate(frames)); - Control::Quit - } - fn client_registration(&mut self, _: &Client, name: &str, reg: bool) { - self.0(JackEvent::ClientRegistration(name.into(), reg)); - } - fn port_registration(&mut self, _: &Client, id: PortId, reg: bool) { - self.0(JackEvent::PortRegistration(id, reg)); - } - fn port_rename(&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control { - self.0(JackEvent::PortRename(id, old.into(), new.into())); - Control::Continue - } - fn ports_connected(&mut self, _: &Client, a: PortId, b: PortId, are: bool) { - self.0(JackEvent::PortsConnected(a, b, are)); - } - fn graph_reorder(&mut self, _: &Client) -> Control { - self.0(JackEvent::GraphReorder); - Control::Continue - } - fn xrun(&mut self, _: &Client) -> Control { - self.0(JackEvent::XRun); - Control::Continue - } -} - -pub trait JackPerfModel { - fn update_from_jack_scope (&self, t0: Option, scope: &ProcessScope); -} - -impl JackPerfModel for PerfModel { - fn update_from_jack_scope (&self, t0: Option, scope: &ProcessScope) { - if let Some(t0) = t0 { - let t1 = self.clock.raw(); - self.used.store( - self.clock.delta_as_nanos(t0, t1) as f64, - Relaxed, - ); - self.window.store( - scope.cycle_times().unwrap().period_usecs as f64, - Relaxed, - ); - } - } -} +mod jack; pub use self::jack::*; +mod jack_event; pub use self::jack_event::*; +mod jack_perf; pub use self::jack_perf::*; /// Trait for thing that has a JACK process callback. pub trait Audio { @@ -287,6 +91,7 @@ pub trait HasJack<'j>: Send + Sync { /// Implement [Audio]: provide JACK callbacks. #[macro_export] macro_rules! impl_audio { + (| $self1:ident: $Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?,$c:ident,$s:ident @@ -315,611 +120,5 @@ pub trait HasJack<'j>: Send + Sync { } } }; -} - -pub trait JackPorts: HasJack<'static> { - /// Register a MIDI input port. - fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; - /// Register a MIDI output port. - fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; - /// Register an audio input port. - fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; - /// Register an audio output port. - fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; -} - -impl> JackPorts for J { - fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - MidiInput::new(self.jack(), name, connect) - } - fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - MidiOutput::new(self.jack(), name, connect) - } - fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - AudioInput::new(self.jack(), name, connect) - } - fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - AudioOutput::new(self.jack(), name, connect) - } -} - -pub trait JackPort: HasJack<'static> { - const KIND: &'static str = "Port"; - type Port: PortSpec + Default; - type Pair: PortSpec + Default; - - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized; - - fn register (jack: &Jack<'static>, name: &impl AsRef) -> Usually> { - jack.with_client(|c|c.register_port::(name.as_ref(), Default::default())) - .map_err(|e|e.into()) - } - - fn close (self) -> Usually<()> where Self: Sized { - let jack = self.jack().clone(); - Ok(jack.with_client(|c|c.unregister_port(self.into_port()))?) - } - - fn into_port (self) -> Port where Self: Sized; - fn port_name (&self) -> &Arc; - fn port (&self) -> &Port; - fn port_mut (&mut self) -> &mut Port; - fn ports (&self, re_name: Option<&str>, re_type: Option<&str>, flags: PortFlags) -> Vec { - self.with_client(|c|c.ports(re_name, re_type, flags)) - } - fn port_by_id (&self, id: u32) -> Option> { - self.with_client(|c|c.port_by_id(id)) - } - fn port_by_name (&self, name: impl AsRef) -> Option> { - self.with_client(|c|c.port_by_name(name.as_ref())) - } - - fn connections (&self) -> &[Connect]; - fn connect_to_matching <'k> (&'k self) -> Usually<()> { - for connect in self.connections().iter() { - match &connect.name { - Some(Exact(name)) => { - *connect.status.write().unwrap() = self.connect_exact(name)?; - }, - Some(RegExp(re)) => { - *connect.status.write().unwrap() = self.connect_regexp(re, connect.scope)?; - }, - _ => {}, - }; - } - Ok(()) - } - fn connect_exact <'k> (&'k self, name: &str) -> - Usually, Arc, ConnectStatus)>> - { - self.with_client(move|c|{ - let mut status = vec![]; - for port in c.ports(None, None, PortFlags::empty()).iter() { - if port.as_str() == &*name { - if let Some(port) = c.port_by_name(port.as_str()) { - let port_status = self.connect_to_unowned(&port)?; - let name = port.name()?.into(); - status.push((port, name, port_status)); - if port_status == Connected { - break - } - } - } - } - Ok(status) - }) - } - fn connect_regexp <'k> ( - &'k self, re: &str, scope: Option - ) -> Usually, Arc, ConnectStatus)>> { - self.with_client(move|c|{ - let mut status = vec![]; - let ports = c.ports(Some(&re), None, PortFlags::empty()); - for port in ports.iter() { - if let Some(port) = c.port_by_name(port.as_str()) { - let port_status = self.connect_to_unowned(&port)?; - let name = port.name()?.into(); - status.push((port, name, port_status)); - if port_status == Connected && scope == Some(One) { - break - } - } - } - Ok(status) - }) - } - /** Connect to a matching port by name. */ - fn connect_to_name (&self, name: impl AsRef) -> Usually { - self.with_client(|c|if let Some(ref port) = c.port_by_name(name.as_ref()) { - self.connect_to_unowned(port) - } else { - Ok(Missing) - }) - } - /** Connect to a matching port by reference. */ - fn connect_to_unowned (&self, port: &Port) -> Usually { - self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { - Connected - } else if let Ok(_) = c.connect_ports(port, self.port()) { - Connected - } else { - Mismatch - })) - } - /** Connect to an owned matching port by reference. */ - fn connect_to_owned (&self, port: &Port) -> Usually { - self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { - Connected - } else if let Ok(_) = c.connect_ports(port, self.port()) { - Connected - } else { - Mismatch - })) - } -} - -/// Audio input port. -#[derive(Debug)] pub struct AudioInput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of ports to connect to. - pub connections: Vec, -} - -/// Audio output port. -#[derive(Debug)] pub struct AudioOutput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of ports to connect to. - pub connections: Vec, -} - -/// MIDI input port. -#[derive(Debug)] pub struct MidiInput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of currently held notes. - pub held: Arc>, - /// List of ports to connect to. - pub connections: Vec, -} - -/// MIDI output port. -#[derive(Debug)] pub struct MidiOutput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of currently held notes. - pub held: Arc>, - /// List of ports to connect to. - pub connections: Vec, - /// Buffer - pub note_buffer: Vec, - /// Buffer - pub output_buffer: Vec>>, -} - -macro_rules! jack_port { - ($($Struct:ty = ($Port:ty => $Pair:ty) $({ $($tt:tt)* })?),*) => { - $( - impl HasJack<'static> for $Struct { - fn jack (&self) -> &Jack<'static> { &self.jack } - } - impl JackPort for $Struct { - type Port = $Port; - type Pair = $Pair; - fn port_name (&self) -> &Arc { - &self.name - } - fn port (&self) -> &Port { - &self.port - } - fn port_mut (&mut self) -> &mut Port { - &mut self.port - } - fn into_port (self) -> Port { - self.port - } - fn connections (&self) -> &[Connect] { - self.connections.as_slice() - } - $($($tt)*)? - } - )* - }; -} - -jack_port!( - AudioInput = (AudioIn => AudioOut) { - const KIND: &'static str = "Audio In"; - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self { - port: Self::register(jack, name)?, - jack: jack.clone(), - name: name.as_ref().into(), - connections: connect.to_vec(), - }; - port.connect_to_matching()?; - Ok(port) - } - }, - - AudioOutput = (AudioOut => AudioIn) { - const KIND: &'static str = "Audio Out"; - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self { - port: Self::register(jack, name)?, - jack: jack.clone(), - name: name.as_ref().into(), - connections: connect.to_vec(), - }; - port.connect_to_matching()?; - Ok(port) - } - }, - - MidiInput = (MidiIn => MidiOut) { - const KIND: &'static str = "MIDI In"; - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self { - port: Self::register(jack, name)?, - jack: jack.clone(), - name: name.as_ref().into(), - connections: connect.to_vec(), - held: Arc::new(RwLock::new([false;128])) - }; - port.connect_to_matching()?; - Ok(port) - } - }, - - MidiOutput = (MidiOut => MidiIn) { - const KIND: &'static str = "MIDI Out"; - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self::register(jack, name)?; - let jack = jack.clone(); - let name = name.as_ref().into(); - let connections = connect.to_vec(); - let port = Self { - jack, - port, - name, - connections, - held: Arc::new([false;128].into()), - note_buffer: vec![0;8], - output_buffer: vec![vec![];65536], - }; - port.connect_to_matching()?; - Ok(port) - } - } -); - -pub type CollectedMidiInput<'a> = Vec, MidiError>)>>; - -/// Trait for thing that may receive MIDI. -pub trait HasMidiIns { - fn midi_ins (&self) -> &Vec; - fn midi_ins_mut (&mut self) -> &mut Vec; - /// Collect MIDI input from app ports (TODO preallocate large buffers) - fn midi_input_collect <'a> (&'a self, scope: &'a ProcessScope) -> CollectedMidiInput<'a> { - self.midi_ins().iter() - .map(|port|port.port().iter(scope) - .map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes))) - .collect::>()) - .collect::>() - } - fn midi_ins_with_sizes <'a> (&'a self) -> - impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a - { - let mut y = 0; - self.midi_ins().iter().enumerate().map(move|(i, input)|{ - let height = 1 + input.connections().len(); - let data = (i, input.port_name(), input.connections(), y, y + height); - y += height; - data - }) - } -} -/// Trait for thing that may output MIDI. -pub trait HasMidiOuts { - fn midi_outs (&self) -> &Vec; - fn midi_outs_mut (&mut self) -> &mut Vec; - fn midi_outs_with_sizes <'a> (&'a self) -> - impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a - { - let mut y = 0; - self.midi_outs().iter().enumerate().map(move|(i, output)|{ - let height = 1 + output.connections().len(); - let data = (i, output.port_name(), output.connections(), y, y + height); - y += height; - data - }) - } - fn midi_outs_emit (&mut self, scope: &ProcessScope) { - for port in self.midi_outs_mut().iter_mut() { - port.buffer_emit(scope) - } - } -} - -impl MidiOutput { - /// Clear the section of the output buffer that we will be using, - /// emitting "all notes off" at start of buffer if requested. - pub fn buffer_clear (&mut self, scope: &ProcessScope, reset: bool) { - let n_frames = (scope.n_frames() as usize).min(self.output_buffer.len()); - for frame in &mut self.output_buffer[0..n_frames] { - frame.clear(); - } - if reset { - all_notes_off(&mut self.output_buffer); - } - } - /// Write a note to the output buffer - pub fn buffer_write <'a> ( - &'a mut self, - sample: usize, - event: LiveEvent, - ) { - self.note_buffer.fill(0); - event.write(&mut self.note_buffer).expect("failed to serialize MIDI event"); - self.output_buffer[sample].push(self.note_buffer.clone()); - // Update the list of currently held notes. - if let LiveEvent::Midi { ref message, .. } = event { - update_keys(&mut*self.held.write().unwrap(), message); - } - } - /// Write a chunk of MIDI data from the output buffer to the output port. - pub fn buffer_emit (&mut self, scope: &ProcessScope) { - let samples = scope.n_frames() as usize; - let mut writer = self.port.writer(scope); - for (time, events) in self.output_buffer.iter().enumerate().take(samples) { - for bytes in events.iter() { - writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{ - panic!("Failed to write MIDI data: {bytes:?}"); - }); - } - } - } -} - -impl MidiInput { - pub fn parsed <'a> (&'a self, scope: &'a ProcessScope) -> impl Iterator, &'a [u8])> { - parse_midi_input(self.port().iter(scope)) - } -} - -/// Return boxed iterator of MIDI events -pub fn parse_midi_input <'a> (input: ::jack::MidiIter<'a>) - -> Box, &'a [u8])> + 'a> -{ - Box::new(input.map(|::jack::RawMidi { time, bytes }|( - time as usize, - LiveEvent::parse(bytes).unwrap(), - bytes - ))) -} - -/// Add "all notes off" to the start of a buffer. -pub fn all_notes_off (output: &mut [Vec>]) { - let mut buf = vec![]; - let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() }; - let evt = LiveEvent::Midi { channel: 0.into(), message: msg }; - evt.write(&mut buf).unwrap(); - output[0].push(buf); -} - -/// Update notes_in array -pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) { - match message { - MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; } - MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; }, - _ => {} - } -} - -impl> + AsMut>> HasMidiIns for T { - fn midi_ins (&self) -> &Vec { self.as_ref() } - fn midi_ins_mut (&mut self) -> &mut Vec { self.as_mut() } -} - -impl> + AsMut>> HasMidiOuts for T { - fn midi_outs (&self) -> &Vec { self.as_ref() } - fn midi_outs_mut (&mut self) -> &mut Vec { self.as_mut() } -} - -impl> AddMidiIn for T { - fn midi_in_add (&mut self) -> Usually<()> { - let index = self.midi_ins().len(); - let port = MidiInput::new(self.jack(), &format!("M/{index}"), &[])?; - self.midi_ins_mut().push(port); - Ok(()) - } -} - -/// Trail for thing that may gain new MIDI ports. -impl> AddMidiOut for T { - fn midi_out_add (&mut self) -> Usually<()> { - let index = self.midi_outs().len(); - let port = MidiOutput::new(self.jack(), &format!("{index}/M"), &[])?; - self.midi_outs_mut().push(port); - Ok(()) - } -} - -/// May create new MIDI input ports. -pub trait AddMidiIn { - fn midi_in_add (&mut self) -> Usually<()>; -} - -/// May create new MIDI output ports. -pub trait AddMidiOut { - fn midi_out_add (&mut self) -> Usually<()>; -} - -#[derive(Clone, Debug, PartialEq)] pub enum ConnectName { - /** Exact match */ - Exact(Arc), - /** Match regular expression */ - RegExp(Arc), -} - -#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectScope { - One, - All -} - -#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectStatus { - Missing, - Disconnected, - Connected, - Mismatch, -} - -/// Port connection manager. -/// -/// ``` -/// let connect = tek::Connect::default(); -/// ``` -#[derive(Clone, Debug, Default)] -pub struct Connect { - pub name: Option, - pub scope: Option, - pub status: Arc, Arc, ConnectStatus)>>>, - pub info: Arc, -} - -impl Connect { - - pub fn new > ( - exact: Option>, - re: Option>, - re_all: Option>, - ) -> Vec { - let mut connections = vec![]; - if let Some(exact ) = exact { for port in exact { connections.push(Self::exact(port)) } } - if let Some(regexp) = re { for port in regexp { connections.push(Self::regexp(port)) } } - if let Some(re_all) = re_all { for port in re_all { connections.push(Self::regexp_all(port)) } } - connections - } - - pub fn midi_ins > ( - jack: &Jack<'static>, - name: &T, - midi_from: &[T], - midi_from_re: Option<&[T]>, - ) -> Usually> { - Ok(Connect::new( - Some(midi_from.into_iter()), - Some([].into_iter()), - midi_from_re.map(|x|x.into_iter())).iter().enumerate() - .map(|(index, connect)|jack.midi_in(&format!("{}/{index}", name.as_ref()), &[connect.clone()])) - .collect::>()?) - } - - pub fn midi_outs > ( - jack: &Jack<'static>, - name: &T, - midi_to: &[T], - midi_to_re: Option<&[T]>, - ) -> Usually> { - Ok(Connect::new( - Some(midi_to.into_iter()), - Some([].into_iter()), - midi_to_re.map(|x|x.into_iter())).iter().enumerate() - .map(|(index, connect)|jack.midi_out(&format!("{index}/{}", name.as_ref()), &[connect.clone()])) - .collect::>()?) - } - - pub fn audio_ins > ( - jack: &Jack<'static>, - name: &T, - audio_from: &[T], - audio_from_re: Option<&[T]>, - ) -> Usually> { - Ok(Connect::new( - Some(audio_from.into_iter()), - Some([].into_iter()), - audio_from_re.map(|x|x.into_iter())).iter().enumerate() - .map(|(index, connect)|jack.audio_in(&format!("{}/{index}", name.as_ref()), &[connect.clone()])) - .collect::>()?) - } - - pub fn audio_outs > ( - jack: &Jack<'static>, - name: &T, - audio_to: &[T], - audio_to_re: Option<&[T]>, - ) -> Usually> { - Ok(Connect::new( - Some(audio_to.into_iter()), - Some([].into_iter()), - audio_to_re.map(|x|x.into_iter())).iter().enumerate() - .map(|(index, connect)|jack.audio_out(&format!("{index}/{}", name.as_ref()), &[connect.clone()])) - .collect::>()?) - } - - /// Connect to this exact port - pub fn exact (name: impl AsRef) -> Self { - let info = format!("=:{}", name.as_ref()).into(); - let name = Some(Exact(name.as_ref().into())); - Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } - } - - pub fn regexp (name: impl AsRef) -> Self { - let info = format!("~:{}", name.as_ref()).into(); - let name = Some(RegExp(name.as_ref().into())); - Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } - } - - pub fn regexp_all (name: impl AsRef) -> Self { - let info = format!("+:{}", name.as_ref()).into(); - let name = Some(RegExp(name.as_ref().into())); - Self { name, scope: Some(All), status: Arc::new(RwLock::new(vec![])), info } - } - - pub fn info (&self) -> Arc { - format!(" ({}) {} {}", { - let status = self.status.read().unwrap(); - let mut ok = 0; - for (_, _, state) in status.iter() { - if *state == Connected { - ok += 1 - } - } - format!("{ok}/{}", status.len()) - }, match self.scope { - None => "x", - Some(One) => " ", - Some(All) => "*", - }, match &self.name { - None => format!("x"), - Some(Exact(name)) => format!("= {name}"), - Some(RegExp(name)) => format!("~ {name}"), - }).into() - } + } diff --git a/src/sing/jack.rs b/src/sing/jack.rs index e69de29..0100fed 100644 --- a/src/sing/jack.rs +++ b/src/sing/jack.rs @@ -0,0 +1,112 @@ +use crate::{*, PerfModel}; +pub use ::jack::{*, contrib::{*, ClosureProcessHandler}}; + +use JackState::*; + +/// Wraps [JackState], and through it [jack::Client] when connected. +/// +/// ``` +/// let jack = tengri::Jack::default(); +/// ``` +#[derive(Clone, Debug, Default)] pub struct Jack<'j> ( + pub(crate) Arc>> +); + +/// This is a connection which may be [Inactive], [Activating], or [Active]. +/// In the [Active] and [Inactive] states, [JackState::client] returns a +/// [jack::Client], which you can use to talk to the JACK API. +/// +/// ``` +/// let state = tengri::JackState::default(); +/// ``` +#[derive(Debug, Default)] pub enum JackState<'j> { + /// Unused + #[default] Inert, + /// Before activation. + Inactive(Client), + /// During activation. + Activating, + /// After activation. Must not be dropped for JACK thread to persist. + Active(DynamicAsyncClient<'j>), +} + +/// Implement [Jack] constructor and methods +impl<'j> Jack<'j> { + /// Register new [Client] and wrap it for shared use. + pub fn new_run + Audio + Send + Sync + 'static> ( + name: impl AsRef, + init: impl FnOnce(Jack<'j>)->Usually + ) -> Usually>> { + Jack::new(name)?.run(init) + } + + pub fn new (name: impl AsRef) -> Usually { + let client = Client::new(name.as_ref(), ClientOptions::NO_START_SERVER)?.0; + Ok(Jack(Arc::new(RwLock::new(JackState::Inactive(client))))) + } + + pub fn run + Audio + Send + Sync + 'static> + (self, init: impl FnOnce(Self)->Usually) -> Usually>> + { + let client_state = self.0.clone(); + let app: Arc> = Arc::new(RwLock::new(init(self)?)); + let mut state = Activating; + std::mem::swap(&mut*client_state.write().unwrap(), &mut state); + if let Inactive(client) = state { + // This is the misc notifications handler. It's a struct that wraps a [Box] + // which performs type erasure on a callback that takes [JackEvent], which is + // one of the available misc notifications. + let notify = JackNotify(Box::new({ + let app = app.clone(); + move|event|(&mut*app.write().unwrap()).handle(event) + }) as BoxedJackEventHandler); + // This is the main processing handler. It's a struct that wraps a [Box] + // which performs type erasure on a callback that takes [Client] and [ProcessScope] + // and passes them down to the `app`'s `process` callback, which in turn + // implements audio and MIDI input and output on a realtime basis. + let process = ::jack::contrib::ClosureProcessHandler::new(Box::new({ + let app = app.clone(); + move|c: &_, s: &_|if let Ok(mut app) = app.write() { + app.process(c, s) + } else { + Control::Quit + } + }) as BoxedAudioHandler); + // Launch a client with the two handlers. + *client_state.write().unwrap() = Active( + client.activate_async(notify, process)? + ); + } else { + unreachable!(); + } + Ok(app) + } + + /// Run something with the client. + pub fn with_client (&self, op: impl FnOnce(&Client)->T) -> T { + match &*self.0.read().unwrap() { + Inert => panic!("jack client not activated"), + Inactive(client) => op(client), + Activating => panic!("jack client has not finished activation"), + Active(client) => op(client.as_client()), + } + } +} + +impl<'j> HasJack<'j> for Jack<'j> { + fn jack (&self) -> &Jack<'j> { + self + } +} + +impl<'j> HasJack<'j> for &Jack<'j> { + fn jack (&self) -> &Jack<'j> { + self + } +} + +impl<'j, T: HasJack<'j>> HasJack<'j> for Arc { + fn jack (&self) -> &Jack<'j> { + (&**self).jack() + } +} diff --git a/src/sing/jack_event.rs b/src/sing/jack_event.rs index e69de29..cfeae6c 100644 --- a/src/sing/jack_event.rs +++ b/src/sing/jack_event.rs @@ -0,0 +1,72 @@ +use crate::{*, time::PerfModel}; +pub use ::jack::{*, contrib::{*, ClosureProcessHandler}}; + +/// Event enum for JACK events. +/// +/// ``` +/// let event = tengri::JackEvent::XRun; // kerpop +/// ``` +#[derive(Debug, Clone, PartialEq)] pub enum JackEvent { + ThreadInit, + Shutdown(ClientStatus, Arc), + Freewheel(bool), + SampleRate(Frames), + ClientRegistration(Arc, bool), + PortRegistration(PortId, bool), + PortRename(PortId, Arc, Arc), + PortsConnected(PortId, PortId, bool), + GraphReorder, + XRun, +} + +/// Generic notification handler that emits [JackEvent] +/// +/// ``` +/// let notify = tengri::JackNotify(|_|{}); +/// ``` +pub struct JackNotify(pub T); + +/// Notification handler wrapper for [BoxedJackEventHandler]. +pub type DynamicNotifications<'j> = + JackNotify>; + +/// Boxed [JackEvent] callback. +pub type BoxedJackEventHandler<'j> = + Box; + +impl NotificationHandler for JackNotify { + fn thread_init(&self, _: &Client) { + self.0(JackEvent::ThreadInit); + } + unsafe fn shutdown(&mut self, status: ClientStatus, reason: &str) { + self.0(JackEvent::Shutdown(status, reason.into())); + } + fn freewheel(&mut self, _: &Client, enabled: bool) { + self.0(JackEvent::Freewheel(enabled)); + } + fn sample_rate(&mut self, _: &Client, frames: Frames) -> Control { + self.0(JackEvent::SampleRate(frames)); + Control::Quit + } + fn client_registration(&mut self, _: &Client, name: &str, reg: bool) { + self.0(JackEvent::ClientRegistration(name.into(), reg)); + } + fn port_registration(&mut self, _: &Client, id: PortId, reg: bool) { + self.0(JackEvent::PortRegistration(id, reg)); + } + fn port_rename(&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control { + self.0(JackEvent::PortRename(id, old.into(), new.into())); + Control::Continue + } + fn ports_connected(&mut self, _: &Client, a: PortId, b: PortId, are: bool) { + self.0(JackEvent::PortsConnected(a, b, are)); + } + fn graph_reorder(&mut self, _: &Client) -> Control { + self.0(JackEvent::GraphReorder); + Control::Continue + } + fn xrun(&mut self, _: &Client) -> Control { + self.0(JackEvent::XRun); + Control::Continue + } +} diff --git a/src/sing/jack_perf.rs b/src/sing/jack_perf.rs index e69de29..ee0d7d6 100644 --- a/src/sing/jack_perf.rs +++ b/src/sing/jack_perf.rs @@ -0,0 +1,22 @@ +use crate::{*, time::PerfModel}; +pub use ::jack::{*, contrib::{*, ClosureProcessHandler}}; + +pub trait JackPerfModel { + fn update_from_jack_scope (&self, t0: Option, scope: &ProcessScope); +} + +impl JackPerfModel for PerfModel { + fn update_from_jack_scope (&self, t0: Option, scope: &ProcessScope) { + if let Some(t0) = t0 { + let t1 = self.clock.raw(); + self.used.store( + self.clock.delta_as_nanos(t0, t1) as f64, + Relaxed, + ); + self.window.store( + scope.cycle_times().unwrap().period_usecs as f64, + Relaxed, + ); + } + } +} diff --git a/src/term.rs b/src/term.rs index 7feaa97..a289e66 100644 --- a/src/term.rs +++ b/src/term.rs @@ -1,72 +1,11 @@ -#[macro_export] macro_rules! tui_app { - ($Struct:ident { $($fields:tt)* }) => { - #[dizzle::namespace(bool)] - #[dizzle::namespace(u16)] - #[dizzle::namespace(Option)] - #[dizzle::namespace(Color)] - #[derive(Debug, Default)] - pub struct $Struct { $($fields)* } - tui_main!($Struct { ..Default::default() }); - } -} - -/// Implement standard [main] entrypoint for TUI apps. -#[macro_export] macro_rules! tui_main { - ($state:expr) => { - pub fn main () -> Usually<()> { - Tui::setup_panic(); - Tui::run_main(Arc::new(RwLock::new($state))) - } - } -} - -/// Enable TUI output for state struct. -#[macro_export] macro_rules! tui_view { - ($self:ident: $State:ty $body:block) => { - impl View for $State { - fn view (&$self) -> impl Draw $body - } - } -} - -#[macro_export] macro_rules! tui_interpret { - ($self:ident: $State:ident, $to:pat, $pat:ident -> $Result:ty { $($body:tt)+ }) => { - impl Interpret for $State { - fn interpret_word <'a> (&'a $self, $to: &mut Tui, $pat: &'a impl Symbol) -> Usually<$Result> { - $($body)+ - } - fn interpret_expr <'a> (&'a self, to: &mut Tui, src: &'a impl Expression) -> Usually<$Result> { - Ok(Some(if let Some(area) = eval_view(self, to, src)? { - area - } else if let Some(area) = eval_view_tui(self, to, src)? { - area - } else { - return Err(format!("App::interpret_expr: unexpected: {src:?}").into()) - })) - } - } - }; -} - -/// Enable TUI keyboard input for main state struct. -#[macro_export] macro_rules! tui_keys { - ($self:ident:$State:ty,$input:ident $($body:tt)+) => { - impl Apply> for $State { - fn apply (&mut $self, $input: &TuiEvent) -> Usually<()> $($body)+ - } - }; -} - use crate::{*, lang::*}; + mod border; pub use self::border::*; mod event; pub use self::event::*; mod keys; pub use self::keys::*; mod buffer; pub use self::buffer::*; -mod repeat; pub use self::repeat::*; -mod scroll; pub use self::scroll::*; -mod colors; pub use self::colors::*; -mod phat; pub use self::phat::*; -mod button; pub use self::button::*; +mod input; pub use self::input::*; +mod output; pub use self::output::*; //use unicode_width::{UnicodeWidthStr, UnicodeWidthChar}; //use rand::distributions::uniform::UniformSampler; @@ -93,13 +32,6 @@ pub(crate) use ::{ }, }; -impl Deref for Tui { type Target = Buffer; fn deref (&self) -> &Buffer { &self.0 } } -impl DerefMut for Tui { fn deref_mut (&mut self) -> &mut Buffer { &mut self.0 } } -impl AsMut for Tui { fn as_mut (&mut self) -> &mut Buffer { &mut self.0 } } -impl Wide for Tui { fn w (&self) -> u16 { self.1.2 } } -impl Tall for Tui { fn h (&self) -> u16 { self.1.3 } } -impl HasOrigin for Tui { fn origin (&self) -> Azimuth { Azimuth::NW } } -impl Xy for Tui { fn x (&self) -> u16 { self.1.0 } fn y (&self) -> u16 { self.1.1 } } /// Terminal output. pub struct Tui( /// Ratatui buffer; area is screen size @@ -107,66 +39,16 @@ pub struct Tui( /// Current draw area pub XYWH ); + +impl Deref for Tui { type Target = Buffer; fn deref (&self) -> &Buffer { &self.0 } } +impl DerefMut for Tui { fn deref_mut (&mut self) -> &mut Buffer { &mut self.0 } } +impl AsMut for Tui { fn as_mut (&mut self) -> &mut Buffer { &mut self.0 } } +impl Wide for Tui { fn w (&self) -> u16 { self.1.2 } } +impl Tall for Tui { fn h (&self) -> u16 { self.1.3 } } +impl HasOrigin for Tui { fn origin (&self) -> Azimuth { Azimuth::NW } } +impl Xy for Tui { fn x (&self) -> u16 { self.1.0 } fn y (&self) -> u16 { self.1.1 } } + impl Tui { - pub fn setup_panic () { - use ::std::panic::{set_hook, PanicHookInfo}; - use ::better_panic::{Settings, Verbosity}; - let panic = Settings::auto() - .verbosity(Verbosity::Full) - .create_panic_handler(); - set_hook(Box::new(move |info: &PanicHookInfo|{ - let _ = Tui::teardown(&mut stdout()); - panic(info); - })); - } - pub fn run_main (state: Arc>) -> Usually<()> where - T: View + Apply> + Send + Sync + 'static - { - Exit::run(|exit|{ - let scan = Duration::from_millis(100); - let frame = Duration::from_millis(10); - let (_input, output) = Tui::io(exit.as_ref(), &state, scan, frame, std::io::stdout())?; - let _ = output.join(); - Tui::teardown(&mut stdout()) - }) - } - /// Spawn the TUI input and output threadsl. - pub fn io < - T: View + Apply> + Send + Sync + 'static, - W: Write + Send + Sync + 'static, - > ( - exited: &Arc, - state: &Arc>, - poll: Duration, - sleep: Duration, - output: W, - ) -> Result<(Task, Task), Box> { - Ok(( - Tui::input(exited, state, poll)?, - Tui::output(exited, state, sleep, output)?, - )) - } - /// Spawn the TUI input thread which reads keys from the terminal. - pub fn input > + Send + Sync + 'static> ( - exited: &Arc, state: &Arc>, poll: Duration - ) -> Result { - let exited = exited.clone(); - let state = state.clone(); - Task::new_poll(exited.clone(), poll, move |_| { - let event = read().unwrap(); - if Exit::is(&event) { - exited.store(true, Relaxed); - } else if let Err(e) = state.write().unwrap().apply(&TuiEvent(event)) { - panic!("{e}") - } - }) - } - pub fn teardown (backend: &mut W) -> Usually<()> { - use ::ratatui::backend::Backend; - stdout().execute(LeaveAlternateScreen)?; - CrosstermBackend::new(backend).show_cursor()?; - disable_raw_mode().map_err(Into::into) - } pub fn new (width: u16, height: u16) -> Self { Self(Buffer::empty(Rect { x: 0, y: 0, width, height }), XYWH(0, 0, width, height)) } @@ -178,17 +60,16 @@ impl Tui { self.0.reset(); } } - pub fn redraw <'b, W: Write> ( - &'b mut self, - back: &mut CrosstermBackend, - mut next: &'b mut Self - ) { + pub fn redraw <'b, W: Write> (&'b mut self, back: &mut CrosstermBackend, mut next: &'b mut Self) { let updates = self.0.diff(&next.0); back.draw(updates.into_iter()).expect("failed to render"); Backend::flush(back).expect("failed to flush output new"); std::mem::swap(self, &mut next); next.0.reset(); } +} + +impl Tui { pub fn update (&mut self, callback: &impl Fn(&mut Cell, u16, u16)) -> XYWH { for row in 0..self.h() { let y = self.y() + row; @@ -203,13 +84,6 @@ impl Tui { } self.xywh() } - pub fn blit (&mut self, text: &impl AsRef, x: u16, y: u16, style: Option