1
//! Final positioning of layout nodes (relative, absolute, and fixed schemes)
2
// +spec:positioning:79d47e - Implements relative, absolute, and fixed positioning schemes
3

            
4
use std::collections::BTreeMap;
5

            
6
use azul_core::{
7
    dom::{NodeId, NodeType},
8
    geom::{LogicalPosition, LogicalRect, LogicalSize},
9
    hit_test::ScrollPosition,
10
    resources::RendererResources,
11
    styled_dom::StyledDom,
12
};
13
use 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

            
23
use 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)]
38
struct 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
48
261870
pub fn get_position_type(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> LayoutPosition {
49
261870
    let Some(id) = dom_id else {
50
595
        return LayoutPosition::Static;
51
    };
52
261275
    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
53
261275
    get_position(styled_dom, id, node_state).unwrap_or_default()
54
261870
}
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
62
70
fn resolve_position_offsets(
63
70
    styled_dom: &StyledDom,
64
70
    dom_id: Option<NodeId>,
65
70
    cb_size: LogicalSize,
66
70
) -> 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
70
    let Some(id) = dom_id else {
74
        return PositionOffsets::default();
75
    };
76
70
    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
70
    let element_font_size = get_element_font_size(styled_dom, id, node_state);
80
70
    let parent_font_size = get_parent_font_size(styled_dom, id, node_state);
81
70
    let root_font_size = get_root_font_size(styled_dom, node_state);
82

            
83
70
    let containing_block_size = PhysicalSize::new(cb_size.width, cb_size.height);
84

            
85
70
    let resolution_context = ResolutionContext {
86
70
        element_font_size,
87
70
        parent_font_size,
88
70
        root_font_size,
89
70
        containing_block_size,
90
70
        element_size: None, // Not needed for position offsets
91
70
        viewport_size: PhysicalSize::new(0.0, 0.0),
92
70
    };
93

            
94
70
    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
70
    offsets.top = match get_css_top(styled_dom, id, node_state) {
100
35
        MultiValue::Exact(pv) => Some(pv.resolve_with_context(&resolution_context, PropertyContext::Height)),
101
35
        _ => None,
102
    };
103

            
104
70
    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
70
        _ => None,
107
    };
108

            
109
    // left/right use Width context (% refers to containing block width)
110
70
    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
70
        _ => None,
113
    };
114

            
115
70
    offsets.right = match get_css_right(styled_dom, id, node_state) {
116
35
        MultiValue::Exact(pv) => Some(pv.resolve_with_context(&resolution_context, PropertyContext::Width)),
117
35
        _ => None,
118
    };
119

            
120
70
    offsets
