From aa7f8e7b1dc4fe0c3f5c041455a865b636887bfb Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Thu, 30 Jul 2026 00:46:39 +0200 Subject: [PATCH 1/2] LibWeb: Move the absolutely positioned layout engine into its own file --- .../LibWeb/Rust/src/layout/abspos_engine.rs | 2027 +++++++++++++++++ .../Rust/src/layout/formatting_context.rs | 2023 ---------------- Libraries/LibWeb/Rust/src/layout/mod.rs | 1 + 3 files changed, 2028 insertions(+), 2023 deletions(-) create mode 100644 Libraries/LibWeb/Rust/src/layout/abspos_engine.rs diff --git a/Libraries/LibWeb/Rust/src/layout/abspos_engine.rs b/Libraries/LibWeb/Rust/src/layout/abspos_engine.rs new file mode 100644 index 0000000000000..d7ee87cb8584c --- /dev/null +++ b/Libraries/LibWeb/Rust/src/layout/abspos_engine.rs @@ -0,0 +1,2027 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +fn axis_modes(style: StyleValues) -> (AbsposAxisMode, AbsposAxisMode) { + ( + if style.inset_left().is_auto() && style.inset_right().is_auto() { + AbsposAxisMode::StaticPosition + } else { + AbsposAxisMode::InsetFromRect + }, + if style.inset_top().is_auto() && style.inset_bottom().is_auto() { + AbsposAxisMode::StaticPosition + } else { + AbsposAxisMode::InsetFromRect + }, + ) +} + +pub(crate) fn aligned_static_offset( + static_position_rect: StaticPositionRect, + margin_box_inline_size: CssPixels, + margin_box_block_size: CssPixels, +) -> LogicalOffset { + let mut offset = static_position_rect.rect.offset; + match static_position_rect.inline_alignment { + StaticPositionAlignment::Start => {} + StaticPositionAlignment::Center => { + offset.inline_offset += (static_position_rect.rect.size.inline_size - margin_box_inline_size) / 2; + } + StaticPositionAlignment::End => { + offset.inline_offset += static_position_rect.rect.size.inline_size - margin_box_inline_size; + } + } + match static_position_rect.block_alignment { + StaticPositionAlignment::Start => {} + StaticPositionAlignment::Center => { + offset.block_offset += (static_position_rect.rect.size.block_size - margin_box_block_size) / 2; + } + StaticPositionAlignment::End => { + offset.block_offset += static_position_rect.rect.size.block_size - margin_box_block_size; + } + } + offset +} + +pub(crate) struct AbsposEngine<'a, 'pass> { + state: &'pass LayoutState, + callbacks: FfiLayoutFcCallbacks, + layout_mode: LayoutMode, + context_box: Node, + grid_context: Option<&'a GridFormattingContext<'pass>>, +} + +impl<'a, 'pass> AbsposEngine<'a, 'pass> { + fn new( + state: &'pass LayoutState, + callbacks: FfiLayoutFcCallbacks, + layout_mode: LayoutMode, + context_box: Node, + grid_context: Option<&'a GridFormattingContext<'pass>>, + ) -> Self { + assert!(!context_box.is_invalid()); + Self { + state, + callbacks, + layout_mode, + context_box, + grid_context, + } + } + + fn sizing(&self) -> SizingContext<'_> { + SizingContext::new(self.state, self.callbacks) + } + + fn style(&self, node: Node) -> StyleValues<'pass> { + self.state.style_facts(&self.callbacks, node) + } + + fn facts(&self, node: Node) -> NodeFacts<'_> { + self.state.node_facts(&self.callbacks, node) + } + + fn used_pointer(&self, node: Node) -> &'pass UsedValues { + self.state.used_values(&self.callbacks, node) + } + + fn try_used_pointer(&self, node: Node) -> Option<&'pass UsedValues> { + self.state.try_used_values(&self.callbacks, node) + } + + fn used(&self, node: Node) -> &'pass UsedValues { + self.used_pointer(node) + } + + fn used_mut(&self, node: Node) -> &'pass UsedValues { + self.used_pointer(node) + } + + fn static_position_containing_block(&self, node: Node) -> Node { + unsafe { (self.callbacks.static_position_containing_block)(self.callbacks.context, self.callbacks.shell(node)) } + } + + fn inline_containing_block(&self, node: Node) -> Node { + self.callbacks.inline_containing_block(node) + } + + fn non_anonymous_containing_block(&self, node: Node) -> Node { + self.callbacks.non_anonymous_containing_block(node) + } + + fn node_is_ancestor(&self, ancestor: Node, node: Node) -> bool { + self.callbacks.is_ancestor(ancestor, node) + } + + fn belongs_to_inline_containing_block(&self, inline_node: Node, node: Node) -> bool { + !self.facts(node).is_anonymous() && self.node_is_ancestor(inline_node, node) + } + + fn resolve_static_position_relative_to_containing_block( + &self, + node: Node, + static_position_rect: StaticPositionRect, + ) -> StaticPositionRect { + let static_position_cb = self.static_position_containing_block(node); + let actual_containing_block = self.callbacks.containing_block(node); + if static_position_cb.is_invalid() || static_position_cb == actual_containing_block { + return static_position_rect; + } + + let mut merge_point = static_position_cb; + while merge_point != actual_containing_block && !self.node_is_ancestor(merge_point, actual_containing_block) { + merge_point = self.callbacks.containing_block(merge_point); + assert!(!merge_point.is_invalid()); + } + + let offset_relative_to_merge_point = |descendant: Node| { + let mut offset = FfiCssPixelPoint::default(); + let mut current = descendant; + while current != merge_point { + let used = self.used(current); + offset = point_add(offset, used.content_offset.get()); + current = self.callbacks.containing_block(current); + assert!(!current.is_invalid()); + } + offset + }; + translate_static_position_between_chains( + static_position_rect, + offset_relative_to_merge_point(static_position_cb), + offset_relative_to_merge_point(actual_containing_block), + ) + } + + fn line_fragments(&self, node: Node) -> Vec { + let mut fragments = Vec::new(); + let Some(lines) = self.state.line_data(self.callbacks.slot_index(node)) else { + return fragments; + }; + for line in &lines.line_boxes { + for fragment in &line.fragments { + let (x, y) = fragment.offset(); + let (width, height) = fragment.size(); + fragments.push(LineFragmentFacts { + layout_node: fragment.layout_node, + is_atomic_inline: fragment.is_atomic_inline, + writing_mode: fragment.writing_mode, + style_block_axis_is_reverse: fragment.style_block_axis_is_reverse, + inline_offset: fragment.inline_offset, + block_offset: fragment.block_offset, + offset: FfiCssPixelPoint { x, y }, + size: FfiCssPixelPoint { x: width, y: height }, + }); + } + } + fragments + } + + fn add_atomic_inline_fragment_rect( + &self, + inline_node: Node, + fragment: LineFragmentFacts, + offset: FfiCssPixelPoint, + bounding_rect: &mut Option, + empty_bounding_rect: &mut Option, + ) { + let Some(child_used) = self.try_used_pointer(fragment.layout_node) else { + return; + }; + let collapsed = child_used.uses_collapsing_borders_model.get(); + let is_horizontal = fragment.writing_mode == writing_mode::HORIZONTAL_TB; + let inline_axis_border_box_start = fragment.inline_offset + - if is_horizontal { + child_used.border_box_left(collapsed) + } else { + child_used.border_box_top(collapsed) + }; + let inline_axis_border_box_extent = if is_horizontal { + child_used.border_box_inline_size(collapsed) + } else { + child_used.border_box_block_size(collapsed) + }; + let block_axis_line_height = self.style(inline_node).line_height; + let block_axis_start = if fragment.style_block_axis_is_reverse { + fragment.block_offset + child_used.border_box_right(collapsed) - block_axis_line_height + } else { + fragment.block_offset + - if is_horizontal { + child_used.border_box_top(collapsed) + } else { + child_used.border_box_left(collapsed) + } + }; + let rect = if is_horizontal { + PhysicalRect { + x: inline_axis_border_box_start, + y: block_axis_start, + width: inline_axis_border_box_extent, + height: block_axis_line_height, + } + } else { + PhysicalRect { + x: block_axis_start, + y: inline_axis_border_box_start, + width: block_axis_line_height, + height: inline_axis_border_box_extent, + } + } + .translated(offset); + add_fragment_rect(rect, bounding_rect, empty_bounding_rect); + } + + fn walk_inline_containing_block( + &self, + inline_node: Node, + node: Node, + offset: FfiCssPixelPoint, + bounding_rect: &mut Option, + empty_bounding_rect: &mut Option, + ) { + for fragment in self.line_fragments(node) { + if !self.belongs_to_inline_containing_block(inline_node, fragment.layout_node) { + continue; + } + if fragment.is_atomic_inline { + self.add_atomic_inline_fragment_rect(inline_node, fragment, offset, bounding_rect, empty_bounding_rect); + continue; + } + add_fragment_rect( + PhysicalRect { + x: fragment.offset.x + offset.x, + y: fragment.offset.y + offset.y, + width: fragment.size.x, + height: fragment.size.y, + }, + bounding_rect, + empty_bounding_rect, + ); + } + + let mut child = self.callbacks.first_child(node); + while !child.is_invalid() { + let next = self.callbacks.next_sibling(child); + let facts = self.facts(child); + if facts.is_absolutely_positioned() || facts.is_floating() { + child = next; + continue; + } + let child_used_pointer = self.try_used_pointer(child); + let child_offset = if let Some(child_used) = child_used_pointer { + point_add(offset, child_used.content_offset.get()) + } else { + offset + }; + if facts.is_box() && !facts.is_anonymous() { + if !self.belongs_to_inline_containing_block(inline_node, child) { + child = next; + continue; + } + if facts.is_atomic_inline() { + child = next; + continue; + } + if let Some(child_used) = child_used_pointer { + let collapsed = child_used.uses_collapsing_borders_model.get(); + let border_box_origin = FfiCssPixelPoint { + x: child_offset.x - child_used.border_left_collapsed(collapsed) - child_used.padding_left.get(), + y: child_offset.y - child_used.border_top_collapsed(collapsed) - child_used.padding_top.get(), + }; + add_fragment_rect( + PhysicalRect { + x: border_box_origin.x, + y: border_box_origin.y, + width: child_used.border_box_inline_size(collapsed), + height: child_used.border_box_block_size(collapsed), + }, + bounding_rect, + empty_bounding_rect, + ); + } + } + self.walk_inline_containing_block(inline_node, child, child_offset, bounding_rect, empty_bounding_rect); + child = next; + } + } + + fn compute_inline_containing_block_rect( + &self, + inline_node: Node, + abspos_containing_block: Node, + ) -> Option { + if self.facts(inline_node).is_anonymous() { + return None; + } + let outer_block = self.non_anonymous_containing_block(inline_node); + if outer_block.is_invalid() { + return None; + } + + let mut outer_offset = FfiCssPixelPoint::default(); + let mut ancestor = outer_block; + while !ancestor.is_invalid() && ancestor != abspos_containing_block { + let used = self.try_used_pointer(ancestor); + if let Some(used) = used { + outer_offset = point_add(outer_offset, used.content_offset.get()); + } + ancestor = self.callbacks.parent(ancestor); + } + + let mut bounding_rect = None; + let mut empty_bounding_rect = None; + self.walk_inline_containing_block( + inline_node, + outer_block, + outer_offset, + &mut bounding_rect, + &mut empty_bounding_rect, + ); + let mut rect = bounding_rect.or(empty_bounding_rect)?; + if let Some(inline_used) = self.try_used_pointer(inline_node) { + rect.x -= inline_used.padding_left.get(); + rect.y -= inline_used.padding_top.get(); + rect.width += inline_used.padding_left.get() + inline_used.padding_right.get(); + rect.height += inline_used.padding_top.get() + inline_used.padding_bottom.get(); + } + Some(rect) + } + + fn base_containing_block_info(&self, node: Node) -> AbsposContainingBlockInfo { + let style = self.style(node); + let (inline_axis_mode, block_axis_mode) = axis_modes(style); + let containing_block = self.callbacks.containing_block(node); + assert!(!containing_block.is_invalid()); + let inline_containing_block = self.inline_containing_block(node); + if !inline_containing_block.is_invalid() + && let Some(rect) = self.compute_inline_containing_block_rect(inline_containing_block, containing_block) + { + return AbsposContainingBlockInfo { + rect: LogicalRect { + offset: LogicalOffset { + inline_offset: rect.x, + block_offset: rect.y, + }, + size: LogicalSize { + inline_size: rect.width, + block_size: rect.height, + }, + }, + inline_axis_mode, + block_axis_mode, + inline_alignment: None, + block_alignment: None, + derives_from_own_computed_values: false, + }; + } + + let containing_block_used = self.used(containing_block); + AbsposContainingBlockInfo { + rect: LogicalRect { + offset: LogicalOffset { + inline_offset: -containing_block_used.padding_left.get(), + block_offset: -containing_block_used.padding_top.get(), + }, + size: LogicalSize { + inline_size: containing_block_used.content_inline_size.get() + + containing_block_used.padding_left.get() + + containing_block_used.padding_right.get(), + block_size: containing_block_used.content_block_size.get() + + containing_block_used.padding_top.get() + + containing_block_used.padding_bottom.get(), + }, + }, + inline_axis_mode, + block_axis_mode, + inline_alignment: None, + block_alignment: None, + derives_from_own_computed_values: false, + } + } + + fn containing_block_info(&self, node: Node) -> AbsposContainingBlockInfo { + let base = self.base_containing_block_info(node); + let Some(grid) = self.grid_context else { + return base; + }; + let mut info = grid.abspos_containing_block_info(node); + let uses_grid_area_as_static_position = self.static_position_containing_block(node) == self.context_box; + if !uses_grid_area_as_static_position { + info.inline_axis_mode = base.inline_axis_mode; + info.block_axis_mode = base.block_axis_mode; + } + info + } +} + +fn add_fragment_rect( + rect: PhysicalRect, + bounding_rect: &mut Option, + empty_bounding_rect: &mut Option, +) { + let destination = if rect.is_empty() { + empty_bounding_rect + } else { + bounding_rect + }; + *destination = Some(destination.map_or(rect, |existing| existing.union(rect))); +} + +fn calc_node_create_px_dimension(value: f64) -> *const c_void { + crate::css::calc::rust_calc_node_create_numeric_dimension( + CALC_NUMERIC_KIND_LENGTH, + value, + crate::css::style_compute::px_length_unit(), + ) + .cast() +} + +struct AnchorResolutionState { + default_anchor_box: Node, + compensates_for_horizontal_scroll: bool, + compensates_for_vertical_scroll: bool, +} + +#[derive(Clone, Copy)] +struct AnchorValueAxis { + is_from_end: bool, + is_horizontal: bool, + containing_block_extent: CssPixels, +} + +#[derive(Clone, Copy)] +struct AnchorCalcCallbackContext<'a, 'pass> { + engine: *const AbsposEngine<'a, 'pass>, + positioned_box: Node, + containing_block: Node, + is_from_end: bool, + is_horizontal_axis: bool, + containing_block_extent: CssPixels, + resolution_state: *mut AnchorResolutionState, +} + +impl AbsposEngine<'_, '_> { + fn anchor_lookup(&self, positioned_box: Node, anchor_name: usize) -> Option { + let eligible_anchor_boxes = self.state.used_value_nodes(); + let eligible_anchor_shells = eligible_anchor_boxes + .iter() + .map(|&node| self.callbacks.shell(node)) + .collect::>(); + // SAFETY: The name handle is retained by either the style snapshot or + // the live anchor() shell. The eligible-node slice is borrowed only + // for this synchronous lookup. + let anchor_box = unsafe { + (self.callbacks.anchor_lookup)( + self.callbacks.context, + self.callbacks.shell(positioned_box), + anchor_name, + eligible_anchor_shells.as_ptr(), + eligible_anchor_shells.len(), + ) + }; + (!anchor_box.is_invalid()).then_some(anchor_box) + } + + fn nearest_scroll_container_ancestor(&self, node: Node) -> Node { + let mut ancestor = self.callbacks.containing_block(node); + while !ancestor.is_invalid() { + if self.facts(ancestor).is_scroll_container() { + return ancestor; + } + ancestor = self.callbacks.containing_block(ancestor); + } + NodeSlotId::INVALID + } + + fn anchor_rect(&self, anchor_box: Node, containing_block: Node) -> PhysicalRect { + let anchor_state = self.used(anchor_box); + let mut anchor_offset = FfiCssPixelPoint::default(); + let mut node = anchor_box; + while node != containing_block { + assert!(!node.is_invalid()); + anchor_offset = point_add(anchor_offset, self.used(node).content_offset.get()); + node = self.callbacks.containing_block(node); + } + anchor_rect_from_geometry(anchor_state, self.used(containing_block), anchor_offset) + } + + fn anchor_side( + &self, + facts: FfiAnchorFunctionFacts, + rect: PhysicalRect, + positioned_box: Node, + containing_block: Node, + is_from_end: bool, + is_horizontal_axis: bool, + ) -> Option { + let containing_block_direction = self.style(containing_block).direction; + let box_direction = self.style(positioned_box).direction; + match facts.side_kind { + FfiAnchorSideKind::Invalid => None, + FfiAnchorSideKind::Top => (!is_horizontal_axis).then_some(rect.top()), + FfiAnchorSideKind::Bottom => (!is_horizontal_axis).then_some(rect.bottom()), + FfiAnchorSideKind::Left => is_horizontal_axis.then_some(rect.left()), + FfiAnchorSideKind::Right => is_horizontal_axis.then_some(rect.right()), + FfiAnchorSideKind::Center => Some(if is_horizontal_axis { + rect.left() + rect.width / 2 + } else { + rect.top() + rect.height / 2 + }), + FfiAnchorSideKind::Start | FfiAnchorSideKind::End => { + let is_start = facts.side_kind == FfiAnchorSideKind::Start; + if is_horizontal_axis { + let use_left = (containing_block_direction == direction::LTR) == is_start; + Some(if use_left { rect.left() } else { rect.right() }) + } else { + Some(if is_start { rect.top() } else { rect.bottom() }) + } + } + FfiAnchorSideKind::SelfStart | FfiAnchorSideKind::SelfEnd => { + let is_start = facts.side_kind == FfiAnchorSideKind::SelfStart; + if is_horizontal_axis { + let use_left = (box_direction == direction::LTR) == is_start; + Some(if use_left { rect.left() } else { rect.right() }) + } else { + Some(if is_start { rect.top() } else { rect.bottom() }) + } + } + FfiAnchorSideKind::Inside | FfiAnchorSideKind::Outside => { + let same_side = facts.side_kind == FfiAnchorSideKind::Inside; + if is_horizontal_axis { + Some(if is_from_end == same_side { + rect.right() + } else { + rect.left() + }) + } else { + Some(if is_from_end == same_side { + rect.bottom() + } else { + rect.top() + }) + } + } + FfiAnchorSideKind::Percentage => { + if is_horizontal_axis { + let (start, end) = if containing_block_direction == direction::LTR { + (rect.left(), rect.right()) + } else { + (rect.right(), rect.left()) + }; + Some(start + CssPixels::nearest_value_for((end - start).to_double() * facts.side_percentage)) + } else { + Some(rect.top() + CssPixels::nearest_value_for(rect.height.to_double() * facts.side_percentage)) + } + } + } + } + + fn note_resolved_anchor_function( + &self, + anchor_box: Node, + is_horizontal_axis: bool, + state: &mut AnchorResolutionState, + ) { + if state.default_anchor_box.is_invalid() { + return; + } + if anchor_box != state.default_anchor_box + && self.nearest_scroll_container_ancestor(anchor_box) + != self.nearest_scroll_container_ancestor(state.default_anchor_box) + { + return; + } + if is_horizontal_axis { + state.compensates_for_horizontal_scroll = true; + } else { + state.compensates_for_vertical_scroll = true; + } + } + + fn resolve_anchor_value( + &self, + value: FfiSizeValue, + positioned_box: Node, + containing_block: Node, + axis: AnchorValueAxis, + resolution_state: &mut AnchorResolutionState, + ) -> Option { + assert!(value.contains_anchor_function); + assert!(!value.calc.is_null()); + let mut callback_context = AnchorCalcCallbackContext { + engine: self, + positioned_box, + containing_block, + is_from_end: axis.is_from_end, + is_horizontal_axis: axis.is_horizontal, + containing_block_extent: axis.containing_block_extent, + resolution_state, + }; + // SAFETY: The calculated handle is retained by the style cache and + // all callback state remains live for this synchronous resolution. + let result = unsafe { + resolve_calc_with_external_resolutions( + value.calc, + axis.containing_block_extent, + (&raw mut callback_context).cast(), + Some(resolve_anchor_non_math_function), + ) + }; + result.resolved.then(|| CssPixels::nearest_value_for(result.value)) + } + + fn resolve_anchor_insets(&self, node: Node) { + // Clear a stale default scroll shift before any early return. + // SAFETY: The node is live and a null anchor clears the weak target. + unsafe { + (self.callbacks.set_default_scroll_shift)( + self.callbacks.context, + self.callbacks.shell(node), + std::ptr::null_mut(), + false, + false, + ); + } + + let style = self.style(node); + let top_contains_anchor = style.inset_top().contains_anchor_function; + let right_contains_anchor = style.inset_right().contains_anchor_function; + let bottom_contains_anchor = style.inset_bottom().contains_anchor_function; + let left_contains_anchor = style.inset_left().contains_anchor_function; + if !top_contains_anchor && !right_contains_anchor && !bottom_contains_anchor && !left_contains_anchor { + return; + } + + let containing_block = self.callbacks.containing_block(node); + if containing_block.is_invalid() { + return; + } + let containing_block_state = self.used(containing_block); + let default_anchor_box = if style.has_position_anchor() { + self.anchor_lookup(node, style.position_anchor_name()) + .unwrap_or(NodeSlotId::INVALID) + } else { + NodeSlotId::INVALID + }; + let mut resolution_state = AnchorResolutionState { + default_anchor_box, + compensates_for_horizontal_scroll: false, + compensates_for_vertical_scroll: false, + }; + let mut resolved = FfiResolvedAnchorInsets::default(); + + if top_contains_anchor { + let value = self.resolve_anchor_value( + style.inset_top(), + node, + containing_block, + AnchorValueAxis { + is_from_end: false, + is_horizontal: false, + containing_block_extent: containing_block_state.content_block_size.get() + + containing_block_state.padding_top.get() + + containing_block_state.padding_bottom.get(), + }, + &mut resolution_state, + ); + resolved.resolves_top = true; + resolved.top_is_auto = value.is_none(); + resolved.top = value.unwrap_or_default(); + } + if right_contains_anchor { + let value = self.resolve_anchor_value( + style.inset_right(), + node, + containing_block, + AnchorValueAxis { + is_from_end: true, + is_horizontal: true, + containing_block_extent: containing_block_state.content_inline_size.get() + + containing_block_state.padding_left.get() + + containing_block_state.padding_right.get(), + }, + &mut resolution_state, + ); + resolved.resolves_right = true; + resolved.right_is_auto = value.is_none(); + resolved.right = value.unwrap_or_default(); + } + if bottom_contains_anchor { + let value = self.resolve_anchor_value( + style.inset_bottom(), + node, + containing_block, + AnchorValueAxis { + is_from_end: true, + is_horizontal: false, + containing_block_extent: containing_block_state.content_block_size.get() + + containing_block_state.padding_top.get() + + containing_block_state.padding_bottom.get(), + }, + &mut resolution_state, + ); + resolved.resolves_bottom = true; + resolved.bottom_is_auto = value.is_none(); + resolved.bottom = value.unwrap_or_default(); + } + if left_contains_anchor { + let value = self.resolve_anchor_value( + style.inset_left(), + node, + containing_block, + AnchorValueAxis { + is_from_end: false, + is_horizontal: true, + containing_block_extent: containing_block_state.content_inline_size.get() + + containing_block_state.padding_left.get() + + containing_block_state.padding_right.get(), + }, + &mut resolution_state, + ); + resolved.resolves_left = true; + resolved.left_is_auto = value.is_none(); + resolved.left = value.unwrap_or_default(); + } + + // SAFETY: The callback synchronously updates this live box's computed + // values with plain auto/px insets. + unsafe { + (self.callbacks.set_resolved_anchor_insets)(self.callbacks.context, self.callbacks.shell(node), resolved); + } + self.state + .replace_resolved_anchor_insets(&self.callbacks, node, resolved); + + if resolution_state.compensates_for_horizontal_scroll || resolution_state.compensates_for_vertical_scroll { + // SAFETY: The anchor and positioned box remain live through the + // pass; C++ stores the anchor as a weak pointer. + unsafe { + (self.callbacks.set_default_scroll_shift)( + self.callbacks.context, + self.callbacks.shell(node), + self.callbacks.shell(resolution_state.default_anchor_box), + resolution_state.compensates_for_horizontal_scroll, + resolution_state.compensates_for_vertical_scroll, + ); + } + } + } +} + +unsafe extern "C" fn resolve_anchor_non_math_function(context: *mut c_void, shell: *const c_void) -> *const c_void { + // SAFETY: The CSS calc engine calls this only during resolve_anchor_value, + // whose stack owns this callback context. + let context = unsafe { &mut *context.cast::>() }; + // SAFETY: The engine pointer is live for the enclosing resolution. + let engine = unsafe { &*context.engine }; + // SAFETY: `shell` is the live Rust style-value handle supplied by the + // CSS calc core. + let facts = unsafe { (engine.callbacks.build_anchor_function_facts)(engine.callbacks.context, shell) }; + let style = engine.style(context.positioned_box); + let anchor_name = if facts.has_anchor_name { + Some(facts.anchor_name) + } else if style.has_position_anchor() { + Some(style.position_anchor_name()) + } else { + None + }; + let mut resolved_node = std::ptr::null(); + if engine.facts(context.positioned_box).is_absolutely_positioned() + && let Some(anchor_name) = anchor_name + && let Some(anchor_box) = engine.anchor_lookup(context.positioned_box, anchor_name) + { + let rect = engine.anchor_rect(anchor_box, context.containing_block); + if let Some(side) = engine.anchor_side( + facts, + rect, + context.positioned_box, + context.containing_block, + context.is_from_end, + context.is_horizontal_axis, + ) { + // SAFETY: The state pointer is live and uniquely used by this + // synchronous resolver. + let resolution_state = unsafe { &mut *context.resolution_state }; + engine.note_resolved_anchor_function(anchor_box, context.is_horizontal_axis, resolution_state); + let inset = if context.is_from_end { + context.containing_block_extent - side + } else { + side + }; + // SAFETY: This CSS crate export transfers one Arc reference to + // the external-resolution snapshot, which releases it after + // calc resolution. + resolved_node = calc_node_create_px_dimension(inset.to_double()); + } + } + if facts.has_anchor_name { + // SAFETY: The C++ facts callback transferred one raw fly-string + // reference for this explicit anchor name. + unsafe { + (engine.callbacks.release_anchor_name_handle)(facts.anchor_name); + } + } + if !resolved_node.is_null() { + return resolved_node; + } + + // SAFETY: The callback borrows fallback data from the live anchor style + // value for this synchronous resolution. + let fallback = unsafe { (engine.callbacks.anchor_function_fallback)(engine.callbacks.context, shell) }; + match fallback.kind { + FfiAnchorFallbackKind::None => std::ptr::null(), + FfiAnchorFallbackKind::Px => calc_node_create_px_dimension(fallback.px.to_double()), + FfiAnchorFallbackKind::Percentage => { + calc_node_create_px_dimension(context.containing_block_extent.to_double() * fallback.fraction) + } + FfiAnchorFallbackKind::Calculated => { + assert!(!fallback.value.is_null()); + let mut nested_context = *context; + let resolved = unsafe { + resolve_calc_with_external_resolutions( + fallback.value, + context.containing_block_extent, + (&raw mut nested_context).cast(), + Some(resolve_anchor_non_math_function), + ) + }; + if !resolved.resolved { + return std::ptr::null(); + } + calc_node_create_px_dimension(resolved.value) + } + FfiAnchorFallbackKind::Anchor => { + assert!(!fallback.value.is_null()); + let mut nested_context = *context; + unsafe { resolve_anchor_non_math_function((&raw mut nested_context).cast(), fallback.value) } + } + } +} + +type AutoPx = Option; + +fn resolve_or_auto(value: FfiSizeValue, basis: CssPixels) -> AutoPx { + (!value.is_auto()).then(|| value.to_px(basis)) +} + +fn auto_px_value(value: AutoPx) -> CssPixels { + value.unwrap_or_default() +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn solve_abspos_axis_for( + available: CssPixels, + target: AutoPx, + clamp_to_zero: bool, + start: AutoPx, + margin_start: AutoPx, + border_start: CssPixels, + padding_start: CssPixels, + size: AutoPx, + padding_end: CssPixels, + border_end: CssPixels, + margin_end: AutoPx, + end: AutoPx, +) -> CssPixels { + let value = available + - auto_px_value(start) + - auto_px_value(margin_start) + - border_start + - padding_start + - auto_px_value(size) + - padding_end + - border_end + - auto_px_value(margin_end) + - auto_px_value(end) + + auto_px_value(target); + if clamp_to_zero { + value.max(CssPixels::default()) + } else { + value + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct ReplacedAxisSolution { + pub(crate) start: CssPixels, + pub(crate) end: CssPixels, + pub(crate) margin_start: CssPixels, + pub(crate) margin_end: CssPixels, +} + +#[derive(Clone, Copy)] +pub(crate) struct ReplacedAxisBehavior { + pub(crate) clear_auto_margins_if_start_is_auto: bool, + pub(crate) clear_negative_auto_margins: bool, +} + +pub(crate) fn solve_replaced_axis( + available: CssPixels, + mut start: AutoPx, + mut end: AutoPx, + mut margin_start: AutoPx, + mut margin_end: AutoPx, + static_offset: CssPixels, + behavior: ReplacedAxisBehavior, +) -> ReplacedAxisSolution { + if start.is_none() && end.is_none() { + start = Some(static_offset); + } + if end.is_none() || (behavior.clear_auto_margins_if_start_is_auto && start.is_none()) { + if margin_start.is_none() { + margin_start = Some(CssPixels::default()); + } + if margin_end.is_none() { + margin_end = Some(CssPixels::default()); + } + } + if margin_start.is_none() && margin_end.is_none() { + let remainder = available - auto_px_value(start) - auto_px_value(end); + if behavior.clear_negative_auto_margins && remainder < CssPixels::default() { + // This deliberately matches the C++ inline-axis implementation, + // which zeroes both margins instead of solving the end margin. + margin_start = Some(CssPixels::default()); + margin_end = Some(CssPixels::default()); + } else { + margin_start = Some(remainder / 2); + margin_end = Some(remainder / 2); + } + } + if start.is_none() { + start = Some(available - auto_px_value(end) - auto_px_value(margin_start) - auto_px_value(margin_end)); + } else if end.is_none() { + end = Some(available - auto_px_value(start) - auto_px_value(margin_start) - auto_px_value(margin_end)); + } else if margin_start.is_none() { + margin_start = Some(available - auto_px_value(start) - auto_px_value(end) - auto_px_value(margin_end)); + } else if margin_end.is_none() { + margin_end = Some(available - auto_px_value(start) - auto_px_value(margin_start) - auto_px_value(end)); + } + if CssPixels::default() + != available + - auto_px_value(start) + - auto_px_value(end) + - auto_px_value(margin_start) + - auto_px_value(margin_end) + { + end = Some(available - auto_px_value(start) - auto_px_value(margin_start) - auto_px_value(margin_end)); + } + ReplacedAxisSolution { + start: auto_px_value(start), + end: auto_px_value(end), + margin_start: auto_px_value(margin_start), + margin_end: auto_px_value(margin_end), + } +} + +impl AbsposEngine<'_, '_> { + fn static_offset(&self, node: Node, rect: StaticPositionRect) -> LogicalOffset { + let used = self.used(node); + let collapsed = used.uses_collapsing_borders_model.get(); + aligned_static_offset( + rect, + used.margin_box_inline_size(collapsed), + used.margin_box_block_size(collapsed), + ) + } + + fn solve_non_replaced_inline_once( + &self, + node: Node, + containing_block_inline_size: CssPixels, + _available_space: AvailableSpace, + constraints: ContainingBlockConstraints, + static_position_rect: StaticPositionRect, + input_inline_size: AutoPx, + ) -> (AutoPx, CssPixels, CssPixels, AutoPx, AutoPx) { + let style = self.style(node); + let used = self.used(node); + let border_left = style.border_left_width; + let border_right = style.border_right_width; + let padding_left = used.padding_left.get(); + let padding_right = used.padding_right.get(); + let computed_left = style.inset_left(); + let computed_right = style.inset_right(); + let mut left = style.inset_left().to_px(containing_block_inline_size); + let mut right = style.inset_right().to_px(containing_block_inline_size); + let mut margin_left = resolve_or_auto(style.margin_left(), containing_block_inline_size); + let mut margin_right = resolve_or_auto(style.margin_right(), containing_block_inline_size); + let mut inline_size = input_inline_size; + + let solve_for_left = |inline_size: AutoPx, margin_left: AutoPx, margin_right: AutoPx, right: CssPixels| { + containing_block_inline_size + - auto_px_value(margin_left) + - border_left + - padding_left + - auto_px_value(inline_size) + - padding_right + - border_right + - auto_px_value(margin_right) + - right + }; + let solve_for_inline_size = |left: CssPixels, margin_left: AutoPx, margin_right: AutoPx, right: CssPixels| { + (containing_block_inline_size + - left + - auto_px_value(margin_left) + - border_left + - padding_left + - padding_right + - border_right + - auto_px_value(margin_right) + - right) + .max(CssPixels::default()) + }; + let solve_for_right = |left: CssPixels, inline_size: AutoPx, margin_left: AutoPx, margin_right: AutoPx| { + containing_block_inline_size + - left + - auto_px_value(margin_left) + - border_left + - padding_left + - auto_px_value(inline_size) + - padding_right + - border_right + - auto_px_value(margin_right) + }; + let shrink_to_fit = |left: CssPixels, margin_left: AutoPx, margin_right: AutoPx, right: CssPixels| { + let available = solve_for_inline_size(left, margin_left, margin_right, right); + let sizing = self.sizing(); + let preferred = sizing.calculate_max_content_inline_size(node, constraints); + if preferred <= available { + preferred + } else { + let preferred_minimum = sizing.calculate_min_content_inline_size(node, constraints); + preferred_minimum.max(available).min(preferred) + } + }; + + if computed_left.is_auto() && inline_size.is_none() && computed_right.is_auto() { + if margin_left.is_none() { + margin_left = Some(CssPixels::default()); + } + if margin_right.is_none() { + margin_right = Some(CssPixels::default()); + } + let content_inline_size = shrink_to_fit(left, margin_left, margin_right, right); + inline_size = Some(content_inline_size); + self.used_mut(node).set_content_inline_size(content_inline_size); + left = self.static_offset(node, static_position_rect).inline_offset; + right = solve_for_right(left, inline_size, margin_left, margin_right); + } + + if !computed_left.is_auto() && inline_size.is_some() && !computed_right.is_auto() { + let available_for_margins = containing_block_inline_size + - border_left + - padding_left + - auto_px_value(inline_size) + - padding_right + - border_right + - left + - right; + if margin_left.is_none() && margin_right.is_none() { + margin_left = Some(available_for_margins / 2); + margin_right = Some(available_for_margins / 2); + return (inline_size, left, right, margin_left, margin_right); + } + if margin_left.is_none() { + margin_left = Some(available_for_margins); + return (inline_size, left, right, margin_left, margin_right); + } + if margin_right.is_none() { + margin_right = Some(available_for_margins); + return (inline_size, left, right, margin_left, margin_right); + } + right = solve_for_right(left, inline_size, margin_left, margin_right); + return (inline_size, left, right, margin_left, margin_right); + } + + if margin_left.is_none() { + margin_left = Some(CssPixels::default()); + } + if margin_right.is_none() { + margin_right = Some(CssPixels::default()); + } + + if computed_left.is_auto() && inline_size.is_none() && !computed_right.is_auto() { + inline_size = Some(shrink_to_fit(left, margin_left, margin_right, right)); + left = solve_for_left(inline_size, margin_left, margin_right, right); + } else if computed_left.is_auto() && computed_right.is_auto() && inline_size.is_some() { + left = self.static_offset(node, static_position_rect).inline_offset; + right = solve_for_right(left, inline_size, margin_left, margin_right); + } else if inline_size.is_none() && computed_right.is_auto() && !computed_left.is_auto() { + inline_size = Some(shrink_to_fit(left, margin_left, margin_right, right)); + right = solve_for_right(left, inline_size, margin_left, margin_right); + } else if computed_left.is_auto() && inline_size.is_some() && !computed_right.is_auto() { + left = solve_for_left(inline_size, margin_left, margin_right, right); + } else if inline_size.is_none() && !computed_left.is_auto() && !computed_right.is_auto() { + inline_size = Some(solve_for_inline_size(left, margin_left, margin_right, right)); + } else if computed_right.is_auto() && !computed_left.is_auto() && inline_size.is_some() { + right = solve_for_right(left, inline_size, margin_left, margin_right); + } + + (inline_size, left, right, margin_left, margin_right) + } + + fn compute_inline_size_for_non_replaced( + &self, + node: Node, + available_space: AvailableSpace, + constraints: ContainingBlockConstraints, + static_position_rect: StaticPositionRect, + ) { + let containing_block_inline_size = available_space.inline_size.to_px_or_zero(); + let style = self.style(node); + let sizing = self.sizing(); + let initial = if self.facts(node).is_table_wrapper() { + Some(sizing.compute_table_box_inline_size_inside_wrapper( + node, + available_space, + constraints, + None, + crate::layout::TableWrapperInlineSizeMode::ClampToAvailableInlineSize, + )) + } else if style.width().is_auto() { + None + } else { + Some(sizing.calculate_inner_inline_size(node, available_space.inline_size, style.width(), constraints)) + }; + let (mut used_inline_size, mut left, mut right, mut margin_left, mut margin_right) = self + .solve_non_replaced_inline_once( + node, + containing_block_inline_size, + available_space, + constraints, + static_position_rect, + initial, + ); + + if !sizing.should_treat_max_inline_size_as_none(node, available_space.inline_size, constraints) { + let max_inline_size = + sizing.calculate_inner_inline_size(node, available_space.inline_size, style.max_width(), constraints); + if auto_px_value(used_inline_size) > max_inline_size { + (used_inline_size, left, right, margin_left, margin_right) = self.solve_non_replaced_inline_once( + node, + containing_block_inline_size, + available_space, + constraints, + static_position_rect, + Some(max_inline_size), + ); + } + } + if !style.min_width().is_auto() { + let min_inline_size = + sizing.calculate_inner_inline_size(node, available_space.inline_size, style.min_width(), constraints); + if auto_px_value(used_inline_size) < min_inline_size { + (used_inline_size, left, right, margin_left, margin_right) = self.solve_non_replaced_inline_once( + node, + containing_block_inline_size, + available_space, + constraints, + static_position_rect, + Some(min_inline_size), + ); + } + } + + let used = self.used_mut(node); + used.set_content_inline_size(auto_px_value(used_inline_size)); + used.inset_left.set(left); + used.inset_right.set(right); + used.margin_left.set(auto_px_value(margin_left)); + used.margin_right.set(auto_px_value(margin_right)); + } + + fn compute_inline_size_for_replaced( + &self, + node: Node, + available_space: AvailableSpace, + constraints: ContainingBlockConstraints, + static_position_rect: StaticPositionRect, + ) { + let sizing = self.sizing(); + let inline_size = sizing.compute_inline_size_for_replaced_element(node, available_space, constraints); + let containing_block_inline_size = available_space.inline_size.to_px_or_zero(); + let style = self.style(node); + let used = self.used(node); + let available = containing_block_inline_size + - inline_size + - style.border_left_width + - used.padding_left.get() + - used.padding_right.get() + - style.border_right_width; + let solution = solve_replaced_axis( + available, + resolve_or_auto(style.inset_left(), containing_block_inline_size), + resolve_or_auto(style.inset_right(), containing_block_inline_size), + resolve_or_auto(style.margin_left(), containing_block_inline_size), + resolve_or_auto(style.margin_right(), containing_block_inline_size), + self.static_offset(node, static_position_rect).inline_offset, + ReplacedAxisBehavior { + clear_auto_margins_if_start_is_auto: true, + clear_negative_auto_margins: true, + }, + ); + + let used = self.used_mut(node); + used.inset_left.set(solution.start); + used.inset_right.set(solution.end); + used.margin_left.set(solution.margin_start); + used.margin_right.set(solution.margin_end); + used.set_content_inline_size(inline_size); + } + + fn compute_inline_size( + &self, + node: Node, + available_space: AvailableSpace, + constraints: ContainingBlockConstraints, + static_position_rect: StaticPositionRect, + ) { + if self + .sizing() + .box_is_sized_as_replaced_element(node, available_space, constraints) + { + self.compute_inline_size_for_replaced(node, available_space, constraints, static_position_rect); + } else { + self.compute_inline_size_for_non_replaced(node, available_space, constraints, static_position_rect); + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum BlockSizePass { + BeforeInsideLayout, + AfterInsideLayout, +} + +impl AbsposEngine<'_, '_> { + fn apply_min_max_block_size_constraints( + &self, + node: Node, + available_space: AvailableSpace, + constraints: ContainingBlockConstraints, + unconstrained: AutoPx, + ) -> AutoPx { + let style = self.style(node); + let sizing = self.sizing(); + let mut constrained = unconstrained; + if !style.max_height().is_none() { + let maximum = sizing.calculate_inner_block_size(node, available_space, style.max_height(), constraints); + if maximum < auto_px_value(constrained) { + constrained = Some(maximum); + } + } + if !style.min_height().is_auto() { + let minimum = sizing.calculate_inner_block_size(node, available_space, style.min_height(), constraints); + if minimum > auto_px_value(constrained) { + constrained = Some(minimum); + } + } + constrained + } + + fn automatic_block_size( + &self, + node: Node, + available_space: AvailableSpace, + constraints: ContainingBlockConstraints, + pass: BlockSizePass, + ) -> AutoPx { + if self.facts(node).creates_block_formatting_context() { + if pass == BlockSizePass::BeforeInsideLayout { + return None; + } + return Some(automatic_block_size_for_bfc_root(self.state, self.callbacks, node)); + } + let inner = self + .used(node) + .available_inner_space_or_constraints_from(available_space); + Some( + self.sizing() + .calculate_fit_content_size(node, SizingAxis::Block, inner, constraints), + ) + } + + fn solve_non_replaced_block_once( + &self, + node: Node, + available_space: AvailableSpace, + constraints: ContainingBlockConstraints, + static_position_rect: StaticPositionRect, + pass: BlockSizePass, + mut block_size: AutoPx, + ) -> (AutoPx, AutoPx, AutoPx, AutoPx, AutoPx) { + let style = self.style(node); + let containing_block_inline_size = available_space.inline_size.to_px_or_zero(); + let containing_block_block_size = available_space.block_size.to_px_or_zero(); + let mut margin_top = resolve_or_auto(style.margin_top(), containing_block_inline_size); + let mut margin_bottom = resolve_or_auto(style.margin_bottom(), containing_block_inline_size); + let mut top = resolve_or_auto(style.inset_top(), containing_block_block_size); + let mut bottom = resolve_or_auto(style.inset_bottom(), containing_block_block_size); + let used = self.used(node); + let padding_top = used.padding_top.get(); + let padding_bottom = used.padding_bottom.get(); + + let solve_for = |length: AutoPx, + clamp_to_zero: bool, + top: AutoPx, + margin_top: AutoPx, + block_size: AutoPx, + margin_bottom: AutoPx, + bottom: AutoPx| { + solve_abspos_axis_for( + containing_block_block_size, + length, + clamp_to_zero, + top, + margin_top, + style.border_top_width, + padding_top, + block_size, + padding_bottom, + style.border_bottom_width, + margin_bottom, + bottom, + ) + }; + + if top.is_none() && block_size.is_none() && bottom.is_none() { + if margin_top.is_none() { + margin_top = Some(CssPixels::default()); + } + if margin_bottom.is_none() { + margin_bottom = Some(CssPixels::default()); + } + let Some(automatic) = self.automatic_block_size(node, available_space, constraints, pass) else { + return (block_size, top, bottom, margin_top, margin_bottom); + }; + block_size = Some(automatic); + let constrained = self.apply_min_max_block_size_constraints(node, available_space, constraints, block_size); + self.used_mut(node).set_content_block_size(auto_px_value(constrained)); + top = Some(self.static_offset(node, static_position_rect).block_offset); + bottom = Some(solve_for( + bottom, + false, + top, + margin_top, + block_size, + margin_bottom, + bottom, + )); + } else if top.is_some() && block_size.is_some() && bottom.is_some() { + if margin_top.is_none() && margin_bottom.is_none() { + let remainder = solve_for( + Some(auto_px_value(margin_top) + auto_px_value(margin_bottom)), + false, + top, + margin_top, + block_size, + margin_bottom, + bottom, + ); + margin_top = Some(remainder / 2); + margin_bottom = Some(remainder / 2); + } else if margin_top.is_none() || margin_bottom.is_none() { + if margin_top.is_none() { + margin_top = Some(solve_for( + margin_top, + false, + top, + margin_top, + block_size, + margin_bottom, + bottom, + )); + } else { + margin_bottom = Some(solve_for( + margin_bottom, + false, + top, + margin_top, + block_size, + margin_bottom, + bottom, + )); + } + } else { + bottom = Some(solve_for( + bottom, + false, + top, + margin_top, + block_size, + margin_bottom, + bottom, + )); + } + } else { + if margin_top.is_none() { + margin_top = Some(CssPixels::default()); + } + if margin_bottom.is_none() { + margin_bottom = Some(CssPixels::default()); + } + + if top.is_none() && block_size.is_none() && bottom.is_some() { + let Some(automatic) = self.automatic_block_size(node, available_space, constraints, pass) else { + return (block_size, top, bottom, margin_top, margin_bottom); + }; + block_size = Some(automatic); + top = Some(solve_for( + top, + false, + top, + margin_top, + block_size, + margin_bottom, + bottom, + )); + } else if top.is_none() && bottom.is_none() && block_size.is_some() { + top = Some(self.static_offset(node, static_position_rect).block_offset); + bottom = Some(solve_for( + bottom, + false, + top, + margin_top, + block_size, + margin_bottom, + bottom, + )); + } else if block_size.is_none() && bottom.is_none() && top.is_some() { + let Some(automatic) = self.automatic_block_size(node, available_space, constraints, pass) else { + return (block_size, top, bottom, margin_top, margin_bottom); + }; + block_size = Some(automatic); + bottom = Some(solve_for( + bottom, + false, + top, + margin_top, + block_size, + margin_bottom, + bottom, + )); + } else if top.is_none() && block_size.is_some() && bottom.is_some() { + top = Some(solve_for( + top, + false, + top, + margin_top, + block_size, + margin_bottom, + bottom, + )); + } else if block_size.is_none() && top.is_some() && bottom.is_some() { + block_size = Some(solve_for( + block_size, + true, + top, + margin_top, + block_size, + margin_bottom, + bottom, + )); + } else if bottom.is_none() && top.is_some() && block_size.is_some() { + bottom = Some(solve_for( + bottom, + false, + top, + margin_top, + block_size, + margin_bottom, + bottom, + )); + } + } + (block_size, top, bottom, margin_top, margin_bottom) + } + + fn compute_block_size_for_non_replaced( + &self, + node: Node, + available_space: AvailableSpace, + constraints: ContainingBlockConstraints, + static_position_rect: StaticPositionRect, + pass: BlockSizePass, + ) { + let style = self.style(node); + let mut intrinsic_available_space = available_space; + intrinsic_available_space.inline_size = AvailableSize::definite(self.used(node).content_inline_size.get()); + let initial = if self.facts(node).is_table_wrapper() { + Some( + self.sizing() + .compute_table_box_block_size_inside_wrapper(node, available_space, constraints), + ) + } else if self + .sizing() + .should_treat_block_size_as_auto(node, available_space, constraints) + { + None + } else { + Some( + self.sizing() + .calculate_inner_block_size(node, intrinsic_available_space, style.height(), constraints), + ) + }; + let (mut used_block_size, mut top, mut bottom, mut margin_top, mut margin_bottom) = + self.solve_non_replaced_block_once(node, available_space, constraints, static_position_rect, pass, initial); + + if used_block_size.is_some() && !style.max_height().is_none() { + let max_block_size = self.sizing().calculate_inner_block_size( + node, + intrinsic_available_space, + style.max_height(), + constraints, + ); + if auto_px_value(used_block_size) > max_block_size { + (used_block_size, top, bottom, margin_top, margin_bottom) = self.solve_non_replaced_block_once( + node, + available_space, + constraints, + static_position_rect, + pass, + Some(max_block_size), + ); + } + } + if used_block_size.is_some() && !style.min_height().is_auto() { + let min_block_size = self.sizing().calculate_inner_block_size( + node, + intrinsic_available_space, + style.min_height(), + constraints, + ); + if auto_px_value(used_block_size) < min_block_size { + (used_block_size, top, bottom, margin_top, margin_bottom) = self.solve_non_replaced_block_once( + node, + available_space, + constraints, + static_position_rect, + pass, + Some(min_block_size), + ); + } + } + if used_block_size.is_none() { + used_block_size = + self.apply_min_max_block_size_constraints(node, available_space, constraints, used_block_size); + } + + let containing_block_inline_size = available_space.inline_size.to_px_or_zero(); + let containing_block_block_size = available_space.block_size.to_px_or_zero(); + let used = self.used_mut(node); + used.set_content_block_size(auto_px_value(used_block_size)); + if style.height().is_auto() && pass == BlockSizePass::BeforeInsideLayout { + return; + } + if !style.height().is_intrinsic_sizing_constraint() { + used.has_definite_block_size.set(true); + } + used.inset_top.set(auto_px_value(top)); + used.inset_bottom.set(auto_px_value(bottom)); + // The local values are already resolved against these bases. Keep the + // variables to document and pin the C++ basis distinction. + let _ = (containing_block_inline_size, containing_block_block_size); + used.margin_top.set(auto_px_value(margin_top)); + used.margin_bottom.set(auto_px_value(margin_bottom)); + } + + fn compute_block_size_for_replaced( + &self, + node: Node, + available_space: AvailableSpace, + constraints: ContainingBlockConstraints, + static_position_rect: StaticPositionRect, + pass: BlockSizePass, + ) { + let block_size = self + .sizing() + .compute_block_size_for_replaced_element(node, available_space, constraints); + let containing_block_block_size = available_space.block_size.to_px_or_zero(); + let style = self.style(node); + let used = self.used(node); + let available = containing_block_block_size + - block_size + - style.border_top_width + - used.padding_top.get() + - used.padding_bottom.get() + - style.border_bottom_width; + // Deliberately pass false for `clear_auto_margins_if_start_is_auto`: + // this matches the C++ condition, which tests only the end inset. + let solution = solve_replaced_axis( + available, + resolve_or_auto(style.inset_top(), containing_block_block_size), + resolve_or_auto(style.inset_bottom(), containing_block_block_size), + resolve_or_auto(style.margin_top(), containing_block_block_size), + resolve_or_auto(style.margin_bottom(), containing_block_block_size), + self.static_offset(node, static_position_rect).block_offset, + ReplacedAxisBehavior { + clear_auto_margins_if_start_is_auto: false, + clear_negative_auto_margins: false, + }, + ); + + let used = self.used_mut(node); + used.set_content_block_size(block_size); + if style.height().is_auto() && pass == BlockSizePass::BeforeInsideLayout { + return; + } + if !style.height().is_intrinsic_sizing_constraint() { + used.has_definite_block_size.set(true); + } + used.inset_top.set(solution.start); + used.inset_bottom.set(solution.end); + used.margin_top.set(solution.margin_start); + used.margin_bottom.set(solution.margin_end); + } + + fn compute_block_size( + &self, + node: Node, + available_space: AvailableSpace, + constraints: ContainingBlockConstraints, + static_position_rect: StaticPositionRect, + pass: BlockSizePass, + ) { + if self + .sizing() + .box_is_sized_as_replaced_element(node, available_space, constraints) + { + self.compute_block_size_for_replaced(node, available_space, constraints, static_position_rect, pass); + } else { + self.compute_block_size_for_non_replaced(node, available_space, constraints, static_position_rect, pass); + } + } +} + +impl<'pass> AbsposEngine<'_, 'pass> { + fn layout_element( + &self, + frame: &mut crate::layout::FcFrame<'pass>, + parent_block: Option<&BlockFormattingContext<'pass>>, + parent_grid: Option<&GridFormattingContext<'pass>>, + node: Node, + inputs: AbsposLayoutInputs, + ) { + assert!(!self.facts(node).is_svg_box()); + let containing_block_size = LogicalSize { + inline_size: clamp_to_max_dimension_value(inputs.containing_block_info.rect.size.inline_size), + block_size: clamp_to_max_dimension_value(inputs.containing_block_info.rect.size.block_size), + }; + let available_space = AvailableSpace { + inline_size: AvailableSize::definite(containing_block_size.inline_size), + block_size: AvailableSize::definite(containing_block_size.block_size), + }; + let constraints = ContainingBlockConstraints { + percentage_basis_inline_size: Some(containing_block_size.inline_size), + percentage_basis_block_size: Some(containing_block_size.block_size), + quirks_mode_percentage_basis_block_size: None, + }; + let style = self.style(node); + { + let used = self.used_mut(node); + used.border_left.set(style.border_left_width); + used.border_right.set(style.border_right_width); + used.border_top.set(style.border_top_width); + used.border_bottom.set(style.border_bottom_width); + used.padding_left + .set(style.padding_left().to_px(containing_block_size.inline_size)); + used.padding_right + .set(style.padding_right().to_px(containing_block_size.inline_size)); + used.padding_top + .set(style.padding_top().to_px(containing_block_size.inline_size)); + used.padding_bottom + .set(style.padding_bottom().to_px(containing_block_size.inline_size)); + } + + self.compute_inline_size(node, available_space, constraints, inputs.static_position_rect); + self.compute_block_size( + node, + available_space, + constraints, + inputs.static_position_rect, + BlockSizePass::BeforeInsideLayout, + ); + + { + let used = self.used_mut(node); + if !style.inset_left().is_auto() && !style.inset_right().is_auto() { + used.has_definite_inline_size.set(true); + } + if !style.inset_top().is_auto() + && !style.inset_bottom().is_auto() + && (style.height().is_auto() || !style.height().is_intrinsic_sizing_constraint()) + { + used.has_definite_block_size.set(true); + } + } + if !self.facts(node).creates_block_formatting_context() { + let block_size_resolved_from_aspect_ratio = style.height().is_auto() + && self.facts(node).has_preferred_aspect_ratio() + && self.used(node).has_definite_inline_size(); + let used = self.used_mut(node); + used.has_definite_inline_size.set(true); + if (!style.height().is_auto() && !style.height().is_intrinsic_sizing_constraint()) + || block_size_resolved_from_aspect_ratio + { + used.has_definite_block_size.set(true); + } + } + + self.sizing() + .make_button_content_box_definite(node, self.layout_mode, available_space, constraints, None); + + let inner_available_space = self + .used(node) + .available_inner_space_or_constraints_from(available_space); + let child_layout = match crate::layout::layout_inside_child( + frame, + parent_block, + parent_grid, + node, + LayoutMode::Normal, + LayoutInput { + available_space: inner_available_space, + containing_block_constraints: constraints, + content_box_position_in_bfc_root: None, + table_grid_min_border_box_block_size: None, + }, + false, + ) { + crate::layout::ChildLayoutOutcome::Created(child_layout) => Some(child_layout), + crate::layout::ChildLayoutOutcome::Skipped => None, + // Absolutely positioned boxes with children establish an + // independent formatting context, so they cannot remain in + // the currently running context. + crate::layout::ChildLayoutOutcome::ReenterCurrent => { + unreachable!("abspos child with contents did not establish a formatting context") + } + }; + + if style.height().is_auto() { + self.compute_block_size( + node, + available_space, + constraints, + inputs.static_position_rect, + BlockSizePass::AfterInsideLayout, + ); + } + + { + let used = self.used_mut(node); + let collapsed = used.uses_collapsing_borders_model.get(); + if let Some(inline_alignment) = inputs.containing_block_info.inline_alignment + && style.inset_left().is_auto() + && style.inset_right().is_auto() + { + let available = containing_block_size.inline_size - used.margin_box_inline_size(collapsed); + match inline_alignment { + AbsposAlignment::Center => { + used.inset_left.set(available / 2); + used.inset_right.set(available / 2); + } + AbsposAlignment::Start => { + used.inset_right.set(available); + } + AbsposAlignment::End => { + used.inset_left.set(available); + } + _ => {} + } + } + if let Some(block_alignment) = inputs.containing_block_info.block_alignment + && style.inset_top().is_auto() + && style.inset_bottom().is_auto() + { + let available = containing_block_size.block_size - used.margin_box_block_size(collapsed); + match block_alignment { + AbsposAlignment::Center => { + used.inset_top.set(available / 2); + used.inset_bottom.set(available / 2); + } + AbsposAlignment::Start | AbsposAlignment::SelfStart => { + used.inset_bottom.set(available); + } + AbsposAlignment::End | AbsposAlignment::SelfEnd => { + used.inset_top.set(available); + } + _ => {} + } + } + } + + let static_offset = self.static_offset(node, inputs.static_position_rect); + let used = self.used(node); + let collapsed = used.uses_collapsing_borders_model.get(); + let mut used_offset = LogicalOffset { + inline_offset: if inputs.containing_block_info.inline_axis_mode == AbsposAxisMode::StaticPosition { + static_offset.inline_offset + } else { + inputs.containing_block_info.rect.offset.inline_offset + used.inset_left.get() + }, + block_offset: if inputs.containing_block_info.block_axis_mode == AbsposAxisMode::StaticPosition { + static_offset.block_offset + } else { + inputs.containing_block_info.rect.offset.block_offset + used.inset_top.get() + }, + }; + used_offset.inline_offset += used.margin_left.get() + used.border_box_left(collapsed); + used_offset.block_offset += used.margin_top.get() + used.border_box_top(collapsed); + crate::layout::place_child( + self.state, + &self.callbacks, + node, + FfiCssPixelPoint { + x: used_offset.inline_offset, + y: used_offset.block_offset, + }, + ); + + let is_measurement = self.state.is_measurement(); + if self.layout_mode == LayoutMode::Normal && !is_measurement { + self.state + .used_values_rare_data_for_node_mut(&self.callbacks, node) + .abspos_layout_inputs = Some(inputs); + } + + if let Some(child_layout) = child_layout { + child_layout.finish(); + } + } + + pub(crate) fn layout_children( + &self, + frame: &mut crate::layout::FcFrame<'pass>, + parent_block: Option<&BlockFormattingContext<'pass>>, + parent_grid: Option<&GridFormattingContext<'pass>>, + box_: Node, + ) { + if self.layout_mode != LayoutMode::Normal { + return; + } + if self.state.is_measurement() { + return; + } + while let Some(child) = self.state.take_next_contained_abspos_child(box_) { + let child_box = child.child_box; + if self.try_used_pointer(child_box).is_none() { + self.state + .create_used_values(&self.callbacks, child_box, ContainingBlockConstraints::default()); + } + self.resolve_anchor_insets(child_box); + let inputs = AbsposLayoutInputs { + static_position_rect: self + .resolve_static_position_relative_to_containing_block(child_box, child.static_position_rect), + containing_block_info: self.containing_block_info(child_box), + }; + self.layout_element(frame, parent_block, parent_grid, child_box, inputs); + } + } + + fn replay(&self, frame: &mut crate::layout::FcFrame<'pass>, node: Node) { + let saved_inputs = self.callbacks.saved_abspos_layout_inputs(node); + let found = saved_inputs.is_some(); + assert!(found); + let mut inputs = saved_inputs.unwrap(); + if !inputs.containing_block_info.derives_from_own_computed_values { + let (inline, block) = axis_modes(self.style(node)); + inputs.containing_block_info.inline_axis_mode = inline; + inputs.containing_block_info.block_axis_mode = block; + } + // Partial relayout uses a fresh state and creates the replay root + // exactly once. + self.state + .create_used_values(&self.callbacks, node, ContainingBlockConstraints::default()); + self.layout_element(frame, None, None, node, inputs); + } + + fn compute_inset(&self, node: Node, containing_block_size: LogicalSize) { + // Most boxes are neither relatively positioned nor carry anchor() + // insets. Preserve the old C++ fast path without populating the + // comprehensive Rust facts caches for those boxes. + // SAFETY: The callback only reads the live node's computed values. + if !unsafe { + (self.callbacks.needs_inset_resolution)(self.callbacks.context, self.callbacks.shell(node)) + } { + return; + } + let initial_style = self.style(node); + if initial_style.inset_top().contains_anchor_function + || initial_style.inset_right().contains_anchor_function + || initial_style.inset_bottom().contains_anchor_function + || initial_style.inset_left().contains_anchor_function + { + self.resolve_anchor_insets(node); + } + let style = self.style(node); + if style.position != positioning::RELATIVE { + return; + } + + let resolve_opposing = |first: FfiSizeValue, second: FfiSizeValue, basis: CssPixels| { + let resolved_first = first.to_px(basis); + let resolved_second = second.to_px(basis); + if first.is_auto() && second.is_auto() { + (CssPixels::default(), CssPixels::default()) + } else if first.is_auto() { + (-resolved_second, resolved_second) + } else { + (resolved_first, -resolved_first) + } + }; + let (left, right) = resolve_opposing( + style.inset_left(), + style.inset_right(), + containing_block_size.inline_size, + ); + + let treat_percentage_as_auto = |value: FfiSizeValue| { + if !value.contains_percentage { + return value; + } + let mut containing_block = self.callbacks.containing_block(node); + while !containing_block.is_invalid() { + let facts = self.facts(containing_block); + if !facts.is_anonymous() || facts.is_table_cell() { + break; + } + containing_block = self.callbacks.containing_block(containing_block); + } + if !containing_block.is_invalid() && !self.used(containing_block).has_definite_block_size() { + FfiSizeValue::auto_value() + } else { + value + } + }; + let (top, bottom) = resolve_opposing( + treat_percentage_as_auto(style.inset_top()), + treat_percentage_as_auto(style.inset_bottom()), + containing_block_size.block_size, + ); + let used = self.used_mut(node); + used.inset_left.set(left); + used.inset_right.set(right); + used.inset_top.set(top); + used.inset_bottom.set(bottom); + } +} + +fn layout_children_for_instance(instance: &mut crate::layout::FormattingContextInstance, box_: Node) { + if instance.layout_mode != LayoutMode::Normal { + return; + } + let crate::layout::FormattingContextInstance { frame, implementation } = instance; + let (parent_block, parent_grid) = match &*implementation { + crate::layout::FcImpl::Block(context) => (Some(context.as_ref()), None), + crate::layout::FcImpl::Grid(context) => (None, Some(context.as_ref())), + _ => (None, None), + }; + AbsposEngine::new(frame.state, frame.callbacks, frame.layout_mode, frame.box_, parent_grid).layout_children( + frame, + parent_block, + parent_grid, + box_, + ); +} + +pub(crate) fn layout_children_native<'pass>( + state: &'pass LayoutState, + callbacks: FfiLayoutFcCallbacks, + layout_mode: LayoutMode, + context_box: Node, + frame: &mut crate::layout::FcFrame<'pass>, + box_: Node, +) { + AbsposEngine::new(state, callbacks, layout_mode, context_box, None).layout_children(frame, None, None, box_); +} + +pub(crate) fn compute_inset_native( + state: &LayoutState, + callbacks: FfiLayoutFcCallbacks, + layout_mode: LayoutMode, + context_box: Node, + node: Node, + inline_size: CssPixels, + block_size: CssPixels, +) { + AbsposEngine::new(state, callbacks, layout_mode, context_box, None).compute_inset( + node, + LogicalSize { + inline_size, + block_size, + }, + ); +} + +fn replay_for_instance(instance: &mut crate::layout::FormattingContextInstance, node: Node) { + assert!(matches!(instance.implementation, crate::layout::FcImpl::AbsposReplay)); + AbsposEngine::new( + instance.state, + instance.callbacks, + instance.layout_mode, + instance.box_, + None, + ) + .replay(&mut instance.frame, node); +} diff --git a/Libraries/LibWeb/Rust/src/layout/formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/formatting_context.rs index f4afd938eb52f..36dacde5aa2bc 100644 --- a/Libraries/LibWeb/Rust/src/layout/formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/formatting_context.rs @@ -188,2029 +188,6 @@ pub(crate) fn anchor_rect_from_geometry( } } -fn axis_modes(style: StyleValues) -> (AbsposAxisMode, AbsposAxisMode) { - ( - if style.inset_left().is_auto() && style.inset_right().is_auto() { - AbsposAxisMode::StaticPosition - } else { - AbsposAxisMode::InsetFromRect - }, - if style.inset_top().is_auto() && style.inset_bottom().is_auto() { - AbsposAxisMode::StaticPosition - } else { - AbsposAxisMode::InsetFromRect - }, - ) -} - -pub(crate) fn aligned_static_offset( - static_position_rect: StaticPositionRect, - margin_box_inline_size: CssPixels, - margin_box_block_size: CssPixels, -) -> LogicalOffset { - let mut offset = static_position_rect.rect.offset; - match static_position_rect.inline_alignment { - StaticPositionAlignment::Start => {} - StaticPositionAlignment::Center => { - offset.inline_offset += (static_position_rect.rect.size.inline_size - margin_box_inline_size) / 2; - } - StaticPositionAlignment::End => { - offset.inline_offset += static_position_rect.rect.size.inline_size - margin_box_inline_size; - } - } - match static_position_rect.block_alignment { - StaticPositionAlignment::Start => {} - StaticPositionAlignment::Center => { - offset.block_offset += (static_position_rect.rect.size.block_size - margin_box_block_size) / 2; - } - StaticPositionAlignment::End => { - offset.block_offset += static_position_rect.rect.size.block_size - margin_box_block_size; - } - } - offset -} - -pub(crate) struct AbsposEngine<'a, 'pass> { - state: &'pass LayoutState, - callbacks: FfiLayoutFcCallbacks, - layout_mode: LayoutMode, - context_box: Node, - grid_context: Option<&'a GridFormattingContext<'pass>>, -} - -impl<'a, 'pass> AbsposEngine<'a, 'pass> { - fn new( - state: &'pass LayoutState, - callbacks: FfiLayoutFcCallbacks, - layout_mode: LayoutMode, - context_box: Node, - grid_context: Option<&'a GridFormattingContext<'pass>>, - ) -> Self { - assert!(!context_box.is_invalid()); - Self { - state, - callbacks, - layout_mode, - context_box, - grid_context, - } - } - - fn sizing(&self) -> SizingContext<'_> { - SizingContext::new(self.state, self.callbacks) - } - - fn style(&self, node: Node) -> StyleValues<'pass> { - self.state.style_facts(&self.callbacks, node) - } - - fn facts(&self, node: Node) -> NodeFacts<'_> { - self.state.node_facts(&self.callbacks, node) - } - - fn used_pointer(&self, node: Node) -> &'pass UsedValues { - self.state.used_values(&self.callbacks, node) - } - - fn try_used_pointer(&self, node: Node) -> Option<&'pass UsedValues> { - self.state.try_used_values(&self.callbacks, node) - } - - fn used(&self, node: Node) -> &'pass UsedValues { - self.used_pointer(node) - } - - fn used_mut(&self, node: Node) -> &'pass UsedValues { - self.used_pointer(node) - } - - fn static_position_containing_block(&self, node: Node) -> Node { - unsafe { (self.callbacks.static_position_containing_block)(self.callbacks.context, self.callbacks.shell(node)) } - } - - fn inline_containing_block(&self, node: Node) -> Node { - self.callbacks.inline_containing_block(node) - } - - fn non_anonymous_containing_block(&self, node: Node) -> Node { - self.callbacks.non_anonymous_containing_block(node) - } - - fn node_is_ancestor(&self, ancestor: Node, node: Node) -> bool { - self.callbacks.is_ancestor(ancestor, node) - } - - fn belongs_to_inline_containing_block(&self, inline_node: Node, node: Node) -> bool { - !self.facts(node).is_anonymous() && self.node_is_ancestor(inline_node, node) - } - - fn resolve_static_position_relative_to_containing_block( - &self, - node: Node, - static_position_rect: StaticPositionRect, - ) -> StaticPositionRect { - let static_position_cb = self.static_position_containing_block(node); - let actual_containing_block = self.callbacks.containing_block(node); - if static_position_cb.is_invalid() || static_position_cb == actual_containing_block { - return static_position_rect; - } - - let mut merge_point = static_position_cb; - while merge_point != actual_containing_block && !self.node_is_ancestor(merge_point, actual_containing_block) { - merge_point = self.callbacks.containing_block(merge_point); - assert!(!merge_point.is_invalid()); - } - - let offset_relative_to_merge_point = |descendant: Node| { - let mut offset = FfiCssPixelPoint::default(); - let mut current = descendant; - while current != merge_point { - let used = self.used(current); - offset = point_add(offset, used.content_offset.get()); - current = self.callbacks.containing_block(current); - assert!(!current.is_invalid()); - } - offset - }; - translate_static_position_between_chains( - static_position_rect, - offset_relative_to_merge_point(static_position_cb), - offset_relative_to_merge_point(actual_containing_block), - ) - } - - fn line_fragments(&self, node: Node) -> Vec { - let mut fragments = Vec::new(); - let Some(lines) = self.state.line_data(self.callbacks.slot_index(node)) else { - return fragments; - }; - for line in &lines.line_boxes { - for fragment in &line.fragments { - let (x, y) = fragment.offset(); - let (width, height) = fragment.size(); - fragments.push(LineFragmentFacts { - layout_node: fragment.layout_node, - is_atomic_inline: fragment.is_atomic_inline, - writing_mode: fragment.writing_mode, - style_block_axis_is_reverse: fragment.style_block_axis_is_reverse, - inline_offset: fragment.inline_offset, - block_offset: fragment.block_offset, - offset: FfiCssPixelPoint { x, y }, - size: FfiCssPixelPoint { x: width, y: height }, - }); - } - } - fragments - } - - fn add_atomic_inline_fragment_rect( - &self, - inline_node: Node, - fragment: LineFragmentFacts, - offset: FfiCssPixelPoint, - bounding_rect: &mut Option, - empty_bounding_rect: &mut Option, - ) { - let Some(child_used) = self.try_used_pointer(fragment.layout_node) else { - return; - }; - let collapsed = child_used.uses_collapsing_borders_model.get(); - let is_horizontal = fragment.writing_mode == writing_mode::HORIZONTAL_TB; - let inline_axis_border_box_start = fragment.inline_offset - - if is_horizontal { - child_used.border_box_left(collapsed) - } else { - child_used.border_box_top(collapsed) - }; - let inline_axis_border_box_extent = if is_horizontal { - child_used.border_box_inline_size(collapsed) - } else { - child_used.border_box_block_size(collapsed) - }; - let block_axis_line_height = self.style(inline_node).line_height; - let block_axis_start = if fragment.style_block_axis_is_reverse { - fragment.block_offset + child_used.border_box_right(collapsed) - block_axis_line_height - } else { - fragment.block_offset - - if is_horizontal { - child_used.border_box_top(collapsed) - } else { - child_used.border_box_left(collapsed) - } - }; - let rect = if is_horizontal { - PhysicalRect { - x: inline_axis_border_box_start, - y: block_axis_start, - width: inline_axis_border_box_extent, - height: block_axis_line_height, - } - } else { - PhysicalRect { - x: block_axis_start, - y: inline_axis_border_box_start, - width: block_axis_line_height, - height: inline_axis_border_box_extent, - } - } - .translated(offset); - add_fragment_rect(rect, bounding_rect, empty_bounding_rect); - } - - fn walk_inline_containing_block( - &self, - inline_node: Node, - node: Node, - offset: FfiCssPixelPoint, - bounding_rect: &mut Option, - empty_bounding_rect: &mut Option, - ) { - for fragment in self.line_fragments(node) { - if !self.belongs_to_inline_containing_block(inline_node, fragment.layout_node) { - continue; - } - if fragment.is_atomic_inline { - self.add_atomic_inline_fragment_rect(inline_node, fragment, offset, bounding_rect, empty_bounding_rect); - continue; - } - add_fragment_rect( - PhysicalRect { - x: fragment.offset.x + offset.x, - y: fragment.offset.y + offset.y, - width: fragment.size.x, - height: fragment.size.y, - }, - bounding_rect, - empty_bounding_rect, - ); - } - - let mut child = self.callbacks.first_child(node); - while !child.is_invalid() { - let next = self.callbacks.next_sibling(child); - let facts = self.facts(child); - if facts.is_absolutely_positioned() || facts.is_floating() { - child = next; - continue; - } - let child_used_pointer = self.try_used_pointer(child); - let child_offset = if let Some(child_used) = child_used_pointer { - point_add(offset, child_used.content_offset.get()) - } else { - offset - }; - if facts.is_box() && !facts.is_anonymous() { - if !self.belongs_to_inline_containing_block(inline_node, child) { - child = next; - continue; - } - if facts.is_atomic_inline() { - child = next; - continue; - } - if let Some(child_used) = child_used_pointer { - let collapsed = child_used.uses_collapsing_borders_model.get(); - let border_box_origin = FfiCssPixelPoint { - x: child_offset.x - child_used.border_left_collapsed(collapsed) - child_used.padding_left.get(), - y: child_offset.y - child_used.border_top_collapsed(collapsed) - child_used.padding_top.get(), - }; - add_fragment_rect( - PhysicalRect { - x: border_box_origin.x, - y: border_box_origin.y, - width: child_used.border_box_inline_size(collapsed), - height: child_used.border_box_block_size(collapsed), - }, - bounding_rect, - empty_bounding_rect, - ); - } - } - self.walk_inline_containing_block(inline_node, child, child_offset, bounding_rect, empty_bounding_rect); - child = next; - } - } - - fn compute_inline_containing_block_rect( - &self, - inline_node: Node, - abspos_containing_block: Node, - ) -> Option { - if self.facts(inline_node).is_anonymous() { - return None; - } - let outer_block = self.non_anonymous_containing_block(inline_node); - if outer_block.is_invalid() { - return None; - } - - let mut outer_offset = FfiCssPixelPoint::default(); - let mut ancestor = outer_block; - while !ancestor.is_invalid() && ancestor != abspos_containing_block { - let used = self.try_used_pointer(ancestor); - if let Some(used) = used { - outer_offset = point_add(outer_offset, used.content_offset.get()); - } - ancestor = self.callbacks.parent(ancestor); - } - - let mut bounding_rect = None; - let mut empty_bounding_rect = None; - self.walk_inline_containing_block( - inline_node, - outer_block, - outer_offset, - &mut bounding_rect, - &mut empty_bounding_rect, - ); - let mut rect = bounding_rect.or(empty_bounding_rect)?; - if let Some(inline_used) = self.try_used_pointer(inline_node) { - rect.x -= inline_used.padding_left.get(); - rect.y -= inline_used.padding_top.get(); - rect.width += inline_used.padding_left.get() + inline_used.padding_right.get(); - rect.height += inline_used.padding_top.get() + inline_used.padding_bottom.get(); - } - Some(rect) - } - - fn base_containing_block_info(&self, node: Node) -> AbsposContainingBlockInfo { - let style = self.style(node); - let (inline_axis_mode, block_axis_mode) = axis_modes(style); - let containing_block = self.callbacks.containing_block(node); - assert!(!containing_block.is_invalid()); - let inline_containing_block = self.inline_containing_block(node); - if !inline_containing_block.is_invalid() - && let Some(rect) = self.compute_inline_containing_block_rect(inline_containing_block, containing_block) - { - return AbsposContainingBlockInfo { - rect: LogicalRect { - offset: LogicalOffset { - inline_offset: rect.x, - block_offset: rect.y, - }, - size: LogicalSize { - inline_size: rect.width, - block_size: rect.height, - }, - }, - inline_axis_mode, - block_axis_mode, - inline_alignment: None, - block_alignment: None, - derives_from_own_computed_values: false, - }; - } - - let containing_block_used = self.used(containing_block); - AbsposContainingBlockInfo { - rect: LogicalRect { - offset: LogicalOffset { - inline_offset: -containing_block_used.padding_left.get(), - block_offset: -containing_block_used.padding_top.get(), - }, - size: LogicalSize { - inline_size: containing_block_used.content_inline_size.get() - + containing_block_used.padding_left.get() - + containing_block_used.padding_right.get(), - block_size: containing_block_used.content_block_size.get() - + containing_block_used.padding_top.get() - + containing_block_used.padding_bottom.get(), - }, - }, - inline_axis_mode, - block_axis_mode, - inline_alignment: None, - block_alignment: None, - derives_from_own_computed_values: false, - } - } - - fn containing_block_info(&self, node: Node) -> AbsposContainingBlockInfo { - let base = self.base_containing_block_info(node); - let Some(grid) = self.grid_context else { - return base; - }; - let mut info = grid.abspos_containing_block_info(node); - let uses_grid_area_as_static_position = self.static_position_containing_block(node) == self.context_box; - if !uses_grid_area_as_static_position { - info.inline_axis_mode = base.inline_axis_mode; - info.block_axis_mode = base.block_axis_mode; - } - info - } -} - -fn add_fragment_rect( - rect: PhysicalRect, - bounding_rect: &mut Option, - empty_bounding_rect: &mut Option, -) { - let destination = if rect.is_empty() { - empty_bounding_rect - } else { - bounding_rect - }; - *destination = Some(destination.map_or(rect, |existing| existing.union(rect))); -} - -fn calc_node_create_px_dimension(value: f64) -> *const c_void { - crate::css::calc::rust_calc_node_create_numeric_dimension( - CALC_NUMERIC_KIND_LENGTH, - value, - crate::css::style_compute::px_length_unit(), - ) - .cast() -} - -struct AnchorResolutionState { - default_anchor_box: Node, - compensates_for_horizontal_scroll: bool, - compensates_for_vertical_scroll: bool, -} - -#[derive(Clone, Copy)] -struct AnchorValueAxis { - is_from_end: bool, - is_horizontal: bool, - containing_block_extent: CssPixels, -} - -#[derive(Clone, Copy)] -struct AnchorCalcCallbackContext<'a, 'pass> { - engine: *const AbsposEngine<'a, 'pass>, - positioned_box: Node, - containing_block: Node, - is_from_end: bool, - is_horizontal_axis: bool, - containing_block_extent: CssPixels, - resolution_state: *mut AnchorResolutionState, -} - -impl AbsposEngine<'_, '_> { - fn anchor_lookup(&self, positioned_box: Node, anchor_name: usize) -> Option { - let eligible_anchor_boxes = self.state.used_value_nodes(); - let eligible_anchor_shells = eligible_anchor_boxes - .iter() - .map(|&node| self.callbacks.shell(node)) - .collect::>(); - // SAFETY: The name handle is retained by either the style snapshot or - // the live anchor() shell. The eligible-node slice is borrowed only - // for this synchronous lookup. - let anchor_box = unsafe { - (self.callbacks.anchor_lookup)( - self.callbacks.context, - self.callbacks.shell(positioned_box), - anchor_name, - eligible_anchor_shells.as_ptr(), - eligible_anchor_shells.len(), - ) - }; - (!anchor_box.is_invalid()).then_some(anchor_box) - } - - fn nearest_scroll_container_ancestor(&self, node: Node) -> Node { - let mut ancestor = self.callbacks.containing_block(node); - while !ancestor.is_invalid() { - if self.facts(ancestor).is_scroll_container() { - return ancestor; - } - ancestor = self.callbacks.containing_block(ancestor); - } - NodeSlotId::INVALID - } - - fn anchor_rect(&self, anchor_box: Node, containing_block: Node) -> PhysicalRect { - let anchor_state = self.used(anchor_box); - let mut anchor_offset = FfiCssPixelPoint::default(); - let mut node = anchor_box; - while node != containing_block { - assert!(!node.is_invalid()); - anchor_offset = point_add(anchor_offset, self.used(node).content_offset.get()); - node = self.callbacks.containing_block(node); - } - anchor_rect_from_geometry(anchor_state, self.used(containing_block), anchor_offset) - } - - fn anchor_side( - &self, - facts: FfiAnchorFunctionFacts, - rect: PhysicalRect, - positioned_box: Node, - containing_block: Node, - is_from_end: bool, - is_horizontal_axis: bool, - ) -> Option { - let containing_block_direction = self.style(containing_block).direction; - let box_direction = self.style(positioned_box).direction; - match facts.side_kind { - FfiAnchorSideKind::Invalid => None, - FfiAnchorSideKind::Top => (!is_horizontal_axis).then_some(rect.top()), - FfiAnchorSideKind::Bottom => (!is_horizontal_axis).then_some(rect.bottom()), - FfiAnchorSideKind::Left => is_horizontal_axis.then_some(rect.left()), - FfiAnchorSideKind::Right => is_horizontal_axis.then_some(rect.right()), - FfiAnchorSideKind::Center => Some(if is_horizontal_axis { - rect.left() + rect.width / 2 - } else { - rect.top() + rect.height / 2 - }), - FfiAnchorSideKind::Start | FfiAnchorSideKind::End => { - let is_start = facts.side_kind == FfiAnchorSideKind::Start; - if is_horizontal_axis { - let use_left = (containing_block_direction == direction::LTR) == is_start; - Some(if use_left { rect.left() } else { rect.right() }) - } else { - Some(if is_start { rect.top() } else { rect.bottom() }) - } - } - FfiAnchorSideKind::SelfStart | FfiAnchorSideKind::SelfEnd => { - let is_start = facts.side_kind == FfiAnchorSideKind::SelfStart; - if is_horizontal_axis { - let use_left = (box_direction == direction::LTR) == is_start; - Some(if use_left { rect.left() } else { rect.right() }) - } else { - Some(if is_start { rect.top() } else { rect.bottom() }) - } - } - FfiAnchorSideKind::Inside | FfiAnchorSideKind::Outside => { - let same_side = facts.side_kind == FfiAnchorSideKind::Inside; - if is_horizontal_axis { - Some(if is_from_end == same_side { - rect.right() - } else { - rect.left() - }) - } else { - Some(if is_from_end == same_side { - rect.bottom() - } else { - rect.top() - }) - } - } - FfiAnchorSideKind::Percentage => { - if is_horizontal_axis { - let (start, end) = if containing_block_direction == direction::LTR { - (rect.left(), rect.right()) - } else { - (rect.right(), rect.left()) - }; - Some(start + CssPixels::nearest_value_for((end - start).to_double() * facts.side_percentage)) - } else { - Some(rect.top() + CssPixels::nearest_value_for(rect.height.to_double() * facts.side_percentage)) - } - } - } - } - - fn note_resolved_anchor_function( - &self, - anchor_box: Node, - is_horizontal_axis: bool, - state: &mut AnchorResolutionState, - ) { - if state.default_anchor_box.is_invalid() { - return; - } - if anchor_box != state.default_anchor_box - && self.nearest_scroll_container_ancestor(anchor_box) - != self.nearest_scroll_container_ancestor(state.default_anchor_box) - { - return; - } - if is_horizontal_axis { - state.compensates_for_horizontal_scroll = true; - } else { - state.compensates_for_vertical_scroll = true; - } - } - - fn resolve_anchor_value( - &self, - value: FfiSizeValue, - positioned_box: Node, - containing_block: Node, - axis: AnchorValueAxis, - resolution_state: &mut AnchorResolutionState, - ) -> Option { - assert!(value.contains_anchor_function); - assert!(!value.calc.is_null()); - let mut callback_context = AnchorCalcCallbackContext { - engine: self, - positioned_box, - containing_block, - is_from_end: axis.is_from_end, - is_horizontal_axis: axis.is_horizontal, - containing_block_extent: axis.containing_block_extent, - resolution_state, - }; - // SAFETY: The calculated handle is retained by the style cache and - // all callback state remains live for this synchronous resolution. - let result = unsafe { - resolve_calc_with_external_resolutions( - value.calc, - axis.containing_block_extent, - (&raw mut callback_context).cast(), - Some(resolve_anchor_non_math_function), - ) - }; - result.resolved.then(|| CssPixels::nearest_value_for(result.value)) - } - - fn resolve_anchor_insets(&self, node: Node) { - // Clear a stale default scroll shift before any early return. - // SAFETY: The node is live and a null anchor clears the weak target. - unsafe { - (self.callbacks.set_default_scroll_shift)( - self.callbacks.context, - self.callbacks.shell(node), - std::ptr::null_mut(), - false, - false, - ); - } - - let style = self.style(node); - let top_contains_anchor = style.inset_top().contains_anchor_function; - let right_contains_anchor = style.inset_right().contains_anchor_function; - let bottom_contains_anchor = style.inset_bottom().contains_anchor_function; - let left_contains_anchor = style.inset_left().contains_anchor_function; - if !top_contains_anchor && !right_contains_anchor && !bottom_contains_anchor && !left_contains_anchor { - return; - } - - let containing_block = self.callbacks.containing_block(node); - if containing_block.is_invalid() { - return; - } - let containing_block_state = self.used(containing_block); - let default_anchor_box = if style.has_position_anchor() { - self.anchor_lookup(node, style.position_anchor_name()) - .unwrap_or(NodeSlotId::INVALID) - } else { - NodeSlotId::INVALID - }; - let mut resolution_state = AnchorResolutionState { - default_anchor_box, - compensates_for_horizontal_scroll: false, - compensates_for_vertical_scroll: false, - }; - let mut resolved = FfiResolvedAnchorInsets::default(); - - if top_contains_anchor { - let value = self.resolve_anchor_value( - style.inset_top(), - node, - containing_block, - AnchorValueAxis { - is_from_end: false, - is_horizontal: false, - containing_block_extent: containing_block_state.content_block_size.get() - + containing_block_state.padding_top.get() - + containing_block_state.padding_bottom.get(), - }, - &mut resolution_state, - ); - resolved.resolves_top = true; - resolved.top_is_auto = value.is_none(); - resolved.top = value.unwrap_or_default(); - } - if right_contains_anchor { - let value = self.resolve_anchor_value( - style.inset_right(), - node, - containing_block, - AnchorValueAxis { - is_from_end: true, - is_horizontal: true, - containing_block_extent: containing_block_state.content_inline_size.get() - + containing_block_state.padding_left.get() - + containing_block_state.padding_right.get(), - }, - &mut resolution_state, - ); - resolved.resolves_right = true; - resolved.right_is_auto = value.is_none(); - resolved.right = value.unwrap_or_default(); - } - if bottom_contains_anchor { - let value = self.resolve_anchor_value( - style.inset_bottom(), - node, - containing_block, - AnchorValueAxis { - is_from_end: true, - is_horizontal: false, - containing_block_extent: containing_block_state.content_block_size.get() - + containing_block_state.padding_top.get() - + containing_block_state.padding_bottom.get(), - }, - &mut resolution_state, - ); - resolved.resolves_bottom = true; - resolved.bottom_is_auto = value.is_none(); - resolved.bottom = value.unwrap_or_default(); - } - if left_contains_anchor { - let value = self.resolve_anchor_value( - style.inset_left(), - node, - containing_block, - AnchorValueAxis { - is_from_end: false, - is_horizontal: true, - containing_block_extent: containing_block_state.content_inline_size.get() - + containing_block_state.padding_left.get() - + containing_block_state.padding_right.get(), - }, - &mut resolution_state, - ); - resolved.resolves_left = true; - resolved.left_is_auto = value.is_none(); - resolved.left = value.unwrap_or_default(); - } - - // SAFETY: The callback synchronously updates this live box's computed - // values with plain auto/px insets. - unsafe { - (self.callbacks.set_resolved_anchor_insets)(self.callbacks.context, self.callbacks.shell(node), resolved); - } - self.state - .replace_resolved_anchor_insets(&self.callbacks, node, resolved); - - if resolution_state.compensates_for_horizontal_scroll || resolution_state.compensates_for_vertical_scroll { - // SAFETY: The anchor and positioned box remain live through the - // pass; C++ stores the anchor as a weak pointer. - unsafe { - (self.callbacks.set_default_scroll_shift)( - self.callbacks.context, - self.callbacks.shell(node), - self.callbacks.shell(resolution_state.default_anchor_box), - resolution_state.compensates_for_horizontal_scroll, - resolution_state.compensates_for_vertical_scroll, - ); - } - } - } -} - -unsafe extern "C" fn resolve_anchor_non_math_function(context: *mut c_void, shell: *const c_void) -> *const c_void { - // SAFETY: The CSS calc engine calls this only during resolve_anchor_value, - // whose stack owns this callback context. - let context = unsafe { &mut *context.cast::>() }; - // SAFETY: The engine pointer is live for the enclosing resolution. - let engine = unsafe { &*context.engine }; - // SAFETY: `shell` is the live Rust style-value handle supplied by the - // CSS calc core. - let facts = unsafe { (engine.callbacks.build_anchor_function_facts)(engine.callbacks.context, shell) }; - let style = engine.style(context.positioned_box); - let anchor_name = if facts.has_anchor_name { - Some(facts.anchor_name) - } else if style.has_position_anchor() { - Some(style.position_anchor_name()) - } else { - None - }; - let mut resolved_node = std::ptr::null(); - if engine.facts(context.positioned_box).is_absolutely_positioned() - && let Some(anchor_name) = anchor_name - && let Some(anchor_box) = engine.anchor_lookup(context.positioned_box, anchor_name) - { - let rect = engine.anchor_rect(anchor_box, context.containing_block); - if let Some(side) = engine.anchor_side( - facts, - rect, - context.positioned_box, - context.containing_block, - context.is_from_end, - context.is_horizontal_axis, - ) { - // SAFETY: The state pointer is live and uniquely used by this - // synchronous resolver. - let resolution_state = unsafe { &mut *context.resolution_state }; - engine.note_resolved_anchor_function(anchor_box, context.is_horizontal_axis, resolution_state); - let inset = if context.is_from_end { - context.containing_block_extent - side - } else { - side - }; - // SAFETY: This CSS crate export transfers one Arc reference to - // the external-resolution snapshot, which releases it after - // calc resolution. - resolved_node = calc_node_create_px_dimension(inset.to_double()); - } - } - if facts.has_anchor_name { - // SAFETY: The C++ facts callback transferred one raw fly-string - // reference for this explicit anchor name. - unsafe { - (engine.callbacks.release_anchor_name_handle)(facts.anchor_name); - } - } - if !resolved_node.is_null() { - return resolved_node; - } - - // SAFETY: The callback borrows fallback data from the live anchor style - // value for this synchronous resolution. - let fallback = unsafe { (engine.callbacks.anchor_function_fallback)(engine.callbacks.context, shell) }; - match fallback.kind { - FfiAnchorFallbackKind::None => std::ptr::null(), - FfiAnchorFallbackKind::Px => calc_node_create_px_dimension(fallback.px.to_double()), - FfiAnchorFallbackKind::Percentage => { - calc_node_create_px_dimension(context.containing_block_extent.to_double() * fallback.fraction) - } - FfiAnchorFallbackKind::Calculated => { - assert!(!fallback.value.is_null()); - let mut nested_context = *context; - let resolved = unsafe { - resolve_calc_with_external_resolutions( - fallback.value, - context.containing_block_extent, - (&raw mut nested_context).cast(), - Some(resolve_anchor_non_math_function), - ) - }; - if !resolved.resolved { - return std::ptr::null(); - } - calc_node_create_px_dimension(resolved.value) - } - FfiAnchorFallbackKind::Anchor => { - assert!(!fallback.value.is_null()); - let mut nested_context = *context; - unsafe { resolve_anchor_non_math_function((&raw mut nested_context).cast(), fallback.value) } - } - } -} - -type AutoPx = Option; - -fn resolve_or_auto(value: FfiSizeValue, basis: CssPixels) -> AutoPx { - (!value.is_auto()).then(|| value.to_px(basis)) -} - -fn auto_px_value(value: AutoPx) -> CssPixels { - value.unwrap_or_default() -} - -#[allow(clippy::too_many_arguments)] -pub(crate) fn solve_abspos_axis_for( - available: CssPixels, - target: AutoPx, - clamp_to_zero: bool, - start: AutoPx, - margin_start: AutoPx, - border_start: CssPixels, - padding_start: CssPixels, - size: AutoPx, - padding_end: CssPixels, - border_end: CssPixels, - margin_end: AutoPx, - end: AutoPx, -) -> CssPixels { - let value = available - - auto_px_value(start) - - auto_px_value(margin_start) - - border_start - - padding_start - - auto_px_value(size) - - padding_end - - border_end - - auto_px_value(margin_end) - - auto_px_value(end) - + auto_px_value(target); - if clamp_to_zero { - value.max(CssPixels::default()) - } else { - value - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) struct ReplacedAxisSolution { - pub(crate) start: CssPixels, - pub(crate) end: CssPixels, - pub(crate) margin_start: CssPixels, - pub(crate) margin_end: CssPixels, -} - -#[derive(Clone, Copy)] -pub(crate) struct ReplacedAxisBehavior { - pub(crate) clear_auto_margins_if_start_is_auto: bool, - pub(crate) clear_negative_auto_margins: bool, -} - -pub(crate) fn solve_replaced_axis( - available: CssPixels, - mut start: AutoPx, - mut end: AutoPx, - mut margin_start: AutoPx, - mut margin_end: AutoPx, - static_offset: CssPixels, - behavior: ReplacedAxisBehavior, -) -> ReplacedAxisSolution { - if start.is_none() && end.is_none() { - start = Some(static_offset); - } - if end.is_none() || (behavior.clear_auto_margins_if_start_is_auto && start.is_none()) { - if margin_start.is_none() { - margin_start = Some(CssPixels::default()); - } - if margin_end.is_none() { - margin_end = Some(CssPixels::default()); - } - } - if margin_start.is_none() && margin_end.is_none() { - let remainder = available - auto_px_value(start) - auto_px_value(end); - if behavior.clear_negative_auto_margins && remainder < CssPixels::default() { - // This deliberately matches the C++ inline-axis implementation, - // which zeroes both margins instead of solving the end margin. - margin_start = Some(CssPixels::default()); - margin_end = Some(CssPixels::default()); - } else { - margin_start = Some(remainder / 2); - margin_end = Some(remainder / 2); - } - } - if start.is_none() { - start = Some(available - auto_px_value(end) - auto_px_value(margin_start) - auto_px_value(margin_end)); - } else if end.is_none() { - end = Some(available - auto_px_value(start) - auto_px_value(margin_start) - auto_px_value(margin_end)); - } else if margin_start.is_none() { - margin_start = Some(available - auto_px_value(start) - auto_px_value(end) - auto_px_value(margin_end)); - } else if margin_end.is_none() { - margin_end = Some(available - auto_px_value(start) - auto_px_value(margin_start) - auto_px_value(end)); - } - if CssPixels::default() - != available - - auto_px_value(start) - - auto_px_value(end) - - auto_px_value(margin_start) - - auto_px_value(margin_end) - { - end = Some(available - auto_px_value(start) - auto_px_value(margin_start) - auto_px_value(margin_end)); - } - ReplacedAxisSolution { - start: auto_px_value(start), - end: auto_px_value(end), - margin_start: auto_px_value(margin_start), - margin_end: auto_px_value(margin_end), - } -} - -impl AbsposEngine<'_, '_> { - fn static_offset(&self, node: Node, rect: StaticPositionRect) -> LogicalOffset { - let used = self.used(node); - let collapsed = used.uses_collapsing_borders_model.get(); - aligned_static_offset( - rect, - used.margin_box_inline_size(collapsed), - used.margin_box_block_size(collapsed), - ) - } - - fn solve_non_replaced_inline_once( - &self, - node: Node, - containing_block_inline_size: CssPixels, - _available_space: AvailableSpace, - constraints: ContainingBlockConstraints, - static_position_rect: StaticPositionRect, - input_inline_size: AutoPx, - ) -> (AutoPx, CssPixels, CssPixels, AutoPx, AutoPx) { - let style = self.style(node); - let used = self.used(node); - let border_left = style.border_left_width; - let border_right = style.border_right_width; - let padding_left = used.padding_left.get(); - let padding_right = used.padding_right.get(); - let computed_left = style.inset_left(); - let computed_right = style.inset_right(); - let mut left = style.inset_left().to_px(containing_block_inline_size); - let mut right = style.inset_right().to_px(containing_block_inline_size); - let mut margin_left = resolve_or_auto(style.margin_left(), containing_block_inline_size); - let mut margin_right = resolve_or_auto(style.margin_right(), containing_block_inline_size); - let mut inline_size = input_inline_size; - - let solve_for_left = |inline_size: AutoPx, margin_left: AutoPx, margin_right: AutoPx, right: CssPixels| { - containing_block_inline_size - - auto_px_value(margin_left) - - border_left - - padding_left - - auto_px_value(inline_size) - - padding_right - - border_right - - auto_px_value(margin_right) - - right - }; - let solve_for_inline_size = |left: CssPixels, margin_left: AutoPx, margin_right: AutoPx, right: CssPixels| { - (containing_block_inline_size - - left - - auto_px_value(margin_left) - - border_left - - padding_left - - padding_right - - border_right - - auto_px_value(margin_right) - - right) - .max(CssPixels::default()) - }; - let solve_for_right = |left: CssPixels, inline_size: AutoPx, margin_left: AutoPx, margin_right: AutoPx| { - containing_block_inline_size - - left - - auto_px_value(margin_left) - - border_left - - padding_left - - auto_px_value(inline_size) - - padding_right - - border_right - - auto_px_value(margin_right) - }; - let shrink_to_fit = |left: CssPixels, margin_left: AutoPx, margin_right: AutoPx, right: CssPixels| { - let available = solve_for_inline_size(left, margin_left, margin_right, right); - let sizing = self.sizing(); - let preferred = sizing.calculate_max_content_inline_size(node, constraints); - if preferred <= available { - preferred - } else { - let preferred_minimum = sizing.calculate_min_content_inline_size(node, constraints); - preferred_minimum.max(available).min(preferred) - } - }; - - if computed_left.is_auto() && inline_size.is_none() && computed_right.is_auto() { - if margin_left.is_none() { - margin_left = Some(CssPixels::default()); - } - if margin_right.is_none() { - margin_right = Some(CssPixels::default()); - } - let content_inline_size = shrink_to_fit(left, margin_left, margin_right, right); - inline_size = Some(content_inline_size); - self.used_mut(node).set_content_inline_size(content_inline_size); - left = self.static_offset(node, static_position_rect).inline_offset; - right = solve_for_right(left, inline_size, margin_left, margin_right); - } - - if !computed_left.is_auto() && inline_size.is_some() && !computed_right.is_auto() { - let available_for_margins = containing_block_inline_size - - border_left - - padding_left - - auto_px_value(inline_size) - - padding_right - - border_right - - left - - right; - if margin_left.is_none() && margin_right.is_none() { - margin_left = Some(available_for_margins / 2); - margin_right = Some(available_for_margins / 2); - return (inline_size, left, right, margin_left, margin_right); - } - if margin_left.is_none() { - margin_left = Some(available_for_margins); - return (inline_size, left, right, margin_left, margin_right); - } - if margin_right.is_none() { - margin_right = Some(available_for_margins); - return (inline_size, left, right, margin_left, margin_right); - } - right = solve_for_right(left, inline_size, margin_left, margin_right); - return (inline_size, left, right, margin_left, margin_right); - } - - if margin_left.is_none() { - margin_left = Some(CssPixels::default()); - } - if margin_right.is_none() { - margin_right = Some(CssPixels::default()); - } - - if computed_left.is_auto() && inline_size.is_none() && !computed_right.is_auto() { - inline_size = Some(shrink_to_fit(left, margin_left, margin_right, right)); - left = solve_for_left(inline_size, margin_left, margin_right, right); - } else if computed_left.is_auto() && computed_right.is_auto() && inline_size.is_some() { - left = self.static_offset(node, static_position_rect).inline_offset; - right = solve_for_right(left, inline_size, margin_left, margin_right); - } else if inline_size.is_none() && computed_right.is_auto() && !computed_left.is_auto() { - inline_size = Some(shrink_to_fit(left, margin_left, margin_right, right)); - right = solve_for_right(left, inline_size, margin_left, margin_right); - } else if computed_left.is_auto() && inline_size.is_some() && !computed_right.is_auto() { - left = solve_for_left(inline_size, margin_left, margin_right, right); - } else if inline_size.is_none() && !computed_left.is_auto() && !computed_right.is_auto() { - inline_size = Some(solve_for_inline_size(left, margin_left, margin_right, right)); - } else if computed_right.is_auto() && !computed_left.is_auto() && inline_size.is_some() { - right = solve_for_right(left, inline_size, margin_left, margin_right); - } - - (inline_size, left, right, margin_left, margin_right) - } - - fn compute_inline_size_for_non_replaced( - &self, - node: Node, - available_space: AvailableSpace, - constraints: ContainingBlockConstraints, - static_position_rect: StaticPositionRect, - ) { - let containing_block_inline_size = available_space.inline_size.to_px_or_zero(); - let style = self.style(node); - let sizing = self.sizing(); - let initial = if self.facts(node).is_table_wrapper() { - Some(sizing.compute_table_box_inline_size_inside_wrapper( - node, - available_space, - constraints, - None, - crate::layout::TableWrapperInlineSizeMode::ClampToAvailableInlineSize, - )) - } else if style.width().is_auto() { - None - } else { - Some(sizing.calculate_inner_inline_size(node, available_space.inline_size, style.width(), constraints)) - }; - let (mut used_inline_size, mut left, mut right, mut margin_left, mut margin_right) = self - .solve_non_replaced_inline_once( - node, - containing_block_inline_size, - available_space, - constraints, - static_position_rect, - initial, - ); - - if !sizing.should_treat_max_inline_size_as_none(node, available_space.inline_size, constraints) { - let max_inline_size = - sizing.calculate_inner_inline_size(node, available_space.inline_size, style.max_width(), constraints); - if auto_px_value(used_inline_size) > max_inline_size { - (used_inline_size, left, right, margin_left, margin_right) = self.solve_non_replaced_inline_once( - node, - containing_block_inline_size, - available_space, - constraints, - static_position_rect, - Some(max_inline_size), - ); - } - } - if !style.min_width().is_auto() { - let min_inline_size = - sizing.calculate_inner_inline_size(node, available_space.inline_size, style.min_width(), constraints); - if auto_px_value(used_inline_size) < min_inline_size { - (used_inline_size, left, right, margin_left, margin_right) = self.solve_non_replaced_inline_once( - node, - containing_block_inline_size, - available_space, - constraints, - static_position_rect, - Some(min_inline_size), - ); - } - } - - let used = self.used_mut(node); - used.set_content_inline_size(auto_px_value(used_inline_size)); - used.inset_left.set(left); - used.inset_right.set(right); - used.margin_left.set(auto_px_value(margin_left)); - used.margin_right.set(auto_px_value(margin_right)); - } - - fn compute_inline_size_for_replaced( - &self, - node: Node, - available_space: AvailableSpace, - constraints: ContainingBlockConstraints, - static_position_rect: StaticPositionRect, - ) { - let sizing = self.sizing(); - let inline_size = sizing.compute_inline_size_for_replaced_element(node, available_space, constraints); - let containing_block_inline_size = available_space.inline_size.to_px_or_zero(); - let style = self.style(node); - let used = self.used(node); - let available = containing_block_inline_size - - inline_size - - style.border_left_width - - used.padding_left.get() - - used.padding_right.get() - - style.border_right_width; - let solution = solve_replaced_axis( - available, - resolve_or_auto(style.inset_left(), containing_block_inline_size), - resolve_or_auto(style.inset_right(), containing_block_inline_size), - resolve_or_auto(style.margin_left(), containing_block_inline_size), - resolve_or_auto(style.margin_right(), containing_block_inline_size), - self.static_offset(node, static_position_rect).inline_offset, - ReplacedAxisBehavior { - clear_auto_margins_if_start_is_auto: true, - clear_negative_auto_margins: true, - }, - ); - - let used = self.used_mut(node); - used.inset_left.set(solution.start); - used.inset_right.set(solution.end); - used.margin_left.set(solution.margin_start); - used.margin_right.set(solution.margin_end); - used.set_content_inline_size(inline_size); - } - - fn compute_inline_size( - &self, - node: Node, - available_space: AvailableSpace, - constraints: ContainingBlockConstraints, - static_position_rect: StaticPositionRect, - ) { - if self - .sizing() - .box_is_sized_as_replaced_element(node, available_space, constraints) - { - self.compute_inline_size_for_replaced(node, available_space, constraints, static_position_rect); - } else { - self.compute_inline_size_for_non_replaced(node, available_space, constraints, static_position_rect); - } - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum BlockSizePass { - BeforeInsideLayout, - AfterInsideLayout, -} - -impl AbsposEngine<'_, '_> { - fn apply_min_max_block_size_constraints( - &self, - node: Node, - available_space: AvailableSpace, - constraints: ContainingBlockConstraints, - unconstrained: AutoPx, - ) -> AutoPx { - let style = self.style(node); - let sizing = self.sizing(); - let mut constrained = unconstrained; - if !style.max_height().is_none() { - let maximum = sizing.calculate_inner_block_size(node, available_space, style.max_height(), constraints); - if maximum < auto_px_value(constrained) { - constrained = Some(maximum); - } - } - if !style.min_height().is_auto() { - let minimum = sizing.calculate_inner_block_size(node, available_space, style.min_height(), constraints); - if minimum > auto_px_value(constrained) { - constrained = Some(minimum); - } - } - constrained - } - - fn automatic_block_size( - &self, - node: Node, - available_space: AvailableSpace, - constraints: ContainingBlockConstraints, - pass: BlockSizePass, - ) -> AutoPx { - if self.facts(node).creates_block_formatting_context() { - if pass == BlockSizePass::BeforeInsideLayout { - return None; - } - return Some(automatic_block_size_for_bfc_root(self.state, self.callbacks, node)); - } - let inner = self - .used(node) - .available_inner_space_or_constraints_from(available_space); - Some( - self.sizing() - .calculate_fit_content_size(node, SizingAxis::Block, inner, constraints), - ) - } - - fn solve_non_replaced_block_once( - &self, - node: Node, - available_space: AvailableSpace, - constraints: ContainingBlockConstraints, - static_position_rect: StaticPositionRect, - pass: BlockSizePass, - mut block_size: AutoPx, - ) -> (AutoPx, AutoPx, AutoPx, AutoPx, AutoPx) { - let style = self.style(node); - let containing_block_inline_size = available_space.inline_size.to_px_or_zero(); - let containing_block_block_size = available_space.block_size.to_px_or_zero(); - let mut margin_top = resolve_or_auto(style.margin_top(), containing_block_inline_size); - let mut margin_bottom = resolve_or_auto(style.margin_bottom(), containing_block_inline_size); - let mut top = resolve_or_auto(style.inset_top(), containing_block_block_size); - let mut bottom = resolve_or_auto(style.inset_bottom(), containing_block_block_size); - let used = self.used(node); - let padding_top = used.padding_top.get(); - let padding_bottom = used.padding_bottom.get(); - - let solve_for = |length: AutoPx, - clamp_to_zero: bool, - top: AutoPx, - margin_top: AutoPx, - block_size: AutoPx, - margin_bottom: AutoPx, - bottom: AutoPx| { - solve_abspos_axis_for( - containing_block_block_size, - length, - clamp_to_zero, - top, - margin_top, - style.border_top_width, - padding_top, - block_size, - padding_bottom, - style.border_bottom_width, - margin_bottom, - bottom, - ) - }; - - if top.is_none() && block_size.is_none() && bottom.is_none() { - if margin_top.is_none() { - margin_top = Some(CssPixels::default()); - } - if margin_bottom.is_none() { - margin_bottom = Some(CssPixels::default()); - } - let Some(automatic) = self.automatic_block_size(node, available_space, constraints, pass) else { - return (block_size, top, bottom, margin_top, margin_bottom); - }; - block_size = Some(automatic); - let constrained = self.apply_min_max_block_size_constraints(node, available_space, constraints, block_size); - self.used_mut(node).set_content_block_size(auto_px_value(constrained)); - top = Some(self.static_offset(node, static_position_rect).block_offset); - bottom = Some(solve_for( - bottom, - false, - top, - margin_top, - block_size, - margin_bottom, - bottom, - )); - } else if top.is_some() && block_size.is_some() && bottom.is_some() { - if margin_top.is_none() && margin_bottom.is_none() { - let remainder = solve_for( - Some(auto_px_value(margin_top) + auto_px_value(margin_bottom)), - false, - top, - margin_top, - block_size, - margin_bottom, - bottom, - ); - margin_top = Some(remainder / 2); - margin_bottom = Some(remainder / 2); - } else if margin_top.is_none() || margin_bottom.is_none() { - if margin_top.is_none() { - margin_top = Some(solve_for( - margin_top, - false, - top, - margin_top, - block_size, - margin_bottom, - bottom, - )); - } else { - margin_bottom = Some(solve_for( - margin_bottom, - false, - top, - margin_top, - block_size, - margin_bottom, - bottom, - )); - } - } else { - bottom = Some(solve_for( - bottom, - false, - top, - margin_top, - block_size, - margin_bottom, - bottom, - )); - } - } else { - if margin_top.is_none() { - margin_top = Some(CssPixels::default()); - } - if margin_bottom.is_none() { - margin_bottom = Some(CssPixels::default()); - } - - if top.is_none() && block_size.is_none() && bottom.is_some() { - let Some(automatic) = self.automatic_block_size(node, available_space, constraints, pass) else { - return (block_size, top, bottom, margin_top, margin_bottom); - }; - block_size = Some(automatic); - top = Some(solve_for( - top, - false, - top, - margin_top, - block_size, - margin_bottom, - bottom, - )); - } else if top.is_none() && bottom.is_none() && block_size.is_some() { - top = Some(self.static_offset(node, static_position_rect).block_offset); - bottom = Some(solve_for( - bottom, - false, - top, - margin_top, - block_size, - margin_bottom, - bottom, - )); - } else if block_size.is_none() && bottom.is_none() && top.is_some() { - let Some(automatic) = self.automatic_block_size(node, available_space, constraints, pass) else { - return (block_size, top, bottom, margin_top, margin_bottom); - }; - block_size = Some(automatic); - bottom = Some(solve_for( - bottom, - false, - top, - margin_top, - block_size, - margin_bottom, - bottom, - )); - } else if top.is_none() && block_size.is_some() && bottom.is_some() { - top = Some(solve_for( - top, - false, - top, - margin_top, - block_size, - margin_bottom, - bottom, - )); - } else if block_size.is_none() && top.is_some() && bottom.is_some() { - block_size = Some(solve_for( - block_size, - true, - top, - margin_top, - block_size, - margin_bottom, - bottom, - )); - } else if bottom.is_none() && top.is_some() && block_size.is_some() { - bottom = Some(solve_for( - bottom, - false, - top, - margin_top, - block_size, - margin_bottom, - bottom, - )); - } - } - (block_size, top, bottom, margin_top, margin_bottom) - } - - fn compute_block_size_for_non_replaced( - &self, - node: Node, - available_space: AvailableSpace, - constraints: ContainingBlockConstraints, - static_position_rect: StaticPositionRect, - pass: BlockSizePass, - ) { - let style = self.style(node); - let mut intrinsic_available_space = available_space; - intrinsic_available_space.inline_size = AvailableSize::definite(self.used(node).content_inline_size.get()); - let initial = if self.facts(node).is_table_wrapper() { - Some( - self.sizing() - .compute_table_box_block_size_inside_wrapper(node, available_space, constraints), - ) - } else if self - .sizing() - .should_treat_block_size_as_auto(node, available_space, constraints) - { - None - } else { - Some( - self.sizing() - .calculate_inner_block_size(node, intrinsic_available_space, style.height(), constraints), - ) - }; - let (mut used_block_size, mut top, mut bottom, mut margin_top, mut margin_bottom) = - self.solve_non_replaced_block_once(node, available_space, constraints, static_position_rect, pass, initial); - - if used_block_size.is_some() && !style.max_height().is_none() { - let max_block_size = self.sizing().calculate_inner_block_size( - node, - intrinsic_available_space, - style.max_height(), - constraints, - ); - if auto_px_value(used_block_size) > max_block_size { - (used_block_size, top, bottom, margin_top, margin_bottom) = self.solve_non_replaced_block_once( - node, - available_space, - constraints, - static_position_rect, - pass, - Some(max_block_size), - ); - } - } - if used_block_size.is_some() && !style.min_height().is_auto() { - let min_block_size = self.sizing().calculate_inner_block_size( - node, - intrinsic_available_space, - style.min_height(), - constraints, - ); - if auto_px_value(used_block_size) < min_block_size { - (used_block_size, top, bottom, margin_top, margin_bottom) = self.solve_non_replaced_block_once( - node, - available_space, - constraints, - static_position_rect, - pass, - Some(min_block_size), - ); - } - } - if used_block_size.is_none() { - used_block_size = - self.apply_min_max_block_size_constraints(node, available_space, constraints, used_block_size); - } - - let containing_block_inline_size = available_space.inline_size.to_px_or_zero(); - let containing_block_block_size = available_space.block_size.to_px_or_zero(); - let used = self.used_mut(node); - used.set_content_block_size(auto_px_value(used_block_size)); - if style.height().is_auto() && pass == BlockSizePass::BeforeInsideLayout { - return; - } - if !style.height().is_intrinsic_sizing_constraint() { - used.has_definite_block_size.set(true); - } - used.inset_top.set(auto_px_value(top)); - used.inset_bottom.set(auto_px_value(bottom)); - // The local values are already resolved against these bases. Keep the - // variables to document and pin the C++ basis distinction. - let _ = (containing_block_inline_size, containing_block_block_size); - used.margin_top.set(auto_px_value(margin_top)); - used.margin_bottom.set(auto_px_value(margin_bottom)); - } - - fn compute_block_size_for_replaced( - &self, - node: Node, - available_space: AvailableSpace, - constraints: ContainingBlockConstraints, - static_position_rect: StaticPositionRect, - pass: BlockSizePass, - ) { - let block_size = self - .sizing() - .compute_block_size_for_replaced_element(node, available_space, constraints); - let containing_block_block_size = available_space.block_size.to_px_or_zero(); - let style = self.style(node); - let used = self.used(node); - let available = containing_block_block_size - - block_size - - style.border_top_width - - used.padding_top.get() - - used.padding_bottom.get() - - style.border_bottom_width; - // Deliberately pass false for `clear_auto_margins_if_start_is_auto`: - // this matches the C++ condition, which tests only the end inset. - let solution = solve_replaced_axis( - available, - resolve_or_auto(style.inset_top(), containing_block_block_size), - resolve_or_auto(style.inset_bottom(), containing_block_block_size), - resolve_or_auto(style.margin_top(), containing_block_block_size), - resolve_or_auto(style.margin_bottom(), containing_block_block_size), - self.static_offset(node, static_position_rect).block_offset, - ReplacedAxisBehavior { - clear_auto_margins_if_start_is_auto: false, - clear_negative_auto_margins: false, - }, - ); - - let used = self.used_mut(node); - used.set_content_block_size(block_size); - if style.height().is_auto() && pass == BlockSizePass::BeforeInsideLayout { - return; - } - if !style.height().is_intrinsic_sizing_constraint() { - used.has_definite_block_size.set(true); - } - used.inset_top.set(solution.start); - used.inset_bottom.set(solution.end); - used.margin_top.set(solution.margin_start); - used.margin_bottom.set(solution.margin_end); - } - - fn compute_block_size( - &self, - node: Node, - available_space: AvailableSpace, - constraints: ContainingBlockConstraints, - static_position_rect: StaticPositionRect, - pass: BlockSizePass, - ) { - if self - .sizing() - .box_is_sized_as_replaced_element(node, available_space, constraints) - { - self.compute_block_size_for_replaced(node, available_space, constraints, static_position_rect, pass); - } else { - self.compute_block_size_for_non_replaced(node, available_space, constraints, static_position_rect, pass); - } - } -} - -impl<'pass> AbsposEngine<'_, 'pass> { - fn layout_element( - &self, - frame: &mut crate::layout::FcFrame<'pass>, - parent_block: Option<&BlockFormattingContext<'pass>>, - parent_grid: Option<&GridFormattingContext<'pass>>, - node: Node, - inputs: AbsposLayoutInputs, - ) { - assert!(!self.facts(node).is_svg_box()); - let containing_block_size = LogicalSize { - inline_size: clamp_to_max_dimension_value(inputs.containing_block_info.rect.size.inline_size), - block_size: clamp_to_max_dimension_value(inputs.containing_block_info.rect.size.block_size), - }; - let available_space = AvailableSpace { - inline_size: AvailableSize::definite(containing_block_size.inline_size), - block_size: AvailableSize::definite(containing_block_size.block_size), - }; - let constraints = ContainingBlockConstraints { - percentage_basis_inline_size: Some(containing_block_size.inline_size), - percentage_basis_block_size: Some(containing_block_size.block_size), - quirks_mode_percentage_basis_block_size: None, - }; - let style = self.style(node); - { - let used = self.used_mut(node); - used.border_left.set(style.border_left_width); - used.border_right.set(style.border_right_width); - used.border_top.set(style.border_top_width); - used.border_bottom.set(style.border_bottom_width); - used.padding_left - .set(style.padding_left().to_px(containing_block_size.inline_size)); - used.padding_right - .set(style.padding_right().to_px(containing_block_size.inline_size)); - used.padding_top - .set(style.padding_top().to_px(containing_block_size.inline_size)); - used.padding_bottom - .set(style.padding_bottom().to_px(containing_block_size.inline_size)); - } - - self.compute_inline_size(node, available_space, constraints, inputs.static_position_rect); - self.compute_block_size( - node, - available_space, - constraints, - inputs.static_position_rect, - BlockSizePass::BeforeInsideLayout, - ); - - { - let used = self.used_mut(node); - if !style.inset_left().is_auto() && !style.inset_right().is_auto() { - used.has_definite_inline_size.set(true); - } - if !style.inset_top().is_auto() - && !style.inset_bottom().is_auto() - && (style.height().is_auto() || !style.height().is_intrinsic_sizing_constraint()) - { - used.has_definite_block_size.set(true); - } - } - if !self.facts(node).creates_block_formatting_context() { - let block_size_resolved_from_aspect_ratio = style.height().is_auto() - && self.facts(node).has_preferred_aspect_ratio() - && self.used(node).has_definite_inline_size(); - let used = self.used_mut(node); - used.has_definite_inline_size.set(true); - if (!style.height().is_auto() && !style.height().is_intrinsic_sizing_constraint()) - || block_size_resolved_from_aspect_ratio - { - used.has_definite_block_size.set(true); - } - } - - self.sizing() - .make_button_content_box_definite(node, self.layout_mode, available_space, constraints, None); - - let inner_available_space = self - .used(node) - .available_inner_space_or_constraints_from(available_space); - let child_layout = match crate::layout::layout_inside_child( - frame, - parent_block, - parent_grid, - node, - LayoutMode::Normal, - LayoutInput { - available_space: inner_available_space, - containing_block_constraints: constraints, - content_box_position_in_bfc_root: None, - table_grid_min_border_box_block_size: None, - }, - false, - ) { - crate::layout::ChildLayoutOutcome::Created(child_layout) => Some(child_layout), - crate::layout::ChildLayoutOutcome::Skipped => None, - // Absolutely positioned boxes with children establish an - // independent formatting context, so they cannot remain in - // the currently running context. - crate::layout::ChildLayoutOutcome::ReenterCurrent => { - unreachable!("abspos child with contents did not establish a formatting context") - } - }; - - if style.height().is_auto() { - self.compute_block_size( - node, - available_space, - constraints, - inputs.static_position_rect, - BlockSizePass::AfterInsideLayout, - ); - } - - { - let used = self.used_mut(node); - let collapsed = used.uses_collapsing_borders_model.get(); - if let Some(inline_alignment) = inputs.containing_block_info.inline_alignment - && style.inset_left().is_auto() - && style.inset_right().is_auto() - { - let available = containing_block_size.inline_size - used.margin_box_inline_size(collapsed); - match inline_alignment { - AbsposAlignment::Center => { - used.inset_left.set(available / 2); - used.inset_right.set(available / 2); - } - AbsposAlignment::Start => { - used.inset_right.set(available); - } - AbsposAlignment::End => { - used.inset_left.set(available); - } - _ => {} - } - } - if let Some(block_alignment) = inputs.containing_block_info.block_alignment - && style.inset_top().is_auto() - && style.inset_bottom().is_auto() - { - let available = containing_block_size.block_size - used.margin_box_block_size(collapsed); - match block_alignment { - AbsposAlignment::Center => { - used.inset_top.set(available / 2); - used.inset_bottom.set(available / 2); - } - AbsposAlignment::Start | AbsposAlignment::SelfStart => { - used.inset_bottom.set(available); - } - AbsposAlignment::End | AbsposAlignment::SelfEnd => { - used.inset_top.set(available); - } - _ => {} - } - } - } - - let static_offset = self.static_offset(node, inputs.static_position_rect); - let used = self.used(node); - let collapsed = used.uses_collapsing_borders_model.get(); - let mut used_offset = LogicalOffset { - inline_offset: if inputs.containing_block_info.inline_axis_mode == AbsposAxisMode::StaticPosition { - static_offset.inline_offset - } else { - inputs.containing_block_info.rect.offset.inline_offset + used.inset_left.get() - }, - block_offset: if inputs.containing_block_info.block_axis_mode == AbsposAxisMode::StaticPosition { - static_offset.block_offset - } else { - inputs.containing_block_info.rect.offset.block_offset + used.inset_top.get() - }, - }; - used_offset.inline_offset += used.margin_left.get() + used.border_box_left(collapsed); - used_offset.block_offset += used.margin_top.get() + used.border_box_top(collapsed); - crate::layout::place_child( - self.state, - &self.callbacks, - node, - FfiCssPixelPoint { - x: used_offset.inline_offset, - y: used_offset.block_offset, - }, - ); - - let is_measurement = self.state.is_measurement(); - if self.layout_mode == LayoutMode::Normal && !is_measurement { - self.state - .used_values_rare_data_for_node_mut(&self.callbacks, node) - .abspos_layout_inputs = Some(inputs); - } - - if let Some(child_layout) = child_layout { - child_layout.finish(); - } - } - - pub(crate) fn layout_children( - &self, - frame: &mut crate::layout::FcFrame<'pass>, - parent_block: Option<&BlockFormattingContext<'pass>>, - parent_grid: Option<&GridFormattingContext<'pass>>, - box_: Node, - ) { - if self.layout_mode != LayoutMode::Normal { - return; - } - if self.state.is_measurement() { - return; - } - while let Some(child) = self.state.take_next_contained_abspos_child(box_) { - let child_box = child.child_box; - if self.try_used_pointer(child_box).is_none() { - self.state - .create_used_values(&self.callbacks, child_box, ContainingBlockConstraints::default()); - } - self.resolve_anchor_insets(child_box); - let inputs = AbsposLayoutInputs { - static_position_rect: self - .resolve_static_position_relative_to_containing_block(child_box, child.static_position_rect), - containing_block_info: self.containing_block_info(child_box), - }; - self.layout_element(frame, parent_block, parent_grid, child_box, inputs); - } - } - - fn replay(&self, frame: &mut crate::layout::FcFrame<'pass>, node: Node) { - let saved_inputs = self.callbacks.saved_abspos_layout_inputs(node); - let found = saved_inputs.is_some(); - assert!(found); - let mut inputs = saved_inputs.unwrap(); - if !inputs.containing_block_info.derives_from_own_computed_values { - let (inline, block) = axis_modes(self.style(node)); - inputs.containing_block_info.inline_axis_mode = inline; - inputs.containing_block_info.block_axis_mode = block; - } - // Partial relayout uses a fresh state and creates the replay root - // exactly once. - self.state - .create_used_values(&self.callbacks, node, ContainingBlockConstraints::default()); - self.layout_element(frame, None, None, node, inputs); - } - - fn compute_inset(&self, node: Node, containing_block_size: LogicalSize) { - // Most boxes are neither relatively positioned nor carry anchor() - // insets. Preserve the old C++ fast path without populating the - // comprehensive Rust facts caches for those boxes. - // SAFETY: The callback only reads the live node's computed values. - if !unsafe { - (self.callbacks.needs_inset_resolution)(self.callbacks.context, self.callbacks.shell(node)) - } { - return; - } - let initial_style = self.style(node); - if initial_style.inset_top().contains_anchor_function - || initial_style.inset_right().contains_anchor_function - || initial_style.inset_bottom().contains_anchor_function - || initial_style.inset_left().contains_anchor_function - { - self.resolve_anchor_insets(node); - } - let style = self.style(node); - if style.position != positioning::RELATIVE { - return; - } - - let resolve_opposing = |first: FfiSizeValue, second: FfiSizeValue, basis: CssPixels| { - let resolved_first = first.to_px(basis); - let resolved_second = second.to_px(basis); - if first.is_auto() && second.is_auto() { - (CssPixels::default(), CssPixels::default()) - } else if first.is_auto() { - (-resolved_second, resolved_second) - } else { - (resolved_first, -resolved_first) - } - }; - let (left, right) = resolve_opposing( - style.inset_left(), - style.inset_right(), - containing_block_size.inline_size, - ); - - let treat_percentage_as_auto = |value: FfiSizeValue| { - if !value.contains_percentage { - return value; - } - let mut containing_block = self.callbacks.containing_block(node); - while !containing_block.is_invalid() { - let facts = self.facts(containing_block); - if !facts.is_anonymous() || facts.is_table_cell() { - break; - } - containing_block = self.callbacks.containing_block(containing_block); - } - if !containing_block.is_invalid() && !self.used(containing_block).has_definite_block_size() { - FfiSizeValue::auto_value() - } else { - value - } - }; - let (top, bottom) = resolve_opposing( - treat_percentage_as_auto(style.inset_top()), - treat_percentage_as_auto(style.inset_bottom()), - containing_block_size.block_size, - ); - let used = self.used_mut(node); - used.inset_left.set(left); - used.inset_right.set(right); - used.inset_top.set(top); - used.inset_bottom.set(bottom); - } -} - -fn layout_children_for_instance(instance: &mut crate::layout::FormattingContextInstance, box_: Node) { - if instance.layout_mode != LayoutMode::Normal { - return; - } - let crate::layout::FormattingContextInstance { frame, implementation } = instance; - let (parent_block, parent_grid) = match &*implementation { - crate::layout::FcImpl::Block(context) => (Some(context.as_ref()), None), - crate::layout::FcImpl::Grid(context) => (None, Some(context.as_ref())), - _ => (None, None), - }; - AbsposEngine::new(frame.state, frame.callbacks, frame.layout_mode, frame.box_, parent_grid).layout_children( - frame, - parent_block, - parent_grid, - box_, - ); -} - -pub(crate) fn layout_children_native<'pass>( - state: &'pass LayoutState, - callbacks: FfiLayoutFcCallbacks, - layout_mode: LayoutMode, - context_box: Node, - frame: &mut crate::layout::FcFrame<'pass>, - box_: Node, -) { - AbsposEngine::new(state, callbacks, layout_mode, context_box, None).layout_children(frame, None, None, box_); -} - -pub(crate) fn compute_inset_native( - state: &LayoutState, - callbacks: FfiLayoutFcCallbacks, - layout_mode: LayoutMode, - context_box: Node, - node: Node, - inline_size: CssPixels, - block_size: CssPixels, -) { - AbsposEngine::new(state, callbacks, layout_mode, context_box, None).compute_inset( - node, - LogicalSize { - inline_size, - block_size, - }, - ); -} - -fn replay_for_instance(instance: &mut crate::layout::FormattingContextInstance, node: Node) { - assert!(matches!(instance.implementation, crate::layout::FcImpl::AbsposReplay)); - AbsposEngine::new( - instance.state, - instance.callbacks, - instance.layout_mode, - instance.box_, - None, - ) - .replay(&mut instance.frame, node); -} - - pub(crate) type Node = NodeSlotId; #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/Libraries/LibWeb/Rust/src/layout/mod.rs b/Libraries/LibWeb/Rust/src/layout/mod.rs index 3ca4e24b27b5a..e47f0a7aa823c 100644 --- a/Libraries/LibWeb/Rust/src/layout/mod.rs +++ b/Libraries/LibWeb/Rust/src/layout/mod.rs @@ -16,6 +16,7 @@ pub(crate) use crate::css::display::*; include!("node_facts.rs"); include!("formatting_context.rs"); +include!("abspos_engine.rs"); include!("block_formatting_context.rs"); include!("flex_formatting_context.rs"); include!("grid_formatting_context.rs"); From 2275a26aa29588679f845c01cb42a5fa2fb68d42 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Thu, 30 Jul 2026 00:48:29 +0200 Subject: [PATCH 2/2] LibWeb: Lay out absolutely positioned boxes after in-flow layout Absolutely positioned boxes were laid out from inside formatting context completions, in the middle of in-flow layout, with a destructor fallback covering every place that never completed its context. That arrangement hid real problems. Layout ran at points no caller stated explicitly, so a forgotten completion was silently patched instead of caught. A box could resolve its static position against geometry that was not final yet, which is why table cells needed their own layout point after vertical alignment. Anchor resolution saw whatever happened to be laid out at the moment a completion ran. And because laying out an absolutely positioned box required live formatting context objects, there was no way to lay out only the absolutely positioned boxes without re-running everything around them. Now every creator of a formatting context completes it explicitly once its root box has a used size, and completion only records work: it places the block context's floats, registers absolutely positioned children, stores grid-area containing blocks while the grid's tracks are still available, and queues the completed root. The layout entry points run one pass over the queue before committing, when all in-flow geometry is final. The saved-layout replay entry now runs the engine on a bare frame directly, which lets the AbsposReplay context type go away entirely. A foreignObject that is a flex or grid container previously never laid out its absolutely positioned children, because the destructor fallback only handled block contexts; explicit completion fixes that, and the new foreignObject-flex-with-abspos-child layout test fails before this change and passes after it. A second new layout test pins subgrid behavior for an absolutely positioned child of a grid container: no track adoption, since it is not a grid item. --- Libraries/LibWeb/Layout/LayoutRustBridge.cpp | 2 - .../LibWeb/Rust/src/layout/abspos_engine.rs | 160 ++++++------------ .../src/layout/block_formatting_context.rs | 5 +- .../src/layout/flex_formatting_context.rs | 2 - .../Rust/src/layout/formatting_context.rs | 119 ++++++------- .../src/layout/grid_formatting_context.rs | 14 +- .../src/layout/inline_formatting_context.rs | 2 - .../LibWeb/Rust/src/layout/layout_state.rs | 96 ++++++++--- Libraries/LibWeb/Rust/src/layout/mod.rs | 1 + ...placed_with_children_formatting_context.rs | 3 +- .../Rust/src/layout/svg_formatting_context.rs | 4 +- .../src/layout/table_formatting_context.rs | 10 -- ...abspos-subgrid-child-of-grid-container.txt | 21 +++ .../foreignObject-flex-with-abspos-child.txt | 30 ++++ ...bspos-subgrid-child-of-grid-container.html | 8 + .../foreignObject-flex-with-abspos-child.html | 14 ++ 16 files changed, 267 insertions(+), 224 deletions(-) create mode 100644 Tests/LibWeb/Layout/expected/grid/abspos-subgrid-child-of-grid-container.txt create mode 100644 Tests/LibWeb/Layout/expected/svg/foreignObject-flex-with-abspos-child.txt create mode 100644 Tests/LibWeb/Layout/input/grid/abspos-subgrid-child-of-grid-container.html create mode 100644 Tests/LibWeb/Layout/input/svg/foreignObject-flex-with-abspos-child.html diff --git a/Libraries/LibWeb/Layout/LayoutRustBridge.cpp b/Libraries/LibWeb/Layout/LayoutRustBridge.cpp index 141385258c960..c8b81fbdd17f7 100644 --- a/Libraries/LibWeb/Layout/LayoutRustBridge.cpp +++ b/Libraries/LibWeb/Layout/LayoutRustBridge.cpp @@ -1111,8 +1111,6 @@ StringView formatting_context_type_name(RustFFI::FfiFormattingContextType type) return "SVG"sv; case RustFFI::FfiFormattingContextType::ReplacedWithChildren: return "Replaced, with children"sv; - case RustFFI::FfiFormattingContextType::AbsposReplay: - return "Abspos replay"sv; case RustFFI::FfiFormattingContextType::InternalReplaced: return "Replaced"sv; case RustFFI::FfiFormattingContextType::InternalDummy: diff --git a/Libraries/LibWeb/Rust/src/layout/abspos_engine.rs b/Libraries/LibWeb/Rust/src/layout/abspos_engine.rs index d7ee87cb8584c..2f36dcded2ac0 100644 --- a/Libraries/LibWeb/Rust/src/layout/abspos_engine.rs +++ b/Libraries/LibWeb/Rust/src/layout/abspos_engine.rs @@ -46,30 +46,14 @@ pub(crate) fn aligned_static_offset( offset } -pub(crate) struct AbsposEngine<'a, 'pass> { +pub(crate) struct AbsposEngine<'pass> { state: &'pass LayoutState, callbacks: FfiLayoutFcCallbacks, - layout_mode: LayoutMode, - context_box: Node, - grid_context: Option<&'a GridFormattingContext<'pass>>, } -impl<'a, 'pass> AbsposEngine<'a, 'pass> { - fn new( - state: &'pass LayoutState, - callbacks: FfiLayoutFcCallbacks, - layout_mode: LayoutMode, - context_box: Node, - grid_context: Option<&'a GridFormattingContext<'pass>>, - ) -> Self { - assert!(!context_box.is_invalid()); - Self { - state, - callbacks, - layout_mode, - context_box, - grid_context, - } +impl<'pass> AbsposEngine<'pass> { + fn new(state: &'pass LayoutState, callbacks: FfiLayoutFcCallbacks) -> Self { + Self { state, callbacks } } fn sizing(&self) -> SizingContext<'_> { @@ -101,7 +85,7 @@ impl<'a, 'pass> AbsposEngine<'a, 'pass> { } fn static_position_containing_block(&self, node: Node) -> Node { - unsafe { (self.callbacks.static_position_containing_block)(self.callbacks.context, self.callbacks.shell(node)) } + self.callbacks.static_position_containing_block(node) } fn inline_containing_block(&self, node: Node) -> Node { @@ -401,19 +385,6 @@ impl<'a, 'pass> AbsposEngine<'a, 'pass> { } } - fn containing_block_info(&self, node: Node) -> AbsposContainingBlockInfo { - let base = self.base_containing_block_info(node); - let Some(grid) = self.grid_context else { - return base; - }; - let mut info = grid.abspos_containing_block_info(node); - let uses_grid_area_as_static_position = self.static_position_containing_block(node) == self.context_box; - if !uses_grid_area_as_static_position { - info.inline_axis_mode = base.inline_axis_mode; - info.block_axis_mode = base.block_axis_mode; - } - info - } } fn add_fragment_rect( @@ -452,8 +423,8 @@ struct AnchorValueAxis { } #[derive(Clone, Copy)] -struct AnchorCalcCallbackContext<'a, 'pass> { - engine: *const AbsposEngine<'a, 'pass>, +struct AnchorCalcCallbackContext<'pass> { + engine: *const AbsposEngine<'pass>, positioned_box: Node, containing_block: Node, is_from_end: bool, @@ -462,7 +433,7 @@ struct AnchorCalcCallbackContext<'a, 'pass> { resolution_state: *mut AnchorResolutionState, } -impl AbsposEngine<'_, '_> { +impl AbsposEngine<'_> { fn anchor_lookup(&self, positioned_box: Node, anchor_name: usize) -> Option { let eligible_anchor_boxes = self.state.used_value_nodes(); let eligible_anchor_shells = eligible_anchor_boxes @@ -772,7 +743,7 @@ impl AbsposEngine<'_, '_> { unsafe extern "C" fn resolve_anchor_non_math_function(context: *mut c_void, shell: *const c_void) -> *const c_void { // SAFETY: The CSS calc engine calls this only during resolve_anchor_value, // whose stack owns this callback context. - let context = unsafe { &mut *context.cast::>() }; + let context = unsafe { &mut *context.cast::>() }; // SAFETY: The engine pointer is live for the enclosing resolution. let engine = unsafe { &*context.engine }; // SAFETY: `shell` is the live Rust style-value handle supplied by the @@ -974,7 +945,7 @@ pub(crate) fn solve_replaced_axis( } } -impl AbsposEngine<'_, '_> { +impl AbsposEngine<'_> { fn static_offset(&self, node: Node, rect: StaticPositionRect) -> LogicalOffset { let used = self.used(node); let collapsed = used.uses_collapsing_borders_model.get(); @@ -1254,7 +1225,7 @@ enum BlockSizePass { AfterInsideLayout, } -impl AbsposEngine<'_, '_> { +impl AbsposEngine<'_> { fn apply_min_max_block_size_constraints( &self, node: Node, @@ -1654,15 +1625,8 @@ impl AbsposEngine<'_, '_> { } } -impl<'pass> AbsposEngine<'_, 'pass> { - fn layout_element( - &self, - frame: &mut crate::layout::FcFrame<'pass>, - parent_block: Option<&BlockFormattingContext<'pass>>, - parent_grid: Option<&GridFormattingContext<'pass>>, - node: Node, - inputs: AbsposLayoutInputs, - ) { +impl<'pass> AbsposEngine<'pass> { + fn layout_element(&self, frame: &mut crate::layout::FcFrame<'pass>, node: Node, inputs: AbsposLayoutInputs) { assert!(!self.facts(node).is_svg_box()); let containing_block_size = LogicalSize { inline_size: clamp_to_max_dimension_value(inputs.containing_block_info.rect.size.inline_size), @@ -1729,15 +1693,15 @@ impl<'pass> AbsposEngine<'_, 'pass> { } self.sizing() - .make_button_content_box_definite(node, self.layout_mode, available_space, constraints, None); + .make_button_content_box_definite(node, LayoutMode::Normal, available_space, constraints, None); let inner_available_space = self .used(node) .available_inner_space_or_constraints_from(available_space); let child_layout = match crate::layout::layout_inside_child( frame, - parent_block, - parent_grid, + None, + None, node, LayoutMode::Normal, LayoutInput { @@ -1839,7 +1803,7 @@ impl<'pass> AbsposEngine<'_, 'pass> { ); let is_measurement = self.state.is_measurement(); - if self.layout_mode == LayoutMode::Normal && !is_measurement { + if !is_measurement { self.state .used_values_rare_data_for_node_mut(&self.callbacks, node) .abspos_layout_inputs = Some(inputs); @@ -1850,20 +1814,9 @@ impl<'pass> AbsposEngine<'_, 'pass> { } } - pub(crate) fn layout_children( - &self, - frame: &mut crate::layout::FcFrame<'pass>, - parent_block: Option<&BlockFormattingContext<'pass>>, - parent_grid: Option<&GridFormattingContext<'pass>>, - box_: Node, - ) { - if self.layout_mode != LayoutMode::Normal { - return; - } - if self.state.is_measurement() { - return; - } - while let Some(child) = self.state.take_next_contained_abspos_child(box_) { + pub(crate) fn layout_children(&self, frame: &mut crate::layout::FcFrame<'pass>) { + debug_assert!(!self.state.is_measurement()); + while let Some(child) = self.state.take_next_contained_abspos_child(frame.box_) { let child_box = child.child_box; if self.try_used_pointer(child_box).is_none() { self.state @@ -1873,9 +1826,11 @@ impl<'pass> AbsposEngine<'_, 'pass> { let inputs = AbsposLayoutInputs { static_position_rect: self .resolve_static_position_relative_to_containing_block(child_box, child.static_position_rect), - containing_block_info: self.containing_block_info(child_box), + containing_block_info: child + .containing_block_info_override + .unwrap_or_else(|| self.base_containing_block_info(child_box)), }; - self.layout_element(frame, parent_block, parent_grid, child_box, inputs); + self.layout_element(frame, child_box, inputs); } } @@ -1893,7 +1848,7 @@ impl<'pass> AbsposEngine<'_, 'pass> { // exactly once. self.state .create_used_values(&self.callbacks, node, ContainingBlockConstraints::default()); - self.layout_element(frame, None, None, node, inputs); + self.layout_element(frame, node, inputs); } fn compute_inset(&self, node: Node, containing_block_size: LogicalSize) { @@ -1967,45 +1922,46 @@ impl<'pass> AbsposEngine<'_, 'pass> { } } -fn layout_children_for_instance(instance: &mut crate::layout::FormattingContextInstance, box_: Node) { - if instance.layout_mode != LayoutMode::Normal { - return; - } - let crate::layout::FormattingContextInstance { frame, implementation } = instance; - let (parent_block, parent_grid) = match &*implementation { - crate::layout::FcImpl::Block(context) => (Some(context.as_ref()), None), - crate::layout::FcImpl::Grid(context) => (None, Some(context.as_ref())), - _ => (None, None), - }; - AbsposEngine::new(frame.state, frame.callbacks, frame.layout_mode, frame.box_, parent_grid).layout_children( - frame, - parent_block, - parent_grid, - box_, - ); +pub(crate) fn layout_contained_abspos_children(frame: &mut crate::layout::FcFrame<'_>) { + AbsposEngine::new(frame.state, frame.callbacks).layout_children(frame); } -pub(crate) fn layout_children_native<'pass>( - state: &'pass LayoutState, +/// Lays out every registered abspos child once the in-flow run has finished. +/// Queues are processed in the order their formatting contexts completed, so +/// deeper containing blocks come first and the root comes last: the layout +/// order anchor() acceptability assumes for absolutely positioned anchors. +/// A formatting context completing during the pass belongs to an absolutely +/// positioned subtree; its children are laid out at that completion point, +/// before later boxes of the queue that produced it. +pub(crate) fn run_abspos_layout_pass( + state: &LayoutState, callbacks: FfiLayoutFcCallbacks, - layout_mode: LayoutMode, - context_box: Node, - frame: &mut crate::layout::FcFrame<'pass>, - box_: Node, + should_collect_devtools_layout_data: bool, ) { - AbsposEngine::new(state, callbacks, layout_mode, context_box, None).layout_children(frame, None, None, box_); + state.set_abspos_layout_pass_is_active(true); + while let Some(root) = state.pop_from_abspos_layout_pass_queue() { + if !state.has_contained_abspos_children(root) { + continue; + } + let mut frame = + crate::layout::FcFrame::new(state, root, LayoutMode::Normal, callbacks, should_collect_devtools_layout_data); + layout_contained_abspos_children(&mut frame); + } + state.set_abspos_layout_pass_is_active(false); + debug_assert!( + state.all_registered_contained_abspos_children_are_laid_out(), + "registered abspos children were left without layout after the pass" + ); } pub(crate) fn compute_inset_native( state: &LayoutState, callbacks: FfiLayoutFcCallbacks, - layout_mode: LayoutMode, - context_box: Node, node: Node, inline_size: CssPixels, block_size: CssPixels, ) { - AbsposEngine::new(state, callbacks, layout_mode, context_box, None).compute_inset( + AbsposEngine::new(state, callbacks).compute_inset( node, LogicalSize { inline_size, @@ -2013,15 +1969,3 @@ pub(crate) fn compute_inset_native( }, ); } - -fn replay_for_instance(instance: &mut crate::layout::FormattingContextInstance, node: Node) { - assert!(matches!(instance.implementation, crate::layout::FcImpl::AbsposReplay)); - AbsposEngine::new( - instance.state, - instance.callbacks, - instance.layout_mode, - instance.box_, - None, - ) - .replay(&mut instance.frame, node); -} diff --git a/Libraries/LibWeb/Rust/src/layout/block_formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/block_formatting_context.rs index 93038db48cba8..675a825eaa172 100644 --- a/Libraries/LibWeb/Rust/src/layout/block_formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/block_formatting_context.rs @@ -309,8 +309,6 @@ impl<'pass> BlockFormattingContext<'pass> { crate::layout::compute_inset_native( self.state, self.callbacks, - self.layout_mode, - self.root, node, containing_block_size.inline_size, containing_block_size.block_size, @@ -2517,6 +2515,9 @@ impl<'pass> BlockFormattingContext<'pass> { None }; let Some(side) = side else { + if let Some(child_layout) = child_layout { + child_layout.finish(); + } return; }; let mut margin_box_ceiling = if let Some(line_builder) = line_builder.as_deref_mut() { diff --git a/Libraries/LibWeb/Rust/src/layout/flex_formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/flex_formatting_context.rs index 687efd1e0373e..19ac51618be11 100644 --- a/Libraries/LibWeb/Rust/src/layout/flex_formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/flex_formatting_context.rs @@ -2398,8 +2398,6 @@ impl<'pass> FlexFormattingContext<'pass> { crate::layout::compute_inset_native( self.state, self.callbacks, - self.layout_mode, - self.flex_container, node, container_inline_size, container_block_size, diff --git a/Libraries/LibWeb/Rust/src/layout/formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/formatting_context.rs index 36dacde5aa2bc..376cf3f0728b3 100644 --- a/Libraries/LibWeb/Rust/src/layout/formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/formatting_context.rs @@ -277,7 +277,8 @@ impl MeasurementState { false, self.callbacks, ); - crate::layout::run_formatting_context(&mut context, input, None, None); + crate::layout::run_formatting_context(&mut context, input, None); + complete_formatting_context_after_root_box_has_used_size(&mut context); crate::layout::ChildLayoutResult { automatic_content_inline_size: context.automatic_content_inline_size, automatic_content_block_size: context.automatic_content_block_size, @@ -2624,7 +2625,6 @@ pub enum FfiFormattingContextType { Table, Svg, ReplacedWithChildren, - AbsposReplay, InternalReplaced, InternalDummy, } @@ -2685,10 +2685,8 @@ impl PendingChildLayout<'_> { } pub(crate) fn finish(mut self) { - parent_did_dimension(&mut self.context); + complete_formatting_context_after_root_box_has_used_size(&mut self.context); } - - pub(crate) fn discard(self) {} } pub(crate) enum ChildLayoutOutcome<'pass> { @@ -2945,6 +2943,10 @@ impl FfiLayoutFcCallbacks { self.node_data(node).inline_containing_block } + pub(crate) fn static_position_containing_block(&self, node: Node) -> Node { + unsafe { (self.static_position_containing_block)(self.context, self.shell(node)) } + } + pub(crate) fn is_ancestor(&self, ancestor: Node, mut node: Node) -> bool { while !node.is_invalid() { if node == ancestor { @@ -2976,6 +2978,26 @@ pub(crate) struct FcFrame<'pass> { pub(crate) automatic_content_block_size: CssPixels, } +impl<'pass> FcFrame<'pass> { + pub(crate) fn new( + state: &'pass LayoutState, + box_: Node, + layout_mode: LayoutMode, + callbacks: FfiLayoutFcCallbacks, + should_collect_devtools_layout_data: bool, + ) -> Self { + Self { + state, + box_, + layout_mode, + callbacks, + should_collect_devtools_layout_data, + automatic_content_inline_size: CssPixels::default(), + automatic_content_block_size: CssPixels::default(), + } + } +} + #[derive(Clone, Copy, Default)] struct FcParents<'parent, 'pass> { block: Option<&'parent BlockFormattingContext<'pass>>, @@ -2991,7 +3013,6 @@ enum FcImpl<'pass> { ReplacedWithChildren, InternalReplaced, InternalDummy, - AbsposReplay, } pub(crate) struct FormattingContextInstance<'pass> { @@ -3111,15 +3132,7 @@ fn create_formatting_context<'pass>( ) -> Box> { assert!(!box_.is_invalid()); - let frame = FcFrame { - state, - box_, - layout_mode, - callbacks, - should_collect_devtools_layout_data, - automatic_content_inline_size: CssPixels::default(), - automatic_content_block_size: CssPixels::default(), - }; + let frame = FcFrame::new(state, box_, layout_mode, callbacks, should_collect_devtools_layout_data); let implementation = match fc_type { FfiFormattingContextType::Block => FcImpl::Block(Box::new( BlockFormattingContext::new(state, box_, layout_mode, callbacks), @@ -3147,7 +3160,6 @@ fn create_formatting_context<'pass>( FfiFormattingContextType::ReplacedWithChildren => FcImpl::ReplacedWithChildren, FfiFormattingContextType::InternalReplaced => FcImpl::InternalReplaced, FfiFormattingContextType::InternalDummy => FcImpl::InternalDummy, - FfiFormattingContextType::AbsposReplay => FcImpl::AbsposReplay, FfiFormattingContextType::Inline => panic!("no Rust implementation for inline formatting contexts"), }; Box::new(FormattingContextInstance { frame, implementation }) @@ -3182,21 +3194,17 @@ fn register_table_abspos_descendants(frame: &mut FcFrame, parent: Node) { } } -fn parent_did_dimension(instance: &mut FormattingContextInstance) { +fn complete_formatting_context_after_root_box_has_used_size(instance: &mut FormattingContextInstance) { if let FcImpl::Block(context) = &instance.implementation { context.parent_context_did_dimension_child_root_box(); - // The table formatting context handles cell abspos layout after vertical alignment. - if instance.callbacks.node_data(instance.box_).table_display != crate::layout::node_data::FfiTableDisplay::TableCell { - let box_ = instance.box_; - layout_children_for_instance(instance, box_); - } - return; } - if instance.layout_mode != LayoutMode::Normal { + let registered_abspos_children_could_never_be_laid_out = + instance.layout_mode != LayoutMode::Normal || instance.frame.state.is_measurement(); + if registered_abspos_children_could_never_be_laid_out { return; } match &instance.implementation { - FcImpl::Block(_) => unreachable!("block contexts return before layout-mode dispatch"), + FcImpl::Block(_) => {} FcImpl::Table(_) => { let box_ = instance.frame.box_; register_table_abspos_descendants(&mut instance.frame, box_); @@ -3208,33 +3216,23 @@ fn parent_did_dimension(instance: &mut FormattingContextInstance) { context.parent_did_dimension(); } FcImpl::Svg(_) | FcImpl::ReplacedWithChildren => {} - FcImpl::InternalReplaced | FcImpl::InternalDummy => { - return; - } - FcImpl::AbsposReplay => panic!("abspos replay has no parent-dimension step"), + FcImpl::InternalReplaced | FcImpl::InternalDummy => return, } - if instance.callbacks.node_data(instance.box_).table_display != crate::layout::node_data::FfiTableDisplay::TableCell { - let box_ = instance.box_; - layout_children_for_instance(instance, box_); + let box_ = instance.box_; + if instance.frame.state.abspos_layout_pass_is_active() { + layout_contained_abspos_children(&mut instance.frame); + } else { + instance.frame.state.enqueue_for_abspos_layout_pass(box_); } } impl Drop for FormattingContextInstance<'_> { fn drop(&mut self) { - let FcImpl::Block(context) = &self.implementation else { - return; - }; - if context.was_notified_after_parent_dimensioned_root() { - return; - } - // HACK: The parent formatting context never notified us after assigning dimensions to our root box. - // Pretend that it did anyway, to make sure absolutely positioned children get laid out. - // FIXME: Get rid of this hack once parent contexts behave properly. - context.parent_context_did_dimension_child_root_box(); - // The table formatting context handles cell abspos layout after vertical alignment. - if self.callbacks.node_data(self.box_).table_display != crate::layout::node_data::FfiTableDisplay::TableCell { - let box_ = self.box_; - layout_children_for_instance(self, box_); + if let FcImpl::Block(context) = &self.implementation { + debug_assert!( + context.was_notified_after_parent_dimensioned_root(), + "block formatting context dropped without being completed" + ); } } } @@ -3243,7 +3241,6 @@ fn run_formatting_context<'pass>( instance: &mut FormattingContextInstance<'pass>, input: LayoutInput, parent_block: Option<&BlockFormattingContext<'pass>>, - _parent_grid: Option<&GridFormattingContext<'pass>>, ) { let FormattingContextInstance { frame, implementation } = instance; match implementation { @@ -3279,7 +3276,6 @@ fn run_formatting_context<'pass>( run(frame, input); } FcImpl::InternalReplaced | FcImpl::InternalDummy => {} - FcImpl::AbsposReplay => panic!("abspos replay is run through its dedicated entry"), } } @@ -3335,7 +3331,7 @@ pub(crate) fn layout_inside_child<'pass>( frame.should_collect_devtools_layout_data, frame.callbacks, ); - run_formatting_context(&mut context, input, parent_block, parent_grid); + run_formatting_context(&mut context, input, parent_block); ChildLayoutOutcome::Created(PendingChildLayout { context }) } @@ -3430,8 +3426,10 @@ pub unsafe extern "C" fn rust_layout_run_root_layout( should_collect_devtools_layout_data, callbacks, ); - run_formatting_context(&mut context, input, None, None); + run_formatting_context(&mut context, input, None); + complete_formatting_context_after_root_box_has_used_size(&mut context); drop(context); + run_abspos_layout_pass(state_ref, callbacks, should_collect_devtools_layout_data); state.commit_replacing(root, std::ptr::null_mut(), &callbacks, sink); }); } @@ -3489,12 +3487,11 @@ pub unsafe extern "C" fn rust_layout_compute_subtree_layout( false, callbacks, ); - run_formatting_context(&mut context, input, None, None); + run_formatting_context(&mut context, input, None); - // Lay out the subtree root's own absolutely positioned children, like the parent formatting - // context would do after dimensioning the root box during a full layout. - parent_did_dimension(&mut context); + complete_formatting_context_after_root_box_has_used_size(&mut context); drop(context); + run_abspos_layout_pass(state_ref, callbacks, false); state.commit_replacing(root, paintable_to_replace, &callbacks, sink); }); } @@ -3522,17 +3519,9 @@ pub unsafe extern "C" fn rust_layout_replay_saved_abspos_layout( let state_ref = &state; let containing_block = callbacks.containing_block(box_); assert!(!containing_block.is_invalid()); - let mut context = create_formatting_context( - state_ref, - containing_block, - FcParents::default(), - FfiFormattingContextType::AbsposReplay, - LayoutMode::Normal, - false, - callbacks, - ); - replay_for_instance(&mut context, box_); - drop(context); + let mut frame = crate::layout::FcFrame::new(state_ref, containing_block, LayoutMode::Normal, callbacks, false); + AbsposEngine::new(state_ref, callbacks).replay(&mut frame, box_); + run_abspos_layout_pass(state_ref, callbacks, false); state.commit_replacing(box_, paintable_to_replace, &callbacks, sink); }); } diff --git a/Libraries/LibWeb/Rust/src/layout/grid_formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/grid_formatting_context.rs index f12013648ef3c..0775c84f30792 100644 --- a/Libraries/LibWeb/Rust/src/layout/grid_formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/grid_formatting_context.rs @@ -3632,8 +3632,6 @@ impl<'pass> GridFormattingContext<'pass> { crate::layout::compute_inset_native( self.state, self.callbacks, - self.layout_mode, - self.grid_container, item.box_, area.size.inline_size, area.size.block_size, @@ -3922,6 +3920,18 @@ impl<'pass> GridFormattingContext<'pass> { } child = next; } + self.state + .override_contained_abspos_child_containing_blocks(self.grid_container, |child| { + let mut info = self.abspos_containing_block_info(child); + let grid_area_is_childs_static_position = + self.callbacks.static_position_containing_block(child) == self.grid_container; + if !grid_area_is_childs_static_position { + let (inline_axis_mode, block_axis_mode) = axis_modes(self.style(child)); + info.inline_axis_mode = inline_axis_mode; + info.block_axis_mode = block_axis_mode; + } + info + }); } // https://www.w3.org/TR/css-grid-2/#abspos-items diff --git a/Libraries/LibWeb/Rust/src/layout/inline_formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/inline_formatting_context.rs index c82d0c0bbbfd0..4baccabd9edca 100644 --- a/Libraries/LibWeb/Rust/src/layout/inline_formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/inline_formatting_context.rs @@ -649,8 +649,6 @@ impl<'context, 'pass> InlineFormattingContext<'context, 'pass> { crate::layout::compute_inset_native( self.state, self.callbacks, - self.layout_mode, - self.containing_block, node, used.content_inline_size.get(), used.content_block_size.get(), diff --git a/Libraries/LibWeb/Rust/src/layout/layout_state.rs b/Libraries/LibWeb/Rust/src/layout/layout_state.rs index 6dc90c30d6f14..5df963ebf4b84 100644 --- a/Libraries/LibWeb/Rust/src/layout/layout_state.rs +++ b/Libraries/LibWeb/Rust/src/layout/layout_state.rs @@ -227,6 +227,7 @@ pub(crate) struct AbsposLayoutInputs { pub(crate) containing_block_info: AbsposContainingBlockInfo, } + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] #[repr(C)] pub struct FfiTableCellCoordinates { @@ -389,16 +390,11 @@ impl Drop for RetainedLayoutHandle { } } -#[derive(Clone, Copy)] -struct ContainedAbsposChild { - child_box: Node, - static_position_rect: StaticPositionRect, -} - #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) struct PendingAbsposChild { pub(crate) child_box: Node, pub(crate) static_position_rect: StaticPositionRect, + pub(crate) containing_block_info_override: Option, } /// A pass-scoped lens over one node's facts. Pure classification reads the @@ -841,7 +837,9 @@ impl<'pass> NodeFacts<'pass> { pub(crate) struct LayoutState { used_values: PagedStore, - contained_abspos_children: PagedStore>>, + contained_abspos_children: PagedStore>>, + abspos_layout_pass_queue_in_completion_order: RefCell>, + abspos_layout_pass_is_active: Cell, style_decode_values: PagedStore, style_snapshots: PagedStore, replaced_content_facts: PagedStore, @@ -895,6 +893,8 @@ impl LayoutState { Self { used_values: PagedStore::default(), contained_abspos_children: PagedStore::default(), + abspos_layout_pass_queue_in_completion_order: RefCell::new(VecDeque::new()), + abspos_layout_pass_is_active: Cell::new(false), style_decode_values: PagedStore::default(), style_snapshots: PagedStore::default(), replaced_content_facts: PagedStore::default(), @@ -1433,38 +1433,82 @@ impl LayoutState { let slot_index = target_box.slot_index(); let children = self.contained_abspos_children.get(slot_index).unwrap_or_else(|| { self.contained_abspos_children - .allocate(slot_index, RefCell::new(Vec::new())) + .allocate(slot_index, RefCell::new(VecDeque::new())) }); let mut children = children.borrow_mut(); - // Entries are inserted in tree order so consumption follows document order. - let mut insertion_index = children.len(); - for (index, entry) in children.iter().enumerate() { - // Every box is laid out at most once per state, so it can only be registered once. - assert_ne!(entry.child_box, child_box); - if insertion_index == children.len() && callbacks.is_before(child_box, entry.child_box) { - insertion_index = index; + if cfg!(debug_assertions) { + for entry in children.iter() { + // Every box is laid out at most once per state, so it can only be registered once. + assert_ne!(entry.child_box, child_box); } } + // Entries are kept in tree order so consumption follows document order. + let insertion_index = children.partition_point(|entry| callbacks.is_before(entry.child_box, child_box)); children.insert( insertion_index, - ContainedAbsposChild { + PendingAbsposChild { child_box, static_position_rect, + containing_block_info_override: None, }, ); } - pub(crate) fn take_next_contained_abspos_child(&self, target_box: Node) -> Option { - let children = self.contained_abspos_children.get(target_box.slot_index())?; - let mut children = children.borrow_mut(); - if children.is_empty() { - return None; + pub(crate) fn has_contained_abspos_children(&self, target_box: Node) -> bool { + self.contained_abspos_children + .get(target_box.slot_index()) + .is_some_and(|children| !children.borrow().is_empty()) + } + + pub(crate) fn all_registered_contained_abspos_children_are_laid_out(&self) -> bool { + let mut all_laid_out = true; + self.contained_abspos_children.for_each(|children| { + all_laid_out &= children.borrow().is_empty(); + }); + all_laid_out + } + + /// Every completed root enqueues even when its queue is still empty: + /// fixed-position descendants of abspos subtrees register against the + /// viewport only while the pass lays those subtrees out. + pub(crate) fn enqueue_for_abspos_layout_pass(&self, target_box: Node) { + self.abspos_layout_pass_queue_in_completion_order + .borrow_mut() + .push_back(target_box); + } + + pub(crate) fn pop_from_abspos_layout_pass_queue(&self) -> Option { + self.abspos_layout_pass_queue_in_completion_order.borrow_mut().pop_front() + } + + pub(crate) fn abspos_layout_pass_is_active(&self) -> bool { + self.abspos_layout_pass_is_active.get() + } + + pub(crate) fn set_abspos_layout_pass_is_active(&self, is_active: bool) { + self.abspos_layout_pass_is_active.set(is_active); + } + + /// By completion time the grid-area geometry is final and every abspos + /// child registering against this containing block has done so. + pub(crate) fn override_contained_abspos_child_containing_blocks( + &self, + target_box: Node, + containing_block_info_for_child: impl Fn(Node) -> AbsposContainingBlockInfo, + ) { + let Some(children) = self.contained_abspos_children.get(target_box.slot_index()) else { + return; + }; + for entry in children.borrow_mut().iter_mut() { + entry.containing_block_info_override = Some(containing_block_info_for_child(entry.child_box)); } - let child = children.remove(0); - Some(PendingAbsposChild { - child_box: child.child_box, - static_position_rect: child.static_position_rect, - }) + } + + pub(crate) fn take_next_contained_abspos_child(&self, target_box: Node) -> Option { + self.contained_abspos_children + .get(target_box.slot_index())? + .borrow_mut() + .pop_front() } fn line_fragment_lookup( diff --git a/Libraries/LibWeb/Rust/src/layout/mod.rs b/Libraries/LibWeb/Rust/src/layout/mod.rs index e47f0a7aa823c..f1fa23a7721a2 100644 --- a/Libraries/LibWeb/Rust/src/layout/mod.rs +++ b/Libraries/LibWeb/Rust/src/layout/mod.rs @@ -56,6 +56,7 @@ use std::cell::RefCell; use std::cell::RefMut; use std::collections::HashMap; use std::collections::HashSet; +use std::collections::VecDeque; use std::ffi::c_void; use std::mem::MaybeUninit; use std::mem::size_of; diff --git a/Libraries/LibWeb/Rust/src/layout/replaced_with_children_formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/replaced_with_children_formatting_context.rs index af22cc4489061..bbdfec383392f 100644 --- a/Libraries/LibWeb/Rust/src/layout/replaced_with_children_formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/replaced_with_children_formatting_context.rs @@ -61,7 +61,6 @@ fn run(frame: &mut FcFrame, layout_input: LayoutInput) { table_grid_min_border_box_block_size: None, }, None, - None, ); frame.automatic_content_inline_size = content_inline_size; @@ -71,5 +70,5 @@ fn run(frame: &mut FcFrame, layout_input: LayoutInput) { crate::layout::place_child(frame.state, &frame.callbacks, wrapper, FfiCssPixelPoint::default()); - crate::layout::parent_did_dimension(&mut bfc); + crate::layout::complete_formatting_context_after_root_box_has_used_size(&mut bfc); } diff --git a/Libraries/LibWeb/Rust/src/layout/svg_formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/svg_formatting_context.rs index 31e04cc02d087..cbdf5c4730dee 100644 --- a/Libraries/LibWeb/Rust/src/layout/svg_formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/svg_formatting_context.rs @@ -695,9 +695,7 @@ impl<'pass> SvgFormattingContext<'pass> { if let Some(clip) = self.first_child_matching(child, |facts| facts.is_clip_box) { self.layout_mask_or_clip(frame, clip); } - // The old SVG formatter's stack-local BFC dies at the end of this - // branch; discard the Rust-owned equivalent at the same point. - child_layout.discard(); + child_layout.finish(); } else if facts.is_graphics_box { self.layout_graphics_element(frame, child, input, parent_svg_transform); } diff --git a/Libraries/LibWeb/Rust/src/layout/table_formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/table_formatting_context.rs index 2102b9afd876f..fd272f4bed064 100644 --- a/Libraries/LibWeb/Rust/src/layout/table_formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/table_formatting_context.rs @@ -2494,16 +2494,6 @@ impl<'pass> TableFormattingContext<'pass> { self.distribute_block_size_to_rows(); self.position_row_boxes(); self.position_cell_boxes(); - for cell in &self.cells { - crate::layout::layout_children_native( - self.state, - self.callbacks, - self.layout_mode, - self.table_box, - frame, - cell.box_, - ); - } table_used.set_content_block_size(self.table_block_size); captions += self.run_caption_layout(frame, parent, CaptionPhase::Bottom, caption_available); // Table captions are positioned between the table margins and its borders (outside the grid box borders) as described in diff --git a/Tests/LibWeb/Layout/expected/grid/abspos-subgrid-child-of-grid-container.txt b/Tests/LibWeb/Layout/expected/grid/abspos-subgrid-child-of-grid-container.txt new file mode 100644 index 0000000000000..da40b0e716872 --- /dev/null +++ b/Tests/LibWeb/Layout/expected/grid/abspos-subgrid-child-of-grid-container.txt @@ -0,0 +1,21 @@ +Viewport <#document> at [0,0] [0+0+0 800 0+0+0] [0+0+0 600 0+0+0] [BFC] children: not-inline + BlockContainer at [0,0] [0+0+0 800 0+0+0] [0+0+0 100 0+0+0] [BFC] children: not-inline + BlockContainer at [0,0] [0+0+0 800 0+0+0] [0+0+0 100 0+0+0] children: not-inline + Box at [0,0] positioned [0+0+0 333 0+0+467] [0+0+0 100 0+0+0] [GFC] children: not-inline + Box at [0,0] positioned [0+0+0 333 0+0+0] [0+0+0 50 0+0+0] [GFC] children: not-inline + BlockContainer at [0,0] [0+0+0 333 0+0+0] [0+0+0 10 0+0+0] [BFC] children: not-inline + BlockContainer at [0,25] [0+0+0 333 0+0+0] [0+0+0 10 0+0+0] [BFC] children: not-inline + BlockContainer <(anonymous)> at [0,100] [0+0+0 800 0+0+0] [0+0+0 0 0+0+0] children: inline + TextNode <#text> (not painted) + +ViewportPaintable (Viewport<#document>) [0,0 800x600] + PaintableWithLines (BlockContainer) [0,0 800x100] + PaintableWithLines (BlockContainer) [0,0 800x100] + Paintable (Box
#outer) [0,0 333x100] + Paintable (Box
#abs) [0,0 333x50] + PaintableWithLines (BlockContainer
#a) [0,0 333x10] + PaintableWithLines (BlockContainer
#b) [0,25 333x10] + PaintableWithLines (BlockContainer(anonymous)) [0,100 800x0] + +SC for Viewport<#document> [0,0 800x600] (z-index: auto) + SC for BlockContainer [0,0 800x100] (z-index: auto) diff --git a/Tests/LibWeb/Layout/expected/svg/foreignObject-flex-with-abspos-child.txt b/Tests/LibWeb/Layout/expected/svg/foreignObject-flex-with-abspos-child.txt new file mode 100644 index 0000000000000..f73fbd86f9250 --- /dev/null +++ b/Tests/LibWeb/Layout/expected/svg/foreignObject-flex-with-abspos-child.txt @@ -0,0 +1,30 @@ +Viewport <#document> at [0,0] [0+0+0 800 0+0+0] [0+0+0 600 0+0+0] [BFC] children: not-inline + BlockContainer at [0,0] [0+0+0 800 0+0+0] [0+0+0 200 0+0+0] [BFC] children: not-inline + BlockContainer at [0,0] [0+0+0 800 0+0+0] [0+0+0 200 0+0+0] children: not-inline + SVGSVGBox at [0,0] [0+0+0 300 0+0+500] [0+0+0 200 0+0+0] [SVG] children: inline + TextNode <#text> (not painted) + SVGForeignObjectBox at [20,30] flex-container(row) [0+0+0 200 0+0+0] [0+0+0 120 0+0+0] [FFC] children: not-inline + BlockContainer <(anonymous)> (not painted) [BFC] children: inline + TextNode <#text> (not painted) + BlockContainer at [20,30] flex-item [0+0+0 30 0+0+0] [0+0+0 20 0+0+0] [BFC] children: not-inline + BlockContainer <(anonymous)> (not painted) [BFC] children: inline + TextNode <#text> (not painted) + BlockContainer at [35,40] positioned [0+0+0 50 0+0+0] [0+0+0 40 0+0+0] [BFC] children: not-inline + BlockContainer <(anonymous)> (not painted) [BFC] children: inline + TextNode <#text> (not painted) + TextNode <#text> (not painted) + BlockContainer <(anonymous)> at [0,200] [0+0+0 800 0+0+0] [0+0+0 0 0+0+0] children: inline + TextNode <#text> (not painted) + +ViewportPaintable (Viewport<#document>) [0,0 800x600] + PaintableWithLines (BlockContainer) [0,0 800x200] + PaintableWithLines (BlockContainer) [0,0 800x200] + SVGSVGPaintable (SVGSVGBox) [0,0 300x200] + SVGForeignObjectPaintable (SVGForeignObjectBox) [20,30 200x120] + PaintableWithLines (BlockContainer
#item) [20,30 30x20] + PaintableWithLines (BlockContainer
#abs) [35,40 50x40] + PaintableWithLines (BlockContainer(anonymous)) [0,200 800x0] + +SC for Viewport<#document> [0,0 800x600] (z-index: auto) + SC for BlockContainer [0,0 800x200] (z-index: auto) + SC for SVGForeignObjectBox [20,30 200x120] (z-index: auto) diff --git a/Tests/LibWeb/Layout/input/grid/abspos-subgrid-child-of-grid-container.html b/Tests/LibWeb/Layout/input/grid/abspos-subgrid-child-of-grid-container.html new file mode 100644 index 0000000000000..8fc5fb8588994 --- /dev/null +++ b/Tests/LibWeb/Layout/input/grid/abspos-subgrid-child-of-grid-container.html @@ -0,0 +1,8 @@ + + +
diff --git a/Tests/LibWeb/Layout/input/svg/foreignObject-flex-with-abspos-child.html b/Tests/LibWeb/Layout/input/svg/foreignObject-flex-with-abspos-child.html new file mode 100644 index 0000000000000..26fb0b2e2e3a3 --- /dev/null +++ b/Tests/LibWeb/Layout/input/svg/foreignObject-flex-with-abspos-child.html @@ -0,0 +1,14 @@ + + + + +
+
+
+