| // Copyright 2016 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/ng_length_utils.h" |
| |
| #include <algorithm> |
| #include "base/optional.h" |
| #include "third_party/blink/renderer/core/layout/layout_box.h" |
| #include "third_party/blink/renderer/core/layout/layout_table_cell.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_block_node.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_space_utils.h" |
| #include "third_party/blink/renderer/core/style/computed_style.h" |
| #include "third_party/blink/renderer/platform/geometry/layout_unit.h" |
| #include "third_party/blink/renderer/platform/geometry/length.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| enum class EBlockAlignment { kStart, kCenter, kEnd }; |
| |
| inline EBlockAlignment BlockAlignment(const ComputedStyle& style, |
| const ComputedStyle& container_style) { |
| bool start_auto = style.MarginStartUsing(container_style).IsAuto(); |
| bool end_auto = style.MarginEndUsing(container_style).IsAuto(); |
| if (start_auto || end_auto) { |
| if (start_auto) |
| return end_auto ? EBlockAlignment::kCenter : EBlockAlignment::kEnd; |
| return EBlockAlignment::kStart; |
| } |
| |
| // If none of the inline margins are auto, look for -webkit- text-align |
| // values (which are really about block alignment). These are typically |
| // mapped from the legacy "align" HTML attribute. |
| switch (container_style.GetTextAlign()) { |
| case ETextAlign::kWebkitLeft: |
| if (container_style.IsLeftToRightDirection()) |
| return EBlockAlignment::kStart; |
| return EBlockAlignment::kEnd; |
| case ETextAlign::kWebkitRight: |
| if (container_style.IsLeftToRightDirection()) |
| return EBlockAlignment::kEnd; |
| return EBlockAlignment::kStart; |
| case ETextAlign::kWebkitCenter: |
| return EBlockAlignment::kCenter; |
| default: |
| return EBlockAlignment::kStart; |
| } |
| } |
| |
| inline bool InlineLengthMayChange(Length length, |
| const NGConstraintSpace& new_space, |
| const NGConstraintSpace& old_space) { |
| // Percentage inline margins will affect the size if the size is unspecified |
| // (auto and similar). So we need to check both available size and the |
| // percentage resolution size in that case. |
| bool is_unspecified = |
| length.IsAuto() || length.IsFitContent() || length.IsFillAvailable(); |
| if (is_unspecified) { |
| if (new_space.AvailableSize().inline_size != |
| old_space.AvailableSize().inline_size) |
| return true; |
| } |
| if (is_unspecified || length.IsPercentOrCalc()) { |
| if (new_space.PercentageResolutionInlineSize() != |
| old_space.PercentageResolutionInlineSize()) |
| return true; |
| } |
| return false; |
| } |
| |
| inline bool BlockLengthMayChange(Length length, |
| const NGConstraintSpace& new_space, |
| const NGConstraintSpace& old_space) { |
| if (length.IsFillAvailable()) { |
| if (new_space.AvailableSize().block_size != |
| old_space.AvailableSize().block_size) |
| return true; |
| } else if (length.IsAuto() || length.IsPercentOrCalc()) { |
| // Note that we check percentage resolution changes for 'auto' values here |
| // (in addition to percent values). The reason is that percentage resolution |
| // block sizes may be passed through auto-sized blocks, in some cases, |
| // e.g. for anonymous blocks, and also in quirks mode. |
| if (new_space.PercentageResolutionBlockSize() != |
| old_space.PercentageResolutionBlockSize()) |
| return true; |
| if (new_space.ReplacedPercentageResolutionBlockSize() != |
| old_space.ReplacedPercentageResolutionBlockSize()) |
| return true; |
| } |
| return false; |
| } |
| |
| } // anonymous namespace |
| |
| bool NeedMinMaxSizeForContentContribution(WritingMode mode, |
| const ComputedStyle& style) { |
| // During the intrinsic sizes pass percentages/calc() are defined to behave |
| // like 'auto'. As a result we need to calculate the intrinsic sizes for any |
| // children with percentages. E.g. |
| // <div style="float:left;"> |
| // <div style="width:30%;">text text</div> |
| // </div> |
| if (mode == WritingMode::kHorizontalTb) { |
| return style.Width().IsIntrinsicOrAuto() || |
| style.Width().IsPercentOrCalc() || style.MinWidth().IsIntrinsic() || |
| style.MaxWidth().IsIntrinsic(); |
| } |
| return style.Height().IsIntrinsicOrAuto() || |
| style.Height().IsPercentOrCalc() || style.MinHeight().IsIntrinsic() || |
| style.MaxHeight().IsIntrinsic(); |
| } |
| |
| LayoutUnit ResolveInlineLength( |
| const NGConstraintSpace& constraint_space, |
| const ComputedStyle& style, |
| const base::Optional<MinMaxSize>& min_and_max, |
| const Length& length, |
| LengthResolveType type, |
| LengthResolvePhase phase, |
| const base::Optional<NGBoxStrut>& opt_border_padding) { |
| DCHECK_GE(constraint_space.AvailableSize().inline_size, LayoutUnit()); |
| DCHECK_GE(constraint_space.PercentageResolutionInlineSize(), LayoutUnit()); |
| DCHECK_EQ(constraint_space.GetWritingMode(), style.GetWritingMode()); |
| |
| if (constraint_space.IsAnonymous()) |
| return constraint_space.AvailableSize().inline_size; |
| |
| if (length.IsMaxSizeNone()) { |
| DCHECK_EQ(type, LengthResolveType::kMaxSize); |
| return LayoutUnit::Max(); |
| } |
| |
| NGBoxStrut border_and_padding = |
| opt_border_padding ? *opt_border_padding |
| : ComputeBorders(constraint_space, style) + |
| ComputePadding(constraint_space, style); |
| |
| if (type == LengthResolveType::kMinSize && length.IsAuto()) |
| return border_and_padding.InlineSum(); |
| |
| // Check if we shouldn't resolve a percentage/calc()/-webkit-fill-available |
| // if we are in the intrinsic sizes phase. |
| if (phase == LengthResolvePhase::kIntrinsic && |
| (length.IsPercentOrCalc() || length.GetType() == kFillAvailable)) { |
| // min-width/min-height should be "0", i.e. no min limit is applied. |
| if (type == LengthResolveType::kMinSize) |
| return border_and_padding.InlineSum(); |
| |
| // max-width/max-height becomes "infinity", i.e. no max limit is applied. |
| if (type == LengthResolveType::kMaxSize) |
| return LayoutUnit::Max(); |
| } |
| |
| switch (length.GetType()) { |
| case kAuto: |
| case kFillAvailable: { |
| LayoutUnit content_size = constraint_space.AvailableSize().inline_size; |
| NGBoxStrut margins = ComputeMarginsForSelf(constraint_space, style); |
| return std::max(border_and_padding.InlineSum(), |
| content_size - margins.InlineSum()); |
| } |
| case kPercent: |
| case kFixed: |
| case kCalculated: { |
| LayoutUnit percentage_resolution_size = |
| constraint_space.PercentageResolutionInlineSize(); |
| LayoutUnit value = ValueForLength(length, percentage_resolution_size); |
| if (style.BoxSizing() == EBoxSizing::kContentBox) { |
| value += border_and_padding.InlineSum(); |
| } else { |
| value = std::max(border_and_padding.InlineSum(), value); |
| } |
| return value; |
| } |
| case kMinContent: |
| case kMaxContent: |
| case kFitContent: { |
| DCHECK(min_and_max.has_value()); |
| LayoutUnit available_size = constraint_space.AvailableSize().inline_size; |
| LayoutUnit value; |
| if (length.IsMinContent()) { |
| value = min_and_max->min_size; |
| } else if (length.IsMaxContent() || available_size == LayoutUnit::Max()) { |
| // If the available space is infinite, fit-content resolves to |
| // max-content. See css-sizing section 2.1. |
| value = min_and_max->max_size; |
| } else { |
| NGBoxStrut margins = ComputeMarginsForSelf(constraint_space, style); |
| LayoutUnit fill_available = |
| std::max(LayoutUnit(), available_size - margins.InlineSum()); |
| value = min_and_max->ShrinkToFit(fill_available); |
| } |
| return value; |
| } |
| case kDeviceWidth: |
| case kDeviceHeight: |
| case kExtendToZoom: |
| NOTREACHED() << "These should only be used for viewport definitions"; |
| FALLTHROUGH; |
| case kMaxSizeNone: |
| default: |
| NOTREACHED(); |
| return border_and_padding.InlineSum(); |
| } |
| } |
| |
| LayoutUnit ResolveBlockLength( |
| const NGConstraintSpace& constraint_space, |
| const ComputedStyle& style, |
| const Length& length, |
| LayoutUnit content_size, |
| LengthResolveType type, |
| LengthResolvePhase phase, |
| const base::Optional<NGBoxStrut>& opt_border_padding) { |
| DCHECK_EQ(constraint_space.GetWritingMode(), style.GetWritingMode()); |
| |
| if (constraint_space.IsAnonymous()) |
| return content_size; |
| |
| if (length.IsMaxSizeNone()) { |
| DCHECK_EQ(type, LengthResolveType::kMaxSize); |
| return LayoutUnit::Max(); |
| } |
| |
| NGBoxStrut border_and_padding = |
| opt_border_padding ? *opt_border_padding |
| : ComputeBorders(constraint_space, style) + |
| ComputePadding(constraint_space, style); |
| |
| if (type == LengthResolveType::kMinSize && length.IsAuto()) |
| return border_and_padding.BlockSum(); |
| |
| // Scrollable percentage-sized children of table cells, in the table |
| // "measure" phase contribute nothing to the row height measurement. |
| // See: https://drafts.csswg.org/css-tables-3/#row-layout |
| if (length.IsPercentOrCalc() && |
| constraint_space.TableCellChildLayoutPhase() == |
| NGTableCellChildLayoutPhase::kMeasure && |
| (style.OverflowY() == EOverflow::kAuto || |
| style.OverflowY() == EOverflow::kScroll)) |
| return border_and_padding.BlockSum(); |
| |
| // When the containing block size to resolve against is indefinite, we |
| // cannot resolve percentages / calc() / -webkit-fill-available. |
| bool size_is_unresolvable = false; |
| if (length.IsPercentOrCalc()) { |
| size_is_unresolvable = |
| phase == LengthResolvePhase::kIntrinsic || |
| constraint_space.PercentageResolutionBlockSize() == NGSizeIndefinite; |
| } else if (length.GetType() == kFillAvailable) { |
| size_is_unresolvable = |
| phase == LengthResolvePhase::kIntrinsic || |
| constraint_space.AvailableSize().block_size == NGSizeIndefinite; |
| } |
| if (size_is_unresolvable) { |
| // min-width/min-height should be "0", i.e. no min limit is applied. |
| if (type == LengthResolveType::kMinSize) |
| return border_and_padding.BlockSum(); |
| |
| // max-width/max-height becomes "infinity", i.e. no max limit is applied. |
| if (type == LengthResolveType::kMaxSize) |
| return LayoutUnit::Max(); |
| |
| // width/height becomes "auto", so we can just return the content size. |
| DCHECK_EQ(type, LengthResolveType::kContentSize); |
| return content_size; |
| } |
| |
| switch (length.GetType()) { |
| case kFillAvailable: { |
| LayoutUnit content_size = constraint_space.AvailableSize().block_size; |
| NGBoxStrut margins = ComputeMarginsForSelf(constraint_space, style); |
| return std::max(border_and_padding.BlockSum(), |
| content_size - margins.BlockSum()); |
| } |
| case kPercent: |
| case kFixed: |
| case kCalculated: { |
| LayoutUnit percentage_resolution_size = |
| constraint_space.PercentageResolutionBlockSize(); |
| LayoutUnit value = ValueForLength(length, percentage_resolution_size); |
| |
| // Percentage-sized children of table cells, in the table "layout" phase, |
| // pretend they have box-sizing: border-box. |
| // TODO(crbug.com/285744): FF/Edge don't do this. Determine if there |
| // would be compat issues for matching their behavior. |
| if (style.BoxSizing() == EBoxSizing::kBorderBox || |
| (length.IsPercentOrCalc() && |
| constraint_space.TableCellChildLayoutPhase() == |
| NGTableCellChildLayoutPhase::kLayout)) { |
| value = std::max(border_and_padding.BlockSum(), value); |
| } else { |
| value += border_and_padding.BlockSum(); |
| } |
| return value; |
| } |
| case kAuto: |
| case kMinContent: |
| case kMaxContent: |
| case kFitContent: |
| #if DCHECK_IS_ON() |
| // Due to how content_size is calculated, it should always include border |
| // and padding. We cannot check for this if we are block-fragmented, |
| // though, because then the block-start border/padding may be in a |
| // different fragmentainer than the block-end border/padding. |
| if (content_size != LayoutUnit(-1) && |
| !constraint_space.HasBlockFragmentation()) |
| DCHECK_GE(content_size, border_and_padding.BlockSum()); |
| #endif // DCHECK_IS_ON() |
| return content_size; |
| case kDeviceWidth: |
| case kDeviceHeight: |
| case kExtendToZoom: |
| NOTREACHED() << "These should only be used for viewport definitions"; |
| FALLTHROUGH; |
| case kMaxSizeNone: |
| default: |
| NOTREACHED(); |
| return border_and_padding.BlockSum(); |
| } |
| } |
| |
| LayoutUnit ResolveMarginPaddingLength(LayoutUnit percentage_resolution_size, |
| const Length& length) { |
| DCHECK_GE(percentage_resolution_size, LayoutUnit()); |
| |
| // Margins and padding always get computed relative to the inline size: |
| // https://www.w3.org/TR/CSS2/box.html#value-def-margin-width |
| // https://www.w3.org/TR/CSS2/box.html#value-def-padding-width |
| switch (length.GetType()) { |
| case kAuto: |
| return LayoutUnit(); |
| case kPercent: |
| case kFixed: |
| case kCalculated: |
| return ValueForLength(length, percentage_resolution_size); |
| case kMinContent: |
| case kMaxContent: |
| case kFillAvailable: |
| case kFitContent: |
| case kExtendToZoom: |
| case kDeviceWidth: |
| case kDeviceHeight: |
| case kMaxSizeNone: |
| FALLTHROUGH; |
| default: |
| NOTREACHED(); |
| return LayoutUnit(); |
| } |
| } |
| |
| MinMaxSize ComputeMinAndMaxContentContribution( |
| WritingMode writing_mode, |
| const ComputedStyle& style, |
| const base::Optional<MinMaxSize>& min_and_max) { |
| // Synthesize a zero-sized constraint space for passing to |
| // ResolveInlineLength. |
| // The constraint space's writing mode has to match the style, so we can't |
| // use the passed-in mode here. |
| NGConstraintSpace space = |
| NGConstraintSpaceBuilder( |
| style.GetWritingMode(), style.GetWritingMode(), |
| /* is_new_fc */ false) |
| .ToConstraintSpace(); |
| |
| LayoutUnit content_size = |
| min_and_max ? min_and_max->max_size : NGSizeIndefinite; |
| |
| MinMaxSize computed_sizes; |
| Length inline_size = writing_mode == WritingMode::kHorizontalTb |
| ? style.Width() |
| : style.Height(); |
| if (inline_size.IsAuto() || inline_size.IsPercentOrCalc() || |
| inline_size.GetType() == kFillAvailable || |
| inline_size.GetType() == kFitContent) { |
| CHECK(min_and_max.has_value()); |
| computed_sizes = *min_and_max; |
| } else { |
| if (IsParallelWritingMode(writing_mode, style.GetWritingMode())) { |
| computed_sizes = ResolveInlineLength( |
| space, style, min_and_max, inline_size, |
| LengthResolveType::kContentSize, LengthResolvePhase::kIntrinsic); |
| } else { |
| computed_sizes = ResolveBlockLength( |
| space, style, inline_size, content_size, |
| LengthResolveType::kContentSize, LengthResolvePhase::kIntrinsic); |
| } |
| } |
| |
| Length max_length = writing_mode == WritingMode::kHorizontalTb |
| ? style.MaxWidth() |
| : style.MaxHeight(); |
| LayoutUnit max; |
| if (IsParallelWritingMode(writing_mode, style.GetWritingMode())) { |
| max = ResolveInlineLength(space, style, min_and_max, max_length, |
| LengthResolveType::kMaxSize, |
| LengthResolvePhase::kIntrinsic); |
| } else { |
| max = ResolveBlockLength(space, style, max_length, content_size, |
| LengthResolveType::kMaxSize, |
| LengthResolvePhase::kIntrinsic); |
| } |
| computed_sizes.Constrain(max); |
| |
| Length min_length = writing_mode == WritingMode::kHorizontalTb |
| ? style.MinWidth() |
| : style.MinHeight(); |
| LayoutUnit min; |
| if (IsParallelWritingMode(writing_mode, style.GetWritingMode())) { |
| min = ResolveInlineLength(space, style, min_and_max, min_length, |
| LengthResolveType::kMinSize, |
| LengthResolvePhase::kIntrinsic); |
| } else { |
| min = ResolveBlockLength(space, style, min_length, content_size, |
| LengthResolveType::kMinSize, |
| LengthResolvePhase::kIntrinsic); |
| } |
| computed_sizes.Encompass(min); |
| |
| return computed_sizes; |
| } |
| |
| MinMaxSize ComputeMinAndMaxContentContribution( |
| const ComputedStyle& parent_style, |
| NGLayoutInputNode child, |
| const MinMaxSizeInput& input) { |
| const ComputedStyle& child_style = child.Style(); |
| WritingMode parent_writing_mode = parent_style.GetWritingMode(); |
| LayoutBox* box = child.GetLayoutBox(); |
| |
| if (box->NeedsPreferredWidthsRecalculation()) { |
| // Some objects (when there's an intrinsic ratio) have their min/max inline |
| // size affected by the block size of their container. We don't really know |
| // whether the containing block of this child did change or is going to |
| // change size. However, this is our only opportunity to make sure that it |
| // gets its min/max widths calculated. |
| box->SetPreferredLogicalWidthsDirty(); |
| } |
| |
| if (IsParallelWritingMode(parent_writing_mode, |
| child_style.GetWritingMode())) { |
| if (!box->PreferredLogicalWidthsDirty()) { |
| return {box->MinPreferredLogicalWidth(), box->MaxPreferredLogicalWidth()}; |
| } |
| // Tables are special; even if a width is specified, they may end up being |
| // sized different. So we just always let the table code handle this. |
| // Replaced elements may size themselves using aspect ratios and block |
| // sizes, so we pass that on as well. |
| if (box->IsTable() || box->IsTablePart() || box->IsLayoutReplaced()) { |
| return {box->MinPreferredLogicalWidth(), box->MaxPreferredLogicalWidth()}; |
| } |
| } |
| |
| base::Optional<MinMaxSize> minmax; |
| if (NeedMinMaxSizeForContentContribution(parent_writing_mode, child_style)) { |
| // We need to set up a constraint space with correct fallback available |
| // inline size in case of orthogonal children. |
| NGConstraintSpace indefinite_constraint_space; |
| const NGConstraintSpace* child_constraint_space = nullptr; |
| if (!IsParallelWritingMode(parent_writing_mode, |
| child_style.GetWritingMode())) { |
| indefinite_constraint_space = |
| CreateIndefiniteConstraintSpaceForChild(parent_style, child); |
| child_constraint_space = &indefinite_constraint_space; |
| } |
| minmax = child.ComputeMinMaxSize(parent_writing_mode, input, |
| child_constraint_space); |
| } |
| |
| MinMaxSize sizes = ComputeMinAndMaxContentContribution(parent_writing_mode, |
| child_style, minmax); |
| if (IsParallelWritingMode(parent_writing_mode, child_style.GetWritingMode())) |
| box->SetPreferredLogicalWidthsFromNG(sizes); |
| return sizes; |
| } |
| |
| LayoutUnit ComputeInlineSizeForFragment( |
| const NGConstraintSpace& space, |
| NGLayoutInputNode node, |
| const base::Optional<NGBoxStrut>& border_padding, |
| const MinMaxSize* override_minmax) { |
| if (space.IsFixedSizeInline()) |
| return space.AvailableSize().inline_size; |
| |
| const ComputedStyle& style = node.Style(); |
| Length logical_width = style.LogicalWidth(); |
| if (logical_width.IsAuto() && space.IsShrinkToFit()) |
| logical_width = Length(kFitContent); |
| |
| LayoutBox* box = node.GetLayoutBox(); |
| // If we have usable cached min/max intrinsic sizes, use those if we can. They |
| // will normally also be constrained to {min,max}-inline-size, but not if |
| // percentages are involved. In such cases we'll have to calculate and apply |
| // the constraints on our own. |
| if (!box->PreferredLogicalWidthsDirty() && !override_minmax && |
| !style.LogicalMinWidth().IsPercentOrCalc() && |
| !style.LogicalMaxWidth().IsPercentOrCalc()) { |
| if (logical_width.GetType() == kFitContent) { |
| // This is not as easy as {min, max}.ShrinkToFit() because we also need |
| // to subtract inline margins from the available size. The code in |
| // ResolveInlineLength knows how to handle that, just call that. |
| |
| MinMaxSize min_and_max = {box->MinPreferredLogicalWidth(), |
| box->MaxPreferredLogicalWidth()}; |
| return ResolveInlineLength(space, style, min_and_max, logical_width, |
| LengthResolveType::kContentSize, |
| LengthResolvePhase::kLayout); |
| } |
| if (logical_width.GetType() == kMinContent) |
| return box->MinPreferredLogicalWidth(); |
| if (logical_width.GetType() == kMaxContent) |
| return box->MaxPreferredLogicalWidth(); |
| } |
| |
| base::Optional<MinMaxSize> min_and_max; |
| if (NeedMinMaxSize(space, style)) { |
| if (override_minmax) { |
| min_and_max = *override_minmax; |
| } else { |
| min_and_max = node.ComputeMinMaxSize(space.GetWritingMode(), |
| MinMaxSizeInput(), &space); |
| // Cache these computed values |
| MinMaxSize contribution = ComputeMinAndMaxContentContribution( |
| style.GetWritingMode(), style, min_and_max); |
| box->SetPreferredLogicalWidthsFromNG(contribution); |
| } |
| } |
| |
| LayoutUnit extent = ResolveInlineLength( |
| space, style, min_and_max, logical_width, LengthResolveType::kContentSize, |
| LengthResolvePhase::kLayout, border_padding); |
| |
| LayoutUnit max = ResolveInlineLength( |
| space, style, min_and_max, style.LogicalMaxWidth(), |
| LengthResolveType::kMaxSize, LengthResolvePhase::kLayout, border_padding); |
| LayoutUnit min = ResolveInlineLength( |
| space, style, min_and_max, style.LogicalMinWidth(), |
| LengthResolveType::kMinSize, LengthResolvePhase::kLayout, border_padding); |
| return ConstrainByMinMax(extent, min, max); |
| } |
| |
| namespace { |
| |
| // Computes the block-size for a fragment, ignoring the fixed block-size if set. |
| LayoutUnit ComputeBlockSizeForFragmentInternal( |
| const NGConstraintSpace& constraint_space, |
| const ComputedStyle& style, |
| LayoutUnit content_size, |
| const base::Optional<NGBoxStrut>& border_padding) { |
| LayoutUnit extent = |
| ResolveBlockLength(constraint_space, style, style.LogicalHeight(), |
| content_size, LengthResolveType::kContentSize, |
| LengthResolvePhase::kLayout, border_padding); |
| if (extent == NGSizeIndefinite) { |
| DCHECK_EQ(content_size, NGSizeIndefinite); |
| return extent; |
| } |
| |
| LayoutUnit max = ResolveBlockLength( |
| constraint_space, style, style.LogicalMaxHeight(), content_size, |
| LengthResolveType::kMaxSize, LengthResolvePhase::kLayout, border_padding); |
| LayoutUnit min = ResolveBlockLength( |
| constraint_space, style, style.LogicalMinHeight(), content_size, |
| LengthResolveType::kMinSize, LengthResolvePhase::kLayout, border_padding); |
| |
| return ConstrainByMinMax(extent, min, max); |
| } |
| |
| } // namespace |
| |
| LayoutUnit ComputeBlockSizeForFragment( |
| const NGConstraintSpace& constraint_space, |
| const ComputedStyle& style, |
| LayoutUnit content_size, |
| const base::Optional<NGBoxStrut>& border_padding) { |
| if (constraint_space.IsFixedSizeBlock()) |
| return constraint_space.AvailableSize().block_size; |
| |
| return ComputeBlockSizeForFragmentInternal(constraint_space, style, |
| content_size, border_padding); |
| } |
| |
| // Computes size for a replaced element. |
| NGLogicalSize ComputeReplacedSize( |
| const NGLayoutInputNode& node, |
| const NGConstraintSpace& space, |
| const base::Optional<MinMaxSize>& child_minmax) { |
| DCHECK(node.IsReplaced()); |
| |
| NGLogicalSize replaced_size; |
| |
| NGLogicalSize default_intrinsic_size; |
| base::Optional<LayoutUnit> computed_inline_size; |
| base::Optional<LayoutUnit> computed_block_size; |
| NGLogicalSize aspect_ratio; |
| |
| node.IntrinsicSize(&default_intrinsic_size, &computed_inline_size, |
| &computed_block_size, &aspect_ratio); |
| |
| const ComputedStyle& style = node.Style(); |
| Length inline_length = style.LogicalWidth(); |
| Length block_length = style.LogicalHeight(); |
| |
| // Compute inline size |
| if (inline_length.IsAuto()) { |
| if (block_length.IsAuto() || aspect_ratio.IsEmpty()) { |
| // Use intrinsic values if inline_size cannot be computed from block_size. |
| if (computed_inline_size.has_value()) |
| replaced_size.inline_size = computed_inline_size.value(); |
| else |
| replaced_size.inline_size = default_intrinsic_size.inline_size; |
| replaced_size.inline_size += |
| (ComputeBorders(space, style) + ComputePadding(space, style)) |
| .InlineSum(); |
| } else { |
| // inline_size is computed from block_size. |
| replaced_size.inline_size = |
| ResolveBlockLength( |
| space, style, block_length, default_intrinsic_size.block_size, |
| LengthResolveType::kContentSize, LengthResolvePhase::kLayout) * |
| aspect_ratio.inline_size / aspect_ratio.block_size; |
| } |
| } else { |
| // inline_size is resolved directly. |
| replaced_size.inline_size = ResolveInlineLength( |
| space, style, child_minmax, inline_length, |
| LengthResolveType::kContentSize, LengthResolvePhase::kLayout); |
| } |
| |
| // Compute block size |
| if (block_length.IsAuto()) { |
| if (inline_length.IsAuto() || aspect_ratio.IsEmpty()) { |
| // Use intrinsic values if block_size cannot be computed from inline_size. |
| if (computed_block_size.has_value()) |
| replaced_size.block_size = LayoutUnit(computed_block_size.value()); |
| else |
| replaced_size.block_size = default_intrinsic_size.block_size; |
| replaced_size.block_size += |
| (ComputeBorders(space, style) + ComputePadding(space, style)) |
| .BlockSum(); |
| } else { |
| // block_size is computed from inline_size. |
| replaced_size.block_size = |
| ResolveInlineLength(space, style, child_minmax, inline_length, |
| LengthResolveType::kContentSize, |
| LengthResolvePhase::kLayout) * |
| aspect_ratio.block_size / aspect_ratio.inline_size; |
| } |
| } else { |
| replaced_size.block_size = ResolveBlockLength( |
| space, style, block_length, default_intrinsic_size.block_size, |
| LengthResolveType::kContentSize, LengthResolvePhase::kLayout); |
| } |
| return replaced_size; |
| } |
| |
| bool SizeMayChange(const ComputedStyle& style, |
| const NGConstraintSpace& new_space, |
| const NGConstraintSpace& old_space) { |
| DCHECK_EQ(new_space.IsFixedSizeInline(), old_space.IsFixedSizeInline()); |
| DCHECK_EQ(new_space.IsFixedSizeBlock(), old_space.IsFixedSizeBlock()); |
| |
| // Go through all length properties, and, depending on length type |
| // (percentages, auto, etc.), check whether the constraint spaces differ in |
| // such a way that the resulting size *may* change. There are currently many |
| // possible false-positive situations here, as we don't rule out length |
| // changes that won't have any effect on the final size (e.g. if inline-size |
| // is 100px, max-inline-size is 50%, and percentage resolution inline size |
| // changes from 1000px to 500px). If the constraint space has "fixed" size in |
| // a dimension, we can skip checking properties in that dimension and just |
| // look for available size changes, since that's how a "fixed" constraint |
| // space works. |
| if (new_space.IsFixedSizeInline()) { |
| if (new_space.AvailableSize().inline_size != |
| old_space.AvailableSize().inline_size) |
| return true; |
| } else { |
| if (InlineLengthMayChange(style.LogicalWidth(), new_space, old_space) || |
| InlineLengthMayChange(style.LogicalMaxWidth(), new_space, old_space) || |
| InlineLengthMayChange(style.LogicalMaxWidth(), new_space, old_space)) |
| return true; |
| } |
| |
| if (new_space.IsFixedSizeBlock()) { |
| if (new_space.AvailableSize().block_size != |
| old_space.AvailableSize().block_size) |
| return true; |
| } else { |
| if (BlockLengthMayChange(style.LogicalHeight(), new_space, old_space) || |
| BlockLengthMayChange(style.LogicalMinHeight(), new_space, old_space) || |
| BlockLengthMayChange(style.LogicalMaxHeight(), new_space, old_space)) |
| return true; |
| } |
| |
| if (new_space.PercentageResolutionInlineSize() != |
| old_space.PercentageResolutionInlineSize()) { |
| // Percentage-based padding is resolved against the inline content box size |
| // of the containing block. |
| if (style.PaddingTop().IsPercentOrCalc() || |
| style.PaddingRight().IsPercentOrCalc() || |
| style.PaddingBottom().IsPercentOrCalc() || |
| style.PaddingLeft().IsPercentOrCalc()) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| int ResolveUsedColumnCount(int computed_count, |
| LayoutUnit computed_size, |
| LayoutUnit used_gap, |
| LayoutUnit available_size) { |
| if (computed_size == NGSizeIndefinite) { |
| DCHECK(computed_count); |
| return computed_count; |
| } |
| DCHECK_GT(computed_size, LayoutUnit()); |
| int count_from_width = |
| ((available_size + used_gap) / (computed_size + used_gap)).ToInt(); |
| count_from_width = std::max(1, count_from_width); |
| if (!computed_count) |
| return count_from_width; |
| return std::max(1, std::min(computed_count, count_from_width)); |
| } |
| |
| int ResolveUsedColumnCount(LayoutUnit available_size, |
| const ComputedStyle& style) { |
| LayoutUnit computed_column_inline_size = |
| style.HasAutoColumnWidth() |
| ? NGSizeIndefinite |
| : std::max(LayoutUnit(1), LayoutUnit(style.ColumnWidth())); |
| LayoutUnit gap = ResolveUsedColumnGap(available_size, style); |
| int computed_count = style.ColumnCount(); |
| return ResolveUsedColumnCount(computed_count, computed_column_inline_size, |
| gap, available_size); |
| } |
| |
| LayoutUnit ResolveUsedColumnInlineSize(int computed_count, |
| LayoutUnit computed_size, |
| LayoutUnit used_gap, |
| LayoutUnit available_size) { |
| int used_count = ResolveUsedColumnCount(computed_count, computed_size, |
| used_gap, available_size); |
| return std::max(((available_size + used_gap) / used_count) - used_gap, |
| LayoutUnit()); |
| } |
| |
| LayoutUnit ResolveUsedColumnInlineSize(LayoutUnit available_size, |
| const ComputedStyle& style) { |
| // Should only attempt to resolve this if columns != auto. |
| DCHECK(!style.HasAutoColumnCount() || !style.HasAutoColumnWidth()); |
| |
| LayoutUnit computed_size = |
| style.HasAutoColumnWidth() |
| ? NGSizeIndefinite |
| : std::max(LayoutUnit(1), LayoutUnit(style.ColumnWidth())); |
| int computed_count = style.HasAutoColumnCount() ? 0 : style.ColumnCount(); |
| LayoutUnit used_gap = ResolveUsedColumnGap(available_size, style); |
| return ResolveUsedColumnInlineSize(computed_count, computed_size, used_gap, |
| available_size); |
| } |
| |
| LayoutUnit ResolveUsedColumnGap(LayoutUnit available_size, |
| const ComputedStyle& style) { |
| if (style.ColumnGap().IsNormal()) |
| return LayoutUnit(style.GetFontDescription().ComputedPixelSize()); |
| return ValueForLength(style.ColumnGap().GetLength(), available_size); |
| } |
| |
| NGPhysicalBoxStrut ComputePhysicalMargins( |
| const ComputedStyle& style, |
| LayoutUnit percentage_resolution_size) { |
| if (!style.HasMargin()) |
| return NGPhysicalBoxStrut(); |
| |
| NGPhysicalBoxStrut physical_dim; |
| physical_dim.left = ResolveMarginPaddingLength(percentage_resolution_size, |
| style.MarginLeft()); |
| physical_dim.right = ResolveMarginPaddingLength(percentage_resolution_size, |
| style.MarginRight()); |
| physical_dim.top = |
| ResolveMarginPaddingLength(percentage_resolution_size, style.MarginTop()); |
| physical_dim.bottom = ResolveMarginPaddingLength(percentage_resolution_size, |
| style.MarginBottom()); |
| return physical_dim; |
| } |
| |
| NGBoxStrut ComputeMarginsFor(const NGConstraintSpace& constraint_space, |
| const ComputedStyle& style, |
| const NGConstraintSpace& compute_for) { |
| if (constraint_space.IsAnonymous()) |
| return NGBoxStrut(); |
| LayoutUnit percentage_resolution_size = |
| constraint_space.PercentageResolutionInlineSizeForParentWritingMode(); |
| return ComputePhysicalMargins(style, percentage_resolution_size) |
| .ConvertToLogical(compute_for.GetWritingMode(), compute_for.Direction()); |
| } |
| |
| NGBoxStrut ComputeMinMaxMargins(const ComputedStyle& parent_style, |
| NGLayoutInputNode child) { |
| // An inline child just produces line-boxes which don't have any margins. |
| if (child.IsInline()) |
| return NGBoxStrut(); |
| |
| Length inline_start_margin_length = |
| child.Style().MarginStartUsing(parent_style); |
| Length inline_end_margin_length = child.Style().MarginEndUsing(parent_style); |
| |
| // TODO(ikilpatrick): We may want to re-visit calculated margins at some |
| // point. Currently "margin-left: calc(10px + 50%)" will resolve to 0px, but |
| // 10px would be more correct, (as percentages resolve to zero). |
| NGBoxStrut margins; |
| if (inline_start_margin_length.IsFixed()) |
| margins.inline_start = LayoutUnit(inline_start_margin_length.Value()); |
| if (inline_end_margin_length.IsFixed()) |
| margins.inline_end = LayoutUnit(inline_end_margin_length.Value()); |
| |
| return margins; |
| } |
| |
| NGBoxStrut ComputeBorders(const NGConstraintSpace& constraint_space, |
| const ComputedStyle& style) { |
| // If we are producing an anonymous fragment (e.g. a column) we shouldn't |
| // have any borders. |
| if (constraint_space.IsAnonymous()) |
| return NGBoxStrut(); |
| |
| NGBoxStrut borders; |
| borders.inline_start = LayoutUnit(style.BorderStartWidth()); |
| borders.inline_end = LayoutUnit(style.BorderEndWidth()); |
| borders.block_start = LayoutUnit(style.BorderBeforeWidth()); |
| borders.block_end = LayoutUnit(style.BorderAfterWidth()); |
| return borders; |
| } |
| |
| NGBoxStrut ComputeBorders(const NGConstraintSpace& constraint_space, |
| const NGLayoutInputNode node) { |
| // If we are producing an anonymous fragment (e.g. a column), it has no |
| // borders, padding or scrollbars. Using the ones from the container can only |
| // cause trouble. |
| if (constraint_space.IsAnonymous()) |
| return NGBoxStrut(); |
| |
| if (node.GetLayoutBox()->IsTableCell()) { |
| LayoutBox* box = node.GetLayoutBox(); |
| return NGBoxStrut(box->BorderStart(), box->BorderEnd(), box->BorderBefore(), |
| box->BorderAfter()); |
| } |
| return ComputeBorders(constraint_space, node.Style()); |
| } |
| |
| NGBoxStrut ComputeIntrinsicPadding(const NGConstraintSpace& constraint_space, |
| const NGLayoutInputNode node) { |
| if (constraint_space.IsAnonymous() || !node.IsTableCell()) |
| return NGBoxStrut(); |
| |
| // At the moment we just access the values set by the parent table layout. |
| // Once we have a NGTableLayoutAlgorithm this should pass the intrinsic |
| // padding via the constraint space object. |
| |
| // TODO(karlo): intrinsic padding can sometimes be negative; that seems |
| // insane, but works in the old code; in NG it trips DCHECKs. |
| return {LayoutUnit(), LayoutUnit(), node.IntrinsicPaddingBlockStart(), |
| node.IntrinsicPaddingBlockEnd()}; |
| } |
| |
| NGBoxStrut ComputePadding(const NGConstraintSpace& constraint_space, |
| const ComputedStyle& style) { |
| // If we are producing an anonymous fragment (e.g. a column) we shouldn't |
| // have any padding. |
| if (constraint_space.IsAnonymous()) |
| return NGBoxStrut(); |
| |
| LayoutUnit percentage_resolution_size = |
| constraint_space.PercentageResolutionInlineSizeForParentWritingMode(); |
| NGBoxStrut padding; |
| padding.inline_start = ResolveMarginPaddingLength(percentage_resolution_size, |
| style.PaddingStart()); |
| padding.inline_end = ResolveMarginPaddingLength(percentage_resolution_size, |
| style.PaddingEnd()); |
| padding.block_start = ResolveMarginPaddingLength(percentage_resolution_size, |
| style.PaddingBefore()); |
| padding.block_end = ResolveMarginPaddingLength(percentage_resolution_size, |
| style.PaddingAfter()); |
| return padding; |
| } |
| |
| |
| bool NeedsInlineSizeToResolveLineLeft(const ComputedStyle& style, |
| const ComputedStyle& container_style) { |
| // In RTL, there's no block alignment where we can guarantee that line-left |
| // doesn't depend on the inline size of a fragment. |
| if (IsRtl(container_style.Direction())) |
| return true; |
| |
| return BlockAlignment(style, container_style) != EBlockAlignment::kStart; |
| } |
| |
| void ResolveInlineMargins(const ComputedStyle& style, |
| const ComputedStyle& container_style, |
| LayoutUnit available_inline_size, |
| LayoutUnit inline_size, |
| NGBoxStrut* margins) { |
| DCHECK(margins) << "Margins cannot be NULL here"; |
| const LayoutUnit used_space = inline_size + margins->InlineSum(); |
| const LayoutUnit available_space = available_inline_size - used_space; |
| if (available_space > LayoutUnit()) { |
| EBlockAlignment alignment = BlockAlignment(style, container_style); |
| if (alignment == EBlockAlignment::kCenter) |
| margins->inline_start += available_space / 2; |
| else if (alignment == EBlockAlignment::kEnd) |
| margins->inline_start += available_space; |
| } |
| margins->inline_end = |
| available_inline_size - inline_size - margins->inline_start; |
| } |
| |
| LayoutUnit LineOffsetForTextAlign(ETextAlign text_align, |
| TextDirection direction, |
| LayoutUnit space_left, |
| LayoutUnit trailing_spaces_width) { |
| bool is_ltr = IsLtr(direction); |
| if (text_align == ETextAlign::kStart || text_align == ETextAlign::kJustify) |
| text_align = is_ltr ? ETextAlign::kLeft : ETextAlign::kRight; |
| else if (text_align == ETextAlign::kEnd) |
| text_align = is_ltr ? ETextAlign::kRight : ETextAlign::kLeft; |
| |
| switch (text_align) { |
| case ETextAlign::kLeft: |
| case ETextAlign::kWebkitLeft: { |
| // The direction of the block should determine what happens with wide |
| // lines. In particular with RTL blocks, wide lines should still spill |
| // out to the left. |
| if (is_ltr) |
| return LayoutUnit(); |
| return space_left.ClampPositiveToZero(); |
| } |
| case ETextAlign::kRight: |
| case ETextAlign::kWebkitRight: { |
| // In RTL, trailing spaces appear on the left of the line. |
| if (UNLIKELY(!is_ltr)) |
| return space_left - trailing_spaces_width; |
| // Wide lines spill out of the block based off direction. |
| // So even if text-align is right, if direction is LTR, wide lines |
| // should overflow out of the right side of the block. |
| if (space_left > LayoutUnit()) |
| return space_left; |
| return LayoutUnit(); |
| } |
| case ETextAlign::kCenter: |
| case ETextAlign::kWebkitCenter: { |
| if (is_ltr) |
| return (space_left / 2).ClampNegativeToZero(); |
| // In RTL, trailing spaces appear on the left of the line. |
| if (space_left > LayoutUnit()) |
| return (space_left / 2).ClampNegativeToZero() - trailing_spaces_width; |
| // In RTL, wide lines should spill out to the left, same as kRight. |
| return space_left - trailing_spaces_width; |
| } |
| default: |
| NOTREACHED(); |
| return LayoutUnit(); |
| } |
| } |
| |
| LayoutUnit InlineOffsetForTextAlign(const ComputedStyle& container_style, |
| LayoutUnit space_left) { |
| TextDirection direction = container_style.Direction(); |
| LayoutUnit line_offset = LineOffsetForTextAlign( |
| container_style.GetTextAlign(), direction, space_left, LayoutUnit()); |
| return IsLtr(direction) ? line_offset : space_left - line_offset; |
| } |
| |
| bool ClampScrollbarToContentBox(NGBoxStrut* scrollbars, |
| LayoutUnit content_box_inline_size) { |
| DCHECK(scrollbars->InlineSum()); |
| if (scrollbars->InlineSum() <= content_box_inline_size) |
| return false; |
| if (scrollbars->inline_end) { |
| DCHECK(!scrollbars->inline_start); |
| scrollbars->inline_end = content_box_inline_size; |
| } else { |
| DCHECK(scrollbars->inline_start); |
| scrollbars->inline_start = content_box_inline_size; |
| } |
| return true; |
| } |
| |
| NGBoxStrut CalculateBorderScrollbarPadding( |
| const NGConstraintSpace& constraint_space, |
| const NGBlockNode node) { |
| // If we are producing an anonymous fragment (e.g. a column), it has no |
| // borders, padding or scrollbars. Using the ones from the container can only |
| // cause trouble. |
| if (constraint_space.IsAnonymous()) |
| return NGBoxStrut(); |
| return ComputeBorders(constraint_space, node) + |
| ComputePadding(constraint_space, node.Style()) + |
| ComputeIntrinsicPadding(constraint_space, node) + |
| node.GetScrollbarSizes(); |
| } |
| |
| NGLogicalSize CalculateBorderBoxSize( |
| const NGConstraintSpace& constraint_space, |
| const NGBlockNode& node, |
| LayoutUnit block_content_size, |
| const base::Optional<NGBoxStrut>& border_padding) { |
| // If we have a percentage size, we need to set the |
| // HasPercentHeightDescendants flag correctly so that flexboz knows it may |
| // need to redo layout and can also do some performance optimizations. |
| if (node.Style().LogicalHeight().IsPercentOrCalc() || |
| node.Style().LogicalMinHeight().IsPercentOrCalc() || |
| node.Style().LogicalMaxHeight().IsPercentOrCalc() || |
| (node.GetLayoutBox()->IsFlexItemIncludingNG() && |
| node.Style().FlexBasis().IsPercentOrCalc())) { |
| // This call has the side-effect of setting HasPercentHeightDescendants |
| // correctly. |
| node.GetLayoutBox()->ComputePercentageLogicalHeight(Length(0, kPercent)); |
| } |
| |
| return NGLogicalSize( |
| ComputeInlineSizeForFragment(constraint_space, node, border_padding), |
| ComputeBlockSizeForFragment(constraint_space, node.Style(), |
| block_content_size, border_padding)); |
| } |
| |
| NGLogicalSize ShrinkAvailableSize(NGLogicalSize size, const NGBoxStrut& inset) { |
| DCHECK_NE(size.inline_size, NGSizeIndefinite); |
| size.inline_size -= inset.InlineSum(); |
| size.inline_size = std::max(size.inline_size, LayoutUnit()); |
| |
| if (size.block_size != NGSizeIndefinite) { |
| size.block_size -= inset.BlockSum(); |
| size.block_size = std::max(size.block_size, LayoutUnit()); |
| } |
| |
| return size; |
| } |
| |
| namespace { |
| |
| // Implements the common part of the child percentage size calculation. Deals |
| // with how percentages are propagated from parent to child in quirks mode. |
| NGLogicalSize AdjustChildPercentageSizeForQuirksAndFlex( |
| const NGConstraintSpace& space, |
| const NGBlockNode node, |
| NGLogicalSize child_percentage_size, |
| LayoutUnit parent_percentage_block_size) { |
| // Flex items may have a fixed block-size, but children shouldn't resolve |
| // their percentages against this. |
| if (space.IsFixedSizeBlock() && !space.FixedSizeBlockIsDefinite()) { |
| DCHECK(node.IsFlexItem()); |
| child_percentage_size.block_size = NGSizeIndefinite; |
| return child_percentage_size; |
| } |
| |
| // In quirks mode the percentage resolution height is passed from parent to |
| // child. |
| // https://quirks.spec.whatwg.org/#the-percentage-height-calculation-quirk |
| if (child_percentage_size.block_size == NGSizeIndefinite && |
| node.GetDocument().InQuirksMode() && !node.Style().IsDisplayTableType() && |
| !node.Style().HasOutOfFlowPosition()) { |
| child_percentage_size.block_size = parent_percentage_block_size; |
| } |
| |
| return child_percentage_size; |
| } |
| |
| } // namespace |
| |
| NGLogicalSize CalculateChildPercentageSize( |
| const NGConstraintSpace& space, |
| const NGBlockNode node, |
| const NGLogicalSize& child_available_size) { |
| // Anonymous block or spaces should pass the percent size straight through. |
| if (space.IsAnonymous() || node.IsAnonymousBlock()) |
| return space.PercentageResolutionSize(); |
| |
| NGLogicalSize child_percentage_size = child_available_size; |
| |
| bool is_table_cell_in_measure_phase = |
| node.IsTableCell() && !space.IsFixedSizeBlock(); |
| |
| // Table cells which are measuring their content, force their children to |
| // have an indefinite percentage resolution size. |
| if (is_table_cell_in_measure_phase) { |
| child_percentage_size.block_size = NGSizeIndefinite; |
| return child_percentage_size; |
| } |
| |
| // Table cell children don't apply the "percentage-quirk". I.e. if their |
| // percentage resolution block-size is indefinite, they don't pass through |
| // their parent's percentage resolution block-size. |
| if (space.TableCellChildLayoutPhase() != |
| NGTableCellChildLayoutPhase::kNotTableCellChild) |
| return child_percentage_size; |
| |
| return AdjustChildPercentageSizeForQuirksAndFlex( |
| space, node, child_percentage_size, |
| space.PercentageResolutionBlockSize()); |
| } |
| |
| NGLogicalSize CalculateReplacedChildPercentageSize( |
| const NGConstraintSpace& space, |
| const NGBlockNode node, |
| NGLogicalSize border_box_size, |
| const NGBoxStrut& border_scrollbar_padding, |
| const NGBoxStrut& border_padding) { |
| // Anonymous block or spaces should pass the percent size straight through. |
| if (space.IsAnonymous() || node.IsAnonymousBlock()) |
| return space.ReplacedPercentageResolutionSize(); |
| |
| bool has_resolvable_block_size = !node.Style().LogicalHeight().IsAuto() || |
| !node.Style().LogicalMinHeight().IsAuto(); |
| |
| bool is_table_cell_in_layout_phase = |
| node.IsTableCell() && space.IsFixedSizeBlock(); |
| |
| // Table cells in the "layout" phase have a fixed block-size. However |
| // replaced children should resolve their percentages against the size given |
| // in the "measure" phase. |
| // |
| // To handle this we recalculate the border-box block-size, ignoring the |
| // fixed size constraint. |
| if (is_table_cell_in_layout_phase && has_resolvable_block_size) { |
| border_box_size.block_size = ComputeBlockSizeForFragmentInternal( |
| space, node.Style(), NGSizeIndefinite, border_padding); |
| } |
| |
| NGLogicalSize child_percentage_size = |
| ShrinkAvailableSize(border_box_size, border_scrollbar_padding); |
| |
| return AdjustChildPercentageSizeForQuirksAndFlex( |
| space, node, child_percentage_size, |
| space.ReplacedPercentageResolutionBlockSize()); |
| } |
| |
| } // namespace blink |