mirror of
https://codeberg.org/unspeaker/tengri.git
synced 2026-07-17 07:56:56 +02:00
Compare commits
25 commits
b44dc02f33
...
ddc53b23c2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddc53b23c2 | ||
|
|
13c886d9e0 | ||
|
|
09463649c6 | ||
|
|
1f60b43f61 | ||
|
|
bf16288884 | ||
|
|
0b9e9c0696 | ||
|
|
90ebae0f26 | ||
|
|
2ab871d10b | ||
|
|
347151f7fc | ||
|
|
4e6c62a7f1 | ||
|
|
53c9438f7d | ||
|
|
dd7091bda1 | ||
|
|
7613c65500 | ||
|
|
f7b05e95fa | ||
|
|
80b086603e | ||
|
|
cb382cf12e | ||
|
|
0273d2ac75 | ||
|
|
e0781571c4 | ||
|
|
9c5bfafb7a | ||
|
|
29035d0b36 | ||
|
|
2dc501c184 | ||
|
|
be14173126 | ||
|
|
145047b7ff | ||
|
|
42a1807c2b | ||
|
|
e074712459 |
35 changed files with 2349 additions and 1823 deletions
2
.gitmodules
vendored
2
.gitmodules
vendored
|
|
@ -3,4 +3,4 @@
|
|||
url = ssh://git@codeberg.org/unspeaker/dizzle.git
|
||||
[submodule "rust-jack"]
|
||||
path = rust-jack
|
||||
url = https://codeberg.org/unspeaker/rust-jack
|
||||
url = ssh://git@codeberg.org/unspeaker/rust-jack
|
||||
|
|
|
|||
0
PAPER.qmd
Normal file
0
PAPER.qmd
Normal file
2
dizzle
2
dizzle
|
|
@ -1 +1 @@
|
|||
Subproject commit e984fbb9fe8e588399744d50841d006454c16657
|
||||
Subproject commit af2a10751f87caef8e5526d1f692c941b8de8357
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
use ::{
|
||||
std::{io::stdout, sync::{Arc, RwLock}},
|
||||
tengri::{*, term::*, lang::*, keys::*, draw::*, space::*, dizzle::*},
|
||||
ratatui::style::Color,
|
||||
tengri::{*, lang::*},
|
||||
};
|
||||
|
||||
tui_main!(State {
|
||||
|
|
@ -10,7 +10,7 @@ tui_main!(State {
|
|||
});
|
||||
|
||||
tui_keys!(|self: State, input| {
|
||||
todo!()
|
||||
Ok(())
|
||||
});
|
||||
|
||||
tui_view!(|self: State| {
|
||||
|
|
@ -18,10 +18,10 @@ tui_view!(|self: State| {
|
|||
let wh = (self.size.w(), self.size.h());
|
||||
let src = VIEWS.get(self.cursor).unwrap_or(&"");
|
||||
let heading = format!("State {}/{} in {:?}", index, VIEWS.len(), &wh);
|
||||
let title = bg(Color::Rgb(60, 10, 10), y_push(1, align_n(heading)));
|
||||
let code = bg(Color::Rgb(10, 60, 10), y_push(2, align_n(format!("{}", src))));
|
||||
let title = bg(Color::Rgb(60, 10, 10), heading.align_n().push_y(1));
|
||||
let code = bg(Color::Rgb(10, 60, 10), format!("{}", src).align_n().push_y(2));
|
||||
//let content = ;//();//bg(Color::Rgb(10, 10, 60), View(self, CstIter::new(src)));
|
||||
let widget = thunk(move|to: &mut Tui|self.interpret(to, &src));
|
||||
let widget = thunk(move|to: &mut Tui|self.interpret(to, &src).map(Some));
|
||||
self.size.of(south(title, north(code, widget)))
|
||||
});
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ struct State {
|
|||
/** User-controllable value. */
|
||||
cursor: usize,
|
||||
/** Rendered window size. */
|
||||
size: crate::space::Size,
|
||||
size: Sizer,
|
||||
}
|
||||
|
||||
impl Interpret<Tui, XYWH<u16>> for State {
|
||||
|
|
|
|||
23
shell.nix
23
shell.nix
|
|
@ -1,13 +1,28 @@
|
|||
#!/usr/bin/env nix-shell
|
||||
{pkgs?import<nixpkgs>{}}:let
|
||||
stdenv = pkgs.clang19Stdenv;
|
||||
name = "tengri";
|
||||
nativeBuildInputs = [ pkgs.pkg-config pkgs.clang pkgs.libclang pkgs.mold pkgs.bacon ];
|
||||
buildInputs = [ pkgs.libclang pkgs.jack2 ];
|
||||
stdenv = pkgs.clang19Stdenv;
|
||||
LIBCLANG_PATH = "${pkgs.libclang.lib.outPath}/lib";
|
||||
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [];
|
||||
in pkgs.mkShell.override {
|
||||
inherit stdenv;
|
||||
} {
|
||||
inherit name nativeBuildInputs buildInputs LIBCLANG_PATH LD_LIBRARY_PATH;
|
||||
inherit name LIBCLANG_PATH LD_LIBRARY_PATH;
|
||||
buildInputs = [
|
||||
pkgs.libclang
|
||||
pkgs.jack2
|
||||
];
|
||||
nativeBuildInputs = [
|
||||
pkgs.pkg-config
|
||||
pkgs.clang
|
||||
pkgs.libclang
|
||||
pkgs.mold
|
||||
pkgs.bacon
|
||||
(pkgs.quarto.overrideAttrs (oldAttrs: { # https://github.com/NixOS/nixpkgs/issues/519484
|
||||
postPatch = (oldAttrs.postPatch or "") + ''
|
||||
substituteInPlace bin/quarto.js \
|
||||
--replace-fail "syntax-highlighting" "highlight-style"
|
||||
'';
|
||||
}))
|
||||
];
|
||||
}
|
||||
|
|
|
|||
189
src/draw.rs
189
src/draw.rs
|
|
@ -1,4 +1,53 @@
|
|||
use crate::{*, lang::*, color::*, space::*};
|
||||
use crate::*;
|
||||
|
||||
/// 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 <D: Draw<Self>> (&mut self, _: D) -> Drawn<u16> {
|
||||
/// println!("placed");
|
||||
/// Ok(None)
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// 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 <T: Draw<Self>> (&mut self, content: T) -> Perhaps<XYWH<Self::Unit>>;
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
|
|
@ -13,34 +62,45 @@ use crate::{*, lang::*, color::*, space::*};
|
|||
/// a [Draw]able.
|
||||
///
|
||||
/// ```
|
||||
/// use tengri::draw::*;
|
||||
/// struct TestScreen(bool);
|
||||
/// impl Screen for TestScreen { type Unit = u16; }
|
||||
/// struct TestWidget(bool);
|
||||
/// impl Draw<TestScreen> for TestWidget {
|
||||
/// fn draw (&self, screen: &mut T) -> Usually<XYWH<u16>> {
|
||||
/// screen.0 |= self.0;
|
||||
/// use tengri::*;
|
||||
/// struct MyWidget(bool);
|
||||
/// impl Draw<Tui> for MyWidget {
|
||||
/// fn draw (self, to: &mut Tui) -> Perhaps<XYWH<u16>> {
|
||||
/// todo!("your draw logic")
|
||||
/// }
|
||||
/// }
|
||||
/// let mut screen = TestScreen(false);
|
||||
/// TestWidget(false).draw(&mut screen);
|
||||
/// TestWidget(true).draw(&mut screen);
|
||||
/// TestWidget(false).draw(&mut screen);
|
||||
/// ```
|
||||
pub trait Draw<T: Screen> {
|
||||
fn draw (self, to: &mut T) -> Usually<XYWH<T::Unit>>;
|
||||
}
|
||||
impl<T: Screen, D: Draw<T>> Draw<T> for &D {
|
||||
fn draw (self, to: &mut T) -> Usually<XYWH<T::Unit>> {
|
||||
todo!()
|
||||
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))
|
||||
}
|
||||
}
|
||||
impl<T: Screen, D: Draw<T>> Draw<T> for Option<D> {
|
||||
fn draw (self, to: &mut T) -> Usually<XYWH<T::Unit>> {
|
||||
todo!()
|
||||
|
||||
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!()
|
||||
//}
|
||||
//}
|
||||
|
||||
/// Emit a [Draw]able.
|
||||
///
|
||||
/// Speculative. How to avoid conflicts with [Draw] proper?
|
||||
|
|
@ -48,74 +108,29 @@ pub trait View<T: Screen> {
|
|||
fn view (&self) -> impl Draw<T>;
|
||||
}
|
||||
|
||||
pub const fn thunk <T: Screen, F: FnOnce(&mut T)->Usually<XYWH<T::Unit>>> (draw: F) -> Thunk<T, F> {
|
||||
Thunk(draw, std::marker::PhantomData)
|
||||
}
|
||||
|
||||
/// Because we can't implement [Draw] for `F: FnOnce...` without conflicts.
|
||||
pub struct Thunk<T: Screen, F: FnOnce(&mut T)->Usually<XYWH<T::Unit>>>(
|
||||
pub F,
|
||||
std::marker::PhantomData<T>
|
||||
);
|
||||
impl<T: Screen, F: FnOnce(&mut T)->Usually<XYWH<T::Unit>>> Draw<T> for Thunk<T, F> {
|
||||
fn draw (self, to: &mut T) -> Usually<XYWH<T::Unit>> {
|
||||
(self.0)(to)
|
||||
impl<T: Screen> View<T> for () {
|
||||
fn view (&self) -> impl Draw<T> {
|
||||
()
|
||||
}
|
||||
}
|
||||
|
||||
/// Only render when condition is true.
|
||||
///
|
||||
/// ```
|
||||
/// # fn test () -> impl tengri::Draw<tengri::Tui> {
|
||||
/// tengri::when(true, "Yes")
|
||||
/// # }
|
||||
/// ```
|
||||
pub const fn when <T: Screen> (condition: bool, draw: impl Draw<T>) -> impl Draw<T> {
|
||||
thunk(move|to: &mut T|if condition { draw.draw(to) } else { Ok(Default::default()) })
|
||||
}
|
||||
|
||||
/// Render one thing if a condition is true and another false.
|
||||
///
|
||||
/// ```
|
||||
/// # fn test () -> impl tengri::Draw<tengri::Tui> {
|
||||
/// tengri::either(true, "Yes", "No")
|
||||
/// # }
|
||||
/// ```
|
||||
pub const fn either <T: Screen> (condition: bool, a: impl Draw<T>, b: impl Draw<T>) -> impl Draw<T> {
|
||||
thunk(move|to: &mut T|if condition { a.draw(to) } else { b.draw(to) })
|
||||
}
|
||||
|
||||
/// Output target.
|
||||
///
|
||||
/// ```
|
||||
/// use tengri::*;
|
||||
///
|
||||
/// struct TestOut(impl TwoD<u16>);
|
||||
///
|
||||
/// impl Screen for TestOut {
|
||||
/// type Unit = u16;
|
||||
/// fn place_at <T: Draw<Self> + ?Sized> (&mut self, area: impl TwoD<u16>, _: &T) {
|
||||
/// println!("placed: {area:?}");
|
||||
/// ()
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// impl Draw<Screen> for String {
|
||||
/// fn draw (&self, to: &mut TestOut) -> Usually<XYWH<u16>> {
|
||||
/// to.area_mut().set_w(self.len() as u16);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub trait Screen: Space<Self::Unit> + Send + Sync + Sized {
|
||||
type Unit: Coord;
|
||||
/// Render drawable in area.
|
||||
fn place <'t, T: Draw<Self> + ?Sized> (&mut self, content: &'t T) {
|
||||
self.place_at(self.xywh(), content)
|
||||
}
|
||||
/// Render drawable in subarea specified by `area`
|
||||
fn place_at <'t, T: Draw<Self> + ?Sized> (
|
||||
&mut self, area: XYWH<Self::Unit>, content: &'t T
|
||||
) {
|
||||
unimplemented!()
|
||||
impl<T: Screen, V: View<T>> Draw<T> for &V {
|
||||
fn draw (self, to: &mut T) -> Perhaps<XYWH<T::Unit>> {
|
||||
self.view().draw(to)
|
||||
}
|
||||
}
|
||||
|
||||
features! {
|
||||
"draw": [
|
||||
color,
|
||||
coord,
|
||||
iter,
|
||||
layout,
|
||||
lrtb,
|
||||
sizer,
|
||||
space,
|
||||
split,
|
||||
thunk,
|
||||
xywh
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
use ::ratatui::style::Color;
|
||||
use crate::lang::impl_from;
|
||||
use ::ratatui::style::Color;
|
||||
use ::rand::distributions::uniform::UniformSampler;
|
||||
pub(crate) use ::palette::{
|
||||
Okhsl, Srgb, OklabHue, Mix, okhsl::UniformOkhsl,
|
||||
convert::{FromColor, FromColorUnclamped}
|
||||
};
|
||||
use rand::distributions::uniform::UniformSampler;
|
||||
|
||||
pub fn rgb (r: u8, g: u8, b: u8) -> ItemColor {
|
||||
let term = Color::Rgb(r, g, b);
|
||||
|
|
@ -29,6 +29,7 @@ pub fn rgb_to_okhsl (color: Color) -> Okhsl<f32> {
|
|||
}
|
||||
|
||||
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),*>)? {
|
||||
|
|
@ -42,8 +43,11 @@ 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 {
|
||||
|
|
@ -80,8 +84,11 @@ pub struct ItemTheme {
|
|||
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();
|
||||
35
src/draw/coord.rs
Normal file
35
src/draw/coord.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
use super::*;
|
||||
|
||||
/// 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()) }
|
||||
}
|
||||
86
src/draw/iter.rs
Normal file
86
src/draw/iter.rs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
use super::*;
|
||||
|
||||
/// Iterate over a collection of the same kind of [Draw]able:
|
||||
pub fn iter <'a, S: Screen, D: 'a, I: Iterator<Item = D>, U: Draw<S>> (
|
||||
_iter: impl Fn()->I, _draw: impl Fn(D, usize)->U,
|
||||
) -> impl Draw<S> {
|
||||
thunk(move|_to: &mut S|{ todo!() })
|
||||
}
|
||||
|
||||
/// Iterate over a collection of the same kind of [Draw]able:
|
||||
pub fn iter_once <'a, S: Screen, D: 'a, U: Draw<S>> (
|
||||
_iter: impl Iterator<Item = D>, _draw: impl Fn(D, usize)->U,
|
||||
) -> impl Draw<S> {
|
||||
thunk(move|_to: &mut S|{ todo!() })
|
||||
}
|
||||
|
||||
/// Iterate over a collection of various [Draw]ables:
|
||||
pub fn iter_dyn <
|
||||
S: Screen, // Target screen
|
||||
D, // Input doesn't need to be [Draw]able, output does
|
||||
V: Fn()->I, // Function that returns the iterator
|
||||
I: Iterator<Item = D>, // Type of the iterator
|
||||
F: Fn(&D, usize)->dyn Draw<S>, // Function that returns [Draw]able from iterator item
|
||||
> (_items: V, _cb: F) -> impl Draw<S> {
|
||||
thunk(move|_to: &mut S|{ todo!() })
|
||||
}
|
||||
|
||||
pub fn iter_north <'a, S: Screen, D: 'a, I: Iterator<Item = D>, U: Draw<S>> (
|
||||
_iter: impl Fn()->I, _draw: impl Fn(D, usize)->U,
|
||||
) -> impl Draw<S> {
|
||||
thunk(move|_to: &mut S|{ todo!() })
|
||||
}
|
||||
|
||||
pub fn iter_east <'a, S: Screen, D: 'a, I: Iterator<Item = D>, U: Draw<S>> (
|
||||
_iter: impl Fn()->I, _draw: impl Fn(D, usize)->U,
|
||||
) -> impl Draw<S> {
|
||||
thunk(move|_to: &mut S|{ todo!() })
|
||||
}
|
||||
|
||||
pub fn iter_east_fixed <'a, S: Screen, D: 'a, I: Iterator<Item = D>, U: Draw<S>> (
|
||||
_height: S::Unit, _iter: impl Fn()->I, _draw: impl Fn(D, usize)->U,
|
||||
) -> impl Draw<S> {
|
||||
thunk(move|_to: &mut S|{ todo!() })
|
||||
}
|
||||
|
||||
pub fn iter_south <'a, S: Screen, D: 'a, I: Iterator<Item = D>, U: Draw<S>> (
|
||||
_iter: impl Fn()->I, _draw: impl Fn(D, usize)->U,
|
||||
) -> impl Draw<S> {
|
||||
thunk(move|_to: &mut S|{ todo!() })
|
||||
}
|
||||
|
||||
pub fn iter_south_fixed <'a, S: Screen, D: 'a, I: Iterator<Item = D>, U: Draw<S>> (
|
||||
_height: S::Unit, _iter: impl Fn()->I, _draw: impl Fn(D, usize)->U,
|
||||
) -> impl Draw<S> {
|
||||
thunk(move|_to: &mut S|{ todo!() })
|
||||
}
|
||||
|
||||
pub fn iter_west <'a, S: Screen, D: 'a, I: Iterator<Item = D>, U: Draw<S>> (
|
||||
_iter: impl Fn()->I, _draw: impl Fn(D, usize)->U,
|
||||
) -> impl Draw<S> {
|
||||
thunk(move|_to: &mut S|{ todo!() })
|
||||
}
|
||||
|
||||
pub fn row_south <S: Screen> (south: S::Unit, height: S::Unit, content: impl Draw<S>)
|
||||
-> impl Draw<S>
|
||||
{
|
||||
content.exact_h(height).push_y(south)
|
||||
}
|
||||
|
||||
pub fn row_north <S: Screen> (north: S::Unit, height: S::Unit, content: impl Draw<S>)
|
||||
-> impl Draw<S>
|
||||
{
|
||||
content.exact_h(height).pull_y(north)
|
||||
}
|
||||
|
||||
pub fn col_east <S: Screen> (east: S::Unit, width: S::Unit, content: impl Draw<S>)
|
||||
-> impl Draw<S>
|
||||
{
|
||||
content.exact_w(width).push_x(east)
|
||||
}
|
||||
|
||||
pub fn col_west <S: Screen> (west: S::Unit, width: S::Unit, content: impl Draw<S>)
|
||||
-> impl Draw<S>
|
||||
{
|
||||
content.exact_w(width).pull_x(west)
|
||||
}
|
||||
339
src/draw/layout.rs
Normal file
339
src/draw/layout.rs
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
use crate::*;
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
fn exact_w <N: Into<Option<S::Unit>>> (self, x: N) -> impl Draw<S> {
|
||||
Exact::W(self, x.into())
|
||||
}
|
||||
fn exact_h <N: Into<Option<S::Unit>>> (self, y: N) -> impl Draw<S> {
|
||||
Exact::H(self, y.into())
|
||||
}
|
||||
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::X(self, y.into())
|
||||
}
|
||||
fn pull_xy <N: Into<Option<S::Unit>>> (self, x: N, y: N) -> impl Draw<S> {
|
||||
Pull::XY(self, x.into(), y.into())
|
||||
}
|
||||
|
||||
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::X(self, y.into())
|
||||
}
|
||||
fn push_xy <N: Into<Option<S::Unit>>> (self, x: N, y: N) -> impl Draw<S> {
|
||||
Push::XY(self, x.into(), y.into())
|
||||
}
|
||||
|
||||
fn align (self, azimuth: impl Into<Option<Azimuth>>) -> impl Draw<S> {
|
||||
Align(azimuth.into(), self)
|
||||
}
|
||||
fn align_c (self) -> impl Draw<S> {
|
||||
Align(Some(Azimuth::C), self)
|
||||
}
|
||||
fn align_x (self) -> impl Draw<S> {
|
||||
Align(Some(Azimuth::X), self)
|
||||
}
|
||||
fn align_y (self) -> impl Draw<S> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Screen, T: Draw<S>> Layout<S> for T {}
|
||||
|
||||
/// Use whole drawing area along one or both axes.
|
||||
///
|
||||
/// ```
|
||||
/// use tengri::Layout;
|
||||
/// let _ = "".full_w();
|
||||
/// let _ = "".full_h();
|
||||
/// let _ = "".full_wh();
|
||||
/// ```
|
||||
pub enum Full<T: Screen, I: Draw<T>> {
|
||||
__(PhantomData<T>),
|
||||
W(I),
|
||||
H(I),
|
||||
WH(I),
|
||||
}
|
||||
|
||||
impl_draw!(<T: Screen, I: Draw<T>,>|self: Full<T, I>, _to: T|{
|
||||
todo!()
|
||||
});
|
||||
|
||||
/// Move content in the positive direction of one or both axes.
|
||||
///
|
||||
/// ```
|
||||
/// use tengri::Layout;
|
||||
/// let _ = "".push_x(1);
|
||||
/// let _ = "".push_y(1);
|
||||
/// let _ = "".push_xy(1, 1);
|
||||
/// ```
|
||||
pub enum Push<T: Screen, I: Draw<T>, X: Into<Option<T::Unit>>> {
|
||||
__(PhantomData<T>),
|
||||
X(I, X),
|
||||
Y(I, X),
|
||||
XY(I, X, X),
|
||||
}
|
||||
|
||||
impl_draw!(<T: Screen, I: Draw<T>, X: Into<Option<T::Unit>>,>|self: Push<T, I, X>, _to: T|{
|
||||
todo!()
|
||||
});
|
||||
|
||||
/// Move content in the negative direction of one or both axes.
|
||||
///
|
||||
/// ```
|
||||
/// use tengri::Layout;
|
||||
/// let _ = "".pull_x(1);
|
||||
/// let _ = "".pull_y(1);
|
||||
/// let _ = "".pull_xy(1, 1);
|
||||
/// ```
|
||||
pub enum Pull<T: Screen, I: Draw<T>, X: Into<Option<T::Unit>>> {
|
||||
__(PhantomData<T>),
|
||||
X(I, X),
|
||||
Y(I, X),
|
||||
XY(I, X, X),
|
||||
}
|
||||
|
||||
impl_draw!(<T: Screen, I: Draw<T>, X: Into<Option<T::Unit>>,>|self: Pull<T, I, X>, _to: T|{
|
||||
todo!()
|
||||
});
|
||||
|
||||
/// Only draw content if area is above a certain size.
|
||||
///
|
||||
/// ```
|
||||
/// use tengri::Layout;
|
||||
/// let _ = "".min_w(1);
|
||||
/// let _ = "".min_h(1);
|
||||
/// let _ = "".min_wh(1, 1);
|
||||
/// ```
|
||||
pub enum Min<T: Screen, I: Draw<T>, X: Into<Option<T::Unit>>> {
|
||||
__(PhantomData<T>),
|
||||
W(I, X),
|
||||
H(I, X),
|
||||
WH(I, X, X),
|
||||
}
|
||||
|
||||
impl_draw!(<T: Screen, I: Draw<T>, X: Into<Option<T::Unit>>,>|self: Min<T, I, X>, _to: T|{
|
||||
todo!()
|
||||
});
|
||||
|
||||
/// Set maximum size of of drawing area.
|
||||
///
|
||||
/// ```
|
||||
/// use tengri::Layout;
|
||||
/// let _ = "".max_w(1);
|
||||
/// let _ = "".max_h(1);
|
||||
/// let _ = "".max_wh(1, 1);
|
||||
/// ```
|
||||
pub enum Max<T: Screen, I: Draw<T>, X: Into<Option<T::Unit>>> {
|
||||
__(PhantomData<T>),
|
||||
W(I, X),
|
||||
H(I, X),
|
||||
WH(I, X, X),
|
||||
}
|
||||
|
||||
impl_draw!(<T: Screen, I: Draw<T>, X: Into<Option<T::Unit>>,>|self: Max<T, I, X>, _to: T|{
|
||||
todo!()
|
||||
});
|
||||
|
||||
/// Set size of of drawing area.
|
||||
///
|
||||
/// ```
|
||||
/// use tengri::Layout;
|
||||
/// let _ = "".exact_w(1);
|
||||
/// let _ = "".exact_h(1);
|
||||
/// let _ = "".exact_wh(1, 1);
|
||||
/// ```
|
||||
pub enum Exact<T: Screen, I: Draw<T>, X: Into<Option<T::Unit>>> {
|
||||
__(PhantomData<T>),
|
||||
W(I, X),
|
||||
H(I, X),
|
||||
WH(I, X, X),
|
||||
}
|
||||
|
||||
impl_draw!(<T: Screen, I: Draw<T>, X: Into<Option<T::Unit>>,>|self: Exact<T, I, X>, _to: T|{
|
||||
todo!()
|
||||
});
|
||||
|
||||
/// Define inner drawing area.
|
||||
///
|
||||
/// ```
|
||||
/// use tengri::Layout;
|
||||
/// let _ = "".pad_w(1);
|
||||
/// let _ = "".pad_h(1);
|
||||
/// let _ = "".pad_wh(1, 1);
|
||||
/// ```
|
||||
pub enum Pad<T: Screen, I: Draw<T>, X: Into<Option<T::Unit>>> {
|
||||
__(PhantomData<T>),
|
||||
W(I, X),
|
||||
H(I, X),
|
||||
WH(I, X, X),
|
||||
}
|
||||
|
||||
impl_draw!(<T: Screen, I: Draw<T>, X: Into<Option<T::Unit>>,>|self: Pad<T, I, X>, _to: T|{
|
||||
todo!()
|
||||
});
|
||||
|
||||
pub struct Align<T>(Option<Azimuth>, T);
|
||||
|
||||
impl_draw!(<S: Screen, T: Draw<S>,>|self: Align<T>, _to: S|{
|
||||
todo!()
|
||||
});
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// use tengri::*;
|
||||
/// let _ = area(None, "unareaed");
|
||||
/// let _ = area(XYWH(1, 2, 3, 4), "southeast");
|
||||
/// let _ = area(Some(XYWH(1, 2, 3, 4)), "southeast");
|
||||
/// ```
|
||||
pub fn area <S: Screen, T: Draw<S>, U: Into<Option<XYWH<S::Unit>>>> (
|
||||
origin: U, it: T
|
||||
) -> Area<S, T> {
|
||||
Area(origin.into(), it)
|
||||
}
|
||||
|
||||
pub struct Area<S: Screen, T: Draw<S>>(Option<XYWH<S::Unit>>, T);
|
||||
|
||||
impl_draw!(<S: Screen, T: Draw<S>,>|self: Area<S, T>, _to: S|{
|
||||
todo!()
|
||||
});
|
||||
|
||||
pub struct Origin<T>(Option<Azimuth>, T);
|
||||
|
||||
impl_draw!(<S: Screen, T: Draw<S>,>|self: Origin<T>, _to: S|{
|
||||
todo!()
|
||||
});
|
||||
|
||||
/// Something that has `[0, 0]` at a particular point.
|
||||
pub trait HasOrigin {
|
||||
fn origin (&self) -> Azimuth;
|
||||
}
|
||||
|
||||
impl<T: AsRef<Azimuth>> HasOrigin for T {
|
||||
fn origin (&self) -> Azimuth {
|
||||
*self.as_ref()
|
||||
}
|
||||
}
|
||||
47
src/draw/lrtb.rs
Normal file
47
src/draw/lrtb.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
use crate::*;
|
||||
use Azimuth::*;
|
||||
|
||||
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) -> impl Iterator<Item = 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) -> impl Iterator<Item = 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!()
|
||||
}
|
||||
}
|
||||
31
src/draw/sizer.rs
Normal file
31
src/draw/sizer.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
use super::*;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
/// 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(Ordering::Relaxed) as u16 }
|
||||
fn y (&self) -> u16 { self.1.load(Ordering::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> {
|
||||
thunk(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)
|
||||
})
|
||||
}
|
||||
}
|
||||
2
src/draw/space.rs
Normal file
2
src/draw/space.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
use crate::*;
|
||||
|
||||
143
src/draw/split.rs
Normal file
143
src/draw/split.rs
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
use super::*;
|
||||
use Azimuth::*;
|
||||
|
||||
pub const fn east <T: Screen> (a: impl Draw<T>, b: impl Draw<T>) -> impl Draw<T> {
|
||||
Split::East.half(a, b)
|
||||
}
|
||||
|
||||
pub const fn north <T: Screen> (a: impl Draw<T>, b: impl Draw<T>) -> impl Draw<T> {
|
||||
Split::North.half(a, b)
|
||||
}
|
||||
|
||||
pub const fn west <T: Screen> (a: impl Draw<T>, b: impl Draw<T>) -> impl Draw<T> {
|
||||
Split::West.half(a, b)
|
||||
}
|
||||
|
||||
pub const fn south <T: Screen> (a: impl Draw<T>, b: impl Draw<T>) -> impl Draw<T> {
|
||||
Split::South.half(a, b)
|
||||
}
|
||||
|
||||
pub const fn above <T: Screen> (a: impl Draw<T>, b: impl Draw<T>) -> impl Draw<T> {
|
||||
Split::Above.half(a, b)
|
||||
}
|
||||
|
||||
pub const fn below <T: Screen> (a: impl Draw<T>, b: impl Draw<T>) -> impl Draw<T> {
|
||||
Split::Below.half(a, b)
|
||||
}
|
||||
|
||||
/// Split along an axis. Direction determines order.
|
||||
#[cfg_attr(test, derive(Arbitrary))]
|
||||
#[derive(Copy, Clone, PartialEq, Debug, Default)] pub enum Split {
|
||||
North,
|
||||
South,
|
||||
East,
|
||||
West,
|
||||
Above,
|
||||
#[default] Below
|
||||
}
|
||||
|
||||
impl Split {
|
||||
|
||||
/// ```
|
||||
/// use tengri::*;
|
||||
/// let _ = Split::Above.half("", "");
|
||||
/// let _ = Split::Below.half("", "");
|
||||
/// let _ = Split::North.half("", "");
|
||||
/// let _ = Split::South.half("", "");
|
||||
/// let _ = Split::East.half("", "");
|
||||
/// let _ = Split::West.half("", "");
|
||||
/// ```
|
||||
pub const fn half <T: Screen> (&self, a: impl Draw<T>, b: impl Draw<T>) -> impl Draw<T> {
|
||||
thunk(move|to: &mut T|{
|
||||
let (area_a, area_b) = to.xywh().split_half(self);
|
||||
let (origin_a, origin_b) = self.origins();
|
||||
let a = a.align(origin_a);
|
||||
let b = b.align(origin_b);
|
||||
match self {
|
||||
Self::Below => {
|
||||
to.show(area(area_b, b))?;
|
||||
to.show(area(area_b, a))?;
|
||||
},
|
||||
_ => {
|
||||
to.show(area(area_a, a))?;
|
||||
to.show(area(area_b, b))?;
|
||||
}
|
||||
}
|
||||
Ok(Some(to.xywh())) // FIXME: compute and return actually used area
|
||||
})
|
||||
}
|
||||
|
||||
/// Newly split areas begin at the center of the split
|
||||
/// to maintain centeredness in the user's field of view.
|
||||
///
|
||||
/// Use [align] to override that and always start
|
||||
/// at the top, bottom, etc.
|
||||
///
|
||||
/// ```
|
||||
/// /*
|
||||
///
|
||||
/// Split east: Split south:
|
||||
/// | | | | A |
|
||||
/// | <-A|B-> | |---------|
|
||||
/// | | | | B |
|
||||
///
|
||||
/// */
|
||||
/// ```
|
||||
const fn origins (&self) -> (Azimuth, Azimuth) {
|
||||
use Azimuth::*;
|
||||
match self {
|
||||
Self::South => (S, N),
|
||||
Self::East => (E, W),
|
||||
Self::North => (N, S),
|
||||
Self::West => (W, E),
|
||||
Self::Above => (C, C),
|
||||
Self::Below => (C, C),
|
||||
}
|
||||
}
|
||||
|
||||
///// ```
|
||||
///// use tengri::*;
|
||||
///// let _ = Split::Below.iter([
|
||||
///// "Leftbar"
|
||||
///// .min_w(10).max_w(15).align(Azimuth::NW),
|
||||
///// "Rightbar"
|
||||
///// .min_w(10).max_w(12).align(Azimuth::NE),
|
||||
///// "Center"
|
||||
///// .min_w(20).max_w(40).align(Azimuth::C),
|
||||
///// ].iter());
|
||||
///// ```
|
||||
//pub fn iter <S: Screen, T: Draw<S>> (&self, _: impl Iterator<Item = T>) {
|
||||
//todo!()
|
||||
//}
|
||||
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! north {
|
||||
($head:expr $(,)?) => { $head };
|
||||
($head:expr, $($tail:expr),* $(,)?) => { north($head, north!($($tail,)*)) };
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! south {
|
||||
($head:expr $(,)?) => { $head };
|
||||
($head:expr, $($tail:expr),* $(,)?) => { south($head, south!($($tail,)*)) };
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! east {
|
||||
($head:expr $(,)?) => { $head };
|
||||
($head:expr, $($tail:expr),* $(,)?) => { east($head, east!($($tail,)*)) };
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! west {
|
||||
($head:expr $(,)?) => { $head };
|
||||
($head:expr $(, $tail:expr)* $(,)?) => { west($head, west!($($tail,)*)) };
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! above {
|
||||
($head:expr $(,)?) => { $head };
|
||||
($head:expr $(, $tail:expr)* $(,)?) => { above($head, above!($($tail,)*)) };
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! below {
|
||||
($head:expr $(,)?) => { $head };
|
||||
($head:expr, $($tail:expr),* $(,)?) => { below($head, below!($($tail,)*)) };
|
||||
}
|
||||
48
src/draw/thunk.rs
Normal file
48
src/draw/thunk.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
use crate::*;
|
||||
|
||||
/// Because we can't implement [Draw] for `F: FnOnce...` without conflicts.
|
||||
pub struct Thunk<T: Screen, F>(pub F, std::marker::PhantomData<T>);
|
||||
|
||||
impl<T: Screen, F: FnOnce(&mut T)->Perhaps<XYWH<T::Unit>>> Draw<T> for Thunk<T, F> {
|
||||
fn draw (self, to: &mut T) -> Perhaps<XYWH<T::Unit>> {
|
||||
(self.0)(to)
|
||||
}
|
||||
}
|
||||
|
||||
/// Basic [Draw]able closure.
|
||||
///
|
||||
/// ```
|
||||
/// # use tengri::*;
|
||||
/// # fn test () -> impl Draw<Tui> {
|
||||
/// thunk(|to: &mut Tui|Ok(Some(to.1)))
|
||||
/// # }
|
||||
/// ```
|
||||
pub const fn thunk <T: Screen, F: FnOnce(&mut T)->Perhaps<XYWH<T::Unit>>> (
|
||||
draw: F
|
||||
) -> Thunk<T, F> {
|
||||
Thunk(draw, 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, draw: impl Draw<T>) -> impl Draw<T> {
|
||||
thunk(move|to: &mut T|if condition { draw.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> {
|
||||
thunk(move|to: &mut T|if condition { a.draw(to) } else { b.draw(to) })
|
||||
}
|
||||
115
src/draw/xywh.rs
Normal file
115
src/draw/xywh.rs
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
use super::*;
|
||||
use Split::*;
|
||||
|
||||
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 {}
|
||||
339
src/eval.rs
339
src/eval.rs
|
|
@ -1,183 +1,229 @@
|
|||
use crate::{*, term::*, draw::*, ratatui::prelude::*};
|
||||
use dizzle::*;
|
||||
use crate::{*, lang::*};
|
||||
use ratatui::style::Color;
|
||||
|
||||
/// 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 ((
|
||||
$name:expr, $expr:expr, $head:expr, $output:ident, $state:ident,
|
||||
$variant:expr, $arg0: ident, $arg1: ident, $arg2: ident,
|
||||
$xy:ident, $x:ident, $y:ident
|
||||
) => {{
|
||||
let variant = $variant;
|
||||
let cb = thunk(move|output: &mut O|$state.interpret(output, &match variant {
|
||||
Some("x") | Some("y") => $arg1,
|
||||
Some("xy") | None => $arg2,
|
||||
_ => panic!("{}: unsupported axis {variant:?}; try /x, /y, /xy", $name)
|
||||
}));
|
||||
|
||||
match variant {
|
||||
|
||||
// XY variant (can be omitted)
|
||||
Some("xy") | None => cb.push_xy(
|
||||
$state.namespace($arg0?)?,
|
||||
$state.namespace($arg1?)?,
|
||||
).draw($output),
|
||||
|
||||
// X variant
|
||||
Some("x") => cb.push_x(
|
||||
$state.namespace($arg0?)?,
|
||||
).draw($output),
|
||||
|
||||
// Y variant
|
||||
Some("y") => cb.push_y(
|
||||
$state.namespace($arg0?)?,
|
||||
).draw($output),
|
||||
|
||||
// Other namespace members are invalid
|
||||
frag => {
|
||||
let name = $name;
|
||||
let expr = $expr;
|
||||
let head = $head;
|
||||
unimplemented!(
|
||||
"{name}/{frag:?} ({expr:?}) ({head:?}) ({:?})",
|
||||
head.src()?.unwrap_or_default().split("/").next()
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}});
|
||||
|
||||
/// 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)
|
||||
}
|
||||
}});
|
||||
|
||||
/// 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) {}
|
||||
/// }
|
||||
/// # use tengri::{*, lang::*};
|
||||
///
|
||||
/// struct State {/*app-specific*/}
|
||||
/// impl<'b> Namespace<'b, bool> for State {}
|
||||
/// impl<'b> Namespace<'b, u16> for State {}
|
||||
/// impl Interpret<Target, ()> 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 = Target { xywh: Default::default() };
|
||||
/// let mut target = Tui::new(80, 25);
|
||||
/// eval_view(&state, &mut target, &"")?;
|
||||
/// eval_view(&state, &mut target, &"(when true (text hello))")?;
|
||||
/// eval_view(&state, &mut target, &"(whe 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> (
|
||||
pub fn eval_view <'a, O: Screen + 'a, S> (
|
||||
state: &S, output: &mut O, expr: &'a impl Expression
|
||||
) -> Usually<bool> where
|
||||
S: Interpret<O, ()>
|
||||
+ for<'b>Namespace<'b, bool>
|
||||
+ for<'b>Namespace<'b, O::Unit>
|
||||
) -> 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 used for dispatch.
|
||||
// Dispatch is proto-namespaced using separator character
|
||||
// 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();
|
||||
// And we also have to do the above binding dance
|
||||
// so that the Perhaps<token>s remain in scope.
|
||||
|
||||
// First `frags.next()` calls returns the namespace.
|
||||
match frags.next() {
|
||||
|
||||
Some("when") => output.place(&when(
|
||||
Some("when") => when(
|
||||
state.namespace(arg0?)?.unwrap(),
|
||||
move|output: &mut O|state.interpret(output, &arg1)
|
||||
)),
|
||||
thunk(move|output: &mut O|{state.interpret(output, &arg1)})
|
||||
).draw(output),
|
||||
|
||||
Some("either") => output.place(&either(
|
||||
Some("either") => either(
|
||||
state.namespace(arg0?)?.unwrap(),
|
||||
move|output: &mut O|state.interpret(output, &arg1),
|
||||
move|output: &mut O|state.interpret(output, &arg2),
|
||||
)),
|
||||
thunk(move|output: &mut O|{state.interpret(output, &arg1)}),
|
||||
thunk(move|output: &mut O|{state.interpret(output, &arg2)}),
|
||||
).draw(output),
|
||||
|
||||
Some("bsp") => output.place(&{
|
||||
let a = move|output: &mut O|state.interpret(output, &arg0);
|
||||
let b = move|output: &mut O|state.interpret(output, &arg1);
|
||||
bsp(match frags.next() {
|
||||
Some("n") => Alignment::N,
|
||||
Some("s") => Alignment::S,
|
||||
Some("e") => Alignment::E,
|
||||
Some("w") => Alignment::W,
|
||||
Some("a") => Alignment::A,
|
||||
Some("b") => Alignment::B,
|
||||
frag => unimplemented!("bsp/{frag:?}")
|
||||
}, a, b)
|
||||
}),
|
||||
// Second `frags.next()` calls returns the namespace member.
|
||||
Some("bsp") => {
|
||||
eval_enum!("bsp", output, state, frags.next(), arg0, Split {
|
||||
"n" => North,
|
||||
"s" => South,
|
||||
"e" => East,
|
||||
"w" => West,
|
||||
"a" => Above,
|
||||
"b" => Below
|
||||
}).half(
|
||||
thunk(move|output: &mut O|{state.interpret(output, &arg0)}),
|
||||
thunk(move|output: &mut O|{state.interpret(output, &arg1)}),
|
||||
).draw(output)
|
||||
},
|
||||
|
||||
Some("align") => output.place(&{
|
||||
let a = move|output: &mut O|state.interpret(output, &arg0).unwrap();
|
||||
align(match frags.next() {
|
||||
Some("c") => Alignment::Center,
|
||||
Some("n") => Alignment::N,
|
||||
Some("s") => Alignment::S,
|
||||
Some("e") => Alignment::E,
|
||||
Some("w") => Alignment::W,
|
||||
Some("x") => Alignment::X,
|
||||
Some("y") => Alignment::Y,
|
||||
frag => unimplemented!("align/{frag:?}")
|
||||
}, a)
|
||||
}),
|
||||
Some("align") => {
|
||||
let content = thunk(move|output: &mut O|{state.interpret(output, &arg0)});
|
||||
content.align(eval_enum!("align", output, state, frags.next(), arg0, Azimuth {
|
||||
"c" => C,
|
||||
"n" => N,
|
||||
"s" => S,
|
||||
"e" => E,
|
||||
"w" => W,
|
||||
"x" => X,
|
||||
"y" => Y
|
||||
})).draw(output)
|
||||
},
|
||||
|
||||
Some("exact") => eval_xy!(
|
||||
"exact", expr, head, output, state, frags.next(),
|
||||
arg0, arg1, arg2, wh_exact, w_exact, h_exact
|
||||
),
|
||||
|
||||
Some("min") => eval_xy!(
|
||||
"min", expr, head, output, state, frags.next(),
|
||||
arg0, arg1, arg2, wh_min, w_min, h_min
|
||||
),
|
||||
|
||||
Some("max") => eval_xy!(
|
||||
"max", expr, head, output, state, frags.next(),
|
||||
arg0, arg1, arg2, wh_max, w_max, h_max
|
||||
),
|
||||
|
||||
Some("push") => eval_xy!(
|
||||
"push", expr, head, output, state, frags.next(),
|
||||
arg0, arg1, arg2, xy_push, x_push, y_push
|
||||
),
|
||||
|
||||
_ => return Ok(None)
|
||||
|
||||
Some("fill") => output.place(&{
|
||||
let a = move|output: &mut O|state.interpret(output, &arg0).unwrap();
|
||||
match frags.next() {
|
||||
Some("xy") | None => fill_wh(a),
|
||||
Some("x") => fill_w(a),
|
||||
Some("y") => fill_h(a),
|
||||
frag => unimplemented!("fill/{frag:?}")
|
||||
}
|
||||
}),
|
||||
|
||||
Some("exact") => output.place(&{
|
||||
let axis = frags.next();
|
||||
let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("exact: unsupported axis {axis:?}") };
|
||||
let cb = move|output: &mut O|state.interpret(output, &arg).unwrap();
|
||||
match axis {
|
||||
Some("xy") | None => exact_wh(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb),
|
||||
Some("x") => exact_w(state.namespace(arg0?)?.unwrap(), cb),
|
||||
Some("y") => exact_h(state.namespace(arg0?)?.unwrap(), cb),
|
||||
frag => unimplemented!("exact/{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!("min: unsupported axis {axis:?}") };
|
||||
let cb = move|output: &mut O|state.interpret(output, &arg).unwrap();
|
||||
match axis {
|
||||
Some("xy") | None => min_wh(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb),
|
||||
Some("x") => min_w(state.namespace(arg0?)?.unwrap(), cb),
|
||||
Some("y") => min_h(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!("max: unsupported axis {axis:?}") };
|
||||
let cb = move|output: &mut O|state.interpret(output, &arg).unwrap();
|
||||
match axis {
|
||||
Some("xy") | None => max_wh(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb),
|
||||
Some("x") => max_w(state.namespace(arg0?)?.unwrap(), cb),
|
||||
Some("y") => max_h(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!("push: unsupported axis {axis:?}") };
|
||||
let cb = move|output: &mut O|state.interpret(output, &arg);
|
||||
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)
|
||||
}
|
||||
|
||||
/// Interpret TUI-specific layout operation.
|
||||
///
|
||||
/// ```
|
||||
/// use tengri::{Namespace, Interpret, Tui, ratatui::prelude::Color};
|
||||
/// use tengri::{*, lang::*, ratatui::prelude::Color};
|
||||
///
|
||||
/// #[namespace(bool {})]
|
||||
/// #[namespace(u8: try_to_u8)]
|
||||
/// #[namespace(u16: try_to_u16)]
|
||||
/// #[namespace(Color: try_to_color)]
|
||||
/// #[interpret(Tui -> Option<XYWH<u16>>: try_eval_tui)]
|
||||
/// struct State;
|
||||
/// impl<'b> Namespace<'b, bool> for State {}
|
||||
/// impl<'b> Namespace<'b, u16> for State {}
|
||||
/// impl<'b> Namespace<'b, Color> for State {}
|
||||
/// impl Interpret<Tui, ()> for State {}
|
||||
/// tengri::lang::primitive!(u8: try_to_u8);
|
||||
/// tengri::lang::primitive!(u16: try_to_u16);
|
||||
/// tengri::lang::namespace!(State: bool {
|
||||
/// });
|
||||
/// tengri::lang::namespace!(State: u8 {
|
||||
/// literal = |x|try_to_u8(x);
|
||||
/// });
|
||||
/// tengri::lang::namespace!(State: u16 {
|
||||
/// literal = |x|try_to_u16(x);
|
||||
/// });
|
||||
/// tengri::lang::namespace!(State: Color {
|
||||
/// expression = |_state| {
|
||||
/// "g" (x: u8) => { ItemTheme::G[x as usize].base.term }
|
||||
/// };
|
||||
/// });
|
||||
/// tengri::lang::interpret!(|self: State, context: Tui, lang|->Option<XYWH<u16>>{
|
||||
/// expression = {
|
||||
/// "text" (...rest) => { todo!() }
|
||||
/// }
|
||||
/// });
|
||||
/// 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 main () -> tengri::Usually<()> {
|
||||
/// let state = State;
|
||||
/// let mut out = Tui::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!)))")?;
|
||||
/// let mut out = Tui::new(80, 25);
|
||||
/// eval_view_tui(&state, &mut out, "")?;
|
||||
/// eval_view_tui(&state, &mut out, "text Hello world!")?;
|
||||
/// eval_view_tui(&state, &mut out, "fg (g 0) (text Hello world!)")?;
|
||||
/// eval_view_tui(&state, &mut out, "bg (g 2) (text Hello world!)")?;
|
||||
/// 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 Tui, expr: impl Expression + 'a
|
||||
) -> Usually<bool> where
|
||||
S: Interpret<Tui, ()>
|
||||
) -> 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>
|
||||
|
|
@ -189,36 +235,43 @@ pub fn eval_view_tui <'a, S> (
|
|||
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) }
|
||||
if let Some(src) = args?.src()? {
|
||||
output.show(src)
|
||||
} else {
|
||||
return Ok(None)
|
||||
}
|
||||
},
|
||||
|
||||
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"));
|
||||
output.place(&fg(color, thunk(move|output: &mut Tui|{
|
||||
if let Some(color) = Namespace::namespace(state, arg0)? {
|
||||
output.show(fg(color, thunk(move|output: &mut Tui|{
|
||||
state.interpret(output, &arg1)?;
|
||||
// FIXME?: don't max out the used area?
|
||||
Ok(output.area().into())
|
||||
Ok(Some(output.area().into()))
|
||||
})))
|
||||
} else {
|
||||
return Err(format!("fg: {arg0:?}: not a color").into())
|
||||
}
|
||||
},
|
||||
|
||||
Some("bg") => {
|
||||
let arg0 = arg0?.expect("bg: expected arg 0 (color)");
|
||||
let color = Namespace::namespace(state, arg0)?.unwrap_or_else(||panic!("bg: {arg0:?}: not a color"));
|
||||
output.place(&bg(color, thunk(move|output: &mut Tui|{
|
||||
if let Some(color) = Namespace::namespace(state, arg0)? {
|
||||
output.show(bg(color, thunk(move|output: &mut Tui|{
|
||||
state.interpret(output, &arg1)?;
|
||||
// FIXME?: don't max out the used area?
|
||||
Ok(output.area().into())
|
||||
Ok(Some(output.area().into()))
|
||||
})))
|
||||
} else {
|
||||
return Err(format!("bg: {arg0:?}: not a color").into())
|
||||
}
|
||||
},
|
||||
|
||||
_ => return Ok(false)
|
||||
_ => return Ok(None)
|
||||
|
||||
};
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use crate::Usually;
|
|||
#[derive(Clone)] pub struct Exit(Arc<AtomicBool>);
|
||||
|
||||
impl Exit {
|
||||
pub fn run <T> (run: impl Fn(Self)->Usually<T>) -> Usually<T> {
|
||||
pub fn run <T> (run: impl FnOnce(Self)->Usually<T>) -> Usually<T> {
|
||||
run(Self(Arc::new(AtomicBool::new(false))))
|
||||
}
|
||||
}
|
||||
|
|
@ -14,4 +14,3 @@ impl AsRef<Arc<AtomicBool>> for Exit {
|
|||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
110
src/keys.rs
110
src/keys.rs
|
|
@ -1,110 +0,0 @@
|
|||
use crate::task::Task;
|
||||
|
||||
use ::std::sync::{Arc, RwLock, atomic::{AtomicBool, Ordering::*}};
|
||||
use ::std::time::Duration;
|
||||
use ::dizzle::{Usually, Apply, impl_from};
|
||||
use ::crossterm::event::{
|
||||
read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState
|
||||
};
|
||||
|
||||
/// Spawn the TUI input thread which reads keys from the terminal.
|
||||
pub fn tui_input <T: Apply<TuiEvent, Usually<T>> + Send + Sync + 'static> (
|
||||
exited: &Arc<AtomicBool>, state: &Arc<RwLock<T>>, poll: Duration
|
||||
) -> Result<Task, std::io::Error> {
|
||||
let exited = exited.clone();
|
||||
let state = state.clone();
|
||||
Task::new_poll(exited.clone(), poll, move |_| {
|
||||
let event = read().unwrap();
|
||||
match event {
|
||||
|
||||
// Hardcoded exit.
|
||||
Event::Key(KeyEvent {
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
code: KeyCode::Char('c'),
|
||||
kind: KeyEventKind::Press,
|
||||
state: KeyEventState::NONE
|
||||
}) => { exited.store(true, Relaxed); },
|
||||
|
||||
// Handle all other events by the state:
|
||||
event => {
|
||||
if let Err(e) = state.write().unwrap().apply(&TuiEvent(event)) {
|
||||
panic!("{e}")
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// TUI input loop event.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)]
|
||||
pub struct TuiEvent(pub Event);
|
||||
impl_from!(TuiEvent: |e: Event| TuiEvent(e));
|
||||
impl_from!(TuiEvent: |c: char| TuiEvent(Event::Key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE))));
|
||||
impl Ord for TuiEvent {
|
||||
fn cmp (&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.partial_cmp(other)
|
||||
.unwrap_or_else(||format!("{:?}", self).cmp(&format!("{other:?}"))) // FIXME perf
|
||||
}
|
||||
}
|
||||
|
||||
/// TUI key spec.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)]
|
||||
pub struct TuiKey(pub Option<KeyCode>, pub KeyModifiers);
|
||||
impl TuiKey {
|
||||
const SPLIT: char = '/';
|
||||
pub fn from_crossterm (event: KeyEvent) -> Self {
|
||||
Self(Some(event.code), event.modifiers)
|
||||
}
|
||||
pub fn to_crossterm (&self) -> Option<Event> {
|
||||
self.0.map(|code|Event::Key(KeyEvent {
|
||||
code,
|
||||
modifiers: self.1,
|
||||
kind: KeyEventKind::Press,
|
||||
state: KeyEventState::NONE,
|
||||
}))
|
||||
}
|
||||
pub fn named (token: &str) -> Option<KeyCode> {
|
||||
use KeyCode::*;
|
||||
Some(match token {
|
||||
"up" => Up,
|
||||
"down" => Down,
|
||||
"left" => Left,
|
||||
"right" => Right,
|
||||
"esc" | "escape" => Esc,
|
||||
"enter" | "return" => Enter,
|
||||
"delete" | "del" => Delete,
|
||||
"backspace" => Backspace,
|
||||
"tab" => Tab,
|
||||
"space" => Char(' '),
|
||||
"comma" => Char(','),
|
||||
"period" => Char('.'),
|
||||
"plus" => Char('+'),
|
||||
"minus" | "dash" => Char('-'),
|
||||
"equal" | "equals" => Char('='),
|
||||
"underscore" => Char('_'),
|
||||
"backtick" => Char('`'),
|
||||
"lt" => Char('<'),
|
||||
"gt" => Char('>'),
|
||||
"cbopen" | "openbrace" => Char('{'),
|
||||
"cbclose" | "closebrace" => Char('}'),
|
||||
"bropen" | "openbracket" => Char('['),
|
||||
"brclose" | "closebracket" => Char(']'),
|
||||
"pgup" | "pageup" => PageUp,
|
||||
"pgdn" | "pagedown" => PageDown,
|
||||
"f1" => F(1),
|
||||
"f2" => F(2),
|
||||
"f3" => F(3),
|
||||
"f4" => F(4),
|
||||
"f5" => F(5),
|
||||
"f6" => F(6),
|
||||
"f7" => F(7),
|
||||
"f8" => F(8),
|
||||
"f9" => F(9),
|
||||
"f10" => F(10),
|
||||
"f11" => F(11),
|
||||
"f12" => F(12),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
40
src/lang.rs
40
src/lang.rs
|
|
@ -1,40 +0,0 @@
|
|||
#[cfg(feature = "term")]
|
||||
impl crate::term::TuiKey {
|
||||
#[cfg(feature = "lang")]
|
||||
pub fn from_dsl (dsl: impl Language) -> Usually<Self> {
|
||||
if let Some(word) = dsl.word()? {
|
||||
let word = word.trim();
|
||||
Ok(if word == ":char" {
|
||||
Self(None, KeyModifiers::NONE)
|
||||
} else if word.chars().nth(0) == Some('@') {
|
||||
let mut key = None;
|
||||
let mut modifiers = KeyModifiers::NONE;
|
||||
let mut tokens = word[1..].split(Self::SPLIT).peekable();
|
||||
while let Some(token) = tokens.next() {
|
||||
if tokens.peek().is_some() {
|
||||
match token {
|
||||
"ctrl" | "Ctrl" | "c" | "C" => modifiers |= KeyModifiers::CONTROL,
|
||||
"alt" | "Alt" | "m" | "M" => modifiers |= KeyModifiers::ALT,
|
||||
"shift" | "Shift" | "s" | "S" => {
|
||||
modifiers |= KeyModifiers::SHIFT;
|
||||
// + TODO normalize character case, BackTab, etc.
|
||||
},
|
||||
_ => panic!("unknown modifier {token}"),
|
||||
}
|
||||
} else {
|
||||
key = if token.len() == 1 {
|
||||
Some(KeyCode::Char(token.chars().next().unwrap()))
|
||||
} else {
|
||||
Some(named_key(token).unwrap_or_else(||panic!("unknown character {token}")))
|
||||
}
|
||||
}
|
||||
}
|
||||
Self(key, modifiers)
|
||||
} else {
|
||||
return Err(format!("TuiKey: unexpected: {word}").into())
|
||||
})
|
||||
} else {
|
||||
return Err(format!("TuiKey: unspecified").into())
|
||||
}
|
||||
}
|
||||
}
|
||||
62
src/lib.rs
62
src/lib.rs
|
|
@ -1,19 +1,24 @@
|
|||
#![feature(anonymous_lifetime_in_impl_trait)]
|
||||
#![feature(associated_type_defaults)]
|
||||
#![feature(const_default)]
|
||||
#![feature(const_option_ops)]
|
||||
//#![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(impl_trait_in_assoc_type)]
|
||||
#![feature(step_trait)]
|
||||
#![feature(trait_alias)]
|
||||
#![feature(type_alias_impl_trait)]
|
||||
#![feature(type_changing_struct_update)]
|
||||
//#![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 = "term")] pub extern crate ratatui;
|
||||
#[cfg(feature = "term")] pub extern crate crossterm;
|
||||
#[cfg(feature = "lang")] pub extern crate dizzle as lang;
|
||||
#[cfg(test)] #[macro_use] pub extern crate proptest;
|
||||
#[cfg(test)] pub(crate) use proptest_derive::Arbitrary;
|
||||
|
||||
pub(crate) use ::{
|
||||
atomic_float::AtomicF64,
|
||||
|
|
@ -21,29 +26,32 @@ pub(crate) use ::{
|
|||
std::ops::{Add, Sub, Mul, Div},
|
||||
std::sync::{Arc, RwLock},
|
||||
std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::*},
|
||||
std::error::Error,
|
||||
std::marker::PhantomData
|
||||
};
|
||||
|
||||
#[cfg(feature = "lang")] pub extern crate dizzle as lang;
|
||||
#[cfg(feature = "lang")] pub use ::dizzle::{self, Usually, Perhaps, impl_default};
|
||||
#[cfg(feature = "lang")] pub mod eval;
|
||||
macro_rules! features {
|
||||
($($feature:literal: [ $($module:ident),* ]),*) => {
|
||||
$(
|
||||
$(
|
||||
#[cfg(feature = $feature)] mod $module;
|
||||
#[cfg(feature = $feature)] pub use $module::*;
|
||||
)*
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "time")] pub mod time;
|
||||
#[cfg(feature = "lang")] pub use ::dizzle::{Usually, Perhaps, impl_default};
|
||||
|
||||
#[cfg(feature = "play")] pub mod exit;
|
||||
#[cfg(feature = "play")] pub mod task;
|
||||
|
||||
#[cfg(feature = "sing")] pub extern crate jack;
|
||||
#[cfg(feature = "sing")] pub mod sing;
|
||||
|
||||
#[cfg(feature = "draw")] pub mod draw;
|
||||
#[cfg(feature = "draw")] pub mod space;
|
||||
#[cfg(feature = "draw")] pub mod color;
|
||||
|
||||
#[cfg(feature = "text")] pub mod text;
|
||||
#[cfg(feature = "term")] pub mod term;
|
||||
#[cfg(feature = "term")] pub mod keys;
|
||||
#[cfg(feature = "term")] pub extern crate ratatui;
|
||||
#[cfg(feature = "term")] pub extern crate crossterm;
|
||||
features! {
|
||||
"lang": [ eval ],
|
||||
"time": [ time ],
|
||||
"play": [ exit, task ],
|
||||
"sing": [ sing ],
|
||||
"text": [ text ],
|
||||
"term": [ term ],
|
||||
"draw": [ draw ]
|
||||
}
|
||||
|
||||
/// Define a trait an implement it for various mutation-enabled wrapper types. */
|
||||
#[macro_export] macro_rules! flex_trait_mut (
|
||||
|
|
@ -86,7 +94,7 @@ pub(crate) use ::{
|
|||
// FIXME: support attrs (docstrings)
|
||||
$($Variant $({ $($arg: $Arg),* })?),*
|
||||
}
|
||||
impl ::tengri::dizzle::Act<$State> for $Command {
|
||||
impl ::tengri::lang::Act<$State> for $Command {
|
||||
fn act (&self, $state: &mut $State) -> Perhaps<Self> {
|
||||
match self {
|
||||
$(Self::$Variant $({ $($arg),* })? => $body,)*
|
||||
|
|
|
|||
0
src/origin.rs
Normal file
0
src/origin.rs
Normal file
191
src/sing.rs
191
src/sing.rs
|
|
@ -1,6 +1,9 @@
|
|||
pub use ::jack::{*, contrib::{*, ClosureProcessHandler}};
|
||||
use crate::{*, time::PerfModel};
|
||||
use JackState::*;
|
||||
|
||||
mod jack; pub use self::jack::*;
|
||||
mod jack_event; pub use self::jack_event::*;
|
||||
mod jack_perf; pub use self::jack_perf::*;
|
||||
|
||||
/// Trait for thing that has a JACK process callback.
|
||||
pub trait Audio {
|
||||
|
|
@ -22,58 +25,6 @@ pub trait Audio {
|
|||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
|
@ -92,138 +43,10 @@ pub type DynamicAudioHandler<'j> =
|
|||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Things that can provide a [jack::Client] reference.
|
||||
///
|
||||
/// ```
|
||||
/// use tengri::{Jack, HasJack};
|
||||
/// use tengri::*;
|
||||
///
|
||||
/// let jack: &Jack = Jacked::default().jack();
|
||||
///
|
||||
|
|
@ -266,10 +89,6 @@ pub trait HasJack<'j>: Send + Sync {
|
|||
}
|
||||
}
|
||||
|
||||
pub trait JackPerfModel {
|
||||
fn update_from_jack_scope (&self, t0: Option<u64>, scope: &ProcessScope);
|
||||
}
|
||||
|
||||
/// Implement [Audio]: provide JACK callbacks.
|
||||
#[macro_export] macro_rules! impl_audio {
|
||||
|
||||
|
|
|
|||
112
src/sing/jack.rs
Normal file
112
src/sing/jack.rs
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
use crate::{*, PerfModel};
|
||||
pub use ::jack::{*, contrib::{*, ClosureProcessHandler}};
|
||||
|
||||
use JackState::*;
|
||||
|
||||
/// Wraps [JackState], and through it [jack::Client] when connected.
|
||||
///
|
||||
/// ```
|
||||
/// let jack = tengri::Jack::default();
|
||||
/// ```
|
||||
#[derive(Clone, Debug, Default)] pub struct Jack<'j> (
|
||||
pub(crate) Arc<RwLock<JackState<'j>>>
|
||||
);
|
||||
|
||||
/// This is a connection which may be [Inactive], [Activating], or [Active].
|
||||
/// In the [Active] and [Inactive] states, [JackState::client] returns a
|
||||
/// [jack::Client], which you can use to talk to the JACK API.
|
||||
///
|
||||
/// ```
|
||||
/// let state = tengri::JackState::default();
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub enum JackState<'j> {
|
||||
/// Unused
|
||||
#[default] Inert,
|
||||
/// Before activation.
|
||||
Inactive(Client),
|
||||
/// During activation.
|
||||
Activating,
|
||||
/// After activation. Must not be dropped for JACK thread to persist.
|
||||
Active(DynamicAsyncClient<'j>),
|
||||
}
|
||||
|
||||
/// Implement [Jack] constructor and methods
|
||||
impl<'j> Jack<'j> {
|
||||
/// Register new [Client] and wrap it for shared use.
|
||||
pub fn new_run <T: HasJack<'j> + Audio + Send + Sync + 'static> (
|
||||
name: impl AsRef<str>,
|
||||
init: impl FnOnce(Jack<'j>)->Usually<T>
|
||||
) -> Usually<Arc<RwLock<T>>> {
|
||||
Jack::new(name)?.run(init)
|
||||
}
|
||||
|
||||
pub fn new (name: impl AsRef<str>) -> Usually<Self> {
|
||||
let client = Client::new(name.as_ref(), ClientOptions::NO_START_SERVER)?.0;
|
||||
Ok(Jack(Arc::new(RwLock::new(JackState::Inactive(client)))))
|
||||
}
|
||||
|
||||
pub fn run <T: HasJack<'j> + Audio + Send + Sync + 'static>
|
||||
(self, init: impl FnOnce(Self)->Usually<T>) -> Usually<Arc<RwLock<T>>>
|
||||
{
|
||||
let client_state = self.0.clone();
|
||||
let app: Arc<RwLock<T>> = Arc::new(RwLock::new(init(self)?));
|
||||
let mut state = Activating;
|
||||
std::mem::swap(&mut*client_state.write().unwrap(), &mut state);
|
||||
if let Inactive(client) = state {
|
||||
// This is the misc notifications handler. It's a struct that wraps a [Box]
|
||||
// which performs type erasure on a callback that takes [JackEvent], which is
|
||||
// one of the available misc notifications.
|
||||
let notify = JackNotify(Box::new({
|
||||
let app = app.clone();
|
||||
move|event|(&mut*app.write().unwrap()).handle(event)
|
||||
}) as BoxedJackEventHandler);
|
||||
// This is the main processing handler. It's a struct that wraps a [Box]
|
||||
// which performs type erasure on a callback that takes [Client] and [ProcessScope]
|
||||
// and passes them down to the `app`'s `process` callback, which in turn
|
||||
// implements audio and MIDI input and output on a realtime basis.
|
||||
let process = ::jack::contrib::ClosureProcessHandler::new(Box::new({
|
||||
let app = app.clone();
|
||||
move|c: &_, s: &_|if let Ok(mut app) = app.write() {
|
||||
app.process(c, s)
|
||||
} else {
|
||||
Control::Quit
|
||||
}
|
||||
}) as BoxedAudioHandler);
|
||||
// Launch a client with the two handlers.
|
||||
*client_state.write().unwrap() = Active(
|
||||
client.activate_async(notify, process)?
|
||||
);
|
||||
} else {
|
||||
unreachable!();
|
||||
}
|
||||
Ok(app)
|
||||
}
|
||||
|
||||
/// Run something with the client.
|
||||
pub fn with_client <T> (&self, op: impl FnOnce(&Client)->T) -> T {
|
||||
match &*self.0.read().unwrap() {
|
||||
Inert => panic!("jack client not activated"),
|
||||
Inactive(client) => op(client),
|
||||
Activating => panic!("jack client has not finished activation"),
|
||||
Active(client) => op(client.as_client()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'j> HasJack<'j> for Jack<'j> {
|
||||
fn jack (&self) -> &Jack<'j> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'j> HasJack<'j> for &Jack<'j> {
|
||||
fn jack (&self) -> &Jack<'j> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'j, T: HasJack<'j>> HasJack<'j> for Arc<T> {
|
||||
fn jack (&self) -> &Jack<'j> {
|
||||
(&**self).jack()
|
||||
}
|
||||
}
|
||||
72
src/sing/jack_event.rs
Normal file
72
src/sing/jack_event.rs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
use crate::{*, time::PerfModel};
|
||||
pub use ::jack::{*, contrib::{*, ClosureProcessHandler}};
|
||||
|
||||
/// Event enum for JACK events.
|
||||
///
|
||||
/// ```
|
||||
/// let event = tengri::JackEvent::XRun; // kerpop
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq)] pub enum JackEvent {
|
||||
ThreadInit,
|
||||
Shutdown(ClientStatus, Arc<str>),
|
||||
Freewheel(bool),
|
||||
SampleRate(Frames),
|
||||
ClientRegistration(Arc<str>, bool),
|
||||
PortRegistration(PortId, bool),
|
||||
PortRename(PortId, Arc<str>, Arc<str>),
|
||||
PortsConnected(PortId, PortId, bool),
|
||||
GraphReorder,
|
||||
XRun,
|
||||
}
|
||||
|
||||
/// Generic notification handler that emits [JackEvent]
|
||||
///
|
||||
/// ```
|
||||
/// let notify = tengri::JackNotify(|_|{});
|
||||
/// ```
|
||||
pub struct JackNotify<T: Fn(JackEvent) + Send>(pub T);
|
||||
|
||||
/// Notification handler wrapper for [BoxedJackEventHandler].
|
||||
pub type DynamicNotifications<'j> =
|
||||
JackNotify<BoxedJackEventHandler<'j>>;
|
||||
|
||||
/// Boxed [JackEvent] callback.
|
||||
pub type BoxedJackEventHandler<'j> =
|
||||
Box<dyn Fn(JackEvent) + Send + Sync + 'j>;
|
||||
|
||||
impl<T: Fn(JackEvent) + Send> NotificationHandler for JackNotify<T> {
|
||||
fn thread_init(&self, _: &Client) {
|
||||
self.0(JackEvent::ThreadInit);
|
||||
}
|
||||
unsafe fn shutdown(&mut self, status: ClientStatus, reason: &str) {
|
||||
self.0(JackEvent::Shutdown(status, reason.into()));
|
||||
}
|
||||
fn freewheel(&mut self, _: &Client, enabled: bool) {
|
||||
self.0(JackEvent::Freewheel(enabled));
|
||||
}
|
||||
fn sample_rate(&mut self, _: &Client, frames: Frames) -> Control {
|
||||
self.0(JackEvent::SampleRate(frames));
|
||||
Control::Quit
|
||||
}
|
||||
fn client_registration(&mut self, _: &Client, name: &str, reg: bool) {
|
||||
self.0(JackEvent::ClientRegistration(name.into(), reg));
|
||||
}
|
||||
fn port_registration(&mut self, _: &Client, id: PortId, reg: bool) {
|
||||
self.0(JackEvent::PortRegistration(id, reg));
|
||||
}
|
||||
fn port_rename(&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control {
|
||||
self.0(JackEvent::PortRename(id, old.into(), new.into()));
|
||||
Control::Continue
|
||||
}
|
||||
fn ports_connected(&mut self, _: &Client, a: PortId, b: PortId, are: bool) {
|
||||
self.0(JackEvent::PortsConnected(a, b, are));
|
||||
}
|
||||
fn graph_reorder(&mut self, _: &Client) -> Control {
|
||||
self.0(JackEvent::GraphReorder);
|
||||
Control::Continue
|
||||
}
|
||||
fn xrun(&mut self, _: &Client) -> Control {
|
||||
self.0(JackEvent::XRun);
|
||||
Control::Continue
|
||||
}
|
||||
}
|
||||
22
src/sing/jack_perf.rs
Normal file
22
src/sing/jack_perf.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
use crate::{*, time::PerfModel};
|
||||
pub use ::jack::{*, contrib::{*, ClosureProcessHandler}};
|
||||
|
||||
pub trait JackPerfModel {
|
||||
fn update_from_jack_scope (&self, t0: Option<u64>, scope: &ProcessScope);
|
||||
}
|
||||
|
||||
impl JackPerfModel for PerfModel {
|
||||
fn update_from_jack_scope (&self, t0: Option<u64>, scope: &ProcessScope) {
|
||||
if let Some(t0) = t0 {
|
||||
let t1 = self.clock.raw();
|
||||
self.used.store(
|
||||
self.clock.delta_as_nanos(t0, t1) as f64,
|
||||
Relaxed,
|
||||
);
|
||||
self.window.store(
|
||||
scope.cycle_times().unwrap().period_usecs as f64,
|
||||
Relaxed,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
515
src/space.rs
515
src/space.rs
|
|
@ -1,515 +0,0 @@
|
|||
use crate::{*, draw::*};
|
||||
#[cfg(test)] use proptest_derive::Arbitrary;
|
||||
|
||||
/// 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: 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 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> X<N> for XYWH<N> { fn x (&self) -> N { self.0 } fn w (&self) -> N { self.2 } }
|
||||
impl<N: Coord> Y<N> for XYWH<N> { fn y (&self) -> N { self.0 } fn h (&self) -> N { self.2 } }
|
||||
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) {
|
||||
use Split::*;
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Something that has `[0, 0]` at a particular point.
|
||||
pub trait HasOrigin { fn origin (&self) -> Origin;
|
||||
}
|
||||
impl<T: AsRef<Origin>> HasOrigin for T { fn origin (&self) -> Origin { *self.as_ref() } }
|
||||
pub struct Anchor<T>(Origin, T);
|
||||
impl<T> AsRef<Origin> for Anchor<T> {
|
||||
fn as_ref (&self) -> &Origin {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl<T: Screen, U: Draw<T>> Draw<T> for Anchor<U> {
|
||||
fn draw (self, to: &mut T) -> Usually<XYWH<T::Unit>> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn origin_c <T: Screen> (a: impl Draw<T>) -> impl Draw<T> {
|
||||
Anchor(Origin::C, a)
|
||||
}
|
||||
pub const fn origin_x <T: Screen> (a: impl Draw<T>) -> impl Draw<T> {
|
||||
Anchor(Origin::X, a)
|
||||
}
|
||||
pub const fn origin_y <T: Screen> (a: impl Draw<T>) -> impl Draw<T> {
|
||||
Anchor(Origin::Y, a)
|
||||
}
|
||||
pub const fn origin_nw <T: Screen> (a: impl Draw<T>) -> impl Draw<T> {
|
||||
Anchor(Origin::NW, a)
|
||||
}
|
||||
pub const fn origin_n <T: Screen> (a: impl Draw<T>) -> impl Draw<T> {
|
||||
Anchor(Origin::N, a)
|
||||
}
|
||||
pub const fn origin_ne <T: Screen> (a: impl Draw<T>) -> impl Draw<T> {
|
||||
Anchor(Origin::NE, a)
|
||||
}
|
||||
pub const fn origin_w <T: Screen> (a: impl Draw<T>) -> impl Draw<T> {
|
||||
Anchor(Origin::W, a)
|
||||
}
|
||||
pub const fn origin_e <T: Screen> (a: impl Draw<T>) -> impl Draw<T> {
|
||||
Anchor(Origin::E, a)
|
||||
}
|
||||
pub const fn origin_sw <T: Screen> (a: impl Draw<T>) -> impl Draw<T> {
|
||||
Anchor(Origin::SW, a)
|
||||
}
|
||||
pub const fn origin_s <T: Screen> (a: impl Draw<T>) -> impl Draw<T> {
|
||||
Anchor(Origin::S, a)
|
||||
}
|
||||
pub const fn origin_se <T: Screen> (a: impl Draw<T>) -> impl Draw<T> {
|
||||
Anchor(Origin::SE, a)
|
||||
}
|
||||
|
||||
/// Where is [0, 0] located?
|
||||
///
|
||||
/// ```
|
||||
/// use tengri::draw::Origin;
|
||||
/// let _ = Origin::NW.align(())
|
||||
/// ```
|
||||
#[cfg_attr(test, derive(Arbitrary))]
|
||||
#[derive(Debug, Copy, Clone, Default)] pub enum Origin {
|
||||
#[default] C, X, Y, NW, N, NE, E, SE, S, SW, W
|
||||
}
|
||||
impl Origin {
|
||||
pub fn align <T: Screen> (&self, a: impl Draw<T>) -> impl Draw<T> {
|
||||
align(*self, a)
|
||||
}
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// use tengri::draw::{align, Origin::*};
|
||||
/// let _ = align(NW, "test");
|
||||
/// let _ = align(SE, "test");
|
||||
/// ```
|
||||
pub fn align <T: Screen> (origin: Origin, a: impl Draw<T>) -> impl Draw<T> {
|
||||
thunk(move|to: &mut T| { todo!() })
|
||||
}
|
||||
pub fn align_n <T: Screen> (a: impl Draw<T>) -> impl Draw<T> {
|
||||
align(Origin::N, a)
|
||||
}
|
||||
|
||||
/// A numeric type that can be used as coordinate.
|
||||
///
|
||||
/// FIXME: Replace with `num` crate?
|
||||
/// FIXME: Use AsRef/AsMut?
|
||||
///
|
||||
/// ```
|
||||
/// use tengri::draw::Coord;
|
||||
/// 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()) }
|
||||
}
|
||||
|
||||
/// A cardinal direction.
|
||||
#[cfg_attr(test, derive(Arbitrary))]
|
||||
#[derive(Copy, Clone, PartialEq, Debug, Default)] pub enum Split {
|
||||
North, South, East, West, Above, #[default] Below
|
||||
}
|
||||
|
||||
pub const fn east <T: Screen> (a: impl Draw<T>, b: impl Draw<T>) -> impl Draw<T> {
|
||||
Split::East.half(a, b)
|
||||
}
|
||||
pub const fn north <T: Screen> (a: impl Draw<T>, b: impl Draw<T>) -> impl Draw<T> {
|
||||
Split::North.half(a, b)
|
||||
}
|
||||
pub const fn west <T: Screen> (a: impl Draw<T>, b: impl Draw<T>) -> impl Draw<T> {
|
||||
Split::West.half(a, b)
|
||||
}
|
||||
pub const fn south <T: Screen> (a: impl Draw<T>, b: impl Draw<T>) -> impl Draw<T> {
|
||||
Split::South.half(a, b)
|
||||
}
|
||||
pub const fn above <T: Screen> (a: impl Draw<T>, b: impl Draw<T>) -> impl Draw<T> {
|
||||
Split::Above.half(a, b)
|
||||
}
|
||||
pub const fn below <T: Screen> (a: impl Draw<T>, b: impl Draw<T>) -> impl Draw<T> {
|
||||
Split::Below.half(a, b)
|
||||
}
|
||||
impl Split {
|
||||
/// ```
|
||||
/// use tengri::draw::Split::*;
|
||||
/// let _ = Above.bsp((), ());
|
||||
/// let _ = Below.bsp((), ());
|
||||
/// let _ = North.bsp((), ());
|
||||
/// let _ = South.bsp((), ());
|
||||
/// let _ = East.bsp((), ());
|
||||
/// let _ = West.bsp((), ());
|
||||
/// ```
|
||||
pub const fn half <T: Screen> (&self, a: impl Draw<T>, b: impl Draw<T>) -> impl Draw<T> {
|
||||
thunk(move|to: &mut T|{
|
||||
let (area_a, area_b) = to.xywh().split_half(self);
|
||||
let (origin_a, origin_b) = self.origins();
|
||||
let a = origin_a.align(a);
|
||||
let b = origin_b.align(b);
|
||||
match self {
|
||||
Self::Below => {
|
||||
to.place_at(area_b, &b);
|
||||
to.place_at(area_b, &a);
|
||||
},
|
||||
_ => {
|
||||
to.place_at(area_a, &a);
|
||||
to.place_at(area_a, &b);
|
||||
}
|
||||
}
|
||||
Ok(to.xywh()) // FIXME: compute and return actually used area
|
||||
})
|
||||
}
|
||||
/// Newly split areas begin at the center of the split
|
||||
/// to maintain centeredness in the user's field of view.
|
||||
///
|
||||
/// Use [align] to override that and always start
|
||||
/// at the top, bottom, etc.
|
||||
///
|
||||
/// ```
|
||||
/// /*
|
||||
///
|
||||
/// Split east: Split south:
|
||||
/// | | | | A |
|
||||
/// | <-A|B-> | |---------|
|
||||
/// | | | | B |
|
||||
///
|
||||
/// */
|
||||
/// ```
|
||||
const fn origins (&self) -> (Origin, Origin) {
|
||||
use Origin::*;
|
||||
match self {
|
||||
Self::South => (S, N),
|
||||
Self::East => (E, W),
|
||||
Self::North => (N, S),
|
||||
Self::West => (W, E),
|
||||
Self::Above => (C, C),
|
||||
Self::Below => (C, C),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Horizontal axis.
|
||||
pub trait X<N: Coord> {
|
||||
fn x (&self) -> N;
|
||||
fn w (&self) -> N { N::zero() }
|
||||
fn w_min (&self) -> N { self.w() }
|
||||
fn w_max (&self) -> N { self.w() }
|
||||
fn iter_x (&self) -> impl Iterator<Item = N> where Self: HasOrigin {
|
||||
self.x_west()..self.x_east()
|
||||
}
|
||||
fn x_west (&self) -> N where Self: HasOrigin {
|
||||
use Origin::*;
|
||||
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 {
|
||||
use Origin::*;
|
||||
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!()
|
||||
}
|
||||
}
|
||||
pub const fn x_push <T: Screen> (x: T::Unit, a: impl Draw<T>) -> impl Draw<T> {
|
||||
a
|
||||
}
|
||||
pub const fn x_pull <T: Screen> (x: T::Unit, a: impl Draw<T>) -> impl Draw<T> {
|
||||
a
|
||||
}
|
||||
pub const fn w_max <T: Screen> (w: Option<T::Unit>, a: impl Draw<T>) -> impl Draw<T> {
|
||||
wh_max(w, None, a)
|
||||
}
|
||||
pub const fn w_min <T: Screen> (w: Option<T::Unit>, a: impl Draw<T>) -> impl Draw<T> {
|
||||
wh_min(w, None, a)
|
||||
}
|
||||
pub const fn w_exact <T: Screen> (w: T::Unit, c: impl Draw<T>) -> impl Draw<T> {
|
||||
wh_exact(Some(w), None, c)
|
||||
}
|
||||
/// Shrink drawing area symmetrically.
|
||||
///
|
||||
/// ```
|
||||
/// let padded = tengri::W(3).pad("Hello");
|
||||
/// ```
|
||||
pub const fn w_pad <T: Screen> (x: T::Unit, draw: impl Draw<T>) -> impl Draw<T> {
|
||||
thunk(move|to: &mut T|draw.draw(todo!()))
|
||||
}
|
||||
|
||||
pub trait Y<N: Coord> {
|
||||
fn y (&self) -> N;
|
||||
fn h (&self) -> N { N::zero() }
|
||||
fn h_min (&self) -> N { self.h() }
|
||||
fn h_max (&self) -> N { self.h() }
|
||||
fn iter_y (&self) -> impl Iterator<Item = 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();
|
||||
use Origin::*;
|
||||
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();
|
||||
use Origin::*;
|
||||
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!()
|
||||
}
|
||||
}
|
||||
pub const fn y_push <T: Screen> (x: T::Unit, a: impl Draw<T>) -> impl Draw<T> {
|
||||
a
|
||||
}
|
||||
pub const fn y_pull <T: Screen> (x: T::Unit, a: impl Draw<T>) -> impl Draw<T> {
|
||||
a
|
||||
}
|
||||
pub const fn h_max <T: Screen> (h: Option<T::Unit>, a: impl Draw<T>) -> impl Draw<T> {
|
||||
wh_max(None, h, a)
|
||||
}
|
||||
pub const fn h_min <T: Screen> (h: Option<T::Unit>, a: impl Draw<T>) -> impl Draw<T> {
|
||||
wh_min(None, h, a)
|
||||
}
|
||||
pub const fn h_exact <T: Screen> (h: T::Unit, c: impl Draw<T>) -> impl Draw<T> {
|
||||
wh_exact(None, Some(h), c)
|
||||
}
|
||||
/// Shrink drawing area symmetrically.
|
||||
///
|
||||
/// ```
|
||||
/// let padded = tengri::W::pad(3, "Hello");
|
||||
/// ```
|
||||
pub const fn h_pad <T: Screen> (x: T::Unit, draw: impl Draw<T>) -> impl Draw<T> {
|
||||
thunk(move|to: &mut T|draw.draw(todo!()))
|
||||
}
|
||||
|
||||
pub trait Space<N: Coord>: X<N> + Y<N> {
|
||||
fn xywh (&self) -> XYWH<N> { XYWH(self.x(), self.y(), self.w(), self.h()) }
|
||||
// FIXME: factor origin
|
||||
fn lrtb (&self) -> [N;4] { [self.x(), self.y(), self.x()+self.w(), self.y()+self.h()] }
|
||||
}
|
||||
impl<N: Coord, T: X<N> + Y<N>> Space<N> for T {}
|
||||
pub const fn xy_push <T: Screen> (x: T::Unit, y: T::Unit, a: impl Draw<T>) -> impl Draw<T> {
|
||||
a
|
||||
}
|
||||
pub const fn xy_pull <T: Screen> (x: T::Unit, y: T::Unit, a: impl Draw<T>) -> impl Draw<T> {
|
||||
a
|
||||
}
|
||||
/// Shrink drawing area symmetrically.
|
||||
///
|
||||
/// ```
|
||||
/// let padded = tengri::WH(3, 5).pad("Hello");
|
||||
/// ```
|
||||
pub const fn wh_pad <T: Screen> (w: T::Unit, h: T::Unit, draw: impl Draw<T>)
|
||||
-> impl Draw<T>
|
||||
{
|
||||
thunk(move|to: &mut T|draw.draw(todo!()))
|
||||
}
|
||||
/// Only draw content if area is above a certain size.
|
||||
///
|
||||
/// ```
|
||||
/// let min = tengri::wh_min(3, 5, "Hello"); // 5x5
|
||||
/// ```
|
||||
pub const fn wh_min <T: Screen> (w: Option<T::Unit>, h: Option<T::Unit>, draw: impl Draw<T>)
|
||||
-> impl Draw<T>
|
||||
{
|
||||
thunk(move|to: &mut T|draw.draw(todo!()))
|
||||
}
|
||||
|
||||
/// Set the maximum width and/or height of the content.
|
||||
///
|
||||
/// ```
|
||||
/// let max = tengri::wh_max(Some(3), Some(5), "Hello");
|
||||
/// ```
|
||||
pub const fn wh_max <T: Screen> (w: Option<T::Unit>, h: Option<T::Unit>, draw: impl Draw<T>)
|
||||
-> impl Draw<T>
|
||||
{
|
||||
thunk(move|to: &mut T|draw.draw(todo!()))
|
||||
}
|
||||
|
||||
/// Set the maximum width and/or height of the content.
|
||||
///
|
||||
/// ```
|
||||
/// let exact = tengri::wh_exact(Some(3), Some(5), "Hello");
|
||||
/// ```
|
||||
pub const fn wh_exact <T: Screen> (w: Option<T::Unit>, h: Option<T::Unit>, draw: impl Draw<T>)
|
||||
-> impl Draw<T>
|
||||
{
|
||||
thunk(move|to: &mut T|draw.draw(todo!()))
|
||||
}
|
||||
|
||||
/// Limit size of drawing area
|
||||
/// ```
|
||||
/// let clipped = tengri::wh_clip(Some(3), Some(5), "Hello");
|
||||
/// ```
|
||||
pub const fn wh_clip <T: Screen> (
|
||||
w: Option<T::Unit>, h: Option<T::Unit>, draw: impl Draw<T>
|
||||
) -> impl Draw<T> {
|
||||
thunk(move|to: &mut T|draw.draw(todo!()))
|
||||
}
|
||||
|
||||
pub const fn wh_full <T: Screen> (a: impl Draw<T>) -> impl Draw<T> { a }
|
||||
pub const fn w_full <T: Screen> (a: impl Draw<T>) -> impl Draw<T> { a }
|
||||
pub const fn h_full <T: Screen> (a: impl Draw<T>) -> impl Draw<T> { a }
|
||||
|
||||
#[macro_export] macro_rules! north {
|
||||
($head:expr $(,)?) => { $head };
|
||||
($head:expr, $($tail:expr),* $(,)?) => { north($head, north!($($tail,)*)) };
|
||||
}
|
||||
#[macro_export] macro_rules! south {
|
||||
($head:expr $(,)?) => { $head };
|
||||
($head:expr, $($tail:expr),* $(,)?) => { south($head, south!($($tail,)*)) };
|
||||
}
|
||||
#[macro_export] macro_rules! east {
|
||||
($head:expr $(,)?) => { $head };
|
||||
($head:expr, $($tail:expr),* $(,)?) => { east($head, east!($($tail,)*)) };
|
||||
}
|
||||
#[macro_export] macro_rules! west {
|
||||
($head:expr $(, $tail:expr)* $(,)?) => { west($head, west!($($tail,)*)) };
|
||||
}
|
||||
#[macro_export] macro_rules! above {
|
||||
($head:expr $(, $tail:expr)* $(,)?) => { above($head, above!($($tail,)*)) };
|
||||
}
|
||||
#[macro_export] macro_rules! below {
|
||||
($head:expr $(,)?) => { $head };
|
||||
($head:expr, $($tail:expr),* $(,)?) => { below($head, below!($($tail,)*)) };
|
||||
}
|
||||
|
||||
/// Iterate over a collection of renderables:
|
||||
///
|
||||
/// ```
|
||||
/// use tengri::draw::{Origin::*, Split::*};
|
||||
/// let _ = Below.iter([
|
||||
/// NW.align(W(15).max(W(10).min("Leftbar"))),
|
||||
/// NE.align(W(12).max(W(10).min("Rightbar"))),
|
||||
/// Center.align(W(40).max(H(20.max("Center"))))
|
||||
/// ].iter(), |x|x);
|
||||
/// ```
|
||||
pub fn iter <
|
||||
S: Screen,
|
||||
D: Draw<S>,
|
||||
V: Fn()->I,
|
||||
I: Iterator<Item = D>,
|
||||
F: Fn(&D)->dyn Draw<S>,
|
||||
> (_items: V, _cb: F) -> impl Draw<S> {
|
||||
thunk(move|_to: &mut S|{ todo!() })
|
||||
}
|
||||
|
||||
pub fn iter_north <'a, S: Screen, D: 'a, I: Iterator<Item = D>, U: Draw<S>> (
|
||||
_iter: impl Fn()->I, _draw: impl Fn(D)->U,
|
||||
) -> impl Draw<S> {
|
||||
thunk(move|_to: &mut S|{ todo!() })
|
||||
}
|
||||
|
||||
pub fn iter_east <'a, S: Screen, D: 'a, I: Iterator<Item = D>, U: Draw<S>> (
|
||||
_iter: impl Fn()->I, _draw: impl Fn(D)->U,
|
||||
) -> impl Draw<S> {
|
||||
thunk(move|_to: &mut S|{ todo!() })
|
||||
}
|
||||
|
||||
pub fn iter_south <'a, S: Screen, D: 'a, I: Iterator<Item = D>, U: Draw<S>> (
|
||||
_iter: impl Fn()->I, _draw: impl Fn(D)->U,
|
||||
) -> impl Draw<S> {
|
||||
thunk(move|_to: &mut S|{ todo!() })
|
||||
}
|
||||
|
||||
pub fn iter_west <'a, S: Screen, D: 'a, I: Iterator<Item = D>, U: Draw<S>> (
|
||||
_iter: impl Fn()->I, _draw: impl Fn(D)->U,
|
||||
) -> impl Draw<S> {
|
||||
thunk(move|_to: &mut S|{ todo!() })
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Size(AtomicUsize, AtomicUsize);
|
||||
impl X<u16> for Size {
|
||||
fn x (&self) -> u16 { 0 }
|
||||
fn w (&self) -> u16 { self.0.load(Relaxed) as u16 }
|
||||
}
|
||||
impl Y<u16> for Size {
|
||||
fn y (&self) -> u16 { 0 }
|
||||
fn h (&self) -> u16 { self.1.load(Relaxed) as u16 }
|
||||
}
|
||||
impl Size {
|
||||
pub const fn of <T: Screen> (&self, of: impl Draw<T>) -> impl Draw<T> {
|
||||
thunk(move|to: &mut T|{
|
||||
let area = of.draw(to)?;
|
||||
self.0.store(area.w().into(), Relaxed);
|
||||
self.1.store(area.h().into(), Relaxed);
|
||||
Ok(area)
|
||||
})
|
||||
}
|
||||
}
|
||||
733
src/term.rs
733
src/term.rs
|
|
@ -1,102 +1,74 @@
|
|||
use crate::{*, lang::*, draw::*, space::{*, Split::*}, color::*, text::*, task::*};
|
||||
use crate::{*, lang::*};
|
||||
|
||||
mod border; pub use self::border::*;
|
||||
mod event; pub use self::event::*;
|
||||
mod keys; pub use self::keys::*;
|
||||
mod buffer; pub use self::buffer::*;
|
||||
mod input; pub use self::input::*;
|
||||
mod output; pub use self::output::*;
|
||||
|
||||
//use unicode_width::{UnicodeWidthStr, UnicodeWidthChar};
|
||||
//use rand::distributions::uniform::UniformSampler;
|
||||
use ::{
|
||||
pub(crate) use ::{
|
||||
std::{
|
||||
io::{stdout, Write},
|
||||
time::Duration,
|
||||
ops::{Deref, DerefMut},
|
||||
},
|
||||
better_panic::{Settings, Verbosity},
|
||||
ratatui::{
|
||||
prelude::{Style, Buffer as ScreenBuffer, Position, Backend, Color},
|
||||
prelude::{Style, Position, Backend, Color},
|
||||
style::{Modifier, Color::*},
|
||||
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, Event, KeyEvent, KeyModifiers, KeyCode, KeyEventKind, KeyEventState
|
||||
},
|
||||
};
|
||||
|
||||
#[macro_export] macro_rules! tui_main {
|
||||
($state:expr) => {
|
||||
pub fn main () -> Usually<()> {
|
||||
tengri::exit::Exit::run(|exit|{
|
||||
let state = Arc::new(RwLock::new($state));
|
||||
let input = ::tengri::keys::tui_input(
|
||||
exit.as_ref(),
|
||||
&state,
|
||||
::std::time::Duration::from_millis(100)
|
||||
)?;
|
||||
let output = ::tengri::term::tui_output(
|
||||
stdout(),
|
||||
exit.as_ref(),
|
||||
&state,
|
||||
::std::time::Duration::from_millis(10)
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Terminal output.
|
||||
pub struct Tui(
|
||||
/// Ratatui buffer; area is screen size
|
||||
pub Buffer,
|
||||
/// Current draw area
|
||||
pub XYWH<u16>
|
||||
);
|
||||
|
||||
#[macro_export] macro_rules! tui_keys {
|
||||
(|$self:ident:$State:ty,$input:ident|$body:block) => {
|
||||
impl Apply<TuiEvent, Usually<Self>> for $State {
|
||||
fn apply (&mut $self, $input: &TuiEvent) -> Usually<Self> $body
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! tui_view {
|
||||
(|$self:ident: $State:ty|$body:block) => {
|
||||
impl View<Tui> for $State {
|
||||
fn view (&$self) -> impl Draw<Tui> $body
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn the TUI output thread which writes colored characters to the terminal.
|
||||
pub fn tui_output <W: Write + Send + Sync + 'static, T: View<Tui> + Send + Sync + 'static> (
|
||||
output: W,
|
||||
exited: &Arc<AtomicBool>,
|
||||
state: &Arc<RwLock<T>>,
|
||||
sleep: Duration
|
||||
) -> Usually<Task> {
|
||||
let state = state.clone();
|
||||
tui_setup()?;
|
||||
let mut backend = CrosstermBackend::new(output);
|
||||
let Size { width, height } = backend.size().expect("get size failed");
|
||||
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 });
|
||||
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() {
|
||||
tui_resize(&mut backend, &mut buffer_a, Rect { x: 0, y: 0, width, height });
|
||||
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());
|
||||
})?)
|
||||
}
|
||||
|
||||
pub struct Tui(pub Buffer, pub XYWH<u16>);
|
||||
impl Screen for Tui { type Unit = u16; }
|
||||
impl Deref for Tui { type Target = Buffer; fn deref (&self) -> &Buffer { &self.0 } }
|
||||
impl DerefMut for Tui { fn deref_mut (&mut self) -> &mut Buffer { &mut self.0 } }
|
||||
impl HasOrigin for Tui { fn origin (&self) -> Origin { Origin::NW } }
|
||||
impl X<u16> for Tui {
|
||||
fn x (&self) -> u16 { self.1.0 }
|
||||
fn w (&self) -> u16 { self.1.2 }
|
||||
}
|
||||
impl Y<u16> for Tui {
|
||||
fn y (&self) -> u16 { self.1.1 }
|
||||
fn h (&self) -> u16 { self.1.3 }
|
||||
impl AsMut<Buffer> for Tui { fn as_mut (&mut self) -> &mut Buffer { &mut self.0 } }
|
||||
impl Wide<u16> for Tui { fn w (&self) -> u16 { self.1.2 } }
|
||||
impl Tall<u16> for Tui { fn h (&self) -> u16 { self.1.3 } }
|
||||
impl HasOrigin for Tui { fn origin (&self) -> Azimuth { Azimuth::NW } }
|
||||
impl Xy<u16> for Tui { fn x (&self) -> u16 { self.1.0 } fn y (&self) -> u16 { self.1.1 } }
|
||||
|
||||
impl Tui {
|
||||
pub fn 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();
|
||||
}
|
||||
}
|
||||
|
||||
impl Tui {
|
||||
pub fn update (&mut self, callback: &impl Fn(&mut Cell, u16, u16)) -> XYWH<u16> {
|
||||
for row in 0..self.h() {
|
||||
|
|
@ -127,588 +99,57 @@ impl Tui {
|
|||
}
|
||||
}
|
||||
}
|
||||
/// Apply foreground color.
|
||||
pub const fn fg (fg: Color, draw: impl Draw<Tui>) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|{
|
||||
to.update(&|cell,_,_|{ cell.set_fg(fg); });
|
||||
draw.draw(to)
|
||||
})
|
||||
}
|
||||
/// Apply background color.
|
||||
pub const fn bg (bg: Color, draw: impl Draw<Tui>) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|{
|
||||
to.update(&|cell,_,_|{ cell.set_bg(bg); });
|
||||
draw.draw(to)
|
||||
})
|
||||
}
|
||||
pub const fn fg_bg (fg: Color, bg: Color, draw: impl Draw<Tui>) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|{
|
||||
to.update(&|cell,_,_|{ cell.set_fg(fg); cell.set_bg(bg); });
|
||||
draw.draw(to)
|
||||
})
|
||||
}
|
||||
pub const fn fill_char (c: char) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|Ok(to.update(&|cell,_,_|{
|
||||
cell.set_char(c);
|
||||
})))
|
||||
}
|
||||
/// Draw contents with modifier applied.
|
||||
pub const fn modify (on: bool, modifier: Modifier, draw: impl Draw<Tui>) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|{
|
||||
fill_mod(on, modifier).draw(to)?;
|
||||
draw.draw(to)
|
||||
})
|
||||
}
|
||||
pub const fn fill_mod (on: bool, modifier: Modifier) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|Ok({
|
||||
if on {
|
||||
to.update(&|cell,_,_|cell.modifier.insert(modifier))
|
||||
} else {
|
||||
to.update(&|cell,_,_|cell.modifier.remove(modifier))
|
||||
}
|
||||
}))
|
||||
}
|
||||
/// Draw contents with bold modifier applied.
|
||||
pub const fn bold (on: bool, draw: impl Draw<Tui>) -> impl Draw<Tui> {
|
||||
modify(on, Modifier::BOLD, draw)
|
||||
}
|
||||
pub const fn fill_ul (color: Option<Color>) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|Ok(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;
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
/// TUI works in u16 coordinates.
|
||||
impl Coord for u16 {
|
||||
fn plus (self, other: Self) -> Self {
|
||||
self.saturating_add(other)
|
||||
/// Implement standard [main] entrypoint for TUI apps.
|
||||
#[macro_export] macro_rules! tui_main {
|
||||
($state:expr) => {
|
||||
pub fn main () -> Usually<()> { tui_run_main(Arc::new(RwLock::new($state))) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Draw<Tui> for u64 {
|
||||
fn draw (self, _to: &mut Tui) -> Usually<XYWH<u16>> { todo!() }
|
||||
}
|
||||
impl Draw<Tui> for f64 {
|
||||
fn draw (self, _to: &mut Tui) -> Usually<XYWH<u16>> { todo!() }
|
||||
}
|
||||
|
||||
mod phat {
|
||||
use super::*;
|
||||
pub const LO: &'static str = "▄";
|
||||
pub const HI: &'static str = "▀";
|
||||
/// A phat line
|
||||
pub fn lo (fg: Color, bg: Color) -> impl Draw<Tui> {
|
||||
h_exact(1, fg_bg(fg, bg, x_repeat(self::phat::LO)))
|
||||
}
|
||||
/// A phat line
|
||||
pub fn hi (fg: Color, bg: Color) -> impl Draw<Tui> {
|
||||
h_exact(1, fg_bg(fg, bg, x_repeat(self::phat::HI)))
|
||||
}
|
||||
}
|
||||
|
||||
mod scroll {
|
||||
pub const ICON_DEC_V: &[char] = &['▲'];
|
||||
pub const ICON_INC_V: &[char] = &['▼'];
|
||||
pub const ICON_DEC_H: &[char] = &[' ', '🞀', ' '];
|
||||
pub const ICON_INC_H: &[char] = &[' ', '🞂', ' '];
|
||||
}
|
||||
|
||||
pub const fn x_repeat (c: &str) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|{
|
||||
let XYWH(x, y, w, h) = to.xywh();
|
||||
for x in x..x+w {
|
||||
if let Some(cell) = to.0.cell_mut(Position::from((x, y))) {
|
||||
cell.set_symbol(&c);
|
||||
}
|
||||
}
|
||||
Ok(XYWH(x, y, w, 1))
|
||||
pub fn tui_run_main <T> (state: Arc<RwLock<T>>) -> Usually<()> where
|
||||
T: View<Tui> + Apply<TuiEvent, Usually<()>> + Send + Sync + 'static
|
||||
{
|
||||
Exit::run(|exit|{
|
||||
let scan = Duration::from_millis(100);
|
||||
let frame = Duration::from_millis(10);
|
||||
let (input, output) = tui_io(exit.as_ref(), &state, scan, frame, std::io::stdout())?;
|
||||
output.join();
|
||||
tui_teardown(&mut stdout())
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn y_repeat (c: &str) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|{
|
||||
let XYWH(x, y, w, h) = to.xywh();
|
||||
for y in y..y+h {
|
||||
if let Some(cell) = to.0.cell_mut(Position::from((x, y))) {
|
||||
cell.set_symbol(&c);
|
||||
}
|
||||
}
|
||||
Ok(XYWH(x, y, 1, h))
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn xy_repeat (c: &str) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|{
|
||||
let XYWH(x, y, w, h) = to.xywh();
|
||||
let a = c.len();
|
||||
for (_v, y) in (y..y+h).enumerate() {
|
||||
for (u, x) in (x..x+w).enumerate() {
|
||||
if let Some(cell) = to.0.cell_mut(Position::from((x, y))) {
|
||||
let u = u % a;
|
||||
cell.set_symbol(&c[u..u+1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(XYWH(x, y, w, h))
|
||||
})
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let _ = tengri::button_2("", "", true);
|
||||
/// let _ = tengri::button_2("", "", false);
|
||||
/// ```
|
||||
pub const fn button_2 <'a> (key: impl Draw<Tui>, label: impl Draw<Tui>, hide: bool) -> impl Draw<Tui> {
|
||||
let c1 = tui_orange();
|
||||
let c2 = tui_g(0);
|
||||
let c3 = tui_g(96);
|
||||
let c4 = tui_g(255);
|
||||
bold(true, fg_bg(c1, c2, east(fg(c2, east(key, fg(c3, "▐"))), when(!hide, fg_bg(c4, c3, label)))))
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let _ = tengri::button_3("", "", "", true);
|
||||
/// let _ = tengri::button_3("", "", "", false);
|
||||
/// ```
|
||||
pub const fn button_3 <'a> (
|
||||
key: impl Draw<Tui>, label: impl Draw<Tui>, value: impl Draw<Tui>, editing: bool,
|
||||
) -> impl Draw<Tui> {
|
||||
bold(true, east(
|
||||
fg_bg(tui_orange(), tui_g(0),
|
||||
east(fg(tui_g(0), "▐"), east(key, fg(if editing { tui_g(128) } else { tui_g(96) }, "▐")))),
|
||||
east(
|
||||
when(!editing, east(fg_bg(tui_g(255), tui_g(96), label), fg_bg(tui_g(128), tui_g(96), "▐"),)),
|
||||
east(fg_bg(tui_g(224), tui_g(128), value), fg_bg(tui_g(128), Reset, "▌"), ))))
|
||||
}
|
||||
|
||||
macro_rules! border {
|
||||
($($T:ident {
|
||||
$nw:literal $n:literal $ne:literal $w:literal $e:literal $sw:literal $s:literal $se:literal
|
||||
$($x:tt)*
|
||||
}),+) => {$(
|
||||
impl BorderStyle for $T {
|
||||
const NW: &'static str = $nw;
|
||||
const N: &'static str = $n;
|
||||
const NE: &'static str = $ne;
|
||||
const W: &'static str = $w;
|
||||
const E: &'static str = $e;
|
||||
const SW: &'static str = $sw;
|
||||
const S: &'static str = $s;
|
||||
const SE: &'static str = $se;
|
||||
$($x)*
|
||||
fn enabled (&self) -> bool { self.0 }
|
||||
}
|
||||
#[derive(Copy, Clone)] pub struct $T(pub bool, pub Style);
|
||||
//impl Layout<Tui> for $T {}
|
||||
impl Draw<Tui> for $T {
|
||||
fn draw (self, to: &mut Tui) -> Usually<XYWH<u16>> {
|
||||
when(self.enabled(), thunk(|to: &mut Tui|BorderStyle::draw(self, to))).draw(to)
|
||||
}
|
||||
}
|
||||
)+}
|
||||
}
|
||||
|
||||
border! {
|
||||
Square {
|
||||
"┌" "─" "┐"
|
||||
"│" "│"
|
||||
"└" "─" "┘" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
SquareBold {
|
||||
"┏" "━" "┓"
|
||||
"┃" "┃"
|
||||
"┗" "━" "┛" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
TabLike {
|
||||
"╭" "─" "╮"
|
||||
"│" "│"
|
||||
"│" " " "│" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Lozenge {
|
||||
"╭" "─" "╮"
|
||||
"│" "│"
|
||||
"╰" "─" "╯" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Brace {
|
||||
"╭" "" "╮"
|
||||
"│" "│"
|
||||
"╰" "" "╯" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
LozengeDotted {
|
||||
"╭" "┅" "╮"
|
||||
"┇" "┇"
|
||||
"╰" "┅" "╯" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Quarter {
|
||||
"▎" "▔" "🮇"
|
||||
"▎" "🮇"
|
||||
"▎" "▁" "🮇" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
QuarterV {
|
||||
"▎" "" "🮇"
|
||||
"▎" "🮇"
|
||||
"▎" "" "🮇" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Chamfer {
|
||||
"🭂" "▔" "🭍"
|
||||
"▎" "🮇"
|
||||
"🭓" "▁" "🭞" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Corners {
|
||||
"🬆" "" "🬊" // 🬴 🬸
|
||||
"" ""
|
||||
"🬱" "" "🬵" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
CornersTall {
|
||||
"🭽" "" "🭾"
|
||||
"" ""
|
||||
"🭼" "" "🭿" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Outer {
|
||||
"🭽" "▔" "🭾"
|
||||
"▏" "▕"
|
||||
"🭼" "▁" "🭿"
|
||||
const W0: &'static str = "[";
|
||||
const E0: &'static str = "]";
|
||||
const N0: &'static str = "⎴";
|
||||
const S0: &'static str = "⎵";
|
||||
fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Thick {
|
||||
"▄" "▄" "▄"
|
||||
"█" "█"
|
||||
"▀" "▀" "▀"
|
||||
fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Rugged {
|
||||
"▄" "▂" "▄"
|
||||
"▐" "▌"
|
||||
"▀" "🮂" "▀"
|
||||
fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Skinny {
|
||||
"▗" "▄" "▖"
|
||||
"▐" "▌"
|
||||
"▝" "▀" "▘"
|
||||
fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Brackets {
|
||||
"⎡" "" "⎤"
|
||||
"" ""
|
||||
"⎣" "" "⎦"
|
||||
const W0: &'static str = "[";
|
||||
const E0: &'static str = "]";
|
||||
const N0: &'static str = "⎴";
|
||||
const S0: &'static str = "⎵";
|
||||
fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Reticle {
|
||||
"⎡" "" "⎤"
|
||||
"" ""
|
||||
"⎣" "" "⎦"
|
||||
const W0: &'static str = "╟";
|
||||
const E0: &'static str = "╢";
|
||||
const N0: &'static str = "┯";
|
||||
const S0: &'static str = "┷";
|
||||
fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
}
|
||||
}
|
||||
|
||||
pub trait BorderStyle: Draw<Tui> + Copy {
|
||||
fn enabled (&self) -> bool;
|
||||
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<XYWH<u16>> {
|
||||
if self.enabled() {
|
||||
self.draw_h(to, None)?;
|
||||
self.draw_v(to, None)?;
|
||||
self.draw_c(to, None)?;
|
||||
}
|
||||
Ok(to.1)
|
||||
}
|
||||
#[inline] fn draw_h (self, to: &mut Tui, style: Option<Style>) -> Usually<XYWH<u16>> {
|
||||
let style = style.or_else(||self.style_horizontal());
|
||||
let y1 = to.y_north();
|
||||
let y2 = to.y_south().saturating_sub(1);
|
||||
for x in to.x_west()..to.x_east().saturating_sub(1) {
|
||||
to.blit(&Self::N, x, y1, style);
|
||||
to.blit(&Self::S, x, y2, style)
|
||||
}
|
||||
Ok(to.xywh())
|
||||
}
|
||||
#[inline] fn draw_v (self, to: &mut Tui, style: Option<Style>) -> Usually<XYWH<u16>> {
|
||||
let area = to.1;
|
||||
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 Tui, style: Option<Style>) -> Usually<XYWH<u16>> {
|
||||
let area = to.1;
|
||||
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() }
|
||||
|
||||
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 = "";
|
||||
}
|
||||
|
||||
/// Stackably padded.
|
||||
///
|
||||
/// ```
|
||||
/// /// TODO
|
||||
/// ```
|
||||
pub fn phat (w: u16, h: u16, [fg, bg, hi, lo]: [Color;4], draw: impl Draw<Tui>) -> impl Draw<Tui> {
|
||||
let top = w_exact(1, self::phat::lo(bg, hi));
|
||||
let low = w_exact(1, self::phat::hi(bg, lo));
|
||||
let draw = fg_bg(fg, bg, draw);
|
||||
wh_min(Some(w), Some(h), south(top, north(low, draw)))
|
||||
}
|
||||
|
||||
fn x_scroll () -> impl Draw<Tui> {
|
||||
thunk(|Tui(buf, XYWH(x1, y1, w, h)): &mut Tui|{
|
||||
let x2 = *x1 + *w;
|
||||
for (i, x) in (*x1..=x2).enumerate() {
|
||||
if let Some(cell) = buf.cell_mut(Position::from((x, *y1))) {
|
||||
if i < (self::scroll::ICON_DEC_H.len()) {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Rgb(0, 0, 0));
|
||||
cell.set_char(self::scroll::ICON_DEC_H[i as usize]);
|
||||
} else if i > (*w as usize - self::scroll::ICON_INC_H.len()) {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Rgb(0, 0, 0));
|
||||
cell.set_char(self::scroll::ICON_INC_H[*w as usize - i]);
|
||||
} else if false {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Reset);
|
||||
cell.set_char('━');
|
||||
} else {
|
||||
cell.set_fg(Rgb(0, 0, 0));
|
||||
cell.set_bg(Reset);
|
||||
cell.set_char('╌');
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(XYWH(*x1, *y1, *w, 1))
|
||||
})
|
||||
}
|
||||
|
||||
fn y_scroll () -> impl Draw<Tui> {
|
||||
thunk(|Tui(buf, XYWH(x1, y1, w, h)): &mut Tui|{
|
||||
let y2 = *y1 + *h;
|
||||
for (i, y) in (*y1..=y2).enumerate() {
|
||||
if let Some(cell) = buf.cell_mut(Position::from((*x1, y))) {
|
||||
if (i as usize) < (self::scroll::ICON_DEC_V.len()) {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Rgb(0, 0, 0));
|
||||
cell.set_char(self::scroll::ICON_DEC_V[i as usize]);
|
||||
} else if (i as usize) > (*h as usize - self::scroll::ICON_INC_V.len()) {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Rgb(0, 0, 0));
|
||||
cell.set_char(self::scroll::ICON_INC_V[*h as usize - i]);
|
||||
} else if false {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Reset);
|
||||
cell.set_char('‖'); // ━
|
||||
} else {
|
||||
cell.set_fg(Rgb(0, 0, 0));
|
||||
cell.set_bg(Reset);
|
||||
cell.set_char('╎'); // ━
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(XYWH(*x1, *y1, 1, *h))
|
||||
})
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
pub fn tui_setup () -> 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)?;
|
||||
CrosstermBackend::new(stdout()).hide_cursor()?;
|
||||
enable_raw_mode().map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn tui_resize <W: Write> (
|
||||
backend: &mut CrosstermBackend<W>,
|
||||
buffer: &mut Buffer,
|
||||
size: Rect
|
||||
) {
|
||||
if buffer.area != size {
|
||||
backend.clear_region(ClearType::All).unwrap();
|
||||
buffer.resize(size);
|
||||
buffer.reset();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tui_teardown <W: Write> (backend: &mut CrosstermBackend<W>) -> Usually<()> {
|
||||
pub fn tui_teardown <W: Write> (backend: &mut W) -> Usually<()> {
|
||||
use ::ratatui::backend::Backend;
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
backend.show_cursor()?;
|
||||
CrosstermBackend::new(backend).show_cursor()?;
|
||||
disable_raw_mode().map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn tui_update (
|
||||
Tui(buf, ..): &mut Tui, area: XYWH<u16>, callback: &impl Fn(&mut Cell, u16, u16)
|
||||
) {
|
||||
}
|
||||
|
||||
/// Draw border around shrinked item.
|
||||
///
|
||||
/// ```
|
||||
/// /// TODO
|
||||
/// ```
|
||||
pub const fn border <T, S: BorderStyle> (on: bool, style: S, draw: impl Draw<Tui>) -> impl Draw<Tui> {
|
||||
let content = wh_pad(1, 1, draw);
|
||||
let outline = when(on, thunk(move|to: &mut Tui|{
|
||||
let XYWH(x, y, w, h) = to.1;
|
||||
if w > 0 && h > 0 {
|
||||
to.blit(&style.border_nw(), x, y, style.style());
|
||||
to.blit(&style.border_ne(), x + w - 1, y, style.style());
|
||||
to.blit(&style.border_sw(), x, y + h - 1, style.style());
|
||||
to.blit(&style.border_se(), x + w - 1, y + h - 1, style.style());
|
||||
for x in x+1..x+w-1 {
|
||||
to.blit(&style.border_n(), x, y, style.style());
|
||||
to.blit(&style.border_s(), x, y + h - 1, style.style());
|
||||
}
|
||||
for y in y+1..y+h-1 {
|
||||
to.blit(&style.border_w(), x, y, style.style());
|
||||
to.blit(&style.border_e(), x + w - 1, y, style.style());
|
||||
}
|
||||
}
|
||||
Ok(XYWH(x, y, w, h))
|
||||
pub fn tui_setup_panic () {
|
||||
use ::std::panic::{set_hook, PanicHookInfo};
|
||||
use ::better_panic::{Settings, Verbosity};
|
||||
let panic = Settings::auto()
|
||||
.verbosity(Verbosity::Full)
|
||||
.create_panic_handler();
|
||||
set_hook(Box::new(move |info: &PanicHookInfo|{
|
||||
let _ = tui_teardown(&mut stdout());
|
||||
panic(info);
|
||||
}));
|
||||
above(outline, content)
|
||||
}
|
||||
|
||||
/// Draw TUI content or its error message.
|
||||
///
|
||||
/// ```
|
||||
/// let _ = tengri::tui::catcher(Ok(Some("hello")));
|
||||
/// let _ = tengri::tui::catcher(Ok(None));
|
||||
/// let _ = tengri::tui::catcher(Err("draw fail".into()));
|
||||
/// ```
|
||||
pub fn catcher <T: Draw<Tui>> (result: Usually<T>) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|match result {
|
||||
Ok(content) => content.draw(to),
|
||||
Err(e) => {
|
||||
let err_fg = Color::Rgb(255,224,244);
|
||||
let err_bg = Color::Rgb(96, 24, 24);
|
||||
let title = east(bold(true, "upsi daisy. "), "rendering failed.");
|
||||
let error = east("\"why?\" ", bold(true, format!("{e}")));
|
||||
fg(err_fg, bg(err_bg, south(title, error))).draw(to)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// TUI buffer sized by `usize` instead of `u16`.
|
||||
#[derive(Default)]
|
||||
pub struct BigBuffer {
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub content: Vec<Cell>
|
||||
}
|
||||
impl_from!(BigBuffer: |size:(usize, usize)| Self::new(size.0, size.1));
|
||||
impl_debug!(BigBuffer |self, f| { write!(f, "[BB {}x{} ({})]", self.width, self.height, self.content.len()) });
|
||||
impl BigBuffer {
|
||||
pub fn new (width: usize, height: usize) -> Self {
|
||||
Self { width, height, content: vec![Cell::default(); width*height] }
|
||||
}
|
||||
pub fn get (&self, x: usize, y: usize) -> Option<&Cell> {
|
||||
let i = self.index_of(x, y);
|
||||
self.content.get(i)
|
||||
}
|
||||
pub fn get_mut (&mut self, x: usize, y: usize) -> Option<&mut Cell> {
|
||||
let i = self.index_of(x, y);
|
||||
self.content.get_mut(i)
|
||||
}
|
||||
pub fn index_of (&self, x: usize, y: usize) -> usize {
|
||||
y * self.width + x
|
||||
}
|
||||
}
|
||||
|
||||
use self::colors::*; mod colors {
|
||||
use ratatui::prelude::Color;
|
||||
pub const fn tui_color_bg () -> Color { Color::Rgb(28, 35, 25) }
|
||||
pub const fn tui_bg0 () -> Color { Color::Rgb(20, 20, 20) }
|
||||
pub const fn tui_bo1 () -> Color { Color::Rgb(100, 110, 40) }
|
||||
pub const fn tui_bo2 () -> Color { Color::Rgb(70, 80, 50) }
|
||||
pub const fn tui_border_bg () -> Color { Color::Rgb(40, 50, 30) }
|
||||
pub const fn tui_border_fg (f: bool) -> Color { if f { tui_bo1() } else { tui_bo2() } }
|
||||
pub const fn tui_brown () -> Color { Color::Rgb(128,255,0) }
|
||||
pub const fn tui_electric () -> Color { Color::Rgb(0,255,128) }
|
||||
pub const fn tui_g (g: u8) -> Color { Color::Rgb(g, g, g) }
|
||||
pub const fn tui_green () -> Color { Color::Rgb(0,255,0) }
|
||||
pub const fn tui_mode_bg () -> Color { Color::Rgb(150, 160, 90) }
|
||||
pub const fn tui_mode_fg () -> Color { Color::Rgb(255, 255, 255) }
|
||||
pub const fn tui_null () -> Color { Color::Reset }
|
||||
pub const fn tui_orange () -> Color { Color::Rgb(255,128,0) }
|
||||
pub const fn tui_red () -> Color { Color::Rgb(255,0, 0) }
|
||||
pub const fn tui_separator_fg (_: bool) -> Color { Color::Rgb(0, 0, 0) }
|
||||
pub const fn tui_status_bar_bg () -> Color { Color::Rgb(28, 35, 25) }
|
||||
pub const fn tui_ti1 () -> Color { Color::Rgb(150, 160, 90) }
|
||||
pub const fn tui_ti2 () -> Color { Color::Rgb(120, 130, 100) }
|
||||
pub const fn tui_title_fg (f: bool) -> Color { if f { tui_ti1() } else { tui_ti2() } }
|
||||
pub const fn tui_yellow () -> Color { Color::Rgb(255,255,0) }
|
||||
/// Spawn the TUI input and output threadsl.
|
||||
pub fn tui_io <
|
||||
T: View<Tui> + Apply<TuiEvent, Usually<()>> + Send + Sync + 'static,
|
||||
W: Write + Send + Sync + 'static,
|
||||
> (
|
||||
exited: &Arc<AtomicBool>,
|
||||
state: &Arc<RwLock<T>>,
|
||||
poll: Duration,
|
||||
sleep: Duration,
|
||||
output: W,
|
||||
) -> Result<(Task, Task), Box<dyn Error>> {
|
||||
let keyboard = tui_input(exited, state, poll)?;
|
||||
let terminal = tui_output(exited, state, sleep, output)?;
|
||||
Ok((keyboard, terminal))
|
||||
}
|
||||
|
|
|
|||
239
src/term/border.rs
Normal file
239
src/term/border.rs
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
use super::*;
|
||||
|
||||
/// Draw border around item shrunk by 1 on each side.
|
||||
///
|
||||
/// ```
|
||||
/// /// TODO
|
||||
/// ```
|
||||
pub fn border (on: bool, style: impl BorderStyle, draw: impl Draw<Tui>) -> impl Draw<Tui> {
|
||||
let content = draw.pad_wh(1, 1);
|
||||
let outline = when(on, thunk(move|to: &mut Tui|{
|
||||
let XYWH(x, y, w, h) = to.1;
|
||||
if w > 0 && h > 0 {
|
||||
to.blit(&style.border_nw(), x, y, style.style());
|
||||
to.blit(&style.border_ne(), x + w - 1, y, style.style());
|
||||
to.blit(&style.border_sw(), x, y + h - 1, style.style());
|
||||
to.blit(&style.border_se(), x + w - 1, y + h - 1, style.style());
|
||||
for x in x+1..x+w-1 {
|
||||
to.blit(&style.border_n(), x, y, style.style());
|
||||
to.blit(&style.border_s(), x, y + h - 1, style.style());
|
||||
}
|
||||
for y in y+1..y+h-1 {
|
||||
to.blit(&style.border_w(), x, y, style.style());
|
||||
to.blit(&style.border_e(), x + w - 1, y, style.style());
|
||||
}
|
||||
}
|
||||
Ok(Some(XYWH(x, y, w, h)))
|
||||
}));
|
||||
above(outline, content)
|
||||
}
|
||||
|
||||
macro_rules! border {
|
||||
($($T:ident {
|
||||
$nw:literal $n:literal $ne:literal $w:literal $e:literal $sw:literal $s:literal $se:literal
|
||||
$($x:tt)*
|
||||
}),+) => {$(
|
||||
impl BorderStyle for $T {
|
||||
const NW: &'static str = $nw;
|
||||
const N: &'static str = $n;
|
||||
const NE: &'static str = $ne;
|
||||
const W: &'static str = $w;
|
||||
const E: &'static str = $e;
|
||||
const SW: &'static str = $sw;
|
||||
const S: &'static str = $s;
|
||||
const SE: &'static str = $se;
|
||||
$($x)*
|
||||
fn enabled (&self) -> bool { self.0 }
|
||||
}
|
||||
#[derive(Copy, Clone)] pub struct $T(pub bool, pub Style);
|
||||
//impl Layout<Tui> for $T {}
|
||||
impl Draw<Tui> for $T {
|
||||
fn draw (self, to: &mut Tui) -> Perhaps<XYWH<u16>> {
|
||||
when(self.enabled(), thunk(|to: &mut Tui|BorderStyle::draw(self, to).map(Some))).draw(to)
|
||||
}
|
||||
}
|
||||
)+}
|
||||
}
|
||||
|
||||
border! {
|
||||
Square {
|
||||
"┌" "─" "┐"
|
||||
"│" "│"
|
||||
"└" "─" "┘" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
SquareBold {
|
||||
"┏" "━" "┓"
|
||||
"┃" "┃"
|
||||
"┗" "━" "┛" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
TabLike {
|
||||
"╭" "─" "╮"
|
||||
"│" "│"
|
||||
"│" " " "│" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Lozenge {
|
||||
"╭" "─" "╮"
|
||||
"│" "│"
|
||||
"╰" "─" "╯" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Brace {
|
||||
"╭" "" "╮"
|
||||
"│" "│"
|
||||
"╰" "" "╯" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
LozengeDotted {
|
||||
"╭" "┅" "╮"
|
||||
"┇" "┇"
|
||||
"╰" "┅" "╯" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Quarter {
|
||||
"▎" "▔" "🮇"
|
||||
"▎" "🮇"
|
||||
"▎" "▁" "🮇" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
QuarterV {
|
||||
"▎" "" "🮇"
|
||||
"▎" "🮇"
|
||||
"▎" "" "🮇" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Chamfer {
|
||||
"🭂" "▔" "🭍"
|
||||
"▎" "🮇"
|
||||
"🭓" "▁" "🭞" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Corners {
|
||||
"🬆" "" "🬊" // 🬴 🬸
|
||||
"" ""
|
||||
"🬱" "" "🬵" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
CornersTall {
|
||||
"🭽" "" "🭾"
|
||||
"" ""
|
||||
"🭼" "" "🭿" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Outer {
|
||||
"🭽" "▔" "🭾"
|
||||
"▏" "▕"
|
||||
"🭼" "▁" "🭿"
|
||||
const W0: &'static str = "[";
|
||||
const E0: &'static str = "]";
|
||||
const N0: &'static str = "⎴";
|
||||
const S0: &'static str = "⎵";
|
||||
fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Thick {
|
||||
"▄" "▄" "▄"
|
||||
"█" "█"
|
||||
"▀" "▀" "▀"
|
||||
fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Rugged {
|
||||
"▄" "▂" "▄"
|
||||
"▐" "▌"
|
||||
"▀" "🮂" "▀"
|
||||
fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Skinny {
|
||||
"▗" "▄" "▖"
|
||||
"▐" "▌"
|
||||
"▝" "▀" "▘"
|
||||
fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Brackets {
|
||||
"⎡" "" "⎤"
|
||||
"" ""
|
||||
"⎣" "" "⎦"
|
||||
const W0: &'static str = "[";
|
||||
const E0: &'static str = "]";
|
||||
const N0: &'static str = "⎴";
|
||||
const S0: &'static str = "⎵";
|
||||
fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Reticle {
|
||||
"⎡" "" "⎤"
|
||||
"" ""
|
||||
"⎣" "" "⎦"
|
||||
const W0: &'static str = "╟";
|
||||
const E0: &'static str = "╢";
|
||||
const N0: &'static str = "┯";
|
||||
const S0: &'static str = "┷";
|
||||
fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
}
|
||||
}
|
||||
|
||||
pub trait BorderStyle: Draw<Tui> + Copy {
|
||||
fn enabled (&self) -> bool;
|
||||
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<XYWH<u16>> {
|
||||
if self.enabled() {
|
||||
self.draw_h(to, None)?;
|
||||
self.draw_v(to, None)?;
|
||||
self.draw_c(to, None)?;
|
||||
}
|
||||
Ok(to.1)
|
||||
}
|
||||
#[inline] fn draw_h (self, to: &mut Tui, style: Option<Style>) -> Usually<XYWH<u16>> {
|
||||
let style = style.or_else(||self.style_horizontal());
|
||||
let y1 = to.y_north();
|
||||
let y2 = to.y_south().saturating_sub(1);
|
||||
for x in to.x_west()..to.x_east().saturating_sub(1) {
|
||||
to.blit(&Self::N, x, y1, style);
|
||||
to.blit(&Self::S, x, y2, style)
|
||||
}
|
||||
Ok(to.xywh())
|
||||
}
|
||||
#[inline] fn draw_v (self, to: &mut Tui, style: Option<Style>) -> Usually<XYWH<u16>> {
|
||||
let area = to.1;
|
||||
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 Tui, style: Option<Style>) -> Usually<XYWH<u16>> {
|
||||
let area = to.1;
|
||||
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() }
|
||||
|
||||
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 = "";
|
||||
}
|
||||
31
src/term/buffer.rs
Normal file
31
src/term/buffer.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
use crate::*;
|
||||
use crate::{*, lang::*, draw::*, task::*, exit::*};
|
||||
use ::ratatui::buffer::Cell;
|
||||
|
||||
/// TUI buffer sized by `usize` instead of `u16`.
|
||||
#[derive(Default)] pub struct BigBuffer {
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub content: Vec<Cell>
|
||||
}
|
||||
|
||||
impl_from!(BigBuffer: |size:(usize, usize)| Self::new(size.0, size.1));
|
||||
|
||||
impl_debug!(BigBuffer |self, f| { write!(f, "[BB {}x{} ({})]", self.width, self.height, self.content.len()) });
|
||||
|
||||
impl BigBuffer {
|
||||
pub fn new (width: usize, height: usize) -> Self {
|
||||
Self { width, height, content: vec![Cell::default(); width*height] }
|
||||
}
|
||||
pub fn get (&self, x: usize, y: usize) -> Option<&Cell> {
|
||||
let i = self.index_of(x, y);
|
||||
self.content.get(i)
|
||||
}
|
||||
pub fn get_mut (&mut self, x: usize, y: usize) -> Option<&mut Cell> {
|
||||
let i = self.index_of(x, y);
|
||||
self.content.get_mut(i)
|
||||
}
|
||||
pub fn index_of (&self, x: usize, y: usize) -> usize {
|
||||
y * self.width + x
|
||||
}
|
||||
}
|
||||
25
src/term/event.rs
Normal file
25
src/term/event.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
use ::dizzle::impl_from;
|
||||
use ::crossterm::event::{Event, KeyEvent, KeyCode, KeyModifiers};
|
||||
|
||||
/// TUI input loop event.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)]
|
||||
pub struct TuiEvent(
|
||||
/// Wrapped [crossterm::event::Event].
|
||||
pub Event
|
||||
);
|
||||
|
||||
impl_from!(TuiEvent: |e: Event| TuiEvent(
|
||||
e));
|
||||
impl_from!(TuiEvent: |e: KeyEvent| TuiEvent(
|
||||
Event::Key(e)));
|
||||
impl_from!(TuiEvent: |c: KeyCode| TuiEvent(
|
||||
Event::Key(KeyEvent::new(c, KeyModifiers::NONE))));
|
||||
impl_from!(TuiEvent: |c: char| TuiEvent(
|
||||
Event::Key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE))));
|
||||
|
||||
impl Ord for TuiEvent {
|
||||
fn cmp (&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.partial_cmp(other)
|
||||
.unwrap_or_else(||format!("{:?}", self).cmp(&format!("{other:?}"))) // FIXME perf
|
||||
}
|
||||
}
|
||||
39
src/term/input.rs
Normal file
39
src/term/input.rs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
use crate::{*, lang::*};
|
||||
|
||||
/// Enable TUI keyboard input for main state struct.
|
||||
#[macro_export] macro_rules! tui_keys {
|
||||
(|$self:ident:$State:ty,$input:ident|$body:block) => {
|
||||
impl Apply<TuiEvent, Usually<()>> for $State {
|
||||
fn apply (&mut $self, $input: &TuiEvent) -> Usually<()> $body
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Spawn the TUI input thread which reads keys from the terminal.
|
||||
pub fn tui_input <T: Apply<TuiEvent, Usually<()>> + Send + Sync + 'static> (
|
||||
exited: &Arc<AtomicBool>, state: &Arc<RwLock<T>>, poll: Duration
|
||||
) -> Result<Task, std::io::Error> {
|
||||
let exited = exited.clone();
|
||||
let state = state.clone();
|
||||
Task::new_poll(exited.clone(), poll, move |_| {
|
||||
let event = read().unwrap();
|
||||
match event {
|
||||
|
||||
// Hardcoded exit.
|
||||
Event::Key(KeyEvent {
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
code: KeyCode::Char('c'),
|
||||
kind: KeyEventKind::Press,
|
||||
state: KeyEventState::NONE
|
||||
}) => { exited.store(true, Relaxed); },
|
||||
|
||||
// Handle all other events by the state:
|
||||
event => {
|
||||
if let Err(e) = state.write().unwrap().apply(&TuiEvent(event)) {
|
||||
panic!("{e}")
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
114
src/term/keys.rs
Normal file
114
src/term/keys.rs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
use crate::{task::Task, term::TuiEvent};
|
||||
use ::std::sync::{Arc, RwLock, atomic::{AtomicBool, Ordering::*}};
|
||||
use ::std::time::Duration;
|
||||
use ::dizzle::{Language, Symbol, Usually, Apply};
|
||||
use ::crossterm::event::{
|
||||
read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState
|
||||
};
|
||||
|
||||
/// TUI key spec.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)]
|
||||
pub struct TuiKey(pub Option<KeyCode>, pub KeyModifiers);
|
||||
|
||||
impl Ord for TuiKey {
|
||||
fn cmp (&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.partial_cmp(other)
|
||||
.unwrap_or_else(||format!("{:?}", self).cmp(&format!("{other:?}"))) // FIXME perf
|
||||
}
|
||||
}
|
||||
|
||||
impl TuiKey {
|
||||
const SPLIT: char = '/';
|
||||
pub fn from_crossterm (event: KeyEvent) -> Self {
|
||||
Self(Some(event.code), event.modifiers)
|
||||
}
|
||||
pub fn to_crossterm (&self) -> Option<Event> {
|
||||
self.0.map(|code|Event::Key(KeyEvent {
|
||||
code,
|
||||
modifiers: self.1,
|
||||
kind: KeyEventKind::Press,
|
||||
state: KeyEventState::NONE,
|
||||
}))
|
||||
}
|
||||
pub fn named (token: &str) -> Option<KeyCode> {
|
||||
use KeyCode::*;
|
||||
Some(match token {
|
||||
"up" => Up,
|
||||
"down" => Down,
|
||||
"left" => Left,
|
||||
"right" => Right,
|
||||
"esc" | "escape" => Esc,
|
||||
"enter" | "return" => Enter,
|
||||
"delete" | "del" => Delete,
|
||||
"backspace" => Backspace,
|
||||
"tab" => Tab,
|
||||
"space" => Char(' '),
|
||||
"comma" => Char(','),
|
||||
"period" => Char('.'),
|
||||
"plus" => Char('+'),
|
||||
"minus" | "dash" => Char('-'),
|
||||
"equal" | "equals" => Char('='),
|
||||
"underscore" => Char('_'),
|
||||
"backtick" => Char('`'),
|
||||
"lt" => Char('<'),
|
||||
"gt" => Char('>'),
|
||||
"cbopen" | "openbrace" => Char('{'),
|
||||
"cbclose" | "closebrace" => Char('}'),
|
||||
"bropen" | "openbracket" => Char('['),
|
||||
"brclose" | "closebracket" => Char(']'),
|
||||
"pgup" | "pageup" => PageUp,
|
||||
"pgdn" | "pagedown" => PageDown,
|
||||
"f1" => F(1),
|
||||
"f2" => F(2),
|
||||
"f3" => F(3),
|
||||
"f4" => F(4),
|
||||
"f5" => F(5),
|
||||
"f6" => F(6),
|
||||
"f7" => F(7),
|
||||
"f8" => F(8),
|
||||
"f9" => F(9),
|
||||
"f10" => F(10),
|
||||
"f11" => F(11),
|
||||
"f12" => F(12),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "lang")]
|
||||
pub fn from_dsl (dsl: impl Language) -> Usually<Self> {
|
||||
if let Some(word) = dsl.word()? {
|
||||
let word = word.trim();
|
||||
Ok(if word == ":char" {
|
||||
Self(None, KeyModifiers::NONE)
|
||||
} else if word.chars().nth(0) == Some('@') {
|
||||
let mut key = None;
|
||||
let mut modifiers = KeyModifiers::NONE;
|
||||
let mut tokens = word[1..].split(Self::SPLIT).peekable();
|
||||
while let Some(token) = tokens.next() {
|
||||
if tokens.peek().is_some() {
|
||||
match token {
|
||||
"ctrl" | "Ctrl" | "c" | "C" => modifiers |= KeyModifiers::CONTROL,
|
||||
"alt" | "Alt" | "m" | "M" => modifiers |= KeyModifiers::ALT,
|
||||
"shift" | "Shift" | "s" | "S" => {
|
||||
modifiers |= KeyModifiers::SHIFT;
|
||||
// + TODO normalize character case, BackTab, etc.
|
||||
},
|
||||
_ => panic!("unknown modifier {token}"),
|
||||
}
|
||||
} else {
|
||||
key = Some(if token.len() == 1 {
|
||||
KeyCode::Char(token.chars().next().unwrap())
|
||||
} else {
|
||||
Self::named(token).unwrap_or_else(||panic!("unknown character {token}"))
|
||||
})
|
||||
}
|
||||
}
|
||||
Self(key, modifiers)
|
||||
} else {
|
||||
return Err(format!("TuiKey: unexpected: {word}").into())
|
||||
})
|
||||
} else {
|
||||
return Err(format!("TuiKey: unspecified").into())
|
||||
}
|
||||
}
|
||||
}
|
||||
348
src/term/output.rs
Normal file
348
src/term/output.rs
Normal file
|
|
@ -0,0 +1,348 @@
|
|||
use crate::*;
|
||||
use ratatui::{prelude::{Style, Position, Backend, Color}};
|
||||
|
||||
/// TUI works in u16 coordinates.
|
||||
impl Coord for u16 { fn plus (self, other: Self) -> Self { self.saturating_add(other) } }
|
||||
|
||||
impl Screen for Tui {
|
||||
type Unit = u16;
|
||||
/// Render drawable in subarea specified by `area`
|
||||
fn show <'t, T: Draw<Self>> (&mut self, content: T) -> 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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable TUI output for state struct.
|
||||
#[macro_export] macro_rules! tui_view {
|
||||
(|$self:ident: $State:ty|$body:block) => {
|
||||
impl View<Tui> for $State {
|
||||
fn view (&$self) -> impl Draw<Tui> $body
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn the TUI output thread which writes colored characters to the terminal.
|
||||
///
|
||||
/// ```
|
||||
/// let state = std::sync::Arc::new(std::sync::RwLock::new(()));
|
||||
/// let _ = tengri::Exit::run(|exit|{
|
||||
/// tengri::tui_output(
|
||||
/// exit.as_ref(),
|
||||
/// &state,
|
||||
/// std::time::Duration::from_millis(10),
|
||||
/// std::io::stdout()
|
||||
/// )
|
||||
/// });
|
||||
/// ```
|
||||
pub fn tui_output <
|
||||
W: Write + Send + Sync + 'static, T: View<Tui> + Send + Sync + 'static
|
||||
> (
|
||||
exited: &Arc<AtomicBool>,
|
||||
state: &Arc<RwLock<T>>,
|
||||
sleep: Duration,
|
||||
output: W,
|
||||
) -> Usually<Task> {
|
||||
let state = state.clone();
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
CrosstermBackend::new(stdout()).hide_cursor()?;
|
||||
enable_raw_mode()?;
|
||||
let mut backend = CrosstermBackend::new(output);
|
||||
let Size { width, height } = backend.size().expect("get size failed");
|
||||
let mut prev = Tui::new(width, height);
|
||||
let mut next = Tui::new(width, height);
|
||||
Ok(Task::new_sleep(exited.clone(), sleep, move |perf| {
|
||||
let Size { width, height } = backend.size().expect("get size failed");
|
||||
if let Ok(state) = state.try_read() {
|
||||
prev.resize(&mut backend, width, height);
|
||||
state.view().draw(&mut next).expect("draw failed"); // TODO draw error
|
||||
prev.redraw(&mut backend, &mut next);
|
||||
}
|
||||
let timer = format!("{:>3.3}ms", perf.used.load(Relaxed));
|
||||
prev.set_string(0, 0, &timer, Style::default());
|
||||
})?)
|
||||
}
|
||||
|
||||
pub use self::colors::*; mod colors {
|
||||
use ratatui::prelude::Color;
|
||||
pub const fn tui_color_bg () -> Color { Color::Rgb(28, 35, 25) }
|
||||
pub const fn tui_bg0 () -> Color { Color::Rgb(20, 20, 20) }
|
||||
pub const fn tui_bo1 () -> Color { Color::Rgb(100, 110, 40) }
|
||||
pub const fn tui_bo2 () -> Color { Color::Rgb(70, 80, 50) }
|
||||
pub const fn tui_border_bg () -> Color { Color::Rgb(40, 50, 30) }
|
||||
pub const fn tui_border_fg (f: bool) -> Color { if f { tui_bo1() } else { tui_bo2() } }
|
||||
pub const fn tui_brown () -> Color { Color::Rgb(128,255,0) }
|
||||
pub const fn tui_electric () -> Color { Color::Rgb(0,255,128) }
|
||||
pub const fn tui_g (g: u8) -> Color { Color::Rgb(g, g, g) }
|
||||
pub const fn tui_green () -> Color { Color::Rgb(0,255,0) }
|
||||
pub const fn tui_mode_bg () -> Color { Color::Rgb(150, 160, 90) }
|
||||
pub const fn tui_mode_fg () -> Color { Color::Rgb(255, 255, 255) }
|
||||
pub const fn tui_null () -> Color { Color::Reset }
|
||||
pub const fn tui_orange () -> Color { Color::Rgb(255,128,0) }
|
||||
pub const fn tui_red () -> Color { Color::Rgb(255,0, 0) }
|
||||
pub const fn tui_separator_fg (_: bool) -> Color { Color::Rgb(0, 0, 0) }
|
||||
pub const fn tui_status_bar_bg () -> Color { Color::Rgb(28, 35, 25) }
|
||||
pub const fn tui_ti1 () -> Color { Color::Rgb(150, 160, 90) }
|
||||
pub const fn tui_ti2 () -> Color { Color::Rgb(120, 130, 100) }
|
||||
pub const fn tui_title_fg (f: bool) -> Color { if f { tui_ti1() } else { tui_ti2() } }
|
||||
pub const fn tui_yellow () -> Color { Color::Rgb(255,255,0) }
|
||||
}
|
||||
|
||||
/// Apply foreground color.
|
||||
pub const fn fg (fg: Color, draw: impl Draw<Tui>) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|{
|
||||
to.update(&|cell,_,_|{ cell.set_fg(fg); });
|
||||
draw.draw(to)
|
||||
})
|
||||
}
|
||||
|
||||
/// Apply background color.
|
||||
pub const fn bg (bg: Color, draw: impl Draw<Tui>) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|{
|
||||
to.update(&|cell,_,_|{ cell.set_bg(bg); });
|
||||
draw.draw(to)
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn fg_bg (fg: Color, bg: Color, draw: impl Draw<Tui>) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|{
|
||||
to.update(&|cell,_,_|{ cell.set_fg(fg); cell.set_bg(bg); });
|
||||
draw.draw(to)
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn fill_char (c: char) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|Ok(Some(to.update(&|cell,_,_|{
|
||||
cell.set_char(c);
|
||||
}))))
|
||||
}
|
||||
|
||||
/// Draw contents with modifier applied.
|
||||
pub const fn modify (on: bool, modifier: Modifier, draw: impl Draw<Tui>) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|{
|
||||
fill_mod(on, modifier).draw(to)?;
|
||||
draw.draw(to)
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn fill_mod (on: bool, modifier: Modifier) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|Ok(Some({
|
||||
if on {
|
||||
to.update(&|cell,_,_|cell.modifier.insert(modifier))
|
||||
} else {
|
||||
to.update(&|cell,_,_|cell.modifier.remove(modifier))
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
/// Draw contents with bold modifier applied.
|
||||
pub const fn bold (on: bool, draw: impl Draw<Tui>) -> impl Draw<Tui> {
|
||||
modify(on, Modifier::BOLD, draw)
|
||||
}
|
||||
|
||||
pub const fn fill_ul (color: Option<Color>) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|Ok(Some(if let Some(color) = color {
|
||||
to.update(&|cell,_,_|{
|
||||
cell.modifier.insert(Modifier::UNDERLINED);
|
||||
cell.underline_color = color;
|
||||
})
|
||||
} else {
|
||||
to.update(&|cell,_,_|{
|
||||
cell.modifier.remove(Modifier::UNDERLINED);
|
||||
cell.underline_color = Reset;
|
||||
})
|
||||
})))
|
||||
}
|
||||
|
||||
mod phat {
|
||||
use super::*;
|
||||
pub const LO: &'static str = "▄";
|
||||
pub const HI: &'static str = "▀";
|
||||
/// A phat line
|
||||
pub fn lo (fg: Color, bg: Color) -> impl Draw<Tui> {
|
||||
fg_bg(fg, bg, x_repeat(self::phat::LO)).exact_h(1)
|
||||
}
|
||||
/// A phat line
|
||||
pub fn hi (fg: Color, bg: Color) -> impl Draw<Tui> {
|
||||
fg_bg(fg, bg, x_repeat(self::phat::HI)).exact_h(1)
|
||||
}
|
||||
}
|
||||
|
||||
mod scroll {
|
||||
pub const ICON_DEC_V: &[char] = &['▲'];
|
||||
pub const ICON_INC_V: &[char] = &['▼'];
|
||||
pub const ICON_DEC_H: &[char] = &[' ', '🞀', ' '];
|
||||
pub const ICON_INC_H: &[char] = &[' ', '🞂', ' '];
|
||||
}
|
||||
|
||||
pub const fn x_repeat (c: &str) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|{
|
||||
let XYWH(x, y, w, _h) = to.xywh();
|
||||
for x in x..x+w {
|
||||
if let Some(cell) = to.0.cell_mut(Position::from((x, y))) {
|
||||
cell.set_symbol(&c);
|
||||
}
|
||||
}
|
||||
Ok(Some(XYWH(x, y, w, 1)))
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn y_repeat (c: &str) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|{
|
||||
let XYWH(x, y, _w, h) = to.xywh();
|
||||
for y in y..y+h {
|
||||
if let Some(cell) = to.0.cell_mut(Position::from((x, y))) {
|
||||
cell.set_symbol(&c);
|
||||
}
|
||||
}
|
||||
Ok(Some(XYWH(x, y, 1, h)))
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn xy_repeat (c: &str) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|{
|
||||
let XYWH(x, y, w, h) = to.xywh();
|
||||
let a = c.len();
|
||||
for (_v, y) in (y..y+h).enumerate() {
|
||||
for (u, x) in (x..x+w).enumerate() {
|
||||
if let Some(cell) = to.0.cell_mut(Position::from((x, y))) {
|
||||
let u = u % a;
|
||||
cell.set_symbol(&c[u..u+1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Some(XYWH(x, y, w, h)))
|
||||
})
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let _ = tengri::button_2("", "", true);
|
||||
/// let _ = tengri::button_2("", "", false);
|
||||
/// ```
|
||||
pub const fn button_2 <'a> (key: impl Draw<Tui>, label: impl Draw<Tui>, hide: bool) -> impl Draw<Tui> {
|
||||
let c1 = tui_orange();
|
||||
let c2 = tui_g(0);
|
||||
let c3 = tui_g(96);
|
||||
let c4 = tui_g(255);
|
||||
bold(true, fg_bg(c1, c2, east(fg(c2, east(key, fg(c3, "▐"))), when(!hide, fg_bg(c4, c3, label)))))
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let _ = tengri::button_3("", "", "", true);
|
||||
/// let _ = tengri::button_3("", "", "", false);
|
||||
/// ```
|
||||
pub const fn button_3 <'a> (
|
||||
key: impl Draw<Tui>, label: impl Draw<Tui>, value: impl Draw<Tui>, editing: bool,
|
||||
) -> impl Draw<Tui> {
|
||||
bold(true, east(
|
||||
fg_bg(tui_orange(), tui_g(0),
|
||||
east(fg(tui_g(0), "▐"), east(key, fg(if editing { tui_g(128) } else { tui_g(96) }, "▐")))),
|
||||
east(
|
||||
when(!editing, east(fg_bg(tui_g(255), tui_g(96), label), fg_bg(tui_g(128), tui_g(96), "▐"),)),
|
||||
east(fg_bg(tui_g(224), tui_g(128), value), fg_bg(tui_g(128), Reset, "▌"), ))))
|
||||
}
|
||||
|
||||
/// Stackably padded.
|
||||
///
|
||||
/// ```
|
||||
/// /// TODO
|
||||
/// ```
|
||||
pub fn phat (w: u16, h: u16, [fg, bg, hi, lo]: [Color;4], draw: impl Draw<Tui>) -> impl Draw<Tui> {
|
||||
let top = self::phat::lo(bg, hi).exact_h(1);
|
||||
let low = self::phat::hi(bg, lo).exact_h(1);
|
||||
let draw = fg_bg(fg, bg, draw);
|
||||
south(top, north(low, draw)).min_wh(w, h)
|
||||
}
|
||||
|
||||
pub fn x_scroll () -> impl Draw<Tui> {
|
||||
thunk(|Tui(buf, XYWH(x1, y1, w, h)): &mut Tui|{
|
||||
let x2 = *x1 + *w;
|
||||
for (i, x) in (*x1..=x2).enumerate() {
|
||||
if let Some(cell) = buf.cell_mut(Position::from((x, *y1))) {
|
||||
if i < (self::scroll::ICON_DEC_H.len()) {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Rgb(0, 0, 0));
|
||||
cell.set_char(self::scroll::ICON_DEC_H[i as usize]);
|
||||
} else if i > (*w as usize - self::scroll::ICON_INC_H.len()) {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Rgb(0, 0, 0));
|
||||
cell.set_char(self::scroll::ICON_INC_H[*w as usize - i]);
|
||||
} else if false {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Reset);
|
||||
cell.set_char('━');
|
||||
} else {
|
||||
cell.set_fg(Rgb(0, 0, 0));
|
||||
cell.set_bg(Reset);
|
||||
cell.set_char('╌');
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Some(XYWH(*x1, *y1, *w, 1)))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn y_scroll () -> impl Draw<Tui> {
|
||||
thunk(|Tui(buf, XYWH(x1, y1, w, h)): &mut Tui|{
|
||||
let y2 = *y1 + *h;
|
||||
for (i, y) in (*y1..=y2).enumerate() {
|
||||
if let Some(cell) = buf.cell_mut(Position::from((*x1, y))) {
|
||||
if (i as usize) < (self::scroll::ICON_DEC_V.len()) {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Rgb(0, 0, 0));
|
||||
cell.set_char(self::scroll::ICON_DEC_V[i as usize]);
|
||||
} else if (i as usize) > (*h as usize - self::scroll::ICON_INC_V.len()) {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Rgb(0, 0, 0));
|
||||
cell.set_char(self::scroll::ICON_INC_V[*h as usize - i]);
|
||||
} else if false {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Reset);
|
||||
cell.set_char('‖'); // ━
|
||||
} else {
|
||||
cell.set_fg(Rgb(0, 0, 0));
|
||||
cell.set_bg(Reset);
|
||||
cell.set_char('╎'); // ━
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Some(XYWH(*x1, *y1, 1, *h)))
|
||||
})
|
||||
}
|
||||
|
||||
/// Draw TUI content or its error message.
|
||||
///
|
||||
/// ```
|
||||
/// for variant in [
|
||||
/// Ok(Some("hello")),
|
||||
/// Ok(None),
|
||||
/// Err("fail".into()),
|
||||
/// ] {
|
||||
/// let _ = tengri::catcher(variant);
|
||||
/// }
|
||||
/// ```
|
||||
pub fn catcher <T: Draw<Tui>> (result: Usually<T>) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|match result {
|
||||
Ok(content) => content.draw(to),
|
||||
Err(e) => {
|
||||
let err_fg = Color::Rgb(255,224,244);
|
||||
let err_bg = Color::Rgb(96, 24, 24);
|
||||
let title = east(bold(true, "upsi daisy. "), "rendering failed.");
|
||||
let error = east("\"why?\" ", bold(true, format!("{e}")));
|
||||
fg(err_fg, bg(err_bg, south(title, error))).draw(to)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
impl_draw!(|self: u64, _to: Tui|{ todo!() });
|
||||
impl_draw!(|self: f64, _to: Tui|{ todo!() });
|
||||
38
src/text.rs
38
src/text.rs
|
|
@ -1,34 +1,21 @@
|
|||
use crate::{Usually, draw::Draw, space::*};
|
||||
use crate::*;
|
||||
pub(crate) use ::unicode_width::*;
|
||||
|
||||
#[cfg(feature = "term")] mod impl_term {
|
||||
use super::*;
|
||||
use crate::term::Tui;
|
||||
use crate::*;
|
||||
use ratatui::prelude::Position;
|
||||
|
||||
impl Draw<Tui> for &str {
|
||||
fn draw (self, to: &mut Tui) -> Usually<XYWH<u16>> {
|
||||
impl_draw!(|self: String, to: Tui|{self.as_str().draw(to)});
|
||||
impl_draw!(|self: std::sync::Arc<str>, to: Tui|{self.as_ref().draw(to)});
|
||||
impl_draw!(|self: &std::sync::Arc<str>, to: Tui|{self.as_ref().draw(to)});
|
||||
impl_draw!(|self: &str, to: Tui|{
|
||||
let XYWH(x, y, w, ..) = to.1.centered_xy([width_chars_max(to.w(), self), 1]);
|
||||
to.text(&self, x, y, w)
|
||||
}
|
||||
}
|
||||
impl Draw<Tui> for String {
|
||||
fn draw (self, to: &mut Tui) -> Usually<XYWH<u16>> {
|
||||
self.as_str().draw(to)
|
||||
}
|
||||
}
|
||||
impl Draw<Tui> for &std::sync::Arc<str> {
|
||||
fn draw (self, to: &mut Tui) -> Usually<XYWH<u16>> {
|
||||
self.as_ref().draw(to)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
impl<T: AsRef<str>> Draw<Tui> for TrimString<T> {
|
||||
fn draw (self, to: &mut Tui) -> Usually<XYWH<u16>> { self.as_ref().draw(to) }
|
||||
}
|
||||
|
||||
impl<T: AsRef<str>> Draw<Tui> for TrimStringRef<'_, T> {
|
||||
fn draw (self, to: &mut Tui) -> Usually<XYWH<u16>> {
|
||||
impl_draw!(<T: AsRef<str>,>|self: TrimString<T>, to: Tui|{self.as_ref().draw(to)});
|
||||
impl_draw!(<T: AsRef<str>,>|self: TrimStringRef<'_, T>, to: Tui|{
|
||||
let XYWH(x, y, w, ..) = to.1;
|
||||
let mut width: u16 = 1;
|
||||
let mut chars = self.1.as_ref().chars();
|
||||
|
|
@ -44,14 +31,13 @@ pub(crate) use ::unicode_width::*;
|
|||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
impl Tui {
|
||||
/// 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) -> Usually<XYWH<u16>> {
|
||||
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() {
|
||||
|
|
@ -67,7 +53,7 @@ pub(crate) use ::unicode_width::*;
|
|||
break
|
||||
}
|
||||
}
|
||||
Ok(XYWH(x0, y, string_width, 1))
|
||||
Ok(Some(XYWH(x0, y, string_width, 1)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue