incular-runtime 0.1.0

Application runtime and scheduling foundations for Incular
Documentation

incular-runtime

Architecture and support

Contract Status
Ownership Application lifecycle, scheduling, reactive dispatch, tasks and frame coordination.
API class application; re-exports and path overrides follow the architecture contract.
Support Available; shared reactive engine and native request lifecycle infrastructure.

Application lifecycle and scheduling foundations for Incular, including frame scheduling, widget-tree updates, input dispatch, animation ticking, resource coordination, and rendering orchestration.

incular-runtime

Owns the controlled frame queue, declarative Application roots, callback lifetimes, and Signal<T> dependency scheduler. schedule_update coalesces direct updates; signal writes queue only Elements whose registered builders read that signal. run_frame processes build updates, incremental layout, then cached paint generation. It has no platform or GPU dependency.

It then performs a COMPOSITE phase that writes retained transform properties and flattens the layer tree. run_frame_at accepts a monotonic Instant for deterministic animation tests; ordinary run_frame supplies Instant::now(). Idle applications request no recurring frames, while an active compositor animation requests the next one.

Production runtime foundation

Tokio is Incular's application async runtime. Each Application/Runtime owns one Tokio multi-thread runtime (time and I/O enabled) and exposes a cloned TokioHandle for advanced libraries. Tokio owns futures, Tokio timers, async I/O, synchronization, and spawn_blocking; Incular deliberately does not implement another executor, timer wheel, or worker pool.

The Incular runtime still owns the UI thread, retained widget/render/layer trees, frame phases, environment/lifecycle state, and native windows. Tokio work returns Send data through a typed UI message bridge. The bridge wakes Winit only for newly queued UI-relevant work and coalesces bursts; the UI owner drains at most 128 messages per native turn. A wake alone never requests a frame—only an actual Signal/widget/compositor mutation does.

Use cx.spawn for a component-owned Tokio task and cx.spawn_into when a completion must update UI-local state. The future and its result are Send, but the completion callback runs solely on the UI thread and can therefore capture a single-threaded Signal. app.spawn is application-scoped; app.tokio_handle() is intentionally available for direct Tokio-native use and does not attach automatic component ownership.

TaskScope aborts normal Tokio tasks when an owner unmounts. A pending completion validates both the scope and its generational ElementId before it can execute, so a stale completion cannot mutate a reused element slot. A started blocking closure cannot safely be force-stopped; cancellation marks its result uninterested and the UI bridge discards it. Shutdown aborts tracked work and calls Tokio's bounded shutdown_timeout policy rather than waiting indefinitely for blocking jobs.

RuntimeEnvironment is the normalized logical environment snapshot: viewport, physical size, DPI, brightness, text scale, logical insets, locale/direction, motion, and input capability. BuildContext records typed field reads so an unrelated environment change does not rebuild the root just because, for example, it only reads text scale.

Multi-window runtime

An Application owns exactly one Tokio runtime and can own many retained windows. Every WindowId is an Incular (slot, generation) value; public APIs never expose Winit IDs, a native window pointer, or a GPU surface. Application::open_window mounts a static root and Application::open_window_with mounts a declarative root. A build may call cx.open_window, while a callback can retain cx.window_opener() and queue a new root later. Native creation is deferred to the desktop adapter's active event-loop callback.

Each window owns its WidgetTree, input and logical focus, environment, semantics tree, compositor work, presentation scheduling, and a window TaskScope. The hierarchy is application → window → component: application tasks survive a window close, while window/component completions are cancelled and generationally rejected once their root closes. WindowHandle operations are data-only messages, so a Tokio worker cannot mutate a native window.

WindowHandle distinguishes command transport from native execution. Ordinary setters return Result<(), WindowCommandEnqueueError>, which only reports whether the command crossed the runtime/UI bridge. Operations that require a native outcome return NativeOperationRequest, an awaitable request completed by the platform backend. Completion is keyed by both NativeRequestId and the generational WindowId; a close/reused slot cannot consume an old result, and each request resolves at most once. Dropping the request detaches result delivery without trying to undo an operation that may already have executed.

