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

View file

@ -99,29 +99,27 @@ impl ArrangerTui {
}
}
}
impl Render<Tui> for ArrangerTui {
fn content (&self) -> Option<impl Render<Tui>> {
let play = PlayPause(self.clock.is_rolling());
let transport = TransportView::new(self, Some(ItemPalette::from(TuiTheme::g(96))), true);
let with_transport = |x|col!([row!(![&play, &transport]), &x]);
let pool_size = if self.phrases.visible { self.splits[1] } else { 0 };
let with_pool = |x|Split::w(false, pool_size, PoolView(&self.phrases), x);
let status = ArrangerStatus::from(self);
let with_editbar = |x|Split::n(false, 1, MidiEditStatus(&self.editor), x);
let with_status = |x|Split::n(false, 2, status, x);
let with_size = |x|lay!([&self.size, x]);
let arranger = ||lay!(|add|{
let color = self.color;
add(&Fill::wh(Tui::bg(color.darkest.rgb, ())))?;
add(&Fill::wh(Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb))))?;
add(&Self::render_mode(self))
});
Some(with_size(with_status(with_editbar(with_pool(with_transport(col!([
Fill::w(Fixed::h(20, arranger())),
Fill::wh(&self.editor),
])))))))
}
}
render!(<Tui>|self: ArrangerTui|{
let play = PlayPause(self.clock.is_rolling());
let transport = TransportView::new(self, Some(ItemPalette::from(TuiTheme::g(96))), true);
let with_transport = |x|col!([row!(![&play, &transport]), &x]);
let pool_size = if self.phrases.visible { self.splits[1] } else { 0 };
let with_pool = |x|Split::w(false, pool_size, PoolView(&self.phrases), x);
let status = ArrangerStatus::from(self);
let with_editbar = |x|Split::n(false, 1, MidiEditStatus(&self.editor), x);
let with_status = |x|Split::n(false, 2, status, x);
let with_size = |x|lay!([&self.size, x]);
let arranger = ||lay!(|add|{
let color = self.color;
add(&Fill::wh(Tui::bg(color.darkest.rgb, "")))?;
add(&Fill::wh(Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb))))?;
add(&Self::render_mode(self))
});
Some(with_size(with_status(with_editbar(with_pool(with_transport(col!([
Fill::w(Fixed::h(20, arranger())),
Fill::wh(&self.editor),
])))))))
});
audio!(|self: ArrangerTui, client, scope|{
// Start profiling cycle
let t0 = self.perf.get_t0();

View file

@ -7,7 +7,7 @@ pub enum ArrangerMode {
/// Tracks are rows
H,
}
render!(<Tui>|self: ArrangerMode|{});
render!(<Tui>|self: ArrangerMode|"");
/// Arranger display mode can be cycled
impl ArrangerMode {
/// Cycle arranger display mode

View file

@ -11,7 +11,7 @@ from!(<'a>|args: &'a ArrangerTui|ArrangerVIns<'a> = Self {
tracks: &args.tracks,
});
render!(<Tui>|self: ArrangerVIns<'a>|());
render!(<Tui>|self: ArrangerVIns<'a>|"");
pub struct ArrangerVOuts<'a> {
size: &'a Measure<Tui>,
@ -23,4 +23,4 @@ from!(<'a>|args: &'a ArrangerTui|ArrangerVOuts<'a> = Self {
tracks: &args.tracks,
});
render!(<Tui>|self: ArrangerVOuts<'a>|());
render!(<Tui>|self: ArrangerVOuts<'a>|"");

View file

@ -3,7 +3,7 @@ use crate::*;
pub struct Bordered<S: BorderStyle, W: Render<Tui>>(pub S, pub W);
render!(<Tui>|self: Bordered<S: BorderStyle, W: Render<Tui>>|{
Fill::wh(lay!([Border(self.0), Padding::xy(1, 1, widget(&self.1))]))
Fill::wh(lay!([Border(self.0), Padding::xy(1, 1, &self.1)]))
});
pub struct Border<S: BorderStyle>(pub S);

View file

@ -1,49 +1,5 @@
use crate::*;
/// 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 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 {}
mod content; pub use self::content::*;
mod render; pub use self::render::*;
mod handle; pub use self::handle::*;

0
src/engine/render.rs Normal file
View file

View file

@ -142,9 +142,9 @@ render!(<Tui>|self:Groovebox|{
});
struct EditStatus<'a, T: Render<Tui>>(&'a Sampler, &'a MidiEditor, usize, T);
impl<'a, T: Render<Tui>> Render<Tui> for EditStatus<'a, T> {
impl<'a, T: Render<Tui>> Content<Tui> for EditStatus<'a, T> {
fn content (&self) -> impl Render<Tui> {
Split::n(false, 9, col!([
Some(Split::n(false, 9, col!([
row!(|add|{
if let Some(sample) = &self.0.mapped[self.2] {
add(&format!("Sample {}", sample.read().unwrap().end))?;
@ -162,7 +162,7 @@ impl<'a, T: Render<Tui>> Render<Tui> for EditStatus<'a, T> {
SampleViewer(None)
})),
]),
]), &self.3)
]), &self.3))
}
}

View file

@ -1,68 +0,0 @@
use crate::*;
/// 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);
}
/// Handle input
pub trait Handle<E: Engine>: Send + Sync {
fn handle (&mut self, context: &E::Input) -> Perhaps<E::Handled>;
}
#[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
}
}
}
}
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)
}
}

