| /* |
| * 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); |
| } |
| |
| } |