Application::platform_capabilities and per-window capability snapshots expose what the active backend/session can actually honor. Before a backend attaches, capabilities are Unknown; explicit headless/unsupported backends publish Unsupported rather than pretending an operation succeeded. Native failures are normalized to stable platform-error categories and the last per-window failure is visible through WindowDiagnostics without retaining native error objects.

Transient hosting has a separate resolved-state diagnostic because capability does not imply that one concrete popup was successfully presented. A WindowHandle exposes transient_presentations(), and the same snapshot appears in WindowDiagnostics. Each visible retained TransientSurfaceId reports its requested presentation, actual Native/Overlay result, and an optional stable fallback reason. Native adapters replace the complete snapshot after host negotiation; closing a window clears it before the generational window record is released, so an old handle cannot claim that a destroyed popup is still live.

Logical transient focus and dismissal stay inside the owning runtime even when presentation is native. Popup-local input is normalized back to owner-local coordinates before retained dispatch; pointer-down can dismiss unrelated transient chains, focus movement preserves the chain containing the new focus, Escape applies role policy, and native parent deactivation closes interactive transients without transferring semantic ownership to a second native window. If a focused popup subtree disappears, stale focus is cleared in the same frame before text-input synchronization.

Window control stays semantic and native-handle free. WindowHandle can ask the compositor/window manager to begin an interactive move or edge/corner resize and can change minimize/maximize/fullscreen, decorations, resizability, size limits, level, icon, and attention policy. Interactive move/resize returns NativeOperationRequest because the OS may reject the gesture. Other setters remain enqueue operations; callers can inspect capabilities before requesting them and WindowDiagnostics records any backend failure. requested_state and observed_state remain distinct so asynchronous window-manager behavior is not mistaken for immediate acknowledgement.

BuildContext::window_handle() gives a declarative window root access to its own command handle without exposing Winit. This is the intended boundary for custom title-bar controls and other window-local actions.

Display topology uses the same stale-identity discipline as windows. The native adapter publishes immutable DisplaySnapshot values keyed by generational DisplayId; a removed monitor disappears immediately, and a later monitor reusing the registry slot receives a newer generation. Application and every live WindowHandle can read the same thread-safe snapshot catalog, including primary/current-display association. WindowHandle::center_on_display and move_relative_to_display operate only when both capability and required geometry are present; they never synthesize Wayland global coordinates.

Window outer position/size are observed native state, not requested state. Moving a window between displays continues through the ordinary native metrics event path, so scale-factor/environment changes occur on the existing generational WindowId and renderer surface rather than replacing the window.

Desktop pointer routing preserves raw information without changing ordinary UI semantics. Listener and raw recognizers receive every representable mouse button plus the complete chord/changed-button metadata. Ordinary buttons, GestureDetector, text editing, scrollbars, custom window chrome, and TapRegion remain primary-button interactions; pressing or releasing a secondary button during a primary drag does not restart or finish that primary sequence. Native surface exit clears retained hover deterministically without blurring focused editors.

The runtime resolves one effective MouseCursor from the retained hit route and the desktop adapter applies it only when it changes. WindowHandle exposes result-bearing set_cursor_grab, set_cursor_visible, and set_cursor_position operations so enqueue success is never mistaken for native cursor-control success.

Signal subscriptions are retained per root. A shared signal rebuilds only the roots that read it; a signal read only by one window leaves every other window idle. Physical metrics, DPI, safe/view insets, and native activation are window fields. Locale, direction, and text scale may still originate from an application default but resolve through the owning BuildContext.

Derived values and asynchronous reactive work

Memo<T> is the opt-in graph path for an expensive or shared derived value. It is created like a Signal, without a BuildContext, and attaches lazily when its get method is first read by a builder. Its computation tracks signals and other memos dynamically; a dirty memo is evaluated once per queued runtime phase and only notifies consumers when its output changes. Cheap expressions should remain ordinary Rust expressions because a memo has bookkeeping and comparison overhead.

Effect is an owner-mounted side effect. Create a stable handle outside a rebuilding builder, call mount from that builder, and the initial run is queued for the reactive phase rather than executing during build. Mounting is idempotent, source reads are retracked after every run, and disposing the runtime detaches its source subscriptions.

