[LayoutReloaded] Introduce FloatingState.
[WebKit-https.git] / Tools / LayoutReloaded / FormattingContext / BlockFormatting / BlockFormattingContext.js
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. ``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.
24  */
25 class BlockFormattingContext extends FormattingContext {
26     constructor(blockFormattingState) {
27         super(blockFormattingState);
28     }
29
30     layout() {
31         // 9.4.1 Block formatting contexts
32         // In a block formatting context, boxes are laid out one after the other, vertically, beginning at the top of a containing block.
33         // The vertical distance between two sibling boxes is determined by the 'margin' properties.
34         // Vertical margins between adjacent block-level boxes in a block formatting context collapse.
35
36         // This is a post-order tree traversal layout.
37         // 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.
38         if (this.formattingRoot().firstInFlowOrFloatChild())
39             this._addToLayoutQueue(this.formattingRoot().firstInFlowOrFloatChild());
40         // 1. Go all the way down to the leaf node
41         // 2. Compute static position and width as we travers down
42         // 3. As we climb back on the tree, compute height and finialize position
43         // (Any subtrees with new formatting contexts need to layout synchronously)
44         while (this._descendantNeedsLayout()) {
45             // Travers down on the descendants until we find a leaf node.
46             while (true) {
47                 let layoutBox = this._nextInLayoutQueue();
48                 this.computeWidth(layoutBox);
49                 this._computeStaticPosition(layoutBox);
50                 if (layoutBox.establishesFormattingContext()) {
51                     this.layoutState().layout(layoutBox);
52                     break;
53                 }
54                 if (!layoutBox.isContainer() || !layoutBox.hasInFlowOrFloatChild())
55                     break;
56                 this._addToLayoutQueue(layoutBox.firstInFlowOrFloatChild());
57             }
58
59             // Climb back on the ancestors and compute height/final position.
60             while (this._descendantNeedsLayout()) {
61                 // All inflow descendants (if there are any) are laid out by now. Let's compute the box's height.
62                 let layoutBox = this._nextInLayoutQueue();
63                 this.computeHeight(layoutBox);
64                 // Adjust position now that we have all the previous floats placed in this context -if needed.
65                 this.floatingContext().computePosition(layoutBox);
66                 // Move in-flow positioned children to their final position.
67                 this._placeInFlowPositionedChildren(layoutBox);
68                 // We are done with laying out this box.
69                 this._removeFromLayoutQueue(layoutBox);
70                 if (layoutBox.nextInFlowOrFloatSibling()) {
71                     this._addToLayoutQueue(layoutBox.nextInFlowOrFloatSibling());
72                     break;
73                 }
74             }
75         }
76         // Place the inflow positioned children.
77         this._placeInFlowPositionedChildren(this.formattingRoot());
78         // And take care of out-of-flow boxes as the final step.
79         this._layoutOutOfFlowDescendants();
80    }
81
82     computeWidth(layoutBox) {
83         if (layoutBox.isOutOfFlowPositioned())
84             return this._computeOutOfFlowWidth(layoutBox);
85         if (layoutBox.isFloatingPositioned())
86             return this._computeFloatingWidth(layoutBox);
87         return this._computeInFlowWidth(layoutBox);
88     }
89
90     computeHeight(layoutBox) {
91         if (layoutBox.isOutOfFlowPositioned())
92             return this._computeOutOfFlowHeight(layoutBox);
93         if (layoutBox.isFloatingPositioned())
94             return this._computeFloatingHeight(layoutBox);
95         return this._computeInFlowHeight(layoutBox);
96     }
97
98     marginTop(layoutBox) {
99         return BlockMarginCollapse.marginTop(layoutBox);
100     }
101
102     marginBottom(layoutBox) {
103         return BlockMarginCollapse.marginBottom(layoutBox);
104     }
105
106     _computeStaticPosition(layoutBox) {
107         // In a block formatting context, boxes are laid out one after the other, vertically, beginning at the top of a containing block.
108         // The vertical distance between two sibling boxes is determined by the 'margin' properties.
109         // Vertical margins between adjacent block-level boxes in a block formatting context collapse.
110         let containingBlockContentBox = this.displayBox(layoutBox.containingBlock()).contentBox();
111         // Start from the top of the container's content box.
112         let previousInFlowSibling = layoutBox.previousInFlowSibling();
113         let contentBottom = containingBlockContentBox.top()
114         if (previousInFlowSibling)
115             contentBottom = this.displayBox(previousInFlowSibling).bottom() + this.marginBottom(previousInFlowSibling);
116         let position = new LayoutPoint(contentBottom, containingBlockContentBox.left());
117         position.moveBy(new LayoutSize(this.marginLeft(layoutBox), this.marginTop(layoutBox)));
118         this.displayBox(layoutBox).setTopLeft(position);
119     }
120
121     _placeInFlowPositionedChildren(container) {
122         if (!container.isContainer())
123             return;
124         // If this layoutBox also establishes a formatting context, then positioning already has happend at the formatting context.
125         if (container.establishesFormattingContext() && container != this.formattingRoot())
126             return;
127         ASSERT(container.isContainer());
128         for (let inFlowChild = container.firstInFlowChild(); inFlowChild; inFlowChild = inFlowChild.nextInFlowSibling()) {
129             if (!inFlowChild.isInFlowPositioned())
130                 continue;
131             this._computeInFlowPositionedPosition(inFlowChild);
132         }
133     }
134
135     _layoutOutOfFlowDescendants() {
136         // This lays out all the out-of-flow boxes that belong to this formatting context even if
137         // the root container is not the containing block.
138         let outOfFlowDescendants = this._outOfFlowDescendants();
139         for (let outOfFlowBox of outOfFlowDescendants) {
140             this._addToLayoutQueue(outOfFlowBox);
141             this.computeWidth(outOfFlowBox);
142             this.layoutState().layout(outOfFlowBox);
143             this.computeHeight(outOfFlowBox);
144             this._computeOutOfFlowPosition(outOfFlowBox);
145             this._removeFromLayoutQueue(outOfFlowBox);
146         }
147     }
148
149     _computeOutOfFlowWidth(layoutBox) {
150         // 10.3.7 Absolutely positioned, non-replaced elements
151
152         // 1. 'left' and 'width' are 'auto' and 'right' is not 'auto', then the width is shrink-to-fit. Then solve for 'left'
153         // 2. 'left' and 'right' are 'auto' and 'width' is not 'auto', then if the 'direction' property of the element establishing
154         //     the static-position containing block is 'ltr' set 'left' to the static position, otherwise set 'right' to the static position.
155         //     Then solve for 'left' (if 'direction is 'rtl') or 'right' (if 'direction' is 'ltr').
156         // 3. 'width' and 'right' are 'auto' and 'left' is not 'auto', then the width is shrink-to-fit . Then solve for 'right'
157         // 4. 'left' is 'auto', 'width' and 'right' are not 'auto', then solve for 'left'
158         // 5. 'width' is 'auto', 'left' and 'right' are not 'auto', then solve for 'width'
159         // 6. 'right' is 'auto', 'left' and 'width' are not 'auto', then solve for 'right'
160         let width = Number.NaN;
161         if (Utils.isWidthAuto(layoutBox) && Utils.isLeftAuto(layoutBox) && Utils.isRightAuto(layoutBox))
162             width = this._shrinkToFitWidth(layoutBox);
163         else if (Utils.isLeftAuto(layoutBox) && Utils.isWidthAuto(layoutBox) && !Utils.isRightAuto(layoutBox))
164             width = this._shrinkToFitWidth(layoutBox); // 1
165         else if (Utils.isLeftAuto(layoutBox) && Utils.isRightAuto(layoutBox) && !Utils.isWidthAuto(layoutBox))
166             width = Utils.width(layoutBox); // 2
167         else if (Utils.isWidthAuto(layoutBox) && Utils.isRightAuto(layoutBox) && !Utils.isLeftAuto(layoutBox))
168             width = this._shrinkToFitWidth(layoutBox); // 3
169         else if (Utils.isLeftAuto(layoutBox) && !Utils.isWidthAuto(layoutBox) && !Utils.isRightAuto(layoutBox))
170             width = Utils.width(layoutBox); // 4
171         else if (Utils.isWidthAuto(layoutBox) && !Utils.isLeftAuto(layoutBox) && !Utils.isRightAuto(layoutBox))
172             width = Math.max(0, this.displayBox(layoutBox.containingBlock()).contentBox().width() - Utils.right(layoutBox) - Utils.left(layoutBox)); // 5
173         else if (Utils.isRightAuto(layoutBox) && !Utils.isLeftAuto(layoutBox) && !Utils.isWidthAuto(layoutBox))
174             width = Utils.width(layoutBox); // 6
175         else
176             ASSERT_NOT_REACHED();
177         width += Utils.computedHorizontalBorderAndPadding(layoutBox.node());
178         this.displayBox(layoutBox).setWidth(width);
179     }
180
181     _computeFloatingWidth(layoutBox) {
182         // FIXME: missing cases
183         this.displayBox(layoutBox).setWidth(Utils.width(layoutBox) + Utils.computedHorizontalBorderAndPadding(layoutBox.node()));
184     }
185
186     _computeInFlowWidth(layoutBox) {
187         if (Utils.isWidthAuto(layoutBox))
188             return this.displayBox(layoutBox).setWidth(this._horizontalConstraint(layoutBox));
189         return this.displayBox(layoutBox).setWidth(Utils.width(layoutBox) + Utils.computedHorizontalBorderAndPadding(layoutBox.node()));
190     }
191
192     _computeOutOfFlowHeight(layoutBox) {
193         // 1. If all three of 'top', 'height', and 'bottom' are auto, set 'top' to the static position and apply rule number three below.
194         // 2. If none of the three are 'auto': If both 'margin-top' and 'margin-bottom' are 'auto', solve the equation under
195         //    the extra constraint that the two margins get equal values. If one of 'margin-top' or 'margin-bottom' is 'auto',
196         //    solve the equation for that value. If the values are over-constrained, ignore the value for 'bottom' and solve for that value.
197         // Otherwise, pick the one of the following six rules that applies.
198
199         // 3. 'top' and 'height' are 'auto' and 'bottom' is not 'auto', then the height is based on the content per 10.6.7,
200         //    set 'auto' values for 'margin-top' and 'margin-bottom' to 0, and solve for 'top'
201         // 4. 'top' and 'bottom' are 'auto' and 'height' is not 'auto', then set 'top' to the static position, set 'auto' values
202         //    for 'margin-top' and 'margin-bottom' to 0, and solve for 'bottom'
203         // 5. 'height' and 'bottom' are 'auto' and 'top' is not 'auto', then the height is based on the content per 10.6.7,
204         //    set 'auto' values for 'margin-top' and 'margin-bottom' to 0, and solve for 'bottom'
205         // 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'
206         // 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'
207         // 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'
208         let height = Number.NaN;
209         if (Utils.isHeightAuto(layoutBox) && Utils.isBottomAuto(layoutBox) && Utils.isTopAuto(layoutBox))
210             height = this._contentHeight(layoutBox); // 1
211         else if (Utils.isTopAuto((layoutBox)) && Utils.isHeightAuto((layoutBox)) && !Utils.isBottomAuto((layoutBox)))
212             height = this._contentHeight(layoutBox); // 3
213         else if (Utils.isTopAuto((layoutBox)) && Utils.isBottomAuto((layoutBox)) && !Utils.isHeightAuto((layoutBox)))
214             height = Utils.height(layoutBox); // 4
215         else if (Utils.isHeightAuto((layoutBox)) && Utils.isBottomAuto((layoutBox)) && !Utils.isTopAuto((layoutBox)))
216             height = this._contentHeight(layoutBox); // 5
217         else if (Utils.isTopAuto((layoutBox)) && !Utils.isHeightAuto((layoutBox)) && !Utils.isBottomAuto((layoutBox)))
218             height = Utils.height(layoutBox); // 6
219         else if (Utils.isHeightAuto((layoutBox)) && !Utils.isTopAuto((layoutBox)) && !Utils.isBottomAuto((layoutBox)))
220             height = Math.max(0, this.displayBox(layoutBox.containingBlock()).contentBox().height() - Utils.bottom(layoutBox) - Utils.top(layoutBox)); // 7
221         else if (Utils.isBottomAuto((layoutBox)) && !Utils.isTopAuto((layoutBox)) && !Utils.isHeightAuto((layoutBox)))
222             height = Utils.height(layoutBox); // 8
223         else
224             ASSERT_NOT_REACHED();
225         height += Utils.computedVerticalBorderAndPadding(layoutBox.node());
226         this.displayBox(layoutBox).setHeight(height);
227     }
228
229     _computeFloatingHeight(layoutBox) {
230         // FIXME: missing cases
231         this.displayBox(layoutBox).setHeight(Utils.height(layoutBox) + Utils.computedVerticalBorderAndPadding(layoutBox.node()));
232     }
233
234     _computeInFlowHeight(layoutBox) {
235         if (Utils.isHeightAuto(layoutBox)) {
236             // Only children in the normal flow are taken into account (i.e., floating boxes and absolutely positioned boxes are ignored,
237             // and relatively positioned boxes are considered without their offset). Note that the child box may be an anonymous block box.
238
239             // The element's height is the distance from its top content edge to the first applicable of the following:
240             // 1. the bottom edge of the last line box, if the box establishes a inline formatting context with one or more lines
241             return this.displayBox(layoutBox).setHeight(this._contentHeight(layoutBox) + Utils.computedVerticalBorderAndPadding(layoutBox.node()));
242         }
243         return this.displayBox(layoutBox).setHeight(Utils.height(layoutBox) + Utils.computedVerticalBorderAndPadding(layoutBox.node()));
244     }
245
246     _horizontalConstraint(layoutBox) {
247         let horizontalConstraint = this.displayBox(layoutBox.containingBlock()).contentBox().width();
248         horizontalConstraint -= this.marginLeft(layoutBox) + this.marginRight(layoutBox);
249         return horizontalConstraint;
250     }
251
252     _contentHeight(layoutBox) {
253         // 10.6.3 Block-level non-replaced elements in normal flow when 'overflow' computes to 'visible'
254         // The element's height is the distance from its top content edge to the first applicable of the following:
255         // 1. the bottom edge of the last line box, if the box establishes a inline formatting context with one or more lines
256         // 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
257         //    with the element's bottom margin
258         // 3. the bottom border edge of the last in-flow child whose top margin doesn't collapse with the element's bottom margin
259         // 4. zero, otherwise
260         // Only children in the normal flow are taken into account.
261         if (!layoutBox.isContainer() || !layoutBox.hasInFlowChild())
262             return 0;
263         if (layoutBox.establishesInlineFormattingContext()) {
264             let lines = this.layoutState().formattingState(layoutBox).lines();
265             if (!lines.length)
266                 return 0;
267             let lastLine = lines[lines.length - 1];
268             return lastLine.rect().bottom();
269         }
270         let top = this.displayBox(layoutBox).contentBox().top();
271         let bottom = this._adjustBottomWithFIXME(layoutBox);
272         return bottom - top;
273     }
274
275     _adjustBottomWithFIXME(layoutBox) {
276         // FIXME: This function is a big FIXME.
277         let lastInFlowChild = layoutBox.lastInFlowChild();
278         let lastInFlowDisplayBox = this.displayBox(lastInFlowChild);
279         let bottom = lastInFlowDisplayBox.bottom() + this.marginBottom(lastInFlowChild);
280         // FIXME: margin for body
281         if (lastInFlowChild.name() == "RenderBody" && Utils.isHeightAuto(lastInFlowChild) && !this.displayBox(lastInFlowChild).contentBox().height())
282             bottom -= this.marginBottom(lastInFlowChild);
283         // FIXME: figure out why floatings part of the initial block formatting context get propagated to HTML
284         if (layoutBox.node().tagName == "HTML") {
285             let floatingBottom = this.floatingContext().bottom();
286             if (!Number.isNaN(floatingBottom))
287                 bottom = Math.max(floatingBottom, bottom);
288         }
289         return bottom;
290     }
291
292     _computeInFlowPositionedPosition(layoutBox) {
293         // Start with the original, static position.
294         let displayBox = this.displayBox(layoutBox);
295         let relativePosition = displayBox.topLeft();
296         // Top/bottom
297         if (!Utils.isTopAuto(layoutBox))
298             relativePosition.shiftTop(Utils.top(layoutBox));
299         else if (!Utils.isBottomAuto(layoutBox))
300             relativePosition.shiftTop(-Utils.bottom(layoutBox));
301         // Left/right
302         if (!Utils.isLeftAuto(layoutBox))
303             relativePosition.shiftLeft(Utils.left(layoutBox));
304         else if (!Utils.isRightAuto(layoutBox))
305             relativePosition.shiftLeft(-Utils.right(layoutBox));
306         displayBox.setTopLeft(relativePosition);
307     }
308
309     _computeOutOfFlowPosition(layoutBox) {
310         let displayBox = this.displayBox(layoutBox);
311         let top = Number.NaN;
312         let containerSize = this.displayBox(layoutBox.containingBlock()).contentBox().size();
313         // Top/bottom
314         if (Utils.isTopAuto(layoutBox) && Utils.isBottomAuto(layoutBox)) {
315             ASSERT(Utils.isStaticallyPositioned(layoutBox));
316             // Vertically statically positioned.
317             // 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).
318             let parent = layoutBox.parent();
319             let parentDisplayBox = this.displayBox(parent);
320             let previousInFlowSibling = layoutBox.previousInFlowSibling();
321             let contentBottom = previousInFlowSibling ? this.displayBox(previousInFlowSibling).bottom() : parentDisplayBox.contentBox().top();
322             top = contentBottom + this.marginTop(layoutBox);
323             // Convert static position (in parent coordinate system) to absolute (in containing block coordindate system).
324             if (parent != layoutBox.containingBlock())
325                 top += this._toAbsolutePosition(parentDisplayBox.topLeft(), parent, layoutBox.containingBlock()).top();
326         } else if (!Utils.isTopAuto(layoutBox))
327             top = Utils.top(layoutBox) + this.marginTop(layoutBox);
328         else if (!Utils.isBottomAuto(layoutBox))
329             top = containerSize.height() - Utils.bottom(layoutBox) - displayBox.height() - this.marginBottom(layoutBox);
330         else
331             ASSERT_NOT_REACHED();
332         // Left/right
333         let left = Number.NaN;
334         if (Utils.isLeftAuto(layoutBox) && Utils.isRightAuto(layoutBox)) {
335             ASSERT(Utils.isStaticallyPositioned(layoutBox));
336             // Horizontally statically positioned.
337             // 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).
338             let parent = layoutBox.parent();
339             let parentDisplayBox = this.displayBox(parent);
340             left = parentDisplayBox.contentBox().left() + this.marginLeft(layoutBox);
341             // Convert static position (in parent coordinate system) to absolute (in containing block coordindate system).
342             if (parent != layoutBox.containingBlock())
343                 left += this._toAbsolutePosition(parentDisplayBox.rect(), parent, layoutBox.containingBlock()).left();
344         } else if (!Utils.isLeftAuto(layoutBox))
345             left = Utils.left(layoutBox) + this.marginLeft(layoutBox);
346         else if (!Utils.isRightAuto(layoutBox))
347             left = containerSize.width() - Utils.right(layoutBox) - displayBox.width() - this.marginRight(layoutBox);
348         else
349             ASSERT_NOT_REACHED();
350         displayBox.setTopLeft(new LayoutPoint(top, left));
351     }
352
353     _shrinkToFitWidth(layoutBox) {
354         // FIXME: this is naive.
355         ASSERT(Utils.isWidthAuto(layoutBox));
356         if (!layoutBox.isContainer() || !layoutBox.hasChild())
357             return 0;
358         let width = 0;
359         for (let inFlowChild = layoutBox.firstInFlowChild(); inFlowChild; inFlowChild = inFlowChild.nextInFlowSibling()) {
360             let widthCandidate = Utils.isWidthAuto(inFlowChild) ? this._shrinkToFitWidth(inFlowChild) : Utils.width(inFlowChild);
361             width = Math.max(width, widthCandidate + Utils.computedHorizontalBorderAndPadding(inFlowChild.node()));
362         }
363         return width;
364     }
365 }