///|
/// TUI Application renderer with reactive signals integration
/// Provides ink-like API for building terminal UIs
///|
/// App manages the render loop and diff-based updates
pub struct App {
width : Int
height : Int
mut prev_buffer : CharBuffer?
mut last_layout : @node.Layout?
handlers : EventHandlers
}
///|
pub fn App::new(width : Int, height : Int) -> App {
{
width,
height,
prev_buffer: None,
last_layout: None,
handlers: EventHandlers::new(),
}
}
///|
/// Render a component tree to ANSI string
pub fn App::render(self : App, component : Component) -> String {
// Compute layout
let layout = compute_layout(component.node, self.width, self.height)
// Save layout for hit testing
self.last_layout = Some(layout)
// Create new buffer
let buf = CharBuffer::new(self.width, self.height)
render_layout(buf, layout, component.styles, component.texts, 0, 0)
// Generate output with diff if possible
let output = match self.prev_buffer {
Some(prev) => buffer_diff_to_ansi(buf, prev)
None =>
// First render: clear screen and render full
ansi_clear_screen() + ansi_move_to(0, 0) + buffer_to_ansi(buf)
}
// Save current buffer for next diff
self.prev_buffer = Some(buf)
output
}
///|
/// Render with cursor hidden and positioned at origin
pub fn App::render_frame(self : App, component : Component) -> String {
ansi_hide_cursor() + self.render(component)
}
///|
/// Create a reactive effect that re-renders when signals change
/// Returns a dispose function
pub fn App::create_render_effect(
self : App,
render_fn : () -> Component,
output_fn : (String) -> Unit,
) -> () -> Unit {
@signals.render_effect(fn() {
let component = render_fn()
let output = self.render_frame(component)
output_fn(output)
})
}
///|
/// Initialize terminal (enter alt screen, hide cursor)
pub fn App::init_terminal() -> String {
ansi_enter_alt_screen() + ansi_hide_cursor() + ansi_clear_screen()
}
///|
/// Restore terminal (show cursor, leave alt screen)
pub fn App::restore_terminal() -> String {
ansi_show_cursor() + ansi_leave_alt_screen()
}
///|
/// Full-screen app helper: sets up alt screen and handles cleanup
pub fn run_app(
width : Int,
height : Int,
render_fn : () -> Component,
output_fn : (String) -> Unit,
) -> () -> Unit {
let app = App::new(width, height)
// Initialize terminal
output_fn(App::init_terminal())
// Create render effect
let dispose = app.create_render_effect(render_fn, output_fn)
// Return cleanup function
fn() {
dispose()
output_fn(App::restore_terminal())
}
}
///|
/// Simple render function for one-shot rendering (no reactivity)
pub fn render_once(width : Int, height : Int, component : Component) -> String {
let app = App::new(width, height)
app.render(component)
}
///|
/// Register a click handler for a component ID
pub fn App::on_click(self : App, id : String, handler : () -> Unit) -> Unit {
self.handlers.on_click(id, ClickHandler(handler))
}
///|
/// Handle a mouse event
/// Returns true if an event handler was invoked
pub fn App::handle_mouse(self : App, event : MouseEvent) -> Bool {
match event.event_type {
MouseEventType::Press =>
match event.button {
MouseButton::Left =>
match self.last_layout {
Some(layout) =>
handle_click(layout, self.handlers, event.x, event.y)
None => false
}
_ => false
}
_ => false
}
}
///|
/// Perform hit test at coordinates
pub fn App::hit_test(self : App, x : Int, y : Int) -> HitTestResult? {
match self.last_layout {
Some(layout) => hit_test_root(layout, x, y)
None => None
}
}
///|
/// Find component by ID
pub fn App::find_by_id(self : App, id : String) -> HitTestResult? {
match self.last_layout {
Some(layout) => find_layout_by_id_root(layout, id)
None => None
}
}
///|
/// Clear the previous buffer to force a full re-render on next render call
pub fn App::clear_prev_buffer(self : App) -> Unit {
self.prev_buffer = None
}