121
70
}
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
135
2785
pub fn position_out_of_flow_elements<T: ParsedFontTrait>(
136
2785
    ctx: &mut LayoutContext<'_, T>,
137
2785
    tree: &mut LayoutTree,
138
2785
    calculated_positions: &mut super::PositionVec,
139
2785
    viewport: LogicalRect,
140
2785
) -> Result<()> {
141
16129
    for node_index in 0..tree.nodes.len() {
142
16129
        let node = &tree.nodes[node_index];
143
16129
        let dom_id = match node.dom_node_id {
144
16090
            Some(id) => id,
145
39
            None => continue,
146
        };
147

            
148
16090
        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
16090
        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
35
                let parent_is_flex_or_grid = node.parent.and_then(|p| tree.get(p)).map_or(false, |pn| {
161
35
                    matches!(pn.formatting_context, FormattingContext::Flex | FormattingContext::Grid)
162
35
                });
163
35
                if parent_is_flex_or_grid {
164
                    continue;
165
35
                }
166
            }
167

            
168
            // Get parent info before any mutable borrows
169
35
            let parent_info: Option<(usize, LogicalPosition, f32, f32, f32, f32)> = {
170
35
                let node = &tree.nodes[node_index];
171
35
                node.parent.and_then(|parent_idx| {
172
35
                    let parent_node = tree.get(parent_idx)?;
173
35
                    let parent_dom_id = parent_node.dom_node_id?;
174
35
                    let parent_position = get_position_type(ctx.styled_dom, Some(parent_dom_id));
175
35
                    if parent_position == LayoutPosition::Absolute
176
35
                        || 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
35
                        None
191
                    }
192
35
                })
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
35
            let containing_block_rect = if position_type == LayoutPosition::Fixed {
212
                viewport
213
            } else {
214
35
                find_absolute_containing_block_rect(
215
35
                    tree,
216
35
                    node_index,
217
35
                    ctx.styled_dom,
218
35
                    calculated_positions,
219
35
                    viewport,
220
                )?
221
            };
222

            
223
            // Get node again after containing block calculation
224
35
            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
35
            let element_size = if let Some(size) = node.used_size {
229
35
                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
35
            let offsets =
254
35
                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
35
            let mut static_pos = calculated_positions
260
35
                .get(node_index)
261
35
                .copied()
262
35
                .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
35
            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
35
            }
279

            
280
35
            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
35
            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
35
            let (margin_top_val, margin_bottom_val, margin_auto,
293
35
                 margin_left_val, margin_right_val, margin_left_auto_flag, margin_right_auto_flag) = {
294
35
                let node = &tree.nodes[node_index];
295
35
                let nbp = node.box_props.unpack();
296
35
                (nbp.margin.top, nbp.margin.bottom,
297
35
                 nbp.margin_auto,
298
35
                 nbp.margin.left, nbp.margin.right,
299
35
                 nbp.margin_auto.left, nbp.margin_auto.right)
300
35
            };
301
            // +spec:positioning:d730e5 - CB height is independent of the abspos element, so percentage heights always resolve
302
35
            let cb_height = containing_block_rect.size.height;
303

            
304
35
            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
35
            let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
309
35
            let is_replaced = matches!(node_data.node_type, NodeType::Image(_))
310
35
                || node_data.is_virtual_view_node();
311
35
            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
35
            let top_is_auto = offsets.top.is_none();
314
35
            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
35
            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
35
            let mut used_margin_top = if margin_auto.top { 0.0 } else { margin_top_val };
325
35
            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
35
            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
35
            } 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
35
            } 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
35
            } 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
35
            } 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
35
            } 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
35
            } 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
35
            } else if bottom_is_auto && !top_is_auto && !height_is_auto {
405
35
                // Rule 6: auto margins to 0, solve for bottom
406
35
                let top_val = offsets.top.unwrap();
407
35
                final_pos.y = containing_block_rect.origin.y + top_val + used_margin_top;
408
35
            } 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
35
                let margin_left = margin_left_val;
423
35
                let margin_right = margin_right_val;
424
35
                let margin_left_auto = margin_left_auto_flag;
425
35
                let margin_right_auto = margin_right_auto_flag;
426
35
                let cb_width = containing_block_rect.size.width;
427
35
                let border_box_width = element_size.width;
428
35
                let left_val = offsets.left;
429
35
                let right_val = offsets.right;
430
35
                let left_is_auto = left_val.is_none();
431
35
                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
35
                let cb_direction = {
436
35
                    let cb_dom_id = if position_type == LayoutPosition::Fixed {
437
                        None // viewport CB, default LTR
438
                    } else {
439
35
                        let mut parent = tree.nodes[node_index].parent;
440
35
                        let mut found = None;
441
35
                        while let Some(pidx) = parent {
442
35
                            if let Some(pnode) = tree.get(pidx) {
443
35
                                if get_position_type(ctx.styled_dom, pnode.dom_node_id).is_positioned() {
444
35
                                    found = pnode.dom_node_id;
445
35
                                    break;
446
                                }
447
                                parent = pnode.parent;
448
                            } else {
449
                                break;
450
                            }
451
                        }
452
35
                        found
453
                    };
454
35
                    match cb_dom_id {
455
35
                        Some(cb_id) => {
456
35
                            let cb_ns = &ctx.styled_dom.styled_nodes.as_container()[cb_id].styled_node_state;
457
35
                            match get_direction_property(ctx.styled_dom, cb_id, cb_ns) {
458
35
                                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
35
                let width_is_auto = get_css_width(ctx.styled_dom, dom_id, node_state).is_auto() && !is_replaced;
469

            
470
35
                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
35
                    let m_left = if margin_left_auto { 0.0 } else { margin_left };
521
35
                    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
35
                    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
35
                    } 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
35
                    } 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
35
                    } 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
35
                    } else if left_is_auto && !width_is_auto && !right_is_auto {
552
35
                        // left auto: solve for left
553
35
                        let right = right_val.unwrap();
554
35
                        let solved_left = cb_width - m_left - border_box_width - m_right - right;
555
35
                        final_pos.x = containing_block_rect.origin.x + solved_left + m_left;
556
35
                    } 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
