wip(108e): layout refactor

This commit is contained in:
🪞👃🪞 2024-12-09 11:00:48 +01:00
parent 265d4a3953
commit ddb3c28c01
21 changed files with 1141 additions and 825 deletions

View file

@ -9,7 +9,6 @@ backtrace = "0.3.72"
better-panic = "0.3.0"
clap = { version = "4.5.4", features = [ "derive" ] }
clojure-reader = "0.1.0"
crossterm = "0.27"
jack = "0.13"
midly = "0.5"
once_cell = "1.19.0"

View file

@ -35,7 +35,7 @@ impl Demo<Tui> {
impl Content for Demo<Tui> {
type Engine = Tui;
fn content (&self) -> impl Render<Engine = Tui> {
fn content (&self) -> dyn Render<Engine = Tui> {
let border_style = Style::default().fg(Color::Rgb(0,0,0));
Align::Center(Layers::new(move|add|{

View file

@ -1,5 +1,6 @@
use crate::*;
use rand::{thread_rng, distributions::uniform::UniformSampler};
pub use ratatui::prelude::Color;
/// A color in OKHSL and RGB representations.
#[derive(Debug, Default, Copy, Clone, PartialEq)]

View file

@ -0,0 +1,76 @@
use crate::*;
pub enum Collect<'a, E: Engine, const N: usize> {
Callback(CallbackCollection<'a, E>),
//Iterator(IteratorCollection<'a, E>),
Array(ArrayCollection<'a, E, N>),
Slice(SliceCollection<'a, E>),
}
impl<'a, E: Engine, const N: usize> Collect<'a, E, N> {
pub fn iter (&'a self) -> CollectIterator<'a, E, N> {
CollectIterator(0, &self)
}
}
impl<'a, E: Engine, const N: usize> From<CallbackCollection<'a, E>> for Collect<'a, E, N> {
fn from (callback: CallbackCollection<'a, E>) -> Self {
Self::Callback(callback)
}
}
impl<'a, E: Engine, const N: usize> From<SliceCollection<'a, E>> for Collect<'a, E, N> {
fn from (slice: SliceCollection<'a, E>) -> Self {
Self::Slice(slice)
}
}
impl<'a, E: Engine, const N: usize> From<ArrayCollection<'a, E, N>> for Collect<'a, E, N>{
fn from (array: ArrayCollection<'a, E, N>) -> Self {
Self::Array(array)
}
}
type CallbackCollection<'a, E> =
&'a dyn Fn(&'a mut dyn FnMut(&dyn Render<Engine = E>)->Usually<()>);
//type IteratorCollection<'a, E> =
//&'a mut dyn Iterator<Item = dyn Render<Engine = E>>;
type SliceCollection<'a, E> =
&'a [&'a dyn Render<Engine = E>];
type ArrayCollection<'a, E, const N: usize> =
[&'a dyn Render<Engine = E>; N];
pub struct CollectIterator<'a, E: Engine, const N: usize>(usize, &'a Collect<'a, E, N>);
impl<'a, E: Engine, const N: usize> Iterator for CollectIterator<'a, E, N> {
type Item = &'a dyn Render<Engine = E>;
fn next (&mut self) -> Option<Self::Item> {
match self.1 {
Collect::Callback(callback) => {
todo!()
},
//Collection::Iterator(iterator) => {
//iterator.next()
//},
Collect::Array(array) => {
if let Some(item) = array.get(self.0) {
self.0 += 1;
Some(item)
} else {
None
}
}
Collect::Slice(slice) => {
if let Some(item) = slice.get(self.0) {
self.0 += 1;
Some(item)
} else {
None
}
}
}
}
}

View file

@ -1,5 +1,4 @@
pub use ratatui;
pub use crossterm;
pub use jack;
pub use midly;
pub use clap;
@ -11,12 +10,8 @@ pub use std::rc::Rc;
pub use std::cell::{Cell, RefCell};
pub use std::marker::PhantomData;
pub(crate) use std::error::Error;
pub(crate) use std::io::{stdout};
pub(crate) use std::thread::{spawn, JoinHandle};
pub(crate) use std::time::Duration;
pub(crate) use atomic_float::*;
pub(crate) use palette::{*, convert::*, okhsl::*};
use better_panic::{Settings, Verbosity};
use std::ops::{Add, Sub, Mul, Div, Rem};
use std::cmp::{Ord, Eq, PartialEq};
use std::fmt::{Debug, Display};
@ -46,7 +41,8 @@ submod! {
pitch
space
time
tui
//tui
layout
}
testmod! {

View file

@ -19,6 +19,9 @@ pub trait Coordinate: Send + Sync + Copy
0.into()
}
}
fn ZERO () -> Self {
0.into()
}
}
impl<T> Coordinate for T where T: Send + Sync + Copy
@ -702,6 +705,9 @@ impl<
#[inline] pub fn down (build: F) -> Self {
Self::new(Direction::Down, build)
}
#[inline] pub fn up (build: F) -> Self {
Self::new(Direction::Up, build)
}
}
impl<E: Engine, F> Render for Stack<E, F>
@ -710,63 +716,119 @@ where
{
type Engine = E;
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
let mut w = 0.into();
let mut h = 0.into();
match self.1 {
Direction::Down => {
(self.0)(&mut |component| {
if h >= to.h() { return Ok(()) }
let size = component.push_y(h).max_y(to.h() - h).min_size(to)?;
if let Some([width, height]) = size.map(|size|size.wh()) {
h = h + height.into();
if width > w { w = width; }
let mut w: E::Unit = 0.into();
let mut h: E::Unit = 0.into();
(self.0)(&mut |component: &dyn Render<Engine = E>| {
let max = to.h().minus(h);
if max > E::Unit::ZERO() {
let item = component.push_y(h).max_y(max);
let size = item.min_size(to)?.map(|size|size.wh());
if let Some([width, height]) = size {
h = h + height.into();
w = w.max(width);
}
}
Ok(())
})?;
Ok(Some([w, h].into()))
},
Direction::Right => {
(self.0)(&mut |component| {
if w >= to.w() { return Ok(()) }
let size = component.push_x(w).max_x(to.w() - w).min_size(to)?;
if let Some([width, height]) = size.map(|size|size.wh()) {
w = w + width.into();
if height > h { h = height }
let mut w: E::Unit = 0.into();
let mut h: E::Unit = 0.into();
(self.0)(&mut |component: &dyn Render<Engine = E>| {
let max = to.w().minus(w);
if max > E::Unit::ZERO() {
let item = component.push_x(w).max_x(max);
let size = item.min_size(to)?.map(|size|size.wh());
if let Some([width, height]) = size {
w = w + width.into();
h = h.max(height);
}
}
Ok(())
})?;
Ok(Some([w, h].into()))
},
_ => todo!()
};
Ok(Some([w, h].into()))
Direction::Up => {
let mut w: E::Unit = 0.into();
let mut h: E::Unit = 0.into();
(self.0)(&mut |component: &dyn Render<Engine = E>| {
let max = to.h().minus(h);
if max > E::Unit::ZERO() {
let item = component.max_y(to.h() - h);
let size = item.min_size(to)?.map(|size|size.wh());
if let Some([width, height]) = size {
h = h + height.into();
w = w.max(width);
}
}
Ok(())
})?;
Ok(Some([w, h].into()))
},
Direction::Left => {
let mut w: E::Unit = 0.into();
let mut h: E::Unit = 0.into();
(self.0)(&mut |component: &dyn Render<Engine = E>| {
if w < to.w() {
todo!();
}
Ok(())
})?;
Ok(Some([w, h].into()))
},
}
}
fn render (&self, to: &mut E::Output) -> Usually<()> {
let area = to.area();
let mut w = 0.into();
let mut h = 0.into();
match self.1 {
Direction::Down => {
(self.0)(&mut |component| {
if h >= area.h() { return Ok(()) }
let item = component.push_y(h).max_y(area.h() - h);
let size = item.min_size(area.wh().into())?;
if let Some([width, height]) = size.map(|size|size.wh()) {
item.render(to)?;
h = h + height;
if width > w { w = width }
};
(self.0)(&mut |item| {
if h < area.h() {
let item = item.push_y(h).max_y(area.h() - h);
let show = item.min_size(area.wh().into())?.map(|s|s.wh());
if let Some([width, height]) = show {
item.render(to)?;
h = h + height;
if width > w { w = width }
};
}
Ok(())
})?;
},
Direction::Right => {
(self.0)(&mut |component| {
if w >= area.w() { return Ok(()) }
let item = component.push_x(w).max_x(area.w() - w);
let size = item.min_size(area.wh().into())?;
if let Some([width, height]) = size.map(|size|size.wh()) {
item.render(to)?;
w = width + w;
if height > h { h = height }
};
(self.0)(&mut |item| {
if w < area.w() {
let item = item.push_x(w).max_x(area.w() - w);
let show = item.min_size(area.wh().into())?.map(|s|s.wh());
if let Some([width, height]) = show {
item.render(to)?;
w = width + w;
if height > h { h = height }
};
}
Ok(())
})?;
},
Direction::Up => {
(self.0)(&mut |item| {
if h < area.h() {
let show = item.min_size([area.w(), area.h().minus(h)].into())?.map(|s|s.wh());
if let Some([width, height]) = show {
item.push_y(area.h() - height).shrink_y(height).render(to)?;
h = h + height;
if width > w { w = width }
};
}
Ok(())
})?;
},

View file

@ -245,9 +245,19 @@ impl Timebase {
impl Default for Timebase {
fn default () -> Self { Self::new(48000f64, 150f64, DEFAULT_PPQ) }
}
#[derive(Debug, Clone)]
pub enum Moment2 {
None,
Zero,
Usec(Microsecond),
Sample(SampleCount),
Pulse(Pulse),
}
/// A point in time in all time scales (microsecond, sample, MIDI pulse)
#[derive(Debug, Default, Clone)]
pub struct Instant {
pub struct Moment {
pub timebase: Arc<Timebase>,
/// Current time in microseconds
pub usec: Microsecond,
@ -256,7 +266,7 @@ pub struct Instant {
/// Current time in MIDI pulses
pub pulse: Pulse,
}
impl Instant {
impl Moment {
pub fn zero (timebase: &Arc<Timebase>) -> Self {
Self { usec: 0.into(), sample: 0.into(), pulse: 0.into(), timebase: timebase.clone() }
}

View file

@ -1,690 +0,0 @@
use crate::*;
pub(crate) use ratatui::buffer::Cell;
pub(crate) use crossterm::{ExecutableCommand};
pub use crossterm::event::{Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState};
pub use ratatui::prelude::{Rect, Style, Color, Buffer};
pub use ratatui::style::{Stylize, Modifier};
use ratatui::backend::{Backend, CrosstermBackend, ClearType};
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::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.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 size = engine.read().unwrap().backend.size().expect("get size failed");
let mut buffer = Buffer::empty(size);
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 {
engine.write().unwrap().backend.clear_region(ClearType::All)
.expect("clear failed");
buffer.resize(size);
buffer.reset();
}
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.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 { 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 {
// TODO remove
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>];
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<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_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(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 Render for &str {
type Engine = Tui;
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 for String {
type Engine = Tui;
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<Engine = Tui>> Render for DebugOverlay<Tui, T> {
type Engine = Tui;
fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
self.0.min_size(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: Render<Engine = Tui>>(pub Option<Style>, pub T);
impl Render for Styled<&str> {
type Engine = Tui;
fn min_size (&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.min_size(to.area().wh())?.unwrap();
Ok(to.blit(&self.1, x, y, None))
}
}
pub trait TuiStyle: Render<Engine = Tui> + Sized {
fn fg (self, color: Color) -> impl Render<Engine = Tui> {
Layers::new(move |add|{ add(&Foreground(color))?; add(&self) })
}
fn bg (self, color: Color) -> impl Render<Engine = Tui> {
Layers::new(move |add|{ add(&Background(color))?; add(&self) })
}
fn bold (self, on: bool) -> impl Render<Engine = Tui> {
Layers::new(move |add|{ add(&Bold(on))?; add(&self) })
}
fn border (self, style: impl BorderStyle) -> impl Render<Engine = Tui> {
Bordered(style, self)
}
}
impl<W: Render<Engine = Tui>> TuiStyle for W {}
pub struct Bold(pub bool);
impl Render for Bold {
type Engine = Tui;
fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> { Ok(Some([0,0])) }
fn render (&self, to: &mut TuiOutput) -> Usually<()> { Ok(to.fill_bold(to.area(), self.0)) }
}
pub struct Foreground(pub Color);
impl Render for Foreground {
type Engine = Tui;
fn min_size (&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 Render for Background {
type Engine = Tui;
fn min_size (&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> Render for Border<S> {
type Engine = Tui;
fn min_size (&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: Render<Engine = Tui>>(pub S, pub W);
impl<S: BorderStyle, W: Render<Engine = Tui>> Content for Bordered<S, W> {
type Engine = Tui;
fn content (&self) -> impl Render<Engine = Tui> {
let content: &dyn Render<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 Render for $T {
type Engine = Tui;
fn min_size (&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 CORNERS: CornersTall = CornersTall(Style {
fg: Some(Color::Rgb(96, 255, 32)),
bg: None,
underline_color: None,
add_modifier: Modifier::empty(),
sub_modifier: Modifier::DIM
});
/// 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>]
}
}
/// 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
}))
}
}
#[macro_export] macro_rules! key_lit {
($code:expr) => {
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:expr) => {
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:expr) => {
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:expr) => {
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
}))
}
}