mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-07 12:16:42 +01:00
676 lines
23 KiB
Rust
676 lines
23 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 {
|
|
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 = 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);
|
|
}
|
|
}
|
|
impl TuiInput {
|
|
pub fn handle_keymap <T> (&self, state: &mut T, keymap: &KeyMap<T>) -> Usually<bool> {
|
|
match self.event() {
|
|
TuiEvent::Input(crossterm::event::Event::Key(event)) => {
|
|
for (code, modifiers, _, _, command) in keymap.iter() {
|
|
if *code == event.code && modifiers.bits() == event.modifiers.bits() {
|
|
return command(state)
|
|
}
|
|
}
|
|
},
|
|
_ => {}
|
|
};
|
|
Ok(false)
|
|
}
|
|
}
|
|
pub type KeyHandler<T> = &'static dyn Fn(&mut T)->Usually<bool>;
|
|
pub type KeyBinding<T> = (KeyCode, KeyModifiers, &'static str, &'static str, KeyHandler<T>);
|
|
pub type KeyMap<T> = [KeyBinding<T>];
|
|
/// Define a keymap
|
|
#[macro_export] macro_rules! keymap {
|
|
($T:ty { $([$k:ident $(($char:literal))?, $m:ident, $n: literal, $d: literal, $f: expr]),* $(,)? }) => {
|
|
&[
|
|
$((KeyCode::$k $(($char))?, KeyModifiers::$m, $n, $d, &$f as KeyHandler<$T>)),*
|
|
] as &'static [KeyBinding<$T>]
|
|
}
|
|
}
|
|
/// Define a key in a keymap
|
|
#[macro_export] macro_rules! map_key {
|
|
($k:ident $(($char:literal))?, $m:ident, $n: literal, $d: literal, $f: expr) => {
|
|
(KeyCode::$k $(($char))?, KeyModifiers::$m, $n, $d, &$f as &dyn Fn()->Usually<bool>)
|
|
}
|
|
}
|
|
/// Shorthand for key match statement
|
|
#[macro_export] macro_rules! match_key {
|
|
($event:expr, {
|
|
$($key:pat=>$block:expr),* $(,)?
|
|
}) => {
|
|
match $event {
|
|
$(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
|
code: $key,
|
|
modifiers: crossterm::event::KeyModifiers::NONE,
|
|
kind: crossterm::event::KeyEventKind::Press,
|
|
state: crossterm::event::KeyEventState::NONE
|
|
}) => {
|
|
$block
|
|
})*
|
|
_ => Ok(None)
|
|
}
|
|
}
|
|
}
|
|
/// Define key pattern in key match statement
|
|
#[macro_export] macro_rules! key {
|
|
($code:pat) => {
|
|
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
|
code: $code,
|
|
modifiers: crossterm::event::KeyModifiers::NONE,
|
|
kind: crossterm::event::KeyEventKind::Press,
|
|
state: crossterm::event::KeyEventState::NONE
|
|
}))
|
|
};
|
|
(Ctrl-$code:pat) => {
|
|
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
|
code: $code,
|
|
modifiers: crossterm::event::KeyModifiers::CONTROL,
|
|
kind: crossterm::event::KeyEventKind::Press,
|
|
state: crossterm::event::KeyEventState::NONE
|
|
}))
|
|
};
|
|
(Alt-$code:pat) => {
|
|
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
|
code: $code,
|
|
modifiers: crossterm::event::KeyModifiers::ALT,
|
|
kind: crossterm::event::KeyEventKind::Press,
|
|
state: crossterm::event::KeyEventState::NONE
|
|
}))
|
|
};
|
|
(Shift-$code:pat) => {
|
|
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
|
code: $code,
|
|
modifiers: crossterm::event::KeyModifiers::SHIFT,
|
|
kind: crossterm::event::KeyEventKind::Press,
|
|
state: crossterm::event::KeyEventState::NONE
|
|
}))
|
|
}
|
|
}
|
|
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))
|
|
}
|
|
}
|
|
impl Widget for String {
|
|
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))
|
|
}
|
|
}
|
|
|
|
impl<T: Widget<Engine = Tui>> Widget for DebugOverlay<Tui, T> {
|
|
type Engine = Tui;
|
|
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
|
self.0.layout(to)
|
|
}
|
|
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
|
let [x, y, w, h] = to.area();
|
|
self.0.render(to)?;
|
|
Ok(to.blit(&format!("{w}x{h}+{x}+{y}"), x, y, Some(Style::default().green())))
|
|
}
|
|
}
|
|
|
|
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))
|
|
}
|
|
}
|
|
|
|
#[macro_export] macro_rules! tui_style {
|
|
($NAME:ident = $fg:expr, $bg:expr, $ul:expr, $add:expr, $sub:expr) => {
|
|
pub const $NAME: Style = Style {
|
|
fg: $fg, bg: $bg, underline_color: $ul, add_modifier: $add, sub_modifier: $sub,
|
|
};
|
|
}
|
|
}
|
|
pub trait TuiStyle: Widget<Engine = Tui> + Sized {
|
|
fn fg (self, color: Color) -> impl Widget<Engine = Tui> {
|
|
Layers::new(move |add|{ add(&Foreground(color))?; add(&self) })
|
|
}
|
|
fn bg (self, color: Color) -> impl Widget<Engine = Tui> {
|
|
Layers::new(move |add|{ add(&Background(color))?; add(&self) })
|
|
}
|
|
fn border (self, style: impl BorderStyle) -> impl Widget<Engine = Tui> {
|
|
Bordered(style, self)
|
|
}
|
|
}
|
|
impl<W: Widget<Engine = Tui>> TuiStyle for W {}
|
|
|
|
pub struct Foreground(pub Color);
|
|
impl Widget for Foreground {
|
|
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_fg(to.area(), self.0))
|
|
}
|
|
}
|
|
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))
|
|
}
|
|
}
|
|
|
|
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() > 0 && area.y() > 0 {
|
|
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 struct Bordered<S: BorderStyle, W: Widget<Engine = Tui>>(pub S, pub W);
|
|
impl<S: BorderStyle, W: Widget<Engine = Tui>> Content for Bordered<S, W> {
|
|
type Engine = Tui;
|
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
let content: &dyn Widget<Engine = Tui> = &self.1;
|
|
lay! { content.inset_xy(1, 1), Border(self.0) }.fill_xy()
|
|
}
|
|
}
|
|
|
|
pub trait BorderStyle: Send + Sync + Copy {
|
|
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)*
|
|
}
|
|
#[derive(Copy, Clone)]
|
|
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 const COLOR_RED: Color = Color::Rgb(191, 97, 106);
|
|
pub const COLOR_YELLOW: Color = Color::Rgb(235, 203, 139);
|
|
pub const COLOR_GREEN: Color = Color::Rgb(163, 190, 140);
|
|
pub const COLOR_PLAYING: Color = Color::Rgb(60, 100, 50);
|
|
pub const COLOR_SEPARATOR: Color = Color::Rgb(0, 0, 0);
|