 Chromium Code Reviews
 Chromium Code Reviews Issue 1308273010:
  Adapt and reland old position sticky implementation  (Closed) 
  Base URL: https://chromium.googlesource.com/chromium/blink.git@master
    
  
    Issue 1308273010:
  Adapt and reland old position sticky implementation  (Closed) 
  Base URL: https://chromium.googlesource.com/chromium/blink.git@master| Index: third_party/WebKit/Source/core/layout/LayoutBoxModelObject.cpp | 
| diff --git a/third_party/WebKit/Source/core/layout/LayoutBoxModelObject.cpp b/third_party/WebKit/Source/core/layout/LayoutBoxModelObject.cpp | 
| index b9ca8b1f631afbef88431a5d1bf0b4e7d39a9e96..59b65b06a3eb2a96ffabc44e2ed7270c9191b8e0 100644 | 
| --- a/third_party/WebKit/Source/core/layout/LayoutBoxModelObject.cpp | 
| +++ b/third_party/WebKit/Source/core/layout/LayoutBoxModelObject.cpp | 
| @@ -38,7 +38,8 @@ | 
| #include "core/layout/LayoutView.h" | 
| #include "core/layout/compositing/CompositedLayerMapping.h" | 
| #include "core/layout/compositing/PaintLayerCompositor.h" | 
| -#include "core/page/scrolling/ScrollingConstraints.h" | 
| +#include "core/page/scrolling/StickyPositionViewportConstraints.h" | 
| +#include "core/page/scrolling/ViewportConstraints.h" | 
| #include "core/paint/PaintLayer.h" | 
| #include "core/style/BorderEdge.h" | 
| #include "core/style/ShadowList.h" | 
| @@ -177,6 +178,31 @@ void LayoutBoxModelObject::styleWillChange(StyleDifference diff, const ComputedS | 
| LayoutObject::styleWillChange(diff, newStyle); | 
| } | 
| +static inline LayoutBox* findScrollAncestor(const LayoutObject* startObject) | 
| +{ | 
| + // If compositing inputs should be fresh, we can get the scroll parent from | 
| + // the PaintLayer. | 
| + PaintLayer* layer = startObject->enclosingLayer(); | 
| + if (layer && !layer->needsCompositingInputsUpdate()) { | 
| 
chrishtr
2016/01/30 01:49:02
The computations for position:sticky should be don
 
flackr
2016/02/03 22:48:38
Currently this is also called during styleChanged
 | 
| + const PaintLayer* scrollAncestor = layer->ancestorScrollingLayer(); | 
| + if (!scrollAncestor) | 
| + return nullptr; | 
| + return toLayoutBox(scrollAncestor->layoutObject()); | 
| + } | 
| + | 
| + LayoutBox* curBox = startObject->containingBlock(); | 
| + // Scrolling propagates along the containing block chain. | 
| + // TODO(flackr): We could add scroll ancestor as an input to layout so that | 
| 
chrishtr
2016/01/30 01:49:03
PaintLayer's scrollParent ancestor-dependent input
 | 
| + // we don't have to walk up to find the scroll ancestor. | 
| + while (curBox && !curBox->isLayoutView()) { | 
| + if (curBox->hasOverflowClip()) | 
| + return curBox; | 
| + curBox = curBox->containingBlock(); | 
| + } | 
| + | 
| + return nullptr; | 
| +} | 
| + | 
| void LayoutBoxModelObject::styleDidChange(StyleDifference diff, const ComputedStyle* oldStyle) | 
| { | 
| bool hadTransform = hasTransformRelatedProperty(); | 
| @@ -267,14 +293,32 @@ void LayoutBoxModelObject::styleDidChange(StyleDifference diff, const ComputedSt | 
| } | 
| if (FrameView *frameView = view()->frameView()) { | 
| - bool newStyleIsViewportConstained = style()->hasViewportConstrainedPosition(); | 
| - bool oldStyleIsViewportConstrained = oldStyle && oldStyle->hasViewportConstrainedPosition(); | 
| + bool newStyleIsViewportConstained = style()->position() == FixedPosition; | 
| + bool oldStyleIsViewportConstrained = oldStyle && oldStyle->position() == FixedPosition; | 
| + bool newStyleIsSticky = style()->position() == StickyPosition; | 
| + bool oldStyleIsSticky = oldStyle && oldStyle->position() == StickyPosition; | 
| + | 
| + // Sticky positioned elements are only viewport constrained if they have no ancestor scroller. | 
| + if (newStyleIsSticky || oldStyleIsSticky) { | 
| + if (!findScrollAncestor(this)) { | 
| + newStyleIsViewportConstained |= newStyleIsSticky; | 
| + oldStyleIsViewportConstrained |= oldStyleIsSticky; | 
| + } | 
| + } | 
| + | 
| if (newStyleIsViewportConstained != oldStyleIsViewportConstrained) { | 
| if (newStyleIsViewportConstained && layer()) | 
| frameView->addViewportConstrainedObject(this); | 
| else | 
| frameView->removeViewportConstrainedObject(this); | 
| } | 
| + | 
| + if (newStyleIsSticky != oldStyleIsSticky) { | 
| + if (newStyleIsSticky) | 
| + frameView->addStickyPositionObject(); | 
| + else | 
| + frameView->removeStickyPositionObject(); | 
| + } | 
| } | 
| } | 
| @@ -596,6 +640,126 @@ LayoutSize LayoutBoxModelObject::relativePositionOffset() const | 
| return offset; | 
| } | 
| +void LayoutBoxModelObject::computeStickyPositionConstraints(StickyPositionViewportConstraints& constraints, const FloatRect& constrainingRect) const | 
| +{ | 
| + FloatSize skippedContainersOffset; | 
| + LayoutBlock* containingBlock = this->containingBlock(); | 
| + // Skip anonymous containing blocks. | 
| + while (containingBlock->isAnonymous()) { | 
| 
chrishtr
2016/01/30 01:49:02
Why exactly? Is there a test for this?
 
flackr
2016/02/03 22:48:38
I was matching firefox and safari on http://flackr
 
chrishtr
2016/02/23 16:31:51
And file a spec bug?
 
flackr
2016/03/07 19:07:33
Done: bug is here https://www.w3.org/Bugs/Public/s
 | 
| + skippedContainersOffset += toFloatSize(FloatPoint(containingBlock->frameRect().location())); | 
| + containingBlock = containingBlock->containingBlock(); | 
| + } | 
| + LayoutBox* scrollAncestor = findScrollAncestor(this); | 
| + | 
| + LayoutRect containerContentRect = containingBlock->contentBoxRect(); | 
| + LayoutUnit maxWidth = containingBlock->availableLogicalWidth(); | 
| + | 
| + // Sticky positioned element ignore any override logical width on the containing block (as they don't call | 
| + // containingBlockLogicalWidthForContent). It's unclear whether this is totally fine. | 
| + // Compute the container-relative area within which the sticky element is allowed to move. | 
| + containerContentRect.contractEdges( | 
| + minimumValueForLength(style()->marginTop(), maxWidth), | 
| + minimumValueForLength(style()->marginRight(), maxWidth), | 
| + minimumValueForLength(style()->marginBottom(), maxWidth), | 
| + minimumValueForLength(style()->marginLeft(), maxWidth)); | 
| + | 
| + // Map to the scroll ancestor. | 
| + constraints.setAbsoluteContainingBlockRect(containingBlock->localToAncestorQuad(FloatRect(containerContentRect), scrollAncestor).boundingBox()); | 
| + | 
| + FloatRect stickyBoxRect = isLayoutInline() | 
| + ? FloatRect(toLayoutInline(this)->linesBoundingBox()) | 
| + : FloatRect(toLayoutBox(this)->frameRect()); | 
| + FloatRect flippedStickyBoxRect = stickyBoxRect; | 
| + containingBlock->flipForWritingMode(flippedStickyBoxRect); | 
| + FloatPoint stickyLocation = flippedStickyBoxRect.location() + skippedContainersOffset; | 
| + | 
| + // FIXME: sucks to call localToAbsolute again, but we can't just offset from the previously computed rect if there are transforms. | 
| 
chrishtr
2016/01/30 01:49:02
s/localToAbsolute/localToAncestorQuad
 
flackr
2016/02/22 22:55:23
Done.
 | 
| + // Map to the view to avoid including page scale factor. | 
| + FloatRect absContainerFrame = containingBlock->localToAncestorQuad(FloatRect(FloatPoint(), FloatSize(containingBlock->size())), scrollAncestor).boundingBox(); | 
| 
chrishtr
2016/01/30 01:49:02
s/absContainerFrame/stickyBoxRect/
 
flackr
2016/02/22 22:55:23
This is the container's frame rect which we add to
 | 
| + | 
| + // If the containing block is our scroll ancestor, its location will not include the scroll offset which we need to include as | 
| + // part of the sticky box rect so we include it here. | 
| + if (containingBlock->hasOverflowClip()) { | 
| + FloatSize scrollOffset(toFloatSize(containingBlock->layer()->scrollableArea()->adjustedScrollOffset())); | 
| + stickyLocation -= scrollOffset; | 
| + } | 
| + | 
| + // We can't call localToAbsolute on |this| because that will recur. FIXME: For now, assume that |this| is not transformed. | 
| 
chrishtr
2016/01/30 01:49:02
What is this comment about again? Why a need to ma
 
flackr
2016/02/22 22:55:23
This actually seems unnecessary. If there is a tra
 | 
| + constraints.setAbsoluteStickyBoxRect(FloatRect(absContainerFrame.location() + toFloatSize(stickyLocation), flippedStickyBoxRect.size())); | 
| + | 
| + LayoutUnit horizontalOffsets = minimumValueForLength(style()->right(), constrainingRect.width()) + | 
| + minimumValueForLength(style()->left(), constrainingRect.width()); | 
| + bool skipRight = false; | 
| + bool skipLeft = false; | 
| + if (!style()->left().isAuto() && !style()->right().isAuto()) { | 
| 
chrishtr
2016/01/30 01:49:02
Document this code and what it's for.
 
flackr
2016/02/22 22:55:23
Done.
 | 
| + if (horizontalOffsets > containerContentRect.width() | 
| + || horizontalOffsets + containerContentRect.width() > constrainingRect.width()) { | 
| + skipRight = style()->isLeftToRightDirection(); | 
| + skipLeft = !skipRight; | 
| + } | 
| + } | 
| + | 
| + if (!style()->left().isAuto() && !skipLeft) { | 
| + constraints.setLeftOffset(minimumValueForLength(style()->left(), constrainingRect.width())); | 
| + constraints.addAnchorEdge(ViewportConstraints::AnchorEdgeLeft); | 
| + } | 
| + | 
| + if (!style()->right().isAuto() && !skipRight) { | 
| + constraints.setRightOffset(minimumValueForLength(style()->right(), constrainingRect.width())); | 
| + constraints.addAnchorEdge(ViewportConstraints::AnchorEdgeRight); | 
| + } | 
| + | 
| + bool skipBottom = false; | 
| + // FIXME(ostap): Exclude top or bottom edge offset depending on the writing mode when related | 
| 
chrishtr
2016/01/30 01:49:02
s/FIXME/TODO/ throughout, and replace names with f
 
flackr
2016/02/22 22:55:23
Done.
 | 
| + // sections are fixed in spec: http://lists.w3.org/Archives/Public/www-style/2014May/0286.html | 
| + LayoutUnit verticalOffsets = minimumValueForLength(style()->top(), constrainingRect.width()) + | 
| + minimumValueForLength(style()->bottom(), constrainingRect.width()); | 
| + if (!style()->top().isAuto() && !style()->bottom().isAuto()) { | 
| + if (verticalOffsets > containerContentRect.height() | 
| + || verticalOffsets + containerContentRect.height() > constrainingRect.height()) { | 
| + skipBottom = true; | 
| + } | 
| + } | 
| + | 
| + if (!style()->top().isAuto()) { | 
| + constraints.setTopOffset(minimumValueForLength(style()->top(), constrainingRect.height())); | 
| + constraints.addAnchorEdge(ViewportConstraints::AnchorEdgeTop); | 
| + } | 
| + | 
| + if (!style()->bottom().isAuto() && !skipBottom) { | 
| + constraints.setBottomOffset(minimumValueForLength(style()->bottom(), constrainingRect.height())); | 
| + constraints.addAnchorEdge(ViewportConstraints::AnchorEdgeBottom); | 
| + } | 
| +} | 
| + | 
| +FloatRect LayoutBoxModelObject::computeStickyConstrainingRect() const | 
| +{ | 
| + FloatRect constrainingRect; | 
| + | 
| + ASSERT(hasLayer()); | 
| + LayoutBox* enclosingClippingBox = findScrollAncestor(this); | 
| + if (enclosingClippingBox) { | 
| + constrainingRect = FloatRect(enclosingClippingBox->overflowClipRect(LayoutPoint())); | 
| + constrainingRect.move(enclosingClippingBox->paddingLeft(), enclosingClippingBox->paddingTop()); | 
| + constrainingRect.contract(FloatSize(enclosingClippingBox->paddingLeft() + enclosingClippingBox->paddingRight(), | 
| + enclosingClippingBox->paddingTop() + enclosingClippingBox->paddingBottom())); | 
| + } else { | 
| + constrainingRect = view()->frameView()->visibleContentRect(); | 
| + } | 
| + | 
| + return constrainingRect; | 
| +} | 
| + | 
| +LayoutSize LayoutBoxModelObject::stickyPositionOffset() const | 
| +{ | 
| + FloatRect constrainingRect = computeStickyConstrainingRect(); | 
| + StickyPositionViewportConstraints constraints; | 
| + computeStickyPositionConstraints(constraints, constrainingRect); | 
| + | 
| + // The sticky offset is physical, so we can just return the delta computed in absolute coords (though it may be wrong with transforms). | 
| + return LayoutSize(constraints.computeStickyOffset(constrainingRect)); | 
| +} | 
| + | 
| LayoutPoint LayoutBoxModelObject::adjustedPositionRelativeToOffsetParent(const LayoutPoint& startPoint) const | 
| { | 
| // If the element is the HTML body element or doesn't have a parent | 
| @@ -618,7 +782,7 @@ LayoutPoint LayoutBoxModelObject::adjustedPositionRelativeToOffsetParent(const L | 
| referencePoint.move(-toLayoutBox(offsetParent)->borderLeft(), -toLayoutBox(offsetParent)->borderTop()); | 
| if (!isOutOfFlowPositioned() || flowThreadContainingBlock()) { | 
| if (isInFlowPositioned()) | 
| - referencePoint.move(relativePositionOffset()); | 
| + referencePoint.move(offsetForInFlowPosition()); | 
| LayoutObject* current; | 
| for (current = parent(); current != offsetParent && current->parent(); current = current->parent()) { | 
| @@ -640,7 +804,13 @@ LayoutPoint LayoutBoxModelObject::adjustedPositionRelativeToOffsetParent(const L | 
| LayoutSize LayoutBoxModelObject::offsetForInFlowPosition() const | 
| { | 
| - return isRelPositioned() ? relativePositionOffset() : LayoutSize(); | 
| + if (isRelPositioned()) | 
| + return relativePositionOffset(); | 
| + | 
| + if (isStickyPositioned()) | 
| + return stickyPositionOffset(); | 
| + | 
| + return LayoutSize(); | 
| } | 
| LayoutUnit LayoutBoxModelObject::offsetLeft() const | 
| @@ -963,7 +1133,7 @@ const LayoutObject* LayoutBoxModelObject::pushMappingToContainer(const LayoutBox | 
| return nullptr; | 
| bool isInline = isLayoutInline(); | 
| - bool isFixedPos = !isInline && style()->position() == FixedPosition; | 
| + bool isFixedPosition = !isInline && style()->position() == FixedPosition; | 
| bool hasTransform = !isInline && hasLayer() && layer()->transform(); | 
| LayoutSize adjustmentForSkippedAncestor; | 
| @@ -975,16 +1145,19 @@ const LayoutObject* LayoutBoxModelObject::pushMappingToContainer(const LayoutBox | 
| bool offsetDependsOnPoint = false; | 
| LayoutSize containerOffset = offsetFromContainer(container, LayoutPoint(), &offsetDependsOnPoint); | 
| + LayoutSize stickyOffset; | 
| + if (style()->position() == StickyPosition) | 
| + stickyOffset = offsetForInFlowPosition(); | 
| bool preserve3D = container->style()->preserves3D() || style()->preserves3D(); | 
| if (shouldUseTransformFromContainer(container)) { | 
| TransformationMatrix t; | 
| getTransformFromContainer(container, containerOffset, t); | 
| t.translateRight(adjustmentForSkippedAncestor.width().toFloat(), adjustmentForSkippedAncestor.height().toFloat()); | 
| - geometryMap.push(this, t, preserve3D, offsetDependsOnPoint, isFixedPos, hasTransform); | 
| + geometryMap.push(this, t, preserve3D, offsetDependsOnPoint, isFixedPosition, hasTransform, LayoutSize(), stickyOffset); | 
| } else { | 
| containerOffset += adjustmentForSkippedAncestor; | 
| - geometryMap.push(this, containerOffset, preserve3D, offsetDependsOnPoint, isFixedPos, hasTransform); | 
| + geometryMap.push(this, containerOffset, preserve3D, offsetDependsOnPoint, isFixedPosition, hasTransform, LayoutSize(), stickyOffset); | 
| } | 
| return ancestorSkipped ? ancestorToStopAt : container; |