1 // Copyright 2017 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "third_party/blink/renderer/core/layout/ng/ng_column_layout_algorithm.h"
6
7 #include <algorithm>
8 #include "third_party/blink/renderer/core/layout/geometry/logical_size.h"
9 #include "third_party/blink/renderer/core/layout/ng/geometry/ng_fragment_geometry.h"
10 #include "third_party/blink/renderer/core/layout/ng/geometry/ng_margin_strut.h"
11 #include "third_party/blink/renderer/core/layout/ng/ng_block_layout_algorithm.h"
12 #include "third_party/blink/renderer/core/layout/ng/ng_box_fragment.h"
13 #include "third_party/blink/renderer/core/layout/ng/ng_constraint_space_builder.h"
14 #include "third_party/blink/renderer/core/layout/ng/ng_fragmentation_utils.h"
15 #include "third_party/blink/renderer/core/layout/ng/ng_length_utils.h"
16 #include "third_party/blink/renderer/core/layout/ng/ng_out_of_flow_layout_part.h"
17 #include "third_party/blink/renderer/core/layout/ng/ng_physical_box_fragment.h"
18 #include "third_party/blink/renderer/core/style/computed_style.h"
19
20 namespace blink {
21
22 namespace {
23
CalculateColumnContentBlockSize(const NGPhysicalContainerFragment & fragment,bool multicol_is_horizontal_writing_mode)24 LayoutUnit CalculateColumnContentBlockSize(
25 const NGPhysicalContainerFragment& fragment,
26 bool multicol_is_horizontal_writing_mode) {
27 // TODO(mstensho): Once LayoutNG is capable of calculating overflow on its
28 // own, we should probably just move over to relying on that machinery,
29 // instead of doing all this on our own.
30 LayoutUnit total_size;
31 for (const auto& child : fragment.Children()) {
32 LayoutUnit size;
33 LayoutUnit offset;
34 if (multicol_is_horizontal_writing_mode) {
35 offset = child.Offset().top;
36 size = child->Size().height;
37 } else {
38 offset = child.Offset().left;
39 size = child->Size().width;
40 }
41 if (child->IsContainer()) {
42 LayoutUnit children_size = CalculateColumnContentBlockSize(
43 To<NGPhysicalContainerFragment>(*child),
44 multicol_is_horizontal_writing_mode);
45 if (size < children_size)
46 size = children_size;
47 }
48 LayoutUnit block_end = offset + size;
49 if (total_size < block_end)
50 total_size = block_end;
51 }
52 return total_size;
53 }
54
IsColumnSpanner(NGBlockNode multicol_container,const NGBlockBreakToken & token)55 inline bool IsColumnSpanner(NGBlockNode multicol_container,
56 const NGBlockBreakToken& token) {
57 // A column spanner may also establish a multicol container on its own, so
58 // before returning true here, make sure that the spanner isn't the multicol
59 // container itself.
60 NGLayoutInputNode broken_node = token.InputNode();
61 return broken_node.IsColumnSpanAll() && broken_node != multicol_container;
62 }
63
64 // Add the break token for the column content that comes after a fragmented
65 // spanner, if any; otherwise, we're past all children.
PushNextColumnBreakToken(scoped_refptr<const NGBlockBreakToken> next_column_token,NGBoxFragmentBuilder * builder)66 void PushNextColumnBreakToken(
67 scoped_refptr<const NGBlockBreakToken> next_column_token,
68 NGBoxFragmentBuilder* builder) {
69 if (next_column_token)
70 builder->AddBreakToken(std::move(next_column_token));
71 else
72 builder->SetHasSeenAllChildren();
73 }
74
75 // Add the spanner's break token, AND another break token for the column content
76 // that comes after. In the next fragment we need to resume layout of the
77 // spanner, and then proceed to the column content - if there's room for both.
78 // Note that it's possible for the spanner to break again in the next fragment.
PushSpannerBreakTokens(scoped_refptr<const NGBlockBreakToken> spanner_break_token,scoped_refptr<const NGBlockBreakToken> next_column_token,NGBoxFragmentBuilder * builder)79 void PushSpannerBreakTokens(
80 scoped_refptr<const NGBlockBreakToken> spanner_break_token,
81 scoped_refptr<const NGBlockBreakToken> next_column_token,
82 NGBoxFragmentBuilder* builder) {
83 builder->AddBreakToken(std::move(spanner_break_token));
84 PushNextColumnBreakToken(std::move(next_column_token), builder);
85 }
86
87 } // namespace
88
NGColumnLayoutAlgorithm(const NGLayoutAlgorithmParams & params)89 NGColumnLayoutAlgorithm::NGColumnLayoutAlgorithm(
90 const NGLayoutAlgorithmParams& params)
91 : NGLayoutAlgorithm(params),
92 early_break_(params.early_break),
93 border_padding_(params.fragment_geometry.border +
94 params.fragment_geometry.padding),
95 border_scrollbar_padding_(border_padding_ +
96 params.fragment_geometry.scrollbar) {
97 AdjustForFragmentation(BreakToken(), &border_scrollbar_padding_);
98 container_builder_.SetIsNewFormattingContext(
99 params.space.IsNewFormattingContext());
100 container_builder_.SetInitialFragmentGeometry(params.fragment_geometry);
101 }
102
Layout()103 scoped_refptr<const NGLayoutResult> NGColumnLayoutAlgorithm::Layout() {
104 LogicalSize border_box_size = container_builder_.InitialBorderBoxSize();
105 content_box_size_ =
106 ShrinkAvailableSize(border_box_size, border_scrollbar_padding_);
107
108 DCHECK_GE(content_box_size_.inline_size, LayoutUnit());
109 column_inline_size_ =
110 ResolveUsedColumnInlineSize(content_box_size_.inline_size, Style());
111
112 column_inline_progression_ =
113 column_inline_size_ +
114 ResolveUsedColumnGap(content_box_size_.inline_size, Style());
115 used_column_count_ =
116 ResolveUsedColumnCount(content_box_size_.inline_size, Style());
117
118 // If we know the block-size of the fragmentainers in an outer fragmentation
119 // context (if any), our columns may be constrained by that, meaning that we
120 // may have to fragment earlier than what we would have otherwise, and, if
121 // that's the case, that we may also not create overflowing columns (in the
122 // inline axis), but rather finish the row and resume in the next row in the
123 // next outer fragmentainer. Note that it is possible to be nested inside a
124 // fragmentation context that doesn't know the block-size of its
125 // fragmentainers. This would be in the first layout pass of an outer multicol
126 // container, before any tentative column block-size has been calculated.
127 is_constrained_by_outer_fragmentation_context_ =
128 ConstraintSpace().HasKnownFragmentainerBlockSize();
129
130 if (ConstraintSpace().HasBlockFragmentation()) {
131 container_builder_.SetHasBlockFragmentation();
132 if (ConstraintSpace().IsInitialColumnBalancingPass())
133 container_builder_.SetIsInitialColumnBalancingPass();
134 }
135
136 container_builder_.SetIsBlockFragmentationContextRoot();
137
138 intrinsic_block_size_ = border_scrollbar_padding_.block_start;
139
140 NGBreakStatus break_status = LayoutChildren();
141 if (break_status == NGBreakStatus::kNeedsEarlierBreak) {
142 // We need to discard this layout and do it again. We found an earlier break
143 // point that's more appealing than the one we ran out of space at.
144 return RelayoutAndBreakEarlier();
145 } else if (break_status == NGBreakStatus::kBrokeBefore) {
146 // If we want to break before, make sure that we're actually at the start.
147 DCHECK(!BreakToken());
148
149 return container_builder_.Abort(NGLayoutResult::kOutOfFragmentainerSpace);
150 }
151
152 // Figure out how much space we've already been able to process in previous
153 // fragments, if this multicol container participates in an outer
154 // fragmentation context.
155 LayoutUnit previously_consumed_block_size;
156 if (const auto* token = BreakToken())
157 previously_consumed_block_size = token->ConsumedBlockSize();
158
159 // TODO(mstensho): Propagate baselines.
160
161 LayoutUnit block_size;
162 if (border_box_size.block_size == kIndefiniteSize) {
163 // Get the block size from the contents if it's auto.
164 block_size = intrinsic_block_size_ + border_scrollbar_padding_.block_end;
165 } else {
166 // TODO(mstensho): end border and padding may overflow the parent
167 // fragmentainer, and we should avoid that.
168 block_size = border_box_size.block_size - previously_consumed_block_size;
169 }
170
171 if (is_constrained_by_outer_fragmentation_context_) {
172 // In addition to establishing one, we're nested inside another
173 // fragmentation context.
174 FinishFragmentation(
175 ConstraintSpace(), BreakToken(), block_size, intrinsic_block_size_,
176 FragmentainerSpaceAtBfcStart(ConstraintSpace()), &container_builder_);
177 } else {
178 container_builder_.SetBlockSize(block_size);
179 container_builder_.SetIntrinsicBlockSize(intrinsic_block_size_);
180 }
181
182 NGOutOfFlowLayoutPart(
183 Node(), ConstraintSpace(),
184 container_builder_.Borders() + container_builder_.Scrollbar(),
185 &container_builder_)
186 .Run();
187
188 return container_builder_.ToBoxFragment();
189 }
190
ComputeMinMaxSizes(const MinMaxSizesInput & input) const191 base::Optional<MinMaxSizes> NGColumnLayoutAlgorithm::ComputeMinMaxSizes(
192 const MinMaxSizesInput& input) const {
193 // First calculate the min/max sizes of columns.
194 NGConstraintSpace space = CreateConstraintSpaceForMinMax();
195 NGFragmentGeometry fragment_geometry =
196 CalculateInitialMinMaxFragmentGeometry(space, Node());
197 NGBlockLayoutAlgorithm algorithm({Node(), fragment_geometry, space});
198 base::Optional<MinMaxSizes> min_max_sizes =
199 algorithm.ComputeMinMaxSizes(input);
200 DCHECK(min_max_sizes.has_value());
201 MinMaxSizes sizes = *min_max_sizes;
202
203 // If column-width is non-auto, pick the larger of that and intrinsic column
204 // width.
205 if (!Style().HasAutoColumnWidth()) {
206 sizes.min_size =
207 std::max(sizes.min_size, LayoutUnit(Style().ColumnWidth()));
208 sizes.max_size = std::max(sizes.max_size, sizes.min_size);
209 }
210
211 // Now convert those column min/max values to multicol container min/max
212 // values. We typically have multiple columns and also gaps between them.
213 int column_count = Style().ColumnCount();
214 DCHECK_GE(column_count, 1);
215 sizes.min_size *= column_count;
216 sizes.max_size *= column_count;
217 LayoutUnit column_gap = ResolveUsedColumnGap(LayoutUnit(), Style());
218 sizes += column_gap * (column_count - 1);
219
220 // TODO(mstensho): Need to include spanners.
221
222 sizes += border_scrollbar_padding_.InlineSum();
223 return sizes;
224 }
225
LayoutChildren()226 NGBreakStatus NGColumnLayoutAlgorithm::LayoutChildren() {
227 NGMarginStrut margin_strut;
228
229 // First extract incoming child break tokens.
230 scoped_refptr<const NGBlockBreakToken> spanner_break_token;
231 scoped_refptr<const NGBlockBreakToken> next_column_token;
232 if (const auto* token = BreakToken()) {
233 // We're resuming layout of this multicol container after an outer
234 // fragmentation break. Resume at the break token of the last column that we
235 // were able to lay out, or before or inside the spanner that caused an
236 // outer fragmentainer break. Note that in some cases, there may be no child
237 // break tokens. That happens if we weren't able to lay out anything at all
238 // in the previous outer fragmentainer, e.g. due to a forced break before
239 // this multicol container, or e.g. if there was leading unbreakable content
240 // that couldn't fit in the space we were offered back then. In other words,
241 // in that case, we're about to create the first fragment for this multicol
242 // container.
243 const auto child_tokens = token->ChildBreakTokens();
244 if (wtf_size_t break_token_count = child_tokens.size()) {
245 wtf_size_t break_token_idx = 0;
246 scoped_refptr<const NGBlockBreakToken> child_token =
247 To<NGBlockBreakToken>(child_tokens[break_token_idx++]);
248 if (child_token && IsColumnSpanner(Node(), *child_token)) {
249 // We're resuming at a column spanner. Get the next break token after
250 // the spanner, if any. That would be the column content to resume at,
251 // once we're done with the spanner.
252 spanner_break_token = child_token;
253 if (break_token_idx < break_token_count) {
254 next_column_token =
255 To<NGBlockBreakToken>(child_tokens[break_token_idx++]);
256 }
257 } else {
258 next_column_token = child_token;
259 }
260 // There shouldn't be any additional break tokens.
261 DCHECK_EQ(break_token_idx, break_token_count);
262 }
263
264 if (token->HasSeenAllChildren())
265 container_builder_.SetHasSeenAllChildren();
266 }
267
268 if (spanner_break_token) {
269 // The multicol container previously broke at a spanner (this may happen if
270 // we're nested inside another fragmentation context), so that's where we'll
271 // resume now.
272 NGBreakStatus break_status = LayoutSpanner(
273 To<NGBlockNode>(spanner_break_token->InputNode()),
274 spanner_break_token.get(), &margin_strut, &spanner_break_token);
275
276 if (spanner_break_token) {
277 DCHECK_EQ(break_status, NGBreakStatus::kContinue);
278 if (spanner_break_token) {
279 // We broke at the spanner again!
280 PushSpannerBreakTokens(std::move(spanner_break_token),
281 std::move(next_column_token),
282 &container_builder_);
283 return NGBreakStatus::kContinue;
284 }
285 } else {
286 // Breaking before the first element in the fragmentainer isn't allowed,
287 // as that would give no content progress, and we'd be stuck forever.
288 DCHECK_EQ(break_status, NGBreakStatus::kContinue);
289 }
290 }
291
292 if (BreakToken() && BreakToken()->HasSeenAllChildren() && !next_column_token)
293 return NGBreakStatus::kContinue;
294
295 // Entering the child main loop. Here we'll alternate between laying out
296 // column content and column spanners, until we're either done, or until
297 // something breaks. Spanners are discovered as part of laying out a row, so
298 // we'll always start with attempting to lay out a row, even if the first
299 // child is a spanner.
300 do {
301 scoped_refptr<const NGLayoutResult> result =
302 LayoutRow(next_column_token.get(), &margin_strut);
303
304 if (!result) {
305 // Not enough outer fragmentainer space to produce any columns at all.
306 container_builder_.SetDidBreak();
307 if (intrinsic_block_size_) {
308 // We have preceding initial border/padding, or a column spanner
309 // (possibly preceded by other spanners or even column content). So we
310 // need to break inside the multicol container. Stop walking the
311 // children, but "continue" layout, so that we produce a fragment. Note
312 // that we normally don't want to break right after initial
313 // border/padding, but will do so as a last resort. It's up to our
314 // containing block to decide what's best.
315 FinishAfterBreakBeforeRow(std::move(next_column_token));
316 return NGBreakStatus::kContinue;
317 }
318 // Otherwise we have nothing here, and need to break before the multicol
319 // container. No fragment will be produced.
320 return NGBreakStatus::kBrokeBefore;
321 }
322
323 next_column_token =
324 To<NGBlockBreakToken>(result->PhysicalFragment().BreakToken());
325
326 // If we didn't find a spanner, it either means that we're through
327 // everything, or that column layout needs to continue from the next outer
328 // fragmentainer.
329 NGBlockNode spanner_node = result->ColumnSpanner();
330 if (!spanner_node)
331 break;
332
333 if (early_break_) {
334 // If this is the child we had previously determined to break before, do
335 // so now and finish layout.
336 DCHECK_EQ(early_break_->Type(), NGEarlyBreak::kBlock);
337 if (early_break_->IsBreakBefore() &&
338 early_break_->BlockNode() == spanner_node) {
339 container_builder_.AddBreakBeforeChild(
340 spanner_node, kBreakAppealPerfect, /* is_forced_break */ false);
341 FinishAfterBreakBeforeSpanner(std::move(next_column_token));
342 return NGBreakStatus::kContinue;
343 }
344 }
345
346 // We found a spanner. Lay it out, and then resume column layout.
347 NGBreakStatus break_status = LayoutSpanner(
348 spanner_node, nullptr, &margin_strut, &spanner_break_token);
349 if (break_status == NGBreakStatus::kNeedsEarlierBreak) {
350 return break_status;
351 } else if (break_status == NGBreakStatus::kBrokeBefore) {
352 DCHECK(ConstraintSpace().HasBlockFragmentation());
353 FinishAfterBreakBeforeSpanner(std::move(next_column_token));
354 return NGBreakStatus::kContinue;
355 } else if (spanner_break_token) {
356 DCHECK_EQ(break_status, NGBreakStatus::kContinue);
357 // We broke inside the spanner. This may happen if we're nested inside
358 // another fragmentation context.
359 PushSpannerBreakTokens(std::move(spanner_break_token),
360 std::move(next_column_token), &container_builder_);
361 return NGBreakStatus::kContinue;
362 }
363 } while (next_column_token);
364
365 // If there's an early break set, we should have found it and returned.
366 DCHECK(!early_break_);
367
368 if (next_column_token) {
369 // We broke inside column content. Add a break token for where to resume
370 // column layout at in the next fragment.
371 container_builder_.AddBreakToken(std::move(next_column_token));
372 } else {
373 // We've gone through all the content. This doesn't necessarily mean that
374 // we're done fragmenting, since the multicol container may be taller than
375 // what the content requires, which means that we might create more
376 // (childless) fragments, if we're nested inside another fragmentation
377 // context. In that case we must make sure to skip the contents when
378 // resuming.
379 container_builder_.SetHasSeenAllChildren();
380
381 intrinsic_block_size_ += margin_strut.Sum();
382 }
383
384 return NGBreakStatus::kContinue;
385 }
386
LayoutRow(const NGBlockBreakToken * next_column_token,NGMarginStrut * margin_strut)387 scoped_refptr<const NGLayoutResult> NGColumnLayoutAlgorithm::LayoutRow(
388 const NGBlockBreakToken* next_column_token,
389 NGMarginStrut* margin_strut) {
390 LogicalSize column_size(column_inline_size_, content_box_size_.block_size);
391
392 // If block-size is non-auto, subtract the space for content we've consumed in
393 // previous fragments. This is necessary when we're nested inside another
394 // fragmentation context.
395 if (column_size.block_size != kIndefiniteSize) {
396 if (BreakToken() && is_constrained_by_outer_fragmentation_context_)
397 column_size.block_size -= BreakToken()->ConsumedBlockSize();
398
399 // Subtract the space already taken in the current fragment (spanners and
400 // earlier column rows).
401 column_size.block_size -= CurrentContentBlockOffset();
402
403 column_size.block_size = column_size.block_size.ClampNegativeToZero();
404 }
405
406 // We balance if block-size is unconstrained, or when we're explicitly told
407 // to. Note that the block-size may be constrained by outer fragmentation
408 // contexts, not just by a block-size specified on this multicol container.
409 bool balance_columns = Style().GetColumnFill() == EColumnFill::kBalance ||
410 (column_size.block_size == kIndefiniteSize &&
411 !is_constrained_by_outer_fragmentation_context_);
412
413 if (balance_columns) {
414 column_size.block_size =
415 CalculateBalancedColumnBlockSize(column_size, next_column_token);
416 }
417
418 // Column rows have no representation in the DOM and have no margins, but
419 // there may be a trailing margin from a preceding spanner.
420 LayoutUnit column_block_offset = intrinsic_block_size_ + margin_strut->Sum();
421
422 bool needs_more_fragments_in_outer = false;
423 bool zero_outer_space_left = false;
424 if (is_constrained_by_outer_fragmentation_context_) {
425 LayoutUnit available_outer_space =
426 FragmentainerSpaceAtBfcStart(ConstraintSpace()) - column_block_offset;
427
428 if (available_outer_space <= LayoutUnit()) {
429 if (available_outer_space < LayoutUnit()) {
430 // We're past the end of the outer fragmentainer (typically due to a
431 // margin). Nothing will fit here, not even zero-size content.
432 return nullptr;
433 }
434
435 // We are out of space, but we're exactly at the end of the outer
436 // fragmentainer. If none of our contents take up space, we're going to
437 // fit, otherwise not. Lay out and find out.
438 zero_outer_space_left = true;
439 }
440
441 // Check if we can fit everything (that's remaining), block-wise, within the
442 // current outer fragmentainer. If we can't, we need to adjust the block
443 // size, and allow the multicol container to continue in a subsequent outer
444 // fragmentainer. Note that we also need to handle indefinite block-size,
445 // because that may happen in a nested multicol container with auto
446 // block-size and column balancing disabled.
447 if (column_size.block_size > available_outer_space ||
448 column_size.block_size == kIndefiniteSize) {
449 column_size.block_size = available_outer_space;
450 needs_more_fragments_in_outer = true;
451 }
452 }
453
454 DCHECK_GE(column_size.block_size, LayoutUnit());
455
456 // New column fragments won't be added to the fragment builder right away,
457 // since we may need to delete them and try again with a different block-size
458 // (colum balancing). Keep them in this list, and add them to the fragment
459 // builder when we have the final column fragments. Or clear the list and
460 // retry otherwise.
461 NGContainerFragmentBuilder::ChildrenVector new_columns;
462
463 scoped_refptr<const NGLayoutResult> result;
464
465 do {
466 scoped_refptr<const NGBlockBreakToken> column_break_token =
467 next_column_token;
468
469 // This is the first column in this fragmentation context if there are no
470 // preceding columns in this row and there are also no preceding rows.
471 bool is_first_fragmentainer = !column_break_token && !BreakToken();
472
473 LayoutUnit column_inline_offset(border_scrollbar_padding_.inline_start);
474 int actual_column_count = 0;
475 int forced_break_count = 0;
476
477 // Each column should calculate their own minimal space shortage. Find the
478 // lowest value of those. This will serve as the column stretch amount, if
479 // we determine that stretching them is necessary and possible (column
480 // balancing).
481 LayoutUnit minimal_space_shortage(LayoutUnit::Max());
482
483 do {
484 // Lay out one column. Each column will become a fragment.
485 NGConstraintSpace child_space = CreateConstraintSpaceForColumns(
486 column_size, is_first_fragmentainer, balance_columns);
487
488 NGFragmentGeometry fragment_geometry =
489 CalculateInitialFragmentGeometry(child_space, Node());
490
491 NGBlockLayoutAlgorithm child_algorithm(
492 {Node(), fragment_geometry, child_space, column_break_token.get()});
493 child_algorithm.SetBoxType(NGPhysicalFragment::kColumnBox);
494 result = child_algorithm.Layout();
495 const auto& column = result->PhysicalFragment();
496
497 // Add the new column fragment to the list, but don't commit anything to
498 // the fragment builder until we know whether these are the final columns.
499 LogicalOffset logical_offset(column_inline_offset, column_block_offset);
500 new_columns.emplace_back(logical_offset, &result->PhysicalFragment());
501
502 LayoutUnit space_shortage = result->MinimalSpaceShortage();
503 if (space_shortage > LayoutUnit()) {
504 minimal_space_shortage =
505 std::min(minimal_space_shortage, space_shortage);
506 }
507 actual_column_count++;
508 if (result->HasForcedBreak())
509 forced_break_count++;
510
511 column_inline_offset += column_inline_progression_;
512
513 if (result->ColumnSpanner())
514 break;
515
516 column_break_token = To<NGBlockBreakToken>(column.BreakToken());
517
518 // If we're participating in an outer fragmentation context, we'll only
519 // allow as many columns as the used value of column-count, so that we
520 // don't overflow in the inline direction. There's one important
521 // exception: If we have determined that this is going to be the last
522 // fragment for this multicol container in the outer fragmentation
523 // context, we'll just allow as many columns as needed (and let them
524 // overflow in the inline direction, if necessary). We're not going to
525 // progress into a next outer fragmentainer if the (remaining part of the)
526 // multicol container fits block-wise in the current outer fragmentainer.
527 if (ConstraintSpace().HasBlockFragmentation() && column_break_token &&
528 actual_column_count >= used_column_count_ &&
529 needs_more_fragments_in_outer) {
530 // We cannot keep any of this if we have zero space left. Then we need
531 // to resume in the next outer fragmentainer.
532 if (zero_outer_space_left)
533 return nullptr;
534
535 container_builder_.SetDidBreak();
536 container_builder_.SetBreakAppeal(kBreakAppealPerfect);
537 break;
538 }
539
540 is_first_fragmentainer = false;
541 } while (column_break_token);
542
543 // TODO(mstensho): Nested column balancing.
544 if (container_builder_.DidBreak())
545 break;
546
547 if (!balance_columns && result->ColumnSpanner()) {
548 // We always have to balance columns preceding a spanner, so if we didn't
549 // do that initially, switch over to column balancing mode now, and lay
550 // out again.
551 balance_columns = true;
552 new_columns.clear();
553 column_size.block_size =
554 CalculateBalancedColumnBlockSize(column_size, next_column_token);
555 continue;
556 }
557
558 // If we overflowed (actual column count larger than what we have room for),
559 // and we're supposed to calculate the column lengths automatically (column
560 // balancing), see if we're able to stretch them.
561 //
562 // We can only stretch the columns if we have at least one column that could
563 // take more content, and we also need to know the stretch amount (minimal
564 // space shortage). We need at least one soft break opportunity to do
565 // this. If forced breaks cause too many breaks, there's no stretch amount
566 // that could prevent the actual column count from overflowing.
567 //
568 // TODO(mstensho): Handle this situation also when we're inside another
569 // balanced multicol container, rather than bailing (which we do now, to
570 // avoid infinite loops). If we exhaust the inner column-count in such
571 // cases, that piece of information may have to be propagated to the outer
572 // multicol, and instead stretch there (not here). We have no such mechanism
573 // in place yet.
574 if (balance_columns && actual_column_count > used_column_count_ &&
575 actual_column_count > forced_break_count + 1 &&
576 minimal_space_shortage != LayoutUnit::Max() &&
577 !ConstraintSpace().IsInsideBalancedColumns()) {
578 LayoutUnit new_column_block_size = StretchColumnBlockSize(
579 minimal_space_shortage, column_size.block_size);
580
581 DCHECK_GE(new_column_block_size, column_size.block_size);
582 if (new_column_block_size > column_size.block_size) {
583 // Remove column fragments and re-attempt layout with taller columns.
584 new_columns.clear();
585 column_size.block_size = new_column_block_size;
586 continue;
587 }
588 }
589 break;
590 } while (true);
591
592 bool is_empty = false;
593
594 // If there was no content inside to process, we don't want the resulting
595 // empty column fragment.
596 if (new_columns.size() == 1u) {
597 const NGPhysicalBoxFragment& column =
598 *To<NGPhysicalBoxFragment>(new_columns[0].fragment.get());
599
600 if (column.Children().size() == 0) {
601 // No content. Keep the trailing margin from any previous column spanner.
602 is_empty = true;
603
604 // TODO(mstensho): It's wrong to keep the empty fragment, just so that
605 // out-of-flow descendants get propagated correctly. Find some other way
606 // of propagating them.
607 if (!column.HasOutOfFlowPositionedDescendants())
608 return result;
609 }
610 }
611
612 intrinsic_block_size_ = column_block_offset + column_size.block_size;
613
614 if (!is_empty) {
615 has_processed_first_child_ = true;
616 container_builder_.SetPreviousBreakAfter(EBreakBetween::kAuto);
617
618 // We added a row. Reset the trailing margin from any previous column
619 // spanner.
620 *margin_strut = NGMarginStrut();
621 }
622
623 // Commit all column fragments to the fragment builder.
624 for (auto column : new_columns) {
625 container_builder_.AddChild(To<NGPhysicalBoxFragment>(*column.fragment),
626 column.offset);
627 }
628
629 return result;
630 }
631
LayoutSpanner(NGBlockNode spanner_node,const NGBlockBreakToken * break_token,NGMarginStrut * margin_strut,scoped_refptr<const NGBlockBreakToken> * spanner_break_token)632 NGBreakStatus NGColumnLayoutAlgorithm::LayoutSpanner(
633 NGBlockNode spanner_node,
634 const NGBlockBreakToken* break_token,
635 NGMarginStrut* margin_strut,
636 scoped_refptr<const NGBlockBreakToken>* spanner_break_token) {
637 *spanner_break_token = nullptr;
638 const ComputedStyle& spanner_style = spanner_node.Style();
639 NGBoxStrut margins = ComputeMarginsFor(
640 spanner_style, content_box_size_.inline_size,
641 ConstraintSpace().GetWritingMode(), ConstraintSpace().Direction());
642
643 if (break_token) {
644 // Truncate block-start margins at fragmentainer breaks (except when the
645 // break is forced), and also make sure that we don't repeat them at the
646 // beginning of every fragment generated from the spanner node.
647 if (!break_token->IsBreakBefore() || !break_token->IsForcedBreak())
648 margins.block_start = LayoutUnit();
649
650 if (break_token->IsBreakBefore()) {
651 // TODO(mstensho): Passing a break-before token shouldn't be a problem,
652 // but it would cause problems for the NGPaintFragment code. Just pass
653 // nullptr. Won't make any difference anyway.
654 break_token = nullptr;
655 }
656 }
657
658 // Collapse the block-start margin of this spanner with the block-end margin
659 // of an immediately preceding spanner, if any.
660 margin_strut->Append(margins.block_start, /* is_quirky */ false);
661
662 LayoutUnit block_offset = intrinsic_block_size_ + margin_strut->Sum();
663 auto spanner_space =
664 CreateConstraintSpaceForSpanner(spanner_node, block_offset);
665
666 const NGEarlyBreak* early_break_in_child = nullptr;
667 if (early_break_ && early_break_->Type() == NGEarlyBreak::kBlock &&
668 early_break_->BlockNode() == spanner_node) {
669 // We're entering a child that we know that we're going to break inside, and
670 // even where to break. Look inside, and pass the inner breakpoint to
671 // layout.
672 early_break_in_child = early_break_->BreakInside();
673 // If there's no break inside, we should already have broken before this
674 // child.
675 DCHECK(early_break_in_child);
676 }
677
678 auto result =
679 spanner_node.Layout(spanner_space, break_token, early_break_in_child);
680
681 if (ConstraintSpace().HasBlockFragmentation() && !early_break_) {
682 // We're nested inside another fragmentation context. Examine this break
683 // point, and determine whether we should break.
684
685 LayoutUnit fragmentainer_block_offset =
686 ConstraintSpace().FragmentainerOffsetAtBfc() + block_offset;
687
688 NGBreakStatus break_status = BreakBeforeChildIfNeeded(
689 ConstraintSpace(), spanner_node, *result.get(),
690 fragmentainer_block_offset, has_processed_first_child_,
691 &container_builder_);
692
693 if (break_status != NGBreakStatus::kContinue) {
694 // We need to break, either before the spanner, or even earlier.
695 return break_status;
696 }
697 }
698
699 NGFragment fragment(ConstraintSpace().GetWritingMode(),
700 result->PhysicalFragment());
701
702 ResolveInlineMargins(spanner_style, Style(), content_box_size_.inline_size,
703 fragment.InlineSize(), &margins);
704
705 LogicalOffset offset(
706 border_scrollbar_padding_.inline_start + margins.inline_start,
707 block_offset);
708 container_builder_.AddResult(*result, offset);
709
710 *margin_strut = NGMarginStrut();
711 margin_strut->Append(margins.block_end, /* is_quirky */ false);
712
713 intrinsic_block_size_ = offset.block_offset + fragment.BlockSize();
714 has_processed_first_child_ = true;
715
716 EBreakBetween break_after = JoinFragmentainerBreakValues(
717 result->FinalBreakAfter(), spanner_node.Style().BreakAfter());
718 container_builder_.SetPreviousBreakAfter(break_after);
719
720 *spanner_break_token =
721 To<NGBlockBreakToken>(result->PhysicalFragment().BreakToken());
722 return NGBreakStatus::kContinue;
723 }
724
CalculateBalancedColumnBlockSize(const LogicalSize & column_size,const NGBlockBreakToken * child_break_token)725 LayoutUnit NGColumnLayoutAlgorithm::CalculateBalancedColumnBlockSize(
726 const LogicalSize& column_size,
727 const NGBlockBreakToken* child_break_token) {
728 // To calculate a balanced column size for one row of columns, we need to
729 // figure out how tall our content is. To do that we need to lay out. Create a
730 // special constraint space for column balancing, without allowing soft
731 // breaks. It will make us lay out all the multicol content as one single tall
732 // strip (unless there are forced breaks). When we're done with this layout
733 // pass, we can examine the result and calculate an ideal column block-size.
734 NGConstraintSpace space = CreateConstraintSpaceForBalancing(column_size);
735 NGFragmentGeometry fragment_geometry =
736 CalculateInitialFragmentGeometry(space, Node());
737
738 // A run of content without explicit (forced) breaks; i.e. the content portion
739 // between two explicit breaks, between fragmentation context start and an
740 // explicit break, between an explicit break and fragmentation context end,
741 // or, in cases when there are no explicit breaks at all: between
742 // fragmentation context start and end. We need to know where the explicit
743 // breaks are, in order to figure out where the implicit breaks will end up,
744 // so that we get the columns properly balanced. A content run starts out as
745 // representing one single column, and we'll add as many additional implicit
746 // breaks as needed into the content runs that are the tallest ones
747 // (ColumnBlockSize()).
748 struct ContentRun {
749 ContentRun(LayoutUnit content_block_size)
750 : content_block_size(content_block_size) {}
751
752 // Return the column block-size that this content run would require,
753 // considering the implicit breaks we have assumed so far.
754 LayoutUnit ColumnBlockSize() const {
755 // Some extra care is required for the division here. We want the
756 // resulting LayoutUnit value to be large enough to prevent overflowing
757 // columns. Use floating point to get higher precision than
758 // LayoutUnit. Then convert it to a LayoutUnit, but round it up to the
759 // nearest value that LayoutUnit is able to represent.
760 return LayoutUnit::FromFloatCeil(
761 float(content_block_size) / float(implicit_breaks_assumed_count + 1));
762 }
763
764 LayoutUnit content_block_size;
765
766 // The number of implicit breaks assumed to exist in this content run.
767 int implicit_breaks_assumed_count = 0;
768 };
769
770 class ContentRuns : public Vector<ContentRun, 1> {
771 public:
772 wtf_size_t IndexWithTallestColumns() const {
773 DCHECK_GT(size(), 0u);
774 wtf_size_t index = 0;
775 LayoutUnit largest_block_size = LayoutUnit::Min();
776 for (size_t i = 0; i < size(); i++) {
777 const ContentRun& run = at(i);
778 LayoutUnit block_size = run.ColumnBlockSize();
779 if (largest_block_size < block_size) {
780 largest_block_size = block_size;
781 index = i;
782 }
783 }
784 return index;
785 }
786
787 // When we have "inserted" (assumed) enough implicit column breaks, this
788 // method returns the block-size of the tallest column.
789 LayoutUnit TallestColumnBlockSize() const {
790 return at(IndexWithTallestColumns()).ColumnBlockSize();
791 }
792 };
793
794 // First split into content runs at explicit (forced) breaks.
795 ContentRuns content_runs;
796 scoped_refptr<const NGBlockBreakToken> break_token = child_break_token;
797 LayoutUnit tallest_unbreakable_block_size;
798 do {
799 NGBlockLayoutAlgorithm balancing_algorithm(
800 {Node(), fragment_geometry, space, break_token.get()});
801 scoped_refptr<const NGLayoutResult> result = balancing_algorithm.Layout();
802
803 // This algorithm should never abort.
804 DCHECK_EQ(result->Status(), NGLayoutResult::kSuccess);
805
806 const NGPhysicalBoxFragment& fragment =
807 To<NGPhysicalBoxFragment>(result->PhysicalFragment());
808 LayoutUnit column_block_size = CalculateColumnContentBlockSize(
809 fragment, IsHorizontalWritingMode(space.GetWritingMode()));
810 content_runs.emplace_back(column_block_size);
811
812 tallest_unbreakable_block_size = std::max(
813 tallest_unbreakable_block_size, result->TallestUnbreakableBlockSize());
814
815 // Stop when we reach a spanner. That's where this row of columns will end.
816 if (result->ColumnSpanner())
817 break;
818
819 break_token = To<NGBlockBreakToken>(fragment.BreakToken());
820 } while (break_token);
821
822 // Then distribute as many implicit breaks into the content runs as we need.
823 int used_column_count =
824 ResolveUsedColumnCount(content_box_size_.inline_size, Style());
825 for (int columns_found = content_runs.size();
826 columns_found < used_column_count; columns_found++) {
827 // The tallest content run (with all assumed implicit breaks added so far
828 // taken into account) is where we assume the next implicit break.
829 wtf_size_t index = content_runs.IndexWithTallestColumns();
830 content_runs[index].implicit_breaks_assumed_count++;
831 }
832
833 if (ConstraintSpace().IsInitialColumnBalancingPass()) {
834 // Nested column balancing. Our outer fragmentation context is in its
835 // initial balancing pass, so it also wants to know the largest unbreakable
836 // block-size.
837 container_builder_.PropagateTallestUnbreakableBlockSize(
838 tallest_unbreakable_block_size);
839 }
840
841 // We now have an estimated minimal block-size for the columns. Roughly
842 // speaking, this is the block-size that the columns will need if we are
843 // allowed to break freely at any offset. This is normally not the case,
844 // though, since there will typically be unbreakable pieces of content, such
845 // as replaced content, lines of text, and other things. We need to actually
846 // lay out into columns to figure out if they are tall enough or not (and
847 // stretch and retry if not). Also honor {,min-,max-}{height,width} properties
848 // before returning.
849 LayoutUnit block_size = std::max(content_runs.TallestColumnBlockSize(),
850 tallest_unbreakable_block_size);
851
852 return ConstrainColumnBlockSize(block_size);
853 }
854
StretchColumnBlockSize(LayoutUnit minimal_space_shortage,LayoutUnit current_column_size) const855 LayoutUnit NGColumnLayoutAlgorithm::StretchColumnBlockSize(
856 LayoutUnit minimal_space_shortage,
857 LayoutUnit current_column_size) const {
858 LayoutUnit length = current_column_size + minimal_space_shortage;
859 // Honor {,min-,max-}{height,width} properties.
860 return ConstrainColumnBlockSize(length);
861 }
862
863 // Constrain a balanced column block size to not overflow the multicol
864 // container.
ConstrainColumnBlockSize(LayoutUnit size) const865 LayoutUnit NGColumnLayoutAlgorithm::ConstrainColumnBlockSize(
866 LayoutUnit size) const {
867 // The {,max-}{height,width} properties are specified on the multicol
868 // container, but here we're calculating the column block sizes inside the
869 // multicol container, which isn't exactly the same. We may shrink the column
870 // block size here, but we'll never stretch it, because the value passed is
871 // the perfect balanced block size. Making it taller would only disrupt the
872 // balanced output, for no reason. The only thing we need to worry about here
873 // is to not overflow the multicol container.
874
875 // First of all we need to convert the size to a value that can be compared
876 // against the resolved properties on the multicol container. That means that
877 // we have to convert the value from content-box to border-box.
878 LayoutUnit extra = border_scrollbar_padding_.BlockSum();
879 size += extra;
880
881 const ComputedStyle& style = Style();
882 LayoutUnit max = ResolveMaxBlockLength(
883 ConstraintSpace(), style, border_padding_, style.LogicalMaxHeight(),
884 LengthResolvePhase::kLayout);
885 LayoutUnit extent = ResolveMainBlockLength(
886 ConstraintSpace(), style, border_padding_, style.LogicalHeight(), size,
887 LengthResolvePhase::kLayout);
888 if (extent != kIndefiniteSize) {
889 // A specified height/width will just constrain the maximum length.
890 max = std::min(max, extent);
891 }
892
893 // Constrain and convert the value back to content-box.
894 size = std::min(size, max);
895 return size - extra;
896 }
897
FinishAfterBreakBeforeRow(scoped_refptr<const NGBlockBreakToken> next_column_token)898 void NGColumnLayoutAlgorithm::FinishAfterBreakBeforeRow(
899 scoped_refptr<const NGBlockBreakToken> next_column_token) {
900 // We broke before a row for columns. We're done here. Take up the remaining
901 // space in the outer fragmentation context.
902 intrinsic_block_size_ = FragmentainerSpaceAtBfcStart(ConstraintSpace());
903
904 // If we were about to resume column layout after a spanner, add a break token
905 // for this, so that we resume there in the next outer fragmentainer. If
906 // there's no such break token, it means that we're at the start of the
907 // multicol container.
908 if (next_column_token)
909 container_builder_.AddBreakToken(std::move(next_column_token));
910 }
911
FinishAfterBreakBeforeSpanner(scoped_refptr<const NGBlockBreakToken> next_column_token)912 void NGColumnLayoutAlgorithm::FinishAfterBreakBeforeSpanner(
913 scoped_refptr<const NGBlockBreakToken> next_column_token) {
914 // We broke before the spanner. We're done here. Take up the remaining space
915 // in the outer fragmentation context.
916 intrinsic_block_size_ = FragmentainerSpaceAtBfcStart(ConstraintSpace());
917
918 // A break token for the spanner has already been inserted, but we also need
919 // to add one for the column contents that follows, so that we know where to
920 // resume, once done with the spanner - or - specify that we're past
921 // everything if there's nothing to resume at (so that we don't restart from
922 // the beginning of the multicol container).
923 PushNextColumnBreakToken(std::move(next_column_token), &container_builder_);
924 }
925
926 scoped_refptr<const NGLayoutResult>
RelayoutAndBreakEarlier()927 NGColumnLayoutAlgorithm::RelayoutAndBreakEarlier() {
928 // Not allowed to recurse!
929 DCHECK(!early_break_);
930
931 const NGEarlyBreak& breakpoint = container_builder_.EarlyBreak();
932 NGLayoutAlgorithmParams params(Node(),
933 container_builder_.InitialFragmentGeometry(),
934 ConstraintSpace(), BreakToken(), &breakpoint);
935 NGColumnLayoutAlgorithm algorithm_with_break(params);
936 NGBoxFragmentBuilder& new_builder = algorithm_with_break.container_builder_;
937 new_builder.SetBoxType(container_builder_.BoxType());
938 // We're not going to run out of space in the next layout pass, since we're
939 // breaking earlier, so no space shortage will be detected. Repeat what we
940 // found in this pass.
941 new_builder.PropagateSpaceShortage(container_builder_.MinimalSpaceShortage());
942 return algorithm_with_break.Layout();
943 }
944
CreateConstraintSpaceForColumns(const LogicalSize & column_size,bool is_first_fragmentainer,bool balance_columns) const945 NGConstraintSpace NGColumnLayoutAlgorithm::CreateConstraintSpaceForColumns(
946 const LogicalSize& column_size,
947 bool is_first_fragmentainer,
948 bool balance_columns) const {
949 NGConstraintSpaceBuilder space_builder(
950 ConstraintSpace(), Style().GetWritingMode(), /* is_new_fc */ true);
951 space_builder.SetAvailableSize(column_size);
952 space_builder.SetPercentageResolutionSize(column_size);
953
954 // To ensure progression, we need something larger than 0 here. The spec
955 // actually says that fragmentainers have to accept at least 1px of content.
956 // See https://www.w3.org/TR/css-break-3/#breaking-rules
957 LayoutUnit column_block_size =
958 std::max(column_size.block_size, LayoutUnit(1));
959
960 space_builder.SetFragmentationType(kFragmentColumn);
961 space_builder.SetFragmentainerBlockSize(column_block_size);
962 space_builder.SetIsAnonymous(true);
963 space_builder.SetIsInColumnBfc();
964 if (balance_columns)
965 space_builder.SetIsInsideBalancedColumns();
966 if (!is_first_fragmentainer) {
967 // Margins at fragmentainer boundaries should be eaten and truncated to
968 // zero. Note that this doesn't apply to margins at forced breaks, but we'll
969 // deal with those when we get to them. Set up a margin strut that eats all
970 // leading adjacent margins.
971 space_builder.SetDiscardingMarginStrut();
972 }
973
974 return space_builder.ToConstraintSpace();
975 }
976
CreateConstraintSpaceForBalancing(const LogicalSize & column_size) const977 NGConstraintSpace NGColumnLayoutAlgorithm::CreateConstraintSpaceForBalancing(
978 const LogicalSize& column_size) const {
979 NGConstraintSpaceBuilder space_builder(
980 ConstraintSpace(), Style().GetWritingMode(), /* is_new_fc */ true);
981 space_builder.SetFragmentationType(kFragmentColumn);
982 space_builder.SetAvailableSize({column_size.inline_size, kIndefiniteSize});
983 space_builder.SetPercentageResolutionSize(column_size);
984 space_builder.SetIsAnonymous(true);
985 space_builder.SetIsInColumnBfc();
986 space_builder.SetIsInsideBalancedColumns();
987
988 return space_builder.ToConstraintSpace();
989 }
990
CreateConstraintSpaceForSpanner(const NGBlockNode & spanner,LayoutUnit block_offset) const991 NGConstraintSpace NGColumnLayoutAlgorithm::CreateConstraintSpaceForSpanner(
992 const NGBlockNode& spanner,
993 LayoutUnit block_offset) const {
994 NGConstraintSpaceBuilder space_builder(
995 ConstraintSpace(), Style().GetWritingMode(), /* is_new_fc */ true);
996 space_builder.SetAvailableSize(content_box_size_);
997 space_builder.SetPercentageResolutionSize(content_box_size_);
998
999 if (ConstraintSpace().HasBlockFragmentation()) {
1000 SetupFragmentation(ConstraintSpace(), spanner, block_offset, &space_builder,
1001 /* is_new_fc */ true);
1002 }
1003
1004 return space_builder.ToConstraintSpace();
1005 }
1006
CreateConstraintSpaceForMinMax() const1007 NGConstraintSpace NGColumnLayoutAlgorithm::CreateConstraintSpaceForMinMax()
1008 const {
1009 NGConstraintSpaceBuilder space_builder(
1010 ConstraintSpace(), Style().GetWritingMode(), /* is_new_fc */ true);
1011 space_builder.SetIsAnonymous(true);
1012 space_builder.SetIsInColumnBfc();
1013
1014 return space_builder.ToConstraintSpace();
1015 }
1016
1017 } // namespace blink
1018