wip: slowly putting it back together

This commit is contained in:
🪞👃🪞 2024-09-04 22:39:43 +03:00
parent 7fbb40fad6
commit 461c60d6b3
18 changed files with 788 additions and 774 deletions

549
crates/tek_core/src/tui.rs Normal file
View file

@ -0,0 +1,549 @@
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 TuiContext {
exited: Arc<AtomicBool>,
buffer: usize,
buffers: [Buffer;2],
backend: CrosstermBackend<Stdout>,
area: Rect,
sleep: Duration,
poll: Duration,
}
impl Engine for TuiContext {
type Handled = bool;
type Rendered = Rect;
fn exited (&self) -> bool {
self.exited.fetch_and(true, Ordering::Relaxed)
}
fn setup (&mut self) -> Usually<()> {
panic_hook_setup();
terminal_setup()
}
fn teardown (&mut self) -> Usually<()> {
terminal_teardown()
}
fn handle (&mut self, state: &mut impl Handle<Self, bool>) -> Usually<()> {
if ::crossterm::event::poll(self.poll).is_ok() {
let event = ::crossterm::event::read().unwrap();
if let Event::Key(KeyEvent {
code: KeyCode::Char('c'), modifiers: KeyModifiers::CONTROL, ..
}) = event {
self.exited.store(true, Ordering::Relaxed);
} else if let Err(e) = state.handle(&mut self) {
panic!("{e}")
}
}
Ok(())
}
fn render (&mut self, state: &impl Render<Self, Rect>) -> Usually<()> {
if let Ok(state) = state.try_read() {
state.render(&mut self).expect("render failed");
}
std::thread::sleep(self.sleep)
}
}
impl TuiContext {
/// Run the main loop.
pub fn run <R: Component<TuiContext> + Sized + 'static> (
state: Arc<RwLock<R>>
) -> Usually<Arc<RwLock<R>>> {
let backend = CrosstermBackend::new(stdout());
let area = backend.size()?;
let engine = Arc::new(Self {
sleep: Duration::from_millis(20),
poll: Duration::from_millis(100),
exited: Arc::new(AtomicBool::new(false)),
buffer: 0,
buffers: [Buffer::empty(area), Buffer::empty(area)],
backend,
area,
});
let _input_thread = {
let engine = engine.clone();
let state = state.clone();
spawn(move || loop {
if engine.exited() {
break
}
engine.handle(&mut state).expect("handle failed");
})
};
let main_thread = {
let engine = engine.clone();
let state = state.clone();
spawn(move || loop {
if engine.exited() {
break
}
engine.render(&mut state).expect("render failed");
})
};
main_thread.join().expect("main thread failed");
Ok(state)
}
pub fn target <'a> (&'a mut self) -> impl TuiTarget + 'a {
let area = self.area();
(self.buffer(), area)
}
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 fill_bg (&mut self, area: Rect, color: Color) {
self.buffer_update(area, &|cell,_,_|{cell.set_bg(color);})
}
pub fn fill_fg (&mut self, area: Rect, color: Color) {
self.buffer_update(area, &|cell,_,_|{cell.set_fg(color);})
}
pub fn fill_ul (&mut self, area: Rect, color: Color) {
self.buffer_update(area, &|cell,_,_|{
cell.modifier = ratatui::prelude::Modifier::UNDERLINED;
cell.underline_color = color;
})
}
pub fn fill_char (&mut self, area: Rect, 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 buffer_update (&mut self, area: Rect, callback: &impl Fn(&mut Cell, u16, u16)) {
let buf = self.buffer();
for row in 0..area.height {
let y = area.y + row;
for col in 0..area.width {
let x = area.x + col;
if x < buf.area.width && y < buf.area.height {
callback(buf.get_mut(x, y), col, row);
}
}
}
}
}
pub trait TuiTarget {
fn area (&self) -> Rect;
fn buffer (&mut self) -> &mut Buffer;
}
impl TuiTarget for TuiContext {
fn area (&self) -> Rect {
self.area
}
fn buffer (&mut self) -> &mut Buffer {
&mut self.buffers[self.buffer]
}
}
impl<'a> TuiTarget for (&'a mut Buffer, Rect) {
fn area (&self) -> Rect {
self.1
}
fn buffer (&mut self) -> &mut Buffer {
self.0
}
}
/// Set up panic hook
pub fn panic_hook_setup () {
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);
}));
}
/// Set up terminal
pub fn terminal_setup () -> Usually<()> {
stdout().execute(EnterAlternateScreen)?;
enable_raw_mode()?;
Ok(())
}
/// Cleanup
pub fn terminal_teardown () -> Usually<()> {
stdout().execute(LeaveAlternateScreen)?;
disable_raw_mode()?;
Ok(())
}
/// A simpler analog to [Render].
pub trait Blit {
// Render something to X, Y coordinates in a buffer, ignoring width/height.
fn blit (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Perhaps<Rect>;
}
/// Text can be rendered.
impl<T: AsRef<str>> Blit for T {
fn blit (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Perhaps<Rect> {
if x < buf.area.width && y < buf.area.height {
buf.set_string(x, y, self.as_ref(), style.unwrap_or(Style::default()));
}
Ok(Some(Rect { x, y, width: self.as_ref().len() as u16, height: 1 }))
}
}
/// Rendering unit struct to Ratatui returns zero-sized [Rect] at render coordinates.
impl Render<TuiContext, Rect> for () {
fn render (&self, to: &mut TuiContext) -> Perhaps<Rect> {
Ok(Some(Rect { x: to.area.x, y: to.area.y, width: 0, height: 0 }))
}
}
pub fn center_box (area: Rect, w: u16, h: u16) -> Rect {
let width = w.min(area.width * 3 / 5);
let height = h.min(area.width * 3 / 5);
let x = area.x + (area.width - width) / 2;
let y = area.y + (area.height - height) / 2;
Rect { x, y, width, height }
}
pub fn half_block (lower: bool, upper: bool) -> Option<char> {
match (lower, upper) {
(true, true) => Some('█'),
(true, false) => Some('▄'),
(false, true) => Some('▀'),
_ => None
}
}
pub struct FillBg(pub Color);
impl Render<TuiContext, Rect> for FillBg {
fn render (&self, to: &mut TuiContext) -> Perhaps<Rect> {
to.fill_bg(to.area, self.0);
Ok(Some(to.area))
}
}
pub trait BorderStyle {
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 = "";
#[inline]
fn draw <'a> (&self, to: &mut TuiOutput<'a>) -> Perhaps<Rect> {
self.draw_horizontal(to.buffer, to.area, None)?;
self.draw_vertical(to.buffer, to.area, None)?;
self.draw_corners(to.buffer, to.area, None)?;
Ok(Some(to.area))
}
#[inline]
fn draw_horizontal (&self, buf: &mut Buffer, area: Rect, style: Option<Style>) -> Usually<Rect> {
let style = style.or_else(||self.style_horizontal());
for x in area.x..(area.x+area.width).saturating_sub(1) {
self.draw_north(buf, x, area.y, style)?;
self.draw_south(buf, x, (area.y + area.height).saturating_sub(1), style)?;
}
Ok(area)
}
#[inline]
fn draw_north (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Perhaps<Rect> {
Self::N.blit(buf, x, y, style)
}
#[inline]
fn draw_south (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Perhaps<Rect> {
Self::S.blit(buf, x, y, style)
}
#[inline]
fn draw_vertical (&self, buf: &mut Buffer, area: Rect, style: Option<Style>) -> Usually<Rect> {
let style = style.or_else(||self.style_vertical());
for y in area.y..(area.y+area.height).saturating_sub(1) {
Self::W.blit(buf, area.x, y, style)?;
Self::E.blit(buf, area.x + area.width - 1, y, style)?;
}
Ok(area)
}
#[inline]
fn draw_corners (&self, buf: &mut Buffer, area: Rect, style: Option<Style>) -> Usually<Rect> {
let style = style.or_else(||self.style_corners());
if area.width > 0 && area.height > 0 {
Self::NW.blit(buf, area.x, area.y, style)?;
Self::NE.blit(buf, area.x + area.width - 1, area.y, style)?;
Self::SW.blit(buf, area.x, area.y + area.height - 1, style)?;
Self::SE.blit(buf, area.x + area.width - 1, area.y + area.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:ty {
$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 Lozenge(pub Style);
pub struct LozengeV(pub Style);
pub struct LozengeDotted(pub Style);
pub struct Quarter(pub Style);
pub struct QuarterV(pub Style);
pub struct Chamfer(pub Style);
pub struct Corners(pub Style);
border! {
Lozenge {
"" "" ""
"" ""
"" "" ""
fn style (&self) -> Option<Style> {
Some(self.0)
}
},
LozengeV {
"" "" ""
"" ""
"" "" ""
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)
}
}
}
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);
}
pub const GRAY: Style = Style {
fg: Some(Color::Gray),
bg: None,
underline_color: None,
add_modifier: Modifier::empty(),
sub_modifier: Modifier::empty(),
};
pub const GRAY_NOT_DIM: Style = Style {
fg: Some(Color::Gray),
bg: None,
underline_color: None,
add_modifier: Modifier::empty(),
sub_modifier: Modifier::DIM,
};
pub const DIM: Style = Style {
fg: None,
bg: None,
underline_color: None,
add_modifier: Modifier::DIM,
sub_modifier: Modifier::empty(),
};
pub const GRAY_DIM: Style = Style {
fg: Some(Color::Gray),
bg: None,
underline_color: None,
add_modifier: Modifier::DIM,
sub_modifier: Modifier::empty(),
};
pub const WHITE_NOT_DIM_BOLD: Style = Style {
fg: Some(Color::White),
bg: None,
underline_color: None,
add_modifier: Modifier::BOLD,
sub_modifier: Modifier::DIM,
};
pub const GRAY_NOT_DIM_BOLD: Style = Style {
fg: Some(Color::Gray),
bg: None,
underline_color: None,
add_modifier: Modifier::BOLD,
sub_modifier: Modifier::DIM,
};
pub const NOT_DIM: Style = Style {
fg: None,
bg: None,
underline_color: None,
add_modifier: Modifier::empty(),
sub_modifier: Modifier::DIM,
};
pub const NOT_DIM_GREEN: Style = Style {
fg: Some(Color::Rgb(96, 255, 32)),
bg: Some(COLOR_BG1),
underline_color: None,
add_modifier: Modifier::empty(),
sub_modifier: Modifier::DIM,
};
pub const NOT_DIM_BOLD: Style = Style {
fg: None,
bg: None,
underline_color: None,
add_modifier: Modifier::BOLD,
sub_modifier: Modifier::DIM,
};