michael@0: /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ts=2 sts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /** michael@0: * compute sticky positioning, both during reflow and when the scrolling michael@0: * container scrolls michael@0: */ michael@0: michael@0: #include "StickyScrollContainer.h" michael@0: #include "nsIFrame.h" michael@0: #include "nsIScrollableFrame.h" michael@0: #include "nsLayoutUtils.h" michael@0: #include "RestyleTracker.h" michael@0: michael@0: using namespace mozilla::css; michael@0: michael@0: namespace mozilla { michael@0: michael@0: void DestroyStickyScrollContainer(void* aPropertyValue) michael@0: { michael@0: delete static_cast(aPropertyValue); michael@0: } michael@0: michael@0: NS_DECLARE_FRAME_PROPERTY(StickyScrollContainerProperty, michael@0: DestroyStickyScrollContainer) michael@0: michael@0: StickyScrollContainer::StickyScrollContainer(nsIScrollableFrame* aScrollFrame) michael@0: : mScrollFrame(aScrollFrame) michael@0: , mScrollPosition() michael@0: { michael@0: mScrollFrame->AddScrollPositionListener(this); michael@0: } michael@0: michael@0: StickyScrollContainer::~StickyScrollContainer() michael@0: { michael@0: mScrollFrame->RemoveScrollPositionListener(this); michael@0: } michael@0: michael@0: // static michael@0: StickyScrollContainer* michael@0: StickyScrollContainer::GetStickyScrollContainerForFrame(nsIFrame* aFrame) michael@0: { michael@0: nsIScrollableFrame* scrollFrame = michael@0: nsLayoutUtils::GetNearestScrollableFrame(aFrame->GetParent(), michael@0: nsLayoutUtils::SCROLLABLE_SAME_DOC | michael@0: nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN); michael@0: if (!scrollFrame) { michael@0: // We might not find any, for instance in the case of michael@0: // michael@0: return nullptr; michael@0: } michael@0: FrameProperties props = static_cast(do_QueryFrame(scrollFrame))-> michael@0: Properties(); michael@0: StickyScrollContainer* s = static_cast michael@0: (props.Get(StickyScrollContainerProperty())); michael@0: if (!s) { michael@0: s = new StickyScrollContainer(scrollFrame); michael@0: props.Set(StickyScrollContainerProperty(), s); michael@0: } michael@0: return s; michael@0: } michael@0: michael@0: // static michael@0: void michael@0: StickyScrollContainer::NotifyReparentedFrameAcrossScrollFrameBoundary(nsIFrame* aFrame, michael@0: nsIFrame* aOldParent) michael@0: { michael@0: nsIScrollableFrame* oldScrollFrame = michael@0: nsLayoutUtils::GetNearestScrollableFrame(aOldParent, michael@0: nsLayoutUtils::SCROLLABLE_SAME_DOC | michael@0: nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN); michael@0: if (!oldScrollFrame) { michael@0: // XXX maybe aFrame has sticky descendants that can be sticky now, but michael@0: // we aren't going to handle that. michael@0: return; michael@0: } michael@0: FrameProperties props = static_cast(do_QueryFrame(oldScrollFrame))-> michael@0: Properties(); michael@0: StickyScrollContainer* oldSSC = static_cast michael@0: (props.Get(StickyScrollContainerProperty())); michael@0: if (!oldSSC) { michael@0: // aOldParent had no sticky descendants, so aFrame doesn't have any sticky michael@0: // descendants, and we're done here. michael@0: return; michael@0: } michael@0: michael@0: auto i = oldSSC->mFrames.Length(); michael@0: while (i-- > 0) { michael@0: nsIFrame* f = oldSSC->mFrames[i]; michael@0: StickyScrollContainer* newSSC = GetStickyScrollContainerForFrame(f); michael@0: if (newSSC != oldSSC) { michael@0: oldSSC->RemoveFrame(f); michael@0: if (newSSC) { michael@0: newSSC->AddFrame(f); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: // static michael@0: StickyScrollContainer* michael@0: StickyScrollContainer::GetStickyScrollContainerForScrollFrame(nsIFrame* aFrame) michael@0: { michael@0: FrameProperties props = aFrame->Properties(); michael@0: return static_cast michael@0: (props.Get(StickyScrollContainerProperty())); michael@0: } michael@0: michael@0: static nscoord michael@0: ComputeStickySideOffset(Side aSide, const nsStyleSides& aOffset, michael@0: nscoord aPercentBasis) michael@0: { michael@0: if (eStyleUnit_Auto == aOffset.GetUnit(aSide)) { michael@0: return NS_AUTOOFFSET; michael@0: } else { michael@0: return nsLayoutUtils::ComputeCBDependentValue(aPercentBasis, michael@0: aOffset.Get(aSide)); michael@0: } michael@0: } michael@0: michael@0: // static michael@0: void michael@0: StickyScrollContainer::ComputeStickyOffsets(nsIFrame* aFrame) michael@0: { michael@0: nsIScrollableFrame* scrollableFrame = michael@0: nsLayoutUtils::GetNearestScrollableFrame(aFrame->GetParent(), michael@0: nsLayoutUtils::SCROLLABLE_SAME_DOC | michael@0: nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN); michael@0: michael@0: if (!scrollableFrame) { michael@0: // Bail. michael@0: return; michael@0: } michael@0: michael@0: nsSize scrollContainerSize = scrollableFrame->GetScrolledFrame()-> michael@0: GetContentRectRelativeToSelf().Size(); michael@0: michael@0: nsMargin computedOffsets; michael@0: const nsStylePosition* position = aFrame->StylePosition(); michael@0: michael@0: computedOffsets.left = ComputeStickySideOffset(eSideLeft, position->mOffset, michael@0: scrollContainerSize.width); michael@0: computedOffsets.right = ComputeStickySideOffset(eSideRight, position->mOffset, michael@0: scrollContainerSize.width); michael@0: computedOffsets.top = ComputeStickySideOffset(eSideTop, position->mOffset, michael@0: scrollContainerSize.height); michael@0: computedOffsets.bottom = ComputeStickySideOffset(eSideBottom, position->mOffset, michael@0: scrollContainerSize.height); michael@0: michael@0: // Store the offset michael@0: FrameProperties props = aFrame->Properties(); michael@0: nsMargin* offsets = static_cast michael@0: (props.Get(nsIFrame::ComputedOffsetProperty())); michael@0: if (offsets) { michael@0: *offsets = computedOffsets; michael@0: } else { michael@0: props.Set(nsIFrame::ComputedOffsetProperty(), michael@0: new nsMargin(computedOffsets)); michael@0: } michael@0: } michael@0: michael@0: void michael@0: StickyScrollContainer::ComputeStickyLimits(nsIFrame* aFrame, nsRect* aStick, michael@0: nsRect* aContain) const michael@0: { michael@0: NS_ASSERTION(nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(aFrame), michael@0: "Can't sticky position individual continuations"); michael@0: michael@0: aStick->SetRect(nscoord_MIN/2, nscoord_MIN/2, nscoord_MAX, nscoord_MAX); michael@0: aContain->SetRect(nscoord_MIN/2, nscoord_MIN/2, nscoord_MAX, nscoord_MAX); michael@0: michael@0: const nsMargin* computedOffsets = static_cast( michael@0: aFrame->Properties().Get(nsIFrame::ComputedOffsetProperty())); michael@0: if (!computedOffsets) { michael@0: // We haven't reflowed the scroll frame yet, so offsets haven't been michael@0: // computed. Bail. michael@0: return; michael@0: } michael@0: michael@0: nsIFrame* scrolledFrame = mScrollFrame->GetScrolledFrame(); michael@0: nsIFrame* cbFrame = aFrame->GetContainingBlock(); michael@0: NS_ASSERTION(cbFrame == scrolledFrame || michael@0: nsLayoutUtils::IsProperAncestorFrame(scrolledFrame, cbFrame), michael@0: "Scroll frame should be an ancestor of the containing block"); michael@0: michael@0: nsRect rect = michael@0: nsLayoutUtils::GetAllInFlowRectsUnion(aFrame, aFrame->GetParent()); michael@0: michael@0: // Containing block limits for the position of aFrame relative to its parent. michael@0: // The margin box of the sticky element stays within the content box of the michael@0: // contaning-block element. michael@0: if (cbFrame != scrolledFrame) { michael@0: *aContain = nsLayoutUtils:: michael@0: GetAllInFlowRectsUnion(cbFrame, aFrame->GetParent(), michael@0: nsLayoutUtils::RECTS_USE_CONTENT_BOX); michael@0: nsRect marginRect = nsLayoutUtils:: michael@0: GetAllInFlowRectsUnion(aFrame, aFrame->GetParent(), michael@0: nsLayoutUtils::RECTS_USE_MARGIN_BOX); michael@0: michael@0: // Deflate aContain by the difference between the union of aFrame's michael@0: // continuations' margin boxes and the union of their border boxes, so that michael@0: // by keeping aFrame within aContain, we keep the union of the margin boxes michael@0: // within the containing block's content box. michael@0: aContain->Deflate(marginRect - rect); michael@0: michael@0: // Deflate aContain by the border-box size, to form a constraint on the michael@0: // upper-left corner of aFrame and continuations. michael@0: aContain->Deflate(nsMargin(0, rect.width, rect.height, 0)); michael@0: } michael@0: michael@0: nsMargin sfPadding = scrolledFrame->GetUsedPadding(); michael@0: nsPoint sfOffset = aFrame->GetParent()->GetOffsetTo(scrolledFrame); michael@0: michael@0: // Top michael@0: if (computedOffsets->top != NS_AUTOOFFSET) { michael@0: aStick->SetTopEdge(mScrollPosition.y + sfPadding.top + michael@0: computedOffsets->top - sfOffset.y); michael@0: } michael@0: michael@0: nsSize sfSize = scrolledFrame->GetContentRectRelativeToSelf().Size(); michael@0: michael@0: // Bottom michael@0: if (computedOffsets->bottom != NS_AUTOOFFSET && michael@0: (computedOffsets->top == NS_AUTOOFFSET || michael@0: rect.height <= sfSize.height - computedOffsets->TopBottom())) { michael@0: aStick->SetBottomEdge(mScrollPosition.y + sfPadding.top + sfSize.height - michael@0: computedOffsets->bottom - rect.height - sfOffset.y); michael@0: } michael@0: michael@0: uint8_t direction = cbFrame->StyleVisibility()->mDirection; michael@0: michael@0: // Left michael@0: if (computedOffsets->left != NS_AUTOOFFSET && michael@0: (computedOffsets->right == NS_AUTOOFFSET || michael@0: direction == NS_STYLE_DIRECTION_LTR || michael@0: rect.width <= sfSize.width - computedOffsets->LeftRight())) { michael@0: aStick->SetLeftEdge(mScrollPosition.x + sfPadding.left + michael@0: computedOffsets->left - sfOffset.x); michael@0: } michael@0: michael@0: // Right michael@0: if (computedOffsets->right != NS_AUTOOFFSET && michael@0: (computedOffsets->left == NS_AUTOOFFSET || michael@0: direction == NS_STYLE_DIRECTION_RTL || michael@0: rect.width <= sfSize.width - computedOffsets->LeftRight())) { michael@0: aStick->SetRightEdge(mScrollPosition.x + sfPadding.left + sfSize.width - michael@0: computedOffsets->right - rect.width - sfOffset.x); michael@0: } michael@0: michael@0: // These limits are for the bounding box of aFrame's continuations. Convert michael@0: // to limits for aFrame itself. michael@0: nsPoint frameOffset = aFrame->GetPosition() - rect.TopLeft(); michael@0: aStick->MoveBy(frameOffset); michael@0: aContain->MoveBy(frameOffset); michael@0: } michael@0: michael@0: nsPoint michael@0: StickyScrollContainer::ComputePosition(nsIFrame* aFrame) const michael@0: { michael@0: nsRect stick; michael@0: nsRect contain; michael@0: ComputeStickyLimits(aFrame, &stick, &contain); michael@0: michael@0: nsPoint position = aFrame->GetNormalPosition(); michael@0: michael@0: // For each sticky direction (top, bottom, left, right), move the frame along michael@0: // the appropriate axis, based on the scroll position, but limit this to keep michael@0: // the element's margin box within the containing block. michael@0: position.y = std::max(position.y, std::min(stick.y, contain.YMost())); michael@0: position.y = std::min(position.y, std::max(stick.YMost(), contain.y)); michael@0: position.x = std::max(position.x, std::min(stick.x, contain.XMost())); michael@0: position.x = std::min(position.x, std::max(stick.XMost(), contain.x)); michael@0: michael@0: return position; michael@0: } michael@0: michael@0: void michael@0: StickyScrollContainer::GetScrollRanges(nsIFrame* aFrame, nsRect* aOuter, michael@0: nsRect* aInner) const michael@0: { michael@0: // We need to use the first in flow; ComputeStickyLimits requires michael@0: // this, at the very least because its call to michael@0: // nsLayoutUtils::GetAllInFlowRectsUnion requires it. michael@0: nsIFrame *firstCont = michael@0: nsLayoutUtils::FirstContinuationOrIBSplitSibling(aFrame); michael@0: michael@0: nsRect stick; michael@0: nsRect contain; michael@0: ComputeStickyLimits(firstCont, &stick, &contain); michael@0: michael@0: aOuter->SetRect(nscoord_MIN/2, nscoord_MIN/2, nscoord_MAX, nscoord_MAX); michael@0: aInner->SetRect(nscoord_MIN/2, nscoord_MIN/2, nscoord_MAX, nscoord_MAX); michael@0: michael@0: const nsPoint normalPosition = aFrame->GetNormalPosition(); michael@0: michael@0: // Bottom and top michael@0: if (stick.YMost() != nscoord_MAX/2) { michael@0: aOuter->SetTopEdge(contain.y - stick.YMost()); michael@0: aInner->SetTopEdge(normalPosition.y - stick.YMost()); michael@0: } michael@0: michael@0: if (stick.y != nscoord_MIN/2) { michael@0: aInner->SetBottomEdge(normalPosition.y - stick.y); michael@0: aOuter->SetBottomEdge(contain.YMost() - stick.y); michael@0: } michael@0: michael@0: // Right and left michael@0: if (stick.XMost() != nscoord_MAX/2) { michael@0: aOuter->SetLeftEdge(contain.x - stick.XMost()); michael@0: aInner->SetLeftEdge(normalPosition.x - stick.XMost()); michael@0: } michael@0: michael@0: if (stick.x != nscoord_MIN/2) { michael@0: aInner->SetRightEdge(normalPosition.x - stick.x); michael@0: aOuter->SetRightEdge(contain.XMost() - stick.x); michael@0: } michael@0: } michael@0: michael@0: void michael@0: StickyScrollContainer::PositionContinuations(nsIFrame* aFrame) michael@0: { michael@0: NS_ASSERTION(nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(aFrame), michael@0: "Should be starting from the first continuation"); michael@0: nsPoint translation = ComputePosition(aFrame) - aFrame->GetNormalPosition(); michael@0: michael@0: // Move all continuation frames by the same amount. michael@0: for (nsIFrame* cont = aFrame; cont; michael@0: cont = nsLayoutUtils::GetNextContinuationOrIBSplitSibling(cont)) { michael@0: cont->SetPosition(cont->GetNormalPosition() + translation); michael@0: } michael@0: } michael@0: michael@0: void michael@0: StickyScrollContainer::UpdatePositions(nsPoint aScrollPosition, michael@0: nsIFrame* aSubtreeRoot) michael@0: { michael@0: #ifdef DEBUG michael@0: { michael@0: nsIFrame* scrollFrameAsFrame = do_QueryFrame(mScrollFrame); michael@0: NS_ASSERTION(!aSubtreeRoot || aSubtreeRoot == scrollFrameAsFrame, michael@0: "If reflowing, should be reflowing the scroll frame"); michael@0: } michael@0: #endif michael@0: mScrollPosition = aScrollPosition; michael@0: michael@0: OverflowChangedTracker oct; michael@0: oct.SetSubtreeRoot(aSubtreeRoot); michael@0: for (nsTArray::size_type i = 0; i < mFrames.Length(); i++) { michael@0: nsIFrame* f = mFrames[i]; michael@0: if (!nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(f)) { michael@0: // This frame was added in nsFrame::Init before we knew it wasn't michael@0: // the first ib-split-sibling. michael@0: mFrames.RemoveElementAt(i); michael@0: --i; michael@0: continue; michael@0: } michael@0: michael@0: if (aSubtreeRoot) { michael@0: // Reflowing the scroll frame, so recompute offsets. michael@0: ComputeStickyOffsets(f); michael@0: } michael@0: // mFrames will only contain first continuations, because we filter in michael@0: // nsIFrame::Init. michael@0: PositionContinuations(f); michael@0: michael@0: f = f->GetParent(); michael@0: if (f != aSubtreeRoot) { michael@0: for (nsIFrame* cont = f; cont; michael@0: cont = nsLayoutUtils::GetNextContinuationOrIBSplitSibling(cont)) { michael@0: oct.AddFrame(cont, OverflowChangedTracker::CHILDREN_CHANGED); michael@0: } michael@0: } michael@0: } michael@0: oct.Flush(); michael@0: } michael@0: michael@0: void michael@0: StickyScrollContainer::ScrollPositionWillChange(nscoord aX, nscoord aY) michael@0: { michael@0: } michael@0: michael@0: void michael@0: StickyScrollContainer::ScrollPositionDidChange(nscoord aX, nscoord aY) michael@0: { michael@0: UpdatePositions(nsPoint(aX, aY), nullptr); michael@0: } michael@0: michael@0: } // namespace mozilla