wip: big flat pt.2: extract engine crate

This commit is contained in:
🪞👃🪞 2024-12-30 17:54:30 +01:00
parent 4a3de618d0
commit a5628fb663
31 changed files with 1738 additions and 888 deletions

27
engine/src/component.rs Normal file
View file

@ -0,0 +1,27 @@
use crate::*;
/// A UI component that can render itself as a [Render], and [Handle] input.
pub trait Component<E: Engine>: Render<E> + Handle<E> {}
/// Everything that implements [Render] and [Handle] is a [Component].
impl<E: Engine, C: Render<E> + Handle<E>> Component<E> for C {}
/// A component that can exit.
pub trait Exit: Send {
//fn exited (&self) -> bool;
//fn exit (&mut self);
//fn boxed (self) -> Box<dyn Exit> where Self: Sized + 'static {
//Box::new(self)
//}
}
/// Marker trait for [Component]s that can [Exit].
pub trait ExitableComponent<E>: Exit + Component<E> where E: Engine {
///// Perform type erasure for collecting heterogeneous components.
//fn boxed (self) -> Box<dyn ExitableComponent<E>> where Self: Sized + 'static {
//Box::new(self)
//}
}
/// All [Components]s that implement [Exit] implement [ExitableComponent].
impl<E: Engine, C: Component<E> + Exit> ExitableComponent<E> for C {}

80
engine/src/engine.rs Normal file
View file

@ -0,0 +1,80 @@
use crate::*;
use std::fmt::{Debug, Display};
use std::ops::{Add, Sub, Mul, Div};
/// Platform backend.
pub trait Engine: Send + Sync + Sized {
/// Input event type
type Input: Input<Self>;
/// Result of handling input
type Handled;
/// Render target
type Output: Output<Self>;
/// Unit of length
type Unit: Coordinate;
/// Rectangle without offset
type Size: Size<Self::Unit> + From<[Self::Unit;2]> + Debug + Copy;
/// Rectangle with offset
type Area: Area<Self::Unit> + From<[Self::Unit;4]> + Debug + Copy;
/// Prepare before run
fn setup (&mut self) -> Usually<()> { Ok(()) }
/// True if done
fn exited (&self) -> bool;
/// Clean up after run
fn teardown (&mut self) -> Usually<()> { Ok(()) }
}
/// A linear coordinate.
pub trait Coordinate: Send + Sync + Copy
+ Add<Self, Output=Self>
+ Sub<Self, Output=Self>
+ Mul<Self, Output=Self>
+ Div<Self, Output=Self>
+ Ord + PartialEq + Eq
+ Debug + Display + Default
+ From<u16> + Into<u16>
+ Into<usize>
+ Into<f64>
{
fn minus (self, other: Self) -> Self {
if self >= other {
self - other
} else {
0.into()
}
}
fn zero () -> Self {
0.into()
}
}
pub trait Size<N: Coordinate> {
fn x (&self) -> N;
fn y (&self) -> N;
#[inline] fn w (&self) -> N { self.x() }
#[inline] fn h (&self) -> N { self.y() }
#[inline] fn wh (&self) -> [N;2] { [self.x(), self.y()] }
#[inline] fn clip_w (&self, w: N) -> [N;2] { [self.w().min(w), self.h()] }
#[inline] fn clip_h (&self, h: N) -> [N;2] { [self.w(), self.h().min(h)] }
#[inline] fn expect_min (&self, w: N, h: N) -> Usually<&Self> {
if self.w() < w || self.h() < h {
Err(format!("min {w}x{h}").into())
} else {
Ok(self)
}
}
}
pub trait Area<N: Coordinate>: Copy {
fn x (&self) -> N;
fn y (&self) -> N;
fn w (&self) -> N;
fn h (&self) -> N;
#[inline] fn expect_min (&self, w: N, h: N) -> Usually<&Self> {
if self.w() < w || self.h() < h {
Err(format!("min {w}x{h}").into())
} else {
Ok(self)
}
}
}

69
engine/src/input.rs Normal file
View file

