1 | /* |
2 | * Copyright (C) 2018 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 "LayoutState.h" |
28 | |
29 | #if ENABLE(LAYOUT_FORMATTING_CONTEXT) |
30 | |
31 | #include "DisplayBox.h" |
32 | #include "InlineTextBox.h" |
33 | #include "LayoutBox.h" |
34 | #include "LayoutContainer.h" |
35 | #include "LayoutTreeBuilder.h" |
36 | #include "RenderBox.h" |
37 | #include "RenderInline.h" |
38 | #include "RenderView.h" |
39 | #include <wtf/text/TextStream.h> |
40 | |
41 | namespace WebCore { |
42 | namespace Layout { |
43 | |
44 | static bool areEssentiallyEqual(float a, LayoutUnit b) |
45 | { |
46 | if (a == b.toFloat()) |
47 | return true; |
48 | |
49 | return fabs(a - b.toFloat()) <= 8 * LayoutUnit::epsilon(); |
50 | } |
51 | |
52 | static bool outputMismatchingSimpleLineInformationIfNeeded(TextStream& stream, const LayoutState& layoutState, const RenderBlockFlow& blockFlow, const Container& inlineFormattingRoot) |
53 | { |
54 | auto* lineLayoutData = blockFlow.simpleLineLayout(); |
55 | if (!lineLayoutData) { |
56 | ASSERT_NOT_REACHED(); |
57 | return true; |
58 | } |
59 | |
60 | auto& inlineFormattingState = layoutState.establishedFormattingState(inlineFormattingRoot); |
61 | ASSERT(is<InlineFormattingState>(inlineFormattingState)); |
62 | auto& inlineRunList = downcast<InlineFormattingState>(inlineFormattingState).inlineRuns(); |
63 | |
64 | if (inlineRunList.size() != lineLayoutData->runCount()) { |
65 | stream << "Mismatching number of runs: simple runs(" << lineLayoutData->runCount() << ") inline runs(" << inlineRunList.size() << ")" ; |
66 | stream.nextLine(); |
67 | return true; |
68 | } |
69 | |
70 | auto mismatched = false; |
71 | for (unsigned i = 0; i < lineLayoutData->runCount(); ++i) { |
72 | auto& simpleRun = lineLayoutData->runAt(i); |
73 | auto& inlineRun = inlineRunList[i]; |
74 | |
75 | auto matchingRuns = areEssentiallyEqual(simpleRun.logicalLeft, inlineRun.logicalLeft()) && areEssentiallyEqual(simpleRun.logicalRight, inlineRun.logicalRight()); |
76 | if (matchingRuns) |
77 | matchingRuns = (simpleRun.start == inlineRun.textContext()->start() && simpleRun.end == (inlineRun.textContext()->start() + inlineRun.textContext()->length())); |
78 | if (matchingRuns) |
79 | continue; |
80 | |
81 | stream << "Mismatching: simple run(" << simpleRun.start << ", " << simpleRun.end << ") (" << simpleRun.logicalLeft << ", " << simpleRun.logicalRight << ") layout run(" << inlineRun.textContext()->start() << ", " << inlineRun.textContext()->start() + inlineRun.textContext()->length() << ") (" << inlineRun.logicalLeft() << ", " << inlineRun.logicalRight() << ")" ; |
82 | stream.nextLine(); |
83 | mismatched = true; |
84 | } |
85 | return mismatched; |
86 | } |
87 | |
88 | static bool checkForMatchingNonTextRuns(const InlineRun& inlineRun, const WebCore::InlineBox& inlineBox) |
89 | { |
90 | return areEssentiallyEqual(inlineBox.logicalLeft(), inlineRun.logicalLeft()) |
91 | && areEssentiallyEqual(inlineBox.logicalRight(), inlineRun.logicalRight()) |
92 | && areEssentiallyEqual(inlineBox.logicalHeight(), inlineRun.logicalHeight()); |
93 | } |
94 | |
95 | static bool checkForMatchingTextRuns(const InlineRun& inlineRun, float logicalLeft, float logicalRight, unsigned start, unsigned end, float logicalHeight) |
96 | { |
97 | return areEssentiallyEqual(logicalLeft, inlineRun.logicalLeft()) |
98 | && areEssentiallyEqual(logicalRight, inlineRun.logicalRight()) |
99 | && start == inlineRun.textContext()->start() |
100 | && (end == (inlineRun.textContext()->start() + inlineRun.textContext()->length())) |
101 | && areEssentiallyEqual(logicalHeight, inlineRun.logicalHeight()); |
102 | } |
103 | |
104 | static void collectFlowBoxSubtree(const InlineFlowBox& flowbox, Vector<WebCore::InlineBox*>& inlineBoxes) |
105 | { |
106 | auto* inlineBox = flowbox.firstLeafChild(); |
107 | auto* lastLeafChild = flowbox.lastLeafChild(); |
108 | while (inlineBox) { |
109 | inlineBoxes.append(inlineBox); |
110 | if (inlineBox == lastLeafChild) |
111 | break; |
112 | inlineBox = inlineBox->nextLeafChild(); |
113 | } |
114 | } |
115 | |
116 | static void collectInlineBoxes(const RenderBlockFlow& root, Vector<WebCore::InlineBox*>& inlineBoxes) |
117 | { |
118 | for (auto* rootLine = root.firstRootBox(); rootLine; rootLine = rootLine->nextRootBox()) { |
119 | for (auto* inlineBox = rootLine->firstChild(); inlineBox; inlineBox = inlineBox->nextOnLine()) { |
120 | if (!is<InlineFlowBox>(inlineBox)) { |
121 | inlineBoxes.append(inlineBox); |
122 | continue; |
123 | } |
124 | collectFlowBoxSubtree(downcast<InlineFlowBox>(*inlineBox), inlineBoxes); |
125 | } |
126 | } |
127 | } |
128 | |
129 | static LayoutUnit resolveForRelativePositionIfNeeded(const InlineTextBox& inlineTextBox) |
130 | { |
131 | LayoutUnit xOffset; |
132 | auto* parent = inlineTextBox.parent(); |
133 | while (is<InlineFlowBox>(parent)) { |
134 | auto& renderer = parent->renderer(); |
135 | if (renderer.isInFlowPositioned()) |
136 | xOffset = renderer.offsetForInFlowPosition().width(); |
137 | parent = parent->parent(); |
138 | } |
139 | return xOffset; |
140 | } |
141 | |
142 | static bool outputMismatchingComplexLineInformationIfNeeded(TextStream& stream, const LayoutState& layoutState, const RenderBlockFlow& blockFlow, const Container& inlineFormattingRoot) |
143 | { |
144 | auto& inlineFormattingState = layoutState.establishedFormattingState(inlineFormattingRoot); |
145 | ASSERT(is<InlineFormattingState>(inlineFormattingState)); |
146 | auto& inlineRunList = downcast<InlineFormattingState>(inlineFormattingState).inlineRuns(); |
147 | |
148 | // Collect inlineboxes. |
149 | Vector<WebCore::InlineBox*> inlineBoxes; |
150 | collectInlineBoxes(blockFlow, inlineBoxes); |
151 | |
152 | auto mismatched = false; |
153 | unsigned runIndex = 0; |
154 | |
155 | if (inlineBoxes.size() != inlineRunList.size()) { |
156 | stream << "Warning: mismatching number of runs: inlineboxes(" << inlineBoxes.size() << ") vs. inline runs(" << inlineRunList.size() << ")" ; |
157 | stream.nextLine(); |
158 | } |
159 | |
160 | for (unsigned inlineBoxIndex = 0; inlineBoxIndex < inlineBoxes.size() && runIndex < inlineRunList.size(); ++inlineBoxIndex) { |
161 | auto* inlineBox = inlineBoxes[inlineBoxIndex]; |
162 | auto* inlineTextBox = is<InlineTextBox>(inlineBox) ? downcast<InlineTextBox>(inlineBox) : nullptr; |
163 | |
164 | auto& inlineRun = inlineRunList[runIndex]; |
165 | auto matchingRuns = false; |
166 | if (inlineTextBox) { |
167 | auto xOffset = resolveForRelativePositionIfNeeded(*inlineTextBox); |
168 | matchingRuns = checkForMatchingTextRuns(inlineRun, inlineTextBox->logicalLeft() + xOffset, |
169 | inlineTextBox->logicalRight() + xOffset, |
170 | inlineTextBox->start(), |
171 | inlineTextBox->end() + 1, |
172 | inlineTextBox->logicalHeight()); |
173 | |
174 | // <span>foobar</span>foobar generates 2 inline text boxes while we only generate one inline run. |
175 | // also <div>foo<img style="float: left;">bar</div> too. |
176 | auto inlineRunEnd = inlineRun.textContext()->start() + inlineRun.textContext()->length(); |
177 | auto textRunMightBeExtended = !matchingRuns && inlineTextBox->end() < inlineRunEnd && inlineBoxIndex < inlineBoxes.size() - 1; |
178 | |
179 | if (textRunMightBeExtended) { |
180 | auto logicalLeft = inlineTextBox->logicalLeft() + xOffset; |
181 | auto logicalRight = inlineTextBox->logicalRight() + xOffset; |
182 | auto start = inlineTextBox->start(); |
183 | auto end = inlineTextBox->end() + 1; |
184 | auto index = ++inlineBoxIndex; |
185 | for (; index < inlineBoxes.size(); ++index) { |
186 | auto* inlineBox = inlineBoxes[index]; |
187 | auto* inlineTextBox = is<InlineTextBox>(inlineBox) ? downcast<InlineTextBox>(inlineBox) : nullptr; |
188 | // Can't mix different inline boxes. |
189 | if (!inlineTextBox) |
190 | break; |
191 | |
192 | auto xOffset = resolveForRelativePositionIfNeeded(*inlineTextBox); |
193 | logicalRight = inlineTextBox->logicalRight() + xOffset; |
194 | end += (inlineTextBox->end() + 1); |
195 | if (checkForMatchingTextRuns(inlineRun, logicalLeft, logicalRight, start, end, inlineTextBox->logicalHeight())) { |
196 | matchingRuns = true; |
197 | inlineBoxIndex = index; |
198 | break; |
199 | } |
200 | |
201 | // Went too far? |
202 | if (end >= inlineRunEnd) |
203 | break; |
204 | } |
205 | } |
206 | } else |
207 | matchingRuns = checkForMatchingNonTextRuns(inlineRun, *inlineBox); |
208 | |
209 | |
210 | if (!matchingRuns) { |
211 | stream << "Mismatching: run " ; |
212 | |
213 | if (inlineTextBox) |
214 | stream << "(" << inlineTextBox->start() << ", " << inlineTextBox->end() + 1 << ")" ; |
215 | stream << " (" << inlineBox->logicalLeft() << ", " << inlineBox->logicalRight() << ") (" << inlineBox->logicalWidth() << "x" << inlineBox->logicalHeight() << ")" ; |
216 | |
217 | stream << "inline run " ; |
218 | if (inlineRun.textContext()) |
219 | stream << "(" << inlineRun.textContext()->start() << ", " << inlineRun.textContext()->start() + inlineRun.textContext()->length() << ") " ; |
220 | stream << "(" << inlineRun.logicalLeft() << ", " << inlineRun.logicalRight() << ") (" << inlineRun.logicalWidth() << "x" << inlineRun.logicalHeight() << ")" ; |
221 | stream.nextLine(); |
222 | mismatched = true; |
223 | } |
224 | ++runIndex; |
225 | } |
226 | return mismatched; |
227 | } |
228 | |
229 | static bool outputMismatchingBlockBoxInformationIfNeeded(TextStream& stream, const LayoutState& context, const RenderBox& renderer, const Box& layoutBox) |
230 | { |
231 | bool firstMismatchingRect = true; |
232 | auto outputRect = [&] (const String& prefix, const LayoutRect& rendererRect, const LayoutRect& layoutRect) { |
233 | if (firstMismatchingRect) { |
234 | stream << (renderer.element() ? renderer.element()->nodeName().utf8().data() : "" ) << " " << renderer.renderName() << "(" << &renderer << ") layoutBox(" << &layoutBox << ")" ; |
235 | stream.nextLine(); |
236 | firstMismatchingRect = false; |
237 | } |
238 | |
239 | stream << prefix.utf8().data() << "\trenderer->(" << rendererRect.x() << "," << rendererRect.y() << ") (" << rendererRect.width() << "x" << rendererRect.height() << ")" |
240 | << "\tlayout->(" << layoutRect.x() << "," << layoutRect.y() << ") (" << layoutRect.width() << "x" << layoutRect.height() << ")" ; |
241 | stream.nextLine(); |
242 | }; |
243 | |
244 | auto renderBoxLikeMarginBox = [](auto& displayBox) { |
245 | // Produce a RenderBox matching margin box. |
246 | auto borderBox = displayBox.borderBox(); |
247 | |
248 | return Display::Box::Rect { |
249 | borderBox.top() - displayBox.nonCollapsedMarginBefore(), |
250 | borderBox.left() - displayBox.computedMarginStart().valueOr(0), |
251 | displayBox.computedMarginStart().valueOr(0) + borderBox.width() + displayBox.computedMarginEnd().valueOr(0), |
252 | displayBox.nonCollapsedMarginBefore() + borderBox.height() + displayBox.nonCollapsedMarginAfter() |
253 | }; |
254 | }; |
255 | |
256 | auto& displayBox = context.displayBoxForLayoutBox(layoutBox); |
257 | |
258 | auto frameRect = renderer.frameRect(); |
259 | // rendering does not offset for relative positioned boxes. |
260 | if (renderer.isInFlowPositioned()) |
261 | frameRect.move(renderer.offsetForInFlowPosition()); |
262 | |
263 | if (frameRect != displayBox.rect()) { |
264 | outputRect("frameBox" , renderer.frameRect(), displayBox.rect()); |
265 | return true; |
266 | } |
267 | |
268 | if (renderer.borderBoxRect() != displayBox.borderBox()) { |
269 | outputRect("borderBox" , renderer.borderBoxRect(), displayBox.borderBox()); |
270 | return true; |
271 | } |
272 | |
273 | if (renderer.paddingBoxRect() != displayBox.paddingBox()) { |
274 | outputRect("paddingBox" , renderer.paddingBoxRect(), displayBox.paddingBox()); |
275 | return true; |
276 | } |
277 | |
278 | if (renderer.contentBoxRect() != displayBox.contentBox()) { |
279 | outputRect("contentBox" , renderer.contentBoxRect(), displayBox.contentBox()); |
280 | return true; |
281 | } |
282 | |
283 | if (renderer.marginBoxRect() != renderBoxLikeMarginBox(displayBox)) { |
284 | // In certain cases, like out-of-flow boxes with margin auto, marginBoxRect() returns 0. It's clearly incorrect, |
285 | // so let's check the individual margin values instead (and at this point we know that all other boxes match). |
286 | auto marginsMatch = displayBox.marginBefore() == renderer.marginBefore() |
287 | && displayBox.marginAfter() == renderer.marginAfter() |
288 | && displayBox.marginStart() == renderer.marginStart() |
289 | && displayBox.marginEnd() == renderer.marginEnd(); |
290 | |
291 | if (!marginsMatch) { |
292 | outputRect("marginBox" , renderer.marginBoxRect(), renderBoxLikeMarginBox(displayBox)); |
293 | return true; |
294 | } |
295 | } |
296 | |
297 | return false; |
298 | } |
299 | |
300 | static bool verifyAndOutputSubtree(TextStream& stream, const LayoutState& context, const RenderBox& renderer, const Box& layoutBox) |
301 | { |
302 | auto mismtachingGeometry = outputMismatchingBlockBoxInformationIfNeeded(stream, context, renderer, layoutBox); |
303 | |
304 | if (!is<Container>(layoutBox)) |
305 | return mismtachingGeometry; |
306 | |
307 | auto& container = downcast<Container>(layoutBox); |
308 | auto* childBox = container.firstChild(); |
309 | auto* childRenderer = renderer.firstChild(); |
310 | |
311 | while (childRenderer) { |
312 | if (!is<RenderBox>(*childRenderer)) { |
313 | childRenderer = childRenderer->nextSibling(); |
314 | continue; |
315 | } |
316 | |
317 | if (!childBox) { |
318 | stream << "Trees are out of sync!" ; |
319 | stream.nextLine(); |
320 | return true; |
321 | } |
322 | |
323 | if (is<RenderBlockFlow>(*childRenderer) && childBox->establishesInlineFormattingContext()) { |
324 | ASSERT(childRenderer->childrenInline()); |
325 | auto& blockFlow = downcast<RenderBlockFlow>(*childRenderer); |
326 | auto& formattingRoot = downcast<Container>(*childBox); |
327 | mismtachingGeometry |= blockFlow.lineLayoutPath() == RenderBlockFlow::SimpleLinesPath ? outputMismatchingSimpleLineInformationIfNeeded(stream, context, blockFlow, formattingRoot) : outputMismatchingComplexLineInformationIfNeeded(stream, context, blockFlow, formattingRoot); |
328 | } else { |
329 | auto mismatchingSubtreeGeometry = verifyAndOutputSubtree(stream, context, downcast<RenderBox>(*childRenderer), *childBox); |
330 | mismtachingGeometry |= mismatchingSubtreeGeometry; |
331 | } |
332 | |
333 | childBox = childBox->nextSibling(); |
334 | childRenderer = childRenderer->nextSibling(); |
335 | } |
336 | |
337 | return mismtachingGeometry; |
338 | } |
339 | |
340 | void LayoutState::verifyAndOutputMismatchingLayoutTree(const RenderView& renderView) const |
341 | { |
342 | TextStream stream; |
343 | auto mismatchingGeometry = verifyAndOutputSubtree(stream, *this, renderView, initialContainingBlock()); |
344 | if (!mismatchingGeometry) |
345 | return; |
346 | #if ENABLE(TREE_DEBUGGING) |
347 | showRenderTree(&renderView); |
348 | showLayoutTree(initialContainingBlock(), this); |
349 | #endif |
350 | WTFLogAlways("%s" , stream.release().utf8().data()); |
351 | ASSERT_NOT_REACHED(); |
352 | } |
353 | |
354 | } |
355 | } |
356 | |
357 | #endif |
358 | |