Action<I, O, E> is an explicitly dispatched asynchronous operation. Reading its state binds it to the current owner; constructing it never starts work. dispatch marks the state as loading, runs through the existing Tokio/UI task bridge, and exposes success, application failure, or runtime cancellation. Dispatches are latest-wins, so a late completion cannot overwrite a newer request. The public facade exposes these names under incular::reactive so they do not collide with the rendering Effect or widget Action types.

Native accessibility

The runtime keeps the retained SemanticsTree independent of paint and native accessibility. A desktop runner owns one incular-accessibility::AccessKitProjection and one accesskit_winit::Adapter for each live WindowId; it calls Application::sync_accessibility only after native AccessKit activation. The tree revision makes unchanged/compositor-only frames a zero-update path. A native action is translated by the adapter to an owned Incular semantic action, then Application::dispatch_accessibility_action resolves it against that same window's retained tree on the UI thread. Generational semantic IDs reject stale actions before a reused element can be touched.

WindowDiagnostics::accessibility exposes value-free, per-window bridge health. Closing a window removes its native adapter/projection record only; other windows keep independent semantic and native trees. The runtime does not construct an additional async executor for accessibility.

The default LastWindowPolicy::ExitOnLastWindow exits when no visible retained window remains, so a hidden auxiliary window cannot accidentally keep a desktop application alive. Applications that intentionally provide background/headless service lifetime must opt into KeepRunning. Application::debug_dump and per-window diagnostics expose frame, input, metric, retained-tree, semantics, and stale-command state without leaking native details.

Opt-in state restoration

The default file-backed restoration store resolves its persistent location with directories::ProjectDirs through incular-platform. It uses the OS local application-data directory, never the cache directory; explicit file and in-memory stores remain available for embedding and tests.

Restoration persists small, declarative application values—not a live widget, element, render, layer, semantics, native-window, WGPU, gesture, callback, or Tokio-task graph. It is disabled unless the application uses Application::new_with_restoration or Application::new_restorable with a RestorationConfig. RestorationConfig::file_backed stores a versioned JSON snapshot under the stable application ID's platform state directory; tests and embedders can use InMemoryRestorationStore or supply a narrow custom RestorationStore instead.

Each value and scope uses an explicit RestorationKey segment. The runtime escapes segments deterministically for its internal path map, never derives identity from an element slot or child position, and rejects concurrently mounted duplicate restorable-window IDs. A builder obtains its scope from BuildContext::restoration_scope, then opts in with cx.restorable(key, default)/cx.restored_signal(key, default). Mutate the returned Restorable<T> rather than a plain Signal to capture updates; plain signals remain deliberately non-persistent.

Text editing, form fields, scroll/page controllers, and navigator snapshots can bind a scope explicitly. Text stores committed text plus a normalized selection, never IME composition; forms recompute validation; scroll restores only a logical offset after layout and clamps it; navigation stores only stable registered route IDs plus JSON arguments/state. Dialogs and arbitrary overlays are transient by default.

Snapshots carry distinct framework and application schema versions. A supplied migration runs before the first UI build observes values. Corrupt, future, or unmigratable files leave the original file available for debugging and start from defaults. Saves are coalesced for 250ms by default, serialize/write on Tokio's blocking pool, enforce an 8 MiB default limit, flush a sibling temporary file, sync it, then rename it. A failed replacement leaves the previous valid snapshot untouched; actual power-loss durability is the filesystem's guarantee, not a stronger framework claim. flush_restoration, app stopping, and platform suspension bypass the ordinary debounce; shutdown waits only a bounded 100ms for the normal save completion.

Restorable windows use WindowRestorationId, not WindowId. Register each auxiliary kind with register_restorable_window_factory, then call restore_restorable_windows before entering the native runner. Incular restores validated logical size/maximized/fullscreen state and skips unknown factories safely; it does not restore arbitrary window position or DPI. A user closing an auxiliary restorable window removes it from the next session, while application shutdown preserves active descriptors. Restoration storage is not encrypted: never place passwords, tokens, or other secrets in it.