tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

StickyScrollContainer.cpp (15919B)


      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 "PresShell.h"
     15 #include "mozilla/OverflowChangedTracker.h"
     16 #include "mozilla/ScrollContainerFrame.h"
     17 #include "nsIFrame.h"
     18 #include "nsIFrameInlines.h"
     19 #include "nsLayoutUtils.h"
     20 
     21 namespace mozilla {
     22 
     23 StickyScrollContainer::StickyScrollContainer(
     24    ScrollContainerFrame* aScrollContainerFrame)
     25    : mScrollContainerFrame(aScrollContainerFrame) {}
     26 
     27 StickyScrollContainer::~StickyScrollContainer() = default;
     28 
     29 // static
     30 StickyScrollContainer* StickyScrollContainer::GetOrCreateForFrame(
     31    nsIFrame* aFrame) {
     32  ScrollContainerFrame* scrollContainerFrame =
     33      nsLayoutUtils::GetNearestScrollContainerFrame(
     34          aFrame->GetParent(), nsLayoutUtils::SCROLLABLE_SAME_DOC |
     35                                   nsLayoutUtils::SCROLLABLE_STOP_AT_PAGE |
     36                                   nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN);
     37  if (!scrollContainerFrame) {
     38    // We might not find any, for instance in the case of
     39    // <html style="position: fixed">
     40    return nullptr;
     41  }
     42  return &scrollContainerFrame->EnsureStickyContainer();
     43 }
     44 
     45 static nscoord ComputeStickySideOffset(Side aSide,
     46                                       const nsStylePosition& aPosition,
     47                                       nscoord aPercentBasis) {
     48  // Guaranteed to resolve any use of anchor function as invalid.
     49  const auto& side = aPosition.GetAnchorResolvedInset(
     50      aSide, AnchorPosOffsetResolutionParams::UseCBFrameSize(
     51                 {nullptr, StylePositionProperty::Sticky}));
     52  if (side->IsAuto()) {
     53    return NS_AUTOOFFSET;
     54  }
     55  return nsLayoutUtils::ComputeCBDependentValue(aPercentBasis,
     56                                                side->AsLengthPercentage());
     57 }
     58 
     59 // static
     60 void StickyScrollContainer::ComputeStickyOffsets(nsIFrame* aFrame) {
     61  ScrollContainerFrame* scrollContainerFrame =
     62      nsLayoutUtils::GetNearestScrollContainerFrame(
     63          aFrame->GetParent(), nsLayoutUtils::SCROLLABLE_SAME_DOC |
     64                                   nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN);
     65 
     66  if (!scrollContainerFrame) {
     67    // Bail.
     68    return;
     69  }
     70 
     71  nsSize scrollContainerSize =
     72      scrollContainerFrame->GetScrolledFrameSizeAccountingForDynamicToolbar();
     73 
     74  nsMargin computedOffsets;
     75  const nsStylePosition* position = aFrame->StylePosition();
     76 
     77  computedOffsets.left =
     78      ComputeStickySideOffset(eSideLeft, *position, scrollContainerSize.width);
     79  computedOffsets.right =
     80      ComputeStickySideOffset(eSideRight, *position, scrollContainerSize.width);
     81  computedOffsets.top =
     82      ComputeStickySideOffset(eSideTop, *position, scrollContainerSize.height);
     83  computedOffsets.bottom = ComputeStickySideOffset(eSideBottom, *position,
     84                                                   scrollContainerSize.height);
     85 
     86  // Store the offset
     87  nsMargin* offsets = aFrame->GetProperty(nsIFrame::ComputedOffsetProperty());
     88  if (offsets) {
     89    *offsets = computedOffsets;
     90  } else {
     91    aFrame->SetProperty(nsIFrame::ComputedOffsetProperty(),
     92                        new nsMargin(computedOffsets));
     93  }
     94 }
     95 
     96 static constexpr nscoord gUnboundedNegative = nscoord_MIN / 2;
     97 static constexpr nscoord gUnboundedExtent = nscoord_MAX;
     98 static constexpr nscoord gUnboundedPositive =
     99    gUnboundedNegative + gUnboundedExtent;
    100 
    101 void StickyScrollContainer::ComputeStickyLimits(nsIFrame* aFrame,
    102                                                nsRect* aStick,
    103                                                nsRect* aContain) const {
    104  NS_ASSERTION(nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(aFrame),
    105               "Can't sticky position individual continuations");
    106 
    107  aStick->SetRect(gUnboundedNegative, gUnboundedNegative, gUnboundedExtent,
    108                  gUnboundedExtent);
    109  aContain->SetRect(gUnboundedNegative, gUnboundedNegative, gUnboundedExtent,
    110                    gUnboundedExtent);
    111 
    112  const nsMargin* computedOffsets =
    113      aFrame->GetProperty(nsIFrame::ComputedOffsetProperty());
    114  if (!computedOffsets) {
    115    // We haven't reflowed the scroll frame yet, so offsets haven't been
    116    // computed. Bail.
    117    return;
    118  }
    119 
    120  nsIFrame* scrolledFrame = mScrollContainerFrame->GetScrolledFrame();
    121  nsIFrame* cbFrame = aFrame->GetContainingBlock();
    122  NS_ASSERTION(cbFrame == scrolledFrame ||
    123                   nsLayoutUtils::IsProperAncestorFrame(scrolledFrame, cbFrame),
    124               "Scroll frame should be an ancestor of the containing block");
    125 
    126  nsRect rect =
    127      nsLayoutUtils::GetAllInFlowRectsUnion(aFrame, aFrame->GetParent());
    128 
    129  // FIXME(bug 1421660): Table row groups aren't supposed to be containing
    130  // blocks, but we treat them as such (maybe it's the right thing to do!).
    131  // Anyway, not having this basically disables position: sticky on table cells,
    132  // which would be really unfortunate, and doesn't match what other browsers
    133  // do.
    134  if (cbFrame != scrolledFrame && cbFrame->IsTableRowGroupFrame()) {
    135    cbFrame = cbFrame->GetContainingBlock();
    136  }
    137 
    138  // Containing block limits for the position of aFrame relative to its parent.
    139  // The margin box of the sticky element stays within the content box of the
    140  // containing-block element.
    141  if (cbFrame == scrolledFrame) {
    142    // cbFrame is the scrolledFrame, and it won't have continuations. Unlike the
    143    // else clause, we consider scrollable overflow rect because the union of
    144    // its in-flow rects doesn't include the scrollable overflow area. We need
    145    // to subtract the padding however, which _is_ included in the scrollable
    146    // area, since we want the content box.
    147    MOZ_ASSERT(cbFrame->GetUsedBorder() == nsMargin(),
    148               "How did the ::-moz-scrolled-frame end up with border?");
    149    *aContain = cbFrame->ScrollableOverflowRectRelativeToSelf();
    150    aContain->Deflate(cbFrame->GetUsedPadding());
    151    nsLayoutUtils::TransformRect(cbFrame, aFrame->GetParent(), *aContain);
    152  } else {
    153    *aContain = nsLayoutUtils::GetAllInFlowRectsUnion(
    154        cbFrame, aFrame->GetParent(),
    155        nsLayoutUtils::GetAllInFlowRectsFlag::UseContentBox);
    156  }
    157 
    158  nsRect marginRect = nsLayoutUtils::GetAllInFlowRectsUnion(
    159      aFrame, aFrame->GetParent(),
    160      nsLayoutUtils::GetAllInFlowRectsFlag::UseMarginBoxWithAutoResolvedAsZero);
    161 
    162  // Deflate aContain by the difference between the union of aFrame's
    163  // continuations' margin boxes and the union of their border boxes, so that
    164  // by keeping aFrame within aContain, we keep the union of the margin boxes
    165  // within the containing block's content box.
    166  aContain->Deflate(marginRect - rect);
    167 
    168  // Deflate aContain by the border-box size, to form a constraint on the
    169  // upper-left corner of aFrame and continuations.
    170  aContain->Deflate(nsMargin(0, rect.width, rect.height, 0));
    171 
    172  nsMargin sfPadding = scrolledFrame->GetUsedPadding();
    173  nsPoint sfOffset = aFrame->GetParent()->GetOffsetTo(scrolledFrame);
    174  nsSize sfSize =
    175      mScrollContainerFrame->GetScrolledFrameSizeAccountingForDynamicToolbar();
    176  StyleDirection direction = cbFrame->StyleVisibility()->mDirection;
    177  nsMargin effectiveOffsets = *computedOffsets;
    178 
    179  if (computedOffsets->top != NS_AUTOOFFSET &&
    180      computedOffsets->bottom != NS_AUTOOFFSET) {
    181    // Decrease the effective end-edge inset of the sticky view rectangle
    182    // when its height is less than the height of the border-box of the sticky
    183    // box.
    184    nscoord stickyViewHeight = sfSize.height - computedOffsets->TopBottom();
    185    if (rect.height > stickyViewHeight) {
    186      nscoord delta = rect.height - stickyViewHeight;
    187      effectiveOffsets.bottom -= delta;
    188    }
    189  }
    190 
    191  if (computedOffsets->left != NS_AUTOOFFSET &&
    192      computedOffsets->right != NS_AUTOOFFSET) {
    193    // Decrease the effective end-edge inset of the sticky view rectangle
    194    // when its width is less than the width of the border-box of the sticky
    195    // box.
    196    nscoord stickyViewWidth = sfSize.width - computedOffsets->LeftRight();
    197    if (rect.width > stickyViewWidth) {
    198      nscoord delta = rect.width - stickyViewWidth;
    199      if (direction == StyleDirection::Ltr) {
    200        effectiveOffsets.right -= delta;
    201      } else {
    202        effectiveOffsets.left -= delta;
    203      }
    204    }
    205  }
    206 
    207  // Top
    208  if (computedOffsets->top != NS_AUTOOFFSET) {
    209    aStick->SetTopEdge(mScrollPosition.y + sfPadding.top +
    210                       effectiveOffsets.top - sfOffset.y);
    211  }
    212 
    213  // Bottom
    214  if (computedOffsets->bottom != NS_AUTOOFFSET) {
    215    aStick->SetBottomEdge(mScrollPosition.y + sfPadding.top + sfSize.height -
    216                          effectiveOffsets.bottom - rect.height - sfOffset.y);
    217  }
    218 
    219  // Left
    220  if (computedOffsets->left != NS_AUTOOFFSET) {
    221    aStick->SetLeftEdge(mScrollPosition.x + sfPadding.left +
    222                        effectiveOffsets.left - sfOffset.x);
    223  }
    224 
    225  // Right
    226  if (computedOffsets->right != NS_AUTOOFFSET) {
    227    aStick->SetRightEdge(mScrollPosition.x + sfPadding.left + sfSize.width -
    228                         effectiveOffsets.right - rect.width - sfOffset.x);
    229  }
    230 
    231  // These limits are for the bounding box of aFrame's continuations. Convert
    232  // to limits for aFrame itself.
    233  nsPoint frameOffset = aFrame->GetPosition() - rect.TopLeft();
    234  aStick->MoveBy(frameOffset);
    235  aContain->MoveBy(frameOffset);
    236 }
    237 
    238 nsPoint StickyScrollContainer::ComputePosition(nsIFrame* aFrame) const {
    239  nsRect stick;
    240  nsRect contain;
    241  ComputeStickyLimits(aFrame, &stick, &contain);
    242 
    243  nsPoint position = aFrame->GetNormalPosition();
    244 
    245  // For each sticky direction (top, bottom, left, right), move the frame along
    246  // the appropriate axis, based on the scroll position, but limit this to keep
    247  // the element's margin box within the containing block.
    248  position.y = std::max(position.y, std::min(stick.y, contain.YMost()));
    249  position.y = std::min(position.y, std::max(stick.YMost(), contain.y));
    250  position.x = std::max(position.x, std::min(stick.x, contain.XMost()));
    251  position.x = std::min(position.x, std::max(stick.XMost(), contain.x));
    252 
    253  return position;
    254 }
    255 
    256 bool StickyScrollContainer::IsStuckInYDirection(nsIFrame* aFrame) const {
    257  nsPoint position = ComputePosition(aFrame);
    258  return position.y != aFrame->GetNormalPosition().y;
    259 }
    260 
    261 void StickyScrollContainer::GetScrollRanges(nsIFrame* aFrame,
    262                                            nsRectAbsolute* aOuter,
    263                                            nsRectAbsolute* aInner) const {
    264  // We need to use the first in flow; continuation frames should not move
    265  // relative to each other and should get identical scroll ranges.
    266  // Also, ComputeStickyLimits requires this.
    267  nsIFrame* firstCont =
    268      nsLayoutUtils::FirstContinuationOrIBSplitSibling(aFrame);
    269 
    270  nsRect stickRect;
    271  nsRect containRect;
    272  ComputeStickyLimits(firstCont, &stickRect, &containRect);
    273 
    274  nsRectAbsolute stick = nsRectAbsolute::FromRect(stickRect);
    275  nsRectAbsolute contain = nsRectAbsolute::FromRect(containRect);
    276 
    277  aOuter->SetBox(gUnboundedNegative, gUnboundedNegative, gUnboundedPositive,
    278                 gUnboundedPositive);
    279  aInner->SetBox(gUnboundedNegative, gUnboundedNegative, gUnboundedPositive,
    280                 gUnboundedPositive);
    281 
    282  const nsPoint normalPosition = firstCont->GetNormalPosition();
    283 
    284  // Bottom and top
    285  if (stick.YMost() != gUnboundedPositive) {
    286    aOuter->SetTopEdge(contain.Y() - stick.YMost());
    287    aInner->SetTopEdge(normalPosition.y - stick.YMost());
    288  }
    289 
    290  if (stick.Y() != gUnboundedNegative) {
    291    aInner->SetBottomEdge(normalPosition.y - stick.Y());
    292    aOuter->SetBottomEdge(contain.YMost() - stick.Y());
    293  }
    294 
    295  // Right and left
    296  if (stick.XMost() != gUnboundedPositive) {
    297    aOuter->SetLeftEdge(contain.X() - stick.XMost());
    298    aInner->SetLeftEdge(normalPosition.x - stick.XMost());
    299  }
    300 
    301  if (stick.X() != gUnboundedNegative) {
    302    aInner->SetRightEdge(normalPosition.x - stick.X());
    303    aOuter->SetRightEdge(contain.XMost() - stick.X());
    304  }
    305 
    306  // Make sure |inner| does not extend outside of |outer|. (The consumers of
    307  // the Layers API, to which this information is propagated, expect this
    308  // invariant to hold.) The calculated value of |inner| can sometimes extend
    309  // outside of |outer|, for example due to margin collapsing, since
    310  // GetNormalPosition() returns the actual position after margin collapsing,
    311  // while |contain| is calculated based on the frame's GetUsedMargin() which
    312  // is pre-collapsing.
    313  // Note that this doesn't necessarily solve all problems stemming from
    314  // comparing pre- and post-collapsing margins (TODO: find a proper solution).
    315  *aInner = aInner->Intersect(*aOuter);
    316  if (aInner->IsEmpty()) {
    317    // This might happen if aInner didn't intersect aOuter at all initially,
    318    // in which case aInner is empty and outside aOuter. Make sure it doesn't
    319    // extend outside aOuter.
    320    *aInner = aInner->MoveInsideAndClamp(*aOuter);
    321  }
    322 }
    323 
    324 void StickyScrollContainer::PositionContinuations(nsIFrame* aFrame) {
    325  NS_ASSERTION(nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(aFrame),
    326               "Should be starting from the first continuation");
    327  bool hadProperty;
    328  nsPoint translation =
    329      ComputePosition(aFrame) - aFrame->GetNormalPosition(&hadProperty);
    330  if (NS_WARN_IF(!hadProperty)) {
    331    // If the frame was never relatively positioned, don't move its position
    332    // dynamically. There are a variety of frames for which `position` doesn't
    333    // really apply like frames inside svg which would get here and be sticky
    334    // only in one direction.
    335    return;
    336  }
    337 
    338  // Move all continuation frames by the same amount.
    339  for (nsIFrame* cont = aFrame; cont;
    340       cont = nsLayoutUtils::GetNextContinuationOrIBSplitSibling(cont)) {
    341    cont->SetPosition(cont->GetNormalPosition() + translation);
    342  }
    343 }
    344 
    345 void StickyScrollContainer::UpdatePositions(nsPoint aScrollPosition,
    346                                            nsIFrame* aSubtreeRoot) {
    347 #ifdef DEBUG
    348  {
    349    nsIFrame* scrollFrameAsFrame = do_QueryFrame(mScrollContainerFrame);
    350    NS_ASSERTION(!aSubtreeRoot || aSubtreeRoot == scrollFrameAsFrame,
    351                 "If reflowing, should be reflowing the scroll frame");
    352  }
    353 #endif
    354  mScrollPosition = aScrollPosition;
    355 
    356  OverflowChangedTracker oct;
    357  oct.SetSubtreeRoot(aSubtreeRoot);
    358  // We need to position ancestors before children, so iter from shallowest.
    359  // Collect a list of frames to be removed, so that we don't invalidate the
    360  // iterator while we're using it.
    361  AutoTArray<nsIFrame*, 8> framesToRemove;
    362  for (nsIFrame* f : mFrames.IterFromShallowest()) {
    363    if (!nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(f)) {
    364      // This frame was added in nsIFrame::DidSetComputedStyle before we knew it
    365      // wasn't the first ib-split-sibling.
    366      framesToRemove.AppendElement(f);
    367      continue;
    368    }
    369    if (aSubtreeRoot) {
    370      // Reflowing the scroll frame, so recompute offsets.
    371      ComputeStickyOffsets(f);
    372    }
    373    // mFrames will only contain first continuations, because we filter in
    374    // nsIFrame::DidSetComputedStyle.
    375    PositionContinuations(f);
    376 
    377    f = f->GetParent();
    378    if (f != aSubtreeRoot) {
    379      for (nsIFrame* cont = f; cont;
    380           cont = nsLayoutUtils::GetNextContinuationOrIBSplitSibling(cont)) {
    381        oct.AddFrame(cont, OverflowChangedTracker::CHILDREN_CHANGED);
    382      }
    383    }
    384  }
    385  for (nsIFrame* f : framesToRemove) {
    386    mFrames.Remove(f);
    387  }
    388  oct.Flush();
    389 }
    390 
    391 void StickyScrollContainer::MarkFramesForReflow() {
    392  PresShell* ps = mScrollContainerFrame->PresShell();
    393  for (nsIFrame* frame : mFrames.IterFromShallowest()) {
    394    ps->FrameNeedsReflow(frame, IntrinsicDirty::None, NS_FRAME_IS_DIRTY);
    395  }
    396 }
    397 }  // namespace mozilla