wip: refactor(tui): 44 errors
Some checks failed
/ build (push) Has been cancelled

This commit is contained in:
same mf who else 2026-02-15 07:07:08 +02:00
parent b7b1055fbc
commit 4fa5d74fa2
26 changed files with 1550 additions and 1548 deletions

View file

@ -49,7 +49,7 @@ impl<N: Coord> WH<N> {
} }
} }
impl<N: Coord> XYWH<N> { impl<N: Coord> XYWH<N> {
fn zero (&self) -> Self { fn zero () -> Self {
Self(0.into(), 0.into(), 0.into(), 0.into()) Self(0.into(), 0.into(), 0.into(), 0.into())
} }
fn x2 (&self) -> N { fn x2 (&self) -> N {
@ -250,8 +250,13 @@ impl<O: Out> Measure<O> {
} }
} }
impl<O: Out> From<[O::Unit; 2]> for Measure<O> { /// FIXME don't convert to u16 specifically
fn from ([x, y]: [O::Unit; 2]) -> Self { Self::new(x, y) } impl<O: Out> HasWH<O::Unit> for Measure<O> {
fn w (&self) -> O::Unit { (self.x.load(Relaxed) as u16).into() }
fn h (&self) -> O::Unit { (self.y.load(Relaxed) as u16).into() }
}
impl<O: Out> From<WH<O::Unit>> for Measure<O> {
fn from (WH(x, y): WH<O::Unit>) -> Self { Self::new(x, y) }
} }
impl<O: Out> Layout<O> for () { impl<O: Out> Layout<O> for () {
@ -460,10 +465,10 @@ layout_op_xy!(1 opt: Expand);
impl<O: Out, T: Layout<O>> Layout<O> for Expand<O::Unit, T> { impl<O: Out, T: Layout<O>> Layout<O> for Expand<O::Unit, T> {
fn layout_w (&self, to: XYWH<O::Unit>) -> O::Unit { fn layout_w (&self, to: XYWH<O::Unit>) -> O::Unit {
self.inner().w(to).plus(self.dx().unwrap_or_default()) self.inner().layout_w(to).plus(self.dx().unwrap_or_default())
} }
fn layout_h (&self, to: XYWH<O::Unit>) -> O::Unit { fn layout_h (&self, to: XYWH<O::Unit>) -> O::Unit {
self.inner().w(to).plus(self.dy().unwrap_or_default()) self.inner().layout_w(to).plus(self.dy().unwrap_or_default())
} }
} }
@ -703,8 +708,8 @@ impl<'a, O, A, B, I, F, G> Layout<O> for Map<O, A, B, I, F, G> where
fn layout (&self, area: XYWH<O::Unit>) -> XYWH<O::Unit> { fn layout (&self, area: XYWH<O::Unit>) -> XYWH<O::Unit> {
let Self { get_iter, get_item, .. } = self; let Self { get_iter, get_item, .. } = self;
let mut index = 0; let mut index = 0;
let [mut min_x, mut min_y] = area.center(); let XY(mut min_x, mut min_y) = area.centered();
let [mut max_x, mut max_y] = area.center(); let XY(mut max_x, mut max_y) = area.center();
for item in get_iter() { for item in get_iter() {
let XYWH(x, y, w, h) = get_item(item, index).layout(area); let XYWH(x, y, w, h) = get_item(item, index).layout(area);
min_x = min_x.min(x); min_x = min_x.min(x);

View file

@ -6,7 +6,7 @@ use crate::*;
/// let xy: XY<u16> = XY(0, 0); /// let xy: XY<u16> = XY(0, 0);
/// ``` /// ```
#[cfg_attr(test, derive(Arbitrary))] #[cfg_attr(test, derive(Arbitrary))]
#[derive(Copy, Clone)] pub struct XY<C: Coord>(pub C, pub C); #[derive(Copy, Clone, Default)] pub struct XY<C: Coord>(pub C, pub C);
/// A size (Width, Height). /// A size (Width, Height).
/// ///
@ -14,7 +14,7 @@ use crate::*;
/// let wh: WH<u16> = WH(0, 0); /// let wh: WH<u16> = WH(0, 0);
/// ``` /// ```
#[cfg_attr(test, derive(Arbitrary))] #[cfg_attr(test, derive(Arbitrary))]
#[derive(Copy, Clone)] pub struct WH<C: Coord>(pub C, pub C); #[derive(Copy, Clone, Default)] pub struct WH<C: Coord>(pub C, pub C);
/// Point with size. /// Point with size.
/// ///
@ -25,7 +25,7 @@ use crate::*;
/// * [ ] TODO: anchor field (determines at which corner/side is X0 Y0) /// * [ ] TODO: anchor field (determines at which corner/side is X0 Y0)
/// ///
#[cfg_attr(test, derive(Arbitrary))] #[cfg_attr(test, derive(Arbitrary))]
#[derive(Copy, Clone)] pub struct XYWH<C: Coord>(pub C, pub C, pub C, pub C); #[derive(Copy, Clone, Default)] pub struct XYWH<C: Coord>(pub C, pub C, pub C, pub C);
/// A cardinal direction. /// A cardinal direction.
/// ///
@ -53,7 +53,7 @@ use crate::*;
/// let measure = Measure::default(); /// let measure = Measure::default();
/// ``` /// ```
#[derive(Default)] pub struct Measure<O: Out> { #[derive(Default)] pub struct Measure<O: Out> {
__: PhantomData<O>, pub __: PhantomData<O>,
pub x: Arc<AtomicUsize>, pub x: Arc<AtomicUsize>,
pub y: Arc<AtomicUsize>, pub y: Arc<AtomicUsize>,
} }

View file

@ -41,6 +41,32 @@ pub trait Out: Send + Sync + Sized {
} }
} }
/// 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. /// Drawable with dynamic dispatch.
pub trait Draw<O: Out> { pub trait Draw<O: Out> {
fn draw (&self, to: &mut O); fn draw (&self, to: &mut O);
@ -93,32 +119,6 @@ pub trait HasContent<O: Out> {
// TODO DOCUMENTME // TODO DOCUMENTME
pub trait Content<O: Out>: Draw<O> + Layout<O> {} pub trait Content<O: Out>: Draw<O> + Layout<O> {}
/// 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()
}
}
// Something that has an origin point (X, Y). // Something that has an origin point (X, Y).
pub trait HasXY<N: Coord> { pub trait HasXY<N: Coord> {
fn x (&self) -> N; fn x (&self) -> N;

View file

@ -304,3 +304,131 @@ pub fn to_focus_command <T: Send + Sync> (input: &TuiIn) -> Option<FocusCommand<
} }
} }
} }
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
//}

View file

