Commit 34c1c0a5 authored by Morten Stenshorne's avatar Morten Stenshorne Committed by Commit Bot

[LayoutNG] Support for forced fragmentainer breaks.

This is about implementing the "forced break" values for the CSS
properties break-after and break-before. The legacy
page-break-before:always and page-break-after:always declarations also
work, if in paged media. For multicol, on the other hand, it's more about
supporting break-before:column and break-after:column.

Forced fragmentainer breaks may only occur at class A break points
[1]. If there's a forced break-before value on a first in-flow child,
or a break-after value on a last in-flow child, the values are to be
propagated through the containing block chain, until we find a
suitable place to break. [2] If we don't find a suitable break point,
no break will be inserted. Initial and final break values inside a
block are propagated upwards via NGLayoutResult.

A few more layout tests pass. A few regress, too, among other things
because we don't handle borders at column boundaries too well yet, and
that we don't disable fragmentation inside scrollable containers.

Some of the code in this CL is taken from LayoutBox in the legacy
engine.

[1] https://www.w3.org/TR/css-break-3/#possible-breaks
[2] https://www.w3.org/TR/css-break-3/#break-between

Cq-Include-Trybots: master.tryserver.chromium.linux:linux_layout_tests_layout_ng
Change-Id: Id67f2ed2d008a6d98a7c7a43d98c345964035e7b
Reviewed-on: https://chromium-review.googlesource.com/835110
Commit-Queue: Morten Stenshorne <mstensho@chromium.org>
Reviewed-by: default avatarIan Kilpatrick <ikilpatrick@chromium.org>
Cr-Commit-Position: refs/heads/master@{#525325}
parent b0dc3da9
......@@ -2570,8 +2570,6 @@ crbug.com/591099 external/wpt/css/css-multicol/multicol-basic-004.html [ Failure
crbug.com/591099 external/wpt/css/css-multicol/multicol-basic-007.xht [ Failure ]
crbug.com/591099 external/wpt/css/css-multicol/multicol-basic-008.xht [ Failure ]
crbug.com/591099 external/wpt/css/css-multicol/multicol-br-inside-avoidcolumn-001.xht [ Failure ]
crbug.com/591099 external/wpt/css/css-multicol/multicol-break-000.xht [ Failure ]
crbug.com/591099 external/wpt/css/css-multicol/multicol-break-001.xht [ Failure ]
crbug.com/591099 external/wpt/css/css-multicol/multicol-clip-001.xht [ Failure ]
crbug.com/591099 external/wpt/css/css-multicol/multicol-clip-002.xht [ Failure ]
crbug.com/591099 external/wpt/css/css-multicol/multicol-collapsing-001.xht [ Failure ]
......@@ -5631,6 +5629,8 @@ crbug.com/591099 fast/multicol/border-radius-clipped-layer.html [ Failure ]
crbug.com/591099 fast/multicol/break-after-always-bottom-margin.html [ Failure ]
crbug.com/591099 fast/multicol/break-after-empty-set-crash.html [ Crash ]
crbug.com/591099 fast/multicol/break-before-first-line-in-first-child.html [ Failure ]
crbug.com/591099 fast/multicol/break-in-scrollable.html [ Failure ]
crbug.com/591099 fast/multicol/break-properties.html [ Failure ]
crbug.com/591099 fast/multicol/caret-range-anonymous-block-rtl.html [ Failure ]
crbug.com/591099 fast/multicol/caret-range-anonymous-block.html [ Failure ]
crbug.com/591099 fast/multicol/caret-range-outside-columns-rtl.html [ Failure ]
......@@ -5643,7 +5643,6 @@ crbug.com/591099 fast/multicol/client-rects-crossing-boundaries.html [ Failure ]
crbug.com/591099 fast/multicol/client-rects-rtl.html [ Failure ]
crbug.com/591099 fast/multicol/client-rects.html [ Failure ]
crbug.com/591099 fast/multicol/clone-block-children-inline-mismatch-crash.html [ Crash ]
crbug.com/591099 fast/multicol/column-break-with-balancing.html [ Failure ]
crbug.com/591099 fast/multicol/column-count-with-rules.html [ Failure ]
crbug.com/591099 fast/multicol/column-rules.html [ Failure ]
crbug.com/591099 fast/multicol/columns-shorthand-parsing.html [ Failure ]
......@@ -5842,7 +5841,6 @@ crbug.com/714962 fast/multicol/newmulticol/regular-block-becomes-multicol.html [
crbug.com/591099 fast/multicol/newmulticol/spanner-inside-child-crash.html [ Crash ]
crbug.com/591099 fast/multicol/newmulticol/table-cell.html [ Failure ]
crbug.com/591099 fast/multicol/newmulticol/unresolvable-percent-max-height-2.html [ Failure ]
crbug.com/591099 fast/multicol/one-column-with-break.html [ Failure ]
crbug.com/591099 fast/multicol/orphaned-line-at-exact-top-of-column.html [ Failure ]
crbug.com/591099 fast/multicol/orphans-relayout.html [ Failure ]
crbug.com/591099 fast/multicol/out-of-flow/abspos-auto-left-right.html [ Pass ]
......@@ -6838,10 +6836,10 @@ crbug.com/591099 fragmentation/abspos-after-forced-break.html [ Failure ]
crbug.com/591099 fragmentation/auto-scrollbar-shrink-to-fit.html [ Failure ]
crbug.com/591099 fragmentation/avoid-break-inside-first-child-nested.html [ Failure ]
crbug.com/591099 fragmentation/avoid-break-inside-first-child.html [ Failure ]
crbug.com/591099 fragmentation/become-fragmented-same-widths.html [ Failure ]
crbug.com/591099 fragmentation/block-after-float-first-child.html [ Failure ]
crbug.com/591099 fragmentation/block-with-float-and-1-orphaned-line.html [ Failure ]
crbug.com/591099 fragmentation/border-spacing-break-before-unbreakable-row.html [ Failure ]
crbug.com/591099 fragmentation/break-before-first-child.html [ Failure ]
crbug.com/591099 fragmentation/break-in-first-table-row-only.html [ Failure ]
crbug.com/591099 fragmentation/break-in-second-table-section.html [ Failure ]
crbug.com/591099 fragmentation/break-in-tbody-after-caption.html [ Failure ]
......
......@@ -141,7 +141,8 @@ scoped_refptr<NGLayoutResult> NGLineBoxFragmentBuilder::ToLineBoxFragment() {
std::move(fragment), oof_positioned_descendants_, positioned_floats_,
unpositioned_floats_, std::move(exclusion_space_), bfc_offset_,
end_margin_strut_,
/* intrinsic_block_size */ LayoutUnit(), NGLayoutResult::kSuccess));
/* intrinsic_block_size */ LayoutUnit(), EBreakBetween::kAuto,
EBreakBetween::kAuto, NGLayoutResult::kSuccess));
}
} // namespace blink
......@@ -369,6 +369,13 @@ scoped_refptr<NGLayoutResult> NGBlockLayoutAlgorithm::Layout() {
HandleFloat(previous_inflow_position, ToNGBlockNode(child),
ToNGBlockBreakToken(child_break_token));
} else {
// 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.
container_builder_.SetInitialBreakBefore(child.Style().BreakBefore());
bool success =
child.CreatesNewFormattingContext()
? HandleNewFormattingContext(child, child_break_token,
......@@ -663,8 +670,11 @@ bool NGBlockLayoutAlgorithm::HandleNewFormattingContext(
container_builder_.Size().inline_size, ConstraintSpace().Direction());
if (ConstraintSpace().HasBlockFragmentation() &&
BreakBeforeChild(child, physical_fragment, logical_offset.block_offset))
BreakBeforeChild(child, *layout_result, logical_offset.block_offset))
return true;
EBreakBetween break_after = JoinFragmentainerBreakValues(
layout_result->FinalBreakAfter(), child.Style().BreakAfter());
container_builder_.SetPreviousBreakAfter(break_after);
intrinsic_block_size_ =
std::max(intrinsic_block_size_,
......@@ -990,8 +1000,11 @@ bool NGBlockLayoutAlgorithm::HandleInflow(
CalculateLogicalOffset(fragment, child_data.margins, child_bfc_offset);
if (ConstraintSpace().HasBlockFragmentation() &&
BreakBeforeChild(child, physical_fragment, logical_offset.block_offset))
BreakBeforeChild(child, *layout_result, logical_offset.block_offset))
return true;
EBreakBetween break_after = JoinFragmentainerBreakValues(
layout_result->FinalBreakAfter(), child.Style().BreakAfter());
container_builder_.SetPreviousBreakAfter(break_after);
// Only modify intrinsic_block_size_ if the fragment is non-empty block.
//
......@@ -1230,10 +1243,10 @@ void NGBlockLayoutAlgorithm::FinalizeForFragmentation() {
bool NGBlockLayoutAlgorithm::BreakBeforeChild(
NGLayoutInputNode child,
const NGPhysicalFragment& physical_fragment,
const NGLayoutResult& layout_result,
LayoutUnit block_offset) {
DCHECK(ConstraintSpace().HasBlockFragmentation());
if (!ShouldBreakBeforeChild(child, physical_fragment, block_offset))
if (!ShouldBreakBeforeChild(child, layout_result, block_offset))
return false;
// The remaining part of the fragmentainer (the unusable space for child
......@@ -1250,11 +1263,14 @@ bool NGBlockLayoutAlgorithm::BreakBeforeChild(
bool NGBlockLayoutAlgorithm::ShouldBreakBeforeChild(
NGLayoutInputNode child,
const NGPhysicalFragment& physical_fragment,
const NGLayoutResult& layout_result,
LayoutUnit block_offset) const {
if (!container_builder_.BfcOffset().has_value())
return false;
const NGPhysicalFragment& physical_fragment =
*layout_result.PhysicalFragment();
// If we haven't used any space at all in the fragmentainer yet, we cannot
// break, or there'd be no progress. We'd end up creating an infinite number
// of fragmentainers without putting any content into them.
......@@ -1274,6 +1290,17 @@ bool NGBlockLayoutAlgorithm::ShouldBreakBeforeChild(
if (space_left <= LayoutUnit())
return true;
EBreakBetween break_before = JoinFragmentainerBreakValues(
child.Style().BreakBefore(), layout_result.InitialBreakBefore());
EBreakBetween break_between =
container_builder_.JoinedBreakBetweenValue(break_before);
if (IsForcedBreakValue(ConstraintSpace(), break_between)) {
// There should be a forced break before this child, and if we're not at the
// first in-flow child, just go ahead and break.
if (has_processed_first_child_)
return true;
}
const auto* token = physical_fragment.BreakToken();
if (!token || token->IsFinished())
return false;
......
......@@ -156,13 +156,13 @@ class CORE_EXPORT NGBlockLayoutAlgorithm
// Insert a fragmentainer break before the child if necessary.
// Return true if a break was inserted, false otherwise.
bool BreakBeforeChild(NGLayoutInputNode child,
const NGPhysicalFragment&,
const NGLayoutResult&,
LayoutUnit block_offset);
// Given a child fragment and the corresponding node's style, return true if
// we need to insert a fragmentainer break in front of it.
bool ShouldBreakBeforeChild(NGLayoutInputNode child,
const NGPhysicalFragment& physical_fragment,
const NGLayoutResult&,
LayoutUnit block_offset) const;
// Final adjustments before fragment creation. We need to prevent the
......
......@@ -1454,6 +1454,59 @@ TEST_F(NGColumnLayoutAlgorithmTest, BreakInsideWithBorder) {
EXPECT_EQ(expectation, dump);
}
TEST_F(NGColumnLayoutAlgorithmTest, ForcedBreaks) {
// This tests that forced breaks are honored, but only at valid class A break
// points (i.e. *between* in-flow block siblings).
SetBodyInnerHTML(R"HTML(
<style>
#parent {
columns: 3;
column-fill: auto;
column-gap: 10px;
width: 320px;
height: 100px;
}
</style>
<div id="container">
<div id="parent">
<div style="float:left; width:1px; height:1px;"></div>
<div style="break-before:column; break-after:column;">
<div style="float:left; width:1px; height:1px;"></div>
<div style="break-after:column; width:50px; height:10px;"></div>
<div style="break-before:column; width:60px; height:10px;"></div>
<div>
<div>
<div style="break-after:column; width:70px; height:10px;"></div>
</div>
</div>
<div style="width:80px; height: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:1x1
offset:0,0 size:100x100
offset:1,0 size:1x1
offset:0,0 size:50x10
offset:110,0 size:100x100
offset:0,0 size:100x100
offset:0,0 size:60x10
offset:0,10 size:100x10
offset:0,0 size:100x10
offset:0,0 size:70x10
offset:220,0 size:100x10
offset:0,0 size:100x10
offset:0,0 size:80x10
)DUMP";
EXPECT_EQ(expectation, dump);
}
TEST_F(NGColumnLayoutAlgorithmTest, MinMax) {
// The multicol container here contains two inline-blocks with a line break
// opportunity between them. We'll test what min/max values we get for the
......
......@@ -14,6 +14,7 @@
#include "core/layout/ng/ng_break_token.h"
#include "core/layout/ng/ng_exclusion_space.h"
#include "core/layout/ng/ng_fragment.h"
#include "core/layout/ng/ng_fragmentation_utils.h"
#include "core/layout/ng/ng_layout_result.h"
#include "core/layout/ng/ng_physical_box_fragment.h"
#include "core/layout/ng/ng_positioned_float.h"
......@@ -201,6 +202,11 @@ void NGFragmentBuilder::AddBaseline(NGBaselineRequest request,
baselines_.push_back(NGBaseline{request, offset});
}
EBreakBetween NGFragmentBuilder::JoinedBreakBetweenValue(
EBreakBetween break_before) const {
return JoinFragmentainerBreakValues(previous_break_after_, break_before);
}
scoped_refptr<NGLayoutResult> NGFragmentBuilder::ToBoxFragment() {
DCHECK_EQ(offsets_.size(), children_.size());
......@@ -240,17 +246,18 @@ scoped_refptr<NGLayoutResult> NGFragmentBuilder::ToBoxFragment() {
return base::AdoptRef(new NGLayoutResult(
std::move(fragment), oof_positioned_descendants_, positioned_floats,
unpositioned_floats_, std::move(exclusion_space_), bfc_offset_,
end_margin_strut_, intrinsic_block_size_, NGLayoutResult::kSuccess));
end_margin_strut_, intrinsic_block_size_, initial_break_before_,
previous_break_after_, NGLayoutResult::kSuccess));
}
scoped_refptr<NGLayoutResult> NGFragmentBuilder::Abort(
NGLayoutResult::NGLayoutResultStatus status) {
Vector<NGOutOfFlowPositionedDescendant> oof_positioned_descendants;
Vector<NGPositionedFloat> positioned_floats;
return base::AdoptRef(
new NGLayoutResult(nullptr, oof_positioned_descendants, positioned_floats,
unpositioned_floats_, nullptr, bfc_offset_,
end_margin_strut_, LayoutUnit(), status));
return base::AdoptRef(new NGLayoutResult(
nullptr, oof_positioned_descendants, positioned_floats,
unpositioned_floats_, nullptr, bfc_offset_, end_margin_strut_,
LayoutUnit(), EBreakBetween::kAuto, EBreakBetween::kAuto, status));
}
// Finds FragmentPairs that define inline containing blocks.
......
......@@ -13,6 +13,7 @@
#include "core/layout/ng/ng_container_fragment_builder.h"
#include "core/layout/ng/ng_layout_result.h"
#include "core/layout/ng/ng_out_of_flow_positioned_descendant.h"
#include "core/style/ComputedStyleConstants.h"
#include "platform/heap/Handle.h"
#include "platform/wtf/Allocator.h"
namespace blink {
......@@ -75,6 +76,19 @@ class CORE_EXPORT NGFragmentBuilder final : public NGContainerFragmentBuilder {
return *this;
}
void SetInitialBreakBefore(EBreakBetween break_before) {
initial_break_before_ = break_before;
}
void SetPreviousBreakAfter(EBreakBetween break_after) {
previous_break_after_ = break_after;
}
// Join/"collapse" the previous (stored) break-after value with the next
// break-before value, to determine how to deal with breaking between two
// in-flow siblings.
EBreakBetween JoinedBreakBetweenValue(EBreakBetween break_before) const;
// Offsets are not supposed to be set during fragment construction, so we
// do not provide a setter here.
......@@ -144,6 +158,13 @@ class CORE_EXPORT NGFragmentBuilder final : public NGContainerFragmentBuilder {
bool did_break_;
LayoutUnit used_block_size_;
// The break-before value on the initial child we cannot honor. There's no
// valid class A break point before a first child, only *between* siblings.
EBreakBetween initial_break_before_ = EBreakBetween::kAuto;
// The break-after value of the previous in-flow sibling.
EBreakBetween previous_break_after_ = EBreakBetween::kAuto;
Vector<scoped_refptr<NGBreakToken>> child_break_tokens_;
scoped_refptr<NGBreakToken> last_inline_break_token_;
......
......@@ -43,4 +43,64 @@ bool IsLastFragment(const NGPhysicalFragment& fragment) {
return !break_token || break_token->IsFinished();
}
// At a class A break point [1], the break value with the highest precedence
// wins. If the two values have the same precedence (e.g. "left" and "right"),
// the value specified on a latter object wins.
//
// [1] https://drafts.csswg.org/css-break/#possible-breaks
inline int FragmentainerBreakPrecedence(EBreakBetween break_value) {
// "auto" has the lowest priority.
// "avoid*" values win over "auto".
// "avoid-page" wins over "avoid-column".
// "avoid" wins over "avoid-page".
// Forced break values win over "avoid".
// Any forced page break value wins over "column" forced break.
// More specific break values (left, right, recto, verso) wins over generic
// "page" values.
switch (break_value) {
default:
NOTREACHED();
// fall-through
case EBreakBetween::kAuto:
return 0;
case EBreakBetween::kAvoidColumn:
return 1;
case EBreakBetween::kAvoidPage:
return 2;
case EBreakBetween::kAvoid:
return 3;
case EBreakBetween::kColumn:
return 4;
case EBreakBetween::kPage:
return 5;
case EBreakBetween::kLeft:
case EBreakBetween::kRight:
case EBreakBetween::kRecto:
case EBreakBetween::kVerso:
return 6;
}
}
EBreakBetween JoinFragmentainerBreakValues(EBreakBetween first_value,
EBreakBetween second_value) {
if (FragmentainerBreakPrecedence(second_value) >=
FragmentainerBreakPrecedence(first_value))
return second_value;
return first_value;
}
bool IsForcedBreakValue(const NGConstraintSpace& constraint_space,
EBreakBetween break_value) {
if (break_value == EBreakBetween::kColumn)
return constraint_space.BlockFragmentationType() == kFragmentColumn;
if (break_value == EBreakBetween::kLeft ||
break_value == EBreakBetween::kPage ||
break_value == EBreakBetween::kRecto ||
break_value == EBreakBetween::kRight ||
break_value == EBreakBetween::kVerso)
return constraint_space.BlockFragmentationType() == kFragmentPage;
return false;
}
} // namespace blink
......@@ -5,6 +5,7 @@
#ifndef NGFragmentationUtils_h
#define NGFragmentationUtils_h
#include "core/style/ComputedStyleConstants.h"
#include "platform/LayoutUnit.h"
namespace blink {
......@@ -24,6 +25,23 @@ bool IsFirstFragment(const NGConstraintSpace&, const NGPhysicalFragment&);
// Return true if the specified fragment is the final fragment of some node.
bool IsLastFragment(const NGPhysicalFragment&);
// Join two adjacent break values specified on break-before and/or break-
// after. avoid* values win over auto values, and forced break values win over
// avoid* values. |first_value| is specified on an element earlier in the flow
// than |second_value|. This method is used at class A break points [1], to join
// the values of the previous break-after and the next break-before, to figure
// out whether we may, must, or should not break at that point. It is also used
// when propagating break-before values from first children and break-after
// values on last children to their container.
//
// [1] https://drafts.csswg.org/css-break/#possible-breaks
EBreakBetween JoinFragmentainerBreakValues(EBreakBetween first_value,
EBreakBetween second_value);
// Return true if the specified break value has a forced break effect in the
// current fragmentation context.
bool IsForcedBreakValue(const NGConstraintSpace&, EBreakBetween);
} // namespace blink
#endif // NGFragmentationUtils_h
......@@ -20,12 +20,16 @@ NGLayoutResult::NGLayoutResult(
const WTF::Optional<NGBfcOffset> bfc_offset,
const NGMarginStrut end_margin_strut,
const LayoutUnit intrinsic_block_size,
EBreakBetween initial_break_before,
EBreakBetween final_break_after,
NGLayoutResultStatus status)
: physical_fragment_(std::move(physical_fragment)),
exclusion_space_(std::move(exclusion_space)),
bfc_offset_(bfc_offset),
end_margin_strut_(end_margin_strut),
intrinsic_block_size_(intrinsic_block_size),
initial_break_before_(initial_break_before),
final_break_after_(final_break_after),
status_(status) {
oof_positioned_descendants_.swap(oof_positioned_descendants);
positioned_floats_.swap(positioned_floats);
......@@ -47,7 +51,8 @@ scoped_refptr<NGLayoutResult> NGLayoutResult::CloneWithoutOffset() const {
return base::AdoptRef(new NGLayoutResult(
physical_fragment_->CloneWithoutOffset(), oof_positioned_descendants,
positioned_floats, unpositioned_floats, std::move(exclusion_space),
bfc_offset_, end_margin_strut_, intrinsic_block_size_, Status()));
bfc_offset_, end_margin_strut_, intrinsic_block_size_,
initial_break_before_, final_break_after_, Status()));
}
} // namespace blink
......@@ -11,6 +11,7 @@
#include "core/layout/ng/geometry/ng_margin_strut.h"
#include "core/layout/ng/ng_out_of_flow_positioned_descendant.h"
#include "core/layout/ng/ng_physical_fragment.h"
#include "core/style/ComputedStyleConstants.h"
#include "platform/wtf/Vector.h"
namespace blink {
......@@ -86,6 +87,14 @@ class CORE_EXPORT NGLayoutResult : public RefCounted<NGLayoutResult> {
return intrinsic_block_size_;
}
// The break-before value on the first child needs to be propagated to the
// container, in search of a valid class A break point.
EBreakBetween InitialBreakBefore() const { return initial_break_before_; }
// The break-after value on the last child needs to be propagated to the
// container, in search of a valid class A break point.
EBreakBetween FinalBreakAfter() const { return final_break_after_; }
scoped_refptr<NGLayoutResult> CloneWithoutOffset() const;
private:
......@@ -102,6 +111,8 @@ class CORE_EXPORT NGLayoutResult : public RefCounted<NGLayoutResult> {
const WTF::Optional<NGBfcOffset> bfc_offset,
const NGMarginStrut end_margin_strut,
const LayoutUnit intrinsic_block_size,
EBreakBetween initial_break_before,
EBreakBetween final_break_after,
NGLayoutResultStatus status);
scoped_refptr<NGPhysicalFragment> physical_fragment_;
......@@ -115,6 +126,9 @@ class CORE_EXPORT NGLayoutResult : public RefCounted<NGLayoutResult> {
const NGMarginStrut end_margin_strut_;
const LayoutUnit intrinsic_block_size_;
EBreakBetween initial_break_before_;
EBreakBetween final_break_after_;
unsigned status_ : 1;
};
......
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