Add is_table for block items (#701)
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::{BlockContainerStyle, BlockItemStyle, BoxGenerationMode, BoxSizing, LayoutBlockContainer, TextAlign};
13
14#[cfg(feature = "content_size")]
15use super::common::content_size::compute_content_size_contribution;
16
17/// Per-child data that is accumulated and modified over the course of the layout algorithm
18struct BlockItem {
19 /// The identifier for the associated node
20 node_id: NodeId,
21
22 /// The "source order" of the item. This is the index of the item within the children iterator,
23 /// and controls the order in which the nodes are placed
24 order: u32,
25
26 /// Items that are tables don't have stretch sizing applied to them
27 is_table: bool,
28
29 /// The base size of this item
30 size: Size<Option<f32>>,
31 /// The minimum allowable size of this item
32 min_size: Size<Option<f32>>,
33 /// The maximum allowable size of this item
34 max_size: Size<Option<f32>>,
35
36 /// The overflow style of the item
37 overflow: Point<Overflow>,
38 /// The width of the item's scrollbars (if it has scrollbars)
39 scrollbar_width: f32,
40
41 /// The position style of the item
42 position: Position,
43 /// The final offset of this item
44 inset: Rect<LengthPercentageAuto>,
45 /// The margin of this item
46 margin: Rect<LengthPercentageAuto>,
47 /// The margin of this item
48 padding: Rect<f32>,
49 /// The margin of this item
50 border: Rect<f32>,
51 /// The sum of padding and border for this item
52 padding_border_sum: Size<f32>,
53
54 /// The computed border box size of this item
55 computed_size: Size<f32>,
56 /// The computed "static position" of this item. The static position is the position
57 /// taking into account padding, border, margins, and scrollbar_gutters but not inset
58 static_position: Point<f32>,
59 /// Whether margins can be collapsed through this item
60 can_be_collapsed_through: bool,
61}
62
63/// Computes the layout of [`LayoutPartialTree`] according to the block layout algorithm
64pub fn compute_block_layout(
65 tree: &mut impl LayoutBlockContainer,
66 node_id: NodeId,
67 inputs: LayoutInput,
68) -> LayoutOutput {
69 let LayoutInput { known_dimensions, parent_size, run_mode, .. } = inputs;
70 let style = tree.get_block_container_style(node_id);
71
72 // Pull these out earlier to avoid borrowing issues
73 let aspect_ratio = style.aspect_ratio();
74 let padding = style.padding().resolve_or_zero(parent_size.width);
75 let border = style.border().resolve_or_zero(parent_size.width);
76 let padding_border_size = (padding + border).sum_axes();
77 let box_sizing_adjustment =
78 if style.box_sizing() == BoxSizing::ContentBox { padding_border_size } else { Size::ZERO };
79
80 let min_size = style
81 .min_size()
82 .maybe_resolve(parent_size)
83 .maybe_apply_aspect_ratio(aspect_ratio)
84 .maybe_add(box_sizing_adjustment);
85 let max_size = style
86 .max_size()
87 .maybe_resolve(parent_size)
88 .maybe_apply_aspect_ratio(aspect_ratio)
89 .maybe_add(box_sizing_adjustment);
90 let clamped_style_size = if inputs.sizing_mode == SizingMode::InherentSize {
91 style
92 .size()
93 .maybe_resolve(parent_size)
94 .maybe_apply_aspect_ratio(aspect_ratio)
95 .maybe_add(box_sizing_adjustment)
96 .maybe_clamp(min_size, max_size)
97 } else {
98 Size::NONE
99 };
100
101 drop(style);
102
103 // If both min and max in a given axis are set and max <= min then this determines the size in that axis
104 let min_max_definite_size = min_size.zip_map(max_size, |min, max| match (min, max) {
105 (Some(min), Some(max)) if max <= min => Some(min),
106 _ => None,
107 });
108
109 let styled_based_known_dimensions =
110 known_dimensions.or(min_max_definite_size).or(clamped_style_size).maybe_max(padding_border_size);
111
112 // Short-circuit layout if the container's size is fully determined by the container's size and the run mode
113 // is ComputeSize (and thus the container's size is all that we're interested in)
114 if run_mode == RunMode::ComputeSize {
115 if let Size { width: Some(width), height: Some(height) } = styled_based_known_dimensions {
116 return LayoutOutput::from_outer_size(Size { width, height });
117 }
118 }
119
120 debug_log!("BLOCK");
121 compute_inner(tree, node_id, LayoutInput { known_dimensions: styled_based_known_dimensions, ..inputs })
122}
123
124/// Computes the layout of [`LayoutBlockContainer`] according to the block layout algorithm
125fn compute_inner(tree: &mut impl LayoutBlockContainer, node_id: NodeId, inputs: LayoutInput) -> LayoutOutput {
126 let LayoutInput {
127 known_dimensions, parent_size, available_space, run_mode, vertical_margins_are_collapsible, ..
128 } = inputs;
129
130 let style = tree.get_block_container_style(node_id);
131 let raw_padding = style.padding();
132 let raw_border = style.border();
133 let raw_margin = style.margin();
134 let aspect_ratio = style.aspect_ratio();
135 let padding = raw_padding.resolve_or_zero(parent_size.width);
136 let border = raw_border.resolve_or_zero(parent_size.width);
137
138 // Scrollbar gutters are reserved when the `overflow` property is set to `Overflow::Scroll`.
139 // However, the axis are switched (transposed) because a node that scrolls vertically needs
140 // *horizontal* space to be reserved for a scrollbar
141 let scrollbar_gutter = {
142 let offsets = style.overflow().transpose().map(|overflow| match overflow {
143 Overflow::Scroll => style.scrollbar_width(),
144 _ => 0.0,
145 });
146 // TODO: make side configurable based on the `direction` property
147 Rect { top: 0.0, left: 0.0, right: offsets.x, bottom: offsets.y }
148 };
149 let padding_border = padding + border;
150 let padding_border_size = padding_border.sum_axes();
151 let content_box_inset = padding_border + scrollbar_gutter;
152 let container_content_box_size = known_dimensions.maybe_sub(content_box_inset.sum_axes());
153
154 let box_sizing_adjustment =
155 if style.box_sizing() == BoxSizing::ContentBox { padding_border_size } else { Size::ZERO };
156 let size =
157 style.size().maybe_resolve(parent_size).maybe_apply_aspect_ratio(aspect_ratio).maybe_add(box_sizing_adjustment);
158 let min_size = style
159 .min_size()
160 .maybe_resolve(parent_size)
161 .maybe_apply_aspect_ratio(aspect_ratio)
162 .maybe_add(box_sizing_adjustment);
163 let max_size = style
164 .max_size()
165 .maybe_resolve(parent_size)
166 .maybe_apply_aspect_ratio(aspect_ratio)
167 .maybe_add(box_sizing_adjustment);
168
169 // Determine margin collapsing behaviour
170 let own_margins_collapse_with_children = Line {
171 start: vertical_margins_are_collapsible.start
172 && !style.overflow().x.is_scroll_container()
173 && !style.overflow().y.is_scroll_container()
174 && style.position() == Position::Relative
175 && padding.top == 0.0
176 && border.top == 0.0,
177 end: vertical_margins_are_collapsible.end
178 && !style.overflow().x.is_scroll_container()
179 && !style.overflow().y.is_scroll_container()
180 && style.position() == Position::Relative
181 && padding.bottom == 0.0
182 && border.bottom == 0.0
183 && size.height.is_none(),
184 };
185 let has_styles_preventing_being_collapsed_through = !style.is_block()
186 || style.overflow().x.is_scroll_container()
187 || style.overflow().y.is_scroll_container()
188 || style.position() == Position::Absolute
189 || padding.top > 0.0
190 || padding.bottom > 0.0
191 || border.top > 0.0
192 || border.bottom > 0.0
193 || matches!(size.height, Some(h) if h > 0.0)
194 || matches!(min_size.height, Some(h) if h > 0.0);
195
196 let text_align = style.text_align();
197
198 drop(style);
199
200 // 1. Generate items
201 let mut items = generate_item_list(tree, node_id, container_content_box_size);
202
203 // 2. Compute container width
204 let container_outer_width = known_dimensions.width.unwrap_or_else(|| {
205 let available_width = available_space.width.maybe_sub(content_box_inset.horizontal_axis_sum());
206 let intrinsic_width = determine_content_based_container_width(tree, &items, available_width)
207 + content_box_inset.horizontal_axis_sum();
208 intrinsic_width.maybe_clamp(min_size.width, max_size.width).maybe_max(Some(padding_border_size.width))
209 });
210
211 // Short-circuit if computing size and both dimensions known
212 if let (RunMode::ComputeSize, Some(container_outer_height)) = (run_mode, known_dimensions.height) {
213 return LayoutOutput::from_outer_size(Size { width: container_outer_width, height: container_outer_height });
214 }
215
216 // 3. Perform final item layout and return content height
217 let resolved_padding = raw_padding.resolve_or_zero(Some(container_outer_width));
218 let resolved_border = raw_border.resolve_or_zero(Some(container_outer_width));
219 let resolved_content_box_inset = resolved_padding + resolved_border + scrollbar_gutter;
220 let (inflow_content_size, intrinsic_outer_height, first_child_top_margin_set, last_child_bottom_margin_set) =
221 perform_final_layout_on_in_flow_children(
222 tree,
223 &mut items,
224 container_outer_width,
225 content_box_inset,
226 resolved_content_box_inset,
227 text_align,
228 own_margins_collapse_with_children,
229 );
230 let container_outer_height = known_dimensions
231 .height
232 .unwrap_or(intrinsic_outer_height.maybe_clamp(min_size.height, max_size.height))
233 .maybe_max(Some(padding_border_size.height));
234 let final_outer_size = Size { width: container_outer_width, height: container_outer_height };
235
236 // Short-circuit if computing size
237 if run_mode == RunMode::ComputeSize {
238 return LayoutOutput::from_outer_size(final_outer_size);
239 }
240
241 // 4. Layout absolutely positioned children
242 let absolute_position_inset = resolved_border + scrollbar_gutter;
243 let absolute_position_area = final_outer_size - absolute_position_inset.sum_axes();
244 let absolute_position_offset = Point { x: absolute_position_inset.left, y: absolute_position_inset.top };
245 let absolute_content_size =
246 perform_absolute_layout_on_absolute_children(tree, &items, absolute_position_area, absolute_position_offset);
247
248 // 5. Perform hidden layout on hidden children
249 let len = tree.child_count(node_id);
250 for order in 0..len {
251 let child = tree.get_child_id(node_id, order);
252 if tree.get_block_child_style(child).box_generation_mode() == BoxGenerationMode::None {
253 tree.set_unrounded_layout(child, &Layout::with_order(order as u32));
254 tree.perform_child_layout(
255 child,
256 Size::NONE,
257 Size::NONE,
258 Size::MAX_CONTENT,
259 SizingMode::InherentSize,
260 Line::FALSE,
261 );
262 }
263 }
264
265 // 7. Determine whether this node can be collapsed through
266 let all_in_flow_children_can_be_collapsed_through =
267 items.iter().all(|item| item.position == Position::Absolute || item.can_be_collapsed_through);
268 let can_be_collapsed_through =
269 !has_styles_preventing_being_collapsed_through && all_in_flow_children_can_be_collapsed_through;
270
271 #[cfg_attr(not(feature = "content_size"), allow(unused_variables))]
272 let content_size = inflow_content_size.f32_max(absolute_content_size);
273
274 LayoutOutput {
275 size: final_outer_size,
276 #[cfg(feature = "content_size")]
277 content_size,
278 first_baselines: Point::NONE,
279 top_margin: if own_margins_collapse_with_children.start {
280 first_child_top_margin_set
281 } else {
282 let margin_top = raw_margin.top.resolve_or_zero(parent_size.width);
283 CollapsibleMarginSet::from_margin(margin_top)
284 },
285 bottom_margin: if own_margins_collapse_with_children.end {
286 last_child_bottom_margin_set
287 } else {
288 let margin_bottom = raw_margin.bottom.resolve_or_zero(parent_size.width);
289 CollapsibleMarginSet::from_margin(margin_bottom)
290 },
291 margins_can_collapse_through: can_be_collapsed_through,
292 }
293}
294
295/// Create a `Vec` of `BlockItem` structs where each item in the `Vec` represents a child of the current node
296#[inline]
297fn generate_item_list(
298 tree: &impl LayoutBlockContainer,
299 node: NodeId,
300 node_inner_size: Size<Option<f32>>,
301) -> Vec<BlockItem> {
302 tree.child_ids(node)
303 .map(|child_node_id| (child_node_id, tree.get_block_child_style(child_node_id)))
304 .filter(|(_, style)| style.box_generation_mode() != BoxGenerationMode::None)
305 .enumerate()
306 .map(|(order, (child_node_id, child_style))| {
307 let aspect_ratio = child_style.aspect_ratio();
308 let padding = child_style.padding().resolve_or_zero(node_inner_size);
309 let border = child_style.border().resolve_or_zero(node_inner_size);
310 let pb_sum = (padding + border).sum_axes();
311 let box_sizing_adjustment =
312 if child_style.box_sizing() == BoxSizing::ContentBox { pb_sum } else { Size::ZERO };
313 BlockItem {
314 node_id: child_node_id,
315 order: order as u32,
316 is_table: child_style.is_table(),
317 size: child_style
318 .size()
319 .maybe_resolve(node_inner_size)
320 .maybe_apply_aspect_ratio(aspect_ratio)
321 .maybe_add(box_sizing_adjustment),
322 min_size: child_style
323 .min_size()
324 .maybe_resolve(node_inner_size)
325 .maybe_apply_aspect_ratio(aspect_ratio)
326 .maybe_add(box_sizing_adjustment),
327 max_size: child_style
328 .max_size()
329 .maybe_resolve(node_inner_size)
330 .maybe_apply_aspect_ratio(aspect_ratio)
331 .maybe_add(box_sizing_adjustment),
332 overflow: child_style.overflow(),
333 scrollbar_width: child_style.scrollbar_width(),
334 position: child_style.position(),
335 inset: child_style.inset(),
336 margin: child_style.margin(),
337 padding,
338 border,
339 padding_border_sum: pb_sum,
340
341 // Fields to be computed later (for now we initialise with dummy values)
342 computed_size: Size::zero(),
343 static_position: Point::zero(),
344 can_be_collapsed_through: false,
345 }
346 })
347 .collect()
348}
349
350/// Compute the content-based width in the case that the width of the container is not known
351#[inline]
352fn determine_content_based_container_width(
353 tree: &mut impl LayoutPartialTree,
354 items: &[BlockItem],
355 available_width: AvailableSpace,
356) -> f32 {
357 let available_space = Size { width: available_width, height: AvailableSpace::MinContent };
358
359 let mut max_child_width = 0.0;
360 for item in items.iter().filter(|item| item.position != Position::Absolute) {
361 let known_dimensions = item.size.maybe_clamp(item.min_size, item.max_size);
362
363 let width = known_dimensions.width.unwrap_or_else(|| {
364 let item_x_margin_sum =
365 item.margin.resolve_or_zero(available_space.width.into_option()).horizontal_axis_sum();
366 let size_and_baselines = tree.perform_child_layout(
367 item.node_id,
368 known_dimensions,
369 Size::NONE,
370 available_space.map_width(|w| w.maybe_sub(item_x_margin_sum)),
371 SizingMode::InherentSize,
372 Line::TRUE,
373 );
374
375 size_and_baselines.size.width + item_x_margin_sum
376 });
377 let width = f32_max(width, item.padding_border_sum.width);
378
379 max_child_width = f32_max(max_child_width, width);
380 }
381
382 max_child_width
383}
384
385/// Compute each child's final size and position
386#[inline]
387fn perform_final_layout_on_in_flow_children(
388 tree: &mut impl LayoutPartialTree,
389 items: &mut [BlockItem],
390 container_outer_width: f32,
391 content_box_inset: Rect<f32>,
392 resolved_content_box_inset: Rect<f32>,
393 text_align: TextAlign,
394 own_margins_collapse_with_children: Line<bool>,
395) -> (Size<f32>, f32, CollapsibleMarginSet, CollapsibleMarginSet) {
396 // Resolve container_inner_width for sizing child nodes using initial content_box_inset
397 let container_inner_width = container_outer_width - content_box_inset.horizontal_axis_sum();
398 let parent_size = Size { width: Some(container_outer_width), height: None };
399 let available_space =
400 Size { width: AvailableSpace::Definite(container_inner_width), height: AvailableSpace::MinContent };
401
402 #[cfg_attr(not(feature = "content_size"), allow(unused_mut))]
403 let mut inflow_content_size = Size::ZERO;
404 let mut committed_y_offset = resolved_content_box_inset.top;
405 let mut first_child_top_margin_set = CollapsibleMarginSet::ZERO;
406 let mut active_collapsible_margin_set = CollapsibleMarginSet::ZERO;
407 let mut is_collapsing_with_first_margin_set = true;
408 for item in items.iter_mut() {
409 if item.position == Position::Absolute {
410 item.static_position = Point { x: resolved_content_box_inset.left, y: committed_y_offset }
411 } else {
412 let item_margin = item.margin.map(|margin| margin.resolve_to_option(container_outer_width));
413 let item_non_auto_margin = item_margin.map(|m| m.unwrap_or(0.0));
414 let item_non_auto_x_margin_sum = item_non_auto_margin.horizontal_axis_sum();
415 let known_dimensions = if item.is_table {
416 Size::NONE
417 } else {
418 item.size
419 .map_width(|width| {
420 // TODO: Allow stretch-sizing to be conditional, as there are exceptions.
421 // e.g. Table children of blocks do not stretch fit
422 Some(
423 width
424 .unwrap_or(container_inner_width - item_non_auto_x_margin_sum)
425 .maybe_clamp(item.min_size.width, item.max_size.width),
426 )
427 })
428 .maybe_clamp(item.min_size, item.max_size)
429 };
430
431 let item_layout = tree.perform_child_layout(
432 item.node_id,
433 known_dimensions,
434 parent_size,
435 available_space.map_width(|w| w.maybe_sub(item_non_auto_x_margin_sum)),
436 SizingMode::InherentSize,
437 Line::TRUE,
438 );
439 let final_size = item_layout.size;
440
441 let top_margin_set = item_layout.top_margin.collapse_with_margin(item_margin.top.unwrap_or(0.0));
442 let bottom_margin_set = item_layout.bottom_margin.collapse_with_margin(item_margin.bottom.unwrap_or(0.0));
443
444 // Expand auto margins to fill available space
445 // Note: Vertical auto-margins for relatively positioned block items simply resolve to 0.
446 // See: https://www.w3.org/TR/CSS21/visudet.html#abs-non-replaced-width
447 let free_x_space = f32_max(0.0, container_inner_width - final_size.width - item_non_auto_x_margin_sum);
448 let x_axis_auto_margin_size = {
449 let auto_margin_count = item_margin.left.is_none() as u8 + item_margin.right.is_none() as u8;
450 if auto_margin_count > 0 {
451 free_x_space / auto_margin_count as f32
452 } else {
453 0.0
454 }
455 };
456 let resolved_margin = Rect {
457 left: item_margin.left.unwrap_or(x_axis_auto_margin_size),
458 right: item_margin.right.unwrap_or(x_axis_auto_margin_size),
459 top: top_margin_set.resolve(),
460 bottom: bottom_margin_set.resolve(),
461 };
462
463 // Resolve item inset
464 let inset =
465 item.inset.zip_size(Size { width: container_inner_width, height: 0.0 }, |p, s| p.maybe_resolve(s));
466 let inset_offset = Point {
467 x: inset.left.or(inset.right.map(|x| -x)).unwrap_or(0.0),
468 y: inset.top.or(inset.bottom.map(|x| -x)).unwrap_or(0.0),
469 };
470
471 let y_margin_offset = if is_collapsing_with_first_margin_set && own_margins_collapse_with_children.start {
472 0.0
473 } else {
474 active_collapsible_margin_set.collapse_with_margin(resolved_margin.top).resolve()
475 };
476
477 item.computed_size = item_layout.size;
478 item.can_be_collapsed_through = item_layout.margins_can_collapse_through;
479 item.static_position = Point {
480 x: resolved_content_box_inset.left,
481 y: committed_y_offset + active_collapsible_margin_set.resolve(),
482 };
483 let mut location = Point {
484 x: resolved_content_box_inset.left + inset_offset.x + resolved_margin.left,
485 y: committed_y_offset + inset_offset.y + y_margin_offset,
486 };
487
488 // Apply alignment
489 let item_outer_width = item_layout.size.width + resolved_margin.horizontal_axis_sum();
490 if item_outer_width < container_inner_width {
491 match text_align {
492 TextAlign::Auto => {
493 // Do nothing
494 }
495 TextAlign::LegacyLeft => {
496 // Do nothing. Left aligned by default.
497 }
498 TextAlign::LegacyRight => location.x += container_inner_width - item_outer_width,
499 TextAlign::LegacyCenter => location.x += (container_inner_width - item_outer_width) / 2.0,
500 }
501 }
502
503 let scrollbar_size = Size {
504 width: if item.overflow.y == Overflow::Scroll { item.scrollbar_width } else { 0.0 },
505 height: if item.overflow.x == Overflow::Scroll { item.scrollbar_width } else { 0.0 },
506 };
507
508 tree.set_unrounded_layout(
509 item.node_id,
510 &Layout {
511 order: item.order,
512 size: item_layout.size,
513 #[cfg(feature = "content_size")]
514 content_size: item_layout.content_size,
515 scrollbar_size,
516 location,
517 padding: item.padding,
518 border: item.border,
519 margin: resolved_margin,
520 },
521 );
522
523 #[cfg(feature = "content_size")]
524 {
525 inflow_content_size = inflow_content_size.f32_max(compute_content_size_contribution(
526 location,
527 final_size,
528 item_layout.content_size,
529 item.overflow,
530 ));
531 }
532
533 // Update first_child_top_margin_set
534 if is_collapsing_with_first_margin_set {
535 if item.can_be_collapsed_through {
536 first_child_top_margin_set = first_child_top_margin_set
537 .collapse_with_set(top_margin_set)
538 .collapse_with_set(bottom_margin_set);
539 } else {
540 first_child_top_margin_set = first_child_top_margin_set.collapse_with_set(top_margin_set);
541 is_collapsing_with_first_margin_set = false;
542 }
543 }
544
545 // Update active_collapsible_margin_set
546 if item.can_be_collapsed_through {
547 active_collapsible_margin_set = active_collapsible_margin_set
548 .collapse_with_set(top_margin_set)
549 .collapse_with_set(bottom_margin_set);
550 } else {
551 committed_y_offset += item_layout.size.height + y_margin_offset;
552 active_collapsible_margin_set = bottom_margin_set;
553 }
554 }
555 }
556
557 let last_child_bottom_margin_set = active_collapsible_margin_set;
558 let bottom_y_margin_offset =
559 if own_margins_collapse_with_children.end { 0.0 } else { last_child_bottom_margin_set.resolve() };
560
561 committed_y_offset += resolved_content_box_inset.bottom + bottom_y_margin_offset;
562 let content_height = f32_max(0.0, committed_y_offset);
563 (inflow_content_size, content_height, first_child_top_margin_set, last_child_bottom_margin_set)
564}
565
566/// Perform absolute layout on all absolutely positioned children.
567#[inline]
568fn perform_absolute_layout_on_absolute_children(
569 tree: &mut impl LayoutBlockContainer,
570 items: &[BlockItem],
571 area_size: Size<f32>,
572 area_offset: Point<f32>,
573) -> Size<f32> {
574 let area_width = area_size.width;
575 let area_height = area_size.height;
576
577 #[cfg_attr(not(feature = "content_size"), allow(unused_mut))]
578 let mut absolute_content_size = Size::ZERO;
579
580 for item in items.iter().filter(|item| item.position == Position::Absolute) {
581 let child_style = tree.get_block_child_style(item.node_id);
582
583 // Skip items that are display:none or are not position:absolute
584 if child_style.box_generation_mode() == BoxGenerationMode::None || child_style.position() != Position::Absolute
585 {
586 continue;
587 }
588
589 let aspect_ratio = child_style.aspect_ratio();
590 let margin = child_style.margin().map(|margin| margin.resolve_to_option(area_width));
591 let padding = child_style.padding().resolve_or_zero(Some(area_width));
592 let border = child_style.border().resolve_or_zero(Some(area_width));
593 let padding_border_sum = (padding + border).sum_axes();
594 let box_sizing_adjustment =
595 if child_style.box_sizing() == BoxSizing::ContentBox { padding_border_sum } else { Size::ZERO };
596
597 // Resolve inset
598 let left = child_style.inset().left.maybe_resolve(area_width);
599 let right = child_style.inset().right.maybe_resolve(area_width);
600 let top = child_style.inset().top.maybe_resolve(area_height);
601 let bottom = child_style.inset().bottom.maybe_resolve(area_height);
602
603 // Compute known dimensions from min/max/inherent size styles
604 let style_size = child_style
605 .size()
606 .maybe_resolve(area_size)
607 .maybe_apply_aspect_ratio(aspect_ratio)
608 .maybe_add(box_sizing_adjustment);
609 let min_size = child_style
610 .min_size()
611 .maybe_resolve(area_size)
612 .maybe_apply_aspect_ratio(aspect_ratio)
613 .maybe_add(box_sizing_adjustment)
614 .or(padding_border_sum.map(Some))
615 .maybe_max(padding_border_sum);
616 let max_size = child_style
617 .max_size()
618 .maybe_resolve(area_size)
619 .maybe_apply_aspect_ratio(aspect_ratio)
620 .maybe_add(box_sizing_adjustment);
621 let mut known_dimensions = style_size.maybe_clamp(min_size, max_size);
622
623 drop(child_style);
624
625 // Fill in width from left/right and reapply aspect ratio if:
626 // - Width is not already known
627 // - Item has both left and right inset properties set
628 if let (None, Some(left), Some(right)) = (known_dimensions.width, left, right) {
629 let new_width_raw = area_width.maybe_sub(margin.left).maybe_sub(margin.right) - left - right;
630 known_dimensions.width = Some(f32_max(new_width_raw, 0.0));
631 known_dimensions = known_dimensions.maybe_apply_aspect_ratio(aspect_ratio).maybe_clamp(min_size, max_size);
632 }
633
634 // Fill in height from top/bottom and reapply aspect ratio if:
635 // - Height is not already known
636 // - Item has both top and bottom inset properties set
637 if let (None, Some(top), Some(bottom)) = (known_dimensions.height, top, bottom) {
638 let new_height_raw = area_height.maybe_sub(margin.top).maybe_sub(margin.bottom) - top - bottom;
639 known_dimensions.height = Some(f32_max(new_height_raw, 0.0));
640 known_dimensions = known_dimensions.maybe_apply_aspect_ratio(aspect_ratio).maybe_clamp(min_size, max_size);
641 }
642
643 let layout_output = tree.perform_child_layout(
644 item.node_id,
645 known_dimensions,
646 area_size.map(Some),
647 Size {
648 width: AvailableSpace::Definite(area_width.maybe_clamp(min_size.width, max_size.width)),
649 height: AvailableSpace::Definite(area_height.maybe_clamp(min_size.height, max_size.height)),
650 },
651 SizingMode::ContentSize,
652 Line::FALSE,
653 );
654 let measured_size = layout_output.size;
655 let final_size = known_dimensions.unwrap_or(measured_size).maybe_clamp(min_size, max_size);
656
657 let non_auto_margin = Rect {
658 left: if left.is_some() { margin.left.unwrap_or(0.0) } else { 0.0 },
659 right: if right.is_some() { margin.right.unwrap_or(0.0) } else { 0.0 },
660 top: if top.is_some() { margin.top.unwrap_or(0.0) } else { 0.0 },
661 bottom: if bottom.is_some() { margin.left.unwrap_or(0.0) } else { 0.0 },
662 };
663
664 // Expand auto margins to fill available space
665 // https://www.w3.org/TR/CSS21/visudet.html#abs-non-replaced-width
666 let auto_margin = {
667 // Auto margins for absolutely positioned elements in block containers only resolve
668 // if inset is set. Otherwise they resolve to 0.
669 let absolute_auto_margin_space = Point {
670 x: right.map(|right| area_size.width - right - left.unwrap_or(0.0)).unwrap_or(final_size.width),
671 y: bottom.map(|bottom| area_size.height - bottom - top.unwrap_or(0.0)).unwrap_or(final_size.height),
672 };
673 let free_space = Size {
674 width: absolute_auto_margin_space.x - final_size.width - non_auto_margin.horizontal_axis_sum(),
675 height: absolute_auto_margin_space.y - final_size.height - non_auto_margin.vertical_axis_sum(),
676 };
677
678 let auto_margin_size = Size {
679 // If all three of 'left', 'width', and 'right' are 'auto': First set any 'auto' values for 'margin-left' and 'margin-right' to 0.
680 // Then, if the 'direction' property of the element establishing the static-position containing block is 'ltr' set 'left' to the
681 // static position and apply rule number three below; otherwise, set 'right' to the static position and apply rule number one below.
682 //
683 // If none of the three is 'auto': If both 'margin-left' and 'margin-right' are 'auto', solve the equation under the extra constraint
684 // that the two margins get equal values, unless this would make them negative, in which case when direction of the containing block is
685 // 'ltr' ('rtl'), set 'margin-left' ('margin-right') to zero and solve for 'margin-right' ('margin-left'). If one of 'margin-left' or
686 // 'margin-right' is 'auto', solve the equation for that value. If the values are over-constrained, ignore the value for 'left' (in case
687 // the 'direction' property of the containing block is 'rtl') or 'right' (in case 'direction' is 'ltr') and solve for that value.
688 width: {
689 let auto_margin_count = margin.left.is_none() as u8 + margin.right.is_none() as u8;
690 if auto_margin_count == 2
691 && (style_size.width.is_none() || style_size.width.unwrap() >= free_space.width)
692 {
693 0.0
694 } else if auto_margin_count > 0 {
695 free_space.width / auto_margin_count as f32
696 } else {
697 0.0
698 }
699 },
700 height: {
701 let auto_margin_count = margin.top.is_none() as u8 + margin.bottom.is_none() as u8;
702 if auto_margin_count == 2
703 && (style_size.height.is_none() || style_size.height.unwrap() >= free_space.height)
704 {
705 0.0
706 } else if auto_margin_count > 0 {
707 free_space.height / auto_margin_count as f32
708 } else {
709 0.0
710 }
711 },
712 };
713
714 Rect {
715 left: margin.left.map(|_| 0.0).unwrap_or(auto_margin_size.width),
716 right: margin.right.map(|_| 0.0).unwrap_or(auto_margin_size.width),
717 top: margin.top.map(|_| 0.0).unwrap_or(auto_margin_size.height),
718 bottom: margin.bottom.map(|_| 0.0).unwrap_or(auto_margin_size.height),
719 }
720 };
721
722 let resolved_margin = Rect {
723 left: margin.left.unwrap_or(auto_margin.left),
724 right: margin.right.unwrap_or(auto_margin.right),
725 top: margin.top.unwrap_or(auto_margin.top),
726 bottom: margin.bottom.unwrap_or(auto_margin.bottom),
727 };
728
729 let location = Point {
730 x: left
731 .map(|left| left + resolved_margin.left)
732 .or(right.map(|right| area_size.width - final_size.width - right - resolved_margin.right))
733 .maybe_add(area_offset.x)
734 .unwrap_or(item.static_position.x + resolved_margin.left),
735 y: top
736 .map(|top| top + resolved_margin.top)
737 .or(bottom.map(|bottom| area_size.height - final_size.height - bottom - resolved_margin.bottom))
738 .maybe_add(area_offset.y)
739 .unwrap_or(item.static_position.y + resolved_margin.top),
740 };
741 // Note: axis intentionally switched here as scrollbars take up space in the opposite axis
742 // to the axis in which scrolling is enabled.
743 let scrollbar_size = Size {
744 width: if item.overflow.y == Overflow::Scroll { item.scrollbar_width } else { 0.0 },
745 height: if item.overflow.x == Overflow::Scroll { item.scrollbar_width } else { 0.0 },
746 };
747
748 tree.set_unrounded_layout(
749 item.node_id,
750 &Layout {
751 order: item.order,
752 size: final_size,
753 #[cfg(feature = "content_size")]
754 content_size: layout_output.content_size,
755 scrollbar_size,
756 location,
757 padding,
758 border,
759 margin: resolved_margin,
760 },
761 );
762
763 #[cfg(feature = "content_size")]
764 {
765 absolute_content_size = absolute_content_size.f32_max(compute_content_size_contribution(
766 location,
767 final_size,
768 layout_output.content_size,
769 item.overflow,
770 ));
771 }
772 }
773
774 absolute_content_size
775}