mirror of
https://codeberg.org/unspeaker/tengri.git
synced 2026-02-21 18:49:04 +01:00
148 lines
5.4 KiB
Rust
148 lines
5.4 KiB
Rust
use crate::*;
|
|
use std::time::Duration;
|
|
|
|
mod tui_buffer; pub use self::tui_buffer::*;
|
|
mod tui_input; pub use self::tui_input::*;
|
|
mod tui_event; pub use self::tui_event::*;
|
|
mod tui_output; pub use self::tui_output::*;
|
|
mod tui_perf; pub use self::tui_perf::*;
|
|
|
|
// The `Tui` struct (the *engine*) implements the
|
|
// `tengri_input::Input` and `tengri_output::Out` traits.
|
|
|
|
// At launch, the `Tui` engine spawns two threads, the render thread and the input thread.
|
|
// the application may further spawn other threads. All threads communicate using shared ownership:
|
|
// `Arc<RwLock<T>>` and `Arc<AtomicT>`. Thus, at launch the engine and application instances are expected to be wrapped in `Arc<RwLock>`.
|
|
pub struct Tui {
|
|
pub exited: Arc<AtomicBool>,
|
|
pub backend: CrosstermBackend<Stdout>,
|
|
pub buffer: Buffer,
|
|
pub area: [u16;4],
|
|
pub perf: PerfModel,
|
|
}
|
|
|
|
impl Tui {
|
|
/// Construct a new TUI engine and wrap it for shared ownership.
|
|
pub fn new () -> Usually<Arc<RwLock<Self>>> {
|
|
let backend = CrosstermBackend::new(stdout());
|
|
let Size { width, height } = backend.size()?;
|
|
Ok(Arc::new(RwLock::new(Self {
|
|
exited: Arc::new(AtomicBool::new(false)),
|
|
buffer: Buffer::empty(Rect { x: 0, y: 0, width, height }),
|
|
area: [0, 0, width, height],
|
|
backend,
|
|
perf: Default::default(),
|
|
})))
|
|
}
|
|
/// True if done
|
|
pub fn exited (&self) -> bool {
|
|
self.exited.fetch_and(true, Relaxed)
|
|
}
|
|
/// Prepare before run
|
|
pub 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)
|
|
}
|
|
/// Update the display buffer.
|
|
pub 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
|
|
}
|
|
/// Clean up after run
|
|
pub fn teardown (&mut self) -> Usually<()> {
|
|
stdout().execute(LeaveAlternateScreen)?;
|
|
self.backend.show_cursor()?;
|
|
disable_raw_mode().map_err(Into::into)
|
|
}
|
|
}
|
|
|
|
pub trait TuiDraw = Draw<TuiOut>;
|
|
pub trait TuiLayout = Layout<TuiOut>;
|
|
pub trait TuiContent = Content<TuiOut>;
|
|
pub trait TuiHandle = Handle<TuiIn>;
|
|
pub trait TuiWidget = TuiDraw + TuiHandle;
|
|
|
|
pub trait TuiRun<T: TuiWidget + 'static> {
|
|
/// Run an app in the main loop.
|
|
fn run (&self, state: &Arc<RwLock<T>>) -> Usually<()>;
|
|
}
|
|
|
|
impl<T: TuiWidget + Send + Sync + 'static> TuiRun<T> for Arc<RwLock<Tui>> {
|
|
fn run (&self, state: &Arc<RwLock<T>>) -> Usually<()> {
|
|
let _input_thread = TuiIn::run_input(self, state, Duration::from_millis(100));
|
|
self.write().unwrap().setup()?;
|
|
let render_thread = TuiOut::run_output(self, state, Duration::from_millis(10))?;
|
|
match render_thread.join() {
|
|
Ok(result) => {
|
|
self.write().unwrap().teardown()?;
|
|
println!("\n\rRan successfully: {result:?}\n\r");
|
|
},
|
|
Err(error) => {
|
|
self.write().unwrap().teardown()?;
|
|
panic!("\n\rDraw thread failed: error={error:?}.\n\r")
|
|
},
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "dsl")]
|
|
pub fn evaluate_output_expression_tui <'a, S> (
|
|
state: &S, output: &mut TuiOut, expr: impl Expression + 'a
|
|
) -> Usually<bool> where
|
|
S: View<TuiOut, ()>
|
|
+ for<'b>Namespace<'b, bool>
|
|
+ for<'b>Namespace<'b, u16>
|
|
+ for<'b>Namespace<'b, Color>
|
|
{
|
|
// See `tengri_output::evaluate_output_expression`
|
|
let head = expr.head()?;
|
|
let mut frags = head.src()?.unwrap_or_default().split("/");
|
|
let args = expr.tail();
|
|
let arg0 = args.head();
|
|
let tail0 = args.tail();
|
|
let arg1 = tail0.head();
|
|
let tail1 = tail0.tail();
|
|
let _arg2 = tail1.head();
|
|
match frags.next() {
|
|
|
|
Some("text") => if let Some(src) = args?.src()? { output.place(&src) },
|
|
|
|
Some("fg") => {
|
|
let arg0 = arg0?.expect("fg: expected arg 0 (color)");
|
|
output.place(&Tui::fg(
|
|
Namespace::<Color>::resolve(state, arg0)?.unwrap_or_else(||panic!("fg: {arg0:?}: not a color")),
|
|
Thunk::new(move|output: &mut TuiOut|state.view(output, &arg1).unwrap()),
|
|
))
|
|
},
|
|
|
|
Some("bg") => {
|
|
let arg0 = arg0?.expect("bg: expected arg 0 (color)");
|
|
output.place(&Tui::bg(
|
|
Namespace::<Color>::resolve(state, arg0)?.unwrap_or_else(||panic!("bg: {arg0:?}: not a color")),
|
|
Thunk::new(move|output: &mut TuiOut|state.view(output, &arg1).unwrap()),
|
|
))
|
|
},
|
|
|
|
_ => return Ok(false)
|
|
|
|
};
|
|
Ok(true)
|
|
}
|