remove workspace
Some checks failed
/ build (push) Has been cancelled

This commit is contained in:
same mf who else 2026-02-21 19:34:07 +02:00
parent 006cddcc16
commit 06f8ed3ae3
13 changed files with 133 additions and 125 deletions

898
src/.scratch.rs Normal file
View file

@ -0,0 +1,898 @@
///// The syntagm `(when :condition :content)` corresponds to a [When] layout element.
//impl<S, A> FromDsl<S> for When<A> where bool: FromDsl<S>, A: FromDsl<S> {
//fn try_provide (state: &S, source: &DslVal<impl DslStr, impl DslExpr>) -> Perhaps<Self> {
//source.exp_match("when", |_, tail|Ok(Some(Self(
//FromDsl::<S>::provide(state,
//tail.nth(0, ||"no condition".into())?, ||"no condition".into())?,
//FromDsl::<S>::provide(state,
//tail.nth(1, ||"no content".into())?, ||"no content".into())?,
//))))
//}
//}
///// The syntagm `(either :condition :content1 :content2)` corresponds to an [Either] layout element.
//impl<S, A, B> FromDsl<S> for Either<A, B> where S: Eval<Ast, bool> + Eval<Ast, A> + Eval<Ast, B> {
//fn try_provide (state: &S, source: &DslVal<impl DslStr, impl DslExpr>) -> Perhaps<Self> {
//source.exp_match("either", |_, tail|Ok(Some(Self(
//state.eval(tail.nth(0, ||"no condition")?, ||"no condition")?,
//state.eval(tail.nth(1, ||"no content 1")?, ||"no content 1")?,
//state.eval(tail.nth(2, ||"no content 1")?, ||"no content 2")?,
//))))
//}
//}
///// The syntagm `(align/* :content)` corresponds to an [Align] layout element,
///// where `*` specifies the direction of the alignment.
//impl<S, A> FromDsl<S> for Align<A> where S: Eval<Option<Ast>, A> {
//fn try_provide (state: &S, source: &DslVal<impl DslStr, impl DslExpr>) -> Perhaps<Self> {
//source.exp_match("align/", |head, tail|Ok(Some(match head {
//"c" => Self::c(state.eval(tail.nth(0, ||"no content")?, ||"no content")),
//"x" => Self::x(state.eval(tail.nth(0, ||"no content")?, ||"no content")),
//"y" => Self::y(state.eval(tail.nth(0, ||"no content")?, ||"no content")),
//"n" => Self::n(state.eval(tail.nth(0, ||"no content")?, ||"no content")),
//"s" => Self::s(state.eval(tail.nth(0, ||"no content")?, ||"no content")),
//"e" => Self::e(state.eval(tail.nth(0, ||"no content")?, ||"no content")),
//"w" => Self::w(state.eval(tail.nth(0, ||"no content")?, ||"no content")),
//"nw" => Self::nw(state.eval(tail.nth(0, ||"no content")?, ||"no content")),
//"ne" => Self::ne(state.eval(tail.nth(0, ||"no content")?, ||"no content")),
//"sw" => Self::sw(state.eval(tail.nth(0, ||"no content")?, ||"no content")),
//"se" => Self::se(state.eval(tail.nth(0, ||"no content")?, ||"no content")),
//_ => return Err("invalid align variant".into())
//})))
//}
//}
///// The syntagm `(bsp/* :content1 :content2)` corresponds to a [Bsp] layout element,
///// where `*` specifies the direction of the split.
//impl<S, A, B> FromDsl<S> for Bsp<A, B> where S: Eval<Option<Ast>, A> + Eval<Option<Ast>, B> {
//fn try_provide (state: &S, source: &DslVal<impl DslStr, impl DslExpr>) -> Perhaps<Self> {
//source.exp_match("bsp/", |head, tail|Ok(Some(match head {
//"n" => Self::n(tail.nth(0, ||"no content 1"), tail.nth(1, ||"no content 2")),
//"s" => Self::s(tail.nth(0, ||"no content 1"), tail.nth(1, ||"no content 2")),
//"e" => Self::e(tail.nth(0, ||"no content 1"), tail.nth(1, ||"no content 2")),
//"w" => Self::w(tail.nth(0, ||"no content 1"), tail.nth(1, ||"no content 2")),
//"a" => Self::a(tail.nth(0, ||"no content 1"), tail.nth(1, ||"no content 2")),
//"b" => Self::b(tail.nth(0, ||"no content 1"), tail.nth(1, ||"no content 2")),
//_ => return Ok(None),
//})))
//}
//}
//#[cfg(feature = "dsl")] take!($Enum<A>, A|state, words|Ok(
//if let Some(Token { value: Key(k), .. }) = words.peek() {
//let mut base = words.clone();
//let content = state.give_or_fail(words, ||format!("{k}: no content"))?;
//return Ok(Some(match words.next() {
//Some(Token{value: Key($x),..}) => Self::x(content),
//Some(Token{value: Key($y),..}) => Self::y(content),
//Some(Token{value: Key($xy),..}) => Self::XY(content),
//_ => unreachable!()
//}))
//} else {
//None
//}));
//#[cfg(feature = "dsl")] take!($Enum<U, A>, U, A|state, words|Ok(
//if let Some(Token { value: Key($x|$y|$xy), .. }) = words.peek() {
//let mut base = words.clone();
//Some(match words.next() {
//Some(Token { value: Key($x), .. }) => Self::x(
//state.give_or_fail(words, ||"x: no unit")?,
//state.give_or_fail(words, ||"x: no content")?,
//),
//Some(Token { value: Key($y), .. }) => Self::y(
//state.give_or_fail(words, ||"y: no unit")?,
//state.give_or_fail(words, ||"y: no content")?,
//),
//Some(Token { value: Key($x), .. }) => Self::XY(
//state.give_or_fail(words, ||"xy: no unit x")?,
//state.give_or_fail(words, ||"xy: no unit y")?,
//state.give_or_fail(words, ||"xy: no content")?
//),
//_ => unreachable!(),
//})
//} else {
//None
//}));
//if let Exp(_, exp) = source.value() {
//let mut rest = exp.clone();
//return Ok(Some(match rest.next().as_ref().and_then(|x|x.key()) {
//Some("bsp/n") => Self::n(
//state.eval(rest.next(), ||"bsp/n: no content 1")?,
//state.eval(rest.next(), ||"bsp/n: no content 2")?,
//),
//Some("bsp/s") => Self::s(
//state.eval(rest.next(), ||"bsp/s: no content 1")?,
//state.eval(rest.next(), ||"bsp/s: no content 2")?,
//),
//Some("bsp/e") => Self::e(
//state.eval(rest.next(), ||"bsp/e: no content 1")?,
//state.eval(rest.next(), ||"bsp/e: no content 2")?,
//),
//Some("bsp/w") => Self::w(
//state.eval(rest.next(), ||"bsp/w: no content 1")?,
//state.eval(rest.next(), ||"bsp/w: no content 2")?,
//),
//Some("bsp/a") => Self::a(
//state.eval(rest.next(), ||"bsp/a: no content 1")?,
//state.eval(rest.next(), ||"bsp/a: no content 2")?,
//),
//Some("bsp/b") => Self::b(
//state.eval(rest.next(), ||"bsp/b: no content 1")?,
//state.eval(rest.next(), ||"bsp/b: no content 2")?,
//),
//_ => return Ok(None),
//}))
//}
//Ok(None)
//if let Exp(_, source) = source.value() {
//let mut rest = source.clone();
//return Ok(Some(match rest.next().as_ref().and_then(|x|x.key()) {
//Some("align/c") => Self::c(state.eval(rest.next(), ||"align/c: no content")?),
//Some("align/x") => Self::x(state.eval(rest.next(), ||"align/x: no content")?),
//Some("align/y") => Self::y(state.eval(rest.next(), ||"align/y: no content")?),
//Some("align/n") => Self::n(state.eval(rest.next(), ||"align/n: no content")?),
//Some("align/s") => Self::s(state.eval(rest.next(), ||"align/s: no content")?),
//Some("align/e") => Self::e(state.eval(rest.next(), ||"align/e: no content")?),
//Some("align/w") => Self::w(state.eval(rest.next(), ||"align/w: no content")?),
//Some("align/nw") => Self::nw(state.eval(rest.next(), ||"align/nw: no content")?),
//Some("align/ne") => Self::ne(state.eval(rest.next(), ||"align/ne: no content")?),
//Some("align/sw") => Self::sw(state.eval(rest.next(), ||"align/sw: no content")?),
//Some("align/se") => Self::se(state.eval(rest.next(), ||"align/se: no content")?),
//_ => return Ok(None),
//}))
//}
//Ok(None)
//Ok(match source.exp_head().and_then(|e|e.key()) {
//Some("either") => Some(Self(
//source.exp_tail().and_then(|t|t.get(0)).map(|x|state.eval(x, ||"when: no condition"))?,
//source.exp_tail().and_then(|t|t.get(1)).map(|x|state.eval(x, ||"when: no content 1"))?,
//source.exp_tail().and_then(|t|t.get(2)).map(|x|state.eval(x, ||"when: no content 2"))?,
//)),
//_ => None
//})
//if let Exp(_, mut exp) = source.value()
//&& let Some(Ast(Key(id))) = exp.peek() && *id == *"either" {
//let _ = exp.next();
//return Ok(Some(Self(
//state.eval(exp.next().unwrap(), ||"either: no condition")?,
//state.eval(exp.next().unwrap(), ||"either: no content 1")?,
//state.eval(exp.next().unwrap(), ||"either: no content 2")?,
//)))
//}
//Ok(None)
//Ok(match source.exp_head().and_then(|e|e.key()) {
//Some("when") => Some(Self(
//source.exp_tail().and_then(|t|t.get(0)).map(|x|state.eval(x, ||"when: no condition"))?,
//source.exp_tail().and_then(|t|t.get(1)).map(|x|state.eval(x, ||"when: no content"))?,
//)),
//_ => None
//})
//use crate::*;
//use Direction::*;
//pub struct Stack<'x, E, F1> {
//__: PhantomData<&'x (E, F1)>,
//direction: Direction,
//callback: F1
//}
//impl<'x, E, F1> Stack<'x, E, F1> {
//pub fn new (direction: Direction, callback: F1) -> Self {
//Self { direction, callback, __: Default::default(), }
//}
//pub fn above (callback: F1) -> Self {
//Self::new(Above, callback)
//}
//pub fn below (callback: F1) -> Self {
//Self::new(Below, callback)
//}
//pub fn north (callback: F1) -> Self {
//Self::new(North, callback)
//}
//pub fn south (callback: F1) -> Self {
//Self::new(South, callback)
//}
//pub fn east (callback: F1) -> Self {
//Self::new(East, callback)
//}
//pub fn west (callback: F1) -> Self {
//Self::new(West, callback)
//}
//}
//impl<'x, E: Out, F1: Fn(&mut dyn FnMut(&dyn Layout<E>))> Layout<E> for Stack<'x, E, F1> {
//fn layout (&self, to: E::Area) -> E::Area {
//let state = StackLayoutState::<E>::new(self.direction, to);
//(self.callback)(&mut |component: &dyn Layout<E>|{
//let StackLayoutState { x, y, w_remaining, h_remaining, .. } = *state.borrow();
//let [_, _, w, h] = component.layout([x, y, w_remaining, h_remaining].into()).xywh();
//state.borrow_mut().grow(w, h);
//});
//let StackLayoutState { w_used, h_used, .. } = *state.borrow();
//match self.direction {
//North | West => { todo!() },
//South | East => { [to.x(), to.y(), w_used, h_used].into() },
//_ => unreachable!(),
//}
//}
//}
//impl<'x, E: Out, F1: Fn(&mut dyn FnMut(&dyn Draw<E>))> Draw<E> for Stack<'x, E, F1> {
//fn draw (&self, to: &mut E) {
//let state = StackLayoutState::<E>::new(self.direction, to.area());
//let to = Rc::new(RefCell::new(to));
//(self.callback)(&mut |component: &dyn Draw<E>|{
//let StackLayoutState { x, y, w_remaining, h_remaining, .. } = *state.borrow();
//let layout = component.layout([x, y, w_remaining, h_remaining].into());
//state.borrow_mut().grow(layout.w(), layout.h());
//to.borrow_mut().place_at(layout, component);
//});
//}
//}
//#[derive(Copy, Clone)]
//struct StackLayoutState<E: Out> {
//direction: Direction,
//x: E::Unit,
//y: E::Unit,
//w_used: E::Unit,
//h_used: E::Unit,
//w_remaining: E::Unit,
//h_remaining: E::Unit,
//}
//impl<E: Out> StackLayoutState<E> {
//fn new (direction: Direction, area: E::Area) -> std::rc::Rc<std::cell::RefCell<Self>> {
//let [x, y, w_remaining, h_remaining] = area.xywh();
//std::rc::Rc::new(std::cell::RefCell::new(Self {
//direction,
//x, y, w_remaining, h_remaining,
//w_used: E::Unit::zero(), h_used: E::Unit::zero()
//}))
//}
//fn grow (&mut self, w: E::Unit, h: E::Unit) -> &mut Self {
//match self.direction {
//South => { self.y = self.y.plus(h);
//self.h_used = self.h_used.plus(h);
//self.h_remaining = self.h_remaining.minus(h);
//self.w_used = self.w_used.max(w); },
//East => { self.x = self.x.plus(w);
//self.w_used = self.w_used.plus(w);
//self.w_remaining = self.w_remaining.minus(w);
//self.h_used = self.h_used.max(h); },
//North | West => { todo!() },
//Above | Below => {},
//};
//self
//}
//fn area_remaining (&self) -> E::Area {
//[self.x, self.y, self.w_remaining, self.h_remaining].into()
//}
//}
////pub struct Stack<'a, E, F1> {
////__: PhantomData<&'a (E, F1)>,
////direction: Direction,
////callback: F1
////}
////impl<'a, E, F1> Stack<'a, E, F1> where
////E: Out, F1: Fn(&mut dyn FnMut(&'a dyn Draw<E>)) + Send + Sync,
////{
////pub fn north (callback: F1) -> Self { Self::new(North, callback) }
////pub fn south (callback: F1) -> Self { Self::new(South, callback) }
////pub fn east (callback: F1) -> Self { Self::new(East, callback) }
////pub fn west (callback: F1) -> Self { Self::new(West, callback) }
////pub fn above (callback: F1) -> Self { Self::new(Above, callback) }
////pub fn below (callback: F1) -> Self { Self::new(Below, callback) }
////pub fn new (direction: Direction, callback: F1) -> Self {
////Self { direction, callback, __: Default::default(), }
////}
////}
////impl<'a, E, F1> Draw<E> for Stack<'a, E, F1> where
////E: Out, F1: Fn(&mut dyn FnMut(&'a dyn Draw<E>)) + Send + Sync,
////{
////fn layout (&self, to: E::Area) -> E::Area {
////let state = StackLayoutState::<E>::new(self.direction, to);
////let mut adder = {
////let state = state.clone();
////move|component: &dyn Draw<E>|{
////let [w, h] = component.layout(state.borrow().area_remaining()).wh();
////state.borrow_mut().grow(w, h);
////}
////};
////(self.callback)(&mut adder);
////let StackLayoutState { w_used, h_used, .. } = *state.borrow();
////match self.direction {
////North | West => { todo!() },
////South | East => { [to.x(), to.y(), w_used, h_used].into() },
////Above | Below => { [to.x(), to.y(), to.w(), to.h()].into() },
////}
////}
////fn draw (&self, to: &mut E) {
////let state = StackLayoutState::<E>::new(self.direction, to.area());
////let mut adder = {
////let state = state.clone();
////move|component: &dyn Draw<E>|{
////let [x, y, w, h] = component.layout(state.borrow().area_remaining()).xywh();
////state.borrow_mut().grow(w, h);
////to.place_at([x, y, w, h].into(), component);
////}
////};
////(self.callback)(&mut adder);
////}
////}
//[>Stack::down(|add|{
//let mut i = 0;
//for (_, name) in self.dirs.iter() {
//if i >= self.scroll {
//add(&Tui::bold(i == self.index, name.as_str()))?;
//}
//i += 1;
//}
//for (_, name) in self.files.iter() {
//if i >= self.scroll {
//add(&Tui::bold(i == self.index, name.as_str()))?;
//}
//i += 1;
//}
//add(&format!("{}/{i}", self.index))?;
//Ok(())
//}));*/
//#[test] fn test_iter_map () {
//struct Foo;
//impl<T: Out> Content<T> for Foo {}
//fn _make_map <T: Out, U: Content<T> + Send + Sync> (data: &Vec<U>) -> impl Draw<T> {
//Map::new(||data.iter(), |_foo, _index|{})
//}
//let _data = vec![Foo, Foo, Foo];
////let map = make_map(&data);
//}
// FIXME
//use crate::{dsl::*, input::*, tui::TuiIn};
//use crossterm::event::{Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState};
//use std::cmp::Ordering;
//#[test] fn test_subcommand () -> Usually<()> {
//#[derive(Debug)] struct Event(crossterm::event::Event);
//impl Eq for Event {}
//impl PartialEq for Event { fn eq (&self, other: &Self) -> bool { todo!() } }
//impl Ord for Event { fn cmp (&self, other: &Self) -> Ordering { todo!() } }
//impl PartialOrd for Event { fn partial_cmp (&self, other: &Self) -> Option<Ordering> { None } }
//struct Test { keys: InputMap<Event, Ast> }
//handle!(TuiIn: |self: Test, input|Ok(None));[>if let Some(command) = self.keys.command(self, input) {
//Ok(Some(true))
//} else {
//Ok(None)
//});*/
//#[tengri_proc::command(Test)]
//impl TestCommand {
//fn do_thing (_state: &mut Test) -> Perhaps<Self> {
//Ok(None)
//}
//fn do_thing_arg (_state: &mut Test, _arg: usize) -> Perhaps<Self> {
//Ok(None)
//}
//fn do_sub (state: &mut Test, command: TestSubcommand) -> Perhaps<Self> {
//Ok(command.execute(state)?.map(|command|Self::DoSub { command }))
//}
//}
//#[tengri_proc::command(Test)]
//impl TestSubcommand {
//fn do_other_thing (_state: &mut Test) -> Perhaps<Self> {
//Ok(None)
//}
//fn do_other_thing_arg (_state: &mut Test, _arg: usize) -> Perhaps<Self> {
//Ok(None)
//}
//}
//let mut test = Test {
//keys: InputMap::from_source("
//(@a do-thing)
//(@b do-thing-arg 0)
//(@c do-sub do-other-thing)
//(@d do-sub do-other-thing-arg 0)
//")?
//};
////assert_eq!(Some(true), test.handle(&TuiIn(Default::default(), Event::Key(KeyEvent {
////kind: KeyEventKind::Press,
////code: KeyCode::Char('a'),
////modifiers: KeyModifiers::NONE,
////state: KeyEventState::NONE,
////})))?);
////assert_eq!(Some(true), test.handle(&TuiIn(Default::default(), Event::Key(KeyEvent {
////kind: KeyEventKind::Press,
////code: KeyCode::Char('b'),
////modifiers: KeyModifiers::NONE,
////state: KeyEventState::NONE,
////})))?);
////assert_eq!(Some(true), test.handle(&TuiIn(Default::default(), Event::Key(KeyEvent {
////kind: KeyEventKind::Press,
////code: KeyCode::Char('c'),
////modifiers: KeyModifiers::NONE,
////state: KeyEventState::NONE,
////})))?);
////assert_eq!(Some(true), test.handle(&TuiIn(Default::default(), Event::Key(KeyEvent {
////kind: KeyEventKind::Press,
////code: KeyCode::Char('d'),
////modifiers: KeyModifiers::NONE,
////state: KeyEventState::NONE,
////})))?);
////assert_eq!(None, test.handle(&TuiIn(Default::default(), Event::Key(KeyEvent {
////kind: KeyEventKind::Press,
////code: KeyCode::Char('z'),
////modifiers: KeyModifiers::NONE,
////state: KeyEventState::NONE,
////})))?);
//Ok(())
//}
//FIXME:
//#[cfg(test)] #[test] fn test_dsl_context () {
//use crate::dsl::{Dsl, Value};
//struct Test;
//#[tengri_proc::expose]
//impl Test {
//fn some_bool (&self) -> bool {
//true
//}
//}
//assert_eq!(Dsl::get(&Test, &Value::Sym(":false")), Some(false));
//assert_eq!(Dsl::get(&Test, &Value::Sym(":true")), Some(true));
//assert_eq!(Dsl::get(&Test, &Value::Sym(":some-bool")), Some(true));
//assert_eq!(Dsl::get(&Test, &Value::Sym(":missing-bool")), None);
//assert_eq!(Dsl::get(&Test, &Value::Num(0)), Some(false));
//assert_eq!(Dsl::get(&Test, &Value::Num(1)), Some(true));
//}
use crate::*;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum FocusState<T: Copy + Debug + PartialEq> {
Focused(T),
Entered(T),
}
impl<T: Copy + Debug + PartialEq> FocusState<T> {
pub fn inner (&self) -> T {
match self {
Self::Focused(inner) => *inner,
Self::Entered(inner) => *inner,
}
}
pub fn set_inner (&mut self, inner: T) {
*self = match self {
Self::Focused(_) => Self::Focused(inner),
Self::Entered(_) => Self::Entered(inner),
}
}
pub fn is_focused (&self) -> bool { matches!(self, Self::Focused(_)) }
pub fn is_entered (&self) -> bool { matches!(self, Self::Entered(_)) }
pub fn focus (&mut self) { *self = Self::Focused(self.inner()) }
pub fn enter (&mut self) { *self = Self::Entered(self.inner()) }
}
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum FocusCommand<T: Send + Sync> {
Up,
Down,
Left,
Right,
Next,
Prev,
Enter,
Exit,
Set(T)
}
impl<F: HasFocus + HasEnter + FocusGrid + FocusOrder> Command<F> for FocusCommand<F::Item> {
fn execute (self, state: &mut F) -> Perhaps<FocusCommand<F::Item>> {
match self {
Self::Next => { state.focus_next(); },
Self::Prev => { state.focus_prev(); },
Self::Up => { state.focus_up(); },
Self::Down => { state.focus_down(); },
Self::Left => { state.focus_left(); },
Self::Right => { state.focus_right(); },
Self::Enter => { state.focus_enter(); },
Self::Exit => { state.focus_exit(); },
Self::Set(to) => { state.set_focused(to); },
}
Ok(None)
}
}
/// Trait for things that have focusable subparts.
pub trait HasFocus {
type Item: Copy + PartialEq + Debug + Send + Sync;
/// Get the currently focused item.
fn focused (&self) -> Self::Item;
/// Get the currently focused item.
fn set_focused (&mut self, to: Self::Item);
/// Loop forward until a specific item is focused.
fn focus_to (&mut self, to: Self::Item) {
self.set_focused(to);
self.focus_updated();
}
/// Run this on focus update
fn focus_updated (&mut self) {}
}
/// Trait for things that have enterable subparts.
pub trait HasEnter: HasFocus {
/// Get the currently focused item.
fn entered (&self) -> bool;
/// Get the currently focused item.
fn set_entered (&mut self, entered: bool);
/// Enter into the currently focused component
fn focus_enter (&mut self) {
self.set_entered(true);
self.focus_updated();
}
/// Exit the currently entered component
fn focus_exit (&mut self) {
self.set_entered(false);
self.focus_updated();
}
}
/// Trait for things that implement directional navigation between focusable elements.
pub trait FocusGrid: HasFocus {
fn focus_layout (&self) -> &[&[Self::Item]];
fn focus_cursor (&self) -> (usize, usize);
fn focus_cursor_mut (&mut self) -> &mut (usize, usize);
fn focus_current (&self) -> Self::Item {
let (x, y) = self.focus_cursor();
self.focus_layout()[y][x]
}
fn focus_update (&mut self) {
self.focus_to(self.focus_current());
self.focus_updated()
}
fn focus_up (&mut self) {
let original_focused = self.focused();
let (_, original_y) = self.focus_cursor();
loop {
let (x, y) = self.focus_cursor();
let next_y = if y == 0 {
self.focus_layout().len().saturating_sub(1)
} else {
y - 1
};
if next_y == original_y {
break
}
let next_x = if self.focus_layout()[y].len() == self.focus_layout()[next_y].len() {
x
} else {
((x as f32 / self.focus_layout()[original_y].len() as f32)
* self.focus_layout()[next_y].len() as f32) as usize
};
*self.focus_cursor_mut() = (next_x, next_y);
if self.focus_current() != original_focused {
break
}
}
self.focus_update();
}
fn focus_down (&mut self) {
let original_focused = self.focused();
let (_, original_y) = self.focus_cursor();
loop {
let (x, y) = self.focus_cursor();
let next_y = if y >= self.focus_layout().len().saturating_sub(1) {
0
} else {
y + 1
};
if next_y == original_y {
break
}
let next_x = if self.focus_layout()[y].len() == self.focus_layout()[next_y].len() {
x
} else {
((x as f32 / self.focus_layout()[original_y].len() as f32)
* self.focus_layout()[next_y].len() as f32) as usize
};
*self.focus_cursor_mut() = (next_x, next_y);
if self.focus_current() != original_focused {
break
}
}
self.focus_update();
}
fn focus_left (&mut self) {
let original_focused = self.focused();
let (original_x, y) = self.focus_cursor();
loop {
let x = self.focus_cursor().0;
let next_x = if x == 0 {
self.focus_layout()[y].len().saturating_sub(1)
} else {
x - 1
};
if next_x == original_x {
break
}
*self.focus_cursor_mut() = (next_x, y);
if self.focus_current() != original_focused {
break
}
}
self.focus_update();
}
fn focus_right (&mut self) {
let original_focused = self.focused();
let (original_x, y) = self.focus_cursor();
loop {
let x = self.focus_cursor().0;
let next_x = if x >= self.focus_layout()[y].len().saturating_sub(1) {
0
} else {
x + 1
};
if next_x == original_x {
break
}
self.focus_cursor_mut().0 = next_x;
if self.focus_current() != original_focused {
break
}
}
self.focus_update();
}
}
/// Trait for things that implement next/prev navigation between focusable elements.
pub trait FocusOrder {
/// Focus the next item.
fn focus_next (&mut self);
/// Focus the previous item.
fn focus_prev (&mut self);
}
/// Next/prev navigation for directional focusables works in the given way.
impl<T: FocusGrid + HasEnter> FocusOrder for T {
/// Focus the next item.
fn focus_next (&mut self) {
let current = self.focused();
let (x, y) = self.focus_cursor();
if x < self.focus_layout()[y].len().saturating_sub(1) {
self.focus_right();
} else {
self.focus_down();
self.focus_cursor_mut().0 = 0;
}
if self.focused() == current { // FIXME: prevent infinite loop
self.focus_next()
}
self.focus_exit();
self.focus_update();
}
/// Focus the previous item.
fn focus_prev (&mut self) {
let current = self.focused();
let (x, _) = self.focus_cursor();
if x > 0 {
self.focus_left();
} else {
self.focus_up();
let (_, y) = self.focus_cursor();
let next_x = self.focus_layout()[y].len().saturating_sub(1);
self.focus_cursor_mut().0 = next_x;
}
if self.focused() == current { // FIXME: prevent infinite loop
self.focus_prev()
}
self.focus_exit();
self.focus_update();
}
}
pub trait FocusWrap<T> {
fn wrap <W: Content<TuiOut>> (self, focus: T, content: &'_ W) -> impl Draw<TuiOut> + '_;
}
pub fn to_focus_command <T: Send + Sync> (input: &TuiIn) -> Option<FocusCommand<T>> {
Some(match input.event() {
kpat!(Tab) => FocusCommand::Next,
kpat!(Shift-Tab) => FocusCommand::Prev,
kpat!(BackTab) => FocusCommand::Prev,
kpat!(Shift-BackTab) => FocusCommand::Prev,
kpat!(Up) => FocusCommand::Up,
kpat!(Down) => FocusCommand::Down,
kpat!(Left) => FocusCommand::Left,
kpat!(Right) => FocusCommand::Right,
kpat!(Enter) => FocusCommand::Enter,
kpat!(Esc) => FocusCommand::Exit,
_ => return None
})
}
#[macro_export] macro_rules! impl_focus {
($Struct:ident $Focus:ident $Grid:expr $(=> [$self:ident : $update_focus:expr])?) => {
impl HasFocus for $Struct {
type Item = $Focus;
/// Get the currently focused item.
fn focused (&self) -> Self::Item {
self.focus.inner()
}
/// Get the currently focused item.
fn set_focused (&mut self, to: Self::Item) {
self.focus.set_inner(to)
}
$(fn focus_updated (&mut $self) { $update_focus })?
}
impl HasEnter for $Struct {
/// Get the currently focused item.
fn entered (&self) -> bool {
self.focus.is_entered()
}
/// Get the currently focused item.
fn set_entered (&mut self, entered: bool) {
if entered {
self.focus.to_entered()
} else {
self.focus.to_focused()
}
}
}
impl FocusGrid for $Struct {
fn focus_cursor (&self) -> (usize, usize) {
self.cursor
}
fn focus_cursor_mut (&mut self) -> &mut (usize, usize) {
&mut self.cursor
}
fn focus_layout (&self) -> &[&[$Focus]] {
use $Focus::*;
&$Grid
}
}
}
}
use crate::*;
pub struct MenuBar<E: Engine, S, C: Command<S>> {
pub menus: Vec<Menu<E, S, C>>,
pub index: usize,
}
impl<E: Engine, S, C: Command<S>> MenuBar<E, S, C> {
pub fn new () -> Self { Self { menus: vec![], index: 0 } }
pub fn add (mut self, menu: Menu<E, S, C>) -> Self {
self.menus.push(menu);
self
}
}
pub struct Menu<E: Engine, S, C: Command<S>> {
pub title: Arc<str>,
pub items: Vec<MenuItem<E, S, C>>,
pub index: Option<usize>,
}
impl<E: Engine, S, C: Command<S>> Menu<E, S, C> {
pub fn new (title: impl AsRef<str>) -> Self {
Self {
title: title.as_ref().to_string(),
items: vec![],
index: None,
}
}
pub fn add (mut self, item: MenuItem<E, S, C>) -> Self {
self.items.push(item);
self
}
pub fn sep (mut self) -> Self {
self.items.push(MenuItem::sep());
self
}
pub fn cmd (mut self, hotkey: &'static str, text: &'static str, command: C) -> Self {
self.items.push(MenuItem::cmd(hotkey, text, command));
self
}
pub fn off (mut self, hotkey: &'static str, text: &'static str) -> Self {
self.items.push(MenuItem::off(hotkey, text));
self
}
}
pub enum MenuItem<E: Engine, S, C: Command<S>> {
/// Unused.
__(PhantomData<E>, PhantomData<S>),
/// A separator. Skip it.
Separator,
/// A menu item with command, description and hotkey.
Command(&'static str, &'static str, C),
/// A menu item that can't be activated but has description and hotkey
Disabled(&'static str, &'static str)
}
impl<E: Engine, S, C: Command<S>> MenuItem<E, S, C> {
pub fn sep () -> Self {
Self::Separator
}
pub fn cmd (hotkey: &'static str, text: &'static str, command: C) -> Self {
Self::Command(hotkey, text, command)
}
pub fn off (hotkey: &'static str, text: &'static str) -> Self {
Self::Disabled(hotkey, text)
}
}
//impl<T: Draw<TuiOut>> Content<TuiOut> for Result<T, Box<dyn std::error::Error>> {
//fn content (&self) -> impl Draw<TuiOut> + '_ {
//Bsp::a(self.as_ref().ok(), self.as_ref().err().map(
//|e|Tui::fg_bg(Color::Rgb(255,255,255), Color::Rgb(32,32,32), e.to_string())
//))
//}
//}
//impl<T: Draw<TuiOut>> Draw<TuiOut> for Result<T, Box<dyn std::error::Error>> {
//fn layout (&self, to: [u16;4]) -> [u16;4] {
//match self {
//Ok(content) => content.layout(to),
//Err(e) => [0, 0, to.w(), to.h()]
//}
//}
//fn draw (&self, to: &mut TuiOut) {
//match self {
//Ok(content) => content.draw(to),
//Err(e) => to.blit(&e.to_string(), 0, 0, Some(Style::default()
//.bg(Color::Rgb(32,32,32))
//.fg(Color::Rgb(255,255,255))))
//}
//}
//}
//let token = token.as_ref();
//if token.len() < 2 {
//Self { valid: false, key: None, mods: KeyModifiers::NONE }
//} else if token.chars().next() != Some('@') {
//Self { valid: false, key: None, mods: KeyModifiers::NONE }
//} else {
//Self { valid: true, key: None, mods: KeyModifiers::NONE }.next(&token[1..])
//}
//}
//pub fn build (self) -> Option<Event> {
//if self.valid && self.key.is_some() {
//Some(Event::Key(KeyEvent::new(self.key.unwrap(), self.mods)))
//} else {
//None
//}
//}
//fn next (mut self, token: &str) -> Self {
//let mut tokens = token.split('-').peekable();
//while let Some(token) = tokens.next() {
//if tokens.peek().is_some() {
//match token {
//"ctrl" | "Ctrl" | "c" | "C" => self.mods |= KeyModifiers::CONTROL,
//"alt" | "Alt" | "m" | "M" => self.mods |= KeyModifiers::ALT,
//"shift" | "Shift" | "s" | "S" => {
//self.mods |= KeyModifiers::SHIFT;
//// + TODO normalize character case, BackTab, etc.
//},
//_ => panic!("unknown modifier {token}"),
//}
//} else {
//self.key = if token.len() == 1 {
//Some(KeyCode::Char(token.chars().next().unwrap()))
//} else {
//Some(Self::named_key(token).unwrap_or_else(||panic!("unknown character {token}")))
//}
//}
//}
//self
//}
//#[test] fn test_parse_key () {
////use KeyModifiers as Mods;
//let _test = |x: &str, y|assert_eq!(KeyMatcher::new(x).build(), Some(Event::Key(y)));
////test(":x",
////KeyEvent::new(KeyCode::Char('x'), Mods::NONE));
////test(":ctrl-x",
////KeyEvent::new(KeyCode::Char('x'), Mods::CONTROL));
////test(":alt-x",
////KeyEvent::new(KeyCode::Char('x'), Mods::ALT));
////test(":shift-x",
////KeyEvent::new(KeyCode::Char('x'), Mods::SHIFT));
////test(":ctrl-alt-shift-x",
////KeyEvent::new(KeyCode::Char('x'), Mods::CONTROL | Mods::ALT | Mods::SHIFT ));
//}

