mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 11:46:41 +01:00
601 lines
17 KiB
Rust
601 lines
17 KiB
Rust
//! Rendering of application to display.
|
|
|
|
use crate::*;
|
|
pub(crate) use ratatui::prelude::CrosstermBackend;
|
|
pub(crate) use ratatui::style::{Stylize, Style, Color};
|
|
pub(crate) use ratatui::layout::Rect;
|
|
pub(crate) use ratatui::buffer::{Buffer, Cell};
|
|
use ratatui::widgets::WidgetRef;
|
|
|
|
/// Main thread render loop
|
|
pub fn render_thread (
|
|
exited: &Arc<AtomicBool>,
|
|
device: &Arc<RwLock<impl Render + Send + Sync + 'static>>
|
|
) -> Usually<JoinHandle<()>> {
|
|
let exited = exited.clone();
|
|
let device = device.clone();
|
|
let mut terminal = ratatui::Terminal::new(CrosstermBackend::new(stdout()))?;
|
|
let sleep = Duration::from_millis(20);
|
|
Ok(spawn(move || loop {
|
|
|
|
if let Ok(device) = device.try_read() {
|
|
terminal.draw(|frame|{
|
|
let area = frame.size();
|
|
let buffer = frame.buffer_mut();
|
|
device
|
|
.render(buffer, area)
|
|
.expect("Failed to render content");
|
|
})
|
|
.expect("Failed to render frame");
|
|
}
|
|
|
|
if exited.fetch_and(true, Ordering::Relaxed) {
|
|
break
|
|
}
|
|
std::thread::sleep(sleep);
|
|
}))
|
|
}
|
|
|
|
pub fn make_dim (buf: &mut Buffer) {
|
|
for cell in buf.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 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 buffer_update (
|
|
buf: &mut Buffer, area: Rect, callback: &impl Fn(&mut Cell, u16, u16)
|
|
) {
|
|
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 fn fill_fg (buf: &mut Buffer, area: Rect, color: Color) {
|
|
buffer_update(buf, area, &|cell,_,_|{cell.set_fg(color);})
|
|
}
|
|
|
|
pub fn fill_bg (buf: &mut Buffer, area: Rect, color: Color) {
|
|
buffer_update(buf, area, &|cell,_,_|{cell.set_bg(color);})
|
|
}
|
|
|
|
pub fn to_fill_bg (color: Color) -> impl Render {
|
|
move |buf: &mut Buffer, area: Rect|{
|
|
fill_bg(buf, area, color);
|
|
Ok(area)
|
|
}
|
|
}
|
|
|
|
pub fn fill_char (buf: &mut Buffer, area: Rect, c: char) {
|
|
buffer_update(buf, area, &|cell,_,_|{cell.set_char(c);})
|
|
}
|
|
|
|
pub fn half_block (lower: bool, upper: bool) -> Option<char> {
|
|
match (lower, upper) {
|
|
(true, true) => Some('█'),
|
|
(true, false) => Some('▄'),
|
|
(false, true) => Some('▀'),
|
|
_ => None
|
|
}
|
|
}
|
|
|
|
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>) -> Usually<Rect>;
|
|
}
|
|
|
|
impl<T: AsRef<str>> Blit for T {
|
|
fn blit (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Usually<Rect> {
|
|
if x < buf.area.width && y < buf.area.height {
|
|
buf.set_string(x, y, self.as_ref(), style.unwrap_or(Style::default()));
|
|
}
|
|
Ok(Rect { x, y, width: self.as_ref().len() as u16, height: 1 })
|
|
}
|
|
}
|
|
|
|
/// Trait for things that render to the display.
|
|
pub trait Render: Send {
|
|
// Render something to an area of the buffer.
|
|
// Returns area used by component.
|
|
// This is insufficient but for the most basic dynamic layout algorithms.
|
|
fn render (&self, _b: &mut Buffer, _a: Rect) -> Usually<Rect> {
|
|
Ok(Rect { x: 0, y: 0, width: 0, height: 0 })
|
|
}
|
|
}
|
|
|
|
/// Implement the `Render` trait.
|
|
#[macro_export] macro_rules! render {
|
|
($T:ty) => {
|
|
impl Render for $T {}
|
|
};
|
|
($T:ty |$self:ident, $buf:ident, $area:ident|$block:expr) => {
|
|
impl Render for $T {
|
|
fn render (&$self, $buf: &mut Buffer, $area: Rect) -> Usually<Rect> {
|
|
$block
|
|
}
|
|
}
|
|
};
|
|
($T:ty = $render:path) => {
|
|
impl Render for $T {
|
|
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
|
$render(self, buf, area)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Render for () {
|
|
fn render (&self, _: &mut Buffer, a: Rect) -> Usually<Rect> {
|
|
Ok(Rect { x: a.x, y: a.y, width: 0, height: 0 })
|
|
}
|
|
}
|
|
|
|
impl<T: Render> Render for Option<T> {
|
|
fn render (&self, b: &mut Buffer, a: Rect) -> Usually<Rect> {
|
|
match self {
|
|
Some(widget) => widget.render(b, a),
|
|
None => ().render(b, a),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<T: Fn(&mut Buffer, Rect) -> Usually<Rect> + Send> Render for T {
|
|
fn render (&self, b: &mut Buffer, a: Rect) -> Usually<Rect> {
|
|
(*self)(b, a)
|
|
}
|
|
}
|
|
|
|
impl<T: Render> Render for Arc<Mutex<T>> {
|
|
fn render (&self, b: &mut Buffer, a: Rect) -> Usually<Rect> {
|
|
self.lock().unwrap().render(b, a)
|
|
}
|
|
}
|
|
|
|
impl<T: Render + Sync> Render for Arc<RwLock<T>> {
|
|
fn render (&self, b: &mut Buffer, a: Rect) -> Usually<Rect> {
|
|
self.read().unwrap().render(b, a)
|
|
}
|
|
}
|
|
|
|
impl WidgetRef for &dyn Render {
|
|
fn render_ref (&self, area: Rect, buf: &mut Buffer) {
|
|
Render::render(*self, buf, area).expect("Failed to render device.");
|
|
}
|
|
}
|
|
|
|
impl WidgetRef for dyn Render {
|
|
fn render_ref (&self, area: Rect, buf: &mut Buffer) {
|
|
Render::render(self, buf, area).expect("Failed to render device.");
|
|
}
|
|
}
|
|
|
|
#[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 struct Layered<'a, const N: usize>(pub [&'a (dyn Render + Sync); N]);
|
|
|
|
impl<'a, const N: usize> Render for Layered<'a, N> {
|
|
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
|
for layer in self.0.iter() {
|
|
layer.render(buf, area)?;
|
|
}
|
|
Ok(area)
|
|
}
|
|
}
|
|
|
|
pub struct If<'a>(pub bool, pub &'a (dyn Render + Sync));
|
|
|
|
impl<'a> Render for If<'a> {
|
|
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
|
match self.0 {
|
|
true => self.1 as &dyn Render,
|
|
false => &() as &dyn Render
|
|
}.render(buf, area)
|
|
}
|
|
}
|
|
|
|
pub struct IfElse<'a>(pub bool, pub &'a (dyn Render + Sync), pub &'a (dyn Render + Sync));
|
|
|
|
impl<'a> Render for IfElse<'a> {
|
|
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
|
match self.0 {
|
|
true => self.1 as &dyn Render,
|
|
false => &() as &dyn Render
|
|
}.render(buf, area)
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone)]
|
|
pub enum Direction { Down, Right }
|
|
|
|
impl Direction {
|
|
pub fn split <'a, const N: usize> (&self, items: [&'a (dyn Render + Sync);N]) -> Split<'a, N> {
|
|
Split(*self, items)
|
|
}
|
|
pub fn split_focus <'a> (&self, index: usize, items: Renderables<'a>, style: Style) -> SplitFocus<'a> {
|
|
SplitFocus(*self, index, items, style)
|
|
}
|
|
pub fn is_down (&self) -> bool {
|
|
match self { Self::Down => true, _ => false }
|
|
}
|
|
pub fn is_right (&self) -> bool {
|
|
match self { Self::Right => true, _ => false }
|
|
}
|
|
}
|
|
|
|
pub struct Split<'a, const N: usize>(
|
|
pub Direction, pub [&'a (dyn Render + Sync);N]
|
|
);
|
|
|
|
impl<'a, const N: usize> Split<'a, N> {
|
|
pub fn down (items: [&'a (dyn Render + Sync);N]) -> Self {
|
|
Self(Direction::Down, items)
|
|
}
|
|
pub fn right (items: [&'a (dyn Render + Sync);N]) -> Self {
|
|
Self(Direction::Right, items)
|
|
}
|
|
pub fn render_areas (&self, buf: &mut Buffer, area: Rect) -> Usually<(Rect, Vec<Rect>)> {
|
|
let Rect { mut x, mut y, mut width, mut height } = area;
|
|
let mut areas = vec![];
|
|
for item in self.1 {
|
|
if width == 0 || height == 0 {
|
|
break
|
|
}
|
|
let result = item.render(buf, Rect { x, y, width, height })?;
|
|
match self.0 {
|
|
Direction::Down => {
|
|
y = y + result.height;
|
|
height = height.saturating_sub(result.height);
|
|
},
|
|
Direction::Right => {
|
|
x = x + result.width;
|
|
width = width.saturating_sub(result.width);
|
|
},
|
|
};
|
|
areas.push(area);
|
|
}
|
|
Ok((area, areas))
|
|
}
|
|
}
|
|
|
|
impl<'a, const N: usize> Render for Split<'a, N> {
|
|
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
|
Ok(self.render_areas(buf, area)?.0)
|
|
}
|
|
}
|
|
|
|
type Renderables<'a> = &'a [&'a (dyn Render + Send + Sync)];
|
|
|
|
pub struct SplitFocus<'a>(pub Direction, pub usize, pub Renderables<'a>, pub Style);
|
|
|
|
impl<'a> SplitFocus<'a> {
|
|
pub fn render_areas (&self, buf: &mut Buffer, area: Rect) -> Usually<(Rect, Vec<Rect>)> {
|
|
let Rect { mut x, mut y, mut width, mut height } = area;
|
|
let mut areas = vec![];
|
|
for item in self.2.iter() {
|
|
if width == 0 || height == 0 {
|
|
break
|
|
}
|
|
let result = item.render(buf, Rect { x, y, width, height })?;
|
|
areas.push(result);
|
|
match self.0 {
|
|
Direction::Down => {
|
|
y = y + result.height;
|
|
height = height.saturating_sub(result.height);
|
|
},
|
|
Direction::Right => {
|
|
x = x + result.width;
|
|
width = width.saturating_sub(result.width);
|
|
},
|
|
}
|
|
Lozenge(self.3).draw(buf, result)?;
|
|
}
|
|
Ok((area, areas))
|
|
}
|
|
}
|
|
|
|
impl<'a> Render for SplitFocus<'a> {
|
|
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
|
Ok(self.render_areas(buf, area)?.0)
|
|
}
|
|
}
|
|
|
|
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 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 (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
|
self.draw_horizontal(buf, area, None)?;
|
|
self.draw_vertical(buf, area, None)?;
|
|
self.draw_corners(buf, area, None)?;
|
|
Ok(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>) -> Usually<Rect> {
|
|
Self::N.blit(buf, x, y, style)
|
|
}
|
|
#[inline]
|
|
fn draw_south (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Usually<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());
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
macro_rules! impl_axis_common { ($A:ident $T:ty) => {
|
|
impl $A<$T> {
|
|
pub fn start_inc (&mut self) -> $T {
|
|
self.start = self.start + 1;
|
|
self.start
|
|
}
|
|
pub fn start_dec (&mut self) -> $T {
|
|
self.start = self.start.saturating_sub(1);
|
|
self.start
|
|
}
|
|
pub fn point_inc (&mut self) -> Option<$T> {
|
|
self.point = self.point.map(|p|p + 1);
|
|
self.point
|
|
}
|
|
pub fn point_dec (&mut self) -> Option<$T> {
|
|
self.point = self.point.map(|p|p.saturating_sub(1));
|
|
self.point
|
|
}
|
|
}
|
|
} }
|
|
|
|
pub struct FixedAxis<T> { pub start: T, pub point: Option<T> }
|
|
impl_axis_common!(FixedAxis u16);
|
|
impl_axis_common!(FixedAxis usize);
|
|
|
|
pub struct ScaledAxis<T> { pub start: T, pub scale: T, pub point: Option<T> }
|
|
impl_axis_common!(ScaledAxis u16);
|
|
impl_axis_common!(ScaledAxis usize);
|
|
impl<T: Copy> ScaledAxis<T> {
|
|
pub fn scale_mut (&mut self, cb: &impl Fn(T)->T) {
|
|
self.scale = cb(self.scale)
|
|
}
|
|
}
|