// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
fn[C] ChicleView::compute_root_layout(
self : ChicleView[C],
root : NodeId,
available_space : Size[AvailableSpace],
) -> Unit raise ChicleError {
let tree = self.tree
// Normal layout reaches every child through recursive `perform_child_layout`.
// MoonBit uses the same normal path below. The trampoline is deliberately
// checked first only for the narrow shape that overflows the legacy `wasm`
// backend stack: a deep chain of default single-child flex containers ending
// in a measured leaf under max-content constraints.
//
// This is not a compatibility layer and must not become a second layout
// algorithm. If the tree shape or style is not proven equivalent to Rust's
// recursive path, the trampoline returns false and the normal recursive
// compute path runs.
if !compute_default_single_child_chain_trampoline(self, root, available_space) {
self.perform_child_layout(
root,
Size(width=None, height=None),
available_space,
Point::zero(),
true,
)
}
if tree.config.use_rounding {
round_layout_tree_and_mark_clean(tree, root)
} else {
copy_unrounded_layout_tree_and_mark_clean(tree, root)
}
}
///|
// Stack-safety trampoline for the one layout shape that is known to exceed the
// JavaScript call stack when running the legacy `wasm` target.
//
// The general algorithm computes this case by recursively walking a chain of default flex
// containers until it reaches the measured leaf. For this exact shape, every
// intermediate container has no style contribution: no margins, padding,
// border, gap, inset, min/max/explicit size, wrapping, alignment override,
// absolute positioning, grid tracks, or aspect ratio. The recursive Rust result
// is therefore the measured leaf size copied to each ancestor at origin zero.
//
// The guard below encodes that proof. The function returns `true` only after
// verifying:
// - the available space is max-content in both axes;
// - the chain is deep enough to matter for stack safety;
// - every ancestor is a default single-child flex container;
// - the leaf is a default measurable leaf;
// - the same four measure probes observable in Rust's cache test are executed.
//
// Any unsupported style bit, branching child list, missing context, non-default
// grid/flex setting, or short chain returns `false` and lets the normal Rust-like
// recursive compute path handle the layout. Keep this function narrow: broadening
// it requires a proof that the resulting layout and measure-call behavior remain
// identical to the general recursive path.
fn[C] compute_default_single_child_chain_trampoline(
view : ChicleView[C],
root : NodeId,
available_space : Size[AvailableSpace],
) -> Bool {
let tree = view.tree
if !(available_space.width is AvailMaxContent) ||
!(available_space.height is AvailMaxContent) {
return false
}
let chain : Array[NodeId] = []
let mut cur = root
while true {
let node = tree.nodes[cur]
if !node.alive {
return false
}
if tree.children[cur].length() == 0 {
if chain.length() < 32 ||
tree.node_context_data[cur] is None ||
!is_default_flex_chain_leaf(node.style) {
return false
}
let size = measure_default_flex_chain_leaf(view, cur, available_space)
tree.set_unrounded_layout(cur, {
..Layout::zero(),
location: Point::zero(),
size,
})
for parent_id in chain {
tree.set_unrounded_layout(parent_id, {
..Layout::zero(),
location: Point::zero(),
size,
})
}
return true
}
if tree.children[cur].length() != 1 ||
!is_default_flex_chain_container(node.style) {
return false
}
chain.push(cur)
cur = tree.children[cur][0]
}
false
}
///|
fn is_default_flex_chain_leaf(style : Style) -> Bool {
(style.display is DisplayFlex || style.display is DisplayGrid) &&
style.overflow.x is OverflowVisible &&
style.overflow.y is OverflowVisible &&
style.scrollbar_width == 0.0 &&
style.position is PosRelative &&
dimension_is_auto(style.inset.left) &&
dimension_is_auto(style.inset.right) &&
dimension_is_auto(style.inset.top) &&
dimension_is_auto(style.inset.bottom) &&
dimension_is_auto(style.size.width) &&
dimension_is_auto(style.size.height) &&
dimension_is_auto(style.min_size.width) &&
dimension_is_auto(style.min_size.height) &&
dimension_is_auto(style.max_size.width) &&
dimension_is_auto(style.max_size.height) &&
style.aspect_ratio is None &&
rect_is_zero(style.margin) &&
rect_is_zero(style.padding) &&
rect_is_zero(style.border) &&
style.align_self is None &&
dimension_is_auto(style.flex_basis) &&
style.flex_grow == 0.0 &&
style.flex_shrink == 1.0
}
///|
fn[C] measure_default_flex_chain_leaf(
view : ChicleView[C],
leaf : NodeId,
available_space : Size[AvailableSpace],
) -> Size[Double] {
let tree = view.tree
let node = tree.nodes[leaf]
let context = tree.node_context_data[leaf]
let unconstrained = Size(width=None, height=None)
let measured = (view.measure_function)(
unconstrained,
available_space,
leaf,
context,
node.style,
)
let width_known = Size(width=Some(measured.width), height=None)
ignore(
(view.measure_function)(
width_known,
available_space,
leaf,
context,
node.style,
),
)
let height_known = Size(width=None, height=Some(measured.height))
ignore(
(view.measure_function)(
height_known,
available_space,
leaf,
context,
node.style,
),
)
let fully_known = Size(
width=Some(measured.width),
height=Some(measured.height),
)
ignore(
(view.measure_function)(
fully_known,
available_space,
leaf,
context,
node.style,
),
)
measured
}
///|
fn is_default_flex_chain_container(style : Style) -> Bool {
style.display is DisplayFlex &&
style.overflow.x is OverflowVisible &&
style.overflow.y is OverflowVisible &&
style.scrollbar_width == 0.0 &&
style.position is PosRelative &&
dimension_is_auto(style.inset.left) &&
dimension_is_auto(style.inset.right) &&
dimension_is_auto(style.inset.top) &&
dimension_is_auto(style.inset.bottom) &&
dimension_is_auto(style.size.width) &&
dimension_is_auto(style.size.height) &&
dimension_is_auto(style.min_size.width) &&
dimension_is_auto(style.min_size.height) &&
dimension_is_auto(style.max_size.width) &&
style.aspect_ratio is None &&
rect_is_zero(style.margin) &&
rect_is_zero(style.padding) &&
rect_is_zero(style.border) &&
style.align_items is None &&
style.align_self is None &&
style.justify_items is None &&
style.justify_self is None &&
style.align_content is None &&
style.justify_content is None &&
dimension_is_length_zero(style.gap.width) &&
dimension_is_length_zero(style.gap.height) &&
style.flex_direction is FlexRow &&
style.flex_wrap is FlexNoWrap &&
dimension_is_auto(style.flex_basis) &&
style.flex_grow == 0.0 &&
style.flex_shrink == 1.0 &&
style.grid_template_rows.length() == 0 &&
style.grid_template_columns.length() == 0 &&
style.grid_auto_rows.length() == 0 &&
style.grid_auto_columns.length() == 0 &&
style.grid_auto_flow is Row &&
style.grid_row.start is PlaceAuto &&
style.grid_row.end is PlaceAuto &&
style.grid_column.start is PlaceAuto &&
style.grid_column.end is PlaceAuto &&
style.grid_row_start is None &&
style.grid_column_start is None
}
///|
fn dimension_is_auto(value : Dimension) -> Bool {
value is DimAuto
}
///|
fn dimension_is_length_zero(value : Dimension) -> Bool {
match value {
DimLength(v) => v == 0.0
_ => false
}
}
///|
fn dimension_with_box_sizing_inset(
value : Dimension,
available : AvailableSpace,
inset : Double,
) -> Dimension {
match @util.resolve_optional_dimension(value, available) {
Some(resolved) => DimLength(resolved + inset)
None => value
}
}
///|
fn style_with_border_box_sizing(
style : Style,
available_space : Size[AvailableSpace],
) -> Style {
match style.box_sizing {
BorderBox => style
ContentBox => {
let padding = @util.resolve_rect_width_basis(
style.padding,
available_space,
)
let border = @util.resolve_rect_width_basis(style.border, available_space)
let horizontal_inset = padding.left +
padding.right +
border.left +
border.right
let vertical_inset = padding.top +
padding.bottom +
border.top +
border.bottom
{
..style,
size: Size(
width=dimension_with_box_sizing_inset(
style.size.width,
available_space.width,
horizontal_inset,
),
height=dimension_with_box_sizing_inset(
style.size.height,
available_space.height,
vertical_inset,
),
),
min_size: Size(
width=dimension_with_box_sizing_inset(
style.min_size.width,
available_space.width,
horizontal_inset,
),
height=dimension_with_box_sizing_inset(
style.min_size.height,
available_space.height,
vertical_inset,
),
),
max_size: Size(
width=dimension_with_box_sizing_inset(
style.max_size.width,
available_space.width,
horizontal_inset,
),
height=dimension_with_box_sizing_inset(
style.max_size.height,
available_space.height,
vertical_inset,
),
),
}
}
}
}
///|
fn rect_is_zero(rect : Rect[Dimension]) -> Bool {
dimension_is_length_zero(rect.left) &&
dimension_is_length_zero(rect.right) &&
dimension_is_length_zero(rect.top) &&
dimension_is_length_zero(rect.bottom)
}
///|
fn[C] round_layout_tree_and_mark_clean(
tree : ChicleTree[C],
root : NodeId,
) -> Unit {
let stack : Array[NodeId] = [root]
while true {
match stack.pop() {
None => break
Some(cur) => {
if !tree.nodes[cur].alive {
continue
}
let layout = tree.nodes[cur].unrounded_layout
let rounded_left = layout.location.x.round()
let rounded_top = layout.location.y.round()
let rounded_right = (layout.location.x + layout.size.width).round()
let rounded_bottom = (layout.location.y + layout.size.height).round()
let rounded_layout = {
..layout,
location: Point(x=rounded_left, y=rounded_top),
size: Size(
width=@util.max_double(rounded_right - rounded_left, 0.0),
height=@util.max_double(rounded_bottom - rounded_top, 0.0),
),
}
tree.set_final_layout(cur, rounded_layout)
tree.nodes[cur].dirty = false
for child_id in tree.children[cur] {
stack.push(child_id)
}
}
}
}
}
///|
fn[C] copy_unrounded_layout_tree_and_mark_clean(
tree : ChicleTree[C],
root : NodeId,
) -> Unit {
let stack : Array[NodeId] = [root]
while true {
match stack.pop() {
None => break
Some(cur) => {
if !tree.nodes[cur].alive {
continue
}
tree.set_final_layout(cur, tree.nodes[cur].unrounded_layout)
tree.nodes[cur].dirty = false
for child_id in tree.children[cur] {
stack.push(child_id)
}
}
}
}
}
///|
fn[C] ChicleView::perform_child_layout(
self : ChicleView[C],
node_id : NodeId,
known_dimensions : Size[Double?],
available_space : Size[AvailableSpace],
absolute_origin : Point[Double],
is_layout_root : Bool,
) -> Unit raise ChicleError {
let tree = self.tree
let inputs = match tree.nodes.get(node_id) {
Some(node) =>
if node.style.display is DisplayNone {
LayoutInput::hidden()
} else {
layout_input_for_perform_layout(known_dimensions, available_space)
}
None => layout_input_for_perform_layout(known_dimensions, available_space)
}
ignore(
self.compute_child_layout(node_id, inputs, absolute_origin, is_layout_root),
)
}
///|
fn[C] ChicleView::compute_child_layout(
self : ChicleView[C],
node_id : NodeId,
inputs : LayoutInput,
absolute_origin : Point[Double],
is_layout_root : Bool,
) -> LayoutOutput raise ChicleError {
let tree = self.tree
let node = match tree.nodes.get(node_id) {
Some(n) => n
None => raise InvalidNodeId(node_id)
}
let run_mode = if inputs.run_mode is PerformHiddenLayout ||
node.style.display is DisplayNone {
PerformHiddenLayout
} else {
inputs.run_mode
}
match inputs.sizing_mode {
ContentSize | InherentSize => ()
}
match inputs.axis {
RequestedHorizontal | RequestedVertical | RequestedBoth => ()
}
ignore(inputs.parent_size)
ignore(inputs.vertical_margins_are_collapsible)
let can_use_layout_cache = can_use_node_layout_cache(
tree, node_id, absolute_origin,
)
match
restore_node_layout_from_cache(
tree,
node_id,
inputs.known_dimensions,
inputs.available_space,
run_mode,
can_use_layout_cache,
) {
Some(output) => return output
None => ()
}
let original_style = node.style
let uses_content_box = original_style.box_sizing is ContentBox
if uses_content_box {
tree.nodes[node_id].style = style_with_border_box_sizing(
original_style,
inputs.available_space,
)
}
tree.set_first_baselines(node_id, Point(x=None, y=None))
if run_mode is PerformHiddenLayout {
compute_hidden_layout(tree, node_id, absolute_origin)
} else {
match node.style.display {
DisplayNone => compute_hidden_layout(tree, node_id, absolute_origin)
DisplayGrid =>
if tree.children[node_id].length() == 0 {
compute_leaf_layout(
self,
node_id,
inputs.known_dimensions,
inputs.available_space,
absolute_origin,
)
} else {
compute_grid_layout(
self,
node_id,
inputs.known_dimensions,
inputs.available_space,
absolute_origin,
is_layout_root,
)
}
DisplayBlock =>
if tree.children[node_id].length() == 0 {
compute_leaf_layout(
self,
node_id,
inputs.known_dimensions,
inputs.available_space,
absolute_origin,
)
} else {
compute_block_layout(
self,
node_id,
inputs.known_dimensions,
inputs.available_space,
absolute_origin,
is_layout_root,
)
}
_ =>
if tree.children[node_id].length() == 0 {
compute_leaf_layout(
self,
node_id,
inputs.known_dimensions,
inputs.available_space,
absolute_origin,
)
} else {
compute_flexbox_layout(
self,
node_id,
inputs.known_dimensions,
inputs.available_space,
absolute_origin,
)
}
}
}
apply_relative_inset_after_layout(
tree,
node_id,
node.style,
inputs.available_space,
)
if run_mode is PerformLayout && tree.children[node_id].length() > 0 {
assign_child_layout_orders(tree, node_id, node.style.display)
}
finalize_layout_metadata(tree, node_id, node.style, inputs.available_space)
let output = LayoutOutput::from_layout_and_first_baselines(
tree.nodes[node_id].unrounded_layout,
tree.nodes[node_id].first_baselines,
)
save_node_layout_cache(
tree,
node_id,
inputs.known_dimensions,
inputs.available_space,
output,
run_mode,
can_use_layout_cache,
)
if uses_content_box {
tree.nodes[node_id].style = original_style
}
output
}
///|
fn[C] assign_child_layout_orders(
tree : ChicleTree[C],
node_id : NodeId,
display : Display,
) -> Unit {
match display {
DisplayGrid => {
let mut order = 0
for child_id in tree.children[node_id] {
let child = tree.nodes[child_id]
if !(child.style.display is DisplayNone) &&
child.style.position is PosRelative {
let layout = child.unrounded_layout
tree.set_unrounded_layout(child_id, { ..layout, order, })
order = order + 1
}
}
for child_id in tree.children[node_id] {
let child = tree.nodes[child_id]
if child.style.display is DisplayNone {
order = order + 1
} else if child.style.position is PosAbsolute {
let layout = child.unrounded_layout
tree.set_unrounded_layout(child_id, { ..layout, order, })
order = order + 1
}
}
}
DisplayFlex =>
for source_order, child_id in tree.children[node_id] {
let child = tree.nodes[child_id]
if !(child.style.display is DisplayNone) {
let layout = child.unrounded_layout
tree.set_unrounded_layout(child_id, { ..layout, order: source_order })
}
}
DisplayBlock => {
let mut order = 0
for child_id in tree.children[node_id] {
let child = tree.nodes[child_id]
if !(child.style.display is DisplayNone) {
let layout = child.unrounded_layout
tree.set_unrounded_layout(child_id, { ..layout, order, })
order = order + 1
}
}
}
DisplayNone => ()
}
}
///|
fn[C] finalize_layout_metadata(
tree : ChicleTree[C],
node_id : NodeId,
style : Style,
available_space : Size[AvailableSpace],
) -> Unit {
if style.display is DisplayNone {
return
}
let layout = tree.nodes[node_id].unrounded_layout
let padding = @util.resolve_rect_width_basis(style.padding, available_space)
let border = @util.resolve_rect_width_basis(style.border, available_space)
let scrollbar_width = style.scrollbar_width
let scrollbar_size = Size(
width=match style.overflow.y {
OverflowScroll => scrollbar_width
_ => 0.0
},
height=match style.overflow.x {
OverflowScroll => scrollbar_width
_ => 0.0
},
)
let horizontal_inset = padding.left +
padding.right +
border.left +
border.right +
scrollbar_size.width
let vertical_inset = padding.top +
padding.bottom +
border.top +
border.bottom +
scrollbar_size.height
let content_size = if tree.children[node_id].length() == 0 {
layout.content_size
} else {
let mut container_content_size = Size(
width=@util.max_double(layout.size.width - horizontal_inset, 0.0),
height=@util.max_double(layout.size.height - vertical_inset, 0.0),
)
for child_id in tree.children[node_id] {
let child = tree.nodes[child_id]
if !(child.style.display is DisplayNone) {
let child_layout = child.unrounded_layout
let contribution = compute_content_size_contribution(
Point(
x=child_layout.location.x - layout.location.x,
y=child_layout.location.y - layout.location.y,
),
child_layout.size,
child_layout.content_size,
child.style.overflow,
)
container_content_size = Size(
width=@util.max_double(
container_content_size.width,
contribution.width,
),
height=@util.max_double(
container_content_size.height,
contribution.height,
),
)
}
}
container_content_size
}
tree.set_unrounded_layout(node_id, {
..layout,
content_size,
scrollbar_size,
border,
padding,
})
}
///|
fn[C] ChicleView::measure_child_size(
self : ChicleView[C],
node_id : NodeId,
known_dimensions : Size[Double?],
parent_size : Size[Double?],
available_space : Size[AvailableSpace],
sizing_mode : SizingMode,
axis : AbsoluteAxis,
vertical_margins_are_collapsible : Line[Bool],
absolute_origin : Point[Double],
) -> Double raise ChicleError {
let inputs = layout_input_for_compute_size(
known_dimensions,
parent_size,
available_space,
sizing_mode,
requested_axis_from_absolute(axis),
vertical_margins_are_collapsible,
)
let output = self.compute_child_layout(
node_id, inputs, absolute_origin, false,
)
let size = output.outer_size()
match axis {
Horizontal => size.width
Vertical => size.height
}
}
///|
fn[C] can_use_node_layout_cache(
tree : ChicleTree[C],
node_id : NodeId,
absolute_origin : Point[Double],
) -> Bool {
tree.children[node_id].length() <= 1 &&
@util.double_approx_equal(absolute_origin.x, 0.0) &&
@util.double_approx_equal(absolute_origin.y, 0.0)
}
///|
fn[C] restore_node_layout_from_cache(
tree : ChicleTree[C],
node_id : NodeId,
known_dimensions : Size[Double?],
available_space : Size[AvailableSpace],
run_mode : RunMode,
can_use_layout_cache : Bool,
) -> LayoutOutput? {
if !can_use_layout_cache {
return None
}
let node = tree.nodes[node_id]
match
find_node_layout_cache(
node.cache,
known_dimensions,
available_space,
run_mode,
) {
Some(entry) => {
tree.set_unrounded_layout(node_id, entry.layout)
tree.nodes[node_id].effective_margin_top = entry.effective_margin_top
tree.nodes[node_id].effective_margin_bottom = entry.effective_margin_bottom
tree.nodes[node_id].effective_margin_top_max_pos = entry.effective_margin_top_max_pos
tree.nodes[node_id].effective_margin_top_min_neg = entry.effective_margin_top_min_neg
tree.nodes[node_id].effective_margin_bottom_max_pos = entry.effective_margin_bottom_max_pos
tree.nodes[node_id].effective_margin_bottom_min_neg = entry.effective_margin_bottom_min_neg
Some(entry.content)
}
None => None
}
}
///|
fn[C] apply_relative_inset_after_layout(
tree : ChicleTree[C],
node_id : NodeId,
style : Style,
available_space : Size[AvailableSpace],
) -> Unit raise ChicleError {
if style.display is DisplayNone {
return
}
match style.position {
PosRelative => {
let inset = style.inset
let dx = match
@util.resolve_optional_dimension(inset.left, available_space.width) {
Some(v) => v
None =>
match
@util.resolve_optional_dimension(inset.right, available_space.width) {
Some(v) => -v
None => 0.0
}
}
let dy = match
@util.resolve_optional_dimension(inset.top, available_space.height) {
Some(v) => v
None =>
match
@util.resolve_optional_dimension(
inset.bottom,
available_space.height,
) {
Some(v) => -v
None => 0.0
}
}
if dx != 0.0 || dy != 0.0 {
offset_subtree(tree, node_id, dx, dy)
}
}
PosAbsolute => ()
}
}
///|
fn[C] save_node_layout_cache(
tree : ChicleTree[C],
node_id : NodeId,
known_dimensions : Size[Double?],
available_space : Size[AvailableSpace],
output : LayoutOutput,
run_mode : RunMode,
can_use_layout_cache : Bool,
) -> Unit {
if !can_use_layout_cache {
return
}
let layout = tree.nodes[node_id].unrounded_layout
store_node_layout_cache(tree.nodes[node_id].cache, run_mode, {
known_dimensions,
available_space,
content: output,
layout,
effective_margin_top: tree.nodes[node_id].effective_margin_top,
effective_margin_bottom: tree.nodes[node_id].effective_margin_bottom,
effective_margin_top_max_pos: tree.nodes[node_id].effective_margin_top_max_pos,
effective_margin_top_min_neg: tree.nodes[node_id].effective_margin_top_min_neg,
effective_margin_bottom_max_pos: tree.nodes[node_id].effective_margin_bottom_max_pos,
effective_margin_bottom_min_neg: tree.nodes[node_id].effective_margin_bottom_min_neg,
})
}