pub mod context;
pub mod handle;
pub mod panic_hook;
mod autoscroll;
mod event_loop;
mod frame;
mod keyboard_defaults;
mod prelude;
mod redraw;
mod stylesheets;
#[cfg(test)]
mod frame_work_tests;
#[cfg(test)]
mod idle_tests;
#[cfg(test)]
mod interaction_chain_tests;
#[cfg(test)]
mod off_event_paint_tests;
#[cfg(test)]
mod route_redraw_tests;
#[cfg(test)]
mod scroll_repaint_tests;
#[cfg(test)]
mod setter_mutation_tests;
#[cfg(test)]
mod sibling_mark_tests;
#[cfg(test)]
mod tests;
use std::io::{self, Stdout};
use std::sync::Arc;
use std::time::Duration;
use crossterm::event::Event as CtEvent;
use redraw::Redraw;
use std::rc::Rc;
use crate::render::backend::Backend;
use crate::render::backend_crossterm::{CrosstermBackend, enter_tui_mode};
use crate::render::{Terminal, TerminalGuard};
use crate::runtime::router::{RouteOutcome, Router};
use crate::runtime::selection::clipboard::{Clipboard, SystemClipboard};
use crate::runtime::url_opener::{SystemUrlOpener, UrlOpener};
use crate::style::{DirtyTracker, Stylesheet};
use crate::{TuiDispatchExt, TuiDom, TuiEvent};
pub use context::{AppContext, ControlFlow};
pub use handle::AppHandle;
pub use stylesheets::StylesheetId;
use handle::AppShared;
type TickCallback = Box<dyn FnMut(&mut AppContext<'_>) -> ControlFlow + 'static>;
pub struct App<B: Backend = CrosstermBackend<Stdout>> {
pub(super) dom: TuiDom,
pub(super) stylesheets: Vec<(StylesheetId, Stylesheet)>,
prelude: prelude::FramePrelude,
stylesheet_ids: stylesheets::StylesheetIdAllocator,
pub(super) terminal: Terminal<B>,
pub(super) tracker: DirtyTracker,
pub(super) router: Router,
tick_rate: Duration,
animation_frame_ms: u32,
on_tick: Option<TickCallback>,
pub(crate) scheduler: crate::runtime::timers::SharedScheduler,
pub(crate) animations: crate::runtime::animation::AnimationRegistry,
autoscroll: autoscroll::AutoscrollSession,
pub(super) redraw: Redraw,
#[cfg(test)]
pub(super) frame_stats: redraw::FrameStats,
pub(super) should_quit: bool,
guard: Option<TerminalGuard>,
shared: Arc<AppShared>,
pub(super) clipboard: Box<dyn Clipboard>,
url_opener: crate::runtime::builtins::a_href::SharedOpener,
}
impl App<CrosstermBackend<Stdout>> {
pub fn new(dom: TuiDom, stylesheet: Stylesheet) -> io::Result<Self> {
panic_hook::install();
let mut stdout = io::stdout();
enter_tui_mode(&mut stdout)?;
let guard = TerminalGuard::new();
let backend = CrosstermBackend::new(io::stdout());
let terminal = Terminal::new(backend)?;
let mut app = Self::build(dom, stylesheet, terminal)?;
app.guard = Some(guard);
app.prelude
.caret_blink
.set_period(Some(crate::runtime::caret_blink::DEFAULT_CARET_BLINK));
Ok(app)
}
}
impl<B: Backend> App<B> {
pub fn with_backend(
dom: TuiDom,
stylesheet: Stylesheet,
terminal: Terminal<B>,
) -> io::Result<Self> {
Self::build(dom, stylesheet, terminal)
}
fn build(mut dom: TuiDom, stylesheet: Stylesheet, terminal: Terminal<B>) -> io::Result<Self> {
let tracker = DirtyTracker::install(&mut dom);
crate::cssom::install_default_observers(&mut dom);
crate::cssom::seed_inline_styles(&mut dom);
let default_opener: Rc<dyn UrlOpener> = Rc::new(SystemUrlOpener);
let url_opener: crate::runtime::builtins::a_href::SharedOpener =
Rc::new(std::cell::RefCell::new(default_opener));
crate::runtime::builtins::a_href::install(&mut dom, url_opener.clone());
crate::runtime::builtins::button::install(&mut dom);
crate::runtime::builtins::label::install(&mut dom);
crate::runtime::builtins::details::install(&mut dom);
crate::runtime::builtins::toggle::install(&mut dom);
crate::runtime::builtins::number::install(&mut dom);
crate::runtime::builtins::form::install(&mut dom);
crate::runtime::builtins::dialog::install(&mut dom);
crate::runtime::builtins::select::install(&mut dom);
crate::runtime::builtins::range::install(&mut dom);
crate::runtime::builtins::tree::install(&mut dom);
crate::runtime::builtins::validation::install(&mut dom);
crate::runtime::builtins::input::seed_all(&mut dom);
crate::runtime::builtins::range::attach_all(&mut dom);
crate::runtime::builtins::select::seed_all(&mut dom);
let prelude = prelude::FramePrelude::install(&mut dom);
crate::runtime::builtins::table::size_all_tables(&mut dom);
crate::runtime::implicit_events::install(&mut dom);
crate::runtime::autofocus::focus_first_autofocus(&mut dom);
let mut stylesheet_ids = stylesheets::StylesheetIdAllocator::default();
let app = Self {
dom,
stylesheets: vec![(stylesheet_ids.allocate(), stylesheet)],
prelude,
stylesheet_ids,
terminal,
tracker,
router: Router::new(),
tick_rate: Duration::from_millis(50),
animation_frame_ms: 16,
on_tick: None,
scheduler: std::rc::Rc::new(std::cell::RefCell::new(
crate::runtime::timers::Scheduler::new(std::time::Instant::now()),
)),
animations: crate::runtime::animation::AnimationRegistry::new(),
autoscroll: autoscroll::AutoscrollSession::default(),
redraw: Redraw::Cascade,
#[cfg(test)]
frame_stats: Default::default(),
should_quit: false,
guard: None,
shared: AppShared::new(),
clipboard: Box::new(SystemClipboard::new()),
url_opener,
};
app.prelude
.sync_sibling_combinators(&app.tracker, &app.stylesheets);
Ok(app)
}
pub fn set_animation_frame_rate(&mut self, fps: u16) {
let fps = fps.clamp(1, 120);
self.animation_frame_ms = (1000 / fps as u32).max(1);
}
pub fn with_caret_blink(mut self, period: Option<Duration>) -> Self {
self.prelude.caret_blink.set_period(period);
self
}
#[cfg(test)]
pub(crate) fn caret_blink_deadline(&self) -> Option<std::time::Instant> {
self.prelude.caret_blink.next_deadline()
}
#[cfg(test)]
pub(crate) fn smooth_scroll_deadline(&self) -> Option<std::time::Instant> {
self.prelude.smooth_scroll_next
}
pub fn with_clipboard(mut self, clipboard: Box<dyn Clipboard>) -> Self {
self.clipboard = clipboard;
self
}
pub fn with_url_opener(self, opener: Rc<dyn UrlOpener>) -> Self {
*self.url_opener.borrow_mut() = opener;
self
}
pub fn handle(&self) -> AppHandle {
AppHandle::from_shared(Arc::clone(&self.shared))
}
pub fn dom_mut(&mut self) -> &mut TuiDom {
self.prelude.touched = true;
&mut self.dom
}
pub fn dom(&self) -> &TuiDom {
&self.dom
}
pub fn terminal(&self) -> &Terminal<B> {
&self.terminal
}
pub fn terminal_mut(&mut self) -> &mut Terminal<B> {
&mut self.terminal
}
pub fn tick_rate(mut self, d: Duration) -> Self {
self.tick_rate = d;
self
}
pub fn on_tick<F>(mut self, f: F) -> Self
where
F: FnMut(&mut AppContext<'_>) -> ControlFlow + 'static,
{
self.on_tick = Some(Box::new(f));
self
}
fn note_route(&mut self, outcome: RouteOutcome) {
self.redraw
.note_if(outcome.redraw_requested, Redraw::Layout);
self.redraw
.note_if(outcome.cascade_requested, Redraw::Cascade);
self.should_quit |= outcome.quit_requested;
}
pub fn handle_event(&mut self, event: CtEvent) {
let _scheduler_guard = crate::runtime::timers::SchedulerGuard::install(&self.scheduler);
crate::rdom_trace!("App::handle_event RAW: {event:?}");
self.prelude.selectedness.flush(&mut self.dom);
self.prelude.touched = true;
match &event {
CtEvent::Key(key) => {
self.prelude.caret_blink.note_input();
self.handle_key_event(*key)
}
CtEvent::Mouse(m) => {
if matches!(m.kind, crossterm::event::MouseEventKind::Down(_)) {
self.prelude.caret_blink.note_input();
}
let (col, row) = (m.column, m.row);
let focused_before = self.dom.focused();
let outcome = self.router.route(&mut self.dom, event);
crate::runtime::focus::visible::note_pointer_focus(&mut self.dom, focused_before);
self.note_route(outcome);
self.note_autoscroll(col, row);
}
CtEvent::Resize(_, _) => {
let root = self.dom.root();
let mut tui = TuiEvent::new("resize");
tui.event.cancelable = false;
self.dom
.dispatch_tui_event(root, &mut tui)
.expect("the document root is never dropped, and `tui` is fresh");
self.redraw.note(Redraw::Cascade);
let mut stdout = std::io::stdout();
if let Err(e) = crate::render::backend_crossterm::enter_mouse_capture(&mut stdout) {
crate::rdom_trace!("Resize: failed to re-arm mouse capture: {e}");
} else {
crate::rdom_trace!("Resize: re-armed mouse capture");
}
}
CtEvent::FocusGained => {
self.prelude.caret_blink.set_terminal_focused(true);
crate::rdom_trace!(
"App::handle_event: FocusGained (capture={:?}, hovered={:?})",
self.dom.pointer_capture(),
self.dom.hovered()
);
}
CtEvent::FocusLost => {
self.prelude.caret_blink.set_terminal_focused(false);
crate::rdom_trace!(
"App::handle_event: FocusLost (capture={:?}, hovered={:?})",
self.dom.pointer_capture(),
self.dom.hovered()
);
}
_ => {
}
}
}
#[cfg(test)]
pub(crate) fn dirty_roots_snapshot(&self) -> Vec<rdom_core::NodeId> {
self.tracker.roots_snapshot()
}
#[cfg(test)]
pub(crate) fn needs_redraw(&self) -> bool {
self.redraw != Redraw::Clean || self.tracker.has_pending()
}
#[cfg(test)]
pub(crate) fn take_frame_stats(&mut self) -> redraw::FrameStats {
std::mem::take(&mut self.frame_stats)
}
#[cfg(test)]
pub(crate) fn should_quit(&self) -> bool {
self.should_quit
}
#[cfg(test)]
pub(crate) fn animations_mut_for_test(
&mut self,
) -> &mut crate::runtime::animation::AnimationRegistry {
&mut self.animations
}
#[cfg(test)]
pub(crate) fn dispatch_animation_events_for_test(&mut self) {
self.dispatch_animation_events();
}
}