@ -1,4 +1,9 @@
#![feature(type_changing_struct_update, trait_alias)] #![feature(type_changing_struct_update, trait_alias)]
use std::{time::Duration, thread::{spawn, JoinHandle}};
use unicode_width::*;
pub use ::{ pub use ::{
dizzle, dizzle,
tengri_input, tengri_input,
@ -8,6 +13,7 @@ pub use ::{
palette, palette,
better_panic, better_panic,
}; };
pub(crate) use ::{ pub(crate) use ::{
dizzle::*, dizzle::*,
tengri_input::*, tengri_input::*,
@ -26,11 +32,10 @@ pub(crate) use ::{
crossterm::{ crossterm::{
ExecutableCommand, ExecutableCommand,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode}, terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode},
event::{Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState}, event::{poll, read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState},
} }
}; };
mod tui_engine; pub use self::tui_engine::*;
mod tui_content; pub use self::tui_content::*;
#[macro_export] macro_rules! tui_main { #[macro_export] macro_rules! tui_main {
($expr:expr) => { ($expr:expr) => {
fn main () -> Usually<()> { fn main () -> Usually<()> {
@ -41,6 +46,256 @@ mod tui_content; pub use self::tui_content::*;
}; };
} }
#[macro_export] macro_rules! has_color {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasColor for $Struct $(<$($L),*$($T),*>)? {
fn color (&$self) -> ItemColor { $cb }
}
}
}
macro_rules! border {
($($T:ident {
$nw:literal $n:literal $ne:literal $w:literal $e:literal $sw:literal $s:literal $se:literal
$($x:tt)*
}),+) => {$(
impl BorderStyle for $T {
const NW: &'static str = $nw;
const N: &'static str = $n;
const NE: &'static str = $ne;
const W: &'static str = $w;
const E: &'static str = $e;
const SW: &'static str = $sw;
const S: &'static str = $s;
const SE: &'static str = $se;
$($x)*
fn enabled (&self) -> bool { self.0 }
}
#[derive(Copy, Clone)] pub struct $T(pub bool, pub Style);
impl Layout<TuiOut> for $T {}
impl Draw<TuiOut> for $T {
fn draw (&self, to: &mut TuiOut) {
if self.enabled() { let _ = BorderStyle::draw(self, to); }
}
}
)+}
}
mod tui_structs; pub use self::tui_structs::*;
mod tui_traits; pub use self::tui_traits::*;
mod tui_impls; pub use self::tui_impls::*;
#[cfg(feature = "dsl")]
pub fn evaluate_output_expression_tui <'a, S> (
state: &S, output: &mut TuiOut, expr: impl Expression + 'a
) -> Usually<bool> where
S: View<TuiOut, ()>
+ for<'b>Namespace<'b, bool>
+ for<'b>Namespace<'b, u16>
+ for<'b>Namespace<'b, Color>
{
// See `tengri_output::evaluate_output_expression`
let head = expr.head()?;
let mut frags = head.src()?.unwrap_or_default().split("/");
let args = expr.tail();
let arg0 = args.head();
let tail0 = args.tail();
let arg1 = tail0.head();
let tail1 = tail0.tail();
let _arg2 = tail1.head();
match frags.next() {
Some("text") => if let Some(src) = args?.src()? { output.place(&src) },
Some("fg") => {
let arg0 = arg0?.expect("fg: expected arg 0 (color)");
output.place(&Tui::fg(
Namespace::<Color>::resolve(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>::resolve(state, arg0)?.unwrap_or_else(||panic!("bg: {arg0:?}: not a color")),
Thunk::new(move|output: &mut TuiOut|state.view(output, &arg1).unwrap()),
))
},
_ => return Ok(false)
};
Ok(true)
}
pub fn named_key (token: &str) -> Option<KeyCode> {
use KeyCode::*;
Some(match token {
"up" => Up,
"down" => Down,
"left" => Left,
"right" => Right,
"esc" | "escape" => Esc,
"enter" | "return" => Enter,
"delete" | "del" => Delete,
"backspace" => Backspace,
"tab" => Tab,
"space" => Char(' '),
"comma" => Char(','),
"period" => Char('.'),
"plus" => Char('+'),
"minus" | "dash" => Char('-'),
"equal" | "equals" => Char('='),
"underscore" => Char('_'),
"backtick" => Char('`'),
"lt" => Char('<'),
"gt" => Char('>'),
"cbopen" | "openbrace" => Char('{'),
"cbclose" | "closebrace" => Char('}'),
"bropen" | "openbracket" => Char('['),
"brclose" | "closebracket" => Char(']'),
"pgup" | "pageup" => PageUp,
"pgdn" | "pagedown" => PageDown,
"f1" => F(1),
"f2" => F(2),
"f3" => F(3),
"f4" => F(4),
"f5" => F(5),
"f6" => F(6),
"f7" => F(7),
"f8" => F(8),
"f9" => F(9),
"f10" => F(10),
"f11" => F(11),
"f12" => F(12),
_ => return None,
})
}
pub fn button_2 <'a> (key: impl Content<TuiOut>, label: impl Content<TuiOut>, editing: bool) -> impl Content<TuiOut> {
Tui::bold(true, Bsp::e(
Tui::fg_bg(Tui::orange(), Tui::g(0), Bsp::e(Tui::fg(Tui::g(0), &""), Bsp::e(key, Tui::fg(Tui::g(96), &"")))),
When::new(!editing, Tui::fg_bg(Tui::g(255), Tui::g(96), label))))
}
pub fn button_3 <'a> (
key: impl Content<TuiOut>, label: impl Content<TuiOut>, value: impl Content<TuiOut>, editing: bool,
) -> impl Content<TuiOut> {
Tui::bold(true, Bsp::e(
Tui::fg_bg(Tui::orange(), Tui::g(0),
Bsp::e(Tui::fg(Tui::g(0), &""), Bsp::e(key, Tui::fg(if editing { Tui::g(128) } else { Tui::g(96) }, "")))),
Bsp::e(
When::new(!editing, Bsp::e(Tui::fg_bg(Tui::g(255), Tui::g(96), label), Tui::fg_bg(Tui::g(128), Tui::g(96), &""),)),
Bsp::e(Tui::fg_bg(Tui::g(224), Tui::g(128), value), Tui::fg_bg(Tui::g(128), Reset, &""), ))))
}
border! {
Square {
"" "" ""
"" ""
"" "" "" fn style (&self) -> Option<Style> { Some(self.1) }
},
SquareBold {
"" "" ""
"" ""
"" "" "" fn style (&self) -> Option<Style> { Some(self.1) }
},
TabLike {
"" "" ""
"" ""
"" " " "" fn style (&self) -> Option<Style> { Some(self.1) }
},
Lozenge {
"" "" ""
"" ""
"" "" "" fn style (&self) -> Option<Style> { Some(self.1) }
},
Brace {
"" "" ""
"" ""
"" "" "" fn style (&self) -> Option<Style> { Some(self.1) }
},
LozengeDotted {
"" "" ""
"" ""
"" "" "" fn style (&self) -> Option<Style> { Some(self.1) }
},
Quarter {
"" "" "🮇"
"" "🮇"
"" "" "🮇" fn style (&self) -> Option<Style> { Some(self.1) }
},
QuarterV {
"" "" "🮇"
"" "🮇"
"" "" "🮇" fn style (&self) -> Option<Style> { Some(self.1) }
},
Chamfer {
"🭂" "" "🭍"
"" "🮇"
"🭓" "" "🭞" fn style (&self) -> Option<Style> { Some(self.1) }
},
Corners {
"🬆" "" "🬊" // 🬴 🬸
"" ""
"🬱" "" "🬵" fn style (&self) -> Option<Style> { Some(self.1) }
},
CornersTall {
"🭽" "" "🭾"
"" ""
"🭼" "" "🭿" fn style (&self) -> Option<Style> { Some(self.1) }
},
Outer {
"🭽" "" "🭾"
"" ""
"🭼" "" "🭿"
const W0: &'static str = "[";
const E0: &'static str = "]";
const N0: &'static str = "";
const S0: &'static str = "";
fn style (&self) -> Option<Style> { Some(self.1) }
},
Thick {
"" "" ""
"" ""
"" "" ""
fn style (&self) -> Option<Style> { Some(self.1) }
},
Rugged {
"" "" ""
"" ""
"" "🮂" ""
fn style (&self) -> Option<Style> { Some(self.1) }
},
Skinny {
"" "" ""
"" ""
"" "" ""
fn style (&self) -> Option<Style> { Some(self.1) }
},
Brackets {
"" "" ""
"" ""
"" "" ""
const W0: &'static str = "[";
const E0: &'static str = "]";
const N0: &'static str = "";
const S0: &'static str = "";
fn style (&self) -> Option<Style> { Some(self.1) }
},
Reticle {
"" "" ""
"" ""
"" "" ""
const W0: &'static str = "";
const E0: &'static str = "";
const N0: &'static str = "";
const S0: &'static str = "";
fn style (&self) -> Option<Style> { Some(self.1) }
}
}
#[cfg(test)] mod tui_test { #[cfg(test)] mod tui_test {
use crate::*; use crate::*;
#[test] fn test_tui_engine () -> Usually<()> { #[test] fn test_tui_engine () -> Usually<()> {

View file

@ -1,12 +1,5 @@
#[allow(unused)] use crate::*; #[allow(unused)] use crate::*;
impl Tui {
pub const fn fg <T> (color: Color, w: T) -> Foreground<Color, T> { Foreground(color, w) }
pub const fn bg <T> (color: Color, w: T) -> Background<Color, T> { Background(color, w) }
pub const fn fg_bg <T> (fg: Color, bg: Color, w: T) -> Background<Color, Foreground<Color, T>> { Background(bg, Foreground(fg, w)) }
pub const fn modify <T> (enable: bool, modifier: Modifier, w: T) -> Modify<T> { Modify(enable, modifier, w) }
pub const fn bold <T> (enable: bool, w: T) -> Modify<T> { Self::modify(enable, Modifier::BOLD, w) }
pub const fn border <S, T> (enable: bool, style: S, w: T) -> Bordered<S, T> { Bordered(enable, style, w) }
}
mod tui_border; pub use self::tui_border::*; mod tui_border; pub use self::tui_border::*;
mod tui_button; pub use self::tui_button::*; mod tui_button; pub use self::tui_button::*;
mod tui_color; pub use self::tui_color::*; mod tui_color; pub use self::tui_color::*;
@ -17,58 +10,3 @@ mod tui_scroll; pub use self::tui_scroll::*;
mod tui_string; pub use self::tui_string::*; mod tui_string; pub use self::tui_string::*;
mod tui_number; //pub use self::tui_number::*; mod tui_number; //pub use self::tui_number::*;
mod tui_tryptich; //pub use self::tui_tryptich::*; mod tui_tryptich; //pub use self::tui_tryptich::*;
impl<T: Content<TuiOut>> Draw<TuiOut> for Foreground<Color, T> {
fn draw (&self, to: &mut TuiOut) {
let area = self.layout(to.area());
to.fill_fg(area, self.0);
to.place_at(area, &self.1);
}
}
impl<T: Content<TuiOut>> Draw<TuiOut> for Background<Color, T> {
fn draw (&self, to: &mut TuiOut) {
let area = self.layout(to.area());
to.fill_bg(area, self.0);
to.place_at(area, &self.1);
}
}
pub struct Modify<T>(pub bool, pub Modifier, pub T);
impl<T: Content<TuiOut>> Layout<TuiOut> for Modify<T> {}
impl<T: Content<TuiOut>> Draw<TuiOut> for Modify<T> {
fn draw (&self, to: &mut TuiOut) {
to.fill_mod(to.area(), self.0, self.1);
self.2.draw(to)
}
}
pub struct Styled<T>(pub Option<Style>, pub T);
impl<T: Content<TuiOut>> Layout<TuiOut> for Styled<T> {}
impl<T: Content<TuiOut>> Draw<TuiOut> for Styled<T> {
fn draw (&self, to: &mut TuiOut) {
to.place(&self.1);
// TODO write style over area
}
}
//impl<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))))
//}
//}
//}

View file

@ -1,63 +0,0 @@
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)
}
}

View file

@ -1,257 +0,0 @@
use crate::*;
impl<S: BorderStyle, W: Content<TuiOut>> HasContent<TuiOut> for Bordered<S, W> {
fn content (&self) -> impl Content<TuiOut> {
Fill::XY(lay!( When::new(self.0, Border(self.0, self.1)), Pad::XY(1, 1, &self.2) ))
}
}
impl<S: BorderStyle> Draw<TuiOut> for Border<S> {
fn draw (&self, to: &mut TuiOut) {
let Border(enabled, style) = self;
if *enabled {
let area = to.area();
if area.w() > 0 && area.y() > 0 {
to.blit(&style.border_nw(), area.x(), area.y(), style.style());
to.blit(&style.border_ne(), area.x() + area.w() - 1, area.y(), style.style());
to.blit(&style.border_sw(), area.x(), area.y() + area.h() - 1, style.style());
to.blit(&style.border_se(), area.x() + area.w() - 1, area.y() + area.h() - 1, style.style());
for x in area.x()+1..area.x()+area.w()-1 {
to.blit(&style.border_n(), x, area.y(), style.style());
to.blit(&style.border_s(), x, area.y() + area.h() - 1, style.style());
}
for y in area.y()+1..area.y()+area.h()-1 {
to.blit(&style.border_w(), area.x(), y, style.style());
to.blit(&style.border_e(), area.x() + area.w() - 1, y, style.style());
}
}
}
}
}
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<[u16;4]> {
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<[u16;4]> {
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<[u16;4]> {
let area = to.area();
let style = style.or_else(||self.style_corners());
let [x, y, width, height] = area.xywh();
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() }
}
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); }
}
}
)+}
}
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) }
}
}

View file

@ -1,18 +0,0 @@
use crate::{*, Color::*};
pub fn button_2 <'a> (key: impl Content<TuiOut>, label: impl Content<TuiOut>, editing: bool) -> impl Content<TuiOut> {
Tui::bold(true, Bsp::e(
Tui::fg_bg(Tui::orange(), Tui::g(0), Bsp::e(Tui::fg(Tui::g(0), &""), Bsp::e(key, Tui::fg(Tui::g(96), &"")))),
When::new(!editing, Tui::fg_bg(Tui::g(255), Tui::g(96), label))))
}
pub fn button_3 <'a> (
key: impl Content<TuiOut>, label: impl Content<TuiOut>, value: impl Content<TuiOut>, editing: bool,
) -> impl Content<TuiOut> {
Tui::bold(true, Bsp::e(
Tui::fg_bg(Tui::orange(), Tui::g(0),
Bsp::e(Tui::fg(Tui::g(0), &""), Bsp::e(key, Tui::fg(if editing { Tui::g(128) } else { Tui::g(96) }, "")))),
Bsp::e(
When::new(!editing, Bsp::e(Tui::fg_bg(Tui::g(255), Tui::g(96), label), Tui::fg_bg(Tui::g(128), Tui::g(96), &""),)),
Bsp::e(Tui::fg_bg(Tui::g(224), Tui::g(128), value), Tui::fg_bg(Tui::g(128), Reset, &""), ))))
}

View file

@ -1,170 +0,0 @@
use crate::*;
use rand::{thread_rng, distributions::uniform::UniformSampler};
impl Tui {
pub const fn null () -> Color { Color::Reset }
pub const fn g (g: u8) -> Color { Color::Rgb(g, g, g) }
pub const fn red () -> Color { Color::Rgb(255,0, 0) }
pub const fn orange () -> Color { Color::Rgb(255,128,0) }
pub const fn yellow () -> Color { Color::Rgb(255,255,0) }
pub const fn brown () -> Color { Color::Rgb(128,255,0) }
pub const fn green () -> Color { Color::Rgb(0,255,0) }
pub const fn electric () -> Color { Color::Rgb(0,255,128) }
//fn bg0 () -> Color { Color::Rgb(20, 20, 20) }
//fn bg () -> Color { Color::Rgb(28, 35, 25) }
//fn border_bg () -> Color { Color::Rgb(40, 50, 30) }
//fn border_fg (f: bool) -> Color { if f { Self::bo1() } else { Self::bo2() } }
//fn title_fg (f: bool) -> Color { if f { Self::ti1() } else { Self::ti2() } }
//fn separator_fg (_: bool) -> Color { Color::Rgb(0, 0, 0) }
//fn mode_bg () -> Color { Color::Rgb(150, 160, 90) }
//fn mode_fg () -> Color { Color::Rgb(255, 255, 255) }
//fn status_bar_bg () -> Color { Color::Rgb(28, 35, 25) }
//fn bo1 () -> Color { Color::Rgb(100, 110, 40) }
//fn bo2 () -> Color { Color::Rgb(70, 80, 50) }
//fn ti1 () -> Color { Color::Rgb(150, 160, 90) }
//fn ti2 () -> Color { Color::Rgb(120, 130, 100) }
}
pub trait HasColor { fn color (&self) -> ItemColor; }
#[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 }
}
}
}
/// A color in OKHSL and RGB representations.
#[derive(Debug, Default, Copy, Clone, PartialEq)] pub struct ItemColor {
pub okhsl: Okhsl<f32>,
pub rgb: Color,
}
from!(ItemColor: |okhsl: Okhsl<f32>|Self { okhsl, rgb: okhsl_to_rgb(okhsl) });
from!(ItemColor: |rgb: Color|Self { rgb, okhsl: rgb_to_okhsl(rgb) });
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")
}
}
// A single color within item theme parameters, in OKHSL and RGB representations.
impl ItemColor {
pub const fn from_rgb (rgb: Color) -> Self {
Self { rgb, okhsl: Okhsl::new_const(OklabHue::new(0.0), 0.0, 0.0) }
}
pub const fn from_okhsl (okhsl: Okhsl<f32>) -> Self {
Self { rgb: Color::Rgb(0, 0, 0), okhsl }
}
pub fn random () -> Self {
let mut rng = thread_rng();
let lo = Okhsl::new(-180.0, 0.01, 0.25);
let hi = Okhsl::new( 180.0, 0.9, 0.5);
UniformOkhsl::new(lo, hi).sample(&mut rng).into()
}
pub fn random_dark () -> Self {
let mut rng = thread_rng();
let lo = Okhsl::new(-180.0, 0.025, 0.075);
let hi = Okhsl::new( 180.0, 0.5, 0.150);
UniformOkhsl::new(lo, hi).sample(&mut rng).into()
}
pub fn random_near (color: Self, distance: f32) -> Self {
color.mix(Self::random(), distance)
}
pub fn mix (&self, other: Self, distance: f32) -> Self {
if distance > 1.0 { panic!("color mixing takes distance between 0.0 and 1.0"); }
self.okhsl.mix(other.okhsl, distance).into()
}
}
/// 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,
}
from!(ItemTheme: |base: Color| Self::from_tui_color(base));
from!(ItemTheme: |base: ItemColor|Self::from_item_color(base));
impl ItemTheme {
pub const G: [Self;256] = {
let mut builder = konst::array::ArrayBuilder::new();
while !builder.is_full() {
let index = builder.len() as u8;
let light = (index as f64 * 1.15) as u8;
let lighter = (index as f64 * 1.7) as u8;
let lightest = (index as f64 * 1.85) as u8;
let dark = (index as f64 * 0.9) as u8;
let darker = (index as f64 * 0.6) as u8;
let darkest = (index as f64 * 0.3) as u8;
builder.push(ItemTheme {
base: ItemColor::from_rgb(Color::Rgb(index, index, index )),
light: ItemColor::from_rgb(Color::Rgb(light, light, light, )),
lighter: ItemColor::from_rgb(Color::Rgb(lighter, lighter, lighter, )),
lightest: ItemColor::from_rgb(Color::Rgb(lightest, lightest, lightest, )),
dark: ItemColor::from_rgb(Color::Rgb(dark, dark, dark, )),
darker: ItemColor::from_rgb(Color::Rgb(darker, darker, darker, )),
darkest: ItemColor::from_rgb(Color::Rgb(darkest, darkest, darkest, )),
});
}
builder.build()
};
pub fn random () -> Self { ItemColor::random().into() }
pub fn random_near (color: Self, distance: f32) -> Self {
color.base.mix(ItemColor::random(), distance).into()
}
pub const G00: Self = {
let color: ItemColor = ItemColor {
okhsl: Okhsl { hue: OklabHue::new(0.0), lightness: 0.0, saturation: 0.0 },
rgb: Color::Rgb(0, 0, 0)
};
Self {
base: color,
light: color,
lighter: color,
lightest: color,
dark: color,
darker: color,
darkest: color,
}
};
pub fn from_tui_color (base: Color) -> Self {
Self::from_item_color(ItemColor::from_rgb(base))
}
pub fn from_item_color (base: ItemColor) -> Self {
let mut light = base.okhsl;
light.lightness = (light.lightness * 1.3).min(1.0);
let mut lighter = light;
lighter.lightness = (lighter.lightness * 1.3).min(1.0);
let mut lightest = base.okhsl;
lightest.lightness = 0.95;
let mut dark = base.okhsl;
dark.lightness = (dark.lightness * 0.75).max(0.0);
dark.saturation = (dark.saturation * 0.75).max(0.0);
let mut darker = dark;
darker.lightness = (darker.lightness * 0.66).max(0.0);
darker.saturation = (darker.saturation * 0.66).max(0.0);
let mut darkest = darker;
darkest.lightness = 0.1;
darkest.saturation = (darkest.saturation * 0.50).max(0.0);
Self {
base,
light: light.into(),
lighter: lighter.into(),
lightest: lightest.into(),
dark: dark.into(),
darker: darker.into(),
darkest: darkest.into(),
}
}
}

View file

@ -1,29 +0,0 @@
use crate::*;
use ratatui::style::Stylize;
// Thunks can be natural error boundaries!
pub struct ErrorBoundary<O: Out, T: Draw<O>>(
std::marker::PhantomData<O>, Perhaps<T>
);
impl<O: Out, T: Draw<O>> ErrorBoundary<O, T> {
pub fn new (content: Perhaps<T>) -> Self {
Self(Default::default(), content)
}
}
impl<T: Draw<TuiOut>> Draw<TuiOut> for ErrorBoundary<TuiOut, T> {
fn draw (&self, to: &mut TuiOut) {
match self.1.as_ref() {
Ok(Some(content)) => content.draw(to),
Ok(None) => to.blit(&"empty?", 0, 0, Some(Style::default().yellow())),
Err(e) => {
let err_fg = Color::Rgb(255,224,244);
let err_bg = Color::Rgb(96,24,24);
let title = Bsp::e(Tui::bold(true, "oops. "), "rendering failed.");
let error = Bsp::e("\"why?\" ", Tui::bold(true, format!("{e}")));
to.place(&Tui::fg_bg(err_fg, err_bg, Bsp::s(title, error)))
}
}
}
}

View file

@ -1,13 +0,0 @@
use crate::*;
impl Draw<TuiOut> for u64 {
fn draw (&self, _to: &mut TuiOut) {
todo!()
}
}
impl Draw<TuiOut> for f64 {
fn draw (&self, _to: &mut TuiOut) {
todo!()
}
}

View file

@ -1,29 +0,0 @@
use crate::*;
/// 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],
}
impl<T> Phat<T> {
pub const LO: &'static str = "";
pub const HI: &'static str = "";
/// A phat line
pub fn lo (fg: Color, bg: Color) -> impl Content<TuiOut> { Fixed::Y(1, Tui::fg_bg(fg, bg, RepeatH(Self::LO))) }
/// A phat line
pub fn hi (fg: Color, bg: Color) -> impl Content<TuiOut> { Fixed::Y(1, Tui::fg_bg(fg, bg, RepeatH(Self::HI))) }
}
impl<T: Content<TuiOut>> HasContent<TuiOut> for Phat<T> {
fn content (&self) -> impl Content<TuiOut> {
let [fg, bg, hi, lo] = self.colors;
let top = Fixed::Y(1, Self::lo(bg, hi));
let low = Fixed::Y(1, Self::hi(bg, lo));
let content = Tui::fg_bg(fg, bg, &self.content);
Min::XY(self.width, self.height, Bsp::s(top, Bsp::n(low, Fill::XY(content))))
}
}

View file

