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 | |
58 | namespace WebCore { |
59 | |
60 | WTF_MAKE_ISO_ALLOCATED_IMPL(InlineTextBox); |
61 | |
62 | struct SameSizeAsInlineTextBox : public InlineBox { |
63 | unsigned variables[1]; |
64 | unsigned short variables2[2]; |
65 | void* pointers[2]; |
66 | }; |
67 | |
68 | COMPILE_ASSERT(sizeof(InlineTextBox) == sizeof(SameSizeAsInlineTextBox), InlineTextBox_should_stay_small); |
69 | |
70 | typedef WTF::HashMap<const InlineTextBox*, LayoutRect> InlineTextBoxOverflowMap; |
71 | static InlineTextBoxOverflowMap* gTextBoxesWithOverflow; |
72 | |
73 | InlineTextBox::~InlineTextBox() |
74 | { |
75 | if (!knownToHaveNoOverflow() && gTextBoxesWithOverflow) |
76 | gTextBoxesWithOverflow->remove(this); |
77 | TextPainter::removeGlyphDisplayList(*this); |
78 | } |
79 | |
80 | bool 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 | |
91 | void InlineTextBox::markDirty(bool dirty) |
92 | { |
93 | if (dirty) { |
94 | m_len = 0; |
95 | m_start = 0; |
96 | } |
97 | InlineBox::markDirty(dirty); |
98 | } |
99 | |
100 | LayoutRect InlineTextBox::logicalOverflowRect() const |
101 | { |
102 | if (knownToHaveNoOverflow() || !gTextBoxesWithOverflow) |
103 | return enclosingIntRect(logicalFrameRect()); |
104 | return gTextBoxesWithOverflow->get(this); |
105 | } |
106 | |
107 | void InlineTextBox::setLogicalOverflowRect(const LayoutRect& rect) |
108 | { |
109 | ASSERT(!knownToHaveNoOverflow()); |
110 | if (!gTextBoxesWithOverflow) |
111 | gTextBoxesWithOverflow = new InlineTextBoxOverflowMap; |
112 | gTextBoxesWithOverflow->add(this, rect); |
113 | } |
114 | |
115 | int 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 | |
124 | LayoutUnit 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 | |
133 | LayoutUnit InlineTextBox::selectionTop() const |
134 | { |
135 | return root().selectionTop(); |
136 | } |
137 | |
138 | LayoutUnit InlineTextBox::selectionBottom() const |
139 | { |
140 | return root().selectionBottom(); |
141 | } |
142 | |
143 | LayoutUnit InlineTextBox::selectionHeight() const |
144 | { |
145 | return root().selectionHeight(); |
146 | } |
147 | |
148 | bool InlineTextBox::isSelected(unsigned startPosition, unsigned endPosition) const |
149 | { |
150 | return clampedOffset(startPosition) < clampedOffset(endPosition); |
151 | } |
152 | |
153 | RenderObject::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 | |
199 | inline const FontCascade& InlineTextBox::lineFont() const |
200 | { |
201 | return combinedText() ? combinedText()->textCombineFont() : lineStyle().fontCascade(); |
202 | } |
203 | |
204 | // FIXME: Share more code with paintMarkedTextBackground(). |
205 | LayoutRect 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 | |
238 | void InlineTextBox::deleteLine() |
239 | { |
240 | renderer().removeTextBox(*this); |
241 | delete this; |
242 | } |
243 | |
244 | void InlineTextBox::() |
245 | { |
246 | if (extracted()) |
247 | return; |
248 | |
249 | renderer().extractTextBox(*this); |
250 | } |
251 | |
252 | void InlineTextBox::attachLine() |
253 | { |
254 | if (!extracted()) |
255 | return; |
256 | |
257 | renderer().attachTextBox(*this); |
258 | } |
259 | |
260 | float 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 | |
330 | bool InlineTextBox::isLineBreak() const |
331 | { |
332 | return renderer().style().preserveNewline() && len() == 1 && renderer().text()[start()] == '\n'; |
333 | } |
334 | |
335 | bool 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 | |
368 | Optional<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 | |
409 | struct 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 | |
430 | struct InlineTextBox::StyledMarkedText : MarkedText { |
431 | StyledMarkedText(const MarkedText& marker) |
432 | : MarkedText { marker } |
433 | { |
434 | } |
435 | |
436 | MarkedTextStyle style; |
437 | }; |
438 | |
439 | static 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 | |
449 | void 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 | |
640 | unsigned 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 | |
659 | std::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 | |
674 | bool InlineTextBox::hasMarkers() const |
675 | { |
676 | return collectMarkedTextsForDocumentMarkers(TextPaintPhase::Decoration).size(); |
677 | } |
678 | |
679 | void 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 | |
686 | FloatRect 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 | |
695 | FloatRect 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 | |
714 | void 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 | |
751 | auto 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 | |
763 | auto 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 | |
804 | auto 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 | |
831 | auto InlineTextBox::coalesceAdjacentMarkedTexts(const Vector<StyledMarkedText>& textsToCoalesce, MarkedTextStylesEqualityFunction ) -> 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 | |
855 | Vector<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 | |
865 | Vector<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 | |
970 | FloatPoint 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 | |
984 | void 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 | |
1002 | void 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 | |
1031 | void 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 | |
1069 | void 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 | |
1127 | void 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 | |
1132 | void 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 | |
1156 | static 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 | |
1163 | void 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 | |
1211 | int InlineTextBox::caretMinOffset() const |
1212 | { |
1213 | return m_start; |
1214 | } |
1215 | |
1216 | int InlineTextBox::caretMaxOffset() const |
1217 | { |
1218 | return m_start + m_len; |
1219 | } |
1220 | |
1221 | float 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 | |
1230 | int 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 | |
1243 | float 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 | |
1270 | TextRun 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 | |
1278 | String 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 | |
1293 | inline const RenderCombineText* InlineTextBox::combinedText() const |
1294 | { |
1295 | return lineStyle().hasTextCombine() && is<RenderCombineText>(renderer()) && downcast<RenderCombineText>(renderer()).isCombined() ? &downcast<RenderCombineText>(renderer()) : nullptr; |
1296 | } |
1297 | |
1298 | ExpansionBehavior 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 | |
1321 | const char* InlineTextBox::boxName() const |
1322 | { |
1323 | return "InlineTextBox" ; |
1324 | } |
1325 | |
1326 | void 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 | |