layout (no-draw) mode for tui

This commit is contained in:
facile pop culture reference 2026-08-01 21:25:29 +03:00
parent a27dacff93
commit 64c83b5460
4 changed files with 167 additions and 105 deletions

View file

@ -1325,21 +1325,45 @@ macro_rules! eval_xy (
crossterm::event::read, crossterm::event::read,
}; };
impl Deref for Tui { type Target = Buffer; fn deref (&self) -> &Buffer { &self.0 } } //impl Deref for Tui { type Target = Buffer; fn deref (&self) -> &Buffer { &self.0 } }
impl DerefMut for Tui { fn deref_mut (&mut self) -> &mut Buffer { &mut self.0 } } //impl DerefMut for Tui { fn deref_mut (&mut self) -> &mut Buffer { &mut self.0 } }
impl AsMut<Buffer> for Tui { fn as_mut (&mut self) -> &mut Buffer { &mut self.0 } } //impl AsMut<Buffer> for Tui { fn as_mut (&mut self) -> &mut Buffer { &mut self.0 } }
impl Wide<u16> for Tui { fn w (&self) -> u16 { self.1.2 } } impl AsRef<XYWH<u16>> for Tui {
impl Tall<u16> for Tui { fn h (&self) -> u16 { self.1.3 } } fn as_ref (&self) -> &XYWH<u16> {
match self {
Self::Draw(_, area) => area,
Self::Layout(area) => area
}
}
}
impl Wide<u16> for Tui {
fn w (&self) -> u16 { self.as_ref().2 }
}
impl Tall<u16> for Tui {
fn h (&self) -> u16 { self.as_ref().3 }
}
impl Xy<u16> for Tui {
fn x (&self) -> u16 { self.as_ref().0 }
fn y (&self) -> u16 { self.as_ref().1 }
}
impl HasOrigin for Tui { fn origin (&self) -> Azimuth { Azimuth::NW } } impl HasOrigin for Tui { fn origin (&self) -> Azimuth { Azimuth::NW } }
impl Xy<u16> for Tui { fn x (&self) -> u16 { self.1.0 } fn y (&self) -> u16 { self.1.1 } }
/// Terminal output. /// Terminal output.
pub struct Tui( pub enum Tui {
/// Ratatui buffer; area is screen size Draw (
pub Buffer, /// Ratatui buffer; area is screen size
/// Current draw area Buffer,
pub XYWH<u16> /// Current draw area
); XYWH<u16>
),
Layout (
XYWH<u16>
)
}
impl Tui { impl Tui {
pub fn setup_panic () { pub fn setup_panic () {
@ -1402,14 +1426,16 @@ macro_rules! eval_xy (
disable_raw_mode().map_err(Into::into) disable_raw_mode().map_err(Into::into)
} }
pub fn new (width: u16, height: u16) -> Self { pub fn new (width: u16, height: u16) -> Self {
Self(Buffer::empty(Rect { x: 0, y: 0, width, height }), XYWH(0, 0, width, height)) Self::Draw(Buffer::empty(Rect { x: 0, y: 0, width, height }), XYWH(0, 0, width, height))
} }
pub fn resize <W: Write> (&mut self, back: &mut CrosstermBackend<W>, width: u16, height: u16) { pub fn resize <W: Write> (&mut self, back: &mut CrosstermBackend<W>, width: u16, height: u16) {
let size = Rect { x: 0, y: 0, width, height }; let size = Rect { x: 0, y: 0, width, height };
if self.0.area != size { if let Self::Draw(buffer, ..) = self {
back.clear_region(ClearType::All).unwrap(); if buffer.area != size {
self.0.resize(size); back.clear_region(ClearType::All).unwrap();
self.0.reset(); buffer.resize(size);
buffer.reset();
}
} }
} }
pub fn redraw <'b, W: Write> ( pub fn redraw <'b, W: Write> (
@ -1417,20 +1443,25 @@ macro_rules! eval_xy (
back: &mut CrosstermBackend<W>, back: &mut CrosstermBackend<W>,
mut next: &'b mut Self mut next: &'b mut Self
) { ) {
let updates = self.0.diff(&next.0); if let Self::Draw(prev, ..) = self && let Self::Draw(next, ..) = next {
back.draw(updates.into_iter()).expect("failed to render"); let updates = prev.diff(&next);
Backend::flush(back).expect("failed to flush output new"); back.draw(updates.into_iter()).expect("failed to render");
std::mem::swap(self, &mut next); Backend::flush(back).expect("failed to flush output new");
next.0.reset(); std::mem::swap(prev, next);
next.reset();
}
} }
pub fn update (&mut self, callback: &impl Fn(&mut Cell, u16, u16)) -> XYWH<u16> { pub fn update (&mut self, callback: &impl Fn(&mut Cell, u16, u16)) -> XYWH<u16> {
for row in 0..self.h() { let XYWH(x0, y0, w, h) = self.area();
let y = self.y() + row; if let Self::Draw(buffer, ..) = self {
for col in 0..self.w() { for row in 0..h {
let x = self.x() + col; let y = y0 + row;
if x < self.0.area.width && y < self.0.area.height { for col in 0..w {
if let Some(cell) = self.0.cell_mut(Position { x, y }) { let x = x0 + col;
callback(cell, col, row); if x < buffer.area.width && y < buffer.area.height {
if let Some(cell) = buffer.cell_mut(Position { x, y }) {
callback(cell, col, row);
}
} }
} }
} }
@ -1438,17 +1469,21 @@ macro_rules! eval_xy (
self.xywh() self.xywh()
} }
pub fn blit (&mut self, text: &impl AsRef<str>, x: u16, y: u16, style: Option<Style>) { pub fn blit (&mut self, text: &impl AsRef<str>, x: u16, y: u16, style: Option<Style>) {
let text = text.as_ref(); if let Self::Draw(buffer, ..) = self {
let style = style.unwrap_or(Style::default()); let text = text.as_ref();
if x < self.0.area.width && y < self.0.area.height { let style = style.unwrap_or(Style::default());
self.0.set_string(x, y, text, style); if x < buffer.area.width && y < buffer.area.height {
buffer.set_string(x, y, text, style);
}
} }
} }
pub fn tint_all (&mut self, fg: Color, bg: Color, modifier: Modifier) { pub fn tint_all (&mut self, fg: Color, bg: Color, modifier: Modifier) {
for cell in self.0.content.iter_mut() { if let Self::Draw(buffer, ..) = self {
cell.fg = fg; for cell in buffer.content.iter_mut() {
cell.bg = bg; cell.fg = fg;
cell.modifier = modifier; cell.bg = bg;
cell.modifier = modifier;
}
} }
} }
/// Spawn the TUI output thread which writes colored characters to the terminal. /// Spawn the TUI output thread which writes colored characters to the terminal.
@ -1488,7 +1523,7 @@ macro_rules! eval_xy (
prev.redraw(&mut backend, &mut next); prev.redraw(&mut backend, &mut next);
} }
let timer = format!("{:>3.3}ms", perf.used.load(Relaxed)); let timer = format!("{:>3.3}ms", perf.used.load(Relaxed));
prev.set_string(0, 0, &timer, Style::default()); prev.blit(&timer, 0, 0, Some(Style::default()));
})?) })?)
} }
/// Draw TUI content or its error message. /// Draw TUI content or its error message.
@ -1647,25 +1682,35 @@ macro_rules! eval_xy (
if string_width > max_width { if string_width > max_width {
break break
} }
if let Some(cell) = self.0.cell_mut(ratatui::prelude::Position { x, y }) { if let Self::Draw(buffer, ..) = self {
cell.set_char(character); if let Some(cell) = buffer.cell_mut(ratatui::prelude::Position { x, y }) {
} else { cell.set_char(character);
break } else {
break
}
} }
} }
Ok(Some(XYWH(x0, y, string_width, 1))) Ok(Some(XYWH(x0, y, string_width, 1)))
} }
/// Get mutable reference to current clipping area
fn area_mut (&mut self) -> &mut XYWH<u16> {
match self {
Self::Draw(_, area) => area,
Self::Layout(area) => area,
}
}
} }
impl Screen for Tui { impl Screen for Tui {
type Unit = u16; type Unit = u16;
/// Render drawable in subarea specified by `area` /// Render drawable in subarea specified by `area`
fn show (&mut self, content: impl Draw<Self>) -> Perhaps<XYWH<u16>> { fn show (&mut self, content: impl Draw<Self>) -> Perhaps<XYWH<u16>> {
let previous_area = self.1; let previous_area = self.area();
Ok(if let Some(area) = content.layout(self.1)? { Ok(if let Some(area) = content.layout(self.area())? {
self.1 = area; *self.area_mut() = area;
if let Some(result_area) = content.draw(self)? { if let Some(result_area) = content.draw(self)? {
self.1 = previous_area; *self.area_mut() = previous_area;
Some(result_area) Some(result_area)
} else { } else {
None None
@ -1677,7 +1722,10 @@ macro_rules! eval_xy (
/// Get current clipping area /// Get current clipping area
fn area (&self) -> XYWH<Self::Unit> { fn area (&self) -> XYWH<Self::Unit> {
self.1 match self {
Self::Draw(_, area) => *area,
Self::Layout(area) => *area,
}
} }
fn clip <T> ( fn clip <T> (
@ -1685,12 +1733,12 @@ macro_rules! eval_xy (
area: impl Into<Option<XYWH<u16>>>, area: impl Into<Option<XYWH<u16>>>,
draw: impl FnOnce(&mut Self)->T draw: impl FnOnce(&mut Self)->T
) -> T { ) -> T {
let prev = self.1; let prev = self.area();
if let Some(area) = area.into() { if let Some(area) = area.into() {
self.1 = area.into(); *self.area_mut() = area.into();
} }
let result = draw(self); let result = draw(self);
self.1 = prev; *self.area_mut() = prev;
result result
} }
} }
@ -1776,8 +1824,10 @@ macro_rules! eval_xy (
break break
} }
let pos = Position { x: x + width - 1, y }; let pos = Position { x: x + width - 1, y };
if let Some(cell) = to.0.cell_mut(pos) { if let Tui::Draw(buffer, _) = to {
cell.set_char(c); if let Some(cell) = buffer.cell_mut(pos) {
cell.set_char(c);
}
} }
width += c.width().unwrap_or(0) as u16; width += c.width().unwrap_or(0) as u16;
} }

View file

@ -8,7 +8,7 @@ use super::{*, draw::*};
pub fn border (on: bool, style: impl BorderStyle, item: impl Draw<Tui>) -> impl Draw<Tui> { pub fn border (on: bool, style: impl BorderStyle, item: impl Draw<Tui>) -> impl Draw<Tui> {
let content = item.pad_wh(1, 1); let content = item.pad_wh(1, 1);
let outline = when(on, draw(move|to: &mut Tui|{ let outline = when(on, draw(move|to: &mut Tui|{
let XYWH(x, y, w, h) = to.1; let XYWH(x, y, w, h) = to.area();
if w > 0 && h > 0 { if w > 0 && h > 0 {
to.blit(&style.border_nw(), x, y, style.style()); to.blit(&style.border_nw(), x, y, style.style());
to.blit(&style.border_ne(), x + w - 1, y, style.style()); to.blit(&style.border_ne(), x + w - 1, y, style.style());
@ -178,7 +178,7 @@ pub trait BorderStyle: Draw<Tui> + Copy {
self.draw_v(to, None)?; self.draw_v(to, None)?;
self.draw_c(to, None)?; self.draw_c(to, None)?;
} }
Ok(to.1) Ok(to.area())
} }
#[inline] fn draw_h (self, to: &mut Tui, style: Option<Style>) -> Usually<XYWH<u16>> { #[inline] fn draw_h (self, to: &mut Tui, style: Option<Style>) -> Usually<XYWH<u16>> {
let style = style.or_else(||self.style_horizontal()); let style = style.or_else(||self.style_horizontal());
@ -191,7 +191,7 @@ pub trait BorderStyle: Draw<Tui> + Copy {
Ok(to.xywh()) Ok(to.xywh())
} }
#[inline] fn draw_v (self, to: &mut Tui, style: Option<Style>) -> Usually<XYWH<u16>> { #[inline] fn draw_v (self, to: &mut Tui, style: Option<Style>) -> Usually<XYWH<u16>> {
let area = to.1; let area = to.area();
let style = style.or_else(||self.style_vertical()); let style = style.or_else(||self.style_vertical());
let [x, x2, y, y2] = area.lrtb(); let [x, x2, y, y2] = area.lrtb();
let h = y2 - y; let h = y2 - y;
@ -207,7 +207,7 @@ pub trait BorderStyle: Draw<Tui> + Copy {
Ok(area) Ok(area)
} }
#[inline] fn draw_c (self, to: &mut Tui, style: Option<Style>) -> Usually<XYWH<u16>> { #[inline] fn draw_c (self, to: &mut Tui, style: Option<Style>) -> Usually<XYWH<u16>> {
let area = to.1; let area = to.area();
let style = style.or_else(||self.style_corners()); let style = style.or_else(||self.style_corners());
let XYWH(x, y, w, h) = area; let XYWH(x, y, w, h) = area;
if w > 1 && h > 1 { if w > 1 && h > 1 {

View file

@ -5,8 +5,10 @@ pub const fn x_repeat (c: &str) -> impl Draw<Tui> {
draw(move|to: &mut Tui|{ draw(move|to: &mut Tui|{
let XYWH(x, y, w, _h) = to.xywh(); let XYWH(x, y, w, _h) = to.xywh();
for x in x..x+w { for x in x..x+w {
if let Some(cell) = to.0.cell_mut(Position::from((x, y))) { if let Tui::Draw(buf, ..) = to {
cell.set_symbol(&c); if let Some(cell) = buf.cell_mut(Position::from((x, y))) {
cell.set_symbol(&c);
}
} }
} }
Ok(Some(XYWH(x, y, w, 1))) Ok(Some(XYWH(x, y, w, 1)))
@ -17,8 +19,10 @@ pub const fn y_repeat (c: &str) -> impl Draw<Tui> {
draw(move|to: &mut Tui|{ draw(move|to: &mut Tui|{
let XYWH(x, y, _w, h) = to.xywh(); let XYWH(x, y, _w, h) = to.xywh();
for y in y..y+h { for y in y..y+h {
if let Some(cell) = to.0.cell_mut(Position::from((x, y))) { if let Tui::Draw(buf, ..) = to {
cell.set_symbol(&c); if let Some(cell) = buf.cell_mut(Position::from((x, y))) {
cell.set_symbol(&c);
}
} }
} }
Ok(Some(XYWH(x, y, 1, h))) Ok(Some(XYWH(x, y, 1, h)))
@ -31,9 +35,11 @@ pub const fn xy_repeat (c: &str) -> impl Draw<Tui> {
let a = c.len(); let a = c.len();
for (_v, y) in (y..y+h).enumerate() { for (_v, y) in (y..y+h).enumerate() {
for (u, x) in (x..x+w).enumerate() { for (u, x) in (x..x+w).enumerate() {
if let Some(cell) = to.0.cell_mut(Position::from((x, y))) { if let Tui::Draw(buf, ..) = to {
let u = u % a; if let Some(cell) = buf.cell_mut(Position::from((x, y))) {
cell.set_symbol(&c[u..u+1]); let u = u % a;
cell.set_symbol(&c[u..u+1]);
}
} }
} }
} }

View file

@ -7,58 +7,64 @@ pub const ICON_DEC_H: &[char] = &[' ', '🞀', ' '];
pub const ICON_INC_H: &[char] = &[' ', '🞂', ' ']; pub const ICON_INC_H: &[char] = &[' ', '🞂', ' '];
pub fn x_scroll () -> impl Draw<Tui> { pub fn x_scroll () -> impl Draw<Tui> {
draw(|Tui(buf, XYWH(x1, y1, w, _h)): &mut Tui|{ draw(|to: &mut Tui|{
let x2 = *x1 + *w; let XYWH(x1, y1, w, _h) = to.area();
for (i, x) in (*x1..=x2).enumerate() { let x2 = x1 + w;
if let Some(cell) = buf.cell_mut(Position::from((x, *y1))) { for (i, x) in (x1..=x2).enumerate() {
if i < (ICON_DEC_H.len()) { if let Tui::Draw(buf, ..) = to {
cell.set_fg(Rgb(255, 255, 255)); if let Some(cell) = buf.cell_mut(Position::from((x, y1))) {
cell.set_bg(Rgb(0, 0, 0)); if i < (ICON_DEC_H.len()) {
cell.set_char(ICON_DEC_H[i as usize]); cell.set_fg(Rgb(255, 255, 255));
} else if i > (*w as usize - ICON_INC_H.len()) { cell.set_bg(Rgb(0, 0, 0));
cell.set_fg(Rgb(255, 255, 255)); cell.set_char(ICON_DEC_H[i as usize]);
cell.set_bg(Rgb(0, 0, 0)); } else if i > (w as usize - ICON_INC_H.len()) {
cell.set_char(ICON_INC_H[*w as usize - i]); cell.set_fg(Rgb(255, 255, 255));
} else if false { cell.set_bg(Rgb(0, 0, 0));
cell.set_fg(Rgb(255, 255, 255)); cell.set_char(ICON_INC_H[w as usize - i]);
cell.set_bg(Reset); } else if false {
cell.set_char('━'); cell.set_fg(Rgb(255, 255, 255));
} else { cell.set_bg(Reset);
cell.set_fg(Rgb(0, 0, 0)); cell.set_char('━');
cell.set_bg(Reset); } else {
cell.set_char('╌'); cell.set_fg(Rgb(0, 0, 0));
cell.set_bg(Reset);
cell.set_char('╌');
}
} }
} }
} }
Ok(Some(XYWH(*x1, *y1, *w, 1))) Ok(Some(XYWH(x1, y1, w, 1)))
}) })
} }
pub fn y_scroll () -> impl Draw<Tui> { pub fn y_scroll () -> impl Draw<Tui> {
draw(|Tui(buf, XYWH(x1, y1, _w, h)): &mut Tui|{ draw(|to: &mut Tui|{
let y2 = *y1 + *h; let XYWH(x1, y1, _w, h) = to.area();
for (i, y) in (*y1..=y2).enumerate() { let y2 = y1 + h;
if let Some(cell) = buf.cell_mut(Position::from((*x1, y))) { for (i, y) in (y1..=y2).enumerate() {
if (i as usize) < (ICON_DEC_V.len()) { if let Tui::Draw(buf, ..) = to {
cell.set_fg(Rgb(255, 255, 255)); if let Some(cell) = buf.cell_mut(Position::from((x1, y))) {
cell.set_bg(Rgb(0, 0, 0)); if (i as usize) < (ICON_DEC_V.len()) {
cell.set_char(ICON_DEC_V[i as usize]); cell.set_fg(Rgb(255, 255, 255));
} else if (i as usize) > (*h as usize - ICON_INC_V.len()) { cell.set_bg(Rgb(0, 0, 0));
cell.set_fg(Rgb(255, 255, 255)); cell.set_char(ICON_DEC_V[i as usize]);
cell.set_bg(Rgb(0, 0, 0)); } else if (i as usize) > (h as usize - ICON_INC_V.len()) {
cell.set_char(ICON_INC_V[*h as usize - i]); cell.set_fg(Rgb(255, 255, 255));
} else if false { cell.set_bg(Rgb(0, 0, 0));
cell.set_fg(Rgb(255, 255, 255)); cell.set_char(ICON_INC_V[h as usize - i]);
cell.set_bg(Reset); } else if false {
cell.set_char('‖'); // ━ cell.set_fg(Rgb(255, 255, 255));
} else { cell.set_bg(Reset);
cell.set_fg(Rgb(0, 0, 0)); cell.set_char('‖'); // ━
cell.set_bg(Reset); } else {
cell.set_char('╎'); // ━ cell.set_fg(Rgb(0, 0, 0));
cell.set_bg(Reset);
cell.set_char('╎'); // ━
}
} }
} }
} }
Ok(Some(XYWH(*x1, *y1, 1, *h))) Ok(Some(XYWH(x1, y1, 1, h)))
}) })
} }