telar 0.2.0

A modular Rust UI framework with its own template language, reactive signals and a self-contained renderer.
//! [`AppRuntime`]: the application as the *runner* drives it, which is not the same object an author writes.
//!
//! Everything an application's widgets touch — signals, layout nodes, overlays, the motion registry, the task queue — lives in a thread-local. In an ordinary build there is one copy of each and the runner can reach them directly. Under `cargo telar dev` the application is a dylib with its **own** copies, and every one of those has to be driven across the boundary instead: the host's `motion::tick` advances an empty registry, the host's `drain_tasks` runs callbacks nobody queued, the host's overlay dispatch consults a registry no widget ever registered in.
//!
//! So there are two runtimes, and the runner drives whichever it was handed: [`LocalApp`] for a tree in this process, [`crate::hot::HotApp`] for one behind a dylib. This trait is the difference between them.
//!
//! **It used to be part of [`App`].** Fourteen `#[doc(hidden)]` methods sat on the trait an application implements, every one of them carrying a paragraph explaining that only the dylib-backed app overrides it — so what an author saw was eighteen methods of which four were theirs, and `impl App for Box<A>` redelegated all eighteen by hand. None of it was reachable from application code and none of it belonged in its way.

use platform_core::{AppCtx, Event, RedrawWaker, WindowCommand, WindowConfig};
use renderer_core::Color;
use web_time::Instant;

use crate::app::App;
use crate::tree::{LocalTree, UiTree};

/// The application as the runner drives it: mount its tree, and reach the runtime that tree lives in.
///
/// An application does not implement this — it implements [`App`], and [`LocalApp`] adapts one. Implement it directly only to host a tree the runner cannot reach any other way, which in this repository means exactly one thing: a dylib.
///
/// Every method defaults to the answer for a tree in **this** process, so an implementor overrides only what its own runtime makes untrue.
pub trait AppRuntime: 'static {
    /// Mounts the UI and hands the runner the tree it will drive.
    ///
    /// Mounting belongs here rather than to the runner because a tree's segment effects must be created in the same reactive runtime as the signals its `view()` reads. Mount on the wrong side and no subscription is ever established: the tree renders once and then never again on its own.
    fn mount(&mut self) -> Box<dyn UiTree>;

    fn clear_color(&self) -> Option<Color>;

    fn window_config(&self) -> Option<WindowConfig>;

    /// Called once per frame before rendering.
    fn on_frame(&mut self, ctx: &mut AppCtx);

    /// Serializes the state this application wants to survive a dylib swap, before its library is unloaded.
    fn hot_snapshot(&self) -> Option<String> {
        None
    }

    /// Hands the snapshot the previous library produced to this one, before its tree mounts.
    fn hot_restore(&self, _blob: &str) {}

    /// Advances the motion engine this application's animations were registered in.
    fn motion_tick(&self, now: Instant) {
        motion_core::tick(now);
    }

    /// Whether any animation is still in flight.
    fn motion_has_active(&self) -> bool {
        motion_core::has_active()
    }

    /// Whether any region repaints itself outside Telar's knowledge, which is what keeps the frame generation moving while its draw commands do not change.
    fn motion_has_continuous(&self) -> bool {
        motion_core::has_continuous()
    }

    /// Re-lays out any dirtied layout root, so a reactive change is reflected before the frame is composed.
    fn relayout(&self) {
        ui_core::relayout_if_dirty();
    }

    /// Opens a reactive batch for the duration of event dispatch, paired with [`end_event_batch`](Self::end_event_batch).
    ///
    /// Without one, a signal written by an event handler flushes immediately and re-runs a segment's effect while its widget is still borrowed for `on_event`: that render is skipped and the segment silently loses its reactive subscriptions. Deferring the flush until every borrow is released keeps them.
    fn begin_event_batch(&self) {
        reactive_core::begin_batch();
    }

    fn end_event_batch(&self) {
        reactive_core::end_batch();
    }

    /// Offers a positioned event to the overlay layer before the widget tree sees it, reporting whether an overlay consumed it.
    fn dispatch_overlays(&self, event: &Event) -> bool {
        ui_core::dispatch_overlays(event) == ui_core::EventResult::Handled
    }

    /// Drains the window-management commands a UI closure queued during dispatch — a title bar's drag, a close button — so the runner can apply them to the OS window.
    fn drain_window_commands(&self) -> Vec<WindowCommand> {
        platform_core::take_window_commands()
    }

    /// Reports the OS light/dark preference into the theme runtime that `follow_system` reads.
    fn set_system_dark(&self, dark: bool) {
        theme_core::set_system_dark(dark);
    }

    /// Runs the completion callbacks of `spawn_task` work that finished since the last frame, on the UI thread.
    fn drain_tasks(&self) {
        reactive_core::drain_tasks();
    }

    /// Gives the reactive runtime the wake a finishing worker uses to run a frame. Without it a task delivers its result into a runtime whose waker slot is empty, and nothing runs until the next input event.
    fn install_task_waker(&self, waker: RedrawWaker) {
        reactive_core::set_task_waker(move || waker.wake());
    }
}

/// An [`App`] whose tree lives in this process — every application that is not a hot-reloaded dylib.
///
/// Holds nothing but the app: each of the runtime methods above is already correct for a tree on this side, so this overrides only what an application actually answers — the four of [`App`], plus the colour-scheme hook, which is the default's own work followed by the application's.
pub struct LocalApp<A: App>(pub A);

impl<A: App> AppRuntime for LocalApp<A> {
    fn mount(&mut self) -> Box<dyn UiTree> {
        Box::new(LocalTree::new(self.0.root()))
    }

    fn clear_color(&self) -> Option<Color> {
        self.0.clear_color()
    }

    fn window_config(&self) -> Option<WindowConfig> {
        self.0.window_config()
    }

    fn on_frame(&mut self, ctx: &mut AppCtx) {
        self.0.on_frame(ctx)
    }

    // The theme runtime this side reads is the default's job; the application is told after it, so an override sees a state that already agrees with the host it is fanning out from.
    fn set_system_dark(&self, dark: bool) {
        theme_core::set_system_dark(dark);
        self.0.on_color_scheme(dark);
    }
}