diff --git a/examples/tui_00.rs b/examples/tui_00.rs index dc166b8..1ac73c5 100644 --- a/examples/tui_00.rs +++ b/examples/tui_00.rs @@ -10,7 +10,7 @@ tui_main!(State { }); tui_keys!(|self: State, input| { - todo!() + Ok(()) }); tui_view!(|self: State| { diff --git a/src/color.rs b/src/color.rs index f16358a..82bdc70 100644 --- a/src/color.rs +++ b/src/color.rs @@ -1,10 +1,10 @@ -use ::ratatui::style::Color; use crate::lang::impl_from; +use ::ratatui::style::Color; +use ::rand::distributions::uniform::UniformSampler; pub(crate) use ::palette::{ Okhsl, Srgb, OklabHue, Mix, okhsl::UniformOkhsl, convert::{FromColor, FromColorUnclamped} }; -use rand::distributions::uniform::UniformSampler; pub fn rgb (r: u8, g: u8, b: u8) -> ItemColor { let term = Color::Rgb(r, g, b); diff --git a/src/draw.rs b/src/draw.rs index 29dbac7..d6462fc 100644 --- a/src/draw.rs +++ b/src/draw.rs @@ -1,6 +1,3 @@ -use crate::*; -pub use crate::space::*; - mod draw; pub use self::draw::*; @@ -13,6 +10,9 @@ pub use self::thunk::*; mod screen; pub use self::screen::*; +use crate::*; +pub use crate::space::*; + /// Only render when condition is true. /// /// ``` diff --git a/src/draw/screen.rs b/src/draw/screen.rs index dc51744..ea6b858 100644 --- a/src/draw/screen.rs +++ b/src/draw/screen.rs @@ -29,6 +29,6 @@ pub trait Screen: Space + Send + Sync + Sized { fn place_at <'t, T: Draw + ?Sized> ( &mut self, _area: XYWH, _content: &'t T ) { - unimplemented!() + unimplemented!("place_at") } } diff --git a/src/exit.rs b/src/exit.rs index 0b79708..afdc2ad 100644 --- a/src/exit.rs +++ b/src/exit.rs @@ -14,4 +14,3 @@ impl AsRef> for Exit { &self.0 } } - diff --git a/src/keys.rs b/src/keys.rs index 9cc54e1..ba56615 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -7,8 +7,17 @@ use ::crossterm::event::{ read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState }; +/// Enable TUI keyboard input for main state struct. +#[macro_export] macro_rules! tui_keys { + (|$self:ident:$State:ty,$input:ident|$body:block) => { + impl Apply> for $State { + fn apply (&mut $self, $input: &TuiEvent) -> Usually<()> $body + } + }; +} + /// Spawn the TUI input thread which reads keys from the terminal. -pub fn tui_input > + Send + Sync + 'static> ( +pub fn tui_input > + Send + Sync + 'static> ( exited: &Arc, state: &Arc>, poll: Duration ) -> Result { let exited = exited.clone(); @@ -39,8 +48,11 @@ pub fn tui_input > + Send + Sync + 'static> ( /// 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 Ord for TuiEvent { fn cmp (&self, other: &Self) -> std::cmp::Ordering { self.partial_cmp(other) @@ -51,6 +63,7 @@ impl Ord for TuiEvent { /// TUI key spec. #[derive(Debug, Clone, Eq, PartialEq, PartialOrd)] pub struct TuiKey(pub Option, pub KeyModifiers); + impl TuiKey { const SPLIT: char = '/'; pub fn from_crossterm (event: KeyEvent) -> Self { @@ -67,43 +80,43 @@ impl TuiKey { pub fn named (token: &str) -> Option { 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('}'), + "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), + "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, }) } diff --git a/src/term.rs b/src/term.rs index 4be440c..ef9ddd1 100644 --- a/src/term.rs +++ b/src/term.rs @@ -1,4 +1,7 @@ -use crate::{*, lang::*, draw::*, space::{*, Split::*}, color::*, text::*, task::*}; +mod border; +pub use self::border::*; + +use crate::{*, lang::*, draw::*, task::*}; //use unicode_width::{UnicodeWidthStr, UnicodeWidthChar}; //use rand::distributions::uniform::UniformSampler; use ::{ @@ -7,51 +10,84 @@ use ::{ time::Duration, ops::{Deref, DerefMut}, }, - better_panic::{Settings, Verbosity}, ratatui::{ - prelude::{Style, Buffer as ScreenBuffer, Position, Backend, Color}, + prelude::{Style, Position, Backend, Color}, style::{Modifier, Color::*}, backend::{CrosstermBackend, ClearType}, layout::{Size, Rect}, buffer::{Buffer, Cell}, + crossterm::{ + ExecutableCommand, + terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode}, + //event::{poll, read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState}, + } }, - crossterm::{ - ExecutableCommand, - terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode}, - //event::{poll, read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState}, - } }; #[macro_export] macro_rules! tui_main { ($state:expr) => { pub fn main () -> Usually<()> { - tengri::exit::Exit::run(|exit|{ - let state = Arc::new(RwLock::new($state)); - let input = ::tengri::keys::tui_input( - exit.as_ref(), - &state, - ::std::time::Duration::from_millis(100) - )?; - let output = ::tengri::term::tui_output( - stdout(), - exit.as_ref(), - &state, - ::std::time::Duration::from_millis(10) - )?; + use ::{ + std::sync::{ + Arc, + RwLock + }, + std::panic::{ + set_hook, + PanicHookInfo, + }, + std::time::{ + Duration + }, + better_panic::{ + Settings, + Verbosity + }, + ratatui::{ + backend::{ + Backend, + CrosstermBackend + }, + crossterm::{ + ExecutableCommand, + terminal::{ + disable_raw_mode, + LeaveAlternateScreen + } + } + }, + tengri::{ + exit::Exit, + keys::tui_input, + term::tui_output, + } + }; + let panic = Settings::auto() + .verbosity(Verbosity::Full) + .create_panic_handler(); + set_hook(Box::new(move |info: &PanicHookInfo|{ + stdout().execute(LeaveAlternateScreen).unwrap(); + CrosstermBackend::new(stdout()).show_cursor().unwrap(); + disable_raw_mode().unwrap(); + panic(info); + })); + Exit::run(|exit|{ + let state = Arc::new(RwLock::new($state)); + let scan = Duration::from_millis(100); + let input = tui_input(exit.as_ref(), &state, scan)?; + let frame = Duration::from_millis(10); + let output = tui_output(stdout(), exit.as_ref(), &state, frame)?; + output.join(); + stdout().execute(LeaveAlternateScreen)?; + CrosstermBackend::new(stdout()).show_cursor()?; + disable_raw_mode()?; Ok(()) }) } } } -#[macro_export] macro_rules! tui_keys { - (|$self:ident:$State:ty,$input:ident|$body:block) => { - impl Apply> for $State { - fn apply (&mut $self, $input: &TuiEvent) -> Usually $body - } - }; -} - +/// Enable TUI output for state struct. #[macro_export] macro_rules! tui_view { (|$self:ident: $State:ty|$body:block) => { impl View for $State { @@ -61,42 +97,94 @@ use ::{ } /// Spawn the TUI output thread which writes colored characters to the terminal. -pub fn tui_output + Send + Sync + 'static> ( +pub fn tui_output < + W: Write + Send + Sync + 'static, T: View + Send + Sync + 'static +> ( output: W, exited: &Arc, state: &Arc>, sleep: Duration ) -> Usually { let state = state.clone(); - tui_setup()?; + stdout().execute(EnterAlternateScreen)?; + CrosstermBackend::new(stdout()).hide_cursor()?; + enable_raw_mode()?; let mut backend = CrosstermBackend::new(output); let Size { width, height } = backend.size().expect("get size failed"); - 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 }); + let mut prev = Tui::new(width, height); + let mut next = Tui::new(width, height); Ok(Task::new_sleep(exited.clone(), sleep, move |perf| { let Size { width, height } = backend.size().expect("get size failed"); if let Ok(state) = state.try_read() { - tui_resize(&mut backend, &mut buffer_a, Rect { x: 0, y: 0, width, height }); - tui_redraw(&mut backend, &mut buffer_a, &mut buffer_b); + prev.resize(&mut backend, width, height); + state.view().draw(&mut next).expect("draw failed"); // TODO draw error + prev.redraw(&mut backend, &mut next); + //tui_redraw(&mut backend, &mut prev, &mut next); } let timer = format!("{:>3.3}ms", perf.used.load(Relaxed)); - buffer_a.set_string(0, 0, &timer, Style::default()); + prev.set_string(0, 0, &timer, Style::default()); })?) } -pub struct Tui(pub Buffer, pub XYWH); -impl Screen for Tui { type Unit = u16; } -impl Deref for Tui { type Target = Buffer; fn deref (&self) -> &Buffer { &self.0 } } -impl DerefMut for Tui { fn deref_mut (&mut self) -> &mut Buffer { &mut self.0 } } +pub fn tui_redraw <'b, W: Write> ( + back: &mut CrosstermBackend, + mut prev: &'b mut Buffer, + mut next: &'b mut Buffer +) { + let updates = prev.diff(&next); + back.draw(updates.into_iter()).expect("failed to render"); + Backend::flush(back).expect("failed to flush output new"); + std::mem::swap(&mut prev, &mut next); + next.reset(); +} + +/// Terminal output. +pub struct Tui( + /// Ratatui buffer; area is screen size + pub Buffer, + /// Current draw area + pub XYWH +); + +impl Tui { + fn new (width: u16, height: u16) -> Self { + Self(Buffer::empty(Rect { x: 0, y: 0, width, height }), XYWH(0, 0, width, height)) + } + fn resize (&mut self, back: &mut CrosstermBackend, width: u16, height: u16) { + let size = Rect { x: 0, y: 0, width, height }; + if self.0.area != size { + back.clear_region(ClearType::All).unwrap(); + self.0.resize(size); + self.0.reset(); + } + } + fn redraw <'b, W: Write> (&'b mut self, back: &mut CrosstermBackend, mut next: &'b mut Self) { + let updates = self.0.diff(&next.0); + back.draw(updates.into_iter()).expect("failed to render"); + Backend::flush(back).expect("failed to flush output new"); + std::mem::swap(self, &mut next); + next.0.reset(); + } +} + +impl Screen for Tui { type Unit = u16; } + +impl Deref for Tui { type Target = Buffer; fn deref (&self) -> &Buffer { &self.0 } } + +impl DerefMut for Tui { fn deref_mut (&mut self) -> &mut Buffer { &mut self.0 } } + impl HasOrigin for Tui { fn origin (&self) -> Origin { Origin::NW } } + impl X for Tui { fn x (&self) -> u16 { self.1.0 } fn w (&self) -> u16 { self.1.2 } } + impl Y for Tui { fn y (&self) -> u16 { self.1.1 } fn h (&self) -> u16 { self.1.3 } } + impl Tui { pub fn update (&mut self, callback: &impl Fn(&mut Cell, u16, u16)) -> XYWH { for row in 0..self.h() { @@ -127,6 +215,7 @@ impl Tui { } } } + /// Apply foreground color. pub const fn fg (fg: Color, draw: impl Draw) -> impl Draw { thunk(move|to: &mut Tui|{ @@ -134,6 +223,7 @@ pub const fn fg (fg: Color, draw: impl Draw) -> impl Draw { draw.draw(to) }) } + /// Apply background color. pub const fn bg (bg: Color, draw: impl Draw) -> impl Draw { thunk(move|to: &mut Tui|{ @@ -141,17 +231,20 @@ pub const fn bg (bg: Color, draw: impl Draw) -> impl Draw { draw.draw(to) }) } + pub const fn fg_bg (fg: Color, bg: Color, draw: impl Draw) -> impl Draw { 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) -> impl Draw { 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) -> impl Draw { thunk(move|to: &mut Tui|{ @@ -159,6 +252,7 @@ pub const fn modify (on: bool, modifier: Modifier, draw: impl Draw) -> impl draw.draw(to) }) } + pub const fn fill_mod (on: bool, modifier: Modifier) -> impl Draw { thunk(move|to: &mut Tui|Ok({ if on { @@ -168,10 +262,12 @@ pub const fn fill_mod (on: bool, modifier: Modifier) -> impl Draw { } })) } + /// Draw contents with bold modifier applied. pub const fn bold (on: bool, draw: impl Draw) -> impl Draw { modify(on, Modifier::BOLD, draw) } + pub const fn fill_ul (color: Option) -> impl Draw { thunk(move|to: &mut Tui|Ok(if let Some(color) = color { to.update(&|cell,_,_|{ @@ -196,6 +292,7 @@ impl Coord for u16 { impl Draw for u64 { fn draw (self, _to: &mut Tui) -> Usually> { todo!() } } + impl Draw for f64 { fn draw (self, _to: &mut Tui) -> Usually> { todo!() } } @@ -288,216 +385,6 @@ pub const fn button_3 <'a> ( east(fg_bg(tui_g(224), tui_g(128), value), 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 for $T {} - impl Draw for $T { - fn draw (self, to: &mut Tui) -> Usually> { - when(self.enabled(), thunk(|to: &mut Tui|BorderStyle::draw(self, to))).draw(to) - } - } - )+} -} - -border! { - Square { - "┌" "─" "┐" - "│" "│" - "└" "─" "┘" fn style (&self) -> Option