diff --git a/Cargo.toml b/Cargo.toml index a3a6d54..7619d5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,9 @@ lto = false resolver = "2" members = [ "./tengri", + "./input", + "./output", + "./tui", "./proc", ] diff --git a/bacon.toml b/bacon.toml index bbcf195..1fb14f2 100644 --- a/bacon.toml +++ b/bacon.toml @@ -14,28 +14,26 @@ l = "job:clippy" [jobs.check] command = ["cargo", "check"] need_stdout = false -watch = ["tengri"] +watch = ["core","dsl","editor","input","output","proc","tengri","tui"] [jobs.clippy-all] command = ["cargo", "clippy"] need_stdout = false -watch = ["tengri"] +watch = ["tek", "deps"] [jobs.test] command = ["cargo", "test"] need_stdout = true -watch = ["tengri"] +watch = ["tek", "deps"] [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 new file mode 100644 index 0000000..7747441 --- /dev/null +++ b/input/Cargo.lock @@ -0,0 +1,7 @@ +# 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 new file mode 100644 index 0000000..506c636 --- /dev/null +++ b/input/Cargo.toml @@ -0,0 +1,14 @@ +[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 new file mode 100644 index 0000000..2fa3ef8 --- /dev/null +++ b/input/README.md @@ -0,0 +1,6 @@ +***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 new file mode 100644 index 0000000..aa72db1 --- /dev/null +++ b/input/input.rs @@ -0,0 +1,125 @@ +#![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 new file mode 100644 index 0000000..24576db --- /dev/null +++ b/input/input_test.rs @@ -0,0 +1,28 @@ +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 new file mode 100644 index 0000000..0e5db23 --- /dev/null +++ b/output/.scratch.rs @@ -0,0 +1,351 @@ + + +/////////////////////////////////////////////////////////////////////////////// + + + ///// 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 new file mode 100644 index 0000000..8b1fd6a --- /dev/null +++ b/output/Cargo.lock @@ -0,0 +1,14 @@ +# 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 new file mode 100644 index 0000000..dedb56e --- /dev/null +++ b/output/Cargo.toml @@ -0,0 +1,21 @@ +[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 new file mode 100644 index 0000000..e9669a4 --- /dev/null +++ b/output/README.md @@ -0,0 +1,20 @@ +***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 new file mode 100644 index 0000000..1c957a5 --- /dev/null +++ b/output/proptest-regressions/area.txt @@ -0,0 +1,7 @@ +# 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 new file mode 100644 index 0000000..76b3fac --- /dev/null +++ b/output/proptest-regressions/direction.txt @@ -0,0 +1,7 @@ +# 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 new file mode 100644 index 0000000..5981536 --- /dev/null +++ b/output/proptest-regressions/op_transform.txt @@ -0,0 +1,10 @@ +# 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 new file mode 100644 index 0000000..b5b7023 --- /dev/null +++ b/output/src/lib.rs @@ -0,0 +1,293 @@ +#![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/tengri/tengri_impl.rs b/output/src/out_impls.rs similarity index 53% rename from tengri/tengri_impl.rs rename to output/src/out_impls.rs index 3edc2b6..f23cda2 100644 --- a/tengri/tengri_impl.rs +++ b/output/src/out_impls.rs @@ -1,116 +1,5 @@ use crate::*; use Alignment::*; -use Direction::*; -use unicode_width::{UnicodeWidthStr, UnicodeWidthChar}; -use rand::{thread_rng, distributions::uniform::UniformSampler}; - -macro_rules! layout_op_xy ( - // Variant for layout ops that take no coordinates - (0: $T: ident) => { - impl $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)))) - } - } - } - } -}); - - -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) - } -} - impl Coord for u16 { fn plus (self, other: Self) -> Self { self.saturating_add(other) } @@ -855,6 +744,30 @@ impl<'a, O, A, B, I, F, G> Draw for Map where } } +#[inline] pub fn map_south( + item_offset: O::Unit, + item_height: O::Unit, + item: impl Content +) -> impl Content { + Push::Y(item_offset, Fixed::Y(item_height, Fill::X(item))) +} + +#[inline] pub fn map_south_west( + item_offset: O::Unit, + item_height: O::Unit, + item: impl Content +) -> impl Content { + Push::Y(item_offset, Align::nw(Fixed::Y(item_height, Fill::X(item)))) +} + +#[inline] pub fn map_east( + item_offset: O::Unit, + item_width: O::Unit, + item: impl Content +) -> impl Content { + Push::X(item_offset, Align::w(Fixed::X(item_width, Fill::Y(item)))) +} + impl Tryptich<(), (), ()> { pub fn center (h: u16) -> Self { Self { h, top: false, left: (0, ()), middle: (0, ()), right: (0, ()) } @@ -981,637 +894,3 @@ impl PerfModel { } } } - -impl Tui { - /// Create and launch a terminal user interface. - pub fn run (join: bool, state: T) -> Usually>> where - T: Handle + Draw + Send + Sync + 'static - { - tui_run(join, &Arc::new(RwLock::new(state))) - } - /// True if done - pub fn exited (&self) -> bool { self.exited.fetch_and(true, Relaxed) } - /// Prepare before run - pub fn setup (&mut self) -> Usually<()> { tui_setup(&mut self.backend) } - /// Clean up after run - pub fn teardown (&mut self) -> Usually<()> { tui_teardown(&mut self.backend) } - /// Apply changes to the display buffer. - pub fn flip (&mut self, mut buffer: Buffer, size: ratatui::prelude::Rect) -> Buffer { - tui_resized(&mut self.backend, &mut self.buffer, size); - tui_redrawn(&mut self.backend, &mut self.buffer, &mut buffer); - buffer - } -} -impl Input for TuiIn { - type Event = TuiEvent; - type Handled = bool; - fn event (&self) -> &TuiEvent { &self.event } - fn done (&self) { self.exited.store(true, Relaxed); } - fn is_done (&self) -> bool { self.exited.fetch_and(true, Relaxed) } -} -impl Ord for TuiEvent { - fn cmp (&self, other: &Self) -> std::cmp::Ordering { - self.partial_cmp(other) - .unwrap_or_else(||format!("{:?}", self).cmp(&format!("{other:?}"))) // FIXME perf - } -} -impl TuiEvent { - pub fn from_crossterm (event: Event) -> Self { Self(event) } - #[cfg(feature = "dsl")] pub fn from_dsl (dsl: impl Language) -> Perhaps { - Ok(TuiKey::from_dsl(dsl)?.to_crossterm().map(Self)) - } -} -impl TuiKey { - const SPLIT: char = '/'; - #[cfg(feature = "dsl")] pub fn from_dsl (dsl: impl Language) -> Usually { - if let Some(word) = dsl.word()? { - let word = word.trim(); - Ok(if word == ":char" { - Self(None, KeyModifiers::NONE) - } else if word.chars().nth(0) == Some('@') { - let mut key = None; - let mut modifiers = KeyModifiers::NONE; - let mut tokens = word[1..].split(Self::SPLIT).peekable(); - while let Some(token) = tokens.next() { - if tokens.peek().is_some() { - match token { - "ctrl" | "Ctrl" | "c" | "C" => modifiers |= KeyModifiers::CONTROL, - "alt" | "Alt" | "m" | "M" => modifiers |= KeyModifiers::ALT, - "shift" | "Shift" | "s" | "S" => { - modifiers |= KeyModifiers::SHIFT; - // + TODO normalize character case, BackTab, etc. - }, - _ => panic!("unknown modifier {token}"), - } - } else { - key = if token.len() == 1 { - Some(KeyCode::Char(token.chars().next().unwrap())) - } else { - Some(named_key(token).unwrap_or_else(||panic!("unknown character {token}"))) - } - } - } - Self(key, modifiers) - } else { - return Err(format!("TuiKey: unexpected: {word}").into()) - }) - } else { - return Err(format!("TuiKey: unspecified").into()) - } - } - pub fn to_crossterm (&self) -> Option { - self.0.map(|code|Event::Key(KeyEvent { - code, - modifiers: self.1, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - })) - } -} -impl Out for TuiOut { - type Unit = u16; - #[inline] fn area (&self) -> XYWH { self.area } - #[inline] fn area_mut (&mut self) -> &mut XYWH { &mut self.area } - #[inline] fn place_at <'t, T: Draw + ?Sized> (&mut self, area: XYWH, content: &'t T) { - let last = self.area(); - *self.area_mut() = area; - content.draw(self); - *self.area_mut() = last; - } -} -impl TuiOut { - #[inline] pub fn with_rect (&mut self, area: XYWH) -> &mut Self { self.area = area; self } - pub fn update (&mut self, area: XYWH, callback: &impl Fn(&mut Cell, u16, u16)) { tui_update(&mut self.buffer, area, callback); } - pub fn fill_char (&mut self, area: XYWH, c: char) { self.update(area, &|cell,_,_|{cell.set_char(c);}) } - pub fn fill_bg (&mut self, area: XYWH, color: Color) { self.update(area, &|cell,_,_|{cell.set_bg(color);}) } - pub fn fill_fg (&mut self, area: XYWH, color: Color) { self.update(area, &|cell,_,_|{cell.set_fg(color);}) } - pub fn fill_mod (&mut self, area: XYWH, on: bool, modifier: Modifier) { - if on { - self.update(area, &|cell,_,_|cell.modifier.insert(modifier)) - } else { - self.update(area, &|cell,_,_|cell.modifier.remove(modifier)) - } - } - pub fn fill_bold (&mut self, area: XYWH, on: bool) { self.fill_mod(area, on, Modifier::BOLD) } - pub fn fill_reversed (&mut self, area: XYWH, on: bool) { self.fill_mod(area, on, Modifier::REVERSED) } - pub fn fill_crossed_out (&mut self, area: XYWH, on: bool) { self.fill_mod(area, on, Modifier::CROSSED_OUT) } - pub fn fill_ul (&mut self, area: XYWH, color: Option) { - if let Some(color) = color { - self.update(area, &|cell,_,_|{ - cell.modifier.insert(ratatui::prelude::Modifier::UNDERLINED); - cell.underline_color = color; - }) - } else { - self.update(area, &|cell,_,_|{ - cell.modifier.remove(ratatui::prelude::Modifier::UNDERLINED); - }) - } - } - pub fn tint_all (&mut self, fg: Color, bg: Color, modifier: Modifier) { - for cell in self.buffer.content.iter_mut() { - cell.fg = fg; - cell.bg = bg; - cell.modifier = modifier; - } - } - pub fn blit (&mut self, text: &impl AsRef, x: u16, y: u16, style: Option