mirror of
https://codeberg.org/unspeaker/tengri.git
synced 2026-04-03 13:30:44 +02:00
This commit is contained in:
parent
b0fbe3c173
commit
9dbf4fcab5
9 changed files with 1101 additions and 1169 deletions
|
|
@ -5,10 +5,9 @@ version = "0.15.0"
|
||||||
description = "UI metaframework."
|
description = "UI metaframework."
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["lang", "sing", "draw", "play", "tui", "text", "time", "rand", "okhsl"]
|
default = ["lang", "sing", "draw", "play", "term", "text", "time", "rand", "okhsl"]
|
||||||
bumpalo = ["dep:bumpalo"]
|
bumpalo = ["dep:bumpalo"]
|
||||||
draw = []
|
draw = []
|
||||||
dsl = ["dep:dizzle"]
|
|
||||||
gui = ["draw", "dep:winit"]
|
gui = ["draw", "dep:winit"]
|
||||||
lang = ["dep:dizzle"]
|
lang = ["dep:dizzle"]
|
||||||
okhsl = ["dep:palette"]
|
okhsl = ["dep:palette"]
|
||||||
|
|
@ -17,7 +16,7 @@ rand = ["dep:rand"]
|
||||||
sing = ["dep:jack"]
|
sing = ["dep:jack"]
|
||||||
text = ["dep:unicode-width"]
|
text = ["dep:unicode-width"]
|
||||||
time = ["dep:quanta"]
|
time = ["dep:quanta"]
|
||||||
tui = ["draw", "dep:ratatui", "dep:crossterm"]
|
term = ["draw", "dep:ratatui", "dep:crossterm"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { version = "1.0" }
|
anyhow = { version = "1.0" }
|
||||||
|
|
|
||||||
2
dizzle
2
dizzle
|
|
@ -1 +1 @@
|
||||||
Subproject commit 068e26dd50699e51e9db01ac23fc0778074647bd
|
Subproject commit 44b2be57ca8d1f69a95f2eb02f4b5474ec77ac0a
|
||||||
102
src/.scratch.rs
102
src/.scratch.rs
|
|
@ -1359,3 +1359,105 @@ impl<E: Engine, S, C: Command<S>> MenuItem<E, S, C> {
|
||||||
////}
|
////}
|
||||||
////}
|
////}
|
||||||
////}
|
////}
|
||||||
|
|
||||||
|
////impl<T: AsMut<Buffer>> TuiOut for T { fn tui_out (&mut self) -> &mut Buffer { self.as_mut() } }
|
||||||
|
|
||||||
|
//impl Tui {
|
||||||
|
///// True if done
|
||||||
|
//pub fn exited (&self) -> bool { self.exited.fetch_and(true, Relaxed) }
|
||||||
|
///// Prepare before run
|
||||||
|
//pub fn setup (&self) -> Usually<()> { tui_setup(&mut*self.backend.write().unwrap()) }
|
||||||
|
///// Clean up after run
|
||||||
|
//pub fn teardown (&self) -> Usually<()> { tui_teardown(&mut*self.backend.write().unwrap()) }
|
||||||
|
///// Apply changes to the display buffer.
|
||||||
|
//pub fn flip (&mut self, mut buffer: Buffer, size: ratatui::prelude::Rect) -> Buffer { tui_flip(self, self.buffer, buffer, size) }
|
||||||
|
///// Create the engine.
|
||||||
|
//pub fn new (output: Box<dyn Write + Send + Sync>) -> Usually<Self> {
|
||||||
|
//let backend = CrosstermBackend::new(output);
|
||||||
|
//let Size { width, height } = backend.size()?;
|
||||||
|
//Ok(Self {
|
||||||
|
//exited: Arc::new(AtomicBool::new(false)),
|
||||||
|
//buffer: Buffer::empty(Rect { x: 0, y: 0, width, height }).into(),
|
||||||
|
//area: XYWH(0, 0, width, height),
|
||||||
|
//perf: Default::default(),
|
||||||
|
//backend: backend.into(),
|
||||||
|
//event: None,
|
||||||
|
//error: None,
|
||||||
|
//})
|
||||||
|
//}
|
||||||
|
///// Run an app in the engine.
|
||||||
|
//pub fn run <T> (mut self, join: bool, state: &Arc<RwLock<T>>) -> Usually<Arc<Self>> where
|
||||||
|
//T: Act<Tui, T> + Draw<Buffer> + Send + Sync + 'static
|
||||||
|
//{
|
||||||
|
//self.setup()?;
|
||||||
|
//let tui = Arc::new(self);
|
||||||
|
//let input_poll = Duration::from_millis(100);
|
||||||
|
//let output_sleep = Duration::from_millis(10); // == 1/MAXFPS (?)
|
||||||
|
//let _input_thread = tui_input(&tui, state, input_poll)?;
|
||||||
|
//let render_thread = tui_output(&tui, state, output_sleep)?;
|
||||||
|
//if join { // Run until render thread ends:
|
||||||
|
//let result = render_thread.join();
|
||||||
|
//tui.teardown()?;
|
||||||
|
//match result {
|
||||||
|
//Ok(result) => println!("\n\rRan successfully: {result:?}\n\r"),
|
||||||
|
//Err(error) => panic!("\n\rDraw thread failed: error={error:?}.\n\r"),
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//Ok(tui)
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
//impl Layout<Tui> for &str {
|
||||||
|
//fn layout (&self, to: XYWH<u16>) -> XYWH<u16> {
|
||||||
|
//to.centered_xy([width_chars_max(to.w(), self), 1])
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
//impl Layout<Tui> for String {
|
||||||
|
//fn layout (&self, to: XYWH<u16>) -> XYWH<u16> {
|
||||||
|
//self.as_str().layout(to)
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
//impl Layout<Tui> for Arc<str> {
|
||||||
|
//fn layout (&self, to: XYWH<u16>) -> XYWH<u16> {
|
||||||
|
//self.as_ref().layout(to)
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
//impl<'a, T: AsRef<str>> Layout<Tui> for TrimString<T> {
|
||||||
|
//fn layout (&self, to: XYWH<u16>) -> XYWH<u16> {
|
||||||
|
//Layout::layout(&self.as_ref(), to)
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
//impl<'a, T: AsRef<str>> Layout<Tui> for TrimStringRef<'a, T> {
|
||||||
|
//fn layout (&self, to: XYWH<u16>) -> XYWH<u16> {
|
||||||
|
//XYWH(to.x(), to.y(), to.w().min(self.0).min(self.1.as_ref().width() as u16), to.h())
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
///// TUI helper defs.
|
||||||
|
//impl Tui {
|
||||||
|
////pub const fn null () -> Color { Color::Reset }
|
||||||
|
////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) }
|
||||||
|
////pub const fn g (g: u8) -> Color { Color::Rgb(g, g, g) }
|
||||||
|
////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) }
|
||||||
|
//}
|
||||||
|
|
|
||||||
129
src/color.rs
129
src/color.rs
|
|
@ -1,11 +1,23 @@
|
||||||
pub(crate) use ::palette::Okhsl;
|
use ::ratatui::style::Color;
|
||||||
|
use crate::lang::impl_from;
|
||||||
|
pub(crate) use ::palette::{Okhsl, Srgb, OklabHue, okhsl::UniformOkhsl};
|
||||||
|
|
||||||
|
pub fn rgb (r: u8, g: u8, b: u8) -> ItemColor { todo!(); }
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait HasColor { fn color (&self) -> ItemColor; }
|
pub trait HasColor { fn color (&self) -> ItemColor; }
|
||||||
|
|
||||||
pub struct ItemColor {}
|
|
||||||
|
|
||||||
pub struct ItemTheme {}
|
|
||||||
|
|
||||||
#[macro_export] macro_rules! has_color {
|
#[macro_export] macro_rules! has_color {
|
||||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasColor for $Struct $(<$($L),*$($T),*>)? {
|
impl $(<$($L),*$($T $(: $U)?),*>)? HasColor for $Struct $(<$($L),*$($T),*>)? {
|
||||||
|
|
@ -14,6 +26,107 @@ pub struct ItemTheme {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rgb (r: u8, g: u8, b: u8) -> ItemColor {
|
pub struct ItemColor {}
|
||||||
todo!();
|
impl_from!(ItemColor: |rgb: Color| Self { rgb, okhsl: rgb_to_okhsl(rgb) });
|
||||||
|
impl_from!(ItemColor: |okhsl: Okhsl<f32>| Self { okhsl, rgb: okhsl_to_rgb(okhsl) });
|
||||||
|
// A single color within item theme parameters, in OKHSL and RGB representations.
|
||||||
|
impl ItemColor {
|
||||||
|
#[cfg(feature = "tui")] pub const fn from_tui (rgb: Color) -> Self {
|
||||||
|
Self { rgb, okhsl: Okhsl::new_const(OklabHue::new(0.0), 0.0, 0.0) }
|
||||||
|
}
|
||||||
|
pub fn random () -> Self {
|
||||||
|
let mut rng = ::rand::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 = ::rand::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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ItemTheme {}
|
||||||
|
impl_from!(ItemTheme: |base: ItemColor| Self::from_item_color(base));
|
||||||
|
impl_from!(ItemTheme: |base: Color| Self::from_tui_color(base));
|
||||||
|
impl ItemTheme {
|
||||||
|
#[cfg(feature = "tui")] 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_tui(Color::Rgb(index, index, index )),
|
||||||
|
light: ItemColor::from_tui(Color::Rgb(light, light, light, )),
|
||||||
|
lighter: ItemColor::from_tui(Color::Rgb(lighter, lighter, lighter, )),
|
||||||
|
lightest: ItemColor::from_tui(Color::Rgb(lightest, lightest, lightest, )),
|
||||||
|
dark: ItemColor::from_tui(Color::Rgb(dark, dark, dark, )),
|
||||||
|
darker: ItemColor::from_tui(Color::Rgb(darker, darker, darker, )),
|
||||||
|
darkest: ItemColor::from_tui(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,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
#[cfg(feature = "tui")] pub fn from_tui_color (base: Color) -> Self {
|
||||||
|
Self::from_item_color(ItemColor::from_tui(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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
41
src/lang.rs
Normal file
41
src/lang.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
#[cfg(feature = "tui")] impl TuiKey {
|
||||||
|
|
||||||
|
#[cfg(feature = "lang")]
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -35,9 +35,9 @@ pub(crate) use ::{
|
||||||
#[cfg(feature = "draw")] pub mod draw;
|
#[cfg(feature = "draw")] pub mod draw;
|
||||||
#[cfg(feature = "draw")] pub mod color;
|
#[cfg(feature = "draw")] pub mod color;
|
||||||
#[cfg(feature = "text")] pub mod text;
|
#[cfg(feature = "text")] pub mod text;
|
||||||
#[cfg(feature = "tui")] pub mod tui;
|
#[cfg(feature = "term")] pub mod term;
|
||||||
#[cfg(feature = "tui")] pub extern crate ratatui;
|
#[cfg(feature = "term")] pub extern crate ratatui;
|
||||||
#[cfg(feature = "tui")] pub extern crate crossterm;
|
#[cfg(feature = "term")] pub extern crate crossterm;
|
||||||
|
|
||||||
/// Define a trait an implement it for various mutation-enabled wrapper types. */
|
/// Define a trait an implement it for various mutation-enabled wrapper types. */
|
||||||
#[macro_export] macro_rules! flex_trait_mut (
|
#[macro_export] macro_rules! flex_trait_mut (
|
||||||
|
|
|
||||||
792
src/term.rs
Normal file
792
src/term.rs
Normal file
|
|
@ -0,0 +1,792 @@
|
||||||
|
use crate::{*, lang::*, play::*, draw::*, color::*, text::*};
|
||||||
|
use unicode_width::{UnicodeWidthStr, UnicodeWidthChar};
|
||||||
|
use rand::distributions::uniform::UniformSampler;
|
||||||
|
use ::{
|
||||||
|
std::{
|
||||||
|
io::{stdout, Write},
|
||||||
|
time::Duration
|
||||||
|
},
|
||||||
|
better_panic::{Settings, Verbosity},
|
||||||
|
ratatui::{
|
||||||
|
prelude::{Color, Style, Buffer, Position, Backend},
|
||||||
|
style::{Modifier, Color::*},
|
||||||
|
backend::{CrosstermBackend, ClearType},
|
||||||
|
layout::{Size, Rect},
|
||||||
|
buffer::Cell
|
||||||
|
},
|
||||||
|
crossterm::{
|
||||||
|
terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode},
|
||||||
|
event::{poll, read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Marker trait for structs that may be root of TUI app.
|
||||||
|
pub trait Tui: Draw<Buffer> + Do<TuiEvent, Perhaps<TuiEvent>> {}
|
||||||
|
|
||||||
|
/// `Tui` is automatically implemented.
|
||||||
|
impl<T: Draw<Buffer> + Do<TuiEvent, Perhaps<TuiEvent>>> Tui for T {}
|
||||||
|
|
||||||
|
/// Spawn the TUI input thread which reads keys from the terminal.
|
||||||
|
pub fn tui_input <T: Act<TuiEvent, T> + Send + Sync + 'static> (
|
||||||
|
exited: &Arc<AtomicBool>, state: &Arc<RwLock<T>>, poll: Duration
|
||||||
|
) -> Result<Thread, std::io::Error> {
|
||||||
|
let exited = exited.clone();
|
||||||
|
let state = state.clone();
|
||||||
|
Thread::new_poll(exited.clone(), poll, move |_| {
|
||||||
|
let event = read().unwrap();
|
||||||
|
match event {
|
||||||
|
|
||||||
|
// Hardcoded exit.
|
||||||
|
Event::Key(KeyEvent {
|
||||||
|
modifiers: KeyModifiers::CONTROL,
|
||||||
|
code: KeyCode::Char('c'),
|
||||||
|
kind: KeyEventKind::Press,
|
||||||
|
state: KeyEventState::NONE
|
||||||
|
}) => { exited.store(true, Relaxed); },
|
||||||
|
|
||||||
|
// Handle all other events by the state:
|
||||||
|
_ => {
|
||||||
|
let event = TuiEvent::from_crossterm(event);
|
||||||
|
if let Err(e) = state.write().unwrap().handle(&event) {
|
||||||
|
panic!("{e}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TUI input loop event.
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)]
|
||||||
|
pub struct TuiEvent(pub Event);
|
||||||
|
impl_from!(TuiEvent: |e: Event| TuiEvent(e));
|
||||||
|
impl_from!(TuiEvent: |c: char| TuiEvent(Event::Key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE))));
|
||||||
|
impl TuiEvent {
|
||||||
|
#[cfg(feature = "dsl")] pub fn from_dsl (dsl: impl Language) -> Perhaps<Self> {
|
||||||
|
Ok(TuiKey::from_dsl(dsl)?.to_crossterm().map(Self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TUI key spec.
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)]
|
||||||
|
pub struct TuiKey(pub Option<KeyCode>, pub KeyModifiers);
|
||||||
|
|
||||||
|
impl TuiKey {
|
||||||
|
const SPLIT: char = '/';
|
||||||
|
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 (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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TUI works in u16 coordinates.
|
||||||
|
impl Coord for u16 { fn plus (self, other: Self) -> Self { self.saturating_add(other) } }
|
||||||
|
|
||||||
|
impl Draw<Buffer> for u64 {
|
||||||
|
fn draw (&self, _to: &mut Buffer) { todo!() }
|
||||||
|
}
|
||||||
|
impl Draw<Buffer> for f64 {
|
||||||
|
fn draw (&self, _to: &mut Buffer) { todo!() }
|
||||||
|
}
|
||||||
|
impl Draw<Buffer> for &str {
|
||||||
|
fn draw (&self, to: &mut Buffer) {
|
||||||
|
let XYWH(x, y, w, ..) = to.centered_xy([width_chars_max(to.w(), self), 1]);
|
||||||
|
to.text(&self, x, y, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Draw<Buffer> for String {
|
||||||
|
fn draw (&self, to: &mut Buffer) { self.as_str().draw(to) }
|
||||||
|
}
|
||||||
|
impl Draw<Buffer> for Arc<str> {
|
||||||
|
fn draw (&self, to: &mut Buffer) { self.as_ref().draw(to) }
|
||||||
|
}
|
||||||
|
|
||||||
|
mod phat {
|
||||||
|
use super::*;
|
||||||
|
pub const LO: &'static str = "▄";
|
||||||
|
pub const HI: &'static str = "▀";
|
||||||
|
/// A phat line
|
||||||
|
pub fn lo (fg: Color, bg: Color) -> impl Draw<Buffer> {
|
||||||
|
H::fixed(1, Tui::fg_bg(fg, bg, X::repeat(self::phat::LO)))
|
||||||
|
}
|
||||||
|
/// A phat line
|
||||||
|
pub fn hi (fg: Color, bg: Color) -> impl Draw<Buffer> {
|
||||||
|
H::fixed(1, Tui::fg_bg(fg, bg, X::repeat(self::phat::HI)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod scroll {
|
||||||
|
pub const ICON_DEC_V: &[char] = &['▲'];
|
||||||
|
pub const ICON_INC_V: &[char] = &['▼'];
|
||||||
|
pub const ICON_DEC_H: &[char] = &[' ', '🞀', ' '];
|
||||||
|
pub const ICON_INC_H: &[char] = &[' ', '🞂', ' '];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn x_repeat (c: char) {
|
||||||
|
move|to: &mut Buffer|{
|
||||||
|
let XYWH(x, y, w, h) = to.area();
|
||||||
|
for x in x..x+w {
|
||||||
|
if let Some(cell) = to.cell_mut(Position::from((x, y))) {
|
||||||
|
cell.set_symbol(&c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn y_repeat (c: char) {
|
||||||
|
move|to: &mut Buffer|{
|
||||||
|
let XYWH(x, y, w, h) = to.area();
|
||||||
|
for y in y..y+h {
|
||||||
|
if let Some(cell) = to.cell_mut(Position::from((x, y))) {
|
||||||
|
cell.set_symbol(&c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn xy_repeat (c: char) {
|
||||||
|
move|to: &mut Buffer|{
|
||||||
|
let XYWH(x, y, w, h) = to.area();
|
||||||
|
let a = c.len();
|
||||||
|
for (_v, y) in (y..y+h).enumerate() {
|
||||||
|
for (u, x) in (x..x+w).enumerate() {
|
||||||
|
if let Some(cell) = to.cell_mut(Position::from((x, y))) {
|
||||||
|
let u = u % a;
|
||||||
|
cell.set_symbol(&c[u..u+1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ```
|
||||||
|
/// let _ = tengri::button_2("", "", true);
|
||||||
|
/// let _ = tengri::button_2("", "", false);
|
||||||
|
/// ```
|
||||||
|
pub fn button_2 <'a> (key: impl Draw<Buffer>, label: impl Draw<Buffer>, editing: bool) -> impl Draw<Buffer> {
|
||||||
|
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(!editing, Tui::fg_bg(Tui::g(255), Tui::g(96), label))))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ```
|
||||||
|
/// let _ = tengri::button_3("", "", "", true);
|
||||||
|
/// let _ = tengri::button_3("", "", "", false);
|
||||||
|
/// ```
|
||||||
|
pub fn button_3 <'a> (
|
||||||
|
key: impl Draw<Buffer>, label: impl Draw<Buffer>, value: impl Draw<Buffer>, editing: bool,
|
||||||
|
) -> impl Draw<Buffer> {
|
||||||
|
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(!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, &"▌"), ))))
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Tui> for $T {}
|
||||||
|
impl Draw<Buffer> for $T {
|
||||||
|
fn draw (&self, to: &mut Buffer) {
|
||||||
|
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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait TuiOut: Screen {
|
||||||
|
fn tui_out (&mut self) -> &mut Buffer;
|
||||||
|
fn update (&mut self, area: impl Area<u16>, callback: &impl Fn(&mut Cell, u16, u16)) {
|
||||||
|
tui_update(self.buffer(), area, callback);
|
||||||
|
}
|
||||||
|
fn fill_char (&mut self, area: impl Area<u16>, c: char) {
|
||||||
|
self.update(area, &|cell,_,_|{cell.set_char(c);})
|
||||||
|
}
|
||||||
|
fn fill_bg (&mut self, area: impl Area<u16>, color: Color) {
|
||||||
|
self.update(area, &|cell,_,_|{cell.set_bg(color);})
|
||||||
|
}
|
||||||
|
fn fill_fg (&mut self, area: impl Area<u16>, color: Color) {
|
||||||
|
self.update(area, &|cell,_,_|{cell.set_fg(color);})
|
||||||
|
}
|
||||||
|
fn fill_mod (&mut self, area: impl Area<u16>, on: bool, modifier: Modifier) {
|
||||||
|
if on {
|
||||||
|
self.update(area, &|cell,_,_|cell.modifier.insert(modifier))
|
||||||
|
} else {
|
||||||
|
self.update(area, &|cell,_,_|cell.modifier.remove(modifier))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn fill_bold (&mut self, area: impl Area<u16>, on: bool) {
|
||||||
|
self.fill_mod(area, on, Modifier::BOLD)
|
||||||
|
}
|
||||||
|
fn fill_reversed (&mut self, area: impl Area<u16>, on: bool) {
|
||||||
|
self.fill_mod(area, on, Modifier::REVERSED)
|
||||||
|
}
|
||||||
|
fn fill_crossed_out (&mut self, area: impl Area<u16>, on: bool) {
|
||||||
|
self.fill_mod(area, on, Modifier::CROSSED_OUT)
|
||||||
|
}
|
||||||
|
fn fill_ul (&mut self, area: impl Area<u16>, color: Option<Color>) {
|
||||||
|
if let Some(color) = color {
|
||||||
|
self.update(area, &|cell,_,_|{
|
||||||
|
cell.modifier.insert(ratatui::prelude::Modifier::UNDERLINED);
|
||||||
|
cell.underline_color = color;
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
self.update(area, &|cell,_,_|{
|
||||||
|
cell.modifier.remove(ratatui::prelude::Modifier::UNDERLINED);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn blit (&mut self, text: &impl AsRef<str>, x: u16, y: u16, style: Option<Style>) {
|
||||||
|
let text = text.as_ref();
|
||||||
|
let style = style.unwrap_or(Style::default());
|
||||||
|
let buf = self.buffer();
|
||||||
|
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)
|
||||||
|
fn text (&mut self, text: &impl AsRef<str>, x0: u16, y: u16, max_width: u16) {
|
||||||
|
let text = text.as_ref();
|
||||||
|
let buf = 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.write().unwrap().cell_mut(ratatui::prelude::Position { x, y }) {
|
||||||
|
cell.set_char(character);
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait BorderStyle: Draw<Buffer> + Copy {
|
||||||
|
fn enabled (&self) -> bool;
|
||||||
|
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 }
|
||||||
|
|
||||||
|
fn enclose (self, w: impl Draw<Buffer>) -> impl Draw<Buffer> {
|
||||||
|
bsp_b(XY::fill(border(self.enabled(), self)), w)
|
||||||
|
}
|
||||||
|
fn enclose2 (self, w: impl Draw<Buffer>) -> impl Draw<Buffer> {
|
||||||
|
bsp_b(XY::pad(1, 1, XY::fill(border(self.enabled(), self))), w)
|
||||||
|
}
|
||||||
|
fn enclose_bg (self, w: impl Draw<Buffer>) -> impl Draw<Buffer> {
|
||||||
|
TuiOut::bg(self.style().unwrap().bg.unwrap_or(Color::Reset),
|
||||||
|
bsp_b(XY::fill(border(self.enabled(), self)), w))
|
||||||
|
}
|
||||||
|
#[inline] fn draw <'a> (&self, to: &mut impl TuiOut) -> Usually<()> {
|
||||||
|
if self.enabled() {
|
||||||
|
self.draw_h(to, None)?;
|
||||||
|
self.draw_v(to, None)?;
|
||||||
|
self.draw_c(to, None)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[inline] fn draw_h (&self, to: &mut impl TuiOut, style: Option<Style>) -> Usually<impl Area<u16>> {
|
||||||
|
let area = to.area();
|
||||||
|
let style = style.or_else(||self.style_horizontal());
|
||||||
|
let [x, x2, y, y2] = area.lrtb();
|
||||||
|
for x in x..x2.saturating_sub(1) {
|
||||||
|
to.blit(&Self::N, x, y, style);
|
||||||
|
to.blit(&Self::S, x, y2.saturating_sub(1), style)
|
||||||
|
}
|
||||||
|
Ok(area)
|
||||||
|
}
|
||||||
|
#[inline] fn draw_v (&self, to: &mut impl TuiOut, style: Option<Style>) -> Usually<impl Area<u16>> {
|
||||||
|
let area = to.area();
|
||||||
|
let style = style.or_else(||self.style_vertical());
|
||||||
|
let [x, x2, y, y2] = area.lrtb();
|
||||||
|
let h = y2 - y;
|
||||||
|
if h > 1 {
|
||||||
|
for y in y..y2.saturating_sub(1) {
|
||||||
|
to.blit(&Self::W, x, y, style);
|
||||||
|
to.blit(&Self::E, x2.saturating_sub(1), y, style);
|
||||||
|
}
|
||||||
|
} else if h > 0 {
|
||||||
|
to.blit(&Self::W0, x, y, style);
|
||||||
|
to.blit(&Self::E0, x2.saturating_sub(1), y, style);
|
||||||
|
}
|
||||||
|
Ok(area)
|
||||||
|
}
|
||||||
|
#[inline] fn draw_c (&self, to: &mut impl TuiOut, style: Option<Style>) -> Usually<impl Area<u16>> {
|
||||||
|
let area = to.area();
|
||||||
|
let style = style.or_else(||self.style_corners());
|
||||||
|
let XYWH(x, y, w, h) = area;
|
||||||
|
if w > 1 && h > 1 {
|
||||||
|
to.blit(&Self::NW, x, y, style);
|
||||||
|
to.blit(&Self::NE, x + w - 1, y, style);
|
||||||
|
to.blit(&Self::SW, x, y + h- 1, style);
|
||||||
|
to.blit(&Self::SE, x + w - 1, y + h - 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() }
|
||||||
|
|
||||||
|
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 = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stackably padded.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// /// TODO
|
||||||
|
/// ```
|
||||||
|
pub fn phat <T, N: Coord> (w: N, h: N, [fg, bg, hi, lo]: [Color;4], draw: impl Tui) -> impl Tui {
|
||||||
|
let top = W::exact(1, self::phat::lo(bg, hi));
|
||||||
|
let low = W::exact(1, self::phat::hi(bg, lo));
|
||||||
|
let draw = Tui::fg_bg(fg, bg, draw);
|
||||||
|
WH::min(w, h, bsp_s(top, bsp_n(low, WH::fill(draw))))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn x_scroll () {
|
||||||
|
|to: &mut Buffer|{
|
||||||
|
let XYWH(x1, y1, w, h) = to.area();
|
||||||
|
let mut buf = to.buffer.write().unwrap();
|
||||||
|
let x2 = x1 + w;
|
||||||
|
for (i, x) in (x1..=x2).enumerate() {
|
||||||
|
if let Some(cell) = buf.cell_mut(Position::from((x, y1))) {
|
||||||
|
if i < (self::scroll::ICON_DEC_H.len()) {
|
||||||
|
cell.set_fg(Rgb(255, 255, 255));
|
||||||
|
cell.set_bg(Rgb(0, 0, 0));
|
||||||
|
cell.set_char(self::scroll::ICON_DEC_H[i as usize]);
|
||||||
|
} else if i > (w as usize - self::scroll::ICON_INC_H.len()) {
|
||||||
|
cell.set_fg(Rgb(255, 255, 255));
|
||||||
|
cell.set_bg(Rgb(0, 0, 0));
|
||||||
|
cell.set_char(self::scroll::ICON_INC_H[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('╌');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn y_scroll () {
|
||||||
|
|to: &mut Buffer|{
|
||||||
|
let XYWH(x1, y1, w, h) = to.area();
|
||||||
|
let mut buf = to.buffer.write().unwrap();
|
||||||
|
let y2 = y1 + h;
|
||||||
|
for (i, y) in (y1..=y2).enumerate() {
|
||||||
|
if let Some(cell) = buf.cell_mut(Position::from((x1, y))) {
|
||||||
|
if (i as usize) < (self::scroll::ICON_DEC_V.len()) {
|
||||||
|
cell.set_fg(Rgb(255, 255, 255));
|
||||||
|
cell.set_bg(Rgb(0, 0, 0));
|
||||||
|
cell.set_char(self::scroll::ICON_DEC_V[i as usize]);
|
||||||
|
} else if (i as usize) > (h as usize - self::scroll::ICON_INC_V.len()) {
|
||||||
|
cell.set_fg(Rgb(255, 255, 255));
|
||||||
|
cell.set_bg(Rgb(0, 0, 0));
|
||||||
|
cell.set_char(self::scroll::ICON_INC_V[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('╎'); // ━
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawn the TUI output thread which writes colored characters to the terminal.
|
||||||
|
pub fn tui_output <W: Write, T: Draw<Buffer> + Send + Sync + 'static> (
|
||||||
|
output: W,
|
||||||
|
exited: &Arc<AtomicBool>,
|
||||||
|
state: &Arc<RwLock<T>>,
|
||||||
|
sleep: Duration
|
||||||
|
) -> Result<Thread, std::io::Error> {
|
||||||
|
let state = state.clone();
|
||||||
|
tui_setup(&mut output)?;
|
||||||
|
let mut backend = CrosstermBackend::new(output);
|
||||||
|
let WH(width, height) = tui_wh(&mut backend);
|
||||||
|
let mut buffer_a = Buffer::empty(Rect { x: 0, y: 0, width, height });
|
||||||
|
let mut buffer_b = Buffer::empty(Rect { x: 0, y: 0, width, height });
|
||||||
|
Thread::new_sleep(exited.clone(), sleep, move |perf| {
|
||||||
|
let size = tui_wh(&mut backend);
|
||||||
|
if let Ok(state) = state.try_read() {
|
||||||
|
tui_resize(&mut backend, &mut buffer_a, size);
|
||||||
|
buffer_a = tui_redraw(&mut backend, &mut buffer_a, &mut buffer_b);
|
||||||
|
}
|
||||||
|
let timer = format!("{:>3.3}ms", perf.used.load(Relaxed));
|
||||||
|
buffer_a.set_string(0, 0, &timer, Style::default());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tui_setup (output: &mut impl Write) -> Usually<()> {
|
||||||
|
let better_panic_handler = Settings::auto().verbosity(Verbosity::Full).create_panic_handler();
|
||||||
|
std::panic::set_hook(Box::new(move |info: &std::panic::PanicHookInfo|{
|
||||||
|
output.execute(LeaveAlternateScreen).unwrap();
|
||||||
|
CrosstermBackend::new(output).show_cursor().unwrap();
|
||||||
|
disable_raw_mode().unwrap();
|
||||||
|
better_panic_handler(info);
|
||||||
|
}));
|
||||||
|
output.execute(EnterAlternateScreen)?;
|
||||||
|
CrosstermBackend::new(output).hide_cursor()?;
|
||||||
|
enable_raw_mode().map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tui_resize <W: Write> (
|
||||||
|
backend: &mut CrosstermBackend<W>,
|
||||||
|
buffer: &mut Buffer,
|
||||||
|
size: WH<u16>
|
||||||
|
) {
|
||||||
|
if buffer.area != size {
|
||||||
|
backend.clear_region(ClearType::All).unwrap();
|
||||||
|
buffer.resize(size);
|
||||||
|
buffer.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tui_redraw <'b, W: Write> (
|
||||||
|
backend: &mut CrosstermBackend<W>,
|
||||||
|
mut prev_buffer: &'b mut Buffer,
|
||||||
|
mut next_buffer: &'b mut Buffer
|
||||||
|
) {
|
||||||
|
let updates = prev_buffer.diff(&next_buffer);
|
||||||
|
backend.draw(updates.into_iter()).expect("failed to render");
|
||||||
|
Backend::flush(backend).expect("failed to flush output new_buffer");
|
||||||
|
std::mem::swap(&mut prev_buffer, &mut next_buffer);
|
||||||
|
next_buffer.reset();
|
||||||
|
next_buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tui_teardown <W: Write> (backend: &mut CrosstermBackend<W>) -> Usually<()> {
|
||||||
|
stdout().execute(LeaveAlternateScreen)?;
|
||||||
|
backend.show_cursor()?;
|
||||||
|
disable_raw_mode().map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tui_update (
|
||||||
|
buf: &mut Buffer, area: XYWH<u16>, callback: &impl Fn(&mut Cell, u16, u16)
|
||||||
|
) {
|
||||||
|
for row in 0..area.h() {
|
||||||
|
let y = area.y() + row;
|
||||||
|
for col in 0..area.w() {
|
||||||
|
let x = area.x() + col;
|
||||||
|
if x < buf.area.width && y < buf.area.height {
|
||||||
|
if let Some(cell) = buf.cell_mut(ratatui::prelude::Position { x, y }) {
|
||||||
|
callback(cell, col, row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn tui_wh <W: Write> (backend: &mut CrosstermBackend<W>) -> WH<u16> {
|
||||||
|
let Size { width, height } = backend.size().expect("get size failed");
|
||||||
|
WH(width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw contents with foreground color applied.
|
||||||
|
pub const fn fg (enabled: bool, color: Color, item: impl Tui) -> impl Tui {
|
||||||
|
|to: &mut Buffer|item.draw(to.with_fg(enabled, color))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw contents with background color applied.
|
||||||
|
pub const fn bg (enabled: bool, color: Color, item: impl Tui) -> impl Tui {
|
||||||
|
|to: &mut Buffer|item.draw(to.with_bg(enabled, color))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw contents with modifier applied.
|
||||||
|
pub const fn modify (enabled: bool, modifier: Modifier, item: impl Tui) -> impl Tui {
|
||||||
|
|to: &mut Buffer|item.draw(to.with_modifier(enabled, modifier, item))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn bold (enabled: bool, item: impl Tui) -> impl Tui {
|
||||||
|
modify(enabled, Modifier::BOLD, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw contents with style applied.
|
||||||
|
pub const fn styled (enabled: bool, style: Style, item: impl Tui) -> impl Tui {
|
||||||
|
|to: &mut Buffer|item.draw(to.with_style(enabled, style, item))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw border around shrinked item.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// /// TODO
|
||||||
|
/// ```
|
||||||
|
pub const fn border <T, S: BorderStyle> (on: bool, style: S, draw: impl Tui) -> impl Tui {
|
||||||
|
WH::fill(bsp_a(when(on, |to: &mut Area|{
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}), pad(Some(1), Some(1), draw)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw TUI content or its error message.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let _ = tengri::tui::catcher(Ok(Some("hello")));
|
||||||
|
/// let _ = tengri::tui::catcher(Ok(None));
|
||||||
|
/// let _ = tengri::tui::catcher(Err("draw fail".into()));
|
||||||
|
/// ```
|
||||||
|
pub fn catcher <T, E> (error: Perhaps<E>, draw: impl Tui) -> impl Tui {
|
||||||
|
move|to: &mut Buffer|match error.as_ref() {
|
||||||
|
Ok(Some(content)) => draw(to),
|
||||||
|
Ok(None) => to.blit(&"<empty>", 0, 0, Some(Style::default().yellow())),
|
||||||
|
Err(e) => {
|
||||||
|
let err_fg = rgb(255,224,244);
|
||||||
|
let err_bg = rgb(96, 24, 24);
|
||||||
|
let title = bsp_e(bold(true, "upsi daisy. "), "rendering failed.");
|
||||||
|
let error = bsp_e("\"why?\" ", bold(true, format!("{e}")));
|
||||||
|
&fg(err_fg, bg(err_bg, bsp_s(title, error)))(to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TUI buffer sized by `usize` instead of `u16`.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct BigBuffer {
|
||||||
|
pub width: usize,
|
||||||
|
pub height: usize,
|
||||||
|
pub content: Vec<Cell>
|
||||||
|
}
|
||||||
|
impl_from!(BigBuffer: |size:(usize, usize)| Self::new(size.0, size.1));
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/text.rs
39
src/text.rs
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::draw::Draw;
|
||||||
pub(crate) use ::unicode_width::*;
|
pub(crate) use ::unicode_width::*;
|
||||||
|
|
||||||
/// Displays an owned [str]-like with fixed maximum width.
|
/// Displays an owned [str]-like with fixed maximum width.
|
||||||
|
|
@ -41,3 +42,41 @@ pub(crate) fn width_chars_max (max: u16, text: impl AsRef<str>) -> u16 {
|
||||||
}
|
}
|
||||||
return width
|
return width
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "term")] mod impl_term {
|
||||||
|
use super::*;
|
||||||
|
use crate::draw::XYWH;
|
||||||
|
use ratatui::prelude::{Buffer, Position};
|
||||||
|
impl<'a, T> Draw<Buffer> for TrimStringRef<'a, T> {
|
||||||
|
fn draw (&self, to: &mut Buffer) {
|
||||||
|
let XYWH(x, y, w, ..) = XYWH(to.x(), to.y(), to.w().min(self.0).min(self.1.as_ref().width() as u16), to.h());
|
||||||
|
to.text(&self, x, y, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T> Draw<Buffer> for TrimString<T> {
|
||||||
|
fn draw (&self, to: &mut Buffer) { self.as_ref().draw(to) }
|
||||||
|
}
|
||||||
|
impl<'a, T: AsRef<str>> Draw<Buffer> for TrimString<T> {
|
||||||
|
fn draw (&self, to: &mut Buffer) { Draw::draw(&self.as_ref(), to) }
|
||||||
|
}
|
||||||
|
impl<T: AsRef<str>> Draw<Buffer> for TrimStringRef<'_, T> {
|
||||||
|
fn draw (&self, target: &mut Buffer) {
|
||||||
|
let area = target.area();
|
||||||
|
let mut buf = target.buffer.write().unwrap();
|
||||||
|
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) = buf.cell_mut(Position {
|
||||||
|
x: area.x() + width - 1,
|
||||||
|
y: area.y()
|
||||||
|
}) {
|
||||||
|
cell.set_char(c);
|
||||||
|
}
|
||||||
|
width += c.width().unwrap_or(0) as u16;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
1154
src/tui.rs
1154
src/tui.rs
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue