tawasuyu

tawasuyu/taffy

A high performance rust-powered UI layout library

Rust 707HTML 660Markdown 12TOML 62344 Definitions

⎇ df46fcf3630e2e8f4ea2c99bbe261154383a4aab src/compute/block.rs Rust · 1597 lines · 74.4 KB History Who touched what raw

Fix clearance calculation and its interaction with margin collapsing df46fcf36 · Nico Burns ·

1//! Computes the CSS block layout algorithm in the case that the block container being laid out contains only block-level boxes
2use crate::geometry::{Line, Point, Rect, Size};
3use crate::style::{AvailableSpace, CoreStyle, LengthPercentageAuto, Overflow, Position};
4use crate::style_helpers::TaffyMaxContent;
5use crate::tree::{CollapsibleMarginSet, Layout, LayoutInput, LayoutOutput, RunMode, SizingMode};
6use crate::tree::{LayoutPartialTree, LayoutPartialTreeExt, NodeId};
7use crate::util::debug::debug_log;
8use crate::util::sys::f32_max;
9use crate::util::sys::Vec;
10use crate::util::MaybeMath;
11use crate::util::{MaybeResolve, ResolveOrZero};
12use crate::{
13    BlockContainerStyle, BlockItemStyle, BoxGenerationMode, BoxSizing, Direction, LayoutBlockContainer, RequestedAxis,
14    TextAlign,
15};
16
17#[cfg(feature = "float_layout")]
18use super::float::{ContentSlot, FloatContext, FloatIntrinsicWidthCalculator};
19#[cfg(feature = "float_layout")]
20use crate::{Clear, Float, FloatDirection};
21
22/// Context for positioning Block and Float boxes within a Block Formatting Context
23pub struct BlockFormattingContext {
24    /// The float positioning context that handles positioning floats within this Block Formatting Context
25    #[cfg(feature = "float_layout")]
26    float_context: FloatContext,
27}
28
29impl Default for BlockFormattingContext {
30    fn default() -> Self {
31        Self {
32            #[cfg(feature = "float_layout")]
33            float_context: FloatContext::new(),
34        }
35    }
36}
37
38impl BlockFormattingContext {
39    /// Create a new `BlockFormattingContext` with the specified width constraint
40    pub fn new() -> Self {
41        Default::default()
42    }
43
44    /// Create an initial `BlockContext` for this `BlockFormattingContext`
45    pub fn root_block_context(&mut self) -> BlockContext<'_> {
46        BlockContext {
47            bfc: self,
48            y_offset: 0.0,
49            insets: [0.0, 0.0],
50            content_box_insets: [0.0, 0.0],
51            float_content_contribution: 0.0,
52            is_root: true,
53            #[cfg(feature = "float_layout")]
54            adjoining_floats: [false, false],
55            #[cfg(feature = "float_layout")]
56            top_adjoining_floats: None,
57        }
58    }
59}
60
61/// Context for each individual Block within a Block Formatting Context
62///
63/// Contains a mutable reference to the BlockFormattingContext + block-specific data
64pub struct BlockContext<'bfc> {
65    /// A mutable reference to the root BlockFormatttingContext that this BlockContext belongs to
66    bfc: &'bfc mut BlockFormattingContext,
67    /// The y-offset of the border-top of the block node, relative to the to the border-top of the
68    /// root node of the Block Formatting Context it belongs to.
69    y_offset: f32,
70    /// The x-inset of the border-box in from each side of the block node, relative to the root node of the Block Formatting Context it belongs to.
71    insets: [f32; 2],
72    /// The x-insets of the content box
73    content_box_insets: [f32; 2],
74    /// The height that floats take up in the element
75    float_content_contribution: f32,
76    /// Whether the node is the root of the Block Formatting Context is belongs to.
77    is_root: bool,
78    /// Whether a float has been placed (on each side) whose position adjoins the current
79    /// margin-collapse strut of this block (i.e. whose final position can still be moved by
80    /// margins that collapse into that strut). Such floats force clearance on cleared elements
81    /// whose margins adjoin the same strut.
82    #[cfg(feature = "float_layout")]
83    adjoining_floats: [bool; 2],
84    /// The value of `adjoining_floats` frozen at the first point at which in-flow content was
85    /// committed within this block (resolving the position of the block's top margin strut).
86    /// `None` if no in-flow content has been committed yet.
87    #[cfg(feature = "float_layout")]
88    top_adjoining_floats: Option<[bool; 2]>,
89}
90
91impl BlockContext<'_> {
92    /// Create a sub-`BlockContext` for a child block node
93    pub fn sub_context(&mut self, additional_y_offset: f32, insets: [f32; 2]) -> BlockContext<'_> {
94        let insets = [self.insets[0] + insets[0], self.insets[1] + insets[1]];
95        BlockContext {
96            bfc: self.bfc,
97            y_offset: self.y_offset + additional_y_offset,
98            insets,
99            content_box_insets: insets,
100            float_content_contribution: 0.0,
101            is_root: false,
102            // Floats adjoining the parent's current strut also adjoin this block's top strut
103            // (if this block's top margin collapses with its first child's, which is checked separately)
104            #[cfg(feature = "float_layout")]
105            adjoining_floats: self.adjoining_floats,
106            #[cfg(feature = "float_layout")]
107            top_adjoining_floats: None,
108        }
109    }
110
111    /// Returns whether this block is the root block of it's Block Formatting Context
112    pub fn is_bfc_root(&self) -> bool {
113        self.is_root
114    }
115}
116
117#[cfg(feature = "float_layout")]
118impl BlockContext<'_> {
119    /// Set the width of the overall Block Formatting Context. This is used to resolve positions
120    /// that are relative to the right of the context such as right-floated boxes.
121    ///
122    /// Sub-blocks within a Block Formatting Context should use the `Self::sub_context` method to create
123    /// a sub-`BlockContext` with `insets` instead.
124    pub fn set_width(&mut self, available_width: f32) {
125        self.bfc.float_context.set_width(available_width);
126    }
127
128    /// Set the x-axis content-box insets of the `BlockContext`. These are the difference between the border-box
129    /// and the content-box of the box (padding + border + scrollbar_gutter).
130    pub fn apply_content_box_inset(&mut self, content_box_x_insets: [f32; 2]) {
131        self.content_box_insets[0] = self.insets[0] + content_box_x_insets[0];
132        self.content_box_insets[1] = self.insets[1] + content_box_x_insets[1];
133    }
134
135    /// Whether the float context contains any floats
136    #[inline(always)]
137    pub fn has_floats(&self) -> bool {
138        self.bfc.float_context.has_floats()
139    }
140
141    /// Whether the float context contains any floats that extend to or below min_y
142    #[inline(always)]
143    pub fn has_active_floats(&self, min_y: f32) -> bool {
144        self.bfc.float_context.has_active_floats(min_y + self.y_offset)
145    }
146
147    /// Position a floated box with the context
148    pub fn place_floated_box(
149        &mut self,
150        floated_box: Size<f32>,
151        min_y: f32,
152        direction: FloatDirection,
153        clear: Clear,
154        adjoins_unresolved_strut: bool,
155    ) -> Point<f32> {
156        if adjoins_unresolved_strut {
157            self.adjoining_floats[direction as usize] = true;
158        }
159        let mut pos = self.bfc.float_context.place_floated_box(
160            floated_box,
161            min_y + self.y_offset,
162            self.content_box_insets,
163            direction,
164            clear,
165        );
166        pos.y -= self.y_offset;
167        pos.x -= self.insets[0];
168
169        self.float_content_contribution = self.float_content_contribution.max(pos.y + floated_box.height);
170
171        pos
172    }
173
174    /// Search a space suitable for laying out non-floated content into
175    pub fn find_content_slot(&self, min_y: f32, clear: Clear, after: Option<usize>) -> ContentSlot {
176        let mut slot =
177            self.bfc.float_context.find_content_slot(min_y + self.y_offset, self.content_box_insets, clear, after);
178        slot.y -= self.y_offset;
179        slot.x -= self.insets[0];
180        slot
181    }
182
183    /// Get the bottom of lowest relevant float for the specific clear property
184    pub fn cleared_threshold(&self, clear: Clear) -> Option<f32> {
185        self.bfc.float_context.cleared_threshold(clear).map(|threshold| threshold - self.y_offset)
186    }
187
188    /// Whether a float that is adjoining the current margin-collapse strut has been placed
189    /// on the side(s) relevant to the passed clear property
190    pub fn has_adjoining_float(&self, clear: Clear) -> bool {
191        match clear {
192            Clear::Left => self.adjoining_floats[0],
193            Clear::Right => self.adjoining_floats[1],
194            Clear::Both => self.adjoining_floats[0] || self.adjoining_floats[1],
195            Clear::None => false,
196        }
197    }
198
199    /// Merge adjoining float flags propagated from a child block into this block's flags
200    fn merge_adjoining_floats(&mut self, flags: [bool; 2]) {
201        self.adjoining_floats[0] |= flags[0];
202        self.adjoining_floats[1] |= flags[1];
203    }
204
205    /// Record that in-flow content has been committed within this block, resolving the position of
206    /// the current margin-collapse strut. Floats placed before this point no longer adjoin the
207    /// current strut. The flags for the block's top strut are frozen at the first commit.
208    fn commit_strut(&mut self) {
209        if self.top_adjoining_floats.is_none() {
210            self.top_adjoining_floats = Some(self.adjoining_floats);
211        }
212        self.adjoining_floats = [false, false];
213    }
214
215    /// The adjoining float flags for this block's top margin strut: floats placed while the
216    /// position of the block's top strut was still unresolved
217    fn top_adjoining_floats(&self) -> [bool; 2] {
218        self.top_adjoining_floats.unwrap_or(self.adjoining_floats)
219    }
220
221    /// Update the height that descendent floats with the height that floats consume
222    /// within a particular child
223    fn add_child_floated_content_height_contribution(&mut self, child_contribution: f32) {
224        self.float_content_contribution = self.float_content_contribution.max(child_contribution);
225    }
226
227    /// Returns the height that descendent floats consume
228    pub fn floated_content_height_contribution(&self) -> f32 {
229        self.float_content_contribution
230    }
231}
232
233#[cfg(not(feature = "float_layout"))]
234impl BlockContext<'_> {
235    #[inline(always)]
236    /// Returns the height that descendent floats consume (always 0.0 when the float feature is disabled)
237    fn float_content_contribution(&self) -> f32 {
238        0.0
239    }
240}
241
242use super::common::alignment::{apply_alignment_fallback, compute_alignment_offset};
243#[cfg(feature = "content_size")]
244use super::common::content_size::compute_content_size_contribution;
245
246/// Per-child data that is accumulated and modified over the course of the layout algorithm
247struct BlockItem {
248    /// The identifier for the associated node
249    node_id: NodeId,
250
251    /// The "source order" of the item. This is the index of the item within the children iterator,
252    /// and controls the order in which the nodes are placed
253    order: u32,
254
255    /// Items that are tables don't have stretch sizing applied to them
256    is_table: bool,
257
258    /// Whether the child is a non-independent block or inline node
259    is_in_same_bfc: bool,
260
261    #[cfg(feature = "float_layout")]
262    /// The `float` style of the node
263    float: Float,
264    #[cfg(feature = "float_layout")]
265    /// The `clear` style of the node
266    clear: Clear,
267
268    /// The base size of this item
269    size: Size<Option<f32>>,
270    /// The minimum allowable size of this item
271    min_size: Size<Option<f32>>,
272    /// The maximum allowable size of this item
273    max_size: Size<Option<f32>>,
274
275    /// The overflow style of the item
276    overflow: Point<Overflow>,
277    /// The width of the item's scrollbars (if it has scrollbars)
278    scrollbar_width: f32,
279
280    /// The position style of the item
281    position: Position,
282    /// The final offset of this item
283    inset: Rect<LengthPercentageAuto>,
284    /// The margin of this item
285    margin: Rect<LengthPercentageAuto>,
286    /// The margin of this item
287    padding: Rect<f32>,
288    /// The margin of this item
289    border: Rect<f32>,
290    /// The sum of padding and border for this item
291    padding_border_sum: Size<f32>,
292
293    /// The computed border box size of this item
294    computed_size: Size<f32>,
295    /// The computed "static position" of this item. The static position is the position
296    /// taking into account padding, border, margins, and scrollbar_gutters but not inset
297    static_position: Point<f32>,
298    /// Whether margins can be collapsed through this item
299    can_be_collapsed_through: bool,
300
301    /// Pending layout for in-flow non-floated items. Held back from `set_unrounded_layout` so the
302    /// post-loop `align-content` pass in `compute_inner` can shift `location.y` before commit.
303    final_layout: Option<Layout>,
304}
305
306/// Computes the layout of [`LayoutPartialTree`] according to the block layout algorithm
307pub fn compute_block_layout(
308    tree: &mut impl LayoutBlockContainer,
309    node_id: NodeId,
310    inputs: LayoutInput,
311    block_ctx: Option<&mut BlockContext<'_>>,
312) -> LayoutOutput {
313    let LayoutInput { known_dimensions, parent_size, run_mode, .. } = inputs;
314    let style = tree.get_block_container_style(node_id);
315
316    // Pull these out earlier to avoid borrowing issues
317    let overflow = style.overflow();
318    let is_scroll_container = overflow.x.is_scroll_container() || overflow.y.is_scroll_container();
319    let aspect_ratio = style.aspect_ratio();
320    let padding = style.padding().resolve_or_zero(parent_size.width, |val, basis| tree.calc(val, basis));
321    let border = style.border().resolve_or_zero(parent_size.width, |val, basis| tree.calc(val, basis));
322    let padding_border_size = (padding + border).sum_axes();
323    let box_sizing_adjustment =
324        if style.box_sizing() == BoxSizing::ContentBox { padding_border_size } else { Size::ZERO };
325
326    let min_size = style
327        .min_size()
328        .maybe_resolve(parent_size, |val, basis| tree.calc(val, basis))
329        .maybe_apply_aspect_ratio(aspect_ratio)
330        .maybe_add(box_sizing_adjustment);
331    let max_size = style
332        .max_size()
333        .maybe_resolve(parent_size, |val, basis| tree.calc(val, basis))
334        .maybe_apply_aspect_ratio(aspect_ratio)
335        .maybe_add(box_sizing_adjustment);
336    let clamped_style_size = if inputs.sizing_mode == SizingMode::InherentSize {
337        style
338            .size()
339            .maybe_resolve(parent_size, |val, basis| tree.calc(val, basis))
340            .maybe_apply_aspect_ratio(aspect_ratio)
341            .maybe_add(box_sizing_adjustment)
342            .maybe_clamp(min_size, max_size)
343    } else {
344        Size::NONE
345    };
346
347    drop(style);
348
349    // If both min and max in a given axis are set and max <= min then this determines the size in that axis
350    let min_max_definite_size = min_size.zip_map(max_size, |min, max| match (min, max) {
351        (Some(min), Some(max)) if max <= min => Some(min),
352        _ => None,
353    });
354
355    let styled_based_known_dimensions =
356        known_dimensions.or(min_max_definite_size).or(clamped_style_size).maybe_max(padding_border_size);
357
358    // Short-circuit layout if the container's size is fully determined by the container's size and the run mode
359    // is ComputeSize (and thus the container's size is all that we're interested in)
360    if run_mode == RunMode::ComputeSize {
361        if let Size { width: Some(width), height: Some(height) } = styled_based_known_dimensions {
362            return LayoutOutput::from_outer_size(Size { width, height });
363        }
364
365        // We can also short-circuit if the width is known and only the width has been requested.
366        if inputs.axis == RequestedAxis::Horizontal {
367            if let Some(width) = styled_based_known_dimensions.width {
368                return LayoutOutput::from_outer_size(Size { width, height: 0.0 });
369            }
370        }
371    }
372
373    // Unwrap the block formatting context if one was passed, or else create a new one
374    debug_log!("BLOCK");
375    match block_ctx {
376        Some(inherited_bfc) if !is_scroll_container => compute_inner(
377            tree,
378            node_id,
379            LayoutInput { known_dimensions: styled_based_known_dimensions, ..inputs },
380            inherited_bfc,
381        ),
382        _ => {
383            let mut root_bfc = BlockFormattingContext::new();
384            let mut root_ctx = root_bfc.root_block_context();
385            compute_inner(
386                tree,
387                node_id,
388                LayoutInput { known_dimensions: styled_based_known_dimensions, ..inputs },
389                &mut root_ctx,
390            )
391        }
392    }
393}
394
395/// Computes the layout of [`LayoutBlockContainer`] according to the block layout algorithm
396fn compute_inner(
397    tree: &mut impl LayoutBlockContainer,
398    node_id: NodeId,
399    inputs: LayoutInput,
400    #[allow(unused_mut)] mut block_ctx: &mut BlockContext<'_>,
401) -> LayoutOutput {
402    let LayoutInput {
403        known_dimensions, parent_size, available_space, run_mode, vertical_margins_are_collapsible, ..
404    } = inputs;
405
406    let style = tree.get_block_container_style(node_id);
407    let raw_padding = style.padding();
408    let raw_border = style.border();
409    let raw_margin = style.margin();
410    let aspect_ratio = style.aspect_ratio();
411    let padding = raw_padding.resolve_or_zero(parent_size.width, |val, basis| tree.calc(val, basis));
412    let border = raw_border.resolve_or_zero(parent_size.width, |val, basis| tree.calc(val, basis));
413    let direction = style.direction();
414
415    // Scrollbar gutters are reserved when the `overflow` property is set to `Overflow::Scroll`.
416    // However, the axis are switched (transposed) because a node that scrolls vertically needs
417    // *horizontal* space to be reserved for a scrollbar
418    let scrollbar_gutter = {
419        let offsets = style.overflow().transpose().map(|overflow| match overflow {
420            Overflow::Scroll => style.scrollbar_width(),
421            _ => 0.0,
422        });
423        match direction {
424            Direction::Ltr => Rect { top: 0.0, left: 0.0, right: offsets.x, bottom: offsets.y },
425            Direction::Rtl => Rect { top: 0.0, left: offsets.x, right: 0.0, bottom: offsets.y },
426        }
427    };
428    let padding_border = padding + border;
429    let padding_border_size = padding_border.sum_axes();
430    let content_box_inset = padding_border + scrollbar_gutter;
431
432    // Apply content box inset
433    #[cfg(feature = "float_layout")]
434    block_ctx.apply_content_box_inset([content_box_inset.left, content_box_inset.right]);
435
436    let box_sizing_adjustment =
437        if style.box_sizing() == BoxSizing::ContentBox { padding_border_size } else { Size::ZERO };
438    let size = style
439        .size()
440        .maybe_resolve(parent_size, |val, basis| tree.calc(val, basis))
441        .maybe_apply_aspect_ratio(aspect_ratio)
442        .maybe_add(box_sizing_adjustment);
443    let min_size = style
444        .min_size()
445        .maybe_resolve(parent_size, |val, basis| tree.calc(val, basis))
446        .maybe_apply_aspect_ratio(aspect_ratio)
447        .maybe_add(box_sizing_adjustment);
448    let max_size = style
449        .max_size()
450        .maybe_resolve(parent_size, |val, basis| tree.calc(val, basis))
451        .maybe_apply_aspect_ratio(aspect_ratio)
452        .maybe_add(box_sizing_adjustment);
453
454    // css-sizing-4: a definite size in one axis transfers through `aspect-ratio`
455    // to make the other definite. Deriving it from `known_dimensions` self-gates
456    // the transfer — a block parent fills an axis only when it's a real
457    // constraint (e.g. the stretched width at final layout) and leaves it None
458    // while probing intrinsic sizes, so measure passes stay content-based. Only a
459    // newly-filled axis is adopted (and clamped); an incoming known size is left
460    // as the parent resolved it (re-clamping would undo padding/border overrides).
461    let known_dimensions = {
462        let derived = known_dimensions.maybe_apply_aspect_ratio(aspect_ratio).maybe_clamp(min_size, max_size);
463        Size { width: known_dimensions.width.or(derived.width), height: known_dimensions.height.or(derived.height) }
464    };
465    let container_content_box_size = known_dimensions.maybe_sub(content_box_inset.sum_axes());
466
467    let overflow = style.overflow();
468    let is_scroll_container = overflow.x.is_scroll_container() || overflow.y.is_scroll_container();
469
470    // Determine margin collapsing behaviour
471    let own_margins_collapse_with_children = Line {
472        start: vertical_margins_are_collapsible.start
473            && !is_scroll_container
474            && style.position() == Position::Relative
475            && padding.top == 0.0
476            && border.top == 0.0,
477        end: vertical_margins_are_collapsible.end
478            && !is_scroll_container
479            && style.position() == Position::Relative
480            && padding.bottom == 0.0
481            && border.bottom == 0.0
482            && size.height.is_none(),
483    };
484    let has_styles_preventing_being_collapsed_through = !style.is_block()
485        || block_ctx.is_bfc_root()
486        || is_scroll_container
487        || style.position() == Position::Absolute
488        || padding.top > 0.0
489        || padding.bottom > 0.0
490        || border.top > 0.0
491        || border.bottom > 0.0
492        || matches!(size.height, Some(h) if h > 0.0)
493        || matches!(min_size.height, Some(h) if h > 0.0);
494
495    let text_align = style.text_align();
496    let align_content = style.align_content();
497    drop(style);
498
499    // 1. Generate items
500    let mut items = generate_item_list(tree, node_id, container_content_box_size);
501
502    // 2. Compute container width
503    let container_outer_width = known_dimensions.width.unwrap_or_else(|| {
504        let available_width = available_space.width.maybe_sub(content_box_inset.horizontal_axis_sum());
505        let intrinsic_width = determine_content_based_container_width(tree, &items, available_width)
506            + content_box_inset.horizontal_axis_sum();
507        intrinsic_width.maybe_clamp(min_size.width, max_size.width).maybe_max(Some(padding_border_size.width))
508    });
509
510    // Short-circuit if computing size and both dimensions known
511    if let (RunMode::ComputeSize, Some(container_outer_height)) = (run_mode, known_dimensions.height) {
512        return LayoutOutput::from_outer_size(Size { width: container_outer_width, height: container_outer_height });
513    }
514
515    // We can also short-circuit if the width is known and only the width has been requested.
516    if run_mode == RunMode::ComputeSize && inputs.axis == RequestedAxis::Horizontal {
517        return LayoutOutput::from_outer_size(Size { width: container_outer_width, height: 0.0 });
518    }
519
520    let container_percentage_resolution_height =
521        known_dimensions.height.or(size.height.maybe_max(min_size.height)).or(min_size.height);
522
523    // 3. Perform final item layout and return content height
524    let resolved_padding = raw_padding.resolve_or_zero(Some(container_outer_width), |val, basis| tree.calc(val, basis));
525    let resolved_border = raw_border.resolve_or_zero(Some(container_outer_width), |val, basis| tree.calc(val, basis));
526    let resolved_content_box_inset = resolved_padding + resolved_border + scrollbar_gutter;
527    #[cfg_attr(not(feature = "content_size"), allow(unused_mut))]
528    let (mut inflow_content_size, mut intrinsic_outer_height, first_child_top_margin_set, last_child_bottom_margin_set) =
529        perform_final_layout_on_in_flow_children(
530            tree,
531            run_mode,
532            &mut items,
533            container_outer_width,
534            container_percentage_resolution_height,
535            content_box_inset,
536            resolved_content_box_inset,
537            text_align,
538            direction,
539            own_margins_collapse_with_children,
540            block_ctx,
541        );
542
543    // Root BFCs contain floats
544    #[cfg(feature = "float_layout")]
545    if block_ctx.is_bfc_root() || is_scroll_container {
546        intrinsic_outer_height = intrinsic_outer_height.max(block_ctx.floated_content_height_contribution());
547    }
548
549    let container_outer_height = known_dimensions
550        .height
551        .unwrap_or(intrinsic_outer_height.maybe_clamp(min_size.height, max_size.height))
552        .maybe_max(Some(padding_border_size.height));
553    let final_outer_size = Size { width: container_outer_width, height: container_outer_height };
554
555    // Apply `align-content` to in-flow non-floated items if requested. The per-item layouts were
556    // held back in `item.final_layout` so that this step can shift `location.y` before tree commit.
557    //
558    // For block layout the entire stack of in-flow children is treated as a single alignment
559    // subject. That means distribution keywords (`space-between`, `space-around`,
560    // `space-evenly`, `stretch`) must invoke the single-subject fallback unconditionally —
561    // which is what passing `num_items = 1` to `apply_alignment_fallback` does. The whole
562    // group then shifts by one offset, with zero inter-item gap.
563    if let Some(align_content) = align_content {
564        let container_inner_height = container_outer_height - resolved_content_box_inset.vertical_axis_sum();
565        let inflow_content_height = intrinsic_outer_height - resolved_content_box_inset.vertical_axis_sum();
566        let free_space = container_inner_height - inflow_content_height;
567        let any_in_flow = items.iter().any(|item| item.final_layout.is_some());
568        if any_in_flow {
569            let keyword = apply_alignment_fallback(free_space, 1, align_content);
570            let group_offset = compute_alignment_offset(free_space, 1, 0.0, keyword, false, true);
571            for item in items.iter_mut() {
572                if let Some(layout) = item.final_layout.as_mut() {
573                    layout.location.y += group_offset;
574                }
575            }
576
577            #[cfg(feature = "content_size")]
578            {
579                inflow_content_size = Size::ZERO;
580                for item in items.iter() {
581                    if let Some(layout) = item.final_layout.as_ref() {
582                        inflow_content_size = inflow_content_size.f32_max(compute_content_size_contribution(
583                            layout.location
584                                + Point { x: -resolved_content_box_inset.left, y: -resolved_content_box_inset.top },
585                            layout.size,
586                            layout.content_size,
587                            item.overflow,
588                        ));
589                    }
590                }
591            }
592        }
593    }
594
595    // Determine whether this node can be collapsed through
596    let all_in_flow_children_can_be_collapsed_through = items.iter().all(|item| {
597        #[cfg(feature = "float_layout")]
598        if item.float.is_floated() {
599            return true;
600        }
601        item.position == Position::Absolute || item.can_be_collapsed_through
602    });
603    let can_be_collapsed_through =
604        !has_styles_preventing_being_collapsed_through && all_in_flow_children_can_be_collapsed_through;
605
606    let mut output = LayoutOutput {
607        size: final_outer_size,
608        #[cfg(feature = "content_size")]
609        content_size: Size::ZERO,
610        first_baselines: Point::NONE,
611        top_margin: if own_margins_collapse_with_children.start {
612            first_child_top_margin_set
613        } else {
614            let margin_top = raw_margin.top.resolve_or_zero(parent_size.width, |val, basis| tree.calc(val, basis));
615            CollapsibleMarginSet::from_margin(margin_top)
616        },
617        bottom_margin: if own_margins_collapse_with_children.end {
618            last_child_bottom_margin_set
619        } else {
620            let margin_bottom =
621                raw_margin.bottom.resolve_or_zero(parent_size.width, |val, basis| tree.calc(val, basis));
622            CollapsibleMarginSet::from_margin(margin_bottom)
623        },
624        margins_can_collapse_through: can_be_collapsed_through,
625    };
626
627    // Short-circuit if computing size.
628    //
629    // Note: it is important that we return the margin-collapsing related outputs here as Parent block containers
630    // rely on the `top_margin`/`bottom_margin` of their children to compute their own intrinsic height.
631    if run_mode == RunMode::ComputeSize {
632        return output;
633    }
634
635    // Commit deferred in-flow layouts to the tree. Floated items already wrote their own layouts.
636    for item in items.iter() {
637        if let Some(layout) = item.final_layout.as_ref() {
638            tree.set_unrounded_layout(item.node_id, layout);
639        }
640    }
641
642    // 4. Layout absolutely positioned children
643    let absolute_position_inset = resolved_border + scrollbar_gutter;
644    let absolute_position_area = final_outer_size - absolute_position_inset.sum_axes();
645    let absolute_position_offset = Point { x: absolute_position_inset.left, y: absolute_position_inset.top };
646    let absolute_content_size = perform_absolute_layout_on_absolute_children(
647        tree,
648        &items,
649        absolute_position_area,
650        absolute_position_offset,
651        direction,
652    );
653
654    #[cfg(feature = "content_size")]
655    {
656        output.content_size = inflow_content_size.f32_max(absolute_content_size);
657    }
658
659    // 5. Perform hidden layout on hidden children
660    let len = tree.child_count(node_id);
661    for order in 0..len {
662        let child = tree.get_child_id(node_id, order);
663        let child_style = tree.get_block_child_style(child);
664        if child_style.box_generation_mode() == BoxGenerationMode::None {
665            drop(child_style);
666            tree.set_unrounded_layout(child, &Layout::with_order(order as u32));
667            tree.perform_child_layout(
668                child,
669                Size::NONE,
670                Size::NONE,
671                Size::MAX_CONTENT,
672                SizingMode::InherentSize,
673                Line::FALSE,
674            );
675        }
676    }
677
678    output
679}
680
681/// Create a `Vec` of `BlockItem` structs where each item in the `Vec` represents a child of the current node
682#[inline]
683fn generate_item_list(
684    tree: &impl LayoutBlockContainer,
685    node: NodeId,
686    node_inner_size: Size<Option<f32>>,
687) -> Vec<BlockItem> {
688    tree.child_ids(node)
689        .map(|child_node_id| (child_node_id, tree.get_block_child_style(child_node_id)))
690        .filter(|(_, style)| style.box_generation_mode() != BoxGenerationMode::None)
691        .enumerate()
692        .map(|(order, (child_node_id, child_style))| {
693            let aspect_ratio = child_style.aspect_ratio();
694            let padding = child_style.padding().resolve_or_zero(node_inner_size, |val, basis| tree.calc(val, basis));
695            let border = child_style.border().resolve_or_zero(node_inner_size, |val, basis| tree.calc(val, basis));
696            let pb_sum = (padding + border).sum_axes();
697            let box_sizing_adjustment =
698                if child_style.box_sizing() == BoxSizing::ContentBox { pb_sum } else { Size::ZERO };
699
700            let position = child_style.position();
701            let overflow = child_style.overflow();
702
703            #[cfg(feature = "float_layout")]
704            let float = child_style.float();
705            #[cfg(feature = "float_layout")]
706            let is_not_floated = float == Float::None;
707
708            #[cfg(not(feature = "float_layout"))]
709            let is_not_floated = true;
710
711            let is_block = child_style.is_block();
712            let is_table = child_style.is_table();
713            let is_scroll_container = overflow.x.is_scroll_container() || overflow.y.is_scroll_container();
714
715            let is_in_same_bfc: bool =
716                is_block && !is_table && position != Position::Absolute && is_not_floated && !is_scroll_container;
717
718            BlockItem {
719                node_id: child_node_id,
720                order: order as u32,
721                is_table,
722                is_in_same_bfc,
723                #[cfg(feature = "float_layout")]
724                float,
725                #[cfg(feature = "float_layout")]
726                clear: child_style.clear(),
727                size: child_style
728                    .size()
729                    .maybe_resolve(node_inner_size, |val, basis| tree.calc(val, basis))
730                    .maybe_apply_aspect_ratio(aspect_ratio)
731                    .maybe_add(box_sizing_adjustment),
732                min_size: child_style
733                    .min_size()
734                    .maybe_resolve(node_inner_size, |val, basis| tree.calc(val, basis))
735                    .maybe_apply_aspect_ratio(aspect_ratio)
736                    .maybe_add(box_sizing_adjustment),
737                max_size: child_style
738                    .max_size()
739                    .maybe_resolve(node_inner_size, |val, basis| tree.calc(val, basis))
740                    .maybe_apply_aspect_ratio(aspect_ratio)
741                    .maybe_add(box_sizing_adjustment),
742                overflow,
743                scrollbar_width: child_style.scrollbar_width(),
744                position,
745                inset: child_style.inset(),
746                margin: child_style.margin(),
747                padding,
748                border,
749                padding_border_sum: pb_sum,
750
751                // Fields to be computed later (for now we initialise with dummy values)
752                computed_size: Size::zero(),
753                static_position: Point::zero(),
754                can_be_collapsed_through: false,
755                final_layout: None,
756            }
757        })
758        .collect()
759}
760
761/// Compute the content-based width in the case that the width of the container is not known
762#[inline]
763fn determine_content_based_container_width(
764    tree: &mut impl LayoutPartialTree,
765    items: &[BlockItem],
766    available_width: AvailableSpace,
767) -> f32 {
768    let available_space = Size { width: available_width, height: AvailableSpace::MinContent };
769
770    let mut max_child_width = 0.0;
771    #[cfg(feature = "float_layout")]
772    let mut float_contribution = FloatIntrinsicWidthCalculator::new(available_width);
773    for item in items.iter().filter(|item| item.position != Position::Absolute) {
774        let known_dimensions = item.size.maybe_clamp(item.min_size, item.max_size);
775
776        let item_x_margin_sum = item
777            .margin
778            .resolve_or_zero(available_space.width.into_option(), |val, basis| tree.calc(val, basis))
779            .horizontal_axis_sum();
780        let width = known_dimensions.width.unwrap_or_else(|| {
781            tree.measure_child_size(
782                item.node_id,
783                known_dimensions,
784                Size::NONE,
785                available_space.map_width(|w| w.maybe_sub(item_x_margin_sum)),
786                SizingMode::InherentSize,
787                crate::AbsoluteAxis::Horizontal,
788                Line::TRUE,
789            )
790        });
791
792        let width = f32_max(width, item.padding_border_sum.width) + item_x_margin_sum;
793
794        #[cfg(feature = "float_layout")]
795        if let Some(direction) = item.float.float_direction() {
796            float_contribution.add_float(width, direction, item.clear);
797            continue;
798        }
799
800        max_child_width = f32_max(max_child_width, width);
801    }
802
803    #[cfg(feature = "float_layout")]
804    {
805        max_child_width = max_child_width.max(float_contribution.result());
806    }
807
808    max_child_width
809}
810
811/// Compute each child's final size and position
812#[inline]
813#[allow(clippy::too_many_arguments)]
814fn perform_final_layout_on_in_flow_children(
815    tree: &mut impl LayoutBlockContainer,
816    run_mode: RunMode,
817    items: &mut [BlockItem],
818    container_outer_width: f32,
819    container_percentage_resolution_height: Option<f32>,
820    content_box_inset: Rect<f32>,
821    resolved_content_box_inset: Rect<f32>,
822    text_align: TextAlign,
823    direction: Direction,
824    own_margins_collapse_with_children: Line<bool>,
825    block_ctx: &mut BlockContext<'_>,
826) -> (Size<f32>, f32, CollapsibleMarginSet, CollapsibleMarginSet) {
827    // Resolve container_inner_width for sizing child nodes using initial content_box_inset
828    let container_inner_width = container_outer_width - resolved_content_box_inset.horizontal_axis_sum();
829    let container_percentage_resolution_height =
830        container_percentage_resolution_height.maybe_sub(resolved_content_box_inset.vertical_axis_sum());
831    let parent_size = Size { width: Some(container_inner_width), height: container_percentage_resolution_height };
832    // Vertical available space in block flow is indefinite, NOT a min-content
833    // constraint: MaxContent is taffy's representation of "indefinite".
834    // Passing MinContent here made every descendant grid believe it was being
835    // sized under a min-content constraint, in which the maximize-tracks step
836    // has zero free space — so auto rows containing only scroll-container
837    // items (overflow != visible, automatic minimum size = 0) collapsed to
838    // zero height. Browsers size such rows to the item's content.
839    let available_space =
840        Size { width: AvailableSpace::Definite(container_inner_width), height: AvailableSpace::MaxContent };
841
842    // TODO: handle nested blocks with different widths
843    #[cfg(feature = "float_layout")]
844    if block_ctx.is_bfc_root() {
845        block_ctx.set_width(container_outer_width);
846        block_ctx.apply_content_box_inset([resolved_content_box_inset.left, resolved_content_box_inset.right]);
847    }
848
849    // If this block's top margin does not collapse with its children's then the position of its
850    // top margin strut is resolved relative to it, and floats adjoining ancestor struts do not
851    // adjoin this block's strut.
852    #[cfg(feature = "float_layout")]
853    if !own_margins_collapse_with_children.start {
854        block_ctx.commit_strut();
855    }
856
857    #[cfg_attr(not(feature = "content_size"), allow(unused_mut))]
858    let mut inflow_content_size = Size::ZERO;
859    let mut committed_y_offset = resolved_content_box_inset.top;
860    let mut y_offset_for_absolute = resolved_content_box_inset.top;
861    let mut first_child_top_margin_set = CollapsibleMarginSet::ZERO;
862    let mut active_collapsible_margin_set = CollapsibleMarginSet::ZERO;
863    let mut is_collapsing_with_first_margin_set = true;
864    // Whether the active margin set contains the margins of a self-collapsing element with
865    // clearance. Such margins collapse with the margins of following siblings but the resulting
866    // margin does not collapse with the bottom margin of the parent block.
867    let mut active_margin_set_has_clearance = false;
868
869    for item in items.iter_mut() {
870        if item.position == Position::Absolute {
871            let x = match direction {
872                Direction::Ltr => resolved_content_box_inset.left,
873                Direction::Rtl => container_outer_width - resolved_content_box_inset.right,
874            };
875            item.static_position = Point { x, y: y_offset_for_absolute }
876        } else {
877            let item_margin = item
878                .margin
879                .map(|margin| margin.resolve_to_option(container_inner_width, |val, basis| tree.calc(val, basis)));
880            let item_non_auto_margin = item_margin.map(|m| m.unwrap_or(0.0));
881            let item_non_auto_x_margin_sum = item_non_auto_margin.horizontal_axis_sum();
882
883            let scrollbar_size = Size {
884                width: if item.overflow.y == Overflow::Scroll { item.scrollbar_width } else { 0.0 },
885                height: if item.overflow.x == Overflow::Scroll { item.scrollbar_width } else { 0.0 },
886            };
887
888            // Handle floated boxes
889            #[cfg(feature = "float_layout")]
890            if let Some(float_direction) = item.float.float_direction() {
891                let item_layout = tree.perform_child_layout(
892                    item.node_id,
893                    Size::NONE,
894                    parent_size,
895                    // available_space,
896                    Size::MAX_CONTENT,
897                    SizingMode::InherentSize,
898                    Line::TRUE,
899                );
900                let margin_box = item_layout.size + item_non_auto_margin.sum_axes();
901
902                // Floats that occur between collapsing margins are positioned as if they had an otherwise
903                // empty anonymous block parent taking part in the flow, so the pending collapsible margins
904                // contribute to the float's minimum y position (unless those margins collapse with the
905                // container's own top margin, in which case they are applied outside the container).
906                //
907                // In the latter case the position of the float is not fully resolved: margins contributed
908                // by later siblings can still collapse into the strut and move the container (and float).
909                // Such floats force clearance on cleared elements whose margins adjoin the same strut.
910                let adjoins_unresolved_strut =
911                    is_collapsing_with_first_margin_set && own_margins_collapse_with_children.start;
912                let y_offset_for_float = if adjoins_unresolved_strut {
913                    committed_y_offset
914                } else {
915                    committed_y_offset + active_collapsible_margin_set.resolve()
916                };
917
918                let mut location = block_ctx.place_floated_box(
919                    margin_box,
920                    y_offset_for_float,
921                    float_direction,
922                    item.clear,
923                    adjoins_unresolved_strut,
924                );
925
926                // Ensure that content that appears after a float does not get positioned before/above the float
927                //
928                // FIXME: this isn't quite right, because a second float at the same location
929                // shouldn't cause content to push down to it's level
930                // committed_y_offset = committed_y_offset.max(location.y);
931                // y_offset_for_absolute = y_offset_for_absolute.max(location.y);
932
933                // Convert the margin-box location returned by float placement into a border-box location
934                // for the output Layout
935                location.y += item_non_auto_margin.top;
936                location.x += item_non_auto_margin.left;
937
938                // println!("BLOCK FLOATED BOX ({:?}) {:?}", item.node_id, float_direction);
939                // println!("w:{} h:{} x:{}, y:{}", margin_box.width, margin_box.height, location.x, location.y);
940
941                tree.set_unrounded_layout(
942                    item.node_id,
943                    &Layout {
944                        order: item.order,
945                        size: item_layout.size,
946                        #[cfg(feature = "content_size")]
947                        content_size: item_layout.content_size,
948                        scrollbar_size,
949                        location,
950                        padding: item.padding,
951                        border: item.border,
952                        margin: item_non_auto_margin,
953                    },
954                );
955
956                #[cfg(feature = "content_size")]
957                {
958                    // TODO: Should content size of floated boxes count as "inflow_content_size"
959                    // or should it be counted separately?
960                    inflow_content_size = inflow_content_size.f32_max(compute_content_size_contribution(
961                        location,
962                        item_layout.size,
963                        item_layout.content_size,
964                        item.overflow,
965                    ));
966                }
967
968                continue;
969            }
970
971            // Handle non-floated boxes
972
973            let mut y_margin_offset: f32 = 0.0;
974            #[cfg(feature = "float_layout")]
975            let mut bfc_margin_separated = false;
976
977            let (stretch_width, float_avoiding_position, float_avoiding_width) = if item.is_in_same_bfc {
978                let stretch_width = container_inner_width - item_non_auto_x_margin_sum;
979                let position = Point { x: 0.0, y: 0.0 };
980                let width = 0.0;
981
982                (stretch_width, position, width)
983            } else {
984                'block: {
985                    // Set y_margin_offset (different bfc child)
986                    let margins_escape =
987                        is_collapsing_with_first_margin_set && own_margins_collapse_with_children.start;
988                    if !margins_escape {
989                        y_margin_offset =
990                            active_collapsible_margin_set.collapse_with_margin(item_non_auto_margin.top).resolve();
991                    };
992                    let min_y = committed_y_offset + y_margin_offset;
993
994                    #[cfg(feature = "float_layout")]
995                    if block_ctx.has_active_floats(committed_y_offset.min(min_y)) {
996                        let mut slot = block_ctx.find_content_slot(min_y, item.clear, None);
997
998                        // A new formatting context whose border box does not fit beside the floats at its
999                        // hypothetical position (with margins collapsed) must be pushed below the floats,
1000                        // separating its margin from them (similar to clearance): the margin no longer
1001                        // collapses with preceding margins and does not move the floats.
1002                        if margins_escape {
1003                            if let Some(item_width) = item.size.width {
1004                                // Only the item's border box must not overlap the floats:
1005                                // its margins may overflow the available space
1006                                let outer_width = item_width;
1007                                // If a float adjoins the strut that this item's margin would collapse into,
1008                                // the collapsed margin would move the float together with the item, so the
1009                                // fit check is made at the strut's unresolved position (where the float is).
1010                                let hypothetical_min_y = if block_ctx.has_adjoining_float(Clear::Both) {
1011                                    committed_y_offset
1012                                } else {
1013                                    committed_y_offset
1014                                        + active_collapsible_margin_set
1015                                            .collapse_with_margin(item_non_auto_margin.top)
1016                                            .resolve()
1017                                };
1018                                let mut candidate = block_ctx.find_content_slot(hypothetical_min_y, item.clear, None);
1019                                if candidate.segment_id.is_some() && outer_width > candidate.width {
1020                                    while candidate.segment_id.is_some() && outer_width > candidate.width {
1021                                        candidate = block_ctx.find_content_slot(
1022                                            hypothetical_min_y,
1023                                            item.clear,
1024                                            candidate.segment_id,
1025                                        );
1026                                    }
1027                                    bfc_margin_separated = true;
1028                                    // The preceding margins (excluding this item's, which is now separated)
1029                                    // still escape and move the container; compensate so that the item's
1030                                    // absolute position stays at the found slot.
1031                                    slot = candidate;
1032                                    slot.y -= active_collapsible_margin_set.resolve();
1033                                }
1034                            }
1035                        }
1036
1037                        let stretch_width = slot.width - item_non_auto_x_margin_sum;
1038                        break 'block (stretch_width, Point { x: slot.x, y: slot.y }, slot.width);
1039                    }
1040
1041                    let stretch_width = container_inner_width - item_non_auto_x_margin_sum;
1042                    break 'block (
1043                        stretch_width,
1044                        Point { x: resolved_content_box_inset.left, y: min_y },
1045                        container_inner_width,
1046                    );
1047                }
1048            };
1049
1050            let known_dimensions = if item.is_table {
1051                Size::NONE
1052            } else {
1053                item.size
1054                    .map_width(|width| {
1055                        // TODO: Allow stretch-sizing to be conditional, as there are exceptions.
1056                        // e.g. Table children of blocks do not stretch fit
1057                        Some(width.unwrap_or(stretch_width).maybe_clamp(item.min_size.width, item.max_size.width))
1058                    })
1059                    .maybe_clamp(item.min_size, item.max_size)
1060            };
1061
1062            //
1063
1064            let inputs = LayoutInput {
1065                run_mode,
1066                sizing_mode: SizingMode::InherentSize,
1067                axis: RequestedAxis::Both,
1068                known_dimensions,
1069                parent_size,
1070                available_space: available_space.map_width(|_| AvailableSpace::Definite(stretch_width)),
1071                vertical_margins_are_collapsible: if item.is_in_same_bfc { Line::TRUE } else { Line::FALSE },
1072            };
1073
1074            #[cfg(feature = "float_layout")]
1075            let clear_threshold = block_ctx.cleared_threshold(item.clear);
1076            #[cfg(feature = "float_layout")]
1077            let clear_pos = clear_threshold.unwrap_or(f32::NEG_INFINITY);
1078            #[cfg(not(feature = "float_layout"))]
1079            let clear_pos = f32::NEG_INFINITY;
1080
1081            let item_layout = if item.is_in_same_bfc {
1082                let width = known_dimensions
1083                    .width
1084                    .expect("Same-bfc child will always have defined width due to stretch sizing");
1085
1086                // TODO: account for auto margins
1087                let inset_left = item_non_auto_margin.left + content_box_inset.left;
1088                let inset_right = container_outer_width - width - inset_left;
1089                let insets = [inset_left, inset_right];
1090
1091                // Compute child layout
1092                let mut child_block_ctx =
1093                    block_ctx.sub_context((y_offset_for_absolute + item_non_auto_margin.top).max(clear_pos), insets);
1094                let output = tree.compute_block_child_layout(item.node_id, inputs, Some(&mut child_block_ctx));
1095
1096                // Extract float contribution from child block context
1097                #[cfg(feature = "float_layout")]
1098                {
1099                    let child_contribution = child_block_ctx.floated_content_height_contribution();
1100                    let child_top_adjoining_floats = child_block_ctx.top_adjoining_floats();
1101                    block_ctx.add_child_floated_content_height_contribution(y_offset_for_absolute + child_contribution);
1102                    // Floats placed while the position of the child's top margin strut was unresolved
1103                    // also adjoin this block's current strut
1104                    block_ctx.merge_adjoining_floats(child_top_adjoining_floats);
1105                }
1106
1107                output
1108            } else {
1109                tree.compute_child_layout(item.node_id, inputs)
1110            };
1111            let final_size = item_layout.size;
1112
1113            let top_margin_set = item_layout.top_margin.collapse_with_margin(item_margin.top.unwrap_or(0.0));
1114            let bottom_margin_set = item_layout.bottom_margin.collapse_with_margin(item_margin.bottom.unwrap_or(0.0));
1115
1116            // Expand auto margins to fill available space
1117            // Note: Vertical auto-margins for relatively positioned block items simply resolve to 0.
1118            // See: https://www.w3.org/TR/CSS21/visudet.html#abs-non-replaced-width
1119            let free_x_space = f32_max(0.0, stretch_width - final_size.width);
1120            let x_axis_auto_margin_size = {
1121                let auto_margin_count = item_margin.left.is_none() as u8 + item_margin.right.is_none() as u8;
1122                if auto_margin_count > 0 {
1123                    free_x_space / auto_margin_count as f32
1124                } else {
1125                    0.0
1126                }
1127            };
1128            let resolved_margin = Rect {
1129                left: item_margin.left.unwrap_or(x_axis_auto_margin_size),
1130                right: item_margin.right.unwrap_or(x_axis_auto_margin_size),
1131                top: top_margin_set.resolve(),
1132                bottom: bottom_margin_set.resolve(),
1133            };
1134
1135            // Resolve item inset
1136            let inset = item.inset.zip_size(Size { width: container_inner_width, height: 0.0 }, |p, s| {
1137                p.maybe_resolve(s, |val, basis| tree.calc(val, basis))
1138            });
1139            let inset_offset = Point {
1140                x: if direction.is_rtl() {
1141                    inset.right.map(|x| -x).or(inset.left).unwrap_or(0.0)
1142                } else {
1143                    inset.left.or(inset.right.map(|x| -x)).unwrap_or(0.0)
1144                },
1145                y: inset.top.or(inset.bottom.map(|x| -x)).unwrap_or(0.0),
1146            };
1147
1148            // Set y_margin_offset (same bfc child)
1149            if item.is_in_same_bfc
1150                && (!is_collapsing_with_first_margin_set || !own_margins_collapse_with_children.start)
1151            {
1152                y_margin_offset = active_collapsible_margin_set.collapse_with_set(top_margin_set).resolve()
1153            };
1154
1155            // Compute clearance (CSS2.2 9.5.2). Clearance is introduced if the hypothetical position of the
1156            // item's top border edge (the position it would have with normal margin collapsing) is not past
1157            // the bottom of the relevant floats. When clearance is introduced the item's top margin no longer
1158            // collapses with preceding margins, and the item's border edge is placed at
1159            // `max(float bottom, hypothetical position)`.
1160            #[cfg(feature = "float_layout")]
1161            let mut has_clearance = false;
1162            #[cfg(not(feature = "float_layout"))]
1163            let has_clearance = false;
1164            #[cfg(feature = "float_layout")]
1165            if item.is_in_same_bfc {
1166                if let Some(threshold) = clear_threshold {
1167                    // The hypothetical position always includes the item's collapsed top margin set, even
1168                    // when those margins collapse with the container's own top margin (and are thus applied
1169                    // outside the container): in that case they still move the container (and hence the item)
1170                    // relative to the floats.
1171                    let hypothetical_y =
1172                        committed_y_offset + active_collapsible_margin_set.collapse_with_set(top_margin_set).resolve();
1173                    // Clearance is forced (regardless of the hypothetical position) if a relevant float is
1174                    // adjoining the margin-collapse strut that the item's top margin would collapse into:
1175                    // if the margins were allowed to collapse they would pull the float down with the item,
1176                    // so clearance is inserted to separate the two, placing the item just below the float.
1177                    let forced_clearance = block_ctx.has_adjoining_float(item.clear);
1178                    if forced_clearance || hypothetical_y < threshold {
1179                        has_clearance = true;
1180                        // Clearance stops the item's top margin collapsing with preceding margins. If those
1181                        // preceding margins collapse with the container's own top margin they are applied
1182                        // outside the container (moving it down), so the item's cleared position within the
1183                        // container must be reduced by that amount to keep its absolute position correct.
1184                        let escaped_margin =
1185                            if is_collapsing_with_first_margin_set && own_margins_collapse_with_children.start {
1186                                active_collapsible_margin_set.resolve()
1187                            } else {
1188                                0.0
1189                            };
1190                        y_margin_offset = threshold - committed_y_offset - escaped_margin;
1191                    }
1192                }
1193            }
1194            #[cfg(feature = "float_layout")]
1195            {
1196                // A new-formatting-context item pushed below floats separates its margin like clearance does
1197                has_clearance |= bfc_margin_separated;
1198            }
1199
1200            item.computed_size = item_layout.size;
1201            item.can_be_collapsed_through = item_layout.margins_can_collapse_through && !has_clearance;
1202            item.static_position = if item.is_in_same_bfc {
1203                let uncleared_y = committed_y_offset + active_collapsible_margin_set.resolve();
1204                Point {
1205                    x: match direction {
1206                        Direction::Ltr => resolved_content_box_inset.left,
1207                        Direction::Rtl => container_outer_width - resolved_content_box_inset.right - final_size.width,
1208                    },
1209                    y: uncleared_y.max(clear_pos),
1210                }
1211            } else {
1212                // TODO: handle inset and margins
1213                Point {
1214                    x: match direction {
1215                        Direction::Ltr => float_avoiding_position.x,
1216                        Direction::Rtl => float_avoiding_position.x + float_avoiding_width - final_size.width,
1217                    },
1218                    y: float_avoiding_position.y,
1219                }
1220            };
1221            let mut location = if item.is_in_same_bfc {
1222                Point {
1223                    x: match direction {
1224                        Direction::Ltr => resolved_content_box_inset.left + inset_offset.x + resolved_margin.left,
1225                        Direction::Rtl => {
1226                            container_outer_width
1227                                - resolved_content_box_inset.right
1228                                - final_size.width
1229                                - resolved_margin.right
1230                                + inset_offset.x
1231                        }
1232                    },
1233                    y: committed_y_offset + y_margin_offset + inset_offset.y,
1234                }
1235            } else {
1236                // TODO: handle inset and margins
1237                Point {
1238                    x: match direction {
1239                        Direction::Ltr => float_avoiding_position.x + resolved_margin.left + inset_offset.x,
1240                        Direction::Rtl => {
1241                            float_avoiding_position.x + float_avoiding_width - final_size.width - resolved_margin.right
1242                                + inset_offset.x
1243                        }
1244                    },
1245                    y: float_avoiding_position.y + inset_offset.y,
1246                }
1247            };
1248
1249            // Apply alignment
1250            let item_outer_width = item_layout.size.width + resolved_margin.horizontal_axis_sum();
1251            if item_outer_width < container_inner_width {
1252                let free_x_space = container_inner_width - item_outer_width;
1253                match (text_align, direction) {
1254                    (TextAlign::Auto, _) => {
1255                        // Do nothing
1256                    }
1257                    (TextAlign::LegacyLeft, Direction::Ltr) => {
1258                        // Do nothing. Left aligned by default.
1259                    }
1260                    (TextAlign::LegacyLeft, Direction::Rtl) => location.x -= free_x_space,
1261                    (TextAlign::LegacyRight, Direction::Ltr) => location.x += free_x_space,
1262                    (TextAlign::LegacyRight, Direction::Rtl) => {
1263                        // Do nothing. Right aligned by default.
1264                    }
1265                    (TextAlign::LegacyCenter, Direction::Ltr) => location.x += free_x_space / 2.0,
1266                    (TextAlign::LegacyCenter, Direction::Rtl) => location.x -= free_x_space / 2.0,
1267                }
1268            }
1269
1270            // Defer `set_unrounded_layout` to the post-loop pass in `compute_inner` so that
1271            // `align-content` can shift `location.y` before the layout is committed to the tree.
1272            item.final_layout = Some(Layout {
1273                order: item.order,
1274                size: item_layout.size,
1275                #[cfg(feature = "content_size")]
1276                content_size: item_layout.content_size,
1277                scrollbar_size,
1278                location,
1279                padding: item.padding,
1280                border: item.border,
1281                margin: resolved_margin,
1282            });
1283
1284            #[cfg(feature = "content_size")]
1285            {
1286                inflow_content_size = inflow_content_size.f32_max(compute_content_size_contribution(
1287                    location + Point { x: -resolved_content_box_inset.left, y: -resolved_content_box_inset.top },
1288                    final_size,
1289                    item_layout.content_size,
1290                    item.overflow,
1291                ));
1292            }
1293
1294            // Update first_child_top_margin_set
1295            //
1296            // The top margin of an item with clearance does not collapse with the container's top margin,
1297            // so clearance terminates collapsing without contributing the item's own margins.
1298            if is_collapsing_with_first_margin_set && has_clearance {
1299                is_collapsing_with_first_margin_set = false;
1300            } else if is_collapsing_with_first_margin_set {
1301                if item.can_be_collapsed_through {
1302                    first_child_top_margin_set = first_child_top_margin_set
1303                        .collapse_with_set(top_margin_set)
1304                        .collapse_with_set(bottom_margin_set);
1305                } else {
1306                    first_child_top_margin_set = first_child_top_margin_set.collapse_with_set(top_margin_set);
1307                    is_collapsing_with_first_margin_set = false;
1308                }
1309            }
1310
1311            // Update active_collapsible_margin_set
1312            if item.can_be_collapsed_through {
1313                active_collapsible_margin_set = active_collapsible_margin_set
1314                    .collapse_with_set(top_margin_set)
1315                    .collapse_with_set(bottom_margin_set);
1316                y_offset_for_absolute = committed_y_offset + item_layout.size.height + y_margin_offset;
1317            } else {
1318                committed_y_offset = location.y - inset_offset.y + item_layout.size.height;
1319                // A self-collapsing item with clearance is not collapsed through (its margins do not collapse
1320                // with margins of preceding siblings), but its top and bottom margins still collapse with each
1321                // other and with the margins of following siblings.
1322                if has_clearance && item_layout.margins_can_collapse_through {
1323                    // The element's border edge stays at the cleared position, but its collapsed margin
1324                    // extends below it: the border edge sits `top margin` inside the collapsed margin, so
1325                    // following content is offset by `collapsed margin - top margin` from the border edge.
1326                    committed_y_offset -= top_margin_set.resolve();
1327                    active_collapsible_margin_set = top_margin_set.collapse_with_set(bottom_margin_set);
1328                    active_margin_set_has_clearance = true;
1329                } else {
1330                    active_collapsible_margin_set = bottom_margin_set;
1331                    active_margin_set_has_clearance = false;
1332                }
1333                y_offset_for_absolute = committed_y_offset + active_collapsible_margin_set.resolve();
1334                // Committing in-flow content resolves the position of the current margin-collapse strut,
1335                // so floats placed before this point no longer force clearance
1336                #[cfg(feature = "float_layout")]
1337                block_ctx.commit_strut();
1338            }
1339        }
1340    }
1341
1342    // The margins of a self-collapsing element with clearance do not collapse with the bottom
1343    // margin of the parent block: they extend the parent's content height instead of escaping it
1344    let last_child_bottom_margin_set =
1345        if active_margin_set_has_clearance { CollapsibleMarginSet::ZERO } else { active_collapsible_margin_set };
1346    let bottom_y_margin_offset = if active_margin_set_has_clearance {
1347        active_collapsible_margin_set.resolve()
1348    } else if own_margins_collapse_with_children.end {
1349        0.0
1350    } else {
1351        last_child_bottom_margin_set.resolve()
1352    };
1353
1354    committed_y_offset += resolved_content_box_inset.bottom + bottom_y_margin_offset;
1355    let content_height = f32_max(0.0, committed_y_offset);
1356    (inflow_content_size, content_height, first_child_top_margin_set, last_child_bottom_margin_set)
1357}
1358
1359/// Perform absolute layout on all absolutely positioned children.
1360#[inline]
1361fn perform_absolute_layout_on_absolute_children(
1362    tree: &mut impl LayoutBlockContainer,
1363    items: &[BlockItem],
1364    area_size: Size<f32>,
1365    area_offset: Point<f32>,
1366    direction: Direction,
1367) -> Size<f32> {
1368    let area_width = area_size.width;
1369    let area_height = area_size.height;
1370
1371    #[cfg_attr(not(feature = "content_size"), allow(unused_mut))]
1372    let mut absolute_content_size = Size::ZERO;
1373
1374    for item in items.iter().filter(|item| item.position == Position::Absolute) {
1375        let child_style = tree.get_block_child_style(item.node_id);
1376
1377        // Skip items that are display:none or are not position:absolute
1378        if child_style.box_generation_mode() == BoxGenerationMode::None || child_style.position() != Position::Absolute
1379        {
1380            continue;
1381        }
1382
1383        let aspect_ratio = child_style.aspect_ratio();
1384        let margin =
1385            child_style.margin().map(|margin| margin.resolve_to_option(area_width, |val, basis| tree.calc(val, basis)));
1386        let padding = child_style.padding().resolve_or_zero(Some(area_width), |val, basis| tree.calc(val, basis));
1387        let border = child_style.border().resolve_or_zero(Some(area_width), |val, basis| tree.calc(val, basis));
1388        let padding_border_sum = (padding + border).sum_axes();
1389        let box_sizing_adjustment =
1390            if child_style.box_sizing() == BoxSizing::ContentBox { padding_border_sum } else { Size::ZERO };
1391
1392        // Resolve inset
1393        let left = child_style.inset().left.maybe_resolve(area_width, |val, basis| tree.calc(val, basis));
1394        let right = child_style.inset().right.maybe_resolve(area_width, |val, basis| tree.calc(val, basis));
1395        let top = child_style.inset().top.maybe_resolve(area_height, |val, basis| tree.calc(val, basis));
1396        let bottom = child_style.inset().bottom.maybe_resolve(area_height, |val, basis| tree.calc(val, basis));
1397
1398        // Compute known dimensions from min/max/inherent size styles
1399        let style_size = child_style
1400            .size()
1401            .maybe_resolve(area_size, |val, basis| tree.calc(val, basis))
1402            .maybe_apply_aspect_ratio(aspect_ratio)
1403            .maybe_add(box_sizing_adjustment);
1404        let min_size = child_style
1405            .min_size()
1406            .maybe_resolve(area_size, |val, basis| tree.calc(val, basis))
1407            .maybe_apply_aspect_ratio(aspect_ratio)
1408            .maybe_add(box_sizing_adjustment)
1409            .or(padding_border_sum.map(Some))
1410            .maybe_max(padding_border_sum);
1411        let max_size = child_style
1412            .max_size()
1413            .maybe_resolve(area_size, |val, basis| tree.calc(val, basis))
1414            .maybe_apply_aspect_ratio(aspect_ratio)
1415            .maybe_add(box_sizing_adjustment);
1416        let mut known_dimensions = style_size.maybe_clamp(min_size, max_size);
1417
1418        drop(child_style);
1419
1420        // Fill in width from left/right and reapply aspect ratio if:
1421        //   - Width is not already known
1422        //   - Item has both left and right inset properties set
1423        if let (None, Some(left), Some(right)) = (known_dimensions.width, left, right) {
1424            let new_width_raw = area_width.maybe_sub(margin.left).maybe_sub(margin.right) - left - right;
1425            known_dimensions.width = Some(f32_max(new_width_raw, 0.0));
1426            known_dimensions = known_dimensions.maybe_apply_aspect_ratio(aspect_ratio).maybe_clamp(min_size, max_size);
1427        }
1428
1429        // Fill in height from top/bottom and reapply aspect ratio if:
1430        //   - Height is not already known
1431        //   - Item has both top and bottom inset properties set
1432        if let (None, Some(top), Some(bottom)) = (known_dimensions.height, top, bottom) {
1433            let new_height_raw = area_height.maybe_sub(margin.top).maybe_sub(margin.bottom) - top - bottom;
1434            known_dimensions.height = Some(f32_max(new_height_raw, 0.0));
1435            known_dimensions = known_dimensions.maybe_apply_aspect_ratio(aspect_ratio).maybe_clamp(min_size, max_size);
1436        }
1437
1438        let measured_size = tree.measure_child_size_both(
1439            item.node_id,
1440            known_dimensions,
1441            area_size.map(Some),
1442            Size {
1443                width: AvailableSpace::Definite(area_width.maybe_clamp(min_size.width, max_size.width)),
1444                height: AvailableSpace::Definite(area_height.maybe_clamp(min_size.height, max_size.height)),
1445            },
1446            SizingMode::ContentSize,
1447            Line::FALSE,
1448        );
1449
1450        let final_size = known_dimensions.unwrap_or(measured_size).maybe_clamp(min_size, max_size);
1451
1452        let layout_output = tree.perform_child_layout(
1453            item.node_id,
1454            final_size.map(Some),
1455            area_size.map(Some),
1456            Size {
1457                width: AvailableSpace::Definite(area_width.maybe_clamp(min_size.width, max_size.width)),
1458                height: AvailableSpace::Definite(area_height.maybe_clamp(min_size.height, max_size.height)),
1459            },
1460            SizingMode::ContentSize,
1461            Line::FALSE,
1462        );
1463
1464        let non_auto_margin = Rect {
1465            left: if left.is_some() { margin.left.unwrap_or(0.0) } else { 0.0 },
1466            right: if right.is_some() { margin.right.unwrap_or(0.0) } else { 0.0 },
1467            top: if top.is_some() { margin.top.unwrap_or(0.0) } else { 0.0 },
1468            bottom: if bottom.is_some() { margin.bottom.unwrap_or(0.0) } else { 0.0 },
1469        };
1470
1471        // Expand auto margins to fill available space
1472        // https://www.w3.org/TR/CSS21/visudet.html#abs-non-replaced-width
1473        let auto_margin = {
1474            // Auto margins for absolutely positioned elements in block containers only resolve
1475            // if inset is set. Otherwise they resolve to 0.
1476            let absolute_auto_margin_space = Point {
1477                x: right.map(|right| area_size.width - right - left.unwrap_or(0.0)).unwrap_or(final_size.width),
1478                y: bottom.map(|bottom| area_size.height - bottom - top.unwrap_or(0.0)).unwrap_or(final_size.height),
1479            };
1480            let free_space = Size {
1481                width: absolute_auto_margin_space.x - final_size.width - non_auto_margin.horizontal_axis_sum(),
1482                height: absolute_auto_margin_space.y - final_size.height - non_auto_margin.vertical_axis_sum(),
1483            };
1484
1485            let auto_margin_size = Size {
1486                // If all three of 'left', 'width', and 'right' are 'auto': First set any 'auto' values for 'margin-left' and 'margin-right' to 0.
1487                // Then, if the 'direction' property of the element establishing the static-position containing block is 'ltr' set 'left' to the
1488                // static position and apply rule number three below; otherwise, set 'right' to the static position and apply rule number one below.
1489                //
1490                // If none of the three is 'auto': If both 'margin-left' and 'margin-right' are 'auto', solve the equation under the extra constraint
1491                // that the two margins get equal values, unless this would make them negative, in which case when direction of the containing block is
1492                // 'ltr' ('rtl'), set 'margin-left' ('margin-right') to zero and solve for 'margin-right' ('margin-left'). If one of 'margin-left' or
1493                // 'margin-right' is 'auto', solve the equation for that value. If the values are over-constrained, ignore the value for 'left' (in case
1494                // the 'direction' property of the containing block is 'rtl') or 'right' (in case 'direction' is 'ltr') and solve for that value.
1495                width: {
1496                    let auto_margin_count = margin.left.is_none() as u8 + margin.right.is_none() as u8;
1497                    if auto_margin_count == 2
1498                        && (style_size.width.is_none() || style_size.width.unwrap() >= free_space.width)
1499                    {
1500                        0.0
1501                    } else if auto_margin_count > 0 {
1502                        free_space.width / auto_margin_count as f32
1503                    } else {
1504                        0.0
1505                    }
1506                },
1507                height: {
1508                    let auto_margin_count = margin.top.is_none() as u8 + margin.bottom.is_none() as u8;
1509                    if auto_margin_count == 2
1510                        && (style_size.height.is_none() || style_size.height.unwrap() >= free_space.height)
1511                    {
1512                        0.0
1513                    } else if auto_margin_count > 0 {
1514                        free_space.height / auto_margin_count as f32
1515                    } else {
1516                        0.0
1517                    }
1518                },
1519            };
1520
1521            Rect {
1522                left: margin.left.map(|_| 0.0).unwrap_or(auto_margin_size.width),
1523                right: margin.right.map(|_| 0.0).unwrap_or(auto_margin_size.width),
1524                top: margin.top.map(|_| 0.0).unwrap_or(auto_margin_size.height),
1525                bottom: margin.bottom.map(|_| 0.0).unwrap_or(auto_margin_size.height),
1526            }
1527        };
1528
1529        let resolved_margin = Rect {
1530            left: margin.left.unwrap_or(auto_margin.left),
1531            right: margin.right.unwrap_or(auto_margin.right),
1532            top: margin.top.unwrap_or(auto_margin.top),
1533            bottom: margin.bottom.unwrap_or(auto_margin.bottom),
1534        };
1535
1536        let x_offset = match (left, right) {
1537            (Some(left), Some(right)) => {
1538                if direction.is_rtl() {
1539                    area_size.width - final_size.width - right - resolved_margin.right
1540                } else {
1541                    left + resolved_margin.left
1542                }
1543            }
1544            (Some(left), None) => left + resolved_margin.left,
1545            (None, Some(right)) => area_size.width - final_size.width - right - resolved_margin.right,
1546            (None, None) => {
1547                if direction.is_rtl() {
1548                    item.static_position.x - final_size.width - resolved_margin.right - area_offset.x
1549                } else {
1550                    item.static_position.x + resolved_margin.left - area_offset.x
1551                }
1552            }
1553        };
1554        let location = Point {
1555            x: x_offset + area_offset.x,
1556            y: top
1557                .map(|top| top + resolved_margin.top)
1558                .or(bottom.map(|bottom| area_size.height - final_size.height - bottom - resolved_margin.bottom))
1559                .maybe_add(area_offset.y)
1560                .unwrap_or(item.static_position.y + resolved_margin.top),
1561        };
1562        // Note: axis intentionally switched here as scrollbars take up space in the opposite axis
1563        // to the axis in which scrolling is enabled.
1564        let scrollbar_size = Size {
1565            width: if item.overflow.y == Overflow::Scroll { item.scrollbar_width } else { 0.0 },
1566            height: if item.overflow.x == Overflow::Scroll { item.scrollbar_width } else { 0.0 },
1567        };
1568
1569        tree.set_unrounded_layout(
1570            item.node_id,
1571            &Layout {
1572                order: item.order,
1573                size: final_size,
1574                #[cfg(feature = "content_size")]
1575                content_size: layout_output.content_size,
1576                scrollbar_size,
1577                location,
1578                padding,
1579                border,
1580                margin: resolved_margin,
1581            },
1582        );
1583
1584        #[cfg(feature = "content_size")]
1585        {
1586            let relative_location = Point { x: location.x - area_offset.x, y: location.y - area_offset.y };
1587            absolute_content_size = absolute_content_size.f32_max(compute_content_size_contribution(
1588                relative_location,
1589                final_size,
1590                layout_output.content_size,
1591                item.overflow,
1592            ));
1593        }
1594    }
1595
1596    absolute_content_size
1597}