From 2dc74657d5d9a0658bae64b8a5a055151aacf8ed Mon Sep 17 00:00:00 2001 From: same mf who else Date: Tue, 17 Feb 2026 04:09:10 +0200 Subject: [PATCH] refactor(output,tui): fix errors, now there's more... --- output/Cargo.toml | 6 +- output/src/lib.rs | 2 + output/src/out_impls.rs | 86 ++- output/src/out_structs.rs | 12 + output/src/out_traits.rs | 4 + tui/src/lib.rs | 196 ++++++- tui/src/tui_impls.rs | 1051 ++++++++++++++----------------------- tui/src/tui_structs.rs | 105 ++-- tui/src/tui_traits.rs | 5 - 9 files changed, 713 insertions(+), 754 deletions(-) diff --git a/output/Cargo.toml b/output/Cargo.toml index ac61804..dedb56e 100644 --- a/output/Cargo.toml +++ b/output/Cargo.toml @@ -9,8 +9,10 @@ bumpalo = [ "dep:bumpalo" ] dsl = [] [dependencies] -dizzle = { path = "../../dizzle" } -bumpalo = { optional = true, workspace = true } +atomic_float = { workspace = true } +bumpalo = { workspace = true, optional = true } +dizzle = { path = "../../dizzle" } +quanta = { workspace = true } [dev-dependencies] tengri = { path = "../tengri", features = [ "dsl", "tui" ] } diff --git a/output/src/lib.rs b/output/src/lib.rs index 40d31f0..da9e1bf 100644 --- a/output/src/lib.rs +++ b/output/src/lib.rs @@ -15,6 +15,8 @@ pub(crate) use std::ops::{Add, Sub, Mul, Div}; pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::Relaxed}}; pub(crate) use std::marker::PhantomData; pub(crate) use dizzle::*; +pub(crate) use quanta::Clock; +pub(crate) use atomic_float::AtomicF64; // Define macros first, so that private macros are available in private modules: diff --git a/output/src/out_impls.rs b/output/src/out_impls.rs index 787d752..5b160ca 100644 --- a/output/src/out_impls.rs +++ b/output/src/out_impls.rs @@ -41,68 +41,68 @@ impl HasWH for O { } impl WH { - fn clip_w (&self, w: N) -> [N;2] { [self.w().min(w), self.h()] } - fn clip_h (&self, h: N) -> [N;2] { [self.w(), self.h().min(h)] } - fn expect_min (&self, w: N, h: N) -> Usually<&Self> { + pub fn clip_w (&self, w: N) -> [N;2] { [self.w().min(w), self.h()] } + pub fn clip_h (&self, h: N) -> [N;2] { [self.w(), self.h().min(h)] } + pub fn expect_min (&self, w: N, h: N) -> Usually<&Self> { if self.w() < w || self.h() < h { return Err(format!("min {w}x{h}").into()) } Ok(self) } } impl XYWH { - fn zero () -> Self { + pub fn zero () -> Self { Self(0.into(), 0.into(), 0.into(), 0.into()) } - fn x2 (&self) -> N { + pub fn x2 (&self) -> N { self.x().plus(self.w()) } - fn y2 (&self) -> N { + pub fn y2 (&self) -> N { self.y().plus(self.h()) } - fn with_w (&self, w: N) -> XYWH { + pub fn with_w (&self, w: N) -> XYWH { Self(self.x(), self.y(), w, self.h()) } - fn with_h (&self, h: N) -> XYWH { + pub fn with_h (&self, h: N) -> XYWH { Self(self.x(), self.y(), self.w(), h) } - fn lrtb (&self) -> XYWH { + pub fn lrtb (&self) -> XYWH { Self(self.x(), self.x2(), self.y(), self.y2()) } - fn clipped_w (&self, w: N) -> XYWH { + pub fn clipped_w (&self, w: N) -> XYWH { Self(self.x(), self.y(), self.w().min(w), self.h()) } - fn clipped_h (&self, h: N) -> XYWH { + pub fn clipped_h (&self, h: N) -> XYWH { Self(self.x(), self.y(), self.w(), self.h().min(h)) } - fn clipped (&self, wh: WH) -> XYWH { + pub fn clipped (&self, wh: WH) -> XYWH { Self(self.x(), self.y(), wh.w(), wh.h()) } /// Iterate over every covered X coordinate. - fn iter_x (&self) -> impl Iterator where N: std::iter::Step { + pub fn iter_x (&self) -> impl Iterator where N: std::iter::Step { let Self(x, _, w, _) = *self; x..(x+w) } /// Iterate over every covered Y coordinate. - fn iter_y (&self) -> impl Iterator where N: std::iter::Step { + pub fn iter_y (&self) -> impl Iterator where N: std::iter::Step { let Self(_, y, _, h) = *self; y..(y+h) } - fn center (&self) -> XY { + pub fn center (&self) -> XY { let Self(x, y, w, h) = self; XY(self.x().plus(self.w()/2.into()), self.y().plus(self.h()/2.into())) } - fn centered (&self) -> XY { + pub fn centered (&self) -> XY { let Self(x, y, w, h) = *self; XY(x.minus(w/2.into()), y.minus(h/2.into())) } - fn centered_x (&self, n: N) -> XYWH { + pub fn centered_x (&self, n: N) -> XYWH { let Self(x, y, w, h) = *self; XYWH((x.plus(w / 2.into())).minus(n / 2.into()), y.plus(h / 2.into()), n, 1.into()) } - fn centered_y (&self, n: N) -> XYWH { + pub fn centered_y (&self, n: N) -> XYWH { let Self(x, y, w, h) = *self; XYWH(x.plus(w / 2.into()), (y.plus(h / 2.into())).minus(n / 2.into()), 1.into(), n) } - fn centered_xy (&self, [n, m]: [N;2]) -> XYWH { + pub fn centered_xy (&self, [n, m]: [N;2]) -> XYWH { let Self(x, y, w, h) = *self; XYWH((x.plus(w / 2.into())).minus(n / 2.into()), (y.plus(h / 2.into())).minus(m / 2.into()), n, m) } @@ -847,3 +847,51 @@ impl Field { Field:: { value, value_fg: fg, value_bg: bg, value_align: align, ..self } } } + +impl Default for PerfModel { + fn default () -> Self { + Self { + enabled: true, + clock: quanta::Clock::new(), + used: Default::default(), + window: Default::default(), + } + } +} + +impl PerfModel { + pub fn get_t0 (&self) -> Option { + if self.enabled { + Some(self.clock.raw()) + } else { + None + } + } + pub fn get_t1 (&self, t0: Option) -> Option { + if let Some(t0) = t0 { + if self.enabled { + Some(self.clock.delta(t0, self.clock.raw())) + } else { + None + } + } else { + None + } + } + pub fn update (&self, t0: Option, microseconds: f64) { + if let Some(t0) = t0 { + let t1 = self.clock.raw(); + self.used.store(self.clock.delta_as_nanos(t0, t1) as f64, Relaxed); + self.window.store(microseconds, Relaxed,); + } + } + pub fn percentage (&self) -> Option { + let window = self.window.load(Relaxed) * 1000.0; + if window > 0.0 { + let used = self.used.load(Relaxed); + Some(100.0 * used / window) + } else { + None + } + } +} diff --git a/output/src/out_structs.rs b/output/src/out_structs.rs index c267a29..8191b3e 100644 --- a/output/src/out_structs.rs +++ b/output/src/out_structs.rs @@ -256,3 +256,15 @@ pub struct Field { pub value_bg: Option, pub value_align: Option, } + +/// Performance counter +#[derive(Debug)] +pub struct PerfModel { + pub enabled: bool, + + pub clock: quanta::Clock, + // In nanoseconds. Time used by last iteration. + pub used: AtomicF64, + // In microseconds. Max prescribed time for iteration (frame, chunk...). + pub window: AtomicF64, +} diff --git a/output/src/out_traits.rs b/output/src/out_traits.rs index 127d1ae..2d81030 100644 --- a/output/src/out_traits.rs +++ b/output/src/out_traits.rs @@ -155,3 +155,7 @@ pub trait Measured { fn measure_width (&self) -> O::Unit { self.measure().w() } fn measure_height (&self) -> O::Unit { self.measure().h() } } + +pub trait HasPerf { + fn perf (&self) -> &PerfModel; +} diff --git a/tui/src/lib.rs b/tui/src/lib.rs index 976d230..ee26633 100644 --- a/tui/src/lib.rs +++ b/tui/src/lib.rs @@ -23,8 +23,8 @@ pub(crate) use ::{ better_panic::{Settings, Verbosity}, palette::{*, convert::*, okhsl::*}, ratatui::{ - prelude::{Color, Style, Buffer}, - style::Modifier, + prelude::{Color, Style, Buffer, Position}, + style::{Stylize, Modifier, Color::*}, backend::{Backend, CrosstermBackend, ClearType}, layout::{Size, Rect}, buffer::Cell @@ -85,8 +85,155 @@ mod tui_structs; pub use self::tui_structs::*; mod tui_traits; pub use self::tui_traits::*; mod tui_impls; pub use self::tui_impls::*; -#[cfg(feature = "dsl")] -pub fn evaluate_output_expression_tui <'a, S> ( +/// Run an app in the main loop. +pub fn tui_run (state: &Arc>) -> Usually>> { + let backend = CrosstermBackend::new(stdout()); + let Size { width, height } = backend.size()?; + let tui = Arc::new(RwLock::new(Tui { + exited: Arc::new(AtomicBool::new(false)), + buffer: Buffer::empty(Rect { x: 0, y: 0, width, height }), + area: [0, 0, width, height], + perf: Default::default(), + backend, + })); + let _input_thread = tui_input(tui, state, Duration::from_millis(100)); + tui.write().unwrap().setup()?; + let render_thread = tui_output(tui, state, Duration::from_millis(10))?; + match render_thread.join() { + Ok(result) => { + tui.write().unwrap().teardown()?; + println!("\n\rRan successfully: {result:?}\n\r"); + }, + Err(error) => { + tui.write().unwrap().teardown()?; + panic!("\n\rDraw thread failed: error={error:?}.\n\r") + }, + } + Ok(tui) +} + +pub fn tui_setup (backend: &mut CrosstermBackend) -> Usually<()> { + let better_panic_handler = Settings::auto().verbosity(Verbosity::Full).create_panic_handler(); + std::panic::set_hook(Box::new(move |info: &std::panic::PanicHookInfo|{ + stdout().execute(LeaveAlternateScreen).unwrap(); + CrosstermBackend::new(stdout()).show_cursor().unwrap(); + disable_raw_mode().unwrap(); + better_panic_handler(info); + })); + stdout().execute(EnterAlternateScreen)?; + backend.hide_cursor()?; + enable_raw_mode().map_err(Into::into) +} + +pub fn tui_teardown (backend: &mut CrosstermBackend) -> Usually<()> { + stdout().execute(LeaveAlternateScreen)?; + backend.show_cursor()?; + disable_raw_mode().map_err(Into::into) +} + +pub fn tui_resized ( + backend: &mut CrosstermBackend, buffer: &mut Buffer, size: ratatui::prelude::Rect +) { + if buffer.area != size { + backend.clear_region(ClearType::All).unwrap(); + buffer.resize(size); + buffer.reset(); + } +} + +pub fn tui_redrawn ( + backend: &mut CrosstermBackend, buffer: &mut Buffer, new_buffer: &mut Buffer +) { + let updates = buffer.diff(&new_buffer); + backend.draw(updates.into_iter()).expect("failed to render"); + backend.flush().expect("failed to flush output new_buffer"); + std::mem::swap(&mut buffer, &mut new_buffer); + new_buffer.reset(); +} + +pub fn tui_update (buf: &mut Buffer, area: XY, 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); + } + } + } + } +} + +/// Spawn the output thread. +pub fn tui_output + Send + Sync + 'static> ( + engine: &Arc>, state: &Arc>, timer: Duration +) -> Result, std::io::Error> { + let exited = engine.read().unwrap().exited.clone(); + let engine = engine.clone(); + let state = state.clone(); + let Size { width, height } = engine.read().unwrap().backend.size().expect("get size failed"); + let mut buffer = Buffer::empty(Rect { x: 0, y: 0, width, height }); + std::thread::Builder::new() + .name("tui output thread".into()) + .spawn(move || loop { + if exited.fetch_and(true, Relaxed) { + break + } + let t0 = engine.read().unwrap().perf.get_t0(); + let Size { width, height } = engine.read().unwrap().backend.size() + .expect("get size failed"); + if let Ok(state) = state.try_read() { + let size = Rect { x: 0, y: 0, width, height }; + if buffer.area != size { + engine.write().unwrap().backend.clear_region(ClearType::All).expect("clear failed"); + buffer.resize(size); + buffer.reset(); + } + let mut output = TuiOut { buffer, area: XYWH(0, 0, width, height) }; + state.draw(&mut output); + buffer = engine.write().unwrap().flip(output.buffer, size); + } + let t1 = (*engine.read().unwrap()).perf.get_t1(t0).unwrap(); + buffer.set_string(0, 0, &format!("{:>3}.{:>3}ms", t1.as_millis(), t1.as_micros() % 1000), Style::default()); + std::thread::sleep(timer); + }) +} + +/// Spawn the input thread. +pub fn tui_input + Send + Sync + 'static> ( + engine: &Arc>, state: &Arc>, timer: Duration +) -> JoinHandle<()> { + let exited = engine.read().unwrap().exited.clone(); + let state = state.clone(); + spawn(move || loop { + if exited.fetch_and(true, Relaxed) { + break + } + if poll(timer).is_ok() { + let event = read().unwrap(); + match event { + Event::Key(KeyEvent { + code: KeyCode::Char('c'), + modifiers: KeyModifiers::CONTROL, + kind: KeyEventKind::Press, + state: KeyEventState::NONE + }) => { + exited.store(true, Relaxed); + }, + _ => { + let exited = exited.clone(); + let event = TuiEvent::from_crossterm(event); + if let Err(e) = state.write().unwrap().handle(&TuiIn { exited, event }) { + panic!("{e}") + } + } + } + } + }) +} + +#[cfg(feature = "dsl")] pub fn evaluate_output_expression_tui <'a, S> ( state: &S, output: &mut TuiOut, expr: impl Expression + 'a ) -> Usually where S: View @@ -296,6 +443,47 @@ border! { } } +pub fn okhsl_to_rgb (color: Okhsl) -> Color { + let Srgb { red, green, blue, .. }: Srgb = 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 { + 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") + } +} + +/// Trim string with [unicode_width]. +pub fn trim_string (max_width: usize, input: impl AsRef) -> String { + let input = input.as_ref(); + let mut output = Vec::with_capacity(input.len()); + let mut width: usize = 1; + let mut chars = input.chars(); + while let Some(c) = chars.next() { + if width > max_width { + break + } + output.push(c); + width += c.width().unwrap_or(0); + } + return output.into_iter().collect() +} + +pub(crate) fn width_chars_max (max: u16, text: impl AsRef) -> u16 { + let mut width: u16 = 0; + let mut chars = text.as_ref().chars(); + while let Some(c) = chars.next() { + width += c.width().unwrap_or(0) as u16; + if width > max { + break + } + } + return width +} + #[cfg(test)] mod tui_test { use crate::*; #[test] fn test_tui_engine () -> Usually<()> { diff --git a/tui/src/tui_impls.rs b/tui/src/tui_impls.rs index a87df69..e29529a 100644 --- a/tui/src/tui_impls.rs +++ b/tui/src/tui_impls.rs @@ -1,130 +1,46 @@ use crate::*; -use ratatui::style::Stylize; -use ratatui::{prelude::Position, style::Color::*}; -use crate::ratatui::prelude::Position; use unicode_width::{UnicodeWidthStr, UnicodeWidthChar}; -use ratatui::prelude::Position; use rand::{thread_rng, distributions::uniform::UniformSampler}; 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 g (g: u8) -> Color { Color::Rgb(g, g, g) } - 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) } - //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) } - /// Construct a new TUI engine and wrap it for shared ownership. - pub fn new () -> Usually>> { - let backend = CrosstermBackend::new(stdout()); - let Size { width, height } = backend.size()?; - Ok(Arc::new(RwLock::new(Self { - exited: Arc::new(AtomicBool::new(false)), - buffer: Buffer::empty(Rect { x: 0, y: 0, width, height }), - area: [0, 0, width, height], - backend, - perf: Default::default(), - }))) - } + /// Create and launch a terminal user interface. + pub fn run (state: &Arc>) -> Usually>> { tui_run(state) } /// True if done - pub fn exited (&self) -> bool { - self.exited.fetch_and(true, Relaxed) - } + pub fn exited (&self) -> bool { self.exited.fetch_and(true, Relaxed) } /// Prepare before run - pub fn setup (&mut self) -> Usually<()> { - let better_panic_handler = Settings::auto().verbosity(Verbosity::Full).create_panic_handler(); - std::panic::set_hook(Box::new(move |info: &std::panic::PanicHookInfo|{ - stdout().execute(LeaveAlternateScreen).unwrap(); - CrosstermBackend::new(stdout()).show_cursor().unwrap(); - disable_raw_mode().unwrap(); - better_panic_handler(info); - })); - stdout().execute(EnterAlternateScreen)?; - self.backend.hide_cursor()?; - enable_raw_mode().map_err(Into::into) - } - /// Update the display buffer. - pub fn flip (&mut self, mut buffer: Buffer, size: ratatui::prelude::Rect) -> Buffer { - if self.buffer.area != size { - self.backend.clear_region(ClearType::All).unwrap(); - self.buffer.resize(size); - self.buffer.reset(); - } - let updates = self.buffer.diff(&buffer); - self.backend.draw(updates.into_iter()).expect("failed to render"); - self.backend.flush().expect("failed to flush output buffer"); - std::mem::swap(&mut self.buffer, &mut buffer); - buffer.reset(); + pub fn setup (&mut self) -> Usually<()> { tui_setup(&mut self.backend) } + /// Clean up after run + pub fn teardown (&mut self) -> Usually<()> { tui_teardown(&mut self.backend) } + /// Apply changes to the display buffer. + pub fn flip (&mut self, mut new_buffer: Buffer, size: ratatui::prelude::Rect) -> Buffer { + let Self { buffer, backend, .. } = self; + tui_resized(&mut backend, &mut buffer, size); + tui_redrawn(&mut backend, &mut buffer, &mut new_buffer); buffer } - /// Clean up after run - pub fn teardown (&mut self) -> Usually<()> { - stdout().execute(LeaveAlternateScreen)?; - self.backend.show_cursor()?; - disable_raw_mode().map_err(Into::into) - } } - -impl TuiRun for Arc> { - fn run (&self, state: &Arc>) -> Usually<()> { - let _input_thread = TuiIn::run_input(self, state, Duration::from_millis(100)); - self.write().unwrap().setup()?; - let render_thread = TuiOut::run_output(self, state, Duration::from_millis(10))?; - match render_thread.join() { - Ok(result) => { - self.write().unwrap().teardown()?; - println!("\n\rRan successfully: {result:?}\n\r"); - }, - Err(error) => { - self.write().unwrap().teardown()?; - panic!("\n\rDraw thread failed: error={error:?}.\n\r") - }, - } - Ok(()) - } +impl Input for TuiIn { + type Event = TuiEvent; + type Handled = bool; + fn event (&self) -> &TuiEvent { &self.event } + fn done (&self) { self.exited.store(true, Relaxed); } + fn is_done (&self) -> bool { self.exited.fetch_and(true, Relaxed) } } - 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 TuiEvent { - pub fn from_crossterm (event: Event) -> Self { - Self(event) - } - #[cfg(feature = "dsl")] - pub fn from_dsl (dsl: impl Language) -> Perhaps { + 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 TuiKey { const SPLIT: char = '/'; - #[cfg(feature = "dsl")] - pub fn from_dsl (dsl: impl Language) -> Usually { + #[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" { @@ -169,25 +85,82 @@ impl TuiKey { })) } } - -pub fn buffer_update (buf: &mut Buffer, area: XY, 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); - } +impl Out for TuiOut { + type Unit = u16; + #[inline] fn area (&self) -> XYWH { self.area } + #[inline] fn area_mut (&mut self) -> &mut XYWH { &mut self.area } + #[inline] fn place_at <'t, T: Draw + ?Sized> (&mut self, area: XYWH, content: &'t T) { + let last = self.area(); + *self.area_mut() = area; + content.draw(self); + *self.area_mut() = last; + } +} +impl TuiOut { + #[inline] pub fn with_rect (&mut self, area: XY) -> &mut Self { self.area = area; self } + pub fn update (&mut self, area: XY, callback: &impl Fn(&mut Cell, u16, u16)) { tui_update(&mut self.buffer, area, callback); } + pub fn fill_char (&mut self, area: XY, c: char) { self.update(area, &|cell,_,_|{cell.set_char(c);}) } + pub fn fill_bg (&mut self, area: XY, color: Color) { self.update(area, &|cell,_,_|{cell.set_bg(color);}) } + pub fn fill_fg (&mut self, area: XY, color: Color) { self.update(area, &|cell,_,_|{cell.set_fg(color);}) } + pub fn fill_mod (&mut self, area: XY, on: bool, modifier: Modifier) { + if on { + self.update(area, &|cell,_,_|cell.modifier.insert(modifier)) + } else { + self.update(area, &|cell,_,_|cell.modifier.remove(modifier)) + } + } + pub fn fill_bold (&mut self, area: XY, on: bool) { self.fill_mod(area, on, Modifier::BOLD) } + pub fn fill_reversed (&mut self, area: XY, on: bool) { self.fill_mod(area, on, Modifier::REVERSED) } + pub fn fill_crossed_out (&mut self, area: XY, on: bool) { self.fill_mod(area, on, Modifier::CROSSED_OUT) } + pub fn fill_ul (&mut self, area: XY, color: Option) { + 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); + }) + } + } + pub 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; + } + } + pub fn blit (&mut self, text: &impl AsRef, x: u16, y: u16, style: Option