///|
/// Absolute positioning, containing-block, zoom, and transform scaling helpers.
///|
fn resolve_inset_for_root(
value : @types.Dimension,
available : Double,
) -> Double? {
match value {
@types.Length(v) => Some(v)
@types.Percent(p) => Some(available * p)
_ => None
}
}
///|
fn inset_is_definite_for_root(
value : @types.Dimension,
available : Double,
) -> Bool {
resolve_inset_for_root(value, available) is Some(_)
}
///|
fn resolve_non_auto_length_for_root(
value : @types.Dimension,
available : Double,
) -> Double {
match value {
@types.Length(v) => v
@types.Percent(p) => available * p
_ => 0.0
}
}
///|
fn resolve_out_of_flow_root_auto_size(
style : @style.Style,
viewport_width : Double,
viewport_height : Double,
) -> @style.Style {
let inset_left = resolve_inset_for_root(style.inset.left, viewport_width)
let inset_right = resolve_inset_for_root(style.inset.right, viewport_width)
let inset_top = resolve_inset_for_root(style.inset.top, viewport_height)
let inset_bottom = resolve_inset_for_root(style.inset.bottom, viewport_height)
let margin_left = resolve_non_auto_length_for_root(
style.margin.left,
viewport_width,
)
let margin_right = resolve_non_auto_length_for_root(
style.margin.right,
viewport_width,
)
let margin_top = resolve_non_auto_length_for_root(
style.margin.top,
viewport_height,
)
let margin_bottom = resolve_non_auto_length_for_root(
style.margin.bottom,
viewport_height,
)
let mut next_style = style
match (next_style.width, inset_left, inset_right) {
(@types.Auto, Some(left), Some(right)) => {
let used = viewport_width - left - right - margin_left - margin_right
let used = if used > 0.0 { used } else { 0.0 }
next_style = { ..next_style, width: @types.Length(used) }
}
_ => ()
}
match (next_style.height, inset_top, inset_bottom) {
(@types.Auto, Some(top), Some(bottom)) => {
let used = viewport_height - top - bottom - margin_top - margin_bottom
let used = if used > 0.0 { used } else { 0.0 }
next_style = { ..next_style, height: @types.Length(used) }
}
_ => ()
}
next_style
}
///|
fn is_svg_container_id(id : String) -> Bool {
let mut end = id.length()
for i = 0; i < id.length(); i = i + 1 {
let c = id[i]
if c == '#' || c == '.' {
end = i
break
}
}
let tag = if end < id.length() {
id.unsafe_substring(start=0, end~)
} else {
id
}
match tag {
"svg"
| "g"
| "defs"
| "clipPath"
| "mask"
| "pattern"
| "symbol"
| "foreignObject" => true
_ => false
}
}
///|
fn establishes_absolute_containing_block(node : @node.Node) -> Bool {
let style = node.style
let containment_applies = !is_no_principal_table_internal_display(
style.display,
)
let containment_establishes_cb = containment_applies &&
(style.contain.paint || style.contain.layout)
let positioned = match style.position {
@types.Static => false
_ => true
}
positioned ||
containment_establishes_cb ||
!style.transform.is_none() ||
style.has_filter ||
is_svg_container_id(node.id)
}
///|
fn is_auto_inset(dim : @types.Dimension) -> Bool {
match dim {
@types.Dimension::Auto => true
_ => false
}
}
///|
fn has_non_auto_horizontal_insets(style : @style.Style) -> Bool {
!is_auto_inset(style.inset.left) || !is_auto_inset(style.inset.right)
}
///|
fn has_non_auto_vertical_insets(style : @style.Style) -> Bool {
!is_auto_inset(style.inset.top) || !is_auto_inset(style.inset.bottom)
}
///|
fn is_flex_or_grid_container_display(display : @types.Display) -> Bool {
match display {
@types.Grid | @types.InlineGrid | @types.Flex | @types.InlineFlex => true
_ => false
}
}
///|
fn compute_abspos_non_auto_inset_alignment_offset(
self_alignment : @types.AlignSelf,
inset_start : @types.Dimension,
inset_end : @types.Dimension,
containing_block_size : Double,
child_size : Double,
) -> Double {
if is_auto_inset(inset_start) || is_auto_inset(inset_end) {
return 0.0
}
let start = resolve_dimension_with_percent_basis(
inset_start, containing_block_size,
)
let end = resolve_dimension_with_percent_basis(
inset_end, containing_block_size,
)
let free_space = containing_block_size - start - end - child_size
match self_alignment {
@types.AlignSelf::Center => free_space / 2.0
@types.AlignSelf::End => free_space
_ => 0.0
}
}
///|
/// Apply zoom and transform scale to a layout tree
/// This modifies the layout dimensions to reflect the visual bounding box
/// as reported by getBoundingClientRect() in browsers
///
/// Note: While CSS zoom is inherited, transform scale is not. However, for
/// getBoundingClientRect() calculations, parent transform affects children's
/// visual bounding boxes. So we pass both zoom and parent_scale to children.
fn apply_zoom_and_scale(
layout : @layout_types.Layout,
node : @node.Node,
parent_zoom : Double,
parent_scale_x : Double,
parent_scale_y : Double,
parent_global_x : Double,
parent_global_y : Double,
has_abs_containing_block_ancestor : Bool,
abs_containing_block_global_x : Double,
abs_containing_block_global_y : Double,
abs_containing_block_width : Double,
abs_containing_block_height : Double,
) -> @layout_types.Layout {
// Calculate effective zoom (parent zoom * node zoom)
let effective_zoom = parent_zoom * node.style.zoom
// Calculate effective scale from transform (node's own scale)
let node_scale_x = node.style.transform.scale_x
let node_scale_y = node.style.transform.scale_y
// Total scale = parent_scale * zoom * node_scale
let total_scale_x = parent_scale_x * effective_zoom * node_scale_x
let total_scale_y = parent_scale_y * effective_zoom * node_scale_y
// Scale for children includes this node's transform (since parent transform affects children visually)
let child_parent_scale_x = total_scale_x / effective_zoom // Remove node zoom, will be re-applied
let child_parent_scale_y = total_scale_y / effective_zoom
// Scale the layout dimensions
let scaled_width = layout.width * total_scale_x
let scaled_height = layout.height * total_scale_y
// Compute translate offset (uses original layout dimensions before scaling)
let translate_x = node.style.transform.compute_translate_x(layout.width)
let translate_y = node.style.transform.compute_translate_y(layout.height)
// Scale position (x, y are relative to parent, scaled by parent's total scale)
// Add translate offset (translate is applied in local coordinates, then scaled)
// CSS transforms scale around the element center by default (transform-origin: 50% 50%).
let origin_adjust_x = (1.0 - node_scale_x) *
layout.width /
2.0 *
parent_scale_x *
effective_zoom
let origin_adjust_y = (1.0 - node_scale_y) *
layout.height /
2.0 *
parent_scale_y *
effective_zoom
let scaled_x = layout.x * parent_scale_x * effective_zoom +
translate_x * parent_scale_x * effective_zoom +
origin_adjust_x
let scaled_y = layout.y * parent_scale_y * effective_zoom +
translate_y * parent_scale_y * effective_zoom +
origin_adjust_y
let global_x = parent_global_x + scaled_x
let global_y = parent_global_y + scaled_y
let establishes_abs_containing_block_here = establishes_absolute_containing_block(
node,
)
let has_abs_containing_block = has_abs_containing_block_ancestor ||
establishes_abs_containing_block_here
let current_abs_containing_block_global_x = if establishes_abs_containing_block_here {
global_x
} else {
abs_containing_block_global_x
}
let current_abs_containing_block_global_y = if establishes_abs_containing_block_here {
global_y
} else {
abs_containing_block_global_y
}
let current_abs_containing_block_width = if establishes_abs_containing_block_here {
scaled_width
} else {
abs_containing_block_width
}
let current_abs_containing_block_height = if establishes_abs_containing_block_here {
scaled_height
} else {
abs_containing_block_height
}
// Process children with updated parent zoom and scale
let scaled_children : Array[@layout_types.Layout] = []
let mut child_node_cursor = 0
for i = 0; i < layout.children.length(); i = i + 1 {
let child_layout = layout.children[i]
if node.children.length() == 0 {
scaled_children.push(child_layout)
continue
}
let mut matched_index : Int? = None
let mut scan_index = child_node_cursor
while scan_index < node.children.length() {
let candidate = node.children[scan_index]
if candidate.style.display == @types.Display::None {
scan_index = scan_index + 1
continue
}
if candidate.id == child_layout.id {
matched_index = Some(scan_index)
break
}
scan_index = scan_index + 1
}
let child_node = match matched_index {
Some(index) => {
child_node_cursor = index + 1
node.children[index]
}
None =>
if child_node_cursor < node.children.length() {
let fallback_index = child_node_cursor
child_node_cursor = fallback_index + 1
node.children[fallback_index]
} else {
node.children[node.children.length() - 1]
}
}
let scaled_child = apply_zoom_and_scale(
child_layout, child_node, effective_zoom, child_parent_scale_x, child_parent_scale_y,
global_x, global_y, has_abs_containing_block, current_abs_containing_block_global_x,
current_abs_containing_block_global_y, current_abs_containing_block_width,
current_abs_containing_block_height,
)
if child_node.style.position == @types.Absolute {
let inset_left_auto = is_auto_inset(child_node.style.inset.left)
let inset_right_auto = is_auto_inset(child_node.style.inset.right)
let inset_top_auto = is_auto_inset(child_node.style.inset.top)
let inset_bottom_auto = is_auto_inset(child_node.style.inset.bottom)
let parent_based_horizontal = if has_non_auto_horizontal_insets(
child_node.style,
) {
if !inset_left_auto {
let parent_left = resolve_dimension_with_percent_basis(
child_node.style.inset.left,
scaled_width,
)
let parent_based_x = parent_left + scaled_child.margin.left
(scaled_child.x - parent_based_x).abs() <= 0.5
} else if !inset_right_auto {
let parent_right = resolve_dimension_with_percent_basis(
child_node.style.inset.right,
scaled_width,
)
let parent_based_x = scaled_width -
scaled_child.width -
parent_right -
scaled_child.margin.right
(scaled_child.x - parent_based_x).abs() <= 0.5
} else {
false
}
} else {
false
}
let parent_based_vertical = if has_non_auto_vertical_insets(
child_node.style,
) {
if !inset_top_auto {
let parent_top = resolve_dimension_with_percent_basis(
child_node.style.inset.top,
scaled_height,
)
let parent_based_y = parent_top + scaled_child.margin.top
(scaled_child.y - parent_based_y).abs() <= 0.5
} else if !inset_bottom_auto {
let parent_bottom = resolve_dimension_with_percent_basis(
child_node.style.inset.bottom,
scaled_height,
)
let parent_based_y = scaled_height -
scaled_child.height -
parent_bottom -
scaled_child.margin.bottom
(scaled_child.y - parent_based_y).abs() <= 0.5
} else {
false
}
} else {
false
}
let adjust_x = if has_non_auto_horizontal_insets(child_node.style) {
if parent_based_horizontal {
let cb_global_x = if has_abs_containing_block {
current_abs_containing_block_global_x
} else {
0.0
}
cb_global_x - global_x
} else {
0.0
}
} else {
0.0
}
let adjust_y = if has_non_auto_vertical_insets(child_node.style) {
if parent_based_vertical {
let cb_global_y = if has_abs_containing_block {
current_abs_containing_block_global_y
} else {
0.0
}
cb_global_y - global_y
} else {
0.0
}
} else {
0.0
}
// Absolute children are laid out in their immediate static parent first.
// For right/bottom inset cases, convert to the true containing-block size.
let cb_size_adjust_x = if !inset_right_auto && inset_left_auto {
let parent_right = resolve_dimension_with_percent_basis(
child_node.style.inset.right,
scaled_width,
)
let parent_based_x = scaled_width -
scaled_child.width -
parent_right -
scaled_child.margin.right
if (scaled_child.x - parent_based_x).abs() <= 0.5 {
current_abs_containing_block_width - scaled_width
} else {
0.0
}
} else {
0.0
}
let cb_size_adjust_y = if !inset_bottom_auto && inset_top_auto {
let parent_bottom = resolve_dimension_with_percent_basis(
child_node.style.inset.bottom,
scaled_height,
)
let parent_based_y = scaled_height -
scaled_child.height -
parent_bottom -
scaled_child.margin.bottom
if (scaled_child.y - parent_based_y).abs() <= 0.5 {
current_abs_containing_block_height - scaled_height
} else {
0.0
}
} else {
0.0
}
let align_adjust_x = if is_flex_or_grid_container_display(
node.style.display,
) {
compute_abspos_non_auto_inset_alignment_offset(
child_node.style.justify_self,
child_node.style.inset.left,
child_node.style.inset.right,
current_abs_containing_block_width,
scaled_child.width,
)
} else {
0.0
}
let align_adjust_y = if is_flex_or_grid_container_display(
node.style.display,
) {
compute_abspos_non_auto_inset_alignment_offset(
child_node.style.align_self,
child_node.style.inset.top,
child_node.style.inset.bottom,
current_abs_containing_block_height,
scaled_child.height,
)
} else {
0.0
}
scaled_children.push({
..scaled_child,
x: scaled_child.x + adjust_x + cb_size_adjust_x + align_adjust_x,
y: scaled_child.y + adjust_y + cb_size_adjust_y + align_adjust_y,
})
} else {
scaled_children.push(scaled_child)
}
}
// Scale margin, padding, border
let scaled_margin : @types.Rect[Double] = {
left: layout.margin.left * total_scale_x,
right: layout.margin.right * total_scale_x,
top: layout.margin.top * total_scale_y,
bottom: layout.margin.bottom * total_scale_y,
}
let scaled_padding : @types.Rect[Double] = {
left: layout.padding.left * total_scale_x,
right: layout.padding.right * total_scale_x,
top: layout.padding.top * total_scale_y,
bottom: layout.padding.bottom * total_scale_y,
}
let scaled_border : @types.Rect[Double] = {
left: layout.border.left * total_scale_x,
right: layout.border.right * total_scale_x,
top: layout.border.top * total_scale_y,
bottom: layout.border.bottom * total_scale_y,
}
{
id: layout.id,
x: scaled_x,
y: scaled_y,
width: scaled_width,
height: scaled_height,
margin: scaled_margin,
padding: scaled_padding,
border: scaled_border,
overflow_x: layout.overflow_x,
overflow_y: layout.overflow_y,
scroll_width: layout.scroll_width * total_scale_x,
scroll_height: layout.scroll_height * total_scale_y,
children: scaled_children,
text: layout.text,
}
}