| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/core/layout/ng/flex/ng_flex_layout_algorithm.h" |
| |
| #include <memory> |
| #include "base/optional.h" |
| #include "third_party/blink/renderer/core/layout/flexible_box_algorithm.h" |
| #include "third_party/blink/renderer/core/layout/layout_box.h" |
| #include "third_party/blink/renderer/core/layout/layout_button.h" |
| #include "third_party/blink/renderer/core/layout/layout_flexible_box.h" |
| #include "third_party/blink/renderer/core/layout/ng/flex/ng_flex_child_iterator.h" |
| #include "third_party/blink/renderer/core/layout/ng/geometry/ng_box_strut.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_block_break_token.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_box_fragment.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_constraint_space.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_constraint_space_builder.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_fragment.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_layout_input_node.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_length_utils.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_out_of_flow_layout_part.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_physical_box_fragment.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_space_utils.h" |
| #include "third_party/blink/renderer/core/paint/paint_layer_scrollable_area.h" |
| #include "third_party/blink/renderer/core/style/computed_style_base_constants.h" |
| #include "third_party/blink/renderer/core/style/computed_style_constants.h" |
| #include "third_party/blink/renderer/platform/text/writing_mode.h" |
| #include "third_party/blink/renderer/platform/wtf/vector.h" |
| |
| namespace blink { |
| |
| NGFlexLayoutAlgorithm::NGFlexLayoutAlgorithm( |
| const NGLayoutAlgorithmParams& params) |
| : NGLayoutAlgorithm(params), |
| is_column_(Style().ResolvedIsColumnFlexDirection()), |
| is_horizontal_flow_(FlexLayoutAlgorithm::IsHorizontalFlow(Style())), |
| is_cross_size_definite_(IsContainerCrossSizeDefinite()) { |
| border_box_size_ = container_builder_.InitialBorderBoxSize(); |
| child_percentage_size_ = CalculateChildPercentageSize( |
| ConstraintSpace(), Node(), ChildAvailableSize()); |
| |
| algorithm_.emplace(&Style(), MainAxisContentExtent(LayoutUnit::Max()), |
| child_percentage_size_, &Node().GetDocument()); |
| } |
| |
| bool NGFlexLayoutAlgorithm::MainAxisIsInlineAxis( |
| const NGBlockNode& child) const { |
| return child.Style().IsHorizontalWritingMode() == |
| FlexLayoutAlgorithm::IsHorizontalFlow(Style()); |
| } |
| |
| LayoutUnit NGFlexLayoutAlgorithm::MainAxisContentExtent( |
| LayoutUnit sum_hypothetical_main_size) const { |
| if (Style().ResolvedIsColumnFlexDirection()) { |
| // Even though we only pass border_padding in the third parameter, the |
| // return value includes scrollbar, so subtract scrollbar to get content |
| // size. |
| // We add |border_scrollbar_padding| to the fourth parameter because |
| // |content_size| needs to be the size of the border box. We've overloaded |
| // the term "content". |
| const LayoutUnit border_scrollbar_padding = |
| BorderScrollbarPadding().BlockSum(); |
| return ComputeBlockSizeForFragment( |
| ConstraintSpace(), Style(), BorderPadding(), |
| sum_hypothetical_main_size.ClampNegativeToZero() + |
| border_scrollbar_padding, |
| border_box_size_.inline_size) - |
| border_scrollbar_padding; |
| } |
| return ChildAvailableSize().inline_size; |
| } |
| |
| namespace { |
| |
| enum AxisEdge { kStart, kCenter, kEnd }; |
| |
| // Maps the resolved justify-content value to a static-position edge. |
| AxisEdge MainAxisStaticPositionEdge(const ComputedStyle& style, |
| bool is_column) { |
| const StyleContentAlignmentData justify = |
| FlexLayoutAlgorithm::ResolvedJustifyContent(style); |
| const ContentPosition content_position = justify.GetPosition(); |
| bool is_reverse_flex = is_column |
| ? style.ResolvedIsColumnReverseFlexDirection() |
| : style.ResolvedIsRowReverseFlexDirection(); |
| |
| if (content_position == ContentPosition::kFlexEnd) |
| return is_reverse_flex ? AxisEdge::kStart : AxisEdge::kEnd; |
| |
| if (content_position == ContentPosition::kCenter || |
| justify.Distribution() == ContentDistributionType::kSpaceAround || |
| justify.Distribution() == ContentDistributionType::kSpaceEvenly) |
| return AxisEdge::kCenter; |
| |
| return is_reverse_flex ? AxisEdge::kEnd : AxisEdge::kStart; |
| } |
| |
| // Maps the resolved alignment value to a static-position edge. |
| AxisEdge CrossAxisStaticPositionEdge(const ComputedStyle& style, |
| const ComputedStyle& child_style) { |
| ItemPosition alignment = |
| FlexLayoutAlgorithm::AlignmentForChild(style, child_style); |
| // AlignmentForChild already accounted for wrap-reverse for kFlexStart and |
| // kFlexEnd, but not kStretch. kStretch is supposed to act like kFlexStart. |
| if (style.FlexWrap() == EFlexWrap::kWrapReverse && |
| alignment == ItemPosition::kStretch) { |
| return AxisEdge::kEnd; |
| } |
| |
| if (alignment == ItemPosition::kFlexEnd) |
| return AxisEdge::kEnd; |
| |
| if (alignment == ItemPosition::kCenter) |
| return AxisEdge::kCenter; |
| |
| return AxisEdge::kStart; |
| } |
| |
| } // namespace |
| |
| void NGFlexLayoutAlgorithm::HandleOutOfFlowPositioned(NGBlockNode child) { |
| AxisEdge main_axis_edge = MainAxisStaticPositionEdge(Style(), is_column_); |
| AxisEdge cross_axis_edge = |
| CrossAxisStaticPositionEdge(Style(), child.Style()); |
| |
| AxisEdge inline_axis_edge = is_column_ ? cross_axis_edge : main_axis_edge; |
| AxisEdge block_axis_edge = is_column_ ? main_axis_edge : cross_axis_edge; |
| |
| using InlineEdge = NGLogicalStaticPosition::InlineEdge; |
| using BlockEdge = NGLogicalStaticPosition::BlockEdge; |
| |
| InlineEdge inline_edge; |
| BlockEdge block_edge; |
| LogicalOffset offset = BorderScrollbarPadding().StartOffset(); |
| |
| // Determine the static-position based off the axis-edge. |
| if (inline_axis_edge == AxisEdge::kStart) { |
| inline_edge = InlineEdge::kInlineStart; |
| } else if (inline_axis_edge == AxisEdge::kCenter) { |
| inline_edge = InlineEdge::kInlineCenter; |
| offset.inline_offset += ChildAvailableSize().inline_size / 2; |
| } else { |
| inline_edge = InlineEdge::kInlineEnd; |
| offset.inline_offset += ChildAvailableSize().inline_size; |
| } |
| |
| // We may not know the final block-size of the fragment yet. This will be |
| // adjusted within the |NGContainerFragmentBuilder| once set. |
| if (block_axis_edge == AxisEdge::kStart) { |
| block_edge = BlockEdge::kBlockStart; |
| } else if (block_axis_edge == AxisEdge::kCenter) { |
| block_edge = BlockEdge::kBlockCenter; |
| offset.block_offset -= BorderScrollbarPadding().BlockSum() / 2; |
| } else { |
| block_edge = BlockEdge::kBlockEnd; |
| offset.block_offset -= BorderScrollbarPadding().BlockSum(); |
| } |
| |
| container_builder_.AddOutOfFlowChildCandidate(child, offset, inline_edge, |
| block_edge); |
| } |
| |
| bool NGFlexLayoutAlgorithm::IsColumnContainerMainSizeDefinite() const { |
| DCHECK(is_column_); |
| // If this flex container is also a flex item, it might have a definite size |
| // imposed on it by its parent flex container. |
| // We can't rely on BlockLengthUnresolvable for this case because that |
| // considers Auto as unresolvable even when the block size is fixed and |
| // definite. |
| if (ConstraintSpace().IsFixedBlockSize() && |
| !ConstraintSpace().IsFixedBlockSizeIndefinite()) |
| return true; |
| Length main_size = Style().LogicalHeight(); |
| return !BlockLengthUnresolvable(ConstraintSpace(), main_size, |
| LengthResolvePhase::kLayout); |
| } |
| |
| bool NGFlexLayoutAlgorithm::IsContainerCrossSizeDefinite() const { |
| // A column flexbox's cross axis is an inline size, so is definite. |
| if (is_column_) |
| return true; |
| // If this flex container is also a flex item, it might have a definite size |
| // imposed on it by its parent flex container. |
| // TODO(dgrogan): Removing this check doesn't cause any tests to fail. Remove |
| // it if unneeded or add a test that needs it. |
| if (ConstraintSpace().IsFixedBlockSize() && |
| !ConstraintSpace().IsFixedBlockSizeIndefinite()) |
| return true; |
| |
| return !BlockLengthUnresolvable(ConstraintSpace(), Style().LogicalHeight(), |
| LengthResolvePhase::kLayout); |
| } |
| |
| bool NGFlexLayoutAlgorithm::DoesItemStretch(const NGBlockNode& child) const { |
| if (!DoesItemCrossSizeComputeToAuto(child)) |
| return false; |
| const ComputedStyle& child_style = child.Style(); |
| // https://drafts.csswg.org/css-flexbox/#valdef-align-items-stretch |
| // If the cross size property of the flex item computes to auto, and neither |
| // of the cross-axis margins are auto, the flex item is stretched. |
| if (is_horizontal_flow_ && |
| (child_style.MarginTop().IsAuto() || child_style.MarginBottom().IsAuto())) |
| return false; |
| if (!is_horizontal_flow_ && |
| (child_style.MarginLeft().IsAuto() || child_style.MarginRight().IsAuto())) |
| return false; |
| return FlexLayoutAlgorithm::AlignmentForChild(Style(), child_style) == |
| ItemPosition::kStretch; |
| } |
| |
| bool NGFlexLayoutAlgorithm::IsItemFlexBasisDefinite( |
| const NGBlockNode& child) const { |
| const Length& flex_basis = child.Style().FlexBasis(); |
| DCHECK(!flex_basis.IsAuto()) |
| << "This is never called with flex_basis.IsAuto, but it'd be trivial to " |
| "support."; |
| if (!is_column_) |
| return true; |
| return !BlockLengthUnresolvable(BuildSpaceForFlexBasis(child), flex_basis, |
| LengthResolvePhase::kLayout); |
| } |
| |
| // This behavior is under discussion: the item's pre-flexing main size |
| // definiteness may no longer imply post-flexing definiteness. |
| // TODO(dgrogan): Have https://crbug.com/1003506 and |
| // https://github.com/w3c/csswg-drafts/issues/4305 been resolved yet? |
| bool NGFlexLayoutAlgorithm::IsItemMainSizeDefinite( |
| const NGBlockNode& child) const { |
| DCHECK(is_column_) |
| << "This method doesn't work with row flexboxes because we assume " |
| "main size is block size when we call BlockLengthUnresolvable."; |
| // Inline sizes are always definite. |
| // TODO(dgrogan): The relevant tests, the last two cases in |
| // css/css-flexbox/percentage-heights-003.html passed even without this, so it |
| // may be untested or unnecessary. |
| if (MainAxisIsInlineAxis(child)) |
| return true; |
| // We need a constraint space for the child to determine resolvability and the |
| // space for flex-basis is sufficient, even though it has some unnecessary |
| // stuff (ShrinkToFit and fixed cross sizes). |
| return !BlockLengthUnresolvable(BuildSpaceForFlexBasis(child), |
| child.Style().LogicalHeight(), |
| LengthResolvePhase::kLayout); |
| } |
| |
| bool NGFlexLayoutAlgorithm::IsItemCrossAxisLengthDefinite( |
| const NGBlockNode& child, |
| const Length& length) const { |
| // We don't consider inline value of 'auto' for the cross-axis min/main/max |
| // size to be definite. Block value of 'auto' is always indefinite. |
| if (length.IsAuto()) |
| return false; |
| // But anything else in the inline direction is definite. |
| if (!MainAxisIsInlineAxis(child)) |
| return true; |
| // If we get here, cross axis is block axis. |
| return !BlockLengthUnresolvable(BuildSpaceForFlexBasis(child), length, |
| LengthResolvePhase::kLayout); |
| } |
| |
| bool NGFlexLayoutAlgorithm::DoesItemCrossSizeComputeToAuto( |
| const NGBlockNode& child) const { |
| const ComputedStyle& child_style = child.Style(); |
| if (is_horizontal_flow_) |
| return child_style.Height().IsAuto(); |
| return child_style.Width().IsAuto(); |
| } |
| |
| // This function is used to handle two requirements from the spec. |
| // (1) Calculating flex base size; case 3E at |
| // https://drafts.csswg.org/css-flexbox/#algo-main-item : If a cross size is |
| // needed to determine the main size (e.g. when the flex item’s main size is |
| // in its block axis) and the flex item’s cross size is auto and not |
| // definite, in this calculation use fit-content as the flex item’s cross size. |
| // The flex base size is the item’s resulting main size. |
| // (2) Cross size determination after main size has been calculated. |
| // https://drafts.csswg.org/css-flexbox/#algo-cross-item : Determine the |
| // hypothetical cross size of each item by performing layout with the used main |
| // size and the available space, treating auto as fit-content. |
| bool NGFlexLayoutAlgorithm::ShouldItemShrinkToFit( |
| const NGBlockNode& child) const { |
| if (MainAxisIsInlineAxis(child)) { |
| // In this case, the cross size is in the item's block axis. The item's |
| // block size is never needed to determine its inline size so don't use |
| // fit-content. |
| return false; |
| } |
| if (!child.Style().LogicalWidth().IsAuto()) { |
| DCHECK(!DoesItemCrossSizeComputeToAuto(child)); |
| // The cross size (item's inline size) is already specified, so don't use |
| // fit-content. |
| return false; |
| } |
| DCHECK(DoesItemCrossSizeComputeToAuto(child)); |
| // If execution reaches here, the item's inline size is its cross size and |
| // computes to auto. In that situation, we only don't use fit-content if the |
| // item qualifies for the first case in |
| // https://drafts.csswg.org/css-flexbox/#definite-sizes : |
| // 1. If a single-line flex container has a definite cross size, the outer |
| // cross size of any stretched flex items is the flex container’s inner cross |
| // size (clamped to the flex item’s min and max cross size) and is considered |
| // definite. |
| if (WillChildCrossSizeBeContainerCrossSize(child)) |
| return false; |
| return true; |
| } |
| |
| bool NGFlexLayoutAlgorithm::WillChildCrossSizeBeContainerCrossSize( |
| const NGBlockNode& child) const { |
| return !algorithm_->IsMultiline() && is_cross_size_definite_ && |
| DoesItemStretch(child); |
| } |
| |
| double NGFlexLayoutAlgorithm::GetMainOverCrossAspectRatio( |
| const NGBlockNode& child) const { |
| DCHECK(child.HasAspectRatio()); |
| LogicalSize aspect_ratio = child.GetAspectRatio(); |
| |
| DCHECK_GT(aspect_ratio.inline_size, 0); |
| DCHECK_GT(aspect_ratio.block_size, 0); |
| |
| double ratio = |
| aspect_ratio.inline_size.ToDouble() / aspect_ratio.block_size.ToDouble(); |
| // Multiplying by ratio will take something in the item's block axis and |
| // convert it to the inline axis. We want to convert from cross size to main |
| // size. If block axis and cross axis are the same, then we already have what |
| // we need. Otherwise we need to use the reciprocal. |
| if (!MainAxisIsInlineAxis(child)) |
| ratio = 1 / ratio; |
| return ratio; |
| } |
| |
| LayoutUnit NGFlexLayoutAlgorithm::CalculateFixedCrossSize( |
| const MinMaxSizes& cross_axis_min_max, |
| const NGBoxStrut& margins) const { |
| if (!is_column_) |
| DCHECK_NE(ChildAvailableSize().block_size, kIndefiniteSize); |
| LayoutUnit available_size = is_column_ ? ChildAvailableSize().inline_size |
| : ChildAvailableSize().block_size; |
| LayoutUnit margin_sum = is_column_ ? margins.InlineSum() : margins.BlockSum(); |
| return cross_axis_min_max.ClampSizeToMinAndMax(available_size - margin_sum); |
| } |
| |
| NGConstraintSpace NGFlexLayoutAlgorithm::BuildSpaceForIntrinsicBlockSize( |
| const NGBlockNode& flex_item, |
| const NGPhysicalBoxStrut& physical_margins = NGPhysicalBoxStrut(), |
| const MinMaxSizes& cross_axis_min_max = MinMaxSizes{ |
| kIndefiniteSize, kIndefiniteSize}) const { |
| const ComputedStyle& child_style = flex_item.Style(); |
| NGConstraintSpaceBuilder space_builder(ConstraintSpace(), |
| child_style.GetWritingDirection(), |
| /* is_new_fc */ true); |
| SetOrthogonalFallbackInlineSizeIfNeeded(Style(), flex_item, &space_builder); |
| space_builder.SetCacheSlot(NGCacheSlot::kMeasure); |
| space_builder.SetIsPaintedAtomically(true); |
| |
| NGBoxStrut margins = physical_margins.ConvertToLogical( |
| ConstraintSpace().GetWritingDirection()); |
| LogicalSize child_available_size = ChildAvailableSize(); |
| if (!ShouldItemShrinkToFit(flex_item)) { |
| space_builder.SetStretchInlineSizeIfAuto(true); |
| if (cross_axis_min_max.min_size != kIndefiniteSize && |
| WillChildCrossSizeBeContainerCrossSize(flex_item)) { |
| LayoutUnit cross_size = |
| CalculateFixedCrossSize(cross_axis_min_max, margins); |
| if (is_column_) { |
| space_builder.SetIsFixedInlineSize(true); |
| child_available_size.inline_size = cross_size; |
| } else { |
| space_builder.SetIsFixedBlockSize(true); |
| child_available_size.block_size = cross_size; |
| } |
| } |
| } |
| |
| // For determining the intrinsic block-size we make %-block-sizes resolve |
| // against an indefinite size. |
| LogicalSize child_percentage_size = child_percentage_size_; |
| if (is_column_) |
| child_percentage_size.block_size = kIndefiniteSize; |
| |
| space_builder.SetAvailableSize(child_available_size); |
| space_builder.SetPercentageResolutionSize(child_percentage_size); |
| // TODO(dgrogan): The SetReplacedPercentageResolutionSize calls in this file |
| // may be untested. Write a test or determine why they're unnecessary. |
| space_builder.SetReplacedPercentageResolutionSize(child_percentage_size); |
| return space_builder.ToConstraintSpace(); |
| } |
| |
| NGConstraintSpace NGFlexLayoutAlgorithm::BuildSpaceForFlexBasis( |
| const NGBlockNode& flex_item) const { |
| NGConstraintSpaceBuilder space_builder( |
| ConstraintSpace(), flex_item.Style().GetWritingDirection(), |
| /* is_new_fc */ true); |
| SetOrthogonalFallbackInlineSizeIfNeeded(Style(), flex_item, &space_builder); |
| |
| // This space is only used for resolving lengths, not for layout. We only |
| // need the available and percentage sizes. |
| space_builder.SetAvailableSize(ChildAvailableSize()); |
| space_builder.SetPercentageResolutionSize(child_percentage_size_); |
| space_builder.SetReplacedPercentageResolutionSize(child_percentage_size_); |
| return space_builder.ToConstraintSpace(); |
| } |
| |
| namespace { |
| |
| // This function will be superseded by |
| // NGReplacedLayoutAlgorithm.ComputeMinMaxSizes, when such method exists. |
| LayoutUnit ComputeIntrinsicInlineSizeForAspectRatioElement( |
| const NGBlockNode& node, |
| const NGConstraintSpace& space, |
| const base::Optional<LayoutUnit> definite_block_size, |
| const MinMaxSizes& used_min_max_block_sizes) { |
| DCHECK(node.HasAspectRatio()); |
| LogicalSize aspect_ratio = node.GetAspectRatio(); |
| const ComputedStyle& style = node.Style(); |
| NGBoxStrut border_padding = |
| ComputeBorders(space, node) + ComputePadding(space, style); |
| |
| base::Optional<LayoutUnit> intrinsic_inline; |
| base::Optional<LayoutUnit> intrinsic_block; |
| |
| base::Optional<LayoutUnit> block_size_border_box; |
| if (definite_block_size.has_value()) { |
| block_size_border_box = definite_block_size; |
| } else { |
| node.IntrinsicSize(&intrinsic_inline, &intrinsic_block); |
| if (intrinsic_block) { |
| block_size_border_box = *intrinsic_block + border_padding.BlockSum(); |
| } |
| } |
| |
| if (block_size_border_box) { |
| LayoutUnit clamped_intrinsic_block_border_box = |
| used_min_max_block_sizes.ClampSizeToMinAndMax(*block_size_border_box); |
| return InlineSizeFromAspectRatio(border_padding, aspect_ratio, |
| EBoxSizing::kContentBox, |
| clamped_intrinsic_block_border_box); |
| } |
| |
| if (intrinsic_inline) { |
| MinMaxSizes inline_min_max = ComputeTransferredMinMaxInlineSizes( |
| aspect_ratio, used_min_max_block_sizes, border_padding, |
| EBoxSizing::kContentBox); |
| LayoutUnit intrinsic_inline_border_box = |
| *intrinsic_inline + border_padding.InlineSum(); |
| return inline_min_max.ClampSizeToMinAndMax(intrinsic_inline_border_box); |
| } |
| |
| // If control flow reaches here, the item has aspect ratio only, no natural |
| // sizes. Spec says: |
| // * If the available space is definite in the inline axis, use the stretch |
| // fit into that size for the inline size and calculate the block size using |
| // the aspect ratio. |
| // https://drafts.csswg.org/css-sizing-3/#intrinsic-sizes |
| DCHECK_NE(space.AvailableSize().inline_size, kIndefiniteSize); |
| NGBoxStrut margins = ComputeMarginsForSelf(space, style); |
| return (space.AvailableSize().inline_size - margins.InlineSum()) |
| .ClampNegativeToZero(); |
| } |
| |
| // The value produced by this function will be available via |
| // replaced_node.Layout()->IntrinsicBlockSize(), once NGReplacedLayoutAlgorithm |
| // exists. |
| LayoutUnit ComputeIntrinsicBlockSizeForAspectRatioElement( |
| const NGBlockNode& node, |
| const NGConstraintSpace& space, |
| const base::Optional<LayoutUnit> definite_inline_size, |
| const MinMaxSizes& used_min_max_inline_sizes) { |
| DCHECK(node.HasAspectRatio()); |
| LogicalSize aspect_ratio = node.GetAspectRatio(); |
| const ComputedStyle& style = node.Style(); |
| NGBoxStrut border_padding = |
| ComputeBorders(space, node) + ComputePadding(space, style); |
| |
| base::Optional<LayoutUnit> intrinsic_inline; |
| base::Optional<LayoutUnit> intrinsic_block; |
| |
| base::Optional<LayoutUnit> inline_size_border_box; |
| if (definite_inline_size.has_value()) { |
| inline_size_border_box = definite_inline_size; |
| } else { |
| node.IntrinsicSize(&intrinsic_inline, &intrinsic_block); |
| if (intrinsic_inline) { |
| inline_size_border_box = *intrinsic_inline + border_padding.InlineSum(); |
| } |
| } |
| |
| if (inline_size_border_box) { |
| // Clamping block size to the transferred inline min/max sizes might be |
| // uninentionally unspecified. See |
| // https://github.com/w3c/csswg-drafts/issues/5583 |
| LayoutUnit clamped_intrinsic_inline_border_box = |
| used_min_max_inline_sizes.ClampSizeToMinAndMax(*inline_size_border_box); |
| return BlockSizeFromAspectRatio(border_padding, aspect_ratio, |
| EBoxSizing::kContentBox, |
| clamped_intrinsic_inline_border_box); |
| } |
| |
| if (intrinsic_block) { |
| MinMaxSizes transferred_block_min_max = {LayoutUnit(), LayoutUnit::Max()}; |
| if (used_min_max_inline_sizes.min_size > LayoutUnit()) { |
| transferred_block_min_max.min_size = BlockSizeFromAspectRatio( |
| border_padding, aspect_ratio, EBoxSizing::kContentBox, |
| used_min_max_inline_sizes.min_size); |
| } |
| if (used_min_max_inline_sizes.max_size != LayoutUnit::Max()) { |
| transferred_block_min_max.max_size = BlockSizeFromAspectRatio( |
| border_padding, aspect_ratio, EBoxSizing::kContentBox, |
| used_min_max_inline_sizes.max_size); |
| } |
| // Minimum size wins over maximum size. |
| transferred_block_min_max.max_size = std::max( |
| transferred_block_min_max.max_size, transferred_block_min_max.min_size); |
| LayoutUnit intrinsic_block_border_box = |
| *intrinsic_block + border_padding.BlockSum(); |
| return transferred_block_min_max.ClampSizeToMinAndMax( |
| intrinsic_block_border_box); |
| } |
| |
| // If control flow reaches here, the item has aspect ratio only, no natural |
| // sizes. Spec says: |
| // * If the available space is definite in the inline axis, use the stretch |
| // fit into that size for the inline size and calculate the block size using |
| // the aspect ratio. |
| // https://drafts.csswg.org/css-sizing-3/#intrinsic-sizes |
| DCHECK_NE(space.AvailableSize().inline_size, kIndefiniteSize); |
| NGBoxStrut margins = ComputeMarginsForSelf(space, style); |
| LayoutUnit stretch_into_available_inline_size( |
| (space.AvailableSize().inline_size - margins.InlineSum()) |
| .ClampNegativeToZero()); |
| return BlockSizeFromAspectRatio(border_padding, aspect_ratio, |
| EBoxSizing::kContentBox, |
| stretch_into_available_inline_size); |
| } |
| |
| } // namespace |
| |
| void NGFlexLayoutAlgorithm::ConstructAndAppendFlexItems() { |
| NGFlexChildIterator iterator(Node()); |
| for (NGBlockNode child = iterator.NextChild(); child; |
| child = iterator.NextChild()) { |
| if (child.IsOutOfFlowPositioned()) { |
| HandleOutOfFlowPositioned(child); |
| continue; |
| } |
| |
| const ComputedStyle& child_style = child.Style(); |
| NGConstraintSpace flex_basis_space = BuildSpaceForFlexBasis(child); |
| |
| NGPhysicalBoxStrut physical_child_margins = |
| ComputePhysicalMargins(flex_basis_space, child_style); |
| |
| NGBoxStrut border_padding_in_child_writing_mode = |
| ComputeBorders(flex_basis_space, child) + |
| ComputePadding(flex_basis_space, child_style); |
| |
| NGPhysicalBoxStrut physical_border_padding( |
| border_padding_in_child_writing_mode.ConvertToPhysical( |
| child_style.GetWritingDirection())); |
| |
| LayoutUnit main_axis_border_padding = |
| is_horizontal_flow_ ? physical_border_padding.HorizontalSum() |
| : physical_border_padding.VerticalSum(); |
| LayoutUnit cross_axis_border_padding = |
| is_horizontal_flow_ ? physical_border_padding.VerticalSum() |
| : physical_border_padding.HorizontalSum(); |
| |
| base::Optional<MinMaxSizesResult> min_max_sizes; |
| auto MinMaxSizesFunc = [&](MinMaxSizesType type) -> MinMaxSizesResult { |
| if (!min_max_sizes) { |
| // We want the child's intrinsic inline sizes in its writing mode, so |
| // pass child's writing mode as the first parameter, which is nominally |
| // |container_writing_mode|. |
| NGConstraintSpace child_space = BuildSpaceForIntrinsicBlockSize(child); |
| MinMaxSizesInput input(ChildAvailableSize().block_size, type); |
| min_max_sizes = child.ComputeMinMaxSizes(child_style.GetWritingMode(), |
| input, &child_space); |
| } |
| return *min_max_sizes; |
| }; |
| |
| MinMaxSizes min_max_sizes_in_main_axis_direction{main_axis_border_padding, |
| LayoutUnit::Max()}; |
| MinMaxSizes min_max_sizes_in_cross_axis_direction{LayoutUnit(), |
| LayoutUnit::Max()}; |
| const Length& max_property_in_main_axis = is_horizontal_flow_ |
| ? child.Style().MaxWidth() |
| : child.Style().MaxHeight(); |
| const Length& max_property_in_cross_axis = is_horizontal_flow_ |
| ? child.Style().MaxHeight() |
| : child.Style().MaxWidth(); |
| const Length& min_property_in_cross_axis = is_horizontal_flow_ |
| ? child.Style().MinHeight() |
| : child.Style().MinWidth(); |
| if (MainAxisIsInlineAxis(child)) { |
| min_max_sizes_in_main_axis_direction.max_size = ResolveMaxInlineLength( |
| flex_basis_space, child_style, border_padding_in_child_writing_mode, |
| MinMaxSizesFunc, max_property_in_main_axis, |
| LengthResolvePhase::kLayout); |
| min_max_sizes_in_cross_axis_direction.max_size = ResolveMaxBlockLength( |
| flex_basis_space, child_style, border_padding_in_child_writing_mode, |
| max_property_in_cross_axis, LengthResolvePhase::kLayout); |
| min_max_sizes_in_cross_axis_direction.min_size = ResolveMinBlockLength( |
| flex_basis_space, child_style, border_padding_in_child_writing_mode, |
| min_property_in_cross_axis, LengthResolvePhase::kLayout); |
| } else { |
| min_max_sizes_in_main_axis_direction.max_size = ResolveMaxBlockLength( |
| flex_basis_space, child_style, border_padding_in_child_writing_mode, |
| max_property_in_main_axis, LengthResolvePhase::kLayout); |
| min_max_sizes_in_cross_axis_direction.max_size = ResolveMaxInlineLength( |
| flex_basis_space, child_style, border_padding_in_child_writing_mode, |
| MinMaxSizesFunc, max_property_in_cross_axis, |
| LengthResolvePhase::kLayout); |
| min_max_sizes_in_cross_axis_direction.min_size = ResolveMinInlineLength( |
| flex_basis_space, child_style, border_padding_in_child_writing_mode, |
| MinMaxSizesFunc, min_property_in_cross_axis, |
| LengthResolvePhase::kLayout); |
| } |
| |
| base::Optional<LayoutUnit> calculated_intrinsic_block_size; |
| auto IntrinsicBlockSizeFunc = [&]() -> LayoutUnit { |
| if (!calculated_intrinsic_block_size) { |
| NGConstraintSpace child_space = BuildSpaceForIntrinsicBlockSize( |
| child, physical_child_margins, |
| min_max_sizes_in_cross_axis_direction); |
| scoped_refptr<const NGLayoutResult> layout_result = |
| child.Layout(child_space, /* break_token */ nullptr); |
| calculated_intrinsic_block_size = layout_result->IntrinsicBlockSize(); |
| } |
| return *calculated_intrinsic_block_size; |
| }; |
| |
| // The logic that calculates flex_base_border_box assumes that the used |
| // value of the flex-basis property is either definite or 'content'. |
| LayoutUnit flex_base_border_box; |
| const Length& specified_length_in_main_axis = |
| is_horizontal_flow_ ? child_style.Width() : child_style.Height(); |
| const Length& flex_basis = child_style.FlexBasis(); |
| Length length_to_resolve = Length::Auto(); |
| if (flex_basis.IsAuto()) { |
| if (!is_column_ || IsItemMainSizeDefinite(child)) |
| length_to_resolve = specified_length_in_main_axis; |
| |
| // 'auto' for items within a -webkit-box resolve as 'fit-content'. |
| if (length_to_resolve.IsAuto() && Style().IsDeprecatedWebkitBox() && |
| (Style().BoxOrient() == EBoxOrient::kHorizontal || |
| Style().BoxAlign() != EBoxAlignment::kStretch)) |
| length_to_resolve = Length::FitContent(); |
| } else if (IsItemFlexBasisDefinite(child)) { |
| length_to_resolve = flex_basis; |
| } |
| |
| if (length_to_resolve.IsAuto()) { |
| // This block means that the used flex-basis is 'content'. In here we |
| // implement parts B,C,D,E of 9.2.3 |
| // https://drafts.csswg.org/css-flexbox/#algo-main-item |
| const Length& cross_axis_length = |
| is_horizontal_flow_ ? child.Style().Height() : child.Style().Width(); |
| // This check should use HasAspectRatio() instead of Style(). |
| // AspectRatio(), but to avoid introducing a behavior change we only |
| // do this for the aspect-ratio property for now until FlexNG ships. |
| bool use_container_cross_size_for_aspect_ratio = |
| (!child.Style().AspectRatio().IsAuto() || |
| (child.HasAspectRatio() && |
| RuntimeEnabledFeatures::FlexAspectRatioEnabled())) && |
| WillChildCrossSizeBeContainerCrossSize(child); |
| if (use_container_cross_size_for_aspect_ratio || |
| (child.HasAspectRatio() && |
| (IsItemCrossAxisLengthDefinite(child, cross_axis_length)))) { |
| // This is Part B of 9.2.3 |
| // https://drafts.csswg.org/css-flexbox/#algo-main-item It requires that |
| // the item has a definite cross size. |
| LayoutUnit cross_size; |
| if (use_container_cross_size_for_aspect_ratio) { |
| NGBoxStrut margins = physical_child_margins.ConvertToLogical( |
| ConstraintSpace().GetWritingDirection()); |
| cross_size = CalculateFixedCrossSize( |
| min_max_sizes_in_cross_axis_direction, margins); |
| } else if (MainAxisIsInlineAxis(child)) { |
| cross_size = ResolveMainBlockLength( |
| flex_basis_space, child_style, |
| border_padding_in_child_writing_mode, cross_axis_length, |
| kIndefiniteSize, LengthResolvePhase::kLayout); |
| } else { |
| cross_size = |
| ResolveMainInlineLength(flex_basis_space, child_style, |
| border_padding_in_child_writing_mode, |
| MinMaxSizesFunc, cross_axis_length); |
| } |
| cross_size = min_max_sizes_in_cross_axis_direction.ClampSizeToMinAndMax( |
| cross_size); |
| flex_base_border_box = |
| LayoutUnit((cross_size - cross_axis_border_padding) * |
| GetMainOverCrossAspectRatio(child) + |
| main_axis_border_padding); |
| } else if (MainAxisIsInlineAxis(child)) { |
| // We're now in parts C, D, and E for what are usually (horizontal-tb |
| // containers AND children) row flex containers. I _think_ the C and D |
| // cases are correctly handled by this code, which was originally |
| // written for case E. |
| |
| // Non-replaced AspectRatio items work fine using |
| // MinMaxSizesFunc(MinMaxSizesType::kContent).sizes.max_size below, so |
| // they don't need to use |
| // ComputeIntrinsicInlineSizeForAspectRatioElement. |
| // Also, ComputeIntrinsicInlineSizeForAspectRatioElement DCHECKs for |
| // non-replaced items. |
| if (child.HasAspectRatio() && child.IsReplaced()) { |
| if (RuntimeEnabledFeatures::FlexAspectRatioEnabled()) { |
| // Legacy uses child.PreferredLogicalWidths() for this case, which |
| // is not exactly correct. |
| // ComputeIntrinsicInlineSizeForAspectRatioElement would honor the |
| // definite block size parameter by multipying it by the aspect |
| // ratio, but if control flow reaches here, we know we don't have a |
| // definite inline size. If we did, we would have fallen into the |
| // "part B" section above, not this "part C, D, E" section. |
| flex_base_border_box = |
| ComputeIntrinsicInlineSizeForAspectRatioElement( |
| child, flex_basis_space, /* definite_block_size */ |
| base::nullopt, min_max_sizes_in_cross_axis_direction); |
| } else { |
| MinMaxSizesInput input(child_percentage_size_.block_size, |
| MinMaxSizesType::kContent); |
| flex_base_border_box = |
| ComputeMinAndMaxContentContribution(Style(), child, input) |
| .sizes.max_size; |
| } |
| } else { |
| flex_base_border_box = |
| MinMaxSizesFunc(MinMaxSizesType::kContent).sizes.max_size; |
| } |
| } else { |
| // Parts C, D, and E for what are usually column flex containers. |
| if (child.HasAspectRatio() && child.IsReplaced() && |
| RuntimeEnabledFeatures::FlexAspectRatioEnabled()) { |
| // Legacy uses the post-layout size for this case, which isn't always |
| // correct. |
| // ComputeIntrinsicBlockSizeForAspectRatioElement would honor the |
| // definite inline size parameter by multipying it by the aspect |
| // ratio, but if control flow reaches here, we know we don't have a |
| // definite inline size. If we did, we would have fallen into the |
| // "part B" section above, not this "part C, D, E" section. |
| flex_base_border_box = ComputeIntrinsicBlockSizeForAspectRatioElement( |
| child, flex_basis_space, base::nullopt /* definite_inline_size */, |
| min_max_sizes_in_cross_axis_direction); |
| } else { |
| flex_base_border_box = IntrinsicBlockSizeFunc(); |
| } |
| } |
| } else { |
| // Part A of 9.2.3 https://drafts.csswg.org/css-flexbox/#algo-main-item |
| if (MainAxisIsInlineAxis(child)) { |
| flex_base_border_box = ResolveMainInlineLength( |
| flex_basis_space, child_style, border_padding_in_child_writing_mode, |
| MinMaxSizesFunc, length_to_resolve); |
| } else { |
| // Flex container's main axis is in child's block direction. Child's |
| // flex basis is in child's block direction. |
| flex_base_border_box = ResolveMainBlockLength( |
| flex_basis_space, child_style, border_padding_in_child_writing_mode, |
| length_to_resolve, IntrinsicBlockSizeFunc, |
| LengthResolvePhase::kLayout); |
| } |
| } |
| |
| // Spec calls this "flex base size" |
| // https://www.w3.org/TR/css-flexbox-1/#algo-main-item |
| // Blink's FlexibleBoxAlgorithm expects it to be content + scrollbar widths, |
| // but no padding or border. |
| DCHECK_GE(flex_base_border_box, main_axis_border_padding); |
| LayoutUnit flex_base_content_size = |
| flex_base_border_box - main_axis_border_padding; |
| |
| const Length& min = is_horizontal_flow_ ? child.Style().MinWidth() |
| : child.Style().MinHeight(); |
| if (algorithm_->ShouldApplyMinSizeAutoForChild(*child.GetLayoutBox())) { |
| LayoutUnit content_size_suggestion; |
| if (MainAxisIsInlineAxis(child)) { |
| if (child.IsReplaced() && child.HasAspectRatio() && |
| RuntimeEnabledFeatures::FlexAspectRatioEnabled()) { |
| base::Optional<LayoutUnit> definite_block_size; |
| if (!BlockLengthUnresolvable(flex_basis_space, |
| child_style.LogicalHeight(), |
| LengthResolvePhase::kLayout)) { |
| definite_block_size = ResolveMainBlockLength( |
| flex_basis_space, child_style, |
| border_padding_in_child_writing_mode, |
| child_style.LogicalHeight(), IntrinsicBlockSizeFunc, |
| LengthResolvePhase::kLayout); |
| } |
| |
| content_size_suggestion = |
| ComputeIntrinsicInlineSizeForAspectRatioElement( |
| child, flex_basis_space, definite_block_size, |
| min_max_sizes_in_cross_axis_direction); |
| } else { |
| content_size_suggestion = |
| MinMaxSizesFunc(MinMaxSizesType::kContent).sizes.min_size; |
| } |
| } else { |
| LayoutUnit intrinsic_block_size; |
| if (child.IsReplaced()) { |
| if (child.HasAspectRatio() && |
| RuntimeEnabledFeatures::FlexAspectRatioEnabled()) { |
| base::Optional<LayoutUnit> definite_inline_size; |
| if (!child_style.LogicalWidth().IsAuto()) { |
| definite_inline_size = ResolveMainInlineLength( |
| flex_basis_space, child_style, |
| border_padding_in_child_writing_mode, MinMaxSizesFunc, |
| child_style.LogicalWidth()); |
| } |
| intrinsic_block_size = |
| ComputeIntrinsicBlockSizeForAspectRatioElement( |
| child, flex_basis_space, definite_inline_size, |
| min_max_sizes_in_cross_axis_direction); |
| } else { |
| base::Optional<LayoutUnit> computed_inline_size; |
| base::Optional<LayoutUnit> computed_block_size; |
| child.IntrinsicSize(&computed_inline_size, &computed_block_size); |
| |
| // The 150 is for replaced elements that have no size, which SVG |
| // can have (maybe others?). |
| intrinsic_block_size = |
| computed_block_size.value_or(LayoutUnit(150)) + |
| border_padding_in_child_writing_mode.BlockSum(); |
| } |
| } else { |
| intrinsic_block_size = IntrinsicBlockSizeFunc(); |
| } |
| content_size_suggestion = intrinsic_block_size; |
| } |
| DCHECK_GE(content_size_suggestion, main_axis_border_padding); |
| |
| if (child.HasAspectRatio()) { |
| content_size_suggestion = |
| AdjustChildSizeForAspectRatioCrossAxisMinAndMax( |
| child, content_size_suggestion, |
| min_max_sizes_in_cross_axis_direction.min_size, |
| min_max_sizes_in_cross_axis_direction.max_size, |
| main_axis_border_padding, cross_axis_border_padding); |
| } |
| |
| LayoutUnit specified_size_suggestion = LayoutUnit::Max(); |
| // If the item’s computed main size property is definite, then the |
| // specified size suggestion is that size. |
| if (MainAxisIsInlineAxis(child)) { |
| if (!specified_length_in_main_axis.IsAuto()) { |
| // TODO(dgrogan): Optimization opportunity: we may have already |
| // resolved specified_length_in_main_axis in the flex basis |
| // calculation. Reuse that if possible. |
| specified_size_suggestion = ResolveMainInlineLength( |
| flex_basis_space, child_style, |
| border_padding_in_child_writing_mode, MinMaxSizesFunc, |
| specified_length_in_main_axis); |
| } |
| } else if (!BlockLengthUnresolvable(flex_basis_space, |
| specified_length_in_main_axis, |
| LengthResolvePhase::kLayout)) { |
| specified_size_suggestion = ResolveMainBlockLength( |
| flex_basis_space, child_style, border_padding_in_child_writing_mode, |
| specified_length_in_main_axis, IntrinsicBlockSizeFunc, |
| LengthResolvePhase::kLayout); |
| DCHECK_NE(specified_size_suggestion, kIndefiniteSize); |
| } |
| |
| LayoutUnit transferred_size_suggestion = LayoutUnit::Max(); |
| if (specified_size_suggestion == LayoutUnit::Max() && |
| child.HasAspectRatio()) { |
| LayoutUnit cross_axis_size(kIndefiniteSize); |
| const Length& cross_axis_length = |
| is_horizontal_flow_ ? child_style.Height() : child_style.Width(); |
| if (IsItemCrossAxisLengthDefinite(child, cross_axis_length)) { |
| // This entire IsItemCrossAxisLengthDefinite block may be unnecessary |
| // after enabling RuntimeEnabledFeatures::FlexAspectRatioEnabled(). |
| if (MainAxisIsInlineAxis(child)) { |
| cross_axis_size = ResolveMainBlockLength( |
| flex_basis_space, child_style, |
| border_padding_in_child_writing_mode, cross_axis_length, |
| kIndefiniteSize, LengthResolvePhase::kLayout); |
| DCHECK_NE(cross_axis_size, kIndefiniteSize); |
| } else { |
| cross_axis_size = |
| ResolveMainInlineLength(flex_basis_space, child_style, |
| border_padding_in_child_writing_mode, |
| MinMaxSizesFunc, cross_axis_length); |
| } |
| } else if (WillChildCrossSizeBeContainerCrossSize(child) && |
| RuntimeEnabledFeatures::FlexAspectRatioEnabled()) { |
| NGBoxStrut margins = physical_child_margins.ConvertToLogical( |
| ConstraintSpace().GetWritingDirection()); |
| cross_axis_size = CalculateFixedCrossSize( |
| min_max_sizes_in_cross_axis_direction, margins); |
| } |
| if (cross_axis_size != kIndefiniteSize) { |
| double ratio = GetMainOverCrossAspectRatio(child); |
| transferred_size_suggestion = LayoutUnit( |
| main_axis_border_padding + |
| ratio * |
| (min_max_sizes_in_cross_axis_direction.ClampSizeToMinAndMax( |
| cross_axis_size) - |
| cross_axis_border_padding)); |
| } |
| } |
| |
| DCHECK(specified_size_suggestion == LayoutUnit::Max() || |
| transferred_size_suggestion == LayoutUnit::Max()); |
| |
| min_max_sizes_in_main_axis_direction.min_size = |
| std::min({specified_size_suggestion, content_size_suggestion, |
| transferred_size_suggestion, |
| min_max_sizes_in_main_axis_direction.max_size}); |
| } else if (MainAxisIsInlineAxis(child)) { |
| min_max_sizes_in_main_axis_direction.min_size = ResolveMinInlineLength( |
| flex_basis_space, child_style, border_padding_in_child_writing_mode, |
| MinMaxSizesFunc, min, LengthResolvePhase::kLayout); |
| } else { |
| min_max_sizes_in_main_axis_direction.min_size = ResolveMinBlockLength( |
| flex_basis_space, child_style, border_padding_in_child_writing_mode, |
| min, LengthResolvePhase::kLayout); |
| } |
| // Flex needs to never give a table a flexed main size that is less than its |
| // min-content size, so floor min-width by min-content size. |
| // TODO(dgrogan): This should probably apply to column flexboxes also, |
| // but that's not what legacy does. |
| if (child.IsTable() && !is_column_) { |
| const NGConstraintSpace* ortho_child_space = nullptr; |
| NGConstraintSpace child_space; |
| if (UNLIKELY(!IsParallelWritingMode(ConstraintSpace().GetWritingMode(), |
| child_style.GetWritingMode()))) { |
| // BuildSpaceForIntrinsicBlockSize sets an indefinite percentage block |
| // size for column flexboxes, which is maybe not what we want for |
| // calculating the intrinsic min/max content sizes of an orthogonal |
| // item, but because control flow only enters this block for row |
| // flexboxes, it doesn't matter. If/when we fix the proximate above |
| // TODO, this will need closer investigation. |
| child_space = BuildSpaceForIntrinsicBlockSize(child); |
| ortho_child_space = &child_space; |
| } |
| MinMaxSizes table_intrinsic_widths = |
| child |
| .ComputeMinMaxSizes( |
| ConstraintSpace().GetWritingMode(), |
| MinMaxSizesInput(child_percentage_size_.block_size, |
| MinMaxSizesType::kContent), |
| ortho_child_space) |
| .sizes; |
| |
| min_max_sizes_in_main_axis_direction.Encompass( |
| table_intrinsic_widths.min_size); |
| } |
| |
| min_max_sizes_in_main_axis_direction -= main_axis_border_padding; |
| DCHECK_GE(min_max_sizes_in_main_axis_direction.min_size, 0); |
| DCHECK_GE(min_max_sizes_in_main_axis_direction.max_size, 0); |
| |
| NGBoxStrut scrollbars = ComputeScrollbarsForNonAnonymous(child); |
| algorithm_ |
| ->emplace_back(nullptr, child.Style(), flex_base_content_size, |
| min_max_sizes_in_main_axis_direction, |
| min_max_sizes_in_cross_axis_direction, |
| main_axis_border_padding, cross_axis_border_padding, |
| physical_child_margins, scrollbars, |
| min_max_sizes.has_value()) |
| .ng_input_node_ = child; |
| } |
| } |
| |
| LayoutUnit |
| NGFlexLayoutAlgorithm::AdjustChildSizeForAspectRatioCrossAxisMinAndMax( |
| const NGBlockNode& child, |
| LayoutUnit content_size_suggestion, |
| LayoutUnit cross_min, |
| LayoutUnit cross_max, |
| LayoutUnit main_axis_border_padding, |
| LayoutUnit cross_axis_border_padding) { |
| DCHECK(child.HasAspectRatio()); |
| |
| double ratio = GetMainOverCrossAspectRatio(child); |
| // Clamp content_suggestion by any definite min and max cross size properties |
| // converted through the aspect ratio. |
| const Length& cross_max_length = is_horizontal_flow_ |
| ? child.Style().MaxHeight() |
| : child.Style().MaxWidth(); |
| DCHECK_GE(cross_max, cross_axis_border_padding); |
| // TODO(dgrogan): No tests fail if we unconditionally apply max_main_length. |
| // Either add a test that needs it or remove it. |
| if (IsItemCrossAxisLengthDefinite(child, cross_max_length)) { |
| LayoutUnit max_main_length = |
| main_axis_border_padding + |
| LayoutUnit((cross_max - cross_axis_border_padding) * ratio); |
| content_size_suggestion = |
| std::min(max_main_length, content_size_suggestion); |
| } |
| |
| const Length& cross_min_length = is_horizontal_flow_ |
| ? child.Style().MinHeight() |
| : child.Style().MinWidth(); |
| DCHECK_GE(cross_min, cross_axis_border_padding); |
| // TODO(dgrogan): Same as above with min_main_length here -- it may be |
| // unneeded or untested. |
| if (IsItemCrossAxisLengthDefinite(child, cross_min_length)) { |
| LayoutUnit min_main_length = |
| main_axis_border_padding + |
| LayoutUnit((cross_min - cross_axis_border_padding) * ratio); |
| content_size_suggestion = |
| std::max(min_main_length, content_size_suggestion); |
| } |
| return content_size_suggestion; |
| } |
| |
| scoped_refptr<const NGLayoutResult> NGFlexLayoutAlgorithm::Layout() { |
| if (auto result = LayoutInternal()) |
| return result; |
| |
| // We may have aborted layout due to a child changing scrollbars, relayout |
| // with the new scrollbar information. |
| return RelayoutIgnoringChildScrollbarChanges(); |
| } |
| |
| scoped_refptr<const NGLayoutResult> |
| NGFlexLayoutAlgorithm::RelayoutIgnoringChildScrollbarChanges() { |
| DCHECK(!ignore_child_scrollbar_changes_); |
| NGLayoutAlgorithmParams params( |
| Node(), container_builder_.InitialFragmentGeometry(), ConstraintSpace(), |
| BreakToken(), /* early_break */ nullptr); |
| NGFlexLayoutAlgorithm algorithm(params); |
| algorithm.ignore_child_scrollbar_changes_ = true; |
| return algorithm.Layout(); |
| } |
| |
| scoped_refptr<const NGLayoutResult> NGFlexLayoutAlgorithm::LayoutInternal() { |
| // Freezing the scrollbars for the sub-tree shouldn't be strictly necessary, |
| // but we do this just in case we trigger an unstable layout. |
| base::Optional<PaintLayerScrollableArea::FreezeScrollbarsScope> |
| freeze_scrollbars; |
| if (ignore_child_scrollbar_changes_) |
| freeze_scrollbars.emplace(); |
| |
| PaintLayerScrollableArea::DelayScrollOffsetClampScope delay_clamp_scope; |
| ConstructAndAppendFlexItems(); |
| |
| LayoutUnit main_axis_start_offset; |
| LayoutUnit main_axis_end_offset; |
| LayoutUnit cross_axis_offset = BorderScrollbarPadding().block_start; |
| if (is_column_) { |
| const bool is_column_reverse = |
| Style().ResolvedIsColumnReverseFlexDirection(); |
| main_axis_start_offset = |
| is_column_reverse ? LayoutUnit() : BorderScrollbarPadding().block_start; |
| main_axis_end_offset = |
| is_column_reverse ? LayoutUnit() : BorderScrollbarPadding().block_end; |
| cross_axis_offset = BorderScrollbarPadding().inline_start; |
| } else if (Style().ResolvedIsRowReverseFlexDirection()) { |
| main_axis_start_offset = BorderScrollbarPadding().inline_end; |
| main_axis_end_offset = BorderScrollbarPadding().inline_start; |
| } else { |
| main_axis_start_offset = BorderScrollbarPadding().inline_start; |
| main_axis_end_offset = BorderScrollbarPadding().inline_end; |
| } |
| FlexLine* line; |
| while ( |
| (line = algorithm_->ComputeNextFlexLine(border_box_size_.inline_size))) { |
| line->SetContainerMainInnerSize( |
| MainAxisContentExtent(line->sum_hypothetical_main_size_)); |
| line->FreezeInflexibleItems(); |
| while (!line->ResolveFlexibleLengths()) { |
| continue; |
| } |
| for (wtf_size_t i = 0; i < line->line_items_.size(); ++i) { |
| FlexItem& flex_item = line->line_items_[i]; |
| |
| const ComputedStyle& child_style = flex_item.ng_input_node_.Style(); |
| NGConstraintSpaceBuilder space_builder(ConstraintSpace(), |
| child_style.GetWritingDirection(), |
| /* is_new_fc */ true); |
| SetOrthogonalFallbackInlineSizeIfNeeded(Style(), flex_item.ng_input_node_, |
| &space_builder); |
| space_builder.SetIsPaintedAtomically(true); |
| |
| LogicalSize available_size; |
| NGBoxStrut margins = flex_item.physical_margins_.ConvertToLogical( |
| ConstraintSpace().GetWritingDirection()); |
| LayoutUnit fixed_aspect_ratio_cross_size = kIndefiniteSize; |
| if (RuntimeEnabledFeatures::FlexAspectRatioEnabled() && |
| flex_item.ng_input_node_.HasAspectRatio() && |
| flex_item.ng_input_node_.IsReplaced()) { |
| // This code derives the cross axis size from the flexed main size and |
| // the aspect ratio. We can delete this code when |
| // NGReplacedLayoutAlgorithm exists, because it will do this for us. |
| NGConstraintSpace flex_basis_space = |
| BuildSpaceForFlexBasis(flex_item.ng_input_node_); |
| const Length& cross_axis_length = |
| is_horizontal_flow_ ? child_style.Height() : child_style.Width(); |
| // Only derive the cross axis size from the aspect ratio if the computed |
| // cross axis length might be indefinite. The item's cross axis length |
| // might still be definite if it is stretched, but that is checked in |
| // the |WillChildCrossSizeBeContainerCrossSize| calls below. |
| if (cross_axis_length.IsAuto() || |
| (MainAxisIsInlineAxis(flex_item.ng_input_node_) && |
| BlockLengthUnresolvable(flex_basis_space, cross_axis_length, |
| LengthResolvePhase::kLayout))) { |
| fixed_aspect_ratio_cross_size = |
| flex_item.min_max_cross_sizes_->ClampSizeToMinAndMax( |
| flex_item.cross_axis_border_padding_ + |
| LayoutUnit( |
| flex_item.flexed_content_size_ / |
| GetMainOverCrossAspectRatio(flex_item.ng_input_node_))); |
| } |
| } |
| if (is_column_) { |
| available_size.inline_size = ChildAvailableSize().inline_size; |
| available_size.block_size = flex_item.flexed_content_size_ + |
| flex_item.main_axis_border_padding_; |
| space_builder.SetIsFixedBlockSize(true); |
| if (WillChildCrossSizeBeContainerCrossSize(flex_item.ng_input_node_)) { |
| space_builder.SetIsFixedInlineSize(true); |
| available_size.inline_size = CalculateFixedCrossSize( |
| flex_item.min_max_cross_sizes_.value(), margins); |
| } else if (fixed_aspect_ratio_cross_size != kIndefiniteSize) { |
| space_builder.SetIsFixedInlineSize(true); |
| available_size.inline_size = fixed_aspect_ratio_cross_size; |
| } |
| // https://drafts.csswg.org/css-flexbox/#definite-sizes |
| // If the flex container has a definite main size, a flex item's |
| // post-flexing main size is treated as definite, even though it can |
| // rely on the indefinite sizes of any flex items in the same line. |
| if (!IsColumnContainerMainSizeDefinite() && |
| !IsItemMainSizeDefinite(flex_item.ng_input_node_)) { |
| space_builder.SetIsFixedBlockSizeIndefinite(true); |
| } |
| } else { |
| available_size.inline_size = flex_item.flexed_content_size_ + |
| flex_item.main_axis_border_padding_; |
| available_size.block_size = ChildAvailableSize().block_size; |
| space_builder.SetIsFixedInlineSize(true); |
| if (WillChildCrossSizeBeContainerCrossSize(flex_item.ng_input_node_)) { |
| space_builder.SetIsFixedBlockSize(true); |
| available_size.block_size = CalculateFixedCrossSize( |
| flex_item.min_max_cross_sizes_.value(), margins); |
| } else if (fixed_aspect_ratio_cross_size != kIndefiniteSize) { |
| space_builder.SetIsFixedBlockSize(true); |
| available_size.block_size = fixed_aspect_ratio_cross_size; |
| } else if (DoesItemStretch(flex_item.ng_input_node_)) { |
| // If we are in a row flexbox, and we don't have a fixed block-size |
| // (yet), use the "measure" cache slot. This will be the first |
| // layout, and we will use the "layout" cache slot if this gets |
| // stretched later. |
| // |
| // Setting the "measure" cache slot on the space writes the result |
| // into both the "measure", and "layout" cache slots. If a subsequent |
| // "layout" occurs, it'll just get written into the "layout" slot. |
| space_builder.SetCacheSlot(NGCacheSlot::kMeasure); |
| } |
| } |
| |
| space_builder.SetAvailableSize(available_size); |
| space_builder.SetPercentageResolutionSize(child_percentage_size_); |
| space_builder.SetReplacedPercentageResolutionSize(child_percentage_size_); |
| |
| // https://drafts.csswg.org/css-flexbox/#algo-cross-item |
| // Determine the hypothetical cross size of each item by performing layout |
| // with the used main size and the available space, treating auto as |
| // fit-content. |
| if (!ShouldItemShrinkToFit(flex_item.ng_input_node_)) |
| space_builder.SetStretchInlineSizeIfAuto(true); |
| |
| // For a button child, we need the baseline type same as the container's |
| // baseline type for UseCounter. For example, if the container's display |
| // property is 'inline-block', we need the last-line baseline of the |
| // child. See the bottom of GiveLinesAndItemsFinalPositionAndSize(). |
| if (Node().IsButton()) { |
| space_builder.SetBaselineAlgorithmType( |
| ConstraintSpace().BaselineAlgorithmType()); |
| } |
| |
| NGConstraintSpace child_space = space_builder.ToConstraintSpace(); |
| flex_item.layout_result_ = |
| flex_item.ng_input_node_.Layout(child_space, nullptr /*break token*/); |
| |
| // TODO(layout-dev): Handle abortions caused by block fragmentation. |
| DCHECK_EQ(flex_item.layout_result_->Status(), NGLayoutResult::kSuccess); |
| |
| flex_item.cross_axis_size_ = |
| is_horizontal_flow_ |
| ? flex_item.layout_result_->PhysicalFragment().Size().height |
| : flex_item.layout_result_->PhysicalFragment().Size().width; |
| } |
| // cross_axis_offset is updated in each iteration of the loop, for passing |
| // in to the next iteration. |
| line->ComputeLineItemsPosition(main_axis_start_offset, main_axis_end_offset, |
| cross_axis_offset); |
| } |
| |
| LayoutUnit intrinsic_block_size = BorderScrollbarPadding().BlockSum(); |
| |
| if (algorithm_->FlexLines().IsEmpty() && Node().HasLineIfEmpty()) { |
| intrinsic_block_size += Node().GetLayoutBox()->LogicalHeightForEmptyLine(); |
| } else { |
| intrinsic_block_size += algorithm_->IntrinsicContentBlockSize(); |
| } |
| |
| intrinsic_block_size = |
| ClampIntrinsicBlockSize(ConstraintSpace(), Node(), |
| BorderScrollbarPadding(), intrinsic_block_size); |
| LayoutUnit block_size = ComputeBlockSizeForFragment( |
| ConstraintSpace(), Style(), BorderPadding(), intrinsic_block_size, |
| border_box_size_.inline_size); |
| |
| container_builder_.SetIntrinsicBlockSize(intrinsic_block_size); |
| container_builder_.SetFragmentsTotalBlockSize(block_size); |
| |
| bool success = GiveLinesAndItemsFinalPositionAndSize(); |
| if (!success) |
| return nullptr; |
| |
| // Un-freeze descendant scrollbars before we run the OOF layout part. |
| freeze_scrollbars.reset(); |
| NGOutOfFlowLayoutPart(Node(), ConstraintSpace(), &container_builder_).Run(); |
| |
| return container_builder_.ToBoxFragment(); |
| } |
| |
| void NGFlexLayoutAlgorithm::ApplyStretchAlignmentToChild(FlexItem& flex_item) { |
| const ComputedStyle& child_style = flex_item.ng_input_node_.Style(); |
| NGConstraintSpaceBuilder space_builder(ConstraintSpace(), |
| child_style.GetWritingDirection(), |
| /* is_new_fc */ true); |
| SetOrthogonalFallbackInlineSizeIfNeeded(Style(), flex_item.ng_input_node_, |
| &space_builder); |
| space_builder.SetIsPaintedAtomically(true); |
| |
| LogicalSize available_size( |
| flex_item.flexed_content_size_ + flex_item.main_axis_border_padding_, |
| flex_item.cross_axis_size_); |
| if (is_column_) { |
| available_size.Transpose(); |
| if (!IsColumnContainerMainSizeDefinite() && |
| !IsItemMainSizeDefinite(flex_item.ng_input_node_)) { |
| space_builder.SetIsFixedBlockSizeIndefinite(true); |
| } |
| } |
| |
| space_builder.SetAvailableSize(available_size); |
| space_builder.SetPercentageResolutionSize(child_percentage_size_); |
| space_builder.SetReplacedPercentageResolutionSize(child_percentage_size_); |
| space_builder.SetIsFixedInlineSize(true); |
| space_builder.SetIsFixedBlockSize(true); |
| NGConstraintSpace child_space = space_builder.ToConstraintSpace(); |
| flex_item.layout_result_ = |
| flex_item.ng_input_node_.Layout(child_space, /* break_token */ nullptr); |
| } |
| |
| bool NGFlexLayoutAlgorithm::GiveLinesAndItemsFinalPositionAndSize() { |
| Vector<FlexLine>& line_contexts = algorithm_->FlexLines(); |
| const LayoutUnit cross_axis_start_edge = |
| line_contexts.IsEmpty() ? LayoutUnit() |
| : line_contexts[0].cross_axis_offset_; |
| |
| LayoutUnit final_content_main_size = |
| container_builder_.InlineSize() - BorderScrollbarPadding().InlineSum(); |
| LayoutUnit final_content_cross_size = container_builder_.FragmentBlockSize() - |
| BorderScrollbarPadding().BlockSum(); |
| if (is_column_) |
| std::swap(final_content_main_size, final_content_cross_size); |
| |
| if (!algorithm_->IsMultiline() && !line_contexts.IsEmpty()) |
| line_contexts[0].cross_axis_extent_ = final_content_cross_size; |
| |
| algorithm_->AlignFlexLines(final_content_cross_size); |
| |
| algorithm_->AlignChildren(); |
| |
| if (Style().FlexWrap() == EFlexWrap::kWrapReverse) { |
| // flex-wrap: wrap-reverse reverses the order of the lines in the container; |
| // FlipForWrapReverse recalculates each item's cross axis position. We have |
| // to do that after AlignChildren sets an initial cross axis position. |
| algorithm_->FlipForWrapReverse(cross_axis_start_edge, |
| final_content_cross_size); |
| } |
| |
| if (Style().ResolvedIsColumnReverseFlexDirection()) { |
| algorithm_->LayoutColumnReverse(final_content_main_size, |
| BorderScrollbarPadding().block_start); |
| } |
| |
| base::Optional<LayoutUnit> fallback_baseline; |
| |
| bool success = true; |
| LayoutUnit overflow_block_size; |
| for (FlexLine& line_context : line_contexts) { |
| for (wtf_size_t child_number = 0; |
| child_number < line_context.line_items_.size(); ++child_number) { |
| FlexItem& flex_item = line_context.line_items_[child_number]; |
| |
| if (DoesItemStretch(flex_item.ng_input_node_)) |
| ApplyStretchAlignmentToChild(flex_item); |
| |
| const auto& physical_fragment = To<NGPhysicalBoxFragment>( |
| flex_item.layout_result_->PhysicalFragment()); |
| |
| // flex_item.desired_location_ stores the main axis offset in X and the |
| // cross axis offset in Y. But AddChild wants offset from parent |
| // rectangle, so we have to transpose for columns. AddChild takes care of |
| // any writing mode differences though. |
| LayoutPoint location = is_column_ |
| ? flex_item.desired_location_.TransposedPoint() |
| : flex_item.desired_location_; |
| |
| NGBoxFragment fragment(ConstraintSpace().GetWritingDirection(), |
| physical_fragment); |
| // Only propagate baselines from children on the first flex-line. |
| if (&line_context == line_contexts.begin()) { |
| PropagateBaselineFromChild(flex_item, fragment, location.Y(), |
| &fallback_baseline); |
| } |
| |
| container_builder_.AddChild(physical_fragment, |
| {location.X(), location.Y()}); |
| |
| flex_item.ng_input_node_.StoreMargins(flex_item.physical_margins_); |
| |
| LayoutUnit margin_block_end = |
| flex_item.physical_margins_ |
| .ConvertToLogical(ConstraintSpace().GetWritingDirection()) |
| .block_end; |
| overflow_block_size = |
| std::max(overflow_block_size, |
| location.Y() + fragment.BlockSize() + margin_block_end); |
| |
| // Detect if the flex-item had its scrollbar state change. If so we need |
| // to relayout as the input to the flex algorithm is incorrect. |
| if (!ignore_child_scrollbar_changes_) { |
| if (flex_item.scrollbars_ != |
| ComputeScrollbarsForNonAnonymous(flex_item.ng_input_node_)) |
| success = false; |
| |
| // The flex-item scrollbars may not have changed, but an descendant's |
| // scrollbars might have causing the min/max sizes to be incorrect. |
| if (flex_item.depends_on_min_max_sizes_ && |
| flex_item.ng_input_node_.GetLayoutBox() |
| ->IntrinsicLogicalWidthsDirty()) |
| success = false; |
| } else { |
| DCHECK_EQ(flex_item.scrollbars_, |
| ComputeScrollbarsForNonAnonymous(flex_item.ng_input_node_)); |
| } |
| } |
| } |
| |
| container_builder_.SetOverflowBlockSize(overflow_block_size + |
| BorderScrollbarPadding().block_end); |
| |
| // Set the baseline to the fallback, if we didn't find any children with |
| // baseline alignment. |
| if (!container_builder_.Baseline() && fallback_baseline) |
| container_builder_.SetBaseline(*fallback_baseline); |
| |
| // TODO(crbug.com/1131352): Avoid control-specific handling. |
| if (Node().IsButton()) { |
| AdjustButtonBaseline(final_content_cross_size); |
| } else if (Node().IsSlider()) { |
| container_builder_.SetBaseline(BorderScrollbarPadding().BlockSum() + |
| final_content_cross_size); |
| } |
| |
| // Signal if we need to relayout with new child scrollbar information. |
| return success; |
| } |
| |
| void NGFlexLayoutAlgorithm::AdjustButtonBaseline( |
| LayoutUnit final_content_cross_size) { |
| // See LayoutButton::BaselinePosition() |
| if (!Node().HasLineIfEmpty() && !Node().ShouldApplyLayoutContainment() && |
| !container_builder_.Baseline()) { |
| // To ensure that we have a consistent baseline when we have no children, |
| // even when we have the anonymous LayoutBlock child, we calculate the |
| // baseline for the empty case manually here. |
| container_builder_.SetBaseline(BorderScrollbarPadding().block_start + |
| final_content_cross_size); |
| return; |
| } |
| |
| // Apply flexbox's baseline as is. That is to say, the baseline of the |
| // first line. |
| // However, we count the differences between it and the last-line baseline |
| // of the anonymous block. crbug.com/690036. |
| // We also have a difference in empty buttons. See crbug.com/304848. |
| |
| const LayoutObject* parent = Node().GetLayoutBox()->Parent(); |
| if (!LayoutButton::ShouldCountWrongBaseline( |
| Style(), parent ? parent->Style() : nullptr)) |
| return; |
| |
| // The button should have at most one child. |
| const NGContainerFragmentBuilder::ChildrenVector& children = |
| container_builder_.Children(); |
| if (children.size() < 1) { |
| const LayoutBlock* layout_block = To<LayoutBlock>(Node().GetLayoutBox()); |
| base::Optional<LayoutUnit> baseline = layout_block->BaselineForEmptyLine( |
| layout_block->IsHorizontalWritingMode() ? kHorizontalLine |
| : kVerticalLine); |
| if (container_builder_.Baseline() != baseline) { |
| UseCounter::Count(Node().GetDocument(), |
| WebFeature::kWrongBaselineOfEmptyLineButton); |
| } |
| return; |
| } |
| DCHECK_EQ(children.size(), 1u); |
| const NGContainerFragmentBuilder::ChildWithOffset& child = children[0]; |
| DCHECK(!child.fragment->IsLineBox()); |
| const NGConstraintSpace& space = ConstraintSpace(); |
| NGBoxFragment fragment(space.GetWritingDirection(), |
| To<NGPhysicalBoxFragment>(*child.fragment)); |
| base::Optional<LayoutUnit> child_baseline = |
| space.BaselineAlgorithmType() == NGBaselineAlgorithmType::kFirstLine |
| ? fragment.FirstBaseline() |
| : fragment.Baseline(); |
| if (child_baseline) |
| child_baseline = *child_baseline + child.offset.block_offset; |
| if (container_builder_.Baseline() != child_baseline) { |
| UseCounter::Count(Node().GetDocument(), |
| WebFeature::kWrongBaselineOfMultiLineButton); |
| } |
| } |
| |
| void NGFlexLayoutAlgorithm::PropagateBaselineFromChild( |
| const FlexItem& flex_item, |
| const NGBoxFragment& fragment, |
| LayoutUnit block_offset, |
| base::Optional<LayoutUnit>* fallback_baseline) { |
| // Check if we've already found an appropriate baseline. |
| if (container_builder_.Baseline()) |
| return; |
| |
| LayoutUnit baseline_offset = |
| block_offset + (Node().IsButton() ? fragment.FirstBaselineOrSynthesize() |
| : fragment.BaselineOrSynthesize()); |
| |
| // We prefer a baseline from a child with baseline alignment, and no |
| // auto-margins in the cross axis (even if we have to synthesize the |
| // baseline). |
| if (FlexLayoutAlgorithm::AlignmentForChild(Style(), flex_item.style_) == |
| ItemPosition::kBaseline && |
| !flex_item.HasAutoMarginsInCrossAxis()) { |
| container_builder_.SetBaseline(baseline_offset); |
| return; |
| } |
| |
| // Set the fallback baseline if it doesn't have a value yet. |
| *fallback_baseline = fallback_baseline->value_or(baseline_offset); |
| } |
| |
| MinMaxSizesResult NGFlexLayoutAlgorithm::ComputeMinMaxSizes( |
| const MinMaxSizesInput& child_input) const { |
| if (auto result = CalculateMinMaxSizesIgnoringChildren( |
| Node(), BorderScrollbarPadding())) |
| return *result; |
| |
| MinMaxSizes sizes; |
| bool depends_on_percentage_block_size = false; |
| |
| int number_of_items = 0; |
| NGFlexChildIterator iterator(Node()); |
| for (NGBlockNode child = iterator.NextChild(); child; |
| child = iterator.NextChild()) { |
| if (child.IsOutOfFlowPositioned()) |
| continue; |
| number_of_items++; |
| |
| MinMaxSizesResult child_result = |
| ComputeMinAndMaxContentContribution(Style(), child, child_input); |
| NGBoxStrut child_margins = ComputeMinMaxMargins(Style(), child); |
| child_result.sizes += child_margins.InlineSum(); |
| |
| depends_on_percentage_block_size |= |
| child_result.depends_on_percentage_block_size; |
| if (is_column_) { |
| sizes.min_size = std::max(sizes.min_size, child_result.sizes.min_size); |
| sizes.max_size = std::max(sizes.max_size, child_result.sizes.max_size); |
| } else { |
| sizes.max_size += child_result.sizes.max_size; |
| if (algorithm_->IsMultiline()) { |
| sizes.min_size = std::max(sizes.min_size, child_result.sizes.min_size); |
| } else { |
| sizes.min_size += child_result.sizes.min_size; |
| } |
| } |
| } |
| if (!is_column_) { |
| LayoutUnit gap_inline_size = |
| (number_of_items - 1) * algorithm_->gap_between_items_; |
| sizes.max_size += gap_inline_size; |
| if (!algorithm_->IsMultiline()) { |
| sizes.min_size += gap_inline_size; |
| } |
| } |
| sizes.max_size = std::max(sizes.max_size, sizes.min_size); |
| |
| // Due to negative margins, it is possible that we calculated a negative |
| // intrinsic width. Make sure that we never return a negative width. |
| sizes.Encompass(LayoutUnit()); |
| sizes += BorderScrollbarPadding().InlineSum(); |
| return {sizes, depends_on_percentage_block_size}; |
| } |
| |
| } // namespace blink |