@ -0,0 +1,69 @@
use crate::*;
use std::sync::{Arc, Mutex, RwLock};
/// Current input state
pub trait Input<E: Engine> {
/// Type of input event
type Event;
/// Currently handled event
fn event (&self) -> &Self::Event;
/// Whether component should exit
fn is_done (&self) -> bool;
/// Mark component as done
fn done (&self);
}
#[macro_export] macro_rules! handle {
(<$E:ty>|$self:ident:$Struct:ty,$input:ident|$handler:expr) => {
impl Handle<$E> for $Struct {
fn handle (&mut $self, $input: &<$E as Engine>::Input) -> Perhaps<<$E as Engine>::Handled> {
$handler
}
}
}
}
/// Handle input
pub trait Handle<E: Engine>: Send + Sync {
fn handle (&mut self, context: &E::Input) -> Perhaps<E::Handled>;
}
impl<E: Engine, H: Handle<E>> Handle<E> for &mut H {
fn handle (&mut self, context: &E::Input) -> Perhaps<E::Handled> {
(*self).handle(context)
}
}
impl<E: Engine, H: Handle<E>> Handle<E> for Option<H> {
fn handle (&mut self, context: &E::Input) -> Perhaps<E::Handled> {
if let Some(ref mut handle) = self {
handle.handle(context)
} else {
Ok(None)
}
}
}
impl<H, E: Engine> Handle<E> for Mutex<H> where H: Handle<E> {
fn handle (&mut self, context: &E::Input) -> Perhaps<E::Handled> {
self.get_mut().unwrap().handle(context)
}
}
impl<H, E: Engine> Handle<E> for Arc<Mutex<H>> where H: Handle<E> {
fn handle (&mut self, context: &E::Input) -> Perhaps<E::Handled> {
self.lock().unwrap().handle(context)
}
}
impl<H, E: Engine> Handle<E> for RwLock<H> where H: Handle<E> {
fn handle (&mut self, context: &E::Input) -> Perhaps<E::Handled> {
self.write().unwrap().handle(context)
}
}
impl<H, E: Engine> Handle<E> for Arc<RwLock<H>> where H: Handle<E> {
fn handle (&mut self, context: &E::Input) -> Perhaps<E::Handled> {
self.write().unwrap().handle(context)
}
}

62
engine/src/lib.rs Normal file
View file

@ -0,0 +1,62 @@
mod component; pub use self::component::*;
mod engine; pub use self::engine::*;
mod input; pub use self::input::*;
mod output; pub use self::output::*;
mod tui; pub use self::tui::*;
pub use std::error::Error;
/// Standard result type.
pub type Usually<T> = Result<T, Box<dyn Error>>;
/// Standard optional result type.
pub type Perhaps<T> = Result<Option<T>, Box<dyn Error>>;
#[cfg(test)] #[test] fn test_stub_engine () -> Usually<()> {
struct TestEngine(bool);
struct TestInput(bool);
struct TestOutput([u16;4]);
enum TestEvent { Test1 }
impl Engine for TestEngine {
type Input = TestInput;
type Handled = ();
type Output = TestOutput;
type Unit = u16;
type Size = [u16;2];
type Area = [u16;4];
fn exited (&self) -> bool {
self.0
}
}
impl Input<TestEngine> for TestInput {
type Event = TestEvent;
fn event (&self) -> &Self::Event {
&TestEvent::Test1
}
fn is_done (&self) -> bool {
self.0
}
fn done (&self) {}
}
impl Output<TestEngine> for TestOutput {
fn area (&self) -> [u16;4] {
self.0
}
fn area_mut (&mut self) -> &mut [u16;4] {
&mut self.0
}
fn render_in (&mut self, _: [u16;4], _: &dyn Render<TestEngine>) -> Usually<()> {
Ok(())
}
}
impl Render<TestEngine> for String {
fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> {
Ok(Some([self.len() as u16, 1]))
}
}
Ok(())
}
#[cfg(test)] #[test] fn test_tui_engine () -> Usually<()> {
Ok(())
}

272
engine/src/output.rs Normal file
View file

