tek/crates/tek_core/src/tui.rs

654 lines
20 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
};
pub struct Tui {
exited: Arc<AtomicBool>,
buffer: Buffer,
backend: CrosstermBackend<Stdout>,
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 = TuiInput;
type Handled = bool;
type Output = TuiOutput;
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();
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 {
/// 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 engine = Self {
exited: Arc::new(AtomicBool::new(false)),
buffer: Buffer::empty(area),
area: area.xywh(),
backend,
};
let engine = Arc::new(RwLock::new(engine));
let _input_thread = Self::spawn_input_thread(&engine, &state, Duration::from_millis(100));
engine.write().unwrap().setup()?;
let render_thread = Self::spawn_render_thread(&engine, &state, Duration::from_millis(10));
render_thread.join().expect("main thread failed");
engine.write().unwrap().teardown()?;
Ok(state)
}
fn spawn_input_thread <R: Component<Tui> + Sized + 'static> (
engine: &Arc<RwLock<Self>>, state: &Arc<RwLock<R>>, poll: Duration
) -> JoinHandle<()> {
let exited = engine.read().unwrap().exited.clone();
let state = state.clone();
spawn(move || loop {
if exited.fetch_and(true, Ordering::Relaxed) {
break
}
if ::crossterm::event::poll(poll).is_ok() {
let event = TuiEvent::Input(::crossterm::event::read().unwrap());
match event {
key!(Ctrl-KeyCode::Char('c')) => {
exited.store(true, Ordering::Relaxed);
},
_ => {
let exited = exited.clone();
if let Err(e) = state.write().unwrap().handle(&TuiInput { event, exited }) {
panic!("{e}")
}
}
}
}
})
}
fn spawn_render_thread <R: Component<Tui> + Sized + 'static> (
engine: &Arc<RwLock<Self>>, state: &Arc<RwLock<R>>, sleep: Duration
) -> JoinHandle<()> {
let exited = engine.read().unwrap().exited.clone();
let engine = engine.clone();
let state = state.clone();
let mut buffer = Buffer::empty(
engine.read().unwrap().backend.size().expect("get size failed")
);
spawn(move || loop {
if exited.fetch_and(true, Ordering::Relaxed) {
break
}
let size = engine.read().unwrap().backend.size().expect("get size failed");
if let Ok(state) = state.try_read() {
if buffer.area != size {
buffer.resize(size);
}
let mut output = TuiOutput { buffer, area: size.xywh() };
state.render(&mut output).expect("render failed");
buffer = engine.write().unwrap().flip(output.buffer, size);
}
std::thread::sleep(sleep);
})
}
fn flip (&mut self, mut buffer: Buffer, size: ratatui::prelude::Rect) -> Buffer {
if self.buffer.area != size {
self.buffer.resize(size);
}
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
}
}
pub struct TuiInput {
event: TuiEvent,
exited: Arc<AtomicBool>,
}
impl Input<Tui> for TuiInput {
type Event = TuiEvent;
fn event (&self) -> &TuiEvent {
&self.event
}
fn is_done (&self) -> bool {
self.exited.fetch_and(true, Ordering::Relaxed)
}
fn done (&self) {
self.exited.store(true, Ordering::Relaxed);
}
}
pub struct TuiOutput {
pub buffer: Buffer,
pub area: [u16;4],
}
impl Output<Tui> for TuiOutput {
#[inline] fn area (&self) -> [u16;4] {
self.area
}
#[inline] fn area_mut (&mut self) -> &mut [u16;4] {
&mut self.area
}
#[inline] fn render_in (&mut self,
area: [u16;4],
widget: &dyn Widget<Engine = Tui>
) -> Usually<()> {
let last = self.area();
*self.area_mut() = area;
widget.render(self)?;
*self.area_mut() = last;
Ok(())
}
}
impl TuiOutput {
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_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
}
}
#[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)
}
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
}
}
#[derive(Default)]
pub struct BigBuffer {
pub width: usize,
pub height: usize,
pub content: Vec<Cell>
}
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
}
}
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 Widget for &str {
type Engine = Tui;
fn layout (&self, _: [u16;2]) -> Perhaps<[u16;2]> {
// TODO: line breaks
Ok(Some([self.chars().count() as u16, 1]))
}
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
let [x, y, ..] = to.area();
//let [w, h] = self.layout(to.area().wh())?.unwrap();
Ok(to.blit(&self, x, y, None))
}
}
pub struct Styled<T: Widget<Engine = Tui>>(pub Option<Style>, pub T);
impl Widget for Styled<&str> {
type Engine = Tui;
fn layout (&self, _: [u16;2]) -> Perhaps<[u16;2]> {
Ok(Some([self.1.chars().count() as u16, 1]))
}
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
// FIXME
let [x, y, ..] = to.area();
//let [w, h] = self.layout(to.area().wh())?.unwrap();
Ok(to.blit(&self.1, x, y, None))
}
}
pub struct Background(pub Color);
impl Widget for Background {
type Engine = Tui;
fn layout (&self, _: [u16;2]) -> Perhaps<[u16;2]> {
Ok(Some([0,0]))
}
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
Ok(to.fill_bg(to.area(), self.0))
}
}
//impl<F> Widget for Layers<Tui, F>
//where
//F: Send + Sync + Fn(&mut dyn FnMut(&dyn Widget<Engine = Tui>)->Usually<()>)->Usually<()>
//{
//type Engine = Tui;
//fn layout (&self, area: [u16;2]) -> Perhaps<[u16;2]> {
//let mut w = 0;
//let mut h = 0;
//(self.0)(&mut |layer| {
//if let Some(layer_area) = layer.layout(area)? {
//w = w.max(layer_area.w());
//h = h.max(layer_area.h());
//}
//Ok(())
//})?;
//Ok(Some([w, h]))
//}
//fn render (&self, to: &mut TuiOutput) -> Usually<()> {
//if let Some(size) = self.layout(to.area().wh())? {
//(self.0)(&mut |layer|to.render_in(to.area().clip(size), &layer))
//} else {
//Ok(())
//}
//}
//}
pub struct Border<S: BorderStyle>(pub S);
impl<S: BorderStyle> Widget for Border<S> {
type Engine = Tui;
fn layout (&self, _: [u16;2]) -> Perhaps<[u16;2]> {
Ok(Some([0, 0]))
}
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
let area = to.area();
if area.w() > 2 && area.y() > 2 {
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());
}
}
Ok(())
}
}
pub trait BorderStyle: Send + Sync {
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 = "";
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 TuiOutput) -> Usually<()> {
self.draw_horizontal(to, None)?;
self.draw_vertical(to, None)?;
self.draw_corners(to, None)?;
Ok(())
}
#[inline]
fn draw_horizontal (&self, to: &mut TuiOutput, 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) {
self.draw_north(to, x, y, style);
self.draw_south(to, x, y2.saturating_sub(1), style);
}
Ok(area)
}
#[inline]
fn draw_north (&self, to: &mut TuiOutput, x: u16, y: u16, style: Option<Style>) -> () {
to.blit(&Self::N, x, y, style)
}
#[inline]
fn draw_south (&self, to: &mut TuiOutput, x: u16, y: u16, style: Option<Style>) -> () {
to.blit(&Self::S, x, y, style)
}
#[inline]
fn draw_vertical (&self, to: &mut TuiOutput, 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();
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);
}
Ok(area)
}
#[inline]
fn draw_corners (&self, to: &mut TuiOutput, 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 > 0 && height > 0 {
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)*
}
pub struct $T(pub Style);
impl Widget for $T {
type Engine = Tui;
fn layout (&self, _: [u16;2]) -> Perhaps<[u16;2]> {
Ok(Some([0,0]))
}
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
self.draw(to)
}
}
)+}
}
border! {
Square {
"" "" ""
"" ""
"" "" ""
fn style (&self) -> Option<Style> { Some(self.0) }
},
SquareBold {
"" "" ""
"" ""
"" "" ""
fn style (&self) -> Option<Style> { Some(self.0) }
},
Tab {
"" "" ""
"" ""
"" " " ""
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) }
}
}
pub const COLOR_BG0: Color = Color::Rgb(30, 33, 36);
pub const COLOR_BG1: Color = Color::Rgb(41, 46, 57);
pub const COLOR_BG2: Color = Color::Rgb(46, 52, 64);
pub const COLOR_BG3: Color = Color::Rgb(59, 66, 82);
pub const COLOR_BG4: Color = Color::Rgb(67, 76, 94);
pub const COLOR_BG5: Color = Color::Rgb(76, 86, 106);
pub trait Theme {
const BG0: Color;
const BG1: Color;
const BG2: Color;
const BG3: Color;
const BG4: Color;
const RED: Color;
const YELLOW: Color;
const GREEN: Color;
const PLAYING: Color;
const SEPARATOR: Color;
fn bg_hier (focused: bool, entered: bool) -> Color {
if focused && entered {
Self::BG3
} else if focused {
Self::BG2
} else {
Self::BG1
}
}
fn bg_hi (focused: bool, entered: bool) -> Color {
if focused && entered {
Self::BG2
} else if focused {
Self::BG1
} else {
Self::BG0
}
}
fn bg_lo (focused: bool, entered: bool) -> Color {
if focused && entered {
Self::BG1
} else if focused {
Self::BG0
} else {
Color::Reset
}
}
fn style_hi (focused: bool, highlight: bool) -> Style {
if highlight && focused {
Style::default().yellow().not_dim()
} else if highlight {
Style::default().yellow().dim()
} else {
Style::default()
}
}
}
pub struct Nord;
impl Theme for Nord {
const BG0: Color = Color::Rgb(41, 46, 57);
const BG1: Color = Color::Rgb(46, 52, 64);
const BG2: Color = Color::Rgb(59, 66, 82);
const BG3: Color = Color::Rgb(67, 76, 94);
const BG4: Color = Color::Rgb(76, 86, 106);
const RED: Color = Color::Rgb(191, 97, 106);
const YELLOW: Color = Color::Rgb(235, 203, 139);
const GREEN: Color = Color::Rgb(163, 190, 140);
const PLAYING: Color = Color::Rgb(60, 100, 50);
const SEPARATOR: Color = Color::Rgb(0, 0, 0);
}