tabula rasa

This commit is contained in:
🪞👃🪞 2025-03-02 14:31:04 +02:00
commit 47b3413d7d
76 changed files with 7000 additions and 0 deletions

19
tui/Cargo.toml Normal file
View file

@ -0,0 +1,19 @@
[package]
name = "tek_tui"
edition = "2021"
version = "0.2.0"
[dependencies]
palette = { version = "0.7.6", features = [ "random" ] }
rand = "0.8.5"
crossterm = "0.28.1"
ratatui = { version = "0.29.0", features = [ "unstable-widget-ref", "underline-color" ] }
better-panic = "0.3.0"
konst = { version = "0.3.16", features = [ "rust_1_83" ] }
atomic_float = "1"
quanta = "0.12.3"
tek_edn = { path = "../edn" }
tek_input = { path = "../input" }
tek_output = { path = "../output" }
#tek_time = { path = "../time" }

15
tui/README.md Normal file
View file

@ -0,0 +1,15 @@
# `tek_tui`
the `Tui` struct (the *engine*) implements the
`tek_input::Input` and `tek_output::Output` traits.
at launch, the `Tui` engine spawns two threads,
a **render thread** and an **input thread**. (the
application may spawn further threads, such as a
**jack thread**.)
all threads communicate using shared ownership,
`Arc<RwLock>` and `Arc<Atomic>`. the engine and
application instances are expected to be wrapped
in `Arc<RwLock>`; internally, those synchronization
mechanisms may be used liberally.

144
tui/examples/demo.rs.old Normal file
View file

@ -0,0 +1,144 @@
use tek::*;
fn main () -> Usually<()> {
Tui::run(Arc::new(RwLock::new(Demo::new())))?;
Ok(())
}
pub struct Demo<E: Engine> {
index: usize,
items: Vec<Box<dyn Render<Engine = E>>>
}
impl Demo<Tui> {
fn new () -> Self {
Self {
index: 0,
items: vec![
//Box::new(tek_sequencer::TransportPlayPauseButton {
//_engine: Default::default(),
//transport: None,
//value: Some(TransportState::Stopped),
//focused: true
//}),
//Box::new(tek_sequencer::TransportPlayPauseButton {
//_engine: Default::default(),
//transport: None,
//value: Some(TransportState::Rolling),
//focused: false
//}),
]
}
}
}
impl Content for Demo<Tui> {
type Engine = Tui;
fn content (&self) -> dyn Render<Engine = Tui> {
let border_style = Style::default().fg(Color::Rgb(0,0,0));
Align::Center(Layers::new(move|add|{
add(&Background(Color::Rgb(0,128,128)))?;
add(&Margin::XY(1, 1, Stack::down(|add|{
add(&Layers::new(|add|{
add(&Background(Color::Rgb(128,96,0)))?;
add(&Border(Square(border_style)))?;
add(&Margin::XY(2, 1, "..."))?;
Ok(())
}).debug())?;
add(&Layers::new(|add|{
add(&Background(Color::Rgb(128,64,0)))?;
add(&Border(Lozenge(border_style)))?;
add(&Margin::XY(4, 2, "---"))?;
Ok(())
}).debug())?;
add(&Layers::new(|add|{
add(&Background(Color::Rgb(96,64,0)))?;
add(&Border(SquareBold(border_style)))?;
add(&Margin::XY(6, 3, "~~~"))?;
Ok(())
}).debug())?;
Ok(())
})).debug())?;
Ok(())
}))
//Align::Center(Margin::X(1, Layers::new(|add|{
//add(&Background(Color::Rgb(128,0,0)))?;
//add(&Stack::down(|add|{
//add(&Margin::Y(1, Layers::new(|add|{
//add(&Background(Color::Rgb(0,128,0)))?;
//add(&Align::Center("12345"))?;
//add(&Align::Center("FOO"))
//})))?;
//add(&Margin::XY(1, 1, Layers::new(|add|{
//add(&Align::Center("1234567"))?;
//add(&Align::Center("BAR"))?;
//add(&Background(Color::Rgb(0,0,128)))
//})))
//}))
//})))
//Align::Y(Layers::new(|add|{
//add(&Background(Color::Rgb(128,0,0)))?;
//add(&Margin::X(1, Align::Center(Stack::down(|add|{
//add(&Align::X(Margin::Y(1, Layers::new(|add|{
//add(&Background(Color::Rgb(0,128,0)))?;
//add(&Align::Center("12345"))?;
//add(&Align::Center("FOO"))
//})))?;
//add(&Margin::XY(1, 1, Layers::new(|add|{
//add(&Align::Center("1234567"))?;
//add(&Align::Center("BAR"))?;
//add(&Background(Color::Rgb(0,0,128)))
//})))?;
//Ok(())
//})))))
//}))
}
}
impl Handle<TuiIn> for Demo<Tui> {
fn handle (&mut self, from: &TuiIn) -> Perhaps<bool> {
use KeyCode::{PageUp, PageDown};
match from.event() {
kexp!(PageUp) => {
self.index = (self.index + 1) % self.items.len();
},
kexp!(PageDown) => {
self.index = if self.index > 1 {
self.index - 1
} else {
self.items.len() - 1
};
},
_ => return Ok(None)
}
Ok(Some(true))
}
}
//lisp!(CONTENT Demo (LET
//(BORDER-STYLE (STYLE (FG (RGB 0 0 0))))
//(BG-COLOR-0 (RGB 0 128 128))
//(BG-COLOR-1 (RGB 128 96 0))
//(BG-COLOR-2 (RGB 128 64 0))
//(BG-COLOR-3 (RGB 96 64 0))
//(CENTER (LAYERS
//(BACKGROUND BG-COLOR-0)
//(OUTSET-XY 1 1 (SPLIT-DOWN
//(LAYERS (BACKGROUND BG-COLOR-1)
//(BORDER SQUARE BORDER-STYLE)
//(OUTSET-XY 2 1 "..."))
//(LAYERS (BACKGROUND BG-COLOR-2)
//(BORDER LOZENGE BORDER-STYLE)
//(OUTSET-XY 4 2 "---"))
//(LAYERS (BACKGROUND BG-COLOR-3)
//(BORDER SQUARE-BOLD BORDER-STYLE)
//(OUTSET-XY 2 1 "~~~"))))))))

1
tui/examples/edn01.edn Normal file
View file

@ -0,0 +1 @@
:hello-world

1
tui/examples/edn02.edn Normal file
View file

@ -0,0 +1 @@
(bsp/s :hello :world)

1
tui/examples/edn03.edn Normal file
View file

@ -0,0 +1 @@
(fill/xy :hello-world)

1
tui/examples/edn04.edn Normal file
View file

@ -0,0 +1 @@
(fixed/xy 20 10 :hello-world)

1
tui/examples/edn05.edn Normal file
View file

@ -0,0 +1 @@
(bsp/s (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world))

1
tui/examples/edn06.edn Normal file
View file

@ -0,0 +1 @@
(bsp/e (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world))

1
tui/examples/edn07.edn Normal file
View file

@ -0,0 +1 @@
(bsp/n (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world))

1
tui/examples/edn08.edn Normal file
View file

@ -0,0 +1 @@
(bsp/w (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world))

1
tui/examples/edn09.edn Normal file
View file

@ -0,0 +1 @@
(bsp/a (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world))

1
tui/examples/edn10.edn Normal file
View file

@ -0,0 +1 @@
(bsp/b (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world))

11
tui/examples/edn11.edn Normal file
View file

@ -0,0 +1,11 @@
(bsp/s
(bsp/e (align/nw (fixed/xy 5 3 :hello))
(bsp/e (align/n (fixed/xy 5 3 :hello))
(align/ne (fixed/xy 5 3 :hello))))
(bsp/s
(bsp/e (align/w (fixed/xy 5 3 :hello))
(bsp/e (align/c (fixed/xy 5 3 :hello))
(align/e (fixed/xy 5 3 :hello))))
(bsp/e (align/sw (fixed/xy 5 3 :hello))
(bsp/e (align/s (fixed/xy 5 3 :hello))
(align/se (fixed/xy 5 3 :hello))))))

