// 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] compute_block_layout(
view : 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 = view.tree
let node = match tree.nodes.get(node_id) {
Some(n) => n
None => raise InvalidNodeId(node_id)
}
let padding = @util.resolve_rect_width_basis(
node.style.padding,
available_space,
)
let border = @util.resolve_rect_width_basis(
node.style.border,
available_space,
)
let scrollbar_w = node.style.scrollbar_width
let scrollbar_x = match node.style.overflow.x {
OverflowScroll => scrollbar_w
_ => 0.0
}
let scrollbar_y = match node.style.overflow.y {
OverflowScroll => scrollbar_w
_ => 0.0
}
let horiz_non_scroll_inset = padding.left +
padding.right +
border.left +
border.right
let vert_non_scroll_inset = padding.top +
padding.bottom +
border.top +
border.bottom
// Overflow::Scroll reserves scrollbar space inside the border box.
// Horizontal scrollbar consumes cross (vertical) space; vertical scrollbar consumes main (horizontal) space.
let horiz_inset = horiz_non_scroll_inset + scrollbar_y
let vert_inset = vert_non_scroll_inset + scrollbar_x
let style_width = @util.resolve_optional_dimension(
node.style.size.width,
available_space.width,
)
let style_height = @util.resolve_optional_dimension(
node.style.size.height,
available_space.height,
)
let specified_width = match known_dimensions.width {
Some(w) => Some(w)
None => style_width
}
let specified_height = match known_dimensions.height {
Some(h) => Some(h)
None => style_height
}
let width_definite = match specified_width {
Some(_) => true
None =>
match available_space.width {
AvailDefinite(_) => true
_ => false
}
}
// Minimal block sizing: border-box.
let raw_width = match specified_width {
Some(w) => w
None =>
match available_space.width {
AvailDefinite(v) => v
_ => horiz_inset
}
}
let raw_height = match specified_height {
Some(h) => h
None =>
match available_space.height {
AvailDefinite(v) => v
_ => vert_inset
}
}
let border_box_width = @util.max_double(
@util.clamp_dimension(
raw_width,
node.style.min_size.width,
node.style.max_size.width,
available_space.width,
),
horiz_non_scroll_inset,
)
let border_box_height = @util.max_double(
@util.clamp_dimension(
raw_height,
node.style.min_size.height,
node.style.max_size.height,
available_space.height,
),
vert_non_scroll_inset,
)
tree.set_unrounded_layout(node_id, {
..Layout::zero(),
location: absolute_origin,
size: Size(width=border_box_width, height=border_box_height),
})
// Containing block for in-flow positioning: content box (padding box minus padding).
let padding_origin = Point(
x=absolute_origin.x + border.left,
y=absolute_origin.y + border.top,
)
let content_origin = Point(
x=padding_origin.x + padding.left,
y=padding_origin.y + padding.top,
)
let content_width = @util.max_double(border_box_width - horiz_inset, 0.0)
let content_height = @util.max_double(border_box_height - vert_inset, 0.0)
let flow_available = Size(
width=match width_definite {
true => @geometry.AvailDefinite(content_width)
false => available_space.width
},
height=match specified_height {
Some(_) => AvailDefinite(content_height)
None => AvailMaxContent
},
)
let margin_basis_width = match available_space.width {
AvailDefinite(v) => v
_ =>
match specified_width {
Some(w) => @util.max_double(w - horiz_inset, 0.0)
None => 0.0
}
}
let margin_basis_available = Size(
width=@geometry.AvailDefinite(margin_basis_width),
height=flow_available.height,
)
let resolved_own_margin = @util.resolve_rect_width_basis(
node.style.margin,
available_space,
)
let own_margin_top = resolved_own_margin.top
let own_margin_bottom = resolved_own_margin.bottom
let parent_overflow_visible = match
(node.style.overflow.x, node.style.overflow.y) {
(OverflowVisible, OverflowVisible) => true
_ => false
}
let mut first_in_flow_child : NodeId? = None
let mut last_in_flow_child : NodeId? = None
for child_id in tree.children[node_id] {
let child = match tree.nodes.get(child_id) {
Some(c) => c
None => raise InvalidNodeId(child_id)
}
match child.style.display {
DisplayNone => ()
_ =>
match child.style.position {
PosRelative => {
match first_in_flow_child {
None => first_in_flow_child = Some(child_id)
Some(_) => ()
}
last_in_flow_child = Some(child_id)
}
PosAbsolute => ()
}
}
}
let collapse_with_children = !is_layout_root &&
parent_overflow_visible &&
specified_height is None
let first_child_allows_parent_child_collapse = match first_in_flow_child {
Some(child_id) => {
let child = match tree.nodes.get(child_id) {
Some(c) => c
None => raise InvalidNodeId(child_id)
}
child.style.display is DisplayBlock
}
None => false
}
let last_child_allows_parent_child_collapse = match last_in_flow_child {
Some(child_id) => {
let child = match tree.nodes.get(child_id) {
Some(c) => c
None => raise InvalidNodeId(child_id)
}
child.style.display is DisplayBlock
}
None => false
}
let collapse_top_with_child = collapse_with_children &&
padding.top == 0.0 &&
border.top == 0.0 &&
first_child_allows_parent_child_collapse
let collapse_bottom_with_child = collapse_with_children &&
padding.bottom == 0.0 &&
border.bottom == 0.0 &&
last_child_allows_parent_child_collapse
let abs_child_ids : Array[NodeId] = []
let abs_child_static_y : Array[Double] = []
let mut flow_anchor_border_bottom : Double = 0.0
let mut flow_margin_collapse : (Double, Double) = (0.0, 0.0)
let mut max_flow_child_width = 0.0
let content_width_definite = match flow_available.width {
AvailDefinite(_) => true
_ => false
}
let content_width_for_flow = match flow_available.width {
AvailDefinite(v) => v
_ => 0.0
}
let mut is_first_in_flow = true
for child_id in tree.children[node_id] {
let child = match tree.nodes.get(child_id) {
Some(c) => c
None => raise InvalidNodeId(child_id)
}
match child.style.display {
DisplayNone => compute_hidden_layout(tree, child_id, Point::zero())
_ =>
match child.style.position {
PosAbsolute => {
abs_child_ids.push(child_id)
abs_child_static_y.push(
flow_anchor_border_bottom +
collapse_state_value(flow_margin_collapse),
)
}
PosRelative => {
// Minimal normal flow: stack children vertically.
let margin = child.style.margin
let margin_left_auto = match margin.left {
DimAuto => true
_ => false
}
let margin_right_auto = match margin.right {
DimAuto => true
_ => false
}
let margin_top_auto = match margin.top {
DimAuto => true
_ => false
}
let resolved_margin = @util.resolve_rect_width_basis(
margin, margin_basis_available,
)
let fixed_margin_left = if margin_left_auto {
0.0
} else {
resolved_margin.left
}
let fixed_margin_right = if margin_right_auto {
0.0
} else {
resolved_margin.right
}
let fixed_margin_top = if margin_top_auto {
0.0
} else {
resolved_margin.top
}
let child_width_auto = match child.style.size.width {
DimAuto => true
_ => false
}
let aspect_ratio = child.style.aspect_ratio
let width_from_aspect = match
(aspect_ratio, child.style.size.width) {
(Some(ratio), DimAuto) =>
match
@util.resolve_optional_dimension(
child.style.size.height,
flow_available.height,
) {
Some(h) if ratio > 0.0 => Some((h * ratio).round())
_ => None
}
_ => None
}
let mut target_width = match width_from_aspect {
Some(w) => Some(w)
None =>
if content_width_definite && child_width_auto {
Some(
@util.max_double(
content_width_for_flow -
fixed_margin_left -
fixed_margin_right,
0.0,
),
)
} else {
None
}
}
match target_width {
Some(w) => {
let mut min_width = @util.resolve_optional_dimension(
child.style.min_size.width,
flow_available.width,
)
let mut max_width = @util.resolve_optional_dimension(
child.style.max_size.width,
flow_available.width,
)
match aspect_ratio {
Some(ratio) =>
if ratio > 0.0 {
match
(
min_width,
@util.resolve_optional_dimension(
child.style.min_size.height,
flow_available.height,
),
) {
(None, Some(h)) => min_width = Some((h * ratio).round())
_ => ()
}
match
(
max_width,
@util.resolve_optional_dimension(
child.style.max_size.height,
flow_available.height,
),
) {
(None, Some(h)) => max_width = Some((h * ratio).round())
_ => ()
}
} else {
()
}
None => ()
}
let mut clamped = w
match min_width {
Some(m) => clamped = @util.max_double(clamped, m)
None => ()
}
match max_width {
Some(m) => if clamped > m { clamped = m } else { () }
None => ()
}
target_width = Some(clamped)
}
None => ()
}
let is_last_in_flow = match last_in_flow_child {
Some(v) => v == child_id
None => false
}
let top_used_base = if collapse_top_with_child && is_first_in_flow {
0.0
} else {
fixed_margin_top
}
let used_before_base = collapse_state_value(
collapse_state_add(flow_margin_collapse, top_used_base),
)
let mut child_y_rel_static = flow_anchor_border_bottom +
used_before_base
view.perform_child_layout(
child_id,
Size(width=target_width, height=None),
margin_basis_available,
Point(
x=content_origin.x + fixed_margin_left,
y=content_origin.y + child_y_rel_static,
),
false,
)
let child_size = tree.nodes[child_id].unrounded_layout.size
let effective_top = tree.nodes[child_id].effective_margin_top
let top_used_effective = if collapse_top_with_child &&
is_first_in_flow {
0.0
} else {
effective_top
}
let used_before_effective = collapse_state_value(
collapse_state_add(flow_margin_collapse, top_used_effective),
)
let delta_y = used_before_effective - used_before_base
if delta_y != 0.0 {
offset_subtree(tree, child_id, 0.0, delta_y)
child_y_rel_static = child_y_rel_static + delta_y
} else {
()
}
let remaining = if content_width_definite {
content_width_for_flow -
fixed_margin_left -
fixed_margin_right -
child_size.width
} else {
0.0
}
let mut auto_left = 0.0
let mut auto_right = 0.0
if content_width_definite {
if margin_left_auto && margin_right_auto {
if remaining > 0.0 {
auto_left = remaining / 2.0
auto_right = remaining / 2.0
} else {
auto_left = 0.0
auto_right = 0.0
}
} else if margin_left_auto {
auto_left = if remaining > 0.0 { remaining } else { 0.0 }
} else if margin_right_auto {
auto_right = if remaining > 0.0 { remaining } else { 0.0 }
} else {
()
}
if auto_left != 0.0 {
offset_subtree(tree, child_id, auto_left, 0.0)
} else {
()
}
} else {
()
}
let effective_bottom_raw = tree.nodes[child_id].effective_margin_bottom
let bottom_used_effective = if collapse_bottom_with_child &&
is_last_in_flow {
0.0
} else {
effective_bottom_raw
}
let through = can_collapse_through_child(
tree, child_id, child, child_size,
)
if through {
flow_margin_collapse = collapse_state_add(
collapse_state_add(flow_margin_collapse, top_used_effective),
bottom_used_effective,
)
} else {
flow_anchor_border_bottom = child_y_rel_static + child_size.height
flow_margin_collapse = collapse_state_from(bottom_used_effective)
}
is_first_in_flow = false
let child_outer_width = fixed_margin_left +
auto_left +
child_size.width +
fixed_margin_right +
auto_right
if child_outer_width > max_flow_child_width {
max_flow_child_width = child_outer_width
} else {
()
}
}
}
}
}
// Auto sizing from in-flow children.
let final_border_box_width = match width_definite {
true => border_box_width
false =>
@util.max_double(
@util.clamp_dimension(
horiz_inset + max_flow_child_width,
node.style.min_size.width,
node.style.max_size.width,
available_space.width,
),
horiz_non_scroll_inset,
)
}
let mut final_border_box_height = match specified_height {
Some(_) => border_box_height
None =>
@util.max_double(
@util.clamp_dimension(
vert_inset +
flow_anchor_border_bottom +
collapse_state_value(flow_margin_collapse),
node.style.min_size.height,
node.style.max_size.height,
available_space.height,
),
vert_non_scroll_inset,
)
}
// Chicle block layout computes container width first, then performs final in-flow layout with that width.
// When this container's width is auto, do a second layout pass so that in-flow children can stretch
// (e.g. auto-width block children) and so that absolute children's static y positions are correct.
if !width_definite {
let final_content_width_for_flow = @util.max_double(
final_border_box_width - horiz_inset,
0.0,
)
let final_content_height_for_flow = @util.max_double(
final_border_box_height - vert_inset,
0.0,
)
let flow_available2 = Size(
width=@geometry.AvailDefinite(final_content_width_for_flow),
height=match specified_height {
Some(_) => AvailDefinite(final_content_height_for_flow)
None => AvailMaxContent
},
)
let margin_basis_available2 = Size(
width=@geometry.AvailDefinite(final_content_width_for_flow),
height=flow_available2.height,
)
abs_child_ids.clear()
abs_child_static_y.clear()
flow_anchor_border_bottom = 0.0
flow_margin_collapse = (0.0, 0.0)
max_flow_child_width = 0.0
let content_width_for_flow2 = final_content_width_for_flow
let mut is_first_in_flow2 = true
for child_id in tree.children[node_id] {
let child = match tree.nodes.get(child_id) {
Some(c) => c
None => raise InvalidNodeId(child_id)
}
match child.style.display {
DisplayNone => compute_hidden_layout(tree, child_id, Point::zero())
_ =>
match child.style.position {
PosAbsolute => {
abs_child_ids.push(child_id)
abs_child_static_y.push(
flow_anchor_border_bottom +
collapse_state_value(flow_margin_collapse),
)
}
PosRelative => {
let margin = child.style.margin
let margin_left_auto = margin.left is DimAuto
let margin_right_auto = margin.right is DimAuto
let margin_top_auto = margin.top is DimAuto
let resolved_margin = @util.resolve_rect_width_basis(
margin, margin_basis_available2,
)
let fixed_margin_left = if margin_left_auto {
0.0
} else {
resolved_margin.left
}
let fixed_margin_right = if margin_right_auto {
0.0
} else {
resolved_margin.right
}
let fixed_margin_top = if margin_top_auto {
0.0
} else {
resolved_margin.top
}
let child_width_auto = child.style.size.width is DimAuto
let aspect_ratio = child.style.aspect_ratio
let width_from_aspect = match
(aspect_ratio, child.style.size.width) {
(Some(ratio), DimAuto) =>
match
@util.resolve_optional_dimension(
child.style.size.height,
flow_available2.height,
) {
Some(h) if ratio > 0.0 => Some((h * ratio).round())
_ => None
}
_ => None
}
let mut target_width = match width_from_aspect {
Some(w) => Some(w)
None =>
if child_width_auto {
Some(
@util.max_double(
content_width_for_flow2 -
fixed_margin_left -
fixed_margin_right,
0.0,
),
)
} else {
None
}
}
match target_width {
Some(w) => {
let mut min_width = @util.resolve_optional_dimension(
child.style.min_size.width,
flow_available2.width,
)
let mut max_width = @util.resolve_optional_dimension(
child.style.max_size.width,
flow_available2.width,
)
match aspect_ratio {
Some(ratio) =>
if ratio > 0.0 {
match
(
min_width,
@util.resolve_optional_dimension(
child.style.min_size.height,
flow_available2.height,
),
) {
(None, Some(h)) =>
min_width = Some((h * ratio).round())
_ => ()
}
match
(
max_width,
@util.resolve_optional_dimension(
child.style.max_size.height,
flow_available2.height,
),
) {
(None, Some(h)) =>
max_width = Some((h * ratio).round())
_ => ()
}
} else {
()
}
None => ()
}
let mut clamped = w
match min_width {
Some(m) => clamped = @util.max_double(clamped, m)
None => ()
}
match max_width {
Some(m) => if clamped > m { clamped = m } else { () }
None => ()
}
target_width = Some(clamped)
}
None => ()
}
let is_last_in_flow = match last_in_flow_child {
Some(v) => v == child_id
None => false
}
let top_used_base = if collapse_top_with_child &&
is_first_in_flow2 {
0.0
} else {
fixed_margin_top
}
let used_before_base = collapse_state_value(
collapse_state_add(flow_margin_collapse, top_used_base),
)
let mut child_y_rel_static = flow_anchor_border_bottom +
used_before_base
view.perform_child_layout(
child_id,
Size(width=target_width, height=None),
flow_available2,
Point(
x=content_origin.x + fixed_margin_left,
y=content_origin.y + child_y_rel_static,
),
false,
)
let child_size = tree.nodes[child_id].unrounded_layout.size
let effective_top = tree.nodes[child_id].effective_margin_top
let top_used_effective = if collapse_top_with_child &&
is_first_in_flow2 {
0.0
} else {
effective_top
}
let used_before_effective = collapse_state_value(
collapse_state_add(flow_margin_collapse, top_used_effective),
)
let delta_y = used_before_effective - used_before_base
if delta_y != 0.0 {
offset_subtree(tree, child_id, 0.0, delta_y)
child_y_rel_static = child_y_rel_static + delta_y
} else {
()
}
let remaining = content_width_for_flow2 -
fixed_margin_left -
fixed_margin_right -
child_size.width
let mut auto_left = 0.0
let mut auto_right = 0.0
if margin_left_auto && margin_right_auto {
if remaining > 0.0 {
auto_left = remaining / 2.0
auto_right = remaining / 2.0
} else {
auto_left = 0.0
auto_right = 0.0
}
} else if margin_left_auto {
auto_left = if remaining > 0.0 { remaining } else { 0.0 }
} else if margin_right_auto {
auto_right = if remaining > 0.0 { remaining } else { 0.0 }
} else {
()
}
if auto_left != 0.0 {
offset_subtree(tree, child_id, auto_left, 0.0)
} else {
()
}
let effective_bottom_raw = tree.nodes[child_id].effective_margin_bottom
let bottom_used_effective = if collapse_bottom_with_child &&
is_last_in_flow {
0.0
} else {
effective_bottom_raw
}
let through = can_collapse_through_child(
tree, child_id, child, child_size,
)
if through {
flow_margin_collapse = collapse_state_add(
collapse_state_add(flow_margin_collapse, top_used_effective),
bottom_used_effective,
)
} else {
flow_anchor_border_bottom = child_y_rel_static +
child_size.height
flow_margin_collapse = collapse_state_from(
bottom_used_effective,
)
}
is_first_in_flow2 = false
let child_outer_width = fixed_margin_left +
auto_left +
child_size.width +
fixed_margin_right +
auto_right
if child_outer_width > max_flow_child_width {
max_flow_child_width = child_outer_width
} else {
()
}
}
}
}
}
// Update auto height after reflow.
final_border_box_height = match specified_height {
Some(_) => final_border_box_height
None =>
@util.max_double(
@util.clamp_dimension(
vert_inset +
flow_anchor_border_bottom +
collapse_state_value(flow_margin_collapse),
node.style.min_size.height,
node.style.max_size.height,
available_space.height,
),
vert_non_scroll_inset,
)
}
}
tree.set_unrounded_layout(node_id, {
..Layout::zero(),
location: absolute_origin,
size: Size(width=final_border_box_width, height=final_border_box_height),
})
compute_block_absolute_children(
view, abs_child_ids, abs_child_static_y, padding_origin, padding, border, final_border_box_width,
final_border_box_height, scrollbar_x, scrollbar_y,
)
let effective_top_state = if collapse_top_with_child {
let first_id = match first_in_flow_child {
Some(v) => v
None => node_id
}
collapse_state_merge(
collapse_state_from(own_margin_top),
(
tree.nodes[first_id].effective_margin_top_max_pos,
tree.nodes[first_id].effective_margin_top_min_neg,
),
)
} else {
collapse_state_from(own_margin_top)
}
let effective_bottom_state = if collapse_bottom_with_child {
let last_id = match last_in_flow_child {
Some(v) => v
None => node_id
}
collapse_state_merge(
collapse_state_from(own_margin_bottom),
(
tree.nodes[last_id].effective_margin_bottom_max_pos,
tree.nodes[last_id].effective_margin_bottom_min_neg,
),
)
} else {
collapse_state_from(own_margin_bottom)
}
set_effective_margin_states(
tree, node_id, effective_top_state, effective_bottom_state,
)
}