View file

@ -1,6 +1,20 @@
#![allow(unused)]
#![allow(clippy::unit_arg)]
pub use ::tek_engine::*;
pub(crate) use ::tek_engine::{
crossterm::{
ExecutableCommand,
EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode};
KeyCode, KeyModifiers, KeyEvent, KeyEventKind, KeyEventState
},
ratatui::{
prelude::{Style, Buffer},
style::{Stylize, Modifier},
backend::{Backend, CrosstermBackend, ClearType}
},
};
pub(crate) use std::cmp::{Ord, Eq, PartialEq};
pub(crate) use std::collections::BTreeMap;
pub(crate) use std::error::Error;
@ -16,6 +30,7 @@ pub(crate) use std::thread::{spawn, JoinHandle};
pub(crate) use std::time::Duration;
pub mod arranger; pub use self::arranger::*;
pub mod border; pub use self::border::*;
pub mod color; pub use self::color::*;
pub mod command; pub use self::command::*;
pub mod engine; pub use self::engine::*;
@ -23,12 +38,10 @@ pub mod event; pub use self::event::*;
pub mod file; pub use self::file::*;
pub mod focus; pub use self::focus::*;
pub mod groovebox; pub use self::groovebox::*;
pub mod input; pub use self::input::*;
pub mod jack; pub use self::jack::*;
pub mod meter; pub use self::meter::*;
pub mod midi; pub use self::midi::*;
pub mod mixer; pub use self::mixer::*;
pub mod output; pub use self::output::*;
pub mod piano_h; pub use self::piano_h::*;
pub mod plugin; pub use self::plugin::*;
pub mod pool; pub use self::pool::*;
@ -36,28 +49,15 @@ pub mod sampler; pub use self::sampler::*;
pub mod sequencer; pub use self::sequencer::*;
pub mod space; pub use self::space::*;
pub mod status; pub use self::status::*;
pub mod style; pub use self::theme::*;
pub mod theme; pub use self::theme::*;
pub mod time; pub use self::time::*;
pub mod transport; pub use self::transport::*;
pub mod tui; pub use self::tui::*;
pub use ::better_panic;
pub(crate) use better_panic::{Settings, Verbosity};
pub use ::atomic_float;
pub(crate) use atomic_float::*;
pub use ::crossterm;
pub(crate) use crossterm::{ExecutableCommand};
pub(crate) use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode};
pub(crate) use crossterm::event::{KeyCode, KeyModifiers, KeyEvent, KeyEventKind, KeyEventState};
pub use ::ratatui;
pub(crate) use ratatui::{
prelude::{Style, Buffer},
style::{Stylize, Modifier},
backend::{Backend, CrosstermBackend, ClearType}
};
pub use ::midly::{self, num::u7};
pub(crate) use ::midly::{
Smf,
@ -75,12 +75,6 @@ pub(crate) use ::palette::{
testmod! { test }
/// 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>>;
/// Define test modules.
#[macro_export] macro_rules! testmod {
($($name:ident)*) => { $(#[cfg(test)] mod $name;)* };
@ -126,3 +120,29 @@ impl Gettable<usize> for AtomicUsize {
impl InteriorMutable<usize> for AtomicUsize {
fn set (&self, value: usize) -> usize { self.swap(value, Relaxed) }
}
#[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
}
}
from!(|size:(usize, usize)| BigBuffer = Self::new(size.0, size.1));

View file

@ -107,11 +107,13 @@ impl Default for MidiEditor {
}
has_size!(<Tui>|self: MidiEditor|&self.size);
render!(<Tui>|self: MidiEditor|{
self.autoscroll();
self.autozoom();
&self.mode
});
//render!(<Tui>|self: MidiEditor|lay!(|add|{add(&self.size)?;add(self.mode)}));//bollocks
pub trait PhraseViewMode: Render<Tui> + HasSize<Tui> + MidiRange + MidiPoint + Debug + Send + Sync {
@ -125,6 +127,12 @@ pub trait PhraseViewMode: Render<Tui> + HasSize<Tui> + MidiRange + MidiPoint + D
}
}
impl Content<Tui> for Box<dyn PhraseViewMode> {
fn content (&self) -> impl Render<Tui> {
Some(&(*self))
}
}
impl MidiView<Tui> for MidiEditor {}
impl TimeRange for MidiEditor {

View file

@ -1,177 +0,0 @@
use crate::*;
/// 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)
}
/// 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>
}
#[macro_export] macro_rules! render {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*E$(,$T:ident$(:$U:path)?)*$(,)?>)?|$cb:expr) => {
impl<E: Engine, $($($L),*$($T $(: $U)?),*)?> Render<E> for $Struct $(<$($L,)* E, $($T),*>)? {
fn content (&$self) -> Option<impl Render<$E>> {
Some($cb)
}
}
};
(<$E:ty>|$self:ident:$Struct:ident$(<
$($($L:lifetime),+)?
$($($T:ident$(:$U:path)?),+)?
>)?|$cb:expr) => {
impl $(<
$($($L),+)?
$($($T$(:$U)?),+)?
>)? Render<$E> for $Struct $(<
$($($L),+)?
$($($T),+)?
>)? {
fn content (&$self) -> Option<impl Render<$E>> {
Some($cb)
}
}
}
}
/// 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<()>;
}
/// A renderable component
pub trait Render<E: Engine>: Send + Sync where (): Render<E> {
fn content (&self) -> Option<impl Render<E>> where Self: Sized {
None::<()>
}
/// 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> Render<E> for () {
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
Ok(None)
}
fn render (&self, to: &mut E::Output) -> Usually<()> {
Ok(())
}
}
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> 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(()))
}
}
/// 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)
}
}