11
tui/examples/edn12.edn Normal file
View file

@ -0,0 +1,11 @@
(bsp/s
(bsp/e (fixed/xy 8 5 (align/nw :hello))
(bsp/e (fixed/xy 8 5 (align/n :hello))
(fixed/xy 8 5 (align/ne :hello))))
(bsp/s
(bsp/e (fixed/xy 8 5 (align/w :hello))
(bsp/e (fixed/xy 8 5 (align/c :hello))
(fixed/xy 8 5 (align/e :hello))))
(bsp/e (fixed/xy 8 5 (align/sw :hello))
(bsp/e (fixed/xy 8 5 (align/s :hello))
(fixed/xy 8 5 (align/se :hello))))))

11
tui/examples/edn13.edn Normal file
View file

@ -0,0 +1,11 @@
(bsp/s
(bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/nw :hello)))
(bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/n :hello)))
(grow/xy 1 1 (fixed/xy 8 5 (align/ne :hello)))))
(bsp/s
(bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/w :hello)))
(bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/c :hello)))
(grow/xy 1 1 (fixed/xy 8 5 (align/e :hello)))))
(bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/sw :hello)))
(bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/s :hello)))
(grow/xy 1 1 (fixed/xy 8 5 (align/se :hello)))))))

73
tui/examples/edn99.edn Normal file
View file

@ -0,0 +1,73 @@
(align/c (bg/behind :bg0 (margin/xy 1 1 (col
(bg/behind :bg1 (border/around :border1 (margin/xy 2 1 :label1)))
(bg/behind :bg2 (border/around :border2 (margin/xy 4 2 :label2)))
(bg/behind :bg3 (border/around :border3 (margin/xy 6 3 :label3)))))))
fn content (&self) -> dyn Render<Engine = Tui> {
let border_style = Style::default().fg(Color::Rgb(0,0,0));
Align::Center(Layers::new(move|add|{
add(&Background(Color::Rgb(0,128,128)))?;
add(&Margin::XY(1, 1, Stack::down(|add|{
add(&Layers::new(|add|{
add(&Background(Color::Rgb(128,96,0)))?;
add(&Border(Square(border_style)))?;
add(&Margin::XY(2, 1, "..."))?;
Ok(())
}).debug())?;
add(&Layers::new(|add|{
add(&Background(Color::Rgb(128,64,0)))?;
add(&Border(Lozenge(border_style)))?;
add(&Margin::XY(4, 2, "---"))?;
Ok(())
}).debug())?;
add(&Layers::new(|add|{
add(&Background(Color::Rgb(96,64,0)))?;
add(&Border(SquareBold(border_style)))?;
add(&Margin::XY(6, 3, "~~~"))?;
Ok(())
}).debug())?;
Ok(())
})).debug())?;
Ok(())
}))
//Align::Center(Margin::X(1, Layers::new(|add|{
//add(&Background(Color::Rgb(128,0,0)))?;
//add(&Stack::down(|add|{
//add(&Margin::Y(1, Layers::new(|add|{
//add(&Background(Color::Rgb(0,128,0)))?;
//add(&Align::Center("12345"))?;
//add(&Align::Center("FOO"))
//})))?;
//add(&Margin::XY(1, 1, Layers::new(|add|{
//add(&Align::Center("1234567"))?;
//add(&Align::Center("BAR"))?;
//add(&Background(Color::Rgb(0,0,128)))
//})))
//}))
//})))
//Align::Y(Layers::new(|add|{
//add(&Background(Color::Rgb(128,0,0)))?;
//add(&Margin::X(1, Align::Center(Stack::down(|add|{
//add(&Align::X(Margin::Y(1, Layers::new(|add|{
//add(&Background(Color::Rgb(0,128,0)))?;
//add(&Align::Center("12345"))?;
//add(&Align::Center("FOO"))
//})))?;
//add(&Margin::XY(1, 1, Layers::new(|add|{
//add(&Align::Center("1234567"))?;
//add(&Align::Center("BAR"))?;
//add(&Background(Color::Rgb(0,0,128)))
//})))?;
//Ok(())
//})))))
//}))
}

66
tui/examples/tui.rs Normal file
View file

@ -0,0 +1,66 @@
use tek_tui::{*, tek_input::*, tek_output::*};
use tek_edn::*;
use std::sync::{Arc, RwLock};
use crossterm::event::{*, KeyCode::*};
use crate::ratatui::style::Color;
fn main () -> Usually<()> {
let state = Arc::new(RwLock::new(Example(10, Measure::new())));
Tui::new().unwrap().run(&state)?;
Ok(())
}
#[derive(Debug)] pub struct Example(usize, Measure<TuiOut>);
const KEYMAP: &str = "(:left prev) (:right next)";
handle!(TuiIn: |self: Example, input|{
let keymap = SourceIter::new(KEYMAP);
let command = keymap.command::<_, ExampleCommand, _>(self, input);
if let Some(command) = command {
command.execute(self)?;
return Ok(Some(true))
}
return Ok(None)
});
enum ExampleCommand { Next, Previous }
atom_command!(ExampleCommand: |app: Example| {
(":prev" [] Some(Self::Previous))
(":next" [] Some(Self::Next))
});
command!(|self: ExampleCommand, state: Example|match self {
Self::Next =>
{ state.0 = (state.0 + 1) % EXAMPLES.len(); None },
Self::Previous =>
{ state.0 = if state.0 > 0 { state.0 - 1 } else { EXAMPLES.len() - 1 }; None },
});
const EXAMPLES: &'static [&'static str] = &[
include_str!("edn01.edn"),
include_str!("edn02.edn"),
include_str!("edn03.edn"),
include_str!("edn04.edn"),
include_str!("edn05.edn"),
include_str!("edn06.edn"),
include_str!("edn07.edn"),
include_str!("edn08.edn"),
include_str!("edn09.edn"),
include_str!("edn10.edn"),
include_str!("edn11.edn"),
include_str!("edn12.edn"),
include_str!("edn13.edn"),
];
view!(TuiOut: |self: Example|{
let index = self.0 + 1;
let wh = self.1.wh();
let src = EXAMPLES[self.0];
let heading = format!("Example {}/{} in {:?}", index, EXAMPLES.len(), &wh);
let title = Tui::bg(Color::Rgb(60, 10, 10), Push::y(1, Align::n(heading)));
let code = Tui::bg(Color::Rgb(10, 60, 10), Push::y(2, Align::n(format!("{}", src))));
let content = Tui::bg(Color::Rgb(10, 10, 60), View(self, SourceIter::new(src)));
self.1.of(Bsp::s(title, Bsp::n(""/*code*/, content)))
}; {
":title" => Tui::bg(Color::Rgb(60, 10, 10), Push::y(1, Align::n(format!("Example {}/{}:", self.0 + 1, EXAMPLES.len())))).boxed(),
":code" => Tui::bg(Color::Rgb(10, 60, 10), Push::y(2, Align::n(format!("{}", EXAMPLES[self.0])))).boxed(),
":hello" => Tui::bg(Color::Rgb(10, 100, 10), "Hello").boxed(),
":world" => Tui::bg(Color::Rgb(100, 10, 10), "world").boxed(),
":hello-world" => "Hello world!".boxed()
});
provide_bool!(bool: |self: Example| {});
provide_num!(u16: |self: Example| {});
provide_num!(usize: |self: Example| {});

59
tui/src/lib.rs Normal file
View file

