Coverage Report

Created: 2018-09-25 14:53

/src/mozilla-central/layout/generic/StickyScrollContainer.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
/**
8
 * compute sticky positioning, both during reflow and when the scrolling
9
 * container scrolls
10
 */
11
12
#include "StickyScrollContainer.h"
13
14
#include "mozilla/OverflowChangedTracker.h"
15
#include "nsIFrame.h"
16
#include "nsIScrollableFrame.h"
17
#include "nsLayoutUtils.h"
18
19
namespace mozilla {
20
21
NS_DECLARE_FRAME_PROPERTY_DELETABLE(StickyScrollContainerProperty,
22
                                    StickyScrollContainer)
23
24
StickyScrollContainer::StickyScrollContainer(nsIScrollableFrame* aScrollFrame)
25
  : mScrollFrame(aScrollFrame)
26
  , mScrollPosition()
27
0
{
28
0
  mScrollFrame->AddScrollPositionListener(this);
29
0
}
30
31
StickyScrollContainer::~StickyScrollContainer()
32
0
{
33
0
  mScrollFrame->RemoveScrollPositionListener(this);
34
0
}
35
36
// static
37
StickyScrollContainer*
38
StickyScrollContainer::GetStickyScrollContainerForFrame(nsIFrame* aFrame)
39
0
{
40
0
  nsIScrollableFrame* scrollFrame =
41
0
    nsLayoutUtils::GetNearestScrollableFrame(aFrame->GetParent(),
42
0
      nsLayoutUtils::SCROLLABLE_SAME_DOC |
43
0
      nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN);
44
0
  if (!scrollFrame) {
45
0
    // We might not find any, for instance in the case of
46
0
    // <html style="position: fixed">
47
0
    return nullptr;
48
0
  }
49
0
  nsIFrame* frame = do_QueryFrame(scrollFrame);
50
0
  StickyScrollContainer* s =
51
0
    frame->GetProperty(StickyScrollContainerProperty());
52
0
  if (!s) {
53
0
    s = new StickyScrollContainer(scrollFrame);
54
0
    frame->SetProperty(StickyScrollContainerProperty(), s);
55
0
  }
56
0
  return s;
57
0
}
58
59
// static
60
void
61
StickyScrollContainer::NotifyReparentedFrameAcrossScrollFrameBoundary(nsIFrame* aFrame,
62
                                                                      nsIFrame* aOldParent)
63
0
{
64
0
  nsIScrollableFrame* oldScrollFrame =
65
0
    nsLayoutUtils::GetNearestScrollableFrame(aOldParent,
66
0
      nsLayoutUtils::SCROLLABLE_SAME_DOC |
67
0
      nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN);
68
0
  if (!oldScrollFrame) {
69
0
    // XXX maybe aFrame has sticky descendants that can be sticky now, but
70
0
    // we aren't going to handle that.
71
0
    return;
72
0
  }
73
0
74
0
  StickyScrollContainer* oldSSC =
75
0
    static_cast<nsIFrame*>(do_QueryFrame(oldScrollFrame))->
76
0
      GetProperty(StickyScrollContainerProperty());
77
0
  if (!oldSSC) {
78
0
    // aOldParent had no sticky descendants, so aFrame doesn't have any sticky
79
0
    // descendants, and we're done here.
80
0
    return;
81
0
  }
82
0
83
0
  auto i = oldSSC->mFrames.Length();
84
0
  while (i-- > 0) {
85
0
    nsIFrame* f = oldSSC->mFrames[i];
86
0
    StickyScrollContainer* newSSC = GetStickyScrollContainerForFrame(f);
87
0
    if (newSSC != oldSSC) {
88
0
      oldSSC->RemoveFrame(f);
89
0
      if (newSSC) {
90
0
        newSSC->AddFrame(f);
91
0
      }
92
0
    }
93
0
  }
94
0
}
95
96
// static
97
StickyScrollContainer*
98
StickyScrollContainer::GetStickyScrollContainerForScrollFrame(nsIFrame* aFrame)
99
0
{
100
0
  return aFrame->GetProperty(StickyScrollContainerProperty());
101
0
}
102
103
static nscoord
104
ComputeStickySideOffset(Side aSide, const nsStyleSides& aOffset,
105
                        nscoord aPercentBasis)
106
0
{
107
0
  if (eStyleUnit_Auto == aOffset.GetUnit(aSide)) {
108
0
    return NS_AUTOOFFSET;
109
0
  } else {
110
0
    return nsLayoutUtils::ComputeCBDependentValue(aPercentBasis,
111
0
                                                  aOffset.Get(aSide));
112
0
  }
113
0
}
114
115
// static
116
void
117
StickyScrollContainer::ComputeStickyOffsets(nsIFrame* aFrame)
118
0
{
119
0
  nsIScrollableFrame* scrollableFrame =
120
0
    nsLayoutUtils::GetNearestScrollableFrame(aFrame->GetParent(),
121
0
      nsLayoutUtils::SCROLLABLE_SAME_DOC |
122
0
      nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN);
123
0
124
0
  if (!scrollableFrame) {
125
0
    // Bail.
126
0
    return;
127
0
  }
128
0
129
0
  nsSize scrollContainerSize = scrollableFrame->GetScrolledFrame()->
130
0
    GetContentRectRelativeToSelf().Size();
131
0
132
0
  nsMargin computedOffsets;
133
0
  const nsStylePosition* position = aFrame->StylePosition();
134
0
135
0
  computedOffsets.left   = ComputeStickySideOffset(eSideLeft, position->mOffset,
136
0
                                                   scrollContainerSize.width);
137
0
  computedOffsets.right  = ComputeStickySideOffset(eSideRight, position->mOffset,
138
0
                                                   scrollContainerSize.width);
139
0
  computedOffsets.top    = ComputeStickySideOffset(eSideTop, position->mOffset,
140
0
                                                   scrollContainerSize.height);
141
0
  computedOffsets.bottom = ComputeStickySideOffset(eSideBottom, position->mOffset,
142
0
                                                   scrollContainerSize.height);
143
0
144
0
  // Store the offset
145
0
  nsMargin* offsets = aFrame->GetProperty(nsIFrame::ComputedOffsetProperty());
146
0
  if (offsets) {
147
0
    *offsets = computedOffsets;
148
0
  } else {
149
0
    aFrame->SetProperty(nsIFrame::ComputedOffsetProperty(),
150
0
                        new nsMargin(computedOffsets));
151
0
  }
152
0
}
153
154
static nscoord gUnboundedNegative = nscoord_MIN / 2;
155
static nscoord gUnboundedExtent = nscoord_MAX;
156
static nscoord gUnboundedPositive = gUnboundedNegative + gUnboundedExtent;
157
158
void
159
StickyScrollContainer::ComputeStickyLimits(nsIFrame* aFrame, nsRect* aStick,
160
                                           nsRect* aContain) const
161
0
{
162
0
  NS_ASSERTION(nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(aFrame),
163
0
               "Can't sticky position individual continuations");
164
0
165
0
  aStick->SetRect(gUnboundedNegative, gUnboundedNegative, gUnboundedExtent, gUnboundedExtent);
166
0
  aContain->SetRect(gUnboundedNegative, gUnboundedNegative, gUnboundedExtent, gUnboundedExtent);
167
0
168
0
  const nsMargin* computedOffsets =
169
0
    aFrame->GetProperty(nsIFrame::ComputedOffsetProperty());
170
0
  if (!computedOffsets) {
171
0
    // We haven't reflowed the scroll frame yet, so offsets haven't been
172
0
    // computed. Bail.
173
0
    return;
174
0
  }
175
0
176
0
  nsIFrame* scrolledFrame = mScrollFrame->GetScrolledFrame();
177
0
  nsIFrame* cbFrame = aFrame->GetContainingBlock();
178
0
  NS_ASSERTION(cbFrame == scrolledFrame ||
179
0
    nsLayoutUtils::IsProperAncestorFrame(scrolledFrame, cbFrame),
180
0
    "Scroll frame should be an ancestor of the containing block");
181
0
182
0
  nsRect rect =
183
0
    nsLayoutUtils::GetAllInFlowRectsUnion(aFrame, aFrame->GetParent());
184
0
185
0
  // FIXME(bug 1421660): Table row groups aren't supposed to be containing
186
0
  // blocks, but we treat them as such (maybe it's the right thing to do!).
187
0
  // Anyway, not having this basically disables position: sticky on table cells,
188
0
  // which would be really unfortunate, and doesn't match what other browsers
189
0
  // do.
190
0
  if (cbFrame != scrolledFrame && cbFrame->IsTableRowGroupFrame()) {
191
0
    cbFrame = cbFrame->GetContainingBlock();
192
0
  }
193
0
194
0
  // Containing block limits for the position of aFrame relative to its parent.
195
0
  // The margin box of the sticky element stays within the content box of the
196
0
  // contaning-block element.
197
0
  if (cbFrame != scrolledFrame) {
198
0
    *aContain = nsLayoutUtils::
199
0
      GetAllInFlowRectsUnion(cbFrame, aFrame->GetParent(),
200
0
                             nsLayoutUtils::RECTS_USE_CONTENT_BOX);
201
0
    nsRect marginRect = nsLayoutUtils::
202
0
      GetAllInFlowRectsUnion(aFrame, aFrame->GetParent(),
203
0
                             nsLayoutUtils::RECTS_USE_MARGIN_BOX);
204
0
205
0
    // Deflate aContain by the difference between the union of aFrame's
206
0
    // continuations' margin boxes and the union of their border boxes, so that
207
0
    // by keeping aFrame within aContain, we keep the union of the margin boxes
208
0
    // within the containing block's content box.
209
0
    aContain->Deflate(marginRect - rect);
210
0
211
0
    // Deflate aContain by the border-box size, to form a constraint on the
212
0
    // upper-left corner of aFrame and continuations.
213
0
    aContain->Deflate(nsMargin(0, rect.width, rect.height, 0));
214
0
  }
215
0
216
0
  nsMargin sfPadding = scrolledFrame->GetUsedPadding();
217
0
  nsPoint sfOffset = aFrame->GetParent()->GetOffsetTo(scrolledFrame);
218
0
219
0
  // Top
220
0
  if (computedOffsets->top != NS_AUTOOFFSET) {
221
0
    aStick->SetTopEdge(mScrollPosition.y + sfPadding.top +
222
0
                       computedOffsets->top - sfOffset.y);
223
0
  }
224
0
225
0
  nsSize sfSize = scrolledFrame->GetContentRectRelativeToSelf().Size();
226
0
227
0
  // Bottom
228
0
  if (computedOffsets->bottom != NS_AUTOOFFSET &&
229
0
      (computedOffsets->top == NS_AUTOOFFSET ||
230
0
       rect.height <= sfSize.height - computedOffsets->TopBottom())) {
231
0
    aStick->SetBottomEdge(mScrollPosition.y + sfPadding.top + sfSize.height -
232
0
                          computedOffsets->bottom - rect.height - sfOffset.y);
233
0
  }
234
0
235
0
  uint8_t direction = cbFrame->StyleVisibility()->mDirection;
236
0
237
0
  // Left
238
0
  if (computedOffsets->left != NS_AUTOOFFSET &&
239
0
      (computedOffsets->right == NS_AUTOOFFSET ||
240
0
       direction == NS_STYLE_DIRECTION_LTR ||
241
0
       rect.width <= sfSize.width - computedOffsets->LeftRight())) {
242
0
    aStick->SetLeftEdge(mScrollPosition.x + sfPadding.left +
243
0
                        computedOffsets->left - sfOffset.x);
244
0
  }
245
0
246
0
  // Right
247
0
  if (computedOffsets->right != NS_AUTOOFFSET &&
248
0
      (computedOffsets->left == NS_AUTOOFFSET ||
249
0
       direction == NS_STYLE_DIRECTION_RTL ||
250
0
       rect.width <= sfSize.width - computedOffsets->LeftRight())) {
251
0
    aStick->SetRightEdge(mScrollPosition.x + sfPadding.left + sfSize.width -
252
0
                         computedOffsets->right - rect.width - sfOffset.x);
253
0
  }
254
0
255
0
  // These limits are for the bounding box of aFrame's continuations. Convert
256
0
  // to limits for aFrame itself.
257
0
  nsPoint frameOffset = aFrame->GetPosition() - rect.TopLeft();
258
0
  aStick->MoveBy(frameOffset);
259
0
  aContain->MoveBy(frameOffset);
260
0
}
261
262
nsPoint
263
StickyScrollContainer::ComputePosition(nsIFrame* aFrame) const
264
0
{
265
0
  nsRect stick;
266
0
  nsRect contain;
267
0
  ComputeStickyLimits(aFrame, &stick, &contain);
268
0
269
0
  nsPoint position = aFrame->GetNormalPosition();
270
0
271
0
  // For each sticky direction (top, bottom, left, right), move the frame along
272
0
  // the appropriate axis, based on the scroll position, but limit this to keep
273
0
  // the element's margin box within the containing block.
274
0
  position.y = std::max(position.y, std::min(stick.y, contain.YMost()));
275
0
  position.y = std::min(position.y, std::max(stick.YMost(), contain.y));
276
0
  position.x = std::max(position.x, std::min(stick.x, contain.XMost()));
277
0
  position.x = std::min(position.x, std::max(stick.XMost(), contain.x));
278
0
279
0
  return position;
280
0
}
281
282
void
283
StickyScrollContainer::GetScrollRanges(nsIFrame* aFrame, nsRectAbsolute* aOuter,
284
                                       nsRectAbsolute* aInner) const
285
0
{
286
0
  // We need to use the first in flow; continuation frames should not move
287
0
  // relative to each other and should get identical scroll ranges.
288
0
  // Also, ComputeStickyLimits requires this.
289
0
  nsIFrame *firstCont =
290
0
    nsLayoutUtils::FirstContinuationOrIBSplitSibling(aFrame);
291
0
292
0
  nsRect stickRect;
293
0
  nsRect containRect;
294
0
  ComputeStickyLimits(firstCont, &stickRect, &containRect);
295
0
296
0
  nsRectAbsolute stick = nsRectAbsolute::FromRect(stickRect);
297
0
  nsRectAbsolute contain = nsRectAbsolute::FromRect(containRect);
298
0
299
0
  aOuter->SetBox(gUnboundedNegative, gUnboundedNegative, gUnboundedPositive, gUnboundedPositive);
300
0
  aInner->SetBox(gUnboundedNegative, gUnboundedNegative, gUnboundedPositive, gUnboundedPositive);
301
0
302
0
  const nsPoint normalPosition = firstCont->GetNormalPosition();
303
0
304
0
  // Bottom and top
305
0
  if (stick.YMost() != gUnboundedPositive) {
306
0
    aOuter->SetTopEdge(contain.Y() - stick.YMost());
307
0
    aInner->SetTopEdge(normalPosition.y - stick.YMost());
308
0
  }
309
0
310
0
  if (stick.Y() != gUnboundedNegative) {
311
0
    aInner->SetBottomEdge(normalPosition.y - stick.Y());
312
0
    aOuter->SetBottomEdge(contain.YMost() - stick.Y());
313
0
  }
314
0
315
0
  // Right and left
316
0
  if (stick.XMost() != gUnboundedPositive) {
317
0
    aOuter->SetLeftEdge(contain.X() - stick.XMost());
318
0
    aInner->SetLeftEdge(normalPosition.x - stick.XMost());
319
0
  }
320
0
321
0
  if (stick.X() != gUnboundedNegative) {
322
0
    aInner->SetRightEdge(normalPosition.x - stick.X());
323
0
    aOuter->SetRightEdge(contain.XMost() - stick.X());
324
0
  }
325
0
326
0
  // Make sure |inner| does not extend outside of |outer|. (The consumers of
327
0
  // the Layers API, to which this information is propagated, expect this
328
0
  // invariant to hold.) The calculated value of |inner| can sometimes extend
329
0
  // outside of |outer|, for example due to margin collapsing, since
330
0
  // GetNormalPosition() returns the actual position after margin collapsing,
331
0
  // while |contain| is calculated based on the frame's GetUsedMargin() which
332
0
  // is pre-collapsing.
333
0
  // Note that this doesn't necessarily solve all problems stemming from
334
0
  // comparing pre- and post-collapsing margins (TODO: find a proper solution).
335
0
  *aInner = aInner->Intersect(*aOuter);
336
0
}
337
338
void
339
StickyScrollContainer::PositionContinuations(nsIFrame* aFrame)
340
0
{
341
0
  NS_ASSERTION(nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(aFrame),
342
0
               "Should be starting from the first continuation");
343
0
  nsPoint translation = ComputePosition(aFrame) - aFrame->GetNormalPosition();
344
0
345
0
  // Move all continuation frames by the same amount.
346
0
  for (nsIFrame* cont = aFrame; cont;
347
0
       cont = nsLayoutUtils::GetNextContinuationOrIBSplitSibling(cont)) {
348
0
    cont->SetPosition(cont->GetNormalPosition() + translation);
349
0
  }
350
0
}
351
352
void
353
StickyScrollContainer::UpdatePositions(nsPoint aScrollPosition,
354
                                       nsIFrame* aSubtreeRoot)
355
0
{
356
#ifdef DEBUG
357
  {
358
    nsIFrame* scrollFrameAsFrame = do_QueryFrame(mScrollFrame);
359
    NS_ASSERTION(!aSubtreeRoot || aSubtreeRoot == scrollFrameAsFrame,
360
                 "If reflowing, should be reflowing the scroll frame");
361
  }
362
#endif
363
  mScrollPosition = aScrollPosition;
364
0
365
0
  OverflowChangedTracker oct;
366
0
  oct.SetSubtreeRoot(aSubtreeRoot);
367
0
  for (nsTArray<nsIFrame*>::size_type i = 0; i < mFrames.Length(); i++) {
368
0
    nsIFrame* f = mFrames[i];
369
0
    if (!nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(f)) {
370
0
      // This frame was added in nsFrame::Init before we knew it wasn't
371
0
      // the first ib-split-sibling.
372
0
      mFrames.RemoveElementAt(i);
373
0
      --i;
374
0
      continue;
375
0
    }
376
0
377
0
    if (aSubtreeRoot) {
378
0
      // Reflowing the scroll frame, so recompute offsets.
379
0
      ComputeStickyOffsets(f);
380
0
    }
381
0
    // mFrames will only contain first continuations, because we filter in
382
0
    // nsIFrame::Init.
383
0
    PositionContinuations(f);
384
0
385
0
    f = f->GetParent();
386
0
    if (f != aSubtreeRoot) {
387
0
      for (nsIFrame* cont = f; cont;
388
0
           cont = nsLayoutUtils::GetNextContinuationOrIBSplitSibling(cont)) {
389
0
        oct.AddFrame(cont, OverflowChangedTracker::CHILDREN_CHANGED);
390
0
      }
391
0
    }
392
0
  }
393
0
  oct.Flush();
394
0
}
395
396
void
397
StickyScrollContainer::ScrollPositionWillChange(nscoord aX, nscoord aY)
398
0
{
399
0
}
400
401
void
402
StickyScrollContainer::ScrollPositionDidChange(nscoord aX, nscoord aY)
403
0
{
404
0
  UpdatePositions(nsPoint(aX, aY), nullptr);
405
0
}
406
407
} // namespace mozilla