@ -1,44 +0,0 @@
use crate::*;
use ratatui::prelude::Position;
pub struct Repeat<'a>(pub &'a str);
impl Draw<TuiOut> for Repeat<'_> {
fn draw (&self, to: &mut TuiOut) {
let [x, y, w, h] = to.area().xywh();
let a = self.0.len();
for (_v, y) in (y..y+h).enumerate() {
for (u, x) in (x..x+w).enumerate() {
if let Some(cell) = to.buffer.cell_mut(Position::from((x, y))) {
let u = u % a;
cell.set_symbol(&self.0[u..u+1]);
}
}
}
}
}
pub struct RepeatV<'a>(pub &'a str);
impl Layout<TuiOut> for RepeatV<'_> {}
impl Draw<TuiOut> for RepeatV<'_> {
fn draw (&self, to: &mut TuiOut) {
let [x, y, _w, h] = to.area().xywh();
for y in y..y+h {
if let Some(cell) = to.buffer.cell_mut(Position::from((x, y))) {
cell.set_symbol(&self.0);
}
}
}
}
pub struct RepeatH<'a>(pub &'a str);
impl Layout<TuiOut> for RepeatH<'_> {}
impl Draw<TuiOut> for RepeatH<'_> {
fn draw (&self, to: &mut TuiOut) {
let [x, y, w, _h] = to.area().xywh();
for x in x..x+w {
if let Some(cell) = to.buffer.cell_mut(Position::from((x, y))) {
cell.set_symbol(&self.0);
}
}
}
}

View file

@ -1,80 +0,0 @@
use crate::*;
use ratatui::{prelude::Position, style::Color::*};
pub struct ScrollbarV {
pub offset: usize,
pub length: usize,
pub total: usize,
}
pub struct ScrollbarH {
pub offset: usize,
pub length: usize,
pub total: usize,
}
impl ScrollbarV {
const ICON_DEC: &[char] = &['▲'];
const ICON_INC: &[char] = &['▼'];
}
impl ScrollbarH {
const ICON_DEC: &[char] = &[' ', '🞀', ' '];
const ICON_INC: &[char] = &[' ', '🞂', ' '];
}
impl Draw<TuiOut> for ScrollbarV {
fn draw (&self, to: &mut TuiOut) {
let [x, y1, _w, h] = to.area().xywh();
let y2 = y1 + h;
for (i, y) in (y1..=y2).enumerate() {
if let Some(cell) = to.buffer.cell_mut(Position::from((x, y))) {
if (i as usize) < (Self::ICON_DEC.len()) {
cell.set_fg(Rgb(255, 255, 255));
cell.set_bg(Rgb(0, 0, 0));
cell.set_char(Self::ICON_DEC[i as usize]);
} else if (i as usize) > (h as usize - Self::ICON_INC.len()) {
cell.set_fg(Rgb(255, 255, 255));
cell.set_bg(Rgb(0, 0, 0));
cell.set_char(Self::ICON_INC[h as usize - i]);
} else if false {
cell.set_fg(Rgb(255, 255, 255));
cell.set_bg(Reset);
cell.set_char('‖'); // ━
} else {
cell.set_fg(Rgb(0, 0, 0));
cell.set_bg(Reset);
cell.set_char('╎'); // ━
}
}
}
}
}
impl Draw<TuiOut> for ScrollbarH {
fn draw (&self, to: &mut TuiOut) {
let [x1, y, w, _h] = to.area().xywh();
let x2 = x1 + w;
for (i, x) in (x1..=x2).enumerate() {
if let Some(cell) = to.buffer.cell_mut(Position::from((x, y))) {
if i < (Self::ICON_DEC.len()) {
cell.set_fg(Rgb(255, 255, 255));
cell.set_bg(Rgb(0, 0, 0));
cell.set_char(Self::ICON_DEC[i as usize]);
} else if i > (w as usize - Self::ICON_INC.len()) {
cell.set_fg(Rgb(255, 255, 255));
cell.set_bg(Rgb(0, 0, 0));
cell.set_char(Self::ICON_INC[w as usize - i]);
} else if false {
cell.set_fg(Rgb(255, 255, 255));
cell.set_bg(Reset);
cell.set_char('━');
} else {
cell.set_fg(Rgb(0, 0, 0));
cell.set_bg(Reset);
cell.set_char('╌');
}
}
}
}
}

View file

@ -1,105 +0,0 @@
use crate::*;
use crate::ratatui::prelude::Position;
use unicode_width::{UnicodeWidthStr, UnicodeWidthChar};
impl Draw<TuiOut> for &str {
fn draw (&self, to: &mut TuiOut) {
let [x, y, w, ..] = self.layout(to.area());
to.text(&self, x, y, w)
}
}
impl Draw<TuiOut> for String {
fn draw (&self, to: &mut TuiOut) {
self.as_str().draw(to)
}
}
impl Draw<TuiOut> for Arc<str> {
fn draw (&self, to: &mut TuiOut) { self.as_ref().draw(to) }
}
impl Layout<TuiOut> for &str {
fn layout (&self, to: [u16;4]) -> [u16;4] { to.center_xy([width_chars_max(to.w(), self), 1]) }
}
impl Layout<TuiOut> for String {
fn layout (&self, to: [u16;4]) -> [u16;4] {
self.as_str().layout(to)
}
}
impl Layout<TuiOut> for Arc<str> {
fn layout (&self, to: [u16;4]) -> [u16;4] {
self.as_ref().layout(to)
}
}
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
}
/// 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()
}
/// 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);
impl<'a, T: AsRef<str>> TrimString<T> {
fn as_ref (&self) -> TrimStringRef<'_, T> { TrimStringRef(self.0, &self.1) }
}
impl<'a, T: AsRef<str>> Draw<TuiOut> for TrimString<T> {
fn draw (&self, to: &mut TuiOut) { Draw::draw(&self.as_ref(), to) }
}
impl<'a, T: AsRef<str>> Layout<TuiOut> for TrimString<T> {
fn layout (&self, to: [u16; 4]) -> [u16;4] { Layout::layout(&self.as_ref(), to) }
}
/// 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);
impl<'a, T: AsRef<str>> Layout<TuiOut> for TrimStringRef<'a, T> {
fn layout (&self, to: [u16; 4]) -> [u16;4] {
[to.x(), to.y(), to.w().min(self.0).min(self.1.as_ref().width() as u16), to.h()]
}
}
impl<T: AsRef<str>> Draw<TuiOut> for TrimStringRef<'_, T> {
fn draw (&self, target: &mut TuiOut) {
let area = target.area();
let mut width: u16 = 1;
let mut chars = self.1.as_ref().chars();
while let Some(c) = chars.next() {
if width > self.0 || width > area.w() {
break
}
if let Some(cell) = target.buffer.cell_mut(Position {
x: area.x() + width - 1,
y: area.y()
}) {
cell.set_char(c);
}
width += c.width().unwrap_or(0) as u16;
}
}
}

View file

@ -1,28 +0,0 @@
use crate::*;
impl<
A: Content<TuiOut>,
B: Content<TuiOut>,
C: Content<TuiOut>,
> HasContent<TuiOut> for Tryptich<A, B, C> {
fn content (&self) -> impl Content<TuiOut> {
let Self { top, h, left: (w_a, ref a), middle: (w_b, ref b), right: (w_c, ref c) } = *self;
Fixed::Y(h, if top {
Bsp::a(
Fill::X(Align::n(Fixed::X(w_b, Align::x(Tui::bg(Color::Reset, b))))),
Bsp::a(
Fill::X(Align::nw(Fixed::X(w_a, Tui::bg(Color::Reset, a)))),
Fill::X(Align::ne(Fixed::X(w_c, Tui::bg(Color::Reset, c)))),
),
)
} else {
Bsp::a(
Fill::XY(Align::c(Fixed::X(w_b, Align::x(Tui::bg(Color::Reset, b))))),
Bsp::a(
Fill::XY(Align::w(Fixed::X(w_a, Tui::bg(Color::Reset, a)))),
Fill::XY(Align::e(Fixed::X(w_c, Tui::bg(Color::Reset, c)))),
),
)
})
}
}

View file

@ -1,148 +0,0 @@
use crate::*;
use std::time::Duration;
mod tui_buffer; pub use self::tui_buffer::*;
mod tui_input; pub use self::tui_input::*;
mod tui_event; pub use self::tui_event::*;
mod tui_output; pub use self::tui_output::*;
mod tui_perf; pub use self::tui_perf::*;
// 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,
}
impl Tui {
/// Construct a new TUI engine and wrap it for shared ownership.
pub fn new () -> Usually<Arc<RwLock<Self>>> {
let backend = CrosstermBackend::new(stdout());
let Size { width, height } = backend.size()?;
Ok(Arc::new(RwLock::new(Self {
exited: Arc::new(AtomicBool::new(false)),
buffer: Buffer::empty(Rect { x: 0, y: 0, width, height }),
area: [0, 0, width, height],
backend,
perf: Default::default(),
})))
}
/// True if done
pub fn exited (&self) -> bool {
self.exited.fetch_and(true, Relaxed)
}
/// Prepare before run
pub fn setup (&mut self) -> 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)?;
self.backend.hide_cursor()?;
enable_raw_mode().map_err(Into::into)
}
/// Update the display buffer.
pub fn flip (&mut self, mut buffer: Buffer, size: ratatui::prelude::Rect) -> Buffer {
if self.buffer.area != size {
self.backend.clear_region(ClearType::All).unwrap();
self.buffer.resize(size);
self.buffer.reset();
}
let updates = self.buffer.diff(&buffer);
self.backend.draw(updates.into_iter()).expect("failed to render");
self.backend.flush().expect("failed to flush output buffer");
std::mem::swap(&mut self.buffer, &mut buffer);
buffer.reset();
buffer
}
/// Clean up after run
pub fn teardown (&mut self) -> Usually<()> {
stdout().execute(LeaveAlternateScreen)?;
self.backend.show_cursor()?;
disable_raw_mode().map_err(Into::into)
}
}
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 TuiRun<T: TuiWidget + 'static> {
/// Run an app in the main loop.
fn run (&self, state: &Arc<RwLock<T>>) -> Usually<()>;
}
impl<T: TuiWidget + Send + Sync + 'static> TuiRun<T> for Arc<RwLock<Tui>> {
fn run (&self, state: &Arc<RwLock<T>>) -> Usually<()> {
let _input_thread = TuiIn::run_input(self, state, Duration::from_millis(100));
self.write().unwrap().setup()?;
let render_thread = TuiOut::run_output(self, state, Duration::from_millis(10))?;
match render_thread.join() {
Ok(result) => {
self.write().unwrap().teardown()?;
println!("\n\rRan successfully: {result:?}\n\r");
},
Err(error) => {
self.write().unwrap().teardown()?;
panic!("\n\rDraw thread failed: error={error:?}.\n\r")
},
}
Ok(())
}
}
#[cfg(feature = "dsl")]
pub fn evaluate_output_expression_tui <'a, S> (
state: &S, output: &mut TuiOut, expr: impl Expression + 'a
) -> Usually<bool> where
S: View<TuiOut, ()>
+ for<'b>Namespace<'b, bool>
+ for<'b>Namespace<'b, u16>
+ for<'b>Namespace<'b, Color>
{
// See `tengri_output::evaluate_output_expression`
let head = expr.head()?;
let mut frags = head.src()?.unwrap_or_default().split("/");
let args = expr.tail();
let arg0 = args.head();
let tail0 = args.tail();
let arg1 = tail0.head();
let tail1 = tail0.tail();
let _arg2 = tail1.head();
match frags.next() {
Some("text") => if let Some(src) = args?.src()? { output.place(&src) },
Some("fg") => {
let arg0 = arg0?.expect("fg: expected arg 0 (color)");
output.place(&Tui::fg(
Namespace::<Color>::resolve(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>::resolve(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)
}

View file

@ -1,44 +0,0 @@
use crate::*;
pub fn buffer_update (buf: &mut Buffer, area: [u16;4], 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);
}
}
}
}
}
#[derive(Default)] pub struct BigBuffer {
pub width: usize,
pub height: usize,
pub content: Vec<Cell>
}
impl_debug!(BigBuffer |self, f| {
write!(f, "[BB {}x{} ({})]", self.width, self.height, self.content.len())
});
impl BigBuffer {
pub fn new (width: usize, height: usize) -> Self {
Self { width, height, content: vec![Cell::default(); width*height] }
}
pub fn get (&self, x: usize, y: usize) -> Option<&Cell> {
let i = self.index_of(x, y);
self.content.get(i)
}
pub fn get_mut (&mut self, x: usize, y: usize) -> Option<&mut Cell> {
let i = self.index_of(x, y);
self.content.get_mut(i)
}
pub fn index_of (&self, x: usize, y: usize) -> usize {
y * self.width + x
}
}
from!(BigBuffer: |size:(usize, usize)| Self::new(size.0, size.1));

