mirror of
https://codeberg.org/unspeaker/tengri.git
synced 2026-09-18 05:16:44 +02:00
support multipass layout, barely
This commit is contained in:
parent
083ce8ba76
commit
7f9b7091e2
20 changed files with 1581 additions and 1783 deletions
517
src/term.rs
Normal file
517
src/term.rs
Normal file
|
|
@ -0,0 +1,517 @@
|
|||
use crate::*;
|
||||
use Color::*;
|
||||
//use unicode_width::{UnicodeWidthStr, UnicodeWidthChar};
|
||||
//use rand::distributions::uniform::UniformSampler;
|
||||
pub(crate) use ::{
|
||||
std::{
|
||||
io::{stdout, Write},
|
||||
time::Duration,
|
||||
},
|
||||
ratatui::{
|
||||
prelude::{Style, Position, Backend, Color},
|
||||
style::{Modifier},
|
||||
backend::{CrosstermBackend, ClearType},
|
||||
layout::{Size, Rect},
|
||||
buffer::{Buffer, Cell},
|
||||
crossterm::{
|
||||
ExecutableCommand,
|
||||
terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode},
|
||||
//event::{poll, read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState},
|
||||
}
|
||||
},
|
||||
crossterm::event::read,
|
||||
};
|
||||
|
||||
mod border; pub use self::border::*;
|
||||
mod buffer; pub use self::buffer::*;
|
||||
mod button; pub use self::button::*;
|
||||
mod event; pub use self::event::*;
|
||||
mod keys; pub use self::keys::*;
|
||||
mod modify; pub use self::modify::*;
|
||||
mod phat; pub use self::phat::*;
|
||||
mod repeat; pub use self::repeat::*;
|
||||
mod scroll; pub use self::scroll::*;
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
#[macro_export] macro_rules! tui_app {
|
||||
($Struct:ident { $($fields:tt)* }) => {
|
||||
#[dizzle::namespace(bool)]
|
||||
#[dizzle::namespace(u8)]
|
||||
#[dizzle::namespace(u16)]
|
||||
#[dizzle::namespace(Option<u16>)]
|
||||
#[dizzle::namespace(Color Tui::eval_color_expr)]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct $Struct { $($fields)* }
|
||||
tui_main!($Struct { ..Default::default() });
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement standard [main] entrypoint for TUI apps.
|
||||
#[cfg(feature = "term")]
|
||||
#[macro_export] macro_rules! tui_main {
|
||||
($state:expr) => {
|
||||
pub fn main () -> Usually<()> {
|
||||
tengri::Tui::setup_panic();
|
||||
tengri::Tui::run_main(::std::sync::Arc::new(::std::sync::RwLock::new($state)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable TUI output for state struct.
|
||||
#[cfg(feature = "term")]
|
||||
#[macro_export] macro_rules! tui_view {
|
||||
($self:ident: $State:ty $body:block) => {
|
||||
impl tengri::View<Tui> for $State {
|
||||
fn view (&$self) -> impl tengri::Draw<tengri::Tui> $body
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable TUI keyboard input for main state struct.
|
||||
#[cfg(feature = "term")]
|
||||
#[macro_export] macro_rules! tui_keys {
|
||||
($self:ident:$State:ty,$input:ident $($body:tt)+) => {
|
||||
impl dizzle::Apply<TuiEvent, Usually<()>> for $State {
|
||||
fn apply (&mut $self, $input: &tengri::TuiEvent) -> Usually<()> $($body)+
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Terminal output.
|
||||
pub enum Tui {
|
||||
Draw (
|
||||
/// Ratatui buffer; area is screen size
|
||||
Buffer,
|
||||
/// Current draw area
|
||||
XYWH<u16>
|
||||
),
|
||||
Layout (
|
||||
/// Draw area; no buffer
|
||||
XYWH<u16>
|
||||
)
|
||||
}
|
||||
|
||||
impl Screen for Tui {
|
||||
type Unit = u16;
|
||||
fn area (&self) -> XYWH<Self::Unit> {
|
||||
self.into()
|
||||
}
|
||||
fn clip <T> (
|
||||
&mut self, area: impl Into<Option<XYWH<u16>>>, draw: &impl Fn(&mut Self)->T,
|
||||
) -> T {
|
||||
let prev = self.area();
|
||||
if let Some(area) = area.into() {
|
||||
*self.area_mut() = area.into();
|
||||
}
|
||||
let result = draw(self);
|
||||
*self.area_mut() = prev;
|
||||
result
|
||||
}
|
||||
fn size (
|
||||
&mut self, area: impl Into<Option<XYWH<u16>>>, draw: impl Draw<Self>,
|
||||
) -> Perhaps<XYWH<Self::Unit>> {
|
||||
Self::Layout(self.area()).clip(area, &|to|draw.draw(to))
|
||||
}
|
||||
fn draw (
|
||||
&mut self, area: impl Into<Option<XYWH<u16>>>, draw: impl Draw<Tui>
|
||||
) -> Perhaps<XYWH<Self::Unit>> {
|
||||
self.clip(area, &|to|draw.draw(to))
|
||||
}
|
||||
|
||||
//fn draw (
|
||||
//&mut self,
|
||||
//area: impl Into<Option<XYWH<u16>>>,
|
||||
//draw: impl Draw<Self>
|
||||
//) -> Perhaps<XYWH<Self::Unit>> {
|
||||
//let prev = self.area();
|
||||
//if let Some(area) = area.into() {
|
||||
//*self.area_mut() = area.into();
|
||||
//}
|
||||
//let result = draw.draw(self);
|
||||
//*self.area_mut() = prev;
|
||||
//result
|
||||
//}
|
||||
}
|
||||
|
||||
impl Tui {
|
||||
pub fn setup_panic () {
|
||||
use ::std::panic::{set_hook, PanicHookInfo};
|
||||
use ::better_panic::{Settings, Verbosity};
|
||||
let panic = Settings::auto()
|
||||
.verbosity(Verbosity::Full)
|
||||
.create_panic_handler();
|
||||
set_hook(Box::new(move |info: &PanicHookInfo|{
|
||||
let _ = Tui::teardown(&mut stdout());
|
||||
panic(info);
|
||||
}));
|
||||
}
|
||||
pub fn run_main <T> (state: Arc<RwLock<T>>) -> Usually<()> where
|
||||
T: View<Tui> + Apply<TuiEvent, Usually<()>> + Send + Sync + 'static
|
||||
{
|
||||
Exit::run(|exit|{
|
||||
let scan = Duration::from_millis(100);
|
||||
let frame = Duration::from_millis(10);
|
||||
let (_input, output) = Tui::io(exit.as_ref(), &state, scan, frame, std::io::stdout())?;
|
||||
let _ = output.join();
|
||||
Tui::teardown(&mut stdout())
|
||||
})
|
||||
}
|
||||
/// Spawn the TUI input and output threadsl.
|
||||
pub fn io <
|
||||
T: View<Tui> + Apply<TuiEvent, Usually<()>> + Send + Sync + 'static,
|
||||
W: Write + Send + Sync + 'static,
|
||||
> (
|
||||
exited: &Arc<AtomicBool>,
|
||||
state: &Arc<RwLock<T>>,
|
||||
poll: Duration,
|
||||
sleep: Duration,
|
||||
output: W,
|
||||
) -> Result<(Task, Task), Box<dyn std::error::Error>> {
|
||||
Ok((
|
||||
Tui::input(exited, state, poll)?,
|
||||
Tui::output(exited, state, sleep, output)?,
|
||||
))
|
||||
}
|
||||
/// Spawn the TUI input thread which reads keys from the terminal.
|
||||
pub fn input <T: Apply<TuiEvent, Usually<()>> + Send + Sync + 'static> (
|
||||
exited: &Arc<AtomicBool>, state: &Arc<RwLock<T>>, poll: Duration
|
||||
) -> Result<Task, std::io::Error> {
|
||||
let exited = exited.clone();
|
||||
let state = state.clone();
|
||||
Task::new_poll(exited.clone(), poll, move |_| {
|
||||
let event = read().unwrap();
|
||||
if Exit::is(&event) {
|
||||
exited.store(true, Relaxed);
|
||||
} else if let Err(e) = state.write().unwrap().apply(&TuiEvent(event)) {
|
||||
panic!("{e}")
|
||||
}
|
||||
})
|
||||
}
|
||||
pub fn teardown <W: Write> (backend: &mut W) -> Usually<()> {
|
||||
use ::ratatui::backend::Backend;
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
CrosstermBackend::new(backend).show_cursor()?;
|
||||
disable_raw_mode().map_err(Into::into)
|
||||
}
|
||||
pub fn new (width: u16, height: u16) -> Self {
|
||||
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) {
|
||||
let size = Rect { x: 0, y: 0, width, height };
|
||||
if let Self::Draw(buffer, ..) = self {
|
||||
if buffer.area != size {
|
||||
back.clear_region(ClearType::All).unwrap();
|
||||
buffer.resize(size);
|
||||
buffer.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn redraw <'b, W: Write> (
|
||||
&'b mut self,
|
||||
back: &mut CrosstermBackend<W>,
|
||||
next: &'b mut Self
|
||||
) {
|
||||
if let Self::Draw(prev, ..) = self && let Self::Draw(next, ..) = next {
|
||||
let updates = prev.diff(&next);
|
||||
back.draw(updates.into_iter()).expect("failed to render");
|
||||
Backend::flush(back).expect("failed to flush output new");
|
||||
std::mem::swap(prev, next);
|
||||
next.reset();
|
||||
}
|
||||
}
|
||||
pub fn update (&mut self, callback: &impl Fn(&mut Cell, u16, u16)) -> XYWH<u16> {
|
||||
let XYWH(x0, y0, w, h) = self.area();
|
||||
if let Self::Draw(buffer, ..) = self {
|
||||
for row in 0..h {
|
||||
let y = y0 + row;
|
||||
for col in 0..w {
|
||||
let x = x0 + col;
|
||||
if x < buffer.area.width && y < buffer.area.height {
|
||||
if let Some(cell) = buffer.cell_mut(Position { x, y }) {
|
||||
callback(cell, col, row);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.xywh()
|
||||
}
|
||||
pub fn blit (&mut self, text: &impl AsRef<str>, x: u16, y: u16, style: Option<Style>) {
|
||||
if let Self::Draw(buffer, ..) = self {
|
||||
let text = text.as_ref();
|
||||
let style = style.unwrap_or(Style::default());
|
||||
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) {
|
||||
if let Self::Draw(buffer, ..) = self {
|
||||
for cell in buffer.content.iter_mut() {
|
||||
cell.fg = fg;
|
||||
cell.bg = bg;
|
||||
cell.modifier = modifier;
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Spawn the TUI output thread which writes colored characters to the terminal.
|
||||
///
|
||||
/// ```
|
||||
/// let state = std::sync::Arc::new(std::sync::RwLock::new(()));
|
||||
/// let _ = tengri::Exit::run(|exit|{
|
||||
/// tengri::Tui::output(
|
||||
/// exit.as_ref(),
|
||||
/// &state,
|
||||
/// std::time::Duration::from_millis(10),
|
||||
/// std::io::stdout()
|
||||
/// )
|
||||
/// });
|
||||
/// ```
|
||||
pub fn output <
|
||||
W: Write + Send + Sync + 'static, T: View<Tui> + Send + Sync + 'static
|
||||
> (
|
||||
exited: &Arc<AtomicBool>,
|
||||
state: &Arc<RwLock<T>>,
|
||||
sleep: Duration,
|
||||
output: W,
|
||||
) -> Usually<Task> {
|
||||
let state = state.clone();
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
CrosstermBackend::new(stdout()).hide_cursor()?;
|
||||
enable_raw_mode()?;
|
||||
let mut backend = CrosstermBackend::new(output);
|
||||
let Size { width, height } = backend.size().expect("get size failed");
|
||||
let mut prev = Tui::new(width, height);
|
||||
let mut next = Tui::new(width, height);
|
||||
Ok(Task::new_sleep(exited.clone(), sleep, move |perf| {
|
||||
let Size { width, height } = backend.size().expect("get size failed");
|
||||
if let Ok(state) = state.try_read() {
|
||||
prev.resize(&mut backend, width, height);
|
||||
state.view().draw(&mut next).expect("draw failed"); // TODO draw error
|
||||
prev.redraw(&mut backend, &mut next);
|
||||
}
|
||||
let timer = format!("{:>3.3}ms", perf.used.load(Relaxed));
|
||||
prev.blit(&timer, 0, 0, Some(Style::default()));
|
||||
})?)
|
||||
}
|
||||
/// Draw TUI content or its error message.
|
||||
///
|
||||
/// ```
|
||||
/// for variant in [
|
||||
/// Ok(Some("hello")),
|
||||
/// Ok(None),
|
||||
/// Err("fail".into()),
|
||||
/// ] {
|
||||
/// let _ = tengri::Tui::catcher(variant);
|
||||
/// }
|
||||
/// ```
|
||||
pub fn catcher <T: Draw<Tui>> (result: &Usually<T>) -> impl Draw<Tui> {
|
||||
draw(move|to: &mut Tui|match result {
|
||||
Ok(content) => content.draw(to),
|
||||
Err(e) => {
|
||||
let err_fg = Color::Rgb(255,224,244);
|
||||
let err_bg = Color::Rgb(96, 24, 24);
|
||||
let title = east(bold(true, "upsi daisy. "), "rendering failed.");
|
||||
let error = east("\"why?\" ", bold(true, format!("{e}")));
|
||||
fg(err_fg, bg(err_bg, south(title, error))).draw(to)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "text")]
|
||||
/// Write a line of text
|
||||
///
|
||||
/// TODO: do a paragraph (handle newlines)
|
||||
pub fn text (&mut self, text: &impl AsRef<str>, x0: u16, y: u16, max_width: u16)
|
||||
-> Perhaps<XYWH<u16>>
|
||||
{
|
||||
let text = text.as_ref();
|
||||
let mut string_width: u16 = 0;
|
||||
for character in text.chars() {
|
||||
let x = x0 + string_width;
|
||||
let character_width = character.width().unwrap_or(0) as u16;
|
||||
string_width += character_width;
|
||||
if string_width > max_width {
|
||||
break
|
||||
}
|
||||
if let Self::Draw(buffer, ..) = self {
|
||||
if let Some(cell) = buffer.cell_mut(ratatui::prelude::Position { x, y }) {
|
||||
cell.set_char(character);
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Some(XYWH(x0, y, string_width, 1)))
|
||||
}
|
||||
|
||||
pub fn buffer (&mut self) -> Option<&mut Buffer> {
|
||||
if let Tui::Draw(buffer, _) = self {
|
||||
Some(buffer)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_char (&mut self, x: u16, y: u16, c: char) {
|
||||
self.buffer()
|
||||
.and_then(|buff|buff.cell_mut(Position { x, y }))
|
||||
.map(|cell|cell.set_char(c));
|
||||
}
|
||||
|
||||
/// Get mutable reference to current clipping area
|
||||
fn area_mut (&mut self) -> &mut XYWH<u16> {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
//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 AsMut<Buffer> for Tui { fn as_mut (&mut self) -> &mut Buffer { &mut self.0 } }
|
||||
impl AsRef<XYWH<u16>> for Tui {
|
||||
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 From<&Tui> for XYWH<u16> {
|
||||
fn from (screen: &Tui) -> XYWH<u16> {
|
||||
match screen {
|
||||
Tui::Draw(_, area) => *area,
|
||||
Tui::Layout(area) => *area,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a: 'b, 'b> From<&'a mut Tui> for &'b mut XYWH<u16> {
|
||||
fn from (screen: &'a mut Tui) -> &'b mut XYWH<u16> {
|
||||
match screen {
|
||||
Tui::Draw(_, area) => area,
|
||||
Tui::Layout(area) => area,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn fill_char (c: char) -> impl Draw<Tui> {
|
||||
draw(move|to: &mut Tui|Ok(Some(to.update(&|cell,_,_|{
|
||||
cell.set_char(c);
|
||||
}))))
|
||||
}
|
||||
|
||||
#[cfg(feature = "text")] impl_draw!(|self: String, to: Tui|{
|
||||
self.as_str().draw(to)
|
||||
});
|
||||
|
||||
#[cfg(feature = "text")] impl_draw!(|self: std::sync::Arc<str>, to: Tui|{
|
||||
self.as_ref().draw(to)
|
||||
});
|
||||
|
||||
#[cfg(feature = "text")] impl_draw!(<T: AsRef<str>,>|self: TrimString<T>, to: Tui|{
|
||||
self.as_ref().draw(to)
|
||||
});
|
||||
|
||||
#[cfg(feature = "text")]
|
||||
impl Draw<Tui> for &str {
|
||||
fn draw (&self, to: &mut Tui) -> Drawn<u16> {
|
||||
let XYWH(x, y, w, h) = to.area();
|
||||
let mut max_w = 0u16;
|
||||
let mut max_h = 0u16;
|
||||
for (index, line) in self.split("\n").enumerate() {
|
||||
max_h += 1;
|
||||
max_w = max_w.max(line.len() as u16);
|
||||
let _ = to.text(&line, x, y + index as u16, width_chars_max(w, line) as u16)?;
|
||||
}
|
||||
Ok(Some(XYWH(x, y, w.min(max_w), h.min(max_h))))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "text")]
|
||||
impl<'t, T: AsRef<str>> Draw<Tui> for TrimStr<'_, T> {
|
||||
fn draw (&self, to: &mut Tui) -> Drawn<u16> {
|
||||
let text = self.1.as_ref();
|
||||
let area = layout_text_u16(text, to.area())?.unwrap();
|
||||
let XYWH(x, y, w, ..) = to.area();
|
||||
let mut width: u16 = 1;
|
||||
let mut chars = text.chars();
|
||||
while let Some(c) = chars.next() {
|
||||
if width > self.0 || width > w {
|
||||
break
|
||||
}
|
||||
to.set_char(x + width - 1, y, c);
|
||||
width += c.width().unwrap_or(0) as u16;
|
||||
}
|
||||
let XYWH(x, y, w, ..) = XYWH(
|
||||
to.x(), to.y(), to.w().min(self.0).min(self.1.as_ref().width() as u16), to.h()
|
||||
);
|
||||
to.text(&self.as_ref(), x, y, w)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "text")]
|
||||
fn layout_text_u16 (text: &str, area: XYWH<u16>) -> Perhaps<XYWH<u16>> {
|
||||
let XYWH(x, y, w, h) = area;
|
||||
let mut max_w = 0u16;
|
||||
let mut max_h = 0u16;
|
||||
for line in text.split("\n") {
|
||||
max_h += 1;
|
||||
max_w = max_w.max(line.len() as u16);
|
||||
}
|
||||
Ok(Some(XYWH(x, y, w.min(max_w), h.min(max_h))))
|
||||
}
|
||||
|
||||
pub struct ShowSize;
|
||||
|
||||
impl Draw<Tui> for ShowSize {
|
||||
fn draw (&self, to: &mut Tui) -> Drawn<u16> {
|
||||
let area = to.area();
|
||||
let info = format!("{area:?}");
|
||||
to.text(&info, area.0, area.1, info.len() as u16)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ShowSizeOf<T>(pub T);
|
||||
|
||||
impl<T: Draw<Tui>> Draw<Tui> for ShowSizeOf<T> {
|
||||
fn draw (&self, to: &mut Tui) -> Drawn<u16> {
|
||||
Ok(self.0.draw(to)?.map(|used|{
|
||||
let _ = to.text(&format!("{used:?}"), used.0, used.1, u16::MAX);
|
||||
used
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "eval")] fn_kw_layout_tui!(kw_tui_text |state, output, expr| {
|
||||
Ok(matches!(expr.head()?, Some("text")).then(||{
|
||||
if let Some(src) = expr.tail().src()? {
|
||||
src.draw(output)
|
||||
} else {
|
||||
return Ok(None)
|
||||
}
|
||||
}).transpose()?.flatten())
|
||||
});
|
||||
|
||||
#[cfg(feature = "text")] #[cfg(test)] #[test] fn test_layout_text_u16 () -> Usually<()> {
|
||||
assert_eq!(layout_text_u16("foo", XYWH(5, 6, 10, 10))?, Some(XYWH(5, 6, 3, 1)));
|
||||
assert_eq!(layout_text_u16("foo\nbarz", XYWH(5, 6, 10, 10))?, Some(XYWH(5, 6, 4, 2)));
|
||||
Ok(())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue