mirror of
https://codeberg.org/unspeaker/tengri.git
synced 2026-09-18 13:26:42 +02:00
Compare commits
No commits in common. "8b24b052caac078f6d0a9d8fde1afa1fbd3b3f56" and "e398541b7efcb98f980c245827b9ce25ba3ae8b1" have entirely different histories.
8b24b052ca
...
e398541b7e
28 changed files with 940 additions and 1795 deletions
95
Cargo.lock
generated
95
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
7
Justfile
7
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
|
||||
|
|
|
|||
2
dizzle
2
dizzle
|
|
@ -1 +1 @@
|
|||
Subproject commit 4424ef7fc3fd8bfbea1fb55b4922f7a8cfd2a8ef
|
||||
Subproject commit 0050385e63ea5d3ab9be7c495dae5fc635396cf0
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
//! Mode 0: Direct draw
|
||||
use ::{std::sync::{Arc, RwLock}, ratatui::style::Color, tengri::*};
|
||||
fn main () {}
|
||||
|
|
@ -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<Action>,
|
||||
/** 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<Tui, Color> for State {
|
||||
fn interpret_expr (&self, to: &mut Tui, expr: &impl Language) -> Usually<Color> {
|
||||
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<Tui, Option<XYWH<u16>>> for State {
|
||||
fn interpret_word (&self, to: &mut Tui, sym: &impl Language) -> Perhaps<XYWH<u16>> {
|
||||
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<XYWH<u16>> {
|
||||
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<Action> {
|
||||
self.cursor = (self.cursor + 1) % VIEWS.len();
|
||||
Ok(Some(Action::Prev))
|
||||
}
|
||||
fn prev (&mut self) -> Perhaps<Action> {
|
||||
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<Self> {
|
||||
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) {}
|
||||
163
examples/tui_00.rs
Normal file
163
examples/tui_00.rs
Normal file
|
|
@ -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<Action>,
|
||||
/** User-controllable value. */
|
||||
cursor: usize,
|
||||
/** Rendered window size. */
|
||||
size: Sizer,
|
||||
}
|
||||
|
||||
impl Interpret<Tui, XYWH<u16>> 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<Self> {
|
||||
use Action::*;
|
||||
match self { Next => Self::next(state), Prev => Self::prev(state), }
|
||||
}
|
||||
|
||||
fn next (state: &mut State) -> Perhaps<Self> {
|
||||
state.cursor = (state.cursor + 1) % VIEWS.len();
|
||||
Ok(Some(Self::Prev))
|
||||
}
|
||||
|
||||
fn prev (state: &mut State) -> Perhaps<Self> {
|
||||
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 ]);
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
//! Mode 02
|
||||
use ::{std::sync::{Arc, RwLock}, ratatui::style::Color, tengri::*};
|
||||
fn main () {}
|
||||
|
||||
|
|
@ -50,6 +50,26 @@ pub fn view (meta: TokenStream, item: TokenStream) -> TokenStream {
|
|||
))
|
||||
}
|
||||
|
||||
pub(crate) fn write <T: ToTokens> (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 };
|
||||
|
|
|
|||
11
shell.nix
11
shell.nix
|
|
@ -1,14 +1,13 @@
|
|||
#!/usr/bin/env nix-shell
|
||||
{pkgs?import<nixpkgs>{}}: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
|
||||
|
|
|
|||
24
src/draw.rs
24
src/draw.rs
|
|
@ -7,22 +7,13 @@ use crate::*;
|
|||
/// struct TestOut { w: u16, h: u16 };
|
||||
/// impl Wide<u16> for TestOut {}
|
||||
/// impl Tall<u16> for TestOut {}
|
||||
/// impl Xy<u16> for TestOut {
|
||||
/// fn x (&self) -> u16 { 0 }
|
||||
/// fn y (&self) -> u16 { 0 }
|
||||
/// }
|
||||
/// impl Xy<u16> 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<Self>) -> Perhaps<XYWH<u16>>
|
||||
/// { println!("placed"); Ok(None) }
|
||||
/// fn area (&self) -> XYWH<Self::Unit>
|
||||
/// { Default::default() }
|
||||
/// fn clip <T> (
|
||||
/// &mut self,
|
||||
/// area: impl Into<Option<XYWH<u16>>>,
|
||||
/// draw: impl FnOnce(&mut Self)->T
|
||||
/// ) -> T
|
||||
/// { draw(self }
|
||||
/// fn show <D: Draw<Self>> (&mut self, _: D) -> Drawn<u16> {
|
||||
/// println!("placed");
|
||||
/// Ok(None)
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// impl_draw!(|self: String, to: TestOut|{
|
||||
|
|
@ -94,11 +85,6 @@ pub trait Draw<S: Screen> {
|
|||
}
|
||||
}
|
||||
|
||||
/// The opposite of [thunk]?
|
||||
pub fn draw <S: Screen, T: Draw<S>> (item: T) -> impl FnOnce(&mut S) -> Perhaps<XYWH<S::Unit>> {
|
||||
move|to: &mut S|item.draw(to)
|
||||
}
|
||||
|
||||
pub type Drawn<U> = Perhaps<XYWH<U>>;
|
||||
|
||||
impl<S: Screen> Draw<S> for () {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
use crate::*;
|
||||
|
||||
impl<S: Screen, T: Draw<S>> Layout<S> for T {}
|
||||
|
||||
pub trait Layout<S: Screen>: Draw<S> + Sized {
|
||||
fn full_w (self) -> impl Draw<S> {
|
||||
Full::W(self)
|
||||
|
|
@ -13,15 +11,12 @@ pub trait Layout<S: Screen>: Draw<S> + Sized {
|
|||
Full::WH(self)
|
||||
}
|
||||
|
||||
/// (bsp/e (exact/w 10 "Hello") "World")
|
||||
fn exact_w <N: Into<Option<S::Unit>>> (self, x: N) -> impl Draw<S> {
|
||||
Exact::W(self, x.into())
|
||||
}
|
||||
/// (bsp/s (exact/h 10 "Hello") "World")
|
||||
fn exact_h <N: Into<Option<S::Unit>>> (self, y: N) -> impl Draw<S> {
|
||||
Exact::H(self, y.into())
|
||||
}
|
||||
/// (exact/wh 10 2 "Hello World")
|
||||
fn exact_wh <N: Into<Option<S::Unit>>> (self, x: N, y: N) -> impl Draw<S> {
|
||||
Exact::WH(self, x.into(), y.into())
|
||||
}
|
||||
|
|
@ -60,7 +55,7 @@ pub trait Layout<S: Screen>: Draw<S> + Sized {
|
|||
Pull::X(self, x.into())
|
||||
}
|
||||
fn pull_y <N: Into<Option<S::Unit>>> (self, y: N) -> impl Draw<S> {
|
||||
Pull::Y(self, y.into())
|
||||
Pull::X(self, y.into())
|
||||
}
|
||||
fn pull_xy <N: Into<Option<S::Unit>>> (self, x: N, y: N) -> impl Draw<S> {
|
||||
Pull::XY(self, x.into(), y.into())
|
||||
|
|
@ -70,7 +65,7 @@ pub trait Layout<S: Screen>: Draw<S> + Sized {
|
|||
Push::X(self, x.into())
|
||||
}
|
||||
fn push_y <N: Into<Option<S::Unit>>> (self, y: N) -> impl Draw<S> {
|
||||
Push::Y(self, y.into())
|
||||
Push::X(self, y.into())
|
||||
}
|
||||
fn push_xy <N: Into<Option<S::Unit>>> (self, x: N, y: N) -> impl Draw<S> {
|
||||
Push::XY(self, x.into(), y.into())
|
||||
|
|
@ -155,6 +150,8 @@ pub trait Layout<S: Screen>: Draw<S> + Sized {
|
|||
}
|
||||
}
|
||||
|
||||
impl<S: Screen, T: Draw<S>> Layout<S> for T {}
|
||||
|
||||
/// Use whole drawing area along one or both axes.
|
||||
///
|
||||
/// ```
|
||||
|
|
@ -282,24 +279,8 @@ pub enum Max<T: Screen, I: Draw<T>, X: Into<Option<T::Unit>>> {
|
|||
WH(I, X, X),
|
||||
}
|
||||
|
||||
impl_draw!(<T: Screen, I: Draw<T>, X: Into<Option<T::Unit>>,>|self: Max<T, I, X>, to: T|{
|
||||
let area: XYWH<T::Unit> = 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!(<T: Screen, I: Draw<T>, X: Into<Option<T::Unit>>,>|self: Max<T, I, X>, _to: T|{
|
||||
todo!()
|
||||
});
|
||||
|
||||
/// Set size of of drawing area.
|
||||
|
|
@ -318,17 +299,19 @@ pub enum Exact<T: Screen, I: Draw<T>, X: Into<Option<T::Unit>>> {
|
|||
}
|
||||
|
||||
impl_draw!(<T: Screen, I: Draw<T>, X: Into<Option<T::Unit>>,>|self: Exact<T, I, X>, to: T|{
|
||||
let area: XYWH<T::Unit> = 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.
|
||||
|
|
|
|||
|
|
@ -37,23 +37,6 @@ pub const fn below <S: Screen, A: Draw<S>, B: Draw<S>> (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 <S: Screen, A: Draw<S>, B: Draw<S>> (&self, a: A, b: B) -> impl Draw<S> {
|
||||
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 <S: Screen, A: Draw<S>, B: Draw<S>> (&self, a: A, b: B) -> impl Draw<S> {
|
||||
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 <S: Screen> (
|
||||
split: &Split,
|
||||
to: &mut S,
|
||||
a: impl Draw<S>,
|
||||
area_a: impl Into<Option<XYWH<S::Unit>>>,
|
||||
origin_a: impl Into<Option<Azimuth>>,
|
||||
b: impl Draw<S>,
|
||||
area_b: impl Into<Option<XYWH<S::Unit>>>,
|
||||
origin_b: impl Into<Option<Azimuth>>,
|
||||
) -> Usually<(Option<XYWH<S::Unit>>, Option<XYWH<S::Unit>>)> {
|
||||
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 <S: Screen> (
|
||||
split: &Split,
|
||||
area: XYWH<S::Unit>,
|
||||
a: &impl Draw<S>,
|
||||
b: &impl Draw<S>,
|
||||
) -> Usually<(Option<XYWH<S::Unit>>, Option<XYWH<S::Unit>>)> {
|
||||
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 <S: Coord> (
|
||||
split: &Split,
|
||||
drawn_a: Option<XYWH<S>>,
|
||||
drawn_b: Option<XYWH<S>>,
|
||||
) -> Option<XYWH<S>> {
|
||||
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,)*)) };
|
||||
|
|
|
|||
40
src/eval.rs
40
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<XYWH<u16>>: 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<XYWH<u16>>{
|
||||
/// 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)
|
||||
|
|
|
|||
10
src/exit.rs
10
src/exit.rs
|
|
@ -1,5 +1,5 @@
|
|||
use crate::*;
|
||||
use std::sync::{Arc, atomic::AtomicBool};
|
||||
use crate::Usually;
|
||||
|
||||
#[derive(Clone)] pub struct Exit(Arc<AtomicBool>);
|
||||
|
||||
|
|
@ -7,14 +7,6 @@ impl Exit {
|
|||
pub fn run <T> (run: impl FnOnce(Self)->Usually<T>) -> Usually<T> {
|
||||
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<Arc<AtomicBool>> for Exit {
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
)*
|
||||
)*
|
||||
|
|
|
|||
813
src/sing.rs
813
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<RwLock<JackState<'j>>>
|
||||
);
|
||||
|
||||
/// 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 <T: HasJack<'j> + Audio + Send + Sync + 'static> (
|
||||
name: impl AsRef<str>,
|
||||
init: impl FnOnce(Jack<'j>)->Usually<T>
|
||||
) -> Usually<Arc<RwLock<T>>> {
|
||||
Jack::new(name)?.run(init)
|
||||
}
|
||||
pub fn new (name: impl AsRef<str>) -> Usually<Self> {
|
||||
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 <T> (&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 <T: HasJack<'j> + Audio + Send + Sync + 'static>
|
||||
(self, init: impl FnOnce(Self)->Usually<T>) -> Usually<Arc<RwLock<T>>>
|
||||
{
|
||||
let client_state = self.0.clone();
|
||||
let app: Arc<RwLock<T>> = 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<T> {
|
||||
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<str>),
|
||||
Freewheel(bool),
|
||||
SampleRate(Frames),
|
||||
ClientRegistration(Arc<str>, bool),
|
||||
PortRegistration(PortId, bool),
|
||||
PortRename(PortId, Arc<str>, Arc<str>),
|
||||
PortsConnected(PortId, PortId, bool),
|
||||
GraphReorder,
|
||||
XRun,
|
||||
}
|
||||
|
||||
/// Generic notification handler that emits [JackEvent]
|
||||
///
|
||||
/// ```
|
||||
/// let notify = tengri::JackNotify(|_|{});
|
||||
/// ```
|
||||
pub struct JackNotify<T: Fn(JackEvent) + Send>(pub T);
|
||||
|
||||
/// Notification handler wrapper for [BoxedJackEventHandler].
|
||||
pub type DynamicNotifications<'j> =
|
||||
JackNotify<BoxedJackEventHandler<'j>>;
|
||||
|
||||
/// Boxed [JackEvent] callback.
|
||||
pub type BoxedJackEventHandler<'j> =
|
||||
Box<dyn Fn(JackEvent) + Send + Sync + 'j>;
|
||||
|
||||
impl<T: Fn(JackEvent) + Send> NotificationHandler for JackNotify<T> {
|
||||
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<u64>, scope: &ProcessScope);
|
||||
}
|
||||
|
||||
impl JackPerfModel for PerfModel {
|
||||
fn update_from_jack_scope (&self, t0: Option<u64>, 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<str>, connect: &[Connect]) -> Usually<MidiInput>;
|
||||
/// Register a MIDI output port.
|
||||
fn midi_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiOutput>;
|
||||
/// Register an audio input port.
|
||||
fn audio_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioInput>;
|
||||
/// Register an audio output port.
|
||||
fn audio_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioOutput>;
|
||||
}
|
||||
|
||||
impl<J: HasJack<'static>> JackPorts for J {
|
||||
fn midi_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiInput> {
|
||||
MidiInput::new(self.jack(), name, connect)
|
||||
}
|
||||
fn midi_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiOutput> {
|
||||
MidiOutput::new(self.jack(), name, connect)
|
||||
}
|
||||
fn audio_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioInput> {
|
||||
AudioInput::new(self.jack(), name, connect)
|
||||
}
|
||||
fn audio_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioOutput> {
|
||||
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<str>, connect: &[Connect])
|
||||
-> Usually<Self> where Self: Sized;
|
||||
|
||||
fn register (jack: &Jack<'static>, name: &impl AsRef<str>) -> Usually<Port<Self::Port>> {
|
||||
jack.with_client(|c|c.register_port::<Self::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<Self::Port> where Self: Sized;
|
||||
fn port_name (&self) -> &Arc<str>;
|
||||
fn port (&self) -> &Port<Self::Port>;
|
||||
fn port_mut (&mut self) -> &mut Port<Self::Port>;
|
||||
fn ports (&self, re_name: Option<&str>, re_type: Option<&str>, flags: PortFlags) -> Vec<String> {
|
||||
self.with_client(|c|c.ports(re_name, re_type, flags))
|
||||
}
|
||||
fn port_by_id (&self, id: u32) -> Option<Port<Unowned>> {
|
||||
self.with_client(|c|c.port_by_id(id))
|
||||
}
|
||||
fn port_by_name (&self, name: impl AsRef<str>) -> Option<Port<Unowned>> {
|
||||
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<Vec<(Port<Unowned>, Arc<str>, 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<ConnectScope>
|
||||
) -> Usually<Vec<(Port<Unowned>, Arc<str>, 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<str>) -> Usually<ConnectStatus> {
|
||||
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<Unowned>) -> Usually<ConnectStatus> {
|
||||
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<Self::Pair>) -> Usually<ConnectStatus> {
|
||||
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<str>,
|
||||
/// Port handle.
|
||||
pub port: Port<AudioIn>,
|
||||
/// List of ports to connect to.
|
||||
pub connections: Vec<Connect>,
|
||||
}
|
||||
|
||||
/// 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<str>,
|
||||
/// Port handle.
|
||||
pub port: Port<AudioOut>,
|
||||
/// List of ports to connect to.
|
||||
pub connections: Vec<Connect>,
|
||||
}
|
||||
|
||||
/// 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<str>,
|
||||
/// Port handle.
|
||||
pub port: Port<MidiIn>,
|
||||
/// List of currently held notes.
|
||||
pub held: Arc<RwLock<[bool;128]>>,
|
||||
/// List of ports to connect to.
|
||||
pub connections: Vec<Connect>,
|
||||
}
|
||||
|
||||
/// 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<str>,
|
||||
/// Port handle.
|
||||
pub port: Port<MidiOut>,
|
||||
/// List of currently held notes.
|
||||
pub held: Arc<RwLock<[bool;128]>>,
|
||||
/// List of ports to connect to.
|
||||
pub connections: Vec<Connect>,
|
||||
/// Buffer
|
||||
pub note_buffer: Vec<u8>,
|
||||
/// Buffer
|
||||
pub output_buffer: Vec<Vec<Vec<u8>>>,
|
||||
}
|
||||
|
||||
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<str> {
|
||||
&self.name
|
||||
}
|
||||
fn port (&self) -> &Port<Self::Port> {
|
||||
&self.port
|
||||
}
|
||||
fn port_mut (&mut self) -> &mut Port<Self::Port> {
|
||||
&mut self.port
|
||||
}
|
||||
fn into_port (self) -> 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<str>, connect: &[Connect])
|
||||
-> Usually<Self> 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<str>, connect: &[Connect])
|
||||
-> Usually<Self> 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<str>, connect: &[Connect])
|
||||
-> Usually<Self> 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<str>, connect: &[Connect])
|
||||
-> Usually<Self> 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<Vec<(u32, Result<LiveEvent<'a>, MidiError>)>>;
|
||||
|
||||
/// Trait for thing that may receive MIDI.
|
||||
pub trait HasMidiIns {
|
||||
fn midi_ins (&self) -> &Vec<MidiInput>;
|
||||
fn midi_ins_mut (&mut self) -> &mut Vec<MidiInput>;
|
||||
/// 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::<Vec<_>>())
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
fn midi_ins_with_sizes <'a> (&'a self) ->
|
||||
impl Iterator<Item=(usize, &'a Arc<str>, &'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<MidiOutput>;
|
||||
fn midi_outs_mut (&mut self) -> &mut Vec<MidiOutput>;
|
||||
fn midi_outs_with_sizes <'a> (&'a self) ->
|
||||
impl Iterator<Item=(usize, &'a Arc<str>, &'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<Item=(usize, LiveEvent<'a>, &'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<dyn Iterator<Item=(usize, LiveEvent<'a>, &'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<Vec<u8>>]) {
|
||||
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<T: AsRef<Vec<MidiInput>> + AsMut<Vec<MidiInput>>> HasMidiIns for T {
|
||||
fn midi_ins (&self) -> &Vec<MidiInput> { self.as_ref() }
|
||||
fn midi_ins_mut (&mut self) -> &mut Vec<MidiInput> { self.as_mut() }
|
||||
}
|
||||
|
||||
impl<T: AsRef<Vec<MidiOutput>> + AsMut<Vec<MidiOutput>>> HasMidiOuts for T {
|
||||
fn midi_outs (&self) -> &Vec<MidiOutput> { self.as_ref() }
|
||||
fn midi_outs_mut (&mut self) -> &mut Vec<MidiOutput> { self.as_mut() }
|
||||
}
|
||||
|
||||
impl<T: HasMidiIns + HasJack<'static>> 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<T: HasMidiOuts + HasJack<'static>> 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<str>),
|
||||
/** Match regular expression */
|
||||
RegExp(Arc<str>),
|
||||
}
|
||||
|
||||
#[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<ConnectName>,
|
||||
pub scope: Option<ConnectScope>,
|
||||
pub status: Arc<RwLock<Vec<(Port<Unowned>, Arc<str>, ConnectStatus)>>>,
|
||||
pub info: Arc<str>,
|
||||
}
|
||||
|
||||
impl Connect {
|
||||
|
||||
pub fn new <T: AsRef<str>> (
|
||||
exact: Option<impl Iterator<Item = T>>,
|
||||
re: Option<impl Iterator<Item = T>>,
|
||||
re_all: Option<impl Iterator<Item = T>>,
|
||||
) -> Vec<Self> {
|
||||
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 <T: AsRef<str>> (
|
||||
jack: &Jack<'static>,
|
||||
name: &T,
|
||||
midi_from: &[T],
|
||||
midi_from_re: Option<&[T]>,
|
||||
) -> Usually<Vec<MidiInput>> {
|
||||
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::<Result<_, _>>()?)
|
||||
}
|
||||
|
||||
pub fn midi_outs <T: AsRef<str>> (
|
||||
jack: &Jack<'static>,
|
||||
name: &T,
|
||||
midi_to: &[T],
|
||||
midi_to_re: Option<&[T]>,
|
||||
) -> Usually<Vec<MidiOutput>> {
|
||||
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::<Result<_, _>>()?)
|
||||
}
|
||||
|
||||
pub fn audio_ins <T: AsRef<str>> (
|
||||
jack: &Jack<'static>,
|
||||
name: &T,
|
||||
audio_from: &[T],
|
||||
audio_from_re: Option<&[T]>,
|
||||
) -> Usually<Vec<AudioInput>> {
|
||||
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::<Result<_, _>>()?)
|
||||
}
|
||||
|
||||
pub fn audio_outs <T: AsRef<str>> (
|
||||
jack: &Jack<'static>,
|
||||
name: &T,
|
||||
audio_to: &[T],
|
||||
audio_to_re: Option<&[T]>,
|
||||
) -> Usually<Vec<AudioOutput>> {
|
||||
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::<Result<_, _>>()?)
|
||||
}
|
||||
|
||||
/// Connect to this exact port
|
||||
pub fn exact (name: impl AsRef<str>) -> 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<str>) -> 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<str>) -> 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<str> {
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
112
src/sing/jack.rs
112
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<RwLock<JackState<'j>>>
|
||||
);
|
||||
|
||||
/// 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 <T: HasJack<'j> + Audio + Send + Sync + 'static> (
|
||||
name: impl AsRef<str>,
|
||||
init: impl FnOnce(Jack<'j>)->Usually<T>
|
||||
) -> Usually<Arc<RwLock<T>>> {
|
||||
Jack::new(name)?.run(init)
|
||||
}
|
||||
|
||||
pub fn new (name: impl AsRef<str>) -> Usually<Self> {
|
||||
let client = Client::new(name.as_ref(), ClientOptions::NO_START_SERVER)?.0;
|
||||
Ok(Jack(Arc::new(RwLock::new(JackState::Inactive(client)))))
|
||||
}
|
||||
|
||||
pub fn run <T: HasJack<'j> + Audio + Send + Sync + 'static>
|
||||
(self, init: impl FnOnce(Self)->Usually<T>) -> Usually<Arc<RwLock<T>>>
|
||||
{
|
||||
let client_state = self.0.clone();
|
||||
let app: Arc<RwLock<T>> = 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 <T> (&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<T> {
|
||||
fn jack (&self) -> &Jack<'j> {
|
||||
(&**self).jack()
|
||||
}
|
||||
}
|
||||
|
|
@ -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<str>),
|
||||
Freewheel(bool),
|
||||
SampleRate(Frames),
|
||||
ClientRegistration(Arc<str>, bool),
|
||||
PortRegistration(PortId, bool),
|
||||
PortRename(PortId, Arc<str>, Arc<str>),
|
||||
PortsConnected(PortId, PortId, bool),
|
||||
GraphReorder,
|
||||
XRun,
|
||||
}
|
||||
|
||||
/// Generic notification handler that emits [JackEvent]
|
||||
///
|
||||
/// ```
|
||||
/// let notify = tengri::JackNotify(|_|{});
|
||||
/// ```
|
||||
pub struct JackNotify<T: Fn(JackEvent) + Send>(pub T);
|
||||
|
||||
/// Notification handler wrapper for [BoxedJackEventHandler].
|
||||
pub type DynamicNotifications<'j> =
|
||||
JackNotify<BoxedJackEventHandler<'j>>;
|
||||
|
||||
/// Boxed [JackEvent] callback.
|
||||
pub type BoxedJackEventHandler<'j> =
|
||||
Box<dyn Fn(JackEvent) + Send + Sync + 'j>;
|
||||
|
||||
impl<T: Fn(JackEvent) + Send> NotificationHandler for JackNotify<T> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
use crate::{*, time::PerfModel};
|
||||
pub use ::jack::{*, contrib::{*, ClosureProcessHandler}};
|
||||
|
||||
pub trait JackPerfModel {
|
||||
fn update_from_jack_scope (&self, t0: Option<u64>, scope: &ProcessScope);
|
||||
}
|
||||
|
||||
impl JackPerfModel for PerfModel {
|
||||
fn update_from_jack_scope (&self, t0: Option<u64>, 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
326
src/term.rs
326
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<u16>)]
|
||||
#[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<Tui> for $State {
|
||||
fn view (&$self) -> impl Draw<Tui> $body
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! tui_interpret {
|
||||
($self:ident: $State:ident, $to:pat, $pat:ident -> $Result:ty { $($body:tt)+ }) => {
|
||||
impl Interpret<Tui, $Result> 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<TuiEvent, Usually<()>> 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<Buffer> for Tui { fn as_mut (&mut self) -> &mut Buffer { &mut self.0 } }
|
||||
impl Wide<u16> for Tui { fn w (&self) -> u16 { self.1.2 } }
|
||||
impl Tall<u16> for Tui { fn h (&self) -> u16 { self.1.3 } }
|
||||
impl HasOrigin for Tui { fn origin (&self) -> Azimuth { Azimuth::NW } }
|
||||
impl Xy<u16> 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<u16>
|
||||
);
|
||||
|
||||
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<Buffer> for Tui { fn as_mut (&mut self) -> &mut Buffer { &mut self.0 } }
|
||||
impl Wide<u16> for Tui { fn w (&self) -> u16 { self.1.2 } }
|
||||
impl Tall<u16> for Tui { fn h (&self) -> u16 { self.1.3 } }
|
||||
impl HasOrigin for Tui { fn origin (&self) -> Azimuth { Azimuth::NW } }
|
||||
impl Xy<u16> 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 <T> (state: Arc<RwLock<T>>) -> Usually<()> where
|
||||
T: View<Tui> + Apply<TuiEvent, Usually<()>> + 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<Tui> + Apply<TuiEvent, Usually<()>> + Send + Sync + 'static,
|
||||
W: Write + Send + Sync + 'static,
|
||||
> (
|
||||
exited: &Arc<AtomicBool>,
|
||||
state: &Arc<RwLock<T>>,
|
||||
poll: Duration,
|
||||
sleep: Duration,
|
||||
output: W,
|
||||
) -> Result<(Task, Task), Box<dyn std::error::Error>> {
|
||||
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 <T: Apply<TuiEvent, Usually<()>> + Send + Sync + 'static> (
|
||||
exited: &Arc<AtomicBool>, state: &Arc<RwLock<T>>, poll: Duration
|
||||
) -> Result<Task, std::io::Error> {
|
||||
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 <W: Write> (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<W>,
|
||||
mut next: &'b mut Self
|
||||
) {
|
||||
pub fn redraw <'b, W: Write> (&'b mut self, back: &mut CrosstermBackend<W>, 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<u16> {
|
||||
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<str>, x: u16, y: u16, style: Option<Style>) {
|
||||
let text = text.as_ref();
|
||||
let style = style.unwrap_or(Style::default());
|
||||
if x < self.0.area.width && y < self.0.area.height {
|
||||
self.0.set_string(x, y, text, style);
|
||||
}
|
||||
}
|
||||
pub fn tint_all (&mut self, fg: Color, bg: Color, modifier: Modifier) {
|
||||
for cell in self.0.content.iter_mut() {
|
||||
cell.fg = fg;
|
||||
|
|
@ -217,133 +91,65 @@ impl Tui {
|
|||
cell.modifier = modifier;
|
||||
}
|
||||
}
|
||||
/// Spawn the TUI output thread which writes colored characters to the terminal.
|
||||
///
|
||||
/// ```
|
||||
/// let state = std::sync::Arc::new(std::sync::RwLock::new(()));
|
||||
/// let _ = tengri::Exit::run(|exit|{
|
||||
/// tengri::Tui::output(
|
||||
/// exit.as_ref(),
|
||||
/// &state,
|
||||
/// std::time::Duration::from_millis(10),
|
||||
/// std::io::stdout()
|
||||
/// )
|
||||
/// });
|
||||
/// ```
|
||||
pub fn output <
|
||||
W: Write + Send + Sync + 'static, T: View<Tui> + Send + Sync + 'static
|
||||
> (
|
||||
exited: &Arc<AtomicBool>,
|
||||
state: &Arc<RwLock<T>>,
|
||||
sleep: Duration,
|
||||
output: W,
|
||||
) -> Usually<Task> {
|
||||
let state = state.clone();
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
CrosstermBackend::new(stdout()).hide_cursor()?;
|
||||
enable_raw_mode()?;
|
||||
let mut backend = CrosstermBackend::new(output);
|
||||
let Size { width, height } = backend.size().expect("get size failed");
|
||||
let mut prev = Tui::new(width, height);
|
||||
let mut next = Tui::new(width, height);
|
||||
Ok(Task::new_sleep(exited.clone(), sleep, move |perf| {
|
||||
let Size { width, height } = backend.size().expect("get size failed");
|
||||
if let Ok(state) = state.try_read() {
|
||||
prev.resize(&mut backend, width, height);
|
||||
state.view().draw(&mut next).expect("draw failed"); // TODO draw error
|
||||
prev.redraw(&mut backend, &mut next);
|
||||
}
|
||||
let timer = format!("{:>3.3}ms", perf.used.load(Relaxed));
|
||||
prev.set_string(0, 0, &timer, Style::default());
|
||||
})?)
|
||||
}
|
||||
/// Draw TUI content or its error message.
|
||||
///
|
||||
/// ```
|
||||
/// for variant in [
|
||||
/// Ok(Some("hello")),
|
||||
/// Ok(None),
|
||||
/// Err("fail".into()),
|
||||
/// ] {
|
||||
/// let _ = tengri::Tui::catcher(variant);
|
||||
/// }
|
||||
/// ```
|
||||
pub fn catcher <T: Draw<Tui>> (result: Usually<T>) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|match result {
|
||||
Ok(content) => content.draw(to),
|
||||
Err(e) => {
|
||||
let err_fg = Color::Rgb(255,224,244);
|
||||
let err_bg = Color::Rgb(96, 24, 24);
|
||||
let title = east(bold(true, "upsi daisy. "), "rendering failed.");
|
||||
let error = east("\"why?\" ", bold(true, format!("{e}")));
|
||||
fg(err_fg, bg(err_bg, south(title, error))).draw(to)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
impl Screen for Tui {
|
||||
type Unit = u16;
|
||||
/// Render drawable in subarea specified by `area`
|
||||
fn show (&mut self, content: impl Draw<Self>) -> Perhaps<XYWH<u16>> {
|
||||
let previous_area = self.1;
|
||||
Ok(if let Some(area) = content.layout(self.1)? {
|
||||
self.1 = area;
|
||||
if let Some(result_area) = content.draw(self)? {
|
||||
self.1 = previous_area;
|
||||
Some(result_area)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
/// Get current clipping area
|
||||
fn area (&self) -> XYWH<Self::Unit> {
|
||||
self.1
|
||||
}
|
||||
|
||||
fn clip <T> (
|
||||
&mut self,
|
||||
area: impl Into<Option<XYWH<u16>>>,
|
||||
draw: impl FnOnce(&mut Self)->T
|
||||
) -> T {
|
||||
let prev = self.1;
|
||||
if let Some(area) = area.into() {
|
||||
self.1 = area.into();
|
||||
pub fn blit (&mut self, text: &impl AsRef<str>, x: u16, y: u16, style: Option<Style>) {
|
||||
let text = text.as_ref();
|
||||
let style = style.unwrap_or(Style::default());
|
||||
if x < self.0.area.width && y < self.0.area.height {
|
||||
self.0.set_string(x, y, text, style);
|
||||
}
|
||||
let result = draw(self);
|
||||
self.1 = prev;
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn fill_char (c: char) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|Ok(Some(to.update(&|cell,_,_|{
|
||||
cell.set_char(c);
|
||||
}))))
|
||||
/// Implement standard [main] entrypoint for TUI apps.
|
||||
#[macro_export] macro_rules! tui_main {
|
||||
($state:expr) => {
|
||||
pub fn main () -> Usually<()> { tui_run_main(Arc::new(RwLock::new($state))) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw contents with modifier applied.
|
||||
pub const fn modify (on: bool, modifier: Modifier, draw: impl Draw<Tui>) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|{
|
||||
fill_mod(on, modifier).draw(to)?;
|
||||
draw.draw(to)
|
||||
pub fn tui_run_main <T> (state: Arc<RwLock<T>>) -> Usually<()> where
|
||||
T: View<Tui> + Apply<TuiEvent, Usually<()>> + 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())?;
|
||||
output.join();
|
||||
tui_teardown(&mut stdout())
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn fill_mod (on: bool, modifier: Modifier) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|Ok(Some({
|
||||
if on {
|
||||
to.update(&|cell,_,_|cell.modifier.insert(modifier))
|
||||
} else {
|
||||
to.update(&|cell,_,_|cell.modifier.remove(modifier))
|
||||
}
|
||||
})))
|
||||
pub fn tui_teardown <W: Write> (backend: &mut W) -> Usually<()> {
|
||||
use ::ratatui::backend::Backend;
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
CrosstermBackend::new(backend).show_cursor()?;
|
||||
disable_raw_mode().map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Draw contents with bold modifier applied.
|
||||
pub const fn bold (on: bool, draw: impl Draw<Tui>) -> impl Draw<Tui> {
|
||||
modify(on, Modifier::BOLD, draw)
|
||||
pub fn tui_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);
|
||||
}));
|
||||
}
|
||||
|
||||
/// Spawn the TUI input and output threadsl.
|
||||
pub fn tui_io <
|
||||
T: View<Tui> + Apply<TuiEvent, Usually<()>> + Send + Sync + 'static,
|
||||
W: Write + Send + Sync + 'static,
|
||||
> (
|
||||
exited: &Arc<AtomicBool>,
|
||||
state: &Arc<RwLock<T>>,
|
||||
poll: Duration,
|
||||
sleep: Duration,
|
||||
output: W,
|
||||
) -> Result<(Task, Task), Box<dyn Error>> {
|
||||
let keyboard = tui_input(exited, state, poll)?;
|
||||
let terminal = tui_output(exited, state, sleep, output)?;
|
||||
Ok((keyboard, terminal))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
|
||||
/// ```
|
||||
/// let _ = tengri::button_2("", "", true);
|
||||
/// let _ = tengri::button_2("", "", false);
|
||||
/// ```
|
||||
pub const fn button_2 <'a> (key: impl Draw<Tui>, label: impl Draw<Tui>, hide: bool) -> impl Draw<Tui> {
|
||||
let c1 = tui_orange();
|
||||
let c2 = tui_g(0);
|
||||
let c3 = tui_g(96);
|
||||
let c4 = tui_g(255);
|
||||
bold(true, fg_bg(c1, c2, east(fg(c2, east(key, fg(c3, "▐"))), when(!hide, fg_bg(c4, c3, label)))))
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let _ = tengri::button_3("", "", "", true);
|
||||
/// let _ = tengri::button_3("", "", "", false);
|
||||
/// ```
|
||||
pub const fn button_3 <'a> (
|
||||
key: impl Draw<Tui>, label: impl Draw<Tui>, value: impl Draw<Tui>, editing: bool,
|
||||
) -> impl Draw<Tui> {
|
||||
bold(true, east(
|
||||
fg_bg(tui_orange(), tui_g(0),
|
||||
east(fg(tui_g(0), "▐"), east(key, fg(if editing { tui_g(128) } else { tui_g(96) }, "▐")))),
|
||||
east(
|
||||
when(!editing, east(fg_bg(tui_g(255), tui_g(96), label), fg_bg(tui_g(128), tui_g(96), "▐"),)),
|
||||
east(fg_bg(tui_g(224), tui_g(128), value), fg_bg(tui_g(128), Reset, "▌"), ))))
|
||||
}
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
use crate::*;
|
||||
use ratatui::prelude::Color;
|
||||
use dizzle::{Ostensibly, Expression, LanguageError::*};
|
||||
|
||||
pub trait ColorDsl<T>: Sized {
|
||||
fn new_g (expr: T, try_to_u8: impl Fn(Perhaps<&str>)->Perhaps<u8>) -> Usually<Self>;
|
||||
fn new_rgb (expr: T, try_to_u8: impl Fn(Perhaps<&str>)->Perhaps<u8>) -> Usually<Self>;
|
||||
}
|
||||
|
||||
impl<T: Expression> ColorDsl<T> for Color {
|
||||
fn new_g (expr: T, try_to_u8: impl Fn(Perhaps<&str>)->Perhaps<u8>) -> Usually<Self> {
|
||||
let n = try_to_u8(expr.tail().map_err(Into::into))?.ok_or(Domain("not gray"))?;
|
||||
Ok(Self::Rgb(n, n, n))
|
||||
}
|
||||
fn new_rgb (expr: T, try_to_u8: impl Fn(Perhaps<&str>)->Perhaps<u8>) -> Usually<Self> {
|
||||
let r = try_to_u8(expr.tail().map_err(Into::into))?
|
||||
.ok_or(Domain("not red"))?;
|
||||
let g = try_to_u8(expr.tail().tail().head().map_err(Into::into))?
|
||||
.ok_or(Domain("not green"))?;
|
||||
let b = try_to_u8(expr.tail().tail().tail().head().map_err(Into::into))?
|
||||
.ok_or(Domain("not blue"))?;
|
||||
Ok(Color::Rgb(r, g, b))
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply foreground color.
|
||||
pub const fn fg (fg: Color, draw: impl Draw<Tui>) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|{
|
||||
to.update(&|cell,_,_|{ cell.set_fg(fg); });
|
||||
draw.draw(to)
|
||||
})
|
||||
}
|
||||
|
||||
/// Apply background color.
|
||||
pub const fn bg (bg: Color, draw: impl Draw<Tui>) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|{
|
||||
to.update(&|cell,_,_|{ cell.set_bg(bg); });
|
||||
draw.draw(to)
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn fg_bg (fg: Color, bg: Color, draw: impl Draw<Tui>) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|{
|
||||
to.update(&|cell,_,_|{ cell.set_fg(fg); cell.set_bg(bg); });
|
||||
draw.draw(to)
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn fill_ul (color: Option<Color>) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|Ok(Some(if let Some(color) = color {
|
||||
to.update(&|cell,_,_|{
|
||||
cell.modifier.insert(Modifier::UNDERLINED);
|
||||
cell.underline_color = color;
|
||||
})
|
||||
} else {
|
||||
to.update(&|cell,_,_|{
|
||||
cell.modifier.remove(Modifier::UNDERLINED);
|
||||
cell.underline_color = Reset;
|
||||
})
|
||||
})))
|
||||
}
|
||||
|
||||
pub const fn tui_color_bg () -> Color { Color::Rgb(28, 35, 25) }
|
||||
pub const fn tui_bg0 () -> Color { Color::Rgb(20, 20, 20) }
|
||||
pub const fn tui_bo1 () -> Color { Color::Rgb(100, 110, 40) }
|
||||
pub const fn tui_bo2 () -> Color { Color::Rgb(70, 80, 50) }
|
||||
pub const fn tui_border_bg () -> Color { Color::Rgb(40, 50, 30) }
|
||||
pub const fn tui_border_fg (f: bool) -> Color { if f { tui_bo1() } else { tui_bo2() } }
|
||||
pub const fn tui_brown () -> Color { Color::Rgb(128,255,0) }
|
||||
pub const fn tui_electric () -> Color { Color::Rgb(0,255,128) }
|
||||
pub const fn tui_g (g: u8) -> Color { Color::Rgb(g, g, g) }
|
||||
pub const fn tui_green () -> Color { Color::Rgb(0,255,0) }
|
||||
pub const fn tui_mode_bg () -> Color { Color::Rgb(150, 160, 90) }
|
||||
pub const fn tui_mode_fg () -> Color { Color::Rgb(255, 255, 255) }
|
||||
pub const fn tui_null () -> Color { Color::Reset }
|
||||
pub const fn tui_orange () -> Color { Color::Rgb(255,128,0) }
|
||||
pub const fn tui_red () -> Color { Color::Rgb(255,0, 0) }
|
||||
pub const fn tui_separator_fg (_: bool) -> Color { Color::Rgb(0, 0, 0) }
|
||||
pub const fn tui_status_bar_bg () -> Color { Color::Rgb(28, 35, 25) }
|
||||
pub const fn tui_ti1 () -> Color { Color::Rgb(150, 160, 90) }
|
||||
pub const fn tui_ti2 () -> Color { Color::Rgb(120, 130, 100) }
|
||||
pub const fn tui_title_fg (f: bool) -> Color { if f { tui_ti1() } else { tui_ti2() } }
|
||||
pub const fn tui_yellow () -> Color { Color::Rgb(255,255,0) }
|
||||
39
src/term/input.rs
Normal file
39
src/term/input.rs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
use crate::{*, lang::*};
|
||||
|
||||
/// Enable TUI keyboard input for main state struct.
|
||||
#[macro_export] macro_rules! tui_keys {
|
||||
(|$self:ident:$State:ty,$input:ident|$body:block) => {
|
||||
impl Apply<TuiEvent, Usually<()>> for $State {
|
||||
fn apply (&mut $self, $input: &TuiEvent) -> Usually<()> $body
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Spawn the TUI input thread which reads keys from the terminal.
|
||||
pub fn tui_input <T: Apply<TuiEvent, Usually<()>> + Send + Sync + 'static> (
|
||||
exited: &Arc<AtomicBool>, state: &Arc<RwLock<T>>, poll: Duration
|
||||
) -> Result<Task, std::io::Error> {
|
||||
let exited = exited.clone();
|
||||
let state = state.clone();
|
||||
Task::new_poll(exited.clone(), poll, move |_| {
|
||||
let event = read().unwrap();
|
||||
match event {
|
||||
|
||||
// Hardcoded exit.
|
||||
Event::Key(KeyEvent {
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
code: KeyCode::Char('c'),
|
||||
kind: KeyEventKind::Press,
|
||||
state: KeyEventState::NONE
|
||||
}) => { exited.store(true, Relaxed); },
|
||||
|
||||
// Handle all other events by the state:
|
||||
event => {
|
||||
if let Err(e) = state.write().unwrap().apply(&TuiEvent(event)) {
|
||||
panic!("{e}")
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
364
src/term/output.rs
Normal file
364
src/term/output.rs
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
use crate::*;
|
||||
use ratatui::{prelude::{Style, Position, Backend, Color}};
|
||||
|
||||
impl Screen for Tui {
|
||||
type Unit = u16;
|
||||
/// Render drawable in subarea specified by `area`
|
||||
fn show (&mut self, content: impl Draw<Self>) -> Perhaps<XYWH<u16>> {
|
||||
let previous_area = self.1;
|
||||
Ok(if let Some(area) = content.layout(self.1)? {
|
||||
self.1 = area;
|
||||
if let Some(result_area) = content.draw(self)? {
|
||||
self.1 = previous_area;
|
||||
Some(result_area)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
/// Get current clipping area
|
||||
fn area (&self) -> XYWH<Self::Unit> {
|
||||
self.1
|
||||
}
|
||||
|
||||
fn clip <T> (
|
||||
&mut self,
|
||||
area: impl Into<Option<XYWH<u16>>>,
|
||||
draw: impl FnOnce(&mut Self)->T
|
||||
) -> T {
|
||||
let prev = self.1;
|
||||
if let Some(area) = area.into() {
|
||||
self.1 = area.into();
|
||||
}
|
||||
let result = draw(self);
|
||||
self.1 = prev;
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable TUI output for state struct.
|
||||
#[macro_export] macro_rules! tui_view {
|
||||
(|$self:ident: $State:ty|$body:block) => {
|
||||
impl View<Tui> for $State {
|
||||
fn view (&$self) -> impl Draw<Tui> $body
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn the TUI output thread which writes colored characters to the terminal.
|
||||
///
|
||||
/// ```
|
||||
/// let state = std::sync::Arc::new(std::sync::RwLock::new(()));
|
||||
/// let _ = tengri::Exit::run(|exit|{
|
||||
/// tengri::tui_output(
|
||||
/// exit.as_ref(),
|
||||
/// &state,
|
||||
/// std::time::Duration::from_millis(10),
|
||||
/// std::io::stdout()
|
||||
/// )
|
||||
/// });
|
||||
/// ```
|
||||
pub fn tui_output <
|
||||
W: Write + Send + Sync + 'static, T: View<Tui> + Send + Sync + 'static
|
||||
> (
|
||||
exited: &Arc<AtomicBool>,
|
||||
state: &Arc<RwLock<T>>,
|
||||
sleep: Duration,
|
||||
output: W,
|
||||
) -> Usually<Task> {
|
||||
let state = state.clone();
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
CrosstermBackend::new(stdout()).hide_cursor()?;
|
||||
enable_raw_mode()?;
|
||||
let mut backend = CrosstermBackend::new(output);
|
||||
let Size { width, height } = backend.size().expect("get size failed");
|
||||
let mut prev = Tui::new(width, height);
|
||||
let mut next = Tui::new(width, height);
|
||||
Ok(Task::new_sleep(exited.clone(), sleep, move |perf| {
|
||||
let Size { width, height } = backend.size().expect("get size failed");
|
||||
if let Ok(state) = state.try_read() {
|
||||
prev.resize(&mut backend, width, height);
|
||||
state.view().draw(&mut next).expect("draw failed"); // TODO draw error
|
||||
prev.redraw(&mut backend, &mut next);
|
||||
}
|
||||
let timer = format!("{:>3.3}ms", perf.used.load(Relaxed));
|
||||
prev.set_string(0, 0, &timer, Style::default());
|
||||
})?)
|
||||
}
|
||||
|
||||
pub use self::colors::*; mod colors {
|
||||
use ratatui::prelude::Color;
|
||||
pub const fn tui_color_bg () -> Color { Color::Rgb(28, 35, 25) }
|
||||
pub const fn tui_bg0 () -> Color { Color::Rgb(20, 20, 20) }
|
||||
pub const fn tui_bo1 () -> Color { Color::Rgb(100, 110, 40) }
|
||||
pub const fn tui_bo2 () -> Color { Color::Rgb(70, 80, 50) }
|
||||
pub const fn tui_border_bg () -> Color { Color::Rgb(40, 50, 30) }
|
||||
pub const fn tui_border_fg (f: bool) -> Color { if f { tui_bo1() } else { tui_bo2() } }
|
||||
pub const fn tui_brown () -> Color { Color::Rgb(128,255,0) }
|
||||
pub const fn tui_electric () -> Color { Color::Rgb(0,255,128) }
|
||||
pub const fn tui_g (g: u8) -> Color { Color::Rgb(g, g, g) }
|
||||
pub const fn tui_green () -> Color { Color::Rgb(0,255,0) }
|
||||
pub const fn tui_mode_bg () -> Color { Color::Rgb(150, 160, 90) }
|
||||
pub const fn tui_mode_fg () -> Color { Color::Rgb(255, 255, 255) }
|
||||
pub const fn tui_null () -> Color { Color::Reset }
|
||||
pub const fn tui_orange () -> Color { Color::Rgb(255,128,0) }
|
||||
pub const fn tui_red () -> Color { Color::Rgb(255,0, 0) }
|
||||
pub const fn tui_separator_fg (_: bool) -> Color { Color::Rgb(0, 0, 0) }
|
||||
pub const fn tui_status_bar_bg () -> Color { Color::Rgb(28, 35, 25) }
|
||||
pub const fn tui_ti1 () -> Color { Color::Rgb(150, 160, 90) }
|
||||
pub const fn tui_ti2 () -> Color { Color::Rgb(120, 130, 100) }
|
||||
pub const fn tui_title_fg (f: bool) -> Color { if f { tui_ti1() } else { tui_ti2() } }
|
||||
pub const fn tui_yellow () -> Color { Color::Rgb(255,255,0) }
|
||||
}
|
||||
|
||||
/// Apply foreground color.
|
||||
pub const fn fg (fg: Color, draw: impl Draw<Tui>) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|{
|
||||
to.update(&|cell,_,_|{ cell.set_fg(fg); });
|
||||
draw.draw(to)
|
||||
})
|
||||
}
|
||||
|
||||
/// Apply background color.
|
||||
pub const fn bg (bg: Color, draw: impl Draw<Tui>) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|{
|
||||
to.update(&|cell,_,_|{ cell.set_bg(bg); });
|
||||
draw.draw(to)
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn fg_bg (fg: Color, bg: Color, draw: impl Draw<Tui>) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|{
|
||||
to.update(&|cell,_,_|{ cell.set_fg(fg); cell.set_bg(bg); });
|
||||
draw.draw(to)
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn fill_char (c: char) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|Ok(Some(to.update(&|cell,_,_|{
|
||||
cell.set_char(c);
|
||||
}))))
|
||||
}
|
||||
|
||||
/// Draw contents with modifier applied.
|
||||
pub const fn modify (on: bool, modifier: Modifier, draw: impl Draw<Tui>) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|{
|
||||
fill_mod(on, modifier).draw(to)?;
|
||||
draw.draw(to)
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn fill_mod (on: bool, modifier: Modifier) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|Ok(Some({
|
||||
if on {
|
||||
to.update(&|cell,_,_|cell.modifier.insert(modifier))
|
||||
} else {
|
||||
to.update(&|cell,_,_|cell.modifier.remove(modifier))
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
/// Draw contents with bold modifier applied.
|
||||
pub const fn bold (on: bool, draw: impl Draw<Tui>) -> impl Draw<Tui> {
|
||||
modify(on, Modifier::BOLD, draw)
|
||||
}
|
||||
|
||||
pub const fn fill_ul (color: Option<Color>) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|Ok(Some(if let Some(color) = color {
|
||||
to.update(&|cell,_,_|{
|
||||
cell.modifier.insert(Modifier::UNDERLINED);
|
||||
cell.underline_color = color;
|
||||
})
|
||||
} else {
|
||||
to.update(&|cell,_,_|{
|
||||
cell.modifier.remove(Modifier::UNDERLINED);
|
||||
cell.underline_color = Reset;
|
||||
})
|
||||
})))
|
||||
}
|
||||
|
||||
mod phat {
|
||||
use super::*;
|
||||
pub const LO: &'static str = "▄";
|
||||
pub const HI: &'static str = "▀";
|
||||
/// A phat line
|
||||
pub fn lo (fg: Color, bg: Color) -> impl Draw<Tui> {
|
||||
fg_bg(fg, bg, x_repeat(self::phat::LO)).exact_h(1)
|
||||
}
|
||||
/// A phat line
|
||||
pub fn hi (fg: Color, bg: Color) -> impl Draw<Tui> {
|
||||
fg_bg(fg, bg, x_repeat(self::phat::HI)).exact_h(1)
|
||||
}
|
||||
}
|
||||
|
||||
mod scroll {
|
||||
pub const ICON_DEC_V: &[char] = &['▲'];
|
||||
pub const ICON_INC_V: &[char] = &['▼'];
|
||||
pub const ICON_DEC_H: &[char] = &[' ', '🞀', ' '];
|
||||
pub const ICON_INC_H: &[char] = &[' ', '🞂', ' '];
|
||||
}
|
||||
|
||||
pub const fn x_repeat (c: &str) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|{
|
||||
let XYWH(x, y, w, _h) = to.xywh();
|
||||
for x in x..x+w {
|
||||
if let Some(cell) = to.0.cell_mut(Position::from((x, y))) {
|
||||
cell.set_symbol(&c);
|
||||
}
|
||||
}
|
||||
Ok(Some(XYWH(x, y, w, 1)))
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn y_repeat (c: &str) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|{
|
||||
let XYWH(x, y, _w, h) = to.xywh();
|
||||
for y in y..y+h {
|
||||
if let Some(cell) = to.0.cell_mut(Position::from((x, y))) {
|
||||
cell.set_symbol(&c);
|
||||
}
|
||||
}
|
||||
Ok(Some(XYWH(x, y, 1, h)))
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn xy_repeat (c: &str) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|{
|
||||
let XYWH(x, y, w, h) = to.xywh();
|
||||
let a = c.len();
|
||||
for (_v, y) in (y..y+h).enumerate() {
|
||||
for (u, x) in (x..x+w).enumerate() {
|
||||
if let Some(cell) = to.0.cell_mut(Position::from((x, y))) {
|
||||
let u = u % a;
|
||||
cell.set_symbol(&c[u..u+1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Some(XYWH(x, y, w, h)))
|
||||
})
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let _ = tengri::button_2("", "", true);
|
||||
/// let _ = tengri::button_2("", "", false);
|
||||
/// ```
|
||||
pub const fn button_2 <'a> (key: impl Draw<Tui>, label: impl Draw<Tui>, hide: bool) -> impl Draw<Tui> {
|
||||
let c1 = tui_orange();
|
||||
let c2 = tui_g(0);
|
||||
let c3 = tui_g(96);
|
||||
let c4 = tui_g(255);
|
||||
bold(true, fg_bg(c1, c2, east(fg(c2, east(key, fg(c3, "▐"))), when(!hide, fg_bg(c4, c3, label)))))
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let _ = tengri::button_3("", "", "", true);
|
||||
/// let _ = tengri::button_3("", "", "", false);
|
||||
/// ```
|
||||
pub const fn button_3 <'a> (
|
||||
key: impl Draw<Tui>, label: impl Draw<Tui>, value: impl Draw<Tui>, editing: bool,
|
||||
) -> impl Draw<Tui> {
|
||||
bold(true, east(
|
||||
fg_bg(tui_orange(), tui_g(0),
|
||||
east(fg(tui_g(0), "▐"), east(key, fg(if editing { tui_g(128) } else { tui_g(96) }, "▐")))),
|
||||
east(
|
||||
when(!editing, east(fg_bg(tui_g(255), tui_g(96), label), fg_bg(tui_g(128), tui_g(96), "▐"),)),
|
||||
east(fg_bg(tui_g(224), tui_g(128), value), fg_bg(tui_g(128), Reset, "▌"), ))))
|
||||
}
|
||||
|
||||
/// Stackably padded.
|
||||
///
|
||||
/// ```
|
||||
/// /// TODO
|
||||
/// ```
|
||||
pub fn phat (w: u16, h: u16, [fg, bg, hi, lo]: [Color;4], draw: impl Draw<Tui>) -> impl Draw<Tui> {
|
||||
let top = self::phat::lo(bg, hi).exact_h(1);
|
||||
let low = self::phat::hi(bg, lo).exact_h(1);
|
||||
let draw = fg_bg(fg, bg, draw);
|
||||
south(top, north(low, draw)).min_wh(w, h)
|
||||
}
|
||||
|
||||
pub fn x_scroll () -> impl Draw<Tui> {
|
||||
thunk(|Tui(buf, XYWH(x1, y1, w, h)): &mut Tui|{
|
||||
let x2 = *x1 + *w;
|
||||
for (i, x) in (*x1..=x2).enumerate() {
|
||||
if let Some(cell) = buf.cell_mut(Position::from((x, *y1))) {
|
||||
if i < (self::scroll::ICON_DEC_H.len()) {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Rgb(0, 0, 0));
|
||||
cell.set_char(self::scroll::ICON_DEC_H[i as usize]);
|
||||
} else if i > (*w as usize - self::scroll::ICON_INC_H.len()) {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Rgb(0, 0, 0));
|
||||
cell.set_char(self::scroll::ICON_INC_H[*w as usize - i]);
|
||||
} else if false {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Reset);
|
||||
cell.set_char('━');
|
||||
} else {
|
||||
cell.set_fg(Rgb(0, 0, 0));
|
||||
cell.set_bg(Reset);
|
||||
cell.set_char('╌');
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Some(XYWH(*x1, *y1, *w, 1)))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn y_scroll () -> impl Draw<Tui> {
|
||||
thunk(|Tui(buf, XYWH(x1, y1, w, h)): &mut Tui|{
|
||||
let y2 = *y1 + *h;
|
||||
for (i, y) in (*y1..=y2).enumerate() {
|
||||
if let Some(cell) = buf.cell_mut(Position::from((*x1, y))) {
|
||||
if (i as usize) < (self::scroll::ICON_DEC_V.len()) {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Rgb(0, 0, 0));
|
||||
cell.set_char(self::scroll::ICON_DEC_V[i as usize]);
|
||||
} else if (i as usize) > (*h as usize - self::scroll::ICON_INC_V.len()) {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Rgb(0, 0, 0));
|
||||
cell.set_char(self::scroll::ICON_INC_V[*h as usize - i]);
|
||||
} else if false {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Reset);
|
||||
cell.set_char('‖'); // ━
|
||||
} else {
|
||||
cell.set_fg(Rgb(0, 0, 0));
|
||||
cell.set_bg(Reset);
|
||||
cell.set_char('╎'); // ━
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Some(XYWH(*x1, *y1, 1, *h)))
|
||||
})
|
||||
}
|
||||
|
||||
/// Draw TUI content or its error message.
|
||||
///
|
||||
/// ```
|
||||
/// for variant in [
|
||||
/// Ok(Some("hello")),
|
||||
/// Ok(None),
|
||||
/// Err("fail".into()),
|
||||
/// ] {
|
||||
/// let _ = tengri::catcher(variant);
|
||||
/// }
|
||||
/// ```
|
||||
pub fn catcher <T: Draw<Tui>> (result: Usually<T>) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|match result {
|
||||
Ok(content) => content.draw(to),
|
||||
Err(e) => {
|
||||
let err_fg = Color::Rgb(255,224,244);
|
||||
let err_bg = Color::Rgb(96, 24, 24);
|
||||
let title = east(bold(true, "upsi daisy. "), "rendering failed.");
|
||||
let error = east("\"why?\" ", bold(true, format!("{e}")));
|
||||
fg(err_fg, bg(err_bg, south(title, error))).draw(to)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
impl_draw!(|self: u64, _to: Tui|{ todo!() });
|
||||
impl_draw!(|self: f64, _to: Tui|{ todo!() });
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
/// Stackably padded.
|
||||
///
|
||||
/// ```
|
||||
/// /// TODO
|
||||
/// ```
|
||||
pub fn phat (w: u16, h: u16, [fg, bg, hi, lo]: [Color;4], draw: impl Draw<Tui>) -> impl Draw<Tui> {
|
||||
let top = phat_lo(bg, hi).exact_h(1);
|
||||
let low = phat_hi(bg, lo).exact_h(1);
|
||||
let draw = fg_bg(fg, bg, draw);
|
||||
south(top, north(low, draw)).min_wh(w, h)
|
||||
}
|
||||
use super::*;
|
||||
pub const LO: &'static str = "▄";
|
||||
pub const HI: &'static str = "▀";
|
||||
/// A phat line
|
||||
fn phat_lo (fg: Color, bg: Color) -> impl Draw<Tui> {
|
||||
fg_bg(fg, bg, x_repeat(self::phat::LO)).exact_h(1)
|
||||
}
|
||||
/// A phat line
|
||||
fn phat_hi (fg: Color, bg: Color) -> impl Draw<Tui> {
|
||||
fg_bg(fg, bg, x_repeat(self::phat::HI)).exact_h(1)
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
use crate::*;
|
||||
use ratatui::{prelude::{Style, Position, Backend, Color}};
|
||||
|
||||
pub const fn x_repeat (c: &str) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|{
|
||||
let XYWH(x, y, w, _h) = to.xywh();
|
||||
for x in x..x+w {
|
||||
if let Some(cell) = to.0.cell_mut(Position::from((x, y))) {
|
||||
cell.set_symbol(&c);
|
||||
}
|
||||
}
|
||||
Ok(Some(XYWH(x, y, w, 1)))
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn y_repeat (c: &str) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|{
|
||||
let XYWH(x, y, _w, h) = to.xywh();
|
||||
for y in y..y+h {
|
||||
if let Some(cell) = to.0.cell_mut(Position::from((x, y))) {
|
||||
cell.set_symbol(&c);
|
||||
}
|
||||
}
|
||||
Ok(Some(XYWH(x, y, 1, h)))
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn xy_repeat (c: &str) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|{
|
||||
let XYWH(x, y, w, h) = to.xywh();
|
||||
let a = c.len();
|
||||
for (_v, y) in (y..y+h).enumerate() {
|
||||
for (u, x) in (x..x+w).enumerate() {
|
||||
if let Some(cell) = to.0.cell_mut(Position::from((x, y))) {
|
||||
let u = u % a;
|
||||
cell.set_symbol(&c[u..u+1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Some(XYWH(x, y, w, h)))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
use crate::*;
|
||||
use ratatui::{prelude::{Style, Position, Backend, Color}};
|
||||
|
||||
pub const ICON_DEC_V: &[char] = &['▲'];
|
||||
pub const ICON_INC_V: &[char] = &['▼'];
|
||||
pub const ICON_DEC_H: &[char] = &[' ', '🞀', ' '];
|
||||
pub const ICON_INC_H: &[char] = &[' ', '🞂', ' '];
|
||||
|
||||
pub fn x_scroll () -> impl Draw<Tui> {
|
||||
thunk(|Tui(buf, XYWH(x1, y1, w, h)): &mut Tui|{
|
||||
let x2 = *x1 + *w;
|
||||
for (i, x) in (*x1..=x2).enumerate() {
|
||||
if let Some(cell) = buf.cell_mut(Position::from((x, *y1))) {
|
||||
if i < (ICON_DEC_H.len()) {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Rgb(0, 0, 0));
|
||||
cell.set_char(ICON_DEC_H[i as usize]);
|
||||
} else if i > (*w as usize - ICON_INC_H.len()) {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Rgb(0, 0, 0));
|
||||
cell.set_char(ICON_INC_H[*w as usize - i]);
|
||||
} else if false {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Reset);
|
||||
cell.set_char('━');
|
||||
} else {
|
||||
cell.set_fg(Rgb(0, 0, 0));
|
||||
cell.set_bg(Reset);
|
||||
cell.set_char('╌');
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Some(XYWH(*x1, *y1, *w, 1)))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn y_scroll () -> impl Draw<Tui> {
|
||||
thunk(|Tui(buf, XYWH(x1, y1, w, h)): &mut Tui|{
|
||||
let y2 = *y1 + *h;
|
||||
for (i, y) in (*y1..=y2).enumerate() {
|
||||
if let Some(cell) = buf.cell_mut(Position::from((*x1, y))) {
|
||||
if (i as usize) < (ICON_DEC_V.len()) {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Rgb(0, 0, 0));
|
||||
cell.set_char(ICON_DEC_V[i as usize]);
|
||||
} else if (i as usize) > (*h as usize - ICON_INC_V.len()) {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Rgb(0, 0, 0));
|
||||
cell.set_char(ICON_INC_V[*h as usize - i]);
|
||||
} else if false {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Reset);
|
||||
cell.set_char('‖'); // ━
|
||||
} else {
|
||||
cell.set_fg(Rgb(0, 0, 0));
|
||||
cell.set_bg(Reset);
|
||||
cell.set_char('╎'); // ━
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Some(XYWH(*x1, *y1, 1, *h)))
|
||||
})
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue