mod builder;
pub mod declarative_router;
#[cfg(feature = "hot-reload")]
mod hot_reload;
mod inspector;
pub mod profiler;
pub mod router;
pub mod screen;
pub mod snapshot;
pub use builder::AppBuilder;
pub use declarative_router::{
declarative_router, is_active, link, use_param, use_params, use_path, use_route,
DeclarativeRouter, Link, ReactiveRouteState, RouteContext, RouteRenderer,
};
#[cfg(feature = "hot-reload")]
pub use hot_reload::{hot_reload, HotReload, HotReloadBuilder, HotReloadConfig, HotReloadEvent};
pub use inspector::{inspector, Inspector, WidgetInfo};
pub use profiler::{
fps_counter, profiler as new_profiler, FpsCounter, Metric, MetricType, Profiler, Sample, Stats,
};
pub use router::{
router, routes, HistoryEntry, NavigationEvent, QueryParams, Route, RouteBuilder, RouteParams,
Router,
};
pub use screen::{
screen_manager, simple_screen, Screen, ScreenConfig, ScreenEvent, ScreenId, ScreenManager,
ScreenMode, ScreenResult, SimpleScreen, Transition,
};
pub use snapshot::{snapshot, Snapshot, SnapshotConfig, SnapshotResult};
use crate::constants::FRAME_DURATION_60FPS;
use crate::dom::DomRenderer;
use crate::event::{Event, KeyEvent};
use crate::layout::LayoutEngine;
use crate::render::{Buffer, Terminal};
use crate::style::{StyleSheet, TransitionManager};
use crate::widget::View;
use std::io::stdout;
use std::time::{Duration, Instant};
#[cfg(feature = "hot-reload")]
use crate::style::parse_css;
#[cfg(feature = "hot-reload")]
use std::fs;
#[cfg(feature = "hot-reload")]
use std::path::PathBuf;
pub type TickHandler<V> = Box<dyn FnMut(&mut V, Duration) -> bool>;
#[inline]
fn is_quit_key(key: &KeyEvent) -> bool {
key.is_ctrl_c()
}
pub struct App {
dom: DomRenderer,
layout: LayoutEngine,
buffers: [Buffer; 2],
current_buffer: usize,
running: bool,
transitions: TransitionManager,
last_tick: Instant,
pub(crate) mouse_capture: bool,
needs_force_redraw: bool,
needs_layout_rebuild: bool,
needs_dom_rebuild: bool,
plugins: crate::plugin::PluginRegistry,
devtools_enabled: bool,
#[cfg(feature = "hot-reload")]
hot_reload: Option<HotReload>,
#[cfg(feature = "hot-reload")]
style_paths: Vec<PathBuf>,
}
impl App {
#[allow(dead_code)] pub(crate) fn new_with_plugins(
initial_size: (u16, u16),
stylesheet: StyleSheet,
mouse_capture: bool,
plugins: crate::plugin::PluginRegistry,
devtools_enabled: bool,
) -> Self {
let (width, height) = initial_size;
Self {
dom: DomRenderer::with_stylesheet(stylesheet),
layout: LayoutEngine::new(),
buffers: [Buffer::new(width, height), Buffer::new(width, height)],
current_buffer: 0,
running: false,
transitions: TransitionManager::new(),
last_tick: Instant::now(),
mouse_capture,
needs_force_redraw: true, needs_layout_rebuild: true, needs_dom_rebuild: true, plugins,
devtools_enabled,
#[cfg(feature = "hot-reload")]
hot_reload: None,
#[cfg(feature = "hot-reload")]
style_paths: Vec::new(),
}
}
#[cfg(feature = "hot-reload")]
pub(crate) fn new_with_hot_reload(
initial_size: (u16, u16),
stylesheet: StyleSheet,
mouse_capture: bool,
plugins: crate::plugin::PluginRegistry,
devtools_enabled: bool,
hot_reload: Option<HotReload>,
style_paths: Vec<PathBuf>,
) -> Self {
let (width, height) = initial_size;
Self {
dom: DomRenderer::with_stylesheet(stylesheet),
layout: LayoutEngine::new(),
buffers: [Buffer::new(width, height), Buffer::new(width, height)],
current_buffer: 0,
running: false,
transitions: TransitionManager::new(),
last_tick: Instant::now(),
mouse_capture,
needs_force_redraw: true,
needs_layout_rebuild: true,
needs_dom_rebuild: true,
plugins,
devtools_enabled,
hot_reload,
style_paths,
}
}
pub fn builder() -> AppBuilder {
AppBuilder::new()
}
pub fn plugins(&self) -> &crate::plugin::PluginRegistry {
&self.plugins
}
pub fn plugins_mut(&mut self) -> &mut crate::plugin::PluginRegistry {
&mut self.plugins
}
pub fn run<V, H>(&mut self, mut view: V, mut handler: H) -> crate::Result<()>
where
V: View,
H: FnMut(&Event, &mut V, &mut Self) -> bool,
{
use crate::event::EventReader;
let mut terminal = Terminal::new(stdout())?;
terminal.init_with_mouse(self.mouse_capture)?;
let (width, height) = terminal.size();
self.plugins.update_terminal_size(width, height);
if let Err(e) = self.plugins.mount() {
crate::log_warn!("Plugin mount failed: {}", e);
}
self.running = true;
self.last_tick = Instant::now();
self.dom.build(&view);
self.draw(&view, &mut terminal, true)?;
let reader = EventReader::new(FRAME_DURATION_60FPS);
while self.running {
#[cfg(feature = "hot-reload")]
{
if let Some(should_reload) = self.check_hot_reload() {
if should_reload {
self.needs_force_redraw = true;
self.draw(&view, &mut terminal, true)?;
}
}
}
let event = reader.read()?;
let should_draw = self.handle_event(event, &mut view, &mut handler);
if should_draw {
self.draw(&view, &mut terminal, false)?;
}
}
if let Err(e) = self.plugins.unmount() {
crate::log_warn!("Plugin unmount failed: {}", e);
}
terminal.restore()?;
Ok(())
}
pub fn run_with_handler<V, H>(&mut self, view: V, mut handler: H) -> crate::Result<()>
where
V: View,
H: FnMut(&KeyEvent, &mut V) -> bool,
{
self.run(view, move |event, view, _app| match event {
Event::Key(key_event) => handler(key_event, view),
_ => false,
})
}
fn handle_event<V, H>(&mut self, event: Event, view: &mut V, handler: &mut H) -> bool
where
V: View,
H: FnMut(&Event, &mut V, &mut Self) -> bool,
{
let mut should_draw = handler(&event, view, self);
match event {
Event::Key(key) if is_quit_key(&key) => {
self.quit();
return false;
}
Event::Resize(w, h) => {
self.buffers[0].resize(w, h);
self.buffers[1].resize(w, h);
self.plugins.update_terminal_size(w, h);
self.needs_force_redraw = true;
self.needs_layout_rebuild = true; should_draw = true;
}
Event::Tick => {
let now = Instant::now();
let delta = now.duration_since(self.last_tick);
self.last_tick = now;
self.transitions.update(delta);
self.transitions.update_nodes(delta);
if let Err(e) = self.plugins.tick(delta) {
crate::log_warn!("Plugin tick failed: {}", e);
}
if self.transitions.has_active() {
should_draw = true;
}
}
_ => {}
}
should_draw || self.needs_force_redraw
}
#[cfg(feature = "hot-reload")]
fn check_hot_reload(&mut self) -> Option<bool> {
let hr = self.hot_reload.as_mut()?;
hr.poll().map(|event| self.handle_hot_reload_event(event))
}
#[cfg(feature = "hot-reload")]
fn handle_hot_reload_event(&mut self, event: HotReloadEvent) -> bool {
match event {
HotReloadEvent::StylesheetChanged(ref path) => {
self.log_and_reload(path, "stylesheet changed");
true
}
HotReloadEvent::FileCreated(ref path) => {
crate::log_debug!("Hot reload: file created {:?}", path);
if self.style_paths.contains(path) {
self.reload_stylesheet(path);
true
} else {
false
}
}
HotReloadEvent::FileDeleted(ref path) => {
crate::log_debug!("Hot reload: file deleted {:?}", path);
false
}
HotReloadEvent::Error(ref e) => {
crate::log_warn!("Hot reload error: {}", e);
false
}
}
}
#[cfg(feature = "hot-reload")]
fn log_and_reload(&mut self, path: &PathBuf, action: &str) {
crate::log_debug!("Hot reload: {action} {:?}", path);
self.reload_stylesheet(path);
}
#[cfg(feature = "hot-reload")]
fn reload_stylesheet(&mut self, path: &PathBuf) {
let content = match fs::read_to_string(path) {
Ok(c) => c,
Err(e) => {
crate::log_warn!("Hot reload: failed to read {:?}: {}", path, e);
return;
}
};
self.parse_and_merge_stylesheet(path, &content);
}
#[cfg(feature = "hot-reload")]
fn parse_and_merge_stylesheet(&mut self, path: &PathBuf, content: &str) {
match parse_css(content) {
Ok(sheet) => {
self.dom.stylesheet_mut().merge(sheet);
self.needs_force_redraw = true;
crate::log_debug!("Hot reload: reloaded {:?}", path);
}
Err(e) => {
crate::log_warn!("Hot reload: failed to parse CSS from {:?}: {}", path, e);
}
}
}
fn draw<V: View, W: std::io::Write>(
&mut self,
view: &V,
terminal: &mut Terminal<W>,
force_redraw: bool,
) -> crate::Result<()> {
let root_dom_id = self.update_dom_and_get_root(view)?;
let (width, height) = self.get_buffer_size();
self.update_layout_tree(root_dom_id, width, height);
let dirty_rects = self.collect_dirty_regions(width, height, force_redraw);
let new_buffer_idx = self.swap_buffers();
self.render_to_buffer(view, new_buffer_idx, &dirty_rects);
self.draw_to_terminal(terminal, new_buffer_idx, force_redraw, &dirty_rects)?;
self.dom.tree_mut().clear_dirty_flags();
Ok(())
}
fn update_dom_and_get_root<V: View>(&mut self, view: &V) -> crate::Result<crate::dom::DomId> {
if self.needs_dom_rebuild {
self.dom.build(view);
self.needs_dom_rebuild = false;
self.needs_layout_rebuild = true;
}
self.dom.compute_styles_with_inheritance();
self.dom.tree().root_id().ok_or_else(|| {
crate::Error::Other(anyhow::anyhow!(
"Root DOM node not found. DOM may not have been built."
))
})
}
fn get_buffer_size(&self) -> (u16, u16) {
(
self.buffers[self.current_buffer].width(),
self.buffers[self.current_buffer].height(),
)
}
fn update_layout_tree(&mut self, root_dom_id: crate::dom::DomId, width: u16, height: u16) {
if self.needs_layout_rebuild {
self.layout.clear();
self.build_layout_tree(root_dom_id);
self.needs_layout_rebuild = false;
} else {
self.update_layout_tree_incremental(root_dom_id);
}
if let Err(e) = self.layout.compute(root_dom_id, width, height) {
crate::log_warn!("Layout compute failed for {:?}: {}", root_dom_id, e);
}
}
fn collect_dirty_regions(
&mut self,
width: u16,
height: u16,
force_redraw: bool,
) -> Vec<crate::layout::Rect> {
let dirty_dom_ids = self.dom.tree_mut().get_dirty_nodes();
let mut dirty_rects = Vec::new();
for dom_id in &dirty_dom_ids {
if let Ok(rect) = self.layout.layout(*dom_id) {
dirty_rects.push(rect);
}
}
if !dirty_rects.is_empty() {
dirty_rects = crate::layout::merge_rects(&dirty_rects);
}
if dirty_rects.is_empty() {
dirty_rects = self.collect_transition_rects(width, height);
}
if dirty_rects.is_empty() && (self.needs_force_redraw || force_redraw) {
let full_screen_rect = crate::layout::Rect::new(0, 0, width, height);
dirty_rects.push(full_screen_rect);
self.needs_force_redraw = false;
}
dirty_rects
}
fn collect_transition_rects(&mut self, width: u16, height: u16) -> Vec<crate::layout::Rect> {
let mut dirty_rects = Vec::new();
if self.transitions.has_active() {
let transition_rects: Vec<crate::layout::Rect> = self
.transitions
.active_node_ids()
.filter_map(|element_id| {
self.dom
.get_by_id(element_id)
.map(|node| node.id)
.and_then(|dom_id| self.layout.layout(dom_id).ok())
})
.collect();
if transition_rects.is_empty() {
if self.transitions.active_properties().next().is_some() {
let full_screen_rect = crate::layout::Rect::new(0, 0, width, height);
dirty_rects.push(full_screen_rect);
}
} else {
dirty_rects.extend(transition_rects);
}
}
dirty_rects
}
fn swap_buffers(&mut self) -> usize {
1 - self.current_buffer
}
fn render_to_buffer<V: View>(
&mut self,
view: &V,
buffer_idx: usize,
dirty_rects: &[crate::layout::Rect],
) {
let (buf_0, buf_1) = self.buffers.split_at_mut(1);
let (new_buffer, old_buffer) = if buffer_idx == 0 {
(&mut buf_0[0], &buf_1[0])
} else {
(&mut buf_1[0], &buf_0[0])
};
if dirty_rects.is_empty() {
new_buffer.copy_from(old_buffer);
return;
}
let area = crate::layout::Rect::new(0, 0, new_buffer.width(), new_buffer.height());
let full_screen = dirty_rects.len() == 1
&& dirty_rects[0].x == 0
&& dirty_rects[0].y == 0
&& dirty_rects[0].width == new_buffer.width()
&& dirty_rects[0].height == new_buffer.height();
if full_screen {
new_buffer.clear();
} else {
new_buffer.copy_from(old_buffer);
new_buffer.clear_regions(dirty_rects);
}
self.dom.render(view, new_buffer, area);
}
fn draw_to_terminal<W: std::io::Write>(
&mut self,
terminal: &mut Terminal<W>,
buffer_idx: usize,
force_redraw: bool,
dirty_rects: &[crate::layout::Rect],
) -> crate::Result<()> {
let old_buffer = &self.buffers[self.current_buffer];
let new_buffer = &self.buffers[buffer_idx];
if force_redraw || self.needs_force_redraw {
terminal.force_redraw(new_buffer)?;
self.needs_force_redraw = false;
} else {
let changes = crate::render::diff(old_buffer, new_buffer, dirty_rects);
terminal.draw_changes(changes, new_buffer)?;
}
self.current_buffer = buffer_idx;
Ok(())
}
fn build_layout_tree(&mut self, dom_id: crate::dom::DomId) {
let children = self
.dom
.tree()
.get(dom_id)
.map(|node| node.children.clone())
.unwrap_or_default();
let style = match self.dom.style_for_with_inheritance(dom_id) {
Some(s) => s,
None => {
crate::log_warn!("Style not found for DOM node {:?}, using default", dom_id);
crate::style::Style::default()
}
};
if let Err(e) = self
.layout
.create_node_with_children(dom_id, &style, &children)
{
crate::log_warn!("Layout node creation failed for {:?}: {}", dom_id, e);
}
for child_dom_id in children {
self.build_layout_tree(child_dom_id);
}
}
fn update_layout_tree_incremental(&mut self, dom_id: crate::dom::DomId) {
let node_exists = self.layout.layout(dom_id).is_ok();
if !node_exists {
self.needs_layout_rebuild = true;
return;
}
let is_dirty = self
.dom
.tree()
.get(dom_id)
.map(|n| n.state.dirty)
.unwrap_or(false);
if is_dirty {
if let Some(style) = self.dom.style_for_with_inheritance(dom_id) {
if let Err(e) = self.layout.update_style(dom_id, &style) {
crate::log_warn!("Layout style update failed for {:?}: {}", dom_id, e);
}
}
}
let children = self
.dom
.tree()
.get(dom_id)
.map(|n| n.children.clone())
.unwrap_or_default();
for child_id in children {
self.update_layout_tree_incremental(child_id);
}
}
pub fn quit(&mut self) {
self.running = false;
}
pub fn request_redraw(&mut self) {
self.needs_force_redraw = true;
}
pub fn request_layout_rebuild(&mut self) {
self.needs_layout_rebuild = true;
}
pub fn request_dom_rebuild(&mut self) {
self.needs_dom_rebuild = true;
self.needs_layout_rebuild = true; }
pub fn is_running(&self) -> bool {
self.running
}
pub fn is_devtools_enabled(&self) -> bool {
self.devtools_enabled
}
pub fn enable_devtools(&mut self) {
self.devtools_enabled = true;
}
pub fn disable_devtools(&mut self) {
self.devtools_enabled = false;
}
pub fn toggle_devtools(&mut self) -> bool {
self.devtools_enabled = !self.devtools_enabled;
self.devtools_enabled
}
pub fn dom_renderer(&mut self) -> &mut DomRenderer {
&mut self.dom
}
pub fn transitions(&self) -> &TransitionManager {
&self.transitions
}
pub fn transitions_mut(&mut self) -> &mut TransitionManager {
&mut self.transitions
}
pub fn start_transition(
&mut self,
property: &str,
from: f32,
to: f32,
transition: &crate::style::Transition,
) {
self.transitions.start(property, from, to, transition);
}
pub fn transition_value(&self, property: &str) -> Option<f32> {
self.transitions.get(property)
}
pub fn has_active_transitions(&self) -> bool {
self.transitions.has_active()
}
}
impl Default for App {
fn default() -> Self {
App::builder().build()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::Key;
struct TestView;
impl View for TestView {
fn render(&self, _ctx: &mut crate::widget::RenderContext) {}
fn meta(&self) -> crate::dom::WidgetMeta {
crate::dom::WidgetMeta::new("TestView")
}
}
fn create_test_app() -> App {
App::new_with_plugins(
(80, 24),
StyleSheet::new(),
false,
crate::plugin::PluginRegistry::new(),
false, )
}
#[test]
fn test_app_builder_and_new() {
let app = App::builder().css(".test { color: red; }").build();
assert!(!app.is_running());
}
#[test]
fn test_app_default() {
let app = App::default();
assert!(!app.is_running());
}
#[test]
fn test_app_quit() {
let mut app = create_test_app();
app.running = true;
assert!(app.is_running());
app.quit();
assert!(!app.is_running());
}
#[test]
fn test_is_quit_key() {
let q_key = KeyEvent::new(Key::Char('q'));
let ctrl_c = KeyEvent::ctrl(Key::Char('c'));
let other_key = KeyEvent::new(Key::Char('a'));
assert!(!is_quit_key(&q_key)); assert!(is_quit_key(&ctrl_c));
assert!(!is_quit_key(&other_key));
}
#[test]
fn test_is_quit_key_other_keys() {
let escape = KeyEvent::new(Key::Escape);
let enter = KeyEvent::new(Key::Enter);
let ctrl_d = KeyEvent::ctrl(Key::Char('d'));
assert!(!is_quit_key(&escape));
assert!(!is_quit_key(&enter));
assert!(!is_quit_key(&ctrl_d));
}
#[test]
fn test_request_redraw() {
let mut app = create_test_app();
app.needs_force_redraw = false;
app.request_redraw();
assert!(app.needs_force_redraw);
}
#[test]
fn test_request_layout_rebuild() {
let mut app = create_test_app();
app.needs_layout_rebuild = false;
app.request_layout_rebuild();
assert!(app.needs_layout_rebuild);
}
#[test]
fn test_request_dom_rebuild() {
let mut app = create_test_app();
app.needs_dom_rebuild = false;
app.needs_layout_rebuild = false;
app.request_dom_rebuild();
assert!(app.needs_dom_rebuild);
assert!(app.needs_layout_rebuild); }
#[test]
fn test_plugins_access() {
let mut app = create_test_app();
let _ = app.plugins();
let _ = app.plugins_mut();
}
#[test]
fn test_dom_renderer_access() {
let mut app = create_test_app();
let _ = app.dom_renderer();
}
#[test]
fn test_transitions_access() {
let mut app = create_test_app();
assert!(!app.has_active_transitions());
let _ = app.transitions();
let _ = app.transitions_mut();
}
#[test]
fn test_transition_value_none() {
let app = create_test_app();
assert!(app.transition_value("opacity").is_none());
}
#[test]
fn test_start_transition() {
let mut app = create_test_app();
let transition = crate::style::Transition {
property: "opacity".to_string(),
duration: Duration::from_millis(300),
delay: Duration::ZERO,
easing: crate::style::Easing::Linear,
};
app.start_transition("opacity", 0.0, 1.0, &transition);
assert!(app.has_active_transitions());
let value = app.transition_value("opacity");
assert!(value.is_some());
}
#[test]
fn test_new_with_plugins_initial_state() {
let app = App::new_with_plugins(
(100, 50),
StyleSheet::new(),
true,
crate::plugin::PluginRegistry::new(),
false, );
assert!(!app.running);
assert!(app.needs_force_redraw);
assert!(app.needs_layout_rebuild);
assert!(app.needs_dom_rebuild);
assert!(app.mouse_capture);
assert!(!app.devtools_enabled);
}
#[test]
fn test_buffer_initialization() {
let app = App::new_with_plugins(
(120, 40),
StyleSheet::new(),
false,
crate::plugin::PluginRegistry::new(),
false, );
assert_eq!(app.buffers[0].width(), 120);
assert_eq!(app.buffers[0].height(), 40);
assert_eq!(app.buffers[1].width(), 120);
assert_eq!(app.buffers[1].height(), 40);
assert_eq!(app.current_buffer, 0);
}
#[test]
fn test_devtools_methods() {
let mut app = create_test_app();
assert!(!app.is_devtools_enabled());
app.enable_devtools();
assert!(app.is_devtools_enabled());
app.disable_devtools();
assert!(!app.is_devtools_enabled());
let result = app.toggle_devtools();
assert!(result);
assert!(app.is_devtools_enabled());
let result = app.toggle_devtools();
assert!(!result);
assert!(!app.is_devtools_enabled());
}
#[test]
fn test_handle_event_quit_q() {
let mut app = create_test_app();
app.running = true;
let mut view = TestView;
let mut handler = |_: &Event, _: &mut TestView, _: &mut App| false;
let event = Event::Key(KeyEvent::new(Key::Char('q')));
let _ = app.handle_event(event, &mut view, &mut handler);
assert!(app.is_running());
}
#[test]
fn test_handle_event_quit_ctrl_c() {
let mut app = create_test_app();
app.running = true;
let mut view = TestView;
let mut handler = |_: &Event, _: &mut TestView, _: &mut App| false;
let event = Event::Key(KeyEvent::ctrl(Key::Char('c')));
let _ = app.handle_event(event, &mut view, &mut handler);
assert!(!app.is_running());
}
#[test]
fn test_handle_event_resize() {
let mut app = create_test_app();
app.needs_force_redraw = false;
app.needs_layout_rebuild = false;
let mut view = TestView;
let mut handler = |_: &Event, _: &mut TestView, _: &mut App| false;
let event = Event::Resize(100, 50);
let should_draw = app.handle_event(event, &mut view, &mut handler);
assert!(should_draw);
assert!(app.needs_force_redraw);
assert!(app.needs_layout_rebuild);
assert_eq!(app.buffers[0].width(), 100);
assert_eq!(app.buffers[0].height(), 50);
}
#[test]
fn test_handle_event_tick() {
let mut app = create_test_app();
let mut view = TestView;
let mut handler = |_: &Event, _: &mut TestView, _: &mut App| false;
let event = Event::Tick;
let _ = app.handle_event(event, &mut view, &mut handler);
}
#[test]
fn test_handle_event_handler_returns_true() {
let mut app = create_test_app();
app.needs_force_redraw = false;
let mut view = TestView;
let mut handler = |_: &Event, _: &mut TestView, _: &mut App| true;
let event = Event::Key(KeyEvent::new(Key::Char('a')));
let should_draw = app.handle_event(event, &mut view, &mut handler);
assert!(should_draw);
}
#[test]
fn test_handle_event_handler_returns_false() {
let mut app = create_test_app();
app.needs_force_redraw = false;
let mut view = TestView;
let mut handler = |_: &Event, _: &mut TestView, _: &mut App| false;
let event = Event::Key(KeyEvent::new(Key::Char('a')));
let should_draw = app.handle_event(event, &mut view, &mut handler);
assert!(!should_draw);
}
}