blob: c616e80ace2afb1cf18a3aae129bec7e5b18b257 [file] [log] [blame]
// Copyright 2017 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_column_layout_algorithm.h"
#include <algorithm>
#include "third_party/blink/renderer/core/layout/geometry/logical_size.h"
#include "third_party/blink/renderer/core/layout/geometry/writing_mode_converter.h"
#include "third_party/blink/renderer/core/layout/ng/geometry/ng_fragment_geometry.h"
#include "third_party/blink/renderer/core/layout/ng/geometry/ng_margin_strut.h"
#include "third_party/blink/renderer/core/layout/ng/ng_block_layout_algorithm.h"
#include "third_party/blink/renderer/core/layout/ng/ng_box_fragment.h"
#include "third_party/blink/renderer/core/layout/ng/ng_constraint_space_builder.h"
#include "third_party/blink/renderer/core/layout/ng/ng_fragmentation_utils.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/style/computed_style.h"
namespace blink {
namespace {
LayoutUnit CalculateColumnContentBlockSize(
const NGPhysicalContainerFragment& fragment,
WritingDirectionMode writing_direction) {
WritingModeConverter converter(writing_direction, fragment.Size());
// Note that what we're doing here is almost the same as what we do when
// calculating overflow, with at least one important difference: If the
// inline-size of a fragment is 0, the overflow rectangle becomes empty, even
// if the fragment's block-size is non-zero. This is correct for overflow
// handling, but it would be wrong for column balancing.
LayoutUnit total_size;
for (const auto& child : fragment.Children()) {
LayoutUnit size = converter.ToLogical(child->Size()).block_size;
LayoutUnit offset =
converter.ToLogical(child.offset, child->Size()).block_offset;
// TODO(mstensho): Need to detect whether we're actually clipping in the
// block direction. The combination of overflow-x:clip and
// overflow-y:visible should enter children here.
if (child->IsContainer() && !child->HasNonVisibleOverflow()) {
LayoutUnit children_size = CalculateColumnContentBlockSize(
To<NGPhysicalContainerFragment>(*child), writing_direction);
if (size < children_size)
size = children_size;
}
LayoutUnit block_end = offset + size;
if (total_size < block_end)
total_size = block_end;
}
return total_size;
}
// An itinerary of multicol container parts to walk separately for layout. A
// part is either a chunk of regular column content, or a column spanner.
class MulticolPartWalker {
STACK_ALLOCATED();
public:
// What to lay out or process next.
struct Entry {
STACK_ALLOCATED();
public:
Entry() = default;
Entry(const NGBlockBreakToken* token, NGBlockNode spanner)
: break_token(token), spanner(spanner) {}
// The incoming break token for the content to process, or null if we're at
// the start.
const NGBlockBreakToken* break_token = nullptr;
// The column spanner node to process, or null if we're dealing with regular
// column content.
NGBlockNode spanner = nullptr;
};
MulticolPartWalker(NGBlockNode multicol_container,
const NGBlockBreakToken* break_token)
: multicol_container_(multicol_container),
parent_break_token_(break_token),
child_token_idx_(0) {
UpdateCurrent();
// The first entry in the first multicol fragment may be empty (that just
// means that we haven't started yet), but if this happens anywhere else, it
// means that we're finished. Nothing inside this multicol container left to
// process.
if (IsResumingLayout(parent_break_token_) && !current_.break_token &&
parent_break_token_->HasSeenAllChildren())
is_finished_ = true;
}
Entry Current() const {
DCHECK(!is_finished_);
return current_;
}
bool IsFinished() const { return is_finished_; }
// Move to the next part.
void Next();
// Move over to the specified spanner, and take it from there.
void MoveToSpanner(NGBlockNode spanner,
const NGBlockBreakToken* next_column_token);
// Push a break token for the column content to resume at.
void AddNextColumnBreakToken(const NGBlockBreakToken& next_column_token);
private:
void MoveToNext();
void UpdateCurrent();
Entry current_;
NGBlockNode spanner_ = nullptr;
NGBlockNode multicol_container_;
const NGBlockBreakToken* parent_break_token_;
scoped_refptr<const NGBlockBreakToken> next_column_token_;
// An index into parent_break_token_'s ChildBreakTokens() vector. Used for
// keeping track of the next child break token to inspect.
wtf_size_t child_token_idx_;
bool is_finished_ = false;
};
void MulticolPartWalker::Next() {
if (is_finished_)
return;
MoveToNext();
if (!is_finished_)
UpdateCurrent();
}
void MulticolPartWalker::MoveToSpanner(
NGBlockNode spanner,
const NGBlockBreakToken* next_column_token) {
*this = MulticolPartWalker(multicol_container_, nullptr);
DCHECK(spanner.IsColumnSpanAll());
spanner_ = spanner;
next_column_token_ = next_column_token;
UpdateCurrent();
}
void MulticolPartWalker::AddNextColumnBreakToken(
const NGBlockBreakToken& next_column_token) {
*this = MulticolPartWalker(multicol_container_, nullptr);
next_column_token_ = &next_column_token;
UpdateCurrent();
}
void MulticolPartWalker::UpdateCurrent() {
DCHECK(!is_finished_);
if (parent_break_token_) {
const auto& child_break_tokens = parent_break_token_->ChildBreakTokens();
if (child_token_idx_ < child_break_tokens.size()) {
const auto* child_break_token =
To<NGBlockBreakToken>(child_break_tokens[child_token_idx_]);
if (child_break_token->InputNode() == multicol_container_) {
current_.spanner = nullptr;
} else {
current_.spanner = To<NGBlockNode>(child_break_token->InputNode());
DCHECK(current_.spanner.IsColumnSpanAll());
}
current_.break_token = child_break_token;
return;
}
}
if (spanner_) {
current_ = Entry(/* break_token */ nullptr, spanner_);
return;
}
if (next_column_token_) {
current_ = Entry(next_column_token_.get(), /* spanner */ nullptr);
return;
}
// The current entry is empty. That's only the case when we're at the very
// start of the multicol container, or if we're past all children.
DCHECK(!is_finished_);
DCHECK(!current_.spanner);
DCHECK(!current_.break_token);
}
void MulticolPartWalker::MoveToNext() {
if (parent_break_token_) {
const auto& child_break_tokens = parent_break_token_->ChildBreakTokens();
if (child_token_idx_ < child_break_tokens.size()) {
child_token_idx_++;
// If we have more incoming break tokens, we'll use that.
if (child_token_idx_ < child_break_tokens.size())
return;
// We just ran out of break tokens. Fall through.
}
}
if (spanner_) {
NGLayoutInputNode next = spanner_.NextSibling();
// Otherwise, if there's a next spanner, we'll use that.
if (next && next.IsColumnSpanAll()) {
spanner_ = To<NGBlockNode>(next);
return;
}
spanner_ = nullptr;
// Otherwise, if we have column content to resume at, use that.
if (next_column_token_)
return;
}
// Otherwise, we're done.
is_finished_ = true;
}
} // namespace
NGColumnLayoutAlgorithm::NGColumnLayoutAlgorithm(
const NGLayoutAlgorithmParams& params)
: NGLayoutAlgorithm(params), early_break_(params.early_break) {}
scoped_refptr<const NGLayoutResult> NGColumnLayoutAlgorithm::Layout() {
const LogicalSize border_box_size = container_builder_.InitialBorderBoxSize();
// TODO(mstensho): This isn't the content-box size, as
// |BorderScrollbarPadding()| has been adjusted for fragmentation. Verify
// that this is the correct size.
column_block_size_ =
ShrinkLogicalSize(border_box_size, BorderScrollbarPadding()).block_size;
DCHECK_GE(ChildAvailableSize().inline_size, LayoutUnit());
column_inline_size_ =
ResolveUsedColumnInlineSize(ChildAvailableSize().inline_size, Style());
column_inline_progression_ =
column_inline_size_ +
ResolveUsedColumnGap(ChildAvailableSize().inline_size, Style());
used_column_count_ =
ResolveUsedColumnCount(ChildAvailableSize().inline_size, Style());
// If we know the block-size of the fragmentainers in an outer fragmentation
// context (if any), our columns may be constrained by that, meaning that we
// may have to fragment earlier than what we would have otherwise, and, if
// that's the case, that we may also not create overflowing columns (in the
// inline axis), but rather finish the row and resume in the next row in the
// next outer fragmentainer. Note that it is possible to be nested inside a
// fragmentation context that doesn't know the block-size of its
// fragmentainers. This would be in the first layout pass of an outer multicol
// container, before any tentative column block-size has been calculated.
is_constrained_by_outer_fragmentation_context_ =
ConstraintSpace().HasKnownFragmentainerBlockSize();
container_builder_.SetIsBlockFragmentationContextRoot();
intrinsic_block_size_ = BorderScrollbarPadding().block_start;
NGBreakStatus break_status = LayoutChildren();
if (break_status == NGBreakStatus::kNeedsEarlierBreak) {
// 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();
} else if (break_status == NGBreakStatus::kBrokeBefore) {
// If we want to break before, make sure that we're actually at the start.
DCHECK(!IsResumingLayout(BreakToken()));
return container_builder_.Abort(NGLayoutResult::kOutOfFragmentainerSpace);
}
intrinsic_block_size_ += BorderScrollbarPadding().block_end;
// Figure out how much space we've already been able to process in previous
// fragments, if this multicol container participates in an outer
// fragmentation context.
LayoutUnit previously_consumed_block_size;
if (const auto* token = BreakToken())
previously_consumed_block_size = token->ConsumedBlockSize();
// Save the unconstrained intrinsic size on the builder before clamping it.
container_builder_.SetOverflowBlockSize(intrinsic_block_size_);
intrinsic_block_size_ =
ClampIntrinsicBlockSize(ConstraintSpace(), Node(),
BorderScrollbarPadding(), intrinsic_block_size_);
LayoutUnit block_size = ComputeBlockSizeForFragment(
ConstraintSpace(), Style(), BorderPadding(),
previously_consumed_block_size + intrinsic_block_size_,
border_box_size.inline_size, Node().ShouldBeConsideredAsReplaced());
container_builder_.SetFragmentsTotalBlockSize(block_size);
container_builder_.SetIntrinsicBlockSize(intrinsic_block_size_);
container_builder_.SetBlockOffsetForAdditionalColumns(
CurrentContentBlockOffset());
if (ConstraintSpace().HasBlockFragmentation()) {
// In addition to establishing one, we're nested inside another
// fragmentation context.
FinishFragmentation(Node(), ConstraintSpace(), BorderPadding().block_end,
FragmentainerSpaceAtBfcStart(ConstraintSpace()),
&container_builder_);
// OOF positioned elements inside a nested fragmentation context are laid
// out at the outermost context. If this multicol has OOF positioned
// elements pending layout, store its node for later use.
if (container_builder_.HasOutOfFlowFragmentainerDescendants()) {
container_builder_.AddMulticolWithPendingOOFs(Node());
}
}
NGOutOfFlowLayoutPart(Node(), ConstraintSpace(), &container_builder_).Run();
return container_builder_.ToBoxFragment();
}
MinMaxSizesResult NGColumnLayoutAlgorithm::ComputeMinMaxSizes(
const MinMaxSizesInput& input) const {
// First calculate the min/max sizes of columns.
NGConstraintSpace space = CreateConstraintSpaceForMinMax();
NGFragmentGeometry fragment_geometry =
CalculateInitialMinMaxFragmentGeometry(space, Node());
NGBlockLayoutAlgorithm algorithm({Node(), fragment_geometry, space});
MinMaxSizesResult result = algorithm.ComputeMinMaxSizes(input);
// If column-width is non-auto, pick the larger of that and intrinsic column
// width.
if (!Style().HasAutoColumnWidth()) {
result.sizes.min_size =
std::max(result.sizes.min_size, LayoutUnit(Style().ColumnWidth()));
result.sizes.max_size =
std::max(result.sizes.max_size, result.sizes.min_size);
}
// Now convert those column min/max values to multicol container min/max
// values. We typically have multiple columns and also gaps between them.
int column_count = Style().ColumnCount();
DCHECK_GE(column_count, 1);
result.sizes.min_size *= column_count;
result.sizes.max_size *= column_count;
LayoutUnit column_gap = ResolveUsedColumnGap(LayoutUnit(), Style());
result.sizes += column_gap * (column_count - 1);
// TODO(mstensho): Need to include spanners.
result.sizes += BorderScrollbarPadding().InlineSum();
return result;
}
NGBreakStatus NGColumnLayoutAlgorithm::LayoutChildren() {
NGMarginStrut margin_strut;
MulticolPartWalker walker(Node(), BreakToken());
while (!walker.IsFinished()) {
auto entry = walker.Current();
const auto* child_break_token = To<NGBlockBreakToken>(entry.break_token);
// If this is regular column content (i.e. not a spanner), or we're at the
// very start, perform column layout. If we're at the very start, and even
// if the child is a spanner (which means that we won't be able to lay out
// any column content at all), we still need to enter here, because that's
// how we create a break token for the column content to resume at. With no
// break token, we wouldn't be able to resume layout after the any initial
// spanners.
if (!entry.spanner) {
scoped_refptr<const NGLayoutResult> result =
LayoutRow(child_break_token, &margin_strut);
if (!result) {
// Not enough outer fragmentainer space to produce any columns at all.
if (intrinsic_block_size_) {
// We have preceding initial border/padding, or a column spanner
// (possibly preceded by other spanners or even column content). So we
// need to break inside the multicol container. Stop walking the
// children, but "continue" layout, so that we produce a
// fragment. Note that we normally don't want to break right after
// initial border/padding, but will do so as a last resort. It's up to
// our containing block to decide what's best. In case there is no
// break token inside, we need to manually mark that we broke.
container_builder_.SetDidBreakSelf();
break;
}
// Otherwise we have nothing here, and need to break before the multicol
// container. No fragment will be produced.
DCHECK(!BreakToken());
return NGBreakStatus::kBrokeBefore;
}
walker.Next();
const auto* next_column_token =
To<NGBlockBreakToken>(result->PhysicalFragment().BreakToken());
if (NGBlockNode spanner_node = result->ColumnSpanner()) {
// We found a spanner, and if there's column content to resume at after
// it, |next_column_token| will be set. Move the walker to the
// spanner. We'll now walk that spanner and any sibling spanners, before
// resuming at |next_column_token|.
walker.MoveToSpanner(spanner_node, next_column_token);
continue;
}
// If we didn't find a spanner, it either means that we're through
// everything, or that column layout needs to continue from the next outer
// fragmentainer.
if (next_column_token)
walker.AddNextColumnBreakToken(*next_column_token);
break;
}
// Attempt to lay out one column spanner.
NGBlockNode spanner_node = entry.spanner;
if (early_break_) {
// 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)
break;
}
NGBreakStatus break_status =
LayoutSpanner(spanner_node, child_break_token, &margin_strut);
walker.Next();
if (break_status == NGBreakStatus::kNeedsEarlierBreak)
return break_status;
if (break_status == NGBreakStatus::kBrokeBefore ||
container_builder_.HasInflowChildBreakInside()) {
break;
}
}
if (!walker.IsFinished() || container_builder_.HasInflowChildBreakInside()) {
// We broke in the main flow. Let this multicol container take up any
// remaining space.
intrinsic_block_size_ = FragmentainerSpaceAtBfcStart(ConstraintSpace());
// Go through any remaining parts that we didn't get to, and push them as
// break tokens for the next (outer) fragmentainer to handle.
for (; !walker.IsFinished(); walker.Next()) {
auto entry = walker.Current();
if (entry.break_token) {
// Copy unhandled incoming break tokens, for the next (outer)
// fragmentainer.
container_builder_.AddBreakToken(entry.break_token);
} else if (entry.spanner) {
// Create break tokens for the spanners that were discovered (but not
// handled) while laying out this (outer) fragmentainer, so that they
// get resumed in the next one (or pushed again, if it won't fit there
// either).
container_builder_.AddBreakBeforeChild(
entry.spanner, kBreakAppealPerfect, /* is_forced_break */ false);
}
}
} else {
// We've gone through all the content. This doesn't necessarily mean that
// we're done fragmenting, since the multicol container may be taller than
// what the content requires, which means that we might create more
// (childless) fragments, if we're nested inside another fragmentation
// context. In that case we must make sure to skip the contents when
// resuming.
container_builder_.SetHasSeenAllChildren();
// TODO(mstensho): Truncate the child margin if it overflows the
// fragmentainer, by using AdjustedMarginAfterFinalChildFragment().
intrinsic_block_size_ += margin_strut.Sum();
}
return NGBreakStatus::kContinue;
}
scoped_refptr<const NGLayoutResult> NGColumnLayoutAlgorithm::LayoutRow(
const NGBlockBreakToken* next_column_token,
NGMarginStrut* margin_strut) {
LogicalSize column_size(column_inline_size_, column_block_size_);
// We're adding a row. Incorporate the trailing margin from any preceding
// column spanner into the layout position.
intrinsic_block_size_ += margin_strut->Sum();
*margin_strut = NGMarginStrut();
// If block-size is non-auto, subtract the space for content we've consumed in
// previous fragments. This is necessary when we're nested inside another
// fragmentation context.
if (column_size.block_size != kIndefiniteSize) {
if (BreakToken() && is_constrained_by_outer_fragmentation_context_)
column_size.block_size -= BreakToken()->ConsumedBlockSize();
// Subtract the space already taken in the current fragment (spanners and
// earlier column rows).
column_size.block_size -= CurrentContentBlockOffset();
column_size.block_size = column_size.block_size.ClampNegativeToZero();
}
bool may_resume_in_next_outer_fragmentainer = false;
bool zero_outer_space_left = false;
LayoutUnit available_outer_space = kIndefiniteSize;
if (is_constrained_by_outer_fragmentation_context_) {
available_outer_space =
FragmentainerSpaceAtBfcStart(ConstraintSpace()) - intrinsic_block_size_;
if (available_outer_space <= LayoutUnit()) {
if (available_outer_space < LayoutUnit()) {
// We're past the end of the outer fragmentainer (typically due to a
// margin). Nothing will fit here, not even zero-size content.
return nullptr;
}
// We are out of space, but we're exactly at the end of the outer
// fragmentainer. If none of our contents take up space, we're going to
// fit, otherwise not. Lay out and find out.
zero_outer_space_left = true;
}
// Determine if we should resume layout in the next outer fragmentation
// context if we run out of space in the current one. This is always the
// thing to do except when block-size is non-auto and short enough to fit in
// the current outer fragmentainer. In such cases we'll allow inner columns
// to overflow its outer fragmentainer (since the inner multicol is too
// short to reach the outer fragmentation line).
if (column_size.block_size == kIndefiniteSize ||
column_size.block_size > available_outer_space)
may_resume_in_next_outer_fragmentainer = true;
}
// We balance if block-size is unconstrained, or when we're explicitly told
// to. Note that the block-size may be constrained by outer fragmentation
// contexts, not just by a block-size specified on this multicol container.
bool balance_columns = Style().GetColumnFill() == EColumnFill::kBalance ||
(column_size.block_size == kIndefiniteSize &&
!is_constrained_by_outer_fragmentation_context_);
if (balance_columns) {
column_size.block_size =
CalculateBalancedColumnBlockSize(column_size, next_column_token);
} else if (available_outer_space != kIndefiniteSize) {
// Finally, resolve any remaining auto block-size, and make sure that we
// don't take up more space than there's room for in the outer fragmentation
// context.
if (column_size.block_size > available_outer_space ||
column_size.block_size == kIndefiniteSize)
column_size.block_size = available_outer_space;
}
DCHECK_GE(column_size.block_size, LayoutUnit());
// New column fragments won't be added to the fragment builder right away,
// since we may need to delete them and try again with a different block-size
// (colum balancing). Keep them in this list, and add them to the fragment
// builder when we have the final column fragments. Or clear the list and
// retry otherwise.
struct ResultWithOffset {
scoped_refptr<const NGLayoutResult> result;
LogicalOffset offset;
ResultWithOffset(scoped_refptr<const NGLayoutResult> result,
LogicalOffset offset)
: result(result), offset(offset) {}
const NGPhysicalBoxFragment& Fragment() const {
return To<NGPhysicalBoxFragment>(result->PhysicalFragment());
}
};
Vector<ResultWithOffset, 16> new_columns;
scoped_refptr<const NGLayoutResult> result;
do {
scoped_refptr<const NGBlockBreakToken> column_break_token =
next_column_token;
bool allow_discard_start_margin =
column_break_token && !column_break_token->IsCausedByColumnSpanner();
bool has_violating_break = false;
LayoutUnit column_inline_offset(BorderScrollbarPadding().inline_start);
int actual_column_count = 0;
int forced_break_count = 0;
// Each column should calculate their own minimal space shortage. Find the
// lowest value of those. This will serve as the column stretch amount, if
// we determine that stretching them is necessary and possible (column
// balancing).
LayoutUnit minimal_space_shortage(LayoutUnit::Max());
do {
// Lay out one column. Each column will become a fragment.
NGConstraintSpace child_space = CreateConstraintSpaceForColumns(
ConstraintSpace(), column_size, ColumnPercentageResolutionSize(),
allow_discard_start_margin, balance_columns);
NGFragmentGeometry fragment_geometry =
CalculateInitialFragmentGeometry(child_space, Node());
NGBlockLayoutAlgorithm child_algorithm(
{Node(), fragment_geometry, child_space, column_break_token.get()});
child_algorithm.SetBoxType(NGPhysicalFragment::kColumnBox);
result = child_algorithm.Layout();
const auto& column = result->PhysicalFragment();
// Add the new column fragment to the list, but don't commit anything to
// the fragment builder until we know whether these are the final columns.
LogicalOffset logical_offset(column_inline_offset, intrinsic_block_size_);
new_columns.emplace_back(result, logical_offset);
LayoutUnit space_shortage = result->MinimalSpaceShortage();
if (space_shortage > LayoutUnit()) {
minimal_space_shortage =
std::min(minimal_space_shortage, space_shortage);
}
actual_column_count++;
if (result->HasForcedBreak())
forced_break_count++;
has_violating_break |= result->HasViolatingBreak();
column_inline_offset += column_inline_progression_;
if (result->ColumnSpanner())
break;
column_break_token = To<NGBlockBreakToken>(column.BreakToken());
// If we're participating in an outer fragmentation context, we'll only
// allow as many columns as the used value of column-count, so that we
// don't overflow in the inline direction. There's one important
// exception: If we have determined that this is going to be the last
// fragment for this multicol container in the outer fragmentation
// context, we'll just allow as many columns as needed (and let them
// overflow in the inline direction, if necessary). We're not going to
// progress into a next outer fragmentainer if the (remaining part of the)
// multicol container fits block-wise in the current outer fragmentainer.
if (ConstraintSpace().HasBlockFragmentation() && column_break_token &&
actual_column_count >= used_column_count_ &&
may_resume_in_next_outer_fragmentainer) {
// We cannot keep any of this if we have zero space left. Then we need
// to resume in the next outer fragmentainer.
if (zero_outer_space_left)
return nullptr;
container_builder_.SetBreakAppeal(kBreakAppealPerfect);
break;
}
allow_discard_start_margin = true;
} while (column_break_token);
if (!balance_columns) {
if (result->ColumnSpanner()) {
// We always have to balance columns preceding a spanner, so if we
// didn't do that initially, switch over to column balancing mode now,
// and lay out again.
balance_columns = true;
new_columns.clear();
column_size.block_size =
CalculateBalancedColumnBlockSize(column_size, next_column_token);
continue;
}
// Balancing not enabled. We're done.
break;
}
// We're balancing columns. Check if the column block-size that we laid out
// with was satisfactory. If not, stretch and retry, if possible.
//
// If we didn't break at any undesirable location and actual column count
// wasn't larger than what we have room for, we're done IF we're also out of
// content (no break token; in nested multicol situations there are cases
// where we only allow as many columns as we have room for, as additional
// columns normally need to continue in the next outer fragmentainer). If we
// have made the columns tall enough to bump into a spanner, it also means
// we need to stop to lay out the spanner(s), and resume column layout
// afterwards.
if (!has_violating_break && actual_column_count <= used_column_count_ &&
(!column_break_token || result->ColumnSpanner()))
break;
// We're in a situation where we'd like to stretch the columns, but then we
// need to know the stretch amount (minimal space shortage).
if (minimal_space_shortage == LayoutUnit::Max())
break;
// We also need at least one soft break opportunity. If forced breaks cause
// too many breaks, there's no stretch amount that could prevent the columns
// from overflowing.
if (actual_column_count <= forced_break_count + 1)
break;
LayoutUnit new_column_block_size =
StretchColumnBlockSize(minimal_space_shortage, column_size.block_size);
// Give up if we cannot get taller columns. The multicol container may have
// a specified block-size preventing taller columns, for instance.
DCHECK_GE(new_column_block_size, column_size.block_size);
if (new_column_block_size <= column_size.block_size) {
if (ConstraintSpace().IsInsideBalancedColumns()) {
// If we're doing nested column balancing, propagate any space shortage
// to the outer multicol container, so that the outer multicol container
// can attempt to stretch, so that this inner one may fit as well.
if (!container_builder_.IsInitialColumnBalancingPass())
container_builder_.PropagateSpaceShortage(minimal_space_shortage);
}
break;
}
// Remove column fragments and re-attempt layout with taller columns.
new_columns.clear();
column_size.block_size = new_column_block_size;
} while (true);
// If we just have one empty fragmentainer, we need to keep the trailing
// margin from any previous column spanner, and also make sure that we don't
// incorrectly consider this to be a class A breakpoint. A fragmentainer may
// end up empty if there's no in-flow content at all inside the multicol
// container, or if the multicol container starts with a spanner.
bool is_empty =
new_columns.size() == 1 && new_columns[0].Fragment().Children().empty();
if (!is_empty) {
has_processed_first_child_ = true;
container_builder_.SetPreviousBreakAfter(EBreakBetween::kAuto);
if (!has_processed_first_column_) {
has_processed_first_column_ = true;
// According to the spec, we should only look for a baseline in the first
// column.
const auto& first_column =
To<NGPhysicalBoxFragment>(new_columns[0].Fragment());
PropagateBaselineFromChild(first_column, intrinsic_block_size_);
}
}
intrinsic_block_size_ += column_size.block_size;
// Commit all column fragments to the fragment builder.
const NGBlockBreakToken* incoming_column_token = next_column_token;
for (auto result_with_offset : new_columns) {
const NGPhysicalBoxFragment& fragment = result_with_offset.Fragment();
container_builder_.AddChild(fragment, result_with_offset.offset);
Node().AddColumnResult(result_with_offset.result, incoming_column_token);
incoming_column_token = To<NGBlockBreakToken>(fragment.BreakToken());
}
return result;
}
NGBreakStatus NGColumnLayoutAlgorithm::LayoutSpanner(
NGBlockNode spanner_node,
const NGBlockBreakToken* break_token,
NGMarginStrut* margin_strut) {
const ComputedStyle& spanner_style = spanner_node.Style();
NGBoxStrut margins =
ComputeMarginsFor(spanner_style, ChildAvailableSize().inline_size,
ConstraintSpace().GetWritingDirection());
AdjustMarginsForFragmentation(break_token, &margins);
// Collapse the block-start margin of this spanner with the block-end margin
// of an immediately preceding spanner, if any.
margin_strut->Append(margins.block_start, /* is_quirky */ false);
LayoutUnit block_offset = intrinsic_block_size_ + margin_strut->Sum();
auto spanner_space =
CreateConstraintSpaceForSpanner(spanner_node, block_offset);
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;
}
}
const auto& spanner_fragment =
To<NGPhysicalBoxFragment>(result->PhysicalFragment());
NGFragment logical_fragment(ConstraintSpace().GetWritingDirection(),
spanner_fragment);
ResolveInlineMargins(spanner_style, Style(), ChildAvailableSize().inline_size,
logical_fragment.InlineSize(), &margins);
LogicalOffset offset(
BorderScrollbarPadding().inline_start + margins.inline_start,
block_offset);
container_builder_.AddResult(*result, offset);
// According to the spec, the first spanner that has a baseline contributes
// with its baseline to the multicol container. This is in contrast to column
// content, where only the first column may contribute with a baseline.
PropagateBaselineFromChild(spanner_fragment, offset.block_offset);
*margin_strut = NGMarginStrut();
margin_strut->Append(margins.block_end, /* is_quirky */ false);
intrinsic_block_size_ = offset.block_offset + logical_fragment.BlockSize();
has_processed_first_child_ = true;
EBreakBetween break_after = JoinFragmentainerBreakValues(
result->FinalBreakAfter(), spanner_node.Style().BreakAfter());
container_builder_.SetPreviousBreakAfter(break_after);
return NGBreakStatus::kContinue;
}
void NGColumnLayoutAlgorithm::PropagateBaselineFromChild(
const NGPhysicalBoxFragment& child,
LayoutUnit block_offset) {
// Bail if a baseline was already found.
if (container_builder_.Baseline())
return;
// According to the spec, multicol containers have no "last baseline set", so,
// unless we're looking for a "first baseline set", we have no work to do.
if (ConstraintSpace().BaselineAlgorithmType() !=
NGBaselineAlgorithmType::kFirstLine)
return;
NGBoxFragment logical_fragment(ConstraintSpace().GetWritingDirection(),
child);
if (auto baseline = logical_fragment.FirstBaseline())
container_builder_.SetBaseline(block_offset + *baseline);
}
LayoutUnit NGColumnLayoutAlgorithm::CalculateBalancedColumnBlockSize(
const LogicalSize& column_size,
const NGBlockBreakToken* child_break_token) {
// To calculate a balanced column size for one row of columns, we need to
// figure out how tall our content is. To do that we need to lay out. Create a
// special constraint space for column balancing, without allowing soft
// breaks. It will make us lay out all the multicol content as one single tall
// strip (unless there are forced breaks). When we're done with this layout
// pass, we can examine the result and calculate an ideal column block-size.
NGConstraintSpace space = CreateConstraintSpaceForBalancing(column_size);
NGFragmentGeometry fragment_geometry =
CalculateInitialFragmentGeometry(space, Node());
// A run of content without explicit (forced) breaks; i.e. the content portion
// between two explicit breaks, between fragmentation context start and an
// explicit break, between an explicit break and fragmentation context end,
// or, in cases when there are no explicit breaks at all: between
// fragmentation context start and end. We need to know where the explicit
// breaks are, in order to figure out where the implicit breaks will end up,
// so that we get the columns properly balanced. A content run starts out as
// representing one single column, and we'll add as many additional implicit
// breaks as needed into the content runs that are the tallest ones
// (ColumnBlockSize()).
struct ContentRun {
ContentRun(LayoutUnit content_block_size)
: content_block_size(content_block_size) {}
// Return the column block-size that this content run would require,
// considering the implicit breaks we have assumed so far.
LayoutUnit ColumnBlockSize() const {
// Some extra care is required for the division here. We want the
// resulting LayoutUnit value to be large enough to prevent overflowing
// columns. Use floating point to get higher precision than
// LayoutUnit. Then convert it to a LayoutUnit, but round it up to the
// nearest value that LayoutUnit is able to represent.
return LayoutUnit::FromFloatCeil(
float(content_block_size) / float(implicit_breaks_assumed_count + 1));
}
LayoutUnit content_block_size;
// The number of implicit breaks assumed to exist in this content run.
int implicit_breaks_assumed_count = 0;
};
class ContentRuns : public Vector<ContentRun, 1> {
public:
wtf_size_t IndexWithTallestColumns() const {
DCHECK_GT(size(), 0u);
wtf_size_t index = 0;
LayoutUnit largest_block_size = LayoutUnit::Min();
for (size_t i = 0; i < size(); i++) {
const ContentRun& run = at(i);
LayoutUnit block_size = run.ColumnBlockSize();
if (largest_block_size < block_size) {
largest_block_size = block_size;
index = i;
}
}
return index;
}
// When we have "inserted" (assumed) enough implicit column breaks, this
// method returns the block-size of the tallest column.
LayoutUnit TallestColumnBlockSize() const {
return at(IndexWithTallestColumns()).ColumnBlockSize();
}
};
// First split into content runs at explicit (forced) breaks.
ContentRuns content_runs;
scoped_refptr<const NGBlockBreakToken> break_token = child_break_token;
tallest_unbreakable_block_size_ = LayoutUnit();
do {
NGBlockLayoutAlgorithm balancing_algorithm(
{Node(), fragment_geometry, space, break_token.get()});
balancing_algorithm.SetBoxType(NGPhysicalFragment::kColumnBox);
scoped_refptr<const NGLayoutResult> result = balancing_algorithm.Layout();
// This algorithm should never abort.
DCHECK_EQ(result->Status(), NGLayoutResult::kSuccess);
const NGPhysicalBoxFragment& fragment =
To<NGPhysicalBoxFragment>(result->PhysicalFragment());
LayoutUnit column_block_size =
CalculateColumnContentBlockSize(fragment, space.GetWritingDirection());
// Encompass the block-size of the (single-strip column) fragment, to
// account for any trailing margins. We let them affect the column
// block-size, for compatibility reasons, if nothing else. The initial
// column balancing pass (i.e. here) is our opportunity to do that fairly
// easily. But note that this doesn't guarantee that no margins will ever
// get truncated. To avoid that we'd need to add some sort of mechanism that
// is invoked in *every* column balancing layout pass, where we'd
// essentially have to treat every margin as unbreakable (which kind of
// sounds both bad and difficult).
//
// We might want to revisit this approach, if it's worth it: Maybe it's
// better to not make any room at all for margins that might end up getting
// truncated. After all, they don't really require any space, so what we're
// doing currently might be seen as unnecessary (and slightly unpredictable)
// column over-stretching.
NGFragment logical_fragment(ConstraintSpace().GetWritingDirection(),
fragment);
column_block_size =
std::max(column_block_size, logical_fragment.BlockSize());
content_runs.emplace_back(column_block_size);
tallest_unbreakable_block_size_ = std::max(
tallest_unbreakable_block_size_, result->TallestUnbreakableBlockSize());
// Stop when we reach a spanner. That's where this row of columns will end.
if (result->ColumnSpanner())
break;
break_token = To<NGBlockBreakToken>(fragment.BreakToken());
} while (break_token);
// Then distribute as many implicit breaks into the content runs as we need.
int used_column_count =
ResolveUsedColumnCount(ChildAvailableSize().inline_size, Style());
for (int columns_found = content_runs.size();
columns_found < used_column_count; columns_found++) {
// The tallest content run (with all assumed implicit breaks added so far
// taken into account) is where we assume the next implicit break.
wtf_size_t index = content_runs.IndexWithTallestColumns();
content_runs[index].implicit_breaks_assumed_count++;
}
if (ConstraintSpace().IsInitialColumnBalancingPass()) {
// Nested column balancing. Our outer fragmentation context is in its
// initial balancing pass, so it also wants to know the largest unbreakable
// block-size.
container_builder_.PropagateTallestUnbreakableBlockSize(
tallest_unbreakable_block_size_);
}
// We now have an estimated minimal block-size for the columns. Roughly
// speaking, this is the block-size that the columns will need if we are
// allowed to break freely at any offset. This is normally not the case,
// though, since there will typically be unbreakable pieces of content, such
// as replaced content, lines of text, and other things. We need to actually
// lay out into columns to figure out if they are tall enough or not (and
// stretch and retry if not). Also honor {,min-,max-}block-size properties
// before returning, and also try to not become shorter than the tallest piece
// of unbreakable content.
return ConstrainColumnBlockSize(content_runs.TallestColumnBlockSize());
}
LayoutUnit NGColumnLayoutAlgorithm::StretchColumnBlockSize(
LayoutUnit minimal_space_shortage,
LayoutUnit current_column_size) const {
LayoutUnit length = current_column_size + minimal_space_shortage;
// Honor {,min-,max-}{height,width} properties.
return ConstrainColumnBlockSize(length);
}
// Constrain a balanced column block size to not overflow the multicol
// container.
LayoutUnit NGColumnLayoutAlgorithm::ConstrainColumnBlockSize(
LayoutUnit size) const {
if (is_constrained_by_outer_fragmentation_context_) {
// Don't become too tall to fit in the outer fragmentation context.
LayoutUnit available_outer_space =
FragmentainerSpaceAtBfcStart(ConstraintSpace()) - intrinsic_block_size_;
DCHECK_GE(available_outer_space, LayoutUnit());
size = std::min(size, available_outer_space);
}
// But avoid becoming shorter than the tallest piece of unbreakable content.
size = std::max(size, tallest_unbreakable_block_size_);
// The {,min-,max-}block-size properties are specified on the multicol
// container, but here we're calculating the column block sizes inside the
// multicol container, which isn't exactly the same. We may shrink the column
// block size here, but we'll never stretch them, because the value passed is
// the perfect balanced block size. Making it taller would only disrupt the
// balanced output, for no reason. The only thing we need to worry about here
// is to not overflow the multicol container.
//
// First of all we need to convert the size to a value that can be compared
// against the resolved properties on the multicol container. That means that
// we have to convert the value from content-box to border-box.
LayoutUnit extra = BorderScrollbarPadding().BlockSum();
size += extra;
const ComputedStyle& style = Style();
LayoutUnit max = ResolveMaxBlockLength(
ConstraintSpace(), style, BorderPadding(), style.LogicalMaxHeight());
LayoutUnit extent = kIndefiniteSize;
if (!style.LogicalHeight().IsAuto()) {
extent = ResolveMainBlockLength(ConstraintSpace(), style, BorderPadding(),
style.LogicalHeight(), kIndefiniteSize);
// A specified block-size will just constrain the maximum length.
if (extent != kIndefiniteSize)
max = std::min(max, extent);
}
// A specified min-block-size may increase the maximum length.
LayoutUnit min = ResolveMinBlockLength(
ConstraintSpace(), style, BorderPadding(), style.LogicalMinHeight());
max = std::max(max, min);
// If this multicol container is nested inside another fragmentation
// context, we need to subtract the space consumed in previous fragments.
if (BreakToken())
max -= BreakToken()->ConsumedBlockSize();
// We may already have used some of the available space in earlier column rows
// or spanners.
max -= CurrentContentBlockOffset();
// Constrain and convert the value back to content-box.
size = std::min(size, max);
return (size - extra).ClampNegativeToZero();
}
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::CreateConstraintSpaceForBalancing(
const LogicalSize& column_size) const {
NGConstraintSpaceBuilder space_builder(
ConstraintSpace(), Style().GetWritingDirection(), /* is_new_fc */ true);
space_builder.SetFragmentationType(kFragmentColumn);
space_builder.SetAvailableSize({column_size.inline_size, kIndefiniteSize});
space_builder.SetStretchInlineSizeIfAuto(true);
space_builder.SetPercentageResolutionSize(ColumnPercentageResolutionSize());
space_builder.SetIsAnonymous(true);
space_builder.SetIsInColumnBfc();
space_builder.SetIsInsideBalancedColumns();
return space_builder.ToConstraintSpace();
}
NGConstraintSpace NGColumnLayoutAlgorithm::CreateConstraintSpaceForSpanner(
const NGBlockNode& spanner,
LayoutUnit block_offset) const {
NGConstraintSpaceBuilder space_builder(
ConstraintSpace(), Style().GetWritingDirection(), /* is_new_fc */ true);
space_builder.SetAvailableSize(ChildAvailableSize());
space_builder.SetStretchInlineSizeIfAuto(true);
space_builder.SetPercentageResolutionSize(ChildAvailableSize());
space_builder.SetBaselineAlgorithmType(
ConstraintSpace().BaselineAlgorithmType());
if (ConstraintSpace().HasBlockFragmentation()) {
SetupSpaceBuilderForFragmentation(ConstraintSpace(), spanner, block_offset,
&space_builder, /* is_new_fc */ true);
}
return space_builder.ToConstraintSpace();
}
NGConstraintSpace NGColumnLayoutAlgorithm::CreateConstraintSpaceForMinMax()
const {
NGConstraintSpaceBuilder space_builder(
ConstraintSpace(), Style().GetWritingDirection(), /* is_new_fc */ true);
space_builder.SetIsAnonymous(true);
space_builder.SetIsInColumnBfc();
return space_builder.ToConstraintSpace();
}
} // namespace blink