@ -0,0 +1,272 @@
use crate::*;
use std::sync::{Arc, Mutex, RwLock};
/// Rendering target
pub trait Output<E: Engine> {
/// Current output area
fn area (&self) -> E::Area;
/// Mutable pointer to area
fn area_mut (&mut self) -> &mut E::Area;
/// Render widget in area
fn render_in (&mut self, area: E::Area, widget: &dyn Render<E>) -> Usually<()>;
}
/// Write content to output buffer.
pub trait Render<E: Engine>: Send + Sync {
/// Minimum size to use
fn min_size (&self, _: E::Size) -> Perhaps<E::Size> {
Ok(None)
}
/// Draw to output render target
fn render (&self, _: &mut E::Output) -> Usually<()> {
Ok(())
}
}
impl<E: Engine> Render<E> for &dyn Render<E> {}
impl<E: Engine> Render<E> for &mut dyn Render<E> {}
impl<E: Engine> Render<E> for Box<dyn Render<E>> {}
impl<E: Engine, R: Render<E>> Render<E> for &R {}
impl<E: Engine, R: Render<E>> Render<E> for &mut R {}
impl<E: Engine, R: Render<E>> Render<E> for Option<R> {}
impl<E: Engine, R: Render<E>> Render<E> for Arc<R> {}
impl<E: Engine, R: Render<E>> Render<E> for Mutex<R> {}
impl<E: Engine, R: Render<E>> Render<E> for RwLock<R> {}
/// Something that can be represented by a renderable component.
pub trait Content<E: Engine>: Render<E> + Send + Sync {
fn content (&self) -> Option<impl Render<E>> where (): Render<E> {
None::<()>
}
}
impl<E: Engine> Content<E> for &dyn Render<E> {}
impl<E: Engine> Content<E> for &mut dyn Render<E> {}
impl<E: Engine> Content<E> for Box<dyn Render<E>> {}
impl<E: Engine, C: Content<E>> Content<E> for &C {}
impl<E: Engine, C: Content<E>> Content<E> for &mut C {}
impl<E: Engine, C: Content<E>> Content<E> for Option<C> {}
impl<E: Engine, C: Content<E>> Content<E> for Arc<C> {}
impl<E: Engine, C: Content<E>> Content<E> for Mutex<C> {}
impl<E: Engine, C: Content<E>> Content<E> for RwLock<C> {}
/****
impl<E: Engine, R: Render<E> + Send + Sync> Content<E> for R {}
//impl<E: Engine, R: Render<E>> Content<E> for R {
//fn content (&self) -> Option<impl Render<E>> {
//Some(self)
//}
//}
/// All implementors of [Content] can be [Render]ed.
impl<E: Engine, C: Content<E>> Render<E> for C {
/// Minimum size to use
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
self.content().map(|content|content.min_size(to))
.unwrap_or(Ok(None))
}
/// Draw to output render target
fn render (&self, to: &mut E::Output) -> Usually<()> {
self.content().map(|content|content.render(to))
.unwrap_or(Ok(()))
}
}
//impl<E: Engine, C: Content<E>> Content<E> for &C {
//fn content (&self) -> Option<impl Render<E>> {
//Some(self)
//}
//}
//impl<E: Engine, C: Content<E>> Content<E> for Option<C> {
//fn content (&self) -> Option<impl Render<E>> {
//Some(self)
//}
//}
/// Define custom content for a struct.
#[macro_export] macro_rules! render {
// Implement for all engines
(|$self:ident:$Struct:ident$(<$($L:lifetime),*E$(,$T:ident$(:$U:path)?)*$(,)?>)?|$cb:expr) => {
impl<E: Engine, $($($L),*$($T $(: $U)?),*)?> Content<E> for $Struct $(<$($L,)* E, $($T),*>)? {
fn content (&$self) -> Option<impl Render<$E>> {
Some($cb)
}
}
};
// Implement for a specific engine
(<$E:ty>|$self:ident:$Struct:ident$(<
$($($L:lifetime),+)?
$($($T:ident$(:$U:path)?),+)?
>)?|$cb:expr) => {
impl $(<
$($($L),+)?
$($($T$(:$U)?),+)?
>)? Content<$E> for $Struct $(<
$($($L),+)?
$($($T),+)?
>)? {
fn content (&$self) -> Option<impl Render<$E>> {
Some($cb)
}
}
}
}
impl<E: Engine, R: Render<E>> Render<E> for &R {
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
(*self).min_size(to)
}
fn render (&self, to: &mut E::Output) -> Usually<()> {
(*self).render(to)
}
}
impl<E: Engine, R: Render<E>> Render<E> for Option<R> {
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
self.map(|content|content.min_size(to))
.unwrap_or(Ok(None))
}
fn render (&self, to: &mut E::Output) -> Usually<()> {
self.map(|content|content.render(to))
.unwrap_or(Ok(()))
}
}
//impl<E: Engine> Render<E> for &dyn Render<E> {
//fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
//(*self).min_size(to)
//}
//fn render (&self, to: &mut E::Output) -> Usually<()> {
//(*self).render(to)
//}
//}
//impl<E: Engine> Render<E> for &mut dyn Render<E> {
//fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
//(*self).min_size(to)
//}
//fn render (&self, to: &mut E::Output) -> Usually<()> {
//(*self).render(to)
//}
//}
//impl<E: Engine> Render<E> for Box<dyn Render<E> + '_> {
//fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
//(**self).min_size(to)
//}
//fn render (&self, to: &mut E::Output) -> Usually<()> {
//(**self).render(to)
//}
//}
//impl<E: Engine, W: Render<E>> Render<E> for Arc<W> {
//fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
//self.as_ref().min_size(to)
//}
//fn render (&self, to: &mut E::Output) -> Usually<()> {
//self.as_ref().render(to)
//}
//}
//impl<E: Engine, W: Render<E>> Render<E> for Mutex<W> {
//fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
//self.lock().unwrap().min_size(to)
//}
//fn render (&self, to: &mut E::Output) -> Usually<()> {
//self.lock().unwrap().render(to)
//}
//}
//impl<E: Engine, W: Render<E>> Render<E> for RwLock<W> {
//fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
//self.read().unwrap().min_size(to)
//}
//fn render (&self, to: &mut E::Output) -> Usually<()> {
//self.read().unwrap().render(to)
//}
//}
//impl<E: Engine, W: Render<E>> Render<E> for Option<W> {
//fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
//Ok(self.as_ref().map(|widget|widget.min_size(to)).transpose()?.flatten())
//}
//fn render (&self, to: &mut E::Output) -> Usually<()> {
//self.as_ref().map(|widget|widget.render(to)).unwrap_or(Ok(()))
//}
//}
/// Cast to dynamic pointer
pub fn widget <E, T> (w: &T) -> &dyn Render<E>
where E: Engine, T: Render<E>
{
w as &dyn Render<E>
}
/// Ad-hoc widget with custom rendering.
pub fn render <E, F> (render: F) -> impl Render<E>
where E: Engine, F: Fn(&mut E::Output)->Usually<()>+Send+Sync
{
Widget::new(|_|Ok(Some([0.into(),0.into()].into())), render)
}
/// A custom [Render] defined by passing layout and render closures in place.
pub struct Widget<
E: Engine,
L: Send + Sync + Fn(E::Size)->Perhaps<E::Size>,
R: Send + Sync + Fn(&mut E::Output)->Usually<()>
>(L, R, PhantomData<E>);
impl<
E: Engine,
L: Send + Sync + Fn(E::Size)->Perhaps<E::Size>,
R: Send + Sync + Fn(&mut E::Output)->Usually<()>
> Widget<E, L, R> {
pub fn new (layout: L, render: R) -> Self {
Self(layout, render, Default::default())
}
}
impl<
E: Engine,
L: Send + Sync + Fn(E::Size)->Perhaps<E::Size>,
R: Send + Sync + Fn(&mut E::Output)->Usually<()>
> Render<E> for Widget<E, L, R> {
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
self.0(to)
}
fn render (&self, to: &mut E::Output) -> Usually<()> {
self.1(to)
}
}
/// Has static methods for conditional rendering,
/// in unary and binary forms.
pub struct Cond;
impl Cond {
/// Render `item` when `cond` is true.
pub fn when <E: Engine, A: Render<E>> (cond: bool, item: A) -> When<E, A> {
When(cond, item, Default::default())
}
/// Render `item` if `cond` is true, otherwise render `other`.
pub fn either <E: Engine, A: Render<E>, B: Render<E>> (cond: bool, item: A, other: B) -> Either<E, A, B> {
Either(cond, item, other, Default::default())
}
}
/// Renders `self.1` when `self.0` is true.
pub struct When<E: Engine, A: Render<E>>(bool, A, PhantomData<E>);
/// Renders `self.1` when `self.0` is true, otherwise renders `self.2`
pub struct Either<E: Engine, A: Render<E>, B: Render<E>>(bool, A, B, PhantomData<E>);
**/

