Coverage Report

Created: 2018-09-25 14:53

/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