| /* |
| * Copyright (C) 2021 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| * DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY |
| * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
| * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON |
| * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
| * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #include "TextBoxPainter.h" |
| |
| #include "CompositionHighlight.h" |
| #include "DocumentMarkerController.h" |
| #include "Editor.h" |
| #include "EventRegion.h" |
| #include "GraphicsContext.h" |
| #include "InlineIteratorLineBox.h" |
| #include "LegacyInlineTextBox.h" |
| #include "LineSelection.h" |
| #include "PaintInfo.h" |
| #include "RenderBlock.h" |
| #include "RenderCombineText.h" |
| #include "RenderText.h" |
| #include "RenderView.h" |
| #include "ShadowData.h" |
| #include "StyledMarkedText.h" |
| #include "TextPaintStyle.h" |
| #include "TextPainter.h" |
| |
| namespace WebCore { |
| |
| static FloatRect calculateDocumentMarkerBounds(const InlineIterator::TextBoxIterator&, const MarkedText&); |
| |
| LegacyTextBoxPainter::LegacyTextBoxPainter(const LegacyInlineTextBox& textBox, PaintInfo& paintInfo, const LayoutPoint& paintOffset) |
| : TextBoxPainter(InlineIterator::BoxLegacyPath { &textBox }, paintInfo, paintOffset) |
| { |
| m_emphasisMarkExistsAndIsAbove = textBox.emphasisMarkExistsAndIsAbove(m_style); |
| } |
| |
| #if ENABLE(LAYOUT_FORMATTING_CONTEXT) |
| ModernTextBoxPainter::ModernTextBoxPainter(const LayoutIntegration::InlineContent& inlineContent, const InlineDisplay::Box& box, PaintInfo& paintInfo, const LayoutPoint& paintOffset) |
| : TextBoxPainter(InlineIterator::BoxModernPath { inlineContent, inlineContent.indexForBox(box) }, paintInfo, paintOffset) |
| { |
| } |
| #endif |
| |
| template<typename TextBoxPath> |
| TextBoxPainter<TextBoxPath>::TextBoxPainter(TextBoxPath&& textBox, PaintInfo& paintInfo, const LayoutPoint& paintOffset) |
| : m_textBox(WTFMove(textBox)) |
| , m_renderer(downcast<RenderText>(m_textBox.renderer())) |
| , m_document(m_renderer.document()) |
| , m_style(m_textBox.style()) |
| , m_logicalRect(m_textBox.isHorizontal() ? m_textBox.visualRectIgnoringBlockDirection() : m_textBox.visualRectIgnoringBlockDirection().transposedRect()) |
| , m_paintTextRun(m_textBox.createTextRun(InlineIterator::CreateTextRunMode::Painting)) |
| , m_paintInfo(paintInfo) |
| , m_selectableRange(m_textBox.selectableRange()) |
| , m_paintRect(computePaintRect(paintOffset)) |
| , m_isFirstLine(m_textBox.isFirstLine()) |
| , m_isCombinedText(is<RenderCombineText>(m_renderer) && downcast<RenderCombineText>(m_renderer).isCombined()) |
| , m_isPrinting(m_document.printing()) |
| , m_haveSelection(computeHaveSelection()) |
| , m_containsComposition(m_renderer.textNode() && m_renderer.frame().editor().compositionNode() == m_renderer.textNode()) |
| , m_useCustomUnderlines(m_containsComposition && m_renderer.frame().editor().compositionUsesCustomUnderlines()) |
| { |
| ASSERT(paintInfo.phase == PaintPhase::Foreground || paintInfo.phase == PaintPhase::Selection || paintInfo.phase == PaintPhase::TextClip || paintInfo.phase == PaintPhase::EventRegion); |
| } |
| |
| template<typename TextBoxPath> |
| TextBoxPainter<TextBoxPath>::~TextBoxPainter() |
| { |
| } |
| |
| template<typename TextBoxPath> |
| InlineIterator::TextBoxIterator TextBoxPainter<TextBoxPath>::makeIterator() const |
| { |
| auto pathCopy = m_textBox; |
| return InlineIterator::TextBoxIterator { WTFMove(pathCopy) }; |
| } |
| |
| template<typename TextBoxPath> |
| void TextBoxPainter<TextBoxPath>::paint() |
| { |
| if (m_paintInfo.phase == PaintPhase::Selection && !m_haveSelection) |
| return; |
| |
| if (m_paintInfo.phase == PaintPhase::EventRegion) { |
| if (m_renderer.parent()->visibleToHitTesting()) |
| m_paintInfo.eventRegionContext->unite(enclosingIntRect(m_paintRect), const_cast<RenderText&>(m_renderer), m_style); |
| return; |
| } |
| |
| bool shouldRotate = !textBox().isHorizontal() && !m_isCombinedText; |
| if (shouldRotate) |
| m_paintInfo.context().concatCTM(rotation(m_paintRect, Clockwise)); |
| |
| if (m_paintInfo.phase == PaintPhase::Foreground) { |
| if (!m_isPrinting) |
| paintBackground(); |
| |
| paintPlatformDocumentMarkers(); |
| } |
| |
| paintForegroundAndDecorations(); |
| |
| if (m_paintInfo.phase == PaintPhase::Foreground) { |
| if (m_useCustomUnderlines) |
| paintCompositionUnderlines(); |
| |
| m_renderer.page().addRelevantRepaintedObject(const_cast<RenderText*>(&m_renderer), enclosingLayoutRect(m_paintRect)); |
| } |
| |
| if (shouldRotate) |
| m_paintInfo.context().concatCTM(rotation(m_paintRect, Counterclockwise)); |
| } |
| |
| template<typename TextBoxPath> |
| MarkedText TextBoxPainter<TextBoxPath>::createMarkedTextFromSelectionInBox() |
| { |
| auto [selectionStart, selectionEnd] = m_renderer.view().selection().rangeForTextBox(m_renderer, m_selectableRange); |
| if (selectionStart < selectionEnd) |
| return { selectionStart, selectionEnd, MarkedText::Selection }; |
| return { }; |
| } |
| |
| template<typename TextBoxPath> |
| void TextBoxPainter<TextBoxPath>::paintBackground() |
| { |
| auto shouldPaintCompositionBackground = m_containsComposition && !m_useCustomUnderlines; |
| #if ENABLE(TEXT_SELECTION) |
| auto hasSelectionWithNonCustomUnderline = m_haveSelection && !m_useCustomUnderlines; |
| #endif |
| |
| auto shouldPaintBackground = [&] { |
| #if ENABLE(TEXT_SELECTION) |
| if (hasSelectionWithNonCustomUnderline) |
| return true; |
| #endif |
| if (shouldPaintCompositionBackground) |
| return true; |
| if (m_document.markers().hasMarkers()) |
| return true; |
| if (m_document.hasHighlight()) |
| return true; |
| return false; |
| }; |
| if (!shouldPaintBackground()) |
| return; |
| |
| if (shouldPaintCompositionBackground) |
| paintCompositionBackground(); |
| |
| Vector<MarkedText> markedTexts; |
| markedTexts.appendVector(MarkedText::collectForDocumentMarkers(m_renderer, m_selectableRange, MarkedText::PaintPhase::Background)); |
| markedTexts.appendVector(MarkedText::collectForHighlights(m_renderer, m_selectableRange, MarkedText::PaintPhase::Background)); |
| |
| #if ENABLE(TEXT_SELECTION) |
| if (hasSelectionWithNonCustomUnderline && !m_paintInfo.context().paintingDisabled()) { |
| auto selectionMarkedText = createMarkedTextFromSelectionInBox(); |
| if (!selectionMarkedText.isEmpty()) |
| markedTexts.append(WTFMove(selectionMarkedText)); |
| } |
| #endif |
| auto styledMarkedTexts = StyledMarkedText::subdivideAndResolve(markedTexts, m_renderer, m_isFirstLine, m_paintInfo); |
| |
| // Coalesce styles of adjacent marked texts to minimize the number of drawing commands. |
| auto coalescedStyledMarkedTexts = StyledMarkedText::coalesceAdjacentWithEqualBackground(styledMarkedTexts); |
| |
| for (auto& markedText : coalescedStyledMarkedTexts) |
| paintBackground(markedText); |
| } |
| |
| template<typename TextBoxPath> |
| void TextBoxPainter<TextBoxPath>::paintForegroundAndDecorations() |
| { |
| auto shouldPaintSelectionForeground = m_haveSelection && !m_useCustomUnderlines; |
| auto hasTextDecoration = !m_style.textDecorationsInEffect().isEmpty(); |
| auto hasHighlightDecoration = m_document.hasHighlight() && !MarkedText::collectForHighlights(m_renderer, m_selectableRange, MarkedText::PaintPhase::Decoration).isEmpty(); |
| auto hasDecoration = hasTextDecoration || hasHighlightDecoration; |
| |
| auto contentMayNeedStyledMarkedText = [&] { |
| if (hasDecoration) |
| return true; |
| if (shouldPaintSelectionForeground) |
| return true; |
| if (m_document.markers().hasMarkers()) |
| return true; |
| if (m_document.hasHighlight()) |
| return true; |
| return false; |
| }; |
| if (!contentMayNeedStyledMarkedText()) { |
| auto& lineStyle = m_isFirstLine ? m_renderer.firstLineStyle() : m_renderer.style(); |
| paintForeground({ MarkedText { m_selectableRange.clamp(textBox().start()), m_selectableRange.clamp(textBox().end()), MarkedText::Unmarked }, |
| StyledMarkedText::computeStyleForUnmarkedMarkedText(m_renderer, lineStyle, m_isFirstLine, m_paintInfo) }); |
| return; |
| } |
| |
| Vector<MarkedText> markedTexts; |
| if (m_paintInfo.phase != PaintPhase::Selection) { |
| // The marked texts for the gaps between document markers and selection are implicitly created by subdividing the entire line. |
| markedTexts.append({ m_selectableRange.clamp(textBox().start()), m_selectableRange.clamp(textBox().end()), MarkedText::Unmarked }); |
| |
| if (!m_isPrinting) { |
| markedTexts.appendVector(MarkedText::collectForDocumentMarkers(m_renderer, m_selectableRange, MarkedText::PaintPhase::Foreground)); |
| markedTexts.appendVector(MarkedText::collectForHighlights(m_renderer, m_selectableRange, MarkedText::PaintPhase::Foreground)); |
| |
| bool shouldPaintDraggedContent = !(m_paintInfo.paintBehavior.contains(PaintBehavior::ExcludeSelection)); |
| if (shouldPaintDraggedContent) { |
| auto markedTextsForDraggedContent = MarkedText::collectForDraggedContent(m_renderer, m_selectableRange); |
| if (!markedTextsForDraggedContent.isEmpty()) { |
| shouldPaintSelectionForeground = false; |
| markedTexts.appendVector(markedTextsForDraggedContent); |
| } |
| } |
| } |
| } |
| // The selection marked text acts as a placeholder when computing the marked texts for the gaps... |
| if (shouldPaintSelectionForeground) { |
| ASSERT(!m_isPrinting); |
| auto selectionMarkedText = createMarkedTextFromSelectionInBox(); |
| if (!selectionMarkedText.isEmpty()) |
| markedTexts.append(WTFMove(selectionMarkedText)); |
| } |
| |
| auto styledMarkedTexts = StyledMarkedText::subdivideAndResolve(markedTexts, m_renderer, m_isFirstLine, m_paintInfo); |
| |
| // ... now remove the selection marked text if we are excluding selection. |
| if (!m_isPrinting && m_paintInfo.paintBehavior.contains(PaintBehavior::ExcludeSelection)) { |
| styledMarkedTexts.removeAllMatching([] (const StyledMarkedText& markedText) { |
| return markedText.type == MarkedText::Selection; |
| }); |
| } |
| |
| if (hasDecoration && m_paintInfo.phase != PaintPhase::Selection) { |
| unsigned length = m_selectableRange.truncation.value_or(m_paintTextRun.length()); |
| unsigned selectionStart = 0; |
| unsigned selectionEnd = 0; |
| if (m_haveSelection) |
| std::tie(selectionStart, selectionEnd) = m_renderer.view().selection().rangeForTextBox(m_renderer, m_selectableRange); |
| |
| FloatRect textDecorationSelectionClipOutRect; |
| if ((m_paintInfo.paintBehavior.contains(PaintBehavior::ExcludeSelection)) && selectionStart < selectionEnd && selectionEnd <= length) { |
| textDecorationSelectionClipOutRect = m_paintRect; |
| float logicalWidthBeforeRange; |
| float logicalWidthAfterRange; |
| float logicalSelectionWidth = fontCascade().widthOfTextRange(m_paintTextRun, selectionStart, selectionEnd, nullptr, &logicalWidthBeforeRange, &logicalWidthAfterRange); |
| // FIXME: Do we need to handle vertical bottom to top text? |
| if (!textBox().isHorizontal()) { |
| textDecorationSelectionClipOutRect.move(0, logicalWidthBeforeRange); |
| textDecorationSelectionClipOutRect.setHeight(logicalSelectionWidth); |
| } else if (textBox().direction() == TextDirection::RTL) { |
| textDecorationSelectionClipOutRect.move(logicalWidthAfterRange, 0); |
| textDecorationSelectionClipOutRect.setWidth(logicalSelectionWidth); |
| } else { |
| textDecorationSelectionClipOutRect.move(logicalWidthBeforeRange, 0); |
| textDecorationSelectionClipOutRect.setWidth(logicalSelectionWidth); |
| } |
| } |
| |
| // Coalesce styles of adjacent marked texts to minimize the number of drawing commands. |
| auto coalescedStyledMarkedTexts = StyledMarkedText::coalesceAdjacentWithEqualDecorations(styledMarkedTexts); |
| |
| for (auto& markedText : coalescedStyledMarkedTexts) { |
| unsigned startOffset = markedText.startOffset; |
| unsigned endOffset = markedText.endOffset; |
| if (startOffset < endOffset) { |
| // Avoid measuring the text when the entire line box is selected as an optimization. |
| FloatRect snappedSelectionRect = m_paintRect; |
| if (startOffset || endOffset != m_paintTextRun.length()) { |
| LayoutRect selectionRect = { m_paintRect.x(), m_paintRect.y(), m_paintRect.width(), m_paintRect.height() }; |
| fontCascade().adjustSelectionRectForText(m_paintTextRun, selectionRect, startOffset, endOffset); |
| snappedSelectionRect = snapRectToDevicePixelsWithWritingDirection(selectionRect, m_document.deviceScaleFactor(), m_paintTextRun.ltr()); |
| } |
| |
| TextDecorationPainter decorationPainter = createDecorationPainter(markedText, textDecorationSelectionClipOutRect, snappedSelectionRect); |
| paintBackgroundDecorations(decorationPainter, markedText, snappedSelectionRect); |
| paintForeground(markedText); |
| paintForegroundDecorations(decorationPainter, snappedSelectionRect); |
| } |
| } |
| } else { |
| // Coalesce styles of adjacent marked texts to minimize the number of drawing commands. |
| auto coalescedStyledMarkedTexts = StyledMarkedText::coalesceAdjacentWithEqualForeground(styledMarkedTexts); |
| |
| for (auto& markedText : coalescedStyledMarkedTexts) |
| paintForeground(markedText); |
| } |
| } |
| |
| template<typename TextBoxPath> |
| void TextBoxPainter<TextBoxPath>::paintCompositionBackground() |
| { |
| auto& editor = m_renderer.frame().editor(); |
| |
| if (!editor.compositionUsesCustomHighlights()) { |
| auto [clampedStart, clampedEnd] = m_selectableRange.clamp(editor.compositionStart(), editor.compositionEnd()); |
| |
| paintBackground(clampedStart, clampedEnd, CompositionHighlight::defaultCompositionFillColor); |
| return; |
| } |
| |
| for (auto& highlight : editor.customCompositionHighlights()) { |
| if (highlight.endOffset <= textBox().start()) |
| continue; |
| |
| if (highlight.startOffset >= textBox().end()) |
| break; |
| |
| auto [clampedStart, clampedEnd] = m_selectableRange.clamp(highlight.startOffset, highlight.endOffset); |
| |
| paintBackground(clampedStart, clampedEnd, highlight.color, BackgroundStyle::Rounded); |
| |
| if (highlight.endOffset > textBox().end()) |
| break; |
| } |
| } |
| |
| template<typename TextBoxPath> |
| void TextBoxPainter<TextBoxPath>::paintBackground(const StyledMarkedText& markedText) |
| { |
| paintBackground(markedText.startOffset, markedText.endOffset, markedText.style.backgroundColor, BackgroundStyle::Normal); |
| } |
| |
| template<typename TextBoxPath> |
| void TextBoxPainter<TextBoxPath>::paintBackground(unsigned startOffset, unsigned endOffset, const Color& color, BackgroundStyle backgroundStyle) |
| { |
| if (startOffset >= endOffset) |
| return; |
| |
| GraphicsContext& context = m_paintInfo.context(); |
| GraphicsContextStateSaver stateSaver { context }; |
| updateGraphicsContext(context, TextPaintStyle { color }); // Don't draw text at all! |
| |
| // Note that if the text is truncated, we let the thing being painted in the truncation |
| // draw its own highlight. |
| auto lineBox = makeIterator()->lineBox(); |
| auto selectionBottom = LineSelection::logicalBottom(*lineBox); |
| auto selectionTop = LineSelection::logicalTopAdjustedForPrecedingBlock(*lineBox); |
| // Use same y positioning and height as for selection, so that when the selection and this subrange are on |
| // the same word there are no pieces sticking out. |
| auto deltaY = LayoutUnit { m_style.isFlippedLinesWritingMode() ? selectionBottom - m_logicalRect.maxY() : m_logicalRect.y() - selectionTop }; |
| auto selectionHeight = LayoutUnit { std::max(0.f, selectionBottom - selectionTop) }; |
| auto selectionRect = LayoutRect { LayoutUnit(m_paintRect.x()), LayoutUnit(m_paintRect.y() - deltaY), LayoutUnit(m_logicalRect.width()), selectionHeight }; |
| fontCascade().adjustSelectionRectForText(m_paintTextRun, selectionRect, startOffset, endOffset); |
| |
| // FIXME: Support painting combined text. See <https://bugs.webkit.org/show_bug.cgi?id=180993>. |
| auto backgroundRect = snapRectToDevicePixelsWithWritingDirection(selectionRect, m_document.deviceScaleFactor(), m_paintTextRun.ltr()); |
| if (backgroundStyle == BackgroundStyle::Rounded) { |
| backgroundRect.expand(-1, -1); |
| backgroundRect.move(0.5, 0.5); |
| context.fillRoundedRect(FloatRoundedRect { backgroundRect, FloatRoundedRect::Radii { 2 } }, color); |
| return; |
| } |
| |
| context.fillRect(backgroundRect, color); |
| } |
| |
| template<typename TextBoxPath> |
| void TextBoxPainter<TextBoxPath>::paintForeground(const StyledMarkedText& markedText) |
| { |
| if (markedText.startOffset >= markedText.endOffset) |
| return; |
| |
| GraphicsContext& context = m_paintInfo.context(); |
| const FontCascade& font = fontCascade(); |
| |
| float emphasisMarkOffset = 0; |
| const AtomString& emphasisMark = m_emphasisMarkExistsAndIsAbove ? m_style.textEmphasisMarkString() : nullAtom(); |
| if (!emphasisMark.isEmpty()) |
| emphasisMarkOffset = *m_emphasisMarkExistsAndIsAbove ? -font.metricsOfPrimaryFont().ascent() - font.emphasisMarkDescent(emphasisMark) : font.metricsOfPrimaryFont().descent() + font.emphasisMarkAscent(emphasisMark); |
| |
| TextPainter textPainter { context, font }; |
| textPainter.setStyle(markedText.style.textStyles); |
| textPainter.setIsHorizontal(textBox().isHorizontal()); |
| if (markedText.style.textShadow) { |
| textPainter.setShadow(&markedText.style.textShadow.value()); |
| if (m_style.hasAppleColorFilter()) |
| textPainter.setShadowColorFilter(&m_style.appleColorFilter()); |
| } |
| textPainter.setEmphasisMark(emphasisMark, emphasisMarkOffset, m_isCombinedText ? &downcast<RenderCombineText>(m_renderer) : nullptr); |
| if (auto* debugShadow = debugTextShadow()) |
| textPainter.setShadow(debugShadow); |
| |
| GraphicsContextStateSaver stateSaver(context, markedText.style.textStyles.strokeWidth > 0 || markedText.type == MarkedText::DraggedContent); |
| if (markedText.type == MarkedText::DraggedContent) |
| context.setAlpha(markedText.style.alpha); |
| updateGraphicsContext(context, markedText.style.textStyles); |
| |
| if constexpr (std::is_same_v<TextBoxPath, InlineIterator::BoxLegacyPath>) |
| textPainter.setGlyphDisplayListIfNeeded(downcast<LegacyInlineTextBox>(*textBox().legacyInlineBox()), m_paintInfo, m_paintTextRun); |
| #if ENABLE(LAYOUT_FORMATTING_CONTEXT) |
| else |
| textPainter.setGlyphDisplayListIfNeeded(textBox().box(), m_paintInfo, m_paintTextRun); |
| #endif |
| |
| // TextPainter wants the box rectangle and text origin of the entire line box. |
| textPainter.paintRange(m_paintTextRun, m_paintRect, textOriginFromPaintRect(m_paintRect), markedText.startOffset, markedText.endOffset); |
| } |
| |
| template<typename TextBoxPath> |
| TextDecorationPainter TextBoxPainter<TextBoxPath>::createDecorationPainter(const StyledMarkedText& markedText, const FloatRect& clipOutRect, const FloatRect& snappedSelectionRect) |
| { |
| GraphicsContext& context = m_paintInfo.context(); |
| |
| updateGraphicsContext(context, markedText.style.textStyles); |
| |
| // Note that if the text is truncated, we let the thing being painted in the truncation |
| // draw its own decoration. |
| GraphicsContextStateSaver stateSaver { context, false }; |
| bool isDraggedContent = markedText.type == MarkedText::DraggedContent; |
| if (isDraggedContent || !clipOutRect.isEmpty()) { |
| stateSaver.save(); |
| if (isDraggedContent) |
| context.setAlpha(markedText.style.alpha); |
| if (!clipOutRect.isEmpty()) |
| context.clipOut(clipOutRect); |
| } |
| |
| // Create painter |
| const FontCascade& font = fontCascade(); |
| auto textDecorations = m_style.textDecorationsInEffect(); |
| textDecorations.add(TextDecorationPainter::textDecorationsInEffectForStyle(markedText.style.textDecorationStyles)); |
| TextDecorationPainter decorationPainter { context, textDecorations, m_renderer, m_isFirstLine, font, markedText.style.textDecorationStyles }; |
| decorationPainter.setTextBox(makeIterator()); |
| decorationPainter.setWidth(snappedSelectionRect.width()); |
| decorationPainter.setIsHorizontal(textBox().isHorizontal()); |
| if (markedText.style.textShadow) { |
| decorationPainter.setTextShadow(&markedText.style.textShadow.value()); |
| if (m_style.hasAppleColorFilter()) |
| decorationPainter.setShadowColorFilter(&m_style.appleColorFilter()); |
| } |
| |
| return decorationPainter; |
| } |
| |
| template<typename TextBoxPath> |
| void TextBoxPainter<TextBoxPath>::paintBackgroundDecorations(TextDecorationPainter& decorationPainter, const StyledMarkedText& markedText, const FloatRect& snappedSelectionRect) |
| { |
| if (m_isCombinedText) |
| m_paintInfo.context().concatCTM(rotation(m_paintRect, Clockwise)); |
| |
| decorationPainter.paintBackgroundDecorations(m_paintTextRun.subRun(markedText.startOffset, markedText.endOffset - markedText.startOffset), textOriginFromPaintRect(snappedSelectionRect), snappedSelectionRect.location()); |
| |
| if (m_isCombinedText) |
| m_paintInfo.context().concatCTM(rotation(m_paintRect, Counterclockwise)); |
| } |
| |
| template<typename TextBoxPath> |
| void TextBoxPainter<TextBoxPath>::paintForegroundDecorations(TextDecorationPainter& decorationPainter, const FloatRect& snappedSelectionRect) |
| { |
| if (m_isCombinedText) |
| m_paintInfo.context().concatCTM(rotation(m_paintRect, Clockwise)); |
| |
| decorationPainter.paintForegroundDecorations(snappedSelectionRect.location()); |
| |
| if (m_isCombinedText) |
| m_paintInfo.context().concatCTM(rotation(m_paintRect, Counterclockwise)); |
| } |
| |
| template<typename TextBoxPath> |
| void TextBoxPainter<TextBoxPath>::paintCompositionUnderlines() |
| { |
| for (auto& underline : m_renderer.frame().editor().customCompositionUnderlines()) { |
| if (underline.endOffset <= textBox().start()) { |
| // Underline is completely before this run. This might be an underline that sits |
| // before the first run we draw, or underlines that were within runs we skipped |
| // due to truncation. |
| continue; |
| } |
| |
| if (underline.startOffset >= textBox().end()) |
| break; // Underline is completely after this run, bail. A later run will paint it. |
| |
| // Underline intersects this run. Paint it. |
| paintCompositionUnderline(underline); |
| |
| if (underline.endOffset > textBox().end()) |
| break; // Underline also runs into the next run. Bail now, no more marker advancement. |
| } |
| } |
| |
| static inline void mirrorRTLSegment(float logicalWidth, TextDirection direction, float& start, float width) |
| { |
| if (direction == TextDirection::LTR) |
| return; |
| start = logicalWidth - width - start; |
| } |
| |
| template<typename TextBoxPath> |
| float TextBoxPainter<TextBoxPath>::textPosition() |
| { |
| // When computing the width of a text run, RenderBlock::computeInlineDirectionPositionsForLine() doesn't include the actual offset |
| // from the containing block edge in its measurement. textPosition() should be consistent so the text are rendered in the same width. |
| if (!m_logicalRect.x()) |
| return 0; |
| return m_logicalRect.x() - makeIterator()->lineBox()->contentLogicalLeft(); |
| } |
| |
| template<typename TextBoxPath> |
| void TextBoxPainter<TextBoxPath>::paintCompositionUnderline(const CompositionUnderline& underline) |
| { |
| float start = 0; // start of line to draw, relative to tx |
| float width = m_logicalRect.width(); // how much line to draw |
| bool useWholeWidth = true; |
| unsigned paintStart = textBox().start(); |
| unsigned paintEnd = textBox().end(); |
| if (paintStart <= underline.startOffset) { |
| paintStart = underline.startOffset; |
| useWholeWidth = false; |
| start = m_renderer.width(textBox().start(), paintStart - textBox().start(), textPosition(), m_isFirstLine); |
| } |
| if (paintEnd != underline.endOffset) { |
| paintEnd = std::min(paintEnd, (unsigned)underline.endOffset); |
| useWholeWidth = false; |
| } |
| if (m_selectableRange.truncation) { |
| paintEnd = std::min(paintEnd, textBox().start() + *m_selectableRange.truncation); |
| useWholeWidth = false; |
| } |
| if (!useWholeWidth) { |
| width = m_renderer.width(paintStart, paintEnd - paintStart, textPosition() + start, m_isFirstLine); |
| mirrorRTLSegment(m_logicalRect.width(), textBox().direction(), start, width); |
| } |
| |
| // Thick marked text underlines are 2px thick as long as there is room for the 2px line under the baseline. |
| // All other marked text underlines are 1px thick. |
| // If there's not enough space the underline will touch or overlap characters. |
| int lineThickness = 1; |
| int baseline = m_style.metricsOfPrimaryFont().ascent(); |
| if (underline.thick && m_logicalRect.height() - baseline >= 2) |
| lineThickness = 2; |
| |
| // We need to have some space between underlines of subsequent clauses, because some input methods do not use different underline styles for those. |
| // We make each line shorter, which has a harmless side effect of shortening the first and last clauses, too. |
| start += 1; |
| width -= 2; |
| |
| auto& style = m_renderer.style(); |
| Color underlineColor = underline.compositionUnderlineColor == CompositionUnderlineColor::TextColor ? style.visitedDependentColorWithColorFilter(CSSPropertyWebkitTextFillColor) : style.colorByApplyingColorFilter(underline.color); |
| |
| GraphicsContext& context = m_paintInfo.context(); |
| context.setStrokeColor(underlineColor); |
| context.setStrokeThickness(lineThickness); |
| context.drawLineForText(FloatRect(m_paintRect.x() + start, m_paintRect.y() + m_logicalRect.height() - lineThickness, width, lineThickness), m_isPrinting); |
| } |
| |
| template<typename TextBoxPath> |
| void TextBoxPainter<TextBoxPath>::paintPlatformDocumentMarkers() |
| { |
| auto markedTexts = MarkedText::collectForDocumentMarkers(m_renderer, m_selectableRange, MarkedText::PaintPhase::Decoration); |
| for (auto& markedText : MarkedText::subdivide(markedTexts, MarkedText::OverlapStrategy::Frontmost)) |
| paintPlatformDocumentMarker(markedText); |
| } |
| |
| FloatRect LegacyTextBoxPainter::calculateUnionOfAllDocumentMarkerBounds(const LegacyInlineTextBox& textBox) |
| { |
| // This must match paintPlatformDocumentMarkers(). |
| FloatRect result; |
| auto markedTexts = MarkedText::collectForDocumentMarkers(textBox.renderer(), textBox.selectableRange(), MarkedText::PaintPhase::Decoration); |
| for (auto& markedText : MarkedText::subdivide(markedTexts, MarkedText::OverlapStrategy::Frontmost)) |
| result = unionRect(result, calculateDocumentMarkerBounds(InlineIterator::textBoxFor(&textBox), markedText)); |
| return result; |
| } |
| |
| template<typename TextBoxPath> |
| void TextBoxPainter<TextBoxPath>::paintPlatformDocumentMarker(const MarkedText& markedText) |
| { |
| // Never print spelling/grammar markers (5327887) |
| if (m_document.printing()) |
| return; |
| |
| auto bounds = calculateDocumentMarkerBounds(makeIterator(), markedText); |
| |
| auto lineStyleForMarkedTextType = [&]() -> DocumentMarkerLineStyle { |
| bool shouldUseDarkAppearance = m_renderer.useDarkAppearance(); |
| switch (markedText.type) { |
| case MarkedText::SpellingError: |
| return { DocumentMarkerLineStyle::Mode::Spelling, shouldUseDarkAppearance }; |
| case MarkedText::GrammarError: |
| return { DocumentMarkerLineStyle::Mode::Grammar, shouldUseDarkAppearance }; |
| case MarkedText::Correction: |
| return { DocumentMarkerLineStyle::Mode::AutocorrectionReplacement, shouldUseDarkAppearance }; |
| case MarkedText::DictationAlternatives: |
| return { DocumentMarkerLineStyle::Mode::DictationAlternatives, shouldUseDarkAppearance }; |
| #if PLATFORM(IOS_FAMILY) |
| case MarkedText::DictationPhraseWithAlternatives: |
| // FIXME: Rename DocumentMarkerLineStyle::TextCheckingDictationPhraseWithAlternatives and remove the PLATFORM(IOS_FAMILY)-guard. |
| return { DocumentMarkerLineStyle::Mode::TextCheckingDictationPhraseWithAlternatives, shouldUseDarkAppearance }; |
| #endif |
| default: |
| ASSERT_NOT_REACHED(); |
| return { DocumentMarkerLineStyle::Mode::Spelling, shouldUseDarkAppearance }; |
| } |
| }; |
| |
| bounds.moveBy(m_paintRect.location()); |
| m_paintInfo.context().drawDotsForDocumentMarker(bounds, lineStyleForMarkedTextType()); |
| } |
| |
| template<typename TextBoxPath> |
| FloatRect TextBoxPainter<TextBoxPath>::computePaintRect(const LayoutPoint& paintOffset) |
| { |
| FloatPoint localPaintOffset(paintOffset); |
| |
| if (m_selectableRange.truncation) { |
| if (m_renderer.containingBlock()->style().direction() != textBox().direction()) { |
| // Make the visible fragment of text hug the edge closest to the rest of the run by moving the origin |
| // at which we start drawing text. |
| // e.g. In the case of LTR text truncated in an RTL Context, the correct behavior is: |
| // |Hello|CBA| -> |...He|CBA| |
| // In order to draw the fragment "He" aligned to the right edge of it's box, we need to start drawing |
| // farther to the right. |
| // NOTE: WebKit's behavior differs from that of IE which appears to just overlay the ellipsis on top of the |
| // truncated string i.e. |Hello|CBA| -> |...lo|CBA| |
| LayoutUnit widthOfVisibleText { m_renderer.width(textBox().start(), *m_selectableRange.truncation, textPosition(), m_isFirstLine) }; |
| LayoutUnit widthOfHiddenText { m_logicalRect.width() - widthOfVisibleText }; |
| LayoutSize truncationOffset(textBox().direction() == TextDirection::LTR ? widthOfHiddenText : -widthOfHiddenText, 0_lu); |
| localPaintOffset.move(textBox().isHorizontal() ? truncationOffset : truncationOffset.transposedSize()); |
| } |
| } |
| |
| localPaintOffset.move(0, m_style.isHorizontalWritingMode() ? 0 : -m_logicalRect.height()); |
| |
| auto visualRect = textBox().visualRectIgnoringBlockDirection(); |
| textBox().containingBlock().flipForWritingMode(visualRect); |
| |
| auto boxOrigin = visualRect.location(); |
| boxOrigin.moveBy(localPaintOffset); |
| return { boxOrigin, FloatSize(m_logicalRect.width(), m_logicalRect.height()) }; |
| } |
| |
| FloatRect calculateDocumentMarkerBounds(const InlineIterator::TextBoxIterator& textBox, const MarkedText& markedText) |
| { |
| auto& font = textBox->fontCascade(); |
| auto ascent = font.metricsOfPrimaryFont().ascent(); |
| auto fontSize = std::min(std::max(font.size(), 10.0f), 40.0f); |
| auto y = ascent + 0.11035 * fontSize; |
| auto height = 0.13247 * fontSize; |
| |
| // Avoid measuring the text when the entire line box is selected as an optimization. |
| if (markedText.startOffset || markedText.endOffset != textBox->selectableRange().clamp(textBox->end())) { |
| TextRun run = textBox->createTextRun(); |
| LayoutRect selectionRect = LayoutRect(0, y, 0, height); |
| font.adjustSelectionRectForText(run, selectionRect, markedText.startOffset, markedText.endOffset); |
| return selectionRect; |
| } |
| |
| return FloatRect(0, y, textBox->logicalWidth(), height); |
| } |
| |
| template<typename TextBoxPath> |
| bool TextBoxPainter<TextBoxPath>::computeHaveSelection() const |
| { |
| if (m_isPrinting || m_paintInfo.phase == PaintPhase::TextClip) |
| return false; |
| |
| return m_renderer.view().selection().highlightStateForTextBox(m_renderer, m_selectableRange) != RenderObject::HighlightState::None; |
| } |
| |
| template<typename TextBoxPath> |
| const FontCascade& TextBoxPainter<TextBoxPath>::fontCascade() const |
| { |
| if (m_isCombinedText) |
| return downcast<RenderCombineText>(m_renderer).textCombineFont(); |
| |
| return m_textBox.style().fontCascade(); |
| } |
| |
| template<typename TextBoxPath> |
| FloatPoint TextBoxPainter<TextBoxPath>::textOriginFromPaintRect(const FloatRect& paintRect) const |
| { |
| FloatPoint textOrigin { paintRect.x(), paintRect.y() + fontCascade().metricsOfPrimaryFont().ascent() }; |
| if (m_isCombinedText) { |
| if (auto newOrigin = downcast<RenderCombineText>(m_renderer).computeTextOrigin(paintRect)) |
| textOrigin = newOrigin.value(); |
| } |
| if (textBox().isHorizontal()) |
| textOrigin.setY(roundToDevicePixel(LayoutUnit { textOrigin.y() }, m_renderer.document().deviceScaleFactor())); |
| else |
| textOrigin.setX(roundToDevicePixel(LayoutUnit { textOrigin.x() }, m_renderer.document().deviceScaleFactor())); |
| return textOrigin; |
| } |
| |
| template<typename TextBoxPath> |
| const ShadowData* TextBoxPainter<TextBoxPath>::debugTextShadow() const |
| { |
| if (!m_renderer.settings().legacyLineLayoutVisualCoverageEnabled()) |
| return nullptr; |
| #if ENABLE(LAYOUT_FORMATTING_CONTEXT) |
| if constexpr (std::is_same_v<TextBoxPath, InlineIterator::BoxModernPath>) |
| return nullptr; |
| #endif |
| |
| static NeverDestroyed<ShadowData> debugTextShadow(LengthPoint(Length(LengthType::Fixed), Length(LengthType::Fixed)), Length(10, LengthType::Fixed), Length(20, LengthType::Fixed), ShadowStyle::Normal, true, SRGBA<uint8_t> { 150, 0, 0, 190 }); |
| return &debugTextShadow.get(); |
| } |
| |
| #if ENABLE(LAYOUT_FORMATTING_CONTEXT) |
| template class TextBoxPainter<InlineIterator::BoxModernPath>; |
| #endif |
| template class TextBoxPainter<InlineIterator::BoxLegacyPath>; |
| |
| } |