use crate::app::Context;
use crate::buffer::{DoubleBuffer, ScreenBuffer};
use crate::component::{Action, Component, ComponentId};
use crate::node::Div;
use crate::node::Node;
use crate::terminal::TerminalRenderer;
use crate::vnode::VNode;
use crate::{Rect, VDom};
use crossterm::{
ExecutableCommand, cursor,
event::{self, Event},
execute,
style::{Print, ResetColor, SetBackgroundColor, SetForegroundColor},
terminal,
};
use std::cell::RefCell;
use std::io;
use std::rc::Rc;
use super::config::RenderConfig;
use super::events::{handle_key_event, handle_mouse_event};
use super::renderer::render_node_to_buffer;
use std::collections::HashMap;
type RenderLogFn = Box<dyn Fn(&str)>;
pub struct ExitSignal;
pub struct App {
vdom: VDom,
running: Rc<RefCell<bool>>,
needs_render: Rc<RefCell<bool>>,
double_buffer: DoubleBuffer,
render_log_fn: Option<RenderLogFn>,
terminal_renderer: TerminalRenderer,
config: RenderConfig,
}
impl App {
pub fn new() -> io::Result<Self> {
terminal::enable_raw_mode()?;
let mut stdout = io::stdout();
stdout.execute(terminal::EnterAlternateScreen)?;
stdout.execute(cursor::Hide)?;
stdout.execute(event::EnableMouseCapture)?;
let running = Rc::new(RefCell::new(true));
let needs_render = Rc::new(RefCell::new(true));
let (width, height) = terminal::size()?;
Ok(Self {
vdom: VDom::new(),
running,
needs_render,
double_buffer: DoubleBuffer::new(width, height),
render_log_fn: None,
terminal_renderer: TerminalRenderer::new(),
config: RenderConfig::default(),
})
}
pub fn run<C>(&mut self, root_component: C) -> io::Result<()>
where
C: Component + Clone,
{
self.run_loop(root_component)
}
pub fn render_config(mut self, config: RenderConfig) -> Self {
self.config = config;
self
}
pub fn disable_all_optimizations(mut self) -> Self {
self.config = RenderConfig::debug();
self
}
pub fn disable_double_buffering(mut self) -> Self {
self.config.double_buffering = false;
self
}
pub fn disable_terminal_optimizations(mut self) -> Self {
self.config.terminal_optimizations = false;
self
}
pub fn disable_cell_diffing(mut self) -> Self {
self.config.cell_diffing = false;
self
}
fn run_loop<C>(&mut self, root_component: C) -> io::Result<()>
where
C: Component + Clone,
{
let mut context = Context::new();
let mut components: HashMap<ComponentId, Box<dyn Component>> = HashMap::new();
let root_id = ComponentId::default();
components.insert(
root_id.clone(),
Box::new(root_component) as Box<dyn Component>,
);
let mut needs_render = true;
while *self.running.borrow() {
let vnode_tree = if let Some(root_component) = components.get(&root_id) {
context.current_component_id = root_id.clone();
let mut temp_components = HashMap::new();
match self.expand_component_tree(
root_component.as_ref(),
&mut context,
&mut temp_components,
) {
Ok(vnode) => {
components.extend(temp_components);
vnode
}
Err(ExitSignal) => {
*self.running.borrow_mut() = false;
break;
}
}
} else {
VNode::div()
};
if needs_render || *self.needs_render.borrow() {
self.vdom.render(vnode_tree);
let (width, height) = terminal::size()?;
self.vdom.layout(width, height);
self.draw()?;
if let Some(log_fn) = &self.render_log_fn {
let debug_string = self.render_tree_debug_string();
log_fn(&debug_string);
}
*self.needs_render.borrow_mut() = false;
needs_render = false;
}
if event::poll(std::time::Duration::from_millis(100))? {
match event::read()? {
Event::Key(key_event) => {
handle_key_event(&self.vdom, key_event);
needs_render = true;
}
Event::Mouse(mouse_event) => {
handle_mouse_event(&self.vdom, mouse_event);
needs_render = true;
}
Event::Resize(width, height) => {
self.vdom.layout(width, height);
self.double_buffer.resize(width, height);
*self.needs_render.borrow_mut() = true;
}
_ => {}
}
}
}
Ok(())
}
fn expand_component_tree(
&self,
component: &dyn Component,
context: &mut Context,
components: &mut HashMap<ComponentId, Box<dyn Component>>,
) -> Result<VNode, ExitSignal> {
let messages = context.drain_all_messages();
for (msg, topic) in messages {
let action = component.update(context, msg, topic.as_deref());
match action {
Action::Update(new_state) => {
context
.states
.insert(context.current_component_id.clone(), new_state);
if let Some(topic_name) = topic
&& context
.topics
.claim_topic(topic_name.clone(), context.current_component_id.clone())
{
context.drain_topic_if_claimed(&topic_name, &context.current_component_id);
}
}
Action::UpdateTopic(topic_name, new_state) => {
context.topics.update_topic(
topic_name.clone(),
new_state,
context.current_component_id.clone(),
);
if let Some(msg_topic) = topic
&& msg_topic == topic_name
{
context.drain_topic_if_claimed(&topic_name, &context.current_component_id);
}
}
Action::Exit => {
return Err(ExitSignal);
}
Action::None => {
}
}
}
let node = component.view(context);
self.node_to_vnode(node, context, components, 0)
}
fn node_to_vnode(
&self,
node: Node,
context: &mut Context,
components: &mut HashMap<ComponentId, Box<dyn Component>>,
child_index: usize,
) -> Result<VNode, ExitSignal> {
match node {
Node::Component(mut component) => {
let parent_id = context.current_component_id.clone();
context.current_component_id = parent_id.child(child_index);
let component_id = context.current_component_id.clone();
component.set_id(component_id.clone());
let vnode = self.expand_component_tree(component.as_ref(), context, components)?;
components.insert(component_id, component);
context.current_component_id = parent_id;
Ok(vnode)
}
Node::Div(div) => {
let mut vnode_children = Vec::new();
for (i, child) in div.children.into_iter().enumerate() {
vnode_children.push(self.node_to_vnode(child, context, components, i)?);
}
let mut vnode_div = Div::new();
vnode_div.children = vnode_children;
vnode_div.styles = div.styles;
vnode_div.events = div.events;
vnode_div.focusable = div.focusable;
vnode_div.focused = div.focused;
Ok(VNode::Div(vnode_div))
}
Node::Text(text) => {
Ok(VNode::Text(text))
}
Node::RichText(rich) => {
Ok(VNode::RichText(rich))
}
}
}
pub fn render_tree_debug_string(&self) -> String {
self.vdom.get_render_tree().debug_string()
}
pub fn set_render_log_fn<F: Fn(&str) + 'static>(&mut self, log_fn: F) {
self.render_log_fn = Some(Box::new(log_fn));
}
fn draw(&mut self) -> io::Result<()> {
if self.config.double_buffering {
self.draw_with_double_buffer()
} else {
self.draw_direct()
}
}
fn draw_with_double_buffer(&mut self) -> io::Result<()> {
self.double_buffer.clear_back();
if let Some(root) = &self.vdom.get_render_tree().root {
let root_ref = root.borrow();
let buffer = self.double_buffer.back_buffer_mut();
let (width, height) = buffer.dimensions();
let clip_rect = Rect::new(0, 0, width, height);
render_node_to_buffer(&root_ref, buffer, &clip_rect, None);
}
if self.config.cell_diffing {
let updates = self.double_buffer.diff();
if self.config.terminal_optimizations {
self.terminal_renderer.apply_updates(updates)?;
} else {
self.terminal_renderer.apply_updates_direct(updates)?;
}
} else {
let buffer = self.double_buffer.back_buffer_mut();
self.terminal_renderer.draw_full_buffer(buffer)?;
}
self.double_buffer.swap();
self.vdom.get_render_tree().clear_all_dirty();
Ok(())
}
fn draw_direct(&mut self) -> io::Result<()> {
execute!(io::stdout(), terminal::Clear(terminal::ClearType::All))?;
let (width, height) = terminal::size()?;
let mut buffer = ScreenBuffer::new(width, height);
if let Some(root) = &self.vdom.get_render_tree().root {
let root_ref = root.borrow();
let clip_rect = Rect::new(0, 0, width, height);
render_node_to_buffer(&root_ref, &mut buffer, &clip_rect, None);
}
let mut stdout = io::stdout();
for y in 0..height {
for x in 0..width {
if let Some(cell) = buffer.get_cell(x, y) {
execute!(stdout, cursor::MoveTo(x, y))?;
if let Some(fg) = &cell.fg {
execute!(
stdout,
SetForegroundColor(self.terminal_renderer.color_to_crossterm(*fg))
)?;
}
if let Some(bg) = &cell.bg {
execute!(
stdout,
SetBackgroundColor(self.terminal_renderer.color_to_crossterm(*bg))
)?;
}
execute!(stdout, Print(cell.char))?;
if cell.fg.is_some() || cell.bg.is_some() {
execute!(stdout, ResetColor)?;
}
}
}
}
self.vdom.get_render_tree().clear_all_dirty();
Ok(())
}
}
impl Drop for App {
fn drop(&mut self) {
use std::io::Write;
let mut stdout = io::stdout();
let _ = stdout.execute(event::DisableMouseCapture);
let _ = stdout.execute(cursor::Show);
let _ = stdout.execute(terminal::LeaveAlternateScreen);
let _ = stdout.flush();
let _ = terminal::disable_raw_mode();
}
}