| /* |
| * 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 "BlockFormattingContext.h" |
| |
| #if ENABLE(LAYOUT_FORMATTING_CONTEXT) |
| |
| #include "FormattingContext.h" |
| #include "InlineFormattingState.h" |
| #include "LayoutChildIterator.h" |
| #include "Logging.h" |
| #include <wtf/text/TextStream.h> |
| |
| namespace WebCore { |
| namespace Layout { |
| |
| HeightAndMargin BlockFormattingContext::Geometry::inFlowNonReplacedHeightAndMargin(const LayoutState& layoutState, const Box& layoutBox, Optional<LayoutUnit> usedHeight) |
| { |
| ASSERT(layoutBox.isInFlow() && !layoutBox.replaced()); |
| ASSERT(layoutBox.isOverflowVisible()); |
| |
| auto compute = [&]() -> HeightAndMargin { |
| |
| // 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& style = layoutBox.style(); |
| auto containingBlockWidth = layoutState.displayBoxForLayoutBox(*layoutBox.containingBlock()).contentBoxWidth(); |
| auto& displayBox = layoutState.displayBoxForLayoutBox(layoutBox); |
| |
| auto nonCollapsedMargin = VerticalMargin::ComputedValues { computedValueIfNotAuto(style.marginBefore(), containingBlockWidth).value_or(0), |
| computedValueIfNotAuto(style.marginAfter(), containingBlockWidth).value_or(0) }; |
| auto collapsedMargin = VerticalMargin::CollapsedValues { MarginCollapse::marginBefore(layoutState, layoutBox), MarginCollapse::marginAfter(layoutState, layoutBox) }; |
| auto borderAndPaddingTop = displayBox.borderTop() + displayBox.paddingTop().value_or(0); |
| |
| auto height = usedHeight ? usedHeight.value() : computedHeightValue(layoutState, layoutBox, HeightType::Normal); |
| if (height) |
| return { height.value(), { nonCollapsedMargin, collapsedMargin } }; |
| |
| if (!is<Container>(layoutBox) || !downcast<Container>(layoutBox).hasInFlowChild()) |
| return { 0, { nonCollapsedMargin, collapsedMargin } }; |
| |
| // 1. the bottom edge of the last line box, if the box establishes a inline formatting context with one or more lines |
| if (layoutBox.establishesInlineFormattingContext()) { |
| // This is temp and will be replaced by the correct display box once inline runs move over to the display tree. |
| auto& inlineRuns = downcast<InlineFormattingState>(layoutState.establishedFormattingState(layoutBox)).inlineRuns(); |
| auto bottomEdge = inlineRuns.isEmpty() ? LayoutUnit() : inlineRuns.last().logicalBottom(); |
| return { bottomEdge, { nonCollapsedMargin, collapsedMargin } }; |
| } |
| |
| // 2. the bottom edge of the bottom (possibly collapsed) margin of its last in-flow child, if the child's bottom margin... |
| auto* lastInFlowChild = downcast<Container>(layoutBox).lastInFlowChild(); |
| ASSERT(lastInFlowChild); |
| if (!MarginCollapse::marginAfterCollapsesWithParentMarginAfter(layoutState, *lastInFlowChild)) { |
| auto& lastInFlowDisplayBox = layoutState.displayBoxForLayoutBox(*lastInFlowChild); |
| return { lastInFlowDisplayBox.bottom() + lastInFlowDisplayBox.marginAfter() - borderAndPaddingTop, { nonCollapsedMargin, collapsedMargin } }; |
| } |
| |
| // 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(layoutState, *inFlowChild)) |
| inFlowChild = inFlowChild->previousInFlowSibling(); |
| if (inFlowChild) { |
| auto& inFlowDisplayBox = layoutState.displayBoxForLayoutBox(*inFlowChild); |
| return { inFlowDisplayBox.top() + inFlowDisplayBox.borderBox().height() - borderAndPaddingTop, { nonCollapsedMargin, collapsedMargin } }; |
| } |
| |
| // 4. zero, otherwise |
| return { 0, { nonCollapsedMargin, collapsedMargin } }; |
| }; |
| |
| auto heightAndMargin = compute(); |
| |
| LOG_WITH_STREAM(FormattingContextLayout, stream << "[Height][Margin] -> inflow non-replaced -> height(" << heightAndMargin.height << "px) margin(" << heightAndMargin.margin.usedValues().before << "px, " << heightAndMargin.margin.usedValues().after << "px) -> layoutBox(" << &layoutBox << ")"); |
| return heightAndMargin; |
| } |
| |
| WidthAndMargin BlockFormattingContext::Geometry::inFlowNonReplacedWidthAndMargin(const LayoutState& layoutState, const Box& layoutBox, Optional<LayoutUnit> usedWidth) |
| { |
| ASSERT(layoutBox.isInFlow() && !layoutBox.replaced()); |
| |
| 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& style = layoutBox.style(); |
| auto* containingBlock = layoutBox.containingBlock(); |
| auto containingBlockWidth = layoutState.displayBoxForLayoutBox(*containingBlock).contentBoxWidth(); |
| auto& displayBox = layoutState.displayBoxForLayoutBox(layoutBox); |
| |
| auto width = computedValueIfNotAuto(usedWidth ? Length { usedWidth.value(), Fixed } : style.logicalWidth(), containingBlockWidth); |
| auto marginStart = computedValueIfNotAuto(style.marginStart(), containingBlockWidth); |
| auto marginEnd = computedValueIfNotAuto(style.marginEnd(), containingBlockWidth); |
| auto nonComputedMarginStart = marginStart.value_or(0); |
| auto nonComputedMarginEnd = marginEnd.value_or(0); |
| auto borderLeft = displayBox.borderLeft(); |
| auto borderRight = displayBox.borderRight(); |
| auto paddingLeft = displayBox.paddingLeft().value_or(0); |
| auto paddingRight = displayBox.paddingRight().value_or(0); |
| |
| // #1 |
| if (width) { |
| auto horizontalSpaceForMargin = containingBlockWidth - (marginStart.value_or(0) + borderLeft + paddingLeft + *width + paddingRight + borderRight + marginEnd.value_or(0)); |
| if (horizontalSpaceForMargin < 0) { |
| marginStart = marginStart.value_or(0); |
| marginEnd = marginEnd.value_or(0); |
| } |
| } |
| |
| // #2 |
| if (width && marginStart && marginEnd) { |
| if (containingBlock->style().isLeftToRightDirection()) |
| marginEnd = containingBlockWidth - (*marginStart + borderLeft + paddingLeft + *width + paddingRight + borderRight); |
| else |
| marginStart = containingBlockWidth - (borderLeft + paddingLeft + *width + paddingRight + borderRight + *marginEnd); |
| } |
| |
| // #3 |
| if (!marginStart && width && marginEnd) |
| marginStart = containingBlockWidth - (borderLeft + paddingLeft + *width + paddingRight + borderRight + *marginEnd); |
| else if (marginStart && !width && marginEnd) |
| width = containingBlockWidth - (*marginStart + borderLeft + paddingLeft + paddingRight + borderRight + *marginEnd); |
| else if (marginStart && width && !marginEnd) |
| marginEnd = containingBlockWidth - (*marginStart + borderLeft + paddingLeft + *width + paddingRight + borderRight); |
| |
| // #4 |
| if (!width) { |
| marginStart = marginStart.value_or(0); |
| marginEnd = marginEnd.value_or(0); |
| width = containingBlockWidth - (*marginStart + borderLeft + paddingLeft + paddingRight + borderRight + *marginEnd); |
| } |
| |
| // #5 |
| if (!marginStart && !marginEnd) { |
| auto horizontalSpaceForMargin = containingBlockWidth - (borderLeft + paddingLeft + *width + paddingRight + borderRight); |
| marginStart = marginEnd = horizontalSpaceForMargin / 2; |
| } |
| |
| ASSERT(width); |
| ASSERT(marginStart); |
| ASSERT(marginEnd); |
| |
| return WidthAndMargin { *width, { *marginStart, *marginEnd }, { nonComputedMarginStart, nonComputedMarginEnd } }; |
| }; |
| |
| auto widthAndMargin = compute(); |
| LOG_WITH_STREAM(FormattingContextLayout, stream << "[Width][Margin] -> inflow non-replaced -> width(" << widthAndMargin.width << "px) margin(" << widthAndMargin.margin.start << "px, " << widthAndMargin.margin.end << "px) -> layoutBox(" << &layoutBox << ")"); |
| return widthAndMargin; |
| } |
| |
| WidthAndMargin BlockFormattingContext::Geometry::inFlowReplacedWidthAndMargin(const LayoutState& layoutState, const Box& layoutBox, Optional<LayoutUnit> usedWidth) |
| { |
| ASSERT(layoutBox.isInFlow() && layoutBox.replaced()); |
| |
| // 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 width = inlineReplacedWidthAndMargin(layoutState, layoutBox, usedWidth).width; |
| // #2 |
| auto nonReplacedWidthAndMargin = inFlowNonReplacedWidthAndMargin(layoutState, layoutBox, width); |
| |
| LOG_WITH_STREAM(FormattingContextLayout, stream << "[Width][Margin] -> inflow replaced -> width(" << width << "px) margin(" << nonReplacedWidthAndMargin.margin.start << "px, " << nonReplacedWidthAndMargin.margin.end << "px) -> layoutBox(" << &layoutBox << ")"); |
| return { width, nonReplacedWidthAndMargin.margin, nonReplacedWidthAndMargin.nonComputedMargin }; |
| } |
| |
| Point BlockFormattingContext::Geometry::staticPosition(const LayoutState& layoutState, const Box& layoutBox) |
| { |
| // 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. |
| // 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). |
| |
| LayoutUnit top; |
| auto& containingBlockDisplayBox = layoutState.displayBoxForLayoutBox(*layoutBox.containingBlock()); |
| if (auto* previousInFlowSibling = layoutBox.previousInFlowSibling()) { |
| auto& previousInFlowDisplayBox = layoutState.displayBoxForLayoutBox(*previousInFlowSibling); |
| top = previousInFlowDisplayBox.bottom() + previousInFlowDisplayBox.marginAfter(); |
| } else |
| top = containingBlockDisplayBox.contentBoxTop(); |
| |
| auto left = containingBlockDisplayBox.contentBoxLeft(); |
| LOG_WITH_STREAM(FormattingContextLayout, stream << "[Position] -> static -> top(" << top << "px) left(" << left << "px) layoutBox(" << &layoutBox << ")"); |
| return { left, top }; |
| } |
| |
| HeightAndMargin BlockFormattingContext::Geometry::inFlowHeightAndMargin(const LayoutState& layoutState, const Box& layoutBox, Optional<LayoutUnit> usedHeight) |
| { |
| 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.replaced()) |
| return inlineReplacedHeightAndMargin(layoutState, layoutBox, usedHeight); |
| |
| HeightAndMargin heightAndMargin; |
| // TODO: Figure out the case for the document element. Let's just complicated-case it for now. |
| if (layoutBox.isOverflowVisible() && !layoutBox.isDocumentBox()) |
| heightAndMargin = inFlowNonReplacedHeightAndMargin(layoutState, layoutBox, usedHeight); |
| 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). |
| heightAndMargin = complicatedCases(layoutState, layoutBox, usedHeight); |
| } |
| |
| if (!Quirks::needsStretching(layoutState, layoutBox)) |
| return heightAndMargin; |
| |
| heightAndMargin = Quirks::stretchedHeight(layoutState, layoutBox, heightAndMargin); |
| |
| LOG_WITH_STREAM(FormattingContextLayout, stream << "[Height][Margin] -> inflow non-replaced -> streched to viewport -> height(" << heightAndMargin.height << "px) margin(" << heightAndMargin.margin.usedValues().before << "px, " << heightAndMargin.margin.usedValues().after << "px) -> layoutBox(" << &layoutBox << ")"); |
| return heightAndMargin; |
| } |
| |
| WidthAndMargin BlockFormattingContext::Geometry::inFlowWidthAndMargin(const LayoutState& layoutState, const Box& layoutBox, Optional<LayoutUnit> usedWidth) |
| { |
| ASSERT(layoutBox.isInFlow()); |
| |
| if (!layoutBox.replaced()) |
| return inFlowNonReplacedWidthAndMargin(layoutState, layoutBox, usedWidth); |
| return inFlowReplacedWidthAndMargin(layoutState, layoutBox, usedWidth); |
| } |
| |
| bool BlockFormattingContext::Geometry::instrinsicWidthConstraintsNeedChildrenWidth(const Box& layoutBox) |
| { |
| if (!is<Container>(layoutBox) || !downcast<Container>(layoutBox).hasInFlowOrFloatingChild()) |
| return false; |
| return layoutBox.style().width().isAuto(); |
| } |
| |
| FormattingContext::InstrinsicWidthConstraints BlockFormattingContext::Geometry::instrinsicWidthConstraints(const LayoutState& layoutState, const Box& layoutBox) |
| { |
| auto& style = layoutBox.style(); |
| if (auto width = fixedValue(style.logicalWidth())) |
| return { *width, *width }; |
| |
| // Minimum/maximum width can't be depending on the containing block's width. |
| if (!style.logicalWidth().isAuto()) |
| return { }; |
| |
| if (!is<Container>(layoutBox)) |
| return { }; |
| |
| LayoutUnit minimumIntrinsicWidth; |
| LayoutUnit maximumIntrinsicWidth; |
| |
| for (auto& child : childrenOfType<Box>(downcast<Container>(layoutBox))) { |
| if (child.isOutOfFlowPositioned()) |
| continue; |
| auto& formattingState = layoutState.formattingStateForBox(child); |
| ASSERT(formattingState.isBlockFormattingState()); |
| auto childInstrinsicWidthConstraints = formattingState.instrinsicWidthConstraints(child); |
| ASSERT(childInstrinsicWidthConstraints); |
| |
| auto& style = child.style(); |
| auto horizontalMarginBorderAndPadding = 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); |
| |
| minimumIntrinsicWidth = std::max(minimumIntrinsicWidth, childInstrinsicWidthConstraints->minimum + horizontalMarginBorderAndPadding); |
| maximumIntrinsicWidth = std::max(maximumIntrinsicWidth, childInstrinsicWidthConstraints->maximum + horizontalMarginBorderAndPadding); |
| } |
| |
| return { minimumIntrinsicWidth, maximumIntrinsicWidth }; |
| } |
| |
| LayoutUnit BlockFormattingContext::Geometry::estimatedMarginBefore(const LayoutState& layoutState, const Box& layoutBox) |
| { |
| ASSERT(layoutBox.isBlockLevelBox()); |
| // Can't estimate vertical margins for out of flow boxes (and we shouldn't need to do it for float boxes). |
| ASSERT(layoutBox.isInFlow()); |
| // Can't cross block formatting context boundary. |
| ASSERT(!layoutBox.establishesBlockFormattingContext()); |
| |
| // Let's just use the normal path for now. |
| return MarginCollapse::marginBefore(layoutState, layoutBox); |
| } |
| |
| LayoutUnit BlockFormattingContext::Geometry::estimatedMarginAfter(const LayoutState& layoutState, const Box& layoutBox) |
| { |
| ASSERT(layoutBox.isBlockLevelBox()); |
| // Can't estimate vertical margins for out of flow boxes (and we shouldn't need to do it for float boxes). |
| ASSERT(layoutBox.isInFlow()); |
| // Can't cross block formatting context boundary. |
| ASSERT(!layoutBox.establishesBlockFormattingContext()); |
| |
| // Let's just use the normal path for now. |
| return MarginCollapse::marginAfter(layoutState, layoutBox); |
| } |
| |
| } |
| } |
| |
| #endif |