blob: 07f5dcc54ec0e7e378788c2da7ebb19a62e56784 [file] [log] [blame]
// 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_block_layout_algorithm.h"
#include <algorithm>
#include <memory>
#include <utility>
#include "base/optional.h"
#include "third_party/blink/renderer/core/frame/web_feature.h"
#include "third_party/blink/renderer/core/layout/layout_object.h"
#include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_node.h"
#include "third_party/blink/renderer/core/layout/ng/inline/ng_physical_line_box_fragment.h"
#include "third_party/blink/renderer/core/layout/ng/list/ng_unpositioned_list_marker.h"
#include "third_party/blink/renderer/core/layout/ng/ng_block_child_iterator.h"
#include "third_party/blink/renderer/core/layout/ng/ng_block_layout_algorithm_utils.h"
#include "third_party/blink/renderer/core/layout/ng/ng_box_fragment_builder.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_early_break.h"
#include "third_party/blink/renderer/core/layout/ng/ng_floats_utils.h"
#include "third_party/blink/renderer/core/layout/ng/ng_fragment.h"
#include "third_party/blink/renderer/core/layout/ng/ng_fragmentation_utils.h"
#include "third_party/blink/renderer/core/layout/ng/ng_layout_result.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_positioned_float.h"
#include "third_party/blink/renderer/core/layout/ng/ng_space_utils.h"
#include "third_party/blink/renderer/core/layout/ng/ng_unpositioned_float.h"
#include "third_party/blink/renderer/core/layout/text_autosizer.h"
#include "third_party/blink/renderer/core/paint/ng/ng_paint_fragment.h"
#include "third_party/blink/renderer/core/style/computed_style.h"
namespace blink {
namespace {
inline scoped_refptr<const NGLayoutResult> LayoutBlockChild(
const NGConstraintSpace& space,
const NGBreakToken* break_token,
const NGEarlyBreak* early_break,
NGBlockNode* node) {
const NGEarlyBreak* early_break_in_child = nullptr;
if (UNLIKELY(early_break && early_break->Type() == NGEarlyBreak::kBlock &&
early_break->BlockNode() == *node)) {
// We're entering a child that we know that we're going to break inside, and
// even where to break. Look inside, and pass the inner breakpoint to
// layout.
early_break_in_child = early_break->BreakInside();
// If there's no break inside, we should already have broken before this
// child.
DCHECK(early_break_in_child);
}
return node->Layout(space, break_token, early_break_in_child);
}
inline scoped_refptr<const NGLayoutResult> LayoutInflow(
const NGConstraintSpace& space,
const NGBreakToken* break_token,
const NGEarlyBreak* early_break,
NGLayoutInputNode* node,
NGInlineChildLayoutContext* context) {
if (auto* inline_node = DynamicTo<NGInlineNode>(node))
return inline_node->Layout(space, break_token, context);
return LayoutBlockChild(space, break_token, early_break,
To<NGBlockNode>(node));
}
NGAdjoiningObjectTypes ToAdjoiningObjectTypes(EClear clear) {
switch (clear) {
default:
NOTREACHED();
FALLTHROUGH;
case EClear::kNone:
return kAdjoiningNone;
case EClear::kLeft:
return kAdjoiningFloatLeft;
case EClear::kRight:
return kAdjoiningFloatRight;
case EClear::kBoth:
return kAdjoiningFloatBoth;
};
}
// Return true if a child is to be cleared past adjoining floats. These are
// floats that would otherwise (if 'clear' were 'none') be pulled down by the
// BFC block offset of the child. If the child is to clear floats, though, we
// obviously need separate the child from the floats and move it past them,
// since that's what clearance is all about. This means that if we have any such
// floats to clear, we know for sure that we get clearance, even before layout.
inline bool HasClearancePastAdjoiningFloats(
NGAdjoiningObjectTypes adjoining_object_types,
const ComputedStyle& child_style,
const ComputedStyle& cb_style) {
return ToAdjoiningObjectTypes(child_style.Clear(cb_style)) &
adjoining_object_types;
}
// Adjust BFC block offset for clearance, if applicable. Return true of
// clearance was applied.
//
// Clearance applies either when the BFC block offset calculated simply isn't
// past all relevant floats, *or* when we have already determined that we're
// directly preceded by clearance.
//
// The latter is the case when we need to force ourselves past floats that would
// otherwise be adjoining, were it not for the predetermined clearance.
// Clearance inhibits margin collapsing and acts as spacing before the
// block-start margin of the child. It needs to be exactly what takes the
// block-start border edge of the cleared block adjacent to the block-end outer
// edge of the "bottommost" relevant float.
//
// We cannot reliably calculate the actual clearance amount at this point,
// because 1) this block right here may actually be a descendant of the block
// that is to be cleared, and 2) we may not yet have separated the margin before
// and after the clearance. None of this matters, though, because we know where
// to place this block if clearance applies: exactly at the ConstraintSpace's
// ClearanceOffset().
bool ApplyClearance(const NGConstraintSpace& constraint_space,
LayoutUnit* bfc_block_offset) {
if (constraint_space.HasClearanceOffset() &&
*bfc_block_offset < constraint_space.ClearanceOffset()) {
*bfc_block_offset = constraint_space.ClearanceOffset();
return true;
}
return false;
}
LayoutUnit LogicalFromBfcLineOffset(LayoutUnit child_bfc_line_offset,
LayoutUnit parent_bfc_line_offset,
LayoutUnit child_inline_size,
LayoutUnit parent_inline_size,
TextDirection direction) {
// We need to respect the current text direction to calculate the logical
// offset correctly.
LayoutUnit relative_line_offset =
child_bfc_line_offset - parent_bfc_line_offset;
LayoutUnit inline_offset =
direction == TextDirection::kLtr
? relative_line_offset
: parent_inline_size - relative_line_offset - child_inline_size;
return inline_offset;
}
LogicalOffset LogicalFromBfcOffsets(const NGBfcOffset& child_bfc_offset,
const NGBfcOffset& parent_bfc_offset,
LayoutUnit child_inline_size,
LayoutUnit parent_inline_size,
TextDirection direction) {
LayoutUnit inline_offset = LogicalFromBfcLineOffset(
child_bfc_offset.line_offset, parent_bfc_offset.line_offset,
child_inline_size, parent_inline_size, direction);
return {inline_offset,
child_bfc_offset.block_offset - parent_bfc_offset.block_offset};
}
inline bool IsEarlyBreakpoint(const NGEarlyBreak& breakpoint,
const NGBoxFragmentBuilder& builder,
NGLayoutInputNode child) {
if (breakpoint.Type() == NGEarlyBreak::kLine)
return child.IsInline() && breakpoint.LineNumber() == builder.LineCount();
if (breakpoint.IsBreakBefore())
return breakpoint.BlockNode() == child;
return false;
}
} // namespace
NGBlockLayoutAlgorithm::NGBlockLayoutAlgorithm(
const NGLayoutAlgorithmParams& params)
: NGLayoutAlgorithm(params),
border_padding_(params.fragment_geometry.border +
params.fragment_geometry.padding),
border_scrollbar_padding_(border_padding_ +
params.fragment_geometry.scrollbar),
is_resuming_(IsResumingLayout(params.break_token)),
exclusion_space_(params.space.ExclusionSpace()),
early_break_(params.early_break) {
container_builder_.SetIsNewFormattingContext(
params.space.IsNewFormattingContext());
container_builder_.SetInitialFragmentGeometry(params.fragment_geometry);
}
// Define the destructor here, so that we can forward-declare more in the
// header.
NGBlockLayoutAlgorithm::~NGBlockLayoutAlgorithm() = default;
void NGBlockLayoutAlgorithm::SetBoxType(NGPhysicalFragment::NGBoxType type) {
container_builder_.SetBoxType(type);
}
base::Optional<MinMaxSize> NGBlockLayoutAlgorithm::ComputeMinMaxSize(
const MinMaxSizeInput& input) const {
base::Optional<MinMaxSize> sizes = CalculateMinMaxSizesIgnoringChildren(
node_, border_scrollbar_padding_, input.size_type);
if (sizes)
return sizes;
sizes.emplace();
LayoutUnit child_percentage_resolution_block_size =
CalculateChildPercentageBlockSizeForMinMax(
ConstraintSpace(), Node(), border_padding_,
input.percentage_resolution_block_size);
const TextDirection direction = Style().Direction();
LayoutUnit float_left_inline_size = input.float_left_inline_size;
LayoutUnit float_right_inline_size = input.float_right_inline_size;
for (NGLayoutInputNode child = Node().FirstChild(); child;
child = child.NextSibling()) {
if (child.IsOutOfFlowPositioned() ||
(child.IsColumnSpanAll() && ConstraintSpace().IsInColumnBfc()))
continue;
const ComputedStyle& child_style = child.Style();
const EClear child_clear = child_style.Clear(Style());
bool child_is_new_fc = child.CreatesNewFormattingContext();
// Conceptually floats and a single new-FC would just get positioned on a
// single "line". If there is a float/new-FC with clearance, this creates a
// new "line", resetting the appropriate float size trackers.
//
// Both of the float size trackers get reset for anything that isn't a float
// (inflow and new-FC) at the end of the loop, as this creates a new "line".
if (child.IsFloating() || child_is_new_fc) {
LayoutUnit float_inline_size =
float_left_inline_size + float_right_inline_size;
if (child_clear != EClear::kNone)
sizes->max_size = std::max(sizes->max_size, float_inline_size);
if (child_clear == EClear::kBoth || child_clear == EClear::kLeft)
float_left_inline_size = LayoutUnit();
if (child_clear == EClear::kBoth || child_clear == EClear::kRight)
float_right_inline_size = LayoutUnit();
}
MinMaxSizeInput child_input(child_percentage_resolution_block_size);
if (child.IsInline() || child.IsAnonymousBlock()) {
child_input.float_left_inline_size = float_left_inline_size;
child_input.float_right_inline_size = float_right_inline_size;
}
MinMaxSize child_sizes;
if (child.IsInline()) {
// From |NGBlockLayoutAlgorithm| perspective, we can handle |NGInlineNode|
// almost the same as |NGBlockNode|, because an |NGInlineNode| includes
// all inline nodes following |child| and their descendants, and produces
// an anonymous box that contains all line boxes.
// |NextSibling| returns the next block sibling, or nullptr, skipping all
// following inline siblings and descendants.
child_sizes =
child.ComputeMinMaxSize(Style().GetWritingMode(), child_input);
} else {
child_sizes =
ComputeMinAndMaxContentContribution(Style(), child, child_input);
}
DCHECK_LE(child_sizes.min_size, child_sizes.max_size) << child.ToString();
// Determine the max inline contribution of the child.
NGBoxStrut margins = ComputeMinMaxMargins(Style(), child);
LayoutUnit max_inline_contribution;
if (child.IsFloating()) {
// A float adds to its inline size to the current "line". The new max
// inline contribution is just the sum of all the floats on that "line".
LayoutUnit float_inline_size = child_sizes.max_size + margins.InlineSum();
// float_inline_size is negative when the float is completely outside of
// the content area, by e.g., negative margins. Such floats do not affect
// the content size.
if (float_inline_size > 0) {
if (child_style.Floating(Style()) == EFloat::kLeft)
float_left_inline_size += float_inline_size;
else
float_right_inline_size += float_inline_size;
}
max_inline_contribution =
float_left_inline_size + float_right_inline_size;
} else if (child_is_new_fc) {
// As floats are line relative, we perform the margin calculations in the
// line relative coordinate system as well.
LayoutUnit margin_line_left = margins.LineLeft(direction);
LayoutUnit margin_line_right = margins.LineRight(direction);
// line_left_inset and line_right_inset are the "distance" from their
// respective edges of the parent that the new-FC would take. If the
// margin is positive the inset is just whichever of the floats inline
// size and margin is larger, and if negative it just subtracts from the
// float inline size.
LayoutUnit line_left_inset =
margin_line_left > LayoutUnit()
? std::max(float_left_inline_size, margin_line_left)
: float_left_inline_size + margin_line_left;
LayoutUnit line_right_inset =
margin_line_right > LayoutUnit()
? std::max(float_right_inline_size, margin_line_right)
: float_right_inline_size + margin_line_right;
max_inline_contribution =
child_sizes.max_size + line_left_inset + line_right_inset;
} else {
// This is just a standard inflow child.
max_inline_contribution = child_sizes.max_size + margins.InlineSum();
}
sizes->max_size = std::max(sizes->max_size, max_inline_contribution);
// The min inline contribution just assumes that floats are all on their own
// "line".
LayoutUnit min_inline_contribution =
child_sizes.min_size + margins.InlineSum();
sizes->min_size = std::max(sizes->min_size, min_inline_contribution);
// Anything that isn't a float will create a new "line" resetting the float
// size trackers.
if (!child.IsFloating()) {
float_left_inline_size = LayoutUnit();
float_right_inline_size = LayoutUnit();
}
}
DCHECK_GE(sizes->min_size, LayoutUnit());
DCHECK_LE(sizes->min_size, sizes->max_size) << Node().ToString();
if (input.size_type == NGMinMaxSizeType::kBorderBoxSize)
*sizes += border_scrollbar_padding_.InlineSum();
return sizes;
}
LogicalOffset NGBlockLayoutAlgorithm::CalculateLogicalOffset(
const NGFragment& fragment,
LayoutUnit child_bfc_line_offset,
const base::Optional<LayoutUnit>& child_bfc_block_offset) {
LayoutUnit inline_size = container_builder_.Size().inline_size;
TextDirection direction = ConstraintSpace().Direction();
if (child_bfc_block_offset && container_builder_.BfcBlockOffset()) {
return LogicalFromBfcOffsets(
{child_bfc_line_offset, *child_bfc_block_offset}, ContainerBfcOffset(),
fragment.InlineSize(), inline_size, direction);
}
LayoutUnit inline_offset = LogicalFromBfcLineOffset(
child_bfc_line_offset, container_builder_.BfcLineOffset(),
fragment.InlineSize(), inline_size, direction);
// If we've reached here, either the parent, or the child don't have a BFC
// block-offset yet. Children in this situation are always placed at a
// logical block-offset of zero.
return {inline_offset, LayoutUnit()};
}
scoped_refptr<const NGLayoutResult> NGBlockLayoutAlgorithm::Layout() {
scoped_refptr<const NGLayoutResult> result;
// Inline children require an inline child layout context to be
// passed between siblings. We want to stack-allocate that one, but
// only on demand, as it's quite big.
if (Node().ChildrenInline())
result = LayoutWithInlineChildLayoutContext();
else
result = Layout(nullptr);
if (UNLIKELY(result->Status() == NGLayoutResult::kNeedsEarlierBreak)) {
// If we found a good break somewhere inside this block, re-layout and break
// at that location.
DCHECK(!early_break_);
DCHECK(result->GetEarlyBreak());
return RelayoutAndBreakEarlier(*result->GetEarlyBreak());
}
return result;
}
NOINLINE scoped_refptr<const NGLayoutResult>
NGBlockLayoutAlgorithm::LayoutWithInlineChildLayoutContext() {
NGInlineChildLayoutContext context;
if (!RuntimeEnabledFeatures::LayoutNGFragmentItemEnabled())
return Layout(&context);
return LayoutWithItemsBuilder(&context);
}
NOINLINE scoped_refptr<const NGLayoutResult>
NGBlockLayoutAlgorithm::LayoutWithItemsBuilder(
NGInlineChildLayoutContext* context) {
NGFragmentItemsBuilder items_builder(&container_builder_);
container_builder_.SetItemsBuilder(&items_builder);
context->SetItemsBuilder(&items_builder);
scoped_refptr<const NGLayoutResult> result = Layout(context);
// Ensure stack-allocated |NGFragmentItemsBuilder| is not used anymore.
// TODO(kojii): Revisit when the storage of |NGFragmentItemsBuilder| is
// finalized.
container_builder_.SetItemsBuilder(nullptr);
context->SetItemsBuilder(nullptr);
return result;
}
NOINLINE scoped_refptr<const NGLayoutResult>
NGBlockLayoutAlgorithm::RelayoutAndBreakEarlier(
const NGEarlyBreak& breakpoint) {
NGLayoutAlgorithmParams params(Node(),
container_builder_.InitialFragmentGeometry(),
ConstraintSpace(), BreakToken(), &breakpoint);
NGBlockLayoutAlgorithm algorithm_with_break(params);
NGBoxFragmentBuilder& new_builder = algorithm_with_break.container_builder_;
new_builder.SetBoxType(container_builder_.BoxType());
// We're not going to run out of space in the next layout pass, since we're
// breaking earlier, so no space shortage will be detected. Repeat what we
// found in this pass.
new_builder.PropagateSpaceShortage(container_builder_.MinimalSpaceShortage());
return algorithm_with_break.Layout();
}
inline scoped_refptr<const NGLayoutResult> NGBlockLayoutAlgorithm::Layout(
NGInlineChildLayoutContext* inline_child_layout_context) {
const LogicalSize border_box_size = container_builder_.InitialBorderBoxSize();
child_available_size_ =
ShrinkAvailableSize(border_box_size, border_scrollbar_padding_);
child_percentage_size_ = CalculateChildPercentageSize(
ConstraintSpace(), Node(), child_available_size_);
replaced_child_percentage_size_ = CalculateReplacedChildPercentageSize(
ConstraintSpace(), Node(), child_available_size_,
border_scrollbar_padding_, border_padding_);
// All of the above calculations with border_scrollbar_padding_ shouldn't
// include the table cell's intrinsic padding. We can now add this.
if (ConstraintSpace().IsTableCell()) {
border_scrollbar_padding_ += ComputeIntrinsicPadding(
ConstraintSpace(), Style(), container_builder_.Scrollbar());
}
if (ConstraintSpace().HasBlockFragmentation()) {
container_builder_.SetHasBlockFragmentation();
// The whereabouts of our container's so far best breakpoint is none of our
// business, but we store its appeal, so that we don't look for breakpoints
// with lower appeal than that.
container_builder_.SetBreakAppeal(ConstraintSpace().EarlyBreakAppeal());
if (ConstraintSpace().IsInitialColumnBalancingPass())
container_builder_.SetIsInitialColumnBalancingPass();
}
container_builder_.SetBfcLineOffset(
ConstraintSpace().BfcOffset().line_offset);
if (NGAdjoiningObjectTypes adjoining_object_types =
ConstraintSpace().AdjoiningObjectTypes()) {
DCHECK(!ConstraintSpace().IsNewFormattingContext());
DCHECK(!container_builder_.BfcBlockOffset());
// If there were preceding adjoining objects, they will be affected when the
// BFC block-offset gets resolved or updated. We then need to roll back and
// re-layout those objects with the new BFC block-offset, once the BFC
// block-offset is updated.
abort_when_bfc_block_offset_updated_ = true;
container_builder_.SetAdjoiningObjectTypes(adjoining_object_types);
}
// If we are resuming from a break token our start border and padding is
// within a previous fragment.
LayoutUnit content_edge =
is_resuming_ ? LayoutUnit() : border_scrollbar_padding_.block_start;
NGPreviousInflowPosition previous_inflow_position = {
LayoutUnit(), ConstraintSpace().MarginStrut(),
/* self_collapsing_child_had_clearance */ false};
// Do not collapse margins between parent and its child if:
//
// A: There is border/padding between them.
// B: This is a new formatting context
// C: We're resuming layout from a break token. Margin struts cannot pass from
// one fragment to another if they are generated by the same block; they
// must be dealt with at the first fragment.
// D: We're forced to stop margin collapsing by a CSS property
//
// In all those cases we can and must resolve the BFC block offset now.
if (border_scrollbar_padding_.block_start || is_resuming_ ||
ConstraintSpace().IsNewFormattingContext() ||
Style().MarginBeforeCollapse() != EMarginCollapse::kCollapse) {
bool discard_subsequent_margins =
previous_inflow_position.margin_strut.discard_margins &&
!border_scrollbar_padding_.block_start;
if (!ResolveBfcBlockOffset(&previous_inflow_position)) {
// There should be no preceding content that depends on the BFC block
// offset of a new formatting context block, and likewise when resuming
// from a break token.
DCHECK(!ConstraintSpace().IsNewFormattingContext());
DCHECK(!is_resuming_);
return container_builder_.Abort(NGLayoutResult::kBfcBlockOffsetResolved);
}
// Move to the content edge. This is where the first child should be placed.
previous_inflow_position.logical_block_offset = content_edge;
// If we resolved the BFC block offset now, the margin strut has been
// reset. If margins are to be discarded, and this box would otherwise have
// adjoining margins between its own margin and those subsequent content,
// we need to make sure subsequent content discard theirs.
if (discard_subsequent_margins)
previous_inflow_position.margin_strut.discard_margins = true;
}
#if DCHECK_IS_ON()
// If this is a new formatting context, we should definitely be at the origin
// here. If we're resuming from a break token (for a block that doesn't
// establish a new formatting context), that may not be the case,
// though. There may e.g. be clearance involved, or inline-start margins.
if (ConstraintSpace().IsNewFormattingContext())
DCHECK_EQ(*container_builder_.BfcBlockOffset(), LayoutUnit());
// If this is a new formatting context, or if we're resuming from a break
// token, no margin strut must be lingering around at this point.
if (ConstraintSpace().IsNewFormattingContext() || is_resuming_)
DCHECK(ConstraintSpace().MarginStrut().IsEmpty());
if (!container_builder_.BfcBlockOffset()) {
// New formatting-contexts, and when we have a self-collapsing child
// affected by clearance must already have their BFC block-offset resolved.
DCHECK(!previous_inflow_position.self_collapsing_child_had_clearance);
DCHECK(!ConstraintSpace().IsNewFormattingContext());
}
#endif
// If this node is a quirky container, (we are in quirks mode and either a
// table cell or body), we set our margin strut to a mode where it only
// considers non-quirky margins. E.g.
// <body>
// <p></p>
// <div style="margin-top: 10px"></div>
// <h1>Hello</h1>
// </body>
// In the above example <p>'s & <h1>'s margins are ignored as they are
// quirky, and we only consider <div>'s 10px margin.
if (node_.IsQuirkyContainer())
previous_inflow_position.margin_strut.is_quirky_container_start = true;
// Before we descend into children (but after we have determined our inline
// size), give the autosizer an opportunity to adjust the font size on the
// children.
TextAutosizer::NGLayoutScope text_autosizer_layout_scope(
Node(), border_box_size.inline_size);
// Try to reuse line box fragments from cached fragments if possible.
// When possible, this adds fragments to |container_builder_| and update
// |previous_inflow_position| and |BreakToken()|.
scoped_refptr<const NGInlineBreakToken> previous_inline_break_token;
NGBlockChildIterator child_iterator(Node().FirstChild(), BreakToken());
// If this layout is blocked by a display-lock, then we pretend this node has
// no children and that there are no break tokens. Due to this, we skip layout
// on these children.
if (Node().LayoutBlockedByDisplayLock(DisplayLockLifecycleTarget::kChildren))
child_iterator = NGBlockChildIterator(NGBlockNode(nullptr), nullptr);
for (auto entry = child_iterator.NextChild();
NGLayoutInputNode child = entry.node;
entry = child_iterator.NextChild(previous_inline_break_token.get())) {
const NGBreakToken* child_break_token = entry.token;
if (child.IsOutOfFlowPositioned()) {
// We don't support fragmentation inside out-of-flow positioned boxes yet,
// but breaking before is fine. This may happen when a column spanner is
// directly followed by an OOF.
DCHECK(!child_break_token ||
(child_break_token->IsBlockType() &&
To<NGBlockBreakToken>(child_break_token)->IsBreakBefore()));
HandleOutOfFlowPositioned(previous_inflow_position,
To<NGBlockNode>(child));
} else if (child.IsFloating()) {
HandleFloat(previous_inflow_position, To<NGBlockNode>(child),
To<NGBlockBreakToken>(child_break_token));
} else if (child.IsListMarker() && !child.ListMarkerOccupiesWholeLine()) {
container_builder_.SetUnpositionedListMarker(
NGUnpositionedListMarker(To<NGBlockNode>(child)));
} else if (child.IsColumnSpanAll() && ConstraintSpace().IsInColumnBfc()) {
// The child is a column spanner. We now need to finish this
// fragmentainer, then abort and let the column layout algorithm handle
// the spanner as a child.
container_builder_.SetColumnSpanner(To<NGBlockNode>(child));
// We also need to find out where to resume column layout after the
// spanner. If it has a next sibling, that's where we'll resume. If not,
// we'll need to keep looking for subsequent content on the way up the
// tree.
if (NGLayoutInputNode next = child.NextSibling()) {
container_builder_.AddBreakBeforeChild(next, kBreakAppealPerfect,
/* is_forced_break */ true);
}
break;
} else {
// If this is the child we had previously determined to break before, do
// so now and finish layout.
if (UNLIKELY(
early_break_ &&
IsEarlyBreakpoint(*early_break_, container_builder_, child))) {
if (!ResolveBfcBlockOffset(&previous_inflow_position)) {
// However, the predetermined breakpoint may be exactly where the BFC
// block-offset gets resolved. If that hasn't yet happened, we need to
// do that first and re-layout at the right BFC block-offset, and THEN
// break.
return container_builder_.Abort(
NGLayoutResult::kBfcBlockOffsetResolved);
}
BreakBeforeChild(child, kBreakAppealPerfect,
/* is_forced_break */ false,
&previous_inflow_position);
break;
}
NGLayoutResult::EStatus status;
if (child.CreatesNewFormattingContext()) {
status = HandleNewFormattingContext(child, child_break_token,
&previous_inflow_position);
previous_inline_break_token = nullptr;
} else {
status = HandleInflow(
child, child_break_token, &previous_inflow_position,
inline_child_layout_context, &previous_inline_break_token);
if (container_builder_.FoundColumnSpanner())
break;
}
// Spanners should be detected above. They can only occur in the same
// block formatting context as the initial one established by the multicol
// container.
DCHECK(!container_builder_.FoundColumnSpanner());
if (status != NGLayoutResult::kSuccess) {
// We need to abort the layout. No fragment will be generated.
return container_builder_.Abort(status);
}
if (ConstraintSpace().HasBlockFragmentation()) {
if (container_builder_.DidBreak() &&
IsFragmentainerOutOfSpace(
previous_inflow_position.logical_block_offset))
break;
// We need to propagate the initial break-before value up our container
// chain, until we reach a container that's not a first child. If we get
// all the way to the root of the fragmentation context without finding
// any such container, we have no valid class A break point, and if a
// forced break was requested, none will be inserted.
if (!has_processed_first_child_ && !child.IsInline())
container_builder_.SetInitialBreakBefore(child.Style().BreakBefore());
has_processed_first_child_ = true;
}
}
}
if (child_iterator.IsAtEnd()) {
// We've gone through all the children. This doesn't necessarily mean that
// we're done fragmenting, as there may be parallel flows [1] (visible
// overflow) still needing more space than what the current fragmentainer
// can provide. It does mean, though, that, for any future fragmentainers,
// we'll just be looking at the break tokens, if any, and *not* start laying
// out any nodes from scratch, since we have started/finished all the
// children, or at least created break tokens for them.
//
// [1] https://drafts.csswg.org/css-break/#parallel-flows
container_builder_.SetHasSeenAllChildren();
}
// The intrinsic block size is not allowed to be less than the content edge
// offset, as that could give us a negative content box size.
intrinsic_block_size_ = content_edge;
// To save space of the stack when we recurse into children, the rest of this
// function is continued within |FinishLayout|. However it should be read as
// one function.
return FinishLayout(&previous_inflow_position);
}
scoped_refptr<const NGLayoutResult> NGBlockLayoutAlgorithm::FinishLayout(
NGPreviousInflowPosition* previous_inflow_position) {
LogicalSize border_box_size = container_builder_.InitialBorderBoxSize();
NGMarginStrut end_margin_strut = previous_inflow_position->margin_strut;
// If the current layout is a new formatting context, we need to encapsulate
// all of our floats.
if (ConstraintSpace().IsNewFormattingContext()) {
intrinsic_block_size_ = std::max(
intrinsic_block_size_, exclusion_space_.ClearanceOffset(EClear::kBoth));
}
// The end margin strut of an in-flow fragment contributes to the size of the
// current fragment if:
// - There is block-end border/scrollbar/padding.
// - There was a self-collapsing child affected by clearance.
// - We are a new formatting context.
// Additionally this fragment produces no end margin strut.
if (border_scrollbar_padding_.block_end ||
previous_inflow_position->self_collapsing_child_had_clearance ||
ConstraintSpace().IsNewFormattingContext()) {
// If we are a quirky container, we ignore any quirky margins and
// just consider normal margins to extend our size. Other UAs
// perform this calculation differently, e.g. by just ignoring the
// *last* quirky margin.
// TODO: revisit previous implementation to avoid changing behavior and
// https://html.spec.whatwg.org/C/#margin-collapsing-quirks
LayoutUnit margin_strut_sum = node_.IsQuirkyContainer()
? end_margin_strut.QuirkyContainerSum()
: end_margin_strut.Sum();
if (!container_builder_.BfcBlockOffset()) {
// If we have collapsed through the block start and all children (if any),
// now is the time to determine the BFC block offset, because finally we
// have found something solid to hang on to (like clearance or a bottom
// border, for instance). If we're a new formatting context, though, we
// shouldn't be here, because then the offset should already have been
// determined.
DCHECK(!ConstraintSpace().IsNewFormattingContext());
if (!ResolveBfcBlockOffset(previous_inflow_position)) {
return container_builder_.Abort(
NGLayoutResult::kBfcBlockOffsetResolved);
}
DCHECK(container_builder_.BfcBlockOffset());
} else {
// The trailing margin strut will be part of our intrinsic block size, but
// only if there is something that separates the end margin strut from the
// input margin strut (typically child content, block start
// border/padding, or this being a new BFC). If the margin strut from a
// previous sibling or ancestor managed to collapse through all our
// children (if any at all, that is), it means that the resulting end
// margin strut actually pushes us down, and it should obviously not be
// doubly accounted for as our block size.
intrinsic_block_size_ = std::max(
intrinsic_block_size_,
previous_inflow_position->logical_block_offset + margin_strut_sum);
}
intrinsic_block_size_ += border_scrollbar_padding_.block_end;
end_margin_strut = NGMarginStrut();
} else {
// Update our intrinsic block size to be just past the block-end border edge
// of the last in-flow child. The pending margin is to be propagated to our
// container, so ignore it.
intrinsic_block_size_ = std::max(
intrinsic_block_size_, previous_inflow_position->logical_block_offset);
}
intrinsic_block_size_ = std::max(intrinsic_block_size_,
CalculateMinimumBlockSize(end_margin_strut));
// TODO(layout-dev): Is CalculateMinimumBlockSize common to other algorithms,
// and should move into ClampIntrinsicBlockSize?
intrinsic_block_size_ = ClampIntrinsicBlockSize(
Node(), border_scrollbar_padding_, intrinsic_block_size_);
// Recompute the block-axis size now that we know our content size.
border_box_size.block_size = ComputeBlockSizeForFragment(
ConstraintSpace(), Style(), border_padding_, intrinsic_block_size_);
container_builder_.SetBlockSize(border_box_size.block_size);
// If our BFC block-offset is still unknown, we check:
// - If we have a non-zero block-size (margins don't collapse through us).
// - If we have a break token. (Even if we are self-collapsing we position
// ourselves at the very start of the fragmentainer).
if (!container_builder_.BfcBlockOffset() &&
(border_box_size.block_size || BreakToken())) {
if (!ResolveBfcBlockOffset(previous_inflow_position))
return container_builder_.Abort(NGLayoutResult::kBfcBlockOffsetResolved);
DCHECK(container_builder_.BfcBlockOffset());
}
if (container_builder_.BfcBlockOffset()) {
// Do not collapse margins between the last in-flow child and bottom margin
// of its parent if:
// - The block-size differs from the intrinsic size.
// - The parent has computed block-size != auto.
if (border_box_size.block_size != intrinsic_block_size_ ||
!BlockLengthUnresolvable(ConstraintSpace(), Style().LogicalHeight(),
LengthResolvePhase::kLayout)) {
end_margin_strut = NGMarginStrut();
}
}
// List markers should have been positioned if we had line boxes, or boxes
// that have line boxes. If there were no line boxes, position without line
// boxes.
if (container_builder_.UnpositionedListMarker() && node_.IsListItem()) {
if (!PositionListMarkerWithoutLineBoxes(previous_inflow_position))
return container_builder_.Abort(NGLayoutResult::kBfcBlockOffsetResolved);
}
container_builder_.SetEndMarginStrut(end_margin_strut);
container_builder_.SetIntrinsicBlockSize(intrinsic_block_size_);
if (container_builder_.BfcBlockOffset()) {
// If we know our BFC block-offset we should have correctly placed all
// adjoining objects, and shouldn't propagate this information to siblings.
container_builder_.ResetAdjoiningObjectTypes();
} else {
// If we don't know our BFC block-offset yet, we know that for
// margin-collapsing purposes we are self-collapsing.
container_builder_.SetIsSelfCollapsing();
// If we've been forced at a particular BFC block-offset, (either from
// clearance past adjoining floats, or a re-layout), we can safely set our
// BFC block-offset now.
if (ConstraintSpace().ForcedBfcBlockOffset()) {
container_builder_.SetBfcBlockOffset(
*ConstraintSpace().ForcedBfcBlockOffset());
}
}
// We only finalize for fragmentation if the fragment has a BFC block offset.
// This may occur with a zero block size fragment. We need to know the BFC
// block offset to determine where the fragmentation line is relative to us.
if (container_builder_.BfcBlockOffset() &&
ConstraintSpace().HasBlockFragmentation()) {
if (!FinalizeForFragmentation())
return container_builder_.Abort(NGLayoutResult::kNeedsEarlierBreak);
}
NGOutOfFlowLayoutPart(
Node(), ConstraintSpace(),
container_builder_.Borders() + container_builder_.Scrollbar(),
&container_builder_)
.Run();
#if DCHECK_IS_ON()
// If we're not participating in a fragmentation context, no block
// fragmentation related fields should have been set.
if (!ConstraintSpace().HasBlockFragmentation())
container_builder_.CheckNoBlockFragmentation();
#endif
PropagateBaselinesFromChildren();
// An exclusion space is confined to nodes within the same formatting context.
if (!ConstraintSpace().IsNewFormattingContext()) {
container_builder_.SetExclusionSpace(std::move(exclusion_space_));
}
if (ConstraintSpace().UseFirstLineStyle())
container_builder_.SetStyleVariant(NGStyleVariant::kFirstLine);
return container_builder_.ToBoxFragment();
}
const NGInlineBreakToken* NGBlockLayoutAlgorithm::TryReuseFragmentsFromCache(
NGInlineNode inline_node,
NGPreviousInflowPosition* previous_inflow_position,
bool* aborted_out) {
DCHECK(RuntimeEnabledFeatures::LayoutNGLineCacheEnabled());
LayoutBox* layout_box = inline_node.GetLayoutBox();
if (layout_box->SelfNeedsLayout())
return nullptr;
// If floats are intruding into this node, re-layout may be needed.
if (!exclusion_space_.IsEmpty())
return nullptr;
// Laying out from a break token is not supported yet, because this logic
// synthesize a break token.
if (BreakToken())
return nullptr;
const NGPaintFragment* lineboxes =
inline_node.ReusableLineBoxContainer(ConstraintSpace());
if (!lineboxes)
return nullptr;
// Following is a copy of logic from HandleInFlow(). They need to keep in
// sync.
if (inline_node.IsEmptyInline())
return nullptr;
if (!ResolveBfcBlockOffset(previous_inflow_position)) {
*aborted_out = true;
return nullptr;
}
DCHECK(container_builder_.BfcBlockOffset());
WritingMode writing_mode = container_builder_.GetWritingMode();
TextDirection direction = container_builder_.Direction();
DCHECK_EQ(writing_mode, lineboxes->Style().GetWritingMode());
DCHECK_EQ(direction, lineboxes->Style().Direction());
const PhysicalSize outer_size = lineboxes->Size();
LayoutUnit used_block_size = previous_inflow_position->logical_block_offset;
const NGBreakToken* last_break_token = nullptr;
for (const NGPaintFragment* child : lineboxes->Children()) {
if (child->IsDirty())
break;
// Abort if the line propagated its descendants to outside of the line. They
// are propagated through NGLayoutResult, which we don't cache.
const NGPhysicalLineBoxFragment* line =
DynamicTo<NGPhysicalLineBoxFragment>(&child->PhysicalFragment());
if (!line || line->HasPropagatedDescendants())
break;
// TODO(kojii): Running the normal layout code at least once for this child
// helps reducing the code to setup internal states after the reuse. Remove
// the last fragment if it is the end of the fragmentation to do so, but we
// should figure out how to setup the states without doing this.
const NGBreakToken* break_token = line->BreakToken();
DCHECK(break_token);
if (break_token->IsFinished())
break;
last_break_token = break_token;
LogicalOffset logical_offset = child->Offset().ConvertToLogical(
writing_mode, direction, outer_size, line->Size());
container_builder_.AddChild(
*line, {logical_offset.inline_offset, used_block_size});
used_block_size += line->Size().ConvertToLogical(writing_mode).block_size;
}
if (!last_break_token)
return nullptr;
// Update the internal states to after the re-used fragments.
previous_inflow_position->logical_block_offset = used_block_size;
// In order to layout the rest of lines, return the break token from the last
// reused line box.
DCHECK(last_break_token);
DCHECK(!last_break_token->IsFinished());
return To<NGInlineBreakToken>(last_break_token);
}
void NGBlockLayoutAlgorithm::HandleOutOfFlowPositioned(
const NGPreviousInflowPosition& previous_inflow_position,
NGBlockNode child) {
DCHECK(child.IsOutOfFlowPositioned());
LogicalOffset static_offset = {border_scrollbar_padding_.inline_start,
previous_inflow_position.logical_block_offset};
// We only include the margin strut in the OOF static-position if we know we
// aren't going to be a zero-block-size fragment.
if (container_builder_.BfcBlockOffset())
static_offset.block_offset += previous_inflow_position.margin_strut.Sum();
if (child.Style().IsOriginalDisplayInlineType()) {
// The static-position of inline-level OOF-positioned nodes depends on
// previous floats (if any).
//
// Due to this we need to mark this node as having adjoining objects, and
// perform a re-layout if our position shifts.
if (!container_builder_.BfcBlockOffset()) {
container_builder_.AddAdjoiningObjectTypes(kAdjoiningInlineOutOfFlow);
abort_when_bfc_block_offset_updated_ = true;
}
LayoutUnit origin_bfc_block_offset =
container_builder_.BfcBlockOffset().value_or(
ConstraintSpace().ExpectedBfcBlockOffset()) +
static_offset.block_offset;
NGBfcOffset origin_bfc_offset = {
ConstraintSpace().BfcOffset().line_offset +
border_scrollbar_padding_.LineLeft(Style().Direction()),
origin_bfc_block_offset};
static_offset.inline_offset += CalculateOutOfFlowStaticInlineLevelOffset(
Style(), origin_bfc_offset, exclusion_space_,
child_available_size_.inline_size);
}
container_builder_.AddOutOfFlowChildCandidate(child, static_offset);
}
void NGBlockLayoutAlgorithm::HandleFloat(
const NGPreviousInflowPosition& previous_inflow_position,
NGBlockNode child,
const NGBlockBreakToken* child_break_token) {
// If we're resuming layout, we must always know our position in the BFC.
DCHECK(!IsResumingLayout(child_break_token) ||
container_builder_.BfcBlockOffset());
NGUnpositionedFloat unpositioned_float(child, child_break_token);
if (!container_builder_.BfcBlockOffset()) {
container_builder_.AddAdjoiningObjectTypes(
unpositioned_float.IsLineLeft(ConstraintSpace().Direction())
? kAdjoiningFloatLeft
: kAdjoiningFloatRight);
// If we don't have a forced BFC block-offset yet, we'll optimistically
// place floats at the "expected" BFC block-offset. If this differs from
// our final BFC block-offset we'll need to re-layout.
if (!ConstraintSpace().ForcedBfcBlockOffset())
abort_when_bfc_block_offset_updated_ = true;
}
// If we don't have a BFC block-offset yet, the "expected" BFC block-offset
// is used to optimistically place floats.
NGBfcOffset origin_bfc_offset = {
ConstraintSpace().BfcOffset().line_offset +
border_scrollbar_padding_.LineLeft(ConstraintSpace().Direction()),
container_builder_.BfcBlockOffset()
? NextBorderEdge(previous_inflow_position)
: ConstraintSpace().ExpectedBfcBlockOffset()};
NGPositionedFloat positioned_float = PositionFloat(
child_available_size_, child_percentage_size_,
replaced_child_percentage_size_, origin_bfc_offset, &unpositioned_float,
ConstraintSpace(), Style(), &exclusion_space_);
const auto& physical_fragment =
positioned_float.layout_result->PhysicalFragment();
LayoutUnit float_inline_size =
NGFragment(ConstraintSpace().GetWritingMode(), physical_fragment)
.InlineSize();
NGBfcOffset bfc_offset = {ConstraintSpace().BfcOffset().line_offset,
container_builder_.BfcBlockOffset().value_or(
ConstraintSpace().ExpectedBfcBlockOffset())};
LogicalOffset logical_offset = LogicalFromBfcOffsets(
positioned_float.bfc_offset, bfc_offset, float_inline_size,
container_builder_.Size().inline_size, ConstraintSpace().Direction());
container_builder_.AddResult(*positioned_float.layout_result, logical_offset);
}
NGLayoutResult::EStatus NGBlockLayoutAlgorithm::HandleNewFormattingContext(
NGLayoutInputNode child,
const NGBreakToken* child_break_token,
NGPreviousInflowPosition* previous_inflow_position) {
DCHECK(child);
DCHECK(!child.IsFloating());
DCHECK(!child.IsOutOfFlowPositioned());
DCHECK(child.CreatesNewFormattingContext());
DCHECK(child.IsBlock());
const ComputedStyle& child_style = child.Style();
const TextDirection direction = ConstraintSpace().Direction();
NGInflowChildData child_data =
ComputeChildData(*previous_inflow_position, child, child_break_token,
/* is_new_fc */ true);
LayoutUnit child_origin_line_offset =
ConstraintSpace().BfcOffset().line_offset +
border_scrollbar_padding_.LineLeft(direction);
// If the child has a block-start margin, and the BFC block offset is still
// unresolved, and we have preceding adjoining floats, things get complicated
// here. Depending on whether the child fits beside the floats, the margin may
// or may not be adjoining with the current margin strut. This affects the
// position of the preceding adjoining floats. We may have to resolve the BFC
// block offset once with the child's margin tentatively adjoining, then
// realize that the child isn't going to fit beside the floats at the current
// position, and therefore re-resolve the BFC block offset with the child's
// margin non-adjoining. This is akin to clearance.
NGMarginStrut adjoining_margin_strut(previous_inflow_position->margin_strut);
adjoining_margin_strut.Append(child_data.margins.block_start,
child_style.HasMarginBeforeQuirk());
LayoutUnit adjoining_bfc_offset_estimate =
child_data.bfc_offset_estimate.block_offset +
adjoining_margin_strut.Sum();
LayoutUnit non_adjoining_bfc_offset_estimate =
child_data.bfc_offset_estimate.block_offset +
previous_inflow_position->margin_strut.Sum();
LayoutUnit child_bfc_offset_estimate = adjoining_bfc_offset_estimate;
bool bfc_offset_already_resolved = false;
bool child_determined_bfc_offset = false;
bool child_margin_got_separated = false;
bool has_adjoining_floats = false;
if (!container_builder_.BfcBlockOffset()) {
has_adjoining_floats =
container_builder_.AdjoiningObjectTypes() & kAdjoiningFloatBoth;
// If this node, or an arbitrary ancestor had clearance past adjoining
// floats, we consider the margin "separated". We should *never* attempt to
// re-resolve the BFC block-offset in this case.
bool has_clearance_past_adjoining_floats =
ConstraintSpace().AncestorHasClearancePastAdjoiningFloats() ||
HasClearancePastAdjoiningFloats(
container_builder_.AdjoiningObjectTypes(), child_style, Style());
if (has_clearance_past_adjoining_floats) {
child_bfc_offset_estimate = NextBorderEdge(*previous_inflow_position);
child_margin_got_separated = true;
} else if (ConstraintSpace().ForcedBfcBlockOffset()) {
// This is not the first time we're here. We already have a suggested BFC
// block offset.
bfc_offset_already_resolved = true;
child_bfc_offset_estimate = *ConstraintSpace().ForcedBfcBlockOffset();
// We require that the BFC block offset be the one we'd get with margins
// adjoining, margins separated, or if clearance was applied to either of
// these. Anything else is a bug.
DCHECK(child_bfc_offset_estimate == adjoining_bfc_offset_estimate ||
child_bfc_offset_estimate == non_adjoining_bfc_offset_estimate ||
child_bfc_offset_estimate == ConstraintSpace().ClearanceOffset());
// Figure out if the child margin has already got separated from the
// margin strut or not.
child_margin_got_separated =
child_bfc_offset_estimate != adjoining_bfc_offset_estimate;
}
// The BFC block offset of this container gets resolved because of this
// child.
child_determined_bfc_offset = true;
// The block-start margin of the child will only affect the parent's
// position if it is adjoining.
if (!child_margin_got_separated) {
SetSubtreeModifiedMarginStrutIfNeeded(
&child_style.MarginBeforeUsing(Style()));
}
if (!ResolveBfcBlockOffset(previous_inflow_position,
child_bfc_offset_estimate)) {
// If we need to abort here, it means that we had preceding unpositioned
// floats. This is only expected if we're here for the first time.
DCHECK(!bfc_offset_already_resolved);
return NGLayoutResult::kBfcBlockOffsetResolved;
}
// We reset the block offset here as it may have been affected by clearance.
child_bfc_offset_estimate = ContainerBfcOffset().block_offset;
}
// If the child has a non-zero block-start margin, our initial estimate will
// be that any pending floats will be flush (block-start-wise) with this
// child, since they are affected by margin collapsing. Furthermore, this
// child's margin may also pull parent blocks downwards. However, this is only
// the case if the child fits beside the floats at the current block
// offset. If it doesn't (or if it gets clearance), the child needs to be
// pushed down. In this case, the child's margin no longer collapses with the
// previous margin strut, so the pending floats and parent blocks need to
// ignore this margin, which may cause them to end up at completely different
// positions than initially estimated. In other words, we'll need another
// layout pass if this happens.
bool abort_if_cleared = child_data.margins.block_start != LayoutUnit() &&
!child_margin_got_separated &&
child_determined_bfc_offset;
NGBfcOffset child_bfc_offset;
scoped_refptr<const NGLayoutResult> layout_result =
LayoutNewFormattingContext(
child, child_break_token, child_data,
{child_origin_line_offset, child_bfc_offset_estimate},
abort_if_cleared, &child_bfc_offset);
if (!layout_result) {
DCHECK(abort_if_cleared);
// Layout got aborted, because the child got pushed down by floats, and we
// may have had pending floats that we tentatively positioned incorrectly
// (since the child's margin shouldn't have affected them). Try again
// without the child's margin. So, we need another layout pass. Figure out
// if we can do it right away from here, or if we have to roll back and
// reposition floats first.
if (child_determined_bfc_offset) {
// The BFC block offset was calculated when we got to this child, with
// the child's margin adjoining. Since that turned out to be wrong,
// re-resolve the BFC block offset without the child's margin.
LayoutUnit old_offset = *container_builder_.BfcBlockOffset();
container_builder_.ResetBfcBlockOffset();
// Re-resolving the BFC block-offset with a different "forced" BFC
// block-offset is only safe if an ancestor *never* had clearance past
// adjoining floats.
DCHECK(!ConstraintSpace().AncestorHasClearancePastAdjoiningFloats());
ResolveBfcBlockOffset(previous_inflow_position,
non_adjoining_bfc_offset_estimate,
/* forced_bfc_block_offset */ base::nullopt);
if ((bfc_offset_already_resolved || has_adjoining_floats) &&
old_offset != *container_builder_.BfcBlockOffset()) {
// The first BFC block offset resolution turned out to be wrong, and we
// positioned preceding adjacent floats based on that. Now we have to
// roll back and position them at the correct offset. The only expected
// incorrect estimate is with the child's margin adjoining. Any other
// incorrect estimate will result in failed layout.
DCHECK_EQ(old_offset, adjoining_bfc_offset_estimate);
return NGLayoutResult::kBfcBlockOffsetResolved;
}
}
child_bfc_offset_estimate = non_adjoining_bfc_offset_estimate;
child_margin_got_separated = true;
// We can re-layout the child right away. This re-layout *must* produce a
// fragment which fits within the exclusion space.
layout_result = LayoutNewFormattingContext(
child, child_break_token, child_data,
{child_origin_line_offset, child_bfc_offset_estimate},
/* abort_if_cleared */ false, &child_bfc_offset);
}
NGFragment fragment(ConstraintSpace().GetWritingMode(),
layout_result->PhysicalFragment());
LogicalOffset logical_offset = LogicalFromBfcOffsets(
child_bfc_offset, ContainerBfcOffset(), fragment.InlineSize(),
container_builder_.Size().inline_size, ConstraintSpace().Direction());
if (ConstraintSpace().HasBlockFragmentation()) {
bool has_container_separation =
has_processed_first_child_ || child_margin_got_separated ||
child_bfc_offset.block_offset > child_bfc_offset_estimate ||
layout_result->IsPushedByFloats();
BreakStatus break_status = BreakBeforeChildIfNeeded(
child, *layout_result, previous_inflow_position,
logical_offset.block_offset, has_container_separation);
if (break_status == kBrokeBefore)
return NGLayoutResult::kSuccess;
if (break_status == kNeedsEarlierBreak)
return NGLayoutResult::kNeedsEarlierBreak;
EBreakBetween break_after = JoinFragmentainerBreakValues(
layout_result->FinalBreakAfter(), child.Style().BreakAfter());
container_builder_.SetPreviousBreakAfter(break_after);
}
if (!PositionOrPropagateListMarker(*layout_result, &logical_offset,
previous_inflow_position))
return NGLayoutResult::kBfcBlockOffsetResolved;
container_builder_.AddResult(*layout_result, logical_offset);
// The margins we store will be used by e.g. getComputedStyle().
// When calculating these values, ignore any floats that might have
// affected the child. This is what Edge does.
ResolveInlineMargins(child_style, Style(), child_available_size_.inline_size,
fragment.InlineSize(), &child_data.margins);
To<NGBlockNode>(child).StoreMargins(ConstraintSpace(), child_data.margins);
*previous_inflow_position = ComputeInflowPosition(
*previous_inflow_position, child, child_data,
child_bfc_offset.block_offset, logical_offset, *layout_result, fragment,
/* self_collapsing_child_had_clearance */ false);
return NGLayoutResult::kSuccess;
}
scoped_refptr<const NGLayoutResult>
NGBlockLayoutAlgorithm::LayoutNewFormattingContext(
NGLayoutInputNode child,
const NGBreakToken* child_break_token,
const NGInflowChildData& child_data,
NGBfcOffset origin_offset,
bool abort_if_cleared,
NGBfcOffset* out_child_bfc_offset) {
const ComputedStyle& child_style = child.Style();
const TextDirection direction = ConstraintSpace().Direction();
const WritingMode writing_mode = ConstraintSpace().GetWritingMode();
// The origin offset is where we should start looking for layout
// opportunities. It needs to be adjusted by the child's clearance.
AdjustToClearance(
exclusion_space_.ClearanceOffset(child_style.Clear(Style())),
&origin_offset);
DCHECK(container_builder_.BfcBlockOffset());
LayoutOpportunityVector opportunities =
exclusion_space_.AllLayoutOpportunities(
origin_offset, child_available_size_.inline_size);
// We should always have at least one opportunity.
DCHECK_GT(opportunities.size(), 0u);
// Now we lay out. This will give us a child fragment and thus its size, which
// means that we can find out if it's actually going to fit. If it doesn't
// fit where it was laid out, and is pushed downwards, we'll lay out over
// again, since a new BFC block offset could result in a new fragment size,
// e.g. when inline size is auto, or if we're block-fragmented.
for (const auto opportunity : opportunities) {
if (abort_if_cleared &&
origin_offset.block_offset < opportunity.rect.BlockStartOffset()) {
// Abort if we got pushed downwards. We need to adjust
// origin_offset.block_offset, reposition any floats affected by that, and
// try again.
return nullptr;
}
// Find the available inline-size which should be given to the child.
LayoutUnit line_left_offset = opportunity.rect.start_offset.line_offset;
LayoutUnit line_right_offset = opportunity.rect.end_offset.line_offset;
LayoutUnit line_left_margin = child_data.margins.LineLeft(direction);
LayoutUnit line_right_margin = child_data.margins.LineRight(direction);
// When the inline dimensions of layout opportunity match the available
// inline-size, a new formatting context can expand outside of the
// opportunity if negative margins are present.
bool can_expand_outside_opportunity =
opportunity.rect.start_offset.line_offset ==
origin_offset.line_offset &&
opportunity.rect.InlineSize() == child_available_size_.inline_size;
if (can_expand_outside_opportunity) {
// No floats have affected the available inline-size, adjust the
// available inline-size by the margins.
DCHECK_EQ(line_left_offset, origin_offset.line_offset);
DCHECK_EQ(line_right_offset,
origin_offset.line_offset + child_available_size_.inline_size);
line_left_offset += line_left_margin;
line_right_offset -= line_right_margin;
} else {
// Margins are applied from the content-box, not the layout opportunity
// area. Instead of adjusting by the size of the margins, we "shrink" the
// available inline-size if required.
line_left_offset = std::max(
line_left_offset,
origin_offset.line_offset + line_left_margin.ClampNegativeToZero());
line_right_offset = std::min(line_right_offset,
origin_offset.line_offset +
child_available_size_.inline_size -
line_right_margin.ClampNegativeToZero());
}
LayoutUnit opportunity_size =
(line_right_offset - line_left_offset).ClampNegativeToZero();
// The available inline size in the child constraint space needs to include
// inline margins, since layout algorithms (both legacy and NG) will resolve
// auto inline size by subtracting the inline margins from available inline
// size. We have calculated a layout opportunity without margins in mind,
// since they overlap with adjacent floats. Now we need to add them.
LayoutUnit child_available_inline_size =
(opportunity_size + child_data.margins.InlineSum())
.ClampNegativeToZero();
NGConstraintSpace child_space = CreateConstraintSpaceForChild(
child, child_data,
{child_available_inline_size, child_available_size_.block_size},
/* is_new_fc */ true);
// All formatting context roots (like this child) should start with an empty
// exclusion space.
DCHECK(child_space.ExclusionSpace().IsEmpty());
scoped_refptr<const NGLayoutResult> layout_result = LayoutBlockChild(
child_space, child_break_token, early_break_, &To<NGBlockNode>(child));
// Since this child establishes a new formatting context, no exclusion space
// should be returned.
DCHECK(layout_result->ExclusionSpace().IsEmpty());
NGFragment fragment(writing_mode, layout_result->PhysicalFragment());
// Check if the fragment will fit in this layout opportunity, if not proceed
// to the next opportunity.
if ((fragment.InlineSize() > opportunity.rect.InlineSize() &&
!can_expand_outside_opportunity) ||
fragment.BlockSize() > opportunity.rect.BlockSize())
continue;
// Now find the fragment's (final) position calculating the auto margins.
NGBoxStrut auto_margins = child_data.margins;
if (child.IsListMarker()) {
// Deal with marker's margin. It happens only when marker needs to occupy
// the whole line.
DCHECK(child.ListMarkerOccupiesWholeLine());
// Because the marker is laid out as a normal block child, its inline
// size is extended to fill up the space. Compute the regular marker size
// from the first child.
const auto& marker_fragment = layout_result->PhysicalFragment();
LayoutUnit marker_inline_size;
if (!marker_fragment.Children().empty()) {
marker_inline_size =
NGFragment(writing_mode, *marker_fragment.Children().front())
.InlineSize();
}
auto_margins.inline_start =
NGUnpositionedListMarker(To<NGBlockNode>(child))
.InlineOffset(marker_inline_size);
auto_margins.inline_end = opportunity.rect.InlineSize() -
fragment.InlineSize() -
auto_margins.inline_start;
} else {
ResolveInlineMargins(child_style, Style(), child_available_inline_size,
fragment.InlineSize(), &auto_margins);
}
// |auto_margins| are initialized as a copy of the child's initial margins.
// To determine the effect of the auto-margins we only apply the difference.
LayoutUnit auto_margin_line_left =
auto_margins.LineLeft(direction) - line_left_margin;
*out_child_bfc_offset = {line_left_offset + auto_margin_line_left,
opportunity.rect.start_offset.block_offset};
return layout_result;
}
NOTREACHED();
return nullptr;
}
NGLayoutResult::EStatus NGBlockLayoutAlgorithm::HandleInflow(
NGLayoutInputNode child,
const NGBreakToken* child_break_token,
NGPreviousInflowPosition* previous_inflow_position,
NGInlineChildLayoutContext* inline_child_layout_context,
scoped_refptr<const NGInlineBreakToken>* previous_inline_break_token) {
DCHECK(child);
DCHECK(!child.IsFloating());
DCHECK(!child.IsOutOfFlowPositioned());
DCHECK(!child.CreatesNewFormattingContext());
auto* child_inline_node = DynamicTo<NGInlineNode>(child);
if (child_inline_node && !child_break_token &&
RuntimeEnabledFeatures::LayoutNGLineCacheEnabled()) {
DCHECK(!*previous_inline_break_token);
bool aborted = false;
*previous_inline_break_token = TryReuseFragmentsFromCache(
*child_inline_node, previous_inflow_position, &aborted);
if (*previous_inline_break_token)
return NGLayoutResult::kSuccess;
if (aborted)
return NGLayoutResult::kBfcBlockOffsetResolved;
}
bool is_non_empty_inline =
child_inline_node && !child_inline_node->IsEmptyInline();
bool has_clearance_past_adjoining_floats =
!container_builder_.BfcBlockOffset() && child.IsBlock() &&
HasClearancePastAdjoiningFloats(container_builder_.AdjoiningObjectTypes(),
child.Style(), Style());
base::Optional<LayoutUnit> forced_bfc_block_offset;
// If we can separate the previous margin strut from what is to follow, do
// that. Then we're able to resolve *our* BFC block offset and position any
// pending floats. There are two situations where this is necessary:
// 1. If the child is to be cleared by adjoining floats.
// 2. If the child is a non-empty inline.
//
// Note this logic is copied to TryReuseFragmentsFromCache(), they need to
// keep in sync.
if (has_clearance_past_adjoining_floats || is_non_empty_inline) {
if (!ResolveBfcBlockOffset(previous_inflow_position))
return NGLayoutResult::kBfcBlockOffsetResolved;
// If we had clearance past any adjoining floats, we already know where the
// child is going to be (the child's margins won't have any effect).
//
// Set the forced BFC block-offset to the appropriate clearance offset to
// force this placement of this child.
if (has_clearance_past_adjoining_floats) {
forced_bfc_block_offset =
exclusion_space_.ClearanceOffset(child.Style().Clear(Style()));
}
}
// Perform layout on the child.
NGInflowChildData child_data =
ComputeChildData(*previous_inflow_position, child, child_break_token,
/* is_new_fc */ false);
NGConstraintSpace child_space = CreateConstraintSpaceForChild(
child, child_data, child_available_size_, /* is_new_fc */ false,
forced_bfc_block_offset, has_clearance_past_adjoining_floats);
scoped_refptr<const NGLayoutResult> layout_result =
LayoutInflow(child_space, child_break_token, early_break_, &child,
inline_child_layout_context);
// To save space of the stack when we recurse into |NGBlockNode::Layout|
// above, the rest of this function is continued within |FinishInflow|.
// However it should be read as one function.
return FinishInflow(child, child_break_token, child_space,
has_clearance_past_adjoining_floats,
std::move(layout_result), &child_data,
previous_inflow_position, inline_child_layout_context,
previous_inline_break_token);
}
NGLayoutResult::EStatus NGBlockLayoutAlgorithm::FinishInflow(
NGLayoutInputNode child,
const NGBreakToken* child_break_token,
const NGConstraintSpace& child_space,
bool has_clearance_past_adjoining_floats,
scoped_refptr<const NGLayoutResult> layout_result,
NGInflowChildData* child_data,
NGPreviousInflowPosition* previous_inflow_position,
NGInlineChildLayoutContext* inline_child_layout_context,
scoped_refptr<const NGInlineBreakToken>* previous_inline_break_token) {
base::Optional<LayoutUnit> child_bfc_block_offset =
layout_result->BfcBlockOffset();
bool is_self_collapsing = layout_result->IsSelfCollapsing();
// Only non self-collapsing children (e.g. "normal children") can be pushed
// by floats in this way.
bool normal_child_had_clearance = layout_result->IsPushedByFloats();
DCHECK(!normal_child_had_clearance || !is_self_collapsing);
// A child may have aborted its layout if it resolved its BFC block-offset.
// If we don't have a BFC block-offset yet, we need to propagate the abort
// signal up to our parent.
if (layout_result->Status() == NGLayoutResult::kBfcBlockOffsetResolved &&
!container_builder_.BfcBlockOffset()) {
// There's no need to do anything apart from resolving the BFC block-offset
// here, so make sure that it aborts before trying to position floats or
// anything like that, which would just be waste of time.
//
// This is simply propagating an abort up to a node which is able to
// restart the layout (a node that has resolved its BFC block-offset).
DCHECK(child_bfc_block_offset);
abort_when_bfc_block_offset_updated_ = true;
LayoutUnit bfc_block_offset = *child_bfc_block_offset;
if (normal_child_had_clearance) {
// If the child has the same clearance-offset as ourselves it means that
// we should *also* resolve ourselves at that offset, (and we also have
// been pushed by floats).
if (ConstraintSpace().ClearanceOffset() == child_space.ClearanceOffset())
container_builder_.SetIsPushedByFloats();
else
bfc_block_offset = NextBorderEdge(*previous_inflow_position);
}
// A new formatting-context may have previously tried to resolve the BFC
// block-offset. In this case we'll have a "forced" BFC block-offset
// present, but we shouldn't apply it (instead preferring the child's new
// BFC block-offset).
DCHECK(!ConstraintSpace().AncestorHasClearancePastAdjoiningFloats());
if (!ResolveBfcBlockOffset(previous_inflow_position, bfc_block_offset,
/* forced_bfc_block_offset */ base::nullopt))
return NGLayoutResult::kBfcBlockOffsetResolved;
}
// We have special behaviour for a self-collapsing child which gets pushed
// down due to clearance, see comment inside |ComputeInflowPosition|.
bool self_collapsing_child_had_clearance =
is_self_collapsing && has_clearance_past_adjoining_floats;
// We try and position the child within the block formatting-context. This
// may cause our BFC block-offset to be resolved, in which case we should
// abort our layout if needed.
if (!child_bfc_block_offset) {
DCHECK(is_self_collapsing);
if (child_space.HasClearanceOffset() && child.Style().HasClear()) {
// This is a self-collapsing child that we collapsed through, so we have
// to detect clearance manually. See if the child's hypothetical border
// edge is past the relevant floats. If it's not, we need to apply
// clearance before it.
LayoutUnit child_block_offset_estimate =
BfcBlockOffset() + layout_result->EndMarginStrut().Sum();
if (child_block_offset_estimate < child_space.ClearanceOffset())
self_collapsing_child_had_clearance = true;
}
}
bool child_had_clearance =
self_collapsing_child_had_clearance || normal_child_had_clearance;
if (child_had_clearance) {
// The child has clearance. Clearance inhibits margin collapsing and acts as
// spacing before the block-start margin of the child. Our BFC block offset
// is therefore resolvable, and if it hasn't already been resolved, we'll
// do it now to separate the child's collapsed margin from this container.
if (!ResolveBfcBlockOffset(previous_inflow_position))
return NGLayoutResult::kBfcBlockOffsetResolved;
} else if (layout_result->SubtreeModifiedMarginStrut()) {
// The child doesn't have clearance, and modified its incoming
// margin-strut. Propagate this information up to our parent if needed.
SetSubtreeModifiedMarginStrutIfNeeded();
}
bool self_collapsing_child_needs_relayout = false;
if (!child_bfc_block_offset) {
// Layout wasn't able to determine the BFC block-offset of the child. This
// has to mean that the child is self-collapsing.
DCHECK(is_self_collapsing);
if (container_builder_.BfcBlockOffset()) {
// Since we know our own BFC block-offset, though, we can calculate that
// of the child as well.
child_bfc_block_offset = PositionSelfCollapsingChildWithParentBfc(
child, child_space, *child_data, *layout_result);
// We may need to relayout this child if it had any (adjoining) objects
// which were positioned in the incorrect place.
if (layout_result->PhysicalFragment().HasAdjoiningObjectDescendants() &&
*child_bfc_block_offset != child_space.ExpectedBfcBlockOffset())
self_collapsing_child_needs_relayout = true;
}
} else if (!child_had_clearance && !is_self_collapsing) {
// Only non self-collapsing children are allowed resolve their parent's BFC
// block-offset. We check the BFC block-offset at the end of layout
// determine if this fragment is self-collapsing.
//
// The child's BFC block-offset is known, and since there's no clearance,
// this container will get the same offset, unless it has already been
// resolved.
if (!ResolveBfcBlockOffset(previous_inflow_position,
*child_bfc_block_offset))
return NGLayoutResult::kBfcBlockOffsetResolved;
}
// We need to re-layout a self-collapsing child if it was affected by
// clearance in order to produce a new margin strut. For example:
// <div style="margin-bottom: 50px;"></div>
// <div id="float" style="height: 50px;"></div>
// <div id="zero" style="clear: left; margin-top: -20px;">
// <div id="zero-inner" style="margin-top: 40px; margin-bottom: -30px;">
// </div>
//
// The end margin strut for #zero will be {50, -30}. #zero will be affected
// by clearance (as 50 > {50, -30}).
//
// As #zero doesn't touch the incoming margin strut now we need to perform a
// relayout with an empty incoming margin strut.
//
// The resulting margin strut in the above example will be {40, -30}. See
// |ComputeInflowPosition| for how this end margin strut is used.
if (self_collapsing_child_had_clearance) {
NGMarginStrut margin_strut;
margin_strut.Append(child_data->margins.block_start,
child.Style().HasMarginBeforeQuirk());
// We only need to relayout if the new margin strut is different to the
// previous one.
if (child_data->margin_strut != margin_strut) {
child_data->margin_strut = margin_strut;
self_collapsing_child_needs_relayout = true;
}
}
// We need to layout a child if we know its BFC block offset and:
// - It aborted its layout as it resolved its BFC block offset.
// - It has some unpositioned floats.
// - It was affected by clearance.
if ((layout_result->Status() == NGLayoutResult::kBfcBlockOffsetResolved ||
self_collapsing_child_needs_relayout) &&
child_bfc_block_offset) {
NGConstraintSpace new_child_space = CreateConstraintSpaceForChild(
child, *child_data, child_available_size_, /* is_new_fc */ false,
child_bfc_block_offset);
layout_result =
LayoutInflow(new_child_space, child_break_token, early_break_, &child,
inline_child_layout_context);
if (layout_result->Status() == NGLayoutResult::kBfcBlockOffsetResolved) {
// Even a second layout pass may abort, if the BFC block offset initially
// calculated turned out to be wrong. This happens when we discover that
// an in-flow block-level descendant that establishes a new formatting
// context doesn't fit beside the floats at its initial position. Allow
// one more pass.
child_bfc_block_offset = layout_result->BfcBlockOffset();
DCHECK(child_bfc_block_offset);
new_child_space = CreateConstraintSpaceForChild(
child, *child_data, child_available_size_, /* is_new_fc */ false,
child_bfc_block_offset);
layout_result =
LayoutInflow(new_child_space, child_break_token, early_break_, &child,
inline_child_layout_context);
}
DCHECK_EQ(layout_result->Status(), NGLayoutResult::kSuccess);
}
// It is now safe to update our version of the exclusion space, and any
// propagated adjoining floats.
exclusion_space_ = layout_result->ExclusionSpace();
// Only self-collapsing children should have adjoining objects.
DCHECK(!layout_result->AdjoiningObjectTypes() || is_self_collapsing);
container_builder_.SetAdjoiningObjectTypes(
layout_result->AdjoiningObjectTypes());
// If we don't know our BFC block-offset yet, and the child stumbled into
// something that needs it (unable to position floats yet), we need abort
// layout, and trigger a re-layout once we manage to resolve it.
//
// NOTE: This check is performed after the optional second layout pass above,
// since we may have been able to resolve our BFC block-offset (e.g. due to
// clearance) and position any descendant floats in the second pass.
// In particular, when it comes to clearance of self-collapsing children, if
// we just applied it and resolved the BFC block-offset to separate the
// margins before and after clearance, we cannot abort and re-layout this
// child, or clearance would be lost.
//
// If we are a new formatting context, the child will get re-laid out once it
// has been positioned.
if (!container_builder_.BfcBlockOffset()) {
abort_when_bfc_block_offset_updated_ |=
layout_result->AdjoiningObjectTypes();
// If our BFC block offset is unknown, and the child got pushed down by
// floats, so will we.
if (layout_result->IsPushedByFloats())
container_builder_.SetIsPushedByFloats();
}
const auto& physical_fragment = layout_result->PhysicalFragment();
NGFragment fragment(ConstraintSpace().GetWritingMode(), physical_fragment);
LogicalOffset logical_offset = CalculateLogicalOffset(
fragment, layout_result->BfcLineOffset(), child_bfc_block_offset);
if (ConstraintSpace().HasBlockFragmentation()) {
// Floats only cause container separation for the outermost block child that
// gets pushed down (the container and the child may have adjoining
// block-start margins).
bool has_container_separation =
has_processed_first_child_ || (layout_result->IsPushedByFloats() &&
!container_builder_.IsPushedByFloats());
BreakStatus break_status = BreakBeforeChildIfNeeded(
child, *layout_result, previous_inflow_position,
logical_offset.block_offset, has_container_separation);
if (break_status == kBrokeBefore)
return NGLayoutResult::kSuccess;
if (break_status == kNeedsEarlierBreak)
return NGLayoutResult::kNeedsEarlierBreak;
EBreakBetween break_after = JoinFragmentainerBreakValues(
layout_result->FinalBreakAfter(), child.Style().BreakAfter());
container_builder_.SetPreviousBreakAfter(break_after);
}
if (!PositionOrPropagateListMarker(*layout_result, &logical_offset,
previous_inflow_position))
return NGLayoutResult::kBfcBlockOffsetResolved;
container_builder_.AddResult(*layout_result, logical_offset);
if (auto* block_child = DynamicTo<NGBlockNode>(child)) {
// We haven't yet resolved margins wrt. overconstrainedness, unless that was
// also required to calculate line-left offset (due to block alignment)
// before layout. Do so now, so that we store the correct values (which is
// required by e.g. getComputedStyle()).
if (!child_data->margins_fully_resolved) {
ResolveInlineMargins(child.Style(), Style(),
child_available_size_.inline_size,
fragment.InlineSize(), &child_data->margins);
child_data->margins_fully_resolved = true;
}
block_child->StoreMargins(ConstraintSpace(), child_data->margins);
}
*previous_inflow_position = ComputeInflowPosition(
*previous_inflow_position, child, *child_data, child_bfc_block_offset,
logical_offset, *layout_result, fragment,
self_collapsing_child_had_clearance);
*previous_inline_break_token =
child.IsInline() ? To<NGInlineBreakToken>(physical_fragment.BreakToken())
: nullptr;
// If a spanner was found inside the child, we need to finish up and propagate
// the spanner to the column layout algorithm, so that it can take care of it.
if (UNLIKELY(ConstraintSpace().IsInColumnBfc())) {
if (NGBlockNode spanner_node = layout_result->ColumnSpanner()) {
container_builder_.SetColumnSpanner(spanner_node);
if (!container_builder_.DidBreak()) {
// If we still haven't found a descendant at which to resume column
// layout after the spanner, look for one now.
if (NGLayoutInputNode next = child.NextSibling()) {
container_builder_.AddBreakBeforeChild(next, kBreakAppealPerfect,
/* is_forced_break */ true);
}
}
}
}
return NGLayoutResult::kSuccess;
}
NGInflowChildData NGBlockLayoutAlgorithm::ComputeChildData(
const NGPreviousInflowPosition& previous_inflow_position,
NGLayoutInputNode child,
const NGBreakToken* child_break_token,
bool is_new_fc) {
DCHECK(child);
DCHECK(!child.IsFloating());
DCHECK_EQ(is_new_fc, child.CreatesNewFormattingContext());
// Calculate margins in parent's writing mode.
bool margins_fully_resolved;
NGBoxStrut margins =
CalculateMargins(child, is_new_fc, &margins_fully_resolved);
// Append the current margin strut with child's block start margin.
// Non empty border/padding, and new formatting-context use cases are handled
// inside of the child's layout
NGMarginStrut margin_strut = previous_inflow_position.margin_strut;
const auto* child_block_break_token =
DynamicTo<NGBlockBreakToken>(child_break_token);
bool is_resuming_after_break = IsResumingLayout(child_block_break_token);
if (child_block_break_token && child_block_break_token->IsForcedBreak()) {
// Margins after a fragmentainer break are usually discarded, but not if
// there's a forced break.
margin_strut = NGMarginStrut();
} else if (is_resuming_after_break) {
// We're past the block-start edge of this child. Don't repeat the margin.
margins.block_start = LayoutUnit();
}
LayoutUnit logical_block_offset =
previous_inflow_position.logical_block_offset;
EMarginCollapse margin_before_collapse = child.Style().MarginBeforeCollapse();
if (margin_before_collapse != EMarginCollapse::kCollapse) {
// Stop margin collapsing on the block-start side of the child.
StopMarginCollapsing(child.Style().MarginBeforeCollapse(),
margins.block_start, &logical_block_offset,
&margin_strut);
if (margin_before_collapse == EMarginCollapse::kSeparate) {
UseCounter::Count(Node().GetDocument(),
WebFeature::kWebkitMarginBeforeCollapseSeparate);
if (margin_strut != previous_inflow_position.margin_strut ||
logical_block_offset !=
previous_inflow_position.logical_block_offset) {
UseCounter::Count(
Node().GetDocument(),
WebFeature::kWebkitMarginBeforeCollapseSeparateMaybeDoesSomething);
}
} else if (margin_before_collapse == EMarginCollapse::kDiscard) {
UseCounter::Count(Node().GetDocument(),
WebFeature::kWebkitMarginBeforeCollapseDiscard);
}
} else {
margin_strut.Append(margins.block_start,
child.Style().HasMarginBeforeQuirk());
SetSubtreeModifiedMarginStrutIfNeeded(&child.Style().MarginBefore());
}
NGBfcOffset child_bfc_offset = {
ConstraintSpace().BfcOffset().line_offset +
border_scrollbar_padding_.LineLeft(ConstraintSpace().Direction()) +
margins.LineLeft(ConstraintSpace().Direction()),
BfcBlockOffset() + logical_block_offset};
return {child_bfc_offset, margin_strut, margins, margins_fully_resolved,
is_resuming_after_break};
}
NGPreviousInflowPosition NGBlockLayoutAlgorithm::ComputeInflowPosition(
const NGPreviousInflowPosition& previous_inflow_position,
const NGLayoutInputNode child,
const NGInflowChildData& child_data,
const base::Optional<LayoutUnit>& child_bfc_block_offset,
const LogicalOffset& logical_offset,
const NGLayoutResult& layout_result,
const NGFragment& fragment,
bool self_collapsing_child_had_clearance) {
// Determine the child's end logical offset, for the next child to use.
LayoutUnit logical_block_offset;
bool is_self_collapsing = layout_result.IsSelfCollapsing();
if (is_self_collapsing) {
// The default behaviour for self-collapsing children is they just pass
// through the previous inflow position.
logical_block_offset = previous_inflow_position.logical_block_offset;
if (self_collapsing_child_had_clearance) {
// If there's clearance, we must have applied that by now and thus
// resolved our BFC block-offset.
DCHECK(container_builder_.BfcBlockOffset());
DCHECK(child_bfc_block_offset.has_value());
// If a self-collapsing child was affected by clearance (that is it got
// pushed down past a float), we need to do something slightly bizarre.
//
// Instead of just passing through the previous inflow position, we make
// the inflow position our new position (which was affected by the
// float), minus what the margin strut which the self-collapsing child
// produced.
//
// Another way of thinking about this is that when you *add* back the
// margin strut, you end up with the same position as you started with.
//
// This is essentially what the spec refers to as clearance [1], and,
// while we normally don't have to calculate it directly, in the case of
// a self-collapsing cleared child like here, we actually have to.
//
// We have to calculate clearance for self-collapsing cleared children,
// because we need the margin that's between the clearance and this block
// to collapse correctly with subsequent content. This is something that
// needs to take place after the margin strut preceding and following the
// clearance have been separated. Clearance may be positive, negative or
// zero, depending on what it takes to (hypothetically) place this child
// just below the last relevant float. Since the margins before and after
// the clearance have been separated, we may have to pull the child back,
// and that's an example of negative clearance.
//
// (In the other case, when a cleared child is non self-collapsing (i.e.
// when we don't end up here), we don't need to explicitly calculate
// clearance, because then we just place its border edge where it should
// be and we're done with it.)
//
// [1] https://www.w3.org/TR/CSS22/visuren.html#flow-control
// First move past the margin that is to precede the clearance. It will
// not participate in any subsequent margin collapsing.
LayoutUnit margin_before_clearance =
previous_inflow_position.margin_strut.Sum();
logical_block_offset += margin_before_clearance;
// Calculate and apply actual clearance.
LayoutUnit clearance = *child_bfc_block_offset -
layout_result.EndMarginStrut().Sum() -
NextBorderEdge(previous_inflow_position);
logical_block_offset += clearance;
}
if (!container_builder_.BfcBlockOffset())
DCHECK_EQ(logical_block_offset, LayoutUnit());
} else {
logical_block_offset = logical_offset.block_offset + fragment.BlockSize();
}
NGMarginStrut margin_strut = layout_result.EndMarginStrut();
EMarginCollapse margin_after_collapse = child.Style().MarginAfterCollapse();
if (margin_after_collapse != EMarginCollapse::kCollapse) {
LayoutUnit logical_block_offset_copy = logical_block_offset;
// Stop margin collapsing on the block-end side of the child.
StopMarginCollapsing(margin_after_collapse, child_data.margins.block_end,
&logical_block_offset, &margin_strut);
if (margin_after_collapse == EMarginCollapse::kSeparate) {
UseCounter::Count(Node().GetDocument(),
WebFeature::kWebkitMarginAfterCollapseSeparate);
if (margin_strut != layout_result.EndMarginStrut() ||
logical_block_offset != logical_block_offset_copy) {
UseCounter::Count(
Node().GetDocument(),
WebFeature::kWebkitMarginAfterCollapseSeparateMaybeDoesSomething);
}
} else if (margin_after_collapse == EMarginCollapse::kDiscard) {
UseCounter::Count(Node().GetDocument(),
WebFeature::kWebkitMarginAfterCollapseDiscard);
}
} else {
// Self collapsing child's end margin can "inherit" quirkiness from its
// start margin. E.g.
// <ol style="margin-bottom: 20px"></ol>
bool is_quirky =
(is_self_collapsing && child.Style().HasMarginBeforeQuirk()) ||
child.Style().HasMarginAfterQuirk();
margin_strut.Append(child_data.margins.block_end, is_quirky);
SetSubtreeModifiedMarginStrutIfNeeded(&child.Style().MarginAfter());
}
// This flag is subtle, but in order to determine our size correctly we need
// to check if our last child is self-collapsing, and it was affected by
// clearance *or* an adjoining self-collapsing sibling was affected by
// clearance. E.g.
// <div id="container">
// <div id="float"></div>
// <div id="zero-with-clearance"></div>
// <div id="another-zero"></div>
// </div>
// In the above case #container's size will depend on the end margin strut of
// #another-zero, even though usually it wouldn't.
bool self_or_sibling_self_collapsing_child_had_clearance =
self_collapsing_child_had_clearance ||
(previous_inflow_position.self_collapsing_child_had_clearance &&
is_self_collapsing);
return {logical_block_offset, margin_strut,
self_or_sibling_self_collapsing_child_had_clearance};
}
LayoutUnit NGBlockLayoutAlgorithm::PositionSelfCollapsingChildWithParentBfc(
const NGLayoutInputNode& child,
const NGConstraintSpace& child_space,
const NGInflowChildData& child_data,
const NGLayoutResult& layout_result) const {
DCHECK(layout_result.IsSelfCollapsing());
// The child must be an in-flow zero-block-size fragment, use its end margin
// strut for positioning.
LayoutUnit child_bfc_block_offset =
child_data.bfc_offset_estimate.block_offset +
layout_result.EndMarginStrut().Sum();
ApplyClearance(child_space, &child_bfc_block_offset);
return child_bfc_block_offset;
}
LayoutUnit NGBlockLayoutAlgorithm::FragmentainerSpaceAvailable() const {
DCHECK(container_builder_.BfcBlockOffset());
return FragmentainerSpaceAtBfcStart(ConstraintSpace()) -
*container_builder_.BfcBlockOffset();
}
bool NGBlockLayoutAlgorithm::IsFragmentainerOutOfSpace(
LayoutUnit block_offset) const {
if (did_break_before_child_)
return true;
if (!ConstraintSpace().HasKnownFragmentainerBlockSize())
return false;
if (!container_builder_.BfcBlockOffset().has_value())
return false;
return block_offset >= FragmentainerSpaceAvailable();
}
bool NGBlockLayoutAlgorithm::FinalizeForFragmentation() {
if (Node().ChildrenInline() && !early_break_) {
if (container_builder_.DidBreak() || first_overflowing_line_) {
if (first_overflowing_line_ &&
first_overflowing_line_ < container_builder_.LineCount()) {
int line_number;
if (fit_all_lines_) {
line_number = first_overflowing_line_;
} else {
// We managed to finish layout of all the lines for the node, which
// means that we won't have enough widows, unless we break earlier
// than where we overflowed.
int line_count = container_builder_.LineCount();
line_number = std::max(line_count - Style().Widows(),
std::min(line_count, int(Style().Orphans())));
}
// We need to layout again, and stop at the right line number.
scoped_refptr<const NGEarlyBreak> breakpoint =
base::AdoptRef(new NGEarlyBreak(line_number));
container_builder_.SetEarlyBreak(breakpoint, kBreakAppealPerfect);
return false;
}
} else {
// Everything could fit in the current fragmentainer, but, depending on
// what comes after, the best location to break at may be between two of
// our lines.
UpdateEarlyBreakBetweenLines();
}
}
if (!ConstraintSpace().HasKnownFragmentainerBlockSize())
return true;
LayoutUnit consumed_block_size =
BreakToken() ? BreakToken()->ConsumedBlockSize() : LayoutUnit();
LayoutUnit block_size =
ComputeBlockSizeForFragment(ConstraintSpace(), Style(), border_padding_,
consumed_block_size + intrinsic_block_size_);
block_size -= consumed_block_size;
DCHECK_GE(block_size, LayoutUnit())
<< "Adding and subtracting the consumed_block_size shouldn't leave the "
"block_size for this fragment smaller than zero.";
LayoutUnit space_left = FragmentainerSpaceAvailable();
if (space_left <= LayoutUnit()) {
// The amount of space available may be zero, or even negative, if the
// border-start edge of this block starts exactly at, or even after the
// fragmentainer boundary. We're going to need a break before this block,
// because no part of it fits in the current fragmentainer. Due to margin
// collapsing with children, this situation is something that we cannot
// always detect prior to layout. The fragment produced by this algorithm is
// going to be thrown away. The parent layout algorithm will eventually
// detect that there's no room for a fragment for this node, and drop the
// fragment on the floor. Therefore it doesn't matter how we set up the
// container builder, so just return.
return true;
}
FinishFragmentation(ConstraintSpace(), block_size, intrinsic_block_size_,
consumed_block_size, space_left, &container_builder_);
return true;
}
NGBlockLayoutAlgorithm::BreakStatus
NGBlockLayoutAlgorithm::BreakBeforeChildIfNeeded(
NGLayoutInputNode child,
const NGLayoutResult& layout_result,
NGPreviousInflowPosition* previous_inflow_position,
LayoutUnit block_offset,
bool has_container_separation) {
DCHECK(ConstraintSpace().HasBlockFragmentation());
// If the BFC offset is unknown, there's nowhere to break, since there's no
// non-empty child content yet (as that would have resolved the BFC offset).
if (!container_builder_.BfcBlockOffset())
return kContinueWithoutBreaking;
// If we already know where to insert the break, we already know that it's not
// going to be here, since that's something we check before entering layout of
// a child.
if (early_break_)
return kContinueWithoutBreaking;
NGBreakAppeal appeal_before = kBreakAppealPerfect;
if (has_container_separation) {
EBreakBetween break_between =
CalculateBreakBetweenValue(child, layout_result, container_builder_);
if (IsForcedBreakValue(ConstraintSpace(), break_between)) {
BreakBeforeChild(child, layout_result, block_offset, kBreakAppealPerfect,
/* is_forced_break */ true, previous_inflow_position);
return kBrokeBefore;
}
// If there's a break-{after,before}:avoid* involved at this breakpoint,
// its appeal will decrease.
if (IsAvoidBreakValue(ConstraintSpace(), break_between))
appeal_before = kBreakAppealViolatingBreakAvoid;
} else {
// This is not a valid break point. If there's no container separation, it
// means that we're breaking before the first piece of in-flow content
// inside this block, even if it's not a valid class C break point [1] We
// really don't want to break here, if we can find something better.
//
// [1] https://www.w3.org/TR/css-break-3/#possible-breaks
appeal_before = kBreakAppealLastResort;
}
const auto& physical_fragment = layout_result.PhysicalFragment();
NGFragment fragment(ConstraintSpace().GetWritingMode(), physical_fragment);
if (!ConstraintSpace().HasKnownFragmentainerBlockSize()) {
if (ConstraintSpace().IsInitialColumnBalancingPass()) {
if (child.IsMonolithic() ||
(child.IsBlock() &&
IsAvoidBreakValue(ConstraintSpace(), child.Style().BreakInside()))) {
// If this is the initial column balancing pass, attempt to make the
// column block-size at least as large as the tallest piece of
// monolithic content and/or block with break-inside:avoid.
container_builder_.PropagateTallestUnbreakableBlockSize(
fragment.BlockSize());
}
}
// We only care about soft breaks if we have a fragmentainer block-size.
// During column balancing this may be unknown.
return kContinueWithoutBreaking;
}
if (IsA<NGBlockBreakToken>(physical_fragment.BreakToken())) {
// The block child broke inside. We now need to decide whether to keep that
// break, or if it would be better to break before it.
NGBreakAppeal appeal_inside = CalculateBreakAppealInside(
ConstraintSpace(), To<NGBlockNode>(child), layout_result);
// Allow breaking inside if it has the same appeal or higher than breaking
// before or breaking earlier.
if (appeal_inside >= appeal_before &&
(!container_builder_.HasEarlyBreak() ||
appeal_inside >= container_builder_.BreakAppeal())) {
container_builder_.SetBreakAppeal(appeal_inside);
return kContinueWithoutBreaking;
}
} else {
LayoutUnit space_left = FragmentainerSpaceAvailable() - block_offset;
bool want_break;
if (child.IsMonolithic()) {
// If the monolithic piece of content (e.g. a line, or block-level
// replaced content) doesn't fit, we need a break.
want_break = fragment.BlockSize() > space_left;
} else {
// If the block-offset is past the fragmentainer boundary (or exactly at
// the boundary), no part of the fragment is going to fit in the current
// fragmentainer. Fragments may be pushed past the fragmentainer boundary
// by margins.
want_break = space_left <= LayoutUnit();
}
if (want_break) {
// If we haven't used any space at all in the fragmentainer yet, though,
// we cannot break even if we really want to, or there'd be no progress.
// We'd end up creating an infinite number of fragmentainers without
// putting any content into them.
if (space_left >= ConstraintSpace().FragmentainerBlockSize())
want_break = false;
}
if (!want_break) {
if (child.IsBlock()) {
// If this doesn't happen, though, we're tentatively not going to break
// before or inside this child, but we'll check the appeal of breaking
// there anyway. It may be the best breakpoint we'll ever find. (Note
// that we only do this for block children, since, when it comes to
// inline layout, we first need to lay out all the line boxes, so that
// we know what do to in order to honor orphans and widows, if at all
// possible.)
UpdateEarlyBreakAtBlockChild(To<NGBlockNode>(child), layout_result,
appeal_before);
}
return kContinueWithoutBreaking;
}
}
// Figure out where to insert a soft break. It will either be before this
// child, or before an earlier sibling, if there's a more appealing breakpoint
// there.
if (child.IsInline()) {
if (!first_overflowing_line_) {
// We're at the first overflowing line. This is the space shortage that
// we are going to report. We do this in spite of not yet knowing
// whether breaking here would violate orphans and widows requests. This
// approach may result in a lower space shortage than what's actually
// true, which leads to more layout passes than we'd otherwise
// need. However, getting this optimal for orphans and widows would
// require an additional piece of machinery. This case should be rare
// enough (to worry about performance), so let's focus on code
// simplicity instead.
PropagateSpaceShortage(layout_result, block_offset);
}
// Attempt to honor orphans and widows requests.
if (int line_count = container_builder_.LineCount()) {
if (!first_overflowing_line_)
first_overflowing_line_ = line_count;
bool is_first_fragment = !BreakToken();
// Figure out how many lines we need before the break. That entails to
// attempt to honor the orphans request.
int minimum_line_count = Style().Orphans();
if (!is_first_fragment) {
// If this isn't the first fragment, it means that there's a break both
// before and after this fragment. So what was seen as trailing widows
// in the previous fragment is essentially orphans for us now.
minimum_line_count =
std::max(minimum_line_count, static_cast<int>(Style().Widows()));
}
if (line_count < minimum_line_count) {
// Not enough orphans. Our only hope is if we can break before the start
// of this block to improve on the situation. That's not something we
// can determine at this point though. Permit the break, but mark it as
// undesirable.
if (appeal_before > kBreakAppealViolatingOrphansAndWidows)
appeal_before = kBreakAppealViolatingOrphansAndWidows;
} else {
// There are enough lines before the break. Try to make sure that
// there'll be enough lines after the break as well. Attempt to honor
// the widows request.
DCHECK_GE(line_count, first_overflowing_line_);
int widows_found = line_count - first_overflowing_line_ + 1;
if (widows_found < Style().Widows()) {
// Although we're out of space, we have to continue layout to figure
// out exactly where to break in order to honor the widows
// request. We'll make sure that we're going to leave at least as many
// lines as specified by the 'widows' property for the next fragment
// (if at all possible), which means that lines that could fit in the
// current fragment (that we have already laid out) may have to be
// saved for the next fragment.
return kContinueWithoutBreaking;
}
// We have determined that there are plenty of lines for the next
// fragment, so we can just break exactly where we ran out of space,
// rather than pushing some of the line boxes over to the next fragment.
}
fit_all_lines_ = true;
}
}
// So, we've run out of space, and the child won't fit here. But if there's a
// breakpoint with higher appeal among earlier siblings, abort and re-layout
// to that breakpoint now.
if (container_builder_.HasEarlyBreak() &&
container_builder_.BreakAppeal() > appeal_before) {
// Found a better place to break. Before aborting, calculate and report
// space shortage from where we'd actually break.
PropagateSpaceShortage(layout_result, block_offset);
return kNeedsEarlierBreak;
}
// Break before the child. Note that there may be a better break further up
// with higher appeal (but it's too early to tell), in which case this
// breakpoint will be replaced.
BreakBeforeChild(child, layout_result, block_offset, appeal_before,
/* is_forced_break */ false, previous_inflow_position);
return kBrokeBefore;
}
void NGBlockLayoutAlgorithm::BreakBeforeChild(
NGLayoutInputNode child,
const NGLayoutResult& layout_result,
LayoutUnit block_offset,
NGBreakAppeal appeal,
bool is_forced_break,
NGPreviousInflowPosition* previous_inflow_position) {
#if DCHECK_IS_ON()
// In order to successfully break before a node, this has to be its first
// fragment.
const auto& physical_fragment = layout_result.PhysicalFragment();
DCHECK(!physical_fragment.IsBox() ||
To<NGPhysicalBoxFragment>(physical_fragment).IsFirstForNode());
#endif
// Report space shortage. Note that we're not doing this for line boxes here
// (only blocks), because line boxes need handle it in their own way (due to
// how we implement widows).
if (child.IsBlock())
PropagateSpaceShortage(layout_result, block_offset);
BreakBeforeChild(child, appeal, is_forced_break, previous_inflow_position);
}
void NGBlockLayoutAlgorithm::BreakBeforeChild(
NGLayoutInputNode child,
NGBreakAppeal appeal,
bool is_forced_break,
NGPreviousInflowPosition* previous_inflow_position) {
did_break_before_child_ = true;
if (ConstraintSpace().HasKnownFragmentainerBlockSize()) {
// The remaining part of the fragmentainer (the unusable space for child
// content, due to the break) should still be occupied by this container.
previous_inflow_position->logical_block_offset =
FragmentainerSpaceAvailable();
}
DCHECK(is_forced_break || ConstraintSpace().HasKnownFragmentainerBlockSize());
// This will drop the fragment (if any) on the floor and retry at the start of
// the next fragmentainer.
container_builder_.AddBreakBeforeChild(child, appeal, is_forced_break);
}
void NGBlockLayoutAlgorithm::PropagateSpaceShortage(
const NGLayoutResult& layout_result,
LayoutUnit block_offset) {
// There's no shortage to report in the initial column balancing pass, since
// we haven't even calculated a tentative column block-size yet.
if (ConstraintSpace().IsInitialColumnBalancingPass())
return;
// Only multicol cares about space shortage.
if (ConstraintSpace().BlockFragmentationType() != kFragmentColumn)
return;
LayoutUnit space_shortage;
if (layout_result.MinimalSpaceShortage() == LayoutUnit::Max()) {
// Calculate space shortage: Figure out how much more space would have been
// sufficient to make the child fit right here in the current fragment.
NGFragment fragment(ConstraintSpace().GetWritingMode(),
layout_result.PhysicalFragment());
LayoutUnit space_left = FragmentainerSpaceAvailable() - block_offset;
space_shortage = fragment.BlockSize() - space_left;
} else {
// However, if space shortage was reported inside the child, use that. If we
// broke inside the child, we didn't complete layout, so calculating space
// shortage for the child as a whole would be impossible and pointless.
space_shortage = layout_result.MinimalSpaceShortage();
}
// TODO(mstensho): Turn this into a DCHECK, when the engine is ready for
// it. Space shortage should really be positive here, or we might ultimately
// fail to stretch the columns (column balancing).
if (space_shortage > LayoutUnit())
container_builder_.PropagateSpaceShortage(space_shortage);
}
void NGBlockLayoutAlgorithm::UpdateEarlyBreakAtBlockChild(
NGBlockNode child,
const NGLayoutResult& layout_result,
NGBreakAppeal appeal_before) {
// We shouldn't be here if we already know where to break.
DCHECK(!early_break_);
// If the child already broke, it's a little too late to look for breakpoints.
DCHECK(!layout_result.PhysicalFragment().BreakToken());
// See if there's a good breakpoint inside the child.
NGBreakAppeal appeal_inside = kBreakAppealLastResort;
if (scoped_refptr<const NGEarlyBreak> breakpoint =
layout_result.GetEarlyBreak()) {
appeal_inside =
CalculateBreakAppealInside(ConstraintSpace(), child, layout_result);
if (container_builder_.BreakAppeal() <= appeal_inside) {
// Found a good breakpoint inside the child. Add the child to the early
// break container chain, and store it.
auto parent_break = base::AdoptRef(new NGEarlyBreak(child, breakpoint));
container_builder_.SetEarlyBreak(parent_break, appeal_inside);
}
}
// Breaking before isn't better if it's less appealing than what we already
// have (obviously), and also not if it has the same appeal as the break
// location inside the child that we just found (when the appeal is the same,
// whatever takes us further wins).
if (appeal_before < container_builder_.BreakAppeal() ||
appeal_before == appeal_inside)
return;
container_builder_.SetEarlyBreak(base::AdoptRef(new NGEarlyBreak(child)),
appeal_before);
}
void NGBlockLayoutAlgorithm::UpdateEarlyBreakBetweenLines() {
// We shouldn't be here if we already know where to break.
DCHECK(!early_break_);
// If the child already broke, it's a little too late to look for breakpoints.
DCHECK(!container_builder_.DidBreak());
int line_count = container_builder_.LineCount();
if (line_count < 2)
return;
// We can break between two of the lines if we have to. Calculate the best
// line number to break before, and the appeal of such a breakpoint.
int line_number =
std::max(line_count - Style().Widows(),
std::min(line_count - 1, static_cast<int>(Style().Orphans())));
NGBreakAppeal appeal = kBreakAppealPerfect;
if (line_number < Style().Orphans() ||
line_count - line_number < Style().Widows()) {
// Not enough lines in this container to satisfy the orphans and/or widows
// requirement. If we break before the last line (i.e. the last possible
// class B breakpoint), we'll fit as much as possible, and that's the best
// we can do.
line_number = line_count - 1;
appeal = kBreakAppealViolatingOrphansAndWidows;
}
if (container_builder_.BreakAppeal() <= appeal) {
scoped_refptr<const NGEarlyBreak> breakpoint =
base::AdoptRef(new NGEarlyBreak(line_number));
container_builder_.SetEarlyBreak(breakpoint, appeal);
}
}
NGBoxStrut NGBlockLayoutAlgorithm::CalculateMargins(
NGLayoutInputNode child,
bool is_new_fc,
bool* margins_fully_resolved) {
// We need to at least partially resolve margins before creating a constraint
// space for layout. Layout needs to know the line-left offset before
// starting. If the line-left offset cannot be calculated without fully
// resolving the margins (because of block alignment), we have to create a
// temporary constraint space now to figure out the inline size first. In all
// other cases we'll postpone full resolution until after child layout, when
// we actually have a child constraint space to use (and know the inline
// size).
*margins_fully_resolved = false;
DCHECK(child);
if (child.IsInline())
return {};
const ComputedStyle& child_style = child.Style();
bool needs_inline_size =
NeedsInlineSizeToResolveLineLeft(child_style, Style());
if (!needs_inline_size && !child_style.MayHaveMargin())
return {};
NGBoxStrut margins = ComputeMarginsFor(
child_style, child_percentage_size_.inline_size,
ConstraintSpace().GetWritingMode(), ConstraintSpace().Direction());
// As long as the child isn't establishing a new formatting context, we need
// to know its line-left offset before layout, to be able to position child
// floats correctly. If we need to resolve auto margins or other alignment
// properties to calculate the line-left offset, we also need to calculate its
// inline size first.
if (!is_new_fc && needs_inline_size) {
NGConstraintSpaceBuilder builder(ConstraintSpace(),
child_style.GetWritingMode(),
/* is_new_fc */ false);
builder.SetAvailableSize(child_available_size_);
builder.SetPercentageResolutionSize(child_percentage_size_);
NGConstraintSpace space = builder.ToConstraintSpace();
NGBoxStrut child_border_padding =
ComputeBorders(space, child) + ComputePadding(space, child.Style());
LayoutUnit child_inline_size =
ComputeInlineSizeForFragment(space, child, child_border_padding);
ResolveInlineMargins(child_style, Style(),
space.AvailableSize().inline_size, child_inline_size,
&margins);
*margins_fully_resolved = true;
}
return margins;
}
// Stop margin collapsing on one side of a block when
// -webkit-margin-{after,before}-collapse is something other than 'collapse'
// (the initial value)
void NGBlockLayoutAlgorithm::StopMarginCollapsing(
EMarginCollapse collapse_value,
LayoutUnit this_margin,
LayoutUnit* logical_block_offset,
NGMarginStrut* margin_strut) {
DCHECK_NE(collapse_value, EMarginCollapse::kCollapse);
if (collapse_value == EMarginCollapse::kSeparate) {
// Separate margins between previously adjoining margins and this margin,
// AND between this margin and adjoining margins to come.
*logical_block_offset += margin_strut->Sum() + this_margin;
*margin_strut = NGMarginStrut();
return;
}
DCHECK_EQ(collapse_value, EMarginCollapse::kDiscard);
// Discard previously adjoining margins, this margin AND all adjoining margins
// to come, so that the sum becomes 0.
margin_strut->discard_margins = true;
SetSubtreeModifiedMarginStrutIfNeeded();
}
NGConstraintSpace NGBlockLayoutAlgorithm::CreateConstraintSpaceForChild(
const NGLayoutInputNode child,
const NGInflowChildData& child_data,
const LogicalSize child_available_size,
bool is_new_fc,
const base::Optional<LayoutUnit> forced_bfc_block_offset,
bool has_clearance_past_adjoining_floats) {
const ComputedStyle& style = Style();
const ComputedStyle& child_style = child.Style();
WritingMode child_writing_mode =
child.IsInline() ? style.GetWritingMode() : child_style.GetWritingMode();
NGConstraintSpaceBuilder builder(ConstraintSpace(), child_writing_mode,
is_new_fc);
SetOrthogonalFallbackInlineSizeIfNeeded(Style(), child, &builder);
if (!IsParallelWritingMode(ConstraintSpace().GetWritingMode(),
child_writing_mode))
builder.SetIsShrinkToFit(child_style.LogicalWidth().IsAuto());
builder.SetAvailableSize(child_available_size);
builder.SetPercentageResolutionSize(child_percentage_size_);
builder.SetReplacedPercentageResolutionSize(replaced_child_percentage_size_);
if (ConstraintSpace().IsTableCell()) {
// If we have a fixed block-size we are in the "layout" mode.
NGTableCellChildLayoutMode mode;
if (ConstraintSpace().IsFixedBlockSize())
mode = NGTableCellChildLayoutMode::kLayout;
else if (ConstraintSpace().IsRestrictedBlockSizeTableCell())
mode = NGTableCellChildLayoutMode::kMeasureRestricted;
else
mode = NGTableCellChildLayoutMode::kMeasure;
builder.SetTableCellChildLayoutMode(mode);
}
if (NGBaseline::ShouldPropagateBaselines(child))
builder.AddBaselineRequests(ConstraintSpace().BaselineRequests());
bool has_bfc_block_offset = container_builder_.BfcBlockOffset().has_value();
// Propagate the |NGConstraintSpace::ForcedBfcBlockOffset| down to our
// children.
if (!has_bfc_block_offset && ConstraintSpace().ForcedBfcBlockOffset())
builder.SetForcedBfcBlockOffset(*ConstraintSpace().ForcedBfcBlockOffset());
if (forced_bfc_block_offset)
builder.SetForcedBfcBlockOffset(*forced_bfc_block_offset);
if (has_bfc_block_offset && child.IsBlock()) {
// Typically we aren't allowed to look at the previous layout result within
// a layout algorithm. However this is fine (honest), as it is just a hint
// to the child algorithm for where floats should be placed. If it doesn't
// have this flag, or gets this estimate wrong, it'll relayout with the
// appropriate "forced" BFC block-offset.
if (const NGLayoutResult* previous_result =
child.GetLayoutBox()->GetCachedLayoutResult()) {
const NGConstraintSpace& prev_space =
previous_result->GetConstraintSpaceForCaching();
// To increase the hit-rate we adjust the previous "optimistic"/"forced"
// BFC block-offset by how much the child has shifted from the previous
// layout.
LayoutUnit bfc_block_delta = child_data.bfc_offset_estimate.block_offset -
prev_space.BfcOffset().block_offset;
if (prev_space.ForcedBfcBlockOffset()) {
builder.SetOptimisticBfcBlockOffset(*prev_space.ForcedBfcBlockOffset() +
bfc_block_delta);
} else if (prev_space.OptimisticBfcBlockOffset()) {
builder.SetOptimisticBfcBlockOffset(
*prev_space.OptimisticBfcBlockOffset() + bfc_block_delta);
}
}
} else if (ConstraintSpace().OptimisticBfcBlockOffset()) {
// Propagate the |NGConstraintSpace::OptimisticBfcBlockOffset| down to our
// children.
builder.SetOptimisticBfcBlockOffset(
*ConstraintSpace().OptimisticBfcBlockOffset());
}
// Propagate the |NGConstraintSpace::AncestorHasClearancePastAdjoiningFloats|
// flag down to our children.
if (!has_bfc_block_offset &&
ConstraintSpace().AncestorHasClearancePastAdjoiningFloats())
builder.SetAncestorHasClearancePastAdjoiningFloats();
if (has_clearance_past_adjoining_floats)
builder.SetAncestorHasClearancePastAdjoiningFloats();
LayoutUnit clearance_offset = ConstraintSpace().IsNewFormattingContext()
? LayoutUnit::Min()
: ConstraintSpace().ClearanceOffset();
if (child.IsBlock()) {
LayoutUnit child_clearance_offset =
exclusion_space_.ClearanceOffset(child_style.Clear(Style()));
clearance_offset = std::max(clearance_offset, child_clearance_offset);
builder.SetTextDirection(child_style.Direction());
// PositionListMarker() requires a first line baseline.
if (container_builder_.UnpositionedListMarker()) {
builder.AddBaselineRequest(
{NGBaselineAlgorithmType::kFirstLine, style.GetFontBaseline()});
}
} else {
builder.SetTextDirection(style.Direction());
}
builder.SetClearanceOffset(clearance_offset);
if (!is_new_fc) {
builder.SetMarginStrut(child_data.margin_strut);
builder.SetBfcOffset(child_data.bfc_offset_estimate);
builder.SetExclusionSpace(exclusion_space_);
if (!has_bfc_block_offset) {
builder.SetAdjoiningObjectTypes(
container_builder_.AdjoiningObjectTypes());
}
} else if (child_data.is_resuming_after_break) {
// If the child is being resumed after a break, margins inside the child may
// be adjoining with the fragmentainer boundary, regardless of whether the
// child establishes a new formatting context or not.
builder.SetDiscardingMarginStrut();
}
if (ConstraintSpace().HasBlockFragmentation()) {
LayoutUnit new_bfc_block_offset;
// If a block establishes a new formatting context, we must know our
// position in the formatting context, to be able to adjust the
// fragmentation line.
if (is_new_fc)
new_bfc_block_offset = child_data.bfc_offset_estimate.block_offset;
SetupFragmentation(ConstraintSpace(), new_bfc_block_offset, &builder,
is_new_fc);
builder.SetEarlyBreakAppeal(container_builder_.BreakAppeal());
}
return builder.ToConstraintSpace();
}
LayoutUnit NGBlockLayoutAlgorithm::ComputeLineBoxBaselineOffset(
const NGBaselineRequest& request,
const NGPhysicalLineBoxFragment& line_box,
LayoutUnit line_box_block_offset) const {
NGLineHeightMetrics metrics =
line_box.BaselineMetrics(request.BaselineType());
DCHECK(!metrics.IsEmpty());
// NGLineHeightMetrics is line-relative, which matches to the flow-relative
// unless this box is in flipped-lines writing-mode.
if (!Style().IsFlippedLinesWritingMode())
return metrics.ascent + line_box_block_offset;
if (Node().IsInlineLevel()) {
// If this box is inline-level, since we're in NGBlockLayoutAlgorithm, this
// is an inline-block.
DCHECK(Node().IsAtomicInlineLevel());
// This box will be flipped when the containing line is flipped. Compute the
// baseline offset from the block-end (right in vertical-lr) content edge.
line_box_block_offset = container_builder_.Size().block_size -
(line_box_block_offset + line_box.Size().width);
return metrics.ascent + line_box_block_offset;
}
// Otherwise, the baseline is offset by the descent from the block-start
// content edge.
return metrics.descent + line_box_block_offset;
}
// Add a baseline from a child box fragment.
// @return false if the specified child is not a box or is OOF.
bool NGBlockLayoutAlgorithm::AddBaseline(const NGBaselineRequest& request,
const NGPhysicalFragment& child,
LayoutUnit child_offset) {
if (child.IsLineBox()) {
const auto& line_box = To<NGPhysicalLineBoxFragment>(child);
// Skip over a line-box which is empty. These don't have any baselines which
// should be added.
if (line_box.IsEmptyLineBox())
return false;
LayoutUnit offset =
ComputeLineBoxBaselineOffset(request, line_box, child_offset);
container_builder_.AddBaseline(request, offset);
return true;
}
if (child.IsFloatingOrOutOfFlowPositioned())
return false;
if (const auto* box = DynamicTo<NGPhysicalBoxFragment>(child)) {
if (base::Optional<LayoutUnit> baseline = box->Baseline(request)) {
container_builder_.AddBaseline(request, *baseline + child_offset);
return true;
}
}
return false;
}
// Propagate computed baselines from children.
// Skip children that do not produce baselines (e.g., empty blocks.)
void NGBlockLayoutAlgorithm::PropagateBaselinesFromChildren() {
const NGBaselineRequestList requests = ConstraintSpace().BaselineRequests();
if (requests.IsEmpty())
return;
for (const auto& request : requests) {
switch (request.AlgorithmType()) {
case NGBaselineAlgorithmType::kAtomicInline: {
if (Node().UseLogicalBottomMarginEdgeForInlineBlockBaseline()) {
LayoutUnit block_end = container_builder_.BlockSize();
NGBoxStrut margins =
ComputeMarginsForSelf(ConstraintSpace(), Style());
container_builder_.AddBaseline(request,
block_end + margins.block_end);
break;
}
const auto& children = container_builder_.Children();
for (auto it = children.rbegin(); it != children.rend(); ++it) {
if (AddBaseline(request, *it->fragment, it->offset.block_offset))
break;
}
break;
}
case NGBaselineAlgorithmType::kFirstLine:
for (const auto& child : container_builder_.Children()) {
if (AddBaseline(request, *child.fragment, child.offset.block_offset))
break;
}
break;
}
}
}
bool NGBlockLayoutAlgorithm::ResolveBfcBlockOffset(
NGPreviousInflowPosition* previous_inflow_position,
LayoutUnit bfc_block_offset,
base::Optional<LayoutUnit> forced_bfc_block_offset) {
if (container_builder_.BfcBlockOffset())
return true;
bfc_block_offset = forced_bfc_block_offset.value_or(bfc_block_offset);
if (ApplyClearance(ConstraintSpace(), &bfc_block_offset))
container_builder_.SetIsPushedByFloats();
container_builder_.SetBfcBlockOffset(bfc_block_offset);
if (NeedsAbortOnBfcBlockOffsetChange())
return false;
// Set the offset to our block-start border edge. We'll now end up at the
// block-start border edge. If the BFC block offset was resolved due to a
// block-start border or padding, that must be added by the caller, for
// subsequent layout to continue at the right position. Whether we need to add
// border+padding or not isn't something we should determine here, so it must
// be dealt with as part of initializing the layout algorithm.
previous_inflow_position->logical_block_offset = LayoutUnit();
// Resolving the BFC offset normally means that we have finished collapsing
// adjoining margins, so that we can reset the margin strut. One exception
// here is if we're resuming after a break, in which case we know that we can
// resolve the BFC offset to the block-start of the fragmentainer
// (block-offset 0). But keep the margin strut, since we're essentially still
// collapsing with the fragmentainer boundary, which will eat / discard all
// adjoining margins - unless this is at a forced break. DCHECK that the strut
// is empty (note that a strut that's set up to eat all margins will also be
// considered to be empty).
if (!is_resuming_)
previous_inflow_position->margin_strut = NGMarginStrut();
else
DCHECK(previous_inflow_position->margin_strut.IsEmpty());
return true;
}
bool NGBlockLayoutAlgorithm::NeedsAbortOnBfcBlockOffsetChange() const {
DCHECK(container_builder_.BfcBlockOffset());
if (!abort_when_bfc_block_offset_updated_)
return false;
// If our position differs from our (potentially optimistic) estimate, abort.
return *container_builder_.BfcBlockOffset() !=
ConstraintSpace().ExpectedBfcBlockOffset();
}
LayoutUnit NGBlockLayoutAlgorithm::CalculateMinimumBlockSize(
const NGMarginStrut& end_margin_strut) {
if (!Node().IsQuirkyAndFillsViewport())
return kIndefiniteSize;
if (!Style().LogicalHeight().IsAuto())
return kIndefiniteSize;
NGBoxStrut margins = ComputeMarginsForSelf(ConstraintSpace(), Style());
LayoutUnit margin_sum;
if (Node().CreatesNewFormattingContext()) {
margin_sum = margins.BlockSum();
} else {
DCHECK(Node().IsBody());
if (container_builder_.BfcBlockOffset()) {
NGMarginStrut body_strut = end_margin_strut;
body_strut.Append(margins.block_end, Style().HasMarginAfterQuirk());
margin_sum = *container_builder_.BfcBlockOffset() -
ConstraintSpace().BfcOffset().block_offset +
body_strut.Sum();
} else {
// The |end_margin_strut| is the block-start margin if the body doesn't
// have a BFC block-offset.
margin_sum = end_margin_strut.Sum() + margins.block_end;
}
}
return (ConstraintSpace().AvailableSize().block_size - margin_sum)
.ClampNegativeToZero();
}
bool NGBlockLayoutAlgorithm::PositionOrPropagateListMarker(
const NGLayoutResult& layout_result,
LogicalOffset* content_offset,
NGPreviousInflowPosition* previous_inflow_position) {
// If this is not a list-item, propagate unpositioned list markers to
// ancestors.
if (!node_.IsListItem()) {
if (layout_result.UnpositionedListMarker()) {
DCHECK(!container_builder_.UnpositionedListMarker());
container_builder_.SetUnpositionedListMarker(
layout_result.UnpositionedListMarker());
}
return true;
}
// If this is a list item, add the unpositioned list marker as a child.
NGUnpositionedListMarker list_marker = layout_result.UnpositionedListMarker();
if (!list_marker) {
list_marker = container_builder_.UnpositionedListMarker();
if (!list_marker)
return true;
container_builder_.SetUnpositionedListMarker(NGUnpositionedListMarker());
}
NGLineHeightMetrics content_metrics;
const NGConstraintSpace& space = ConstraintSpace();
const NGPhysicalFragment& content = layout_result.PhysicalFragment();
FontBaseline baseline_type = Style().GetFontBaseline();
if (list_marker.CanAddToBox(space, baseline_type, content,
&content_metrics)) {
// TODO: We are reusing the ConstraintSpace for LI here. It works well for
// now because authors cannot style list-markers currently. If we want to
// support `::marker` pseudo, we need to create ConstraintSpace for marker
// separately.
scoped_refptr<const NGLayoutResult> marker_layout_result =
list_marker.Layout(space, container_builder_.Style(), baseline_type);
DCHECK(marker_layout_result);
// If the BFC block-offset of li is still not resolved, resolved it now.
if (!container_builder_.BfcBlockOffset() &&
marker_layout_result->BfcBlockOffset()) {
// TODO: Currently the margin-top of marker is always zero. To support
// `::marker` pseudo, we should count marker's margin-top in.
#if DCHECK_IS_ON()
list_marker.CheckMargin();
#endif
if (!ResolveBfcBlockOffset(previous_inflow_position))
return false;
}
list_marker.AddToBox(space, baseline_type, content,
border_scrollbar_padding_, content_metrics,
*marker_layout_result, content_offset,
&container_builder_);
return true;
}
// If the list marker could not be positioned against this child because it
// does not have the baseline to align to, keep it as unpositioned and try
// the next child.
container_builder_.SetUnpositionedListMarker(list_marker);
return true;
}
bool NGBlockLayoutAlgorithm::PositionListMarkerWithoutLineBoxes(
NGPreviousInflowPosition* previous_inflow_position) {
DCHECK(node_.IsListItem());
DCHECK(container_builder_.UnpositionedListMarker());
NGUnpositionedListMarker list_marker =
container_builder_.UnpositionedListMarker();
const NGConstraintSpace& space = ConstraintSpace();
FontBaseline baseline_type = Style().GetFontBaseline();
// Layout the list marker.
scoped_refptr<const NGLayoutResult> marker_layout_result =
list_marker.Layout(space, container_builder_.Style(), baseline_type);
DCHECK(marker_layout_result);
// If the BFC block-offset of li is still not resolved, resolve it now.
if (!container_builder_.BfcBlockOffset() &&
marker_layout_result->BfcBlockOffset()) {
// TODO: Currently the margin-top of marker is always zero. To support
// `::marker` pseudo, we should count marker's margin-top in.
#if DCHECK_IS_ON()
list_marker.CheckMargin();
#endif
if (!ResolveBfcBlockOffset(previous_inflow_position))
return false;
}
// Position the list marker without aligning to line boxes.
LayoutUnit marker_block_size = list_marker.AddToBoxWithoutLineBoxes(
space, baseline_type, *marker_layout_result, &container_builder_);
container_builder_.SetUnpositionedListMarker(NGUnpositionedListMarker());
// Whether the list marker should affect the block size or not is not
// well-defined, but 3 out of 4 impls do.
// https://github.com/w3c/csswg-drafts/issues/2418
//
// The BFC block-offset has been resolved after layout marker. We'll always
// include the marker into the block-size.
if (container_builder_.BfcBlockOffset()) {
intrinsic_block_size_ = std::max(marker_block_size, intrinsic_block_size_);
container_builder_.SetIntrinsicBlockSize(intrinsic_block_size_);
container_builder_.SetBlockSize(
std::max(marker_block_size, container_builder_.Size().block_size));
}
return true;
}
} // namespace blink