summaryrefslogtreecommitdiffstats
path: root/layout/generic/ScrollSnap.cpp
blob: 218d8755758c999f610b38da64f74c190f89804c (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
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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 "ScrollSnap.h"

#include "FrameMetrics.h"

#include "mozilla/Maybe.h"
#include "mozilla/Preferences.h"
#include "mozilla/StaticPrefs_layout.h"
#include "nsLineLayout.h"

namespace mozilla {

using layers::ScrollSnapInfo;

/**
 * Keeps track of the current best edge to snap to. The criteria for
 * adding an edge depends on the scrolling unit.
 */
class CalcSnapPoints final {
 public:
  CalcSnapPoints(ScrollUnit aUnit, const nsPoint& aDestination,
                 const nsPoint& aStartPos);
  void AddHorizontalEdge(nscoord aEdge);
  void AddVerticalEdge(nscoord aEdge);
  void AddEdge(nscoord aEdge, nscoord aDestination, nscoord aStartPos,
               nscoord aScrollingDirection, nscoord* aBestEdge,
               nscoord* aSecondBestEdge, bool* aEdgeFound);
  void AddEdgeInterval(nscoord aInterval, nscoord aMinPos, nscoord aMaxPos,
                       nscoord aOffset, nscoord aDestination, nscoord aStartPos,
                       nscoord aScrollingDirection, nscoord* aBestEdge,
                       nscoord* aSecondBestEdge, bool* aEdgeFound);
  nsPoint GetBestEdge() const;
  nscoord XDistanceBetweenBestAndSecondEdge() const {
    return std::abs(mBestEdge.x - mSecondBestEdge.x);
  }
  nscoord YDistanceBetweenBestAndSecondEdge() const {
    return std::abs(mBestEdge.y - mSecondBestEdge.y);
  }

 protected:
  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
  nsPoint mSecondBestEdge;    // keeps track of the position of the current
                              // second 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(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;
  mSecondBestEdge = nsPoint(nscoord_MAX, nscoord_MAX);
  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, &mSecondBestEdge.y, &mHorizontalEdgeFound);
}

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

void CalcSnapPoints::AddEdge(nscoord aEdge, nscoord aDestination,
                             nscoord aStartPos, nscoord aScrollingDirection,
                             nscoord* aBestEdge, nscoord* aSecondBestEdge,
                             bool* aEdgeFound) {
  // ScrollUnit::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 != ScrollUnit::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;
    }
    // ScrollUnit::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 != ScrollUnit::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;
  }

  // A utility function to update the best and the second best edges in the
  // given conditions.
  // |aIsCloserThanBest| True if the current candidate is closer than the best
  // edge.
  // |aIsCloserThanSecond| True if the current candidate is closer than
  // the second best edge.
  auto updateBestEdges = [&](bool aIsCloserThanBest, bool aIsCloserThanSecond) {
    if (aIsCloserThanBest) {
      *aSecondBestEdge = *aBestEdge;
      *aBestEdge = aEdge;
    } else if (aIsCloserThanSecond) {
      *aSecondBestEdge = aEdge;
    }
  };

  if (mUnit == ScrollUnit::DEVICE_PIXELS || mUnit == ScrollUnit::LINES) {
    nscoord distance = std::abs(aEdge - aDestination);
    updateBestEdges(distance < std::abs(*aBestEdge - aDestination),
                    distance < std::abs(*aSecondBestEdge - aDestination));
  } else if (mUnit == ScrollUnit::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;

    nscoord secondOvershoot =
        (*aSecondBestEdge - aDestination) * aScrollingDirection;

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

static void ProcessSnapPositions(CalcSnapPoints& aCalcSnapPoints,
                                 const ScrollSnapInfo& aSnapInfo) {
  for (auto position : aSnapInfo.mSnapPositionX) {
    aCalcSnapPoints.AddVerticalEdge(position);
  }
  for (auto position : aSnapInfo.mSnapPositionY) {
    aCalcSnapPoints.AddHorizontalEdge(position);
  }
}

Maybe<nsPoint> ScrollSnapUtils::GetSnapPointForDestination(
    const ScrollSnapInfo& aSnapInfo, ScrollUnit aUnit,
    const nsRect& aScrollRange, const nsPoint& aStartPos,
    const nsPoint& aDestination) {
  if (aSnapInfo.mScrollSnapStrictnessY == StyleScrollSnapStrictness::None &&
      aSnapInfo.mScrollSnapStrictnessX == StyleScrollSnapStrictness::None) {
    return Nothing();
  }

  if (!aSnapInfo.HasSnapPositions()) {
    return Nothing();
  }

  CalcSnapPoints calcSnapPoints(aUnit, aDestination, aStartPos);

  ProcessSnapPositions(calcSnapPoints, aSnapInfo);

  // If the distance between the first and the second candidate snap points
  // is larger than the snapport size and the snapport is covered by larger
  // elements, any points inside the covering area should be valid snap
  // points.
  // https://drafts.csswg.org/css-scroll-snap-1/#snap-overflow
  // NOTE: |aDestination| sometimes points outside of the scroll range, e.g.
  // by the APZC fling, so for the overflow checks we need to clamp it.
  nsPoint clampedDestination = aScrollRange.ClampPoint(aDestination);
  for (auto range : aSnapInfo.mXRangeWiderThanSnapport) {
    if (range.IsValid(clampedDestination.x, aSnapInfo.mSnapportSize.width) &&
        calcSnapPoints.XDistanceBetweenBestAndSecondEdge() >
            aSnapInfo.mSnapportSize.width) {
      calcSnapPoints.AddVerticalEdge(clampedDestination.x);
      break;
    }
  }
  for (auto range : aSnapInfo.mYRangeWiderThanSnapport) {
    if (range.IsValid(clampedDestination.y, aSnapInfo.mSnapportSize.height) &&
        calcSnapPoints.YDistanceBetweenBestAndSecondEdge() >
            aSnapInfo.mSnapportSize.height) {
      calcSnapPoints.AddHorizontalEdge(clampedDestination.y);
      break;
    }
  }

  bool snapped = false;
  nsPoint finalPos = calcSnapPoints.GetBestEdge();
  nscoord proximityThreshold =
      StaticPrefs::layout_css_scroll_snap_proximity_threshold();
  proximityThreshold = nsPresContext::CSSPixelsToAppUnits(proximityThreshold);
  if (aSnapInfo.mScrollSnapStrictnessY ==
          StyleScrollSnapStrictness::Proximity &&
      std::abs(aDestination.y - finalPos.y) > proximityThreshold) {
    finalPos.y = aDestination.y;
  } else {
    snapped = true;
  }
  if (aSnapInfo.mScrollSnapStrictnessX ==
          StyleScrollSnapStrictness::Proximity &&
      std::abs(aDestination.x - finalPos.x) > proximityThreshold) {
    finalPos.x = aDestination.x;
  } else {
    snapped = true;
  }
  return snapped ? Some(finalPos) : Nothing();
}

}  // namespace mozilla