View file

@ -1,148 +0,0 @@
use crate::*;
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)] pub struct TuiEvent(pub Event);
impl Ord for TuiEvent {
fn cmp (&self, other: &Self) -> std::cmp::Ordering {
self.partial_cmp(other)
.unwrap_or_else(||format!("{:?}", self).cmp(&format!("{other:?}"))) // FIXME perf
}
}
impl TuiEvent {
pub fn from_crossterm (event: Event) -> Self {
Self(event)
}
#[cfg(feature = "dsl")]
pub fn from_dsl (dsl: impl Language) -> Perhaps<Self> {
Ok(TuiKey::from_dsl(dsl)?.to_crossterm().map(Self))
}
}
pub struct TuiKey(Option<KeyCode>, KeyModifiers);
impl TuiKey {
const SPLIT: char = '/';
#[cfg(feature = "dsl")]
pub fn from_dsl (dsl: impl Language) -> Usually<Self> {
if let Some(word) = dsl.word()? {
let word = word.trim();
Ok(if word == ":char" {
Self(None, KeyModifiers::NONE)
} else if word.chars().nth(0) == Some('@') {
let mut key = None;
let mut modifiers = KeyModifiers::NONE;
let mut tokens = word[1..].split(Self::SPLIT).peekable();
while let Some(token) = tokens.next() {
if tokens.peek().is_some() {
match token {
"ctrl" | "Ctrl" | "c" | "C" => modifiers |= KeyModifiers::CONTROL,
"alt" | "Alt" | "m" | "M" => modifiers |= KeyModifiers::ALT,
"shift" | "Shift" | "s" | "S" => {
modifiers |= KeyModifiers::SHIFT;
// + TODO normalize character case, BackTab, etc.
},
_ => panic!("unknown modifier {token}"),
}
} else {
key = if token.len() == 1 {
Some(KeyCode::Char(token.chars().next().unwrap()))
} else {
Some(named_key(token).unwrap_or_else(||panic!("unknown character {token}")))
}
}
}
Self(key, modifiers)
} else {
return Err(format!("TuiKey: unexpected: {word}").into())
})
} else {
return Err(format!("TuiKey: unspecified").into())
}
}
pub fn to_crossterm (&self) -> Option<Event> {
self.0.map(|code|Event::Key(KeyEvent {
code,
modifiers: self.1,
kind: KeyEventKind::Press,
state: KeyEventState::NONE,
}))
}
}
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 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
//}

View file

@ -1,55 +0,0 @@
use crate::*;
use std::time::Duration;
use std::thread::{spawn, JoinHandle};
use crossterm::event::{Event, poll, read};
#[derive(Debug, Clone)]
pub struct TuiIn {
/// Exit flag
pub exited: Arc<AtomicBool>,
/// Input event
pub event: TuiEvent,
}
impl Input for TuiIn {
type Event = TuiEvent;
type Handled = bool;
fn event (&self) -> &TuiEvent { &self.event }
fn is_done (&self) -> bool { self.exited.fetch_and(true, Relaxed) }
fn done (&self) { self.exited.store(true, Relaxed); }
}
impl TuiIn {
/// Spawn the input thread.
pub fn run_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}")
}
}
}
}
})
}
}

View file

@ -1,150 +0,0 @@
use crate::*;
use std::time::Duration;
use std::thread::{spawn, JoinHandle};
use unicode_width::*;
#[derive(Default)]
pub struct TuiOut {
pub buffer: Buffer,
pub area: [u16;4]
}
impl Out for TuiOut {
type Unit = u16;
type Size = [Self::Unit;2];
type Area = [Self::Unit;4];
#[inline] fn area (&self) -> [u16;4] {
self.area
}
#[inline] fn area_mut (&mut self) -> &mut [u16;4] {
&mut self.area
}
#[inline] fn place_at <'t, T: Draw<Self> + ?Sized> (&mut self, area: [u16;4], content: &'t T) {
let last = self.area();
*self.area_mut() = area;
content.draw(self);
*self.area_mut() = last;
}
}
impl TuiOut {
/// Spawn the output thread.
pub fn run_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: [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);
})
}
#[inline]
pub fn with_rect (&mut self, area: [u16;4]) -> &mut Self {
self.area = area;
self
}
pub fn blit (
&mut self, text: &impl AsRef<str>, x: u16, y: u16, style: Option<Style>
) {
let text = text.as_ref();
let buf = &mut self.buffer;
let style = style.unwrap_or(Style::default());
if x < buf.area.width && y < buf.area.height {
buf.set_string(x, y, text, style);
}
}
/// Write a line of text
///
/// TODO: do a paragraph (handle newlines)
pub fn text (&mut self, text: &impl AsRef<str>, x0: u16, y: u16, max_width: u16) {
let text = text.as_ref();
let buf = &mut self.buffer;
let mut string_width: u16 = 0;
for character in text.chars() {
let x = x0 + string_width;
let character_width = character.width().unwrap_or(0) as u16;
string_width += character_width;
if string_width > max_width {
break
}
if let Some(cell) = buf.cell_mut(ratatui::prelude::Position { x, y }) {
cell.set_char(character);
} else {
break
}
}
}
pub fn buffer_update (&mut self, area: [u16;4], callback: &impl Fn(&mut Cell, u16, u16)) {
buffer_update(&mut self.buffer, area, callback);
}
pub fn fill_char (&mut self, area: [u16;4], c: char) {
self.buffer_update(area, &|cell,_,_|{cell.set_char(c);})
}
pub fn fill_bg (&mut self, area: [u16;4], color: Color) {
self.buffer_update(area, &|cell,_,_|{cell.set_bg(color);})
}
pub fn fill_fg (&mut self, area: [u16;4], color: Color) {
self.buffer_update(area, &|cell,_,_|{cell.set_fg(color);})
}
pub fn fill_mod (&mut self, area: [u16;4], on: bool, modifier: Modifier) {
if on {
self.buffer_update(area, &|cell,_,_|cell.modifier.insert(modifier))
} else {
self.buffer_update(area, &|cell,_,_|cell.modifier.remove(modifier))
}
}
pub fn fill_bold (&mut self, area: [u16;4], on: bool) {
self.fill_mod(area, on, Modifier::BOLD)
}
pub fn fill_reversed (&mut self, area: [u16;4], on: bool) {
self.fill_mod(area, on, Modifier::REVERSED)
}
pub fn fill_crossed_out (&mut self, area: [u16;4], on: bool) {
self.fill_mod(area, on, Modifier::CROSSED_OUT)
}
pub fn fill_ul (&mut self, area: [u16;4], color: Option<Color>) {
if let Some(color) = color {
self.buffer_update(area, &|cell,_,_|{
cell.modifier.insert(ratatui::prelude::Modifier::UNDERLINED);
cell.underline_color = color;
})
} else {
self.buffer_update(area, &|cell,_,_|{
cell.modifier.remove(ratatui::prelude::Modifier::UNDERLINED);
})
}
}
pub fn tint_all (&mut self, fg: Color, bg: Color, modifier: Modifier) {
for cell in self.buffer.content.iter_mut() {
cell.fg = fg;
cell.bg = bg;
cell.modifier = modifier;
}
}
}

View file

@ -1,64 +0,0 @@
use crate::*;
/// 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,
}
pub trait HasPerf {
fn perf (&self) -> &PerfModel;
}
impl Default for PerfModel {
fn default () -> Self {
Self {
enabled: true,
clock: quanta::Clock::new(),
used: Default::default(),
window: Default::default(),
}
}
}
impl PerfModel {
pub fn get_t0 (&self) -> Option<u64> {
if self.enabled {
Some(self.clock.raw())
} else {
None
}
}
pub fn get_t1 (&self, t0: Option<u64>) -> Option<std::time::Duration> {
if let Some(t0) = t0 {
if self.enabled {
Some(self.clock.delta(t0, self.clock.raw()))
} else {
None
}
} else {
None
}
}
pub fn update (&self, t0: Option<u64>, microseconds: f64) {
if let Some(t0) = t0 {
let t1 = self.clock.raw();
self.used.store(self.clock.delta_as_nanos(t0, t1) as f64, Relaxed);
self.window.store(microseconds, Relaxed,);
}
}
pub fn percentage (&self) -> Option<f64> {
let window = self.window.load(Relaxed) * 1000.0;
if window > 0.0 {
let used = self.used.load(Relaxed);
Some(100.0 * used / window)
} else {
None
}
}
}

885
tui/src/tui_impls.rs Normal file
View file

