| // 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_fragmentation_utils.h" |
| |
| #include "third_party/blink/renderer/core/core_export.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_block_break_token.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_box_fragment.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_box_fragment_builder.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_constraint_space.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_constraint_space_builder.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_length_utils.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 { |
| |
| // 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(); |
| FALLTHROUGH; |
| 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; |
| // TODO(mstensho): The innermost fragmentation type doesn't tell us everything |
| // here. We might want to force a break to the next page, even if we're in |
| // multicol (printing multicol, for instance). |
| 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; |
| } |
| |
| template <typename Property> |
| bool IsAvoidBreakValue(const NGConstraintSpace& constraint_space, |
| Property break_value) { |
| if (break_value == Property::kAvoid) |
| return constraint_space.HasBlockFragmentation(); |
| if (break_value == Property::kAvoidColumn) |
| return constraint_space.BlockFragmentationType() == kFragmentColumn; |
| // TODO(mstensho): The innermost fragmentation type doesn't tell us everything |
| // here. We might want to avoid breaking to the next page, even if we're |
| // in multicol (printing multicol, for instance). |
| if (break_value == Property::kAvoidPage) |
| return constraint_space.BlockFragmentationType() == kFragmentPage; |
| return false; |
| } |
| // The properties break-after, break-before and break-inside may all specify |
| // avoid* values. break-after and break-before use EBreakBetween, and |
| // break-inside uses EBreakInside. |
| template bool CORE_TEMPLATE_EXPORT IsAvoidBreakValue(const NGConstraintSpace&, |
| EBreakBetween); |
| template bool CORE_TEMPLATE_EXPORT IsAvoidBreakValue(const NGConstraintSpace&, |
| EBreakInside); |
| |
| EBreakBetween CalculateBreakBetweenValue(NGLayoutInputNode child, |
| const NGLayoutResult& layout_result, |
| const NGBoxFragmentBuilder& builder) { |
| if (child.IsInline()) |
| return EBreakBetween::kAuto; |
| EBreakBetween break_before = JoinFragmentainerBreakValues( |
| child.Style().BreakBefore(), layout_result.InitialBreakBefore()); |
| return builder.JoinedBreakBetweenValue(break_before); |
| } |
| |
| NGBreakAppeal CalculateBreakAppealBefore(const NGConstraintSpace& space, |
| NGLayoutInputNode child, |
| const NGLayoutResult& layout_result, |
| const NGBoxFragmentBuilder& builder, |
| bool has_container_separation) { |
| if (!has_container_separation) { |
| // This is not a valid break point. If there's no container separation, it |
| // means that we're breaking before the first piece of in-flow content |
| // inside this block, even if it's not a valid class C break point [1]. We |
| // really don't want to break here, if we can find something better. |
| // |
| // [1] https://www.w3.org/TR/css-break-3/#possible-breaks |
| return kBreakAppealLastResort; |
| } |
| |
| EBreakBetween break_between = |
| CalculateBreakBetweenValue(child, layout_result, builder); |
| // If there's a break-{after,before}:avoid* involved at this breakpoint, |
| // its appeal will decrease. |
| if (IsAvoidBreakValue(space, break_between)) |
| return kBreakAppealViolatingBreakAvoid; |
| |
| return kBreakAppealPerfect; |
| } |
| |
| NGBreakAppeal CalculateBreakAppealInside(const NGConstraintSpace& space, |
| NGBlockNode child, |
| const NGLayoutResult& layout_result) { |
| if (layout_result.HasForcedBreak()) |
| return kBreakAppealPerfect; |
| const auto& physical_fragment = layout_result.PhysicalFragment(); |
| NGBreakAppeal appeal; |
| // If we actually broke, get the appeal from the break token. Otherwise, get |
| // the early break appeal. |
| if (const auto* block_break_token = |
| DynamicTo<NGBlockBreakToken>(physical_fragment.BreakToken())) |
| appeal = block_break_token->BreakAppeal(); |
| else |
| appeal = layout_result.EarlyBreakAppeal(); |
| |
| // We don't let break-inside:avoid affect the child's stored break appeal, but |
| // we rather handle it now, on the outside. The reason is that we want to be |
| // able to honor any 'avoid' values on break-before or break-after among the |
| // children of the child, even if we need to disregrard a break-inside:avoid |
| // rule on the child itself. This prevents us from violating more rules than |
| // necessary: if we need to break inside the child (even if it should be |
| // avoided), we'll at least break at the most appealing location inside. |
| if (appeal > kBreakAppealViolatingBreakAvoid && |
| IsAvoidBreakValue(space, child.Style().BreakInside())) |
| appeal = kBreakAppealViolatingBreakAvoid; |
| return appeal; |
| } |
| |
| void SetupSpaceBuilderForFragmentation(const NGConstraintSpace& parent_space, |
| const NGLayoutInputNode& child, |
| LayoutUnit fragmentainer_offset_delta, |
| NGConstraintSpaceBuilder* builder, |
| bool is_new_fc) { |
| DCHECK(parent_space.HasBlockFragmentation()); |
| |
| // If the child is truly unbreakable, it won't participate in block |
| // fragmentation. If it's too tall to fit, it will either overflow the |
| // fragmentainer or get brutally sliced into pieces (without looking for |
| // allowed breakpoints, since there are none, by definition), depending on |
| // fragmentation type (multicol vs. printing). We still need to perform block |
| // fragmentation inside inline nodes, though: While the line box itself is |
| // monolithic, there may be floats inside, which are fragmentable. |
| if (child.IsMonolithic() && !child.IsInline()) |
| return; |
| |
| builder->SetFragmentainerBlockSize(parent_space.FragmentainerBlockSize()); |
| builder->SetFragmentainerOffsetAtBfc(parent_space.FragmentainerOffsetAtBfc() + |
| fragmentainer_offset_delta); |
| builder->SetFragmentationType(parent_space.BlockFragmentationType()); |
| |
| if (parent_space.IsInColumnBfc() && !is_new_fc) |
| builder->SetIsInColumnBfc(); |
| } |
| |
| void SetupFragmentBuilderForFragmentation( |
| const NGConstraintSpace& space, |
| const NGBlockBreakToken* previous_break_token, |
| NGBoxFragmentBuilder* builder) { |
| builder->SetHasBlockFragmentation(); |
| builder->SetPreviousBreakToken(previous_break_token); |
| |
| // The whereabouts of our container's so far best breakpoint is none of our |
| // business, but we store its appeal, so that we don't look for breakpoints |
| // with lower appeal than that. |
| builder->SetBreakAppeal(space.EarlyBreakAppeal()); |
| |
| if (space.IsInitialColumnBalancingPass()) |
| builder->SetIsInitialColumnBalancingPass(); |
| |
| unsigned sequence_number = 0; |
| if (previous_break_token && !previous_break_token->IsBreakBefore()) { |
| sequence_number = previous_break_token->SequenceNumber() + 1; |
| builder->SetIsFirstForNode(false); |
| } |
| builder->SetSequenceNumber(sequence_number); |
| |
| builder->AdjustBorderScrollbarPaddingForFragmentation(previous_break_token); |
| } |
| |
| bool IsNodeFullyGrown(NGBlockNode node, |
| const NGConstraintSpace& space, |
| LayoutUnit current_total_block_size, |
| const NGBoxStrut& border_padding, |
| LayoutUnit inline_size) { |
| // Pass an "infinite" intrinsic size to see how the block-size is |
| // constrained. If it doesn't affect the block size, it means that the node |
| // cannot grow any further. |
| LayoutUnit max_block_size = ComputeBlockSizeForFragment( |
| space, node.Style(), border_padding, LayoutUnit::Max(), inline_size, |
| node.ShouldBeConsideredAsReplaced()); |
| DCHECK_GE(max_block_size, current_total_block_size); |
| return max_block_size == current_total_block_size; |
| } |
| |
| bool FinishFragmentation(NGBlockNode node, |
| const NGConstraintSpace& space, |
| LayoutUnit trailing_border_padding, |
| LayoutUnit space_left, |
| NGBoxFragmentBuilder* builder) { |
| const NGBlockBreakToken* previous_break_token = builder->PreviousBreakToken(); |
| LayoutUnit previously_consumed_block_size; |
| if (previous_break_token && !previous_break_token->IsBreakBefore()) |
| previously_consumed_block_size = previous_break_token->ConsumedBlockSize(); |
| bool is_past_end = |
| previous_break_token && previous_break_token->IsAtBlockEnd(); |
| |
| LayoutUnit fragments_total_block_size = builder->FragmentsTotalBlockSize(); |
| LayoutUnit desired_block_size = |
| fragments_total_block_size - previously_consumed_block_size; |
| |
| // Consumed block-size stored in the break tokens is always stretched to the |
| // fragmentainers. If this wasn't also the case for all previous fragments |
| // (because we reached the end of the node and were overflowing), we may end |
| // up with negative values here. |
| desired_block_size = desired_block_size.ClampNegativeToZero(); |
| |
| LayoutUnit intrinsic_block_size = builder->IntrinsicBlockSize(); |
| |
| LayoutUnit final_block_size = desired_block_size; |
| |
| if (builder->FoundColumnSpanner()) { |
| builder->SetDidBreakSelf(); |
| |
| // A break before a spanner is a forced break, and is thus "perfect". It |
| // need not be weighed against other possible break points. |
| builder->SetBreakAppeal(kBreakAppealPerfect); |
| } |
| |
| if (is_past_end) { |
| final_block_size = intrinsic_block_size = LayoutUnit(); |
| } else if (builder->FoundColumnSpanner()) { |
| // There's a column spanner (or more) inside. This means that layout got |
| // interrupted and thus hasn't reached the end of this block yet. We're |
| // going to resume inside this block when done with the spanner(s). This is |
| // true even if there is no column content siblings after the spanner(s). |
| // |
| // <div style="columns:2;"> |
| // <div id="container" style="height:100px;"> |
| // <div id="child" style="height:20px;"></div> |
| // <div style="column-span:all;"></div> |
| // </div> |
| // </div> |
| // |
| // We'll create fragments for #container both before and after the spanner. |
| // Before the spanner we'll create one for each column, each 10px tall |
| // (height of #child divided into 2 columns). After the spanner, there's no |
| // more content, but the specified height is 100px, so distribute what we |
| // haven't already consumed (100px - 20px = 80px) over two columns. We get |
| // two fragments for #container after the spanner, each 40px tall. |
| final_block_size = std::min(final_block_size, intrinsic_block_size) - |
| trailing_border_padding; |
| } else if (space_left != kIndefiniteSize && desired_block_size > space_left) { |
| // We're taller than what we have room for. We don't want to use more than |
| // |space_left|, but if the intrinsic block-size is larger than that, it |
| // means that there's something unbreakable (monolithic) inside (or we'd |
| // already have broken inside). We'll allow this to overflow the |
| // fragmentainer. |
| // |
| // TODO(mstensho): This is desired behavior for multicol, but not ideal for |
| // printing, where we'd prefer the unbreakable content to be sliced into |
| // different pages, lest it be clipped and lost. |
| // |
| // There is a last-resort breakpoint before trailing border and padding, so |
| // first check if we can break there and still make progress. |
| DCHECK_GE(intrinsic_block_size, trailing_border_padding); |
| DCHECK_GE(desired_block_size, trailing_border_padding); |
| |
| LayoutUnit subtractable_border_padding; |
| if (desired_block_size > trailing_border_padding) |
| subtractable_border_padding = trailing_border_padding; |
| |
| final_block_size = |
| std::min(desired_block_size - subtractable_border_padding, |
| std::max(space_left, |
| intrinsic_block_size - subtractable_border_padding)); |
| |
| // We'll only need to break inside if we need more space after any |
| // unbreakable content that we may have forcefully fitted here. |
| if (final_block_size < desired_block_size) |
| builder->SetDidBreakSelf(); |
| } |
| |
| LogicalBoxSides sides; |
| // If this isn't the first fragment, omit the block-start border. |
| if (previously_consumed_block_size) |
| sides.block_start = false; |
| // If this isn't the last fragment with same-flow content, omit the block-end |
| // border. If something overflows the node, we'll keep on creating empty |
| // fragments to contain the overflow (which establishes a parallel flow), but |
| // those fragments should make no room (nor paint) block-end border/paddding. |
| if (builder->DidBreakSelf() || is_past_end) |
| sides.block_end = false; |
| builder->SetSidesToInclude(sides); |
| |
| builder->SetConsumedBlockSize(previously_consumed_block_size + |
| final_block_size); |
| builder->SetFragmentBlockSize(final_block_size); |
| |
| if (builder->FoundColumnSpanner()) |
| return true; |
| |
| if (space_left == kIndefiniteSize) { |
| // We don't know how space is available (initial column balancing pass), so |
| // we won't break. |
| builder->SetIsAtBlockEnd(); |
| return true; |
| } |
| |
| if (builder->HasChildBreakInside()) { |
| // We broke before or inside one of our children. Even if we fit within the |
| // remaining space, and even if the child involved in the break were to be |
| // in a parallel flow, we still need to prepare a break token for this node, |
| // so that we can resume layout of its broken or unstarted children in the |
| // next fragmentainer. |
| // |
| // If we're at the end of the node, we need to mark the outgoing break token |
| // as such. This is a way for the parent algorithm to determine whether we |
| // need to insert a break there, or whether we may continue with any sibling |
| // content. If we are allowed to continue, while there's still child content |
| // left to be laid out, said content ends up in a parallel flow. |
| // https://www.w3.org/TR/css-break-3/#parallel-flows |
| // |
| // TODO(mstensho): The spec actually says that we enter a parallel flow once |
| // we're past the block-end *content edge*, but here we're checking against |
| // the *border edge* instead. Does it matter? |
| if (is_past_end) { |
| builder->SetIsAtBlockEnd(); |
| // We entered layout already at the end of the block (but with overflowing |
| // children). So we should take up no more space on our own. |
| DCHECK_EQ(desired_block_size, LayoutUnit()); |
| } else if (desired_block_size <= space_left) { |
| // We have room for the calculated block-size in the current |
| // fragmentainer, but we need to figure out whether this node is going to |
| // produce more non-zero block-size fragments or not. |
| // |
| // If the block-size is constrained / fixed (in which case |
| // IsNodeFullyGrown() will return true now), we know that we're at the |
| // end. If block-size is unconstrained (or at least allowed to grow a bit |
| // more), we're only at the end if no in-flow content inside broke. |
| |
| bool was_broken_by_child = builder->HasInflowChildBreakInside(); |
| if (!was_broken_by_child && space.IsNewFormattingContext()) |
| was_broken_by_child = builder->HasFloatBreakInside(); |
| |
| if (!was_broken_by_child || |
| IsNodeFullyGrown(node, space, fragments_total_block_size, |
| builder->BorderPadding(), |
| builder->InitialBorderBoxSize().inline_size)) |
| builder->SetIsAtBlockEnd(); |
| |
| // If we're going to break just because of floats or out-of-flow child |
| // breaks, no break appeal will have been recorded so far, since we only |
| // update the appeal at same-flow breakpoints, and since we start off by |
| // assuming the lowest appeal, upgrade it now. There's nothing here that |
| // makes breaking inside less appealing than perfect. |
| if (!builder->HasInflowChildBreakInside()) |
| builder->SetBreakAppeal(kBreakAppealPerfect); |
| } |
| |
| if (builder->IsAtBlockEnd()) { |
| // This node is to be resumed in the next fragmentainer. Make sure that |
| // consumed block-size includes the entire remainder of the fragmentainer. |
| // The fragment will normally take up all that space, but not if we've |
| // reached the end of the node (and we are breaking because of |
| // overflow). We include the entire fragmentainer in consumed block-size |
| // in order to write offsets correctly back to legacy layout. |
| builder->SetConsumedBlockSize(previously_consumed_block_size + |
| std::max(final_block_size, space_left)); |
| } |
| |
| return true; |
| } |
| |
| if (desired_block_size > space_left) { |
| // No child inside broke, but we're too tall to fit. |
| NGBreakAppeal break_appeal = kBreakAppealPerfect; |
| if (!previously_consumed_block_size) { |
| // This is the first fragment generated for the node. Avoid breaking |
| // inside block-start border, scrollbar and padding, if possible. No valid |
| // breakpoints there. |
| const NGFragmentGeometry& geometry = builder->InitialFragmentGeometry(); |
| LayoutUnit block_start_unbreakable_space = |
| geometry.border.block_start + geometry.scrollbar.block_start + |
| geometry.padding.block_start; |
| if (space_left < block_start_unbreakable_space) |
| break_appeal = kBreakAppealLastResort; |
| } |
| if (space.BlockFragmentationType() == kFragmentColumn && |
| !space.IsInitialColumnBalancingPass()) |
| builder->PropagateSpaceShortage(desired_block_size - space_left); |
| if (desired_block_size <= intrinsic_block_size) { |
| // We only want to break inside if there's a valid class C breakpoint [1]. |
| // That is, we need a non-zero gap between the last child (outer block-end |
| // edge) and this container (inner block-end edge). We've just found that |
| // not to be the case. If we have found a better early break, we should |
| // break there. Otherwise mark the break as unappealing, as breaking here |
| // means that we're going to break inside the block-end padding or border, |
| // or right before them. No valid breakpoints there. |
| // |
| // [1] https://www.w3.org/TR/css-break-3/#possible-breaks |
| if (builder->HasEarlyBreak()) |
| return false; |
| break_appeal = kBreakAppealLastResort; |
| } |
| builder->SetBreakAppeal(break_appeal); |
| return true; |
| } |
| |
| // The end of the block fits in the current fragmentainer. |
| builder->SetIsAtBlockEnd(); |
| return true; |
| } |
| |
| NGBreakStatus BreakBeforeChildIfNeeded(const NGConstraintSpace& space, |
| NGLayoutInputNode child, |
| const NGLayoutResult& layout_result, |
| LayoutUnit fragmentainer_block_offset, |
| bool has_container_separation, |
| NGBoxFragmentBuilder* builder) { |
| DCHECK(space.HasBlockFragmentation()); |
| |
| if (has_container_separation) { |
| EBreakBetween break_between = |
| CalculateBreakBetweenValue(child, layout_result, *builder); |
| if (IsForcedBreakValue(space, break_between)) { |
| BreakBeforeChild(space, child, layout_result, fragmentainer_block_offset, |
| kBreakAppealPerfect, /* is_forced_break */ true, |
| builder); |
| return NGBreakStatus::kBrokeBefore; |
| } |
| } |
| |
| NGBreakAppeal appeal_before = CalculateBreakAppealBefore( |
| space, child, layout_result, *builder, has_container_separation); |
| |
| // Attempt to move past the break point, and if we can do that, also assess |
| // the appeal of breaking there, even if we didn't. |
| if (MovePastBreakpoint(space, child, layout_result, |
| fragmentainer_block_offset, appeal_before, builder)) |
| return NGBreakStatus::kContinue; |
| |
| // Breaking inside the child isn't appealing, and we're out of space. Figure |
| // out where to insert a soft break. It will either be before this child, or |
| // before an earlier sibling, if there's a more appealing breakpoint there. |
| if (!AttemptSoftBreak(space, child, layout_result, fragmentainer_block_offset, |
| appeal_before, builder)) |
| return NGBreakStatus::kNeedsEarlierBreak; |
| |
| return NGBreakStatus::kBrokeBefore; |
| } |
| |
| void BreakBeforeChild(const NGConstraintSpace& space, |
| NGLayoutInputNode child, |
| const NGLayoutResult& layout_result, |
| LayoutUnit fragmentainer_block_offset, |
| base::Optional<NGBreakAppeal> appeal, |
| bool is_forced_break, |
| NGBoxFragmentBuilder* builder) { |
| #if DCHECK_IS_ON() |
| if (layout_result.Status() == NGLayoutResult::kSuccess) { |
| // In order to successfully break before a node, this has to be its first |
| // fragment. |
| const auto& physical_fragment = layout_result.PhysicalFragment(); |
| DCHECK(!physical_fragment.IsBox() || |
| To<NGPhysicalBoxFragment>(physical_fragment).IsFirstForNode()); |
| } |
| #endif |
| |
| // Report space shortage. Note that we're not doing this for line boxes here |
| // (only blocks), because line boxes need handle it in their own way (due to |
| // how we implement widows). |
| if (child.IsBlock() && space.HasKnownFragmentainerBlockSize()) { |
| PropagateSpaceShortage(space, layout_result, fragmentainer_block_offset, |
| builder); |
| } |
| |
| // If the fragmentainer block-size is unknown, we have no reason to insert |
| // soft breaks. |
| DCHECK(is_forced_break || space.HasKnownFragmentainerBlockSize()); |
| |
| // We'll drop the fragment (if any) on the floor and retry at the start of the |
| // next fragmentainer. |
| builder->AddBreakBeforeChild(child, appeal, is_forced_break); |
| } |
| |
| void PropagateSpaceShortage(const NGConstraintSpace& space, |
| const NGLayoutResult& layout_result, |
| LayoutUnit fragmentainer_block_offset, |
| NGBoxFragmentBuilder* builder) { |
| // Space shortage is only reported for soft breaks, and they can only exist if |
| // we know the fragmentainer block-size. |
| DCHECK(space.HasKnownFragmentainerBlockSize()); |
| |
| // Only multicol cares about space shortage. |
| if (space.BlockFragmentationType() != kFragmentColumn) |
| return; |
| |
| LayoutUnit space_shortage; |
| if (layout_result.MinimalSpaceShortage() == LayoutUnit::Max()) { |
| // Calculate space shortage: Figure out how much more space would have been |
| // sufficient to make the child fragment fit right here in the current |
| // fragmentainer. If layout aborted, though, we can't propagate anything. |
| if (layout_result.Status() != NGLayoutResult::kSuccess) |
| return; |
| NGFragment fragment(space.GetWritingDirection(), |
| layout_result.PhysicalFragment()); |
| space_shortage = fragmentainer_block_offset + fragment.BlockSize() - |
| space.FragmentainerBlockSize(); |
| } else { |
| // However, if space shortage was reported inside the child, use that. If we |
| // broke inside the child, we didn't complete layout, so calculating space |
| // shortage for the child as a whole would be impossible and pointless. |
| space_shortage = layout_result.MinimalSpaceShortage(); |
| } |
| |
| // TODO(mstensho): Turn this into a DCHECK, when the engine is ready for |
| // it. Space shortage should really be positive here, or we might ultimately |
| // fail to stretch the columns (column balancing). |
| if (space_shortage > LayoutUnit()) |
| builder->PropagateSpaceShortage(space_shortage); |
| } |
| |
| bool MovePastBreakpoint(const NGConstraintSpace& space, |
| NGLayoutInputNode child, |
| const NGLayoutResult& layout_result, |
| LayoutUnit fragmentainer_block_offset, |
| NGBreakAppeal appeal_before, |
| NGBoxFragmentBuilder* builder) { |
| if (layout_result.Status() != NGLayoutResult::kSuccess) { |
| // Layout aborted - no fragment was produced. There's nothing to move |
| // past. We need to break before. |
| DCHECK_EQ(layout_result.Status(), NGLayoutResult::kOutOfFragmentainerSpace); |
| return false; |
| } |
| |
| const auto& physical_fragment = layout_result.PhysicalFragment(); |
| NGFragment fragment(space.GetWritingDirection(), physical_fragment); |
| |
| if (!space.HasKnownFragmentainerBlockSize()) { |
| if (space.IsInitialColumnBalancingPass() && builder) { |
| if (child.IsMonolithic() || |
| (child.IsBlock() && |
| IsAvoidBreakValue(space, child.Style().BreakInside()))) { |
| // If this is the initial column balancing pass, attempt to make the |
| // column block-size at least as large as the tallest piece of |
| // monolithic content and/or block with break-inside:avoid. |
| PropagateUnbreakableBlockSize(fragment.BlockSize(), |
| fragmentainer_block_offset, builder); |
| } |
| } |
| // We only care about soft breaks if we have a fragmentainer block-size. |
| // During column balancing this may be unknown. |
| return true; |
| } |
| |
| const auto* break_token = |
| DynamicTo<NGBlockBreakToken>(physical_fragment.BreakToken()); |
| |
| LayoutUnit space_left = |
| FragmentainerCapacity(space) - fragmentainer_block_offset; |
| |
| // If we haven't used any space at all in the fragmentainer yet, we cannot |
| // break before this child, or there'd be no progress. We'd risk creating an |
| // infinite number of fragmentainers without putting any content into them. |
| bool refuse_break_before = space_left >= FragmentainerCapacity(space); |
| |
| // If the child starts past the end of the fragmentainer (probably due to a |
| // block-start margin), we must break before it. |
| bool must_break_before = false; |
| if (space_left < LayoutUnit()) { |
| must_break_before = true; |
| } else if (space_left == LayoutUnit()) { |
| // If the child starts exactly at the end, we'll allow the child here if the |
| // fragment contains the block-end of the child, or if it's a column |
| // spanner. Otherwise we have to break before it. We don't want empty |
| // fragments with nothing useful inside, if it's to be resumed in the next |
| // fragmentainer. |
| must_break_before = !layout_result.ColumnSpanner() && break_token && |
| !break_token->IsAtBlockEnd(); |
| } |
| if (must_break_before) { |
| DCHECK(!refuse_break_before); |
| return false; |
| } |
| |
| if (break_token) { |
| // The block child broke inside. We now need to decide whether to keep that |
| // break, or if it would be better to break before it. |
| NGBreakAppeal appeal_inside = CalculateBreakAppealInside( |
| space, To<NGBlockNode>(child), layout_result); |
| // Allow breaking inside if it has the same appeal or higher than breaking |
| // before or breaking earlier. Also, if breaking before is impossible, break |
| // inside regardless of appeal. |
| bool want_break_inside = refuse_break_before; |
| if (!want_break_inside && appeal_inside >= appeal_before) { |
| if (!builder || !builder->HasEarlyBreak() || |
| appeal_inside >= builder->BreakAppeal()) |
| want_break_inside = true; |
| } |
| if (want_break_inside) { |
| if (builder) |
| builder->SetBreakAppeal(appeal_inside); |
| return true; |
| } |
| } else if (refuse_break_before || fragment.BlockSize() <= space_left) { |
| // The child either fits, or we are not allowed to break. So we can move |
| // past this breakpoint. |
| if (child.IsBlock() && builder) { |
| // We're tentatively not going to break before or inside this child, but |
| // we'll check the appeal of breaking there anyway. It may be the best |
| // breakpoint we'll ever find. (Note that we only do this for block |
| // children, since, when it comes to inline layout, we first need to lay |
| // out all the line boxes, so that we know what do to in order to honor |
| // orphans and widows, if at all possible.) |
| UpdateEarlyBreakAtBlockChild(space, To<NGBlockNode>(child), layout_result, |
| appeal_before, builder); |
| } |
| return true; |
| } |
| |
| // We don't want to break inside, so we should attempt to break before. |
| return false; |
| } |
| |
| void UpdateEarlyBreakAtBlockChild(const NGConstraintSpace& space, |
| NGBlockNode child, |
| const NGLayoutResult& layout_result, |
| NGBreakAppeal appeal_before, |
| NGBoxFragmentBuilder* builder) { |
| // If the child already broke, it's a little too late to look for breakpoints. |
| DCHECK(!layout_result.PhysicalFragment().BreakToken()); |
| |
| // See if there's a good breakpoint inside the child. |
| NGBreakAppeal appeal_inside = kBreakAppealLastResort; |
| if (scoped_refptr<const NGEarlyBreak> breakpoint = |
| layout_result.GetEarlyBreak()) { |
| appeal_inside = CalculateBreakAppealInside(space, child, layout_result); |
| if (builder->BreakAppeal() <= appeal_inside) { |
| // Found a good breakpoint inside the child. Add the child to the early |
| // break container chain, and store it. |
| auto parent_break = base::AdoptRef(new NGEarlyBreak(child, breakpoint)); |
| builder->SetEarlyBreak(parent_break, appeal_inside); |
| } |
| } |
| |
| // Breaking before isn't better if it's less appealing than what we already |
| // have (obviously), and also not if it has the same appeal as the break |
| // location inside the child that we just found (when the appeal is the same, |
| // whatever takes us further wins). |
| if (appeal_before < builder->BreakAppeal() || appeal_before == appeal_inside) |
| return; |
| |
| builder->SetEarlyBreak(base::AdoptRef(new NGEarlyBreak(child)), |
| appeal_before); |
| } |
| |
| bool AttemptSoftBreak(const NGConstraintSpace& space, |
| NGLayoutInputNode child, |
| const NGLayoutResult& layout_result, |
| LayoutUnit fragmentainer_block_offset, |
| NGBreakAppeal appeal_before, |
| NGBoxFragmentBuilder* builder) { |
| // if there's a breakpoint with higher appeal among earlier siblings, we need |
| // to abort and re-layout to that breakpoint. |
| if (builder->HasEarlyBreak() && builder->BreakAppeal() > appeal_before) { |
| // Found a better place to break. Before aborting, calculate and report |
| // space shortage from where we'd actually break. |
| PropagateSpaceShortage(space, layout_result, fragmentainer_block_offset, |
| builder); |
| return false; |
| } |
| |
| // Break before the child. Note that there may be a better break further up |
| // with higher appeal (but it's too early to tell), in which case this |
| // breakpoint will be replaced. |
| BreakBeforeChild(space, child, layout_result, fragmentainer_block_offset, |
| appeal_before, /* is_forced_break */ false, builder); |
| return true; |
| } |
| |
| NGConstraintSpace CreateConstraintSpaceForColumns( |
| const NGConstraintSpace& parent_space, |
| LogicalSize column_size, |
| LogicalSize percentage_resolution_size, |
| bool allow_discard_start_margin, |
| bool balance_columns) { |
| NGConstraintSpaceBuilder space_builder( |
| parent_space, parent_space.GetWritingDirection(), /* is_new_fc */ true); |
| space_builder.SetAvailableSize(column_size); |
| space_builder.SetPercentageResolutionSize(percentage_resolution_size); |
| space_builder.SetStretchInlineSizeIfAuto(true); |
| space_builder.SetFragmentationType(kFragmentColumn); |
| space_builder.SetFragmentainerBlockSize(column_size.block_size); |
| space_builder.SetIsAnonymous(true); |
| space_builder.SetIsInColumnBfc(); |
| if (balance_columns) |
| space_builder.SetIsInsideBalancedColumns(); |
| if (allow_discard_start_margin) { |
| // Unless it's the first column in the multicol container, or the first |
| // column after a spanner, margins at fragmentainer boundaries should be |
| // eaten and truncated to zero. Note that this doesn't apply to margins at |
| // forced breaks, but we'll deal with those when we get to them. Set up a |
| // margin strut that eats all leading adjacent margins. |
| space_builder.SetDiscardingMarginStrut(); |
| } |
| |
| space_builder.SetBaselineAlgorithmType(parent_space.BaselineAlgorithmType()); |
| |
| return space_builder.ToConstraintSpace(); |
| } |
| |
| NGBoxFragmentBuilder CreateContainerBuilderForMulticol( |
| const NGBlockNode& multicol, |
| const NGConstraintSpace& space, |
| const NGFragmentGeometry& fragment_geometry) { |
| const ComputedStyle* style = &multicol.Style(); |
| NGBoxFragmentBuilder multicol_container_builder(multicol, style, &space, |
| style->GetWritingDirection()); |
| multicol_container_builder.SetIsNewFormattingContext(true); |
| multicol_container_builder.SetInitialFragmentGeometry(fragment_geometry); |
| multicol_container_builder.SetIsBlockFragmentationContextRoot(); |
| |
| return multicol_container_builder; |
| } |
| |
| NGConstraintSpace CreateConstraintSpaceForMulticol( |
| const NGBlockNode& multicol) { |
| WritingDirectionMode writing_direction_mode = |
| multicol.Style().GetWritingDirection(); |
| NGConstraintSpaceBuilder space_builder( |
| writing_direction_mode.GetWritingMode(), writing_direction_mode, |
| /* is_new_fc */ true); |
| return space_builder.ToConstraintSpace(); |
| } |
| |
| } // namespace blink |