all possible insanities simultaneously.

but at least deleting more than i'm writing (hopefully!)
This commit is contained in:
same mf who else 2026-03-21 15:30:25 +02:00
parent 9dbf4fcab5
commit 5d627f7669
5 changed files with 868 additions and 815 deletions

View file

@ -1439,25 +1439,4 @@ impl<E: Engine, S, C: Command<S>> MenuItem<E, S, C> {
///// TUI helper defs. ///// TUI helper defs.
//impl Tui { //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) }
//} //}

View file

@ -55,7 +55,15 @@ impl ItemColor {
} }
} }
pub struct ItemTheme {} pub struct ItemTheme {
pub base: ItemColor,
pub light: ItemColor,
pub lighter: ItemColor,
pub lightest: ItemColor,
pub dark: ItemColor,
pub darker: ItemColor,
pub darkest: ItemColor,
}
impl_from!(ItemTheme: |base: ItemColor| Self::from_item_color(base)); impl_from!(ItemTheme: |base: ItemColor| Self::from_item_color(base));
impl_from!(ItemTheme: |base: Color| Self::from_tui_color(base)); impl_from!(ItemTheme: |base: Color| Self::from_tui_color(base));
impl ItemTheme { impl ItemTheme {

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
use crate::{*, lang::*, play::*, draw::*, color::*, text::*}; use crate::{*, lang::*, play::*, draw::{*, Split::*}, color::*, text::*};
use unicode_width::{UnicodeWidthStr, UnicodeWidthChar}; use unicode_width::{UnicodeWidthStr, UnicodeWidthChar};
use rand::distributions::uniform::UniformSampler; use rand::distributions::uniform::UniformSampler;
use ::{ use ::{
@ -8,149 +8,140 @@ use ::{
}, },
better_panic::{Settings, Verbosity}, better_panic::{Settings, Verbosity},
ratatui::{ ratatui::{
prelude::{Color, Style, Buffer, Position, Backend}, prelude::{Style, Buffer as ScreenBuffer, Position, Backend, Color},
style::{Modifier, Color::*}, style::{Modifier, Color::*},
backend::{CrosstermBackend, ClearType}, backend::{CrosstermBackend, ClearType},
layout::{Size, Rect}, layout::{Size, Rect},
buffer::Cell buffer::{Buffer, Cell},
}, },
crossterm::{ crossterm::{
ExecutableCommand,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode}, terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode},
event::{poll, read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState}, event::{poll, read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState},
} }
}; };
/// Marker trait for structs that may be root of TUI app. pub struct Tui(pub Buffer, pub XYWH<u16>);
pub trait Tui: Draw<Buffer> + Do<TuiEvent, Perhaps<TuiEvent>> {} impl Screen for Tui { type Unit = u16; }
impl HasX<u16> for Tui { fn x (&self) -> X<u16> { self.1.x() } }
/// `Tui` is automatically implemented. impl HasY<u16> for Tui { fn y (&self) -> Y<u16> { self.1.y() } }
impl<T: Draw<Buffer> + Do<TuiEvent, Perhaps<TuiEvent>>> Tui for T {} impl HasW<u16> for Tui { fn w (&self) -> W<u16> { self.1.w() } }
impl HasH<u16> for Tui { fn h (&self) -> H<u16> { self.1.h() } }
/// Spawn the TUI input thread which reads keys from the terminal. impl HasOrigin for Tui { fn origin (&self) -> Origin { Origin::NW } }
pub fn tui_input <T: Act<TuiEvent, T> + Send + Sync + 'static> ( impl AsRef<Buffer> for Tui { fn as_ref (&self) -> &Buffer { &self.0 } }
exited: &Arc<AtomicBool>, state: &Arc<RwLock<T>>, poll: Duration impl AsMut<Buffer> for Tui { fn as_mut (&mut self) -> &mut Buffer { &mut self.0 } }
) -> Result<Thread, std::io::Error> { impl AsRef<XYWH<u16>> for Tui { fn as_ref (&self) -> &XYWH<u16> { &self.0 } }
let exited = exited.clone(); impl AsMut<XYWH<u16>> for Tui { fn as_mut (&mut self) -> &mut XYWH<u16> { &mut self.0 } }
let state = state.clone(); impl Tui {
Thread::new_poll(exited.clone(), poll, move |_| { fn update (&mut self, callback: &impl Fn(&mut Cell, u16, u16)) -> WH<u16> {
let event = read().unwrap(); tui_update(self.0, self.1, callback);
match event { self.1.wh()
}
// Hardcoded exit. fn tint_all (&mut self, fg: Color, bg: Color, modifier: Modifier) {
Event::Key(KeyEvent { for cell in self.buffer().content.iter_mut() {
modifiers: KeyModifiers::CONTROL, cell.fg = fg;
code: KeyCode::Char('c'), cell.bg = bg;
kind: KeyEventKind::Press, cell.modifier = modifier;
state: KeyEventState::NONE }
}) => { exited.store(true, Relaxed); }, }
fn blit (&mut self, text: &impl AsRef<str>, x: u16, y: u16, style: Option<Style>) {
// Handle all other events by the state: let text = text.as_ref();
_ => { let style = style.unwrap_or(Style::default());
let event = TuiEvent::from_crossterm(event); let buf = self.buffer();
if let Err(e) = state.write().unwrap().handle(&event) { if x < buf.area.width && y < buf.area.height {
panic!("{e}") buf.set_string(x, y, text, style);
} }
} }
}
/// Apply foreground color.
pub const fn fg (fg: Color, draw: impl Draw<Tui>) -> impl Draw<Tui> {
thunk(move|to: &mut Tui|{
to.update(&|cell,_,_|{ cell.set_fg(fg); });
draw.draw(to)
})
}
/// Apply background color.
pub const fn bg (bg: Color, draw: impl Draw<Tui>) -> impl Draw<Tui> {
thunk(move|to: &mut Tui|{
to.update(&|cell,_,_|{ cell.set_bg(bg); });
draw.draw(to)
})
}
pub const fn fg_bg (fg: Color, bg: Color, draw: impl Draw<Tui>) -> impl Draw<Tui> {
thunk(move|to: &mut Tui|{
to.update(&|cell,_,_|{ cell.set_fg(fg); cell.set_bg(bg); });
draw.draw(to)
})
}
pub const fn fill_char (c: char) {
thunk(move|to: &mut Tui|Ok(to.update(&|cell,_,_|{
cell.set_char(c);
})))
}
/// Draw contents with modifier applied.
pub const fn modify (on: bool, modifier: Modifier, draw: impl Draw<Tui>) -> impl Draw<Tui> {
thunk(move|to: &mut Tui|{
fill_mod(on, modifier).draw(to)?;
draw.draw(to)
})
}
pub const fn fill_mod (on: bool, modifier: Modifier) {
thunk(move|to: &mut Tui|{
if on {
to.update(&|cell,_,_|cell.modifier.insert(modifier))
} else {
to.update(&|cell,_,_|cell.modifier.remove(modifier))
}
})
}
/// Draw contents with bold modifier applied.
pub const fn bold (on: bool, draw: impl Draw<Tui>) -> impl Draw<Tui> {
modify(on, Modifier::BOLD, draw)
}
pub const fn fill_ul (on: bool, color: Option<Color>) {
thunk(move|to: &mut Tui|{
if on {
to.update(&|cell,_,_|{
cell.modifier.insert(Modifier::UNDERLINED);
cell.underline_color = color;
})
} else {
to.update(&|cell,_,_|&|cell,_,_|{
cell.modifier.remove(Modifier::UNDERLINED);
cell.underline_color = Reset;
})
} }
}) })
} }
/// 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. /// TUI works in u16 coordinates.
impl Coord for u16 { fn plus (self, other: Self) -> Self { self.saturating_add(other) } } impl Coord for u16 {
fn plus (self, other: Self) -> Self {
self.saturating_add(other)
}
}
impl Draw<Buffer> for u64 { impl Draw<Tui> for u64 {
fn draw (&self, _to: &mut Buffer) { todo!() } fn draw (&self, _to: &mut Tui) -> Usually<WH<u16>> { todo!() }
} }
impl Draw<Buffer> for f64 { impl Draw<Tui> for f64 {
fn draw (&self, _to: &mut Buffer) { todo!() } fn draw (&self, _to: &mut Tui) -> Usually<WH<u16>> { todo!() }
} }
impl Draw<Buffer> for &str { impl Draw<Tui> for &str {
fn draw (&self, to: &mut Buffer) { fn draw (&self, to: &mut Tui) -> Usually<WH<u16>> {
let XYWH(x, y, w, ..) = to.centered_xy([width_chars_max(to.w(), self), 1]); let XYWH(x, y, w, ..) = to.centered_xy([width_chars_max(to.w(), self), 1]);
to.text(&self, x, y, w) to.text(&self, x, y, w)
} }
} }
impl Draw<Buffer> for String { impl Draw<Tui> for String {
fn draw (&self, to: &mut Buffer) { self.as_str().draw(to) } fn draw (&self, to: &mut Tui) -> Usually<WH<u16>> {
self.as_str().draw(to)
}
} }
impl Draw<Buffer> for Arc<str> { impl Draw<Tui> for Arc<str> {
fn draw (&self, to: &mut Buffer) { self.as_ref().draw(to) } fn draw (&self, to: &mut Tui) -> Usually<WH<u16>> {
self.as_ref().draw(to)
}
} }
mod phat { mod phat {
@ -158,12 +149,12 @@ mod phat {
pub const LO: &'static str = ""; pub const LO: &'static str = "";
pub const HI: &'static str = ""; pub const HI: &'static str = "";
/// A phat line /// A phat line
pub fn lo (fg: Color, bg: Color) -> impl Draw<Buffer> { pub fn lo (fg: Color, bg: Color) -> impl Draw<Tui> {
H::fixed(1, Tui::fg_bg(fg, bg, X::repeat(self::phat::LO))) H::exact(1, fg_bg(fg, bg, x_repeat(self::phat::LO)))
} }
/// A phat line /// A phat line
pub fn hi (fg: Color, bg: Color) -> impl Draw<Buffer> { pub fn hi (fg: Color, bg: Color) -> impl Draw<Tui> {
H::fixed(1, Tui::fg_bg(fg, bg, X::repeat(self::phat::HI))) H::exact(1, fg_bg(fg, bg, x_repeat(self::phat::HI)))
} }
} }
@ -174,66 +165,71 @@ mod scroll {
pub const ICON_INC_H: &[char] = &[' ', '🞂', ' ']; pub const ICON_INC_H: &[char] = &[' ', '🞂', ' '];
} }
fn x_repeat (c: char) { pub const fn x_repeat (c: &str) -> impl Draw<Tui> {
move|to: &mut Buffer|{ thunk(move|to: &mut Tui|{
let XYWH(x, y, w, h) = to.area(); let XYWH(x, y, w, h) = to.xywh();
for x in x..x+w { for x in x..x+w {
if let Some(cell) = to.cell_mut(Position::from((x, y))) { if let Some(cell) = to.0.cell_mut(Position::from((x, y))) {
cell.set_symbol(&c); cell.set_symbol(&c);
} }
} }
} Ok(WH(w, 1))
})
} }
fn y_repeat (c: char) { pub const fn y_repeat (c: &str) -> impl Draw<Tui> {
move|to: &mut Buffer|{ thunk(move|to: &mut Tui|{
let XYWH(x, y, w, h) = to.area(); let XYWH(x, y, w, h) = to.xywh();
for y in y..y+h { for y in y..y+h {
if let Some(cell) = to.cell_mut(Position::from((x, y))) { if let Some(cell) = to.0.cell_mut(Position::from((x, y))) {
cell.set_symbol(&c); cell.set_symbol(&c);
} }
} }
} Ok(WH(1, h))
})
} }
fn xy_repeat (c: char) { pub const fn xy_repeat (c: &str) -> impl Draw<Tui> {
move|to: &mut Buffer|{ thunk(move|to: &mut Tui|{
let XYWH(x, y, w, h) = to.area(); let XYWH(x, y, w, h) = to.xywh();
let a = c.len(); let a = c.len();
for (_v, y) in (y..y+h).enumerate() { for (_v, y) in (y..y+h).enumerate() {
for (u, x) in (x..x+w).enumerate() { for (u, x) in (x..x+w).enumerate() {
if let Some(cell) = to.cell_mut(Position::from((x, y))) { if let Some(cell) = to.0.cell_mut(Position::from((x, y))) {
let u = u % a; let u = u % a;
cell.set_symbol(&c[u..u+1]); cell.set_symbol(&c[u..u+1]);
} }
} }
} }
} Ok(WH(w, h))
})
} }
/// ``` /// ```
/// let _ = tengri::button_2("", "", true); /// let _ = tengri::button_2("", "", true);
/// let _ = tengri::button_2("", "", false); /// let _ = tengri::button_2("", "", false);
/// ``` /// ```
pub fn button_2 <'a> (key: impl Draw<Buffer>, label: impl Draw<Buffer>, editing: bool) -> impl Draw<Buffer> { pub const fn button_2 <'a> (key: impl Draw<Tui>, label: impl Draw<Tui>, hide: bool) -> impl Draw<Tui> {
Tui::bold(true, bsp_e( let c1 = tui_orange();
Tui::fg_bg(Tui::orange(), Tui::g(0), bsp_e(Tui::fg(Tui::g(0), &""), bsp_e(key, Tui::fg(Tui::g(96), &"")))), let c2 = tui_g(0);
when(!editing, Tui::fg_bg(Tui::g(255), Tui::g(96), label)))) let c3 = tui_g(96);
let c4 = tui_g(255);
bold(true, fg_bg(c1, c2, east(fg(c2, east(key, fg(c3, &""))), when(!hide, fg_bg(c4, c3, label)))))
} }
/// ``` /// ```
/// let _ = tengri::button_3("", "", "", true); /// let _ = tengri::button_3("", "", "", true);
/// let _ = tengri::button_3("", "", "", false); /// let _ = tengri::button_3("", "", "", false);
/// ``` /// ```
pub fn button_3 <'a> ( pub const fn button_3 <'a> (
key: impl Draw<Buffer>, label: impl Draw<Buffer>, value: impl Draw<Buffer>, editing: bool, key: impl Draw<Tui>, label: impl Draw<Tui>, value: impl Draw<Tui>, editing: bool,
) -> impl Draw<Buffer> { ) -> impl Draw<Tui> {
Tui::bold(true, bsp_e( bold(true, east(
Tui::fg_bg(Tui::orange(), Tui::g(0), 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) }, "")))), east(fg(tui_g(0), &""), east(key, fg(if editing { tui_g(128) } else { tui_g(96) }, "")))),
bsp_e( east(
when(!editing, bsp_e(Tui::fg_bg(Tui::g(255), Tui::g(96), label), Tui::fg_bg(Tui::g(128), Tui::g(96), &""),)), when(!editing, east(fg_bg(tui_g(255), tui_g(96), label), 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, &""), )))) east(fg_bg(tui_g(224), tui_g(128), value), fg_bg(tui_g(128), Reset, &""), ))))
} }
macro_rules! border { macro_rules! border {
@ -255,9 +251,9 @@ macro_rules! border {
} }
#[derive(Copy, Clone)] pub struct $T(pub bool, pub Style); #[derive(Copy, Clone)] pub struct $T(pub bool, pub Style);
//impl Layout<Tui> for $T {} //impl Layout<Tui> for $T {}
impl Draw<Buffer> for $T { impl Draw<Tui> for $T {
fn draw (&self, to: &mut Buffer) { fn draw (&self, to: &mut Tui) -> Usually<WH<u16>> {
if self.enabled() { let _ = BorderStyle::draw(self, to); } when(self.enabled(), |to: &mut Tui|BorderStyle::draw(self, to)).draw(to)
} }
} }
)+} )+}
@ -369,87 +365,7 @@ border! {
} }
} }
pub trait TuiOut: Screen { pub trait BorderStyle: Draw<Tui> + Copy {
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 enabled (&self) -> bool;
fn border_n (&self) -> &str { Self::N } fn border_n (&self) -> &str { Self::N }
fn border_s (&self) -> &str { Self::S } fn border_s (&self) -> &str { Self::S }
@ -460,17 +376,17 @@ pub trait BorderStyle: Draw<Buffer> + Copy {
fn border_sw (&self) -> &str { Self::SW } fn border_sw (&self) -> &str { Self::SW }
fn border_se (&self) -> &str { Self::SE } fn border_se (&self) -> &str { Self::SE }
fn enclose (self, w: impl Draw<Buffer>) -> impl Draw<Buffer> { fn enclose (self, w: impl Draw<Tui>) -> impl Draw<Tui> {
bsp_b(XY::fill(border(self.enabled(), self)), w) below(WH::fill(border(self.enabled(), self)), w)
} }
fn enclose2 (self, w: impl Draw<Buffer>) -> impl Draw<Buffer> { fn enclose2 (self, w: impl Draw<Tui>) -> impl Draw<Tui> {
bsp_b(XY::pad(1, 1, XY::fill(border(self.enabled(), self))), w) below(WH::pad(1, 1, WH::fill(border(self.enabled(), self))), w)
} }
fn enclose_bg (self, w: impl Draw<Buffer>) -> impl Draw<Buffer> { fn enclose_bg (self, w: impl Draw<Tui>) -> impl Draw<Tui> {
TuiOut::bg(self.style().unwrap().bg.unwrap_or(Color::Reset), Tui::bg(self.style().unwrap().bg.unwrap_or(Color::Reset),
bsp_b(XY::fill(border(self.enabled(), self)), w)) below(WH::fill(border(self.enabled(), self)), w))
} }
#[inline] fn draw <'a> (&self, to: &mut impl TuiOut) -> Usually<()> { #[inline] fn draw <'a> (&self, to: &mut impl Draw<Tui>) -> Usually<()> {
if self.enabled() { if self.enabled() {
self.draw_h(to, None)?; self.draw_h(to, None)?;
self.draw_v(to, None)?; self.draw_v(to, None)?;
@ -478,7 +394,7 @@ pub trait BorderStyle: Draw<Buffer> + Copy {
} }
Ok(()) Ok(())
} }
#[inline] fn draw_h (&self, to: &mut impl TuiOut, style: Option<Style>) -> Usually<impl Area<u16>> { #[inline] fn draw_h (&self, to: &mut impl Draw<Tui>, style: Option<Style>) -> Usually<XYWH<u16>> {
let area = to.area(); let area = to.area();
let style = style.or_else(||self.style_horizontal()); let style = style.or_else(||self.style_horizontal());
let [x, x2, y, y2] = area.lrtb(); let [x, x2, y, y2] = area.lrtb();
@ -488,7 +404,7 @@ pub trait BorderStyle: Draw<Buffer> + Copy {
} }
Ok(area) Ok(area)
} }
#[inline] fn draw_v (&self, to: &mut impl TuiOut, style: Option<Style>) -> Usually<impl Area<u16>> { #[inline] fn draw_v (&self, to: &mut impl Draw<Tui>, style: Option<Style>) -> Usually<XYWH<u16>> {
let area = to.area(); let area = to.area();
let style = style.or_else(||self.style_vertical()); let style = style.or_else(||self.style_vertical());
let [x, x2, y, y2] = area.lrtb(); let [x, x2, y, y2] = area.lrtb();
@ -504,7 +420,7 @@ pub trait BorderStyle: Draw<Buffer> + Copy {
} }
Ok(area) Ok(area)
} }
#[inline] fn draw_c (&self, to: &mut impl TuiOut, style: Option<Style>) -> Usually<impl Area<u16>> { #[inline] fn draw_c (&self, to: &mut impl Draw<Tui>, style: Option<Style>) -> Usually<XYWH<u16>> {
let area = to.area(); let area = to.area();
let style = style.or_else(||self.style_corners()); let style = style.or_else(||self.style_corners());
let XYWH(x, y, w, h) = area; let XYWH(x, y, w, h) = area;
@ -541,15 +457,15 @@ pub trait BorderStyle: Draw<Buffer> + Copy {
/// ``` /// ```
/// /// TODO /// /// TODO
/// ``` /// ```
pub fn phat <T, N: Coord> (w: N, h: N, [fg, bg, hi, lo]: [Color;4], draw: impl Tui) -> impl Tui { pub fn phat <T, N: Coord> (w: N, h: N, [fg, bg, hi, lo]: [Color;4], draw: impl Draw<Tui>) -> impl Draw<Tui> {
let top = W::exact(1, self::phat::lo(bg, hi)); let top = W::exact(1, self::phat::lo(bg, hi));
let low = W::exact(1, self::phat::hi(bg, lo)); let low = W::exact(1, self::phat::hi(bg, lo));
let draw = Tui::fg_bg(fg, bg, draw); let draw = fg_bg(fg, bg, draw);
WH::min(w, h, bsp_s(top, bsp_n(low, WH::fill(draw)))) WH::min(w, h, south(top, north(low, WH::fill(draw))))
} }
fn x_scroll () { fn x_scroll () {
|to: &mut Buffer|{ |to: &mut Tui|{
let XYWH(x1, y1, w, h) = to.area(); let XYWH(x1, y1, w, h) = to.area();
let mut buf = to.buffer.write().unwrap(); let mut buf = to.buffer.write().unwrap();
let x2 = x1 + w; let x2 = x1 + w;
@ -578,7 +494,7 @@ fn x_scroll () {
} }
fn y_scroll () { fn y_scroll () {
|to: &mut Buffer|{ |to: &mut Tui|{
let XYWH(x1, y1, w, h) = to.area(); let XYWH(x1, y1, w, h) = to.area();
let mut buf = to.buffer.write().unwrap(); let mut buf = to.buffer.write().unwrap();
let y2 = y1 + h; let y2 = y1 + h;
@ -607,7 +523,7 @@ fn y_scroll () {
} }
/// Spawn the TUI output thread which writes colored characters to the terminal. /// Spawn the TUI output thread which writes colored characters to the terminal.
pub fn tui_output <W: Write, T: Draw<Buffer> + Send + Sync + 'static> ( pub fn tui_output <W: Write, T: Draw<Tui> + Send + Sync + 'static> (
output: W, output: W,
exited: &Arc<AtomicBool>, exited: &Arc<AtomicBool>,
state: &Arc<RwLock<T>>, state: &Arc<RwLock<T>>,
@ -645,7 +561,7 @@ pub fn tui_setup (output: &mut impl Write) -> Usually<()> {
pub fn tui_resize <W: Write> ( pub fn tui_resize <W: Write> (
backend: &mut CrosstermBackend<W>, backend: &mut CrosstermBackend<W>,
buffer: &mut Buffer, buffer: &mut Tui,
size: WH<u16> size: WH<u16>
) { ) {
if buffer.area != size { if buffer.area != size {
@ -675,14 +591,14 @@ pub fn tui_teardown <W: Write> (backend: &mut CrosstermBackend<W>) -> Usually<()
} }
pub fn tui_update ( pub fn tui_update (
buf: &mut Buffer, area: XYWH<u16>, callback: &impl Fn(&mut Cell, u16, u16) buf: &mut Tui, area: XYWH<u16>, callback: &impl Fn(&mut Cell, u16, u16)
) { ) {
for row in 0..area.h() { for row in 0..area.h().0 {
let y = area.y() + row; let y = area.y().0 + row;
for col in 0..area.w() { for col in 0..area.w().0 {
let x = area.x() + col; let x = area.x() + col;
if x < buf.area.width && y < buf.area.height { if x < buf.area.width && y < buf.area.height {
if let Some(cell) = buf.cell_mut(ratatui::prelude::Position { x, y }) { if let Some(cell) = buf.cell_mut(Position { x, y }) {
callback(cell, col, row); callback(cell, col, row);
} }
} }
@ -695,37 +611,14 @@ pub(crate) fn tui_wh <W: Write> (backend: &mut CrosstermBackend<W>) -> WH<u16> {
WH(width, height) 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. /// Draw border around shrinked item.
/// ///
/// ``` /// ```
/// /// TODO /// /// TODO
/// ``` /// ```
pub const fn border <T, S: BorderStyle> (on: bool, style: S, draw: impl Tui) -> impl Tui { pub const fn border <T, S: BorderStyle> (on: bool, style: S, draw: impl Draw<Tui>) -> impl Draw<Tui> {
WH::fill(bsp_a(when(on, |to: &mut Area|{ let content = pad(Some(1), Some(1), draw);
let outline = when(on, |to: &mut Tui|{
let area = to.area(); let area = to.area();
if area.w() > 0 && area.y() > 0 { if area.w() > 0 && area.y() > 0 {
to.blit(&style.border_nw(), area.x(), area.y(), style.style()); to.blit(&style.border_nw(), area.x(), area.y(), style.style());
@ -741,7 +634,8 @@ pub const fn border <T, S: BorderStyle> (on: bool, style: S, draw: impl Tui) ->
to.blit(&style.border_e(), area.x() + area.w() - 1, y, style.style()); to.blit(&style.border_e(), area.x() + area.w() - 1, y, style.style());
} }
} }
}), pad(Some(1), Some(1), draw))) });
Above.split(outline, content)
} }
/// Draw TUI content or its error message. /// Draw TUI content or its error message.
@ -751,18 +645,18 @@ pub const fn border <T, S: BorderStyle> (on: bool, style: S, draw: impl Tui) ->
/// let _ = tengri::tui::catcher(Ok(None)); /// let _ = tengri::tui::catcher(Ok(None));
/// let _ = tengri::tui::catcher(Err("draw fail".into())); /// let _ = tengri::tui::catcher(Err("draw fail".into()));
/// ``` /// ```
pub fn catcher <T, E> (error: Perhaps<E>, draw: impl Tui) -> impl Tui { pub fn catcher <T, E> (error: Usually<E>, draw: impl Draw<Tui>) -> impl Draw<Tui> {
move|to: &mut Buffer|match error.as_ref() { thunk(move|to: &mut Tui|match error.as_ref() {
Ok(Some(content)) => draw(to), Ok(Some(content)) => draw(to),
Ok(None) => to.blit(&"<empty>", 0, 0, Some(Style::default().yellow())), Ok(None) => to.blit(&"<empty>", 0, 0, Some(Style::default().yellow())),
Err(e) => { Err(e) => {
let err_fg = rgb(255,224,244); let err_fg = rgb(255,224,244);
let err_bg = rgb(96, 24, 24); let err_bg = rgb(96, 24, 24);
let title = bsp_e(bold(true, "upsi daisy. "), "rendering failed."); let title = east(bold(true, "upsi daisy. "), "rendering failed.");
let error = bsp_e("\"why?\" ", bold(true, format!("{e}"))); let error = east("\"why?\" ", bold(true, format!("{e}")));
&fg(err_fg, bg(err_bg, bsp_s(title, error)))(to) &fg(err_fg, bg(err_bg, south(title, error)))(to)
} }
} })
} }
/// TUI buffer sized by `usize` instead of `u16`. /// TUI buffer sized by `usize` instead of `u16`.
@ -790,3 +684,141 @@ impl BigBuffer {
y * self.width + x y * self.width + x
} }
} }
use self::colors::*; mod colors {
use ratatui::prelude::Color;
pub const fn tui_color_bg () -> Color { Color::Rgb(28, 35, 25) }
pub const fn tui_bg0 () -> Color { Color::Rgb(20, 20, 20) }
pub const fn tui_bo1 () -> Color { Color::Rgb(100, 110, 40) }
pub const fn tui_bo2 () -> Color { Color::Rgb(70, 80, 50) }
pub const fn tui_border_bg () -> Color { Color::Rgb(40, 50, 30) }
pub const fn tui_border_fg (f: bool) -> Color { if f { tui_bo1() } else { tui_bo2() } }
pub const fn tui_brown () -> Color { Color::Rgb(128,255,0) }
pub const fn tui_electric () -> Color { Color::Rgb(0,255,128) }
pub const fn tui_g (g: u8) -> Color { Color::Rgb(g, g, g) }
pub const fn tui_green () -> Color { Color::Rgb(0,255,0) }
pub const fn tui_mode_bg () -> Color { Color::Rgb(150, 160, 90) }
pub const fn tui_mode_fg () -> Color { Color::Rgb(255, 255, 255) }
pub const fn tui_null () -> Color { Color::Reset }
pub const fn tui_orange () -> Color { Color::Rgb(255,128,0) }
pub const fn tui_red () -> Color { Color::Rgb(255,0, 0) }
pub const fn tui_separator_fg (_: bool) -> Color { Color::Rgb(0, 0, 0) }
pub const fn tui_status_bar_bg () -> Color { Color::Rgb(28, 35, 25) }
pub const fn tui_ti1 () -> Color { Color::Rgb(150, 160, 90) }
pub const fn tui_ti2 () -> Color { Color::Rgb(120, 130, 100) }
pub const fn tui_title_fg (f: bool) -> Color { if f { tui_ti1() } else { tui_ti2() } }
pub const fn tui_yellow () -> Color { Color::Rgb(255,255,0) }
}
/// Spawn the TUI input thread which reads keys from the terminal.
pub fn tui_input <T: Do<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 = "lang")]
pub fn from_dsl (dsl: impl Language) -> Usually<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,
})
}
}

View file

@ -1,4 +1,4 @@
use crate::draw::Draw; use crate::{Usually, draw::{Draw, WH}};
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.
@ -46,37 +46,60 @@ pub(crate) fn width_chars_max (max: u16, text: impl AsRef<str>) -> u16 {
#[cfg(feature = "term")] mod impl_term { #[cfg(feature = "term")] mod impl_term {
use super::*; use super::*;
use crate::draw::XYWH; use crate::draw::XYWH;
use ratatui::prelude::{Buffer, Position}; use crate::term::Tui;
impl<'a, T> Draw<Buffer> for TrimStringRef<'a, T> { use ratatui::prelude::Position;
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()); impl Draw<Tui> for str {
to.text(&self, x, y, w) fn draw (&self, to: &mut Tui) -> Usually<WH<u16>> { todo!() }
}
} }
impl<T> Draw<Buffer> for TrimString<T> {
fn draw (&self, to: &mut Buffer) { self.as_ref().draw(to) } impl<T: AsRef<str>> Draw<Tui> for TrimString<T> {
fn draw (&self, to: &mut Tui) -> Usually<WH<u16>> { 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<Tui> for TrimStringRef<'_, T> {
} fn draw (&self, to: &mut Tui) -> Usually<WH<u16>> {
impl<T: AsRef<str>> Draw<Buffer> for TrimStringRef<'_, T> { let area = to.area();
fn draw (&self, target: &mut Buffer) {
let area = target.area();
let mut buf = target.buffer.write().unwrap();
let mut width: u16 = 1; let mut width: u16 = 1;
let mut chars = self.1.as_ref().chars(); let mut chars = self.1.as_ref().chars();
while let Some(c) = chars.next() { while let Some(c) = chars.next() {
if width > self.0 || width > area.w() { if width > self.0 || width > area.w() {
break break
} }
if let Some(cell) = buf.cell_mut(Position { let x = area.x() + width - 1;
x: area.x() + width - 1, let y = area.y();
y: area.y() let pos = Position { x, y };
}) { if let Some(cell) = to.cell_mut(pos) {
cell.set_char(c); cell.set_char(c);
} }
width += c.width().unwrap_or(0) as u16; width += c.width().unwrap_or(0) as u16;
} }
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 Tui {
/// 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
}
}
} }
} }
} }