use crate::{*, lang::*}; use ratatui::{prelude::{Style, Position, Backend, Color}}; /// TUI works in u16 coordinates. impl Coord for u16 { fn plus (self, other: Self) -> Self { self.saturating_add(other) } } impl_draw!(|self: u64, _to: Tui|{ todo!() }); impl_draw!(|self: f64, _to: Tui|{ todo!() }); impl Screen for Tui { type Unit = u16; /// Render drawable in subarea specified by `area` fn show <'t, T: Draw> (&mut self, content: T) -> Perhaps> { let previous_area = self.1; Ok(if let Some(area) = content.layout(self.1)? { self.1 = area; if let Some(result_area) = content.draw(self)? { self.1 = previous_area; Some(result_area) } else { None } } else { None }) } } /// Enable TUI output for state struct. #[macro_export] macro_rules! tui_view { (|$self:ident: $State:ty|$body:block) => { impl View for $State { fn view (&$self) -> impl Draw $body } } } /// Spawn the TUI output thread which writes colored characters to the terminal. /// /// ``` /// let state = Arc::new(RwLock::new(())); /// Exit::run(|exit|tui_output(exit, state, sleep, stdout())) /// ``` pub fn tui_output < W: Write + Send + Sync + 'static, T: View + Send + Sync + 'static > ( exited: &Arc, state: &Arc>, sleep: Duration, output: W, ) -> Usually { let state = state.clone(); 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 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() { prev.resize(&mut backend, width, height); state.view().draw(&mut next).expect("draw failed"); // TODO draw error prev.redraw(&mut backend, &mut next); } let timer = format!("{:>3.3}ms", perf.used.load(Relaxed)); prev.set_string(0, 0, &timer, Style::default()); })?) } 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) } } /// Apply foreground color. pub const fn fg (fg: Color, draw: impl Draw) -> impl Draw { 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) -> impl Draw { 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) -> 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(Some(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|{ fill_mod(on, modifier).draw(to)?; draw.draw(to) }) } pub const fn fill_mod (on: bool, modifier: Modifier) -> impl Draw { thunk(move|to: &mut Tui|Ok(Some({ 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) -> impl Draw { modify(on, Modifier::BOLD, draw) } pub const fn fill_ul (color: Option) -> impl Draw { thunk(move|to: &mut Tui|Ok(Some(if let Some(color) = color { to.update(&|cell,_,_|{ cell.modifier.insert(Modifier::UNDERLINED); cell.underline_color = color; }) } else { to.update(&|cell,_,_|{ cell.modifier.remove(Modifier::UNDERLINED); cell.underline_color = Reset; }) }))) } 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 { h_exact(1, fg_bg(fg, bg, x_repeat(self::phat::LO))) } /// A phat line pub fn hi (fg: Color, bg: Color) -> impl Draw { h_exact(1, 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] = &[' ', '🞂', ' ']; } pub const fn x_repeat (c: &str) -> impl Draw { thunk(move|to: &mut Tui|{ let XYWH(x, y, w, _h) = to.xywh(); for x in x..x+w { if let Some(cell) = to.0.cell_mut(Position::from((x, y))) { cell.set_symbol(&c); } } Ok(Some(XYWH(x, y, w, 1))) }) } pub const fn y_repeat (c: &str) -> impl Draw { thunk(move|to: &mut Tui|{ let XYWH(x, y, _w, h) = to.xywh(); for y in y..y+h { if let Some(cell) = to.0.cell_mut(Position::from((x, y))) { cell.set_symbol(&c); } } Ok(Some(XYWH(x, y, 1, h))) }) } pub const fn xy_repeat (c: &str) -> impl Draw { thunk(move|to: &mut Tui|{ let XYWH(x, y, w, h) = to.xywh(); 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.0.cell_mut(Position::from((x, y))) { let u = u % a; cell.set_symbol(&c[u..u+1]); } } } Ok(Some(XYWH(x, y, w, h))) }) } /// ``` /// let _ = tengri::button_2("", "", true); /// let _ = tengri::button_2("", "", false); /// ``` pub const fn button_2 <'a> (key: impl Draw, label: impl Draw, hide: bool) -> impl Draw { let c1 = tui_orange(); let c2 = tui_g(0); 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("", "", "", false); /// ``` pub const fn button_3 <'a> ( key: impl Draw, label: impl Draw, value: impl Draw, editing: bool, ) -> impl Draw { bold(true, east( fg_bg(tui_orange(), tui_g(0), east(fg(tui_g(0), "▐"), east(key, fg(if editing { tui_g(128) } else { tui_g(96) }, "▐")))), east( when(!editing, east(fg_bg(tui_g(255), tui_g(96), label), fg_bg(tui_g(128), tui_g(96), "▐"),)), east(fg_bg(tui_g(224), tui_g(128), value), fg_bg(tui_g(128), Reset, "▌"), )))) } /// Stackably padded. /// /// ``` /// /// TODO /// ``` pub fn phat (w: u16, h: u16, [fg, bg, hi, lo]: [Color;4], draw: impl Draw) -> impl Draw { let top = w_exact(1, self::phat::lo(bg, hi)); let low = w_exact(1, self::phat::hi(bg, lo)); let draw = fg_bg(fg, bg, draw); wh_min(Some(w), Some(h), south(top, north(low, draw))) } fn x_scroll () -> impl Draw { thunk(|Tui(buf, XYWH(x1, y1, w, h)): &mut Tui|{ 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('╌'); } } } Ok(Some(XYWH(*x1, *y1, *w, 1))) }) } fn y_scroll () -> impl Draw { thunk(|Tui(buf, XYWH(x1, y1, w, h)): &mut Tui|{ 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('╎'); // ━ } } } Ok(Some(XYWH(*x1, *y1, 1, *h))) }) } pub fn tui_update ( Tui(buf, ..): &mut Tui, area: XYWH, callback: &impl Fn(&mut Cell, u16, u16) ) { } /// Draw TUI content or its error message. /// /// ``` /// let _ = tengri::term::catcher(Ok(Some("hello"))); /// let _ = tengri::term::catcher(Ok(None)); /// let _ = tengri::term::catcher(Err("draw fail".into())); /// ``` pub fn catcher > (result: Usually) -> impl Draw { thunk(move|to: &mut Tui|match result { Ok(content) => content.draw(to), Err(e) => { let err_fg = Color::Rgb(255,224,244); let err_bg = Color::Rgb(96, 24, 24); let title = east(bold(true, "upsi daisy. "), "rendering failed."); let error = east("\"why?\" ", bold(true, format!("{e}"))); fg(err_fg, bg(err_bg, south(title, error))).draw(to) } }) }