blob: ddadc0953aa523450448ae373c2103e6e0c61f8e [file] [log] [blame]
// Copyright 2018 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/inline/ng_line_truncator.h"
#include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_box_state.h"
#include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_item_result.h"
#include "third_party/blink/renderer/core/layout/ng/inline/ng_text_fragment_builder.h"
#include "third_party/blink/renderer/core/layout/ng/ng_physical_box_fragment.h"
#include "third_party/blink/renderer/platform/fonts/font_baseline.h"
#include "third_party/blink/renderer/platform/fonts/shaping/harfbuzz_shaper.h"
#include "third_party/blink/renderer/platform/fonts/shaping/shape_result_view.h"
namespace blink {
NGLineTruncator::NGLineTruncator(const NGLineInfo& line_info)
: line_style_(&line_info.LineStyle()),
available_width_(line_info.AvailableWidth()),
line_direction_(line_info.BaseDirection()) {}
LayoutUnit NGLineTruncator::TruncateLine(
LayoutUnit line_width,
NGLineBoxFragmentBuilder::ChildList* line_box,
NGInlineLayoutStateStack* box_states) {
// Shape the ellipsis and compute its inline size.
// The ellipsis is styled according to the line style.
// https://drafts.csswg.org/css-ui/#ellipsing-details
const ComputedStyle* ellipsis_style = line_style_.get();
const Font& font = ellipsis_style->GetFont();
const SimpleFontData* font_data = font.PrimaryFont();
DCHECK(font_data);
String ellipsis_text =
font_data && font_data->GlyphForCharacter(kHorizontalEllipsisCharacter)
? String(&kHorizontalEllipsisCharacter, 1)
: String(u"...");
HarfBuzzShaper shaper(ellipsis_text);
scoped_refptr<ShapeResultView> ellipsis_shape_result =
ShapeResultView::Create(shaper.Shape(&font, line_direction_).get());
LayoutUnit ellipsis_width = ellipsis_shape_result->SnappedWidth();
// Loop children from the logical last to the logical first to determine where
// to place the ellipsis. Children maybe truncated or moved as part of the
// process.
NGLineBoxFragmentBuilder::Child* ellpisized_child = nullptr;
scoped_refptr<const NGPhysicalTextFragment> truncated_fragment;
if (IsLtr(line_direction_)) {
NGLineBoxFragmentBuilder::Child* first_child = line_box->FirstInFlowChild();
for (auto it = line_box->rbegin(); it != line_box->rend(); it++) {
auto& child = *it;
if (EllipsizeChild(line_width, ellipsis_width, &child == first_child,
&child, &truncated_fragment)) {
ellpisized_child = &child;
break;
}
}
} else {
NGLineBoxFragmentBuilder::Child* first_child = line_box->LastInFlowChild();
for (auto& child : *line_box) {
if (EllipsizeChild(line_width, ellipsis_width, &child == first_child,
&child, &truncated_fragment)) {
ellpisized_child = &child;
break;
}
}
}
// Abort if ellipsis could not be placed.
if (!ellpisized_child)
return line_width;
// Truncate the text fragment if needed.
if (truncated_fragment) {
DCHECK(ellpisized_child->fragment);
// In order to preserve layout information before truncated, hide the
// original fragment and insert a truncated one.
size_t child_index_to_truncate = ellpisized_child - line_box->begin();
line_box->InsertChild(child_index_to_truncate + 1);
box_states->ChildInserted(child_index_to_truncate + 1);
NGLineBoxFragmentBuilder::Child* child_to_truncate =
&(*line_box)[child_index_to_truncate];
ellpisized_child = std::next(child_to_truncate);
*ellpisized_child = *child_to_truncate;
HideChild(child_to_truncate);
LayoutUnit new_inline_size = line_style_->IsHorizontalWritingMode()
? truncated_fragment->Size().width
: truncated_fragment->Size().height;
DCHECK_LE(new_inline_size, ellpisized_child->inline_size);
if (UNLIKELY(IsRtl(line_direction_))) {
ellpisized_child->offset.inline_offset +=
ellpisized_child->inline_size - new_inline_size;
}
ellpisized_child->inline_size = new_inline_size;
ellpisized_child->fragment = std::move(truncated_fragment);
}
// Create the ellipsis, associating it with the ellipsized child.
LayoutObject* ellipsized_layout_object =
ellpisized_child->PhysicalFragment()->GetMutableLayoutObject();
DCHECK(ellipsized_layout_object && ellipsized_layout_object->IsInline() &&
(ellipsized_layout_object->IsText() ||
ellipsized_layout_object->IsAtomicInlineLevel()));
NGTextFragmentBuilder builder(line_style_->GetWritingMode());
builder.SetText(ellipsized_layout_object, ellipsis_text, ellipsis_style,
true /* is_ellipsis_style */,
std::move(ellipsis_shape_result));
// Now the offset of the ellpisis is determined. Place the ellpisis into the
// line box.
LayoutUnit ellipsis_inline_offset =
IsLtr(line_direction_)
? ellpisized_child->offset.inline_offset +
ellpisized_child->inline_size
: ellpisized_child->offset.inline_offset - ellipsis_width;
FontBaseline baseline_type = line_style_->GetFontBaseline();
NGLineHeightMetrics ellipsis_metrics(font_data->GetFontMetrics(),
baseline_type);
line_box->AddChild(
builder.ToTextFragment(),
LogicalOffset{ellipsis_inline_offset, -ellipsis_metrics.ascent},
ellipsis_width, 0);
return std::max(ellipsis_inline_offset + ellipsis_width, line_width);
}
// Hide this child from being painted. Leaves a hidden fragment so that layout
// queries such as |offsetWidth| work as if it is not truncated.
void NGLineTruncator::HideChild(NGLineBoxFragmentBuilder::Child* child) {
DCHECK(child->HasInFlowFragment());
if (const NGPhysicalTextFragment* text = child->fragment.get()) {
child->fragment = text->CloneAsHiddenForPaint();
return;
}
if (const NGLayoutResult* layout_result = child->layout_result.get()) {
// Need to propagate OOF descendants in this inline-block child.
const auto& fragment =
To<NGPhysicalBoxFragment>(layout_result->PhysicalFragment());
if (fragment.HasOutOfFlowPositionedDescendants())
return;
// If this child has self painting layer, not producing fragments will not
// suppress painting because layers are painted separately. Move it out of
// the clipping area.
if (fragment.HasSelfPaintingLayer()) {
// |available_width_| may not be enough when the containing block has
// paddings, because clipping is at the content box but ellipsizing is at
// the padding box. Just move to the max because we don't know paddings,
// and max should do what we need.
child->offset.inline_offset = LayoutUnit::NearlyMax();
return;
}
child->layout_result = fragment.CloneAsHiddenForPaint();
return;
}
NOTREACHED();
}
// Return the offset to place the ellipsis.
//
// This function may truncate or move the child so that the ellipsis can fit.
bool NGLineTruncator::EllipsizeChild(
LayoutUnit line_width,
LayoutUnit ellipsis_width,
bool is_first_child,
NGLineBoxFragmentBuilder::Child* child,
scoped_refptr<const NGPhysicalTextFragment>* truncated_fragment) {
DCHECK(truncated_fragment && !*truncated_fragment);
// Leave out-of-flow children as is.
if (!child->HasInFlowFragment())
return false;
// Inline boxes should not be ellipsized. Usually they will be created in the
// later phase, but empty inline box are already created.
if (child->layout_result &&
child->layout_result->PhysicalFragment().IsInlineBox())
return false;
// Can't place ellipsis if this child is completely outside of the box.
LayoutUnit child_inline_offset =
IsLtr(line_direction_)
? child->offset.inline_offset
: line_width - (child->offset.inline_offset + child->inline_size);
LayoutUnit space_for_child = available_width_ - child_inline_offset;
if (space_for_child <= 0) {
// This child is outside of the content box, but we still need to hide it.
// When the box has paddings, this child outside of the content box maybe
// still inside of the clipping box.
if (!is_first_child)
HideChild(child);
return false;
}
// At least part of this child is in the box.
// If not all of this child can fit, try to truncate.
space_for_child -= ellipsis_width;
if (space_for_child < child->inline_size &&
!TruncateChild(space_for_child, is_first_child, *child,
truncated_fragment)) {
// This child is partially in the box, but it should not be visible because
// earlier sibling will be truncated and ellipsized.
if (!is_first_child)
HideChild(child);
return false;
}
return true;
}
// Truncate the specified child. Returns true if truncated successfully, false
// otherwise.
//
// Note that this function may return true even if it can't fit the child when
// |is_first_child|, because the spec defines that the first character or atomic
// inline-level element on a line must be clipped rather than ellipsed.
// https://drafts.csswg.org/css-ui/#text-overflow
bool NGLineTruncator::TruncateChild(
LayoutUnit space_for_child,
bool is_first_child,
const NGLineBoxFragmentBuilder::Child& child,
scoped_refptr<const NGPhysicalTextFragment>* truncated_fragment) {
DCHECK(truncated_fragment && !*truncated_fragment);
// If the space is not enough, try the next child.
if (space_for_child <= 0 && !is_first_child)
return false;
// Only text fragments can be truncated.
if (!child.fragment)
return is_first_child;
auto& fragment = To<NGPhysicalTextFragment>(*child.fragment);
// No need to truncate empty results.
if (!fragment.TextShapeResult())
return is_first_child;
// TODO(layout-dev): Add support for OffsetToFit to ShapeResultView to avoid
// this copy.
scoped_refptr<blink::ShapeResult> shape_result =
fragment.TextShapeResult()->CreateShapeResult();
if (!shape_result)
return is_first_child;
// Compute the offset to truncate.
unsigned new_length = shape_result->OffsetToFit(
IsLtr(line_direction_) ? space_for_child
: shape_result->Width() - space_for_child,
line_direction_);
DCHECK_LE(new_length, fragment.Length());
if (!new_length || new_length == fragment.Length()) {
if (!is_first_child)
return false;
new_length = !new_length ? 1 : new_length - 1;
}
// Truncate the text fragment.
*truncated_fragment =
line_direction_ == shape_result->Direction()
? fragment.TrimText(fragment.StartOffset(),
fragment.StartOffset() + new_length)
: fragment.TrimText(fragment.StartOffset() + new_length,
fragment.EndOffset());
return true;
}
} // namespace blink