271template <typename UnitType, typename RectType>
272static UnitType minForAxis(RectType rect, ScrollEventAxis axis)
273{
274 return axis == ScrollEventAxis::Horizontal ? rect.x() : rect.y();
275}
276
277template <typename UnitType>
278static bool isNearEnoughToOffsetForProximity(ScrollSnapStrictness strictness, UnitType scrollDestination, UnitType candidateSnapOffset, UnitType viewportLength)
279{
280 if (strictness != ScrollSnapStrictness::Proximity)
281 return true;
282
283 // This is an arbitrary choice for what it means to be "in proximity" of a snap offset. We should play around with
284 // this and see what feels best.
285 static const float ratioOfScrollPortAxisLengthToBeConsideredForProximity = 0.3;
286 return std::abs(float {candidateSnapOffset - scrollDestination}) <= (viewportLength * ratioOfScrollPortAxisLengthToBeConsideredForProximity);
287}
288
289template <typename UnitType, typename RectType, typename SizeType>
290static std::pair<UnitType, unsigned> calculateClosestSnapOffset(const ScrollSnapOffsetsInfo<UnitType, RectType>& info, ScrollEventAxis axis, const SizeType& viewportSize, UnitType scrollDestinationOffset, float velocity, Optional<UnitType> originalOffset)
291{
292 const auto& snapOffsets = info.offsetsForAxis(axis);
293 auto pairForNoSnapping = std::make_pair(scrollDestinationOffset, invalidSnapOffsetIndex);
294 if (snapOffsets.isEmpty())
295 return pairForNoSnapping;
296
297 // The first snap point with `scroll-snap-stop: always` between the original position and destination takes precedence.
298 if (originalOffset) {
299 if (auto firstSnapStopOffset = findFirstSnapStopOffsetBetweenOriginAndDestination(snapOffsets, *originalOffset, scrollDestinationOffset))
300 return *firstSnapStopOffset;
301 }
302
303 auto viewportLength = axis == ScrollEventAxis::Horizontal ? viewportSize.width() : viewportSize.height();
304 bool landedInsideSnapAreaThatConsumesViewport = false;
305 Optional<std::pair<UnitType, unsigned>> previous, next;
306 Optional<RectType> previousSnapArea, nextSnapArea;
307 for (unsigned i = 0; i < snapOffsets.size(); i++) {
308 const auto& snapArea = info.snapAreas[snapOffsets[i].snapAreaIndex];
309 landedInsideSnapAreaThatConsumesViewport |= (minForAxis<UnitType>(snapArea, axis) <= scrollDestinationOffset && maxForAxis<UnitType>(snapArea, axis) >= (scrollDestinationOffset + viewportLength));
310 // If the scroll landed exactly on a snap offset, choose that one.
311 UnitType potentialSnapOffset = snapOffsets[i].offset;
312 if (potentialSnapOffset == scrollDestinationOffset)
313 return std::make_pair(potentialSnapOffset, i);
314
315 if (potentialSnapOffset < scrollDestinationOffset) {
316 previous = std::make_pair(potentialSnapOffset, i);
317 previousSnapArea = snapArea;
318 } else if (!next && potentialSnapOffset > scrollDestinationOffset) {
319 next = std::make_pair(potentialSnapOffset, i);
320 nextSnapArea = snapArea;
321 }
322 }
323
324 // From https://www.w3.org/TR/css-scroll-snap-1/#snap-overflow
325 // "If the snap area is larger than the snapport in a particular axis, then any scroll position
326 // in which the snap area covers the snapport, and the distance between the geometrically
327 // previous and subsequent snap positions in that axis is larger than size of the snapport in
328 // that axis, is a valid snap position in that axis. The UA may use the specified alignment as a
329 // more precise target for certain scroll operations (e.g. explicit paging)."
330 if (landedInsideSnapAreaThatConsumesViewport && (!previous || !next || ((*next).first - (*previous).first) >= viewportLength))
331 return pairForNoSnapping;
332
333 if (originalOffset) {
334 // If this is a directional scroll and there are no scroll stops in a particular direction,
335 // don't scroll past the edge of the scroll area.
336 if (!next && previous)
337 return std::make_pair(std::max((*previous).first, maxForAxis<UnitType>(*previousSnapArea, axis) - viewportLength), invalidSnapOffsetIndex);
338 if (!previous && next)
339 return std::make_pair(minForAxis<UnitType>(*nextSnapArea, axis), invalidSnapOffsetIndex);
340
341 // From https://www.w3.org/TR/css-scroll-snap-1/#choosing
342 // "User agents must ensure that a user can “escape” a snap position, regardless of the scroll
343 // method. For example, if the snap type is mandatory and the next snap position is more than
344 // two screen-widths away, a naïve “always snap to nearest” selection algorithm might “trap” the
345 // user if their end position was only one screen-width away. Instead, a smarter algorithm that
346 // only returned to the starting snap position if the end-point was a fairly small distance from
347 // it, and otherwise ignored the starting snap position, would give better behavior."
348 // We only do this if we are in a situation where we are choosing between two compatible
349 // snap offsets.
350 if (*originalOffset < scrollDestinationOffset && (*previous).first <= *originalOffset)
351 previous.reset();
352 if (*originalOffset > scrollDestinationOffset && (*next).first >= *originalOffset)
353 next.reset();
354 }
355
356 if (next && !isNearEnoughToOffsetForProximity(info.strictness, scrollDestinationOffset, (*next).first, viewportLength))
357 next.reset();
358 if (previous && !isNearEnoughToOffsetForProximity(info.strictness, scrollDestinationOffset, (*previous).first, viewportLength))
359 previous.reset();
360
361 // If we don't have compatible offsets to choose between, just do the obvious thing
362 // and do no snapping or only select the single compatible offset.
363 if (!previous && !next)
364 return pairForNoSnapping;
365 if (!previous)
366 return *next;
367 if (!next)
368 return *previous;
369
370 // If this scroll isn't directional, then choose whatever snap point is closer.
371 UnitType previousSnapOffset = (*previous).first;
372 UnitType nextSnapOffset = (*next).first;
373 if (!std::abs(velocity)) {
374 bool isCloserToPreviousSnapIndex = scrollDestinationOffset - previousSnapOffset <= nextSnapOffset - scrollDestinationOffset;
375 return isCloserToPreviousSnapIndex ? *previous : *next;
376 }
377
378 // Finally, if we have a directional scroll prefer the snap point that is in the scroll direction.
379 return velocity < 0 ? *previous : *next;
380}
381
382
383template <> template <>
384std::pair<LayoutUnit, unsigned> LayoutScrollSnapOffsetsInfo::closestSnapOffset(ScrollEventAxis axis, const LayoutSize& viewportSize, LayoutUnit scrollDestinationOffset, float velocity, Optional<LayoutUnit> originalOffsetForDirectionalSnapping) const
385{
386 return calculateClosestSnapOffset<LayoutUnit, LayoutRect, LayoutSize>(*this, axis, viewportSize, scrollDestinationOffset, velocity, originalOffsetForDirectionalSnapping);
387}
388
389template <> template <>
390std::pair<float, unsigned> FloatScrollSnapOffsetsInfo::closestSnapOffset(ScrollEventAxis axis, const FloatSize& viewportSize, float scrollDestinationOffset, float velocity, Optional<float> originalOffsetForDirectionalSnapping) const