@ -0,0 +1,59 @@
mod tui_buffer; pub use self::tui_buffer::*;
mod tui_color; pub use self::tui_color::*;
mod tui_content; pub use self::tui_content::*;
mod tui_engine; pub use self::tui_engine::*;
mod tui_file; pub use self::tui_file::*;
mod tui_input; pub use self::tui_input::*;
mod tui_output; pub use self::tui_output::*;
mod tui_perf; pub use self::tui_perf::*;
pub use ::tek_edn;// pub(crate) use ::tek_edn::*;
//pub use ::tek_time; pub(crate) use ::tek_time::*;
pub use ::tek_input; pub(crate) use tek_input::*;
pub use ::tek_output; pub(crate) use tek_output::*;
pub use ::better_panic; pub(crate) use better_panic::{Settings, Verbosity};
pub use ::palette; pub(crate) use ::palette::{*, convert::*, okhsl::*};
pub use ::crossterm; pub(crate) use crossterm::{
ExecutableCommand,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode},
event::{Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState},
};
pub use ::ratatui; pub(crate) use ratatui::{
prelude::{Color, Style, Buffer},
style::Modifier,
backend::{Backend, CrosstermBackend, ClearType},
layout::{Size, Rect},
buffer::Cell
};
pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicBool, Ordering::*}};
pub(crate) use std::io::{stdout, Stdout};
pub(crate) use std::path::PathBuf;
pub(crate) use std::ffi::OsString;
pub(crate) use atomic_float::AtomicF64;
#[macro_export] macro_rules! from {
($(<$($lt:lifetime),+>)?|$state:ident:$Source:ty|$Target:ty=$cb:expr) => {
impl $(<$($lt),+>)? From<$Source> for $Target {
fn from ($state:$Source) -> Self { $cb }
}
};
}
#[cfg(test)] #[test] fn test_tui_engine () -> Usually<()> {
use crate::*;
use std::sync::{Arc, RwLock};
struct TestComponent(String);
impl Content<TuiOut> for TestComponent {
fn content (&self) -> impl Render<TuiOut> {
Some(self.0.as_str())
}
}
impl Handle<TuiIn> for TestComponent {
fn handle (&mut self, from: &TuiIn) -> Perhaps<bool> {
Ok(None)
}
}
let engine = Tui::new()?;
engine.read().unwrap().exited.store(true, std::sync::atomic::Ordering::Relaxed);
let state = TestComponent("hello world".into());
let state = std::sync::Arc::new(std::sync::RwLock::new(state));
//engine.run(&state)?;
Ok(())
}

39
tui/src/tui_buffer.rs Normal file
View file

@ -0,0 +1,39 @@
use crate::*;
pub fn buffer_update (buf: &mut Buffer, area: [u16;4], 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 {
callback(buf.get_mut(x, y), col, row);
}
}
}
}
#[derive(Default)] pub struct BigBuffer {
pub width: usize,
pub height: usize,
pub content: Vec<Cell>
}
impl std::fmt::Debug for BigBuffer {
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
write!(f, "[BB {}x{} ({})]", self.width, self.height, self.content.len())
}
}
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
}
}
from!(|size:(usize, usize)| BigBuffer = Self::new(size.0, size.1));

163
tui/src/tui_color.rs Normal file
View file

@ -0,0 +1,163 @@
use crate::*;
use rand::{thread_rng, distributions::uniform::UniformSampler};
impl Tui {
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) }
}
pub trait HasColor { fn color (&self) -> ItemColor; }
#[macro_export] macro_rules! has_color {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasColor for $Struct $(<$($L),*$($T),*>)? {
fn color (&$self) -> ItemColor { $cb }
}
}
}
/// A color in OKHSL and RGB representations.
#[derive(Debug, Default, Copy, Clone, PartialEq)] pub struct ItemColor {
pub okhsl: Okhsl<f32>,
pub rgb: Color,
}
from!(|okhsl: Okhsl<f32>|ItemColor = Self { okhsl, rgb: okhsl_to_rgb(okhsl) });
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,)
}
from!(|rgb: Color|ItemColor = Self { rgb, okhsl: rgb_to_okhsl(rgb) });
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")
}
}
// A single color within item theme parameters, in OKHSL and RGB representations.
impl ItemColor {
pub const fn from_rgb (rgb: Color) -> Self {
Self { rgb, okhsl: Okhsl::new_const(OklabHue::new(0.0), 0.0, 0.0) }
}
pub const fn from_okhsl (okhsl: Okhsl<f32>) -> Self {
Self { rgb: Color::Rgb(0, 0, 0), okhsl }
}
pub fn random () -> Self {
let mut rng = thread_rng();
let lo = Okhsl::new(-180.0, 0.01, 0.25);
let hi = Okhsl::new( 180.0, 0.9, 0.5);
UniformOkhsl::new(lo, hi).sample(&mut rng).into()
}
pub fn random_dark () -> Self {
let mut rng = thread_rng();
let lo = Okhsl::new(-180.0, 0.025, 0.075);
let hi = Okhsl::new( 180.0, 0.5, 0.150);
UniformOkhsl::new(lo, hi).sample(&mut rng).into()
}
pub fn random_near (color: Self, distance: f32) -> Self {
color.mix(Self::random(), distance)
}
pub fn mix (&self, other: Self, distance: f32) -> Self {
if distance > 1.0 { panic!("color mixing takes distance between 0.0 and 1.0"); }
self.okhsl.mix(other.okhsl, distance).into()
}
}
/// A color in OKHSL and RGB with lighter and darker variants.
#[derive(Debug, Default, Copy, Clone, PartialEq)] pub struct ItemPalette {
pub base: ItemColor,
pub light: ItemColor,
pub lighter: ItemColor,
pub lightest: ItemColor,
pub dark: ItemColor,
pub darker: ItemColor,
pub darkest: ItemColor,
}
impl ItemPalette {
pub const G: [Self;256] = {
let mut builder = konst::array::ArrayBuilder::new();
while !builder.is_full() {
let index = builder.len() as u8;
let light = (index as f64 * 1.3) as u8;
let lighter = (index as f64 * 1.6) as u8;
let lightest = (index as f64 * 1.9) as u8;
let dark = (index as f64 * 0.9) as u8;
let darker = (index as f64 * 0.6) as u8;
let darkest = (index as f64 * 0.3) as u8;
builder.push(ItemPalette {
base: ItemColor::from_rgb(Color::Rgb(index, index, index )),
light: ItemColor::from_rgb(Color::Rgb(light, light, light, )),
lighter: ItemColor::from_rgb(Color::Rgb(lighter, lighter, lighter, )),
lightest: ItemColor::from_rgb(Color::Rgb(lightest, lightest, lightest, )),
dark: ItemColor::from_rgb(Color::Rgb(dark, dark, dark, )),
darker: ItemColor::from_rgb(Color::Rgb(darker, darker, darker, )),
darkest: ItemColor::from_rgb(Color::Rgb(darkest, darkest, darkest, )),
});
}
builder.build()
};
pub fn random () -> Self { ItemColor::random().into() }
pub fn random_near (color: Self, distance: f32) -> Self {
color.base.mix(ItemColor::random(), distance).into()
}
pub const G00: Self = {
let color: ItemColor = ItemColor {
okhsl: Okhsl { hue: OklabHue::new(0.0), lightness: 0.0, saturation: 0.0 },
rgb: Color::Rgb(0, 0, 0)
};
Self {
base: color,
light: color,
lighter: color,
lightest: color,
dark: color,
darker: color,
darkest: color,
}
};
pub fn from_tui_color (base: Color) -> Self {
Self::from_item_color(ItemColor::from_rgb(base))
}
pub fn from_item_color (base: ItemColor) -> Self {
let mut light = base.okhsl;
light.lightness = (light.lightness * 1.3).min(1.0);
let mut lighter = light;
lighter.lightness = (lighter.lightness * 1.3).min(1.0);
let mut lightest = base.okhsl;
lightest.lightness = 0.95;
let mut dark = base.okhsl;
dark.lightness = (dark.lightness * 0.75).max(0.0);
dark.saturation = (dark.saturation * 0.75).max(0.0);
let mut darker = dark;
darker.lightness = (darker.lightness * 0.66).max(0.0);
darker.saturation = (darker.saturation * 0.66).max(0.0);
let mut darkest = darker;
darkest.lightness = 0.1;
darkest.saturation = (darkest.saturation * 0.50).max(0.0);
Self {
base,
light: light.into(),
lighter: lighter.into(),
lightest: lightest.into(),
dark: dark.into(),
darker: darker.into(),
darkest: darkest.into(),
}
}
}
from!(|base: Color| ItemPalette = Self::from_tui_color(base));
from!(|base: ItemColor|ItemPalette = Self::from_item_color(base));

