tengri/src/lib.rs
facile pop culture reference a27dacff93 moar flat
2026-08-01 17:52:37 +03:00

2035 lines
72 KiB
Rust

//#![feature(anonymous_lifetime_in_impl_trait)]
//#![feature(associated_type_defaults)]
//#![feature(const_default)]
//#![feature(const_option_ops)]
//#![feature(const_precise_live_drops)]
//#![feature(const_trait_impl)]
//#![feature(impl_trait_in_assoc_type)]
//#![feature(step_trait)]
//#![feature(trait_alias)]
//#![feature(type_alias_impl_trait)]
//#![feature(type_changing_struct_update)]
pub extern crate atomic_float;
pub extern crate palette;
pub extern crate better_panic;
pub extern crate unicode_width;
#[cfg(feature = "sing")] pub extern crate jack;
#[cfg(feature = "midi")] pub extern crate midly;
#[cfg(feature = "term")] pub extern crate ratatui;
#[cfg(feature = "term")] pub extern crate crossterm;
#[cfg(feature = "lang")] pub extern crate dizzle;
#[cfg(test)] #[macro_use] pub extern crate proptest;
#[cfg(test)] pub(crate) use proptest_derive::Arbitrary;
pub(crate) use ::{
atomic_float::AtomicF64,
std::fmt::{Debug, Display},
std::ops::{Add, Sub, Mul, Div},
std::sync::{Arc, RwLock},
std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::*},
std::marker::PhantomData
};
macro_rules! features {
($($feature:literal: [ $($module:ident),* ]),*) => {
$(
$(
#[cfg(feature = $feature)] pub mod $module;
#[cfg(feature = $feature)] pub use $module::*;
)*
)*
}
}
#[cfg(feature = "lang")] pub use ::dizzle::{Usually, Perhaps};
#[cfg(feature = "lang")] use ::dizzle::*;
features! {
"time": [ time ],
"sing": [ sing ]
}
/// 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),*) })*
}
};
);
/// Implement [Handle] for given `State` and `handler`.
#[macro_export] macro_rules! impl_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
//}
//}
}
}
/// Implement [Default].
#[macro_export] macro_rules! impl_default {
($T:ty:$e:expr) => { impl Default for $T { fn default () -> Self { $e } } };
}
/// Implement [`Debug`] in bulk.
#[macro_export] macro_rules! impl_debug (($($S:ty|$self:ident,$w:ident|$body:block)*)=>{
$(impl std::fmt::Debug for $S { fn fmt (&$self, $w: &mut std::fmt::Formatter) -> std::fmt::Result $body })*
});
/// Implement [`From`] in bulk.
#[macro_export] macro_rules! impl_from (
($(<$($lt:lifetime),+>)?$Target:ty:|$state:ident:$Source:ty|$cb:expr) => {
impl $(<$($lt),+>)? From<$Source> for $Target { fn from ($state:$Source) -> Self { $cb }}
};
($($Struct:ty { $( $(<$($l:lifetime),* $($T:ident$(:$U:ident)?),*>)? ($source:ident: $From:ty) $expr:expr );+ $(;)? })*) => { $(
$(impl $(<$($l),* $($T$(:$U)?),*>)? From<$From> for $Struct { fn from ($source: $From) -> Self { $expr } })+
)* };
);
/// Implement [AsRef].
#[macro_export] macro_rules! impl_as_ref (($T:ty: |$self:ident:$S:ty|$x:expr)=>{
impl AsRef<$T> for $S { fn as_ref (&$self) -> &$T { $x } }
});
/// Implement [AsMut].
#[macro_export] macro_rules! impl_as_mut (($T:ty: |$self:ident:$S:ty|$x:expr)=>{
impl AsMut<$T> for $S { fn as_mut (&mut $self) -> &mut $T { $x } }
});
/// Implement [AsRefOpt].
#[macro_export] macro_rules! impl_as_ref_opt (($T:ty: |$self:ident:$S:ty|$x:expr)=>{
impl AsRefOpt<$T> for $S { fn as_ref_opt (&$self) -> Option<&$T> { $x } }
});
/// Implement [AsMutOpt].
#[macro_export] macro_rules! impl_as_mut_opt (($T:ty: |$self:ident:$S:ty|$x:expr)=>{
impl AsMutOpt<$T> for $S { fn as_mut_opt (&mut $self) -> Option<&mut $T> { $x } }
});
pub trait AsRefOpt<T> { fn as_ref_opt (&self) -> Option<&T>; }
pub trait AsMutOpt<T> { fn as_mut_opt (&mut self) -> Option<&mut T>; }
/// Implement [AsRef] and [AsMut].
#[macro_export] macro_rules! impl_has (
($T:ty: |$self:ident:$S:ty|$x:expr)=>{
impl AsRef<$T> for $S {
fn as_ref (&$self) -> &$T { &$x }
}
impl AsMut<$T> for $S {
fn as_mut (&mut $self) -> &mut $T { &mut $x }
}
};
($T:ty: |$self:ident:$S:ty|$x:block;$y:block)=>{
impl AsRef<$T> for $S {
fn as_ref (&$self) -> &$T $x
}
impl AsMut<$T> for $S {
fn as_mut (&mut $self) -> &mut $T $y
}
}
);
/// Some layout operations exist in multiple variants that take a single argument.
/// Their handling in [eval_view] is uniform and goes like this:
macro_rules! eval_enum ((
$name:literal, $output:ident, $state:ident, $value:expr, $arg0: ident, $Enum:ident {
$($v:literal => $V:ident),* $(,)?
}
) => {{
match $value {
$(Some($v) => $Enum::$V,)*
frag => unimplemented!("{}/{frag:?}", $name)
}
}});
/// Some layout operations exist in XY, X, and Y variants that take 3 or 2 arguments.
/// Their handling in [eval_view] is uniform and goes like this:
macro_rules! eval_xy (
// Valueless variant:
// (fill/x ...)
// (fill/y ...)
// (fill/xy ...)
(
$name:expr => $expr:expr, $head:expr, $output:ident, $state:ident,
$variant:expr, $xy:ident, $x:ident, $y:ident, $arg: ident,
) => {{
// frags.next(): 2nd slash-delimited fragment: /x, /y, /xy
let variant = $variant;
let thunk = draw(move|screen|$state.interpret(screen, &$arg));
match variant {
// X variant
Some("x") => thunk.$x().draw($output),
// Y variant
Some("y") => thunk.$y().draw($output),
// XY variant (can be omitted)
Some("xy") | None => thunk.$xy().draw($output),
// Other namespace members are invalid
frag => invalid_variant($name, frag, $expr, $head)
}
}};
// Variadic variant:
// (push/x n ...)
// (push/y n ...)
// (push/xy n m ...)
(
$name:expr => $expr:expr, $head:expr, $output:ident, $state:ident,
$variant:expr, $xy:ident, $x:ident, $y:ident, $arg0: ident, $arg1: ident, $arg2: ident,
) => {{
// frags.next(): 2nd slash-delimited fragment: /x, /y, /xy
let variant = $variant;
let thunk = draw(move|screen|$state.interpret(screen, &match variant {
Some("x") | Some("y") => $arg1,
Some("xy") | None => $arg2,
_ => panic!("{}: unsupported axis {variant:?}; try /x, /y, /xy", $name)
}));
match variant {
// X variant
Some("x") => thunk.$x($state.namespace($arg0?)?)
.draw($output),
// Y variant
Some("y") => thunk.$y($state.namespace($arg0?)?)
.draw($output),
// XY variant (can be omitted)
Some("xy") | None => thunk.$xy($state.namespace($arg0?)?, $state.namespace($arg1?)?)
.draw($output),
// Other namespace members are invalid
frag => invalid_variant($name, frag, $expr, $head)
}
}};
);
#[cfg(feature = "exit")] pub use self::exit::*;
#[cfg(feature = "exit")] mod exit {
use crate::*;
use std::sync::{Arc, atomic::AtomicBool};
use crossterm::event::*;
#[derive(Clone)] pub struct Exit(Arc<AtomicBool>);
impl Exit {
pub fn run <T> (run: impl FnOnce(Self)->Usually<T>) -> Usually<T> {
run(Self(Arc::new(AtomicBool::new(false))))
}
pub fn is (event: &Event) -> bool {
matches!(event, Event::Key(KeyEvent {
modifiers: KeyModifiers::CONTROL,
code: KeyCode::Char('c'),
kind: KeyEventKind::Press,
state: KeyEventState::NONE
}))
}
}
impl AsRef<Arc<AtomicBool>> for Exit {
fn as_ref (&self) -> &Arc<AtomicBool> {
&self.0
}
}
}
#[cfg(feature = "play")] pub use self::task::*;
#[cfg(feature = "play")] mod task {
use std::{
time::Duration,
sync::{Arc, atomic::{AtomicBool, Ordering::*}},
thread::{Builder, JoinHandle, sleep},
};
#[cfg(feature = "term")] use ::crossterm::event::poll;
use crate::time::PerfModel;
#[derive(Debug)] pub struct Task {
/// Exit flag.
pub exit: Arc<AtomicBool>,
/// Performance counter.
pub perf: Arc<PerfModel>,
/// Use this to wait for the thread to finish.
pub join: JoinHandle<()>,
}
impl Task {
/// Spawn a TUI thread that runs `callt least one, then repeats until `exit`.
pub fn new <F> (exit: Arc<AtomicBool>, mut call: F) -> Result<Self, std::io::Error>
where F: FnMut(&PerfModel)->() + Send + Sync + 'static
{
let perf = Arc::new(PerfModel::default());
Ok(Self {
exit: exit.clone(),
perf: perf.clone(),
join: Builder::new().name("tengri tui output".into()).spawn(move || {
while !exit.fetch_and(true, Relaxed) {
let _ = perf.cycle(&mut call);
}
})?.into()
})
}
/// Spawn a thread that runs `call` least one, then repeats
/// until `exit`, sleeping for `time` msec after every iteration.
pub fn new_sleep <F> (
exit: Arc<AtomicBool>, time: Duration, mut call: F
) -> Result<Self, std::io::Error>
where F: FnMut(&PerfModel)->() + Send + Sync + 'static
{
Self::new(exit, move |perf| {
let _ = call(perf);
sleep(time);
})
}
/// Spawn a thread that uses [crossterm::event::poll]
/// to run `call` every `time` msec.
#[cfg(feature = "term")] pub fn new_poll <F> (
exit: Arc<AtomicBool>, time: Duration, mut call: F
) -> Result<Self, std::io::Error>
where F: FnMut(&PerfModel)->() + Send + Sync + 'static
{
Self::new(exit, move |perf| {
if poll(time).is_ok() {
let _ = call(perf);
}
})
}
pub fn join (self) -> Result<(), Box<dyn std::any::Any + Send>> {
self.join.join()
}
}
}
#[cfg(feature = "draw")] pub use self::draw::*;
#[cfg(feature = "draw")] mod draw {
use crate::*;
use Azimuth::*;
use Split::*;
/// Output target.
///
/// ```
/// use tengri::*;
/// struct TestOut { w: u16, h: u16 };
/// impl Wide<u16> for TestOut {}
/// impl Tall<u16> for TestOut {}
/// impl Xy<u16> for TestOut {
/// fn x (&self) -> u16 { 0 }
/// fn y (&self) -> u16 { 0 }
/// }
/// impl Screen for TestOut {
/// type Unit = u16;
/// fn show (&mut self, _: impl Draw<Self>) -> Perhaps<XYWH<u16>> {
/// println!("placed");
/// Ok(None)
/// }
/// fn area (&self) -> XYWH<Self::Unit> {
/// Default::default()
/// }
/// fn clip <T> (
/// &mut self,
/// area: impl Into<Option<XYWH<u16>>>,
/// draw: impl FnOnce(&mut Self)->T
/// ) -> T {
/// draw(self)
/// }
/// }
///
/// impl_draw!(|self: String, to: TestOut|{
/// to.w = self.len() as u16;
/// Ok(None)
/// });
/// ```
pub trait Screen: Xy<Self::Unit> + Wh<Self::Unit> + Send + Sync + Sized {
type Unit: Coord;
/// Render drawable in subarea specified by `area`
fn show (&mut self, content: impl Draw<Self>) -> Perhaps<XYWH<Self::Unit>>;
/// Get current clipping area
fn area (&self) -> XYWH<Self::Unit>;
/// Set clipping area
fn clip <T> (
&mut self,
area: impl Into<Option<XYWH<Self::Unit>>>,
draw: impl FnOnce(&mut Self)->T
) -> T;
}
/// Implement the [Draw] trait for a particular drawable and [Screen].
///
/// ```
/// use tengri::*;
/// struct MyDrawable;
/// impl_draw!(|self: MyDrawable, to: Tui|{
/// todo!("your draw logic")
/// });
/// ```
#[macro_export] macro_rules! impl_draw (
($(<$($T:ident: $Trait:path,)+>)?|
$self:ident:$Self:path, $to:ident:$To:ty
|$draw:block)=>{ impl$(<$($T:$Trait),+>)? Draw<$To> for $Self {
fn draw ($self, $to: &mut $To) -> Perhaps<XYWH<<$To as Screen>::Unit>> $draw
} };
($(<$($T:ident: $Trait:path,)+>)?|
$self:ident:$Self:ty, $to:ident:$To:ty
|$draw:block)=>{ impl$(<$($T:$Trait),+>)? Draw<$To> for $Self {
fn draw ($self, $to: &mut $To) -> Perhaps<XYWH<<$To as Screen>::Unit>> $draw
} }
);
/// Drawable that supports dynamic dispatch.
///
/// Drawables are composable, e.g. the [when] and [either] conditionals
/// or the layout constraints.
///
/// Drawables are consumable, i.e. the [Draw::draw] method receives an
/// owned `self` and does not return it, consuming the drawable.
///
/// To draw a thing multiple times, instead of explicitly constructing it
/// every time, implement the [View] trait instead, which will construct
/// a [Draw]able.
///
/// ```
/// use tengri::*;
/// struct MyWidget(bool);
/// impl Draw<Tui> for MyWidget {
/// fn draw (self, to: &mut Tui) -> Perhaps<XYWH<u16>> {
/// todo!("your draw logic")
/// }
/// }
/// ```
pub trait Draw<S: Screen> {
fn draw (self, to: &mut S) -> Drawn<S::Unit>;
fn layout (&self, area: XYWH<S::Unit>) -> Drawn<S::Unit> {
Ok(Some(area))
}
}
/// Emit a [Draw]able.
///
/// Speculative. How to avoid conflicts with [Draw] proper?
pub trait View<T: Screen> {
fn view (&self) -> impl Draw<T>;
}
impl<T: Screen> View<T> for () {
fn view (&self) -> impl Draw<T> {
()
}
}
/// Return a [Draw]able.
///
/// ```
/// # use tengri::*;
/// let _ = view::<Tui, _, _>(||"drawable");
/// let _ = view::<Tui, _, _>(||Some("drawable"));
/// ```
pub const fn view <S: Screen, T: Draw<S>, F: Fn()->T> (view: F) -> impl View<S> {
ViewThunk(view, PhantomData)
}
/// Because we can't implement [Draw] for `F: FnOnce...` without conflicts.
pub struct ViewThunk<S: Screen, F>(pub F, std::marker::PhantomData<S>);
impl<S: Screen, T: Draw<S>, F: Fn()->T> View<S> for ViewThunk<S, F> {
fn view (&self) -> impl Draw<S> {
self.0()
}
}
/// Because we can't implement [Draw] for `F: FnOnce...` without conflicts.
pub struct DrawThunk<S: Screen, F>(pub F, std::marker::PhantomData<S>);
impl<T: Screen, F: FnOnce(&mut T)->Perhaps<XYWH<T::Unit>>> Draw<T> for DrawThunk<T, F> {
fn draw (self, to: &mut T) -> Perhaps<XYWH<T::Unit>> {
(self.0)(to)
}
}
/// Basic [Draw]able closure.
///
/// ```
/// # use tengri::*;
/// let _ = draw(|to: &mut Tui|Ok(Some(to.1)));
/// ```
pub const fn draw <T: Screen, F: FnOnce(&mut T)->Perhaps<XYWH<T::Unit>>> (
item: F
) -> DrawThunk<T, F> {
DrawThunk(item, std::marker::PhantomData)
}
/// Only render when condition is true.
///
/// ```
/// # use tengri::*;
/// # fn test () -> impl Draw<Tui> {
/// when(true, "Yes")
/// # }
/// ```
pub const fn when <T: Screen> (condition: bool, item: impl Draw<T>) -> impl Draw<T> {
draw(move|to: &mut T|if condition { item.draw(to) } else { Ok(Default::default()) })
}
/// Render one thing if a condition is true and another false.
///
/// ```
/// # use tengri::*;
/// # fn test () -> impl Draw<Tui> {
/// either(true, "Yes", "No")
/// # }
/// ```
pub const fn either <T: Screen> (condition: bool, a: impl Draw<T>, b: impl Draw<T>) -> impl Draw<T> {
draw(move|to: &mut T|if condition { a.draw(to) } else { b.draw(to) })
}
pub type Drawn<U> = Perhaps<XYWH<U>>;
impl<S: Screen> Draw<S> for () {
fn draw (self, _: &mut S) -> Drawn<S::Unit> {
Ok(None)
}
}
impl_draw!(<S: Screen, D: Draw<S>,>|self: Option<D>, to: S|{
self.map(|it|it.draw(to)).transpose().map(Option::unwrap_or_default)
});
//impl<S: Screen, D: Draw<S>> Draw<S> for RwLock<D> {
//fn draw (self, __: &mut S) -> Drawn<S::Unit> {
//todo!()
//}
//}
//impl<T: Screen, D: Draw<T>> Draw<T> for Arc<D> {
//fn draw (self, __: &mut T) -> Perhaps<XYWH<T::Unit>> {
//todo!()
//}
//}
impl<T: Screen, V: View<T>> Draw<T> for &V {
fn draw (self, to: &mut T) -> Perhaps<XYWH<T::Unit>> {
self.view().draw(to)
}
}
pub trait Xy<N: Coord> {
fn x (&self) -> N;
fn y (&self) -> N;
}
pub trait Wh<N: Coord>: Wide<N> + Tall<N> {
fn wh (&self) -> [N;2];
}
pub trait Xywh<N: Coord>: Xy<N> + Wh<N> {
fn xywh (&self) -> XYWH<N> {
XYWH(self.x(), self.y(), self.w(), self.h())
}
}
pub trait Wide<N: Coord>: Xy<N> {
fn w (&self) -> N { N::zero() }
fn w_min (&self) -> N { self.w() }
fn w_max (&self) -> N { self.w() }
}
pub trait Tall<N: Coord> {
fn h (&self) -> N { N::zero() }
fn h_min (&self) -> N { self.h() }
fn h_max (&self) -> N { self.h() }
}
/// Point with size.
///
/// ```
/// # use tengri::*;
/// let xywh = XYWH(0u16, 0, 0, 0);
/// assert_eq!(XYWH(10u16, 10, 20, 20).center(), (20, 20));
/// ```
///
/// * [ ] TODO: origin field (determines at which corner/side is X0 Y0)
///
#[cfg_attr(test, derive(Arbitrary))] #[derive(Copy, Clone, Debug, Default, PartialEq)]
pub struct XYWH<N: Coord>(pub N, pub N, pub N, pub N);
impl<N: Coord> Xy<N> for XYWH<N> {
fn x (&self) -> N { self.0 }
fn y (&self) -> N { self.1 }
}
impl<N: Coord> Wide<N> for XYWH<N> { fn w (&self) -> N { self.2 } }
impl<N: Coord> Tall<N> for XYWH<N> { fn h (&self) -> N { self.3 } }
impl<N: Coord> XYWH<N> {
pub fn zero () -> Self {
Self(0.into(), 0.into(), 0.into(), 0.into())
}
pub fn center (&self) -> (N, N) {
let Self(x, y, w, h) = *self;
(x.plus(w/2.into()), y.plus(h/2.into()))
}
pub fn centered (&self) -> (N, N) {
let Self(x, y, w, h) = *self;
(x.minus(w/2.into()), y.minus(h/2.into()))
}
pub fn centered_x (&self, n: N) -> Self {
let Self(x, y, w, h) = *self;
let x_center = (x.plus(w / 2.into())).minus(n / 2.into());
let y_center = y.plus(h / 2.into());
XYWH(x_center, y_center, n, 1.into())
}
pub fn centered_y (&self, n: N) -> Self {
let Self(x, y, w, h) = *self;
let x_center = x.plus(w / 2.into());
let y_corner = (y.plus(h / 2.into())).minus(n / 2.into());
XYWH(x_center, y_corner, 1.into(), n)
}
pub fn centered_xy (&self, [n, m]: [N;2]) -> Self {
let Self(x, y, w, h) = *self;
let x_center = (x.plus(w / 2.into())).minus(n / 2.into());
let y_corner = (y.plus(h / 2.into())).minus(m / 2.into());
XYWH(x_center, y_corner, n, m)
}
pub fn split_half (&self, direction: &Split) -> (Self, Self) {
let XYWH(x, y, w, h) = self.xywh();
match direction {
South => (XYWH(x, y, w, h - h / 2.into()), XYWH(x, y + h / 2.into(), w, h / 2.into())),
East => (XYWH(x, y, w - w / 2.into(), h), XYWH(x + w / 2.into(), y, w / 2.into(), h)),
North => (XYWH(x, y + h / 2.into(), w, h - h / 2.into()), XYWH(x, y, w, h / 2.into())),
West => (XYWH(x + w / 2.into(), y, w - w / 2.into(), h), XYWH(x, y, w / 2.into(), h)),
Above | Below => (XYWH(x, y, w, h), XYWH(x, y, w, h))
}
}
}
impl From<&ratatui::prelude::Rect> for XYWH<u16> {
fn from (rect: &ratatui::prelude::Rect) -> Self {
Self(rect.x, rect.y, rect.width, rect.height)
}
}
impl<N: Coord, T: Wide<N> + Tall<N>> Wh<N> for T {
fn wh (&self) -> [N;2] {
[self.w(), self.h()]
}
}
impl<N: Coord, T: Xy<N> + Wh<N>> Xywh<N> for T {}
impl<N: Coord, T: Xywh<N>> Lrtb<N> for T {}
pub trait Lrtb<N: Coord>: Xywh<N> {
fn lrtb (&self) -> [N;4] {
// FIXME: factor origin
[self.x(), self.y(), self.x()+self.w(), self.y()+self.h()]
}
fn iter_x (&self) -> std::ops::Range<N> where Self: HasOrigin {
self.x_west()..self.x_east()
}
fn x_west (&self) -> N where Self: HasOrigin {
let w = self.w();
let a = self.origin();
let d = match a { NW|W|SW => 0.into(), N|X|C|Y|S => w/2.into(), NE|E|SE => w };
self.x().minus(d)
}
fn x_east (&self) -> N where Self: HasOrigin {
let w = self.w();
let a = self.origin();
let d = match a { NW|W|SW => w, N|X|C|Y|S => w/2.into(), NE|E|SE => 0.into() };
self.x().plus(d)
}
fn x_center (&self) -> N where Self: HasOrigin {
todo!()
}
fn iter_y (&self) -> std::ops::Range<N> where Self: HasOrigin {
self.y_north()..self.y_south()
}
fn y_north (&self) -> N where Self: HasOrigin {
let a = self.origin();
let h = self.h();
let d = match a { NW|N|NE => 0.into(), W|X|C|Y|E => h/2.into(), SW|S|SE => h };
self.y().minus(d)
}
fn y_south (&self) -> N where Self: HasOrigin {
let a = self.origin();
let h = self.h();
let d = match a { NW|N|NE => h, W|X|C|Y|E => h/2.into(), SW|S|SE => 0.into() };
self.y().plus(d)
}
fn y_center (&self) -> N where Self: HasOrigin {
todo!()
}
}
}
#[cfg(feature = "draw")] pub use self::coord::*;
#[cfg(feature = "draw")] mod coord {
use crate::*;
/// A numeric type that can be used as coordinate.
///
/// FIXME: Replace with `num` crate?
/// FIXME: Use AsRef/AsMut?
///
/// ```
/// use tengri::*;
/// let a: u16 = Coord::zero();
/// let b: u16 = a.plus(1);
/// let c: u16 = a.minus(2);
/// let d = a.atomic();
/// ```
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>
//+ std::iter::Step
{
/// Zero in own type.
fn zero () -> Self { 0.into() }
/// Addition.
fn plus (self, other: Self) -> Self;
/// Saturating subtraction.
fn minus (self, other: Self) -> Self { if self >= other { self - other } else { 0.into() } }
/// Convert to [AtomicUsize].
fn atomic (self) -> AtomicUsize { AtomicUsize::new(self.into()) }
}
/// TUI works in u16 coordinates.
impl Coord for u16 {
fn plus (self, other: Self) -> Self { self.saturating_add(other) }
}
}
#[cfg(feature = "draw")] pub use self::layout::*;
#[cfg(feature = "draw")] mod layout {
use crate::*;
impl<S: Screen, T: Draw<S>> Layout<S> for T {}
pub trait Layout<S: Screen>: Draw<S> + Sized {
fn full_w (self) -> impl Draw<S> {
Full::W(self)
}
fn full_h (self) -> impl Draw<S> {
Full::H(self)
}
fn full_wh (self) -> impl Draw<S> {
Full::WH(self)
}
/// (bsp/e (exact/w 10 "Hello") "World")
fn exact_w <N: Into<Option<S::Unit>>> (self, x: N) -> impl Draw<S> {
Exact::W(self, x.into())
}
/// (bsp/s (exact/h 10 "Hello") "World")
fn exact_h <N: Into<Option<S::Unit>>> (self, y: N) -> impl Draw<S> {
Exact::H(self, y.into())
}
/// (exact/wh 10 2 "Hello World")
fn exact_wh <N: Into<Option<S::Unit>>> (self, x: N, y: N) -> impl Draw<S> {
Exact::WH(self, x.into(), y.into())
}
fn min_w <N: Into<Option<S::Unit>>> (self, x: N) -> impl Draw<S> {
Min::W(self, x.into())
}
fn min_h <N: Into<Option<S::Unit>>> (self, y: N) -> impl Draw<S> {
Min::H(self, y.into())
}
fn min_wh <N: Into<Option<S::Unit>>> (self, x: N, y: N) -> impl Draw<S> {
Min::WH(self, x.into(), y.into())
}
fn max_w <N: Into<Option<S::Unit>>> (self, x: N) -> impl Draw<S> {
Max::W(self, x.into())
}
fn max_h <N: Into<Option<S::Unit>>> (self, y: N) -> impl Draw<S> {
Max::H(self, y.into())
}
fn max_wh <N: Into<Option<S::Unit>>> (self, x: N, y: N) -> impl Draw<S> {
Max::WH(self, x.into(), y.into())
}
fn pad_w <N: Into<Option<S::Unit>>> (self, x: N) -> impl Draw<S> {
Pad::W(self, x.into())
}
fn pad_h <N: Into<Option<S::Unit>>> (self, y: N) -> impl Draw<S> {
Pad::H(self, y.into())
}
fn pad_wh <N: Into<Option<S::Unit>>> (self, x: N, y: N) -> impl Draw<S> {
Pad::WH(self, x.into(), y.into())
}
fn pull_x <N: Into<Option<S::Unit>>> (self, x: N) -> impl Draw<S> {
Pull::X(self, x.into())
}
fn pull_y <N: Into<Option<S::Unit>>> (self, y: N) -> impl Draw<S> {
Pull::Y(self, y.into())
}
fn pull_xy <N: Into<Option<S::Unit>>> (self, x: N, y: N) -> impl Draw<S> {
Pull::XY(self, x.into(), y.into())
}
fn push_x <N: Into<Option<S::Unit>>> (self, x: N) -> impl Draw<S> {
Push::X(self, x.into())
}
fn push_y <N: Into<Option<S::Unit>>> (self, y: N) -> impl Draw<S> {
Push::Y(self, y.into())
}
fn push_xy <N: Into<Option<S::Unit>>> (self, x: N, y: N) -> impl Draw<S> {
Push::XY(self, x.into(), y.into())
}
fn align (self, azimuth: impl Into<Option<Azimuth>>) -> Align<Self> {
Align(azimuth.into(), self)
}
fn align_c (self) -> Align<Self> {
Align(Some(Azimuth::C), self)
}
fn align_x (self) -> Align<Self> {
Align(Some(Azimuth::X), self)
}
fn align_y (self) -> Align<Self> {
Align(Some(Azimuth::Y), self)
}
fn align_n (self) -> impl Draw<S> {
Align(Some(Azimuth::N), self)
}
fn align_s (self) -> impl Draw<S> {
Align(Some(Azimuth::S), self)
}
fn align_e (self) -> impl Draw<S> {
Align(Some(Azimuth::E), self)
}
fn align_w (self) -> impl Draw<S> {
Align(Some(Azimuth::W), self)
}
fn align_ne (self) -> impl Draw<S> {
Align(Some(Azimuth::NE), self)
}
fn align_se (self) -> impl Draw<S> {
Align(Some(Azimuth::SE), self)
}
fn align_nw (self) -> impl Draw<S> {
Align(Some(Azimuth::NW), self)
}
fn align_sw (self) -> impl Draw<S> {
Align(Some(Azimuth::SW), self)
}
fn origin (self, azimuth: impl Into<Option<Azimuth>>) -> impl Draw<S> {
Origin(azimuth.into(), self)
}
fn origin_c (self) -> impl Draw<S> {
Origin(Some(Azimuth::C), self)
}
fn origin_x (self) -> impl Draw<S> {
Origin(Some(Azimuth::X), self)
}
fn origin_y (self) -> impl Draw<S> {
Origin(Some(Azimuth::Y), self)
}
fn origin_n (self) -> impl Draw<S> {
Origin(Some(Azimuth::N), self)
}
fn origin_s (self) -> impl Draw<S> {
Origin(Some(Azimuth::S), self)
}
fn origin_e (self) -> impl Draw<S> {
Origin(Some(Azimuth::E), self)
}
fn origin_w (self) -> impl Draw<S> {
Origin(Some(Azimuth::W), self)
}
fn origin_ne (self) -> impl Draw<S> {
Origin(Some(Azimuth::NE), self)
}
fn origin_se (self) -> impl Draw<S> {
Origin(Some(Azimuth::SE), self)
}
fn origin_nw (self) -> impl Draw<S> {
Origin(Some(Azimuth::NW), self)
}
fn origin_sw (self) -> impl Draw<S> {
Origin(Some(Azimuth::SW), self)
}
}
/// Where is [0, 0] located?
///
/// ```
/// use tengri::*;
/// use Azimuth::*;
/// let _ = "".align(NW);
/// ```
#[cfg_attr(test, derive(Arbitrary))]
#[derive(Debug, Copy, Clone, Default)] pub enum Azimuth {
#[default] C, X, Y, NW, N, NE, E, SE, S, SW, W
}
/// Uses [AtomicUsize] to measure size during\
/// rendering (which is normally read-only).
#[derive(Default, Debug, Clone)]
pub struct Sizer(
/// Width
pub Arc<AtomicUsize>,
/// Height
pub Arc<AtomicUsize>,
);
impl Xy<u16> for Sizer {
fn x (&self) -> u16 {
self.0.load(Relaxed) as u16
}
fn y (&self) -> u16 {
self.1.load(Relaxed) as u16
}
}
impl Wide<u16> for Sizer {
fn w (&self) -> u16 {
self.0.load(Relaxed) as u16
}
}
impl Tall<u16> for Sizer {
fn h (&self) -> u16 {
self.1.load(Relaxed) as u16
}
}
impl PartialEq for Sizer {
fn eq (&self, _: &Self) -> bool { todo!() }
}
impl Sizer {
pub const fn of <T: Screen> (&self, of: impl Draw<T>) -> impl Draw<T> {
draw(move|to: &mut T|{
let area = of.draw(to)?;
self.0.store(area.map(|a|a.w()).unwrap_or(T::Unit::zero()).into(), Relaxed);
self.1.store(area.map(|a|a.h()).unwrap_or(T::Unit::zero()).into(), Relaxed);
Ok(area)
})
}
}
mod align; pub use self::align::*;
mod area; pub use self::area::*;
mod exact; pub use self::exact::*;
mod full; pub use self::full::*;
mod iter; pub use self::iter::*;
mod max; pub use self::max::*;
mod min; pub use self::min::*;
mod origin; pub use self::origin::*;
mod pad; pub use self::pad::*;
mod pull; pub use self::pull::*;
mod push; pub use self::push::*;
mod split; pub use self::split::*;
}
#[cfg(feature = "draw")] pub use self::color::*;
#[cfg(feature = "draw")] mod color {
use crate::*;
use dizzle::LanguageError::*;
use ::ratatui::style::Color;
use ::rand::distributions::uniform::UniformSampler;
pub(crate) use ::palette::{
Okhsl, Srgb, OklabHue, Mix, okhsl::UniformOkhsl,
convert::{FromColor, FromColorUnclamped}
};
pub fn rgb (r: u8, g: u8, b: u8) -> ItemColor {
let term = Color::Rgb(r, g, b);
ItemColor { okhsl: rgb_to_okhsl(term), term }
}
pub fn g (g: u8) -> Color {
Color::Rgb(g, g, g)
}
pub fn okhsl_to_rgb (color: Okhsl<f32>) -> Color {
let Srgb { red, green, blue, .. }: Srgb<f32> = Srgb::from_color_unclamped(color);
Color::Rgb((red * 255.0) as u8, (green * 255.0) as u8, (blue * 255.0) as u8,)
}
pub fn rgb_to_okhsl (color: Color) -> Okhsl<f32> {
if let Color::Rgb(r, g, b) = color {
Okhsl::from_color(Srgb::new(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0))
} else {
unreachable!("only Color::Rgb is supported")
}
}
pub trait HasColor { fn color (&self) -> ItemColor; }
#[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 }
}
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub struct ItemColor {
pub term: Color,
pub okhsl: Okhsl<f32>
}
impl_from!(ItemColor: |term: Color| Self { term, okhsl: rgb_to_okhsl(term) });
impl_from!(ItemColor: |okhsl: Okhsl<f32>| Self { okhsl, term: okhsl_to_rgb(okhsl) });
// A single color within item theme parameters, in OKHSL and RGB representations.
impl ItemColor {
#[cfg(feature = "term")] pub const fn from_tui (term: Color) -> Self {
Self { term, okhsl: Okhsl::new_const(OklabHue::new(0.0), 0.0, 0.0) }
}
pub fn random () -> Self {
let mut rng = ::rand::thread_rng();
let lo = Okhsl::new(-180.0, 0.01, 0.25);
let hi = Okhsl::new( 180.0, 0.9, 0.5);
UniformOkhsl::new(lo, hi).sample(&mut rng).into()
}
pub fn random_dark () -> Self {
let mut rng = ::rand::thread_rng();
let lo = Okhsl::new(-180.0, 0.025, 0.075);
let hi = Okhsl::new( 180.0, 0.5, 0.150);
UniformOkhsl::new(lo, hi).sample(&mut rng).into()
}
pub fn random_near (color: Self, distance: f32) -> Self {
color.mix(Self::random(), distance)
}
pub fn mix (&self, other: Self, distance: f32) -> Self {
if distance > 1.0 { panic!("color mixing takes distance between 0.0 and 1.0"); }
self.okhsl.mix(other.okhsl, distance).into()
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub struct ItemTheme {
pub base: ItemColor,
pub light: ItemColor,
pub lighter: ItemColor,
pub lightest: ItemColor,
pub dark: ItemColor,
pub darker: ItemColor,
pub darkest: ItemColor,
}
impl_from!(ItemTheme: |base: ItemColor| Self::from_item_color(base));
impl_from!(ItemTheme: |base: Color| Self::from_tui_color(base));
impl ItemTheme {
#[cfg(feature = "term")] pub const G: [Self;256] = {
let mut builder = dizzle::konst::array::ArrayBuilder::new();
while !builder.is_full() {
let index = builder.len() as u8;
let light = (index as f64 * 1.15) as u8;
let lighter = (index as f64 * 1.7) as u8;
let lightest = (index as f64 * 1.85) as u8;
let dark = (index as f64 * 0.9) as u8;
let darker = (index as f64 * 0.6) as u8;
let darkest = (index as f64 * 0.3) as u8;
builder.push(ItemTheme {
base: ItemColor::from_tui(Color::Rgb(index, index, index )),
light: ItemColor::from_tui(Color::Rgb(light, light, light, )),
lighter: ItemColor::from_tui(Color::Rgb(lighter, lighter, lighter, )),
lightest: ItemColor::from_tui(Color::Rgb(lightest, lightest, lightest, )),
dark: ItemColor::from_tui(Color::Rgb(dark, dark, dark, )),
darker: ItemColor::from_tui(Color::Rgb(darker, darker, darker, )),
darkest: ItemColor::from_tui(Color::Rgb(darkest, darkest, darkest, )),
});
}
builder.build()
};
pub fn random () -> Self { ItemColor::random().into() }
pub fn random_near (color: Self, distance: f32) -> Self {
color.base.mix(ItemColor::random(), distance).into()
}
pub const G00: Self = {
let color: ItemColor = ItemColor {
okhsl: Okhsl { hue: OklabHue::new(0.0), lightness: 0.0, saturation: 0.0 },
term: Color::Rgb(0, 0, 0)
};
Self {
base: color,
light: color,
lighter: color,
lightest: color,
dark: color,
darker: color,
darkest: color,
}
};
#[cfg(feature = "term")] pub fn from_tui_color (base: Color) -> Self {
Self::from_item_color(ItemColor::from_tui(base))
}
pub fn from_item_color (base: ItemColor) -> Self {
let mut light = base.okhsl;
light.lightness = (light.lightness * 1.3).min(1.0);
let mut lighter = light;
lighter.lightness = (lighter.lightness * 1.3).min(1.0);
let mut lightest = base.okhsl;
lightest.lightness = 0.95;
let mut dark = base.okhsl;
dark.lightness = (dark.lightness * 0.75).max(0.0);
dark.saturation = (dark.saturation * 0.75).max(0.0);
let mut darker = dark;
darker.lightness = (darker.lightness * 0.66).max(0.0);
darker.saturation = (darker.saturation * 0.66).max(0.0);
let mut darkest = darker;
darkest.lightness = 0.1;
darkest.saturation = (darkest.saturation * 0.50).max(0.0);
Self {
base,
light: light.into(),
lighter: lighter.into(),
lightest: lightest.into(),
dark: dark.into(),
darker: darker.into(),
darkest: darkest.into(),
}
}
}
pub trait ColorDsl<T>: Sized {
fn new_g (expr: T, try_to_u8: impl Fn(Perhaps<&str>)->Perhaps<u8>) -> Usually<Self>;
fn new_rgb (expr: T, try_to_u8: impl Fn(Perhaps<&str>)->Perhaps<u8>) -> Usually<Self>;
}
impl<T: Expression> ColorDsl<T> for Color {
fn new_g (expr: T, try_to_u8: impl Fn(Perhaps<&str>)->Perhaps<u8>) -> Usually<Self> {
let n = try_to_u8(expr.tail().map_err(Into::into))?.ok_or(Domain("not gray"))?;
Ok(Self::Rgb(n, n, n))
}
fn new_rgb (expr: T, try_to_u8: impl Fn(Perhaps<&str>)->Perhaps<u8>) -> Usually<Self> {
let r = try_to_u8(expr.tail().map_err(Into::into))?
.ok_or(Domain("not red"))?;
let g = try_to_u8(expr.tail().tail().head().map_err(Into::into))?
.ok_or(Domain("not green"))?;
let b = try_to_u8(expr.tail().tail().tail().head().map_err(Into::into))?
.ok_or(Domain("not blue"))?;
Ok(Color::Rgb(r, g, b))
}
}
}
#[cfg(feature = "draw")] pub use self::text::*;
#[cfg(feature = "text")] mod text {
#![allow(unused)]
pub(crate) use ::unicode_width::*;
/// Displays an owned [str]-like with fixed maximum width.
///
/// Width is computed using [unicode_width].
pub struct TrimString<T: AsRef<str>>(pub u16, pub T);
impl<T: AsRef<str>> AsRef<str> for TrimString<T> {
fn as_ref (&self) -> &str {
self.1.as_ref()
}
}
impl<'a, T: AsRef<str>> TrimString<T> {
fn to_ref (&self) -> TrimStr<'_, T> {
TrimStr(self.0, &self.1)
}
}
/// Displays a borrowed [str]-like with fixed maximum width
///
/// Width is computed using [unicode_width].
pub struct TrimStr<'a, T: AsRef<str>>(pub u16, pub &'a T);
impl<T: AsRef<str>> AsRef<str> for TrimStr<'_, T> {
fn as_ref (&self) -> &str {
self.1.as_ref()
}
}
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
}
/// 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()
}
}
#[cfg(feature = "term")] pub use self::term::*;
#[cfg(feature = "term")] mod term {
use crate::*;
use Color::*;
#[macro_export] macro_rules! tui_app {
($Struct:ident { $($fields:tt)* }) => {
#[dizzle::namespace(bool)]
#[dizzle::namespace(u8)]
#[dizzle::namespace(u16)]
#[dizzle::namespace(Option<u16>)]
#[dizzle::namespace(Color Tui::eval_color_expr)]
#[derive(Debug, Default)]
pub struct $Struct { $($fields)* }
tui_main!($Struct { ..Default::default() });
}
}
/// Implement standard [main] entrypoint for TUI apps.
#[macro_export] macro_rules! tui_main {
($state:expr) => {
pub fn main () -> Usually<()> {
tengri::Tui::setup_panic();
tengri::Tui::run_main(
::std::sync::Arc::new(::std::sync::RwLock::new($state))
)
}
}
}
/// Enable TUI output for state struct.
#[macro_export] macro_rules! tui_view {
($self:ident: $State:ty $body:block) => {
impl tengri::View<Tui> for $State {
fn view (&$self) -> impl tengri::Draw<tengri::Tui> $body
}
}
}
#[macro_export] macro_rules! tui_interpret {
($self:ident: $State:ident, $to:pat, $pat:ident -> $Result:ty { $($body:tt)+ }) => {
impl dizzle::Interpret<tengri::Tui, $Result> for $State {
fn interpret_word <'a> (
&'a $self, $to: &mut Tui, $pat: &'a impl dizzle::Symbol
) -> Usually<$Result> {
$($body)+
}
fn interpret_expr <'a> (
&'a self, to: &mut Tui, src: &'a impl tengri::Expression
) -> Usually<$Result> {
Ok(Some(if let Some(area) = tengri::eval_view(self, to, src)? {
area
} else if let Some(area) = tengri::Tui::eval_view(self, to, src)? {
area
} else {
return Err(format!("App::interpret_expr: unexpected: {src:?}").into())
}))
}
}
};
}
/// Enable TUI keyboard input for main state struct.
#[macro_export] macro_rules! tui_keys {
($self:ident:$State:ty,$input:ident $($body:tt)+) => {
impl dizzle::Apply<TuiEvent, Usually<()>> for $State {
fn apply (&mut $self, $input: &tengri::TuiEvent) -> Usually<()> $($body)+
}
};
}
//use unicode_width::{UnicodeWidthStr, UnicodeWidthChar};
//use rand::distributions::uniform::UniformSampler;
pub(crate) use ::{
std::{
io::{stdout, Write},
time::Duration,
ops::{Deref, DerefMut},
},
ratatui::{
prelude::{Style, Position, Backend, Color},
style::{Modifier},
backend::{CrosstermBackend, ClearType},
layout::{Size, Rect},
buffer::{Buffer, Cell},
crossterm::{
ExecutableCommand,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode},
//event::{poll, read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState},
}
},
crossterm::event::read,
};
impl Deref for Tui { type Target = Buffer; fn deref (&self) -> &Buffer { &self.0 } }
impl DerefMut for Tui { fn deref_mut (&mut self) -> &mut Buffer { &mut self.0 } }
impl AsMut<Buffer> for Tui { fn as_mut (&mut self) -> &mut Buffer { &mut self.0 } }
impl Wide<u16> for Tui { fn w (&self) -> u16 { self.1.2 } }
impl Tall<u16> for Tui { fn h (&self) -> u16 { self.1.3 } }
impl HasOrigin for Tui { fn origin (&self) -> Azimuth { Azimuth::NW } }
impl Xy<u16> for Tui { fn x (&self) -> u16 { self.1.0 } fn y (&self) -> u16 { self.1.1 } }
/// Terminal output.
pub struct Tui(
/// Ratatui buffer; area is screen size
pub Buffer,
/// Current draw area
pub XYWH<u16>
);
impl Tui {
pub fn setup_panic () {
use ::std::panic::{set_hook, PanicHookInfo};
use ::better_panic::{Settings, Verbosity};
let panic = Settings::auto()
.verbosity(Verbosity::Full)
.create_panic_handler();
set_hook(Box::new(move |info: &PanicHookInfo|{
let _ = Tui::teardown(&mut stdout());
panic(info);
}));
}
pub fn run_main <T> (state: Arc<RwLock<T>>) -> Usually<()> where
T: View<Tui> + Apply<TuiEvent, Usually<()>> + Send + Sync + 'static
{
Exit::run(|exit|{
let scan = Duration::from_millis(100);
let frame = Duration::from_millis(10);
let (_input, output) = Tui::io(exit.as_ref(), &state, scan, frame, std::io::stdout())?;
let _ = output.join();
Tui::teardown(&mut stdout())
})
}
/// Spawn the TUI input and output threadsl.
pub fn io <
T: View<Tui> + Apply<TuiEvent, Usually<()>> + Send + Sync + 'static,
W: Write + Send + Sync + 'static,
> (
exited: &Arc<AtomicBool>,
state: &Arc<RwLock<T>>,
poll: Duration,
sleep: Duration,
output: W,
) -> Result<(Task, Task), Box<dyn std::error::Error>> {
Ok((
Tui::input(exited, state, poll)?,
Tui::output(exited, state, sleep, output)?,
))
}
/// Spawn the TUI input thread which reads keys from the terminal.
pub fn input <T: Apply<TuiEvent, Usually<()>> + Send + Sync + 'static> (
exited: &Arc<AtomicBool>, state: &Arc<RwLock<T>>, poll: Duration
) -> Result<Task, std::io::Error> {
let exited = exited.clone();
let state = state.clone();
Task::new_poll(exited.clone(), poll, move |_| {
let event = read().unwrap();
if Exit::is(&event) {
exited.store(true, Relaxed);
} else if let Err(e) = state.write().unwrap().apply(&TuiEvent(event)) {
panic!("{e}")
}
})
}
pub fn teardown <W: Write> (backend: &mut W) -> Usually<()> {
use ::ratatui::backend::Backend;
stdout().execute(LeaveAlternateScreen)?;
CrosstermBackend::new(backend).show_cursor()?;
disable_raw_mode().map_err(Into::into)
}
pub fn new (width: u16, height: u16) -> Self {
Self(Buffer::empty(Rect { x: 0, y: 0, width, height }), XYWH(0, 0, width, height))
}
pub fn resize <W: Write> (&mut self, back: &mut CrosstermBackend<W>, width: u16, height: u16) {
let size = Rect { x: 0, y: 0, width, height };
if self.0.area != size {
back.clear_region(ClearType::All).unwrap();
self.0.resize(size);
self.0.reset();
}
}
pub fn redraw <'b, W: Write> (
&'b mut self,
back: &mut CrosstermBackend<W>,
mut next: &'b mut Self
) {
let updates = self.0.diff(&next.0);
back.draw(updates.into_iter()).expect("failed to render");
Backend::flush(back).expect("failed to flush output new");
std::mem::swap(self, &mut next);
next.0.reset();
}
pub fn update (&mut self, callback: &impl Fn(&mut Cell, u16, u16)) -> XYWH<u16> {
for row in 0..self.h() {
let y = self.y() + row;
for col in 0..self.w() {
let x = self.x() + col;
if x < self.0.area.width && y < self.0.area.height {
if let Some(cell) = self.0.cell_mut(Position { x, y }) {
callback(cell, col, row);
}
}
}
}
self.xywh()
}
pub fn blit (&mut self, text: &impl AsRef<str>, x: u16, y: u16, style: Option<Style>) {
let text = text.as_ref();
let style = style.unwrap_or(Style::default());
if x < self.0.area.width && y < self.0.area.height {
self.0.set_string(x, y, text, style);
}
}
pub fn tint_all (&mut self, fg: Color, bg: Color, modifier: Modifier) {
for cell in self.0.content.iter_mut() {
cell.fg = fg;
cell.bg = bg;
cell.modifier = modifier;
}
}
/// Spawn the TUI output thread which writes colored characters to the terminal.
///
/// ```
/// let state = std::sync::Arc::new(std::sync::RwLock::new(()));
/// let _ = tengri::Exit::run(|exit|{
/// tengri::Tui::output(
/// exit.as_ref(),
/// &state,
/// std::time::Duration::from_millis(10),
/// std::io::stdout()
/// )
/// });
/// ```
pub fn output <
W: Write + Send + Sync + 'static, T: View<Tui> + Send + Sync + 'static
> (
exited: &Arc<AtomicBool>,
state: &Arc<RwLock<T>>,
sleep: Duration,
output: W,
) -> Usually<Task> {
let state = state.clone();
stdout().execute(EnterAlternateScreen)?;
CrosstermBackend::new(stdout()).hide_cursor()?;
enable_raw_mode()?;
let mut backend = CrosstermBackend::new(output);
let Size { width, height } = backend.size().expect("get size failed");
let mut prev = Tui::new(width, height);
let mut next = Tui::new(width, height);
Ok(Task::new_sleep(exited.clone(), sleep, move |perf| {
let Size { width, height } = backend.size().expect("get size failed");
if let Ok(state) = state.try_read() {
prev.resize(&mut backend, width, height);
state.view().draw(&mut next).expect("draw failed"); // TODO draw error
prev.redraw(&mut backend, &mut next);
}
let timer = format!("{:>3.3}ms", perf.used.load(Relaxed));
prev.set_string(0, 0, &timer, Style::default());
})?)
}
/// Draw TUI content or its error message.
///
/// ```
/// for variant in [
/// Ok(Some("hello")),
/// Ok(None),
/// Err("fail".into()),
/// ] {
/// let _ = tengri::Tui::catcher(variant);
/// }
/// ```
pub fn catcher <T: Draw<Tui>> (result: Usually<T>) -> impl Draw<Tui> {
draw(move|to: &mut Tui|match result {
Ok(content) => content.draw(to),
Err(e) => {
let err_fg = Color::Rgb(255,224,244);
let err_bg = Color::Rgb(96, 24, 24);
let title = east(bold(true, "upsi daisy. "), "rendering failed.");
let error = east("\"why?\" ", bold(true, format!("{e}")));
fg(err_fg, bg(err_bg, south(title, error))).draw(to)
}
})
}
/// Interpret TUI-specific layout operation.
///
/// ```
/// use tengri::{*, dizzle::*, ratatui::prelude::Color};
///
/// #[namespace(bool)]
/// #[namespace(u8)]
/// #[namespace(u16)]
/// #[namespace(Color get_color)]
/// struct State;
///
/// impl Interpret<Tui, Option<XYWH<u16>>> for State {
/// fn interpret_expr <'a> (&'a self, _: &mut Tui, lang: &'a impl Expression)
/// -> Usually<Option<XYWH<u16>>>
/// {
/// Ok(None)
/// }
/// }
///
/// fn get_color (state: &State, src: impl Language) -> Perhaps<Color> {
/// Ok(if let Some(color) = Tui::eval_color_expr(state, &src)? {
/// Some(color)
/// } else if let Some(sym) = src.word()? {
/// Some(match sym {
/// ":color/bg" => Color::Rgb(28, 32, 36),
/// ":color/fg" => Color::Rgb(98, 92, 96),
/// _ => return Err(format!("not a color: {sym}").into())
/// })
/// } else {
/// return Err(format!("not a color: {:?}", src.src()?).into())
/// })
/// }
///
/// # fn main () -> tengri::Usually<()> {
/// let state = State;
/// let mut out = Tui::new(80, 25);
/// Tui::eval_view(&state, &mut out, "")?;
/// Tui::eval_view(&state, &mut out, "text Hello world!")?;
/// Tui::eval_view(&state, &mut out, "fg (g 0) (text Hello world!)")?;
/// Tui::eval_view(&state, &mut out, "bg (g 2) (text Hello world!)")?;
/// Tui::eval_view(&state, &mut out, "(bg (g 3) (fg (g 4) (text Hello world!)))")?;
/// # Ok(()) }
/// ```
pub fn eval_view <'a, S> (
state: &S, to: &mut Tui, expr: impl Expression + 'a
) -> Perhaps<XYWH<u16>> where
S: Interpret<Tui, Option<XYWH<u16>>>
+ 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();
match frags.next() {
Some("text") => {
if let Some(src) = args?.src()? {
src.draw(to)
} else {
return Ok(None)
}
},
Some("fg") => {
let arg0 = arg0?.expect("fg: expected arg 0 (color)");
if let Some(color) = Namespace::namespace(state, arg0)? {
fg(color, draw(move|to: &mut Tui|state.interpret(to, &arg1))).draw(to)
} else {
return Err(format!("fg: {arg0:?}: not a color").into())
}
},
Some("bg") => {
let arg0 = arg0?.expect("bg: expected arg 0 (color)");
if let Some(color) = Namespace::namespace(state, arg0)? {
bg(color, draw(move|to: &mut Tui|state.interpret(to, &arg1))).draw(to)
} else {
return Err(format!("bg: {arg0:?}: not a color").into())
}
},
_ => return Ok(None)
}
}
pub fn eval_color_expr (state: &impl for<'a> Namespace<'a, u8>, src: impl Language)
-> Perhaps<Color>
{
if let Some(expr) = src.expr()? {
match (expr.head()?, expr.tail()?) {
(Some("g"), Some(tail)) => {
let n: u8 = state.namespace(tail.head().map_err(Into::into))?.ok_or(LanguageError::Domain("not gray"))?;
Ok(Some(Color::Rgb(n, n, n)))
},
(Some("rgb"), Some(tail)) => {
let r: u8 = state.namespace(tail.head().map_err(Into::into))?
.ok_or(LanguageError::Domain("not red"))?;
let g: u8 = state.namespace(tail.tail().head().map_err(Into::into))?
.ok_or(LanguageError::Domain("not green"))?;
let b: u8 = state.namespace(tail.tail().tail().head().map_err(Into::into))?
.ok_or(LanguageError::Domain("not blue"))?;
Ok(Some(Color::Rgb(r, g, b)))
},
(Some(_), _) => return Err(format!("not a color expression: {expr}").into()),
(None, _) => return Err(format!("not a color expression: {expr}").into()),
}
} else {
Ok(None)
}
}
#[cfg(feature = "text")]
/// Write a line of text
///
/// TODO: do a paragraph (handle newlines)
pub fn text (&mut self, text: &impl AsRef<str>, x0: u16, y: u16, max_width: u16)
-> Perhaps<XYWH<u16>>
{
let text = text.as_ref();
let mut string_width: u16 = 0;
for character in text.chars() {
let x = x0 + string_width;
let character_width = character.width().unwrap_or(0) as u16;
string_width += character_width;
if string_width > max_width {
break
}
if let Some(cell) = self.0.cell_mut(ratatui::prelude::Position { x, y }) {
cell.set_char(character);
} else {
break
}
}
Ok(Some(XYWH(x0, y, string_width, 1)))
}
}
impl Screen for Tui {
type Unit = u16;
/// Render drawable in subarea specified by `area`
fn show (&mut self, content: impl Draw<Self>) -> Perhaps<XYWH<u16>> {
let previous_area = self.1;
Ok(if let Some(area) = content.layout(self.1)? {
self.1 = area;
if let Some(result_area) = content.draw(self)? {
self.1 = previous_area;
Some(result_area)
} else {
None
}
} else {
None
})
}
/// Get current clipping area
fn area (&self) -> XYWH<Self::Unit> {
self.1
}
fn clip <T> (
&mut self,
area: impl Into<Option<XYWH<u16>>>,
draw: impl FnOnce(&mut Self)->T
) -> T {
let prev = self.1;
if let Some(area) = area.into() {
self.1 = area.into();
}
let result = draw(self);
self.1 = prev;
result
}
}
pub const fn fill_char (c: char) -> impl Draw<Tui> {
draw(move|to: &mut Tui|Ok(Some(to.update(&|cell,_,_|{
cell.set_char(c);
}))))
}
/// Draw contents with modifier applied.
pub const fn modify (on: bool, modifier: Modifier, item: impl Draw<Tui>) -> impl Draw<Tui> {
draw(move|to: &mut Tui|{
fill_mod(on, modifier).draw(to)?;
item.draw(to)
})
}
pub const fn fill_mod (on: bool, modifier: Modifier) -> impl Draw<Tui> {
draw(move|to: &mut Tui|Ok(Some({
if on {
to.update(&|cell,_,_|cell.modifier.insert(modifier))
} else {
to.update(&|cell,_,_|cell.modifier.remove(modifier))
}
})))
}
/// Draw contents with bold modifier applied.
pub const fn bold (on: bool, item: impl Draw<Tui>) -> impl Draw<Tui> {
modify(on, Modifier::BOLD, item)
}
#[cfg(feature = "text")]
impl_draw!(|self: String, to: Tui|{
self.as_str().draw(to)
});
#[cfg(feature = "text")]
impl_draw!(|self: std::sync::Arc<str>, to: Tui|{
self.as_ref().draw(to)
});
#[cfg(feature = "text")]
impl_draw!(|self: &std::sync::Arc<str>, to: Tui|{
self.as_ref().draw(to)
});
#[cfg(feature = "text")]
impl_draw!(<T: AsRef<str>,>|self: TrimString<T>, to: Tui|{
self.as_ref().draw(to)
});
#[cfg(feature = "text")]
impl Draw<Tui> for &str {
fn layout (&self, area: XYWH<u16>) -> Perhaps<XYWH<u16>> {
layout_text_u16(self, area)
}
fn draw (self, to: &mut Tui) -> Drawn<u16> {
let area = self.layout(to.area())?.unwrap();
////let info = format!("{area:?}");
//to.text(&self, area.0, area.1, self.len() as u16)
for (index, line) in self.split("\n").enumerate() {
let _ = to.text(
&line, area.0, area.1 + index as u16, width_chars_max(area.2, line) as u16
)?;
}
Ok(Some(area))
}
}
#[cfg(feature = "text")]
impl<'t, T: AsRef<str>> Draw<Tui> for TrimStr<'_, T> {
fn layout (&self, area: XYWH<u16>) -> Perhaps<XYWH<u16>> {
layout_text_u16(self.as_ref(), area)
}
fn draw (self, to: &mut Tui) -> Drawn<u16> {
let XYWH(x, y, w, ..) = to.area();
let mut width: u16 = 1;
let mut chars = self.1.as_ref().chars();
while let Some(c) = chars.next() {
if width > self.0 || width > w {
break
}
let pos = Position { x: x + width - 1, y };
if let Some(cell) = to.0.cell_mut(pos) {
cell.set_char(c);
}
width += c.width().unwrap_or(0) as u16;
}
let XYWH(x, y, w, ..) = XYWH(
to.x(), to.y(), to.w().min(self.0).min(self.1.as_ref().width() as u16), to.h()
);
to.text(&self.as_ref(), x, y, w)
}
}
#[cfg(feature = "text")]
fn layout_text_u16 (text: &str, area: XYWH<u16>) -> Perhaps<XYWH<u16>> {
let XYWH(x, y, w, h) = area;
let mut max_w = 0u16;
let mut max_h = 0u16;
for line in text.split("\n") {
max_h += 1;
max_w = max_w.max(line.len() as u16);
}
Ok(Some(XYWH(x, y, w.min(max_w), h.min(max_h))))
}
pub struct ShowSize;
impl Draw<Tui> for ShowSize {
fn layout (&self, area: XYWH<u16>) -> Perhaps<XYWH<u16>> {
let info = format!("{area:?}");
Ok(Some(XYWH(area.0, area.1, info.len() as u16, 1)))
}
fn draw (self, to: &mut Tui) -> Drawn<u16> {
let area = to.area();
let info = format!("{area:?}");
to.text(&info, area.0, area.1, info.len() as u16)
}
}
pub struct ShowSizeOf<T>(pub T);
impl<T: Draw<Tui>> Draw<Tui> for ShowSizeOf<T> {
fn layout (&self, area: XYWH<u16>) -> Perhaps<XYWH<u16>> {
self.0.layout(area)
}
fn draw (self, to: &mut Tui) -> Drawn<u16> {
Ok(self.0.draw(to)?.map(|used|{
let _ = to.text(&format!("{used:?}"), used.0, used.1, u16::MAX);
used
}))
}
}
/// Apply foreground color.
pub const fn fg (fg: Color, item: impl Draw<Tui>) -> impl Draw<Tui> {
draw(move|to: &mut Tui|{
to.update(&|cell,_,_|{ cell.set_fg(fg); });
item.draw(to)
})
}
/// Apply background color.
pub const fn bg (bg: Color, item: impl Draw<Tui>) -> impl Draw<Tui> {
Background(bg, item)
}
pub struct Background<T>(Color, T);
impl<T: Draw<Tui>> Draw<Tui> for Background<T> {
fn layout (&self, area: XYWH<u16>) -> Drawn<u16> {
self.1.layout(area)
}
fn draw (self, to: &mut Tui) -> Drawn<u16> {
self.layout(to.area()).map(|area|to.clip(area, |to|{
to.update(&|cell,_,_|{ cell.set_bg(self.0); });
self.1.draw(to)
}))?
}
}
pub const fn fg_bg (fg: Color, bg: Color, item: impl Draw<Tui>) -> impl Draw<Tui> {
draw(move|to: &mut Tui|{
to.update(&|cell,_,_|{ cell.set_fg(fg); cell.set_bg(bg); });
item.draw(to)
})
}
pub const fn fill_ul (color: Option<Color>) -> impl Draw<Tui> {
draw(move|to: &mut Tui|Ok(Some(if let Some(color) = color {
to.update(&|cell,_,_|{
cell.modifier.insert(Modifier::UNDERLINED);
cell.underline_color = color;
})
} else {
to.update(&|cell,_,_|{
cell.modifier.remove(Modifier::UNDERLINED);
cell.underline_color = Reset;
})
})))
}
pub const fn tui_color_bg () -> Color { Color::Rgb(28, 35, 25) }
pub const fn tui_bg0 () -> Color { Color::Rgb(20, 20, 20) }
pub const fn tui_bo1 () -> Color { Color::Rgb(100, 110, 40) }
pub const fn tui_bo2 () -> Color { Color::Rgb(70, 80, 50) }
pub const fn tui_border_bg () -> Color { Color::Rgb(40, 50, 30) }
pub const fn tui_border_fg (f: bool) -> Color { if f { tui_bo1() } else { tui_bo2() } }
pub const fn tui_brown () -> Color { Color::Rgb(128,255,0) }
pub const fn tui_electric () -> Color { Color::Rgb(0,255,128) }
pub const fn tui_g (g: u8) -> Color { Color::Rgb(g, g, g) }
pub const fn tui_green () -> Color { Color::Rgb(0,255,0) }
pub const fn tui_mode_bg () -> Color { Color::Rgb(150, 160, 90) }
pub const fn tui_mode_fg () -> Color { Color::Rgb(255, 255, 255) }
pub const fn tui_null () -> Color { Color::Reset }
pub const fn tui_orange () -> Color { Color::Rgb(255,128,0) }
pub const fn tui_red () -> Color { Color::Rgb(255,0, 0) }
pub const fn tui_separator_fg (_: bool) -> Color { Color::Rgb(0, 0, 0) }
pub const fn tui_status_bar_bg () -> Color { Color::Rgb(28, 35, 25) }
pub const fn tui_ti1 () -> Color { Color::Rgb(150, 160, 90) }
pub const fn tui_ti2 () -> Color { Color::Rgb(120, 130, 100) }
pub const fn tui_title_fg (f: bool) -> Color { if f { tui_ti1() } else { tui_ti2() } }
pub const fn tui_yellow () -> Color { Color::Rgb(255,255,0) }
mod border; pub use self::border::*;
mod event; pub use self::event::*;
mod keys; pub use self::keys::*;
mod buffer; pub use self::buffer::*;
mod repeat; pub use self::repeat::*;
mod scroll; pub use self::scroll::*;
mod phat; pub use self::phat::*;
mod button; pub use self::button::*;
#[cfg(test)] mod test {
use crate::{*, term::*};
#[cfg(feature = "text")] #[test] fn test_layout_text_u16 () -> Usually<()> {
assert_eq!(layout_text_u16("foo", XYWH(5, 6, 10, 10))?, Some(XYWH(5, 6, 3, 1)));
assert_eq!(layout_text_u16("foo\nbarz", XYWH(5, 6, 10, 10))?, Some(XYWH(5, 6, 4, 2)));
Ok(())
}
}
}
#[cfg(feature = "eval")] pub use self::eval::*;
#[cfg(feature = "eval")] mod eval {
use crate::*;
/// Interpret layout operation.
///
/// ```
/// # use tengri::{*, dizzle::*};
///
/// struct State {/*app-specific*/}
/// impl<'b> Namespace<'b, u16> for State {}
/// impl<'b> Namespace<'b, bool> for State {}
/// impl<'b> Namespace<'b, Option<u16>> for State {}
/// impl Interpret<Tui, Option<XYWH<u16>>> for State {}
///
/// # fn main () -> tengri::Usually<()> {
/// let state = State {};
/// let mut target = Tui::new(80, 25);
/// eval_view(&state, &mut target, &"")?;
/// eval_view(&state, &mut target, &"(whe true (text hello))")?;
/// eval_view(&state, &mut target, &"(either true (text hello) (text world))")?;
/// // TODO test all
/// # Ok(()) }
/// ```
pub fn eval_view <'a, O: Screen + 'a, S> (
state: &S, output: &mut O, expr: &'a impl Expression
) -> Perhaps<XYWH<O::Unit>> where
S: Interpret<O, Option<XYWH<O::Unit>>>
+ for<'b> Namespace<'b, bool>
+ for<'b> Namespace<'b, O::Unit>
+ for<'b> Namespace<'b, Option<O::Unit>>
{
// First element of expression is name of the operation.
// These are quasi-namespaced using the 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
// Here we just reference them, so that they are in scope.
// Dereferencing them happens in the dispatch branch.
let args = expr.tail();
let arg0 = args.head();
let tail0 = args.tail();
let arg1 = tail0.head();
let tail1 = tail0.tail();
let arg2 = tail1.head();
// First `frags.next()` calls returns the namespace.
match frags.next() {
Some("when") => when(
state.namespace(arg0?)?.unwrap(),
draw(move|output: &mut O|{state.interpret(output, &arg1)})
).draw(output),
Some("either") => either(
state.namespace(arg0?)?.unwrap(),
draw(move|output: &mut O|{state.interpret(output, &arg1)}),
draw(move|output: &mut O|{state.interpret(output, &arg2)}),
).draw(output),
Some("bsp") => eval_enum!("bsp", output, state, frags.next(), arg0, Split {
"n" => North, "s" => South, "e" => East, "w" => West, "a" => Above, "b" => Below
}).stack(
draw(move|output: &mut O|{state.interpret(output, &arg0)}),
draw(move|output: &mut O|{state.interpret(output, &arg1)}),
).draw(output),
Some("align") => draw(move|output: &mut O|{state.interpret(output, &arg0)})
.align(eval_enum!("align", output, state, frags.next(), arg0, Azimuth {
"c" => C, "x" => X, "y" => Y,
"n" => N, "s" => S, "e" => E, "w" => W,
"nw" => NW, "sw" => SW, "ne" => NE, "se" => SE,
})).draw(output),
Some("exact") => eval_xy!(
"exact" => expr, head, output, state, frags.next(), exact_wh, exact_w, exact_h, arg0, arg1, arg2,
),
Some("fixed") => eval_xy!(
"fixed" => expr, head, output, state, frags.next(), exact_wh, exact_w, exact_h, arg0, arg1, arg2,
),
Some("min") => eval_xy!(
"min" => expr, head, output, state, frags.next(), min_wh, min_w, min_h, arg0, arg1, arg2,
),
Some("max") => eval_xy!(
"max" => expr, head, output, state, frags.next(), max_wh, max_w, max_h, arg0, arg1, arg2,
),
Some("push") => eval_xy!(
"push" => expr, head, output, state, frags.next(), push_xy, push_x, push_y, arg0, arg1, arg2,
),
Some("fill") => eval_xy!(
"fill" => expr, head, output, state, frags.next(), full_wh, full_w, full_h, arg0,
),
_ => return Ok(None)
}
}
fn invalid_variant <T> (
name: &str,
frag: impl Language,
expr: impl Language,
head: impl Language,
) -> Usually<T> {
unimplemented!(
"{name}/{frag:?} ({expr:?}) ({head:?}) ({:?})",
head.src()?.unwrap_or_default().split("/").next()
)
}
}