refactor(output,tui): fix errors, now there's more...
Some checks failed
/ build (push) Has been cancelled

This commit is contained in:
same mf who else 2026-02-17 04:09:10 +02:00
parent 4fa5d74fa2
commit 2dc74657d5
9 changed files with 713 additions and 754 deletions

View file

@ -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" ] }

View file

@ -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:

View file

@ -41,68 +41,68 @@ impl<O: Out> HasWH<O::Unit> for O {
}
impl<N: Coord> WH<N> {
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<N: Coord> XYWH<N> {
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<N> {
pub fn with_w (&self, w: N) -> XYWH<N> {
Self(self.x(), self.y(), w, self.h())
}
fn with_h (&self, h: N) -> XYWH<N> {
pub fn with_h (&self, h: N) -> XYWH<N> {
Self(self.x(), self.y(), self.w(), h)
}
fn lrtb (&self) -> XYWH<N> {
pub fn lrtb (&self) -> XYWH<N> {
Self(self.x(), self.x2(), self.y(), self.y2())
}
fn clipped_w (&self, w: N) -> XYWH<N> {
pub fn clipped_w (&self, w: N) -> XYWH<N> {
Self(self.x(), self.y(), self.w().min(w), self.h())
}
fn clipped_h (&self, h: N) -> XYWH<N> {
pub fn clipped_h (&self, h: N) -> XYWH<N> {
Self(self.x(), self.y(), self.w(), self.h().min(h))
}
fn clipped (&self, wh: WH<N>) -> XYWH<N> {
pub fn clipped (&self, wh: WH<N>) -> XYWH<N> {
Self(self.x(), self.y(), wh.w(), wh.h())
}
/// Iterate over every covered X coordinate.
fn iter_x (&self) -> impl Iterator<Item = N> where N: std::iter::Step {
pub fn iter_x (&self) -> impl Iterator<Item = N> 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<Item = N> where N: std::iter::Step {
pub fn iter_y (&self) -> impl Iterator<Item = N> where N: std::iter::Step {
let Self(_, y, _, h) = *self;
y..(y+h)
}
fn center (&self) -> XY<N> {
pub fn center (&self) -> XY<N> {
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<N> {
pub fn centered (&self) -> XY<N> {
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<N> {
pub fn centered_x (&self, n: N) -> XYWH<N> {
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<N> {
pub fn centered_y (&self, n: N) -> XYWH<N> {
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<N> {
pub fn centered_xy (&self, [n, m]: [N;2]) -> XYWH<N> {
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<C, T, U> Field<C, T, U> {
Field::<C, T, V> { 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<u64> {
if self.enabled {
Some(self.clock.raw())
} else {
None
}
}
pub fn get_t1 (&self, t0: Option<u64>) -> Option<std::time::Duration> {
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<u64>, 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<f64> {
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
}
}
}

View file

@ -256,3 +256,15 @@ pub struct Field<C, T, U> {
pub value_bg: Option<C>,
pub value_align: Option<Direction>,
}
/// 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,
}

View file

@ -155,3 +155,7 @@ pub trait Measured<O: Out> {
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;
}

View file

@ -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 <T>(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 {
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<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() {
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 <T: Draw<TuiOut> + Send + Sync + 'static> (
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();
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 <T: Handle<TuiIn> + Send + Sync + 'static> (
engine: &Arc<RwLock<Tui>>, state: &Arc<RwLock<T>>, 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<bool> where
S: View<TuiOut, ()>
@ -296,6 +443,47 @@ border! {
}
}
pub fn okhsl_to_rgb (color: Okhsl<f32>) -> Color {
let Srgb { red, green, blue, .. }: Srgb<f32> = 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<f32> {
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<str>) -> 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<str>) -> 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<()> {

File diff suppressed because it is too large Load diff

View file

@ -13,34 +13,29 @@ pub struct Tui {
pub perf: PerfModel,
}
#[derive(Default)] pub struct BigBuffer {
pub width: usize,
pub height: usize,
pub content: Vec<Cell>
#[derive(Debug, Clone)] pub struct TuiIn {
/// Input event
pub event: TuiEvent,
/// Exit flag
pub exited: Arc<AtomicBool>,
}
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)] pub struct TuiEvent(pub Event);
pub struct TuiKey(Option<KeyCode>, KeyModifiers);
#[derive(Debug, Clone)]
pub struct TuiIn {
/// Exit flag
pub exited: Arc<AtomicBool>,
/// Input event
pub event: TuiEvent,
#[derive(Default)] pub struct TuiOut {
pub buffer: Buffer,
pub area: XYWH<u16>,
}
/// 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,
/// TUI buffer sized by `usize` instead of `u16`.
#[derive(Default)] pub struct BigBuffer {
pub width: usize,
pub height: usize,
pub content: Vec<Cell>
}
/// A color in OKHSL and RGB representations.
#[derive(Debug, Default, Copy, Clone, PartialEq)] pub struct ItemColor {
pub okhsl: Okhsl<f32>,
@ -57,29 +52,10 @@ pub struct PerfModel {
pub darkest: ItemColor,
}
/// A cell that takes up 3 rows on its own,
/// but stacks, giving (N+1)*2 rows per N cells.
pub struct Phat<T> {
pub width: u16,
pub height: u16,
pub content: T,
pub colors: [Color;4],
}
pub struct Repeat<'a>(pub &'a str);
pub struct RepeatV<'a>(pub &'a str);
pub struct RepeatH<'a>(pub &'a str);
pub struct Modify<T>(pub bool, pub Modifier, pub T);
pub struct ScrollbarV {
pub offset: usize,
pub length: usize,
pub total: usize,
}
pub struct Styled<T>(pub Option<Style>, pub T);
pub struct ScrollbarH {
pub offset: usize,
pub length: usize,
pub total: usize,
}
/// Displays an owned [str]-like with fixed maximum width.
///
/// Width is computed using [unicode_width].
@ -90,35 +66,32 @@ pub struct TrimString<T: AsRef<str>>(pub u16, pub T);
/// Width is computed using [unicode_width].
pub struct TrimStringRef<'a, T: AsRef<str>>(pub u16, pub &'a T);
// Thunks can be natural error boundaries!
/// Thunks can be natural error boundaries!
pub struct ErrorBoundary<O: Out, T: Draw<O>>(
std::marker::PhantomData<O>, Perhaps<T>
std::marker::PhantomData<O>,
Perhaps<T>
);
pub struct Modify<T>(pub bool, pub Modifier, pub T);
pub struct Styled<T>(pub Option<Style>, pub T);
#[derive(Default)]
pub struct TuiOut {
pub buffer: Buffer,
pub area: XYWH<u16>,
/// Repeat a string, e.g. for background
pub enum Repeat<'a> {
X(&'a str),
Y(&'a str),
XY(&'a str)
}
impl Out for TuiOut {
type Unit = u16;
#[inline] fn area (&self) -> XYWH<u16> {
self.area
}
#[inline] fn area_mut (&mut self) -> &mut XYWH<u16> {
&mut self.area
}
#[inline] fn place_at <'t, T: Draw<Self> + ?Sized> (
&mut self, area: XYWH<u16>, content: &'t T
) {
let last = self.area();
*self.area_mut() = area;
content.draw(self);
*self.area_mut() = last;
}
/// Scroll indicator
pub enum Scrollbar {
/// Horizontal scrollbar
X { offset: usize, length: usize, total: usize, },
/// Vertical scrollbar
Y { offset: usize, length: usize, total: usize, }
}
/// A cell that takes up 3 rows on its own,
/// but stacks, giving (N+1)*2 rows per N cells.
pub struct Phat<T> {
pub width: u16,
pub height: u16,
pub content: T,
pub colors: [Color;4],
}

View file

@ -10,11 +10,6 @@ pub trait TuiHandle = Handle<TuiIn>;
pub trait TuiWidget = TuiDraw + TuiHandle;
pub trait TuiRun<T: TuiWidget + 'static> {
/// Run an app in the main loop.
fn run (&self, state: &Arc<RwLock<T>>) -> Usually<()>;
}
pub trait HasColor { fn color (&self) -> ItemColor; }
pub trait BorderStyle: Content<TuiOut> + Copy {