mirror of
https://codeberg.org/unspeaker/tengri.git
synced 2026-02-21 10:39:03 +01:00
This commit is contained in:
parent
b7b1055fbc
commit
4fa5d74fa2
26 changed files with 1550 additions and 1548 deletions
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
//}
|
||||||
261
tui/src/lib.rs
261
tui/src/lib.rs
|
|
@ -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<()> {
|
||||||
|
|
|
||||||
|
|
@ -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))))
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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, &"▌"), ))))
|
|
||||||
}
|
|
||||||
|
|
@ -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(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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('╌');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)))),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
@ -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));
|
|
||||||
|
|
@ -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
|
|
||||||
//}
|
|
||||||
|
|
@ -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}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
885
tui/src/tui_impls.rs
Normal 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
124
tui/src/tui_structs.rs
Normal 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
112
tui/src/tui_traits.rs
Normal 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() }
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue