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
53namespace WebCore {
54
55static bool initializeIndicator(TextIndicatorData&, Frame&, const Range&, FloatSize margin, bool indicatesCurrentSelection);
56
57TextIndicator::TextIndicator(const TextIndicatorData& data)
58 : m_data(data)
59{
60}
61
62TextIndicator::~TextIndicator() = default;
63
64Ref<TextIndicator> TextIndicator::create(const TextIndicatorData& data)
65{
66 return adoptRef(*new TextIndicator(data));
67}
68
69RefPtr<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
100RefPtr<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
117static 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
133static 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
152static 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
161static 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
187static 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
198static 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
212static 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
225static 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
261static 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
285static 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