///|
/// LayoutTree - Manages layout nodes with parent references and dirty propagation
///|
/// Layout tree with parent tracking for dirty propagation
pub struct LayoutTree {
root : LayoutNode
// Map from node uid to parent uid (root has no entry)
parent_map : Map[Int, Int]
// Map from uid to node for fast lookup
node_map : Map[Int, LayoutNode]
// Viewport size
mut viewport_width : Double
mut viewport_height : Double
// Resource management for intrinsic-sized elements
resources : ResourceRegistry
}
///|
/// Build parent and node maps recursively (without setting callbacks)
fn build_maps_internal(
node : LayoutNode,
parent_uid : Int?,
parent_map : Map[Int, Int],
node_map : Map[Int, LayoutNode],
) -> Unit {
node_map[node.uid] = node
match parent_uid {
Some(pid) => parent_map[node.uid] = pid
None => ()
}
for child in node.children {
build_maps_internal(child, Some(node.uid), parent_map, node_map)
}
}
///|
pub fn LayoutTree::new(
root : LayoutNode,
viewport_width : Double,
viewport_height : Double,
) -> LayoutTree {
let parent_map : Map[Int, Int] = {}
let node_map : Map[Int, LayoutNode] = {}
build_maps_internal(root, None, parent_map, node_map)
let tree : LayoutTree = {
root,
parent_map,
node_map,
viewport_width,
viewport_height,
resources: ResourceRegistry::new(),
}
// Set dirty callback on all nodes for proper parent propagation
tree.setup_dirty_callbacks()
tree
}
///|
/// Set up dirty callbacks on all nodes to propagate children_dirty to parents
fn LayoutTree::setup_dirty_callbacks(self : LayoutTree) -> Unit {
self.node_map.each(fn(_uid, node) {
node.on_dirty = Some(fn(uid) { self.propagate_children_dirty(uid) })
})
}
///|
/// Create tree from immutable Node
pub fn LayoutTree::from_node(
node : @node.Node,
viewport_width : Double,
viewport_height : Double,
) -> LayoutTree {
let root = LayoutNode::from_node(node)
LayoutTree::new(root, viewport_width, viewport_height)
}
///|
/// Find a node by uid
pub fn LayoutTree::find_node(self : LayoutTree, uid : Int) -> LayoutNode? {
self.node_map.get(uid)
}
///|
/// Find a node by string id
pub fn LayoutTree::find_node_by_id(
self : LayoutTree,
node_id : String,
) -> LayoutNode? {
let mut result : LayoutNode? = None
self.node_map.each(fn(_uid, node) {
if node.id == node_id {
result = Some(node)
}
})
match result {
Some(_) => result
None => {
self.node_map.each(fn(_uid, node) {
match node.id.find("#") {
Some(idx) => {
let source_id = node.id.unsafe_substring(
start=idx + 1,
end=node.id.length(),
)
if source_id == node_id {
result = Some(node)
}
}
None => ()
}
})
result
}
}
}
///|
/// Get the resource registry for external management
/// This allows decoupling resource management from LayoutTree
pub fn LayoutTree::get_resources(self : LayoutTree) -> ResourceRegistry {
self.resources
}
///|
/// Get parent of a node
pub fn LayoutTree::get_parent(self : LayoutTree, uid : Int) -> LayoutNode? {
match self.parent_map.get(uid) {
Some(parent_uid) => self.node_map.get(parent_uid)
None => None
}
}
///|
/// Propagate children_dirty flag up to root
/// CSS Containment: contain: layout stops propagation since internal layout
/// changes don't affect ancestors
fn LayoutTree::propagate_children_dirty(
self : LayoutTree,
from_uid : Int,
) -> Unit {
let mut current_uid = from_uid
let mut done = false
while !done {
match self.parent_map.get(current_uid) {
Some(parent_uid) =>
match self.node_map.get(parent_uid) {
Some(parent) =>
// CSS Containment optimization: contain: layout isolates internal layout
// When a node has layout containment, its internal changes don't affect
// the layout of its ancestors, so we can stop dirty propagation here
if parent.style.contain.layout {
// Still mark this parent as children_dirty, but don't propagate further
parent.mark_children_dirty()
done = true
} else if parent.children_dirty {
done = true
} else {
parent.mark_children_dirty()
current_uid = parent_uid
}
None => done = true
}
None => done = true
}
}
}
///|
/// Mark a node as dirty and propagate to ancestors
pub fn LayoutTree::mark_node_dirty(self : LayoutTree, uid : Int) -> Unit {
match self.node_map.get(uid) {
Some(node) => {
node.mark_dirty()
self.propagate_children_dirty(uid)
}
None => ()
}
}
///|
/// Update viewport size and mark dependent nodes dirty
pub fn LayoutTree::resize_viewport(
self : LayoutTree,
width : Double,
height : Double,
) -> Unit {
let width_changed = (self.viewport_width - width).abs() > 0.001
let height_changed = (self.viewport_height - height).abs() > 0.001
self.viewport_width = width
self.viewport_height = height
// Only mark nodes that depend on viewport dimensions
if width_changed || height_changed {
mark_viewport_dependents_dirty(self.root, width_changed, height_changed)
}
}
///|
/// Recursively mark nodes dirty if they depend on viewport dimensions
fn mark_viewport_dependents_dirty(
node : LayoutNode,
width_changed : Bool,
height_changed : Bool,
) -> Unit {
let deps = node.dependencies
// Check if this node needs to be marked dirty
let needs_dirty = match deps {
// Static nodes never need re-layout from viewport changes
Static => false
// Viewport-dependent nodes (vw/vh) - always dirty on viewport change
Viewport => true
// For root-level nodes, parent dimensions come from viewport
// ParentWidth/ParentHeight/ParentBoth depend on parent, which at root = viewport
ParentWidth => width_changed
ParentHeight => height_changed
ParentBoth => width_changed || height_changed
// Intrinsic nodes may need re-layout if children change
Intrinsic => false
}
if needs_dirty {
node.mark_dirty()
}
// Always recurse to children - they may have viewport dependencies
for child in node.children {
mark_viewport_dependents_dirty(child, width_changed, height_changed)
}
}
///|
/// Check if any layout is needed
pub fn LayoutTree::needs_layout(self : LayoutTree) -> Bool {
self.root.needs_layout()
}
///|
/// Clear all dirty flags in tree (after full layout)
fn clear_all_dirty(node : LayoutNode) -> Unit {
node.clear_dirty()
for child in node.children {
clear_all_dirty(child)
}
}
///|
/// Compute layout using existing layout system (full recomputation)
pub fn LayoutTree::compute_full(self : LayoutTree) -> @layout_types.Layout {
// Reset intrinsic cache for full recomputation
@dispatch.reset_intrinsic_cache()
let node = self.root.to_node()
let ctx : @layout_types.LayoutContext = {
available_width: self.viewport_width,
available_height: Some(self.viewport_height),
sizing_mode: @layout_types.Definite,
viewport_width: self.viewport_width,
viewport_height: self.viewport_height,
stretch_width: false,
stretch_height: false,
}
let result = @dispatch.compute_layout(node, ctx)
clear_all_dirty(self.root)
result
}
///|
/// Compute layout with incremental optimization
/// Uses per-node caching for better performance
pub fn LayoutTree::compute_incremental(
self : LayoutTree,
) -> @layout_types.Layout {
let stats = CacheStats::new()
compute_tree_incremental(self, stats)
}
// =============================================================================
// Resource Management API
// =============================================================================
///|
/// Register a pending resource for an intrinsic-sized element (e.g., image)
/// Returns a ResourceId that can be used to resolve the size later
pub fn LayoutTree::register_resource(
self : LayoutTree,
node_uid : Int,
placeholder_width? : Double = 300.0,
placeholder_height? : Double = 150.0,
) -> ResourceId {
let rid = self.resources.next_resource_id()
self.resources.add_node(rid, node_uid)
self.resources.mark_pending(rid)
// Set up the node with placeholder measure
match self.node_map.get(node_uid) {
Some(node) => {
node.intrinsic_state = Some(
Pending(placeholder_width~, placeholder_height~),
)
node.measure = Some(
create_fixed_measure(placeholder_width, placeholder_height),
)
node.mark_dirty()
self.propagate_children_dirty(node_uid)
}
None => ()
}
rid
}
///|
/// Attach an additional node to an existing resource (for shared resources like same src)
pub fn LayoutTree::attach_resource(
self : LayoutTree,
rid : ResourceId,
node_uid : Int,
placeholder_width? : Double = 300.0,
placeholder_height? : Double = 150.0,
) -> Unit {
self.resources.add_node(rid, node_uid)
// Set up the node with placeholder measure
match self.node_map.get(node_uid) {
Some(node) => {
node.intrinsic_state = Some(
Pending(placeholder_width~, placeholder_height~),
)
node.measure = Some(
create_fixed_measure(placeholder_width, placeholder_height),
)
node.mark_dirty()
self.propagate_children_dirty(node_uid)
}
None => ()
}
}
///|
/// Resolve a resource with its actual size
pub fn LayoutTree::resolve_resource(
self : LayoutTree,
rid : ResourceId,
width : Double,
height : Double,
) -> Unit {
self.resources.mark_resolved(rid)
let aspect = if height > 0.0 { Some(width / height) } else { None }
for node_uid in self.resources.get_nodes(rid) {
match self.node_map.get(node_uid) {
Some(node) => {
node.intrinsic_state = Some(Resolved(width~, height~))
node.measure = Some(create_fixed_measure(width, height))
// Only set aspect-ratio if not already specified by CSS
match node.style.aspect_ratio {
None => node.style.aspect_ratio = aspect
Some(_) => () // CSS takes precedence
}
node.mark_dirty()
self.propagate_children_dirty(node_uid)
}
None => ()
}
}
}
///|
/// Mark a resource as failed (e.g., image load error)
pub fn LayoutTree::fail_resource(self : LayoutTree, rid : ResourceId) -> Unit {
self.resources.mark_resolved(rid)
for node_uid in self.resources.get_nodes(rid) {
match self.node_map.get(node_uid) {
Some(node) => {
node.intrinsic_state = Some(Error)
node.measure = Some(
create_fixed_measure(
error_placeholder_width, error_placeholder_height,
),
)
node.mark_dirty()
self.propagate_children_dirty(node_uid)
}
None => ()
}
}
}
///|
/// Check if a resource is still pending
pub fn LayoutTree::is_resource_pending(
self : LayoutTree,
rid : ResourceId,
) -> Bool {
self.resources.is_pending(rid)
}
///|
/// Get all pending resources
pub fn LayoutTree::get_pending_resources(
self : LayoutTree,
) -> Array[ResourceId] {
self.resources.get_all_pending()
}
///|
/// Scan the tree for nodes with Pending intrinsic state and register them as resources
/// Returns a map from uid (as string) to ResourceId
/// This is useful after building a tree from HTML to register all pending images
pub fn LayoutTree::register_pending_images(
self : LayoutTree,
) -> Map[String, ResourceId] {
let uid_to_rid : Map[String, ResourceId] = {}
self.node_map.each(fn(_uid, node) {
match node.intrinsic_state {
Some(Pending(_)) => {
// This node needs a resource
let key = node.uid.to_string()
match uid_to_rid.get(key) {
Some(_) => () // Already registered
None => {
// New resource - register it
let rid = self.resources.next_resource_id()
self.resources.add_node(rid, node.uid)
self.resources.mark_pending(rid)
uid_to_rid[key] = rid
}
}
}
_ => ()
}
})
uid_to_rid
}
///|
/// Scan the tree for nodes with Pending intrinsic state and register them as resources
/// Uses provided src->uid mapping to share resources for same src
/// Returns a map from src to ResourceId
pub fn LayoutTree::register_pending_images_with_src(
self : LayoutTree,
uid_to_src : Map[Int, String],
) -> Map[String, ResourceId] {
let src_to_rid : Map[String, ResourceId] = {}
self.node_map.each(fn(uid, node) {
match node.intrinsic_state {
Some(Pending(_)) =>
// Get src for this node
match uid_to_src.get(uid) {
Some(src) =>
match src_to_rid.get(src) {
Some(rid) =>
// Already have a resource for this src, attach this node
self.resources.add_node(rid, node.uid)
None => {
// New resource - register it
let rid = self.resources.next_resource_id()
self.resources.add_node(rid, node.uid)
self.resources.mark_pending(rid)
src_to_rid[src] = rid
}
}
None => {
// No src mapping, use uid as key
let key = uid.to_string()
let rid = self.resources.next_resource_id()
self.resources.add_node(rid, node.uid)
self.resources.mark_pending(rid)
src_to_rid[key] = rid
}
}
_ => ()
}
})
src_to_rid
}
// =============================================================================
// Dirty Region Collection
// =============================================================================
///|
/// Collect bounding rects of all nodes that have new layouts
/// Returns absolute positions (relative to viewport origin)
/// Use this after calculate_layout() to get regions that need redrawing
pub fn LayoutTree::collect_dirty_rects(
self : LayoutTree,
) -> Array[@types.BoundingRect] {
let rects : Array[@types.BoundingRect] = []
collect_dirty_rects_recursive(self.root, 0.0, 0.0, rects)
rects
}
///|
/// Recursively collect dirty rects with absolute positioning
fn collect_dirty_rects_recursive(
node : LayoutNode,
offset_x : Double,
offset_y : Double,
rects : Array[@types.BoundingRect],
) -> Unit {
let abs_x = offset_x + node.computed_x
let abs_y = offset_y + node.computed_y
if node.has_new_layout {
rects.push(
@types.BoundingRect::new(
abs_x,
abs_y,
node.computed_width,
node.computed_height,
),
)
}
// Recurse to children
for child in node.children {
collect_dirty_rects_recursive(child, abs_x, abs_y, rects)
}
}
///|
/// Collect dirty rects and merge overlapping ones into a single union rect
/// More efficient when many small regions change
pub fn LayoutTree::collect_dirty_union(
self : LayoutTree,
) -> @types.BoundingRect? {
let rects = self.collect_dirty_rects()
if rects.length() == 0 {
return None
}
let mut union = rects[0]
for i = 1; i < rects.length(); i = i + 1 {
union = union.union(rects[i])
}
Some(union)
}
///|
/// Mark all nodes' layouts as seen (clear has_new_layout flags)
/// Call this after processing dirty regions
pub fn LayoutTree::mark_all_layouts_seen(self : LayoutTree) -> Unit {
mark_layouts_seen_recursive(self.root)
}
///|
fn mark_layouts_seen_recursive(node : LayoutNode) -> Unit {
node.has_new_layout = false
for child in node.children {
mark_layouts_seen_recursive(child)
}
}