1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
6 #include "core/paint/InlineTextBoxPainter.h"
8 #include "core/editing/CompositionUnderline.h"
9 #include "core/editing/Editor.h"
10 #include "core/editing/markers/DocumentMarkerController.h"
11 #include "core/editing/markers/RenderedDocumentMarker.h"
12 #include "core/frame/LocalFrame.h"
13 #include "core/layout/LayoutBlock.h"
14 #include "core/layout/LayoutTextCombine.h"
15 #include "core/layout/LayoutTheme.h"
16 #include "core/layout/api/LineLayoutBox.h"
17 #include "core/layout/api/LineLayoutText.h"
18 #include "core/layout/line/InlineTextBox.h"
19 #include "core/paint/BoxPainter.h"
20 #include "core/paint/LayoutObjectDrawingRecorder.h"
21 #include "core/paint/PaintInfo.h"
22 #include "core/paint/TextPainter.h"
23 #include "platform/graphics/GraphicsContextStateSaver.h"
24 #include "wtf/Optional.h"
28 typedef WTF::HashMap
<const InlineTextBox
*, TextBlobPtr
> InlineTextBoxBlobCacheMap
;
29 static InlineTextBoxBlobCacheMap
* gTextBlobCache
;
31 static const int misspellingLineThickness
= 3;
33 void InlineTextBoxPainter::removeFromTextBlobCache(InlineTextBox
& inlineTextBox
)
36 gTextBlobCache
->remove(&inlineTextBox
);
39 static TextBlobPtr
* addToTextBlobCache(InlineTextBox
& inlineTextBox
)
42 gTextBlobCache
= new InlineTextBoxBlobCacheMap
;
43 return &gTextBlobCache
->add(&inlineTextBox
, nullptr).storedValue
->value
;
46 static bool paintsMarkerHighlights(const LayoutObject
& layoutObject
)
48 return layoutObject
.node() && layoutObject
.document().markers().hasMarkers(layoutObject
.node());
51 static bool paintsCompositionMarkers(const LayoutObject
& layoutObject
)
53 return layoutObject
.node() && layoutObject
.document().markers().markersFor(layoutObject
.node(), DocumentMarker::Composition
).size() > 0;
56 void InlineTextBoxPainter::paint(const PaintInfo
& paintInfo
, const LayoutPoint
& paintOffset
)
58 if (!shouldPaintTextBox(paintInfo
))
61 ASSERT(paintInfo
.phase
!= PaintPhaseOutline
&& paintInfo
.phase
!= PaintPhaseSelfOutline
&& paintInfo
.phase
!= PaintPhaseChildOutlines
);
63 LayoutRect logicalVisualOverflow
= m_inlineTextBox
.logicalOverflowRect();
64 LayoutUnit logicalStart
= logicalVisualOverflow
.x() + (m_inlineTextBox
.isHorizontal() ? paintOffset
.x() : paintOffset
.y());
65 LayoutUnit logicalExtent
= logicalVisualOverflow
.width();
67 LayoutUnit paintEnd
= m_inlineTextBox
.isHorizontal() ? paintInfo
.rect
.maxX() : paintInfo
.rect
.maxY();
68 LayoutUnit paintStart
= m_inlineTextBox
.isHorizontal() ? paintInfo
.rect
.x() : paintInfo
.rect
.y();
70 // We round the y-axis to ensure consistent line heights.
71 LayoutPoint adjustedPaintOffset
= LayoutPoint(paintOffset
.x(), paintOffset
.y().round());
73 if (logicalStart
>= paintEnd
|| logicalStart
+ logicalExtent
<= paintStart
)
76 bool isPrinting
= paintInfo
.isPrinting();
78 // Determine whether or not we're selected.
79 bool haveSelection
= !isPrinting
&& paintInfo
.phase
!= PaintPhaseTextClip
&& m_inlineTextBox
.selectionState() != SelectionNone
;
80 if (!haveSelection
&& paintInfo
.phase
== PaintPhaseSelection
) {
81 // When only painting the selection, don't bother to paint if there is none.
85 // The text clip phase already has a LayoutObjectDrawingRecorder. Text clips are initiated only in BoxPainter::paintLayerExtended,
86 // which is already within a LayoutObjectDrawingRecorder.
87 Optional
<LayoutObjectDrawingRecorder
> drawingRecorder
;
88 if (paintInfo
.phase
!= PaintPhaseTextClip
) {
89 if (LayoutObjectDrawingRecorder::useCachedDrawingIfPossible(*paintInfo
.context
, m_inlineTextBox
, paintInfo
.phase
, paintOffset
))
91 LayoutRect
paintRect(logicalVisualOverflow
);
92 m_inlineTextBox
.logicalRectToPhysicalRect(paintRect
);
93 if (paintInfo
.phase
!= PaintPhaseSelection
&& (haveSelection
|| paintsMarkerHighlights(m_inlineTextBox
.layoutObject())))
94 paintRect
.unite(m_inlineTextBox
.localSelectionRect(m_inlineTextBox
.start(), m_inlineTextBox
.start() + m_inlineTextBox
.len()));
95 paintRect
.moveBy(adjustedPaintOffset
);
96 drawingRecorder
.emplace(*paintInfo
.context
, m_inlineTextBox
, paintInfo
.phase
, paintRect
, paintOffset
);
99 if (m_inlineTextBox
.truncation() != cNoTruncation
) {
100 if (m_inlineTextBox
.lineLayoutItem().containingBlock().style()->isLeftToRightDirection() != m_inlineTextBox
.isLeftToRightDirection()) {
101 // Make the visible fragment of text hug the edge closest to the rest of the run by moving the origin
102 // at which we start drawing text.
103 // e.g. In the case of LTR text truncated in an RTL Context, the correct behavior is:
104 // |Hello|CBA| -> |...He|CBA|
105 // In order to draw the fragment "He" aligned to the right edge of it's box, we need to start drawing
106 // farther to the right.
107 // NOTE: WebKit's behavior differs from that of IE which appears to just overlay the ellipsis on top of the
108 // truncated string i.e. |Hello|CBA| -> |...lo|CBA|
109 LayoutUnit widthOfVisibleText
= m_inlineTextBox
.lineLayoutItem().width(m_inlineTextBox
.start(), m_inlineTextBox
.truncation(), m_inlineTextBox
.textPos(), m_inlineTextBox
.isLeftToRightDirection() ? LTR
: RTL
, m_inlineTextBox
.isFirstLineStyle());
110 LayoutUnit widthOfHiddenText
= m_inlineTextBox
.logicalWidth() - widthOfVisibleText
;
111 // FIXME: The hit testing logic also needs to take this translation into account.
112 LayoutSize
truncationOffset(m_inlineTextBox
.isLeftToRightDirection() ? widthOfHiddenText
: -widthOfHiddenText
, 0);
113 adjustedPaintOffset
.move(m_inlineTextBox
.isHorizontal() ? truncationOffset
: truncationOffset
.transposedSize());
117 GraphicsContext
* context
= paintInfo
.context
;
118 const ComputedStyle
& styleToUse
= m_inlineTextBox
.lineLayoutItem().styleRef(m_inlineTextBox
.isFirstLineStyle());
120 LayoutPoint
boxOrigin(m_inlineTextBox
.locationIncludingFlipping());
121 boxOrigin
.move(adjustedPaintOffset
.x(), adjustedPaintOffset
.y());
122 LayoutRect
boxRect(boxOrigin
, LayoutSize(m_inlineTextBox
.logicalWidth(), m_inlineTextBox
.logicalHeight()));
124 bool shouldRotate
= false;
125 LayoutTextCombine
* combinedText
= nullptr;
126 if (!m_inlineTextBox
.isHorizontal()) {
127 if (styleToUse
.hasTextCombine() && m_inlineTextBox
.lineLayoutItem().isCombineText()) {
128 combinedText
= &toLayoutTextCombine(m_inlineTextBox
.layoutObject());
129 if (!combinedText
->isCombined())
130 combinedText
= nullptr;
133 combinedText
->updateFont();
134 boxRect
.setWidth(combinedText
->inlineWidthForLayout());
137 context
->concatCTM(TextPainter::rotation(boxRect
, TextPainter::Clockwise
));
141 // Determine text colors.
142 TextPainter::Style textStyle
= TextPainter::textPaintingStyle(m_inlineTextBox
.layoutObject(), styleToUse
, paintInfo
);
143 TextPainter::Style selectionStyle
= TextPainter::selectionPaintingStyle(m_inlineTextBox
.layoutObject(), haveSelection
, paintInfo
, textStyle
);
144 bool paintSelectedTextOnly
= (paintInfo
.phase
== PaintPhaseSelection
);
145 bool paintSelectedTextSeparately
= !paintSelectedTextOnly
&& textStyle
!= selectionStyle
;
148 const Font
& font
= styleToUse
.font();
150 LayoutPoint
textOrigin(boxOrigin
.x(), boxOrigin
.y() + font
.fontMetrics().ascent());
152 // 1. Paint backgrounds behind text if needed. Examples of such backgrounds include selection
153 // and composition highlights.
154 if (paintInfo
.phase
!= PaintPhaseSelection
&& paintInfo
.phase
!= PaintPhaseTextClip
&& !isPrinting
) {
155 paintDocumentMarkers(context
, boxOrigin
, styleToUse
, font
, true);
157 if (haveSelection
&& !paintsCompositionMarkers(m_inlineTextBox
.layoutObject())) {
159 paintSelection
<InlineTextBoxPainter::PaintOptions::CombinedText
>(context
, boxRect
, styleToUse
, font
, selectionStyle
.fillColor
, combinedText
);
161 paintSelection
<InlineTextBoxPainter::PaintOptions::Normal
>(context
, boxRect
, styleToUse
, font
, selectionStyle
.fillColor
);
165 // 2. Now paint the foreground, including text and decorations like underline/overline (in quirks mode only).
166 int length
= m_inlineTextBox
.len();
167 StringView string
= m_inlineTextBox
.lineLayoutItem().text().createView();
168 ASSERT(m_inlineTextBox
.start() + length
<= string
.length());
169 if (static_cast<unsigned>(length
) != string
.length() || m_inlineTextBox
.start())
170 string
.narrow(m_inlineTextBox
.start(), length
);
171 int maximumLength
= m_inlineTextBox
.lineLayoutItem().textLength() - m_inlineTextBox
.start();
173 StringBuilder charactersWithHyphen
;
174 TextRun textRun
= m_inlineTextBox
.constructTextRun(styleToUse
, font
, string
, maximumLength
, m_inlineTextBox
.hasHyphen() ? &charactersWithHyphen
: 0);
175 if (m_inlineTextBox
.hasHyphen())
176 length
= textRun
.length();
178 int selectionStart
= 0;
179 int selectionEnd
= 0;
180 if (paintSelectedTextOnly
|| paintSelectedTextSeparately
)
181 m_inlineTextBox
.selectionStartEnd(selectionStart
, selectionEnd
);
183 bool respectHyphen
= selectionEnd
== static_cast<int>(m_inlineTextBox
.len()) && m_inlineTextBox
.hasHyphen();
185 selectionEnd
= textRun
.length();
187 if (m_inlineTextBox
.truncation() != cNoTruncation
) {
188 selectionStart
= std::min
<int>(selectionStart
, m_inlineTextBox
.truncation());
189 selectionEnd
= std::min
<int>(selectionEnd
, m_inlineTextBox
.truncation());
190 length
= m_inlineTextBox
.truncation();
193 TextPainter
textPainter(context
, font
, textRun
, textOrigin
, boxRect
, m_inlineTextBox
.isHorizontal());
194 TextEmphasisPosition emphasisMarkPosition
;
195 bool hasTextEmphasis
= m_inlineTextBox
.getEmphasisMarkPosition(styleToUse
, emphasisMarkPosition
);
197 textPainter
.setEmphasisMark(styleToUse
.textEmphasisMarkString(), emphasisMarkPosition
);
199 textPainter
.setCombinedText(combinedText
);
201 if (!paintSelectedTextOnly
) {
202 // FIXME: Truncate right-to-left text correctly.
204 int endOffset
= length
;
205 if (paintSelectedTextSeparately
&& selectionStart
< selectionEnd
) {
206 startOffset
= selectionEnd
;
207 endOffset
= selectionStart
;
210 // FIXME: This cache should probably ultimately be held somewhere else.
211 // A hashmap is convenient to avoid a memory hit when the
212 // RuntimeEnabledFeature is off.
213 bool textBlobIsCacheable
= startOffset
== 0 && endOffset
== length
;
214 TextBlobPtr
* cachedTextBlob
= 0;
215 if (textBlobIsCacheable
)
216 cachedTextBlob
= addToTextBlobCache(m_inlineTextBox
);
217 textPainter
.paint(startOffset
, endOffset
, length
, textStyle
, cachedTextBlob
);
220 if ((paintSelectedTextOnly
|| paintSelectedTextSeparately
) && selectionStart
< selectionEnd
) {
221 // paint only the text that is selected
222 bool textBlobIsCacheable
= selectionStart
== 0 && selectionEnd
== length
;
223 TextBlobPtr
* cachedTextBlob
= 0;
224 if (textBlobIsCacheable
)
225 cachedTextBlob
= addToTextBlobCache(m_inlineTextBox
);
226 textPainter
.paint(selectionStart
, selectionEnd
, length
, selectionStyle
, cachedTextBlob
);
230 TextDecoration textDecorations
= styleToUse
.textDecorationsInEffect();
231 if (textDecorations
!= TextDecorationNone
&& !paintSelectedTextOnly
) {
232 GraphicsContextStateSaver
stateSaver(*context
, false);
233 TextPainter::updateGraphicsContext(context
, textStyle
, m_inlineTextBox
.isHorizontal(), stateSaver
);
235 context
->concatCTM(TextPainter::rotation(boxRect
, TextPainter::Clockwise
));
236 paintDecoration(paintInfo
, boxOrigin
, textDecorations
);
238 context
->concatCTM(TextPainter::rotation(boxRect
, TextPainter::Counterclockwise
));
241 if (paintInfo
.phase
== PaintPhaseForeground
)
242 paintDocumentMarkers(context
, boxOrigin
, styleToUse
, font
, false);
245 context
->concatCTM(TextPainter::rotation(boxRect
, TextPainter::Counterclockwise
));
248 bool InlineTextBoxPainter::shouldPaintTextBox(const PaintInfo
& paintInfo
)
250 // When painting selection, we want to include a highlight when the
251 // selection spans line breaks. In other cases such as invisible elements
252 // or those with no text that are not line breaks, we can skip painting
254 // TODO(wkorman): Constrain line break painting to appropriate paint phase.
255 // This code path is only called in PaintPhaseForeground whereas we would
256 // expect PaintPhaseSelection. The existing haveSelection logic in paint()
257 // tests for != PaintPhaseTextClip.
258 bool paintLineBreaks
= RuntimeEnabledFeatures::selectionPaintingWithoutSelectionGapsEnabled()
259 // TODO(wkorman): Remove horizontal and RTL restrictions once operational.
260 && m_inlineTextBox
.isHorizontal()
261 && m_inlineTextBox
.isLeftToRightDirection();
262 if ((!paintLineBreaks
&& m_inlineTextBox
.isLineBreak())
263 || !paintInfo
.shouldPaintWithinRoot(&m_inlineTextBox
.layoutObject())
264 || m_inlineTextBox
.lineLayoutItem().style()->visibility() != VISIBLE
265 || m_inlineTextBox
.truncation() == cFullTruncation
266 || !m_inlineTextBox
.len())
271 unsigned InlineTextBoxPainter::underlinePaintStart(const CompositionUnderline
& underline
)
273 return std::max(static_cast<unsigned>(m_inlineTextBox
.start()), underline
.startOffset
);
276 unsigned InlineTextBoxPainter::underlinePaintEnd(const CompositionUnderline
& underline
)
278 unsigned paintEnd
= std::min(m_inlineTextBox
.end() + 1, underline
.endOffset
); // end() points at the last char, not past it.
279 if (m_inlineTextBox
.truncation() != cNoTruncation
)
280 paintEnd
= std::min(paintEnd
, static_cast<unsigned>(m_inlineTextBox
.start() + m_inlineTextBox
.truncation()));
284 void InlineTextBoxPainter::paintSingleCompositionBackgroundRun(GraphicsContext
* context
, const LayoutPoint
& boxOrigin
, const ComputedStyle
& style
, const Font
& font
, Color backgroundColor
, int startPos
, int endPos
)
286 if (backgroundColor
== Color::transparent
)
289 int sPos
= std::max(startPos
- static_cast<int>(m_inlineTextBox
.start()), 0);
290 int ePos
= std::min(endPos
- static_cast<int>(m_inlineTextBox
.start()), static_cast<int>(m_inlineTextBox
.len()));
294 int deltaY
= m_inlineTextBox
.lineLayoutItem().style()->isFlippedLinesWritingMode() ? m_inlineTextBox
.root().selectionBottom() - m_inlineTextBox
.logicalBottom() : m_inlineTextBox
.logicalTop() - m_inlineTextBox
.root().selectionTop();
295 int selHeight
= m_inlineTextBox
.root().selectionHeight();
296 FloatPoint
localOrigin(boxOrigin
.x().toFloat(), boxOrigin
.y().toFloat() - deltaY
);
297 context
->drawHighlightForText(font
, m_inlineTextBox
.constructTextRun(style
, font
), localOrigin
, selHeight
, backgroundColor
, sPos
, ePos
);
300 void InlineTextBoxPainter::paintDocumentMarkers(GraphicsContext
* pt
, const LayoutPoint
& boxOrigin
, const ComputedStyle
& style
, const Font
& font
, bool background
)
302 if (!m_inlineTextBox
.lineLayoutItem().node())
305 DocumentMarkerVector markers
= m_inlineTextBox
.lineLayoutItem().document().markers().markersFor(m_inlineTextBox
.lineLayoutItem().node());
306 DocumentMarkerVector::const_iterator markerIt
= markers
.begin();
308 // Give any document markers that touch this run a chance to draw before the text has been drawn.
309 // Note end() points at the last char, not one past it like endOffset and ranges do.
310 for ( ; markerIt
!= markers
.end(); ++markerIt
) {
311 DocumentMarker
* marker
= *markerIt
;
313 // Paint either the background markers or the foreground markers, but not both
314 switch (marker
->type()) {
315 case DocumentMarker::Grammar
:
316 case DocumentMarker::Spelling
:
320 case DocumentMarker::TextMatch
:
324 case DocumentMarker::Composition
:
330 if (marker
->endOffset() <= m_inlineTextBox
.start()) {
331 // marker is completely before this run. This might be a marker that sits before the
332 // first run we draw, or markers that were within runs we skipped due to truncation.
335 if (marker
->startOffset() > m_inlineTextBox
.end()) {
336 // marker is completely after this run, bail. A later run will paint it.
340 // marker intersects this run. Paint it.
341 switch (marker
->type()) {
342 case DocumentMarker::Spelling
:
343 m_inlineTextBox
.paintDocumentMarker(pt
, boxOrigin
, marker
, style
, font
, false);
345 case DocumentMarker::Grammar
:
346 m_inlineTextBox
.paintDocumentMarker(pt
, boxOrigin
, marker
, style
, font
, true);
348 case DocumentMarker::TextMatch
:
349 m_inlineTextBox
.paintTextMatchMarker(pt
, boxOrigin
, marker
, style
, font
);
351 case DocumentMarker::Composition
:
353 CompositionUnderline
underline(marker
->startOffset(), marker
->endOffset(), marker
->underlineColor(), marker
->thick(), marker
->backgroundColor());
355 paintSingleCompositionBackgroundRun(pt
, boxOrigin
, style
, font
, underline
.backgroundColor
, underlinePaintStart(underline
), underlinePaintEnd(underline
));
357 paintCompositionUnderline(pt
, boxOrigin
, underline
);
361 ASSERT_NOT_REACHED();
366 static GraphicsContext::DocumentMarkerLineStyle
lineStyleForMarkerType(DocumentMarker::MarkerType markerType
)
368 switch (markerType
) {
369 case DocumentMarker::Spelling
:
370 return GraphicsContext::DocumentMarkerSpellingLineStyle
;
371 case DocumentMarker::Grammar
:
372 return GraphicsContext::DocumentMarkerGrammarLineStyle
;
374 ASSERT_NOT_REACHED();
375 return GraphicsContext::DocumentMarkerSpellingLineStyle
;
379 void InlineTextBoxPainter::paintDocumentMarker(GraphicsContext
* pt
, const LayoutPoint
& boxOrigin
, DocumentMarker
* marker
, const ComputedStyle
& style
, const Font
& font
, bool grammar
)
381 // Never print spelling/grammar markers (5327887)
382 if (m_inlineTextBox
.lineLayoutItem().document().printing())
385 if (m_inlineTextBox
.truncation() == cFullTruncation
)
388 LayoutUnit start
= 0; // start of line to draw, relative to tx
389 LayoutUnit width
= m_inlineTextBox
.logicalWidth(); // how much line to draw
391 // Determine whether we need to measure text
392 bool markerSpansWholeBox
= true;
393 if (m_inlineTextBox
.start() <= marker
->startOffset())
394 markerSpansWholeBox
= false;
395 if ((m_inlineTextBox
.end() + 1) != marker
->endOffset()) // end points at the last char, not past it
396 markerSpansWholeBox
= false;
397 if (m_inlineTextBox
.truncation() != cNoTruncation
)
398 markerSpansWholeBox
= false;
400 if (!markerSpansWholeBox
|| grammar
) {
401 int startPosition
= std::max
<int>(marker
->startOffset() - m_inlineTextBox
.start(), 0);
402 int endPosition
= std::min
<int>(marker
->endOffset() - static_cast<int>(m_inlineTextBox
.start()), m_inlineTextBox
.len());
404 if (m_inlineTextBox
.truncation() != cNoTruncation
)
405 endPosition
= std::min
<int>(endPosition
, m_inlineTextBox
.truncation());
407 // Calculate start & width
408 int deltaY
= m_inlineTextBox
.lineLayoutItem().style()->isFlippedLinesWritingMode() ? m_inlineTextBox
.root().selectionBottom() - m_inlineTextBox
.logicalBottom() : m_inlineTextBox
.logicalTop() - m_inlineTextBox
.root().selectionTop();
409 int selHeight
= m_inlineTextBox
.root().selectionHeight();
410 LayoutPoint
startPoint(boxOrigin
.x(), boxOrigin
.y() - deltaY
);
411 TextRun run
= m_inlineTextBox
.constructTextRun(style
, font
);
413 // FIXME: Convert the document markers to float rects.
414 IntRect markerRect
= enclosingIntRect(font
.selectionRectForText(run
, FloatPoint(startPoint
), selHeight
, startPosition
, endPosition
));
415 start
= markerRect
.x() - startPoint
.x();
416 width
= markerRect
.width();
419 // IMPORTANT: The misspelling underline is not considered when calculating the text bounds, so we have to
420 // make sure to fit within those bounds. This means the top pixel(s) of the underline will overlap the
421 // bottom pixel(s) of the glyphs in smaller font sizes. The alternatives are to increase the line spacing (bad!!)
422 // or decrease the underline thickness. The overlap is actually the most useful, and matches what AppKit does.
423 // So, we generally place the underline at the bottom of the text, but in larger fonts that's not so good so
424 // we pin to two pixels under the baseline.
425 int lineThickness
= misspellingLineThickness
;
426 int baseline
= m_inlineTextBox
.lineLayoutItem().style(m_inlineTextBox
.isFirstLineStyle())->fontMetrics().ascent();
427 int descent
= m_inlineTextBox
.logicalHeight() - baseline
;
429 if (descent
<= (lineThickness
+ 2)) {
430 // Place the underline at the very bottom of the text in small/medium fonts.
431 underlineOffset
= m_inlineTextBox
.logicalHeight() - lineThickness
;
433 // In larger fonts, though, place the underline up near the baseline to prevent a big gap.
434 underlineOffset
= baseline
+ 2;
436 pt
->drawLineForDocumentMarker(FloatPoint((boxOrigin
.x() + start
).toFloat(), (boxOrigin
.y() + underlineOffset
).toFloat()), width
.toFloat(), lineStyleForMarkerType(marker
->type()));
439 template <InlineTextBoxPainter::PaintOptions options
>
440 void InlineTextBoxPainter::paintSelection(GraphicsContext
* context
, const LayoutRect
& boxRect
, const ComputedStyle
& style
, const Font
& font
, Color textColor
, LayoutTextCombine
* combinedText
)
442 // See if we have a selection to paint at all.
444 m_inlineTextBox
.selectionStartEnd(sPos
, ePos
);
448 Color c
= m_inlineTextBox
.lineLayoutItem().selectionBackgroundColor();
452 // If the text color ends up being the same as the selection background, invert the selection
455 c
= Color(0xff - c
.red(), 0xff - c
.green(), 0xff - c
.blue());
457 // If the text is truncated, let the thing being painted in the truncation
458 // draw its own highlight.
459 int length
= m_inlineTextBox
.truncation() != cNoTruncation
? m_inlineTextBox
.truncation() : m_inlineTextBox
.len();
460 StringView string
= m_inlineTextBox
.lineLayoutItem().text().createView();
462 if (string
.length() != static_cast<unsigned>(length
) || m_inlineTextBox
.start())
463 string
.narrow(m_inlineTextBox
.start(), length
);
465 StringBuilder charactersWithHyphen
;
466 bool respectHyphen
= ePos
== length
&& m_inlineTextBox
.hasHyphen();
467 TextRun textRun
= m_inlineTextBox
.constructTextRun(style
, font
, string
, m_inlineTextBox
.lineLayoutItem().textLength() - m_inlineTextBox
.start(), respectHyphen
? &charactersWithHyphen
: 0);
469 ePos
= textRun
.length();
471 GraphicsContextStateSaver
stateSaver(*context
);
473 if (options
== InlineTextBoxPainter::PaintOptions::CombinedText
) {
474 ASSERT(combinedText
);
475 // We can't use the height of m_inlineTextBox because LayoutTextCombine's inlineTextBox is horizontal within vertical flow
476 LayoutRect
clipRect(boxRect
);
477 combinedText
->transformLayoutRect(clipRect
);
478 context
->clip(FloatRect(clipRect
));
479 combinedText
->transformToInlineCoordinates(*context
, boxRect
);
480 context
->drawHighlightForText(font
, textRun
, FloatPoint(boxRect
.location()), boxRect
.height(), c
, sPos
, ePos
);
484 LayoutUnit selectionBottom
= m_inlineTextBox
.root().selectionBottom();
485 LayoutUnit selectionTop
= m_inlineTextBox
.root().selectionTopAdjustedForPrecedingBlock();
487 int deltaY
= roundToInt(m_inlineTextBox
.lineLayoutItem().style()->isFlippedLinesWritingMode() ? selectionBottom
- m_inlineTextBox
.logicalBottom() : m_inlineTextBox
.logicalTop() - selectionTop
);
488 int selHeight
= std::max(0, roundToInt(selectionBottom
- selectionTop
));
490 FloatPoint
localOrigin(boxRect
.x().toFloat(), (boxRect
.y() - deltaY
).toFloat());
492 LayoutUnit selectionWidth
= m_inlineTextBox
.logicalWidth();
493 LayoutRect selectionRect
= LayoutRect(font
.selectionRectForText(textRun
, localOrigin
, selHeight
, sPos
, ePos
));
494 if (m_inlineTextBox
.hasWrappedSelectionNewline()) {
495 expandToIncludeNewlineForSelection(selectionRect
);
496 // TODO(wkorman): Make this work with RTL and vertical text.
497 selectionWidth
+= m_inlineTextBox
.newlineSpaceWidth();
499 FloatRect
clipRect(localOrigin
, FloatSize(selectionWidth
.toFloat(), selHeight
));
500 // TODO(wkorman): Experiment with not clipping.
501 context
->clip(clipRect
);
502 context
->fillRect(FloatRect(selectionRect
), c
);
505 void InlineTextBoxPainter::expandToIncludeNewlineForSelection(LayoutRect
& rect
)
507 // TODO(wkorman): Make this work with RTL and vertical text.
508 rect
.expand(FloatRectOutsets(0, m_inlineTextBox
.newlineSpaceWidth(), 0, 0));
511 static int computeUnderlineOffset(const TextUnderlinePosition underlinePosition
, const FontMetrics
& fontMetrics
, const InlineTextBox
* inlineTextBox
, const float textDecorationThickness
)
513 // Compute the gap between the font and the underline. Use at least one
514 // pixel gap, if underline is thick then use a bigger gap.
517 // Underline position of zero means draw underline on Baseline Position,
518 // in Blink we need at least 1-pixel gap to adding following check.
519 // Positive underline Position means underline should be drawn above baselin e
520 // and negative value means drawing below baseline, negating the value as in Blink
521 // downward Y-increases.
523 if (fontMetrics
.underlinePosition())
524 gap
= -fontMetrics
.underlinePosition();
526 gap
= std::max
<int>(1, ceilf(textDecorationThickness
/ 2.f
));
528 // FIXME: We support only horizontal text for now.
529 switch (underlinePosition
) {
530 case TextUnderlinePositionAuto
:
531 return fontMetrics
.ascent() + gap
; // Position underline near the alphabetic baseline.
532 case TextUnderlinePositionUnder
: {
533 // Position underline relative to the under edge of the lowest element's content box.
534 const LayoutUnit offset
= inlineTextBox
->root().maxLogicalTop() - inlineTextBox
->logicalTop();
536 return inlineTextBox
->logicalHeight() + gap
+ offset
;
537 return inlineTextBox
->logicalHeight() + gap
;
541 ASSERT_NOT_REACHED();
542 return fontMetrics
.ascent() + gap
;
545 static bool shouldSetDecorationAntialias(TextDecorationStyle decorationStyle
)
547 return decorationStyle
== TextDecorationStyleDotted
|| decorationStyle
== TextDecorationStyleDashed
;
550 static bool shouldSetDecorationAntialias(TextDecorationStyle underline
, TextDecorationStyle overline
, TextDecorationStyle linethrough
)
552 return shouldSetDecorationAntialias(underline
) || shouldSetDecorationAntialias(overline
) || shouldSetDecorationAntialias(linethrough
);
555 static StrokeStyle
textDecorationStyleToStrokeStyle(TextDecorationStyle decorationStyle
)
557 StrokeStyle strokeStyle
= SolidStroke
;
558 switch (decorationStyle
) {
559 case TextDecorationStyleSolid
:
560 strokeStyle
= SolidStroke
;
562 case TextDecorationStyleDouble
:
563 strokeStyle
= DoubleStroke
;
565 case TextDecorationStyleDotted
:
566 strokeStyle
= DottedStroke
;
568 case TextDecorationStyleDashed
:
569 strokeStyle
= DashedStroke
;
571 case TextDecorationStyleWavy
:
572 strokeStyle
= WavyStroke
;
579 static void adjustStepToDecorationLength(float& step
, float& controlPointDistance
, float length
)
586 unsigned stepCount
= static_cast<unsigned>(length
/ step
);
588 // Each Bezier curve starts at the same pixel that the previous one
589 // ended. We need to subtract (stepCount - 1) pixels when calculating the
590 // length covered to account for that.
591 float uncoveredLength
= length
- (stepCount
* step
- (stepCount
- 1));
592 float adjustment
= uncoveredLength
/ stepCount
;
594 controlPointDistance
+= adjustment
;
598 * Draw one cubic Bezier curve and repeat the same pattern long the the decoration's axis.
599 * The start point (p1), controlPoint1, controlPoint2 and end point (p2) of the Bezier curve
600 * form a diamond shape:
612 * (x1, y1) p1 + . + p2 (x2, y2) - <--- Decoration's axis
615 * . . | controlPointDistance
624 static void strokeWavyTextDecoration(GraphicsContext
* context
, FloatPoint p1
, FloatPoint p2
, float strokeThickness
)
626 context
->adjustLineToPixelBoundaries(p1
, p2
, strokeThickness
, context
->strokeStyle());
631 // Distance between decoration's axis and Bezier curve's control points.
632 // The height of the curve is based on this distance. Use a minimum of 6 pixels distance since
633 // the actual curve passes approximately at half of that distance, that is 3 pixels.
634 // The minimum height of the curve is also approximately 3 pixels. Increases the curve's height
635 // as strockThickness increases to make the curve looks better.
636 float controlPointDistance
= 3 * std::max
<float>(2, strokeThickness
);
638 // Increment used to form the diamond shape between start point (p1), control
639 // points and end point (p2) along the axis of the decoration. Makes the
640 // curve wider as strockThickness increases to make the curve looks better.
641 float step
= 2 * std::max
<float>(2, strokeThickness
);
643 bool isVerticalLine
= (p1
.x() == p2
.x());
645 if (isVerticalLine
) {
646 ASSERT(p1
.x() == p2
.x());
648 float xAxis
= p1
.x();
652 if (p1
.y() < p2
.y()) {
660 adjustStepToDecorationLength(step
, controlPointDistance
, y2
- y1
);
661 FloatPoint
controlPoint1(xAxis
+ controlPointDistance
, 0);
662 FloatPoint
controlPoint2(xAxis
- controlPointDistance
, 0);
664 for (float y
= y1
; y
+ 2 * step
<= y2
;) {
665 controlPoint1
.setY(y
+ step
);
666 controlPoint2
.setY(y
+ step
);
668 path
.addBezierCurveTo(controlPoint1
, controlPoint2
, FloatPoint(xAxis
, y
));
671 ASSERT(p1
.y() == p2
.y());
673 float yAxis
= p1
.y();
677 if (p1
.x() < p2
.x()) {
685 adjustStepToDecorationLength(step
, controlPointDistance
, x2
- x1
);
686 FloatPoint
controlPoint1(0, yAxis
+ controlPointDistance
);
687 FloatPoint
controlPoint2(0, yAxis
- controlPointDistance
);
689 for (float x
= x1
; x
+ 2 * step
<= x2
;) {
690 controlPoint1
.setX(x
+ step
);
691 controlPoint2
.setX(x
+ step
);
693 path
.addBezierCurveTo(controlPoint1
, controlPoint2
, FloatPoint(x
, yAxis
));
697 context
->setShouldAntialias(true);
698 context
->strokePath(path
);
701 static void paintAppliedDecoration(GraphicsContext
* context
, FloatPoint start
, float width
, float doubleOffset
, int wavyOffsetFactor
,
702 LayoutObject::AppliedTextDecoration decoration
, float thickness
, bool antialiasDecoration
, bool isPrinting
)
704 context
->setStrokeStyle(textDecorationStyleToStrokeStyle(decoration
.style
));
705 context
->setStrokeColor(decoration
.color
);
707 switch (decoration
.style
) {
708 case TextDecorationStyleWavy
:
709 strokeWavyTextDecoration(context
, start
+ FloatPoint(0, doubleOffset
* wavyOffsetFactor
), start
+ FloatPoint(width
, doubleOffset
* wavyOffsetFactor
), thickness
);
711 case TextDecorationStyleDotted
:
712 case TextDecorationStyleDashed
:
713 context
->setShouldAntialias(antialiasDecoration
);
716 context
->drawLineForText(FloatPoint(start
), width
, isPrinting
);
718 if (decoration
.style
== TextDecorationStyleDouble
)
719 context
->drawLineForText(start
+ FloatPoint(0, doubleOffset
), width
, isPrinting
);
723 void InlineTextBoxPainter::paintDecoration(const PaintInfo
& paintInfo
, const LayoutPoint
& boxOrigin
, TextDecoration deco
)
725 if (m_inlineTextBox
.truncation() == cFullTruncation
)
728 GraphicsContext
* context
= paintInfo
.context
;
729 GraphicsContextStateSaver
stateSaver(*context
);
731 LayoutPoint
localOrigin(boxOrigin
);
733 LayoutUnit width
= m_inlineTextBox
.logicalWidth();
734 if (m_inlineTextBox
.truncation() != cNoTruncation
) {
735 width
= m_inlineTextBox
.lineLayoutItem().width(m_inlineTextBox
.start(), m_inlineTextBox
.truncation(), m_inlineTextBox
.textPos(), m_inlineTextBox
.isLeftToRightDirection() ? LTR
: RTL
, m_inlineTextBox
.isFirstLineStyle());
736 if (!m_inlineTextBox
.isLeftToRightDirection())
737 localOrigin
.move(m_inlineTextBox
.logicalWidth() - width
, 0);
740 // Get the text decoration colors.
741 LayoutObject::AppliedTextDecoration underline
, overline
, linethrough
;
742 m_inlineTextBox
.layoutObject().getTextDecorations(deco
, underline
, overline
, linethrough
, true);
743 if (m_inlineTextBox
.isFirstLineStyle())
744 m_inlineTextBox
.layoutObject().getTextDecorations(deco
, underline
, overline
, linethrough
, true, true);
746 // Use a special function for underlines to get the positioning exactly right.
747 bool isPrinting
= paintInfo
.isPrinting();
749 const ComputedStyle
& styleToUse
= m_inlineTextBox
.lineLayoutItem().styleRef(m_inlineTextBox
.isFirstLineStyle());
750 float baseline
= styleToUse
.fontMetrics().ascent();
752 // Set the thick of the line to be 10% (or something else ?)of the computed font size and not less than 1px.
753 // Using computedFontSize should take care of zoom as well.
755 // Update Underline thickness, in case we have Faulty Font Metrics calculating underline thickness by old method.
756 float textDecorationThickness
= styleToUse
.fontMetrics().underlineThickness();
757 int fontHeightInt
= (int)(styleToUse
.fontMetrics().floatHeight() + 0.5);
758 if ((textDecorationThickness
== 0.f
) || (textDecorationThickness
>= (fontHeightInt
>> 1)))
759 textDecorationThickness
= std::max(1.f
, styleToUse
.computedFontSize() / 10.f
);
761 context
->setStrokeThickness(textDecorationThickness
);
763 bool antialiasDecoration
= shouldSetDecorationAntialias(overline
.style
, underline
.style
, linethrough
.style
);
765 // Offset between lines - always non-zero, so lines never cross each other.
766 float doubleOffset
= textDecorationThickness
+ 1.f
;
768 if (deco
& TextDecorationUnderline
) {
769 const int underlineOffset
= computeUnderlineOffset(styleToUse
.textUnderlinePosition(), styleToUse
.fontMetrics(), &m_inlineTextBox
, textDecorationThickness
);
770 paintAppliedDecoration(context
, FloatPoint(localOrigin
) + FloatPoint(0, underlineOffset
), width
.toFloat(), doubleOffset
, 1, underline
, textDecorationThickness
, antialiasDecoration
, isPrinting
);
772 if (deco
& TextDecorationOverline
) {
773 paintAppliedDecoration(context
, FloatPoint(localOrigin
), width
.toFloat(), -doubleOffset
, 1, overline
, textDecorationThickness
, antialiasDecoration
, isPrinting
);
775 if (deco
& TextDecorationLineThrough
) {
776 const float lineThroughOffset
= 2 * baseline
/ 3;
777 paintAppliedDecoration(context
, FloatPoint(localOrigin
) + FloatPoint(0, lineThroughOffset
), width
.toFloat(), doubleOffset
, 0, linethrough
, textDecorationThickness
, antialiasDecoration
, isPrinting
);
781 void InlineTextBoxPainter::paintCompositionUnderline(GraphicsContext
* ctx
, const LayoutPoint
& boxOrigin
, const CompositionUnderline
& underline
)
783 if (underline
.color
== Color::transparent
)
786 if (m_inlineTextBox
.truncation() == cFullTruncation
)
789 unsigned paintStart
= underlinePaintStart(underline
);
790 unsigned paintEnd
= underlinePaintEnd(underline
);
792 // start of line to draw, relative to paintOffset.
793 float start
= paintStart
== static_cast<unsigned>(m_inlineTextBox
.start()) ? 0 :
794 m_inlineTextBox
.lineLayoutItem().width(m_inlineTextBox
.start(), paintStart
- m_inlineTextBox
.start(), m_inlineTextBox
.textPos(), m_inlineTextBox
.isLeftToRightDirection() ? LTR
: RTL
, m_inlineTextBox
.isFirstLineStyle());
795 // how much line to draw
796 float width
= (paintStart
== static_cast<unsigned>(m_inlineTextBox
.start()) && paintEnd
== static_cast<unsigned>(m_inlineTextBox
.end()) + 1) ? m_inlineTextBox
.logicalWidth().toFloat() :
797 m_inlineTextBox
.lineLayoutItem().width(paintStart
, paintEnd
- paintStart
, m_inlineTextBox
.textPos() + start
, m_inlineTextBox
.isLeftToRightDirection() ? LTR
: RTL
, m_inlineTextBox
.isFirstLineStyle());
799 // Thick marked text underlines are 2px thick as long as there is room for the 2px line under the baseline.
800 // All other marked text underlines are 1px thick.
801 // If there's not enough space the underline will touch or overlap characters.
802 int lineThickness
= 1;
803 int baseline
= m_inlineTextBox
.lineLayoutItem().style(m_inlineTextBox
.isFirstLineStyle())->fontMetrics().ascent();
804 if (underline
.thick
&& m_inlineTextBox
.logicalHeight() - baseline
>= 2)
807 // We need to have some space between underlines of subsequent clauses, because some input methods do not use different underline styles for those.
808 // We make each line shorter, which has a harmless side effect of shortening the first and last clauses, too.
812 ctx
->setStrokeColor(underline
.color
);
813 ctx
->setStrokeThickness(lineThickness
);
814 ctx
->drawLineForText(FloatPoint(boxOrigin
.x() + start
, (boxOrigin
.y() + m_inlineTextBox
.logicalHeight() - lineThickness
).toFloat()), width
, m_inlineTextBox
.lineLayoutItem().document().printing());
817 void InlineTextBoxPainter::paintTextMatchMarker(GraphicsContext
* pt
, const LayoutPoint
& boxOrigin
, DocumentMarker
* marker
, const ComputedStyle
& style
, const Font
& font
)
819 // Use same y positioning and height as for selection, so that when the selection and this highlight are on
820 // the same word there are no pieces sticking out.
821 int deltaY
= m_inlineTextBox
.lineLayoutItem().style()->isFlippedLinesWritingMode() ? m_inlineTextBox
.root().selectionBottom() - m_inlineTextBox
.logicalBottom() : m_inlineTextBox
.logicalTop() - m_inlineTextBox
.root().selectionTop();
822 int selHeight
= m_inlineTextBox
.root().selectionHeight();
824 int sPos
= std::max(marker
->startOffset() - m_inlineTextBox
.start(), (unsigned)0);
825 int ePos
= std::min(marker
->endOffset() - m_inlineTextBox
.start(), m_inlineTextBox
.len());
826 TextRun run
= m_inlineTextBox
.constructTextRun(style
, font
);
828 // Optionally highlight the text
829 if (m_inlineTextBox
.layoutObject().frame()->editor().markedTextMatchesAreHighlighted()) {
830 Color color
= marker
->activeMatch() ?
831 LayoutTheme::theme().platformActiveTextSearchHighlightColor() :
832 LayoutTheme::theme().platformInactiveTextSearchHighlightColor();
833 GraphicsContextStateSaver
stateSaver(*pt
);
834 pt
->clip(FloatRect(boxOrigin
.x().toFloat(), (boxOrigin
.y() - deltaY
).toFloat(), m_inlineTextBox
.logicalWidth().toFloat(), selHeight
));
835 pt
->drawHighlightForText(font
, run
, FloatPoint(boxOrigin
.x().toFloat(), (boxOrigin
.y() - deltaY
).toFloat()), selHeight
, color
, sPos
, ePos
);
837 // Also Highlight the text with color:transparent
838 if (style
.visitedDependentColor(CSSPropertyColor
) == Color::transparent
) {
839 int length
= m_inlineTextBox
.len();
840 TextPainter::Style textStyle
;
841 // When we use the text as a clip, we only care about the alpha, thus we make all the colors black.
842 textStyle
.currentColor
= textStyle
.fillColor
= textStyle
.strokeColor
= textStyle
.emphasisMarkColor
= Color::black
;
843 textStyle
.strokeWidth
= style
.textStrokeWidth();
844 textStyle
.shadow
= 0;
846 LayoutRect
boxRect(boxOrigin
, LayoutSize(m_inlineTextBox
.logicalWidth(), m_inlineTextBox
.logicalHeight()));
847 LayoutPoint
textOrigin(boxOrigin
.x(), boxOrigin
.y() + font
.fontMetrics().ascent());
848 TextPainter
textPainter(pt
, font
, run
, textOrigin
, boxRect
, m_inlineTextBox
.isHorizontal());
850 textPainter
.paint(sPos
, ePos
, length
, textStyle
, 0);