1/*
2 * (C) 1999 Lars Knoll (knoll@kde.org)
3 * (C) 2000 Dirk Mueller (mueller@kde.org)
4 * Copyright (C) 2004-2017 Apple Inc. All rights reserved.
5 *
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Library General Public
8 * License as published by the Free Software Foundation; either
9 * version 2 of the License, or (at your option) any later version.
10 *
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Library General Public License for more details.
15 *
16 * You should have received a copy of the GNU Library General Public License
17 * along with this library; see the file COPYING.LIB. If not, write to
18 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19 * Boston, MA 02110-1301, USA.
20 *
21 */
22
23#include "config.h"
24#include "InlineTextBox.h"
25
26#include "BreakLines.h"
27#include "DashArray.h"
28#include "Document.h"
29#include "DocumentMarkerController.h"
30#include "Editor.h"
31#include "EllipsisBox.h"
32#include "EventRegion.h"
33#include "Frame.h"
34#include "GraphicsContext.h"
35#include "HitTestResult.h"
36#include "ImageBuffer.h"
37#include "InlineTextBoxStyle.h"
38#include "MarkedText.h"
39#include "Page.h"
40#include "PaintInfo.h"
41#include "RenderBlock.h"
42#include "RenderCombineText.h"
43#include "RenderLineBreak.h"
44#include "RenderRubyRun.h"
45#include "RenderRubyText.h"
46#include "RenderTheme.h"
47#include "RenderView.h"
48#include "RenderedDocumentMarker.h"
49#include "Text.h"
50#include "TextDecorationPainter.h"
51#include "TextPaintStyle.h"
52#include "TextPainter.h"
53#include <stdio.h>
54#include <wtf/IsoMallocInlines.h>
55#include <wtf/text/CString.h>
56#include <wtf/text/TextStream.h>
57
58namespace WebCore {
59
60WTF_MAKE_ISO_ALLOCATED_IMPL(InlineTextBox);
61
62struct SameSizeAsInlineTextBox : public InlineBox {
63 unsigned variables[1];
64 unsigned short variables2[2];
65 void* pointers[2];
66};
67
68COMPILE_ASSERT(sizeof(InlineTextBox) == sizeof(SameSizeAsInlineTextBox), InlineTextBox_should_stay_small);
69
70typedef WTF::HashMap<const InlineTextBox*, LayoutRect> InlineTextBoxOverflowMap;
71static InlineTextBoxOverflowMap* gTextBoxesWithOverflow;
72
73InlineTextBox::~InlineTextBox()
74{
75 if (!knownToHaveNoOverflow() && gTextBoxesWithOverflow)
76 gTextBoxesWithOverflow->remove(this);
77 TextPainter::removeGlyphDisplayList(*this);
78}
79
80bool InlineTextBox::hasTextContent() const
81{
82 if (m_len > 1)
83 return true;
84 if (auto* combinedText = this->combinedText()) {
85 ASSERT(m_len == 1);
86 return !combinedText->combinedStringForRendering().isEmpty();
87 }
88 return m_len;
89}
90
91void InlineTextBox::markDirty(bool dirty)
92{
93 if (dirty) {
94 m_len = 0;
95 m_start = 0;
96 }
97 InlineBox::markDirty(dirty);
98}
99
100LayoutRect InlineTextBox::logicalOverflowRect() const
101{
102 if (knownToHaveNoOverflow() || !gTextBoxesWithOverflow)
103 return enclosingIntRect(logicalFrameRect());
104 return gTextBoxesWithOverflow->get(this);
105}
106
107void InlineTextBox::setLogicalOverflowRect(const LayoutRect& rect)
108{
109 ASSERT(!knownToHaveNoOverflow());
110 if (!gTextBoxesWithOverflow)
111 gTextBoxesWithOverflow = new InlineTextBoxOverflowMap;
112 gTextBoxesWithOverflow->add(this, rect);
113}
114
115int InlineTextBox::baselinePosition(FontBaseline baselineType) const
116{
117 if (!parent())
118 return 0;
119 if (&parent()->renderer() == renderer().parent())
120 return parent()->baselinePosition(baselineType);
121 return downcast<RenderBoxModelObject>(*renderer().parent()).baselinePosition(baselineType, isFirstLine(), isHorizontal() ? HorizontalLine : VerticalLine, PositionOnContainingLine);
122}
123
124LayoutUnit InlineTextBox::lineHeight() const
125{
126 if (!renderer().parent())
127 return 0;
128 if (&parent()->renderer() == renderer().parent())
129 return parent()->lineHeight();
130 return downcast<RenderBoxModelObject>(*renderer().parent()).lineHeight(isFirstLine(), isHorizontal() ? HorizontalLine : VerticalLine, PositionOnContainingLine);
131}
132
133LayoutUnit InlineTextBox::selectionTop() const
134{
135 return root().selectionTop();
136}
137
138LayoutUnit InlineTextBox::selectionBottom() const
139{
140 return root().selectionBottom();
141}
142
143LayoutUnit InlineTextBox::selectionHeight() const
144{
145 return root().selectionHeight();
146}
147
148bool InlineTextBox::isSelected(unsigned startPosition, unsigned endPosition) const
149{
150 return clampedOffset(startPosition) < clampedOffset(endPosition);
151}
152
153RenderObject::SelectionState InlineTextBox::selectionState()
154{
155 RenderObject::SelectionState state = renderer().selectionState();
156 if (state == RenderObject::SelectionStart || state == RenderObject::SelectionEnd || state == RenderObject::SelectionBoth) {
157 auto& selection = renderer().view().selection();
158 auto startPos = selection.startPosition();
159 auto endPos = selection.endPosition();
160 // The position after a hard line break is considered to be past its end.
161 ASSERT(start() + len() >= (isLineBreak() ? 1 : 0));
162 unsigned lastSelectable = start() + len() - (isLineBreak() ? 1 : 0);
163
164 bool start = (state != RenderObject::SelectionEnd && startPos >= m_start && startPos < m_start + m_len);
165 bool end = (state != RenderObject::SelectionStart && endPos > m_start && endPos <= lastSelectable);
166 if (start && end)
167 state = RenderObject::SelectionBoth;
168 else if (start)
169 state = RenderObject::SelectionStart;
170 else if (end)
171 state = RenderObject::SelectionEnd;
172 else if ((state == RenderObject::SelectionEnd || startPos < m_start) &&
173 (state == RenderObject::SelectionStart || endPos > lastSelectable))
174 state = RenderObject::SelectionInside;
175 else if (state == RenderObject::SelectionBoth)
176 state = RenderObject::SelectionNone;
177 }
178
179 // If there are ellipsis following, make sure their selection is updated.
180 if (m_truncation != cNoTruncation && root().ellipsisBox()) {
181 EllipsisBox* ellipsis = root().ellipsisBox();
182 if (state != RenderObject::SelectionNone) {
183 unsigned selectionStart;
184 unsigned selectionEnd;
185 std::tie(selectionStart, selectionEnd) = selectionStartEnd();
186 // The ellipsis should be considered to be selected if the end of
187 // the selection is past the beginning of the truncation and the
188 // beginning of the selection is before or at the beginning of the
189 // truncation.
190 ellipsis->setSelectionState(selectionEnd >= m_truncation && selectionStart <= m_truncation ?
191 RenderObject::SelectionInside : RenderObject::SelectionNone);
192 } else
193 ellipsis->setSelectionState(RenderObject::SelectionNone);
194 }
195
196 return state;
197}
198
199inline const FontCascade& InlineTextBox::lineFont() const
200{
201 return combinedText() ? combinedText()->textCombineFont() : lineStyle().fontCascade();
202}
203
204// FIXME: Share more code with paintMarkedTextBackground().
205LayoutRect InlineTextBox::localSelectionRect(unsigned startPos, unsigned endPos) const
206{
207 unsigned sPos = clampedOffset(startPos);
208 unsigned ePos = clampedOffset(endPos);
209
210 if (sPos >= ePos && !(startPos == endPos && startPos >= start() && startPos <= (start() + len())))
211 return { };
212
213 LayoutUnit selectionTop = this->selectionTop();
214 LayoutUnit selectionHeight = this->selectionHeight();
215
216 TextRun textRun = createTextRun();
217
218 LayoutRect selectionRect = LayoutRect(LayoutPoint(logicalLeft(), selectionTop), LayoutSize(logicalWidth(), selectionHeight));
219 // Avoid measuring the text when the entire line box is selected as an optimization.
220 if (sPos || ePos != textRun.length())
221 lineFont().adjustSelectionRectForText(textRun, selectionRect, sPos, ePos);
222 // FIXME: The computation of the snapped selection rect differs from the computation of this rect
223 // in paintMarkedTextBackground(). See <https://bugs.webkit.org/show_bug.cgi?id=138913>.
224 IntRect snappedSelectionRect = enclosingIntRect(selectionRect);
225 LayoutUnit logicalWidth = snappedSelectionRect.width();
226 if (snappedSelectionRect.x() > logicalRight())
227 logicalWidth = 0;
228 else if (snappedSelectionRect.maxX() > logicalRight())
229 logicalWidth = logicalRight() - snappedSelectionRect.x();
230
231 LayoutPoint topPoint = isHorizontal() ? LayoutPoint(snappedSelectionRect.x(), selectionTop) : LayoutPoint(selectionTop, snappedSelectionRect.x());
232 LayoutUnit width = isHorizontal() ? logicalWidth : selectionHeight;
233 LayoutUnit height = isHorizontal() ? selectionHeight : logicalWidth;
234
235 return LayoutRect(topPoint, LayoutSize(width, height));
236}
237
238void InlineTextBox::deleteLine()
239{
240 renderer().removeTextBox(*this);
241 delete this;
242}
243
244void InlineTextBox::extractLine()
245{
246 if (extracted())
247 return;
248
249 renderer().extractTextBox(*this);
250}
251
252void InlineTextBox::attachLine()
253{
254 if (!extracted())
255 return;
256
257 renderer().attachTextBox(*this);
258}
259
260float InlineTextBox::placeEllipsisBox(bool flowIsLTR, float visibleLeftEdge, float visibleRightEdge, float ellipsisWidth, float &truncatedWidth, bool& foundBox)
261{
262 if (foundBox) {
263 m_truncation = cFullTruncation;
264 return -1;
265 }
266
267 // For LTR this is the left edge of the box, for RTL, the right edge in parent coordinates.
268 float ellipsisX = flowIsLTR ? visibleRightEdge - ellipsisWidth : visibleLeftEdge + ellipsisWidth;
269
270 // Criteria for full truncation:
271 // LTR: the left edge of the ellipsis is to the left of our text run.
272 // RTL: the right edge of the ellipsis is to the right of our text run.
273 bool ltrFullTruncation = flowIsLTR && ellipsisX <= left();
274 bool rtlFullTruncation = !flowIsLTR && ellipsisX >= left() + logicalWidth();
275 if (ltrFullTruncation || rtlFullTruncation) {
276 // Too far. Just set full truncation, but return -1 and let the ellipsis just be placed at the edge of the box.
277 m_truncation = cFullTruncation;
278 foundBox = true;
279 return -1;
280 }
281
282 bool ltrEllipsisWithinBox = flowIsLTR && (ellipsisX < right());
283 bool rtlEllipsisWithinBox = !flowIsLTR && (ellipsisX > left());
284 if (ltrEllipsisWithinBox || rtlEllipsisWithinBox) {
285 foundBox = true;
286
287 // The inline box may have different directionality than it's parent. Since truncation
288 // behavior depends both on both the parent and the inline block's directionality, we
289 // must keep track of these separately.
290 bool ltr = isLeftToRightDirection();
291 if (ltr != flowIsLTR) {
292 // Width in pixels of the visible portion of the box, excluding the ellipsis.
293 int visibleBoxWidth = visibleRightEdge - visibleLeftEdge - ellipsisWidth;
294 ellipsisX = ltr ? left() + visibleBoxWidth : right() - visibleBoxWidth;
295 }
296
297 int offset = offsetForPosition(ellipsisX, false);
298 if (offset == 0) {
299 // No characters should be rendered. Set ourselves to full truncation and place the ellipsis at the min of our start
300 // and the ellipsis edge.
301 m_truncation = cFullTruncation;
302 truncatedWidth += ellipsisWidth;
303 return flowIsLTR ? std::min(ellipsisX, x()) : std::max(ellipsisX, right() - ellipsisWidth);
304 }
305
306 // Set the truncation index on the text run.
307 m_truncation = offset;
308
309 // If we got here that means that we were only partially truncated and we need to return the pixel offset at which
310 // to place the ellipsis.
311 float widthOfVisibleText = renderer().width(m_start, offset, textPos(), isFirstLine());
312
313 // The ellipsis needs to be placed just after the last visible character.
314 // Where "after" is defined by the flow directionality, not the inline
315 // box directionality.
316 // e.g. In the case of an LTR inline box truncated in an RTL flow then we can
317 // have a situation such as |Hello| -> |...He|
318 truncatedWidth += widthOfVisibleText + ellipsisWidth;
319 if (flowIsLTR)
320 return left() + widthOfVisibleText;
321 else
322 return right() - widthOfVisibleText - ellipsisWidth;
323 }
324 truncatedWidth += logicalWidth();
325 return -1;
326}
327
328
329
330bool InlineTextBox::isLineBreak() const
331{
332 return renderer().style().preserveNewline() && len() == 1 && renderer().text()[start()] == '\n';
333}
334
335bool InlineTextBox::nodeAtPoint(const HitTestRequest& request, HitTestResult& result, const HitTestLocation& locationInContainer, const LayoutPoint& accumulatedOffset, LayoutUnit /* lineTop */, LayoutUnit /*lineBottom*/,
336 HitTestAction /*hitTestAction*/)
337{
338 if (!visibleToHitTesting())
339 return false;
340
341 if (isLineBreak())
342 return false;
343
344 if (m_truncation == cFullTruncation)
345 return false;
346
347 FloatRect rect(locationIncludingFlipping(), size());
348 // Make sure truncated text is ignored while hittesting.
349 if (m_truncation != cNoTruncation) {
350 LayoutUnit widthOfVisibleText = renderer().width(m_start, m_truncation, textPos(), isFirstLine());
351
352 if (isHorizontal())
353 renderer().style().isLeftToRightDirection() ? rect.setWidth(widthOfVisibleText) : rect.shiftXEdgeTo(right() - widthOfVisibleText);
354 else
355 rect.setHeight(widthOfVisibleText);
356 }
357
358 rect.moveBy(accumulatedOffset);
359
360 if (locationInContainer.intersects(rect)) {
361 renderer().updateHitTestResult(result, flipForWritingMode(locationInContainer.point() - toLayoutSize(accumulatedOffset)));
362 if (result.addNodeToListBasedTestResult(renderer().textNode(), request, locationInContainer, rect) == HitTestProgress::Stop)
363 return true;
364 }
365 return false;
366}
367
368Optional<bool> InlineTextBox::emphasisMarkExistsAndIsAbove(const RenderStyle& style) const
369{
370 // This function returns true if there are text emphasis marks and they are suppressed by ruby text.
371 if (style.textEmphasisMark() == TextEmphasisMark::None)
372 return WTF::nullopt;
373
374 const OptionSet<TextEmphasisPosition> horizontalMask { TextEmphasisPosition::Left, TextEmphasisPosition::Right };
375
376 auto emphasisPosition = style.textEmphasisPosition();
377 auto emphasisPositionHorizontalValue = emphasisPosition & horizontalMask;
378 ASSERT(!((emphasisPosition & TextEmphasisPosition::Over) && (emphasisPosition & TextEmphasisPosition::Under)));
379 ASSERT(emphasisPositionHorizontalValue != horizontalMask);
380
381 bool isAbove = false;
382 if (!emphasisPositionHorizontalValue)
383 isAbove = emphasisPosition.contains(TextEmphasisPosition::Over);
384 else if (style.isHorizontalWritingMode())
385 isAbove = emphasisPosition.contains(TextEmphasisPosition::Over);
386 else
387 isAbove = emphasisPositionHorizontalValue == TextEmphasisPosition::Right;
388
389 if ((style.isHorizontalWritingMode() && (emphasisPosition & TextEmphasisPosition::Under))
390 || (!style.isHorizontalWritingMode() && (emphasisPosition & TextEmphasisPosition::Left)))
391 return isAbove; // Ruby text is always over, so it cannot suppress emphasis marks under.
392
393 RenderBlock* containingBlock = renderer().containingBlock();
394 if (!containingBlock->isRubyBase())
395 return isAbove; // This text is not inside a ruby base, so it does not have ruby text over it.
396
397 if (!is<RenderRubyRun>(*containingBlock->parent()))
398 return isAbove; // Cannot get the ruby text.
399
400 RenderRubyText* rubyText = downcast<RenderRubyRun>(*containingBlock->parent()).rubyText();
401
402 // The emphasis marks over are suppressed only if there is a ruby text box and it not empty.
403 if (rubyText && rubyText->hasLines())
404 return WTF::nullopt;
405
406 return isAbove;
407}
408
409struct InlineTextBox::MarkedTextStyle {
410 static bool areBackgroundMarkedTextStylesEqual(const MarkedTextStyle& a, const MarkedTextStyle& b)
411 {
412 return a.backgroundColor == b.backgroundColor;
413 }
414 static bool areForegroundMarkedTextStylesEqual(const MarkedTextStyle& a, const MarkedTextStyle& b)
415 {
416 return a.textStyles == b.textStyles && a.textShadow == b.textShadow && a.alpha == b.alpha;
417 }
418 static bool areDecorationMarkedTextStylesEqual(const MarkedTextStyle& a, const MarkedTextStyle& b)
419 {
420 return a.textDecorationStyles == b.textDecorationStyles && a.textShadow == b.textShadow && a.alpha == b.alpha;
421 }
422
423 Color backgroundColor;
424 TextPaintStyle textStyles;
425 TextDecorationPainter::Styles textDecorationStyles;
426 Optional<ShadowData> textShadow;
427 float alpha;
428};
429
430struct InlineTextBox::StyledMarkedText : MarkedText {
431 StyledMarkedText(const MarkedText& marker)
432 : MarkedText { marker }
433 {
434 }
435
436 MarkedTextStyle style;
437};
438
439static MarkedText createMarkedTextFromSelectionInBox(const InlineTextBox& box)
440{
441 unsigned selectionStart;
442 unsigned selectionEnd;
443 std::tie(selectionStart, selectionEnd) = box.selectionStartEnd();
444 if (selectionStart < selectionEnd)
445 return { selectionStart, selectionEnd, MarkedText::Selection };
446 return { };
447}
448
449void InlineTextBox::paint(PaintInfo& paintInfo, const LayoutPoint& paintOffset, LayoutUnit /*lineTop*/, LayoutUnit /*lineBottom*/)
450{
451 if (isLineBreak() || !paintInfo.shouldPaintWithinRoot(renderer()) || renderer().style().visibility() != Visibility::Visible
452 || m_truncation == cFullTruncation || paintInfo.phase == PaintPhase::Outline || !hasTextContent())
453 return;
454
455 ASSERT(paintInfo.phase != PaintPhase::SelfOutline && paintInfo.phase != PaintPhase::ChildOutlines);
456
457 LayoutUnit logicalLeftSide = logicalLeftVisualOverflow();
458 LayoutUnit logicalRightSide = logicalRightVisualOverflow();
459 LayoutUnit logicalStart = logicalLeftSide + (isHorizontal() ? paintOffset.x() : paintOffset.y());
460 LayoutUnit logicalExtent = logicalRightSide - logicalLeftSide;
461
462 LayoutUnit paintEnd = isHorizontal() ? paintInfo.rect.maxX() : paintInfo.rect.maxY();
463 LayoutUnit paintStart = isHorizontal() ? paintInfo.rect.x() : paintInfo.rect.y();
464
465 FloatPoint localPaintOffset(paintOffset);
466
467 if (logicalStart >= paintEnd || logicalStart + logicalExtent <= paintStart)
468 return;
469
470 bool isPrinting = renderer().document().printing();
471
472 // Determine whether or not we're selected.
473 bool haveSelection = !isPrinting && paintInfo.phase != PaintPhase::TextClip && selectionState() != RenderObject::SelectionNone;
474 if (!haveSelection && paintInfo.phase == PaintPhase::Selection) {
475 // When only painting the selection, don't bother to paint if there is none.
476 return;
477 }
478
479 if (m_truncation != cNoTruncation) {
480 if (renderer().containingBlock()->style().isLeftToRightDirection() != isLeftToRightDirection()) {
481 // Make the visible fragment of text hug the edge closest to the rest of the run by moving the origin
482 // at which we start drawing text.
483 // e.g. In the case of LTR text truncated in an RTL Context, the correct behavior is:
484 // |Hello|CBA| -> |...He|CBA|
485 // In order to draw the fragment "He" aligned to the right edge of it's box, we need to start drawing
486 // farther to the right.
487 // NOTE: WebKit's behavior differs from that of IE which appears to just overlay the ellipsis on top of the
488 // truncated string i.e. |Hello|CBA| -> |...lo|CBA|
489 LayoutUnit widthOfVisibleText = renderer().width(m_start, m_truncation, textPos(), isFirstLine());
490 LayoutUnit widthOfHiddenText = logicalWidth() - widthOfVisibleText;
491 LayoutSize truncationOffset(isLeftToRightDirection() ? widthOfHiddenText : -widthOfHiddenText, 0_lu);
492 localPaintOffset.move(isHorizontal() ? truncationOffset : truncationOffset.transposedSize());
493 }
494 }
495
496 GraphicsContext& context = paintInfo.context();
497
498 const RenderStyle& lineStyle = this->lineStyle();
499
500 localPaintOffset.move(0, lineStyle.isHorizontalWritingMode() ? 0 : -logicalHeight());
501
502 FloatPoint boxOrigin = locationIncludingFlipping();
503 boxOrigin.moveBy(localPaintOffset);
504 FloatRect boxRect(boxOrigin, FloatSize(logicalWidth(), logicalHeight()));
505
506 if (paintInfo.phase == PaintPhase::EventRegion) {
507 if (visibleToHitTesting())
508 paintInfo.eventRegionContext->unite(enclosingIntRect(boxRect), renderer().style());
509 return;
510 }
511
512 auto* combinedText = this->combinedText();
513
514 bool shouldRotate = !isHorizontal() && !combinedText;
515 if (shouldRotate)
516 context.concatCTM(rotation(boxRect, Clockwise));
517
518 // Determine whether or not we have composition underlines to draw.
519 bool containsComposition = renderer().textNode() && renderer().frame().editor().compositionNode() == renderer().textNode();
520 bool useCustomUnderlines = containsComposition && renderer().frame().editor().compositionUsesCustomUnderlines();
521
522 MarkedTextStyle unmarkedStyle = computeStyleForUnmarkedMarkedText(paintInfo);
523
524 // 1. Paint backgrounds behind text if needed. Examples of such backgrounds include selection
525 // and composition underlines.
526 if (paintInfo.phase != PaintPhase::Selection && paintInfo.phase != PaintPhase::TextClip && !isPrinting) {
527 if (containsComposition && !useCustomUnderlines)
528 paintCompositionBackground(paintInfo, boxOrigin);
529
530 Vector<MarkedText> markedTexts = collectMarkedTextsForDocumentMarkers(TextPaintPhase::Background);
531#if ENABLE(TEXT_SELECTION)
532 if (haveSelection && !useCustomUnderlines && !context.paintingDisabled()) {
533 auto selectionMarkedText = createMarkedTextFromSelectionInBox(*this);
534 if (!selectionMarkedText.isEmpty())
535 markedTexts.append(WTFMove(selectionMarkedText));
536 }
537#endif
538 auto styledMarkedTexts = subdivideAndResolveStyle(markedTexts, unmarkedStyle, paintInfo);
539
540 // Coalesce styles of adjacent marked texts to minimize the number of drawing commands.
541 auto coalescedStyledMarkedTexts = coalesceAdjacentMarkedTexts(styledMarkedTexts, &MarkedTextStyle::areBackgroundMarkedTextStylesEqual);
542
543 paintMarkedTexts(paintInfo, TextPaintPhase::Background, boxRect, coalescedStyledMarkedTexts);
544 }
545
546 // FIXME: Right now, InlineTextBoxes never call addRelevantUnpaintedObject() even though they might
547 // legitimately be unpainted if they are waiting on a slow-loading web font. We should fix that, and
548 // when we do, we will have to account for the fact the InlineTextBoxes do not always have unique
549 // renderers and Page currently relies on each unpainted object having a unique renderer.
550 if (paintInfo.phase == PaintPhase::Foreground)
551 renderer().page().addRelevantRepaintedObject(&renderer(), IntRect(boxOrigin.x(), boxOrigin.y(), logicalWidth(), logicalHeight()));
552
553 if (paintInfo.phase == PaintPhase::Foreground)
554 paintPlatformDocumentMarkers(context, boxOrigin);
555
556 // 2. Now paint the foreground, including text and decorations like underline/overline (in quirks mode only).
557 bool shouldPaintSelectionForeground = haveSelection && !useCustomUnderlines;
558 Vector<MarkedText> markedTexts;
559 if (paintInfo.phase != PaintPhase::Selection) {
560 // The marked texts for the gaps between document markers and selection are implicitly created by subdividing the entire line.
561 markedTexts.append({ clampedOffset(m_start), clampedOffset(end() + 1), MarkedText::Unmarked });
562 if (!isPrinting) {
563 markedTexts.appendVector(collectMarkedTextsForDocumentMarkers(TextPaintPhase::Foreground));
564
565 bool shouldPaintDraggedContent = !(paintInfo.paintBehavior.contains(PaintBehavior::ExcludeSelection));
566 if (shouldPaintDraggedContent) {
567 auto markedTextsForDraggedContent = collectMarkedTextsForDraggedContent();
568 if (!markedTextsForDraggedContent.isEmpty()) {
569 shouldPaintSelectionForeground = false;
570 markedTexts.appendVector(markedTextsForDraggedContent);
571 }
572 }
573 }
574 }
575 // The selection marked text acts as a placeholder when computing the marked texts for the gaps...
576 if (shouldPaintSelectionForeground) {
577 ASSERT(!isPrinting);
578 auto selectionMarkedText = createMarkedTextFromSelectionInBox(*this);
579 if (!selectionMarkedText.isEmpty())
580 markedTexts.append(WTFMove(selectionMarkedText));
581 }
582
583 auto styledMarkedTexts = subdivideAndResolveStyle(markedTexts, unmarkedStyle, paintInfo);
584
585 // ... now remove the selection marked text if we are excluding selection.
586 if (!isPrinting && paintInfo.paintBehavior.contains(PaintBehavior::ExcludeSelection))
587 styledMarkedTexts.removeAllMatching([] (const StyledMarkedText& markedText) { return markedText.type == MarkedText::Selection; });
588
589 // Coalesce styles of adjacent marked texts to minimize the number of drawing commands.
590 auto coalescedStyledMarkedTexts = coalesceAdjacentMarkedTexts(styledMarkedTexts, &MarkedTextStyle::areForegroundMarkedTextStylesEqual);
591
592 paintMarkedTexts(paintInfo, TextPaintPhase::Foreground, boxRect, coalescedStyledMarkedTexts);
593
594 // Paint decorations
595 auto textDecorations = lineStyle.textDecorationsInEffect();
596 if (!textDecorations.isEmpty() && paintInfo.phase != PaintPhase::Selection) {
597 TextRun textRun = createTextRun();
598 unsigned length = textRun.length();
599 if (m_truncation != cNoTruncation)
600 length = m_truncation;
601 unsigned selectionStart = 0;
602 unsigned selectionEnd = 0;
603 if (haveSelection)
604 std::tie(selectionStart, selectionEnd) = selectionStartEnd();
605
606 FloatRect textDecorationSelectionClipOutRect;
607 if ((paintInfo.paintBehavior.contains(PaintBehavior::ExcludeSelection)) && selectionStart < selectionEnd && selectionEnd <= length) {
608 textDecorationSelectionClipOutRect = logicalOverflowRect();
609 textDecorationSelectionClipOutRect.moveBy(localPaintOffset);
610 float logicalWidthBeforeRange;
611 float logicalWidthAfterRange;
612 float logicalSelectionWidth = lineFont().widthOfTextRange(textRun, selectionStart, selectionEnd, nullptr, &logicalWidthBeforeRange, &logicalWidthAfterRange);
613 // FIXME: Do we need to handle vertical bottom to top text?
614 if (!isHorizontal()) {
615 textDecorationSelectionClipOutRect.move(0, logicalWidthBeforeRange);
616 textDecorationSelectionClipOutRect.setHeight(logicalSelectionWidth);
617 } else if (direction() == TextDirection::RTL) {
618 textDecorationSelectionClipOutRect.move(logicalWidthAfterRange, 0);
619 textDecorationSelectionClipOutRect.setWidth(logicalSelectionWidth);
620 } else {
621 textDecorationSelectionClipOutRect.move(logicalWidthBeforeRange, 0);
622 textDecorationSelectionClipOutRect.setWidth(logicalSelectionWidth);
623 }
624 }
625
626 // Coalesce styles of adjacent marked texts to minimize the number of drawing commands.
627 auto coalescedStyledMarkedTexts = coalesceAdjacentMarkedTexts(styledMarkedTexts, &MarkedTextStyle::areDecorationMarkedTextStylesEqual);
628
629 paintMarkedTexts(paintInfo, TextPaintPhase::Decoration, boxRect, coalescedStyledMarkedTexts, textDecorationSelectionClipOutRect);
630 }
631
632 // 3. Paint fancy decorations, including composition underlines and platform-specific underlines for spelling errors, grammar errors, et cetera.
633 if (paintInfo.phase == PaintPhase::Foreground && useCustomUnderlines)
634 paintCompositionUnderlines(paintInfo, boxOrigin);
635
636 if (shouldRotate)
637 context.concatCTM(rotation(boxRect, Counterclockwise));
638}
639
640unsigned InlineTextBox::clampedOffset(unsigned x) const
641{
642 unsigned offset = std::max(std::min(x, m_start + m_len), m_start) - m_start;
643 if (m_truncation == cFullTruncation)
644 return offset;
645 if (m_truncation != cNoTruncation)
646 offset = std::min<unsigned>(offset, m_truncation);
647 else if (offset == m_len) {
648 // Fix up the offset if we are combined text or have a hyphen because we manage these embellishments.
649 // That is, they are not reflected in renderer().text(). We treat combined text as a single unit.
650 // We also treat the last codepoint in this box and the hyphen as a single unit.
651 if (auto* combinedText = this->combinedText())
652 offset = combinedText->combinedStringForRendering().length();
653 else if (hasHyphen())
654 offset += lineStyle().hyphenString().length();
655 }
656 return offset;
657}
658
659std::pair<unsigned, unsigned> InlineTextBox::selectionStartEnd() const
660{
661 auto selectionState = renderer().selectionState();
662 if (selectionState == RenderObject::SelectionInside)
663 return { 0, clampedOffset(m_start + m_len) };
664
665 auto start = renderer().view().selection().startPosition();
666 auto end = renderer().view().selection().endPosition();
667 if (selectionState == RenderObject::SelectionStart)
668 end = renderer().text().length();
669 else if (selectionState == RenderObject::SelectionEnd)
670 start = 0;
671 return { clampedOffset(start), clampedOffset(end) };
672}
673
674bool InlineTextBox::hasMarkers() const
675{
676 return collectMarkedTextsForDocumentMarkers(TextPaintPhase::Decoration).size();
677}
678
679void InlineTextBox::paintPlatformDocumentMarkers(GraphicsContext& context, const FloatPoint& boxOrigin)
680{
681 // This must match calculateUnionOfAllDocumentMarkerBounds().
682 for (auto& markedText : subdivide(collectMarkedTextsForDocumentMarkers(TextPaintPhase::Decoration), OverlapStrategy::Frontmost))
683 paintPlatformDocumentMarker(context, boxOrigin, markedText);
684}
685
686FloatRect InlineTextBox::calculateUnionOfAllDocumentMarkerBounds() const
687{
688 // This must match paintPlatformDocumentMarkers().
689 FloatRect result;
690 for (auto& markedText : subdivide(collectMarkedTextsForDocumentMarkers(TextPaintPhase::Decoration), OverlapStrategy::Frontmost))
691 result = unionRect(result, calculateDocumentMarkerBounds(markedText));
692 return result;
693}
694
695FloatRect InlineTextBox::calculateDocumentMarkerBounds(const MarkedText& markedText) const
696{
697 auto& font = lineFont();
698 auto ascent = font.fontMetrics().ascent();
699 auto fontSize = std::min(std::max(font.size(), 10.0f), 40.0f);
700 auto y = ascent + 0.11035 * fontSize;
701 auto height = 0.13247 * fontSize;
702
703 // Avoid measuring the text when the entire line box is selected as an optimization.
704 if (markedText.startOffset || markedText.endOffset != clampedOffset(end() + 1)) {
705 TextRun run = createTextRun();
706 LayoutRect selectionRect = LayoutRect(0, y, 0, height);
707 lineFont().adjustSelectionRectForText(run, selectionRect, markedText.startOffset, markedText.endOffset);
708 return selectionRect;
709 }
710
711 return FloatRect(0, y, logicalWidth(), height);
712}
713
714void InlineTextBox::paintPlatformDocumentMarker(GraphicsContext& context, const FloatPoint& boxOrigin, const MarkedText& markedText)
715{
716 // Never print spelling/grammar markers (5327887)
717 if (renderer().document().printing())
718 return;
719
720 if (m_truncation == cFullTruncation)
721 return;
722
723 auto bounds = calculateDocumentMarkerBounds(markedText);
724
725 auto lineStyleForMarkedTextType = [&]() -> DocumentMarkerLineStyle {
726 bool shouldUseDarkAppearance = renderer().useDarkAppearance();
727 switch (markedText.type) {
728 case MarkedText::SpellingError:
729 return { DocumentMarkerLineStyle::Mode::Spelling, shouldUseDarkAppearance };
730 case MarkedText::GrammarError:
731 return { DocumentMarkerLineStyle::Mode::Grammar, shouldUseDarkAppearance };
732 case MarkedText::Correction:
733 return { DocumentMarkerLineStyle::Mode::AutocorrectionReplacement, shouldUseDarkAppearance };
734 case MarkedText::DictationAlternatives:
735 return { DocumentMarkerLineStyle::Mode::DictationAlternatives, shouldUseDarkAppearance };
736#if PLATFORM(IOS_FAMILY)
737 case MarkedText::DictationPhraseWithAlternatives:
738 // FIXME: Rename DocumentMarkerLineStyle::TextCheckingDictationPhraseWithAlternatives and remove the PLATFORM(IOS_FAMILY)-guard.
739 return { DocumentMarkerLineStyle::Mode::TextCheckingDictationPhraseWithAlternatives, shouldUseDarkAppearance };
740#endif
741 default:
742 ASSERT_NOT_REACHED();
743 return { DocumentMarkerLineStyle::Mode::Spelling, shouldUseDarkAppearance };
744 }
745 };
746
747 bounds.moveBy(boxOrigin);
748 context.drawDotsForDocumentMarker(bounds, lineStyleForMarkedTextType());
749}
750
751auto InlineTextBox::computeStyleForUnmarkedMarkedText(const PaintInfo& paintInfo) const -> MarkedTextStyle
752{
753 auto& lineStyle = this->lineStyle();
754
755 MarkedTextStyle style;
756 style.textDecorationStyles = TextDecorationPainter::stylesForRenderer(renderer(), lineStyle.textDecorationsInEffect(), isFirstLine());
757 style.textStyles = computeTextPaintStyle(renderer().frame(), lineStyle, paintInfo);
758 style.textShadow = ShadowData::clone(paintInfo.forceTextColor() ? nullptr : lineStyle.textShadow());
759 style.alpha = 1;
760 return style;
761}
762
763auto InlineTextBox::resolveStyleForMarkedText(const MarkedText& markedText, const MarkedTextStyle& baseStyle, const PaintInfo& paintInfo) -> StyledMarkedText
764{
765 MarkedTextStyle style = baseStyle;
766 switch (markedText.type) {
767 case MarkedText::Correction:
768 case MarkedText::DictationAlternatives:
769#if PLATFORM(IOS_FAMILY)
770 // FIXME: See <rdar://problem/8933352>. Also, remove the PLATFORM(IOS_FAMILY)-guard.
771 case MarkedText::DictationPhraseWithAlternatives:
772#endif
773 case MarkedText::GrammarError:
774 case MarkedText::SpellingError:
775 case MarkedText::Unmarked:
776 break;
777 case MarkedText::DraggedContent:
778 style.alpha = 0.25;
779 break;
780 case MarkedText::Selection: {
781 style.textStyles = computeTextSelectionPaintStyle(style.textStyles, renderer(), lineStyle(), paintInfo, style.textShadow);
782
783 Color selectionBackgroundColor = renderer().selectionBackgroundColor();
784 style.backgroundColor = selectionBackgroundColor;
785 if (selectionBackgroundColor.isValid() && selectionBackgroundColor.alpha() && style.textStyles.fillColor == selectionBackgroundColor)
786 style.backgroundColor = { 0xff - selectionBackgroundColor.red(), 0xff - selectionBackgroundColor.green(), 0xff - selectionBackgroundColor.blue() };
787 break;
788 }
789 case MarkedText::TextMatch: {
790 // Text matches always use the light system appearance.
791 OptionSet<StyleColor::Options> styleColorOptions = { StyleColor::Options::UseSystemAppearance };
792#if PLATFORM(MAC)
793 style.textStyles.fillColor = renderer().theme().systemColor(CSSValueAppleSystemLabel, styleColorOptions);
794#endif
795 style.backgroundColor = markedText.marker->isActiveMatch() ? renderer().theme().activeTextSearchHighlightColor(styleColorOptions) : renderer().theme().inactiveTextSearchHighlightColor(styleColorOptions);
796 break;
797 }
798 }
799 StyledMarkedText styledMarkedText = markedText;
800 styledMarkedText.style = WTFMove(style);
801 return styledMarkedText;
802}
803
804auto InlineTextBox::subdivideAndResolveStyle(const Vector<MarkedText>& textsToSubdivide, const MarkedTextStyle& baseStyle, const PaintInfo& paintInfo) -> Vector<StyledMarkedText>
805{
806 if (textsToSubdivide.isEmpty())
807 return { };
808
809 auto markedTexts = subdivide(textsToSubdivide);
810 ASSERT(!markedTexts.isEmpty());
811 if (UNLIKELY(markedTexts.isEmpty()))
812 return { };
813
814 // Compute frontmost overlapping styled marked texts.
815 Vector<StyledMarkedText> frontmostMarkedTexts;
816 frontmostMarkedTexts.reserveInitialCapacity(markedTexts.size());
817 frontmostMarkedTexts.uncheckedAppend(resolveStyleForMarkedText(markedTexts[0], baseStyle, paintInfo));
818 for (auto it = markedTexts.begin() + 1, end = markedTexts.end(); it != end; ++it) {
819 StyledMarkedText& previousStyledMarkedText = frontmostMarkedTexts.last();
820 if (previousStyledMarkedText.startOffset == it->startOffset && previousStyledMarkedText.endOffset == it->endOffset) {
821 // Marked texts completely cover each other.
822 previousStyledMarkedText = resolveStyleForMarkedText(*it, previousStyledMarkedText.style, paintInfo);
823 continue;
824 }
825 frontmostMarkedTexts.uncheckedAppend(resolveStyleForMarkedText(*it, baseStyle, paintInfo));
826 }
827
828 return frontmostMarkedTexts;
829}
830
831auto InlineTextBox::coalesceAdjacentMarkedTexts(const Vector<StyledMarkedText>& textsToCoalesce, MarkedTextStylesEqualityFunction areMarkedTextStylesEqual) -> Vector<StyledMarkedText>
832{
833 if (textsToCoalesce.isEmpty())
834 return { };
835
836 auto areAdjacentMarkedTextsWithSameStyle = [&] (const StyledMarkedText& a, const StyledMarkedText& b) {
837 return a.endOffset == b.startOffset && areMarkedTextStylesEqual(a.style, b.style);
838 };
839
840 Vector<StyledMarkedText> styledMarkedTexts;
841 styledMarkedTexts.reserveInitialCapacity(textsToCoalesce.size());
842 styledMarkedTexts.uncheckedAppend(textsToCoalesce[0]);
843 for (auto it = textsToCoalesce.begin() + 1, end = textsToCoalesce.end(); it != end; ++it) {
844 StyledMarkedText& previousStyledMarkedText = styledMarkedTexts.last();
845 if (areAdjacentMarkedTextsWithSameStyle(previousStyledMarkedText, *it)) {
846 previousStyledMarkedText.endOffset = it->endOffset;
847 continue;
848 }
849 styledMarkedTexts.uncheckedAppend(*it);
850 }
851
852 return styledMarkedTexts;
853}
854
855Vector<MarkedText> InlineTextBox::collectMarkedTextsForDraggedContent()
856{
857 using DraggendContentRange = std::pair<unsigned, unsigned>;
858 auto draggedContentRanges = renderer().draggedContentRangesBetweenOffsets(m_start, m_start + m_len);
859 Vector<MarkedText> result = draggedContentRanges.map([this] (const DraggendContentRange& range) -> MarkedText {
860 return { clampedOffset(range.first), clampedOffset(range.second), MarkedText::DraggedContent };
861 });
862 return result;
863}
864
865Vector<MarkedText> InlineTextBox::collectMarkedTextsForDocumentMarkers(TextPaintPhase phase) const
866{
867 ASSERT_ARG(phase, phase == TextPaintPhase::Background || phase == TextPaintPhase::Foreground || phase == TextPaintPhase::Decoration);
868
869 if (!renderer().textNode())
870 return { };
871
872 Vector<RenderedDocumentMarker*> markers = renderer().document().markers().markersFor(*renderer().textNode());
873
874 auto markedTextTypeForMarkerType = [] (DocumentMarker::MarkerType type) {
875 switch (type) {
876 case DocumentMarker::Spelling:
877 return MarkedText::SpellingError;
878 case DocumentMarker::Grammar:
879 return MarkedText::GrammarError;
880 case DocumentMarker::CorrectionIndicator:
881 return MarkedText::Correction;
882 case DocumentMarker::TextMatch:
883 return MarkedText::TextMatch;
884 case DocumentMarker::DictationAlternatives:
885 return MarkedText::DictationAlternatives;
886#if PLATFORM(IOS_FAMILY)
887 case DocumentMarker::DictationPhraseWithAlternatives:
888 return MarkedText::DictationPhraseWithAlternatives;
889#endif
890 default:
891 return MarkedText::Unmarked;
892 }
893 };
894
895 Vector<MarkedText> markedTexts;
896 markedTexts.reserveInitialCapacity(markers.size());
897
898 // Give any document markers that touch this run a chance to draw before the text has been drawn.
899 // Note end() points at the last char, not one past it like endOffset and ranges do.
900 for (auto* marker : markers) {
901 // Collect either the background markers or the foreground markers, but not both
902 switch (marker->type()) {
903 case DocumentMarker::Grammar:
904 case DocumentMarker::Spelling:
905 case DocumentMarker::CorrectionIndicator:
906 case DocumentMarker::Replacement:
907 case DocumentMarker::DictationAlternatives:
908#if PLATFORM(IOS_FAMILY)
909 // FIXME: Remove the PLATFORM(IOS_FAMILY)-guard.
910 case DocumentMarker::DictationPhraseWithAlternatives:
911#endif
912 if (phase != TextPaintPhase::Decoration)
913 continue;
914 break;
915 case DocumentMarker::TextMatch:
916 if (!renderer().frame().editor().markedTextMatchesAreHighlighted())
917 continue;
918 if (phase == TextPaintPhase::Decoration)
919 continue;
920 break;
921#if ENABLE(TELEPHONE_NUMBER_DETECTION)
922 case DocumentMarker::TelephoneNumber:
923 if (!renderer().frame().editor().markedTextMatchesAreHighlighted())
924 continue;
925 if (phase != TextPaintPhase::Background)
926 continue;
927 break;
928#endif
929 default:
930 continue;
931 }
932
933 if (marker->endOffset() <= start()) {
934 // Marker is completely before this run. This might be a marker that sits before the
935 // first run we draw, or markers that were within runs we skipped due to truncation.
936 continue;
937 }
938
939 if (marker->startOffset() > end()) {
940 // Marker is completely after this run, bail. A later run will paint it.
941 break;
942 }
943
944 // Marker intersects this run. Collect it.
945 switch (marker->type()) {
946 case DocumentMarker::Spelling:
947 case DocumentMarker::CorrectionIndicator:
948 case DocumentMarker::DictationAlternatives:
949 case DocumentMarker::Grammar:
950#if PLATFORM(IOS_FAMILY)
951 // FIXME: See <rdar://problem/8933352>. Also, remove the PLATFORM(IOS_FAMILY)-guard.
952 case DocumentMarker::DictationPhraseWithAlternatives:
953#endif
954 case DocumentMarker::TextMatch:
955 markedTexts.uncheckedAppend({ clampedOffset(marker->startOffset()), clampedOffset(marker->endOffset()), markedTextTypeForMarkerType(marker->type()), marker });
956 break;
957 case DocumentMarker::Replacement:
958 break;
959#if ENABLE(TELEPHONE_NUMBER_DETECTION)
960 case DocumentMarker::TelephoneNumber:
961 break;
962#endif
963 default:
964 ASSERT_NOT_REACHED();
965 }
966 }
967 return markedTexts;
968}
969
970FloatPoint InlineTextBox::textOriginFromBoxRect(const FloatRect& boxRect) const
971{
972 FloatPoint textOrigin { boxRect.x(), boxRect.y() + lineFont().fontMetrics().ascent() };
973 if (auto* combinedText = this->combinedText()) {
974 if (auto newOrigin = combinedText->computeTextOrigin(boxRect))
975 textOrigin = newOrigin.value();
976 }
977 if (isHorizontal())
978 textOrigin.setY(roundToDevicePixel(LayoutUnit { textOrigin.y() }, renderer().document().deviceScaleFactor()));
979 else
980 textOrigin.setX(roundToDevicePixel(LayoutUnit { textOrigin.x() }, renderer().document().deviceScaleFactor()));
981 return textOrigin;
982}
983
984void InlineTextBox::paintMarkedTexts(PaintInfo& paintInfo, TextPaintPhase phase, const FloatRect& boxRect, const Vector<StyledMarkedText>& markedTexts, const FloatRect& decorationClipOutRect)
985{
986 switch (phase) {
987 case TextPaintPhase::Background:
988 for (auto& markedText : markedTexts)
989 paintMarkedTextBackground(paintInfo, boxRect.location(), markedText.style.backgroundColor, markedText.startOffset, markedText.endOffset);
990 return;
991 case TextPaintPhase::Foreground:
992 for (auto& markedText : markedTexts)
993 paintMarkedTextForeground(paintInfo, boxRect, markedText);
994 return;
995 case TextPaintPhase::Decoration:
996 for (auto& markedText : markedTexts)
997 paintMarkedTextDecoration(paintInfo, boxRect, decorationClipOutRect, markedText);
998 return;
999 }
1000}
1001
1002void InlineTextBox::paintMarkedTextBackground(PaintInfo& paintInfo, const FloatPoint& boxOrigin, const Color& color, unsigned clampedStartOffset, unsigned clampedEndOffset)
1003{
1004 if (clampedStartOffset >= clampedEndOffset)
1005 return;
1006
1007 GraphicsContext& context = paintInfo.context();
1008 GraphicsContextStateSaver stateSaver { context };
1009 updateGraphicsContext(context, TextPaintStyle { color }); // Don't draw text at all!
1010
1011 // Note that if the text is truncated, we let the thing being painted in the truncation
1012 // draw its own highlight.
1013 TextRun textRun = createTextRun();
1014
1015 const RootInlineBox& rootBox = root();
1016 LayoutUnit selectionBottom = rootBox.selectionBottom();
1017 LayoutUnit selectionTop = rootBox.selectionTopAdjustedForPrecedingBlock();
1018
1019 // Use same y positioning and height as for selection, so that when the selection and this subrange are on
1020 // the same word there are no pieces sticking out.
1021 LayoutUnit deltaY = renderer().style().isFlippedLinesWritingMode() ? selectionBottom - logicalBottom() : logicalTop() - selectionTop;
1022 LayoutUnit selectionHeight = std::max<LayoutUnit>(0, selectionBottom - selectionTop);
1023
1024 LayoutRect selectionRect = LayoutRect(boxOrigin.x(), boxOrigin.y() - deltaY, logicalWidth(), selectionHeight);
1025 lineFont().adjustSelectionRectForText(textRun, selectionRect, clampedStartOffset, clampedEndOffset);
1026
1027 // FIXME: Support painting combined text. See <https://bugs.webkit.org/show_bug.cgi?id=180993>.
1028 context.fillRect(snapRectToDevicePixelsWithWritingDirection(selectionRect, renderer().document().deviceScaleFactor(), textRun.ltr()), color);
1029}
1030
1031void InlineTextBox::paintMarkedTextForeground(PaintInfo& paintInfo, const FloatRect& boxRect, const StyledMarkedText& markedText)
1032{
1033 if (markedText.startOffset >= markedText.endOffset)
1034 return;
1035
1036 GraphicsContext& context = paintInfo.context();
1037 const FontCascade& font = lineFont();
1038 const RenderStyle& lineStyle = this->lineStyle();
1039
1040 float emphasisMarkOffset = 0;
1041 Optional<bool> markExistsAndIsAbove = emphasisMarkExistsAndIsAbove(lineStyle);
1042 const AtomicString& emphasisMark = markExistsAndIsAbove ? lineStyle.textEmphasisMarkString() : nullAtom();
1043 if (!emphasisMark.isEmpty())
1044 emphasisMarkOffset = *markExistsAndIsAbove ? -font.fontMetrics().ascent() - font.emphasisMarkDescent(emphasisMark) : font.fontMetrics().descent() + font.emphasisMarkAscent(emphasisMark);
1045
1046 TextPainter textPainter { context };
1047 textPainter.setFont(font);
1048 textPainter.setStyle(markedText.style.textStyles);
1049 textPainter.setIsHorizontal(isHorizontal());
1050 if (markedText.style.textShadow) {
1051 textPainter.setShadow(&markedText.style.textShadow.value());
1052 if (lineStyle.hasAppleColorFilter())
1053 textPainter.setShadowColorFilter(&lineStyle.appleColorFilter());
1054 }
1055 textPainter.setEmphasisMark(emphasisMark, emphasisMarkOffset, combinedText());
1056
1057 TextRun textRun = createTextRun();
1058 textPainter.setGlyphDisplayListIfNeeded(*this, paintInfo, font, context, textRun);
1059
1060 GraphicsContextStateSaver stateSaver { context, false };
1061 if (markedText.type == MarkedText::DraggedContent) {
1062 stateSaver.save();
1063 context.setAlpha(markedText.style.alpha);
1064 }
1065 // TextPainter wants the box rectangle and text origin of the entire line box.
1066 textPainter.paintRange(textRun, boxRect, textOriginFromBoxRect(boxRect), markedText.startOffset, markedText.endOffset);
1067}
1068
1069void InlineTextBox::paintMarkedTextDecoration(PaintInfo& paintInfo, const FloatRect& boxRect, const FloatRect& clipOutRect, const StyledMarkedText& markedText)
1070{
1071 if (m_truncation == cFullTruncation)
1072 return;
1073
1074 GraphicsContext& context = paintInfo.context();
1075 updateGraphicsContext(context, markedText.style.textStyles);
1076
1077 bool isCombinedText = combinedText();
1078 if (isCombinedText)
1079 context.concatCTM(rotation(boxRect, Clockwise));
1080
1081 // 1. Compute text selection
1082 unsigned startOffset = markedText.startOffset;
1083 unsigned endOffset = markedText.endOffset;
1084 if (startOffset >= endOffset)
1085 return;
1086
1087 // Note that if the text is truncated, we let the thing being painted in the truncation
1088 // draw its own decoration.
1089 TextRun textRun = createTextRun();
1090
1091 // Avoid measuring the text when the entire line box is selected as an optimization.
1092 FloatRect snappedSelectionRect = boxRect;
1093 if (startOffset || endOffset != textRun.length()) {
1094 LayoutRect selectionRect = { boxRect.x(), boxRect.y(), boxRect.width(), boxRect.height() };
1095 lineFont().adjustSelectionRectForText(textRun, selectionRect, startOffset, endOffset);
1096 snappedSelectionRect = snapRectToDevicePixelsWithWritingDirection(selectionRect, renderer().document().deviceScaleFactor(), textRun.ltr());
1097 }
1098
1099 // 2. Paint
1100 TextDecorationPainter decorationPainter { context, lineStyle().textDecorationsInEffect(), renderer(), isFirstLine(), lineFont(), markedText.style.textDecorationStyles };
1101 decorationPainter.setInlineTextBox(this);
1102 decorationPainter.setWidth(snappedSelectionRect.width());
1103 decorationPainter.setIsHorizontal(isHorizontal());
1104 if (markedText.style.textShadow) {
1105 decorationPainter.setTextShadow(&markedText.style.textShadow.value());
1106 if (lineStyle().hasAppleColorFilter())
1107 decorationPainter.setShadowColorFilter(&lineStyle().appleColorFilter());
1108 }
1109
1110 {
1111 GraphicsContextStateSaver stateSaver { context, false };
1112 bool isDraggedContent = markedText.type == MarkedText::DraggedContent;
1113 if (isDraggedContent || !clipOutRect.isEmpty()) {
1114 stateSaver.save();
1115 if (isDraggedContent)
1116 context.setAlpha(markedText.style.alpha);
1117 if (!clipOutRect.isEmpty())
1118 context.clipOut(clipOutRect);
1119 }
1120 decorationPainter.paintTextDecoration(textRun.subRun(startOffset, endOffset - startOffset), textOriginFromBoxRect(snappedSelectionRect), snappedSelectionRect.location());
1121 }
1122
1123 if (isCombinedText)
1124 context.concatCTM(rotation(boxRect, Counterclockwise));
1125}
1126
1127void InlineTextBox::paintCompositionBackground(PaintInfo& paintInfo, const FloatPoint& boxOrigin)
1128{
1129 paintMarkedTextBackground(paintInfo, boxOrigin, Color::compositionFill, clampedOffset(renderer().frame().editor().compositionStart()), clampedOffset(renderer().frame().editor().compositionEnd()));
1130}
1131
1132void InlineTextBox::paintCompositionUnderlines(PaintInfo& paintInfo, const FloatPoint& boxOrigin) const
1133{
1134 if (m_truncation == cFullTruncation)
1135 return;
1136
1137 for (auto& underline : renderer().frame().editor().customCompositionUnderlines()) {
1138 if (underline.endOffset <= m_start) {
1139 // Underline is completely before this run. This might be an underline that sits
1140 // before the first run we draw, or underlines that were within runs we skipped
1141 // due to truncation.
1142 continue;
1143 }
1144
1145 if (underline.startOffset > end())
1146 break; // Underline is completely after this run, bail. A later run will paint it.
1147
1148 // Underline intersects this run. Paint it.
1149 paintCompositionUnderline(paintInfo, boxOrigin, underline);
1150
1151 if (underline.endOffset > end() + 1)
1152 break; // Underline also runs into the next run. Bail now, no more marker advancement.
1153 }
1154}
1155
1156static inline void mirrorRTLSegment(float logicalWidth, TextDirection direction, float& start, float width)
1157{
1158 if (direction == TextDirection::LTR)
1159 return;
1160 start = logicalWidth - width - start;
1161}
1162
1163void InlineTextBox::paintCompositionUnderline(PaintInfo& paintInfo, const FloatPoint& boxOrigin, const CompositionUnderline& underline) const
1164{
1165 if (m_truncation == cFullTruncation)
1166 return;
1167
1168 float start = 0; // start of line to draw, relative to tx
1169 float width = logicalWidth(); // how much line to draw
1170 bool useWholeWidth = true;
1171 unsigned paintStart = m_start;
1172 unsigned paintEnd = end() + 1; // end points at the last char, not past it
1173 if (paintStart <= underline.startOffset) {
1174 paintStart = underline.startOffset;
1175 useWholeWidth = false;
1176 start = renderer().width(m_start, paintStart - m_start, textPos(), isFirstLine());
1177 }
1178 if (paintEnd != underline.endOffset) { // end points at the last char, not past it
1179 paintEnd = std::min(paintEnd, (unsigned)underline.endOffset);
1180 useWholeWidth = false;
1181 }
1182 if (m_truncation != cNoTruncation) {
1183 paintEnd = std::min(paintEnd, (unsigned)m_start + m_truncation);
1184 useWholeWidth = false;
1185 }
1186 if (!useWholeWidth) {
1187 width = renderer().width(paintStart, paintEnd - paintStart, textPos() + start, isFirstLine());
1188 mirrorRTLSegment(logicalWidth(), direction(), start, width);
1189 }
1190
1191 // Thick marked text underlines are 2px thick as long as there is room for the 2px line under the baseline.
1192 // All other marked text underlines are 1px thick.
1193 // If there's not enough space the underline will touch or overlap characters.
1194 int lineThickness = 1;
1195 int baseline = lineStyle().fontMetrics().ascent();
1196 if (underline.thick && logicalHeight() - baseline >= 2)
1197 lineThickness = 2;
1198
1199 // We need to have some space between underlines of subsequent clauses, because some input methods do not use different underline styles for those.
1200 // We make each line shorter, which has a harmless side effect of shortening the first and last clauses, too.
1201 start += 1;
1202 width -= 2;
1203
1204 GraphicsContext& context = paintInfo.context();
1205 Color underlineColor = underline.compositionUnderlineColor == CompositionUnderlineColor::TextColor ? renderer().style().visitedDependentColorWithColorFilter(CSSPropertyWebkitTextFillColor) : renderer().style().colorByApplyingColorFilter(underline.color);
1206 context.setStrokeColor(underlineColor);
1207 context.setStrokeThickness(lineThickness);
1208 context.drawLineForText(FloatRect(boxOrigin.x() + start, boxOrigin.y() + logicalHeight() - lineThickness, width, lineThickness), renderer().document().printing());
1209}
1210
1211int InlineTextBox::caretMinOffset() const
1212{
1213 return m_start;
1214}
1215
1216int InlineTextBox::caretMaxOffset() const
1217{
1218 return m_start + m_len;
1219}
1220
1221float InlineTextBox::textPos() const
1222{
1223 // When computing the width of a text run, RenderBlock::computeInlineDirectionPositionsForLine() doesn't include the actual offset
1224 // from the containing block edge in its measurement. textPos() should be consistent so the text are rendered in the same width.
1225 if (logicalLeft() == 0)
1226 return 0;
1227 return logicalLeft() - root().logicalLeft();
1228}
1229
1230int InlineTextBox::offsetForPosition(float lineOffset, bool includePartialGlyphs) const
1231{
1232 if (isLineBreak())
1233 return 0;
1234 if (lineOffset - logicalLeft() > logicalWidth())
1235 return isLeftToRightDirection() ? len() : 0;
1236 if (lineOffset - logicalLeft() < 0)
1237 return isLeftToRightDirection() ? 0 : len();
1238 bool ignoreCombinedText = true;
1239 bool ignoreHyphen = true;
1240 return lineFont().offsetForPosition(createTextRun(ignoreCombinedText, ignoreHyphen), lineOffset - logicalLeft(), includePartialGlyphs);
1241}
1242
1243float InlineTextBox::positionForOffset(unsigned offset) const
1244{
1245 ASSERT(offset >= m_start);
1246 ASSERT(offset <= m_start + len());
1247
1248 if (isLineBreak())
1249 return logicalLeft();
1250
1251 unsigned startOffset;
1252 unsigned endOffset;
1253 if (isLeftToRightDirection()) {
1254 startOffset = 0;
1255 endOffset = clampedOffset(offset);
1256 } else {
1257 startOffset = clampedOffset(offset);
1258 endOffset = m_len;
1259 }
1260
1261 // FIXME: Do we need to add rightBearing here?
1262 LayoutRect selectionRect = LayoutRect(logicalLeft(), 0, 0, 0);
1263 bool ignoreCombinedText = true;
1264 bool ignoreHyphen = true;
1265 TextRun textRun = createTextRun(ignoreCombinedText, ignoreHyphen);
1266 lineFont().adjustSelectionRectForText(textRun, selectionRect, startOffset, endOffset);
1267 return snapRectToDevicePixelsWithWritingDirection(selectionRect, renderer().document().deviceScaleFactor(), textRun.ltr()).maxX();
1268}
1269
1270TextRun InlineTextBox::createTextRun(bool ignoreCombinedText, bool ignoreHyphen) const
1271{
1272 const auto& style = lineStyle();
1273 TextRun textRun { text(ignoreCombinedText, ignoreHyphen), textPos(), expansion(), expansionBehavior(), direction(), dirOverride() || style.rtlOrdering() == Order::Visual, !renderer().canUseSimpleFontCodePath() };
1274 textRun.setTabSize(!style.collapseWhiteSpace(), style.tabSize());
1275 return textRun;
1276}
1277
1278String InlineTextBox::text(bool ignoreCombinedText, bool ignoreHyphen) const
1279{
1280 if (auto* combinedText = this->combinedText()) {
1281 if (ignoreCombinedText)
1282 return renderer().text().substring(m_start, m_len);
1283 return combinedText->combinedStringForRendering();
1284 }
1285 if (hasHyphen()) {
1286 if (ignoreHyphen)
1287 return renderer().text().substring(m_start, m_len);
1288 return makeString(StringView(renderer().text()).substring(m_start, m_len), lineStyle().hyphenString());
1289 }
1290 return renderer().text().substring(m_start, m_len);
1291}
1292
1293inline const RenderCombineText* InlineTextBox::combinedText() const
1294{
1295 return lineStyle().hasTextCombine() && is<RenderCombineText>(renderer()) && downcast<RenderCombineText>(renderer()).isCombined() ? &downcast<RenderCombineText>(renderer()) : nullptr;
1296}
1297
1298ExpansionBehavior InlineTextBox::expansionBehavior() const
1299{
1300 ExpansionBehavior leadingBehavior;
1301 if (forceLeadingExpansion())
1302 leadingBehavior = ForceLeadingExpansion;
1303 else if (canHaveLeadingExpansion())
1304 leadingBehavior = AllowLeadingExpansion;
1305 else
1306 leadingBehavior = ForbidLeadingExpansion;
1307
1308 ExpansionBehavior trailingBehavior;
1309 if (forceTrailingExpansion())
1310 trailingBehavior = ForceTrailingExpansion;
1311 else if (expansion() && nextLeafChild() && !nextLeafChild()->isLineBreak())
1312 trailingBehavior = AllowTrailingExpansion;
1313 else
1314 trailingBehavior = ForbidTrailingExpansion;
1315
1316 return leadingBehavior | trailingBehavior;
1317}
1318
1319#if ENABLE(TREE_DEBUGGING)
1320
1321const char* InlineTextBox::boxName() const
1322{
1323 return "InlineTextBox";
1324}
1325
1326void InlineTextBox::outputLineBox(TextStream& stream, bool mark, int depth) const
1327{
1328 stream << "-------- " << (isDirty() ? "D" : "-") << "-";
1329
1330 int printedCharacters = 0;
1331 if (mark) {
1332 stream << "*";
1333 ++printedCharacters;
1334 }
1335 while (++printedCharacters <= depth * 2)
1336 stream << " ";
1337
1338 String value = renderer().text();
1339 value = value.substring(start(), len());
1340 value.replaceWithLiteral('\\', "\\\\");
1341 value.replaceWithLiteral('\n', "\\n");
1342 stream << boxName() << " " << FloatRect(x(), y(), width(), height()) << " (" << this << ") renderer->(" << &renderer() << ") run(" << start() << ", " << start() + len() << ") \"" << value.utf8().data() << "\"";
1343 stream.nextLine();
1344}
1345
1346#endif
1347
1348} // namespace WebCore
1349