mirror of
https://codeberg.org/unspeaker/tengri.git
synced 2026-02-21 18:49:04 +01:00
refactor: flatten this too
This commit is contained in:
parent
b4ebdb8ff8
commit
5d61cc6c5f
34 changed files with 3053 additions and 3167 deletions
|
|
@ -9,9 +9,6 @@ lto = false
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"./tengri",
|
"./tengri",
|
||||||
"./input",
|
|
||||||
"./output",
|
|
||||||
"./tui",
|
|
||||||
"./proc",
|
"./proc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,26 +14,28 @@ l = "job:clippy"
|
||||||
[jobs.check]
|
[jobs.check]
|
||||||
command = ["cargo", "check"]
|
command = ["cargo", "check"]
|
||||||
need_stdout = false
|
need_stdout = false
|
||||||
watch = ["core","dsl","editor","input","output","proc","tengri","tui"]
|
watch = ["tengri"]
|
||||||
|
|
||||||
[jobs.clippy-all]
|
[jobs.clippy-all]
|
||||||
command = ["cargo", "clippy"]
|
command = ["cargo", "clippy"]
|
||||||
need_stdout = false
|
need_stdout = false
|
||||||
watch = ["tek", "deps"]
|
watch = ["tengri"]
|
||||||
|
|
||||||
[jobs.test]
|
[jobs.test]
|
||||||
command = ["cargo", "test"]
|
command = ["cargo", "test"]
|
||||||
need_stdout = true
|
need_stdout = true
|
||||||
watch = ["tek", "deps"]
|
watch = ["tengri"]
|
||||||
|
|
||||||
[jobs.doc]
|
[jobs.doc]
|
||||||
command = ["cargo", "doc", "--no-deps"]
|
command = ["cargo", "doc", "--no-deps"]
|
||||||
need_stdout = false
|
need_stdout = false
|
||||||
|
watch = ["tengri"]
|
||||||
|
|
||||||
[jobs.doc-open]
|
[jobs.doc-open]
|
||||||
command = ["cargo", "doc", "--no-deps", "--open"]
|
command = ["cargo", "doc", "--no-deps", "--open"]
|
||||||
need_stdout = false
|
need_stdout = false
|
||||||
on_success = "back" # so that we don't open the browser at each change
|
on_success = "back" # so that we don't open the browser at each change
|
||||||
|
watch = ["tengri"]
|
||||||
|
|
||||||
[skin]
|
[skin]
|
||||||
status_fg = 15
|
status_fg = 15
|
||||||
|
|
|
||||||
7
input/Cargo.lock
generated
7
input/Cargo.lock
generated
|
|
@ -1,7 +0,0 @@
|
||||||
# This file is automatically @generated by Cargo.
|
|
||||||
# It is not intended for manual editing.
|
|
||||||
version = 4
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tengri_engine"
|
|
||||||
version = "0.2.0"
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "tengri_input"
|
|
||||||
description = "UI metaframework, input layer."
|
|
||||||
version = { workspace = true }
|
|
||||||
edition = { workspace = true }
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
path = "input.rs"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
dizzle = { path = "../../dizzle" }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
tengri_tui = { path = "../tui" }
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
***tengri_input*** is where tengri's input handling is defined.
|
|
||||||
|
|
||||||
the following items are provided:
|
|
||||||
* `Input` trait, for defining for input sources
|
|
||||||
* `Handle` trait and `handle!` macro, for defining input handlers
|
|
||||||
* `Command` trait and the `command!` macro, for defining commands that inputs may result in
|
|
||||||
125
input/input.rs
125
input/input.rs
|
|
@ -1,125 +0,0 @@
|
||||||
#![feature(associated_type_defaults)]
|
|
||||||
#![feature(if_let_guard)]
|
|
||||||
|
|
||||||
pub(crate) use dizzle::*;
|
|
||||||
|
|
||||||
#[cfg(test)] mod input_test;
|
|
||||||
|
|
||||||
/// Event source
|
|
||||||
pub trait Input: Sized {
|
|
||||||
/// Type of input event
|
|
||||||
type Event;
|
|
||||||
/// Result of handling input
|
|
||||||
type Handled; // TODO: make this an Option<Box dyn Command<Self>> containing the undo
|
|
||||||
/// Currently handled event
|
|
||||||
fn event (&self) -> &Self::Event;
|
|
||||||
/// Whether component should exit
|
|
||||||
fn is_done (&self) -> bool;
|
|
||||||
/// Mark component as done
|
|
||||||
fn done (&self);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Define a trait an implement it for various mutation-enabled wrapper types. */
|
|
||||||
#[macro_export] macro_rules! flex_trait_mut (
|
|
||||||
($Trait:ident $(<$($A:ident:$T:ident),+>)? {
|
|
||||||
$(fn $fn:ident (&mut $self:ident $(, $arg:ident:$ty:ty)*) -> $ret:ty $body:block)*
|
|
||||||
})=>{
|
|
||||||
pub trait $Trait $(<$($A: $T),+>)? {
|
|
||||||
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret $body)*
|
|
||||||
}
|
|
||||||
impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for &mut _T_ {
|
|
||||||
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { (*$self).$fn($($arg),*) })*
|
|
||||||
}
|
|
||||||
impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for Option<_T_> {
|
|
||||||
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret {
|
|
||||||
if let Some(this) = $self { this.$fn($($arg),*) } else { Ok(None) }
|
|
||||||
})*
|
|
||||||
}
|
|
||||||
impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::Mutex<_T_> {
|
|
||||||
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { $self.get_mut().unwrap().$fn($($arg),*) })*
|
|
||||||
}
|
|
||||||
impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::Arc<::std::sync::Mutex<_T_>> {
|
|
||||||
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { $self.lock().unwrap().$fn($($arg),*) })*
|
|
||||||
}
|
|
||||||
impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::RwLock<_T_> {
|
|
||||||
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { $self.write().unwrap().$fn($($arg),*) })*
|
|
||||||
}
|
|
||||||
impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::Arc<::std::sync::RwLock<_T_>> {
|
|
||||||
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { $self.write().unwrap().$fn($($arg),*) })*
|
|
||||||
}
|
|
||||||
};
|
|
||||||
);
|
|
||||||
|
|
||||||
flex_trait_mut!(Handle <E: Input> {
|
|
||||||
fn handle (&mut self, _input: &E) -> Perhaps<E::Handled> {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
pub trait Command<S>: Send + Sync + Sized {
|
|
||||||
fn execute (&self, state: &mut S) -> Perhaps<Self>;
|
|
||||||
fn delegate <T> (&self, state: &mut S, wrap: impl Fn(Self)->T) -> Perhaps<T>
|
|
||||||
where Self: Sized
|
|
||||||
{
|
|
||||||
Ok(self.execute(state)?.map(wrap))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S, T: Command<S>> Command<S> for Option<T> {
|
|
||||||
fn execute (&self, _: &mut S) -> Perhaps<Self> {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
fn delegate <U> (&self, _: &mut S, _: impl Fn(Self)->U) -> Perhaps<U>
|
|
||||||
where Self: Sized
|
|
||||||
{
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Implement [Command] for given `State` and `handler`
|
|
||||||
#[macro_export] macro_rules! command {
|
|
||||||
($(<$($l:lifetime),+>)?|$self:ident:$Command:ty,$state:ident:$State:ty|$handler:expr) => {
|
|
||||||
impl$(<$($l),+>)? ::tengri::input::Command<$State> for $Command {
|
|
||||||
fn execute (&$self, $state: &mut $State) -> Perhaps<Self> {
|
|
||||||
Ok($handler)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export] macro_rules! def_command (($Command:ident: |$state:ident: $State:ty| {
|
|
||||||
$($Variant:ident$({$($arg:ident:$Arg:ty),+ $(,)?})?=>$body:expr),* $(,)?
|
|
||||||
})=>{
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum $Command {
|
|
||||||
$($Variant $({ $($arg: $Arg),* })?),*
|
|
||||||
}
|
|
||||||
impl Command<$State> for $Command {
|
|
||||||
fn execute (&self, $state: &mut $State) -> Perhaps<Self> {
|
|
||||||
match self {
|
|
||||||
$(Self::$Variant $({ $($arg),* })? => $body,)*
|
|
||||||
_ => unimplemented!("Command<{}>: {self:?}", stringify!($State)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Implement [Handle] for given `State` and `handler`.
|
|
||||||
#[macro_export] macro_rules! handle {
|
|
||||||
(|$self:ident:$State:ty,$input:ident|$handler:expr) => {
|
|
||||||
impl<E: Engine> ::tengri::input::Handle<E> for $State {
|
|
||||||
fn handle (&mut $self, $input: &E) -> Perhaps<E::Handled> {
|
|
||||||
$handler
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
($E:ty: |$self:ident:$State:ty,$input:ident|$handler:expr) => {
|
|
||||||
impl ::tengri::input::Handle<$E> for $State {
|
|
||||||
fn handle (&mut $self, $input: &$E) ->
|
|
||||||
Perhaps<<$E as ::tengri::input::Input>::Handled>
|
|
||||||
{
|
|
||||||
$handler
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
#[test] fn test_stub_input () -> Usually<()> {
|
|
||||||
use crate::*;
|
|
||||||
struct TestInput(bool);
|
|
||||||
enum TestEvent { Test1 }
|
|
||||||
impl Input for TestInput {
|
|
||||||
type Event = TestEvent;
|
|
||||||
type Handled = ();
|
|
||||||
fn event (&self) -> &Self::Event {
|
|
||||||
&TestEvent::Test1
|
|
||||||
}
|
|
||||||
fn is_done (&self) -> bool {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
fn done (&self) {}
|
|
||||||
}
|
|
||||||
let _ = TestInput(true).event();
|
|
||||||
assert!(TestInput(true).is_done());
|
|
||||||
assert!(!TestInput(false).is_done());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
//#[cfg(all(test, feature = "dsl"))] #[test] fn test_dsl_keymap () -> Usually<()> {
|
|
||||||
//let _keymap = CstIter::new("");
|
|
||||||
//Ok(())
|
|
||||||
//}
|
|
||||||
|
|
||||||
|
|
@ -1,351 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
|
|
||||||
///// The syntagm `(when :condition :content)` corresponds to a [When] layout element.
|
|
||||||
//impl<S, A> FromDsl<S> for When<A> where bool: FromDsl<S>, A: FromDsl<S> {
|
|
||||||
//fn try_provide (state: &S, source: &DslVal<impl DslStr, impl DslExpr>) -> Perhaps<Self> {
|
|
||||||
//source.exp_match("when", |_, tail|Ok(Some(Self(
|
|
||||||
//FromDsl::<S>::provide(state,
|
|
||||||
//tail.nth(0, ||"no condition".into())?, ||"no condition".into())?,
|
|
||||||
//FromDsl::<S>::provide(state,
|
|
||||||
//tail.nth(1, ||"no content".into())?, ||"no content".into())?,
|
|
||||||
//))))
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
///// The syntagm `(either :condition :content1 :content2)` corresponds to an [Either] layout element.
|
|
||||||
//impl<S, A, B> FromDsl<S> for Either<A, B> where S: Eval<Ast, bool> + Eval<Ast, A> + Eval<Ast, B> {
|
|
||||||
//fn try_provide (state: &S, source: &DslVal<impl DslStr, impl DslExpr>) -> Perhaps<Self> {
|
|
||||||
//source.exp_match("either", |_, tail|Ok(Some(Self(
|
|
||||||
//state.eval(tail.nth(0, ||"no condition")?, ||"no condition")?,
|
|
||||||
//state.eval(tail.nth(1, ||"no content 1")?, ||"no content 1")?,
|
|
||||||
//state.eval(tail.nth(2, ||"no content 1")?, ||"no content 2")?,
|
|
||||||
//))))
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
///// The syntagm `(align/* :content)` corresponds to an [Align] layout element,
|
|
||||||
///// where `*` specifies the direction of the alignment.
|
|
||||||
//impl<S, A> FromDsl<S> for Align<A> where S: Eval<Option<Ast>, A> {
|
|
||||||
//fn try_provide (state: &S, source: &DslVal<impl DslStr, impl DslExpr>) -> Perhaps<Self> {
|
|
||||||
//source.exp_match("align/", |head, tail|Ok(Some(match head {
|
|
||||||
//"c" => Self::c(state.eval(tail.nth(0, ||"no content")?, ||"no content")),
|
|
||||||
//"x" => Self::x(state.eval(tail.nth(0, ||"no content")?, ||"no content")),
|
|
||||||
//"y" => Self::y(state.eval(tail.nth(0, ||"no content")?, ||"no content")),
|
|
||||||
//"n" => Self::n(state.eval(tail.nth(0, ||"no content")?, ||"no content")),
|
|
||||||
//"s" => Self::s(state.eval(tail.nth(0, ||"no content")?, ||"no content")),
|
|
||||||
//"e" => Self::e(state.eval(tail.nth(0, ||"no content")?, ||"no content")),
|
|
||||||
//"w" => Self::w(state.eval(tail.nth(0, ||"no content")?, ||"no content")),
|
|
||||||
//"nw" => Self::nw(state.eval(tail.nth(0, ||"no content")?, ||"no content")),
|
|
||||||
//"ne" => Self::ne(state.eval(tail.nth(0, ||"no content")?, ||"no content")),
|
|
||||||
//"sw" => Self::sw(state.eval(tail.nth(0, ||"no content")?, ||"no content")),
|
|
||||||
//"se" => Self::se(state.eval(tail.nth(0, ||"no content")?, ||"no content")),
|
|
||||||
//_ => return Err("invalid align variant".into())
|
|
||||||
//})))
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
///// The syntagm `(bsp/* :content1 :content2)` corresponds to a [Bsp] layout element,
|
|
||||||
///// where `*` specifies the direction of the split.
|
|
||||||
//impl<S, A, B> FromDsl<S> for Bsp<A, B> where S: Eval<Option<Ast>, A> + Eval<Option<Ast>, B> {
|
|
||||||
//fn try_provide (state: &S, source: &DslVal<impl DslStr, impl DslExpr>) -> Perhaps<Self> {
|
|
||||||
//source.exp_match("bsp/", |head, tail|Ok(Some(match head {
|
|
||||||
//"n" => Self::n(tail.nth(0, ||"no content 1"), tail.nth(1, ||"no content 2")),
|
|
||||||
//"s" => Self::s(tail.nth(0, ||"no content 1"), tail.nth(1, ||"no content 2")),
|
|
||||||
//"e" => Self::e(tail.nth(0, ||"no content 1"), tail.nth(1, ||"no content 2")),
|
|
||||||
//"w" => Self::w(tail.nth(0, ||"no content 1"), tail.nth(1, ||"no content 2")),
|
|
||||||
//"a" => Self::a(tail.nth(0, ||"no content 1"), tail.nth(1, ||"no content 2")),
|
|
||||||
//"b" => Self::b(tail.nth(0, ||"no content 1"), tail.nth(1, ||"no content 2")),
|
|
||||||
//_ => return Ok(None),
|
|
||||||
//})))
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//#[cfg(feature = "dsl")] take!($Enum<A>, A|state, words|Ok(
|
|
||||||
//if let Some(Token { value: Key(k), .. }) = words.peek() {
|
|
||||||
//let mut base = words.clone();
|
|
||||||
//let content = state.give_or_fail(words, ||format!("{k}: no content"))?;
|
|
||||||
//return Ok(Some(match words.next() {
|
|
||||||
//Some(Token{value: Key($x),..}) => Self::x(content),
|
|
||||||
//Some(Token{value: Key($y),..}) => Self::y(content),
|
|
||||||
//Some(Token{value: Key($xy),..}) => Self::XY(content),
|
|
||||||
//_ => unreachable!()
|
|
||||||
//}))
|
|
||||||
//} else {
|
|
||||||
//None
|
|
||||||
//}));
|
|
||||||
//#[cfg(feature = "dsl")] take!($Enum<U, A>, U, A|state, words|Ok(
|
|
||||||
//if let Some(Token { value: Key($x|$y|$xy), .. }) = words.peek() {
|
|
||||||
//let mut base = words.clone();
|
|
||||||
//Some(match words.next() {
|
|
||||||
//Some(Token { value: Key($x), .. }) => Self::x(
|
|
||||||
//state.give_or_fail(words, ||"x: no unit")?,
|
|
||||||
//state.give_or_fail(words, ||"x: no content")?,
|
|
||||||
//),
|
|
||||||
//Some(Token { value: Key($y), .. }) => Self::y(
|
|
||||||
//state.give_or_fail(words, ||"y: no unit")?,
|
|
||||||
//state.give_or_fail(words, ||"y: no content")?,
|
|
||||||
//),
|
|
||||||
//Some(Token { value: Key($x), .. }) => Self::XY(
|
|
||||||
//state.give_or_fail(words, ||"xy: no unit x")?,
|
|
||||||
//state.give_or_fail(words, ||"xy: no unit y")?,
|
|
||||||
//state.give_or_fail(words, ||"xy: no content")?
|
|
||||||
//),
|
|
||||||
//_ => unreachable!(),
|
|
||||||
//})
|
|
||||||
//} else {
|
|
||||||
//None
|
|
||||||
//}));
|
|
||||||
//if let Exp(_, exp) = source.value() {
|
|
||||||
//let mut rest = exp.clone();
|
|
||||||
//return Ok(Some(match rest.next().as_ref().and_then(|x|x.key()) {
|
|
||||||
//Some("bsp/n") => Self::n(
|
|
||||||
//state.eval(rest.next(), ||"bsp/n: no content 1")?,
|
|
||||||
//state.eval(rest.next(), ||"bsp/n: no content 2")?,
|
|
||||||
//),
|
|
||||||
//Some("bsp/s") => Self::s(
|
|
||||||
//state.eval(rest.next(), ||"bsp/s: no content 1")?,
|
|
||||||
//state.eval(rest.next(), ||"bsp/s: no content 2")?,
|
|
||||||
//),
|
|
||||||
//Some("bsp/e") => Self::e(
|
|
||||||
//state.eval(rest.next(), ||"bsp/e: no content 1")?,
|
|
||||||
//state.eval(rest.next(), ||"bsp/e: no content 2")?,
|
|
||||||
//),
|
|
||||||
//Some("bsp/w") => Self::w(
|
|
||||||
//state.eval(rest.next(), ||"bsp/w: no content 1")?,
|
|
||||||
//state.eval(rest.next(), ||"bsp/w: no content 2")?,
|
|
||||||
//),
|
|
||||||
//Some("bsp/a") => Self::a(
|
|
||||||
//state.eval(rest.next(), ||"bsp/a: no content 1")?,
|
|
||||||
//state.eval(rest.next(), ||"bsp/a: no content 2")?,
|
|
||||||
//),
|
|
||||||
//Some("bsp/b") => Self::b(
|
|
||||||
//state.eval(rest.next(), ||"bsp/b: no content 1")?,
|
|
||||||
//state.eval(rest.next(), ||"bsp/b: no content 2")?,
|
|
||||||
//),
|
|
||||||
//_ => return Ok(None),
|
|
||||||
//}))
|
|
||||||
//}
|
|
||||||
//Ok(None)
|
|
||||||
//if let Exp(_, source) = source.value() {
|
|
||||||
//let mut rest = source.clone();
|
|
||||||
//return Ok(Some(match rest.next().as_ref().and_then(|x|x.key()) {
|
|
||||||
//Some("align/c") => Self::c(state.eval(rest.next(), ||"align/c: no content")?),
|
|
||||||
//Some("align/x") => Self::x(state.eval(rest.next(), ||"align/x: no content")?),
|
|
||||||
//Some("align/y") => Self::y(state.eval(rest.next(), ||"align/y: no content")?),
|
|
||||||
//Some("align/n") => Self::n(state.eval(rest.next(), ||"align/n: no content")?),
|
|
||||||
//Some("align/s") => Self::s(state.eval(rest.next(), ||"align/s: no content")?),
|
|
||||||
//Some("align/e") => Self::e(state.eval(rest.next(), ||"align/e: no content")?),
|
|
||||||
//Some("align/w") => Self::w(state.eval(rest.next(), ||"align/w: no content")?),
|
|
||||||
//Some("align/nw") => Self::nw(state.eval(rest.next(), ||"align/nw: no content")?),
|
|
||||||
//Some("align/ne") => Self::ne(state.eval(rest.next(), ||"align/ne: no content")?),
|
|
||||||
//Some("align/sw") => Self::sw(state.eval(rest.next(), ||"align/sw: no content")?),
|
|
||||||
//Some("align/se") => Self::se(state.eval(rest.next(), ||"align/se: no content")?),
|
|
||||||
//_ => return Ok(None),
|
|
||||||
//}))
|
|
||||||
//}
|
|
||||||
//Ok(None)
|
|
||||||
//Ok(match source.exp_head().and_then(|e|e.key()) {
|
|
||||||
//Some("either") => Some(Self(
|
|
||||||
//source.exp_tail().and_then(|t|t.get(0)).map(|x|state.eval(x, ||"when: no condition"))?,
|
|
||||||
//source.exp_tail().and_then(|t|t.get(1)).map(|x|state.eval(x, ||"when: no content 1"))?,
|
|
||||||
//source.exp_tail().and_then(|t|t.get(2)).map(|x|state.eval(x, ||"when: no content 2"))?,
|
|
||||||
//)),
|
|
||||||
//_ => None
|
|
||||||
//})
|
|
||||||
//if let Exp(_, mut exp) = source.value()
|
|
||||||
//&& let Some(Ast(Key(id))) = exp.peek() && *id == *"either" {
|
|
||||||
//let _ = exp.next();
|
|
||||||
//return Ok(Some(Self(
|
|
||||||
//state.eval(exp.next().unwrap(), ||"either: no condition")?,
|
|
||||||
//state.eval(exp.next().unwrap(), ||"either: no content 1")?,
|
|
||||||
//state.eval(exp.next().unwrap(), ||"either: no content 2")?,
|
|
||||||
//)))
|
|
||||||
//}
|
|
||||||
//Ok(None)
|
|
||||||
//Ok(match source.exp_head().and_then(|e|e.key()) {
|
|
||||||
//Some("when") => Some(Self(
|
|
||||||
//source.exp_tail().and_then(|t|t.get(0)).map(|x|state.eval(x, ||"when: no condition"))?,
|
|
||||||
//source.exp_tail().and_then(|t|t.get(1)).map(|x|state.eval(x, ||"when: no content"))?,
|
|
||||||
//)),
|
|
||||||
//_ => None
|
|
||||||
//})
|
|
||||||
//use crate::*;
|
|
||||||
//use Direction::*;
|
|
||||||
|
|
||||||
//pub struct Stack<'x, E, F1> {
|
|
||||||
//__: PhantomData<&'x (E, F1)>,
|
|
||||||
//direction: Direction,
|
|
||||||
//callback: F1
|
|
||||||
//}
|
|
||||||
|
|
||||||
//impl<'x, E, F1> Stack<'x, E, F1> {
|
|
||||||
//pub fn new (direction: Direction, callback: F1) -> Self {
|
|
||||||
//Self { direction, callback, __: Default::default(), }
|
|
||||||
//}
|
|
||||||
//pub fn above (callback: F1) -> Self {
|
|
||||||
//Self::new(Above, callback)
|
|
||||||
//}
|
|
||||||
//pub fn below (callback: F1) -> Self {
|
|
||||||
//Self::new(Below, callback)
|
|
||||||
//}
|
|
||||||
//pub fn north (callback: F1) -> Self {
|
|
||||||
//Self::new(North, callback)
|
|
||||||
//}
|
|
||||||
//pub fn south (callback: F1) -> Self {
|
|
||||||
//Self::new(South, callback)
|
|
||||||
//}
|
|
||||||
//pub fn east (callback: F1) -> Self {
|
|
||||||
//Self::new(East, callback)
|
|
||||||
//}
|
|
||||||
//pub fn west (callback: F1) -> Self {
|
|
||||||
//Self::new(West, callback)
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
|
|
||||||
//impl<'x, E: Out, F1: Fn(&mut dyn FnMut(&dyn Layout<E>))> Layout<E> for Stack<'x, E, F1> {
|
|
||||||
//fn layout (&self, to: E::Area) -> E::Area {
|
|
||||||
//let state = StackLayoutState::<E>::new(self.direction, to);
|
|
||||||
//(self.callback)(&mut |component: &dyn Layout<E>|{
|
|
||||||
//let StackLayoutState { x, y, w_remaining, h_remaining, .. } = *state.borrow();
|
|
||||||
//let [_, _, w, h] = component.layout([x, y, w_remaining, h_remaining].into()).xywh();
|
|
||||||
//state.borrow_mut().grow(w, h);
|
|
||||||
//});
|
|
||||||
//let StackLayoutState { w_used, h_used, .. } = *state.borrow();
|
|
||||||
//match self.direction {
|
|
||||||
//North | West => { todo!() },
|
|
||||||
//South | East => { [to.x(), to.y(), w_used, h_used].into() },
|
|
||||||
//_ => unreachable!(),
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//impl<'x, E: Out, F1: Fn(&mut dyn FnMut(&dyn Draw<E>))> Draw<E> for Stack<'x, E, F1> {
|
|
||||||
//fn draw (&self, to: &mut E) {
|
|
||||||
//let state = StackLayoutState::<E>::new(self.direction, to.area());
|
|
||||||
//let to = Rc::new(RefCell::new(to));
|
|
||||||
//(self.callback)(&mut |component: &dyn Draw<E>|{
|
|
||||||
//let StackLayoutState { x, y, w_remaining, h_remaining, .. } = *state.borrow();
|
|
||||||
//let layout = component.layout([x, y, w_remaining, h_remaining].into());
|
|
||||||
//state.borrow_mut().grow(layout.w(), layout.h());
|
|
||||||
//to.borrow_mut().place_at(layout, component);
|
|
||||||
//});
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
|
|
||||||
//#[derive(Copy, Clone)]
|
|
||||||
//struct StackLayoutState<E: Out> {
|
|
||||||
//direction: Direction,
|
|
||||||
//x: E::Unit,
|
|
||||||
//y: E::Unit,
|
|
||||||
//w_used: E::Unit,
|
|
||||||
//h_used: E::Unit,
|
|
||||||
//w_remaining: E::Unit,
|
|
||||||
//h_remaining: E::Unit,
|
|
||||||
//}
|
|
||||||
//impl<E: Out> StackLayoutState<E> {
|
|
||||||
//fn new (direction: Direction, area: E::Area) -> std::rc::Rc<std::cell::RefCell<Self>> {
|
|
||||||
//let [x, y, w_remaining, h_remaining] = area.xywh();
|
|
||||||
//std::rc::Rc::new(std::cell::RefCell::new(Self {
|
|
||||||
//direction,
|
|
||||||
//x, y, w_remaining, h_remaining,
|
|
||||||
//w_used: E::Unit::zero(), h_used: E::Unit::zero()
|
|
||||||
//}))
|
|
||||||
//}
|
|
||||||
//fn grow (&mut self, w: E::Unit, h: E::Unit) -> &mut Self {
|
|
||||||
//match self.direction {
|
|
||||||
//South => { self.y = self.y.plus(h);
|
|
||||||
//self.h_used = self.h_used.plus(h);
|
|
||||||
//self.h_remaining = self.h_remaining.minus(h);
|
|
||||||
//self.w_used = self.w_used.max(w); },
|
|
||||||
//East => { self.x = self.x.plus(w);
|
|
||||||
//self.w_used = self.w_used.plus(w);
|
|
||||||
//self.w_remaining = self.w_remaining.minus(w);
|
|
||||||
//self.h_used = self.h_used.max(h); },
|
|
||||||
//North | West => { todo!() },
|
|
||||||
//Above | Below => {},
|
|
||||||
//};
|
|
||||||
//self
|
|
||||||
//}
|
|
||||||
//fn area_remaining (&self) -> E::Area {
|
|
||||||
//[self.x, self.y, self.w_remaining, self.h_remaining].into()
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
|
|
||||||
////pub struct Stack<'a, E, F1> {
|
|
||||||
////__: PhantomData<&'a (E, F1)>,
|
|
||||||
////direction: Direction,
|
|
||||||
////callback: F1
|
|
||||||
////}
|
|
||||||
////impl<'a, E, F1> Stack<'a, E, F1> where
|
|
||||||
////E: Out, F1: Fn(&mut dyn FnMut(&'a dyn Draw<E>)) + Send + Sync,
|
|
||||||
////{
|
|
||||||
////pub fn north (callback: F1) -> Self { Self::new(North, callback) }
|
|
||||||
////pub fn south (callback: F1) -> Self { Self::new(South, callback) }
|
|
||||||
////pub fn east (callback: F1) -> Self { Self::new(East, callback) }
|
|
||||||
////pub fn west (callback: F1) -> Self { Self::new(West, callback) }
|
|
||||||
////pub fn above (callback: F1) -> Self { Self::new(Above, callback) }
|
|
||||||
////pub fn below (callback: F1) -> Self { Self::new(Below, callback) }
|
|
||||||
////pub fn new (direction: Direction, callback: F1) -> Self {
|
|
||||||
////Self { direction, callback, __: Default::default(), }
|
|
||||||
////}
|
|
||||||
////}
|
|
||||||
////impl<'a, E, F1> Draw<E> for Stack<'a, E, F1> where
|
|
||||||
////E: Out, F1: Fn(&mut dyn FnMut(&'a dyn Draw<E>)) + Send + Sync,
|
|
||||||
////{
|
|
||||||
////fn layout (&self, to: E::Area) -> E::Area {
|
|
||||||
////let state = StackLayoutState::<E>::new(self.direction, to);
|
|
||||||
////let mut adder = {
|
|
||||||
////let state = state.clone();
|
|
||||||
////move|component: &dyn Draw<E>|{
|
|
||||||
////let [w, h] = component.layout(state.borrow().area_remaining()).wh();
|
|
||||||
////state.borrow_mut().grow(w, h);
|
|
||||||
////}
|
|
||||||
////};
|
|
||||||
////(self.callback)(&mut adder);
|
|
||||||
////let StackLayoutState { w_used, h_used, .. } = *state.borrow();
|
|
||||||
////match self.direction {
|
|
||||||
////North | West => { todo!() },
|
|
||||||
////South | East => { [to.x(), to.y(), w_used, h_used].into() },
|
|
||||||
////Above | Below => { [to.x(), to.y(), to.w(), to.h()].into() },
|
|
||||||
////}
|
|
||||||
////}
|
|
||||||
////fn draw (&self, to: &mut E) {
|
|
||||||
////let state = StackLayoutState::<E>::new(self.direction, to.area());
|
|
||||||
////let mut adder = {
|
|
||||||
////let state = state.clone();
|
|
||||||
////move|component: &dyn Draw<E>|{
|
|
||||||
////let [x, y, w, h] = component.layout(state.borrow().area_remaining()).xywh();
|
|
||||||
////state.borrow_mut().grow(w, h);
|
|
||||||
////to.place_at([x, y, w, h].into(), component);
|
|
||||||
////}
|
|
||||||
////};
|
|
||||||
////(self.callback)(&mut adder);
|
|
||||||
////}
|
|
||||||
////}
|
|
||||||
|
|
||||||
//[>Stack::down(|add|{
|
|
||||||
//let mut i = 0;
|
|
||||||
//for (_, name) in self.dirs.iter() {
|
|
||||||
//if i >= self.scroll {
|
|
||||||
//add(&Tui::bold(i == self.index, name.as_str()))?;
|
|
||||||
//}
|
|
||||||
//i += 1;
|
|
||||||
//}
|
|
||||||
//for (_, name) in self.files.iter() {
|
|
||||||
//if i >= self.scroll {
|
|
||||||
//add(&Tui::bold(i == self.index, name.as_str()))?;
|
|
||||||
//}
|
|
||||||
//i += 1;
|
|
||||||
//}
|
|
||||||
//add(&format!("{}/{i}", self.index))?;
|
|
||||||
//Ok(())
|
|
||||||
//}));*/
|
|
||||||
|
|
||||||
|
|
||||||
//#[test] fn test_iter_map () {
|
|
||||||
//struct Foo;
|
|
||||||
//impl<T: Out> Content<T> for Foo {}
|
|
||||||
//fn _make_map <T: Out, U: Content<T> + Send + Sync> (data: &Vec<U>) -> impl Draw<T> {
|
|
||||||
//Map::new(||data.iter(), |_foo, _index|{})
|
|
||||||
//}
|
|
||||||
//let _data = vec![Foo, Foo, Foo];
|
|
||||||
////let map = make_map(&data);
|
|
||||||
//}
|
|
||||||
14
output/Cargo.lock
generated
14
output/Cargo.lock
generated
|
|
@ -1,14 +0,0 @@
|
||||||
# This file is automatically @generated by Cargo.
|
|
||||||
# It is not intended for manual editing.
|
|
||||||
version = 4
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tengri_engine"
|
|
||||||
version = "0.2.0"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tengri_layout"
|
|
||||||
version = "0.2.0"
|
|
||||||
dependencies = [
|
|
||||||
"tengri_engine",
|
|
||||||
]
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "tengri_output"
|
|
||||||
description = "UI metaframework, output layer."
|
|
||||||
version = { workspace = true }
|
|
||||||
edition = { workspace = true }
|
|
||||||
|
|
||||||
[features]
|
|
||||||
bumpalo = [ "dep:bumpalo" ]
|
|
||||||
dsl = []
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
atomic_float = { workspace = true }
|
|
||||||
bumpalo = { workspace = true, optional = true }
|
|
||||||
dizzle = { path = "../../dizzle" }
|
|
||||||
quanta = { workspace = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
tengri = { path = "../tengri", features = [ "dsl", "tui" ] }
|
|
||||||
tengri_tui = { path = "../tui" }
|
|
||||||
proptest = { workspace = true }
|
|
||||||
proptest-derive = { workspace = true }
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
***tengri_output*** is an abstract interface layout framework.
|
|
||||||
|
|
||||||
it expresses the following notions:
|
|
||||||
|
|
||||||
* [**space:**](./src/space.rs) `Direction`, `Coordinate`, `Area`, `Size`, `Measure`
|
|
||||||
|
|
||||||
* [**output:**](./src/output.rs) `Out`, `Draw`, `Content`
|
|
||||||
* the layout operators are generic over `Draw` and/or `Content`
|
|
||||||
* the traits `Draw` and `Content` are generic over `Out`
|
|
||||||
* implement `Out` to bring a layout to a new backend:
|
|
||||||
[see `TuiOut` in `tengri_tui`](../tui/src/tui_engine/tui_output.rs)
|
|
||||||
|
|
||||||
* [**layout:**](./src/layout.rs)
|
|
||||||
* conditionals: `When`, `Either`
|
|
||||||
* iteration: `Map`
|
|
||||||
* concatenation: `Bsp`
|
|
||||||
* positioning: `Align`, `Push`, `Pull`
|
|
||||||
* sizing: `Fill`, `Fixed`, `Expand`, `Shrink`, `Min`, `Max`
|
|
||||||
* implement custom components (that may be backend-dependent):
|
|
||||||
[see `tui_content` in `tengri_tui`](../tui/src/tui_content)
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
# Seeds for failure cases proptest has generated in the past. It is
|
|
||||||
# automatically read and these particular cases re-run before any
|
|
||||||
# novel cases are generated.
|
|
||||||
#
|
|
||||||
# It is recommended to check this file in to source control so that
|
|
||||||
# everyone who runs the test benefits from these saved cases.
|
|
||||||
cc d2cd65ec39a1bf43c14bb2d3196c7e84ba854411360e570f06dd7ede62b0fd61 # shrinks to x = 0, y = 43998, w = 0, h = 43076, a = 0, b = 0
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
# Seeds for failure cases proptest has generated in the past. It is
|
|
||||||
# automatically read and these particular cases re-run before any
|
|
||||||
# novel cases are generated.
|
|
||||||
#
|
|
||||||
# It is recommended to check this file in to source control so that
|
|
||||||
# everyone who runs the test benefits from these saved cases.
|
|
||||||
cc 5b236150b286e479089d5bf6accc8ffbc3c0b0a1f955682af1987f342930d31e # shrinks to x = 0, y = 0, w = 0, h = 0, a = 1
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
# Seeds for failure cases proptest has generated in the past. It is
|
|
||||||
# automatically read and these particular cases re-run before any
|
|
||||||
# novel cases are generated.
|
|
||||||
#
|
|
||||||
# It is recommended to check this file in to source control so that
|
|
||||||
# everyone who runs the test benefits from these saved cases.
|
|
||||||
cc b05b448ca4eb29304cae506927639494cae99a9e1ab40c58ac9dcb70d1ea1298 # shrinks to op_x = Some(0), op_y = None, content = "", x = 0, y = 46377, w = 0, h = 38318
|
|
||||||
cc efdb7136c68396fa7c632cc6d3b304545ada1ba134269278f890639559a17575 # shrinks to op_x = Some(0), op_y = Some(32768), content = "", x = 0, y = 0, w = 0, h = 0
|
|
||||||
cc f6d43c39db04f4c0112fe998ef68cff0a4454cd9791775a3014cc81997fbadf4 # shrinks to op_x = Some(10076), op_y = None, content = "", x = 60498, y = 0, w = 0, h = 0
|
|
||||||
cc 3cabc97f3fa3a83fd5f8cf2c619ed213c2be5e9b1cb13e5178bde87dd838e2f4 # shrinks to op_x = Some(3924), op_y = None, content = "", x = 63574, y = 0, w = 0, h = 0
|
|
||||||
|
|
@ -1,293 +0,0 @@
|
||||||
#![feature(step_trait)]
|
|
||||||
#![feature(type_alias_impl_trait)]
|
|
||||||
#![feature(impl_trait_in_assoc_type)]
|
|
||||||
#![feature(const_precise_live_drops)]
|
|
||||||
#![feature(type_changing_struct_update)]
|
|
||||||
#![feature(anonymous_lifetime_in_impl_trait)]
|
|
||||||
#![feature(const_option_ops)]
|
|
||||||
#![feature(const_trait_impl)]
|
|
||||||
#![feature(const_default)]
|
|
||||||
#![feature(trait_alias)]
|
|
||||||
//#![feature(non_lifetime_binders)]
|
|
||||||
pub(crate) use self::Direction::*;
|
|
||||||
pub(crate) use std::fmt::{Debug, Display};
|
|
||||||
pub(crate) use std::ops::{Add, Sub, Mul, Div};
|
|
||||||
pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::Relaxed}};
|
|
||||||
pub(crate) use std::marker::PhantomData;
|
|
||||||
pub(crate) use dizzle::*;
|
|
||||||
//pub(crate) use quanta::Clock;
|
|
||||||
pub(crate) use atomic_float::AtomicF64;
|
|
||||||
|
|
||||||
// Define macros first, so that private macros are available in private modules:
|
|
||||||
|
|
||||||
/// Clear a pre-allocated buffer, then write into it.
|
|
||||||
#[macro_export] macro_rules! rewrite {
|
|
||||||
($buf:ident, $($rest:tt)*) => { |$buf,_,_|{ $buf.clear(); write!($buf, $($rest)*) } }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// FIXME: This macro should be some variant of `eval`, too.
|
|
||||||
/// But taking into account the different signatures (resolving them into 1?)
|
|
||||||
#[cfg(feature = "dsl")] #[macro_export] macro_rules! draw {
|
|
||||||
($State:ident: $Output:ident: $layers:expr) => {
|
|
||||||
impl Draw<$Output> for $State {
|
|
||||||
fn draw (&self, to: &mut $Output) {
|
|
||||||
for layer in $layers { layer(self, to) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// FIXME: This is generic: should be called `eval` and be part of [dizzle].
|
|
||||||
#[cfg(feature = "dsl")] #[macro_export] macro_rules! view {
|
|
||||||
($State:ident: $Output:ident: $namespaces:expr) => {
|
|
||||||
impl View<$Output, ()> for $State {
|
|
||||||
fn view_expr <'a> (&'a self, to: &mut $Output, expr: &'a impl Expression) -> Usually<()> {
|
|
||||||
for namespace in $namespaces { if namespace(self, to, expr)? { return Ok(()) } }
|
|
||||||
Err(format!("{}::<{}, ()>::view_expr: unexpected: {expr:?}",
|
|
||||||
stringify! { $State },
|
|
||||||
stringify! { $Output }).into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stack things on top of each other,
|
|
||||||
#[macro_export] macro_rules! lay (($($expr:expr),* $(,)?) => {{ let bsp = (); $(let bsp = Bsp::b(bsp, $expr);)*; bsp }});
|
|
||||||
|
|
||||||
/// Stack southward.
|
|
||||||
#[macro_export] macro_rules! col (($($expr:expr),* $(,)?) => {{ let bsp = (); $(let bsp = Bsp::s(bsp, $expr);)*; bsp }});
|
|
||||||
|
|
||||||
/// Stack northward.
|
|
||||||
#[macro_export] macro_rules! col_up (($($expr:expr),* $(,)?) => {{ let bsp = (); $(let bsp = Bsp::n(bsp, $expr);)*; bsp }});
|
|
||||||
|
|
||||||
/// Stack eastward.
|
|
||||||
#[macro_export] macro_rules! row (($($expr:expr),* $(,)?) => {{ let bsp = (); $(let bsp = Bsp::e(bsp, $expr);)*; bsp }});
|
|
||||||
|
|
||||||
/// Define layout operation.
|
|
||||||
macro_rules! layout_op_xy (
|
|
||||||
// Variant for layout ops that take no coordinates
|
|
||||||
(0: $T: ident) => {
|
|
||||||
impl<A> $T<A> {
|
|
||||||
#[inline] pub const fn inner (&self) -> &A {
|
|
||||||
match self { Self::X(c) | Self::Y(c) | Self::XY(c) => c }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<O: Out, T: Content<O>> Draw<O> for $T<T> {
|
|
||||||
fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// Variant for layout ops that take one coordinate
|
|
||||||
(1: $T: ident) => {
|
|
||||||
impl<U, A> $T<U, A> {
|
|
||||||
#[inline] pub const fn inner (&self) -> &A {
|
|
||||||
match self { Self::X(_, c) | Self::Y(_, c) | Self::XY(_, _, c) => c, }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<O: Out, T: Content<O>> Draw<O> for $T<O::Unit, T> {
|
|
||||||
fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) }
|
|
||||||
}
|
|
||||||
impl<U: Coord, A> $T<U, A> {
|
|
||||||
#[inline] pub fn dx (&self) -> U {
|
|
||||||
match self { Self::X(x, _) | Self::XY(x, ..) => *x, _ => 0.into() }
|
|
||||||
}
|
|
||||||
#[inline] pub fn dy (&self) -> U {
|
|
||||||
match self { Self::Y(y, _) | Self::XY(y, ..) => *y, _ => 0.into() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
(1 opt: $T: ident) => {
|
|
||||||
impl<U, A> $T<U, A> {
|
|
||||||
#[inline] pub const fn inner (&self) -> &A {
|
|
||||||
match self { Self::X(_, c) | Self::Y(_, c) | Self::XY(_, _, c) => c, }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<O: Out, T: Content<O>> Draw<O> for $T<O::Unit, T> {
|
|
||||||
fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) }
|
|
||||||
}
|
|
||||||
impl<U: Coord, A> $T<U, A> {
|
|
||||||
#[inline] pub const fn dx (&self) -> Option<U> {
|
|
||||||
match self { Self::X(x, _) | Self::XY(x, ..) => Some(*x), _ => None }
|
|
||||||
}
|
|
||||||
#[inline] pub const fn dy (&self) -> Option<U> {
|
|
||||||
match self { Self::Y(y, _) | Self::XY(y, ..) => Some(*y), _ => None }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
);
|
|
||||||
|
|
||||||
// Implement layout op that increments X and/or Y by fixed amount.
|
|
||||||
macro_rules! push_pull(($T:ident: $method: ident)=>{
|
|
||||||
layout_op_xy!(1: $T);
|
|
||||||
impl<O: Out, T: Layout<O>> Layout<O> for $T<O::Unit, T> {
|
|
||||||
fn layout_x (&self, area: XYWH<O::Unit>) -> O::Unit { area.x().$method(self.dx()) }
|
|
||||||
fn layout_y (&self, area: XYWH<O::Unit>) -> O::Unit { area.y().$method(self.dy()) }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
macro_rules! impl_map_direction (($name:ident, $axis:ident, $align:ident)=>{
|
|
||||||
impl<'a, O, A, B, I, F> Map<
|
|
||||||
O, A, Push<O::Unit, Align<Fixed<O::Unit, Fill<B>>>>, I, F, fn(A, usize)->B
|
|
||||||
> where
|
|
||||||
O: Out,
|
|
||||||
B: Draw<O>,
|
|
||||||
I: Iterator<Item = A> + Send + Sync + 'a,
|
|
||||||
F: Fn() -> I + Send + Sync + 'a
|
|
||||||
{
|
|
||||||
pub const fn $name (
|
|
||||||
size: O::Unit,
|
|
||||||
get_iter: F,
|
|
||||||
get_item: impl Fn(A, usize)->B + Send + Sync
|
|
||||||
) -> Map<
|
|
||||||
O, A,
|
|
||||||
Push<O::Unit, Align<Fixed<O::Unit, B>>>,
|
|
||||||
I, F,
|
|
||||||
impl Fn(A, usize)->Push<O::Unit, Align<Fixed<O::Unit, B>>> + Send + Sync
|
|
||||||
> {
|
|
||||||
Map {
|
|
||||||
__: PhantomData,
|
|
||||||
get_iter,
|
|
||||||
get_item: move |item: A, index: usize|{
|
|
||||||
// FIXME: multiply
|
|
||||||
let mut push: O::Unit = O::Unit::from(0u16);
|
|
||||||
for _ in 0..index {
|
|
||||||
push = push + size;
|
|
||||||
}
|
|
||||||
Push::$axis(push, Align::$align(Fixed::$axis(size, get_item(item, index))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
mod out_traits; pub use self::out_traits::*;
|
|
||||||
mod out_structs; pub use self::out_structs::*;
|
|
||||||
mod out_impls; pub use self::out_impls::*;
|
|
||||||
#[cfg(test)] mod out_tests;
|
|
||||||
|
|
||||||
#[cfg(feature = "dsl")]
|
|
||||||
pub fn evaluate_output_expression <'a, O: Out + 'a, S> (
|
|
||||||
state: &S, output: &mut O, expr: &'a impl Expression
|
|
||||||
) -> Usually<bool> where
|
|
||||||
S: View<O, ()>
|
|
||||||
+ for<'b>Namespace<'b, bool>
|
|
||||||
+ for<'b>Namespace<'b, O::Unit>
|
|
||||||
{
|
|
||||||
// First element of expression is used for dispatch.
|
|
||||||
// Dispatch is proto-namespaced using separator character
|
|
||||||
let head = expr.head()?;
|
|
||||||
let mut frags = head.src()?.unwrap_or_default().split("/");
|
|
||||||
// The rest of the tokens in the expr are arguments.
|
|
||||||
// Their meanings depend on the dispatched operation
|
|
||||||
let args = expr.tail();
|
|
||||||
let arg0 = args.head();
|
|
||||||
let tail0 = args.tail();
|
|
||||||
let arg1 = tail0.head();
|
|
||||||
let tail1 = tail0.tail();
|
|
||||||
let arg2 = tail1.head();
|
|
||||||
// And we also have to do the above binding dance
|
|
||||||
// so that the Perhaps<token>s remain in scope.
|
|
||||||
match frags.next() {
|
|
||||||
|
|
||||||
Some("when") => output.place(&When::new(
|
|
||||||
state.namespace(arg0?)?.unwrap(),
|
|
||||||
Thunk::new(move|output: &mut O|state.view(output, &arg1).unwrap())
|
|
||||||
)),
|
|
||||||
|
|
||||||
Some("either") => output.place(&Either::new(
|
|
||||||
state.namespace(arg0?)?.unwrap(),
|
|
||||||
Thunk::new(move|output: &mut O|state.view(output, &arg1).unwrap()),
|
|
||||||
Thunk::new(move|output: &mut O|state.view(output, &arg2).unwrap())
|
|
||||||
)),
|
|
||||||
|
|
||||||
Some("bsp") => output.place(&{
|
|
||||||
let a = Thunk::new(move|output: &mut O|state.view(output, &arg0).unwrap());
|
|
||||||
let b = Thunk::new(move|output: &mut O|state.view(output, &arg1).unwrap());
|
|
||||||
match frags.next() {
|
|
||||||
Some("n") => Bsp::n(a, b),
|
|
||||||
Some("s") => Bsp::s(a, b),
|
|
||||||
Some("e") => Bsp::e(a, b),
|
|
||||||
Some("w") => Bsp::w(a, b),
|
|
||||||
Some("a") => Bsp::a(a, b),
|
|
||||||
Some("b") => Bsp::b(a, b),
|
|
||||||
frag => unimplemented!("bsp/{frag:?}")
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
Some("align") => output.place(&{
|
|
||||||
let a = Thunk::new(move|output: &mut O|state.view(output, &arg0).unwrap());
|
|
||||||
match frags.next() {
|
|
||||||
Some("n") => Align::n(a),
|
|
||||||
Some("s") => Align::s(a),
|
|
||||||
Some("e") => Align::e(a),
|
|
||||||
Some("w") => Align::w(a),
|
|
||||||
Some("x") => Align::x(a),
|
|
||||||
Some("y") => Align::y(a),
|
|
||||||
Some("c") => Align::c(a),
|
|
||||||
frag => unimplemented!("align/{frag:?}")
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
Some("fill") => output.place(&{
|
|
||||||
let a = Thunk::new(move|output: &mut O|state.view(output, &arg0).unwrap());
|
|
||||||
match frags.next() {
|
|
||||||
Some("xy") | None => Fill::XY(a),
|
|
||||||
Some("x") => Fill::X(a),
|
|
||||||
Some("y") => Fill::Y(a),
|
|
||||||
frag => unimplemented!("fill/{frag:?}")
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
Some("fixed") => output.place(&{
|
|
||||||
let axis = frags.next();
|
|
||||||
let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") };
|
|
||||||
let cb = Thunk::new(move|output: &mut O|state.view(output, &arg).unwrap());
|
|
||||||
match axis {
|
|
||||||
Some("xy") | None => Fixed::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb),
|
|
||||||
Some("x") => Fixed::X(state.namespace(arg0?)?.unwrap(), cb),
|
|
||||||
Some("y") => Fixed::Y(state.namespace(arg0?)?.unwrap(), cb),
|
|
||||||
frag => unimplemented!("fixed/{frag:?} ({expr:?}) ({head:?}) ({:?})",
|
|
||||||
head.src()?.unwrap_or_default().split("/").next())
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
Some("min") => output.place(&{
|
|
||||||
let axis = frags.next();
|
|
||||||
let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") };
|
|
||||||
let cb = Thunk::new(move|output: &mut O|state.view(output, &arg).unwrap());
|
|
||||||
match axis {
|
|
||||||
Some("xy") | None => Min::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb),
|
|
||||||
Some("x") => Min::X(state.namespace(arg0?)?.unwrap(), cb),
|
|
||||||
Some("y") => Min::Y(state.namespace(arg0?)?.unwrap(), cb),
|
|
||||||
frag => unimplemented!("min/{frag:?}")
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
Some("max") => output.place(&{
|
|
||||||
let axis = frags.next();
|
|
||||||
let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") };
|
|
||||||
let cb = Thunk::new(move|output: &mut O|state.view(output, &arg).unwrap());
|
|
||||||
match axis {
|
|
||||||
Some("xy") | None => Max::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb),
|
|
||||||
Some("x") => Max::X(state.namespace(arg0?)?.unwrap(), cb),
|
|
||||||
Some("y") => Max::Y(state.namespace(arg0?)?.unwrap(), cb),
|
|
||||||
frag => unimplemented!("max/{frag:?}")
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
Some("push") => output.place(&{
|
|
||||||
let axis = frags.next();
|
|
||||||
let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") };
|
|
||||||
let cb = Thunk::new(move|output: &mut O|state.view(output, &arg).unwrap());
|
|
||||||
match axis {
|
|
||||||
Some("xy") | None => Push::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb),
|
|
||||||
Some("x") => Push::X(state.namespace(arg0?)?.unwrap(), cb),
|
|
||||||
Some("y") => Push::Y(state.namespace(arg0?)?.unwrap(), cb),
|
|
||||||
frag => unimplemented!("push/{frag:?}")
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
_ => return Ok(false)
|
|
||||||
|
|
||||||
};
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
|
|
@ -1,132 +0,0 @@
|
||||||
use proptest::{prelude::*, option::of};
|
|
||||||
use proptest_derive::Arbitrary;
|
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
#[test] fn test_area () {
|
|
||||||
assert_eq!(XYWH(10u16, 10, 20, 20).center(), XY(20, 20));
|
|
||||||
}
|
|
||||||
|
|
||||||
proptest! {
|
|
||||||
#[test] fn proptest_direction (
|
|
||||||
d in prop_oneof![
|
|
||||||
Just(North), Just(South),
|
|
||||||
Just(East), Just(West),
|
|
||||||
Just(Above), Just(Below)
|
|
||||||
],
|
|
||||||
x in u16::MIN..u16::MAX,
|
|
||||||
y in u16::MIN..u16::MAX,
|
|
||||||
w in u16::MIN..u16::MAX,
|
|
||||||
h in u16::MIN..u16::MAX,
|
|
||||||
a in u16::MIN..u16::MAX,
|
|
||||||
) {
|
|
||||||
let _ = d.split_fixed(XYWH(x, y, w, h), a);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
proptest! {
|
|
||||||
#[test] fn proptest_area (
|
|
||||||
x in u16::MIN..u16::MAX,
|
|
||||||
y in u16::MIN..u16::MAX,
|
|
||||||
w in u16::MIN..u16::MAX,
|
|
||||||
h in u16::MIN..u16::MAX,
|
|
||||||
a in u16::MIN..u16::MAX,
|
|
||||||
b in u16::MIN..u16::MAX,
|
|
||||||
) {
|
|
||||||
let _: XYWH<u16> = XYWH::zero();
|
|
||||||
//let _: XYWH<u16> = XYWH::from_position([a, b]);
|
|
||||||
//let _: XYWH<u16> = XYWH::from_size([a, b]);
|
|
||||||
let area: XYWH<u16> = XYWH(x, y, w, h);
|
|
||||||
//let _ = area.expect_min(a, b);
|
|
||||||
let _ = area.xy();
|
|
||||||
let _ = area.wh();
|
|
||||||
//let _ = area.xywh();
|
|
||||||
let _ = area.clipped_h(a);
|
|
||||||
let _ = area.clipped_w(b);
|
|
||||||
let _ = area.clipped(WH(a, b));
|
|
||||||
//let _ = area.set_w(a);
|
|
||||||
//let _ = area.set_h(b);
|
|
||||||
let _ = area.x2();
|
|
||||||
let _ = area.y2();
|
|
||||||
let _ = area.lrtb();
|
|
||||||
let _ = area.center();
|
|
||||||
let _ = area.centered();
|
|
||||||
let _ = area.centered_x(a);
|
|
||||||
let _ = area.centered_y(b);
|
|
||||||
let _ = area.centered_xy([a, b]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
proptest! {
|
|
||||||
#[test] fn proptest_size (
|
|
||||||
x in u16::MIN..u16::MAX,
|
|
||||||
y in u16::MIN..u16::MAX,
|
|
||||||
a in u16::MIN..u16::MAX,
|
|
||||||
b in u16::MIN..u16::MAX,
|
|
||||||
) {
|
|
||||||
let size = WH(x, y);
|
|
||||||
let _ = size.w();
|
|
||||||
let _ = size.h();
|
|
||||||
let _ = size.wh();
|
|
||||||
let _ = size.clip_w(a);
|
|
||||||
let _ = size.clip_h(b);
|
|
||||||
//let _ = size.expect_min(a, b);
|
|
||||||
//let _ = size.to_area_pos();
|
|
||||||
//let _ = size.to_area_size();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! test_op_transform {
|
|
||||||
($fn:ident, $Op:ident) => {
|
|
||||||
proptest! {
|
|
||||||
#[test] fn $fn (
|
|
||||||
op_x in of(u16::MIN..u16::MAX),
|
|
||||||
op_y in of(u16::MIN..u16::MAX),
|
|
||||||
content in "\\PC*",
|
|
||||||
x in u16::MIN..u16::MAX,
|
|
||||||
y in u16::MIN..u16::MAX,
|
|
||||||
w in u16::MIN..u16::MAX,
|
|
||||||
h in u16::MIN..u16::MAX,
|
|
||||||
) {
|
|
||||||
if let Some(op) = match (op_x, op_y) {
|
|
||||||
(Some(x), Some(y)) => Some($Op::XY(x, y, content)),
|
|
||||||
(Some(x), None) => Some($Op::X(x, content)),
|
|
||||||
(None, Some(y)) => Some($Op::Y(y, content)),
|
|
||||||
_ => None
|
|
||||||
} {
|
|
||||||
//assert_eq!(Content::layout(&op, [x, y, w, h]),
|
|
||||||
//Draw::layout(&op, [x, y, w, h]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
test_op_transform!(proptest_op_fixed, Fixed);
|
|
||||||
test_op_transform!(proptest_op_min, Min);
|
|
||||||
test_op_transform!(proptest_op_max, Max);
|
|
||||||
test_op_transform!(proptest_op_push, Push);
|
|
||||||
test_op_transform!(proptest_op_pull, Pull);
|
|
||||||
test_op_transform!(proptest_op_shrink, Shrink);
|
|
||||||
test_op_transform!(proptest_op_expand, Expand);
|
|
||||||
test_op_transform!(proptest_op_padding, Pad);
|
|
||||||
|
|
||||||
proptest! {
|
|
||||||
#[test] fn proptest_op_bsp (
|
|
||||||
d in prop_oneof![
|
|
||||||
Just(North), Just(South),
|
|
||||||
Just(East), Just(West),
|
|
||||||
Just(Above), Just(Below)
|
|
||||||
],
|
|
||||||
a in "\\PC*",
|
|
||||||
b in "\\PC*",
|
|
||||||
x in u16::MIN..u16::MAX,
|
|
||||||
y in u16::MIN..u16::MAX,
|
|
||||||
w in u16::MIN..u16::MAX,
|
|
||||||
h in u16::MIN..u16::MAX,
|
|
||||||
) {
|
|
||||||
let bsp = Bsp(d, a, b);
|
|
||||||
//assert_eq!(
|
|
||||||
//Content::layout(&bsp, [x, y, w, h]),
|
|
||||||
//Draw::layout(&bsp, [x, y, w, h]),
|
|
||||||
//);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,153 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
/// Drawing target.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use tengri::output::*;
|
|
||||||
/// struct TestOut(XYWH<u16>);
|
|
||||||
/// impl Out for TestOut {
|
|
||||||
/// type Unit = u16;
|
|
||||||
/// fn area (&self) -> XYWH<u16> { self.0 }
|
|
||||||
/// fn area_mut (&mut self) -> &mut XYWH<u16> { &mut self.0 }
|
|
||||||
/// fn place_at <T: Draw<Self> + ?Sized> (&mut self, area: XYWH<u16>, _: &T) {
|
|
||||||
/// println!("place_at: {area:?}");
|
|
||||||
/// ()
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// impl Draw<TestOut> for String {
|
|
||||||
/// fn draw (&self, to: &mut TestOut) {
|
|
||||||
/// //to.area_mut().set_w(self.len() as u16);
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
pub trait Out: Send + Sync + Sized {
|
|
||||||
/// Unit of length
|
|
||||||
type Unit: Coord;
|
|
||||||
/// Current output area
|
|
||||||
fn area (&self) -> XYWH<Self::Unit>;
|
|
||||||
/// Mutable pointer to area.
|
|
||||||
fn area_mut (&mut self) -> &mut XYWH<Self::Unit>;
|
|
||||||
/// Render drawable in area specified by `area`
|
|
||||||
fn place_at <'t, T: Draw<Self> + ?Sized> (&mut self, area: XYWH<Self::Unit>, content: &'t T);
|
|
||||||
/// Render drawable in area specified by `T::layout(self.area())`
|
|
||||||
#[inline] fn place <'t, T: Content<Self> + ?Sized> (&mut self, content: &'t T) {
|
|
||||||
self.place_at(content.layout(self.area()), content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A numeric type that can be used as coordinate.
|
|
||||||
///
|
|
||||||
/// FIXME: Replace this ad-hoc trait with `num` crate.
|
|
||||||
pub trait Coord: Send + Sync + Copy
|
|
||||||
+ Add<Self, Output=Self>
|
|
||||||
+ Sub<Self, Output=Self>
|
|
||||||
+ Mul<Self, Output=Self>
|
|
||||||
+ Div<Self, Output=Self>
|
|
||||||
+ Ord + PartialEq + Eq
|
|
||||||
+ Debug + Display + Default
|
|
||||||
+ From<u16> + Into<u16>
|
|
||||||
+ Into<usize>
|
|
||||||
+ Into<f64>
|
|
||||||
{
|
|
||||||
fn plus (self, other: Self) -> Self;
|
|
||||||
fn minus (self, other: Self) -> Self {
|
|
||||||
if self >= other { self - other } else { 0.into() }
|
|
||||||
}
|
|
||||||
fn atomic (self) -> AtomicUsize {
|
|
||||||
AtomicUsize::new(self.into())
|
|
||||||
}
|
|
||||||
fn zero () -> Self {
|
|
||||||
0.into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Drawable with dynamic dispatch.
|
|
||||||
pub trait Draw<O: Out> {
|
|
||||||
fn draw (&self, to: &mut O);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// FIXME: This is a general implementation: should be called `Eval` and be part of [dizzle].
|
|
||||||
/// Matches [Language] expressions to renderings for a given [Output] target.
|
|
||||||
pub trait View<O, U> {
|
|
||||||
fn view_expr <'a> (&'a self, _output: &mut O, expr: &'a impl Expression) -> Usually<U> {
|
|
||||||
Err(format!("View::view_expr: no exprs defined: {expr:?}").into())
|
|
||||||
}
|
|
||||||
fn view_word <'a> (&'a self, _output: &mut O, word: &'a impl Symbol) -> Usually<U> {
|
|
||||||
Err(format!("View::view_word: no words defined: {word:?}").into())
|
|
||||||
}
|
|
||||||
fn view <'a> (&'a self, output: &mut O, dsl: &'a impl Language) -> Usually<U> {
|
|
||||||
match (dsl.expr(), dsl.word()) {
|
|
||||||
(Ok(Some(e)), _ ) => self.view_expr(output, &e),
|
|
||||||
(_, Ok(Some(w))) => self.view_word(output, &w),
|
|
||||||
(Err(e), _ ) => Err(format!("invalid view expr:\n{dsl:?}\n{e}").into()),
|
|
||||||
(_, Err(w) ) => Err(format!("invalid view word:\n{dsl:?}\n{w}").into()),
|
|
||||||
(Ok(None), Ok(None) ) => Err(format!("empty view:\n{dsl:?}").into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Outputs combinator.
|
|
||||||
pub trait Lay<O: Out>: Sized {}
|
|
||||||
|
|
||||||
/// Drawable area of display.
|
|
||||||
pub trait Layout<O: Out> {
|
|
||||||
fn layout_x (&self, to: XYWH<O::Unit>) -> O::Unit { to.x() }
|
|
||||||
fn layout_y (&self, to: XYWH<O::Unit>) -> O::Unit { to.y() }
|
|
||||||
fn layout_w_min (&self, _t: XYWH<O::Unit>) -> O::Unit { 0.into() }
|
|
||||||
fn layout_w_max (&self, to: XYWH<O::Unit>) -> O::Unit { to.w() }
|
|
||||||
fn layout_w (&self, to: XYWH<O::Unit>) -> O::Unit { to.w().max(self.layout_w_min(to)).min(self.layout_w_max(to)) }
|
|
||||||
fn layout_h_min (&self, _t: XYWH<O::Unit>) -> O::Unit { 0.into() }
|
|
||||||
fn layout_h_max (&self, to: XYWH<O::Unit>) -> O::Unit { to.h() }
|
|
||||||
fn layout_h (&self, to: XYWH<O::Unit>) -> O::Unit { to.h().max(self.layout_h_min(to)).min(self.layout_h_max(to)) }
|
|
||||||
fn layout (&self, to: XYWH<O::Unit>) -> XYWH<O::Unit> {
|
|
||||||
XYWH(self.layout_x(to), self.layout_y(to), self.layout_w(to), self.layout_h(to))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait HasContent<O: Out> {
|
|
||||||
fn content (&self) -> impl Content<O>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO DOCUMENTME
|
|
||||||
pub trait Content<O: Out>: Draw<O> + Layout<O> {}
|
|
||||||
|
|
||||||
// Something that has an origin point (X, Y).
|
|
||||||
pub trait HasXY<N: Coord> {
|
|
||||||
fn x (&self) -> N;
|
|
||||||
fn y (&self) -> N;
|
|
||||||
fn xy (&self) -> XY<N> { XY(self.x(), self.y()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Something that has a size (W, H).
|
|
||||||
pub trait HasWH<N: Coord> {
|
|
||||||
fn w (&self) -> N;
|
|
||||||
fn h (&self) -> N;
|
|
||||||
fn wh (&self) -> WH<N> { WH(self.w(), self.h()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Something that has a 2D bounding box (X, Y, W, H).
|
|
||||||
//
|
|
||||||
// FIXME: The other way around?
|
|
||||||
pub trait HasXYWH<N: Coord>: HasXY<N> + HasWH<N> {
|
|
||||||
fn x2 (&self) -> N { self.x().plus(self.w()) }
|
|
||||||
fn y2 (&self) -> N { self.y().plus(self.h()) }
|
|
||||||
fn xywh (&self) -> XYWH<N> { XYWH(self.x(), self.y(), self.w(), self.h()) }
|
|
||||||
fn expect_min (&self, w: N, h: N) -> Usually<&Self> {
|
|
||||||
if self.w() < w || self.h() < h {
|
|
||||||
Err(format!("min {w}x{h}").into())
|
|
||||||
} else {
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Something that has a [Measure] of its rendered size.
|
|
||||||
pub trait Measured<O: Out> {
|
|
||||||
fn measure (&self) -> &Measure<O>;
|
|
||||||
fn measure_width (&self) -> O::Unit { self.measure().w() }
|
|
||||||
fn measure_height (&self) -> O::Unit { self.measure().h() }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait HasPerf {
|
|
||||||
fn perf (&self) -> &PerfModel;
|
|
||||||
}
|
|
||||||
883
tengri/.scratch.rs
Normal file
883
tengri/.scratch.rs
Normal file
|
|
@ -0,0 +1,883 @@
|
||||||
|
///// The syntagm `(when :condition :content)` corresponds to a [When] layout element.
|
||||||
|
//impl<S, A> FromDsl<S> for When<A> where bool: FromDsl<S>, A: FromDsl<S> {
|
||||||
|
//fn try_provide (state: &S, source: &DslVal<impl DslStr, impl DslExpr>) -> Perhaps<Self> {
|
||||||
|
//source.exp_match("when", |_, tail|Ok(Some(Self(
|
||||||
|
//FromDsl::<S>::provide(state,
|
||||||
|
//tail.nth(0, ||"no condition".into())?, ||"no condition".into())?,
|
||||||
|
//FromDsl::<S>::provide(state,
|
||||||
|
//tail.nth(1, ||"no content".into())?, ||"no content".into())?,
|
||||||
|
//))))
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
///// The syntagm `(either :condition :content1 :content2)` corresponds to an [Either] layout element.
|
||||||
|
//impl<S, A, B> FromDsl<S> for Either<A, B> where S: Eval<Ast, bool> + Eval<Ast, A> + Eval<Ast, B> {
|
||||||
|
//fn try_provide (state: &S, source: &DslVal<impl DslStr, impl DslExpr>) -> Perhaps<Self> {
|
||||||
|
//source.exp_match("either", |_, tail|Ok(Some(Self(
|
||||||
|
//state.eval(tail.nth(0, ||"no condition")?, ||"no condition")?,
|
||||||
|
//state.eval(tail.nth(1, ||"no content 1")?, ||"no content 1")?,
|
||||||
|
//state.eval(tail.nth(2, ||"no content 1")?, ||"no content 2")?,
|
||||||
|
//))))
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
///// The syntagm `(align/* :content)` corresponds to an [Align] layout element,
|
||||||
|
///// where `*` specifies the direction of the alignment.
|
||||||
|
//impl<S, A> FromDsl<S> for Align<A> where S: Eval<Option<Ast>, A> {
|
||||||
|
//fn try_provide (state: &S, source: &DslVal<impl DslStr, impl DslExpr>) -> Perhaps<Self> {
|
||||||
|
//source.exp_match("align/", |head, tail|Ok(Some(match head {
|
||||||
|
//"c" => Self::c(state.eval(tail.nth(0, ||"no content")?, ||"no content")),
|
||||||
|
//"x" => Self::x(state.eval(tail.nth(0, ||"no content")?, ||"no content")),
|
||||||
|
//"y" => Self::y(state.eval(tail.nth(0, ||"no content")?, ||"no content")),
|
||||||
|
//"n" => Self::n(state.eval(tail.nth(0, ||"no content")?, ||"no content")),
|
||||||
|
//"s" => Self::s(state.eval(tail.nth(0, ||"no content")?, ||"no content")),
|
||||||
|
//"e" => Self::e(state.eval(tail.nth(0, ||"no content")?, ||"no content")),
|
||||||
|
//"w" => Self::w(state.eval(tail.nth(0, ||"no content")?, ||"no content")),
|
||||||
|
//"nw" => Self::nw(state.eval(tail.nth(0, ||"no content")?, ||"no content")),
|
||||||
|
//"ne" => Self::ne(state.eval(tail.nth(0, ||"no content")?, ||"no content")),
|
||||||
|
//"sw" => Self::sw(state.eval(tail.nth(0, ||"no content")?, ||"no content")),
|
||||||
|
//"se" => Self::se(state.eval(tail.nth(0, ||"no content")?, ||"no content")),
|
||||||
|
//_ => return Err("invalid align variant".into())
|
||||||
|
//})))
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
///// The syntagm `(bsp/* :content1 :content2)` corresponds to a [Bsp] layout element,
|
||||||
|
///// where `*` specifies the direction of the split.
|
||||||
|
//impl<S, A, B> FromDsl<S> for Bsp<A, B> where S: Eval<Option<Ast>, A> + Eval<Option<Ast>, B> {
|
||||||
|
//fn try_provide (state: &S, source: &DslVal<impl DslStr, impl DslExpr>) -> Perhaps<Self> {
|
||||||
|
//source.exp_match("bsp/", |head, tail|Ok(Some(match head {
|
||||||
|
//"n" => Self::n(tail.nth(0, ||"no content 1"), tail.nth(1, ||"no content 2")),
|
||||||
|
//"s" => Self::s(tail.nth(0, ||"no content 1"), tail.nth(1, ||"no content 2")),
|
||||||
|
//"e" => Self::e(tail.nth(0, ||"no content 1"), tail.nth(1, ||"no content 2")),
|
||||||
|
//"w" => Self::w(tail.nth(0, ||"no content 1"), tail.nth(1, ||"no content 2")),
|
||||||
|
//"a" => Self::a(tail.nth(0, ||"no content 1"), tail.nth(1, ||"no content 2")),
|
||||||
|
//"b" => Self::b(tail.nth(0, ||"no content 1"), tail.nth(1, ||"no content 2")),
|
||||||
|
//_ => return Ok(None),
|
||||||
|
//})))
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//#[cfg(feature = "dsl")] take!($Enum<A>, A|state, words|Ok(
|
||||||
|
//if let Some(Token { value: Key(k), .. }) = words.peek() {
|
||||||
|
//let mut base = words.clone();
|
||||||
|
//let content = state.give_or_fail(words, ||format!("{k}: no content"))?;
|
||||||
|
//return Ok(Some(match words.next() {
|
||||||
|
//Some(Token{value: Key($x),..}) => Self::x(content),
|
||||||
|
//Some(Token{value: Key($y),..}) => Self::y(content),
|
||||||
|
//Some(Token{value: Key($xy),..}) => Self::XY(content),
|
||||||
|
//_ => unreachable!()
|
||||||
|
//}))
|
||||||
|
//} else {
|
||||||
|
//None
|
||||||
|
//}));
|
||||||
|
//#[cfg(feature = "dsl")] take!($Enum<U, A>, U, A|state, words|Ok(
|
||||||
|
//if let Some(Token { value: Key($x|$y|$xy), .. }) = words.peek() {
|
||||||
|
//let mut base = words.clone();
|
||||||
|
//Some(match words.next() {
|
||||||
|
//Some(Token { value: Key($x), .. }) => Self::x(
|
||||||
|
//state.give_or_fail(words, ||"x: no unit")?,
|
||||||
|
//state.give_or_fail(words, ||"x: no content")?,
|
||||||
|
//),
|
||||||
|
//Some(Token { value: Key($y), .. }) => Self::y(
|
||||||
|
//state.give_or_fail(words, ||"y: no unit")?,
|
||||||
|
//state.give_or_fail(words, ||"y: no content")?,
|
||||||
|
//),
|
||||||
|
//Some(Token { value: Key($x), .. }) => Self::XY(
|
||||||
|
//state.give_or_fail(words, ||"xy: no unit x")?,
|
||||||
|
//state.give_or_fail(words, ||"xy: no unit y")?,
|
||||||
|
//state.give_or_fail(words, ||"xy: no content")?
|
||||||
|
//),
|
||||||
|
//_ => unreachable!(),
|
||||||
|
//})
|
||||||
|
//} else {
|
||||||
|
//None
|
||||||
|
//}));
|
||||||
|
//if let Exp(_, exp) = source.value() {
|
||||||
|
//let mut rest = exp.clone();
|
||||||
|
//return Ok(Some(match rest.next().as_ref().and_then(|x|x.key()) {
|
||||||
|
//Some("bsp/n") => Self::n(
|
||||||
|
//state.eval(rest.next(), ||"bsp/n: no content 1")?,
|
||||||
|
//state.eval(rest.next(), ||"bsp/n: no content 2")?,
|
||||||
|
//),
|
||||||
|
//Some("bsp/s") => Self::s(
|
||||||
|
//state.eval(rest.next(), ||"bsp/s: no content 1")?,
|
||||||
|
//state.eval(rest.next(), ||"bsp/s: no content 2")?,
|
||||||
|
//),
|
||||||
|
//Some("bsp/e") => Self::e(
|
||||||
|
//state.eval(rest.next(), ||"bsp/e: no content 1")?,
|
||||||
|
//state.eval(rest.next(), ||"bsp/e: no content 2")?,
|
||||||
|
//),
|
||||||
|
//Some("bsp/w") => Self::w(
|
||||||
|
//state.eval(rest.next(), ||"bsp/w: no content 1")?,
|
||||||
|
//state.eval(rest.next(), ||"bsp/w: no content 2")?,
|
||||||
|
//),
|
||||||
|
//Some("bsp/a") => Self::a(
|
||||||
|
//state.eval(rest.next(), ||"bsp/a: no content 1")?,
|
||||||
|
//state.eval(rest.next(), ||"bsp/a: no content 2")?,
|
||||||
|
//),
|
||||||
|
//Some("bsp/b") => Self::b(
|
||||||
|
//state.eval(rest.next(), ||"bsp/b: no content 1")?,
|
||||||
|
//state.eval(rest.next(), ||"bsp/b: no content 2")?,
|
||||||
|
//),
|
||||||
|
//_ => return Ok(None),
|
||||||
|
//}))
|
||||||
|
//}
|
||||||
|
//Ok(None)
|
||||||
|
//if let Exp(_, source) = source.value() {
|
||||||
|
//let mut rest = source.clone();
|
||||||
|
//return Ok(Some(match rest.next().as_ref().and_then(|x|x.key()) {
|
||||||
|
//Some("align/c") => Self::c(state.eval(rest.next(), ||"align/c: no content")?),
|
||||||
|
//Some("align/x") => Self::x(state.eval(rest.next(), ||"align/x: no content")?),
|
||||||
|
//Some("align/y") => Self::y(state.eval(rest.next(), ||"align/y: no content")?),
|
||||||
|
//Some("align/n") => Self::n(state.eval(rest.next(), ||"align/n: no content")?),
|
||||||
|
//Some("align/s") => Self::s(state.eval(rest.next(), ||"align/s: no content")?),
|
||||||
|
//Some("align/e") => Self::e(state.eval(rest.next(), ||"align/e: no content")?),
|
||||||
|
//Some("align/w") => Self::w(state.eval(rest.next(), ||"align/w: no content")?),
|
||||||
|
//Some("align/nw") => Self::nw(state.eval(rest.next(), ||"align/nw: no content")?),
|
||||||
|
//Some("align/ne") => Self::ne(state.eval(rest.next(), ||"align/ne: no content")?),
|
||||||
|
//Some("align/sw") => Self::sw(state.eval(rest.next(), ||"align/sw: no content")?),
|
||||||
|
//Some("align/se") => Self::se(state.eval(rest.next(), ||"align/se: no content")?),
|
||||||
|
//_ => return Ok(None),
|
||||||
|
//}))
|
||||||
|
//}
|
||||||
|
//Ok(None)
|
||||||
|
//Ok(match source.exp_head().and_then(|e|e.key()) {
|
||||||
|
//Some("either") => Some(Self(
|
||||||
|
//source.exp_tail().and_then(|t|t.get(0)).map(|x|state.eval(x, ||"when: no condition"))?,
|
||||||
|
//source.exp_tail().and_then(|t|t.get(1)).map(|x|state.eval(x, ||"when: no content 1"))?,
|
||||||
|
//source.exp_tail().and_then(|t|t.get(2)).map(|x|state.eval(x, ||"when: no content 2"))?,
|
||||||
|
//)),
|
||||||
|
//_ => None
|
||||||
|
//})
|
||||||
|
//if let Exp(_, mut exp) = source.value()
|
||||||
|
//&& let Some(Ast(Key(id))) = exp.peek() && *id == *"either" {
|
||||||
|
//let _ = exp.next();
|
||||||
|
//return Ok(Some(Self(
|
||||||
|
//state.eval(exp.next().unwrap(), ||"either: no condition")?,
|
||||||
|
//state.eval(exp.next().unwrap(), ||"either: no content 1")?,
|
||||||
|
//state.eval(exp.next().unwrap(), ||"either: no content 2")?,
|
||||||
|
//)))
|
||||||
|
//}
|
||||||
|
//Ok(None)
|
||||||
|
//Ok(match source.exp_head().and_then(|e|e.key()) {
|
||||||
|
//Some("when") => Some(Self(
|
||||||
|
//source.exp_tail().and_then(|t|t.get(0)).map(|x|state.eval(x, ||"when: no condition"))?,
|
||||||
|
//source.exp_tail().and_then(|t|t.get(1)).map(|x|state.eval(x, ||"when: no content"))?,
|
||||||
|
//)),
|
||||||
|
//_ => None
|
||||||
|
//})
|
||||||
|
//use crate::*;
|
||||||
|
//use Direction::*;
|
||||||
|
|
||||||
|
//pub struct Stack<'x, E, F1> {
|
||||||
|
//__: PhantomData<&'x (E, F1)>,
|
||||||
|
//direction: Direction,
|
||||||
|
//callback: F1
|
||||||
|
//}
|
||||||
|
|
||||||
|
//impl<'x, E, F1> Stack<'x, E, F1> {
|
||||||
|
//pub fn new (direction: Direction, callback: F1) -> Self {
|
||||||
|
//Self { direction, callback, __: Default::default(), }
|
||||||
|
//}
|
||||||
|
//pub fn above (callback: F1) -> Self {
|
||||||
|
//Self::new(Above, callback)
|
||||||
|
//}
|
||||||
|
//pub fn below (callback: F1) -> Self {
|
||||||
|
//Self::new(Below, callback)
|
||||||
|
//}
|
||||||
|
//pub fn north (callback: F1) -> Self {
|
||||||
|
//Self::new(North, callback)
|
||||||
|
//}
|
||||||
|
//pub fn south (callback: F1) -> Self {
|
||||||
|
//Self::new(South, callback)
|
||||||
|
//}
|
||||||
|
//pub fn east (callback: F1) -> Self {
|
||||||
|
//Self::new(East, callback)
|
||||||
|
//}
|
||||||
|
//pub fn west (callback: F1) -> Self {
|
||||||
|
//Self::new(West, callback)
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
//impl<'x, E: Out, F1: Fn(&mut dyn FnMut(&dyn Layout<E>))> Layout<E> for Stack<'x, E, F1> {
|
||||||
|
//fn layout (&self, to: E::Area) -> E::Area {
|
||||||
|
//let state = StackLayoutState::<E>::new(self.direction, to);
|
||||||
|
//(self.callback)(&mut |component: &dyn Layout<E>|{
|
||||||
|
//let StackLayoutState { x, y, w_remaining, h_remaining, .. } = *state.borrow();
|
||||||
|
//let [_, _, w, h] = component.layout([x, y, w_remaining, h_remaining].into()).xywh();
|
||||||
|
//state.borrow_mut().grow(w, h);
|
||||||
|
//});
|
||||||
|
//let StackLayoutState { w_used, h_used, .. } = *state.borrow();
|
||||||
|
//match self.direction {
|
||||||
|
//North | West => { todo!() },
|
||||||
|
//South | East => { [to.x(), to.y(), w_used, h_used].into() },
|
||||||
|
//_ => unreachable!(),
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//impl<'x, E: Out, F1: Fn(&mut dyn FnMut(&dyn Draw<E>))> Draw<E> for Stack<'x, E, F1> {
|
||||||
|
//fn draw (&self, to: &mut E) {
|
||||||
|
//let state = StackLayoutState::<E>::new(self.direction, to.area());
|
||||||
|
//let to = Rc::new(RefCell::new(to));
|
||||||
|
//(self.callback)(&mut |component: &dyn Draw<E>|{
|
||||||
|
//let StackLayoutState { x, y, w_remaining, h_remaining, .. } = *state.borrow();
|
||||||
|
//let layout = component.layout([x, y, w_remaining, h_remaining].into());
|
||||||
|
//state.borrow_mut().grow(layout.w(), layout.h());
|
||||||
|
//to.borrow_mut().place_at(layout, component);
|
||||||
|
//});
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
//#[derive(Copy, Clone)]
|
||||||
|
//struct StackLayoutState<E: Out> {
|
||||||
|
//direction: Direction,
|
||||||
|
//x: E::Unit,
|
||||||
|
//y: E::Unit,
|
||||||
|
//w_used: E::Unit,
|
||||||
|
//h_used: E::Unit,
|
||||||
|
//w_remaining: E::Unit,
|
||||||
|
//h_remaining: E::Unit,
|
||||||
|
//}
|
||||||
|
//impl<E: Out> StackLayoutState<E> {
|
||||||
|
//fn new (direction: Direction, area: E::Area) -> std::rc::Rc<std::cell::RefCell<Self>> {
|
||||||
|
//let [x, y, w_remaining, h_remaining] = area.xywh();
|
||||||
|
//std::rc::Rc::new(std::cell::RefCell::new(Self {
|
||||||
|
//direction,
|
||||||
|
//x, y, w_remaining, h_remaining,
|
||||||
|
//w_used: E::Unit::zero(), h_used: E::Unit::zero()
|
||||||
|
//}))
|
||||||
|
//}
|
||||||
|
//fn grow (&mut self, w: E::Unit, h: E::Unit) -> &mut Self {
|
||||||
|
//match self.direction {
|
||||||
|
//South => { self.y = self.y.plus(h);
|
||||||
|
//self.h_used = self.h_used.plus(h);
|
||||||
|
//self.h_remaining = self.h_remaining.minus(h);
|
||||||
|
//self.w_used = self.w_used.max(w); },
|
||||||
|
//East => { self.x = self.x.plus(w);
|
||||||
|
//self.w_used = self.w_used.plus(w);
|
||||||
|
//self.w_remaining = self.w_remaining.minus(w);
|
||||||
|
//self.h_used = self.h_used.max(h); },
|
||||||
|
//North | West => { todo!() },
|
||||||
|
//Above | Below => {},
|
||||||
|
//};
|
||||||
|
//self
|
||||||
|
//}
|
||||||
|
//fn area_remaining (&self) -> E::Area {
|
||||||
|
//[self.x, self.y, self.w_remaining, self.h_remaining].into()
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
////pub struct Stack<'a, E, F1> {
|
||||||
|
////__: PhantomData<&'a (E, F1)>,
|
||||||
|
////direction: Direction,
|
||||||
|
////callback: F1
|
||||||
|
////}
|
||||||
|
////impl<'a, E, F1> Stack<'a, E, F1> where
|
||||||
|
////E: Out, F1: Fn(&mut dyn FnMut(&'a dyn Draw<E>)) + Send + Sync,
|
||||||
|
////{
|
||||||
|
////pub fn north (callback: F1) -> Self { Self::new(North, callback) }
|
||||||
|
////pub fn south (callback: F1) -> Self { Self::new(South, callback) }
|
||||||
|
////pub fn east (callback: F1) -> Self { Self::new(East, callback) }
|
||||||
|
////pub fn west (callback: F1) -> Self { Self::new(West, callback) }
|
||||||
|
////pub fn above (callback: F1) -> Self { Self::new(Above, callback) }
|
||||||
|
////pub fn below (callback: F1) -> Self { Self::new(Below, callback) }
|
||||||
|
////pub fn new (direction: Direction, callback: F1) -> Self {
|
||||||
|
////Self { direction, callback, __: Default::default(), }
|
||||||
|
////}
|
||||||
|
////}
|
||||||
|
////impl<'a, E, F1> Draw<E> for Stack<'a, E, F1> where
|
||||||
|
////E: Out, F1: Fn(&mut dyn FnMut(&'a dyn Draw<E>)) + Send + Sync,
|
||||||
|
////{
|
||||||
|
////fn layout (&self, to: E::Area) -> E::Area {
|
||||||
|
////let state = StackLayoutState::<E>::new(self.direction, to);
|
||||||
|
////let mut adder = {
|
||||||
|
////let state = state.clone();
|
||||||
|
////move|component: &dyn Draw<E>|{
|
||||||
|
////let [w, h] = component.layout(state.borrow().area_remaining()).wh();
|
||||||
|
////state.borrow_mut().grow(w, h);
|
||||||
|
////}
|
||||||
|
////};
|
||||||
|
////(self.callback)(&mut adder);
|
||||||
|
////let StackLayoutState { w_used, h_used, .. } = *state.borrow();
|
||||||
|
////match self.direction {
|
||||||
|
////North | West => { todo!() },
|
||||||
|
////South | East => { [to.x(), to.y(), w_used, h_used].into() },
|
||||||
|
////Above | Below => { [to.x(), to.y(), to.w(), to.h()].into() },
|
||||||
|
////}
|
||||||
|
////}
|
||||||
|
////fn draw (&self, to: &mut E) {
|
||||||
|
////let state = StackLayoutState::<E>::new(self.direction, to.area());
|
||||||
|
////let mut adder = {
|
||||||
|
////let state = state.clone();
|
||||||
|
////move|component: &dyn Draw<E>|{
|
||||||
|
////let [x, y, w, h] = component.layout(state.borrow().area_remaining()).xywh();
|
||||||
|
////state.borrow_mut().grow(w, h);
|
||||||
|
////to.place_at([x, y, w, h].into(), component);
|
||||||
|
////}
|
||||||
|
////};
|
||||||
|
////(self.callback)(&mut adder);
|
||||||
|
////}
|
||||||
|
////}
|
||||||
|
|
||||||
|
//[>Stack::down(|add|{
|
||||||
|
//let mut i = 0;
|
||||||
|
//for (_, name) in self.dirs.iter() {
|
||||||
|
//if i >= self.scroll {
|
||||||
|
//add(&Tui::bold(i == self.index, name.as_str()))?;
|
||||||
|
//}
|
||||||
|
//i += 1;
|
||||||
|
//}
|
||||||
|
//for (_, name) in self.files.iter() {
|
||||||
|
//if i >= self.scroll {
|
||||||
|
//add(&Tui::bold(i == self.index, name.as_str()))?;
|
||||||
|
//}
|
||||||
|
//i += 1;
|
||||||
|
//}
|
||||||
|
//add(&format!("{}/{i}", self.index))?;
|
||||||
|
//Ok(())
|
||||||
|
//}));*/
|
||||||
|
|
||||||
|
|
||||||
|
//#[test] fn test_iter_map () {
|
||||||
|
//struct Foo;
|
||||||
|
//impl<T: Out> Content<T> for Foo {}
|
||||||
|
//fn _make_map <T: Out, U: Content<T> + Send + Sync> (data: &Vec<U>) -> impl Draw<T> {
|
||||||
|
//Map::new(||data.iter(), |_foo, _index|{})
|
||||||
|
//}
|
||||||
|
//let _data = vec![Foo, Foo, Foo];
|
||||||
|
////let map = make_map(&data);
|
||||||
|
//}
|
||||||
|
|
||||||
|
// FIXME
|
||||||
|
//use crate::{dsl::*, input::*, tui::TuiIn};
|
||||||
|
//use crossterm::event::{Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState};
|
||||||
|
//use std::cmp::Ordering;
|
||||||
|
|
||||||
|
//#[test] fn test_subcommand () -> Usually<()> {
|
||||||
|
//#[derive(Debug)] struct Event(crossterm::event::Event);
|
||||||
|
//impl Eq for Event {}
|
||||||
|
//impl PartialEq for Event { fn eq (&self, other: &Self) -> bool { todo!() } }
|
||||||
|
//impl Ord for Event { fn cmp (&self, other: &Self) -> Ordering { todo!() } }
|
||||||
|
//impl PartialOrd for Event { fn partial_cmp (&self, other: &Self) -> Option<Ordering> { None } }
|
||||||
|
//struct Test { keys: InputMap<Event, Ast> }
|
||||||
|
|
||||||
|
//handle!(TuiIn: |self: Test, input|Ok(None));[>if let Some(command) = self.keys.command(self, input) {
|
||||||
|
//Ok(Some(true))
|
||||||
|
//} else {
|
||||||
|
//Ok(None)
|
||||||
|
//});*/
|
||||||
|
|
||||||
|
//#[tengri_proc::command(Test)]
|
||||||
|
//impl TestCommand {
|
||||||
|
//fn do_thing (_state: &mut Test) -> Perhaps<Self> {
|
||||||
|
//Ok(None)
|
||||||
|
//}
|
||||||
|
//fn do_thing_arg (_state: &mut Test, _arg: usize) -> Perhaps<Self> {
|
||||||
|
//Ok(None)
|
||||||
|
//}
|
||||||
|
//fn do_sub (state: &mut Test, command: TestSubcommand) -> Perhaps<Self> {
|
||||||
|
//Ok(command.execute(state)?.map(|command|Self::DoSub { command }))
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
//#[tengri_proc::command(Test)]
|
||||||
|
//impl TestSubcommand {
|
||||||
|
//fn do_other_thing (_state: &mut Test) -> Perhaps<Self> {
|
||||||
|
//Ok(None)
|
||||||
|
//}
|
||||||
|
//fn do_other_thing_arg (_state: &mut Test, _arg: usize) -> Perhaps<Self> {
|
||||||
|
//Ok(None)
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
//let mut test = Test {
|
||||||
|
//keys: InputMap::from_source("
|
||||||
|
//(@a do-thing)
|
||||||
|
//(@b do-thing-arg 0)
|
||||||
|
//(@c do-sub do-other-thing)
|
||||||
|
//(@d do-sub do-other-thing-arg 0)
|
||||||
|
//")?
|
||||||
|
//};
|
||||||
|
|
||||||
|
////assert_eq!(Some(true), test.handle(&TuiIn(Default::default(), Event::Key(KeyEvent {
|
||||||
|
////kind: KeyEventKind::Press,
|
||||||
|
////code: KeyCode::Char('a'),
|
||||||
|
////modifiers: KeyModifiers::NONE,
|
||||||
|
////state: KeyEventState::NONE,
|
||||||
|
////})))?);
|
||||||
|
////assert_eq!(Some(true), test.handle(&TuiIn(Default::default(), Event::Key(KeyEvent {
|
||||||
|
////kind: KeyEventKind::Press,
|
||||||
|
////code: KeyCode::Char('b'),
|
||||||
|
////modifiers: KeyModifiers::NONE,
|
||||||
|
////state: KeyEventState::NONE,
|
||||||
|
////})))?);
|
||||||
|
////assert_eq!(Some(true), test.handle(&TuiIn(Default::default(), Event::Key(KeyEvent {
|
||||||
|
////kind: KeyEventKind::Press,
|
||||||
|
////code: KeyCode::Char('c'),
|
||||||
|
////modifiers: KeyModifiers::NONE,
|
||||||
|
////state: KeyEventState::NONE,
|
||||||
|
////})))?);
|
||||||
|
////assert_eq!(Some(true), test.handle(&TuiIn(Default::default(), Event::Key(KeyEvent {
|
||||||
|
////kind: KeyEventKind::Press,
|
||||||
|
////code: KeyCode::Char('d'),
|
||||||
|
////modifiers: KeyModifiers::NONE,
|
||||||
|
////state: KeyEventState::NONE,
|
||||||
|
////})))?);
|
||||||
|
////assert_eq!(None, test.handle(&TuiIn(Default::default(), Event::Key(KeyEvent {
|
||||||
|
////kind: KeyEventKind::Press,
|
||||||
|
////code: KeyCode::Char('z'),
|
||||||
|
////modifiers: KeyModifiers::NONE,
|
||||||
|
////state: KeyEventState::NONE,
|
||||||
|
////})))?);
|
||||||
|
//Ok(())
|
||||||
|
//}
|
||||||
|
|
||||||
|
//FIXME:
|
||||||
|
//#[cfg(test)] #[test] fn test_dsl_context () {
|
||||||
|
//use crate::dsl::{Dsl, Value};
|
||||||
|
|
||||||
|
//struct Test;
|
||||||
|
//#[tengri_proc::expose]
|
||||||
|
//impl Test {
|
||||||
|
//fn some_bool (&self) -> bool {
|
||||||
|
//true
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//assert_eq!(Dsl::get(&Test, &Value::Sym(":false")), Some(false));
|
||||||
|
//assert_eq!(Dsl::get(&Test, &Value::Sym(":true")), Some(true));
|
||||||
|
//assert_eq!(Dsl::get(&Test, &Value::Sym(":some-bool")), Some(true));
|
||||||
|
//assert_eq!(Dsl::get(&Test, &Value::Sym(":missing-bool")), None);
|
||||||
|
//assert_eq!(Dsl::get(&Test, &Value::Num(0)), Some(false));
|
||||||
|
//assert_eq!(Dsl::get(&Test, &Value::Num(1)), Some(true));
|
||||||
|
//}
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum FocusState<T: Copy + Debug + PartialEq> {
|
||||||
|
Focused(T),
|
||||||
|
Entered(T),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Copy + Debug + PartialEq> FocusState<T> {
|
||||||
|
pub fn inner (&self) -> T {
|
||||||
|
match self {
|
||||||
|
Self::Focused(inner) => *inner,
|
||||||
|
Self::Entered(inner) => *inner,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn set_inner (&mut self, inner: T) {
|
||||||
|
*self = match self {
|
||||||
|
Self::Focused(_) => Self::Focused(inner),
|
||||||
|
Self::Entered(_) => Self::Entered(inner),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn is_focused (&self) -> bool { matches!(self, Self::Focused(_)) }
|
||||||
|
pub fn is_entered (&self) -> bool { matches!(self, Self::Entered(_)) }
|
||||||
|
pub fn focus (&mut self) { *self = Self::Focused(self.inner()) }
|
||||||
|
pub fn enter (&mut self) { *self = Self::Entered(self.inner()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||||
|
pub enum FocusCommand<T: Send + Sync> {
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Next,
|
||||||
|
Prev,
|
||||||
|
Enter,
|
||||||
|
Exit,
|
||||||
|
Set(T)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F: HasFocus + HasEnter + FocusGrid + FocusOrder> Command<F> for FocusCommand<F::Item> {
|
||||||
|
fn execute (self, state: &mut F) -> Perhaps<FocusCommand<F::Item>> {
|
||||||
|
match self {
|
||||||
|
Self::Next => { state.focus_next(); },
|
||||||
|
Self::Prev => { state.focus_prev(); },
|
||||||
|
Self::Up => { state.focus_up(); },
|
||||||
|
Self::Down => { state.focus_down(); },
|
||||||
|
Self::Left => { state.focus_left(); },
|
||||||
|
Self::Right => { state.focus_right(); },
|
||||||
|
Self::Enter => { state.focus_enter(); },
|
||||||
|
Self::Exit => { state.focus_exit(); },
|
||||||
|
Self::Set(to) => { state.set_focused(to); },
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for things that have focusable subparts.
|
||||||
|
pub trait HasFocus {
|
||||||
|
type Item: Copy + PartialEq + Debug + Send + Sync;
|
||||||
|
/// Get the currently focused item.
|
||||||
|
fn focused (&self) -> Self::Item;
|
||||||
|
/// Get the currently focused item.
|
||||||
|
fn set_focused (&mut self, to: Self::Item);
|
||||||
|
/// Loop forward until a specific item is focused.
|
||||||
|
fn focus_to (&mut self, to: Self::Item) {
|
||||||
|
self.set_focused(to);
|
||||||
|
self.focus_updated();
|
||||||
|
}
|
||||||
|
/// Run this on focus update
|
||||||
|
fn focus_updated (&mut self) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for things that have enterable subparts.
|
||||||
|
pub trait HasEnter: HasFocus {
|
||||||
|
/// Get the currently focused item.
|
||||||
|
fn entered (&self) -> bool;
|
||||||
|
/// Get the currently focused item.
|
||||||
|
fn set_entered (&mut self, entered: bool);
|
||||||
|
/// Enter into the currently focused component
|
||||||
|
fn focus_enter (&mut self) {
|
||||||
|
self.set_entered(true);
|
||||||
|
self.focus_updated();
|
||||||
|
}
|
||||||
|
/// Exit the currently entered component
|
||||||
|
fn focus_exit (&mut self) {
|
||||||
|
self.set_entered(false);
|
||||||
|
self.focus_updated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for things that implement directional navigation between focusable elements.
|
||||||
|
pub trait FocusGrid: HasFocus {
|
||||||
|
fn focus_layout (&self) -> &[&[Self::Item]];
|
||||||
|
fn focus_cursor (&self) -> (usize, usize);
|
||||||
|
fn focus_cursor_mut (&mut self) -> &mut (usize, usize);
|
||||||
|
fn focus_current (&self) -> Self::Item {
|
||||||
|
let (x, y) = self.focus_cursor();
|
||||||
|
self.focus_layout()[y][x]
|
||||||
|
}
|
||||||
|
fn focus_update (&mut self) {
|
||||||
|
self.focus_to(self.focus_current());
|
||||||
|
self.focus_updated()
|
||||||
|
}
|
||||||
|
fn focus_up (&mut self) {
|
||||||
|
let original_focused = self.focused();
|
||||||
|
let (_, original_y) = self.focus_cursor();
|
||||||
|
loop {
|
||||||
|
let (x, y) = self.focus_cursor();
|
||||||
|
let next_y = if y == 0 {
|
||||||
|
self.focus_layout().len().saturating_sub(1)
|
||||||
|
} else {
|
||||||
|
y - 1
|
||||||
|
};
|
||||||
|
if next_y == original_y {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
let next_x = if self.focus_layout()[y].len() == self.focus_layout()[next_y].len() {
|
||||||
|
x
|
||||||
|
} else {
|
||||||
|
((x as f32 / self.focus_layout()[original_y].len() as f32)
|
||||||
|
* self.focus_layout()[next_y].len() as f32) as usize
|
||||||
|
};
|
||||||
|
*self.focus_cursor_mut() = (next_x, next_y);
|
||||||
|
if self.focus_current() != original_focused {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.focus_update();
|
||||||
|
}
|
||||||
|
fn focus_down (&mut self) {
|
||||||
|
let original_focused = self.focused();
|
||||||
|
let (_, original_y) = self.focus_cursor();
|
||||||
|
loop {
|
||||||
|
let (x, y) = self.focus_cursor();
|
||||||
|
let next_y = if y >= self.focus_layout().len().saturating_sub(1) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
y + 1
|
||||||
|
};
|
||||||
|
if next_y == original_y {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
let next_x = if self.focus_layout()[y].len() == self.focus_layout()[next_y].len() {
|
||||||
|
x
|
||||||
|
} else {
|
||||||
|
((x as f32 / self.focus_layout()[original_y].len() as f32)
|
||||||
|
* self.focus_layout()[next_y].len() as f32) as usize
|
||||||
|
};
|
||||||
|
*self.focus_cursor_mut() = (next_x, next_y);
|
||||||
|
if self.focus_current() != original_focused {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.focus_update();
|
||||||
|
}
|
||||||
|
fn focus_left (&mut self) {
|
||||||
|
let original_focused = self.focused();
|
||||||
|
let (original_x, y) = self.focus_cursor();
|
||||||
|
loop {
|
||||||
|
let x = self.focus_cursor().0;
|
||||||
|
let next_x = if x == 0 {
|
||||||
|
self.focus_layout()[y].len().saturating_sub(1)
|
||||||
|
} else {
|
||||||
|
x - 1
|
||||||
|
};
|
||||||
|
if next_x == original_x {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
*self.focus_cursor_mut() = (next_x, y);
|
||||||
|
if self.focus_current() != original_focused {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.focus_update();
|
||||||
|
}
|
||||||
|
fn focus_right (&mut self) {
|
||||||
|
let original_focused = self.focused();
|
||||||
|
let (original_x, y) = self.focus_cursor();
|
||||||
|
loop {
|
||||||
|
let x = self.focus_cursor().0;
|
||||||
|
let next_x = if x >= self.focus_layout()[y].len().saturating_sub(1) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
x + 1
|
||||||
|
};
|
||||||
|
if next_x == original_x {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
self.focus_cursor_mut().0 = next_x;
|
||||||
|
if self.focus_current() != original_focused {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.focus_update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for things that implement next/prev navigation between focusable elements.
|
||||||
|
pub trait FocusOrder {
|
||||||
|
/// Focus the next item.
|
||||||
|
fn focus_next (&mut self);
|
||||||
|
/// Focus the previous item.
|
||||||
|
fn focus_prev (&mut self);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Next/prev navigation for directional focusables works in the given way.
|
||||||
|
impl<T: FocusGrid + HasEnter> FocusOrder for T {
|
||||||
|
/// Focus the next item.
|
||||||
|
fn focus_next (&mut self) {
|
||||||
|
let current = self.focused();
|
||||||
|
let (x, y) = self.focus_cursor();
|
||||||
|
if x < self.focus_layout()[y].len().saturating_sub(1) {
|
||||||
|
self.focus_right();
|
||||||
|
} else {
|
||||||
|
self.focus_down();
|
||||||
|
self.focus_cursor_mut().0 = 0;
|
||||||
|
}
|
||||||
|
if self.focused() == current { // FIXME: prevent infinite loop
|
||||||
|
self.focus_next()
|
||||||
|
}
|
||||||
|
self.focus_exit();
|
||||||
|
self.focus_update();
|
||||||
|
}
|
||||||
|
/// Focus the previous item.
|
||||||
|
fn focus_prev (&mut self) {
|
||||||
|
let current = self.focused();
|
||||||
|
let (x, _) = self.focus_cursor();
|
||||||
|
if x > 0 {
|
||||||
|
self.focus_left();
|
||||||
|
} else {
|
||||||
|
self.focus_up();
|
||||||
|
let (_, y) = self.focus_cursor();
|
||||||
|
let next_x = self.focus_layout()[y].len().saturating_sub(1);
|
||||||
|
self.focus_cursor_mut().0 = next_x;
|
||||||
|
}
|
||||||
|
if self.focused() == current { // FIXME: prevent infinite loop
|
||||||
|
self.focus_prev()
|
||||||
|
}
|
||||||
|
self.focus_exit();
|
||||||
|
self.focus_update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait FocusWrap<T> {
|
||||||
|
fn wrap <W: Content<TuiOut>> (self, focus: T, content: &'_ W) -> impl Draw<TuiOut> + '_;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_focus_command <T: Send + Sync> (input: &TuiIn) -> Option<FocusCommand<T>> {
|
||||||
|
Some(match input.event() {
|
||||||
|
kpat!(Tab) => FocusCommand::Next,
|
||||||
|
kpat!(Shift-Tab) => FocusCommand::Prev,
|
||||||
|
kpat!(BackTab) => FocusCommand::Prev,
|
||||||
|
kpat!(Shift-BackTab) => FocusCommand::Prev,
|
||||||
|
kpat!(Up) => FocusCommand::Up,
|
||||||
|
kpat!(Down) => FocusCommand::Down,
|
||||||
|
kpat!(Left) => FocusCommand::Left,
|
||||||
|
kpat!(Right) => FocusCommand::Right,
|
||||||
|
kpat!(Enter) => FocusCommand::Enter,
|
||||||
|
kpat!(Esc) => FocusCommand::Exit,
|
||||||
|
_ => return None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export] macro_rules! impl_focus {
|
||||||
|
($Struct:ident $Focus:ident $Grid:expr $(=> [$self:ident : $update_focus:expr])?) => {
|
||||||
|
impl HasFocus for $Struct {
|
||||||
|
type Item = $Focus;
|
||||||
|
/// Get the currently focused item.
|
||||||
|
fn focused (&self) -> Self::Item {
|
||||||
|
self.focus.inner()
|
||||||
|
}
|
||||||
|
/// Get the currently focused item.
|
||||||
|
fn set_focused (&mut self, to: Self::Item) {
|
||||||
|
self.focus.set_inner(to)
|
||||||
|
}
|
||||||
|
$(fn focus_updated (&mut $self) { $update_focus })?
|
||||||
|
}
|
||||||
|
impl HasEnter for $Struct {
|
||||||
|
/// Get the currently focused item.
|
||||||
|
fn entered (&self) -> bool {
|
||||||
|
self.focus.is_entered()
|
||||||
|
}
|
||||||
|
/// Get the currently focused item.
|
||||||
|
fn set_entered (&mut self, entered: bool) {
|
||||||
|
if entered {
|
||||||
|
self.focus.to_entered()
|
||||||
|
} else {
|
||||||
|
self.focus.to_focused()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FocusGrid for $Struct {
|
||||||
|
fn focus_cursor (&self) -> (usize, usize) {
|
||||||
|
self.cursor
|
||||||
|
}
|
||||||
|
fn focus_cursor_mut (&mut self) -> &mut (usize, usize) {
|
||||||
|
&mut self.cursor
|
||||||
|
}
|
||||||
|
fn focus_layout (&self) -> &[&[$Focus]] {
|
||||||
|
use $Focus::*;
|
||||||
|
&$Grid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
use crate::*;
|
||||||
|
pub struct MenuBar<E: Engine, S, C: Command<S>> {
|
||||||
|
pub menus: Vec<Menu<E, S, C>>,
|
||||||
|
pub index: usize,
|
||||||
|
}
|
||||||
|
impl<E: Engine, S, C: Command<S>> MenuBar<E, S, C> {
|
||||||
|
pub fn new () -> Self { Self { menus: vec![], index: 0 } }
|
||||||
|
pub fn add (mut self, menu: Menu<E, S, C>) -> Self {
|
||||||
|
self.menus.push(menu);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub struct Menu<E: Engine, S, C: Command<S>> {
|
||||||
|
pub title: Arc<str>,
|
||||||
|
pub items: Vec<MenuItem<E, S, C>>,
|
||||||
|
pub index: Option<usize>,
|
||||||
|
}
|
||||||
|
impl<E: Engine, S, C: Command<S>> Menu<E, S, C> {
|
||||||
|
pub fn new (title: impl AsRef<str>) -> Self {
|
||||||
|
Self {
|
||||||
|
title: title.as_ref().to_string(),
|
||||||
|
items: vec![],
|
||||||
|
index: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn add (mut self, item: MenuItem<E, S, C>) -> Self {
|
||||||
|
self.items.push(item);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub fn sep (mut self) -> Self {
|
||||||
|
self.items.push(MenuItem::sep());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub fn cmd (mut self, hotkey: &'static str, text: &'static str, command: C) -> Self {
|
||||||
|
self.items.push(MenuItem::cmd(hotkey, text, command));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub fn off (mut self, hotkey: &'static str, text: &'static str) -> Self {
|
||||||
|
self.items.push(MenuItem::off(hotkey, text));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub enum MenuItem<E: Engine, S, C: Command<S>> {
|
||||||
|
/// Unused.
|
||||||
|
__(PhantomData<E>, PhantomData<S>),
|
||||||
|
/// A separator. Skip it.
|
||||||
|
Separator,
|
||||||
|
/// A menu item with command, description and hotkey.
|
||||||
|
Command(&'static str, &'static str, C),
|
||||||
|
/// A menu item that can't be activated but has description and hotkey
|
||||||
|
Disabled(&'static str, &'static str)
|
||||||
|
}
|
||||||
|
impl<E: Engine, S, C: Command<S>> MenuItem<E, S, C> {
|
||||||
|
pub fn sep () -> Self {
|
||||||
|
Self::Separator
|
||||||
|
}
|
||||||
|
pub fn cmd (hotkey: &'static str, text: &'static str, command: C) -> Self {
|
||||||
|
Self::Command(hotkey, text, command)
|
||||||
|
}
|
||||||
|
pub fn off (hotkey: &'static str, text: &'static str) -> Self {
|
||||||
|
Self::Disabled(hotkey, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//impl<T: Draw<TuiOut>> Content<TuiOut> for Result<T, Box<dyn std::error::Error>> {
|
||||||
|
//fn content (&self) -> impl Draw<TuiOut> + '_ {
|
||||||
|
//Bsp::a(self.as_ref().ok(), self.as_ref().err().map(
|
||||||
|
//|e|Tui::fg_bg(Color::Rgb(255,255,255), Color::Rgb(32,32,32), e.to_string())
|
||||||
|
//))
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
//impl<T: Draw<TuiOut>> Draw<TuiOut> for Result<T, Box<dyn std::error::Error>> {
|
||||||
|
//fn layout (&self, to: [u16;4]) -> [u16;4] {
|
||||||
|
//match self {
|
||||||
|
//Ok(content) => content.layout(to),
|
||||||
|
//Err(e) => [0, 0, to.w(), to.h()]
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//fn draw (&self, to: &mut TuiOut) {
|
||||||
|
//match self {
|
||||||
|
//Ok(content) => content.draw(to),
|
||||||
|
//Err(e) => to.blit(&e.to_string(), 0, 0, Some(Style::default()
|
||||||
|
//.bg(Color::Rgb(32,32,32))
|
||||||
|
//.fg(Color::Rgb(255,255,255))))
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
//let token = token.as_ref();
|
||||||
|
//if token.len() < 2 {
|
||||||
|
//Self { valid: false, key: None, mods: KeyModifiers::NONE }
|
||||||
|
//} else if token.chars().next() != Some('@') {
|
||||||
|
//Self { valid: false, key: None, mods: KeyModifiers::NONE }
|
||||||
|
//} else {
|
||||||
|
//Self { valid: true, key: None, mods: KeyModifiers::NONE }.next(&token[1..])
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//pub fn build (self) -> Option<Event> {
|
||||||
|
//if self.valid && self.key.is_some() {
|
||||||
|
//Some(Event::Key(KeyEvent::new(self.key.unwrap(), self.mods)))
|
||||||
|
//} else {
|
||||||
|
//None
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//fn next (mut self, token: &str) -> Self {
|
||||||
|
//let mut tokens = token.split('-').peekable();
|
||||||
|
//while let Some(token) = tokens.next() {
|
||||||
|
//if tokens.peek().is_some() {
|
||||||
|
//match token {
|
||||||
|
//"ctrl" | "Ctrl" | "c" | "C" => self.mods |= KeyModifiers::CONTROL,
|
||||||
|
//"alt" | "Alt" | "m" | "M" => self.mods |= KeyModifiers::ALT,
|
||||||
|
//"shift" | "Shift" | "s" | "S" => {
|
||||||
|
//self.mods |= KeyModifiers::SHIFT;
|
||||||
|
//// + TODO normalize character case, BackTab, etc.
|
||||||
|
//},
|
||||||
|
//_ => panic!("unknown modifier {token}"),
|
||||||
|
//}
|
||||||
|
//} else {
|
||||||
|
//self.key = if token.len() == 1 {
|
||||||
|
//Some(KeyCode::Char(token.chars().next().unwrap()))
|
||||||
|
//} else {
|
||||||
|
//Some(Self::named_key(token).unwrap_or_else(||panic!("unknown character {token}")))
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//self
|
||||||
|
//}
|
||||||
|
|
@ -6,21 +6,33 @@ version = { workspace = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = [ "input", "output", "tui" ]
|
default = [ "input", "output", "tui" ]
|
||||||
input = [ "tengri_input" ]
|
bumpalo = [ "dep:bumpalo" ]
|
||||||
output = [ "tengri_output" ]
|
input = [ ]
|
||||||
tui = [ "tengri_tui" ]
|
output = [ ]
|
||||||
dsl = [ "tengri_output/dsl", "tengri_tui/dsl" ]
|
tui = [ ]
|
||||||
|
dsl = [ ]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
atomic_float = { workspace = true }
|
||||||
|
better-panic = { workspace = true }
|
||||||
|
bumpalo = { workspace = true, optional = true }
|
||||||
|
crossterm = { workspace = true }
|
||||||
dizzle = { workspace = true }
|
dizzle = { workspace = true }
|
||||||
tengri_input = { workspace = true, optional = true }
|
palette = { workspace = true }
|
||||||
tengri_output = { workspace = true, optional = true }
|
quanta = { workspace = true }
|
||||||
tengri_tui = { workspace = true, optional = true }
|
rand = { workspace = true }
|
||||||
|
ratatui = { workspace = true }
|
||||||
|
unicode-width = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tengri_proc = { workspace = true }
|
tengri_proc = { workspace = true }
|
||||||
tengri = { workspace = true, features = [ "dsl" ] }
|
tengri = { workspace = true, features = [ "dsl" ] }
|
||||||
crossterm = { workspace = true }
|
crossterm = { workspace = true }
|
||||||
|
proptest = { workspace = true }
|
||||||
|
proptest-derive = { workspace = true }
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "tengri.rs"
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")']
|
[target.'cfg(target_os = "linux")']
|
||||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||||
|
|
|
||||||
|
|
@ -15,3 +15,35 @@ as well as:
|
||||||
* [***tengri***](./tengri), the top-level reexport crate
|
* [***tengri***](./tengri), the top-level reexport crate
|
||||||
|
|
||||||
tengri is published under [**AGPL3**](./LICENSE).
|
tengri is published under [**AGPL3**](./LICENSE).
|
||||||
|
|
||||||
|
## Input
|
||||||
|
|
||||||
|
***tengri_input*** is where tengri's input handling is defined.
|
||||||
|
|
||||||
|
the following items are provided:
|
||||||
|
* `Input` trait, for defining for input sources
|
||||||
|
* `Handle` trait and `handle!` macro, for defining input handlers
|
||||||
|
* `Command` trait and the `command!` macro, for defining commands that inputs may result in
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
***tengri_output*** is an abstract interface layout framework.
|
||||||
|
|
||||||
|
it expresses the following notions:
|
||||||
|
|
||||||
|
* [**space:**](./src/space.rs) `Direction`, `Coordinate`, `Area`, `Size`, `Measure`
|
||||||
|
|
||||||
|
* [**output:**](./src/output.rs) `Out`, `Draw`, `Content`
|
||||||
|
* the layout operators are generic over `Draw` and/or `Content`
|
||||||
|
* the traits `Draw` and `Content` are generic over `Out`
|
||||||
|
* implement `Out` to bring a layout to a new backend:
|
||||||
|
[see `TuiOut` in `tengri_tui`](../tui/src/tui_engine/tui_output.rs)
|
||||||
|
|
||||||
|
* [**layout:**](./src/layout.rs)
|
||||||
|
* conditionals: `When`, `Either`
|
||||||
|
* iteration: `Map`
|
||||||
|
* concatenation: `Bsp`
|
||||||
|
* positioning: `Align`, `Push`, `Pull`
|
||||||
|
* sizing: `Fill`, `Fixed`, `Expand`, `Shrink`, `Min`, `Max`
|
||||||
|
* implement custom components (that may be backend-dependent):
|
||||||
|
[see `tui_content` in `tengri_tui`](../tui/src/tui_content)
|
||||||
|
|
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
pub use ::dizzle::*;
|
|
||||||
#[cfg(feature="output")] pub use ::tengri_output as output;
|
|
||||||
#[cfg(feature="input")] pub use ::tengri_input as input;
|
|
||||||
#[cfg(feature="tui")] pub use ::tengri_tui as tui;
|
|
||||||
|
|
||||||
#[cfg(test)] extern crate tengri_proc;
|
|
||||||
#[cfg(test)] mod test {
|
|
||||||
// FIXME
|
|
||||||
//use crate::*;
|
|
||||||
//use crate::{dsl::*, input::*, tui::TuiIn};
|
|
||||||
//use crossterm::event::{Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState};
|
|
||||||
//use std::cmp::Ordering;
|
|
||||||
|
|
||||||
//#[test] fn test_subcommand () -> Usually<()> {
|
|
||||||
//#[derive(Debug)] struct Event(crossterm::event::Event);
|
|
||||||
//impl Eq for Event {}
|
|
||||||
//impl PartialEq for Event { fn eq (&self, other: &Self) -> bool { todo!() } }
|
|
||||||
//impl Ord for Event { fn cmp (&self, other: &Self) -> Ordering { todo!() } }
|
|
||||||
//impl PartialOrd for Event { fn partial_cmp (&self, other: &Self) -> Option<Ordering> { None } }
|
|
||||||
//struct Test { keys: InputMap<Event, Ast> }
|
|
||||||
|
|
||||||
//handle!(TuiIn: |self: Test, input|Ok(None));[>if let Some(command) = self.keys.command(self, input) {
|
|
||||||
//Ok(Some(true))
|
|
||||||
//} else {
|
|
||||||
//Ok(None)
|
|
||||||
//});*/
|
|
||||||
|
|
||||||
//#[tengri_proc::command(Test)]
|
|
||||||
//impl TestCommand {
|
|
||||||
//fn do_thing (_state: &mut Test) -> Perhaps<Self> {
|
|
||||||
//Ok(None)
|
|
||||||
//}
|
|
||||||
//fn do_thing_arg (_state: &mut Test, _arg: usize) -> Perhaps<Self> {
|
|
||||||
//Ok(None)
|
|
||||||
//}
|
|
||||||
//fn do_sub (state: &mut Test, command: TestSubcommand) -> Perhaps<Self> {
|
|
||||||
//Ok(command.execute(state)?.map(|command|Self::DoSub { command }))
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
|
|
||||||
//#[tengri_proc::command(Test)]
|
|
||||||
//impl TestSubcommand {
|
|
||||||
//fn do_other_thing (_state: &mut Test) -> Perhaps<Self> {
|
|
||||||
//Ok(None)
|
|
||||||
//}
|
|
||||||
//fn do_other_thing_arg (_state: &mut Test, _arg: usize) -> Perhaps<Self> {
|
|
||||||
//Ok(None)
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
|
|
||||||
//let mut test = Test {
|
|
||||||
//keys: InputMap::from_source("
|
|
||||||
//(@a do-thing)
|
|
||||||
//(@b do-thing-arg 0)
|
|
||||||
//(@c do-sub do-other-thing)
|
|
||||||
//(@d do-sub do-other-thing-arg 0)
|
|
||||||
//")?
|
|
||||||
//};
|
|
||||||
|
|
||||||
////assert_eq!(Some(true), test.handle(&TuiIn(Default::default(), Event::Key(KeyEvent {
|
|
||||||
////kind: KeyEventKind::Press,
|
|
||||||
////code: KeyCode::Char('a'),
|
|
||||||
////modifiers: KeyModifiers::NONE,
|
|
||||||
////state: KeyEventState::NONE,
|
|
||||||
////})))?);
|
|
||||||
////assert_eq!(Some(true), test.handle(&TuiIn(Default::default(), Event::Key(KeyEvent {
|
|
||||||
////kind: KeyEventKind::Press,
|
|
||||||
////code: KeyCode::Char('b'),
|
|
||||||
////modifiers: KeyModifiers::NONE,
|
|
||||||
////state: KeyEventState::NONE,
|
|
||||||
////})))?);
|
|
||||||
////assert_eq!(Some(true), test.handle(&TuiIn(Default::default(), Event::Key(KeyEvent {
|
|
||||||
////kind: KeyEventKind::Press,
|
|
||||||
////code: KeyCode::Char('c'),
|
|
||||||
////modifiers: KeyModifiers::NONE,
|
|
||||||
////state: KeyEventState::NONE,
|
|
||||||
////})))?);
|
|
||||||
////assert_eq!(Some(true), test.handle(&TuiIn(Default::default(), Event::Key(KeyEvent {
|
|
||||||
////kind: KeyEventKind::Press,
|
|
||||||
////code: KeyCode::Char('d'),
|
|
||||||
////modifiers: KeyModifiers::NONE,
|
|
||||||
////state: KeyEventState::NONE,
|
|
||||||
////})))?);
|
|
||||||
////assert_eq!(None, test.handle(&TuiIn(Default::default(), Event::Key(KeyEvent {
|
|
||||||
////kind: KeyEventKind::Press,
|
|
||||||
////code: KeyCode::Char('z'),
|
|
||||||
////modifiers: KeyModifiers::NONE,
|
|
||||||
////state: KeyEventState::NONE,
|
|
||||||
////})))?);
|
|
||||||
//Ok(())
|
|
||||||
//}
|
|
||||||
|
|
||||||
//FIXME:
|
|
||||||
//#[cfg(test)] #[test] fn test_dsl_context () {
|
|
||||||
//use crate::dsl::{Dsl, Value};
|
|
||||||
|
|
||||||
//struct Test;
|
|
||||||
//#[tengri_proc::expose]
|
|
||||||
//impl Test {
|
|
||||||
//fn some_bool (&self) -> bool {
|
|
||||||
//true
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//assert_eq!(Dsl::get(&Test, &Value::Sym(":false")), Some(false));
|
|
||||||
//assert_eq!(Dsl::get(&Test, &Value::Sym(":true")), Some(true));
|
|
||||||
//assert_eq!(Dsl::get(&Test, &Value::Sym(":some-bool")), Some(true));
|
|
||||||
//assert_eq!(Dsl::get(&Test, &Value::Sym(":missing-bool")), None);
|
|
||||||
//assert_eq!(Dsl::get(&Test, &Value::Num(0)), Some(false));
|
|
||||||
//assert_eq!(Dsl::get(&Test, &Value::Num(1)), Some(true));
|
|
||||||
//}
|
|
||||||
|
|
||||||
}
|
|
||||||
900
tengri/tengri.rs
Normal file
900
tengri/tengri.rs
Normal file
|
|
@ -0,0 +1,900 @@
|
||||||
|
#![feature(anonymous_lifetime_in_impl_trait)]
|
||||||
|
#![feature(associated_type_defaults)]
|
||||||
|
#![feature(const_default)]
|
||||||
|
#![feature(const_option_ops)]
|
||||||
|
#![feature(const_precise_live_drops)]
|
||||||
|
#![feature(const_trait_impl)]
|
||||||
|
#![feature(if_let_guard)]
|
||||||
|
#![feature(impl_trait_in_assoc_type)]
|
||||||
|
#![feature(step_trait)]
|
||||||
|
#![feature(trait_alias)]
|
||||||
|
#![feature(type_alias_impl_trait)]
|
||||||
|
#![feature(type_changing_struct_update)]
|
||||||
|
//#![feature(non_lifetime_binders)]
|
||||||
|
mod tengri_impl;
|
||||||
|
mod tengri_type; pub use self::tengri_type::*;
|
||||||
|
mod tengri_trait; pub use self::tengri_trait::*;
|
||||||
|
mod tengri_struct; pub use self::tengri_struct::*;
|
||||||
|
//pub(crate) use quanta::Clock;
|
||||||
|
pub extern crate atomic_float; pub(crate) use atomic_float::AtomicF64;
|
||||||
|
pub extern crate ratatui;
|
||||||
|
pub extern crate crossterm;
|
||||||
|
pub extern crate palette;
|
||||||
|
pub extern crate better_panic;
|
||||||
|
extern crate dizzle; pub use dizzle::*;
|
||||||
|
#[cfg(test)] extern crate tengri_proc;
|
||||||
|
use std::{time::Duration, thread::{spawn, JoinHandle}, io::Write};
|
||||||
|
use unicode_width::*;
|
||||||
|
pub(crate) use ::{
|
||||||
|
std::{
|
||||||
|
io::{stdout, Stdout},
|
||||||
|
sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::*}},
|
||||||
|
fmt::{Debug, Display},
|
||||||
|
ops::{Add, Sub, Mul, Div},
|
||||||
|
marker::PhantomData,
|
||||||
|
},
|
||||||
|
better_panic::{Settings, Verbosity},
|
||||||
|
palette::{*, convert::*, okhsl::*},
|
||||||
|
ratatui::{
|
||||||
|
prelude::{Color, Style, Buffer, Position},
|
||||||
|
style::{Stylize, Modifier, Color::*},
|
||||||
|
backend::{Backend, CrosstermBackend, ClearType},
|
||||||
|
layout::{Size, Rect},
|
||||||
|
buffer::Cell
|
||||||
|
},
|
||||||
|
crossterm::{
|
||||||
|
ExecutableCommand,
|
||||||
|
terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode},
|
||||||
|
event::{poll, read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Define macros first, so that private macros are available in private modules:
|
||||||
|
|
||||||
|
/// Clear a pre-allocated buffer, then write into it.
|
||||||
|
#[macro_export] macro_rules! rewrite {
|
||||||
|
($buf:ident, $($rest:tt)*) => { |$buf,_,_|{ $buf.clear(); write!($buf, $($rest)*) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// FIXME: This macro should be some variant of `eval`, too.
|
||||||
|
/// But taking into account the different signatures (resolving them into 1?)
|
||||||
|
#[cfg(feature = "dsl")] #[macro_export] macro_rules! draw {
|
||||||
|
($State:ident: $Output:ident: $layers:expr) => {
|
||||||
|
impl Draw<$Output> for $State {
|
||||||
|
fn draw (&self, to: &mut $Output) {
|
||||||
|
for layer in $layers { layer(self, to) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// FIXME: This is generic: should be called `eval` and be part of [dizzle].
|
||||||
|
#[cfg(feature = "dsl")] #[macro_export] macro_rules! view {
|
||||||
|
($State:ident: $Output:ident: $namespaces:expr) => {
|
||||||
|
impl View<$Output, ()> for $State {
|
||||||
|
fn view_expr <'a> (&'a self, to: &mut $Output, expr: &'a impl Expression) -> Usually<()> {
|
||||||
|
for namespace in $namespaces { if namespace(self, to, expr)? { return Ok(()) } }
|
||||||
|
Err(format!("{}::<{}, ()>::view_expr: unexpected: {expr:?}",
|
||||||
|
stringify! { $State },
|
||||||
|
stringify! { $Output }).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stack things on top of each other,
|
||||||
|
#[macro_export] macro_rules! lay (($($expr:expr),* $(,)?) => {{ let bsp = (); $(let bsp = Bsp::b(bsp, $expr);)*; bsp }});
|
||||||
|
|
||||||
|
/// Stack southward.
|
||||||
|
#[macro_export] macro_rules! col (($($expr:expr),* $(,)?) => {{ let bsp = (); $(let bsp = Bsp::s(bsp, $expr);)*; bsp }});
|
||||||
|
|
||||||
|
/// Stack northward.
|
||||||
|
#[macro_export] macro_rules! col_up (($($expr:expr),* $(,)?) => {{ let bsp = (); $(let bsp = Bsp::n(bsp, $expr);)*; bsp }});
|
||||||
|
|
||||||
|
/// Stack eastward.
|
||||||
|
#[macro_export] macro_rules! row (($($expr:expr),* $(,)?) => {{ let bsp = (); $(let bsp = Bsp::e(bsp, $expr);)*; bsp }});
|
||||||
|
|
||||||
|
/// Define layout operation.
|
||||||
|
#[cfg(feature = "dsl")] pub fn evaluate_output_expression <'a, O: Out + 'a, S> (
|
||||||
|
state: &S, output: &mut O, expr: &'a impl Expression
|
||||||
|
) -> Usually<bool> where
|
||||||
|
S: View<O, ()>
|
||||||
|
+ for<'b>Namespace<'b, bool>
|
||||||
|
+ for<'b>Namespace<'b, O::Unit>
|
||||||
|
{
|
||||||
|
// First element of expression is used for dispatch.
|
||||||
|
// Dispatch is proto-namespaced using separator character
|
||||||
|
let head = expr.head()?;
|
||||||
|
let mut frags = head.src()?.unwrap_or_default().split("/");
|
||||||
|
// The rest of the tokens in the expr are arguments.
|
||||||
|
// Their meanings depend on the dispatched operation
|
||||||
|
let args = expr.tail();
|
||||||
|
let arg0 = args.head();
|
||||||
|
let tail0 = args.tail();
|
||||||
|
let arg1 = tail0.head();
|
||||||
|
let tail1 = tail0.tail();
|
||||||
|
let arg2 = tail1.head();
|
||||||
|
// And we also have to do the above binding dance
|
||||||
|
// so that the Perhaps<token>s remain in scope.
|
||||||
|
match frags.next() {
|
||||||
|
|
||||||
|
Some("when") => output.place(&When::new(
|
||||||
|
state.namespace(arg0?)?.unwrap(),
|
||||||
|
Thunk::new(move|output: &mut O|state.view(output, &arg1).unwrap())
|
||||||
|
)),
|
||||||
|
|
||||||
|
Some("either") => output.place(&Either::new(
|
||||||
|
state.namespace(arg0?)?.unwrap(),
|
||||||
|
Thunk::new(move|output: &mut O|state.view(output, &arg1).unwrap()),
|
||||||
|
Thunk::new(move|output: &mut O|state.view(output, &arg2).unwrap())
|
||||||
|
)),
|
||||||
|
|
||||||
|
Some("bsp") => output.place(&{
|
||||||
|
let a = Thunk::new(move|output: &mut O|state.view(output, &arg0).unwrap());
|
||||||
|
let b = Thunk::new(move|output: &mut O|state.view(output, &arg1).unwrap());
|
||||||
|
match frags.next() {
|
||||||
|
Some("n") => Bsp::n(a, b),
|
||||||
|
Some("s") => Bsp::s(a, b),
|
||||||
|
Some("e") => Bsp::e(a, b),
|
||||||
|
Some("w") => Bsp::w(a, b),
|
||||||
|
Some("a") => Bsp::a(a, b),
|
||||||
|
Some("b") => Bsp::b(a, b),
|
||||||
|
frag => unimplemented!("bsp/{frag:?}")
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
Some("align") => output.place(&{
|
||||||
|
let a = Thunk::new(move|output: &mut O|state.view(output, &arg0).unwrap());
|
||||||
|
match frags.next() {
|
||||||
|
Some("n") => Align::n(a),
|
||||||
|
Some("s") => Align::s(a),
|
||||||
|
Some("e") => Align::e(a),
|
||||||
|
Some("w") => Align::w(a),
|
||||||
|
Some("x") => Align::x(a),
|
||||||
|
Some("y") => Align::y(a),
|
||||||
|
Some("c") => Align::c(a),
|
||||||
|
frag => unimplemented!("align/{frag:?}")
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
Some("fill") => output.place(&{
|
||||||
|
let a = Thunk::new(move|output: &mut O|state.view(output, &arg0).unwrap());
|
||||||
|
match frags.next() {
|
||||||
|
Some("xy") | None => Fill::XY(a),
|
||||||
|
Some("x") => Fill::X(a),
|
||||||
|
Some("y") => Fill::Y(a),
|
||||||
|
frag => unimplemented!("fill/{frag:?}")
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
Some("fixed") => output.place(&{
|
||||||
|
let axis = frags.next();
|
||||||
|
let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") };
|
||||||
|
let cb = Thunk::new(move|output: &mut O|state.view(output, &arg).unwrap());
|
||||||
|
match axis {
|
||||||
|
Some("xy") | None => Fixed::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb),
|
||||||
|
Some("x") => Fixed::X(state.namespace(arg0?)?.unwrap(), cb),
|
||||||
|
Some("y") => Fixed::Y(state.namespace(arg0?)?.unwrap(), cb),
|
||||||
|
frag => unimplemented!("fixed/{frag:?} ({expr:?}) ({head:?}) ({:?})",
|
||||||
|
head.src()?.unwrap_or_default().split("/").next())
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
Some("min") => output.place(&{
|
||||||
|
let axis = frags.next();
|
||||||
|
let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") };
|
||||||
|
let cb = Thunk::new(move|output: &mut O|state.view(output, &arg).unwrap());
|
||||||
|
match axis {
|
||||||
|
Some("xy") | None => Min::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb),
|
||||||
|
Some("x") => Min::X(state.namespace(arg0?)?.unwrap(), cb),
|
||||||
|
Some("y") => Min::Y(state.namespace(arg0?)?.unwrap(), cb),
|
||||||
|
frag => unimplemented!("min/{frag:?}")
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
Some("max") => output.place(&{
|
||||||
|
let axis = frags.next();
|
||||||
|
let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") };
|
||||||
|
let cb = Thunk::new(move|output: &mut O|state.view(output, &arg).unwrap());
|
||||||
|
match axis {
|
||||||
|
Some("xy") | None => Max::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb),
|
||||||
|
Some("x") => Max::X(state.namespace(arg0?)?.unwrap(), cb),
|
||||||
|
Some("y") => Max::Y(state.namespace(arg0?)?.unwrap(), cb),
|
||||||
|
frag => unimplemented!("max/{frag:?}")
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
Some("push") => output.place(&{
|
||||||
|
let axis = frags.next();
|
||||||
|
let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") };
|
||||||
|
let cb = Thunk::new(move|output: &mut O|state.view(output, &arg).unwrap());
|
||||||
|
match axis {
|
||||||
|
Some("xy") | None => Push::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb),
|
||||||
|
Some("x") => Push::X(state.namespace(arg0?)?.unwrap(), cb),
|
||||||
|
Some("y") => Push::Y(state.namespace(arg0?)?.unwrap(), cb),
|
||||||
|
frag => unimplemented!("push/{frag:?}")
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
_ => return Ok(false)
|
||||||
|
|
||||||
|
};
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implement [Command] for given `State` and `handler`
|
||||||
|
#[macro_export] macro_rules! command {
|
||||||
|
($(<$($l:lifetime),+>)?|$self:ident:$Command:ty,$state:ident:$State:ty|$handler:expr) => {
|
||||||
|
impl$(<$($l),+>)? ::tengri::Command<$State> for $Command {
|
||||||
|
fn execute (&$self, $state: &mut $State) -> Perhaps<Self> {
|
||||||
|
Ok($handler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export] macro_rules! def_command (($Command:ident: |$state:ident: $State:ty| {
|
||||||
|
$($Variant:ident$({$($arg:ident:$Arg:ty),+ $(,)?})?=>$body:expr),* $(,)?
|
||||||
|
})=>{
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum $Command {
|
||||||
|
$($Variant $({ $($arg: $Arg),* })?),*
|
||||||
|
}
|
||||||
|
impl Command<$State> for $Command {
|
||||||
|
fn execute (&self, $state: &mut $State) -> Perhaps<Self> {
|
||||||
|
match self {
|
||||||
|
$(Self::$Variant $({ $($arg),* })? => $body,)*
|
||||||
|
_ => unimplemented!("Command<{}>: {self:?}", stringify!($State)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Implement [Handle] for given `State` and `handler`.
|
||||||
|
#[macro_export] macro_rules! handle {
|
||||||
|
(|$self:ident:$State:ty,$input:ident|$handler:expr) => {
|
||||||
|
impl<E: Engine> ::tengri::Handle<E> for $State {
|
||||||
|
fn handle (&mut $self, $input: &E) -> Perhaps<E::Handled> {
|
||||||
|
$handler
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($E:ty: |$self:ident:$State:ty,$input:ident|$handler:expr) => {
|
||||||
|
impl ::tengri::Handle<$E> for $State {
|
||||||
|
fn handle (&mut $self, $input: &$E) ->
|
||||||
|
Perhaps<<$E as ::tengri::Input>::Handled>
|
||||||
|
{
|
||||||
|
$handler
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export] macro_rules! tui_main {
|
||||||
|
($expr:expr) => {
|
||||||
|
fn main () -> Usually<()> {
|
||||||
|
tengri::Tui::run(true, $expr)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export] macro_rules! has_color {
|
||||||
|
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||||
|
impl $(<$($L),*$($T $(: $U)?),*>)? HasColor for $Struct $(<$($L),*$($T),*>)? {
|
||||||
|
fn color (&$self) -> ItemColor { $cb }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<TuiOut> for $T {}
|
||||||
|
impl Draw<TuiOut> for $T {
|
||||||
|
fn draw (&self, to: &mut TuiOut) {
|
||||||
|
if self.enabled() { let _ = BorderStyle::draw(self, to); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)+}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run an app in the main loop.
|
||||||
|
pub fn tui_run <T: Send + Sync + Draw<TuiOut> + Handle<TuiIn> + 'static> (
|
||||||
|
join: bool,
|
||||||
|
state: &Arc<RwLock<T>>
|
||||||
|
) -> Usually<Arc<RwLock<Tui>>> {
|
||||||
|
let backend = CrosstermBackend::new(stdout());
|
||||||
|
let Size { width, height } = backend.size()?;
|
||||||
|
let tui = Arc::new(RwLock::new(Tui {
|
||||||
|
exited: Arc::new(AtomicBool::new(false)),
|
||||||
|
buffer: Buffer::empty(Rect { x: 0, y: 0, width, height }),
|
||||||
|
area: [0, 0, width, height],
|
||||||
|
perf: Default::default(),
|
||||||
|
backend,
|
||||||
|
}));
|
||||||
|
let _input_thread = tui_input(tui.clone(), state, Duration::from_millis(100));
|
||||||
|
tui.write().unwrap().setup()?;
|
||||||
|
let render_thread = tui_output(tui.clone(), state, Duration::from_millis(10))?;
|
||||||
|
if join {
|
||||||
|
match render_thread.join() {
|
||||||
|
Ok(result) => {
|
||||||
|
tui.write().unwrap().teardown()?;
|
||||||
|
println!("\n\rRan successfully: {result:?}\n\r");
|
||||||
|
},
|
||||||
|
Err(error) => {
|
||||||
|
tui.write().unwrap().teardown()?;
|
||||||
|
panic!("\n\rDraw thread failed: error={error:?}.\n\r")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(tui)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tui_setup <W: Write> (
|
||||||
|
backend: &mut CrosstermBackend<W>
|
||||||
|
) -> Usually<()> {
|
||||||
|
let better_panic_handler = Settings::auto().verbosity(Verbosity::Full).create_panic_handler();
|
||||||
|
std::panic::set_hook(Box::new(move |info: &std::panic::PanicHookInfo|{
|
||||||
|
stdout().execute(LeaveAlternateScreen).unwrap();
|
||||||
|
CrosstermBackend::new(stdout()).show_cursor().unwrap();
|
||||||
|
disable_raw_mode().unwrap();
|
||||||
|
better_panic_handler(info);
|
||||||
|
}));
|
||||||
|
stdout().execute(EnterAlternateScreen)?;
|
||||||
|
backend.hide_cursor()?;
|
||||||
|
enable_raw_mode().map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tui_teardown <W: Write> (backend: &mut CrosstermBackend<W>) -> Usually<()> {
|
||||||
|
stdout().execute(LeaveAlternateScreen)?;
|
||||||
|
backend.show_cursor()?;
|
||||||
|
disable_raw_mode().map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tui_resized <W: Write> (
|
||||||
|
backend: &mut CrosstermBackend<W>,
|
||||||
|
buffer: &mut Buffer,
|
||||||
|
size: ratatui::prelude::Rect
|
||||||
|
) {
|
||||||
|
if buffer.area != size {
|
||||||
|
backend.clear_region(ClearType::All).unwrap();
|
||||||
|
buffer.resize(size);
|
||||||
|
buffer.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tui_redrawn <'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_update (
|
||||||
|
buf: &mut Buffer, area: XYWH<u16>, callback: &impl Fn(&mut Cell, u16, u16)
|
||||||
|
) {
|
||||||
|
for row in 0..area.h() {
|
||||||
|
let y = area.y() + row;
|
||||||
|
for col in 0..area.w() {
|
||||||
|
let x = area.x() + col;
|
||||||
|
if x < buf.area.width && y < buf.area.height {
|
||||||
|
if let Some(cell) = buf.cell_mut(ratatui::prelude::Position { x, y }) {
|
||||||
|
callback(cell, col, row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawn the output thread.
|
||||||
|
pub fn tui_output <T: Draw<TuiOut> + Send + Sync + 'static> (
|
||||||
|
engine: Arc<RwLock<Tui>>, state: &Arc<RwLock<T>>, timer: Duration
|
||||||
|
) -> Result<JoinHandle<()>, std::io::Error> {
|
||||||
|
let exited = engine.read().unwrap().exited.clone();
|
||||||
|
let engine = engine.clone();
|
||||||
|
let state = state.clone();
|
||||||
|
let Size { width, height } = engine.read().unwrap().backend.size().expect("get size failed");
|
||||||
|
let mut buffer = Buffer::empty(Rect { x: 0, y: 0, width, height });
|
||||||
|
std::thread::Builder::new()
|
||||||
|
.name("tui output thread".into())
|
||||||
|
.spawn(move || loop {
|
||||||
|
if exited.fetch_and(true, Relaxed) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
let t0 = engine.read().unwrap().perf.get_t0();
|
||||||
|
let Size { width, height } = engine.read().unwrap().backend.size()
|
||||||
|
.expect("get size failed");
|
||||||
|
if let Ok(state) = state.try_read() {
|
||||||
|
let size = Rect { x: 0, y: 0, width, height };
|
||||||
|
if buffer.area != size {
|
||||||
|
engine.write().unwrap().backend.clear_region(ClearType::All).expect("clear failed");
|
||||||
|
buffer.resize(size);
|
||||||
|
buffer.reset();
|
||||||
|
}
|
||||||
|
let mut output = TuiOut { buffer, area: XYWH(0, 0, width, height) };
|
||||||
|
state.draw(&mut output);
|
||||||
|
buffer = engine.write().unwrap().flip(output.buffer, size);
|
||||||
|
}
|
||||||
|
let t1 = (*engine.read().unwrap()).perf.get_t1(t0).unwrap();
|
||||||
|
buffer.set_string(0, 0, &format!("{:>3}.{:>3}ms", t1.as_millis(), t1.as_micros() % 1000), Style::default());
|
||||||
|
std::thread::sleep(timer);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawn the input thread.
|
||||||
|
pub fn tui_input <T: Handle<TuiIn> + Send + Sync + 'static> (
|
||||||
|
engine: Arc<RwLock<Tui>>, state: &Arc<RwLock<T>>, timer: Duration
|
||||||
|
) -> JoinHandle<()> {
|
||||||
|
let exited = engine.read().unwrap().exited.clone();
|
||||||
|
let state = state.clone();
|
||||||
|
spawn(move || loop {
|
||||||
|
if exited.fetch_and(true, Relaxed) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if poll(timer).is_ok() {
|
||||||
|
let event = read().unwrap();
|
||||||
|
match event {
|
||||||
|
Event::Key(KeyEvent {
|
||||||
|
code: KeyCode::Char('c'),
|
||||||
|
modifiers: KeyModifiers::CONTROL,
|
||||||
|
kind: KeyEventKind::Press,
|
||||||
|
state: KeyEventState::NONE
|
||||||
|
}) => {
|
||||||
|
exited.store(true, Relaxed);
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
let exited = exited.clone();
|
||||||
|
let event = TuiEvent::from_crossterm(event);
|
||||||
|
if let Err(e) = state.write().unwrap().handle(&TuiIn { exited, event }) {
|
||||||
|
panic!("{e}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "dsl")] pub fn evaluate_output_expression_tui <'a, S> (
|
||||||
|
state: &S, output: &mut TuiOut, expr: impl Expression + 'a
|
||||||
|
) -> Usually<bool> where
|
||||||
|
S: View<TuiOut, ()>
|
||||||
|
+ for<'b>Namespace<'b, bool>
|
||||||
|
+ for<'b>Namespace<'b, u16>
|
||||||
|
+ for<'b>Namespace<'b, Color>
|
||||||
|
{
|
||||||
|
// See `tengri::evaluate_output_expression`
|
||||||
|
let head = expr.head()?;
|
||||||
|
let mut frags = head.src()?.unwrap_or_default().split("/");
|
||||||
|
let args = expr.tail();
|
||||||
|
let arg0 = args.head();
|
||||||
|
let tail0 = args.tail();
|
||||||
|
let arg1 = tail0.head();
|
||||||
|
let tail1 = tail0.tail();
|
||||||
|
let _arg2 = tail1.head();
|
||||||
|
match frags.next() {
|
||||||
|
|
||||||
|
Some("text") => if let Some(src) = args?.src()? { output.place(&src) },
|
||||||
|
|
||||||
|
Some("fg") => {
|
||||||
|
let arg0 = arg0?.expect("fg: expected arg 0 (color)");
|
||||||
|
output.place(&Tui::fg(
|
||||||
|
Namespace::<Color>::namespace(state, arg0)?.unwrap_or_else(||panic!("fg: {arg0:?}: not a color")),
|
||||||
|
Thunk::new(move|output: &mut TuiOut|state.view(output, &arg1).unwrap()),
|
||||||
|
))
|
||||||
|
},
|
||||||
|
|
||||||
|
Some("bg") => {
|
||||||
|
let arg0 = arg0?.expect("bg: expected arg 0 (color)");
|
||||||
|
output.place(&Tui::bg(
|
||||||
|
Namespace::<Color>::namespace(state, arg0)?.unwrap_or_else(||panic!("bg: {arg0:?}: not a color")),
|
||||||
|
Thunk::new(move|output: &mut TuiOut|state.view(output, &arg1).unwrap()),
|
||||||
|
))
|
||||||
|
},
|
||||||
|
|
||||||
|
_ => return Ok(false)
|
||||||
|
|
||||||
|
};
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn named_key (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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn button_2 <'a> (key: impl Content<TuiOut>, label: impl Content<TuiOut>, editing: bool) -> impl Content<TuiOut> {
|
||||||
|
Tui::bold(true, Bsp::e(
|
||||||
|
Tui::fg_bg(Tui::orange(), Tui::g(0), Bsp::e(Tui::fg(Tui::g(0), &"▐"), Bsp::e(key, Tui::fg(Tui::g(96), &"▐")))),
|
||||||
|
When::new(!editing, Tui::fg_bg(Tui::g(255), Tui::g(96), label))))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn button_3 <'a> (
|
||||||
|
key: impl Content<TuiOut>, label: impl Content<TuiOut>, value: impl Content<TuiOut>, editing: bool,
|
||||||
|
) -> impl Content<TuiOut> {
|
||||||
|
Tui::bold(true, Bsp::e(
|
||||||
|
Tui::fg_bg(Tui::orange(), Tui::g(0),
|
||||||
|
Bsp::e(Tui::fg(Tui::g(0), &"▐"), Bsp::e(key, Tui::fg(if editing { Tui::g(128) } else { Tui::g(96) }, "▐")))),
|
||||||
|
Bsp::e(
|
||||||
|
When::new(!editing, Bsp::e(Tui::fg_bg(Tui::g(255), Tui::g(96), label), Tui::fg_bg(Tui::g(128), Tui::g(96), &"▐"),)),
|
||||||
|
Bsp::e(Tui::fg_bg(Tui::g(224), Tui::g(128), value), Tui::fg_bg(Tui::g(128), Reset, &"▌"), ))))
|
||||||
|
}
|
||||||
|
|
||||||
|
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 fn okhsl_to_rgb (color: Okhsl<f32>) -> Color {
|
||||||
|
let Srgb { red, green, blue, .. }: Srgb<f32> = Srgb::from_color_unclamped(color);
|
||||||
|
Color::Rgb((red * 255.0) as u8, (green * 255.0) as u8, (blue * 255.0) as u8,)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rgb_to_okhsl (color: Color) -> Okhsl<f32> {
|
||||||
|
if let Color::Rgb(r, g, b) = color {
|
||||||
|
Okhsl::from_color(Srgb::new(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0))
|
||||||
|
} else {
|
||||||
|
unreachable!("only Color::Rgb is supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trim string with [unicode_width].
|
||||||
|
pub fn trim_string (max_width: usize, input: impl AsRef<str>) -> String {
|
||||||
|
let input = input.as_ref();
|
||||||
|
let mut output = Vec::with_capacity(input.len());
|
||||||
|
let mut width: usize = 1;
|
||||||
|
let mut chars = input.chars();
|
||||||
|
while let Some(c) = chars.next() {
|
||||||
|
if width > max_width {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
output.push(c);
|
||||||
|
width += c.width().unwrap_or(0);
|
||||||
|
}
|
||||||
|
return output.into_iter().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn width_chars_max (max: u16, text: impl AsRef<str>) -> u16 {
|
||||||
|
let mut width: u16 = 0;
|
||||||
|
let mut chars = text.as_ref().chars();
|
||||||
|
while let Some(c) = chars.next() {
|
||||||
|
width += c.width().unwrap_or(0) as u16;
|
||||||
|
if width > max {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return width
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline] pub fn map_south<O: Out>(
|
||||||
|
item_offset: O::Unit,
|
||||||
|
item_height: O::Unit,
|
||||||
|
item: impl Content<O>
|
||||||
|
) -> impl Content<O> {
|
||||||
|
Push::Y(item_offset, Fixed::Y(item_height, Fill::X(item)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline] pub fn map_south_west<O: Out>(
|
||||||
|
item_offset: O::Unit,
|
||||||
|
item_height: O::Unit,
|
||||||
|
item: impl Content<O>
|
||||||
|
) -> impl Content<O> {
|
||||||
|
Push::Y(item_offset, Align::nw(Fixed::Y(item_height, Fill::X(item))))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline] pub fn map_east<O: Out>(
|
||||||
|
item_offset: O::Unit,
|
||||||
|
item_width: O::Unit,
|
||||||
|
item: impl Content<O>
|
||||||
|
) -> impl Content<O> {
|
||||||
|
Push::X(item_offset, Align::w(Fixed::X(item_width, Fill::Y(item))))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)] mod test {
|
||||||
|
use proptest::{prelude::*, option::of};
|
||||||
|
use proptest_derive::Arbitrary;
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
#[test] fn proptest_direction (
|
||||||
|
d in prop_oneof![
|
||||||
|
Just(North), Just(South),
|
||||||
|
Just(East), Just(West),
|
||||||
|
Just(Above), Just(Below)
|
||||||
|
],
|
||||||
|
x in u16::MIN..u16::MAX,
|
||||||
|
y in u16::MIN..u16::MAX,
|
||||||
|
w in u16::MIN..u16::MAX,
|
||||||
|
h in u16::MIN..u16::MAX,
|
||||||
|
a in u16::MIN..u16::MAX,
|
||||||
|
) {
|
||||||
|
let _ = d.split_fixed(XYWH(x, y, w, h), a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
#[test] fn proptest_area (
|
||||||
|
x in u16::MIN..u16::MAX,
|
||||||
|
y in u16::MIN..u16::MAX,
|
||||||
|
w in u16::MIN..u16::MAX,
|
||||||
|
h in u16::MIN..u16::MAX,
|
||||||
|
a in u16::MIN..u16::MAX,
|
||||||
|
b in u16::MIN..u16::MAX,
|
||||||
|
) {
|
||||||
|
let _: XYWH<u16> = XYWH::zero();
|
||||||
|
//let _: XYWH<u16> = XYWH::from_position([a, b]);
|
||||||
|
//let _: XYWH<u16> = XYWH::from_size([a, b]);
|
||||||
|
let area: XYWH<u16> = XYWH(x, y, w, h);
|
||||||
|
//let _ = area.expect_min(a, b);
|
||||||
|
let _ = area.xy();
|
||||||
|
let _ = area.wh();
|
||||||
|
//let _ = area.xywh();
|
||||||
|
let _ = area.clipped_h(a);
|
||||||
|
let _ = area.clipped_w(b);
|
||||||
|
let _ = area.clipped(WH(a, b));
|
||||||
|
//let _ = area.set_w(a);
|
||||||
|
//let _ = area.set_h(b);
|
||||||
|
let _ = area.x2();
|
||||||
|
let _ = area.y2();
|
||||||
|
let _ = area.lrtb();
|
||||||
|
let _ = area.center();
|
||||||
|
let _ = area.centered();
|
||||||
|
let _ = area.centered_x(a);
|
||||||
|
let _ = area.centered_y(b);
|
||||||
|
let _ = area.centered_xy([a, b]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
#[test] fn proptest_size (
|
||||||
|
x in u16::MIN..u16::MAX,
|
||||||
|
y in u16::MIN..u16::MAX,
|
||||||
|
a in u16::MIN..u16::MAX,
|
||||||
|
b in u16::MIN..u16::MAX,
|
||||||
|
) {
|
||||||
|
let size = WH(x, y);
|
||||||
|
let _ = size.w();
|
||||||
|
let _ = size.h();
|
||||||
|
let _ = size.wh();
|
||||||
|
let _ = size.clip_w(a);
|
||||||
|
let _ = size.clip_h(b);
|
||||||
|
//let _ = size.expect_min(a, b);
|
||||||
|
//let _ = size.to_area_pos();
|
||||||
|
//let _ = size.to_area_size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! test_op_transform {
|
||||||
|
($fn:ident, $Op:ident) => {
|
||||||
|
proptest! {
|
||||||
|
#[test] fn $fn (
|
||||||
|
op_x in of(u16::MIN..u16::MAX),
|
||||||
|
op_y in of(u16::MIN..u16::MAX),
|
||||||
|
content in "\\PC*",
|
||||||
|
x in u16::MIN..u16::MAX,
|
||||||
|
y in u16::MIN..u16::MAX,
|
||||||
|
w in u16::MIN..u16::MAX,
|
||||||
|
h in u16::MIN..u16::MAX,
|
||||||
|
) {
|
||||||
|
if let Some(op) = match (op_x, op_y) {
|
||||||
|
(Some(x), Some(y)) => Some($Op::XY(x, y, content)),
|
||||||
|
(Some(x), None) => Some($Op::X(x, content)),
|
||||||
|
(None, Some(y)) => Some($Op::Y(y, content)),
|
||||||
|
_ => None
|
||||||
|
} {
|
||||||
|
//assert_eq!(Content::layout(&op, [x, y, w, h]),
|
||||||
|
//Draw::layout(&op, [x, y, w, h]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
test_op_transform!(proptest_op_fixed, Fixed);
|
||||||
|
test_op_transform!(proptest_op_min, Min);
|
||||||
|
test_op_transform!(proptest_op_max, Max);
|
||||||
|
test_op_transform!(proptest_op_push, Push);
|
||||||
|
test_op_transform!(proptest_op_pull, Pull);
|
||||||
|
test_op_transform!(proptest_op_shrink, Shrink);
|
||||||
|
test_op_transform!(proptest_op_expand, Expand);
|
||||||
|
test_op_transform!(proptest_op_padding, Pad);
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
#[test] fn proptest_op_bsp (
|
||||||
|
d in prop_oneof![
|
||||||
|
Just(North), Just(South),
|
||||||
|
Just(East), Just(West),
|
||||||
|
Just(Above), Just(Below)
|
||||||
|
],
|
||||||
|
a in "\\PC*",
|
||||||
|
b in "\\PC*",
|
||||||
|
x in u16::MIN..u16::MAX,
|
||||||
|
y in u16::MIN..u16::MAX,
|
||||||
|
w in u16::MIN..u16::MAX,
|
||||||
|
h in u16::MIN..u16::MAX,
|
||||||
|
) {
|
||||||
|
let bsp = Bsp(d, a, b);
|
||||||
|
//assert_eq!(
|
||||||
|
//Content::layout(&bsp, [x, y, w, h]),
|
||||||
|
//Draw::layout(&bsp, [x, y, w, h]),
|
||||||
|
//);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test] fn test_tui_engine () -> Usually<()> {
|
||||||
|
//use std::sync::{Arc, RwLock};
|
||||||
|
struct TestComponent(String);
|
||||||
|
impl Draw<TuiOut> for TestComponent {
|
||||||
|
fn draw (&self, _to: &mut TuiOut) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Handle<TuiIn> for TestComponent {
|
||||||
|
fn handle (&mut self, _from: &TuiIn) -> Perhaps<bool> {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let engine = Tui::run(false, TestComponent("hello world".into()))?;
|
||||||
|
engine.read().unwrap().exited.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
//engine.run(&state)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,116 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
use Alignment::*;
|
use Alignment::*;
|
||||||
|
use Direction::*;
|
||||||
|
use unicode_width::{UnicodeWidthStr, UnicodeWidthChar};
|
||||||
|
use rand::{thread_rng, distributions::uniform::UniformSampler};
|
||||||
|
|
||||||
|
macro_rules! layout_op_xy (
|
||||||
|
// Variant for layout ops that take no coordinates
|
||||||
|
(0: $T: ident) => {
|
||||||
|
impl<A> $T<A> {
|
||||||
|
#[inline] pub const fn inner (&self) -> &A {
|
||||||
|
match self { Self::X(c) | Self::Y(c) | Self::XY(c) => c }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<O: Out, T: Content<O>> Draw<O> for $T<T> {
|
||||||
|
fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Variant for layout ops that take one coordinate
|
||||||
|
(1: $T: ident) => {
|
||||||
|
impl<U, A> $T<U, A> {
|
||||||
|
#[inline] pub const fn inner (&self) -> &A {
|
||||||
|
match self { Self::X(_, c) | Self::Y(_, c) | Self::XY(_, _, c) => c, }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<O: Out, T: Content<O>> Draw<O> for $T<O::Unit, T> {
|
||||||
|
fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) }
|
||||||
|
}
|
||||||
|
impl<U: Coord, A> $T<U, A> {
|
||||||
|
#[inline] pub fn dx (&self) -> U {
|
||||||
|
match self { Self::X(x, _) | Self::XY(x, ..) => *x, _ => 0.into() }
|
||||||
|
}
|
||||||
|
#[inline] pub fn dy (&self) -> U {
|
||||||
|
match self { Self::Y(y, _) | Self::XY(y, ..) => *y, _ => 0.into() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(1 opt: $T: ident) => {
|
||||||
|
impl<U, A> $T<U, A> {
|
||||||
|
#[inline] pub const fn inner (&self) -> &A {
|
||||||
|
match self { Self::X(_, c) | Self::Y(_, c) | Self::XY(_, _, c) => c, }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<O: Out, T: Content<O>> Draw<O> for $T<O::Unit, T> {
|
||||||
|
fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) }
|
||||||
|
}
|
||||||
|
impl<U: Coord, A> $T<U, A> {
|
||||||
|
#[inline] pub const fn dx (&self) -> Option<U> {
|
||||||
|
match self { Self::X(x, _) | Self::XY(x, ..) => Some(*x), _ => None }
|
||||||
|
}
|
||||||
|
#[inline] pub const fn dy (&self) -> Option<U> {
|
||||||
|
match self { Self::Y(y, _) | Self::XY(y, ..) => Some(*y), _ => None }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
);
|
||||||
|
|
||||||
|
// Implement layout op that increments X and/or Y by fixed amount.
|
||||||
|
macro_rules! push_pull(($T:ident: $method: ident)=>{
|
||||||
|
layout_op_xy!(1: $T);
|
||||||
|
impl<O: Out, T: Layout<O>> Layout<O> for $T<O::Unit, T> {
|
||||||
|
fn layout_x (&self, area: XYWH<O::Unit>) -> O::Unit { area.x().$method(self.dx()) }
|
||||||
|
fn layout_y (&self, area: XYWH<O::Unit>) -> O::Unit { area.y().$method(self.dy()) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
macro_rules! impl_map_direction (($name:ident, $axis:ident, $align:ident)=>{
|
||||||
|
impl<'a, O, A, B, I, F> Map<
|
||||||
|
O, A, Push<O::Unit, Align<Fixed<O::Unit, Fill<B>>>>, I, F, fn(A, usize)->B
|
||||||
|
> where
|
||||||
|
O: Out,
|
||||||
|
B: Draw<O>,
|
||||||
|
I: Iterator<Item = A> + Send + Sync + 'a,
|
||||||
|
F: Fn() -> I + Send + Sync + 'a
|
||||||
|
{
|
||||||
|
pub const fn $name (
|
||||||
|
size: O::Unit,
|
||||||
|
get_iter: F,
|
||||||
|
get_item: impl Fn(A, usize)->B + Send + Sync
|
||||||
|
) -> Map<
|
||||||
|
O, A,
|
||||||
|
Push<O::Unit, Align<Fixed<O::Unit, B>>>,
|
||||||
|
I, F,
|
||||||
|
impl Fn(A, usize)->Push<O::Unit, Align<Fixed<O::Unit, B>>> + Send + Sync
|
||||||
|
> {
|
||||||
|
Map {
|
||||||
|
__: PhantomData,
|
||||||
|
get_iter,
|
||||||
|
get_item: move |item: A, index: usize|{
|
||||||
|
// FIXME: multiply
|
||||||
|
let mut push: O::Unit = O::Unit::from(0u16);
|
||||||
|
for _ in 0..index {
|
||||||
|
push = push + size;
|
||||||
|
}
|
||||||
|
Push::$axis(push, Align::$align(Fixed::$axis(size, get_item(item, index))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
impl<S, T: Command<S>> Command<S> for Option<T> {
|
||||||
|
fn execute (&self, _: &mut S) -> Perhaps<Self> {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
fn delegate <U> (&self, _: &mut S, _: impl Fn(Self)->U) -> Perhaps<U>
|
||||||
|
where Self: Sized
|
||||||
|
{
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
impl Coord for u16 {
|
impl Coord for u16 {
|
||||||
fn plus (self, other: Self) -> Self { self.saturating_add(other) }
|
fn plus (self, other: Self) -> Self { self.saturating_add(other) }
|
||||||
|
|
@ -744,30 +855,6 @@ impl<'a, O, A, B, I, F, G> Draw<O> for Map<O, A, B, I, F, G> where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline] pub fn map_south<O: Out>(
|
|
||||||
item_offset: O::Unit,
|
|
||||||
item_height: O::Unit,
|
|
||||||
item: impl Content<O>
|
|
||||||
) -> impl Content<O> {
|
|
||||||
Push::Y(item_offset, Fixed::Y(item_height, Fill::X(item)))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline] pub fn map_south_west<O: Out>(
|
|
||||||
item_offset: O::Unit,
|
|
||||||
item_height: O::Unit,
|
|
||||||
item: impl Content<O>
|
|
||||||
) -> impl Content<O> {
|
|
||||||
Push::Y(item_offset, Align::nw(Fixed::Y(item_height, Fill::X(item))))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline] pub fn map_east<O: Out>(
|
|
||||||
item_offset: O::Unit,
|
|
||||||
item_width: O::Unit,
|
|
||||||
item: impl Content<O>
|
|
||||||
) -> impl Content<O> {
|
|
||||||
Push::X(item_offset, Align::w(Fixed::X(item_width, Fill::Y(item))))
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Tryptich<(), (), ()> {
|
impl Tryptich<(), (), ()> {
|
||||||
pub fn center (h: u16) -> Self {
|
pub fn center (h: u16) -> Self {
|
||||||
Self { h, top: false, left: (0, ()), middle: (0, ()), right: (0, ()) }
|
Self { h, top: false, left: (0, ()), middle: (0, ()), right: (0, ()) }
|
||||||
|
|
@ -894,3 +981,637 @@ impl PerfModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Tui {
|
||||||
|
/// Create and launch a terminal user interface.
|
||||||
|
pub fn run <T> (join: bool, state: T) -> Usually<Arc<RwLock<Self>>> where
|
||||||
|
T: Handle<TuiIn> + Draw<TuiOut> + Send + Sync + 'static
|
||||||
|
{
|
||||||
|
tui_run(join, &Arc::new(RwLock::new(state)))
|
||||||
|
}
|
||||||
|
/// True if done
|
||||||
|
pub fn exited (&self) -> bool { self.exited.fetch_and(true, Relaxed) }
|
||||||
|
/// Prepare before run
|
||||||
|
pub fn setup (&mut self) -> Usually<()> { tui_setup(&mut self.backend) }
|
||||||
|
/// Clean up after run
|
||||||
|
pub fn teardown (&mut self) -> Usually<()> { tui_teardown(&mut self.backend) }
|
||||||
|
/// Apply changes to the display buffer.
|
||||||
|
pub fn flip (&mut self, mut buffer: Buffer, size: ratatui::prelude::Rect) -> Buffer {
|
||||||
|
tui_resized(&mut self.backend, &mut self.buffer, size);
|
||||||
|
tui_redrawn(&mut self.backend, &mut self.buffer, &mut buffer);
|
||||||
|
buffer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Input for TuiIn {
|
||||||
|
type Event = TuiEvent;
|
||||||
|
type Handled = bool;
|
||||||
|
fn event (&self) -> &TuiEvent { &self.event }
|
||||||
|
fn done (&self) { self.exited.store(true, Relaxed); }
|
||||||
|
fn is_done (&self) -> bool { self.exited.fetch_and(true, Relaxed) }
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl TuiEvent {
|
||||||
|
pub fn from_crossterm (event: Event) -> Self { Self(event) }
|
||||||
|
#[cfg(feature = "dsl")] pub fn from_dsl (dsl: impl Language) -> Perhaps<Self> {
|
||||||
|
Ok(TuiKey::from_dsl(dsl)?.to_crossterm().map(Self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl TuiKey {
|
||||||
|
const SPLIT: char = '/';
|
||||||
|
#[cfg(feature = "dsl")] 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn to_crossterm (&self) -> Option<Event> {
|
||||||
|
self.0.map(|code|Event::Key(KeyEvent {
|
||||||
|
code,
|
||||||
|
modifiers: self.1,
|
||||||
|
kind: KeyEventKind::Press,
|
||||||
|
state: KeyEventState::NONE,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Out for TuiOut {
|
||||||
|
type Unit = u16;
|
||||||
|
#[inline] fn area (&self) -> XYWH<u16> { self.area }
|
||||||
|
#[inline] fn area_mut (&mut self) -> &mut XYWH<u16> { &mut self.area }
|
||||||
|
#[inline] fn place_at <'t, T: Draw<Self> + ?Sized> (&mut self, area: XYWH<u16>, content: &'t T) {
|
||||||
|
let last = self.area();
|
||||||
|
*self.area_mut() = area;
|
||||||
|
content.draw(self);
|
||||||
|
*self.area_mut() = last;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl TuiOut {
|
||||||
|
#[inline] pub fn with_rect (&mut self, area: XYWH<u16>) -> &mut Self { self.area = area; self }
|
||||||
|
pub fn update (&mut self, area: XYWH<u16>, callback: &impl Fn(&mut Cell, u16, u16)) { tui_update(&mut self.buffer, area, callback); }
|
||||||
|
pub fn fill_char (&mut self, area: XYWH<u16>, c: char) { self.update(area, &|cell,_,_|{cell.set_char(c);}) }
|
||||||
|
pub fn fill_bg (&mut self, area: XYWH<u16>, color: Color) { self.update(area, &|cell,_,_|{cell.set_bg(color);}) }
|
||||||
|
pub fn fill_fg (&mut self, area: XYWH<u16>, color: Color) { self.update(area, &|cell,_,_|{cell.set_fg(color);}) }
|
||||||
|
pub fn fill_mod (&mut self, area: XYWH<u16>, on: bool, modifier: Modifier) {
|
||||||
|
if on {
|
||||||
|
self.update(area, &|cell,_,_|cell.modifier.insert(modifier))
|
||||||
|
} else {
|
||||||
|
self.update(area, &|cell,_,_|cell.modifier.remove(modifier))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn fill_bold (&mut self, area: XYWH<u16>, on: bool) { self.fill_mod(area, on, Modifier::BOLD) }
|
||||||
|
pub fn fill_reversed (&mut self, area: XYWH<u16>, on: bool) { self.fill_mod(area, on, Modifier::REVERSED) }
|
||||||
|
pub fn fill_crossed_out (&mut self, area: XYWH<u16>, on: bool) { self.fill_mod(area, on, Modifier::CROSSED_OUT) }
|
||||||
|
pub fn fill_ul (&mut self, area: XYWH<u16>, color: Option<Color>) {
|
||||||
|
if let Some(color) = color {
|
||||||
|
self.update(area, &|cell,_,_|{
|
||||||
|
cell.modifier.insert(ratatui::prelude::Modifier::UNDERLINED);
|
||||||
|
cell.underline_color = color;
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
self.update(area, &|cell,_,_|{
|
||||||
|
cell.modifier.remove(ratatui::prelude::Modifier::UNDERLINED);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn tint_all (&mut self, fg: Color, bg: Color, modifier: Modifier) {
|
||||||
|
for cell in self.buffer.content.iter_mut() {
|
||||||
|
cell.fg = fg;
|
||||||
|
cell.bg = bg;
|
||||||
|
cell.modifier = modifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn blit (&mut self, text: &impl AsRef<str>, x: u16, y: u16, style: Option<Style>) {
|
||||||
|
let text = text.as_ref();
|
||||||
|
let buf = &mut self.buffer;
|
||||||
|
let style = style.unwrap_or(Style::default());
|
||||||
|
if x < buf.area.width && y < buf.area.height {
|
||||||
|
buf.set_string(x, y, text, style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Write a line of text
|
||||||
|
///
|
||||||
|
/// TODO: do a paragraph (handle newlines)
|
||||||
|
pub fn text (&mut self, text: &impl AsRef<str>, x0: u16, y: u16, max_width: u16) {
|
||||||
|
let text = text.as_ref();
|
||||||
|
let buf = &mut self.buffer;
|
||||||
|
let mut string_width: u16 = 0;
|
||||||
|
for character in text.chars() {
|
||||||
|
let x = x0 + string_width;
|
||||||
|
let character_width = character.width().unwrap_or(0) as u16;
|
||||||
|
string_width += character_width;
|
||||||
|
if string_width > max_width {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if let Some(cell) = buf.cell_mut(ratatui::prelude::Position { x, y }) {
|
||||||
|
cell.set_char(character);
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A single color within item theme parameters, in OKHSL and RGB representations.
|
||||||
|
impl ItemColor {
|
||||||
|
pub const fn from_rgb (rgb: Color) -> Self {
|
||||||
|
Self { rgb, okhsl: Okhsl::new_const(OklabHue::new(0.0), 0.0, 0.0) }
|
||||||
|
}
|
||||||
|
pub const fn from_okhsl (okhsl: Okhsl<f32>) -> Self {
|
||||||
|
Self { rgb: Color::Rgb(0, 0, 0), okhsl }
|
||||||
|
}
|
||||||
|
pub fn random () -> Self {
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
let lo = Okhsl::new(-180.0, 0.01, 0.25);
|
||||||
|
let hi = Okhsl::new( 180.0, 0.9, 0.5);
|
||||||
|
UniformOkhsl::new(lo, hi).sample(&mut rng).into()
|
||||||
|
}
|
||||||
|
pub fn random_dark () -> Self {
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
let lo = Okhsl::new(-180.0, 0.025, 0.075);
|
||||||
|
let hi = Okhsl::new( 180.0, 0.5, 0.150);
|
||||||
|
UniformOkhsl::new(lo, hi).sample(&mut rng).into()
|
||||||
|
}
|
||||||
|
pub fn random_near (color: Self, distance: f32) -> Self {
|
||||||
|
color.mix(Self::random(), distance)
|
||||||
|
}
|
||||||
|
pub fn mix (&self, other: Self, distance: f32) -> Self {
|
||||||
|
if distance > 1.0 { panic!("color mixing takes distance between 0.0 and 1.0"); }
|
||||||
|
self.okhsl.mix(other.okhsl, distance).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ItemTheme {
|
||||||
|
pub const G: [Self;256] = {
|
||||||
|
let mut builder = konst::array::ArrayBuilder::new();
|
||||||
|
while !builder.is_full() {
|
||||||
|
let index = builder.len() as u8;
|
||||||
|
let light = (index as f64 * 1.15) as u8;
|
||||||
|
let lighter = (index as f64 * 1.7) as u8;
|
||||||
|
let lightest = (index as f64 * 1.85) as u8;
|
||||||
|
let dark = (index as f64 * 0.9) as u8;
|
||||||
|
let darker = (index as f64 * 0.6) as u8;
|
||||||
|
let darkest = (index as f64 * 0.3) as u8;
|
||||||
|
builder.push(ItemTheme {
|
||||||
|
base: ItemColor::from_rgb(Color::Rgb(index, index, index )),
|
||||||
|
light: ItemColor::from_rgb(Color::Rgb(light, light, light, )),
|
||||||
|
lighter: ItemColor::from_rgb(Color::Rgb(lighter, lighter, lighter, )),
|
||||||
|
lightest: ItemColor::from_rgb(Color::Rgb(lightest, lightest, lightest, )),
|
||||||
|
dark: ItemColor::from_rgb(Color::Rgb(dark, dark, dark, )),
|
||||||
|
darker: ItemColor::from_rgb(Color::Rgb(darker, darker, darker, )),
|
||||||
|
darkest: ItemColor::from_rgb(Color::Rgb(darkest, darkest, darkest, )),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
builder.build()
|
||||||
|
};
|
||||||
|
pub fn random () -> Self { ItemColor::random().into() }
|
||||||
|
pub fn random_near (color: Self, distance: f32) -> Self {
|
||||||
|
color.base.mix(ItemColor::random(), distance).into()
|
||||||
|
}
|
||||||
|
pub const G00: Self = {
|
||||||
|
let color: ItemColor = ItemColor {
|
||||||
|
okhsl: Okhsl { hue: OklabHue::new(0.0), lightness: 0.0, saturation: 0.0 },
|
||||||
|
rgb: Color::Rgb(0, 0, 0)
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
base: color,
|
||||||
|
light: color,
|
||||||
|
lighter: color,
|
||||||
|
lightest: color,
|
||||||
|
dark: color,
|
||||||
|
darker: color,
|
||||||
|
darkest: color,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
pub fn from_tui_color (base: Color) -> Self {
|
||||||
|
Self::from_item_color(ItemColor::from_rgb(base))
|
||||||
|
}
|
||||||
|
pub fn from_item_color (base: ItemColor) -> Self {
|
||||||
|
let mut light = base.okhsl;
|
||||||
|
light.lightness = (light.lightness * 1.3).min(1.0);
|
||||||
|
let mut lighter = light;
|
||||||
|
lighter.lightness = (lighter.lightness * 1.3).min(1.0);
|
||||||
|
let mut lightest = base.okhsl;
|
||||||
|
lightest.lightness = 0.95;
|
||||||
|
let mut dark = base.okhsl;
|
||||||
|
dark.lightness = (dark.lightness * 0.75).max(0.0);
|
||||||
|
dark.saturation = (dark.saturation * 0.75).max(0.0);
|
||||||
|
let mut darker = dark;
|
||||||
|
darker.lightness = (darker.lightness * 0.66).max(0.0);
|
||||||
|
darker.saturation = (darker.saturation * 0.66).max(0.0);
|
||||||
|
let mut darkest = darker;
|
||||||
|
darkest.lightness = 0.1;
|
||||||
|
darkest.saturation = (darkest.saturation * 0.50).max(0.0);
|
||||||
|
Self {
|
||||||
|
base,
|
||||||
|
light: light.into(),
|
||||||
|
lighter: lighter.into(),
|
||||||
|
lightest: lightest.into(),
|
||||||
|
dark: dark.into(),
|
||||||
|
darker: darker.into(),
|
||||||
|
darkest: darkest.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T> Phat<T> {
|
||||||
|
pub const LO: &'static str = "▄";
|
||||||
|
pub const HI: &'static str = "▀";
|
||||||
|
/// A phat line
|
||||||
|
pub fn lo (fg: Color, bg: Color) -> impl Content<TuiOut> {
|
||||||
|
Fixed::Y(1, Tui::fg_bg(fg, bg, Repeat::X(Self::LO)))
|
||||||
|
}
|
||||||
|
/// A phat line
|
||||||
|
pub fn hi (fg: Color, bg: Color) -> impl Content<TuiOut> {
|
||||||
|
Fixed::Y(1, Tui::fg_bg(fg, bg, Repeat::X(Self::HI)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Scrollbar {
|
||||||
|
const ICON_DEC_V: &[char] = &['▲'];
|
||||||
|
const ICON_INC_V: &[char] = &['▼'];
|
||||||
|
const ICON_DEC_H: &[char] = &[' ', '🞀', ' '];
|
||||||
|
const ICON_INC_H: &[char] = &[' ', '🞂', ' '];
|
||||||
|
}
|
||||||
|
impl<'a, T: AsRef<str>> TrimString<T> {
|
||||||
|
fn as_ref (&self) -> TrimStringRef<'_, T> { TrimStringRef(self.0, &self.1) }
|
||||||
|
}
|
||||||
|
impl<O: Out, T: Draw<O>> ErrorBoundary<O, T> {
|
||||||
|
pub fn new (content: Perhaps<T>) -> Self { Self(Default::default(), content) }
|
||||||
|
}
|
||||||
|
mod content {
|
||||||
|
use super::*;
|
||||||
|
impl<S: BorderStyle, W: Content<TuiOut>> HasContent<TuiOut> for Bordered<S, W> {
|
||||||
|
fn content (&self) -> impl Content<TuiOut> {
|
||||||
|
Fill::XY(lay!( When::new(self.0, Border(self.0, self.1)), Pad::XY(1, 1, &self.2) ))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<
|
||||||
|
A: Content<TuiOut>,
|
||||||
|
B: Content<TuiOut>,
|
||||||
|
C: Content<TuiOut>,
|
||||||
|
> HasContent<TuiOut> for Tryptich<A, B, C> {
|
||||||
|
fn content (&self) -> impl Content<TuiOut> {
|
||||||
|
let Self { top, h, left: (w_a, ref a), middle: (w_b, ref b), right: (w_c, ref c) } = *self;
|
||||||
|
Fixed::Y(h, if top {
|
||||||
|
Bsp::a(
|
||||||
|
Fill::X(Align::n(Fixed::X(w_b, Align::x(Tui::bg(Color::Reset, b))))),
|
||||||
|
Bsp::a(
|
||||||
|
Fill::X(Align::nw(Fixed::X(w_a, Tui::bg(Color::Reset, a)))),
|
||||||
|
Fill::X(Align::ne(Fixed::X(w_c, Tui::bg(Color::Reset, c)))),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Bsp::a(
|
||||||
|
Fill::XY(Align::c(Fixed::X(w_b, Align::x(Tui::bg(Color::Reset, b))))),
|
||||||
|
Bsp::a(
|
||||||
|
Fill::XY(Align::w(Fixed::X(w_a, Tui::bg(Color::Reset, a)))),
|
||||||
|
Fill::XY(Align::e(Fixed::X(w_c, Tui::bg(Color::Reset, c)))),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T: Content<TuiOut>> HasContent<TuiOut> for Phat<T> {
|
||||||
|
fn content (&self) -> impl Content<TuiOut> {
|
||||||
|
let [fg, bg, hi, lo] = self.colors;
|
||||||
|
let top = Fixed::Y(1, Self::lo(bg, hi));
|
||||||
|
let low = Fixed::Y(1, Self::hi(bg, lo));
|
||||||
|
let content = Tui::fg_bg(fg, bg, &self.content);
|
||||||
|
Min::XY(self.width, self.height, Bsp::s(top, Bsp::n(low, Fill::XY(content))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod layout {
|
||||||
|
use super::*;
|
||||||
|
impl<T: Content<TuiOut>> Layout<TuiOut> for Modify<T> {}
|
||||||
|
impl<T: Content<TuiOut>> Layout<TuiOut> for Styled<T> {}
|
||||||
|
impl Layout<TuiOut> for Repeat<'_> {}
|
||||||
|
impl Layout<TuiOut> for &str {
|
||||||
|
fn layout (&self, to: XYWH<u16>) -> XYWH<u16> {
|
||||||
|
to.centered_xy([width_chars_max(to.w(), self), 1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Layout<TuiOut> for String {
|
||||||
|
fn layout (&self, to: XYWH<u16>) -> XYWH<u16> {
|
||||||
|
self.as_str().layout(to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Layout<TuiOut> for Arc<str> {
|
||||||
|
fn layout (&self, to: XYWH<u16>) -> XYWH<u16> {
|
||||||
|
self.as_ref().layout(to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'a, T: AsRef<str>> Layout<TuiOut> for TrimString<T> {
|
||||||
|
fn layout (&self, to: XYWH<u16>) -> XYWH<u16> {
|
||||||
|
Layout::layout(&self.as_ref(), to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'a, T: AsRef<str>> Layout<TuiOut> for TrimStringRef<'a, T> {
|
||||||
|
fn layout (&self, to: XYWH<u16>) -> XYWH<u16> {
|
||||||
|
XYWH(to.x(), to.y(), to.w().min(self.0).min(self.1.as_ref().width() as u16), to.h())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod draw {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl<T: Draw<TuiOut>> Draw<TuiOut> for ErrorBoundary<TuiOut, T> {
|
||||||
|
fn draw (&self, to: &mut TuiOut) {
|
||||||
|
match self.1.as_ref() {
|
||||||
|
Ok(Some(content)) => content.draw(to),
|
||||||
|
Ok(None) => to.blit(&"empty?", 0, 0, Some(Style::default().yellow())),
|
||||||
|
Err(e) => {
|
||||||
|
let err_fg = Color::Rgb(255,224,244);
|
||||||
|
let err_bg = Color::Rgb(96,24,24);
|
||||||
|
let title = Bsp::e(Tui::bold(true, "oops. "), "rendering failed.");
|
||||||
|
let error = Bsp::e("\"why?\" ", Tui::bold(true, format!("{e}")));
|
||||||
|
to.place(&Tui::fg_bg(err_fg, err_bg, Bsp::s(title, error)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Draw<TuiOut> for u64 {
|
||||||
|
fn draw (&self, _to: &mut TuiOut) {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Draw<TuiOut> for f64 {
|
||||||
|
fn draw (&self, _to: &mut TuiOut) {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Draw<TuiOut> for Repeat<'_> {
|
||||||
|
fn draw (&self, to: &mut TuiOut) {
|
||||||
|
let XYWH(x, y, w, h) = to.area();
|
||||||
|
match self {
|
||||||
|
Self::X(c) => {
|
||||||
|
for x in x..x+w {
|
||||||
|
if let Some(cell) = to.buffer.cell_mut(Position::from((x, y))) {
|
||||||
|
cell.set_symbol(&c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Self::Y(c) => {
|
||||||
|
for y in y..y+h {
|
||||||
|
if let Some(cell) = to.buffer.cell_mut(Position::from((x, y))) {
|
||||||
|
cell.set_symbol(&c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Self::XY(c) => {
|
||||||
|
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.buffer.cell_mut(Position::from((x, y))) {
|
||||||
|
let u = u % a;
|
||||||
|
cell.set_symbol(&c[u..u+1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Draw<TuiOut> for Scrollbar {
|
||||||
|
fn draw (&self, to: &mut TuiOut) {
|
||||||
|
let XYWH(x1, y1, w, h) = to.area();
|
||||||
|
match self {
|
||||||
|
Self::X { .. } => {
|
||||||
|
let x2 = x1 + w;
|
||||||
|
for (i, x) in (x1..=x2).enumerate() {
|
||||||
|
if let Some(cell) = to.buffer.cell_mut(Position::from((x, y1))) {
|
||||||
|
if i < (Self::ICON_DEC_H.len()) {
|
||||||
|
cell.set_fg(Rgb(255, 255, 255));
|
||||||
|
cell.set_bg(Rgb(0, 0, 0));
|
||||||
|
cell.set_char(Self::ICON_DEC_H[i as usize]);
|
||||||
|
} else if i > (w as usize - Self::ICON_INC_H.len()) {
|
||||||
|
cell.set_fg(Rgb(255, 255, 255));
|
||||||
|
cell.set_bg(Rgb(0, 0, 0));
|
||||||
|
cell.set_char(Self::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('╌');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Self::Y { .. } => {
|
||||||
|
let y2 = y1 + h;
|
||||||
|
for (i, y) in (y1..=y2).enumerate() {
|
||||||
|
if let Some(cell) = to.buffer.cell_mut(Position::from((x1, y))) {
|
||||||
|
if (i as usize) < (Self::ICON_DEC_V.len()) {
|
||||||
|
cell.set_fg(Rgb(255, 255, 255));
|
||||||
|
cell.set_bg(Rgb(0, 0, 0));
|
||||||
|
cell.set_char(Self::ICON_DEC_V[i as usize]);
|
||||||
|
} else if (i as usize) > (h as usize - Self::ICON_INC_V.len()) {
|
||||||
|
cell.set_fg(Rgb(255, 255, 255));
|
||||||
|
cell.set_bg(Rgb(0, 0, 0));
|
||||||
|
cell.set_char(Self::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('╎'); // ━
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Draw<TuiOut> for &str {
|
||||||
|
fn draw (&self, to: &mut TuiOut) {
|
||||||
|
let XYWH(x, y, w, ..) = self.layout(to.area());
|
||||||
|
to.text(&self, x, y, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Draw<TuiOut> for String {
|
||||||
|
fn draw (&self, to: &mut TuiOut) {
|
||||||
|
self.as_str().draw(to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Draw<TuiOut> for Arc<str> {
|
||||||
|
fn draw (&self, to: &mut TuiOut) { self.as_ref().draw(to) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Content<TuiOut>> Draw<TuiOut> for Foreground<Color, T> {
|
||||||
|
fn draw (&self, to: &mut TuiOut) {
|
||||||
|
let area = self.layout(to.area());
|
||||||
|
to.fill_fg(area, self.0);
|
||||||
|
to.place_at(area, &self.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Content<TuiOut>> Draw<TuiOut> for Background<Color, T> {
|
||||||
|
fn draw (&self, to: &mut TuiOut) {
|
||||||
|
let area = self.layout(to.area());
|
||||||
|
to.fill_bg(area, self.0);
|
||||||
|
to.place_at(area, &self.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Content<TuiOut>> Draw<TuiOut> for Modify<T> {
|
||||||
|
fn draw (&self, to: &mut TuiOut) {
|
||||||
|
to.fill_mod(to.area(), self.0, self.1);
|
||||||
|
self.2.draw(to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Content<TuiOut>> Draw<TuiOut> for Styled<T> {
|
||||||
|
fn draw (&self, to: &mut TuiOut) {
|
||||||
|
to.place(&self.1);
|
||||||
|
// TODO write style over area
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: BorderStyle> Draw<TuiOut> for Border<S> {
|
||||||
|
fn draw (&self, to: &mut TuiOut) {
|
||||||
|
let Border(enabled, style) = self;
|
||||||
|
if *enabled {
|
||||||
|
let area = to.area();
|
||||||
|
if area.w() > 0 && area.y() > 0 {
|
||||||
|
to.blit(&style.border_nw(), area.x(), area.y(), style.style());
|
||||||
|
to.blit(&style.border_ne(), area.x() + area.w() - 1, area.y(), style.style());
|
||||||
|
to.blit(&style.border_sw(), area.x(), area.y() + area.h() - 1, style.style());
|
||||||
|
to.blit(&style.border_se(), area.x() + area.w() - 1, area.y() + area.h() - 1, style.style());
|
||||||
|
for x in area.x()+1..area.x()+area.w()-1 {
|
||||||
|
to.blit(&style.border_n(), x, area.y(), style.style());
|
||||||
|
to.blit(&style.border_s(), x, area.y() + area.h() - 1, style.style());
|
||||||
|
}
|
||||||
|
for y in area.y()+1..area.y()+area.h()-1 {
|
||||||
|
to.blit(&style.border_w(), area.x(), y, style.style());
|
||||||
|
to.blit(&style.border_e(), area.x() + area.w() - 1, y, style.style());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: AsRef<str>> Draw<TuiOut> for TrimString<T> {
|
||||||
|
fn draw (&self, to: &mut TuiOut) { Draw::draw(&self.as_ref(), to) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: AsRef<str>> Draw<TuiOut> for TrimStringRef<'_, T> {
|
||||||
|
fn draw (&self, target: &mut TuiOut) {
|
||||||
|
let area = target.area();
|
||||||
|
let mut width: u16 = 1;
|
||||||
|
let mut chars = self.1.as_ref().chars();
|
||||||
|
while let Some(c) = chars.next() {
|
||||||
|
if width > self.0 || width > area.w() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if let Some(cell) = target.buffer.cell_mut(Position {
|
||||||
|
x: area.x() + width - 1,
|
||||||
|
y: area.y()
|
||||||
|
}) {
|
||||||
|
cell.set_char(c);
|
||||||
|
}
|
||||||
|
width += c.width().unwrap_or(0) as u16;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TUI helper defs.
|
||||||
|
impl Tui {
|
||||||
|
pub const fn fg <T> (color: Color, w: T) -> Foreground<Color, T> { Foreground(color, w) }
|
||||||
|
pub const fn bg <T> (color: Color, w: T) -> Background<Color, T> { Background(color, w) }
|
||||||
|
pub const fn fg_bg <T> (fg: Color, bg: Color, w: T) -> Background<Color, Foreground<Color, T>> { Background(bg, Foreground(fg, w)) }
|
||||||
|
pub const fn modify <T> (enable: bool, modifier: Modifier, w: T) -> Modify<T> { Modify(enable, modifier, w) }
|
||||||
|
pub const fn bold <T> (enable: bool, w: T) -> Modify<T> { Self::modify(enable, Modifier::BOLD, w) }
|
||||||
|
pub const fn border <S, T> (enable: bool, style: S, w: T) -> Bordered<S, T> { Bordered(enable, style, w) }
|
||||||
|
|
||||||
|
pub const fn null () -> Color { Color::Reset }
|
||||||
|
pub const fn red () -> Color { Color::Rgb(255,0, 0) }
|
||||||
|
pub const fn orange () -> Color { Color::Rgb(255,128,0) }
|
||||||
|
pub const fn yellow () -> Color { Color::Rgb(255,255,0) }
|
||||||
|
pub const fn brown () -> Color { Color::Rgb(128,255,0) }
|
||||||
|
pub const fn green () -> Color { Color::Rgb(0,255,0) }
|
||||||
|
pub const fn electric () -> Color { Color::Rgb(0,255,128) }
|
||||||
|
pub const fn g (g: u8) -> Color { Color::Rgb(g, g, g) }
|
||||||
|
//fn bg0 () -> Color { Color::Rgb(20, 20, 20) }
|
||||||
|
//fn bg () -> Color { Color::Rgb(28, 35, 25) }
|
||||||
|
//fn border_bg () -> Color { Color::Rgb(40, 50, 30) }
|
||||||
|
//fn border_fg (f: bool) -> Color { if f { Self::bo1() } else { Self::bo2() } }
|
||||||
|
//fn title_fg (f: bool) -> Color { if f { Self::ti1() } else { Self::ti2() } }
|
||||||
|
//fn separator_fg (_: bool) -> Color { Color::Rgb(0, 0, 0) }
|
||||||
|
//fn mode_bg () -> Color { Color::Rgb(150, 160, 90) }
|
||||||
|
//fn mode_fg () -> Color { Color::Rgb(255, 255, 255) }
|
||||||
|
//fn status_bar_bg () -> Color { Color::Rgb(28, 35, 25) }
|
||||||
|
//fn bo1 () -> Color { Color::Rgb(100, 110, 40) }
|
||||||
|
//fn bo2 () -> Color { Color::Rgb(70, 80, 50) }
|
||||||
|
//fn ti1 () -> Color { Color::Rgb(150, 160, 90) }
|
||||||
|
//fn ti2 () -> Color { Color::Rgb(120, 130, 100) }
|
||||||
|
}
|
||||||
|
|
||||||
|
from!(BigBuffer: |size:(usize, usize)| Self::new(size.0, size.1));
|
||||||
|
from!(ItemTheme: |base: Color| Self::from_tui_color(base));
|
||||||
|
from!(ItemTheme: |base: ItemColor| Self::from_item_color(base));
|
||||||
|
from!(ItemColor: |okhsl: Okhsl<f32>| Self { okhsl, rgb: okhsl_to_rgb(okhsl) });
|
||||||
|
from!(ItemColor: |rgb: Color| Self { rgb, okhsl: rgb_to_okhsl(rgb) });
|
||||||
|
impl_debug!(BigBuffer |self, f| {
|
||||||
|
write!(f, "[BB {}x{} ({})]", self.width, self.height, self.content.len())
|
||||||
|
});
|
||||||
|
|
@ -1,10 +1,88 @@
|
||||||
#[cfg(test)] use proptest_derive::Arbitrary;
|
#[cfg(test)] use proptest_derive::Arbitrary;
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
|
/// The `Tui` struct (the *engine*) implements the
|
||||||
|
/// `tengri_input::Input` and `tengri_output::Out` traits.
|
||||||
|
/// At launch, the `Tui` engine spawns two threads, the render thread and the input thread.
|
||||||
|
/// the application may further spawn other threads. All threads communicate using shared ownership:
|
||||||
|
/// `Arc<RwLock<T>>` and `Arc<AtomicT>`. Thus, at launch the engine and application instances are expected to be wrapped in `Arc<RwLock>`.
|
||||||
|
pub struct Tui {
|
||||||
|
pub exited: Arc<AtomicBool>,
|
||||||
|
pub backend: CrosstermBackend<Stdout>,
|
||||||
|
pub buffer: Buffer,
|
||||||
|
pub area: [u16;4],
|
||||||
|
pub perf: PerfModel,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)] pub struct TuiIn {
|
||||||
|
/// Input event
|
||||||
|
pub event: TuiEvent,
|
||||||
|
/// Exit flag
|
||||||
|
pub exited: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)] pub struct TuiEvent(
|
||||||
|
pub Event
|
||||||
|
);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)] pub struct TuiKey(
|
||||||
|
pub Option<KeyCode>,
|
||||||
|
pub KeyModifiers
|
||||||
|
);
|
||||||
|
|
||||||
|
#[derive(Default)] pub struct TuiOut {
|
||||||
|
pub buffer: Buffer,
|
||||||
|
pub area: XYWH<u16>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TUI buffer sized by `usize` instead of `u16`.
|
||||||
|
#[derive(Default)] pub struct BigBuffer {
|
||||||
|
pub width: usize,
|
||||||
|
pub height: usize,
|
||||||
|
pub content: Vec<Cell>
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A color in OKHSL and RGB representations.
|
||||||
|
#[derive(Debug, Default, Copy, Clone, PartialEq)] pub struct ItemColor {
|
||||||
|
pub okhsl: Okhsl<f32>,
|
||||||
|
pub rgb: Color,
|
||||||
|
}
|
||||||
|
/// A color in OKHSL and RGB with lighter and darker variants.
|
||||||
|
#[derive(Debug, Default, Copy, Clone, PartialEq)] pub struct ItemTheme {
|
||||||
|
pub base: ItemColor,
|
||||||
|
pub light: ItemColor,
|
||||||
|
pub lighter: ItemColor,
|
||||||
|
pub lightest: ItemColor,
|
||||||
|
pub dark: ItemColor,
|
||||||
|
pub darker: ItemColor,
|
||||||
|
pub darkest: ItemColor,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Modify<T>(pub bool, pub Modifier, pub T);
|
||||||
|
|
||||||
|
pub struct Styled<T>(pub Option<Style>, pub T);
|
||||||
|
|
||||||
|
/// Displays an owned [str]-like with fixed maximum width.
|
||||||
|
///
|
||||||
|
/// Width is computed using [unicode_width].
|
||||||
|
pub struct TrimString<T: AsRef<str>>(pub u16, pub T);
|
||||||
|
|
||||||
|
/// Displays a borrowed [str]-like with fixed maximum width
|
||||||
|
///
|
||||||
|
/// Width is computed using [unicode_width].
|
||||||
|
pub struct TrimStringRef<'a, T: AsRef<str>>(pub u16, pub &'a T);
|
||||||
|
|
||||||
|
/// Thunks can be natural error boundaries!
|
||||||
|
pub struct ErrorBoundary<O: Out, T: Draw<O>>(
|
||||||
|
pub std::marker::PhantomData<O>,
|
||||||
|
pub Perhaps<T>
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
/// A point (X, Y).
|
/// A point (X, Y).
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// let xy = tengri::output::XY(0u16, 0);
|
/// let xy = tengri::XY(0u16, 0);
|
||||||
/// ```
|
/// ```
|
||||||
#[cfg_attr(test, derive(Arbitrary))]
|
#[cfg_attr(test, derive(Arbitrary))]
|
||||||
#[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct XY<C: Coord>(
|
#[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct XY<C: Coord>(
|
||||||
|
|
@ -14,7 +92,7 @@ use crate::*;
|
||||||
/// A size (Width, Height).
|
/// A size (Width, Height).
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// let wh = tengri::output::WH(0u16, 0);
|
/// let wh = tengri::WH(0u16, 0);
|
||||||
/// ```
|
/// ```
|
||||||
#[cfg_attr(test, derive(Arbitrary))]
|
#[cfg_attr(test, derive(Arbitrary))]
|
||||||
#[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct WH<C: Coord>(
|
#[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct WH<C: Coord>(
|
||||||
|
|
@ -24,7 +102,8 @@ use crate::*;
|
||||||
/// Point with size.
|
/// Point with size.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// let xywh = tengri::output::XYWH(0u16, 0, 0, 0);
|
/// let xywh = tengri::XYWH(0u16, 0, 0, 0);
|
||||||
|
/// assert_eq!(XYWH(10u16, 10, 20, 20).center(), XY(20, 20));
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// * [ ] TODO: anchor field (determines at which corner/side is X0 Y0)
|
/// * [ ] TODO: anchor field (determines at which corner/side is X0 Y0)
|
||||||
|
|
@ -37,7 +116,7 @@ use crate::*;
|
||||||
/// A cardinal direction.
|
/// A cardinal direction.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// let direction = tengri::output::Direction::Above;
|
/// let direction = tengri::Direction::Above;
|
||||||
/// ```
|
/// ```
|
||||||
#[cfg_attr(test, derive(Arbitrary))]
|
#[cfg_attr(test, derive(Arbitrary))]
|
||||||
#[derive(Copy, Clone, PartialEq, Debug)] pub enum Direction {
|
#[derive(Copy, Clone, PartialEq, Debug)] pub enum Direction {
|
||||||
|
|
@ -47,7 +126,7 @@ use crate::*;
|
||||||
/// 9th of area to place.
|
/// 9th of area to place.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// let alignment = tengri::output::Alignment::Center;
|
/// let alignment = tengri::Alignment::Center;
|
||||||
/// ```
|
/// ```
|
||||||
#[cfg_attr(test, derive(Arbitrary))]
|
#[cfg_attr(test, derive(Arbitrary))]
|
||||||
#[derive(Debug, Copy, Clone, Default)] pub enum Alignment {
|
#[derive(Debug, Copy, Clone, Default)] pub enum Alignment {
|
||||||
|
|
@ -57,7 +136,7 @@ use crate::*;
|
||||||
/// A widget that tracks its rendered width and height.
|
/// A widget that tracks its rendered width and height.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// let measure = tengri::output::Measure::<tengri::tui::TuiOut>::default();
|
/// let measure = tengri::Measure::<tengri::tui::TuiOut>::default();
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Default)] pub struct Measure<O: Out> {
|
#[derive(Default)] pub struct Measure<O: Out> {
|
||||||
pub __: PhantomData<O>,
|
pub __: PhantomData<O>,
|
||||||
|
|
@ -68,8 +147,8 @@ use crate::*;
|
||||||
/// Show an item only when a condition is true.
|
/// Show an item only when a condition is true.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// fn test () -> impl tengri::output::Draw<tengri::tui::TuiOut> {
|
/// fn test () -> impl tengri::Draw<tengri::tui::TuiOut> {
|
||||||
/// tengri::output::when(true, "Yes")
|
/// tengri::when(true, "Yes")
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub struct When<O, T>(pub bool, pub T, pub PhantomData<O>);
|
pub struct When<O, T>(pub bool, pub T, pub PhantomData<O>);
|
||||||
|
|
@ -80,8 +159,8 @@ pub const fn when<O, T>(condition: bool, content: T) -> When<O, T> {
|
||||||
/// Show one item if a condition is true and another if the condition is false.
|
/// Show one item if a condition is true and another if the condition is false.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// fn test () -> impl tengri::output::Draw<tengri::tui::TuiOut> {
|
/// fn test () -> impl tengri::Draw<tengri::tui::TuiOut> {
|
||||||
/// tengri::output::either(true, "Yes", "No")
|
/// tengri::either(true, "Yes", "No")
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub struct Either<E, A, B>(pub bool, pub A, pub B, pub PhantomData<E>);
|
pub struct Either<E, A, B>(pub bool, pub A, pub B, pub PhantomData<E>);
|
||||||
|
|
@ -92,56 +171,56 @@ pub const fn either<E, A, B>(condition: bool, content_a: A, content_b: B) -> Eit
|
||||||
/// Increment X and/or Y coordinate.
|
/// Increment X and/or Y coordinate.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// let pushed = tengri::output::Push::XY(2, 2, "Hello");
|
/// let pushed = tengri::Push::XY(2, 2, "Hello");
|
||||||
/// ```
|
/// ```
|
||||||
pub enum Push<U, A> { X(U, A), Y(U, A), XY(U, U, A), }
|
pub enum Push<U, A> { X(U, A), Y(U, A), XY(U, U, A), }
|
||||||
|
|
||||||
/// Decrement X and/or Y coordinate.
|
/// Decrement X and/or Y coordinate.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// let pulled = tengri::output::Pull::XY(2, 2, "Hello");
|
/// let pulled = tengri::Pull::XY(2, 2, "Hello");
|
||||||
/// ```
|
/// ```
|
||||||
pub enum Pull<U, A> { X(U, A), Y(U, A), XY(U, U, A), }
|
pub enum Pull<U, A> { X(U, A), Y(U, A), XY(U, U, A), }
|
||||||
|
|
||||||
/// Set the content to fill the container.
|
/// Set the content to fill the container.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// let filled = tengri::output::Fill::XY("Hello");
|
/// let filled = tengri::Fill::XY("Hello");
|
||||||
/// ```
|
/// ```
|
||||||
pub enum Fill<A> { X(A), Y(A), XY(A) }
|
pub enum Fill<A> { X(A), Y(A), XY(A) }
|
||||||
|
|
||||||
/// Set fixed size for content.
|
/// Set fixed size for content.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// let fixed = tengri::output::Fixed::XY(3, 5, "Hello"); // 3x5
|
/// let fixed = tengri::Fixed::XY(3, 5, "Hello"); // 3x5
|
||||||
/// ```
|
/// ```
|
||||||
pub enum Fixed<U, A> { X(U, A), Y(U, A), XY(U, U, A), }
|
pub enum Fixed<U, A> { X(U, A), Y(U, A), XY(U, U, A), }
|
||||||
|
|
||||||
/// Set the maximum width and/or height of the content.
|
/// Set the maximum width and/or height of the content.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// let maximum = tengri::output::Min::XY(3, 5, "Hello"); // 3x1
|
/// let maximum = tengri::Min::XY(3, 5, "Hello"); // 3x1
|
||||||
/// ```
|
/// ```
|
||||||
pub enum Max<U, A> { X(U, A), Y(U, A), XY(U, U, A), }
|
pub enum Max<U, A> { X(U, A), Y(U, A), XY(U, U, A), }
|
||||||
|
|
||||||
/// Set the minimum width and/or height of the content.
|
/// Set the minimum width and/or height of the content.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// let minimam = tengri::output::Min::XY(3, 5, "Hello"); // 5x5
|
/// let minimam = tengri::Min::XY(3, 5, "Hello"); // 5x5
|
||||||
/// ```
|
/// ```
|
||||||
pub enum Min<U, A> { X(U, A), Y(U, A), XY(U, U, A), }
|
pub enum Min<U, A> { X(U, A), Y(U, A), XY(U, U, A), }
|
||||||
|
|
||||||
/// Decrease the width and/or height of the content.
|
/// Decrease the width and/or height of the content.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// let shrunk = tengri::output::Shrink::XY(2, 0, "Hello"); // 1x1
|
/// let shrunk = tengri::Shrink::XY(2, 0, "Hello"); // 1x1
|
||||||
/// ```
|
/// ```
|
||||||
pub enum Shrink<U, A> { X(U, A), Y(U, A), XY(U, U, A), }
|
pub enum Shrink<U, A> { X(U, A), Y(U, A), XY(U, U, A), }
|
||||||
|
|
||||||
/// Increaase the width and/or height of the content.
|
/// Increaase the width and/or height of the content.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// let expanded = tengri::output::Expand::XY(5, 3, "HELLO"); // 15x3
|
/// let expanded = tengri::Expand::XY(5, 3, "HELLO"); // 15x3
|
||||||
/// ```
|
/// ```
|
||||||
pub enum Expand<U, A> { X(U, A), Y(U, A), XY(U, U, A), }
|
pub enum Expand<U, A> { X(U, A), Y(U, A), XY(U, U, A), }
|
||||||
|
|
||||||
|
|
@ -184,7 +263,7 @@ pub enum Pad<U, A> { X(U, A), Y(U, A), XY(U, U, A), }
|
||||||
/// TODO DOCUMENTME
|
/// TODO DOCUMENTME
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use tengri::output::{Bounded, XYWH};
|
/// use tengri::{Bounded, XYWH};
|
||||||
/// let area = XYWH(0, 0, 0, 0);
|
/// let area = XYWH(0, 0, 0, 0);
|
||||||
/// let content = "";
|
/// let content = "";
|
||||||
/// let bounded: Bounded<tengri::tui::TuiOut, _> = Bounded(area, content);
|
/// let bounded: Bounded<tengri::tui::TuiOut, _> = Bounded(area, content);
|
||||||
|
|
@ -194,7 +273,7 @@ pub struct Bounded<O: Out, D>(pub XYWH<O::Unit>, pub D);
|
||||||
/// Draws items from an iterator.
|
/// Draws items from an iterator.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// // FIXME let map = tengri::output::Map(||[].iter(), |_|{});
|
/// // FIXME let map = tengri::Map(||[].iter(), |_|{});
|
||||||
/// ```
|
/// ```
|
||||||
pub struct Map<O, A, B, I, F, G>
|
pub struct Map<O, A, B, I, F, G>
|
||||||
where
|
where
|
||||||
|
|
@ -288,3 +367,27 @@ pub struct PerfModel {
|
||||||
// In microseconds. Max prescribed time for iteration (frame, chunk...).
|
// In microseconds. Max prescribed time for iteration (frame, chunk...).
|
||||||
pub window: AtomicF64,
|
pub window: AtomicF64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Repeat a string, e.g. for background
|
||||||
|
pub enum Repeat<'a> {
|
||||||
|
X(&'a str),
|
||||||
|
Y(&'a str),
|
||||||
|
XY(&'a str)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scroll indicator
|
||||||
|
pub enum Scrollbar {
|
||||||
|
/// Horizontal scrollbar
|
||||||
|
X { offset: usize, length: usize, total: usize, },
|
||||||
|
/// Vertical scrollbar
|
||||||
|
Y { offset: usize, length: usize, total: usize, }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A cell that takes up 3 rows on its own,
|
||||||
|
/// but stacks, giving (N+1)*2 rows per N cells.
|
||||||
|
pub struct Phat<T> {
|
||||||
|
pub width: u16,
|
||||||
|
pub height: u16,
|
||||||
|
pub content: T,
|
||||||
|
pub colors: [Color;4],
|
||||||
|
}
|
||||||
343
tengri/tengri_trait.rs
Normal file
343
tengri/tengri_trait.rs
Normal file
|
|
@ -0,0 +1,343 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Source of [Input::Event]s: keyboard, mouse...
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// use crate::*;
|
||||||
|
/// struct TestInput(bool);
|
||||||
|
/// enum TestEvent { Test1 }
|
||||||
|
/// impl Input for TestInput {
|
||||||
|
/// type Event = TestEvent;
|
||||||
|
/// type Handled = ();
|
||||||
|
/// fn event (&self) -> &Self::Event {
|
||||||
|
/// &TestEvent::Test1
|
||||||
|
/// }
|
||||||
|
/// fn is_done (&self) -> bool {
|
||||||
|
/// self.0
|
||||||
|
/// }
|
||||||
|
/// fn done (&self) {}
|
||||||
|
/// }
|
||||||
|
/// let _ = TestInput(true).event();
|
||||||
|
/// assert!(TestInput(true).is_done());
|
||||||
|
/// assert!(!TestInput(false).is_done());
|
||||||
|
/// Ok(())
|
||||||
|
/// ```
|
||||||
|
pub trait Input: Sized {
|
||||||
|
/// Type of input event
|
||||||
|
type Event;
|
||||||
|
/// Result of handling input
|
||||||
|
type Handled; // TODO: make this an Option<Box dyn Command<Self>> containing the undo
|
||||||
|
/// Currently handled event
|
||||||
|
fn event (&self) -> &Self::Event;
|
||||||
|
/// Whether component should exit
|
||||||
|
fn is_done (&self) -> bool;
|
||||||
|
/// Mark component as done
|
||||||
|
fn done (&self);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// State mutation.
|
||||||
|
pub trait Command<S>: Send + Sync + Sized {
|
||||||
|
fn execute (&self, state: &mut S) -> Perhaps<Self>;
|
||||||
|
fn delegate <T> (&self, state: &mut S, wrap: impl Fn(Self)->T) -> Perhaps<T>
|
||||||
|
where Self: Sized
|
||||||
|
{
|
||||||
|
Ok(self.execute(state)?.map(wrap))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drawing target.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use tengri::output::*;
|
||||||
|
/// struct TestOut(XYWH<u16>);
|
||||||
|
/// impl Out for TestOut {
|
||||||
|
/// type Unit = u16;
|
||||||
|
/// fn area (&self) -> XYWH<u16> { self.0 }
|
||||||
|
/// fn area_mut (&mut self) -> &mut XYWH<u16> { &mut self.0 }
|
||||||
|
/// fn place_at <T: Draw<Self> + ?Sized> (&mut self, area: XYWH<u16>, _: &T) {
|
||||||
|
/// println!("place_at: {area:?}");
|
||||||
|
/// ()
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// impl Draw<TestOut> for String {
|
||||||
|
/// fn draw (&self, to: &mut TestOut) {
|
||||||
|
/// //to.area_mut().set_w(self.len() as u16);
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub trait Out: Send + Sync + Sized {
|
||||||
|
/// Unit of length
|
||||||
|
type Unit: Coord;
|
||||||
|
/// Current output area
|
||||||
|
fn area (&self) -> XYWH<Self::Unit>;
|
||||||
|
/// Mutable pointer to area.
|
||||||
|
fn area_mut (&mut self) -> &mut XYWH<Self::Unit>;
|
||||||
|
/// Render drawable in area specified by `area`
|
||||||
|
fn place_at <'t, T: Draw<Self> + ?Sized> (&mut self, area: XYWH<Self::Unit>, content: &'t T);
|
||||||
|
/// Render drawable in area specified by `T::layout(self.area())`
|
||||||
|
#[inline] fn place <'t, T: Content<Self> + ?Sized> (&mut self, content: &'t T) {
|
||||||
|
self.place_at(content.layout(self.area()), content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A numeric type that can be used as coordinate.
|
||||||
|
///
|
||||||
|
/// FIXME: Replace this ad-hoc trait with `num` crate.
|
||||||
|
pub trait Coord: Send + Sync + Copy
|
||||||
|
+ Add<Self, Output=Self>
|
||||||
|
+ Sub<Self, Output=Self>
|
||||||
|
+ Mul<Self, Output=Self>
|
||||||
|
+ Div<Self, Output=Self>
|
||||||
|
+ Ord + PartialEq + Eq
|
||||||
|
+ Debug + Display + Default
|
||||||
|
+ From<u16> + Into<u16>
|
||||||
|
+ Into<usize>
|
||||||
|
+ Into<f64>
|
||||||
|
{
|
||||||
|
fn plus (self, other: Self) -> Self;
|
||||||
|
fn minus (self, other: Self) -> Self {
|
||||||
|
if self >= other { self - other } else { 0.into() }
|
||||||
|
}
|
||||||
|
fn atomic (self) -> AtomicUsize {
|
||||||
|
AtomicUsize::new(self.into())
|
||||||
|
}
|
||||||
|
fn zero () -> Self {
|
||||||
|
0.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drawable with dynamic dispatch.
|
||||||
|
pub trait Draw<O: Out> {
|
||||||
|
fn draw (&self, to: &mut O);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// FIXME: This is a general implementation: should be called `Eval` and be part of [dizzle].
|
||||||
|
/// Matches [Language] expressions to renderings for a given [Output] target.
|
||||||
|
pub trait View<O, U> {
|
||||||
|
fn view_expr <'a> (&'a self, _output: &mut O, expr: &'a impl Expression) -> Usually<U> {
|
||||||
|
Err(format!("View::view_expr: no exprs defined: {expr:?}").into())
|
||||||
|
}
|
||||||
|
fn view_word <'a> (&'a self, _output: &mut O, word: &'a impl Symbol) -> Usually<U> {
|
||||||
|
Err(format!("View::view_word: no words defined: {word:?}").into())
|
||||||
|
}
|
||||||
|
fn view <'a> (&'a self, output: &mut O, dsl: &'a impl Language) -> Usually<U> {
|
||||||
|
match (dsl.expr(), dsl.word()) {
|
||||||
|
(Ok(Some(e)), _ ) => self.view_expr(output, &e),
|
||||||
|
(_, Ok(Some(w))) => self.view_word(output, &w),
|
||||||
|
(Err(e), _ ) => Err(format!("invalid view expr:\n{dsl:?}\n{e}").into()),
|
||||||
|
(_, Err(w) ) => Err(format!("invalid view word:\n{dsl:?}\n{w}").into()),
|
||||||
|
(Ok(None), Ok(None) ) => Err(format!("empty view:\n{dsl:?}").into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Outputs combinator.
|
||||||
|
pub trait Lay<O: Out>: Sized {}
|
||||||
|
|
||||||
|
/// Drawable area of display.
|
||||||
|
pub trait Layout<O: Out> {
|
||||||
|
fn layout_x (&self, to: XYWH<O::Unit>) -> O::Unit { to.x() }
|
||||||
|
fn layout_y (&self, to: XYWH<O::Unit>) -> O::Unit { to.y() }
|
||||||
|
fn layout_w_min (&self, _t: XYWH<O::Unit>) -> O::Unit { 0.into() }
|
||||||
|
fn layout_w_max (&self, to: XYWH<O::Unit>) -> O::Unit { to.w() }
|
||||||
|
fn layout_w (&self, to: XYWH<O::Unit>) -> O::Unit { to.w().max(self.layout_w_min(to)).min(self.layout_w_max(to)) }
|
||||||
|
fn layout_h_min (&self, _t: XYWH<O::Unit>) -> O::Unit { 0.into() }
|
||||||
|
fn layout_h_max (&self, to: XYWH<O::Unit>) -> O::Unit { to.h() }
|
||||||
|
fn layout_h (&self, to: XYWH<O::Unit>) -> O::Unit { to.h().max(self.layout_h_min(to)).min(self.layout_h_max(to)) }
|
||||||
|
fn layout (&self, to: XYWH<O::Unit>) -> XYWH<O::Unit> {
|
||||||
|
XYWH(self.layout_x(to), self.layout_y(to), self.layout_w(to), self.layout_h(to))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait HasContent<O: Out> {
|
||||||
|
fn content (&self) -> impl Content<O>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO DOCUMENTME
|
||||||
|
pub trait Content<O: Out>: Draw<O> + Layout<O> {}
|
||||||
|
|
||||||
|
// Something that has an origin point (X, Y).
|
||||||
|
pub trait HasXY<N: Coord> {
|
||||||
|
fn x (&self) -> N;
|
||||||
|
fn y (&self) -> N;
|
||||||
|
fn xy (&self) -> XY<N> { XY(self.x(), self.y()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Something that has a size (W, H).
|
||||||
|
pub trait HasWH<N: Coord> {
|
||||||
|
fn w (&self) -> N;
|
||||||
|
fn h (&self) -> N;
|
||||||
|
fn wh (&self) -> WH<N> { WH(self.w(), self.h()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Something that has a 2D bounding box (X, Y, W, H).
|
||||||
|
//
|
||||||
|
// FIXME: The other way around?
|
||||||
|
pub trait HasXYWH<N: Coord>: HasXY<N> + HasWH<N> {
|
||||||
|
fn x2 (&self) -> N { self.x().plus(self.w()) }
|
||||||
|
fn y2 (&self) -> N { self.y().plus(self.h()) }
|
||||||
|
fn xywh (&self) -> XYWH<N> { XYWH(self.x(), self.y(), self.w(), self.h()) }
|
||||||
|
fn expect_min (&self, w: N, h: N) -> Usually<&Self> {
|
||||||
|
if self.w() < w || self.h() < h {
|
||||||
|
Err(format!("min {w}x{h}").into())
|
||||||
|
} else {
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Something that has a [Measure] of its rendered size.
|
||||||
|
pub trait Measured<O: Out> {
|
||||||
|
fn measure (&self) -> &Measure<O>;
|
||||||
|
fn measure_width (&self) -> O::Unit { self.measure().w() }
|
||||||
|
fn measure_height (&self) -> O::Unit { self.measure().h() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait HasPerf {
|
||||||
|
fn perf (&self) -> &PerfModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait TuiDraw = Draw<TuiOut>;
|
||||||
|
|
||||||
|
pub trait TuiLayout = Layout<TuiOut>;
|
||||||
|
|
||||||
|
pub trait TuiContent = Content<TuiOut>;
|
||||||
|
|
||||||
|
pub trait TuiHandle = Handle<TuiIn>;
|
||||||
|
|
||||||
|
pub trait TuiWidget = TuiDraw + TuiHandle;
|
||||||
|
|
||||||
|
pub trait HasColor { fn color (&self) -> ItemColor; }
|
||||||
|
|
||||||
|
pub trait BorderStyle: Content<TuiOut> + Copy {
|
||||||
|
fn enabled (&self) -> bool;
|
||||||
|
fn enclose (self, w: impl Content<TuiOut>) -> impl Content<TuiOut> {
|
||||||
|
Bsp::b(Fill::XY(Border(self.enabled(), self)), w)
|
||||||
|
}
|
||||||
|
fn enclose2 (self, w: impl Content<TuiOut>) -> impl Content<TuiOut> {
|
||||||
|
Bsp::b(Pad::XY(1, 1, Fill::XY(Border(self.enabled(), self))), w)
|
||||||
|
}
|
||||||
|
fn enclose_bg (self, w: impl Content<TuiOut>) -> impl Content<TuiOut> {
|
||||||
|
Tui::bg(self.style().unwrap().bg.unwrap_or(Color::Reset),
|
||||||
|
Bsp::b(Fill::XY(Border(self.enabled(), self)), w))
|
||||||
|
}
|
||||||
|
const NW: &'static str = "";
|
||||||
|
const N: &'static str = "";
|
||||||
|
const NE: &'static str = "";
|
||||||
|
const E: &'static str = "";
|
||||||
|
const SE: &'static str = "";
|
||||||
|
const S: &'static str = "";
|
||||||
|
const SW: &'static str = "";
|
||||||
|
const W: &'static str = "";
|
||||||
|
|
||||||
|
const N0: &'static str = "";
|
||||||
|
const S0: &'static str = "";
|
||||||
|
const W0: &'static str = "";
|
||||||
|
const E0: &'static str = "";
|
||||||
|
|
||||||
|
fn border_n (&self) -> &str { Self::N }
|
||||||
|
fn border_s (&self) -> &str { Self::S }
|
||||||
|
fn border_e (&self) -> &str { Self::E }
|
||||||
|
fn border_w (&self) -> &str { Self::W }
|
||||||
|
fn border_nw (&self) -> &str { Self::NW }
|
||||||
|
fn border_ne (&self) -> &str { Self::NE }
|
||||||
|
fn border_sw (&self) -> &str { Self::SW }
|
||||||
|
fn border_se (&self) -> &str { Self::SE }
|
||||||
|
#[inline] fn draw <'a> (
|
||||||
|
&self, to: &mut TuiOut
|
||||||
|
) -> Usually<()> {
|
||||||
|
if self.enabled() {
|
||||||
|
self.draw_horizontal(to, None)?;
|
||||||
|
self.draw_vertical(to, None)?;
|
||||||
|
self.draw_corners(to, None)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[inline] fn draw_horizontal (
|
||||||
|
&self, to: &mut TuiOut, style: Option<Style>
|
||||||
|
) -> Usually<XYWH<u16>> {
|
||||||
|
let area = to.area();
|
||||||
|
let style = style.or_else(||self.style_horizontal());
|
||||||
|
let [x, x2, y, y2] = area.lrtb();
|
||||||
|
for x in x..x2.saturating_sub(1) {
|
||||||
|
to.blit(&Self::N, x, y, style);
|
||||||
|
to.blit(&Self::S, x, y2.saturating_sub(1), style)
|
||||||
|
}
|
||||||
|
Ok(area)
|
||||||
|
}
|
||||||
|
#[inline] fn draw_vertical (
|
||||||
|
&self, to: &mut TuiOut, style: Option<Style>
|
||||||
|
) -> Usually<XYWH<u16>> {
|
||||||
|
let area = to.area();
|
||||||
|
let style = style.or_else(||self.style_vertical());
|
||||||
|
let [x, x2, y, y2] = area.lrtb();
|
||||||
|
let h = y2 - y;
|
||||||
|
if h > 1 {
|
||||||
|
for y in y..y2.saturating_sub(1) {
|
||||||
|
to.blit(&Self::W, x, y, style);
|
||||||
|
to.blit(&Self::E, x2.saturating_sub(1), y, style);
|
||||||
|
}
|
||||||
|
} else if h > 0 {
|
||||||
|
to.blit(&Self::W0, x, y, style);
|
||||||
|
to.blit(&Self::E0, x2.saturating_sub(1), y, style);
|
||||||
|
}
|
||||||
|
Ok(area)
|
||||||
|
}
|
||||||
|
#[inline] fn draw_corners (
|
||||||
|
&self, to: &mut TuiOut, style: Option<Style>
|
||||||
|
) -> Usually<XYWH<u16>> {
|
||||||
|
let area = to.area();
|
||||||
|
let style = style.or_else(||self.style_corners());
|
||||||
|
let XYWH(x, y, width, height) = area;
|
||||||
|
if width > 1 && height > 1 {
|
||||||
|
to.blit(&Self::NW, x, y, style);
|
||||||
|
to.blit(&Self::NE, x + width - 1, y, style);
|
||||||
|
to.blit(&Self::SW, x, y + height - 1, style);
|
||||||
|
to.blit(&Self::SE, x + width - 1, y + height - 1, style);
|
||||||
|
}
|
||||||
|
Ok(area)
|
||||||
|
}
|
||||||
|
#[inline] fn style (&self) -> Option<Style> { None }
|
||||||
|
#[inline] fn style_horizontal (&self) -> Option<Style> { self.style() }
|
||||||
|
#[inline] fn style_vertical (&self) -> Option<Style> { self.style() }
|
||||||
|
#[inline] fn style_corners (&self) -> Option<Style> { self.style() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Define a trait an implement it for various mutation-enabled wrapper types. */
|
||||||
|
#[macro_export] macro_rules! flex_trait_mut (
|
||||||
|
($Trait:ident $(<$($A:ident:$T:ident),+>)? {
|
||||||
|
$(fn $fn:ident (&mut $self:ident $(, $arg:ident:$ty:ty)*) -> $ret:ty $body:block)*
|
||||||
|
})=>{
|
||||||
|
pub trait $Trait $(<$($A: $T),+>)? {
|
||||||
|
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret $body)*
|
||||||
|
}
|
||||||
|
impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for &mut _T_ {
|
||||||
|
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { (*$self).$fn($($arg),*) })*
|
||||||
|
}
|
||||||
|
impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for Option<_T_> {
|
||||||
|
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret {
|
||||||
|
if let Some(this) = $self { this.$fn($($arg),*) } else { Ok(None) }
|
||||||
|
})*
|
||||||
|
}
|
||||||
|
impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::Mutex<_T_> {
|
||||||
|
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { $self.get_mut().unwrap().$fn($($arg),*) })*
|
||||||
|
}
|
||||||
|
impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::Arc<::std::sync::Mutex<_T_>> {
|
||||||
|
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { $self.lock().unwrap().$fn($($arg),*) })*
|
||||||
|
}
|
||||||
|
impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::RwLock<_T_> {
|
||||||
|
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { $self.write().unwrap().$fn($($arg),*) })*
|
||||||
|
}
|
||||||
|
impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::Arc<::std::sync::RwLock<_T_>> {
|
||||||
|
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { $self.write().unwrap().$fn($($arg),*) })*
|
||||||
|
}
|
||||||
|
};
|
||||||
|
);
|
||||||
|
|
||||||
|
flex_trait_mut!(Handle <E: Input> {
|
||||||
|
fn handle (&mut self, _input: &E) -> Perhaps<E::Handled> {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
0
tengri/tengri_type.rs
Normal file
0
tengri/tengri_type.rs
Normal file
|
|
@ -14,15 +14,6 @@ dizzle = { workspace = true }
|
||||||
tengri_input = { workspace = true }
|
tengri_input = { workspace = true }
|
||||||
tengri_output = { workspace = true }
|
tengri_output = { workspace = true }
|
||||||
|
|
||||||
atomic_float = { workspace = true }
|
|
||||||
better-panic = { workspace = true }
|
|
||||||
bumpalo = { workspace = true, optional = true }
|
|
||||||
crossterm = { workspace = true }
|
|
||||||
palette = { workspace = true }
|
|
||||||
quanta = { workspace = true }
|
|
||||||
rand = { workspace = true }
|
|
||||||
ratatui = { workspace = true }
|
|
||||||
unicode-width = { workspace = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tengri = { workspace = true, features = [ "dsl" ] }
|
tengri = { workspace = true, features = [ "dsl" ] }
|
||||||
|
|
|
||||||
|
|
@ -1,434 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum FocusState<T: Copy + Debug + PartialEq> {
|
|
||||||
Focused(T),
|
|
||||||
Entered(T),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Copy + Debug + PartialEq> FocusState<T> {
|
|
||||||
pub fn inner (&self) -> T {
|
|
||||||
match self {
|
|
||||||
Self::Focused(inner) => *inner,
|
|
||||||
Self::Entered(inner) => *inner,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn set_inner (&mut self, inner: T) {
|
|
||||||
*self = match self {
|
|
||||||
Self::Focused(_) => Self::Focused(inner),
|
|
||||||
Self::Entered(_) => Self::Entered(inner),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn is_focused (&self) -> bool { matches!(self, Self::Focused(_)) }
|
|
||||||
pub fn is_entered (&self) -> bool { matches!(self, Self::Entered(_)) }
|
|
||||||
pub fn focus (&mut self) { *self = Self::Focused(self.inner()) }
|
|
||||||
pub fn enter (&mut self) { *self = Self::Entered(self.inner()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Debug)]
|
|
||||||
pub enum FocusCommand<T: Send + Sync> {
|
|
||||||
Up,
|
|
||||||
Down,
|
|
||||||
Left,
|
|
||||||
Right,
|
|
||||||
Next,
|
|
||||||
Prev,
|
|
||||||
Enter,
|
|
||||||
Exit,
|
|
||||||
Set(T)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<F: HasFocus + HasEnter + FocusGrid + FocusOrder> Command<F> for FocusCommand<F::Item> {
|
|
||||||
fn execute (self, state: &mut F) -> Perhaps<FocusCommand<F::Item>> {
|
|
||||||
match self {
|
|
||||||
Self::Next => { state.focus_next(); },
|
|
||||||
Self::Prev => { state.focus_prev(); },
|
|
||||||
Self::Up => { state.focus_up(); },
|
|
||||||
Self::Down => { state.focus_down(); },
|
|
||||||
Self::Left => { state.focus_left(); },
|
|
||||||
Self::Right => { state.focus_right(); },
|
|
||||||
Self::Enter => { state.focus_enter(); },
|
|
||||||
Self::Exit => { state.focus_exit(); },
|
|
||||||
Self::Set(to) => { state.set_focused(to); },
|
|
||||||
}
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Trait for things that have focusable subparts.
|
|
||||||
pub trait HasFocus {
|
|
||||||
type Item: Copy + PartialEq + Debug + Send + Sync;
|
|
||||||
/// Get the currently focused item.
|
|
||||||
fn focused (&self) -> Self::Item;
|
|
||||||
/// Get the currently focused item.
|
|
||||||
fn set_focused (&mut self, to: Self::Item);
|
|
||||||
/// Loop forward until a specific item is focused.
|
|
||||||
fn focus_to (&mut self, to: Self::Item) {
|
|
||||||
self.set_focused(to);
|
|
||||||
self.focus_updated();
|
|
||||||
}
|
|
||||||
/// Run this on focus update
|
|
||||||
fn focus_updated (&mut self) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Trait for things that have enterable subparts.
|
|
||||||
pub trait HasEnter: HasFocus {
|
|
||||||
/// Get the currently focused item.
|
|
||||||
fn entered (&self) -> bool;
|
|
||||||
/// Get the currently focused item.
|
|
||||||
fn set_entered (&mut self, entered: bool);
|
|
||||||
/// Enter into the currently focused component
|
|
||||||
fn focus_enter (&mut self) {
|
|
||||||
self.set_entered(true);
|
|
||||||
self.focus_updated();
|
|
||||||
}
|
|
||||||
/// Exit the currently entered component
|
|
||||||
fn focus_exit (&mut self) {
|
|
||||||
self.set_entered(false);
|
|
||||||
self.focus_updated();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Trait for things that implement directional navigation between focusable elements.
|
|
||||||
pub trait FocusGrid: HasFocus {
|
|
||||||
fn focus_layout (&self) -> &[&[Self::Item]];
|
|
||||||
fn focus_cursor (&self) -> (usize, usize);
|
|
||||||
fn focus_cursor_mut (&mut self) -> &mut (usize, usize);
|
|
||||||
fn focus_current (&self) -> Self::Item {
|
|
||||||
let (x, y) = self.focus_cursor();
|
|
||||||
self.focus_layout()[y][x]
|
|
||||||
}
|
|
||||||
fn focus_update (&mut self) {
|
|
||||||
self.focus_to(self.focus_current());
|
|
||||||
self.focus_updated()
|
|
||||||
}
|
|
||||||
fn focus_up (&mut self) {
|
|
||||||
let original_focused = self.focused();
|
|
||||||
let (_, original_y) = self.focus_cursor();
|
|
||||||
loop {
|
|
||||||
let (x, y) = self.focus_cursor();
|
|
||||||
let next_y = if y == 0 {
|
|
||||||
self.focus_layout().len().saturating_sub(1)
|
|
||||||
} else {
|
|
||||||
y - 1
|
|
||||||
};
|
|
||||||
if next_y == original_y {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
let next_x = if self.focus_layout()[y].len() == self.focus_layout()[next_y].len() {
|
|
||||||
x
|
|
||||||
} else {
|
|
||||||
((x as f32 / self.focus_layout()[original_y].len() as f32)
|
|
||||||
* self.focus_layout()[next_y].len() as f32) as usize
|
|
||||||
};
|
|
||||||
*self.focus_cursor_mut() = (next_x, next_y);
|
|
||||||
if self.focus_current() != original_focused {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.focus_update();
|
|
||||||
}
|
|
||||||
fn focus_down (&mut self) {
|
|
||||||
let original_focused = self.focused();
|
|
||||||
let (_, original_y) = self.focus_cursor();
|
|
||||||
loop {
|
|
||||||
let (x, y) = self.focus_cursor();
|
|
||||||
let next_y = if y >= self.focus_layout().len().saturating_sub(1) {
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
y + 1
|
|
||||||
};
|
|
||||||
if next_y == original_y {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
let next_x = if self.focus_layout()[y].len() == self.focus_layout()[next_y].len() {
|
|
||||||
x
|
|
||||||
} else {
|
|
||||||
((x as f32 / self.focus_layout()[original_y].len() as f32)
|
|
||||||
* self.focus_layout()[next_y].len() as f32) as usize
|
|
||||||
};
|
|
||||||
*self.focus_cursor_mut() = (next_x, next_y);
|
|
||||||
if self.focus_current() != original_focused {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.focus_update();
|
|
||||||
}
|
|
||||||
fn focus_left (&mut self) {
|
|
||||||
let original_focused = self.focused();
|
|
||||||
let (original_x, y) = self.focus_cursor();
|
|
||||||
loop {
|
|
||||||
let x = self.focus_cursor().0;
|
|
||||||
let next_x = if x == 0 {
|
|
||||||
self.focus_layout()[y].len().saturating_sub(1)
|
|
||||||
} else {
|
|
||||||
x - 1
|
|
||||||
};
|
|
||||||
if next_x == original_x {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
*self.focus_cursor_mut() = (next_x, y);
|
|
||||||
if self.focus_current() != original_focused {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.focus_update();
|
|
||||||
}
|
|
||||||
fn focus_right (&mut self) {
|
|
||||||
let original_focused = self.focused();
|
|
||||||
let (original_x, y) = self.focus_cursor();
|
|
||||||
loop {
|
|
||||||
let x = self.focus_cursor().0;
|
|
||||||
let next_x = if x >= self.focus_layout()[y].len().saturating_sub(1) {
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
x + 1
|
|
||||||
};
|
|
||||||
if next_x == original_x {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
self.focus_cursor_mut().0 = next_x;
|
|
||||||
if self.focus_current() != original_focused {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.focus_update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Trait for things that implement next/prev navigation between focusable elements.
|
|
||||||
pub trait FocusOrder {
|
|
||||||
/// Focus the next item.
|
|
||||||
fn focus_next (&mut self);
|
|
||||||
/// Focus the previous item.
|
|
||||||
fn focus_prev (&mut self);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Next/prev navigation for directional focusables works in the given way.
|
|
||||||
impl<T: FocusGrid + HasEnter> FocusOrder for T {
|
|
||||||
/// Focus the next item.
|
|
||||||
fn focus_next (&mut self) {
|
|
||||||
let current = self.focused();
|
|
||||||
let (x, y) = self.focus_cursor();
|
|
||||||
if x < self.focus_layout()[y].len().saturating_sub(1) {
|
|
||||||
self.focus_right();
|
|
||||||
} else {
|
|
||||||
self.focus_down();
|
|
||||||
self.focus_cursor_mut().0 = 0;
|
|
||||||
}
|
|
||||||
if self.focused() == current { // FIXME: prevent infinite loop
|
|
||||||
self.focus_next()
|
|
||||||
}
|
|
||||||
self.focus_exit();
|
|
||||||
self.focus_update();
|
|
||||||
}
|
|
||||||
/// Focus the previous item.
|
|
||||||
fn focus_prev (&mut self) {
|
|
||||||
let current = self.focused();
|
|
||||||
let (x, _) = self.focus_cursor();
|
|
||||||
if x > 0 {
|
|
||||||
self.focus_left();
|
|
||||||
} else {
|
|
||||||
self.focus_up();
|
|
||||||
let (_, y) = self.focus_cursor();
|
|
||||||
let next_x = self.focus_layout()[y].len().saturating_sub(1);
|
|
||||||
self.focus_cursor_mut().0 = next_x;
|
|
||||||
}
|
|
||||||
if self.focused() == current { // FIXME: prevent infinite loop
|
|
||||||
self.focus_prev()
|
|
||||||
}
|
|
||||||
self.focus_exit();
|
|
||||||
self.focus_update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait FocusWrap<T> {
|
|
||||||
fn wrap <W: Content<TuiOut>> (self, focus: T, content: &'_ W) -> impl Draw<TuiOut> + '_;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_focus_command <T: Send + Sync> (input: &TuiIn) -> Option<FocusCommand<T>> {
|
|
||||||
Some(match input.event() {
|
|
||||||
kpat!(Tab) => FocusCommand::Next,
|
|
||||||
kpat!(Shift-Tab) => FocusCommand::Prev,
|
|
||||||
kpat!(BackTab) => FocusCommand::Prev,
|
|
||||||
kpat!(Shift-BackTab) => FocusCommand::Prev,
|
|
||||||
kpat!(Up) => FocusCommand::Up,
|
|
||||||
kpat!(Down) => FocusCommand::Down,
|
|
||||||
kpat!(Left) => FocusCommand::Left,
|
|
||||||
kpat!(Right) => FocusCommand::Right,
|
|
||||||
kpat!(Enter) => FocusCommand::Enter,
|
|
||||||
kpat!(Esc) => FocusCommand::Exit,
|
|
||||||
_ => return None
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export] macro_rules! impl_focus {
|
|
||||||
($Struct:ident $Focus:ident $Grid:expr $(=> [$self:ident : $update_focus:expr])?) => {
|
|
||||||
impl HasFocus for $Struct {
|
|
||||||
type Item = $Focus;
|
|
||||||
/// Get the currently focused item.
|
|
||||||
fn focused (&self) -> Self::Item {
|
|
||||||
self.focus.inner()
|
|
||||||
}
|
|
||||||
/// Get the currently focused item.
|
|
||||||
fn set_focused (&mut self, to: Self::Item) {
|
|
||||||
self.focus.set_inner(to)
|
|
||||||
}
|
|
||||||
$(fn focus_updated (&mut $self) { $update_focus })?
|
|
||||||
}
|
|
||||||
impl HasEnter for $Struct {
|
|
||||||
/// Get the currently focused item.
|
|
||||||
fn entered (&self) -> bool {
|
|
||||||
self.focus.is_entered()
|
|
||||||
}
|
|
||||||
/// Get the currently focused item.
|
|
||||||
fn set_entered (&mut self, entered: bool) {
|
|
||||||
if entered {
|
|
||||||
self.focus.to_entered()
|
|
||||||
} else {
|
|
||||||
self.focus.to_focused()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FocusGrid for $Struct {
|
|
||||||
fn focus_cursor (&self) -> (usize, usize) {
|
|
||||||
self.cursor
|
|
||||||
}
|
|
||||||
fn focus_cursor_mut (&mut self) -> &mut (usize, usize) {
|
|
||||||
&mut self.cursor
|
|
||||||
}
|
|
||||||
fn focus_layout (&self) -> &[&[$Focus]] {
|
|
||||||
use $Focus::*;
|
|
||||||
&$Grid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
use crate::*;
|
|
||||||
pub struct MenuBar<E: Engine, S, C: Command<S>> {
|
|
||||||
pub menus: Vec<Menu<E, S, C>>,
|
|
||||||
pub index: usize,
|
|
||||||
}
|
|
||||||
impl<E: Engine, S, C: Command<S>> MenuBar<E, S, C> {
|
|
||||||
pub fn new () -> Self { Self { menus: vec![], index: 0 } }
|
|
||||||
pub fn add (mut self, menu: Menu<E, S, C>) -> Self {
|
|
||||||
self.menus.push(menu);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub struct Menu<E: Engine, S, C: Command<S>> {
|
|
||||||
pub title: Arc<str>,
|
|
||||||
pub items: Vec<MenuItem<E, S, C>>,
|
|
||||||
pub index: Option<usize>,
|
|
||||||
}
|
|
||||||
impl<E: Engine, S, C: Command<S>> Menu<E, S, C> {
|
|
||||||
pub fn new (title: impl AsRef<str>) -> Self {
|
|
||||||
Self {
|
|
||||||
title: title.as_ref().to_string(),
|
|
||||||
items: vec![],
|
|
||||||
index: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn add (mut self, item: MenuItem<E, S, C>) -> Self {
|
|
||||||
self.items.push(item);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
pub fn sep (mut self) -> Self {
|
|
||||||
self.items.push(MenuItem::sep());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
pub fn cmd (mut self, hotkey: &'static str, text: &'static str, command: C) -> Self {
|
|
||||||
self.items.push(MenuItem::cmd(hotkey, text, command));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
pub fn off (mut self, hotkey: &'static str, text: &'static str) -> Self {
|
|
||||||
self.items.push(MenuItem::off(hotkey, text));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub enum MenuItem<E: Engine, S, C: Command<S>> {
|
|
||||||
/// Unused.
|
|
||||||
__(PhantomData<E>, PhantomData<S>),
|
|
||||||
/// A separator. Skip it.
|
|
||||||
Separator,
|
|
||||||
/// A menu item with command, description and hotkey.
|
|
||||||
Command(&'static str, &'static str, C),
|
|
||||||
/// A menu item that can't be activated but has description and hotkey
|
|
||||||
Disabled(&'static str, &'static str)
|
|
||||||
}
|
|
||||||
impl<E: Engine, S, C: Command<S>> MenuItem<E, S, C> {
|
|
||||||
pub fn sep () -> Self {
|
|
||||||
Self::Separator
|
|
||||||
}
|
|
||||||
pub fn cmd (hotkey: &'static str, text: &'static str, command: C) -> Self {
|
|
||||||
Self::Command(hotkey, text, command)
|
|
||||||
}
|
|
||||||
pub fn off (hotkey: &'static str, text: &'static str) -> Self {
|
|
||||||
Self::Disabled(hotkey, text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//impl<T: Draw<TuiOut>> Content<TuiOut> for Result<T, Box<dyn std::error::Error>> {
|
|
||||||
//fn content (&self) -> impl Draw<TuiOut> + '_ {
|
|
||||||
//Bsp::a(self.as_ref().ok(), self.as_ref().err().map(
|
|
||||||
//|e|Tui::fg_bg(Color::Rgb(255,255,255), Color::Rgb(32,32,32), e.to_string())
|
|
||||||
//))
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
|
|
||||||
//impl<T: Draw<TuiOut>> Draw<TuiOut> for Result<T, Box<dyn std::error::Error>> {
|
|
||||||
//fn layout (&self, to: [u16;4]) -> [u16;4] {
|
|
||||||
//match self {
|
|
||||||
//Ok(content) => content.layout(to),
|
|
||||||
//Err(e) => [0, 0, to.w(), to.h()]
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//fn draw (&self, to: &mut TuiOut) {
|
|
||||||
//match self {
|
|
||||||
//Ok(content) => content.draw(to),
|
|
||||||
//Err(e) => to.blit(&e.to_string(), 0, 0, Some(Style::default()
|
|
||||||
//.bg(Color::Rgb(32,32,32))
|
|
||||||
//.fg(Color::Rgb(255,255,255))))
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
|
|
||||||
//let token = token.as_ref();
|
|
||||||
//if token.len() < 2 {
|
|
||||||
//Self { valid: false, key: None, mods: KeyModifiers::NONE }
|
|
||||||
//} else if token.chars().next() != Some('@') {
|
|
||||||
//Self { valid: false, key: None, mods: KeyModifiers::NONE }
|
|
||||||
//} else {
|
|
||||||
//Self { valid: true, key: None, mods: KeyModifiers::NONE }.next(&token[1..])
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//pub fn build (self) -> Option<Event> {
|
|
||||||
//if self.valid && self.key.is_some() {
|
|
||||||
//Some(Event::Key(KeyEvent::new(self.key.unwrap(), self.mods)))
|
|
||||||
//} else {
|
|
||||||
//None
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//fn next (mut self, token: &str) -> Self {
|
|
||||||
//let mut tokens = token.split('-').peekable();
|
|
||||||
//while let Some(token) = tokens.next() {
|
|
||||||
//if tokens.peek().is_some() {
|
|
||||||
//match token {
|
|
||||||
//"ctrl" | "Ctrl" | "c" | "C" => self.mods |= KeyModifiers::CONTROL,
|
|
||||||
//"alt" | "Alt" | "m" | "M" => self.mods |= KeyModifiers::ALT,
|
|
||||||
//"shift" | "Shift" | "s" | "S" => {
|
|
||||||
//self.mods |= KeyModifiers::SHIFT;
|
|
||||||
//// + TODO normalize character case, BackTab, etc.
|
|
||||||
//},
|
|
||||||
//_ => panic!("unknown modifier {token}"),
|
|
||||||
//}
|
|
||||||
//} else {
|
|
||||||
//self.key = if token.len() == 1 {
|
|
||||||
//Some(KeyCode::Char(token.chars().next().unwrap()))
|
|
||||||
//} else {
|
|
||||||
//Some(Self::named_key(token).unwrap_or_else(||panic!("unknown character {token}")))
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//self
|
|
||||||
//}
|
|
||||||
490
tui/src/lib.rs
490
tui/src/lib.rs
|
|
@ -1,490 +0,0 @@
|
||||||
#![feature(type_changing_struct_update, trait_alias)]
|
|
||||||
pub extern crate dizzle;
|
|
||||||
pub extern crate tengri_input;
|
|
||||||
pub extern crate tengri_output;
|
|
||||||
pub extern crate ratatui;
|
|
||||||
pub extern crate crossterm;
|
|
||||||
pub extern crate palette;
|
|
||||||
pub extern crate better_panic;
|
|
||||||
pub use ::tengri_output::PerfModel;
|
|
||||||
use std::{time::Duration, thread::{spawn, JoinHandle}, io::Write};
|
|
||||||
use unicode_width::*;
|
|
||||||
pub(crate) use ::{
|
|
||||||
dizzle::*, tengri_input::*, tengri_output::*,
|
|
||||||
std::{io::{stdout, Stdout}, sync::{Arc, RwLock, atomic::{AtomicBool, Ordering::*}}},
|
|
||||||
better_panic::{Settings, Verbosity},
|
|
||||||
palette::{*, convert::*, okhsl::*},
|
|
||||||
ratatui::{
|
|
||||||
prelude::{Color, Style, Buffer, Position},
|
|
||||||
style::{Stylize, Modifier, Color::*},
|
|
||||||
backend::{Backend, CrosstermBackend, ClearType},
|
|
||||||
layout::{Size, Rect},
|
|
||||||
buffer::Cell
|
|
||||||
},
|
|
||||||
crossterm::{
|
|
||||||
ExecutableCommand,
|
|
||||||
terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode},
|
|
||||||
event::{poll, read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
#[macro_export] macro_rules! tui_main {
|
|
||||||
($expr:expr) => {
|
|
||||||
fn main () -> Usually<()> {
|
|
||||||
tengri_tui::Tui::run(true, $expr)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export] macro_rules! has_color {
|
|
||||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
|
||||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasColor for $Struct $(<$($L),*$($T),*>)? {
|
|
||||||
fn color (&$self) -> ItemColor { $cb }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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<TuiOut> for $T {}
|
|
||||||
impl Draw<TuiOut> for $T {
|
|
||||||
fn draw (&self, to: &mut TuiOut) {
|
|
||||||
if self.enabled() { let _ = BorderStyle::draw(self, to); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)+}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod tui_structs; pub use self::tui_structs::*;
|
|
||||||
mod tui_traits; pub use self::tui_traits::*;
|
|
||||||
mod tui_impls;
|
|
||||||
#[cfg(test)] mod tui_test;
|
|
||||||
|
|
||||||
/// Run an app in the main loop.
|
|
||||||
pub fn tui_run <T: Send + Sync + Draw<TuiOut> + Handle<TuiIn> + 'static> (
|
|
||||||
join: bool,
|
|
||||||
state: &Arc<RwLock<T>>
|
|
||||||
) -> Usually<Arc<RwLock<Tui>>> {
|
|
||||||
let backend = CrosstermBackend::new(stdout());
|
|
||||||
let Size { width, height } = backend.size()?;
|
|
||||||
let tui = Arc::new(RwLock::new(Tui {
|
|
||||||
exited: Arc::new(AtomicBool::new(false)),
|
|
||||||
buffer: Buffer::empty(Rect { x: 0, y: 0, width, height }),
|
|
||||||
area: [0, 0, width, height],
|
|
||||||
perf: Default::default(),
|
|
||||||
backend,
|
|
||||||
}));
|
|
||||||
let _input_thread = tui_input(tui.clone(), state, Duration::from_millis(100));
|
|
||||||
tui.write().unwrap().setup()?;
|
|
||||||
let render_thread = tui_output(tui.clone(), state, Duration::from_millis(10))?;
|
|
||||||
if join {
|
|
||||||
match render_thread.join() {
|
|
||||||
Ok(result) => {
|
|
||||||
tui.write().unwrap().teardown()?;
|
|
||||||
println!("\n\rRan successfully: {result:?}\n\r");
|
|
||||||
},
|
|
||||||
Err(error) => {
|
|
||||||
tui.write().unwrap().teardown()?;
|
|
||||||
panic!("\n\rDraw thread failed: error={error:?}.\n\r")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(tui)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tui_setup <W: Write> (
|
|
||||||
backend: &mut CrosstermBackend<W>
|
|
||||||
) -> Usually<()> {
|
|
||||||
let better_panic_handler = Settings::auto().verbosity(Verbosity::Full).create_panic_handler();
|
|
||||||
std::panic::set_hook(Box::new(move |info: &std::panic::PanicHookInfo|{
|
|
||||||
stdout().execute(LeaveAlternateScreen).unwrap();
|
|
||||||
CrosstermBackend::new(stdout()).show_cursor().unwrap();
|
|
||||||
disable_raw_mode().unwrap();
|
|
||||||
better_panic_handler(info);
|
|
||||||
}));
|
|
||||||
stdout().execute(EnterAlternateScreen)?;
|
|
||||||
backend.hide_cursor()?;
|
|
||||||
enable_raw_mode().map_err(Into::into)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tui_teardown <W: Write> (backend: &mut CrosstermBackend<W>) -> Usually<()> {
|
|
||||||
stdout().execute(LeaveAlternateScreen)?;
|
|
||||||
backend.show_cursor()?;
|
|
||||||
disable_raw_mode().map_err(Into::into)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tui_resized <W: Write> (
|
|
||||||
backend: &mut CrosstermBackend<W>,
|
|
||||||
buffer: &mut Buffer,
|
|
||||||
size: ratatui::prelude::Rect
|
|
||||||
) {
|
|
||||||
if buffer.area != size {
|
|
||||||
backend.clear_region(ClearType::All).unwrap();
|
|
||||||
buffer.resize(size);
|
|
||||||
buffer.reset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tui_redrawn <'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_update (
|
|
||||||
buf: &mut Buffer, area: XYWH<u16>, callback: &impl Fn(&mut Cell, u16, u16)
|
|
||||||
) {
|
|
||||||
for row in 0..area.h() {
|
|
||||||
let y = area.y() + row;
|
|
||||||
for col in 0..area.w() {
|
|
||||||
let x = area.x() + col;
|
|
||||||
if x < buf.area.width && y < buf.area.height {
|
|
||||||
if let Some(cell) = buf.cell_mut(ratatui::prelude::Position { x, y }) {
|
|
||||||
callback(cell, col, row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Spawn the output thread.
|
|
||||||
pub fn tui_output <T: Draw<TuiOut> + Send + Sync + 'static> (
|
|
||||||
engine: Arc<RwLock<Tui>>, state: &Arc<RwLock<T>>, timer: Duration
|
|
||||||
) -> Result<JoinHandle<()>, std::io::Error> {
|
|
||||||
let exited = engine.read().unwrap().exited.clone();
|
|
||||||
let engine = engine.clone();
|
|
||||||
let state = state.clone();
|
|
||||||
let Size { width, height } = engine.read().unwrap().backend.size().expect("get size failed");
|
|
||||||
let mut buffer = Buffer::empty(Rect { x: 0, y: 0, width, height });
|
|
||||||
std::thread::Builder::new()
|
|
||||||
.name("tui output thread".into())
|
|
||||||
.spawn(move || loop {
|
|
||||||
if exited.fetch_and(true, Relaxed) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
let t0 = engine.read().unwrap().perf.get_t0();
|
|
||||||
let Size { width, height } = engine.read().unwrap().backend.size()
|
|
||||||
.expect("get size failed");
|
|
||||||
if let Ok(state) = state.try_read() {
|
|
||||||
let size = Rect { x: 0, y: 0, width, height };
|
|
||||||
if buffer.area != size {
|
|
||||||
engine.write().unwrap().backend.clear_region(ClearType::All).expect("clear failed");
|
|
||||||
buffer.resize(size);
|
|
||||||
buffer.reset();
|
|
||||||
}
|
|
||||||
let mut output = TuiOut { buffer, area: XYWH(0, 0, width, height) };
|
|
||||||
state.draw(&mut output);
|
|
||||||
buffer = engine.write().unwrap().flip(output.buffer, size);
|
|
||||||
}
|
|
||||||
let t1 = (*engine.read().unwrap()).perf.get_t1(t0).unwrap();
|
|
||||||
buffer.set_string(0, 0, &format!("{:>3}.{:>3}ms", t1.as_millis(), t1.as_micros() % 1000), Style::default());
|
|
||||||
std::thread::sleep(timer);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Spawn the input thread.
|
|
||||||
pub fn tui_input <T: Handle<TuiIn> + Send + Sync + 'static> (
|
|
||||||
engine: Arc<RwLock<Tui>>, state: &Arc<RwLock<T>>, timer: Duration
|
|
||||||
) -> JoinHandle<()> {
|
|
||||||
let exited = engine.read().unwrap().exited.clone();
|
|
||||||
let state = state.clone();
|
|
||||||
spawn(move || loop {
|
|
||||||
if exited.fetch_and(true, Relaxed) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if poll(timer).is_ok() {
|
|
||||||
let event = read().unwrap();
|
|
||||||
match event {
|
|
||||||
Event::Key(KeyEvent {
|
|
||||||
code: KeyCode::Char('c'),
|
|
||||||
modifiers: KeyModifiers::CONTROL,
|
|
||||||
kind: KeyEventKind::Press,
|
|
||||||
state: KeyEventState::NONE
|
|
||||||
}) => {
|
|
||||||
exited.store(true, Relaxed);
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
let exited = exited.clone();
|
|
||||||
let event = TuiEvent::from_crossterm(event);
|
|
||||||
if let Err(e) = state.write().unwrap().handle(&TuiIn { exited, event }) {
|
|
||||||
panic!("{e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "dsl")] pub fn evaluate_output_expression_tui <'a, S> (
|
|
||||||
state: &S, output: &mut TuiOut, expr: impl Expression + 'a
|
|
||||||
) -> Usually<bool> where
|
|
||||||
S: View<TuiOut, ()>
|
|
||||||
+ for<'b>Namespace<'b, bool>
|
|
||||||
+ for<'b>Namespace<'b, u16>
|
|
||||||
+ for<'b>Namespace<'b, Color>
|
|
||||||
{
|
|
||||||
// See `tengri_output::evaluate_output_expression`
|
|
||||||
let head = expr.head()?;
|
|
||||||
let mut frags = head.src()?.unwrap_or_default().split("/");
|
|
||||||
let args = expr.tail();
|
|
||||||
let arg0 = args.head();
|
|
||||||
let tail0 = args.tail();
|
|
||||||
let arg1 = tail0.head();
|
|
||||||
let tail1 = tail0.tail();
|
|
||||||
let _arg2 = tail1.head();
|
|
||||||
match frags.next() {
|
|
||||||
|
|
||||||
Some("text") => if let Some(src) = args?.src()? { output.place(&src) },
|
|
||||||
|
|
||||||
Some("fg") => {
|
|
||||||
let arg0 = arg0?.expect("fg: expected arg 0 (color)");
|
|
||||||
output.place(&Tui::fg(
|
|
||||||
Namespace::<Color>::namespace(state, arg0)?.unwrap_or_else(||panic!("fg: {arg0:?}: not a color")),
|
|
||||||
Thunk::new(move|output: &mut TuiOut|state.view(output, &arg1).unwrap()),
|
|
||||||
))
|
|
||||||
},
|
|
||||||
|
|
||||||
Some("bg") => {
|
|
||||||
let arg0 = arg0?.expect("bg: expected arg 0 (color)");
|
|
||||||
output.place(&Tui::bg(
|
|
||||||
Namespace::<Color>::namespace(state, arg0)?.unwrap_or_else(||panic!("bg: {arg0:?}: not a color")),
|
|
||||||
Thunk::new(move|output: &mut TuiOut|state.view(output, &arg1).unwrap()),
|
|
||||||
))
|
|
||||||
},
|
|
||||||
|
|
||||||
_ => return Ok(false)
|
|
||||||
|
|
||||||
};
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn named_key (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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn button_2 <'a> (key: impl Content<TuiOut>, label: impl Content<TuiOut>, editing: bool) -> impl Content<TuiOut> {
|
|
||||||
Tui::bold(true, Bsp::e(
|
|
||||||
Tui::fg_bg(Tui::orange(), Tui::g(0), Bsp::e(Tui::fg(Tui::g(0), &"▐"), Bsp::e(key, Tui::fg(Tui::g(96), &"▐")))),
|
|
||||||
When::new(!editing, Tui::fg_bg(Tui::g(255), Tui::g(96), label))))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn button_3 <'a> (
|
|
||||||
key: impl Content<TuiOut>, label: impl Content<TuiOut>, value: impl Content<TuiOut>, editing: bool,
|
|
||||||
) -> impl Content<TuiOut> {
|
|
||||||
Tui::bold(true, Bsp::e(
|
|
||||||
Tui::fg_bg(Tui::orange(), Tui::g(0),
|
|
||||||
Bsp::e(Tui::fg(Tui::g(0), &"▐"), Bsp::e(key, Tui::fg(if editing { Tui::g(128) } else { Tui::g(96) }, "▐")))),
|
|
||||||
Bsp::e(
|
|
||||||
When::new(!editing, Bsp::e(Tui::fg_bg(Tui::g(255), Tui::g(96), label), Tui::fg_bg(Tui::g(128), Tui::g(96), &"▐"),)),
|
|
||||||
Bsp::e(Tui::fg_bg(Tui::g(224), Tui::g(128), value), Tui::fg_bg(Tui::g(128), Reset, &"▌"), ))))
|
|
||||||
}
|
|
||||||
|
|
||||||
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 fn okhsl_to_rgb (color: Okhsl<f32>) -> Color {
|
|
||||||
let Srgb { red, green, blue, .. }: Srgb<f32> = Srgb::from_color_unclamped(color);
|
|
||||||
Color::Rgb((red * 255.0) as u8, (green * 255.0) as u8, (blue * 255.0) as u8,)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rgb_to_okhsl (color: Color) -> Okhsl<f32> {
|
|
||||||
if let Color::Rgb(r, g, b) = color {
|
|
||||||
Okhsl::from_color(Srgb::new(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0))
|
|
||||||
} else {
|
|
||||||
unreachable!("only Color::Rgb is supported")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Trim string with [unicode_width].
|
|
||||||
pub fn trim_string (max_width: usize, input: impl AsRef<str>) -> String {
|
|
||||||
let input = input.as_ref();
|
|
||||||
let mut output = Vec::with_capacity(input.len());
|
|
||||||
let mut width: usize = 1;
|
|
||||||
let mut chars = input.chars();
|
|
||||||
while let Some(c) = chars.next() {
|
|
||||||
if width > max_width {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
output.push(c);
|
|
||||||
width += c.width().unwrap_or(0);
|
|
||||||
}
|
|
||||||
return output.into_iter().collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn width_chars_max (max: u16, text: impl AsRef<str>) -> u16 {
|
|
||||||
let mut width: u16 = 0;
|
|
||||||
let mut chars = text.as_ref().chars();
|
|
||||||
while let Some(c) = chars.next() {
|
|
||||||
width += c.width().unwrap_or(0) as u16;
|
|
||||||
if width > max {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return width
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
#[allow(unused)] use crate::*;
|
|
||||||
|
|
||||||
mod tui_border; pub use self::tui_border::*;
|
|
||||||
mod tui_button; pub use self::tui_button::*;
|
|
||||||
mod tui_color; pub use self::tui_color::*;
|
|
||||||
mod tui_error; pub use self::tui_error::*;
|
|
||||||
mod tui_phat; pub use self::tui_phat::*;
|
|
||||||
mod tui_repeat; pub use self::tui_repeat::*;
|
|
||||||
mod tui_scroll; pub use self::tui_scroll::*;
|
|
||||||
mod tui_string; pub use self::tui_string::*;
|
|
||||||
mod tui_number; //pub use self::tui_number::*;
|
|
||||||
mod tui_tryptich; //pub use self::tui_tryptich::*;
|
|
||||||
|
|
@ -1,637 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
use unicode_width::{UnicodeWidthStr, UnicodeWidthChar};
|
|
||||||
use rand::{thread_rng, distributions::uniform::UniformSampler};
|
|
||||||
|
|
||||||
impl Tui {
|
|
||||||
/// Create and launch a terminal user interface.
|
|
||||||
pub fn run <T> (join: bool, state: T) -> Usually<Arc<RwLock<Self>>> where
|
|
||||||
T: Handle<TuiIn> + Draw<TuiOut> + Send + Sync + 'static
|
|
||||||
{
|
|
||||||
tui_run(join, &Arc::new(RwLock::new(state)))
|
|
||||||
}
|
|
||||||
/// True if done
|
|
||||||
pub fn exited (&self) -> bool { self.exited.fetch_and(true, Relaxed) }
|
|
||||||
/// Prepare before run
|
|
||||||
pub fn setup (&mut self) -> Usually<()> { tui_setup(&mut self.backend) }
|
|
||||||
/// Clean up after run
|
|
||||||
pub fn teardown (&mut self) -> Usually<()> { tui_teardown(&mut self.backend) }
|
|
||||||
/// Apply changes to the display buffer.
|
|
||||||
pub fn flip (&mut self, mut buffer: Buffer, size: ratatui::prelude::Rect) -> Buffer {
|
|
||||||
tui_resized(&mut self.backend, &mut self.buffer, size);
|
|
||||||
tui_redrawn(&mut self.backend, &mut self.buffer, &mut buffer);
|
|
||||||
buffer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Input for TuiIn {
|
|
||||||
type Event = TuiEvent;
|
|
||||||
type Handled = bool;
|
|
||||||
fn event (&self) -> &TuiEvent { &self.event }
|
|
||||||
fn done (&self) { self.exited.store(true, Relaxed); }
|
|
||||||
fn is_done (&self) -> bool { self.exited.fetch_and(true, Relaxed) }
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl TuiEvent {
|
|
||||||
pub fn from_crossterm (event: Event) -> Self { Self(event) }
|
|
||||||
#[cfg(feature = "dsl")] pub fn from_dsl (dsl: impl Language) -> Perhaps<Self> {
|
|
||||||
Ok(TuiKey::from_dsl(dsl)?.to_crossterm().map(Self))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl TuiKey {
|
|
||||||
const SPLIT: char = '/';
|
|
||||||
#[cfg(feature = "dsl")] 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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn to_crossterm (&self) -> Option<Event> {
|
|
||||||
self.0.map(|code|Event::Key(KeyEvent {
|
|
||||||
code,
|
|
||||||
modifiers: self.1,
|
|
||||||
kind: KeyEventKind::Press,
|
|
||||||
state: KeyEventState::NONE,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Out for TuiOut {
|
|
||||||
type Unit = u16;
|
|
||||||
#[inline] fn area (&self) -> XYWH<u16> { self.area }
|
|
||||||
#[inline] fn area_mut (&mut self) -> &mut XYWH<u16> { &mut self.area }
|
|
||||||
#[inline] fn place_at <'t, T: Draw<Self> + ?Sized> (&mut self, area: XYWH<u16>, content: &'t T) {
|
|
||||||
let last = self.area();
|
|
||||||
*self.area_mut() = area;
|
|
||||||
content.draw(self);
|
|
||||||
*self.area_mut() = last;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl TuiOut {
|
|
||||||
#[inline] pub fn with_rect (&mut self, area: XYWH<u16>) -> &mut Self { self.area = area; self }
|
|
||||||
pub fn update (&mut self, area: XYWH<u16>, callback: &impl Fn(&mut Cell, u16, u16)) { tui_update(&mut self.buffer, area, callback); }
|
|
||||||
pub fn fill_char (&mut self, area: XYWH<u16>, c: char) { self.update(area, &|cell,_,_|{cell.set_char(c);}) }
|
|
||||||
pub fn fill_bg (&mut self, area: XYWH<u16>, color: Color) { self.update(area, &|cell,_,_|{cell.set_bg(color);}) }
|
|
||||||
pub fn fill_fg (&mut self, area: XYWH<u16>, color: Color) { self.update(area, &|cell,_,_|{cell.set_fg(color);}) }
|
|
||||||
pub fn fill_mod (&mut self, area: XYWH<u16>, on: bool, modifier: Modifier) {
|
|
||||||
if on {
|
|
||||||
self.update(area, &|cell,_,_|cell.modifier.insert(modifier))
|
|
||||||
} else {
|
|
||||||
self.update(area, &|cell,_,_|cell.modifier.remove(modifier))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn fill_bold (&mut self, area: XYWH<u16>, on: bool) { self.fill_mod(area, on, Modifier::BOLD) }
|
|
||||||
pub fn fill_reversed (&mut self, area: XYWH<u16>, on: bool) { self.fill_mod(area, on, Modifier::REVERSED) }
|
|
||||||
pub fn fill_crossed_out (&mut self, area: XYWH<u16>, on: bool) { self.fill_mod(area, on, Modifier::CROSSED_OUT) }
|
|
||||||
pub fn fill_ul (&mut self, area: XYWH<u16>, color: Option<Color>) {
|
|
||||||
if let Some(color) = color {
|
|
||||||
self.update(area, &|cell,_,_|{
|
|
||||||
cell.modifier.insert(ratatui::prelude::Modifier::UNDERLINED);
|
|
||||||
cell.underline_color = color;
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
self.update(area, &|cell,_,_|{
|
|
||||||
cell.modifier.remove(ratatui::prelude::Modifier::UNDERLINED);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn tint_all (&mut self, fg: Color, bg: Color, modifier: Modifier) {
|
|
||||||
for cell in self.buffer.content.iter_mut() {
|
|
||||||
cell.fg = fg;
|
|
||||||
cell.bg = bg;
|
|
||||||
cell.modifier = modifier;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn blit (&mut self, text: &impl AsRef<str>, x: u16, y: u16, style: Option<Style>) {
|
|
||||||
let text = text.as_ref();
|
|
||||||
let buf = &mut self.buffer;
|
|
||||||
let style = style.unwrap_or(Style::default());
|
|
||||||
if x < buf.area.width && y < buf.area.height {
|
|
||||||
buf.set_string(x, y, text, style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Write a line of text
|
|
||||||
///
|
|
||||||
/// TODO: do a paragraph (handle newlines)
|
|
||||||
pub fn text (&mut self, text: &impl AsRef<str>, x0: u16, y: u16, max_width: u16) {
|
|
||||||
let text = text.as_ref();
|
|
||||||
let buf = &mut self.buffer;
|
|
||||||
let mut string_width: u16 = 0;
|
|
||||||
for character in text.chars() {
|
|
||||||
let x = x0 + string_width;
|
|
||||||
let character_width = character.width().unwrap_or(0) as u16;
|
|
||||||
string_width += character_width;
|
|
||||||
if string_width > max_width {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if let Some(cell) = buf.cell_mut(ratatui::prelude::Position { x, y }) {
|
|
||||||
cell.set_char(character);
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// A single color within item theme parameters, in OKHSL and RGB representations.
|
|
||||||
impl ItemColor {
|
|
||||||
pub const fn from_rgb (rgb: Color) -> Self {
|
|
||||||
Self { rgb, okhsl: Okhsl::new_const(OklabHue::new(0.0), 0.0, 0.0) }
|
|
||||||
}
|
|
||||||
pub const fn from_okhsl (okhsl: Okhsl<f32>) -> Self {
|
|
||||||
Self { rgb: Color::Rgb(0, 0, 0), okhsl }
|
|
||||||
}
|
|
||||||
pub fn random () -> Self {
|
|
||||||
let mut rng = thread_rng();
|
|
||||||
let lo = Okhsl::new(-180.0, 0.01, 0.25);
|
|
||||||
let hi = Okhsl::new( 180.0, 0.9, 0.5);
|
|
||||||
UniformOkhsl::new(lo, hi).sample(&mut rng).into()
|
|
||||||
}
|
|
||||||
pub fn random_dark () -> Self {
|
|
||||||
let mut rng = thread_rng();
|
|
||||||
let lo = Okhsl::new(-180.0, 0.025, 0.075);
|
|
||||||
let hi = Okhsl::new( 180.0, 0.5, 0.150);
|
|
||||||
UniformOkhsl::new(lo, hi).sample(&mut rng).into()
|
|
||||||
}
|
|
||||||
pub fn random_near (color: Self, distance: f32) -> Self {
|
|
||||||
color.mix(Self::random(), distance)
|
|
||||||
}
|
|
||||||
pub fn mix (&self, other: Self, distance: f32) -> Self {
|
|
||||||
if distance > 1.0 { panic!("color mixing takes distance between 0.0 and 1.0"); }
|
|
||||||
self.okhsl.mix(other.okhsl, distance).into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl ItemTheme {
|
|
||||||
pub const G: [Self;256] = {
|
|
||||||
let mut builder = konst::array::ArrayBuilder::new();
|
|
||||||
while !builder.is_full() {
|
|
||||||
let index = builder.len() as u8;
|
|
||||||
let light = (index as f64 * 1.15) as u8;
|
|
||||||
let lighter = (index as f64 * 1.7) as u8;
|
|
||||||
let lightest = (index as f64 * 1.85) as u8;
|
|
||||||
let dark = (index as f64 * 0.9) as u8;
|
|
||||||
let darker = (index as f64 * 0.6) as u8;
|
|
||||||
let darkest = (index as f64 * 0.3) as u8;
|
|
||||||
builder.push(ItemTheme {
|
|
||||||
base: ItemColor::from_rgb(Color::Rgb(index, index, index )),
|
|
||||||
light: ItemColor::from_rgb(Color::Rgb(light, light, light, )),
|
|
||||||
lighter: ItemColor::from_rgb(Color::Rgb(lighter, lighter, lighter, )),
|
|
||||||
lightest: ItemColor::from_rgb(Color::Rgb(lightest, lightest, lightest, )),
|
|
||||||
dark: ItemColor::from_rgb(Color::Rgb(dark, dark, dark, )),
|
|
||||||
darker: ItemColor::from_rgb(Color::Rgb(darker, darker, darker, )),
|
|
||||||
darkest: ItemColor::from_rgb(Color::Rgb(darkest, darkest, darkest, )),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
builder.build()
|
|
||||||
};
|
|
||||||
pub fn random () -> Self { ItemColor::random().into() }
|
|
||||||
pub fn random_near (color: Self, distance: f32) -> Self {
|
|
||||||
color.base.mix(ItemColor::random(), distance).into()
|
|
||||||
}
|
|
||||||
pub const G00: Self = {
|
|
||||||
let color: ItemColor = ItemColor {
|
|
||||||
okhsl: Okhsl { hue: OklabHue::new(0.0), lightness: 0.0, saturation: 0.0 },
|
|
||||||
rgb: Color::Rgb(0, 0, 0)
|
|
||||||
};
|
|
||||||
Self {
|
|
||||||
base: color,
|
|
||||||
light: color,
|
|
||||||
lighter: color,
|
|
||||||
lightest: color,
|
|
||||||
dark: color,
|
|
||||||
darker: color,
|
|
||||||
darkest: color,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
pub fn from_tui_color (base: Color) -> Self {
|
|
||||||
Self::from_item_color(ItemColor::from_rgb(base))
|
|
||||||
}
|
|
||||||
pub fn from_item_color (base: ItemColor) -> Self {
|
|
||||||
let mut light = base.okhsl;
|
|
||||||
light.lightness = (light.lightness * 1.3).min(1.0);
|
|
||||||
let mut lighter = light;
|
|
||||||
lighter.lightness = (lighter.lightness * 1.3).min(1.0);
|
|
||||||
let mut lightest = base.okhsl;
|
|
||||||
lightest.lightness = 0.95;
|
|
||||||
let mut dark = base.okhsl;
|
|
||||||
dark.lightness = (dark.lightness * 0.75).max(0.0);
|
|
||||||
dark.saturation = (dark.saturation * 0.75).max(0.0);
|
|
||||||
let mut darker = dark;
|
|
||||||
darker.lightness = (darker.lightness * 0.66).max(0.0);
|
|
||||||
darker.saturation = (darker.saturation * 0.66).max(0.0);
|
|
||||||
let mut darkest = darker;
|
|
||||||
darkest.lightness = 0.1;
|
|
||||||
darkest.saturation = (darkest.saturation * 0.50).max(0.0);
|
|
||||||
Self {
|
|
||||||
base,
|
|
||||||
light: light.into(),
|
|
||||||
lighter: lighter.into(),
|
|
||||||
lightest: lightest.into(),
|
|
||||||
dark: dark.into(),
|
|
||||||
darker: darker.into(),
|
|
||||||
darkest: darkest.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> Phat<T> {
|
|
||||||
pub const LO: &'static str = "▄";
|
|
||||||
pub const HI: &'static str = "▀";
|
|
||||||
/// A phat line
|
|
||||||
pub fn lo (fg: Color, bg: Color) -> impl Content<TuiOut> {
|
|
||||||
Fixed::Y(1, Tui::fg_bg(fg, bg, Repeat::X(Self::LO)))
|
|
||||||
}
|
|
||||||
/// A phat line
|
|
||||||
pub fn hi (fg: Color, bg: Color) -> impl Content<TuiOut> {
|
|
||||||
Fixed::Y(1, Tui::fg_bg(fg, bg, Repeat::X(Self::HI)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Scrollbar {
|
|
||||||
const ICON_DEC_V: &[char] = &['▲'];
|
|
||||||
const ICON_INC_V: &[char] = &['▼'];
|
|
||||||
const ICON_DEC_H: &[char] = &[' ', '🞀', ' '];
|
|
||||||
const ICON_INC_H: &[char] = &[' ', '🞂', ' '];
|
|
||||||
}
|
|
||||||
impl<'a, T: AsRef<str>> TrimString<T> {
|
|
||||||
fn as_ref (&self) -> TrimStringRef<'_, T> { TrimStringRef(self.0, &self.1) }
|
|
||||||
}
|
|
||||||
impl<O: Out, T: Draw<O>> ErrorBoundary<O, T> {
|
|
||||||
pub fn new (content: Perhaps<T>) -> Self { Self(Default::default(), content) }
|
|
||||||
}
|
|
||||||
mod content {
|
|
||||||
use super::*;
|
|
||||||
impl<S: BorderStyle, W: Content<TuiOut>> HasContent<TuiOut> for Bordered<S, W> {
|
|
||||||
fn content (&self) -> impl Content<TuiOut> {
|
|
||||||
Fill::XY(lay!( When::new(self.0, Border(self.0, self.1)), Pad::XY(1, 1, &self.2) ))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<
|
|
||||||
A: Content<TuiOut>,
|
|
||||||
B: Content<TuiOut>,
|
|
||||||
C: Content<TuiOut>,
|
|
||||||
> HasContent<TuiOut> for Tryptich<A, B, C> {
|
|
||||||
fn content (&self) -> impl Content<TuiOut> {
|
|
||||||
let Self { top, h, left: (w_a, ref a), middle: (w_b, ref b), right: (w_c, ref c) } = *self;
|
|
||||||
Fixed::Y(h, if top {
|
|
||||||
Bsp::a(
|
|
||||||
Fill::X(Align::n(Fixed::X(w_b, Align::x(Tui::bg(Color::Reset, b))))),
|
|
||||||
Bsp::a(
|
|
||||||
Fill::X(Align::nw(Fixed::X(w_a, Tui::bg(Color::Reset, a)))),
|
|
||||||
Fill::X(Align::ne(Fixed::X(w_c, Tui::bg(Color::Reset, c)))),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Bsp::a(
|
|
||||||
Fill::XY(Align::c(Fixed::X(w_b, Align::x(Tui::bg(Color::Reset, b))))),
|
|
||||||
Bsp::a(
|
|
||||||
Fill::XY(Align::w(Fixed::X(w_a, Tui::bg(Color::Reset, a)))),
|
|
||||||
Fill::XY(Align::e(Fixed::X(w_c, Tui::bg(Color::Reset, c)))),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T: Content<TuiOut>> HasContent<TuiOut> for Phat<T> {
|
|
||||||
fn content (&self) -> impl Content<TuiOut> {
|
|
||||||
let [fg, bg, hi, lo] = self.colors;
|
|
||||||
let top = Fixed::Y(1, Self::lo(bg, hi));
|
|
||||||
let low = Fixed::Y(1, Self::hi(bg, lo));
|
|
||||||
let content = Tui::fg_bg(fg, bg, &self.content);
|
|
||||||
Min::XY(self.width, self.height, Bsp::s(top, Bsp::n(low, Fill::XY(content))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod layout {
|
|
||||||
use super::*;
|
|
||||||
impl<T: Content<TuiOut>> Layout<TuiOut> for Modify<T> {}
|
|
||||||
impl<T: Content<TuiOut>> Layout<TuiOut> for Styled<T> {}
|
|
||||||
impl Layout<TuiOut> for Repeat<'_> {}
|
|
||||||
impl Layout<TuiOut> for &str {
|
|
||||||
fn layout (&self, to: XYWH<u16>) -> XYWH<u16> {
|
|
||||||
to.centered_xy([width_chars_max(to.w(), self), 1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Layout<TuiOut> for String {
|
|
||||||
fn layout (&self, to: XYWH<u16>) -> XYWH<u16> {
|
|
||||||
self.as_str().layout(to)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Layout<TuiOut> for Arc<str> {
|
|
||||||
fn layout (&self, to: XYWH<u16>) -> XYWH<u16> {
|
|
||||||
self.as_ref().layout(to)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<'a, T: AsRef<str>> Layout<TuiOut> for TrimString<T> {
|
|
||||||
fn layout (&self, to: XYWH<u16>) -> XYWH<u16> {
|
|
||||||
Layout::layout(&self.as_ref(), to)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<'a, T: AsRef<str>> Layout<TuiOut> for TrimStringRef<'a, T> {
|
|
||||||
fn layout (&self, to: XYWH<u16>) -> XYWH<u16> {
|
|
||||||
XYWH(to.x(), to.y(), to.w().min(self.0).min(self.1.as_ref().width() as u16), to.h())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod draw {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
impl<T: Draw<TuiOut>> Draw<TuiOut> for ErrorBoundary<TuiOut, T> {
|
|
||||||
fn draw (&self, to: &mut TuiOut) {
|
|
||||||
match self.1.as_ref() {
|
|
||||||
Ok(Some(content)) => content.draw(to),
|
|
||||||
Ok(None) => to.blit(&"empty?", 0, 0, Some(Style::default().yellow())),
|
|
||||||
Err(e) => {
|
|
||||||
let err_fg = Color::Rgb(255,224,244);
|
|
||||||
let err_bg = Color::Rgb(96,24,24);
|
|
||||||
let title = Bsp::e(Tui::bold(true, "oops. "), "rendering failed.");
|
|
||||||
let error = Bsp::e("\"why?\" ", Tui::bold(true, format!("{e}")));
|
|
||||||
to.place(&Tui::fg_bg(err_fg, err_bg, Bsp::s(title, error)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Draw<TuiOut> for u64 {
|
|
||||||
fn draw (&self, _to: &mut TuiOut) {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Draw<TuiOut> for f64 {
|
|
||||||
fn draw (&self, _to: &mut TuiOut) {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Draw<TuiOut> for Repeat<'_> {
|
|
||||||
fn draw (&self, to: &mut TuiOut) {
|
|
||||||
let XYWH(x, y, w, h) = to.area();
|
|
||||||
match self {
|
|
||||||
Self::X(c) => {
|
|
||||||
for x in x..x+w {
|
|
||||||
if let Some(cell) = to.buffer.cell_mut(Position::from((x, y))) {
|
|
||||||
cell.set_symbol(&c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Self::Y(c) => {
|
|
||||||
for y in y..y+h {
|
|
||||||
if let Some(cell) = to.buffer.cell_mut(Position::from((x, y))) {
|
|
||||||
cell.set_symbol(&c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Self::XY(c) => {
|
|
||||||
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.buffer.cell_mut(Position::from((x, y))) {
|
|
||||||
let u = u % a;
|
|
||||||
cell.set_symbol(&c[u..u+1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Draw<TuiOut> for Scrollbar {
|
|
||||||
fn draw (&self, to: &mut TuiOut) {
|
|
||||||
let XYWH(x1, y1, w, h) = to.area();
|
|
||||||
match self {
|
|
||||||
Self::X { .. } => {
|
|
||||||
let x2 = x1 + w;
|
|
||||||
for (i, x) in (x1..=x2).enumerate() {
|
|
||||||
if let Some(cell) = to.buffer.cell_mut(Position::from((x, y1))) {
|
|
||||||
if i < (Self::ICON_DEC_H.len()) {
|
|
||||||
cell.set_fg(Rgb(255, 255, 255));
|
|
||||||
cell.set_bg(Rgb(0, 0, 0));
|
|
||||||
cell.set_char(Self::ICON_DEC_H[i as usize]);
|
|
||||||
} else if i > (w as usize - Self::ICON_INC_H.len()) {
|
|
||||||
cell.set_fg(Rgb(255, 255, 255));
|
|
||||||
cell.set_bg(Rgb(0, 0, 0));
|
|
||||||
cell.set_char(Self::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('╌');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Self::Y { .. } => {
|
|
||||||
let y2 = y1 + h;
|
|
||||||
for (i, y) in (y1..=y2).enumerate() {
|
|
||||||
if let Some(cell) = to.buffer.cell_mut(Position::from((x1, y))) {
|
|
||||||
if (i as usize) < (Self::ICON_DEC_V.len()) {
|
|
||||||
cell.set_fg(Rgb(255, 255, 255));
|
|
||||||
cell.set_bg(Rgb(0, 0, 0));
|
|
||||||
cell.set_char(Self::ICON_DEC_V[i as usize]);
|
|
||||||
} else if (i as usize) > (h as usize - Self::ICON_INC_V.len()) {
|
|
||||||
cell.set_fg(Rgb(255, 255, 255));
|
|
||||||
cell.set_bg(Rgb(0, 0, 0));
|
|
||||||
cell.set_char(Self::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('╎'); // ━
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Draw<TuiOut> for &str {
|
|
||||||
fn draw (&self, to: &mut TuiOut) {
|
|
||||||
let XYWH(x, y, w, ..) = self.layout(to.area());
|
|
||||||
to.text(&self, x, y, w)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Draw<TuiOut> for String {
|
|
||||||
fn draw (&self, to: &mut TuiOut) {
|
|
||||||
self.as_str().draw(to)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Draw<TuiOut> for Arc<str> {
|
|
||||||
fn draw (&self, to: &mut TuiOut) { self.as_ref().draw(to) }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Content<TuiOut>> Draw<TuiOut> for Foreground<Color, T> {
|
|
||||||
fn draw (&self, to: &mut TuiOut) {
|
|
||||||
let area = self.layout(to.area());
|
|
||||||
to.fill_fg(area, self.0);
|
|
||||||
to.place_at(area, &self.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Content<TuiOut>> Draw<TuiOut> for Background<Color, T> {
|
|
||||||
fn draw (&self, to: &mut TuiOut) {
|
|
||||||
let area = self.layout(to.area());
|
|
||||||
to.fill_bg(area, self.0);
|
|
||||||
to.place_at(area, &self.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Content<TuiOut>> Draw<TuiOut> for Modify<T> {
|
|
||||||
fn draw (&self, to: &mut TuiOut) {
|
|
||||||
to.fill_mod(to.area(), self.0, self.1);
|
|
||||||
self.2.draw(to)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Content<TuiOut>> Draw<TuiOut> for Styled<T> {
|
|
||||||
fn draw (&self, to: &mut TuiOut) {
|
|
||||||
to.place(&self.1);
|
|
||||||
// TODO write style over area
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S: BorderStyle> Draw<TuiOut> for Border<S> {
|
|
||||||
fn draw (&self, to: &mut TuiOut) {
|
|
||||||
let Border(enabled, style) = self;
|
|
||||||
if *enabled {
|
|
||||||
let area = to.area();
|
|
||||||
if area.w() > 0 && area.y() > 0 {
|
|
||||||
to.blit(&style.border_nw(), area.x(), area.y(), style.style());
|
|
||||||
to.blit(&style.border_ne(), area.x() + area.w() - 1, area.y(), style.style());
|
|
||||||
to.blit(&style.border_sw(), area.x(), area.y() + area.h() - 1, style.style());
|
|
||||||
to.blit(&style.border_se(), area.x() + area.w() - 1, area.y() + area.h() - 1, style.style());
|
|
||||||
for x in area.x()+1..area.x()+area.w()-1 {
|
|
||||||
to.blit(&style.border_n(), x, area.y(), style.style());
|
|
||||||
to.blit(&style.border_s(), x, area.y() + area.h() - 1, style.style());
|
|
||||||
}
|
|
||||||
for y in area.y()+1..area.y()+area.h()-1 {
|
|
||||||
to.blit(&style.border_w(), area.x(), y, style.style());
|
|
||||||
to.blit(&style.border_e(), area.x() + area.w() - 1, y, style.style());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, T: AsRef<str>> Draw<TuiOut> for TrimString<T> {
|
|
||||||
fn draw (&self, to: &mut TuiOut) { Draw::draw(&self.as_ref(), to) }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: AsRef<str>> Draw<TuiOut> for TrimStringRef<'_, T> {
|
|
||||||
fn draw (&self, target: &mut TuiOut) {
|
|
||||||
let area = target.area();
|
|
||||||
let mut width: u16 = 1;
|
|
||||||
let mut chars = self.1.as_ref().chars();
|
|
||||||
while let Some(c) = chars.next() {
|
|
||||||
if width > self.0 || width > area.w() {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if let Some(cell) = target.buffer.cell_mut(Position {
|
|
||||||
x: area.x() + width - 1,
|
|
||||||
y: area.y()
|
|
||||||
}) {
|
|
||||||
cell.set_char(c);
|
|
||||||
}
|
|
||||||
width += c.width().unwrap_or(0) as u16;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// TUI helper defs.
|
|
||||||
impl Tui {
|
|
||||||
pub const fn fg <T> (color: Color, w: T) -> Foreground<Color, T> { Foreground(color, w) }
|
|
||||||
pub const fn bg <T> (color: Color, w: T) -> Background<Color, T> { Background(color, w) }
|
|
||||||
pub const fn fg_bg <T> (fg: Color, bg: Color, w: T) -> Background<Color, Foreground<Color, T>> { Background(bg, Foreground(fg, w)) }
|
|
||||||
pub const fn modify <T> (enable: bool, modifier: Modifier, w: T) -> Modify<T> { Modify(enable, modifier, w) }
|
|
||||||
pub const fn bold <T> (enable: bool, w: T) -> Modify<T> { Self::modify(enable, Modifier::BOLD, w) }
|
|
||||||
pub const fn border <S, T> (enable: bool, style: S, w: T) -> Bordered<S, T> { Bordered(enable, style, w) }
|
|
||||||
|
|
||||||
pub const fn null () -> Color { Color::Reset }
|
|
||||||
pub const fn red () -> Color { Color::Rgb(255,0, 0) }
|
|
||||||
pub const fn orange () -> Color { Color::Rgb(255,128,0) }
|
|
||||||
pub const fn yellow () -> Color { Color::Rgb(255,255,0) }
|
|
||||||
pub const fn brown () -> Color { Color::Rgb(128,255,0) }
|
|
||||||
pub const fn green () -> Color { Color::Rgb(0,255,0) }
|
|
||||||
pub const fn electric () -> Color { Color::Rgb(0,255,128) }
|
|
||||||
pub const fn g (g: u8) -> Color { Color::Rgb(g, g, g) }
|
|
||||||
//fn bg0 () -> Color { Color::Rgb(20, 20, 20) }
|
|
||||||
//fn bg () -> Color { Color::Rgb(28, 35, 25) }
|
|
||||||
//fn border_bg () -> Color { Color::Rgb(40, 50, 30) }
|
|
||||||
//fn border_fg (f: bool) -> Color { if f { Self::bo1() } else { Self::bo2() } }
|
|
||||||
//fn title_fg (f: bool) -> Color { if f { Self::ti1() } else { Self::ti2() } }
|
|
||||||
//fn separator_fg (_: bool) -> Color { Color::Rgb(0, 0, 0) }
|
|
||||||
//fn mode_bg () -> Color { Color::Rgb(150, 160, 90) }
|
|
||||||
//fn mode_fg () -> Color { Color::Rgb(255, 255, 255) }
|
|
||||||
//fn status_bar_bg () -> Color { Color::Rgb(28, 35, 25) }
|
|
||||||
//fn bo1 () -> Color { Color::Rgb(100, 110, 40) }
|
|
||||||
//fn bo2 () -> Color { Color::Rgb(70, 80, 50) }
|
|
||||||
//fn ti1 () -> Color { Color::Rgb(150, 160, 90) }
|
|
||||||
//fn ti2 () -> Color { Color::Rgb(120, 130, 100) }
|
|
||||||
}
|
|
||||||
|
|
||||||
from!(BigBuffer: |size:(usize, usize)| Self::new(size.0, size.1));
|
|
||||||
from!(ItemTheme: |base: Color| Self::from_tui_color(base));
|
|
||||||
from!(ItemTheme: |base: ItemColor| Self::from_item_color(base));
|
|
||||||
from!(ItemColor: |okhsl: Okhsl<f32>| Self { okhsl, rgb: okhsl_to_rgb(okhsl) });
|
|
||||||
from!(ItemColor: |rgb: Color| Self { rgb, okhsl: rgb_to_okhsl(rgb) });
|
|
||||||
impl_debug!(BigBuffer |self, f| {
|
|
||||||
write!(f, "[BB {}x{} ({})]", self.width, self.height, self.content.len())
|
|
||||||
});
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
/// The `Tui` struct (the *engine*) implements the
|
|
||||||
/// `tengri_input::Input` and `tengri_output::Out` traits.
|
|
||||||
/// At launch, the `Tui` engine spawns two threads, the render thread and the input thread.
|
|
||||||
/// the application may further spawn other threads. All threads communicate using shared ownership:
|
|
||||||
/// `Arc<RwLock<T>>` and `Arc<AtomicT>`. Thus, at launch the engine and application instances are expected to be wrapped in `Arc<RwLock>`.
|
|
||||||
pub struct Tui {
|
|
||||||
pub exited: Arc<AtomicBool>,
|
|
||||||
pub backend: CrosstermBackend<Stdout>,
|
|
||||||
pub buffer: Buffer,
|
|
||||||
pub area: [u16;4],
|
|
||||||
pub perf: PerfModel,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)] pub struct TuiIn {
|
|
||||||
/// Input event
|
|
||||||
pub event: TuiEvent,
|
|
||||||
/// Exit flag
|
|
||||||
pub exited: Arc<AtomicBool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)] pub struct TuiEvent(pub Event);
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)] pub struct TuiKey(
|
|
||||||
pub Option<KeyCode>,
|
|
||||||
pub KeyModifiers
|
|
||||||
);
|
|
||||||
|
|
||||||
#[derive(Default)] pub struct TuiOut {
|
|
||||||
pub buffer: Buffer,
|
|
||||||
pub area: XYWH<u16>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// TUI buffer sized by `usize` instead of `u16`.
|
|
||||||
#[derive(Default)] pub struct BigBuffer {
|
|
||||||
pub width: usize,
|
|
||||||
pub height: usize,
|
|
||||||
pub content: Vec<Cell>
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A color in OKHSL and RGB representations.
|
|
||||||
#[derive(Debug, Default, Copy, Clone, PartialEq)] pub struct ItemColor {
|
|
||||||
pub okhsl: Okhsl<f32>,
|
|
||||||
pub rgb: Color,
|
|
||||||
}
|
|
||||||
/// A color in OKHSL and RGB with lighter and darker variants.
|
|
||||||
#[derive(Debug, Default, Copy, Clone, PartialEq)] pub struct ItemTheme {
|
|
||||||
pub base: ItemColor,
|
|
||||||
pub light: ItemColor,
|
|
||||||
pub lighter: ItemColor,
|
|
||||||
pub lightest: ItemColor,
|
|
||||||
pub dark: ItemColor,
|
|
||||||
pub darker: ItemColor,
|
|
||||||
pub darkest: ItemColor,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Modify<T>(pub bool, pub Modifier, pub T);
|
|
||||||
|
|
||||||
pub struct Styled<T>(pub Option<Style>, pub T);
|
|
||||||
|
|
||||||
/// Displays an owned [str]-like with fixed maximum width.
|
|
||||||
///
|
|
||||||
/// Width is computed using [unicode_width].
|
|
||||||
pub struct TrimString<T: AsRef<str>>(pub u16, pub T);
|
|
||||||
|
|
||||||
/// Displays a borrowed [str]-like with fixed maximum width
|
|
||||||
///
|
|
||||||
/// Width is computed using [unicode_width].
|
|
||||||
pub struct TrimStringRef<'a, T: AsRef<str>>(pub u16, pub &'a T);
|
|
||||||
|
|
||||||
/// Thunks can be natural error boundaries!
|
|
||||||
pub struct ErrorBoundary<O: Out, T: Draw<O>>(
|
|
||||||
pub std::marker::PhantomData<O>,
|
|
||||||
pub Perhaps<T>
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Repeat a string, e.g. for background
|
|
||||||
pub enum Repeat<'a> {
|
|
||||||
X(&'a str),
|
|
||||||
Y(&'a str),
|
|
||||||
XY(&'a str)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Scroll indicator
|
|
||||||
pub enum Scrollbar {
|
|
||||||
/// Horizontal scrollbar
|
|
||||||
X { offset: usize, length: usize, total: usize, },
|
|
||||||
/// Vertical scrollbar
|
|
||||||
Y { offset: usize, length: usize, total: usize, }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A cell that takes up 3 rows on its own,
|
|
||||||
/// but stacks, giving (N+1)*2 rows per N cells.
|
|
||||||
pub struct Phat<T> {
|
|
||||||
pub width: u16,
|
|
||||||
pub height: u16,
|
|
||||||
pub content: T,
|
|
||||||
pub colors: [Color;4],
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +1,5 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
#[test] fn test_tui_engine () -> Usually<()> {
|
|
||||||
//use std::sync::{Arc, RwLock};
|
|
||||||
struct TestComponent(String);
|
|
||||||
impl Draw<TuiOut> for TestComponent {
|
|
||||||
fn draw (&self, _to: &mut TuiOut) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Handle<TuiIn> for TestComponent {
|
|
||||||
fn handle (&mut self, _from: &TuiIn) -> Perhaps<bool> {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let engine = Tui::run(false, TestComponent("hello world".into()))?;
|
|
||||||
engine.read().unwrap().exited.store(true, std::sync::atomic::Ordering::Relaxed);
|
|
||||||
//engine.run(&state)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
//#[test] fn test_parse_key () {
|
//#[test] fn test_parse_key () {
|
||||||
////use KeyModifiers as Mods;
|
////use KeyModifiers as Mods;
|
||||||
//let _test = |x: &str, y|assert_eq!(KeyMatcher::new(x).build(), Some(Event::Key(y)));
|
//let _test = |x: &str, y|assert_eq!(KeyMatcher::new(x).build(), Some(Event::Key(y)));
|
||||||
|
|
|
||||||
|
|
@ -1,107 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
pub trait TuiDraw = Draw<TuiOut>;
|
|
||||||
|
|
||||||
pub trait TuiLayout = Layout<TuiOut>;
|
|
||||||
|
|
||||||
pub trait TuiContent = Content<TuiOut>;
|
|
||||||
|
|
||||||
pub trait TuiHandle = Handle<TuiIn>;
|
|
||||||
|
|
||||||
pub trait TuiWidget = TuiDraw + TuiHandle;
|
|
||||||
|
|
||||||
pub trait HasColor { fn color (&self) -> ItemColor; }
|
|
||||||
|
|
||||||
pub trait BorderStyle: Content<TuiOut> + Copy {
|
|
||||||
fn enabled (&self) -> bool;
|
|
||||||
fn enclose (self, w: impl Content<TuiOut>) -> impl Content<TuiOut> {
|
|
||||||
Bsp::b(Fill::XY(Border(self.enabled(), self)), w)
|
|
||||||
}
|
|
||||||
fn enclose2 (self, w: impl Content<TuiOut>) -> impl Content<TuiOut> {
|
|
||||||
Bsp::b(Pad::XY(1, 1, Fill::XY(Border(self.enabled(), self))), w)
|
|
||||||
}
|
|
||||||
fn enclose_bg (self, w: impl Content<TuiOut>) -> impl Content<TuiOut> {
|
|
||||||
Tui::bg(self.style().unwrap().bg.unwrap_or(Color::Reset),
|
|
||||||
Bsp::b(Fill::XY(Border(self.enabled(), self)), w))
|
|
||||||
}
|
|
||||||
const NW: &'static str = "";
|
|
||||||
const N: &'static str = "";
|
|
||||||
const NE: &'static str = "";
|
|
||||||
const E: &'static str = "";
|
|
||||||
const SE: &'static str = "";
|
|
||||||
const S: &'static str = "";
|
|
||||||
const SW: &'static str = "";
|
|
||||||
const W: &'static str = "";
|
|
||||||
|
|
||||||
const N0: &'static str = "";
|
|
||||||
const S0: &'static str = "";
|
|
||||||
const W0: &'static str = "";
|
|
||||||
const E0: &'static str = "";
|
|
||||||
|
|
||||||
fn border_n (&self) -> &str { Self::N }
|
|
||||||
fn border_s (&self) -> &str { Self::S }
|
|
||||||
fn border_e (&self) -> &str { Self::E }
|
|
||||||
fn border_w (&self) -> &str { Self::W }
|
|
||||||
fn border_nw (&self) -> &str { Self::NW }
|
|
||||||
fn border_ne (&self) -> &str { Self::NE }
|
|
||||||
fn border_sw (&self) -> &str { Self::SW }
|
|
||||||
fn border_se (&self) -> &str { Self::SE }
|
|
||||||
#[inline] fn draw <'a> (
|
|
||||||
&self, to: &mut TuiOut
|
|
||||||
) -> Usually<()> {
|
|
||||||
if self.enabled() {
|
|
||||||
self.draw_horizontal(to, None)?;
|
|
||||||
self.draw_vertical(to, None)?;
|
|
||||||
self.draw_corners(to, None)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
#[inline] fn draw_horizontal (
|
|
||||||
&self, to: &mut TuiOut, style: Option<Style>
|
|
||||||
) -> Usually<XYWH<u16>> {
|
|
||||||
let area = to.area();
|
|
||||||
let style = style.or_else(||self.style_horizontal());
|
|
||||||
let [x, x2, y, y2] = area.lrtb();
|
|
||||||
for x in x..x2.saturating_sub(1) {
|
|
||||||
to.blit(&Self::N, x, y, style);
|
|
||||||
to.blit(&Self::S, x, y2.saturating_sub(1), style)
|
|
||||||
}
|
|
||||||
Ok(area)
|
|
||||||
}
|
|
||||||
#[inline] fn draw_vertical (
|
|
||||||
&self, to: &mut TuiOut, style: Option<Style>
|
|
||||||
) -> Usually<XYWH<u16>> {
|
|
||||||
let area = to.area();
|
|
||||||
let style = style.or_else(||self.style_vertical());
|
|
||||||
let [x, x2, y, y2] = area.lrtb();
|
|
||||||
let h = y2 - y;
|
|
||||||
if h > 1 {
|
|
||||||
for y in y..y2.saturating_sub(1) {
|
|
||||||
to.blit(&Self::W, x, y, style);
|
|
||||||
to.blit(&Self::E, x2.saturating_sub(1), y, style);
|
|
||||||
}
|
|
||||||
} else if h > 0 {
|
|
||||||
to.blit(&Self::W0, x, y, style);
|
|
||||||
to.blit(&Self::E0, x2.saturating_sub(1), y, style);
|
|
||||||
}
|
|
||||||
Ok(area)
|
|
||||||
}
|
|
||||||
#[inline] fn draw_corners (
|
|
||||||
&self, to: &mut TuiOut, style: Option<Style>
|
|
||||||
) -> Usually<XYWH<u16>> {
|
|
||||||
let area = to.area();
|
|
||||||
let style = style.or_else(||self.style_corners());
|
|
||||||
let XYWH(x, y, width, height) = area;
|
|
||||||
if width > 1 && height > 1 {
|
|
||||||
to.blit(&Self::NW, x, y, style);
|
|
||||||
to.blit(&Self::NE, x + width - 1, y, style);
|
|
||||||
to.blit(&Self::SW, x, y + height - 1, style);
|
|
||||||
to.blit(&Self::SE, x + width - 1, y + height - 1, style);
|
|
||||||
}
|
|
||||||
Ok(area)
|
|
||||||
}
|
|
||||||
#[inline] fn style (&self) -> Option<Style> { None }
|
|
||||||
#[inline] fn style_horizontal (&self) -> Option<Style> { self.style() }
|
|
||||||
#[inline] fn style_vertical (&self) -> Option<Style> { self.style() }
|
|
||||||
#[inline] fn style_corners (&self) -> Option<Style> { self.style() }
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue