From 7c2c79296ff59a43e63580d9946ee301c60f7155 Mon Sep 17 00:00:00 2001 From: Botond Ballo Date: Mon, 27 Jul 2015 14:07:58 -0400 Subject: [PATCH] Bug 1131359 - Port the double-tap-to-zoom functionality of BrowserElementPanning.js to C++. r=kats --- dom/ipc/TabChild.cpp | 77 ++-------- gfx/layers/apz/util/DoubleTapToZoom.cpp | 178 ++++++++++++++++++++++++ gfx/layers/apz/util/DoubleTapToZoom.h | 29 ++++ gfx/layers/moz.build | 3 + layout/base/Units.h | 5 + 5 files changed, 229 insertions(+), 63 deletions(-) create mode 100644 gfx/layers/apz/util/DoubleTapToZoom.cpp create mode 100644 gfx/layers/apz/util/DoubleTapToZoom.h diff --git a/dom/ipc/TabChild.cpp b/dom/ipc/TabChild.cpp index 2e0c8a25603..85003d8423d 100644 --- a/dom/ipc/TabChild.cpp +++ b/dom/ipc/TabChild.cpp @@ -27,6 +27,7 @@ #include "mozilla/layers/APZCTreeManager.h" #include "mozilla/layers/APZEventState.h" #include "mozilla/layers/CompositorChild.h" +#include "mozilla/layers/DoubleTapToZoom.h" #include "mozilla/layers/ImageBridgeChild.h" #include "mozilla/layers/ShadowLayers.h" #include "mozilla/layout/RenderFrameChild.h" @@ -116,7 +117,6 @@ NS_IMPL_ISUPPORTS(ContentListener, nsIDOMEventListener) static const CSSSize kDefaultViewportSize(980, 480); -static const char BROWSER_ZOOM_TO_RECT[] = "browser-zoom-to-rect"; static const char BEFORE_FIRST_PAINT[] = "before-first-paint"; typedef nsDataHashtable TabChildMap; @@ -283,39 +283,6 @@ TabChildBase::ProcessUpdateFrame(const FrameMetrics& aFrameMetrics) FrameMetrics newMetrics = aFrameMetrics; APZCCallbackHelper::UpdateRootFrame(newMetrics); - - CSSSize cssCompositedSize = newMetrics.CalculateCompositedSizeInCssPixels(); - // The BrowserElementScrolling helper must know about these updated metrics - // for other functions it performs, such as double tap handling. - // Note, %f must not be used because it is locale specific! - nsString data; - data.AppendPrintf("{ \"x\" : %d", NS_lround(newMetrics.GetScrollOffset().x)); - data.AppendPrintf(", \"y\" : %d", NS_lround(newMetrics.GetScrollOffset().y)); - data.AppendLiteral(", \"viewport\" : "); - data.AppendLiteral("{ \"width\" : "); - data.AppendFloat(newMetrics.GetViewport().width); - data.AppendLiteral(", \"height\" : "); - data.AppendFloat(newMetrics.GetViewport().height); - data.AppendLiteral(" }"); - data.AppendLiteral(", \"cssPageRect\" : "); - data.AppendLiteral("{ \"x\" : "); - data.AppendFloat(newMetrics.GetScrollableRect().x); - data.AppendLiteral(", \"y\" : "); - data.AppendFloat(newMetrics.GetScrollableRect().y); - data.AppendLiteral(", \"width\" : "); - data.AppendFloat(newMetrics.GetScrollableRect().width); - data.AppendLiteral(", \"height\" : "); - data.AppendFloat(newMetrics.GetScrollableRect().height); - data.AppendLiteral(" }"); - data.AppendLiteral(", \"cssCompositedRect\" : "); - data.AppendLiteral("{ \"width\" : "); - data.AppendFloat(cssCompositedSize.width); - data.AppendLiteral(", \"height\" : "); - data.AppendFloat(cssCompositedSize.height); - data.AppendLiteral(" }"); - data.AppendLiteral(" }"); - - DispatchMessageManagerMessage(NS_LITERAL_STRING("Viewport:Change"), data); } NS_IMETHODIMP @@ -638,23 +605,7 @@ TabChild::Observe(nsISupports *aSubject, const char *aTopic, const char16_t *aData) { - if (!strcmp(aTopic, BROWSER_ZOOM_TO_RECT)) { - nsCOMPtr docShell(do_QueryInterface(aSubject)); - nsCOMPtr tabChild(TabChild::GetFrom(docShell)); - if (tabChild == this) { - nsCOMPtr doc(GetDocument()); - uint32_t presShellId; - ViewID viewId; - if (APZCCallbackHelper::GetOrCreateScrollIdentifiers(doc->GetDocumentElement(), - &presShellId, &viewId)) { - CSSRect rect; - sscanf(NS_ConvertUTF16toUTF8(aData).get(), - "{\"x\":%f,\"y\":%f,\"w\":%f,\"h\":%f}", - &rect.x, &rect.y, &rect.width, &rect.height); - SendZoomToRect(presShellId, viewId, rect); - } - } - } else if (!strcmp(aTopic, BEFORE_FIRST_PAINT)) { + if (!strcmp(aTopic, BEFORE_FIRST_PAINT)) { if (AsyncPanZoomEnabled()) { nsCOMPtr subject(do_QueryInterface(aSubject)); nsCOMPtr doc(GetDocument()); @@ -1764,14 +1715,18 @@ TabChild::RecvHandleDoubleTap(const CSSPoint& aPoint, const Modifiers& aModifier // Note: there is nothing to do with the modifiers here, as we are not // synthesizing any sort of mouse event. CSSPoint point = APZCCallbackHelper::ApplyCallbackTransform(aPoint, aGuid); - nsString data; - data.AppendLiteral("{ \"x\" : "); - data.AppendFloat(point.x); - data.AppendLiteral(", \"y\" : "); - data.AppendFloat(point.y); - data.AppendLiteral(" }"); - - DispatchMessageManagerMessage(NS_LITERAL_STRING("Gesture:DoubleTap"), data); + nsCOMPtr document = GetDocument(); + CSSRect zoomToRect = CalculateRectToZoomTo(document, point); + // The double-tap can be dispatched by any scroll frame (so |aGuid| could be + // the guid of any scroll frame), but the zoom-to-rect operation must be + // performed by the root content scroll frame, so query its identifiers + // for the SendZoomToRect() call rather than using the ones from |aGuid|. + uint32_t presShellId; + ViewID viewId; + if (APZCCallbackHelper::GetOrCreateScrollIdentifiers( + document->GetDocumentElement(), &presShellId, &viewId)) { + SendZoomToRect(presShellId, viewId, zoomToRect); + } return true; } @@ -2454,7 +2409,6 @@ TabChild::RecvDestroy() nsCOMPtr observerService = mozilla::services::GetObserverService(); - observerService->RemoveObserver(this, BROWSER_ZOOM_TO_RECT); observerService->RemoveObserver(this, BEFORE_FIRST_PAINT); const nsAttrValue::EnumTable* table = @@ -2656,9 +2610,6 @@ TabChild::InitRenderingState(const TextureFactoryIdentifier& aTextureFactoryIden mozilla::services::GetObserverService(); if (observerService) { - observerService->AddObserver(this, - BROWSER_ZOOM_TO_RECT, - false); observerService->AddObserver(this, BEFORE_FIRST_PAINT, false); diff --git a/gfx/layers/apz/util/DoubleTapToZoom.cpp b/gfx/layers/apz/util/DoubleTapToZoom.cpp new file mode 100644 index 00000000000..9665bb1876f --- /dev/null +++ b/gfx/layers/apz/util/DoubleTapToZoom.cpp @@ -0,0 +1,178 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#include "DoubleTapToZoom.h" + +#include // for std::min, std::max + +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/dom/Element.h" +#include "nsCOMPtr.h" +#include "nsIContent.h" +#include "nsIDocument.h" +#include "nsIDOMHTMLLIElement.h" +#include "nsIDOMHTMLQuoteElement.h" +#include "nsIDOMWindow.h" +#include "nsIFrame.h" +#include "nsIFrameInlines.h" +#include "nsIPresShell.h" +#include "nsLayoutUtils.h" +#include "nsStyleConsts.h" + +namespace mozilla { +namespace layers { + +// Returns the DOM element found at |aPoint|, interpreted as being relative to +// the root frame of |aShell|. If the point is inside a subdocument, returns +// an element inside the subdocument, rather than the subdocument element +// (and does so recursively). +// The implementation was adapted from nsDocument::ElementFromPoint(), with +// the notable exception that we don't pass nsLayoutUtils::IGNORE_CROSS_DOC +// to GetFrameForPoint(), so as to get the behaviour described above in the +// presence of subdocuments. +static already_AddRefed +ElementFromPoint(const nsCOMPtr& aShell, + const CSSPoint& aPoint) +{ + if (nsIFrame* rootFrame = aShell->GetRootFrame()) { + if (nsIFrame* frame = nsLayoutUtils::GetFrameForPoint(rootFrame, + CSSPoint::ToAppUnits(aPoint), + nsLayoutUtils::IGNORE_PAINT_SUPPRESSION | + nsLayoutUtils::IGNORE_ROOT_SCROLL_FRAME)) { + while (frame && (!frame->GetContent() || frame->GetContent()->IsInAnonymousSubtree())) { + frame = nsLayoutUtils::GetParentOrPlaceholderFor(frame); + } + nsIContent* content = frame->GetContent(); + if (content && !content->IsElement()) { + content = content->GetParent(); + } + if (content) { + nsCOMPtr result = content->AsElement(); + return result.forget(); + } + } + } + return nullptr; +} + +static bool +ShouldZoomToElement(const nsCOMPtr& aElement) { + if (nsIFrame* frame = aElement->GetPrimaryFrame()) { + if (frame->GetDisplay() == NS_STYLE_DISPLAY_INLINE) { + return false; + } + } + if (aElement->IsAnyOfHTMLElements(nsGkAtoms::li, nsGkAtoms::q)) { + return false; + } + return true; +} + +// Calculate the bounding rect of |aElement|, relative to the origin +// of the document associated with |aShell|. +// |aRootScrollFrame| should be the root scroll frame of the document in +// question. +// The implementation is adapted from Element::GetBoundingClientRect(). +static CSSRect +GetBoundingContentRect(const nsCOMPtr& aShell, + const nsCOMPtr& aElement, + const nsIScrollableFrame* aRootScrollFrame) { + if (nsIFrame* frame = aElement->GetPrimaryFrame()) { + return CSSRect::FromAppUnits( + nsLayoutUtils::GetAllInFlowRectsUnion( + frame, + aShell->GetRootFrame(), + nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS) + + aRootScrollFrame->GetScrollPosition()); + } + return CSSRect(); +} + +static bool +IsRectZoomedIn(const CSSRect& aRect, const CSSRect& aCompositedArea) +{ + // This functions checks to see if the area of the rect visible in the + // composition bounds (i.e. the overlapArea variable below) is approximately + // the max area of the rect we can show. + CSSRect overlap = aCompositedArea.Intersect(aRect); + float overlapArea = overlap.width * overlap.height; + float availHeight = std::min(aRect.width * aCompositedArea.height / aCompositedArea.width, + aRect.height); + float showing = overlapArea / (aRect.width * availHeight); + float ratioW = aRect.width / aCompositedArea.width; + float ratioH = aRect.height / aCompositedArea.height; + + return showing > 0.9 && (ratioW > 0.9 || ratioH > 0.9); +} + +CSSRect +CalculateRectToZoomTo(const nsCOMPtr& aRootContentDocument, + const CSSPoint& aPoint) +{ + // Ensure the layout information we get is up-to-date. + aRootContentDocument->FlushPendingNotifications(Flush_Layout); + + // An empty rect as return value is interpreted as "zoom out". + const CSSRect zoomOut; + + nsCOMPtr shell = aRootContentDocument->GetShell(); + if (!shell) { + return zoomOut; + } + + nsIScrollableFrame* rootScrollFrame = shell->GetRootScrollFrameAsScrollable(); + if (!rootScrollFrame) { + return zoomOut; + } + + nsCOMPtr element = ElementFromPoint(shell, aPoint); + if (!element) { + return zoomOut; + } + + while (element && !ShouldZoomToElement(element)) { + element = element->GetParentElement(); + } + + if (!element) { + return zoomOut; + } + + FrameMetrics metrics = nsLayoutUtils::CalculateBasicFrameMetrics(rootScrollFrame); + CSSRect compositedArea(metrics.GetScrollOffset(), metrics.CalculateCompositedSizeInCssPixels()); + const CSSCoord margin = 15; + CSSRect rect = GetBoundingContentRect(shell, element, rootScrollFrame); + rect = CSSRect(std::max(metrics.GetScrollableRect().x, rect.x - margin), + rect.y, + rect.width + 2 * margin, + rect.height); + // Constrict the rect to the screen's right edge + rect.width = std::min(rect.width, metrics.GetScrollableRect().XMost() - rect.x); + + // If the rect is already taking up most of the visible area and is + // stretching the width of the page, then we want to zoom out instead. + if (IsRectZoomedIn(rect, compositedArea)) { + return zoomOut; + } + + CSSRect rounded(rect); + rounded.Round(); + + // If the block we're zooming to is really tall, and the user double-tapped + // more than a screenful of height from the top of it, then adjust the + // y-coordinate so that we center the actual point the user double-tapped + // upon. This prevents flying to the top of the page when double-tapping + // to zoom in (bug 761721). The 1.2 multiplier is just a little fuzz to + // compensate for 'rect' including horizontal margins but not vertical ones. + CSSCoord cssTapY = metrics.GetScrollOffset().y + aPoint.y; + if ((rect.height > rounded.height) && (cssTapY > rounded.y + (rounded.height * 1.2))) { + rounded.y = cssTapY - (rounded.height / 2); + } + + return rounded; +} + +} +} diff --git a/gfx/layers/apz/util/DoubleTapToZoom.h b/gfx/layers/apz/util/DoubleTapToZoom.h new file mode 100644 index 00000000000..7b8723865c1 --- /dev/null +++ b/gfx/layers/apz/util/DoubleTapToZoom.h @@ -0,0 +1,29 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozilla_layers_DoubleTapToZoom_h +#define mozilla_layers_DoubleTapToZoom_h + +#include "Units.h" + +class nsIDocument; +template class nsCOMPtr; + +namespace mozilla { +namespace layers { + +/** + * For a double tap at |aPoint|, return the rect to which the browser + * should zoom in response, or an empty rect if the browser should zoom out. + * |aDocument| should be the root content document for the content that was + * tapped. + */ +CSSRect CalculateRectToZoomTo(const nsCOMPtr& aRootContentDocument, + const CSSPoint& aPoint); + +} +} + +#endif /* mozilla_layers_DoubleTapToZoom_h */ diff --git a/gfx/layers/moz.build b/gfx/layers/moz.build index af9b1624e9f..7f38135e54e 100644 --- a/gfx/layers/moz.build +++ b/gfx/layers/moz.build @@ -104,6 +104,7 @@ EXPORTS.mozilla.layers += [ 'apz/util/APZEventState.h', 'apz/util/APZThreadUtils.h', 'apz/util/ChromeProcessController.h', + 'apz/util/DoubleTapToZoom.h', 'apz/util/InputAPZContext.h', 'AtomicRefCountedWithFinalize.h', 'AxisPhysicsModel.h', @@ -247,6 +248,7 @@ UNIFIED_SOURCES += [ 'apz/util/APZEventState.cpp', 'apz/util/APZThreadUtils.cpp', 'apz/util/ChromeProcessController.cpp', + 'apz/util/DoubleTapToZoom.cpp', 'apz/util/InputAPZContext.cpp', 'AxisPhysicsModel.cpp', 'AxisPhysicsMSDModel.cpp', @@ -384,6 +386,7 @@ include('/ipc/chromium/chromium-config.mozbuild') LOCAL_INCLUDES += [ '/docshell/base', # for nsDocShell.h '/layout/base', # for TouchManager.h + '/layout/generic', # for nsTextFrame.h ] FINAL_LIBRARY = 'xul' diff --git a/layout/base/Units.h b/layout/base/Units.h index d40ad4bbb3b..85fac335b3e 100644 --- a/layout/base/Units.h +++ b/layout/base/Units.h @@ -164,6 +164,11 @@ struct CSSPixel { NSAppUnitsToFloatPixels(aPoint.y, float(AppUnitsPerCSSPixel()))); } + static CSSSize FromAppUnits(const nsSize& aSize) { + return CSSSize(NSAppUnitsToFloatPixels(aSize.width, float(AppUnitsPerCSSPixel())), + NSAppUnitsToFloatPixels(aSize.height, float(AppUnitsPerCSSPixel()))); + } + static CSSRect FromAppUnits(const nsRect& aRect) { return CSSRect(NSAppUnitsToFloatPixels(aRect.x, float(AppUnitsPerCSSPixel())), NSAppUnitsToFloatPixels(aRect.y, float(AppUnitsPerCSSPixel())),