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
22
Cargo.lock
generated
22
Cargo.lock
generated
|
|
@ -1419,8 +1419,7 @@ dependencies = [
|
||||||
"quanta",
|
"quanta",
|
||||||
"rand",
|
"rand",
|
||||||
"symphonia",
|
"symphonia",
|
||||||
"tek_edn",
|
"tek_tui",
|
||||||
"tek_layout",
|
|
||||||
"toml",
|
"toml",
|
||||||
"uuid",
|
"uuid",
|
||||||
"wavers",
|
"wavers",
|
||||||
|
|
@ -1439,11 +1438,6 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tek_engine"
|
name = "tek_engine"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
|
||||||
"better-panic",
|
|
||||||
"crossterm",
|
|
||||||
"ratatui",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tek_layout"
|
name = "tek_layout"
|
||||||
|
|
@ -1452,6 +1446,20 @@ dependencies = [
|
||||||
"tek_engine",
|
"tek_engine",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tek_tui"
|
||||||
|
version = "0.2.0"
|
||||||
|
dependencies = [
|
||||||
|
"better-panic",
|
||||||
|
"crossterm",
|
||||||
|
"palette",
|
||||||
|
"rand",
|
||||||
|
"ratatui",
|
||||||
|
"tek_edn",
|
||||||
|
"tek_engine",
|
||||||
|
"tek_layout",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.69"
|
version = "1.0.69"
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,7 @@ edition = "2021"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tek_layout = { path = "./layout" }
|
tek_tui = { path = "./tui" }
|
||||||
tek_edn = { optional = true, path = "./edn" }
|
|
||||||
|
|
||||||
atomic_float = "1.0.0"
|
atomic_float = "1.0.0"
|
||||||
backtrace = "0.3.72"
|
backtrace = "0.3.72"
|
||||||
|
|
@ -29,7 +28,7 @@ wavers = "1.4.3"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["edn"]
|
default = ["edn"]
|
||||||
edn = ["tek_edn"]
|
edn = ["tek_tui/edn"]
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "tek_arranger"
|
name = "tek_arranger"
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@
|
||||||
*,
|
*,
|
||||||
jack::*,
|
jack::*,
|
||||||
tek_layout::Measure,
|
tek_layout::Measure,
|
||||||
tek_engine::{Usually, tui::{Tui, TuiRun, ratatui::prelude::Color}}
|
tek_engine::Usually,
|
||||||
|
tek_tui::{Tui, TuiRun, ItemPalette, ItemColor, ratatui::prelude::Color}
|
||||||
};
|
};
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,3 @@ edition = "2021"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
crossterm = "0.28.1"
|
|
||||||
ratatui = { version = "0.29.0", features = [ "unstable-widget-ref", "underline-color" ] }
|
|
||||||
better-panic = "0.3.0"
|
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ mod engine; pub use self::engine::*;
|
||||||
mod input; pub use self::input::*;
|
mod input; pub use self::input::*;
|
||||||
mod output; pub use self::output::*;
|
mod output; pub use self::output::*;
|
||||||
|
|
||||||
pub mod tui;
|
|
||||||
|
|
||||||
pub use std::error::Error;
|
pub use std::error::Error;
|
||||||
|
|
||||||
/// Standard result type.
|
/// Standard result type.
|
||||||
|
|
@ -15,6 +13,16 @@ pub type Usually<T> = Result<T, Box<dyn Error>>;
|
||||||
/// Standard optional result type.
|
/// Standard optional result type.
|
||||||
pub type Perhaps<T> = Result<Option<T>, Box<dyn Error>>;
|
pub type Perhaps<T> = Result<Option<T>, Box<dyn Error>>;
|
||||||
|
|
||||||
|
/// Prototypal case of implementor macro.
|
||||||
|
/// Saves 4loc per data pats.
|
||||||
|
#[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_dimensions () {
|
#[cfg(test)] #[test] fn test_dimensions () {
|
||||||
assert_eq!(Area::center(&[10u16, 10, 20, 20]), [20, 20]);
|
assert_eq!(Area::center(&[10u16, 10, 20, 20]), [20, 20]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
use std::fmt::{Debug, Display};
|
use std::fmt::{Debug, Display};
|
||||||
use std::ops::{Add, Sub, Mul, Div};
|
use std::ops::{Add, Sub, Mul, Div};
|
||||||
|
|
||||||
|
impl Coordinate for u16 {}
|
||||||
|
|
||||||
/// A linear coordinate.
|
/// A linear coordinate.
|
||||||
pub trait Coordinate: Send + Sync + Copy
|
pub trait Coordinate: Send + Sync + Copy
|
||||||
+ Add<Self, Output=Self>
|
+ Add<Self, Output=Self>
|
||||||
|
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
mod tui_output; pub use self::tui_output::*;
|
|
||||||
mod tui_input; pub use self::tui_input::*;
|
|
||||||
mod tui_run; pub use self::tui_run::*;
|
|
||||||
|
|
||||||
use crate::*;
|
|
||||||
use std::sync::{Arc, RwLock, atomic::{AtomicBool, Ordering::*}};
|
|
||||||
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
|
|
||||||
};
|
|
||||||
|
|
||||||
impl Coordinate for u16 {}
|
|
||||||
|
|
||||||
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.
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,105 +0,0 @@
|
||||||
use crate::{*, tui::*};
|
|
||||||
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
|
|
||||||
})
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
use crate::{*, tui::*};
|
|
||||||
|
|
||||||
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 {}
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
use crate::{*, tui::*};
|
|
||||||
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);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
66
src/lib.rs
66
src/lib.rs
|
|
@ -5,39 +5,34 @@
|
||||||
#![feature(impl_trait_in_assoc_type)]
|
#![feature(impl_trait_in_assoc_type)]
|
||||||
#![feature(associated_type_defaults)]
|
#![feature(associated_type_defaults)]
|
||||||
|
|
||||||
pub use ::tek_layout;
|
pub use ::tek_tui::{self, tek_engine, tek_layout};
|
||||||
pub use ::tek_layout::tek_engine;
|
pub(crate) use ::tek_tui::{
|
||||||
|
|
||||||
pub(crate) use ::tek_layout::{
|
|
||||||
*,
|
*,
|
||||||
|
tek_edn::*,
|
||||||
|
tek_layout::*,
|
||||||
tek_engine::{
|
tek_engine::{
|
||||||
|
from,
|
||||||
Usually, Perhaps,
|
Usually, Perhaps,
|
||||||
Output, Content, Render, Thunk, render, Engine, Size, Area,
|
Output, Content, Render, Thunk, render, Engine, Size, Area,
|
||||||
Input, handle, Handle, command, Command, input_to_command, InputToCommand,
|
Input, handle, Handle, command, Command, input_to_command, InputToCommand, keymap, EventMap,
|
||||||
keymap, kexp, kpat, EventMap,
|
},
|
||||||
tui::{
|
Tui,
|
||||||
Tui,
|
TuiIn, key, ctrl, shift, alt, kexp, kpat,
|
||||||
TuiIn, key, ctrl, shift, alt,
|
TuiOut,
|
||||||
TuiOut,
|
crossterm::{
|
||||||
crossterm::{
|
self,
|
||||||
self,
|
event::{
|
||||||
event::{
|
Event, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers,
|
||||||
Event, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers,
|
KeyCode::{self, *},
|
||||||
KeyCode::{self, *},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ratatui::{
|
|
||||||
self,
|
|
||||||
prelude::{Color, Style, Stylize, Buffer, Modifier},
|
|
||||||
buffer::Cell,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
ratatui::{
|
||||||
|
self,
|
||||||
|
prelude::{Color, Style, Stylize, Buffer, Modifier},
|
||||||
|
buffer::Cell,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use ::tek_edn;
|
|
||||||
pub(crate) use ::tek_edn::*;
|
|
||||||
|
|
||||||
pub(crate) use std::cmp::{Ord, Eq, PartialEq};
|
pub(crate) use std::cmp::{Ord, Eq, PartialEq};
|
||||||
pub(crate) use std::collections::BTreeMap;
|
pub(crate) use std::collections::BTreeMap;
|
||||||
pub(crate) use std::error::Error;
|
pub(crate) use std::error::Error;
|
||||||
|
|
@ -53,9 +48,7 @@ pub(crate) use std::thread::{spawn, JoinHandle};
|
||||||
pub(crate) use std::time::Duration;
|
pub(crate) use std::time::Duration;
|
||||||
|
|
||||||
pub mod arranger; pub use self::arranger::*;
|
pub mod arranger; pub use self::arranger::*;
|
||||||
pub mod border; pub use self::border::*;
|
|
||||||
pub mod clock; pub use self::clock::*;
|
pub mod clock; pub use self::clock::*;
|
||||||
pub mod color; pub use self::color::*;
|
|
||||||
pub mod field; pub use self::field::*;
|
pub mod field; pub use self::field::*;
|
||||||
pub mod file; pub use self::file::*;
|
pub mod file; pub use self::file::*;
|
||||||
pub mod focus; pub use self::focus::*;
|
pub mod focus; pub use self::focus::*;
|
||||||
|
|
@ -70,8 +63,6 @@ pub mod pool; pub use self::pool::*;
|
||||||
pub mod sampler; pub use self::sampler::*;
|
pub mod sampler; pub use self::sampler::*;
|
||||||
pub mod sequencer; pub use self::sequencer::*;
|
pub mod sequencer; pub use self::sequencer::*;
|
||||||
pub mod status; pub use self::status::*;
|
pub mod status; pub use self::status::*;
|
||||||
pub mod style; pub use self::style::*;
|
|
||||||
pub mod theme; pub use self::theme::*;
|
|
||||||
|
|
||||||
pub use ::atomic_float;
|
pub use ::atomic_float;
|
||||||
pub(crate) use atomic_float::*;
|
pub(crate) use atomic_float::*;
|
||||||
|
|
@ -84,13 +75,6 @@ pub(crate) use ::midly::{
|
||||||
live::LiveEvent,
|
live::LiveEvent,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use ::palette;
|
|
||||||
pub(crate) use ::palette::{
|
|
||||||
*,
|
|
||||||
convert::*,
|
|
||||||
okhsl::*
|
|
||||||
};
|
|
||||||
|
|
||||||
testmod! { test }
|
testmod! { test }
|
||||||
|
|
||||||
/// Define test modules.
|
/// Define test modules.
|
||||||
|
|
@ -98,16 +82,6 @@ testmod! { test }
|
||||||
($($name:ident)*) => { $(#[cfg(test)] mod $name;)* };
|
($($name:ident)*) => { $(#[cfg(test)] mod $name;)* };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prototypal case of implementor macro.
|
|
||||||
/// Saves 4loc per data pats.
|
|
||||||
#[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 }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Gettable<T> {
|
pub trait Gettable<T> {
|
||||||
/// Returns current value
|
/// Returns current value
|
||||||
fn get (&self) -> T;
|
fn get (&self) -> T;
|
||||||
|
|
|
||||||
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::*
|
||||||
|
};
|
||||||
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);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue