| // 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 "third_party/blink/renderer/core/paint/text_painter_base.h" |
| |
| #include "third_party/blink/renderer/core/dom/document.h" |
| #include "third_party/blink/renderer/core/layout/text_decoration_offset_base.h" |
| #include "third_party/blink/renderer/core/paint/applied_decoration_painter.h" |
| #include "third_party/blink/renderer/core/paint/box_painter_base.h" |
| #include "third_party/blink/renderer/core/paint/paint_info.h" |
| #include "third_party/blink/renderer/core/paint/selection_painting_utils.h" |
| #include "third_party/blink/renderer/core/paint/text_decoration_info.h" |
| #include "third_party/blink/renderer/core/style/computed_style.h" |
| #include "third_party/blink/renderer/core/style/shadow_list.h" |
| #include "third_party/blink/renderer/platform/fonts/font.h" |
| #include "third_party/blink/renderer/platform/geometry/length_functions.h" |
| #include "third_party/blink/renderer/platform/graphics/graphics_context.h" |
| #include "third_party/blink/renderer/platform/graphics/graphics_context_state_saver.h" |
| #include "third_party/blink/renderer/platform/wtf/assertions.h" |
| #include "third_party/blink/renderer/platform/wtf/text/character_names.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| // We usually use the text decoration thickness to determine how far |
| // ink-skipped text decorations should be away from the glyph |
| // contours. Cap this at 5 CSS px in each direction when thickness |
| // growths larger than that. A value of 13 closely matches FireFox' |
| // implementation. |
| constexpr float kDecorationClipMaxDilation = 13; |
| |
| float DoubleOffsetFromThickness(float thickness_pixels) { |
| return thickness_pixels + 1.0f; |
| } |
| |
| } // anonymous namespace |
| |
| TextPainterBase::TextPainterBase(GraphicsContext& context, |
| const Font& font, |
| const PhysicalOffset& text_origin, |
| const PhysicalRect& text_frame_rect, |
| bool horizontal) |
| : graphics_context_(context), |
| font_(font), |
| text_origin_(text_origin), |
| text_frame_rect_(text_frame_rect), |
| horizontal_(horizontal), |
| has_combined_text_(false), |
| emphasis_mark_offset_(0), |
| ellipsis_offset_(0) {} |
| |
| TextPainterBase::~TextPainterBase() = default; |
| |
| void TextPainterBase::SetEmphasisMark(const AtomicString& emphasis_mark, |
| TextEmphasisPosition position) { |
| emphasis_mark_ = emphasis_mark; |
| const SimpleFontData* font_data = font_.PrimaryFont(); |
| DCHECK(font_data); |
| |
| if (!font_data || emphasis_mark.IsNull()) { |
| emphasis_mark_offset_ = 0; |
| } else if ((horizontal_ && (position == TextEmphasisPosition::kOverRight || |
| position == TextEmphasisPosition::kOverLeft)) || |
| (!horizontal_ && |
| (position == TextEmphasisPosition::kOverRight || |
| position == TextEmphasisPosition::kUnderRight))) { |
| emphasis_mark_offset_ = -font_data->GetFontMetrics().Ascent() - |
| font_.EmphasisMarkDescent(emphasis_mark); |
| } else { |
| DCHECK(position == TextEmphasisPosition::kUnderRight || |
| position == TextEmphasisPosition::kUnderLeft || |
| position == TextEmphasisPosition::kOverLeft); |
| emphasis_mark_offset_ = font_data->GetFontMetrics().Descent() + |
| font_.EmphasisMarkAscent(emphasis_mark); |
| } |
| } |
| |
| // static |
| void TextPainterBase::UpdateGraphicsContext( |
| GraphicsContext& context, |
| const TextPaintStyle& text_style, |
| bool horizontal, |
| GraphicsContextStateSaver& state_saver) { |
| TextDrawingModeFlags mode = context.TextDrawingMode(); |
| if (text_style.stroke_width > 0) { |
| TextDrawingModeFlags new_mode = mode | kTextModeStroke; |
| if (mode != new_mode) { |
| state_saver.SaveIfNeeded(); |
| context.SetTextDrawingMode(new_mode); |
| mode = new_mode; |
| } |
| } |
| |
| if (mode & kTextModeFill && text_style.fill_color != context.FillColor()) |
| context.SetFillColor(text_style.fill_color); |
| |
| if (mode & kTextModeStroke) { |
| if (text_style.stroke_color != context.StrokeColor()) |
| context.SetStrokeColor(text_style.stroke_color); |
| if (text_style.stroke_width != context.StrokeThickness()) |
| context.SetStrokeThickness(text_style.stroke_width); |
| } |
| |
| if (text_style.shadow) { |
| state_saver.SaveIfNeeded(); |
| context.SetDrawLooper(text_style.shadow->CreateDrawLooper( |
| DrawLooperBuilder::kShadowIgnoresAlpha, text_style.current_color, |
| text_style.color_scheme, horizontal)); |
| } |
| } |
| |
| Color TextPainterBase::TextColorForWhiteBackground(Color text_color) { |
| int distance_from_white = DifferenceSquared(text_color, Color::kWhite); |
| // semi-arbitrarily chose 65025 (255^2) value here after a few tests; |
| return distance_from_white > 65025 ? text_color : text_color.Dark(); |
| } |
| |
| // static |
| TextPaintStyle TextPainterBase::TextPaintingStyle(const Document& document, |
| const ComputedStyle& style, |
| const PaintInfo& paint_info) { |
| TextPaintStyle text_style; |
| text_style.stroke_width = style.TextStrokeWidth(); |
| text_style.color_scheme = style.UsedColorScheme(); |
| bool is_printing = paint_info.IsPrinting(); |
| |
| if (paint_info.phase == PaintPhase::kTextClip) { |
| // When we use the text as a clip, we only care about the alpha, thus we |
| // make all the colors black. |
| text_style.current_color = Color::kBlack; |
| text_style.fill_color = Color::kBlack; |
| text_style.stroke_color = Color::kBlack; |
| text_style.emphasis_mark_color = Color::kBlack; |
| text_style.shadow = nullptr; |
| } else { |
| text_style.current_color = |
| style.VisitedDependentColor(GetCSSPropertyColor()); |
| text_style.fill_color = |
| style.VisitedDependentColor(GetCSSPropertyWebkitTextFillColor()); |
| text_style.stroke_color = |
| style.VisitedDependentColor(GetCSSPropertyWebkitTextStrokeColor()); |
| text_style.emphasis_mark_color = |
| style.VisitedDependentColor(GetCSSPropertyWebkitTextEmphasisColor()); |
| text_style.shadow = style.TextShadow(); |
| |
| // Adjust text color when printing with a white background. |
| DCHECK_EQ(document.Printing(), is_printing); |
| bool force_background_to_white = |
| BoxPainterBase::ShouldForceWhiteBackgroundForPrintEconomy(document, |
| style); |
| if (force_background_to_white) { |
| text_style.fill_color = |
| TextColorForWhiteBackground(text_style.fill_color); |
| text_style.stroke_color = |
| TextColorForWhiteBackground(text_style.stroke_color); |
| text_style.emphasis_mark_color = |
| TextColorForWhiteBackground(text_style.emphasis_mark_color); |
| } |
| } |
| |
| return text_style; |
| } |
| |
| TextPaintStyle TextPainterBase::SelectionPaintingStyle( |
| const Document& document, |
| const ComputedStyle& style, |
| Node* node, |
| bool have_selection, |
| const PaintInfo& paint_info, |
| const TextPaintStyle& text_style) { |
| return SelectionPaintingUtils::SelectionPaintingStyle( |
| document, style, node, have_selection, text_style, paint_info); |
| } |
| |
| void TextPainterBase::DecorationsStripeIntercepts( |
| float upper, |
| float stripe_width, |
| float dilation, |
| const Vector<Font::TextIntercept>& text_intercepts) { |
| for (auto intercept : text_intercepts) { |
| FloatPoint clip_origin(text_origin_); |
| FloatRect clip_rect( |
| clip_origin + FloatPoint(intercept.begin_, upper), |
| FloatSize(intercept.end_ - intercept.begin_, stripe_width)); |
| clip_rect.InflateX(dilation); |
| // We need to ensure the clip rectangle is covering the full underline |
| // extent. For horizontal drawing, using enclosingIntRect would be |
| // sufficient, since we can clamp to full device pixels that way. However, |
| // for vertical drawing, we have a transformation applied, which breaks the |
| // integers-equal-device pixels assumption, so vertically inflating by 1 |
| // pixel makes sure we're always covering. This should only be done on the |
| // clipping rectangle, not when computing the glyph intersects. |
| clip_rect.InflateY(1.0); |
| |
| if (!clip_rect.IsFinite()) |
| continue; |
| graphics_context_.ClipOut(clip_rect); |
| } |
| } |
| |
| void TextPainterBase::PaintDecorationsExceptLineThrough( |
| const TextDecorationOffsetBase& decoration_offset, |
| TextDecorationInfo& decoration_info, |
| const PaintInfo& paint_info, |
| const Vector<AppliedTextDecoration>& decorations, |
| const TextPaintStyle& text_style, |
| bool* has_line_through_decoration) { |
| GraphicsContext& context = paint_info.context; |
| GraphicsContextStateSaver state_saver(context); |
| UpdateGraphicsContext(context, text_style, horizontal_, state_saver); |
| |
| if (has_combined_text_) |
| context.ConcatCTM(Rotation(text_frame_rect_, kClockwise)); |
| |
| // text-underline-position may flip underline and overline. |
| ResolvedUnderlinePosition underline_position = |
| decoration_info.UnderlinePosition(); |
| bool flip_underline_and_overline = false; |
| if (underline_position == ResolvedUnderlinePosition::kOver) { |
| flip_underline_and_overline = true; |
| underline_position = ResolvedUnderlinePosition::kUnder; |
| } |
| |
| for (size_t applied_decoration_index = 0; |
| applied_decoration_index < decorations.size(); |
| ++applied_decoration_index) { |
| const AppliedTextDecoration& decoration = |
| decorations[applied_decoration_index]; |
| 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); |
| |
| decoration_info.SetDecorationIndex(applied_decoration_index); |
| |
| float resolved_thickness = decoration_info.ResolvedThickness(); |
| context.SetStrokeThickness(resolved_thickness); |
| |
| if (has_underline && decoration_info.FontData()) { |
| const int paint_underline_offset = |
| decoration_offset.ComputeUnderlineOffset( |
| underline_position, decoration_info.Style().ComputedFontSize(), |
| decoration_info.FontData()->GetFontMetrics(), |
| decoration.UnderlineOffset(), resolved_thickness); |
| decoration_info.SetPerLineData( |
| TextDecoration::kUnderline, paint_underline_offset, |
| DoubleOffsetFromThickness(resolved_thickness), 1); |
| PaintDecorationUnderOrOverLine(context, decoration_info, |
| TextDecoration::kUnderline); |
| } |
| |
| if (has_overline && decoration_info.FontData()) { |
| FontVerticalPositionType position = |
| flip_underline_and_overline ? FontVerticalPositionType::TopOfEmHeight |
| : FontVerticalPositionType::TextTop; |
| const int paint_overline_offset = |
| decoration_offset.ComputeUnderlineOffsetForUnder( |
| decoration_info.Style().TextUnderlineOffset(), |
| decoration_info.Style().ComputedFontSize(), resolved_thickness, |
| position); |
| decoration_info.SetPerLineData( |
| TextDecoration::kOverline, paint_overline_offset, |
| -DoubleOffsetFromThickness(resolved_thickness), 1); |
| PaintDecorationUnderOrOverLine(context, decoration_info, |
| TextDecoration::kOverline); |
| } |
| |
| // 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); |
| } |
| |
| // Restore rotation as needed. |
| if (has_combined_text_) |
| context.ConcatCTM(Rotation(text_frame_rect_, kCounterclockwise)); |
| } |
| |
| void TextPainterBase::PaintDecorationsOnlyLineThrough( |
| TextDecorationInfo& decoration_info, |
| const PaintInfo& paint_info, |
| const Vector<AppliedTextDecoration>& decorations, |
| const TextPaintStyle& text_style) { |
| GraphicsContext& context = paint_info.context; |
| GraphicsContextStateSaver state_saver(context); |
| UpdateGraphicsContext(context, text_style, horizontal_, state_saver); |
| |
| if (has_combined_text_) |
| context.ConcatCTM(Rotation(text_frame_rect_, kClockwise)); |
| |
| for (size_t applied_decoration_index = 0; |
| applied_decoration_index < decorations.size(); |
| ++applied_decoration_index) { |
| const AppliedTextDecoration& decoration = |
| decorations[applied_decoration_index]; |
| TextDecoration lines = decoration.Lines(); |
| if (EnumHasFlags(lines, TextDecoration::kLineThrough)) { |
| decoration_info.SetDecorationIndex(applied_decoration_index); |
| |
| float resolved_thickness = decoration_info.ResolvedThickness(); |
| context.SetStrokeThickness(resolved_thickness); |
| |
| // For increased line thickness, the line-through decoration needs to grow |
| // in both directions from its origin, subtract half the thickness to keep |
| // it centered at the same origin. |
| const float line_through_offset = |
| 2 * decoration_info.Baseline() / 3 - resolved_thickness / 2; |
| // Floor double_offset in order to avoid double-line gap to appear |
| // of different size depending on position where the double line |
| // is drawn because of rounding downstream in |
| // GraphicsContext::DrawLineForText. |
| decoration_info.SetPerLineData( |
| TextDecoration::kLineThrough, line_through_offset, |
| floorf(DoubleOffsetFromThickness(resolved_thickness)), 0); |
| AppliedDecorationPainter decoration_painter(context, decoration_info, |
| TextDecoration::kLineThrough); |
| // No skip: ink for line-through, |
| // compare https://github.com/w3c/csswg-drafts/issues/711 |
| decoration_painter.Paint(); |
| } |
| } |
| |
| // Restore rotation as needed. |
| if (has_combined_text_) |
| context.ConcatCTM(Rotation(text_frame_rect_, kCounterclockwise)); |
| } |
| |
| void TextPainterBase::PaintDecorationUnderOrOverLine( |
| GraphicsContext& context, |
| TextDecorationInfo& decoration_info, |
| TextDecoration line) { |
| AppliedDecorationPainter decoration_painter(context, decoration_info, line); |
| if (decoration_info.Style().TextDecorationSkipInk() == |
| ETextDecorationSkipInk::kAuto) { |
| FloatRect decoration_bounds = decoration_info.BoundsForLine(line); |
| ClipDecorationsStripe( |
| decoration_info.InkSkipClipUpper(decoration_bounds.Y()), |
| decoration_bounds.Height(), |
| std::min(decoration_info.ResolvedThickness(), |
| kDecorationClipMaxDilation)); |
| } |
| decoration_painter.Paint(); |
| } |
| |
| } // namespace blink |