use std::{
io::{Write, stdout},
panic::{set_hook, take_hook},
sync::Once,
time::Instant,
};
use termal::codes::{
DISABLE_ALTERNATIVE_BUFFER, ENABLE_ALTERNATIVE_BUFFER, ERASE_SCREEN,
HIDE_CURSOR, SHOW_CURSOR,
};
use crate::{
buffer::Buffer,
error::Error,
geometry::{Padding, Rect, Vec2},
prelude::MouseEvent,
term::{
Action, Application, Frame,
backend::{Backend, DefaultBackend, Event},
disable_bracketed_paste, disable_mouse_capture,
enable_bracketed_paste, enable_mouse_capture,
},
widgets::{Element, EventResult, LayoutNode, Spacer, Widget},
};
static HOOK_SET: Once = Once::new();
#[derive(Debug)]
pub struct Term<M: 'static = (), B: Backend = DefaultBackend> {
backend: B,
prev: Option<Buffer>,
prev_widget: Option<Element<M>>,
small: Option<Element<M>>,
layout: LayoutNode,
relayout: bool,
padding: Padding,
setuped: bool,
mouse_enabled: bool,
paste_enabled: bool,
last_size: Vec2,
}
impl<M, B: Backend> Term<M, B>
where
M: Clone + 'static,
{
pub fn new() -> Self {
Self::custom(B::default())
}
pub fn init() -> Result<Self, Error> {
let mut term = Self::new();
term = term.setup()?;
Ok(term)
}
pub fn setup(mut self) -> Result<Self, Error> {
if !self.setuped {
B::enable_raw_mode()?;
print!(
"{}{}{}",
ENABLE_ALTERNATIVE_BUFFER, ERASE_SCREEN, HIDE_CURSOR
);
_ = stdout().flush();
HOOK_SET.call_once(|| {
let hook = take_hook();
set_hook(Box::new(move |pi| {
Self::restore();
hook(pi);
}));
});
self.setuped = true;
}
Ok(self)
}
pub fn custom(backend: B) -> Self {
Self {
backend,
prev: None,
prev_widget: None,
small: None,
layout: LayoutNode::default(),
relayout: false,
padding: Padding::default(),
setuped: false,
mouse_enabled: false,
paste_enabled: false,
last_size: Vec2::default(),
}
}
pub fn with_mouse(mut self) -> Self {
if !self.mouse_enabled {
enable_mouse_capture();
self.mouse_enabled = true;
}
self
}
pub fn with_paste(mut self) -> Self {
if !self.paste_enabled {
enable_bracketed_paste();
self.paste_enabled = true;
}
self
}
pub fn disable_mouse(&mut self) {
if self.mouse_enabled {
disable_mouse_capture();
self.mouse_enabled = false;
}
}
pub fn disable_paste(&mut self) {
if self.paste_enabled {
disable_bracketed_paste();
self.paste_enabled = false;
}
}
pub fn padding<T: Into<Padding>>(mut self, padding: T) -> Self {
self.padding = padding.into();
self
}
pub fn small_screen<T>(mut self, small_screen: T) -> Self
where
T: Into<Element<M>>,
{
self.small = Some(small_screen.into());
self
}
pub fn clear_layout(&mut self) {
self.layout = LayoutNode::default();
}
pub fn render<T>(&mut self, widget: T) -> Result<(), Error>
where
T: Into<Element<M>>,
{
let widget = widget.into();
let rect = self.get_rect()?;
self.render_widget(widget, rect);
Ok(())
}
pub fn draw<F>(&mut self, get_widget: F) -> Result<(), Error>
where
F: FnOnce(&Frame) -> Element<M>,
{
let rect = self.get_rect()?;
let frame = Frame::new(rect);
let widget = get_widget(&frame);
self.render_widget(widget, rect);
Ok(())
}
pub fn rerender(&mut self) -> Result<(), Error> {
let wid = self.prev_widget.take().ok_or(Error::NoPreviousWidget)?;
let rect = self.get_rect()?;
self.render_widget(wid, rect);
Ok(())
}
pub fn run<A>(&mut self, app: &mut A) -> Result<(), Error>
where
A: Application<Message = M>,
{
self.draw(|f| app.view(f))?;
let mut last_tick = Instant::now();
let timeout = app.poll_timeout();
loop {
let mut action = Action::NONE;
if let Some(event) = self.backend.read_event(timeout)? {
match event {
Event::Mouse(ref e) => action |= self.handle_mouse(app, e),
Event::Resize(_, _) => action |= Action::RENDER,
_ => {}
}
action |= app.event(event);
action |= Action::RELAYOUT;
}
let now = Instant::now();
let delta = now.duration_since(last_tick);
last_tick = now;
action |= app.update(delta);
if action.contains(Action::QUIT) {
break;
}
if action.contains(Action::RELAYOUT) {
self.relayout = true;
}
if action.contains(Action::RENDER) {
self.draw(|f| app.view(f))?;
} else if action.contains(Action::RERENDER) {
self.rerender()?;
}
}
Ok(())
}
pub fn restore() {
if B::is_raw_mode_enabled() {
print!("{}{}", DISABLE_ALTERNATIVE_BUFFER, SHOW_CURSOR);
_ = B::disable_raw_mode();
}
disable_mouse_capture();
disable_bracketed_paste();
_ = stdout().flush();
}
pub fn get_size(&self) -> Option<(usize, usize)> {
self.backend.get_size().ok()
}
fn handle_mouse<A>(&mut self, app: &mut A, event: &MouseEvent) -> Action
where
A: Application<Message = M>,
{
let Some(root) = &self.prev_widget else {
return Action::NONE;
};
match root.on_event(&self.layout, event) {
EventResult::None => Action::NONE,
EventResult::Consumed => Action::RERENDER,
EventResult::Response(m) => app.message(m),
}
}
fn render_widget(&mut self, widget: Element<M>, rect: Rect) {
let mut buffer = Buffer::empty(rect);
let dummy: Element<M> = Spacer::new().into();
let prev = if self.relayout {
self.relayout = false;
&dummy
} else {
self.prev_widget.as_ref().unwrap_or(&dummy)
};
match &self.small {
Some(small)
if rect.width() < widget.width(rect.size())
|| rect.height() < widget.height(rect.size()) =>
{
self.layout.diff(prev, small);
self.layout.layout(small, rect);
small.render(&mut buffer, &self.layout);
}
_ => {
self.layout.diff(prev, &widget);
self.layout.layout(&widget, rect);
widget.render(&mut buffer, &self.layout);
}
};
self.prev_widget = Some(widget);
match &self.prev {
Some(prev) => buffer.render_diff(prev),
None => buffer.render(),
}
self.prev = Some(buffer);
}
fn get_rect(&mut self) -> Result<Rect, Error> {
let (w, h) = self.get_size().ok_or(Error::UnknownTerminalSize)?;
let pos = Vec2::new(1 + self.padding.left, 1 + self.padding.top);
let size = Vec2::new(
w.saturating_sub(self.padding.get_horizontal()),
h.saturating_sub(self.padding.get_vertical()),
);
if size != self.last_size {
self.last_size = size;
}
Ok(Rect::from_coords(pos, size))
}
}
impl<M> Default for Term<M, DefaultBackend> {
fn default() -> Self {
Self {
backend: Default::default(),
prev: Default::default(),
prev_widget: Default::default(),
small: Default::default(),
layout: Default::default(),
relayout: false,
padding: Default::default(),
setuped: false,
mouse_enabled: false,
paste_enabled: false,
last_size: Default::default(),
}
}
}
impl<M, B: Backend> Drop for Term<M, B> {
fn drop(&mut self) {
if self.mouse_enabled {
disable_mouse_capture();
}
if self.paste_enabled {
disable_bracketed_paste();
}
if self.setuped {
print!("{}{}", DISABLE_ALTERNATIVE_BUFFER, SHOW_CURSOR);
_ = stdout().flush();
_ = B::disable_raw_mode();
}
}
}