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