wip: refactor(tui): 1 error left

how do i flip buffer
This commit is contained in:
same mf who else 2026-02-20 00:28:56 +02:00
parent 3bdef228e8
commit 8ca90b9aa0
5 changed files with 86 additions and 58 deletions

View file

@ -64,8 +64,8 @@ impl<N: Coord> XYWH<N> {
pub fn with_h (&self, h: N) -> XYWH<N> {
Self(self.x(), self.y(), self.w(), h)
}
pub fn lrtb (&self) -> XYWH<N> {
Self(self.x(), self.x2(), self.y(), self.y2())
pub fn lrtb (&self) -> [N;4] {
[self.x(), self.x2(), self.y(), self.y2()]
}
pub fn clipped_w (&self, w: N) -> XYWH<N> {
Self(self.x(), self.y(), self.w().min(w), self.h())

View file

@ -1,7 +1,6 @@
#![feature(type_changing_struct_update, trait_alias)]
use std::{time::Duration, thread::{spawn, JoinHandle}};
use std::{time::Duration, thread::{spawn, JoinHandle}, io::Write};
use unicode_width::*;
pub use ::{
@ -86,7 +85,9 @@ mod tui_traits; pub use self::tui_traits::*;
mod tui_impls; pub use self::tui_impls::*;
/// Run an app in the main loop.
pub fn tui_run <T>(state: &Arc<RwLock<T>>) -> Usually<Arc<RwLock<Tui>>> {
pub fn tui_run <T: Send + Sync + Draw<TuiOut> + Handle<TuiIn> + 'static> (
state: &Arc<RwLock<T>>
) -> Usually<Arc<RwLock<Tui>>> {
let backend = CrosstermBackend::new(stdout());
let Size { width, height } = backend.size()?;
let tui = Arc::new(RwLock::new(Tui {
@ -96,9 +97,9 @@ pub fn tui_run <T>(state: &Arc<RwLock<T>>) -> Usually<Arc<RwLock<Tui>>> {
perf: Default::default(),
backend,
}));
let _input_thread = tui_input(tui, state, Duration::from_millis(100));
let _input_thread = tui_input(tui.clone(), state, Duration::from_millis(100));
tui.write().unwrap().setup()?;
let render_thread = tui_output(tui, state, Duration::from_millis(10))?;
let render_thread = tui_output(tui.clone(), state, Duration::from_millis(10))?;
match render_thread.join() {
Ok(result) => {
tui.write().unwrap().teardown()?;
@ -112,7 +113,9 @@ pub fn tui_run <T>(state: &Arc<RwLock<T>>) -> Usually<Arc<RwLock<Tui>>> {
Ok(tui)
}
pub fn tui_setup (backend: &mut CrosstermBackend) -> Usually<()> {
pub fn tui_setup <W: Write> (
backend: &mut CrosstermBackend<W>
) -> 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();
@ -125,14 +128,14 @@ pub fn tui_setup (backend: &mut CrosstermBackend) -> Usually<()> {
enable_raw_mode().map_err(Into::into)
}
pub fn tui_teardown (backend: &mut CrosstermBackend) -> Usually<()> {
pub fn tui_teardown <W: Write> (backend: &mut CrosstermBackend<W>) -> 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
pub fn tui_resized <W: Write> (
backend: &mut CrosstermBackend<W>, buffer: &mut Buffer, size: ratatui::prelude::Rect
) {
if buffer.area != size {
backend.clear_region(ClearType::All).unwrap();
@ -141,17 +144,21 @@ pub fn tui_resized (
}
}
pub fn tui_redrawn (
backend: &mut CrosstermBackend, buffer: &mut Buffer, new_buffer: &mut Buffer
pub fn tui_redrawn <'b, W: Write> (
backend: &mut CrosstermBackend<W>,
mut prev_buffer: &'b mut Buffer,
mut next_buffer: &'b mut Buffer
) {
let updates = buffer.diff(&new_buffer);
let updates = prev_buffer.diff(&next_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();
Backend::flush(backend).expect("failed to flush output new_buffer");
std::mem::swap(&mut prev_buffer, &mut next_buffer);
next_buffer.reset();
}
pub fn tui_update (buf: &mut Buffer, area: XY<u16>, callback: &impl Fn(&mut Cell, u16, u16)) {
pub fn tui_update (
buf: &mut Buffer, area: XYWH<u16>, callback: &impl Fn(&mut Cell, u16, u16)
) {
for row in 0..area.h() {
let y = area.y() + row;
for col in 0..area.w() {
@ -167,7 +174,7 @@ pub fn tui_update (buf: &mut Buffer, area: XY<u16>, callback: &impl Fn(&mut Cell
/// Spawn the output thread.
pub fn tui_output <T: Draw<TuiOut> + Send + Sync + 'static> (
engine: &Arc<RwLock<Tui>>, state: &Arc<RwLock<T>>, timer: Duration
engine: Arc<RwLock<Tui>>, state: &Arc<RwLock<T>>, timer: Duration
) -> Result<JoinHandle<()>, std::io::Error> {
let exited = engine.read().unwrap().exited.clone();
let engine = engine.clone();
@ -202,7 +209,7 @@ pub fn tui_output <T: Draw<TuiOut> + Send + Sync + 'static> (
/// Spawn the input thread.
pub fn tui_input <T: Handle<TuiIn> + Send + Sync + 'static> (
engine: &Arc<RwLock<Tui>>, state: &Arc<RwLock<T>>, timer: Duration
engine: Arc<RwLock<Tui>>, state: &Arc<RwLock<T>>, timer: Duration
) -> JoinHandle<()> {
let exited = engine.read().unwrap().exited.clone();
let state = state.clone();

View file

@ -4,7 +4,11 @@ use rand::{thread_rng, distributions::uniform::UniformSampler};
impl Tui {
/// Create and launch a terminal user interface.
pub fn run <T> (state: &Arc<RwLock<T>>) -> Usually<Arc<RwLock<Self>>> { tui_run(state) }
pub fn run <T: Send + Sync + Handle<TuiIn> + Draw<TuiOut> + 'static> (
state: &Arc<RwLock<T>>
) -> Usually<Arc<RwLock<Self>>> {
tui_run(state)
}
/// True if done
pub fn exited (&self) -> bool { self.exited.fetch_and(true, Relaxed) }
/// Prepare before run
@ -97,22 +101,22 @@ impl Out for TuiOut {
}
}
impl TuiOut {
#[inline] pub fn with_rect (&mut self, area: XY<u16>) -> &mut Self { self.area = area; self }
pub fn update (&mut self, area: XY<u16>, callback: &impl Fn(&mut Cell, u16, u16)) { tui_update(&mut self.buffer, area, callback); }
pub fn fill_char (&mut self, area: XY<u16>, c: char) { self.update(area, &|cell,_,_|{cell.set_char(c);}) }
pub fn fill_bg (&mut self, area: XY<u16>, color: Color) { self.update(area, &|cell,_,_|{cell.set_bg(color);}) }
pub fn fill_fg (&mut self, area: XY<u16>, color: Color) { self.update(area, &|cell,_,_|{cell.set_fg(color);}) }
pub fn fill_mod (&mut self, area: XY<u16>, on: bool, modifier: Modifier) {
#[inline] pub fn with_rect (&mut self, area: XYWH<u16>) -> &mut Self { self.area = area; self }
pub fn update (&mut self, area: XYWH<u16>, callback: &impl Fn(&mut Cell, u16, u16)) { tui_update(&mut self.buffer, area, callback); }
pub fn fill_char (&mut self, area: XYWH<u16>, c: char) { self.update(area, &|cell,_,_|{cell.set_char(c);}) }
pub fn fill_bg (&mut self, area: XYWH<u16>, color: Color) { self.update(area, &|cell,_,_|{cell.set_bg(color);}) }
pub fn fill_fg (&mut self, area: XYWH<u16>, color: Color) { self.update(area, &|cell,_,_|{cell.set_fg(color);}) }
pub fn fill_mod (&mut self, area: XYWH<u16>, 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<u16>, on: bool) { self.fill_mod(area, on, Modifier::BOLD) }
pub fn fill_reversed (&mut self, area: XY<u16>, on: bool) { self.fill_mod(area, on, Modifier::REVERSED) }
pub fn fill_crossed_out (&mut self, area: XY<u16>, on: bool) { self.fill_mod(area, on, Modifier::CROSSED_OUT) }
pub fn fill_ul (&mut self, area: XY<u16>, color: Option<Color>) {
pub fn fill_bold (&mut self, area: XYWH<u16>, on: bool) { self.fill_mod(area, on, Modifier::BOLD) }
pub fn fill_reversed (&mut self, area: XYWH<u16>, on: bool) { self.fill_mod(area, on, Modifier::REVERSED) }
pub fn fill_crossed_out (&mut self, area: XYWH<u16>, on: bool) { self.fill_mod(area, on, Modifier::CROSSED_OUT) }
pub fn fill_ul (&mut self, area: XYWH<u16>, color: Option<Color>) {
if let Some(color) = color {
self.update(area, &|cell,_,_|{
cell.modifier.insert(ratatui::prelude::Modifier::UNDERLINED);
@ -282,11 +286,11 @@ impl<T> Phat<T> {
pub const HI: &'static str = "";
/// A phat line
pub fn lo (fg: Color, bg: Color) -> impl Content<TuiOut> {
Fixed::Y(1, Tui::fg_bg(fg, bg, RepeatH(Self::LO)))
Fixed::Y(1, Tui::fg_bg(fg, bg, Repeat::X(Self::LO)))
}
/// A phat line
pub fn hi (fg: Color, bg: Color) -> impl Content<TuiOut> {
Fixed::Y(1, Tui::fg_bg(fg, bg, RepeatH(Self::HI)))
Fixed::Y(1, Tui::fg_bg(fg, bg, Repeat::X(Self::HI)))
}
}
impl Scrollbar {
@ -350,14 +354,30 @@ mod layout {
impl<T: Content<TuiOut>> Layout<TuiOut> for Modify<T> {}
impl<T: Content<TuiOut>> Layout<TuiOut> for Styled<T> {}
impl Layout<TuiOut> for Repeat<'_> {}
impl Layout<TuiOut> for &str { fn layout (&self, to: XY<u16>) -> XY<u16> { to.center_xy([width_chars_max(to.w(), self), 1]) } }
impl Layout<TuiOut> for String { fn layout (&self, to: XY<u16>) -> XY<u16> { self.as_str().layout(to) } }
impl Layout<TuiOut> for Arc<str> { fn layout (&self, to: XY<u16>) -> XY<u16> { self.as_ref().layout(to) } }
impl Layout<TuiOut> for &str {
fn layout (&self, to: XYWH<u16>) -> XYWH<u16> {
to.centered_xy([width_chars_max(to.w(), self), 1])
}
}
impl Layout<TuiOut> for String {
fn layout (&self, to: XYWH<u16>) -> XYWH<u16> {
self.as_str().layout(to)
}
}
impl Layout<TuiOut> for Arc<str> {
fn layout (&self, to: XYWH<u16>) -> XYWH<u16> {
self.as_ref().layout(to)
}
}
impl<'a, T: AsRef<str>> Layout<TuiOut> for TrimString<T> {
fn layout (&self, to: XY<u16>) -> XY<u16> { Layout::layout(&self.as_ref(), to) }
fn layout (&self, to: XYWH<u16>) -> XYWH<u16> {
Layout::layout(&self.as_ref(), to)
}
}
impl<'a, T: AsRef<str>> Layout<TuiOut> for TrimStringRef<'a, T> {
fn layout (&self, to: XY<u16>) -> XY<u16> { [to.x(), to.y(), to.w().min(self.0).min(self.1.as_ref().width() as u16), to.h()] }
fn layout (&self, to: XYWH<u16>) -> XYWH<u16> {
XYWH(to.x(), to.y(), to.w().min(self.0).min(self.1.as_ref().width() as u16), to.h())
}
}
}
@ -394,9 +414,9 @@ mod draw {
impl Draw<TuiOut> for Repeat<'_> {
fn draw (&self, to: &mut TuiOut) {
let XYWH(x, y, w, h) = to.area();
match self {
Self::X(c) => {
let [x, y, w, _h] = to.area().xywh();
for x in x..x+w {
if let Some(cell) = to.buffer.cell_mut(Position::from((x, y))) {
cell.set_symbol(&c);
@ -404,21 +424,19 @@ mod draw {
}
},
Self::Y(c) => {
let [x, y, _w, h] = to.area().xywh();
for y in y..y+h {
if let Some(cell) = to.buffer.cell_mut(Position::from((x, y))) {
cell.set_symbol(&c);
}
}
},
Self::XY => {
let [x, y, w, h] = to.area().xywh();
let a = self.0.len();
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) = to.buffer.cell_mut(Position::from((x, y))) {
let u = u % a;
cell.set_symbol(&self.0[u..u+1]);
cell.set_symbol(&c[u..u+1]);
}
}
}
@ -429,7 +447,7 @@ mod draw {
impl Draw<TuiOut> for Scrollbar {
fn draw (&self, to: &mut TuiOut) {
let [x1, y1, w, h] = to.area().xywh();
let XYWH(x1, y1, w, h) = to.area();
match self {
Self::X { .. } => {
let x2 = x1 + w;
@ -439,10 +457,10 @@ mod draw {
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.len()) {
} 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[w as usize - i]);
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);
@ -462,11 +480,11 @@ mod draw {
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.len()) {
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[h as usize - i]);
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);
@ -485,7 +503,7 @@ mod draw {
impl Draw<TuiOut> for &str {
fn draw (&self, to: &mut TuiOut) {
let [x, y, w, ..] = self.layout(to.area());
let XYWH(x, y, w, ..) = self.layout(to.area());
to.text(&self, x, y, w)
}
}

View file

@ -22,7 +22,10 @@ pub struct Tui {
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)] pub struct TuiEvent(pub Event);
pub struct TuiKey(Option<KeyCode>, KeyModifiers);
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)] pub struct TuiKey(
pub Option<KeyCode>,
pub KeyModifiers
);
#[derive(Default)] pub struct TuiOut {
pub buffer: Buffer,
@ -68,8 +71,8 @@ pub struct TrimStringRef<'a, T: AsRef<str>>(pub u16, pub &'a T);
/// Thunks can be natural error boundaries!
pub struct ErrorBoundary<O: Out, T: Draw<O>>(
std::marker::PhantomData<O>,
Perhaps<T>
pub std::marker::PhantomData<O>,
pub Perhaps<T>
);
/// Repeat a string, e.g. for background

View file

@ -58,7 +58,7 @@ pub trait BorderStyle: Content<TuiOut> + Copy {
}
#[inline] fn draw_horizontal (
&self, to: &mut TuiOut, style: Option<Style>
) -> Usually<[u16;4]> {
) -> Usually<XYWH<u16>> {
let area = to.area();
let style = style.or_else(||self.style_horizontal());
let [x, x2, y, y2] = area.lrtb();
@ -70,7 +70,7 @@ pub trait BorderStyle: Content<TuiOut> + Copy {
}
#[inline] fn draw_vertical (
&self, to: &mut TuiOut, style: Option<Style>
) -> Usually<[u16;4]> {
) -> Usually<XYWH<u16>> {
let area = to.area();
let style = style.or_else(||self.style_vertical());
let [x, x2, y, y2] = area.lrtb();
@ -88,10 +88,10 @@ pub trait BorderStyle: Content<TuiOut> + Copy {
}
#[inline] fn draw_corners (
&self, to: &mut TuiOut, style: Option<Style>
) -> Usually<[u16;4]> {
) -> Usually<XYWH<u16>> {
let area = to.area();
let style = style.or_else(||self.style_corners());
let [x, y, width, height] = area.xywh();
let XYWH(x, y, width, height) = area;
if width > 1 && height > 1 {
to.blit(&Self::NW, x, y, style);
to.blit(&Self::NE, x + width - 1, y, style);