440
engine/src/tui.rs Normal file
View file

@ -0,0 +1,440 @@
use crate::*;
use std::sync::{Arc, RwLock, atomic::{AtomicBool, Ordering::*}};
use std::io::{stdout, Stdout};
use std::time::Duration;
use std::thread::{spawn, JoinHandle};
pub use ::better_panic;
pub(crate) use better_panic::{Settings, Verbosity};
pub use ::crossterm;
pub(crate) use crossterm::{
ExecutableCommand,
event::*,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode},
event::{KeyCode, KeyModifiers, KeyEvent, KeyEventKind, KeyEventState},
};
pub use ::ratatui;
pub(crate) use ratatui::{
prelude::{Color, Style, Buffer},
style::Modifier,
backend::{Backend, CrosstermBackend, ClearType},
buffer::Cell
};
impl Coordinate for u16 {}
impl Size<u16> for [u16;2] {
fn x (&self) -> u16 { self[0] }
fn y (&self) -> u16 { self[1] }
}
impl Area<u16> for [u16;4] {
fn x (&self) -> u16 { self[0] }
fn y (&self) -> u16 { self[1] }
fn w (&self) -> u16 { self[2] }
fn h (&self) -> u16 { self[3] }
}
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, 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::PanicHookInfo|{
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.x, area.y, area.width, area.height],
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, Relaxed) {
break
}
if ::crossterm::event::poll(poll).is_ok() {
let event = TuiEvent::Input(::crossterm::event::read().unwrap());
match event {
key_pat!(Ctrl-KeyCode::Char('c')) => {
exited.store(true, 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 size = engine.read().unwrap().backend.size().expect("get size failed");
let mut buffer = Buffer::empty(size);
spawn(move || loop {
if exited.fetch_and(true, Relaxed) {
break
}
let size = engine.read().unwrap().backend.size()
.expect("get size failed");
if let Ok(state) = state.try_read() {
if buffer.area != size {
engine.write().unwrap().backend.clear_region(ClearType::All)
.expect("clear failed");
buffer.resize(size);
buffer.reset();
}
let mut output = TuiOutput {
buffer,
area: [size.x, size.y, size.width, size.height]
};
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.backend.clear_region(ClearType::All).unwrap();
self.buffer.resize(size);
self.buffer.reset();
}
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 {
pub(crate) exited: Arc<AtomicBool>,
pub(crate) event: TuiEvent,
}
#[derive(Debug, Clone, PartialEq)]
pub enum TuiEvent {
/// Terminal input
Input(Event),
}
impl Input<Tui> for TuiInput {
type Event = TuiEvent;
fn event (&self) -> &TuiEvent {
&self.event
}
fn is_done (&self) -> bool {
self.exited.fetch_and(true, Relaxed)
}
fn done (&self) {
self.exited.store(true, Relaxed);
}
}
#[macro_export] macro_rules! key_pat {
(Ctrl-Alt-$code:pat) => { key_event_pat!($code, KeyModifiers::CONTROL | KeyModifiers::ALT) };
(Ctrl-$code:pat) => { key_event_pat!($code, KeyModifiers::CONTROL) };
(Alt-$code:pat) => { key_event_pat!($code, KeyModifiers::ALT) };
(Shift-$code:pat) => { key_event_pat!($code, KeyModifiers::SHIFT) };
($code:pat) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent {
code: $code,
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
state: KeyEventState::NONE
})) };
}
#[macro_export] macro_rules! key_event_pat {
($code:pat) => {
TuiEvent::Input(crossterm::event::Event::Key(KeyEvent {
code: $code,
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
state: KeyEventState::NONE
}))
};
($code:pat, $modifiers: pat) => {
TuiEvent::Input(crossterm::event::Event::Key(KeyEvent {
code: $code,
modifiers: $modifiers,
kind: KeyEventKind::Press,
state: KeyEventState::NONE
}))
};
}
#[macro_export] macro_rules! kexp {
(Ctrl-Alt-$code:ident) => { key_event_expr!($code, KeyModifiers::from_bits(0b0000_0110).unwrap()) };
(Ctrl-$code:ident) => { key_event_expr!($code, KeyModifiers::CONTROL) };
(Alt-$code:ident) => { key_event_expr!($code, KeyModifiers::ALT) };
(Shift-$code:ident) => { key_event_expr!($code, KeyModifiers::SHIFT) };
($code:ident) => { key_event_expr!($code) };
($code:expr) => { key_event_expr!($code) };
}
#[macro_export] macro_rules! key_expr {
(Ctrl-Alt-$code:ident) => { key_event_expr!($code, KeyModifiers::CONTROL | KeyModifiers::ALT) };
(Ctrl-$code:ident) => { key_event_expr!($code, KeyModifiers::CONTROL) };
(Alt-$code:ident) => { key_event_expr!($code, KeyModifiers::ALT) };
(Shift-$code:ident) => { key_event_expr!($code, KeyModifiers::SHIFT) };
($code:ident) => { key_event_expr!($code) };
}
#[macro_export] macro_rules! key_event_expr {
($code:expr, $modifiers: expr) => {
TuiEvent::Input(crossterm::event::Event::Key(KeyEvent {
code: $code,
modifiers: $modifiers,
kind: KeyEventKind::Press,
state: KeyEventState::NONE
}))
};
($code:expr) => {
TuiEvent::Input(crossterm::event::Event::Key(KeyEvent {
code: $code,
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
state: KeyEventState::NONE
}))
};
}
/*
/// Define a key
pub const fn key (code: KeyCode) -> KeyEvent {
let modifiers = KeyModifiers::NONE;
let kind = KeyEventKind::Press;
let state = KeyEventState::NONE;
KeyEvent { code, modifiers, kind, state }
}
/// Add Ctrl modifier to key
pub const fn ctrl (key: KeyEvent) -> KeyEvent {
KeyEvent { modifiers: key.modifiers.union(KeyModifiers::CONTROL), ..key }
}
/// Add Alt modifier to key
pub const fn alt (key: KeyEvent) -> KeyEvent {
KeyEvent { modifiers: key.modifiers.union(KeyModifiers::ALT), ..key }
}
/// Add Shift modifier to key
pub const fn shift (key: KeyEvent) -> KeyEvent {
KeyEvent { modifiers: key.modifiers.union(KeyModifiers::SHIFT), ..key }
}
/// 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>]
}
}
*/
/*
impl TuiInput {
// TODO remove
pub fn handle_keymap <T> (&self, state: &mut T, keymap: &KeyMap<T>) -> Usually<bool> {
match self.event() {
TuiEvent::Input(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>];
*/
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 Render<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_bold (&mut self, area: [u16;4], on: bool) {
if on {
self.buffer_update(area, &|cell,_,_|cell.modifier.insert(Modifier::BOLD))
} else {
self.buffer_update(area, &|cell,_,_|cell.modifier.remove(Modifier::BOLD))
}
}
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
}
}
//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
}
}
//impl<T: Content<Tui>> Render<Tui> for T {}
impl Render<Tui> for &str {
fn min_size (&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.min_size(to.area().wh())?.unwrap();
Ok(to.blit(&self, x, y, None))
}
}
impl Render<Tui> for String {
fn min_size (&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.min_size(to.area().wh())?.unwrap();
Ok(to.blit(&self, x, y, None))
}
}
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);
}
}
}
}