| // 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/inline/ng_physical_text_fragment.h" |
| |
| #include "third_party/blink/renderer/core/dom/node.h" |
| #include "third_party/blink/renderer/core/layout/layout_text_fragment.h" |
| #include "third_party/blink/renderer/core/layout/line/line_orientation_utils.h" |
| #include "third_party/blink/renderer/core/layout/ng/geometry/ng_logical_size.h" |
| #include "third_party/blink/renderer/core/layout/ng/geometry/ng_physical_offset_rect.h" |
| #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_item.h" |
| #include "third_party/blink/renderer/core/layout/ng/inline/ng_text_fragment_builder.h" |
| #include "third_party/blink/renderer/core/style/computed_style.h" |
| #include "third_party/blink/renderer/platform/fonts/shaping/shape_result_view.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| struct SameSizeAsNGPhysicalTextFragment : NGPhysicalFragment { |
| void* pointers[2]; |
| NGPhysicalOffsetRect rect; |
| unsigned offsets[2]; |
| }; |
| |
| static_assert(sizeof(NGPhysicalTextFragment) == |
| sizeof(SameSizeAsNGPhysicalTextFragment), |
| "NGPhysicalTextFragment should stay small"); |
| |
| inline bool IsPhysicalTextFragmentAnonymousText( |
| const LayoutObject* layout_object) { |
| if (!layout_object) |
| return false; |
| if (layout_object->IsText() && ToLayoutText(layout_object)->IsTextFragment()) |
| return !ToLayoutTextFragment(layout_object)->AssociatedTextNode(); |
| const Node* node = layout_object->GetNode(); |
| return !node || node->IsPseudoElement(); |
| } |
| |
| NGLineOrientation ToLineOrientation(WritingMode writing_mode) { |
| switch (writing_mode) { |
| case WritingMode::kHorizontalTb: |
| return NGLineOrientation::kHorizontal; |
| case WritingMode::kVerticalRl: |
| case WritingMode::kVerticalLr: |
| case WritingMode::kSidewaysRl: |
| return NGLineOrientation::kClockWiseVertical; |
| case WritingMode::kSidewaysLr: |
| return NGLineOrientation::kCounterClockWiseVertical; |
| } |
| NOTREACHED(); |
| return NGLineOrientation::kHorizontal; |
| } |
| |
| } // anonymous namespace |
| |
| NGPhysicalTextFragment::NGPhysicalTextFragment( |
| LayoutObject* layout_object, |
| const ComputedStyle& style, |
| NGStyleVariant style_variant, |
| NGTextType text_type, |
| const String& text, |
| unsigned start_offset, |
| unsigned end_offset, |
| NGPhysicalSize size, |
| NGLineOrientation line_orientation, |
| scoped_refptr<const ShapeResultView> shape_result) |
| : NGPhysicalFragment(layout_object, |
| style, |
| style_variant, |
| size, |
| kFragmentText, |
| text_type), |
| text_(text), |
| start_offset_(start_offset), |
| end_offset_(end_offset), |
| shape_result_(shape_result) { |
| DCHECK(shape_result_ || IsFlowControl()) << ToString(); |
| line_orientation_ = static_cast<unsigned>(line_orientation); |
| is_anonymous_text_ = IsPhysicalTextFragmentAnonymousText(layout_object); |
| |
| self_ink_overflow_ = ComputeSelfInkOverflow(); |
| } |
| |
| NGPhysicalTextFragment::NGPhysicalTextFragment(NGTextFragmentBuilder* builder) |
| : NGPhysicalFragment(builder, kFragmentText, builder->text_type_), |
| text_(builder->text_), |
| start_offset_(builder->start_offset_), |
| end_offset_(builder->end_offset_), |
| shape_result_(std::move(builder->shape_result_)) { |
| DCHECK(shape_result_ || IsFlowControl()) << ToString(); |
| line_orientation_ = |
| static_cast<unsigned>(ToLineOrientation(builder->GetWritingMode())); |
| is_anonymous_text_ = |
| IsPhysicalTextFragmentAnonymousText(builder->layout_object_); |
| |
| self_ink_overflow_ = ComputeSelfInkOverflow(); |
| } |
| |
| // Convert logical cooridnate to local physical coordinate. |
| NGPhysicalOffsetRect NGPhysicalTextFragment::ConvertToLocal( |
| const LayoutRect& logical_rect) const { |
| switch (LineOrientation()) { |
| case NGLineOrientation::kHorizontal: |
| return NGPhysicalOffsetRect(logical_rect); |
| case NGLineOrientation::kClockWiseVertical: |
| return {{size_.width - logical_rect.MaxY(), logical_rect.X()}, |
| {logical_rect.Height(), logical_rect.Width()}}; |
| case NGLineOrientation::kCounterClockWiseVertical: |
| return {{logical_rect.Y(), size_.height - logical_rect.MaxX()}, |
| {logical_rect.Height(), logical_rect.Width()}}; |
| } |
| NOTREACHED(); |
| return NGPhysicalOffsetRect(logical_rect); |
| } |
| |
| // Compute the inline position from text offset, in logical coordinate relative |
| // to this fragment. |
| LayoutUnit NGPhysicalTextFragment::InlinePositionForOffset( |
| unsigned offset, |
| LayoutUnit (*round)(float), |
| AdjustMidCluster adjust_mid_cluster) const { |
| DCHECK_GE(offset, start_offset_); |
| DCHECK_LE(offset, end_offset_); |
| |
| offset -= start_offset_; |
| if (shape_result_) { |
| // TODO(layout-dev): Move caret position out of ShapeResult and into a |
| // separate support class that can take a ShapeResult or ShapeResultView. |
| // Allows for better code separation and avoids the extra copy below. |
| return round(shape_result_->CreateShapeResult()->CaretPositionForOffset( |
| offset, Text(), adjust_mid_cluster)); |
| } |
| |
| // This fragment is a flow control because otherwise ShapeResult exists. |
| DCHECK(IsFlowControl()); |
| DCHECK_EQ(1u, Length()); |
| if (!offset || UNLIKELY(IsRtl(Style().Direction()))) |
| return LayoutUnit(); |
| return IsHorizontal() ? Size().width : Size().height; |
| } |
| |
| LayoutUnit NGPhysicalTextFragment::InlinePositionForOffset( |
| unsigned offset) const { |
| return InlinePositionForOffset(offset, LayoutUnit::FromFloatRound, |
| AdjustMidCluster::kToEnd); |
| } |
| |
| std::pair<LayoutUnit, LayoutUnit> |
| NGPhysicalTextFragment::LineLeftAndRightForOffsets(unsigned start_offset, |
| unsigned end_offset) const { |
| DCHECK_LE(start_offset, end_offset); |
| DCHECK_GE(start_offset, start_offset_); |
| DCHECK_LE(end_offset, end_offset_); |
| |
| const LayoutUnit start_position = InlinePositionForOffset( |
| start_offset, LayoutUnit::FromFloatFloor, AdjustMidCluster::kToStart); |
| const LayoutUnit end_position = InlinePositionForOffset( |
| end_offset, LayoutUnit::FromFloatCeil, AdjustMidCluster::kToEnd); |
| |
| // Swap positions if RTL. |
| return (UNLIKELY(start_position > end_position)) |
| ? std::make_pair(end_position, start_position) |
| : std::make_pair(start_position, end_position); |
| } |
| |
| NGPhysicalOffsetRect NGPhysicalTextFragment::LocalRect( |
| unsigned start_offset, |
| unsigned end_offset) const { |
| LayoutUnit start_position, end_position; |
| std::tie(start_position, end_position) = |
| LineLeftAndRightForOffsets(start_offset, end_offset); |
| const LayoutUnit inline_size = end_position - start_position; |
| switch (LineOrientation()) { |
| case NGLineOrientation::kHorizontal: |
| return {{start_position, LayoutUnit()}, {inline_size, Size().height}}; |
| case NGLineOrientation::kClockWiseVertical: |
| return {{LayoutUnit(), start_position}, {Size().width, inline_size}}; |
| case NGLineOrientation::kCounterClockWiseVertical: |
| return {{LayoutUnit(), Size().height - end_position}, |
| {Size().width, inline_size}}; |
| } |
| NOTREACHED(); |
| return {}; |
| } |
| |
| NGPhysicalOffsetRect NGPhysicalTextFragment::ComputeSelfInkOverflow() const { |
| if (UNLIKELY(!shape_result_)) |
| return LocalRect(); |
| |
| // Glyph bounds is in logical coordinate, origin at the alphabetic baseline. |
| LayoutRect ink_overflow = EnclosingLayoutRect(shape_result_->Bounds()); |
| |
| // Make the origin at the logical top of this fragment. |
| const ComputedStyle& style = Style(); |
| const Font& font = style.GetFont(); |
| if (const SimpleFontData* font_data = font.PrimaryFont()) { |
| ink_overflow.SetY( |
| ink_overflow.Y() + |
| font_data->GetFontMetrics().FixedAscent(kAlphabeticBaseline)); |
| } |
| |
| if (float stroke_width = style.TextStrokeWidth()) { |
| ink_overflow.Inflate(LayoutUnit::FromFloatCeil(stroke_width / 2.0f)); |
| } |
| |
| if (style.GetTextEmphasisMark() != TextEmphasisMark::kNone) { |
| LayoutUnit emphasis_mark_height = |
| LayoutUnit(font.EmphasisMarkHeight(style.TextEmphasisMarkString())); |
| DCHECK_GT(emphasis_mark_height, LayoutUnit()); |
| if (style.GetTextEmphasisLineLogicalSide() == LineLogicalSide::kOver) { |
| ink_overflow.ShiftYEdgeTo( |
| std::min(ink_overflow.Y(), -emphasis_mark_height)); |
| } else { |
| LayoutUnit logical_height = |
| style.IsHorizontalWritingMode() ? Size().height : Size().width; |
| ink_overflow.ShiftMaxYEdgeTo( |
| std::max(ink_overflow.MaxY(), logical_height + emphasis_mark_height)); |
| } |
| } |
| |
| if (ShadowList* text_shadow = style.TextShadow()) { |
| LayoutRectOutsets text_shadow_logical_outsets = |
| LineOrientationLayoutRectOutsets( |
| LayoutRectOutsets(text_shadow->RectOutsetsIncludingOriginal()), |
| style.GetWritingMode()); |
| text_shadow_logical_outsets.ClampNegativeToZero(); |
| ink_overflow.Expand(text_shadow_logical_outsets); |
| } |
| |
| ink_overflow = LayoutRect(EnclosingIntRect(ink_overflow)); |
| |
| // Uniting the frame rect ensures that non-ink spaces such side bearings, or |
| // even space characters, are included in the visual rect for decorations. |
| NGPhysicalOffsetRect local_ink_overflow = ConvertToLocal(ink_overflow); |
| local_ink_overflow.Unite(LocalRect()); |
| return local_ink_overflow; |
| } |
| |
| scoped_refptr<const NGPhysicalFragment> NGPhysicalTextFragment::TrimText( |
| unsigned new_start_offset, |
| unsigned new_end_offset) const { |
| DCHECK(shape_result_); |
| DCHECK_GE(new_start_offset, StartOffset()); |
| DCHECK_GT(new_end_offset, new_start_offset); |
| DCHECK_LE(new_end_offset, EndOffset()); |
| // TODO(layout-dev): Add sub-range version of CreateShapeResult to avoid |
| // this double copy. |
| scoped_refptr<ShapeResult> new_shape_result = |
| shape_result_->CreateShapeResult()->SubRange(new_start_offset, |
| new_end_offset); |
| LayoutUnit new_inline_size = new_shape_result->SnappedWidth(); |
| return base::AdoptRef(new NGPhysicalTextFragment( |
| layout_object_, Style(), static_cast<NGStyleVariant>(style_variant_), |
| TextType(), text_, new_start_offset, new_end_offset, |
| IsHorizontal() ? NGPhysicalSize{new_inline_size, size_.height} |
| : NGPhysicalSize{size_.width, new_inline_size}, |
| LineOrientation(), ShapeResultView::Create(new_shape_result.get()))); |
| } |
| |
| unsigned NGPhysicalTextFragment::TextOffsetForPoint( |
| const NGPhysicalOffset& point) const { |
| const ComputedStyle& style = Style(); |
| const LayoutUnit& point_in_line_direction = |
| style.IsHorizontalWritingMode() ? point.left : point.top; |
| if (const ShapeResultView* shape_result = TextShapeResult()) { |
| // TODO(layout-dev): Move caret logic out of ShapeResult into separate |
| // support class for code health and to avoid this copy. |
| return shape_result->CreateShapeResult()->CaretOffsetForHitTest( |
| point_in_line_direction.ToFloat(), Text(), BreakGlyphs) + |
| StartOffset(); |
| } |
| |
| // Flow control fragments such as forced line break, tabulation, soft-wrap |
| // opportunities, etc. do not have ShapeResult. |
| DCHECK(IsFlowControl()); |
| |
| // Zero-inline-size objects such as newline always return the start offset. |
| NGLogicalSize size = Size().ConvertToLogical(style.GetWritingMode()); |
| if (!size.inline_size) |
| return StartOffset(); |
| |
| // Sized objects such as tabulation returns the next offset if the given point |
| // is on the right half. |
| LayoutUnit inline_offset = IsLtr(ResolvedDirection()) |
| ? point_in_line_direction |
| : size.inline_size - point_in_line_direction; |
| DCHECK_EQ(1u, Length()); |
| return inline_offset <= size.inline_size / 2 ? StartOffset() : EndOffset(); |
| } |
| |
| UBiDiLevel NGPhysicalTextFragment::BidiLevel() const { |
| // TODO(xiaochengh): Make the implementation more efficient with, e.g., |
| // binary search and/or LayoutNGText::InlineItems(). |
| const auto& items = InlineItemsOfContainingBlock(); |
| const NGInlineItem* containing_item = std::find_if( |
| items.begin(), items.end(), [this](const NGInlineItem& item) { |
| return item.StartOffset() <= StartOffset() && |
| item.EndOffset() >= EndOffset(); |
| }); |
| DCHECK(containing_item); |
| DCHECK_NE(containing_item, items.end()); |
| return containing_item->BidiLevel(); |
| } |
| |
| TextDirection NGPhysicalTextFragment::ResolvedDirection() const { |
| if (TextShapeResult()) |
| return TextShapeResult()->Direction(); |
| return DirectionFromLevel(BidiLevel()); |
| } |
| |
| } // namespace blink |