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 | |