522
tui/src/tui_content.rs Normal file
View file

@ -0,0 +1,522 @@
use crate::*;
use crate::Color::*;
use ratatui::prelude::Position;
macro_rules! impl_content_layout_render {
($Output:ty: |$self:ident: $Struct:ty, $to:ident| layout = $layout:expr; render = $render:expr) => {
impl Content<$Output> for $Struct {
fn layout (&$self, $to: [u16;4]) -> [u16;4] { $layout }
fn render (&$self, $to: &mut $Output) { $render }
}
}
}
impl_content_layout_render!(TuiOut: |self: &str, to|
layout = to.center_xy([self.chars().count() as u16, 1]);
render = {let [x, y, ..] = Content::layout(self, to.area());
to.blit(self, x, y, None)});
impl_content_layout_render!(TuiOut: |self: String, to|
layout = to.center_xy([self.chars().count() as u16, 1]);
render = {let [x, y, ..] = Content::layout(self, to.area());
to.blit(self, x, y, None)});
impl_content_layout_render!(TuiOut: |self: std::sync::RwLock<String>, to|
layout = Content::<TuiOut>::layout(&self.read().unwrap(), to);
render = Content::<TuiOut>::render(&self.read().unwrap(), to));
impl_content_layout_render!(TuiOut: |self: std::sync::RwLockReadGuard<'_, String>, to|
layout = Content::<TuiOut>::layout(&**self, to);
render = Content::<TuiOut>::render(&**self, to));
impl_content_layout_render!(TuiOut: |self: Arc<str>, to|
layout = to.center_xy([self.chars().count() as u16, 1]);
render = to.blit(self, to.area.x(), to.area.y(), None));
impl<T: Content<TuiOut>> Content<TuiOut> for std::sync::Arc<T> {
fn layout (&self, to: [u16;4]) -> [u16;4] {
Content::<TuiOut>::layout(&**self, to)
}
fn render (&self, to: &mut TuiOut) {
Content::<TuiOut>::render(&**self, to)
}
}
pub struct FieldH<T, U>(pub ItemPalette, pub T, pub U);
impl<T: Content<TuiOut>, U: Content<TuiOut>> Content<TuiOut> for FieldH<T, U> {
fn content (&self) -> impl Render<TuiOut> {
let Self(ItemPalette { darkest, dark, lighter, lightest, .. }, title, value) = self;
row!(
Tui::fg_bg(dark.rgb, darkest.rgb, ""),
Tui::fg_bg(lighter.rgb, dark.rgb, Tui::bold(true, title)),
Tui::fg_bg(dark.rgb, darkest.rgb, ""),
Tui::fg_bg(lightest.rgb, darkest.rgb, value),
)
}
}
pub struct FieldV<T, U>(pub ItemPalette, pub T, pub U);
impl<T: Content<TuiOut>, U: Content<TuiOut>> Content<TuiOut> for FieldV<T, U> {
fn content (&self) -> impl Render<TuiOut> {
let Self(ItemPalette { darkest, dark, lighter, lightest, .. }, title, value) = self;
let sep1 = Tui::bg(darkest.rgb, Tui::fg(dark.rgb, ""));
let sep2 = Tui::bg(darkest.rgb, Tui::fg(dark.rgb, ""));
let title = Tui::bg(dark.rgb, Tui::fg(lighter.rgb, Tui::bold(true, title)));
let value = Tui::bg(darkest.rgb, Tui::fg(lightest.rgb, value));
Bsp::e(Bsp::s(row!(sep1, title, sep2), value), " ")
}
}
pub struct Repeat<'a>(pub &'a str);
impl Content<TuiOut> for Repeat<'_> {
fn layout (&self, to: [u16;4]) -> [u16;4] { to }
fn render (&self, to: &mut TuiOut) {
let [x, y, w, h] = to.area().xywh();
let a = self.0.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]);
}
}
}
}
}
pub struct RepeatV<'a>(pub &'a str);
impl Content<TuiOut> for RepeatV<'_> {
fn layout (&self, to: [u16;4]) -> [u16;4] { to }
fn render (&self, to: &mut TuiOut) {
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(&self.0);
}
}
}
}
pub struct RepeatH<'a>(pub &'a str);
impl Content<TuiOut> for RepeatH<'_> {
fn layout (&self, to: [u16;4]) -> [u16;4] { to }
fn render (&self, to: &mut TuiOut) {
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(&self.0);
}
}
}
}
pub struct ScrollbarV {
pub offset: usize,
pub length: usize,
pub total: usize,
}
impl ScrollbarV {
const ICON_DEC: &[char] = &['▲'];
const ICON_INC: &[char] = &['▼'];
}
impl Content<TuiOut> for ScrollbarV {
fn render (&self, to: &mut TuiOut) {
let [x, y1, _w, h] = to.area().xywh();
let y2 = y1 + h;
for (i, y) in (y1..=y2).enumerate() {
if let Some(cell) = to.buffer.cell_mut(Position::from((x, y))) {
if (i as usize) < (Self::ICON_DEC.len()) {
cell.set_fg(Rgb(255, 255, 255));
cell.set_bg(Rgb(0, 0, 0));
cell.set_char(Self::ICON_DEC[i as usize]);
} else if (i as usize) > (h as usize - Self::ICON_INC.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]);
} 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('╎'); // ━
}
}
}
}
}
pub struct ScrollbarH {
pub offset: usize,
pub length: usize,
pub total: usize,
}
impl ScrollbarH {
const ICON_DEC: &[char] = &[' ', '🞀', ' '];
const ICON_INC: &[char] = &[' ', '🞂', ' '];
}
impl Content<TuiOut> for ScrollbarH {
fn render (&self, to: &mut TuiOut) {
let [x1, y, w, _h] = to.area().xywh();
let x2 = x1 + w;
for (i, x) in (x1..=x2).enumerate() {
if let Some(cell) = to.buffer.cell_mut(Position::from((x, y))) {
if i < (Self::ICON_DEC.len()) {
cell.set_fg(Rgb(255, 255, 255));
cell.set_bg(Rgb(0, 0, 0));
cell.set_char(Self::ICON_DEC[x as usize]);
} else if i > (w as usize - Self::ICON_INC.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]);
} 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('╌');
}
}
}
}
}
/// 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],
}
impl<T> Phat<T> {
/// A phat line
pub fn lo (fg: Color, bg: Color) -> impl Content<TuiOut> {
Fixed::y(1, Tui::fg_bg(fg, bg, RepeatH(&"")))
}
/// A phat line
pub fn hi (fg: Color, bg: Color) -> impl Content<TuiOut> {
Fixed::y(1, Tui::fg_bg(fg, bg, RepeatH(&"")))
}
}
impl<T: Content<TuiOut>> Content<TuiOut> for Phat<T> {
fn content (&self) -> impl Render<TuiOut> {
let [fg, bg, hi, lo] = self.colors;
let top = Fixed::y(1, Self::lo(bg, hi));
let low = Fixed::y(1, Self::hi(bg, lo));
let content = Tui::fg_bg(fg, bg, &self.content);
Min::xy(self.width, self.height, Bsp::s(top, Bsp::n(low, Fill::xy(content))))
}
}
pub trait TuiStyle {
fn fg <R: Content<TuiOut>> (color: Color, w: R) -> Foreground<R> {
Foreground(color, w)
}
fn bg <R: Content<TuiOut>> (color: Color, w: R) -> Background<R> {
Background(color, w)
}
fn fg_bg <R: Content<TuiOut>> (fg: Color, bg: Color, w: R) -> Background<Foreground<R>> {
Background(bg, Foreground(fg, w))
}
fn bold <R: Content<TuiOut>> (enable: bool, w: R) -> Bold<R> {
Bold(enable, w)
}
fn border <R: Content<TuiOut>, S: BorderStyle> (enable: bool, style: S, w: R) -> Bordered<S, R> {
Bordered(enable, style, w)
}
}
impl TuiStyle for Tui {}
pub struct Bold<R: Content<TuiOut>>(pub bool, R);
impl<R: Content<TuiOut>> Content<TuiOut> for Bold<R> {
fn content (&self) -> impl Render<TuiOut> { &self.1 }
fn render (&self, to: &mut TuiOut) {
to.fill_bold(to.area(), self.0);
self.1.render(to)
}
}
pub struct Foreground<R: Content<TuiOut>>(pub Color, R);
impl<R: Content<TuiOut>> Content<TuiOut> for Foreground<R> {
fn content (&self) -> impl Render<TuiOut> { &self.1 }
fn render (&self, to: &mut TuiOut) {
to.fill_fg(to.area(), self.0);
self.1.render(to)
}
}
pub struct Background<R: Content<TuiOut>>(pub Color, R);
impl<R: Content<TuiOut>> Content<TuiOut> for Background<R> {
fn content (&self) -> impl Render<TuiOut> { &self.1 }
fn render (&self, to: &mut TuiOut) {
to.fill_bg(to.area(), self.0);
self.1.render(to)
}
}
pub struct Styled<R: Content<TuiOut>>(pub Option<Style>, pub R);
impl<R: Content<TuiOut>> Content<TuiOut> for Styled<R> {
fn content (&self) -> impl Render<TuiOut> { &self.1 }
fn render (&self, to: &mut TuiOut) {
to.place(self.content().layout(to.area()), &self.content());
// TODO write style over area
}
}
pub struct Bordered<S: BorderStyle, W: Content<TuiOut>>(pub bool, pub S, pub W);
content!(TuiOut: |self: Bordered<S: BorderStyle, W: Content<TuiOut>>|Fill::xy(
lay!(When::new(self.0, Border(self.0, self.1)), Padding::xy(1, 1, &self.2))
));
pub struct Border<S: BorderStyle>(pub bool, pub S);
render!(TuiOut: |self: Border<S: BorderStyle>, to| {
if self.0 {
let area = to.area();
if area.w() > 0 && area.y() > 0 {
to.blit(&self.1.nw(), area.x(), area.y(), self.1.style());
to.blit(&self.1.ne(), area.x() + area.w() - 1, area.y(), self.1.style());
to.blit(&self.1.sw(), area.x(), area.y() + area.h() - 1, self.1.style());
to.blit(&self.1.se(), area.x() + area.w() - 1, area.y() + area.h() - 1, self.1.style());
for x in area.x()+1..area.x()+area.w()-1 {
to.blit(&self.1.n(), x, area.y(), self.1.style());
to.blit(&self.1.s(), x, area.y() + area.h() - 1, self.1.style());
}
for y in area.y()+1..area.y()+area.h()-1 {
to.blit(&self.1.w(), area.x(), y, self.1.style());
to.blit(&self.1.e(), area.x() + area.w() - 1, y, self.1.style());
}
}
}
});
pub trait BorderStyle: Send + Sync + Copy {
fn enabled (&self) -> bool;
fn enclose <W: Content<TuiOut>> (self, w: W) -> impl Content<TuiOut> {
Bsp::b(Fill::xy(Border(self.enabled(), self)), w)
}
fn enclose2 <W: Content<TuiOut>> (self, w: W) -> impl Content<TuiOut> {
Bsp::b(Margin::xy(1, 1, Fill::xy(Border(self.enabled(), self))), w)
}
fn enclose_bg <W: Content<TuiOut>> (self, w: W) -> impl Content<TuiOut> {
Tui::bg(self.style().unwrap().bg.unwrap_or(Color::Reset),
Bsp::b(Fill::xy(Border(self.enabled(), self)), w))
}
const NW: &'static str = "";
const N: &'static str = "";
const NE: &'static str = "";
const E: &'static str = "";
const SE: &'static str = "";
const S: &'static str = "";
const SW: &'static str = "";
const W: &'static str = "";
const N0: &'static str = "";
const S0: &'static str = "";
const W0: &'static str = "";
const E0: &'static str = "";
fn n (&self) -> &str { Self::N }
fn s (&self) -> &str { Self::S }
fn e (&self) -> &str { Self::E }
fn w (&self) -> &str { Self::W }
fn nw (&self) -> &str { Self::NW }
fn ne (&self) -> &str { Self::NE }
fn sw (&self) -> &str { Self::SW }
fn se (&self) -> &str { Self::SE }
#[inline] fn draw <'a> (
&self, to: &mut TuiOut
) -> Usually<()> {
if self.enabled() {
self.draw_horizontal(to, None)?;
self.draw_vertical(to, None)?;
self.draw_corners(to, None)?;
}
Ok(())
}
#[inline] fn draw_horizontal (
&self, to: &mut TuiOut, style: Option<Style>
) -> Usually<[u16;4]> {
let area = to.area();
let style = style.or_else(||self.style_horizontal());
let [x, x2, y, y2] = area.lrtb();
for x in x..x2.saturating_sub(1) {
to.blit(&Self::N, x, y, style);
to.blit(&Self::S, x, y2.saturating_sub(1), style)
}
Ok(area)
}
#[inline] fn draw_vertical (
&self, to: &mut TuiOut, style: Option<Style>
) -> Usually<[u16;4]> {
let area = to.area();
let style = style.or_else(||self.style_vertical());
let [x, x2, y, y2] = area.lrtb();
let h = y2 - y;
if h > 1 {
for y in y..y2.saturating_sub(1) {
to.blit(&Self::W, x, y, style);
to.blit(&Self::E, x2.saturating_sub(1), y, style);
}
} else if h > 0 {
to.blit(&Self::W0, x, y, style);
to.blit(&Self::E0, x2.saturating_sub(1), y, style);
}
Ok(area)
}
#[inline] fn draw_corners (
&self, to: &mut TuiOut, style: Option<Style>
) -> Usually<[u16;4]> {
let area = to.area();
let style = style.or_else(||self.style_corners());
let [x, y, width, height] = area.xywh();
if width > 1 && height > 1 {
to.blit(&Self::NW, x, y, style);
to.blit(&Self::NE, x + width - 1, y, style);
to.blit(&Self::SW, x, y + height - 1, style);
to.blit(&Self::SE, x + width - 1, y + height - 1, style);
}
Ok(area)
}
#[inline] fn style (&self) -> Option<Style> { None }
#[inline] fn style_horizontal (&self) -> Option<Style> { self.style() }
#[inline] fn style_vertical (&self) -> Option<Style> { self.style() }
#[inline] fn style_corners (&self) -> Option<Style> { self.style() }
}
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 { false }
}
#[derive(Copy, Clone)] pub struct $T(pub bool, pub Style);
impl Content<TuiOut> for $T {
fn render (&self, to: &mut TuiOut) {
if self.enabled() { let _ = self.draw(to); }
}
}
)+}
}
border! {
Square {
"" "" ""
"" ""
"" "" "" fn style (&self) -> Option<Style> { Some(self.1) }
},
SquareBold {
"" "" ""
"" ""
"" "" "" fn style (&self) -> Option<Style> { Some(self.1) }
},
TabLike {
"" "" ""
"" ""
"" " " "" fn style (&self) -> Option<Style> { Some(self.1) }
},
Lozenge {
"" "" ""
"" ""
"" "" "" fn style (&self) -> Option<Style> { Some(self.1) }
},
Brace {
"" "" ""
"" ""
"" "" "" fn style (&self) -> Option<Style> { Some(self.1) }
},
LozengeDotted {
"" "" ""
"" ""
"" "" "" fn style (&self) -> Option<Style> { Some(self.1) }
},
Quarter {
"" "" "🮇"
"" "🮇"
"" "" "🮇" fn style (&self) -> Option<Style> { Some(self.1) }
},
QuarterV {
"" "" "🮇"
"" "🮇"
"" "" "🮇" fn style (&self) -> Option<Style> { Some(self.1) }
},
Chamfer {
"🭂" "" "🭍"
"" "🮇"
"🭓" "" "🭞" fn style (&self) -> Option<Style> { Some(self.1) }
},
Corners {
"🬆" "" "🬊" // 🬴 🬸
"" ""
"🬱" "" "🬵" fn style (&self) -> Option<Style> { Some(self.1) }
},
CornersTall {
"🭽" "" "🭾"
"" ""
"🭼" "" "🭿" fn style (&self) -> Option<Style> { Some(self.1) }
},
Outer {
"🭽" "" "🭾"
"" ""
"🭼" "" "🭿"
const W0: &'static str = "[";
const E0: &'static str = "]";
const N0: &'static str = "";
const S0: &'static str = "";
fn style (&self) -> Option<Style> { Some(self.1) }
},
Thick {
"" "" ""
"" ""
"" "" ""
fn style (&self) -> Option<Style> { Some(self.1) }
},
Rugged {
"" "" ""
"" ""
"" "🮂" ""
fn style (&self) -> Option<Style> { Some(self.1) }
},
Skinny {
"" "" ""
"" ""
"" "" ""
fn style (&self) -> Option<Style> { Some(self.1) }
},
Brackets {
"" "" ""
"" ""
"" "" ""
const W0: &'static str = "[";
const E0: &'static str = "]";
const N0: &'static str = "";
const S0: &'static str = "";
fn style (&self) -> Option<Style> { Some(self.1) }
},
Reticle {
"" "" ""
"" ""
"" "" ""
const W0: &'static str = "";
const E0: &'static str = "";
const N0: &'static str = "";
const S0: &'static str = "";
fn style (&self) -> Option<Style> { Some(self.1) }
}
}
//impl<S: BorderStyle, R: Content<TuiOut>> Content<TuiOut> for Bordered<S, R> {
//fn content (&self) -> impl Render<TuiOut> {
//let content: &dyn Content<TuiOut> = &self.1;
//lay! { content.padding_xy(1, 1), Border(self.0) }.fill_xy()
//}
//}

82
tui/src/tui_engine.rs Normal file
View file

@ -0,0 +1,82 @@
use crate::*;
use std::time::Duration;
pub struct Tui {
pub exited: Arc<AtomicBool>,
pub backend: CrosstermBackend<Stdout>,
pub buffer: Buffer,
pub area: [u16;4],
pub perf: PerfModel,
}
impl Tui {
/// Construct a new TUI engine and wrap it for shared ownership.
pub fn new () -> Usually<Arc<RwLock<Self>>> {
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(),
})))
}
/// True if done
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();
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)
}
}
pub trait TuiRun<R: Render<TuiOut> + Handle<TuiIn> + 'static> {
/// Run an app in the main loop.
fn run (&self, state: &Arc<RwLock<R>>) -> Usually<()>;
}
impl<T: Render<TuiOut> + Handle<TuiIn> + 'static> TuiRun<T> for Arc<RwLock<Tui>> {
fn run (&self, state: &Arc<RwLock<T>>) -> 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\rRender thread failed: {error:?}.\n\r")
},
}
Ok(())
}
}

87
tui/src/tui_file.rs Normal file
View file

@ -0,0 +1,87 @@
use crate::*;
/// Browses for phrase to import/export
#[derive(Debug, Clone)]
pub struct FileBrowser {
pub cwd: PathBuf,
pub dirs: Vec<(OsString, String)>,
pub files: Vec<(OsString, String)>,
pub filter: String,
pub index: usize,
pub scroll: usize,
pub size: Measure<TuiOut>
}
/// Commands supported by [FileBrowser]
#[derive(Debug, Clone, PartialEq)]
pub enum FileBrowserCommand {
Begin,
Cancel,
Confirm,
Select(usize),
Chdir(PathBuf),
Filter(Arc<str>),
}
content!(TuiOut: |self: FileBrowser| /*Stack::down(|add|{
let mut i = 0;
for (_, name) in self.dirs.iter() {
if i >= self.scroll {
add(&Tui::bold(i == self.index, name.as_str()))?;
}
i += 1;
}
for (_, name) in self.files.iter() {
if i >= self.scroll {
add(&Tui::bold(i == self.index, name.as_str()))?;
}
i += 1;
}
add(&format!("{}/{i}", self.index))?;
Ok(())
})*/"todo");
impl FileBrowser {
pub fn new (cwd: Option<PathBuf>) -> Usually<Self> {
let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? };
let mut dirs = vec![];
let mut files = vec![];
for entry in std::fs::read_dir(&cwd)? {
let entry = entry?;
let name = entry.file_name();
let decoded = name.clone().into_string().unwrap_or_else(|_|"<unreadable>".to_string());
let meta = entry.metadata()?;
if meta.is_dir() {
dirs.push((name, format!("📁 {decoded}")));
} else if meta.is_file() {
files.push((name, format!("📄 {decoded}")));
}
}
Ok(Self {
cwd,
dirs,
files,
filter: "".to_string(),
index: 0,
scroll: 0,
size: Measure::new(),
})
}
pub fn len (&self) -> usize {
self.dirs.len() + self.files.len()
}
pub fn is_dir (&self) -> bool {
self.index < self.dirs.len()
}
pub fn is_file (&self) -> bool {
self.index >= self.dirs.len()
}
pub fn path (&self) -> PathBuf {
self.cwd.join(if self.is_dir() {
&self.dirs[self.index].0
} else if self.is_file() {
&self.files[self.index - self.dirs.len()].0
} else {
unreachable!()
})
}
pub fn chdir (&self) -> Usually<Self> {
Self::new(Some(self.path()))
}
}

306
tui/src/tui_focus.rs Normal file
View file

@ -0,0 +1,306 @@
use crate::*;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum FocusState<T: Copy + Debug + PartialEq> {
Focused(T),
Entered(T),
}
impl<T: Copy + Debug + PartialEq> FocusState<T> {
pub fn inner (&self) -> T {
match self {
Self::Focused(inner) => *inner,
Self::Entered(inner) => *inner,
}
}
pub fn set_inner (&mut self, inner: T) {
*self = match self {
Self::Focused(_) => Self::Focused(inner),
Self::Entered(_) => Self::Entered(inner),
}
}
pub fn is_focused (&self) -> bool { matches!(self, Self::Focused(_)) }
pub fn is_entered (&self) -> bool { matches!(self, Self::Entered(_)) }
pub fn focus (&mut self) { *self = Self::Focused(self.inner()) }
pub fn enter (&mut self) { *self = Self::Entered(self.inner()) }
}
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum FocusCommand<T: Send + Sync> {
Up,
Down,
Left,
Right,
Next,
Prev,
Enter,
Exit,
Set(T)
}
impl<F: HasFocus + HasEnter + FocusGrid + FocusOrder> Command<F> for FocusCommand<F::Item> {
fn execute (self, state: &mut F) -> Perhaps<FocusCommand<F::Item>> {
match self {
Self::Next => { state.focus_next(); },
Self::Prev => { state.focus_prev(); },
Self::Up => { state.focus_up(); },
Self::Down => { state.focus_down(); },
Self::Left => { state.focus_left(); },
Self::Right => { state.focus_right(); },
Self::Enter => { state.focus_enter(); },
Self::Exit => { state.focus_exit(); },
Self::Set(to) => { state.set_focused(to); },
}
Ok(None)
}
}
/// Trait for things that have focusable subparts.
pub trait HasFocus {
type Item: Copy + PartialEq + Debug + Send + Sync;
/// Get the currently focused item.
fn focused (&self) -> Self::Item;
/// Get the currently focused item.
fn set_focused (&mut self, to: Self::Item);
/// Loop forward until a specific item is focused.
fn focus_to (&mut self, to: Self::Item) {
self.set_focused(to);
self.focus_updated();
}
/// Run this on focus update
fn focus_updated (&mut self) {}
}
/// Trait for things that have enterable subparts.
pub trait HasEnter: HasFocus {
/// Get the currently focused item.
fn entered (&self) -> bool;
/// Get the currently focused item.
fn set_entered (&mut self, entered: bool);
/// Enter into the currently focused component
fn focus_enter (&mut self) {
self.set_entered(true);
self.focus_updated();
}
/// Exit the currently entered component
fn focus_exit (&mut self) {
self.set_entered(false);
self.focus_updated();
}
}
/// Trait for things that implement directional navigation between focusable elements.
pub trait FocusGrid: HasFocus {
fn focus_layout (&self) -> &[&[Self::Item]];
fn focus_cursor (&self) -> (usize, usize);
fn focus_cursor_mut (&mut self) -> &mut (usize, usize);
fn focus_current (&self) -> Self::Item {
let (x, y) = self.focus_cursor();
self.focus_layout()[y][x]
}
fn focus_update (&mut self) {
self.focus_to(self.focus_current());
self.focus_updated()
}
fn focus_up (&mut self) {
let original_focused = self.focused();
let (_, original_y) = self.focus_cursor();
loop {
let (x, y) = self.focus_cursor();
let next_y = if y == 0 {
self.focus_layout().len().saturating_sub(1)
} else {
y - 1
};
if next_y == original_y {
break
}
let next_x = if self.focus_layout()[y].len() == self.focus_layout()[next_y].len() {
x
} else {
((x as f32 / self.focus_layout()[original_y].len() as f32)
* self.focus_layout()[next_y].len() as f32) as usize
};
*self.focus_cursor_mut() = (next_x, next_y);
if self.focus_current() != original_focused {
break
}
}
self.focus_update();
}
fn focus_down (&mut self) {
let original_focused = self.focused();
let (_, original_y) = self.focus_cursor();
loop {
let (x, y) = self.focus_cursor();
let next_y = if y >= self.focus_layout().len().saturating_sub(1) {
0
} else {
y + 1
};
if next_y == original_y {
break
}
let next_x = if self.focus_layout()[y].len() == self.focus_layout()[next_y].len() {
x
} else {
((x as f32 / self.focus_layout()[original_y].len() as f32)
* self.focus_layout()[next_y].len() as f32) as usize
};
*self.focus_cursor_mut() = (next_x, next_y);
if self.focus_current() != original_focused {
break
}
}
self.focus_update();
}
fn focus_left (&mut self) {
let original_focused = self.focused();
let (original_x, y) = self.focus_cursor();
loop {
let x = self.focus_cursor().0;
let next_x = if x == 0 {
self.focus_layout()[y].len().saturating_sub(1)
} else {
x - 1
};
if next_x == original_x {
break
}
*self.focus_cursor_mut() = (next_x, y);
if self.focus_current() != original_focused {
break
}
}
self.focus_update();
}
fn focus_right (&mut self) {
let original_focused = self.focused();
let (original_x, y) = self.focus_cursor();
loop {
let x = self.focus_cursor().0;
let next_x = if x >= self.focus_layout()[y].len().saturating_sub(1) {
0
} else {
x + 1
};
if next_x == original_x {
break
}
self.focus_cursor_mut().0 = next_x;
if self.focus_current() != original_focused {
break
}
}
self.focus_update();
}
}
/// Trait for things that implement next/prev navigation between focusable elements.
pub trait FocusOrder {
/// Focus the next item.
fn focus_next (&mut self);
/// Focus the previous item.
fn focus_prev (&mut self);
}
/// Next/prev navigation for directional focusables works in the given way.
impl<T: FocusGrid + HasEnter> FocusOrder for T {
/// Focus the next item.
fn focus_next (&mut self) {
let current = self.focused();
let (x, y) = self.focus_cursor();
if x < self.focus_layout()[y].len().saturating_sub(1) {
self.focus_right();
} else {
self.focus_down();
self.focus_cursor_mut().0 = 0;
}
if self.focused() == current { // FIXME: prevent infinite loop
self.focus_next()
}
self.focus_exit();
self.focus_update();
}
/// Focus the previous item.
fn focus_prev (&mut self) {
let current = self.focused();
let (x, _) = self.focus_cursor();
if x > 0 {
self.focus_left();
} else {
self.focus_up();
let (_, y) = self.focus_cursor();
let next_x = self.focus_layout()[y].len().saturating_sub(1);
self.focus_cursor_mut().0 = next_x;
}
if self.focused() == current { // FIXME: prevent infinite loop
self.focus_prev()
}
self.focus_exit();
self.focus_update();
}
}
pub trait FocusWrap<T> {
fn wrap <W: Content<TuiOut>> (self, focus: T, content: &'_ W) -> impl Content<TuiOut> + '_;
}
pub fn to_focus_command <T: Send + Sync> (input: &TuiIn) -> Option<FocusCommand<T>> {
Some(match input.event() {
kpat!(Tab) => FocusCommand::Next,
kpat!(Shift-Tab) => FocusCommand::Prev,
kpat!(BackTab) => FocusCommand::Prev,
kpat!(Shift-BackTab) => FocusCommand::Prev,
kpat!(Up) => FocusCommand::Up,
kpat!(Down) => FocusCommand::Down,
kpat!(Left) => FocusCommand::Left,
kpat!(Right) => FocusCommand::Right,
kpat!(Enter) => FocusCommand::Enter,
kpat!(Esc) => FocusCommand::Exit,
_ => return None
})
}
#[macro_export] macro_rules! impl_focus {
($Struct:ident $Focus:ident $Grid:expr $(=> [$self:ident : $update_focus:expr])?) => {
impl HasFocus for $Struct {
type Item = $Focus;
/// Get the currently focused item.
fn focused (&self) -> Self::Item {
self.focus.inner()
}
/// Get the currently focused item.
fn set_focused (&mut self, to: Self::Item) {
self.focus.set_inner(to)
}
$(fn focus_updated (&mut $self) { $update_focus })?
}
impl HasEnter for $Struct {
/// Get the currently focused item.
fn entered (&self) -> bool {
self.focus.is_entered()
}
/// Get the currently focused item.
fn set_entered (&mut self, entered: bool) {
if entered {
self.focus.to_entered()
} else {
self.focus.to_focused()
}
}
}
impl FocusGrid for $Struct {
fn focus_cursor (&self) -> (usize, usize) {
self.cursor
}
fn focus_cursor_mut (&mut self) -> &mut (usize, usize) {
&mut self.cursor
}
fn focus_layout (&self) -> &[&[$Focus]] {
use $Focus::*;
&$Grid
}
}
}
}

146
tui/src/tui_input.rs Normal file
View file

@ -0,0 +1,146 @@
use crate::*;
use std::time::Duration;
use std::thread::{spawn, JoinHandle};
use crossterm::event::{poll, read};
#[derive(Debug, Clone)] pub struct TuiIn(pub Arc<AtomicBool>, pub Event);
impl Input for TuiIn {
type Event = Event;
type Handled = bool;
fn event (&self) -> &Event { &self.1 }
fn is_done (&self) -> bool { self.0.fetch_and(true, Relaxed) }
fn done (&self) { self.0.store(true, Relaxed); }
}
impl TuiIn {
/// Spawn the input thread.
pub fn run_input <T: Handle<TuiIn> + '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 {
crossterm::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();
if let Err(e) = state.write().unwrap().handle(&TuiIn(exited, event)) {
panic!("{e}")
}
}
}
}
})
}
}
impl AtomInput for TuiIn {
fn matches_atom (&self, token: &str) -> bool {
if let Some(event) = KeyMatcher::new(token).build() {
&event == self.event()
} else {
false
}
}
//fn get_event (item: &AtomItem<impl AsRef<str>>) -> Option<Event> {
//match item { AtomItem::Sym(s) => KeyMatcher::new(s).build(), _ => None }
//}
}
struct KeyMatcher {
valid: bool,
key: Option<KeyCode>,
mods: KeyModifiers,
}
impl KeyMatcher {
fn new (token: impl AsRef<str>) -> Self {
let token = token.as_ref();
if token.len() < 2 {
Self { valid: false, key: None, mods: KeyModifiers::NONE }
} else if token.chars().next() != Some('@') {
Self { valid: false, key: None, mods: KeyModifiers::NONE }
} else {
Self { valid: true, key: None, mods: KeyModifiers::NONE }.next(&token[1..])
}
}
fn next (mut self, token: &str) -> Self {
let mut tokens = token.split('-').peekable();
while let Some(token) = tokens.next() {
if tokens.peek().is_some() {
match token {
"ctrl" | "Ctrl" | "c" | "C" => self.mods |= KeyModifiers::CONTROL,
"alt" | "Alt" | "m" | "M" => self.mods |= KeyModifiers::ALT,
"shift" | "Shift" | "s" | "S" => {
self.mods |= KeyModifiers::SHIFT;
// + TODO normalize character case, BackTab, etc.
},
_ => panic!("unknown modifier {token}"),
}
} else {
self.key = if token.len() == 1 {
Some(KeyCode::Char(token.chars().next().unwrap()))
} else {
Some(Self::named_key(token).unwrap_or_else(||panic!("unknown character {token}")))
}
}
}
self
}
fn named_key (token: &str) -> Option<KeyCode> {
use KeyCode::*;
Some(match token {
"up" => Up,
"down" => Down,
"left" => Left,
"right" => Right,
"enter" | "return" => Enter,
"delete" | "del" => Delete,
"tab" => Tab,
"space" => Char(' '),
"comma" => Char(','),
"period" => Char('.'),
"plus" => Char('+'),
"minus" | "dash" => Char('-'),
"equal" | "equals" => Char('='),
"underscore" => Char('_'),
"backtick" => Char('`'),
"lt" => Char('<'),
"gt" => Char('>'),
"openbracket" => Char('['),
"closebracket" => Char(']'),
_ => return None,
})
}
fn build (self) -> Option<Event> {
if self.valid && self.key.is_some() {
Some(Event::Key(KeyEvent::new(self.key.unwrap(), self.mods)))
} else {
None
}
}
}
#[cfg(test)] #[test] fn test_parse_key () {
use KeyModifiers as Mods;
let test = |x: &str, y|assert_eq!(KeyMatcher::new(x).build(), Some(Event::Key(y)));
//test(":x",
//KeyEvent::new(KeyCode::Char('x'), Mods::NONE));
//test(":ctrl-x",
//KeyEvent::new(KeyCode::Char('x'), Mods::CONTROL));
//test(":alt-x",
//KeyEvent::new(KeyCode::Char('x'), Mods::ALT));
//test(":shift-x",
//KeyEvent::new(KeyCode::Char('x'), Mods::SHIFT));
//test(":ctrl-alt-shift-x",
//KeyEvent::new(KeyCode::Char('x'), Mods::CONTROL | Mods::ALT | Mods::SHIFT ));
}

