///|
fn RuntimeState::request_redraw(self : RuntimeState) -> Unit {
self.dirty.needs_redraw = true
match self.redraw_callback {
Some(callback) => callback()
None => ()
}
}
///|
fn RuntimeState::request_rebuild_wakeup(self : RuntimeState) -> Unit {
if !self.dirty.needs_redraw {
self.dirty.rebuild_wakeup_redraw = true
}
self.request_redraw()
}
///|
fn RuntimeState::mark_visual_work(self : RuntimeState) -> Unit {
self.dirty.rebuild_wakeup_redraw = false
}
///|
fn RuntimeState::settle_rebuild_wakeup(self : RuntimeState) -> Unit {
if self.dirty.rebuild_wakeup_redraw &&
!self.dirty.needs_rebuild &&
!self.dirty.needs_layout &&
!self.dirty.needs_paint &&
!self.animfocus.active_animations {
self.dirty.needs_redraw = false
self.dirty.rebuild_wakeup_redraw = false
}
}
///|
fn RuntimeState::ensure_render_tree(self : RuntimeState) -> Unit {
self.rebuild_if_possible()
if self.dirty.needs_layout || self.dirty.needs_paint {
self.render_current_tree()
}
if self.dirty.needs_platform {
self.refresh_platform_tree()
}
}
///|
fn RuntimeState::refresh_platform_tree(self : RuntimeState) -> Unit {
self.ensure_layout_tree()
self.platform_tree = {
root: self.element_tree.root.platform_with_environment(
self.layout_tree.root,
RenderEnvironment::from_environment(self.environment).with_text_system(
self.text_system,
),
),
}
self.dirty.needs_platform = false
self.dirty.dirty_platform_elements.clear()
self.refresh_dirty_element_queue()
self.settle_rebuild_wakeup()
}
///|
fn RuntimeState::ensure_layout_tree(self : RuntimeState) -> Unit {
let mut passes = 0
while self.dirty.needs_rebuild || self.dirty.needs_layout {
if passes >= 64 {
abort("runtime layout did not stabilize after 64 passes")
}
passes = passes + 1
self.layout_current_tree()
}
// Overlay/presentation placement runs after the layout fixpoint so anchor
// resolution never re-enters layout and never keys layout caches.
self.apply_overlay_placements()
}
///|
fn RuntimeState::layout_current_tree(self : RuntimeState) -> Unit {
self.rebuild_if_possible()
self.stats.layout_pass_count = self.stats.layout_pass_count + 1
self.layout_tree = {
root: self.element_tree.root.place_with_text_system(
Rect::new(x=0.0, y=0.0, width=self.size.width, height=self.size.height),
environment=self.environment,
text_system=self.text_system,
layout_direction=self.environment.layout_direction,
),
}
self.anchor_registry.clear()
self.element_tree.root.collect_anchor_frames(
self.layout_tree.root,
self.anchor_registry,
)
self.dirty.needs_rebuild = false
self.dirty.needs_layout = false
self.dirty.dirty_layout_elements.clear()
self.refresh_dirty_element_queue()
ignore(self.try_pending_scroll_from_current_layout())
}
///|
/// Compute final overlay frames from the fresh anchor registry. Overlay hosts
/// position their presentation children during layout only provisionally
/// (correct sizes, placeholder origins); this pass resolves the anchors and,
/// when a computed frame differs from the placed frame, re-places the tree
/// with the overrides applied. The registry is position-snapshotted from the
/// base tree, so the resolved frames are stable across subsequent cache hits.
fn RuntimeState::apply_overlay_placements(self : RuntimeState) -> Unit {
let context : @core.OverlayPlacementContext = {
viewport: Rect::new(
x=0.0,
y=0.0,
width=self.size.width,
height=self.size.height,
),
host_frame: Rect::new(x=0.0, y=0.0, width=0.0, height=0.0),
child_sizes: self.layout_tree.root.children.map(child => child.frame.size),
anchor_frames: self.anchor_registry.snapshot(),
state: self.element_tree.root.control.view_state,
}
let overrides : Array[(Int, Array[(Int, Rect)])] = []
let spines : Array[ElementId] = []
let layers : Array[ElementId] = []
let active_surfaces : Array[ElementId] = []
collect_overlay_overrides(
self.element_tree.root,
self.layout_tree.root,
context,
[],
overrides,
spines,
layers,
active_surfaces,
)
self.layer_stack = layers
self.track_overlay_surface_lifecycle(active_surfaces)
if overrides.is_empty() {
self.overlay_overrides = []
return
}
self.overlay_overrides = overrides
// Force a re-place that consumes the overrides; unchanged subtrees still
// reuse their measured caches, so only positioned frames are recomputed.
self.element_tree.root.layout_cache.clear()
self.layout_tree = {
root: self.element_tree.root.place_with_text_system(
Rect::new(x=0.0, y=0.0, width=self.size.width, height=self.size.height),
environment=self.environment,
text_system=self.text_system,
layout_direction=self.environment.layout_direction,
overlay_overrides=self.overlay_overrides,
),
}
// Ancestor render caches embed descendant paint bounds; the re-place moved
// overlay frames, so every cache along the hosts' ancestor spines is stale.
for id in spines {
self.mark_dirty(id)
}
self.dirty.needs_paint = true
self.dirty.needs_redraw = true
}
///|
/// Descend the element/placed trees in lockstep and collect final overlay
/// frames for every node that reports `place_overlays` positions differing
/// from its currently placed child frames. `spines` receives the ancestor
/// element paths (including the host) of every host whose frames changed, so
/// stale ancestor render caches can be cleared.
fn collect_overlay_overrides(
element : ElementNode,
placed : PlacedNode,
context : @core.OverlayPlacementContext,
path : Array[ElementId],
out : Array[(Int, Array[(Int, Rect)])],
spines : Array[ElementId],
layers : Array[ElementId],
active_surfaces : Array[ElementId],
) -> Unit {
let child_path : Array[ElementId] = path.copy()
child_path.push(element.id)
match
element.view_node.place_overlays({
..context,
host_frame: placed.frame,
child_sizes: placed.children.map(child => child.frame.size),
state: element.control.view_state,
}) {
Some(pairs) => {
// Overrides must cover every overlay-positioned child: the re-place
// rebuilds the spine and would otherwise drop unchanged children back to
// their provisional frames.
let any_changed = pairs
.iter()
.any(pair => {
pair.0 < placed.children.length() &&
placed.children[pair.0].frame != pair.1
})
if any_changed {
out.push((element.id.value(), pairs))
for id in child_path {
spines.push(id)
}
}
if pairs.length() > 0 &&
element.view_node.keyboard_policy() is TopMostDismissal {
layers.push(element.id)
}
for pair in pairs {
if pair.0 < element.children.length() {
active_surfaces.push(element.children[pair.0].id)
}
}
}
None => ()
}
for index, child in element.children {
if index < placed.children.length() {
collect_overlay_overrides(
child,
placed.children[index],
context,
child_path,
out,
spines,
layers,
active_surfaces,
)
}
}
}
///|
fn RuntimeState::render_current_tree(self : RuntimeState) -> Unit {
let previous_render = self.render_tree
let dirty_ids = self.dirty.dirty_paint_elements.to_array()
let force_full_reason = match self.dirty.full_damage_reason {
Some(reason) => Some(reason)
None =>
if previous_render.root.element_id.value() == 0 {
Some("initial frame")
} else {
None
}
}
self.ensure_layout_tree()
self.stats.paint_pass_count = self.stats.paint_pass_count + 1
self.render_tree = {
root: self.element_tree.root.render_focused_with_text_system(
self.layout_tree.root,
focused=self.animfocus.focused_element,
environment=self.environment,
text_system=self.text_system,
now_ms=self.animfocus.now_ms,
overlay_enter=self.overlay_enter_times,
),
}
// Fold the per-subtree animation flag up to the root: if any painted node
// reported a mid-animation sample, keep the host ticking redraws until the
// animation settles. Controls clear this once progress reaches 1.0. Exit
// ghosts are command snapshots without render nodes, so their in-flight
// fades must be latched here explicitly or they freeze mid-fade.
self.animfocus.active_animations = self.render_tree.root.animating ||
!self.exiting_overlays.is_empty()
// If an animation is still in flight, request another frame right after the
// paint that observed it. This is evaluated after `active_animations` is
// updated, so once a transition settles the flag flips false and the redraw
// loop stops naturally instead of ticking one extra frame forever.
if self.animfocus.active_animations {
self.dirty.needs_redraw = true
match self.redraw_callback {
Some(callback) => callback()
None => ()
}
}
let damage = self.compute_damage_region(
previous_render,
self.render_tree,
dirty_ids,
force_full_reason~,
)
self.stats.last_damage_region = damage
// Accumulate across paint passes: events can force a paint
// (`ensure_render_tree`) long before the host builds the next frame, and
// each pass's damage describes only what that pass re-painted. The frame
// must carry the union of everything painted since the last frame, or the
// renderer clips away earlier changes and stale pixels ghost through.
self.dirty.accumulated_damage = self.dirty.accumulated_damage.union_with(
damage,
surface=Rect::new(
x=0.0,
y=0.0,
width=self.size.width,
height=self.size.height,
),
)
self.dirty.full_damage_reason = None
self.dirty.needs_rebuild = false
self.dirty.needs_layout = false
self.dirty.needs_paint = false
self.dirty.dirty_paint_elements.clear()
self.refresh_dirty_element_queue()
if self.dirty.needs_platform {
self.refresh_platform_tree()
}
}
///|
fn RuntimeState::compute_damage_region(
self : RuntimeState,
previous : RenderTree,
current : RenderTree,
dirty_ids : Array[ElementId],
force_full_reason~ : String?,
) -> DamageRegion {
match force_full_reason {
Some(reason) => DamageRegion::full(reason)
None => {
if dirty_ids.is_empty() {
return DamageRegion::full("dirty bounds unavailable")
}
// Every id in the dirty set was marked paint-dirty on itself, so its
// own paint output may change anywhere inside its bounds. Descendant
// rects never cover that: a scroll offset that an ancestor re-paints
// moves content across the whole subtree, and clearing only the leaf
// rects would leave previous-frame pixels (e.g. a scrolled image)
// ghosting outside them. Union each id's previous and current damage
// bounds unconditionally. One pass per tree keeps the index build O(n)
// regardless of the number of dirty ids.
let previous_index = DamageIndex::build(previous.root)
let current_index = DamageIndex::build(current.root)
let surface = Rect::new(
x=0.0,
y=0.0,
width=self.size.width,
height=self.size.height,
)
let rects : Array[Rect] = []
let mut missing_bounds = false
for id in dirty_ids {
let mut found_bounds = false
match previous_index.damage_bounds(id) {
Some(bounds) => {
rects.push(bounds)
found_bounds = true
}
None => ()
}
match current_index.damage_bounds(id) {
Some(bounds) => {
rects.push(bounds)
found_bounds = true
}
None => ()
}
if !found_bounds {
missing_bounds = true
}
}
if missing_bounds || rects.is_empty() {
DamageRegion::full("dirty bounds unavailable")
} else {
DamageRegion::from_rects(rects, surface)
}
}
}
}
///|
fn RuntimeState::queue_dirty_element(
self : RuntimeState,
id : ElementId,
) -> Unit {
self.dirty.dirty_elements.push(id)
}
///|
fn RuntimeState::queue_build_dirty(self : RuntimeState, id : ElementId) -> Unit {
self.dirty.dirty_build_elements.push(id)
}
///|
fn RuntimeState::queue_layout_dirty(
self : RuntimeState,
id : ElementId,
) -> Unit {
self.dirty.dirty_layout_elements.push(id)
}
///|
fn RuntimeState::queue_paint_dirty(self : RuntimeState, id : ElementId) -> Unit {
self.dirty.dirty_paint_elements.push(id)
}
///|
fn RuntimeState::queue_semantics_dirty(
self : RuntimeState,
id : ElementId,
) -> Unit {
self.dirty.dirty_semantics_elements.push(id)
}
///|
fn RuntimeState::queue_platform_dirty(
self : RuntimeState,
id : ElementId,
) -> Unit {
self.dirty.dirty_platform_elements.push(id)
}
///|
fn RuntimeState::mark_build_dirty(self : RuntimeState, id : ElementId) -> Unit {
self.mark_visual_work()
self.queue_dirty_element(id)
self.queue_build_dirty(id)
self.queue_layout_dirty(id)
self.queue_paint_dirty(id)
self.queue_semantics_dirty(id)
self.queue_platform_dirty(id)
self.dirty.needs_rebuild = true
self.dirty.needs_layout = true
self.dirty.needs_paint = true
self.dirty.needs_semantics = true
self.dirty.needs_platform = true
self.dirty.needs_redraw = true
}
///|
fn RuntimeState::mark_layout_dirty(self : RuntimeState, id : ElementId) -> Unit {
self.mark_visual_work()
self.queue_dirty_element(id)
self.queue_layout_dirty(id)
self.queue_paint_dirty(id)
self.queue_semantics_dirty(id)
self.queue_platform_dirty(id)
self.dirty.needs_layout = true
self.dirty.needs_paint = true
self.dirty.needs_semantics = true
self.dirty.needs_platform = true
self.dirty.needs_redraw = true
}
///|
fn RuntimeState::mark_dirty(self : RuntimeState, id : ElementId) -> Unit {
self.mark_visual_work()
self.queue_dirty_element(id)
self.queue_paint_dirty(id)
ignore(self.element_tree.root.mark_paint_dirty_by_id(id))
self.dirty.needs_paint = true
self.dirty.needs_redraw = true
}
///|
fn RuntimeState::mark_semantics_dirty(
self : RuntimeState,
id : ElementId,
) -> Unit {
self.queue_semantics_dirty(id)
ignore(self.element_tree.root.mark_semantics_dirty_by_id(id))
self.dirty.needs_semantics = true
}
///|
fn RuntimeState::mark_platform_dirty(
self : RuntimeState,
id : ElementId,
) -> Unit {
self.queue_platform_dirty(id)
ignore(self.element_tree.root.mark_platform_dirty_by_id(id))
self.dirty.needs_platform = true
}
///|
fn RuntimeState::mark_current_dirty_elements(self : RuntimeState) -> Unit {
let ids = DirtyIdSet::new()
let build_ids = DirtyIdSet::new()
let layout_ids = DirtyIdSet::new()
let paint_ids = DirtyIdSet::new()
let semantics_ids = DirtyIdSet::new()
let platform_ids = DirtyIdSet::new()
self.element_tree.root.collect_dirty_element_ids_by_kind(
ids, build_ids, layout_ids, paint_ids, semantics_ids, platform_ids,
)
if ids.is_empty() {
self.mark_dirty(self.element_tree.id())
} else {
for id in ids.items {
self.queue_dirty_element(id)
}
if !build_ids.is_empty() {
for id in build_ids.items {
self.dirty.dirty_build_elements.push(id)
}
self.dirty.needs_rebuild = true
}
if !layout_ids.is_empty() {
self.mark_visual_work()
for id in layout_ids.items {
self.dirty.dirty_layout_elements.push(id)
}
self.dirty.needs_layout = true
}
for id in paint_ids.items {
self.dirty.dirty_paint_elements.push(id)
}
for id in semantics_ids.items {
self.mark_semantics_dirty(id)
}
for id in platform_ids.items {
self.mark_platform_dirty(id)
}
if !paint_ids.is_empty() {
self.mark_visual_work()
self.dirty.needs_paint = true
self.dirty.needs_redraw = true
}
if !semantics_ids.is_empty() {
self.dirty.needs_semantics = true
}
}
}
///|
fn RuntimeState::dirty_count(self : RuntimeState) -> Int {
self.dirty.dirty_elements.length()
}
///|
fn RuntimeState::dirty_element_ids(self : RuntimeState) -> Array[ElementId] {
self.dirty.dirty_elements.to_array()
}
///|
fn RuntimeState::refresh_dirty_element_queue(self : RuntimeState) -> Unit {
self.dirty.dirty_elements.clear()
for id in self.dirty.dirty_build_elements.items {
self.dirty.dirty_elements.push(id)
}
for id in self.dirty.dirty_layout_elements.items {
self.dirty.dirty_elements.push(id)
}
for id in self.dirty.dirty_paint_elements.items {
self.dirty.dirty_elements.push(id)
}
for id in self.dirty.dirty_semantics_elements.items {
self.dirty.dirty_elements.push(id)
}
for id in self.dirty.dirty_platform_elements.items {
self.dirty.dirty_elements.push(id)
}
}
///|
/// Overlay surface lifecycle (ADR 0033): surfaces that disappeared since the
/// previous pass are retained as fading command snapshots (exit); surfaces
/// that just appeared get an enter fade driven by the frame clock through the
/// render pass (see `RenderNode.render_opacity`).
fn RuntimeState::track_overlay_surface_lifecycle(
self : RuntimeState,
active_surfaces : Array[ElementId],
) -> Unit {
let now_ms = self.animfocus.now_ms
// Exit retention: capture removed surfaces from the previous render tree.
for id in self.active_surface_ids {
let still_active = active_surfaces.any(active => active == id)
if !still_active {
match self.render_tree.root.find_render_node(id) {
Some(node) => {
let commands : Array[DrawCommand] = []
node.collect_draw_commands(commands)
if commands.length() > 0 {
self.exiting_overlays.push({
commands,
started_ms: now_ms,
duration_ms: self.overlay_transition_duration_ms(),
})
self.dirty.full_damage_reason = Some("overlay exited")
}
}
None => ()
}
}
}
self.active_surface_ids = active_surfaces.copy()
// Enter fade bookkeeping: record first-seen time. Completed entries stay
// registered so an active surface is never re-recorded (which would restart
// its fade every placement pass and make it flicker); entries are dropped
// only when their surface disappears.
let mut index = 0
while index < self.overlay_enter_times.length() {
let entry = self.overlay_enter_times[index]
let still_active = active_surfaces.any(active => active == entry.0)
if !still_active {
ignore(self.overlay_enter_times.remove(index))
} else {
index = index + 1
}
}
for id in active_surfaces {
let known = self.overlay_enter_times.any(entry => entry.0 == id)
if !known {
self.overlay_enter_times.push((id, now_ms))
}
}
let any_fading = self.overlay_enter_times.any(entry => {
now_ms - entry.1 < self.overlay_transition_duration_ms()
})
if any_fading {
self.animfocus.active_animations = true
}
}
///|
/// Duration of overlay enter/exit fades, resolved from the environment's
/// resolved theme motion tier. Reduced-motion environments complete
/// immediately (duration 0), matching the core animation primitives.
fn RuntimeState::overlay_transition_duration_ms(self : RuntimeState) -> Double {
if self.environment.reduced_motion {
return 0.0
}
self.environment.theme.motion.normal_ms
}