mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 11:46:41 +01:00
extract tui support code to tek_tui
This commit is contained in:
parent
1a9077427c
commit
1faf5bb6df
22 changed files with 477 additions and 450 deletions
18
tui/Cargo.toml
Normal file
18
tui/Cargo.toml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "tek_tui"
|
||||
edition = "2021"
|
||||
version = "0.2.0"
|
||||
|
||||
[dependencies]
|
||||
tek_engine = { path = "../engine" }
|
||||
tek_layout = { path = "../layout" }
|
||||
tek_edn = { optional = true, path = "../edn" }
|
||||
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"
|
||||
|
||||
[features]
|
||||
default = ["edn"]
|
||||
edn = ["tek_edn"]
|
||||
3
tui/README.md
Normal file
3
tui/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# `tek_tui`
|
||||
|
||||
tui utilities.
|
||||
44
tui/src/lib.rs
Normal file
44
tui/src/lib.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
pub use ::tek_engine;
|
||||
pub use ::tek_layout;
|
||||
pub use ::tek_edn;
|
||||
pub(crate) use tek_layout::*;
|
||||
pub(crate) use tek_engine::*;
|
||||
|
||||
mod tui_engine; pub use self::tui_engine::*;
|
||||
mod tui_input; pub use self::tui_input::*;
|
||||
mod tui_output; pub use self::tui_output::*;
|
||||
mod tui_run; pub use self::tui_run::*;
|
||||
|
||||
mod tui_color; pub use self::tui_color::*;
|
||||
mod tui_style; pub use self::tui_style::*;
|
||||
mod tui_theme; pub use self::tui_theme::*;
|
||||
mod tui_border; pub use self::tui_border::*;
|
||||
|
||||
pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicBool, Ordering::*}};
|
||||
pub(crate) use std::io::{stdout, Stdout};
|
||||
|
||||
pub use ::better_panic;
|
||||
pub(crate) use better_panic::{Settings, Verbosity};
|
||||
|
||||
pub use ::crossterm;
|
||||
pub(crate) use crossterm::{
|
||||
ExecutableCommand,
|
||||
terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode},
|
||||
event::{KeyCode, KeyModifiers, KeyEvent, 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 use ::palette;
|
||||
pub(crate) use ::palette::{
|
||||
*,
|
||||
convert::*,
|
||||
okhsl::*
|
||||
};
|
||||
248
tui/src/tui_border.rs
Normal file
248
tui/src/tui_border.rs
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
use crate::*;
|
||||
|
||||
pub struct Bordered<S: BorderStyle, W: Content<Tui>>(pub S, pub W);
|
||||
|
||||
render!(Tui: (self: Bordered<S: BorderStyle, W: Content<Tui>>) => {
|
||||
Fill::xy(lay!(Border(self.0), Padding::xy(1, 1, &self.1)))
|
||||
});
|
||||
|
||||
pub struct Border<S: BorderStyle>(pub S);
|
||||
|
||||
render!(Tui: |self: Border<S: BorderStyle>, to| {
|
||||
let area = to.area();
|
||||
if area.w() > 0 && area.y() > 0 {
|
||||
to.blit(&self.0.nw(), area.x(), area.y(), self.0.style());
|
||||
to.blit(&self.0.ne(), area.x() + area.w() - 1, area.y(), self.0.style());
|
||||
to.blit(&self.0.sw(), area.x(), area.y() + area.h() - 1, self.0.style());
|
||||
to.blit(&self.0.se(), area.x() + area.w() - 1, area.y() + area.h() - 1, self.0.style());
|
||||
for x in area.x()+1..area.x()+area.w()-1 {
|
||||
to.blit(&self.0.n(), x, area.y(), self.0.style());
|
||||
to.blit(&self.0.s(), x, area.y() + area.h() - 1, self.0.style());
|
||||
}
|
||||
for y in area.y()+1..area.y()+area.h()-1 {
|
||||
to.blit(&self.0.w(), area.x(), y, self.0.style());
|
||||
to.blit(&self.0.e(), area.x() + area.w() - 1, y, self.0.style());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
pub trait BorderStyle: Send + Sync + Copy {
|
||||
fn wrap <W: Content<Tui>> (self, w: W) -> Bordered<Self, W> {
|
||||
Bordered(self, w)
|
||||
}
|
||||
fn enclose <W: Content<Tui>> (self, w: W) -> impl Content<Tui> {
|
||||
lay!(Fill::xy(Border(self)), w)
|
||||
}
|
||||
fn enclose_bg <W: Content<Tui>> (self, w: W) -> impl Content<Tui> {
|
||||
Tui::bg(self.style().unwrap().bg.unwrap_or(Color::Reset), lay!(
|
||||
Fill::xy(Border(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<()> {
|
||||
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)*
|
||||
}
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct $T(pub Style);
|
||||
impl Content<Tui> for $T {
|
||||
fn render (&self, to: &mut TuiOut) { self.draw(to); }
|
||||
}
|
||||
)+}
|
||||
}
|
||||
|
||||
border! {
|
||||
Square {
|
||||
"┌" "─" "┐"
|
||||
"│" "│"
|
||||
"└" "─" "┘" fn style (&self) -> Option<Style> { Some(self.0) }
|
||||
},
|
||||
SquareBold {
|
||||
"┏" "━" "┓"
|
||||
"┃" "┃"
|
||||
"┗" "━" "┛" fn style (&self) -> Option<Style> { Some(self.0) }
|
||||
},
|
||||
TabLike {
|
||||
"╭" "─" "╮"
|
||||
"│" "│"
|
||||
"│" " " "│" fn style (&self) -> Option<Style> { Some(self.0) }
|
||||
},
|
||||
Lozenge {
|
||||
"╭" "─" "╮"
|
||||
"│" "│"
|
||||
"╰" "─" "╯" fn style (&self) -> Option<Style> { Some(self.0) }
|
||||
},
|
||||
Brace {
|
||||
"╭" "" "╮"
|
||||
"│" "│"
|
||||
"╰" "" "╯" fn style (&self) -> Option<Style> { Some(self.0) }
|
||||
},
|
||||
LozengeDotted {
|
||||
"╭" "┅" "╮"
|
||||
"┇" "┇"
|
||||
"╰" "┅" "╯" fn style (&self) -> Option<Style> { Some(self.0) }
|
||||
},
|
||||
Quarter {
|
||||
"▎" "▔" "🮇"
|
||||
"▎" "🮇"
|
||||
"▎" "▁" "🮇" fn style (&self) -> Option<Style> { Some(self.0) }
|
||||
},
|
||||
QuarterV {
|
||||
"▎" "" "🮇"
|
||||
"▎" "🮇"
|
||||
"▎" "" "🮇" fn style (&self) -> Option<Style> { Some(self.0) }
|
||||
},
|
||||
Chamfer {
|
||||
"🭂" "▔" "🭍"
|
||||
"▎" "🮇"
|
||||
"🭓" "▁" "🭞" fn style (&self) -> Option<Style> { Some(self.0) }
|
||||
},
|
||||
Corners {
|
||||
"🬆" "" "🬊" // 🬴 🬸
|
||||
"" ""
|
||||
"🬱" "" "🬵" fn style (&self) -> Option<Style> { Some(self.0) }
|
||||
},
|
||||
CornersTall {
|
||||
"🭽" "" "🭾"
|
||||
"" ""
|
||||
"🭼" "" "🭿" fn style (&self) -> Option<Style> { Some(self.0) }
|
||||
},
|
||||
Outer {
|
||||
"🭽" "▔" "🭾"
|
||||
"▏" "▕"
|
||||
"🭼" "▁" "🭿"
|
||||
const W0: &'static str = "[";
|
||||
const E0: &'static str = "]";
|
||||
const N0: &'static str = "⎴";
|
||||
const S0: &'static str = "⎵";
|
||||
fn style (&self) -> Option<Style> { Some(self.0) }
|
||||
},
|
||||
Brackets {
|
||||
"⎡" "" "⎤"
|
||||
"" ""
|
||||
"⎣" "" "⎦"
|
||||
const W0: &'static str = "[";
|
||||
const E0: &'static str = "]";
|
||||
const N0: &'static str = "⎴";
|
||||
const S0: &'static str = "⎵";
|
||||
fn style (&self) -> Option<Style> { Some(self.0) }
|
||||
},
|
||||
Reticle {
|
||||
"⎡" "" "⎤"
|
||||
"" ""
|
||||
"⎣" "" "⎦"
|
||||
const W0: &'static str = "╟";
|
||||
const E0: &'static str = "╢";
|
||||
const N0: &'static str = "┯";
|
||||
const S0: &'static str = "┷";
|
||||
fn style (&self) -> Option<Style> { Some(self.0) }
|
||||
}
|
||||
}
|
||||
|
||||
pub const CORNERS: Brackets = Brackets(Style {
|
||||
fg: Some(Color::Rgb(96, 255, 32)),
|
||||
bg: None,
|
||||
underline_color: None,
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::DIM
|
||||
});
|
||||
|
||||
pub const RETICLE: Reticle = Reticle(Style {
|
||||
fg: Some(Color::Rgb(96, 255, 32)),
|
||||
bg: None,
|
||||
underline_color: None,
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::DIM
|
||||
});
|
||||
104
tui/src/tui_color.rs
Normal file
104
tui/src/tui_color.rs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
use crate::*;
|
||||
use rand::{thread_rng, distributions::uniform::UniformSampler};
|
||||
|
||||
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,
|
||||
}
|
||||
/// 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,
|
||||
}
|
||||
from!(|okhsl: Okhsl<f32>|ItemColor = Self { okhsl, rgb: okhsl_to_rgb(okhsl) });
|
||||
from!(|rgb: Color|ItemColor = Self { rgb, okhsl: rgb_to_okhsl(rgb) });
|
||||
// A single color within item theme parameters, in OKHSL and RGB representations.
|
||||
impl ItemColor {
|
||||
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()
|
||||
}
|
||||
}
|
||||
from!(|base: Color|ItemPalette = Self::from(ItemColor::from(base)));
|
||||
from!(|base: ItemColor|ItemPalette = {
|
||||
let mut light = base.okhsl;
|
||||
light.lightness = (light.lightness * 1.3).min(Okhsl::<f32>::max_lightness());
|
||||
let mut lighter = light;
|
||||
lighter.lightness = (lighter.lightness * 1.3).min(Okhsl::<f32>::max_lightness());
|
||||
let mut lightest = base.okhsl;
|
||||
lightest.lightness = 0.95;
|
||||
|
||||
let mut dark = base.okhsl;
|
||||
dark.lightness = (dark.lightness * 0.75).max(Okhsl::<f32>::min_lightness());
|
||||
dark.saturation = (dark.saturation * 0.75).max(Okhsl::<f32>::min_saturation());
|
||||
let mut darker = dark;
|
||||
darker.lightness = (darker.lightness * 0.66).max(Okhsl::<f32>::min_lightness());
|
||||
darker.saturation = (darker.saturation * 0.66).max(Okhsl::<f32>::min_saturation());
|
||||
let mut darkest = darker;
|
||||
darkest.lightness = 0.1;
|
||||
darkest.saturation = (darkest.saturation * 0.50).max(Okhsl::<f32>::min_saturation());
|
||||
|
||||
Self {
|
||||
base,
|
||||
light: light.into(),
|
||||
lighter: lighter.into(),
|
||||
lightest: lightest.into(),
|
||||
dark: dark.into(),
|
||||
darker: darker.into(),
|
||||
darkest: darkest.into(),
|
||||
}
|
||||
});
|
||||
impl ItemPalette {
|
||||
pub fn random () -> Self {
|
||||
ItemColor::random().into()
|
||||
}
|
||||
pub fn random_near (color: Self, distance: f32) -> Self {
|
||||
color.base.mix(ItemColor::random(), distance).into()
|
||||
}
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
65
tui/src/tui_engine.rs
Normal file
65
tui/src/tui_engine.rs
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
use crate::*;
|
||||
|
||||
pub struct Tui {
|
||||
pub exited: Arc<AtomicBool>,
|
||||
pub buffer: Buffer,
|
||||
pub backend: CrosstermBackend<Stdout>,
|
||||
pub area: [u16;4], // FIXME auto resize
|
||||
}
|
||||
|
||||
impl Engine for Tui {
|
||||
type Unit = u16;
|
||||
type Size = [Self::Unit;2];
|
||||
type Area = [Self::Unit;4];
|
||||
type Input = TuiIn;
|
||||
type Handled = bool;
|
||||
type Output = TuiOut;
|
||||
fn exited (&self) -> bool {
|
||||
self.exited.fetch_and(true, Relaxed)
|
||||
}
|
||||
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)
|
||||
}
|
||||
fn teardown (&mut self) -> Usually<()> {
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
self.backend.show_cursor()?;
|
||||
disable_raw_mode().map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
})))
|
||||
}
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
106
tui/src/tui_input.rs
Normal file
106
tui/src/tui_input.rs
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
use crate::*;
|
||||
pub use crossterm::event::Event;
|
||||
use Event as CrosstermEvent;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TuiIn(pub Arc<AtomicBool>, pub CrosstermEvent);
|
||||
|
||||
impl Input<Tui> for TuiIn {
|
||||
type Event = Event;
|
||||
fn event (&self) -> &CrosstermEvent { &self.1 }
|
||||
fn is_done (&self) -> bool { self.0.fetch_and(true, Relaxed) }
|
||||
fn done (&self) { self.0.store(true, Relaxed); }
|
||||
}
|
||||
|
||||
/// Define a key
|
||||
pub const fn key (code: KeyCode) -> Event {
|
||||
let modifiers = KeyModifiers::NONE;
|
||||
let kind = KeyEventKind::Press;
|
||||
let state = KeyEventState::NONE;
|
||||
Event::Key(KeyEvent { code, modifiers, kind, state })
|
||||
}
|
||||
|
||||
/// Add Ctrl modifier to key
|
||||
pub const fn ctrl (event: Event) -> Event {
|
||||
match event {
|
||||
Event::Key(mut event) => {
|
||||
event.modifiers = event.modifiers.union(KeyModifiers::CONTROL)
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
event
|
||||
}
|
||||
|
||||
/// Add Alt modifier to key
|
||||
pub const fn alt (event: Event) -> Event {
|
||||
match event {
|
||||
Event::Key(mut event) => {
|
||||
event.modifiers = event.modifiers.union(KeyModifiers::ALT)
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
event
|
||||
}
|
||||
|
||||
/// Add Shift modifier to key
|
||||
pub const fn shift (event: Event) -> Event {
|
||||
match event {
|
||||
Event::Key(mut event) => {
|
||||
event.modifiers = event.modifiers.union(KeyModifiers::SHIFT)
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
event
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! kpat {
|
||||
(Ctrl-Alt-$code:pat) => { kpat!($code, KeyModifiers::CONTROL | KeyModifiers::ALT) };
|
||||
(Ctrl-$code:pat) => { kpat!($code, KeyModifiers::CONTROL) };
|
||||
(Alt-$code:pat) => { kpat!($code, KeyModifiers::ALT) };
|
||||
(Shift-$code:pat) => { kpat!($code, KeyModifiers::SHIFT) };
|
||||
($code:pat) => {
|
||||
crossterm::event::Event::Key(KeyEvent {
|
||||
code: $code,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
kind: KeyEventKind::Press,
|
||||
state: KeyEventState::NONE
|
||||
})
|
||||
};
|
||||
($code:pat, $modifiers: pat) => {
|
||||
crossterm::event::Event::Key(KeyEvent {
|
||||
code: $code,
|
||||
modifiers: $modifiers,
|
||||
kind: KeyEventKind::Press,
|
||||
state: KeyEventState::NONE
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! kexp {
|
||||
(Ctrl-Alt-$code:ident) => { key_event_expr!($code, KeyModifiers::from_bits(0b0000_0110).unwrap()) };
|
||||
(Ctrl-$code:ident) => { key_event_expr!($code, KeyModifiers::CONTROL) };
|
||||
(Alt-$code:ident) => { key_event_expr!($code, KeyModifiers::ALT) };
|
||||
(Shift-$code:ident) => { key_event_expr!($code, KeyModifiers::SHIFT) };
|
||||
($code:ident) => { key_event_expr!($code) };
|
||||
($code:expr) => { key_event_expr!($code) };
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! key_event_expr {
|
||||
($code:expr, $modifiers: expr) => {
|
||||
crossterm::event::Event::Key(KeyEvent {
|
||||
code: $code,
|
||||
modifiers: $modifiers,
|
||||
kind: KeyEventKind::Press,
|
||||
state: KeyEventState::NONE
|
||||
})
|
||||
};
|
||||
($code:expr) => {
|
||||
crossterm::event::Event::Key(KeyEvent {
|
||||
code: $code,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
kind: KeyEventKind::Press,
|
||||
state: KeyEventState::NONE
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
116
tui/src/tui_output.rs
Normal file
116
tui/src/tui_output.rs
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
use crate::*;
|
||||
|
||||
pub struct TuiOut {
|
||||
pub buffer: Buffer,
|
||||
pub area: [u16;4]
|
||||
}
|
||||
|
||||
impl Output<Tui> for TuiOut {
|
||||
#[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<Tui>) {
|
||||
let last = self.area();
|
||||
*self.area_mut() = area;
|
||||
content.render(self);
|
||||
*self.area_mut() = last;
|
||||
}
|
||||
}
|
||||
|
||||
impl TuiOut {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
impl Content<Tui> for &str {
|
||||
fn layout (&self, to: [u16;4]) -> [u16;4] {
|
||||
to.center_xy([self.chars().count() as u16, 1])
|
||||
}
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
to.blit(self, to.area.x(), to.area.y(), None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Content<Tui> for String {
|
||||
fn layout (&self, to: [u16;4]) -> [u16;4] {
|
||||
to.center_xy([self.chars().count() as u16, 1])
|
||||
}
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
to.blit(self, to.area.x(), to.area.y(), None)
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
//impl Area<u16> for Rect {
|
||||
//fn x (&self) -> u16 { self.x }
|
||||
//fn y (&self) -> u16 { self.y }
|
||||
//fn w (&self) -> u16 { self.width }
|
||||
//fn h (&self) -> u16 { self.height }
|
||||
//}
|
||||
|
||||
pub fn half_block (lower: bool, upper: bool) -> Option<char> {
|
||||
match (lower, upper) {
|
||||
(true, true) => Some('█'),
|
||||
(true, false) => Some('▄'),
|
||||
(false, true) => Some('▀'),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
|
||||
//impl<T: Content<Tui>> Render<Tui> for T {}
|
||||
74
tui/src/tui_run.rs
Normal file
74
tui/src/tui_run.rs
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
use crate::*;
|
||||
use ratatui::prelude::Size;
|
||||
use std::time::Duration;
|
||||
use std::thread::{spawn, JoinHandle};
|
||||
|
||||
pub trait TuiRun<R: Render<Tui> + Handle<Tui> + Sized + 'static> {
|
||||
/// Run an app in the main loop.
|
||||
fn run (&self, state: &Arc<RwLock<R>>) -> Usually<()>;
|
||||
/// Spawn the input thread.
|
||||
fn run_input (&self, state: &Arc<RwLock<R>>, poll: Duration) -> JoinHandle<()>;
|
||||
/// Spawn the output thread.
|
||||
fn run_output (&self, state: &Arc<RwLock<R>>, sleep: Duration) -> JoinHandle<()>;
|
||||
}
|
||||
|
||||
impl<T: Render<Tui> + Handle<Tui> + Sized + 'static> TuiRun<T> for Arc<RwLock<Tui>> {
|
||||
fn run (&self, state: &Arc<RwLock<T>>) -> Usually<()> {
|
||||
let _input_thread = self.run_input(state, Duration::from_millis(100));
|
||||
self.write().unwrap().setup()?;
|
||||
let render_thread = self.run_output(state, Duration::from_millis(10));
|
||||
render_thread.join().expect("main thread failed");
|
||||
self.write().unwrap().teardown()?;
|
||||
Ok(())
|
||||
}
|
||||
fn run_input (&self, state: &Arc<RwLock<T>>, poll: Duration) -> JoinHandle<()> {
|
||||
let exited = self.read().unwrap().exited.clone();
|
||||
let state = state.clone();
|
||||
spawn(move || loop {
|
||||
if exited.fetch_and(true, Relaxed) {
|
||||
break
|
||||
}
|
||||
if ::crossterm::event::poll(poll).is_ok() {
|
||||
let event = ::crossterm::event::read().unwrap();
|
||||
match event {
|
||||
kpat!(Ctrl-KeyCode::Char('c')) => {
|
||||
exited.store(true, Relaxed);
|
||||
},
|
||||
_ => {
|
||||
let exited = exited.clone();
|
||||
if let Err(e) = state.write().unwrap().handle(&TuiIn(exited, event)) {
|
||||
panic!("{e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
fn run_output (&self, state: &Arc<RwLock<T>>, sleep: Duration) -> JoinHandle<()> {
|
||||
let exited = self.read().unwrap().exited.clone();
|
||||
let engine = self.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 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);
|
||||
}
|
||||
std::thread::sleep(sleep);
|
||||
})
|
||||
}
|
||||
}
|
||||
86
tui/src/tui_style.rs
Normal file
86
tui/src/tui_style.rs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait TuiStyle {
|
||||
fn fg <R: Content<Tui>> (color: Color, w: R) -> Foreground<R> {
|
||||
Foreground(color, w)
|
||||
}
|
||||
fn bg <R: Content<Tui>> (color: Color, w: R) -> Background<R> {
|
||||
Background(color, w)
|
||||
}
|
||||
fn fg_bg <R: Content<Tui>> (fg: Color, bg: Color, w: R) -> Background<Foreground<R>> {
|
||||
Background(bg, Foreground(fg, w))
|
||||
}
|
||||
fn bold <R: Content<Tui>> (on: bool, w: R) -> Bold<R> {
|
||||
Bold(on, w)
|
||||
}
|
||||
fn border <R: Content<Tui>, S: BorderStyle> (style: S, w: R) -> Bordered<S, R> {
|
||||
Bordered(style, w)
|
||||
}
|
||||
}
|
||||
|
||||
impl TuiStyle for Tui {}
|
||||
|
||||
pub struct Bold<R: Content<Tui>>(pub bool, R);
|
||||
impl<R: Content<Tui>> Content<Tui> for Bold<R> {
|
||||
fn content (&self) -> impl Render<Tui> { &self.1 }
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
to.fill_bold(to.area(), self.0);
|
||||
self.1.render(to)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Foreground<R: Content<Tui>>(pub Color, R);
|
||||
impl<R: Content<Tui>> Content<Tui> for Foreground<R> {
|
||||
fn content (&self) -> impl Render<Tui> { &self.1 }
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
to.fill_fg(to.area(), self.0);
|
||||
self.1.render(to)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Background<R: Content<Tui>>(pub Color, R);
|
||||
impl<R: Content<Tui>> Content<Tui> for Background<R> {
|
||||
fn content (&self) -> impl Render<Tui> { &self.1 }
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
to.fill_bg(to.area(), self.0);
|
||||
self.1.render(to)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Styled<R: Content<Tui>>(pub Option<Style>, pub R);
|
||||
impl Content<Tui> for Styled<&str> {
|
||||
fn content (&self) -> impl Render<Tui> { &self.1 }
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
// FIXME
|
||||
let [x, y, ..] = to.area();
|
||||
//let [w, h] = self.min_size(to.area().wh())?.unwrap();
|
||||
to.blit(&self.1, x, y, None)
|
||||
}
|
||||
}
|
||||
|
||||
//pub trait TuiStyle: Render<Tui> + Sized {
|
||||
//fn fg (self, color: Color) -> impl Render<Tui> {
|
||||
//Layers::new(move |add|{ add(&Foreground(color))?; add(&self) })
|
||||
//}
|
||||
//fn bg (self, color: Color) -> impl Render<Tui> {
|
||||
//Layers::new(move |add|{ add(&Background(color))?; add(&self) })
|
||||
//}
|
||||
//fn bold (self, on: bool) -> impl Render<Tui> {
|
||||
//Layers::new(move |add|{ add(&Bold(on))?; add(&self) })
|
||||
//}
|
||||
//fn border <S: BorderStyle> (self, style: S) -> impl Render<Tui> {
|
||||
//Bordered(style, self)
|
||||
//}
|
||||
//}
|
||||
|
||||
//impl<R: Content<Tui>> TuiStyle for R {}
|
||||
|
||||
//impl<S: BorderStyle> Content<Tui> for Border<S> {
|
||||
//}
|
||||
|
||||
//impl<S: BorderStyle, R: Content<Tui>> Content<Tui> for Bordered<S, R> {
|
||||
//fn content (&self) -> impl Render<Tui> {
|
||||
//let content: &dyn Content<Tui> = &self.1;
|
||||
//lay! { content.padding_xy(1, 1), Border(self.0) }.fill_xy()
|
||||
//}
|
||||
//}
|
||||
61
tui/src/tui_theme.rs
Normal file
61
tui/src/tui_theme.rs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Copy,Clone)]
|
||||
pub struct TuiTheme;
|
||||
|
||||
impl Theme for TuiTheme {}
|
||||
|
||||
pub trait Theme {
|
||||
const HOTKEY_FG: Color = Color::Rgb(255, 255, 0);
|
||||
fn null () -> Color {
|
||||
Color::Reset
|
||||
}
|
||||
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 (focused: bool) -> Color {
|
||||
if focused { Self::bo1() } else { Self::bo2() }
|
||||
}
|
||||
fn title_fg (focused: bool) -> Color {
|
||||
if focused { 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)
|
||||
}
|
||||
fn orange () -> Color {
|
||||
Color::Rgb(255,128,0)
|
||||
}
|
||||
fn yellow () -> Color {
|
||||
Color::Rgb(255,255,0)
|
||||
}
|
||||
fn g (g: u8) -> Color {
|
||||
Color::Rgb(g, g, g)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue