mirror of
https://codeberg.org/unspeaker/tengri.git
synced 2026-03-13 12:10:44 +01:00
This commit is contained in:
parent
ad070a4cbb
commit
8f0a2accce
13 changed files with 1819 additions and 2157 deletions
11
Cargo.toml
11
Cargo.toml
|
|
@ -5,12 +5,15 @@ version = "0.15.0"
|
||||||
description = "UI metaframework."
|
description = "UI metaframework."
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["tui", "jack", "dsl"]
|
default = ["lang", "sing", "draw", "tui"]
|
||||||
|
lang = ["dep:dizzle"]
|
||||||
|
sing = ["dep:jack"]
|
||||||
|
draw = []
|
||||||
|
tui = ["draw", "dep:ratatui", "dep:crossterm"]
|
||||||
|
gui = ["draw", "dep:winit"]
|
||||||
|
|
||||||
bumpalo = ["dep:bumpalo"]
|
bumpalo = ["dep:bumpalo"]
|
||||||
tui = ["dep:ratatui", "dep:crossterm"]
|
|
||||||
gui = ["dep:winit"]
|
|
||||||
dsl = ["dep:dizzle"]
|
dsl = ["dep:dizzle"]
|
||||||
jack = ["dep:jack"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { version = "1.0" }
|
anyhow = { version = "1.0" }
|
||||||
|
|
|
||||||
22
README.md
22
README.md
|
|
@ -1,34 +1,36 @@
|
||||||
***tengri*** is the skygod in whose dream the [**`tek`**](https://codeberg.org/unspeaker/tek)
|
# tengri [](https://nogithub.codeberg.page)
|
||||||
is happening.
|
|
||||||
|
|
||||||
# features
|
***tengri*** is the skygod in whose dream the [**`tek`**](https://codeberg.org/unspeaker/tek)
|
||||||
|
is happening. it looks at us from its [**`perch`**](https://codeberg.org/unspeaker/perch).
|
||||||
|
|
||||||
|
## features
|
||||||
|
|
||||||
it is here to do the following:
|
it is here to do the following:
|
||||||
|
|
||||||
## sing
|
### sing
|
||||||
|
|
||||||
connect to jack audio connection kit to process chunks of audio and midi.
|
connect to jack audio connection kit to process chunks of audio and midi.
|
||||||
|
|
||||||
## draw
|
### draw
|
||||||
|
|
||||||
abstract interface layout system for defining interface layouts abstractly.
|
abstract interface layout system for defining interface layouts abstractly.
|
||||||
|
|
||||||
## play
|
### play
|
||||||
|
|
||||||
the input handling system.
|
the input handling system.
|
||||||
|
|
||||||
## tui
|
### tui
|
||||||
|
|
||||||
uses `ratatui` to run in a terminal.
|
uses `ratatui` to run in a terminal.
|
||||||
|
|
||||||
## gui (todo)
|
### gui (todo-todo-todo)
|
||||||
|
|
||||||
opens windows, runs shaders in them and/or delegates them (to e.g. plugin guis).
|
opens windows, runs shaders in them and/or delegates them (to e.g. plugin guis).
|
||||||
|
|
||||||
## lang
|
### lang
|
||||||
|
|
||||||
uses `dizzle` to let you livecode all of the above.
|
uses `dizzle` to let you livecode all of the above.
|
||||||
|
|
||||||
# license
|
## license
|
||||||
|
|
||||||
here and now, the blessings of `tengri` are invokable under the [**`AGPL3`**](./LICENSE).
|
here and now, the blessings of `tengri` are invokable under the [**`AGPL3`**](./LICENSE).
|
||||||
|
|
|
||||||
407
src/draw.rs
Normal file
407
src/draw.rs
Normal file
|
|
@ -0,0 +1,407 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// A numeric type that can be used as coordinate.
|
||||||
|
///
|
||||||
|
/// FIXME: Replace this ad-hoc trait with `num` crate.
|
||||||
|
pub trait Coord: Send + Sync + Copy
|
||||||
|
+ Add<Self, Output=Self>
|
||||||
|
+ Sub<Self, Output=Self>
|
||||||
|
+ Mul<Self, Output=Self>
|
||||||
|
+ Div<Self, Output=Self>
|
||||||
|
+ Ord + PartialEq + Eq
|
||||||
|
+ Debug + Display + Default
|
||||||
|
+ From<u16> + Into<u16>
|
||||||
|
+ Into<usize>
|
||||||
|
+ Into<f64>
|
||||||
|
{
|
||||||
|
fn zero () -> Self { 0.into() }
|
||||||
|
fn plus (self, other: Self) -> Self;
|
||||||
|
fn minus (self, other: Self) -> Self { if self >= other { self - other } else { 0.into() } }
|
||||||
|
fn atomic (self) -> AtomicUsize { AtomicUsize::new(self.into()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Point along horizontal axis.
|
||||||
|
pub trait X<N: Coord> { fn x (&self) -> N { N::zero() } }
|
||||||
|
|
||||||
|
/// Point along vertical axis.
|
||||||
|
pub trait Y<N: Coord> { fn y (&self) -> N { N::zero() } }
|
||||||
|
|
||||||
|
/// Length along horizontal axis.
|
||||||
|
pub trait W<N: Coord> { fn w (&self) -> N { N::zero() } }
|
||||||
|
|
||||||
|
/// Length along vertical axis.
|
||||||
|
pub trait H<N: Coord> { fn h (&self) -> N { N::zero() } }
|
||||||
|
|
||||||
|
/// Which corner/side of a box is 0, 0
|
||||||
|
pub trait Anchor { fn anchor (&self) -> Alignment; }
|
||||||
|
|
||||||
|
/// Area along (X, Y).
|
||||||
|
pub trait Area<N: Coord>: W<N> + H<N> { fn wh (&self) -> WH<N> { WH(self.w(), self.h()) } }
|
||||||
|
|
||||||
|
/// Point along (X, Y).
|
||||||
|
pub trait Point<N: Coord>: X<N> + Y<N> { fn xy (&self) -> XY<N> { XY(self.x(), self.y()) } }
|
||||||
|
|
||||||
|
// Something that has a 2D bounding box (X, Y, W, H).
|
||||||
|
pub trait Bounded<N: Coord>: Point<N> + Area<N> + Anchor<N> {
|
||||||
|
fn x2 (&self) -> N { self.x().plus(self.w()) }
|
||||||
|
fn y2 (&self) -> N { self.y().plus(self.h()) }
|
||||||
|
fn xywh (&self) -> [N;4] { [..self.xy(), ..self.wh()] }
|
||||||
|
fn expect_min (&self, w: N, h: N) -> Usually<&Self> {
|
||||||
|
if self.w() < w || self.h() < h {
|
||||||
|
Err(format!("min {w}x{h}").into())
|
||||||
|
} else {
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A point (X, Y).
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let xy = tengri::XY(0u16, 0);
|
||||||
|
/// ```
|
||||||
|
#[cfg_attr(test, derive(Arbitrary))]
|
||||||
|
#[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct XY<C: Coord>(pub C, pub C);
|
||||||
|
|
||||||
|
/// A size (Width, Height).
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let wh = tengri::WH(0u16, 0);
|
||||||
|
/// ```
|
||||||
|
#[cfg_attr(test, derive(Arbitrary))]
|
||||||
|
#[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct WH<C: Coord>(pub C, pub C);
|
||||||
|
|
||||||
|
/// Point with size.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let xywh = tengri::XYWH(0u16, 0, 0, 0);
|
||||||
|
/// assert_eq!(tengri::XYWH(10u16, 10, 20, 20).center(), tengri::XY(20, 20));
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// * [ ] TODO: anchor field (determines at which corner/side is X0 Y0)
|
||||||
|
///
|
||||||
|
#[cfg_attr(test, derive(Arbitrary))]
|
||||||
|
#[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct XYWH<C: Coord>(
|
||||||
|
pub C, pub C, pub C, pub C
|
||||||
|
);
|
||||||
|
|
||||||
|
/// A cardinal direction.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let direction = tengri::Direction::Above;
|
||||||
|
/// ```
|
||||||
|
#[cfg_attr(test, derive(Arbitrary))]
|
||||||
|
#[derive(Copy, Clone, PartialEq, Debug)] pub enum Direction {
|
||||||
|
North, South, East, West, Above, Below
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 9th of area to place.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let alignment = tengri::Alignment::Center;
|
||||||
|
/// ```
|
||||||
|
#[cfg_attr(test, derive(Arbitrary))]
|
||||||
|
#[derive(Debug, Copy, Clone, Default)] pub enum Alignment {
|
||||||
|
#[default] Center, X, Y, NW, N, NE, E, SE, S, SW, W
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drawable with dynamic dispatch.
|
||||||
|
pub trait Draw<T>: Fn(&mut T)->Usually<()> { fn draw (&self, to: &mut T) -> Usually<()>; }
|
||||||
|
|
||||||
|
/// Drawable thunk.
|
||||||
|
impl<T, F: Fn(&mut T)->()> Draw<T> for F { fn draw (&self, to: &mut T) -> Usually<()> { self(to) } }
|
||||||
|
|
||||||
|
impl<T> Draw<T> for () { fn draw (&self, _: &mut S) -> Usually<()> { Ok(()) } }
|
||||||
|
impl<T> Draw<T> for Box<dyn Draw<T>> { fn draw (&self, to: &mut S) -> Usually<()> { (**self).draw(to) } }
|
||||||
|
|
||||||
|
impl<T, D: Draw<T>> Draw<T> for &D { fn draw (&self, to: &mut S) -> Usually<()> { (*self).draw(to) } }
|
||||||
|
impl<T, D: Draw<T>> Draw<T> for &mut D { fn draw (&self, to: &mut S) -> Usually<()> { (**self).draw(to) } }
|
||||||
|
impl<T, D: Draw<T>> Draw<T> for Arc<D> { fn draw (&self, to: &mut S) -> Usually<()> { (**self).draw(to) } }
|
||||||
|
impl<T, D: Draw<T>> Draw<T> for RwLock<D> { fn draw (&self, to: &mut S) -> Usually<()> { self.read().unwrap().draw(to) } }
|
||||||
|
impl<T, D: Draw<T>> Draw<T> for Option<D> { fn draw (&self, to: &mut S) { if let Some(draw) = self { draw.draw(to) } } }
|
||||||
|
|
||||||
|
/// Draw the content or its error message.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let _ = tengri::Catcher::<tengri::Tui, &'static str>::new(Ok(Some("hello")));
|
||||||
|
/// let _ = tengri::Catcher::<tengri::Tui, &'static str>::new(Ok(None));
|
||||||
|
/// let _ = tengri::Catcher::<tengri::Tui, &'static str>::new(Err("draw fail".into()));
|
||||||
|
/// ```
|
||||||
|
pub fn catcher <T, E> (error: Perhaps<E>, draw: impl Draw<T>) -> impl Draw<T> {
|
||||||
|
move|to|match self.0.as_ref() {
|
||||||
|
Ok(Some(content)) => draw(to),
|
||||||
|
Ok(None) => to.blit(&"<empty>", 0, 0, Some(Style::default().yellow())),
|
||||||
|
Err(e) => {
|
||||||
|
let err_fg = Color::Rgb(255,224,244);
|
||||||
|
let err_bg = Color::Rgb(96,24,24);
|
||||||
|
let title = Bsp::e(Tui::bold(true, "oopsie daisy. "), "rendering failed.");
|
||||||
|
let error = Bsp::e("\"why?\" ", Tui::bold(true, format!("{e}")));
|
||||||
|
to.place(&Tui::fg_bg(err_fg, err_bg, Bsp::s(title, error)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Only render when condition is true.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// fn test () -> impl tengri::Draw<tengri::Tui> {
|
||||||
|
/// tengri::when(true, "Yes")
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn when <T> (condition: bool, draw: impl Draw<T>) -> impl Draw<T> {
|
||||||
|
move|to|Ok(if condition { draw(to)? } })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render one thing if a condition is true and another false.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// fn test () -> impl tengri::Draw<tengri::Tui> {
|
||||||
|
/// tengri::either(true, "Yes", "No")
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn either <T> (condition: bool, a: impl Draw<T>, b: impl Draw<T>) -> impl Draw<T> {
|
||||||
|
move|to|Ok(if condition { a(to)? } else { b(to)? })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set maximum width and/or height of drawing area.
|
||||||
|
pub fn clip <T, N: Coord> (w: Option<N>, h: Option<N>, draw: impl Draw<T>) -> impl Draw<T> {
|
||||||
|
move|to|draw(to.clip(w, h))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shrink drawing area symmetrically.
|
||||||
|
pub fn pad <T, N: Coord> (w: Option<N>, h: Option<N>, draw: impl Draw<T>) -> impl Draw<T> {
|
||||||
|
move|to|draw(to.pad(w, h))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Only draw content if area is above a certain size.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let minimum = tengri::Min::XY(3, 5, "Hello"); // 5x5
|
||||||
|
/// ```
|
||||||
|
pub fn min <T, N: Coord> (w: N, draw: impl Draw<T>) -> impl Draw<T> {
|
||||||
|
move|to|draw(to.min(w, h))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the maximum width and/or height of the content.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let maximum = tengri::Max::XY(3, 5, "Hello"); // 3x1
|
||||||
|
/// ```
|
||||||
|
pub fn max <T, N: Coord> (x: N, draw: impl Draw<T>) -> impl Draw<T> {
|
||||||
|
move|to|draw(to.max(w, h))
|
||||||
|
}
|
||||||
|
|
||||||
|
// pub fn displace ...
|
||||||
|
|
||||||
|
/// Shrink by amount on each axis.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// /// TODO
|
||||||
|
/// ```
|
||||||
|
pub fn border <T, S: BorderStyle> (on: bool, style: S, draw: impl Draw<T>) -> impl Draw<T> {
|
||||||
|
fill_wh(above(when(on, |output|{/*TODO*/}), pad(Some(1), Some(1), draw)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stackably padded.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// /// TODO
|
||||||
|
/// ```
|
||||||
|
pub fn phat <T, N: Coord> (
|
||||||
|
w: N, h: N, [fg, bg, hi, lo]: [Color;4], draw: impl Draw<T>
|
||||||
|
) -> impl Draw<T> {
|
||||||
|
let top = exact_w(1, Self::lo(bg, hi));
|
||||||
|
let low = exact_h(1, Self::hi(bg, lo));
|
||||||
|
let draw = TuiOut::fg_bg(fg, bg, draw);
|
||||||
|
min_wh(w, h, bsp_s(top, bsp_n(low, fill_wh(draw))))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter <T> (items: impl Iterator<Item = dyn Draw<T>>) -> impl Draw<T> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Split screen between two items, or layer them atop each other.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use tengri::Direction::*;
|
||||||
|
/// let _ = tengri::draw::bsp(Above, (), ());
|
||||||
|
/// let _ = tengri::draw::bsp(Below, (), ());
|
||||||
|
/// let _ = tengri::draw::bsp(North, (), ());
|
||||||
|
/// let _ = tengri::draw::bsp(South, (), ());
|
||||||
|
/// let _ = tengri::draw::bsp(East, (), ());
|
||||||
|
/// let _ = tengri::draw::bsp(West, (), ());
|
||||||
|
/// ```
|
||||||
|
pub fn bsp <T> (dir: Direction, a: impl Draw<T>, b: impl Draw<T>) -> impl Draw<T> {
|
||||||
|
move|to|{
|
||||||
|
//fn split_cardinal <N: Coord> (
|
||||||
|
//direction: Direction, area: XYWH<N>, a: N
|
||||||
|
//) -> (XYWH<N>, XYWH<N>) {
|
||||||
|
//let XYWH(x, y, w, h) = area;
|
||||||
|
//match direction {
|
||||||
|
//North => (XYWH(x, y.plus(h).minus(a), w, a), XYWH(x, y, w, h.minus(a))),
|
||||||
|
//South => (XYWH(x, y, w, a), XYWH(x, y.plus(a), w, h.minus(a))),
|
||||||
|
//East => (XYWH(x, y, a, h), XYWH(x.plus(a), y, w.minus(a), h)),
|
||||||
|
//West => (XYWH(x.plus(w).minus(a), y, a, h), XYWH(x, y, w.minus(a), h)),
|
||||||
|
//Above | Below => (area, area)
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//fn bsp_areas <N: Coord, A: TwoD<N>, B: TwoD<N>> (
|
||||||
|
//area: [N;4],
|
||||||
|
//direction: Direction,
|
||||||
|
//a: &A,
|
||||||
|
//b: &B,
|
||||||
|
//) -> [XYWH<N>;3] {
|
||||||
|
//let XYWH(x, y, w, h) = area;
|
||||||
|
//let WH(aw, ah) = a.layout(area).wh();
|
||||||
|
//let WH(bw, bh) = b.layout(match direction {
|
||||||
|
//South => XYWH(x, y + ah, w, h.minus(ah)),
|
||||||
|
//North => XYWH(x, y, w, h.minus(ah)),
|
||||||
|
//East => XYWH(x + aw, y, w.minus(aw), h),
|
||||||
|
//West => XYWH(x, y, w.minus(aw), h),
|
||||||
|
//Above => area,
|
||||||
|
//Below => area,
|
||||||
|
//}).wh();
|
||||||
|
//match direction {
|
||||||
|
//Above | Below => {
|
||||||
|
//let XYWH(x, y, w, h) = area.centered_xy([aw.max(bw), ah.max(bh)]);
|
||||||
|
//let a = XYWH((x + w/2.into()).minus(aw/2.into()), (y + h/2.into()).minus(ah/2.into()), aw, ah);
|
||||||
|
//let b = XYWH((x + w/2.into()).minus(bw/2.into()), (y + h/2.into()).minus(bh/2.into()), bw, bh);
|
||||||
|
//[a.into(), b.into(), XYWH(x, y, w, h)]
|
||||||
|
//},
|
||||||
|
//South => {
|
||||||
|
//let XYWH(x, y, w, h) = area.centered_xy([aw.max(bw), ah + bh]);
|
||||||
|
//let a = XYWH((x + w/2.into()).minus(aw/2.into()), y, aw, ah);
|
||||||
|
//let b = XYWH((x + w/2.into()).minus(bw/2.into()), y + ah, bw, bh);
|
||||||
|
//[a.into(), b.into(), XYWH(x, y, w, h)]
|
||||||
|
//},
|
||||||
|
//North => {
|
||||||
|
//let XYWH(x, y, w, h) = area.centered_xy([aw.max(bw), ah + bh]);
|
||||||
|
//let a = XYWH((x + (w/2.into())).minus(aw/2.into()), y + bh, aw, ah);
|
||||||
|
//let b = XYWH((x + (w/2.into())).minus(bw/2.into()), y, bw, bh);
|
||||||
|
//[a.into(), b.into(), XYWH(x, y, w, h)]
|
||||||
|
//},
|
||||||
|
//East => {
|
||||||
|
//let XYWH(x, y, w, h) = area.centered_xy([aw + bw, ah.max(bh)]);
|
||||||
|
//let a = XYWH(x, (y + h/2.into()).minus(ah/2.into()), aw, ah);
|
||||||
|
//let b = XYWH(x + aw, (y + h/2.into()).minus(bh/2.into()), bw, bh);
|
||||||
|
//[a.into(), b.into(), XYWH(x, y, w, h)]
|
||||||
|
//},
|
||||||
|
//West => {
|
||||||
|
//let XYWH(x, y, w, h) = area.centered_xy([aw + bw, ah.max(bh)]);
|
||||||
|
//let a = XYWH(x + bw, (y + h/2.into()).minus(ah/2.into()), aw, ah);
|
||||||
|
//let b = XYWH(x, (y + h/2.into()).minus(bh/2.into()), bw, bh);
|
||||||
|
//[a.into(), b.into(), XYWH(x, y, w, h)]
|
||||||
|
//},
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//pub fn iter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 3-column layout with center priority.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// /// TODO
|
||||||
|
/// ```
|
||||||
|
pub fn tryptich <T, N: Coord> (
|
||||||
|
top: boolean, [(w_a, ref a), (w_b, ref b), (w_c, ref c)]: [(N, impl Draw<T>);3]
|
||||||
|
) -> impl Draw<T> {
|
||||||
|
let Self { top, h, left: (w_a, ref a), middle: (w_b, ref b), right: (w_c, ref c) } = *self;
|
||||||
|
let a = exact_w(w_a, a);
|
||||||
|
let b = exact_w(w_b, align_w(b));
|
||||||
|
let c = exact_w(w_c, c);
|
||||||
|
exact_h(h, if top {
|
||||||
|
bsp_above(fill_w(align_n(b)), bsp_above(fill_w(align_nw(a)), fill_w(align_ne(c))))
|
||||||
|
} else {
|
||||||
|
bsp_above(fill_wh(align_c(b)), bsp_above(fill_wh(align_w(a)), fill_wh(align_e(c))))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<N: Unit, O: Screen<N>> Draw<O> for Measure<N> {
|
||||||
|
fn draw (&self, to: &mut O) {
|
||||||
|
// TODO: 🡘 🡙 ←🡙→ indicator to expand window when too small
|
||||||
|
self.x.store(to.area().w().into(), Relaxed);
|
||||||
|
self.y.store(to.area().h().into(), Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<O: Screen, T: Draw<O>> Draw<O> for Bounded<O, T> {
|
||||||
|
fn draw (&self, to: &mut O) {
|
||||||
|
let area = to.area();
|
||||||
|
*to.area_mut() = self.0;
|
||||||
|
self.1.draw(to);
|
||||||
|
*to.area_mut() = area;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<O: Screen, T: Draw<O>> Draw<O> for Align<T> {
|
||||||
|
fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), &self.1).draw(to) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<O: Screen, T: Draw<O>> Draw<O> for Pad<O::Unit, T> {
|
||||||
|
fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<O: Screen, Head: Draw<O>, Tail: Draw<O>> Draw<O> for Bsp<Head, Tail> {
|
||||||
|
fn draw (&self, to: &mut O) {
|
||||||
|
let [a, b, _] = bsp_areas(to.area(), self.0, &self.1, &self.2);
|
||||||
|
//panic!("{a:?} {b:?}");
|
||||||
|
if self.0 == Below {
|
||||||
|
to.show(a, &self.1);
|
||||||
|
to.show(b, &self.2);
|
||||||
|
} else {
|
||||||
|
to.show(b, &self.2);
|
||||||
|
to.show(a, &self.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear a pre-allocated buffer, then write into it.
|
||||||
|
#[macro_export] macro_rules! rewrite {
|
||||||
|
($buf:ident, $($rest:tt)*) => { |$buf,_,_|{ $buf.clear(); write!($buf, $($rest)*) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// FIXME: This macro should be some variant of `eval`, too.
|
||||||
|
/// But taking into account the different signatures (resolving them into 1?)
|
||||||
|
#[cfg(feature = "lang")] #[macro_export] macro_rules! draw {
|
||||||
|
($State:ident: $Output:ident: $layers:expr) => {
|
||||||
|
impl Draw<$Output> for $State {
|
||||||
|
fn draw (&self, to: &mut $Output) {
|
||||||
|
for layer in $layers { layer(self, to) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// FIXME: This is generic: should be called `eval` and be part of [dizzle].
|
||||||
|
#[cfg(feature = "lang")] #[macro_export] macro_rules! view {
|
||||||
|
($State:ident: $Output:ident: $namespaces:expr) => {
|
||||||
|
impl Understand<$Output, ()> for $State {
|
||||||
|
fn understand_expr <'a> (&'a self, to: &mut $Output, expr: &'a impl Expression) -> Usually<()> {
|
||||||
|
for namespace in $namespaces { if namespace(self, to, expr)? { return Ok(()) } }
|
||||||
|
Err(format!("{}::<{}, ()>::understand_expr: unexpected: {expr:?}",
|
||||||
|
stringify! { $State },
|
||||||
|
stringify! { $Output }).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stack things on top of each other,
|
||||||
|
#[macro_export] macro_rules! lay (($($expr:expr),* $(,)?) => {{
|
||||||
|
let bsp = (); $(let bsp = Bsp::b(bsp, $expr);)* bsp
|
||||||
|
}});
|
||||||
|
|
||||||
|
/// Stack southward.
|
||||||
|
#[macro_export] macro_rules! col (($($expr:expr),* $(,)?) => {{
|
||||||
|
let bsp = (); $(let bsp = Bsp::s(bsp, $expr);)* bsp
|
||||||
|
}});
|
||||||
|
|
||||||
|
/// Stack northward.
|
||||||
|
#[macro_export] macro_rules! col_up (($($expr:expr),* $(,)?) => {{
|
||||||
|
let bsp = (); $(let bsp = Bsp::n(bsp, $expr);)* bsp
|
||||||
|
}});
|
||||||
|
|
||||||
|
/// Stack eastward.
|
||||||
|
#[macro_export] macro_rules! row (($($expr:expr),* $(,)?) => {{
|
||||||
|
let bsp = (); $(let bsp = Bsp::e(bsp, $expr);)* bsp
|
||||||
|
}});
|
||||||
41
src/play.rs
Normal file
41
src/play.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Define an enum containing commands, and implement [Command] trait for over given `State`.
|
||||||
|
#[macro_export] macro_rules! def_command (
|
||||||
|
($Command:ident: |$state:ident: $State:ty| {
|
||||||
|
// FIXME: support attrs (docstrings)
|
||||||
|
$($Variant:ident$({$($arg:ident:$Arg:ty),+ $(,)?})?=>$body:expr),* $(,)?
|
||||||
|
})=>{
|
||||||
|
#[derive(Debug)] pub enum $Command {
|
||||||
|
// FIXME: support attrs (docstrings)
|
||||||
|
$($Variant $({ $($arg: $Arg),* })?),*
|
||||||
|
}
|
||||||
|
impl ::tengri::Command<$State> for $Command {
|
||||||
|
fn execute (&self, $state: &mut $State) -> Perhaps<Self> {
|
||||||
|
match self {
|
||||||
|
$(Self::$Variant $({ $($arg),* })? => $body,)*
|
||||||
|
_ => unimplemented!("Command<{}>: {self:?}", stringify!($State)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Implement [Handle] for given `State` and `handler`.
|
||||||
|
#[macro_export] macro_rules! handle {
|
||||||
|
(|$self:ident:$State:ty,$input:ident|$handler:expr) => {
|
||||||
|
impl<E: Engine> ::tengri::Handle<E> for $State {
|
||||||
|
fn handle (&mut $self, $input: &E) -> Perhaps<E::Handled> {
|
||||||
|
$handler
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($E:ty: |$self:ident:$State:ty,$input:ident|$handler:expr) => {
|
||||||
|
impl ::tengri::Handle<$E> for $State {
|
||||||
|
fn handle (&mut $self, $input: &$E) ->
|
||||||
|
Perhaps<<$E as ::tengri::Input>::Handled>
|
||||||
|
{
|
||||||
|
$handler
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
201
src/sing.rs
Normal file
201
src/sing.rs
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
use crate::*;
|
||||||
|
use ::jack::*;
|
||||||
|
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>),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Event enum for JACK events.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let event = tengri::JackEvent::XRun;
|
||||||
|
/// ```
|
||||||
|
#[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);
|
||||||
|
|
||||||
|
/// Running JACK [AsyncClient] with maximum type erasure.
|
||||||
|
///
|
||||||
|
/// One [Box] contains function that handles [JackEvent]s.
|
||||||
|
///
|
||||||
|
/// Another [Box] containing a function that handles realtime IO.
|
||||||
|
///
|
||||||
|
/// That's all it knows about them.
|
||||||
|
pub type DynamicAsyncClient<'j>
|
||||||
|
= AsyncClient<DynamicNotifications<'j>, DynamicAudioHandler<'j>>;
|
||||||
|
|
||||||
|
/// Notification handler wrapper for [BoxedAudioHandler].
|
||||||
|
pub type DynamicAudioHandler<'j> =
|
||||||
|
::jack::contrib::ClosureProcessHandler<(), BoxedAudioHandler<'j>>;
|
||||||
|
|
||||||
|
/// Boxed realtime callback.
|
||||||
|
pub type BoxedAudioHandler<'j> =
|
||||||
|
Box<dyn FnMut(&Client, &ProcessScope) -> Control + Send + Sync + 'j>;
|
||||||
|
|
||||||
|
/// 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<'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 } }
|
||||||
|
|
||||||
|
/// 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<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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
518
src/tengri.rs
518
src/tengri.rs
|
|
@ -9,32 +9,24 @@
|
||||||
#![feature(trait_alias)]
|
#![feature(trait_alias)]
|
||||||
#![feature(type_alias_impl_trait)]
|
#![feature(type_alias_impl_trait)]
|
||||||
#![feature(type_changing_struct_update)]
|
#![feature(type_changing_struct_update)]
|
||||||
|
pub extern crate atomic_float;
|
||||||
|
pub extern crate palette;
|
||||||
|
pub extern crate better_panic;
|
||||||
|
pub extern crate unicode_width;
|
||||||
|
mod tengri_macros;
|
||||||
mod tengri_struct; pub use self::tengri_struct::*;
|
mod tengri_struct; pub use self::tengri_struct::*;
|
||||||
mod tengri_trait; pub use self::tengri_trait::*;
|
mod tengri_trait; pub use self::tengri_trait::*;
|
||||||
mod tengri_impl; pub use self::tengri_impl::*;
|
mod tengri_impl; pub use self::tengri_impl::*;
|
||||||
|
mod tengri_fns; pub use self::tengri_fns::*;
|
||||||
|
|
||||||
#[cfg(test)] pub(crate) use proptest_derive::Arbitrary;
|
#[cfg(test)] pub(crate) use proptest_derive::Arbitrary;
|
||||||
pub extern crate dizzle; pub use dizzle::*;
|
|
||||||
pub extern crate atomic_float; pub(crate) use atomic_float::AtomicF64;
|
pub(crate) use ::{
|
||||||
pub extern crate palette; pub(crate) use ::palette::{*, convert::*, okhsl::*};
|
atomic_float::AtomicF64,
|
||||||
pub extern crate better_panic; pub(crate) use better_panic::{Settings, Verbosity};
|
palette::{*, convert::*, okhsl::*},
|
||||||
pub extern crate unicode_width; pub(crate) use unicode_width::*;
|
better_panic::{Settings, Verbosity},
|
||||||
#[cfg(feature = "jack")] pub extern crate jack;
|
unicode_width::*,
|
||||||
#[cfg(feature = "jack")] pub use jack::{*, contrib::{*, ClosureProcessHandler}};
|
std::{
|
||||||
#[cfg(feature = "tui")] pub extern crate ratatui;
|
|
||||||
#[cfg(feature = "tui")] pub(crate) use ::ratatui::{
|
|
||||||
prelude::{Color, Style, Buffer, Position},
|
|
||||||
style::{Stylize, Modifier, Color::*},
|
|
||||||
backend::{Backend, CrosstermBackend, ClearType},
|
|
||||||
layout::{Size, Rect},
|
|
||||||
buffer::Cell
|
|
||||||
};
|
|
||||||
#[cfg(feature = "tui")] pub extern crate crossterm;
|
|
||||||
#[cfg(feature = "tui")] pub(crate) use ::crossterm::{
|
|
||||||
ExecutableCommand,
|
|
||||||
terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode},
|
|
||||||
event::{poll, read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState},
|
|
||||||
};
|
|
||||||
pub(crate) use ::std::{
|
|
||||||
io::{stdout, Stdout, Write},
|
io::{stdout, Stdout, Write},
|
||||||
sync::{Arc, Weak, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::*}},
|
sync::{Arc, Weak, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::*}},
|
||||||
fmt::{Debug, Display},
|
fmt::{Debug, Display},
|
||||||
|
|
@ -42,469 +34,31 @@ pub(crate) use ::std::{
|
||||||
marker::PhantomData,
|
marker::PhantomData,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
thread::{spawn, JoinHandle}
|
thread::{spawn, JoinHandle}
|
||||||
};
|
|
||||||
|
|
||||||
// Define macros first, so that private macros are available in private modules:
|
|
||||||
|
|
||||||
/// Clear a pre-allocated buffer, then write into it.
|
|
||||||
#[macro_export] macro_rules! rewrite {
|
|
||||||
($buf:ident, $($rest:tt)*) => { |$buf,_,_|{ $buf.clear(); write!($buf, $($rest)*) } }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// FIXME: This macro should be some variant of `eval`, too.
|
|
||||||
/// But taking into account the different signatures (resolving them into 1?)
|
|
||||||
#[cfg(feature = "dsl")] #[macro_export] macro_rules! draw {
|
|
||||||
($State:ident: $Output:ident: $layers:expr) => {
|
|
||||||
impl Draw<$Output> for $State {
|
|
||||||
fn draw (&self, to: &mut $Output) {
|
|
||||||
for layer in $layers { layer(self, to) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// FIXME: This is generic: should be called `eval` and be part of [dizzle].
|
|
||||||
#[cfg(feature = "dsl")] #[macro_export] macro_rules! view {
|
|
||||||
($State:ident: $Output:ident: $namespaces:expr) => {
|
|
||||||
impl Understand<$Output, ()> for $State {
|
|
||||||
fn understand_expr <'a> (&'a self, to: &mut $Output, expr: &'a impl Expression) -> Usually<()> {
|
|
||||||
for namespace in $namespaces { if namespace(self, to, expr)? { return Ok(()) } }
|
|
||||||
Err(format!("{}::<{}, ()>::understand_expr: unexpected: {expr:?}",
|
|
||||||
stringify! { $State },
|
|
||||||
stringify! { $Output }).into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stack things on top of each other,
|
|
||||||
#[macro_export] macro_rules! lay (($($expr:expr),* $(,)?) => {{
|
|
||||||
let bsp = (); $(let bsp = Bsp::b(bsp, $expr);)* bsp
|
|
||||||
}});
|
|
||||||
|
|
||||||
/// Stack southward.
|
|
||||||
#[macro_export] macro_rules! col (($($expr:expr),* $(,)?) => {{
|
|
||||||
let bsp = (); $(let bsp = Bsp::s(bsp, $expr);)* bsp
|
|
||||||
}});
|
|
||||||
|
|
||||||
/// Stack northward.
|
|
||||||
#[macro_export] macro_rules! col_up (($($expr:expr),* $(,)?) => {{
|
|
||||||
let bsp = (); $(let bsp = Bsp::n(bsp, $expr);)* bsp
|
|
||||||
}});
|
|
||||||
|
|
||||||
/// Stack eastward.
|
|
||||||
#[macro_export] macro_rules! row (($($expr:expr),* $(,)?) => {{
|
|
||||||
let bsp = (); $(let bsp = Bsp::e(bsp, $expr);)* bsp
|
|
||||||
}});
|
|
||||||
|
|
||||||
/// Define an enum containing commands, and implement [Command] trait for over given `State`.
|
|
||||||
#[macro_export] macro_rules! def_command (
|
|
||||||
($Command:ident: |$state:ident: $State:ty| {
|
|
||||||
// FIXME: support attrs (docstrings)
|
|
||||||
$($Variant:ident$({$($arg:ident:$Arg:ty),+ $(,)?})?=>$body:expr),* $(,)?
|
|
||||||
})=>{
|
|
||||||
#[derive(Debug)] pub enum $Command {
|
|
||||||
// FIXME: support attrs (docstrings)
|
|
||||||
$($Variant $({ $($arg: $Arg),* })?),*
|
|
||||||
}
|
|
||||||
impl ::tengri::Command<$State> for $Command {
|
|
||||||
fn execute (&self, $state: &mut $State) -> Perhaps<Self> {
|
|
||||||
match self {
|
|
||||||
$(Self::$Variant $({ $($arg),* })? => $body,)*
|
|
||||||
_ => unimplemented!("Command<{}>: {self:?}", stringify!($State)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Implement [Handle] for given `State` and `handler`.
|
|
||||||
#[macro_export] macro_rules! handle {
|
|
||||||
(|$self:ident:$State:ty,$input:ident|$handler:expr) => {
|
|
||||||
impl<E: Engine> ::tengri::Handle<E> for $State {
|
|
||||||
fn handle (&mut $self, $input: &E) -> Perhaps<E::Handled> {
|
|
||||||
$handler
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
($E:ty: |$self:ident:$State:ty,$input:ident|$handler:expr) => {
|
|
||||||
impl ::tengri::Handle<$E> for $State {
|
|
||||||
fn handle (&mut $self, $input: &$E) ->
|
|
||||||
Perhaps<<$E as ::tengri::Input>::Handled>
|
|
||||||
{
|
|
||||||
$handler
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export] macro_rules! tui_main {
|
#[cfg(feature = "lang")] extern crate dizzle;
|
||||||
($expr:expr) => {
|
#[cfg(feature = "lang")] pub use ::dizzle::self as lang;
|
||||||
fn main () -> Usually<()> {
|
|
||||||
let engine = ::tengri::Tui::new(Box::new(stdout()))?;
|
#[cfg(feature = "draw")] pub mod draw;
|
||||||
let state = ::std::sync::Arc::new(std::sync::RwLock::new($expr));
|
|
||||||
engine.run(true, &state)?;
|
#[cfg(feature = "tui")] pub extern crate ratatui;
|
||||||
Ok(())
|
#[cfg(feature = "tui")] pub extern crate crossterm;
|
||||||
|
#[cfg(feature = "tui")] pub(crate) use ::{
|
||||||
|
ratatui::{
|
||||||
|
prelude::{Color, Style, Buffer, Position},
|
||||||
|
style::{Stylize, Modifier, Color::*},
|
||||||
|
backend::{Backend, CrosstermBackend, ClearType},
|
||||||
|
layout::{Size, Rect},
|
||||||
|
buffer::Cell
|
||||||
|
},
|
||||||
|
crossterm::{
|
||||||
|
ExecutableCommand,
|
||||||
|
terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode},
|
||||||
|
event::{poll, read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export] macro_rules! has_color {
|
#[cfg(feature = "sing")] pub extern crate jack;
|
||||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
#[cfg(feature = "sing")] pub mod sing;
|
||||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasColor for $Struct $(<$($L),*$($T),*>)? {
|
#[cfg(feature = "sing")] pub use ::jack::{*, contrib::{*, ClosureProcessHandler}};
|
||||||
fn color (&$self) -> ItemColor { $cb }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Define layout operation.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use tengri::*;
|
|
||||||
/// struct Target { xywh: XYWH<u16> /*platform-specific*/}
|
|
||||||
/// impl tengri::Out for Target {
|
|
||||||
/// type Unit = u16;
|
|
||||||
/// fn area (&self) -> XYWH<u16> { self.xywh }
|
|
||||||
/// fn area_mut (&mut self) -> &mut XYWH<u16> { &mut self.xywh }
|
|
||||||
/// fn place_at <'t, T: Draw<Self> + ?Sized> (&mut self, area: XYWH<u16>, content: &'t T) {}
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// struct State {/*app-specific*/}
|
|
||||||
/// impl<'b> Namespace<'b, bool> for State {}
|
|
||||||
/// impl<'b> Namespace<'b, u16> for State {}
|
|
||||||
/// impl Understand<Target, ()> for State {}
|
|
||||||
///
|
|
||||||
/// # fn main () -> tengri::Usually<()> {
|
|
||||||
/// let state = State {};
|
|
||||||
/// let mut target = Target { xywh: Default::default() };
|
|
||||||
/// evaluate_output_expression(&state, &mut target, &"")?;
|
|
||||||
/// evaluate_output_expression(&state, &mut target, &"(when true (text hello))")?;
|
|
||||||
/// evaluate_output_expression(&state, &mut target, &"(either true (text hello) (text world))")?;
|
|
||||||
/// // TODO test all
|
|
||||||
/// # Ok(()) }
|
|
||||||
/// ```
|
|
||||||
#[cfg(feature = "dsl")] pub fn evaluate_output_expression <'a, O: Out + 'a, S> (
|
|
||||||
state: &S, output: &mut O, expr: &'a impl Expression
|
|
||||||
) -> Usually<bool> where
|
|
||||||
S: Understand<O, ()>
|
|
||||||
+ for<'b>Namespace<'b, bool>
|
|
||||||
+ for<'b>Namespace<'b, O::Unit>
|
|
||||||
{
|
|
||||||
// First element of expression is used for dispatch.
|
|
||||||
// Dispatch is proto-namespaced using separator character
|
|
||||||
let head = expr.head()?;
|
|
||||||
let mut frags = head.src()?.unwrap_or_default().split("/");
|
|
||||||
// The rest of the tokens in the expr are arguments.
|
|
||||||
// Their meanings depend on the dispatched operation
|
|
||||||
let args = expr.tail();
|
|
||||||
let arg0 = args.head();
|
|
||||||
let tail0 = args.tail();
|
|
||||||
let arg1 = tail0.head();
|
|
||||||
let tail1 = tail0.tail();
|
|
||||||
let arg2 = tail1.head();
|
|
||||||
// And we also have to do the above binding dance
|
|
||||||
// so that the Perhaps<token>s remain in scope.
|
|
||||||
match frags.next() {
|
|
||||||
|
|
||||||
Some("when") => output.place(&When::new(
|
|
||||||
state.namespace(arg0?)?.unwrap(),
|
|
||||||
Thunk::new(move|output: &mut O|state.understand(output, &arg1).unwrap())
|
|
||||||
)),
|
|
||||||
|
|
||||||
Some("either") => output.place(&Either::new(
|
|
||||||
state.namespace(arg0?)?.unwrap(),
|
|
||||||
Thunk::new(move|output: &mut O|state.understand(output, &arg1).unwrap()),
|
|
||||||
Thunk::new(move|output: &mut O|state.understand(output, &arg2).unwrap())
|
|
||||||
)),
|
|
||||||
|
|
||||||
Some("bsp") => output.place(&{
|
|
||||||
let a = Thunk::new(move|output: &mut O|state.understand(output, &arg0).unwrap());
|
|
||||||
let b = Thunk::new(move|output: &mut O|state.understand(output, &arg1).unwrap());
|
|
||||||
match frags.next() {
|
|
||||||
Some("n") => Bsp::n(a, b),
|
|
||||||
Some("s") => Bsp::s(a, b),
|
|
||||||
Some("e") => Bsp::e(a, b),
|
|
||||||
Some("w") => Bsp::w(a, b),
|
|
||||||
Some("a") => Bsp::a(a, b),
|
|
||||||
Some("b") => Bsp::b(a, b),
|
|
||||||
frag => unimplemented!("bsp/{frag:?}")
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
Some("align") => output.place(&{
|
|
||||||
let a = Thunk::new(move|output: &mut O|state.understand(output, &arg0).unwrap());
|
|
||||||
match frags.next() {
|
|
||||||
Some("n") => Align::n(a),
|
|
||||||
Some("s") => Align::s(a),
|
|
||||||
Some("e") => Align::e(a),
|
|
||||||
Some("w") => Align::w(a),
|
|
||||||
Some("x") => Align::x(a),
|
|
||||||
Some("y") => Align::y(a),
|
|
||||||
Some("c") => Align::c(a),
|
|
||||||
frag => unimplemented!("align/{frag:?}")
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
Some("fill") => output.place(&{
|
|
||||||
let a = Thunk::new(move|output: &mut O|state.understand(output, &arg0).unwrap());
|
|
||||||
match frags.next() {
|
|
||||||
Some("xy") | None => Fill::XY(a),
|
|
||||||
Some("x") => Fill::X(a),
|
|
||||||
Some("y") => Fill::Y(a),
|
|
||||||
frag => unimplemented!("fill/{frag:?}")
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
Some("fixed") => output.place(&{
|
|
||||||
let axis = frags.next();
|
|
||||||
let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") };
|
|
||||||
let cb = Thunk::new(move|output: &mut O|state.understand(output, &arg).unwrap());
|
|
||||||
match axis {
|
|
||||||
Some("xy") | None => Fixed::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb),
|
|
||||||
Some("x") => Fixed::X(state.namespace(arg0?)?.unwrap(), cb),
|
|
||||||
Some("y") => Fixed::Y(state.namespace(arg0?)?.unwrap(), cb),
|
|
||||||
frag => unimplemented!("fixed/{frag:?} ({expr:?}) ({head:?}) ({:?})",
|
|
||||||
head.src()?.unwrap_or_default().split("/").next())
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
Some("min") => output.place(&{
|
|
||||||
let axis = frags.next();
|
|
||||||
let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") };
|
|
||||||
let cb = Thunk::new(move|output: &mut O|state.understand(output, &arg).unwrap());
|
|
||||||
match axis {
|
|
||||||
Some("xy") | None => Min::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb),
|
|
||||||
Some("x") => Min::X(state.namespace(arg0?)?.unwrap(), cb),
|
|
||||||
Some("y") => Min::Y(state.namespace(arg0?)?.unwrap(), cb),
|
|
||||||
frag => unimplemented!("min/{frag:?}")
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
Some("max") => output.place(&{
|
|
||||||
let axis = frags.next();
|
|
||||||
let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") };
|
|
||||||
let cb = Thunk::new(move|output: &mut O|state.understand(output, &arg).unwrap());
|
|
||||||
match axis {
|
|
||||||
Some("xy") | None => Max::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb),
|
|
||||||
Some("x") => Max::X(state.namespace(arg0?)?.unwrap(), cb),
|
|
||||||
Some("y") => Max::Y(state.namespace(arg0?)?.unwrap(), cb),
|
|
||||||
frag => unimplemented!("max/{frag:?}")
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
Some("push") => output.place(&{
|
|
||||||
let axis = frags.next();
|
|
||||||
let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") };
|
|
||||||
let cb = Thunk::new(move|output: &mut O|state.understand(output, &arg).unwrap());
|
|
||||||
match axis {
|
|
||||||
Some("xy") | None => Push::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb),
|
|
||||||
Some("x") => Push::X(state.namespace(arg0?)?.unwrap(), cb),
|
|
||||||
Some("y") => Push::Y(state.namespace(arg0?)?.unwrap(), cb),
|
|
||||||
frag => unimplemented!("push/{frag:?}")
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
_ => return Ok(false)
|
|
||||||
|
|
||||||
};
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Trim string with [unicode_width].
|
|
||||||
pub fn trim_string (max_width: usize, input: impl AsRef<str>) -> String {
|
|
||||||
let input = input.as_ref();
|
|
||||||
let mut output = Vec::with_capacity(input.len());
|
|
||||||
let mut width: usize = 1;
|
|
||||||
let mut chars = input.chars();
|
|
||||||
while let Some(c) = chars.next() {
|
|
||||||
if width > max_width {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
output.push(c);
|
|
||||||
width += c.width().unwrap_or(0);
|
|
||||||
}
|
|
||||||
return output.into_iter().collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn width_chars_max (max: u16, text: impl AsRef<str>) -> u16 {
|
|
||||||
let mut width: u16 = 0;
|
|
||||||
let mut chars = text.as_ref().chars();
|
|
||||||
while let Some(c) = chars.next() {
|
|
||||||
width += c.width().unwrap_or(0) as u16;
|
|
||||||
if width > max {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return width
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline] pub fn map_south<O: Out>(
|
|
||||||
item_offset: O::Unit,
|
|
||||||
item_height: O::Unit,
|
|
||||||
item: impl Content<O>
|
|
||||||
) -> impl Content<O> {
|
|
||||||
Push::Y(item_offset, Fixed::Y(item_height, Fill::X(item)))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline] pub fn map_south_west<O: Out>(
|
|
||||||
item_offset: O::Unit,
|
|
||||||
item_height: O::Unit,
|
|
||||||
item: impl Content<O>
|
|
||||||
) -> impl Content<O> {
|
|
||||||
Push::Y(item_offset, Align::nw(Fixed::Y(item_height, Fill::X(item))))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline] pub fn map_east<O: Out>(
|
|
||||||
item_offset: O::Unit,
|
|
||||||
item_width: O::Unit,
|
|
||||||
item: impl Content<O>
|
|
||||||
) -> impl Content<O> {
|
|
||||||
Push::X(item_offset, Align::w(Fixed::X(item_width, Fill::Y(item))))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)] mod test {
|
|
||||||
use proptest::{prelude::*, option::of};
|
|
||||||
use proptest_derive::Arbitrary;
|
|
||||||
use crate::*;
|
|
||||||
use Direction::*;
|
|
||||||
|
|
||||||
proptest! {
|
|
||||||
#[test] fn proptest_direction (
|
|
||||||
d in prop_oneof![
|
|
||||||
Just(North), Just(South),
|
|
||||||
Just(East), Just(West),
|
|
||||||
Just(Above), Just(Below)
|
|
||||||
],
|
|
||||||
x in u16::MIN..u16::MAX,
|
|
||||||
y in u16::MIN..u16::MAX,
|
|
||||||
w in u16::MIN..u16::MAX,
|
|
||||||
h in u16::MIN..u16::MAX,
|
|
||||||
a in u16::MIN..u16::MAX,
|
|
||||||
) {
|
|
||||||
let _ = d.split_fixed(XYWH(x, y, w, h), a);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
proptest! {
|
|
||||||
#[test] fn proptest_area (
|
|
||||||
x in u16::MIN..u16::MAX,
|
|
||||||
y in u16::MIN..u16::MAX,
|
|
||||||
w in u16::MIN..u16::MAX,
|
|
||||||
h in u16::MIN..u16::MAX,
|
|
||||||
a in u16::MIN..u16::MAX,
|
|
||||||
b in u16::MIN..u16::MAX,
|
|
||||||
) {
|
|
||||||
let _: XYWH<u16> = XYWH::zero();
|
|
||||||
//let _: XYWH<u16> = XYWH::from_position([a, b]);
|
|
||||||
//let _: XYWH<u16> = XYWH::from_size([a, b]);
|
|
||||||
let area: XYWH<u16> = XYWH(x, y, w, h);
|
|
||||||
//let _ = area.expect_min(a, b);
|
|
||||||
let _ = area.xy();
|
|
||||||
let _ = area.wh();
|
|
||||||
//let _ = area.xywh();
|
|
||||||
let _ = area.clipped_h(a);
|
|
||||||
let _ = area.clipped_w(b);
|
|
||||||
let _ = area.clipped(WH(a, b));
|
|
||||||
//let _ = area.set_w(a);
|
|
||||||
//let _ = area.set_h(b);
|
|
||||||
let _ = area.x2();
|
|
||||||
let _ = area.y2();
|
|
||||||
let _ = area.lrtb();
|
|
||||||
let _ = area.center();
|
|
||||||
let _ = area.centered();
|
|
||||||
let _ = area.centered_x(a);
|
|
||||||
let _ = area.centered_y(b);
|
|
||||||
let _ = area.centered_xy([a, b]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
proptest! {
|
|
||||||
#[test] fn proptest_size (
|
|
||||||
x in u16::MIN..u16::MAX,
|
|
||||||
y in u16::MIN..u16::MAX,
|
|
||||||
a in u16::MIN..u16::MAX,
|
|
||||||
b in u16::MIN..u16::MAX,
|
|
||||||
) {
|
|
||||||
let size = WH(x, y);
|
|
||||||
let _ = size.w();
|
|
||||||
let _ = size.h();
|
|
||||||
let _ = size.wh();
|
|
||||||
let _ = size.clip_w(a);
|
|
||||||
let _ = size.clip_h(b);
|
|
||||||
//let _ = size.expect_min(a, b);
|
|
||||||
//let _ = size.to_area_pos();
|
|
||||||
//let _ = size.to_area_size();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! test_op_transform {
|
|
||||||
($fn:ident, $Op:ident) => {
|
|
||||||
proptest! {
|
|
||||||
#[test] fn $fn (
|
|
||||||
op_x in of(u16::MIN..u16::MAX),
|
|
||||||
op_y in of(u16::MIN..u16::MAX),
|
|
||||||
content in "\\PC*",
|
|
||||||
x in u16::MIN..u16::MAX,
|
|
||||||
y in u16::MIN..u16::MAX,
|
|
||||||
w in u16::MIN..u16::MAX,
|
|
||||||
h in u16::MIN..u16::MAX,
|
|
||||||
) {
|
|
||||||
if let Some(op) = match (op_x, op_y) {
|
|
||||||
(Some(x), Some(y)) => Some($Op::XY(x, y, content)),
|
|
||||||
(Some(x), None) => Some($Op::X(x, content)),
|
|
||||||
(None, Some(y)) => Some($Op::Y(y, content)),
|
|
||||||
_ => None
|
|
||||||
} {
|
|
||||||
//assert_eq!(Content::layout(&op, [x, y, w, h]),
|
|
||||||
//Draw::layout(&op, [x, y, w, h]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
test_op_transform!(proptest_op_fixed, Fixed);
|
|
||||||
test_op_transform!(proptest_op_min, Min);
|
|
||||||
test_op_transform!(proptest_op_max, Max);
|
|
||||||
test_op_transform!(proptest_op_push, Push);
|
|
||||||
test_op_transform!(proptest_op_pull, Pull);
|
|
||||||
test_op_transform!(proptest_op_shrink, Shrink);
|
|
||||||
test_op_transform!(proptest_op_expand, Expand);
|
|
||||||
test_op_transform!(proptest_op_padding, Pad);
|
|
||||||
|
|
||||||
proptest! {
|
|
||||||
#[test] fn proptest_op_bsp (
|
|
||||||
d in prop_oneof![
|
|
||||||
Just(North), Just(South),
|
|
||||||
Just(East), Just(West),
|
|
||||||
Just(Above), Just(Below)
|
|
||||||
],
|
|
||||||
a in "\\PC*",
|
|
||||||
b in "\\PC*",
|
|
||||||
x in u16::MIN..u16::MAX,
|
|
||||||
y in u16::MIN..u16::MAX,
|
|
||||||
w in u16::MIN..u16::MAX,
|
|
||||||
h in u16::MIN..u16::MAX,
|
|
||||||
) {
|
|
||||||
let bsp = Bsp(d, a, b);
|
|
||||||
//assert_eq!(
|
|
||||||
//Content::layout(&bsp, [x, y, w, h]),
|
|
||||||
//Draw::layout(&bsp, [x, y, w, h]),
|
|
||||||
//);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//#[test] fn test_tui_engine () -> Usually<()> {
|
|
||||||
////use std::sync::{Arc, RwLock};
|
|
||||||
//struct TestComponent(String);
|
|
||||||
//impl Draw<TuiOut> for TestComponent {
|
|
||||||
//fn draw (&self, _to: &mut TuiOut) {}
|
|
||||||
//}
|
|
||||||
//impl Handle<TuiIn> for TestComponent {
|
|
||||||
//fn handle (&mut self, _from: &TuiIn) -> Perhaps<bool> { Ok(None) }
|
|
||||||
//}
|
|
||||||
//let engine = Tui::new(Box::<&mut Vec<u8>>::new(vec![0u8;0].as_mut()))?;
|
|
||||||
//let state = engine.run(false, &Arc::new(RwLock::new(TestComponent("hello world".into()))))?;
|
|
||||||
//state.read().unwrap().exited.store(true, std::sync::atomic::Ordering::Relaxed);
|
|
||||||
//Ok(())
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
182
src/tengri_fns.rs
Normal file
182
src/tengri_fns.rs
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Interpret layout operation.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use tengri::*;
|
||||||
|
/// struct Target { xywh: XYWH<u16> /*platform-specific*/}
|
||||||
|
/// impl tengri::Out for Target {
|
||||||
|
/// type Unit = u16;
|
||||||
|
/// fn area (&self) -> XYWH<u16> { self.xywh }
|
||||||
|
/// fn area_mut (&mut self) -> &mut XYWH<u16> { &mut self.xywh }
|
||||||
|
/// fn show <'t, T: Draw<Self> + ?Sized> (&mut self, area: XYWH<u16>, content: &'t T) {}
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// struct State {/*app-specific*/}
|
||||||
|
/// impl<'b> Namespace<'b, bool> for State {}
|
||||||
|
/// impl<'b> Namespace<'b, u16> for State {}
|
||||||
|
/// impl Understand<Target, ()> for State {}
|
||||||
|
///
|
||||||
|
/// # fn main () -> tengri::Usually<()> {
|
||||||
|
/// let state = State {};
|
||||||
|
/// let mut target = Target { xywh: Default::default() };
|
||||||
|
/// eval_view(&state, &mut target, &"")?;
|
||||||
|
/// eval_view(&state, &mut target, &"(when true (text hello))")?;
|
||||||
|
/// eval_view(&state, &mut target, &"(either true (text hello) (text world))")?;
|
||||||
|
/// // TODO test all
|
||||||
|
/// # Ok(()) }
|
||||||
|
/// ```
|
||||||
|
#[cfg(feature = "dsl")] pub fn eval_view <'a, O: Screen + 'a, S> (
|
||||||
|
state: &S, output: &mut O, expr: &'a impl Expression
|
||||||
|
) -> Usually<bool> where
|
||||||
|
S: Understand<O, ()>
|
||||||
|
+ for<'b>Namespace<'b, bool>
|
||||||
|
+ for<'b>Namespace<'b, O::Unit>
|
||||||
|
{
|
||||||
|
// First element of expression is used for dispatch.
|
||||||
|
// Dispatch is proto-namespaced using separator character
|
||||||
|
let head = expr.head()?;
|
||||||
|
let mut frags = head.src()?.unwrap_or_default().split("/");
|
||||||
|
// The rest of the tokens in the expr are arguments.
|
||||||
|
// Their meanings depend on the dispatched operation
|
||||||
|
let args = expr.tail();
|
||||||
|
let arg0 = args.head();
|
||||||
|
let tail0 = args.tail();
|
||||||
|
let arg1 = tail0.head();
|
||||||
|
let tail1 = tail0.tail();
|
||||||
|
let arg2 = tail1.head();
|
||||||
|
// And we also have to do the above binding dance
|
||||||
|
// so that the Perhaps<token>s remain in scope.
|
||||||
|
match frags.next() {
|
||||||
|
|
||||||
|
Some("when") => output.place(&When::new(
|
||||||
|
state.namespace(arg0?)?.unwrap(),
|
||||||
|
Thunk::new(move|output: &mut O|state.understand(output, &arg1).unwrap())
|
||||||
|
)),
|
||||||
|
|
||||||
|
Some("either") => output.place(&Either::new(
|
||||||
|
state.namespace(arg0?)?.unwrap(),
|
||||||
|
Thunk::new(move|output: &mut O|state.understand(output, &arg1).unwrap()),
|
||||||
|
Thunk::new(move|output: &mut O|state.understand(output, &arg2).unwrap())
|
||||||
|
)),
|
||||||
|
|
||||||
|
Some("bsp") => output.place(&{
|
||||||
|
let a = Thunk::new(move|output: &mut O|state.understand(output, &arg0).unwrap());
|
||||||
|
let b = Thunk::new(move|output: &mut O|state.understand(output, &arg1).unwrap());
|
||||||
|
match frags.next() {
|
||||||
|
Some("n") => Bsp::n(a, b),
|
||||||
|
Some("s") => Bsp::s(a, b),
|
||||||
|
Some("e") => Bsp::e(a, b),
|
||||||
|
Some("w") => Bsp::w(a, b),
|
||||||
|
Some("a") => Bsp::a(a, b),
|
||||||
|
Some("b") => Bsp::b(a, b),
|
||||||
|
frag => unimplemented!("bsp/{frag:?}")
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
Some("align") => output.place(&{
|
||||||
|
let a = Thunk::new(move|output: &mut O|state.understand(output, &arg0).unwrap());
|
||||||
|
match frags.next() {
|
||||||
|
Some("n") => Align::n(a),
|
||||||
|
Some("s") => Align::s(a),
|
||||||
|
Some("e") => Align::e(a),
|
||||||
|
Some("w") => Align::w(a),
|
||||||
|
Some("x") => Align::x(a),
|
||||||
|
Some("y") => Align::y(a),
|
||||||
|
Some("c") => Align::c(a),
|
||||||
|
frag => unimplemented!("align/{frag:?}")
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
Some("fill") => output.place(&{
|
||||||
|
let a = Thunk::new(move|output: &mut O|state.understand(output, &arg0).unwrap());
|
||||||
|
match frags.next() {
|
||||||
|
Some("xy") | None => Fill::XY(a),
|
||||||
|
Some("x") => Fill::X(a),
|
||||||
|
Some("y") => Fill::Y(a),
|
||||||
|
frag => unimplemented!("fill/{frag:?}")
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
Some("fixed") => output.place(&{
|
||||||
|
let axis = frags.next();
|
||||||
|
let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") };
|
||||||
|
let cb = Thunk::new(move|output: &mut O|state.understand(output, &arg).unwrap());
|
||||||
|
match axis {
|
||||||
|
Some("xy") | None => Fixed::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb),
|
||||||
|
Some("x") => Fixed::X(state.namespace(arg0?)?.unwrap(), cb),
|
||||||
|
Some("y") => Fixed::Y(state.namespace(arg0?)?.unwrap(), cb),
|
||||||
|
frag => unimplemented!("fixed/{frag:?} ({expr:?}) ({head:?}) ({:?})",
|
||||||
|
head.src()?.unwrap_or_default().split("/").next())
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
Some("min") => output.place(&{
|
||||||
|
let axis = frags.next();
|
||||||
|
let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") };
|
||||||
|
let cb = Thunk::new(move|output: &mut O|state.understand(output, &arg).unwrap());
|
||||||
|
match axis {
|
||||||
|
Some("xy") | None => Min::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb),
|
||||||
|
Some("x") => Min::X(state.namespace(arg0?)?.unwrap(), cb),
|
||||||
|
Some("y") => Min::Y(state.namespace(arg0?)?.unwrap(), cb),
|
||||||
|
frag => unimplemented!("min/{frag:?}")
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
Some("max") => output.place(&{
|
||||||
|
let axis = frags.next();
|
||||||
|
let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") };
|
||||||
|
let cb = Thunk::new(move|output: &mut O|state.understand(output, &arg).unwrap());
|
||||||
|
match axis {
|
||||||
|
Some("xy") | None => Max::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb),
|
||||||
|
Some("x") => Max::X(state.namespace(arg0?)?.unwrap(), cb),
|
||||||
|
Some("y") => Max::Y(state.namespace(arg0?)?.unwrap(), cb),
|
||||||
|
frag => unimplemented!("max/{frag:?}")
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
Some("push") => output.place(&{
|
||||||
|
let axis = frags.next();
|
||||||
|
let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") };
|
||||||
|
let cb = Thunk::new(move|output: &mut O|state.understand(output, &arg).unwrap());
|
||||||
|
match axis {
|
||||||
|
Some("xy") | None => Push::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb),
|
||||||
|
Some("x") => Push::X(state.namespace(arg0?)?.unwrap(), cb),
|
||||||
|
Some("y") => Push::Y(state.namespace(arg0?)?.unwrap(), cb),
|
||||||
|
frag => unimplemented!("push/{frag:?}")
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
_ => return Ok(false)
|
||||||
|
|
||||||
|
};
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trim string with [unicode_width].
|
||||||
|
pub fn trim_string (max_width: usize, input: impl AsRef<str>) -> String {
|
||||||
|
let input = input.as_ref();
|
||||||
|
let mut output = Vec::with_capacity(input.len());
|
||||||
|
let mut width: usize = 1;
|
||||||
|
let mut chars = input.chars();
|
||||||
|
while let Some(c) = chars.next() {
|
||||||
|
if width > max_width {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
output.push(c);
|
||||||
|
width += c.width().unwrap_or(0);
|
||||||
|
}
|
||||||
|
return output.into_iter().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn width_chars_max (max: u16, text: impl AsRef<str>) -> u16 {
|
||||||
|
let mut width: u16 = 0;
|
||||||
|
let mut chars = text.as_ref().chars();
|
||||||
|
while let Some(c) = chars.next() {
|
||||||
|
width += c.width().unwrap_or(0) as u16;
|
||||||
|
if width > max {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return width
|
||||||
|
}
|
||||||
1080
src/tengri_impl.rs
1080
src/tengri_impl.rs
File diff suppressed because it is too large
Load diff
60
src/tengri_macros.rs
Normal file
60
src/tengri_macros.rs
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Define an enum containing commands, and implement [Command] trait for over given `State`.
|
||||||
|
#[macro_export] macro_rules! def_command (
|
||||||
|
($Command:ident: |$state:ident: $State:ty| {
|
||||||
|
// FIXME: support attrs (docstrings)
|
||||||
|
$($Variant:ident$({$($arg:ident:$Arg:ty),+ $(,)?})?=>$body:expr),* $(,)?
|
||||||
|
})=>{
|
||||||
|
#[derive(Debug)] pub enum $Command {
|
||||||
|
// FIXME: support attrs (docstrings)
|
||||||
|
$($Variant $({ $($arg: $Arg),* })?),*
|
||||||
|
}
|
||||||
|
impl ::tengri::Command<$State> for $Command {
|
||||||
|
fn execute (&self, $state: &mut $State) -> Perhaps<Self> {
|
||||||
|
match self {
|
||||||
|
$(Self::$Variant $({ $($arg),* })? => $body,)*
|
||||||
|
_ => unimplemented!("Command<{}>: {self:?}", stringify!($State)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Implement [Handle] for given `State` and `handler`.
|
||||||
|
#[macro_export] macro_rules! handle {
|
||||||
|
(|$self:ident:$State:ty,$input:ident|$handler:expr) => {
|
||||||
|
impl<E: Engine> ::tengri::Handle<E> for $State {
|
||||||
|
fn handle (&mut $self, $input: &E) -> Perhaps<E::Handled> {
|
||||||
|
$handler
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($E:ty: |$self:ident:$State:ty,$input:ident|$handler:expr) => {
|
||||||
|
impl ::tengri::Handle<$E> for $State {
|
||||||
|
fn handle (&mut $self, $input: &$E) ->
|
||||||
|
Perhaps<<$E as ::tengri::Input>::Handled>
|
||||||
|
{
|
||||||
|
$handler
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export] macro_rules! tui_main {
|
||||||
|
($expr:expr) => {
|
||||||
|
fn main () -> Usually<()> {
|
||||||
|
let engine = ::tengri::Tui::new(Box::new(stdout()))?;
|
||||||
|
let state = ::std::sync::Arc::new(std::sync::RwLock::new($expr));
|
||||||
|
engine.run(true, &state)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export] macro_rules! has_color {
|
||||||
|
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||||
|
impl $(<$($L),*$($T $(: $U)?),*>)? HasColor for $Struct $(<$($L),*$($T),*>)? {
|
||||||
|
fn color (&$self) -> ItemColor { $cb }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,28 +1,5 @@
|
||||||
pub use self::logical::*; mod logical {
|
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
/// Thunks can be natural error boundaries!
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let _ = tengri::ErrorBoundary::<tengri::Tui, &'static str>::new(Ok(Some("hello")));
|
|
||||||
/// let _ = tengri::ErrorBoundary::<tengri::Tui, &'static str>::new(Ok(None));
|
|
||||||
/// let _ = tengri::ErrorBoundary::<tengri::Tui, &'static str>::new(Err("draw fail".into()));
|
|
||||||
/// ```
|
|
||||||
pub struct ErrorBoundary<O: Out, T: Draw<O>>(
|
|
||||||
pub Perhaps<T>,
|
|
||||||
pub PhantomData<O>,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Late-evaluate a rendering.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let _ = tengri::Thunk::<tengri::Tui, _>::new(|out|{});
|
|
||||||
/// ```
|
|
||||||
pub struct Thunk<O: Out, F: Fn(&mut O)>(
|
|
||||||
pub F,
|
|
||||||
pub PhantomData<O>,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Memoize a rendering.
|
/// Memoize a rendering.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
|
|
@ -32,117 +9,19 @@ pub use self::logical::*; mod logical {
|
||||||
pub value: T,
|
pub value: T,
|
||||||
pub view: Arc<RwLock<U>>
|
pub view: Arc<RwLock<U>>
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub use self::spatial::*; mod spatial {
|
pub use self::spatial::*; mod spatial {
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
/// A binary split or layer.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let _ = tengri::Bsp::n((), ());
|
|
||||||
/// let _ = tengri::Bsp::s((), ());
|
|
||||||
/// let _ = tengri::Bsp::e((), ());
|
|
||||||
/// let _ = tengri::Bsp::w((), ());
|
|
||||||
/// let _ = tengri::Bsp::a((), ());
|
|
||||||
/// let _ = tengri::Bsp::b((), ());
|
|
||||||
/// ```
|
|
||||||
pub struct Bsp<Head, Tail>(
|
|
||||||
/// Direction of split
|
|
||||||
pub(crate) Direction,
|
|
||||||
/// First element.
|
|
||||||
pub(crate) Head,
|
|
||||||
/// Second element.
|
|
||||||
pub(crate) Tail,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// A point (X, Y).
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let xy = tengri::XY(0u16, 0);
|
|
||||||
/// ```
|
|
||||||
#[cfg_attr(test, derive(Arbitrary))]
|
|
||||||
#[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct XY<C: Coord>(
|
|
||||||
pub C, pub C
|
|
||||||
);
|
|
||||||
|
|
||||||
/// A size (Width, Height).
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let wh = tengri::WH(0u16, 0);
|
|
||||||
/// ```
|
|
||||||
#[cfg_attr(test, derive(Arbitrary))]
|
|
||||||
#[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct WH<C: Coord>(
|
|
||||||
pub C, pub C
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Point with size.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let xywh = tengri::XYWH(0u16, 0, 0, 0);
|
|
||||||
/// assert_eq!(tengri::XYWH(10u16, 10, 20, 20).center(), tengri::XY(20, 20));
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// * [ ] TODO: anchor field (determines at which corner/side is X0 Y0)
|
|
||||||
///
|
|
||||||
#[cfg_attr(test, derive(Arbitrary))]
|
|
||||||
#[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct XYWH<C: Coord>(
|
|
||||||
pub C, pub C, pub C, pub C
|
|
||||||
);
|
|
||||||
|
|
||||||
/// A cardinal direction.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let direction = tengri::Direction::Above;
|
|
||||||
/// ```
|
|
||||||
#[cfg_attr(test, derive(Arbitrary))]
|
|
||||||
#[derive(Copy, Clone, PartialEq, Debug)] pub enum Direction {
|
|
||||||
North, South, East, West, Above, Below
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 9th of area to place.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let alignment = tengri::Alignment::Center;
|
|
||||||
/// ```
|
|
||||||
#[cfg_attr(test, derive(Arbitrary))]
|
|
||||||
#[derive(Debug, Copy, Clone, Default)] pub enum Alignment {
|
|
||||||
#[default] Center, X, Y, NW, N, NE, E, SE, S, SW, W
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A widget that tracks its rendered width and height.
|
/// A widget that tracks its rendered width and height.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// let measure = tengri::Measure::<tengri::Tui>::default();
|
/// let measure = tengri::Measure::<u16>::default();
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Default)] pub struct Measure<O: Out> {
|
#[derive(Default)] pub struct Measure<N: Coord> {
|
||||||
pub __: PhantomData<O>,
|
|
||||||
pub x: Arc<AtomicUsize>,
|
pub x: Arc<AtomicUsize>,
|
||||||
pub y: Arc<AtomicUsize>,
|
pub y: Arc<AtomicUsize>,
|
||||||
}
|
pub __: PhantomData<N>,
|
||||||
|
|
||||||
/// Show an item only when a condition is true.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// fn test () -> impl tengri::Draw<tengri::Tui> {
|
|
||||||
/// tengri::when(true, "Yes")
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
pub struct When<O, T>(pub bool, pub T, pub PhantomData<O>);
|
|
||||||
pub const fn when<O, T>(condition: bool, content: T) -> When<O, T> {
|
|
||||||
When(condition, content, PhantomData)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Show one item if a condition is true and another if the condition is false.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// fn test () -> impl tengri::Draw<tengri::Tui> {
|
|
||||||
/// tengri::either(true, "Yes", "No")
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
pub struct Either<E, A, B>(pub bool, pub A, pub B, pub PhantomData<E>);
|
|
||||||
pub const fn either<E, A, B>(condition: bool, content_a: A, content_b: B) -> Either<E, A, B> {
|
|
||||||
Either(condition, content_a, content_b, PhantomData)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Increment X and/or Y coordinate.
|
/// Increment X and/or Y coordinate.
|
||||||
|
|
@ -173,20 +52,6 @@ pub use self::spatial::*; mod spatial {
|
||||||
/// ```
|
/// ```
|
||||||
pub enum Fixed<U, A> { X(U, A), Y(U, A), XY(U, U, A), }
|
pub enum Fixed<U, A> { X(U, A), Y(U, A), XY(U, U, A), }
|
||||||
|
|
||||||
/// Set the maximum width and/or height of the content.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let maximum = tengri::Max::XY(3, 5, "Hello"); // 3x1
|
|
||||||
/// ```
|
|
||||||
pub enum Max<U, A> { X(U, A), Y(U, A), XY(U, U, A), }
|
|
||||||
|
|
||||||
/// Set the minimum width and/or height of the content.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let minimum = tengri::Min::XY(3, 5, "Hello"); // 5x5
|
|
||||||
/// ```
|
|
||||||
pub enum Min<U, A> { X(U, A), Y(U, A), XY(U, U, A), }
|
|
||||||
|
|
||||||
/// Decrease the width and/or height of the content.
|
/// Decrease the width and/or height of the content.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
|
|
@ -247,11 +112,9 @@ pub use self::spatial::*; mod spatial {
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use tengri::{Bounded, XYWH};
|
/// use tengri::{Bounded, XYWH};
|
||||||
/// let area = XYWH(0, 0, 0, 0);
|
/// let bounded: Bounded<tengri::Tui, _> = Bounded(0, 0 ,0 ,0 ,"");
|
||||||
/// let content = "";
|
|
||||||
/// let bounded: Bounded<tengri::Tui, _> = Bounded(area, content);
|
|
||||||
/// ```
|
/// ```
|
||||||
pub struct Bounded<O: Out, D>(pub XYWH<O::Unit>, pub D);
|
pub struct Bounded<N: Coord, D>(N, N, N, N, pub D);
|
||||||
|
|
||||||
/// Draws items from an iterator.
|
/// Draws items from an iterator.
|
||||||
///
|
///
|
||||||
|
|
@ -303,75 +166,44 @@ pub use self::spatial::*; mod spatial {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "tui")] pub use self::terminal::*;
|
#[derive(Clone)] pub struct Exit(Arc<AtomicBool>);
|
||||||
#[cfg(feature = "tui")] mod terminal {
|
impl Exit {
|
||||||
use crate::*;
|
pub fn run <T> (run: impl Fn()->Usually<T>) -> Usually<T> {
|
||||||
|
run(Self(Arc::new(AtomicBool::new(false))))
|
||||||
/// The TUI engine.
|
}
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # fn main () -> tengri::Usually<()> {
|
|
||||||
/// let tui = tengri::Tui::new(Box::new(vec![0u8;0]))?;
|
|
||||||
/// # Ok(()) }
|
|
||||||
/// ```
|
|
||||||
pub struct Tui {
|
|
||||||
/// Exit flag
|
|
||||||
pub exited: Arc<AtomicBool>,
|
|
||||||
/// Error message
|
|
||||||
pub error: Option<Arc<str>>,
|
|
||||||
/// Performance counter
|
|
||||||
pub perf: PerfModel,
|
|
||||||
/// Ratatui backend
|
|
||||||
pub backend: RwLock<CrosstermBackend<Box<dyn Write + Send + Sync>>>,
|
|
||||||
/// Current tnput event
|
|
||||||
pub event: Option<TuiEvent>,
|
|
||||||
/// Current output buffer
|
|
||||||
pub buffer: RwLock<Buffer>,
|
|
||||||
/// FIXME unused?
|
|
||||||
pub area: XYWH<u16>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)] pub struct TuiThread {
|
#[derive(Debug)] pub struct Thread {
|
||||||
|
/// Exit flag.
|
||||||
pub exit: Arc<AtomicBool>,
|
pub exit: Arc<AtomicBool>,
|
||||||
|
/// Performance counter.
|
||||||
pub perf: Arc<PerfModel>,
|
pub perf: Arc<PerfModel>,
|
||||||
|
/// Use this to wait for the thread to finish.
|
||||||
pub join: JoinHandle<()>,
|
pub join: JoinHandle<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)] pub struct TuiEvent(
|
#[cfg(feature = "tui")] pub use self::terminal::*;
|
||||||
pub Event
|
#[cfg(feature = "tui")] mod terminal {
|
||||||
);
|
use crate::*;
|
||||||
|
/// TUI input loop event.
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)] pub struct TuiKey(
|
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)]
|
||||||
pub Option<KeyCode>,
|
pub struct TuiEvent(pub Event);
|
||||||
pub KeyModifiers
|
/// TUI key spec.
|
||||||
);
|
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)]
|
||||||
|
pub struct TuiKey(pub Option<KeyCode>, pub KeyModifiers);
|
||||||
/// TUI buffer sized by `usize` instead of `u16`.
|
/// TUI buffer sized by `usize` instead of `u16`.
|
||||||
#[derive(Default)] pub struct BigBuffer {
|
#[derive(Default)] pub struct BigBuffer {
|
||||||
pub width: usize,
|
pub width: usize,
|
||||||
pub height: usize,
|
pub height: usize,
|
||||||
pub content: Vec<Cell>
|
pub content: Vec<Cell>
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO DOCUMENTME
|
|
||||||
pub struct Foreground<Color, Item>(pub Color, pub Item);
|
pub struct Foreground<Color, Item>(pub Color, pub Item);
|
||||||
|
|
||||||
// TODO DOCUMENTME
|
|
||||||
pub struct Background<Color, Item>(pub Color, pub Item);
|
pub struct Background<Color, Item>(pub Color, pub Item);
|
||||||
|
|
||||||
pub struct Modify<T>(pub bool, pub Modifier, pub T);
|
pub struct Modify<T>(pub bool, pub Modifier, pub T);
|
||||||
|
|
||||||
pub struct Styled<T>(pub Option<Style>, pub T);
|
pub struct Styled<T>(pub Option<Style>, pub T);
|
||||||
|
|
||||||
/// A cell that takes up 3 rows on its own,
|
/// A cell that takes up 3 rows on its own,
|
||||||
/// but stacks, giving (N+1)*2 rows per N cells.
|
/// but stacks, giving (N+1)*2 rows per N cells.
|
||||||
pub struct Phat<T> {
|
pub struct Phat<T> { pub width: u16, pub height: u16, pub content: T, pub colors: [Color;4], }
|
||||||
pub width: u16,
|
|
||||||
pub height: u16,
|
|
||||||
pub content: T,
|
|
||||||
pub colors: [Color;4],
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A three-column layout.
|
/// A three-column layout.
|
||||||
pub struct Tryptich<A, B, C> {
|
pub struct Tryptich<A, B, C> {
|
||||||
pub top: bool,
|
pub top: bool,
|
||||||
|
|
@ -380,14 +212,8 @@ pub use self::spatial::*; mod spatial {
|
||||||
pub middle: (u16, B),
|
pub middle: (u16, B),
|
||||||
pub right: (u16, C),
|
pub right: (u16, C),
|
||||||
}
|
}
|
||||||
|
/// Repeat a string, e.g. for background fill
|
||||||
/// Repeat a string, e.g. for background
|
pub enum Repeat<'a> { X(&'a str), Y(&'a str), XY(&'a str) }
|
||||||
pub enum Repeat<'a> {
|
|
||||||
X(&'a str),
|
|
||||||
Y(&'a str),
|
|
||||||
XY(&'a str)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Scroll indicator
|
/// Scroll indicator
|
||||||
pub enum Scrollbar {
|
pub enum Scrollbar {
|
||||||
/// Horizontal scrollbar
|
/// Horizontal scrollbar
|
||||||
|
|
@ -411,86 +237,6 @@ mod textual {
|
||||||
|
|
||||||
#[cfg(feature = "jack")] pub use self::aural::*;
|
#[cfg(feature = "jack")] pub use self::aural::*;
|
||||||
#[cfg(feature = "jack")] mod aural {
|
#[cfg(feature = "jack")] mod aural {
|
||||||
use crate::*;
|
|
||||||
use ::jack::*;
|
|
||||||
|
|
||||||
/// 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>),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Event enum for JACK events.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let event = tengri::JackEvent::XRun;
|
|
||||||
/// ```
|
|
||||||
#[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);
|
|
||||||
|
|
||||||
/// Running JACK [AsyncClient] with maximum type erasure.
|
|
||||||
///
|
|
||||||
/// One [Box] contains function that handles [JackEvent]s.
|
|
||||||
///
|
|
||||||
/// Another [Box] containing a function that handles realtime IO.
|
|
||||||
///
|
|
||||||
/// That's all it knows about them.
|
|
||||||
pub type DynamicAsyncClient<'j>
|
|
||||||
= AsyncClient<DynamicNotifications<'j>, DynamicAudioHandler<'j>>;
|
|
||||||
|
|
||||||
/// Notification handler wrapper for [BoxedAudioHandler].
|
|
||||||
pub type DynamicAudioHandler<'j> =
|
|
||||||
::jack::contrib::ClosureProcessHandler<(), BoxedAudioHandler<'j>>;
|
|
||||||
|
|
||||||
/// Boxed realtime callback.
|
|
||||||
pub type BoxedAudioHandler<'j> =
|
|
||||||
Box<dyn FnMut(&Client, &ProcessScope) -> Control + Send + Sync + 'j>;
|
|
||||||
|
|
||||||
/// 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>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub use self::temporal::*; mod temporal {
|
pub use self::temporal::*; mod temporal {
|
||||||
|
|
|
||||||
144
src/tengri_test.rs
Normal file
144
src/tengri_test.rs
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
use crate::*;
|
||||||
|
use Direction::*;
|
||||||
|
use proptest::{prelude::*, option::of};
|
||||||
|
use proptest_derive::Arbitrary;
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
#[test] fn proptest_direction (
|
||||||
|
d in prop_oneof![
|
||||||
|
Just(North), Just(South),
|
||||||
|
Just(East), Just(West),
|
||||||
|
Just(Above), Just(Below)
|
||||||
|
],
|
||||||
|
x in u16::MIN..u16::MAX,
|
||||||
|
y in u16::MIN..u16::MAX,
|
||||||
|
w in u16::MIN..u16::MAX,
|
||||||
|
h in u16::MIN..u16::MAX,
|
||||||
|
a in u16::MIN..u16::MAX,
|
||||||
|
) {
|
||||||
|
let _ = d.split_fixed(XYWH(x, y, w, h), a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
#[test] fn proptest_area (
|
||||||
|
x in u16::MIN..u16::MAX,
|
||||||
|
y in u16::MIN..u16::MAX,
|
||||||
|
w in u16::MIN..u16::MAX,
|
||||||
|
h in u16::MIN..u16::MAX,
|
||||||
|
a in u16::MIN..u16::MAX,
|
||||||
|
b in u16::MIN..u16::MAX,
|
||||||
|
) {
|
||||||
|
let _: XYWH<u16> = XYWH::zero();
|
||||||
|
//let _: XYWH<u16> = XYWH::from_position([a, b]);
|
||||||
|
//let _: XYWH<u16> = XYWH::from_size([a, b]);
|
||||||
|
let area: XYWH<u16> = XYWH(x, y, w, h);
|
||||||
|
//let _ = area.expect_min(a, b);
|
||||||
|
let _ = area.xy();
|
||||||
|
let _ = area.wh();
|
||||||
|
//let _ = area.xywh();
|
||||||
|
let _ = area.clipped_h(a);
|
||||||
|
let _ = area.clipped_w(b);
|
||||||
|
let _ = area.clipped(WH(a, b));
|
||||||
|
//let _ = area.set_w(a);
|
||||||
|
//let _ = area.set_h(b);
|
||||||
|
let _ = area.x2();
|
||||||
|
let _ = area.y2();
|
||||||
|
let _ = area.lrtb();
|
||||||
|
let _ = area.center();
|
||||||
|
let _ = area.centered();
|
||||||
|
let _ = area.centered_x(a);
|
||||||
|
let _ = area.centered_y(b);
|
||||||
|
let _ = area.centered_xy([a, b]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
#[test] fn proptest_size (
|
||||||
|
x in u16::MIN..u16::MAX,
|
||||||
|
y in u16::MIN..u16::MAX,
|
||||||
|
a in u16::MIN..u16::MAX,
|
||||||
|
b in u16::MIN..u16::MAX,
|
||||||
|
) {
|
||||||
|
let size = WH(x, y);
|
||||||
|
let _ = size.w();
|
||||||
|
let _ = size.h();
|
||||||
|
let _ = size.wh();
|
||||||
|
let _ = size.clip_w(a);
|
||||||
|
let _ = size.clip_h(b);
|
||||||
|
//let _ = size.expect_min(a, b);
|
||||||
|
//let _ = size.to_area_pos();
|
||||||
|
//let _ = size.to_area_size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! test_op_transform {
|
||||||
|
($fn:ident, $Op:ident) => {
|
||||||
|
proptest! {
|
||||||
|
#[test] fn $fn (
|
||||||
|
op_x in of(u16::MIN..u16::MAX),
|
||||||
|
op_y in of(u16::MIN..u16::MAX),
|
||||||
|
content in "\\PC*",
|
||||||
|
x in u16::MIN..u16::MAX,
|
||||||
|
y in u16::MIN..u16::MAX,
|
||||||
|
w in u16::MIN..u16::MAX,
|
||||||
|
h in u16::MIN..u16::MAX,
|
||||||
|
) {
|
||||||
|
if let Some(op) = match (op_x, op_y) {
|
||||||
|
(Some(x), Some(y)) => Some($Op::XY(x, y, content)),
|
||||||
|
(Some(x), None) => Some($Op::X(x, content)),
|
||||||
|
(None, Some(y)) => Some($Op::Y(y, content)),
|
||||||
|
_ => None
|
||||||
|
} {
|
||||||
|
//assert_eq!(Content::layout(&op, [x, y, w, h]),
|
||||||
|
//Draw::layout(&op, [x, y, w, h]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
test_op_transform!(proptest_op_fixed, Fixed);
|
||||||
|
test_op_transform!(proptest_op_min, Min);
|
||||||
|
test_op_transform!(proptest_op_max, Max);
|
||||||
|
test_op_transform!(proptest_op_push, Push);
|
||||||
|
test_op_transform!(proptest_op_pull, Pull);
|
||||||
|
test_op_transform!(proptest_op_shrink, Shrink);
|
||||||
|
test_op_transform!(proptest_op_expand, Expand);
|
||||||
|
test_op_transform!(proptest_op_padding, Pad);
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
#[test] fn proptest_op_bsp (
|
||||||
|
d in prop_oneof![
|
||||||
|
Just(North), Just(South),
|
||||||
|
Just(East), Just(West),
|
||||||
|
Just(Above), Just(Below)
|
||||||
|
],
|
||||||
|
a in "\\PC*",
|
||||||
|
b in "\\PC*",
|
||||||
|
x in u16::MIN..u16::MAX,
|
||||||
|
y in u16::MIN..u16::MAX,
|
||||||
|
w in u16::MIN..u16::MAX,
|
||||||
|
h in u16::MIN..u16::MAX,
|
||||||
|
) {
|
||||||
|
let bsp = Bsp(d, a, b);
|
||||||
|
//assert_eq!(
|
||||||
|
//Content::layout(&bsp, [x, y, w, h]),
|
||||||
|
//Draw::layout(&bsp, [x, y, w, h]),
|
||||||
|
//);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//#[test] fn test_tui_engine () -> Usually<()> {
|
||||||
|
////use std::sync::{Arc, RwLock};
|
||||||
|
//struct TestComponent(String);
|
||||||
|
//impl Draw<TuiOut> for TestComponent {
|
||||||
|
//fn draw (&self, _to: &mut TuiOut) {}
|
||||||
|
//}
|
||||||
|
//impl Handle<TuiIn> for TestComponent {
|
||||||
|
//fn handle (&mut self, _from: &TuiIn) -> Perhaps<bool> { Ok(None) }
|
||||||
|
//}
|
||||||
|
//let engine = Tui::new(Box::<&mut Vec<u8>>::new(vec![0u8;0].as_mut()))?;
|
||||||
|
//let state = engine.run(false, &Arc::new(RwLock::new(TestComponent("hello world".into()))))?;
|
||||||
|
//state.read().unwrap().exited.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
//Ok(())
|
||||||
|
//}
|
||||||
|
|
@ -1,127 +1,18 @@
|
||||||
pub use self::input::*; mod input {
|
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
/// Something that will exit and not resume, e.g. the main input loop.
|
|
||||||
/// ```
|
|
||||||
/// use ::tengri::Done;
|
|
||||||
/// use ::std::sync::atomic::{AtomicBool, Ordering};
|
|
||||||
/// use Ordering::Relaxed;
|
|
||||||
///
|
|
||||||
/// struct Example(AtomicBool);
|
|
||||||
/// impl Done for Example {
|
|
||||||
/// fn is_done (&self) -> bool { self.0.load(Relaxed) }
|
|
||||||
/// fn done (&self) { self.0.store(true, Relaxed) }
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// assert!(Example(true.into()).is_done());
|
|
||||||
/// assert!(!Example(false.into()).is_done());
|
|
||||||
///
|
|
||||||
/// let state = Example(false.into());
|
|
||||||
/// while !state.is_done() {
|
|
||||||
/// state.done(); // exit immediately
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
pub trait Done {
|
|
||||||
fn is_done (&self) -> bool;
|
|
||||||
fn done (&self);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Source of [Input::Event]s: keyboard, mouse...
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use ::tengri::{Done, Input};
|
|
||||||
/// use ::std::sync::atomic::{AtomicBool, Ordering};
|
|
||||||
/// use Ordering::Relaxed;
|
|
||||||
///
|
|
||||||
/// struct Example(AtomicBool);
|
|
||||||
/// impl Done for Example {
|
|
||||||
/// fn is_done (&self) -> bool { self.0.load(Relaxed) }
|
|
||||||
/// fn done (&self) { self.0.store(true, Relaxed) }
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// enum TestEvent { Test1 }
|
|
||||||
/// impl Input for Example {
|
|
||||||
/// type Event = TestEvent;
|
|
||||||
/// type Handled = ();
|
|
||||||
/// fn event (&self) -> &Self::Event {
|
|
||||||
/// &TestEvent::Test1
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// let _ = Example(true.into()).event();
|
|
||||||
/// ```
|
|
||||||
pub trait Input: Done + Sized {
|
|
||||||
/// Type of input event
|
|
||||||
type Event;
|
|
||||||
/// Result of handling input
|
|
||||||
type Handled; // TODO: make this an Option<Box dyn Command<Self>> containing the undo
|
|
||||||
/// Currently handled event
|
|
||||||
fn event (&self) -> &Self::Event;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// State mutation.
|
|
||||||
pub trait Command<S>: Send + Sync + Sized {
|
|
||||||
fn execute (&self, state: &mut S) -> Perhaps<Self>;
|
|
||||||
fn delegate <T> (&self, state: &mut S, wrap: impl Fn(Self)->T) -> Perhaps<T>
|
|
||||||
where Self: Sized
|
|
||||||
{
|
|
||||||
Ok(self.execute(state)?.map(wrap))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Define a trait an implement it for various mutation-enabled wrapper types. */
|
|
||||||
#[macro_export] macro_rules! flex_trait_mut (
|
|
||||||
($Trait:ident $(<$($A:ident:$T:ident),+>)? {
|
|
||||||
$(fn $fn:ident (&mut $self:ident $(, $arg:ident:$ty:ty)*) -> $ret:ty $body:block)*
|
|
||||||
})=>{
|
|
||||||
pub trait $Trait $(<$($A: $T),+>)? {
|
|
||||||
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret $body)*
|
|
||||||
}
|
|
||||||
impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for &mut _T_ {
|
|
||||||
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { (*$self).$fn($($arg),*) })*
|
|
||||||
}
|
|
||||||
impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for Option<_T_> {
|
|
||||||
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret {
|
|
||||||
if let Some(this) = $self { this.$fn($($arg),*) } else { Ok(None) }
|
|
||||||
})*
|
|
||||||
}
|
|
||||||
impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::Mutex<_T_> {
|
|
||||||
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { $self.get_mut().unwrap().$fn($($arg),*) })*
|
|
||||||
}
|
|
||||||
impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::Arc<::std::sync::Mutex<_T_>> {
|
|
||||||
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { $self.lock().unwrap().$fn($($arg),*) })*
|
|
||||||
}
|
|
||||||
impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::RwLock<_T_> {
|
|
||||||
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { $self.write().unwrap().$fn($($arg),*) })*
|
|
||||||
}
|
|
||||||
impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::Arc<::std::sync::RwLock<_T_>> {
|
|
||||||
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { $self.write().unwrap().$fn($($arg),*) })*
|
|
||||||
}
|
|
||||||
};
|
|
||||||
);
|
|
||||||
|
|
||||||
flex_trait_mut!(Handle <E: Input> {
|
|
||||||
fn handle (&mut self, _input: &E) -> Perhaps<E::Handled> {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub use self::output::*; mod output {
|
|
||||||
use crate::*;
|
|
||||||
/// Output target.
|
/// Output target.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use tengri::*;
|
/// use tengri::*;
|
||||||
///
|
///
|
||||||
/// struct TestOut(XYWH<u16>);
|
/// struct TestOut(impl TwoD<u16>);
|
||||||
///
|
///
|
||||||
/// impl tengri::Out for TestOut {
|
/// impl tengri::Out for TestOut {
|
||||||
/// type Unit = u16;
|
/// type Unit = u16;
|
||||||
/// fn area (&self) -> XYWH<u16> { self.0 }
|
/// fn area (&self) -> impl TwoD<u16> { self.0 }
|
||||||
/// fn area_mut (&mut self) -> &mut XYWH<u16> { &mut self.0 }
|
/// fn area_mut (&mut self) -> &mut impl TwoD<u16> { &mut self.0 }
|
||||||
/// fn place_at <T: Draw<Self> + ?Sized> (&mut self, area: XYWH<u16>, _: &T) {
|
/// fn show <T: Draw<Self> + ?Sized> (&mut self, area: impl TwoD<u16>, _: &T) {
|
||||||
/// println!("place_at: {area:?}");
|
/// println!("show: {area:?}");
|
||||||
/// ()
|
/// ()
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
|
|
@ -132,140 +23,105 @@ pub use self::output::*; mod output {
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub trait Out: Send + Sync + Sized {
|
pub trait Screen: W<Self::Unit> + H<Self::Unit> + Send + Sync + Sized {
|
||||||
/// Unit of length
|
|
||||||
type Unit: Coord;
|
type Unit: Coord;
|
||||||
/// Current output area
|
|
||||||
fn area (&self) -> XYWH<Self::Unit>;
|
|
||||||
/// Mutable pointer to area.
|
|
||||||
fn area_mut (&mut self) -> &mut XYWH<Self::Unit>;
|
|
||||||
/// Render drawable in area specified by `area`
|
/// Render drawable in area specified by `area`
|
||||||
fn place_at <'t, T: Draw<Self> + ?Sized> (&mut self, area: XYWH<Self::Unit>, content: &'t T);
|
fn show <'t, T: Draw<Self> + ?Sized> (&mut self, area: TwoD<Self::Unit>, content: &'t T);
|
||||||
/// Render drawable in area specified by `T::layout(self.area())`
|
|
||||||
#[inline] fn place <'t, T: Content<Self> + ?Sized> (&mut self, content: &'t T) {
|
|
||||||
self.place_at(content.layout(self.area()), content)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait HasColor {
|
||||||
|
fn color (&self) -> ItemColor;
|
||||||
}
|
}
|
||||||
/// A numeric type that can be used as coordinate.
|
|
||||||
///
|
|
||||||
/// FIXME: Replace this ad-hoc trait with `num` crate.
|
|
||||||
pub trait Coord: Send + Sync + Copy
|
|
||||||
+ Add<Self, Output=Self>
|
|
||||||
+ Sub<Self, Output=Self>
|
|
||||||
+ Mul<Self, Output=Self>
|
|
||||||
+ Div<Self, Output=Self>
|
|
||||||
+ Ord + PartialEq + Eq
|
|
||||||
+ Debug + Display + Default
|
|
||||||
+ From<u16> + Into<u16>
|
|
||||||
+ Into<usize>
|
|
||||||
+ Into<f64>
|
|
||||||
{
|
|
||||||
fn plus (self, other: Self) -> Self;
|
|
||||||
fn minus (self, other: Self) -> Self {
|
|
||||||
if self >= other { self - other } else { 0.into() }
|
|
||||||
}
|
|
||||||
fn atomic (self) -> AtomicUsize {
|
|
||||||
AtomicUsize::new(self.into())
|
|
||||||
}
|
|
||||||
fn zero () -> Self {
|
|
||||||
0.into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Drawable with dynamic dispatch.
|
|
||||||
pub trait Draw<O: Out> { fn draw (&self, to: &mut O); }
|
|
||||||
/// Outputs combinator.
|
|
||||||
pub trait Lay<O: Out>: Sized {}
|
|
||||||
/// Drawable area of display.
|
|
||||||
pub trait Layout<O: Out> {
|
|
||||||
fn layout_x (&self, to: XYWH<O::Unit>) -> O::Unit { to.x() }
|
|
||||||
fn layout_y (&self, to: XYWH<O::Unit>) -> O::Unit { to.y() }
|
|
||||||
fn layout_w_min (&self, _t: XYWH<O::Unit>) -> O::Unit { 0.into() }
|
|
||||||
fn layout_w_max (&self, to: XYWH<O::Unit>) -> O::Unit { to.w() }
|
|
||||||
fn layout_w (&self, to: XYWH<O::Unit>) -> O::Unit { to.w().max(self.layout_w_min(to)).min(self.layout_w_max(to)) }
|
|
||||||
fn layout_h_min (&self, _t: XYWH<O::Unit>) -> O::Unit { 0.into() }
|
|
||||||
fn layout_h_max (&self, to: XYWH<O::Unit>) -> O::Unit { to.h() }
|
|
||||||
fn layout_h (&self, to: XYWH<O::Unit>) -> O::Unit { to.h().max(self.layout_h_min(to)).min(self.layout_h_max(to)) }
|
|
||||||
fn layout (&self, to: XYWH<O::Unit>) -> XYWH<O::Unit> {
|
|
||||||
XYWH(self.layout_x(to), self.layout_y(to), self.layout_w(to), self.layout_h(to))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO DOCUMENTME
|
|
||||||
pub trait Content<O: Out>: Draw<O> + Layout<O> {}
|
|
||||||
// TODO DOCUMENTME
|
|
||||||
pub trait HasContent<O: Out> { fn content (&self) -> impl Content<O>; }
|
|
||||||
// Something that has an origin point (X, Y).
|
|
||||||
pub trait HasXY<N: Coord> {
|
|
||||||
fn x (&self) -> N;
|
|
||||||
fn y (&self) -> N;
|
|
||||||
fn xy (&self) -> XY<N> { XY(self.x(), self.y()) }
|
|
||||||
}
|
|
||||||
// Something that has a size (W, H).
|
|
||||||
pub trait HasWH<N: Coord> {
|
|
||||||
fn w (&self) -> N;
|
|
||||||
fn h (&self) -> N;
|
|
||||||
fn wh (&self) -> WH<N> { WH(self.w(), self.h()) }
|
|
||||||
}
|
|
||||||
// Something that has a 2D bounding box (X, Y, W, H).
|
|
||||||
//
|
|
||||||
// FIXME: The other way around?
|
|
||||||
pub trait HasXYWH<N: Coord>: HasXY<N> + HasWH<N> {
|
|
||||||
fn x2 (&self) -> N { self.x().plus(self.w()) }
|
|
||||||
fn y2 (&self) -> N { self.y().plus(self.h()) }
|
|
||||||
fn xywh (&self) -> XYWH<N> { XYWH(self.x(), self.y(), self.w(), self.h()) }
|
|
||||||
fn expect_min (&self, w: N, h: N) -> Usually<&Self> {
|
|
||||||
if self.w() < w || self.h() < h {
|
|
||||||
Err(format!("min {w}x{h}").into())
|
|
||||||
} else {
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub trait HasPerf { fn perf (&self) -> &PerfModel; }
|
|
||||||
pub trait HasColor { fn color (&self) -> ItemColor; }
|
|
||||||
// Something that has a [Measure] of its rendered size.
|
// Something that has a [Measure] of its rendered size.
|
||||||
pub trait Measured<O: Out> {
|
pub trait Measured<O: Screen> {
|
||||||
fn measure (&self) -> &Measure<O>;
|
fn measure (&self) -> &Measure<O>;
|
||||||
fn measure_width (&self) -> O::Unit { self.measure().w() }
|
fn measure_width (&self) -> O::Unit { self.measure().w() }
|
||||||
fn measure_height (&self) -> O::Unit { self.measure().h() }
|
fn measure_height (&self) -> O::Unit { self.measure().h() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "tui")] pub trait TuiOut: Screen {
|
||||||
|
fn tui_out (&mut self) -> &mut Buffer;
|
||||||
|
fn update (&mut self, area: impl TwoD<u16>, callback: &impl Fn(&mut Cell, u16, u16)) {
|
||||||
|
tui_update(self.buffer(), area, callback);
|
||||||
|
}
|
||||||
|
fn fill_char (&mut self, area: impl TwoD<u16>, c: char) {
|
||||||
|
self.update(area, &|cell,_,_|{cell.set_char(c);})
|
||||||
|
}
|
||||||
|
fn fill_bg (&mut self, area: impl TwoD<u16>, color: Color) {
|
||||||
|
self.update(area, &|cell,_,_|{cell.set_bg(color);})
|
||||||
|
}
|
||||||
|
fn fill_fg (&mut self, area: impl TwoD<u16>, color: Color) {
|
||||||
|
self.update(area, &|cell,_,_|{cell.set_fg(color);})
|
||||||
|
}
|
||||||
|
fn fill_mod (&mut self, area: impl TwoD<u16>, on: bool, modifier: Modifier) {
|
||||||
|
if on {
|
||||||
|
self.update(area, &|cell,_,_|cell.modifier.insert(modifier))
|
||||||
|
} else {
|
||||||
|
self.update(area, &|cell,_,_|cell.modifier.remove(modifier))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn fill_bold (&mut self, area: impl TwoD<u16>, on: bool) {
|
||||||
|
self.fill_mod(area, on, Modifier::BOLD)
|
||||||
|
}
|
||||||
|
fn fill_reversed (&mut self, area: impl TwoD<u16>, on: bool) {
|
||||||
|
self.fill_mod(area, on, Modifier::REVERSED)
|
||||||
|
}
|
||||||
|
fn fill_crossed_out (&mut self, area: impl TwoD<u16>, on: bool) {
|
||||||
|
self.fill_mod(area, on, Modifier::CROSSED_OUT)
|
||||||
|
}
|
||||||
|
fn fill_ul (&mut self, area: impl TwoD<u16>, color: Option<Color>) {
|
||||||
|
if let Some(color) = color {
|
||||||
|
self.update(area, &|cell,_,_|{
|
||||||
|
cell.modifier.insert(ratatui::prelude::Modifier::UNDERLINED);
|
||||||
|
cell.underline_color = color;
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
self.update(area, &|cell,_,_|{
|
||||||
|
cell.modifier.remove(ratatui::prelude::Modifier::UNDERLINED);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn tint_all (&mut self, fg: Color, bg: Color, modifier: Modifier) {
|
||||||
|
for cell in self.buffer().content.iter_mut() {
|
||||||
|
cell.fg = fg;
|
||||||
|
cell.bg = bg;
|
||||||
|
cell.modifier = modifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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());
|
||||||
|
let buf = self.buffer();
|
||||||
|
if x < buf.area.width && y < buf.area.height {
|
||||||
|
buf.set_string(x, y, text, style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Write a line of text
|
||||||
|
///
|
||||||
|
/// TODO: do a paragraph (handle newlines)
|
||||||
|
fn text (&mut self, text: &impl AsRef<str>, x0: u16, y: u16, max_width: u16) {
|
||||||
|
let text = text.as_ref();
|
||||||
|
let buf = self.buffer();
|
||||||
|
let mut string_width: u16 = 0;
|
||||||
|
for character in text.chars() {
|
||||||
|
let x = x0 + string_width;
|
||||||
|
let character_width = character.width().unwrap_or(0) as u16;
|
||||||
|
string_width += character_width;
|
||||||
|
if string_width > max_width {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if let Some(cell) = buf.write().unwrap().cell_mut(ratatui::prelude::Position { x, y }) {
|
||||||
|
cell.set_char(character);
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "tui")] pub use self::tui::*;
|
#[cfg(feature = "tui")] pub trait BorderStyle: Draw<Buffer> + Copy {
|
||||||
#[cfg(feature = "tui")] mod tui {
|
|
||||||
use crate::*;
|
|
||||||
use ratatui::prelude::*;
|
|
||||||
pub trait TuiDraw = Draw<Tui>;
|
|
||||||
pub trait TuiLayout = crate::Layout<Tui>;
|
|
||||||
pub trait TuiContent = Content<Tui>;
|
|
||||||
pub trait TuiHandle = Handle<Tui>;
|
|
||||||
pub trait TuiWidget = TuiDraw + TuiHandle;
|
|
||||||
pub trait BorderStyle: Content<Tui> + Copy {
|
|
||||||
fn enabled (&self) -> bool;
|
fn enabled (&self) -> bool;
|
||||||
fn enclose (self, w: impl Content<Tui>) -> impl Content<Tui> {
|
|
||||||
Bsp::b(Fill::XY(Border(self.enabled(), self)), w)
|
|
||||||
}
|
|
||||||
fn enclose2 (self, w: impl Content<Tui>) -> impl Content<Tui> {
|
|
||||||
Bsp::b(Pad::XY(1, 1, Fill::XY(Border(self.enabled(), self))), w)
|
|
||||||
}
|
|
||||||
fn enclose_bg (self, w: impl Content<Tui>) -> impl Content<Tui> {
|
|
||||||
Tui::bg(self.style().unwrap().bg.unwrap_or(Color::Reset),
|
|
||||||
Bsp::b(Fill::XY(Border(self.enabled(), self)), w))
|
|
||||||
}
|
|
||||||
const NW: &'static str = "";
|
|
||||||
const N: &'static str = "";
|
|
||||||
const NE: &'static str = "";
|
|
||||||
const E: &'static str = "";
|
|
||||||
const SE: &'static str = "";
|
|
||||||
const S: &'static str = "";
|
|
||||||
const SW: &'static str = "";
|
|
||||||
const W: &'static str = "";
|
|
||||||
|
|
||||||
const N0: &'static str = "";
|
|
||||||
const S0: &'static str = "";
|
|
||||||
const W0: &'static str = "";
|
|
||||||
const E0: &'static str = "";
|
|
||||||
|
|
||||||
fn border_n (&self) -> &str { Self::N }
|
fn border_n (&self) -> &str { Self::N }
|
||||||
fn border_s (&self) -> &str { Self::S }
|
fn border_s (&self) -> &str { Self::S }
|
||||||
fn border_e (&self) -> &str { Self::E }
|
fn border_e (&self) -> &str { Self::E }
|
||||||
|
|
@ -274,19 +130,26 @@ pub use self::output::*; mod output {
|
||||||
fn border_ne (&self) -> &str { Self::NE }
|
fn border_ne (&self) -> &str { Self::NE }
|
||||||
fn border_sw (&self) -> &str { Self::SW }
|
fn border_sw (&self) -> &str { Self::SW }
|
||||||
fn border_se (&self) -> &str { Self::SE }
|
fn border_se (&self) -> &str { Self::SE }
|
||||||
#[inline] fn draw <'a> (
|
|
||||||
&self, to: &mut Tui
|
fn enclose (self, w: impl Draw<Buffer>) -> impl Draw<Buffer> {
|
||||||
) -> Usually<()> {
|
Bsp::b(Fill::XY(Border(self.enabled(), self)), w)
|
||||||
|
}
|
||||||
|
fn enclose2 (self, w: impl Draw<Buffer>) -> impl Draw<Buffer> {
|
||||||
|
Bsp::b(Pad::XY(1, 1, Fill::XY(Border(self.enabled(), self))), w)
|
||||||
|
}
|
||||||
|
fn enclose_bg (self, w: impl Draw<Buffer>) -> impl Draw<Buffer> {
|
||||||
|
TuiOut::bg(self.style().unwrap().bg.unwrap_or(Color::Reset),
|
||||||
|
Bsp::b(Fill::XY(Border(self.enabled(), self)), w))
|
||||||
|
}
|
||||||
|
#[inline] fn draw <'a> (&self, to: &mut impl TuiOut) -> Usually<()> {
|
||||||
if self.enabled() {
|
if self.enabled() {
|
||||||
self.draw_horizontal(to, None)?;
|
self.draw_h(to, None)?;
|
||||||
self.draw_vertical(to, None)?;
|
self.draw_v(to, None)?;
|
||||||
self.draw_corners(to, None)?;
|
self.draw_c(to, None)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
#[inline] fn draw_horizontal (
|
#[inline] fn draw_h (&self, to: &mut impl TuiOut, style: Option<Style>) -> Usually<impl TwoD<u16>> {
|
||||||
&self, to: &mut Tui, style: Option<Style>
|
|
||||||
) -> Usually<XYWH<u16>> {
|
|
||||||
let area = to.area();
|
let area = to.area();
|
||||||
let style = style.or_else(||self.style_horizontal());
|
let style = style.or_else(||self.style_horizontal());
|
||||||
let [x, x2, y, y2] = area.lrtb();
|
let [x, x2, y, y2] = area.lrtb();
|
||||||
|
|
@ -296,9 +159,7 @@ pub use self::output::*; mod output {
|
||||||
}
|
}
|
||||||
Ok(area)
|
Ok(area)
|
||||||
}
|
}
|
||||||
#[inline] fn draw_vertical (
|
#[inline] fn draw_v (&self, to: &mut impl TuiOut, style: Option<Style>) -> Usually<impl TwoD<u16>> {
|
||||||
&self, to: &mut Tui, style: Option<Style>
|
|
||||||
) -> Usually<XYWH<u16>> {
|
|
||||||
let area = to.area();
|
let area = to.area();
|
||||||
let style = style.or_else(||self.style_vertical());
|
let style = style.or_else(||self.style_vertical());
|
||||||
let [x, x2, y, y2] = area.lrtb();
|
let [x, x2, y, y2] = area.lrtb();
|
||||||
|
|
@ -314,17 +175,15 @@ pub use self::output::*; mod output {
|
||||||
}
|
}
|
||||||
Ok(area)
|
Ok(area)
|
||||||
}
|
}
|
||||||
#[inline] fn draw_corners (
|
#[inline] fn draw_c (&self, to: &mut impl TuiOut, style: Option<Style>) -> Usually<impl TwoD<u16>> {
|
||||||
&self, to: &mut Tui, style: Option<Style>
|
|
||||||
) -> Usually<XYWH<u16>> {
|
|
||||||
let area = to.area();
|
let area = to.area();
|
||||||
let style = style.or_else(||self.style_corners());
|
let style = style.or_else(||self.style_corners());
|
||||||
let XYWH(x, y, width, height) = area;
|
let XYWH(x, y, w, h) = area;
|
||||||
if width > 1 && height > 1 {
|
if w > 1 && h > 1 {
|
||||||
to.blit(&Self::NW, x, y, style);
|
to.blit(&Self::NW, x, y, style);
|
||||||
to.blit(&Self::NE, x + width - 1, y, style);
|
to.blit(&Self::NE, x + w - 1, y, style);
|
||||||
to.blit(&Self::SW, x, y + height - 1, style);
|
to.blit(&Self::SW, x, y + h- 1, style);
|
||||||
to.blit(&Self::SE, x + width - 1, y + height - 1, style);
|
to.blit(&Self::SE, x + w - 1, y + h - 1, style);
|
||||||
}
|
}
|
||||||
Ok(area)
|
Ok(area)
|
||||||
}
|
}
|
||||||
|
|
@ -332,13 +191,21 @@ pub use self::output::*; mod output {
|
||||||
#[inline] fn style_horizontal (&self) -> Option<Style> { self.style() }
|
#[inline] fn style_horizontal (&self) -> Option<Style> { self.style() }
|
||||||
#[inline] fn style_vertical (&self) -> Option<Style> { self.style() }
|
#[inline] fn style_vertical (&self) -> Option<Style> { self.style() }
|
||||||
#[inline] fn style_corners (&self) -> Option<Style> { self.style() }
|
#[inline] fn style_corners (&self) -> Option<Style> { self.style() }
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "jack")] pub use self::jack::*;
|
const NW: &'static str = "";
|
||||||
#[cfg(feature = "jack")] mod jack {
|
const N: &'static str = "";
|
||||||
use crate::*;
|
const NE: &'static str = "";
|
||||||
use ::jack::{*, contrib::{*, Position}};
|
const E: &'static str = "";
|
||||||
|
const SE: &'static str = "";
|
||||||
|
const S: &'static str = "";
|
||||||
|
const SW: &'static str = "";
|
||||||
|
const W: &'static str = "";
|
||||||
|
|
||||||
|
const N0: &'static str = "";
|
||||||
|
const S0: &'static str = "";
|
||||||
|
const W0: &'static str = "";
|
||||||
|
const E0: &'static str = "";
|
||||||
|
}
|
||||||
|
|
||||||
/// Things that can provide a [jack::Client] reference.
|
/// Things that can provide a [jack::Client] reference.
|
||||||
///
|
///
|
||||||
|
|
@ -353,28 +220,22 @@ pub use self::output::*; mod output {
|
||||||
/// fn jack (&self) -> &Jack<'j> { &self.0 }
|
/// fn jack (&self) -> &Jack<'j> { &self.0 }
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub trait HasJack<'j>: Send + Sync {
|
#[cfg(feature = "jack")] pub trait HasJack<'j>: Send + Sync {
|
||||||
|
|
||||||
/// Return the internal [jack::Client] handle
|
/// Return the internal [jack::Client] handle
|
||||||
/// that lets you call the JACK API.
|
/// that lets you call the JACK API.
|
||||||
fn jack (&self) -> &Jack<'j>;
|
fn jack (&self) -> &Jack<'j>;
|
||||||
|
|
||||||
fn with_client <T> (&self, op: impl FnOnce(&Client)->T) -> T {
|
fn with_client <T> (&self, op: impl FnOnce(&Client)->T) -> T {
|
||||||
self.jack().with_client(op)
|
self.jack().with_client(op)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn port_by_name (&self, name: &str) -> Option<Port<Unowned>> {
|
fn port_by_name (&self, name: &str) -> Option<Port<Unowned>> {
|
||||||
self.with_client(|client|client.port_by_name(name))
|
self.with_client(|client|client.port_by_name(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn port_by_id (&self, id: u32) -> Option<Port<Unowned>> {
|
fn port_by_id (&self, id: u32) -> Option<Port<Unowned>> {
|
||||||
self.with_client(|c|c.port_by_id(id))
|
self.with_client(|c|c.port_by_id(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn register_port <PS: PortSpec + Default> (&self, name: impl AsRef<str>) -> Usually<Port<PS>> {
|
fn register_port <PS: PortSpec + Default> (&self, name: impl AsRef<str>) -> Usually<Port<PS>> {
|
||||||
self.with_client(|client|Ok(client.register_port(name.as_ref(), PS::default())?))
|
self.with_client(|client|Ok(client.register_port(name.as_ref(), PS::default())?))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync_lead (&self, enable: bool, callback: impl Fn(TimebaseInfo)->Position) -> Usually<()> {
|
fn sync_lead (&self, enable: bool, callback: impl Fn(TimebaseInfo)->Position) -> Usually<()> {
|
||||||
if enable {
|
if enable {
|
||||||
self.with_client(|client|match client.register_timebase_callback(false, callback) {
|
self.with_client(|client|match client.register_timebase_callback(false, callback) {
|
||||||
|
|
@ -384,7 +245,6 @@ pub use self::output::*; mod output {
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync_follow (&self, _enable: bool) -> Usually<()> {
|
fn sync_follow (&self, _enable: bool) -> Usually<()> {
|
||||||
// TODO: sync follow
|
// TODO: sync follow
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -392,16 +252,13 @@ pub use self::output::*; mod output {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trait for thing that has a JACK process callback.
|
/// Trait for thing that has a JACK process callback.
|
||||||
pub trait Audio {
|
#[cfg(feature = "jack")] pub trait Audio {
|
||||||
|
|
||||||
/// Handle a JACK event.
|
/// Handle a JACK event.
|
||||||
fn handle (&mut self, _event: JackEvent) {}
|
fn handle (&mut self, _event: JackEvent) {}
|
||||||
|
|
||||||
/// Projecss a JACK chunk.
|
/// Projecss a JACK chunk.
|
||||||
fn process (&mut self, _: &Client, _: &ProcessScope) -> Control {
|
fn process (&mut self, _: &Client, _: &ProcessScope) -> Control {
|
||||||
Control::Continue
|
Control::Continue
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The JACK process callback function passed to the server.
|
/// The JACK process callback function passed to the server.
|
||||||
fn callback (
|
fn callback (
|
||||||
state: &Arc<RwLock<Self>>, client: &Client, scope: &ProcessScope
|
state: &Arc<RwLock<Self>>, client: &Client, scope: &ProcessScope
|
||||||
|
|
@ -412,11 +269,10 @@ pub use self::output::*; mod output {
|
||||||
Control::Quit
|
Control::Quit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Implement [Audio]: provide JACK callbacks.
|
/// Implement [Audio]: provide JACK callbacks.
|
||||||
#[macro_export] macro_rules! impl_audio {
|
#[cfg(feature = "jack")] #[macro_export] macro_rules! impl_audio {
|
||||||
|
|
||||||
(|
|
(|
|
||||||
$self1:ident:
|
$self1:ident:
|
||||||
|
|
@ -449,7 +305,37 @@ pub use self::output::*; mod output {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait JackPerfModel {
|
#[cfg(feature = "jack")] pub trait JackPerfModel {
|
||||||
fn update_from_jack_scope (&self, t0: Option<u64>, scope: &ProcessScope);
|
fn update_from_jack_scope (&self, t0: Option<u64>, scope: &ProcessScope);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Define a trait an implement it for various mutation-enabled wrapper types. */
|
||||||
|
#[macro_export] macro_rules! flex_trait_mut (
|
||||||
|
($Trait:ident $(<$($A:ident:$T:ident),+>)? {
|
||||||
|
$(fn $fn:ident (&mut $self:ident $(, $arg:ident:$ty:ty)*) -> $ret:ty $body:block)*
|
||||||
|
})=>{
|
||||||
|
pub trait $Trait $(<$($A: $T),+>)? {
|
||||||
|
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret $body)*
|
||||||
}
|
}
|
||||||
|
impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for &mut _T_ {
|
||||||
|
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { (*$self).$fn($($arg),*) })*
|
||||||
|
}
|
||||||
|
impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for Option<_T_> {
|
||||||
|
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret {
|
||||||
|
if let Some(this) = $self { this.$fn($($arg),*) } else { Ok(None) }
|
||||||
|
})*
|
||||||
|
}
|
||||||
|
impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::Mutex<_T_> {
|
||||||
|
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { $self.get_mut().unwrap().$fn($($arg),*) })*
|
||||||
|
}
|
||||||
|
impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::Arc<::std::sync::Mutex<_T_>> {
|
||||||
|
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { $self.lock().unwrap().$fn($($arg),*) })*
|
||||||
|
}
|
||||||
|
impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::RwLock<_T_> {
|
||||||
|
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { $self.write().unwrap().$fn($($arg),*) })*
|
||||||
|
}
|
||||||
|
impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::Arc<::std::sync::RwLock<_T_>> {
|
||||||
|
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { $self.write().unwrap().$fn($($arg),*) })*
|
||||||
|
}
|
||||||
|
};
|
||||||
|
);
|
||||||
|
|
|
||||||
200
src/tui.rs
Normal file
200
src/tui.rs
Normal file
|
|
@ -0,0 +1,200 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "tui")] pub use self::tui_fns::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "tui")] mod tui_fns {
|
||||||
|
use crate::*;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Interpret TUI-specific layout operation.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use tengri::{Namespace, Understand, Tui, ratatui::prelude::Color};
|
||||||
|
///
|
||||||
|
/// struct State;
|
||||||
|
/// impl<'b> Namespace<'b, bool> for State {}
|
||||||
|
/// impl<'b> Namespace<'b, u16> for State {}
|
||||||
|
/// impl<'b> Namespace<'b, Color> for State {}
|
||||||
|
/// impl Understand<Tui, ()> for State {}
|
||||||
|
/// # fn main () -> tengri::Usually<()> {
|
||||||
|
/// let state = State;
|
||||||
|
/// let mut out = TuiOut::default();
|
||||||
|
/// tengri::eval_view_tui(&state, &mut out, "")?;
|
||||||
|
/// tengri::eval_view_tui(&state, &mut out, "text Hello world!")?;
|
||||||
|
/// tengri::eval_view_tui(&state, &mut out, "fg (g 0) (text Hello world!)")?;
|
||||||
|
/// tengri::eval_view_tui(&state, &mut out, "bg (g 2) (text Hello world!)")?;
|
||||||
|
/// tengri::eval_view_tui(&state, &mut out, "(bg (g 3) (fg (g 4) (text Hello world!)))")?;
|
||||||
|
/// # Ok(()) }
|
||||||
|
/// ```
|
||||||
|
pub fn eval_view_tui <'a, S> (
|
||||||
|
state: &S, output: &mut Buffer, expr: impl Expression + 'a
|
||||||
|
) -> Usually<bool> where
|
||||||
|
S: Understand<TuiOut, ()>
|
||||||
|
+ for<'b>Namespace<'b, bool>
|
||||||
|
+ for<'b>Namespace<'b, u16>
|
||||||
|
+ for<'b>Namespace<'b, Color>
|
||||||
|
{
|
||||||
|
// See `tengri::eval_view`
|
||||||
|
let head = expr.head()?;
|
||||||
|
let mut frags = head.src()?.unwrap_or_default().split("/");
|
||||||
|
let args = expr.tail();
|
||||||
|
let arg0 = args.head();
|
||||||
|
let tail0 = args.tail();
|
||||||
|
let arg1 = tail0.head();
|
||||||
|
let tail1 = tail0.tail();
|
||||||
|
let arg2 = tail1.head();
|
||||||
|
match frags.next() {
|
||||||
|
|
||||||
|
Some("text") => {
|
||||||
|
if let Some(src) = args?.src()? { output.place(&src) }
|
||||||
|
},
|
||||||
|
|
||||||
|
Some("fg") => {
|
||||||
|
let arg0 = arg0?.expect("fg: expected arg 0 (color)");
|
||||||
|
let color = Namespace::namespace(state, arg0)?.unwrap_or_else(||panic!("fg: {arg0:?}: not a color"));
|
||||||
|
let thunk = Thunk::new(move|output: &mut Buffer|state.understand(output, &arg1).unwrap());
|
||||||
|
output.place(&TuiOut::fg(color, thunk))
|
||||||
|
},
|
||||||
|
|
||||||
|
Some("bg") => {
|
||||||
|
//panic!("expr: {expr:?}\nhead: {head:?}\nfrags: {frags:?}\nargs: {args:?}\narg0: {arg0:?}\ntail0: {tail0:?}\narg1: {arg1:?}\ntail1: {tail1:?}\narg2: {arg2:?}");
|
||||||
|
//panic!("head: {head:?}\narg0: {arg0:?}\narg1: {arg1:?}\narg2: {arg2:?}");;
|
||||||
|
//panic!("head: {head:?}\narg0: {arg0:?}\narg1: {arg1:?}\narg2: {arg2:?}");
|
||||||
|
let arg0 = arg0?.expect("bg: expected arg 0 (color)");
|
||||||
|
let color = Namespace::namespace(state, arg0)?.unwrap_or_else(||panic!("bg: {arg0:?}: not a color"));
|
||||||
|
let thunk = Thunk::new(move|output: &mut Buffer|state.understand(output, &arg1).unwrap());
|
||||||
|
output.place(&TuiOut::bg(color, thunk))
|
||||||
|
},
|
||||||
|
|
||||||
|
_ => return Ok(false)
|
||||||
|
|
||||||
|
};
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "tui")] pub fn tui_setup <W: Write> (backend: &mut CrosstermBackend<W>) -> Usually<()> {
|
||||||
|
let better_panic_handler = Settings::auto().verbosity(Verbosity::Full).create_panic_handler();
|
||||||
|
std::panic::set_hook(Box::new(move |info: &std::panic::PanicHookInfo|{
|
||||||
|
stdout().execute(LeaveAlternateScreen).unwrap();
|
||||||
|
CrosstermBackend::new(stdout()).show_cursor().unwrap();
|
||||||
|
disable_raw_mode().unwrap();
|
||||||
|
better_panic_handler(info);
|
||||||
|
}));
|
||||||
|
stdout().execute(EnterAlternateScreen)?;
|
||||||
|
backend.hide_cursor()?;
|
||||||
|
enable_raw_mode().map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "tui")] pub fn tui_teardown <W: Write> (backend: &mut CrosstermBackend<W>) -> Usually<()> {
|
||||||
|
stdout().execute(LeaveAlternateScreen)?;
|
||||||
|
backend.show_cursor()?;
|
||||||
|
disable_raw_mode().map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "tui")] pub fn tui_update (
|
||||||
|
buf: &mut Buffer, area: XYWH<u16>, callback: &impl Fn(&mut Cell, u16, u16)
|
||||||
|
) {
|
||||||
|
for row in 0..area.h() {
|
||||||
|
let y = area.y() + row;
|
||||||
|
for col in 0..area.w() {
|
||||||
|
let x = area.x() + col;
|
||||||
|
if x < buf.area.width && y < buf.area.height {
|
||||||
|
if let Some(cell) = buf.cell_mut(ratatui::prelude::Position { x, y }) {
|
||||||
|
callback(cell, col, row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "tui")] pub(crate) fn tui_wh <W: Write> (
|
||||||
|
backend: &mut CrosstermBackend<W>
|
||||||
|
) -> WH<u16> {
|
||||||
|
let Size { width, height } = backend.size().expect("get size failed");
|
||||||
|
WH(width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawn the TUI input thread which reads keys from the terminal.
|
||||||
|
#[cfg(feature = "tui")] pub fn tui_input <T: Act<TuiEvent, T> + Send + Sync + 'static> (
|
||||||
|
exited: &Arc<AtomicBool>, state: &Arc<RwLock<T>>, poll: Duration
|
||||||
|
) -> Result<Thread, std::io::Error> {
|
||||||
|
let exited = exited.clone();
|
||||||
|
let state = state.clone();
|
||||||
|
Thread::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:
|
||||||
|
_ => {
|
||||||
|
let event = TuiEvent::from_crossterm(event);
|
||||||
|
if let Err(e) = state.write().unwrap().handle(&event) {
|
||||||
|
panic!("{e}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawn the TUI output thread which writes colored characters to the terminal.
|
||||||
|
#[cfg(feature = "tui")] pub fn tui_output <T: Draw<Buffer> + Send + Sync + 'static> (
|
||||||
|
exited: &Arc<AtomicBool>, state: &Arc<RwLock<T>>, sleep: Duration
|
||||||
|
) -> Result<Thread, std::io::Error> {
|
||||||
|
let state = state.clone();
|
||||||
|
let mut backend = CrosstermBackend::new(stdout());
|
||||||
|
let WH(width, height) = tui_wh(&mut backend);
|
||||||
|
let mut buffer_a = Buffer::empty(Rect { x: 0, y: 0, width, height });
|
||||||
|
let mut buffer_b = Buffer::empty(Rect { x: 0, y: 0, width, height });
|
||||||
|
Thread::new_sleep(exited.clone(), sleep, move |perf| {
|
||||||
|
let size = tui_wh(&mut backend);
|
||||||
|
if let Ok(state) = state.try_read() {
|
||||||
|
tui_resize(&mut backend, &mut buffer_a, size);
|
||||||
|
buffer_a = tui_redraw(&mut backend, &mut buffer_a, &mut buffer_b);
|
||||||
|
}
|
||||||
|
let timer = format!("{:>3.3}ms", perf.used.load(Relaxed));
|
||||||
|
buffer_a.set_string(0, 0, &timer, Style::default());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "tui")] pub fn tui_resize <W: Write> (
|
||||||
|
backend: &mut CrosstermBackend<W>,
|
||||||
|
buffer: &mut Buffer,
|
||||||
|
size: WH<u16>
|
||||||
|
) {
|
||||||
|
if buffer.area != size {
|
||||||
|
backend.clear_region(ClearType::All).unwrap();
|
||||||
|
buffer.resize(size);
|
||||||
|
buffer.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "tui")] pub fn tui_redraw <'b, W: Write> (
|
||||||
|
backend: &mut CrosstermBackend<W>,
|
||||||
|
mut prev_buffer: &'b mut Buffer,
|
||||||
|
mut next_buffer: &'b mut Buffer
|
||||||
|
) {
|
||||||
|
let updates = prev_buffer.diff(&next_buffer);
|
||||||
|
backend.draw(updates.into_iter()).expect("failed to render");
|
||||||
|
Backend::flush(backend).expect("failed to flush output new_buffer");
|
||||||
|
std::mem::swap(&mut prev_buffer, &mut next_buffer);
|
||||||
|
next_buffer.reset();
|
||||||
|
next_buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export] macro_rules! tui_main {
|
||||||
|
($expr:expr) => {
|
||||||
|
fn main () -> Usually<()> {
|
||||||
|
let engine = ::tengri::Tui::new(Box::new(stdout()))?;
|
||||||
|
let state = ::std::sync::Arc::new(std::sync::RwLock::new($expr));
|
||||||
|
engine.run(true, &state)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue