| /* |
| * Copyright (C) 2021-2023 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 "CaretRectComputation.h" |
| #include "CompositionHighlight.h" |
| #include "DocumentMarkerController.h" |
| #include "Editor.h" |
| #include "EventRegion.h" |
| #include "GraphicsContext.h" |
| #include "HTMLAnchorElement.h" |
| #include "InlineIteratorBoxInlines.h" |
| #include "InlineIteratorLineBox.h" |
| #include "InlineIteratorTextBoxInlines.h" |
| #include "InlineTextBoxStyle.h" |
| #include "LineSelection.h" |
| #include "PaintInfo.h" |
| #include "RenderBlock.h" |
| #include "RenderBoxModelObjectInlines.h" |
| #include "RenderCombineText.h" |
| #include "RenderElementStyleInlines.h" |
| #include "RenderElementInlines.h" |
| #include "RenderObjectInlines.h" |
| #include "RenderText.h" |
| #include "RenderTheme.h" |
| #include "RenderView.h" |
| #include "RenderedDocumentMarker.h" |
| #include "Settings.h" |
| #include "StyleTextDecorationLine.h" |
| #include "StyleTextDecorationThickness.h" |
| #include "StyledMarkedText.h" |
| #include "TextPaintStyle.h" |
| #include "TextPainter.h" |
| #include <ranges> |
| |
| #if ENABLE(WRITING_TOOLS) |
| #include "GraphicsContextCG.h" |
| #endif |
| |
| namespace WebCore { |
| |
| // This is temporary and will be removed when subpixel inline layout is enabled. |
| static float snap(float value, const RenderText& renderer) |
| { |
| return renderer.settings().subpixelInlineLayoutEnabled() ? value : roundf(value); |
| } |
| |
| static FloatRect calculateDocumentMarkerBounds(const InlineIterator::TextBoxIterator&, const MarkedText&); |
| |
| static std::optional<bool> emphasisMarkExistsAndIsAbove(const RenderText& renderer, const RenderStyle& style) |
| { |
| // This function returns true if there are text emphasis marks and they are suppressed by ruby text. |
| if (style.textEmphasisStyle().isNone()) |
| return { }; |
| |
| auto emphasisPosition = style.textEmphasisPosition(); |
| bool isAbove = !emphasisPosition.contains(Style::TextEmphasisPositionValue::Under); |
| if (style.writingMode().isVerticalTypographic()) |
| isAbove = !emphasisPosition.contains(Style::TextEmphasisPositionValue::Left); |
| |
| auto findRubyAnnotation = [&]() -> RenderBlockFlow* { |
| for (auto* baseCandidate = renderer.parent(); baseCandidate; baseCandidate = baseCandidate->parent()) { |
| if (!baseCandidate->isInline()) |
| return { }; |
| if (baseCandidate->style().display() == DisplayType::RubyBase) { |
| if (auto* annotationCandidate = dynamicDowncast<RenderBlockFlow>(baseCandidate->nextSibling()); annotationCandidate && annotationCandidate->style().display() == DisplayType::RubyAnnotation) |
| return annotationCandidate; |
| return { }; |
| } |
| } |
| return { }; |
| }; |
| |
| if (auto* annotation = findRubyAnnotation()) { |
| // The emphasis marks are suppressed only if there is a ruby annotation box on the same side and it is not empty. |
| if (annotation->hasContentfulInlineLine() && isAbove == (annotation->style().rubyPosition() == RubyPosition::Over)) |
| return { }; |
| } |
| |
| return isAbove; |
| } |
| |
| struct ShapedContent { |
| StringBuilder text; |
| float visualLeft { 0.f }; // visual left of the shaped content. |
| size_t textBoxStartOffset { 0 }; // text box's position relative to the shaped content. |
| float textBoxVisualLeft { 0.f }; // text box's left relative to the visual left of the shaped content. |
| }; |
| static void buildTextForShaping(ShapedContent& shapedContent, InlineIterator::BoxModernPath textBox, bool needsTextBoxVisualLeft = false) |
| { |
| ASSERT(textBox.direction() == TextDirection::RTL); |
| |
| auto shapingBoundaryIterator = textBox; |
| // 1. Find shaping boundary start when we are at the end or inside a shape range (note that we deal with |
| // rtl content hence the opposite direction walk) |
| // 2. Walk from the start to the end and build the text content. |
| |
| auto moveToShapingBoundaryStart = [&] { |
| if (shapingBoundaryIterator.box().text().isAtShapingBoundaryStart()) |
| return; |
| |
| shapingBoundaryIterator.traverseNextLeafOnLine(); |
| for (; !shapingBoundaryIterator.atEnd(); shapingBoundaryIterator.traverseNextLeafOnLine()) { |
| auto& displayBox = shapingBoundaryIterator.box(); |
| if (displayBox.isText()) { |
| shapedContent.textBoxStartOffset += displayBox.text().length(); |
| if (displayBox.text().isAtShapingBoundaryStart()) |
| break; |
| } |
| } |
| }; |
| moveToShapingBoundaryStart(); |
| |
| if (shapingBoundaryIterator.atEnd() || !shapingBoundaryIterator.isText()) { |
| ASSERT_NOT_REACHED(); |
| return; |
| } |
| |
| auto buildTextContent = [&] { |
| for (; !shapingBoundaryIterator.atEnd(); shapingBoundaryIterator.traversePreviousLeafOnLine()) { |
| auto& displayBox = shapingBoundaryIterator.box(); |
| if (!displayBox.isText()) |
| continue; |
| auto& text = displayBox.text(); |
| if (shapingBoundaryIterator.direction() == TextDirection::LTR) { |
| shapedContent.text.clear(); |
| return; |
| } |
| |
| shapedContent.text.append(text.renderedContent()); |
| if (text.isAtShapingBoundaryEnd()) { |
| shapedContent.visualLeft = displayBox.visualRectIgnoringBlockDirection().x(); |
| return; |
| } |
| } |
| // We should always find the boundary end. |
| ASSERT_NOT_REACHED(); |
| shapedContent.text.clear(); |
| }; |
| buildTextContent(); |
| |
| if (shapedContent.text.isEmpty()) { |
| ASSERT_NOT_REACHED(); |
| return; |
| } |
| |
| auto computeVisualLeftForTextBox = [&] { |
| if (!needsTextBoxVisualLeft) |
| return; |
| // Starting from visual left, walk all the way to the current text box. |
| for (; !shapingBoundaryIterator.atEnd(); shapingBoundaryIterator.traverseNextLeafOnLine()) { |
| if (shapingBoundaryIterator == textBox) |
| return; |
| auto& displayBox = shapingBoundaryIterator.box(); |
| if (displayBox.isText()) |
| shapedContent.textBoxVisualLeft += displayBox.visualRectIgnoringBlockDirection().width(); |
| } |
| }; |
| computeVisualLeftForTextBox(); |
| } |
| |
| TextBoxPainter::TextBoxPainter(const LayoutIntegration::InlineContent& inlineContent, const InlineDisplay::Box& box, const RenderStyle& style, PaintInfo& paintInfo, const LayoutPoint& paintOffset) |
| : m_textBox(InlineIterator::BoxModernPath { inlineContent, inlineContent.indexForBox(box) }) |
| , m_renderer(downcast<RenderText>(m_textBox.renderer())) |
| , m_document(m_renderer->document()) |
| , m_style(style) |
| , m_logicalRect(m_textBox.isHorizontal() ? m_textBox.visualRectIgnoringBlockDirection() : m_textBox.visualRectIgnoringBlockDirection().transposedRect()) |
| , m_paintTextRun(m_textBox.textRun()) |
| , m_paintInfo(paintInfo) |
| , m_selectableRange(m_textBox.selectableRange()) |
| , m_paintOffset(paintOffset) |
| , m_paintRect(computePaintRect(paintOffset)) |
| , m_isFirstLine(m_textBox.isFirstFormattedLine()) |
| , m_isCombinedText([&] { |
| auto* combineTextRenderer = dynamicDowncast<RenderCombineText>(m_renderer.get()); |
| return combineTextRenderer && combineTextRenderer->isCombined(); |
| }()) |
| , m_isPrinting(m_document->printing()) |
| , m_haveSelection(computeHaveSelection()) |
| { |
| ASSERT(paintInfo.phase == PaintPhase::Foreground || paintInfo.phase == PaintPhase::Selection || paintInfo.phase == PaintPhase::TextClip || paintInfo.phase == PaintPhase::EventRegion || paintInfo.phase == PaintPhase::Accessibility); |
| |
| Ref editor = m_renderer->frame().editor(); |
| m_containsComposition = m_renderer->textNode() && editor->compositionNode() == m_renderer->textNode(); |
| m_compositionWithCustomUnderlines = m_containsComposition && editor->compositionUsesCustomUnderlines(); |
| } |
| |
| TextBoxPainter::~TextBoxPainter() = default; |
| |
| InlineIterator::TextBoxIterator TextBoxPainter::makeIterator() const |
| { |
| auto pathCopy = m_textBox; |
| return InlineIterator::TextBoxIterator { WTF::move(pathCopy) }; |
| } |
| |
| void TextBoxPainter::paint() |
| { |
| if (m_paintInfo.paintBehavior.contains(PaintBehavior::ExcludeText)) |
| return; |
| |
| if (m_paintInfo.phase == PaintPhase::Selection && !m_haveSelection) |
| return; |
| |
| if (m_paintInfo.phase == PaintPhase::EventRegion) { |
| constexpr OptionSet<HitTestRequest::Type> hitType { HitTestRequest::Type::IgnoreCSSPointerEventsProperty }; |
| if (m_renderer->parent()->visibleToHitTesting(hitType)) |
| m_paintInfo.eventRegionContext()->unite(FloatRoundedRect(m_paintRect), m_renderer, m_style); |
| return; |
| } |
| |
| std::optional<RotationDirection> glyphRotation; |
| if (!textBox().isHorizontal() && !m_isCombinedText) { |
| glyphRotation = textBox().writingMode().isLineOverLeft() |
| ? RotationDirection::Counterclockwise |
| : RotationDirection::Clockwise; |
| m_paintInfo.context().concatCTM(rotation(m_paintRect, *glyphRotation)); |
| } |
| |
| if (m_paintInfo.phase == PaintPhase::Accessibility) { |
| if (glyphRotation) { |
| auto transform = rotation(m_paintRect, *glyphRotation); |
| m_paintInfo.accessibilityRegionContext()->takeBounds(m_renderer, transform.mapRect(m_paintRect), textBox().lineIndex()); |
| } else |
| m_paintInfo.accessibilityRegionContext()->takeBounds(m_renderer, m_paintRect, textBox().lineIndex()); |
| |
| return; |
| } |
| |
| if (m_paintInfo.phase == PaintPhase::Foreground) { |
| auto shouldPaintBackgroundFill = [&] { |
| if (m_isPrinting) |
| return false; |
| #if ENABLE(TEXT_SELECTION) |
| if (m_haveSelection && !m_compositionWithCustomUnderlines) |
| return true; |
| #endif |
| if (m_containsComposition && !m_compositionWithCustomUnderlines) |
| return true; |
| if (CheckedPtr markers = m_document->markersIfExists(); markers && markers->hasMarkers()) |
| return true; |
| if (m_document->hasHighlight()) |
| return true; |
| return false; |
| }; |
| if (shouldPaintBackgroundFill()) |
| paintBackgroundFill(); |
| |
| paintPlatformDocumentMarkers(); |
| } |
| |
| paintForegroundAndDecorations(); |
| |
| if (m_paintInfo.phase == PaintPhase::Foreground) { |
| if (m_compositionWithCustomUnderlines) |
| paintCompositionUnderlines(); |
| |
| m_renderer->page().addRelevantRepaintedObject(m_renderer, enclosingLayoutRect(m_paintRect)); |
| |
| bool isOnlyTextBoxForElement = [&]() { |
| if (m_textBox.boxIndex() != 1) |
| return false; |
| auto& content = m_textBox.inlineContent().displayContent(); |
| return content.lines.size() == 1 && content.boxes.size() == 2; |
| }(); |
| |
| m_document->didPaintText(textBox().formattingContextRoot(), textBox().visualRectIgnoringBlockDirection(), isOnlyTextBoxForElement); |
| } |
| |
| if (glyphRotation) { |
| auto backRotation = *glyphRotation == RotationDirection::Clockwise |
| ? RotationDirection::Counterclockwise |
| : RotationDirection::Clockwise; |
| m_paintInfo.context().concatCTM(rotation(m_paintRect, backRotation)); |
| } |
| } |
| |
| std::pair<unsigned, unsigned> TextBoxPainter::selectionStartEnd() const |
| { |
| return m_renderer->view().selection().rangeForTextBox(m_renderer, m_selectableRange); |
| } |
| |
| MarkedText TextBoxPainter::createMarkedTextFromSelectionInBox() |
| { |
| auto [selectionStart, selectionEnd] = selectionStartEnd(); |
| if (selectionStart < selectionEnd) |
| return { selectionStart, selectionEnd, MarkedText::Type::Selection }; |
| return { }; |
| } |
| |
| void TextBoxPainter::paintCompositionForeground(const StyledMarkedText& markedText) |
| { |
| auto hasCompositionCustomHighlights = [&]() { |
| if (!m_containsComposition) |
| return false; |
| |
| Ref editor = m_renderer->frame().editor(); |
| return editor->compositionUsesCustomHighlights(); |
| }; |
| |
| if (!hasCompositionCustomHighlights()) { |
| paintForeground(markedText); |
| return; |
| } |
| |
| // The highlight ranges must be "packed" so that there is no non-empty interval between |
| // any two adjacent highlight ranges. This is needed since otherwise, `paintForeground` |
| // will not be called in those would-be non-empty intervals. |
| Ref editor = m_renderer->frame().editor(); |
| auto highlights = editor->customCompositionHighlights(); |
| |
| Vector<CompositionHighlight> highlightsWithForeground; |
| highlightsWithForeground.append({ textBox().start(), highlights[0].startOffset, { }, { } }); |
| |
| for (size_t i = 0; i < highlights.size(); ++i) { |
| highlightsWithForeground.append(highlights[i]); |
| if (i != highlights.size() - 1) |
| highlightsWithForeground.append({ highlights[i].endOffset, highlights[i + 1].startOffset, { }, { } }); |
| } |
| |
| highlightsWithForeground.append({ highlights.last().endOffset, textBox().end(), { }, { } }); |
| |
| for (auto& highlight : highlightsWithForeground) { |
| auto style = StyledMarkedText::computeStyleForUnmarkedMarkedText(m_renderer, m_style, m_isFirstLine, m_paintInfo); |
| |
| if (highlight.endOffset <= textBox().start()) |
| continue; |
| |
| if (highlight.startOffset >= textBox().end()) |
| break; |
| |
| auto [clampedStart, clampedEnd] = m_selectableRange.clamp(highlight.startOffset, highlight.endOffset); |
| |
| if (highlight.foregroundColor) |
| style.textStyles.fillColor = *highlight.foregroundColor; |
| |
| paintForeground({ MarkedText { clampedStart, clampedEnd, MarkedText::Type::Unmarked }, style }); |
| |
| if (highlight.endOffset > textBox().end()) |
| break; |
| } |
| } |
| |
| void TextBoxPainter::paintForegroundAndDecorations() |
| { |
| auto shouldPaintSelectionForeground = m_haveSelection && !m_compositionWithCustomUnderlines; |
| auto hasTextDecoration = !m_style->textDecorationLineInEffect().isNone(); |
| auto hasHighlightDecoration = m_document->hasHighlight() && !MarkedText::collectForHighlights(m_renderer, m_selectableRange, MarkedText::PaintPhase::Decoration).isEmpty(); |
| |
| auto hasSpellingOrGrammarDecoration = [&] { |
| auto markedTexts = MarkedText::collectForDocumentMarkers(m_renderer, m_selectableRange, MarkedText::PaintPhase::Decoration); |
| |
| auto hasSpellingError = markedTexts.containsIf([](auto&& markedText) { |
| return markedText.type == MarkedText::Type::SpellingError; |
| }); |
| |
| if (hasSpellingError) { |
| auto spellingErrorStyle = m_renderer->spellingErrorPseudoStyle(); |
| if (spellingErrorStyle) |
| return !spellingErrorStyle->textDecorationLineInEffect().isNone(); |
| } |
| |
| auto hasGrammarError = markedTexts.containsIf([](auto&& markedText) { |
| return markedText.type == MarkedText::Type::GrammarError; |
| }); |
| |
| if (hasGrammarError) { |
| auto grammarErrorStyle = m_renderer->grammarErrorPseudoStyle(); |
| if (grammarErrorStyle) |
| return !grammarErrorStyle->textDecorationLineInEffect().isNone(); |
| } |
| |
| return false; |
| }; |
| |
| auto hasDecoration = hasTextDecoration || hasHighlightDecoration || hasSpellingOrGrammarDecoration(); |
| |
| auto contentMayNeedStyledMarkedText = [&] { |
| if (hasDecoration) |
| return true; |
| if (shouldPaintSelectionForeground) |
| return true; |
| if (CheckedPtr markers = m_document->markersIfExists(); markers && markers->hasMarkers()) |
| return true; |
| if (m_document->hasHighlight()) |
| return true; |
| return false; |
| }; |
| auto startPosition = m_selectableRange.clamp(textBox().start()); |
| auto endPosition = m_selectableRange.clamp(textBox().end()); |
| |
| if (!contentMayNeedStyledMarkedText()) { |
| auto markedText = MarkedText { startPosition, endPosition, MarkedText::Type::Unmarked }; |
| auto styledMarkedText = StyledMarkedText { markedText, StyledMarkedText::computeStyleForUnmarkedMarkedText(m_renderer, m_style, m_isFirstLine, m_paintInfo) }; |
| paintCompositionForeground(styledMarkedText); |
| 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({ startPosition, endPosition, MarkedText::Type::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::collectForDraggedAndTransparentContent(DocumentMarkerType::DraggedContent, m_renderer, m_selectableRange); |
| if (!markedTextsForDraggedContent.isEmpty()) { |
| shouldPaintSelectionForeground = false; |
| markedTexts.appendVector(WTF::move(markedTextsForDraggedContent)); |
| } |
| } |
| auto markedTextsForTransparentContent = MarkedText::collectForDraggedAndTransparentContent(DocumentMarkerType::TransparentContent, m_renderer, m_selectableRange); |
| if (!markedTextsForTransparentContent.isEmpty()) |
| markedTexts.appendVector(WTF::move(markedTextsForTransparentContent)); |
| } |
| } |
| // 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(WTF::move(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::Type::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) = selectionStartEnd(); |
| |
| 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, 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. |
| auto snappedPaintRect = snapRectToDevicePixelsWithWritingDirection(LayoutRect { m_paintRect }, m_document->deviceScaleFactor(), m_paintTextRun.ltr()); |
| if (startOffset || endOffset != m_paintTextRun.length()) { |
| LayoutRect selectionRect = { m_paintRect.x(), m_paintRect.y(), m_paintRect.width(), m_paintRect.height() }; |
| fontCascade().adjustSelectionRectForText(m_renderer->canUseSimplifiedTextMeasuring().value_or(false), m_paintTextRun, selectionRect, startOffset, endOffset); |
| snappedPaintRect = snapRectToDevicePixelsWithWritingDirection(selectionRect, m_document->deviceScaleFactor(), m_paintTextRun.ltr()); |
| } |
| auto decorationPainter = createDecorationPainter(markedText, textDecorationSelectionClipOutRect); |
| paintBackgroundDecorations(decorationPainter, markedText, snappedPaintRect); |
| paintCompositionForeground(markedText); |
| paintForegroundDecorations(decorationPainter, markedText, snappedPaintRect); |
| } |
| } |
| } else { |
| // Coalesce styles of adjacent marked texts to minimize the number of drawing commands. |
| auto coalescedStyledMarkedTexts = StyledMarkedText::coalesceAdjacentWithEqualForeground(styledMarkedTexts); |
| |
| if (coalescedStyledMarkedTexts.isEmpty()) |
| return; |
| |
| for (auto& markedText : coalescedStyledMarkedTexts) |
| paintCompositionForeground(markedText); |
| } |
| } |
| |
| void TextBoxPainter::paintBackgroundFill() |
| { |
| if (m_containsComposition && !m_compositionWithCustomUnderlines) { |
| Ref editor = m_renderer->frame().editor(); |
| |
| if (editor->compositionUsesCustomHighlights()) { |
| for (auto& highlight : editor->customCompositionHighlights()) { |
| if (!highlight.backgroundColor) |
| continue; |
| |
| if (highlight.endOffset <= textBox().start()) |
| continue; |
| |
| if (highlight.startOffset >= textBox().end()) |
| break; |
| |
| auto [clampedStart, clampedEnd] = m_selectableRange.clamp(highlight.startOffset, highlight.endOffset); |
| paintBackgroundFillForRange(clampedStart, clampedEnd, *highlight.backgroundColor, BackgroundStyle::Rounded); |
| |
| if (highlight.endOffset > textBox().end()) |
| break; |
| } |
| } else { |
| auto [clampedStart, clampedEnd] = m_selectableRange.clamp(editor->compositionStart(), editor->compositionEnd()); |
| paintBackgroundFillForRange(clampedStart, clampedEnd, CompositionHighlight::defaultCompositionFillColor, BackgroundStyle::Normal); |
| } |
| } |
| |
| 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) |
| auto hasSelectionWithNonCustomUnderline = m_haveSelection && !m_compositionWithCustomUnderlines; |
| if (hasSelectionWithNonCustomUnderline && !m_paintInfo.context().paintingDisabled()) { |
| auto selectionMarkedText = createMarkedTextFromSelectionInBox(); |
| if (!selectionMarkedText.isEmpty()) |
| markedTexts.append(WTF::move(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) |
| paintBackgroundFillForRange(markedText.startOffset, markedText.endOffset, markedText.style.backgroundColor, BackgroundStyle::Normal); |
| } |
| |
| LayoutRect TextBoxPainter::selectionRectForRange(unsigned startOffset, unsigned endOffset) const |
| { |
| // 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 { writingMode().isLineInverted() ? 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 }; |
| |
| if (isInsideShapedContent()) { |
| auto shapedContent = ShapedContent { }; |
| |
| buildTextForShaping(shapedContent, m_textBox, true); |
| selectionRect.setX(selectionRect.x() - shapedContent.textBoxVisualLeft); |
| auto selectionLength = endOffset - startOffset; |
| auto adjustedStartOffset = shapedContent.textBoxStartOffset + startOffset; |
| |
| auto characterScanForCodePath = true; |
| auto expansion = m_textBox.box().expansion(); |
| auto paintRect = m_paintRect; |
| paintRect.shiftXEdgeTo(shapedContent.visualLeft); |
| auto run = TextRun { shapedContent.text, paintRect.x(), expansion.horizontalExpansion, expansion.behavior, m_textBox.direction(), m_style->rtlOrdering() == Order::Visual, characterScanForCodePath }; |
| |
| fontCascade().adjustSelectionRectForText(false, run, selectionRect, adjustedStartOffset, adjustedStartOffset + selectionLength); |
| return selectionRect; |
| } |
| |
| fontCascade().adjustSelectionRectForText(m_renderer->canUseSimplifiedTextMeasuring().value_or(false), m_paintTextRun, selectionRect, startOffset, endOffset); |
| return selectionRect; |
| } |
| |
| void TextBoxPainter::paintBackgroundFillForRange(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! |
| |
| auto selectionRect = selectionRectForRange(startOffset, endOffset); |
| |
| if (m_paintTextRun.length() == endOffset - startOffset) { |
| // FIXME: We should reconsider re-measuring the content when non-whitespace runs are joined together (see webkit.org/b/251318). |
| auto unAdjustedSelectionRectMaxX = LayoutUnit { m_paintRect.x() + m_logicalRect.width() }; |
| auto visualRight = std::max(selectionRect.maxX(), unAdjustedSelectionRectMaxX); |
| selectionRect.shiftMaxXEdgeTo(visualRight); |
| } |
| |
| // FIXME: Support painting combined text. See <https://bugs.webkit.org/show_bug.cgi?id=180993>. |
| auto backgroundRect = snapRectToDevicePixels(selectionRect, m_document->deviceScaleFactor()); |
| if (backgroundStyle == BackgroundStyle::Rounded) { |
| backgroundRect.expand(-1, -1); |
| backgroundRect.move(0.5, 0.5); |
| context.fillRoundedRect(FloatRoundedRect { backgroundRect, CornerRadii { 2 } }, color); |
| return; |
| } |
| |
| context.fillRect(backgroundRect, color); |
| } |
| |
| void TextBoxPainter::paintForeground(const StyledMarkedText& markedText) |
| { |
| if (markedText.startOffset >= markedText.endOffset) |
| return; |
| |
| auto& context = m_paintInfo.context(); |
| const FontCascade& font = fontCascade(); |
| |
| float emphasisMarkOffset = 0; |
| auto emphasisExistsAndIsAbove = emphasisMarkExistsAndIsAbove(m_renderer, m_style); |
| auto& emphasisMark = emphasisExistsAndIsAbove ? m_style->textEmphasisStyle().markString() : nullAtom(); |
| if (!emphasisMark.isEmpty()) |
| emphasisMarkOffset = *emphasisExistsAndIsAbove ? -snap(font.metricsOfPrimaryFont().ascent(), m_renderer) - font.emphasisMarkDescent(emphasisMark) : snap(font.metricsOfPrimaryFont().descent(), m_renderer) + font.emphasisMarkAscent(emphasisMark); |
| |
| TextPainter textPainter { |
| context, |
| font, |
| m_style, |
| markedText.style.textStyles, |
| markedText.style.textShadow, |
| (!markedText.style.textShadow.isNone() && !m_style->appleColorFilter().isNone()) ? m_style->appleColorFilter() : Style::AppleColorFilter::none(), |
| emphasisMark, |
| emphasisMarkOffset, |
| m_isCombinedText ? &downcast<RenderCombineText>(m_renderer.get()) : nullptr |
| }; |
| |
| bool isTransparentMarkedText = markedText.type == MarkedText::Type::DraggedContent || markedText.type == MarkedText::Type::TransparentContent; |
| GraphicsContextStateSaver stateSaver(context, markedText.style.textStyles.strokeWidth > 0 || isTransparentMarkedText); |
| if (isTransparentMarkedText) |
| context.setAlpha(markedText.style.alpha); |
| updateGraphicsContext(context, markedText.style.textStyles); |
| |
| if (isInsideShapedContent() && paintForegroundForShapeRange(textPainter)) |
| return; |
| textPainter.setGlyphDisplayListIfNeeded(textBox().box(), m_paintInfo, m_style, m_paintTextRun); |
| // 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); |
| } |
| |
| bool TextBoxPainter::paintForegroundForShapeRange(TextPainter& textPainter) |
| { |
| ASSERT(m_document->settings().textShapingAcrossInlineBoxes()); |
| ASSERT(m_textBox.direction() == TextDirection::RTL); |
| |
| auto& context = m_paintInfo.context(); |
| |
| auto clipRect = [&] { |
| // We could also just use ink overflow here but since non-range painting |
| // sets up no clipping, we should not do that either here. |
| auto rect = FloatRect::infiniteRect(); |
| rect.setX(m_paintRect.x()); |
| // Note that this is RTL direction. |
| auto& textContent = m_textBox.box().text(); |
| if (!textContent.isAtShapingBoundaryStart()) |
| rect.setWidth(m_paintRect.width()); |
| // FIXME: Setup a semi-inifite rect for the (visually) first box where x is -inifite with fixed maxX. |
| return rect; |
| }; |
| context.save(); |
| context.clip(clipRect()); |
| auto shapedContent = ShapedContent { }; |
| buildTextForShaping(shapedContent, m_textBox); |
| |
| if (shapedContent.text.isEmpty()) |
| return false; |
| |
| auto paintRect = m_paintRect; |
| paintRect.shiftXEdgeTo(m_paintOffset.x() + shapedContent.visualLeft); |
| |
| auto characterScanForCodePath = true; |
| auto expansion = m_textBox.box().expansion(); |
| auto run = TextRun { shapedContent.text, paintRect.x(), expansion.horizontalExpansion, expansion.behavior, m_textBox.direction(), m_style->rtlOrdering() == Order::Visual, characterScanForCodePath }; |
| run.disableSpacing(); |
| |
| textPainter.paintRange(run, paintRect, textOriginFromPaintRect(paintRect), 0, shapedContent.text.length()); |
| context.restore(); |
| return true; |
| } |
| |
| TextDecorationPainter TextBoxPainter::createDecorationPainter(const StyledMarkedText& markedText, const FloatRect& clipOutRect) |
| { |
| auto& 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 isTransparentContent = markedText.type == MarkedText::Type::DraggedContent || markedText.type == MarkedText::Type::TransparentContent; |
| if (isTransparentContent || !clipOutRect.isEmpty()) { |
| stateSaver.save(); |
| if (isTransparentContent) |
| context.setAlpha(markedText.style.alpha); |
| if (!clipOutRect.isEmpty()) |
| context.clipOut(clipOutRect); |
| } |
| |
| // Create painter |
| return { |
| context, |
| fontCascade(), |
| markedText.style.textShadow, |
| (!markedText.style.textShadow.isNone() && !m_style->appleColorFilter().isNone()) ? m_style->appleColorFilter() : Style::AppleColorFilter::none(), |
| m_document->printing(), |
| writingMode() |
| }; |
| } |
| |
| static inline float computedTextDecorationThickness(const RenderStyle& styleToUse, float deviceScaleFactor) |
| { |
| return ceilToDevicePixel(styleToUse.textDecorationThickness().resolve(styleToUse), deviceScaleFactor); |
| } |
| |
| static inline float computedAutoTextDecorationThickness(const RenderStyle& styleToUse, float deviceScaleFactor) |
| { |
| return ceilToDevicePixel(Style::TextDecorationThickness { CSS::Keyword::Auto { } }.resolve(styleToUse), deviceScaleFactor); |
| } |
| |
| static inline float computedLinethroughCenter(const RenderStyle& styleToUse, float textDecorationThickness, float autoTextDecorationThickness) |
| { |
| auto center = 2 * styleToUse.metricsOfPrimaryFont().ascent() / 3 + autoTextDecorationThickness / 2; |
| return center - textDecorationThickness / 2; |
| } |
| |
| static inline Style::TextDecorationLine computedTextDecorationType(const RenderStyle& style, const TextDecorationPainter::Styles& textDecorationStyles) |
| { |
| auto textDecorations = style.textDecorationLineInEffect(); |
| textDecorations.addOrReplaceIfNotNone(TextDecorationPainter::textDecorationsInEffectForStyle(textDecorationStyles)); |
| return textDecorations; |
| } |
| |
| static inline const RenderStyle& decoratingBoxStyleForInlineBox(const InlineIterator::InlineBox& inlineBox, bool isFirstLine) |
| { |
| if (!inlineBox.isRootInlineBox()) |
| return inlineBox.style(); |
| // "When specified on or propagated to a block container that establishes an inline formatting context, the decorations are propagated to an anonymous |
| // inline box that wraps all the in-flow inline-level children of the block container" |
| // https://drafts.csswg.org/css-text-decor-4/#line-decoration |
| // Sadly we don't have the concept of anonymous inline box for all inline-level chidren when content forces us to generate anonymous block containers. |
| for (const RenderElement* ancestor = &inlineBox.renderer(); ancestor; ancestor = ancestor->parent()) { |
| if (!ancestor->isAnonymous()) |
| return isFirstLine ? ancestor->firstLineStyle() : ancestor->style(); |
| } |
| ASSERT_NOT_REACHED(); |
| return inlineBox.style(); |
| } |
| |
| static inline bool isDecoratingBoxForBackground(const InlineIterator::InlineBox& inlineBox, const RenderStyle& styleToUse) |
| { |
| RefPtr element = inlineBox.renderer().element(); |
| if (element && (is<HTMLAnchorElement>(*element) || element->hasTagName(HTMLNames::fontTag))) { |
| // <font> and <a> are always considered decorating boxes. |
| return true; |
| } |
| return styleToUse.textDecorationLine().containsAny({ Style::TextDecorationLine::Flag::Underline, Style::TextDecorationLine::Flag::Overline }) |
| || (inlineBox.isRootInlineBox() && styleToUse.textDecorationLineInEffect().containsAny({ Style::TextDecorationLine::Flag::Underline, Style::TextDecorationLine::Flag::Overline })); |
| } |
| |
| void TextBoxPainter::collectDecoratingBoxesForBackgroundPainting(DecoratingBoxList& decoratingBoxList, const InlineIterator::TextBoxIterator& textBox, FloatPoint textBoxLocation, const TextDecorationPainter::Styles& overrideDecorationStyle) |
| { |
| auto ancestorInlineBox = textBox->parentInlineBox(); |
| if (!ancestorInlineBox) { |
| ASSERT_NOT_REACHED(); |
| return; |
| } |
| |
| if (ancestorInlineBox->isRootInlineBox()) { |
| decoratingBoxList.append({ ancestorInlineBox, decoratingBoxStyleForInlineBox(*ancestorInlineBox, m_isFirstLine), overrideDecorationStyle, textBoxLocation }); |
| return; |
| } |
| |
| if (writingMode().isLineInverted()) { |
| // FIXME: underlineOffsetForTextBoxPainting returns incorrect value for vertical-lr. |
| decoratingBoxList.append({ ancestorInlineBox, m_style.get(), overrideDecorationStyle, textBoxLocation }); |
| return; |
| } |
| |
| enum UseOverriderDecorationStyle : bool { No, Yes }; |
| auto appendIfIsDecoratingBoxForBackground = [&] (auto& inlineBox, auto useOverriderDecorationStyle) { |
| auto& style = decoratingBoxStyleForInlineBox(*inlineBox, m_isFirstLine); |
| |
| auto computedDecorationStyle = [&] { |
| return TextDecorationPainter::stylesForRenderer(inlineBox->renderer(), style.textDecorationLineInEffect(), m_isFirstLine); |
| }; |
| if (!isDecoratingBoxForBackground(*inlineBox, style)) { |
| // Some cases even non-decoration boxes may have some decoration pieces coming from the marked text (e.g. highlight). |
| if (useOverriderDecorationStyle == UseOverriderDecorationStyle::No || overrideDecorationStyle == computedDecorationStyle()) |
| return; |
| } |
| |
| auto decoratingBoxLocation = textBoxLocation; |
| auto parentInlineBox = textBox->parentInlineBox(); |
| // Normally text box top is aligned with the parent inline box top (i.e. textBox->logicalTop() == parentInlineBox->logicalTop()) but when inline box sides are trimmed (see: text-box property) |
| // inline box gets an offset while text box does not. |
| if (&inlineBox->renderer() != &parentInlineBox->renderer()) { |
| auto decoratingBoxContentBoxTop = inlineBox->logicalTop() + (!inlineBox->isRootInlineBox() ? inlineBox->renderer().borderAndPaddingBefore() : LayoutUnit(0_lu)); |
| auto parentInlineBoxContentBoxTop = parentInlineBox->logicalTop() + (!parentInlineBox->isRootInlineBox() ? parentInlineBox->renderer().borderAndPaddingBefore() : LayoutUnit(0_lu)); |
| decoratingBoxLocation.moveBy(FloatPoint { 0.f, decoratingBoxContentBoxTop - parentInlineBoxContentBoxTop + snap(textBoxEdgeAdjustmentForUnderline(parentInlineBox->style()), m_renderer) }); |
| } else |
| decoratingBoxLocation.moveBy(FloatPoint { 0.f, snap(textBoxEdgeAdjustmentForUnderline(inlineBox->style()), m_renderer) }); |
| auto& decorationStyleToUse = useOverriderDecorationStyle == UseOverriderDecorationStyle::Yes ? overrideDecorationStyle : computedDecorationStyle(); |
| decoratingBoxList.append({ inlineBox, style, decorationStyleToUse, decoratingBoxLocation }); |
| }; |
| |
| // FIXME: Figure out if the decoration styles coming from the styled marked text should be used only on the closest inline box (direct parent). |
| appendIfIsDecoratingBoxForBackground(ancestorInlineBox, UseOverriderDecorationStyle::Yes); |
| while (!ancestorInlineBox->isRootInlineBox()) { |
| ancestorInlineBox = ancestorInlineBox->parentInlineBox(); |
| if (!ancestorInlineBox) { |
| ASSERT_NOT_REACHED(); |
| break; |
| } |
| appendIfIsDecoratingBoxForBackground(ancestorInlineBox, UseOverriderDecorationStyle::No); |
| } |
| } |
| |
| void TextBoxPainter::paintBackgroundDecorations(TextDecorationPainter& decorationPainter, const StyledMarkedText& markedText, const FloatRect& textBoxPaintRect) |
| { |
| if (m_isCombinedText) |
| m_paintInfo.context().concatCTM(rotation(m_paintRect, RotationDirection::Clockwise)); |
| |
| auto textRun = m_paintTextRun.subRun(markedText.startOffset, markedText.endOffset - markedText.startOffset); |
| |
| auto textBox = makeIterator(); |
| auto decoratingBoxList = DecoratingBoxList { }; |
| collectDecoratingBoxesForBackgroundPainting(decoratingBoxList, textBox, textBoxPaintRect.location(), markedText.style.textDecorationStyles); |
| |
| for (auto& decoratingBox : decoratingBoxList | std::views::reverse) { |
| auto computedTextDecorationType = WebCore::computedTextDecorationType(decoratingBox.style.get(), decoratingBox.textDecorationStyles); |
| auto computedBackgroundDecorationGeometry = [&] { |
| auto textDecorationThickness = computedTextDecorationThickness(decoratingBox.style.get(), m_document->deviceScaleFactor()); |
| auto underlineOffset = [&] { |
| if (!computedTextDecorationType.hasUnderline()) |
| return 0.f; |
| auto baseOffset = underlineOffsetForTextBoxPainting(*decoratingBox.inlineBox, decoratingBox.style.get()); |
| auto wavyOffset = decoratingBox.textDecorationStyles.underline.decorationStyle == TextDecorationStyle::Wavy ? wavyOffsetFromDecoration() : 0.f; |
| return baseOffset + wavyOffset; |
| }; |
| auto autoTextDecorationThickness = computedAutoTextDecorationThickness(decoratingBox.style.get(), m_document->deviceScaleFactor()); |
| auto overlineOffset = [&] { |
| if (!computedTextDecorationType.hasOverline()) |
| return 0.f; |
| auto baseOffset = overlineOffsetForTextBoxPainting(*decoratingBox.inlineBox, decoratingBox.style.get()); |
| baseOffset += (autoTextDecorationThickness - textDecorationThickness); |
| auto wavyOffset = decoratingBox.textDecorationStyles.overline.decorationStyle == TextDecorationStyle::Wavy ? wavyOffsetFromDecoration() : 0.f; |
| return baseOffset - wavyOffset; |
| }; |
| |
| return TextDecorationPainter::BackgroundDecorationGeometry { |
| textOriginFromPaintRect(textBoxPaintRect), |
| decoratingBox.location, |
| textBoxPaintRect.width(), |
| textDecorationThickness, |
| underlineOffset(), |
| overlineOffset(), |
| computedLinethroughCenter(decoratingBox.style.get(), textDecorationThickness, autoTextDecorationThickness), |
| snap(decoratingBox.style->metricsOfPrimaryFont().ascent(), m_renderer) + 2.f, |
| wavyStrokeParameters(decoratingBox.style->computedFontSize()) |
| }; |
| }; |
| |
| decorationPainter.paintBackgroundDecorations(m_style, textRun, computedBackgroundDecorationGeometry(), computedTextDecorationType, decoratingBox.textDecorationStyles, m_document->deviceScaleFactor()); |
| } |
| |
| if (m_isCombinedText) |
| m_paintInfo.context().concatCTM(rotation(m_paintRect, RotationDirection::Counterclockwise)); |
| } |
| |
| static const RenderStyle& decoratingBoxStyle(const InlineIterator::TextBoxIterator& textBox) |
| { |
| if (auto parentInlineBox = textBox->parentInlineBox()) |
| return parentInlineBox->style(); |
| ASSERT_NOT_REACHED(); |
| return textBox->style(); |
| } |
| |
| void TextBoxPainter::paintForegroundDecorations(TextDecorationPainter& decorationPainter, const StyledMarkedText& markedText, const FloatRect& textBoxPaintRect) |
| { |
| auto textBox = makeIterator(); |
| auto& styleForDecoration = decoratingBoxStyle(textBox); |
| auto computedTextDecorationType = [&] { |
| auto textDecorations = styleForDecoration.textDecorationLineInEffect(); |
| textDecorations.addOrReplaceIfNotNone(TextDecorationPainter::textDecorationsInEffectForStyle(markedText.style.textDecorationStyles)); |
| return textDecorations; |
| }(); |
| |
| if (!computedTextDecorationType.hasLineThrough()) |
| return; |
| |
| if (m_isCombinedText) |
| m_paintInfo.context().concatCTM(rotation(m_paintRect, RotationDirection::Clockwise)); |
| |
| auto deviceScaleFactor = m_document->deviceScaleFactor(); |
| auto textDecorationThickness = computedTextDecorationThickness(styleForDecoration, deviceScaleFactor); |
| auto linethroughCenter = computedLinethroughCenter(styleForDecoration, textDecorationThickness, computedAutoTextDecorationThickness(styleForDecoration, deviceScaleFactor)); |
| decorationPainter.paintForegroundDecorations({ textBoxPaintRect.location() |
| , textBoxPaintRect.width() |
| , textDecorationThickness |
| , linethroughCenter |
| , wavyStrokeParameters(styleForDecoration.computedFontSize()) }, markedText.style.textDecorationStyles); |
| |
| if (m_isCombinedText) |
| m_paintInfo.context().concatCTM(rotation(m_paintRect, RotationDirection::Counterclockwise)); |
| } |
| |
| static CornerRadii radiiForUnderline(const CompositionUnderline& underline, unsigned markedTextStartOffset, unsigned markedTextEndOffset) |
| { |
| auto radii = CornerRadii { 0 }; |
| |
| #if HAVE(REDESIGNED_TEXT_CURSOR) |
| if (!redesignedTextCursorEnabled()) |
| return radii; |
| |
| if (underline.startOffset >= markedTextStartOffset) { |
| radii.setTopLeft({ 1, 1 }); |
| radii.setBottomLeft({ 1, 1 }); |
| } |
| |
| if (underline.endOffset <= markedTextEndOffset) { |
| radii.setTopRight({ 1, 1 }); |
| radii.setBottomRight({ 1, 1 }); |
| } |
| #else |
| UNUSED_PARAM(underline); |
| UNUSED_PARAM(markedTextStartOffset); |
| UNUSED_PARAM(markedTextEndOffset); |
| #endif |
| |
| return radii; |
| } |
| |
| #if HAVE(REDESIGNED_TEXT_CURSOR) |
| enum class TrimSide : bool { |
| Left, |
| Right, |
| }; |
| |
| static CornerRadii trimRadii(const CornerRadii& radii, TrimSide trimSide) |
| { |
| switch (trimSide) { |
| case TrimSide::Left: |
| return { { }, radii.topRight(), { }, radii.bottomRight() }; |
| case TrimSide::Right: |
| return { radii.topLeft(), { }, radii.bottomLeft(), { } }; |
| } |
| } |
| |
| enum class SnapDirection : uint8_t { |
| Left, |
| Right, |
| Both, |
| }; |
| |
| static FloatRect snapRectToDevicePixelsInDirection(const FloatRect& rect, float deviceScaleFactor, SnapDirection snapDirection) |
| { |
| const auto layoutRect = LayoutRect { rect }; |
| switch (snapDirection) { |
| case SnapDirection::Left: |
| return snapRectToDevicePixelsWithWritingDirection(layoutRect, deviceScaleFactor, true); |
| case SnapDirection::Right: |
| return snapRectToDevicePixelsWithWritingDirection(layoutRect, deviceScaleFactor, false); |
| case SnapDirection::Both: |
| auto snappedRectLeft = snapRectToDevicePixelsWithWritingDirection(layoutRect, deviceScaleFactor, true); |
| return snapRectToDevicePixelsWithWritingDirection(LayoutRect { snappedRectLeft }, deviceScaleFactor, false); |
| } |
| } |
| |
| enum class TextBoxFragmentLocationWithinLayoutBox : uint8_t { First = 1 << 0, Last = 1 << 1 }; |
| static OptionSet<TextBoxFragmentLocationWithinLayoutBox> textBoxFragmentLocationWithinLayoutBox(const InlineIterator::BoxModernPath& textBox) |
| { |
| OptionSet<TextBoxFragmentLocationWithinLayoutBox> location; |
| if (textBox.box().isFirstForLayoutBox()) |
| location.add(TextBoxFragmentLocationWithinLayoutBox::First); |
| if (textBox.box().isLastForLayoutBox()) |
| location.add(TextBoxFragmentLocationWithinLayoutBox::Last); |
| return location; |
| } |
| #endif |
| |
| void TextBoxPainter::fillCompositionUnderline(float start, float width, const CompositionUnderline& underline, const CornerRadii& radii, bool hasLiveConversion) const |
| { |
| #if HAVE(REDESIGNED_TEXT_CURSOR) |
| if (!redesignedTextCursorEnabled()) |
| #endif |
| { |
| // 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 = snap(m_style->metricsOfPrimaryFont().ascent(), m_renderer); |
| 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 underlineColor = underline.compositionUnderlineColor == CompositionUnderlineColor::TextColor |
| ? m_style->visitedDependentTextFillColorApplyingColorFilter() |
| : Style::ColorResolver { m_style }.colorResolvingCurrentColorApplyingColorFilter(underline.color); |
| |
| auto& 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); |
| return; |
| } |
| |
| #if HAVE(REDESIGNED_TEXT_CURSOR) |
| if (!underline.color.isVisible()) |
| return; |
| |
| // 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 = snap(m_style->metricsOfPrimaryFont().ascent(), m_renderer); |
| if (m_logicalRect.height() - baseline >= 2) |
| lineThickness = 2; |
| |
| auto underlineColor = [this] { |
| #if PLATFORM(MAC) |
| auto cssColorValue = CSSValueAppleSystemControlAccent; |
| #else |
| auto cssColorValue = CSSValueAppleSystemBlue; |
| #endif |
| auto styleColorOptions = m_renderer->styleColorOptions(); |
| return RenderTheme::singleton().systemColor(cssColorValue, styleColorOptions | StyleColorOptions::UseSystemAppearance); |
| }(); |
| |
| if (!underline.thick && hasLiveConversion) |
| underlineColor = underlineColor.colorWithAlpha(0.35); |
| |
| auto& context = m_paintInfo.context(); |
| context.setStrokeColor(underlineColor); |
| context.setStrokeThickness(lineThickness); |
| |
| auto rect = FloatRect(m_paintRect.x() + start, m_paintRect.y() + m_logicalRect.height() - lineThickness, width, lineThickness); |
| |
| if (radii.isZero()) { |
| context.drawLineForText(rect, m_isPrinting); |
| return; |
| } |
| |
| // We cannot directly draw rounded edges for every rect, since a single textbox path may be split up over multiple rects. |
| // Drawing rounded edges unconditionally could then produce broken underlines between continuous rects. |
| // As a mitigation, we consult the textbox path to understand the current rect's position in the textbox path. |
| // If we're the only box in the path, then we fallback to unconditionally drawing rounded edges. |
| // If not, we flatten out the right, left, or both edges depending on whether we're at the start, end, or middle of a path, respectively. |
| auto fragmentLocation = textBoxFragmentLocationWithinLayoutBox(m_textBox); |
| auto deviceScaleFactor = m_document->deviceScaleFactor(); |
| if (fragmentLocation.containsAll({ TextBoxFragmentLocationWithinLayoutBox::First, TextBoxFragmentLocationWithinLayoutBox::Last })) |
| context.fillRoundedRect(FloatRoundedRect { rect, radii }, underlineColor); |
| else if (fragmentLocation == TextBoxFragmentLocationWithinLayoutBox::First) |
| context.fillRoundedRect(FloatRoundedRect { snapRectToDevicePixelsInDirection(rect, deviceScaleFactor, SnapDirection::Right), trimRadii(radii, TrimSide::Right) }, underlineColor); |
| else if (fragmentLocation == TextBoxFragmentLocationWithinLayoutBox::Last) |
| context.fillRoundedRect(FloatRoundedRect { snapRectToDevicePixelsInDirection(rect, deviceScaleFactor, SnapDirection::Left), trimRadii(radii, TrimSide::Left) }, underlineColor); |
| else { |
| ASSERT(fragmentLocation.isEmpty()); |
| // This text fragment is right in the middle of the box content. |
| context.fillRect(snapRectToDevicePixelsInDirection(rect, deviceScaleFactor, SnapDirection::Both), underlineColor); |
| } |
| #else |
| UNUSED_PARAM(radii); |
| UNUSED_PARAM(hasLiveConversion); |
| #endif |
| } |
| |
| void TextBoxPainter::paintCompositionUnderlines() |
| { |
| auto& underlines = m_renderer->frame().editor().customCompositionUnderlines(); |
| auto underlineCount = underlines.size(); |
| |
| if (!underlineCount) |
| return; |
| |
| auto hasLiveConversion = false; |
| |
| auto markedTextStartOffset = underlines[0].startOffset; |
| auto markedTextEndOffset = underlines[0].endOffset; |
| |
| for (const auto& underline : underlines) { |
| if (underline.thick) |
| hasLiveConversion = true; |
| |
| if (underline.startOffset < markedTextStartOffset) |
| markedTextStartOffset = underline.startOffset; |
| |
| if (underline.endOffset > markedTextEndOffset) |
| markedTextEndOffset = underline.endOffset; |
| } |
| |
| for (size_t i = 0; i < underlineCount; ++i) { |
| auto& underline = underlines[i]; |
| 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. |
| |
| auto underlineRadii = radiiForUnderline(underline, markedTextStartOffset, markedTextEndOffset); |
| |
| // Underline intersects this run. Paint it. |
| paintCompositionUnderline(underline, underlineRadii, hasLiveConversion); |
| |
| 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; |
| } |
| |
| float TextBoxPainter::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(); |
| } |
| |
| void TextBoxPainter::paintCompositionUnderline(const CompositionUnderline& underline, const CornerRadii& radii, bool hasLiveConversion) |
| { |
| 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); |
| } |
| |
| fillCompositionUnderline(start, width, underline, radii, hasLiveConversion); |
| } |
| |
| static void removeMarkersPaintedByTextDecorationPainter(const RenderText& renderer, Vector<MarkedText>& markedTexts) |
| { |
| // SpellingError marked text that is styled via ::spelling-error is removed from being painted here and it is painted as regular text-decoration at TextDecorationPainter, |
| // unless its text-decoration-line is spelling-error itself. In the latter case we should paint decoration with our native spelling error markers. |
| auto spellingErrorPseudoStyle = renderer.spellingErrorPseudoStyle(); |
| if (spellingErrorPseudoStyle && !spellingErrorPseudoStyle->textDecorationLineInEffect().isSpellingError()) { |
| markedTexts.removeAllMatching([] (auto&& markedText) { |
| return markedText.type == MarkedText::Type::SpellingError; |
| }); |
| } |
| |
| // GrammarError marked text that is styled via ::grammar-error is removed from being painted here and it is painted as regular text-decoration at TextDecorationPainter |
| auto grammarErrorPseudoStyle = renderer.grammarErrorPseudoStyle(); |
| if (grammarErrorPseudoStyle && !grammarErrorPseudoStyle->textDecorationLineInEffect().isNone()) { |
| markedTexts.removeAllMatching([] (auto&& markedText) { |
| return markedText.type == MarkedText::Type::GrammarError; |
| }); |
| } |
| } |
| |
| static std::optional<MarkedText> markedTextForTextDecorationLineSpellingError(const RenderText& renderer) |
| { |
| if (!renderer.style().textDecorationLineInEffect().isSpellingError()) |
| return std::nullopt; |
| return std::make_optional<MarkedText>({ 0, static_cast<unsigned>(renderer.length()), MarkedText::Type::SpellingError }); |
| } |
| |
| static std::optional<MarkedText> markedTextForTextDecorationLineGrammarError(const RenderText& renderer) |
| { |
| if (!renderer.style().textDecorationLineInEffect().isGrammarError()) |
| return std::nullopt; |
| return std::make_optional<MarkedText>({ 0, static_cast<unsigned>(renderer.length()), MarkedText::Type::GrammarError }); |
| } |
| |
| void TextBoxPainter::paintPlatformDocumentMarkers() |
| { |
| auto markedTexts = MarkedText::collectForDocumentMarkers(m_renderer, m_selectableRange, MarkedText::PaintPhase::Decoration); |
| // We want to paint text-decoration-line: spelling-error and grammar-error the same way we natively paint text marked with spelling errors |
| auto textDecorationLineSpellingErrorAsMarkedText = markedTextForTextDecorationLineSpellingError(m_renderer); |
| auto textDecorationLineGrammarErrorAsMarkedText = markedTextForTextDecorationLineGrammarError(m_renderer); |
| |
| if (markedTexts.isEmpty() && !textDecorationLineSpellingErrorAsMarkedText && !textDecorationLineGrammarErrorAsMarkedText) |
| return; |
| |
| // Defer painting to TextDecorationPainter if needed |
| removeMarkersPaintedByTextDecorationPainter(m_renderer, markedTexts); |
| |
| auto transparentContentMarkedTexts = MarkedText::collectForDraggedAndTransparentContent(DocumentMarkerType::TransparentContent, m_renderer, m_selectableRange); |
| |
| // Ensure the transparent content marked texts go first in the vector, so that they take precedence over |
| // the other marked texts when being subdivided so that they do not get painted. |
| Vector<MarkedText> allMarkedTexts; |
| allMarkedTexts.appendVector(transparentContentMarkedTexts); |
| allMarkedTexts.appendVector(markedTexts); |
| if (textDecorationLineSpellingErrorAsMarkedText) |
| allMarkedTexts.append(*textDecorationLineSpellingErrorAsMarkedText); |
| if (textDecorationLineGrammarErrorAsMarkedText) |
| allMarkedTexts.append(*textDecorationLineGrammarErrorAsMarkedText); |
| |
| for (auto& markedText : MarkedText::subdivide(allMarkedTexts, MarkedText::OverlapStrategy::Frontmost)) { |
| switch (markedText.type) { |
| case MarkedText::Type::DraggedContent: |
| case MarkedText::Type::TransparentContent: |
| continue; |
| |
| default: |
| paintPlatformDocumentMarker(markedText); |
| break; |
| } |
| } |
| } |
| |
| #if ENABLE(WRITING_TOOLS) |
| |
| constexpr Seconds writingToolsAnimationLoop = 10000_ms; |
| static void drawWritingToolsUnderline(GraphicsContext& context, const FloatRect& rect, IntSize frameSize) |
| { |
| auto radius = rect.height() / 2.0; |
| auto minX = rect.x(); |
| auto maxX = rect.maxX(); |
| auto minY = rect.y(); |
| auto maxY = rect.maxY(); |
| auto midY = (minY + maxY) / 2.0; |
| |
| auto frameX = frameSize.width(); |
| auto frameY = frameSize.height(); |
| |
| constexpr auto redColor = SRGBA<uint8_t> { 227, 100, 136 }; |
| constexpr auto yellowColor = SRGBA<uint8_t> { 242, 225, 162 }; |
| constexpr auto purpleColor = SRGBA<uint8_t> { 154, 109, 209 }; |
| |
| auto animationProgress = (MonotonicTime::now() % writingToolsAnimationLoop).value() / 10; |
| |
| auto xOffset = frameX * fmod(animationProgress + midY / frameY, 1.0); |
| constexpr std::array colorList { purpleColor, redColor, yellowColor, redColor, purpleColor, purpleColor, redColor, yellowColor, redColor, purpleColor }; |
| |
| Ref gradient = Gradient::create(Gradient::LinearData { FloatPoint(0 - xOffset, 0), FloatPoint(frameX * 2 - xOffset, frameY) }, { ColorInterpolationMethod::SRGB { }, AlphaPremultiplication::Unpremultiplied }); |
| |
| auto colorStop = 0.f; |
| auto colorIncrement = 1.0 / colorList.size(); |
| for (auto color : colorList) { |
| gradient->addColorStop({ colorStop, color }); |
| colorStop += colorIncrement; |
| } |
| |
| context.save(); |
| context.setFillGradient(WTF::move(gradient)); |
| |
| Path path; |
| path.moveTo(FloatPoint(minX + radius, maxY)); |
| path.addArc(FloatPoint(minX + radius, midY), radius, piOverTwoDouble, 3 * piOverTwoDouble, RotationDirection::Clockwise); |
| path.addLineTo(FloatPoint(maxX - radius, minY)); |
| path.addArc(FloatPoint(maxX - radius, midY), radius, 3 * piOverTwoDouble, piOverTwoDouble, RotationDirection::Clockwise); |
| |
| context.fillPath(path); |
| context.restore(); |
| } |
| |
| #endif // ENABLE(WRITING_TOOLS) |
| |
| void TextBoxPainter::paintPlatformDocumentMarker(const MarkedText& markedText) |
| { |
| // Never print document markers (rdar://5327887) |
| if (m_document->printing()) |
| return; |
| |
| auto bounds = calculateDocumentMarkerBounds(makeIterator(), markedText); |
| bounds.moveBy(m_paintRect.location()); |
| |
| #if ENABLE(WRITING_TOOLS) |
| if (markedText.type == MarkedText::Type::WritingToolsTextSuggestion) { |
| drawWritingToolsUnderline(m_paintInfo.context(), bounds, m_renderer->frame().view()->size()); |
| return; |
| } |
| #endif |
| |
| auto lineStyleMode = [&] { |
| switch (markedText.type) { |
| case MarkedText::Type::SpellingError: |
| return DocumentMarkerLineStyleMode::Spelling; |
| case MarkedText::Type::GrammarError: |
| return DocumentMarkerLineStyleMode::Grammar; |
| case MarkedText::Type::Correction: |
| return DocumentMarkerLineStyleMode::AutocorrectionReplacement; |
| case MarkedText::Type::DictationAlternatives: |
| return DocumentMarkerLineStyleMode::DictationAlternatives; |
| #if PLATFORM(IOS_FAMILY) |
| case MarkedText::Type::DictationPhraseWithAlternatives: |
| // FIXME: Rename DocumentMarkerLineStyle::TextCheckingDictationPhraseWithAlternatives and remove the PLATFORM(IOS_FAMILY)-guard. |
| return DocumentMarkerLineStyleMode::TextCheckingDictationPhraseWithAlternatives; |
| #endif |
| default: |
| ASSERT_NOT_REACHED(); |
| return DocumentMarkerLineStyleMode::Spelling; |
| } |
| }(); |
| |
| auto lineStyleColor = RenderTheme::singleton().documentMarkerLineColor(m_renderer, lineStyleMode); |
| if (auto* marker = markedText.marker) |
| lineStyleColor = lineStyleColor.colorWithAlphaMultipliedBy(marker->opacity()); |
| |
| m_paintInfo.context().drawDotsForDocumentMarker(bounds, { lineStyleMode, lineStyleColor }); |
| } |
| |
| FloatRect TextBoxPainter::computePaintRect(const LayoutPoint& paintOffset) |
| { |
| FloatPoint localPaintOffset(paintOffset); |
| if (writingMode().isVertical()) { |
| localPaintOffset.move(0, -m_logicalRect.height()); |
| if (writingMode().isLineOverLeft()) |
| localPaintOffset.move(m_logicalRect.height(), m_logicalRect.width()); |
| } |
| |
| auto visualRect = textBox().visualRectIgnoringBlockDirection(); |
| textBox().formattingContextRoot().flipForWritingMode(visualRect); |
| |
| auto boxOrigin = visualRect.location(); |
| |
| boxOrigin.moveBy(localPaintOffset); |
| if (writingMode().isVertical()) { |
| // This is required by the CTM rotation we do for vertical content. |
| boxOrigin.setX(roundToDevicePixel(LayoutUnit { boxOrigin.x() }, m_document->deviceScaleFactor())); |
| } |
| return { boxOrigin, FloatSize(m_logicalRect.width(), m_logicalRect.height()) }; |
| } |
| |
| FloatRect calculateDocumentMarkerBounds(const InlineIterator::TextBoxIterator& textBox, const MarkedText& markedText) |
| { |
| auto& font = textBox->fontCascade(); |
| auto [y, height] = DocumentMarkerController::markerYPositionAndHeightForFont(font); |
| |
| // Avoid measuring the text when the entire line box is selected as an optimization. |
| if (markedText.startOffset || markedText.endOffset != textBox->selectableRange().clamp(textBox->end())) { |
| auto run = textBox->textRun(); |
| auto selectionRect = LayoutRect { 0_lu, y, 0_lu, height }; |
| font.adjustSelectionRectForText(textBox->renderer().canUseSimplifiedTextMeasuring().value_or(false), run, selectionRect, markedText.startOffset, markedText.endOffset); |
| return selectionRect; |
| } |
| |
| return FloatRect(0, y, textBox->logicalWidth(), height); |
| } |
| |
| bool TextBoxPainter::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; |
| } |
| |
| const FontCascade& TextBoxPainter::fontCascade() const |
| { |
| if (m_isCombinedText) |
| return downcast<RenderCombineText>(m_renderer.get()).textCombineFont(); |
| |
| return m_style->fontCascade(); |
| } |
| |
| FloatPoint TextBoxPainter::textOriginFromPaintRect(const FloatRect& paintRect) const |
| { |
| auto ascent = snap(m_style->metricsOfPrimaryFont().ascent(), m_renderer); |
| if (writingMode().isVertical()) { |
| // FIXME: This is required by the CTM rotation logic. We should eventually (re)move it though. |
| ascent = roundToDevicePixel(LayoutUnit { ascent }, m_document->deviceScaleFactor()); |
| } |
| |
| auto textOrigin = FloatPoint { paintRect.x(), paintRect.y() + ascent }; |
| |
| if (m_isCombinedText) { |
| if (auto newOrigin = downcast<RenderCombineText>(m_renderer.get()).computeTextOrigin(paintRect)) |
| textOrigin = newOrigin.value(); |
| } |
| |
| if (writingMode().isHorizontal()) |
| textOrigin.setY(roundToDevicePixel(LayoutUnit { textOrigin.y() }, m_document->deviceScaleFactor())); |
| else |
| textOrigin.setX(roundToDevicePixel(LayoutUnit { textOrigin.x() }, m_document->deviceScaleFactor())); |
| |
| return textOrigin; |
| } |
| |
| bool TextBoxPainter::isInsideShapedContent() const |
| { |
| auto& textContent = textBox().box().text(); |
| return textContent.isAtShapingBoundaryStart() || textContent.isAtShapingBoundaryEnd() || textContent.isInsideShapingBoundary(); |
| } |
| |
| } |