[LayoutReloaded] Remove left/right width/height setters from Layout.Box
[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) {
27         super(root);
28         // New block formatting context always establishes a new floating context.
29         this.m_floatingContext = new FloatingContext(this);
30     }
31
32     layout(layoutContext) {
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                     layoutContext.layoutFormattingContext(layoutBox.establishedFormattingContext());
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(layoutContext);
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 parent = layoutBox.parent();
113         // Start from the top of the container's content box.
114         let previousInFlowSibling = layoutBox.previousInFlowSibling();
115         let contentBottom = previousInFlowSibling ? previousInFlowSibling.bottomRight().top() + this.marginBottom(previousInFlowSibling) : parent.contentBox().top();
116         let position = new LayoutPoint(contentBottom, parent.contentBox().left());
117         position.moveBy(new LayoutSize(this.marginLeft(layoutBox), this.marginTop(layoutBox)));
118         this.toDisplayBox(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.rootContainer())
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(layoutContext) {
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._computeStaticPosition(outOfFlowBox);
143             layoutContext.layoutFormattingContext(outOfFlowBox.establishedFormattingContext());
144             this.computeHeight(outOfFlowBox);
145             this._computeOutOfFlowPosition(outOfFlowBox);
146             this._removeFromLayoutQueue(outOfFlowBox);
147         }
148     }
149
150     _computeOutOfFlowWidth(layoutBox) {
151         // 10.3.7 Absolutely positioned, non-replaced elements
152
153         // 1. 'left' and 'width' are 'auto' and 'right' is not 'auto', then the width is shrink-to-fit. Then solve for 'left'
154         // 2. 'left' and 'right' are 'auto' and 'width' is not 'auto', then if the 'direction' property of the element establishing
155         //     the static-position containing block is 'ltr' set 'left' to the static position, otherwise set 'right' to the static position.
156         //     Then solve for 'left' (if 'direction is 'rtl') or 'right' (if 'direction' is 'ltr').
157         // 3. 'width' and 'right' are 'auto' and 'left' is not 'auto', then the width is shrink-to-fit . Then solve for 'right'
158         // 4. 'left' is 'auto', 'width' and 'right' are not 'auto', then solve for 'left'
159         // 5. 'width' is 'auto', 'left' and 'right' are not 'auto', then solve for 'width'
160         // 6. 'right' is 'auto', 'left' and 'width' are not 'auto', then solve for 'right'
161         let width = Number.NaN;
162         if (Utils.isWidthAuto(layoutBox) && Utils.isLeftAuto(layoutBox) && Utils.isRightAuto(layoutBox))
163             width = this._shrinkToFitWidth(layoutBox);
164         else if (Utils.isLeftAuto(layoutBox) && Utils.isWidthAuto(layoutBox) && !Utils.isRightAuto(layoutBox))
165             width = this._shrinkToFitWidth(layoutBox); // 1
166         else if (Utils.isLeftAuto(layoutBox) && Utils.isRightAuto(layoutBox) && !Utils.isWidthAuto(layoutBox))
167             width = Utils.width(layoutBox); // 2
168         else if (Utils.isWidthAuto(layoutBox) && Utils.isRightAuto(layoutBox) && !Utils.isLeftAuto(layoutBox))
169             width = this._shrinkToFitWidth(layoutBox); // 3
170         else if (Utils.isLeftAuto(layoutBox) && !Utils.isWidthAuto(layoutBox) && !Utils.isRightAuto(layoutBox))
171             width = Utils.width(layoutBox); // 4
172         else if (Utils.isWidthAuto(layoutBox) && !Utils.isLeftAuto(layoutBox) && !Utils.isRightAuto(layoutBox))
173             width = Math.max(0, layoutBox.containingBlock().contentBox().width() - Utils.right(layoutBox) - Utils.left(layoutBox)); // 5
174         else if (Utils.isRightAuto(layoutBox) && !Utils.isLeftAuto(layoutBox) && !Utils.isWidthAuto(layoutBox))
175             width = Utils.width(layoutBox); // 6
176         else
177             ASSERT_NOT_REACHED();
178         width += Utils.computedHorizontalBorderAndPadding(layoutBox.node());
179         this.toDisplayBox(layoutBox).setWidth(width);
180     }
181
182     _computeFloatingWidth(layoutBox) {
183         // FIXME: missing cases
184         this.toDisplayBox(layoutBox).setWidth(Utils.width(layoutBox) + Utils.computedHorizontalBorderAndPadding(layoutBox.node()));
185     }
186
187     _computeInFlowWidth(layoutBox) {
188         if (Utils.isWidthAuto(layoutBox))
189             return this.toDisplayBox(layoutBox).setWidth(this._horizontalConstraint(layoutBox));
190         return this.toDisplayBox(layoutBox).setWidth(Utils.width(layoutBox) + Utils.computedHorizontalBorderAndPadding(layoutBox.node()));
191     }
192
193     _computeOutOfFlowHeight(layoutBox) {
194         // 1. If all three of 'top', 'height', and 'bottom' are auto, set 'top' to the static position and apply rule number three below.
195         // 2. If none of the three are 'auto': If both 'margin-top' and 'margin-bottom' are 'auto', solve the equation under
196         //    the extra constraint that the two margins get equal values. If one of 'margin-top' or 'margin-bottom' is 'auto',
197         //    solve the equation for that value. If the values are over-constrained, ignore the value for 'bottom' and solve for that value.
198         // Otherwise, pick the one of the following six rules that applies.
199
200         // 3. 'top' and 'height' are 'auto' and 'bottom' is not 'auto', then the height is based on the content per 10.6.7,
201         //    set 'auto' values for 'margin-top' and 'margin-bottom' to 0, and solve for 'top'
202         // 4. 'top' and 'bottom' are 'auto' and 'height' is not 'auto', then set 'top' to the static position, set 'auto' values
203         //    for 'margin-top' and 'margin-bottom' to 0, and solve for 'bottom'
204         // 5. 'height' and 'bottom' are 'auto' and 'top' is not 'auto', then the height is based on the content per 10.6.7,
205         //    set 'auto' values for 'margin-top' and 'margin-bottom' to 0, and solve for 'bottom'
206         // 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'
207         // 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'
208         // 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'
209         let height = Number.NaN;
210         if (Utils.isHeightAuto(layoutBox) && Utils.isBottomAuto(layoutBox) && Utils.isTopAuto(layoutBox))
211             height = this._contentHeight(layoutBox); // 1
212         else if (Utils.isTopAuto((layoutBox)) && Utils.isHeightAuto((layoutBox)) && !Utils.isBottomAuto((layoutBox)))
213             height = this._contentHeight(layoutBox); // 3
214         else if (Utils.isTopAuto((layoutBox)) && Utils.isBottomAuto((layoutBox)) && !Utils.isHeightAuto((layoutBox)))
215             height = Utils.height(layoutBox); // 4
216         else if (Utils.isHeightAuto((layoutBox)) && Utils.isBottomAuto((layoutBox)) && !Utils.isTopAuto((layoutBox)))
217             height = this._contentHeight(layoutBox); // 5
218         else if (Utils.isTopAuto((layoutBox)) && !Utils.isHeightAuto((layoutBox)) && !Utils.isBottomAuto((layoutBox)))
219             height = Utils.height(layoutBox); // 6
220         else if (Utils.isHeightAuto((layoutBox)) && !Utils.isTopAuto((layoutBox)) && !Utils.isBottomAuto((layoutBox)))
221             height = Math.max(0, layoutBox.containingBlock().contentBox().height() - Utils.bottom(layoutBox) - Utils.top(layoutBox)); // 7
222         else if (Utils.isBottomAuto((layoutBox)) && !Utils.isTopAuto((layoutBox)) && !Utils.isHeightAuto((layoutBox)))
223             height = Utils.height(layoutBox); // 8
224         else
225             ASSERT_NOT_REACHED();
226         height += Utils.computedVerticalBorderAndPadding(layoutBox.node());
227         this.toDisplayBox(layoutBox).setHeight(height);
228     }
229
230     _computeFloatingHeight(layoutBox) {
231         // FIXME: missing cases
232         this.toDisplayBox(layoutBox).setHeight(Utils.height(layoutBox) + Utils.computedVerticalBorderAndPadding(layoutBox.node()));
233     }
234
235     _computeInFlowHeight(layoutBox) {
236         if (Utils.isHeightAuto(layoutBox)) {
237             // Only children in the normal flow are taken into account (i.e., floating boxes and absolutely positioned boxes are ignored,
238             // and relatively positioned boxes are considered without their offset). Note that the child box may be an anonymous block box.
239
240             // The element's height is the distance from its top content edge to the first applicable of the following:
241             // 1. the bottom edge of the last line box, if the box establishes a inline formatting context with one or more lines
242             return this.toDisplayBox(layoutBox).setHeight(this._contentHeight(layoutBox) + Utils.computedVerticalBorderAndPadding(layoutBox.node()));
243         }
244         return this.toDisplayBox(layoutBox).setHeight(Utils.height(layoutBox) + Utils.computedVerticalBorderAndPadding(layoutBox.node()));
245     }
246
247     _horizontalConstraint(layoutBox) {
248         let horizontalConstraint = layoutBox.containingBlock().contentBox().width();
249         horizontalConstraint -= this.marginLeft(layoutBox) + this.marginRight(layoutBox);
250         return horizontalConstraint;
251     }
252
253     _contentHeight(layoutBox) {
254         // 10.6.3 Block-level non-replaced elements in normal flow when 'overflow' computes to 'visible'
255         // The element's height is the distance from its top content edge to the first applicable of the following:
256         // 1. the bottom edge of the last line box, if the box establishes a inline formatting context with one or more lines
257         // 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
258         //    with the element's bottom margin
259         // 3. the bottom border edge of the last in-flow child whose top margin doesn't collapse with the element's bottom margin
260         // 4. zero, otherwise
261         // Only children in the normal flow are taken into account.
262         if (!layoutBox.isContainer() || !layoutBox.hasInFlowChild())
263             return 0;
264         if (layoutBox.establishesInlineFormattingContext()) {
265             ASSERT(layoutBox.establishedFormattingContext());
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 = layoutBox.contentBox().top();
273         let bottom = this._adjustBottomWithFIXME(layoutBox);
274         return bottom - top;
275     }
276
277     _adjustBottomWithFIXME(layoutBox) {
278         let lastInFlowChild = layoutBox.lastInFlowChild();
279         let bottom = lastInFlowChild.rect().bottom() + this.marginBottom(lastInFlowChild);
280         // FIXME: margin for body
281         if (lastInFlowChild.name() == "RenderBody" && Utils.isHeightAuto(lastInFlowChild) && !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 relativePosition = layoutBox.topLeft();
295         // Top/bottom
296         if (!Utils.isTopAuto(layoutBox))
297             relativePosition.shiftTop(Utils.top(layoutBox));
298         else if (!Utils.isBottomAuto(layoutBox))
299             relativePosition.shiftTop(-Utils.bottom(layoutBox));
300         // Left/right
301         if (!Utils.isLeftAuto(layoutBox))
302             relativePosition.shiftLeft(Utils.left(layoutBox));
303         else if (!Utils.isRightAuto(layoutBox))
304             relativePosition.shiftLeft(-Utils.right(layoutBox));
305         this.toDisplayBox(layoutBox).setTopLeft(relativePosition);
306     }
307
308     _computeOutOfFlowPosition(layoutBox) {
309         let containerSize = layoutBox.containingBlock().contentBox().size();
310         // Top/bottom
311         let top = Number.NaN;
312         if (Utils.isTopAuto(layoutBox) && Utils.isBottomAuto(layoutBox)) {
313             // Convert static position to absolute.
314             top = this._toAbsolutePosition(layoutBox).top();
315         } else if (!Utils.isTopAuto(layoutBox))
316             top = Utils.top(layoutBox) + this.marginTop(layoutBox);
317         else if (!Utils.isBottomAuto(layoutBox))
318             top = containerSize.height() - Utils.bottom(layoutBox) - layoutBox.rect().height() - this.marginBottom(layoutBox);
319         else
320             ASSERT_NOT_REACHED();
321         // Left/right
322         let left = Number.NaN;
323         if (Utils.isLeftAuto(layoutBox) && Utils.isRightAuto(layoutBox)) {
324             // Convert static position to absolute.
325             left = this._toAbsolutePosition(layoutBox).left();
326         } else if (!Utils.isLeftAuto(layoutBox))
327             left = Utils.left(layoutBox) + this.marginLeft(layoutBox);
328         else if (!Utils.isRightAuto(layoutBox))
329             left = containerSize.width() - Utils.right(layoutBox) - layoutBox.rect().width() - this.marginRight(layoutBox);
330         else
331             ASSERT_NOT_REACHED();
332         this.toDisplayBox(layoutBox).setTopLeft(new LayoutPoint(top, left));
333     }
334
335     _shrinkToFitWidth(layoutBox) {
336         // FIXME: this is naive.
337         ASSERT(Utils.isWidthAuto(layoutBox));
338         if (!layoutBox.isContainer() || !layoutBox.hasChild())
339             return 0;
340         let width = 0;
341         for (let inFlowChild = layoutBox.firstInFlowChild(); inFlowChild; inFlowChild = inFlowChild.nextInFlowSibling()) {
342             let widthCandidate = Utils.isWidthAuto(inFlowChild) ? this._shrinkToFitWidth(inFlowChild) : Utils.width(inFlowChild);
343             width = Math.max(width, widthCandidate + Utils.computedHorizontalBorderAndPadding(inFlowChild.node()));
344         }
345         return width;
346     }
347 }