///|
using @core {optional_rect_union}
///|
fn ElementNode::render_with_environment(
self : ElementNode,
placed : PlacedNode,
focused : ElementId?,
environment : RenderEnvironment,
overlay_enter? : Array[(ElementId, Double)] = [],
) -> RenderNode {
let bounds = placed.frame
// An in-flight enter fade changes the subtree's opacity every frame; the
// render cache is keyed on frame/focused only, so a fading node must bypass
// it (read and write) or the first faded frame is served forever.
let enter_fading = overlay_enter_opacity(
overlay_enter,
self.id,
environment.now_ms,
overlay_fade_duration_ms(environment),
)
is Some(_)
if environment.matches_default_cache_key() &&
!enter_fading &&
!self.has_dirty_paint() &&
!self.has_dirty_layout() &&
self.render_cache.cached_render_frame == Some(bounds) &&
self.render_cache.cached_render_focused == focused {
match self.render_cache.cached_render {
Some(render) => {
self.render_cache.hit_count = self.render_cache.hit_count + 1
return render
}
None => ()
}
}
self.render_cache.miss_count = self.render_cache.miss_count + 1
let children : Array[RenderNode] = []
let commands : Array[DrawCommand] = []
let post_commands : Array[DrawCommand] = []
let overlay_commands : Array[DrawCommand] = []
let plan = if !enter_fading &&
environment.matches_default_cache_key() &&
!self.dirty.needs_paint &&
self.render_cache.cached_render_frame == Some(bounds) &&
self.render_cache.cached_render_focused == focused {
match self.render_cache.cached_plan {
Some(plan) => plan
None =>
self.view_node.paint({
frame: bounds,
child_frames: placed.children.map(child => child.frame),
focused: focused == Some(self.id),
state: self.control.view_context(focused == Some(self.id)),
text_control: self.control.text_context(),
environment: render_environment_to_view_environment(environment),
style: environment.style,
text_system: environment.text_system,
now_ms: environment.now_ms,
})
}
} else {
self.view_node.paint({
frame: bounds,
child_frames: placed.children.map(child => child.frame),
focused: focused == Some(self.id),
state: self.control.view_context(focused == Some(self.id)),
text_control: self.control.text_context(),
environment: render_environment_to_view_environment(environment),
style: environment.style,
text_system: environment.text_system,
now_ms: environment.now_ms,
})
}
commands.append(plan.commands)
for layer in plan.child_layers {
children.push(view_paint_layer_render_node(self.id, layer))
}
post_commands.append(plan.post_commands)
overlay_commands.append(plan.overlay_commands)
// Child hit regions come from the placed tree (frame ∩ accumulated clip);
// hoisted overlay children escape ancestor clips entirely, so no paint-time
// clip trimming is needed here.
let child_environment = environment.with_child_context(self.view_node)
for index, child in self.children {
if index < placed.children.length() {
let rendered = child.render_with_environment(
placed.children[index],
focused,
child_environment,
overlay_enter~,
)
let rendered = {
..rendered,
is_overlay_child: placed.overlay_child_indices.contains(index),
}
children.push(rendered)
}
}
self.dirty.needs_paint = false
self.refresh_descendant_dirty_flags()
self.render_cache.content_revision = self.render_cache.content_revision + 1
let paint_bounds = render_node_paint_bounds(
bounds, commands, children, post_commands, overlay_commands,
)
// A subtree is animating if this node's own paint reported a mid-animation
// sample or any rendered child did. Folded up so the runtime only needs to
// check the root to keep redraws ticking.
let child_animating = children.iter().any(child => child.animating)
let animating = plan.animating || child_animating || enter_fading
let render = {
element_id: self.id,
frame: bounds,
children,
is_overlay_child: false,
paint_bounds,
hit_rect: Some(placed.hit_rect),
render_opacity: overlay_enter_opacity(
overlay_enter,
self.id,
environment.now_ms,
overlay_fade_duration_ms(environment),
),
content_revision: self.render_cache.content_revision,
is_repaint_boundary: false,
layer_cache_key: None,
paint_commands: commands,
post_paint_commands: post_commands,
overlay_paint_commands: overlay_commands,
cached_draw_commands: None,
cached_overlay_draw_commands: None,
animating,
}
let render = if plan.repaint_boundary {
render.with_repaint_boundary_cache()
} else {
render
}
// A fading node must not persist into the cache: its opacity changes every
// frame while its frame/key stays identical.
if environment.matches_default_cache_key() && !enter_fading {
self.render_cache.cached_render_frame = Some(bounds)
self.render_cache.cached_render_focused = focused
self.render_cache.cached_plan = Some(plan)
self.render_cache.cached_render = Some(render)
}
render
}
///|
fn view_paint_layer_render_node(
element_id : ElementId,
layer : @core.ViewPaintLayer,
) -> RenderNode {
let paint_bounds = render_node_paint_bounds(
layer.frame,
layer.commands,
[],
[],
layer.overlay_commands,
)
{
element_id,
frame: layer.frame,
children: [],
is_overlay_child: false,
paint_bounds,
hit_rect: None,
render_opacity: None,
content_revision: layer.content_revision,
is_repaint_boundary: true,
// An empty cache key opts the layer out of the retained-layer cache: its
// commands are painted fresh every frame (e.g. a viewport-culled block
// whose raster would depend on scroll position). Everything else retains
// under its key.
layer_cache_key: if layer.cache_key == "" {
None
} else {
Some(layer.cache_key)
},
paint_commands: layer.commands,
post_paint_commands: [],
overlay_paint_commands: layer.overlay_commands,
cached_draw_commands: Some(layer.commands),
cached_overlay_draw_commands: Some(layer.overlay_commands),
animating: false,
}
}
///|
fn render_node_paint_bounds(
frame : Rect,
commands : Array[DrawCommand],
children : Array[RenderNode],
post_commands : Array[DrawCommand],
overlay_commands : Array[DrawCommand],
) -> Rect {
let mut bounds : Rect? = Some(frame)
for command in commands {
bounds = optional_rect_union(bounds, command.bounds())
}
for child in children {
bounds = optional_rect_union(bounds, Some(child.paint_bounds))
}
for command in post_commands {
bounds = optional_rect_union(bounds, command.bounds())
}
for command in overlay_commands {
bounds = optional_rect_union(bounds, command.bounds())
}
match bounds {
Some(rect) => rect
None => frame
}
}
///|
/// Overlay enter/exit fade duration resolved from the paint environment:
/// reduced-motion completes immediately, otherwise the resolved theme's
/// normal motion tier (same source as `RuntimeState::overlay_transition_duration_ms`).
fn overlay_fade_duration_ms(environment : RenderEnvironment) -> Double {
if environment.reduced_motion {
return 0.0
}
environment.environment.theme.motion.normal_ms
}
///|
/// Enter-fade opacity for an overlay surface: 1.0 once the fade completed (or
/// when the surface is not tracked). A zero duration (reduced motion) always
/// takes the terminal branch.
fn overlay_enter_opacity(
overlay_enter : Array[(ElementId, Double)],
element_id : ElementId,
now_ms : Double,
duration_ms : Double,
) -> Double? {
let mut started_ms : Double? = None
for entry in overlay_enter {
if entry.0 == element_id {
started_ms = Some(entry.1)
}
}
match started_ms {
Some(started) => {
let elapsed = now_ms - started
if elapsed >= duration_ms {
None
} else {
Some((elapsed / duration_ms).clamp(min=0.0, max=1.0))
}
}
None => None
}
}