2 * Copyright (C) 2018 Apple Inc. All Rights Reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
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.
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 class BlockFormattingContext extends FormattingContext {
26 constructor(root, layoutState) {
27 super(root, layoutState);
28 // New block formatting context always establishes a new floating context.
29 this.m_floatingContext = new FloatingContext(this);
33 // 9.4.1 Block formatting contexts
34 // In a block formatting context, boxes are laid out one after the other, vertically, beginning at the top of a containing block.
35 // The vertical distance between two sibling boxes is determined by the 'margin' properties.
36 // Vertical margins between adjacent block-level boxes in a block formatting context collapse.
38 // This is a post-order tree traversal layout.
39 // The root container layout is done in the formatting context it lives in, not that one it creates, so let's start with the first child.
40 if (this.rootContainer().firstInFlowOrFloatChild())
41 this._addToLayoutQueue(this.rootContainer().firstInFlowOrFloatChild());
42 // 1. Go all the way down to the leaf node
43 // 2. Compute static position and width as we travers down
44 // 3. As we climb back on the tree, compute height and finialize position
45 // (Any subtrees with new formatting contexts need to layout synchronously)
46 while (this._descendantNeedsLayout()) {
47 // Travers down on the descendants until we find a leaf node.
49 let layoutBox = this._nextInLayoutQueue();
50 this.computeWidth(layoutBox);
51 this._computeStaticPosition(layoutBox);
52 if (layoutBox.establishesFormattingContext()) {
53 this.layoutContext().layout(layoutBox);
56 if (!layoutBox.isContainer() || !layoutBox.hasInFlowOrFloatChild())
58 this._addToLayoutQueue(layoutBox.firstInFlowOrFloatChild());
61 // Climb back on the ancestors and compute height/final position.
62 while (this._descendantNeedsLayout()) {
63 // All inflow descendants (if there are any) are laid out by now. Let's compute the box's height.
64 let layoutBox = this._nextInLayoutQueue();
65 this.computeHeight(layoutBox);
66 // Adjust position now that we have all the previous floats placed in this context -if needed.
67 this.floatingContext().computePosition(layoutBox);
68 // Move in-flow positioned children to their final position.
69 this._placeInFlowPositionedChildren(layoutBox);
70 // We are done with laying out this box.
71 this._removeFromLayoutQueue(layoutBox);
72 if (layoutBox.nextInFlowOrFloatSibling()) {
73 this._addToLayoutQueue(layoutBox.nextInFlowOrFloatSibling());
78 // Place the inflow positioned children.
79 this._placeInFlowPositionedChildren(this.rootContainer());
80 // And take care of out-of-flow boxes as the final step.
81 this._layoutOutOfFlowDescendants();
84 computeWidth(layoutBox) {
85 if (layoutBox.isOutOfFlowPositioned())
86 return this._computeOutOfFlowWidth(layoutBox);
87 if (layoutBox.isFloatingPositioned())
88 return this._computeFloatingWidth(layoutBox);
89 return this._computeInFlowWidth(layoutBox);
92 computeHeight(layoutBox) {
93 if (layoutBox.isOutOfFlowPositioned())
94 return this._computeOutOfFlowHeight(layoutBox);
95 if (layoutBox.isFloatingPositioned())
96 return this._computeFloatingHeight(layoutBox);
97 return this._computeInFlowHeight(layoutBox);
100 marginTop(layoutBox) {
101 return BlockMarginCollapse.marginTop(layoutBox);
104 marginBottom(layoutBox) {
105 return BlockMarginCollapse.marginBottom(layoutBox);
108 _computeStaticPosition(layoutBox) {
109 // In a block formatting context, boxes are laid out one after the other, vertically, beginning at the top of a containing block.
110 // The vertical distance between two sibling boxes is determined by the 'margin' properties.
111 // Vertical margins between adjacent block-level boxes in a block formatting context collapse.
112 let containingBlockContentBox = this.toDisplayBox(layoutBox.containingBlock()).contentBox();
113 // Start from the top of the container's content box.
114 let previousInFlowSibling = layoutBox.previousInFlowSibling();
115 let contentBottom = containingBlockContentBox.top()
116 if (previousInFlowSibling)
117 contentBottom = this.toDisplayBox(previousInFlowSibling).bottom() + this.marginBottom(previousInFlowSibling);
118 let position = new LayoutPoint(contentBottom, containingBlockContentBox.left());
119 position.moveBy(new LayoutSize(this.marginLeft(layoutBox), this.marginTop(layoutBox)));
120 this.toDisplayBox(layoutBox).setTopLeft(position);
123 _placeInFlowPositionedChildren(container) {
124 if (!container.isContainer())
126 // If this layoutBox also establishes a formatting context, then positioning already has happend at the formatting context.
127 if (container.establishesFormattingContext() && container != this.rootContainer())
129 ASSERT(container.isContainer());
130 for (let inFlowChild = container.firstInFlowChild(); inFlowChild; inFlowChild = inFlowChild.nextInFlowSibling()) {
131 if (!inFlowChild.isInFlowPositioned())
133 this._computeInFlowPositionedPosition(inFlowChild);
137 _layoutOutOfFlowDescendants() {
138 // This lays out all the out-of-flow boxes that belong to this formatting context even if
139 // the root container is not the containing block.
140 let outOfFlowDescendants = this._outOfFlowDescendants();
141 for (let outOfFlowBox of outOfFlowDescendants) {
142 this._addToLayoutQueue(outOfFlowBox);
143 this.computeWidth(outOfFlowBox);
144 this.layoutContext().layout(outOfFlowBox);
145 this.computeHeight(outOfFlowBox);
146 this._computeOutOfFlowPosition(outOfFlowBox);
147 this._removeFromLayoutQueue(outOfFlowBox);
151 _computeOutOfFlowWidth(layoutBox) {
152 // 10.3.7 Absolutely positioned, non-replaced elements
154 // 1. 'left' and 'width' are 'auto' and 'right' is not 'auto', then the width is shrink-to-fit. Then solve for 'left'
155 // 2. 'left' and 'right' are 'auto' and 'width' is not 'auto', then if the 'direction' property of the element establishing
156 // the static-position containing block is 'ltr' set 'left' to the static position, otherwise set 'right' to the static position.
157 // Then solve for 'left' (if 'direction is 'rtl') or 'right' (if 'direction' is 'ltr').
158 // 3. 'width' and 'right' are 'auto' and 'left' is not 'auto', then the width is shrink-to-fit . Then solve for 'right'
159 // 4. 'left' is 'auto', 'width' and 'right' are not 'auto', then solve for 'left'
160 // 5. 'width' is 'auto', 'left' and 'right' are not 'auto', then solve for 'width'
161 // 6. 'right' is 'auto', 'left' and 'width' are not 'auto', then solve for 'right'
162 let width = Number.NaN;
163 if (Utils.isWidthAuto(layoutBox) && Utils.isLeftAuto(layoutBox) && Utils.isRightAuto(layoutBox))
164 width = this._shrinkToFitWidth(layoutBox);
165 else if (Utils.isLeftAuto(layoutBox) && Utils.isWidthAuto(layoutBox) && !Utils.isRightAuto(layoutBox))
166 width = this._shrinkToFitWidth(layoutBox); // 1
167 else if (Utils.isLeftAuto(layoutBox) && Utils.isRightAuto(layoutBox) && !Utils.isWidthAuto(layoutBox))
168 width = Utils.width(layoutBox); // 2
169 else if (Utils.isWidthAuto(layoutBox) && Utils.isRightAuto(layoutBox) && !Utils.isLeftAuto(layoutBox))
170 width = this._shrinkToFitWidth(layoutBox); // 3
171 else if (Utils.isLeftAuto(layoutBox) && !Utils.isWidthAuto(layoutBox) && !Utils.isRightAuto(layoutBox))
172 width = Utils.width(layoutBox); // 4
173 else if (Utils.isWidthAuto(layoutBox) && !Utils.isLeftAuto(layoutBox) && !Utils.isRightAuto(layoutBox))
174 width = Math.max(0, this.toDisplayBox(layoutBox.containingBlock()).contentBox().width() - Utils.right(layoutBox) - Utils.left(layoutBox)); // 5
175 else if (Utils.isRightAuto(layoutBox) && !Utils.isLeftAuto(layoutBox) && !Utils.isWidthAuto(layoutBox))
176 width = Utils.width(layoutBox); // 6
178 ASSERT_NOT_REACHED();
179 width += Utils.computedHorizontalBorderAndPadding(layoutBox.node());
180 this.toDisplayBox(layoutBox).setWidth(width);
183 _computeFloatingWidth(layoutBox) {
184 // FIXME: missing cases
185 this.toDisplayBox(layoutBox).setWidth(Utils.width(layoutBox) + Utils.computedHorizontalBorderAndPadding(layoutBox.node()));
188 _computeInFlowWidth(layoutBox) {
189 if (Utils.isWidthAuto(layoutBox))
190 return this.toDisplayBox(layoutBox).setWidth(this._horizontalConstraint(layoutBox));
191 return this.toDisplayBox(layoutBox).setWidth(Utils.width(layoutBox) + Utils.computedHorizontalBorderAndPadding(layoutBox.node()));
194 _computeOutOfFlowHeight(layoutBox) {
195 // 1. If all three of 'top', 'height', and 'bottom' are auto, set 'top' to the static position and apply rule number three below.
196 // 2. If none of the three are 'auto': If both 'margin-top' and 'margin-bottom' are 'auto', solve the equation under
197 // the extra constraint that the two margins get equal values. If one of 'margin-top' or 'margin-bottom' is 'auto',
198 // solve the equation for that value. If the values are over-constrained, ignore the value for 'bottom' and solve for that value.
199 // Otherwise, pick the one of the following six rules that applies.
201 // 3. 'top' and 'height' are 'auto' and 'bottom' is not 'auto', then the height is based on the content per 10.6.7,
202 // set 'auto' values for 'margin-top' and 'margin-bottom' to 0, and solve for 'top'
203 // 4. 'top' and 'bottom' are 'auto' and 'height' is not 'auto', then set 'top' to the static position, set 'auto' values
204 // for 'margin-top' and 'margin-bottom' to 0, and solve for 'bottom'
205 // 5. 'height' and 'bottom' are 'auto' and 'top' is not 'auto', then the height is based on the content per 10.6.7,
206 // set 'auto' values for 'margin-top' and 'margin-bottom' to 0, and solve for 'bottom'
207 // 6. 'top' is 'auto', 'height' and 'bottom' are not 'auto', then set 'auto' values for 'margin-top' and 'margin-bottom' to 0, and solve for 'top'
208 // 7. 'height' is 'auto', 'top' and 'bottom' are not 'auto', then 'auto' values for 'margin-top' and 'margin-bottom' are set to 0 and solve for 'height'
209 // 8. 'bottom' is 'auto', 'top' and 'height' are not 'auto', then set 'auto' values for 'margin-top' and 'margin-bottom' to 0 and solve for 'bottom'
210 let height = Number.NaN;
211 if (Utils.isHeightAuto(layoutBox) && Utils.isBottomAuto(layoutBox) && Utils.isTopAuto(layoutBox))
212 height = this._contentHeight(layoutBox); // 1
213 else if (Utils.isTopAuto((layoutBox)) && Utils.isHeightAuto((layoutBox)) && !Utils.isBottomAuto((layoutBox)))
214 height = this._contentHeight(layoutBox); // 3
215 else if (Utils.isTopAuto((layoutBox)) && Utils.isBottomAuto((layoutBox)) && !Utils.isHeightAuto((layoutBox)))
216 height = Utils.height(layoutBox); // 4
217 else if (Utils.isHeightAuto((layoutBox)) && Utils.isBottomAuto((layoutBox)) && !Utils.isTopAuto((layoutBox)))
218 height = this._contentHeight(layoutBox); // 5
219 else if (Utils.isTopAuto((layoutBox)) && !Utils.isHeightAuto((layoutBox)) && !Utils.isBottomAuto((layoutBox)))
220 height = Utils.height(layoutBox); // 6
221 else if (Utils.isHeightAuto((layoutBox)) && !Utils.isTopAuto((layoutBox)) && !Utils.isBottomAuto((layoutBox)))
222 height = Math.max(0, this.toDisplayBox(layoutBox.containingBlock()).contentBox().height() - Utils.bottom(layoutBox) - Utils.top(layoutBox)); // 7
223 else if (Utils.isBottomAuto((layoutBox)) && !Utils.isTopAuto((layoutBox)) && !Utils.isHeightAuto((layoutBox)))
224 height = Utils.height(layoutBox); // 8
226 ASSERT_NOT_REACHED();
227 height += Utils.computedVerticalBorderAndPadding(layoutBox.node());
228 this.toDisplayBox(layoutBox).setHeight(height);
231 _computeFloatingHeight(layoutBox) {
232 // FIXME: missing cases
233 this.toDisplayBox(layoutBox).setHeight(Utils.height(layoutBox) + Utils.computedVerticalBorderAndPadding(layoutBox.node()));
236 _computeInFlowHeight(layoutBox) {
237 if (Utils.isHeightAuto(layoutBox)) {
238 // Only children in the normal flow are taken into account (i.e., floating boxes and absolutely positioned boxes are ignored,
239 // and relatively positioned boxes are considered without their offset). Note that the child box may be an anonymous block box.
241 // The element's height is the distance from its top content edge to the first applicable of the following:
242 // 1. the bottom edge of the last line box, if the box establishes a inline formatting context with one or more lines
243 return this.toDisplayBox(layoutBox).setHeight(this._contentHeight(layoutBox) + Utils.computedVerticalBorderAndPadding(layoutBox.node()));
245 return this.toDisplayBox(layoutBox).setHeight(Utils.height(layoutBox) + Utils.computedVerticalBorderAndPadding(layoutBox.node()));
248 _horizontalConstraint(layoutBox) {
249 let horizontalConstraint = this.toDisplayBox(layoutBox.containingBlock()).contentBox().width();
250 horizontalConstraint -= this.marginLeft(layoutBox) + this.marginRight(layoutBox);
251 return horizontalConstraint;
254 _contentHeight(layoutBox) {
255 // 10.6.3 Block-level non-replaced elements in normal flow when 'overflow' computes to 'visible'
256 // The element's height is the distance from its top content edge to the first applicable of the following:
257 // 1. the bottom edge of the last line box, if the box establishes a inline formatting context with one or more lines
258 // 2. the bottom edge of the bottom (possibly collapsed) margin of its last in-flow child, if the child's bottom margin does not collapse
259 // with the element's bottom margin
260 // 3. the bottom border edge of the last in-flow child whose top margin doesn't collapse with the element's bottom margin
261 // 4. zero, otherwise
262 // Only children in the normal flow are taken into account.
263 if (!layoutBox.isContainer() || !layoutBox.hasInFlowChild())
265 if (layoutBox.establishesInlineFormattingContext()) {
266 let lines = layoutBox.establishedFormattingContext().lines();
269 let lastLine = lines[lines.length - 1];
270 return lastLine.rect().bottom();
272 let top = this.toDisplayBox(layoutBox).contentBox().top();
273 let bottom = this._adjustBottomWithFIXME(layoutBox);
277 _adjustBottomWithFIXME(layoutBox) {
278 // FIXME: This function is a big FIXME.
279 let lastInFlowChild = layoutBox.lastInFlowChild();
280 let lastInFlowDisplayBox = lastInFlowChild.displayBox();
281 let bottom = lastInFlowDisplayBox.bottom() + this.marginBottom(lastInFlowChild);
282 // FIXME: margin for body
283 if (lastInFlowChild.name() == "RenderBody" && Utils.isHeightAuto(lastInFlowChild) && !this.toDisplayBox(lastInFlowChild).contentBox().height())
284 bottom -= this.marginBottom(lastInFlowChild);
285 // FIXME: figure out why floatings part of the initial block formatting context get propagated to HTML
286 if (layoutBox.node().tagName == "HTML") {
287 let floatingBottom = this.floatingContext().bottom();
288 if (!Number.isNaN(floatingBottom))
289 bottom = Math.max(floatingBottom, bottom);
294 _computeInFlowPositionedPosition(layoutBox) {
295 // Start with the original, static position.
296 let displayBox = this.toDisplayBox(layoutBox);
297 let relativePosition = displayBox.topLeft();
299 if (!Utils.isTopAuto(layoutBox))
300 relativePosition.shiftTop(Utils.top(layoutBox));
301 else if (!Utils.isBottomAuto(layoutBox))
302 relativePosition.shiftTop(-Utils.bottom(layoutBox));
304 if (!Utils.isLeftAuto(layoutBox))
305 relativePosition.shiftLeft(Utils.left(layoutBox));
306 else if (!Utils.isRightAuto(layoutBox))
307 relativePosition.shiftLeft(-Utils.right(layoutBox));
308 displayBox.setTopLeft(relativePosition);
311 _computeOutOfFlowPosition(layoutBox) {
312 let displayBox = this.toDisplayBox(layoutBox);
313 let top = Number.NaN;
314 let containerSize = this.toDisplayBox(layoutBox.containingBlock()).contentBox().size();
316 if (Utils.isTopAuto(layoutBox) && Utils.isBottomAuto(layoutBox)) {
317 ASSERT(Utils.isStaticallyPositioned(layoutBox));
318 // Vertically statically positioned.
319 // FIXME: Figure out if it is actually valid that we use the parent box as the container (which is not even in this formatting context).
320 let parent = layoutBox.parent();
321 let parentDisplayBox = parent.displayBox();
322 let previousInFlowSibling = layoutBox.previousInFlowSibling();
323 let contentBottom = previousInFlowSibling ? previousInFlowSibling.displayBox().bottom() : parentDisplayBox.contentBox().top();
324 top = contentBottom + this.marginTop(layoutBox);
325 // Convert static position (in parent coordinate system) to absolute (in containing block coordindate system).
326 if (parent != layoutBox.containingBlock())
327 top += this._toAbsolutePosition(parentDisplayBox.topLeft(), parent, layoutBox.containingBlock()).top();
328 } else if (!Utils.isTopAuto(layoutBox))
329 top = Utils.top(layoutBox) + this.marginTop(layoutBox);
330 else if (!Utils.isBottomAuto(layoutBox))
331 top = containerSize.height() - Utils.bottom(layoutBox) - displayBox.height() - this.marginBottom(layoutBox);
333 ASSERT_NOT_REACHED();
335 let left = Number.NaN;
336 if (Utils.isLeftAuto(layoutBox) && Utils.isRightAuto(layoutBox)) {
337 ASSERT(Utils.isStaticallyPositioned(layoutBox));
338 // Horizontally statically positioned.
339 // FIXME: Figure out if it is actually valid that we use the parent box as the container (which is not even in this formatting context).
340 let parent = layoutBox.parent();
341 let parentDisplayBox = parent.displayBox();
342 left = parentDisplayBox.contentBox().left() + this.marginLeft(layoutBox);
343 // Convert static position (in parent coordinate system) to absolute (in containing block coordindate system).
344 if (parent != layoutBox.containingBlock())
345 left += this._toAbsolutePosition(parentDisplayBox.rect(), parent, layoutBox.containingBlock()).left();
346 } else if (!Utils.isLeftAuto(layoutBox))
347 left = Utils.left(layoutBox) + this.marginLeft(layoutBox);
348 else if (!Utils.isRightAuto(layoutBox))
349 left = containerSize.width() - Utils.right(layoutBox) - displayBox.width() - this.marginRight(layoutBox);
351 ASSERT_NOT_REACHED();
352 displayBox.setTopLeft(new LayoutPoint(top, left));
355 _shrinkToFitWidth(layoutBox) {
356 // FIXME: this is naive.
357 ASSERT(Utils.isWidthAuto(layoutBox));
358 if (!layoutBox.isContainer() || !layoutBox.hasChild())
361 for (let inFlowChild = layoutBox.firstInFlowChild(); inFlowChild; inFlowChild = inFlowChild.nextInFlowSibling()) {
362 let widthCandidate = Utils.isWidthAuto(inFlowChild) ? this._shrinkToFitWidth(inFlowChild) : Utils.width(inFlowChild);
363 width = Math.max(width, widthCandidate + Utils.computedHorizontalBorderAndPadding(inFlowChild.node()));