tek/crates/tek_core/src/tui.rs

222 lines
7.6 KiB
Rust

use crate::*;
pub(crate) use ratatui::buffer::Cell;
pub(crate) use crossterm::{ExecutableCommand};
pub use crossterm::event::{Event, KeyEvent, KeyCode, KeyModifiers};
pub use ratatui::prelude::{Rect, Style, Color, Buffer};
pub use ratatui::style::{Stylize, Modifier};
use ratatui::backend::{Backend, CrosstermBackend};
use std::io::Stdout;
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen,
enable_raw_mode, disable_raw_mode
};
submod! { tui_border tui_buffer tui_colors tui_layout }
pub struct Tui {
exited: Arc<AtomicBool>,
buffer: usize,
buffers: [Buffer;2],
backend: CrosstermBackend<Stdout>,
event: RwLock<Option<TuiEvent>>,
area: [u16;4],
}
impl Engine for Tui {
type Unit = u16;
type Area = [Self::Unit;4];
type HandleInput = Self;
type Handled = bool;
type RenderInput = Self;
type Rendered = Self::Area;
fn exited (&self) -> bool {
self.exited.fetch_and(true, Ordering::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::PanicInfo|{
stdout().execute(LeaveAlternateScreen).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)
}
// FIXME
fn area (&self) -> Self::Area {
self.area
}
#[inline]
fn with_area (&mut self, x: u16, y: u16, w: u16, h: u16) -> &mut Self {
self.with_rect([x, y, w, h]);
self
}
}
impl Tui {
/// Run the main loop.
pub fn run <R: Component<Tui> + Sized + 'static> (
state: Arc<RwLock<R>>
) -> Usually<Arc<RwLock<R>>> {
let backend = CrosstermBackend::new(stdout());
let area = backend.size()?;
let mut engine = Self {
exited: Arc::new(AtomicBool::new(false)),
event: RwLock::new(None),
buffer: 0,
buffers: [Buffer::empty(area), Buffer::empty(area)],
backend,
area: area.xywh(),
};
engine.setup()?;
let engine = Arc::new(RwLock::new(engine));
let _input_thread = {
let engine = engine.clone();
let state = state.clone();
let poll = Duration::from_millis(100);
spawn(move || loop {
if ::crossterm::event::poll(poll).is_ok() {
let event = TuiEvent::Input(::crossterm::event::read().unwrap());
match event {
key!(Ctrl-KeyCode::Char('c')) => {
engine.write().unwrap().exited.store(true, Ordering::Relaxed);
},
_ => {
*engine.write().unwrap().event.write().unwrap() = Some(event);
if let Err(e) = state.write().unwrap().handle(&*engine.read().unwrap()) {
panic!("{e}")
}
}
}
}
if engine.read().unwrap().exited() {
break
}
//engine.read().unwrap().handle(&mut *state.write().unwrap()).expect("handle failed");
})
};
let main_thread = {
let engine = engine.clone();
let state = state.clone();
let sleep = Duration::from_millis(20);
spawn(move || loop {
if let (Ok(mut engine), Ok(state)) = (engine.write(), state.try_read()) {
if engine.exited() {
break
}
engine.area = engine.backend.size().expect("get size failed").xywh();
state.render(&mut engine).expect("render failed");
engine.flip();
}
std::thread::sleep(sleep);
})
};
main_thread.join().expect("main thread failed");
engine.write().unwrap().teardown()?;
Ok(state)
}
pub fn event (&self) -> TuiEvent {
self.event.read().unwrap().clone().unwrap()
}
pub fn buffer (&mut self) -> &mut Buffer {
&mut self.buffers[self.buffer]
}
fn flip (&mut self) {
let previous_buffer = &self.buffers[1 - self.buffer];
let current_buffer = &self.buffers[self.buffer];
let updates = previous_buffer.diff(current_buffer);
self.backend.draw(updates.into_iter()).expect("failed to render");
self.buffers[1 - self.buffer].reset();
self.buffer = 1 - self.buffer;
}
pub fn buffer_update (&mut self, area: [u16;4], callback: &impl Fn(&mut Cell, u16, u16)) {
buffer_update(self.buffer(), area, callback)
}
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>
) -> Perhaps<[u16;4]> {
let text = text.as_ref();
let buf = self.buffer();
if x < buf.area.width && y < buf.area.height {
buf.set_string(x, y, text, style.unwrap_or(Style::default()));
}
Ok(Some([x, y, text.len() as u16, 1]))
}
#[inline]
pub fn alter_area (
&mut self, alter: impl Fn([u16;4])->[u16;4]
) -> &mut Self {
let [x, y, w, h] = alter(self.area.xywh());
self.with_area(x, y, w, h)
}
#[inline]
pub fn with_rect (&mut self, area: [u16;4]) -> &mut Self {
self.area = area;
self
}
}
#[derive(Debug, Clone)]
pub enum TuiEvent {
/// Terminal input
Input(::crossterm::event::Event),
/// Update values but not the whole form.
Update,
/// Update the whole form.
Redraw,
/// Device gains focus
Focus,
/// Device loses focus
Blur,
// /// JACK notification
// Jack(JackEvent)
}
/// Rendering unit struct to Ratatui returns zero-sized [Area] at render coordinates.
impl Render<Tui> for () {
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
Ok(Some([to.area.x(), to.area.y(), 0, 0]))
}
}
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
}
}