///|
/// Global counter for generating unique node IDs
let node_uid_counter : Ref[Int] = { val: 0 }
///|
/// Generate a new unique node ID
fn next_uid() -> Int {
let uid = node_uid_counter.val
node_uid_counter.val = uid + 1
uid
}
///|
/// A node in the layout tree
pub struct Node {
id : String
mut uid : Int // Unique identifier for caching (stabilized across re-renders for incremental reflow)
style : @style.Style
children : Array[Node]
measure : @layout_types.MeasureFunc?
text : String? // Text content for text nodes
src : String? // Image source for replaced image content
/// Originating `@dom.DomTree` node id, when this render node was built from a
/// live DOM (the dynamic-rendering path). `None` for the static parse path.
/// A stable, mutation-surviving identity used to key incremental reflow (see
/// docs/incremental-reflow-design.md); does not affect layout or cascade.
mut dom_id : Int?
}
///|
/// Attach the originating DomTree node id (dynamic-rendering path).
pub fn Node::set_dom_id(self : Node, dom_id : Int?) -> Unit {
self.dom_id = dom_id
}
///|
/// Overwrite the cache uid. Used to stabilize uids across re-renders so
/// incremental reflow can match a rebuilt node to its prior layout (see
/// `stabilize_uids`).
pub fn Node::set_uid(self : Node, uid : Int) -> Unit {
self.uid = uid
}
///|
pub fn Node::new(
id : String,
style : @style.Style,
children : Array[Node],
) -> Node {
{
id,
uid: next_uid(),
style,
children,
measure: None,
text: None,
src: None,
dom_id: None,
}
}
///|
/// Create a node with a specific uid (for incremental layout)
pub fn Node::with_uid(
id : String,
uid : Int,
style : @style.Style,
children : Array[Node],
) -> Node {
{
id,
uid,
style,
children,
measure: None,
text: None,
src: None,
dom_id: None,
}
}
///|
/// Create a node with a specific uid and measure function
pub fn Node::with_uid_and_measure(
id : String,
uid : Int,
style : @style.Style,
children : Array[Node],
measure : @layout_types.MeasureFunc?,
text : String?,
src? : String? = None,
) -> Node {
{ id, uid, style, children, measure, text, src, dom_id: None }
}
///|
pub fn Node::leaf(id : String, style : @style.Style) -> Node {
{
id,
uid: next_uid(),
style,
children: [],
measure: None,
text: None,
src: None,
dom_id: None,
}
}
///|
/// Create a leaf node with a custom measure function
/// Optional text parameter for alt text (images) or other content
pub fn Node::with_measure(
id : String,
style : @style.Style,
measure : @layout_types.MeasureFunc,
text? : String,
src? : String? = None,
) -> Node {
{
id,
uid: next_uid(),
style,
children: [],
measure: Some(measure),
text,
src,
dom_id: None,
}
}
///|
/// Create a text node with content
pub fn Node::text(
id : String,
style : @style.Style,
measure : @layout_types.MeasureFunc,
content : String,
) -> Node {
{
id,
uid: next_uid(),
style,
children: [],
measure: Some(measure),
text: Some(content),
src: None,
dom_id: None,
}
}
///|
/// Dispatch function type - passed through layout computation to enable
/// cross-layout-type dispatch without global state.
/// The function takes (node, context, dispatch) and returns Layout.
pub(all) struct DispatchFn(
(Node, @layout_types.LayoutContext, DispatchFn) -> @layout_types.Layout
)
///|
/// @deprecated Use DispatchFn instead
/// Layout dispatch function type (kept for backward compatibility with incremental compute)
pub(all) struct LayoutDispatchFunc(
(Node, @layout_types.LayoutContext) -> @layout_types.Layout
)
///|
/// Global layout dispatch function (for incremental compute caching)
let layout_dispatcher : Ref[LayoutDispatchFunc?] = { val: None }
///|
/// Set the global layout dispatcher (used by incremental compute for caching)
pub fn set_layout_dispatcher(f : LayoutDispatchFunc) -> Unit {
layout_dispatcher.val = Some(f)
}
///|
/// Get the global layout dispatcher
pub fn get_layout_dispatcher() -> LayoutDispatchFunc? {
layout_dispatcher.val
}
///|
/// Global "is this node's whole subtree unchanged since the last layout?"
/// predicate, keyed by `uid`. Installed by the incremental layout driver
/// (`compute_tree_incremental`) over the `LayoutTree`'s dirty state, and read by
/// the block-flow memoization in `layout/block` to decide whether a clean child
/// subtree can be served from its cached `compute_with_collapse` result.
///
/// Dependency-inverted on purpose: `layout/block` cannot depend on `layout/tree`
/// (the dispatcher cycle), so the cleanliness signal flows through this core hook
/// the same way the layout dispatcher does. Absent an installed predicate (the
/// non-incremental / full-layout path) it returns `false`, so memoization is off
/// by default and the static path is byte-for-byte unchanged.
let node_clean_predicate : Ref[((Int) -> Bool)?] = { val: None }
///|
/// Install the subtree-clean predicate for the duration of an incremental layout.
/// Pass `None` (via `clear_node_clean_predicate`) to remove it afterwards.
pub fn set_node_clean_predicate(f : (Int) -> Bool) -> Unit {
node_clean_predicate.val = Some(f)
}
///|
/// Remove the subtree-clean predicate (back to the conservative default where
/// every node reports not-clean, disabling block-flow memoization).
pub fn clear_node_clean_predicate() -> Unit {
node_clean_predicate.val = None
}
///|
/// Whether the node with this `uid` has an entirely unchanged subtree since the
/// last layout, per the installed predicate. `false` when no predicate is
/// installed (the safe default: recompute).
pub fn node_is_clean(uid : Int) -> Bool {
match node_clean_predicate.val {
Some(pred) => pred(uid)
None => false
}
}
///|
/// Whether an incremental layout is in progress (the clean predicate is
/// installed). Block-flow memoization populates its cache only while this is
/// true, so the static / full-layout path neither reads nor writes it.
pub fn node_incremental_active() -> Bool {
node_clean_predicate.val is Some(_)
}
///|
/// Create a minimal fallback layout for a node (used when dispatch is unavailable)
pub fn fallback_layout(
node : Node,
ctx : @layout_types.LayoutContext,
) -> @layout_types.Layout {
let width = match node.style.width {
@types.Dimension::Length(w) => w
@types.Dimension::Percent(p) => ctx.available_width * p
@types.Dimension::Auto => ctx.available_width
@types.Dimension::MinContent => 0.0
@types.Dimension::MaxContent => ctx.available_width
@types.Dimension::FitContent(_) => ctx.available_width
@types.Dimension::Calc(px, pct) => ctx.available_width * pct + px
@types.Dimension::MathFn(op, terms) =>
@types.apply_math_op(
op,
terms.map(fn(t) { t.0 + t.1 * ctx.available_width }),
)
}
let height = match node.style.height {
@types.Dimension::Length(h) => h
@types.Dimension::Percent(p) => ctx.available_height.unwrap_or(0.0) * p
@types.Dimension::Auto => 0.0
@types.Dimension::MinContent => 0.0
@types.Dimension::MaxContent => 0.0
@types.Dimension::FitContent(_) => 0.0
@types.Dimension::Calc(px, pct) =>
ctx.available_height.unwrap_or(0.0) * pct + px
@types.Dimension::MathFn(op, terms) =>
@types.apply_math_op(
op,
terms.map(fn(t) { t.0 + t.1 * ctx.available_height.unwrap_or(0.0) }),
)
}
{
id: node.id,
x: 0.0,
y: 0.0,
width,
height,
margin: @types.Rect::zero(),
padding: @types.Rect::zero(),
border: @types.Rect::zero(),
overflow_x: @types.Visible,
overflow_y: @types.Visible,
scroll_width: width,
scroll_height: height,
children: [],
text: node.text,
}
}
///|
/// Allocation-free prefix test. `String::has_prefix` in core uses Boyer-Moore-
/// Horspool and allocates a skip table per call; layout calls it per node (text
/// detection via `id.has_prefix("#text")`), which made it the single largest
/// allocation site in the render pipeline. This compares the prefix directly.
pub fn str_has_prefix(s : String, prefix : String) -> Bool {
let plen = prefix.length()
if plen == 0 {
return true
}
if s.length() < plen {
return false
}
for i = 0; i < plen; i = i + 1 {
if s[i].to_int() != prefix[i].to_int() {
return false
}
}
true
}