@ -0,0 +1,885 @@
use crate::*;
use ratatui::style::Stylize;
use ratatui::{prelude::Position, style::Color::*};
use crate::ratatui::prelude::Position;
use unicode_width::{UnicodeWidthStr, UnicodeWidthChar};
use ratatui::prelude::Position;
use rand::{thread_rng, distributions::uniform::UniformSampler};
impl Tui {
pub const fn fg <T> (color: Color, w: T) -> Foreground<Color, T> { Foreground(color, w) }
pub const fn bg <T> (color: Color, w: T) -> Background<Color, T> { Background(color, w) }
pub const fn fg_bg <T> (fg: Color, bg: Color, w: T) -> Background<Color, Foreground<Color, T>> { Background(bg, Foreground(fg, w)) }
pub const fn modify <T> (enable: bool, modifier: Modifier, w: T) -> Modify<T> { Modify(enable, modifier, w) }
pub const fn bold <T> (enable: bool, w: T) -> Modify<T> { Self::modify(enable, Modifier::BOLD, w) }
pub const fn border <S, T> (enable: bool, style: S, w: T) -> Bordered<S, T> { Bordered(enable, style, w) }
pub const fn null () -> Color { Color::Reset }
pub const fn g (g: u8) -> Color { Color::Rgb(g, g, g) }
pub const fn red () -> Color { Color::Rgb(255,0, 0) }
pub const fn orange () -> Color { Color::Rgb(255,128,0) }
pub const fn yellow () -> Color { Color::Rgb(255,255,0) }
pub const fn brown () -> Color { Color::Rgb(128,255,0) }
pub const fn green () -> Color { Color::Rgb(0,255,0) }
pub const fn electric () -> Color { Color::Rgb(0,255,128) }
//fn bg0 () -> Color { Color::Rgb(20, 20, 20) }
//fn bg () -> Color { Color::Rgb(28, 35, 25) }
//fn border_bg () -> Color { Color::Rgb(40, 50, 30) }
//fn border_fg (f: bool) -> Color { if f { Self::bo1() } else { Self::bo2() } }
//fn title_fg (f: bool) -> Color { if f { Self::ti1() } else { Self::ti2() } }
//fn separator_fg (_: bool) -> Color { Color::Rgb(0, 0, 0) }
//fn mode_bg () -> Color { Color::Rgb(150, 160, 90) }
//fn mode_fg () -> Color { Color::Rgb(255, 255, 255) }
//fn status_bar_bg () -> Color { Color::Rgb(28, 35, 25) }
//fn bo1 () -> Color { Color::Rgb(100, 110, 40) }
//fn bo2 () -> Color { Color::Rgb(70, 80, 50) }
//fn ti1 () -> Color { Color::Rgb(150, 160, 90) }
//fn ti2 () -> Color { Color::Rgb(120, 130, 100) }
/// Construct a new TUI engine and wrap it for shared ownership.
pub fn new () -> Usually<Arc<RwLock<Self>>> {
let backend = CrosstermBackend::new(stdout());
let Size { width, height } = backend.size()?;
Ok(Arc::new(RwLock::new(Self {
exited: Arc::new(AtomicBool::new(false)),
buffer: Buffer::empty(Rect { x: 0, y: 0, width, height }),
area: [0, 0, width, height],
backend,
perf: Default::default(),
})))
}
/// True if done
pub fn exited (&self) -> bool {
self.exited.fetch_and(true, Relaxed)
}
/// Prepare before run
pub fn setup (&mut self) -> 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)?;
self.backend.hide_cursor()?;
enable_raw_mode().map_err(Into::into)
}
/// Update the display buffer.
pub fn flip (&mut self, mut buffer: Buffer, size: ratatui::prelude::Rect) -> Buffer {
if self.buffer.area != size {
self.backend.clear_region(ClearType::All).unwrap();
self.buffer.resize(size);
self.buffer.reset();
}
let updates = self.buffer.diff(&buffer);
self.backend.draw(updates.into_iter()).expect("failed to render");
self.backend.flush().expect("failed to flush output buffer");
std::mem::swap(&mut self.buffer, &mut buffer);
buffer.reset();
buffer
}
/// Clean up after run
pub fn teardown (&mut self) -> Usually<()> {
stdout().execute(LeaveAlternateScreen)?;
self.backend.show_cursor()?;
disable_raw_mode().map_err(Into::into)
}
}
impl<T: TuiWidget + Send + Sync + 'static> TuiRun<T> for Arc<RwLock<Tui>> {
fn run (&self, state: &Arc<RwLock<T>>) -> Usually<()> {
let _input_thread = TuiIn::run_input(self, state, Duration::from_millis(100));
self.write().unwrap().setup()?;
let render_thread = TuiOut::run_output(self, state, Duration::from_millis(10))?;
match render_thread.join() {
Ok(result) => {
self.write().unwrap().teardown()?;
println!("\n\rRan successfully: {result:?}\n\r");
},
Err(error) => {
self.write().unwrap().teardown()?;
panic!("\n\rDraw thread failed: error={error:?}.\n\r")
},
}
Ok(())
}
}
impl Ord for TuiEvent {
fn cmp (&self, other: &Self) -> std::cmp::Ordering {
self.partial_cmp(other)
.unwrap_or_else(||format!("{:?}", self).cmp(&format!("{other:?}"))) // FIXME perf
}
}
impl TuiEvent {
pub fn from_crossterm (event: Event) -> Self {
Self(event)
}
#[cfg(feature = "dsl")]
pub fn from_dsl (dsl: impl Language) -> Perhaps<Self> {
Ok(TuiKey::from_dsl(dsl)?.to_crossterm().map(Self))
}
}
impl TuiKey {
const SPLIT: char = '/';
#[cfg(feature = "dsl")]
pub fn from_dsl (dsl: impl Language) -> Usually<Self> {
if let Some(word) = dsl.word()? {
let word = word.trim();
Ok(if word == ":char" {
Self(None, KeyModifiers::NONE)
} else if word.chars().nth(0) == Some('@') {
let mut key = None;
let mut modifiers = KeyModifiers::NONE;
let mut tokens = word[1..].split(Self::SPLIT).peekable();
while let Some(token) = tokens.next() {
if tokens.peek().is_some() {
match token {
"ctrl" | "Ctrl" | "c" | "C" => modifiers |= KeyModifiers::CONTROL,
"alt" | "Alt" | "m" | "M" => modifiers |= KeyModifiers::ALT,
"shift" | "Shift" | "s" | "S" => {
modifiers |= KeyModifiers::SHIFT;
// + TODO normalize character case, BackTab, etc.
},
_ => panic!("unknown modifier {token}"),
}
} else {
key = if token.len() == 1 {
Some(KeyCode::Char(token.chars().next().unwrap()))
} else {
Some(named_key(token).unwrap_or_else(||panic!("unknown character {token}")))
}
}
}
Self(key, modifiers)
} else {
return Err(format!("TuiKey: unexpected: {word}").into())
})
} else {
return Err(format!("TuiKey: unspecified").into())
}
}
pub fn to_crossterm (&self) -> Option<Event> {
self.0.map(|code|Event::Key(KeyEvent {
code,
modifiers: self.1,
kind: KeyEventKind::Press,
state: KeyEventState::NONE,
}))
}
}
pub fn buffer_update (buf: &mut Buffer, area: XY<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);
}
}
}
}
}
impl_debug!(BigBuffer |self, f| {
write!(f, "[BB {}x{} ({})]", self.width, self.height, self.content.len())
});
impl BigBuffer {
pub fn new (width: usize, height: usize) -> Self {
Self { width, height, content: vec![Cell::default(); width*height] }
}
pub fn get (&self, x: usize, y: usize) -> Option<&Cell> {
let i = self.index_of(x, y);
self.content.get(i)
}
pub fn get_mut (&mut self, x: usize, y: usize) -> Option<&mut Cell> {
let i = self.index_of(x, y);
self.content.get_mut(i)
}
pub fn index_of (&self, x: usize, y: usize) -> usize {
y * self.width + x
}
}
from!(BigBuffer: |size:(usize, usize)| Self::new(size.0, size.1));
impl Input for TuiIn {
type Event = TuiEvent;
type Handled = bool;
fn event (&self) -> &TuiEvent { &self.event }
fn is_done (&self) -> bool { self.exited.fetch_and(true, Relaxed) }
fn done (&self) { self.exited.store(true, Relaxed); }
}
impl TuiIn {
/// Spawn the input thread.
pub fn run_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}")
}
}
}
}
})
}
}
pub trait HasPerf {
fn perf (&self) -> &PerfModel;
}
impl Default for PerfModel {
fn default () -> Self {
Self {
enabled: true,
clock: quanta::Clock::new(),
used: Default::default(),
window: Default::default(),
}
}
}
impl PerfModel {
pub fn get_t0 (&self) -> Option<u64> {
if self.enabled {
Some(self.clock.raw())
} else {
None
}
}
pub fn get_t1 (&self, t0: Option<u64>) -> Option<std::time::Duration> {
if let Some(t0) = t0 {
if self.enabled {
Some(self.clock.delta(t0, self.clock.raw()))
} else {
None
}
} else {
None
}
}
pub fn update (&self, t0: Option<u64>, microseconds: f64) {
if let Some(t0) = t0 {
let t1 = self.clock.raw();
self.used.store(self.clock.delta_as_nanos(t0, t1) as f64, Relaxed);
self.window.store(microseconds, Relaxed,);
}
}
pub fn percentage (&self) -> Option<f64> {
let window = self.window.load(Relaxed) * 1000.0;
if window > 0.0 {
let used = self.used.load(Relaxed);
Some(100.0 * used / window)
} else {
None
}
}
}
from!(ItemColor: |okhsl: Okhsl<f32>|Self { okhsl, rgb: okhsl_to_rgb(okhsl) });
from!(ItemColor: |rgb: Color|Self { rgb, okhsl: rgb_to_okhsl(rgb) });
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")
}
}
// A single color within item theme parameters, in OKHSL and RGB representations.
impl ItemColor {
pub const fn from_rgb (rgb: Color) -> Self {
Self { rgb, okhsl: Okhsl::new_const(OklabHue::new(0.0), 0.0, 0.0) }
}
pub const fn from_okhsl (okhsl: Okhsl<f32>) -> Self {
Self { rgb: Color::Rgb(0, 0, 0), okhsl }
}
pub fn random () -> Self {
let mut rng = thread_rng();
let lo = Okhsl::new(-180.0, 0.01, 0.25);
let hi = Okhsl::new( 180.0, 0.9, 0.5);
UniformOkhsl::new(lo, hi).sample(&mut rng).into()
}
pub fn random_dark () -> Self {
let mut rng = thread_rng();
let lo = Okhsl::new(-180.0, 0.025, 0.075);
let hi = Okhsl::new( 180.0, 0.5, 0.150);
UniformOkhsl::new(lo, hi).sample(&mut rng).into()
}
pub fn random_near (color: Self, distance: f32) -> Self {
color.mix(Self::random(), distance)
}
pub fn mix (&self, other: Self, distance: f32) -> Self {
if distance > 1.0 { panic!("color mixing takes distance between 0.0 and 1.0"); }
self.okhsl.mix(other.okhsl, distance).into()
}
}
from!(ItemTheme: |base: Color| Self::from_tui_color(base));
from!(ItemTheme: |base: ItemColor|Self::from_item_color(base));
impl ItemTheme {
pub const G: [Self;256] = {
let mut builder = konst::array::ArrayBuilder::new();
while !builder.is_full() {
let index = builder.len() as u8;
let light = (index as f64 * 1.15) as u8;
let lighter = (index as f64 * 1.7) as u8;
let lightest = (index as f64 * 1.85) as u8;
let dark = (index as f64 * 0.9) as u8;
let darker = (index as f64 * 0.6) as u8;
let darkest = (index as f64 * 0.3) as u8;
builder.push(ItemTheme {
base: ItemColor::from_rgb(Color::Rgb(index, index, index )),
light: ItemColor::from_rgb(Color::Rgb(light, light, light, )),
lighter: ItemColor::from_rgb(Color::Rgb(lighter, lighter, lighter, )),
lightest: ItemColor::from_rgb(Color::Rgb(lightest, lightest, lightest, )),
dark: ItemColor::from_rgb(Color::Rgb(dark, dark, dark, )),
darker: ItemColor::from_rgb(Color::Rgb(darker, darker, darker, )),
darkest: ItemColor::from_rgb(Color::Rgb(darkest, darkest, darkest, )),
});
}
builder.build()
};
pub fn random () -> Self { ItemColor::random().into() }
pub fn random_near (color: Self, distance: f32) -> Self {
color.base.mix(ItemColor::random(), distance).into()
}
pub const G00: Self = {
let color: ItemColor = ItemColor {
okhsl: Okhsl { hue: OklabHue::new(0.0), lightness: 0.0, saturation: 0.0 },
rgb: Color::Rgb(0, 0, 0)
};
Self {
base: color,
light: color,
lighter: color,
lightest: color,
dark: color,
darker: color,
darkest: color,
}
};
pub fn from_tui_color (base: Color) -> Self {
Self::from_item_color(ItemColor::from_rgb(base))
}
pub fn from_item_color (base: ItemColor) -> Self {
let mut light = base.okhsl;
light.lightness = (light.lightness * 1.3).min(1.0);
let mut lighter = light;
lighter.lightness = (lighter.lightness * 1.3).min(1.0);
let mut lightest = base.okhsl;
lightest.lightness = 0.95;
let mut dark = base.okhsl;
dark.lightness = (dark.lightness * 0.75).max(0.0);
dark.saturation = (dark.saturation * 0.75).max(0.0);
let mut darker = dark;
darker.lightness = (darker.lightness * 0.66).max(0.0);
darker.saturation = (darker.saturation * 0.66).max(0.0);
let mut darkest = darker;
darkest.lightness = 0.1;
darkest.saturation = (darkest.saturation * 0.50).max(0.0);
Self {
base,
light: light.into(),
lighter: lighter.into(),
lightest: lightest.into(),
dark: dark.into(),
darker: darker.into(),
darkest: darkest.into(),
}
}
}
impl Draw<TuiOut> for u64 {
fn draw (&self, _to: &mut TuiOut) {
todo!()
}
}
impl Draw<TuiOut> for f64 {
fn draw (&self, _to: &mut TuiOut) {
todo!()
}
}
impl<T> Phat<T> {
pub const LO: &'static str = "";
pub const HI: &'static str = "";
/// A phat line
pub fn lo (fg: Color, bg: Color) -> impl Content<TuiOut> { Fixed::Y(1, Tui::fg_bg(fg, bg, RepeatH(Self::LO))) }
/// A phat line
pub fn hi (fg: Color, bg: Color) -> impl Content<TuiOut> { Fixed::Y(1, Tui::fg_bg(fg, bg, RepeatH(Self::HI))) }
}
impl<T: Content<TuiOut>> HasContent<TuiOut> for Phat<T> {
fn content (&self) -> impl Content<TuiOut> {
let [fg, bg, hi, lo] = self.colors;
let top = Fixed::Y(1, Self::lo(bg, hi));
let low = Fixed::Y(1, Self::hi(bg, lo));
let content = Tui::fg_bg(fg, bg, &self.content);
Min::XY(self.width, self.height, Bsp::s(top, Bsp::n(low, Fill::XY(content))))
}
}
impl Draw<TuiOut> for Repeat<'_> {
fn draw (&self, to: &mut TuiOut) {
let [x, y, w, h] = to.area().xywh();
let a = self.0.len();
for (_v, y) in (y..y+h).enumerate() {
for (u, x) in (x..x+w).enumerate() {
if let Some(cell) = to.buffer.cell_mut(Position::from((x, y))) {
let u = u % a;
cell.set_symbol(&self.0[u..u+1]);
}
}
}
}
}
impl Layout<TuiOut> for RepeatV<'_> {}
impl Draw<TuiOut> for RepeatV<'_> {
fn draw (&self, to: &mut TuiOut) {
let [x, y, _w, h] = to.area().xywh();
for y in y..y+h {
if let Some(cell) = to.buffer.cell_mut(Position::from((x, y))) {
cell.set_symbol(&self.0);
}
}
}
}
impl Layout<TuiOut> for RepeatH<'_> {}
impl Draw<TuiOut> for RepeatH<'_> {
fn draw (&self, to: &mut TuiOut) {
let [x, y, w, _h] = to.area().xywh();
for x in x..x+w {
if let Some(cell) = to.buffer.cell_mut(Position::from((x, y))) {
cell.set_symbol(&self.0);
}
}
}
}
impl ScrollbarV {
const ICON_DEC: &[char] = &['▲'];
const ICON_INC: &[char] = &['▼'];
}
impl ScrollbarH {
const ICON_DEC: &[char] = &[' ', '🞀', ' '];
const ICON_INC: &[char] = &[' ', '🞂', ' '];
}
impl Draw<TuiOut> for ScrollbarV {
fn draw (&self, to: &mut TuiOut) {
let [x, y1, _w, h] = to.area().xywh();
let y2 = y1 + h;
for (i, y) in (y1..=y2).enumerate() {
if let Some(cell) = to.buffer.cell_mut(Position::from((x, y))) {
if (i as usize) < (Self::ICON_DEC.len()) {
cell.set_fg(Rgb(255, 255, 255));
cell.set_bg(Rgb(0, 0, 0));
cell.set_char(Self::ICON_DEC[i as usize]);
} else if (i as usize) > (h as usize - Self::ICON_INC.len()) {
cell.set_fg(Rgb(255, 255, 255));
cell.set_bg(Rgb(0, 0, 0));
cell.set_char(Self::ICON_INC[h as usize - i]);
} else if false {
cell.set_fg(Rgb(255, 255, 255));
cell.set_bg(Reset);
cell.set_char('‖'); // ━
} else {
cell.set_fg(Rgb(0, 0, 0));
cell.set_bg(Reset);
cell.set_char('╎'); // ━
}
}
}
}
}
impl Draw<TuiOut> for ScrollbarH {
fn draw (&self, to: &mut TuiOut) {
let [x1, y, w, _h] = to.area().xywh();
let x2 = x1 + w;
for (i, x) in (x1..=x2).enumerate() {
if let Some(cell) = to.buffer.cell_mut(Position::from((x, y))) {
if i < (Self::ICON_DEC.len()) {
cell.set_fg(Rgb(255, 255, 255));
cell.set_bg(Rgb(0, 0, 0));
cell.set_char(Self::ICON_DEC[i as usize]);
} else if i > (w as usize - Self::ICON_INC.len()) {
cell.set_fg(Rgb(255, 255, 255));
cell.set_bg(Rgb(0, 0, 0));
cell.set_char(Self::ICON_INC[w as usize - i]);
} else if false {
cell.set_fg(Rgb(255, 255, 255));
cell.set_bg(Reset);
cell.set_char('━');
} else {
cell.set_fg(Rgb(0, 0, 0));
cell.set_bg(Reset);
cell.set_char('╌');
}
}
}
}
}
impl Draw<TuiOut> for &str {
fn draw (&self, to: &mut TuiOut) {
let [x, y, w, ..] = self.layout(to.area());
to.text(&self, x, y, w)
}
}
impl Draw<TuiOut> for String {
fn draw (&self, to: &mut TuiOut) {
self.as_str().draw(to)
}
}
impl Draw<TuiOut> for Arc<str> {
fn draw (&self, to: &mut TuiOut) { self.as_ref().draw(to) }
}
impl Layout<TuiOut> for &str {
fn layout (&self, to: XY<u16>) -> XY<u16> { to.center_xy([width_chars_max(to.w(), self), 1]) }
}
impl Layout<TuiOut> for String {
fn layout (&self, to: XY<u16>) -> XY<u16> {
self.as_str().layout(to)
}
}
impl Layout<TuiOut> for Arc<str> {
fn layout (&self, to: XY<u16>) -> XY<u16> {
self.as_ref().layout(to)
}
}
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
}
/// 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()
}
impl<'a, T: AsRef<str>> TrimString<T> {
fn as_ref (&self) -> TrimStringRef<'_, T> { TrimStringRef(self.0, &self.1) }
}
impl<'a, T: AsRef<str>> Draw<TuiOut> for TrimString<T> {
fn draw (&self, to: &mut TuiOut) { Draw::draw(&self.as_ref(), to) }
}
impl<'a, T: AsRef<str>> Layout<TuiOut> for TrimString<T> {
fn layout (&self, to: XY<u16>) -> XY<u16> { Layout::layout(&self.as_ref(), to) }
}
impl<'a, T: AsRef<str>> Layout<TuiOut> for TrimStringRef<'a, T> {
fn layout (&self, to: XY<u16>) -> XY<u16> {
[to.x(), to.y(), to.w().min(self.0).min(self.1.as_ref().width() as u16), to.h()]
}
}
impl<T: AsRef<str>> Draw<TuiOut> for TrimStringRef<'_, T> {
fn draw (&self, target: &mut TuiOut) {
let area = target.area();
let mut width: u16 = 1;
let mut chars = self.1.as_ref().chars();
while let Some(c) = chars.next() {
if width > self.0 || width > area.w() {
break
}
if let Some(cell) = target.buffer.cell_mut(Position {
x: area.x() + width - 1,
y: area.y()
}) {
cell.set_char(c);
}
width += c.width().unwrap_or(0) as u16;
}
}
}
impl<
A: Content<TuiOut>,
B: Content<TuiOut>,
C: Content<TuiOut>,
> HasContent<TuiOut> for Tryptich<A, B, C> {
fn content (&self) -> impl Content<TuiOut> {
let Self { top, h, left: (w_a, ref a), middle: (w_b, ref b), right: (w_c, ref c) } = *self;
Fixed::Y(h, if top {
Bsp::a(
Fill::X(Align::n(Fixed::X(w_b, Align::x(Tui::bg(Color::Reset, b))))),
Bsp::a(
Fill::X(Align::nw(Fixed::X(w_a, Tui::bg(Color::Reset, a)))),
Fill::X(Align::ne(Fixed::X(w_c, Tui::bg(Color::Reset, c)))),
),
)
} else {
Bsp::a(
Fill::XY(Align::c(Fixed::X(w_b, Align::x(Tui::bg(Color::Reset, b))))),
Bsp::a(
Fill::XY(Align::w(Fixed::X(w_a, Tui::bg(Color::Reset, a)))),
Fill::XY(Align::e(Fixed::X(w_c, Tui::bg(Color::Reset, c)))),
),
)
})
}
}
impl<O: Out, T: Draw<O>> ErrorBoundary<O, T> {
pub fn new (content: Perhaps<T>) -> Self {
Self(Default::default(), content)
}
}
impl<T: Draw<TuiOut>> Draw<TuiOut> for ErrorBoundary<TuiOut, T> {
fn draw (&self, to: &mut TuiOut) {
match self.1.as_ref() {
Ok(Some(content)) => content.draw(to),
Ok(None) => to.blit(&"empty?", 0, 0, Some(Style::default().yellow())),
Err(e) => {
let err_fg = Color::Rgb(255,224,244);
let err_bg = Color::Rgb(96,24,24);
let title = Bsp::e(Tui::bold(true, "oops. "), "rendering failed.");
let error = Bsp::e("\"why?\" ", Tui::bold(true, format!("{e}")));
to.place(&Tui::fg_bg(err_fg, err_bg, Bsp::s(title, error)))
}
}
}
}
impl<S: BorderStyle, W: Content<TuiOut>> HasContent<TuiOut> for Bordered<S, W> {
fn content (&self) -> impl Content<TuiOut> {
Fill::XY(lay!( When::new(self.0, Border(self.0, self.1)), Pad::XY(1, 1, &self.2) ))
}
}
impl<S: BorderStyle> Draw<TuiOut> for Border<S> {
fn draw (&self, to: &mut TuiOut) {
let Border(enabled, style) = self;
if *enabled {
let area = to.area();
if area.w() > 0 && area.y() > 0 {
to.blit(&style.border_nw(), area.x(), area.y(), style.style());
to.blit(&style.border_ne(), area.x() + area.w() - 1, area.y(), style.style());
to.blit(&style.border_sw(), area.x(), area.y() + area.h() - 1, style.style());
to.blit(&style.border_se(), area.x() + area.w() - 1, area.y() + area.h() - 1, style.style());
for x in area.x()+1..area.x()+area.w()-1 {
to.blit(&style.border_n(), x, area.y(), style.style());
to.blit(&style.border_s(), x, area.y() + area.h() - 1, style.style());
}
for y in area.y()+1..area.y()+area.h()-1 {
to.blit(&style.border_w(), area.x(), y, style.style());
to.blit(&style.border_e(), area.x() + area.w() - 1, y, style.style());
}
}
}
}
}
impl<T: Content<TuiOut>> Draw<TuiOut> for Foreground<Color, T> {
fn draw (&self, to: &mut TuiOut) {
let area = self.layout(to.area());
to.fill_fg(area, self.0);
to.place_at(area, &self.1);
}
}
impl<T: Content<TuiOut>> Draw<TuiOut> for Background<Color, T> {
fn draw (&self, to: &mut TuiOut) {
let area = self.layout(to.area());
to.fill_bg(area, self.0);
to.place_at(area, &self.1);
}
}
impl<T: Content<TuiOut>> Layout<TuiOut> for Modify<T> {}
impl<T: Content<TuiOut>> Draw<TuiOut> for Modify<T> {
fn draw (&self, to: &mut TuiOut) {
to.fill_mod(to.area(), self.0, self.1);
self.2.draw(to)
}
}
impl<T: Content<TuiOut>> Layout<TuiOut> for Styled<T> {}
impl<T: Content<TuiOut>> Draw<TuiOut> for Styled<T> {
fn draw (&self, to: &mut TuiOut) {
to.place(&self.1);
// TODO write style over area
}
}
impl TuiOut {
/// Spawn the output thread.
pub fn run_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: [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);
})
}
#[inline]
pub fn with_rect (&mut self, area: XY<u16>) -> &mut Self {
self.area = area;
self
}
pub fn blit (
&mut self, text: &impl AsRef<str>, x: u16, y: u16, style: Option<Style>
) {
let text = text.as_ref();
let buf = &mut self.buffer;
let style = style.unwrap_or(Style::default());
if x < buf.area.width && y < buf.area.height {
buf.set_string(x, y, text, style);
}
}
/// Write a line of text
///
/// TODO: do a paragraph (handle newlines)
pub fn text (&mut self, text: &impl AsRef<str>, x0: u16, y: u16, max_width: u16) {
let text = text.as_ref();
let buf = &mut self.buffer;
let mut string_width: u16 = 0;
for character in text.chars() {
let x = x0 + string_width;
let character_width = character.width().unwrap_or(0) as u16;
string_width += character_width;
if string_width > max_width {
break
}
if let Some(cell) = buf.cell_mut(ratatui::prelude::Position { x, y }) {
cell.set_char(character);
} else {
break
}
}
}
pub fn buffer_update (&mut self, area: XY<u16>, callback: &impl Fn(&mut Cell, u16, u16)) {
buffer_update(&mut self.buffer, area, callback);
}
pub fn fill_char (&mut self, area: XY<u16>, c: char) {
self.buffer_update(area, &|cell,_,_|{cell.set_char(c);})
}
pub fn fill_bg (&mut self, area: XY<u16>, color: Color) {
self.buffer_update(area, &|cell,_,_|{cell.set_bg(color);})
}
pub fn fill_fg (&mut self, area: XY<u16>, color: Color) {
self.buffer_update(area, &|cell,_,_|{cell.set_fg(color);})
}
pub fn fill_mod (&mut self, area: XY<u16>, on: bool, modifier: Modifier) {
if on {
self.buffer_update(area, &|cell,_,_|cell.modifier.insert(modifier))
} else {
self.buffer_update(area, &|cell,_,_|cell.modifier.remove(modifier))
}
}
pub fn fill_bold (&mut self, area: XY<u16>, on: bool) {
self.fill_mod(area, on, Modifier::BOLD)
}
pub fn fill_reversed (&mut self, area: XY<u16>, on: bool) {
self.fill_mod(area, on, Modifier::REVERSED)
}
pub fn fill_crossed_out (&mut self, area: XY<u16>, on: bool) {
self.fill_mod(area, on, Modifier::CROSSED_OUT)
}
pub fn fill_ul (&mut self, area: XY<u16>, color: Option<Color>) {
if let Some(color) = color {
self.buffer_update(area, &|cell,_,_|{
cell.modifier.insert(ratatui::prelude::Modifier::UNDERLINED);
cell.underline_color = color;
})
} else {
self.buffer_update(area, &|cell,_,_|{
cell.modifier.remove(ratatui::prelude::Modifier::UNDERLINED);
})
}
}
pub fn tint_all (&mut self, fg: Color, bg: Color, modifier: Modifier) {
for cell in self.buffer.content.iter_mut() {
cell.fg = fg;
cell.bg = bg;
cell.modifier = modifier;
}
}
}

