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