[LayoutReloaded] Introduce FormattingState (Block/Inline/etc)
[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(root, layoutState) {
27         super(root, layoutState);
28         // New block formatting context always establishes a new floating context.
29         this.m_floatingContext = new FloatingContext(this);
30     }
31
32     layout() {
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.
37
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.
48             while (true) {
49                 let layoutBox = this._nextInLayoutQueue();
50                 this.computeWidth(layoutBox);
51                 this._computeStaticPosition(layoutBox);
52                 if (layoutBox.establishesFormattingContext()) {
53                     this.layoutContext().layout(layoutBox);
54                     break;
55                 }
56                 if (!layoutBox.isContainer() || !layoutBox.hasInFlowOrFloatChild())
57                     break;
58                 this._addToLayoutQueue(layoutBox.firstInFlowOrFloatChild());
59             }
60
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());
74                     break;
75                 }
76             }
77         }
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();
82    }
83
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);
90     }
91
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);
98     }
99
100     marginTop(layoutBox) {
101         return BlockMarginCollapse.marginTop(layoutBox);
102     }
103
104     marginBottom(layoutBox) {
105         return BlockMarginCollapse.marginBottom(layoutBox);
106     }
107
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);
121     }
122
123     _placeInFlowPositionedChildren(container) {
124         if (!container.isContainer())
125             return;
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())
128             return;
129         ASSERT(container.isContainer());
130         for (let inFlowChild = container.firstInFlowChild(); inFlowChild; inFlowChild = inFlowChild.nextInFlowSibling()) {
131             if (!inFlowChild.isInFlowPositioned())
132                 continue;
133             this._computeInFlowPositionedPosition(inFlowChild);
134         }
135     }
136
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);
148         }
149     }
150
151     _computeOutOfFlowWidth(layoutBox) {
152         // 10.3.7 Absolutely positioned, non-replaced elements
153
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
177         else
178             ASSERT_NOT_REACHED();
179         width += Utils.computedHorizontalBorderAndPadding(layoutBox.node());
180         this.toDisplayBox(layoutBox).setWidth(width);
181     }
182
183     _computeFloatingWidth(layoutBox) {
184         // FIXME: missing cases
185         this.toDisplayBox(layoutBox).setWidth(Utils.width(layoutBox) + Utils.computedHorizontalBorderAndPadding(layoutBox.node()));
186     }
187
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()));
192     }
193
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.
200
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
225         else
226             ASSERT_NOT_REACHED();
227         height += Utils.computedVerticalBorderAndPadding(layoutBox.node());
228         this.toDisplayBox(layoutBox).setHeight(height);
229     }
230
231     _computeFloatingHeight(layoutBox) {
232         // FIXME: missing cases
233         this.toDisplayBox(layoutBox).setHeight(Utils.height(layoutBox) + Utils.computedVerticalBorderAndPadding(layoutBox.node()));
234     }
235
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.
240
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()));
244         }
245         return this.toDisplayBox(layoutBox).setHeight(Utils.height(layoutBox) + Utils.computedVerticalBorderAndPadding(layoutBox.node()));
246     }
247
248     _horizontalConstraint(layoutBox) {
249         let horizontalConstraint = this.toDisplayBox(layoutBox.containingBlock()).contentBox().width();
250         horizontalConstraint -= this.marginLeft(layoutBox) + this.marginRight(layoutBox);
251         return horizontalConstraint;
252     }
253
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())
264             return 0;
265         if (layoutBox.establishesInlineFormattingContext()) {
266             let lines = layoutBox.establishedFormattingContext().lines();
267             if (!lines.length)
268                 return 0;
269             let lastLine = lines[lines.length - 1];
270             return lastLine.rect().bottom();
271         }
272         let top = this.toDisplayBox(layoutBox).contentBox().top();
273         let bottom = this._adjustBottomWithFIXME(layoutBox);
274         return bottom - top;
275     }
276
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);
290         }
291         return bottom;
292     }
293
294     _computeInFlowPositionedPosition(layoutBox) {
295         // Start with the original, static position.
296         let displayBox = this.toDisplayBox(layoutBox);
297         let relativePosition = displayBox.topLeft();
298         // Top/bottom
299         if (!Utils.isTopAuto(layoutBox))
300             relativePosition.shiftTop(Utils.top(layoutBox));
301         else if (!Utils.isBottomAuto(layoutBox))
302             relativePosition.shiftTop(-Utils.bottom(layoutBox));
303         // Left/right
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);
309     }
310
311     _computeOutOfFlowPosition(layoutBox) {
312         let displayBox = this.toDisplayBox(layoutBox);
313         let top = Number.NaN;
314         let containerSize = this.toDisplayBox(layoutBox.containingBlock()).contentBox().size();
315         // Top/bottom
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);
332         else
333             ASSERT_NOT_REACHED();
334         // Left/right
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);
350         else
351             ASSERT_NOT_REACHED();
352         displayBox.setTopLeft(new LayoutPoint(top, left));
353     }
354
355     _shrinkToFitWidth(layoutBox) {
356         // FIXME: this is naive.
357         ASSERT(Utils.isWidthAuto(layoutBox));
358         if (!layoutBox.isContainer() || !layoutBox.hasChild())
359             return 0;
360         let width = 0;
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()));
364         }
365         return width;
366     }
367 }