Skip to main content

taffy/compute/
oof.rs

1//! Shared positioning pass for out-of-flow (`position: absolute` / `position: fixed`) boxes.
2//!
3//! Layout algorithms do not lay out their out-of-flow children directly (unless they are the
4//! child's containing block). Instead they emit [`OofCandidate`] records which bubble up the tree
5//! via [`LayoutOutput::oof_candidates`](crate::LayoutOutput) until they reach the box's containing
6//! block, which lays the box out using the routine in this module.
7use crate::geometry::{Line, Point, Rect, Size};
8use crate::style::{
9    AlignItemsKeyword, AlignSelf, AlignmentSafety, AvailableSpace, ContainingBlockClaims, CoreStyle, OofItemStyle,
10    Overflow,
11};
12#[cfg(feature = "grid")]
13use crate::tree::DetailedLayoutInfo;
14use crate::tree::{
15    AxisStaticPosition, Layout, LayoutContainingBlock, LayoutInput, LayoutOutput, LayoutPartialTreeExt, NodeId,
16    OofCandidate, OofCandidates, OofPositioningArea, RequestedAxis, RunMode, SizingMode,
17};
18use crate::util::sys::{f32_max, Vec};
19use crate::util::{MaybeMath, MaybeResolve, ResolveOrZero};
20use crate::{AxisStaticEdge, BoxSizing, Direction};
21
22#[cfg(feature = "content_size")]
23use super::common::scrollable_overflow::compute_scrollable_overflow_contribution;
24use super::common::sizing_keyword::resolve_absolute_sizing_keywords;
25
26/// Resolve the final static offset of an out-of-flow box from its static position, given the
27/// box's final size and resolved margins.
28///
29/// In each axis the box's margin box is aligned within the static-position area according to the
30/// recorded alignment keyword. When the alignment is `safe` and the margin box overflows the
31/// area, the fallback keyword is used instead (the `safe` overflow-position keyword from CSS
32/// Box Alignment). The returned offset locates the box's border box and is relative to whatever
33/// the static-position areas are relative to (the containing block's border box once a candidate
34/// has bubbled to its containing block).
35pub fn resolve_static_offset(
36    static_position: Point<AxisStaticPosition>,
37    final_size: Size<f32>,
38    resolved_margin: Rect<f32>,
39) -> Point<f32> {
40    Point {
41        x: resolve_static_offset_axis(
42            static_position.x,
43            final_size.width,
44            Line { start: resolved_margin.left, end: resolved_margin.right },
45        ),
46        y: resolve_static_offset_axis(
47            static_position.y,
48            final_size.height,
49            Line { start: resolved_margin.top, end: resolved_margin.bottom },
50        ),
51    }
52}
53
54/// Single-axis version of [`resolve_static_offset`]
55fn resolve_static_offset_axis(sp: AxisStaticPosition, size: f32, margin: Line<f32>) -> f32 {
56    let margin_box_size = size + margin.start + margin.end;
57    let overflows = matches!(sp.align.safety, AlignmentSafety::Safe) && margin_box_size > sp.area.end - sp.area.start;
58    let keyword = if overflows { sp.align.fallback } else { sp.align.keyword };
59    align_in_line(sp.area, margin_box_size, keyword) + margin.start
60}
61
62/// Position a box of `size` within `container` so that it is aligned to the given physical edge
63fn align_in_line(container: Line<f32>, size: f32, edge: AxisStaticEdge) -> f32 {
64    match edge {
65        AxisStaticEdge::Start => container.start,
66        AxisStaticEdge::End => container.end - size,
67        AxisStaticEdge::Center => (container.start + container.end - size) / 2.0,
68    }
69}
70
71/// The self-alignment of an out-of-flow box in one axis, resolved relative to the writing mode
72/// of its containing block (`Start` is the logical start edge of the containing block).
73#[derive(Copy, Clone, PartialEq, Eq, Debug)]
74enum OofAlignment {
75    /// `normal` (or `auto`): stretch-fit sizing for non-replaced, non-table boxes; start alignment
76    Normal,
77    /// `stretch`: stretch-fit sizing; start alignment
78    Stretch,
79    /// Aligned to the logical start edge
80    Start,
81    /// Centered
82    Center,
83    /// Aligned to the logical end edge
84    End,
85}
86
87impl OofAlignment {
88    /// Convert an `align-self`/`justify-self` value for an out-of-flow box into a logical
89    /// alignment relative to the containing block's writing mode.
90    ///
91    /// `alignment` must already have had `self-start`/`self-end` resolved against the containing
92    /// block's direction (see [`AlignSelf::resolve_self_relative`]).
93    fn from_resolved(alignment: AlignSelf) -> Self {
94        debug_assert!(
95            !matches!(alignment.keyword, AlignItemsKeyword::SelfStart | AlignItemsKeyword::SelfEnd),
96            "self-start/self-end must be resolved before computing out-of-flow alignment"
97        );
98        match alignment.keyword {
99            AlignItemsKeyword::Normal => Self::Normal,
100            AlignItemsKeyword::Stretch => Self::Stretch,
101            AlignItemsKeyword::Center => Self::Center,
102            AlignItemsKeyword::End | AlignItemsKeyword::FlexEnd | AlignItemsKeyword::SelfEnd => Self::End,
103            // The fallback alignment of `baseline` is `start`
104            AlignItemsKeyword::Start
105            | AlignItemsKeyword::FlexStart
106            | AlignItemsKeyword::SelfStart
107            | AlignItemsKeyword::Baseline => Self::Start,
108        }
109    }
110
111    /// The physical edge of the containing block the box's margin box is aligned to once its
112    /// size has been determined. `Normal` and `Stretch` behave as `Start`.
113    fn physical_edge(self, reversed: bool) -> AxisStaticEdge {
114        match (self, reversed) {
115            (Self::Center, _) => AxisStaticEdge::Center,
116            (Self::End, false) | (Self::Normal | Self::Stretch | Self::Start, true) => AxisStaticEdge::End,
117            (Self::End, true) | (Self::Normal | Self::Stretch | Self::Start, false) => AxisStaticEdge::Start,
118        }
119    }
120}
121
122/// The inputs to the absolute positioning layout model
123/// (<https://www.w3.org/TR/css-position-3/#abspos-layout>) in a single axis. All coordinates
124/// are physical (left-to-right / top-to-bottom) and relative to the start of the containing
125/// block's inset-resolution area.
126#[derive(Copy, Clone, Debug)]
127struct OofAxis {
128    /// The size of the containing block in this axis
129    cb_size: f32,
130    /// The box's resolved inset properties (`None` = `auto`)
131    inset: Line<Option<f32>>,
132    /// The box's resolved margins (`None` = `auto`)
133    margin: Line<Option<f32>>,
134    /// The box's static position (relative to the containing block's inset-resolution area)
135    static_position: AxisStaticPosition,
136    /// The box's self-alignment in this axis
137    alignment: OofAlignment,
138    /// The overflow-position modifier of the box's self-alignment in this axis
139    safety: AlignmentSafety,
140    /// Whether this is the inline axis of the containing block
141    is_inline: bool,
142    /// Whether the logical start edge of the containing block is the physical end edge of this
143    /// axis (the inline axis of an RTL containing block)
144    reversed: bool,
145    /// Whether the containing block is a scroll container which scrolls in this axis
146    is_scroll_axis: bool,
147}
148
149impl OofAxis {
150    /// The physical edge corresponding to the logical start edge of the containing block
151    fn start_edge(&self) -> AxisStaticEdge {
152        if self.reversed {
153            AxisStaticEdge::End
154        } else {
155            AxisStaticEdge::Start
156        }
157    }
158
159    /// The physical edge corresponding to the logical end edge of the containing block
160    fn end_edge(&self) -> AxisStaticEdge {
161        if self.reversed {
162            AxisStaticEdge::Start
163        } else {
164            AxisStaticEdge::End
165        }
166    }
167
168    /// The inset-modified containing block (IMCB) in this axis
169    /// <https://www.w3.org/TR/css-position-3/#resolving-insets>
170    fn inset_modified_containing_block(&self) -> Line<f32> {
171        let (mut imcb, weaker_edge) = match (self.inset.start, self.inset.end) {
172            (Some(start), Some(end)) => (Line { start, end: self.cb_size - end }, self.end_edge()),
173            // A lone auto inset resolves to zero, and is the weaker inset
174            (Some(start), None) => (Line { start, end: self.cb_size }, AxisStaticEdge::End),
175            (None, Some(end)) => (Line { start: 0.0, end: self.cb_size - end }, AxisStaticEdge::Start),
176            // Both insets are auto: resolve them from the static position rectangle and the
177            // alignment within it
178            (None, None) => {
179                let sp = self.static_position;
180                let imcb = match sp.align.keyword {
181                    AxisStaticEdge::Start => Line { start: sp.area.start, end: self.cb_size },
182                    AxisStaticEdge::End => Line { start: 0.0, end: sp.area.end },
183                    AxisStaticEdge::Center => {
184                        // Center the IMCB on the static position rectangle's center, bounded by
185                        // the nearer edge of the containing block
186                        let center = (sp.area.start + sp.area.end) / 2.0;
187                        let start_distance = center;
188                        let end_distance = self.cb_size - center;
189                        if start_distance.abs() <= end_distance.abs() {
190                            Line { start: 0.0, end: 2.0 * start_distance }
191                        } else {
192                            Line { start: self.cb_size - 2.0 * end_distance, end: self.cb_size }
193                        }
194                    }
195                };
196                (imcb, self.end_edge())
197            }
198        };
199
200        // Overconstrained insets: the weaker inset is reduced to bring the IMCB size up to zero
201        // <https://www.w3.org/TR/css-position-3/#resolving-insets>
202        if imcb.end < imcb.start {
203            match weaker_edge {
204                AxisStaticEdge::Start => imcb.start = imcb.end,
205                _ => imcb.end = imcb.start,
206            }
207        }
208        imcb
209    }
210
211    /// The size of the IMCB in this axis: the space available to the box's margin box
212    fn inset_modified_containing_block_size(&self) -> f32 {
213        let imcb = self.inset_modified_containing_block();
214        imcb.end - imcb.start
215    }
216
217    /// The stretch-fit size of the box's border box: the IMCB minus the non-auto margins (auto
218    /// margins are treated as zero) <https://www.w3.org/TR/css-position-3/#abspos-auto-size>
219    fn stretch_fit_size(&self) -> f32 {
220        let non_auto_margin_sum = self.margin.start.unwrap_or(0.0) + self.margin.end.unwrap_or(0.0);
221        f32_max(self.inset_modified_containing_block_size() - non_auto_margin_sum, 0.0)
222    }
223
224    /// Whether an `auto` size in this axis is the stretch-fit size (rather than fit-content)
225    /// <https://www.w3.org/TR/css-position-3/#abspos-auto-size>
226    fn is_stretch_sized(&self, is_replaced: bool, is_table: bool) -> bool {
227        let no_auto_inset = self.inset.start.is_some() && self.inset.end.is_some();
228        no_auto_inset
229            && match self.alignment {
230                OofAlignment::Stretch => true,
231                OofAlignment::Normal => !is_replaced && !is_table,
232                _ => false,
233            }
234    }
235
236    /// Resolve the box's auto margins given its final size
237    /// <https://www.w3.org/TR/css-position-3/#abspos-margins>
238    fn resolve_margins(&self, size: f32) -> Line<f32> {
239        let both_insets = self.inset.start.is_some() && self.inset.end.is_some();
240        let imcb = self.inset_modified_containing_block();
241        let free_space = imcb.end - imcb.start - size;
242        match (self.margin.start, self.margin.end) {
243            (Some(start), Some(end)) => Line { start, end },
244            // If either inset is auto then auto margins resolve to zero
245            _ if !both_insets => Line { start: self.margin.start.unwrap_or(0.0), end: self.margin.end.unwrap_or(0.0) },
246            (Some(start), None) => Line { start, end: free_space - start },
247            (None, Some(end)) => Line { start: free_space - end, end },
248            (None, None) => {
249                // In the inline axis, negative free space is absorbed entirely by the end margin
250                if free_space < 0.0 && self.is_inline {
251                    if self.reversed {
252                        Line { start: free_space, end: 0.0 }
253                    } else {
254                        Line { start: 0.0, end: free_space }
255                    }
256                } else {
257                    Line { start: free_space / 2.0, end: free_space / 2.0 }
258                }
259            }
260        }
261    }
262
263    /// Compute the physical start of the box's border box given its final size and margins
264    /// <https://www.w3.org/TR/css-position-3/#abspos-alignment>
265    fn position(&self, size: f32, margin: Line<f32>) -> f32 {
266        let imcb = self.inset_modified_containing_block();
267        match (self.inset.start, self.inset.end) {
268            // Both insets are auto: static alignment, with safe overflow relative to the IMCB
269            (None, None) => {
270                let sp = self.static_position;
271                let margin_box_size = size + margin.start + margin.end;
272                let offset = if sp.align.safety == AlignmentSafety::Safe && margin_box_size > imcb.end - imcb.start {
273                    align_in_line(imcb, margin_box_size, self.start_edge())
274                } else {
275                    align_in_line(sp.area, margin_box_size, sp.align.keyword)
276                };
277                offset + margin.start
278            }
279            // One auto inset: the margin box is aligned to the edge of the stronger inset
280            (Some(_), None) => imcb.start + margin.start,
281            (None, Some(_)) => imcb.end - size - margin.end,
282            (Some(_), Some(_)) => {
283                // Auto margins have already absorbed all of the free space
284                if self.margin.start.is_none() || self.margin.end.is_none() {
285                    return imcb.start + margin.start;
286                }
287
288                let margin_box_size = size + margin.start + margin.end;
289                let imcb_size = imcb.end - imcb.start;
290                let edge = self.alignment.physical_edge(self.reversed);
291                let margin_box_start = match self.safety {
292                    AlignmentSafety::Unsafe => align_in_line(imcb, margin_box_size, edge),
293                    AlignmentSafety::Safe => {
294                        let edge = if margin_box_size > imcb_size { self.start_edge() } else { edge };
295                        align_in_line(imcb, margin_box_size, edge)
296                    }
297                    // Default overflow alignment for absolutely positioned boxes:
298                    // <https://www.w3.org/TR/css-align-3/#auto-safety-position>
299                    AlignmentSafety::Default => {
300                        if margin_box_size <= imcb_size || self.alignment == OofAlignment::Normal {
301                            // 1. Fits within the IMCB: align as specified
302                            align_in_line(imcb, margin_box_size, edge)
303                        } else {
304                            // The overflow limit rect is the bounding rect of the IMCB and the
305                            // containing block, extended to infinity in the scrollable direction
306                            // of a scroll container
307                            let mut limit = Line { start: imcb.start.min(0.0), end: f32_max(imcb.end, self.cb_size) };
308                            if self.is_scroll_axis {
309                                if self.reversed {
310                                    limit.start = f32::NEG_INFINITY;
311                                } else {
312                                    limit.end = f32::INFINITY;
313                                }
314                            }
315                            if margin_box_size <= limit.end - limit.start {
316                                // 2. Fits within the overflow limit rect: cover the IMCB and align
317                                // as specified as far as possible without overflowing the limit
318                                let start = align_in_line(imcb, margin_box_size, edge);
319                                f32_max(start.min(limit.end - margin_box_size), limit.start)
320                            } else {
321                                // 3. Start-align within the overflow limit rect
322                                align_in_line(limit, margin_box_size, self.start_edge())
323                            }
324                        }
325                    }
326                };
327                margin_box_start + margin.start
328            }
329        }
330    }
331}
332
333/// Run the out-of-flow positioning pass for `node_id` after its layout algorithm has produced
334/// `output`: lay out the out-of-flow candidates in `output.oof_candidates` for which the node is
335/// the containing block, record them as the node's hoisted children, and replace
336/// `output.oof_candidates` with the unclaimed remainder (which bubble further up the tree).
337/// The scrollable overflow contributed by the claimed boxes is merged into
338/// `output.scrollable_overflow_rect`.
339///
340/// This should be called once per `RunMode::PerformLayout` container layout, after the layout
341/// algorithm (block/flexbox/grid) has run, and *inside* any layout caching wrapper (such as
342/// [`compute_cached_layout`](crate::compute_cached_layout)) so that cache hits do not re-run the
343/// pass. It is a no-op when `output.oof_positioning_area` is `None` (leaf and size-only outputs).
344///
345/// Which candidates the node claims is determined by the node's style via
346/// [`CoreStyle::is_containing_block`]. `position: fixed` boxes unclaimed by every ancestor are
347/// claimed by the final root positioning pass in [`compute_root_layout`](crate::compute_root_layout).
348pub fn compute_oof_layout(tree: &mut impl LayoutContainingBlock, node_id: NodeId, output: &mut LayoutOutput) {
349    let Some(area) = output.oof_positioning_area else { return };
350    if output.oof_candidates.is_empty() {
351        tree.clear_hoisted_children(node_id);
352        return;
353    }
354
355    let style = tree.get_core_container_style(node_id);
356    let direction = style.direction();
357    let claims = style.is_containing_block();
358    drop(style);
359
360    let candidates = output.oof_candidates.take();
361    let result = compute_oof_layout_for_area(tree, node_id, candidates.as_slice(), area, direction, claims);
362    // Always record the hoisted child list (even when empty) so that lists recorded by previous
363    // layout runs do not persist
364    tree.clear_hoisted_children(node_id);
365    tree.add_hoisted_children(node_id, &result.hoisted);
366    output.oof_candidates = result.unclaimed;
367    #[cfg(feature = "content_size")]
368    {
369        output.scrollable_overflow_rect = output.scrollable_overflow_rect.union(result.scrollable_overflow_rect);
370    }
371}
372
373/// The result of laying out out-of-flow candidates against an explicit positioning area.
374pub struct OofLayoutResult {
375    /// Candidates claimed and laid out by this pass, in document order.
376    pub hoisted: Vec<NodeId>,
377    /// Candidates not claimed by this pass, which must continue bubbling.
378    pub unclaimed: OofCandidates,
379    /// Scrollable overflow contributed by the claimed candidates, relative to the positioning
380    /// area's origin.
381    pub scrollable_overflow_rect: Rect<f32>,
382}
383
384/// Lay out out-of-flow candidates against an explicit positioning area.
385///
386/// Unlike [`compute_oof_layout`], this function does not update the geometry owner's hoisted
387/// children or merge overflow into a [`LayoutOutput`]. This is useful when the CSS containing
388/// block does not have its own layout node and another node owns the resulting geometry.
389///
390/// `geometry_owner` supplies the layout tree operations, detailed layout information, and
391/// scroll-container state used by the positioning pass. `direction` and `claims` describe the
392/// actual CSS containing block.
393pub fn compute_oof_layout_for_area(
394    tree: &mut impl LayoutContainingBlock,
395    geometry_owner: NodeId,
396    candidates: &[OofCandidate],
397    area: OofPositioningArea,
398    direction: Direction,
399    claims: ContainingBlockClaims,
400) -> OofLayoutResult {
401    let overflow = tree.get_core_container_style(geometry_owner).overflow();
402
403    let mut hoisted = Vec::new();
404    let mut unclaimed = OofCandidates::new();
405    let scrollable_overflow_rect = perform_oof_layout(
406        tree,
407        geometry_owner,
408        candidates,
409        area.size,
410        area.offset,
411        direction,
412        claims,
413        overflow,
414        &mut hoisted,
415        &mut unclaimed,
416    );
417
418    OofLayoutResult { hoisted, unclaimed, scrollable_overflow_rect }
419}
420
421/// The result of laying out a single out-of-flow box with [`layout_oof_box`].
422pub(crate) struct OofBoxLayout {
423    /// The box's final layout (its `location` is relative to the containing block's border box)
424    pub layout: Layout,
425    /// Out-of-flow candidates surfaced from within the box's subtree, with anchors relative to
426    /// the box's own border box
427    pub surfaced: OofCandidates,
428    /// The box's `overflow` style
429    #[cfg(feature = "content_size")]
430    pub overflow: Point<Overflow>,
431    /// The box's `contain` style
432    #[cfg(feature = "content_size")]
433    pub contain: crate::style::Contain,
434}
435
436/// Size and position a single out-of-flow box against its containing block, and store its layout.
437///
438/// - `area_size`/`area_offset` describe the inset-resolution area of the containing block
439///   (border box minus borders and scrollbar gutters), relative to the containing block's border box.
440/// - `container_overflow` is the `overflow` style of the containing block, which determines
441///   whether overflowing boxes may be aligned into its scrollable overflow area.
442/// - `grid_owner` is the node whose detailed layout info is consulted for grid placement: if it is
443///   a grid container, the box is positioned relative to the grid area determined by its
444///   grid-placement properties rather than the passed area.
445/// - `before_layout` is called with the inputs of the final layout pass just before it runs, which
446///   allows the caller to inspect the layout cache for the box.
447#[inline(always)]
448#[allow(clippy::too_many_arguments)]
449pub(crate) fn layout_oof_box<Tree: LayoutContainingBlock>(
450    tree: &mut Tree,
451    candidate: OofCandidate,
452    area_size: Size<f32>,
453    area_offset: Point<f32>,
454    direction: Direction,
455    container_overflow: Point<Overflow>,
456    #[cfg(feature = "grid")] grid_owner: Option<NodeId>,
457    before_layout: impl FnOnce(&mut Tree, &LayoutInput),
458) -> OofBoxLayout {
459    let child_style = tree.get_oof_item_style(candidate.node);
460
461    // If the current node is a grid container then the box is positioned relative to the
462    // grid area determined by its grid-placement properties (which falls back to the passed
463    // area for `auto` placement)
464    #[cfg(feature = "grid")]
465    let (area_size, area_offset) = match grid_owner.map(|owner| tree.get_detailed_layout_info(owner)) {
466        Some(DetailedLayoutInfo::Grid(grid_info)) => {
467            let area_rect = Rect {
468                left: area_offset.x,
469                right: area_offset.x + area_size.width,
470                top: area_offset.y,
471                bottom: area_offset.y + area_size.height,
472            };
473            let grid_area = grid_info.resolve_absolute_grid_area(
474                child_style.grid_row(),
475                child_style.grid_column(),
476                direction,
477                area_rect,
478            );
479            (
480                Size { width: grid_area.right - grid_area.left, height: grid_area.bottom - grid_area.top },
481                Point { x: grid_area.left, y: grid_area.top },
482            )
483        }
484        _ => (area_size, area_offset),
485    };
486    let area_width = area_size.width;
487    let area_height = area_size.height;
488
489    let aspect_ratio = child_style.aspect_ratio();
490    let margin =
491        child_style.margin().map(|margin| margin.resolve_to_option(area_width, |val, basis| tree.calc(val, basis)));
492    let padding = child_style.padding().resolve_or_zero(Some(area_width), |val, basis| tree.calc(val, basis));
493    let border = child_style.border().resolve_or_zero(Some(area_width), |val, basis| tree.calc(val, basis));
494    let padding_border_sum = (padding + border).sum_axes();
495    let box_sizing_adjustment =
496        if child_style.box_sizing() == BoxSizing::ContentBox { padding_border_sum } else { Size::ZERO };
497
498    // Resolve inset
499    let left = child_style.inset().left.maybe_resolve(area_width, |val, basis| tree.calc(val, basis));
500    let right = child_style.inset().right.maybe_resolve(area_width, |val, basis| tree.calc(val, basis));
501    let top = child_style.inset().top.maybe_resolve(area_height, |val, basis| tree.calc(val, basis));
502    let bottom = child_style.inset().bottom.maybe_resolve(area_height, |val, basis| tree.calc(val, basis));
503
504    // Compute known dimensions from min/max/inherent size styles
505    let size_style = child_style.size();
506    let style_size = size_style
507        .maybe_resolve(area_size, |val, basis| tree.calc(val, basis))
508        .maybe_apply_aspect_ratio(aspect_ratio)
509        .maybe_add(box_sizing_adjustment);
510    let mut min_size = child_style
511        .min_size()
512        .maybe_resolve(area_size, |val, basis| tree.calc(val, basis))
513        .maybe_apply_aspect_ratio(aspect_ratio)
514        .maybe_add(box_sizing_adjustment)
515        .or(padding_border_sum.map(Some))
516        .maybe_max(padding_border_sum);
517    let max_size = child_style
518        .max_size()
519        .maybe_resolve(area_size, |val, basis| tree.calc(val, basis))
520        .maybe_apply_aspect_ratio(aspect_ratio)
521        .maybe_add(box_sizing_adjustment);
522    let mut known_dimensions = style_size.maybe_clamp(min_size, max_size);
523
524    let is_replaced = child_style.is_replaced();
525    let is_table = child_style.is_table();
526    let item_direction = child_style.direction();
527    // Taffy only supports the `horizontal-tb` writing mode, so only the inline axis can be reversed.
528    // `auto` (`None`) behaves as `normal` for absolutely positioned boxes: it does not defer to the
529    // `*-items` properties of the containing block.
530    let justify_self =
531        child_style.justify_self().unwrap_or(AlignSelf::NORMAL).resolve_self_relative(item_direction, direction, true);
532    let align_self =
533        child_style.align_self().unwrap_or(AlignSelf::NORMAL).resolve_self_relative(item_direction, direction, false);
534    let overflow = child_style.overflow();
535    let scrollbar_width = child_style.scrollbar_width();
536    #[cfg(feature = "content_size")]
537    let contain = child_style.contain();
538
539    drop(child_style);
540
541    // Static positions are relative to the containing block's border box: make them relative to
542    // the inset-resolution area
543    let static_position = Point {
544        x: AxisStaticPosition {
545            area: candidate.static_position.x.area.map(|v| v - area_offset.x),
546            align: candidate.static_position.x.align,
547        },
548        y: AxisStaticPosition {
549            area: candidate.static_position.y.area.map(|v| v - area_offset.y),
550            align: candidate.static_position.y.align,
551        },
552    };
553
554    let horizontal = OofAxis {
555        cb_size: area_width,
556        inset: Line { start: left, end: right },
557        margin: Line { start: margin.left, end: margin.right },
558        static_position: static_position.x,
559        alignment: OofAlignment::from_resolved(justify_self),
560        safety: justify_self.safety,
561        is_inline: true,
562        reversed: direction.is_rtl(),
563        is_scroll_axis: container_overflow.x.is_scroll_container(),
564    };
565    let vertical = OofAxis {
566        cb_size: area_height,
567        inset: Line { start: top, end: bottom },
568        margin: Line { start: margin.top, end: margin.bottom },
569        static_position: static_position.y,
570        alignment: OofAlignment::from_resolved(align_self),
571        safety: align_self.safety,
572        is_inline: false,
573        reversed: false,
574        is_scroll_axis: container_overflow.y.is_scroll_container(),
575    };
576    let stretch_fit_size = Size { width: horizontal.stretch_fit_size(), height: vertical.stretch_fit_size() };
577    // The space available to the box's border box is its stretch-fit size (the IMCB minus its
578    // non-auto margins). The available space for a table never exceeds that of the containing
579    // block (minus the margins) <https://www.w3.org/TR/css-tables-3/#abspos>
580    let mut available_space = stretch_fit_size;
581    if is_table {
582        let non_auto_margin = margin.map(|m| m.unwrap_or(0.0));
583        available_space = available_space.f32_min(Size {
584            width: f32_max(area_width - non_auto_margin.horizontal_axis_sum(), 0.0),
585            height: f32_max(area_height - non_auto_margin.vertical_axis_sum(), 0.0),
586        });
587    }
588    let available_space = available_space.maybe_clamp(min_size, max_size).map(AvailableSpace::Definite);
589
590    // Resolve any sizing keywords (min-content, max-content, fit-content, fit-content(...),
591    // stretch) in the size styles. An explicitly sized axis takes precedence over the
592    // automatic size below.
593    if size_style.width.is_sizing_keyword() || size_style.height.is_sizing_keyword() {
594        resolve_absolute_sizing_keywords(
595            tree,
596            candidate.node,
597            &mut known_dimensions,
598            size_style,
599            area_size,
600            Rect { left, right, top, bottom },
601            margin,
602            SizingMode::ContentSize,
603        );
604        known_dimensions = known_dimensions.maybe_apply_aspect_ratio(aspect_ratio).maybe_clamp(min_size, max_size);
605    }
606
607    // Resolve automatic sizes <https://www.w3.org/TR/css-position-3/#abspos-auto-size>
608    //
609    // An auto size is the stretch-fit size (the IMCB minus margins) if the self-alignment in
610    // that axis is `stretch` (or `normal` for a non-replaced, non-table box) and neither inset is
611    // auto. Otherwise it is the fit-content size, which is measured below. Stretch-fit sizes are
612    // resolved first so that a size in the other axis can be transferred through the aspect ratio.
613    // The block axis is the ratio-dependent axis, so a stretched inline size is transferred
614    // through the aspect ratio rather than also stretching the block size. An explicit
615    // `stretch` in both axes stretches both and ignores the aspect ratio.
616    let stretch_width = known_dimensions.width.is_none() && horizontal.is_stretch_sized(is_replaced, is_table);
617    let stretch_height = known_dimensions.height.is_none() && vertical.is_stretch_sized(is_replaced, is_table);
618    let both_explicit_stretch =
619        horizontal.alignment == OofAlignment::Stretch && vertical.alignment == OofAlignment::Stretch;
620    let stretch_aspect_ratio =
621        if stretch_width && stretch_height && both_explicit_stretch { None } else { aspect_ratio };
622    if stretch_width {
623        if is_table {
624            // A table cannot be stretched smaller than its content: stretch-fit is a minimum
625            min_size.width = Some(f32_max(min_size.width.unwrap_or(0.0), stretch_fit_size.width));
626        } else {
627            known_dimensions.width = Some(stretch_fit_size.width);
628            known_dimensions =
629                known_dimensions.maybe_apply_aspect_ratio(stretch_aspect_ratio).maybe_clamp(min_size, max_size);
630        }
631    }
632    if known_dimensions.height.is_none() && stretch_height {
633        if is_table {
634            min_size.height = Some(f32_max(min_size.height.unwrap_or(0.0), stretch_fit_size.height));
635        } else {
636            known_dimensions.height = Some(stretch_fit_size.height);
637            known_dimensions =
638                known_dimensions.maybe_apply_aspect_ratio(stretch_aspect_ratio).maybe_clamp(min_size, max_size);
639        }
640    }
641
642    let final_size = match (known_dimensions.width, known_dimensions.height) {
643        (Some(width), Some(height)) => Size { width, height },
644        _ => {
645            let measured_size = tree.measure_child_size_both(
646                candidate.node,
647                known_dimensions,
648                area_size.map(Some),
649                available_space,
650                SizingMode::ContentSize,
651                Line::FALSE,
652            );
653            known_dimensions.unwrap_or(measured_size)
654        }
655    }
656    .maybe_clamp(min_size, max_size);
657
658    let layout_input = LayoutInput {
659        known_dimensions: final_size.map(Some),
660        known_dimensions_are_definite: Size { width: true, height: true },
661        parent_size: area_size.map(Some),
662        available_space,
663        sizing_mode: SizingMode::ContentSize,
664        axis: RequestedAxis::Both,
665        run_mode: RunMode::PerformLayout,
666        vertical_margins_are_collapsible: Line::FALSE,
667    };
668    before_layout(tree, &layout_input);
669    let mut layout_output = tree.compute_child_layout(candidate.node, layout_input);
670
671    // Resolve auto margins <https://www.w3.org/TR/css-position-3/#abspos-margins>
672    let horizontal_margin = horizontal.resolve_margins(final_size.width);
673    let vertical_margin = vertical.resolve_margins(final_size.height);
674    let resolved_margin = Rect {
675        left: horizontal_margin.start,
676        right: horizontal_margin.end,
677        top: vertical_margin.start,
678        bottom: vertical_margin.end,
679    };
680
681    // Align the margin box within the IMCB <https://www.w3.org/TR/css-position-3/#abspos-alignment>
682    let location = Point {
683        x: horizontal.position(final_size.width, horizontal_margin) + area_offset.x,
684        y: vertical.position(final_size.height, vertical_margin) + area_offset.y,
685    };
686
687    // Note: axis intentionally switched here as scrollbars take up space in the opposite axis
688    // to the axis in which scrolling is enabled.
689    let scrollbar_size = Size {
690        width: if overflow.y == Overflow::Scroll { scrollbar_width } else { 0.0 },
691        height: if overflow.x == Overflow::Scroll { scrollbar_width } else { 0.0 },
692    };
693
694    let layout = Layout {
695        order: candidate.order,
696        size: final_size,
697        #[cfg(feature = "content_size")]
698        scrollable_overflow_rect: layout_output.scrollable_overflow_rect,
699        scrollbar_size,
700        location,
701        padding,
702        border,
703        margin: resolved_margin,
704    };
705    tree.set_unrounded_layout(candidate.node, &layout);
706
707    OofBoxLayout {
708        layout,
709        surfaced: layout_output.oof_candidates.take(),
710        #[cfg(feature = "content_size")]
711        overflow,
712        #[cfg(feature = "content_size")]
713        contain,
714    }
715}
716
717/// Perform final layout on the out-of-flow candidates for which the current node is the
718/// containing block, and collect the remainder into `unclaimed` for further bubbling.
719///
720/// - `node_id` is the current node. If its detailed layout info (as returned by
721///   [`LayoutContainingBlock::get_detailed_layout_info`]) indicates that it is a grid container,
722///   each claimed box is positioned relative to the grid area determined by its grid-placement
723///   properties rather than the passed inset-resolution area.
724/// - `candidates` is the merged, document-ordered list of candidates held by the current node
725///   (direct out-of-flow children plus candidates bubbled from in-flow children). Anchors must be
726///   relative to the current node's border box.
727/// - `area_size`/`area_offset` describe the inset-resolution area of the current node
728///   (border box minus borders and scrollbar gutters).
729/// - `claims` describes which out-of-flow positions the current node acts as a containing block
730///   for (see [`CoreStyle::is_containing_block`]). The final root positioning pass claims all.
731/// - `container_overflow` is the `overflow` style of the containing block.
732///
733/// Laying out a claimed box may surface further candidates from within its subtree (e.g. a
734/// `position: fixed` descendant of a `position: absolute` box). These are re-swept: claimed ones
735/// are appended to the work list, unclaimed ones are added to `unclaimed`.
736///
737/// The ids of claimed boxes are appended to `hoisted` in order. Returns the scrollable overflow
738/// contribution of the claimed boxes.
739#[allow(clippy::too_many_arguments)]
740pub(crate) fn perform_oof_layout(
741    tree: &mut impl LayoutContainingBlock,
742    node_id: NodeId,
743    candidates: &[OofCandidate],
744    area_size: Size<f32>,
745    area_offset: Point<f32>,
746    direction: Direction,
747    claims: ContainingBlockClaims,
748    container_overflow: Point<Overflow>,
749    hoisted: &mut Vec<NodeId>,
750    unclaimed: &mut OofCandidates,
751) -> Rect<f32> {
752    #[cfg_attr(not(feature = "content_size"), allow(unused_mut))]
753    let mut absolute_overflow_rect = Rect::ZERO;
754
755    if candidates.is_empty() {
756        return absolute_overflow_rect;
757    }
758
759    #[cfg(feature = "content_size")]
760    let is_scroll_container = container_overflow.x.is_scroll_container() || container_overflow.y.is_scroll_container();
761
762    // Split the candidate list into an initial work list of claimed candidates and the unclaimed
763    // remainder. Further claimed candidates surfaced while laying out a claimed box are appended
764    // to the work list (Blink-style re-sweep).
765    let mut worklist: Vec<OofCandidate> = Vec::new();
766    for candidate in candidates {
767        if claims.for_position(candidate.position) {
768            worklist.push(*candidate);
769        } else {
770            unclaimed.push(*candidate);
771        }
772    }
773
774    #[cfg(not(feature = "grid"))]
775    let _ = node_id;
776
777    let mut index = 0;
778    while index < worklist.len() {
779        let candidate = worklist[index];
780        index += 1;
781        hoisted.push(candidate.node);
782
783        let OofBoxLayout {
784            layout,
785            surfaced,
786            #[cfg(feature = "content_size")]
787            overflow,
788            #[cfg(feature = "content_size")]
789            contain,
790        } = layout_oof_box(
791            tree,
792            candidate,
793            area_size,
794            area_offset,
795            direction,
796            container_overflow,
797            #[cfg(feature = "grid")]
798            Some(node_id),
799            |_, _| {},
800        );
801        let location = layout.location;
802        let final_size = layout.size;
803
804        // Re-sweep any candidates surfaced from within the box's subtree, translating their
805        // anchors so they are relative to this node's border box
806        let mut surfaced = surfaced;
807        if !surfaced.is_empty() {
808            surfaced.translate(location);
809            for surfaced_candidate in surfaced.iter() {
810                if claims.for_position(surfaced_candidate.position) {
811                    worklist.push(*surfaced_candidate);
812                } else {
813                    unclaimed.push(*surfaced_candidate);
814                }
815            }
816        }
817
818        #[cfg(feature = "content_size")]
819        {
820            // Location is measured from the scroll origin (the inline-start edge: right side in RTL)
821            let relative_location = if direction.is_rtl() {
822                Point {
823                    x: area_size.width - (location.x - area_offset.x) - final_size.width,
824                    y: location.y - area_offset.y,
825                }
826            } else {
827                Point { x: location.x - area_offset.x, y: location.y - area_offset.y }
828            };
829            absolute_overflow_rect = absolute_overflow_rect.union(compute_scrollable_overflow_contribution(
830                relative_location,
831                final_size,
832                layout.scrollable_overflow_rect,
833                overflow,
834                contain,
835                is_scroll_container,
836            ));
837        }
838    }
839
840    absolute_overflow_rect
841}
842
843#[cfg(test)]
844mod tests {
845    use super::resolve_static_offset;
846    use crate::geometry::{Line, Point, Rect, Size};
847    use crate::style::AlignmentSafety;
848    use crate::tree::{AxisStaticAlign, AxisStaticEdge, AxisStaticPosition};
849
850    fn sp(
851        area: Line<f32>,
852        keyword: AxisStaticEdge,
853        safety: AlignmentSafety,
854        fallback: AxisStaticEdge,
855    ) -> AxisStaticPosition {
856        AxisStaticPosition { area, align: AxisStaticAlign { keyword, safety, fallback } }
857    }
858
859    fn resolve_x(sp_x: AxisStaticPosition, width: f32, margin: Rect<f32>) -> f32 {
860        let position = Point { x: sp_x, y: AxisStaticPosition::from_edge(0.0, AxisStaticEdge::Start) };
861        resolve_static_offset(position, Size { width, height: 10.0 }, margin).x
862    }
863
864    const AREA: Line<f32> = Line { start: 10.0, end: 110.0 };
865    const NO_MARGIN: Rect<f32> = Rect { left: 0.0, right: 0.0, top: 0.0, bottom: 0.0 };
866    const MARGIN: Rect<f32> = Rect { left: 5.0, right: 15.0, top: 0.0, bottom: 0.0 };
867
868    #[test]
869    fn start_alignment() {
870        let sp_x = sp(AREA, AxisStaticEdge::Start, AlignmentSafety::Unsafe, AxisStaticEdge::Start);
871        assert_eq!(resolve_x(sp_x, 20.0, NO_MARGIN), 10.0);
872        assert_eq!(resolve_x(sp_x, 20.0, MARGIN), 15.0);
873    }
874
875    #[test]
876    fn end_alignment() {
877        let sp_x = sp(AREA, AxisStaticEdge::End, AlignmentSafety::Unsafe, AxisStaticEdge::End);
878        assert_eq!(resolve_x(sp_x, 20.0, NO_MARGIN), 90.0);
879        assert_eq!(resolve_x(sp_x, 20.0, MARGIN), 75.0);
880    }
881
882    #[test]
883    fn center_alignment() {
884        let sp_x = sp(AREA, AxisStaticEdge::Center, AlignmentSafety::Unsafe, AxisStaticEdge::Center);
885        assert_eq!(resolve_x(sp_x, 20.0, NO_MARGIN), 50.0);
886        // Center offsets by half the margin difference
887        assert_eq!(resolve_x(sp_x, 20.0, MARGIN), 45.0);
888    }
889
890    #[test]
891    fn safe_alignment_falls_back_only_on_overflow() {
892        let sp_x = sp(AREA, AxisStaticEdge::End, AlignmentSafety::Safe, AxisStaticEdge::Start);
893        // Fits: aligned to the end
894        assert_eq!(resolve_x(sp_x, 20.0, NO_MARGIN), 90.0);
895        // Margin box exactly fills the area: no fallback
896        assert_eq!(resolve_x(sp_x, 100.0, NO_MARGIN), 10.0);
897        // Margin box overflows the area: falls back to start
898        assert_eq!(resolve_x(sp_x, 120.0, NO_MARGIN), 10.0);
899        assert_eq!(resolve_x(sp_x, 90.0, MARGIN), 15.0);
900    }
901
902    #[test]
903    fn unsafe_alignment_never_falls_back() {
904        let sp_x = sp(AREA, AxisStaticEdge::End, AlignmentSafety::Unsafe, AxisStaticEdge::Start);
905        assert_eq!(resolve_x(sp_x, 120.0, NO_MARGIN), -10.0);
906    }
907
908    #[test]
909    fn degenerate_area() {
910        // A zero-extent area (static positions emitted by block layout) behaves as an anchor point
911        let anchor = AxisStaticPosition::from_edge(40.0, AxisStaticEdge::Start);
912        assert_eq!(resolve_x(anchor, 20.0, NO_MARGIN), 40.0);
913        let anchor_end = AxisStaticPosition::from_edge(40.0, AxisStaticEdge::End);
914        assert_eq!(resolve_x(anchor_end, 20.0, NO_MARGIN), 20.0);
915    }
916}