| /* |
| * Copyright (C) 2022 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. |
| */ |
| #include "config.h" |
| #include "GridMasonryLayout.h" |
| |
| #include "GridLayoutFunctions.h" |
| #include "RenderBoxInlines.h" |
| #include "RenderGrid.h" |
| #include "RenderStyle+GettersInlines.h" |
| #include "StyleFlowTolerance.h" |
| #include "StyleGridPositionsResolver.h" |
| #include "StylePrimitiveNumericTypes+Evaluation.h" |
| #include "WritingMode.h" |
| |
| namespace WebCore { |
| |
| void GridMasonryLayout::initializeMasonry(unsigned gridAxisTracks, Style::GridTrackSizingDirection masonryAxisDirection) |
| { |
| // Reset global variables as they may contain state from previous runs of Masonry. |
| m_masonryAxisDirection = masonryAxisDirection; |
| m_masonryAxisGridGap = m_renderGrid->gridGap(m_masonryAxisDirection); |
| m_gridAxisTracksCount = gridAxisTracks; |
| m_gridContentSize = 0; |
| m_itemOffsets.clear(); |
| |
| m_renderGrid->currentGrid().setupGridForMasonryLayout(); |
| m_renderGrid->populateExplicitGridAndOrderIterator(); |
| |
| resizeAndResetRunningPositions(); |
| } |
| |
| void GridMasonryLayout::performMasonryPlacement(const GridTrackSizingAlgorithm& algorithm, unsigned gridAxisTracks, Style::GridTrackSizingDirection masonryAxisDirection, GridMasonryLayout::MasonryLayoutPhase layoutPhase) |
| { |
| initializeMasonry(gridAxisTracks, masonryAxisDirection); |
| |
| m_renderGrid->populateGridPositionsForDirection(algorithm, Style::GridTrackSizingDirection::Columns); |
| m_renderGrid->populateGridPositionsForDirection(algorithm, Style::GridTrackSizingDirection::Rows); |
| |
| // 2.3 Masonry Layout Algorithm |
| // https://drafts.csswg.org/css-grid-3/#masonry-layout-algorithm |
| |
| // the insertIntoGridAndLayoutItem() will modify the m_autoFlowNextCursor, so m_autoFlowNextCursor needs to be reset. |
| m_autoFlowNextCursor = 0; |
| |
| placeMasonryItems(algorithm, layoutPhase); |
| } |
| |
| void GridMasonryLayout::resizeAndResetRunningPositions() |
| { |
| m_runningPositions.fill(LayoutUnit(), m_gridAxisTracksCount); |
| } |
| |
| void GridMasonryLayout::placeMasonryItems(const GridTrackSizingAlgorithm& algorithm, GridMasonryLayout::MasonryLayoutPhase layoutPhase) |
| { |
| if (!m_gridAxisTracksCount) |
| return; |
| |
| auto& grid = m_renderGrid->currentGrid(); |
| for (CheckedPtr gridItem = grid.orderIterator().first(); gridItem; gridItem = grid.orderIterator().next()) { |
| if (grid.orderIterator().shouldSkipChild(*gridItem)) |
| continue; |
| |
| auto gridArea = hasDefiniteGridAxisPosition(*gridItem, gridAxisDirection()) ? gridAreaForDefiniteGridAxisItem(*gridItem) : gridAreaForIndefiniteGridAxisItem(*gridItem); |
| insertIntoGridAndLayoutItem(algorithm, *gridItem, gridArea, layoutPhase); |
| } |
| } |
| |
| GridArea GridMasonryLayout::gridAreaForDefiniteGridAxisItem(const RenderBox& gridItem) const |
| { |
| auto itemSpan = m_renderGrid->currentGrid().gridItemSpan(gridItem, gridAxisDirection()); |
| ASSERT(!itemSpan.isIndefinite()); |
| itemSpan.translate(m_renderGrid->currentGrid().explicitGridStart(gridAxisDirection())); |
| return masonryGridAreaFromGridAxisSpan(itemSpan); |
| } |
| |
| LayoutUnit GridMasonryLayout::calculateMasonryIntrinsicLogicalWidth(RenderBox& gridItem, GridMasonryLayout::MasonryLayoutPhase layoutPhase) |
| { |
| switch (layoutPhase) { |
| case MasonryLayoutPhase::MinContentPhase: |
| return gridItem.computeIntrinsicLogicalWidthUsing(CSS::Keyword::MinContent { }, { }, gridItem.borderAndPaddingLogicalWidth()); |
| case MasonryLayoutPhase::MaxContentPhase: |
| return gridItem.computeIntrinsicLogicalWidthUsing(CSS::Keyword::MaxContent { }, { }, gridItem.borderAndPaddingLogicalWidth()); |
| case MasonryLayoutPhase::LayoutPhase: |
| ASSERT_NOT_REACHED(); |
| return { }; |
| } |
| |
| return { }; |
| } |
| |
| void GridMasonryLayout::setItemContainingBlockToGridArea(const GridTrackSizingAlgorithm& algorithm, RenderBox& gridItem) |
| { |
| CheckedPtr<RenderGrid> containingBlock = dynamicDowncast<RenderGrid>(gridItem.containingBlock()); |
| if (!containingBlock) { |
| ASSERT_NOT_REACHED(); |
| return; |
| } |
| |
| // FIXME: We need to set both axes here because RenderGrid sets and expects them all over the place. |
| // Ideally we untangle all that and only set the grid axis that we need. webkit.org/b/305136 |
| auto direction = gridAxisDirection(); |
| if (direction == Style::GridTrackSizingDirection::Columns) { |
| gridItem.setGridAreaContentLogicalWidth(algorithm.gridAreaBreadthForGridItem(gridItem, direction)); |
| gridItem.setGridAreaContentLogicalHeight(containingBlock->availableLogicalHeightForContentBox()); |
| } else { |
| gridItem.setGridAreaContentLogicalHeight(algorithm.gridAreaBreadthForGridItem(gridItem, direction)); |
| gridItem.setGridAreaContentLogicalWidth(containingBlock->contentBoxLogicalWidth()); |
| } |
| |
| // FIXME(249230): Try to cache masonry layout sizes |
| gridItem.setChildNeedsLayout(MarkOnlyThis); |
| } |
| |
| void GridMasonryLayout::insertIntoGridAndLayoutItem(const GridTrackSizingAlgorithm& algorithm, RenderBox& gridItem, const GridArea& area, GridMasonryLayout::MasonryLayoutPhase layoutPhase) |
| { |
| auto shouldOverrideLogicalWidth = [&](RenderBox& gridItem, GridMasonryLayout::MasonryLayoutPhase layoutPhase) { |
| if (layoutPhase == MasonryLayoutPhase::LayoutPhase) |
| return false; |
| |
| if (!(gridItem.style().logicalWidth().isAuto() || gridItem.style().logicalWidth().isPercent())) |
| return false; |
| |
| ASSERT(m_renderGrid->isMasonry(Style::GridTrackSizingDirection::Columns)); |
| |
| if (gridItem.style().writingMode().isOrthogonal(m_renderGrid->style().writingMode())) |
| return false; |
| |
| if (auto* renderGrid = dynamicDowncast<RenderGrid>(gridItem); renderGrid && renderGrid->isSubgridRows()) |
| return false; |
| |
| return true; |
| }; |
| |
| if (shouldOverrideLogicalWidth(gridItem, layoutPhase)) |
| gridItem.setOverridingBorderBoxLogicalWidth(calculateMasonryIntrinsicLogicalWidth(gridItem, layoutPhase)); |
| |
| m_renderGrid->currentGrid().insert(gridItem, area); |
| setItemContainingBlockToGridArea(algorithm, gridItem); |
| gridItem.layoutIfNeeded(); |
| updateRunningPositions(gridItem, area); |
| m_autoFlowNextCursor = gridAxisSpanFromArea(area).endLine() % m_gridAxisTracksCount; |
| } |
| |
| LayoutUnit GridMasonryLayout::masonryAxisMarginBoxForItem(const RenderBox& gridItem) |
| { |
| LayoutUnit marginBoxSize; |
| if (m_masonryAxisDirection == Style::GridTrackSizingDirection::Rows) { |
| if (GridLayoutFunctions::isOrthogonalGridItem(m_renderGrid, gridItem)) |
| marginBoxSize = gridItem.isHorizontalWritingMode() ? gridItem.width() + gridItem.horizontalMarginExtent() : gridItem.height() + gridItem.verticalMarginExtent(); |
| else |
| marginBoxSize = gridItem.logicalHeight() + gridItem.marginLogicalHeight(); |
| |
| } else { |
| if (GridLayoutFunctions::isOrthogonalGridItem(m_renderGrid, gridItem)) |
| marginBoxSize = gridItem.isHorizontalWritingMode() ? gridItem.height() + gridItem.verticalMarginExtent() : gridItem.width() + gridItem.horizontalMarginExtent(); |
| else |
| marginBoxSize = gridItem.logicalWidth() + gridItem.marginLogicalWidth(); |
| } |
| return marginBoxSize; |
| } |
| |
| void GridMasonryLayout::updateRunningPositions(const RenderBox& gridItem, const GridArea& area) |
| { |
| auto gridAxisSpan = gridAxisSpanFromArea(area); |
| ASSERT(gridAxisSpan.startLine() < m_runningPositions.size() && gridAxisSpan.endLine() <= m_runningPositions.size()); |
| gridAxisSpan.clamp(m_runningPositions.size()); |
| |
| LayoutUnit previousRunningPosition; |
| for (auto line : gridAxisSpan) |
| previousRunningPosition = std::max(previousRunningPosition, m_runningPositions[line]); |
| |
| auto newRunningPosition = masonryAxisMarginBoxForItem(gridItem) + previousRunningPosition + m_masonryAxisGridGap; |
| m_gridContentSize = std::max(m_gridContentSize, newRunningPosition - m_masonryAxisGridGap); |
| |
| for (auto span : gridAxisSpan) |
| m_runningPositions[span] = newRunningPosition; |
| |
| updateItemOffset(gridItem, previousRunningPosition); |
| } |
| |
| void GridMasonryLayout::updateItemOffset(const RenderBox& gridItem, LayoutUnit offset) |
| { |
| // We set() and not add() to update the value if the |gridItem| is already inserted |
| m_itemOffsets.set(gridItem, offset); |
| } |
| |
| LayoutUnit GridMasonryLayout::maxRunningPositionForSpan(unsigned startLine, unsigned spanLength) const |
| { |
| LayoutUnit maxPosition; |
| for (unsigned lineOffset = 0; lineOffset < spanLength; lineOffset++) |
| maxPosition = std::max(maxPosition, m_runningPositions[startLine + lineOffset]); |
| return maxPosition; |
| } |
| |
| GridArea GridMasonryLayout::gridAreaForIndefiniteGridAxisItem(const RenderBox& item) |
| { |
| auto itemSpanLength = Style::GridPositionsResolver::spanSizeForAutoPlacedItem(item, gridAxisDirection()); |
| auto gridAxisLines = m_gridAxisTracksCount + 1; |
| |
| // Get flow-tolerance from the masonry container's style |
| const auto& tolerance = m_renderGrid->style().flowTolerance(); |
| |
| if (tolerance.isInfinite()) { |
| // Infinite tolerance: place items strictly in order without considering track lengths |
| // Use round-robin placement starting from the cursor position |
| auto startingLine = m_autoFlowNextCursor; |
| |
| // If the item doesn't fit at the cursor position, wrap to the beginning |
| if (startingLine + itemSpanLength > m_gridAxisTracksCount) |
| startingLine = 0; |
| |
| auto gridAxisPosition = GridSpan::translatedDefiniteGridSpan(startingLine, startingLine + itemSpanLength); |
| return masonryGridAreaFromGridAxisSpan(gridAxisPosition); |
| } |
| |
| // For normal and length-percentage tolerances, find positions within tolerance of the shortest track |
| // Calculate tolerance value |
| auto contentBoxSize = gridAxisDirection() == Style::GridTrackSizingDirection::Columns |
| ? m_renderGrid->contentBoxLogicalHeight() |
| : m_renderGrid->contentBoxLogicalWidth(); |
| |
| LayoutUnit toleranceValue = tolerance.switchOn( |
| [&](const CSS::Keyword::Normal&) -> LayoutUnit { |
| // Normal resolves to 1em |
| return LayoutUnit { m_renderGrid->checkedStyle()->computedFontSize() }; |
| }, |
| [&](const typename Style::FlowTolerance::Fixed& fixed) -> LayoutUnit { |
| return LayoutUnit { fixed.resolveZoom(m_renderGrid->style().usedZoomForLength()) }; |
| }, |
| [&](const typename Style::FlowTolerance::Percentage& percentage) -> LayoutUnit { |
| return Style::evaluate<LayoutUnit>(percentage, contentBoxSize); |
| }, |
| [&](const typename Style::FlowTolerance::Calc& calc) -> LayoutUnit { |
| return Style::evaluate<LayoutUnit>(calc, contentBoxSize, m_renderGrid->style().usedZoomForLength()); |
| }, |
| [](const CSS::Keyword::Infinite&) -> LayoutUnit { |
| // This case shouldn't be reached due to the outer if check |
| ASSERT_NOT_REACHED(); |
| return LayoutUnit { }; |
| } |
| ); |
| |
| // Step 1: Find the absolute shortest position across all tracks |
| auto maxStartingLine = gridAxisLines - itemSpanLength; |
| LayoutUnit absoluteShortest = LayoutUnit::max(); |
| for (unsigned i = 0; i < maxStartingLine; i++) |
| absoluteShortest = std::min(absoluteShortest, maxRunningPositionForSpan(i, itemSpanLength)); |
| |
| // Step 2: Find first position within tolerance of shortest, starting from the cursor position. |
| unsigned smallestMaxPosLine = 0; |
| auto autoFlowNextCursorShift = (m_autoFlowNextCursor > maxStartingLine) ? 0 : m_autoFlowNextCursor; |
| for (unsigned i = 0; i < maxStartingLine; i++) { |
| auto startingLine = (autoFlowNextCursorShift + i) % maxStartingLine; |
| |
| auto maxPosForCurrentStartingLine = maxRunningPositionForSpan(startingLine, itemSpanLength); |
| |
| // Accept first position within tolerance of the absolute shortest |
| if (maxPosForCurrentStartingLine <= absoluteShortest + toleranceValue) { |
| smallestMaxPosLine = startingLine; |
| break; |
| } |
| } |
| |
| auto gridAxisPosition = GridSpan::translatedDefiniteGridSpan(smallestMaxPosLine, smallestMaxPosLine + itemSpanLength); |
| return masonryGridAreaFromGridAxisSpan(gridAxisPosition); |
| } |
| |
| LayoutUnit GridMasonryLayout::offsetForGridItem(const RenderBox& gridItem) const |
| { |
| const auto& offsetIter = m_itemOffsets.find(gridItem); |
| if (offsetIter == m_itemOffsets.end()) |
| return 0_lu; |
| return offsetIter->value; |
| } |
| |
| inline Style::GridTrackSizingDirection GridMasonryLayout::gridAxisDirection() const |
| { |
| // The masonry axis and grid axis can never be the same. |
| // They are always perpendicular to each other. |
| return orthogonalDirection(m_masonryAxisDirection); |
| } |
| |
| bool GridMasonryLayout::hasDefiniteGridAxisPosition(const RenderBox& gridItem, Style::GridTrackSizingDirection gridAxisDirection) const |
| { |
| return !Style::GridPositionsResolver::resolveGridPositionsFromStyle(m_renderGrid, gridItem, gridAxisDirection).isIndefinite(); |
| } |
| |
| GridSpan GridMasonryLayout::gridAxisSpanFromArea(const GridArea& gridArea) const |
| { |
| return gridArea.span(gridAxisDirection()); |
| } |
| |
| GridArea GridMasonryLayout::masonryGridAreaFromGridAxisSpan(const GridSpan& gridAxisSpan) const |
| { |
| return m_masonryAxisDirection == Style::GridTrackSizingDirection::Rows |
| ? GridArea { m_masonryAxisSpan, gridAxisSpan } |
| : GridArea { gridAxisSpan, m_masonryAxisSpan }; |
| } |
| } // end namespace WebCore |