Skip to content

Commit

Permalink
[scroll-anchoring] Implement suppression triggers
Browse files Browse the repository at this point in the history
https://bugs.webkit.org/show_bug.cgi?id=261719
rdar://115704143

Reviewed by Simon Fraser.

Implement suppression of scroll anchoring adjustments according to the spec
(https://www.w3.org/TR/css-scroll-anchoring-1/#suppression-triggers). This involves suppressing scroll
 anchoring adjustments  when certain “suppression trigger” operations occur when an anchor element has
 been chosen but before a scroll anchoring adjustment has occurred, that would cause the next scroll
anchoring adjustment to be incorrect. These suppression triggers are certain style changes on the anchor
element or any element in the anchor element ancestor chain, up to and including the scrolling element
that owns the scroll anchroing controller, as well as changing to or from being absolutely posititioned,
for any element under the owning scrolling element.

* LayoutTests/imported/w3c/web-platform-tests/css/css-scroll-anchoring/ancestor-change-heuristic-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/css/css-scroll-anchoring/heuristic-with-offset-update-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/css/css-scroll-anchoring/opt-out-dynamic-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/css/css-scroll-anchoring/opt-out-dynamic-scroller-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/css/css-scroll-anchoring/position-change-heuristic-expected.txt:
* Source/WebCore/page/LocalFrameView.cpp:
(WebCore::LocalFrameView::scheduleResizeEventIfNeeded):
* Source/WebCore/page/scrolling/ScrollAnchoringController.cpp:
(WebCore::elementIsScrollableArea):
(WebCore::setInScrollAnchoringAncestorChain):
(WebCore::ScrollAnchoringController::invalidateAnchorElement):
(WebCore::ScrollAnchoringController::notifyChildHadSuppressingStyleChange):
(WebCore::scrollAnchoringControllerForElement):
(WebCore::ScrollAnchoringController::notifyParentScrollAnchoringControllerHadSuppressingStyleChange):
(WebCore::ScrollAnchoringController::didFindPriorityCandidate):
(WebCore::ScrollAnchoringController::chooseAnchorElement):
(WebCore::ScrollAnchoringController::adjustScrollPositionForAnchoring):
* Source/WebCore/page/scrolling/ScrollAnchoringController.h:
* Source/WebCore/rendering/RenderElement.cpp:
(WebCore::RenderElement::styleDidChange):
* Source/WebCore/rendering/RenderObject.h:
(WebCore::RenderObject::isInScrollAnchoringAncestorChain const):
(WebCore::RenderObject::setIsInScrollAnchoringAncestorChain):
(WebCore::RenderObject::RenderObjectBitfields::RenderObjectBitfields):
* Source/WebCore/rendering/style/RenderStyle.cpp:
(WebCore::RenderStyle::scrollAnchoringSuppressionStyleDidChange const):
(WebCore::RenderStyle::absolutePositionStyleDidChange const):
* Source/WebCore/rendering/style/RenderStyle.h:

Canonical link: https://commits.webkit.org/270455@main
  • Loading branch information
nmoucht committed Nov 9, 2023
1 parent 9dc96cf commit b02b493
Show file tree
Hide file tree
Showing 14 changed files with 142 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@

FAIL Ancestor changes in document scroller. assert_equals: expected 150 but got 220
FAIL Ancestor changes in scrollable <div>. assert_equals: expected 150 but got 220
PASS Ancestor changes in document scroller.
PASS Ancestor changes in scrollable <div>.

Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@

FAIL Positioned ancestors with dynamic changes to offsets trigger scroll suppressions. assert_equals: expected 200 but got 310
PASS Positioned ancestors with dynamic changes to offsets trigger scroll suppressions.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
content

FAIL Dynamically styling 'overflow-anchor: none' on the anchor node should prevent scroll anchoring assert_equals: expected 150 but got 200
PASS Dynamically styling 'overflow-anchor: none' on the anchor node should prevent scroll anchoring

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
content

FAIL Dynamically styling 'overflow-anchor: none' on the scroller element should prevent scroll anchoring assert_equals: expected 150 but got 200
PASS Dynamically styling 'overflow-anchor: none' on the scroller element should prevent scroll anchoring

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@

FAIL Position changes in document scroller. assert_equals: expected 200 but got 175
FAIL Position changes in scrollable <div>. assert_equals: expected 200 but got 175
PASS Position changes in document scroller.
PASS Position changes in scrollable <div>.

1 change: 1 addition & 0 deletions Source/WebCore/page/LocalFrameView.h
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,7 @@ class LocalFrameView final : public FrameView {
void updateScrollAnchoringElement() final;
void updateScrollPositionForScrollAnchoringController() final;
void invalidateScrollAnchoringElement() final;
ScrollAnchoringController* scrollAnchoringController() { return m_scrollAnchoringController.get(); }

private:
explicit LocalFrameView(LocalFrame&);
Expand Down
47 changes: 43 additions & 4 deletions Source/WebCore/page/scrolling/ScrollAnchoringController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,23 @@ ScrollAnchoringController::ScrollAnchoringController(ScrollableArea& owningScrol
: m_owningScrollableArea(owningScroller)
{ }

ScrollAnchoringController::~ScrollAnchoringController()
{
invalidateAnchorElement();
}

LocalFrameView& ScrollAnchoringController::frameView()
{
if (is<RenderLayerScrollableArea>(m_owningScrollableArea))
return downcast<RenderLayerScrollableArea>(m_owningScrollableArea).layer().renderer().view().frameView();
return downcast<LocalFrameView>(downcast<ScrollView>(m_owningScrollableArea));
}

static bool elementIsScrollableArea(const Element& element, const ScrollableArea& scrollableArea)
{
return element.renderBox() && element.renderBox()->layer() && element.renderBox()->layer()->scrollableArea() == &scrollableArea;
}

void ScrollAnchoringController::invalidateAnchorElement()
{
if (m_midUpdatingScrollPositionForAnchorElement)
Expand Down Expand Up @@ -85,6 +95,30 @@ FloatPoint ScrollAnchoringController::computeOffsetFromOwningScroller(RenderObje
return FloatPoint(candidate.absoluteBoundingBoxRect().location() - boundingRectForScrollableArea(m_owningScrollableArea).location());
}

void ScrollAnchoringController::notifyChildHadSuppressingStyleChange()
{
LOG_WITH_STREAM(ScrollAnchoring, stream << "ScrollAnchoringController::notifyChildHadSuppressingStyleChange() for scroller: " << m_owningScrollableArea);

m_shouldSupressScrollPositionUpdate = true;
}

bool ScrollAnchoringController::isInScrollAnchoringAncestorChain(const RenderObject& object)
{
RefPtr iterElement = m_anchorElement.get();

while (iterElement) {
if (auto* renderer = iterElement->renderer()) {
LOG_WITH_STREAM(ScrollAnchoring, stream << "ScrollAnchoringController::isInScrollAnchoringAncestorChain() checking for : " <<object << " current Element: " << *iterElement);
if (&object == renderer)
return true;
}
if (iterElement && elementIsScrollableArea(*iterElement, m_owningScrollableArea))
break;
iterElement = iterElement->parentElement();
}
return false;
}

static RefPtr<Element> anchorElementForPriorityCandidate(Element* element)
{
while (element) {
Expand Down Expand Up @@ -249,16 +283,21 @@ void ScrollAnchoringController::updateAnchorElement()

void ScrollAnchoringController::adjustScrollPositionForAnchoring()
{
SetForScope midUpdatingScrollPositionForAnchorElement(m_midUpdatingScrollPositionForAnchorElement, true);
auto queued = std::exchange(m_isQueuedForScrollPositionUpdate, false);
auto supressed = std::exchange(m_shouldSupressScrollPositionUpdate, false);
if (!m_anchorElement || !queued)
return;
auto renderBox = m_anchorElement->renderer();
if (!renderBox) {
auto* renderer = m_anchorElement->renderer();
if (!renderer || supressed) {
invalidateAnchorElement();
updateAnchorElement();
if (supressed)
LOG_WITH_STREAM(ScrollAnchoring, stream << "ScrollAnchoringController::updateScrollPosition() supressing scroll adjustment for frame: " << frameView() << " for scroller: " << m_owningScrollableArea);
return;
}
FloatSize adjustment = computeOffsetFromOwningScroller(*renderBox) - m_lastOffsetForAnchorElement;
SetForScope midUpdatingScrollPositionForAnchorElement(m_midUpdatingScrollPositionForAnchorElement, true);

FloatSize adjustment = computeOffsetFromOwningScroller(*renderer) - m_lastOffsetForAnchorElement;
if (!adjustment.isZero()) {
auto newScrollPosition = m_owningScrollableArea.scrollPosition() + IntPoint(adjustment.width(), adjustment.height());
LOG_WITH_STREAM(ScrollAnchoring, stream << "ScrollAnchoringController::updateScrollPosition() for frame: " << frameView() << " for scroller: " << m_owningScrollableArea << " adjusting from: " << m_owningScrollableArea.scrollPosition() << " to: " << newScrollPosition);
Expand Down
6 changes: 6 additions & 0 deletions Source/WebCore/page/scrolling/ScrollAnchoringController.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#pragma once

#include "Document.h"
#include "Element.h"
#include "FloatPoint.h"
#include "ScrollTypes.h"
#include <wtf/WeakPtr.h>
Expand All @@ -44,11 +45,15 @@ class ScrollAnchoringController final : public CanMakeWeakPtr<ScrollAnchoringCon
WTF_MAKE_FAST_ALLOCATED;
public:
explicit ScrollAnchoringController(ScrollableArea&);
~ScrollAnchoringController();
void invalidateAnchorElement();
void adjustScrollPositionForAnchoring();
void selectAnchorElement();
void chooseAnchorElement(Document&);
void updateAnchorElement();
void notifyChildHadSuppressingStyleChange();
bool isInScrollAnchoringAncestorChain(const RenderObject&);
Element* anchorElement() const { return m_anchorElement.get(); }

private:
Element* findAnchorElementRecursive(Element*);
Expand All @@ -62,6 +67,7 @@ class ScrollAnchoringController final : public CanMakeWeakPtr<ScrollAnchoringCon
FloatPoint m_lastOffsetForAnchorElement;
bool m_midUpdatingScrollPositionForAnchorElement { false };
bool m_isQueuedForScrollPositionUpdate { false };
bool m_shouldSupressScrollPositionUpdate { false };
};

} // namespace WebCore
8 changes: 8 additions & 0 deletions Source/WebCore/rendering/RenderElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1010,6 +1010,14 @@ void RenderElement::styleDidChange(StyleDifference diff, const RenderStyle* oldS
updateOutlineAutoAncestor(hasOutlineAuto);
issueRepaintForOutlineAuto(hasOutlineAuto ? outlineStyleForRepaint().outlineSize() : oldStyle->outlineSize());
}

bool shouldCheckIfInAncestorChain = false;
if (frame().settings().cssScrollAnchoringEnabled() && (style().outOfFlowPositionStyleDidChange(oldStyle) || (shouldCheckIfInAncestorChain = style().scrollAnchoringSuppressionStyleDidChange(oldStyle)))) {
LOG_WITH_STREAM(ScrollAnchoring, stream << "RenderElement::styleDidChange() found node with style change: " << *this << " from: " << oldStyle->position() <<" to: " << style().position());
auto* controller = findScrollAnchoringControllerForRenderer(*this);
if (controller && (!shouldCheckIfInAncestorChain || (shouldCheckIfInAncestorChain && controller->isInScrollAnchoringAncestorChain(*this))))
controller->notifyChildHadSuppressingStyleChange();
}
}

void RenderElement::insertedIntoTree(IsInternalMove isInternalMove)
Expand Down
1 change: 1 addition & 0 deletions Source/WebCore/rendering/RenderLayerScrollableArea.h
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ class RenderLayerScrollableArea final : public ScrollableArea {
void updateScrollAnchoringElement() final;
void updateScrollPositionForScrollAnchoringController() final;
void invalidateScrollAnchoringElement() final;
ScrollAnchoringController* scrollAnchoringController() { return m_scrollAnchoringController.get(); }

private:
bool hasHorizontalOverflow() const;
Expand Down
17 changes: 17 additions & 0 deletions Source/WebCore/rendering/RenderObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
#include "RenderLayer.h"
#include "RenderLayerBacking.h"
#include "RenderLayerCompositor.h"
#include "RenderLayerScrollableArea.h"
#include "RenderLineBreak.h"
#include "RenderMultiColumnFlow.h"
#include "RenderMultiColumnSet.h"
Expand Down Expand Up @@ -2346,6 +2347,22 @@ Vector<FloatRect> RenderObject::clientBorderAndTextRects(const SimpleRange& rang
return borderAndTextRects(range, CoordinateSpace::Client, { });
}

ScrollAnchoringController* RenderObject::findScrollAnchoringControllerForRenderer(const RenderObject& renderer)
{
if (renderer.hasLayer()) {
if (auto* scrollableArea = downcast<RenderLayerModelObject>(renderer).layer()->scrollableArea())
return scrollableArea->scrollAnchoringController();
}
for (auto* enclosingLayer = renderer.enclosingLayer(); enclosingLayer; enclosingLayer = enclosingLayer->parent()) {
if (RenderLayerScrollableArea* scrollableArea = enclosingLayer->scrollableArea()) {
auto controller = scrollableArea->scrollAnchoringController();
if (controller && controller->anchorElement())
return controller;
}
}
return renderer.view().frameView().scrollAnchoringController();
}

#if PLATFORM(IOS_FAMILY)

static bool intervalsSufficientlyOverlap(int startA, int endA, int startB, int endB)
Expand Down
2 changes: 2 additions & 0 deletions Source/WebCore/rendering/RenderObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,8 @@ class RenderObject : public CachedImageClient, public CanMakeCheckedPtr {

bool everHadLayout() const { return m_bitfields.everHadLayout(); }

static ScrollAnchoringController* findScrollAnchoringControllerForRenderer(const RenderObject&);

bool childrenInline() const { return m_bitfields.childrenInline(); }
virtual void setChildrenInline(bool b) { m_bitfields.setChildrenInline(b); }

Expand Down
55 changes: 55 additions & 0 deletions Source/WebCore/rendering/style/RenderStyle.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
#include "FontSelector.h"
#include "InlineIteratorTextBox.h"
#include "InlineTextBoxStyle.h"
#include "Logging.h"
#include "MotionPath.h"
#include "Pagination.h"
#include "PathTraversalState.h"
Expand Down Expand Up @@ -1379,6 +1380,60 @@ bool RenderStyle::changeRequiresRecompositeLayer(const RenderStyle& other, Optio
return false;
}

bool RenderStyle::scrollAnchoringSuppressionStyleDidChange(const RenderStyle* other) const
{
// https://drafts.csswg.org/css-scroll-anchoring/#suppression-triggers
// Determine if there are any style changes that should result in an scroll anchoring suppression
if (!other)
return false;

if (m_nonInheritedData->boxData.ptr() != other->m_nonInheritedData->boxData.ptr()) {
if (m_nonInheritedData->boxData->width() != other->m_nonInheritedData->boxData->width()
|| m_nonInheritedData->boxData->minWidth() != other->m_nonInheritedData->boxData->minWidth()
|| m_nonInheritedData->boxData->maxWidth() != other->m_nonInheritedData->boxData->maxWidth()
|| m_nonInheritedData->boxData->height() != other->m_nonInheritedData->boxData->height()
|| m_nonInheritedData->boxData->minHeight() != other->m_nonInheritedData->boxData->minHeight()
|| m_nonInheritedData->boxData->maxHeight() != other->m_nonInheritedData->boxData->maxHeight())
return true;
}

if (overflowAnchor() != other->overflowAnchor() && overflowAnchor() == OverflowAnchor::None)
return true;

if (position() != other->position())
return true;

if (m_nonInheritedData->surroundData.ptr() && other->m_nonInheritedData->surroundData.ptr() && m_nonInheritedData->surroundData != other->m_nonInheritedData->surroundData) {
if (m_nonInheritedData->surroundData->margin != other->m_nonInheritedData->surroundData->margin)
return true;

if (m_nonInheritedData->surroundData->padding != other->m_nonInheritedData->surroundData->padding)
return true;
}

if (position() != PositionType::Static) {
if (m_nonInheritedData->surroundData->offset != other->m_nonInheritedData->surroundData->offset)
return true;
}

if (hasTransformRelatedProperty() != other->hasTransformRelatedProperty())
return true;

return false;
}

bool RenderStyle::outOfFlowPositionStyleDidChange(const RenderStyle* other) const
{
// https://drafts.csswg.org/css-scroll-anchoring/#suppression-triggers
// Determine if there is a style change that causes an element to become or stop
// being absolutely or fixed positioned
if (other && m_nonInheritedData.ptr() != other->m_nonInheritedData.ptr()) {
if (hasOutOfFlowPosition() != other->hasOutOfFlowPosition())
return true;
}
return false;
}

StyleDifference RenderStyle::diff(const RenderStyle& other, OptionSet<StyleDifferenceContextSensitiveProperty>& changedContextSensitiveProperties) const
{
changedContextSensitiveProperties = OptionSet<StyleDifferenceContextSensitiveProperty>();
Expand Down
2 changes: 2 additions & 0 deletions Source/WebCore/rendering/style/RenderStyle.h
Original file line number Diff line number Diff line change
Expand Up @@ -2142,6 +2142,8 @@ class RenderStyle {
static constexpr BlockStepInsert initialBlockStepInsert();
inline BlockStepInsert blockStepInsert() const;
inline void setBlockStepInsert(BlockStepInsert);
bool scrollAnchoringSuppressionStyleDidChange(const RenderStyle*) const;
bool outOfFlowPositionStyleDidChange(const RenderStyle*) const;

private:
struct NonInheritedFlags {
Expand Down

0 comments on commit b02b493

Please sign in to comment.