View file

@ -2,7 +2,7 @@ use crate::*;
use super::note_y_iter;
pub struct PianoHorizontalCursor<'a>(pub(crate) &'a PianoHorizontal);
render!(<Tui>|self: PianoHorizontalCursor<'a>|render(|to|Ok({
render!(<Tui>|self: PianoHorizontalCursor<'a>|render(|to: &mut TuiOutput|Ok({
let style = Some(Style::default().fg(self.0.color.lightest.rgb));
let note_hi = self.0.note_hi();
let note_len = self.0.note_len();

View file

@ -2,7 +2,7 @@ use crate::*;
use super::note_y_iter;
pub struct PianoHorizontalNotes<'a>(pub(crate) &'a PianoHorizontal);
render!(<Tui>|self: PianoHorizontalNotes<'a>|render(|to|Ok({
render!(<Tui>|self: PianoHorizontalNotes<'a>|render(|to: &mut TuiOutput|Ok({
let time_start = self.0.time_start().get();
let note_lo = self.0.note_lo().get();
let note_hi = self.0.note_hi();

View file

@ -1,7 +1,7 @@
use crate::*;
pub struct PianoHorizontalTimeline<'a>(pub(crate) &'a PianoHorizontal);
render!(<Tui>|self: PianoHorizontalTimeline<'a>|render(|to|{
render!(<Tui>|self: PianoHorizontalTimeline<'a>|render(|to: &mut TuiOutput|{
let [x, y, w, h] = to.area();
let style = Some(Style::default().dim());
let length = self.0.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);

View file

@ -5,7 +5,7 @@ use ratatui::{prelude::Rect, widgets::{Widget, canvas::{Canvas, Points, Line}}};
const EMPTY: &[(f64, f64)] = &[(0., 0.), (1., 1.), (2., 2.), (0., 2.), (2., 0.)];
pub struct SampleViewer(pub Option<Arc<RwLock<Sample>>>);
render!(<Tui>|self: SampleViewer|render(|to|{
render!(<Tui>|self: SampleViewer|render(|to: &mut TuiOutput|{
let [x, y, width, height] = to.area();
let area = Rect { x, y, width, height };
let min_db = -40.0;

View file

@ -14,126 +14,32 @@ mod split; pub use self::split::*;
/// A cardinal direction.
#[derive(Copy, Clone, PartialEq)]
pub enum Direction { North, South, West, East, }
use self::Direction::*;
pub use self::Direction::{self, *};
/// 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>);
/// Renders multiple things on top of each other,
/// in the order they are provided by the callback.
/// Total size is largest width x largest height.
pub struct Layers<
E: Engine,
F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render<E>)->Usually<()>)->Usually<()>
>(pub F, PhantomData<E>);
/// Shorthand for defining an instance of [Layers].
#[macro_export] macro_rules! lay {
([$($expr:expr),* $(,)?]) => {
Layers::new(move|add|{ $(add(&$expr)?;)* Ok(()) })
};
(![$($expr:expr),* $(,)?]) => {
Layers::new(|add|{ $(add(&$expr)?;)* Ok(()) })
};
($expr:expr) => {
Layers::new($expr)
};
}
/// 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)
impl Direction {
#[inline] fn split_fixed <N: Coordinate> (self, area: Area<N>, a: N) -> ([N;4],[N;4]) {
match area {
Direction::North => (
[area.x(), (area.y()+area.h()).minus(a), area.w(), a],
[area.x(), area.y(), area.w(), area.h().minus(a)],
),
Direction::South => (
[area.x(), area.y(), area.w(), a],
[area.x(), area.y() + a, area.w(), area.h().minus(a)],
),
Direction::East => (
[area.x(), area.y(), a, area.h()],
[area.x() + a, area.y(), area.w().minus(a), area.h()],
),
Direction::West => (
[area.x() + area.w() - a, area.y(), a, area.h()],
[area.x(), area.y(), area.w() - a, area.h()],
),
}
}
}
pub trait HasSize<E: Engine> {
fn size (&self) -> &Measure<E>;
}
#[macro_export] macro_rules! has_size {
(<$E:ty>|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasSize<$E> for $Struct $(<$($L),*$($T),*>)? {
fn size (&$self) -> &Measure<$E> { $cb }
}
}
}
/// A widget that tracks its render width and height
#[derive(Default)]
pub struct Measure<E: Engine> {
_engine: PhantomData<E>,
pub x: Arc<AtomicUsize>,
pub y: Arc<AtomicUsize>,
}
pub struct ShowMeasure<'a>(&'a Measure<Tui>);
pub trait LayoutDebug<E: Engine> {
fn debug <W: Render<E>> (other: W) -> DebugOverlay<E, W> {
DebugOverlay(Default::default(), other)
}
}
pub struct DebugOverlay<E: Engine, W: Render<E>>(PhantomData<E>, pub W);
pub trait Area<N: Coordinate>: Copy {
fn x (&self) -> N;
fn y (&self) -> N;
fn w (&self) -> N;
fn h (&self) -> N;
trait AreaMod<N: Coordinate>: Area<N> {
fn x2 (&self) -> N {
self.x() + self.w()
}
@ -176,35 +82,72 @@ pub trait Area<N: Coordinate>: Copy {
#[inline] fn clip (&self, wh: impl Size<N>) -> [N;4] {
[self.x(), self.y(), wh.w(), wh.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)
}
}
#[inline] fn split_fixed (&self, direction: Direction, a: N) -> ([N;4],[N;4]) {
match direction {
Direction::North => (
[self.x(), (self.y()+self.h()).minus(a), self.w(), a],
[self.x(), self.y(), self.w(), self.h().minus(a)],
),
Direction::South => (
[self.x(), self.y(), self.w(), a],
[self.x(), self.y() + a, self.w(), self.h().minus(a)],
),
Direction::East => (
[self.x(), self.y(), a, self.h()],
[self.x() + a, self.y(), self.w().minus(a), self.h()],
),
Direction::West => (
[self.x() + self.w() - a, self.y(), a, self.h()],
[self.x(), self.y(), self.w() - a, self.h()],
),
}
/// Renders multiple things on top of each other,
/// in the order they are provided by the callback.
/// Total size is largest width x largest height.
pub struct Layers<
E: Engine,
F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render<E>)->Usually<()>)->Usually<()>
>(pub F, PhantomData<E>);
/// Shorthand for defining an instance of [Layers].
#[macro_export] macro_rules! lay {
([$($expr:expr),* $(,)?]) => {
Layers::new(move|add|{ $(add(&$expr)?;)* Ok(()) })
};
(![$($expr:expr),* $(,)?]) => {
Layers::new(|add|{ $(add(&$expr)?;)* Ok(()) })
};
($expr:expr) => {
Layers::new($expr)
};
}
pub trait HasSize<E: Engine> {
fn size (&self) -> &Measure<E>;
}
#[macro_export] macro_rules! has_size {
(<$E:ty>|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasSize<$E> for $Struct $(<$($L),*$($T),*>)? {
fn size (&$self) -> &Measure<$E> { $cb }
}
}
}
/// A widget that tracks its render width and height
#[derive(Default)]
pub struct Measure<E: Engine> {
_engine: PhantomData<E>,
pub x: Arc<AtomicUsize>,
pub y: Arc<AtomicUsize>,
}
pub struct ShowMeasure<'a>(&'a Measure<Tui>);
pub trait LayoutDebug<E: Engine> {
fn debug <W: Render<E>> (other: W) -> DebugOverlay<E, W> {
DebugOverlay(Default::default(), other)
}
}
pub struct DebugOverlay<E: Engine, W: Render<E>>(PhantomData<E>, pub W);
impl<T: Render<Tui>> Render<Tui> for DebugOverlay<Tui, T> {
fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
self.1.min_size(to)
}
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
let [x, y, w, h] = to.area();
self.1.render(to)?;
Ok(to.blit(&format!("{w}x{h}+{x}+{y}"), x, y, Some(Style::default().green())))
}
}
macro_rules! by_axis {
(!$Enum:ident) => {
impl<E: Engine, T: Render<E>> $Enum<E, T> {

View file

@ -56,7 +56,7 @@ impl Measure<Tui> {
}
}
render!(<Tui>|self: ShowMeasure<'a>|render(|to|Ok({
render!(<Tui>|self: ShowMeasure<'a>|render(|to: &mut TuiOutput|Ok({
let w = self.0.w();
let h = self.0.h();
to.blit(&format!(" {w} x {h} "), to.area.x(), to.area.y(), Some(

View file

@ -1,143 +1,3 @@
use crate::*;
mod tui_input; pub(crate) use self::tui_input::*;
pub use self::tui_input::TuiInput;
mod tui_output; pub(crate) use self::tui_output::*;
pub use self::tui_output::TuiOutput;
mod tui_style;
mod tui_theme; pub(crate) use self::tui_theme::*;
mod tui_border; pub(crate) use self::tui_border::*;
////////////////////////////////////////////////////////
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
}
}

View file

@ -1,160 +0,0 @@
use crate::*;
use ::crossterm::event::*;
pub struct TuiInput {
pub(crate) exited: Arc<AtomicBool>,
pub(crate) event: TuiEvent,
}
#[derive(Debug, Clone, PartialEq)]
pub enum TuiEvent {
/// Terminal input
Input(Event),
/// JACK notification
_Jack(JackEvent)
}
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
}))
};
}
pub(crate) type KeyMapping<const N: usize, T> = [(TuiEvent, &'static dyn Fn(&T)->PhraseCommand);N];
#[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>];
*/

View file

@ -1,158 +0,0 @@
use crate::*;
use ratatui::buffer::Cell;
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
}
}
#[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
}
}
from!(|size:(usize, usize)| BigBuffer = Self::new(size.0, size.1));
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 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 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))
}
}
impl<T: Render<Tui>> Render<Tui> for DebugOverlay<Tui, T> {
fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
self.1.min_size(to)
}
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
let [x, y, w, h] = to.area();
self.1.render(to)?;
Ok(to.blit(&format!("{w}x{h}+{x}+{y}"), x, y, Some(Style::default().green())))
}
}