35
            super::pos_set(calculated_positions, node_index, final_pos);
587
16055
        }
588
    }
589
2785
    Ok(())
590
2785
}
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
606
2785
pub fn adjust_relative_positions<T: ParsedFontTrait>(
607
2785
    ctx: &mut LayoutContext<'_, T>,
608
2785
    tree: &LayoutTree,
609
2785
    calculated_positions: &mut super::PositionVec,
610
2785
    viewport: LogicalRect, // The viewport is needed if the root element is relative.
611
2785
) -> Result<()> {
612
    // Iterate through all nodes. We need the index to modify the position map.
613
16129
    for node_index in 0..tree.nodes.len() {
614
16129
        let node = &tree.nodes[node_index];
615
16129
        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
16129
        if position_type != LayoutPosition::Relative && position_type != LayoutPosition::Sticky {
621
16094
            continue;
622
35
        }
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
35
            let display = get_display_property(ctx.styled_dom, node.dom_node_id);
629
35
            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
35
                if matches!(
635
35
                    d,
636
                    LayoutDisplay::TableColumnGroup
637
                        | LayoutDisplay::TableColumn
638
                        | LayoutDisplay::TableCell
639
                        | LayoutDisplay::TableCaption
640
                ) {
641
                    continue;
642
35
                }
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
35
        let containing_block_size = node.parent
649
35
            .and_then(|parent_idx| tree.get(parent_idx))
650
35
            .map(|parent_node| {
651
                // Get parent's writing mode to correctly calculate its inner (content) size.
652
35
                let parent_wm = parent_node.dom_node_id
653
35
                    .map(|pid| {
654
35
                        let ps = &ctx.styled_dom.styled_nodes.as_container()[pid].styled_node_state;
655
35
                        get_writing_mode(ctx.styled_dom, pid, ps).unwrap_or_default()
656
35
                    })
657
35
                    .unwrap_or_default();
658
35
                let parent_used_size = parent_node.used_size.unwrap_or_default();
659
35
                parent_node.box_props.inner_size(parent_used_size, parent_wm)
660
35
            })
661
            // The root element is relatively positioned. Its containing block is the viewport.
662
35
            .unwrap_or(viewport.size);
663

            
664
        // +spec:positioning:418c74 - inset percentages resolve against containing block size per axis; auto is unconstrained
665
35
        let offsets =
666
35
            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
35
        let Some(current_pos) = calculated_positions.get_mut(node_index) else {
670
            continue;
671
        };
672

            
673
35
        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
35
        let mut delta_x = 0.0;
679
35
        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
35
        if let Some(top) = offsets.top {
699
            delta_y = top;
700
35
        } else if let Some(bottom) = offsets.bottom {
701
            delta_y = -bottom;
702
35
        }
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
35
        let cb_direction = node.parent
709
35
            .and_then(|parent_idx| tree.get(parent_idx))
710
35
            .and_then(|parent_node| {
711
35
                let parent_dom_id = parent_node.dom_node_id?;
712
35
                let parent_state =
713
35
                    &ctx.styled_dom.styled_nodes.as_container()[parent_dom_id].styled_node_state;
714
35
                match get_direction_property(ctx.styled_dom, parent_dom_id, parent_state) {
715
35
                    MultiValue::Exact(v) => Some(v),
716
                    _ => None,
717
                }
718
35
            })
719
35
            .unwrap_or(StyleDirection::Ltr);
720
        // +spec:containing-block:6d4fb1 - over-constrained relative positioning: ltrโ†’left wins, rtlโ†’right wins
721
35
        match cb_direction {
722
            StyleDirection::Ltr => {
723
35
                if let Some(left) = offsets.left {
724
                    delta_x = left;
725
35
                } 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
35
                }
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
35
        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
35
        }
778
    }
779
2785
    Ok(())
780
2785
}
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)]
787
pub 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.
806
fn 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).
885
fn 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
919
2730
pub fn adjust_sticky_positions<T: ParsedFontTrait>(
920
2730
    ctx: &mut LayoutContext<'_, T>,
921
2730
    tree: &LayoutTree,
922
2730
    calculated_positions: &mut super::PositionVec,
923
2730
    scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
924
2730
    viewport: LogicalRect,
925
2730
) -> Result<()> {
926
15855
    for node_index in 0..tree.nodes.len() {
927
15855
        let node = &tree.nodes[node_index];
928
15855
        let position_type = get_position_type(ctx.styled_dom, node.dom_node_id);
929

            
930
15855
        if position_type != LayoutPosition::Sticky {
931
15855
            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 {
            let sticky_edge = scrollport.origin.y + scroll_offset.y + scrollport.size.height - bottom_inset;
            let border_bottom = current_pos.y + element_size.height;
            if border_bottom > sticky_edge {
                shift_y = shift_y.min(sticky_edge - border_bottom);
            }
        }
        if let Some(left_inset) = offsets.left {
            let sticky_edge = scrollport.origin.x + scroll_offset.x + left_inset;
            let border_left = current_pos.x;
            if border_left < sticky_edge {
                shift_x = shift_x.max(sticky_edge - border_left);
            }
        }
        if let Some(right_inset) = offsets.right {
            let sticky_edge = scrollport.origin.x + scroll_offset.x + scrollport.size.width - right_inset;
            let border_right = current_pos.x + element_size.width;
            if border_right > sticky_edge {
                shift_x = shift_x.min(sticky_edge - border_right);
            }
        }
        // Constrain: the margin box must remain within the containing block
        if shift_y != 0.0 {
            let margin_box_top = current_pos.y - margin.top + shift_y;
            let margin_box_bottom = current_pos.y + element_size.height + margin.bottom + shift_y;
            if margin_box_top < containing_block.origin.y {
                shift_y += containing_block.origin.y - margin_box_top;
            }
            let cb_bottom = containing_block.origin.y + containing_block.size.height;
            if margin_box_bottom > cb_bottom {
                shift_y -= margin_box_bottom - cb_bottom;
            }
        }
        if shift_x != 0.0 {
            let margin_box_left = current_pos.x - margin.left + shift_x;
            let margin_box_right = current_pos.x + element_size.width + margin.right + shift_x;
            if margin_box_left < containing_block.origin.x {
                shift_x += containing_block.origin.x - margin_box_left;
            }
            let cb_right = containing_block.origin.x + containing_block.size.width;
            if margin_box_right > cb_right {
                shift_x -= margin_box_right - cb_right;
            }
        }
        if shift_x != 0.0 || shift_y != 0.0 {
            current_pos.x += shift_x;
            current_pos.y += shift_y;
            ctx.debug_log(&format!(
                "Adjusted sticky element #{} from {:?} to {:?}",
                node_index, static_pos, *current_pos
            ));
        }
    }
2730
    Ok(())
2730
}
// +spec:positioning:22f165 - absolute/fixed containing block: nearest positioned ancestor's padding-box, or initial CB
/// Helper to find the containing block for an absolutely positioned element.
/// CSS 2.1 Section 10.1: The containing block for absolutely positioned elements
/// is the padding box of the nearest positioned ancestor.
// +spec:containing-block:10af51 - absolutely positioned element's CB is nearest positioned ancestor
// +spec:positioning:2d0dbb - containing block for abspos is padding-box of nearest positioned ancestor, or initial CB
// +spec:positioning:3ac06c - abspos positioned relative to containing block ignoring fragmentation breaks
// +spec:positioning:d7e4b4 - containing block of abspos element is always definite (returns concrete LogicalRect)
// +spec:positioning:fc9dba - containing block resolution for absolutely positioned boxes
///
/// Returns a `LogicalRect` representing the padding-box of the nearest
/// positioned ancestor, or the viewport (initial containing block) if none exists.
/// This is the unified entry point used by both sizing and positioning phases.
// +spec:containing-block:18ae8e - Absolute positioning: abs-pos box establishes new CB for normal flow and abs-pos (but not fixed) descendants
// +spec:containing-block:b6cb8b - containing block for abs-pos is nearest positioned ancestor
// +spec:display-property:5a39bc - containing block for abspos is nearest positioned ancestor or initial containing block
// +spec:positioning:09a0fa - Absolute positioning: CB is padding-box of nearest positioned ancestor
// +spec:positioning:467cb1 - Containing block for abs pos = nearest positioned ancestor or initial CB
// +spec:positioning:99d0bb - containing block for absolute elements is nearest positioned ancestor
// +spec:positioning:92e099 - containing block for abs pos is nearest positioned ancestor or initial CB
// +spec:positioning:f57523 - containing block of abspos element is always definite (returns concrete LogicalRect)
// +spec:width-calculation:bf1aa6 - abspos CB is nearest positioned ancestor, else initial CB
// Containing block for absolutely positioned elements is established by
// nearest positioned ancestor (relative/absolute/fixed), or initial containing block if none.
// +spec:positioning:8f50de - relatively positioned parent serves as containing block for abspos descendants
// +spec:containing-block:6bcb0c - containing block is padding edge of nearest positioned ancestor, or initial containing block if none
// +spec:containing-block:bf17e5 - containing block for abspos is padding box of nearest positioned ancestor, or initial CB
// +spec:containing-block:d0f92d - containing block for positioned box is nearest positioned ancestor, or initial containing block
// +spec:containing-block:d7e013 - containing block for positioned box is nearest positioned ancestor or initial CB
// +spec:containing-block:05bc0d - positioning an element changes which ancestor establishes the CB for its descendants
// +spec:positioning:355ee4 - CB for abspos is padding edge of nearest positioned ancestor, or initial CB
// +spec:positioning:383794 - Containing block for abspos is nearest positioned ancestor, or initial containing block if none
// +spec:positioning:5b3e43 - Containing block for abs-pos is padding box of nearest positioned ancestor, or initial CB
// +spec:positioning:882e67 - containing block for abs pos is nearest positioned ancestor or initial CB
// +spec:positioning:292c5c - relative parent serves as containing block for absolute descendants
// +spec:positioning:00ce38 - CB for absolute is padding edge of nearest positioned ancestor
35
pub fn find_absolute_containing_block_rect(
35
    tree: &LayoutTree,
35
    node_index: usize,
35
    styled_dom: &StyledDom,
35
    calculated_positions: &super::PositionVec,
35
    viewport: LogicalRect,
35
) -> Result<LogicalRect> {
    // +spec:positioning:748d87 - walk up to nearest positioned ancestor for CB
35
    let mut current_parent_idx = tree.get(node_index).and_then(|n| n.parent);
    // +spec:positioning:aa361e - values other than static make a box positioned and establish an abspos containing block
35
    while let Some(parent_index) = current_parent_idx {
35
        let parent_node = tree.get(parent_index).ok_or(LayoutError::InvalidTree)?;
35
        if get_position_type(styled_dom, parent_node.dom_node_id).is_positioned() {
            // calculated_positions stores margin-box positions
35
            let margin_box_pos = calculated_positions
35
                .get(parent_index)
35
                .copied()
35
                .unwrap_or_default();
            // used_size is the border-box size
35
            let border_box_size = parent_node.used_size.unwrap_or_default();
            // +spec:containing-block:6bcb0c - containing block formed by padding edge of nearest positioned ancestor
            // +spec:positioning:df1921 - abs-pos percentage widths resolve against padding box of containing block
            // Calculate padding-box origin (margin-box + border)
35
            let pbp = parent_node.box_props.unpack();
35
            let padding_box_pos = LogicalPosition::new(
35
                margin_box_pos.x + pbp.border.left,
35
                margin_box_pos.y + pbp.border.top,
            );
            // Calculate padding-box size (border-box - borders)
35
            let padding_box_size = LogicalSize::new(
35
                border_box_size.width
35
                    - pbp.border.left
35
                    - pbp.border.right,
35
                border_box_size.height
35
                    - pbp.border.top
35
                    - pbp.border.bottom,
            );
35
            return Ok(LogicalRect::new(padding_box_pos, padding_box_size));
        }
        current_parent_idx = parent_node.parent;
    }
    // +spec:positioning:3d88c9 - abspos available space is always definite (viewport or positioned ancestor padding box)
    // No positioned ancestor found: fall back to initial containing block (viewport)
    // +spec:containing-block:141dcc - absolute element with no positioned ancestor uses initial containing block
    // +spec:containing-block:657f2f - containing block becomes initial containing block when no positioned ancestors
    // +spec:containing-block:7f5090 - if no ancestor establishes one, absolute positioning CB is initial containing block
    // +spec:containing-block:7f5090 - fallback to initial containing block when no positioned ancestor
    // +spec:containing-block:ad5ebc - no positioned ancestor: containing block becomes the initial containing block
    // +spec:display-property:813192 - abspos containing block falls back to initial containing block (viewport) when no positioned ancestor
    Ok(viewport)
35
}