| 1 | /* |
| 2 | * Copyright (C) 2010, 2015-2016 Apple Inc. All rights reserved. |
| 3 | * |
| 4 | * Redistribution and use in source and binary forms, with or without |
| 5 | * modification, are permitted provided that the following conditions |
| 6 | * are met: |
| 7 | * 1. Redistributions of source code must retain the above copyright |
| 8 | * notice, this list of conditions and the following disclaimer. |
| 9 | * 2. Redistributions in binary form must reproduce the above copyright |
| 10 | * notice, this list of conditions and the following disclaimer in the |
| 11 | * documentation and/or other materials provided with the distribution. |
| 12 | * |
| 13 | * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| 14 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| 15 | * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| 16 | * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| 17 | * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| 18 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| 19 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| 20 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| 21 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| 22 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| 23 | * THE POSSIBILITY OF SUCH DAMAGE. |
| 24 | */ |
| 25 | |
| 26 | #include "config.h" |
| 27 | #include "TextIndicator.h" |
| 28 | |
| 29 | #include "ColorHash.h" |
| 30 | #include "Document.h" |
| 31 | #include "Editor.h" |
| 32 | #include "Element.h" |
| 33 | #include "Frame.h" |
| 34 | #include "FrameSelection.h" |
| 35 | #include "FrameSnapshotting.h" |
| 36 | #include "FrameView.h" |
| 37 | #include "GeometryUtilities.h" |
| 38 | #include "GraphicsContext.h" |
| 39 | #include "ImageBuffer.h" |
| 40 | #include "IntRect.h" |
| 41 | #include "NodeTraversal.h" |
| 42 | #include "Range.h" |
| 43 | #include "RenderElement.h" |
| 44 | #include "RenderObject.h" |
| 45 | #include "RenderText.h" |
| 46 | #include "TextIterator.h" |
| 47 | #include "TextPaintStyle.h" |
| 48 | |
| 49 | #if PLATFORM(IOS_FAMILY) |
| 50 | #include "SelectionRect.h" |
| 51 | #endif |
| 52 | |
| 53 | namespace WebCore { |
| 54 | |
| 55 | static bool initializeIndicator(TextIndicatorData&, Frame&, const Range&, FloatSize margin, bool indicatesCurrentSelection); |
| 56 | |
| 57 | TextIndicator::TextIndicator(const TextIndicatorData& data) |
| 58 | : m_data(data) |
| 59 | { |
| 60 | } |
| 61 | |
| 62 | TextIndicator::~TextIndicator() = default; |
| 63 | |
| 64 | Ref<TextIndicator> TextIndicator::create(const TextIndicatorData& data) |
| 65 | { |
| 66 | return adoptRef(*new TextIndicator(data)); |
| 67 | } |
| 68 | |
| 69 | RefPtr<TextIndicator> TextIndicator::createWithRange(const Range& range, TextIndicatorOptions options, TextIndicatorPresentationTransition presentationTransition, FloatSize margin) |
| 70 | { |
| 71 | Frame* frame = range.startContainer().document().frame(); |
| 72 | |
| 73 | if (!frame) |
| 74 | return nullptr; |
| 75 | |
| 76 | Ref<Frame> protector(*frame); |
| 77 | |
| 78 | VisibleSelection oldSelection = frame->selection().selection(); |
| 79 | OptionSet<TemporarySelectionOption> temporarySelectionOptions; |
| 80 | temporarySelectionOptions.add(TemporarySelectionOption::DoNotSetFocus); |
| 81 | #if PLATFORM(IOS_FAMILY) |
| 82 | temporarySelectionOptions.add(TemporarySelectionOption::IgnoreSelectionChanges); |
| 83 | temporarySelectionOptions.add(TemporarySelectionOption::EnableAppearanceUpdates); |
| 84 | #endif |
| 85 | TemporarySelectionChange selectionChange(*frame, { range }, temporarySelectionOptions); |
| 86 | |
| 87 | TextIndicatorData data; |
| 88 | |
| 89 | data.presentationTransition = presentationTransition; |
| 90 | data.options = options; |
| 91 | |
| 92 | bool indicatesCurrentSelection = areRangesEqual(&range, oldSelection.toNormalizedRange().get()); |
| 93 | |
| 94 | if (!initializeIndicator(data, *frame, range, margin, indicatesCurrentSelection)) |
| 95 | return nullptr; |
| 96 | |
| 97 | return TextIndicator::create(data); |
| 98 | } |
| 99 | |
| 100 | RefPtr<TextIndicator> TextIndicator::createWithSelectionInFrame(Frame& frame, TextIndicatorOptions options, TextIndicatorPresentationTransition presentationTransition, FloatSize margin) |
| 101 | { |
| 102 | RefPtr<Range> range = frame.selection().toNormalizedRange(); |
| 103 | if (!range) |
| 104 | return nullptr; |
| 105 | |
| 106 | TextIndicatorData data; |
| 107 | |
| 108 | data.presentationTransition = presentationTransition; |
| 109 | data.options = options; |
| 110 | |
| 111 | if (!initializeIndicator(data, frame, *range, margin, true)) |
| 112 | return nullptr; |
| 113 | |
| 114 | return TextIndicator::create(data); |
| 115 | } |
| 116 | |
| 117 | static bool hasNonInlineOrReplacedElements(const Range& range) |
| 118 | { |
| 119 | Node* stopNode = range.pastLastNode(); |
| 120 | for (Node* node = range.firstNode(); node != stopNode; node = NodeTraversal::next(*node)) { |
| 121 | if (!node) |
| 122 | continue; |
| 123 | RenderObject* renderer = node->renderer(); |
| 124 | if (!renderer) |
| 125 | continue; |
| 126 | if ((!renderer->isInline() || renderer->isReplaced()) && range.intersectsNode(*node).releaseReturnValue()) |
| 127 | return true; |
| 128 | } |
| 129 | |
| 130 | return false; |
| 131 | } |
| 132 | |
| 133 | static SnapshotOptions snapshotOptionsForTextIndicatorOptions(TextIndicatorOptions options) |
| 134 | { |
| 135 | SnapshotOptions snapshotOptions = SnapshotOptionsNone; |
| 136 | |
| 137 | if (!(options & TextIndicatorOptionPaintAllContent)) { |
| 138 | if (options & TextIndicatorOptionPaintBackgrounds) |
| 139 | snapshotOptions |= SnapshotOptionsPaintSelectionAndBackgroundsOnly; |
| 140 | else { |
| 141 | snapshotOptions |= SnapshotOptionsPaintSelectionOnly; |
| 142 | |
| 143 | if (!(options & TextIndicatorOptionRespectTextColor)) |
| 144 | snapshotOptions |= SnapshotOptionsForceBlackText; |
| 145 | } |
| 146 | } else |
| 147 | snapshotOptions |= SnapshotOptionsExcludeSelectionHighlighting; |
| 148 | |
| 149 | return snapshotOptions; |
| 150 | } |
| 151 | |
| 152 | static RefPtr<Image> takeSnapshot(Frame& frame, IntRect rect, SnapshotOptions options, float& scaleFactor, const Vector<FloatRect>& clipRectsInDocumentCoordinates) |
| 153 | { |
| 154 | std::unique_ptr<ImageBuffer> buffer = snapshotFrameRectWithClip(frame, rect, clipRectsInDocumentCoordinates, options); |
| 155 | if (!buffer) |
| 156 | return nullptr; |
| 157 | scaleFactor = buffer->resolutionScale(); |
| 158 | return ImageBuffer::sinkIntoImage(WTFMove(buffer), PreserveResolution::Yes); |
| 159 | } |
| 160 | |
| 161 | static bool takeSnapshots(TextIndicatorData& data, Frame& frame, IntRect snapshotRect, const Vector<FloatRect>& clipRectsInDocumentCoordinates) |
| 162 | { |
| 163 | SnapshotOptions snapshotOptions = snapshotOptionsForTextIndicatorOptions(data.options); |
| 164 | |
| 165 | data.contentImage = takeSnapshot(frame, snapshotRect, snapshotOptions, data.contentImageScaleFactor, clipRectsInDocumentCoordinates); |
| 166 | if (!data.contentImage) |
| 167 | return false; |
| 168 | |
| 169 | if (data.options & TextIndicatorOptionIncludeSnapshotWithSelectionHighlight) { |
| 170 | float snapshotScaleFactor; |
| 171 | data.contentImageWithHighlight = takeSnapshot(frame, snapshotRect, SnapshotOptionsNone, snapshotScaleFactor, clipRectsInDocumentCoordinates); |
| 172 | ASSERT(!data.contentImageWithHighlight || data.contentImageScaleFactor == snapshotScaleFactor); |
| 173 | } |
| 174 | |
| 175 | if (data.options & TextIndicatorOptionIncludeSnapshotOfAllVisibleContentWithoutSelection) { |
| 176 | float snapshotScaleFactor; |
| 177 | auto snapshotRect = frame.view()->visibleContentRect(); |
| 178 | data.contentImageWithoutSelection = takeSnapshot(frame, snapshotRect, SnapshotOptionsPaintEverythingExcludingSelection, snapshotScaleFactor, { }); |
| 179 | data.contentImageWithoutSelectionRectInRootViewCoordinates = frame.view()->contentsToRootView(snapshotRect); |
| 180 | } |
| 181 | |
| 182 | return true; |
| 183 | } |
| 184 | |
| 185 | #if PLATFORM(IOS_FAMILY) |
| 186 | |
| 187 | static void getSelectionRectsForRange(Vector<FloatRect>& resultingRects, const Range& range) |
| 188 | { |
| 189 | Vector<SelectionRect> selectionRectsForRange; |
| 190 | Vector<FloatRect> selectionRectsForRangeInBoundingRectCoordinates; |
| 191 | range.collectSelectionRects(selectionRectsForRange); |
| 192 | for (auto selectionRect : selectionRectsForRange) |
| 193 | resultingRects.append(selectionRect.rect()); |
| 194 | } |
| 195 | |
| 196 | #endif |
| 197 | |
| 198 | static bool styleContainsComplexBackground(const RenderStyle& style) |
| 199 | { |
| 200 | if (style.hasBlendMode()) |
| 201 | return true; |
| 202 | |
| 203 | if (style.hasBackgroundImage()) |
| 204 | return true; |
| 205 | |
| 206 | if (style.hasBackdropFilter()) |
| 207 | return true; |
| 208 | |
| 209 | return false; |
| 210 | } |
| 211 | |
| 212 | static HashSet<Color> estimatedTextColorsForRange(const Range& range) |
| 213 | { |
| 214 | HashSet<Color> colors; |
| 215 | for (TextIterator iterator(&range); !iterator.atEnd(); iterator.advance()) { |
| 216 | auto* node = iterator.node(); |
| 217 | if (!is<Text>(node) || !is<RenderText>(node->renderer())) |
| 218 | continue; |
| 219 | |
| 220 | colors.add(node->renderer()->style().color()); |
| 221 | } |
| 222 | return colors; |
| 223 | } |
| 224 | |
| 225 | static Color estimatedBackgroundColorForRange(const Range& range, const Frame& frame) |
| 226 | { |
| 227 | auto estimatedBackgroundColor = frame.view() ? frame.view()->documentBackgroundColor() : Color::transparent; |
| 228 | |
| 229 | RenderElement* renderer = nullptr; |
| 230 | auto commonAncestor = range.commonAncestorContainer(); |
| 231 | while (commonAncestor) { |
| 232 | if (is<RenderElement>(commonAncestor->renderer())) { |
| 233 | renderer = downcast<RenderElement>(commonAncestor->renderer()); |
| 234 | break; |
| 235 | } |
| 236 | commonAncestor = commonAncestor->parentOrShadowHostElement(); |
| 237 | } |
| 238 | |
| 239 | auto boundingRectForRange = enclosingIntRect(range.absoluteBoundingRect(Range::RespectClippingForTextRects::Yes)); |
| 240 | Vector<Color> parentRendererBackgroundColors; |
| 241 | for (; !!renderer; renderer = renderer->parent()) { |
| 242 | auto absoluteBoundingBox = renderer->absoluteBoundingBoxRect(); |
| 243 | auto& style = renderer->style(); |
| 244 | if (!absoluteBoundingBox.contains(boundingRectForRange) || !style.hasBackground()) |
| 245 | continue; |
| 246 | |
| 247 | if (styleContainsComplexBackground(style)) |
| 248 | return estimatedBackgroundColor; |
| 249 | |
| 250 | auto visitedDependentBackgroundColor = style.visitedDependentColor(CSSPropertyBackgroundColor); |
| 251 | if (visitedDependentBackgroundColor != Color::transparent) |
| 252 | parentRendererBackgroundColors.append(visitedDependentBackgroundColor); |
| 253 | } |
| 254 | parentRendererBackgroundColors.reverse(); |
| 255 | for (const auto& backgroundColor : parentRendererBackgroundColors) |
| 256 | estimatedBackgroundColor = estimatedBackgroundColor.blend(backgroundColor); |
| 257 | |
| 258 | return estimatedBackgroundColor; |
| 259 | } |
| 260 | |
| 261 | static bool hasAnyIllegibleColors(TextIndicatorData& data, const Color& backgroundColor, HashSet<Color>&& textColors) |
| 262 | { |
| 263 | if (data.options & TextIndicatorOptionPaintAllContent) |
| 264 | return false; |
| 265 | |
| 266 | if (!(data.options & TextIndicatorOptionUseBoundingRectAndPaintAllContentForComplexRanges)) |
| 267 | return false; |
| 268 | |
| 269 | if (!(data.options & TextIndicatorOptionComputeEstimatedBackgroundColor)) |
| 270 | return false; |
| 271 | |
| 272 | bool hasOnlyLegibleTextColors = true; |
| 273 | if (data.options & TextIndicatorOptionRespectTextColor) { |
| 274 | for (auto& textColor : textColors) { |
| 275 | hasOnlyLegibleTextColors = textColorIsLegibleAgainstBackgroundColor(textColor, backgroundColor); |
| 276 | if (!hasOnlyLegibleTextColors) |
| 277 | break; |
| 278 | } |
| 279 | } else |
| 280 | hasOnlyLegibleTextColors = textColorIsLegibleAgainstBackgroundColor(Color::black, backgroundColor); |
| 281 | |
| 282 | return !hasOnlyLegibleTextColors || textColors.isEmpty(); |
| 283 | } |
| 284 | |
| 285 | static bool initializeIndicator(TextIndicatorData& data, Frame& frame, const Range& range, FloatSize margin, bool indicatesCurrentSelection) |
| 286 | { |
| 287 | if (auto* document = frame.document()) |
| 288 | document->updateLayoutIgnorePendingStylesheets(); |
| 289 | |
| 290 | bool treatRangeAsComplexDueToIllegibleTextColors = false; |
| 291 | if (data.options & TextIndicatorOptionComputeEstimatedBackgroundColor) { |
| 292 | data.estimatedBackgroundColor = estimatedBackgroundColorForRange(range, frame); |
| 293 | treatRangeAsComplexDueToIllegibleTextColors = hasAnyIllegibleColors(data, data.estimatedBackgroundColor, estimatedTextColorsForRange(range)); |
| 294 | } |
| 295 | |
| 296 | Vector<FloatRect> textRects; |
| 297 | |
| 298 | // FIXME (138888): Ideally we wouldn't remove the margin in this case, but we need to |
| 299 | // ensure that the indicator and indicator-with-highlight overlap precisely, and |
| 300 | // we can't add a margin to the indicator-with-highlight. |
| 301 | if (indicatesCurrentSelection && !(data.options & TextIndicatorOptionIncludeMarginIfRangeMatchesSelection)) |
| 302 | margin = FloatSize(); |
| 303 | |
| 304 | FrameSelection::TextRectangleHeight textRectHeight = (data.options & TextIndicatorOptionTightlyFitContent) ? FrameSelection::TextRectangleHeight::TextHeight : FrameSelection::TextRectangleHeight::SelectionHeight; |
| 305 | |
| 306 | if ((data.options & TextIndicatorOptionUseBoundingRectAndPaintAllContentForComplexRanges) && (hasNonInlineOrReplacedElements(range) || treatRangeAsComplexDueToIllegibleTextColors)) |
| 307 | data.options |= TextIndicatorOptionPaintAllContent; |
| 308 | #if PLATFORM(IOS_FAMILY) |
| 309 | else if (data.options & TextIndicatorOptionUseSelectionRectForSizing) |
| 310 | getSelectionRectsForRange(textRects, range); |
| 311 | #endif |
| 312 | else { |
| 313 | Vector<IntRect> absoluteTextRects; |
| 314 | range.absoluteTextRects(absoluteTextRects, textRectHeight == FrameSelection::TextRectangleHeight::SelectionHeight, nullptr, Range::RespectClippingForTextRects::Yes); |
| 315 | |
| 316 | textRects.reserveInitialCapacity(absoluteTextRects.size()); |
| 317 | for (auto& rect : absoluteTextRects) |
| 318 | textRects.uncheckedAppend(rect); |
| 319 | } |
| 320 | |
| 321 | if (textRects.isEmpty()) |
| 322 | textRects.append(range.absoluteBoundingRect(Range::RespectClippingForTextRects::Yes)); |
| 323 | |
| 324 | auto frameView = frame.view(); |
| 325 | |
| 326 | // Use the exposedContentRect/viewExposedRect instead of visibleContentRect to avoid creating a huge indicator for a large view inside a scroll view. |
| 327 | IntRect contentsClipRect; |
| 328 | #if PLATFORM(IOS_FAMILY) |
| 329 | contentsClipRect = enclosingIntRect(frameView->exposedContentRect()); |
| 330 | #else |
| 331 | if (auto viewExposedRect = frameView->viewExposedRect()) |
| 332 | contentsClipRect = frameView->viewToContents(enclosingIntRect(*viewExposedRect)); |
| 333 | else |
| 334 | contentsClipRect = frameView->visibleContentRect(); |
| 335 | #endif |
| 336 | |
| 337 | if (data.options & TextIndicatorOptionExpandClipBeyondVisibleRect) { |
| 338 | contentsClipRect.inflateX(contentsClipRect.width() / 2); |
| 339 | contentsClipRect.inflateY(contentsClipRect.height() / 2); |
| 340 | } |
| 341 | |
| 342 | FloatRect textBoundingRectInRootViewCoordinates; |
| 343 | FloatRect textBoundingRectInDocumentCoordinates; |
| 344 | Vector<FloatRect> clippedTextRectsInDocumentCoordinates; |
| 345 | Vector<FloatRect> textRectsInRootViewCoordinates; |
| 346 | for (const FloatRect& textRect : textRects) { |
| 347 | FloatRect clippedTextRect; |
| 348 | if (data.options & TextIndicatorOptionDoNotClipToVisibleRect) |
| 349 | clippedTextRect = textRect; |
| 350 | else |
| 351 | clippedTextRect = intersection(textRect, contentsClipRect); |
| 352 | if (clippedTextRect.isEmpty()) |
| 353 | continue; |
| 354 | |
| 355 | clippedTextRectsInDocumentCoordinates.append(clippedTextRect); |
| 356 | |
| 357 | FloatRect textRectInDocumentCoordinatesIncludingMargin = clippedTextRect; |
| 358 | textRectInDocumentCoordinatesIncludingMargin.inflateX(margin.width()); |
| 359 | textRectInDocumentCoordinatesIncludingMargin.inflateY(margin.height()); |
| 360 | textBoundingRectInDocumentCoordinates.unite(textRectInDocumentCoordinatesIncludingMargin); |
| 361 | |
| 362 | FloatRect textRectInRootViewCoordinates = frame.view()->contentsToRootView(enclosingIntRect(textRectInDocumentCoordinatesIncludingMargin)); |
| 363 | textRectsInRootViewCoordinates.append(textRectInRootViewCoordinates); |
| 364 | textBoundingRectInRootViewCoordinates.unite(textRectInRootViewCoordinates); |
| 365 | } |
| 366 | |
| 367 | Vector<FloatRect> textRectsInBoundingRectCoordinates; |
| 368 | for (auto rect : textRectsInRootViewCoordinates) { |
| 369 | rect.moveBy(-textBoundingRectInRootViewCoordinates.location()); |
| 370 | textRectsInBoundingRectCoordinates.append(rect); |
| 371 | } |
| 372 | |
| 373 | // Store the selection rect in window coordinates, to be used subsequently |
| 374 | // to determine if the indicator and selection still precisely overlap. |
| 375 | data.selectionRectInRootViewCoordinates = frame.view()->contentsToRootView(enclosingIntRect(frame.selection().selectionBounds())); |
| 376 | data.textBoundingRectInRootViewCoordinates = textBoundingRectInRootViewCoordinates; |
| 377 | data.textRectsInBoundingRectCoordinates = textRectsInBoundingRectCoordinates; |
| 378 | |
| 379 | return takeSnapshots(data, frame, enclosingIntRect(textBoundingRectInDocumentCoordinates), clippedTextRectsInDocumentCoordinates); |
| 380 | } |
| 381 | |
| 382 | } // namespace WebCore |
| 383 | |