summaryrefslogtreecommitdiffstats
path: root/layout/generic/ScrollSnap.cpp
blob: 96eb729259082ee78e1fa822735221b59943c4f2 (plain)
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
305
306
307
308
309
310
311
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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 "FrameMetrics.h"
#include "ScrollSnap.h"
#include "gfxPrefs.h"
#include "mozilla/Maybe.h"
#include "mozilla/Preferences.h"
#include "nsLineLayout.h"

namespace mozilla {

using layers::ScrollSnapInfo;

/**
 * Stores candidate snapping edges.
 */
class SnappingEdgeCallback {
public:
  virtual void AddHorizontalEdge(nscoord aEdge) = 0;
  virtual void AddVerticalEdge(nscoord aEdge) = 0;
  virtual void AddHorizontalEdgeInterval(const nsRect &aScrollRange,
                                         nscoord aInterval,
                                         nscoord aOffset) = 0;
  virtual void AddVerticalEdgeInterval(const nsRect &aScrollRange,
                                       nscoord aInterval,
                                       nscoord aOffset) = 0;
};

/**
 * Keeps track of the current best edge to snap to. The criteria for
 * adding an edge depends on the scrolling unit.
 */
class CalcSnapPoints : public SnappingEdgeCallback {
public:
  CalcSnapPoints(nsIScrollableFrame::ScrollUnit aUnit,
                 const nsPoint& aDestination,
                 const nsPoint& aStartPos);
  virtual void AddHorizontalEdge(nscoord aEdge) override;
  virtual void AddVerticalEdge(nscoord aEdge) override;
  virtual void AddHorizontalEdgeInterval(const nsRect &aScrollRange,
                                         nscoord aInterval, nscoord aOffset)
                                         override;
  virtual void AddVerticalEdgeInterval(const nsRect &aScrollRange,
                                       nscoord aInterval, nscoord aOffset)
                                       override;
  void AddEdge(nscoord aEdge,
               nscoord aDestination,
               nscoord aStartPos,
               nscoord aScrollingDirection,
               nscoord* aBestEdge,
               bool* aEdgeFound);
  void AddEdgeInterval(nscoord aInterval,
                       nscoord aMinPos,
                       nscoord aMaxPos,
                       nscoord aOffset,
                       nscoord aDestination,
                       nscoord aStartPos,
                       nscoord aScrollingDirection,
                       nscoord* aBestEdge,
                       bool* aEdgeFound);
  nsPoint GetBestEdge() const;
protected:
  nsIScrollableFrame::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
  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(nsIScrollableFrame::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;
  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,
          &mHorizontalEdgeFound);
}

void
CalcSnapPoints::AddVerticalEdge(nscoord aEdge)
{
  AddEdge(aEdge, mDestination.x, mStartPos.x, mScrollingDirection.x, &mBestEdge.x,
          &mVerticalEdgeFound);
}

void
CalcSnapPoints::AddHorizontalEdgeInterval(const nsRect &aScrollRange,
                                          nscoord aInterval, nscoord aOffset)
{
  AddEdgeInterval(aInterval, aScrollRange.y, aScrollRange.YMost(), aOffset,
                  mDestination.y, mStartPos.y, mScrollingDirection.y,
                  &mBestEdge.y, &mHorizontalEdgeFound);
}

void
CalcSnapPoints::AddVerticalEdgeInterval(const nsRect &aScrollRange,
                                        nscoord aInterval, nscoord aOffset)
{
  AddEdgeInterval(aInterval, aScrollRange.x, aScrollRange.XMost(), aOffset,
                  mDestination.x, mStartPos.x, mScrollingDirection.x,
                  &mBestEdge.x, &mVerticalEdgeFound);
}

void
CalcSnapPoints::AddEdge(nscoord aEdge, nscoord aDestination, nscoord aStartPos,
                        nscoord aScrollingDirection, nscoord* aBestEdge,
                        bool *aEdgeFound)
{
  // nsIScrollableFrame::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 != nsIScrollableFrame::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;
    }
    // nsIScrollableFrame::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 != nsIScrollableFrame::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;
  }
  if (mUnit == nsIScrollableFrame::DEVICE_PIXELS ||
      mUnit == nsIScrollableFrame::LINES) {
    if (std::abs(aEdge - aDestination) < std::abs(*aBestEdge - aDestination)) {
      *aBestEdge = aEdge;
    }
  } else if (mUnit == nsIScrollableFrame::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;

    // edges between the current position and the scrolling destination are favoured
    // to preserve context
    if (overshoot < 0 && (overshoot > curOvershoot || curOvershoot >= 0)) {
      *aBestEdge = aEdge;
    }
    // if there are no edges between the current position and the scrolling destination
    // the closest edge beyond the destination is used
    if (overshoot > 0 && overshoot < curOvershoot) {
      *aBestEdge = aEdge;
    }
  } else if (mUnit == nsIScrollableFrame::WHOLE) {
    // the edge closest to the top/bottom/left/right is used, depending on scrolling direction
    if (aScrollingDirection > 0 && aEdge > *aBestEdge) {
      *aBestEdge = aEdge;
    } else if (aScrollingDirection < 0 && aEdge < *aBestEdge) {
      *aBestEdge = aEdge;
    }
  } 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, 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,
            aEdgeFound);
  }
  edge += aInterval;
  if (edge >= aMinPos && edge <= aMaxPos) {
    AddEdge(edge, aDestination, aStartPos, aScrollingDirection, aBestEdge,
            aEdgeFound);
  }
}

