///|
fn RuntimeState::set_action_commands(
self : RuntimeState,
commands : ActionCommandMap,
) -> Unit {
self.action_commands = commands
}
///|
fn RuntimeState::action_commands(self : RuntimeState) -> Array[ActionCommand] {
self.action_commands.commands()
}
///|
fn RuntimeState::dispatch_command(
self : RuntimeState,
intent : CommandIntent,
) -> Bool {
let text_result = self.dispatch_focused_text_command(intent)
if text_result.handled {
text_result.changed
} else if self.action_commands.dispatch_intent(intent) {
self.request_activation_rebuild()
true
} else {
false
}
}
///|
fn RuntimeState::dispatch_action_command(
self : RuntimeState,
command : ActionCommand,
) -> Bool {
if self.action_commands.dispatch_command(command) {
self.request_activation_rebuild()
true
} else {
false
}
}
///|
fn RuntimeState::dispatch_focused_text_command(
self : RuntimeState,
intent : CommandIntent,
paste_text? : String? = None,
) -> TextCommandResult {
match self.animfocus.focused_element {
Some(target) => {
let outcome = self.element_tree.root.dispatch_text_command(
target,
intent,
paste_text~,
environment=self.environment,
text_system=self.text_system,
)
let result = outcome.result
if result.changed {
self.dirty.needs_redraw = true
self.dirty.needs_rebuild = true
self.dirty.needs_paint = true
self.mark_build_dirty(self.element_tree.id())
}
self.deliver_messages(outcome.deliveries)
result
}
None => TextCommandResult::ignored()
}
}
///|
fn RuntimeState::dispatch_event(self : RuntimeState, event : AppEvent) -> Unit {
// Additive observation hook: broadcast every AppEvent before processing.
// Defaults to a no-op; only active when an observer is installed (agent layer).
(self.event_observer)(event)
match event {
FrameTick(frame) => {
// Advance the animation clock so controls can compute transition
// progress from `ViewPaintContext.now_ms`. While any subtree is still
// animating, keep paint + redraw dirty so the host ticks another frame;
// once every animation settles (`active_animations` flips false) this
// becomes a passive clock update with no extra redraw.
self.animfocus.now_ms = frame.time_ms
if self.animfocus.active_animations {
self.dirty.needs_paint = true
self.dirty.needs_redraw = true
self.mark_dirty(self.element_tree.id())
}
}
Resize(size) => {
self.size = size
self.request_root_rebuild(reason="viewport resized")
self.dirty.needs_redraw = true
self.dirty.needs_layout = true
self.dirty.needs_paint = true
self.element_tree.root.mark_subtree_layout_dirty()
self.mark_build_dirty(self.element_tree.id())
}
Redraw(_) => {
self.dirty.needs_redraw = false
self.dirty.rebuild_wakeup_redraw = false
}
Pointer(event) => ignore(self.dispatch_pointer_event(event))
Keyboard(event) => {
// Top-most dismissal layer stack (ADR 0033): overlay hosts registered
// during the placement pass receive Escape/Back top-most-first, so a
// popover nested inside a dialog dismisses before the dialog.
let layer_consumed = self.dispatch_dismissal_layers(event)
if layer_consumed {
return
}
let shortcut = self.element_tree.root.dispatch_keyboard_shortcut(
event,
environment=self.environment,
text_system=self.text_system,
)
if shortcut.changed || shortcut.activated {
self.dirty.needs_redraw = self.dirty.needs_redraw || shortcut.changed
self.dirty.needs_rebuild = self.dirty.needs_rebuild ||
shortcut.activated
self.dirty.needs_paint = self.dirty.needs_paint || shortcut.changed
self.mark_current_dirty_elements()
if shortcut.activated {
self.request_activation_rebuild()
}
self.deliver_messages(shortcut.deliveries)
} else if self.action_commands.dispatch_shortcut(event) is Some(_) {
self.deliver_messages(shortcut.deliveries)
self.request_activation_rebuild()
} else if event.pressed && event.key == "Tab" {
self.deliver_messages(shortcut.deliveries)
match self.animfocus.focused_element {
Some(target) => {
let constrained_target = self.element_tree.root.constrained_focus_target(
Some(target),
current=Some(target),
)
if constrained_target != Some(target) {
ignore(self.move_focus(reverse=event.modifiers.shift))
} else if self.element_tree.root.accepts_tab_input(target) {
let result = self.element_tree.root.dispatch_keyboard(
target,
event,
environment=self.environment,
text_system=self.text_system,
)
self.dirty.needs_redraw = self.dirty.needs_redraw ||
result.changed
self.dirty.needs_rebuild = self.dirty.needs_rebuild ||
result.activated
self.dirty.needs_paint = self.dirty.needs_paint || result.changed
if result.changed || result.activated {
self.mark_current_dirty_elements()
}
if result.activated {
self.request_activation_rebuild()
}
self.deliver_messages(result.deliveries)
} else {
ignore(self.move_focus(reverse=event.modifiers.shift))
}
}
None => ignore(self.move_focus(reverse=event.modifiers.shift))
}
} else {
self.deliver_messages(shortcut.deliveries)
match self.animfocus.focused_element {
Some(target) => {
let result = self.element_tree.root.dispatch_keyboard(
target,
event,
environment=self.environment,
text_system=self.text_system,
)
self.dirty.needs_redraw = self.dirty.needs_redraw || result.changed
self.dirty.needs_rebuild = self.dirty.needs_rebuild ||
result.activated
self.dirty.needs_paint = self.dirty.needs_paint || result.changed
if result.changed || result.activated {
self.mark_current_dirty_elements()
}
if result.activated {
self.request_activation_rebuild()
}
self.deliver_messages(result.deliveries)
}
None => ()
}
}
}
TextInput(event) =>
match self.animfocus.focused_element {
Some(target) => {
let result = self.element_tree.root.dispatch_text_input(
target,
event,
environment=self.environment,
text_system=self.text_system,
)
self.dirty.needs_redraw = self.dirty.needs_redraw || result.changed
self.dirty.needs_rebuild = self.dirty.needs_rebuild ||
result.activated
self.dirty.needs_paint = self.dirty.needs_paint || result.changed
if result.changed || result.activated {
self.mark_current_dirty_elements()
}
if result.activated {
self.request_activation_rebuild()
}
self.deliver_messages(result.deliveries)
}
None => ()
}
DragDrop(event) => {
let result = self.element_tree.root.dispatch_drag_drop(
event,
self.render_tree.root,
)
self.dirty.needs_redraw = self.dirty.needs_redraw || result.changed
self.dirty.needs_rebuild = self.dirty.needs_rebuild || result.activated
self.dirty.needs_paint = self.dirty.needs_paint || result.changed
if result.changed || result.activated {
self.mark_current_dirty_elements()
}
if result.activated {
self.request_activation_rebuild()
}
self.deliver_messages(result.deliveries)
}
PlatformView(id, event) => {
let result = self.element_tree.root.dispatch_web_view_event(
id,
event,
environment=self.environment,
text_system=self.text_system,
)
if result.changed || result.activated || result.captured {
self.dirty.needs_rebuild = true
self.dirty.needs_redraw = true
self.dirty.needs_paint = true
self.mark_current_dirty_elements()
self.request_activation_rebuild()
}
self.deliver_messages(result.deliveries)
}
CloseRequested => ()
}
}
///|
fn RuntimeState::commit_focus(self : RuntimeState, next : ElementId?) -> Bool {
let previous = self.animfocus.focused_element
if previous == next {
return false
}
self.animfocus.focused_element = next
match previous {
Some(id) => {
self.mark_dirty(id)
self.mark_semantics_dirty(id)
}
None => ()
}
match next {
Some(id) => {
self.mark_dirty(id)
self.mark_semantics_dirty(id)
}
None => ()
}
true
}
///|
/// Dispatches one pointer event and reports whether the runtime consumed it.
/// This is intentionally separate from `dispatch_event`: browser hosts need
/// this result synchronously, while ordinary hosts can continue to ignore it.
fn RuntimeState::dispatch_pointer_event(
self : RuntimeState,
event : PointerEvent,
) -> InputDispatchResult {
self.ensure_render_tree()
self.last_event_hit_path = self.element_tree.root.inspector_hit_path(
event.position,
self.render_tree.root,
)
let release_pointer = match self.animfocus.active_pointer_capture {
Some(_) => event.phase == Up || event.phase == Cancel
None => false
}
if event.phase == Down {
let previous_focus = self.animfocus.focused_element
let candidate_focus = self.element_tree.root.find_focusable_at(
event.position,
self.render_tree.root,
)
ignore(
self.commit_focus(
self.element_tree.root.constrained_focus_target(
candidate_focus,
current=previous_focus,
),
),
)
}
let result = match self.animfocus.active_pointer_capture {
Some(target) =>
if event.phase == Down {
self.element_tree.root.dispatch_pointer(
event,
self.render_tree.root,
environment=self.environment,
text_system=self.text_system,
capture_lookup=false,
now_ms=self.animfocus.now_ms,
)
} else {
match
self.element_tree.root.dispatch_pointer_to_target(
target,
event,
self.render_tree.root,
environment=self.environment,
text_system=self.text_system,
now_ms=self.animfocus.now_ms,
) {
Some(result) => result
None => {
self.animfocus.active_pointer_capture = None
self.element_tree.root.dispatch_pointer(
event,
self.render_tree.root,
environment=self.environment,
text_system=self.text_system,
capture_lookup=false,
now_ms=self.animfocus.now_ms,
)
}
}
}
None =>
self.element_tree.root.dispatch_pointer(
event,
self.render_tree.root,
environment=self.environment,
text_system=self.text_system,
capture_lookup=false,
now_ms=self.animfocus.now_ms,
)
}
// Views can claim focus from the pointer handler (e.g. picker popup
// interaction). Prefer that claim over the geometry-only hit from
// `find_focusable_at` so focus tracks the control that handled the event.
match result.focused {
Some(target) => ignore(self.commit_focus(Some(target)))
None => ()
}
if event.phase == Down {
// Standard controls report `captured` on a handled Down. Retain the
// focusable hit target so the matching Up reaches the same element even
// if browser focus/semantics updates repaint the tree between DOM events.
// A control with an explicit local capture keeps precedence.
self.animfocus.active_pointer_capture = match
self.element_tree.root.active_pointer_capture_id(Move) {
Some(target) => Some(target)
None =>
if result.captured {
match result.capture_target {
Some(target) => Some(target)
None => self.animfocus.focused_element
}
} else {
None
}
}
} else if event.phase == Up || event.phase == Cancel {
self.animfocus.active_pointer_capture = None
}
self.dirty.needs_redraw = self.dirty.needs_redraw || result.changed
self.dirty.needs_rebuild = self.dirty.needs_rebuild || result.activated
self.dirty.needs_paint = self.dirty.needs_paint || result.changed
if result.changed || result.activated {
self.mark_current_dirty_elements()
}
if result.activated {
self.request_activation_rebuild()
}
let dispatch_result = {
// A wheel event is consumed only when a scroll target actually moved. In
// particular, this keeps the browser default available at a scroll edge.
// Pointer capture is a separate host concern and must not stand in for
// consumption on every input phase.
handled: if event.wheel_delta.x != 0.0 || event.wheel_delta.y != 0.0 {
result.changed
} else {
result.captured || result.changed || result.activated
},
capture_pointer: event.phase == Down && result.captured,
release_pointer,
}
self.deliver_messages(result.deliveries)
dispatch_result
}
///|
fn RuntimeState::move_focus(
self : RuntimeState,
reverse? : Bool = false,
) -> Bool {
self.ensure_render_tree()
let previous_focus = self.animfocus.focused_element
let next_focus = self.element_tree.root.next_focus(
current=previous_focus,
reverse~,
)
self.commit_focus(next_focus)
}
///|
fn RuntimeState::focus_key(self : RuntimeState, key : String) -> Bool {
self.ensure_render_tree()
let found = self.element_tree.root.focusable_key_target(key)
match found {
Some(target) => {
let previous_focus = self.animfocus.focused_element
let constrained = self.element_tree.root.constrained_focus_target(
Some(target),
current=previous_focus,
)
println("[focus-dbg] constrained_ok=\{constrained == Some(target)}")
if constrained == Some(target) {
ignore(self.commit_focus(Some(target)))
true
} else {
false
}
}
None => false
}
}
///|
fn RuntimeState::advance_time(self : RuntimeState, time_ms : Double) -> Unit {
self.dispatch_event(FrameTick(self.clock.frame(time_ms)))
}
///|
fn RuntimeState::clear_stale_pointer_capture(self : RuntimeState) -> Unit {
match self.animfocus.active_pointer_capture {
Some(target) =>
if !self.element_tree.root.contains_element_id(target) {
self.animfocus.active_pointer_capture = None
}
None => ()
}
}
///|
/// Dispatch a keyboard event to the registered dismissal layers, top-most
/// first. Returns true when a layer consumed the event.
fn RuntimeState::dispatch_dismissal_layers(
self : RuntimeState,
event : KeyboardEvent,
) -> Bool {
let mut consumed = false
let mut index = self.layer_stack.length() - 1
while index >= 0 {
if !consumed {
let result = self.element_tree.root.dispatch_keyboard(
self.layer_stack[index],
event,
environment=self.environment,
text_system=self.text_system,
)
if result.changed || result.activated {
self.dirty.needs_redraw = self.dirty.needs_redraw || result.changed
self.dirty.needs_paint = self.dirty.needs_paint || result.changed
self.dirty.needs_rebuild = self.dirty.needs_rebuild || result.activated
self.mark_current_dirty_elements()
self.deliver_messages(result.deliveries)
consumed = true
}
}
index = index - 1
}
consumed
}