1use crate::geometry::{Line, Point, Rect, Size};
8use crate::style::{
9 AlignItemsKeyword, AlignSelf, AlignmentSafety, AvailableSpace, ContainingBlockClaims, CoreStyle, OofItemStyle,
10 Overflow,
11};
12#[cfg(feature = "grid")]
13use crate::tree::DetailedLayoutInfo;
14use crate::tree::{
15 AxisStaticPosition, Layout, LayoutContainingBlock, LayoutInput, LayoutOutput, LayoutPartialTreeExt, NodeId,
16 OofCandidate, OofCandidates, OofPositioningArea, RequestedAxis, RunMode, SizingMode,
17};
18use crate::util::sys::{f32_max, Vec};
19use crate::util::{MaybeMath, MaybeResolve, ResolveOrZero};
20use crate::{AxisStaticEdge, BoxSizing, Direction};
21
22#[cfg(feature = "content_size")]
23use super::common::scrollable_overflow::compute_scrollable_overflow_contribution;
24use super::common::sizing_keyword::resolve_absolute_sizing_keywords;
25
26pub fn resolve_static_offset(
36 static_position: Point<AxisStaticPosition>,
37 final_size: Size<f32>,
38 resolved_margin: Rect<f32>,
39) -> Point<f32> {
40 Point {
41 x: resolve_static_offset_axis(
42 static_position.x,
43 final_size.width,
44 Line { start: resolved_margin.left, end: resolved_margin.right },
45 ),
46 y: resolve_static_offset_axis(
47 static_position.y,
48 final_size.height,
49 Line { start: resolved_margin.top, end: resolved_margin.bottom },
50 ),
51 }
52}
53
54fn resolve_static_offset_axis(sp: AxisStaticPosition, size: f32, margin: Line<f32>) -> f32 {
56 let margin_box_size = size + margin.start + margin.end;
57 let overflows = matches!(sp.align.safety, AlignmentSafety::Safe) && margin_box_size > sp.area.end - sp.area.start;
58 let keyword = if overflows { sp.align.fallback } else { sp.align.keyword };
59 align_in_line(sp.area, margin_box_size, keyword) + margin.start
60}
61
62fn align_in_line(container: Line<f32>, size: f32, edge: AxisStaticEdge) -> f32 {
64 match edge {
65 AxisStaticEdge::Start => container.start,
66 AxisStaticEdge::End => container.end - size,
67 AxisStaticEdge::Center => (container.start + container.end - size) / 2.0,
68 }
69}
70
71#[derive(Copy, Clone, PartialEq, Eq, Debug)]
74enum OofAlignment {
75 Normal,
77 Stretch,
79 Start,
81 Center,
83 End,
85}
86
87impl OofAlignment {
88 fn from_resolved(alignment: AlignSelf) -> Self {
94 debug_assert!(
95 !matches!(alignment.keyword, AlignItemsKeyword::SelfStart | AlignItemsKeyword::SelfEnd),
96 "self-start/self-end must be resolved before computing out-of-flow alignment"
97 );
98 match alignment.keyword {
99 AlignItemsKeyword::Normal => Self::Normal,
100 AlignItemsKeyword::Stretch => Self::Stretch,
101 AlignItemsKeyword::Center => Self::Center,
102 AlignItemsKeyword::End | AlignItemsKeyword::FlexEnd | AlignItemsKeyword::SelfEnd => Self::End,
103 AlignItemsKeyword::Start
105 | AlignItemsKeyword::FlexStart
106 | AlignItemsKeyword::SelfStart
107 | AlignItemsKeyword::Baseline => Self::Start,
108 }
109 }
110
111 fn physical_edge(self, reversed: bool) -> AxisStaticEdge {
114 match (self, reversed) {
115 (Self::Center, _) => AxisStaticEdge::Center,
116 (Self::End, false) | (Self::Normal | Self::Stretch | Self::Start, true) => AxisStaticEdge::End,
117 (Self::End, true) | (Self::Normal | Self::Stretch | Self::Start, false) => AxisStaticEdge::Start,
118 }
119 }
120}
121
122#[derive(Copy, Clone, Debug)]
127struct OofAxis {
128 cb_size: f32,
130 inset: Line<Option<f32>>,
132 margin: Line<Option<f32>>,
134 static_position: AxisStaticPosition,
136 alignment: OofAlignment,
138 safety: AlignmentSafety,
140 is_inline: bool,
142 reversed: bool,
145 is_scroll_axis: bool,
147}
148
149impl OofAxis {
150 fn start_edge(&self) -> AxisStaticEdge {
152 if self.reversed {
153 AxisStaticEdge::End
154 } else {
155 AxisStaticEdge::Start
156 }
157 }
158
159 fn end_edge(&self) -> AxisStaticEdge {
161 if self.reversed {
162 AxisStaticEdge::Start
163 } else {
164 AxisStaticEdge::End
165 }
166 }
167
168 fn inset_modified_containing_block(&self) -> Line<f32> {
171 let (mut imcb, weaker_edge) = match (self.inset.start, self.inset.end) {
172 (Some(start), Some(end)) => (Line { start, end: self.cb_size - end }, self.end_edge()),
173 (Some(start), None) => (Line { start, end: self.cb_size }, AxisStaticEdge::End),
175 (None, Some(end)) => (Line { start: 0.0, end: self.cb_size - end }, AxisStaticEdge::Start),
176 (None, None) => {
179 let sp = self.static_position;
180 let imcb = match sp.align.keyword {
181 AxisStaticEdge::Start => Line { start: sp.area.start, end: self.cb_size },
182 AxisStaticEdge::End => Line { start: 0.0, end: sp.area.end },
183 AxisStaticEdge::Center => {
184 let center = (sp.area.start + sp.area.end) / 2.0;
187 let start_distance = center;
188 let end_distance = self.cb_size - center;
189 if start_distance.abs() <= end_distance.abs() {
190 Line { start: 0.0, end: 2.0 * start_distance }
191 } else {
192 Line { start: self.cb_size - 2.0 * end_distance, end: self.cb_size }
193 }
194 }
195 };
196 (imcb, self.end_edge())
197 }
198 };
199
200 if imcb.end < imcb.start {
203 match weaker_edge {
204 AxisStaticEdge::Start => imcb.start = imcb.end,
205 _ => imcb.end = imcb.start,
206 }
207 }
208 imcb
209 }
210
211 fn inset_modified_containing_block_size(&self) -> f32 {
213 let imcb = self.inset_modified_containing_block();
214 imcb.end - imcb.start
215 }
216
217 fn stretch_fit_size(&self) -> f32 {
220 let non_auto_margin_sum = self.margin.start.unwrap_or(0.0) + self.margin.end.unwrap_or(0.0);
221 f32_max(self.inset_modified_containing_block_size() - non_auto_margin_sum, 0.0)
222 }
223
224 fn is_stretch_sized(&self, is_replaced: bool, is_table: bool) -> bool {
227 let no_auto_inset = self.inset.start.is_some() && self.inset.end.is_some();
228 no_auto_inset
229 && match self.alignment {
230 OofAlignment::Stretch => true,
231 OofAlignment::Normal => !is_replaced && !is_table,
232 _ => false,
233 }
234 }
235
236 fn resolve_margins(&self, size: f32) -> Line<f32> {
239 let both_insets = self.inset.start.is_some() && self.inset.end.is_some();
240 let imcb = self.inset_modified_containing_block();
241 let free_space = imcb.end - imcb.start - size;
242 match (self.margin.start, self.margin.end) {
243 (Some(start), Some(end)) => Line { start, end },
244 _ if !both_insets => Line { start: self.margin.start.unwrap_or(0.0), end: self.margin.end.unwrap_or(0.0) },
246 (Some(start), None) => Line { start, end: free_space - start },
247 (None, Some(end)) => Line { start: free_space - end, end },
248 (None, None) => {
249 if free_space < 0.0 && self.is_inline {
251 if self.reversed {
252 Line { start: free_space, end: 0.0 }
253 } else {
254 Line { start: 0.0, end: free_space }
255 }
256 } else {
257 Line { start: free_space / 2.0, end: free_space / 2.0 }
258 }
259 }
260 }
261 }
262
263 fn position(&self, size: f32, margin: Line<f32>) -> f32 {
266 let imcb = self.inset_modified_containing_block();
267 match (self.inset.start, self.inset.end) {
268 (None, None) => {
270 let sp = self.static_position;
271 let margin_box_size = size + margin.start + margin.end;
272 let offset = if sp.align.safety == AlignmentSafety::Safe && margin_box_size > imcb.end - imcb.start {
273 align_in_line(imcb, margin_box_size, self.start_edge())
274 } else {
275 align_in_line(sp.area, margin_box_size, sp.align.keyword)
276 };
277 offset + margin.start
278 }
279 (Some(_), None) => imcb.start + margin.start,
281 (None, Some(_)) => imcb.end - size - margin.end,
282 (Some(_), Some(_)) => {
283 if self.margin.start.is_none() || self.margin.end.is_none() {
285 return imcb.start + margin.start;
286 }
287
288 let margin_box_size = size + margin.start + margin.end;
289 let imcb_size = imcb.end - imcb.start;
290 let edge = self.alignment.physical_edge(self.reversed);
291 let margin_box_start = match self.safety {
292 AlignmentSafety::Unsafe => align_in_line(imcb, margin_box_size, edge),
293 AlignmentSafety::Safe => {
294 let edge = if margin_box_size > imcb_size { self.start_edge() } else { edge };
295 align_in_line(imcb, margin_box_size, edge)
296 }
297 AlignmentSafety::Default => {
300 if margin_box_size <= imcb_size || self.alignment == OofAlignment::Normal {
301 align_in_line(imcb, margin_box_size, edge)
303 } else {
304 let mut limit = Line { start: imcb.start.min(0.0), end: f32_max(imcb.end, self.cb_size) };
308 if self.is_scroll_axis {
309 if self.reversed {
310 limit.start = f32::NEG_INFINITY;
311 } else {
312 limit.end = f32::INFINITY;
313 }
314 }
315 if margin_box_size <= limit.end - limit.start {
316 let start = align_in_line(imcb, margin_box_size, edge);
319 f32_max(start.min(limit.end - margin_box_size), limit.start)
320 } else {
321 align_in_line(limit, margin_box_size, self.start_edge())
323 }
324 }
325 }
326 };
327 margin_box_start + margin.start
328 }
329 }
330 }
331}
332
333pub fn compute_oof_layout(tree: &mut impl LayoutContainingBlock, node_id: NodeId, output: &mut LayoutOutput) {
349 let Some(area) = output.oof_positioning_area else { return };
350 if output.oof_candidates.is_empty() {
351 tree.clear_hoisted_children(node_id);
352 return;
353 }
354
355 let style = tree.get_core_container_style(node_id);
356 let direction = style.direction();
357 let claims = style.is_containing_block();
358 drop(style);
359
360 let candidates = output.oof_candidates.take();
361 let result = compute_oof_layout_for_area(tree, node_id, candidates.as_slice(), area, direction, claims);
362 tree.clear_hoisted_children(node_id);
365 tree.add_hoisted_children(node_id, &result.hoisted);
366 output.oof_candidates = result.unclaimed;
367 #[cfg(feature = "content_size")]
368 {
369 output.scrollable_overflow_rect = output.scrollable_overflow_rect.union(result.scrollable_overflow_rect);
370 }
371}
372
373pub struct OofLayoutResult {
375 pub hoisted: Vec<NodeId>,
377 pub unclaimed: OofCandidates,
379 pub scrollable_overflow_rect: Rect<f32>,
382}
383
384pub fn compute_oof_layout_for_area(
394 tree: &mut impl LayoutContainingBlock,
395 geometry_owner: NodeId,
396 candidates: &[OofCandidate],
397 area: OofPositioningArea,
398 direction: Direction,
399 claims: ContainingBlockClaims,
400) -> OofLayoutResult {
401 let overflow = tree.get_core_container_style(geometry_owner).overflow();
402
403 let mut hoisted = Vec::new();
404 let mut unclaimed = OofCandidates::new();
405 let scrollable_overflow_rect = perform_oof_layout(
406 tree,
407 geometry_owner,
408 candidates,
409 area.size,
410 area.offset,
411 direction,
412 claims,
413 overflow,
414 &mut hoisted,
415 &mut unclaimed,
416 );
417
418 OofLayoutResult { hoisted, unclaimed, scrollable_overflow_rect }
419}
420
421pub(crate) struct OofBoxLayout {
423 pub layout: Layout,
425 pub surfaced: OofCandidates,
428 #[cfg(feature = "content_size")]
430 pub overflow: Point<Overflow>,
431 #[cfg(feature = "content_size")]
433 pub contain: crate::style::Contain,
434}
435
436#[inline(always)]
448#[allow(clippy::too_many_arguments)]
449pub(crate) fn layout_oof_box<Tree: LayoutContainingBlock>(
450 tree: &mut Tree,
451 candidate: OofCandidate,
452 area_size: Size<f32>,
453 area_offset: Point<f32>,
454 direction: Direction,
455 container_overflow: Point<Overflow>,
456 #[cfg(feature = "grid")] grid_owner: Option<NodeId>,
457 before_layout: impl FnOnce(&mut Tree, &LayoutInput),
458) -> OofBoxLayout {
459 let child_style = tree.get_oof_item_style(candidate.node);
460
461 #[cfg(feature = "grid")]
465 let (area_size, area_offset) = match grid_owner.map(|owner| tree.get_detailed_layout_info(owner)) {
466 Some(DetailedLayoutInfo::Grid(grid_info)) => {
467 let area_rect = Rect {
468 left: area_offset.x,
469 right: area_offset.x + area_size.width,
470 top: area_offset.y,
471 bottom: area_offset.y + area_size.height,
472 };
473 let grid_area = grid_info.resolve_absolute_grid_area(
474 child_style.grid_row(),
475 child_style.grid_column(),
476 direction,
477 area_rect,
478 );
479 (
480 Size { width: grid_area.right - grid_area.left, height: grid_area.bottom - grid_area.top },
481 Point { x: grid_area.left, y: grid_area.top },
482 )
483 }
484 _ => (area_size, area_offset),
485 };
486 let area_width = area_size.width;
487 let area_height = area_size.height;
488
489 let aspect_ratio = child_style.aspect_ratio();
490 let margin =
491 child_style.margin().map(|margin| margin.resolve_to_option(area_width, |val, basis| tree.calc(val, basis)));
492 let padding = child_style.padding().resolve_or_zero(Some(area_width), |val, basis| tree.calc(val, basis));
493 let border = child_style.border().resolve_or_zero(Some(area_width), |val, basis| tree.calc(val, basis));
494 let padding_border_sum = (padding + border).sum_axes();
495 let box_sizing_adjustment =
496 if child_style.box_sizing() == BoxSizing::ContentBox { padding_border_sum } else { Size::ZERO };
497
498 let left = child_style.inset().left.maybe_resolve(area_width, |val, basis| tree.calc(val, basis));
500 let right = child_style.inset().right.maybe_resolve(area_width, |val, basis| tree.calc(val, basis));
501 let top = child_style.inset().top.maybe_resolve(area_height, |val, basis| tree.calc(val, basis));
502 let bottom = child_style.inset().bottom.maybe_resolve(area_height, |val, basis| tree.calc(val, basis));
503
504 let size_style = child_style.size();
506 let style_size = size_style
507 .maybe_resolve(area_size, |val, basis| tree.calc(val, basis))
508 .maybe_apply_aspect_ratio(aspect_ratio)
509 .maybe_add(box_sizing_adjustment);
510 let mut min_size = child_style
511 .min_size()
512 .maybe_resolve(area_size, |val, basis| tree.calc(val, basis))
513 .maybe_apply_aspect_ratio(aspect_ratio)
514 .maybe_add(box_sizing_adjustment)
515 .or(padding_border_sum.map(Some))
516 .maybe_max(padding_border_sum);
517 let max_size = child_style
518 .max_size()
519 .maybe_resolve(area_size, |val, basis| tree.calc(val, basis))
520 .maybe_apply_aspect_ratio(aspect_ratio)
521 .maybe_add(box_sizing_adjustment);
522 let mut known_dimensions = style_size.maybe_clamp(min_size, max_size);
523
524 let is_replaced = child_style.is_replaced();
525 let is_table = child_style.is_table();
526 let item_direction = child_style.direction();
527 let justify_self =
531 child_style.justify_self().unwrap_or(AlignSelf::NORMAL).resolve_self_relative(item_direction, direction, true);
532 let align_self =
533 child_style.align_self().unwrap_or(AlignSelf::NORMAL).resolve_self_relative(item_direction, direction, false);
534 let overflow = child_style.overflow();
535 let scrollbar_width = child_style.scrollbar_width();
536 #[cfg(feature = "content_size")]
537 let contain = child_style.contain();
538
539 drop(child_style);
540
541 let static_position = Point {
544 x: AxisStaticPosition {
545 area: candidate.static_position.x.area.map(|v| v - area_offset.x),
546 align: candidate.static_position.x.align,
547 },
548 y: AxisStaticPosition {
549 area: candidate.static_position.y.area.map(|v| v - area_offset.y),
550 align: candidate.static_position.y.align,
551 },
552 };
553
554 let horizontal = OofAxis {
555 cb_size: area_width,
556 inset: Line { start: left, end: right },
557 margin: Line { start: margin.left, end: margin.right },
558 static_position: static_position.x,
559 alignment: OofAlignment::from_resolved(justify_self),
560 safety: justify_self.safety,
561 is_inline: true,
562 reversed: direction.is_rtl(),
563 is_scroll_axis: container_overflow.x.is_scroll_container(),
564 };
565 let vertical = OofAxis {
566 cb_size: area_height,
567 inset: Line { start: top, end: bottom },
568 margin: Line { start: margin.top, end: margin.bottom },
569 static_position: static_position.y,
570 alignment: OofAlignment::from_resolved(align_self),
571 safety: align_self.safety,
572 is_inline: false,
573 reversed: false,
574 is_scroll_axis: container_overflow.y.is_scroll_container(),
575 };
576 let stretch_fit_size = Size { width: horizontal.stretch_fit_size(), height: vertical.stretch_fit_size() };
577 let mut available_space = stretch_fit_size;
581 if is_table {
582 let non_auto_margin = margin.map(|m| m.unwrap_or(0.0));
583 available_space = available_space.f32_min(Size {
584 width: f32_max(area_width - non_auto_margin.horizontal_axis_sum(), 0.0),
585 height: f32_max(area_height - non_auto_margin.vertical_axis_sum(), 0.0),
586 });
587 }
588 let available_space = available_space.maybe_clamp(min_size, max_size).map(AvailableSpace::Definite);
589
590 if size_style.width.is_sizing_keyword() || size_style.height.is_sizing_keyword() {
594 resolve_absolute_sizing_keywords(
595 tree,
596 candidate.node,
597 &mut known_dimensions,
598 size_style,
599 area_size,
600 Rect { left, right, top, bottom },
601 margin,
602 SizingMode::ContentSize,
603 );
604 known_dimensions = known_dimensions.maybe_apply_aspect_ratio(aspect_ratio).maybe_clamp(min_size, max_size);
605 }
606
607 let stretch_width = known_dimensions.width.is_none() && horizontal.is_stretch_sized(is_replaced, is_table);
617 let stretch_height = known_dimensions.height.is_none() && vertical.is_stretch_sized(is_replaced, is_table);
618 let both_explicit_stretch =
619 horizontal.alignment == OofAlignment::Stretch && vertical.alignment == OofAlignment::Stretch;
620 let stretch_aspect_ratio =
621 if stretch_width && stretch_height && both_explicit_stretch { None } else { aspect_ratio };
622 if stretch_width {
623 if is_table {
624 min_size.width = Some(f32_max(min_size.width.unwrap_or(0.0), stretch_fit_size.width));
626 } else {
627 known_dimensions.width = Some(stretch_fit_size.width);
628 known_dimensions =
629 known_dimensions.maybe_apply_aspect_ratio(stretch_aspect_ratio).maybe_clamp(min_size, max_size);
630 }
631 }
632 if known_dimensions.height.is_none() && stretch_height {
633 if is_table {
634 min_size.height = Some(f32_max(min_size.height.unwrap_or(0.0), stretch_fit_size.height));
635 } else {
636 known_dimensions.height = Some(stretch_fit_size.height);
637 known_dimensions =
638 known_dimensions.maybe_apply_aspect_ratio(stretch_aspect_ratio).maybe_clamp(min_size, max_size);
639 }
640 }
641
642 let final_size = match (known_dimensions.width, known_dimensions.height) {
643 (Some(width), Some(height)) => Size { width, height },
644 _ => {
645 let measured_size = tree.measure_child_size_both(
646 candidate.node,
647 known_dimensions,
648 area_size.map(Some),
649 available_space,
650 SizingMode::ContentSize,
651 Line::FALSE,
652 );
653 known_dimensions.unwrap_or(measured_size)
654 }
655 }
656 .maybe_clamp(min_size, max_size);
657
658 let layout_input = LayoutInput {
659 known_dimensions: final_size.map(Some),
660 known_dimensions_are_definite: Size { width: true, height: true },
661 parent_size: area_size.map(Some),
662 available_space,
663 sizing_mode: SizingMode::ContentSize,
664 axis: RequestedAxis::Both,
665 run_mode: RunMode::PerformLayout,
666 vertical_margins_are_collapsible: Line::FALSE,
667 };
668 before_layout(tree, &layout_input);
669 let mut layout_output = tree.compute_child_layout(candidate.node, layout_input);
670
671 let horizontal_margin = horizontal.resolve_margins(final_size.width);
673 let vertical_margin = vertical.resolve_margins(final_size.height);
674 let resolved_margin = Rect {
675 left: horizontal_margin.start,
676 right: horizontal_margin.end,
677 top: vertical_margin.start,
678 bottom: vertical_margin.end,
679 };
680
681 let location = Point {
683 x: horizontal.position(final_size.width, horizontal_margin) + area_offset.x,
684 y: vertical.position(final_size.height, vertical_margin) + area_offset.y,
685 };
686
687 let scrollbar_size = Size {
690 width: if overflow.y == Overflow::Scroll { scrollbar_width } else { 0.0 },
691 height: if overflow.x == Overflow::Scroll { scrollbar_width } else { 0.0 },
692 };
693
694 let layout = Layout {
695 order: candidate.order,
696 size: final_size,
697 #[cfg(feature = "content_size")]
698 scrollable_overflow_rect: layout_output.scrollable_overflow_rect,
699 scrollbar_size,
700 location,
701 padding,
702 border,
703 margin: resolved_margin,
704 };
705 tree.set_unrounded_layout(candidate.node, &layout);
706
707 OofBoxLayout {
708 layout,
709 surfaced: layout_output.oof_candidates.take(),
710 #[cfg(feature = "content_size")]
711 overflow,
712 #[cfg(feature = "content_size")]
713 contain,
714 }
715}
716
717#[allow(clippy::too_many_arguments)]
740pub(crate) fn perform_oof_layout(
741 tree: &mut impl LayoutContainingBlock,
742 node_id: NodeId,
743 candidates: &[OofCandidate],
744 area_size: Size<f32>,
745 area_offset: Point<f32>,
746 direction: Direction,
747 claims: ContainingBlockClaims,
748 container_overflow: Point<Overflow>,
749 hoisted: &mut Vec<NodeId>,
750 unclaimed: &mut OofCandidates,
751) -> Rect<f32> {
752 #[cfg_attr(not(feature = "content_size"), allow(unused_mut))]
753 let mut absolute_overflow_rect = Rect::ZERO;
754
755 if candidates.is_empty() {
756 return absolute_overflow_rect;
757 }
758
759 #[cfg(feature = "content_size")]
760 let is_scroll_container = container_overflow.x.is_scroll_container() || container_overflow.y.is_scroll_container();
761
762 let mut worklist: Vec<OofCandidate> = Vec::new();
766 for candidate in candidates {
767 if claims.for_position(candidate.position) {
768 worklist.push(*candidate);
769 } else {
770 unclaimed.push(*candidate);
771 }
772 }
773
774 #[cfg(not(feature = "grid"))]
775 let _ = node_id;
776
777 let mut index = 0;
778 while index < worklist.len() {
779 let candidate = worklist[index];
780 index += 1;
781 hoisted.push(candidate.node);
782
783 let OofBoxLayout {
784 layout,
785 surfaced,
786 #[cfg(feature = "content_size")]
787 overflow,
788 #[cfg(feature = "content_size")]
789 contain,
790 } = layout_oof_box(
791 tree,
792 candidate,
793 area_size,
794 area_offset,
795 direction,
796 container_overflow,
797 #[cfg(feature = "grid")]
798 Some(node_id),
799 |_, _| {},
800 );
801 let location = layout.location;
802 let final_size = layout.size;
803
804 let mut surfaced = surfaced;
807 if !surfaced.is_empty() {
808 surfaced.translate(location);
809 for surfaced_candidate in surfaced.iter() {
810 if claims.for_position(surfaced_candidate.position) {
811 worklist.push(*surfaced_candidate);
812 } else {
813 unclaimed.push(*surfaced_candidate);
814 }
815 }
816 }
817
818 #[cfg(feature = "content_size")]
819 {
820 let relative_location = if direction.is_rtl() {
822 Point {
823 x: area_size.width - (location.x - area_offset.x) - final_size.width,
824 y: location.y - area_offset.y,
825 }
826 } else {
827 Point { x: location.x - area_offset.x, y: location.y - area_offset.y }
828 };
829 absolute_overflow_rect = absolute_overflow_rect.union(compute_scrollable_overflow_contribution(
830 relative_location,
831 final_size,
832 layout.scrollable_overflow_rect,
833 overflow,
834 contain,
835 is_scroll_container,
836 ));
837 }
838 }
839
840 absolute_overflow_rect
841}
842
843#[cfg(test)]
844mod tests {
845 use super::resolve_static_offset;
846 use crate::geometry::{Line, Point, Rect, Size};
847 use crate::style::AlignmentSafety;
848 use crate::tree::{AxisStaticAlign, AxisStaticEdge, AxisStaticPosition};
849
850 fn sp(
851 area: Line<f32>,
852 keyword: AxisStaticEdge,
853 safety: AlignmentSafety,
854 fallback: AxisStaticEdge,
855 ) -> AxisStaticPosition {
856 AxisStaticPosition { area, align: AxisStaticAlign { keyword, safety, fallback } }
857 }
858
859 fn resolve_x(sp_x: AxisStaticPosition, width: f32, margin: Rect<f32>) -> f32 {
860 let position = Point { x: sp_x, y: AxisStaticPosition::from_edge(0.0, AxisStaticEdge::Start) };
861 resolve_static_offset(position, Size { width, height: 10.0 }, margin).x
862 }
863
864 const AREA: Line<f32> = Line { start: 10.0, end: 110.0 };
865 const NO_MARGIN: Rect<f32> = Rect { left: 0.0, right: 0.0, top: 0.0, bottom: 0.0 };
866 const MARGIN: Rect<f32> = Rect { left: 5.0, right: 15.0, top: 0.0, bottom: 0.0 };
867
868 #[test]
869 fn start_alignment() {
870 let sp_x = sp(AREA, AxisStaticEdge::Start, AlignmentSafety::Unsafe, AxisStaticEdge::Start);
871 assert_eq!(resolve_x(sp_x, 20.0, NO_MARGIN), 10.0);
872 assert_eq!(resolve_x(sp_x, 20.0, MARGIN), 15.0);
873 }
874
875 #[test]
876 fn end_alignment() {
877 let sp_x = sp(AREA, AxisStaticEdge::End, AlignmentSafety::Unsafe, AxisStaticEdge::End);
878 assert_eq!(resolve_x(sp_x, 20.0, NO_MARGIN), 90.0);
879 assert_eq!(resolve_x(sp_x, 20.0, MARGIN), 75.0);
880 }
881
882 #[test]
883 fn center_alignment() {
884 let sp_x = sp(AREA, AxisStaticEdge::Center, AlignmentSafety::Unsafe, AxisStaticEdge::Center);
885 assert_eq!(resolve_x(sp_x, 20.0, NO_MARGIN), 50.0);
886 assert_eq!(resolve_x(sp_x, 20.0, MARGIN), 45.0);
888 }
889
890 #[test]
891 fn safe_alignment_falls_back_only_on_overflow() {
892 let sp_x = sp(AREA, AxisStaticEdge::End, AlignmentSafety::Safe, AxisStaticEdge::Start);
893 assert_eq!(resolve_x(sp_x, 20.0, NO_MARGIN), 90.0);
895 assert_eq!(resolve_x(sp_x, 100.0, NO_MARGIN), 10.0);
897 assert_eq!(resolve_x(sp_x, 120.0, NO_MARGIN), 10.0);
899 assert_eq!(resolve_x(sp_x, 90.0, MARGIN), 15.0);
900 }
901
902 #[test]
903 fn unsafe_alignment_never_falls_back() {
904 let sp_x = sp(AREA, AxisStaticEdge::End, AlignmentSafety::Unsafe, AxisStaticEdge::Start);
905 assert_eq!(resolve_x(sp_x, 120.0, NO_MARGIN), -10.0);
906 }
907
908 #[test]
909 fn degenerate_area() {
910 let anchor = AxisStaticPosition::from_edge(40.0, AxisStaticEdge::Start);
912 assert_eq!(resolve_x(anchor, 20.0, NO_MARGIN), 40.0);
913 let anchor_end = AxisStaticPosition::from_edge(40.0, AxisStaticEdge::End);
914 assert_eq!(resolve_x(anchor_end, 20.0, NO_MARGIN), 20.0);
915 }
916}