Skip to content
Permalink
Browse files
[css-scroll-snap] make resnap follow scroll snap target if necessary
https://bugs.webkit.org/show_bug.cgi?id=244745
<rdar://99557242>

Reviewed by Martin Robinson.

CSS scroll snap spec (https://www.w3.org/TR/css-scroll-snap-1/#re-snap): "If multiple boxes were snapped
before and their snap positions no longer coincide, then if one of them is focused or targeted, the scroll
container must re-snap to that one and otherwise which one to re-snap to is UA-defined." To acheive this,
we add an id to scroll offset which represents the associated element to that scroll offset. We also add a bool which
represents wether the associated element is focused. The id of the currently snapped element is added
to ScrollSnapAnimatorState and for each relayout, check if it is necessary to preserve the currently
snapped element.

* LayoutTests/imported/w3c/web-platform-tests/css/css-scroll-snap/snap-after-relayout/resnap-to-focused-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/css/css-scroll-snap/snap-after-relayout/snap-to-different-targets-expected.txt:
* Source/WebCore/page/FrameView.cpp:
(WebCore::FrameView::updateSnapOffsets):
* Source/WebCore/page/scrolling/ScrollSnapOffsetsInfo.cpp:
(WebCore::updateSnapOffsetsForScrollableArea):
(WebCore::convertOffsetInfo):
* Source/WebCore/page/scrolling/ScrollSnapOffsetsInfo.h:
(WebCore::operator<<):
* Source/WebCore/platform/ScrollSnapAnimatorState.cpp:
(WebCore::ScrollSnapAnimatorState::setFocusedElementForAxis):
(WebCore::ScrollSnapAnimatorState::preserveCurrentTargetForAxis):
(WebCore::ScrollSnapAnimatorState::resnapAfterLayout):
* Source/WebCore/platform/ScrollSnapAnimatorState.h:
(WebCore::ScrollSnapAnimatorState::activeSnapIDForAxis const):
(WebCore::ScrollSnapAnimatorState::setActiveSnapIndexIDForAxis):
* Source/WebCore/platform/ScrollableArea.cpp:
(WebCore::ScrollableArea::resnapAfterLayout):
* Source/WebCore/rendering/RenderLayerScrollableArea.cpp:
(WebCore::RenderLayerScrollableArea::updateSnapOffsets):
* Source/WebKit/Shared/RemoteLayerTree/RemoteScrollingCoordinatorTransaction.cpp:
(ArgumentCoder<SnapOffset<float>>::encode):
(ArgumentCoder<SnapOffset<float>>::decode):

Canonical link: https://commits.webkit.org/254773@main
  • Loading branch information
nmoucht committed Sep 23, 2022
1 parent 431ab9f commit 5ed2b1dffea4b8da9c99f3e9eee1e792b66f37ef
Show file tree
Hide file tree
Showing 10 changed files with 95 additions and 20 deletions.
@@ -1,3 +1,3 @@

FAIL Resnap to focused element after relayout assert_equals: After resize, should snap to row 4. expected 4 but got 3
PASS Resnap to focused element after relayout

@@ -1,3 +1,3 @@

FAIL Scroller should snap to at least one of the targets if unable to snap toboth after a layout change. assert_true: expected true got false
PASS Scroller should snap to at least one of the targets if unable to snap toboth after a layout change.

@@ -936,7 +936,7 @@ void FrameView::updateSnapOffsets()
LayoutRect viewport = LayoutRect(IntPoint(), baseLayoutViewportSize());
viewport.move(-rootRenderer->marginLeft(), -rootRenderer->marginTop());

updateSnapOffsetsForScrollableArea(*this, *rootRenderer, *styleToUse, viewport, rootRenderer->style().writingMode(), rootRenderer->style().direction());
updateSnapOffsetsForScrollableArea(*this, *rootRenderer, *styleToUse, viewport, rootRenderer->style().writingMode(), rootRenderer->style().direction(), frame().document()->focusedElement());
}

bool FrameView::isScrollSnapInProgress() const
@@ -277,7 +277,7 @@ static std::pair<bool, bool> axesFlippedForWritingModeAndDirection(WritingMode w
return std::make_pair(hasVerticalWritingMode ? blockAxisFlipped : inlineAxisFlipped, hasVerticalWritingMode ? inlineAxisFlipped : blockAxisFlipped);
}

