diff --git a/Cargo.toml b/Cargo.toml index 7619d5c..a3a6d54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,9 +9,6 @@ lto = false resolver = "2" members = [ "./tengri", - "./input", - "./output", - "./tui", "./proc", ] diff --git a/bacon.toml b/bacon.toml index 1fb14f2..bbcf195 100644 --- a/bacon.toml +++ b/bacon.toml @@ -14,26 +14,28 @@ l = "job:clippy" [jobs.check] command = ["cargo", "check"] need_stdout = false -watch = ["core","dsl","editor","input","output","proc","tengri","tui"] +watch = ["tengri"] [jobs.clippy-all] command = ["cargo", "clippy"] need_stdout = false -watch = ["tek", "deps"] +watch = ["tengri"] [jobs.test] command = ["cargo", "test"] need_stdout = true -watch = ["tek", "deps"] +watch = ["tengri"] [jobs.doc] command = ["cargo", "doc", "--no-deps"] need_stdout = false +watch = ["tengri"] [jobs.doc-open] command = ["cargo", "doc", "--no-deps", "--open"] need_stdout = false on_success = "back" # so that we don't open the browser at each change +watch = ["tengri"] [skin] status_fg = 15 diff --git a/input/Cargo.lock b/input/Cargo.lock deleted file mode 100644 index 7747441..0000000 --- a/input/Cargo.lock +++ /dev/null @@ -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" diff --git a/input/Cargo.toml b/input/Cargo.toml deleted file mode 100644 index 506c636..0000000 --- a/input/Cargo.toml +++ /dev/null @@ -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" } diff --git a/input/README.md b/input/README.md deleted file mode 100644 index 2fa3ef8..0000000 --- a/input/README.md +++ /dev/null @@ -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 diff --git a/input/input.rs b/input/input.rs deleted file mode 100644 index aa72db1..0000000 --- a/input/input.rs +++ /dev/null @@ -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> 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 { - fn handle (&mut self, _input: &E) -> Perhaps { - Ok(None) - } -}); - -pub trait Command: Send + Sync + Sized { - fn execute (&self, state: &mut S) -> Perhaps; - fn delegate (&self, state: &mut S, wrap: impl Fn(Self)->T) -> Perhaps - where Self: Sized - { - Ok(self.execute(state)?.map(wrap)) - } -} - -impl> Command for Option { - fn execute (&self, _: &mut S) -> Perhaps { - Ok(None) - } - fn delegate (&self, _: &mut S, _: impl Fn(Self)->U) -> Perhaps - 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 { - 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 { - 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 ::tengri::input::Handle for $State { - fn handle (&mut $self, $input: &E) -> Perhaps { - $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 - } - } - } -} diff --git a/input/input_test.rs b/input/input_test.rs deleted file mode 100644 index 24576db..0000000 --- a/input/input_test.rs +++ /dev/null @@ -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(()) -//} - diff --git a/output/.scratch.rs b/output/.scratch.rs deleted file mode 100644 index 0e5db23..0000000 --- a/output/.scratch.rs +++ /dev/null @@ -1,351 +0,0 @@ - - -/////////////////////////////////////////////////////////////////////////////// - - - ///// The syntagm `(when :condition :content)` corresponds to a [When] layout element. - //impl FromDsl for When where bool: FromDsl, A: FromDsl { - //fn try_provide (state: &S, source: &DslVal) -> Perhaps { - //source.exp_match("when", |_, tail|Ok(Some(Self( - //FromDsl::::provide(state, - //tail.nth(0, ||"no condition".into())?, ||"no condition".into())?, - //FromDsl::::provide(state, - //tail.nth(1, ||"no content".into())?, ||"no content".into())?, - //)))) - //} - //} - ///// The syntagm `(either :condition :content1 :content2)` corresponds to an [Either] layout element. - //impl FromDsl for Either where S: Eval + Eval + Eval { - //fn try_provide (state: &S, source: &DslVal) -> Perhaps { - //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 FromDsl for Align where S: Eval, A> { - //fn try_provide (state: &S, source: &DslVal) -> Perhaps { - //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 FromDsl for Bsp where S: Eval, A> + Eval, B> { - //fn try_provide (state: &S, source: &DslVal) -> Perhaps { - //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|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|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))> Layout for Stack<'x, E, F1> { - //fn layout (&self, to: E::Area) -> E::Area { - //let state = StackLayoutState::::new(self.direction, to); - //(self.callback)(&mut |component: &dyn Layout|{ - //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))> Draw for Stack<'x, E, F1> { - //fn draw (&self, to: &mut E) { - //let state = StackLayoutState::::new(self.direction, to.area()); - //let to = Rc::new(RefCell::new(to)); - //(self.callback)(&mut |component: &dyn Draw|{ - //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 { - //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 StackLayoutState { - //fn new (direction: Direction, area: E::Area) -> std::rc::Rc> { - //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)) + 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 for Stack<'a, E, F1> where - ////E: Out, F1: Fn(&mut dyn FnMut(&'a dyn Draw)) + Send + Sync, -////{ - ////fn layout (&self, to: E::Area) -> E::Area { - ////let state = StackLayoutState::::new(self.direction, to); - ////let mut adder = { - ////let state = state.clone(); - ////move|component: &dyn Draw|{ - ////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::::new(self.direction, to.area()); - ////let mut adder = { - ////let state = state.clone(); - ////move|component: &dyn Draw|{ - ////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 Content for Foo {} - //fn _make_map + Send + Sync> (data: &Vec) -> impl Draw { - //Map::new(||data.iter(), |_foo, _index|{}) - //} - //let _data = vec![Foo, Foo, Foo]; - ////let map = make_map(&data); -//} diff --git a/output/Cargo.lock b/output/Cargo.lock deleted file mode 100644 index 8b1fd6a..0000000 --- a/output/Cargo.lock +++ /dev/null @@ -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", -] diff --git a/output/Cargo.toml b/output/Cargo.toml deleted file mode 100644 index dedb56e..0000000 --- a/output/Cargo.toml +++ /dev/null @@ -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 } diff --git a/output/README.md b/output/README.md deleted file mode 100644 index e9669a4..0000000 --- a/output/README.md +++ /dev/null @@ -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) diff --git a/output/proptest-regressions/area.txt b/output/proptest-regressions/area.txt deleted file mode 100644 index 1c957a5..0000000 --- a/output/proptest-regressions/area.txt +++ /dev/null @@ -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 diff --git a/output/proptest-regressions/direction.txt b/output/proptest-regressions/direction.txt deleted file mode 100644 index 76b3fac..0000000 --- a/output/proptest-regressions/direction.txt +++ /dev/null @@ -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 diff --git a/output/proptest-regressions/op_transform.txt b/output/proptest-regressions/op_transform.txt deleted file mode 100644 index 5981536..0000000 --- a/output/proptest-regressions/op_transform.txt +++ /dev/null @@ -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 diff --git a/output/src/lib.rs b/output/src/lib.rs deleted file mode 100644 index b5b7023..0000000 --- a/output/src/lib.rs +++ /dev/null @@ -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 $T { - #[inline] pub const fn inner (&self) -> &A { - match self { Self::X(c) | Self::Y(c) | Self::XY(c) => c } - } - } - impl> Draw for $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 $T { - #[inline] pub const fn inner (&self) -> &A { - match self { Self::X(_, c) | Self::Y(_, c) | Self::XY(_, _, c) => c, } - } - } - impl> Draw for $T { - fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) } - } - impl $T { - #[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 $T { - #[inline] pub const fn inner (&self) -> &A { - match self { Self::X(_, c) | Self::Y(_, c) | Self::XY(_, _, c) => c, } - } - } - impl> Draw for $T { - fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) } - } - impl $T { - #[inline] pub const fn dx (&self) -> Option { - match self { Self::X(x, _) | Self::XY(x, ..) => Some(*x), _ => None } - } - #[inline] pub const fn dy (&self) -> Option { - 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> Layout for $T { - fn layout_x (&self, area: XYWH) -> O::Unit { area.x().$method(self.dx()) } - fn layout_y (&self, area: XYWH) -> 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>>>, I, F, fn(A, usize)->B - > where - O: Out, - B: Draw, - I: Iterator + 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>>, - I, F, - impl Fn(A, usize)->Push>> + 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 where - S: View - + 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 Perhapss 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) -} diff --git a/output/src/out_tests.rs b/output/src/out_tests.rs deleted file mode 100644 index 72bdabe..0000000 --- a/output/src/out_tests.rs +++ /dev/null @@ -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 = XYWH::zero(); - //let _: XYWH = XYWH::from_position([a, b]); - //let _: XYWH = XYWH::from_size([a, b]); - let area: XYWH = 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]), - //); - } -} diff --git a/output/src/out_traits.rs b/output/src/out_traits.rs deleted file mode 100644 index 63176ab..0000000 --- a/output/src/out_traits.rs +++ /dev/null @@ -1,153 +0,0 @@ -use crate::*; - -/// Drawing target. -/// -/// ``` -/// use tengri::output::*; -/// struct TestOut(XYWH); -/// impl Out for TestOut { -/// type Unit = u16; -/// fn area (&self) -> XYWH { self.0 } -/// fn area_mut (&mut self) -> &mut XYWH { &mut self.0 } -/// fn place_at + ?Sized> (&mut self, area: XYWH, _: &T) { -/// println!("place_at: {area:?}"); -/// () -/// } -/// } -/// impl Draw 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; - /// Mutable pointer to area. - fn area_mut (&mut self) -> &mut XYWH; - /// Render drawable in area specified by `area` - fn place_at <'t, T: Draw + ?Sized> (&mut self, area: XYWH, content: &'t T); - /// Render drawable in area specified by `T::layout(self.area())` - #[inline] fn place <'t, T: Content + ?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 - + Sub - + Mul - + Div - + Ord + PartialEq + Eq - + Debug + Display + Default - + From + Into - + Into - + Into -{ - fn plus (self, other: Self) -> Self; - fn minus (self, other: Self) -> Self { - if self >= other { self - other } else { 0.into() } - } - fn atomic (self) -> AtomicUsize { - AtomicUsize::new(self.into()) - } - fn zero () -> Self { - 0.into() - } -} - -/// Drawable with dynamic dispatch. -pub trait Draw { - 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 { - fn view_expr <'a> (&'a self, _output: &mut O, expr: &'a impl Expression) -> Usually { - Err(format!("View::view_expr: no exprs defined: {expr:?}").into()) - } - fn view_word <'a> (&'a self, _output: &mut O, word: &'a impl Symbol) -> Usually { - Err(format!("View::view_word: no words defined: {word:?}").into()) - } - fn view <'a> (&'a self, output: &mut O, dsl: &'a impl Language) -> Usually { - 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: Sized {} - -/// Drawable area of display. -pub trait Layout { - fn layout_x (&self, to: XYWH) -> O::Unit { to.x() } - fn layout_y (&self, to: XYWH) -> O::Unit { to.y() } - fn layout_w_min (&self, _t: XYWH) -> O::Unit { 0.into() } - fn layout_w_max (&self, to: XYWH) -> O::Unit { to.w() } - fn layout_w (&self, to: XYWH) -> 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 { 0.into() } - fn layout_h_max (&self, to: XYWH) -> O::Unit { to.h() } - fn layout_h (&self, to: XYWH) -> O::Unit { to.h().max(self.layout_h_min(to)).min(self.layout_h_max(to)) } - fn layout (&self, to: XYWH) -> XYWH { - XYWH(self.layout_x(to), self.layout_y(to), self.layout_w(to), self.layout_h(to)) - } -} - -pub trait HasContent { - fn content (&self) -> impl Content; -} - -// TODO DOCUMENTME -pub trait Content: Draw + Layout {} - -// Something that has an origin point (X, Y). -pub trait HasXY { - fn x (&self) -> N; - fn y (&self) -> N; - fn xy (&self) -> XY { XY(self.x(), self.y()) } -} - -// Something that has a size (W, H). -pub trait HasWH { - fn w (&self) -> N; - fn h (&self) -> N; - fn wh (&self) -> WH { WH(self.w(), self.h()) } -} - -// Something that has a 2D bounding box (X, Y, W, H). -// -// FIXME: The other way around? -pub trait HasXYWH: HasXY + HasWH { - fn x2 (&self) -> N { self.x().plus(self.w()) } - fn y2 (&self) -> N { self.y().plus(self.h()) } - fn xywh (&self) -> XYWH { 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 { - fn measure (&self) -> &Measure; - 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; -} diff --git a/tengri/.scratch.rs b/tengri/.scratch.rs new file mode 100644 index 0000000..55af234 --- /dev/null +++ b/tengri/.scratch.rs @@ -0,0 +1,883 @@ + ///// The syntagm `(when :condition :content)` corresponds to a [When] layout element. + //impl FromDsl for When where bool: FromDsl, A: FromDsl { + //fn try_provide (state: &S, source: &DslVal) -> Perhaps { + //source.exp_match("when", |_, tail|Ok(Some(Self( + //FromDsl::::provide(state, + //tail.nth(0, ||"no condition".into())?, ||"no condition".into())?, + //FromDsl::::provide(state, + //tail.nth(1, ||"no content".into())?, ||"no content".into())?, + //)))) + //} + //} + ///// The syntagm `(either :condition :content1 :content2)` corresponds to an [Either] layout element. + //impl FromDsl for Either where S: Eval + Eval + Eval { + //fn try_provide (state: &S, source: &DslVal) -> Perhaps { + //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 FromDsl for Align where S: Eval, A> { + //fn try_provide (state: &S, source: &DslVal) -> Perhaps { + //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 FromDsl for Bsp where S: Eval, A> + Eval, B> { + //fn try_provide (state: &S, source: &DslVal) -> Perhaps { + //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|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|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))> Layout for Stack<'x, E, F1> { + //fn layout (&self, to: E::Area) -> E::Area { + //let state = StackLayoutState::::new(self.direction, to); + //(self.callback)(&mut |component: &dyn Layout|{ + //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))> Draw for Stack<'x, E, F1> { + //fn draw (&self, to: &mut E) { + //let state = StackLayoutState::::new(self.direction, to.area()); + //let to = Rc::new(RefCell::new(to)); + //(self.callback)(&mut |component: &dyn Draw|{ + //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 { + //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 StackLayoutState { + //fn new (direction: Direction, area: E::Area) -> std::rc::Rc> { + //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)) + 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 for Stack<'a, E, F1> where + ////E: Out, F1: Fn(&mut dyn FnMut(&'a dyn Draw)) + Send + Sync, +////{ + ////fn layout (&self, to: E::Area) -> E::Area { + ////let state = StackLayoutState::::new(self.direction, to); + ////let mut adder = { + ////let state = state.clone(); + ////move|component: &dyn Draw|{ + ////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::::new(self.direction, to.area()); + ////let mut adder = { + ////let state = state.clone(); + ////move|component: &dyn Draw|{ + ////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 Content for Foo {} + //fn _make_map + Send + Sync> (data: &Vec) -> impl Draw { + //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 { None } } + //struct Test { keys: InputMap } + + //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 { + //Ok(None) + //} + //fn do_thing_arg (_state: &mut Test, _arg: usize) -> Perhaps { + //Ok(None) + //} + //fn do_sub (state: &mut Test, command: TestSubcommand) -> Perhaps { + //Ok(command.execute(state)?.map(|command|Self::DoSub { command })) + //} + //} + + //#[tengri_proc::command(Test)] + //impl TestSubcommand { + //fn do_other_thing (_state: &mut Test) -> Perhaps { + //Ok(None) + //} + //fn do_other_thing_arg (_state: &mut Test, _arg: usize) -> Perhaps { + //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 { + Focused(T), + Entered(T), +} + +impl FocusState { + 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 { + Up, + Down, + Left, + Right, + Next, + Prev, + Enter, + Exit, + Set(T) +} + +impl Command for FocusCommand { + fn execute (self, state: &mut F) -> Perhaps> { + 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 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 { + fn wrap > (self, focus: T, content: &'_ W) -> impl Draw + '_; +} + +pub fn to_focus_command (input: &TuiIn) -> Option> { + 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> { + pub menus: Vec>, + pub index: usize, +} +impl> MenuBar { + pub fn new () -> Self { Self { menus: vec![], index: 0 } } + pub fn add (mut self, menu: Menu) -> Self { + self.menus.push(menu); + self + } +} +pub struct Menu> { + pub title: Arc, + pub items: Vec>, + pub index: Option, +} +impl> Menu { + pub fn new (title: impl AsRef) -> Self { + Self { + title: title.as_ref().to_string(), + items: vec![], + index: None, + } + } + pub fn add (mut self, item: MenuItem) -> 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> { + /// Unused. + __(PhantomData, PhantomData), + /// 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> MenuItem { + 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> Content for Result> { + //fn content (&self) -> impl Draw + '_ { + //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> Draw for Result> { + //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 { + //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 + //} diff --git a/tengri/Cargo.toml b/tengri/Cargo.toml index e67c76e..f5927cb 100644 --- a/tengri/Cargo.toml +++ b/tengri/Cargo.toml @@ -6,21 +6,33 @@ version = { workspace = true } [features] default = [ "input", "output", "tui" ] -input = [ "tengri_input" ] -output = [ "tengri_output" ] -tui = [ "tengri_tui" ] -dsl = [ "tengri_output/dsl", "tengri_tui/dsl" ] +bumpalo = [ "dep:bumpalo" ] +input = [ ] +output = [ ] +tui = [ ] +dsl = [ ] [dependencies] +atomic_float = { workspace = true } +better-panic = { workspace = true } +bumpalo = { workspace = true, optional = true } +crossterm = { workspace = true } dizzle = { workspace = true } -tengri_input = { workspace = true, optional = true } -tengri_output = { workspace = true, optional = true } -tengri_tui = { workspace = true, optional = true } +palette = { workspace = true } +quanta = { workspace = true } +rand = { workspace = true } +ratatui = { workspace = true } +unicode-width = { workspace = true } [dev-dependencies] -tengri_proc = { workspace = true } -tengri = { workspace = true, features = [ "dsl" ] } -crossterm = { workspace = true } +tengri_proc = { workspace = true } +tengri = { workspace = true, features = [ "dsl" ] } +crossterm = { workspace = true } +proptest = { workspace = true } +proptest-derive = { workspace = true } + +[lib] +path = "tengri.rs" [target.'cfg(target_os = "linux")'] rustflags = ["-C", "link-arg=-fuse-ld=mold"] diff --git a/tengri/README.md b/tengri/README.md index 962f196..0aec24e 100644 --- a/tengri/README.md +++ b/tengri/README.md @@ -15,3 +15,35 @@ as well as: * [***tengri***](./tengri), the top-level reexport crate 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) diff --git a/tengri/src/lib.rs b/tengri/src/lib.rs deleted file mode 100644 index 5b12960..0000000 --- a/tengri/src/lib.rs +++ /dev/null @@ -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 { None } } - //struct Test { keys: InputMap } - - //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 { - //Ok(None) - //} - //fn do_thing_arg (_state: &mut Test, _arg: usize) -> Perhaps { - //Ok(None) - //} - //fn do_sub (state: &mut Test, command: TestSubcommand) -> Perhaps { - //Ok(command.execute(state)?.map(|command|Self::DoSub { command })) - //} - //} - - //#[tengri_proc::command(Test)] - //impl TestSubcommand { - //fn do_other_thing (_state: &mut Test) -> Perhaps { - //Ok(None) - //} - //fn do_other_thing_arg (_state: &mut Test, _arg: usize) -> Perhaps { - //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)); - //} - -} diff --git a/tengri/tengri.rs b/tengri/tengri.rs new file mode 100644 index 0000000..cb21ece --- /dev/null +++ b/tengri/tengri.rs @@ -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 where + S: View + + 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 Perhapss 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 { + 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 { + 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 ::tengri::Handle for $State { + fn handle (&mut $self, $input: &E) -> Perhaps { + $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 for $T {} + impl Draw 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 + Handle + 'static> ( + join: bool, + state: &Arc> +) -> Usually>> { + 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 ( + backend: &mut CrosstermBackend +) -> 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 (backend: &mut CrosstermBackend) -> Usually<()> { + stdout().execute(LeaveAlternateScreen)?; + backend.show_cursor()?; + disable_raw_mode().map_err(Into::into) +} + +pub fn tui_resized ( + backend: &mut CrosstermBackend, + 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, + 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, 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 + Send + Sync + 'static> ( + engine: Arc>, state: &Arc>, timer: Duration +) -> Result, 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 + Send + Sync + 'static> ( + engine: Arc>, state: &Arc>, 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 where + S: View + + 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::::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::::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 { + 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, label: impl Content, editing: bool) -> impl Content { + 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, label: impl Content, value: impl Content, editing: bool, +) -> impl Content { + 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