63
tui/src/tui_menu.rs Normal file
View file

@ -0,0 +1,63 @@
use crate::*;
pub struct MenuBar<E: Engine, S, C: Command<S>> {
pub menus: Vec<Menu<E, S, C>>,
pub index: usize,
}
impl<E: Engine, S, C: Command<S>> MenuBar<E, S, C> {
pub fn new () -> Self { Self { menus: vec![], index: 0 } }
pub fn add (mut self, menu: Menu<E, S, C>) -> Self {
self.menus.push(menu);
self
}
}
pub struct Menu<E: Engine, S, C: Command<S>> {
pub title: Arc<str>,
pub items: Vec<MenuItem<E, S, C>>,
pub index: Option<usize>,
}
impl<E: Engine, S, C: Command<S>> Menu<E, S, C> {
pub fn new (title: impl AsRef<str>) -> Self {
Self {
title: title.as_ref().to_string(),
items: vec![],
index: None,
}
}
pub fn add (mut self, item: MenuItem<E, S, C>) -> Self {
self.items.push(item);
self
}
pub fn sep (mut self) -> Self {
self.items.push(MenuItem::sep());
self
}
pub fn cmd (mut self, hotkey: &'static str, text: &'static str, command: C) -> Self {
self.items.push(MenuItem::cmd(hotkey, text, command));
self
}
pub fn off (mut self, hotkey: &'static str, text: &'static str) -> Self {
self.items.push(MenuItem::off(hotkey, text));
self
}
}
pub enum MenuItem<E: Engine, S, C: Command<S>> {
/// Unused.
__(PhantomData<E>, PhantomData<S>),
/// A separator. Skip it.
Separator,
/// A menu item with command, description and hotkey.
Command(&'static str, &'static str, C),
/// A menu item that can't be activated but has description and hotkey
Disabled(&'static str, &'static str)
}
impl<E: Engine, S, C: Command<S>> MenuItem<E, S, C> {
pub fn sep () -> Self {
Self::Separator
}
pub fn cmd (hotkey: &'static str, text: &'static str, command: C) -> Self {
Self::Command(hotkey, text, command)
}
pub fn off (hotkey: &'static str, text: &'static str) -> Self {
Self::Disabled(hotkey, text)
}
}

104
tui/src/tui_output.rs Normal file
View file

@ -0,0 +1,104 @@
use crate::*;
use std::time::Duration;
use std::thread::{spawn, JoinHandle};
#[derive(Default)]
pub struct TuiOut {
pub buffer: Buffer,
pub area: [u16;4]
}
impl Output for TuiOut {
type Unit = u16;
type Size = [Self::Unit;2];
type Area = [Self::Unit;4];
#[inline] fn area (&self) -> [u16;4] { self.area }
#[inline] fn area_mut (&mut self) -> &mut [u16;4] { &mut self.area }
#[inline] fn place (&mut self, area: [u16;4], content: &impl Render<TuiOut>) {
let last = self.area();
*self.area_mut() = area;
content.render(self);
*self.area_mut() = last;
}
}
impl TuiOut {
/// Spawn the output thread.
pub fn run_output <T: Render<TuiOut> + Send + Sync + 'static> (
engine: &Arc<RwLock<Tui>>,
state: &Arc<RwLock<T>>,
timer: Duration
) -> JoinHandle<()> {
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 });
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: [0, 0, width, height] };
state.render(&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);
})
}
pub fn buffer_update (&mut self, area: [u16;4], callback: &impl Fn(&mut Cell, u16, u16)) {
buffer_update(&mut self.buffer, area, callback);
}
pub fn fill_bold (&mut self, area: [u16;4], on: bool) {
if on {
self.buffer_update(area, &|cell,_,_|cell.modifier.insert(Modifier::BOLD))
} else {
self.buffer_update(area, &|cell,_,_|cell.modifier.remove(Modifier::BOLD))
}
}
pub fn fill_bg (&mut self, area: [u16;4], color: Color) {
self.buffer_update(area, &|cell,_,_|{cell.set_bg(color);})
}
pub fn fill_fg (&mut self, area: [u16;4], color: Color) {
self.buffer_update(area, &|cell,_,_|{cell.set_fg(color);})
}
pub fn fill_ul (&mut self, area: [u16;4], color: Color) {
self.buffer_update(area, &|cell,_,_|{
cell.modifier = ratatui::prelude::Modifier::UNDERLINED;
cell.underline_color = color;
})
}
pub fn fill_char (&mut self, area: [u16;4], c: char) {
self.buffer_update(area, &|cell,_,_|{cell.set_char(c);})
}
pub fn make_dim (&mut self) {
for cell in self.buffer.content.iter_mut() {
cell.bg = ratatui::style::Color::Rgb(30,30,30);
cell.fg = ratatui::style::Color::Rgb(100,100,100);
cell.modifier = ratatui::style::Modifier::DIM;
}
}
pub fn blit (
&mut self, text: &impl AsRef<str>, x: u16, y: u16, style: Option<Style>
) {
let text = text.as_ref();
let buf = &mut self.buffer;
if x < buf.area.width && y < buf.area.height {
buf.set_string(x, y, text, style.unwrap_or(Style::default()));
}
}
#[inline]
pub fn with_rect (&mut self, area: [u16;4]) -> &mut Self {
self.area = area;
self
}
}

64
tui/src/tui_perf.rs Normal file
View file

@ -0,0 +1,64 @@
use crate::*;
/// Performance counter
#[derive(Debug)]
pub struct PerfModel {
pub enabled: bool,
clock: quanta::Clock,
// In nanoseconds. Time used by last iteration.
used: AtomicF64,
// In microseconds. Max prescribed time for iteration (frame, chunk...).
window: AtomicF64,
}
pub trait HasPerf {
fn perf (&self) -> &PerfModel;
}
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
}
}
}