56
src/README.md Normal file
View file

@ -0,0 +1,56 @@
***tengri*** is a metaframework for building interactive applications with rust. (aren't we all?)
tengri is developed as part of [***tek***](https://codeberg.org/unspeaker/tek),
a music program for terminals.
tengri contains:
* [***dizzle***](./dsl), a framework for defining domain-specific languages
* [***output***](./output), an abstract UI layout framework
* [***input***](./input), an abstract UI event framework
* [***tui***](./tui), an implementation of tengri over [***ratatui***](https://ratatui.rs/).
as well as:
* [***core***](./core), the shared definitions ("utils") module
* [***proc***](./proc), the space for procedural macros
* [***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)
## TUI
***tengri_tui*** implements [tengri_output](../output) and [tengri_input](../input)
on top of [ratatui](https://ratatui.rs/) and [crossterm](https://github.com/crossterm-rs/crossterm).
tengri is published under [**AGPL3**](../LICENSE).

903
src/tengri.rs Normal file
View file

@ -0,0 +1,903 @@
#![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)]
//pub(crate) use quanta::Clock;
pub extern crate atomic_float;
pub(crate) use atomic_float::AtomicF64;
pub extern crate ratatui; pub(crate) use ::ratatui::{
prelude::{Color, Style, Buffer, Position},
style::{Stylize, Modifier, Color::*},
backend::{Backend, CrosstermBackend, ClearType},
layout::{Size, Rect},
buffer::Cell
};
pub extern crate crossterm;
pub(crate) use ::crossterm::{
ExecutableCommand,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode},
event::{poll, read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState},
};
pub extern crate palette;
pub(crate) use ::palette::{*, convert::*, okhsl::*};
pub extern crate better_panic;
pub(crate) use better_panic::{Settings, Verbosity};
pub extern crate unicode_width;
pub(crate) use unicode_width::*;
//#[cfg(test)] extern crate tengri_proc;
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::*;
#[macro_export] pub extern crate dizzle;
pub use dizzle::*;
use std::{time::Duration, thread::{spawn, JoinHandle}, io::Write};
pub(crate) use ::std::{
io::{stdout, Stdout},
sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::*}},
fmt::{Debug, Display},
ops::{Add, Sub, Mul, Div},
marker::PhantomData,
};
// 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 Understand<$Output, ()> for $State {
fn understand_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!("{}::<{}, ()>::understand_expr: unexpected: {expr:?}",
stringify! { $State },
stringify! { $Output }).into())
}
}
}
}
/// Stack things on top of each other,
#[macro_export] macro_rules! lay (($($expr:expr),* $(,)?) => {{ let bsp = (); $(let bsp = Bsp::b(bsp, $expr);)*; bsp }});
/// Stack southward.
#[macro_export] macro_rules! col (($($expr:expr),* $(,)?) => {{ let bsp = (); $(let bsp = Bsp::s(bsp, $expr);)*; bsp }});
/// Stack northward.
#[macro_export] macro_rules! col_up (($($expr:expr),* $(,)?) => {{ let bsp = (); $(let bsp = Bsp::n(bsp, $expr);)*; bsp }});
/// Stack eastward.
#[macro_export] macro_rules! row (($($expr:expr),* $(,)?) => {{ let bsp = (); $(let bsp = Bsp::e(bsp, $expr);)*; bsp }});
/// Define layout operation.
#[cfg(feature = "dsl")] pub fn evaluate_output_expression <'a, O: Out + 'a, S> (
state: &S, output: &mut O, expr: &'a impl Expression
) -> Usually<bool> where
S: Understand<O, ()>
+ for<'b>Namespace<'b, bool>
+ for<'b>Namespace<'b, O::Unit>
{
// First element of expression is used for dispatch.
// Dispatch is proto-namespaced using separator character
let head = expr.head()?;
let mut frags = head.src()?.unwrap_or_default().split("/");
// The rest of the tokens in the expr are arguments.
// Their meanings depend on the dispatched operation
let args = expr.tail();
let arg0 = args.head();
let tail0 = args.tail();
let arg1 = tail0.head();
let tail1 = tail0.tail();
let arg2 = tail1.head();
// And we also have to do the above binding dance
// so that the Perhaps<token>s remain in scope.
match frags.next() {
Some("when") => output.place(&When::new(
state.namespace(arg0?)?.unwrap(),
Thunk::new(move|output: &mut O|state.understand(output, &arg1).unwrap())
)),
Some("either") => output.place(&Either::new(
state.namespace(arg0?)?.unwrap(),
Thunk::new(move|output: &mut O|state.understand(output, &arg1).unwrap()),
Thunk::new(move|output: &mut O|state.understand(output, &arg2).unwrap())
)),
Some("bsp") => output.place(&{
let a = Thunk::new(move|output: &mut O|state.understand(output, &arg0).unwrap());
let b = Thunk::new(move|output: &mut O|state.understand(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.understand(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.understand(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.understand(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.understand(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.understand(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.understand(output, &arg).unwrap());
match axis {
Some("xy") | None => Push::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb),
Some("x") => Push::X(state.namespace(arg0?)?.unwrap(), cb),
Some("y") => Push::Y(state.namespace(arg0?)?.unwrap(), cb),
frag => unimplemented!("push/{frag:?}")
}
}),
_ => return Ok(false)
};
Ok(true)
}
/// Implement [Command] for given `State` and `handler`
#[macro_export] macro_rules! command {
($(<$($l:lifetime),+>)?|$self:ident:$Command:ty,$state:ident:$State:ty|$handler:expr) => {
impl$(<$($l),+>)? ::tengri::Command<$State> for $Command {
fn execute (&$self, $state: &mut $State) -> Perhaps<Self> {
Ok($handler)
}
}
};
}
#[macro_export] macro_rules! def_command (($Command:ident: |$state:ident: $State:ty| {
$($Variant:ident$({$($arg:ident:$Arg:ty),+ $(,)?})?=>$body:expr),* $(,)?
})=>{
#[derive(Debug)]
pub enum $Command {
$($Variant $({ $($arg: $Arg),* })?),*
}
impl Command<$State> for $Command {
fn execute (&self, $state: &mut $State) -> Perhaps<Self> {
match self {
$(Self::$Variant $({ $($arg),* })? => $body,)*
_ => unimplemented!("Command<{}>: {self:?}", stringify!($State)),
}
}
}
});
/// Implement [Handle] for given `State` and `handler`.
#[macro_export] macro_rules! handle {
(|$self:ident:$State:ty,$input:ident|$handler:expr) => {
impl<E: Engine> ::tengri::Handle<E> for $State {
fn handle (&mut $self, $input: &E) -> Perhaps<E::Handled> {
$handler
}
}
};
($E:ty: |$self:ident:$State:ty,$input:ident|$handler:expr) => {
impl ::tengri::Handle<$E> for $State {
fn handle (&mut $self, $input: &$E) ->
Perhaps<<$E as ::tengri::Input>::Handled>
{
$handler
}
}
}
}
#[macro_export] macro_rules! tui_main {
($expr:expr) => {
fn main () -> Usually<()> {
tengri::Tui::new(stdout()).run(true, $expr)?;
Ok(())
}
};
}
#[macro_export] macro_rules! has_color {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasColor for $Struct $(<$($L),*$($T),*>)? {
fn color (&$self) -> ItemColor { $cb }
}
}
}
macro_rules! border {
($($T:ident {
$nw:literal $n:literal $ne:literal $w:literal $e:literal $sw:literal $s:literal $se:literal
$($x:tt)*
}),+) => {$(
impl BorderStyle for $T {
const NW: &'static str = $nw;
const N: &'static str = $n;
const NE: &'static str = $ne;
const W: &'static str = $w;
const E: &'static str = $e;
const SW: &'static str = $sw;
const S: &'static str = $s;
const SE: &'static str = $se;
$($x)*
fn enabled (&self) -> bool { self.0 }
}
#[derive(Copy, Clone)] pub struct $T(pub bool, pub Style);
impl Layout<TuiOut> for $T {}
impl Draw<TuiOut> for $T {
fn draw (&self, to: &mut TuiOut) {
if self.enabled() { let _ = BorderStyle::draw(self, to); }
}
}
)+}
}
pub fn tui_setup <W: Write> (
backend: &mut CrosstermBackend<W>
) -> Usually<()> {
let better_panic_handler = Settings::auto().verbosity(Verbosity::Full).create_panic_handler();
std::panic::set_hook(Box::new(move |info: &std::panic::PanicHookInfo|{
stdout().execute(LeaveAlternateScreen).unwrap();
CrosstermBackend::new(stdout()).show_cursor().unwrap();
disable_raw_mode().unwrap();
better_panic_handler(info);
}));
stdout().execute(EnterAlternateScreen)?;
backend.hide_cursor()?;
enable_raw_mode().map_err(Into::into)
}
pub fn tui_teardown <W: Write> (backend: &mut CrosstermBackend<W>) -> Usually<()> {
stdout().execute(LeaveAlternateScreen)?;
backend.show_cursor()?;
disable_raw_mode().map_err(Into::into)
}
pub fn tui_resized <W: Write> (
backend: &mut CrosstermBackend<W>,
buffer: &mut Buffer,
size: ratatui::prelude::Rect
) {
if buffer.area != size {
backend.clear_region(ClearType::All).unwrap();
buffer.resize(size);
buffer.reset();
}
}
pub fn tui_redrawn <'b, W: Write> (
backend: &mut CrosstermBackend<W>,
mut prev_buffer: &'b mut Buffer,
mut next_buffer: &'b mut Buffer
) {
let updates = prev_buffer.diff(&next_buffer);
backend.draw(updates.into_iter()).expect("failed to render");
Backend::flush(backend).expect("failed to flush output new_buffer");
std::mem::swap(&mut prev_buffer, &mut next_buffer);
next_buffer.reset();
}
pub fn tui_update (
buf: &mut Buffer, area: XYWH<u16>, callback: &impl Fn(&mut Cell, u16, u16)
) {
for row in 0..area.h() {
let y = area.y() + row;
for col in 0..area.w() {
let x = area.x() + col;
if x < buf.area.width && y < buf.area.height {
if let Some(cell) = buf.cell_mut(ratatui::prelude::Position { x, y }) {
callback(cell, col, row);
}
}
}
}
}
/// Spawn the output thread.
pub fn tui_output <T: Draw<TuiOut> + Send + Sync + 'static> (
engine: Arc<RwLock<Tui>>, state: &Arc<RwLock<T>>, timer: Duration
) -> Result<JoinHandle<()>, std::io::Error> {
let exited = engine.read().unwrap().exited.clone();
let engine = engine.clone();
let state = state.clone();
let Size { width, height } = engine.read().unwrap().backend.size().expect("get size failed");
let mut buffer = Buffer::empty(Rect { x: 0, y: 0, width, height });
std::thread::Builder::new()
.name("tui output thread".into())
.spawn(move || loop {
if exited.fetch_and(true, Relaxed) {
break
}
let t0 = engine.read().unwrap().perf.get_t0();
let Size { width, height } = engine.read().unwrap().backend.size()
.expect("get size failed");
if let Ok(state) = state.try_read() {
let size = Rect { x: 0, y: 0, width, height };
if buffer.area != size {
engine.write().unwrap().backend.clear_region(ClearType::All).expect("clear failed");
buffer.resize(size);
buffer.reset();
}
let mut output = TuiOut { buffer, area: XYWH(0, 0, width, height) };
state.draw(&mut output);
buffer = engine.write().unwrap().flip(output.buffer, size);
}
let t1 = (*engine.read().unwrap()).perf.get_t1(t0).unwrap();
buffer.set_string(0, 0, &format!("{:>3}.{:>3}ms", t1.as_millis(), t1.as_micros() % 1000), Style::default());
std::thread::sleep(timer);
})
}
/// Spawn the input thread.
pub fn tui_input <T: Handle<TuiIn> + Send + Sync + 'static> (
engine: Arc<RwLock<Tui>>, state: &Arc<RwLock<T>>, timer: Duration
) -> JoinHandle<()> {
let exited = engine.read().unwrap().exited.clone();
let state = state.clone();
spawn(move || loop {
if exited.fetch_and(true, Relaxed) {
break
}
if poll(timer).is_ok() {
let event = read().unwrap();
match event {
Event::Key(KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
kind: KeyEventKind::Press,
state: KeyEventState::NONE
}) => {
exited.store(true, Relaxed);
},
_ => {
let exited = exited.clone();
let event = TuiEvent::from_crossterm(event);
if let Err(e) = state.write().unwrap().handle(&TuiIn { exited, event }) {
panic!("{e}")
}
}
}
}
})
}
/// Should be impl something or other...
///
/// ```
/// struct State;
/// impl<'b> Namespace<'b, bool> for State {}
/// impl<'b> Namespace<'b, u16> for State {}
/// impl<'b> Namespace<'b, Color> for State {}
/// impl Understand<TuiOut, ()> for State {}
/// let state = State;
/// let out = TuiOut::default();
/// tengri::evaluate_output_expression_tui(&state, &mut out, "")?;
/// tengri::evaluate_output_expression_tui(&state, &mut out, "text Hello world!")?;
/// tengri::evaluate_output_expression_tui(&state, &mut out, "fg (g 0) (text Hello world!)")?;
/// tengri::evaluate_output_expression_tui(&state, &mut out, "bg (g 2) (text Hello world!)")?;
/// tengri::evaluate_output_expression_tui(&state, &mut out, "(bg (g 3) (fg (g 4) (text Hello world!)))")?;
/// ```
#[cfg(feature = "dsl")] pub fn evaluate_output_expression_tui <'a, S> (
state: &S, output: &mut TuiOut, expr: impl Expression + 'a
) -> Usually<bool> where
S: Understand<TuiOut, ()>
+ for<'b>Namespace<'b, bool>
+ for<'b>Namespace<'b, u16>
+ for<'b>Namespace<'b, Color>
{
// See `tengri::evaluate_output_expression`
let head = expr.head()?;
let mut frags = head.src()?.unwrap_or_default().split("/");
let args = expr.tail();
let arg0 = args.head();
let tail0 = args.tail();
let arg1 = tail0.head();
let tail1 = tail0.tail();
let _arg2 = tail1.head();
match frags.next() {
Some("text") => {
if let Some(src) = args?.src()? { output.place(&src) }
},
Some("fg") => {
let arg0 = arg0?.expect("fg: expected arg 0 (color)");
let color = Namespace::namespace(state, arg0)?.unwrap_or_else(||panic!("fg: {arg0:?}: not a color"));
let thunk = Thunk::new(move|output: &mut TuiOut|state.understand(output, &arg1).unwrap());
output.place(&Tui::fg(color, thunk))
},
Some("bg") => {
let arg0 = arg0?.expect("bg: expected arg 0 (color)");
let color = Namespace::namespace(state, arg0)?.unwrap_or_else(||panic!("bg: {arg0:?}: not a color"));
let thunk = Thunk::new(move|output: &mut TuiOut|state.understand(output, &arg1).unwrap());
output.place(&Tui::bg(color, thunk))
},
_ => return Ok(false)
};
Ok(true)
}
pub fn named_key (token: &str) -> Option<KeyCode> {
use KeyCode::*;
Some(match token {
"up" => Up,
"down" => Down,
"left" => Left,
"right" => Right,
"esc" | "escape" => Esc,
"enter" | "return" => Enter,
"delete" | "del" => Delete,
"backspace" => Backspace,
"tab" => Tab,
"space" => Char(' '),
"comma" => Char(','),
"period" => Char('.'),
"plus" => Char('+'),
"minus" | "dash" => Char('-'),
"equal" | "equals" => Char('='),
"underscore" => Char('_'),
"backtick" => Char('`'),
"lt" => Char('<'),
"gt" => Char('>'),
"cbopen" | "openbrace" => Char('{'),
"cbclose" | "closebrace" => Char('}'),
"bropen" | "openbracket" => Char('['),
"brclose" | "closebracket" => Char(']'),
"pgup" | "pageup" => PageUp,
"pgdn" | "pagedown" => PageDown,
"f1" => F(1),
"f2" => F(2),
"f3" => F(3),
"f4" => F(4),
"f5" => F(5),
"f6" => F(6),
"f7" => F(7),
"f8" => F(8),
"f9" => F(9),
"f10" => F(10),
"f11" => F(11),
"f12" => F(12),
_ => return None,
})
}
/// ```
/// let _ = button_2("", "", true);
/// let _ = button_2("", "", false);
/// ```
pub fn button_2 <'a> (key: impl Content<TuiOut>, label: impl Content<TuiOut>, editing: bool) -> impl Content<TuiOut> {
Tui::bold(true, Bsp::e(
Tui::fg_bg(Tui::orange(), Tui::g(0), Bsp::e(Tui::fg(Tui::g(0), &""), Bsp::e(key, Tui::fg(Tui::g(96), &"")))),
When::new(!editing, Tui::fg_bg(Tui::g(255), Tui::g(96), label))))
}
/// ```
/// let _ = button_3("", "", "", true);
/// let _ = button_3("", "", "", false);
/// ```
pub fn button_3 <'a> (
key: impl Content<TuiOut>, label: impl Content<TuiOut>, value: impl Content<TuiOut>, editing: bool,
) -> impl Content<TuiOut> {
Tui::bold(true, Bsp::e(
Tui::fg_bg(Tui::orange(), Tui::g(0),
Bsp::e(Tui::fg(Tui::g(0), &""), Bsp::e(key, Tui::fg(if editing { Tui::g(128) } else { Tui::g(96) }, "")))),
Bsp::e(
When::new(!editing, Bsp::e(Tui::fg_bg(Tui::g(255), Tui::g(96), label), Tui::fg_bg(Tui::g(128), Tui::g(96), &""),)),
Bsp::e(Tui::fg_bg(Tui::g(224), Tui::g(128), value), Tui::fg_bg(Tui::g(128), Reset, &""), ))))
}
border! {
Square {
"" "" ""
"" ""
"" "" "" fn style (&self) -> Option<Style> { Some(self.1) }
},
SquareBold {
"" "" ""
"" ""
"" "" "" fn style (&self) -> Option<Style> { Some(self.1) }
},
TabLike {
"" "" ""
"" ""
"" " " "" fn style (&self) -> Option<Style> { Some(self.1) }
},
Lozenge {
"" "" ""
"" ""
"" "" "" fn style (&self) -> Option<Style> { Some(self.1) }
},
Brace {
"" "" ""
"" ""
"" "" "" fn style (&self) -> Option<Style> { Some(self.1) }
},
LozengeDotted {
"" "" ""
"" ""
"" "" "" fn style (&self) -> Option<Style> { Some(self.1) }
},
Quarter {
"" "" "🮇"
"" "🮇"
"" "" "🮇" fn style (&self) -> Option<Style> { Some(self.1) }
},
QuarterV {
"" "" "🮇"
"" "🮇"
"" "" "🮇" fn style (&self) -> Option<Style> { Some(self.1) }
},
Chamfer {
"🭂" "" "🭍"
"" "🮇"
"🭓" "" "🭞" fn style (&self) -> Option<Style> { Some(self.1) }
},
Corners {
"🬆" "" "🬊" // 🬴 🬸
"" ""
"🬱" "" "🬵" fn style (&self) -> Option<Style> { Some(self.1) }
},
CornersTall {
"🭽" "" "🭾"
"" ""
"🭼" "" "🭿" fn style (&self) -> Option<Style> { Some(self.1) }
},
Outer {
"🭽" "" "🭾"
"" ""
"🭼" "" "🭿"
const W0: &'static str = "[";
const E0: &'static str = "]";
const N0: &'static str = "";
const S0: &'static str = "";
fn style (&self) -> Option<Style> { Some(self.1) }
},
Thick {
"" "" ""
"" ""
"" "" ""
fn style (&self) -> Option<Style> { Some(self.1) }
},
Rugged {
"" "" ""
"" ""
"" "🮂" ""
fn style (&self) -> Option<Style> { Some(self.1) }
},
Skinny {
"" "" ""
"" ""
"" "" ""
fn style (&self) -> Option<Style> { Some(self.1) }
},
Brackets {
"" "" ""
"" ""
"" "" ""
const W0: &'static str = "[";
const E0: &'static str = "]";
const N0: &'static str = "";
const S0: &'static str = "";
fn style (&self) -> Option<Style> { Some(self.1) }
},
Reticle {
"" "" ""
"" ""
"" "" ""
const W0: &'static str = "";
const E0: &'static str = "";
const N0: &'static str = "";
const S0: &'static str = "";
fn style (&self) -> Option<Style> { Some(self.1) }
}
}
pub fn okhsl_to_rgb (color: Okhsl<f32>) -> Color {
let Srgb { red, green, blue, .. }: Srgb<f32> = Srgb::from_color_unclamped(color);
Color::Rgb((red * 255.0) as u8, (green * 255.0) as u8, (blue * 255.0) as u8,)
}
pub fn rgb_to_okhsl (color: Color) -> Okhsl<f32> {
if let Color::Rgb(r, g, b) = color {
Okhsl::from_color(Srgb::new(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0))
} else {
unreachable!("only Color::Rgb is supported")
}
}
/// Trim string with [unicode_width].
pub fn trim_string (max_width: usize, input: impl AsRef<str>) -> String {
let input = input.as_ref();
let mut output = Vec::with_capacity(input.len());
let mut width: usize = 1;
let mut chars = input.chars();
while let Some(c) = chars.next() {
if width > max_width {
break
}
output.push(c);
width += c.width().unwrap_or(0);
}
return output.into_iter().collect()
}
pub(crate) fn width_chars_max (max: u16, text: impl AsRef<str>) -> u16 {
let mut width: u16 = 0;
let mut chars = text.as_ref().chars();
while let Some(c) = chars.next() {
width += c.width().unwrap_or(0) as u16;
if width > max {
break
}
}
return width
}
#[inline] pub fn map_south<O: Out>(
item_offset: O::Unit,
item_height: O::Unit,
item: impl Content<O>
) -> impl Content<O> {
Push::Y(item_offset, Fixed::Y(item_height, Fill::X(item)))
}
#[inline] pub fn map_south_west<O: Out>(
item_offset: O::Unit,
item_height: O::Unit,
item: impl Content<O>
) -> impl Content<O> {
Push::Y(item_offset, Align::nw(Fixed::Y(item_height, Fill::X(item))))
}
#[inline] pub fn map_east<O: Out>(
item_offset: O::Unit,
item_width: O::Unit,
item: impl Content<O>
) -> impl Content<O> {
Push::X(item_offset, Align::w(Fixed::X(item_width, Fill::Y(item))))
}
#[cfg(test)] mod test {
use proptest::{prelude::*, option::of};
use proptest_derive::Arbitrary;
use crate::*;
proptest! {
#[test] fn proptest_direction (
d in prop_oneof![
Just(North), Just(South),
Just(East), Just(West),
Just(Above), Just(Below)
],
x in u16::MIN..u16::MAX,
y in u16::MIN..u16::MAX,
w in u16::MIN..u16::MAX,
h in u16::MIN..u16::MAX,
a in u16::MIN..u16::MAX,
) {
let _ = d.split_fixed(XYWH(x, y, w, h), a);
}
}
proptest! {
#[test] fn proptest_area (
x in u16::MIN..u16::MAX,
y in u16::MIN..u16::MAX,
w in u16::MIN..u16::MAX,
h in u16::MIN..u16::MAX,
a in u16::MIN..u16::MAX,
b in u16::MIN..u16::MAX,
) {
let _: XYWH<u16> = XYWH::zero();
//let _: XYWH<u16> = XYWH::from_position([a, b]);
//let _: XYWH<u16> = XYWH::from_size([a, b]);
let area: XYWH<u16> = XYWH(x, y, w, h);
//let _ = area.expect_min(a, b);
let _ = area.xy();
let _ = area.wh();
//let _ = area.xywh();
let _ = area.clipped_h(a);
let _ = area.clipped_w(b);
let _ = area.clipped(WH(a, b));
//let _ = area.set_w(a);
//let _ = area.set_h(b);
let _ = area.x2();
let _ = area.y2();
let _ = area.lrtb();
let _ = area.center();
let _ = area.centered();
let _ = area.centered_x(a);
let _ = area.centered_y(b);
let _ = area.centered_xy([a, b]);
}
}
proptest! {
#[test] fn proptest_size (
x in u16::MIN..u16::MAX,
y in u16::MIN..u16::MAX,
a in u16::MIN..u16::MAX,
b in u16::MIN..u16::MAX,
) {
let size = WH(x, y);
let _ = size.w();
let _ = size.h();
let _ = size.wh();
let _ = size.clip_w(a);
let _ = size.clip_h(b);
//let _ = size.expect_min(a, b);
//let _ = size.to_area_pos();
//let _ = size.to_area_size();
}
}
macro_rules! test_op_transform {
($fn:ident, $Op:ident) => {
proptest! {
#[test] fn $fn (
op_x in of(u16::MIN..u16::MAX),
op_y in of(u16::MIN..u16::MAX),
content in "\\PC*",
x in u16::MIN..u16::MAX,
y in u16::MIN..u16::MAX,
w in u16::MIN..u16::MAX,
h in u16::MIN..u16::MAX,
) {
if let Some(op) = match (op_x, op_y) {
(Some(x), Some(y)) => Some($Op::XY(x, y, content)),
(Some(x), None) => Some($Op::X(x, content)),
(None, Some(y)) => Some($Op::Y(y, content)),
_ => None
} {
//assert_eq!(Content::layout(&op, [x, y, w, h]),
//Draw::layout(&op, [x, y, w, h]));
}
}
}
}
}
test_op_transform!(proptest_op_fixed, Fixed);
test_op_transform!(proptest_op_min, Min);
test_op_transform!(proptest_op_max, Max);
test_op_transform!(proptest_op_push, Push);
test_op_transform!(proptest_op_pull, Pull);
test_op_transform!(proptest_op_shrink, Shrink);
test_op_transform!(proptest_op_expand, Expand);
test_op_transform!(proptest_op_padding, Pad);
proptest! {
#[test] fn proptest_op_bsp (
d in prop_oneof![
Just(North), Just(South),
Just(East), Just(West),
Just(Above), Just(Below)
],
a in "\\PC*",
b in "\\PC*",
x in u16::MIN..u16::MAX,
y in u16::MIN..u16::MAX,
w in u16::MIN..u16::MAX,
h in u16::MIN..u16::MAX,
) {
let bsp = Bsp(d, a, b);
//assert_eq!(
//Content::layout(&bsp, [x, y, w, h]),
//Draw::layout(&bsp, [x, y, w, h]),
//);
}
}
#[test] fn test_tui_engine () -> Usually<()> {
//use std::sync::{Arc, RwLock};
struct TestComponent(String);
impl Draw<TuiOut> for TestComponent {
fn draw (&self, _to: &mut TuiOut) {
}
}
impl Handle<TuiIn> for TestComponent {
fn handle (&mut self, _from: &TuiIn) -> Perhaps<bool> {
Ok(None)
}
}
let mut output = String::new();
let engine = Tui::new(&mut output).run(false, TestComponent("hello world".into()))?;
engine.read().unwrap().exited.store(true, std::sync::atomic::Ordering::Relaxed);
//engine.run(&state)?;
Ok(())
}
}

