blob: 0de733d24e146d508a0cab9d493ded61e8365efd [file] [log] [blame]
/*
* Copyright (C) 2012 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 "RenderMultiColumnSet.h"
#include "FrameView.h"
#include "HitTestResult.h"
#include "PaintInfo.h"
#include "RenderBoxFragmentInfo.h"
#include "RenderLayer.h"
#include "RenderMultiColumnFlow.h"
#include "RenderMultiColumnSpannerPlaceholder.h"
#include "RenderView.h"
#include <wtf/IsoMallocInlines.h>
namespace WebCore {
WTF_MAKE_ISO_ALLOCATED_IMPL(RenderMultiColumnSet);
RenderMultiColumnSet::RenderMultiColumnSet(RenderFragmentedFlow& fragmentedFlow, RenderStyle&& style)
: RenderFragmentContainerSet(fragmentedFlow.document(), WTFMove(style), fragmentedFlow)
, m_maxColumnHeight(RenderFragmentedFlow::maxLogicalHeight())
, m_minSpaceShortage(RenderFragmentedFlow::maxLogicalHeight())
{
}
RenderMultiColumnSet* RenderMultiColumnSet::nextSiblingMultiColumnSet() const
{
for (RenderObject* sibling = nextSibling(); sibling; sibling = sibling->nextSibling()) {
if (is<RenderMultiColumnSet>(*sibling))
return downcast<RenderMultiColumnSet>(sibling);
}
return nullptr;
}
RenderMultiColumnSet* RenderMultiColumnSet::previousSiblingMultiColumnSet() const
{
for (RenderObject* sibling = previousSibling(); sibling; sibling = sibling->previousSibling()) {
if (is<RenderMultiColumnSet>(*sibling))
return downcast<RenderMultiColumnSet>(sibling);
}
return nullptr;
}
RenderObject* RenderMultiColumnSet::firstRendererInFragmentedFlow() const
{
if (RenderBox* sibling = RenderMultiColumnFlow::previousColumnSetOrSpannerSiblingOf(this)) {
// Adjacent sets should not occur. Currently we would have no way of figuring out what each
// of them contains then.
ASSERT(!sibling->isRenderMultiColumnSet());
if (RenderMultiColumnSpannerPlaceholder* placeholder = multiColumnFlow()->findColumnSpannerPlaceholder(sibling))
return placeholder->nextInPreOrderAfterChildren();
ASSERT_NOT_REACHED();
}
return fragmentedFlow()->firstChild();
}
RenderObject* RenderMultiColumnSet::lastRendererInFragmentedFlow() const
{
if (RenderBox* sibling = RenderMultiColumnFlow::nextColumnSetOrSpannerSiblingOf(this)) {
// Adjacent sets should not occur. Currently we would have no way of figuring out what each
// of them contains then.
ASSERT(!sibling->isRenderMultiColumnSet());
if (RenderMultiColumnSpannerPlaceholder* placeholder = multiColumnFlow()->findColumnSpannerPlaceholder(sibling))
return placeholder->previousInPreOrder();
ASSERT_NOT_REACHED();
}
return fragmentedFlow()->lastLeafChild();
}
static bool precedesRenderer(const RenderObject* renderer, const RenderObject* boundary)
{
for (; renderer; renderer = renderer->nextInPreOrder()) {
if (renderer == boundary)
return true;
}
return false;
}
bool RenderMultiColumnSet::containsRendererInFragmentedFlow(const RenderObject& renderer) const
{
if (!previousSiblingMultiColumnSet() && !nextSiblingMultiColumnSet()) {
// There is only one set. This is easy, then.
return renderer.isDescendantOf(m_fragmentedFlow);
}
RenderObject* firstRenderer = firstRendererInFragmentedFlow();
RenderObject* lastRenderer = lastRendererInFragmentedFlow();
ASSERT(firstRenderer);
ASSERT(lastRenderer);
// This is SLOW! But luckily very uncommon.
return precedesRenderer(firstRenderer, &renderer) && precedesRenderer(&renderer, lastRenderer);
}
void RenderMultiColumnSet::setLogicalTopInFragmentedFlow(LayoutUnit logicalTop)
{
LayoutRect rect = fragmentedFlowPortionRect();
if (isHorizontalWritingMode())
rect.setY(logicalTop);
else
rect.setX(logicalTop);
setFragmentedFlowPortionRect(rect);
}
void RenderMultiColumnSet::setLogicalBottomInFragmentedFlow(LayoutUnit logicalBottom)
{
LayoutRect rect = fragmentedFlowPortionRect();
if (isHorizontalWritingMode())
rect.shiftMaxYEdgeTo(logicalBottom);
else
rect.shiftMaxXEdgeTo(logicalBottom);
setFragmentedFlowPortionRect(rect);
}
LayoutUnit RenderMultiColumnSet::heightAdjustedForSetOffset(LayoutUnit height) const
{
RenderBlockFlow& multicolBlock = downcast<RenderBlockFlow>(*parent());
LayoutUnit contentLogicalTop = logicalTop() - multicolBlock.borderAndPaddingBefore();
height -= contentLogicalTop;
return std::max(height, 1_lu); // Let's avoid zero height, as that would probably cause an infinite amount of columns to be created.
}
LayoutUnit RenderMultiColumnSet::pageLogicalTopForOffset(LayoutUnit offset) const
{
unsigned columnIndex = columnIndexAtOffset(offset, AssumeNewColumns);
return logicalTopInFragmentedFlow() + columnIndex * computedColumnHeight();
}
void RenderMultiColumnSet::setAndConstrainColumnHeight(LayoutUnit newHeight)
{
m_computedColumnHeight = newHeight;
if (m_computedColumnHeight > m_maxColumnHeight)
m_computedColumnHeight = m_maxColumnHeight;
// FIXME: The available column height is not the same as the constrained height specified
// by the pagination API. The column set in this case is allowed to be bigger than the
// height of a single column. We cache available column height in order to use it
// in computeLogicalHeight later. This is pretty gross, and maybe there's a better way
// to formalize the idea of clamped column heights without having a view dependency
// here.
m_availableColumnHeight = m_computedColumnHeight;
if (multiColumnFlow() && !multiColumnFlow()->progressionIsInline() && parent()->isRenderView()) {
int pageLength = view().frameView().pagination().pageLength;
if (pageLength)
m_computedColumnHeight = pageLength;
}
m_columnHeightComputed = true;
// FIXME: the height may also be affected by the enclosing pagination context, if any.
}
unsigned RenderMultiColumnSet::findRunWithTallestColumns() const
{
unsigned indexWithLargestHeight = 0;
LayoutUnit largestHeight;
LayoutUnit previousOffset;
size_t runCount = m_contentRuns.size();
ASSERT(runCount);
for (size_t i = 0; i < runCount; i++) {
const ContentRun& run = m_contentRuns[i];
LayoutUnit height = run.columnLogicalHeight(previousOffset);
if (largestHeight < height) {
largestHeight = height;
indexWithLargestHeight = i;
}
previousOffset = run.breakOffset();
}
return indexWithLargestHeight;
}
void RenderMultiColumnSet::distributeImplicitBreaks()
{
#ifndef NDEBUG
// There should be no implicit breaks assumed at this point.
for (unsigned i = 0; i < forcedBreaksCount(); i++)
ASSERT(!m_contentRuns[i].assumedImplicitBreaks());
#endif // NDEBUG
// Insert a final content run to encompass all content. This will include overflow if this is
// the last set.
addForcedBreak(logicalBottomInFragmentedFlow());
unsigned breakCount = forcedBreaksCount();
// If there is room for more breaks (to reach the used value of column-count), imagine that we
// insert implicit breaks at suitable locations. At any given time, the content run with the
// currently tallest columns will get another implicit break "inserted", which will increase its
// column count by one and shrink its columns' height. Repeat until we have the desired total
// number of breaks. The largest column height among the runs will then be the initial column
// height for the balancer to use.
while (breakCount < m_computedColumnCount) {
unsigned index = findRunWithTallestColumns();
m_contentRuns[index].assumeAnotherImplicitBreak();
breakCount++;
}
}
LayoutUnit RenderMultiColumnSet::calculateBalancedHeight(bool initial) const
{
if (initial) {
// Start with the lowest imaginable column height.
unsigned index = findRunWithTallestColumns();
LayoutUnit startOffset = index > 0 ? m_contentRuns[index - 1].breakOffset() : logicalTopInFragmentedFlow();
return std::max<LayoutUnit>(m_contentRuns[index].columnLogicalHeight(startOffset), m_minimumColumnHeight);
}
LayoutUnit sizeContainmentShortage = std::max<LayoutUnit>(LayoutUnit(), m_spaceShortageForSizeContainment);
if (columnCount() <= computedColumnCount()) {
// With the current column height, the content fits without creating overflowing columns. We're done.
return m_computedColumnHeight + sizeContainmentShortage;
}
if (forcedBreaksCount() >= computedColumnCount()) {
// Too many forced breaks to allow any implicit breaks. Initial balancing should already
// have set a good height. There's nothing more we should do.
return m_computedColumnHeight + sizeContainmentShortage;
}
// If the initial guessed column height wasn't enough, stretch it now. Stretch by the lowest
// amount of space shortage found during layout.
ASSERT(m_minSpaceShortage > 0); // We should never _shrink_ the height!
// ASSERT(m_minSpaceShortage != RenderFragmentedFlow::maxLogicalHeight()); // If this happens, we probably have a bug.
if (m_minSpaceShortage == RenderFragmentedFlow::maxLogicalHeight())
return m_computedColumnHeight + sizeContainmentShortage; // So bail out rather than looping infinitely.
auto toAdd = std::max<LayoutUnit>(sizeContainmentShortage, m_minSpaceShortage);
return m_computedColumnHeight + toAdd;
}
void RenderMultiColumnSet::clearForcedBreaks()
{
m_contentRuns.clear();
}
void RenderMultiColumnSet::addForcedBreak(LayoutUnit offsetFromFirstPage)
{
if (!requiresBalancing())
return;
if (!m_contentRuns.isEmpty() && offsetFromFirstPage <= m_contentRuns.last().breakOffset())
return;
// Append another item as long as we haven't exceeded used column count. What ends up in the
// overflow area shouldn't affect column balancing.
if (m_contentRuns.size() < m_computedColumnCount)
m_contentRuns.append(ContentRun(offsetFromFirstPage));
}
bool RenderMultiColumnSet::recalculateColumnHeight(bool initial)
{
LayoutUnit oldColumnHeight = m_computedColumnHeight;
if (requiresBalancing()) {
if (initial)
distributeImplicitBreaks();
LayoutUnit newColumnHeight = calculateBalancedHeight(initial);
setAndConstrainColumnHeight(newColumnHeight);
// After having calculated an initial column height, the multicol container typically needs at
// least one more layout pass with a new column height, but if a height was specified, we only
// need to do this if we think that we need less space than specified. Conversely, if we
// determined that the columns need to be as tall as the specified height of the container, we
// have already laid it out correctly, and there's no need for another pass.
} else {
// The position of the column set may have changed, in which case height available for
// columns may have changed as well.
setAndConstrainColumnHeight(m_computedColumnHeight);
}
if (m_computedColumnHeight == oldColumnHeight)
return false; // No change. We're done.
m_minSpaceShortage = RenderFragmentedFlow::maxLogicalHeight();
return true; // Need another pass.
}
void RenderMultiColumnSet::recordSpaceShortage(LayoutUnit spaceShortage)
{
if (spaceShortage >= m_minSpaceShortage)
return;
// The space shortage is what we use as our stretch amount. We need a positive number here in
// order to get anywhere. Some lines actually have zero height. Ignore them.
if (spaceShortage > 0)
m_minSpaceShortage = spaceShortage;
}
void RenderMultiColumnSet::updateLogicalWidth()
{
setComputedColumnWidthAndCount(multiColumnFlow()->columnWidth(), multiColumnFlow()->columnCount()); // FIXME: This will eventually vary if we are contained inside fragments.
// FIXME: When we add fragments support, we'll start it off at the width of the multi-column
// block in that particular fragment.
setLogicalWidth(multiColumnBlockFlow()->contentLogicalWidth());
}
bool RenderMultiColumnSet::requiresBalancing() const
{
if (!multiColumnFlow()->progressionIsInline())
return false;
if (RenderBox* next = RenderMultiColumnFlow::nextColumnSetOrSpannerSiblingOf(this)) {
if (!next->isRenderMultiColumnSet() && !next->isLegend()) {
// If we're followed by a spanner, we need to balance.
ASSERT(multiColumnFlow()->findColumnSpannerPlaceholder(next));
return true;
}
}
RenderBlockFlow* container = multiColumnBlockFlow();
if (container->style().columnFill() == ColumnFill::Balance)
return true;
return !multiColumnFlow()->columnHeightAvailable();
}
void RenderMultiColumnSet::prepareForLayout(bool initial)
{
// Guess box logical top. This might eliminate the need for another layout pass.
if (RenderBox* previous = RenderMultiColumnFlow::previousColumnSetOrSpannerSiblingOf(this))
setLogicalTop(previous->logicalBottom() + previous->marginAfter());
else
setLogicalTop(multiColumnBlockFlow()->borderAndPaddingBefore());
if (initial)
m_maxColumnHeight = calculateMaxColumnHeight();
if (requiresBalancing()) {
if (initial) {
m_computedColumnHeight = 0;
m_availableColumnHeight = 0;
m_columnHeightComputed = false;
}
} else
setAndConstrainColumnHeight(heightAdjustedForSetOffset(multiColumnFlow()->columnHeightAvailable()));
// Set box width.
updateLogicalWidth();
// Any breaks will be re-inserted during layout, so get rid of what we already have.
clearForcedBreaks();
// Nuke previously stored minimum column height. Contents may have changed for all we know.
m_minimumColumnHeight = 0;
m_spaceShortageForSizeContainment = 0;
// Start with "infinite" flow thread portion height until height is known.
setLogicalBottomInFragmentedFlow(RenderFragmentedFlow::maxLogicalHeight());
setNeedsLayout(MarkOnlyThis);
}
void RenderMultiColumnSet::beginFlow(RenderBlock* container)
{
RenderMultiColumnFlow* fragmentedFlow = multiColumnFlow();
// At this point layout is exactly at the beginning of this set. Store block offset from flow
// thread start.
LayoutUnit logicalTopInFragmentedFlow = fragmentedFlow->offsetFromLogicalTopOfFirstFragment(container) + container->logicalHeight();
setLogicalTopInFragmentedFlow(logicalTopInFragmentedFlow);
}
void RenderMultiColumnSet::endFlow(RenderBlock* container, LayoutUnit bottomInContainer)
{
RenderMultiColumnFlow* fragmentedFlow = multiColumnFlow();
// At this point layout is exactly at the end of this set. Store block offset from flow thread
// start. Also note that a new column height may have affected the height used in the flow
// thread (because of struts), which may affect the number of columns. So we also need to update
// the flow thread portion height in order to be able to calculate actual column-count.
LayoutUnit logicalBottomInFragmentedFlow = fragmentedFlow->offsetFromLogicalTopOfFirstFragment(container) + bottomInContainer;
setLogicalBottomInFragmentedFlow(logicalBottomInFragmentedFlow);
container->setLogicalHeight(bottomInContainer);
}
void RenderMultiColumnSet::layout()
{
RenderBlockFlow::layout();
// At this point the logical top and bottom of the column set are known. Update maximum column
// height (multicol height may be constrained).
m_maxColumnHeight = calculateMaxColumnHeight();
if (!nextSiblingMultiColumnSet()) {
// This is the last set, i.e. the last fragment. Seize the opportunity to validate them.
multiColumnFlow()->validateFragments();
}
}
RenderBox::LogicalExtentComputedValues RenderMultiColumnSet::computeLogicalHeight(LayoutUnit, LayoutUnit logicalTop) const
{
return { m_availableColumnHeight, logicalTop, ComputedMarginValues() };
}
LayoutUnit RenderMultiColumnSet::calculateMaxColumnHeight() const
{
RenderBlockFlow* multicolBlock = multiColumnBlockFlow();
const RenderStyle& multicolStyle = multicolBlock->style();
LayoutUnit availableHeight = multiColumnFlow()->columnHeightAvailable();
LayoutUnit maxColumnHeight = availableHeight ? availableHeight : RenderFragmentedFlow::maxLogicalHeight();
if (!multicolStyle.logicalMaxHeight().isUndefined())
maxColumnHeight = std::min(maxColumnHeight, multicolBlock->computeContentLogicalHeight(MaxSize, multicolStyle.logicalMaxHeight(), std::nullopt).value_or(maxColumnHeight));
return heightAdjustedForSetOffset(maxColumnHeight);
}
LayoutUnit RenderMultiColumnSet::columnGap() const
{
// FIXME: Eventually we will cache the column gap when the widths of columns start varying, but for now we just
// go to the parent block to get the gap.
RenderBlockFlow& parentBlock = downcast<RenderBlockFlow>(*parent());
if (parentBlock.style().columnGap().isNormal())
return parentBlock.style().fontDescription().computedPixelSize(); // "1em" is recommended as the normal gap setting. Matches <p> margins.
return valueForLength(parentBlock.style().columnGap().length(), parentBlock.availableLogicalWidth());
}
unsigned RenderMultiColumnSet::columnCount() const
{
// We must always return a value of 1 or greater. Column count = 0 is a meaningless situation,
// and will confuse and cause problems in other parts of the code.
auto computedColumnHeight = this->computedColumnHeight();
if (computedColumnHeight <= 0)
return 1;
// Our portion rect determines our column count. We have as many columns as needed to fit all the content.
auto logicalHeightInColumns = fragmentedFlow()->isHorizontalWritingMode() ? fragmentedFlowPortionRect().height() : fragmentedFlowPortionRect().width();
if (logicalHeightInColumns <= 0)
return 1;
unsigned count = ceilf(static_cast<float>(logicalHeightInColumns) / computedColumnHeight);
ASSERT(count >= 1);
return count;
}
LayoutUnit RenderMultiColumnSet::columnLogicalLeft(unsigned index) const
{
LayoutUnit colLogicalWidth = computedColumnWidth();
LayoutUnit colLogicalLeft = borderAndPaddingLogicalLeft();
LayoutUnit colGap = columnGap();
bool progressionReversed = multiColumnFlow()->progressionIsReversed();
bool progressionInline = multiColumnFlow()->progressionIsInline();
if (progressionInline) {
if (style().isLeftToRightDirection() ^ progressionReversed)
colLogicalLeft += index * (colLogicalWidth + colGap);
else
colLogicalLeft += contentLogicalWidth() - colLogicalWidth - index * (colLogicalWidth + colGap);
}
return colLogicalLeft;
}
LayoutUnit RenderMultiColumnSet::columnLogicalTop(unsigned index) const
{
LayoutUnit colLogicalHeight = computedColumnHeight();
LayoutUnit colLogicalTop = borderAndPaddingBefore();
LayoutUnit colGap = columnGap();
bool progressionReversed = multiColumnFlow()->progressionIsReversed();
bool progressionInline = multiColumnFlow()->progressionIsInline();
if (!progressionInline) {
if (!progressionReversed)
colLogicalTop += index * (colLogicalHeight + colGap);
else
colLogicalTop += contentLogicalHeight() - colLogicalHeight - index * (colLogicalHeight + colGap);
}
return colLogicalTop;
}
LayoutRect RenderMultiColumnSet::columnRectAt(unsigned index) const
{
LayoutUnit colLogicalWidth = computedColumnWidth();
LayoutUnit colLogicalHeight = computedColumnHeight();
if (isHorizontalWritingMode())
return LayoutRect(columnLogicalLeft(index), columnLogicalTop(index), colLogicalWidth, colLogicalHeight);
return LayoutRect(columnLogicalTop(index), columnLogicalLeft(index), colLogicalHeight, colLogicalWidth);
}
unsigned RenderMultiColumnSet::columnIndexAtOffset(LayoutUnit offset, ColumnIndexCalculationMode mode) const
{
LayoutRect portionRect(fragmentedFlowPortionRect());
// Handle the offset being out of range.
LayoutUnit fragmentedFlowLogicalTop = isHorizontalWritingMode() ? portionRect.y() : portionRect.x();
if (offset < fragmentedFlowLogicalTop)
return 0;
// If we're laying out right now, we cannot constrain against some logical bottom, since it
// isn't known yet. Otherwise, just return the last column if we're past the logical bottom.
if (mode == ClampToExistingColumns) {
LayoutUnit fragmentedFlowLogicalBottom = isHorizontalWritingMode() ? portionRect.maxY() : portionRect.maxX();
if (offset >= fragmentedFlowLogicalBottom)
return columnCount() - 1;
}
// Sometimes computedColumnHeight() is 0 here: see https://bugs.webkit.org/show_bug.cgi?id=132884
if (!computedColumnHeight())
return 0;
// Just divide by the column height to determine the correct column.
return static_cast<float>(offset - fragmentedFlowLogicalTop) / computedColumnHeight();
}
std::pair<unsigned, unsigned> RenderMultiColumnSet::firstAndLastColumnsFromOffsets(LayoutUnit topOffset, LayoutUnit bottomOffset) const
{
auto portionRect = fragmentedFlowPortionRect();
// Handle the offset being out of range.
auto fragmentedFlowLogicalTop = isHorizontalWritingMode() ? portionRect.y() : portionRect.x();
auto fragmentedFlowLogicalBottom = isHorizontalWritingMode() ? portionRect.maxY() : portionRect.maxX();
auto computeColumnIndex = [&](LayoutUnit offset, bool isBottom) -> unsigned {
if (offset < fragmentedFlowLogicalTop)
return 0;
if (offset >= fragmentedFlowLogicalBottom)
return columnCount() - 1;
// Sometimes computedColumnHeight() is 0 here: see https://bugs.webkit.org/show_bug.cgi?id=132884
auto columnHeight = computedColumnHeight();
if (!columnHeight)
return 0;
auto columnIndex = static_cast<float>(offset - fragmentedFlowLogicalTop) / columnHeight;
if (isBottom && WTF::isIntegral(columnIndex) && bottomOffset > topOffset && columnIndex > 0)
columnIndex -= 1;
return static_cast<unsigned>(columnIndex);
};
return { computeColumnIndex(topOffset, false), computeColumnIndex(bottomOffset, true) };
}
LayoutRect RenderMultiColumnSet::fragmentedFlowPortionRectAt(unsigned index) const
{
LayoutRect portionRect = fragmentedFlowPortionRect();
if (isHorizontalWritingMode())
portionRect = LayoutRect(portionRect.x(), portionRect.y() + index * computedColumnHeight(), portionRect.width(), computedColumnHeight());
else
portionRect = LayoutRect(portionRect.x() + index * computedColumnHeight(), portionRect.y(), computedColumnHeight(), portionRect.height());
return portionRect;
}
LayoutRect RenderMultiColumnSet::fragmentedFlowPortionOverflowRect(const LayoutRect& portionRect, unsigned index, unsigned colCount, LayoutUnit colGap)
{
// This function determines the portion of the flow thread that paints for the column. Along the inline axis, columns are
// unclipped at outside edges (i.e., the first and last column in the set), and they clip to half the column
// gap along interior edges.
//
// In the block direction, we will not clip overflow out of the top of the first column, or out of the bottom of
// the last column. This applies only to the true first column and last column across all column sets.
//
// FIXME: Eventually we will know overflow on a per-column basis, but we can't do this until we have a painting
// mode that understands not to paint contents from a previous column in the overflow area of a following column.
// This problem applies to fragments and pages as well and is not unique to columns.
bool progressionReversed = multiColumnFlow()->progressionIsReversed();
bool isFirstColumn = !index;
bool isLastColumn = index == colCount - 1;
bool isLeftmostColumn = style().isLeftToRightDirection() ^ progressionReversed ? isFirstColumn : isLastColumn;
bool isRightmostColumn = style().isLeftToRightDirection() ^ progressionReversed ? isLastColumn : isFirstColumn;
// Calculate the overflow rectangle, based on the flow thread's, clipped at column logical
// top/bottom unless it's the first/last column.
LayoutRect overflowRect = overflowRectForFragmentedFlowPortion(portionRect, isFirstColumn && isFirstFragment(), isLastColumn && isLastFragment(), VisualOverflow);
// For RenderViews only (i.e., iBooks), avoid overflowing into neighboring columns, by clipping in the middle of adjacent column gaps. Also make sure that we avoid rounding errors.
if (&view() == parent()) {
if (isHorizontalWritingMode()) {
if (!isLeftmostColumn)
overflowRect.shiftXEdgeTo(portionRect.x() - colGap / 2);
if (!isRightmostColumn)
overflowRect.shiftMaxXEdgeTo(portionRect.maxX() + colGap - colGap / 2);
} else {
if (!isLeftmostColumn)
overflowRect.shiftYEdgeTo(portionRect.y() - colGap / 2);
if (!isRightmostColumn)
overflowRect.shiftMaxYEdgeTo(portionRect.maxY() + colGap - colGap / 2);
}
}
return overflowRect;
}
void RenderMultiColumnSet::paintColumnRules(PaintInfo& paintInfo, const LayoutPoint& paintOffset)
{
if (paintInfo.context().paintingDisabled())
return;
RenderMultiColumnFlow* fragmentedFlow = multiColumnFlow();
const RenderStyle& blockStyle = parent()->style();
const Color& ruleColor = blockStyle.visitedDependentColorWithColorFilter(CSSPropertyColumnRuleColor);
bool ruleTransparent = blockStyle.columnRuleIsTransparent();
BorderStyle ruleStyle = collapsedBorderStyle(blockStyle.columnRuleStyle());
LayoutUnit ruleThickness { blockStyle.columnRuleWidth() };
LayoutUnit colGap = columnGap();
bool renderRule = ruleStyle > BorderStyle::Hidden && !ruleTransparent;
if (!renderRule)
return;
unsigned colCount = columnCount();
if (colCount <= 1)
return;
bool antialias = shouldAntialiasLines(paintInfo.context());
if (fragmentedFlow->progressionIsInline()) {
bool leftToRight = style().isLeftToRightDirection() ^ fragmentedFlow->progressionIsReversed();
LayoutUnit currLogicalLeftOffset = leftToRight ? 0_lu : contentLogicalWidth();
LayoutUnit ruleAdd = logicalLeftOffsetForContent();
LayoutUnit ruleLogicalLeft = leftToRight ? 0_lu : contentLogicalWidth();
LayoutUnit inlineDirectionSize = computedColumnWidth();
BoxSide boxSide = isHorizontalWritingMode()
? leftToRight ? BoxSide::Left : BoxSide::Right
: leftToRight ? BoxSide::Top : BoxSide::Bottom;
for (unsigned i = 0; i < colCount; i++) {
// Move to the next position.
if (leftToRight) {
ruleLogicalLeft += inlineDirectionSize + colGap / 2;
currLogicalLeftOffset += inlineDirectionSize + colGap;
} else {
ruleLogicalLeft -= (inlineDirectionSize + colGap / 2);
currLogicalLeftOffset -= (inlineDirectionSize + colGap);
}
// Now paint the column rule.
if (i < colCount - 1) {
LayoutUnit ruleLeft = isHorizontalWritingMode() ? paintOffset.x() + ruleLogicalLeft - ruleThickness / 2 + ruleAdd : paintOffset.x() + borderLeft() + paddingLeft();
LayoutUnit ruleRight = isHorizontalWritingMode() ? ruleLeft + ruleThickness : ruleLeft + contentWidth();
LayoutUnit ruleTop = isHorizontalWritingMode() ? paintOffset.y() + borderTop() + paddingTop() : paintOffset.y() + ruleLogicalLeft - ruleThickness / 2 + ruleAdd;
LayoutUnit ruleBottom = isHorizontalWritingMode() ? ruleTop + contentHeight() : ruleTop + ruleThickness;
IntRect pixelSnappedRuleRect = snappedIntRect(ruleLeft, ruleTop, ruleRight - ruleLeft, ruleBottom - ruleTop);
drawLineForBoxSide(paintInfo.context(), pixelSnappedRuleRect, boxSide, ruleColor, ruleStyle, 0, 0, antialias);
}
ruleLogicalLeft = currLogicalLeftOffset;
}
} else {
bool topToBottom = !style().isFlippedBlocksWritingMode() ^ fragmentedFlow->progressionIsReversed();
LayoutUnit ruleLeft = isHorizontalWritingMode() ? 0_lu : colGap / 2 - colGap - ruleThickness / 2;
LayoutUnit ruleWidth = isHorizontalWritingMode() ? contentWidth() : ruleThickness;
LayoutUnit ruleTop = isHorizontalWritingMode() ? colGap / 2 - colGap - ruleThickness / 2 : 0_lu;
LayoutUnit ruleHeight = isHorizontalWritingMode() ? ruleThickness : contentHeight();
LayoutRect ruleRect(ruleLeft, ruleTop, ruleWidth, ruleHeight);
if (!topToBottom) {
if (isHorizontalWritingMode())
ruleRect.setY(height() - ruleRect.maxY());
else
ruleRect.setX(width() - ruleRect.maxX());
}
ruleRect.moveBy(paintOffset);
BoxSide boxSide = isHorizontalWritingMode() ? topToBottom ? BoxSide::Top : BoxSide::Bottom : topToBottom ? BoxSide::Left : BoxSide::Right;
LayoutSize step(0_lu, topToBottom ? computedColumnHeight() + colGap : -(computedColumnHeight() + colGap));
if (!isHorizontalWritingMode())
step = step.transposedSize();
for (unsigned i = 1; i < colCount; i++) {
ruleRect.move(step);
IntRect pixelSnappedRuleRect = snappedIntRect(ruleRect);
drawLineForBoxSide(paintInfo.context(), pixelSnappedRuleRect, boxSide, ruleColor, ruleStyle, 0, 0, antialias);
}
}
}
void RenderMultiColumnSet::repaintFragmentedFlowContent(const LayoutRect& repaintRect)
{
// Figure out the start and end columns and only check within that range so that we don't walk the
// entire column set. Put the repaint rect into flow thread coordinates by flipping it first.
LayoutRect fragmentedFlowRepaintRect(repaintRect);
fragmentedFlow()->flipForWritingMode(fragmentedFlowRepaintRect);
// Now we can compare this rect with the flow thread portions owned by each column. First let's
// just see if the repaint rect intersects our flow thread portion at all.
LayoutRect clippedRect(fragmentedFlowRepaintRect);
clippedRect.intersect(RenderFragmentContainer::fragmentedFlowPortionOverflowRect());
if (clippedRect.isEmpty())
return;
// Now we know we intersect at least one column. Let's figure out the logical top and logical
// bottom of the area we're repainting.
LayoutUnit repaintLogicalTop = isHorizontalWritingMode() ? fragmentedFlowRepaintRect.y() : fragmentedFlowRepaintRect.x();
LayoutUnit repaintLogicalBottom = (isHorizontalWritingMode() ? fragmentedFlowRepaintRect.maxY() : fragmentedFlowRepaintRect.maxX()) - 1;
// FIXME: this should use firstAndLastColumnsFromOffsets.
unsigned startColumn = columnIndexAtOffset(repaintLogicalTop);
unsigned endColumn = columnIndexAtOffset(repaintLogicalBottom);
LayoutUnit colGap = columnGap();
unsigned colCount = columnCount();
for (unsigned i = startColumn; i <= endColumn; i++) {
LayoutRect colRect = columnRectAt(i);
// Get the portion of the flow thread that corresponds to this column.
LayoutRect fragmentedFlowPortion = fragmentedFlowPortionRectAt(i);
// Now get the overflow rect that corresponds to the column.
LayoutRect fragmentedFlowOverflowPortion = fragmentedFlowPortionOverflowRect(fragmentedFlowPortion, i, colCount, colGap);
// Do a repaint for this specific column.
flipForWritingMode(colRect);
repaintFragmentedFlowContentRectangle(repaintRect, fragmentedFlowPortion, colRect.location(), &fragmentedFlowOverflowPortion);
}
}
Vector<LayoutRect> RenderMultiColumnSet::fragmentRectsForFlowContentRect(const LayoutRect& rect)
{
auto fragmentedFlowRect = rect;
fragmentedFlow()->flipForWritingMode(fragmentedFlowRect);
auto logicalTop = isHorizontalWritingMode() ? fragmentedFlowRect.y() : fragmentedFlowRect.x();
auto logicalBottom = isHorizontalWritingMode() ? fragmentedFlowRect.maxY() : fragmentedFlowRect.maxX();
auto startAndEndColumns = firstAndLastColumnsFromOffsets(logicalTop, logicalBottom);
Vector<LayoutRect> perColumnRects;
LayoutUnit colGap = columnGap();
unsigned colCount = columnCount();
for (unsigned i = startAndEndColumns.first; i <= startAndEndColumns.second; i++) {
auto colRect = columnRectAt(i);
flipForWritingMode(colRect);
auto fragmentedFlowPortion = fragmentedFlowPortionRectAt(i);
auto fragmentedFlowOverflowPortion = fragmentedFlowPortionOverflowRect(fragmentedFlowPortion, i, colCount, colGap);
auto rectInColumn = fragmentedFlowContentRectangle(rect, fragmentedFlowPortion, colRect.location(), &fragmentedFlowOverflowPortion);
flipForWritingMode(rectInColumn);
perColumnRects.append(rectInColumn);
}
return perColumnRects;
}
LayoutUnit RenderMultiColumnSet::initialBlockOffsetForPainting() const
{
bool progressionReversed = multiColumnFlow()->progressionIsReversed();
bool progressionIsInline = multiColumnFlow()->progressionIsInline();
LayoutUnit result;
if (!progressionIsInline && progressionReversed) {
LayoutRect colRect = columnRectAt(0);
result = isHorizontalWritingMode() ? colRect.y() : colRect.x();
}
return result;
}
void RenderMultiColumnSet::collectLayerFragments(LayerFragments& fragments, const LayoutRect& layerBoundingBox, const LayoutRect& dirtyRect)
{
// Let's start by introducing the different coordinate systems involved here. They are different
// in how they deal with writing modes and columns. RenderLayer rectangles tend to be more
// physical than the rectangles used in RenderObject & co.
//
// The two rectangles passed to this method are physical, except that we pretend that there's
// only one long column (that's the flow thread). They are relative to the top left corner of
// the flow thread. All rectangles being compared to the dirty rect also need to be in this
// coordinate system.
//
// Then there's the output from this method - the stuff we put into the list of fragments. The
// translationOffset point is the actual physical translation required to get from a location in
// the flow thread to a location in some column. The paginationClip rectangle is in the same
// coordinate system as the two rectangles passed to this method (i.e. physical, in flow thread
// coordinates, pretending that there's only one long column).
//
// All other rectangles in this method are slightly less physical, when it comes to how they are
// used with different writing modes, but they aren't really logical either. They are just like
// RenderBox::frameRect(). More precisely, the sizes are physical, and the inline direction
// coordinate is too, but the block direction coordinate is always "logical top". These
// rectangles also pretend that there's only one long column, i.e. they are for the flow thread.
//
// To sum up: input and output from this method are "physical" RenderLayer-style rectangles and
// points, while inside this method we mostly use the RenderObject-style rectangles (with the
// block direction coordinate always being logical top).
// Put the layer bounds into flow thread-local coordinates by flipping it first. Since we're in
// a renderer, most rectangles are represented this way.
LayoutRect layerBoundsInFragmentedFlow(layerBoundingBox);
fragmentedFlow()->flipForWritingMode(layerBoundsInFragmentedFlow);
// Now we can compare with the flow thread portions owned by each column. First let's
// see if the rect intersects our flow thread portion at all.
LayoutRect clippedRect(layerBoundsInFragmentedFlow);
clippedRect.intersect(RenderFragmentContainer::fragmentedFlowPortionOverflowRect());
if (clippedRect.isEmpty())
return;
// Now we know we intersect at least one column. Let's figure out the logical top and logical
// bottom of the area we're checking.
LayoutUnit layerLogicalTop = isHorizontalWritingMode() ? layerBoundsInFragmentedFlow.y() : layerBoundsInFragmentedFlow.x();
LayoutUnit layerLogicalBottom = (isHorizontalWritingMode() ? layerBoundsInFragmentedFlow.maxY() : layerBoundsInFragmentedFlow.maxX()) - 1;
// Figure out the start and end columns and only check within that range so that we don't walk the
// entire column set.
// FIXME: this should use firstAndLastColumnsFromOffsets.
unsigned startColumn = columnIndexAtOffset(layerLogicalTop);
unsigned endColumn = columnIndexAtOffset(layerLogicalBottom);
LayoutUnit colLogicalWidth = computedColumnWidth();
LayoutUnit colGap = columnGap();
unsigned colCount = columnCount();
bool progressionReversed = multiColumnFlow()->progressionIsReversed();
bool progressionIsInline = multiColumnFlow()->progressionIsInline();
LayoutUnit initialBlockOffset = initialBlockOffsetForPainting();
for (unsigned i = startColumn; i <= endColumn; i++) {
// Get the portion of the flow thread that corresponds to this column.
LayoutRect fragmentedFlowPortion = fragmentedFlowPortionRectAt(i);
// Now get the overflow rect that corresponds to the column.
LayoutRect fragmentedFlowOverflowPortion = fragmentedFlowPortionOverflowRect(fragmentedFlowPortion, i, colCount, colGap);
// In order to create a fragment we must intersect the portion painted by this column.
LayoutRect clippedRect(layerBoundsInFragmentedFlow);
clippedRect.intersect(fragmentedFlowOverflowPortion);
if (clippedRect.isEmpty())
continue;
// We also need to intersect the dirty rect. We have to apply a translation and shift based off
// our column index.
LayoutSize translationOffset;
LayoutUnit inlineOffset = progressionIsInline ? i * (colLogicalWidth + colGap) : 0_lu;
bool leftToRight = style().isLeftToRightDirection() ^ progressionReversed;
if (!leftToRight) {
inlineOffset = -inlineOffset;
if (progressionReversed)
inlineOffset += contentLogicalWidth() - colLogicalWidth;
}
translationOffset.setWidth(inlineOffset);
LayoutUnit blockOffset = initialBlockOffset + logicalTop() - fragmentedFlow()->logicalTop() + (isHorizontalWritingMode() ? -fragmentedFlowPortion.y() : -fragmentedFlowPortion.x());
if (!progressionIsInline) {
if (!progressionReversed)
blockOffset = i * colGap;
else
blockOffset -= i * (computedColumnHeight() + colGap);
}
if (isFlippedWritingMode(style().writingMode()))
blockOffset = -blockOffset;
translationOffset.setHeight(blockOffset);
if (!isHorizontalWritingMode())
translationOffset = translationOffset.transposedSize();
// Shift the dirty rect to be in flow thread coordinates with this translation applied.
LayoutRect translatedDirtyRect(dirtyRect);
translatedDirtyRect.move(-translationOffset);
// See if we intersect the dirty rect.
clippedRect = layerBoundingBox;
clippedRect.intersect(translatedDirtyRect);
if (clippedRect.isEmpty())
continue;
// Something does need to paint in this column. Make a fragment now and supply the physical translation
// offset and the clip rect for the column with that offset applied.
LayerFragment fragment;
fragment.paginationOffset = translationOffset;
LayoutRect flippedFragmentedFlowOverflowPortion(fragmentedFlowOverflowPortion);
// Flip it into more a physical (RenderLayer-style) rectangle.
fragmentedFlow()->flipForWritingMode(flippedFragmentedFlowOverflowPortion);
fragment.paginationClip = flippedFragmentedFlowOverflowPortion;
fragments.append(fragment);
}
}
LayoutPoint RenderMultiColumnSet::columnTranslationForOffset(const LayoutUnit& offset) const
{
unsigned startColumn = columnIndexAtOffset(offset);
LayoutUnit colGap = columnGap();
LayoutRect fragmentedFlowPortion = fragmentedFlowPortionRectAt(startColumn);
LayoutPoint translationOffset;
bool progressionReversed = multiColumnFlow()->progressionIsReversed();
bool progressionIsInline = multiColumnFlow()->progressionIsInline();
LayoutUnit initialBlockOffset = initialBlockOffsetForPainting();
translationOffset.setX(columnLogicalLeft(startColumn));
LayoutUnit blockOffset = initialBlockOffset - (isHorizontalWritingMode() ? fragmentedFlowPortion.y() : fragmentedFlowPortion.x());
if (!progressionIsInline) {
if (!progressionReversed)
blockOffset = startColumn * colGap;
else
blockOffset -= startColumn * (computedColumnHeight() + colGap);
}
if (isFlippedWritingMode(style().writingMode()))
blockOffset = -blockOffset;
translationOffset.setY(blockOffset);
if (!isHorizontalWritingMode())
translationOffset = translationOffset.transposedPoint();
return translationOffset;
}
void RenderMultiColumnSet::adjustFragmentBoundsFromFragmentedFlowPortionRect(LayoutRect&) const
{
// This only fires for named flow thread compositing code, so let's make sure to ASSERT if this ever gets invoked.
ASSERT_NOT_REACHED();
}
void RenderMultiColumnSet::addOverflowFromChildren()
{
// FIXME: Need to do much better here.
unsigned colCount = columnCount();
if (!colCount)
return;
LayoutRect lastRect = columnRectAt(colCount - 1);
addLayoutOverflow(lastRect);
if (!hasNonVisibleOverflow())
addVisualOverflow(lastRect);
}
VisiblePosition RenderMultiColumnSet::positionForPoint(const LayoutPoint& logicalPoint, const RenderFragmentContainer*)
{
return multiColumnFlow()->positionForPoint(translateFragmentPointToFragmentedFlow(logicalPoint, ClampHitTestTranslationToColumns), this);
}
LayoutPoint RenderMultiColumnSet::translateFragmentPointToFragmentedFlow(const LayoutPoint & logicalPoint, ColumnHitTestTranslationMode clampMode) const
{
// Determine which columns we intersect.
LayoutUnit colGap = columnGap();
LayoutUnit halfColGap = colGap / 2;
bool progressionIsInline = multiColumnFlow()->progressionIsInline();
LayoutPoint point = logicalPoint;
for (unsigned i = 0; i < columnCount(); i++) {
// Add in half the column gap to the left and right of the rect.
LayoutRect colRect = columnRectAt(i);
if (isHorizontalWritingMode() == progressionIsInline) {
LayoutRect gapAndColumnRect(colRect.x() - halfColGap, colRect.y(), colRect.width() + colGap, colRect.height());
if (point.x() >= gapAndColumnRect.x() && point.x() < gapAndColumnRect.maxX()) {
if (clampMode == ClampHitTestTranslationToColumns) {
if (progressionIsInline) {
// FIXME: The clamping that follows is not completely right for right-to-left
// content.
// Clamp everything above the column to its top left.
if (point.y() < gapAndColumnRect.y())
point = gapAndColumnRect.location();
// Clamp everything below the column to the next column's top left. If there is
// no next column, this still maps to just after this column.
else if (point.y() >= gapAndColumnRect.maxY()) {
point = gapAndColumnRect.location();
point.move(0_lu, gapAndColumnRect.height());
}
} else {
if (point.x() < colRect.x())
point.setX(colRect.x());
else if (point.x() >= colRect.maxX())
point.setX(colRect.maxX() - 1);
}
}
LayoutSize offsetInColumn = point - colRect.location();
LayoutRect fragmentedFlowPortion = fragmentedFlowPortionRectAt(i);
return fragmentedFlowPortion.location() + offsetInColumn;
}
} else {
LayoutRect gapAndColumnRect(colRect.x(), colRect.y() - halfColGap, colRect.width(), colRect.height() + colGap);
if (point.y() >= gapAndColumnRect.y() && point.y() < gapAndColumnRect.maxY()) {
if (clampMode == ClampHitTestTranslationToColumns) {
if (progressionIsInline) {
// FIXME: The clamping that follows is not completely right for right-to-left
// content.
// Clamp everything above the column to its top left.
if (point.x() < gapAndColumnRect.x())
point = gapAndColumnRect.location();
// Clamp everything below the column to the next column's top left. If there is
// no next column, this still maps to just after this column.
else if (point.x() >= gapAndColumnRect.maxX()) {
point = gapAndColumnRect.location();
point.move(gapAndColumnRect.width(), 0_lu);
}
} else {
if (point.y() < colRect.y())
point.setY(colRect.y());
else if (point.y() >= colRect.maxY())
point.setY(colRect.maxY() - 1);
}
}
LayoutSize offsetInColumn = point - colRect.location();
LayoutRect fragmentedFlowPortion = fragmentedFlowPortionRectAt(i);
return fragmentedFlowPortion.location() + offsetInColumn;
}
}
}
return logicalPoint;
}
Node* RenderMultiColumnSet::nodeForHitTest() const
{
return document().documentElement();
}
void RenderMultiColumnSet::updateHitTestResult(HitTestResult& result, const LayoutPoint& point)
{
if (result.innerNode() || !parent()->isRenderView())
return;
// Note this does not work with column spans, but once we implement RenderPageSet, we can move this code
// over there instead (and spans of course won't be allowed on pages).
if (auto* node = nodeForHitTest()) {
result.setInnerNode(node);
if (!result.innerNonSharedNode())
result.setInnerNonSharedNode(node);
LayoutPoint adjustedPoint = translateFragmentPointToFragmentedFlow(point);
view().offsetForContents(adjustedPoint);
result.setLocalPoint(adjustedPoint);
}
}
ASCIILiteral RenderMultiColumnSet::renderName() const
{
return "RenderMultiColumnSet"_s;
}
}