breaking: refactor: cooking
Some checks failed
/ build (push) Has been cancelled

This commit is contained in:
same mf who else 2026-03-08 22:13:12 +02:00
parent ad070a4cbb
commit 8f0a2accce
13 changed files with 1819 additions and 2157 deletions

View file

@ -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" }

View file

@ -1,34 +1,36 @@
***tengri*** is the skygod in whose dream the [**`tek`**](https://codeberg.org/unspeaker/tek) # tengri [![Please don't upload to GitHub](https://nogithub.codeberg.page/badge.svg)](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
View 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
View 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
View 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,
);
}
}
}

View file

@ -9,502 +9,56 @@
#![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::{
io::{stdout, Stdout, Write},
sync::{Arc, Weak, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::*}},
fmt::{Debug, Display},
ops::{Add, Sub, Mul, Div},
marker::PhantomData,
time::Duration,
thread::{spawn, JoinHandle}
}
};
#[cfg(feature = "lang")] extern crate dizzle;
#[cfg(feature = "lang")] pub use ::dizzle::self as lang;
#[cfg(feature = "draw")] pub mod draw;
#[cfg(feature = "tui")] pub extern crate ratatui; #[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 extern crate crossterm;
#[cfg(feature = "tui")] pub(crate) use ::crossterm::{ #[cfg(feature = "tui")] pub(crate) use ::{
ExecutableCommand, ratatui::{
terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode}, prelude::{Color, Style, Buffer, Position},
event::{poll, read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState}, style::{Stylize, Modifier, Color::*},
}; backend::{Backend, CrosstermBackend, ClearType},
pub(crate) use ::std::{ layout::{Size, Rect},
io::{stdout, Stdout, Write}, buffer::Cell
sync::{Arc, Weak, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::*}}, },
fmt::{Debug, Display}, crossterm::{
ops::{Add, Sub, Mul, Div}, ExecutableCommand,
marker::PhantomData, terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode},
time::Duration, event::{poll, read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState},
thread::{spawn, JoinHandle} }
}; };
// Define macros first, so that private macros are available in private modules: #[cfg(feature = "sing")] pub extern crate jack;
#[cfg(feature = "sing")] pub mod sing;
/// Clear a pre-allocated buffer, then write into it. #[cfg(feature = "sing")] pub use ::jack::{*, contrib::{*, ClosureProcessHandler}};
#[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 {
($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 }
}
}
}
/// 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
View 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
}

File diff suppressed because it is too large Load diff

60
src/tengri_macros.rs Normal file
View 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 }
}
}
}

View file

@ -1,148 +1,27 @@
pub use self::logical::*; mod logical { use crate::*;
use crate::*;
/// Thunks can be natural error boundaries! /// Memoize a rendering.
/// ///
/// ``` /// ```
/// let _ = tengri::ErrorBoundary::<tengri::Tui, &'static str>::new(Ok(Some("hello"))); /// let _ = tengri::Memo::new((), ());
/// let _ = tengri::ErrorBoundary::<tengri::Tui, &'static str>::new(Ok(None)); /// ```
/// let _ = tengri::ErrorBoundary::<tengri::Tui, &'static str>::new(Err("draw fail".into())); #[derive(Debug, Default)] pub struct Memo<T, U> {
/// ``` pub value: T,
pub struct ErrorBoundary<O: Out, T: Draw<O>>( pub view: Arc<RwLock<U>>
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.
///
/// ```
/// let _ = tengri::Memo::new((), ());
/// ```
#[derive(Debug, Default)] pub struct Memo<T, U> {
pub value: T,
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 {
} }
} }
#[derive(Clone)] pub struct Exit(Arc<AtomicBool>);
impl Exit {
pub fn run <T> (run: impl Fn()->Usually<T>) -> Usually<T> {
run(Self(Arc::new(AtomicBool::new(false))))
}
}
#[derive(Debug)] pub struct Thread {
/// Exit flag.
pub exit: Arc<AtomicBool>,
/// Performance counter.
pub perf: Arc<PerfModel>,
/// Use this to wait for the thread to finish.
pub join: JoinHandle<()>,
}
#[cfg(feature = "tui")] pub use self::terminal::*; #[cfg(feature = "tui")] pub use self::terminal::*;
#[cfg(feature = "tui")] mod terminal { #[cfg(feature = "tui")] mod terminal {
use crate::*; use crate::*;
/// TUI input loop event.
/// The TUI engine. #[derive(Debug, Clone, Eq, PartialEq, PartialOrd)]
/// pub struct TuiEvent(pub Event);
/// ``` /// TUI key spec.
/// # fn main () -> tengri::Usually<()> { #[derive(Debug, Clone, Eq, PartialEq, PartialOrd)]
/// let tui = tengri::Tui::new(Box::new(vec![0u8;0]))?; pub struct TuiKey(pub Option<KeyCode>, pub KeyModifiers);
/// # 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 {
pub exit: Arc<AtomicBool>,
pub perf: Arc<PerfModel>,
pub join: JoinHandle<()>,
}
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)] pub struct TuiEvent(
pub Event
);
#[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
View 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(())
//}

