Bug 1131359 - Port the double-tap-to-zoom functionality of BrowserElementPanning.js to C++. r=kats
authorBotond Ballo <botond@mozilla.com>
Mon, 27 Jul 2015 14:07:58 -0400
changeset 286913 4b0ef3b98b72dad40666062030043d10cac5e267
parent 286912 f2fc1cd10a395aacb222515ee75e4b031925e2b1
child 286914 0d65dafbdce5b801b0009807d2b4b23ce7e0d91c
push id5067
push userraliiev@mozilla.com
push dateMon, 21 Sep 2015 14:04:52 +0000
treeherdermozilla-beta@14221ffe5b2f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskats
bugs1131359
milestone42.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1131359 - Port the double-tap-to-zoom functionality of BrowserElementPanning.js to C++. r=kats
dom/ipc/TabChild.cpp
gfx/layers/apz/util/DoubleTapToZoom.cpp
gfx/layers/apz/util/DoubleTapToZoom.h
gfx/layers/moz.build
layout/base/Units.h
--- a/dom/ipc/TabChild.cpp
+++ b/dom/ipc/TabChild.cpp
@@ -22,16 +22,17 @@
 #include "mozilla/plugins/PluginWidgetChild.h"
 #include "mozilla/IMEStateManager.h"
 #include "mozilla/ipc/DocumentRendererChild.h"
 #include "mozilla/ipc/FileDescriptorUtils.h"
 #include "mozilla/layers/APZCCallbackHelper.h"
 #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"
 #include "mozilla/MouseEvents.h"
 #include "mozilla/Services.h"
 #include "mozilla/StaticPtr.h"
 #include "mozilla/TextEvents.h"
 #include "mozilla/TouchEvents.h"
@@ -111,17 +112,16 @@ using namespace mozilla::layout;
 using namespace mozilla::docshell;
 using namespace mozilla::widget;
 using namespace mozilla::jsipc;
 
 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<nsUint64HashKey, TabChild*> TabChildMap;
 static TabChildMap* sTabChildren;
 
 class TabChild::DelayedFireContextMenuEvent final : public nsITimerCallback
 {
 public:
@@ -278,49 +278,16 @@ void
 TabChildBase::ProcessUpdateFrame(const FrameMetrics& aFrameMetrics)
 {
     if (!mGlobal || !mTabChildGlobal) {
         return;
     }
 
     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
 ContentListener::HandleEvent(nsIDOMEvent* aEvent)
 {
   RemoteDOMEvent remoteEvent;
   remoteEvent.mEvent = do_QueryInterface(aEvent);
   NS_ENSURE_STATE(remoteEvent.mEvent);
@@ -633,33 +600,17 @@ TabChild::TabChild(nsIContentChild* aMan
   }
 }
 
 NS_IMETHODIMP
 TabChild::Observe(nsISupports *aSubject,
                   const char *aTopic,
                   const char16_t *aData)
 {
-  if (!strcmp(aTopic, BROWSER_ZOOM_TO_RECT)) {
-    nsCOMPtr<nsIDocShell> docShell(do_QueryInterface(aSubject));
-    nsCOMPtr<nsITabChild> tabChild(TabChild::GetFrom(docShell));
-    if (tabChild == this) {
-      nsCOMPtr<nsIDocument> 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<nsIDocument> subject(do_QueryInterface(aSubject));
       nsCOMPtr<nsIDocument> doc(GetDocument());
 
       if (SameCOMIdentity(subject, doc)) {
         nsCOMPtr<nsIPresShell> shell(doc->GetShell());
         if (shell) {
           shell->SetIsFirstPaint(true);
@@ -1759,24 +1710,28 @@ TabChild::RecvHandleDoubleTap(const CSSP
 
     if (!mGlobal || !mTabChildGlobal) {
         return true;
     }
 
     // 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<nsIDocument> 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;
 }
 
 bool
 TabChild::RecvHandleSingleTap(const CSSPoint& aPoint, const Modifiers& aModifiers, const ScrollableLayerGuid& aGuid)
 {
   if (mGlobal && mTabChildGlobal) {
@@ -2449,17 +2404,16 @@ TabChild::RecvDestroy()
     // run script.
     MOZ_ASSERT(nsContentUtils::IsSafeToRunScript());
     mTabChildGlobal->DispatchTrustedEvent(NS_LITERAL_STRING("unload"));
   }
 
   nsCOMPtr<nsIObserverService> observerService =
     mozilla::services::GetObserverService();
 
-  observerService->RemoveObserver(this, BROWSER_ZOOM_TO_RECT);
   observerService->RemoveObserver(this, BEFORE_FIRST_PAINT);
 
   const nsAttrValue::EnumTable* table =
     AudioChannelService::GetAudioChannelTable();
 
   nsAutoCString topic;
   for (uint32_t i = 0; table[i].tag; ++i) {
     topic.Assign("audiochannel-activity-");
@@ -2652,19 +2606,16 @@ TabChild::InitRenderingState(const Textu
       mLayersId = aLayersId;
     }
 
     nsCOMPtr<nsIObserverService> observerService =
         mozilla::services::GetObserverService();
 
     if (observerService) {
         observerService->AddObserver(this,
-                                     BROWSER_ZOOM_TO_RECT,
-                                     false);
-        observerService->AddObserver(this,
                                      BEFORE_FIRST_PAINT,
                                      false);
     }
     return true;
 }
 
 void
 TabChild::SetBackgroundColor(const nscolor& aColor)
new file mode 100644
--- /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 <algorithm>  // 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<dom::Element>
+ElementFromPoint(const nsCOMPtr<nsIPresShell>& 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<dom::Element> result = content->AsElement();
+        return result.forget();
+      }
+    }
+  }
+  return nullptr;
+}
+
+static bool
+ShouldZoomToElement(const nsCOMPtr<dom::Element>& 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<nsIPresShell>& aShell,
+                       const nsCOMPtr<dom::Element>& 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<nsIDocument>& 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<nsIPresShell> shell = aRootContentDocument->GetShell();
+  if (!shell) {
+    return zoomOut;
+  }
+
+  nsIScrollableFrame* rootScrollFrame = shell->GetRootScrollFrameAsScrollable();
+  if (!rootScrollFrame) {
+    return zoomOut;
+  }
+
+  nsCOMPtr<dom::Element> 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;
+}
+
+}
+}
new file mode 100644
--- /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 T> 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<nsIDocument>& aRootContentDocument,
+                              const CSSPoint& aPoint);
+
+}
+}
+
+#endif /* mozilla_layers_DoubleTapToZoom_h */
--- a/gfx/layers/moz.build
+++ b/gfx/layers/moz.build
@@ -99,16 +99,17 @@ EXPORTS.mozilla.layers += [
     'apz/src/APZUtils.h',
     'apz/src/AsyncPanZoomAnimation.h',
     'apz/testutil/APZTestData.h',
     'apz/util/ActiveElementManager.h',
     'apz/util/APZCCallbackHelper.h',
     'apz/util/APZEventState.h',
     'apz/util/APZThreadUtils.h',
     'apz/util/ChromeProcessController.h',
+    'apz/util/DoubleTapToZoom.h',
     'apz/util/InputAPZContext.h',
     'AtomicRefCountedWithFinalize.h',
     'AxisPhysicsModel.h',
     'AxisPhysicsMSDModel.h',
     'basic/BasicCompositor.h',
     'basic/MacIOSurfaceTextureHostBasic.h',
     'basic/TextureHostBasic.h',
     'client/CanvasClient.h',
@@ -242,16 +243,17 @@ UNIFIED_SOURCES += [
     'apz/src/TaskThrottler.cpp',
     'apz/src/WheelScrollAnimation.cpp',
     'apz/testutil/APZTestData.cpp',
     'apz/util/ActiveElementManager.cpp',
     'apz/util/APZCCallbackHelper.cpp',
     'apz/util/APZEventState.cpp',
     'apz/util/APZThreadUtils.cpp',
     'apz/util/ChromeProcessController.cpp',
+    'apz/util/DoubleTapToZoom.cpp',
     'apz/util/InputAPZContext.cpp',
     'AxisPhysicsModel.cpp',
     'AxisPhysicsMSDModel.cpp',
     'basic/BasicCanvasLayer.cpp',
     'basic/BasicColorLayer.cpp',
     'basic/BasicCompositor.cpp',
     'basic/BasicContainerLayer.cpp',
     'basic/BasicImages.cpp',
@@ -379,16 +381,17 @@ IPDL_SOURCES = [
 
 FAIL_ON_WARNINGS = True
 
 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'
 
 if CONFIG['MOZ_DEBUG']:
     DEFINES['D3D_DEBUG_INFO'] = True
 
 if CONFIG['MOZ_ENABLE_D3D10_LAYER']:
--- a/layout/base/Units.h
+++ b/layout/base/Units.h
@@ -159,16 +159,21 @@ struct CSSPixel {
 
   // Conversions from app units
 
   static CSSPoint FromAppUnits(const nsPoint& aPoint) {
     return CSSPoint(NSAppUnitsToFloatPixels(aPoint.x, float(AppUnitsPerCSSPixel())),
                     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())),
                    NSAppUnitsToFloatPixels(aRect.width, float(AppUnitsPerCSSPixel())),
                    NSAppUnitsToFloatPixels(aRect.height, float(AppUnitsPerCSSPixel())));
   }
 
   static CSSMargin FromAppUnits(const nsMargin& aMargin) {