///|
/// Runtime-owned HostRuntimeDriver (ADR 0018). Moved from backend so
/// that backend carries platform-neutral host contracts only.
pub(all) struct HostRuntimeDriver {
runtime : AppRuntime
scheduler : RedrawScheduler
frame_clock : (() -> Double)?
mut close_requested : Bool
mut frame_time_ms : Double
}
///|
pub fn HostRuntimeDriver::new(
runtime~ : AppRuntime,
schedule_redraw~ : () -> Unit,
frame_clock? : (() -> Double)? = None,
) -> HostRuntimeDriver {
let driver = {
runtime,
scheduler: RedrawScheduler::new(schedule=schedule_redraw),
frame_clock,
close_requested: false,
frame_time_ms: 0.0,
}
runtime.set_redraw_callback(() => driver.request_redraw())
driver
}
///|
pub fn HostRuntimeDriver::dispatch(
self : HostRuntimeDriver,
event : @backend_host.Event,
) -> @backend_host.EventResult {
match event {
Resized(metrics) => {
self.runtime.dispatch_event(Resize(metrics.logical_size))
self.request_redraw()
}
Pointer(event) => {
self.runtime.dispatch_event(Pointer(event))
self.request_redraw_if_needed()
}
Scroll(event) => {
self.runtime.dispatch_event(Pointer(event.to_pointer_event()))
self.request_redraw_if_needed()
}
Focus(event) =>
match event.to_core() {
Some(core_event) => {
self.runtime.dispatch_event(core_event)
self.request_redraw_if_needed()
}
None => ()
}
Keyboard(event) => {
self.runtime.dispatch_event(Keyboard(event))
self.request_redraw_if_needed()
}
TextInput(event) => {
self.runtime.dispatch_event(TextInput(event))
self.request_redraw_if_needed()
}
DragDrop(event) => {
self.runtime.dispatch_event(DragDrop(event))
self.request_redraw_if_needed()
}
ThemeChanged(scheme) => {
self.runtime.set_environment(
self.runtime.environment().with_system_scheme(scheme),
)
self.request_redraw_if_needed()
}
Command(intent) =>
if self.runtime.dispatch_command(intent) {
self.request_redraw_if_needed()
}
RedrawRequested => {
self.begin_redraw()
self.finish_redraw()
}
CloseRequested => {
if !self.runtime.dispatch_presentation_host_close() {
self.close_requested = true
}
self.request_redraw_if_needed()
}
}
self.event_result()
}
///|
/// Dispatch a platform view event directly to the runtime tree.
/// Used by the `PlatformViewPlugin` infrastructure to route decoded native
/// platform-view events without going through `Event` (which no longer
/// carries a platform-view variant).
pub fn HostRuntimeDriver::dispatch_platform_view(
self : HostRuntimeDriver,
id : String,
event : @core.PlatformViewEvent,
) -> Unit {
self.runtime.dispatch_event(PlatformView(id, event))
self.request_redraw()
}
///|
pub fn HostRuntimeDriver::perform_semantics_action(
self : HostRuntimeDriver,
request : PerformSemanticsActionRequest,
) -> Result[SemanticsActionReceipt, SemanticsActionDispatchError] {
match self.runtime.perform_semantics_action(request) {
Ok(receipt) => {
self.request_redraw_if_needed()
Ok(receipt)
}
Err(error) => Err(error)
}
}
///|
/// Reads the same atomically committed stream used by Agent hosts. Native
/// adapters keep their own cursor and do not inspect the runtime tree directly.
pub fn HostRuntimeDriver::read_semantics(
self : HostRuntimeDriver,
since? : @core.SemanticsGeneration,
) -> Result[SemanticsRead, SemanticsReadError] {
match since {
Some(generation) => self.runtime.read_semantics(since=generation)
None => self.runtime.read_semantics()
}
}
///|
pub fn HostRuntimeDriver::dispatch_command_with_services(
self : HostRuntimeDriver,
intent : @core.CommandIntent,
services : @backend_host.HostServiceBridge,
) -> @backend_host.EventResult {
match intent {
Copy | Cut => {
let result = self.runtime.dispatch_focused_text_command(intent)
if result.handled {
match result.clipboard_text {
Some(text) => ignore(services.dispatch(Clipboard(WriteText(text))))
None => ()
}
self.request_redraw_if_needed()
} else if self.runtime.dispatch_command(intent) {
self.request_redraw_if_needed()
}
}
Paste => {
let mut handled = false
match services.dispatch(Clipboard(ReadText)) {
ClipboardText(text) => {
let result = self.runtime.dispatch_focused_text_command(
intent,
paste_text=Some(text),
)
if result.handled {
handled = true
}
self.request_redraw_if_needed()
}
Pending(_) => handled = true
_ => ()
}
if !handled && self.runtime.dispatch_command(intent) {
self.request_redraw_if_needed()
}
}
_ =>
if self.runtime.dispatch_command(intent) {
self.request_redraw_if_needed()
}
}
self.event_result()
}
///|
pub fn HostRuntimeDriver::dispatch_action_command_with_services(
self : HostRuntimeDriver,
command : @core.ActionCommand,
services : @backend_host.HostServiceBridge,
) -> @backend_host.EventResult {
match command.intent {
Copy | Cut | Paste =>
self.dispatch_command_with_services(command.intent, services)
_ => {
if self.runtime.dispatch_action_command(command) {
self.request_redraw_if_needed()
} else if command.intent != Activate {
ignore(self.dispatch_command_with_services(command.intent, services))
}
self.event_result()
}
}
}
///|
pub fn HostRuntimeDriver::dispatch_keyboard_with_services(
self : HostRuntimeDriver,
event : @core.KeyboardEvent,
services : @backend_host.HostServiceBridge,
) -> @backend_host.EventResult {
match keyboard_service_command_intent(event) {
Some(intent) => self.dispatch_command_with_services(intent, services)
None => self.dispatch(Keyboard(event))
}
}
///|
/// Route Escape and platform Back through the same top-most presentation
/// dismissal path. Enter remains owned by the focused/default action.
pub fn HostRuntimeDriver::dispatch_escape(
self : HostRuntimeDriver,
) -> @backend_host.EventResult {
ignore(
self.runtime.dispatch_event(
Keyboard(@core.KeyboardEvent::new(key="Escape")),
),
)
self.request_redraw_if_needed()
self.event_result()
}
///|
pub fn HostRuntimeDriver::dispatch_system_back(
self : HostRuntimeDriver,
) -> @backend_host.EventResult {
ignore(self.runtime.dispatch_presentation_back())
self.request_redraw_if_needed()
self.event_result()
}
///|
pub fn HostRuntimeDriver::show_context_menu(
self : HostRuntimeDriver,
services : @backend_host.HostServiceBridge,
) -> @backend_host.EventResult {
let commands = self.context_menu_commands()
if commands.length() == 0 {
return self.event_result()
}
match services.dispatch(ShowMenu(commands)) {
MenuAction(command) =>
ignore(self.dispatch_action_command_with_services(command, services))
MenuCommand(intent) =>
ignore(self.dispatch_command_with_services(intent, services))
_ => ()
}
self.event_result()
}
///|
/// The menu the host shows for whatever is under the pointer.
///
/// A focused text control contributes the standard editing group (Undo/Redo,
/// Cut/Copy/Paste, Select All) *in addition to* whatever the program
/// contributes, rather than instead of it: VS Code, Typora and Obsidian all
/// lead the menu with the editing group and follow it with the surface's own
/// actions, so a selection adds Cut/Copy instead of replacing the app's
/// formatting menu. Copy and Cut stay disabled while there is no selection.
/// Without focus there is no text target, so only the program's commands
/// apply.
fn HostRuntimeDriver::context_menu_commands(
self : HostRuntimeDriver,
) -> Array[@core.ActionCommand] {
let input = self.runtime.focused_text_input()
let commands = self.document_context_menu_commands()
if input.focused {
focused_text_context_menu_commands(input) + commands
} else {
commands
}
}
///|
fn HostRuntimeDriver::document_context_menu_commands(
self : HostRuntimeDriver,
) -> Array[@core.ActionCommand] {
self.runtime
.action_commands()
.filter(command => command.context_menu)
.dedupe_context_menu_commands()
}
///|
fn focused_text_context_menu_commands(
input : @core.TextInputState,
) -> Array[@core.ActionCommand] {
let has_selection = text_input_state_has_active_selection(input)
@core.default_text_commands().map(command => {
match command.intent {
Copy | Cut => { ..command, enabled: has_selection, }
_ => command
}
})
}
///|
fn text_input_state_has_active_selection(input : @core.TextInputState) -> Bool {
match input.selection {
Some(selection) => !selection.normalized().is_collapsed()
None => false
}
}
///|
fn Array::dedupe_context_menu_commands(
self : Array[@core.ActionCommand],
) -> Array[@core.ActionCommand] {
let commands : Array[@core.ActionCommand] = []
for command in self {
if !commands.any(existing => same_context_menu_command(existing, command)) {
commands.push(command)
}
}
commands
}
///|
fn same_context_menu_command(
left : @core.ActionCommand,
right : @core.ActionCommand,
) -> Bool {
left.intent == right.intent &&
left.label == right.label &&
left.enabled == right.enabled &&
left.group == right.group &&
left.description == right.description &&
left.context_menu == right.context_menu
}
///|
fn keyboard_service_command_intent(
event : @core.KeyboardEvent,
) -> @core.CommandIntent? {
if !event.pressed ||
event.repeat ||
event.modifiers.alt ||
event.modifiers.shift ||
!(event.modifiers.meta || event.modifiers.control) {
None
} else if event.key == "c" || event.key == "C" {
Some(Copy)
} else if event.key == "x" || event.key == "X" {
Some(Cut)
} else if event.key == "v" || event.key == "V" {
Some(Paste)
} else {
None
}
}
///|
pub fn HostRuntimeDriver::request_redraw(self : HostRuntimeDriver) -> Unit {
self.scheduler.request()
}
///|
pub fn HostRuntimeDriver::set_redraw_scheduler(
self : HostRuntimeDriver,
schedule_redraw : () -> Unit,
) -> Unit {
self.scheduler.set_schedule(schedule_redraw)
}
///|
pub fn HostRuntimeDriver::request_redraw_if_needed(
self : HostRuntimeDriver,
) -> Unit {
if self.runtime.needs_redraw() {
self.scheduler.request_dirty()
}
}
///|
/// Start a redraw and advance the animation clock one step.
///
/// With a host `frame_clock` the tick carries real monotonic milliseconds, so
/// paint-time animations (`ViewPaintContext.now_ms`) run in real time on any
/// refresh rate — 60Hz, 120Hz ProMotion, variable refresh — instead of a
/// synthetic 16ms-per-frame step. Without a clock the fixed synthetic step is
/// kept, which still lets animations progress without user input.
///
/// `HostRuntimeDriver::draw_frame` may advance the same clock again through
/// `profile_draw_frame`; the duplicate tick lands on ~the same timestamp
/// (delta ≈ 0), which is harmless.
pub fn HostRuntimeDriver::begin_redraw(self : HostRuntimeDriver) -> Unit {
self.scheduler.begin_redraw()
match self.frame_clock {
Some(clock) => self.runtime.advance_time(clock())
None => {
self.frame_time_ms = self.frame_time_ms + 16.0
self.runtime.advance_time(self.frame_time_ms)
}
}
}
///|
pub fn HostRuntimeDriver::finish_redraw(self : HostRuntimeDriver) -> Unit {
self.runtime.dispatch_event(Redraw(Explicit))
self.scheduler.finish_redraw()
}
///|
pub fn HostRuntimeDriver::draw_commands(
self : HostRuntimeDriver,
) -> Array[@core.DrawCommand] {
self.runtime.draw_commands()
}
///|
pub fn HostRuntimeDriver::draw_frame(
self : HostRuntimeDriver,
) -> @core.DrawFrame {
match self.frame_clock {
Some(clock) => self.runtime.profile_draw_frame(clock).frame()
None => self.runtime.draw_frame()
}
}
///|
pub fn HostRuntimeDriver::environment(
self : HostRuntimeDriver,
) -> @core.Environment {
self.runtime.environment()
}
///|
pub fn HostRuntimeDriver::set_environment(
self : HostRuntimeDriver,
environment : @core.Environment,
) -> Unit {
self.runtime.set_environment(environment)
}
///|
pub fn HostRuntimeDriver::set_text_system(
self : HostRuntimeDriver,
text_system : @core.TextSystem,
) -> Unit {
self.runtime.set_text_system(text_system)
}
///|
pub fn HostRuntimeDriver::focused_text_input(
self : HostRuntimeDriver,
) -> @core.TextInputState {
self.runtime.focused_text_input()
}
///|
pub fn HostRuntimeDriver::size(self : HostRuntimeDriver) -> @core.Size {
self.runtime.size()
}
///|
pub fn HostRuntimeDriver::event_result(
self : HostRuntimeDriver,
) -> @backend_host.EventResult {
{
needs_rebuild: self.runtime.needs_rebuild(),
needs_redraw: self.runtime.needs_redraw(),
redraw_requested: self.scheduler.is_redraw_requested(),
close_requested: self.close_requested,
}
}
///|
/// Dispatch a service completion through the runtime driver. Moved from
/// backend per ADR 0018 so backend stays runtime-neutral.
pub fn HostRuntimeDriver::dispatch_service_completion(
self : HostRuntimeDriver,
completion : @backend_host.HostCompletedServiceResponse,
services : @backend_host.HostServiceBridge,
) -> @backend_host.EventResult {
match (completion.request, completion.response) {
(Clipboard(ReadText), ClipboardText(text)) => {
let result = self.runtime.dispatch_focused_text_command(
Paste,
paste_text=Some(text),
)
if result.handled {
self.request_redraw_if_needed()
} else if self.runtime.dispatch_command(Paste) {
self.request_redraw_if_needed()
}
}
(ShowMenu(_), MenuAction(command)) =>
ignore(self.dispatch_action_command_with_services(command, services))
(ShowMenu(_), MenuCommand(intent)) =>
ignore(self.dispatch_command_with_services(intent, services))
(QuerySystemTheme, SystemTheme(scheme)) => {
self.runtime.set_environment(
self.runtime.environment().with_system_scheme(scheme),
)
self.request_redraw_if_needed()
}
_ => ()
}
self.event_result()
}