mirror of
https://codeberg.org/unspeaker/tengri.git
synced 2026-02-22 10:59:02 +01:00
917 lines
32 KiB
Rust
917 lines
32 KiB
Rust
#![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 View<$Output, ()> for $State {
|
|
fn view_expr <'a> (&'a self, to: &mut $Output, expr: &'a impl Expression) -> Usually<()> {
|
|
for namespace in $namespaces { if namespace(self, to, expr)? { return Ok(()) } }
|
|
Err(format!("{}::<{}, ()>::view_expr: unexpected: {expr:?}",
|
|
stringify! { $State },
|
|
stringify! { $Output }).into())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Stack things on top of each other,
|
|
#[macro_export] macro_rules! lay (($($expr:expr),* $(,)?) => {{ let bsp = (); $(let bsp = Bsp::b(bsp, $expr);)*; bsp }});
|
|
|
|
/// Stack southward.
|
|
#[macro_export] macro_rules! col (($($expr:expr),* $(,)?) => {{ let bsp = (); $(let bsp = Bsp::s(bsp, $expr);)*; bsp }});
|
|
|
|
/// Stack northward.
|
|
#[macro_export] macro_rules! col_up (($($expr:expr),* $(,)?) => {{ let bsp = (); $(let bsp = Bsp::n(bsp, $expr);)*; bsp }});
|
|
|
|
/// Stack eastward.
|
|
#[macro_export] macro_rules! row (($($expr:expr),* $(,)?) => {{ let bsp = (); $(let bsp = Bsp::e(bsp, $expr);)*; bsp }});
|
|
|
|
/// Define layout operation.
|
|
#[cfg(feature = "dsl")] pub fn evaluate_output_expression <'a, O: Out + 'a, S> (
|
|
state: &S, output: &mut O, expr: &'a impl Expression
|
|
) -> Usually<bool> where
|
|
S: View<O, ()>
|
|
+ for<'b>Namespace<'b, bool>
|
|
+ for<'b>Namespace<'b, O::Unit>
|
|
{
|
|
// First element of expression is used for dispatch.
|
|
// Dispatch is proto-namespaced using separator character
|
|
let head = expr.head()?;
|
|
let mut frags = head.src()?.unwrap_or_default().split("/");
|
|
// The rest of the tokens in the expr are arguments.
|
|
// Their meanings depend on the dispatched operation
|
|
let args = expr.tail();
|
|
let arg0 = args.head();
|
|
let tail0 = args.tail();
|
|
let arg1 = tail0.head();
|
|
let tail1 = tail0.tail();
|
|
let arg2 = tail1.head();
|
|
// And we also have to do the above binding dance
|
|
// so that the Perhaps<token>s remain in scope.
|
|
match frags.next() {
|
|
|
|
Some("when") => output.place(&When::new(
|
|
state.namespace(arg0?)?.unwrap(),
|
|
Thunk::new(move|output: &mut O|state.view(output, &arg1).unwrap())
|
|
)),
|
|
|
|
Some("either") => output.place(&Either::new(
|
|
state.namespace(arg0?)?.unwrap(),
|
|
Thunk::new(move|output: &mut O|state.view(output, &arg1).unwrap()),
|
|
Thunk::new(move|output: &mut O|state.view(output, &arg2).unwrap())
|
|
)),
|
|
|
|
Some("bsp") => output.place(&{
|
|
let a = Thunk::new(move|output: &mut O|state.view(output, &arg0).unwrap());
|
|
let b = Thunk::new(move|output: &mut O|state.view(output, &arg1).unwrap());
|
|
match frags.next() {
|
|
Some("n") => Bsp::n(a, b),
|
|
Some("s") => Bsp::s(a, b),
|
|
Some("e") => Bsp::e(a, b),
|
|
Some("w") => Bsp::w(a, b),
|
|
Some("a") => Bsp::a(a, b),
|
|
Some("b") => Bsp::b(a, b),
|
|
frag => unimplemented!("bsp/{frag:?}")
|
|
}
|
|
}),
|
|
|
|
Some("align") => output.place(&{
|
|
let a = Thunk::new(move|output: &mut O|state.view(output, &arg0).unwrap());
|
|
match frags.next() {
|
|
Some("n") => Align::n(a),
|
|
Some("s") => Align::s(a),
|
|
Some("e") => Align::e(a),
|
|
Some("w") => Align::w(a),
|
|
Some("x") => Align::x(a),
|
|
Some("y") => Align::y(a),
|
|
Some("c") => Align::c(a),
|
|
frag => unimplemented!("align/{frag:?}")
|
|
}
|
|
}),
|
|
|
|
Some("fill") => output.place(&{
|
|
let a = Thunk::new(move|output: &mut O|state.view(output, &arg0).unwrap());
|
|
match frags.next() {
|
|
Some("xy") | None => Fill::XY(a),
|
|
Some("x") => Fill::X(a),
|
|
Some("y") => Fill::Y(a),
|
|
frag => unimplemented!("fill/{frag:?}")
|
|
}
|
|
}),
|
|
|
|
Some("fixed") => output.place(&{
|
|
let axis = frags.next();
|
|
let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") };
|
|
let cb = Thunk::new(move|output: &mut O|state.view(output, &arg).unwrap());
|
|
match axis {
|
|
Some("xy") | None => Fixed::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb),
|
|
Some("x") => Fixed::X(state.namespace(arg0?)?.unwrap(), cb),
|
|
Some("y") => Fixed::Y(state.namespace(arg0?)?.unwrap(), cb),
|
|
frag => unimplemented!("fixed/{frag:?} ({expr:?}) ({head:?}) ({:?})",
|
|
head.src()?.unwrap_or_default().split("/").next())
|
|
}
|
|
}),
|
|
|
|
Some("min") => output.place(&{
|
|
let axis = frags.next();
|
|
let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") };
|
|
let cb = Thunk::new(move|output: &mut O|state.view(output, &arg).unwrap());
|
|
match axis {
|
|
Some("xy") | None => Min::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb),
|
|
Some("x") => Min::X(state.namespace(arg0?)?.unwrap(), cb),
|
|
Some("y") => Min::Y(state.namespace(arg0?)?.unwrap(), cb),
|
|
frag => unimplemented!("min/{frag:?}")
|
|
}
|
|
}),
|
|
|
|
Some("max") => output.place(&{
|
|
let axis = frags.next();
|
|
let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") };
|
|
let cb = Thunk::new(move|output: &mut O|state.view(output, &arg).unwrap());
|
|
match axis {
|
|
Some("xy") | None => Max::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb),
|
|
Some("x") => Max::X(state.namespace(arg0?)?.unwrap(), cb),
|
|
Some("y") => Max::Y(state.namespace(arg0?)?.unwrap(), cb),
|
|
frag => unimplemented!("max/{frag:?}")
|
|
}
|
|
}),
|
|
|
|
Some("push") => output.place(&{
|
|
let axis = frags.next();
|
|
let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") };
|
|
let cb = Thunk::new(move|output: &mut O|state.view(output, &arg).unwrap());
|
|
match axis {
|
|
Some("xy") | None => Push::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb),
|
|
Some("x") => Push::X(state.namespace(arg0?)?.unwrap(), cb),
|
|
Some("y") => Push::Y(state.namespace(arg0?)?.unwrap(), cb),
|
|
frag => unimplemented!("push/{frag:?}")
|
|
}
|
|
}),
|
|
|
|
_ => return Ok(false)
|
|
|
|
};
|
|
Ok(true)
|
|
}
|
|
|
|
/// Implement [Command] for given `State` and `handler`
|
|
#[macro_export] macro_rules! command {
|
|
($(<$($l:lifetime),+>)?|$self:ident:$Command:ty,$state:ident:$State:ty|$handler:expr) => {
|
|
impl$(<$($l),+>)? ::tengri::Command<$State> for $Command {
|
|
fn execute (&$self, $state: &mut $State) -> Perhaps<Self> {
|
|
Ok($handler)
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
#[macro_export] macro_rules! def_command (($Command:ident: |$state:ident: $State:ty| {
|
|
$($Variant:ident$({$($arg:ident:$Arg:ty),+ $(,)?})?=>$body:expr),* $(,)?
|
|
})=>{
|
|
#[derive(Debug)]
|
|
pub enum $Command {
|
|
$($Variant $({ $($arg: $Arg),* })?),*
|
|
}
|
|
impl Command<$State> for $Command {
|
|
fn execute (&self, $state: &mut $State) -> Perhaps<Self> {
|
|
match self {
|
|
$(Self::$Variant $({ $($arg),* })? => $body,)*
|
|
_ => unimplemented!("Command<{}>: {self:?}", stringify!($State)),
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
/// Implement [Handle] for given `State` and `handler`.
|
|
#[macro_export] macro_rules! handle {
|
|
(|$self:ident:$State:ty,$input:ident|$handler:expr) => {
|
|
impl<E: Engine> ::tengri::Handle<E> for $State {
|
|
fn handle (&mut $self, $input: &E) -> Perhaps<E::Handled> {
|
|
$handler
|
|
}
|
|
}
|
|
};
|
|
($E:ty: |$self:ident:$State:ty,$input:ident|$handler:expr) => {
|
|
impl ::tengri::Handle<$E> for $State {
|
|
fn handle (&mut $self, $input: &$E) ->
|
|
Perhaps<<$E as ::tengri::Input>::Handled>
|
|
{
|
|
$handler
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[macro_export] macro_rules! tui_main {
|
|
($expr:expr) => {
|
|
fn main () -> Usually<()> {
|
|
tengri::Tui::run(true, $expr)?;
|
|
Ok(())
|
|
}
|
|
};
|
|
}
|
|
|
|
#[macro_export] macro_rules! has_color {
|
|
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
|
impl $(<$($L),*$($T $(: $U)?),*>)? HasColor for $Struct $(<$($L),*$($T),*>)? {
|
|
fn color (&$self) -> ItemColor { $cb }
|
|
}
|
|
}
|
|
}
|
|
|
|
macro_rules! border {
|
|
($($T:ident {
|
|
$nw:literal $n:literal $ne:literal $w:literal $e:literal $sw:literal $s:literal $se:literal
|
|
$($x:tt)*
|
|
}),+) => {$(
|
|
impl BorderStyle for $T {
|
|
const NW: &'static str = $nw;
|
|
const N: &'static str = $n;
|
|
const NE: &'static str = $ne;
|
|
const W: &'static str = $w;
|
|
const E: &'static str = $e;
|
|
const SW: &'static str = $sw;
|
|
const S: &'static str = $s;
|
|
const SE: &'static str = $se;
|
|
$($x)*
|
|
fn enabled (&self) -> bool { self.0 }
|
|
}
|
|
#[derive(Copy, Clone)] pub struct $T(pub bool, pub Style);
|
|
impl Layout<TuiOut> for $T {}
|
|
impl Draw<TuiOut> for $T {
|
|
fn draw (&self, to: &mut TuiOut) {
|
|
if self.enabled() { let _ = BorderStyle::draw(self, to); }
|
|
}
|
|
}
|
|
)+}
|
|
}
|
|
|
|
/// Run an app in the main loop.
|
|
pub fn tui_run <T: Send + Sync + Draw<TuiOut> + Handle<TuiIn> + 'static> (
|
|
join: bool,
|
|
state: &Arc<RwLock<T>>
|
|
) -> Usually<Arc<RwLock<Tui>>> {
|
|
let backend = CrosstermBackend::new(stdout());
|
|
let Size { width, height } = backend.size()?;
|
|
let tui = Arc::new(RwLock::new(Tui {
|
|
exited: Arc::new(AtomicBool::new(false)),
|
|
buffer: Buffer::empty(Rect { x: 0, y: 0, width, height }),
|
|
area: [0, 0, width, height],
|
|
perf: Default::default(),
|
|
backend,
|
|
}));
|
|
let _input_thread = tui_input(tui.clone(), state, Duration::from_millis(100));
|
|
tui.write().unwrap().setup()?;
|
|
let render_thread = tui_output(tui.clone(), state, Duration::from_millis(10))?;
|
|
if join {
|
|
match render_thread.join() {
|
|
Ok(result) => {
|
|
tui.write().unwrap().teardown()?;
|
|
println!("\n\rRan successfully: {result:?}\n\r");
|
|
},
|
|
Err(error) => {
|
|
tui.write().unwrap().teardown()?;
|
|
panic!("\n\rDraw thread failed: error={error:?}.\n\r")
|
|
},
|
|
}
|
|
}
|
|
Ok(tui)
|
|
}
|
|
|
|
pub fn tui_setup <W: Write> (
|
|
backend: &mut CrosstermBackend<W>
|
|
) -> Usually<()> {
|
|
let better_panic_handler = Settings::auto().verbosity(Verbosity::Full).create_panic_handler();
|
|
std::panic::set_hook(Box::new(move |info: &std::panic::PanicHookInfo|{
|
|
stdout().execute(LeaveAlternateScreen).unwrap();
|
|
CrosstermBackend::new(stdout()).show_cursor().unwrap();
|
|
disable_raw_mode().unwrap();
|
|
better_panic_handler(info);
|
|
}));
|
|
stdout().execute(EnterAlternateScreen)?;
|
|
backend.hide_cursor()?;
|
|
enable_raw_mode().map_err(Into::into)
|
|
}
|
|
|
|
pub fn tui_teardown <W: Write> (backend: &mut CrosstermBackend<W>) -> Usually<()> {
|
|
stdout().execute(LeaveAlternateScreen)?;
|
|
backend.show_cursor()?;
|
|
disable_raw_mode().map_err(Into::into)
|
|
}
|
|
|
|
pub fn tui_resized <W: Write> (
|
|
backend: &mut CrosstermBackend<W>,
|
|
buffer: &mut Buffer,
|
|
size: ratatui::prelude::Rect
|
|
) {
|
|
if buffer.area != size {
|
|
backend.clear_region(ClearType::All).unwrap();
|
|
buffer.resize(size);
|
|
buffer.reset();
|
|
}
|
|
}
|
|
|
|
pub fn tui_redrawn <'b, W: Write> (
|
|
backend: &mut CrosstermBackend<W>,
|
|
mut prev_buffer: &'b mut Buffer,
|
|
mut next_buffer: &'b mut Buffer
|
|
) {
|
|
let updates = prev_buffer.diff(&next_buffer);
|
|
backend.draw(updates.into_iter()).expect("failed to render");
|
|
Backend::flush(backend).expect("failed to flush output new_buffer");
|
|
std::mem::swap(&mut prev_buffer, &mut next_buffer);
|
|
next_buffer.reset();
|
|
}
|
|
|
|
pub fn tui_update (
|
|
buf: &mut Buffer, area: XYWH<u16>, callback: &impl Fn(&mut Cell, u16, u16)
|
|
) {
|
|
for row in 0..area.h() {
|
|
let y = area.y() + row;
|
|
for col in 0..area.w() {
|
|
let x = area.x() + col;
|
|
if x < buf.area.width && y < buf.area.height {
|
|
if let Some(cell) = buf.cell_mut(ratatui::prelude::Position { x, y }) {
|
|
callback(cell, col, row);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Spawn the output thread.
|
|
pub fn tui_output <T: Draw<TuiOut> + Send + Sync + 'static> (
|
|
engine: Arc<RwLock<Tui>>, state: &Arc<RwLock<T>>, timer: Duration
|
|
) -> Result<JoinHandle<()>, std::io::Error> {
|
|
let exited = engine.read().unwrap().exited.clone();
|
|
let engine = engine.clone();
|
|
let state = state.clone();
|
|
let Size { width, height } = engine.read().unwrap().backend.size().expect("get size failed");
|
|
let mut buffer = Buffer::empty(Rect { x: 0, y: 0, width, height });
|
|
std::thread::Builder::new()
|
|
.name("tui output thread".into())
|
|
.spawn(move || loop {
|
|
if exited.fetch_and(true, Relaxed) {
|
|
break
|
|
}
|
|
let t0 = engine.read().unwrap().perf.get_t0();
|
|
let Size { width, height } = engine.read().unwrap().backend.size()
|
|
.expect("get size failed");
|
|
if let Ok(state) = state.try_read() {
|
|
let size = Rect { x: 0, y: 0, width, height };
|
|
if buffer.area != size {
|
|
engine.write().unwrap().backend.clear_region(ClearType::All).expect("clear failed");
|
|
buffer.resize(size);
|
|
buffer.reset();
|
|
}
|
|
let mut output = TuiOut { buffer, area: XYWH(0, 0, width, height) };
|
|
state.draw(&mut output);
|
|
buffer = engine.write().unwrap().flip(output.buffer, size);
|
|
}
|
|
let t1 = (*engine.read().unwrap()).perf.get_t1(t0).unwrap();
|
|
buffer.set_string(0, 0, &format!("{:>3}.{:>3}ms", t1.as_millis(), t1.as_micros() % 1000), Style::default());
|
|
std::thread::sleep(timer);
|
|
})
|
|
}
|
|
|
|
/// Spawn the input thread.
|
|
pub fn tui_input <T: Handle<TuiIn> + Send + Sync + 'static> (
|
|
engine: Arc<RwLock<Tui>>, state: &Arc<RwLock<T>>, timer: Duration
|
|
) -> JoinHandle<()> {
|
|
let exited = engine.read().unwrap().exited.clone();
|
|
let state = state.clone();
|
|
spawn(move || loop {
|
|
if exited.fetch_and(true, Relaxed) {
|
|
break
|
|
}
|
|
if poll(timer).is_ok() {
|
|
let event = read().unwrap();
|
|
match event {
|
|
Event::Key(KeyEvent {
|
|
code: KeyCode::Char('c'),
|
|
modifiers: KeyModifiers::CONTROL,
|
|
kind: KeyEventKind::Press,
|
|
state: KeyEventState::NONE
|
|
}) => {
|
|
exited.store(true, Relaxed);
|
|
},
|
|
_ => {
|
|
let exited = exited.clone();
|
|
let event = TuiEvent::from_crossterm(event);
|
|
if let Err(e) = state.write().unwrap().handle(&TuiIn { exited, event }) {
|
|
panic!("{e}")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
#[cfg(feature = "dsl")] pub fn evaluate_output_expression_tui <'a, S> (
|
|
state: &S, output: &mut TuiOut, expr: impl Expression + 'a
|
|
) -> Usually<bool> where
|
|
S: View<TuiOut, ()>
|
|
+ for<'b>Namespace<'b, bool>
|
|
+ for<'b>Namespace<'b, u16>
|
|
+ for<'b>Namespace<'b, Color>
|
|
{
|
|
// See `tengri::evaluate_output_expression`
|
|
let head = expr.head()?;
|
|
let mut frags = head.src()?.unwrap_or_default().split("/");
|
|
let args = expr.tail();
|
|
let arg0 = args.head();
|
|
let tail0 = args.tail();
|
|
let arg1 = tail0.head();
|
|
let tail1 = tail0.tail();
|
|
let _arg2 = tail1.head();
|
|
match frags.next() {
|
|
|
|
Some("text") => if let Some(src) = args?.src()? { output.place(&src) },
|
|
|
|
Some("fg") => {
|
|
let arg0 = arg0?.expect("fg: expected arg 0 (color)");
|
|
output.place(&Tui::fg(
|
|
Namespace::<Color>::namespace(state, arg0)?.unwrap_or_else(||panic!("fg: {arg0:?}: not a color")),
|
|
Thunk::new(move|output: &mut TuiOut|state.view(output, &arg1).unwrap()),
|
|
))
|
|
},
|
|
|
|
Some("bg") => {
|
|
let arg0 = arg0?.expect("bg: expected arg 0 (color)");
|
|
output.place(&Tui::bg(
|
|
Namespace::<Color>::namespace(state, arg0)?.unwrap_or_else(||panic!("bg: {arg0:?}: not a color")),
|
|
Thunk::new(move|output: &mut TuiOut|state.view(output, &arg1).unwrap()),
|
|
))
|
|
},
|
|
|
|
_ => return Ok(false)
|
|
|
|
};
|
|
Ok(true)
|
|
}
|
|
|
|
pub fn named_key (token: &str) -> Option<KeyCode> {
|
|
use KeyCode::*;
|
|
Some(match token {
|
|
"up" => Up,
|
|
"down" => Down,
|
|
"left" => Left,
|
|
"right" => Right,
|
|
"esc" | "escape" => Esc,
|
|
"enter" | "return" => Enter,
|
|
"delete" | "del" => Delete,
|
|
"backspace" => Backspace,
|
|
"tab" => Tab,
|
|
"space" => Char(' '),
|
|
"comma" => Char(','),
|
|
"period" => Char('.'),
|
|
"plus" => Char('+'),
|
|
"minus" | "dash" => Char('-'),
|
|
"equal" | "equals" => Char('='),
|
|
"underscore" => Char('_'),
|
|
"backtick" => Char('`'),
|
|
"lt" => Char('<'),
|
|
"gt" => Char('>'),
|
|
"cbopen" | "openbrace" => Char('{'),
|
|
"cbclose" | "closebrace" => Char('}'),
|
|
"bropen" | "openbracket" => Char('['),
|
|
"brclose" | "closebracket" => Char(']'),
|
|
"pgup" | "pageup" => PageUp,
|
|
"pgdn" | "pagedown" => PageDown,
|
|
"f1" => F(1),
|
|
"f2" => F(2),
|
|
"f3" => F(3),
|
|
"f4" => F(4),
|
|
"f5" => F(5),
|
|
"f6" => F(6),
|
|
"f7" => F(7),
|
|
"f8" => F(8),
|
|
"f9" => F(9),
|
|
"f10" => F(10),
|
|
"f11" => F(11),
|
|
"f12" => F(12),
|
|
_ => return None,
|
|
})
|
|
}
|
|
|
|
/// ```
|
|
/// 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 engine = Tui::run(false, TestComponent("hello world".into()))?;
|
|
engine.read().unwrap().exited.store(true, std::sync::atomic::Ordering::Relaxed);
|
|
//engine.run(&state)?;
|
|
Ok(())
|
|
}
|
|
}
|