blob: ff9cc08c1c42b13f5ee1417ccc9f64fc76bc3bf3 [file] [log] [blame]
// 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/paint/ng/ng_highlight_painter.h"
#include "third_party/blink/renderer/core/dom/node.h"
#include "third_party/blink/renderer/core/editing/editor.h"
#include "third_party/blink/renderer/core/editing/frame_selection.h"
#include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h"
#include "third_party/blink/renderer/core/editing/markers/styleable_marker.h"
#include "third_party/blink/renderer/core/editing/markers/text_marker_base.h"
#include "third_party/blink/renderer/core/layout/layout_object.h"
#include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_cursor.h"
#include "third_party/blink/renderer/core/paint/document_marker_painter.h"
#include "third_party/blink/renderer/core/paint/highlight_painting_utils.h"
#include "third_party/blink/renderer/core/paint/inline_text_box_painter.h"
#include "third_party/blink/renderer/core/paint/ng/ng_text_painter.h"
#include "third_party/blink/renderer/core/paint/paint_info.h"
namespace blink {
namespace {
DocumentMarkerVector ComputeMarkersToPaint(Node* node, bool is_ellipsis) {
// TODO(yoichio): Handle first-letter
auto* text_node = DynamicTo<Text>(node);
if (!text_node)
return DocumentMarkerVector();
// We don't paint any marker on ellipsis.
if (is_ellipsis)
return DocumentMarkerVector();
DocumentMarkerController& document_marker_controller =
node->GetDocument().Markers();
return document_marker_controller.ComputeMarkersToPaint(*text_node);
}
unsigned GetTextContentOffset(const Text& text, unsigned offset) {
// TODO(yoichio): Sanitize DocumentMarker around text length.
const Position position(text, std::min(offset, text.length()));
const NGOffsetMapping* const offset_mapping =
NGOffsetMapping::GetFor(position);
DCHECK(offset_mapping);
const base::Optional<unsigned>& ng_offset =
offset_mapping->GetTextContentOffset(position);
DCHECK(ng_offset.has_value());
return ng_offset.value();
}
// ClampOffset modifies |offset| fixed in a range of |text_fragment| start/end
// offsets.
// |offset| points not each character but each span between character.
// With that concept, we can clear catch what is inside start / end.
// Suppose we have "foo_bar"('_' is a space).
// There are 8 offsets for that:
// f o o _ b a r
// 0 1 2 3 4 5 6 7
// If "bar" is a TextFragment. That start(), end() {4, 7} correspond this
// offset. If a marker has StartOffset / EndOffset as {2, 6},
// ClampOffset returns{ 4,6 }, which represents "ba" on "foo_bar".
unsigned ClampOffset(unsigned offset, const NGFragmentItem& text_fragment) {
return std::min(std::max(offset, text_fragment.StartOffset()),
text_fragment.EndOffset());
}
PhysicalRect MarkerRectForForeground(const NGFragmentItem& text_fragment,
StringView text,
unsigned start_offset,
unsigned end_offset) {
LayoutUnit start_position, end_position;
std::tie(start_position, end_position) =
text_fragment.LineLeftAndRightForOffsets(text, start_offset, end_offset);
const LayoutUnit height = text_fragment.Size()
.ConvertToLogical(static_cast<WritingMode>(
text_fragment.Style().GetWritingMode()))
.block_size;
return {start_position, LayoutUnit(), end_position - start_position, height};
}
void PaintRect(GraphicsContext& context,
const PhysicalRect& rect,
const Color color) {
if (!color.Alpha())
return;
if (rect.size.IsEmpty())
return;
const IntRect pixel_snapped_rect = PixelSnappedIntRect(rect);
if (!pixel_snapped_rect.IsEmpty())
context.FillRect(pixel_snapped_rect, color);
}
void PaintRect(GraphicsContext& context,
const PhysicalOffset& location,
const PhysicalRect& rect,
const Color color) {
PaintRect(context, PhysicalRect(rect.offset + location, rect.size), color);
}
Color SelectionBackgroundColor(const Document& document,
const ComputedStyle& style,
Node* node,
Color text_color) {
const Color color = HighlightPaintingUtils::HighlightBackgroundColor(
document, style, node, kPseudoIdSelection);
if (!color.Alpha())
return Color();
// If the text color ends up being the same as the selection background,
// invert the selection background.
if (text_color == color)
return Color(0xff - color.Red(), 0xff - color.Green(), 0xff - color.Blue());
return color;
}
} // namespace
NGHighlightPainter::SelectionPaintState::SelectionPaintState(
const NGInlineCursor& containing_block)
: SelectionPaintState(containing_block,
containing_block.Current()
.GetLayoutObject()
->GetDocument()
.GetFrame()
->Selection()) {}
NGHighlightPainter::SelectionPaintState::SelectionPaintState(
const NGInlineCursor& containing_block,
const FrameSelection& frame_selection)
: selection_status_(
frame_selection.ComputeLayoutSelectionStatus(containing_block)),
state_(frame_selection.ComputeLayoutSelectionStateForCursor(
containing_block.Current())),
containing_block_(containing_block) {}
void NGHighlightPainter::SelectionPaintState::ComputeSelectionStyle(
const Document& document,
const ComputedStyle& style,
Node* node,
const PaintInfo& paint_info,
const TextPaintStyle& text_style) {
selection_style_ = TextPainterBase::SelectionPaintingStyle(
document, style, node, paint_info, text_style);
paint_selected_text_only_ =
(paint_info.phase == PaintPhase::kSelectionDragImage);
}
PhysicalRect NGHighlightPainter::SelectionPaintState::ComputeSelectionRect(
const PhysicalOffset& box_offset) {
if (!selection_rect_) {
selection_rect_ =
containing_block_.CurrentLocalSelectionRectForText(selection_status_);
selection_rect_->offset += box_offset;
}
return *selection_rect_;
}
// Logic is copied from InlineTextBoxPainter::PaintSelection.
// |selection_start| and |selection_end| should be between
// [text_fragment.StartOffset(), text_fragment.EndOffset()].
void NGHighlightPainter::SelectionPaintState::PaintSelectionBackground(
GraphicsContext& context,
Node* node,
const Document& document,
const ComputedStyle& style,
const base::Optional<AffineTransform>& rotation) {
const Color color = SelectionBackgroundColor(document, style, node,
selection_style_.fill_color);
if (!rotation) {
PaintRect(context, *selection_rect_, color);
return;
}
// PaintRect tries to pixel-snap the given rect, but if we’re painting in a
// non-horizontal writing mode, our context has been transformed, regressing
// tests like <paint/invalidation/repaint-across-writing-mode-boundary>. To
// fix this, we undo the transformation temporarily, then use the original
// physical coordinates (before MapSelectionRectIntoRotatedSpace).
DCHECK(selection_rect_before_rotation_);
context.ConcatCTM(rotation->Inverse());
PaintRect(context, *selection_rect_before_rotation_, color);
context.ConcatCTM(*rotation);
}
// Called before we paint vertical selected text under a rotation transform.
void NGHighlightPainter::SelectionPaintState::MapSelectionRectIntoRotatedSpace(
const AffineTransform& rotation) {
DCHECK(selection_rect_);
selection_rect_before_rotation_.emplace(*selection_rect_);
*selection_rect_ = PhysicalRect::EnclosingRect(
rotation.Inverse().MapRect(FloatRect(*selection_rect_)));
}
// Paint the selected text only.
void NGHighlightPainter::SelectionPaintState::PaintSelectedText(
NGTextPainter& text_painter,
unsigned length,
const TextPaintStyle& text_style,
DOMNodeId node_id) {
text_painter.PaintSelectedText(selection_status_.start, selection_status_.end,
length, text_style, selection_style_,
*selection_rect_, node_id);
}
// Paint the given text range in the given style, suppressing the text proper
// (painting shadows only) where selected.
void NGHighlightPainter::SelectionPaintState::
PaintSuppressingTextProperWhereSelected(NGTextPainter& text_painter,
unsigned start_offset,
unsigned end_offset,
unsigned length,
const TextPaintStyle& text_style,
DOMNodeId node_id) {
// First paint the shadows for the whole range.
if (text_style.shadow) {
text_painter.Paint(start_offset, end_offset, length, text_style, node_id,
NGTextPainter::kShadowsOnly);
}
// Then paint the text proper for any unselected parts in storage order, so
// that they’re always on top of the shadows.
if (start_offset < selection_status_.start) {
text_painter.Paint(start_offset, selection_status_.start, length,
text_style, node_id, NGTextPainter::kTextProperOnly);
}
if (selection_status_.end < end_offset) {
text_painter.Paint(selection_status_.end, end_offset, length, text_style,
node_id, NGTextPainter::kTextProperOnly);
}
}
NGHighlightPainter::NGHighlightPainter(
NGTextPainter& text_painter,
const PaintInfo& paint_info,
const NGInlineCursor& cursor,
const NGFragmentItem& fragment_item,
const PhysicalOffset& box_origin,
const ComputedStyle& style,
base::Optional<SelectionPaintState> selection,
bool is_printing)
: text_painter_(text_painter),
paint_info_(paint_info),
cursor_(cursor),
fragment_item_(fragment_item),
box_origin_(box_origin),
style_(style),
selection_(selection),
layout_object_(fragment_item_.GetLayoutObject()),
node_(layout_object_->GetNode()),
markers_(ComputeMarkersToPaint(node_, fragment_item_.IsEllipsis())),
skip_backgrounds_(is_printing ||
paint_info.phase == PaintPhase::kTextClip ||
paint_info.phase == PaintPhase::kSelectionDragImage) {}
void NGHighlightPainter::Paint(Phase phase) {
if (markers_.IsEmpty())
return;
if (skip_backgrounds_ && phase == kBackground)
return;
DCHECK(fragment_item_.GetNode());
const auto& text_node = To<Text>(*fragment_item_.GetNode());
const StringView text = cursor_.CurrentText();
for (const DocumentMarker* marker : markers_) {
const unsigned marker_start_offset =
GetTextContentOffset(text_node, marker->StartOffset());
const unsigned marker_end_offset =
GetTextContentOffset(text_node, marker->EndOffset());
const unsigned paint_start_offset =
ClampOffset(marker_start_offset, fragment_item_);
const unsigned paint_end_offset =
ClampOffset(marker_end_offset, fragment_item_);
if (paint_start_offset == paint_end_offset)
continue;
switch (marker->GetType()) {
case DocumentMarker::kSpelling:
case DocumentMarker::kGrammar: {
if (fragment_item_.GetNode()->GetDocument().Printing())
break;
if (phase == kBackground)
continue;
DocumentMarkerPainter::PaintDocumentMarker(
paint_info_, box_origin_, style_, marker->GetType(),
MarkerRectForForeground(fragment_item_, text, paint_start_offset,
paint_end_offset));
} break;
case DocumentMarker::kTextFragment:
case DocumentMarker::kTextMatch: {
const Document& document = node_->GetDocument();
if (marker->GetType() == DocumentMarker::kTextMatch &&
!document.GetFrame()->GetEditor().MarkedTextMatchesAreHighlighted())
break;
const auto& text_marker = To<TextMarkerBase>(*marker);
if (phase == kBackground) {
Color color;
if (marker->GetType() == DocumentMarker::kTextMatch) {
color = LayoutTheme::GetTheme().PlatformTextSearchHighlightColor(
text_marker.IsActiveMatch(), document.InForcedColorsMode(),
style_.UsedColorScheme());
} else {
color = HighlightPaintingUtils::HighlightBackgroundColor(
document, style_, node_, kPseudoIdTargetText);
}
PaintRect(paint_info_.context, PhysicalOffset(box_origin_),
fragment_item_.LocalRect(text, paint_start_offset,
paint_end_offset),
color);
break;
}
const TextPaintStyle text_style =
DocumentMarkerPainter::ComputeTextPaintStyleFrom(
document, node_, style_, text_marker, paint_info_);
if (text_style.current_color == Color::kTransparent)
break;
text_painter_.Paint(paint_start_offset, paint_end_offset,
paint_end_offset - paint_start_offset, text_style,
kInvalidDOMNodeId);
} break;
case DocumentMarker::kComposition:
case DocumentMarker::kActiveSuggestion:
case DocumentMarker::kSuggestion: {
const auto& styleable_marker = To<StyleableMarker>(*marker);
if (phase == kBackground) {
PaintRect(paint_info_.context, PhysicalOffset(box_origin_),
fragment_item_.LocalRect(text, paint_start_offset,
paint_end_offset),
styleable_marker.BackgroundColor());
break;
}
if (DocumentMarkerPainter::ShouldPaintMarkerUnderline(
styleable_marker)) {
const SimpleFontData* font_data = style_.GetFont().PrimaryFont();
DocumentMarkerPainter::PaintStyleableMarkerUnderline(
paint_info_.context, box_origin_, styleable_marker, style_,
FloatRect(MarkerRectForForeground(
fragment_item_, text, paint_start_offset, paint_end_offset)),
LayoutUnit(font_data->GetFontMetrics().Height()),
fragment_item_.GetNode()->GetDocument().InDarkMode());
}
} break;
default:
NOTREACHED();
break;
}
}
}
} // namespace blink