michael@0: /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 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: #include "APZCCallbackHelper.h" michael@0: #include "gfxPrefs.h" // For gfxPrefs::LayersTilesEnabled, LayersTileWidth/Height michael@0: #include "mozilla/Preferences.h" michael@0: #include "nsIScrollableFrame.h" michael@0: #include "nsLayoutUtils.h" michael@0: #include "nsIDOMElement.h" michael@0: #include "nsIInterfaceRequestorUtils.h" michael@0: michael@0: namespace mozilla { michael@0: namespace layers { michael@0: michael@0: bool michael@0: APZCCallbackHelper::HasValidPresShellId(nsIDOMWindowUtils* aUtils, michael@0: const FrameMetrics& aMetrics) michael@0: { michael@0: MOZ_ASSERT(aUtils); michael@0: michael@0: uint32_t presShellId; michael@0: nsresult rv = aUtils->GetPresShellId(&presShellId); michael@0: MOZ_ASSERT(NS_SUCCEEDED(rv)); michael@0: return NS_SUCCEEDED(rv) && aMetrics.mPresShellId == presShellId; michael@0: } michael@0: michael@0: /** michael@0: * Expands a given rectangle to the next tile boundary. Note, this will michael@0: * expand the rectangle if it is already on tile boundaries. michael@0: */ michael@0: static CSSRect ExpandDisplayPortToTileBoundaries( michael@0: const CSSRect& aDisplayPort, michael@0: const CSSToLayerScale& aLayerPixelsPerCSSPixel) michael@0: { michael@0: // Convert the given rect to layer coordinates so we can inflate to tile michael@0: // boundaries (layer space corresponds to texture pixel space here). michael@0: LayerRect displayPortInLayerSpace = aDisplayPort * aLayerPixelsPerCSSPixel; michael@0: michael@0: // Inflate the rectangle by 1 so that we always push to the next tile michael@0: // boundary. This is desirable to stop from having a rectangle with a michael@0: // moving origin occasionally being smaller when it coincidentally lines michael@0: // up to tile boundaries. michael@0: displayPortInLayerSpace.Inflate(1); michael@0: michael@0: // Now nudge the rectangle to the nearest equal or larger tile boundary. michael@0: int32_t tileWidth = gfxPrefs::LayersTileWidth(); michael@0: int32_t tileHeight = gfxPrefs::LayersTileHeight(); michael@0: gfxFloat left = tileWidth * floor(displayPortInLayerSpace.x / tileWidth); michael@0: gfxFloat right = tileWidth * ceil(displayPortInLayerSpace.XMost() / tileWidth); michael@0: gfxFloat top = tileHeight * floor(displayPortInLayerSpace.y / tileHeight); michael@0: gfxFloat bottom = tileHeight * ceil(displayPortInLayerSpace.YMost() / tileHeight); michael@0: michael@0: displayPortInLayerSpace = LayerRect(left, top, right - left, bottom - top); michael@0: CSSRect displayPort = displayPortInLayerSpace / aLayerPixelsPerCSSPixel; michael@0: michael@0: return displayPort; michael@0: } michael@0: michael@0: static void michael@0: MaybeAlignAndClampDisplayPort(mozilla::layers::FrameMetrics& aFrameMetrics, michael@0: const CSSPoint& aActualScrollOffset) michael@0: { michael@0: // Correct the display-port by the difference between the requested scroll michael@0: // offset and the resulting scroll offset after setting the requested value. michael@0: if (!aFrameMetrics.GetUseDisplayPortMargins()) { michael@0: CSSRect& displayPort = aFrameMetrics.mDisplayPort; michael@0: displayPort += aFrameMetrics.GetScrollOffset() - aActualScrollOffset; michael@0: michael@0: // Expand the display port to the next tile boundaries, if tiled thebes layers michael@0: // are enabled. michael@0: if (gfxPrefs::LayersTilesEnabled()) { michael@0: // We don't use LayersPixelsPerCSSPixel() here as mCumulativeResolution on michael@0: // this FrameMetrics may be incorrect (and is about to be reset by mZoom). michael@0: displayPort = michael@0: ExpandDisplayPortToTileBoundaries(displayPort + aActualScrollOffset, michael@0: aFrameMetrics.GetZoom() * michael@0: ScreenToLayerScale(1.0)) michael@0: - aActualScrollOffset; michael@0: } michael@0: michael@0: // Finally, clamp the display port to the expanded scrollable rect. michael@0: CSSRect scrollableRect = aFrameMetrics.GetExpandedScrollableRect(); michael@0: displayPort = scrollableRect.Intersect(displayPort + aActualScrollOffset) michael@0: - aActualScrollOffset; michael@0: } else { michael@0: LayerPoint shift = michael@0: (aFrameMetrics.GetScrollOffset() - aActualScrollOffset) * michael@0: aFrameMetrics.LayersPixelsPerCSSPixel(); michael@0: LayerMargin margins = aFrameMetrics.GetDisplayPortMargins(); michael@0: margins.left -= shift.x; michael@0: margins.right += shift.x; michael@0: margins.top -= shift.y; michael@0: margins.bottom += shift.y; michael@0: aFrameMetrics.SetDisplayPortMargins(margins); michael@0: } michael@0: } michael@0: michael@0: static void michael@0: RecenterDisplayPort(mozilla::layers::FrameMetrics& aFrameMetrics) michael@0: { michael@0: if (!aFrameMetrics.GetUseDisplayPortMargins()) { michael@0: CSSSize compositionSize = aFrameMetrics.CalculateCompositedSizeInCssPixels(); michael@0: aFrameMetrics.mDisplayPort.x = (compositionSize.width - aFrameMetrics.mDisplayPort.width) / 2; michael@0: aFrameMetrics.mDisplayPort.y = (compositionSize.height - aFrameMetrics.mDisplayPort.height) / 2; michael@0: } else { michael@0: LayerMargin margins = aFrameMetrics.GetDisplayPortMargins(); michael@0: margins.right = margins.left = margins.LeftRight() / 2; michael@0: margins.top = margins.bottom = margins.TopBottom() / 2; michael@0: aFrameMetrics.SetDisplayPortMargins(margins); michael@0: } michael@0: } michael@0: michael@0: static CSSPoint michael@0: ScrollFrameTo(nsIScrollableFrame* aFrame, const CSSPoint& aPoint, bool& aSuccessOut) michael@0: { michael@0: aSuccessOut = false; michael@0: michael@0: if (!aFrame) { michael@0: return aPoint; michael@0: } michael@0: michael@0: CSSPoint targetScrollPosition = aPoint; michael@0: michael@0: // If the frame is overflow:hidden on a particular axis, we don't want to allow michael@0: // user-driven scroll on that axis. Simply set the scroll position on that axis michael@0: // to whatever it already is. Note that this will leave the APZ's async scroll michael@0: // position out of sync with the gecko scroll position, but APZ can deal with that michael@0: // (by design). Note also that when we run into this case, even if both axes michael@0: // have overflow:hidden, we want to set aSuccessOut to true, so that the displayport michael@0: // follows the async scroll position rather than the gecko scroll position. michael@0: CSSPoint geckoScrollPosition = CSSPoint::FromAppUnits(aFrame->GetScrollPosition()); michael@0: if (aFrame->GetScrollbarStyles().mVertical == NS_STYLE_OVERFLOW_HIDDEN) { michael@0: targetScrollPosition.y = geckoScrollPosition.y; michael@0: } michael@0: if (aFrame->GetScrollbarStyles().mHorizontal == NS_STYLE_OVERFLOW_HIDDEN) { michael@0: targetScrollPosition.x = geckoScrollPosition.x; michael@0: } michael@0: michael@0: // If the scrollable frame is currently in the middle of an async or smooth michael@0: // scroll then we don't want to interrupt it (see bug 961280). michael@0: // Also if the scrollable frame got a scroll request from something other than us michael@0: // since the last layers update, then we don't want to push our scroll request michael@0: // because we'll clobber that one, which is bad. michael@0: if (!aFrame->IsProcessingAsyncScroll() && michael@0: (!aFrame->OriginOfLastScroll() || aFrame->OriginOfLastScroll() == nsGkAtoms::apz)) { michael@0: aFrame->ScrollToCSSPixelsApproximate(targetScrollPosition, nsGkAtoms::apz); michael@0: geckoScrollPosition = CSSPoint::FromAppUnits(aFrame->GetScrollPosition()); michael@0: aSuccessOut = true; michael@0: } michael@0: // Return the final scroll position after setting it so that anything that relies michael@0: // on it can have an accurate value. Note that even if we set it above re-querying it michael@0: // is a good idea because it may have gotten clamped or rounded. michael@0: return geckoScrollPosition; michael@0: } michael@0: michael@0: void michael@0: APZCCallbackHelper::UpdateRootFrame(nsIDOMWindowUtils* aUtils, michael@0: FrameMetrics& aMetrics) michael@0: { michael@0: // Precondition checks michael@0: MOZ_ASSERT(aUtils); michael@0: if (aMetrics.GetScrollId() == FrameMetrics::NULL_SCROLL_ID) { michael@0: return; michael@0: } michael@0: michael@0: // Set the scroll port size, which determines the scroll range. For example if michael@0: // a 500-pixel document is shown in a 100-pixel frame, the scroll port length would michael@0: // be 100, and gecko would limit the maximum scroll offset to 400 (so as to prevent michael@0: // overscroll). Note that if the content here was zoomed to 2x, the document would michael@0: // be 1000 pixels long but the frame would still be 100 pixels, and so the maximum michael@0: // scroll range would be 900. Therefore this calculation depends on the zoom applied michael@0: // to the content relative to the container. michael@0: CSSSize scrollPort = aMetrics.CalculateCompositedSizeInCssPixels(); michael@0: aUtils->SetScrollPositionClampingScrollPortSize(scrollPort.width, scrollPort.height); michael@0: michael@0: // Scroll the window to the desired spot michael@0: nsIScrollableFrame* sf = nsLayoutUtils::FindScrollableFrameFor(aMetrics.GetScrollId()); michael@0: bool scrollUpdated = false; michael@0: CSSPoint actualScrollOffset = ScrollFrameTo(sf, aMetrics.GetScrollOffset(), scrollUpdated); michael@0: michael@0: if (!scrollUpdated) { michael@0: // For whatever reason we couldn't update the scroll offset on the scroll frame, michael@0: // which means the data APZ used for its displayport calculation is stale. Fall michael@0: // back to a sane default behaviour. Note that we don't tile-align the recentered michael@0: // displayport because tile-alignment depends on the scroll position, and the michael@0: // scroll position here is out of our control. See bug 966507 comment 21 for a michael@0: // more detailed explanation. michael@0: RecenterDisplayPort(aMetrics); michael@0: } michael@0: michael@0: // Correct the display port due to the difference between mScrollOffset and the michael@0: // actual scroll offset, possibly align it to tile boundaries (if tiled layers are michael@0: // enabled), and clamp it to the scrollable rect. michael@0: MaybeAlignAndClampDisplayPort(aMetrics, actualScrollOffset); michael@0: michael@0: aMetrics.SetScrollOffset(actualScrollOffset); michael@0: michael@0: // The mZoom variable on the frame metrics stores the CSS-to-screen scale for this michael@0: // frame. This scale includes all of the (cumulative) resolutions set on the presShells michael@0: // from the root down to this frame. However, when setting the resolution, we only michael@0: // want the piece of the resolution that corresponds to this presShell, rather than michael@0: // all of the cumulative stuff, so we need to divide out the parent resolutions. michael@0: // Finally, we multiply by a ScreenToLayerScale of 1.0f because the goal here is to michael@0: // take the async zoom calculated by the APZC and tell gecko about it (turning it into michael@0: // a "sync" zoom) which will update the resolution at which the layer is painted. michael@0: ParentLayerToLayerScale presShellResolution = michael@0: aMetrics.GetZoom() michael@0: / aMetrics.mDevPixelsPerCSSPixel michael@0: / aMetrics.GetParentResolution() michael@0: * ScreenToLayerScale(1.0f); michael@0: aUtils->SetResolution(presShellResolution.scale, presShellResolution.scale); michael@0: michael@0: // Finally, we set the displayport. michael@0: nsCOMPtr content = nsLayoutUtils::FindContentFor(aMetrics.GetScrollId()); michael@0: if (!content) { michael@0: return; michael@0: } michael@0: nsCOMPtr element = do_QueryInterface(content); michael@0: if (!element) { michael@0: return; michael@0: } michael@0: if (!aMetrics.GetUseDisplayPortMargins()) { michael@0: aUtils->SetDisplayPortForElement(aMetrics.mDisplayPort.x, michael@0: aMetrics.mDisplayPort.y, michael@0: aMetrics.mDisplayPort.width, michael@0: aMetrics.mDisplayPort.height, michael@0: element, 0); michael@0: } else { michael@0: gfx::IntSize alignment = gfxPrefs::LayersTilesEnabled() michael@0: ? gfx::IntSize(gfxPrefs::LayersTileWidth(), gfxPrefs::LayersTileHeight()) : michael@0: gfx::IntSize(0, 0); michael@0: LayerMargin margins = aMetrics.GetDisplayPortMargins(); michael@0: aUtils->SetDisplayPortMarginsForElement(margins.left, michael@0: margins.top, michael@0: margins.right, michael@0: margins.bottom, michael@0: alignment.width, michael@0: alignment.height, michael@0: element, 0); michael@0: CSSRect baseCSS = aMetrics.mCompositionBounds / aMetrics.GetZoomToParent(); michael@0: nsRect base(baseCSS.x * nsPresContext::AppUnitsPerCSSPixel(), michael@0: baseCSS.y * nsPresContext::AppUnitsPerCSSPixel(), michael@0: baseCSS.width * nsPresContext::AppUnitsPerCSSPixel(), michael@0: baseCSS.height * nsPresContext::AppUnitsPerCSSPixel()); michael@0: nsLayoutUtils::SetDisplayPortBaseIfNotSet(content, base); michael@0: } michael@0: } michael@0: michael@0: void michael@0: APZCCallbackHelper::UpdateSubFrame(nsIContent* aContent, michael@0: FrameMetrics& aMetrics) michael@0: { michael@0: // Precondition checks michael@0: MOZ_ASSERT(aContent); michael@0: if (aMetrics.GetScrollId() == FrameMetrics::NULL_SCROLL_ID) { michael@0: return; michael@0: } michael@0: michael@0: nsCOMPtr utils = GetDOMWindowUtils(aContent); michael@0: if (!utils) { michael@0: return; michael@0: } michael@0: michael@0: // We currently do not support zooming arbitrary subframes. They can only michael@0: // be scrolled, so here we only have to set the scroll position and displayport. michael@0: michael@0: nsIScrollableFrame* sf = nsLayoutUtils::FindScrollableFrameFor(aMetrics.GetScrollId()); michael@0: bool scrollUpdated = false; michael@0: CSSPoint actualScrollOffset = ScrollFrameTo(sf, aMetrics.GetScrollOffset(), scrollUpdated); michael@0: michael@0: nsCOMPtr element = do_QueryInterface(aContent); michael@0: if (element) { michael@0: if (!scrollUpdated) { michael@0: RecenterDisplayPort(aMetrics); michael@0: } michael@0: MaybeAlignAndClampDisplayPort(aMetrics, actualScrollOffset); michael@0: if (!aMetrics.GetUseDisplayPortMargins()) { michael@0: utils->SetDisplayPortForElement(aMetrics.mDisplayPort.x, michael@0: aMetrics.mDisplayPort.y, michael@0: aMetrics.mDisplayPort.width, michael@0: aMetrics.mDisplayPort.height, michael@0: element, 0); michael@0: } else { michael@0: gfx::IntSize alignment = gfxPrefs::LayersTilesEnabled() michael@0: ? gfx::IntSize(gfxPrefs::LayersTileWidth(), gfxPrefs::LayersTileHeight()) : michael@0: gfx::IntSize(0, 0); michael@0: LayerMargin margins = aMetrics.GetDisplayPortMargins(); michael@0: utils->SetDisplayPortMarginsForElement(margins.left, michael@0: margins.top, michael@0: margins.right, michael@0: margins.bottom, michael@0: alignment.width, michael@0: alignment.height, michael@0: element, 0); michael@0: CSSRect baseCSS = aMetrics.mCompositionBounds / aMetrics.GetZoomToParent(); michael@0: nsRect base(baseCSS.x * nsPresContext::AppUnitsPerCSSPixel(), michael@0: baseCSS.y * nsPresContext::AppUnitsPerCSSPixel(), michael@0: baseCSS.width * nsPresContext::AppUnitsPerCSSPixel(), michael@0: baseCSS.height * nsPresContext::AppUnitsPerCSSPixel()); michael@0: nsLayoutUtils::SetDisplayPortBaseIfNotSet(aContent, base); michael@0: } michael@0: } michael@0: michael@0: aMetrics.SetScrollOffset(actualScrollOffset); michael@0: } michael@0: michael@0: already_AddRefed michael@0: APZCCallbackHelper::GetDOMWindowUtils(const nsIDocument* aDoc) michael@0: { michael@0: nsCOMPtr utils; michael@0: nsCOMPtr window = aDoc->GetDefaultView(); michael@0: if (window) { michael@0: utils = do_GetInterface(window); michael@0: } michael@0: return utils.forget(); michael@0: } michael@0: michael@0: already_AddRefed michael@0: APZCCallbackHelper::GetDOMWindowUtils(const nsIContent* aContent) michael@0: { michael@0: nsCOMPtr utils; michael@0: nsIDocument* doc = aContent->GetCurrentDoc(); michael@0: if (doc) { michael@0: utils = GetDOMWindowUtils(doc); michael@0: } michael@0: return utils.forget(); michael@0: } michael@0: michael@0: bool michael@0: APZCCallbackHelper::GetScrollIdentifiers(const nsIContent* aContent, michael@0: uint32_t* aPresShellIdOut, michael@0: FrameMetrics::ViewID* aViewIdOut) michael@0: { michael@0: if (!aContent || !nsLayoutUtils::FindIDFor(aContent, aViewIdOut)) { michael@0: return false; michael@0: } michael@0: nsCOMPtr utils = GetDOMWindowUtils(aContent); michael@0: return utils && (utils->GetPresShellId(aPresShellIdOut) == NS_OK); michael@0: } michael@0: michael@0: class AcknowledgeScrollUpdateEvent : public nsRunnable michael@0: { michael@0: typedef mozilla::layers::FrameMetrics::ViewID ViewID; michael@0: michael@0: public: michael@0: AcknowledgeScrollUpdateEvent(const ViewID& aScrollId, const uint32_t& aScrollGeneration) michael@0: : mScrollId(aScrollId) michael@0: , mScrollGeneration(aScrollGeneration) michael@0: { michael@0: } michael@0: michael@0: NS_IMETHOD Run() { michael@0: MOZ_ASSERT(NS_IsMainThread()); michael@0: michael@0: nsIScrollableFrame* sf = nsLayoutUtils::FindScrollableFrameFor(mScrollId); michael@0: if (sf) { michael@0: sf->ResetOriginIfScrollAtGeneration(mScrollGeneration); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: protected: michael@0: ViewID mScrollId; michael@0: uint32_t mScrollGeneration; michael@0: }; michael@0: michael@0: void michael@0: APZCCallbackHelper::AcknowledgeScrollUpdate(const FrameMetrics::ViewID& aScrollId, michael@0: const uint32_t& aScrollGeneration) michael@0: { michael@0: nsCOMPtr r1 = new AcknowledgeScrollUpdateEvent(aScrollId, aScrollGeneration); michael@0: if (!NS_IsMainThread()) { michael@0: NS_DispatchToMainThread(r1); michael@0: } else { michael@0: r1->Run(); michael@0: } michael@0: } michael@0: michael@0: void michael@0: APZCCallbackHelper::UpdateCallbackTransform(const FrameMetrics& aApzcMetrics, const FrameMetrics& aActualMetrics) michael@0: { michael@0: nsCOMPtr content = nsLayoutUtils::FindContentFor(aApzcMetrics.GetScrollId()); michael@0: if (!content) { michael@0: return; michael@0: } michael@0: CSSPoint scrollDelta = aApzcMetrics.GetScrollOffset() - aActualMetrics.GetScrollOffset(); michael@0: content->SetProperty(nsGkAtoms::apzCallbackTransform, new CSSPoint(scrollDelta), michael@0: nsINode::DeleteProperty); michael@0: } michael@0: michael@0: CSSPoint michael@0: APZCCallbackHelper::ApplyCallbackTransform(const CSSPoint& aInput, const ScrollableLayerGuid& aGuid) michael@0: { michael@0: // XXX: technically we need to walk all the way up the layer tree from the layer michael@0: // represented by |aGuid.mScrollId| up to the root of the layer tree and apply michael@0: // the input transforms at each level in turn. However, it is quite difficult michael@0: // to do this given that the structure of the layer tree may be different from michael@0: // the structure of the content tree. Also it may be impossible to do correctly michael@0: // at this point because there are other CSS transforms and such interleaved in michael@0: // between so applying the inputTransforms all in a row at the end may leave michael@0: // some things transformed improperly. In practice we should rarely hit scenarios michael@0: // where any of this matters, so I'm skipping it for now and just doing the single michael@0: // transform for the layer that the input hit. michael@0: michael@0: if (aGuid.mScrollId != FrameMetrics::NULL_SCROLL_ID) { michael@0: nsCOMPtr content = nsLayoutUtils::FindContentFor(aGuid.mScrollId); michael@0: if (content) { michael@0: void* property = content->GetProperty(nsGkAtoms::apzCallbackTransform); michael@0: if (property) { michael@0: CSSPoint delta = (*static_cast(property)); michael@0: return aInput + delta; michael@0: } michael@0: } michael@0: } michael@0: return aInput; michael@0: } michael@0: michael@0: nsIntPoint michael@0: APZCCallbackHelper::ApplyCallbackTransform(const nsIntPoint& aPoint, michael@0: const ScrollableLayerGuid& aGuid, michael@0: const CSSToLayoutDeviceScale& aScale) michael@0: { michael@0: LayoutDevicePoint point = LayoutDevicePoint(aPoint.x, aPoint.y); michael@0: point = ApplyCallbackTransform(point / aScale, aGuid) * aScale; michael@0: LayoutDeviceIntPoint ret = gfx::RoundedToInt(point); michael@0: return nsIntPoint(ret.x, ret.y); michael@0: } michael@0: michael@0: } michael@0: }