[LayoutReloaded] Set inline-block box position.
[WebKit-https.git] / Tools / LayoutReloaded / FormattingContext / InlineFormatting / InlineFormattingContext.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
26 /*
27 class InlineFormattingContext {
28 public:
29     void layout() override;
30
31 private:
32     void handleInlineContainer(const Layout::Container&);
33     void handleInlineBlockContainer(const Layout::Container&);
34     void handleInlineContent(const Layout::Box&);
35     void handleInlineBox(const Layout::InlineBox&);
36     void handleText(const Layout::InlineBox&);
37     void handleFloatingBox(const Layout::Box&);
38 };
39 */
40 class InlineFormattingContext extends FormattingContext {
41     constructor(inlineFormattingState) {
42         super(inlineFormattingState);
43         ASSERT(this.formattingRoot().isBlockContainerBox());
44     }
45
46     layout() {
47         // 9.4.2 Inline formatting contexts
48         // In an inline formatting context, boxes are laid out horizontally, one after the other, beginning at the top of a containing block.
49         if (!this.formattingRoot().firstChild())
50             return;
51         this.m_line = this._createNewLine();
52         let inlineContainerStack = new Array();
53         this._addToLayoutQueue(this._firstInFlowChildWithNeedsLayout(this.formattingRoot()));
54         while (this._descendantNeedsLayout()) {
55             let layoutBox = this._nextInLayoutQueue();
56             if (layoutBox.isInlineContainer()) {
57                 let containerStart = inlineContainerStack.indexOf(layoutBox) == -1;
58                 this._handleInlineContainer(layoutBox, containerStart);
59                 // container start.
60                 if (containerStart) {
61                     inlineContainerStack.push(layoutBox);
62                     this._addToLayoutQueue(this._firstInFlowChildWithNeedsLayout(layoutBox));
63                     // Keep the inline container in the layout stack so that we can finish it when all the descendants are all set.
64                     continue;
65                 }
66                 // container end.
67                 inlineContainerStack.pop(layoutBox);
68             } else
69                 this._handleInlineContent(layoutBox);
70             this._clearNeedsLayoutAndMoveToNextSibling(layoutBox);
71         }
72         // Place the inflow positioned children.
73         this._placeInFlowPositionedChildren(this.formattingRoot());
74         // And take care of out-of-flow boxes as the final step.
75         this._layoutOutOfFlowDescendants(this.formattingRoot());
76         this._commitLine();
77         ASSERT(!inlineContainerStack.length);
78         ASSERT(!this.formattingState().layoutNeeded());
79    }
80
81     _handleInlineContainer(inlineContainer, containerStart) {
82         ASSERT(!inlineContainer.establishesFormattingContext());
83         if (containerStart) {
84             this._adjustLineForInlineContainerStart(inlineContainer);
85             return;
86         }
87         this._adjustLineForInlineContainerEnd(inlineContainer);
88         // Place inflow positioned children.
89         this._placeInFlowPositionedChildren(inlineContainer);
90     }
91
92
93     _handleInlineContent(layoutBox) {
94         if (layoutBox.isInlineBox())
95             this._handleInlineBox(layoutBox);
96         else if (layoutBox.isFloatingPositioned())
97             this._handleFloatingBox(layoutBox);
98         else if (layoutBox.isInlineBlockBox())
99             this._handleInlineBlock(layoutBox);
100         else
101             ASSERT_NOT_REACHED();
102     }
103
104     _handleInlineBox(inlineBox) {
105         if (inlineBox.text())
106             return this._handleText(inlineBox);
107         else
108             return this._handleReplaced(inlineBox);
109     }
110
111     _handleInlineBlock(inlineBlockBox) {
112         ASSERT(inlineBlockBox.establishesFormattingContext());
113         let displayBox = this.displayBox(inlineBlockBox);
114
115         // TODO: auto width/height and check if content actually at all.
116         this._adjustLineForInlineContainerStart(inlineBlockBox);
117         displayBox.setWidth(Utils.width(inlineBlockBox) + Utils.computedHorizontalBorderAndPadding(inlineBlockBox.node()));
118         this.layoutState().formattingContext(inlineBlockBox).layout();
119         displayBox.setHeight(Utils.height(inlineBlockBox) + Utils.computedVerticalBorderAndPadding(inlineBlockBox.node()));
120         this._line().addInlineBox(displayBox.size());
121         displayBox.setTopLeft(this._line().lastLineBox().lineBoxRect.topLeft());
122         this._adjustLineForInlineContainerEnd(inlineBlockBox);
123     }
124
125     _handleText(inlineBox) {
126         // FIXME: This is a extremely simple line breaking algorithm.
127         let currentPosition = 0;
128         let text = inlineBox.text().content();
129         while (text.length) {
130             let textRuns = Utils.textRunsForLine(text, this._line().availableWidth(), this.formattingRoot());
131             if (!textRuns.length)
132                 break;
133             for (let run of textRuns)
134                 this._line().addTextLineBox(run.startPosition, run.endPosition, new LayoutSize(run.width, Utils.textHeight(inlineBox)));
135             text = text.slice(textRuns[textRuns.length - 1].endPosition, text.length);
136             // Commit the line unless we run out of content.
137             if (text.length)
138                 this._commitLine();
139         }
140     }
141
142     _handleFloatingBox(floatingBox) {
143         this._computeFloatingWidth(floatingBox);
144         this.layoutState().formattingContext(floatingBox).layout();
145         this._computeFloatingHeight(floatingBox);
146         let displayBox = this.displayBox(floatingBox);
147         // Position this float statically first, the floating context will figure it out the final position.
148         let floatingStaticPosition = this._line().rect().topLeft();
149         if (displayBox.width() > this._line().availableWidth())
150             floatingStaticPosition = new LayoutPoint(this._line().rect().bottom(), this.displayBox(this.formattingRoot()).contentBox().left());
151         displayBox.setTopLeft(floatingStaticPosition);
152         this.floatingContext().computePosition(floatingBox);
153         // Check if the floating box is actually on the current line or got pushed further down.
154         if (displayBox.top() >= this._line().rect().bottom())
155             return;
156         let floatWidth = displayBox.width();
157         this._line().shrink(floatWidth);
158         if (Utils.isFloatingLeft(floatingBox))
159             this._line().moveContentHorizontally(floatWidth);
160     }
161
162     _handleReplaced(replacedBox) {
163         // TODO: intrinsic size and check if content actually at all.
164         let displayBox = this.displayBox(replacedBox);
165         this._adjustLineForInlineContainerStart(replacedBox);
166         displayBox.setWidth(Utils.width(replacedBox) + Utils.computedHorizontalBorderAndPadding(replacedBox.node()));
167
168         displayBox.setHeight(Utils.height(replacedBox) + Utils.computedVerticalBorderAndPadding(replacedBox.node()));
169         this._line().addInlineBox(displayBox.size());
170         displayBox.setTopLeft(this._line().lastLineBox().lineBoxRect.topLeft());
171         this._adjustLineForInlineContainerEnd(replacedBox);
172    }
173
174     _adjustLineForInlineContainerStart(inlineContainer) {
175         let offset = this.marginLeft(inlineContainer) + Utils.computedBorderAndPaddingLeft(inlineContainer.node());
176         this._line().adjustWithOffset(offset);
177     }
178
179     _adjustLineForInlineContainerEnd(inlineContainer) {
180         let offset = this.marginRight(inlineContainer) + Utils.computedBorderAndPaddingRight(inlineContainer.node());
181         this._line().adjustWithOffset(offset);
182     }
183
184     _commitLine() {
185         if (this._line().isEmpty())
186             return;
187         this.formattingState().appendLine(this._line());
188         this.m_line = this._createNewLine();
189     }
190
191     _line() {
192         return this.m_line;
193     }
194
195     _createNewLine() {
196         let lineRect = this.displayBox(this.formattingRoot()).contentBox();
197         let lines = this.formattingState().lines();
198         if (lines.length)
199             lineRect.setTop(lines[lines.length - 1].rect().bottom());
200         // Find floatings on this line.
201         // Offset the vertical position if the floating context belongs to the parent formatting context.
202         let lineTopInFloatingPosition = this._mapFloatingVerticalPosition(lineRect.top());
203         let floatingLeft = this._mapFloatingHorizontalPosition(this.floatingContext().left(lineTopInFloatingPosition));
204         let floatingRight = this._mapFloatingHorizontalPosition(this.floatingContext().right(lineTopInFloatingPosition));
205         if (!Number.isNaN(floatingLeft) && !Number.isNaN(floatingRight)) {
206             // Floats on both sides.
207             lineRect.setLeft(floatingLeft);
208             lineRect.setWidth(floatingRight - floatingLeft);
209         } else if (!Number.isNaN(floatingLeft)) {
210             lineRect.setLeft(floatingLeft);
211             lineRect.shrinkBy(new LayoutSize(floatingLeft, 0));
212         } else if (!Number.isNaN(floatingRight))
213             lineRect.setRight(floatingRight);
214
215         return new Line(lineRect.topLeft(), Utils.computedLineHeight(this.formattingRoot().node()), lineRect.width());
216     }
217
218     _mapFloatingVerticalPosition(verticalPosition) {
219         // Floats position are relative to their formatting root (which might not be this formatting root).
220         let root = this.displayBox(this.formattingRoot());
221         let floatFormattingRoot = this.displayBox(this.floatingContext().formattingRoot());
222         if (root == floatFormattingRoot)
223             return verticalPosition;
224         let rootTop = Utils.mapPosition(root.topLeft(), root, floatFormattingRoot).top();
225         return rootTop + root.contentBox().top() + verticalPosition;
226     }
227
228     _mapFloatingHorizontalPosition(horizontalPosition) {
229         if (Number.isNaN(horizontalPosition))
230             return horizontalPosition;
231         // Floats position are relative to their formatting root (which might not be this formatting root).
232         let root = this.displayBox(this.formattingRoot());
233         let floatFormattingRoot = this.displayBox(this.floatingContext().formattingRoot());
234         if (root == floatFormattingRoot)
235             return horizontalPosition;
236         let rootLeft = Utils.mapPosition(root.topLeft(), root, floatFormattingRoot).left();
237         rootLeft += root.contentBox().left();
238         // The left most float is to the right of the root.
239         if (rootLeft >= horizontalPosition)
240             return root.contentBox().left();
241         return horizontalPosition - rootLeft;
242      }
243
244     _clearNeedsLayoutAndMoveToNextSibling(layoutBox) {
245         this._removeFromLayoutQueue(layoutBox);
246         this.formattingState().clearNeedsLayout(layoutBox);
247         this._addToLayoutQueue(this._nextInFlowSiblingWithNeedsLayout(layoutBox));
248     }
249 }
250