# incular-runtime
## Architecture and support
| Ownership | Application lifecycle, scheduling, reactive dispatch, tasks and frame coordination. |
| API class | application; re-exports and path overrides follow the [architecture contract](https://github.com/FabortLabs/incular/blob/master/system-design/ARCHITECTURE.md). |
| 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.