⎇ df46fcf3630e2e8f4ea2c99bbe261154383a4aab
src/compute/block.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
Fix clearance calculation and its interaction with margin collapsing
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}