1649
src/tengri_impl.rs Normal file

File diff suppressed because it is too large Load diff

394
src/tengri_struct.rs Normal file
View file

@ -0,0 +1,394 @@
#[cfg(test)] use proptest_derive::Arbitrary;
use crate::*;
/// The `Tui` struct (the *engine*) implements the
/// `tengri_input::Input` and `tengri_output::Out` traits.
/// At launch, the `Tui` engine spawns two threads, the render thread and the input thread.
/// the application may further spawn other threads. All threads communicate using shared ownership:
/// `Arc<RwLock<T>>` and `Arc<AtomicT>`. Thus, at launch the engine and application instances are expected to be wrapped in `Arc<RwLock>`.
pub struct Tui {
pub exited: Arc<AtomicBool>,
pub backend: CrosstermBackend<Stdout>,
pub buffer: Buffer,
pub area: [u16;4],
pub perf: PerfModel,
}
#[derive(Debug, Clone)] pub struct TuiIn {
/// Input event
pub event: TuiEvent,
/// Exit flag
pub exited: Arc<AtomicBool>,
}
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)] pub struct TuiEvent(
pub Event
);
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)] pub struct TuiKey(
pub Option<KeyCode>,
pub KeyModifiers
);
#[derive(Default)] pub struct TuiOut {
pub buffer: Buffer,
pub area: XYWH<u16>,
}
/// TUI buffer sized by `usize` instead of `u16`.
#[derive(Default)] pub struct BigBuffer {
pub width: usize,
pub height: usize,
pub content: Vec<Cell>
}
/// A color in OKHSL and RGB representations.
#[derive(Debug, Default, Copy, Clone, PartialEq)] pub struct ItemColor {
pub okhsl: Okhsl<f32>,
pub rgb: Color,
}
/// A color in OKHSL and RGB with lighter and darker variants.
#[derive(Debug, Default, Copy, Clone, PartialEq)] pub struct ItemTheme {
pub base: ItemColor,
pub light: ItemColor,
pub lighter: ItemColor,
pub lightest: ItemColor,
pub dark: ItemColor,
pub darker: ItemColor,
pub darkest: ItemColor,
}
pub struct Modify<T>(pub bool, pub Modifier, pub T);
pub struct Styled<T>(pub Option<Style>, pub T);
/// Displays an owned [str]-like with fixed maximum width.
///
/// Width is computed using [unicode_width].
pub struct TrimString<T: AsRef<str>>(pub u16, pub T);
/// Displays a borrowed [str]-like with fixed maximum width
///
/// Width is computed using [unicode_width].
pub struct TrimStringRef<'a, T: AsRef<str>>(pub u16, pub &'a T);
/// Thunks can be natural error boundaries!
pub struct ErrorBoundary<O: Out, T: Draw<O>>(
pub std::marker::PhantomData<O>,
pub Perhaps<T>
);
/// A point (X, Y).
///
/// ```
/// let xy = tengri::XY(0u16, 0);
/// ```
#[cfg_attr(test, derive(Arbitrary))]
#[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct XY<C: Coord>(
pub C, pub C
);
/// A size (Width, Height).
///
/// ```
/// let wh = tengri::WH(0u16, 0);
/// ```
#[cfg_attr(test, derive(Arbitrary))]
#[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct WH<C: Coord>(
pub C, pub C
);
/// Point with size.
///
/// ```
/// let xywh = tengri::XYWH(0u16, 0, 0, 0);
/// assert_eq!(XYWH(10u16, 10, 20, 20).center(), XY(20, 20));
/// ```
///
/// * [ ] TODO: anchor field (determines at which corner/side is X0 Y0)
///
#[cfg_attr(test, derive(Arbitrary))]
#[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct XYWH<C: Coord>(
pub C, pub C, pub C, pub C
);
/// A cardinal direction.
///
/// ```
/// let direction = tengri::Direction::Above;
/// ```
#[cfg_attr(test, derive(Arbitrary))]
#[derive(Copy, Clone, PartialEq, Debug)] pub enum Direction {
North, South, East, West, Above, Below
}
/// 9th of area to place.
///
/// ```
/// let alignment = tengri::Alignment::Center;
/// ```
#[cfg_attr(test, derive(Arbitrary))]
#[derive(Debug, Copy, Clone, Default)] pub enum Alignment {
#[default] Center, X, Y, NW, N, NE, E, SE, S, SW, W
}
/// A widget that tracks its rendered width and height.
///
/// ```
/// let measure = tengri::Measure::<tengri::tui::TuiOut>::default();
/// ```
#[derive(Default)] pub struct Measure<O: Out> {
pub __: PhantomData<O>,
pub x: Arc<AtomicUsize>,
pub y: Arc<AtomicUsize>,
}
/// Show an item only when a condition is true.
///
/// ```
/// fn test () -> impl tengri::Draw<tengri::tui::TuiOut> {
/// tengri::when(true, "Yes")
/// }
/// ```
pub struct When<O, T>(pub bool, pub T, pub PhantomData<O>);
pub const fn when<O, T>(condition: bool, content: T) -> When<O, T> {
When(condition, content, PhantomData)
}
/// Show one item if a condition is true and another if the condition is false.
///
/// ```
/// fn test () -> impl tengri::Draw<tengri::tui::TuiOut> {
/// tengri::either(true, "Yes", "No")
/// }
/// ```
pub struct Either<E, A, B>(pub bool, pub A, pub B, pub PhantomData<E>);
pub const fn either<E, A, B>(condition: bool, content_a: A, content_b: B) -> Either<E, A, B> {
Either(condition, content_a, content_b, PhantomData)
}
/// Increment X and/or Y coordinate.
///
/// ```
/// let pushed = tengri::Push::XY(2, 2, "Hello");
/// ```
pub enum Push<U, A> { X(U, A), Y(U, A), XY(U, U, A), }
/// Decrement X and/or Y coordinate.
///
/// ```
/// let pulled = tengri::Pull::XY(2, 2, "Hello");
/// ```
pub enum Pull<U, A> { X(U, A), Y(U, A), XY(U, U, A), }
/// Set the content to fill the container.
///
/// ```
/// let filled = tengri::Fill::XY("Hello");
/// ```
pub enum Fill<A> { X(A), Y(A), XY(A) }
/// Set fixed size for content.
///
/// ```
/// let fixed = tengri::Fixed::XY(3, 5, "Hello"); // 3x5
/// ```
pub enum Fixed<U, A> { X(U, A), Y(U, A), XY(U, U, A), }
/// Set the maximum width and/or height of the content.
///
/// ```
/// let maximum = tengri::Min::XY(3, 5, "Hello"); // 3x1
/// ```
pub enum Max<U, A> { X(U, A), Y(U, A), XY(U, U, A), }
/// Set the minimum width and/or height of the content.
///
/// ```
/// let minimam = tengri::Min::XY(3, 5, "Hello"); // 5x5
/// ```
pub enum Min<U, A> { X(U, A), Y(U, A), XY(U, U, A), }
/// Decrease the width and/or height of the content.
///
/// ```
/// let shrunk = tengri::Shrink::XY(2, 0, "Hello"); // 1x1
/// ```
pub enum Shrink<U, A> { X(U, A), Y(U, A), XY(U, U, A), }
/// Increaase the width and/or height of the content.
///
/// ```
/// let expanded = tengri::Expand::XY(5, 3, "HELLO"); // 15x3
/// ```
pub enum Expand<U, A> { X(U, A), Y(U, A), XY(U, U, A), }
/// Align position of inner area to middle, side, or corner of outer area.
///
///
/// ```
/// use ::tengri::{output::*, tui::*};
/// let area = XYWH(10u16, 10, 20, 20);
/// fn test (area: XYWH<u16>, item: &impl Draw<TuiOut>, expected: [u16;4]) {
/// //assert_eq!(Lay::layout(item, area), expected);
/// //assert_eq!(Draw::layout(item, area), expected);
/// };
///
/// let four = ||Fixed::XY(4, 4, "");
/// test(area, &Align::nw(four()), [10, 10, 4, 4]);
/// test(area, &Align::n(four()), [18, 10, 4, 4]);
/// test(area, &Align::ne(four()), [26, 10, 4, 4]);
/// test(area, &Align::e(four()), [26, 18, 4, 4]);
/// test(area, &Align::se(four()), [26, 26, 4, 4]);
/// test(area, &Align::s(four()), [18, 26, 4, 4]);
/// test(area, &Align::sw(four()), [10, 26, 4, 4]);
/// test(area, &Align::w(four()), [10, 18, 4, 4]);
///
/// let two_by_four = ||Fixed::XY(4, 2, "");
/// test(area, &Align::nw(two_by_four()), [10, 10, 4, 2]);
/// test(area, &Align::n(two_by_four()), [18, 10, 4, 2]);
/// test(area, &Align::ne(two_by_four()), [26, 10, 4, 2]);
/// test(area, &Align::e(two_by_four()), [26, 19, 4, 2]);
/// test(area, &Align::se(two_by_four()), [26, 28, 4, 2]);
/// test(area, &Align::s(two_by_four()), [18, 28, 4, 2]);
/// test(area, &Align::sw(two_by_four()), [10, 28, 4, 2]);
/// test(area, &Align::w(two_by_four()), [10, 19, 4, 2]);
/// ```
pub struct Align<T>(pub Alignment, pub T);
// TODO DOCUMENTME
pub enum Pad<U, A> { X(U, A), Y(U, A), XY(U, U, A), }
/// TODO DOCUMENTME
///
/// ```
/// use tengri::{Bounded, XYWH};
/// let area = XYWH(0, 0, 0, 0);
/// let content = "";
/// let bounded: Bounded<tengri::tui::TuiOut, _> = Bounded(area, content);
/// ```
pub struct Bounded<O: Out, D>(pub XYWH<O::Unit>, pub D);
/// Draws items from an iterator.
///
/// ```
/// // FIXME let map = tengri::Map(||[].iter(), |_|{});
/// ```
pub struct Map<O, A, B, I, F, G>
where
I: Iterator<Item = A> + Send + Sync,
F: Fn() -> I + Send + Sync,
{
/// Function that returns iterator over stacked components
pub get_iter: F,
/// Function that returns each stacked component
pub get_item: G,
pub __: PhantomData<(O, B)>,
}
// TODO DOCUMENTME
pub struct Lazy<O, T, F>(
pub F,
pub PhantomData<(O, T)>
);
// TODO DOCUMENTME
pub struct Thunk<O: Out, F: Fn(&mut O)>(
pub PhantomData<O>,
pub F
);
// TODO DOCUMENTME
#[derive(Debug, Default)] pub struct Memo<T, U> {
pub value: T,
pub view: Arc<RwLock<U>>
}
/// A binary split or layer.
pub struct Bsp<Head, Tail>(
/// Direction of split
pub(crate) Direction,
/// First element.
pub(crate) Head,
/// Second element.
pub(crate) Tail,
);
// TODO DOCUMENTME
pub struct Bordered<S, W>(pub bool, pub S, pub W);
// TODO DOCUMENTME
pub struct Border<S>(pub bool, pub S);
// TODO DOCUMENTME
pub struct Foreground<Color, Item>(pub Color, pub Item);
// TODO DOCUMENTME
pub struct Background<Color, Item>(pub Color, pub Item);
// TODO DOCUMENTME
pub struct FieldH<Theme, Label, Value>(pub Theme, pub Label, pub Value);
// TODO DOCUMENTME
pub struct FieldV<Theme, Label, Value>(pub Theme, pub Label, pub Value);
/// A three-column layout.
pub struct Tryptich<A, B, C> {
pub top: bool,
pub h: u16,
pub left: (u16, A),
pub middle: (u16, B),
pub right: (u16, C),
}
// TODO:
pub struct Field<C, T, U> {
pub direction: Direction,
pub label: Option<T>,
pub label_fg: Option<C>,
pub label_bg: Option<C>,
pub label_align: Option<Direction>,
pub value: Option<U>,
pub value_fg: Option<C>,
pub value_bg: Option<C>,
pub value_align: Option<Direction>,
}
/// Performance counter
#[derive(Debug)]
pub struct PerfModel {
pub enabled: bool,
pub clock: quanta::Clock,
// In nanoseconds. Time used by last iteration.
pub used: AtomicF64,
// In microseconds. Max prescribed time for iteration (frame, chunk...).
pub window: AtomicF64,
}
/// Repeat a string, e.g. for background
pub enum Repeat<'a> {
X(&'a str),
Y(&'a str),
XY(&'a str)
}
/// Scroll indicator
pub enum Scrollbar {
/// Horizontal scrollbar
X { offset: usize, length: usize, total: usize, },
/// Vertical scrollbar
Y { offset: usize, length: usize, total: usize, }
}
/// A cell that takes up 3 rows on its own,
/// but stacks, giving (N+1)*2 rows per N cells.
pub struct Phat<T> {
pub width: u16,
pub height: u16,
pub content: T,
pub colors: [Color;4],
}

323
src/tengri_trait.rs Normal file
View file

@ -0,0 +1,323 @@
use crate::*;
/// Source of [Input::Event]s: keyboard, mouse...
///
/// ```
///
/// use crate::*;
/// struct TestInput(bool);
/// enum TestEvent { Test1 }
/// impl Input for TestInput {
/// type Event = TestEvent;
/// type Handled = ();
/// fn event (&self) -> &Self::Event {
/// &TestEvent::Test1
/// }
/// fn is_done (&self) -> bool {
/// self.0
/// }
/// fn done (&self) {}
/// }
/// let _ = TestInput(true).event();
/// assert!(TestInput(true).is_done());
/// assert!(!TestInput(false).is_done());
/// Ok(())
/// ```
pub trait Input: Sized {
/// Type of input event
type Event;
/// Result of handling input
type Handled; // TODO: make this an Option<Box dyn Command<Self>> containing the undo
/// Currently handled event
fn event (&self) -> &Self::Event;
/// Whether component should exit
fn is_done (&self) -> bool;
/// Mark component as done
fn done (&self);
}
/// State mutation.
pub trait Command<S>: Send + Sync + Sized {
fn execute (&self, state: &mut S) -> Perhaps<Self>;
fn delegate <T> (&self, state: &mut S, wrap: impl Fn(Self)->T) -> Perhaps<T>
where Self: Sized
{
Ok(self.execute(state)?.map(wrap))
}
}
/// Drawing target.
///
/// ```
/// use tengri::output::*;
/// struct TestOut(XYWH<u16>);
/// impl Out for TestOut {
/// type Unit = u16;
/// fn area (&self) -> XYWH<u16> { self.0 }
/// fn area_mut (&mut self) -> &mut XYWH<u16> { &mut self.0 }
/// fn place_at <T: Draw<Self> + ?Sized> (&mut self, area: XYWH<u16>, _: &T) {
/// println!("place_at: {area:?}");
/// ()
/// }
/// }
/// impl Draw<TestOut> for String {
/// fn draw (&self, to: &mut TestOut) {
/// //to.area_mut().set_w(self.len() as u16);
/// }
/// }
/// ```
pub trait Out: Send + Sync + Sized {
/// Unit of length
type Unit: Coord;
/// Current output area
fn area (&self) -> XYWH<Self::Unit>;
/// Mutable pointer to area.
fn area_mut (&mut self) -> &mut XYWH<Self::Unit>;
/// Render drawable in area specified by `area`
fn place_at <'t, T: Draw<Self> + ?Sized> (&mut self, area: XYWH<Self::Unit>, content: &'t T);
/// Render drawable in area specified by `T::layout(self.area())`
#[inline] fn place <'t, T: Content<Self> + ?Sized> (&mut self, content: &'t T) {
self.place_at(content.layout(self.area()), content)
}
}
/// A numeric type that can be used as coordinate.
///
/// FIXME: Replace this ad-hoc trait with `num` crate.
pub trait Coord: Send + Sync + Copy
+ Add<Self, Output=Self>
+ Sub<Self, Output=Self>
+ Mul<Self, Output=Self>
+ Div<Self, Output=Self>
+ Ord + PartialEq + Eq
+ Debug + Display + Default
+ From<u16> + Into<u16>
+ Into<usize>
+ Into<f64>
{
fn plus (self, other: Self) -> Self;
fn minus (self, other: Self) -> Self {
if self >= other { self - other } else { 0.into() }
}
fn atomic (self) -> AtomicUsize {
AtomicUsize::new(self.into())
}
fn zero () -> Self {
0.into()
}
}
/// Drawable with dynamic dispatch.
pub trait Draw<O: Out> {
fn draw (&self, to: &mut O);
}
/// Outputs combinator.
pub trait Lay<O: Out>: Sized {}
/// Drawable area of display.
pub trait Layout<O: Out> {
fn layout_x (&self, to: XYWH<O::Unit>) -> O::Unit { to.x() }
fn layout_y (&self, to: XYWH<O::Unit>) -> O::Unit { to.y() }
fn layout_w_min (&self, _t: XYWH<O::Unit>) -> O::Unit { 0.into() }
fn layout_w_max (&self, to: XYWH<O::Unit>) -> O::Unit { to.w() }
fn layout_w (&self, to: XYWH<O::Unit>) -> O::Unit { to.w().max(self.layout_w_min(to)).min(self.layout_w_max(to)) }
fn layout_h_min (&self, _t: XYWH<O::Unit>) -> O::Unit { 0.into() }
fn layout_h_max (&self, to: XYWH<O::Unit>) -> O::Unit { to.h() }
fn layout_h (&self, to: XYWH<O::Unit>) -> O::Unit { to.h().max(self.layout_h_min(to)).min(self.layout_h_max(to)) }
fn layout (&self, to: XYWH<O::Unit>) -> XYWH<O::Unit> {
XYWH(self.layout_x(to), self.layout_y(to), self.layout_w(to), self.layout_h(to))
}
}
pub trait HasContent<O: Out> {
fn content (&self) -> impl Content<O>;
}
// TODO DOCUMENTME
pub trait Content<O: Out>: Draw<O> + Layout<O> {}
// Something that has an origin point (X, Y).
pub trait HasXY<N: Coord> {
fn x (&self) -> N;
fn y (&self) -> N;
fn xy (&self) -> XY<N> { XY(self.x(), self.y()) }
}
// Something that has a size (W, H).
pub trait HasWH<N: Coord> {
fn w (&self) -> N;
fn h (&self) -> N;
fn wh (&self) -> WH<N> { WH(self.w(), self.h()) }
}
// Something that has a 2D bounding box (X, Y, W, H).
//
// FIXME: The other way around?
pub trait HasXYWH<N: Coord>: HasXY<N> + HasWH<N> {
fn x2 (&self) -> N { self.x().plus(self.w()) }
fn y2 (&self) -> N { self.y().plus(self.h()) }
fn xywh (&self) -> XYWH<N> { XYWH(self.x(), self.y(), self.w(), self.h()) }
fn expect_min (&self, w: N, h: N) -> Usually<&Self> {
if self.w() < w || self.h() < h {
Err(format!("min {w}x{h}").into())
} else {
Ok(self)
}
}
}
// Something that has a [Measure] of its rendered size.
pub trait Measured<O: Out> {
fn measure (&self) -> &Measure<O>;
fn measure_width (&self) -> O::Unit { self.measure().w() }
fn measure_height (&self) -> O::Unit { self.measure().h() }
}
pub trait HasPerf {
fn perf (&self) -> &PerfModel;
}
pub trait TuiDraw = Draw<TuiOut>;
pub trait TuiLayout = Layout<TuiOut>;
pub trait TuiContent = Content<TuiOut>;
pub trait TuiHandle = Handle<TuiIn>;
pub trait TuiWidget = TuiDraw + TuiHandle;
pub trait HasColor { fn color (&self) -> ItemColor; }
pub trait BorderStyle: Content<TuiOut> + Copy {
fn enabled (&self) -> bool;
fn enclose (self, w: impl Content<TuiOut>) -> impl Content<TuiOut> {
Bsp::b(Fill::XY(Border(self.enabled(), self)), w)
}
fn enclose2 (self, w: impl Content<TuiOut>) -> impl Content<TuiOut> {
Bsp::b(Pad::XY(1, 1, Fill::XY(Border(self.enabled(), self))), w)
}
fn enclose_bg (self, w: impl Content<TuiOut>) -> impl Content<TuiOut> {
Tui::bg(self.style().unwrap().bg.unwrap_or(Color::Reset),
Bsp::b(Fill::XY(Border(self.enabled(), self)), w))
}
const NW: &'static str = "";
const N: &'static str = "";
const NE: &'static str = "";
const E: &'static str = "";
const SE: &'static str = "";
const S: &'static str = "";
const SW: &'static str = "";
const W: &'static str = "";
const N0: &'static str = "";
const S0: &'static str = "";
const W0: &'static str = "";
const E0: &'static str = "";
fn border_n (&self) -> &str { Self::N }
fn border_s (&self) -> &str { Self::S }
fn border_e (&self) -> &str { Self::E }
fn border_w (&self) -> &str { Self::W }
fn border_nw (&self) -> &str { Self::NW }
fn border_ne (&self) -> &str { Self::NE }
fn border_sw (&self) -> &str { Self::SW }
fn border_se (&self) -> &str { Self::SE }
#[inline] fn draw <'a> (
&self, to: &mut TuiOut
) -> Usually<()> {
if self.enabled() {
self.draw_horizontal(to, None)?;
self.draw_vertical(to, None)?;
self.draw_corners(to, None)?;
}
Ok(())
}
#[inline] fn draw_horizontal (
&self, to: &mut TuiOut, style: Option<Style>
) -> Usually<XYWH<u16>> {
let area = to.area();
let style = style.or_else(||self.style_horizontal());
let [x, x2, y, y2] = area.lrtb();
for x in x..x2.saturating_sub(1) {
to.blit(&Self::N, x, y, style);
to.blit(&Self::S, x, y2.saturating_sub(1), style)
}
Ok(area)
}
#[inline] fn draw_vertical (
&self, to: &mut TuiOut, style: Option<Style>
) -> Usually<XYWH<u16>> {
let area = to.area();
let style = style.or_else(||self.style_vertical());
let [x, x2, y, y2] = area.lrtb();
let h = y2 - y;
if h > 1 {
for y in y..y2.saturating_sub(1) {
to.blit(&Self::W, x, y, style);
to.blit(&Self::E, x2.saturating_sub(1), y, style);
}
} else if h > 0 {
to.blit(&Self::W0, x, y, style);
to.blit(&Self::E0, x2.saturating_sub(1), y, style);
}
Ok(area)
}
#[inline] fn draw_corners (
&self, to: &mut TuiOut, style: Option<Style>
) -> Usually<XYWH<u16>> {
let area = to.area();
let style = style.or_else(||self.style_corners());
let XYWH(x, y, width, height) = area;
if width > 1 && height > 1 {
to.blit(&Self::NW, x, y, style);
to.blit(&Self::NE, x + width - 1, y, style);
to.blit(&Self::SW, x, y + height - 1, style);
to.blit(&Self::SE, x + width - 1, y + height - 1, style);
}
Ok(area)
}
#[inline] fn style (&self) -> Option<Style> { None }
#[inline] fn style_horizontal (&self) -> Option<Style> { self.style() }
#[inline] fn style_vertical (&self) -> Option<Style> { self.style() }
#[inline] fn style_corners (&self) -> Option<Style> { self.style() }
}
/// Define a trait an implement it for various mutation-enabled wrapper types. */
#[macro_export] macro_rules! flex_trait_mut (
($Trait:ident $(<$($A:ident:$T:ident),+>)? {
$(fn $fn:ident (&mut $self:ident $(, $arg:ident:$ty:ty)*) -> $ret:ty $body:block)*
})=>{
pub trait $Trait $(<$($A: $T),+>)? {
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret $body)*
}
impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for &mut _T_ {
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { (*$self).$fn($($arg),*) })*
}
impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for Option<_T_> {
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret {
if let Some(this) = $self { this.$fn($($arg),*) } else { Ok(None) }
})*
}
impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::Mutex<_T_> {
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { $self.get_mut().unwrap().$fn($($arg),*) })*
}
impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::Arc<::std::sync::Mutex<_T_>> {
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { $self.lock().unwrap().$fn($($arg),*) })*
}
impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::RwLock<_T_> {
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { $self.write().unwrap().$fn($($arg),*) })*
}
impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::Arc<::std::sync::RwLock<_T_>> {
$(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { $self.write().unwrap().$fn($($arg),*) })*
}
};
);
flex_trait_mut!(Handle <E: Input> {
fn handle (&mut self, _input: &E) -> Perhaps<E::Handled> {
Ok(None)
}
});

0
src/tengri_type.rs Normal file
View file