blob: 4d64d17d1921b7daee6edcea76fdd30b30c5edef [file] [log] [blame]
/*
* 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. AND ITS CONTRIBUTORS ``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 ITS 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.
*/
#include "config.h"
#include "BlockFormattingGeometry.h"
#if ENABLE(LAYOUT_FORMATTING_CONTEXT)
#include "BlockFormattingContext.h"
#include "BlockFormattingQuirks.h"
#include "BlockMarginCollapse.h"
#include "InlineFormattingState.h"
#include "LayoutBoxGeometry.h"
#include "LayoutChildIterator.h"
#include "LayoutContext.h"
#include "LayoutInitialContainingBlock.h"
#include "LayoutReplacedBox.h"
#include "Logging.h"
#include <wtf/text/TextStream.h>
namespace WebCore {
namespace Layout {
BlockFormattingGeometry::BlockFormattingGeometry(const BlockFormattingContext& blockFormattingContext)
: FormattingGeometry(blockFormattingContext)
{
}
ContentHeightAndMargin BlockFormattingGeometry::inFlowNonReplacedContentHeightAndMargin(const Box& layoutBox, const HorizontalConstraints& horizontalConstraints, const OverriddenVerticalValues& overriddenVerticalValues) const
{
ASSERT(layoutBox.isInFlow() && !layoutBox.isReplacedBox());
ASSERT(layoutBox.isOverflowVisible());
auto compute = [&](const auto& overriddenVerticalValues) -> ContentHeightAndMargin {
// 10.6.3 Block-level non-replaced elements in normal flow when 'overflow' computes to 'visible'
//
// If 'margin-top', or 'margin-bottom' are 'auto', their used value is 0.
// If 'height' is 'auto', the height depends on whether the element has any block-level children and whether it has padding or borders:
// The element's height is the distance from its top content edge to the first applicable of the following:
// 1. the bottom edge of the last line box, if the box establishes a inline formatting context with one or more lines
// 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 with the element's bottom margin
// 3. the bottom border edge of the last in-flow child whose top margin doesn't collapse with the element's bottom margin
// 4. zero, otherwise
// Only children in the normal flow are taken into account (i.e., floating boxes and absolutely positioned boxes are ignored,
// and relatively positioned boxes are considered without their offset). Note that the child box may be an anonymous block box.
auto& boxGeometry = formattingContext().geometryForBox(layoutBox);
auto computedVerticalMargin = FormattingGeometry::computedVerticalMargin(layoutBox, horizontalConstraints);
auto nonCollapsedMargin = UsedVerticalMargin::NonCollapsedValues { computedVerticalMargin.before.value_or(0), computedVerticalMargin.after.value_or(0) };
auto borderAndPaddingTop = boxGeometry.borderTop() + boxGeometry.paddingTop().value_or(0);
auto height = overriddenVerticalValues.height ? overriddenVerticalValues.height.value() : computedHeight(layoutBox);
if (height)
return { *height, nonCollapsedMargin };
if (!is<ContainerBox>(layoutBox) || !downcast<ContainerBox>(layoutBox).hasInFlowChild())
return { 0, nonCollapsedMargin };
// 1. the bottom edge of the last line box, if the box establishes a inline formatting context with one or more lines
auto& layoutContainer = downcast<ContainerBox>(layoutBox);
if (layoutContainer.establishesInlineFormattingContext()) {
auto& inlineFormattingState = layoutState().formattingStateForInlineFormattingContext(layoutContainer);
auto& lines = inlineFormattingState.lines();
// Even empty containers generate one line.
ASSERT(!lines.isEmpty());
return { toLayoutUnit(lines.last().lineBoxLogicalRect().bottom() + inlineFormattingState.clearGapAfterLastLine()) - borderAndPaddingTop, nonCollapsedMargin };
}
// 2. the bottom edge of the bottom (possibly collapsed) margin of its last in-flow child, if the child's bottom margin...
auto marginCollapse = BlockMarginCollapse { layoutState(), formattingContext().formattingState() };
auto& lastInFlowChild = *layoutContainer.lastInFlowChild();
if (!marginCollapse.marginAfterCollapsesWithParentMarginAfter(lastInFlowChild)) {
auto& lastInFlowBoxGeometry = formattingContext().geometryForBox(lastInFlowChild);
auto bottomEdgeOfBottomMargin = BoxGeometry::borderBoxRect(lastInFlowBoxGeometry).bottom() + lastInFlowBoxGeometry.marginAfter();
return { bottomEdgeOfBottomMargin - borderAndPaddingTop, nonCollapsedMargin };
}
// 3. the bottom border edge of the last in-flow child whose top margin doesn't collapse with the element's bottom margin
auto* inFlowChild = &lastInFlowChild;
while (inFlowChild && marginCollapse.marginBeforeCollapsesWithParentMarginAfter(*inFlowChild))
inFlowChild = inFlowChild->previousInFlowSibling();
if (inFlowChild) {
auto& inFlowBoxGeometry = formattingContext().geometryForBox(*inFlowChild);
return { BoxGeometry::borderBoxTop(inFlowBoxGeometry) + inFlowBoxGeometry.borderBox().height() - borderAndPaddingTop, nonCollapsedMargin };
}
// 4. zero, otherwise
return { 0, nonCollapsedMargin };
};
// 10.6.7 'Auto' heights for block-level formatting context boxes.
auto isAutoHeight = !overriddenVerticalValues.height && !computedHeight(layoutBox);
if (isAutoHeight && (layoutBox.establishesFormattingContext() && !layoutBox.establishesInlineFormattingContext()))
return compute( OverriddenVerticalValues { contentHeightForFormattingContextRoot(downcast<ContainerBox>(layoutBox)) });
return compute(overriddenVerticalValues);
}
ContentWidthAndMargin BlockFormattingGeometry::inFlowNonReplacedContentWidthAndMargin(const Box& layoutBox, const HorizontalConstraints& horizontalConstraints, const OverriddenHorizontalValues& overriddenHorizontalValues) const
{
ASSERT(layoutBox.isInFlow());
auto compute = [&]() {
// 10.3.3 Block-level, non-replaced elements in normal flow
//
// The following constraints must hold among the used values of the other properties:
// 'margin-left' + 'border-left-width' + 'padding-left' + 'width' + 'padding-right' + 'border-right-width' + 'margin-right' = width of containing block
//
// 1. If 'width' is not 'auto' and 'border-left-width' + 'padding-left' + 'width' + 'padding-right' + 'border-right-width'
// (plus any of 'margin-left' or 'margin-right' that are not 'auto') is larger than the width of the containing block, then
// any 'auto' values for 'margin-left' or 'margin-right' are, for the following rules, treated as zero.
//
// 2. If all of the above have a computed value other than 'auto', the values are said to be "over-constrained" and one of the used values will
// have to be different from its computed value. If the 'direction' property of the containing block has the value 'ltr', the specified value
// of 'margin-right' is ignored and the value is calculated so as to make the equality true. If the value of 'direction' is 'rtl',
// this happens to 'margin-left' instead.
//
// 3. If there is exactly one value specified as 'auto', its used value follows from the equality.
//
// 4. If 'width' is set to 'auto', any other 'auto' values become '0' and 'width' follows from the resulting equality.
//
// 5. If both 'margin-left' and 'margin-right' are 'auto', their used values are equal. This horizontally centers the element with respect to the
// edges of the containing block.
auto containingBlockWidth = horizontalConstraints.logicalWidth;
auto& containingBlockStyle = layoutBox.containingBlock().style();
auto& boxGeometry = formattingContext().geometryForBox(layoutBox);
auto width = overriddenHorizontalValues.width ? overriddenHorizontalValues.width : computedWidth(layoutBox, containingBlockWidth);
auto computedHorizontalMargin = FormattingGeometry::computedHorizontalMargin(layoutBox, horizontalConstraints);
UsedHorizontalMargin usedHorizontalMargin;
auto borderLeft = boxGeometry.borderLeft();
auto borderRight = boxGeometry.borderRight();
auto paddingLeft = boxGeometry.paddingLeft().value_or(0);
auto paddingRight = boxGeometry.paddingRight().value_or(0);
// #1
if (width) {
auto horizontalSpaceForMargin = containingBlockWidth - (computedHorizontalMargin.start.value_or(0) + borderLeft + paddingLeft + *width + paddingRight + borderRight + computedHorizontalMargin.end.value_or(0));
if (horizontalSpaceForMargin < 0)
usedHorizontalMargin = { computedHorizontalMargin.start.value_or(0), computedHorizontalMargin.end.value_or(0) };
}
// #2
if (width && computedHorizontalMargin.start && computedHorizontalMargin.end) {
if (containingBlockStyle.isLeftToRightDirection()) {
usedHorizontalMargin.start = *computedHorizontalMargin.start;
usedHorizontalMargin.end = containingBlockWidth - (usedHorizontalMargin.start + borderLeft + paddingLeft + *width + paddingRight + borderRight);
} else {
usedHorizontalMargin.end = *computedHorizontalMargin.end;
usedHorizontalMargin.start = containingBlockWidth - (borderLeft + paddingLeft + *width + paddingRight + borderRight + usedHorizontalMargin.end);
}
}
// #3
if (!computedHorizontalMargin.start && width && computedHorizontalMargin.end) {
usedHorizontalMargin.end = *computedHorizontalMargin.end;
usedHorizontalMargin.start = containingBlockWidth - (borderLeft + paddingLeft + *width + paddingRight + borderRight + usedHorizontalMargin.end);
} else if (computedHorizontalMargin.start && !width && computedHorizontalMargin.end) {
usedHorizontalMargin = { *computedHorizontalMargin.start, *computedHorizontalMargin.end };
width = containingBlockWidth - (usedHorizontalMargin.start + borderLeft + paddingLeft + paddingRight + borderRight + usedHorizontalMargin.end);
} else if (computedHorizontalMargin.start && width && !computedHorizontalMargin.end) {
usedHorizontalMargin.start = *computedHorizontalMargin.start;
usedHorizontalMargin.end = containingBlockWidth - (usedHorizontalMargin.start + borderLeft + paddingLeft + *width + paddingRight + borderRight);
}
// #4
if (!width) {
usedHorizontalMargin = { computedHorizontalMargin.start.value_or(0), computedHorizontalMargin.end.value_or(0) };
width = containingBlockWidth - (usedHorizontalMargin.start + borderLeft + paddingLeft + paddingRight + borderRight + usedHorizontalMargin.end);
}
// #5
if (!computedHorizontalMargin.start && !computedHorizontalMargin.end) {
auto horizontalSpaceForMargin = containingBlockWidth - (borderLeft + paddingLeft + *width + paddingRight + borderRight);
usedHorizontalMargin = { horizontalSpaceForMargin / 2, horizontalSpaceForMargin / 2 };
}
auto shouldApplyCenterAlignForBlockContent = containingBlockStyle.textAlign() == TextAlignMode::WebKitCenter && (computedHorizontalMargin.start || computedHorizontalMargin.end);
if (shouldApplyCenterAlignForBlockContent) {
auto borderBoxWidth = (borderLeft + paddingLeft + *width + paddingRight + borderRight);
auto marginStart = computedHorizontalMargin.start.value_or(0);
auto marginEnd = computedHorizontalMargin.end.value_or(0);
auto centeredLogicalLeftForMarginBox = std::max((containingBlockWidth - borderBoxWidth - marginStart - marginEnd) / 2, 0_lu);
usedHorizontalMargin.start = centeredLogicalLeftForMarginBox + marginStart;
usedHorizontalMargin.end = containingBlockWidth - borderBoxWidth - marginStart + marginEnd;
}
ASSERT(width);
return ContentWidthAndMargin { *width, usedHorizontalMargin };
};
auto contentWidthAndMargin = compute();
LOG_WITH_STREAM(FormattingContextLayout, stream << "[Width][Margin] -> inflow non-replaced -> width(" << contentWidthAndMargin.contentWidth << "px) margin(" << contentWidthAndMargin.usedMargin.start << "px, " << contentWidthAndMargin.usedMargin.end << "px) -> layoutBox(" << &layoutBox << ")");
return contentWidthAndMargin;
}
ContentWidthAndMargin BlockFormattingGeometry::inFlowReplacedContentWidthAndMargin(const ReplacedBox& replacedBox, const HorizontalConstraints& horizontalConstraints, const OverriddenHorizontalValues& overriddenHorizontalValues) const
{
ASSERT(replacedBox.isInFlow());
// 10.3.4 Block-level, replaced elements in normal flow
//
// 1. The used value of 'width' is determined as for inline replaced elements.
// 2. Then the rules for non-replaced block-level elements are applied to determine the margins.
// #1
auto usedWidth = inlineReplacedContentWidthAndMargin(replacedBox, horizontalConstraints, { }, overriddenHorizontalValues).contentWidth;
// #2
auto nonReplacedWidthAndMargin = inFlowNonReplacedContentWidthAndMargin(replacedBox, horizontalConstraints, OverriddenHorizontalValues { usedWidth, overriddenHorizontalValues.margin });
LOG_WITH_STREAM(FormattingContextLayout, stream << "[Width][Margin] -> inflow replaced -> width(" << usedWidth << "px) margin(" << nonReplacedWidthAndMargin.usedMargin.start << "px, " << nonReplacedWidthAndMargin.usedMargin.end << "px) -> layoutBox(" << &replacedBox << ")");
return { usedWidth, nonReplacedWidthAndMargin.usedMargin };
}
LayoutUnit BlockFormattingGeometry::staticVerticalPosition(const Box& layoutBox, LayoutUnit containingBlockContentBoxTop) const
{
// https://www.w3.org/TR/CSS22/visuren.html#block-formatting
// In a block formatting context, boxes are laid out one after the other, vertically, beginning at the top of a containing block.
// The vertical distance between two sibling boxes is determined by the 'margin' properties.
// Vertical margins between adjacent block-level boxes in a block formatting context collapse.
if (auto* previousInFlowSibling = layoutBox.previousInFlowSibling()) {
auto& previousInFlowBoxGeometry = formattingContext().geometryForBox(*previousInFlowSibling);
return BoxGeometry::borderBoxRect(previousInFlowBoxGeometry).bottom() + previousInFlowBoxGeometry.marginAfter();
}
return containingBlockContentBoxTop;
}
LayoutUnit BlockFormattingGeometry::staticHorizontalPosition(const Box& layoutBox, const HorizontalConstraints& horizontalConstraints) const
{
// https://www.w3.org/TR/CSS22/visuren.html#block-formatting
// In a block formatting context, each box's left outer edge touches the left edge of the containing block (for right-to-left formatting, right edges touch).
return horizontalConstraints.logicalLeft + formattingContext().geometryForBox(layoutBox).marginStart();
}
ContentHeightAndMargin BlockFormattingGeometry::inFlowContentHeightAndMargin(const Box& layoutBox, const HorizontalConstraints& horizontalConstraints, const OverriddenVerticalValues& overriddenVerticalValues) const
{
ASSERT(layoutBox.isInFlow());
// 10.6.2 Inline replaced elements, block-level replaced elements in normal flow, 'inline-block'
// replaced elements in normal flow and floating replaced elements
if (layoutBox.isReplacedBox())
return inlineReplacedContentHeightAndMargin(downcast<ReplacedBox>(layoutBox), horizontalConstraints, { }, overriddenVerticalValues);
ContentHeightAndMargin contentHeightAndMargin;
if (layoutBox.isOverflowVisible() && !layoutBox.isDocumentBox()) {
// TODO: Figure out the case for the document element. Let's just complicated-case it for now.
contentHeightAndMargin = inFlowNonReplacedContentHeightAndMargin(layoutBox, horizontalConstraints, overriddenVerticalValues);
} else {
// 10.6.6 Complicated cases
// Block-level, non-replaced elements in normal flow when 'overflow' does not compute to 'visible' (except if the 'overflow' property's value has been propagated to the viewport).
contentHeightAndMargin = complicatedCases(layoutBox, horizontalConstraints, overriddenVerticalValues);
}
if (layoutState().inQuirksMode()) {
if (auto stretchedInFlowHeight = formattingContext().formattingQuirks().stretchedInFlowHeightIfApplicable(layoutBox, contentHeightAndMargin))
contentHeightAndMargin.contentHeight = *stretchedInFlowHeight;
}
LOG_WITH_STREAM(FormattingContextLayout, stream << "[Height][Margin] -> inflow non-replaced -> streched to viewport -> height(" << contentHeightAndMargin.contentHeight << "px) margin(" << contentHeightAndMargin.nonCollapsedMargin.before << "px, " << contentHeightAndMargin.nonCollapsedMargin.after << "px) -> layoutBox(" << &layoutBox << ")");
return contentHeightAndMargin;
}
ContentWidthAndMargin BlockFormattingGeometry::inFlowContentWidthAndMargin(const Box& layoutBox, const HorizontalConstraints& horizontalConstraints, const OverriddenHorizontalValues& overriddenHorizontalValues) const
{
ASSERT(layoutBox.isInFlow());
if (!layoutBox.isReplacedBox())
return inFlowNonReplacedContentWidthAndMargin(layoutBox, horizontalConstraints, overriddenHorizontalValues);
return inFlowReplacedContentWidthAndMargin(downcast<ReplacedBox>(layoutBox), horizontalConstraints, overriddenHorizontalValues);
}
ContentWidthAndMargin BlockFormattingGeometry::computedContentWidthAndMargin(const Box& layoutBox, const HorizontalConstraints& horizontalConstraints, std::optional<LayoutUnit> availableWidthFloatAvoider) const
{
auto compute = [&] (auto constraintsForWidth, std::optional<LayoutUnit> usedWidth) {
if (layoutBox.isFloatingPositioned())
return floatingContentWidthAndMargin(layoutBox, constraintsForWidth, { usedWidth, { } });
if (layoutBox.isInFlow())
return inFlowContentWidthAndMargin(layoutBox, constraintsForWidth, { usedWidth, { } });
ASSERT_NOT_REACHED();
return ContentWidthAndMargin { };
};
auto horizontalConstraintsForWidth = horizontalConstraints;
if (layoutBox.style().logicalWidth().isAuto() && availableWidthFloatAvoider) {
// While the non-auto width values should all be resolved against the containing block's width, when
// the width is auto the available horizontal space is shrunk by neighboring floats.
horizontalConstraintsForWidth.logicalWidth = *availableWidthFloatAvoider;
}
auto contentWidthAndMargin = compute(horizontalConstraintsForWidth, { });
auto availableWidth = horizontalConstraints.logicalWidth;
if (auto maxWidth = computedMaxWidth(layoutBox, availableWidth)) {
auto maxWidthAndMargin = compute(horizontalConstraints, maxWidth);
if (contentWidthAndMargin.contentWidth > maxWidthAndMargin.contentWidth)
contentWidthAndMargin = maxWidthAndMargin;
}
auto minWidth = computedMinWidth(layoutBox, availableWidth).value_or(0);
auto minWidthAndMargin = compute(horizontalConstraints, minWidth);
if (contentWidthAndMargin.contentWidth < minWidthAndMargin.contentWidth)
contentWidthAndMargin = minWidthAndMargin;
return contentWidthAndMargin;
}
IntrinsicWidthConstraints BlockFormattingGeometry::intrinsicWidthConstraints(const Box& layoutBox) const
{
auto fixedMarginBorderAndPadding = [&](auto& layoutBox) {
auto& style = layoutBox.style();
return fixedValue(style.marginStart()).value_or(0)
+ LayoutUnit { style.borderLeftWidth() }
+ fixedValue(style.paddingLeft()).value_or(0)
+ fixedValue(style.paddingRight()).value_or(0)
+ LayoutUnit { style.borderRightWidth() }
+ fixedValue(style.marginEnd()).value_or(0);
};
auto computedIntrinsicWidthConstraints = [&]() -> IntrinsicWidthConstraints {
auto logicalWidth = layoutBox.style().logicalWidth();
// Minimum/maximum width can't be depending on the containing block's width.
auto needsResolvedContainingBlockWidth = logicalWidth.isCalculated() || logicalWidth.isPercent() || logicalWidth.isRelative();
if (needsResolvedContainingBlockWidth)
return { };
if (auto width = fixedValue(logicalWidth))
return { *width, *width };
if (layoutBox.isReplacedBox()) {
auto& replacedBox = downcast<ReplacedBox>(layoutBox);
if (replacedBox.hasIntrinsicWidth()) {
auto replacedWidth = replacedBox.intrinsicWidth();
return { replacedWidth, replacedWidth };
}
return { };
}
if (!is<ContainerBox>(layoutBox) || !downcast<ContainerBox>(layoutBox).hasInFlowOrFloatingChild())
return { };
if (layoutBox.isSizeContainmentBox()) {
// The intrinsic sizes of the size containment box are determined as if the element had no content,
// following the same logic as when sizing as if empty.
return { };
}
auto& layoutState = this->layoutState();
if (layoutBox.establishesFormattingContext()) {
auto intrinsicWidthConstraints = LayoutContext::createFormattingContext(downcast<ContainerBox>(layoutBox), const_cast<LayoutState&>(layoutState))->computedIntrinsicWidthConstraints();
if (logicalWidth.isMinContent())
return { intrinsicWidthConstraints.minimum, intrinsicWidthConstraints.minimum };
if (logicalWidth.isMaxContent())
return { intrinsicWidthConstraints.maximum, intrinsicWidthConstraints.maximum };
return intrinsicWidthConstraints;
}
auto intrinsicWidthConstraints = IntrinsicWidthConstraints { };
auto& formattingState = layoutState.formattingStateForBox(layoutBox);
for (auto& child : childrenOfType<Box>(downcast<ContainerBox>(layoutBox))) {
if (child.isOutOfFlowPositioned() || (child.isFloatAvoider() && !child.hasFloatClear()))
continue;
auto childIntrinsicWidthConstraints = formattingState.intrinsicWidthConstraintsForBox(child);
ASSERT(childIntrinsicWidthConstraints);
intrinsicWidthConstraints.minimum = std::max(intrinsicWidthConstraints.minimum, childIntrinsicWidthConstraints->minimum);
intrinsicWidthConstraints.maximum = std::max(intrinsicWidthConstraints.maximum, childIntrinsicWidthConstraints->maximum);
}
return intrinsicWidthConstraints;
};
// FIXME Check for box-sizing: border-box;
auto intrinsicWidthConstraints = constrainByMinMaxWidth(layoutBox, computedIntrinsicWidthConstraints());
intrinsicWidthConstraints.expand(fixedMarginBorderAndPadding(layoutBox));
return intrinsicWidthConstraints;
}
}
}
#endif