blob: ccfccce01a6220ce2a97d42aa31852e4d1f9c7a9 [file] [log] [blame]
// Copyright 2016 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 "core/layout/ng/inline/ng_inline_layout_algorithm.h"
#include "core/layout/ng/inline/ng_bidi_paragraph.h"
#include "core/layout/ng/inline/ng_inline_break_token.h"
#include "core/layout/ng/inline/ng_inline_node.h"
#include "core/layout/ng/inline/ng_line_box_fragment.h"
#include "core/layout/ng/inline/ng_line_box_fragment_builder.h"
#include "core/layout/ng/inline/ng_line_breaker.h"
#include "core/layout/ng/inline/ng_text_fragment.h"
#include "core/layout/ng/inline/ng_text_fragment_builder.h"
#include "core/layout/ng/layout_ng_block_flow.h"
#include "core/layout/ng/ng_block_layout_algorithm.h"
#include "core/layout/ng/ng_box_fragment.h"
#include "core/layout/ng/ng_constraint_space.h"
#include "core/layout/ng/ng_constraint_space_builder.h"
#include "core/layout/ng/ng_floats_utils.h"
#include "core/layout/ng/ng_fragment_builder.h"
#include "core/layout/ng/ng_layout_result.h"
#include "core/layout/ng/ng_length_utils.h"
#include "core/layout/ng/ng_space_utils.h"
#include "core/layout/ng/ng_unpositioned_float.h"
#include "core/style/ComputedStyle.h"
#include "platform/text/BidiRunList.h"
namespace blink {
namespace {
NGLogicalOffset GetOriginPointForFloats(
const NGLogicalOffset& container_bfc_offset,
LayoutUnit content_size) {
NGLogicalOffset origin_point = container_bfc_offset;
origin_point.block_offset += content_size;
return origin_point;
}
} // namespace
NGInlineLayoutAlgorithm::NGInlineLayoutAlgorithm(
NGInlineNode inline_node,
NGConstraintSpace* space,
NGInlineBreakToken* break_token)
: NGLayoutAlgorithm(inline_node, space, break_token),
is_horizontal_writing_mode_(
blink::IsHorizontalWritingMode(space->WritingMode())),
disallow_first_line_rules_(false),
space_builder_(space) {
container_builder_.MutableUnpositionedFloats() = space->UnpositionedFloats();
// TODO(crbug.com/716930): We may be an empty LayoutInline due to splitting.
// Only resolve our BFC offset if we know that we are non-empty as we may
// need to pass through our margin strut.
if (!inline_node.Items().IsEmpty()) {
LayoutUnit bfc_block_offset = ConstraintSpace().BfcOffset().block_offset;
bfc_block_offset += ConstraintSpace().MarginStrut().Sum();
MaybeUpdateFragmentBfcOffset(ConstraintSpace(), bfc_block_offset,
&container_builder_);
PositionPendingFloats(bfc_block_offset, &container_builder_,
MutableConstraintSpace());
}
if (!is_horizontal_writing_mode_)
baseline_type_ = FontBaseline::kIdeographicBaseline;
border_and_padding_ = ComputeBorders(ConstraintSpace(), Style()) +
ComputePadding(ConstraintSpace(), Style());
if (break_token) {
// If a break_token is given, we're re-starting layout for 2nd or later
// lines, and that the first line we create should not use the first line
// rules.
DCHECK(!break_token->IsFinished());
DCHECK(break_token->TextOffset() || break_token->ItemIndex());
disallow_first_line_rules_ = true;
} else {
auto& engine = Node().GetLayoutObject()->GetDocument().GetStyleEngine();
disallow_first_line_rules_ = !engine.UsesFirstLineRules();
}
FindNextLayoutOpportunity();
}
bool NGInlineLayoutAlgorithm::IsFirstLine() const {
return !disallow_first_line_rules_ && container_builder_.Children().IsEmpty();
}
const ComputedStyle& NGInlineLayoutAlgorithm::FirstLineStyle() const {
return Node().GetLayoutObject()->FirstLineStyleRef();
}
const ComputedStyle& NGInlineLayoutAlgorithm::LineStyle() const {
return IsFirstLine() ? FirstLineStyle() : Style();
}
LayoutUnit NGInlineLayoutAlgorithm::AvailableWidth() const {
return current_opportunity_.InlineSize();
}
// The offset of 'line-left' side.
// https://drafts.csswg.org/css-writing-modes/#line-left
LayoutUnit NGInlineLayoutAlgorithm::LogicalLeftOffset() const {
// TODO(kojii): We need to convert 'line start' to 'line left'. They're
// different in RTL. Maybe there are more where start and left are misused.
return current_opportunity_.InlineStartOffset() -
ConstraintSpace().BfcOffset().inline_offset;
}
bool NGInlineLayoutAlgorithm::CreateLine(
NGInlineItemResults* item_results,
RefPtr<NGInlineBreakToken> break_token) {
if (Node().IsBidiEnabled())
BidiReorder(item_results);
if (!PlaceItems(item_results, break_token))
return false;
// Prepare for the next line.
NGLogicalOffset origin_point =
GetOriginPointForFloats(ContainerBfcOffset(), content_size_);
PositionPendingFloats(origin_point.block_offset, &container_builder_,
MutableConstraintSpace());
FindNextLayoutOpportunity();
return true;
}
void NGInlineLayoutAlgorithm::BidiReorder(NGInlineItemResults* line_items) {
// TODO(kojii): UAX#9 L1 is not supported yet. Supporting L1 may change
// embedding levels of parts of runs, which requires to split items.
// http://unicode.org/reports/tr9/#L1
// BidiResolver does not support L1 crbug.com/316409.
// Create a list of chunk indices in the visual order.
// ICU |ubidi_getVisualMap()| works for a run of characters. Since we can
// handle the direction of each run, we use |ubidi_reorderVisual()| to reorder
// runs instead of characters.
Vector<UBiDiLevel, 32> levels;
levels.ReserveInitialCapacity(line_items->size());
const Vector<NGInlineItem>& items = Node().Items();
for (const auto& item_result : *line_items)
levels.push_back(items[item_result.item_index].BidiLevel());
Vector<int32_t, 32> indices_in_visual_order(line_items->size());
NGBidiParagraph::IndicesInVisualOrder(levels, &indices_in_visual_order);
// Reorder to the visual order.
NGInlineItemResults line_items_in_visual_order(line_items->size());
for (unsigned visual_index = 0; visual_index < indices_in_visual_order.size();
visual_index++) {
unsigned logical_index = indices_in_visual_order[visual_index];
line_items_in_visual_order[visual_index] =
std::move((*line_items)[logical_index]);
}
// Keep Open before Close in the visual order.
HashMap<LayoutObject*, unsigned> first_index;
for (unsigned i = 0; i < line_items_in_visual_order.size(); i++) {
NGInlineItemResult& item_result = line_items_in_visual_order[i];
const NGInlineItem& item = items[item_result.item_index];
if (item.Type() != NGInlineItem::kOpenTag &&
item.Type() != NGInlineItem::kCloseTag) {
continue;
}
auto result = first_index.insert(item.GetLayoutObject(), i);
if (!result.is_new_entry && item.Type() == NGInlineItem::kOpenTag) {
std::swap(line_items_in_visual_order[i],
line_items_in_visual_order[result.stored_value->value]);
}
}
line_items->swap(line_items_in_visual_order);
}
// TODO(glebl): Add the support of clearance for inline floats.
void NGInlineLayoutAlgorithm::LayoutAndPositionFloat(
LayoutUnit end_position,
LayoutObject* layout_object) {
NGBlockNode node(ToLayoutBox(layout_object));
NGLogicalOffset origin_offset =
GetOriginPointForFloats(ContainerBfcOffset(), content_size_);
const ComputedStyle& float_style = node.Style();
NGBoxStrut margins = ComputeMargins(ConstraintSpace(), float_style,
ConstraintSpace().WritingMode(),
ConstraintSpace().Direction());
// TODO(ikilpatrick): Add support for float break tokens inside an inline
// layout context.
RefPtr<NGUnpositionedFloat> unpositioned_float = NGUnpositionedFloat::Create(
current_opportunity_.size, ConstraintSpace().PercentageResolutionSize(),
origin_offset, ContainerBfcOffset(), margins, node,
/* break_token */ nullptr);
unpositioned_float->parent_bfc_block_offset =
ContainerBfcOffset().block_offset;
LayoutUnit inline_size = ComputeInlineSizeForUnpositionedFloat(
MutableConstraintSpace(), unpositioned_float.Get());
bool float_does_not_fit = end_position + inline_size + margins.InlineSum() >
current_opportunity_.InlineSize();
// Check if we already have a pending float. That's because a float cannot be
// higher than any block or floated box generated before.
if (!container_builder_.UnpositionedFloats().IsEmpty() ||
float_does_not_fit) {
container_builder_.AddUnpositionedFloat(unpositioned_float);
} else {
container_builder_.AddPositionedFloat(
PositionFloat(unpositioned_float.Get(), MutableConstraintSpace()));
FindNextLayoutOpportunity();
}
}
bool NGInlineLayoutAlgorithm::PlaceItems(
NGInlineItemResults* line_items,
RefPtr<NGInlineBreakToken> break_token) {
const Vector<NGInlineItem>& items = Node().Items();
const ComputedStyle& line_style = LineStyle();
NGLineHeightMetrics line_metrics(line_style, baseline_type_);
NGLineHeightMetrics line_metrics_with_leading = line_metrics;
line_metrics_with_leading.AddLeading(line_style.ComputedLineHeightAsFixed());
NGLineBoxFragmentBuilder line_box(Node());
line_box.SetWritingMode(ConstraintSpace().WritingMode());
// Compute heights of all inline items by placing the dominant baseline at 0.
// The baseline is adjusted after the height of the line box is computed.
NGTextFragmentBuilder text_builder(Node());
NGInlineBoxState* box =
box_states_.OnBeginPlaceItems(&LineStyle(), baseline_type_);
// Place items from line-left to line-right along with the baseline.
// Items are already bidi-reordered to the visual order.
LayoutUnit position;
for (auto& item_result : *line_items) {
const NGInlineItem& item = items[item_result.item_index];
if (item.Type() == NGInlineItem::kText ||
item.Type() == NGInlineItem::kControl) {
DCHECK(item.GetLayoutObject()->IsText());
DCHECK(!box->text_metrics.IsEmpty());
text_builder.SetSize(
{item_result.inline_size, box->text_metrics.LineHeight()});
// Take all used fonts into account if 'line-height: normal'.
if (box->include_used_fonts && item.Type() == NGInlineItem::kText) {
box->AccumulateUsedFonts(item, item_result.start_offset,
item_result.end_offset, baseline_type_);
}
RefPtr<NGPhysicalTextFragment> text_fragment =
text_builder.ToTextFragment(item_result.item_index,
item_result.start_offset,
item_result.end_offset);
line_box.AddChild(std::move(text_fragment), {position, box->text_top});
} else if (item.Type() == NGInlineItem::kOpenTag) {
box = box_states_.OnOpenTag(item, &line_box);
// Compute text metrics for all inline boxes since even empty inlines
// influence the line height.
// https://drafts.csswg.org/css2/visudet.html#line-height
box->ComputeTextMetrics(*item.Style(), baseline_type_);
text_builder.SetDirection(box->style->Direction());
// TODO(kojii): We may need more conditions to create box fragments.
if (item.Style()->HasBoxDecorationBackground()) {
// TODO(kojii): These are once computed in NGLineBreaker. Should copy to
// NGInlineItemResult to reuse here.
NGBoxStrut borders = ComputeBorders(*constraint_space_, *item.Style());
NGBoxStrut paddings = ComputePadding(*constraint_space_, *item.Style());
// TODO(kojii): Set paint edges.
box->SetNeedsBoxFragment(position,
borders.block_start + paddings.block_start,
borders.BlockSum() + paddings.BlockSum());
}
} else if (item.Type() == NGInlineItem::kCloseTag) {
position += item_result.inline_size;
box = box_states_.OnCloseTag(item, &line_box, box, baseline_type_,
position);
continue;
} else if (item.Type() == NGInlineItem::kAtomicInline) {
box = PlaceAtomicInline(item, &item_result, position, &line_box,
&text_builder);
} else if (item.Type() == NGInlineItem::kOutOfFlowPositioned) {
// TODO(layout-dev): Report the correct static position for the out of
// flow descendant. We can't do this here yet as it doesn't know the
// size of the line box.
container_builder_.AddOutOfFlowDescendant(
// Absolute positioning blockifies the box's display type.
// https://drafts.csswg.org/css-display/#transformations
NGBlockNode(ToLayoutBox(item.GetLayoutObject())),
NGStaticPosition::Create(ConstraintSpace().WritingMode(),
ConstraintSpace().Direction(),
NGPhysicalOffset()));
continue;
} else {
continue;
}
position += item_result.inline_size;
}
if (line_box.Children().IsEmpty()) {
return true; // The line was empty.
}
box_states_.OnEndPlaceItems(&line_box, baseline_type_, position);
// The baselines are always placed at pixel boundaries. Not doing so results
// in incorrect layout of text decorations, most notably underlines.
LayoutUnit baseline = content_size_ + line_box.Metrics().ascent;
baseline = LayoutUnit(baseline.Round());
// Check if the line fits into the constraint space in block direction.
LayoutUnit line_bottom = baseline + line_box.Metrics().descent;
if (!container_builder_.Children().IsEmpty() &&
ConstraintSpace().AvailableSize().block_size != NGSizeIndefinite &&
line_bottom > ConstraintSpace().AvailableSize().block_size) {
return false;
}
line_box.SetBreakToken(std::move(break_token));
// TODO(kojii): Implement flipped line (vertical-lr). In this case, line_top
// and block_start do not match.
// Up until this point, children are placed so that the dominant baseline is
// at 0. Move them to the final baseline position, and set the logical top of
// the line box to the line top.
line_box.MoveChildrenInBlockDirection(baseline);
LayoutUnit inline_size = position;
NGLogicalOffset offset(LogicalLeftOffset(),
baseline - box_states_.LineBoxState().metrics.ascent);
ApplyTextAlign(&offset.inline_offset, inline_size,
current_opportunity_.size.inline_size);
line_box.SetInlineSize(inline_size);
container_builder_.AddChild(line_box.ToLineBoxFragment(), offset);
max_inline_size_ = std::max(max_inline_size_, inline_size);
content_size_ = line_bottom;
return true;
}
// TODO(kojii): Currently, this function does not change item_result, but
// when NG paint is enabled, this will std::move() the LayoutResult.
NGInlineBoxState* NGInlineLayoutAlgorithm::PlaceAtomicInline(
const NGInlineItem& item,
NGInlineItemResult* item_result,
LayoutUnit position,
NGLineBoxFragmentBuilder* line_box,
NGTextFragmentBuilder* text_builder) {
DCHECK(item_result->layout_result);
NGInlineBoxState* box = box_states_.OnOpenTag(item, line_box);
// For replaced elements, inline-block elements, and inline-table elements,
// the height is the height of their margin box.
// https://drafts.csswg.org/css2/visudet.html#line-height
NGBoxFragment fragment(
ConstraintSpace().WritingMode(),
ToNGPhysicalBoxFragment(
item_result->layout_result->PhysicalFragment().Get()));
LayoutUnit block_size =
fragment.BlockSize() + item_result->margins.BlockSum();
// TODO(kojii): Add baseline position to NGPhysicalFragment.
LayoutBox* layout_box = ToLayoutBox(item.GetLayoutObject());
LineDirectionMode line_direction_mode =
IsHorizontalWritingMode() ? LineDirectionMode::kHorizontalLine
: LineDirectionMode::kVerticalLine;
LayoutUnit baseline_offset(layout_box->BaselinePosition(
baseline_type_, IsFirstLine(), line_direction_mode));
NGLineHeightMetrics metrics(baseline_offset, block_size - baseline_offset);
box->metrics.Unite(metrics);
// TODO(kojii): Figure out what to do with OOF in NGLayoutResult.
// Floats are ok because atomic inlines are BFC?
// TODO(kojii): Try to eliminate the wrapping text fragment and use the
// |fragment| directly. Currently |CopyFragmentDataToLayoutBlockFlow|
// requires a text fragment.
text_builder->SetDirection(item.Style()->Direction());
text_builder->SetSize({fragment.InlineSize(), block_size});
LayoutUnit line_top = item_result->margins.block_start - metrics.ascent;
RefPtr<NGPhysicalTextFragment> text_fragment = text_builder->ToTextFragment(
item_result->item_index, item_result->start_offset,
item_result->end_offset);
line_box->AddChild(std::move(text_fragment), {position, line_top});
return box_states_.OnCloseTag(item, line_box, box, baseline_type_,
LayoutUnit(0));
}
void NGInlineLayoutAlgorithm::ApplyTextAlign(LayoutUnit* line_left,
LayoutUnit inline_size,
LayoutUnit available_width) {
// TODO(kojii): Implement text-align-last.
ETextAlign text_align = LineStyle().GetTextAlign();
switch (text_align) {
case ETextAlign::kRight:
case ETextAlign::kWebkitRight:
// Wide lines spill out of the block based off direction.
// So even if text-align is right, if direction is LTR, wide lines should
// overflow out of the right side of the block.
// TODO(kojii): Investigate how to handle trailing spaces.
if (inline_size < available_width ||
!LineStyle().IsLeftToRightDirection())
*line_left += available_width - inline_size;
break;
default:
// TODO(layout-dev): Implement.
// Refer to LayoutBlockFlow::UpdateLogicalWidthForAlignment().
break;
}
}
void NGInlineLayoutAlgorithm::FindNextLayoutOpportunity() {
NGLogicalOffset iter_offset = ConstraintSpace().BfcOffset();
if (container_builder_.BfcOffset()) {
iter_offset = ContainerBfcOffset();
iter_offset +=
{border_and_padding_.inline_start, border_and_padding_.block_start};
}
iter_offset.block_offset += content_size_;
auto* iter = MutableConstraintSpace()->LayoutOpportunityIterator(iter_offset);
NGLayoutOpportunity opportunity = iter->Next();
if (!opportunity.IsEmpty())
current_opportunity_ = opportunity;
}
RefPtr<NGLayoutResult> NGInlineLayoutAlgorithm::Layout() {
// If we are resuming from a break token our start border and padding is
// within a previous fragment.
content_size_ = BreakToken() ? LayoutUnit() : border_and_padding_.block_start;
NGLineBreaker line_breaker(Node(), constraint_space_, BreakToken());
NGInlineItemResults item_results;
while (true) {
line_breaker.NextLine(&item_results, this);
if (item_results.IsEmpty())
break;
CreateLine(&item_results, line_breaker.CreateBreakToken());
item_results.clear();
}
// TODO(crbug.com/716930): Avoid calculating border/padding twice.
if (!BreakToken())
content_size_ -= border_and_padding_.block_start;
// TODO(kojii): Check if the line box width should be content or available.
NGLogicalSize size(max_inline_size_, content_size_);
container_builder_.SetSize(size).SetOverflowSize(size);
// TODO(crbug.com/716930): We may be an empty LayoutInline due to splitting.
// Margin struts shouldn't need to be passed through like this once we've
// removed LayoutInline splitting.
if (!container_builder_.BfcOffset()) {
container_builder_.SetEndMarginStrut(ConstraintSpace().MarginStrut());
}
return container_builder_.ToBoxFragment();
}
} // namespace blink