Commit a9f390c2 authored by Morten Stenshorne's avatar Morten Stenshorne Committed by Commit Bot

[LayoutNG] Support fragmentainer breaks between column spanners.

While it's nice on its own to handle this, this CL can also be used as
an example to lean on when implementing block fragmentation support in
other layout modes, such as flexbox and tables.

We still don't support early breaks before column rows. That may also be
a useful thing to support, but it would require some more work, since
we'd need another way of specifying early breaks, because column rows
aren't uniquely identified by nodes. NGEarlyBreak currently only
supports breaks before line numbers or nodes.

Bug: 829028
Change-Id: Id1517790826223402c6ea4cd72ca565e0bd0e368
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1865325
Commit-Queue: Morten Stenshorne <mstensho@chromium.org>
Reviewed-by: default avatarIan Kilpatrick <ikilpatrick@chromium.org>
Cr-Commit-Position: refs/heads/master@{#707261}
parent 1fbbcc1d
...@@ -233,24 +233,8 @@ class CORE_EXPORT NGBlockLayoutAlgorithm ...@@ -233,24 +233,8 @@ class CORE_EXPORT NGBlockLayoutAlgorithm
// returned. // returned.
bool FinalizeForFragmentation(); bool FinalizeForFragmentation();
// Insert a fragmentainer break before the child if necessary. In that case, // Insert a fragmentainer break before the child if necessary.
// the previous in-flow position will be updated, we'll return |kBrokeBefore|. // See |::blink::BreakBeforeChildIfNeeded()| for more documentation.
// If we don't break inside, we'll consider the appeal of doing so anyway (and
// store it as the most appealing break point so far if that's the case),
// since we might have to go back and break here. Return |kContinue| if we're
// to continue laying out. If |kNeedsEarlierBreak| is returned, it means that
// we ran out of space, but shouldn't break before the child, but rather abort
// layout, and re-layout to a previously found good breakpoint. If
// |has_container_separation| is true, it means that we're at a valid
// breakpoint. We obviously prefer valid breakpoints, but sometimes we need to
// break at undesirable locations. Class A breakpoints occur between block
// siblings. Class B breakpoints between line boxes. Both these breakpoint
// classes imply that we're already past the first in-flow child in the
// container, but there's also another way of achieving container separation:
// class C breakpoints. Those occur if there's a positive gap between the
// block-start content edge of the container and the block-start margin edge
// of the first in-flow child. This can happen when in-flow content is pushed
// down by floats. https://www.w3.org/TR/css-break-3/#possible-breaks
NGBreakStatus BreakBeforeChildIfNeeded(NGLayoutInputNode child, NGBreakStatus BreakBeforeChildIfNeeded(NGLayoutInputNode child,
const NGLayoutResult&, const NGLayoutResult&,
NGPreviousInflowPosition*, NGPreviousInflowPosition*,
......
...@@ -200,6 +200,10 @@ class CORE_EXPORT NGBoxFragmentBuilder final ...@@ -200,6 +200,10 @@ class CORE_EXPORT NGBoxFragmentBuilder final
break_appeal_ = appeal; break_appeal_ = appeal;
} }
bool HasEarlyBreak() const { return early_break_.get(); } bool HasEarlyBreak() const { return early_break_.get(); }
const NGEarlyBreak& EarlyBreak() const {
DCHECK(early_break_.get());
return *early_break_.get();
}
// Set the highest break appeal found so far. This is either: // Set the highest break appeal found so far. This is either:
// 1: The highest appeal of a breakpoint found by our container // 1: The highest appeal of a breakpoint found by our container
......
...@@ -62,6 +62,17 @@ inline bool IsColumnSpanner(NGBlockNode multicol_container, ...@@ -62,6 +62,17 @@ inline bool IsColumnSpanner(NGBlockNode multicol_container,
return broken_node.IsColumnSpanAll() && broken_node != multicol_container; return broken_node.IsColumnSpanAll() && broken_node != multicol_container;
} }
// Add the break token for the column content that comes after a fragmented
// spanner, if any; otherwise, we're past all children.
void PushNextColumnBreakToken(
scoped_refptr<const NGBlockBreakToken> next_column_token,
NGBoxFragmentBuilder* builder) {
if (next_column_token)
builder->AddBreakToken(std::move(next_column_token));
else
builder->SetHasSeenAllChildren();
}
// Add the spanner's break token, AND another break token for the column content // Add the spanner's break token, AND another break token for the column content
// that comes after. In the next fragment we need to resume layout of the // that comes after. In the next fragment we need to resume layout of the
// spanner, and then proceed to the column content - if there's room for both. // spanner, and then proceed to the column content - if there's room for both.
...@@ -71,10 +82,7 @@ void PushSpannerBreakTokens( ...@@ -71,10 +82,7 @@ void PushSpannerBreakTokens(
scoped_refptr<const NGBlockBreakToken> next_column_token, scoped_refptr<const NGBlockBreakToken> next_column_token,
NGBoxFragmentBuilder* builder) { NGBoxFragmentBuilder* builder) {
builder->AddBreakToken(std::move(spanner_break_token)); builder->AddBreakToken(std::move(spanner_break_token));
if (next_column_token) PushNextColumnBreakToken(std::move(next_column_token), builder);
builder->AddBreakToken(std::move(next_column_token));
else
builder->SetHasSeenAllChildren();
} }
} // namespace } // namespace
...@@ -82,6 +90,7 @@ void PushSpannerBreakTokens( ...@@ -82,6 +90,7 @@ void PushSpannerBreakTokens(
NGColumnLayoutAlgorithm::NGColumnLayoutAlgorithm( NGColumnLayoutAlgorithm::NGColumnLayoutAlgorithm(
const NGLayoutAlgorithmParams& params) const NGLayoutAlgorithmParams& params)
: NGLayoutAlgorithm(params), : NGLayoutAlgorithm(params),
early_break_(params.early_break),
border_padding_(params.fragment_geometry.border + border_padding_(params.fragment_geometry.border +
params.fragment_geometry.padding), params.fragment_geometry.padding),
border_scrollbar_padding_(border_padding_ + border_scrollbar_padding_(border_padding_ +
...@@ -130,7 +139,11 @@ scoped_refptr<const NGLayoutResult> NGColumnLayoutAlgorithm::Layout() { ...@@ -130,7 +139,11 @@ scoped_refptr<const NGLayoutResult> NGColumnLayoutAlgorithm::Layout() {
if (!IsResumingLayout(BreakToken())) if (!IsResumingLayout(BreakToken()))
intrinsic_block_size_ = border_scrollbar_padding_.block_start; intrinsic_block_size_ = border_scrollbar_padding_.block_start;
LayoutChildren(); if (!LayoutChildren()) {
// We need to discard this layout and do it again. We found an earlier break
// point that's more appealing than the one we ran out of space at.
return RelayoutAndBreakEarlier();
}
// Figure out how much space we've already been able to process in previous // Figure out how much space we've already been able to process in previous
// fragments, if this multicol container participates in an outer // fragments, if this multicol container participates in an outer
...@@ -212,7 +225,7 @@ base::Optional<MinMaxSize> NGColumnLayoutAlgorithm::ComputeMinMaxSize( ...@@ -212,7 +225,7 @@ base::Optional<MinMaxSize> NGColumnLayoutAlgorithm::ComputeMinMaxSize(
return sizes; return sizes;
} }
void NGColumnLayoutAlgorithm::LayoutChildren() { bool NGColumnLayoutAlgorithm::LayoutChildren() {
NGMarginStrut margin_strut; NGMarginStrut margin_strut;
// First extract incoming child break tokens. // First extract incoming child break tokens.
...@@ -258,20 +271,28 @@ void NGColumnLayoutAlgorithm::LayoutChildren() { ...@@ -258,20 +271,28 @@ void NGColumnLayoutAlgorithm::LayoutChildren() {
// The multicol container previously broke at a spanner (this may happen if // The multicol container previously broke at a spanner (this may happen if
// we're nested inside another fragmentation context), so that's where we'll // we're nested inside another fragmentation context), so that's where we'll
// resume now. // resume now.
spanner_break_token = NGBreakStatus break_status = LayoutSpanner(
LayoutSpanner(To<NGBlockNode>(spanner_break_token->InputNode()), To<NGBlockNode>(spanner_break_token->InputNode()),
spanner_break_token.get(), &margin_strut); spanner_break_token.get(), &margin_strut, &spanner_break_token);
if (spanner_break_token) { if (spanner_break_token) {
// We broke at the spanner again! DCHECK_EQ(break_status, NGBreakStatus::kContinue);
PushSpannerBreakTokens(std::move(spanner_break_token), if (spanner_break_token) {
std::move(next_column_token), &container_builder_); // We broke at the spanner again!
return; PushSpannerBreakTokens(std::move(spanner_break_token),
std::move(next_column_token),
&container_builder_);
return true;
}
} else {
// Breaking before the first element in the fragmentainer isn't allowed,
// as that would give no content progress, and we'd be stuck forever.
DCHECK_EQ(break_status, NGBreakStatus::kContinue);
} }
} }
if (BreakToken() && BreakToken()->HasSeenAllChildren() && !next_column_token) if (BreakToken() && BreakToken()->HasSeenAllChildren() && !next_column_token)
return; return true;
// Entering the child main loop. Here we'll alternate between laying out // Entering the child main loop. Here we'll alternate between laying out
// column content and column spanners, until we're either done, or until // column content and column spanners, until we're either done, or until
...@@ -291,18 +312,41 @@ void NGColumnLayoutAlgorithm::LayoutChildren() { ...@@ -291,18 +312,41 @@ void NGColumnLayoutAlgorithm::LayoutChildren() {
if (!spanner_node) if (!spanner_node)
break; break;
// We found a spanner. Lay it out, and then resume column layout. if (early_break_) {
spanner_break_token = LayoutSpanner(spanner_node, nullptr, &margin_strut); // If this is the child we had previously determined to break before, do
// so now and finish layout.
DCHECK_EQ(early_break_->Type(), NGEarlyBreak::kBlock);
if (early_break_->IsBreakBefore() &&
early_break_->BlockNode() == spanner_node) {
container_builder_.AddBreakBeforeChild(
spanner_node, kBreakAppealPerfect, /* is_forced_break */ false);
FinishAfterBreakBeforeSpanner(std::move(next_column_token));
return true;
}
}
if (spanner_break_token) { // We found a spanner. Lay it out, and then resume column layout.
// We broke before or inside the spanner. This may happen if we're nested NGBreakStatus break_status = LayoutSpanner(
// inside another fragmentation context. spanner_node, nullptr, &margin_strut, &spanner_break_token);
if (break_status == NGBreakStatus::kNeedsEarlierBreak) {
return false;
} else if (break_status == NGBreakStatus::kBrokeBefore) {
DCHECK(ConstraintSpace().HasBlockFragmentation());
FinishAfterBreakBeforeSpanner(std::move(next_column_token));
return true;
} else if (spanner_break_token) {
DCHECK_EQ(break_status, NGBreakStatus::kContinue);
// We broke inside the spanner. This may happen if we're nested inside
// another fragmentation context.
PushSpannerBreakTokens(std::move(spanner_break_token), PushSpannerBreakTokens(std::move(spanner_break_token),
std::move(next_column_token), &container_builder_); std::move(next_column_token), &container_builder_);
return; return true;
} }
} while (next_column_token); } while (next_column_token);
// If there's an early break set, we should have found it and returned.
DCHECK(!early_break_);
if (next_column_token) { if (next_column_token) {
// We broke inside column content. Add a break token for where to resume // We broke inside column content. Add a break token for where to resume
// column layout at in the next fragment. // column layout at in the next fragment.
...@@ -318,6 +362,8 @@ void NGColumnLayoutAlgorithm::LayoutChildren() { ...@@ -318,6 +362,8 @@ void NGColumnLayoutAlgorithm::LayoutChildren() {
intrinsic_block_size_ += margin_strut.Sum(); intrinsic_block_size_ += margin_strut.Sum();
} }
return true;
} }
scoped_refptr<const NGLayoutResult> NGColumnLayoutAlgorithm::LayoutRow( scoped_refptr<const NGLayoutResult> NGColumnLayoutAlgorithm::LayoutRow(
...@@ -511,7 +557,7 @@ scoped_refptr<const NGLayoutResult> NGColumnLayoutAlgorithm::LayoutRow( ...@@ -511,7 +557,7 @@ scoped_refptr<const NGLayoutResult> NGColumnLayoutAlgorithm::LayoutRow(
break; break;
} while (true); } while (true);
bool keep_margin = false; bool is_empty = false;
// If there was no content inside to process, we don't want the resulting // If there was no content inside to process, we don't want the resulting
// empty column fragment. // empty column fragment.
...@@ -521,7 +567,7 @@ scoped_refptr<const NGLayoutResult> NGColumnLayoutAlgorithm::LayoutRow( ...@@ -521,7 +567,7 @@ scoped_refptr<const NGLayoutResult> NGColumnLayoutAlgorithm::LayoutRow(
if (column.Children().size() == 0) { if (column.Children().size() == 0) {
// No content. Keep the trailing margin from any previous column spanner. // No content. Keep the trailing margin from any previous column spanner.
keep_margin = true; is_empty = true;
// TODO(mstensho): It's wrong to keep the empty fragment, just so that // TODO(mstensho): It's wrong to keep the empty fragment, just so that
// out-of-flow descendants get propagated correctly. Find some other way // out-of-flow descendants get propagated correctly. Find some other way
...@@ -533,9 +579,14 @@ scoped_refptr<const NGLayoutResult> NGColumnLayoutAlgorithm::LayoutRow( ...@@ -533,9 +579,14 @@ scoped_refptr<const NGLayoutResult> NGColumnLayoutAlgorithm::LayoutRow(
intrinsic_block_size_ = column_block_offset + column_size.block_size; intrinsic_block_size_ = column_block_offset + column_size.block_size;
// We added a row. Reset the trailing margin from any previous column spanner. if (!is_empty) {
if (!keep_margin) has_processed_first_child_ = true;
container_builder_.SetPreviousBreakAfter(EBreakBetween::kAuto);
// We added a row. Reset the trailing margin from any previous column
// spanner.
*margin_strut = NGMarginStrut(); *margin_strut = NGMarginStrut();
}
// Commit all column fragments to the fragment builder. // Commit all column fragments to the fragment builder.
for (auto column : new_columns) { for (auto column : new_columns) {
...@@ -546,20 +597,23 @@ scoped_refptr<const NGLayoutResult> NGColumnLayoutAlgorithm::LayoutRow( ...@@ -546,20 +597,23 @@ scoped_refptr<const NGLayoutResult> NGColumnLayoutAlgorithm::LayoutRow(
return result; return result;
} }
scoped_refptr<const NGBlockBreakToken> NGColumnLayoutAlgorithm::LayoutSpanner( NGBreakStatus NGColumnLayoutAlgorithm::LayoutSpanner(
NGBlockNode spanner_node, NGBlockNode spanner_node,
const NGBlockBreakToken* break_token, const NGBlockBreakToken* break_token,
NGMarginStrut* margin_strut) { NGMarginStrut* margin_strut,
scoped_refptr<const NGBlockBreakToken>* spanner_break_token) {
*spanner_break_token = nullptr;
const ComputedStyle& spanner_style = spanner_node.Style(); const ComputedStyle& spanner_style = spanner_node.Style();
NGBoxStrut margins = ComputeMarginsFor( NGBoxStrut margins = ComputeMarginsFor(
spanner_style, content_box_size_.inline_size, spanner_style, content_box_size_.inline_size,
ConstraintSpace().GetWritingMode(), ConstraintSpace().Direction()); ConstraintSpace().GetWritingMode(), ConstraintSpace().Direction());
if (break_token) { if (break_token) {
// Truncate block-start margins at fragmentainer breaks, and also make sure // Truncate block-start margins at fragmentainer breaks (except when the
// that we don't repeat them at the beginning of every fragment generated // break is forced), and also make sure that we don't repeat them at the
// from the spanner node. // beginning of every fragment generated from the spanner node.
margins.block_start = LayoutUnit(); if (!break_token->IsBreakBefore() || !break_token->IsForcedBreak())
margins.block_start = LayoutUnit();
if (break_token->IsBreakBefore()) { if (break_token->IsBreakBefore()) {
// TODO(mstensho): Passing a break-before token shouldn't be a problem, // TODO(mstensho): Passing a break-before token shouldn't be a problem,
...@@ -573,12 +627,42 @@ scoped_refptr<const NGBlockBreakToken> NGColumnLayoutAlgorithm::LayoutSpanner( ...@@ -573,12 +627,42 @@ scoped_refptr<const NGBlockBreakToken> NGColumnLayoutAlgorithm::LayoutSpanner(
// of an immediately preceding spanner, if any. // of an immediately preceding spanner, if any.
margin_strut->Append(margins.block_start, /* is_quirky */ false); margin_strut->Append(margins.block_start, /* is_quirky */ false);
// TODO(mstensho): outer fragmentainer breaks between spanners and rows (the
// spanner may be unbreakable inside, and we may be in a nested fragmentation
// context and out of space).
LayoutUnit block_offset = intrinsic_block_size_ + margin_strut->Sum(); LayoutUnit block_offset = intrinsic_block_size_ + margin_strut->Sum();
auto spanner_space = CreateConstraintSpaceForSpanner(block_offset); auto spanner_space = CreateConstraintSpaceForSpanner(block_offset);
auto result = spanner_node.Layout(spanner_space, break_token);
const NGEarlyBreak* early_break_in_child = nullptr;
if (early_break_ && early_break_->Type() == NGEarlyBreak::kBlock &&
early_break_->BlockNode() == spanner_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);
}
auto result =
spanner_node.Layout(spanner_space, break_token, early_break_in_child);
if (ConstraintSpace().HasBlockFragmentation() && !early_break_) {
// We're nested inside another fragmentation context. Examine this break
// point, and determine whether we should break.
LayoutUnit fragmentainer_block_offset =
ConstraintSpace().FragmentainerOffsetAtBfc() + block_offset;
NGBreakStatus break_status = BreakBeforeChildIfNeeded(
ConstraintSpace(), spanner_node, *result.get(),
fragmentainer_block_offset, has_processed_first_child_,
&container_builder_);
if (break_status != NGBreakStatus::kContinue) {
// We need to break, either before the spanner, or even earlier.
return break_status;
}
}
NGFragment fragment(ConstraintSpace().GetWritingMode(), NGFragment fragment(ConstraintSpace().GetWritingMode(),
result->PhysicalFragment()); result->PhysicalFragment());
...@@ -593,18 +677,16 @@ scoped_refptr<const NGBlockBreakToken> NGColumnLayoutAlgorithm::LayoutSpanner( ...@@ -593,18 +677,16 @@ scoped_refptr<const NGBlockBreakToken> NGColumnLayoutAlgorithm::LayoutSpanner(
*margin_strut = NGMarginStrut(); *margin_strut = NGMarginStrut();
margin_strut->Append(margins.block_end, /* is_quirky */ false); margin_strut->Append(margins.block_end, /* is_quirky */ false);
// TODO(mstensho): The correct thing would be to weigh any break inside
// against the appeal of breaking before the spanner, like we do in
// BreakBeforeChildIfNeeded() for the block layout algorithm. Just setting the
// appeal to perfect isn't right, but we're doing it for now, so that any
// break inside the spanner (in case we're nested inside another fragmentation
// context) isn't just discarded.
if (ConstraintSpace().HasBlockFragmentation())
container_builder_.SetBreakAppeal(kBreakAppealPerfect);
intrinsic_block_size_ = offset.block_offset + fragment.BlockSize(); intrinsic_block_size_ = offset.block_offset + fragment.BlockSize();
has_processed_first_child_ = true;
return To<NGBlockBreakToken>(result->PhysicalFragment().BreakToken()); EBreakBetween break_after = JoinFragmentainerBreakValues(
result->FinalBreakAfter(), spanner_node.Style().BreakAfter());
container_builder_.SetPreviousBreakAfter(break_after);
*spanner_break_token =
To<NGBlockBreakToken>(result->PhysicalFragment().BreakToken());
return NGBreakStatus::kContinue;
} }
LayoutUnit NGColumnLayoutAlgorithm::CalculateBalancedColumnBlockSize( LayoutUnit NGColumnLayoutAlgorithm::CalculateBalancedColumnBlockSize(
...@@ -784,6 +866,39 @@ LayoutUnit NGColumnLayoutAlgorithm::CurrentContentBlockOffset() const { ...@@ -784,6 +866,39 @@ LayoutUnit NGColumnLayoutAlgorithm::CurrentContentBlockOffset() const {
return intrinsic_block_size_ - border_scrollbar_padding_.block_start; return intrinsic_block_size_ - border_scrollbar_padding_.block_start;
} }
void NGColumnLayoutAlgorithm::FinishAfterBreakBeforeSpanner(
scoped_refptr<const NGBlockBreakToken> next_column_token) {
// We broke before the spanner. We're done here. Take up the remaining space
// in the outer fragmentation context.
intrinsic_block_size_ = FragmentainerSpaceAtBfcStart(ConstraintSpace());
// A break token for the spanner has already been inserted, but we also need
// to add one for the column contents that follows, so that we know where to
// resume, once done with the spanner - or - specify that we're past
// everything if there's nothing to resume at (so that we don't restart from
// the beginning of the multicol container).
PushNextColumnBreakToken(std::move(next_column_token), &container_builder_);
}
scoped_refptr<const NGLayoutResult>
NGColumnLayoutAlgorithm::RelayoutAndBreakEarlier() {
// Not allowed to recurse!
DCHECK(!early_break_);
const NGEarlyBreak& breakpoint = container_builder_.EarlyBreak();
NGLayoutAlgorithmParams params(Node(),
container_builder_.InitialFragmentGeometry(),
ConstraintSpace(), BreakToken(), &breakpoint);
NGColumnLayoutAlgorithm 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();
}
NGConstraintSpace NGColumnLayoutAlgorithm::CreateConstraintSpaceForColumns( NGConstraintSpace NGColumnLayoutAlgorithm::CreateConstraintSpaceForColumns(
const LogicalSize& column_size, const LogicalSize& column_size,
bool is_first_fragmentainer, bool is_first_fragmentainer,
......
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
namespace blink { namespace blink {
enum class NGBreakStatus;
class NGBlockNode; class NGBlockNode;
class NGBlockBreakToken; class NGBlockBreakToken;
class NGConstraintSpace; class NGConstraintSpace;
...@@ -29,8 +30,10 @@ class CORE_EXPORT NGColumnLayoutAlgorithm ...@@ -29,8 +30,10 @@ class CORE_EXPORT NGColumnLayoutAlgorithm
const MinMaxSizeInput&) const override; const MinMaxSizeInput&) const override;
private: private:
// Lay out as many children as we can. // Lay out as many children as we can. If false is returned, it means that we
void LayoutChildren(); // ran out of space at an unappealing location, and need to relayout and break
// earlier (because we have a better breakpoint there).
bool LayoutChildren();
// Lay out one row of columns. The layout result returned is for the last // Lay out one row of columns. The layout result returned is for the last
// column that was laid out. The rows themselves don't create fragments. // column that was laid out. The rows themselves don't create fragments.
...@@ -38,13 +41,16 @@ class CORE_EXPORT NGColumnLayoutAlgorithm ...@@ -38,13 +41,16 @@ class CORE_EXPORT NGColumnLayoutAlgorithm
const NGBlockBreakToken* next_column_token, const NGBlockBreakToken* next_column_token,
NGMarginStrut*); NGMarginStrut*);
// Lay out a column spanner. Will return a break token if we break before or // Lay out a column spanner. The return value will tell whether to break
// inside the spanner. If no break token is returned, it means that we can // before the spanner or not. If we're not to break before the spanner, but
// proceed to the next row of columns. // rather inside, |spanner_break_token| will be set, so that we know where to
scoped_refptr<const NGBlockBreakToken> LayoutSpanner( // resume in the next outer fragmentainer. If |NGBreakStatus::kContinue| is
NGBlockNode spanner_node, // returned, and no break token was set, it means that we can proceed to the
const NGBlockBreakToken* break_token, // next row of columns.
NGMarginStrut*); NGBreakStatus LayoutSpanner(NGBlockNode spanner_node,
const NGBlockBreakToken* break_token,
NGMarginStrut*,
scoped_refptr<const NGBlockBreakToken>*);
LayoutUnit CalculateBalancedColumnBlockSize( LayoutUnit CalculateBalancedColumnBlockSize(
const LogicalSize& column_size, const LogicalSize& column_size,
...@@ -58,6 +64,16 @@ class CORE_EXPORT NGColumnLayoutAlgorithm ...@@ -58,6 +64,16 @@ class CORE_EXPORT NGColumnLayoutAlgorithm
LayoutUnit ConstrainColumnBlockSize(LayoutUnit size) const; LayoutUnit ConstrainColumnBlockSize(LayoutUnit size) const;
LayoutUnit CurrentContentBlockOffset() const; LayoutUnit CurrentContentBlockOffset() const;
// Finalize layout after breaking before a spanner.
void FinishAfterBreakBeforeSpanner(
scoped_refptr<const NGBlockBreakToken> next_column_token);
// Lay out again, this time with a predefined good breakpoint that we
// discovered in the first pass. This happens when we run out of space in a
// fragmentainer at an less-than-ideal location, due to breaking restrictions,
// such as break-before:avoid or break-after:avoid.
scoped_refptr<const NGLayoutResult> RelayoutAndBreakEarlier();
NGConstraintSpace CreateConstraintSpaceForColumns( NGConstraintSpace CreateConstraintSpaceForColumns(
const LogicalSize& column_size, const LogicalSize& column_size,
bool is_first_fragmentainer, bool is_first_fragmentainer,
...@@ -68,6 +84,9 @@ class CORE_EXPORT NGColumnLayoutAlgorithm ...@@ -68,6 +84,9 @@ class CORE_EXPORT NGColumnLayoutAlgorithm
LayoutUnit block_offset) const; LayoutUnit block_offset) const;
NGConstraintSpace CreateConstraintSpaceForMinMax() const; NGConstraintSpace CreateConstraintSpaceForMinMax() const;
// When set, this will specify where to break before or inside.
const NGEarlyBreak* early_break_ = nullptr;
const NGBoxStrut border_padding_; const NGBoxStrut border_padding_;
const NGBoxStrut border_scrollbar_padding_; const NGBoxStrut border_scrollbar_padding_;
LogicalSize content_box_size_; LogicalSize content_box_size_;
...@@ -76,6 +95,11 @@ class CORE_EXPORT NGColumnLayoutAlgorithm ...@@ -76,6 +95,11 @@ class CORE_EXPORT NGColumnLayoutAlgorithm
LayoutUnit column_inline_progression_; LayoutUnit column_inline_progression_;
LayoutUnit intrinsic_block_size_; LayoutUnit intrinsic_block_size_;
bool is_constrained_by_outer_fragmentation_context_ = false; bool is_constrained_by_outer_fragmentation_context_ = false;
// This will be set during (outer) block fragmentation once we've processed
// the first piece of content of the multicol container. It is used to check
// if we're at a valid class A breakpoint (between block-level siblings).
bool has_processed_first_child_ = false;
}; };
} // namespace blink } // namespace blink
......
...@@ -4672,6 +4672,424 @@ TEST_F(NGColumnLayoutAlgorithmTest, BreakInsideSpannerWithContent) { ...@@ -4672,6 +4672,424 @@ TEST_F(NGColumnLayoutAlgorithmTest, BreakInsideSpannerWithContent) {
EXPECT_EQ(expectation, dump); EXPECT_EQ(expectation, dump);
} }
TEST_F(NGColumnLayoutAlgorithmTest, ForcedBreakBetweenSpanners) {
// There are two spanners in a nested multicol. They could fit in the same
// outer column, but there's a forced break between them.
SetBodyInnerHTML(R"HTML(
<style>
.outer { columns:3; height:100px; column-fill:auto; column-gap:10px; width:320px; }
.inner { columns:2; }
</style>
<div id="container">
<div class="outer">
<div class="inner">
<div style="column-span:all; break-inside:avoid; width:55px; height:40px;"></div>
<div style="column-span:all; break-before:column; break-inside:avoid; width:66px; height:40px;"></div>
</div>
</div>
</div>
)HTML");
String dump = DumpFragmentTree(GetElementById("container"));
String expectation = R"DUMP(.:: LayoutNG Physical Fragment Tree ::.
offset:unplaced size:1000x100
offset:0,0 size:320x100
offset:0,0 size:100x100
offset:0,0 size:100x100
offset:0,0 size:55x40
offset:110,0 size:100x40
offset:0,0 size:100x40
offset:0,0 size:66x40
)DUMP";
EXPECT_EQ(expectation, dump);
}
TEST_F(NGColumnLayoutAlgorithmTest, ForcedBreakBetweenSpanners2) {
// There are two spanners in a nested multicol. They could fit in the same
// outer column, but there's a forced break between them.
SetBodyInnerHTML(R"HTML(
<style>
.outer { columns:3; height:100px; column-fill:auto; column-gap:10px; width:320px; }
.inner { columns:2; }
</style>
<div id="container">
<div class="outer">
<div class="inner">
<div style="column-span:all; break-after:column; break-inside:avoid; width:55px; height:40px;"></div>
<div style="column-span:all; break-inside:avoid; width:66px; height:40px;"></div>
</div>
</div>
</div>
)HTML");
String dump = DumpFragmentTree(GetElementById("container"));
String expectation = R"DUMP(.:: LayoutNG Physical Fragment Tree ::.
offset:unplaced size:1000x100
offset:0,0 size:320x100
offset:0,0 size:100x100
offset:0,0 size:100x100
offset:0,0 size:55x40
offset:110,0 size:100x40
offset:0,0 size:100x40
offset:0,0 size:66x40
)DUMP";
EXPECT_EQ(expectation, dump);
}
TEST_F(NGColumnLayoutAlgorithmTest, ForcedBreakBetweenSpanners3) {
// There are two spanners in a nested multicol. They could fit in the same
// outer column, but there's a forced break after the last child of the first
// spanner.
SetBodyInnerHTML(R"HTML(
<style>
.outer { columns:3; height:100px; column-fill:auto; column-gap:10px; width:320px; }
.inner { columns:2; }
</style>
<div id="container">
<div class="outer">
<div class="inner">
<div style="column-span:all; break-inside:avoid; width:55px; height:40px;">
<div style="width:33px; height:10px;"></div>
<div style="break-after:column; width:44px; height:10px;"></div>
</div>
<div style="column-span:all; break-inside:avoid; width:66px; height:40px;"></div>
</div>
</div>
</div>
)HTML");
String dump = DumpFragmentTree(GetElementById("container"));
String expectation = R"DUMP(.:: LayoutNG Physical Fragment Tree ::.
offset:unplaced size:1000x100
offset:0,0 size:320x100
offset:0,0 size:100x100
offset:0,0 size:100x100
offset:0,0 size:55x40
offset:0,0 size:33x10
offset:0,10 size:44x10
offset:110,0 size:100x40
offset:0,0 size:100x40
offset:0,0 size:66x40
)DUMP";
EXPECT_EQ(expectation, dump);
}
TEST_F(NGColumnLayoutAlgorithmTest, ForcedBreakBetweenSpanners4) {
// There are two spanners in a nested multicol. They could fit in the same
// outer column, but there's a forced break before the first child of the
// last spanner.
SetBodyInnerHTML(R"HTML(
<style>
.outer { columns:3; height:100px; column-fill:auto; column-gap:10px; width:320px; }
.inner { columns:2; }
</style>
<div id="container">
<div class="outer">
<div class="inner">
<div style="column-span:all; break-inside:avoid; width:55px; height:40px;"></div>
<div style="column-span:all; break-inside:avoid; width:66px; height:40px;">
<div style="break-before:column; width:33px; height:10px;"></div>
<div style="width:44px; height:10px;"></div>
</div>
</div>
</div>
</div>
)HTML");
String dump = DumpFragmentTree(GetElementById("container"));
String expectation = R"DUMP(.:: LayoutNG Physical Fragment Tree ::.
offset:unplaced size:1000x100
offset:0,0 size:320x100
offset:0,0 size:100x100
offset:0,0 size:100x100
offset:0,0 size:55x40
offset:110,0 size:100x40
offset:0,0 size:100x40
offset:0,0 size:66x40
offset:0,0 size:33x10
offset:0,10 size:44x10
)DUMP";
EXPECT_EQ(expectation, dump);
}
TEST_F(NGColumnLayoutAlgorithmTest, ForcedBreakBetweenSpanners5) {
// There are two spanners in a nested multicol. They could fit in the same
// outer column, but there's a forced break between them. The second spanner
// has a top margin, which should be retained, due to the forced break.
SetBodyInnerHTML(R"HTML(
<style>
.outer { columns:3; height:100px; column-fill:auto; column-gap:10px; width:320px; }
.inner { columns:2; }
</style>
<div id="container">
<div class="outer">
<div class="inner">
<div style="column-span:all; break-inside:avoid; width:55px; height:40px;"></div>
<div style="column-span:all; break-before:column; break-inside:avoid; width:66px; height:40px; margin-top:10px;"></div>
</div>
</div>
</div>
)HTML");
String dump = DumpFragmentTree(GetElementById("container"));
String expectation = R"DUMP(.:: LayoutNG Physical Fragment Tree ::.
offset:unplaced size:1000x100
offset:0,0 size:320x100
offset:0,0 size:100x100
offset:0,0 size:100x100
offset:0,0 size:55x40
offset:110,0 size:100x50
offset:0,0 size:100x50
offset:0,10 size:66x40
)DUMP";
EXPECT_EQ(expectation, dump);
}
TEST_F(NGColumnLayoutAlgorithmTest, SoftBreakBetweenSpanners) {
// There are two spanners in a nested multicol. They won't fit in the same
// outer column, and we don't want to break inside. So we should break between
// them.
SetBodyInnerHTML(R"HTML(
<style>
.outer { columns:3; height:100px; column-fill:auto; column-gap:10px; width:320px; }
.inner { columns:2; }
</style>
<div id="container">
<div class="outer">
<div class="inner">
<div style="column-span:all; break-inside:avoid; width:55px; height:60px;"></div>
<div style="column-span:all; break-inside:avoid; width:66px; height:60px;"></div>
</div>
</div>
</div>
)HTML");
String dump = DumpFragmentTree(GetElementById("container"));
String expectation = R"DUMP(.:: LayoutNG Physical Fragment Tree ::.
offset:unplaced size:1000x100
offset:0,0 size:320x100
offset:0,0 size:100x100
offset:0,0 size:100x100
offset:0,0 size:55x60
offset:110,0 size:100x60
offset:0,0 size:100x60
offset:0,0 size:66x60
)DUMP";
EXPECT_EQ(expectation, dump);
}
TEST_F(NGColumnLayoutAlgorithmTest, SoftBreakBetweenSpanners2) {
// There are two spanners in a nested multicol. They won't fit in the same
// outer column, and we don't want to break inside. So we should break between
// them. The second spanner has a top margin, but it should be truncated since
// it's at a soft break.
SetBodyInnerHTML(R"HTML(
<style>
.outer { columns:3; height:100px; column-fill:auto; column-gap:10px; width:320px; }
.inner { columns:2; }
</style>
<div id="container">
<div class="outer">
<div class="inner">
<div style="column-span:all; break-inside:avoid; width:55px; height:60px;"></div>
<div style="column-span:all; break-inside:avoid; width:66px; height:60px; margin-top:10px;"></div>
</div>
</div>
</div>
)HTML");
String dump = DumpFragmentTree(GetElementById("container"));
String expectation = R"DUMP(.:: LayoutNG Physical Fragment Tree ::.
offset:unplaced size:1000x100
offset:0,0 size:320x100
offset:0,0 size:100x100
offset:0,0 size:100x100
offset:0,0 size:55x60
offset:110,0 size:100x60
offset:0,0 size:100x60
offset:0,0 size:66x60
)DUMP";
EXPECT_EQ(expectation, dump);
}
TEST_F(NGColumnLayoutAlgorithmTest, AvoidSoftBreakBetweenSpanners) {
// There are three spanners in a nested multicol. The first two could fit in
// the same outer column, but the third one is too tall, and we also don't
// want to break before that one.So we should break between the two first
// spanners.
SetBodyInnerHTML(R"HTML(
<style>
.outer { columns:3; height:100px; column-fill:auto; column-gap:10px; width:320px; }
.inner { columns:2; }
</style>
<div id="container">
<div class="outer">
<div class="inner">
<div style="column-span:all; break-inside:avoid; width:55px; height:40px;"></div>
<div style="column-span:all; break-inside:avoid; width:66px; height:40px;"></div>
<div style="column-span:all; break-inside:avoid; break-before:avoid; width:77px; height:60px;"></div>
</div>
</div>
</div>
)HTML");
String dump = DumpFragmentTree(GetElementById("container"));
String expectation = R"DUMP(.:: LayoutNG Physical Fragment Tree ::.
offset:unplaced size:1000x100
offset:0,0 size:320x100
offset:0,0 size:100x100
offset:0,0 size:100x100
offset:0,0 size:55x40
offset:110,0 size:100x100
offset:0,0 size:100x100
offset:0,0 size:66x40
offset:0,40 size:77x60
)DUMP";
EXPECT_EQ(expectation, dump);
}
TEST_F(NGColumnLayoutAlgorithmTest, AvoidSoftBreakBetweenSpanners2) {
// There are two spanners in a nested multicol. They won't fit in the same
// outer column, but we don't want to break inside the second one, and also
// not between the spanners. The first spanner is breakable, so we should
// break at the most appealing breakpoint there, i.e. before its last child.
SetBodyInnerHTML(R"HTML(
<style>
.outer { columns:3; height:100px; column-fill:auto; column-gap:10px; width:320px; }
.inner { columns:2; }
.content { break-inside:avoid; height:20px; }
</style>
<div id="container">
<div class="outer">
<div class="inner">
<div style="column-span:all; width:11px;">
<div class="content" style="width:22px;"></div>
<div class="content" style="width:33px;"></div>
<div class="content" style="width:44px;"></div>
</div>
<div style="column-span:all; break-inside:avoid; break-before:avoid; width:55px; height:60px;"></div>
</div>
</div>
</div>
)HTML");
String dump = DumpFragmentTree(GetElementById("container"));
String expectation = R"DUMP(.:: LayoutNG Physical Fragment Tree ::.
offset:unplaced size:1000x100
offset:0,0 size:320x100
offset:0,0 size:100x100
offset:0,0 size:100x100
offset:0,0 size:11x100
offset:0,0 size:22x20
offset:0,20 size:33x20
offset:110,0 size:100x80
offset:0,0 size:100x80
offset:0,0 size:11x20
offset:0,0 size:44x20
offset:0,20 size:55x60
)DUMP";
EXPECT_EQ(expectation, dump);
}
TEST_F(NGColumnLayoutAlgorithmTest, AvoidSoftBreakBetweenSpanners3) {
// Violate orphans and widows requests rather than break-between avoidance
// requests.
SetBodyInnerHTML(R"HTML(
<style>
.outer {
columns:3;
height:100px;
column-fill:auto;
column-gap:10px;
width:320px;
line-height: 20px;
orphans: 3;
widows: 3;
}
.inner { columns:2; }
</style>
<div id="container">
<div class="outer">
<div class="inner">
<div style="column-span:all; width:11px;">
<br>
<br>
<br>
</div>
<div style="column-span:all; break-inside:avoid; break-before:avoid; width:55px; height:60px;"></div>
</div>
</div>
</div>
)HTML");
String dump = DumpFragmentTree(GetElementById("container"));
String expectation = R"DUMP(.:: LayoutNG Physical Fragment Tree ::.
offset:unplaced size:1000x100
offset:0,0 size:320x100
offset:0,0 size:100x100
offset:0,0 size:100x100
offset:0,0 size:11x100
offset:0,0 size:0x20
offset:0,9 size:0x1
offset:0,20 size:0x20
offset:0,9 size:0x1
offset:110,0 size:100x80
offset:0,0 size:100x80
offset:0,0 size:11x20
offset:0,0 size:0x20
offset:0,9 size:0x1
offset:0,20 size:55x60
)DUMP";
EXPECT_EQ(expectation, dump);
}
TEST_F(NGColumnLayoutAlgorithmTest, SoftBreakBetweenRowAndSpanner) {
// We have a nested multicol with some column content, followed by a
// spanner. Everything won't fit in the same outer column, and we don't want
// to break inside the spanner. Break between the row of columns and the
// spanner.
SetBodyInnerHTML(R"HTML(
<style>
.outer {
columns:3;
height:100px;
column-fill:auto;
column-gap:10px;
width:320px;
}
.inner { columns:2; column-gap:10px; }
.content { break-inside:avoid; height:20px; }
</style>
<div id="container">
<div class="outer">
<div class="inner">
<div class="content" style="width:11px;"></div>
<div class="content" style="width:22px;"></div>
<div class="content" style="width:33px;"></div>
<div style="column-span:all; break-inside:avoid; width:44px; height:70px;"></div>
</div>
</div>
</div>
)HTML");
String dump = DumpFragmentTree(GetElementById("container"));
String expectation = R"DUMP(.:: LayoutNG Physical Fragment Tree ::.
offset:unplaced size:1000x100
offset:0,0 size:320x100
offset:0,0 size:100x100
offset:0,0 size:100x100
offset:0,0 size:45x40
offset:0,0 size:11x20
offset:0,20 size:22x20
offset:55,0 size:45x20
offset:0,0 size:33x20
offset:110,0 size:100x70
offset:0,0 size:100x70
offset:0,0 size:44x70
)DUMP";
EXPECT_EQ(expectation, dump);
}
TEST_F(NGColumnLayoutAlgorithmTest, SpannerAsMulticol) { TEST_F(NGColumnLayoutAlgorithmTest, SpannerAsMulticol) {
SetBodyInnerHTML(R"HTML( SetBodyInnerHTML(R"HTML(
<style> <style>
......
...@@ -213,6 +213,44 @@ void FinishFragmentation(const NGConstraintSpace& space, ...@@ -213,6 +213,44 @@ void FinishFragmentation(const NGConstraintSpace& space,
builder->SetIntrinsicBlockSize(intrinsic_block_size); builder->SetIntrinsicBlockSize(intrinsic_block_size);
} }
NGBreakStatus BreakBeforeChildIfNeeded(const NGConstraintSpace& space,
NGLayoutInputNode child,
const NGLayoutResult& layout_result,
LayoutUnit fragmentainer_block_offset,
bool has_container_separation,
NGBoxFragmentBuilder* builder) {
DCHECK(space.HasBlockFragmentation());
if (has_container_separation) {
EBreakBetween break_between =
CalculateBreakBetweenValue(child, layout_result, *builder);
if (IsForcedBreakValue(space, break_between)) {
BreakBeforeChild(space, child, layout_result, fragmentainer_block_offset,
kBreakAppealPerfect, /* is_forced_break */ true,
builder);
return NGBreakStatus::kBrokeBefore;
}
}
NGBreakAppeal appeal_before = CalculateBreakAppealBefore(
space, child, layout_result, *builder, has_container_separation);
// Attempt to move past the break point, and if we can do that, also assess
// the appeal of breaking there, even if we didn't.
if (MovePastBreakpoint(space, child, layout_result,
fragmentainer_block_offset, appeal_before, builder))
return NGBreakStatus::kContinue;
// Breaking inside the child isn't appealing, and we're out of space. 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 (!AttemptSoftBreak(space, child, layout_result, fragmentainer_block_offset,
appeal_before, builder))
return NGBreakStatus::kNeedsEarlierBreak;
return NGBreakStatus::kBrokeBefore;
}
void BreakBeforeChild(const NGConstraintSpace& space, void BreakBeforeChild(const NGConstraintSpace& space,
NGLayoutInputNode child, NGLayoutInputNode child,
const NGLayoutResult& layout_result, const NGLayoutResult& layout_result,
......
...@@ -109,6 +109,30 @@ enum class NGBreakStatus { ...@@ -109,6 +109,30 @@ enum class NGBreakStatus {
kNeedsEarlierBreak kNeedsEarlierBreak
}; };
// Insert a fragmentainer break before the child if necessary. In that case, the
// previous in-flow position will be updated, we'll return |kBrokeBefore|. If we
// don't break inside, we'll consider the appeal of doing so anyway (and store
// it as the most appealing break point so far if that's the case), since we
// might have to go back and break here. Return |kContinue| if we're to continue
// laying out. If |kNeedsEarlierBreak| is returned, it means that we ran out of
// space, but shouldn't break before the child, but rather abort layout, and
// re-layout to a previously found good breakpoint. If
// |has_container_separation| is true, it means that we're at a valid
// breakpoint. We obviously prefer valid breakpoints, but sometimes we need to
// break at undesirable locations. Class A breakpoints occur between block
// siblings. Class B breakpoints between line boxes. Both these breakpoint
// classes imply that we're already past the first in-flow child in the
// container, but there's also another way of achieving container separation:
// class C breakpoints. Those occur if there's a positive gap between the
// block-start content edge of the container and the block-start margin edge of
// the first in-flow child. https://www.w3.org/TR/css-break-3/#possible-breaks
NGBreakStatus BreakBeforeChildIfNeeded(const NGConstraintSpace&,
NGLayoutInputNode child,
const NGLayoutResult&,
LayoutUnit fragmentainer_block_offset,
bool has_container_separation,
NGBoxFragmentBuilder*);
// Insert a break before the child, and propagate space shortage if needed. // Insert a break before the child, and propagate space shortage if needed.
void BreakBeforeChild(const NGConstraintSpace&, void BreakBeforeChild(const NGConstraintSpace&,
NGLayoutInputNode child, NGLayoutInputNode child,
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment