/* -*- 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/. */ /* * rendering object that is the root of the frame tree, which contains * the document's scrollbars and contains fixed-positioned elements */ #include "mozilla/ViewportFrame.h" #include "mozilla/ComputedStyleInlines.h" #include "mozilla/PresShell.h" #include "mozilla/ProfilerLabels.h" #include "mozilla/RestyleManager.h" #include "nsGkAtoms.h" #include "nsIScrollableFrame.h" #include "nsAbsoluteContainingBlock.h" #include "nsCanvasFrame.h" #include "nsLayoutUtils.h" #include "nsSubDocumentFrame.h" #include "nsPlaceholderFrame.h" #include "MobileViewportManager.h" using namespace mozilla; typedef nsAbsoluteContainingBlock::AbsPosReflowFlags AbsPosReflowFlags; ViewportFrame* NS_NewViewportFrame(PresShell* aPresShell, ComputedStyle* aStyle) { return new (aPresShell) ViewportFrame(aStyle, aPresShell->GetPresContext()); } NS_IMPL_FRAMEARENA_HELPERS(ViewportFrame) NS_QUERYFRAME_HEAD(ViewportFrame) NS_QUERYFRAME_ENTRY(ViewportFrame) NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame) void ViewportFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, nsIFrame* aPrevInFlow) { nsContainerFrame::Init(aContent, aParent, aPrevInFlow); // No need to call CreateView() here - the frame ctor will call SetView() // with the ViewManager's root view, so we'll assign it in SetViewInternal(). nsIFrame* parent = nsLayoutUtils::GetCrossDocParentFrameInProcess(this); if (parent) { nsFrameState state = parent->GetStateBits(); AddStateBits(state & (NS_FRAME_IN_POPUP)); } } void ViewportFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, const nsDisplayListSet& aLists) { AUTO_PROFILER_LABEL("ViewportFrame::BuildDisplayList", GRAPHICS_DisplayListBuilding); nsIFrame* kid = mFrames.FirstChild(); if (!kid) { return; } nsDisplayListCollection set(aBuilder); BuildDisplayListForChild(aBuilder, kid, set); // If we have a scrollframe then it takes care of creating the display list // for the top layer, but otherwise we need to do it here. if (!kid->IsScrollFrame()) { bool isOpaque = false; if (auto* list = BuildDisplayListForTopLayer(aBuilder, &isOpaque)) { if (isOpaque) { set.DeleteAll(aBuilder); } set.PositionedDescendants()->AppendToTop(list); } } set.MoveTo(aLists); } #ifdef DEBUG /** * Returns whether we are going to put an element in the top layer for * fullscreen. This function should matches the CSS rule in ua.css. */ static bool ShouldInTopLayerForFullscreen(dom::Element* aElement) { return !!aElement->GetParent(); } #endif // DEBUG static void BuildDisplayListForTopLayerFrame(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame, nsDisplayList* aList) { nsRect visible; nsRect dirty; DisplayListClipState::AutoSaveRestore clipState(aBuilder); nsDisplayListBuilder::AutoCurrentActiveScrolledRootSetter asrSetter(aBuilder); nsDisplayListBuilder::OutOfFlowDisplayData* savedOutOfFlowData = nsDisplayListBuilder::GetOutOfFlowData(aFrame); if (savedOutOfFlowData) { visible = savedOutOfFlowData->GetVisibleRectForFrame(aBuilder, aFrame, &dirty); // This function is called after we've finished building display items for // the root scroll frame. That means that the content clip from the root // scroll frame is no longer on aBuilder. However, we need to make sure // that the display items we build in this function have finite clipped // bounds with respect to the root ASR, so we restore the *combined clip* // that we saved earlier. The combined clip will include the clip from the // root scroll frame. clipState.SetClipChainForContainingBlockDescendants( savedOutOfFlowData->mCombinedClipChain); asrSetter.SetCurrentActiveScrolledRoot( savedOutOfFlowData->mContainingBlockActiveScrolledRoot); asrSetter.SetCurrentScrollParentId(savedOutOfFlowData->mScrollParentId); } nsDisplayListBuilder::AutoBuildingDisplayList buildingForChild( aBuilder, aFrame, visible, dirty); nsDisplayList list(aBuilder); aFrame->BuildDisplayListForStackingContext(aBuilder, &list); aList->AppendToTop(&list); } static bool BackdropListIsOpaque(ViewportFrame* aFrame, nsDisplayListBuilder* aBuilder, nsDisplayList* aList) { // The common case for ::backdrop elements on the top layer is a single // fixed position container, holding an opaque background color covering // the whole viewport. if (aList->Length() != 1 || aList->GetTop()->GetType() != DisplayItemType::TYPE_FIXED_POSITION) { return false; } // Make sure the fixed position container isn't clipped or scrollable. nsDisplayFixedPosition* fixed = static_cast(aList->GetTop()); if (fixed->GetActiveScrolledRoot() || fixed->GetClipChain()) { return false; } nsDisplayList* children = fixed->GetChildren(); if (!children->GetTop() || children->GetTop()->GetType() != DisplayItemType::TYPE_BACKGROUND_COLOR) { return false; } nsDisplayBackgroundColor* child = static_cast(children->GetTop()); if (child->GetActiveScrolledRoot() || child->GetClipChain()) { return false; } // Check that the background color is both opaque, and covering the // whole viewport. bool dummy; nsRegion opaque = child->GetOpaqueRegion(aBuilder, &dummy); return opaque.Contains(aFrame->GetRect()); } nsDisplayWrapList* ViewportFrame::BuildDisplayListForTopLayer( nsDisplayListBuilder* aBuilder, bool* aIsOpaque) { nsDisplayList topLayerList(aBuilder); nsTArray topLayer = PresContext()->Document()->GetTopLayer(); for (dom::Element* elem : topLayer) { nsIFrame* frame = elem->GetPrimaryFrame(); if (!frame) { continue; } if (frame->IsHiddenByContentVisibilityOnAnyAncestor( nsIFrame::IncludeContentVisibility::Hidden)) { continue; } // There are two cases where an element in fullscreen is not in // the top layer: // 1. When building display list for purpose other than painting, // it is possible that there is inconsistency between the style // info and the content tree. // 2. This is an element which we are not going to put in the top // layer for fullscreen. See ShouldInTopLayerForFullscreen(). // In both cases, we want to skip the frame here and paint it in // the normal path. if (frame->StyleDisplay()->mTopLayer == StyleTopLayer::None) { MOZ_ASSERT(!aBuilder->IsForPainting() || !ShouldInTopLayerForFullscreen(elem)); continue; } MOZ_ASSERT(ShouldInTopLayerForFullscreen(elem)); // Inner SVG, MathML elements, as well as children of some XUL // elements are not allowed to be out-of-flow. They should not // be handled as top layer element here. if (!frame->HasAnyStateBits(NS_FRAME_OUT_OF_FLOW)) { MOZ_ASSERT(!elem->GetParent()->IsHTMLElement(), "HTML element should always be out-of-flow if in the top " "layer"); continue; } if (nsIFrame* backdropPh = frame->GetChildList(FrameChildListID::Backdrop).FirstChild()) { MOZ_ASSERT(!backdropPh->GetNextSibling(), "more than one ::backdrop?"); MOZ_ASSERT(backdropPh->HasAnyStateBits(NS_FRAME_FIRST_REFLOW), "did you intend to reflow ::backdrop placeholders?"); nsIFrame* backdropFrame = nsPlaceholderFrame::GetRealFrameForPlaceholder(backdropPh); BuildDisplayListForTopLayerFrame(aBuilder, backdropFrame, &topLayerList); if (aIsOpaque) { *aIsOpaque = BackdropListIsOpaque(this, aBuilder, &topLayerList); } } BuildDisplayListForTopLayerFrame(aBuilder, frame, &topLayerList); } if (nsCanvasFrame* canvasFrame = PresShell()->GetCanvasFrame()) { if (dom::Element* container = canvasFrame->GetCustomContentContainer()) { if (nsIFrame* frame = container->GetPrimaryFrame()) { MOZ_ASSERT(frame->StyleDisplay()->mTopLayer != StyleTopLayer::None, "ua.css should ensure this"); MOZ_ASSERT(frame->HasAnyStateBits(NS_FRAME_OUT_OF_FLOW)); BuildDisplayListForTopLayerFrame(aBuilder, frame, &topLayerList); } } } if (topLayerList.IsEmpty()) { return nullptr; } nsPoint offset = aBuilder->GetCurrentFrame()->GetOffsetTo(this); nsDisplayListBuilder::AutoBuildingDisplayList buildingDisplayList( aBuilder, this, aBuilder->GetVisibleRect() + offset, aBuilder->GetDirtyRect() + offset); // Wrap the whole top layer in a single item with maximum z-index, // and append it at the very end, so that it stays at the topmost. nsDisplayWrapList* wrapList = MakeDisplayItemWithIndex( aBuilder, this, 2, &topLayerList, aBuilder->CurrentActiveScrolledRoot(), false); if (!wrapList) { return nullptr; } wrapList->SetOverrideZIndex( std::numeric_limitsZIndex())>::max()); return wrapList; } #ifdef DEBUG void ViewportFrame::AppendFrames(ChildListID aListID, nsFrameList&& aFrameList) { NS_ASSERTION(aListID == FrameChildListID::Principal, "unexpected child list"); NS_ASSERTION(GetChildList(aListID).IsEmpty(), "Shouldn't have any kids!"); nsContainerFrame::AppendFrames(aListID, std::move(aFrameList)); } void ViewportFrame::InsertFrames(ChildListID aListID, nsIFrame* aPrevFrame, const nsLineList::iterator* aPrevFrameLine, nsFrameList&& aFrameList) { NS_ASSERTION(aListID == FrameChildListID::Principal, "unexpected child list"); NS_ASSERTION(GetChildList(aListID).IsEmpty(), "Shouldn't have any kids!"); nsContainerFrame::InsertFrames(aListID, aPrevFrame, aPrevFrameLine, std::move(aFrameList)); } void ViewportFrame::RemoveFrame(DestroyContext& aContext, ChildListID aListID, nsIFrame* aOldFrame) { NS_ASSERTION(aListID == FrameChildListID::Principal, "unexpected child list"); nsContainerFrame::RemoveFrame(aContext, aListID, aOldFrame); } #endif /* virtual */ nscoord ViewportFrame::GetMinISize(gfxContext* aRenderingContext) { nscoord result; DISPLAY_MIN_INLINE_SIZE(this, result); if (mFrames.IsEmpty()) result = 0; else result = mFrames.FirstChild()->GetMinISize(aRenderingContext); return result; } /* virtual */ nscoord ViewportFrame::GetPrefISize(gfxContext* aRenderingContext) { nscoord result; DISPLAY_PREF_INLINE_SIZE(this, result); if (mFrames.IsEmpty()) result = 0; else result = mFrames.FirstChild()->GetPrefISize(aRenderingContext); return result; } nsPoint ViewportFrame::AdjustReflowInputForScrollbars( ReflowInput* aReflowInput) const { // Get our prinicpal child frame and see if we're scrollable nsIFrame* kidFrame = mFrames.FirstChild(); nsIScrollableFrame* scrollingFrame = do_QueryFrame(kidFrame); if (scrollingFrame) { WritingMode wm = aReflowInput->GetWritingMode(); LogicalMargin scrollbars(wm, scrollingFrame->GetActualScrollbarSizes()); aReflowInput->SetComputedISize( aReflowInput->ComputedISize() - scrollbars.IStartEnd(wm), ReflowInput::ResetResizeFlags::No); aReflowInput->SetAvailableISize(aReflowInput->AvailableISize() - scrollbars.IStartEnd(wm)); aReflowInput->SetComputedBSize( aReflowInput->ComputedBSize() - scrollbars.BStartEnd(wm), ReflowInput::ResetResizeFlags::No); return nsPoint(scrollbars.Left(wm), scrollbars.Top(wm)); } return nsPoint(0, 0); } nsRect ViewportFrame::AdjustReflowInputAsContainingBlock( ReflowInput* aReflowInput) const { const nsPoint origin = AdjustReflowInputForScrollbars(aReflowInput); nsRect rect(origin, aReflowInput->ComputedPhysicalSize()); rect.SizeTo(AdjustViewportSizeForFixedPosition(rect)); return rect; } void ViewportFrame::Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize, const ReflowInput& aReflowInput, nsReflowStatus& aStatus) { MarkInReflow(); DO_GLOBAL_REFLOW_COUNT("ViewportFrame"); DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); NS_FRAME_TRACE_REFLOW_IN("ViewportFrame::Reflow"); // Because |Reflow| sets ComputedBSize() on the child to our // ComputedBSize(). AddStateBits(NS_FRAME_CONTAINS_RELATIVE_BSIZE); // Set our size up front, since some parts of reflow depend on it // being already set. Note that the computed height may be // unconstrained; that's ok. Consumers should watch out for that. SetSize(aReflowInput.ComputedPhysicalSize()); // Reflow the main content first so that the placeholders of the // fixed-position frames will be in the right places on an initial // reflow. nscoord kidBSize = 0; WritingMode wm = aReflowInput.GetWritingMode(); if (mFrames.NotEmpty()) { // Deal with a non-incremental reflow or an incremental reflow // targeted at our one-and-only principal child frame. if (aReflowInput.ShouldReflowAllKids() || mFrames.FirstChild()->IsSubtreeDirty()) { // Reflow our one-and-only principal child frame nsIFrame* kidFrame = mFrames.FirstChild(); ReflowOutput kidDesiredSize(aReflowInput); const WritingMode kidWM = kidFrame->GetWritingMode(); LogicalSize availableSpace = aReflowInput.AvailableSize(kidWM); ReflowInput kidReflowInput(aPresContext, aReflowInput, kidFrame, availableSpace); // Reflow the frame kidReflowInput.SetComputedBSize(aReflowInput.ComputedBSize()); ReflowChild(kidFrame, aPresContext, kidDesiredSize, kidReflowInput, 0, 0, ReflowChildFlags::Default, aStatus); kidBSize = kidDesiredSize.BSize(wm); FinishReflowChild(kidFrame, aPresContext, kidDesiredSize, &kidReflowInput, 0, 0, ReflowChildFlags::Default); } else { kidBSize = LogicalSize(wm, mFrames.FirstChild()->GetSize()).BSize(wm); } } NS_ASSERTION(aReflowInput.AvailableISize() != NS_UNCONSTRAINEDSIZE, "shouldn't happen anymore"); // Return the max size as our desired size LogicalSize maxSize(wm, aReflowInput.AvailableISize(), // Being flowed initially at an unconstrained block size // means we should return our child's intrinsic size. aReflowInput.ComputedBSize() != NS_UNCONSTRAINEDSIZE ? aReflowInput.ComputedBSize() : kidBSize); aDesiredSize.SetSize(wm, maxSize); aDesiredSize.SetOverflowAreasToDesiredBounds(); if (HasAbsolutelyPositionedChildren()) { // Make a copy of the reflow input and change the computed width and height // to reflect the available space for the fixed items ReflowInput reflowInput(aReflowInput); if (reflowInput.AvailableBSize() == NS_UNCONSTRAINEDSIZE) { // We have an intrinsic-block-size document with abs-pos/fixed-pos // children. Set the available block-size and computed block-size to our // chosen block-size. reflowInput.SetAvailableBSize(maxSize.BSize(wm)); // Not having border/padding simplifies things NS_ASSERTION(reflowInput.ComputedPhysicalBorderPadding() == nsMargin(), "Viewports can't have border/padding"); reflowInput.SetComputedBSize(maxSize.BSize(wm)); } nsRect rect = AdjustReflowInputAsContainingBlock(&reflowInput); AbsPosReflowFlags flags = AbsPosReflowFlags::CBWidthAndHeightChanged; // XXX could be optimized GetAbsoluteContainingBlock()->Reflow(this, aPresContext, reflowInput, aStatus, rect, flags, /* aOverflowAreas = */ nullptr); } if (mFrames.NotEmpty()) { ConsiderChildOverflow(aDesiredSize.mOverflowAreas, mFrames.FirstChild()); } // If we were dirty then do a repaint if (HasAnyStateBits(NS_FRAME_IS_DIRTY)) { InvalidateFrame(); } // Clipping is handled by the document container (e.g., nsSubDocumentFrame), // so we don't need to change our overflow areas. FinishAndStoreOverflow(&aDesiredSize); NS_FRAME_TRACE_REFLOW_OUT("ViewportFrame::Reflow", aStatus); } void ViewportFrame::UpdateStyle(ServoRestyleState& aRestyleState) { RefPtr newStyle = aRestyleState.StyleSet().ResolveInheritingAnonymousBoxStyle( Style()->GetPseudoType(), nullptr); MOZ_ASSERT(!GetNextContinuation(), "Viewport has continuations?"); SetComputedStyle(newStyle); UpdateStyleOfOwnedAnonBoxes(aRestyleState); } void ViewportFrame::AppendDirectlyOwnedAnonBoxes( nsTArray& aResult) { if (mFrames.NotEmpty()) { aResult.AppendElement(mFrames.FirstChild()); } } nsSize ViewportFrame::AdjustViewportSizeForFixedPosition( const nsRect& aViewportRect) const { nsSize result = aViewportRect.Size(); mozilla::PresShell* presShell = PresShell(); // Layout fixed position elements to the visual viewport size if and only if // it has been set and it is larger than the computed size, otherwise use the // computed size. if (presShell->IsVisualViewportSizeSet()) { if (presShell->GetDynamicToolbarState() == DynamicToolbarState::Collapsed && result < presShell->GetVisualViewportSizeUpdatedByDynamicToolbar()) { // We need to use the viewport size updated by the dynamic toolbar in the // case where the dynamic toolbar is completely hidden. result = presShell->GetVisualViewportSizeUpdatedByDynamicToolbar(); } else if (result < presShell->GetVisualViewportSize()) { result = presShell->GetVisualViewportSize(); } } // Expand the size to the layout viewport size if necessary. const nsSize layoutViewportSize = presShell->GetLayoutViewportSize(); if (result < layoutViewportSize) { result = layoutViewportSize; } return result; } #ifdef DEBUG_FRAME_DUMP nsresult ViewportFrame::GetFrameName(nsAString& aResult) const { return MakeFrameName(u"Viewport"_ns, aResult); } #endif