/* * Copyright (C) 2018 Apple Inc. All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ class LayoutPoint { constructor(top, left) { this.m_top = top; this.m_left = left; } setLeft(left) { this.m_left = left; } setTop(top) { this.m_top = top; } left() { return this.m_left; } top() { return this.m_top; } shiftLeft(distance) { this.m_left += distance; } shiftTop(distance) { this.m_top += distance; } moveBy(distance) { if (distance.top && distance.left) { this.m_top += distance.top(); this.m_left += distance.left(); } else if (distance.width && distance.height) { this.m_top += distance.height(); this.m_left += distance.width(); } } equal(other) { return this.top() == other.top() && this.left() == other.left(); } clone() { return new LayoutPoint(this.top(), this.left()); } } class LayoutSize { constructor(width, height) { this.m_width = width; this.m_height = height; } setWidth(width) { this.m_width = width; } setHeight(height) { this.m_height = height; } width() { return this.m_width; } height() { return this.m_height; } growBy(distance) { this.m_width += distance.width(); this.m_height += distance.height(); } shrinkBy(distance) { this.m_width -= distance.width(); this.m_height -= distance.height(); } isEmpty() { return this.m_width <= 0 || this.m_height <= 0; } equal(other) { return this.width() == other.width() && this.height() == other.height(); } clone() { return new LayoutSize(this.width(), this.height()); } } class LayoutRect { constructor(topLeft, size) { this.m_topLeft = topLeft.clone(); this.m_size = size.clone(); } setTop(top) { this.m_topLeft.setTop(top); } setLeft(left) { this.m_topLeft.setLeft(left); } setBottom(bottom) { this.m_size.setHeight(bottom - this.m_topLeft.top()); } setRight(right) { this.m_size.setWidth(right - this.m_topLeft.left()); } left() { return this.m_topLeft.left(); } top() { return this.m_topLeft.top(); } bottom() { return this.m_topLeft.top() + this.m_size.height(); } right() { return this.m_topLeft.left() + this.m_size.width(); } setTopLeft(topLeft) { this.m_topLeft = topLeft.clone(); } topLeft() { return this.m_topLeft.clone(); } topRight() { return new LayoutPoint(this.top(), this.right()); } bottomRight() { return new LayoutPoint(this.bottom(), this.right()); } setWidth(width) { this.m_size.setWidth(width); } setHeight(height) { this.m_size.setHeight(height); } setSize(newSize) { this.m_size = newSize.clone(); } size() { return this.m_size.clone(); } width() { return this.m_size.width(); } height() { return this.m_size.height(); } growBy(distance) { this.m_size.growBy(distance); } shrinkBy(distance) { this.m_size.shrinkBy(distance); } moveBy(distance) { this.m_topLeft.moveBy(distance); } growHorizontally(distance) { this.m_size.setWidth(this.m_size.width() + distance); } moveHorizontally(distance) { this.m_topLeft.shiftLeft(distance); } moveVertically(distance) { this.m_topLeft.shiftTop(distance); } isEmpty() { return this.m_size.isEmpty(); } equal(other) { return this.m_topLeft.equal(other.topLeft()) && this.m_size.equal(other.size()); } intersects(other) { return !this.isEmpty() && !other.isEmpty() && this.left() < other.right() && other.left() < this.right() && this.top() < other.bottom() && other.top() < this.bottom(); } contains(other) { return this.left() <= other.left() && this.right() >= other.right() && this.top() <= other.top() && this.bottom() >= other.bottom(); } clone() { return new LayoutRect(this.topLeft().clone(), this.size().clone()); } } function ASSERT_NOT_REACHED() { throw Error("Should not reach!"); } function ASSERT(statement) { if (statement) return; throw Error("Assertion failure"); } class Utils { static computedValue(strValue, baseValue) { if (strValue.indexOf("px") > -1) return parseFloat(strValue); if (strValue.indexOf("%") > -1) return parseFloat(strValue) * baseValue / 100; return Number.NaN; } static propertyIsAuto(propertyName, box) { if (box.isAnonymous()) return true; return window.getComputedStyle(box.node()).isPropertyValueInitial(propertyName); } static isWidthAuto(box) { return Utils.propertyIsAuto("width", box); } static isHeightAuto(box) { return Utils.propertyIsAuto("height", box); } static isTopAuto(box) { return Utils.propertyIsAuto("top", box); } static isLeftAuto(box) { return Utils.propertyIsAuto("left", box); } static isBottomAuto(box) { return Utils.propertyIsAuto("bottom", box); } static isRightAuto(box) { return Utils.propertyIsAuto("right", box); } static width(box) { ASSERT(!Utils.isWidthAuto(box)); return parseFloat(window.getComputedStyle(box.node()).width); } static height(box) { ASSERT(!Utils.isHeightAuto(box)); return parseFloat(window.getComputedStyle(box.node()).height); } static top(box) { return parseFloat(box.node().style.top); } static bottom(box) { return parseFloat(box.node().style.bottom); } static left(box) { return parseFloat(box.node().style.left); } static right(box) { return parseFloat(box.node().style.right); } static hasBorderTop(box) { return window.getComputedStyle(box.node()).borderTopWidth != "0px"; } static hasBorderBottom(box) { return window.getComputedStyle(box.node()).borderBottomWidth != "0px"; } static hasPaddingTop(box) { return window.getComputedStyle(box.node()).paddingTop != "0px"; } static hasPaddingBottom(box) { return window.getComputedStyle(box.node()).paddingBottom != "0px"; } static computedMarginTop(node) { return Utils.computedValue(window.getComputedStyle(node).marginTop); } static computedMarginLeft(node) { return Utils.computedValue(window.getComputedStyle(node).marginLeft); } static computedMarginBottom(node) { return Utils.computedValue(window.getComputedStyle(node).marginBottom); } static computedMarginRight(node) { return Utils.computedValue(window.getComputedStyle(node).marginRight); } static computedBorderTopLeft(node) { return new LayoutSize(Utils.computedValue(window.getComputedStyle(node).borderLeftWidth), Utils.computedValue(window.getComputedStyle(node).borderTopWidth)); } static computedBorderBottomRight(node) { return new LayoutSize(Utils.computedValue(window.getComputedStyle(node).borderRightWidth), Utils.computedValue(window.getComputedStyle(node).borderBottomWidth)); } static computedPaddingTopLeft(node) { return new LayoutSize(Utils.computedValue(window.getComputedStyle(node).paddingLeft), Utils.computedValue(window.getComputedStyle(node).paddingTop)); } static computedPaddingBottomRight(node) { return new LayoutSize(Utils.computedValue(window.getComputedStyle(node).paddingRight), Utils.computedValue(window.getComputedStyle(node).paddingBottom)); } static computedBorderAndPaddingTop(node) { return Utils.computedBorderTopLeft(node).height() + Utils.computedPaddingTopLeft(node).height(); } static computedBorderAndPaddingLeft(node) { return Utils.computedBorderTopLeft(node).width() + Utils.computedPaddingTopLeft(node).width(); } static computedBorderAndPaddingTop(node) { return Utils.computedBorderTopLeft(node).height() + Utils.computedPaddingTopLeft(node).height(); } static computedBorderAndPaddingLeft(node) { return Utils.computedBorderTopLeft(node).width() + Utils.computedPaddingTopLeft(node).width(); } static computedBorderAndPaddingBottom(node) { return Utils.computedBorderBottomRight(node).height() + Utils.computedPaddingBottomRight(node).height(); } static computedBorderAndPaddingRight(node) { return Utils.computedBorderBottomRight(node).width() + Utils.computedPaddingBottomRight(node).width(); } static computedHorizontalBorderAndPadding(node) { return this.computedBorderAndPaddingLeft(node) + this.computedBorderAndPaddingRight(node); } static computedVerticalBorderAndPadding(node) { return this.computedBorderAndPaddingTop(node) + this.computedBorderAndPaddingBottom(node); } static computedLineHeight(node) { return Utils.computedValue(window.getComputedStyle(node).lineHeight); } static hasClear(box) { return Utils.hasClearLeft(box) || Utils.hasClearRight(box) || Utils.hasClearBoth(box); } static hasClearLeft(box) { return window.getComputedStyle(box.node()).clear == "left"; } static hasClearRight(box) { return window.getComputedStyle(box.node()).clear == "right"; } static hasClearBoth(box) { return window.getComputedStyle(box.node()).clear == "both"; } static isBlockLevelElement(node) { if (!node) return false; let display = window.getComputedStyle(node).display; return display == "block" || display == "list-item" || display == "table"; } static isBlockContainerElement(node) { if (!node || node.nodeType != Node.ELEMENT_NODE) return false; let display = window.getComputedStyle(node).display; return display == "block" || display == "list-item" || display == "inline-block" || display == "table-cell" || display == "table-caption"; //TODO && !replaced element } static isInlineLevelElement(node) { let display = window.getComputedStyle(node).display; return display == "inline" || display == "inline-block" || display == "inline-table"; } static isTableElement(node) { let display = window.getComputedStyle(node).display; return display == "table" || display == "inline-table"; } static isInlineBlockElement(node) { if (!node || node.nodeType != Node.ELEMENT_NODE) return false; let display = window.getComputedStyle(node).display; return display == "inline-block"; } static isRelativelyPositioned(box) { if (box.isAnonymous()) return false; let node = box.node(); return window.getComputedStyle(node).position == "relative"; } static isAbsolutelyPositioned(box) { if (box.isAnonymous()) return false; let node = box.node(); return window.getComputedStyle(node).position == "absolute"; } static isFixedPositioned(box) { if (box.isAnonymous()) return false; let node = box.node(); return window.getComputedStyle(node).position == "fixed"; } static isStaticallyPositioned(box) { if (box.isAnonymous()) return true; let node = box.node(); return (Utils.propertyIsAuto("top", box) && Utils.propertyIsAuto("bottom", box)) || (Utils.propertyIsAuto("left", box) && Utils.propertyIsAuto("right", box)); } static isOverflowVisible(box) { return window.getComputedStyle(box.node()).overflow == "visible"; } static isFloatingPositioned(box) { if (box.isAnonymous()) return false; let node = box.node(); return window.getComputedStyle(node).float != "none"; } static isFloatingLeft(box) { let node = box.node(); return window.getComputedStyle(node).float == "left"; } static mapPosition(position, box, container) { ASSERT(box instanceof Display.Box); ASSERT(container instanceof Display.Box); if (box == container) return position; for (let ascendant = box.parent(); ascendant && ascendant != container; ascendant = ascendant.parent()) position.moveBy(ascendant.topLeft()); return position; } static marginBox(box, container) { let marginBox = box.marginBox(); let mappedPosition = Utils.mapPosition(marginBox.topLeft(), box, container); return new LayoutRect(mappedPosition, marginBox.size()); } static borderBox(box, container) { let borderBox = box.borderBox(); let mappedPosition = Utils.mapPosition(box.topLeft(), box, container); mappedPosition.moveBy(borderBox.topLeft()); return new LayoutRect(mappedPosition, borderBox.size()); } static contentBox(box, container) { let contentBox = box.contentBox(); let mappedPosition = Utils.mapPosition(box.topLeft(), box, container); mappedPosition.moveBy(contentBox.topLeft()); return new LayoutRect(mappedPosition, contentBox.size()); } static textRuns(text, container) { return window.collectTextRuns(text, container.node()); } static textRunsForLine(text, availableSpace, container) { return window.collectTextRuns(text, container.node(), availableSpace); } static nextBreakingOpportunity(textBox, currentPosition) { return window.nextBreakingOpportunity(textBox.content(), currentPosition); } static measureText(texBox, start, end) { return texBox.node().textWidth(start, end); } static textHeight(textBox) { return textBox.text().node().textHeight(); } static layoutBoxById(layoutBoxId, box) { if (box.id() == layoutBoxId) return box; if (!box.isContainer()) return null; // Super inefficient but this is all temporary anyway. for (let child = box.firstChild(); child; child = child.nextSibling()) { if (child.id() == layoutBoxId) return child; let foundIt = Utils.layoutBoxById(layoutBoxId, child); if (foundIt) return foundIt; } return null; } // "RenderView at (0,0) size 1317x366\n HTML RenderBlock at (0,0) size 1317x116\n BODY RenderBody at (8,8) size 1301x100\n DIV RenderBlock at (0,0) size 100x100\n"; static layoutTreeDump(layoutState) { return this._dumpBox(layoutState, layoutState.rootContainer(), 1) + this._dumpTree(layoutState, layoutState.rootContainer(), 2); } static _dumpBox(layoutState, box, level) { // Skip anonymous boxes for now -This is the case where WebKit does not generate an anon inline container for text content where the text is a direct child // of a block container. let indentation = " ".repeat(level); if (box.isInlineBox()) { if (box.text()) return indentation + "#text RenderText\n"; } if (box.name() == "RenderInline") { if (box.isInFlowPositioned()) { let displayBox = layoutState.displayBox(box); let boxRect = displayBox.rect(); return indentation + box.node().tagName + " " + box.name() + " (" + Utils.precisionRoundWithDecimals(boxRect.left()) + ", " + Utils.precisionRoundWithDecimals(boxRect.top()) + ")\n"; } return indentation + box.node().tagName + " " + box.name() + "\n"; } if (box.isAnonymous()) return ""; let displayBox = layoutState.displayBox(box); let boxRect = displayBox.rect(); return indentation + (box.node().tagName ? (box.node().tagName + " ") : "") + box.name() + " at (" + Utils.precisionRound(boxRect.left()) + "," + Utils.precisionRound(boxRect.top()) + ") size " + Utils.precisionRound(boxRect.width()) + "x" + Utils.precisionRound(boxRect.height()) + "\n"; } static _dumpLines(layoutState, root, level) { ASSERT(root.establishesInlineFormattingContext()); let inlineFormattingState = layoutState.establishedFormattingState(root); let lines = inlineFormattingState.lines(); let content = ""; let indentation = " ".repeat(level); lines.forEach(function(line) { let lineRect = line.rect(); content += indentation + "RootInlineBox at (" + lineRect.left() + "," + lineRect.top() + ") size " + Utils.precisionRound(lineRect.width()) + "x" + lineRect.height() + "\n"; line.lineBoxes().forEach(function(lineBox) { let indentation = " ".repeat(level + 1); let inlineBoxName = lineBox.startPosition === undefined ? "InlineBox" : "InlineTextBox"; content += indentation + inlineBoxName + " at (" + Utils.precisionRound(lineBox.lineBoxRect.left()) + "," + Utils.precisionRound(lineBox.lineBoxRect.top()) + ") size " + Utils.precisionRound(lineBox.lineBoxRect.width()) + "x" + lineBox.lineBoxRect.height() + "\n"; }); }); return content; } static _dumpTree(layoutState, root, level) { let content = ""; if (root.isBlockContainerBox() && root.establishesInlineFormattingContext()) content += this._dumpLines(layoutState, root, level); for (let child = root.firstChild(); child; child = child.nextSibling()) { content += this._dumpBox(layoutState, child, level); if (child.isContainer()) content += this._dumpTree(layoutState, child, level + 1, content); } return content; } static precisionRoundWithDecimals(number) { return number.toFixed(2); } static precisionRound(number) { let factor = Math.pow(10, 2); return Math.round(number * factor) / factor; } }