| // 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_absolute_utils.h" |
| |
| #include <algorithm> |
| #include "third_party/blink/renderer/core/layout/ng/geometry/ng_static_position.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_length_utils.h" |
| #include "third_party/blink/renderer/core/style/computed_style.h" |
| #include "third_party/blink/renderer/platform/geometry/length_functions.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| // Dominant side: |
| // htb ltr => top left |
| // htb rtl => top right |
| // vlr ltr => top left |
| // vlr rtl => bottom left |
| // vrl ltr => top right |
| // vrl rtl => bottom right |
| bool IsLeftDominant(const WritingDirectionMode writing_direction) { |
| return (writing_direction.GetWritingMode() != WritingMode::kVerticalRl) && |
| !(writing_direction.IsHorizontal() && writing_direction.IsRtl()); |
| } |
| |
| bool IsTopDominant(const WritingDirectionMode writing_direction) { |
| return writing_direction.IsHorizontal() || writing_direction.IsLtr(); |
| } |
| |
| // A direction agnostic version of |NGLogicalStaticPosition::InlineEdge|, and |
| // |NGLogicalStaticPosition::BlockEdge|. |
| enum StaticPositionEdge { kStart, kCenter, kEnd }; |
| |
| inline StaticPositionEdge GetStaticPositionEdge( |
| NGLogicalStaticPosition::InlineEdge inline_edge) { |
| switch (inline_edge) { |
| case NGLogicalStaticPosition::InlineEdge::kInlineStart: |
| return kStart; |
| case NGLogicalStaticPosition::InlineEdge::kInlineCenter: |
| return kCenter; |
| case NGLogicalStaticPosition::InlineEdge::kInlineEnd: |
| return kEnd; |
| } |
| } |
| |
| inline StaticPositionEdge GetStaticPositionEdge( |
| NGLogicalStaticPosition::BlockEdge block_edge) { |
| switch (block_edge) { |
| case NGLogicalStaticPosition::BlockEdge::kBlockStart: |
| return kStart; |
| case NGLogicalStaticPosition::BlockEdge::kBlockCenter: |
| return kCenter; |
| case NGLogicalStaticPosition::BlockEdge::kBlockEnd: |
| return kEnd; |
| } |
| } |
| |
| inline LayoutUnit StaticPositionStartInset(StaticPositionEdge edge, |
| LayoutUnit static_position_offset, |
| LayoutUnit size) { |
| switch (edge) { |
| case kStart: |
| return static_position_offset; |
| case kCenter: |
| return static_position_offset - (size / 2); |
| case kEnd: |
| return static_position_offset - size; |
| } |
| } |
| |
| inline LayoutUnit StaticPositionEndInset(StaticPositionEdge edge, |
| LayoutUnit static_position_offset, |
| LayoutUnit available_size, |
| LayoutUnit size) { |
| switch (edge) { |
| case kStart: |
| return available_size - static_position_offset - size; |
| case kCenter: |
| return available_size - static_position_offset - (size / 2); |
| case kEnd: |
| return available_size - static_position_offset; |
| } |
| } |
| |
| LayoutUnit ComputeShrinkToFitSize( |
| bool is_table, |
| const base::Optional<MinMaxSizes>& min_max_sizes, |
| LayoutUnit available_size, |
| LayoutUnit computed_available_size, |
| LayoutUnit margin_start, |
| LayoutUnit margin_end) { |
| // The available-size given to tables isn't allowed to exceed the |
| // available-size of the containing-block. |
| if (is_table) |
| computed_available_size = std::min(computed_available_size, available_size); |
| return min_max_sizes->ShrinkToFit( |
| (computed_available_size - margin_start - margin_end) |
| .ClampNegativeToZero()); |
| } |
| |
| // Implement the absolute size resolution algorithm. |
| // https://www.w3.org/TR/css-position-3/#abs-non-replaced-width |
| // https://www.w3.org/TR/css-position-3/#abs-non-replaced-height |
| // |min_max_sizes| can have no value if an element is replaced, and has no |
| // intrinsic width or height, but has an aspect ratio. |
| void ComputeAbsoluteSize(const LayoutUnit border_padding_size, |
| const base::Optional<MinMaxSizes>& min_max_sizes, |
| const LayoutUnit margin_percentage_resolution_size, |
| const LayoutUnit available_size, |
| const Length& margin_start_length, |
| const Length& margin_end_length, |
| const Length& inset_start_length, |
| const Length& inset_end_length, |
| const LayoutUnit min_size, |
| const LayoutUnit max_size, |
| const LayoutUnit static_position_offset, |
| StaticPositionEdge static_position_edge, |
| bool is_start_dominant, |
| bool is_block_direction, |
| bool is_table, |
| base::Optional<LayoutUnit> size, |
| LayoutUnit* size_out, |
| LayoutUnit* inset_start_out, |
| LayoutUnit* inset_end_out, |
| LayoutUnit* margin_start_out, |
| LayoutUnit* margin_end_out) { |
| DCHECK_NE(available_size, kIndefiniteSize); |
| |
| base::Optional<LayoutUnit> margin_start; |
| if (!margin_start_length.IsAuto()) { |
| margin_start = MinimumValueForLength(margin_start_length, |
| margin_percentage_resolution_size); |
| } |
| base::Optional<LayoutUnit> margin_end; |
| if (!margin_end_length.IsAuto()) { |
| margin_end = MinimumValueForLength(margin_end_length, |
| margin_percentage_resolution_size); |
| } |
| base::Optional<LayoutUnit> inset_start; |
| if (!inset_start_length.IsAuto()) { |
| inset_start = MinimumValueForLength(inset_start_length, available_size); |
| } |
| base::Optional<LayoutUnit> inset_end; |
| if (!inset_end_length.IsAuto()) { |
| inset_end = MinimumValueForLength(inset_end_length, available_size); |
| } |
| |
| // Solving the equation: |
| // |inset_start| + |margin_start| + |size| + |margin_end| + |inset_end| = |
| // |available_size| |
| if (!inset_start && !inset_end && !size) { |
| // "If all three of left, width, and right are auto:" |
| if (!margin_start) |
| margin_start = LayoutUnit(); |
| if (!margin_end) |
| margin_end = LayoutUnit(); |
| |
| LayoutUnit computed_available_size; |
| switch (static_position_edge) { |
| case kStart: |
| // The available-size for the start static-position "grows" towards the |
| // end edge. |
| // | *----------->| |
| computed_available_size = available_size - static_position_offset; |
| break; |
| case kCenter: |
| // The available-size for the center static-position "grows" towards |
| // both edges (equally), and stops when it hits the first one. |
| // |<-----*----> | |
| computed_available_size = |
| 2 * std::min(static_position_offset, |
| available_size - static_position_offset); |
| break; |
| case kEnd: |
| // The available-size for the end static-position "grows" towards the |
| // start edge. |
| // |<-----* | |
| computed_available_size = static_position_offset; |
| break; |
| } |
| size = ComputeShrinkToFitSize(is_table, min_max_sizes, available_size, |
| computed_available_size, *margin_start, |
| *margin_end); |
| LayoutUnit margin_size = *size + *margin_start + *margin_end; |
| if (is_start_dominant) { |
| inset_start = StaticPositionStartInset( |
| static_position_edge, static_position_offset, margin_size); |
| } else { |
| inset_end = |
| StaticPositionEndInset(static_position_edge, static_position_offset, |
| available_size, margin_size); |
| } |
| } else if (inset_start && inset_end && size) { |
| // "If left, right, and width are not auto:" |
| // Compute margins. |
| LayoutUnit margin_space = |
| available_size - *inset_start - *inset_end - *size; |
| |
| if (!margin_start && !margin_end) { |
| // When both margins are auto. |
| if (margin_space > 0 || is_block_direction) { |
| margin_start = margin_space / 2; |
| margin_end = margin_space - *margin_start; |
| } else { |
| // Margins are negative. |
| if (is_start_dominant) { |
| margin_start = LayoutUnit(); |
| margin_end = margin_space; |
| } else { |
| margin_start = margin_space; |
| margin_end = LayoutUnit(); |
| } |
| } |
| } else if (!margin_start) { |
| margin_start = margin_space - *margin_end; |
| } else if (!margin_end) { |
| margin_end = margin_space - *margin_start; |
| } else { |
| // Are the values over-constrained? |
| LayoutUnit margin_extra = margin_space - *margin_start - *margin_end; |
| if (margin_extra) { |
| // Relax the end. |
| if (is_start_dominant) |
| inset_end = *inset_end + margin_extra; |
| else |
| inset_start = *inset_start + margin_extra; |
| } |
| } |
| } |
| |
| // Set any unknown margins. |
| if (!margin_start) |
| margin_start = LayoutUnit(); |
| if (!margin_end) |
| margin_end = LayoutUnit(); |
| |
| // Rules 1 through 3: 2 out of 3 are unknown. |
| if (!inset_start && !size) { |
| // Rule 1: left/width are unknown. |
| DCHECK(inset_end.has_value()); |
| LayoutUnit computed_available_size = available_size - *inset_end; |
| size = ComputeShrinkToFitSize(is_table, min_max_sizes, available_size, |
| computed_available_size, *margin_start, |
| *margin_end); |
| } else if (!inset_start && !inset_end) { |
| // Rule 2. |
| DCHECK(size.has_value()); |
| LayoutUnit margin_size = *size + *margin_start + *margin_end; |
| if (is_start_dominant) { |
| inset_start = StaticPositionStartInset( |
| static_position_edge, static_position_offset, margin_size); |
| } else { |
| inset_end = |
| StaticPositionEndInset(static_position_edge, static_position_offset, |
| available_size, margin_size); |
| } |
| } else if (!size && !inset_end) { |
| // Rule 3. |
| LayoutUnit computed_available_size = available_size - *inset_start; |
| size = ComputeShrinkToFitSize(is_table, min_max_sizes, available_size, |
| computed_available_size, *margin_start, |
| *margin_end); |
| } |
| |
| // Rules 4 through 6: 1 out of 3 are unknown. |
| if (!inset_start) { |
| inset_start = |
| available_size - *size - *inset_end - *margin_start - *margin_end; |
| } else if (!inset_end) { |
| inset_end = |
| available_size - *size - *inset_start - *margin_start - *margin_end; |
| } else if (!size) { |
| LayoutUnit computed_available_size = |
| available_size - *inset_start - *inset_end; |
| if (is_table) { |
| size = ComputeShrinkToFitSize(is_table, min_max_sizes, available_size, |
| computed_available_size, *margin_start, |
| *margin_end); |
| } else { |
| size = computed_available_size - *margin_start - *margin_end; |
| } |
| } |
| |
| // If calculated |size| is outside of min/max constraints, rerun the |
| // algorithm with the constrained |size|. |
| LayoutUnit constrained_size = ConstrainByMinMax(*size, min_size, max_size); |
| if (size != constrained_size) { |
| // Because this function only changes "size" when it's not already set, it |
| // is safe to recursively call ourselves here because on the second call it |
| // is guaranteed to be within |min_size| and |max_size|. |
| ComputeAbsoluteSize( |
| border_padding_size, min_max_sizes, margin_percentage_resolution_size, |
| available_size, margin_start_length, margin_end_length, |
| inset_start_length, inset_end_length, min_size, max_size, |
| static_position_offset, static_position_edge, is_start_dominant, |
| is_block_direction, is_table, constrained_size, size_out, |
| inset_start_out, inset_end_out, margin_start_out, margin_end_out); |
| return; |
| } |
| |
| // Negative sizes are not allowed. |
| *size_out = std::max(*size, border_padding_size); |
| *inset_start_out = *inset_start + *margin_start; |
| *inset_end_out = *inset_end + *margin_end; |
| *margin_start_out = *margin_start; |
| *margin_end_out = *margin_end; |
| } |
| |
| } // namespace |
| |
| // NOTE: Out-of-flow positioned tables require special handling: |
| // - The specified inline-size/block-size is always considered as 'auto', and |
| // instead treated as an additional "min" constraint. |
| // - They can't be "stretched" by inset constraints, ("left: 0; right: 0;"), |
| // instead they always perform shrink-to-fit sizing within this |
| // available-size, (and this is why we always compute the min/max content |
| // sizes for them). |
| // - When performing shrink-to-fit sizing, the given available size can never |
| // exceed the available-size of the containing-block (e.g. with insets |
| // similar to: "left: -100px; right: -100px"). |
| |
| bool AbsoluteNeedsChildInlineSize(const NGBlockNode& node) { |
| if (node.IsTable()) |
| return true; |
| const auto& style = node.Style(); |
| return style.LogicalWidth().IsContentOrIntrinsic() || |
| style.LogicalMinWidth().IsContentOrIntrinsic() || |
| style.LogicalMaxWidth().IsContentOrIntrinsic() || |
| (style.LogicalWidth().IsAuto() && |
| (style.LogicalLeft().IsAuto() || style.LogicalRight().IsAuto())); |
| } |
| |
| bool AbsoluteNeedsChildBlockSize(const NGBlockNode& node) { |
| if (node.IsTable()) |
| return true; |
| const auto& style = node.Style(); |
| return style.LogicalHeight().IsContentOrIntrinsic() || |
| style.LogicalMinHeight().IsContentOrIntrinsic() || |
| style.LogicalMaxHeight().IsContentOrIntrinsic() || |
| (style.LogicalHeight().IsAuto() && |
| (style.LogicalTop().IsAuto() || style.LogicalBottom().IsAuto())); |
| } |
| |
| bool IsInlineSizeComputableFromBlockSize(const NGBlockNode& node) { |
| const auto& style = node.Style(); |
| DCHECK(style.HasOutOfFlowPosition()); |
| if (style.AspectRatio().IsAuto()) |
| return false; |
| // An explicit block size should take precedence over specified insets. |
| bool have_inline_size = |
| style.LogicalWidth().IsFixed() || style.LogicalWidth().IsPercentOrCalc(); |
| bool have_block_size = style.LogicalHeight().IsFixed() || |
| style.LogicalHeight().IsPercentOrCalc(); |
| if (have_inline_size) |
| return false; |
| if (have_block_size) |
| return true; |
| // If we have block insets but no inline insets, we compute based on the |
| // insets. |
| return !AbsoluteNeedsChildBlockSize(node) && |
| AbsoluteNeedsChildInlineSize(node); |
| } |
| |
| void ComputeOutOfFlowInlineDimensions( |
| const NGBlockNode& node, |
| const NGConstraintSpace& space, |
| const NGBoxStrut& border_padding, |
| const NGLogicalStaticPosition& static_position, |
| const base::Optional<MinMaxSizes>& minmax_content_sizes, |
| const base::Optional<MinMaxSizes>& minmax_intrinsic_sizes_for_ar, |
| const base::Optional<LogicalSize>& replaced_size, |
| const WritingDirectionMode container_writing_direction, |
| NGLogicalOutOfFlowDimensions* dimensions) { |
| DCHECK(dimensions); |
| |
| const auto& style = node.Style(); |
| Length min_inline_length = style.LogicalMinWidth(); |
| base::Optional<MinMaxSizes> min_size_minmax = minmax_content_sizes; |
| // We don't need to check for IsInlineSizeComputableFromBlockSize; this is |
| // done by the caller. |
| if (minmax_intrinsic_sizes_for_ar) { |
| min_inline_length = Length::MinIntrinsic(); |
| min_size_minmax = minmax_intrinsic_sizes_for_ar; |
| } |
| LayoutUnit min_inline_size = ResolveMinInlineLength( |
| space, style, border_padding, min_size_minmax, min_inline_length); |
| LayoutUnit max_inline_size = |
| ResolveMaxInlineLength(space, style, border_padding, minmax_content_sizes, |
| style.LogicalMaxWidth()); |
| |
| // This implements the transferred min/max sizes per |
| // https://drafts.csswg.org/css-sizing-4/#aspect-ratio |
| if (!style.AspectRatio().IsAuto() && |
| dimensions->size.block_size == kIndefiniteSize) { |
| MinMaxSizes sizes = |
| ComputeMinMaxInlineSizesFromAspectRatio(space, style, border_padding); |
| min_inline_size = std::max(sizes.min_size, min_inline_size); |
| max_inline_size = std::min(sizes.max_size, max_inline_size); |
| } |
| |
| // Tables are never allowed to go below their min-content size. |
| const bool is_table = node.IsTable(); |
| if (is_table) |
| min_inline_size = std::max(min_inline_size, minmax_content_sizes->min_size); |
| |
| base::Optional<LayoutUnit> inline_size; |
| if (!style.LogicalWidth().IsAuto()) { |
| inline_size = |
| ResolveMainInlineLength(space, style, border_padding, |
| minmax_content_sizes, style.LogicalWidth()); |
| } else if (replaced_size.has_value()) { |
| inline_size = replaced_size->inline_size; |
| } else if (IsInlineSizeComputableFromBlockSize(node)) { |
| DCHECK(minmax_content_sizes.has_value()); |
| inline_size = minmax_content_sizes->min_size; |
| } |
| |
| const auto writing_direction = style.GetWritingDirection(); |
| bool is_start_dominant; |
| if (writing_direction.IsHorizontal()) { |
| is_start_dominant = IsLeftDominant(container_writing_direction) == |
| IsLeftDominant(writing_direction); |
| } else { |
| is_start_dominant = IsTopDominant(container_writing_direction) == |
| IsTopDominant(writing_direction); |
| } |
| |
| ComputeAbsoluteSize( |
| border_padding.InlineSum(), minmax_content_sizes, |
| space.PercentageResolutionInlineSizeForParentWritingMode(), |
| space.AvailableSize().inline_size, style.MarginStart(), style.MarginEnd(), |
| style.LogicalInlineStart(), style.LogicalInlineEnd(), min_inline_size, |
| max_inline_size, static_position.offset.inline_offset, |
| GetStaticPositionEdge(static_position.inline_edge), is_start_dominant, |
| false /* is_block_direction */, is_table, inline_size, |
| &dimensions->size.inline_size, &dimensions->inset.inline_start, |
| &dimensions->inset.inline_end, &dimensions->margins.inline_start, |
| &dimensions->margins.inline_end); |
| } |
| |
| void ComputeOutOfFlowBlockDimensions( |
| const NGBlockNode& node, |
| const NGConstraintSpace& space, |
| const NGBoxStrut& border_padding, |
| const NGLogicalStaticPosition& static_position, |
| const base::Optional<LayoutUnit>& child_block_size, |
| const base::Optional<LogicalSize>& replaced_size, |
| const WritingDirectionMode container_writing_direction, |
| NGLogicalOutOfFlowDimensions* dimensions) { |
| const auto& style = node.Style(); |
| // After partial size has been computed, child block size is either unknown, |
| // or fully computed, there is no minmax. To express this, a 'fixed' minmax |
| // is created where min and max are the same. |
| base::Optional<MinMaxSizes> min_max_sizes; |
| if (child_block_size.has_value()) |
| min_max_sizes = MinMaxSizes{*child_block_size, *child_block_size}; |
| |
| LayoutUnit child_block_size_or_indefinite = |
| child_block_size.value_or(kIndefiniteSize); |
| |
| LayoutUnit min_block_size = ResolveMinBlockLength( |
| space, style, border_padding, style.LogicalMinHeight()); |
| LayoutUnit max_block_size = ResolveMaxBlockLength( |
| space, style, border_padding, style.LogicalMaxHeight()); |
| |
| // Tables are never allowed to go below their "auto" block-size. |
| const bool is_table = node.IsTable(); |
| if (is_table) |
| min_block_size = std::max(min_block_size, min_max_sizes->min_size); |
| |
| base::Optional<LayoutUnit> block_size; |
| if (!style.LogicalHeight().IsAuto()) { |
| block_size = ResolveMainBlockLength(space, style, border_padding, |
| style.LogicalHeight(), |
| child_block_size_or_indefinite); |
| } else if (replaced_size.has_value()) { |
| block_size = replaced_size->block_size; |
| } |
| |
| const auto writing_direction = style.GetWritingDirection(); |
| bool is_start_dominant; |
| if (writing_direction.IsHorizontal()) { |
| is_start_dominant = IsTopDominant(container_writing_direction) == |
| IsTopDominant(writing_direction); |
| } else { |
| is_start_dominant = IsLeftDominant(container_writing_direction) == |
| IsLeftDominant(writing_direction); |
| } |
| |
| ComputeAbsoluteSize( |
| border_padding.BlockSum(), min_max_sizes, |
| space.PercentageResolutionInlineSizeForParentWritingMode(), |
| space.AvailableSize().block_size, style.MarginBefore(), |
| style.MarginAfter(), style.LogicalTop(), style.LogicalBottom(), |
| min_block_size, max_block_size, static_position.offset.block_offset, |
| GetStaticPositionEdge(static_position.block_edge), is_start_dominant, |
| true /* is_block_direction */, is_table, block_size, |
| &dimensions->size.block_size, &dimensions->inset.block_start, |
| &dimensions->inset.block_end, &dimensions->margins.block_start, |
| &dimensions->margins.block_end); |
| } |
| |
| } // namespace blink |