d32583cad7e172ef481e03aad4e7dde343c11598
[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         this._adjustLineForInlineContainerEnd(inlineBlockBox);
122     }
123
124     _handleText(inlineBox) {
125         // FIXME: This is a extremely simple line breaking algorithm.
126         let currentPosition = 0;
127         let text = inlineBox.text().content();
128         while (text.length) {
129             let textRuns = Utils.textRunsForLine(text, this._line().availableWidth(), this.formattingRoot());
130             if (!textRuns.length)
131                 break;
132             for (let run of textRuns)
133                 this._line().addTextLineBox(run.startPosition, run.endPosition, new LayoutSize(run.width, Utils.textHeight(inlineBox)));
134             text = text.slice(textRuns[textRuns.length - 1].endPosition, text.length);
135             // Commit the line unless we run out of content.
136             if (text.length)
137                 this._commitLine();
138         }
139     }
140
141     _handleFloatingBox(floatingBox) {
142         this._computeFloatingWidth(floatingBox);
143         this.layoutState().formattingContext(floatingBox).layout();
144         this._computeFloatingHeight(floatingBox);
145         let displayBox = this.displayBox(floatingBox);
146         // Position this float statically first, the floating context will figure it out the final position.
147         let floatingStaticPosition = this._line().rect().topLeft();
148         if (displayBox.width() > this._line().availableWidth())
149             floatingStaticPosition = new LayoutPoint(this._line().rect().bottom(), this.displayBox(this.formattingRoot()).contentBox().left());
150         displayBox.setTopLeft(floatingStaticPosition);
151         this.floatingContext().computePosition(floatingBox);
152         // Check if the floating box is actually on the current line or got pushed further down.
153         if (displayBox.top() >= this._line().rect().bottom())
154             return;
155         let floatWidth = displayBox.width();
156         this._line().shrink(floatWidth);
157         if (Utils.isFloatingLeft(floatingBox))
158             this._line().moveContentHorizontally(floatWidth);
159     }
160
161     _handleReplaced(replacedBox) {
162         // TODO: intrinsic size and check if content actually at all.
163         let displayBox = this.displayBox(replacedBox);
164         this._adjustLineForInlineContainerStart(replacedBox);
165         displayBox.setWidth(Utils.width(replacedBox) + Utils.computedHorizontalBorderAndPadding(replacedBox.node()));
166
167         displayBox.setHeight(Utils.height(replacedBox) + Utils.computedVerticalBorderAndPadding(replacedBox.node()));
168         this._line().addInlineBox(displayBox.size());
169         displayBox.setTopLeft(this._line().lastLineBox().lineBoxRect.topLeft());
170         this._adjustLineForInlineContainerEnd(replacedBox);
171    }
172
173     _adjustLineForInlineContainerStart(inlineContainer) {
174         let offset = this.marginLeft(inlineContainer) + Utils.computedBorderAndPaddingLeft(inlineContainer.node());
175         this._line().adjustWithOffset(offset);
176     }
177
178     _adjustLineForInlineContainerEnd(inlineContainer) {
179         let offset = this.marginRight(inlineContainer) + Utils.computedBorderAndPaddingRight(inlineContainer.node());
180         this._line().adjustWithOffset(offset);
181     }
182
183     _commitLine() {
184         if (this._line().isEmpty())
185             return;
186         this.formattingState().appendLine(this._line());
187         this.m_line = this._createNewLine();
188     }
189
190     _line() {
191         return this.m_line;
192     }
193
194     _createNewLine() {
195         let lineRect = this.displayBox(this.formattingRoot()).contentBox();
196         let lines = this.formattingState().lines();
197         if (lines.length)
198             lineRect.setTop(lines[lines.length - 1].rect().bottom());
199         // Find floatings on this line.
200         // Offset the vertical position if the floating context belongs to the parent formatting context.
201         let lineTopInFloatingPosition = this._mapFloatingVerticalPosition(lineRect.top());
202         let floatingLeft = this._mapFloatingHorizontalPosition(this.floatingContext().left(lineTopInFloatingPosition));
203         let floatingRight = this._mapFloatingHorizontalPosition(this.floatingContext().right(lineTopInFloatingPosition));
204         if (!Number.isNaN(floatingLeft) && !Number.isNaN(floatingRight)) {
205             // Floats on both sides.
206             lineRect.setLeft(floatingLeft);
207             lineRect.setWidth(floatingRight - floatingLeft);
208         } else if (!Number.isNaN(floatingLeft)) {
209             lineRect.setLeft(floatingLeft);
210             lineRect.shrinkBy(new LayoutSize(floatingLeft, 0));
211         } else if (!Number.isNaN(floatingRight))
212             lineRect.setRight(floatingRight);
213
214         return new Line(lineRect.topLeft(), Utils.computedLineHeight(this.formattingRoot().node()), lineRect.width());
215     }
216
217     _mapFloatingVerticalPosition(verticalPosition) {
218         // Floats position are relative to their formatting root (which might not be this formatting root).
219         let root = this.displayBox(this.formattingRoot());
220         let floatFormattingRoot = this.displayBox(this.floatingContext().formattingRoot());
221         if (root == floatFormattingRoot)
222             return verticalPosition;
223         let rootTop = Utils.mapPosition(root.topLeft(), root, floatFormattingRoot).top();
224         return rootTop + root.contentBox().top() + verticalPosition;
225     }
226
227     _mapFloatingHorizontalPosition(horizontalPosition) {
228         if (Number.isNaN(horizontalPosition))
229             return horizontalPosition;
230         // Floats position are relative to their formatting root (which might not be this formatting root).
231         let root = this.displayBox(this.formattingRoot());
232         let floatFormattingRoot = this.displayBox(this.floatingContext().formattingRoot());
233         if (root == floatFormattingRoot)
234             return horizontalPosition;
235         let rootLeft = Utils.mapPosition(root.topLeft(), root, floatFormattingRoot).left();
236         rootLeft += root.contentBox().left();
237         // The left most float is to the right of the root.
238         if (rootLeft >= horizontalPosition)
239             return root.contentBox().left();
240         return horizontalPosition - rootLeft;
241      }
242
243     _clearNeedsLayoutAndMoveToNextSibling(layoutBox) {
244         this._removeFromLayoutQueue(layoutBox);
245         this.formattingState().clearNeedsLayout(layoutBox);
246         this._addToLayoutQueue(this._nextInFlowSiblingWithNeedsLayout(layoutBox));
247     }
248 }
249