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