static void
ProcessScrollSnapCoordinates(SnappingEdgeCallback& aCallback,
                             const nsTArray<nsPoint>& aScrollSnapCoordinates,
                             const nsPoint& aScrollSnapDestination) {
  for (nsPoint snapCoords : aScrollSnapCoordinates) {
    // Make them relative to the scroll snap destination.
    snapCoords -= aScrollSnapDestination;

    aCallback.AddVerticalEdge(snapCoords.x);
    aCallback.AddHorizontalEdge(snapCoords.y);
  }
}

Maybe<nsPoint> ScrollSnapUtils::GetSnapPointForDestination(
    const ScrollSnapInfo& aSnapInfo,
    nsIScrollableFrame::ScrollUnit aUnit,
    const nsSize& aScrollPortSize,
    const nsRect& aScrollRange,
    const nsPoint& aStartPos,
    const nsPoint& aDestination)
{
  if (aSnapInfo.mScrollSnapTypeY == NS_STYLE_SCROLL_SNAP_TYPE_NONE &&
      aSnapInfo.mScrollSnapTypeX == NS_STYLE_SCROLL_SNAP_TYPE_NONE) {
    return Nothing();
  }

  nsPoint destPos = aSnapInfo.mScrollSnapDestination;

  CalcSnapPoints calcSnapPoints(aUnit, aDestination, aStartPos);

  if (aSnapInfo.mScrollSnapIntervalX.isSome()) {
    nscoord interval = aSnapInfo.mScrollSnapIntervalX.value();
    calcSnapPoints.AddVerticalEdgeInterval(aScrollRange, interval, destPos.x);
  }
  if (aSnapInfo.mScrollSnapIntervalY.isSome()) {
    nscoord interval = aSnapInfo.mScrollSnapIntervalY.value();
    calcSnapPoints.AddHorizontalEdgeInterval(aScrollRange, interval, destPos.y);
  }

  ProcessScrollSnapCoordinates(calcSnapPoints, aSnapInfo.mScrollSnapCoordinates, destPos);
  bool snapped = false;
  nsPoint finalPos = calcSnapPoints.GetBestEdge();
  nscoord proximityThreshold = gfxPrefs::ScrollSnapProximityThreshold();
  proximityThreshold = nsPresContext::CSSPixelsToAppUnits(proximityThreshold);
  if (aSnapInfo.mScrollSnapTypeY == NS_STYLE_SCROLL_SNAP_TYPE_PROXIMITY &&
      std::abs(aDestination.y - finalPos.y) > proximityThreshold) {
    finalPos.y = aDestination.y;
  } else {
    snapped = true;
  }
  if (aSnapInfo.mScrollSnapTypeX == NS_STYLE_SCROLL_SNAP_TYPE_PROXIMITY &&
      std::abs(aDestination.x - finalPos.x) > proximityThreshold) {
    finalPos.x = aDestination.x;
  } else {
    snapped = true;
  }
  return snapped ? Some(finalPos) : Nothing();
}

}  // namespace mozilla