Skip to content

Commit

Permalink
NGHighlightPainter: generalize background ::selection painting logic
Browse files Browse the repository at this point in the history
This changes is mostly a refactor, which:
- generalizes the ::selection behavior in Paint­Selection­Background and
move it to Paint­Highlight­Background, so all highlight pseudos now correctly paint their backgrounds pixel-snapped in physical space
regardless of ‘writing-mode’.
- merge Selection­Background­Color into Highlight­Background­Color, without
generalizing any of the ::selection behavior to other highlights.
- merge the ::selection background step of Paint­Highlight­Overlays into
the loop that handles all other highlights.

There is a follow up bug (crbug.com/1480139) to calculate the custom
highlight background rect the same way we calculate the ::select
background rect, which accounts for line heights and line breaks.

Bug: 1434114
Change-Id: I51a5d030cedad33d70e306b5a1c940823b59acc7
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4803362
Commit-Queue: Stephen Chenney <schenney@chromium.org>
Reviewed-by: Stephen Chenney <schenney@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1209405}
  • Loading branch information
spectranaut authored and Chromium LUCI CQ committed Oct 13, 2023
1 parent 3785fcc commit be0da47
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 109 deletions.
33 changes: 26 additions & 7 deletions third_party/blink/renderer/core/highlight/highlight_style_utils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,10 @@ bool UseDefaultHighlightColors(const ComputedStyle* pseudo_style,
} // anonymous namespace

