summaryrefslogtreecommitdiff
path: root/Source/WebCore/rendering/TextDecorationPainter.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'Source/WebCore/rendering/TextDecorationPainter.cpp')
-rw-r--r--Source/WebCore/rendering/TextDecorationPainter.cpp428
1 files changed, 428 insertions, 0 deletions
diff --git a/Source/WebCore/rendering/TextDecorationPainter.cpp b/Source/WebCore/rendering/TextDecorationPainter.cpp
new file mode 100644
index 000000000..d74949a9e
--- /dev/null
+++ b/Source/WebCore/rendering/TextDecorationPainter.cpp
@@ -0,0 +1,428 @@
+/*
+ * (C) 1999 Lars Knoll (knoll@kde.org)
+ * (C) 2000 Dirk Mueller (mueller@kde.org)
+ * Copyright (C) 2004-2015 Apple Inc. All rights reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public License
+ * along with this library; see the file COPYING.LIB. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ */
+
+#include "config.h"
+#include "TextDecorationPainter.h"
+
+#include "FontCascade.h"
+#include "GraphicsContext.h"
+#include "HTMLAnchorElement.h"
+#include "HTMLFontElement.h"
+#include "InlineTextBoxStyle.h"
+#include "RenderBlock.h"
+#include "RenderStyle.h"
+#include "RenderText.h"
+#include "ShadowData.h"
+#include "TextRun.h"
+
+namespace WebCore {
+
+static void adjustStepToDecorationLength(float& step, float& controlPointDistance, float length)
+{
+ ASSERT(step > 0);
+
+ if (length <= 0)
+ return;
+
+ unsigned stepCount = 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 uncoveredLength = length - (stepCount * step - (stepCount - 1));
+ float adjustment = uncoveredLength / stepCount;
+ step += adjustment;
+ controlPointDistance += adjustment;
+}
+
+/*
+ * Draw one 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
+ */
+static void strokeWavyTextDecoration(GraphicsContext& context, const FloatPoint& start, const FloatPoint& end, float strokeThickness)
+{
+ FloatPoint p1 = start;
+ FloatPoint p2 = end;
+ context.adjustLineToPixelBoundaries(p1, p2, strokeThickness, context.strokeStyle());
+
+ Path path;
+ path.moveTo(p1);
+
+ float controlPointDistance;
+ float step;
+ getWavyStrokeParameters(strokeThickness, controlPointDistance, step);
+
+ bool isVerticalLine = (p1.x() == p2.x());
+
+ if (isVerticalLine) {
+ ASSERT(p1.x() == p2.x());
+
+ float xAxis = 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, controlPointDistance, y2 - y1);
+ FloatPoint controlPoint1(xAxis + controlPointDistance, 0);
+ FloatPoint controlPoint2(xAxis - controlPointDistance, 0);
+
+ for (float y = y1; y + 2 * step <= y2;) {
+ controlPoint1.setY(y + step);
+ controlPoint2.setY(y + step);
+ y += 2 * step;
+ path.addBezierCurveTo(controlPoint1, controlPoint2, FloatPoint(xAxis, y));
+ }
+ } else {
+ ASSERT(p1.y() == p2.y());
+
+ float yAxis = 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, controlPointDistance, x2 - x1);
+ FloatPoint controlPoint1(0, yAxis + controlPointDistance);
+ FloatPoint controlPoint2(0, yAxis - controlPointDistance);
+
+ for (float x = x1; x + 2 * step <= x2;) {
+ controlPoint1.setX(x + step);
+ controlPoint2.setX(x + step);
+ x += 2 * step;
+ path.addBezierCurveTo(controlPoint1, controlPoint2, FloatPoint(x, yAxis));
+ }
+ }
+
+ context.setShouldAntialias(true);
+ context.strokePath(path);
+}
+
+#if ENABLE(CSS3_TEXT_DECORATION_SKIP_INK)
+static bool compareTuples(std::pair<float, float> l, std::pair<float, float> r)
+{
+ return l.first < r.first;
+}
+
+static DashArray translateIntersectionPointsToSkipInkBoundaries(const DashArray& intersections, float dilationAmount, float totalWidth)
+{
+ ASSERT(!(intersections.size() % 2));
+
+ // Step 1: Make pairs so we can sort based on range starting-point. We dilate the ranges in this step as well.
+ Vector<std::pair<float, float>> tuples;
+ for (auto i = intersections.begin(); i != intersections.end(); i++, i++)
+ tuples.append(std::make_pair(*i - dilationAmount, *(i + 1) + dilationAmount));
+ std::sort(tuples.begin(), tuples.end(), &compareTuples);
+
+ // Step 2: Deal with intersecting ranges.
+ Vector<std::pair<float, float>> intermediateTuples;
+ if (tuples.size() >= 2) {
+ intermediateTuples.append(*tuples.begin());
+ for (auto i = tuples.begin() + 1; i != tuples.end(); i++) {
+ float& firstEnd = intermediateTuples.last().second;
+ float secondStart = i->first;
+ float secondEnd = i->second;
+ if (secondStart <= firstEnd && secondEnd <= firstEnd) {
+ // Ignore this range completely
+ } else if (secondStart <= firstEnd)
+ firstEnd = secondEnd;
+ else
+ intermediateTuples.append(*i);
+ }
+ } else
+ intermediateTuples = tuples;
+
+ // Step 3: Output the space between the ranges, but only if the space warrants an underline.
+ float previous = 0;
+ DashArray result;
+ for (const auto& tuple : intermediateTuples) {
+ if (tuple.first - previous > dilationAmount) {
+ result.append(previous);
+ result.append(tuple.first);
+ }
+ previous = tuple.second;
+ }
+ if (totalWidth - previous > dilationAmount) {
+ result.append(previous);
+ result.append(totalWidth);
+ }
+
+ return result;
+}
+
+static void drawSkipInkUnderline(GraphicsContext& context, const FontCascade& font, const TextRun& textRun, const FloatPoint& textOrigin, const FloatPoint& localOrigin,
+ float underlineOffset, float width, bool isPrinting, bool doubleLines, StrokeStyle strokeStyle)
+{
+ FloatPoint adjustedLocalOrigin = localOrigin;
+ adjustedLocalOrigin.move(0, underlineOffset);
+ FloatRect underlineBoundingBox = context.computeUnderlineBoundsForText(adjustedLocalOrigin, width, isPrinting);
+ DashArray intersections = font.dashesForIntersectionsWithRect(textRun, textOrigin, underlineBoundingBox);
+ DashArray a = translateIntersectionPointsToSkipInkBoundaries(intersections, underlineBoundingBox.height(), width);
+ ASSERT(!(a.size() % 2));
+ context.drawLinesForText(adjustedLocalOrigin, a, isPrinting, doubleLines, strokeStyle);
+}
+#endif
+
+static StrokeStyle textDecorationStyleToStrokeStyle(TextDecorationStyle decorationStyle)
+{
+ StrokeStyle strokeStyle = SolidStroke;
+ switch (decorationStyle) {
+ case TextDecorationStyleSolid:
+ strokeStyle = SolidStroke;
+ break;
+ case TextDecorationStyleDouble:
+ strokeStyle = DoubleStroke;
+ break;
+ case TextDecorationStyleDotted:
+ strokeStyle = DottedStroke;
+ break;
+ case TextDecorationStyleDashed:
+ strokeStyle = DashedStroke;
+ break;
+ case TextDecorationStyleWavy:
+ strokeStyle = WavyStroke;
+ break;
+ }
+
+ return strokeStyle;
+}
+
+TextDecorationPainter::TextDecorationPainter(GraphicsContext& context, TextDecoration decoration, const RenderText& renderer, bool isFirstLine)
+ : m_context(context)
+ , m_decoration(decoration)
+ , m_wavyOffset(wavyOffsetFromDecoration())
+ , m_isPrinting(renderer.document().printing())
+ , m_styles(stylesForRenderer(renderer, m_decoration, isFirstLine))
+ , m_lineStyle(isFirstLine ? renderer.firstLineStyle() : renderer.style())
+{
+}
+
+void TextDecorationPainter::paintTextDecoration(const TextRun& textRun, const FloatPoint& textOrigin, const FloatPoint& boxOrigin)
+{
+#if !ENABLE(CSS3_TEXT_DECORATION_SKIP_INK)
+ UNUSED_PARAM(textRun);
+ UNUSED_PARAM(textOrigin);
+#endif
+ ASSERT(m_font);
+ float textDecorationThickness = textDecorationStrokeThickness(m_lineStyle.fontSize());
+ m_context.setStrokeThickness(textDecorationThickness);
+ FloatPoint localOrigin = boxOrigin;
+
+ auto paintDecoration = [&](TextDecoration decoration, TextDecorationStyle style, const Color& color, const FloatPoint& start, const FloatPoint& end, int offset) {
+ m_context.setStrokeColor(color);
+
+ auto strokeStyle = textDecorationStyleToStrokeStyle(style);
+
+ if (style == TextDecorationStyleWavy)
+ strokeWavyTextDecoration(m_context, start, end, textDecorationThickness);
+ else if (decoration == TextDecorationUnderline || decoration == TextDecorationOverline) {
+#if ENABLE(CSS3_TEXT_DECORATION_SKIP_INK)
+ if ((m_lineStyle.textDecorationSkip() == TextDecorationSkipInk || m_lineStyle.textDecorationSkip() == TextDecorationSkipAuto) && m_isHorizontal) {
+ if (!m_context.paintingDisabled())
+ drawSkipInkUnderline(m_context, *m_font, textRun, textOrigin, localOrigin, offset, m_width, m_isPrinting, style == TextDecorationStyleDouble, strokeStyle);
+ } else
+ // FIXME: Need to support text-decoration-skip: none.
+#endif
+ m_context.drawLineForText(start, m_width, m_isPrinting, style == TextDecorationStyleDouble, strokeStyle);
+
+ } else {
+ ASSERT(decoration == TextDecorationLineThrough);
+ m_context.drawLineForText(start, m_width, m_isPrinting, style == TextDecorationStyleDouble, strokeStyle);
+ }
+ };
+
+ bool linesAreOpaque = !m_isPrinting
+ && (!(m_decoration & TextDecorationUnderline) || m_styles.underlineColor.isOpaque())
+ && (!(m_decoration & TextDecorationOverline) || m_styles.overlineColor.isOpaque())
+ && (!(m_decoration & TextDecorationLineThrough) || m_styles.linethroughColor.isOpaque());
+
+ int extraOffset = 0;
+ bool clipping = !linesAreOpaque && m_shadow && m_shadow->next();
+ if (clipping) {
+ FloatRect clipRect(localOrigin, FloatSize(m_width, m_baseline + 2));
+ for (const ShadowData* shadow = m_shadow; shadow; shadow = shadow->next()) {
+ int shadowExtent = shadow->paintingExtent();
+ FloatRect shadowRect(localOrigin, FloatSize(m_width, m_baseline + 2));
+ shadowRect.inflate(shadowExtent);
+ int shadowX = m_isHorizontal ? shadow->x() : shadow->y();
+ int shadowY = m_isHorizontal ? shadow->y() : -shadow->x();
+ shadowRect.move(shadowX, shadowY);
+ clipRect.unite(shadowRect);
+ extraOffset = std::max(extraOffset, std::max(0, shadowY) + shadowExtent);
+ }
+ m_context.save();
+ m_context.clip(clipRect);
+ extraOffset += m_baseline + 2;
+ localOrigin.move(0, extraOffset);
+ }
+
+ const ShadowData* shadow = m_shadow;
+ do {
+ if (shadow) {
+ if (!shadow->next()) {
+ // The last set of lines paints normally inside the clip.
+ localOrigin.move(0, -extraOffset);
+ extraOffset = 0;
+ }
+ int shadowX = m_isHorizontal ? shadow->x() : shadow->y();
+ int shadowY = m_isHorizontal ? shadow->y() : -shadow->x();
+ m_context.setShadow(FloatSize(shadowX, shadowY - extraOffset), shadow->radius(), shadow->color());
+ shadow = shadow->next();
+ }
+
+ // These decorations should match the visual overflows computed in visualOverflowForDecorations()
+ if (m_decoration & TextDecorationUnderline) {
+ const int offset = computeUnderlineOffset(m_lineStyle.textUnderlinePosition(), m_lineStyle.fontMetrics(), m_inlineTextBox, textDecorationThickness);
+ int wavyOffset = m_styles.underlineStyle == TextDecorationStyleWavy ? m_wavyOffset : 0;
+ FloatPoint start = localOrigin + FloatSize(0, offset + wavyOffset);
+ FloatPoint end = localOrigin + FloatSize(m_width, offset + wavyOffset);
+ paintDecoration(TextDecorationUnderline, m_styles.underlineStyle, m_styles.underlineColor, start, end, offset);
+ }
+ if (m_decoration & TextDecorationOverline) {
+ int wavyOffset = m_styles.overlineStyle == TextDecorationStyleWavy ? m_wavyOffset : 0;
+ FloatPoint start = localOrigin - FloatSize(0, wavyOffset);
+ FloatPoint end = localOrigin + FloatSize(m_width, -wavyOffset);
+ paintDecoration(TextDecorationOverline, m_styles.overlineStyle, m_styles.overlineColor, start, end, 0);
+ }
+ if (m_decoration & TextDecorationLineThrough) {
+ FloatPoint start = localOrigin + FloatSize(0, 2 * m_baseline / 3);
+ FloatPoint end = localOrigin + FloatSize(m_width, 2 * m_baseline / 3);
+ paintDecoration(TextDecorationLineThrough, m_styles.linethroughStyle, m_styles.linethroughColor, start, end, 0);
+ }
+ } while (shadow);
+
+ if (clipping)
+ m_context.restore();
+ else if (m_shadow)
+ m_context.clearShadow();
+}
+
+static Color decorationColor(const RenderStyle& style)
+{
+ // Check for text decoration color first.
+ Color result = style.visitedDependentColor(CSSPropertyWebkitTextDecorationColor);
+ if (result.isValid())
+ return result;
+ if (style.textStrokeWidth() > 0) {
+ // Prefer stroke color if possible but not if it's fully transparent.
+ result = style.visitedDependentColor(CSSPropertyWebkitTextStrokeColor);
+ if (result.isVisible())
+ return result;
+ }
+
+ return style.visitedDependentColor(CSSPropertyWebkitTextFillColor);
+}
+
+static void collectStylesForRenderer(TextDecorationPainter::Styles& result, const RenderObject& renderer, unsigned requestedDecorations, bool firstLineStyle)
+{
+ unsigned remainingDecoration = requestedDecorations;
+ auto extractDecorations = [&] (const RenderStyle& style, unsigned decorations) {
+ auto color = decorationColor(style);
+ auto decorationStyle = style.textDecorationStyle();
+
+ if (decorations & TextDecorationUnderline) {
+ remainingDecoration &= ~TextDecorationUnderline;
+ result.underlineColor = color;
+ result.underlineStyle = decorationStyle;
+ }
+ if (decorations & TextDecorationOverline) {
+ remainingDecoration &= ~TextDecorationOverline;
+ result.overlineColor = color;
+ result.overlineStyle = decorationStyle;
+ }
+ if (decorations & TextDecorationLineThrough) {
+ remainingDecoration &= ~TextDecorationLineThrough;
+ result.linethroughColor = color;
+ result.linethroughStyle = decorationStyle;
+ }
+
+ };
+
+ auto* current = &renderer;
+ do {
+ auto& style = firstLineStyle ? current->firstLineStyle() : current->style();
+ extractDecorations(style, style.textDecoration());
+
+ if (current->isRubyText())
+ return;
+
+ current = current->parent();
+ if (current && current->isAnonymousBlock() && downcast<RenderBlock>(*current).continuation())
+ current = downcast<RenderBlock>(*current).continuation();
+
+ if (!remainingDecoration)
+ break;
+
+ } while (current && !is<HTMLAnchorElement>(current->node()) && !is<HTMLFontElement>(current->node()));
+
+ // If we bailed out, use the element we bailed out at (typically a <font> or <a> element).
+ if (remainingDecoration && current) {
+ auto& style = firstLineStyle ? current->firstLineStyle() : current->style();
+ extractDecorations(style, remainingDecoration);
+ }
+}
+
+auto TextDecorationPainter::stylesForRenderer(const RenderObject& renderer, unsigned requestedDecorations, bool firstLineStyle) -> Styles
+{
+ Styles result;
+ collectStylesForRenderer(result, renderer, requestedDecorations, false);
+ if (firstLineStyle)
+ collectStylesForRenderer(result, renderer, requestedDecorations, true);
+ return result;
+}
+
+} // namespace WebCore