ScrollSnapOffsetsInfo.cpp [plain text]
#include "config.h"
#include "ScrollSnapOffsetsInfo.h"
#if ENABLE(CSS_SCROLL_SNAP)
#include "ElementChildIterator.h"
#include "LayoutRect.h"
#include "Length.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 LayoutType>
static void indicesOfNearestSnapOffsetRanges(LayoutType offset, const Vector<ScrollOffsetRange<LayoutType>>& snapOffsetRanges, unsigned& lowerIndex, unsigned& upperIndex)
{
if (snapOffsetRanges.isEmpty()) {
lowerIndex = invalidSnapOffsetIndex;
upperIndex = invalidSnapOffsetIndex;
return;
}
int lowerIndexAsInt = -1;
int upperIndexAsInt = snapOffsetRanges.size();
do {
int middleIndex = (lowerIndexAsInt + upperIndexAsInt) / 2;
auto& range = snapOffsetRanges[middleIndex];
if (range.start < offset && offset < range.end) {
lowerIndexAsInt = middleIndex;
upperIndexAsInt = middleIndex;
break;
}
if (offset > range.end)
lowerIndexAsInt = middleIndex;
else
upperIndexAsInt = middleIndex;
} while (lowerIndexAsInt < upperIndexAsInt - 1);
if (offset <= snapOffsetRanges.first().start)
lowerIndex = invalidSnapOffsetIndex;
else
lowerIndex = lowerIndexAsInt;
if (offset >= snapOffsetRanges.last().end)
upperIndex = invalidSnapOffsetIndex;
else
upperIndex = upperIndexAsInt;
}
template <typename LayoutType>
static void indicesOfNearestSnapOffsets(LayoutType offset, const Vector<LayoutType>& snapOffsets, unsigned& lowerIndex, unsigned& upperIndex)
{
lowerIndex = 0;
upperIndex = snapOffsets.size() - 1;
while (lowerIndex < upperIndex - 1) {
int middleIndex = (lowerIndex + upperIndex) / 2;
auto middleOffset = snapOffsets[middleIndex];
if (offset == middleOffset) {
upperIndex = middleIndex;
lowerIndex = middleIndex;
break;
}
if (offset > middleOffset)
lowerIndex = middleIndex;
else
upperIndex = middleIndex;
}
}
template <typename LayoutType>
static LayoutType closestSnapOffset(const Vector<LayoutType>& snapOffsets, const Vector<ScrollOffsetRange<LayoutType>>& snapOffsetRanges, LayoutType scrollDestination, float velocity, unsigned& activeSnapIndex, Optional<LayoutType> originalPositionForDirectionalSnapping)
{
ASSERT(snapOffsets.size());
activeSnapIndex = 0;
unsigned lowerSnapOffsetRangeIndex;
unsigned upperSnapOffsetRangeIndex;
indicesOfNearestSnapOffsetRanges<LayoutType>(scrollDestination, snapOffsetRanges, lowerSnapOffsetRangeIndex, upperSnapOffsetRangeIndex);
if (lowerSnapOffsetRangeIndex == upperSnapOffsetRangeIndex && upperSnapOffsetRangeIndex != invalidSnapOffsetIndex) {
activeSnapIndex = invalidSnapOffsetIndex;
return scrollDestination;
}
if (scrollDestination <= snapOffsets.first())
return snapOffsets.first();
activeSnapIndex = snapOffsets.size() - 1;
if (scrollDestination >= snapOffsets.last())
return snapOffsets.last();
unsigned lowerIndex;
unsigned upperIndex;
indicesOfNearestSnapOffsets<LayoutType>(scrollDestination, snapOffsets, lowerIndex, upperIndex);
LayoutType lowerSnapPosition = snapOffsets[lowerIndex];
LayoutType upperSnapPosition = snapOffsets[upperIndex];
if (!std::abs(velocity)) {
bool isCloserToLowerSnapPosition = scrollDestination - lowerSnapPosition <= upperSnapPosition - scrollDestination;
activeSnapIndex = isCloserToLowerSnapPosition ? lowerIndex : upperIndex;
return isCloserToLowerSnapPosition ? lowerSnapPosition : upperSnapPosition;
}
if (velocity < 0) {
if (lowerSnapOffsetRangeIndex == invalidSnapOffsetIndex || lowerSnapPosition >= snapOffsetRanges[lowerSnapOffsetRangeIndex].end) {
activeSnapIndex = lowerIndex;
return lowerSnapPosition;
}
if (!originalPositionForDirectionalSnapping.hasValue() || *originalPositionForDirectionalSnapping > upperSnapPosition) {
activeSnapIndex = upperIndex;
return upperSnapPosition;
}
} else {
if (upperSnapOffsetRangeIndex == invalidSnapOffsetIndex || snapOffsetRanges[upperSnapOffsetRangeIndex].start >= upperSnapPosition) {
activeSnapIndex = upperIndex;
return upperSnapPosition;
}
if (!originalPositionForDirectionalSnapping.hasValue() || *originalPositionForDirectionalSnapping < lowerSnapPosition) {
activeSnapIndex = lowerIndex;
return lowerSnapPosition;
}
}
activeSnapIndex = invalidSnapOffsetIndex;
return scrollDestination;
}
LayoutUnit closestSnapOffset(const Vector<LayoutUnit>& snapOffsets, const Vector<ScrollOffsetRange<LayoutUnit>>& snapOffsetRanges, LayoutUnit scrollDestination, float velocity, unsigned& activeSnapIndex, Optional<LayoutUnit> originalPositionForDirectionalSnapping)
{
return closestSnapOffset<LayoutUnit>(snapOffsets, snapOffsetRanges, scrollDestination, velocity, activeSnapIndex, originalPositionForDirectionalSnapping);
}
float closestSnapOffset(const Vector<float>& snapOffsets, const Vector<ScrollOffsetRange<float>>& snapOffsetRanges, float scrollDestination, float velocity, unsigned& activeSnapIndex, Optional<float> originalPositionForDirectionalSnapping)
{
return closestSnapOffset<float>(snapOffsets, snapOffsetRanges, scrollDestination, velocity, activeSnapIndex, originalPositionForDirectionalSnapping);
}
enum class InsetOrOutset {
Inset,
Outset
};
static LayoutRect computeScrollSnapPortOrAreaRect(const LayoutRect& rect, const LengthBox& insetOrOutsetBox, InsetOrOutset insetOrOutset)
{
LayoutBoxExtent extents(valueForLength(insetOrOutsetBox.top(), rect.height()), valueForLength(insetOrOutsetBox.right(), rect.width()), valueForLength(insetOrOutsetBox.bottom(), rect.height()), valueForLength(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;
}
}
template<typename T>
TextStream& operator<<(TextStream& ts, const ScrollOffsetRange<T>& range)
{
ts << "start: " << range.start << " end: " << range.end;
return ts;
}
static void computeAxisProximitySnapOffsetRanges(const Vector<LayoutUnit>& snapOffsets, Vector<ScrollOffsetRange<LayoutUnit>>& offsetRanges, LayoutUnit scrollPortAxisLength)
{
static const float ratioOfScrollPortAxisLengthToBeConsideredForProximity = 0.3;
if (snapOffsets.size() < 2)
return;
LayoutUnit proximityDistance { ratioOfScrollPortAxisLengthToBeConsideredForProximity * scrollPortAxisLength };
for (size_t index = 1; index < snapOffsets.size(); ++index) {
auto startOffset = snapOffsets[index - 1] + proximityDistance;
auto endOffset = snapOffsets[index] - proximityDistance;
if (startOffset < endOffset)
offsetRanges.append({ startOffset, endOffset });
}
}
void updateSnapOffsetsForScrollableArea(ScrollableArea& scrollableArea, const RenderBox& scrollingElementBox, const RenderStyle& scrollingElementStyle, LayoutRect viewportRectInBorderBoxCoordinates)
{
auto scrollSnapType = scrollingElementStyle.scrollSnapType();
const auto& boxesWithScrollSnapPositions = scrollingElementBox.view().boxesWithScrollSnapPositions();
if (scrollSnapType.strictness == ScrollSnapStrictness::None || boxesWithScrollSnapPositions.isEmpty()) {
scrollableArea.clearSnapOffsets();
return;
}
Vector<LayoutUnit> verticalSnapOffsets;
Vector<LayoutUnit> horizontalSnapOffsets;
Vector<ScrollOffsetRange<LayoutUnit>> verticalSnapOffsetRanges;
Vector<ScrollOffsetRange<LayoutUnit>> horizontalSnapOffsetRanges;
HashSet<float> seenVerticalSnapOffsets;
HashSet<float> seenHorizontalSnapOffsets;
bool hasHorizontalSnapOffsets = scrollSnapType.axis == ScrollSnapAxis::Both || scrollSnapType.axis == ScrollSnapAxis::XAxis || scrollSnapType.axis == ScrollSnapAxis::Inline;
bool hasVerticalSnapOffsets = scrollSnapType.axis == ScrollSnapAxis::Both || scrollSnapType.axis == ScrollSnapAxis::YAxis || scrollSnapType.axis == ScrollSnapAxis::Block;
auto maxScrollOffset = scrollableArea.maximumScrollOffset();
auto scrollPosition = LayoutPoint { scrollableArea.scrollPosition() };
bool scrollerIsRTL = !scrollingElementBox.style().isLeftToRightDirection();
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;
auto scrollSnapArea = LayoutRect(child->localToContainerQuad(FloatQuad(child->borderBoundingBox()), &scrollingElementBox).boundingBox());
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();
if (hasHorizontalSnapOffsets && alignment.x != ScrollSnapAxisAlignType::None) {
auto absoluteScrollXPosition = computeScrollSnapAlignOffset(scrollSnapArea.x(), scrollSnapArea.maxX(), alignment.x, scrollerIsRTL) - computeScrollSnapAlignOffset(scrollSnapPort.x(), scrollSnapPort.maxX(), alignment.x, scrollerIsRTL);
auto absoluteScrollOffset = clampTo<int>(scrollableArea.scrollOffsetFromPosition({ roundToInt(absoluteScrollXPosition), 0 }).x(), 0, maxScrollOffset.x());
if (!seenHorizontalSnapOffsets.contains(absoluteScrollOffset)) {
seenHorizontalSnapOffsets.add(absoluteScrollOffset);
horizontalSnapOffsets.append(absoluteScrollOffset);
}
}
if (hasVerticalSnapOffsets && alignment.y != ScrollSnapAxisAlignType::None) {
auto absoluteScrollYPosition = computeScrollSnapAlignOffset(scrollSnapArea.y(), scrollSnapArea.maxY(), alignment.y, false) - computeScrollSnapAlignOffset(scrollSnapPort.y(), scrollSnapPort.maxY(), alignment.y, false);
auto absoluteScrollOffset = clampTo<int>(scrollableArea.scrollOffsetFromPosition({ 0, roundToInt(absoluteScrollYPosition) }).y(), 0, maxScrollOffset.y());
if (!seenVerticalSnapOffsets.contains(absoluteScrollOffset)) {
seenVerticalSnapOffsets.add(absoluteScrollOffset);
verticalSnapOffsets.append(absoluteScrollOffset);
}
}
}
if (!horizontalSnapOffsets.isEmpty()) {
std::sort(horizontalSnapOffsets.begin(), horizontalSnapOffsets.end());
if (scrollSnapType.strictness == ScrollSnapStrictness::Proximity)
computeAxisProximitySnapOffsetRanges(horizontalSnapOffsets, horizontalSnapOffsetRanges, scrollSnapPort.width());
LOG_WITH_STREAM(ScrollSnap, stream << " => Computed horizontal scroll snap offsets: " << horizontalSnapOffsets);
LOG_WITH_STREAM(ScrollSnap, stream << " => Computed horizontal scroll snap offset ranges: " << horizontalSnapOffsetRanges);
scrollableArea.setHorizontalSnapOffsets(horizontalSnapOffsets);
scrollableArea.setHorizontalSnapOffsetRanges(horizontalSnapOffsetRanges);
} else
scrollableArea.clearHorizontalSnapOffsets();
if (!verticalSnapOffsets.isEmpty()) {
std::sort(verticalSnapOffsets.begin(), verticalSnapOffsets.end());
if (scrollSnapType.strictness == ScrollSnapStrictness::Proximity)
computeAxisProximitySnapOffsetRanges(verticalSnapOffsets, verticalSnapOffsetRanges, scrollSnapPort.height());
LOG_WITH_STREAM(ScrollSnap, stream << " => Computed vertical scroll snap offsets: " << verticalSnapOffsets);
LOG_WITH_STREAM(ScrollSnap, stream << " => Computed vertical scroll snap offset ranges: " << verticalSnapOffsetRanges);
scrollableArea.setVerticalSnapOffsets(verticalSnapOffsets);
scrollableArea.setVerticalSnapOffsetRanges(verticalSnapOffsetRanges);
} else
scrollableArea.clearVerticalSnapOffsets();
}
}
#endif // ENABLE(CSS_SCROLL_SNAP)