| // Copyright 2020 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_ruby_utils.h" |
| |
| #include <tuple> |
| #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_cursor.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_logical_line_item.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_physical_box_fragment.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_physical_container_fragment.h" |
| #include "third_party/blink/renderer/platform/fonts/font_height.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| std::tuple<LayoutUnit, LayoutUnit> AdjustTextOverUnderOffsetsForEmHeight( |
| LayoutUnit over, |
| LayoutUnit under, |
| const ComputedStyle& style, |
| const ShapeResultView& shape_view) { |
| DCHECK_LE(over, under); |
| const SimpleFontData* primary_font_data = style.GetFont().PrimaryFont(); |
| if (!primary_font_data) |
| return std::make_pair(over, under); |
| const auto font_baseline = style.GetFontBaseline(); |
| const LayoutUnit line_height = under - over; |
| const LayoutUnit primary_ascent = |
| primary_font_data->GetFontMetrics().FixedAscent(font_baseline); |
| const LayoutUnit primary_descent = line_height - primary_ascent; |
| |
| DCHECK(IsMainThread()); |
| DEFINE_STATIC_LOCAL(Vector<ShapeResult::RunFontData>, run_fonts, ()); |
| DCHECK_EQ(run_fonts.size(), 0u); |
| // We don't use ShapeResultView::FallbackFonts() because we can't know if the |
| // primary font is actually used with FallbackFonts(). |
| shape_view.GetRunFontData(&run_fonts); |
| const LayoutUnit kNoDiff = LayoutUnit::Max(); |
| LayoutUnit over_diff = kNoDiff; |
| LayoutUnit under_diff = kNoDiff; |
| for (const auto& run_font : run_fonts) { |
| const SimpleFontData* font_data = run_font.font_data_; |
| if (!font_data) |
| continue; |
| const FontHeight normalized_height = |
| font_data->NormalizedTypoAscentAndDescent(font_baseline); |
| // Floor() is better than Round(). We should not subtract pixels larger |
| // than |primary_ascent - em_box.ascent|. |
| const LayoutUnit current_over_diff( |
| (primary_ascent - normalized_height.ascent) |
| .ClampNegativeToZero() |
| .Floor()); |
| const LayoutUnit current_under_diff( |
| (primary_descent - normalized_height.descent) |
| .ClampNegativeToZero() |
| .Floor()); |
| over_diff = std::min(over_diff, current_over_diff); |
| under_diff = std::min(under_diff, current_under_diff); |
| } |
| run_fonts.resize(0); |
| if (over_diff == kNoDiff) |
| over_diff = LayoutUnit(); |
| if (under_diff == kNoDiff) |
| under_diff = LayoutUnit(); |
| return std::make_tuple(over + over_diff, under - under_diff); |
| } |
| |
| } // anonymous namespace |
| |
| PhysicalRect AdjustTextRectForEmHeight(const PhysicalRect& rect, |
| const ComputedStyle& style, |
| const ShapeResultView* shape_view, |
| WritingMode writing_mode) { |
| if (!shape_view) |
| return rect; |
| const LayoutUnit line_height = IsHorizontalWritingMode(writing_mode) |
| ? rect.size.height |
| : rect.size.width; |
| LayoutUnit over, under; |
| std::tie(over, under) = AdjustTextOverUnderOffsetsForEmHeight( |
| LayoutUnit(), line_height, style, *shape_view); |
| const LayoutUnit over_diff = over; |
| const LayoutUnit under_diff = line_height - under; |
| const LayoutUnit new_line_height = under - over; |
| |
| if (IsHorizontalWritingMode(writing_mode)) { |
| return {{rect.offset.left, rect.offset.top + over_diff}, |
| PhysicalSize(rect.size.width, new_line_height)}; |
| } |
| if (IsFlippedLinesWritingMode(writing_mode)) { |
| return {{rect.offset.left + under_diff, rect.offset.top}, |
| PhysicalSize(new_line_height, rect.size.height)}; |
| } |
| return {{rect.offset.left + over_diff, rect.offset.top}, |
| PhysicalSize(new_line_height, rect.size.height)}; |
| } |
| |
| // See LayoutRubyRun::GetOverhang(). |
| NGAnnotationOverhang GetOverhang(const NGInlineItemResult& item) { |
| NGAnnotationOverhang overhang; |
| if (!item.layout_result) |
| return overhang; |
| |
| const auto& run_fragment = |
| To<NGPhysicalContainerFragment>(item.layout_result->PhysicalFragment()); |
| LayoutUnit start_overhang = LayoutUnit::Max(); |
| LayoutUnit end_overhang = LayoutUnit::Max(); |
| bool found_line = false; |
| const ComputedStyle* ruby_text_style = nullptr; |
| for (const auto& child_link : run_fragment.PostLayoutChildren()) { |
| const NGPhysicalFragment& child_fragment = *child_link.get(); |
| const LayoutObject* layout_object = child_fragment.GetLayoutObject(); |
| if (!layout_object) |
| continue; |
| if (layout_object->IsRubyText()) { |
| ruby_text_style = layout_object->Style(); |
| continue; |
| } |
| if (layout_object->IsRubyBase()) { |
| const ComputedStyle& base_style = child_fragment.Style(); |
| const auto writing_direction = base_style.GetWritingDirection(); |
| const LayoutUnit base_inline_size = |
| NGFragment(writing_direction, child_fragment).InlineSize(); |
| // RubyBase's inline_size is always same as RubyRun's inline_size. |
| // Overhang values are offsets from RubyBase's inline edges to |
| // the outmost text. |
| for (const auto& base_child_link : |
| To<NGPhysicalContainerFragment>(child_fragment) |
| .PostLayoutChildren()) { |
| const LayoutUnit line_inline_size = |
| NGFragment(writing_direction, *base_child_link).InlineSize(); |
| if (line_inline_size == LayoutUnit()) |
| continue; |
| found_line = true; |
| const LayoutUnit start = |
| base_child_link.offset |
| .ConvertToLogical(writing_direction, child_fragment.Size(), |
| base_child_link.get()->Size()) |
| .inline_offset; |
| const LayoutUnit end = base_inline_size - start - line_inline_size; |
| start_overhang = std::min(start_overhang, start); |
| end_overhang = std::min(end_overhang, end); |
| } |
| } |
| } |
| |
| if (!found_line || !ruby_text_style) |
| return overhang; |
| DCHECK_NE(start_overhang, LayoutUnit::Max()); |
| DCHECK_NE(end_overhang, LayoutUnit::Max()); |
| // We allow overhang up to the half of ruby text font size. |
| const LayoutUnit half_width_of_ruby_font = |
| LayoutUnit(ruby_text_style->FontSize()) / 2; |
| overhang.start = std::min(start_overhang, half_width_of_ruby_font); |
| overhang.end = std::min(end_overhang, half_width_of_ruby_font); |
| return overhang; |
| } |
| |
| // See LayoutRubyRun::GetOverhang(). |
| bool CanApplyStartOverhang(const NGLineInfo& line_info, |
| LayoutUnit& start_overhang) { |
| if (start_overhang <= LayoutUnit()) |
| return false; |
| const NGInlineItemResults& items = line_info.Results(); |
| // Requires at least the current item and the previous item. |
| if (items.size() < 2) |
| return false; |
| // Find a previous item other than kOpenTag/kCloseTag. |
| // Searching items in the logical order doesn't work well with bidi |
| // reordering. However, it's difficult to compute overhang after bidi |
| // reordering because it affects line breaking. |
| wtf_size_t previous_index = items.size() - 2; |
| while ((items[previous_index].item->Type() == NGInlineItem::kOpenTag || |
| items[previous_index].item->Type() == NGInlineItem::kCloseTag) && |
| previous_index > 0) |
| --previous_index; |
| const NGInlineItemResult& previous_item = items[previous_index]; |
| if (previous_item.item->Type() != NGInlineItem::kText) |
| return false; |
| const NGInlineItem& current_item = *items.back().item; |
| if (previous_item.item->Style()->FontSize() > |
| current_item.Style()->FontSize()) |
| return false; |
| start_overhang = std::min(start_overhang, previous_item.inline_size); |
| return true; |
| } |
| |
| // See LayoutRubyRun::GetOverhang(). |
| LayoutUnit CommitPendingEndOverhang(NGLineInfo* line_info) { |
| DCHECK(line_info); |
| NGInlineItemResults* items = line_info->MutableResults(); |
| if (items->size() < 2U) |
| return LayoutUnit(); |
| const NGInlineItemResult& text_item = items->back(); |
| if (text_item.item->Type() == NGInlineItem::kControl) |
| return LayoutUnit(); |
| DCHECK(text_item.item->Type() == NGInlineItem::kText); |
| wtf_size_t i = items->size() - 2; |
| while ((*items)[i].item->Type() != NGInlineItem::kAtomicInline) { |
| const auto type = (*items)[i].item->Type(); |
| if (type != NGInlineItem::kOpenTag && type != NGInlineItem::kCloseTag) |
| return LayoutUnit(); |
| if (i-- == 0) |
| return LayoutUnit(); |
| } |
| NGInlineItemResult& atomic_inline_item = (*items)[i]; |
| if (!atomic_inline_item.layout_result->PhysicalFragment().IsRubyRun()) |
| return LayoutUnit(); |
| if (atomic_inline_item.pending_end_overhang <= LayoutUnit()) |
| return LayoutUnit(); |
| if (atomic_inline_item.item->Style()->FontSize() < |
| text_item.item->Style()->FontSize()) |
| return LayoutUnit(); |
| // Ideally we should refer to inline_size of |text_item| instead of the width |
| // of the NGInlineItem's ShapeResult. However it's impossible to compute |
| // inline_size of |text_item| before calling BreakText(), and BreakText() |
| // requires precise |position_| which takes |end_overhang| into account. |
| LayoutUnit end_overhang = |
| std::min(atomic_inline_item.pending_end_overhang, |
| LayoutUnit(text_item.item->TextShapeResult()->Width())); |
| DCHECK_EQ(atomic_inline_item.margins.inline_end, LayoutUnit()); |
| atomic_inline_item.margins.inline_end = -end_overhang; |
| atomic_inline_item.inline_size -= end_overhang; |
| atomic_inline_item.pending_end_overhang = LayoutUnit(); |
| return end_overhang; |
| } |
| |
| NGAnnotationMetrics ComputeAnnotationOverflow( |
| const NGLogicalLineItems& logical_line, |
| const FontHeight& line_box_metrics, |
| LayoutUnit line_over, |
| const ComputedStyle& line_style) { |
| // Min/max position of content and annotations, ignoring line-height. |
| LayoutUnit content_over = line_over + line_box_metrics.ascent; |
| LayoutUnit content_under = content_over; |
| |
| bool has_over_annotation = false; |
| bool has_under_annotation = false; |
| |
| const LayoutUnit line_under = line_over + line_box_metrics.LineHeight(); |
| bool has_over_emphasis = false; |
| bool has_under_emphasis = false; |
| for (const NGLogicalLineItem& item : logical_line) { |
| if (!item.HasInFlowFragment()) |
| continue; |
| if (item.IsControl()) |
| continue; |
| LayoutUnit item_over = item.BlockOffset(); |
| LayoutUnit item_under = item.BlockEndOffset(); |
| if (item.shape_result) { |
| if (const auto* style = item.Style()) { |
| std::tie(item_over, item_under) = AdjustTextOverUnderOffsetsForEmHeight( |
| item_over, item_under, *style, *item.shape_result); |
| } |
| } else { |
| const auto* fragment = item.PhysicalFragment(); |
| if (fragment && fragment->IsRubyRun()) { |
| PhysicalRect rect = |
| To<NGPhysicalBoxFragment>(fragment)->ScrollableOverflow( |
| NGPhysicalFragment::kEmHeight); |
| LayoutUnit block_size; |
| if (IsHorizontalWritingMode(line_style.GetWritingMode())) { |
| item_under = item_over + rect.Bottom(); |
| item_over += rect.offset.top; |
| block_size = fragment->Size().height; |
| } else { |
| block_size = fragment->Size().width; |
| // We assume 'over' is always on right in vertical writing modes. |
| // TODO(layout-dev): sideways-lr support. |
| DCHECK(line_style.IsFlippedBlocksWritingMode() || |
| line_style.IsFlippedLinesWritingMode()); |
| item_under = item_over + block_size; |
| item_over = item_under - rect.Right(); |
| item_under -= rect.offset.left; |
| } |
| |
| // Check if we really have an annotation. |
| if (const auto* layout_result = item.layout_result.get()) { |
| LayoutUnit overflow = layout_result->AnnotationOverflow(); |
| if (IsFlippedLinesWritingMode(line_style.GetWritingMode())) |
| overflow = -overflow; |
| if (overflow < LayoutUnit()) |
| has_over_annotation = true; |
| else if (overflow > LayoutUnit()) |
| has_under_annotation = true; |
| } |
| } else if (item.IsInlineBox()) { |
| continue; |
| } |
| } |
| content_over = std::min(content_over, item_over); |
| content_under = std::max(content_under, item_under); |
| |
| if (const auto* style = item.Style()) { |
| if (style->GetTextEmphasisMark() != TextEmphasisMark::kNone) { |
| if (style->GetTextEmphasisLineLogicalSide() == LineLogicalSide::kOver) |
| has_over_emphasis = true; |
| else |
| has_under_emphasis = true; |
| } |
| } |
| } |
| |
| // Probably this is an empty line. We should secure font-size space. |
| const LayoutUnit font_size(line_style.ComputedFontSize()); |
| if (content_under - content_over < font_size) { |
| LayoutUnit half_leading = (line_box_metrics.LineHeight() - font_size) / 2; |
| half_leading = half_leading.ClampNegativeToZero(); |
| content_over = line_over + half_leading; |
| content_under = line_under - half_leading; |
| } |
| |
| // Don't provide annotation space if text-emphasis exists. |
| // TODO(layout-dev): If the text-emphasis is in [line_over, line_under], |
| // this line can provide annotation space. |
| if (has_over_emphasis) |
| content_over = std::min(content_over, line_over); |
| if (has_under_emphasis) |
| content_under = std::max(content_under, line_under); |
| |
| // With some fonts, text fragment sizes can exceed line-height. |
| // We'd like to set overflow only if we have annotations. |
| // This affects fast/ruby/line-height.html on macOS. |
| if (content_over < line_over && !has_over_annotation) |
| content_over = line_over; |
| if (content_under > line_under && !has_under_annotation) |
| content_under = line_under; |
| |
| return {(line_over - content_over).ClampNegativeToZero(), |
| (content_under - line_under).ClampNegativeToZero(), |
| (content_over - line_over).ClampNegativeToZero(), |
| (line_under - content_under).ClampNegativeToZero()}; |
| } |
| |
| } // namespace blink |