1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304
|
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "ScrollSnap.h"
#include "FrameMetrics.h"
#include "mozilla/Maybe.h"
#include "mozilla/Preferences.h"
#include "mozilla/StaticPrefs_layout.h"
#include "nsLineLayout.h"
namespace mozilla {
using layers::ScrollSnapInfo;
/**
* Keeps track of the current best edge to snap to. The criteria for
* adding an edge depends on the scrolling unit.
*/
class CalcSnapPoints final {
public:
CalcSnapPoints(ScrollUnit aUnit, const nsPoint& aDestination,
const nsPoint& aStartPos);
void AddHorizontalEdge(nscoord aEdge);
void AddVerticalEdge(nscoord aEdge);
void AddEdge(nscoord aEdge, nscoord aDestination, nscoord aStartPos,
nscoord aScrollingDirection, nscoord* aBestEdge,
nscoord* aSecondBestEdge, bool* aEdgeFound);
void AddEdgeInterval(nscoord aInterval, nscoord aMinPos, nscoord aMaxPos,
nscoord aOffset, nscoord aDestination, nscoord aStartPos,
nscoord aScrollingDirection, nscoord* aBestEdge,
nscoord* aSecondBestEdge, bool* aEdgeFound);
nsPoint GetBestEdge() const;
nscoord XDistanceBetweenBestAndSecondEdge() const {
return std::abs(mBestEdge.x - mSecondBestEdge.x);
}
nscoord YDistanceBetweenBestAndSecondEdge() const {
return std::abs(mBestEdge.y - mSecondBestEdge.y);
}
protected:
ScrollUnit mUnit;
nsPoint mDestination; // gives the position after scrolling but before
// snapping
nsPoint mStartPos; // gives the position before scrolling
nsIntPoint mScrollingDirection; // always -1, 0, or 1
nsPoint mBestEdge; // keeps track of the position of the current best edge
nsPoint mSecondBestEdge; // keeps track of the position of the current
// second best edge
bool mHorizontalEdgeFound; // true if mBestEdge.x is storing a valid
// horizontal edge
bool mVerticalEdgeFound; // true if mBestEdge.y is storing a valid vertical
// edge
};
CalcSnapPoints::CalcSnapPoints(ScrollUnit aUnit, const nsPoint& aDestination,
const nsPoint& aStartPos) {
mUnit = aUnit;
mDestination = aDestination;
mStartPos = aStartPos;
nsPoint direction = aDestination - aStartPos;
mScrollingDirection = nsIntPoint(0, 0);
if (direction.x < 0) {
mScrollingDirection.x = -1;
}
if (direction.x > 0) {
mScrollingDirection.x = 1;
}
if (direction.y < 0) {
mScrollingDirection.y = -1;
}
if (direction.y > 0) {
mScrollingDirection.y = 1;
}
mBestEdge = aDestination;
mSecondBestEdge = nsPoint(nscoord_MAX, nscoord_MAX);
mHorizontalEdgeFound = false;
mVerticalEdgeFound = false;
}
nsPoint CalcSnapPoints::GetBestEdge() const {
return nsPoint(mVerticalEdgeFound ? mBestEdge.x : mStartPos.x,
mHorizontalEdgeFound ? mBestEdge.y : mStartPos.y);
}
void CalcSnapPoints::AddHorizontalEdge(nscoord aEdge) {
AddEdge(aEdge, mDestination.y, mStartPos.y, mScrollingDirection.y,
&mBestEdge.y, &mSecondBestEdge.y, &mHorizontalEdgeFound);
}
void CalcSnapPoints::AddVerticalEdge(nscoord aEdge) {
AddEdge(aEdge, mDestination.x, mStartPos.x, mScrollingDirection.x,
&mBestEdge.x, &mSecondBestEdge.x, &mVerticalEdgeFound);
}
void CalcSnapPoints::AddEdge(nscoord aEdge, nscoord aDestination,
nscoord aStartPos, nscoord aScrollingDirection,
nscoord* aBestEdge, nscoord* aSecondBestEdge,
bool* aEdgeFound) {
// ScrollUnit::DEVICE_PIXELS indicates that we are releasing a drag
// gesture or any other user input event that sets an absolute scroll
// position. In this case, scroll snapping is expected to travel in any
// direction. Otherwise, we will restrict the direction of the scroll
// snapping movement based on aScrollingDirection.
if (mUnit != ScrollUnit::DEVICE_PIXELS) {
// Unless DEVICE_PIXELS, we only want to snap to points ahead of the
// direction we are scrolling
if (aScrollingDirection == 0) {
// The scroll direction is neutral - will not hit a snap point.
return;
}
// ScrollUnit::WHOLE indicates that we are navigating to "home" or
// "end". In this case, we will always select the first or last snap point
// regardless of the direction of the scroll. Otherwise, we will select
// scroll snapping points only in the direction specified by
// aScrollingDirection.
if (mUnit != ScrollUnit::WHOLE) {
// Direction of the edge from the current position (before scrolling) in
// the direction of scrolling
nscoord direction = (aEdge - aStartPos) * aScrollingDirection;
if (direction <= 0) {
// The edge is not in the direction we are scrolling, skip it.
return;
}
}
}
if (!*aEdgeFound) {
*aBestEdge = aEdge;
*aEdgeFound = true;
return;
}
// A utility function to update the best and the second best edges in the
// given conditions.
// |aIsCloserThanBest| True if the current candidate is closer than the best
// edge.
// |aIsCloserThanSecond| True if the current candidate is closer than
// the second best edge.
auto updateBestEdges = [&](bool aIsCloserThanBest, bool aIsCloserThanSecond) {
if (aIsCloserThanBest) {
*aSecondBestEdge = *aBestEdge;
*aBestEdge = aEdge;
} else if (aIsCloserThanSecond) {
*aSecondBestEdge = aEdge;
}
};
if (mUnit == ScrollUnit::DEVICE_PIXELS || mUnit == ScrollUnit::LINES) {
nscoord distance = std::abs(aEdge - aDestination);
updateBestEdges(distance < std::abs(*aBestEdge - aDestination),
distance < std::abs(*aSecondBestEdge - aDestination));
} else if (mUnit == ScrollUnit::PAGES) {
// distance to the edge from the scrolling destination in the direction of
// scrolling
nscoord overshoot = (aEdge - aDestination) * aScrollingDirection;
// distance to the current best edge from the scrolling destination in the
// direction of scrolling
nscoord curOvershoot = (*aBestEdge - aDestination) * aScrollingDirection;
nscoord secondOvershoot =
(*aSecondBestEdge - aDestination) * aScrollingDirection;
// edges between the current position and the scrolling destination are
// favoured to preserve context
if (overshoot < 0) {
updateBestEdges(overshoot > curOvershoot || curOvershoot >= 0,
overshoot > secondOvershoot || secondOvershoot >= 0);
}
// if there are no edges between the current position and the scrolling
// destination the closest edge beyond the destination is used
if (overshoot > 0) {
updateBestEdges(overshoot < curOvershoot, overshoot < secondOvershoot);
}
} else if (mUnit == ScrollUnit::WHOLE) {
// the edge closest to the top/bottom/left/right is used, depending on
// scrolling direction
if (aScrollingDirection > 0) {
updateBestEdges(aEdge > *aBestEdge, aEdge > *aSecondBestEdge);
} else if (aScrollingDirection < 0) {
updateBestEdges(aEdge < *aBestEdge, aEdge < *aSecondBestEdge);
}
} else {
NS_ERROR("Invalid scroll mode");
return;
}
}
void CalcSnapPoints::AddEdgeInterval(
nscoord aInterval, nscoord aMinPos, nscoord aMaxPos, nscoord aOffset,
nscoord aDestination, nscoord aStartPos, nscoord aScrollingDirection,
nscoord* aBestEdge, nscoord* aSecondBestEdge, bool* aEdgeFound) {
if (aInterval == 0) {
// When interval is 0, there are no scroll snap points.
// Avoid division by zero and bail.
return;
}
// The only possible candidate interval snap points are the edges immediately
// surrounding aDestination.
// aDestination must be clamped to the scroll
// range in order to handle cases where the best matching snap point would
// result in scrolling out of bounds. This clamping must be prior to
// selecting the two interval edges.
nscoord clamped = std::max(std::min(aDestination, aMaxPos), aMinPos);
// Add each edge in the interval immediately before aTarget and after aTarget
// Do not add edges that are out of range.
nscoord r = (clamped + aOffset) % aInterval;
if (r < aMinPos) {
r += aInterval;
}
nscoord edge = clamped - r;
if (edge >= aMinPos && edge <= aMaxPos) {
AddEdge(edge, aDestination, aStartPos, aScrollingDirection, aBestEdge,
aSecondBestEdge, aEdgeFound);
}
edge += aInterval;
if (edge >= aMinPos && edge <= aMaxPos) {
AddEdge(edge, aDestination, aStartPos, aScrollingDirection, aBestEdge,
aSecondBestEdge, aEdgeFound);
}
}
static void ProcessSnapPositions(CalcSnapPoints& aCalcSnapPoints,
const ScrollSnapInfo& aSnapInfo) {
for (auto position : aSnapInfo.mSnapPositionX) {
aCalcSnapPoints.AddVerticalEdge(position);
}
for (auto position : aSnapInfo.mSnapPositionY) {
aCalcSnapPoints.AddHorizontalEdge(position);
}
}
Maybe<nsPoint> ScrollSnapUtils::GetSnapPointForDestination(
const ScrollSnapInfo& aSnapInfo, ScrollUnit aUnit,
const nsRect& aScrollRange, const nsPoint& aStartPos,
const nsPoint& aDestination) {
if (aSnapInfo.mScrollSnapStrictnessY == StyleScrollSnapStrictness::None &&
aSnapInfo.mScrollSnapStrictnessX == StyleScrollSnapStrictness::None) {
return Nothing();
}
if (!aSnapInfo.HasSnapPositions()) {
return Nothing();
}
CalcSnapPoints calcSnapPoints(aUnit, aDestination, aStartPos);
ProcessSnapPositions(calcSnapPoints, aSnapInfo);
// If the distance between the first and the second candidate snap points
// is larger than the snapport size and the snapport is covered by larger
// elements, any points inside the covering area should be valid snap
// points.
// https://drafts.csswg.org/css-scroll-snap-1/#snap-overflow
// NOTE: |aDestination| sometimes points outside of the scroll range, e.g.
// by the APZC fling, so for the overflow checks we need to clamp it.
nsPoint clampedDestination = aScrollRange.ClampPoint(aDestination);
for (auto range : aSnapInfo.mXRangeWiderThanSnapport) {
if (range.IsValid(clampedDestination.x, aSnapInfo.mSnapportSize.width) &&
calcSnapPoints.XDistanceBetweenBestAndSecondEdge() >
aSnapInfo.mSnapportSize.width) {
calcSnapPoints.AddVerticalEdge(clampedDestination.x);
break;
}
}
for (auto range : aSnapInfo.mYRangeWiderThanSnapport) {
if (range.IsValid(clampedDestination.y, aSnapInfo.mSnapportSize.height) &&
calcSnapPoints.YDistanceBetweenBestAndSecondEdge() >
aSnapInfo.mSnapportSize.height) {
calcSnapPoints.AddHorizontalEdge(clampedDestination.y);
break;
}
}
bool snapped = false;
nsPoint finalPos = calcSnapPoints.GetBestEdge();
nscoord proximityThreshold =
StaticPrefs::layout_css_scroll_snap_proximity_threshold();
proximityThreshold = nsPresContext::CSSPixelsToAppUnits(proximityThreshold);
if (aSnapInfo.mScrollSnapStrictnessY ==
StyleScrollSnapStrictness::Proximity &&
std::abs(aDestination.y - finalPos.y) > proximityThreshold) {
finalPos.y = aDestination.y;
} else {
snapped = true;
}
if (aSnapInfo.mScrollSnapStrictnessX ==
StyleScrollSnapStrictness::Proximity &&
std::abs(aDestination.x - finalPos.x) > proximityThreshold) {
finalPos.x = aDestination.x;
} else {
snapped = true;
}
return snapped ? Some(finalPos) : Nothing();
}
} // namespace mozilla
|