use crate::{*, lang::*, play::*, draw::*, color::*}; 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}, } }; /// One trait to bind 'em. pub trait Tui: Draw + Do> { fn handle (&mut self, e: TuiEvent) -> Perhaps { Ok(None) } fn render (&self, to: &mut Buffer) {} } /// Rendering is delegated to [Tui] impl. impl Draw for T { fn draw (&self, to: &mut Buffer) { Tui::render(self, to) } } /// Event handling is delegated to [Tui] impl. impl Do> for T { fn apply (&mut self, event: TuiEvent) -> Perhaps { Tui::handle(self, event) } } /// Spawn the TUI output thread which writes colored characters to the terminal. pub fn tui_output + Send + Sync + 'static> ( output: W, exited: &Arc, state: &Arc>, sleep: Duration ) -> Result { 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 ( backend: &mut CrosstermBackend, buffer: &mut Buffer, size: WH ) { 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, 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 (backend: &mut CrosstermBackend) -> Usually<()> { stdout().execute(LeaveAlternateScreen)?; backend.show_cursor()?; disable_raw_mode().map_err(Into::into) } pub fn tui_update ( buf: &mut Buffer, area: XYWH, 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 (backend: &mut CrosstermBackend) -> WH { let Size { width, height } = backend.size().expect("get size failed"); WH(width, height) } /// Draw contents with foreground color applied. pub 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 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 fn modify (enabled: bool, modifier: Modifier, item: impl Tui) -> impl Tui { |to: &mut Buffer|item.draw(to.with_modifier(enabled, modifier, modifier)) } /// Draw contents with style applied. pub fn styled (enabled: bool, style: Style, item: impl Tui) -> impl Tui { |to: &mut Buffer|item.draw(to.with_style(enabled, style, modifier)) } /// Draw border around shrinked item. /// /// ``` /// /// TODO /// ``` pub fn border (on: bool, style: S, draw: impl Tui) -> impl Tui { fill_wh(bsp_a(when(on, |output|{/*TODO*/}), 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 (error: Perhaps, draw: impl Tui) -> impl Tui { move|to: &mut Buffer|match error.as_ref() { Ok(Some(content)) => draw(to), Ok(None) => to.blit(&"", 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 } /// Spawn the TUI input thread which reads keys from the terminal. pub fn tui_input + Send + Sync + 'static> ( exited: &Arc, state: &Arc>, poll: Duration ) -> Result { 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}") } } } }) } impl Coord for u16 { fn plus (self, other: Self) -> Self { self.saturating_add(other) } } impl_from!(BigBuffer: |size:(usize, usize)| Self::new(size.0, size.1)); impl_from!(ItemTheme: |base: ItemColor| Self::from_item_color(base)); impl_from!(ItemTheme: |base: Color| Self::from_tui_color(base)); impl_from!(ItemColor: |rgb: Color| Self { rgb, okhsl: rgb_to_okhsl(rgb) }); impl_from!(ItemColor: |okhsl: Okhsl| Self { okhsl, rgb: okhsl_to_rgb(okhsl) }); impl_debug!(BigBuffer |self, f| { write!(f, "[BB {}x{} ({})]", self.width, self.height, self.content.len()) }); impl Screen for Buffer { type Unit = u16; } impl TuiOut for Buffer { fn tui_out (&mut self) -> &mut Buffer { self } } impl TuiOut for Buffer { fn tui_out (&mut self) -> &mut Buffer { self } } //impl> 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) -> Usually { 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 (mut self, join: bool, state: &Arc>) -> Usually> where T: Act + Draw + 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 TuiEvent { pub fn from_crossterm (event: Event) -> Self { Self(event) } #[cfg(feature = "dsl")] pub fn from_dsl (dsl: impl Language) -> Perhaps { 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 } } impl From for TuiEvent { fn from (e: Event) -> Self { Self(e) } } impl From for TuiEvent { fn from (c: char) -> Self { Self(Event::Key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE))) } } impl TuiKey { const SPLIT: char = '/'; #[cfg(feature = "dsl")] pub fn from_dsl (dsl: impl Language) -> Usually { if let Some(word) = dsl.word()? { let word = word.trim(); Ok(if word == ":char" { Self(None, KeyModifiers::NONE) } else if word.chars().nth(0) == Some('@') { let mut key = None; let mut modifiers = KeyModifiers::NONE; let mut tokens = word[1..].split(Self::SPLIT).peekable(); while let Some(token) = tokens.next() { if tokens.peek().is_some() { match token { "ctrl" | "Ctrl" | "c" | "C" => modifiers |= KeyModifiers::CONTROL, "alt" | "Alt" | "m" | "M" => modifiers |= KeyModifiers::ALT, "shift" | "Shift" | "s" | "S" => { modifiers |= KeyModifiers::SHIFT; // + TODO normalize character case, BackTab, etc. }, _ => panic!("unknown modifier {token}"), } } else { key = if token.len() == 1 { Some(KeyCode::Char(token.chars().next().unwrap())) } else { Some(named_key(token).unwrap_or_else(||panic!("unknown character {token}"))) } } } Self(key, modifiers) } else { return Err(format!("TuiKey: unexpected: {word}").into()) }) } else { return Err(format!("TuiKey: unspecified").into()) } } pub fn to_crossterm (&self) -> Option { self.0.map(|code|Event::Key(KeyEvent { code, modifiers: self.1, kind: KeyEventKind::Press, state: KeyEventState::NONE, })) } } 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 } } impl Phat { pub const LO: &'static str = "▄"; pub const HI: &'static str = "▀"; /// A phat line pub fn lo (fg: Color, bg: Color) -> impl Draw { Fixed::Y(1, Tui::fg_bg(fg, bg, Repeat::X(Self::LO))) } /// A phat line pub fn hi (fg: Color, bg: Color) -> impl Draw { Fixed::Y(1, Tui::fg_bg(fg, bg, Repeat::X(Self::HI))) } } impl Scrollbar { const ICON_DEC_V: &[char] = &['▲']; const ICON_INC_V: &[char] = &['▼']; const ICON_DEC_H: &[char] = &[' ', '🞀', ' ']; const ICON_INC_H: &[char] = &[' ', '🞂', ' ']; } impl Layout for &str { fn layout (&self, to: XYWH) -> XYWH { to.centered_xy([width_chars_max(to.w(), self), 1]) } } impl Layout for String { fn layout (&self, to: XYWH) -> XYWH { self.as_str().layout(to) } } impl Layout for Arc { fn layout (&self, to: XYWH) -> XYWH { self.as_ref().layout(to) } } impl<'a, T: AsRef> Layout for TrimString { fn layout (&self, to: XYWH) -> XYWH { Layout::layout(&self.as_ref(), to) } } impl<'a, T: AsRef> Layout for TrimStringRef<'a, T> { fn layout (&self, to: XYWH) -> XYWH { XYWH(to.x(), to.y(), to.w().min(self.0).min(self.1.as_ref().width() as u16), to.h()) } } impl Draw for u64 { fn draw (&self, _to: &mut T) { todo!() } } impl Draw for f64 { fn draw (&self, _to: &mut Buffer) { todo!() } } impl Draw for Repeat<'_> { fn draw (&self, to: &mut Buffer) { let XYWH(x, y, w, h) = to.area(); let mut buf = to.buffer.write().unwrap(); match self { Self::X(c) => { for x in x..x+w { if let Some(cell) = buf.cell_mut(Position::from((x, y))) { cell.set_symbol(&c); } } }, Self::Y(c) => { for y in y..y+h { if let Some(cell) = buf.cell_mut(Position::from((x, y))) { cell.set_symbol(&c); } } }, Self::XY(c) => { let a = c.len(); for (_v, y) in (y..y+h).enumerate() { for (u, x) in (x..x+w).enumerate() { if let Some(cell) = buf.cell_mut(Position::from((x, y))) { let u = u % a; cell.set_symbol(&c[u..u+1]); } } } }, } } } impl Draw for Scrollbar { fn draw (&self, to: &mut Buffer) { let XYWH(x1, y1, w, h) = to.area(); let mut buf = to.buffer.write().unwrap(); match self { Self::X { .. } => { 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::ICON_DEC_H.len()) { cell.set_fg(Rgb(255, 255, 255)); cell.set_bg(Rgb(0, 0, 0)); cell.set_char(Self::ICON_DEC_H[i as usize]); } else if i > (w as usize - Self::ICON_INC_H.len()) { cell.set_fg(Rgb(255, 255, 255)); cell.set_bg(Rgb(0, 0, 0)); cell.set_char(Self::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('╌'); } } } }, Self::Y { .. } => { 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::ICON_DEC_V.len()) { cell.set_fg(Rgb(255, 255, 255)); cell.set_bg(Rgb(0, 0, 0)); cell.set_char(Self::ICON_DEC_V[i as usize]); } else if (i as usize) > (h as usize - Self::ICON_INC_V.len()) { cell.set_fg(Rgb(255, 255, 255)); cell.set_bg(Rgb(0, 0, 0)); cell.set_char(Self::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('╎'); // ━ } } } }, } } } impl Draw for &str { fn draw (&self, to: &mut Buffer) { let XYWH(x, y, w, ..) = self.layout(to.area()); to.text(&self, x, y, w) } } impl Draw for String { fn draw (&self, to: &mut Buffer) { self.as_str().draw(to) } } impl Draw for Arc { fn draw (&self, to: &mut Buffer) { self.as_ref().draw(to) } } impl> Draw for Background { fn draw (&self, to: &mut Buffer) { let area = self.layout(to.area()); to.fill_bg(area, self.0); to.show(area, &self.1); } } impl> Draw for Modify { fn draw (&self, to: &mut Buffer) { to.fill_mod(to.area(), self.0, self.1); self.2.draw(to) } } impl> Draw for Styled { fn draw (&self, to: &mut Buffer) { to.place(&self.1); // TODO write style over area } } impl Draw for Border { fn draw (&self, to: &mut Buffer) { let Border(enabled, style) = self; if *enabled { let area = to.area(); if area.w() > 0 && area.y() > 0 { to.blit(&style.border_nw(), area.x(), area.y(), style.style()); to.blit(&style.border_ne(), area.x() + area.w() - 1, area.y(), style.style()); to.blit(&style.border_sw(), area.x(), area.y() + area.h() - 1, style.style()); to.blit(&style.border_se(), area.x() + area.w() - 1, area.y() + area.h() - 1, style.style()); for x in area.x()+1..area.x()+area.w()-1 { to.blit(&style.border_n(), x, area.y(), style.style()); to.blit(&style.border_s(), x, area.y() + area.h() - 1, style.style()); } for y in area.y()+1..area.y()+area.h()-1 { to.blit(&style.border_w(), area.x(), y, style.style()); to.blit(&style.border_e(), area.x() + area.w() - 1, y, style.style()); } } } } } impl<'a, T: AsRef> Draw for TrimString { fn draw (&self, to: &mut Buffer) { Draw::draw(&self.as_ref(), to) } } impl> Draw 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; } } } /// TUI helper defs. impl Tui { pub const fn fg (color: Color, w: T) -> Foreground { Foreground(color, w) } pub const fn bg (color: Color, w: T) -> Background { Background(color, w) } pub const fn fg_bg (fg: Color, bg: Color, w: T) -> Background> { Background(bg, Foreground(fg, w)) } pub const fn modify (enable: bool, modifier: Modifier, w: T) -> Modify { Modify(enable, modifier, w) } pub const fn bold (enable: bool, w: T) -> Modify { Self::modify(enable, Modifier::BOLD, w) } pub const fn border (enable: bool, style: S, w: T) -> Bordered { Bordered(enable, style, w) } 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) } } pub fn named_key (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('}'), "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, }) } impl> Layout for Background { fn layout (&self, to: XYWH) -> XYWH { self.1.layout(to) } } impl Tryptich<(), (), ()> { pub fn center (h: u16) -> Self { Self { h, top: false, left: (0, ()), middle: (0, ()), right: (0, ()) } } pub fn top (h: u16) -> Self { Self { h, top: true, left: (0, ()), middle: (0, ()), right: (0, ()) } } } impl Tryptich { pub fn left (self, w: u16, content: D) -> Tryptich { Tryptich { left: (w, content), ..self } } pub fn middle (self, w: u16, content: D) -> Tryptich { Tryptich { middle: (w, content), ..self } } pub fn right (self, w: u16, content: D) -> Tryptich { Tryptich { right: (w, content), ..self } } } /// ``` /// let _ = tengri::button_2("", "", true); /// let _ = tengri::button_2("", "", false); /// ``` pub fn button_2 <'a> (key: impl Draw, label: impl Draw, editing: bool) -> impl Draw { Tui::bold(true, Bsp::e( Tui::fg_bg(Tui::orange(), Tui::g(0), Bsp::e(Tui::fg(Tui::g(0), &"▐"), Bsp::e(key, Tui::fg(Tui::g(96), &"▐")))), When::new(!editing, Tui::fg_bg(Tui::g(255), Tui::g(96), label)))) } /// ``` /// let _ = tengri::button_3("", "", "", true); /// let _ = tengri::button_3("", "", "", false); /// ``` pub fn button_3 <'a> ( key: impl Draw, label: impl Draw, value: impl Draw, editing: bool, ) -> impl Draw { Tui::bold(true, Bsp::e( Tui::fg_bg(Tui::orange(), Tui::g(0), Bsp::e(Tui::fg(Tui::g(0), &"▐"), Bsp::e(key, Tui::fg(if editing { Tui::g(128) } else { Tui::g(96) }, "▐")))), Bsp::e( When::new(!editing, Bsp::e(Tui::fg_bg(Tui::g(255), Tui::g(96), label), Tui::fg_bg(Tui::g(128), Tui::g(96), &"▐"),)), Bsp::e(Tui::fg_bg(Tui::g(224), Tui::g(128), value), Tui::fg_bg(Tui::g(128), Reset, &"▌"), )))) } 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 Buffer) { if self.enabled() { let _ = BorderStyle::draw(self, to); } } } )+} } border! { Square { "┌" "─" "┐" "│" "│" "└" "─" "┘" fn style (&self) -> Option