use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use crate::cmd::{Cmd, CmdExecutor};
use crate::core::{Element, VNode};
use crate::hooks::use_mouse::is_mouse_enabled;
use crate::layout::LayoutEngine;
use crate::renderer::Terminal;
use crate::runtime::{RuntimeContext, set_current_runtime, with_runtime};
use tokio::sync::mpsc;
use super::builder::{AppOptions, CancelToken};
use super::filter::FilterChain;
use super::pipeline::RenderPipeline;
use super::registry::{AppRuntime, AppSink, RenderHandle, register_app};
use super::runtime::EventLoop;
use super::runtime_bridge::RuntimeBridge;
use super::static_content::StaticRenderer;
use super::terminal_controller::TerminalController;
pub struct App<F>
where
F: Fn() -> Element,
{
component: F,
terminal: Terminal,
layout_engine: LayoutEngine,
options: AppOptions,
should_exit: Arc<AtomicBool>,
runtime: Arc<AppRuntime>,
static_renderer: StaticRenderer,
last_width: u16,
last_height: u16,
filter_chain: FilterChain,
cancel_token: Option<CancelToken>,
cmd_executor: CmdExecutor,
cmd_render_rx: Option<mpsc::UnboundedReceiver<()>>,
runtime_context: Rc<RefCell<RuntimeContext>>,
previous_vnode: Option<VNode>,
}
impl<F> App<F>
where
F: Fn() -> Element,
{
pub fn new(component: F) -> Self {
Self::with_options(component, AppOptions::default())
}
pub fn with_options(component: F, options: AppOptions) -> Self {
Self::with_options_and_filters(component, options, FilterChain::new())
}
pub fn with_options_and_filters(
component: F,
options: AppOptions,
filter_chain: FilterChain,
) -> Self {
Self::with_full_config(component, options, filter_chain, None)
}
pub fn with_full_config(
component: F,
options: AppOptions,
filter_chain: FilterChain,
cancel_token: Option<CancelToken>,
) -> Self {
let runtime = AppRuntime::new(options.alternate_screen);
let render_handle = RenderHandle::new(runtime.clone());
let should_exit = Arc::new(AtomicBool::new(false));
let runtime_context = Rc::new(RefCell::new(RuntimeContext::with_app_control(
should_exit.clone(),
render_handle.clone(),
)));
let (cmd_render_tx, cmd_render_rx) = mpsc::unbounded_channel();
let cmd_executor = CmdExecutor::new(cmd_render_tx);
let runtime_clone = runtime.clone();
runtime_context
.borrow_mut()
.set_render_callback(Arc::new(move || {
runtime_clone.request_render();
}));
let (initial_width, initial_height) = Terminal::size().unwrap_or((80, 24));
Self {
component,
terminal: Terminal::new(),
layout_engine: LayoutEngine::new(),
options,
should_exit,
runtime,
static_renderer: StaticRenderer::new(),
last_width: initial_width,
last_height: initial_height,
filter_chain,
cancel_token,
cmd_executor,
cmd_render_rx: Some(cmd_render_rx),
runtime_context,
previous_vnode: None,
}
}
pub fn run(mut self) -> std::io::Result<()> {
let _app_guard = register_app(self.runtime.clone());
set_current_runtime(Some(self.runtime_context.clone()));
struct CurrentRuntimeGuard;
impl Drop for CurrentRuntimeGuard {
fn drop(&mut self) {
set_current_runtime(None);
}
}
let _runtime_guard = CurrentRuntimeGuard;
if self.options.alternate_screen {
self.terminal.enter()?;
self.runtime.set_alt_screen_state(true);
} else {
self.terminal.enter_inline()?;
self.runtime.set_alt_screen_state(false);
}
let filter_chain = std::mem::take(&mut self.filter_chain);
let frame_rate =
super::frame_rate::FrameRateController::new(self.options.to_frame_rate_config());
let shared_stats = frame_rate.shared_stats();
self.runtime_context
.borrow_mut()
.set_frame_rate_stats(shared_stats);
let mut event_loop = EventLoop::with_filters(
self.runtime.clone(),
self.should_exit.clone(),
frame_rate,
self.options.exit_on_ctrl_c,
filter_chain,
);
if let Some(ref token) = self.cancel_token {
event_loop = event_loop.with_cancel_flag(token.flag());
}
if let Some(rx) = self.cmd_render_rx.take() {
event_loop = event_loop.with_render_rx(rx);
}
loop {
event_loop.run(|| {
RuntimeBridge::handle_exec_requests(&mut self.terminal, &self.runtime)?;
RuntimeBridge::handle_terminal_commands(
&mut self.terminal,
&self.runtime,
&mut self.last_width,
&mut self.last_height,
)?;
RuntimeBridge::handle_mode_switch_request(&mut self.terminal, &self.runtime)?;
RuntimeBridge::handle_println_messages(&mut self.terminal, &self.runtime)?;
let (width, height) = Terminal::size()?;
if width != self.last_width || height != self.last_height {
self.handle_resize(width, height);
}
self.render_frame()
})?;
if self.runtime.take_suspend_request() {
self.handle_suspend()?;
continue;
}
break;
}
if self.terminal.is_alt_screen() {
self.terminal.exit()?;
} else {
self.terminal.exit_inline()?;
}
Ok(())
}
#[cfg(unix)]
fn handle_suspend(&mut self) -> std::io::Result<()> {
self.terminal.suspend()?;
crate::runtime::suspend_self()?;
self.terminal.resume()?;
self.runtime.request_render();
Ok(())
}
#[cfg(not(unix))]
fn handle_suspend(&mut self) -> std::io::Result<()> {
Ok(())
}
fn handle_resize(&mut self, new_width: u16, new_height: u16) {
TerminalController::handle_resize(
&mut self.terminal,
&mut self.last_width,
&mut self.last_height,
new_width,
new_height,
);
}
fn render_frame(&mut self) -> std::io::Result<()> {
let (width, height) = Terminal::size()?;
let root = with_runtime(self.runtime_context.clone(), || (self.component)());
let cmds = self.runtime_context.borrow_mut().take_cmds();
if !cmds.is_empty() {
self.cmd_executor.execute(Cmd::batch(cmds));
}
if is_mouse_enabled() {
self.terminal.enable_mouse()?;
} else {
self.terminal.disable_mouse()?;
}
let new_static_lines = self.static_renderer.extract_static_content(&root, width);
if !new_static_lines.is_empty() {
self.static_renderer
.commit_static_content(&new_static_lines, &mut self.terminal)?;
}
let dynamic_root = self.static_renderer.filter_static_elements(&root);
let rendered = RenderPipeline::render_dynamic_frame(
&dynamic_root,
width,
height,
&mut self.layout_engine,
&self.runtime_context,
&mut self.previous_vnode,
);
self.terminal.render(&rendered)
}
pub fn exit(&self) {
self.should_exit.store(true, Ordering::SeqCst);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Element;
use crate::renderer::registry::{is_alt_screen, render_handle};
#[test]
fn test_registry_cleanup_on_drop() {
let runtime = AppRuntime::new(false);
{
let _guard = register_app(runtime);
assert!(render_handle().is_some());
assert_eq!(is_alt_screen(), Some(false));
}
assert!(render_handle().is_none());
assert_eq!(is_alt_screen(), None);
}
#[test]
fn test_exit_sets_should_exit_flag() {
let app = App::new(|| Element::text("ok"));
assert!(!app.should_exit.load(Ordering::SeqCst));
app.exit();
assert!(app.should_exit.load(Ordering::SeqCst));
}
#[test]
fn test_exit_updates_runtime_context_exit_state() {
let app = App::new(|| Element::text("ok"));
assert!(!app.runtime_context.borrow().should_exit());
app.exit();
assert!(app.runtime_context.borrow().should_exit());
}
}