output: refactor Content and Render traits

This commit is contained in:
🪞👃🪞 2025-09-06 08:46:52 +03:00
parent 74b3af2212
commit 1c21a85f27
24 changed files with 346 additions and 613 deletions

View file

@ -5,7 +5,7 @@ macro_rules! impl_content_layout_render {
layout = $layout:expr;
render = $render:expr) =>
{
impl Content<$Output> for $Struct {
impl Render<$Output> for $Struct {
fn layout (&$self, $to: [u16;4]) -> [u16;4] { $layout }
fn render (&$self, $to: &mut $Output) { $render }
}
@ -25,19 +25,19 @@ mod tui_string; pub use self::tui_string::*;
mod tui_style; pub use self::tui_style::*;
mod tui_tryptich; pub use self::tui_tryptich::*;
impl<T: Content<TuiOut>> Content<TuiOut> for std::sync::Arc<T> {
impl<T: Render<TuiOut>> Render<TuiOut> for std::sync::Arc<T> {
fn layout (&self, to: [u16;4]) -> [u16;4] {
Content::<TuiOut>::layout(&**self, to)
Render::<TuiOut>::layout(&**self, to)
}
fn render (&self, to: &mut TuiOut) {
Content::<TuiOut>::render(&**self, to)
Render::<TuiOut>::render(&**self, to)
}
}
impl<T: Content<TuiOut>> Content<TuiOut> for Result<T, Box<dyn std::error::Error>> {
fn content (&self) -> impl Render<TuiOut> {
Bsp::a(self.as_ref().ok(), self.as_ref().err()
.map(|e|Tui::fg_bg(Color::Rgb(255,255,255), Color::Rgb(32,32,32), e.to_string())))
impl<T: Render<TuiOut>> Content<TuiOut> for Result<T, Box<dyn std::error::Error>> {
fn content (&self) -> Option<impl Render<TuiOut> + '_> {
Some(Bsp::a(self.as_ref().ok(), self.as_ref().err()
.map(|e|Tui::fg_bg(Color::Rgb(255,255,255), Color::Rgb(32,32,32), e.to_string()))))
}
}

View file

@ -1,40 +1,49 @@
use crate::*;
pub struct Bordered<S: BorderStyle, W: Content<TuiOut>>(pub bool, pub S, pub W);
content!(TuiOut: |self: Bordered<S: BorderStyle, W: Content<TuiOut>>|Fill::xy(
lay!(When::new(self.0, Border(self.0, self.1)), Padding::xy(1, 1, &self.2))
));
pub struct Bordered<S, W>(pub bool, pub S, pub W);
impl<S: BorderStyle, W: Render<TuiOut>> Content<TuiOut> for Bordered<S, W> {
fn content (&self) -> Option<impl Render<TuiOut> + '_> {
Some(Fill::xy(
lay!(When::new(self.0, Border(self.0, self.1)), Padding::xy(1, 1, &self.2))
))
}
}
pub struct Border<S: BorderStyle>(pub bool, pub S);
render!(TuiOut: |self: Border<S: BorderStyle>, to| {
if self.0 {
let area = to.area();
if area.w() > 0 && area.y() > 0 {
to.blit(&self.1.nw(), area.x(), area.y(), self.1.style());
to.blit(&self.1.ne(), area.x() + area.w() - 1, area.y(), self.1.style());
to.blit(&self.1.sw(), area.x(), area.y() + area.h() - 1, self.1.style());
to.blit(&self.1.se(), area.x() + area.w() - 1, area.y() + area.h() - 1, self.1.style());
for x in area.x()+1..area.x()+area.w()-1 {
to.blit(&self.1.n(), x, area.y(), self.1.style());
to.blit(&self.1.s(), x, area.y() + area.h() - 1, self.1.style());
}
for y in area.y()+1..area.y()+area.h()-1 {
to.blit(&self.1.w(), area.x(), y, self.1.style());
to.blit(&self.1.e(), area.x() + area.w() - 1, y, self.1.style());
impl<S: BorderStyle> Render<TuiOut> for Border<S> {
fn layout (&self, area: [u16;4]) -> [u16;4] {
self.1.layout(area)
}
fn render (&self, to: &mut TuiOut) {
if self.0 {
let area = to.area();
if area.w() > 0 && area.y() > 0 {
to.blit(&self.1.nw(), area.x(), area.y(), self.1.style());
to.blit(&self.1.ne(), area.x() + area.w() - 1, area.y(), self.1.style());
to.blit(&self.1.sw(), area.x(), area.y() + area.h() - 1, self.1.style());
to.blit(&self.1.se(), area.x() + area.w() - 1, area.y() + area.h() - 1, self.1.style());
for x in area.x()+1..area.x()+area.w()-1 {
to.blit(&self.1.n(), x, area.y(), self.1.style());
to.blit(&self.1.s(), x, area.y() + area.h() - 1, self.1.style());
}
for y in area.y()+1..area.y()+area.h()-1 {
to.blit(&self.1.w(), area.x(), y, self.1.style());
to.blit(&self.1.e(), area.x() + area.w() - 1, y, self.1.style());
}
}
}
}
});
}
pub trait BorderStyle: Send + Sync + Copy {
pub trait BorderStyle: Render<TuiOut> + Copy {
fn enabled (&self) -> bool;
fn enclose <W: Content<TuiOut>> (self, w: W) -> impl Content<TuiOut> {
fn enclose <W: Render<TuiOut>> (self, w: W) -> impl Render<TuiOut> {
Bsp::b(Fill::xy(Border(self.enabled(), self)), w)
}
fn enclose2 <W: Content<TuiOut>> (self, w: W) -> impl Content<TuiOut> {
fn enclose2 <W: Render<TuiOut>> (self, w: W) -> impl Render<TuiOut> {
Bsp::b(Margin::xy(1, 1, Fill::xy(Border(self.enabled(), self))), w)
}
fn enclose_bg <W: Content<TuiOut>> (self, w: W) -> impl Content<TuiOut> {
fn enclose_bg <W: Render<TuiOut>> (self, w: W) -> impl Render<TuiOut> {
Tui::bg(self.style().unwrap().bg.unwrap_or(Color::Reset),
Bsp::b(Fill::xy(Border(self.enabled(), self)), w))
}
@ -138,7 +147,7 @@ macro_rules! border {
fn enabled (&self) -> bool { self.0 }
}
#[derive(Copy, Clone)] pub struct $T(pub bool, pub Style);
impl Content<TuiOut> for $T {
impl Render<TuiOut> for $T {
fn render (&self, to: &mut TuiOut) {
if self.enabled() { let _ = self.draw(to); }
}

View file

@ -2,23 +2,26 @@ use crate::*;
use ratatui::style::Stylize;
// Thunks can be natural error boundaries!
pub struct ErrorBoundary<O: Output, T: Content<O>>(std::marker::PhantomData<O>, Perhaps<T>);
pub struct ErrorBoundary<O: Output, T: Render<O>>(
std::marker::PhantomData<O>, Perhaps<T>
);
impl<O: Output, T: Content<O>> ErrorBoundary<O, T> {
impl<O: Output, T: Render<O>> ErrorBoundary<O, T> {
pub fn new (content: Perhaps<T>) -> Self {
Self(Default::default(), content)
}
}
impl<T: Content<TuiOut>> Content<TuiOut> for ErrorBoundary<TuiOut, T> {
fn content (&self) -> impl Render<TuiOut> + '_ {
ThunkRender::new(|to|match self.1.as_ref() {
impl<T: Render<TuiOut>> Render<TuiOut> for ErrorBoundary<TuiOut, T> {
fn render (&self, to: &mut TuiOut) {
match self.1.as_ref() {
Ok(Some(content)) => content.render(to),
Ok(None) => to.blit(&"empty?", 0, 0, Some(Style::default().yellow())),
Err(e) => Content::render(&Tui::fg_bg(
Err(e) => Tui::fg_bg(
Color::Rgb(255,224,244), Color::Rgb(96,24,24), Bsp::s(
Bsp::e(Tui::bold(true, "oops. "), "rendering failed."),
Bsp::e("\"why?\" ", Tui::bold(true, &format!("{e}"))))), to)
})
Bsp::e("\"why?\" ", Tui::bold(true, &format!("{e}"))))
).render(to)
}
}
}

View file

@ -1,30 +1,30 @@
use crate::*;
pub struct FieldH<T, U>(pub ItemTheme, pub T, pub U);
impl<T: Content<TuiOut>, U: Content<TuiOut>> Content<TuiOut> for FieldH<T, U> {
fn content (&self) -> impl Render<TuiOut> {
impl<T: Render<TuiOut>, U: Render<TuiOut>> Content<TuiOut> for FieldH<T, U> {
fn content (&self) -> Option<impl Render<TuiOut> + '_> {
let Self(ItemTheme { darkest, dark, lightest, .. }, title, value) = self;
row!(
Some(row!(
Tui::fg_bg(dark.rgb, darkest.rgb, ""),
Tui::fg_bg(lightest.rgb, dark.rgb, title),
Tui::fg_bg(dark.rgb, darkest.rgb, ""),
Tui::fg_bg(lightest.rgb, darkest.rgb, Tui::bold(true, value)),
)
))
}
}
pub struct FieldV<T, U>(pub ItemTheme, pub T, pub U);
impl<T: Content<TuiOut>, U: Content<TuiOut>> Content<TuiOut> for FieldV<T, U> {
fn content (&self) -> impl Render<TuiOut> {
impl<T: Render<TuiOut>, U: Render<TuiOut>> Content<TuiOut> for FieldV<T, U> {
fn content (&self) -> Option<impl Render<TuiOut> + '_> {
let Self(ItemTheme { darkest, dark, lightest, .. }, title, value) = self;
Bsp::n(
Some(Bsp::n(
Align::w(Tui::bg(darkest.rgb, Tui::fg(lightest.rgb, Tui::bold(true, value)))),
Fill::x(Align::w(row!(
Tui::bg(darkest.rgb, Tui::fg(dark.rgb, "")),
Tui::bg(dark.rgb, Tui::fg(lightest.rgb, title)),
Tui::bg(darkest.rgb, Tui::fg(dark.rgb, "")),
)))
)
))
}
}
@ -40,9 +40,9 @@ pub struct Field<T, U> {
pub value_bg: Option<ItemColor>,
pub value_align: Option<Direction>,
}
impl<T: Content<TuiOut>, U: Content<TuiOut>> Content<TuiOut> for Field<T, U> {
fn content (&self) -> impl Render<TuiOut> {
"TODO"
impl<T: Render<TuiOut>, U: Render<TuiOut>> Content<TuiOut> for Field<T, U> {
fn content (&self) -> Option<impl Render<TuiOut> + '_> {
Some("TODO")
}
}

View file

@ -1,5 +1,13 @@
use crate::*;
render!(TuiOut: |self: u64, _to|todo!());
impl Render<TuiOut> for u64 {
fn render (&self, _to: &mut TuiOut) {
todo!()
}
}
render!(TuiOut: |self: f64, _to|todo!());
impl Render<TuiOut> for f64 {
fn render (&self, _to: &mut TuiOut) {
todo!()
}
}

View file

@ -12,24 +12,24 @@ impl<T> Phat<T> {
pub const LO: &'static str = "";
pub const HI: &'static str = "";
/// A phat line
pub fn lo (fg: Color, bg: Color) -> impl Content<TuiOut> {
pub fn lo (fg: Color, bg: Color) -> impl Render<TuiOut> {
Fixed::y(1, Tui::fg_bg(fg, bg, RepeatH(Self::LO)))
}
/// A phat line
pub fn hi (fg: Color, bg: Color) -> impl Content<TuiOut> {
pub fn hi (fg: Color, bg: Color) -> impl Render<TuiOut> {
Fixed::y(1, Tui::fg_bg(fg, bg, RepeatH(Self::HI)))
}
}
impl<T: Content<TuiOut>> Content<TuiOut> for Phat<T> {
fn content (&self) -> impl Render<TuiOut> {
impl<T: Render<TuiOut>> Content<TuiOut> for Phat<T> {
fn content (&self) -> Option<impl Render<TuiOut> + '_> {
let [fg, bg, hi, lo] = self.colors;
let top = Fixed::y(1, Self::lo(bg, hi));
let low = Fixed::y(1, Self::hi(bg, lo));
let content = Tui::fg_bg(fg, bg, &self.content);
Min::xy(
Some(Min::xy(
self.width,
self.height,
Bsp::s(top, Bsp::n(low, Fill::xy(content)))
)
))
}
}

View file

@ -2,8 +2,10 @@ use crate::*;
use ratatui::prelude::Position;
pub struct Repeat<'a>(pub &'a str);
impl Content<TuiOut> for Repeat<'_> {
fn layout (&self, to: [u16;4]) -> [u16;4] { to }
impl Render<TuiOut> for Repeat<'_> {
fn layout (&self, to: [u16;4]) -> [u16;4] {
to
}
fn render (&self, to: &mut TuiOut) {
let [x, y, w, h] = to.area().xywh();
let a = self.0.len();
@ -19,8 +21,10 @@ impl Content<TuiOut> for Repeat<'_> {
}
pub struct RepeatV<'a>(pub &'a str);
impl Content<TuiOut> for RepeatV<'_> {
fn layout (&self, to: [u16;4]) -> [u16;4] { to }
impl Render<TuiOut> for RepeatV<'_> {
fn layout (&self, to: [u16;4]) -> [u16;4] {
to
}
fn render (&self, to: &mut TuiOut) {
let [x, y, _w, h] = to.area().xywh();
for y in y..y+h {
@ -32,8 +36,10 @@ impl Content<TuiOut> for RepeatV<'_> {
}
pub struct RepeatH<'a>(pub &'a str);
impl Content<TuiOut> for RepeatH<'_> {
fn layout (&self, to: [u16;4]) -> [u16;4] { to }
impl Render<TuiOut> for RepeatH<'_> {
fn layout (&self, to: [u16;4]) -> [u16;4] {
to
}
fn render (&self, to: &mut TuiOut) {
let [x, y, w, _h] = to.area().xywh();
for x in x..x+w {

View file

@ -23,7 +23,7 @@ impl ScrollbarH {
const ICON_INC: &[char] = &[' ', '🞂', ' '];
}
impl Content<TuiOut> for ScrollbarV {
impl Render<TuiOut> for ScrollbarV {
fn render (&self, to: &mut TuiOut) {
let [x, y1, _w, h] = to.area().xywh();
let y2 = y1 + h;
@ -51,7 +51,7 @@ impl Content<TuiOut> for ScrollbarV {
}
}
impl Content<TuiOut> for ScrollbarH {
impl Render<TuiOut> for ScrollbarH {
fn render (&self, to: &mut TuiOut) {
let [x1, y, w, _h] = to.area().xywh();
let x2 = x1 + w;

View file

@ -4,24 +4,24 @@ use unicode_width::{UnicodeWidthStr, UnicodeWidthChar};
impl_content_layout_render!(TuiOut: |self: &str, to|
layout = to.center_xy([width_chars_max(to.w(), self), 1]);
render = {let [x, y, w, ..] = Content::layout(self, to.area());
render = {let [x, y, w, ..] = Render::layout(self, to.area());
to.text(self, x, y, w)});
impl_content_layout_render!(TuiOut: |self: String, to|
layout = Content::<TuiOut>::layout(&self.as_str(), to);
render = Content::<TuiOut>::render(&self.as_str(), to));
layout = Render::<TuiOut>::layout(&self.as_str(), to);
render = Render::<TuiOut>::render(&self.as_str(), to));
impl_content_layout_render!(TuiOut: |self: Arc<str>, to|
layout = Content::<TuiOut>::layout(&self.as_ref(), to);
render = Content::<TuiOut>::render(&self.as_ref(), to));
layout = Render::<TuiOut>::layout(&self.as_ref(), to);
render = Render::<TuiOut>::render(&self.as_ref(), to));
impl_content_layout_render!(TuiOut: |self: std::sync::RwLock<String>, to|
layout = Content::<TuiOut>::layout(&self.read().unwrap(), to);
render = Content::<TuiOut>::render(&self.read().unwrap(), to));
layout = Render::<TuiOut>::layout(&self.read().unwrap(), to);
render = Render::<TuiOut>::render(&self.read().unwrap(), to));
impl_content_layout_render!(TuiOut: |self: std::sync::RwLockReadGuard<'_, String>, to|
layout = Content::<TuiOut>::layout(&**self, to);
render = Content::<TuiOut>::render(&**self, to));
layout = Render::<TuiOut>::layout(&**self, to);
render = Render::<TuiOut>::render(&**self, to));
fn width_chars_max (max: u16, text: impl AsRef<str>) -> u16 {
let mut width: u16 = 0;
@ -61,12 +61,12 @@ impl<'a, T: AsRef<str>> TrimString<T> {
TrimStringRef(self.0, &self.1)
}
}
impl<'a, T: AsRef<str>> Content<TuiOut> for TrimString<T> {
impl<'a, T: AsRef<str>> Render<TuiOut> for TrimString<T> {
fn layout (&self, to: [u16; 4]) -> [u16;4] {
Content::layout(&self.as_ref(), to)
Render::layout(&self.as_ref(), to)
}
fn render (&self, to: &mut TuiOut) {
Content::render(&self.as_ref(), to)
Render::render(&self.as_ref(), to)
}
}
@ -75,7 +75,7 @@ impl<'a, T: AsRef<str>> Content<TuiOut> for TrimString<T> {
/// Width is computed using [unicode_width].
pub struct TrimStringRef<'a, T: AsRef<str>>(pub u16, pub &'a T);
impl<T: AsRef<str>> Content<TuiOut> for TrimStringRef<'_, T> {
impl<T: AsRef<str>> Render<TuiOut> for TrimStringRef<'_, T> {
fn layout (&self, to: [u16; 4]) -> [u16;4] {
[to.x(), to.y(), to.w().min(self.0).min(self.1.as_ref().width() as u16), to.h()]
}

View file

@ -1,60 +1,70 @@
use crate::*;
pub trait TuiStyle {
fn fg <R: Content<TuiOut>> (color: Color, w: R) -> Foreground<R> {
fn fg <R: Render<TuiOut>> (color: Color, w: R) -> Foreground<R> {
Foreground(color, w)
}
fn bg <R: Content<TuiOut>> (color: Color, w: R) -> Background<R> {
fn bg <R: Render<TuiOut>> (color: Color, w: R) -> Background<R> {
Background(color, w)
}
fn fg_bg <R: Content<TuiOut>> (fg: Color, bg: Color, w: R) -> Background<Foreground<R>> {
fn fg_bg <R: Render<TuiOut>> (fg: Color, bg: Color, w: R) -> Background<Foreground<R>> {
Background(bg, Foreground(fg, w))
}
fn modify <R: Content<TuiOut>> (enable: bool, modifier: Modifier, w: R) -> Modify<R> {
fn modify <R: Render<TuiOut>> (enable: bool, modifier: Modifier, w: R) -> Modify<R> {
Modify(enable, modifier, w)
}
fn bold <R: Content<TuiOut>> (enable: bool, w: R) -> Modify<R> {
fn bold <R: Render<TuiOut>> (enable: bool, w: R) -> Modify<R> {
Self::modify(enable, Modifier::BOLD, w)
}
fn border <R: Content<TuiOut>, S: BorderStyle> (enable: bool, style: S, w: R) -> Bordered<S, R> {
fn border <R: Render<TuiOut>, S: BorderStyle> (enable: bool, style: S, w: R) -> Bordered<S, R> {
Bordered(enable, style, w)
}
}
impl TuiStyle for Tui {}
pub struct Foreground<R: Content<TuiOut>>(pub Color, pub R);
impl<R: Content<TuiOut>> Content<TuiOut> for Foreground<R> {
fn content (&self) -> impl Render<TuiOut> { &self.1 }
pub struct Foreground<R>(pub Color, pub R);
impl<R: Render<TuiOut>> Render<TuiOut> for Foreground<R> {
fn layout (&self, to: [u16;4]) -> [u16;4] {
self.1.layout(to)
}
fn render (&self, to: &mut TuiOut) {
to.fill_fg(to.area(), self.0);
self.1.render(to)
let area = self.layout(to.area());
to.fill_fg(area, self.0);
to.place(area, &self.1);
}
}
pub struct Background<R: Content<TuiOut>>(pub Color, pub R);
impl<R: Content<TuiOut>> Content<TuiOut> for Background<R> {
fn content (&self) -> impl Render<TuiOut> { &self.1 }
pub struct Background<R>(pub Color, pub R);
impl<R: Render<TuiOut>> Render<TuiOut> for Background<R> {
fn layout (&self, to: [u16;4]) -> [u16;4] {
self.1.layout(to)
}
fn render (&self, to: &mut TuiOut) {
to.fill_bg(to.area(), self.0);
self.1.render(to)
let area = self.layout(to.area());
to.fill_bg(area, self.0);
to.place(area, &self.1);
}
}
pub struct Modify<R: Content<TuiOut>>(pub bool, pub Modifier, pub R);
impl<R: Content<TuiOut>> Content<TuiOut> for Modify<R> {
fn content (&self) -> impl Render<TuiOut> { &self.2 }
pub struct Modify<R: Render<TuiOut>>(pub bool, pub Modifier, pub R);
impl<R: Render<TuiOut>> Render<TuiOut> for Modify<R> {
fn layout (&self, to: [u16;4]) -> [u16;4] {
self.2.layout(to)
}
fn render (&self, to: &mut TuiOut) {
to.fill_mod(to.area(), self.0, self.1);
self.2.render(to)
}
}
pub struct Styled<R: Content<TuiOut>>(pub Option<Style>, pub R);
impl<R: Content<TuiOut>> Content<TuiOut> for Styled<R> {
fn content (&self) -> impl Render<TuiOut> { &self.1 }
pub struct Styled<R: Render<TuiOut>>(pub Option<Style>, pub R);
impl<R: Render<TuiOut>> Render<TuiOut> for Styled<R> {
fn layout (&self, to: [u16;4]) -> [u16;4] {
self.1.layout(to)
}
fn render (&self, to: &mut TuiOut) {
to.place(self.content().layout(to.area()), &self.content());
to.place(self.1.layout(to.area()), &self.1);
// TODO write style over area
}
}

View file

@ -31,10 +31,10 @@ impl<A, B, C> Tryptich<A, B, C> {
}
impl<A, B, C> Content<TuiOut> for Tryptich<A, B, C>
where A: Content<TuiOut>, B: Content<TuiOut>, C: Content<TuiOut> {
fn content (&self) -> impl Render<TuiOut> {
where A: Render<TuiOut>, B: Render<TuiOut>, C: Render<TuiOut> {
fn content (&self) -> Option<impl Render<TuiOut> + '_> {
let Self { top, h, left: (w_a, ref a), middle: (w_b, ref b), right: (w_c, ref c) } = *self;
Fixed::y(h, if top {
Some(Fixed::y(h, if top {
Bsp::a(
Fill::x(Align::n(Fixed::x(w_b, Align::x(Tui::bg(Color::Reset, b))))),
Bsp::a(
@ -50,7 +50,7 @@ where A: Content<TuiOut>, B: Content<TuiOut>, C: Content<TuiOut> {
Fill::xy(Align::e(Fixed::x(w_c, Tui::bg(Color::Reset, c)))),
),
)
})
}))
}
}

View file

@ -10,6 +10,7 @@ impl TuiEvent {
pub fn from_crossterm (event: Event) -> Self {
Self(event)
}
#[cfg(feature = "dsl")]
pub fn from_dsl (dsl: impl Dsl) -> Perhaps<Self> {
Ok(TuiKey::from_dsl(dsl)?.to_crossterm().map(Self))
}
@ -17,6 +18,7 @@ impl TuiEvent {
pub struct TuiKey(Option<KeyCode>, KeyModifiers);
impl TuiKey {
const SPLIT: char = '/';
#[cfg(feature = "dsl")]
pub fn from_dsl (dsl: impl Dsl) -> Usually<Self> {
if let Some(word) = dsl.word()? {
let word = word.trim();