azul_layout/solver3/positioning.rs
1//! Final positioning of layout nodes (relative, absolute, and fixed schemes)
2// +spec:positioning:79d47e - Implements relative, absolute, and fixed positioning schemes
3
4use std::collections::BTreeMap;
5
6use azul_core::{
7 dom::{NodeId, NodeType},
8 geom::{LogicalPosition, LogicalRect, LogicalSize},
9 hit_test::ScrollPosition,
10 resources::RendererResources,
11 styled_dom::StyledDom,
12};
13use azul_css::{
14 corety::LayoutDebugMessage,
15 css::CssPropertyValue,
16 props::{
17 basic::pixel::PixelValue,
18 layout::{LayoutPosition, LayoutWritingMode},
19 property::{CssProperty, CssPropertyType},
20 },
21};
22
23use crate::{
24 font_traits::{FontLoaderTrait, ParsedFontTrait},
25 solver3::{
26 fc::{layout_formatting_context, LayoutConstraints, TextAlign},
27 getters::{
28 get_aspect_ratio_property, get_direction_property, get_display_property, get_writing_mode, get_position, MultiValue,
29 get_css_top, get_css_bottom, get_css_left, get_css_right,
30 get_css_height, get_css_width,
31 },
32 layout_tree::LayoutTree,
33 LayoutContext, LayoutError, Result,
34 },
35};
36
37#[derive(Debug, Default)]
38struct PositionOffsets {
39 top: Option<f32>,
40 right: Option<f32>,
41 bottom: Option<f32>,
42 left: Option<f32>,
43}
44
45// +spec:positioning:94ef0f - position property: static|relative|absolute|sticky|fixed, initial static, applies to all elements except table-column-group/table-column
46/// Looks up the `position` property using the compact-cache-aware getter.
47// +spec:positioning:ba937d - positioned elements have position != static
48pub fn get_position_type(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> LayoutPosition {
49 let Some(id) = dom_id else {
50 return LayoutPosition::Static;
51 };
52 let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
53 get_position(styled_dom, id, node_state).unwrap_or_default()
54}
55
56// +spec:positioning:bda1d5 - resolves inset properties (top/right/bottom/left) as inward offsets per CSS Position 3 §3.1
57// +spec:positioning:bf9168 - resolves inset properties (top/right/bottom/left) to control positioned box location
58// +spec:positioning:f8e0a1 - inset properties (top/right/bottom/left) resolved for positioned elements; auto = unconstrained
59/// Reads and resolves `top`, `right`, `bottom`, `left` properties,
60/// including percentages relative to the containing block's size, and em/rem units.
61// +spec:positioning:7ec143 - top/right/bottom/left offset resolution with percentage against containing block
62fn resolve_position_offsets(
63 styled_dom: &StyledDom,
64 dom_id: Option<NodeId>,
65 cb_size: LogicalSize,
66) -> PositionOffsets {
67 use azul_css::props::basic::pixel::{PhysicalSize, PropertyContext, ResolutionContext};
68
69 use crate::solver3::getters::{
70 get_element_font_size, get_parent_font_size, get_root_font_size,
71 };
72
73 let Some(id) = dom_id else {
74 return PositionOffsets::default();
75 };
76 let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
77
78 // Create resolution context with font sizes and containing block size
79 let element_font_size = get_element_font_size(styled_dom, id, node_state);
80 let parent_font_size = get_parent_font_size(styled_dom, id, node_state);
81 let root_font_size = get_root_font_size(styled_dom, node_state);
82
83 let containing_block_size = PhysicalSize::new(cb_size.width, cb_size.height);
84
85 let resolution_context = ResolutionContext {
86 element_font_size,
87 parent_font_size,
88 root_font_size,
89 containing_block_size,
90 element_size: None, // Not needed for position offsets
91 viewport_size: PhysicalSize::new(0.0, 0.0),
92 };
93
94 let mut offsets = PositionOffsets::default();
95
96 // +spec:containing-block:d4b3b9 - percentage offsets resolve against CB width (left/right) or height (top/bottom)
97 // Resolve offsets using compact-cache-aware getters
98 // top/bottom use Height context (% refers to containing block height)
99 offsets.top = match get_css_top(styled_dom, id, node_state) {
100 MultiValue::Exact(pv) => Some(pv.resolve_with_context(&resolution_context, PropertyContext::Height)),
101 _ => None,
102 };
103
104 offsets.bottom = match get_css_bottom(styled_dom, id, node_state) {
105 MultiValue::Exact(pv) => Some(pv.resolve_with_context(&resolution_context, PropertyContext::Height)),
106 _ => None,
107 };
108
109 // left/right use Width context (% refers to containing block width)
110 offsets.left = match get_css_left(styled_dom, id, node_state) {
111 MultiValue::Exact(pv) => Some(pv.resolve_with_context(&resolution_context, PropertyContext::Width)),
112 _ => None,
113 };
114
115 offsets.right = match get_css_right(styled_dom, id, node_state) {
116 MultiValue::Exact(pv) => Some(pv.resolve_with_context(&resolution_context, PropertyContext::Width)),
117 _ => None,
118 };
119
120 offsets
121}
122
123// +spec:block-formatting-context:f5f992 - Out-of-flow: floated or absolutely positioned boxes laid out outside normal flow
124// +spec:positioning:bb19f8 - absolute/fixed positioning: out-of-flow, positioned relative to containing block/viewport
125/// After the main layout pass, this function iterates through the tree and correctly
126/// calculates the final positions of out-of-flow elements (`absolute`, `fixed`).
127// +spec:positioning:5bfef3 - abspos elements use static position for auto offsets, resolve against nearest positioned ancestor CB
128// +spec:positioning:7fff75 - Absolute positioning: removed from flow, offset relative to containing block, establishes new CB
129// +spec:positioning:839cbb - absolute elements positioned/sized solely relative to their containing block, modified by inset properties
130// +spec:positioning:898590 - absolute positioning takes elements out of flow and positions them relative to containing block
131// +spec:positioning:c37c1b - abspos boxes laid out in containing block after its final size is determined
132// +spec:positioning:cbe481 - absolute positioning removes elements from flow and positions them relative to containing block
133// +spec:positioning:ebff77 - absolute positioning layout model (replaces old §6 abspos model)
134// +spec:positioning:3b3ba4 - Absolute positioning: box offset from containing block, removed from normal flow; fixed positioning: CB = viewport
135pub fn position_out_of_flow_elements<T: ParsedFontTrait>(
136 ctx: &mut LayoutContext<'_, T>,
137 tree: &mut LayoutTree,
138 calculated_positions: &mut super::PositionVec,
139 viewport: LogicalRect,
140) -> Result<()> {
141 for node_index in 0..tree.nodes.len() {
142 let node = &tree.nodes[node_index];
143 let dom_id = match node.dom_node_id {
144 Some(id) => id,
145 None => continue,
146 };
147
148 let position_type = get_position_type(ctx.styled_dom, Some(dom_id));
149
150 // +spec:positioning:1d87f6 - Fixed/absolute positioning schemes with box offset resolution (top/right/bottom/left)
151 // +spec:positioning:8bde1d - absolute: out of flow, positioned by containing block
152 // +spec:positioning:c11be9 - absolute positioning: effect of box offsets depends on which properties are auto (non-replaced) or intrinsic dimensions (replaced)
153 // +spec:positioning:9020aa - "absolutely positioned" means position:absolute or position:fixed
154 if position_type == LayoutPosition::Absolute || position_type == LayoutPosition::Fixed {
155 // is a grid container have their CB determined by grid-placement properties;
156 // Taffy already handles this during grid layout, so skip re-positioning here.
157 // Same applies to flex containers (Flexbox §4.1).
158 {
159 use azul_core::dom::FormattingContext;
160 let parent_is_flex_or_grid = node.parent.and_then(|p| tree.get(p)).map_or(false, |pn| {
161 matches!(pn.formatting_context, FormattingContext::Flex | FormattingContext::Grid)
162 });
163 if parent_is_flex_or_grid {
164 continue;
165 }
166 }
167
168 // Get parent info before any mutable borrows
169 let parent_info: Option<(usize, LogicalPosition, f32, f32, f32, f32)> = {
170 let node = &tree.nodes[node_index];
171 node.parent.and_then(|parent_idx| {
172 let parent_node = tree.get(parent_idx)?;
173 let parent_dom_id = parent_node.dom_node_id?;
174 let parent_position = get_position_type(ctx.styled_dom, Some(parent_dom_id));
175 if parent_position == LayoutPosition::Absolute
176 || parent_position == LayoutPosition::Fixed
177 {
178 calculated_positions.get(parent_idx).map(|parent_pos| {
179 let pbp = parent_node.box_props.unpack();
180 (
181 parent_idx,
182 *parent_pos,
183 pbp.border.left,
184 pbp.border.top,
185 pbp.padding.left,
186 pbp.padding.top,
187 )
188 })
189 } else {
190 None
191 }
192 })
193 };
194
195 // +spec:containing-block:17a946 - fixed boxes use viewport as containing block
196 // +spec:containing-block:83a32a - fixed positioning: containing block is viewport; absolute: nearest positioned ancestor or initial CB
197 // +spec:containing-block:9b617d - fixed elements use viewport (initial fixed containing block)
198 // +spec:containing-block:899e47 - fixed elements use viewport (initial fixed containing block)
199 // +spec:containing-block:faa9a3 - fixed positioning falls back to initial containing block (viewport) when no ancestor establishes one
200 // +spec:containing-block:faa9a3 - fixed positioning CB falls back to initial containing block (viewport) when no ancestor establishes one
201 // +spec:positioning:067eab - CB for fixed = viewport, for absolute = nearest positioned ancestor
202 // +spec:positioning:067eab - fixed CB is viewport; absolute CB is nearest positioned ancestor's padding-box
203 // +spec:positioning:9777da - fixed positioning uses viewport as containing block
204 // +spec:positioning:9777da - Fixed positioning uses viewport as containing block
205 // +spec:positioning:9ccf9a - fixed-position CB is viewport (transform/will-change/contain could override, not yet implemented)
206 // +spec:positioning:a68970 - fixed positioning uses viewport as containing block
207 // +spec:positioning:8fff44 - fixed: same as absolute but positioned relative to viewport
208 // +spec:positioning:744713 - fixed position uses viewport as containing block
209 // +spec:positioning:f0ad47 - fixed elements use viewport as containing block; content outside viewport cannot be scrolled to
210 // +spec:containing-block:df8387 - fixed positioning: containing block is the viewport
211 let containing_block_rect = if position_type == LayoutPosition::Fixed {
212 viewport
213 } else {
214 find_absolute_containing_block_rect(
215 tree,
216 node_index,
217 ctx.styled_dom,
218 calculated_positions,
219 viewport,
220 )?
221 };
222
223 // Get node again after containing block calculation
224 let node = &tree.nodes[node_index];
225
226 // Calculate used size for out-of-flow elements (they don't get sized during normal
227 // layout)
228 let element_size = if let Some(size) = node.used_size {
229 size
230 } else {
231 // Element hasn't been sized yet - calculate it now using containing block
232 let intrinsic = tree.warm(node_index).and_then(|w| w.intrinsic_sizes).unwrap_or_default();
233 let size = crate::solver3::sizing::calculate_used_size_for_node(
234 ctx.styled_dom,
235 Some(dom_id),
236 containing_block_rect.size,
237 intrinsic,
238 &node.box_props.unpack(),
239 ctx.viewport_size,
240 )?;
241
242 // Store the calculated size in the tree node
243 if let Some(node_mut) = tree.get_mut(node_index) {
244 node_mut.used_size = Some(size);
245 }
246
247 size
248 };
249
250 // +spec:positioning:dc23fa - sizing/positioning into inset-modified containing block (§4)
251 // +spec:positioning:623e45 - inset properties reduce the containing block into the inset-modified containing block
252 // Resolve offsets using the now-known containing block size.
253 let offsets =
254 resolve_position_offsets(ctx.styled_dom, Some(dom_id), containing_block_rect.size);
255
256 // +spec:box-model:ae3899 - static position is the margin-edge position from normal flow
257 // +spec:positioning:9a90a3 - static position: the position the element would have had in normal flow
258 // +spec:positioning:ca3e89 - static-position rectangle uses block-start inline-start alignment (CSS2.1 hypothetical box)
259 let mut static_pos = calculated_positions
260 .get(node_index)
261 .copied()
262 .unwrap_or_default();
263
264 // Special case: If this is a fixed-position element and it has a positioned
265 // parent, update static_pos to be relative to the parent's final absolute
266 // position (content-box). The initial static_pos from process_out_of_flow_children
267 // may include border/padding offsets, so we must always recalculate here.
268 if position_type == LayoutPosition::Fixed {
269 if let Some((_, parent_pos, border_left, border_top, padding_left, padding_top)) =
270 parent_info
271 {
272 // Add parent's border and padding to get content-box position
273 static_pos = LogicalPosition::new(
274 parent_pos.x + border_left + padding_left,
275 parent_pos.y + border_top + padding_top,
276 );
277 }
278 }
279
280 let mut final_pos = LogicalPosition::zero();
281
282 // +spec:box-model:ea2f43 - top + margin + border + padding + height + bottom = CB height
283 // +spec:box-model:b4f5b3 - vertical constraint equation for abs-pos non-replaced elements
284 // +spec:positioning:16d82c - vertical dimension constraint for abs-positioned non-replaced elements
285 // +spec:positioning:8f474b - §10.6.4 vertical constraint for absolutely positioned non-replaced elements
286 // +spec:positioning:50218d - absolute: top margin edge offset below containing block top edge
287 // top + margin-top + border-top + padding-top + height + padding-bottom +
288 // border-bottom + margin-bottom + bottom = containing block height
289 let node_state = &ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
290
291 // Extract all box_props values upfront to avoid borrow conflicts with tree.get_mut()
292 let (margin_top_val, margin_bottom_val, margin_auto,
293 margin_left_val, margin_right_val, margin_left_auto_flag, margin_right_auto_flag) = {
294 let node = &tree.nodes[node_index];
295 let nbp = node.box_props.unpack();
296 (nbp.margin.top, nbp.margin.bottom,
297 nbp.margin_auto,
298 nbp.margin.left, nbp.margin.right,
299 nbp.margin_auto.left, nbp.margin_auto.right)
300 };
301 // +spec:positioning:d730e5 - CB height is independent of the abspos element, so percentage heights always resolve
302 let cb_height = containing_block_rect.size.height;
303
304 let css_height = get_css_height(ctx.styled_dom, dom_id, node_state);
305 // +spec:replaced-elements:7d8ba8 - §10.6.5: for absolutely positioned replaced
306 // elements, height is determined first (as for inline replaced elements), so treat
307 // it as "not auto" in the constraint equation even if CSS says auto.
308 let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
309 let is_replaced = matches!(node_data.node_type, NodeType::Image(_))
310 || node_data.is_virtual_view_node();
311 let height_is_auto = css_height.is_auto() && !is_replaced;
312 // +spec:overflow:941a06 - resolve auto inset properties: if only one is auto, solved to zero via constraint; if both auto, use static position
313 let top_is_auto = offsets.top.is_none();
314 let bottom_is_auto = offsets.bottom.is_none();
315
316 // element_size is border-box (includes border + padding + content).
317 // The constraint equation is:
318 // top + margin-top + border-box-height + margin-bottom + bottom = CB height
319 // (border-top, padding-top, content-height, padding-bottom, border-bottom
320 // are all inside border-box-height)
321 let mut used_height = element_size.height;
322 // +spec:height-calculation:44939a - set auto values for margin-top/margin-bottom to 0
323 // +spec:height-calculation:2f6e10 - if bottom is auto, replace auto margin-top/margin-bottom with 0
324 let mut used_margin_top = if margin_auto.top { 0.0 } else { margin_top_val };
325 let mut used_margin_bottom = if margin_auto.bottom { 0.0 } else { margin_bottom_val };
326
327 // +spec:box-model:3a9c2a - resolving auto insets: static position fallback when insets are auto
328 // +spec:box-model:bd442c - weaker inset resolves to align margin box with inset-modified CB edge
329 // +spec:height-calculation:93e91c - abs non-replaced height: auto margin centering, single auto margin solve, over-constrained ignore bottom
330 // +spec:positioning:6e7732 - §10.6.4 vertical constraint equation for abspos non-replaced elements
331 // +spec:positioning:b63d0f - absolute positioning with top:auto uses static position (change bars example)
332 // +spec:positioning:da8a0c - resolving auto insets: normal alignment treated as start, so auto insets resolve to static position
333 // +spec:positioning:820b22 - 10.6.4: absolutely positioned non-replaced elements vertical constraint equation and 6 rules
334 if top_is_auto && height_is_auto && bottom_is_auto {
335 // +spec:positioning:08e0ac - absolute element with top:auto uses static position (current line)
336 // +spec:positioning:aab294 - both inset properties auto: resolve to static position
337 // +spec:positioning:d9bb3c - hypothetical position: UA may guess static position rather than fully computing hypothetical box
338 // All three auto: set top to static position, height from content, solve for bottom
339 // +spec:height-calculation:51627d - auto margins to 0, top = static position, height from content (rule 3)
340 // +spec:positioning:460f2f - All three auto: set top to static position, height from content, solve for bottom
341 final_pos.y = static_pos.y;
342 } else if !top_is_auto && !height_is_auto && !bottom_is_auto {
343 // +spec:overflow:fc0c9e - over-constrained abspos: auto margins minimize overflow (CSS2.1 equivalent of Box Alignment 3 safe alignment)
344 // +spec:positioning:88f760 - auto margins of absolutely-positioned boxes (vertical)
345 // None are auto: over-constrained case
346 // +spec:height-calculation:03c071 - none auto: equal auto margins, solve single auto margin, or ignore bottom if over-constrained
347 let top_val = offsets.top.unwrap();
348 let bottom_val = offsets.bottom.unwrap();
349 if margin_auto.top && margin_auto.bottom {
350 // +spec:height-calculation:5112a4 - both margin-top/bottom auto: solve with equal values
351 let available = cb_height - top_val - used_height - bottom_val;
352 let each = available / 2.0;
353 used_margin_top = each;
354 used_margin_bottom = each;
355 } else if margin_auto.top {
356 used_margin_top = cb_height - top_val - used_height - used_margin_bottom - bottom_val;
357 } else if margin_auto.bottom {
358 used_margin_bottom = cb_height - top_val - used_height - used_margin_top - bottom_val;
359 }
360 // else: over-constrained, ignore bottom
361 final_pos.y = containing_block_rect.origin.y + top_val + used_margin_top;
362 } else if top_is_auto && height_is_auto && !bottom_is_auto {
363 // +spec:height-calculation:909b50 - top and height auto, bottom not auto: height from BFC auto heights, solve for top
364 // Rule 1: height from content, auto margins to 0, solve for top
365 let bottom_val = offsets.bottom.unwrap();
366 let top_val = cb_height - used_margin_top - used_height - used_margin_bottom - bottom_val;
367 final_pos.y = containing_block_rect.origin.y + top_val + used_margin_top;
368 } else if top_is_auto && bottom_is_auto && !height_is_auto {
369 // +spec:positioning:64e1ba - top+bottom auto, height not auto: set top to static position, solve for bottom
370 final_pos.y = static_pos.y;
371 } else if height_is_auto && bottom_is_auto && !top_is_auto {
372 // Rule 3: height from content, auto margins to 0, solve for bottom
373 let top_val = offsets.top.unwrap();
374 final_pos.y = containing_block_rect.origin.y + top_val + used_margin_top;
375 } else if top_is_auto && !height_is_auto && !bottom_is_auto {
376 // +spec:height-calculation:33dce8 - top auto, height and bottom not auto: solve for top
377 // Rule 4: auto margins to 0, solve for top
378 let bottom_val = offsets.bottom.unwrap();
379 let top_val = cb_height - used_margin_top - used_height - used_margin_bottom - bottom_val;
380 final_pos.y = containing_block_rect.origin.y + top_val + used_margin_top;
381 } else if height_is_auto && !top_is_auto && !bottom_is_auto {
382 // +spec:intrinsic-sizing:566a43 - abspos auto height with non-auto insets: stretch-fit size
383 // +spec:intrinsic-sizing:c7227f - except: if box has aspect-ratio, ratio-dependent axis uses max-content
384 let has_aspect_ratio = matches!(
385 get_aspect_ratio_property(ctx.styled_dom, dom_id, node_state),
386 MultiValue::Exact(azul_css::props::style::effects::StyleAspectRatio::Ratio(_))
387 );
388 let top_val = offsets.top.unwrap();
389 let bottom_val = offsets.bottom.unwrap();
390 if !has_aspect_ratio {
391 // solve for height from constraint equation (stretch-fit):
392 // height = cb_height - top - margin_top - margin_bottom - bottom
393 // +spec:containing-block:b3f0dd - clamp effective CB size to zero when insets exceed it (weaker inset reduced)
394 used_height = (cb_height - top_val - used_margin_top - used_margin_bottom - bottom_val).max(0.0);
395 }
396 // else: keep content-based height (max-content) per aspect-ratio exception
397 final_pos.y = containing_block_rect.origin.y + top_val + used_margin_top;
398 // Update the element size with the resolved height
399 if let Some(node_mut) = tree.get_mut(node_index) {
400 if let Some(ref mut size) = node_mut.used_size {
401 size.height = used_height;
402 }
403 }
404 } else if bottom_is_auto && !top_is_auto && !height_is_auto {
405 // Rule 6: auto margins to 0, solve for bottom
406 let top_val = offsets.top.unwrap();
407 final_pos.y = containing_block_rect.origin.y + top_val + used_margin_top;
408 } else {
409 // Fallback to static position
410 final_pos.y = static_pos.y;
411 }
412
413 // +spec:box-model:984243 - horizontal constraint equation for abs-pos non-replaced elements
414 // +spec:positioning:3be194 - position abs replaced element after establishing width
415 // Constraint: left + margin-left + border-left + padding-left + width +
416 // +spec:width-calculation:1661b4 - constraint equation and six rules for abs-pos horizontal (§10.3.7)
417 // left + margin-left + border-left + padding-left + width +
418 // padding-right + border-right + margin-right + right = CB width
419 // Since element_size.width is border-box (border + padding + content),
420 // simplifies to: left + margin-left + border_box_width + margin-right + right = CB width
421 {
422 let margin_left = margin_left_val;
423 let margin_right = margin_right_val;
424 let margin_left_auto = margin_left_auto_flag;
425 let margin_right_auto = margin_right_auto_flag;
426 let cb_width = containing_block_rect.size.width;
427 let border_box_width = element_size.width;
428 let left_val = offsets.left;
429 let right_val = offsets.right;
430 let left_is_auto = left_val.is_none();
431 let right_is_auto = right_val.is_none();
432
433 // Get direction of containing block for over-constrained resolution
434 use azul_css::props::style::StyleDirection;
435 let cb_direction = {
436 let cb_dom_id = if position_type == LayoutPosition::Fixed {
437 None // viewport CB, default LTR
438 } else {
439 let mut parent = tree.nodes[node_index].parent;
440 let mut found = None;
441 while let Some(pidx) = parent {
442 if let Some(pnode) = tree.get(pidx) {
443 if get_position_type(ctx.styled_dom, pnode.dom_node_id).is_positioned() {
444 found = pnode.dom_node_id;
445 break;
446 }
447 parent = pnode.parent;
448 } else {
449 break;
450 }
451 }
452 found
453 };
454 match cb_dom_id {
455 Some(cb_id) => {
456 let cb_ns = &ctx.styled_dom.styled_nodes.as_container()[cb_id].styled_node_state;
457 match get_direction_property(ctx.styled_dom, cb_id, cb_ns) {
458 MultiValue::Exact(v) => v,
459 _ => StyleDirection::Ltr,
460 }
461 }
462 None => StyleDirection::Ltr,
463 }
464 };
465
466 // +spec:replaced-elements:7d8ba8 - §10.3.8: for absolutely positioned replaced elements, width is determined
467 // first (as for inline replaced), so treat as "not auto" in the constraint.
468 let width_is_auto = get_css_width(ctx.styled_dom, dom_id, node_state).is_auto() && !is_replaced;
469
470 if !left_is_auto && !width_is_auto && !right_is_auto {
471 // +spec:positioning:88f760 - auto margins of absolutely-positioned boxes (horizontal)
472 // +spec:width-calculation:942c77 - abs-pos non-replaced width: auto margins, over-constrained resolution
473 // None of left/width/right are auto — solve for margins or handle over-constrained
474 // +spec:width-calculation:dff69d - §10.3.7 abs-pos non-replaced: none auto → equal auto margins, solve single auto margin, or over-constrained
475 let left = left_val.unwrap();
476 let right = right_val.unwrap();
477 let remaining = cb_width - left - border_box_width - right;
478
479 // +spec:writing-modes:9c3b40 - abspos auto margins: if negative remaining in inline axis, start margin=0, end margin gets remainder
480 if margin_left_auto && margin_right_auto {
481 // +spec:positioning:ab47b3 - auto margins can be negative in absolute positioning
482 // Both margins auto: equal values unless negative
483 let each_margin = remaining / 2.0;
484 if each_margin < 0.0 {
485 match cb_direction {
486 StyleDirection::Ltr => {
487 final_pos.x = containing_block_rect.origin.x + left;
488 }
489 StyleDirection::Rtl => {
490 final_pos.x = containing_block_rect.origin.x + left + remaining;
491 }
492 }
493 } else {
494 final_pos.x = containing_block_rect.origin.x + left + each_margin;
495 }
496 } else if margin_left_auto {
497 let solved_margin_left = remaining - margin_right;
498 final_pos.x = containing_block_rect.origin.x + left + solved_margin_left;
499 } else if margin_right_auto {
500 final_pos.x = containing_block_rect.origin.x + left + margin_left;
501 } else {
502 // Over-constrained: ignore right (LTR) or left (RTL)
503 match cb_direction {
504 StyleDirection::Ltr => {
505 final_pos.x = containing_block_rect.origin.x + left + margin_left;
506 }
507 StyleDirection::Rtl => {
508 let solved_left = cb_width - margin_left - border_box_width - margin_right - right;
509 final_pos.x = containing_block_rect.origin.x + solved_left + margin_left;
510 }
511 }
512 }
513 } else {
514 // +spec:overflow:f323cb - auto inset: align margin box to stronger inset edge (may overflow CB)
515 // +spec:width-calculation:bbf97a - set auto margins to 0 for abspos when left/width/right has auto
516 // Set auto margins to 0, apply six rules
517 // +spec:box-model:2da091 - if either inset is auto, auto margins resolve to zero
518 // +spec:intrinsic-sizing:087b57 - abspos auto margins resolve to 0 when any inset is auto
519 // +spec:width-calculation:0c29ce - set auto margins to 0, then apply six rules for abs pos width
520 let m_left = if margin_left_auto { 0.0 } else { margin_left };
521 let m_right = if margin_right_auto { 0.0 } else { margin_right };
522
523 // +spec:width-calculation:2b2852 - all three auto: set auto margins to 0, use static position for left (LTR)
524 // +spec:width-calculation:c120b3 - all three of left/width/right auto: set auto margins to 0, then use direction to pick static position
525 if left_is_auto && width_is_auto && right_is_auto {
526 match cb_direction {
527 StyleDirection::Ltr => {
528 // Set left to static position, apply rule 3 (width from content, solve for right)
529 final_pos.x = static_pos.x;
530 }
531 StyleDirection::Rtl => {
532 // Set right to static position, apply rule 1 (width from content, solve for left)
533 let static_offset = static_pos.x - containing_block_rect.origin.x;
534 let right_static = (cb_width - static_offset - border_box_width).max(0.0);
535 let solved_left = cb_width - m_left - border_box_width - m_right - right_static;
536 final_pos.x = containing_block_rect.origin.x + solved_left + m_left;
537 }
538 }
539 } else if left_is_auto && width_is_auto && !right_is_auto {
540 // left+width auto, right not auto: width from content, solve for left
541 let right = right_val.unwrap();
542 let solved_left = cb_width - m_left - border_box_width - m_right - right;
543 final_pos.x = containing_block_rect.origin.x + solved_left + m_left;
544 } else if left_is_auto && !width_is_auto && right_is_auto {
545 // left+right auto: set left to static position (LTR)
546 final_pos.x = static_pos.x;
547 } else if !left_is_auto && width_is_auto && right_is_auto {
548 // width+right auto: position from left
549 let left = left_val.unwrap();
550 final_pos.x = containing_block_rect.origin.x + left + m_left;
551 } else if left_is_auto && !width_is_auto && !right_is_auto {
552 // left auto: solve for left
553 let right = right_val.unwrap();
554 let solved_left = cb_width - m_left - border_box_width - m_right - right;
555 final_pos.x = containing_block_rect.origin.x + solved_left + m_left;
556 } else if !left_is_auto && width_is_auto && !right_is_auto {
557 // +spec:intrinsic-sizing:566a43 - abspos auto width with non-auto insets: stretch-fit size
558 // +spec:intrinsic-sizing:c7227f - except: if box has aspect-ratio, ratio-dependent axis uses max-content
559 let has_aspect_ratio = matches!(
560 get_aspect_ratio_property(ctx.styled_dom, dom_id, node_state),
561 MultiValue::Exact(azul_css::props::style::effects::StyleAspectRatio::Ratio(_))
562 );
563 let left = left_val.unwrap();
564 let right = right_val.unwrap();
565 if !has_aspect_ratio {
566 // width = cb_width - left - margin_left - margin_right - right
567 let used_width = (cb_width - left - m_left - m_right - right).max(0.0);
568 if let Some(node_mut) = tree.get_mut(node_index) {
569 if let Some(ref mut size) = node_mut.used_size {
570 size.width = used_width;
571 }
572 }
573 }
574 // else: keep content-based width (max-content) per aspect-ratio exception
575 final_pos.x = containing_block_rect.origin.x + left + m_left;
576 } else if !left_is_auto && !width_is_auto && right_is_auto {
577 // right auto: position from left
578 let left = left_val.unwrap();
579 final_pos.x = containing_block_rect.origin.x + left + m_left;
580 } else {
581 final_pos.x = static_pos.x;
582 }
583 }
584 }
585
586 super::pos_set(calculated_positions, node_index, final_pos);
587 }
588 }
589 Ok(())
590}
591
592// +spec:positioning:5b0d7f - relative positioning: offset from normal flow position, siblings unaffected
593// +spec:positioning:8afbe2 - Relative positioning preserves normal flow size and space; only visual offset applied after layout
594// +spec:positioning:3502d5 - relative and absolute positioning supported for combined use
595// +spec:positioning:b22222 - relative positioning: offset from static position, purely visual effect
596// +spec:positioning:b814b6 - relative/absolute/fixed positioning scheme (CSS Positioned Layout Module Level 3)
597/// Final pass to shift relatively positioned elements from their static flow position.
598// +spec:block-formatting-context:60ccf9 - relative positioning shifts inline boxes as a unit after normal flow
599// +spec:display-property:17239f - relative positioning offsets element after normal flow; abspos elements taken out of flow
600// +spec:positioning:cbe066 - relative positioning implementation
601///
602/// Resolves percentage-based offsets for `top`, `left`, etc.
603/// For relatively positioned elements, percentages are
604/// relative to the dimensions of the parent element's content box.
605// +spec:positioning:2d8e15 - relative positioning shifts elements as a unit after normal flow without affecting surrounding content
606pub fn adjust_relative_positions<T: ParsedFontTrait>(
607 ctx: &mut LayoutContext<'_, T>,
608 tree: &LayoutTree,
609 calculated_positions: &mut super::PositionVec,
610 viewport: LogicalRect, // The viewport is needed if the root element is relative.
611) -> Result<()> {
612 // Iterate through all nodes. We need the index to modify the position map.
613 for node_index in 0..tree.nodes.len() {
614 let node = &tree.nodes[node_index];
615 let position_type = get_position_type(ctx.styled_dom, node.dom_node_id);
616
617 // +spec:block-formatting-context:faa1cf - static boxes: top/right/bottom/left do not apply
618 // Early continue for non-relative positioning
619 // +spec:overflow:cfb09a - Sticky positioning uses relative-like offsets, clamped to nearest scrollport at scroll time
620 if position_type != LayoutPosition::Relative && position_type != LayoutPosition::Sticky {
621 continue;
622 }
623
624 // +spec:table-layout:6cb73b - position:relative effect on table elements is undefined; skip them
625 // +spec:table-layout:718f91 - relative positioning on table-row/row-group shifts all contents
626 {
627 use azul_css::props::layout::LayoutDisplay;
628 let display = get_display_property(ctx.styled_dom, node.dom_node_id);
629 if let MultiValue::Exact(d) = display {
630 // +spec:positioning:4614dd - position does not apply to table-column-group or table-column boxes
631 // Table-row and row-group elements DO support relative positioning:
632 // the shift affects all contents including cells originating in the row.
633 // Table-column, table-column-group, table-cell, and table-caption do not.
634 if matches!(
635 d,
636 LayoutDisplay::TableColumnGroup
637 | LayoutDisplay::TableColumn
638 | LayoutDisplay::TableCell
639 | LayoutDisplay::TableCaption
640 ) {
641 continue;
642 }
643 }
644 }
645
646 // Determine the containing block size for resolving percentages.
647 // For `position: relative`, this is the parent's content box size.
648 let containing_block_size = node.parent
649 .and_then(|parent_idx| tree.get(parent_idx))
650 .map(|parent_node| {
651 // Get parent's writing mode to correctly calculate its inner (content) size.
652 let parent_wm = parent_node.dom_node_id
653 .map(|pid| {
654 let ps = &ctx.styled_dom.styled_nodes.as_container()[pid].styled_node_state;
655 get_writing_mode(ctx.styled_dom, pid, ps).unwrap_or_default()
656 })
657 .unwrap_or_default();
658 let parent_used_size = parent_node.used_size.unwrap_or_default();
659 parent_node.box_props.inner_size(parent_used_size, parent_wm)
660 })
661 // The root element is relatively positioned. Its containing block is the viewport.
662 .unwrap_or(viewport.size);
663
664 // +spec:positioning:418c74 - inset percentages resolve against containing block size per axis; auto is unconstrained
665 let offsets =
666 resolve_position_offsets(ctx.styled_dom, node.dom_node_id, containing_block_size);
667
668 // Get a mutable reference to the position and apply the offsets.
669 let Some(current_pos) = calculated_positions.get_mut(node_index) else {
670 continue;
671 };
672
673 let initial_pos = *current_pos;
674
675 // +spec:positioning:5eb813 - relative positioning offsets contents from normal flow position
676 // +spec:positioning:a2e5f1 - relative positioning shifts element from static position (vs absolute/float)
677 // top/bottom/left/right offsets are applied relative to the static position.
678 let mut delta_x = 0.0;
679 let mut delta_y = 0.0;
680
681 // +spec:positioning:218b50 - Relative positioning: top=-bottom, left=-right, direction-dependent resolution, top wins over bottom
682 // According to CSS 2.1 Section 9.4.3:
683 // - For `top` and `bottom`: if both are specified, `top` wins and `bottom` is ignored
684 // - For `left` and `right`: depends on direction (ltr/rtl)
685 // - In LTR: if both specified, `left` wins and `right` is ignored
686 // - In RTL: if both specified, `right` wins and `left` is ignored
687
688 // +spec:overflow:53dffd - both left/right auto → used values are 0, boxes stay in original position
689 // +spec:positioning:5a099e - negative offsets can cause overlapping (no clamping applied)
690 // +spec:positioning:d189de - bottom offset for relative positioning is with respect to the box's own bottom edge
691 // +spec:positioning:d80f47 - opposing inset values are negations: top wins over bottom, left/right per direction
692 // +spec:positioning:ecc27c - relative positioning: left/right move box horizontally without changing size, left = -right
693 // +spec:positioning:50218d - relative: offset from static position (top edges of box itself)
694 // both auto → 0; one auto → negative of other; neither auto → bottom ignored (top wins)
695 // +spec:positioning:ac768b - relative positioning: both auto→0, one auto→neg of other, neither→top wins; direction-aware left/right
696 // +spec:positioning:e3727e - top/bottom: both auto→0, one auto→negative of other, neither auto→bottom ignored
697 // Vertical positioning: `top` takes precedence over `bottom`
698 if let Some(top) = offsets.top {
699 delta_y = top;
700 } else if let Some(bottom) = offsets.bottom {
701 delta_y = -bottom;
702 }
703
704 // +spec:positioning:1732e8 - left/right for relatively positioned elements determined by 9.4.3 rules
705 // Spec: "If the 'direction' property of the containing block is 'ltr', the value of 'left' wins"
706 // Get the direction of the containing block (parent), not the element itself
707 use azul_css::props::style::StyleDirection;
708 let cb_direction = node.parent
709 .and_then(|parent_idx| tree.get(parent_idx))
710 .and_then(|parent_node| {
711 let parent_dom_id = parent_node.dom_node_id?;
712 let parent_state =
713 &ctx.styled_dom.styled_nodes.as_container()[parent_dom_id].styled_node_state;
714 match get_direction_property(ctx.styled_dom, parent_dom_id, parent_state) {
715 MultiValue::Exact(v) => Some(v),
716 _ => None,
717 }
718 })
719 .unwrap_or(StyleDirection::Ltr);
720 // +spec:containing-block:6d4fb1 - over-constrained relative positioning: ltr→left wins, rtl→right wins
721 match cb_direction {
722 StyleDirection::Ltr => {
723 if let Some(left) = offsets.left {
724 delta_x = left;
725 } else if let Some(right) = offsets.right {
726 // +spec:overflow:fb426c - left auto: used value is minus the value of right
727 delta_x = -right;
728 }
729 }
730 StyleDirection::Rtl => {
731 if let Some(right) = offsets.right {
732 delta_x = -right;
733 } else if let Some(left) = offsets.left {
734 delta_x = left;
735 }
736 }
737 }
738
739 // +spec:overflow:f1e1ce - relative positioning may cause overflow:auto/scroll boxes to need scrollbars
740 // Only apply the shift if there is a non-zero delta.
741 if delta_x != 0.0 || delta_y != 0.0 {
742 current_pos.x += delta_x;
743 current_pos.y += delta_y;
744
745 ctx.debug_log(&format!(
746 "Adjusted relative element #{} from {:?} to {:?} (delta: {}, {})",
747 node_index, initial_pos, *current_pos, delta_x, delta_y
748 ));
749
750 // +spec:table-layout:ec2600 - For table-row-group, table-header-group, table-footer-group, or table-row,
751 // the relative shift affects all contents of the box including table cells.
752 // Propagate the delta to all descendant nodes.
753 {
754 use azul_css::props::layout::LayoutDisplay;
755 let display = get_display_property(ctx.styled_dom, node.dom_node_id);
756 let is_table_row_like = matches!(
757 display,
758 MultiValue::Exact(
759 LayoutDisplay::TableRowGroup
760 | LayoutDisplay::TableHeaderGroup
761 | LayoutDisplay::TableFooterGroup
762 | LayoutDisplay::TableRow
763 )
764 );
765 if is_table_row_like {
766 // Shift all children (and their descendants) by the same delta
767 let mut stack = tree.children(node_index).to_vec();
768 while let Some(child_idx) = stack.pop() {
769 if let Some(child_pos) = calculated_positions.get_mut(child_idx) {
770 child_pos.x += delta_x;
771 child_pos.y += delta_y;
772 }
773 stack.extend_from_slice(tree.children(child_idx));
774 }
775 }
776 }
777 }
778 }
779 Ok(())
780}
781
782/// Sticky positioning constraints computed at layout time.
783/// At scroll time, the sticky box's position is clamped so that
784/// it remains within the sticky view rectangle (scrollport inset by these values).
785// +spec:overflow:bac4e5 - sticky view rectangle from inset properties relative to nearest scrollport
786#[derive(Debug, Clone)]
787pub struct StickyConstraints {
788 /// Inset from the top edge of the nearest scrollport (0 if auto).
789 pub top_inset: f32,
790 /// Inset from the right edge of the nearest scrollport (0 if auto).
791 pub right_inset: f32,
792 /// Inset from the bottom edge of the nearest scrollport (0 if auto).
793 pub bottom_inset: f32,
794 /// Inset from the left edge of the nearest scrollport (0 if auto).
795 pub left_inset: f32,
796 /// Normal-flow position of the sticky element (border-box origin).
797 pub normal_flow_position: LogicalPosition,
798 /// Border-box size of the sticky element.
799 pub border_box_size: LogicalSize,
800 /// The scrollport rect (content-box of nearest scroll container).
801 pub scrollport: LogicalRect,
802}
803
804/// Finds the nearest scrollport (ancestor with overflow: scroll or auto) for a node.
805/// Returns the content-box rect of the scrollport, or the viewport if none found.
806fn find_nearest_scrollport(
807 tree: &LayoutTree,
808 node_index: usize,
809 styled_dom: &StyledDom,
810 calculated_positions: &super::PositionVec,
811 viewport: LogicalRect,
812) -> LogicalRect {
813 use crate::solver3::getters::{get_overflow_x, get_overflow_y};
814 use azul_css::props::layout::LayoutOverflow;
815
816 let mut current_parent_idx = tree.get(node_index).and_then(|n| n.parent);
817
818 while let Some(parent_index) = current_parent_idx {
819 let parent_node = match tree.get(parent_index) {
820 Some(n) => n,
821 None => break,
822 };
823 let parent_dom_id = match parent_node.dom_node_id {
824 Some(id) => id,
825 None => {
826 current_parent_idx = parent_node.parent;
827 continue;
828 }
829 };
830
831 let node_state = &styled_dom.styled_nodes.as_container()[parent_dom_id].styled_node_state;
832 let ox = get_overflow_x(styled_dom, parent_dom_id, node_state);
833 let oy = get_overflow_y(styled_dom, parent_dom_id, node_state);
834
835 let is_scrollport = matches!(
836 ox,
837 MultiValue::Exact(LayoutOverflow::Scroll | LayoutOverflow::Auto)
838 ) || matches!(
839 oy,
840 MultiValue::Exact(LayoutOverflow::Scroll | LayoutOverflow::Auto)
841 );
842
843 if is_scrollport {
844 let margin_box_pos = calculated_positions
845 .get(parent_index)
846 .copied()
847 .unwrap_or_default();
848 let border_box_size = parent_node.used_size.unwrap_or_default();
849
850 // Content-box = margin-box pos + border + padding, size - border - padding
851 let pbp = parent_node.box_props.unpack();
852 let content_pos = LogicalPosition::new(
853 margin_box_pos.x
854 + pbp.border.left
855 + pbp.padding.left,
856 margin_box_pos.y
857 + pbp.border.top
858 + pbp.padding.top,
859 );
860 let content_size = LogicalSize::new(
861 (border_box_size.width
862 - pbp.border.left
863 - pbp.border.right
864 - pbp.padding.left
865 - pbp.padding.right)
866 .max(0.0),
867 (border_box_size.height
868 - pbp.border.top
869 - pbp.border.bottom
870 - pbp.padding.top
871 - pbp.padding.bottom)
872 .max(0.0),
873 );
874 return LogicalRect::new(content_pos, content_size);
875 }
876
877 current_parent_idx = parent_node.parent;
878 }
879
880 viewport
881}
882
883/// Find the scroll offset of the nearest scroll container ancestor.
884/// Returns the scroll offset as a LogicalPosition (how far the content has scrolled).
885fn find_nearest_scroll_offset(
886 tree: &LayoutTree,
887 node_index: usize,
888 scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
889) -> LogicalPosition {
890 let mut parent = tree.get(node_index).and_then(|n| n.parent);
891 while let Some(pidx) = parent {
892 if let Some(pnode) = tree.get(pidx) {
893 if let Some(dom_id) = pnode.dom_node_id {
894 if let Some(scroll_pos) = scroll_offsets.get(&dom_id) {
895 let offset_x = scroll_pos.children_rect.origin.x - scroll_pos.parent_rect.origin.x;
896 let offset_y = scroll_pos.children_rect.origin.y - scroll_pos.parent_rect.origin.y;
897 return LogicalPosition::new(offset_x, offset_y);
898 }
899 }
900 parent = pnode.parent;
901 } else {
902 break;
903 }
904 }
905 LogicalPosition::zero()
906}
907
908/// Adjusts positions of sticky-positioned elements based on scroll offset.
909///
910/// Sticky positioning works like relative positioning, but the element's position
911/// is constrained by its inset properties (top/right/bottom/left) relative to the
912/// nearest scrollport (scroll container ancestor). The margin box is further
913/// constrained to remain within the containing block.
914///
915/// +spec:position-sticky:9449f1 - for sticky positioning, insets represent offsets from scrollport edge
916/// +spec:position-sticky:75412d - multiple sticky boxes in same container offset independently
917/// +spec:box-model:af9af8 - sticky positioning: shift element to stay within sticky view rectangle, margin box constrained to containing block
918/// +spec:overflow:bac4e5 - compute sticky view rectangle, clamp end-edge insets to border box size
919pub fn adjust_sticky_positions<T: ParsedFontTrait>(
920 ctx: &mut LayoutContext<'_, T>,
921 tree: &LayoutTree,
922 calculated_positions: &mut super::PositionVec,
923 scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
924 viewport: LogicalRect,
925) -> Result<()> {
926 for node_index in 0..tree.nodes.len() {
927 let node = &tree.nodes[node_index];
928 let position_type = get_position_type(ctx.styled_dom, node.dom_node_id);
929
930 if position_type != LayoutPosition::Sticky {
931 continue;
932 }
933
934 let dom_id = match node.dom_node_id {
935 Some(id) => id,
936 None => continue,
937 };
938
939 // Find the nearest scrollport for this sticky element
940 let scrollport = find_nearest_scrollport(
941 tree,
942 node_index,
943 ctx.styled_dom,
944 calculated_positions,
945 viewport,
946 );
947
948 // The containing block for percentage resolution is the parent's content box
949 let containing_block = node.parent
950 .and_then(|parent_idx| {
951 let parent_node = tree.get(parent_idx)?;
952 let parent_pos = calculated_positions.get(parent_idx).copied().unwrap_or_default();
953 let parent_size = parent_node.used_size.unwrap_or_default();
954 let parent_wm = parent_node.dom_node_id
955 .map(|pid| {
956 let ps = &ctx.styled_dom.styled_nodes.as_container()[pid].styled_node_state;
957 get_writing_mode(ctx.styled_dom, pid, ps).unwrap_or_default()
958 })
959 .unwrap_or_default();
960 let pbp = parent_node.box_props.unpack();
961 let content_size = pbp.inner_size(parent_size, parent_wm);
962 let content_origin = LogicalPosition::new(
963 parent_pos.x + pbp.border.left + pbp.padding.left,
964 parent_pos.y + pbp.border.top + pbp.padding.top,
965 );
966 Some(LogicalRect::new(content_origin, content_size))
967 })
968 .unwrap_or(viewport);
969
970 // Resolve inset properties (top, right, bottom, left)
971 let offsets = resolve_position_offsets(ctx.styled_dom, Some(dom_id), scrollport.size);
972
973 // Get the scroll offset from the nearest scroll container
974 let scroll_offset = find_nearest_scroll_offset(tree, node_index, scroll_offsets);
975
976 let Some(current_pos) = calculated_positions.get_mut(node_index) else {
977 continue;
978 };
979
980 let static_pos = *current_pos;
981 let element_size = node.used_size.unwrap_or_default();
982 let nbp = node.box_props.unpack();
983 let margin = &nbp.margin;
984
985 let mut shift_x = 0.0f32;
986 let mut shift_y = 0.0f32;
987
988 // For each side: if inset is not auto, clamp the border edge to stay
989 // within the sticky view rectangle (scrollport inset by the specified amount).
990 // The scroll offset shifts the effective scrollport position.
991 if let Some(top_inset) = offsets.top {
992 let sticky_edge = scrollport.origin.y + scroll_offset.y + top_inset;
993 let border_top = current_pos.y;
994 if border_top < sticky_edge {
995 shift_y = shift_y.max(sticky_edge - border_top);
996 }
997 }
998
999 if let Some(bottom_inset) = offsets.bottom {
1000 let sticky_edge = scrollport.origin.y + scroll_offset.y + scrollport.size.height - bottom_inset;
1001 let border_bottom = current_pos.y + element_size.height;
1002 if border_bottom > sticky_edge {
1003 shift_y = shift_y.min(sticky_edge - border_bottom);
1004 }
1005 }
1006
1007 if let Some(left_inset) = offsets.left {
1008 let sticky_edge = scrollport.origin.x + scroll_offset.x + left_inset;
1009 let border_left = current_pos.x;
1010 if border_left < sticky_edge {
1011 shift_x = shift_x.max(sticky_edge - border_left);
1012 }
1013 }
1014
1015 if let Some(right_inset) = offsets.right {
1016 let sticky_edge = scrollport.origin.x + scroll_offset.x + scrollport.size.width - right_inset;
1017 let border_right = current_pos.x + element_size.width;
1018 if border_right > sticky_edge {
1019 shift_x = shift_x.min(sticky_edge - border_right);
1020 }
1021 }
1022
1023 // Constrain: the margin box must remain within the containing block
1024 if shift_y != 0.0 {
1025 let margin_box_top = current_pos.y - margin.top + shift_y;
1026 let margin_box_bottom = current_pos.y + element_size.height + margin.bottom + shift_y;
1027 if margin_box_top < containing_block.origin.y {
1028 shift_y += containing_block.origin.y - margin_box_top;
1029 }
1030 let cb_bottom = containing_block.origin.y + containing_block.size.height;
1031 if margin_box_bottom > cb_bottom {
1032 shift_y -= margin_box_bottom - cb_bottom;
1033 }
1034 }
1035
1036 if shift_x != 0.0 {
1037 let margin_box_left = current_pos.x - margin.left + shift_x;
1038 let margin_box_right = current_pos.x + element_size.width + margin.right + shift_x;
1039 if margin_box_left < containing_block.origin.x {
1040 shift_x += containing_block.origin.x - margin_box_left;
1041 }
1042 let cb_right = containing_block.origin.x + containing_block.size.width;
1043 if margin_box_right > cb_right {
1044 shift_x -= margin_box_right - cb_right;
1045 }
1046 }
1047
1048 if shift_x != 0.0 || shift_y != 0.0 {
1049 current_pos.x += shift_x;
1050 current_pos.y += shift_y;
1051
1052 ctx.debug_log(&format!(
1053 "Adjusted sticky element #{} from {:?} to {:?}",
1054 node_index, static_pos, *current_pos
1055 ));
1056 }
1057 }
1058 Ok(())
1059}
1060
1061// +spec:positioning:22f165 - absolute/fixed containing block: nearest positioned ancestor's padding-box, or initial CB
1062/// Helper to find the containing block for an absolutely positioned element.
1063/// CSS 2.1 Section 10.1: The containing block for absolutely positioned elements
1064/// is the padding box of the nearest positioned ancestor.
1065// +spec:containing-block:10af51 - absolutely positioned element's CB is nearest positioned ancestor
1066// +spec:positioning:2d0dbb - containing block for abspos is padding-box of nearest positioned ancestor, or initial CB
1067// +spec:positioning:3ac06c - abspos positioned relative to containing block ignoring fragmentation breaks
1068// +spec:positioning:d7e4b4 - containing block of abspos element is always definite (returns concrete LogicalRect)
1069// +spec:positioning:fc9dba - containing block resolution for absolutely positioned boxes
1070///
1071/// Returns a `LogicalRect` representing the padding-box of the nearest
1072/// positioned ancestor, or the viewport (initial containing block) if none exists.
1073/// This is the unified entry point used by both sizing and positioning phases.
1074// +spec:containing-block:18ae8e - Absolute positioning: abs-pos box establishes new CB for normal flow and abs-pos (but not fixed) descendants
1075// +spec:containing-block:b6cb8b - containing block for abs-pos is nearest positioned ancestor
1076// +spec:display-property:5a39bc - containing block for abspos is nearest positioned ancestor or initial containing block
1077// +spec:positioning:09a0fa - Absolute positioning: CB is padding-box of nearest positioned ancestor
1078// +spec:positioning:467cb1 - Containing block for abs pos = nearest positioned ancestor or initial CB
1079// +spec:positioning:99d0bb - containing block for absolute elements is nearest positioned ancestor
1080// +spec:positioning:92e099 - containing block for abs pos is nearest positioned ancestor or initial CB
1081// +spec:positioning:f57523 - containing block of abspos element is always definite (returns concrete LogicalRect)
1082// +spec:width-calculation:bf1aa6 - abspos CB is nearest positioned ancestor, else initial CB
1083// Containing block for absolutely positioned elements is established by
1084// nearest positioned ancestor (relative/absolute/fixed), or initial containing block if none.
1085// +spec:positioning:8f50de - relatively positioned parent serves as containing block for abspos descendants
1086// +spec:containing-block:6bcb0c - containing block is padding edge of nearest positioned ancestor, or initial containing block if none
1087// +spec:containing-block:bf17e5 - containing block for abspos is padding box of nearest positioned ancestor, or initial CB
1088// +spec:containing-block:d0f92d - containing block for positioned box is nearest positioned ancestor, or initial containing block
1089// +spec:containing-block:d7e013 - containing block for positioned box is nearest positioned ancestor or initial CB
1090// +spec:containing-block:05bc0d - positioning an element changes which ancestor establishes the CB for its descendants
1091// +spec:positioning:355ee4 - CB for abspos is padding edge of nearest positioned ancestor, or initial CB
1092// +spec:positioning:383794 - Containing block for abspos is nearest positioned ancestor, or initial containing block if none
1093// +spec:positioning:5b3e43 - Containing block for abs-pos is padding box of nearest positioned ancestor, or initial CB
1094// +spec:positioning:882e67 - containing block for abs pos is nearest positioned ancestor or initial CB
1095// +spec:positioning:292c5c - relative parent serves as containing block for absolute descendants
1096// +spec:positioning:00ce38 - CB for absolute is padding edge of nearest positioned ancestor
1097pub fn find_absolute_containing_block_rect(
1098 tree: &LayoutTree,
1099 node_index: usize,
1100 styled_dom: &StyledDom,
1101 calculated_positions: &super::PositionVec,
1102 viewport: LogicalRect,
1103) -> Result<LogicalRect> {
1104 // +spec:positioning:748d87 - walk up to nearest positioned ancestor for CB
1105 let mut current_parent_idx = tree.get(node_index).and_then(|n| n.parent);
1106
1107 // +spec:positioning:aa361e - values other than static make a box positioned and establish an abspos containing block
1108 while let Some(parent_index) = current_parent_idx {
1109 let parent_node = tree.get(parent_index).ok_or(LayoutError::InvalidTree)?;
1110
1111 if get_position_type(styled_dom, parent_node.dom_node_id).is_positioned() {
1112 // calculated_positions stores margin-box positions
1113 let margin_box_pos = calculated_positions
1114 .get(parent_index)
1115 .copied()
1116 .unwrap_or_default();
1117 // used_size is the border-box size
1118 let border_box_size = parent_node.used_size.unwrap_or_default();
1119
1120 // +spec:containing-block:6bcb0c - containing block formed by padding edge of nearest positioned ancestor
1121 // +spec:positioning:df1921 - abs-pos percentage widths resolve against padding box of containing block
1122 // Calculate padding-box origin (margin-box + border)
1123 let pbp = parent_node.box_props.unpack();
1124 let padding_box_pos = LogicalPosition::new(
1125 margin_box_pos.x + pbp.border.left,
1126 margin_box_pos.y + pbp.border.top,
1127 );
1128
1129 // Calculate padding-box size (border-box - borders)
1130 let padding_box_size = LogicalSize::new(
1131 border_box_size.width
1132 - pbp.border.left
1133 - pbp.border.right,
1134 border_box_size.height
1135 - pbp.border.top
1136 - pbp.border.bottom,
1137 );
1138
1139 return Ok(LogicalRect::new(padding_box_pos, padding_box_size));
1140 }
1141 current_parent_idx = parent_node.parent;
1142 }
1143
1144 // +spec:positioning:3d88c9 - abspos available space is always definite (viewport or positioned ancestor padding box)
1145 // No positioned ancestor found: fall back to initial containing block (viewport)
1146 // +spec:containing-block:141dcc - absolute element with no positioned ancestor uses initial containing block
1147 // +spec:containing-block:657f2f - containing block becomes initial containing block when no positioned ancestors
1148 // +spec:containing-block:7f5090 - if no ancestor establishes one, absolute positioning CB is initial containing block
1149 // +spec:containing-block:7f5090 - fallback to initial containing block when no positioned ancestor
1150 // +spec:containing-block:ad5ebc - no positioned ancestor: containing block becomes the initial containing block
1151 // +spec:display-property:813192 - abspos containing block falls back to initial containing block (viewport) when no positioned ancestor
1152 Ok(viewport)
1153}