void updateSnapOffsetsForScrollableArea(ScrollableArea& scrollableArea, const RenderBox& scrollingElementBox, const RenderStyle& scrollingElementStyle, LayoutRect viewportRectInBorderBoxCoordinates, WritingMode writingMode, TextDirection textDirection)
void updateSnapOffsetsForScrollableArea(ScrollableArea& scrollableArea, const RenderBox& scrollingElementBox, const RenderStyle& scrollingElementStyle, LayoutRect viewportRectInBorderBoxCoordinates, WritingMode writingMode, TextDirection textDirection, Element* focusedElement)
{
auto scrollSnapType = scrollingElementStyle.scrollSnapType();
const auto& boxesWithScrollSnapPositions = scrollingElementBox.view().boxesWithScrollSnapPositions();
@@ -286,13 +286,13 @@ void updateSnapOffsetsForScrollableArea(ScrollableArea& scrollableArea, const Re
return;
}

auto addOrUpdateStopForSnapOffset = [](HashMap<LayoutUnit, SnapOffset<LayoutUnit>>& offsets, LayoutUnit newOffset, ScrollSnapStop stop, bool hasSnapAreaLargerThanViewport, size_t snapAreaIndices)
auto addOrUpdateStopForSnapOffset = [](HashMap<LayoutUnit, SnapOffset<LayoutUnit>>& offsets, LayoutUnit newOffset, ScrollSnapStop stop, bool hasSnapAreaLargerThanViewport, uint64_t snapTargetID, bool isFocused, size_t snapAreaIndices)
{
if (!offsets.isValidKey(newOffset))
return;

auto offset = offsets.ensure(newOffset, [&] {
return SnapOffset<LayoutUnit> { newOffset, stop, hasSnapAreaLargerThanViewport, { } };
return SnapOffset<LayoutUnit> { newOffset, stop, hasSnapAreaLargerThanViewport, snapTargetID, isFocused, { } };
});

// If the offset already exists, we ensure that it has ScrollSnapStop::Always, when appropriate.
@@ -371,12 +371,12 @@ void updateSnapOffsetsForScrollableArea(ScrollableArea& scrollableArea, const Re
if (snapsHorizontally) {
auto absoluteScrollXPosition = computeScrollSnapAlignOffset(scrollSnapArea.x(), scrollSnapArea.maxX(), xAlign, areaXAxisFlipped) - computeScrollSnapAlignOffset(scrollSnapPort.x(), scrollSnapPort.maxX(), xAlign, areaXAxisFlipped);
auto absoluteScrollOffset = clampTo<int>(scrollableArea.scrollOffsetFromPosition({ roundToInt(absoluteScrollXPosition), 0 }).x(), 0, maxScrollOffset.x());
addOrUpdateStopForSnapOffset(horizontalSnapOffsetsMap, absoluteScrollOffset, stop, scrollSnapAreaAsOffsets.width() > scrollSnapPort.width(), snapAreas.size() - 1);
addOrUpdateStopForSnapOffset(horizontalSnapOffsetsMap, absoluteScrollOffset, stop, scrollSnapAreaAsOffsets.width() > scrollSnapPort.width(), child->element()->identifier().toUInt64(), focusedElement == child->element(), snapAreas.size() - 1);
}
if (snapsVertically) {
auto absoluteScrollYPosition = computeScrollSnapAlignOffset(scrollSnapArea.y(), scrollSnapArea.maxY(), yAlign, areaYAxisFlipped) - computeScrollSnapAlignOffset(scrollSnapPort.y(), scrollSnapPort.maxY(), yAlign, areaYAxisFlipped);
auto absoluteScrollOffset = clampTo<int>(scrollableArea.scrollOffsetFromPosition({ 0, roundToInt(absoluteScrollYPosition) }).y(), 0, maxScrollOffset.y());
addOrUpdateStopForSnapOffset(verticalSnapOffsetsMap, absoluteScrollOffset, stop, scrollSnapAreaAsOffsets.height() > scrollSnapPort.height(), snapAreas.size() - 1);
addOrUpdateStopForSnapOffset(verticalSnapOffsetsMap, absoluteScrollOffset, stop, scrollSnapAreaAsOffsets.height() > scrollSnapPort.height(), child->element()->identifier().toUInt64(), focusedElement == child->element(), snapAreas.size() - 1);
}

if (!snapAreas.isEmpty())
@@ -424,7 +424,7 @@ static ScrollSnapOffsetsInfo<OutputType, OutputRectType> convertOffsetInfo(const
auto convertOffsets = [scaleFactor](const Vector<SnapOffset<InputType>>& input)
{
return input.map([scaleFactor](auto& offset) -> SnapOffset<OutputType> {
return { convertOffsetUnit(offset.offset, scaleFactor), offset.stop, offset.hasSnapAreaLargerThanViewport, offset.snapAreaIndices };
return { convertOffsetUnit(offset.offset, scaleFactor), offset.stop, offset.hasSnapAreaLargerThanViewport, offset.snapTargetID, offset.isFocused, offset.snapAreaIndices };
});
};

@@ -38,12 +38,15 @@ namespace WebCore {
class ScrollableArea;
class RenderBox;
class RenderStyle;
class Element;

template <typename T>
struct SnapOffset {
T offset;
ScrollSnapStop stop;
bool hasSnapAreaLargerThanViewport;
uint64_t snapTargetID;
bool isFocused;
Vector<size_t> snapAreaIndices;
};

@@ -97,11 +100,11 @@ WEBCORE_EXPORT std::pair<LayoutUnit, std::optional<unsigned>> LayoutScrollSnapOf
// Update the snap offsets for this scrollable area, given the RenderBox of the scroll container, the RenderStyle
// which defines the scroll-snap properties, and the viewport rectangle with the origin at the top left of
// the scrolling container's border box.
void updateSnapOffsetsForScrollableArea(ScrollableArea&, const RenderBox& scrollingElementBox, const RenderStyle& scrollingElementStyle, LayoutRect viewportRectInBorderBoxCoordinates, WritingMode, TextDirection);
void updateSnapOffsetsForScrollableArea(ScrollableArea&, const RenderBox& scrollingElementBox, const RenderStyle& scrollingElementStyle, LayoutRect viewportRectInBorderBoxCoordinates, WritingMode, TextDirection, Element*);

template <typename T> WTF::TextStream& operator<<(WTF::TextStream& ts, SnapOffset<T> offset)
{
ts << offset.offset;
ts << offset.offset << " snapTargetID: " << offset.snapTargetID << " isFocused: " << offset.isFocused;
if (offset.stop == ScrollSnapStop::Always)
ts << " (always)";
return ts;
@@ -95,18 +95,72 @@ float ScrollSnapAnimatorState::adjustedScrollDestination(ScrollEventAxis axis, F
return offset * pageScale;
}

void ScrollSnapAnimatorState::setFocusedElementForAxis(ScrollEventAxis axis)
{
auto snapOffsets = snapOffsetsForAxis(axis);
auto found = std::find_if(snapOffsets.begin(), snapOffsets.end(), [](SnapOffset<LayoutUnit> p) -> bool {
return p.isFocused;
});
if (found == snapOffsets.end())
return;
auto newIndex = std::distance(snapOffsets.begin(), found);
auto newID = snapOffsets[newIndex].snapTargetID;
setActiveSnapIndexForAxis(axis, newIndex);
setActiveSnapIndexIDForAxis(axis, newID);
}

bool ScrollSnapAnimatorState::preserveCurrentTargetForAxis(ScrollEventAxis axis)
{
auto snapID = activeSnapIDForAxis(axis);
auto snapOffsets = snapOffsetsForAxis(axis);

auto found = std::find_if(snapOffsets.begin(), snapOffsets.end(), [snapID](SnapOffset<LayoutUnit> p) -> bool {
return p.snapTargetID == *snapID;
});
if (found == snapOffsets.end())
return false;

setActiveSnapIndexForAxis(axis, std::distance(snapOffsets.begin(), found));
return true;
}

bool ScrollSnapAnimatorState::resnapAfterLayout(ScrollOffset scrollOffset, const ScrollExtents& scrollExtents, float pageScale)
{
// Check if we need to set the active target to a focused element
setFocusedElementForAxis(ScrollEventAxis::Vertical);
setFocusedElementForAxis(ScrollEventAxis::Horizontal);

bool snapPointChanged = false;
// If we are already snapped in a particular axis, maintain that. Otherwise, snap to the nearest eligible snap point.
auto activeHorizontalIndex = activeSnapIndexForAxis(ScrollEventAxis::Horizontal);
if (!activeHorizontalIndex || *activeHorizontalIndex >= snapOffsetsForAxis(ScrollEventAxis::Horizontal).size())
snapPointChanged |= setNearestScrollSnapIndexForAxisAndOffset(ScrollEventAxis::Horizontal, scrollOffset, scrollExtents, pageScale);

auto activeVerticalIndex = activeSnapIndexForAxis(ScrollEventAxis::Vertical);
if (!activeVerticalIndex || *activeVerticalIndex >= snapOffsetsForAxis(ScrollEventAxis::Vertical).size())
auto activeHorizontalID = activeSnapIDForAxis(ScrollEventAxis::Horizontal);
auto activeVerticalID = activeSnapIDForAxis(ScrollEventAxis::Vertical);
auto snapOffsetsVertical = snapOffsetsForAxis(ScrollEventAxis::Vertical);
auto snapOffsetsHorizontal = snapOffsetsForAxis(ScrollEventAxis::Horizontal);

// Check if we need to set the current indices
if (!activeVerticalIndex || *activeVerticalIndex >= snapOffsetsForAxis(ScrollEventAxis::Vertical).size()) {
snapPointChanged |= setNearestScrollSnapIndexForAxisAndOffset(ScrollEventAxis::Vertical, scrollOffset, scrollExtents, pageScale);

activeVerticalIndex = activeSnapIndexForAxis(ScrollEventAxis::Vertical);
}
if (!activeHorizontalIndex || *activeHorizontalIndex >= snapOffsetsForAxis(ScrollEventAxis::Horizontal).size()) {
snapPointChanged |= setNearestScrollSnapIndexForAxisAndOffset(ScrollEventAxis::Horizontal, scrollOffset, scrollExtents, pageScale);
activeHorizontalIndex = activeSnapIndexForAxis(ScrollEventAxis::Horizontal);
}

// If we have an active target, see if we need to preserve it
if (activeHorizontalID && activeHorizontalIndex) {
if (*activeHorizontalID != snapOffsetsHorizontal[*activeHorizontalIndex].snapTargetID)
snapPointChanged |= preserveCurrentTargetForAxis(ScrollEventAxis::Horizontal);
} else if (activeVerticalID && activeVerticalIndex && *activeVerticalID != snapOffsetsVertical[*activeVerticalIndex].snapTargetID)
snapPointChanged |= preserveCurrentTargetForAxis(ScrollEventAxis::Vertical);

// If we do not have current targets and are snapped to multiple targets, set them
if ((!activeHorizontalID && activeHorizontalIndex) && (!activeVerticalID && activeVerticalIndex) && snapOffsetsForAxis(ScrollEventAxis::Horizontal)[*activeHorizontalIndex].snapTargetID != snapOffsetsForAxis(ScrollEventAxis::Vertical)[*activeVerticalIndex].snapTargetID) {
setActiveSnapIndexIDForAxis(ScrollEventAxis::Horizontal, snapOffsetsForAxis(ScrollEventAxis::Horizontal)[*activeHorizontalIndex].snapTargetID);
setActiveSnapIndexIDForAxis(ScrollEventAxis::Vertical, snapOffsetsForAxis(ScrollEventAxis::Vertical)[*activeVerticalIndex].snapTargetID);
}
LOG_WITH_STREAM(ScrollSnap, stream << "ScrollSnapAnimatorState::resnapAfterLayout() current target: horizontal: " << activeHorizontalID << " vertical: "<< activeVerticalID);
return snapPointChanged;
}

@@ -73,6 +73,11 @@ class ScrollSnapAnimatorState {
{
return axis == ScrollEventAxis::Horizontal ? m_activeSnapIndexX : m_activeSnapIndexY;
}

std::optional<unsigned> activeSnapIDForAxis(ScrollEventAxis axis) const
{
return axis == ScrollEventAxis::Horizontal ? m_activeSnapIDX : m_activeSnapIDY;
}

void setActiveSnapIndexForAxis(ScrollEventAxis axis, std::optional<unsigned> index)
{
@@ -81,6 +86,14 @@ class ScrollSnapAnimatorState {
else
m_activeSnapIndexY = index;
}

void setActiveSnapIndexIDForAxis(ScrollEventAxis axis, std::optional<unsigned> snapIndexID)
{
if (axis == ScrollEventAxis::Horizontal)
m_activeSnapIDX = snapIndexID;
else
m_activeSnapIDY = snapIndexID;
}

std::optional<unsigned> closestSnapPointForOffset(ScrollEventAxis, ScrollOffset, const ScrollExtents&, float pageScale) const;
float adjustedScrollDestination(ScrollEventAxis, FloatPoint destinationOffset, float velocity, std::optional<float> originalOffset, const ScrollExtents&, float pageScale) const;
@@ -98,7 +111,8 @@ class ScrollSnapAnimatorState {

void transitionToUserInteractionState();
void transitionToDestinationReachedState();

bool preserveCurrentTargetForAxis(ScrollEventAxis);
void setFocusedElementForAxis(ScrollEventAxis);
private:
std::pair<float, std::optional<unsigned>> targetOffsetForStartOffset(ScrollEventAxis, const ScrollExtents&, float startOffset, FloatPoint predictedOffset, float pageScale, float initialDelta) const;
bool setupAnimationForState(ScrollSnapState, const ScrollExtents&, float pageScale, const FloatPoint& initialOffset, const FloatSize& initialVelocity, const FloatSize& initialDelta);
@@ -112,6 +126,8 @@ class ScrollSnapAnimatorState {

std::optional<unsigned> m_activeSnapIndexX;
std::optional<unsigned> m_activeSnapIndexY;
std::optional<unsigned> m_activeSnapIDX;
std::optional<unsigned> m_activeSnapIDY;
};

WTF::TextStream& operator<<(WTF::TextStream&, const ScrollSnapAnimatorState&);
@@ -551,7 +551,6 @@ void ScrollableArea::resnapAfterLayout()
}

if (correctedOffset != currentOffset) {
LOG_WITH_STREAM(ScrollSnap, stream << " adjusting offset from " << currentOffset << " to " << correctedOffset);
auto position = scrollPositionFromOffset(correctedOffset);
if (scrollAnimationStatus() == ScrollAnimationStatus::NotAnimating)
scrollToOffsetWithoutAnimation(correctedOffset);
@@ -1552,7 +1552,7 @@ void RenderLayerScrollableArea::updateSnapOffsets()
return;

RenderBox* box = m_layer.enclosingElement()->renderBox();
updateSnapOffsetsForScrollableArea(*this, *box, box->style(), box->paddingBoxRect(), box->style().writingMode(), box->style().direction());
updateSnapOffsetsForScrollableArea(*this, *box, box->style(), box->paddingBoxRect(), box->style().writingMode(), box->style().direction(), m_layer.renderer().document().focusedElement());
}

bool RenderLayerScrollableArea::isScrollSnapInProgress() const
@@ -536,6 +536,7 @@ void ArgumentCoder<SnapOffset<float>>::encode(Encoder& encoder, const SnapOffset
encoder << offset.offset;
encoder << offset.stop;
encoder << offset.hasSnapAreaLargerThanViewport;
encoder << offset.snapTargetID;
encoder << offset.snapAreaIndices;
}

@@ -547,6 +548,8 @@ bool ArgumentCoder<SnapOffset<float>>::decode(Decoder& decoder, SnapOffset<float
return false;
if (!decoder.decode(offset.hasSnapAreaLargerThanViewport))
return false;
if (!decoder.decode(offset.snapTargetID))
return false;
if (!decoder.decode(offset.snapAreaIndices))
return false;
return true;

0 comments on commit 5ed2b1d

Please sign in to comment.