// Returns the used value of the given <color>-valued |property|, taking into
// account forced colors, default highlight colors, and ‘currentColor’ fallback.
// account forced colors, default highlight colors, and ‘currentColor’ fallback
// by means of the previous_layer_color parameter.
// If the current layer's color already accounts for the currentColor fallback,
// then the current layer's color can be supplied for the previous_layer_color.
Color HighlightStyleUtils::ResolveColor(
const Document& document,
const ComputedStyle& originating_style,
Expand Down Expand Up @@ -350,7 +353,7 @@ Color HighlightStyleUtils::HighlightBackgroundColor(
const Document& document,
const ComputedStyle& style,
Node* node,
absl::optional<Color> previous_layer_color,
absl::optional<Color> current_layer_color,
PseudoId pseudo,
const AtomicString& pseudo_argument) {
if (pseudo == kPseudoIdSelection) {
Expand All @@ -363,11 +366,27 @@ Color HighlightStyleUtils::HighlightBackgroundColor(
HighlightPseudoStyle(node, style, pseudo, pseudo_argument);
Color result =
ResolveColor(document, style, pseudo_style, pseudo,
GetCSSPropertyBackgroundColor(), previous_layer_color);
if (pseudo == kPseudoIdSelection && NodeIsReplaced(node)) {
// Avoid that ::selection full obscures selected replaced elements like
// images.
return result.BlendWithWhite();
GetCSSPropertyBackgroundColor(), current_layer_color);

if (pseudo == kPseudoIdSelection) {
if (NodeIsReplaced(node)) {
// Avoid that ::selection full obscures selected replaced elements like
// images.
return result.BlendWithWhite();
}
if (result.IsFullyTransparent()) {
return Color::kTransparent;
}
// If the text color ends up being the same as the selection background,
// invert the selection background.
if (current_layer_color && *current_layer_color == result) {
if (node) {
UseCounter::Count(node->GetDocument(),
WebFeature::kSelectionBackgroundColorInversion);
}
return Color(0xff - result.Red(), 0xff - result.Green(),
0xff - result.Blue());
}
}
return result;
}
Expand Down
225 changes: 123 additions & 102 deletions third_party/blink/renderer/core/paint/ng/ng_highlight_painter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -193,28 +193,6 @@ void PaintRect(GraphicsContext& context,
auto_dark_mode);
}

Color SelectionBackgroundColor(const Document& document,
const ComputedStyle& style,
Node* node,
Color text_color) {
const Color color = HighlightStyleUtils::HighlightBackgroundColor(
document, style, node, absl::nullopt, kPseudoIdSelection);
if (color.IsFullyTransparent()) {
return Color();
}

// If the text color ends up being the same as the selection background,
// invert the selection background.
if (text_color == color) {
if (node) {
UseCounter::Count(node->GetDocument(),
WebFeature::kSelectionBackgroundColorInversion);
}
return Color(0xff - color.Red(), 0xff - color.Green(), 0xff - color.Blue());
}
return color;
}

const HighlightRegistry* GetHighlightRegistry(const Node* node) {
if (!node)
return nullptr;
Expand All @@ -230,33 +208,6 @@ const LayoutSelectionStatus* GetSelectionStatus(
return &selection->Status();
}

const DocumentMarkerVector* SelectMarkers(const HighlightLayer& layer,
const DocumentMarkerVector& custom,
const DocumentMarkerVector& grammar,
const DocumentMarkerVector& spelling,
const DocumentMarkerVector& target) {
switch (layer.type) {
case HighlightLayerType::kOriginating:
NOTREACHED();
break;
case HighlightLayerType::kCustom:
return &custom;
case HighlightLayerType::kGrammar:
return &grammar;
case HighlightLayerType::kSpelling:
return &spelling;
case HighlightLayerType::kTargetText:
return &target;
case HighlightLayerType::kSelection:
NOTREACHED();
break;
default:
NOTREACHED();
}

return nullptr;
}

// Returns true if the styles for the given spelling or grammar pseudo require
// the full overlay painting algorithm.
bool HasNonTrivialSpellingGrammarStyles(const NGFragmentItem& fragment_item,
Expand Down Expand Up @@ -433,25 +384,11 @@ void NGHighlightPainter::SelectionPaintState::PaintSelectionBackground(
const Document& document,
const ComputedStyle& style,
const absl::optional<AffineTransform>& rotation) {
const Color color = SelectionBackgroundColor(document, style, node,
selection_style_.current_color);

AutoDarkMode auto_dark_mode(
PaintAutoDarkMode(style, DarkModeFilter::ElementRole::kSelection));

if (!rotation) {
PaintRect(context, PhysicalSelectionRect(), color, auto_dark_mode);
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).
context.ConcatCTM(rotation->Inverse());
PaintRect(context, PhysicalSelectionRect(), color, auto_dark_mode);
context.ConcatCTM(*rotation);
const Color color = HighlightStyleUtils::HighlightBackgroundColor(
document, style, node, selection_style_.current_color,
kPseudoIdSelection);
NGHighlightPainter::PaintHighlightBackground(
context, node, document, style, color, PhysicalSelectionRect(), rotation);
}

// Paint the selected text only.
Expand Down Expand Up @@ -920,6 +857,76 @@ void NGHighlightPainter::PaintOriginatingText(const TextPaintStyle& text_style,
}
}

LayoutSelectionStatus NGHighlightPainter::GetSelectionStatusFromMarker(
const Member<DocumentMarker>& marker,
const MarkerRangeMappingContext* mapping_context) {
const auto [paint_start_offset, paint_end_offset] =
mapping_context->MapToTextContent(*marker);
return LayoutSelectionStatus{paint_start_offset, paint_end_offset,
SelectSoftLineBreak::kNotSelected};
}

Vector<LayoutSelectionStatus> NGHighlightPainter::GetHighlights(
const LayerPaintState& layer) {
Vector<LayoutSelectionStatus> result{};

switch (layer.id.type) {
case HighlightLayerType::kOriginating:
NOTREACHED();
break;
case HighlightLayerType::kCustom: {
const MarkerRangeMappingContext mapping_context(fragment_item_);
for (const auto& marker : custom_) {
// Filter custom highlight markers to one highlight at a time.
auto* custom = To<CustomHighlightMarker>(marker.Get());
if (custom->GetHighlightName() != layer.id.PseudoArgument()) {
continue;
}
result.push_back(
GetSelectionStatusFromMarker(marker, &mapping_context));
}
break;
}
case HighlightLayerType::kGrammar: {
const MarkerRangeMappingContext mapping_context(fragment_item_);
for (const auto& marker : grammar_) {
result.push_back(
GetSelectionStatusFromMarker(marker, &mapping_context));
}
break;
}
case HighlightLayerType::kSpelling: {
const MarkerRangeMappingContext mapping_context(fragment_item_);
for (const auto& marker : spelling_) {
result.push_back(
GetSelectionStatusFromMarker(marker, &mapping_context));
}
break;
}
case HighlightLayerType::kTargetText: {
const MarkerRangeMappingContext mapping_context(fragment_item_);
for (const auto& marker : target_) {
result.push_back(
GetSelectionStatusFromMarker(marker, &mapping_context));
}
break;
}
case HighlightLayerType::kSelection:
result.push_back(*GetSelectionStatus(selection_));
break;
}
return result;
}

const PhysicalRect NGHighlightPainter::ComputeBackgroundRect(
StringView text,
unsigned start_offset,
unsigned end_offset) {
const PhysicalRect& rect =
fragment_item_.LocalRect(text, start_offset, end_offset);
return PhysicalRect(rect.offset + PhysicalOffset(box_origin_), rect.size);
}

void NGHighlightPainter::PaintHighlightOverlays(
const TextPaintStyle& originating_text_style,
DOMNodeId node_id,
Expand All @@ -937,61 +944,49 @@ void NGHighlightPainter::PaintHighlightOverlays(
// For each overlay, paint its backgrounds and shadows over every highlighted
// range in full.
for (const LayerPaintState& layer : layers_) {
if (layer.id.type == HighlightLayerType::kOriginating ||
layer.id.type == HighlightLayerType::kSelection)
if (layer.id.type == HighlightLayerType::kOriginating) {
continue;
}

const DocumentMarkerVector* markers =
SelectMarkers(layer.id, custom_, grammar_, spelling_, target_);
if (layer.id.type == HighlightLayerType::kSelection &&
!paint_marker_backgrounds) {
continue;
}

const MarkerRangeMappingContext mapping_context(fragment_item_);
for (const auto& marker : *markers) {
if (layer.id.type == HighlightLayerType::kCustom) {
// Filter custom highlight markers to one highlight at a time.
auto* custom = To<CustomHighlightMarker>(marker.Get());
if (custom->GetHighlightName() != layer.id.PseudoArgument())
continue;
}
Vector<LayoutSelectionStatus> highlights = GetHighlights(layer);

const auto [paint_start_offset, paint_end_offset] =
mapping_context.MapToTextContent(*marker);
const unsigned length = paint_end_offset - paint_start_offset;
for (const auto& highlight : highlights) {
const unsigned length = highlight.end - highlight.start;
if (length == 0)
continue;

const StringView text = cursor_.CurrentText();

// TODO(crbug.com/1480139) ComputeBackgroundRect should use the same logic
// as CurrentLocalSelectionRectForText, that is, it should expand
// selection to the line height and extend for line breaks.
const PhysicalRect& rect =
layer.id.type == HighlightLayerType::kSelection
? selection_->PhysicalSelectionRect()
: ComputeBackgroundRect(text, highlight.start, highlight.end);

Color background_color = HighlightStyleUtils::HighlightBackgroundColor(
document, originating_style_, node_, layer.text_style.current_color,
layer.id.PseudoId(), layer.id.PseudoArgument());

// TODO(crbug.com/1434114) paint rects pixel-snapped in physical space,
// not writing-mode space (SelectionPaintState::PaintSelectionBackground)
PaintRect(
paint_info_.context, PhysicalOffset(box_origin_),
fragment_item_.LocalRect(text, paint_start_offset, paint_end_offset),
background_color,
PaintAutoDarkMode(originating_style_,
DarkModeFilter::ElementRole::kSelection));
PaintHighlightBackground(paint_info_.context, node_, document,
originating_style_, background_color, rect,
rotation);

if (layer.text_style.shadow) {
text_painter_.Paint(
fragment_paint_info_.Slice(paint_start_offset, paint_end_offset),
fragment_paint_info_.Slice(highlight.start, highlight.end),
layer.text_style, node_id, foreground_auto_dark_mode_,
TextPainterBase::kShadowsOnly);
}
}
}

// Paint ::selection background.
// TODO(crbug.com/1434114) generalise ::selection painting logic to support
// all highlights, then merge this branch into the loop above
if (UNLIKELY(selection_)) {
if (paint_marker_backgrounds) {
selection_->PaintSelectionBackground(paint_info_.context, node_, document,
originating_style_, rotation);
}
}

// For each overlay, paint the text proper over every highlighted range,
// except any parts for which we’re not the topmost active highlight.
for (const LayerPaintState& layer : layers_) {
Expand Down Expand Up @@ -1050,6 +1045,32 @@ unsigned NGHighlightPainter::GetTextContentOffset(const Text& text,
return ng_offset.value();
}

void NGHighlightPainter::PaintHighlightBackground(
GraphicsContext& context,
Node* node,
const Document& document,
const ComputedStyle& style,
Color color,
const PhysicalRect& rect,
const absl::optional<AffineTransform>& rotation) {
AutoDarkMode auto_dark_mode(
PaintAutoDarkMode(style, DarkModeFilter::ElementRole::kSelection));

if (!rotation) {
PaintRect(context, rect, color, auto_dark_mode);
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).
context.ConcatCTM(rotation->Inverse());
PaintRect(context, rect, color, auto_dark_mode);
context.ConcatCTM(*rotation);
}

PseudoId NGHighlightPainter::PseudoFor(DocumentMarker::MarkerType type) {
switch (type) {
case DocumentMarker::kSpelling:
Expand Down
21 changes: 21 additions & 0 deletions third_party/blink/renderer/core/paint/ng/ng_highlight_painter.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ struct NGTextFragmentPaintInfo;
struct PaintInfo;
struct PhysicalOffset;

namespace {
class MarkerRangeMappingContext;
}

// Highlight overlay painter for LayoutNG. Operates on NGFragmentItem that
// IsText(). Delegates to NGTextPainter to paint the text itself.
class CORE_EXPORT NGHighlightPainter {
Expand Down Expand Up @@ -196,6 +200,15 @@ class CORE_EXPORT NGHighlightPainter {
bool paint_marker_backgrounds,
absl::optional<AffineTransform> rotation);

static void PaintHighlightBackground(
GraphicsContext& context,
Node* node,
const Document& document,
const ComputedStyle& style,
Color color,
const PhysicalRect& rect,
const absl::optional<AffineTransform>& rotation);

// Return the text content offset for a particular fragment offset.
static unsigned GetTextContentOffset(const Text& text, unsigned offset);

Expand Down Expand Up @@ -230,6 +243,14 @@ class CORE_EXPORT NGHighlightPainter {

private:
Case ComputePaintCase() const;

const PhysicalRect ComputeBackgroundRect(StringView text,
unsigned start_offset,
unsigned end_offset);
Vector<LayoutSelectionStatus> GetHighlights(const LayerPaintState& layer);
LayoutSelectionStatus GetSelectionStatusFromMarker(
const Member<DocumentMarker>& marker,
const MarkerRangeMappingContext* mapping_context);
void FastPaintSpellingGrammarDecorations(const Text& text_node,
const StringView& text,
const DocumentMarkerVector& markers);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!doctype html>
<link rel="stylesheet" href="../../css-pseudo/support/highlights.css">
<meta charset="utf-8">
<style>
:root {
writing-mode: vertical-rl;
}

.highlighted {
background-color: yellow;
color: blue;
}
</style>
<body>
<div class="highlight_reftest"><span class="highlighted">One two </span><span>three…</span></div>

0 comments on commit be0da47

Please sign in to comment.