Platform Host Contract
This document describes the shared boundary contract of
moui/backend. For an overview, see Architecture.
Root backend defines platform-neutral protocols and DTOs: input capabilities,
coordinate policies, Event, WindowId, window requests, services, IME,
accessibility, and platform views. SurfaceMetrics lives in core. Per ADR 0018,
HostRuntimeDriver, RedrawScheduler, and HostWallClock live in
moui/runtime; backend consumes them as contracts but does not own
them.
backend/common implements the contract. It owns WindowRegistry,
WindowRequestQueue, the unique platform-window map, lifecycle/frame/input
state, text-input synchronization, and service adapters. It converts
already-neutral Close, Focus, resize/scale, redraw, and surface lifecycle facts
to Event, keeps surface lifecycle state, and normalizes logical
coordinates. It imports this contract but is not imported by it. Raw native
pointer, keyboard, IME, drag, and modifier decoding belongs to the concrete
platform backend; WeChat's direct Canvas2D callbacks remain a deliberately
non-window-hosted exception.
Root backend exposes the closure-based WindowRequests port;
backend/common implements it with WindowRequestQueue so runtime or platform code can enqueue
open, focus, close, resize, minimize, show, and primary-window requests without
embedding those requests in a platform backend. OpenWindow requests carry a
platform-neutral scene id and payload alongside title and metrics, so future
multi-window hosts have enough app-level identity to choose the runtime/content
for the new platform window. WindowSceneResolver in backend/common/lifecycle
is the matching shared contract for resolving those scene requests into new
AppRuntime instances or
explicit scene rejections before a platform backend allocates a native window.
HostEventSource in runtime is the subscription adapter for app-owned
host-event fanout: platform code can publish normalized Event values,
while apps map selected events back into typed Program messages through
Subscription::host_event; cancellation removes the publisher handler so late
host events do not re-enter stale app state.
HostWindowEventSource is the matching host-layer subscription adapter for
window-scoped platform events: platform code can publish a WindowId plus
normalized Event, while apps map those HostWindowEvent values through
Subscription::window_event; cancellation removes the publisher handler so late
window events do not re-enter stale app state.
HostPlatformEventSources bundles the host-event and window-event sources for
platform runtimes. Web, macOS, Windows, and Linux app options can carry that
bundle; after a raw platform event is normalized and dispatched through the
matching HostRuntimeDriver, the backend publishes the same Event with
its WindowId so app-owned Subscription::host_event and
Subscription::window_event adapters can observe real runtime events without
moving platform event conversion into core.
@services.TimerSource and @services.RouteSource are the app-facing
subscription adapters. Platform schedulers and history/deep-link integrations
adapt into those sources at the composition edge; cancellation runs their
cleanup and stale callbacks cannot re-enter the Program. backend does not
export a second app-facing timer or route model.
backend/common.resolve_open_request pairs a successful scene resolution
with the created registry record so the host can keep window id, scene metadata,
and runtime together. WindowRuntimeSlot then wraps that record with a
HostRuntimeDriver, giving future multi-window hosts a shared per-window
runtime/driver shape before platform-specific window and renderer handles are
attached. WindowRuntimeSlots is the matching collection for lookup,
primary/focused slot selection, record synchronization, and closed-window
cleanup, including shared helpers for inserting and syncing slots from
WindowRegistry, applying platform-neutral window requests, and applying
host lifecycle events while keeping the slot record aligned.
PlatformWindowMap binds platform window ids from wzzc-dev/window to
MoUI WindowId values so event dispatch can route through the host registry
instead of assuming one global window.
HostWebViewCapabilities is the capability contract for native platform
WebViews. The independent moui_webview addon owns WebViewHost and
WebViewController; hosts sync DrawFrame.platform_views to concrete WebView
objects, validate and dispatch WebViewEvent values, and drain controller tasks
at the platform edge. Browser Web wasm reports unavailable instead of creating
an iframe overlay. On macOS, the WKWebView host also consumes
DrawFrame.overlay_bounds: for a full-surface WebView it keeps the active
transparent Skia presenter above the WebView and excludes that sibling region
from WebView hit testing, so
already-rendered MoUI dialogs, sheets, and popovers remain visible and
interactive while the page stays visible underneath. Ordinary full-surface
platform-view frames clear transparently and pass presenter hit testing through
to WKWebView, so modal transitions do not reattach the WebView's remote layer.
This is currently macOS-only;
Web, Windows, and Linux do not expose MoUI overlays over native platform
views. The macOS WebView reserves a fixed 32-point top drag region. Blank
space there starts native window dragging, while interactive DOM controls and
elements marked data-moui-no-drag report no-drag rectangles and keep normal
WebView clicks.
Web, macOS, Windows, and Linux should convert their native window events into
Event and then let AppRuntime update state, rebuild, and emit
DrawCommand values.
The active Web, macOS, Windows, and Linux hosts all open a primary
WindowRecord, register the existing runtime/driver as a primary
WindowRuntimeSlot, bind the platform window id to the host id, route
incoming platform window events through that mapping, apply resize/focus/close
Event values through the registry, sync slot records after lifecycle
changes, and remove slot, platform binding, and record when a host window is
disposed. That makes multi-window lifecycle state a shared host concern instead
of a future platform-specific rewrite. Platform entries also accept a shared
WindowRequestQueue through AppBuilder::window_requests and drain
focus, close, resize, minimize, show, and set-primary requests at the platform
edge. The same queue records ordered request completions, making accepted
operations and explicit rejections observable. Active backends use the shared
queue drain helper so completion recording stays a host contract instead of a
platform-local loop.
WindowCommands is the higher-level command facade over the same queue for
app-facing open/focus/resize/minimize/show/close helpers and shared draining
into a registry or window runtime slots.
Every application entrypoint calls @runtime.run_app, supplies ordered
RendererProvider values, supplies one platform entry, and calls
run. Renderer-specific options and native-handle policy are captured by
renderer providers; platform-
specific options are captured by the platform entry. With a resolver, OpenWindow
requests resolve a scene into a new AppRuntime, create another platform
window, create an opaque HostSurface, bind the first accepting provider to a
RendererSession, register a per-window
HostRuntimeDriver, bind the platform id, and then route redraw, events,
context menus, service completions, IME sync, and disposal through
window-indexed slots. Without a resolver, hosts reject OpenWindow with the
shared unavailable-resolver message.
RendererSession is the renderer-neutral live handle used by native host
cores. Its stable constructor core contains resize, frame rendering,
present-completion drain, text-system access, present-count diagnostics, and
disposal. Image decoding, resource status, cache, and retained-layer residency
belong exclusively to the session. Host cores depend on core,
runtime, backend, the neutral render contract, and the platform
window package. They do not import moui_wgpu_renderer, moui_skia_renderer, wgpu_mbt,
moui_skia, or another concrete renderer. Platform backends own native window
handles, neutral CPU presenters, opaque native surface/display handles, and
lifecycle/I/O callbacks.
render/common owns provider negotiation and shared algorithms; renderer
modules own decode, native bindings, platform route policy, and renderer
diagnostics. Rejection leaves no persistent resources; a bound session is the
sole owner of accepted renderer/native-surface resources and disposes them
idempotently.
Renderer image work is event-driven. A session emits an opaque
RendererImageLoadRequest with a source and token through RendererEvent.
backend/common/image stores only cancellable I/O tasks, reads bytes from
HostImageSource, and sends an ImageResourceLoadCompletion with the same
token back to RendererSession::apply_image_load_completion. The session
returns whether the completion was applied; stale, duplicate, or disposed
tokens are inert and only an applied change requests redraw. Platform backends
never store renderer resource status or revisions. Each renderer provider
supplies a RendererImageDecoder that owns format detection, decode, and
resource cache updates. The host source and scheduler do not decode images,
mutate renderer caches, or live in core.
RendererDescriptor and RendererSelection remain renderer facade reporting tools:
they describe static capability identity and matching, not native host runtime
assembly. View still describes UI declaration trees only, and
ControlledValue[T, Msg] is the immutable TEA/control value bridge; it has no
setter path into an application model.
Shell Embedding Bridge
moui/backend is the only owner of the neutral HostServiceRequest,
HostServiceResponse, HostServiceCapabilities, HostServiceBridge, request-id,
and completion contracts. The internal implementations are deliberately
asymmetric where the hosts are asymmetric:
backend/common/services/desktopowns the synchronous desktop router. macOS, Windows, and Linux provide native clipboard, URL, dialog, menu, and settings closures; the package routes shared text/binary file and directory implementations frombackend/common/services/native.backend/common/services/nativeowns native@fsservice I/O shared by desktop and embedded backends.backend/common/image/nativeowns the renderer-neutral raw-byteHostImageSource.backend/common/services/embeddedowns the callback queue. Clipboard and platform channel requests returnPending(id)in FIFO order, complete at most once, reject duplicate/late responses, and are cancelled during dispose. Other desktop-only requests returnUnavailablesynchronously.
EmbeddedRuntimeHostBridge is the private Android/iOS/HarmonyOS runtime
aggregation boundary and composes backend/common/services/embedded. It
coalesces EmbeddedImeRequest updates, transports runtime-owned full/delta
semantics commits with SemanticsNodeId and SemanticsGeneration, synchronizes
platform-view placements/events, and maps pending service requests to the
unchanged native wire schema. Its cursor suppresses unchanged transport without
becoming a second revision authority. A disposed bridge cancels outstanding
services and rejects late responses.
Window Host Owners
Platform backends hold narrow owners directly instead of delegating all state to an aggregate coordinator:
common/lifecycleowns records, runtime slots, requests, platform-window maps, logical phase/surface generation, re-entry blocking, exit intent, and exactly-once close;common/frameowns each liveRendererSession, pending/present completion, redraw/resize, and IME frame hooks;common/imageowns cancellable loader tasks, tokenized completion delivery, callback detach, and cancellation. It does not store renderer resource status, cache residency, repaint revisions, or a backend repaint tracker;common/inputowns neutral conversion and pointer/text/IME session state;common/servicesowns the service facade, async completion, and bridge lifetime.
Platforms contribute WindowSurfaceActions for native operations. Root
backend/common exposes stateless workflows that take the relevant owners and
actions explicitly; it retains no window state and is not another aggregate
coordinator. Close order is fixed: block lifecycle re-entry; detach image
callbacks and cancel work; close embedded and service channels; dispose the
renderer session; dispose platform views/native host resources; clear
mappings/runtime/registry; finish close.
Embedded-runtime backends share moui/backend/common/embedded
(HostedWindowBackend, HostedWindow projection closures, HostedRuntimeSession).
Android/iOS/HarmonyOS window_hosted.mbt are thin shells (platform window
creation, surface handles, six ApplicationHandler slots, host simulation
pump, IME sink injection) around that shared shell. Their HostCmd values are
decoded by the stateless wzzc-dev/window/internal/embedded_dispatch adapter.
common/lifecycle::EmbeddedLifecycle uniquely owns logical phase, surface
generation, primary-window routing, detach, and exit intent. EmbeddedSession
composes it with frame/image/input/services owners plus renderer, IME,
semantics, platform-view, and transport capabilities; it does not own another
phase or frame loop. Web stores the narrow owners directly and uses the same
stateless workflows; browser DOM routing stays in moui/backend/web.
TextInputEvent::ReplaceText and SetSelection preserve arbitrary native IME
replacement and UTF-16 selection updates. Mobile requests include text,
selection, composition, caret, and candidate rectangle without changing the
desktop window_core.ImeRequest contract.
Accessibility actions enter through
AppRuntime::perform_semantics_action(PerformSemanticsActionRequest). Requests
carry a SemanticsNodeId, typed SemanticsAction, and exact runtime generation.
The runtime owns stale-generation, removed-node, enabled, capability, and
handler validation. Host adapters only transport the request and receipt; they
must not repeat validation or convert an action back into screen-coordinate
input. Web semantics-only commits are synchronized independently of redraw.
Typed wire services live on the same boundary. HostServiceBridge exposes
capability-checked dispatch for clipboard, file dialogs, menus, open-URL, and
system-theme requests. The app-facing @services.PlatformServices facade adds
neutral contracts for notifications, tray items, permission prompts, sharing,
printing, protocol/file associations, window-state persistence, and an
optional foreground/background lifecycle source. These operations use the
PlatformChannel extension point (channel="moui.platform") so concrete
platform backends can add native behavior without importing platform types into
the app package. Backends can report unavailable services without
pretending that app code can call platform APIs directly.
HostCapabilitySummary folds those service flags together with input,
window-lifecycle, text-input, IME, drag/drop, async-service, and native
accessibility readiness. It is a high-level reporting API for apps, diagnostics,
and Showcase. Its preflight_fields() helper emits a renderer-neutral ready/gap
field string for provider/package audits such as native Skia preflight logs;
HostServiceBridge, HostInputContract, and platform backend setup remain the
source of truth for actual behavior.
Apps do not consume this bridge. @backend_common.app_services(...) adapts it to
@services.AppServices, and @backend_common.app_environment(...) combines those
services with optional @services.TimerSource and @services.RouteSource.
PlatformServices::unavailable() is the default, so a host must opt into each
new operation and may return an explicit ServiceError::unavailable when the
matching OS capability or permission is absent. AppLifecycleSource follows
the same subscription ownership and cancellation rules as route sources.
Platform backends expose app_environment() to composition roots; Program
closures capture the environment without placing it in business Model data.
Services that cannot finish synchronously, especially browser clipboard reads
and file dialogs that need a permission or picker callback, can return
HostServiceResponse::Pending through @backend_common.ServiceAsyncQueue. The host drains
pending requests into an in-flight set at the platform edge, completes them with
the original request attached, and records the completion. Runtime-owned
effects such as async paste are handed to HostRuntimeDriver. The host adapter
converts app-owned operations to ServiceTask[T]; apps receive
ServiceTaskResult::Success, Failure, or Cancelled through their typed
message loop. Request ids are protocol values; queue handlers stay in
backend/common, and
stale task dispatch is rejected by the runtime task lifecycle.
The Web backend wires that queue to browser host imports and exported wasm
completion callbacks for clipboard reads and file pickers.
Web, macOS, and Windows entrypoints query that bridge at startup and install
the reported light/dark scheme into AppRuntime before the first host driver
layout/redraw pass, so initial view builds see the platform color scheme through
ViewEnvironment reads during the initial Program view build.
ThemeChanged window events are also normalized into Event::ThemeChanged;
HostRuntimeDriver applies them to runtime environment instead of leaking a
platform-specific event into app code.
Keyboard shortcuts, menus, and host command responses share the
ActionCommand/CommandIntent model. ActionCommandMap is the platform-neutral
dispatcher for matching shortcuts and invoking enabled command handlers.
Application-facing command metadata aliases are exported from moui/views
alongside the command palette and menu constructors; lower-level runtime and
host integration can still use the core contract directly.
The views package also provides view-level menu helpers over the same
metadata: menu_bar, command_menu, and context_menu_region render buttons,
rows, surfaces, and overlays as ordinary View[Msg] values. They are fallback
or app-authored menu UI, not platform menu services.
Copy, cut, and paste shortcuts are routed through HostRuntimeDriver with the
active HostServiceBridge so focused text controls use the platform clipboard
when that service is available, while app-level command handlers still run when
no text command handles the intent.
Secondary-button context-menu requests are recognized by the host event layer:
platform backends skip ordinary pointer dispatch for those events, then native
menu-capable hosts ask HostServiceBridge::ShowMenu to present the current
runtime action commands and dispatch the selected intent back through
HostRuntimeDriver.
Application menus (L2) use @services.MenuServices::install_application with
ApplicationMenu descriptors derived from the same typed Program command
declarations. ApplicationMenuPlacement::MenuBar is the default and creates a
top-level menu. ApplicationMenu::application(items=...) selects
ApplicationMenuPlacement::ApplicationMenu; on macOS those commands are
inserted into the standard application menu after About and before Services,
without replacing Services, Hide, or Quit. macOS owns the AppKit target/action
bridge and retains the installed MoonBit callback until the menu is replaced.
Windows, Linux, and Web currently return Unavailable. Selection delivery for
top-level menus still uses the platform action handler installed by the
entrypoint (for example @window_macos.set_system_menu_action_handler). See
Non-render cookbook and
Showcase's Platform workspace (examples/showcase/app/platform).
App-facing multi-window lifecycle requests go through WindowActions
(open, close, focus, set_primary, resize, minimize, show) on the
shared WindowRequestQueue. Each resolved scene remains an independent
AppRuntime; shared state is app-owned. See examples/multi_window.
File drop targets use the View::on_file_drop modifier; hosts normalize native
file drag/drop positions and paths before the runtime dispatches typed messages
to the hit view. views.drop_zone and views.file_import_panel are view-level
workflow shells over that modifier; their browse action remains an app message,
so effect-capable app code can call AppServices::files().open_file(...) and
return its typed ServiceTask::effect. Web file import may
expose browser-selected file names or handles rather than native filesystem
paths, while native hosts can return platform paths through the same selection
array.
See Platform notes for setup, backend-specific constraints, and validation commands.