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 #ifndef mozilla_layout_ScrollAnchorContainer_h_
8 #define mozilla_layout_ScrollAnchorContainer_h_
9 
10 #include "nsPoint.h"
11 #include "mozilla/Saturate.h"
12 
13 class nsFrameList;
14 class nsIFrame;
15 class nsIScrollableFrame;
16 
17 namespace mozilla {
18 class ScrollFrameHelper;
19 }  // namespace mozilla
20 
21 namespace mozilla {
22 namespace layout {
23 
24 /**
25  * A scroll anchor container finds a descendent element of a scrollable frame
26  * to be an anchor node. After every reflow, the scroll anchor will apply
27  * scroll adjustments to keep the anchor node in the same relative position.
28  *
29  * See: https://drafts.csswg.org/css-scroll-anchoring/
30  */
31 class ScrollAnchorContainer final {
32  public:
33   explicit ScrollAnchorContainer(ScrollFrameHelper* aScrollFrame);
34   ~ScrollAnchorContainer();
35 
36   /**
37    * Returns the nearest scroll anchor container that could select aFrame as an
38    * anchor node.
39    */
40   static ScrollAnchorContainer* FindFor(nsIFrame* aFrame);
41 
42   /**
43    * Returns the frame that is the selected anchor node or null if no anchor
44    * is selected.
45    */
AnchorNode()46   nsIFrame* AnchorNode() const { return mAnchorNode; }
47 
48   /**
49    * Returns the frame that owns this scroll anchor container. This is always
50    * non-null.
51    */
52   nsIFrame* Frame() const;
53 
54   /**
55    * Returns the frame that owns this scroll anchor container as a scrollable
56    * frame. This is always non-null.
57    */
58   nsIScrollableFrame* ScrollableFrame() const;
59 
60   /**
61    * Find a suitable anchor node among the descendants of the scrollable frame.
62    * This should only be called after the scroll anchor has been invalidated.
63    */
64   void SelectAnchor();
65 
66   /**
67    * Whether this scroll frame can maintain an anchor node at the moment.
68    */
69   bool CanMaintainAnchor() const;
70 
71   /**
72    * Notify the scroll anchor container that its scroll frame has been
73    * scrolled by a user and should invalidate itself.
74    */
75   void UserScrolled();
76 
77   /**
78    * Notify the scroll anchor container that a reflow has happened and it
79    * should query its anchor to see if a scroll adjustment needs to occur.
80    */
81   void ApplyAdjustments();
82 
83   /**
84    * Notify the scroll anchor container that it should suppress any scroll
85    * adjustment that may happen after the next layout flush.
86    */
87   void SuppressAdjustments();
88 
89   /**
90    * Notify this scroll anchor container that its anchor node should be
91    * invalidated, and recomputed at the next available opportunity if
92    * ScheduleSelection is Yes.
93    */
94   enum class ScheduleSelection { No, Yes };
95   void InvalidateAnchor(ScheduleSelection = ScheduleSelection::Yes);
96 
97   /**
98    * Notify this scroll anchor container that it will be destroyed along with
99    * its parent frame.
100    */
101   void Destroy();
102 
103  private:
104   // Represents an assessment of a frame's suitability as a scroll anchor,
105   // from the scroll-anchoring spec's "candidate examination algorithm":
106   // https://drafts.csswg.org/css-scroll-anchoring-1/#candidate-examination
107   enum class ExamineResult {
108     // The frame is an excluded subtree or fully clipped and should be ignored.
109     // This corresponds with step 1 in the algorithm.
110     Exclude,
111     // This frame is an anonymous or inline box and its descendants should be
112     // searched to find an anchor node. If none are found, then continue
113     // searching. This is implied by the prologue of the algorithm, and
114     // should be made explicit in the spec [1].
115     //
116     // [1] https://github.com/w3c/csswg-drafts/issues/3489
117     PassThrough,
118     // The frame is partially visible and its descendants should be searched to
119     // find an anchor node. If none are found then this frame should be
120     // selected. This corresponds with step 3 in the algorithm.
121     Traverse,
122     // The frame is fully visible and should be selected as an anchor node. This
123     // corresponds with step 2 in the algorithm.
124     Accept,
125   };
126 
127   ExamineResult ExamineAnchorCandidate(nsIFrame* aPrimaryFrame) const;
128 
129   // Search a frame's children to find an anchor node. Returns the frame for a
130   // valid anchor node, if one was found in the frames descendants, or null
131   // otherwise.
132   nsIFrame* FindAnchorIn(nsIFrame* aFrame) const;
133 
134   // Search a child list to find an anchor node. Returns the frame for a valid
135   // anchor node, if one was found in this child list, or null otherwise.
136   nsIFrame* FindAnchorInList(const nsFrameList& aFrameList) const;
137 
138   // Notes that a given adjustment has happened, and maybe disables scroll
139   // anchoring on this scroller altogether based on various prefs.
140   void AdjustmentMade(nscoord aAdjustment);
141 
142   // The owner of this scroll anchor container
143   ScrollFrameHelper* mScrollFrame;
144 
145   // The anchor node that we will scroll to keep in the same relative position
146   // after reflows. This may be null if we were not able to select a valid
147   // scroll anchor
148   nsIFrame* mAnchorNode;
149 
150   // The last offset of the scroll anchor node's scrollable overflow rect start
151   // edge relative to the scroll-port start edge, in the block axis of the
152   // scroll frame. This is used for calculating the distance to scroll to keep
153   // the anchor node in the same relative position
154   nscoord mLastAnchorOffset;
155 
156   // The number of consecutive scroll anchoring adjustments that have happened
157   // without a user scroll.
158   SaturateUint32 mConsecutiveScrollAnchoringAdjustments{0};
159 
160   // The total length that has been adjusted by all the consecutive adjustments
161   // referenced above. Note that this is a sum, so that oscillating adjustments
162   // average towards zero.
163   nscoord mConsecutiveScrollAnchoringAdjustmentLength{0};
164 
165   // True if we've been disabled by the heuristic controlled by
166   // layout.css.scroll-anchoring.max-consecutive-adjustments and
167   // layout.css.scroll-anchoring.min-adjustment-threshold.
168   bool mDisabled : 1;
169 
170   // True if when we selected the current scroll anchor, there were unlaid out
171   // children that could be better anchor nodes after layout.
172   bool mAnchorMightBeSubOptimal : 1;
173   // True if we should recalculate our anchor node at the next chance
174   bool mAnchorNodeIsDirty : 1;
175   // True if we are applying a scroll anchor adjustment
176   bool mApplyingAnchorAdjustment : 1;
177   // True if we should suppress anchor adjustments
178   bool mSuppressAnchorAdjustment : 1;
179 };
180 
181 }  // namespace layout
182 }  // namespace mozilla
183 
184 #endif  // mozilla_layout_ScrollAnchorContainer_h_
185