blob: edeb426fdd4e86cee65166181f1a2f7d0eb6f7fc [file] [log] [blame]
/*
* Copyright (C) 2014-2022 Apple Inc. All rights reserved.
* Copyright (C) 2020 Igalia S.L.
*
* 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. AND ITS CONTRIBUTORS ``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 ITS 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 "ScrollSnapOffsetsInfo.h"
#include "ElementChildIterator.h"
#include "LayoutRect.h"
#include "Length.h"
#include "Logging.h"
#include "RenderBox.h"
#include "RenderStyle.h"
#include "RenderView.h"
#include "ScrollableArea.h"
#include "StyleScrollSnapPoints.h"
#include <wtf/text/StringConcatenateNumbers.h>
namespace WebCore {
template <typename UnitType, typename RectType>
static std::pair<UnitType, UnitType> rangeForAxis(RectType rect, ScrollEventAxis axis)
{
return axis == ScrollEventAxis::Horizontal ? std::make_pair(rect.x(), rect.maxX()) : std::make_pair(rect.y(), rect.maxY());
}
template <typename UnitType>
struct PotentialSnapPointSearchResult {
std::optional<std::pair<UnitType, unsigned>> previous;
std::optional<std::pair<UnitType, unsigned>> next;
std::optional<std::pair<UnitType, unsigned>> snapStop;
bool landedInsideSnapAreaThatConsumesViewport;
};
template <typename InfoType, typename UnitType>
static PotentialSnapPointSearchResult<UnitType> searchForPotentialSnapPoints(const InfoType& info, ScrollEventAxis axis, UnitType viewportLength, UnitType destinationOffset, std::optional<UnitType> originalOffset)
{
const auto& snapOffsets = info.offsetsForAxis(axis);
std::optional<std::pair<UnitType, unsigned>> previous, next, exact, snapStop;
bool landedInsideSnapAreaThatConsumesViewport = false;
// A particular snap stop is better if it's between the original offset and destination offset and closer original
// offset than the previously selected snap stop. We always want to stop at the snap stop closest to the original offset.
auto isBetterSnapStop = [&](UnitType candidate) {
if (!originalOffset)
return false;
auto original = *originalOffset;
if (candidate <= std::min(destinationOffset, original) || candidate >= std::max(destinationOffset, original))
return false;
return !snapStop || std::abs(float { candidate - original }) < std::abs(float { (*snapStop).first - original });
};
for (unsigned i = 0; i < snapOffsets.size(); i++) {
if (!landedInsideSnapAreaThatConsumesViewport && snapOffsets[i].hasSnapAreaLargerThanViewport) {
for (auto snapAreaIndices : snapOffsets[i].snapAreaIndices) {
auto [snapAreaMin, snapAreaMax] = rangeForAxis<UnitType>(info.snapAreas[snapAreaIndices], axis);
if (snapAreaMin <= destinationOffset && snapAreaMax >= (destinationOffset + viewportLength)) {
landedInsideSnapAreaThatConsumesViewport = true;
break;
}
}
}
UnitType potentialSnapOffset = snapOffsets[i].offset;
if (potentialSnapOffset == destinationOffset)
exact = std::make_pair(potentialSnapOffset, i);
else if (potentialSnapOffset < destinationOffset)
previous = std::make_pair(potentialSnapOffset, i);
else if (!next && potentialSnapOffset > destinationOffset)
next = std::make_pair(potentialSnapOffset, i);
if (snapOffsets[i].stop == ScrollSnapStop::Always && isBetterSnapStop(potentialSnapOffset))
snapStop = std::make_pair(potentialSnapOffset, i);
}
if (exact)
return { exact, exact, snapStop, landedInsideSnapAreaThatConsumesViewport };
return { previous, next, snapStop, landedInsideSnapAreaThatConsumesViewport };
}
template <typename UnitType, typename PointType>
static UnitType componentForAxis(PointType point, ScrollEventAxis axis)
{
return axis == ScrollEventAxis::Horizontal ? point.x() : point.y();
}
template <typename InfoType, typename UnitType, typename PointType, typename SizeType>
static bool hasCompatibleSnapArea(const InfoType& info, const SnapOffset<UnitType>& snapOffset, ScrollEventAxis axis, const SizeType& viewportSize, PointType destinationOffsetPoint)
{
auto otherAxis = axis == ScrollEventAxis::Horizontal ? ScrollEventAxis::Vertical : ScrollEventAxis::Horizontal;
auto scrollDestinationInOtherAxis = componentForAxis<UnitType, PointType>(destinationOffsetPoint, otherAxis);
auto viewportLengthInOtherAxis = axis == ScrollEventAxis::Horizontal ? viewportSize.height() : viewportSize.width();
return snapOffset.snapAreaIndices.findIf([&] (auto index) {
const auto& snapArea = info.snapAreas[index];
auto [otherAxisMin, otherAxisMax] = rangeForAxis<UnitType>(snapArea, otherAxis);
return (scrollDestinationInOtherAxis + viewportLengthInOtherAxis) > otherAxisMin && scrollDestinationInOtherAxis < otherAxisMax;
}) != notFound;
}
template <typename InfoType, typename UnitType, typename PointType, typename SizeType>
static void adjustPreviousAndNextForOnScreenSnapAreas(const InfoType& info, ScrollEventAxis axis, const SizeType& viewportSize, PointType destinationOffsetPoint, PotentialSnapPointSearchResult<UnitType>& searchResult)
{
// hasCompatibleSnapArea needs to look at all compatible snap areas, which might be a large
// number for snap areas arranged in a grid. Since this might be expensive, this code tries
// to look at the mostly closest compatible snap areas first.
const auto& snapOffsets = info.offsetsForAxis(axis);
if (searchResult.previous) {
unsigned oldIndex = (*searchResult.previous).second;
searchResult.previous.reset();
for (unsigned offset = 0; offset <= oldIndex; offset++) {
unsigned index = oldIndex - offset;
const auto& snapOffset = snapOffsets[index];
if (hasCompatibleSnapArea(info, snapOffset, axis, viewportSize, destinationOffsetPoint)) {
searchResult.previous = { snapOffset.offset, index };
break;
}
}
}
if (searchResult.next) {
unsigned oldIndex = (*searchResult.next).second;
searchResult.next.reset();
for (unsigned index = oldIndex; index < snapOffsets.size(); index++) {
const auto& snapOffset = snapOffsets[index];
if (hasCompatibleSnapArea(info, snapOffset, axis, viewportSize, destinationOffsetPoint)) {
searchResult.next = { snapOffset.offset, index };
break;
}
}
}
}
template <typename InfoType, typename SizeType, typename LayoutType, typename PointType>
static std::pair<LayoutType, std::optional<unsigned>> closestSnapOffsetWithInfoAndAxis(const InfoType& info, ScrollEventAxis axis, const SizeType& viewportSize, PointType scrollDestinationOffsetPoint, float velocity, std::optional<LayoutType> originalOffsetForDirectionalSnapping)
{
auto scrollDestinationOffset = axis == ScrollEventAxis::Horizontal ? scrollDestinationOffsetPoint.x() : scrollDestinationOffsetPoint.y();
const auto& snapOffsets = info.offsetsForAxis(axis);
auto pairForNoSnapping = std::make_pair(scrollDestinationOffset, std::nullopt);
if (snapOffsets.isEmpty())
return pairForNoSnapping;
auto viewportLength = axis == ScrollEventAxis::Horizontal ? viewportSize.width() : viewportSize.height();
auto searchResult = searchForPotentialSnapPoints(info, axis, viewportLength, scrollDestinationOffset, originalOffsetForDirectionalSnapping);
if (searchResult.snapStop)
return *(searchResult.snapStop);
adjustPreviousAndNextForOnScreenSnapAreas<InfoType, LayoutType, PointType, SizeType>(info, axis, viewportSize, scrollDestinationOffsetPoint, searchResult);
auto& previous = searchResult.previous;
auto& next = searchResult.next;
// From https://www.w3.org/TR/css-scroll-snap-1/#snap-overflow
// "If the snap area is larger than the snapport in a particular axis, then any scroll position
// in which the snap area covers the snapport, and the distance between the geometrically
// previous and subsequent snap positions in that axis is larger than size of the snapport in
// that axis, is a valid snap position in that axis. The UA may use the specified alignment as a
// more precise target for certain scroll operations (e.g. explicit paging)."
if (searchResult.landedInsideSnapAreaThatConsumesViewport && (!previous || !next || ((*next).first - (*previous).first) >= viewportLength))
return pairForNoSnapping;
auto isNearEnoughToOffsetForProximity = [&](LayoutType candidateSnapOffset) {
if (info.strictness != ScrollSnapStrictness::Proximity)
return true;
// This is an arbitrary choice for what it means to be "in proximity" of a snap offset. We should play around with
// this and see what feels best.
static const float ratioOfScrollPortAxisLengthToBeConsideredForProximity = 0.3;
return std::abs(float {candidateSnapOffset - scrollDestinationOffset}) <= (viewportLength * ratioOfScrollPortAxisLengthToBeConsideredForProximity);
};
if (scrollDestinationOffset <= snapOffsets.first().offset)
return isNearEnoughToOffsetForProximity(snapOffsets.first().offset) ? std::make_pair(snapOffsets.first().offset, std::make_optional(0u)) : pairForNoSnapping;
if (scrollDestinationOffset >= snapOffsets.last().offset) {
unsigned lastIndex = static_cast<unsigned>(snapOffsets.size() - 1);
return isNearEnoughToOffsetForProximity(snapOffsets.last().offset) ? std::make_pair(snapOffsets.last().offset, std::make_optional(lastIndex)) : pairForNoSnapping;
}
if (previous && !isNearEnoughToOffsetForProximity((*previous).first))
previous.reset();
if (next && !isNearEnoughToOffsetForProximity((*next).first))
next.reset();
if (originalOffsetForDirectionalSnapping) {
// From https://www.w3.org/TR/css-scroll-snap-1/#choosing
// "User agents must ensure that a user can “escape” a snap position, regardless of the scroll
// method. For example, if the snap type is mandatory and the next snap position is more than
// two screen-widths away, a naïve “always snap to nearest” selection algorithm might “trap” the
//
// For a directional scroll, we never snap back to the original scroll position or before it,
// always preferring the snap offset in the scroll direction.
auto& originalOffset = *originalOffsetForDirectionalSnapping;
if (originalOffset < scrollDestinationOffset && previous && (*previous).first <= originalOffset)
previous.reset();
if (originalOffset > scrollDestinationOffset && next && (*next).first >= originalOffset)
next.reset();
}
if (!previous && !next)
return pairForNoSnapping;
if (!previous)
return *next;
if (!next)
return *previous;
// If this scroll isn't directional, then choose whatever snap point is closer, otherwise pick the offset in the scroll direction.
if (!std::abs(velocity))
return (scrollDestinationOffset - (*previous).first) <= ((*next).first - scrollDestinationOffset) ? *previous : *next;
return velocity < 0 ? *previous : *next;
}
enum class InsetOrOutset {
Inset,
Outset
};
static LayoutRect computeScrollSnapPortOrAreaRect(const LayoutRect& rect, const LengthBox& insetOrOutsetBox, InsetOrOutset insetOrOutset)
{
// We are using minimumValueForLength here for insetOrOutset box, because if this value is defined by scroll-padding then the
// Length of any side may be "auto." In that case, we want to use 0, because that is how WebKit currently interprets an "auto"
// value for scroll-padding. See: https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-padding
LayoutBoxExtent extents(
minimumValueForLength(insetOrOutsetBox.top(), rect.height()), minimumValueForLength(insetOrOutsetBox.right(), rect.width()),
minimumValueForLength(insetOrOutsetBox.bottom(), rect.height()), minimumValueForLength(insetOrOutsetBox.left(), rect.width()));
auto snapPortOrArea(rect);
if (insetOrOutset == InsetOrOutset::Inset)
snapPortOrArea.contract(extents);
else
snapPortOrArea.expand(extents);
return snapPortOrArea;
}
static LayoutUnit computeScrollSnapAlignOffset(LayoutUnit minLocation, LayoutUnit maxLocation, ScrollSnapAxisAlignType alignment, bool axisIsFlipped)
{
switch (alignment) {
case ScrollSnapAxisAlignType::Start:
return axisIsFlipped ? maxLocation : minLocation;
case ScrollSnapAxisAlignType::Center:
return (minLocation + maxLocation) / 2;
case ScrollSnapAxisAlignType::End:
return axisIsFlipped ? minLocation : maxLocation;
default:
ASSERT_NOT_REACHED();
return 0;
}
}
static std::pair<bool, bool> axesFlippedForWritingModeAndDirection(WritingMode writingMode, TextDirection textDirection)
{
// text-direction flips the inline axis and writing-mode can flip the block axis. Whether or
// not the writing-mode is vertical determines the physical orientation of the block and inline axes.
bool hasVerticalWritingMode = isVerticalWritingMode(writingMode);
bool blockAxisFlipped = isFlippedWritingMode(writingMode);
bool inlineAxisFlipped = textDirection == TextDirection::RTL;
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)
{
auto scrollSnapType = scrollingElementStyle.scrollSnapType();
const auto& boxesWithScrollSnapPositions = scrollingElementBox.view().boxesWithScrollSnapPositions();
if (scrollSnapType.strictness == ScrollSnapStrictness::None || boxesWithScrollSnapPositions.isEmpty()) {
scrollableArea.clearSnapOffsets();
return;
}
auto addOrUpdateStopForSnapOffset = [](HashMap<LayoutUnit, SnapOffset<LayoutUnit>>& offsets, LayoutUnit newOffset, ScrollSnapStop stop, bool hasSnapAreaLargerThanViewport, size_t snapAreaIndices)
{
if (!offsets.isValidKey(newOffset))
return;
auto offset = offsets.ensure(newOffset, [&] {
return SnapOffset<LayoutUnit> { newOffset, stop, hasSnapAreaLargerThanViewport, { } };
});
// If the offset already exists, we ensure that it has ScrollSnapStop::Always, when appropriate.
if (stop == ScrollSnapStop::Always)
offset.iterator->value.stop = ScrollSnapStop::Always;
offset.iterator->value.hasSnapAreaLargerThanViewport |= hasSnapAreaLargerThanViewport;
offset.iterator->value.snapAreaIndices.append(snapAreaIndices);
};
HashMap<LayoutUnit, SnapOffset<LayoutUnit>> verticalSnapOffsetsMap;
HashMap<LayoutUnit, SnapOffset<LayoutUnit>> horizontalSnapOffsetsMap;
Vector<LayoutRect> snapAreas;
auto maxScrollOffset = scrollableArea.maximumScrollOffset();
maxScrollOffset.clampNegativeToZero();
auto scrollPosition = LayoutPoint { scrollableArea.scrollPosition() };
auto [scrollerXAxisFlipped, scrollerYAxisFlipped] = axesFlippedForWritingModeAndDirection(writingMode, textDirection);
bool scrollerHasVerticalWritingMode = isVerticalWritingMode(writingMode);
bool hasHorizontalSnapOffsets = scrollSnapType.axis == ScrollSnapAxis::Both || scrollSnapType.axis == ScrollSnapAxis::XAxis;
bool hasVerticalSnapOffsets = scrollSnapType.axis == ScrollSnapAxis::Both || scrollSnapType.axis == ScrollSnapAxis::YAxis;
if (scrollSnapType.axis == ScrollSnapAxis::Block) {
hasHorizontalSnapOffsets = scrollerHasVerticalWritingMode;
hasVerticalSnapOffsets = !scrollerHasVerticalWritingMode;
}
if (scrollSnapType.axis == ScrollSnapAxis::Inline) {
hasHorizontalSnapOffsets = !scrollerHasVerticalWritingMode;
hasVerticalSnapOffsets = scrollerHasVerticalWritingMode;
}
// The bounds of the scrolling container's snap port, where the top left of the scrolling container's border box is the origin.
auto scrollSnapPort = computeScrollSnapPortOrAreaRect(viewportRectInBorderBoxCoordinates, scrollingElementStyle.scrollPadding(), InsetOrOutset::Inset);
LOG_WITH_STREAM(ScrollSnap, stream << "Computing scroll snap offsets for " << scrollableArea << " in snap port " << scrollSnapPort);
for (auto* child : boxesWithScrollSnapPositions) {
if (child->enclosingScrollableContainerForSnapping() != &scrollingElementBox)
continue;
// The bounds of the child element's snap area, where the top left of the scrolling container's border box is the origin.
// The snap area is the bounding box of the child element's border box, after applying transformations.
OptionSet<MapCoordinatesMode> options = { UseTransforms, IgnoreStickyOffsets };
auto scrollSnapArea = LayoutRect(child->localToContainerQuad(FloatQuad(child->borderBoundingBox()), &scrollingElementBox, options).boundingBox());
// localToContainerQuad will transform the scroll snap area by the scroll position, except in the case that this position is
// coming from a ScrollView. We want the transformed area, but without scroll position taken into account.
if (!scrollableArea.isScrollView())
scrollSnapArea.moveBy(scrollPosition);
scrollSnapArea = computeScrollSnapPortOrAreaRect(scrollSnapArea, child->style().scrollMargin(), InsetOrOutset::Outset);
LOG_WITH_STREAM(ScrollSnap, stream << " Considering scroll snap target area " << scrollSnapArea);
auto alignment = child->style().scrollSnapAlign();
auto stop = child->style().scrollSnapStop();
// From https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-align:
// "Start and end alignments are resolved with respect to the writing mode of the snap container unless the
// scroll snap area is larger than the snapport, in which case they are resolved with respect to the writing
// mode of the box itself."
bool areaXAxisFlipped = scrollerXAxisFlipped;
bool areaYAxisFlipped = scrollerYAxisFlipped;
bool areaHasVerticalWritingMode = isVerticalWritingMode(child->style().writingMode());
if ((areaHasVerticalWritingMode && scrollSnapArea.height() > scrollSnapPort.height()) || (!areaHasVerticalWritingMode && scrollSnapArea.width() > scrollSnapPort.width()))
std::tie(areaXAxisFlipped, areaYAxisFlipped) = axesFlippedForWritingModeAndDirection(child->style().writingMode(), child->style().direction());
ScrollSnapAxisAlignType xAlign = scrollerHasVerticalWritingMode ? alignment.blockAlign : alignment.inlineAlign;
ScrollSnapAxisAlignType yAlign = scrollerHasVerticalWritingMode ? alignment.inlineAlign : alignment.blockAlign;
bool snapsHorizontally = hasHorizontalSnapOffsets && xAlign != ScrollSnapAxisAlignType::None;
bool snapsVertically = hasVerticalSnapOffsets && yAlign != ScrollSnapAxisAlignType::None;
if (!snapsHorizontally && !snapsVertically)
continue;
// The scroll snap area is defined via its scroll position, so convert the snap area rectangle to be relative to scroll offsets.
auto snapAreaOriginRelativeToBorderEdge = scrollSnapArea.location() - scrollSnapPort.location();
LayoutRect scrollSnapAreaAsOffsets(scrollableArea.scrollOffsetFromPosition(roundedIntPoint(snapAreaOriginRelativeToBorderEdge)), scrollSnapArea.size());
snapAreas.append(scrollSnapAreaAsOffsets);
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);
}
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);
}
if (!snapAreas.isEmpty())
LOG_WITH_STREAM(ScrollSnap, stream << " => Computed snap areas: " << snapAreas);
}
auto compareSnapOffsets = [](const SnapOffset<LayoutUnit>& a, const SnapOffset<LayoutUnit>& b)
{
return a.offset < b.offset;
};
Vector<SnapOffset<LayoutUnit>> horizontalSnapOffsets = copyToVector(horizontalSnapOffsetsMap.values());
if (!horizontalSnapOffsets.isEmpty()) {
std::sort(horizontalSnapOffsets.begin(), horizontalSnapOffsets.end(), compareSnapOffsets);
LOG_WITH_STREAM(ScrollSnap, stream << " => Computed horizontal scroll snap offsets: " << horizontalSnapOffsets);
}
Vector<SnapOffset<LayoutUnit>> verticalSnapOffsets = copyToVector(verticalSnapOffsetsMap.values());
if (!verticalSnapOffsets.isEmpty()) {
std::sort(verticalSnapOffsets.begin(), verticalSnapOffsets.end(), compareSnapOffsets);
LOG_WITH_STREAM(ScrollSnap, stream << " => Computed vertical scroll snap offsets: " << verticalSnapOffsets);
}
scrollableArea.setScrollSnapOffsetInfo({
scrollSnapType.strictness,
horizontalSnapOffsets,
verticalSnapOffsets,
snapAreas
});
}
static float convertOffsetUnit(LayoutUnit input, float deviceScaleFactor)
{
return roundToDevicePixel(input, deviceScaleFactor, false);
}
static LayoutUnit convertOffsetUnit(float input, float /* scaleFactor */)
{
return LayoutUnit(input);
}
template <typename InputType, typename InputRectType, typename OutputType, typename OutputRectType>
static ScrollSnapOffsetsInfo<OutputType, OutputRectType> convertOffsetInfo(const ScrollSnapOffsetsInfo<InputType, InputRectType>& input, float scaleFactor = 0.0)
{
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 };
});
};
auto convertRects = [scaleFactor](const Vector<InputRectType>& input)
{
return input.map([scaleFactor](auto& rect) -> OutputRectType {
return {
convertOffsetUnit(rect.x(), scaleFactor), convertOffsetUnit(rect.y(), scaleFactor),
convertOffsetUnit(rect.width(), scaleFactor), convertOffsetUnit(rect.height(), scaleFactor)
};
});
};
return {
input.strictness,
convertOffsets(input.horizontalSnapOffsets),
convertOffsets(input.verticalSnapOffsets),
convertRects(input.snapAreas),
};
}
template <> template <>
LayoutScrollSnapOffsetsInfo FloatScrollSnapOffsetsInfo::convertUnits(float /* unusedScaleFactor */) const
{
return convertOffsetInfo<float, FloatRect, LayoutUnit, LayoutRect>(*this);
}
template <> template <>
FloatScrollSnapOffsetsInfo LayoutScrollSnapOffsetsInfo::convertUnits(float deviceScaleFactor) const
{
return convertOffsetInfo<LayoutUnit, LayoutRect, float, FloatRect>(*this, deviceScaleFactor);
}
template <> template <>
std::pair<LayoutUnit, std::optional<unsigned>> LayoutScrollSnapOffsetsInfo::closestSnapOffset(ScrollEventAxis axis, const LayoutSize& viewportSize, LayoutPoint scrollDestinationOffset, float velocity, std::optional<LayoutUnit> originalPositionForDirectionalSnapping) const
{
return closestSnapOffsetWithInfoAndAxis(*this, axis, viewportSize, scrollDestinationOffset, velocity, originalPositionForDirectionalSnapping);
}
template <> template<>
std::pair<float, std::optional<unsigned>> FloatScrollSnapOffsetsInfo::closestSnapOffset(ScrollEventAxis axis, const FloatSize& viewportSize, FloatPoint scrollDestinationOffset, float velocity, std::optional<float> originalPositionForDirectionalSnapping) const
{
return closestSnapOffsetWithInfoAndAxis(*this, axis, viewportSize, scrollDestinationOffset, velocity, originalPositionForDirectionalSnapping);
}
}