blob: 4d0ca83c20c755a3f62faae41ee936f9222653e3 [file] [log] [blame]
// Copyright 2014 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/paint/InlineTextBoxPainter.h"
#include "build/build_config.h"
#include "core/editing/Editor.h"
#include "core/editing/markers/CompositionMarker.h"
#include "core/editing/markers/DocumentMarkerController.h"
#include "core/editing/markers/TextMatchMarker.h"
#include "core/frame/LocalFrame.h"
#include "core/layout/LayoutTextCombine.h"
#include "core/layout/LayoutTheme.h"
#include "core/layout/api/LineLayoutAPIShim.h"
#include "core/layout/api/LineLayoutBox.h"
#include "core/layout/line/InlineTextBox.h"
#include "core/paint/AppliedDecorationPainter.h"
#include "core/paint/DecorationInfo.h"
#include "core/paint/PaintInfo.h"
#include "core/paint/TextPainter.h"
#include "platform/graphics/GraphicsContextStateSaver.h"
#include "platform/graphics/paint/DrawingRecorder.h"
#include "platform/graphics/paint/PaintRecord.h"
#include "platform/graphics/paint/PaintRecorder.h"
#include "platform/graphics/paint/PaintShader.h"
#include "platform/wtf/Optional.h"
#include "third_party/skia/include/effects/SkGradientShader.h"
namespace blink {
namespace {
// If an inline text box is truncated by an ellipsis, text box markers paint
// over the ellipsis and other marker types don't. Other marker types that want
// the normal behavior should use MarkerPaintStartAndEnd().
std::pair<unsigned, unsigned> GetTextMatchMarkerPaintOffsets(
const DocumentMarker& marker,
const InlineTextBox& text_box) {
DCHECK_EQ(DocumentMarker::kTextMatch, marker.GetType());
const unsigned start_offset = marker.StartOffset() > text_box.Start()
? marker.StartOffset() - text_box.Start()
: 0U;
const unsigned end_offset =
std::min(marker.EndOffset() - text_box.Start(), text_box.Len());
return std::make_pair(start_offset, end_offset);
}
} // anonymous namespace
static LineLayoutItem EnclosingUnderlineObject(
const InlineTextBox* inline_text_box) {
bool first_line = inline_text_box->IsFirstLineStyle();
for (LineLayoutItem current = inline_text_box->Parent()->GetLineLayoutItem();
;) {
if (current.IsLayoutBlock())
return current;
if (!current.IsLayoutInline() || current.IsRubyText())
return nullptr;
const ComputedStyle& style_to_use = current.StyleRef(first_line);
if (EnumHasFlags(style_to_use.GetTextDecoration(),
TextDecoration::kUnderline))
return current;
current = current.Parent();
if (!current)
return current;
if (Node* node = current.GetNode()) {
if (isHTMLAnchorElement(node) || node->HasTagName(HTMLNames::fontTag))
return current;
}
}
}
static int ComputeUnderlineOffsetForUnder(
const ComputedStyle& style,
const InlineTextBox* inline_text_box,
LineLayoutItem decorating_box,
float text_decoration_thickness,
LineVerticalPositionType position_type) {
const RootInlineBox& root = inline_text_box->Root();
FontBaseline baseline_type = root.BaselineType();
LayoutUnit offset = inline_text_box->OffsetTo(position_type, baseline_type);
// Compute offset to the farthest position of the decorating box.
LayoutUnit logical_top = inline_text_box->LogicalTop();
LayoutUnit position = logical_top + offset;
LayoutUnit farthest = root.FarthestPositionForUnderline(
decorating_box, position_type, baseline_type, position);
// Round() looks more logical but Floor() produces better results in
// positive/negative offsets, in horizontal/vertical flows, on Win/Mac/Linux.
int offset_int = (farthest - logical_top).Floor();
// Gaps are not needed for TextTop because it generally has internal
// leadings.
if (position_type == LineVerticalPositionType::TextTop)
return offset_int;
return !IsLineOverSide(position_type) ? offset_int + 1 : offset_int - 1;
}
static int ComputeUnderlineOffsetForRoman(
const FontMetrics& font_metrics,
const float text_decoration_thickness) {
// Compute the gap between the font and the underline. Use at least one
// pixel gap, if underline is thick then use a bigger gap.
int gap = 0;
// Underline position of zero means draw underline on Baseline Position,
// in Blink we need at least 1-pixel gap to adding following check.
// Positive underline Position means underline should be drawn above baseline
// and negative value means drawing below baseline, negating the value as in
// Blink downward Y-increases.
if (font_metrics.UnderlinePosition())
gap = -font_metrics.UnderlinePosition();
else
gap = std::max<int>(1, ceilf(text_decoration_thickness / 2.f));
// Position underline near the alphabetic baseline.
return font_metrics.Ascent() + gap;
}
static int ComputeUnderlineOffset(ResolvedUnderlinePosition underline_position,
const ComputedStyle& style,
const FontMetrics& font_metrics,
const InlineTextBox* inline_text_box,
LineLayoutItem decorating_box,
const float text_decoration_thickness) {
switch (underline_position) {
default:
NOTREACHED();
// Fall through.
case ResolvedUnderlinePosition::kRoman:
return ComputeUnderlineOffsetForRoman(font_metrics,
text_decoration_thickness);
case ResolvedUnderlinePosition::kUnder:
// Position underline at the under edge of the lowest element's
// content box.
return ComputeUnderlineOffsetForUnder(
style, inline_text_box, decorating_box, text_decoration_thickness,
LineVerticalPositionType::BottomOfEmHeight);
}
}
static const int kMisspellingLineThickness = 3;
LayoutObject& InlineTextBoxPainter::InlineLayoutObject() const {
return *LineLayoutAPIShim::LayoutObjectFrom(
inline_text_box_.GetLineLayoutItem());
}
bool InlineTextBoxPainter::PaintsMarkerHighlights(
const LayoutObject& layout_object) {
return layout_object.GetNode() &&
layout_object.GetDocument().Markers().HasMarkers(
layout_object.GetNode());
}
static void PrepareContextForDecoration(
GraphicsContext& context,
GraphicsContextStateSaver& state_saver,
bool is_horizontal,
const TextPainterBase::Style& text_style,
const LayoutTextCombine* combined_text,
const LayoutRect& box_rect) {
TextPainterBase::UpdateGraphicsContext(context, text_style, is_horizontal,
state_saver);
if (combined_text) {
context.ConcatCTM(
TextPainterBase::Rotation(box_rect, TextPainterBase::kClockwise));
}
}
static void RestoreContextFromDecoration(GraphicsContext& context,
const LayoutTextCombine* combined_text,
const LayoutRect& box_rect) {
if (combined_text) {
context.ConcatCTM(TextPainterBase::Rotation(
box_rect, TextPainterBase::kCounterclockwise));
}
}
static void ComputeOriginAndWidthForBox(const InlineTextBox& box,
LayoutPoint& local_origin,
LayoutUnit& width) {
if (box.Truncation() != kCNoTruncation) {
bool ltr = box.IsLeftToRightDirection();
bool flow_is_ltr =
box.GetLineLayoutItem().Style()->IsLeftToRightDirection();
width = LayoutUnit(box.GetLineLayoutItem().Width(
ltr == flow_is_ltr ? box.Start() : box.Start() + box.Truncation(),
ltr == flow_is_ltr ? box.Truncation() : box.Len() - box.Truncation(),
box.TextPos(), flow_is_ltr ? TextDirection::kLtr : TextDirection::kRtl,
box.IsFirstLineStyle()));
if (!flow_is_ltr) {
local_origin.Move(box.LogicalWidth() - width, LayoutUnit());
}
}
}
static void PaintDecorationsExceptLineThrough(
TextPainter& text_painter,
bool& has_line_through_decoration,
const InlineTextBox& box,
const DecorationInfo& decoration_info,
const LineLayoutItem& decorating_box,
const PaintInfo& paint_info,
const Vector<AppliedTextDecoration>& decorations) {
GraphicsContext& context = paint_info.context;
GraphicsContextStateSaver state_saver(context);
context.SetStrokeThickness(decoration_info.thickness);
// text-underline-position may flip underline and overline.
ResolvedUnderlinePosition underline_position =
decoration_info.underline_position;
bool flip_underline_and_overline = false;
if (underline_position == ResolvedUnderlinePosition::kOver) {
flip_underline_and_overline = true;
underline_position = ResolvedUnderlinePosition::kUnder;
}
for (const AppliedTextDecoration& decoration : decorations) {
TextDecoration lines = decoration.Lines();
bool has_underline = EnumHasFlags(lines, TextDecoration::kUnderline);
bool has_overline = EnumHasFlags(lines, TextDecoration::kOverline);
if (flip_underline_and_overline) {
std::swap(has_underline, has_overline);
}
if (has_underline && decoration_info.font_data) {
const int underline_offset = ComputeUnderlineOffset(
underline_position, *decoration_info.style,
decoration_info.font_data->GetFontMetrics(), &box, decorating_box,
decoration_info.thickness);
text_painter.PaintDecorationUnderOrOverLine(
context, decoration_info, decoration, underline_offset,
decoration_info.double_offset);
}
if (has_overline) {
const int overline_offset = ComputeUnderlineOffsetForUnder(
*decoration_info.style, &box, decorating_box,
decoration_info.thickness,
flip_underline_and_overline ? LineVerticalPositionType::TopOfEmHeight
: LineVerticalPositionType::TextTop);
text_painter.PaintDecorationUnderOrOverLine(
context, decoration_info, decoration, overline_offset,
-decoration_info.double_offset);
}
// We could instead build a vector of the TextDecoration instances needing
// line-through but this is a rare case so better to avoid vector overhead.
has_line_through_decoration |=
EnumHasFlags(lines, TextDecoration::kLineThrough);
}
}
void InlineTextBoxPainter::Paint(const PaintInfo& paint_info,
const LayoutPoint& paint_offset) {
if (!ShouldPaintTextBox(paint_info))
return;
DCHECK(!ShouldPaintSelfOutline(paint_info.phase) &&
!ShouldPaintDescendantOutlines(paint_info.phase));
LayoutRect logical_visual_overflow = inline_text_box_.LogicalOverflowRect();
LayoutUnit logical_start =
logical_visual_overflow.X() +
(inline_text_box_.IsHorizontal() ? paint_offset.X() : paint_offset.Y());
LayoutUnit logical_extent = logical_visual_overflow.Width();
// We round the y-axis to ensure consistent line heights.
LayoutPoint adjusted_paint_offset(paint_offset);
if (inline_text_box_.IsHorizontal()) {
adjusted_paint_offset.SetY(LayoutUnit(adjusted_paint_offset.Y().Round()));
if (!paint_info.GetCullRect().IntersectsHorizontalRange(
logical_start, logical_start + logical_extent))
return;
} else {
adjusted_paint_offset.SetX(LayoutUnit(adjusted_paint_offset.X().Round()));
if (!paint_info.GetCullRect().IntersectsVerticalRange(
logical_start, logical_start + logical_extent))
return;
}
bool is_printing = paint_info.IsPrinting();
// Determine whether or not we're selected.
bool have_selection =
!is_printing && paint_info.phase != kPaintPhaseTextClip &&
inline_text_box_.GetSelectionState() != SelectionState::kNone;
if (!have_selection && paint_info.phase == kPaintPhaseSelection) {
// When only painting the selection, don't bother to paint if there is none.
return;
}
// The text clip phase already has a LayoutObjectDrawingRecorder. Text clips
// are initiated only in BoxPainter::paintFillLayer, which is already within a
// LayoutObjectDrawingRecorder.
Optional<DrawingRecorder> drawing_recorder;
if (paint_info.phase != kPaintPhaseTextClip) {
if (DrawingRecorder::UseCachedDrawingIfPossible(
paint_info.context, inline_text_box_,
DisplayItem::PaintPhaseToDrawingType(paint_info.phase)))
return;
LayoutRect paint_rect(logical_visual_overflow);
inline_text_box_.LogicalRectToPhysicalRect(paint_rect);
if (paint_info.phase != kPaintPhaseSelection &&
(have_selection || PaintsMarkerHighlights(InlineLayoutObject())))
paint_rect.Unite(inline_text_box_.LocalSelectionRect(
inline_text_box_.Start(),
inline_text_box_.Start() + inline_text_box_.Len()));
paint_rect.MoveBy(adjusted_paint_offset);
drawing_recorder.emplace(
paint_info.context, inline_text_box_,
DisplayItem::PaintPhaseToDrawingType(paint_info.phase),
FloatRect(paint_rect));
}
GraphicsContext& context = paint_info.context;
const ComputedStyle& style_to_use =
inline_text_box_.GetLineLayoutItem().StyleRef(
inline_text_box_.IsFirstLineStyle());
LayoutPoint box_origin(inline_text_box_.PhysicalLocation() + paint_offset);
if (inline_text_box_.IsHorizontal()) {
box_origin.SetY(LayoutUnit(box_origin.Y().Round()));
} else {
box_origin.SetX(LayoutUnit(box_origin.X().Round()));
}
LayoutRect box_rect(box_origin, LayoutSize(inline_text_box_.LogicalWidth(),
inline_text_box_.LogicalHeight()));
int length = inline_text_box_.Len();
const String& layout_item_string =
inline_text_box_.GetLineLayoutItem().GetText();
// TODO(szager): Figure out why this CHECK sometimes fails, it shouldn't.
CHECK(inline_text_box_.Start() + length <= layout_item_string.length());
String first_line_string;
if (inline_text_box_.IsFirstLineStyle()) {
first_line_string = layout_item_string;
ApplyTextTransform(
inline_text_box_.GetLineLayoutItem().Style(
inline_text_box_.IsFirstLineStyle()),
first_line_string,
inline_text_box_.GetLineLayoutItem().PreviousCharacter());
}
StringView string =
StringView(inline_text_box_.IsFirstLineStyle() ? first_line_string
: layout_item_string,
inline_text_box_.Start(), length);
int maximum_length = inline_text_box_.GetLineLayoutItem().TextLength() -
inline_text_box_.Start();
StringBuilder characters_with_hyphen;
TextRun text_run = inline_text_box_.ConstructTextRun(
style_to_use, string, maximum_length,
inline_text_box_.HasHyphen() ? &characters_with_hyphen : 0);
if (inline_text_box_.HasHyphen())
length = text_run.length();
bool should_rotate = false;
LayoutTextCombine* combined_text = nullptr;
if (!inline_text_box_.IsHorizontal()) {
if (style_to_use.HasTextCombine() &&
inline_text_box_.GetLineLayoutItem().IsCombineText()) {
combined_text = &ToLayoutTextCombine(InlineLayoutObject());
if (!combined_text->IsCombined())
combined_text = nullptr;
}
if (combined_text) {
combined_text->UpdateFont();
box_rect.SetWidth(combined_text->InlineWidthForLayout());
// Justfication applies to before and after the combined text as if
// it is an ideographic character, and is prohibited inside the
// combined text.
if (float expansion = text_run.Expansion()) {
text_run.SetExpansion(0);
if (text_run.AllowsLeadingExpansion()) {
if (text_run.AllowsTrailingExpansion())
expansion /= 2;
LayoutSize offset =
LayoutSize(LayoutUnit(), LayoutUnit::FromFloatRound(expansion));
box_origin.Move(offset);
box_rect.Move(offset);
}
}
} else {
should_rotate = true;
context.ConcatCTM(
TextPainterBase::Rotation(box_rect, TextPainterBase::kClockwise));
}
}
// Determine text colors.
TextPainterBase::Style text_style = TextPainterBase::TextPaintingStyle(
inline_text_box_.GetLineLayoutItem().GetDocument(), style_to_use,
paint_info);
TextPainterBase::Style selection_style = TextPainter::SelectionPaintingStyle(
inline_text_box_.GetLineLayoutItem(), have_selection, paint_info,
text_style);
bool paint_selected_text_only = (paint_info.phase == kPaintPhaseSelection);
bool paint_selected_text_separately =
!paint_selected_text_only && text_style != selection_style;
// Set our font.
const Font& font = style_to_use.GetFont();
const SimpleFontData* font_data = font.PrimaryFont();
DCHECK(font_data);
int ascent = font_data ? font_data->GetFontMetrics().Ascent() : 0;
LayoutPoint text_origin(box_origin.X(), box_origin.Y() + ascent);
const DocumentMarkerVector& markers_to_paint = ComputeMarkersToPaint();
// 1. Paint backgrounds behind text if needed. Examples of such backgrounds
// include selection and composition highlights.
if (paint_info.phase != kPaintPhaseSelection &&
paint_info.phase != kPaintPhaseTextClip && !is_printing) {
PaintDocumentMarkers(markers_to_paint, paint_info, box_origin, style_to_use,
font, DocumentMarkerPaintPhase::kBackground);
if (have_selection) {
if (combined_text)
PaintSelection<InlineTextBoxPainter::PaintOptions::kCombinedText>(
context, box_rect, style_to_use, font, selection_style.fill_color,
combined_text);
else
PaintSelection<InlineTextBoxPainter::PaintOptions::kNormal>(
context, box_rect, style_to_use, font, selection_style.fill_color);
}
}
// 2. Now paint the foreground, including text and decorations.
int selection_start = 0;
int selection_end = 0;
if (paint_selected_text_only || paint_selected_text_separately)
inline_text_box_.SelectionStartEnd(selection_start, selection_end);
bool respect_hyphen =
selection_end == static_cast<int>(inline_text_box_.Len()) &&
inline_text_box_.HasHyphen();
if (respect_hyphen)
selection_end = text_run.length();
bool ltr = inline_text_box_.IsLeftToRightDirection();
bool flow_is_ltr = inline_text_box_.GetLineLayoutItem()
.ContainingBlock()
.Style()
->IsLeftToRightDirection();
const PaintOffsets& selection_offsets =
ApplyTruncationToPaintOffsets({static_cast<unsigned>(selection_start),
static_cast<unsigned>(selection_end)});
selection_start = selection_offsets.start;
selection_end = selection_offsets.end;
if (inline_text_box_.Truncation() != kCNoTruncation) {
// In a mixed-direction flow the ellipsis is at the start of the text
// rather than at the end of it.
length =
ltr == flow_is_ltr ? inline_text_box_.Truncation() : text_run.length();
}
TextPainter text_painter(context, font, text_run, text_origin, box_rect,
inline_text_box_.IsHorizontal());
TextEmphasisPosition emphasis_mark_position;
bool has_text_emphasis = inline_text_box_.GetEmphasisMarkPosition(
style_to_use, emphasis_mark_position);
if (has_text_emphasis)
text_painter.SetEmphasisMark(style_to_use.TextEmphasisMarkString(),
emphasis_mark_position);
if (combined_text)
text_painter.SetCombinedText(combined_text);
if (inline_text_box_.Truncation() != kCNoTruncation && ltr != flow_is_ltr)
text_painter.SetEllipsisOffset(inline_text_box_.Truncation());
if (!paint_selected_text_only) {
// Paint text decorations except line-through.
DecorationInfo decoration_info;
bool has_line_through_decoration = false;
if (style_to_use.TextDecorationsInEffect() != TextDecoration::kNone &&
inline_text_box_.Truncation() != kCFullTruncation) {
LayoutPoint local_origin = LayoutPoint(box_origin);
LayoutUnit width = inline_text_box_.LogicalWidth();
ComputeOriginAndWidthForBox(inline_text_box_, local_origin, width);
const LineLayoutItem& decorating_box =
EnclosingUnderlineObject(&inline_text_box_);
const ComputedStyle* decorating_box_style =
decorating_box ? decorating_box.Style() : nullptr;
text_painter.ComputeDecorationInfo(decoration_info, box_origin,
local_origin, width,
inline_text_box_.Root().BaselineType(),
style_to_use, decorating_box_style);
GraphicsContextStateSaver state_saver(context, false);
PrepareContextForDecoration(context, state_saver,
inline_text_box_.IsHorizontal(), text_style,
combined_text, box_rect);
PaintDecorationsExceptLineThrough(
text_painter, has_line_through_decoration, inline_text_box_,
decoration_info, decorating_box, paint_info,
style_to_use.AppliedTextDecorations());
RestoreContextFromDecoration(context, combined_text, box_rect);
}
int start_offset = 0;
int end_offset = length;
// Where the text and its flow have opposite directions then our offset into
// the text given by |truncation| is at the start of the part that will be
// visible.
if (inline_text_box_.Truncation() != kCNoTruncation && ltr != flow_is_ltr) {
start_offset = inline_text_box_.Truncation();
end_offset = text_run.length();
}
if (paint_selected_text_separately && selection_start < selection_end) {
start_offset = selection_end;
end_offset = selection_start;
}
text_painter.Paint(start_offset, end_offset, length, text_style);
// Paint line-through decoration if needed.
if (has_line_through_decoration) {
GraphicsContextStateSaver state_saver(context, false);
PrepareContextForDecoration(context, state_saver,
inline_text_box_.IsHorizontal(), text_style,
combined_text, box_rect);
text_painter.PaintDecorationsOnlyLineThrough(
decoration_info, paint_info, style_to_use.AppliedTextDecorations());
RestoreContextFromDecoration(context, combined_text, box_rect);
}
}
if ((paint_selected_text_only || paint_selected_text_separately) &&
selection_start < selection_end) {
// paint only the text that is selected
text_painter.Paint(selection_start, selection_end, length, selection_style);
}
if (paint_info.phase == kPaintPhaseForeground) {
PaintDocumentMarkers(markers_to_paint, paint_info, box_origin, style_to_use,
font, DocumentMarkerPaintPhase::kForeground);
}
if (should_rotate) {
context.ConcatCTM(TextPainterBase::Rotation(
box_rect, TextPainterBase::kCounterclockwise));
}
}
bool InlineTextBoxPainter::ShouldPaintTextBox(const PaintInfo& paint_info) {
// When painting selection, we want to include a highlight when the
// selection spans line breaks. In other cases such as invisible elements
// or those with no text that are not line breaks, we can skip painting
// wholesale.
// TODO(wkorman): Constrain line break painting to appropriate paint phase.
// This code path is only called in PaintPhaseForeground whereas we would
// expect PaintPhaseSelection. The existing haveSelection logic in paint()
// tests for != PaintPhaseTextClip.
if (inline_text_box_.GetLineLayoutItem().Style()->Visibility() !=
EVisibility::kVisible ||
inline_text_box_.Truncation() == kCFullTruncation ||
!inline_text_box_.Len())
return false;
return true;
}
InlineTextBoxPainter::PaintOffsets
InlineTextBoxPainter::ApplyTruncationToPaintOffsets(
const InlineTextBoxPainter::PaintOffsets& offsets) {
const unsigned short truncation = inline_text_box_.Truncation();
if (truncation == kCNoTruncation)
return offsets;
// If we're in mixed-direction mode (LTR text in an RTL box or vice-versa),
// the truncation ellipsis is at the *start* of the text box rather than the
// end.
bool ltr = inline_text_box_.IsLeftToRightDirection();
bool flow_is_ltr = inline_text_box_.GetLineLayoutItem()
.ContainingBlock()
.Style()
->IsLeftToRightDirection();
// truncation is relative to the start of the InlineTextBox, not the text
// node.
if (ltr == flow_is_ltr) {
return {std::min<unsigned>(offsets.start, truncation),
std::min<unsigned>(offsets.end, truncation)};
}
return {std::max<unsigned>(offsets.start, truncation),
std::max<unsigned>(offsets.end, truncation)};
}
InlineTextBoxPainter::PaintOffsets InlineTextBoxPainter::MarkerPaintStartAndEnd(
const DocumentMarker& marker) {
// Text match markers are painted differently (in an inline text box truncated
// by an ellipsis, they paint over the ellipsis) and so should not use this
// function.
DCHECK_NE(DocumentMarker::kTextMatch, marker.GetType());
DCHECK(inline_text_box_.Truncation() != kCFullTruncation);
DCHECK(inline_text_box_.Len());
// Start painting at the beginning of the text or the specified underline
// start offset, whichever is greater.
unsigned paint_start =
std::max(inline_text_box_.Start(), marker.StartOffset());
// Cap the maximum paint start to the last character in the text box.
paint_start = std::min(paint_start, inline_text_box_.end());
// End painting just past the end of the text or the specified underline end
// offset, whichever is less.
unsigned paint_end = std::min(
inline_text_box_.end() + 1,
marker.EndOffset()); // end() points at the last char, not past it.
// paint_start and paint_end are currently relative to the start of the text
// node. Subtract to make them relative to the start of the InlineTextBox.
paint_start -= inline_text_box_.Start();
paint_end -= inline_text_box_.Start();
return ApplyTruncationToPaintOffsets({paint_start, paint_end});
}
void InlineTextBoxPainter::PaintSingleMarkerBackgroundRun(
GraphicsContext& context,
const LayoutPoint& box_origin,
const ComputedStyle& style,
const Font& font,
Color background_color,
int start_pos,
int end_pos) {
if (background_color == Color::kTransparent)
return;
int delta_y =
(inline_text_box_.GetLineLayoutItem().Style()->IsFlippedLinesWritingMode()
? inline_text_box_.Root().SelectionBottom() -
inline_text_box_.LogicalBottom()
: inline_text_box_.LogicalTop() -
inline_text_box_.Root().SelectionTop())
.ToInt();
int sel_height = inline_text_box_.Root().SelectionHeight().ToInt();
FloatPoint local_origin(box_origin.X().ToFloat(),
box_origin.Y().ToFloat() - delta_y);
context.DrawHighlightForText(font, inline_text_box_.ConstructTextRun(style),
local_origin, sel_height, background_color,
start_pos, end_pos);
}
DocumentMarkerVector InlineTextBoxPainter::ComputeMarkersToPaint() const {
// We don't render composition or spelling markers that overlap suggestion
// markers.
Node* const node = inline_text_box_.GetLineLayoutItem().GetNode();
if (!node)
return DocumentMarkerVector();
DocumentMarkerController& document_marker_controller =
inline_text_box_.GetLineLayoutItem().GetDocument().Markers();
// Note: DocumentMarkerController::MarkersFor() returns markers sorted by
// start offset.
const DocumentMarkerVector& suggestion_markers =
document_marker_controller.MarkersFor(node, DocumentMarker::kSuggestion);
if (suggestion_markers.IsEmpty()) {
// If there are no suggestion markers, we can return early as a minor
// performance optimization.
DocumentMarker::MarkerTypes remaining_types = DocumentMarker::AllMarkers();
remaining_types.Remove(DocumentMarker::kSuggestion);
return document_marker_controller.MarkersFor(node, remaining_types);
}
const DocumentMarkerVector& markers_overridden_by_suggestion_markers =
document_marker_controller.MarkersFor(
node, DocumentMarker::kComposition | DocumentMarker::kSpelling);
Vector<unsigned> suggestion_starts;
Vector<unsigned> suggestion_ends;
for (const DocumentMarker* suggestion_marker : suggestion_markers) {
suggestion_starts.push_back(suggestion_marker->StartOffset());
suggestion_ends.push_back(suggestion_marker->EndOffset());
}
std::sort(suggestion_starts.begin(), suggestion_starts.end());
std::sort(suggestion_ends.begin(), suggestion_ends.end());
unsigned suggestion_starts_index = 0;
unsigned suggestion_ends_index = 0;
unsigned number_suggestions_currently_inside = 0;
DocumentMarkerVector markers_to_paint;
for (DocumentMarker* marker : markers_overridden_by_suggestion_markers) {
while (suggestion_starts_index < suggestion_starts.size() &&
suggestion_starts[suggestion_starts_index] <=
marker->StartOffset()) {
++suggestion_starts_index;
++number_suggestions_currently_inside;
}
while (suggestion_ends_index < suggestion_ends.size() &&
suggestion_ends[suggestion_ends_index] <= marker->StartOffset()) {
++suggestion_ends_index;
--number_suggestions_currently_inside;
}
// At this point, number_suggestions_currently_inside should be equal to the
// number of suggestion markers overlapping the point marker->StartOffset()
// (marker endpoints don't count as overlapping).
// Marker is overlapped by a suggestion marker, do not paint.
if (number_suggestions_currently_inside)
continue;
// Verify that no suggestion marker starts before the current marker ends.
if (suggestion_starts_index < suggestion_starts.size() &&
suggestion_starts[suggestion_starts_index] < marker->EndOffset())
continue;
markers_to_paint.push_back(marker);
}
markers_to_paint.AppendVector(suggestion_markers);
DocumentMarker::MarkerTypes remaining_types = DocumentMarker::AllMarkers();
remaining_types.Remove(DocumentMarker::kComposition |
DocumentMarker::kSpelling |
DocumentMarker::kSuggestion);
markers_to_paint.AppendVector(
document_marker_controller.MarkersFor(node, remaining_types));
return markers_to_paint;
}
void InlineTextBoxPainter::PaintDocumentMarkers(
const DocumentMarkerVector& markers_to_paint,
const PaintInfo& paint_info,
const LayoutPoint& box_origin,
const ComputedStyle& style,
const Font& font,
DocumentMarkerPaintPhase marker_paint_phase) {
if (!inline_text_box_.GetLineLayoutItem().GetNode())
return;
DCHECK(inline_text_box_.Truncation() != kCFullTruncation);
DCHECK(inline_text_box_.Len());
DocumentMarkerVector::const_iterator marker_it = markers_to_paint.begin();
// Give any document markers that touch this run a chance to draw before the
// text has been drawn. Note end() points at the last char, not one past it
// like endOffset and ranges do.
for (; marker_it != markers_to_paint.end(); ++marker_it) {
DCHECK(*marker_it);
const DocumentMarker& marker = **marker_it;
if (marker.EndOffset() <= inline_text_box_.Start()) {
// marker is completely before this run. This might be a marker that sits
// before the first run we draw, or markers that were within runs we
// skipped due to truncation.
continue;
}
if (marker.StartOffset() > inline_text_box_.end()) {
// marker is completely after this run, bail. A later run will paint it.
break;
}
// marker intersects this run. Paint it.
switch (marker.GetType()) {
case DocumentMarker::kSpelling:
if (marker_paint_phase == DocumentMarkerPaintPhase::kBackground)
continue;
inline_text_box_.PaintDocumentMarker(paint_info.context, box_origin,
marker, style, font, false);
break;
case DocumentMarker::kGrammar:
if (marker_paint_phase == DocumentMarkerPaintPhase::kBackground)
continue;
inline_text_box_.PaintDocumentMarker(paint_info.context, box_origin,
marker, style, font, true);
break;
case DocumentMarker::kTextMatch:
if (marker_paint_phase == DocumentMarkerPaintPhase::kBackground) {
inline_text_box_.PaintTextMatchMarkerBackground(
paint_info, box_origin, ToTextMatchMarker(marker), style, font);
} else {
inline_text_box_.PaintTextMatchMarkerForeground(
paint_info, box_origin, ToTextMatchMarker(marker), style, font);
}
break;
case DocumentMarker::kComposition:
case DocumentMarker::kActiveSuggestion:
case DocumentMarker::kSuggestion: {
const StyleableMarker& styleable_marker = ToStyleableMarker(marker);
if (marker_paint_phase == DocumentMarkerPaintPhase::kBackground) {
const PaintOffsets marker_offsets =
MarkerPaintStartAndEnd(styleable_marker);
PaintSingleMarkerBackgroundRun(
paint_info.context, box_origin, style, font,
styleable_marker.BackgroundColor(), marker_offsets.start,
marker_offsets.end);
} else {
PaintStyleableMarkerUnderline(paint_info.context, box_origin,
styleable_marker, style, font);
}
} break;
default:
// Marker is not painted, or painting code has not been added yet
break;
}
}
}
namespace {
#if !defined(OS_MACOSX)
static const float kMarkerWidth = 4;
static const float kMarkerHeight = 2;
sk_sp<PaintRecord> RecordMarker(DocumentMarker::MarkerType marker_type) {
SkColor color =
(marker_type == DocumentMarker::kGrammar)
? LayoutTheme::GetTheme().PlatformGrammarMarkerUnderlineColor().Rgb()
: LayoutTheme::GetTheme()
.PlatformSpellingMarkerUnderlineColor()
.Rgb();
// Record the path equivalent to this legacy pattern:
// X o o X o o X
// o X o o X o
// Adjust the phase such that f' == 0 is "pixel"-centered
// (for optimal rasterization at native rez).
SkPath path;
path.moveTo(kMarkerWidth * -3 / 8, kMarkerHeight * 3 / 4);
path.cubicTo(kMarkerWidth * -1 / 8, kMarkerHeight * 3 / 4,
kMarkerWidth * -1 / 8, kMarkerHeight * 1 / 4,
kMarkerWidth * 1 / 8, kMarkerHeight * 1 / 4);
path.cubicTo(kMarkerWidth * 3 / 8, kMarkerHeight * 1 / 4,
kMarkerWidth * 3 / 8, kMarkerHeight * 3 / 4,
kMarkerWidth * 5 / 8, kMarkerHeight * 3 / 4);
path.cubicTo(kMarkerWidth * 7 / 8, kMarkerHeight * 3 / 4,
kMarkerWidth * 7 / 8, kMarkerHeight * 1 / 4,
kMarkerWidth * 9 / 8, kMarkerHeight * 1 / 4);
PaintFlags flags;
flags.setAntiAlias(true);
flags.setColor(color);
flags.setStyle(PaintFlags::kStroke_Style);
flags.setStrokeWidth(kMarkerHeight * 1 / 2);
PaintRecorder recorder;
recorder.beginRecording(kMarkerWidth, kMarkerHeight);
recorder.getRecordingCanvas()->drawPath(path, flags);
return recorder.finishRecordingAsPicture();
}
#else // defined(OS_MACOSX)
static const float kMarkerWidth = 4;
static const float kMarkerHeight = 3;
sk_sp<PaintRecord> RecordMarker(DocumentMarker::MarkerType marker_type) {
SkColor color =
(marker_type == DocumentMarker::kGrammar)
? LayoutTheme::GetTheme().PlatformGrammarMarkerUnderlineColor().Rgb()
: LayoutTheme::GetTheme()
.PlatformSpellingMarkerUnderlineColor()
.Rgb();
// Match the artwork used by the Mac.
static const float kR = 1.5f;
// top->bottom translucent gradient.
const SkColor colors[2] = {
SkColorSetARGB(0x48,
SkColorGetR(color),
SkColorGetG(color),
SkColorGetB(color)),
color
};
const SkPoint pts[2] = {
SkPoint::Make(0, 0),
SkPoint::Make(0, 2 * kR)
};
PaintFlags flags;
flags.setAntiAlias(true);
flags.setColor(color);
flags.setShader(PaintShader::MakeLinearGradient(
pts, colors, nullptr, ARRAY_SIZE(colors), SkShader::kClamp_TileMode));
PaintRecorder recorder;
recorder.beginRecording(kMarkerWidth, kMarkerHeight);
recorder.getRecordingCanvas()->drawOval(SkRect::MakeWH(2 * kR, 2 * kR),
flags);
return recorder.finishRecordingAsPicture();
}
#endif // defined(OS_MACOSX)
void DrawDocumentMarker(GraphicsContext& context,
const FloatPoint& pt,
float width,
DocumentMarker::MarkerType marker_type,
float zoom) {
DCHECK(marker_type == DocumentMarker::kSpelling ||
marker_type == DocumentMarker::kGrammar);
DEFINE_STATIC_LOCAL(PaintRecord*, spelling_marker,
(RecordMarker(DocumentMarker::kSpelling).release()));
DEFINE_STATIC_LOCAL(PaintRecord*, grammar_marker,
(RecordMarker(DocumentMarker::kGrammar).release()));
const auto& marker = marker_type == DocumentMarker::kSpelling
? spelling_marker
: grammar_marker;
// Position already includes zoom and device scale factor.
SkScalar origin_x = WebCoreFloatToSkScalar(pt.X());
SkScalar origin_y = WebCoreFloatToSkScalar(pt.Y());
#if defined(OS_MACOSX)
// Make sure to draw only complete dots, and finish inside the marked text.
width -= fmodf(width, kMarkerWidth * zoom);
#else
// Offset it vertically by 1 so that there's some space under the text.
origin_y += 1;
#endif
const auto rect = SkRect::MakeWH(width, kMarkerHeight * zoom);
const auto local_matrix = SkMatrix::MakeScale(zoom, zoom);
PaintFlags flags;
flags.setAntiAlias(true);
flags.setShader(PaintShader::MakePaintRecord(
sk_ref_sp(marker), FloatRect(0, 0, kMarkerWidth, kMarkerHeight),
SkShader::kRepeat_TileMode, SkShader::kClamp_TileMode, &local_matrix));
// Apply the origin translation as a global transform. This ensures that the
// shader local matrix depends solely on zoom => Skia can reuse the same
// cached tile for all markers at a given zoom level.
GraphicsContextStateSaver saver(context);
context.Translate(origin_x, origin_y);
context.DrawRect(rect, flags);
}
} // anonymous ns
void InlineTextBoxPainter::PaintDocumentMarker(GraphicsContext& context,
const LayoutPoint& box_origin,
const DocumentMarker& marker,
const ComputedStyle& style,
const Font& font,
bool grammar) {
// Never print spelling/grammar markers (5327887)
if (inline_text_box_.GetLineLayoutItem().GetDocument().Printing())
return;
if (inline_text_box_.Truncation() == kCFullTruncation)
return;
LayoutUnit start; // start of line to draw, relative to tx
LayoutUnit width = inline_text_box_.LogicalWidth(); // how much line to draw
// Determine whether we need to measure text
bool marker_spans_whole_box = true;
if (inline_text_box_.Start() <= marker.StartOffset())
marker_spans_whole_box = false;
if ((inline_text_box_.end() + 1) !=
marker.EndOffset()) // end points at the last char, not past it
marker_spans_whole_box = false;
if (inline_text_box_.Truncation() != kCNoTruncation)
marker_spans_whole_box = false;
if (!marker_spans_whole_box || grammar) {
const PaintOffsets& marker_offsets = MarkerPaintStartAndEnd(marker);
// Calculate start & width
int delta_y = (inline_text_box_.GetLineLayoutItem()
.Style()
->IsFlippedLinesWritingMode()
? inline_text_box_.Root().SelectionBottom() -
inline_text_box_.LogicalBottom()
: inline_text_box_.LogicalTop() -
inline_text_box_.Root().SelectionTop())
.ToInt();
int sel_height = inline_text_box_.Root().SelectionHeight().ToInt();
LayoutPoint start_point(box_origin.X(), box_origin.Y() - delta_y);
TextRun run = inline_text_box_.ConstructTextRun(style);
// FIXME: Convert the document markers to float rects.
IntRect marker_rect = EnclosingIntRect(
font.SelectionRectForText(run, FloatPoint(start_point), sel_height,
marker_offsets.start, marker_offsets.end));
start = marker_rect.X() - start_point.X();
width = LayoutUnit(marker_rect.Width());
}
// IMPORTANT: The misspelling underline is not considered when calculating the
// text bounds, so we have to make sure to fit within those bounds. This
// means the top pixel(s) of the underline will overlap the bottom pixel(s) of
// the glyphs in smaller font sizes. The alternatives are to increase the
// line spacing (bad!!) or decrease the underline thickness. The overlap is
// actually the most useful, and matches what AppKit does. So, we generally
// place the underline at the bottom of the text, but in larger fonts that's
// not so good so we pin to two pixels under the baseline.
int line_thickness = kMisspellingLineThickness;
const SimpleFontData* font_data =
inline_text_box_.GetLineLayoutItem()
.Style(inline_text_box_.IsFirstLineStyle())
->GetFont()
.PrimaryFont();
DCHECK(font_data);
int baseline = font_data ? font_data->GetFontMetrics().Ascent() : 0;
int descent = (inline_text_box_.LogicalHeight() - baseline).ToInt();
int underline_offset;
if (descent <= (line_thickness + 2)) {
// Place the underline at the very bottom of the text in small/medium fonts.
underline_offset =
(inline_text_box_.LogicalHeight() - line_thickness).ToInt();
} else {
// In larger fonts, though, place the underline up near the baseline to
// prevent a big gap.
underline_offset = baseline + 2;
}
DrawDocumentMarker(context,
FloatPoint((box_origin.X() + start).ToFloat(),
(box_origin.Y() + underline_offset).ToFloat()),
width.ToFloat(), marker.GetType(), style.EffectiveZoom());
}
template <InlineTextBoxPainter::PaintOptions options>
void InlineTextBoxPainter::PaintSelection(GraphicsContext& context,
const LayoutRect& box_rect,
const ComputedStyle& style,
const Font& font,
Color text_color,
LayoutTextCombine* combined_text) {
// See if we have a selection to paint at all.
int s_pos, e_pos;
inline_text_box_.SelectionStartEnd(s_pos, e_pos);
if (s_pos >= e_pos)
return;
Color c = inline_text_box_.GetLineLayoutItem().SelectionBackgroundColor();
if (!c.Alpha())
return;
// If the text color ends up being the same as the selection background,
// invert the selection background.
if (text_color == c)
c = Color(0xff - c.Red(), 0xff - c.Green(), 0xff - c.Blue());
// If the text is truncated, let the thing being painted in the truncation
// draw its own highlight.
unsigned start = inline_text_box_.Start();
int length = inline_text_box_.Len();
bool ltr = inline_text_box_.IsLeftToRightDirection();
bool flow_is_ltr = inline_text_box_.GetLineLayoutItem()
.ContainingBlock()
.Style()
->IsLeftToRightDirection();
if (inline_text_box_.Truncation() != kCNoTruncation) {
// In a mixed-direction flow the ellipsis is at the start of the text
// so we need to start after it. Otherwise we just need to make sure
// the end of the text is where the ellipsis starts.
if (ltr != flow_is_ltr)
s_pos = std::max<int>(s_pos, inline_text_box_.Truncation());
else
length = inline_text_box_.Truncation();
}
StringView string(inline_text_box_.GetLineLayoutItem().GetText(), start,
static_cast<unsigned>(length));
StringBuilder characters_with_hyphen;
bool respect_hyphen = e_pos == length && inline_text_box_.HasHyphen();
TextRun text_run = inline_text_box_.ConstructTextRun(
style, string,
inline_text_box_.GetLineLayoutItem().TextLength() -
inline_text_box_.Start(),
respect_hyphen ? &characters_with_hyphen : 0);
if (respect_hyphen)
e_pos = text_run.length();
GraphicsContextStateSaver state_saver(context);
if (options == InlineTextBoxPainter::PaintOptions::kCombinedText) {
DCHECK(combined_text);
// We can't use the height of m_inlineTextBox because LayoutTextCombine's
// inlineTextBox is horizontal within vertical flow
combined_text->TransformToInlineCoordinates(context, box_rect, true);
context.DrawHighlightForText(font, text_run,
FloatPoint(box_rect.Location()),
box_rect.Height().ToInt(), c, s_pos, e_pos);
return;
}
LayoutUnit selection_bottom = inline_text_box_.Root().SelectionBottom();
LayoutUnit selection_top = inline_text_box_.Root().SelectionTop();
int delta_y = RoundToInt(
inline_text_box_.GetLineLayoutItem().Style()->IsFlippedLinesWritingMode()
? selection_bottom - inline_text_box_.LogicalBottom()
: inline_text_box_.LogicalTop() - selection_top);
int sel_height = std::max(0, RoundToInt(selection_bottom - selection_top));
FloatPoint local_origin(box_rect.X().ToFloat(),
(box_rect.Y() - delta_y).ToFloat());
LayoutRect selection_rect = LayoutRect(font.SelectionRectForText(
text_run, local_origin, sel_height, s_pos, e_pos));
// For line breaks, just painting a selection where the line break itself
// is rendered is sufficient. Don't select it if there's an ellipsis
// there.
if (inline_text_box_.HasWrappedSelectionNewline() &&
inline_text_box_.Truncation() == kCNoTruncation &&
!inline_text_box_.IsLineBreak())
ExpandToIncludeNewlineForSelection(selection_rect);
// Line breaks report themselves as having zero width for layout purposes,
// and so will end up positioned at (0, 0), even though we paint their
// selection highlight with character width. For RTL then, we have to
// explicitly shift the selection rect over to paint in the right location.
if (!inline_text_box_.IsLeftToRightDirection() &&
inline_text_box_.IsLineBreak())
selection_rect.Move(-selection_rect.Width(), LayoutUnit());
if (!flow_is_ltr && !ltr && inline_text_box_.Truncation() != kCNoTruncation)
selection_rect.Move(
inline_text_box_.LogicalWidth() - selection_rect.Width(), LayoutUnit());
context.FillRect(FloatRect(selection_rect), c);
}
void InlineTextBoxPainter::ExpandToIncludeNewlineForSelection(
LayoutRect& rect) {
FloatRectOutsets outsets = FloatRectOutsets();
float space_width = inline_text_box_.NewlineSpaceWidth();
if (inline_text_box_.IsLeftToRightDirection())
outsets.SetRight(space_width);
else
outsets.SetLeft(space_width);
rect.Expand(outsets);
}
void InlineTextBoxPainter::PaintStyleableMarkerUnderline(
GraphicsContext& context,
const LayoutPoint& box_origin,
const StyleableMarker& marker,
const ComputedStyle& style,
const Font& font) {
if (marker.UnderlineColor() == Color::kTransparent)
return;
if (inline_text_box_.Truncation() == kCFullTruncation)
return;
const PaintOffsets marker_offsets = MarkerPaintStartAndEnd(marker);
const TextRun& run = inline_text_box_.ConstructTextRun(style);
// Pass 0 for height since we only care about the width
const FloatRect& marker_rect = font.SelectionRectForText(
run, FloatPoint(), 0, marker_offsets.start, marker_offsets.end);
// start of line to draw, relative to box_origin.X()
LayoutUnit start = LayoutUnit(marker_rect.X());
LayoutUnit width = LayoutUnit(marker_rect.Width());
// We need to have some space between underlines of subsequent clauses,
// because some input methods do not use different underline styles for those.
// We make each line shorter, which has a harmless side effect of shortening
// the first and last clauses, too.
start += 1;
width -= 2;
// Thick marked text underlines are 2px thick as long as there is room for the
// 2px line under the baseline. All other marked text underlines are 1px
// thick. If there's not enough space the underline will touch or overlap
// characters.
int line_thickness = 1;
const SimpleFontData* font_data =
inline_text_box_.GetLineLayoutItem()
.Style(inline_text_box_.IsFirstLineStyle())
->GetFont()
.PrimaryFont();
DCHECK(font_data);
int baseline = font_data ? font_data->GetFontMetrics().Ascent() : 0;
if (marker.IsThick() && inline_text_box_.LogicalHeight() - baseline >= 2)
line_thickness = 2;
context.SetStrokeColor(marker.UnderlineColor());
context.SetStrokeThickness(line_thickness);
context.DrawLineForText(
FloatPoint(
box_origin.X() + start,
(box_origin.Y() + inline_text_box_.LogicalHeight() - line_thickness)
.ToFloat()),
width);
}
void InlineTextBoxPainter::PaintTextMatchMarkerForeground(
const PaintInfo& paint_info,
const LayoutPoint& box_origin,
const TextMatchMarker& marker,
const ComputedStyle& style,
const Font& font) {
if (!InlineLayoutObject()
.GetFrame()
->GetEditor()
.MarkedTextMatchesAreHighlighted())
return;
const auto paint_offsets =
GetTextMatchMarkerPaintOffsets(marker, inline_text_box_);
TextRun run = inline_text_box_.ConstructTextRun(style);
Color text_color =
LayoutTheme::GetTheme().PlatformTextSearchColor(marker.IsActiveMatch());
if (style.VisitedDependentColor(CSSPropertyColor) == text_color)
return;
const SimpleFontData* font_data = font.PrimaryFont();
DCHECK(font_data);
if (!font_data)
return;
TextPainterBase::Style text_style;
text_style.current_color = text_style.fill_color = text_style.stroke_color =
text_style.emphasis_mark_color = text_color;
text_style.stroke_width = style.TextStrokeWidth();
text_style.shadow = 0;
LayoutRect box_rect(box_origin, LayoutSize(inline_text_box_.LogicalWidth(),
inline_text_box_.LogicalHeight()));
LayoutPoint text_origin(
box_origin.X(), box_origin.Y() + font_data->GetFontMetrics().Ascent());
TextPainter text_painter(paint_info.context, font, run, text_origin, box_rect,
inline_text_box_.IsHorizontal());
text_painter.Paint(paint_offsets.first, paint_offsets.second,
inline_text_box_.Len(), text_style);
}
void InlineTextBoxPainter::PaintTextMatchMarkerBackground(
const PaintInfo& paint_info,
const LayoutPoint& box_origin,
const TextMatchMarker& marker,
const ComputedStyle& style,
const Font& font) {
if (!LineLayoutAPIShim::LayoutObjectFrom(inline_text_box_.GetLineLayoutItem())
->GetFrame()
->GetEditor()
.MarkedTextMatchesAreHighlighted())
return;
const auto paint_offsets =
GetTextMatchMarkerPaintOffsets(marker, inline_text_box_);
TextRun run = inline_text_box_.ConstructTextRun(style);
Color color = LayoutTheme::GetTheme().PlatformTextSearchHighlightColor(
marker.IsActiveMatch());
GraphicsContext& context = paint_info.context;
GraphicsContextStateSaver state_saver(context);
LayoutRect box_rect(box_origin, LayoutSize(inline_text_box_.LogicalWidth(),
inline_text_box_.LogicalHeight()));
context.Clip(FloatRect(box_rect));
context.DrawHighlightForText(font, run, FloatPoint(box_origin),
box_rect.Height().ToInt(), color,
paint_offsets.first, paint_offsets.second);
}
} // namespace blink