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, TextLayoutCache},
25
    solver3::{
26
        fc::{layout_formatting_context, FloatingContext, 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
pub(crate) struct PositionOffsets {
39
    pub(crate) top: Option<f32>,
40
    pub(crate) right: Option<f32>,
41
    pub(crate) bottom: Option<f32>,
42
    pub(crate) 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
573276
pub fn get_position_type(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> LayoutPosition {
49
573276
    let Some(id) = dom_id else {
50
968
        return LayoutPosition::Static;
51
    };
52
572308
    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
53
572308
    get_position(styled_dom, id, node_state).unwrap_or_default()
54
573276
}
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
19712
pub(crate) fn resolve_position_offsets(
63
19712
    styled_dom: &StyledDom,
64
19712
    dom_id: Option<NodeId>,
65
19712
    cb_size: LogicalSize,
66
19712
) -> 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
19712
    let Some(id) = dom_id else {
74
        return PositionOffsets::default();
75
    };
76
19712
    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
19712
    let element_font_size = get_element_font_size(styled_dom, id, node_state);
80
19712
    let parent_font_size = get_parent_font_size(styled_dom, id, node_state);
81
19712
    let root_font_size = get_root_font_size(styled_dom, node_state);
82

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

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

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

            
104
19712
    offsets.bottom = match get_css_bottom(styled_dom, id, node_state) {
105
352
        MultiValue::Exact(pv) => Some(pv.resolve_with_context(&resolution_context, PropertyContext::Height)),
106
19360
        _ => None,
107
    };
108

            
109
    // left/right use Width context (% refers to containing block width)
110
19712
    offsets.left = match get_css_left(styled_dom, id, node_state) {
111
19140
        MultiValue::Exact(pv) => Some(pv.resolve_with_context(&resolution_context, PropertyContext::Width)),
112
572
        _ => None,
113
    };
114

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

            
120
19712
    offsets
121
19712
}
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
4723
pub fn position_out_of_flow_elements<T: ParsedFontTrait>(
136
4723
    ctx: &mut LayoutContext<'_, T>,
137
4723
    tree: &mut LayoutTree,
138
4723
    text_cache: &mut TextLayoutCache,
139
4723
    calculated_positions: &mut super::PositionVec,
140
4723
    viewport: LogicalRect,
141
4723
) {
142
    // Returns `()` (not Result<()>): inner fallible calls use skip-on-err (see above), so this fn
143
    // never propagates Err. Avoids the lift-fragile Result<(),LayoutError> Ok-niche read.
144
44785
    for node_index in 0..tree.nodes.len() {
145
44785
        let node = &tree.nodes[node_index];
146
44785
        let dom_id = match node.dom_node_id {
147
44693
            Some(id) => id,
148
92
            None => continue,
149
        };
150

            
151
44693
        let position_type = get_position_type(ctx.styled_dom, Some(dom_id));
152

            
153
        // +spec:positioning:1d87f6 - Fixed/absolute positioning schemes with box offset resolution (top/right/bottom/left)
154
        // +spec:positioning:8bde1d - absolute: out of flow, positioned by containing block
155
        // +spec:positioning:c11be9 - absolute positioning: effect of box offsets depends on which properties are auto (non-replaced) or intrinsic dimensions (replaced)
156
        // +spec:positioning:9020aa - "absolutely positioned" means position:absolute or position:fixed
157
44693
        if position_type == LayoutPosition::Absolute || position_type == LayoutPosition::Fixed {
158
            // is a grid container have their CB determined by grid-placement properties;
159
            // Taffy already handles this during grid layout, so skip re-positioning here.
160
            // Same applies to flex containers (Flexbox ยง4.1).
161
            {
162
                use azul_core::dom::FormattingContext;
163
12762
                let parent_is_flex_or_grid = node.parent.and_then(|p| tree.get(p)).map_or(false, |pn| {
164
12454
                    matches!(pn.formatting_context, FormattingContext::Flex | FormattingContext::Grid)
165
12454
                });
166
12762
                if parent_is_flex_or_grid {
167
                    continue;
168
12762
                }
169
            }
170

            
171
            // Get parent info before any mutable borrows
172
12762
            let parent_info: Option<(usize, LogicalPosition, f32, f32, f32, f32)> = {
173
12762
                let node = &tree.nodes[node_index];
174
12762
                node.parent.and_then(|parent_idx| {
175
12454
                    let parent_node = tree.get(parent_idx)?;
176
12454
                    let parent_dom_id = parent_node.dom_node_id?;
177
12454
                    let parent_position = get_position_type(ctx.styled_dom, Some(parent_dom_id));
178
12454
                    if parent_position == LayoutPosition::Absolute
179
134
                        || parent_position == LayoutPosition::Fixed
180
                    {
181
12320
                        calculated_positions.get(parent_idx).map(|parent_pos| {
182
12320
                            let pbp = parent_node.box_props.unpack();
183
12320
                            (
184
12320
                                parent_idx,
185
12320
                                *parent_pos,
186
12320
                                pbp.border.left,
187
12320
                                pbp.border.top,
188
12320
                                pbp.padding.left,
189
12320
                                pbp.padding.top,
190
12320
                            )
191
12320
                        })
192
                    } else {
193
134
                        None
194
                    }
195
12454
                })
196
            };
197

            
198
            // +spec:containing-block:17a946 - fixed boxes use viewport as containing block
199
            // +spec:containing-block:83a32a - fixed positioning: containing block is viewport; absolute: nearest positioned ancestor or initial CB
200
            // +spec:containing-block:9b617d - fixed elements use viewport (initial fixed containing block)
201
            // +spec:containing-block:899e47 - fixed elements use viewport (initial fixed containing block)
202
            // +spec:containing-block:faa9a3 - fixed positioning falls back to initial containing block (viewport) when no ancestor establishes one
203
            // +spec:containing-block:faa9a3 - fixed positioning CB falls back to initial containing block (viewport) when no ancestor establishes one
204
            // +spec:positioning:067eab - CB for fixed = viewport, for absolute = nearest positioned ancestor
205
            // +spec:positioning:067eab - fixed CB is viewport; absolute CB is nearest positioned ancestor's padding-box
206
            // +spec:positioning:9777da - fixed positioning uses viewport as containing block
207
            // +spec:positioning:9777da - Fixed positioning uses viewport as containing block
208
            // +spec:positioning:9ccf9a - fixed-position CB is viewport (transform/will-change/contain could override, not yet implemented)
209
            // +spec:positioning:a68970 - fixed positioning uses viewport as containing block
210
            // +spec:positioning:8fff44 - fixed: same as absolute but positioned relative to viewport
211
            // +spec:positioning:744713 - fixed position uses viewport as containing block
212
            // +spec:positioning:f0ad47 - fixed elements use viewport as containing block; content outside viewport cannot be scrolled to
213
            // +spec:containing-block:df8387 - fixed positioning: containing block is the viewport
214
12762
            let containing_block_rect = if position_type == LayoutPosition::Fixed {
215
                viewport
216
            } else {
217
                // skip-on-err (was `?`): a CB-resolution failure for one out-of-flow node skips
218
                // that node rather than aborting the whole layout. Lets this fn return `()`
219
                // (no Result<(),LayoutError> Ok-niche read, which the remillโ†’wasm lift mis-lowers).
220
12762
                match find_absolute_containing_block_rect(
221
12762
                    tree,
222
12762
                    node_index,
223
12762
                    ctx.styled_dom,
224
12762
                    calculated_positions,
225
12762
                    viewport,
226
12762
                ) {
227
12762
                    Ok(r) => r,
228
                    Err(_) => continue,
229
                }
230
            };
231

            
232
            // Get node again after containing block calculation
233
12762
            let node = &tree.nodes[node_index];
234

            
235
            // Calculate used size for out-of-flow elements (they don't get sized during normal
236
            // layout)
237
12762
            let element_size = if let Some(size) = node.used_size {
238
12673
                size
239
            } else {
240
                // Element hasn't been sized yet - calculate it now using containing block
241
89
                let intrinsic = tree.warm(node_index).and_then(|w| w.intrinsic_sizes).unwrap_or_default();
242
89
                let size = match crate::solver3::sizing::calculate_used_size_for_node(
243
89
                    ctx.styled_dom,
244
89
                    Some(dom_id),
245
89
                    &containing_block_rect.size,
246
89
                    intrinsic,
247
89
                    &node.box_props.unpack(),
248
89
                    &ctx.viewport_size,
249
89
                ) {
250
89
                    Ok(s) => s,
251
                    Err(_) => continue,
252
                };
253

            
254
                // Store the calculated size in the tree node
255
89
                if let Some(node_mut) = tree.get_mut(node_index) {
256
89
                    node_mut.used_size = Some(size);
257
89
                }
258

            
259
89
                size
260
            };
261

            
262
            // +spec:positioning:dc23fa - sizing/positioning into inset-modified containing block (ยง4)
263
            // +spec:positioning:623e45 - inset properties reduce the containing block into the inset-modified containing block
264
            // Resolve offsets using the now-known containing block size.
265
12762
            let offsets =
266
12762
                resolve_position_offsets(ctx.styled_dom, Some(dom_id), containing_block_rect.size);
267

            
268
            // +spec:box-model:ae3899 - static position is the margin-edge position from normal flow
269
            // +spec:positioning:9a90a3 - static position: the position the element would have had in normal flow
270
            // +spec:positioning:ca3e89 - static-position rectangle uses block-start inline-start alignment (CSS2.1 hypothetical box)
271
12762
            let mut static_pos = calculated_positions
272
12762
                .get(node_index)
273
12762
                .copied()
274
12762
                .unwrap_or_default();
275

            
276
            // Special case: If this is a fixed-position element and it has a positioned
277
            // parent, update static_pos to be relative to the parent's final absolute
278
            // position (content-box). The initial static_pos from process_out_of_flow_children
279
            // may include border/padding offsets, so we must always recalculate here.
280
12762
            if position_type == LayoutPosition::Fixed {
281
                if let Some((_, parent_pos, border_left, border_top, padding_left, padding_top)) =
282
                    parent_info
283
                {
284
                    // Add parent's border and padding to get content-box position
285
                    static_pos = LogicalPosition::new(
286
                        parent_pos.x + border_left + padding_left,
287
                        parent_pos.y + border_top + padding_top,
288
                    );
289
                }
290
12762
            }
291

            
292
12762
            let mut final_pos = LogicalPosition::zero();
293

            
294
            // +spec:box-model:ea2f43 - top + margin + border + padding + height + bottom = CB height
295
            // +spec:box-model:b4f5b3 - vertical constraint equation for abs-pos non-replaced elements
296
            // +spec:positioning:16d82c - vertical dimension constraint for abs-positioned non-replaced elements
297
            // +spec:positioning:8f474b - ยง10.6.4 vertical constraint for absolutely positioned non-replaced elements
298
            // +spec:positioning:50218d - absolute: top margin edge offset below containing block top edge
299
            // top + margin-top + border-top + padding-top + height + padding-bottom +
300
            // border-bottom + margin-bottom + bottom = containing block height
301
12762
            let node_state = &ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
302

            
303
            // Extract all box_props values upfront to avoid borrow conflicts with tree.get_mut()
304
12762
            let (margin_top_val, margin_bottom_val, margin_auto,
305
12762
                 margin_left_val, margin_right_val, margin_left_auto_flag, margin_right_auto_flag) = {
306
12762
                let node = &tree.nodes[node_index];
307
12762
                let nbp = node.box_props.unpack();
308
12762
                (nbp.margin.top, nbp.margin.bottom,
309
12762
                 nbp.margin_auto,
310
12762
                 nbp.margin.left, nbp.margin.right,
311
12762
                 nbp.margin_auto.left, nbp.margin_auto.right)
312
12762
            };
313
            // +spec:positioning:d730e5 - CB height is independent of the abspos element, so percentage heights always resolve
314
12762
            let cb_height = containing_block_rect.size.height;
315

            
316
12762
            let css_height = get_css_height(ctx.styled_dom, dom_id, node_state);
317
            // +spec:replaced-elements:7d8ba8 - ยง10.6.5: for absolutely positioned replaced
318
            // elements, height is determined first (as for inline replaced elements), so treat
319
            // it as "not auto" in the constraint equation even if CSS says auto.
320
12762
            let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
321
12762
            let is_replaced = matches!(node_data.node_type, NodeType::Image(_))
322
12762
                || node_data.is_virtual_view_node();
323
12762
            let height_is_auto = css_height.is_auto() && !is_replaced;
324
            // +spec:overflow:941a06 - resolve auto inset properties: if only one is auto, solved to zero via constraint; if both auto, use static position
325
12762
            let top_is_auto = offsets.top.is_none();
326
12762
            let bottom_is_auto = offsets.bottom.is_none();
327

            
328
            // element_size is border-box (includes border + padding + content).
329
            // The constraint equation is:
330
            //   top + margin-top + border-box-height + margin-bottom + bottom = CB height
331
            // (border-top, padding-top, content-height, padding-bottom, border-bottom
332
            //  are all inside border-box-height)
333
12762
            let mut used_height = element_size.height;
334
            // +spec:height-calculation:44939a - set auto values for margin-top/margin-bottom to 0
335
            // +spec:height-calculation:2f6e10 - if bottom is auto, replace auto margin-top/margin-bottom with 0
336
12762
            let mut used_margin_top = if margin_auto.top { 0.0 } else { margin_top_val };
337
12762
            let mut used_margin_bottom = if margin_auto.bottom { 0.0 } else { margin_bottom_val };
338

            
339
            // +spec:box-model:3a9c2a - resolving auto insets: static position fallback when insets are auto
340
            // +spec:box-model:bd442c - weaker inset resolves to align margin box with inset-modified CB edge
341
            // +spec:height-calculation:93e91c - abs non-replaced height: auto margin centering, single auto margin solve, over-constrained ignore bottom
342
            // +spec:positioning:6e7732 - ยง10.6.4 vertical constraint equation for abspos non-replaced elements
343
            // +spec:positioning:b63d0f - absolute positioning with top:auto uses static position (change bars example)
344
            // +spec:positioning:da8a0c - resolving auto insets: normal alignment treated as start, so auto insets resolve to static position
345
            // +spec:positioning:820b22 - 10.6.4: absolutely positioned non-replaced elements vertical constraint equation and 6 rules
346
12762
            if top_is_auto && height_is_auto && bottom_is_auto {
347
                // +spec:positioning:08e0ac - absolute element with top:auto uses static position (current line)
348
                // +spec:positioning:aab294 - both inset properties auto: resolve to static position
349
                // +spec:positioning:d9bb3c - hypothetical position: UA may guess static position rather than fully computing hypothetical box
350
                // All three auto: set top to static position, height from content, solve for bottom
351
                // +spec:height-calculation:51627d - auto margins to 0, top = static position, height from content (rule 3)
352
                // +spec:positioning:460f2f - All three auto: set top to static position, height from content, solve for bottom
353
                final_pos.y = static_pos.y;
354
12762
            } else if !top_is_auto && !height_is_auto && !bottom_is_auto {
355
                // +spec:overflow:fc0c9e - over-constrained abspos: auto margins minimize overflow (CSS2.1 equivalent of Box Alignment 3 safe alignment)
356
                // +spec:positioning:88f760 - auto margins of absolutely-positioned boxes (vertical)
357
                // None are auto: over-constrained case
358
                // +spec:height-calculation:03c071 - none auto: equal auto margins, solve single auto margin, or ignore bottom if over-constrained
359
                let top_val = offsets.top.unwrap();
360
                let bottom_val = offsets.bottom.unwrap();
361
                if margin_auto.top && margin_auto.bottom {
362
                    // +spec:height-calculation:5112a4 - both margin-top/bottom auto: solve with equal values
363
                    let available = cb_height - top_val - used_height - bottom_val;
364
                    let each = available / 2.0;
365
                    used_margin_top = each;
366
                    used_margin_bottom = each;
367
                } else if margin_auto.top {
368
                    used_margin_top = cb_height - top_val - used_height - used_margin_bottom - bottom_val;
369
                } else if margin_auto.bottom {
370
                    used_margin_bottom = cb_height - top_val - used_height - used_margin_top - bottom_val;
371
                }
372
                // else: over-constrained, ignore bottom
373
                final_pos.y = containing_block_rect.origin.y + top_val + used_margin_top;
374
12762
            } else if top_is_auto && height_is_auto && !bottom_is_auto {
375
                // +spec:height-calculation:909b50 - top and height auto, bottom not auto: height from BFC auto heights, solve for top
376
                // Rule 1: height from content, auto margins to 0, solve for top
377
                let bottom_val = offsets.bottom.unwrap();
378
                let top_val = cb_height - used_margin_top - used_height - used_margin_bottom - bottom_val;
379
                final_pos.y = containing_block_rect.origin.y + top_val + used_margin_top;
380
12762
            } else if top_is_auto && bottom_is_auto && !height_is_auto {
381
                // +spec:positioning:64e1ba - top+bottom auto, height not auto: set top to static position, solve for bottom
382
                final_pos.y = static_pos.y;
383
12762
            } else if height_is_auto && bottom_is_auto && !top_is_auto {
384
6160
                // Rule 3: height from content, auto margins to 0, solve for bottom
385
6160
                let top_val = offsets.top.unwrap();
386
6160
                final_pos.y = containing_block_rect.origin.y + top_val + used_margin_top;
387
6602
            } else if top_is_auto && !height_is_auto && !bottom_is_auto {
388
                // +spec:height-calculation:33dce8 - top auto, height and bottom not auto: solve for top
389
                // Rule 4: auto margins to 0, solve for top
390
                let bottom_val = offsets.bottom.unwrap();
391
                let top_val = cb_height - used_margin_top - used_height - used_margin_bottom - bottom_val;
392
                final_pos.y = containing_block_rect.origin.y + top_val + used_margin_top;
393
6602
            } else if height_is_auto && !top_is_auto && !bottom_is_auto {
394
                // +spec:intrinsic-sizing:566a43 - abspos auto height with non-auto insets: stretch-fit size
395
                // +spec:intrinsic-sizing:c7227f - except: if box has aspect-ratio, ratio-dependent axis uses max-content
396
90
                let has_aspect_ratio = matches!(
397
90
                    get_aspect_ratio_property(ctx.styled_dom, dom_id, node_state),
398
                    MultiValue::Exact(azul_css::props::style::effects::StyleAspectRatio::Ratio(_))
399
                );
400
90
                let top_val = offsets.top.unwrap();
401
90
                let bottom_val = offsets.bottom.unwrap();
402
90
                if !has_aspect_ratio {
403
90
                    // solve for height from constraint equation (stretch-fit):
404
90
                    // height = cb_height - top - margin_top - margin_bottom - bottom
405
90
                    // +spec:containing-block:b3f0dd - clamp effective CB size to zero when insets exceed it (weaker inset reduced)
406
90
                    used_height = (cb_height - top_val - used_margin_top - used_margin_bottom - bottom_val).max(0.0);
407
90
                }
408
                // else: keep content-based height (max-content) per aspect-ratio exception
409
90
                final_pos.y = containing_block_rect.origin.y + top_val + used_margin_top;
410
                // Update the element size with the resolved height
411
90
                if let Some(node_mut) = tree.get_mut(node_index) {
412
90
                    if let Some(ref mut size) = node_mut.used_size {
413
90
                        size.height = used_height;
414
90
                    }
415
                }
416
6512
            } else if bottom_is_auto && !top_is_auto && !height_is_auto {
417
6512
                // Rule 6: auto margins to 0, solve for bottom
418
6512
                let top_val = offsets.top.unwrap();
419
6512
                final_pos.y = containing_block_rect.origin.y + top_val + used_margin_top;
420
6512
            } else {
421
                // Fallback to static position
422
                final_pos.y = static_pos.y;
423
            }
424

            
425
            // +spec:box-model:984243 - horizontal constraint equation for abs-pos non-replaced elements
426
            // +spec:positioning:3be194 - position abs replaced element after establishing width
427
            // Constraint: left + margin-left + border-left + padding-left + width +
428
            // +spec:width-calculation:1661b4 - constraint equation and six rules for abs-pos horizontal (ยง10.3.7)
429
            // left + margin-left + border-left + padding-left + width +
430
            //   padding-right + border-right + margin-right + right = CB width
431
            // Since element_size.width is border-box (border + padding + content),
432
            // simplifies to: left + margin-left + border_box_width + margin-right + right = CB width
433
            {
434
12762
                let margin_left = margin_left_val;
435
12762
                let margin_right = margin_right_val;
436
12762
                let margin_left_auto = margin_left_auto_flag;
437
12762
                let margin_right_auto = margin_right_auto_flag;
438
12762
                let cb_width = containing_block_rect.size.width;
439
12762
                let border_box_width = element_size.width;
440
12762
                let left_val = offsets.left;
441
12762
                let right_val = offsets.right;
442
12762
                let left_is_auto = left_val.is_none();
443
12762
                let right_is_auto = right_val.is_none();
444

            
445
                // Get direction of containing block for over-constrained resolution
446
                use azul_css::props::style::StyleDirection;
447
12762
                let cb_direction = {
448
12762
                    let cb_dom_id = if position_type == LayoutPosition::Fixed {
449
                        None // viewport CB, default LTR
450
                    } else {
451
12762
                        let mut parent = tree.nodes[node_index].parent;
452
12762
                        let mut found = None;
453
12762
                        while let Some(pidx) = parent {
454
12454
                            if let Some(pnode) = tree.get(pidx) {
455
12454
                                if get_position_type(ctx.styled_dom, pnode.dom_node_id).is_positioned() {
456
12454
                                    found = pnode.dom_node_id;
457
12454
                                    break;
458
                                }
459
                                parent = pnode.parent;
460
                            } else {
461
                                break;
462
                            }
463
                        }
464
12762
                        found
465
                    };
466
12762
                    match cb_dom_id {
467
12454
                        Some(cb_id) => {
468
12454
                            let cb_ns = &ctx.styled_dom.styled_nodes.as_container()[cb_id].styled_node_state;
469
12454
                            match get_direction_property(ctx.styled_dom, cb_id, cb_ns) {
470
12454
                                MultiValue::Exact(v) => v,
471
                                _ => StyleDirection::Ltr,
472
                            }
473
                        }
474
308
                        None => StyleDirection::Ltr,
475
                    }
476
                };
477

            
478
                // +spec:replaced-elements:7d8ba8 - ยง10.3.8: for absolutely positioned replaced elements, width is determined
479
                // first (as for inline replaced), so treat as "not auto" in the constraint.
480
12762
                let width_is_auto = get_css_width(ctx.styled_dom, dom_id, node_state).is_auto() && !is_replaced;
481

            
482
12762
                if !left_is_auto && !width_is_auto && !right_is_auto {
483
                    // +spec:positioning:88f760 - auto margins of absolutely-positioned boxes (horizontal)
484
                    // +spec:width-calculation:942c77 - abs-pos non-replaced width: auto margins, over-constrained resolution
485
                    // None of left/width/right are auto โ€” solve for margins or handle over-constrained
486
                    // +spec:width-calculation:dff69d - ยง10.3.7 abs-pos non-replaced: none auto โ†’ equal auto margins, solve single auto margin, or over-constrained
487
                    let left = left_val.unwrap();
488
                    let right = right_val.unwrap();
489
                    let remaining = cb_width - left - border_box_width - right;
490

            
491
                    // +spec:writing-modes:9c3b40 - abspos auto margins: if negative remaining in inline axis, start margin=0, end margin gets remainder
492
                    if margin_left_auto && margin_right_auto {
493
                        // +spec:positioning:ab47b3 - auto margins can be negative in absolute positioning
494
                        // Both margins auto: equal values unless negative
495
                        let each_margin = remaining / 2.0;
496
                        if each_margin < 0.0 {
497
                            match cb_direction {
498
                                StyleDirection::Ltr => {
499
                                    final_pos.x = containing_block_rect.origin.x + left;
500
                                }
501
                                StyleDirection::Rtl => {
502
                                    final_pos.x = containing_block_rect.origin.x + left + remaining;
503
                                }
504
                            }
505
                        } else {
506
                            final_pos.x = containing_block_rect.origin.x + left + each_margin;
507
                        }
508
                    } else if margin_left_auto {
509
                        let solved_margin_left = remaining - margin_right;
510
                        final_pos.x = containing_block_rect.origin.x + left + solved_margin_left;
511
                    } else if margin_right_auto {
512
                        final_pos.x = containing_block_rect.origin.x + left + margin_left;
513
                    } else {
514
                        // Over-constrained: ignore right (LTR) or left (RTL)
515
                        match cb_direction {
516
                            StyleDirection::Ltr => {
517
                                final_pos.x = containing_block_rect.origin.x + left + margin_left;
518
                            }
519
                            StyleDirection::Rtl => {
520
                                let solved_left = cb_width - margin_left - border_box_width - margin_right - right;
521
                                final_pos.x = containing_block_rect.origin.x + solved_left + margin_left;
522
                            }
523
                        }
524
                    }
525
                } else {
526
                    // +spec:overflow:f323cb - auto inset: align margin box to stronger inset edge (may overflow CB)
527
                    // +spec:width-calculation:bbf97a - set auto margins to 0 for abspos when left/width/right has auto
528
                    // Set auto margins to 0, apply six rules
529
                    // +spec:box-model:2da091 - if either inset is auto, auto margins resolve to zero
530
                    // +spec:intrinsic-sizing:087b57 - abspos auto margins resolve to 0 when any inset is auto
531
                    // +spec:width-calculation:0c29ce - set auto margins to 0, then apply six rules for abs pos width
532
12762
                    let m_left = if margin_left_auto { 0.0 } else { margin_left };
533
12762
                    let m_right = if margin_right_auto { 0.0 } else { margin_right };
534

            
535
                    // +spec:width-calculation:2b2852 - all three auto: set auto margins to 0, use static position for left (LTR)
536
                    // +spec:width-calculation:c120b3 - all three of left/width/right auto: set auto margins to 0, then use direction to pick static position
537
12762
                    if left_is_auto && width_is_auto && right_is_auto {
538
                        match cb_direction {
539
                            StyleDirection::Ltr => {
540
                                // Set left to static position, apply rule 3 (width from content, solve for right)
541
                                final_pos.x = static_pos.x;
542
                            }
543
                            StyleDirection::Rtl => {
544
                                // Set right to static position, apply rule 1 (width from content, solve for left)
545
                                let static_offset = static_pos.x - containing_block_rect.origin.x;
546
                                let right_static = (cb_width - static_offset - border_box_width).max(0.0);
547
                                let solved_left = cb_width - m_left - border_box_width - m_right - right_static;
548
                                final_pos.x = containing_block_rect.origin.x + solved_left + m_left;
549
                            }
550
                        }
551
12762
                    } else if left_is_auto && width_is_auto && !right_is_auto {
552
                        // left+width auto, right not auto: width from content, 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
12762
                    } else if left_is_auto && !width_is_auto && right_is_auto {
557
                        // left+right auto: set left to static position (LTR)
558
                        final_pos.x = static_pos.x;
559
12762
                    } else if !left_is_auto && width_is_auto && right_is_auto {
560
6160
                        // width+right auto: position from left
561
6160
                        let left = left_val.unwrap();
562
6160
                        final_pos.x = containing_block_rect.origin.x + left + m_left;
563
6602
                    } else if left_is_auto && !width_is_auto && !right_is_auto {
564
44
                        // left auto: solve for left
565
44
                        let right = right_val.unwrap();
566
44
                        let solved_left = cb_width - m_left - border_box_width - m_right - right;
567
44
                        final_pos.x = containing_block_rect.origin.x + solved_left + m_left;
568
6558
                    } else if !left_is_auto && width_is_auto && !right_is_auto {
569
                        // +spec:intrinsic-sizing:566a43 - abspos auto width with non-auto insets: stretch-fit size
570
                        // +spec:intrinsic-sizing:c7227f - except: if box has aspect-ratio, ratio-dependent axis uses max-content
571
90
                        let has_aspect_ratio = matches!(
572
90
                            get_aspect_ratio_property(ctx.styled_dom, dom_id, node_state),
573
                            MultiValue::Exact(azul_css::props::style::effects::StyleAspectRatio::Ratio(_))
574
                        );
575
90
                        let left = left_val.unwrap();
576
90
                        let right = right_val.unwrap();
577
90
                        if !has_aspect_ratio {
578
                            // width = cb_width - left - margin_left - margin_right - right
579
90
                            let used_width = (cb_width - left - m_left - m_right - right).max(0.0);
580
90
                            if let Some(node_mut) = tree.get_mut(node_index) {
581
90
                                if let Some(ref mut size) = node_mut.used_size {
582
90
                                    size.width = used_width;
583
90
                                }
584
                            }
585
                        }
586
                        // else: keep content-based width (max-content) per aspect-ratio exception
587
90
                        final_pos.x = containing_block_rect.origin.x + left + m_left;
588
6468
                    } else if !left_is_auto && !width_is_auto && right_is_auto {
589
6468
                        // right auto: position from left
590
6468
                        let left = left_val.unwrap();
591
6468
                        final_pos.x = containing_block_rect.origin.x + left + m_left;
592
6468
                    } else {
593
                        final_pos.x = static_pos.x;
594
                    }
595
                }
596
            }
597

            
598
12762
            super::pos_set(calculated_positions, node_index, final_pos);
599

            
600
            // The absolute box is now at its FINAL, definite size. Lay out its
601
            // content against that box if a percentage-height child collapsed โ€”
602
            // which happens because (a) the taffy-bridge layout path that handles
603
            // flex-nested blocks never runs `process_out_of_flow_children`, so an
604
            // abs child's subtree is otherwise NEVER laid out, and (b) even on the
605
            // solver3 path the subtree is laid out BEFORE the stretch-fit height is
606
            // resolved here. Either way the child saw a 0-height containing block.
607
            // Re-flowing now (the abs height is independent of its content, so this
608
            // can't loop) lets `height:100%` children resolve against the real box.
609
            // (Root cause of the slippy-map VirtualView blank-bounds bug.)
610
12762
            if height_is_auto {
611
6250
                let (used_size, inner, child_collapsed) = {
612
6250
                    let n = &tree.nodes[node_index];
613
6250
                    let used = n.used_size.unwrap_or_default();
614
6250
                    let inner = n.box_props.inner_size(used, LayoutWritingMode::HorizontalTb);
615
6250
                    let collapsed = inner.height > 1.0
616
6250
                        && tree.children(node_index).iter().any(|&c| {
617
2
                            tree.get(c)
618
2
                                .and_then(|cn| cn.used_size)
619
2
                                .map_or(true, |s| s.height < 1.0)
620
2
                        });
621
6250
                    (used, inner, collapsed)
622
                };
623
6250
                let _ = used_size;
624
6250
                if child_collapsed {
625
1
                    let constraints = LayoutConstraints {
626
1
                        available_size: inner,
627
1
                        writing_mode: LayoutWritingMode::HorizontalTb,
628
1
                        writing_mode_ctx: super::geometry::WritingModeContext::default(),
629
1
                        bfc_state: None,
630
1
                        text_align: TextAlign::Start,
631
1
                        containing_block_size: inner,
632
1
                        available_width_type:
633
1
                            crate::text3::cache::AvailableSpace::Definite(inner.width),
634
1
                    };
635
1
                    let mut reflow_float_cache: std::collections::HashMap<usize, FloatingContext> =
636
1
                        std::collections::HashMap::new();
637
1
                    let _ = layout_formatting_context(
638
1
                        ctx,
639
1
                        tree,
640
1
                        text_cache,
641
1
                        node_index,
642
1
                        &constraints,
643
1
                        &mut reflow_float_cache,
644
1
                    );
645
6249
                }
646
6512
            }
647
31931
        }
648
    }
649
4723
}
650

            
651
// +spec:positioning:5b0d7f - relative positioning: offset from normal flow position, siblings unaffected
652
// +spec:positioning:8afbe2 - Relative positioning preserves normal flow size and space; only visual offset applied after layout
653
// +spec:positioning:3502d5 - relative and absolute positioning supported for combined use
654
// +spec:positioning:b22222 - relative positioning: offset from static position, purely visual effect
655
// +spec:positioning:b814b6 - relative/absolute/fixed positioning scheme (CSS Positioned Layout Module Level 3)
656
/// Final pass to shift relatively positioned elements from their static flow position.
657
// +spec:block-formatting-context:60ccf9 - relative positioning shifts inline boxes as a unit after normal flow
658
// +spec:display-property:17239f - relative positioning offsets element after normal flow; abspos elements taken out of flow
659
// +spec:positioning:cbe066 - relative positioning implementation
660
///
661
/// Resolves percentage-based offsets for `top`, `left`, etc.
662
/// For relatively positioned elements, percentages are
663
/// relative to the dimensions of the parent element's content box.
664
// +spec:positioning:2d8e15 - relative positioning shifts elements as a unit after normal flow without affecting surrounding content
665
4723
pub fn adjust_relative_positions<T: ParsedFontTrait>(
666
4723
    ctx: &mut LayoutContext<'_, T>,
667
4723
    tree: &LayoutTree,
668
4723
    calculated_positions: &mut super::PositionVec,
669
4723
    viewport: LogicalRect, // The viewport is needed if the root element is relative.
670
4723
) {
671
    // NOTE: returns `()` (not `Result<()>`). This fn is Ok-always โ€” its only `?` are on `Option`
672
    // inside `.and_then` closures, never propagating to the fn body. The previous `Result<(),
673
    // LayoutError>` return forced the `?` at the call site to read an Ok-niche discriminant, which
674
    // the remillโ†’wasm lift mis-lowers (per-build, ASLR-dependent) โ†’ a FALSE Err that aborted the
675
    // whole layout in the web backend (rect=0). Matches sibling reposition_* fns that return ().
676
    // Iterate through all nodes. We need the index to modify the position map.
677
44785
    for node_index in 0..tree.nodes.len() {
678
44785
        let node = &tree.nodes[node_index];
679
44785
        let position_type = get_position_type(ctx.styled_dom, node.dom_node_id);
680

            
681
        // +spec:block-formatting-context:faa1cf - static boxes: top/right/bottom/left do not apply
682
        // Early continue for non-relative positioning
683
        // +spec:overflow:cfb09a - Sticky positioning uses relative-like offsets, clamped to nearest scrollport at scroll time
684
44785
        if position_type != LayoutPosition::Relative && position_type != LayoutPosition::Sticky {
685
44343
            continue;
686
442
        }
687

            
688
        // +spec:table-layout:6cb73b - position:relative effect on table elements is undefined; skip them
689
        // +spec:table-layout:718f91 - relative positioning on table-row/row-group shifts all contents
690
        {
691
            use azul_css::props::layout::LayoutDisplay;
692
442
            let display = get_display_property(ctx.styled_dom, node.dom_node_id);
693
442
            if let MultiValue::Exact(d) = display {
694
                // +spec:positioning:4614dd - position does not apply to table-column-group or table-column boxes
695
                // Table-row and row-group elements DO support relative positioning:
696
                // the shift affects all contents including cells originating in the row.
697
                // Table-column, table-column-group, table-cell, and table-caption do not.
698
442
                if matches!(
699
442
                    d,
700
                    LayoutDisplay::TableColumnGroup
701
                        | LayoutDisplay::TableColumn
702
                        | LayoutDisplay::TableCell
703
                        | LayoutDisplay::TableCaption
704
                ) {
705
                    continue;
706
442
                }
707
            }
708
        }
709

            
710
        // Determine the containing block size for resolving percentages.
711
        // For `position: relative`, this is the parent's content box size.
712
442
        let containing_block_size = node.parent
713
442
            .and_then(|parent_idx| tree.get(parent_idx))
714
442
            .map(|parent_node| {
715
                // Get parent's writing mode to correctly calculate its inner (content) size.
716
442
                let parent_wm = parent_node.dom_node_id
717
442
                    .map(|pid| {
718
442
                        let ps = &ctx.styled_dom.styled_nodes.as_container()[pid].styled_node_state;
719
442
                        get_writing_mode(ctx.styled_dom, pid, ps).unwrap_or_default()
720
442
                    })
721
442
                    .unwrap_or_default();
722
442
                let parent_used_size = parent_node.used_size.unwrap_or_default();
723
442
                parent_node.box_props.inner_size(parent_used_size, parent_wm)
724
442
            })
725
            // The root element is relatively positioned. Its containing block is the viewport.
726
442
            .unwrap_or(viewport.size);
727

            
728
        // +spec:positioning:418c74 - inset percentages resolve against containing block size per axis; auto is unconstrained
729
442
        let offsets =
730
442
            resolve_position_offsets(ctx.styled_dom, node.dom_node_id, containing_block_size);
731

            
732
        // Get a mutable reference to the position and apply the offsets.
733
442
        let Some(current_pos) = calculated_positions.get_mut(node_index) else {
734
            continue;
735
        };
736

            
737
442
        let initial_pos = *current_pos;
738

            
739
        // +spec:positioning:5eb813 - relative positioning offsets contents from normal flow position
740
        // +spec:positioning:a2e5f1 - relative positioning shifts element from static position (vs absolute/float)
741
        // top/bottom/left/right offsets are applied relative to the static position.
742
442
        let mut delta_x = 0.0;
743
442
        let mut delta_y = 0.0;
744

            
745
        // +spec:positioning:218b50 - Relative positioning: top=-bottom, left=-right, direction-dependent resolution, top wins over bottom
746
        // According to CSS 2.1 Section 9.4.3:
747
        // - For `top` and `bottom`: if both are specified, `top` wins and `bottom` is ignored
748
        // - For `left` and `right`: depends on direction (ltr/rtl)
749
        //   - In LTR: if both specified, `left` wins and `right` is ignored
750
        //   - In RTL: if both specified, `right` wins and `left` is ignored
751

            
752
        // +spec:overflow:53dffd - both left/right auto โ†’ used values are 0, boxes stay in original position
753
        // +spec:positioning:5a099e - negative offsets can cause overlapping (no clamping applied)
754
        // +spec:positioning:d189de - bottom offset for relative positioning is with respect to the box's own bottom edge
755
        // +spec:positioning:d80f47 - opposing inset values are negations: top wins over bottom, left/right per direction
756
        // +spec:positioning:ecc27c - relative positioning: left/right move box horizontally without changing size, left = -right
757
        // +spec:positioning:50218d - relative: offset from static position (top edges of box itself)
758
        // both auto โ†’ 0; one auto โ†’ negative of other; neither auto โ†’ bottom ignored (top wins)
759
        // +spec:positioning:ac768b - relative positioning: both autoโ†’0, one autoโ†’neg of other, neitherโ†’top wins; direction-aware left/right
760
        // +spec:positioning:e3727e - top/bottom: both autoโ†’0, one autoโ†’negative of other, neither autoโ†’bottom ignored
761
        // Vertical positioning: `top` takes precedence over `bottom`
762
442
        if let Some(top) = offsets.top {
763
            delta_y = top;
764
442
        } else if let Some(bottom) = offsets.bottom {
765
            delta_y = -bottom;
766
442
        }
767

            
768
        // +spec:positioning:1732e8 - left/right for relatively positioned elements determined by 9.4.3 rules
769
        // Spec: "If the 'direction' property of the containing block is 'ltr', the value of 'left' wins"
770
        // Get the direction of the containing block (parent), not the element itself
771
        use azul_css::props::style::StyleDirection;
772
442
        let cb_direction = node.parent
773
442
            .and_then(|parent_idx| tree.get(parent_idx))
774
442
            .and_then(|parent_node| {
775
442
                let parent_dom_id = parent_node.dom_node_id?;
776
442
                let parent_state =
777
442
                    &ctx.styled_dom.styled_nodes.as_container()[parent_dom_id].styled_node_state;
778
442
                match get_direction_property(ctx.styled_dom, parent_dom_id, parent_state) {
779
442
                    MultiValue::Exact(v) => Some(v),
780
                    _ => None,
781
                }
782
442
            })
783
442
            .unwrap_or(StyleDirection::Ltr);
784
        // +spec:containing-block:6d4fb1 - over-constrained relative positioning: ltrโ†’left wins, rtlโ†’right wins
785
442
        match cb_direction {
786
            StyleDirection::Ltr => {
787
442
                if let Some(left) = offsets.left {
788
                    delta_x = left;
789
442
                } else if let Some(right) = offsets.right {
790
                    // +spec:overflow:fb426c - left auto: used value is minus the value of right
791
                    delta_x = -right;
792
442
                }
793
            }
794
            StyleDirection::Rtl => {
795
                if let Some(right) = offsets.right {
796
                    delta_x = -right;
797
                } else if let Some(left) = offsets.left {
798
                    delta_x = left;
799
                }
800
            }
801
        }
802

            
803
        // +spec:overflow:f1e1ce - relative positioning may cause overflow:auto/scroll boxes to need scrollbars
804
        // Only apply the shift if there is a non-zero delta.
805
442
        if delta_x != 0.0 || delta_y != 0.0 {
806
            current_pos.x += delta_x;
807
            current_pos.y += delta_y;
808

            
809
            ctx.debug_log(&format!(
810
                "Adjusted relative element #{} from {:?} to {:?} (delta: {}, {})",
811
                node_index, initial_pos, *current_pos, delta_x, delta_y
812
            ));
813

            
814
            // +spec:table-layout:ec2600 - For table-row-group, table-header-group, table-footer-group, or table-row,
815
            // the relative shift affects all contents of the box including table cells.
816
            // Propagate the delta to all descendant nodes.
817
            {
818
                use azul_css::props::layout::LayoutDisplay;
819
                let display = get_display_property(ctx.styled_dom, node.dom_node_id);
820
                let is_table_row_like = matches!(
821
                    display,
822
                    MultiValue::Exact(
823
                        LayoutDisplay::TableRowGroup
824
                        | LayoutDisplay::TableHeaderGroup
825
                        | LayoutDisplay::TableFooterGroup
826
                        | LayoutDisplay::TableRow
827
                    )
828
                );
829
                if is_table_row_like {
830
                    // Shift all children (and their descendants) by the same delta
831
                    let mut stack = tree.children(node_index).to_vec();
832
                    while let Some(child_idx) = stack.pop() {
833
                        if let Some(child_pos) = calculated_positions.get_mut(child_idx) {
834
                            child_pos.x += delta_x;
835
                            child_pos.y += delta_y;
836
                        }
837
                        stack.extend_from_slice(tree.children(child_idx));
838
                    }
839
                }
840
            }
841
442
        }
842
    }
843
4723
}
844

            
845
/// Sticky positioning constraints computed at layout time.
846
/// At scroll time, the sticky box's position is clamped so that
847
/// it remains within the sticky view rectangle (scrollport inset by these values).
848
// +spec:overflow:bac4e5 - sticky view rectangle from inset properties relative to nearest scrollport
849
#[derive(Debug, Clone)]
850
pub struct StickyConstraints {
851
    /// Inset from the top edge of the nearest scrollport (0 if auto).
852
    pub top_inset: f32,
853
    /// Inset from the right edge of the nearest scrollport (0 if auto).
854
    pub right_inset: f32,
855
    /// Inset from the bottom edge of the nearest scrollport (0 if auto).
856
    pub bottom_inset: f32,
857
    /// Inset from the left edge of the nearest scrollport (0 if auto).
858
    pub left_inset: f32,
859
    /// Normal-flow position of the sticky element (border-box origin).
860
    pub normal_flow_position: LogicalPosition,
861
    /// Border-box size of the sticky element.
862
    pub border_box_size: LogicalSize,
863
    /// The scrollport rect (content-box of nearest scroll container).
864
    pub scrollport: LogicalRect,
865
}
866

            
867
/// Finds the nearest scrollport (ancestor with overflow: scroll or auto) for a node.
868
/// Returns the content-box rect of the scrollport, or the viewport if none found.
869
fn find_nearest_scrollport(
870
    tree: &LayoutTree,
871
    node_index: usize,
872
    styled_dom: &StyledDom,
873
    calculated_positions: &super::PositionVec,
874
    viewport: LogicalRect,
875
) -> LogicalRect {
876
    use crate::solver3::getters::{get_overflow_x, get_overflow_y};
877
    use azul_css::props::layout::LayoutOverflow;
878

            
879
    let mut current_parent_idx = tree.get(node_index).and_then(|n| n.parent);
880

            
881
    while let Some(parent_index) = current_parent_idx {
882
        let parent_node = match tree.get(parent_index) {
883
            Some(n) => n,
884
            None => break,
885
        };
886
        let parent_dom_id = match parent_node.dom_node_id {
887
            Some(id) => id,
888
            None => {
889
                current_parent_idx = parent_node.parent;
890
                continue;
891
            }
892
        };
893

            
894
        let node_state = &styled_dom.styled_nodes.as_container()[parent_dom_id].styled_node_state;
895
        let ox = get_overflow_x(styled_dom, parent_dom_id, node_state);
896
        let oy = get_overflow_y(styled_dom, parent_dom_id, node_state);
897

            
898
        let is_scrollport = matches!(
899
            ox,
900
            MultiValue::Exact(LayoutOverflow::Scroll | LayoutOverflow::Auto)
901
        ) || matches!(
902
            oy,
903
            MultiValue::Exact(LayoutOverflow::Scroll | LayoutOverflow::Auto)
904
        );
905

            
906
        if is_scrollport {
907
            let margin_box_pos = calculated_positions
908
                .get(parent_index)
909
                .copied()
910
                .unwrap_or_default();
911
            let border_box_size = parent_node.used_size.unwrap_or_default();
912

            
913
            // Content-box = margin-box pos + border + padding, size - border - padding
914
            let pbp = parent_node.box_props.unpack();
915
            let content_pos = LogicalPosition::new(
916
                margin_box_pos.x
917
                    + pbp.border.left
918
                    + pbp.padding.left,
919
                margin_box_pos.y
920
                    + pbp.border.top
921
                    + pbp.padding.top,
922
            );
923
            let content_size = LogicalSize::new(
924
                (border_box_size.width
925
                    - pbp.border.left
926
                    - pbp.border.right
927
                    - pbp.padding.left
928
                    - pbp.padding.right)
929
                    .max(0.0),
930
                (border_box_size.height
931
                    - pbp.border.top
932
                    - pbp.border.bottom
933
                    - pbp.padding.top
934
                    - pbp.padding.bottom)
935
                    .max(0.0),
936
            );
937
            return LogicalRect::new(content_pos, content_size);
938
        }
939

            
940
        current_parent_idx = parent_node.parent;
941
    }
942

            
943
    viewport
944
}
945

            
946
/// Find the scroll offset of the nearest scroll container ancestor.
947
/// Returns the scroll offset as a LogicalPosition (how far the content has scrolled).
948
fn find_nearest_scroll_offset(
949
    tree: &LayoutTree,
950
    node_index: usize,
951
    scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
952
) -> LogicalPosition {
953
    let mut parent = tree.get(node_index).and_then(|n| n.parent);
954
    while let Some(pidx) = parent {
955
        if let Some(pnode) = tree.get(pidx) {
956
            if let Some(dom_id) = pnode.dom_node_id {
957
                if let Some(scroll_pos) = scroll_offsets.get(&dom_id) {
958
                    let offset_x = scroll_pos.children_rect.origin.x - scroll_pos.parent_rect.origin.x;
959
                    let offset_y = scroll_pos.children_rect.origin.y - scroll_pos.parent_rect.origin.y;
960
                    return LogicalPosition::new(offset_x, offset_y);
961
                }
962
            }
963
            parent = pnode.parent;
964
        } else {
965
            break;
966
        }
967
    }
968
    LogicalPosition::zero()
969
}
970

            
971
/// Adjusts positions of sticky-positioned elements based on scroll offset.
972
///
973
/// Sticky positioning works like relative positioning, but the element's position
974
/// is constrained by its inset properties (top/right/bottom/left) relative to the
975
/// nearest scrollport (scroll container ancestor). The margin box is further
976
/// constrained to remain within the containing block.
977
///
978
/// +spec:position-sticky:9449f1 - for sticky positioning, insets represent offsets from scrollport edge
979
/// +spec:position-sticky:75412d - multiple sticky boxes in same container offset independently
980
/// +spec:box-model:af9af8 - sticky positioning: shift element to stay within sticky view rectangle, margin box constrained to containing block
981
/// +spec:overflow:bac4e5 - compute sticky view rectangle, clamp end-edge insets to border box size
982
4664
pub fn adjust_sticky_positions<T: ParsedFontTrait>(
983
4664
    ctx: &mut LayoutContext<'_, T>,
984
4664
    tree: &LayoutTree,
985
4664
    calculated_positions: &mut super::PositionVec,
986
4664
    scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
987
4664
    viewport: LogicalRect,
988
4664
) {
989
    // Returns `()` (not `Result<()>`): Ok-always (its only `?` is Option-`?` in an `.and_then`
990
    // closure). Avoids the lift-fragile Result<(),LayoutError> Ok-niche read at the call site.
991
44484
    for node_index in 0..tree.nodes.len() {
992
44484
        let node = &tree.nodes[node_index];
993
44484
        let position_type = get_position_type(ctx.styled_dom, node.dom_node_id);
994

            
995
44484
        if position_type != LayoutPosition::Sticky {
996
44484
            continue;
997
        }
998

            
999
        let dom_id = match node.dom_node_id {
            Some(id) => id,
            None => continue,
        };
        // Find the nearest scrollport for this sticky element
        let scrollport = find_nearest_scrollport(
            tree,
            node_index,
            ctx.styled_dom,
            calculated_positions,
            viewport,
        );
        // The containing block for percentage resolution is the parent's content box
        let containing_block = node.parent
            .and_then(|parent_idx| {
                let parent_node = tree.get(parent_idx)?;
                let parent_pos = calculated_positions.get(parent_idx).copied().unwrap_or_default();
                let parent_size = parent_node.used_size.unwrap_or_default();
                let parent_wm = parent_node.dom_node_id
                    .map(|pid| {
                        let ps = &ctx.styled_dom.styled_nodes.as_container()[pid].styled_node_state;
                        get_writing_mode(ctx.styled_dom, pid, ps).unwrap_or_default()
                    })
                    .unwrap_or_default();
                let pbp = parent_node.box_props.unpack();
                let content_size = pbp.inner_size(parent_size, parent_wm);
                let content_origin = LogicalPosition::new(
                    parent_pos.x + pbp.border.left + pbp.padding.left,
                    parent_pos.y + pbp.border.top + pbp.padding.top,
                );
                Some(LogicalRect::new(content_origin, content_size))
            })
            .unwrap_or(viewport);
        // Resolve inset properties (top, right, bottom, left)
        let offsets = resolve_position_offsets(ctx.styled_dom, Some(dom_id), scrollport.size);
        // Get the scroll offset from the nearest scroll container
        let scroll_offset = find_nearest_scroll_offset(tree, node_index, scroll_offsets);
        let Some(current_pos) = calculated_positions.get_mut(node_index) else {
            continue;
        };
        let static_pos = *current_pos;
        let element_size = node.used_size.unwrap_or_default();
        let nbp = node.box_props.unpack();
        let margin = &nbp.margin;
        let mut shift_x = 0.0f32;
        let mut shift_y = 0.0f32;
        // For each side: if inset is not auto, clamp the border edge to stay
        // within the sticky view rectangle (scrollport inset by the specified amount).
        // The scroll offset shifts the effective scrollport position.
        if let Some(top_inset) = offsets.top {
            let sticky_edge = scrollport.origin.y + scroll_offset.y + top_inset;
            let border_top = current_pos.y;
            if border_top < sticky_edge {
                shift_y = shift_y.max(sticky_edge - border_top);
            }
        }
        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
            ));
        }
    }
4664
}
// +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
12848
pub fn find_absolute_containing_block_rect(
12848
    tree: &LayoutTree,
12848
    node_index: usize,
12848
    styled_dom: &StyledDom,
12848
    calculated_positions: &super::PositionVec,
12848
    viewport: LogicalRect,
12848
) -> Result<LogicalRect> {
    // +spec:positioning:748d87 - walk up to nearest positioned ancestor for CB
12848
    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
12848
    while let Some(parent_index) = current_parent_idx {
12540
        let parent_node = tree.get(parent_index).ok_or(LayoutError::InvalidTree)?;
12540
        if get_position_type(styled_dom, parent_node.dom_node_id).is_positioned() {
            // calculated_positions stores margin-box positions
12540
            let margin_box_pos = calculated_positions
12540
                .get(parent_index)
12540
                .copied()
12540
                .unwrap_or_default();
            // used_size is the border-box size
12540
            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)
12540
            let pbp = parent_node.box_props.unpack();
12540
            let padding_box_pos = LogicalPosition::new(
12540
                margin_box_pos.x + pbp.border.left,
12540
                margin_box_pos.y + pbp.border.top,
            );
            // Calculate padding-box size (border-box - borders)
12540
            let padding_box_size = LogicalSize::new(
12540
                border_box_size.width
12540
                    - pbp.border.left
12540
                    - pbp.border.right,
12540
                border_box_size.height
12540
                    - pbp.border.top
12540
                    - pbp.border.bottom,
            );
12540
            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
308
    Ok(viewport)
12848
}