blob: 0e1ba78cca3274746f54b784941e34fb1946164a [file] [log] [blame]
// 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/paint/text_decoration_info.h"
#include "third_party/blink/renderer/core/paint/text_paint_style.h"
#include "third_party/blink/renderer/core/style/computed_style.h"
#include "third_party/blink/renderer/platform/geometry/length_functions.h"
#include "third_party/blink/renderer/platform/graphics/graphics_context.h"
namespace blink {
namespace {
static int kUndefinedDecorationIndex = -1;
static ResolvedUnderlinePosition ResolveUnderlinePosition(
const ComputedStyle& style,
FontBaseline baseline_type) {
// |auto| should resolve to |under| to avoid drawing through glyphs in
// scripts where it would not be appropriate (e.g., ideographs.)
// However, this has performance implications. For now, we only work with
// vertical text.
switch (baseline_type) {
case kAlphabeticBaseline:
if (style.TextUnderlinePosition() & kTextUnderlinePositionUnder)
return ResolvedUnderlinePosition::kUnder;
if (style.TextUnderlinePosition() & kTextUnderlinePositionFromFont)
return ResolvedUnderlinePosition::kNearAlphabeticBaselineFromFont;
return ResolvedUnderlinePosition::kNearAlphabeticBaselineAuto;
case kIdeographicBaseline:
// Compute language-appropriate default underline position.
// https://drafts.csswg.org/css-text-decor-3/#default-stylesheet
UScriptCode script = style.GetFontDescription().GetScript();
if (script == USCRIPT_KATAKANA_OR_HIRAGANA || script == USCRIPT_HANGUL) {
if (style.TextUnderlinePosition() & kTextUnderlinePositionLeft) {
return ResolvedUnderlinePosition::kUnder;
}
return ResolvedUnderlinePosition::kOver;
}
if (style.TextUnderlinePosition() & kTextUnderlinePositionRight) {
return ResolvedUnderlinePosition::kOver;
}
return ResolvedUnderlinePosition::kUnder;
}
NOTREACHED();
return ResolvedUnderlinePosition::kNearAlphabeticBaselineAuto;
}
static bool ShouldSetDecorationAntialias(const ComputedStyle& style) {
for (const auto& decoration : style.AppliedTextDecorations()) {
ETextDecorationStyle decoration_style = decoration.Style();
if (decoration_style == ETextDecorationStyle::kDotted ||
decoration_style == ETextDecorationStyle::kDashed)
return true;
}
return false;
}
static float ComputeDecorationThickness(
const TextDecorationThickness text_decoration_thickness,
const ComputedStyle& style,
const SimpleFontData* font_data) {
float auto_underline_thickness =
std::max(1.f, style.ComputedFontSize() / 10.f);
if (text_decoration_thickness.IsAuto())
return auto_underline_thickness;
// In principle we would not need to test for font_data if
// |text_decoration_thickness.Thickness()| is fixed, but a null font_data here
// would be a rare / error situation anyway, so practically, we can
// early out here.
if (!font_data)
return auto_underline_thickness;
if (text_decoration_thickness.IsFromFont()) {
base::Optional<float> underline_thickness_font_metric =
font_data->GetFontMetrics().UnderlineThickness().value();
if (!underline_thickness_font_metric)
return auto_underline_thickness;
return std::max(1.f, underline_thickness_font_metric.value());
}
DCHECK(!text_decoration_thickness.IsFromFont());
const Length& thickness_length = text_decoration_thickness.Thickness();
float font_size = font_data->PlatformData().size();
float text_decoration_thickness_pixels =
FloatValueForLength(thickness_length, font_size);
return std::max(1.f, text_decoration_thickness_pixels);
}
static void AdjustStepToDecorationLength(float& step,
float& control_point_distance,
float length) {
DCHECK_GT(step, 0);
if (length <= 0)
return;
unsigned step_count = static_cast<unsigned>(length / step);
// Each Bezier curve starts at the same pixel that the previous one
// ended. We need to subtract (stepCount - 1) pixels when calculating the
// length covered to account for that.
float uncovered_length = length - (step_count * step - (step_count - 1));
float adjustment = uncovered_length / step_count;
step += adjustment;
control_point_distance += adjustment;
}
static enum StrokeStyle TextDecorationStyleToStrokeStyle(
ETextDecorationStyle decoration_style) {
enum StrokeStyle stroke_style = kSolidStroke;
switch (decoration_style) {
case ETextDecorationStyle::kSolid:
stroke_style = kSolidStroke;
break;
case ETextDecorationStyle::kDouble:
stroke_style = kDoubleStroke;
break;
case ETextDecorationStyle::kDotted:
stroke_style = kDottedStroke;
break;
case ETextDecorationStyle::kDashed:
stroke_style = kDashedStroke;
break;
case ETextDecorationStyle::kWavy:
stroke_style = kWavyStroke;
break;
}
return stroke_style;
}
static int TextDecorationToLineDataIndex(TextDecoration line) {
switch (line) {
case TextDecoration::kUnderline:
return 0;
case TextDecoration::kOverline:
return 1;
case TextDecoration::kLineThrough:
return 2;
default:
NOTREACHED();
return 0;
}
}
} // anonymous namespace
TextDecorationInfo::TextDecorationInfo(
const PhysicalOffset& box_origin,
PhysicalOffset local_origin,
LayoutUnit width,
FontBaseline baseline_type,
const ComputedStyle& style,
const base::Optional<AppliedTextDecoration> selection_text_decoration,
const ComputedStyle* decorating_box_style)
: style_(style),
selection_text_decoration_(selection_text_decoration),
baseline_type_(baseline_type),
width_(width),
font_data_(style_.GetFont().PrimaryFont()),
baseline_(font_data_ ? font_data_->GetFontMetrics().FloatAscent() : 0),
underline_position_(ResolveUnderlinePosition(style_, baseline_type_)),
local_origin_(FloatPoint(local_origin)),
antialias_(ShouldSetDecorationAntialias(style)),
decoration_index_(kUndefinedDecorationIndex) {
DCHECK(font_data_);
for (const AppliedTextDecoration& decoration :
style_.AppliedTextDecorations()) {
applied_decorations_thickness_.push_back(ComputeUnderlineThickness(
decoration.Thickness(), decorating_box_style));
}
DCHECK_EQ(style_.AppliedTextDecorations().size(),
applied_decorations_thickness_.size());
}
void TextDecorationInfo::SetDecorationIndex(int decoration_index) {
DCHECK_LT(decoration_index,
static_cast<int>(applied_decorations_thickness_.size()));
decoration_index_ = decoration_index;
}
void TextDecorationInfo::SetPerLineData(TextDecoration line,
float line_offset,
float double_offset,
int wavy_offset_factor) {
int index = TextDecorationToLineDataIndex(line);
line_data_[index].line_offset = line_offset;
line_data_[index].double_offset = double_offset;
line_data_[index].wavy_offset_factor = wavy_offset_factor;
line_data_[index].stroke_path.reset();
}
ETextDecorationStyle TextDecorationInfo::DecorationStyle() const {
return style_.AppliedTextDecorations()[decoration_index_].Style();
}
Color TextDecorationInfo::LineColor() const {
// Find the matched normal and selection |AppliedTextDecoration|
// and use the text-decoration-color from selection when it is.
if (selection_text_decoration_ &&
style_.AppliedTextDecorations()[decoration_index_].Lines() ==
selection_text_decoration_.value().Lines()) {
return selection_text_decoration_.value().GetColor();
}
return style_.AppliedTextDecorations()[decoration_index_].GetColor();
}
FloatPoint TextDecorationInfo::StartPoint(TextDecoration line) const {
return local_origin_ +
FloatPoint(
0, line_data_[TextDecorationToLineDataIndex(line)].line_offset);
}
float TextDecorationInfo::DoubleOffset(TextDecoration line) const {
return line_data_[TextDecorationToLineDataIndex(line)].double_offset;
}
enum StrokeStyle TextDecorationInfo::StrokeStyle() const {
return TextDecorationStyleToStrokeStyle(DecorationStyle());
}
float TextDecorationInfo::ComputeUnderlineThickness(
const TextDecorationThickness& applied_decoration_thickness,
const ComputedStyle* decorating_box_style) {
float thickness = 0;
if ((underline_position_ ==
ResolvedUnderlinePosition::kNearAlphabeticBaselineAuto) ||
underline_position_ ==
ResolvedUnderlinePosition::kNearAlphabeticBaselineFromFont) {
thickness = ComputeDecorationThickness(applied_decoration_thickness, style_,
style_.GetFont().PrimaryFont());
} else {
// Compute decorating box. Position and thickness are computed from the
// decorating box.
// Only for non-Roman for now for the performance implications.
// https:// drafts.csswg.org/css-text-decor-3/#decorating-box
if (decorating_box_style) {
thickness = ComputeDecorationThickness(
applied_decoration_thickness, *decorating_box_style,
decorating_box_style->GetFont().PrimaryFont());
} else {
thickness = ComputeDecorationThickness(
applied_decoration_thickness, style_, style_.GetFont().PrimaryFont());
}
}
return thickness;
}
FloatRect TextDecorationInfo::BoundsForLine(TextDecoration line) const {
FloatPoint start_point = StartPoint(line);
switch (DecorationStyle()) {
case ETextDecorationStyle::kDotted:
case ETextDecorationStyle::kDashed:
return BoundsForDottedOrDashed(line);
case ETextDecorationStyle::kWavy:
return BoundsForWavy(line);
case ETextDecorationStyle::kDouble:
if (DoubleOffset(line) > 0) {
return FloatRect(start_point.X(), start_point.Y(), width_,
DoubleOffset(line) + ResolvedThickness());
}
return FloatRect(start_point.X(), start_point.Y() + DoubleOffset(line),
width_, -DoubleOffset(line) + ResolvedThickness());
case ETextDecorationStyle::kSolid:
return FloatRect(start_point.X(), start_point.Y(), width_,
ResolvedThickness());
default:
break;
}
NOTREACHED();
return FloatRect();
}
FloatRect TextDecorationInfo::BoundsForDottedOrDashed(
TextDecoration line) const {
int line_data_index = TextDecorationToLineDataIndex(line);
if (!line_data_[line_data_index].stroke_path) {
// These coordinate transforms need to match what's happening in
// GraphicsContext's drawLineForText and drawLine.
FloatPoint start_point = StartPoint(line);
line_data_[TextDecorationToLineDataIndex(line)].stroke_path =
GraphicsContext::GetPathForTextLine(
start_point, width_, ResolvedThickness(),
TextDecorationStyleToStrokeStyle(DecorationStyle()));
}
StrokeData stroke_data;
stroke_data.SetThickness(roundf(ResolvedThickness()));
stroke_data.SetStyle(TextDecorationStyleToStrokeStyle(DecorationStyle()));
return line_data_[line_data_index].stroke_path.value().StrokeBoundingRect(
stroke_data);
}
FloatRect TextDecorationInfo::BoundsForWavy(TextDecoration line) const {
StrokeData stroke_data;
stroke_data.SetThickness(ResolvedThickness());
return PrepareWavyStrokePath(line)->StrokeBoundingRect(stroke_data);
}
/*
* Prepare a path for a cubic Bezier curve and repeat the same pattern long the
* the decoration's axis. The start point (p1), controlPoint1, controlPoint2
* and end point (p2) of the Bezier curve form a diamond shape:
*
* step
* |-----------|
*
* controlPoint1
* +
*
*
* . .
* . .
* . .
* (x1, y1) p1 + . + p2 (x2, y2) - <--- Decoration's axis
* . . |
* . . |
* . . | controlPointDistance
* |
* |
* + -
* controlPoint2
*
* |-----------|
* step
*/
base::Optional<Path> TextDecorationInfo::PrepareWavyStrokePath(
TextDecoration line) const {
int line_data_index = TextDecorationToLineDataIndex(line);
if (line_data_[line_data_index].stroke_path)
return line_data_[line_data_index].stroke_path;
FloatPoint start_point = StartPoint(line);
float wave_offset =
DoubleOffset(line) *
line_data_[TextDecorationToLineDataIndex(line)].wavy_offset_factor;
FloatPoint p1(start_point + FloatPoint(0, wave_offset));
FloatPoint p2(start_point + FloatPoint(width_, wave_offset));
GraphicsContext::AdjustLineToPixelBoundaries(p1, p2, ResolvedThickness());
Path& path = line_data_[line_data_index].stroke_path.emplace();
path.MoveTo(p1);
// Distance between decoration's axis and Bezier curve's control points.
// The height of the curve is based on this distance. Use a minimum of 6
// pixels distance since
// the actual curve passes approximately at half of that distance, that is 3
// pixels.
// The minimum height of the curve is also approximately 3 pixels. Increases
// the curve's height
// as strockThickness increases to make the curve looks better.
float control_point_distance = 3 * std::max<float>(2, ResolvedThickness());
// Increment used to form the diamond shape between start point (p1),
// control points and end point (p2) along the axis of the decoration. Makes
// the curve wider as strockThickness increases to make the curve looks
// better.
float step = 2 * std::max<float>(2, ResolvedThickness());
bool is_vertical_line = (p1.X() == p2.X());
if (is_vertical_line) {
DCHECK(p1.X() == p2.X());
float x_axis = p1.X();
float y1;
float y2;
if (p1.Y() < p2.Y()) {
y1 = p1.Y();
y2 = p2.Y();
} else {
y1 = p2.Y();
y2 = p1.Y();
}
AdjustStepToDecorationLength(step, control_point_distance, y2 - y1);
FloatPoint control_point1(x_axis + control_point_distance, 0);
FloatPoint control_point2(x_axis - control_point_distance, 0);
for (float y = y1; y + 2 * step <= y2;) {
control_point1.SetY(y + step);
control_point2.SetY(y + step);
y += 2 * step;
path.AddBezierCurveTo(control_point1, control_point2,
FloatPoint(x_axis, y));
}
} else {
DCHECK(p1.Y() == p2.Y());
float y_axis = p1.Y();
float x1;
float x2;
if (p1.X() < p2.X()) {
x1 = p1.X();
x2 = p2.X();
} else {
x1 = p2.X();
x2 = p1.X();
}
AdjustStepToDecorationLength(step, control_point_distance, x2 - x1);
FloatPoint control_point1(0, y_axis + control_point_distance);
FloatPoint control_point2(0, y_axis - control_point_distance);
for (float x = x1; x + 2 * step <= x2;) {
control_point1.SetX(x + step);
control_point2.SetX(x + step);
x += 2 * step;
path.AddBezierCurveTo(control_point1, control_point2,
FloatPoint(x, y_axis));
}
}
return line_data_[line_data_index].stroke_path;
}
} // namespace blink