View file

@ -1,455 +1,341 @@
pub use self::input::*; mod input { use crate::*;
use crate::*;
/// Something that will exit and not resume, e.g. the main input loop. /// Output target.
/// ``` ///
/// use ::tengri::Done; /// ```
/// use ::std::sync::atomic::{AtomicBool, Ordering}; /// use tengri::*;
/// use Ordering::Relaxed; ///
/// /// struct TestOut(impl TwoD<u16>);
/// struct Example(AtomicBool); ///
/// impl Done for Example { /// impl tengri::Out for TestOut {
/// fn is_done (&self) -> bool { self.0.load(Relaxed) } /// type Unit = u16;
/// fn done (&self) { self.0.store(true, Relaxed) } /// fn area (&self) -> impl TwoD<u16> { self.0 }
/// } /// fn area_mut (&mut self) -> &mut impl TwoD<u16> { &mut self.0 }
/// /// fn show <T: Draw<Self> + ?Sized> (&mut self, area: impl TwoD<u16>, _: &T) {
/// assert!(Example(true.into()).is_done()); /// println!("show: {area:?}");
/// assert!(!Example(false.into()).is_done()); /// ()
/// /// }
/// let state = Example(false.into()); /// }
/// while !state.is_done() { ///
/// state.done(); // exit immediately /// impl tengri::Draw<TestOut> for String {
/// } /// fn draw (&self, to: &mut TestOut) {
/// ``` /// //to.area_mut().set_w(self.len() as u16);
pub trait Done { /// }
fn is_done (&self) -> bool; /// }
fn done (&self); /// ```
} pub trait Screen: W<Self::Unit> + H<Self::Unit> + Send + Sync + Sized {
type Unit: Coord;
/// Source of [Input::Event]s: keyboard, mouse... /// Render drawable in area specified by `area`
/// fn show <'t, T: Draw<Self> + ?Sized> (&mut self, area: TwoD<Self::Unit>, content: &'t T);
/// ```
/// 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 { pub trait HasColor {
use crate::*; fn color (&self) -> ItemColor;
/// Output target. }
///
/// ``` // Something that has a [Measure] of its rendered size.
/// use tengri::*; pub trait Measured<O: Screen> {
/// fn measure (&self) -> &Measure<O>;
/// struct TestOut(XYWH<u16>); fn measure_width (&self) -> O::Unit { self.measure().w() }
/// fn measure_height (&self) -> O::Unit { self.measure().h() }
/// impl tengri::Out for TestOut { }
/// type Unit = u16;
/// fn area (&self) -> XYWH<u16> { self.0 } #[cfg(feature = "tui")] pub trait TuiOut: Screen {
/// fn area_mut (&mut self) -> &mut XYWH<u16> { &mut self.0 } fn tui_out (&mut self) -> &mut Buffer;
/// fn place_at <T: Draw<Self> + ?Sized> (&mut self, area: XYWH<u16>, _: &T) { fn update (&mut self, area: impl TwoD<u16>, callback: &impl Fn(&mut Cell, u16, u16)) {
/// println!("place_at: {area:?}"); 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);})
/// }
/// impl tengri::Draw<TestOut> for String { fn fill_bg (&mut self, area: impl TwoD<u16>, color: Color) {
/// fn draw (&self, to: &mut TestOut) { self.update(area, &|cell,_,_|{cell.set_bg(color);})
/// //to.area_mut().set_w(self.len() as u16); }
/// } fn fill_fg (&mut self, area: impl TwoD<u16>, color: Color) {
/// } self.update(area, &|cell,_,_|{cell.set_fg(color);})
/// ``` }
pub trait Out: Send + Sync + Sized { fn fill_mod (&mut self, area: impl TwoD<u16>, on: bool, modifier: Modifier) {
/// Unit of length if on {
type Unit: Coord; self.update(area, &|cell,_,_|cell.modifier.insert(modifier))
/// Current output area } else {
fn area (&self) -> XYWH<Self::Unit>; self.update(area, &|cell,_,_|cell.modifier.remove(modifier))
/// Mutable pointer to area.
fn area_mut (&mut self) -> &mut XYWH<Self::Unit>;
/// Render drawable in area specified by `area`
fn place_at <'t, T: Draw<Self> + ?Sized> (&mut self, area: XYWH<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)
} }
} }
/// A numeric type that can be used as coordinate. 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
/// ///
/// FIXME: Replace this ad-hoc trait with `num` crate. /// TODO: do a paragraph (handle newlines)
pub trait Coord: Send + Sync + Copy fn text (&mut self, text: &impl AsRef<str>, x0: u16, y: u16, max_width: u16) {
+ Add<Self, Output=Self> let text = text.as_ref();
+ Sub<Self, Output=Self> let buf = self.buffer();
+ Mul<Self, Output=Self> let mut string_width: u16 = 0;
+ Div<Self, Output=Self> for character in text.chars() {
+ Ord + PartialEq + Eq let x = x0 + string_width;
+ Debug + Display + Default let character_width = character.width().unwrap_or(0) as u16;
+ From<u16> + Into<u16> string_width += character_width;
+ Into<usize> if string_width > max_width {
+ Into<f64> break
{ }
fn plus (self, other: Self) -> Self; if let Some(cell) = buf.write().unwrap().cell_mut(ratatui::prelude::Position { x, y }) {
fn minus (self, other: Self) -> Self { cell.set_char(character);
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 { } else {
Ok(self) break
} }
} }
} }
pub trait HasPerf { fn perf (&self) -> &PerfModel; }
pub trait HasColor { fn color (&self) -> ItemColor; }
// Something that has a [Measure] of its rendered size.
pub trait Measured<O: Out> {
fn measure (&self) -> &Measure<O>;
fn measure_width (&self) -> O::Unit { self.measure().w() }
fn measure_height (&self) -> O::Unit { self.measure().h() }
}
}
#[cfg(feature = "tui")] pub use self::tui::*;
#[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 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_s (&self) -> &str { Self::S }
fn border_e (&self) -> &str { Self::E }
fn border_w (&self) -> &str { Self::W }
fn border_nw (&self) -> &str { Self::NW }
fn border_ne (&self) -> &str { Self::NE }
fn border_sw (&self) -> &str { Self::SW }
fn border_se (&self) -> &str { Self::SE }
#[inline] fn draw <'a> (
&self, to: &mut Tui
) -> Usually<()> {
if self.enabled() {
self.draw_horizontal(to, None)?;
self.draw_vertical(to, None)?;
self.draw_corners(to, None)?;
}
Ok(())
}
#[inline] fn draw_horizontal (
&self, to: &mut Tui, style: Option<Style>
) -> Usually<XYWH<u16>> {
let area = to.area();
let style = style.or_else(||self.style_horizontal());
let [x, x2, y, y2] = area.lrtb();
for x in x..x2.saturating_sub(1) {
to.blit(&Self::N, x, y, style);
to.blit(&Self::S, x, y2.saturating_sub(1), style)
}
Ok(area)
}
#[inline] fn draw_vertical (
&self, to: &mut Tui, style: Option<Style>
) -> Usually<XYWH<u16>> {
let area = to.area();
let style = style.or_else(||self.style_vertical());
let [x, x2, y, y2] = area.lrtb();
let h = y2 - y;
if h > 1 {
for y in y..y2.saturating_sub(1) {
to.blit(&Self::W, x, y, style);
to.blit(&Self::E, x2.saturating_sub(1), y, style);
}
} else if h > 0 {
to.blit(&Self::W0, x, y, style);
to.blit(&Self::E0, x2.saturating_sub(1), y, style);
}
Ok(area)
}
#[inline] fn draw_corners (
&self, to: &mut Tui, style: Option<Style>
) -> Usually<XYWH<u16>> {
let area = to.area();
let style = style.or_else(||self.style_corners());
let XYWH(x, y, width, height) = area;
if width > 1 && height > 1 {
to.blit(&Self::NW, x, y, style);
to.blit(&Self::NE, x + width - 1, y, style);
to.blit(&Self::SW, x, y + height - 1, style);
to.blit(&Self::SE, x + width - 1, y + height - 1, style);
}
Ok(area)
}
#[inline] fn style (&self) -> Option<Style> { None }
#[inline] fn style_horizontal (&self) -> Option<Style> { self.style() }
#[inline] fn style_vertical (&self) -> Option<Style> { self.style() }
#[inline] fn style_corners (&self) -> Option<Style> { self.style() }
}
} }
#[cfg(feature = "jack")] pub use self::jack::*; #[cfg(feature = "tui")] pub trait BorderStyle: Draw<Buffer> + Copy {
#[cfg(feature = "jack")] mod jack { fn enabled (&self) -> bool;
use crate::*; fn border_n (&self) -> &str { Self::N }
use ::jack::{*, contrib::{*, Position}}; fn border_s (&self) -> &str { Self::S }
fn border_e (&self) -> &str { Self::E }
fn border_w (&self) -> &str { Self::W }
fn border_nw (&self) -> &str { Self::NW }
fn border_ne (&self) -> &str { Self::NE }
fn border_sw (&self) -> &str { Self::SW }
fn border_se (&self) -> &str { Self::SE }
/// Things that can provide a [jack::Client] reference. fn enclose (self, w: impl Draw<Buffer>) -> impl Draw<Buffer> {
/// Bsp::b(Fill::XY(Border(self.enabled(), self)), w)
/// ```
/// use tengri::{Jack, HasJack};
///
/// let jack: &Jack = Jacked::default().jack();
///
/// #[derive(Default)] struct Jacked<'j>(Jack<'j>);
///
/// impl<'j> HasJack<'j> for Jacked<'j> {
/// fn jack (&self) -> &Jack<'j> { &self.0 }
/// }
/// ```
pub trait HasJack<'j>: Send + Sync {
/// Return the internal [jack::Client] handle
/// that lets you call the JACK API.
fn jack (&self) -> &Jack<'j>;
fn with_client <T> (&self, op: impl FnOnce(&Client)->T) -> T {
self.jack().with_client(op)
}
fn port_by_name (&self, name: &str) -> Option<Port<Unowned>> {
self.with_client(|client|client.port_by_name(name))
}
fn port_by_id (&self, id: u32) -> Option<Port<Unowned>> {
self.with_client(|c|c.port_by_id(id))
}
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())?))
}
fn sync_lead (&self, enable: bool, callback: impl Fn(TimebaseInfo)->Position) -> Usually<()> {
if enable {
self.with_client(|client|match client.register_timebase_callback(false, callback) {
Ok(_) => Ok(()),
Err(e) => Err(e)
})?
}
Ok(())
}
fn sync_follow (&self, _enable: bool) -> Usually<()> {
// TODO: sync follow
Ok(())
}
} }
fn enclose2 (self, w: impl Draw<Buffer>) -> impl Draw<Buffer> {
/// Trait for thing that has a JACK process callback. Bsp::b(Pad::XY(1, 1, Fill::XY(Border(self.enabled(), self))), w)
pub trait Audio {
/// Handle a JACK event.
fn handle (&mut self, _event: JackEvent) {}
/// Projecss a JACK chunk.
fn process (&mut self, _: &Client, _: &ProcessScope) -> Control {
Control::Continue
}
/// The JACK process callback function passed to the server.
fn callback (
state: &Arc<RwLock<Self>>, client: &Client, scope: &ProcessScope
) -> Control where Self: Sized {
if let Ok(mut state) = state.write() {
state.process(client, scope)
} else {
Control::Quit
}
}
} }
fn enclose_bg (self, w: impl Draw<Buffer>) -> impl Draw<Buffer> {
/// Implement [Audio]: provide JACK callbacks. TuiOut::bg(self.style().unwrap().bg.unwrap_or(Color::Reset),
#[macro_export] macro_rules! impl_audio { Bsp::b(Fill::XY(Border(self.enabled(), self)), w))
(|
$self1:ident:
$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?,$c:ident,$s:ident
|$cb:expr$(;|$self2:ident,$e:ident|$cb2:expr)?) => {
impl $(<$($L),*$($T $(: $U)?),*>)? Audio for $Struct $(<$($L),*$($T),*>)? {
#[inline] fn process (&mut $self1, $c: &Client, $s: &ProcessScope) -> Control { $cb }
$(#[inline] fn handle (&mut $self2, $e: JackEvent) { $cb2 })?
}
};
($Struct:ident: $process:ident, $handle:ident) => {
impl Audio for $Struct {
#[inline] fn process (&mut self, c: &Client, s: &ProcessScope) -> Control {
$process(self, c, s)
}
#[inline] fn handle (&mut self, e: JackEvent) {
$handle(self, e)
}
}
};
($Struct:ident: $process:ident) => {
impl Audio for $Struct {
#[inline] fn process (&mut self, c: &Client, s: &ProcessScope) -> Control {
$process(self, c, s)
}
}
};
} }
#[inline] fn draw <'a> (&self, to: &mut impl TuiOut) -> Usually<()> {
if self.enabled() {
self.draw_h(to, None)?;
self.draw_v(to, None)?;
self.draw_c(to, None)?;
}
Ok(())
}
#[inline] fn draw_h (&self, to: &mut impl TuiOut, style: Option<Style>) -> Usually<impl TwoD<u16>> {
let area = to.area();
let style = style.or_else(||self.style_horizontal());
let [x, x2, y, y2] = area.lrtb();
for x in x..x2.saturating_sub(1) {
to.blit(&Self::N, x, y, style);
to.blit(&Self::S, x, y2.saturating_sub(1), style)
}
Ok(area)
}
#[inline] fn draw_v (&self, to: &mut impl TuiOut, style: Option<Style>) -> Usually<impl TwoD<u16>> {
let area = to.area();
let style = style.or_else(||self.style_vertical());
let [x, x2, y, y2] = area.lrtb();
let h = y2 - y;
if h > 1 {
for y in y..y2.saturating_sub(1) {
to.blit(&Self::W, x, y, style);
to.blit(&Self::E, x2.saturating_sub(1), y, style);
}
} else if h > 0 {
to.blit(&Self::W0, x, y, style);
to.blit(&Self::E0, x2.saturating_sub(1), y, style);
}
Ok(area)
}
#[inline] fn draw_c (&self, to: &mut impl TuiOut, style: Option<Style>) -> Usually<impl TwoD<u16>> {
let area = to.area();
let style = style.or_else(||self.style_corners());
let XYWH(x, y, w, h) = area;
if w > 1 && h > 1 {
to.blit(&Self::NW, x, y, style);
to.blit(&Self::NE, x + w - 1, y, style);
to.blit(&Self::SW, x, y + h- 1, style);
to.blit(&Self::SE, x + w - 1, y + h - 1, style);
}
Ok(area)
}
#[inline] fn style (&self) -> Option<Style> { None }
#[inline] fn style_horizontal (&self) -> Option<Style> { self.style() }
#[inline] fn style_vertical (&self) -> Option<Style> { self.style() }
#[inline] fn style_corners (&self) -> Option<Style> { self.style() }
pub trait JackPerfModel { const NW: &'static str = "";
fn update_from_jack_scope (&self, t0: Option<u64>, scope: &ProcessScope); 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 = "";
}
/// Things that can provide a [jack::Client] reference.
///
/// ```
/// use tengri::{Jack, HasJack};
///
/// let jack: &Jack = Jacked::default().jack();
///
/// #[derive(Default)] struct Jacked<'j>(Jack<'j>);
///
/// impl<'j> HasJack<'j> for Jacked<'j> {
/// fn jack (&self) -> &Jack<'j> { &self.0 }
/// }
/// ```
#[cfg(feature = "jack")] pub trait HasJack<'j>: Send + Sync {
/// Return the internal [jack::Client] handle
/// that lets you call the JACK API.
fn jack (&self) -> &Jack<'j>;
fn with_client <T> (&self, op: impl FnOnce(&Client)->T) -> T {
self.jack().with_client(op)
}
fn port_by_name (&self, name: &str) -> Option<Port<Unowned>> {
self.with_client(|client|client.port_by_name(name))
}
fn port_by_id (&self, id: u32) -> Option<Port<Unowned>> {
self.with_client(|c|c.port_by_id(id))
}
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())?))
}
fn sync_lead (&self, enable: bool, callback: impl Fn(TimebaseInfo)->Position) -> Usually<()> {
if enable {
self.with_client(|client|match client.register_timebase_callback(false, callback) {
Ok(_) => Ok(()),
Err(e) => Err(e)
})?
}
Ok(())
}
fn sync_follow (&self, _enable: bool) -> Usually<()> {
// TODO: sync follow
Ok(())
} }
} }
/// Trait for thing that has a JACK process callback.
#[cfg(feature = "jack")] pub trait Audio {
/// Handle a JACK event.
fn handle (&mut self, _event: JackEvent) {}
/// Projecss a JACK chunk.
fn process (&mut self, _: &Client, _: &ProcessScope) -> Control {
Control::Continue
}
/// The JACK process callback function passed to the server.
fn callback (
state: &Arc<RwLock<Self>>, client: &Client, scope: &ProcessScope
) -> Control where Self: Sized {
if let Ok(mut state) = state.write() {
state.process(client, scope)
} else {
Control::Quit
}
}
}
/// Implement [Audio]: provide JACK callbacks.
#[cfg(feature = "jack")] #[macro_export] macro_rules! impl_audio {
(|
$self1:ident:
$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?,$c:ident,$s:ident
|$cb:expr$(;|$self2:ident,$e:ident|$cb2:expr)?) => {
impl $(<$($L),*$($T $(: $U)?),*>)? Audio for $Struct $(<$($L),*$($T),*>)? {
#[inline] fn process (&mut $self1, $c: &Client, $s: &ProcessScope) -> Control { $cb }
$(#[inline] fn handle (&mut $self2, $e: JackEvent) { $cb2 })?
}
};
($Struct:ident: $process:ident, $handle:ident) => {
impl Audio for $Struct {
#[inline] fn process (&mut self, c: &Client, s: &ProcessScope) -> Control {
$process(self, c, s)
}
#[inline] fn handle (&mut self, e: JackEvent) {
$handle(self, e)
}
}
};
($Struct:ident: $process:ident) => {
impl Audio for $Struct {
#[inline] fn process (&mut self, c: &Client, s: &ProcessScope) -> Control {
$process(self, c, s)
}
}
};
}
#[cfg(feature = "jack")] pub trait JackPerfModel {
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
View 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(())
}
};
}