124
tui/src/tui_structs.rs Normal file
View file

@ -0,0 +1,124 @@
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(Default)] pub struct BigBuffer {
pub width: usize,
pub height: usize,
pub content: Vec<Cell>
}
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)] pub struct TuiEvent(pub Event);
pub struct TuiKey(Option<KeyCode>, KeyModifiers);
#[derive(Debug, Clone)]
pub struct TuiIn {
/// Exit flag
pub exited: Arc<AtomicBool>,
/// Input event
pub event: TuiEvent,
}
/// 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,
}
/// 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,
}
/// 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],
}
pub struct Repeat<'a>(pub &'a str);
pub struct RepeatV<'a>(pub &'a str);
pub struct RepeatH<'a>(pub &'a str);
pub struct ScrollbarV {
pub offset: usize,
pub length: usize,
pub total: usize,
}
pub struct ScrollbarH {
pub offset: usize,
pub length: usize,
pub total: usize,
}
/// 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>>(
std::marker::PhantomData<O>, Perhaps<T>
);
pub struct Modify<T>(pub bool, pub Modifier, pub T);
pub struct Styled<T>(pub Option<Style>, pub T);
#[derive(Default)]
pub struct TuiOut {
pub buffer: Buffer,
pub area: XYWH<u16>,
}
impl Out for TuiOut {
type Unit = u16;
#[inline] fn area (&self) -> XYWH<u16> {
self.area
}
#[inline] fn area_mut (&mut self) -> &mut XYWH<u16> {
&mut self.area
}
#[inline] fn place_at <'t, T: Draw<Self> + ?Sized> (
&mut self, area: XYWH<u16>, content: &'t T
) {
let last = self.area();
*self.area_mut() = area;
content.draw(self);
*self.area_mut() = last;
}
}

112
tui/src/tui_traits.rs Normal file
View file

@ -0,0 +1,112 @@
use crate::*;
pub trait TuiDraw = Draw<TuiOut>;
pub trait TuiLayout = Layout<TuiOut>;
pub trait TuiContent = Content<TuiOut>;
pub trait TuiHandle = Handle<TuiIn>;
pub trait TuiWidget = TuiDraw + TuiHandle;
pub trait TuiRun<T: TuiWidget + 'static> {
/// Run an app in the main loop.
fn run (&self, state: &Arc<RwLock<T>>) -> Usually<()>;
}
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<[u16;4]> {
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<[u16;4]> {
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<[u16;4]> {
let area = to.area();
let style = style.or_else(||self.style_corners());
let [x, y, width, height] = area.xywh();
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() }
}