rustact 0.1.0

Async terminal UI framework inspired by React, built on top of ratatui and tokio.
Documentation
use std::io::{Stdout, stdout};

use anyhow::Context;
use crossterm::cursor::{Hide, Show};
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
use crossterm::execute;
use crossterm::terminal::{
    EnterAlternateScreen, LeaveAlternateScreen, SetTitle, disable_raw_mode, enable_raw_mode,
};
use ratatui::backend::{CrosstermBackend, TestBackend};
use ratatui::layout::Rect;
use ratatui::{Frame, Terminal};

use crate::interactions::reset_button_hitboxes;
use crate::runtime::View;
use crate::text_input::TextInputs;

mod widgets;

use widgets::{
    render_block, render_button, render_flex, render_form, render_gauge, render_layers,
    render_list, render_modal, render_table, render_tabs, render_text, render_text_input,
    render_toast_stack, render_tree,
};

pub struct Renderer {
    terminal: RendererKind,
}

enum RendererKind {
    Crossterm(Terminal<CrosstermBackend<Stdout>>),
    Headless(Terminal<TestBackend>),
}

impl Renderer {
    pub fn new(title: &str) -> anyhow::Result<Self> {
        enable_raw_mode().context("enable raw mode")?;
        let mut stdout = stdout();
        execute!(
            stdout,
            EnterAlternateScreen,
            EnableMouseCapture,
            Hide,
            SetTitle(title)
        )
        .context("prepare terminal")?;
        let backend = CrosstermBackend::new(stdout);
        let terminal = Terminal::new(backend).context("build terminal")?;
        Ok(Self {
            terminal: RendererKind::Crossterm(terminal),
        })
    }

    pub fn headless() -> anyhow::Result<Self> {
        let backend = TestBackend::new(80, 24);
        let terminal = Terminal::new(backend).context("build headless terminal")?;
        Ok(Self {
            terminal: RendererKind::Headless(terminal),
        })
    }

    pub fn draw(&mut self, view: &View) -> anyhow::Result<()> {
        reset_button_hitboxes();
        TextInputs::reset_hitboxes();
        match &mut self.terminal {
            RendererKind::Crossterm(terminal) => {
                terminal.draw(|frame| {
                    let area = frame.size();
                    render_view(frame, area, view);
                })?;
            }
            RendererKind::Headless(terminal) => {
                terminal.draw(|frame| {
                    let area = frame.size();
                    render_view(frame, area, view);
                })?;
            }
        }
        Ok(())
    }
}

impl Drop for Renderer {
    fn drop(&mut self) {
        if matches!(self.terminal, RendererKind::Crossterm(_)) {
            let _ = disable_raw_mode();
            let mut stdout = stdout();
            let _ = execute!(
                stdout,
                Show,
                DisableMouseCapture,
                LeaveAlternateScreen,
                SetTitle("Terminal")
            );
        }
    }
}

fn render_view(frame: &mut Frame<'_>, area: Rect, view: &View) {
    match view {
        View::Empty => {}
        View::Text(text) => render_text(frame, area, text),
        View::Flex(flex) => render_flex(frame, area, flex, render_view),
        View::Block(block) => render_block(frame, area, block, render_view),
        View::List(list) => render_list(frame, area, list),
        View::Gauge(gauge) => render_gauge(frame, area, gauge),
        View::Button(button) => render_button(frame, area, button),
        View::Table(table) => render_table(frame, area, table),
        View::Tree(tree) => render_tree(frame, area, tree),
        View::Form(form) => render_form(frame, area, form),
        View::Input(input) => render_text_input(frame, area, input),
        View::Tabs(tabs) => render_tabs(frame, area, tabs, render_view),
        View::Layered(layers) => render_layers(frame, area, layers, render_view),
        View::Modal(modal) => render_modal(frame, area, modal, render_view),
        View::ToastStack(stack) => render_toast_stack(frame, area, stack),
    }
}