summaryrefslogtreecommitdiffstats
path: root/layout/generic/ScrollAnchorContainer.h
blob: e86a937da644e666cccb26e3c402a62dcf37e3db (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
/* -*- 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/. */

#ifndef mozilla_layout_ScrollAnchorContainer_h_
#define mozilla_layout_ScrollAnchorContainer_h_

#include "nsPoint.h"
#include "mozilla/Saturate.h"
#include "mozilla/TimeStamp.h"

class nsFrameList;
class nsHTMLScrollFrame;
class nsIFrame;
class nsIScrollableFrame;

namespace mozilla::layout {

/**
 * A scroll anchor container finds a descendent element of a scrollable frame
 * to be an anchor node. After every reflow, the scroll anchor will apply
 * scroll adjustments to keep the anchor node in the same relative position.
 *
 * See: https://drafts.csswg.org/css-scroll-anchoring/
 */
class ScrollAnchorContainer final {
 public:
  explicit ScrollAnchorContainer(nsHTMLScrollFrame* aScrollFrame);
  ~ScrollAnchorContainer();

  /**
   * Returns the nearest scroll anchor container that could select aFrame as an
   * anchor node.
   */
  static ScrollAnchorContainer* FindFor(nsIFrame* aFrame);

  /**
   * Returns the frame that is the selected anchor node or null if no anchor
   * is selected.
   */
  nsIFrame* AnchorNode() const { return mAnchorNode; }

  // The owner of this scroll anchor container.
  nsHTMLScrollFrame* Frame() const;

  /**
   * Returns the frame that owns this scroll anchor container as a scrollable
   * frame. This is always non-null.
   */
  nsIScrollableFrame* ScrollableFrame() const;

  /**
   * Find a suitable anchor node among the descendants of the scrollable frame.
   * This should only be called after the scroll anchor has been invalidated.
   */
  void SelectAnchor();

  /**
   * Whether this scroll frame can maintain an anchor node at the moment.
   */
  bool CanMaintainAnchor() const;

  /**
   * Notify the scroll anchor container that its scroll frame has been
   * scrolled by a user and should invalidate itself.
   */
  void UserScrolled();

  /**
   * Notify the scroll anchor container that a reflow has happened and it
   * should query its anchor to see if a scroll adjustment needs to occur.
   */
  void ApplyAdjustments();

  /**
   * Notify the scroll anchor container that it should suppress any scroll
   * adjustment that may happen after the next layout flush.
   */
  void SuppressAdjustments();

  /**
   * Notify this scroll anchor container that its anchor node should be
   * invalidated, and recomputed at the next available opportunity if
   * ScheduleSelection is Yes.
   */
  enum class ScheduleSelection { No, Yes };
  void InvalidateAnchor(ScheduleSelection = ScheduleSelection::Yes);

  /**
   * Notify this scroll anchor container that it will be destroyed along with
   * its parent frame.
   */
  void Destroy();

 private:
  // Represents an assessment of a frame's suitability as a scroll anchor,
  // from the scroll-anchoring spec's "candidate examination algorithm":
  // https://drafts.csswg.org/css-scroll-anchoring-1/#candidate-examination
  enum class ExamineResult {
    // The frame is an excluded subtree or fully clipped and should be ignored.
    // This corresponds with step 1 in the algorithm.
    Exclude,
    // This frame is an anonymous or inline box and its descendants should be
    // searched to find an anchor node. If none are found, then continue
    // searching. This is implied by the prologue of the algorithm, and
    // should be made explicit in the spec [1].
    //
    // [1] https://github.com/w3c/csswg-drafts/issues/3489
    PassThrough,
    // The frame is partially visible and its descendants should be searched to
    // find an anchor node. If none are found then this frame should be
    // selected. This corresponds with step 3 in the algorithm.
    Traverse,
    // The frame is fully visible and should be selected as an anchor node. This
    // corresponds with step 2 in the algorithm.
    Accept,
  };

  ExamineResult ExamineAnchorCandidate(nsIFrame* aPrimaryFrame) const;

  // Search a frame's children to find an anchor node. Returns the frame for a
  // valid anchor node, if one was found in the frames descendants, or null
  // otherwise.
  nsIFrame* FindAnchorIn(nsIFrame* aFrame) const;

  // Search a child list to find an anchor node. Returns the frame for a valid
  // anchor node, if one was found in this child list, or null otherwise.
  nsIFrame* FindAnchorInList(const nsFrameList& aFrameList) const;

  // Notes that a given adjustment has happened, and maybe disables scroll
  // anchoring on this scroller altogether based on various prefs.
  void AdjustmentMade(nscoord aAdjustment);

  // The anchor node that we will scroll to keep in the same relative position
  // after reflows. This may be null if we were not able to select a valid
  // scroll anchor
  nsIFrame* mAnchorNode = nullptr;

  // The last offset of the scroll anchor node's scrollable overflow rect start
  // edge relative to the scroll-port start edge, in the block axis of the
  // scroll frame. This is used for calculating the distance to scroll to keep
  // the anchor node in the same relative position
  nscoord mLastAnchorOffset = 0;

  struct DisablingHeuristic {
    // The number of consecutive scroll anchoring adjustments that have happened
    // without a user scroll.
    SaturateUint32 mConsecutiveScrollAnchoringAdjustments{0};

    // The total length that has been adjusted by all the consecutive
    // adjustments referenced above. Note that this is a sum, so that
    // oscillating adjustments average towards zero.
    nscoord mConsecutiveScrollAnchoringAdjustmentLength{0};

    // The time we started checking for adjustments.
    TimeStamp mTimeStamp;

    // Returns whether anchoring should get disabled.
    bool AdjustmentMade(const ScrollAnchorContainer&, nscoord aAdjustment);
    void Reset();
  } mHeuristic;

  // True if we've been disabled by the heuristic controlled by
  // layout.css.scroll-anchoring.max-consecutive-adjustments and
  // layout.css.scroll-anchoring.min-adjustment-threshold.
  bool mDisabled : 1;

  // True if when we selected the current scroll anchor, there were unlaid out
  // children that could be better anchor nodes after layout.
  bool mAnchorMightBeSubOptimal : 1;
  // True if we should recalculate our anchor node at the next chance
  bool mAnchorNodeIsDirty : 1;
  // True if we are applying a scroll anchor adjustment
  bool mApplyingAnchorAdjustment : 1;
  // True if we should suppress anchor adjustments
  bool mSuppressAnchorAdjustment : 1;
};

}  // namespace mozilla::layout

#endif  // mozilla_layout_ScrollAnchorContainer_h_