Bug 750974: Move basic pan/zoom logic into Gecko C++ r=cjones,roc sr=smaug[widget/]
authorDoug Sherk<bugzilla@sherk.me>
Thu, 19 Jul 2012 23:48:25 -0700
changeset 100251 67bb92bf9989e9aec99316cd2571cf6721b87ad0
parent 100250 ecb9536114625eb610b6506dc13e33362acfc624
child 100252 19b28e14df61d51ff1666b1a64ca44a26e093231
push id270
push userpvanderbeken@mozilla.com
push dateThu, 06 Mar 2014 09:24:21 +0000
reviewerscjones, roc, smaug
bugs750974
milestone17.0a1
Bug 750974: Move basic pan/zoom logic into Gecko C++ r=cjones,roc sr=smaug[widget/]
gfx/layers/Makefile.in
gfx/layers/ipc/AsyncPanZoomController.cpp
gfx/layers/ipc/AsyncPanZoomController.h
gfx/layers/ipc/Axis.cpp
gfx/layers/ipc/Axis.h
gfx/layers/ipc/GeckoContentController.h
gfx/layers/ipc/GestureEventListener.cpp
gfx/layers/ipc/GestureEventListener.h
widget/InputData.h
widget/Makefile.in
--- a/gfx/layers/Makefile.in
+++ b/gfx/layers/Makefile.in
@@ -105,35 +105,42 @@ CPPSRCS += \
         ThebesLayerD3D10.cpp \
         $(NULL)
 endif
 endif
 
 EXPORTS_NAMESPACES = gfxipc mozilla/layers
 EXPORTS_gfxipc = ShadowLayerUtils.h
 EXPORTS_mozilla/layers =\
+        AsyncPanZoomController.h \
+        Axis.h \
         CompositorCocoaWidgetHelper.h \
         CompositorChild.h \
         CompositorParent.h \
+        GeckoContentController.h \
+        GestureEventListener.h \
         ImageBridgeChild.h \
         ImageBridgeParent.h \
         ImageContainerChild.h \
         ImageContainerParent.h \
         ShadowLayers.h \
         ShadowLayersChild.h \
         ShadowLayersParent.h \
         ShadowLayersManager.h \
         RenderTrace.h \
         SharedImageUtils.h \
         $(NULL)
 
 CPPSRCS += \
+        AsyncPanZoomController.cpp \
+        Axis.cpp \
         CompositorCocoaWidgetHelper.cpp \
         CompositorChild.cpp \
         CompositorParent.cpp \
+        GestureEventListener.cpp \
         ImageBridgeChild.cpp \
         ImageBridgeParent.cpp \
         ImageContainerChild.cpp \
         ImageContainerParent.cpp \
         ShadowLayers.cpp \
         ShadowLayerChild.cpp \
         ShadowLayersChild.cpp \
         ShadowLayerParent.cpp \
@@ -157,9 +164,13 @@ ifeq ($(MOZ_WIDGET_TOOLKIT),gonk)
 EXPORTS_mozilla/layers += ShadowLayerUtilsGralloc.h
 CPPSRCS += ShadowLayerUtilsGralloc.cpp
 endif
 
 include $(topsrcdir)/config/rules.mk
 
 include $(topsrcdir)/ipc/chromium/chromium-config.mk
 
+LOCAL_INCLUDES += \
+        -I$(topsrcdir)/content/events/src \
+        $(NULL)
+
 CXXFLAGS += $(MOZ_CAIRO_CFLAGS) $(MOZ_PIXMAN_CFLAGS) $(TK_CFLAGS)
new file mode 100644
--- /dev/null
+++ b/gfx/layers/ipc/AsyncPanZoomController.cpp
@@ -0,0 +1,729 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=4 ts=8 et 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/. */
+
+#include "CompositorParent.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/Util.h"
+#include "mozilla/XPCOM.h"
+#include "mozilla/Monitor.h"
+#include "AsyncPanZoomController.h"
+#include "GestureEventListener.h"
+#include "nsIThreadManager.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+namespace layers {
+
+static const float EPSILON = 0.0001;
+
+/**
+ * Maximum amount of time while panning before sending a viewport change. This
+ * will asynchronously repaint the page. It is also forced when panning stops.
+ */
+static const PRInt32 PAN_REPAINT_INTERVAL = 250;
+
+/**
+ * Maximum amount of time flinging before sending a viewport change. This will
+ * asynchronously repaint the page.
+ */
+static const PRInt32 FLING_REPAINT_INTERVAL = 75;
+
+AsyncPanZoomController::AsyncPanZoomController(GeckoContentController* aGeckoContentController,
+                                               GestureBehavior aGestures)
+  :  mGeckoContentController(aGeckoContentController),
+     mX(this),
+     mY(this),
+     mMonitor("AsyncPanZoomController"),
+     mLastSampleTime(TimeStamp::Now()),
+     mState(NOTHING),
+     mDPI(72)
+{
+  if (aGestures == USE_GESTURE_DETECTOR) {
+    mGestureEventListener = new GestureEventListener(this);
+  }
+
+  SetDPI(mDPI);
+}
+
+AsyncPanZoomController::~AsyncPanZoomController() {
+
+}
+
+static gfx::Point
+WidgetSpaceToCompensatedViewportSpace(const gfx::Point& aPoint,
+                                      gfxFloat aCurrentZoom)
+{
+  // Transform the input point from local widget space to the content document
+  // space that the user is seeing, from last composite.
+  gfx::Point pt(aPoint);
+  pt = pt / aCurrentZoom;
+
+  // FIXME/bug 775451: this doesn't attempt to compensate for content transforms
+  // in effect on the compositor.  The problem is that it's very hard for us to
+  // know what content CSS pixel is at widget point 0,0 based on information
+  // available here.  So we use this hacky implementation for now, which works
+  // in quiescent states.
+
+  return pt;
+}
+
+nsEventStatus
+AsyncPanZoomController::HandleInputEvent(const nsInputEvent& aEvent,
+                                         nsInputEvent* aOutEvent)
+{
+  float currentZoom;
+  gfx::Point currentScrollOffset, lastScrollOffset;
+  {
+    MonitorAutoLock monitor(mMonitor);
+    currentZoom = mFrameMetrics.mResolution.width;
+    currentScrollOffset = gfx::Point(mFrameMetrics.mViewportScrollOffset.x,
+                                     mFrameMetrics.mViewportScrollOffset.y);
+    lastScrollOffset = gfx::Point(mLastContentPaintMetrics.mViewportScrollOffset.x,
+                                  mLastContentPaintMetrics.mViewportScrollOffset.y);
+  }
+
+  nsEventStatus status;
+  switch (aEvent.eventStructType) {
+  case NS_TOUCH_EVENT: {
+    MultiTouchInput event(static_cast<const nsTouchEvent&>(aEvent));
+    status = HandleInputEvent(event);
+    break;
+  }
+  case NS_MOUSE_EVENT: {
+    MultiTouchInput event(static_cast<const nsMouseEvent&>(aEvent));
+    status = HandleInputEvent(event);
+    break;
+  }
+  default:
+    status = nsEventStatus_eIgnore;
+    break;
+  }
+
+  switch (aEvent.eventStructType) {
+  case NS_TOUCH_EVENT: {
+    nsTouchEvent* touchEvent = static_cast<nsTouchEvent*>(aOutEvent);
+    const nsTArray<nsCOMPtr<nsIDOMTouch> >& touches = touchEvent->touches;
+    for (PRUint32 i = 0; i < touches.Length(); ++i) {
+      nsIDOMTouch* touch = touches[i];
+      if (touch) {
+        gfx::Point refPoint = WidgetSpaceToCompensatedViewportSpace(
+          gfx::Point(touch->mRefPoint.x, touch->mRefPoint.y),
+          currentZoom);
+        touch->mRefPoint = nsIntPoint(refPoint.x, refPoint.y);
+      }
+    }
+    break;
+  }
+  default: {
+    gfx::Point refPoint = WidgetSpaceToCompensatedViewportSpace(
+      gfx::Point(aOutEvent->refPoint.x, aOutEvent->refPoint.y),
+      currentZoom);
+    aOutEvent->refPoint = nsIntPoint(refPoint.x, refPoint.y);
+    break;
+  }
+  }
+
+  return status;
+}
+
+nsEventStatus AsyncPanZoomController::HandleInputEvent(const InputData& aEvent) {
+  nsEventStatus rv = nsEventStatus_eIgnore;
+
+  if (mGestureEventListener) {
+    nsEventStatus rv = mGestureEventListener->HandleInputEvent(aEvent);
+    if (rv == nsEventStatus_eConsumeNoDefault)
+      return rv;
+  }
+
+  switch (aEvent.mInputType) {
+  case MULTITOUCH_INPUT: {
+    const MultiTouchInput& multiTouchInput = aEvent.AsMultiTouchInput();
+    switch (multiTouchInput.mType) {
+      case MultiTouchInput::MULTITOUCH_START: rv = OnTouchStart(multiTouchInput); break;
+      case MultiTouchInput::MULTITOUCH_MOVE: rv = OnTouchMove(multiTouchInput); break;
+      case MultiTouchInput::MULTITOUCH_END: rv = OnTouchEnd(multiTouchInput); break;
+      case MultiTouchInput::MULTITOUCH_CANCEL: rv = OnTouchCancel(multiTouchInput); break;
+      default: NS_WARNING("Unhandled multitouch"); break;
+    }
+    break;
+  }
+  case PINCHGESTURE_INPUT: {
+    const PinchGestureInput& pinchGestureInput = aEvent.AsPinchGestureInput();
+    switch (pinchGestureInput.mType) {
+      case PinchGestureInput::PINCHGESTURE_START: rv = OnScaleBegin(pinchGestureInput); break;
+      case PinchGestureInput::PINCHGESTURE_SCALE: rv = OnScale(pinchGestureInput); break;
+      case PinchGestureInput::PINCHGESTURE_END: rv = OnScaleEnd(pinchGestureInput); break;
+      default: NS_WARNING("Unhandled pinch gesture"); break;
+    }
+    break;
+  }
+  case TAPGESTURE_INPUT: {
+    const TapGestureInput& tapGestureInput = aEvent.AsTapGestureInput();
+    switch (tapGestureInput.mType) {
+      case TapGestureInput::TAPGESTURE_LONG: rv = OnLongPress(tapGestureInput); break;
+      case TapGestureInput::TAPGESTURE_UP: rv = OnSingleTapUp(tapGestureInput); break;
+      case TapGestureInput::TAPGESTURE_CONFIRMED: rv = OnSingleTapConfirmed(tapGestureInput); break;
+      case TapGestureInput::TAPGESTURE_DOUBLE: rv = OnDoubleTap(tapGestureInput); break;
+      case TapGestureInput::TAPGESTURE_CANCEL: rv = OnCancelTap(tapGestureInput); break;
+      default: NS_WARNING("Unhandled tap gesture"); break;
+    }
+    break;
+  }
+  default: NS_WARNING("Unhandled input event"); break;
+  }
+
+  mLastEventTime = aEvent.mTime;
+  return rv;
+}
+
+nsEventStatus AsyncPanZoomController::OnTouchStart(const MultiTouchInput& aEvent) {
+  SingleTouchData& touch = GetFirstSingleTouch(aEvent);
+
+  nsIntPoint point = touch.mScreenPoint;
+  PRInt32 xPos = point.x, yPos = point.y;
+
+  switch (mState) {
+    case FLING:
+      CancelAnimation();
+      // Fall through.
+    case NOTHING:
+      mX.StartTouch(xPos);
+      mY.StartTouch(yPos);
+      mState = TOUCHING;
+      break;
+    case TOUCHING:
+    case PANNING:
+    case PINCHING:
+      NS_WARNING("Received impossible touch in OnTouchStart");
+      break;
+    default:
+      NS_WARNING("Unhandled case in OnTouchStart");
+      break;
+  }
+
+  return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnTouchMove(const MultiTouchInput& aEvent) {
+  SingleTouchData& touch = GetFirstSingleTouch(aEvent);
+  nsIntPoint point = touch.mScreenPoint;
+  PRInt32 xPos = point.x, yPos = point.y;
+
+  switch (mState) {
+    case FLING:
+    case NOTHING:
+      // May happen if the user double-taps and drags without lifting after the
+      // second tap. Ignore the move if this happens.
+      return nsEventStatus_eIgnore;
+
+    case TOUCHING: {
+      float panThreshold = 1.0f/16.0f * mDPI;
+      if (PanDistance(aEvent) < panThreshold) {
+        return nsEventStatus_eIgnore;
+      }
+      mLastRepaint = aEvent.mTime;
+      mX.StartTouch(xPos);
+      mY.StartTouch(yPos);
+      mState = PANNING;
+      return nsEventStatus_eConsumeNoDefault;
+    }
+
+    case PANNING:
+      TrackTouch(aEvent);
+      return nsEventStatus_eConsumeNoDefault;
+
+    case PINCHING:
+      // The scale gesture listener should have handled this.
+      NS_WARNING("Gesture listener should have handled pinching in OnTouchMove.");
+      return nsEventStatus_eIgnore;
+  }
+
+  return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnTouchEnd(const MultiTouchInput& aEvent) {
+  switch (mState) {
+  case FLING:
+    // Should never happen.
+    NS_WARNING("Received impossible touch end in OnTouchEnd.");
+    // Fall through.
+  case NOTHING:
+    // May happen if the user double-taps and drags without lifting after the
+    // second tap. Ignore if this happens.
+    return nsEventStatus_eIgnore;
+
+  case TOUCHING:
+    mState = NOTHING;
+    return nsEventStatus_eIgnore;
+
+  case PANNING:
+    {
+      MonitorAutoLock monitor(mMonitor);
+      ScheduleComposite();
+      RequestContentRepaint();
+    }
+    mState = FLING;
+    mLastSampleTime = TimeStamp::Now();
+    return nsEventStatus_eConsumeNoDefault;
+  case PINCHING:
+    mState = NOTHING;
+    // Scale gesture listener should have handled this.
+    NS_WARNING("Gesture listener should have handled pinching in OnTouchEnd.");
+    return nsEventStatus_eIgnore;
+  }
+
+  return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnTouchCancel(const MultiTouchInput& aEvent) {
+  mState = NOTHING;
+  return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnScaleBegin(const PinchGestureInput& aEvent) {
+  mState = PINCHING;
+  mLastZoomFocus = aEvent.mFocusPoint;
+
+  return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnScale(const PinchGestureInput& aEvent) {
+  float prevSpan = aEvent.mPreviousSpan;
+  if (fabsf(prevSpan) <= EPSILON || fabsf(aEvent.mCurrentSpan) <= EPSILON) {
+    // We're still handling it; we've just decided to throw this event away.
+    return nsEventStatus_eConsumeNoDefault;
+  }
+
+  float spanRatio = aEvent.mCurrentSpan / aEvent.mPreviousSpan;
+
+  {
+    MonitorAutoLock monitor(mMonitor);
+
+    float scale = mFrameMetrics.mResolution.width;
+
+    nsIntPoint focusPoint = aEvent.mFocusPoint;
+    PRInt32 xFocusChange = (mLastZoomFocus.x - focusPoint.x) / scale, yFocusChange = (mLastZoomFocus.y - focusPoint.y) / scale;
+    // If displacing by the change in focus point will take us off page bounds,
+    // then reduce the displacement such that it doesn't.
+    if (mX.DisplacementWillOverscroll(xFocusChange) != Axis::OVERSCROLL_NONE) {
+      xFocusChange -= mX.DisplacementWillOverscrollAmount(xFocusChange);
+    }
+    if (mY.DisplacementWillOverscroll(yFocusChange) != Axis::OVERSCROLL_NONE) {
+      yFocusChange -= mY.DisplacementWillOverscrollAmount(yFocusChange);
+    }
+    ScrollBy(nsIntPoint(xFocusChange, yFocusChange));
+
+    // When we zoom in with focus, we can zoom too much towards the boundaries
+    // that we actually go over them. These are the needed displacements along
+    // either axis such that we don't overscroll the boundaries when zooming.
+    PRInt32 neededDisplacementX = 0, neededDisplacementY = 0;
+
+    // Only do the scaling if we won't go over 8x zoom in or out.
+    bool doScale = (scale < 8.0f && spanRatio > 1.0f) || (scale > 0.125f && spanRatio < 1.0f);
+
+    // If this zoom will take it over 8x zoom in either direction, but it's not
+    // already there, then normalize it.
+    if (scale * spanRatio > 8.0f) {
+      spanRatio = scale / 8.0f;
+    } else if (scale * spanRatio < 0.125f) {
+      spanRatio = scale / 0.125f;
+    }
+
+    if (doScale) {
+      switch (mX.ScaleWillOverscroll(spanRatio, focusPoint.x))
+      {
+        case Axis::OVERSCROLL_NONE:
+          break;
+        case Axis::OVERSCROLL_MINUS:
+        case Axis::OVERSCROLL_PLUS:
+          neededDisplacementX = -mX.ScaleWillOverscrollAmount(spanRatio, focusPoint.x);
+          break;
+        case Axis::OVERSCROLL_BOTH:
+          // If scaling this way will make us overscroll in both directions, then
+          // we must already be at the maximum zoomed out amount. In this case, we
+          // don't want to allow this scaling to go through and instead clamp it
+          // here.
+          doScale = false;
+          break;
+      }
+    }
+
+    if (doScale) {
+      switch (mY.ScaleWillOverscroll(spanRatio, focusPoint.y))
+      {
+        case Axis::OVERSCROLL_NONE:
+          break;
+        case Axis::OVERSCROLL_MINUS:
+        case Axis::OVERSCROLL_PLUS:
+          neededDisplacementY = -mY.ScaleWillOverscrollAmount(spanRatio, focusPoint.y);
+          break;
+        case Axis::OVERSCROLL_BOTH:
+          doScale = false;
+          break;
+      }
+    }
+
+    if (doScale) {
+      ScaleWithFocus(scale * spanRatio,
+                     focusPoint);
+
+      if (neededDisplacementX != 0 || neededDisplacementY != 0) {
+        ScrollBy(nsIntPoint(neededDisplacementX, neededDisplacementY));
+      }
+
+      ScheduleComposite();
+      // We don't want to redraw on every scale, so don't use
+      // RequestContentRepaint()
+    }
+
+    mLastZoomFocus = focusPoint;
+  }
+
+  return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnScaleEnd(const PinchGestureInput& aEvent) {
+  mState = PANNING;
+  mX.StartTouch(aEvent.mFocusPoint.x);
+  mY.StartTouch(aEvent.mFocusPoint.y);
+  {
+    MonitorAutoLock monitor(mMonitor);
+    ScheduleComposite();
+    RequestContentRepaint();
+  }
+
+  return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnLongPress(const TapGestureInput& aEvent) {
+  // XXX: Implement this.
+  return nsEventStatus_eIgnore;
+}
+
+nsEventStatus AsyncPanZoomController::OnSingleTapUp(const TapGestureInput& aEvent) {
+  // XXX: Implement this.
+  return nsEventStatus_eIgnore;
+}
+
+nsEventStatus AsyncPanZoomController::OnSingleTapConfirmed(const TapGestureInput& aEvent) {
+  // XXX: Implement this.
+  return nsEventStatus_eIgnore;
+}
+
+nsEventStatus AsyncPanZoomController::OnDoubleTap(const TapGestureInput& aEvent) {
+  // XXX: Implement this.
+  return nsEventStatus_eIgnore;
+}
+
+nsEventStatus AsyncPanZoomController::OnCancelTap(const TapGestureInput& aEvent) {
+  // XXX: Implement this.
+  return nsEventStatus_eIgnore;
+}
+
+float AsyncPanZoomController::PanDistance(const MultiTouchInput& aEvent) {
+  SingleTouchData& touch = GetFirstSingleTouch(aEvent);
+  nsIntPoint point = touch.mScreenPoint;
+  PRInt32 xPos = point.x, yPos = point.y;
+  mX.UpdateWithTouchAtDevicePoint(xPos, 0);
+  mY.UpdateWithTouchAtDevicePoint(yPos, 0);
+  return NS_hypot(mX.PanDistance(), mY.PanDistance()) * mFrameMetrics.mResolution.width;
+}
+
+const nsPoint AsyncPanZoomController::GetVelocityVector() {
+  return nsPoint(
+    mX.GetVelocity(),
+    mY.GetVelocity()
+  );
+}
+
+void AsyncPanZoomController::TrackTouch(const MultiTouchInput& aEvent) {
+  SingleTouchData& touch = GetFirstSingleTouch(aEvent);
+  nsIntPoint point = touch.mScreenPoint;
+  PRInt32 xPos = point.x, yPos = point.y, timeDelta = aEvent.mTime - mLastEventTime;
+
+  // Probably a duplicate event, just throw it away.
+  if (!timeDelta) {
+    return;
+  }
+
+  {
+    MonitorAutoLock monitor(mMonitor);
+    mX.UpdateWithTouchAtDevicePoint(xPos, timeDelta);
+    mY.UpdateWithTouchAtDevicePoint(yPos, timeDelta);
+
+    // We want to inversely scale it because when you're zoomed further in, a
+    // larger swipe should move you a shorter distance.
+    float inverseScale = 1 / mFrameMetrics.mResolution.width;
+
+    PRInt32 xDisplacement = mX.UpdateAndGetDisplacement(inverseScale);
+    PRInt32 yDisplacement = mY.UpdateAndGetDisplacement(inverseScale);
+    if (!xDisplacement && !yDisplacement) {
+      return;
+    }
+
+    ScrollBy(nsIntPoint(xDisplacement, yDisplacement));
+    ScheduleComposite();
+
+    if (aEvent.mTime - mLastRepaint >= PAN_REPAINT_INTERVAL) {
+      RequestContentRepaint();
+      mLastRepaint = aEvent.mTime;
+    }
+  }
+}
+
+SingleTouchData& AsyncPanZoomController::GetFirstSingleTouch(const MultiTouchInput& aEvent) {
+  return (SingleTouchData&)aEvent.mTouches[0];
+}
+
+bool AsyncPanZoomController::DoFling(const TimeDuration& aDelta) {
+  if (mState != FLING) {
+    return false;
+  }
+
+  if (!mX.FlingApplyFrictionOrCancel(aDelta) && !mY.FlingApplyFrictionOrCancel(aDelta)) {
+    RequestContentRepaint();
+    mState = NOTHING;
+    return false;
+  }
+
+  // We want to inversely scale it because when you're zoomed further in, a
+  // larger swipe should move you a shorter distance.
+  float inverseScale = 1 / mFrameMetrics.mResolution.width;
+
+  ScrollBy(nsIntPoint(
+    mX.UpdateAndGetDisplacement(inverseScale),
+    mY.UpdateAndGetDisplacement(inverseScale)
+  ));
+  RequestContentRepaint();
+
+  return true;
+}
+
+void AsyncPanZoomController::CancelAnimation() {
+  mState = NOTHING;
+}
+
+void AsyncPanZoomController::SetCompositorParent(CompositorParent* aCompositorParent) {
+  mCompositorParent = aCompositorParent;
+}
+
+void AsyncPanZoomController::ScrollBy(const nsIntPoint& aOffset) {
+  nsIntPoint newOffset(mFrameMetrics.mViewportScrollOffset.x + aOffset.x,
+                       mFrameMetrics.mViewportScrollOffset.y + aOffset.y);
+  FrameMetrics metrics(mFrameMetrics);
+  metrics.mViewportScrollOffset = newOffset;
+  mFrameMetrics = metrics;
+}
+
+void AsyncPanZoomController::SetPageRect(const gfx::Rect& aCSSPageRect) {
+  FrameMetrics metrics = mFrameMetrics;
+  gfx::Rect pageSize = aCSSPageRect;
+  float scale = mFrameMetrics.mResolution.width;
+
+  // The page rect is the css page rect scaled by the current zoom.
+  pageSize.ScaleRoundOut(scale);
+
+  // Round the page rect so we don't get any truncation, then get the nsIntRect
+  // from this.
+  metrics.mContentRect = nsIntRect(pageSize.x, pageSize.y, pageSize.width, pageSize.height);
+  metrics.mCSSContentRect = aCSSPageRect;
+
+  mFrameMetrics = metrics;
+}
+
+void AsyncPanZoomController::ScaleWithFocus(float aScale, const nsIntPoint& aFocus) {
+  FrameMetrics metrics(mFrameMetrics);
+
+  // Don't set the scale to the inputted value, but rather multiply it in.
+  float scaleFactor = aScale / metrics.mResolution.width;
+
+  metrics.mResolution.width = metrics.mResolution.height = aScale;
+
+  // Force a recalculation of the page rect based on the new zoom and the
+  // current CSS page rect (which is unchanged since it's not affected by zoom).
+  SetPageRect(mFrameMetrics.mCSSContentRect);
+
+  nsIntPoint scrollOffset = metrics.mViewportScrollOffset;
+
+  scrollOffset.x += aFocus.x * (scaleFactor - 1.0f);
+  scrollOffset.y += aFocus.y * (scaleFactor - 1.0f);
+
+  metrics.mViewportScrollOffset = scrollOffset;
+
+  mFrameMetrics = metrics;
+}
+
+const nsIntRect AsyncPanZoomController::CalculatePendingDisplayPort() {
+  float scale = mFrameMetrics.mResolution.width;
+  nsIntRect viewport = mFrameMetrics.mViewport;
+  viewport.ScaleRoundIn(1 / scale);
+
+  const float SIZE_MULTIPLIER = 2.0f;
+  nsIntPoint scrollOffset = mFrameMetrics.mViewportScrollOffset;
+  gfx::Rect contentRect = mFrameMetrics.mCSSContentRect;
+
+  // Paint a larger portion of the screen than just what we can see. This makes
+  // it less likely that we'll checkerboard when panning around and Gecko hasn't
+  // repainted yet.
+  float desiredWidth = viewport.width * SIZE_MULTIPLIER,
+        desiredHeight = viewport.height * SIZE_MULTIPLIER;
+
+  // The displayport is relative to the current scroll offset. Here's a little
+  // diagram to make it easier to see:
+  //
+  //       - - - -
+  //       |     |
+  //    *************
+  //    *  |     |  *
+  // - -*- @------ -*- -
+  // |  *  |=====|  *  |
+  //    *  |=====|  *
+  // |  *  |=====|  *  |
+  // - -*- ------- -*- -
+  //    *  |     |  *
+  //    *************
+  //       |     |
+  //       - - - -
+  //
+  // The full --- area with === inside it is the actual viewport rect, the *** area
+  // is the displayport, and the - - - area is an imaginary additional page on all 4
+  // borders of the actual page. Notice that the displayport intersects half-way with
+  // each of the imaginary extra pages. The @ symbol at the top left of the
+  // viewport marks the current scroll offset. From the @ symbol to the far left
+  // and far top, it is clear that this distance is 1/4 of the displayport's
+  // height/width dimension.
+  gfx::Rect displayPort(-desiredWidth / 4, -desiredHeight / 4, desiredWidth, desiredHeight);
+
+  // Check if the desired boundaries go over the CSS page rect along the top or
+  // left. If they do, shift them to the right or down.
+  float oldDisplayPortX = displayPort.x, oldDisplayPortY = displayPort.y;
+  if (displayPort.X() + scrollOffset.x < contentRect.X()) {
+    displayPort.x = contentRect.X() - scrollOffset.x;
+  }
+  if (displayPort.Y() + scrollOffset.y < contentRect.Y()) {
+    displayPort.y = contentRect.Y() - scrollOffset.y;
+  }
+
+  // We don't need to paint the extra area that was going to overlap with the
+  // content rect. Subtract out this extra width or height.
+  displayPort.width -= displayPort.x - oldDisplayPortX;
+  displayPort.height -= displayPort.y - oldDisplayPortY;
+
+  // Check if the desired boundaries go over the CSS page rect along the right
+  // or bottom. If they do, subtract out some height or width such that they
+  // perfectly align with the end of the CSS page rect.
+  if (displayPort.XMost() + scrollOffset.x > contentRect.XMost()) {
+    displayPort.width = NS_MAX(0.0f, contentRect.XMost() - (displayPort.X() + scrollOffset.x));
+  }
+  if (displayPort.YMost() + scrollOffset.y > contentRect.YMost()) {
+    displayPort.height = NS_MAX(0.0f, contentRect.YMost() - (displayPort.Y() + scrollOffset.y));
+  }
+
+  // Round the displayport so we don't get any truncation, then get the nsIntRect
+  // from this.
+  displayPort.Round();
+  return nsIntRect(displayPort.x, displayPort.y, displayPort.width, displayPort.height);
+}
+
+void AsyncPanZoomController::SetDPI(int aDPI) {
+  mDPI = aDPI;
+}
+
+void AsyncPanZoomController::ScheduleComposite() {
+  if (mCompositorParent) {
+    mCompositorParent->ScheduleRenderOnCompositorThread();
+  }
+}
+
+void AsyncPanZoomController::RequestContentRepaint() {
+  mFrameMetrics.mDisplayPort = CalculatePendingDisplayPort();
+  mGeckoContentController->RequestContentRepaint(mFrameMetrics);
+}
+
+bool AsyncPanZoomController::SampleContentTransformForFrame(const TimeStamp& aSampleTime,
+                                                            const FrameMetrics& aFrame,
+                                                            const gfx3DMatrix& aCurrentTransform,
+                                                            gfx3DMatrix* aNewTransform) {
+  // The eventual return value of this function. The compositor needs to know
+  // whether or not to advance by a frame as soon as it can. For example, if a
+  // fling is happening, it has to keep compositing so that the animation is
+  // smooth. If an animation frame is requested, it is the compositor's
+  // responsibility to schedule a composite.
+  bool requestAnimationFrame = false;
+
+  // Scales on the root layer, on what's currently painted.
+  float rootScaleX = aCurrentTransform.GetXScale(),
+        rootScaleY = aCurrentTransform.GetYScale();
+
+  nsIntPoint metricsScrollOffset(0, 0);
+  nsIntPoint scrollOffset;
+  float localScaleX, localScaleY;
+
+  {
+    MonitorAutoLock mon(mMonitor);
+
+    // If a fling is currently happening, apply it now. We can pull the updated
+    // metrics afterwards.
+    requestAnimationFrame = requestAnimationFrame || DoFling(aSampleTime - mLastSampleTime);
+
+    // Current local transform; this is not what's painted but rather what PZC has
+    // transformed due to touches like panning or pinching. Eventually, the root
+    // layer transform will become this during runtime, but we must wait for Gecko
+    // to repaint.
+    localScaleX = mFrameMetrics.mResolution.width;
+    localScaleY = mFrameMetrics.mResolution.height;
+
+    if (aFrame.IsScrollable()) {
+      metricsScrollOffset = aFrame.mViewportScrollOffset;
+    }
+
+    scrollOffset = mFrameMetrics.mViewportScrollOffset;
+  }
+
+  nsIntPoint scrollCompensation(
+    (scrollOffset.x / rootScaleX - metricsScrollOffset.x) * localScaleX,
+    (scrollOffset.y / rootScaleY - metricsScrollOffset.y) * localScaleY);
+
+  ViewTransform treeTransform(-scrollCompensation, localScaleX, localScaleY);
+  *aNewTransform = gfx3DMatrix(treeTransform) * aCurrentTransform;
+
+  mLastSampleTime = aSampleTime;
+
+  return requestAnimationFrame;
+}
+
+void AsyncPanZoomController::NotifyLayersUpdated(const FrameMetrics& aViewportFrame, bool aIsFirstPaint) {
+  MonitorAutoLock monitor(mMonitor);
+
+  mLastContentPaintMetrics = aViewportFrame;
+
+  if (aIsFirstPaint || mFrameMetrics.IsDefault()) {
+    mX.StopTouch();
+    mY.StopTouch();
+    mFrameMetrics = aViewportFrame;
+    mFrameMetrics.mResolution.width = 1 / mFrameMetrics.mResolution.width;
+    mFrameMetrics.mResolution.height = 1 / mFrameMetrics.mResolution.height;
+    SetPageRect(mFrameMetrics.mCSSContentRect);
+  } else if (!mFrameMetrics.mContentRect.IsEqualEdges(aViewportFrame.mContentRect)) {
+    mFrameMetrics.mCSSContentRect = aViewportFrame.mCSSContentRect;
+    SetPageRect(mFrameMetrics.mCSSContentRect);
+  }
+}
+
+const FrameMetrics& AsyncPanZoomController::GetFrameMetrics() {
+  mMonitor.AssertCurrentThreadOwns();
+  return mFrameMetrics;
+}
+
+void AsyncPanZoomController::UpdateViewportSize(int aWidth, int aHeight) {
+  MonitorAutoLock mon(mMonitor);
+  FrameMetrics metrics = GetFrameMetrics();
+  metrics.mViewport = nsIntRect(0, 0, aWidth, aHeight);
+  mFrameMetrics = metrics;
+}
+
+}
+}
new file mode 100644
--- /dev/null
+++ b/gfx/layers/ipc/AsyncPanZoomController.h
@@ -0,0 +1,360 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=4 ts=8 et 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/. */
+
+#ifndef mozilla_layers_AsyncPanZoomController_h
+#define mozilla_layers_AsyncPanZoomController_h
+
+#include "GeckoContentController.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/TimeStamp.h"
+#include "InputData.h"
+#include "Axis.h"
+
+namespace mozilla {
+namespace layers {
+
+class CompositorParent;
+class GestureEventListener;
+
+/**
+ * Controller for all panning and zooming logic. Any time a user input is
+ * detected and it must be processed in some way to affect what the user sees,
+ * it goes through here. Listens for any input event from InputData and can
+ * optionally handle nsGUIEvent-derived touch events, but this must be done on
+ * the main thread. Note that this class completely cross-platform.
+ *
+ * Input events originate on the UI thread of the platform that this runs on,
+ * and are then sent to this class. This class processes the event in some way;
+ * for example, a touch move will usually lead to a panning of content (though
+ * of course there are exceptions, such as if content preventDefaults the event,
+ * or if the target frame is not scrollable). The compositor interacts with this
+ * class by locking it and querying it for the current transform matrix based on
+ * the panning and zooming logic that was invoked on the UI thread.
+ *
+ * Currently, each outer DOM window (i.e. a website in a tab, but not any
+ * subframes) has its own AsyncPanZoomController. In the future, to support
+ * asynchronously scrolled subframes, we want to have one AsyncPanZoomController
+ * per frame.
+ */
+class AsyncPanZoomController {
+  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AsyncPanZoomController)
+
+  typedef mozilla::MonitorAutoLock MonitorAutoLock;
+
+public:
+  enum GestureBehavior {
+    // The platform code is responsible for forwarding gesture events here. We
+    // will not attempt to generate gesture events from MultiTouchInputs.
+    DEFAULT_GESTURES,
+    // An instance of GestureEventListener is used to detect gestures. This is
+    // handled completely internally within this class.
+    USE_GESTURE_DETECTOR
+  };
+
+  AsyncPanZoomController(GeckoContentController* aController,
+                         GestureBehavior aGestures = DEFAULT_GESTURES);
+  ~AsyncPanZoomController();
+
+  // --------------------------------------------------------------------------
+  // These methods must only be called on the controller/UI thread.
+  //
+
+  /**
+   * General handler for incoming input events. Manipulates the frame metrics
+   * basde on what type of input it is. For example, a PinchGestureEvent will
+   * cause scaling.
+   */
+
+  nsEventStatus HandleInputEvent(const InputData& aEvent);
+
+  /**
+   * Special handler for nsInputEvents. Also sets |aOutEvent| (which is assumed
+   * to be an already-existing instance of an nsInputEvent which may be an
+   * nsTouchEvent) to have its touch points in DOM space. This is so that the
+   * touches can be passed through the DOM and content can handle them.
+   *
+   * NOTE: Be careful of invoking the nsInputEvent variant. This can only be
+   * called on the main thread. See widget/InputData.h for more information on
+   * why we have InputData and nsInputEvent separated.
+   */
+  nsEventStatus HandleInputEvent(const nsInputEvent& aEvent,
+                                 nsInputEvent* aOutEvent);
+
+  /**
+   * Updates the viewport size, i.e. the dimensions of the frame (not
+   * necessarily the screen) content will actually be rendered onto in device
+   * pixels for example, a subframe will not take the entire screen, but we
+   * still want to know how big it is in device pixels. Ideally we want to be
+   * using CSS pixels everywhere inside here, but in this case we need to know
+   * how large of a displayport to set so we use these dimensions plus some
+   * extra.
+   *
+   * XXX: Use nsIntRect instead.
+   */
+  void UpdateViewportSize(int aWidth, int aHeight);
+
+  // --------------------------------------------------------------------------
+  // These methods must only be called on the compositor thread.
+  //
+
+  /**
+   * The compositor calls this when it's about to draw pannable/zoomable content
+   * and is setting up transforms for compositing the layer tree. This is not
+   * idempotent. For example, a fling transform can be applied each time this is
+   * called (though not necessarily). |aSampleTime| is the time that this is
+   * sampled at; this is used for interpolating animations. Calling this sets a
+   * new transform in |aNewTransform| which should be applied directly to the
+   * shadow layer of the frame (do not multiply it in as the code already does
+   * this internally with |aCurrentTransform|.
+   *
+   * Return value indicates whether or not any currently running animation
+   * should continue. That is, if true, the compositor should schedule another
+   * composite.
+   */
+  bool SampleContentTransformForFrame(const TimeStamp& aSampleTime,
+                                      const FrameMetrics& aFrame,
+                                      const gfx3DMatrix& aCurrentTransform,
+                                      gfx3DMatrix* aNewTransform);
+
+  /**
+   * A shadow layer update has arrived. |aViewportFrame| is the new FrameMetrics
+   * for the top-level frame. |aIsFirstPaint| is a flag passed from the shadow
+   * layers code indicating that the frame metrics being sent with this call are
+   * the initial metrics and the initial paint of the frame has just happened.
+   */
+  void NotifyLayersUpdated(const FrameMetrics& aViewportFrame, bool aIsFirstPaint);
+
+  /**
+   * The platform implementation must set the compositor parent so that we can
+   * request composites.
+   */
+  void SetCompositorParent(CompositorParent* aCompositorParent);
+
+  // --------------------------------------------------------------------------
+  // These methods can be called from any thread.
+  //
+
+  /**
+   * Sets the CSS page rect, and calculates a new page rect based on the zoom
+   * level of the current metrics and the passed in CSS page rect.
+   */
+  void SetPageRect(const gfx::Rect& aCSSPageRect);
+
+  /**
+   * Sets the DPI of the device for use within panning and zooming logic. It is
+   * a platform responsibility to set this on initialization of this class and
+   * whenever it changes.
+   */
+  void SetDPI(int aDPI);
+
+protected:
+  /**
+   * Helper method for touches beginning. Sets everything up for panning and any
+   * multitouch gestures.
+   */
+  nsEventStatus OnTouchStart(const MultiTouchInput& aEvent);
+
+  /**
+   * Helper method for touches moving. Does any transforms needed when panning.
+   */
+  nsEventStatus OnTouchMove(const MultiTouchInput& aEvent);
+
+  /**
+   * Helper method for touches ending. Redraws the screen if necessary and does
+   * any cleanup after a touch has ended.
+   */
+  nsEventStatus OnTouchEnd(const MultiTouchInput& aEvent);
+
+  /**
+   * Helper method for touches being cancelled. Treated roughly the same as a
+   * touch ending (OnTouchEnd()).
+   */
+  nsEventStatus OnTouchCancel(const MultiTouchInput& aEvent);
+
+  /**
+   * Helper method for scales beginning. Distinct from the OnTouch* handlers in
+   * that this implies some outside implementation has determined that the user
+   * is pinching.
+   */
+  nsEventStatus OnScaleBegin(const PinchGestureInput& aEvent);
+
+  /**
+   * Helper method for scaling. As the user moves their fingers when pinching,
+   * this changes the scale of the page.
+   */
+  nsEventStatus OnScale(const PinchGestureInput& aEvent);
+
+  /**
+   * Helper method for scales ending. Redraws the screen if necessary and does
+   * any cleanup after a scale has ended.
+   */
+  nsEventStatus OnScaleEnd(const PinchGestureInput& aEvent);
+
+  /**
+   * Helper method for long press gestures.
+   *
+   * XXX: Implement this.
+   */
+  nsEventStatus OnLongPress(const TapGestureInput& aEvent);
+
+  /**
+   * Helper method for single tap gestures.
+   *
+   * XXX: Implement this.
+   */
+  nsEventStatus OnSingleTapUp(const TapGestureInput& aEvent);
+
+  /**
+   * Helper method for a single tap confirmed.
+   *
+   * XXX: Implement this.
+   */
+  nsEventStatus OnSingleTapConfirmed(const TapGestureInput& aEvent);
+
+  /**
+   * Helper method for double taps.
+   *
+   * XXX: Implement this.
+   */
+  nsEventStatus OnDoubleTap(const TapGestureInput& aEvent);
+
+  /**
+   * Helper method to cancel any gesture currently going to Gecko. Used
+   * primarily when a user taps the screen over some clickable content but then
+   * pans down instead of letting go (i.e. to cancel a previous touch so that a
+   * new one can properly take effect.
+   */
+  nsEventStatus OnCancelTap(const TapGestureInput& aEvent);
+
+  /**
+   * Scrolls the viewport by an X,Y offset.
+   */
+  void ScrollBy(const nsIntPoint& aOffset);
+
+  /**
+   * Scales the viewport by an amount (note that it multiplies this scale in to
+   * the current scale, it doesn't set it to |aScale|). Also considers a focus
+   * point so that the page zooms outward from that point.
+   *
+   * XXX: Fix focus point calculations.
+   */
+  void ScaleWithFocus(float aScale, const nsIntPoint& aFocus);
+
+  /**
+   * Schedules a composite on the compositor thread. Wrapper for
+   * CompositorParent::ScheduleRenderOnCompositorThread().
+   */
+  void ScheduleComposite();
+
+  /**
+   * Cancels any currently running animation. Note that all this does is set the
+   * state of the AsyncPanZoomController back to NOTHING, but it is the
+   * animation's responsibility to check this before advancing.
+   */
+  void CancelAnimation();
+
+  /**
+   * Gets the displacement of the current touch since it began. That is, it is
+   * the distance between the current position and the initial position of the
+   * current touch (this only makes sense if a touch is currently happening and
+   * OnTouchMove() is being invoked).
+   */
+  float PanDistance(const MultiTouchInput& aEvent);
+
+  /**
+   * Gets a vector of the velocities of each axis.
+   */
+  const nsPoint GetVelocityVector();
+
+  /**
+   * Gets a reference to the first SingleTouchData from a MultiTouchInput.  This
+   * gets only the first one and assumes the rest are either missing or not
+   * relevant.
+   */
+  SingleTouchData& GetFirstSingleTouch(const MultiTouchInput& aEvent);
+
+  /**
+   * Does any panning required due to a new touch event.
+   */
+  void TrackTouch(const MultiTouchInput& aEvent);
+
+  /**
+   * Recalculates the displayport. Ideally, this should paint an area bigger
+   * than the actual screen. The viewport refers to the size of the screen,
+   * while the displayport is the area actually painted by Gecko. We paint
+   * a larger area than the screen so that when you scroll down, you don't
+   * checkerboard immediately.
+   */
+  const nsIntRect CalculatePendingDisplayPort();
+
+  /**
+   * Utility function to send updated FrameMetrics to Gecko so that it can paint
+   * the displayport area. Calls into GeckoContentController to do the actual
+   * work.
+   */
+  void RequestContentRepaint();
+
+  /**
+   * Advances a fling by an interpolated amount based on the passed in |aDelta|.
+   * This should be called whenever sampling the content transform for this
+   * frame. Returns true if the fling animation should be advanced by one frame,
+   * or false if there is no fling or the fling has ended.
+   */
+  bool DoFling(const TimeDuration& aDelta);
+
+  /**
+   * Gets the current frame metrics. This is *not* the Gecko copy stored in the
+   * layers code.
+   */
+  const FrameMetrics& GetFrameMetrics();
+
+private:
+  enum PanZoomState {
+    NOTHING,        /* no touch-start events received */
+    FLING,          /* all touches removed, but we're still scrolling page */
+    TOUCHING,       /* one touch-start event received */
+    PANNING,        /* panning without axis lock */
+    PINCHING,       /* nth touch-start, where n > 1. this mode allows pan and zoom */
+  };
+
+  nsRefPtr<CompositorParent> mCompositorParent;
+  nsRefPtr<GeckoContentController> mGeckoContentController;
+  nsRefPtr<GestureEventListener> mGestureEventListener;
+
+  // Both |mFrameMetrics| and |mLastContentPaintMetrics| are protected by the
+  // monitor. Do not read from or modify either of them without locking.
+  FrameMetrics mFrameMetrics;
+  // These are the metrics at last content paint, the most recent
+  // values we were notified of in NotifyLayersUpdate().
+  FrameMetrics mLastContentPaintMetrics;
+
+  AxisX mX;
+  AxisY mY;
+
+  Monitor mMonitor;
+
+  // The last time the compositor has sampled the content transform for this
+  // frame.
+  TimeStamp mLastSampleTime;
+  // The last time a touch event came through on the UI thread.
+  PRInt32 mLastEventTime;
+  // The last time a repaint has been requested from Gecko.
+  PRInt32 mLastRepaint;
+
+  // Stores the previous focus point if there is a pinch gesture happening. Used
+  // to allow panning by moving multiple fingers (thus moving the focus point).
+  nsIntPoint mLastZoomFocus;
+  PanZoomState mState;
+  int mDPI;
+
+  friend class Axis;
+};
+
+}
+}
+
+#endif // mozilla_layers_PanZoomController_h
new file mode 100644
--- /dev/null
+++ b/gfx/layers/ipc/Axis.cpp
@@ -0,0 +1,295 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=4 ts=8 et 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/. */
+
+#include "Axis.h"
+#include "AsyncPanZoomController.h"
+
+namespace mozilla {
+namespace layers {
+
+static const float EPSILON = 0.0001;
+
+/**
+ * Milliseconds per frame, used to judge how much displacement should have
+ * happened every frame based on the velocity calculated from touch events.
+ */
+static const float MS_PER_FRAME = 1000.0f / 60.0f;
+
+/**
+ * Maximum acceleration that can happen between two frames. Velocity is
+ * throttled if it's above this. This may happen if a time delta is very low,
+ * or we get a touch point very far away from the previous position for some
+ * reason.
+ */
+static const float MAX_EVENT_ACCELERATION = 12;
+
+/**
+ * Amount of friction applied during flings when going above
+ * VELOCITY_THRESHOLD.
+ */
+static const float FLING_FRICTION_FAST = 0.010;
+
+/**
+ * Amount of friction applied during flings when going below
+ * VELOCITY_THRESHOLD.
+ */
+static const float FLING_FRICTION_SLOW = 0.008;
+
+/**
+ * Maximum velocity before fling friction increases.
+ */
+static const float VELOCITY_THRESHOLD = 10;
+
+/**
+ * When flinging, if the velocity goes below this number, we just stop the
+ * animation completely. This is to prevent asymptotically approaching 0
+ * velocity and rerendering unnecessarily.
+ */
+static const float FLING_STOPPED_THRESHOLD = 0.1f;
+
+Axis::Axis(AsyncPanZoomController* aAsyncPanZoomController)
+  : mPos(0.0f),
+    mVelocity(0.0f),
+    mAsyncPanZoomController(aAsyncPanZoomController)
+{
+
+}
+
+void Axis::UpdateWithTouchAtDevicePoint(PRInt32 aPos, PRInt32 aTimeDelta) {
+  float newVelocity = MS_PER_FRAME * (mPos - aPos) / aTimeDelta;
+
+  bool curVelocityIsLow = fabsf(newVelocity) < 1.0f;
+  bool directionChange = (mVelocity > 0) != (newVelocity != 0);
+
+  // If a direction change has happened, or the current velocity due to this new
+  // touch is relatively low, then just apply it. If not, throttle it.
+  if (curVelocityIsLow || (directionChange && fabs(newVelocity) - EPSILON <= 0.0f)) {
+    mVelocity = newVelocity;
+  } else {
+    float maxChange = fabsf(mVelocity * aTimeDelta * MAX_EVENT_ACCELERATION);
+    mVelocity = NS_MIN(mVelocity + maxChange, NS_MAX(mVelocity - maxChange, newVelocity));
+  }
+
+  mVelocity = newVelocity;
+  mPos = aPos;
+}
+
+void Axis::StartTouch(PRInt32 aPos) {
+  mStartPos = aPos;
+  mPos = aPos;
+  mVelocity = 0.0f;
+}
+
+PRInt32 Axis::UpdateAndGetDisplacement(float aScale) {
+  PRInt32 displacement = NS_lround(mVelocity * aScale);
+  // If this displacement will cause an overscroll, throttle it. Can potentially
+  // bring it to 0 even if the velocity is high.
+  if (DisplacementWillOverscroll(displacement) != OVERSCROLL_NONE) {
+    displacement -= DisplacementWillOverscrollAmount(displacement);
+  }
+  return displacement;
+}
+
+float Axis::PanDistance() {
+  return fabsf(mPos - mStartPos);
+}
+
+void Axis::StopTouch() {
+  mVelocity = 0.0f;
+}
+
+bool Axis::FlingApplyFrictionOrCancel(const TimeDuration& aDelta) {
+  if (fabsf(mVelocity) <= FLING_STOPPED_THRESHOLD) {
+    // If the velocity is very low, just set it to 0 and stop the fling,
+    // otherwise we'll just asymptotically approach 0 and the user won't
+    // actually see any changes.
+    mVelocity = 0.0f;
+    return false;
+  } else if (fabsf(mVelocity) >= VELOCITY_THRESHOLD) {
+    mVelocity *= 1.0f - FLING_FRICTION_FAST * aDelta.ToMilliseconds();
+  } else {
+    mVelocity *= 1.0f - FLING_FRICTION_SLOW * aDelta.ToMilliseconds();
+  }
+  return true;
+}
+
+Axis::Overscroll Axis::GetOverscroll() {
+  // If the current pan takes the viewport to the left of or above the current
+  // page rect.
+  bool minus = GetOrigin() < GetPageStart();
+  // If the current pan takes the viewport to the right of or below the current
+  // page rect.
+  bool plus = GetViewportEnd() > GetPageEnd();
+  if (minus && plus) {
+    return OVERSCROLL_BOTH;
+  }
+  if (minus) {
+    return OVERSCROLL_MINUS;
+  }
+  if (plus) {
+    return OVERSCROLL_PLUS;
+  }
+  return OVERSCROLL_NONE;
+}
+
+PRInt32 Axis::GetExcess() {
+  switch (GetOverscroll()) {
+  case OVERSCROLL_MINUS: return GetOrigin() - GetPageStart();
+  case OVERSCROLL_PLUS: return GetViewportEnd() - GetPageEnd();
+  case OVERSCROLL_BOTH: return (GetViewportEnd() - GetPageEnd()) + (GetPageStart() - GetOrigin());
+  default: return 0;
+  }
+}
+
+Axis::Overscroll Axis::DisplacementWillOverscroll(PRInt32 aDisplacement) {
+  // If the current pan plus a displacement takes the viewport to the left of or
+  // above the current page rect.
+  bool minus = GetOrigin() + aDisplacement < GetPageStart();
+  // If the current pan plus a displacement takes the viewport to the right of or
+  // below the current page rect.
+  bool plus = GetViewportEnd() + aDisplacement > GetPageEnd();
+  if (minus && plus) {
+    return OVERSCROLL_BOTH;
+  }
+  if (minus) {
+    return OVERSCROLL_MINUS;
+  }
+  if (plus) {
+    return OVERSCROLL_PLUS;
+  }
+  return OVERSCROLL_NONE;
+}
+
+PRInt32 Axis::DisplacementWillOverscrollAmount(PRInt32 aDisplacement) {
+  switch (DisplacementWillOverscroll(aDisplacement)) {
+  case OVERSCROLL_MINUS: return (GetOrigin() + aDisplacement) - GetPageStart();
+  case OVERSCROLL_PLUS: return (GetViewportEnd() + aDisplacement) - GetPageEnd();
+  // Don't handle overscrolled in both directions; a displacement can't cause
+  // this, it must have already been zoomed out too far.
+  default: return 0;
+  }
+}
+
+Axis::Overscroll Axis::ScaleWillOverscroll(float aScale, PRInt32 aFocus) {
+  PRInt32 originAfterScale = NS_lround((GetOrigin() + aFocus) * aScale - aFocus);
+
+  bool both = ScaleWillOverscrollBothSides(aScale);
+  bool minus = originAfterScale < NS_lround(GetPageStart() * aScale);
+  bool plus = (originAfterScale + GetViewportLength()) > NS_lround(GetPageEnd() * aScale);
+
+  if ((minus && plus) || both) {
+    return OVERSCROLL_BOTH;
+  }
+  if (minus) {
+    return OVERSCROLL_MINUS;
+  }
+  if (plus) {
+    return OVERSCROLL_PLUS;
+  }
+  return OVERSCROLL_NONE;
+}
+
+PRInt32 Axis::ScaleWillOverscrollAmount(float aScale, PRInt32 aFocus) {
+  PRInt32 originAfterScale = NS_lround((GetOrigin() + aFocus) * aScale - aFocus);
+  switch (ScaleWillOverscroll(aScale, aFocus)) {
+  case OVERSCROLL_MINUS: return originAfterScale - NS_lround(GetPageStart() * aScale);
+  case OVERSCROLL_PLUS: return (originAfterScale + GetViewportLength()) - NS_lround(GetPageEnd() * aScale);
+  // Don't handle OVERSCROLL_BOTH. Client code is expected to deal with it.
+  default: return 0;
+  }
+}
+
+float Axis::GetVelocity() {
+  return mVelocity;
+}
+
+PRInt32 Axis::GetViewportEnd() {
+  return GetOrigin() + GetViewportLength();
+}
+
+PRInt32 Axis::GetPageEnd() {
+  return GetPageStart() + GetPageLength();
+}
+
+PRInt32 Axis::GetOrigin() {
+  nsIntPoint origin = mAsyncPanZoomController->GetFrameMetrics().mViewportScrollOffset;
+  return GetPointOffset(origin);
+}
+
+PRInt32 Axis::GetViewportLength() {
+  nsIntRect viewport = mAsyncPanZoomController->GetFrameMetrics().mViewport;
+  return GetRectLength(viewport);
+}
+
+PRInt32 Axis::GetPageStart() {
+  nsIntRect pageRect = mAsyncPanZoomController->GetFrameMetrics().mContentRect;
+  return GetRectOffset(pageRect);
+}
+
+PRInt32 Axis::GetPageLength() {
+  nsIntRect pageRect = mAsyncPanZoomController->GetFrameMetrics().mContentRect;
+  return GetRectLength(pageRect);
+}
+
+bool Axis::ScaleWillOverscrollBothSides(float aScale) {
+  const FrameMetrics& metrics = mAsyncPanZoomController->GetFrameMetrics();
+
+  float currentScale = metrics.mResolution.width;
+  gfx::Rect cssContentRect = metrics.mCSSContentRect;
+  cssContentRect.ScaleRoundIn(currentScale * aScale);
+
+  nsIntRect contentRect = nsIntRect(cssContentRect.x,
+                                    cssContentRect.y,
+                                    cssContentRect.width,
+                                    cssContentRect.height);
+
+  return GetRectLength(contentRect) < GetRectLength(metrics.mViewport);
+}
+
+AxisX::AxisX(AsyncPanZoomController* aAsyncPanZoomController)
+  : Axis(aAsyncPanZoomController)
+{
+
+}
+
+PRInt32 AxisX::GetPointOffset(const nsIntPoint& aPoint)
+{
+  return aPoint.x;
+}
+
+PRInt32 AxisX::GetRectLength(const nsIntRect& aRect)
+{
+  return aRect.width;
+}
+
+PRInt32 AxisX::GetRectOffset(const nsIntRect& aRect)
+{
+  return aRect.x;
+}
+
+AxisY::AxisY(AsyncPanZoomController* aAsyncPanZoomController)
+  : Axis(aAsyncPanZoomController)
+{
+
+}
+
+PRInt32 AxisY::GetPointOffset(const nsIntPoint& aPoint)
+{
+  return aPoint.y;
+}
+
+PRInt32 AxisY::GetRectLength(const nsIntRect& aRect)
+{
+  return aRect.height;
+}
+
+PRInt32 AxisY::GetRectOffset(const nsIntRect& aRect)
+{
+  return aRect.y;
+}
+
+}
+}
new file mode 100644
--- /dev/null
+++ b/gfx/layers/ipc/Axis.h
@@ -0,0 +1,188 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=4 ts=8 et 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/. */
+
+#ifndef mozilla_layers_Axis_h
+#define mozilla_layers_Axis_h
+
+#include "nsGUIEvent.h"
+#include "mozilla/TimeStamp.h"
+
+namespace mozilla {
+namespace layers {
+
+class AsyncPanZoomController;
+
+/**
+ * Helper class to maintain each axis of movement (X,Y) for panning and zooming.
+ * Note that everything here is specific to one axis; that is, the X axis knows
+ * nothing about the Y axis and vice versa.
+ */
+class Axis {
+public:
+  Axis(AsyncPanZoomController* aAsyncPanZoomController);
+
+  enum Overscroll {
+    // Overscroll is not happening at all.
+    OVERSCROLL_NONE = 0,
+    // Overscroll is happening in the negative direction. This means either to
+    // the left or to the top depending on the axis.
+    OVERSCROLL_MINUS,
+    // Overscroll is happening in the positive direction. This means either to
+    // the right or to the bottom depending on the axis.
+    OVERSCROLL_PLUS,
+    // Overscroll is happening both ways. This only means something when the
+    // page is scaled out to a smaller size than the viewport.
+    OVERSCROLL_BOTH
+  };
+
+  /**
+   * Notify this Axis that a new touch has been received, including a time delta
+   * indicating how long it has been since the previous one. This triggers a
+   * recalculation of velocity.
+   */
+  void UpdateWithTouchAtDevicePoint(PRInt32 aPos, PRInt32 aTimeDelta);
+
+  /**
+   * Notify this Axis that a touch has begun, i.e. the user has put their finger
+   * on the screen but has not yet tried to pan.
+   */
+  void StartTouch(PRInt32 aPos);
+
+  /**
+   * Notify this Axis that a touch has ended. Useful for stopping flings when a
+   * user puts their finger down in the middle of one (i.e. to stop a previous
+   * touch including its fling so that a new one can take its place).
+   */
+  void StopTouch();
+
+  /**
+   * Gets displacement that should have happened since the previous touch.
+   * Note: Does not reset the displacement. It gets recalculated on the next
+   * updateWithTouchAtDevicePoint(), however it is not safe to assume this will
+   * be the same on every call. This also checks for page boundaries and will
+   * return an adjusted displacement to prevent the viewport from overscrolling
+   * the page rect. An example of where this might matter is when you call it,
+   * apply a displacement that takes you to the boundary of the page, then call
+   * it again. The result will be different in this case.
+   */
+  PRInt32 UpdateAndGetDisplacement(float aScale);
+
+  /**
+   * Gets the distance between the starting position of the touch supplied in
+   * startTouch() and the current touch from the last
+   * updateWithTouchAtDevicePoint().
+   */
+  float PanDistance();
+
+  /**
+   * Applies friction during a fling, or cancels the fling if the velocity is
+   * too low. Returns true if the fling should continue to another frame, or
+   * false if it should end. |aDelta| is the amount of time that has passed
+   * since the last time friction was applied.
+   */
+  bool FlingApplyFrictionOrCancel(const TimeDuration& aDelta);
+
+  /**
+   * Gets the overscroll state of the axis in its current position.
+   */
+  Overscroll GetOverscroll();
+
+  /**
+   * If there is overscroll, returns the amount. Sign depends on in what
+   * direction it is overscrolling. Positive excess means that it is
+   * overscrolling in the positive direction, whereas negative excess means
+   * that it is overscrolling in the negative direction. If there is overscroll
+   * in both directions, this returns 0; it assumes that you check
+   * GetOverscroll() first.
+   */
+  PRInt32 GetExcess();
+
+  /**
+   * Gets the raw velocity of this axis at this moment.
+   */
+  float GetVelocity();
+
+  /**
+   * Gets the overscroll state of the axis given an additional displacement.
+   * That is to say, if the given displacement is applied, this will tell you
+   * whether or not it will overscroll, and in what direction.
+   */
+  Overscroll DisplacementWillOverscroll(PRInt32 aDisplacement);
+
+  /**
+   * If a displacement will overscroll the axis, this returns the amount and in
+   * what direction. Similar to getExcess() but takes a displacement to apply.
+   */
+  PRInt32 DisplacementWillOverscrollAmount(PRInt32 aDisplacement);
+
+  /**
+   * Gets the overscroll state of the axis given a scaling of the page. That is
+   * to say, if the given scale is applied, this will tell you whether or not
+   * it will overscroll, and in what direction.
+   *
+   * |aFocus| is the point at which the scale is focused at. We will offset the
+   * scroll offset in such a way that it remains in the same place on the page
+   * relative.
+   */
+  Overscroll ScaleWillOverscroll(float aScale, PRInt32 aFocus);
+
+  /**
+   * If a scale will overscroll the axis, this returns the amount and in what
+   * direction. Similar to getExcess() but takes a displacement to apply.
+   *
+   * |aFocus| is the point at which the scale is focused at. We will offset the
+   * scroll offset in such a way that it remains in the same place on the page
+   * relative.
+   */
+  PRInt32 ScaleWillOverscrollAmount(float aScale, PRInt32 aFocus);
+
+  /**
+   * Checks if an axis will overscroll in both directions by computing the
+   * content rect and checking that its height/width (depending on the axis)
+   * does not overextend past the viewport.
+   *
+   * This gets called by ScaleWillOverscroll().
+   */
+  bool ScaleWillOverscrollBothSides(float aScale);
+
+  PRInt32 GetOrigin();
+  PRInt32 GetViewportLength();
+  PRInt32 GetPageStart();
+  PRInt32 GetPageLength();
+  PRInt32 GetViewportEnd();
+  PRInt32 GetPageEnd();
+
+  virtual PRInt32 GetPointOffset(const nsIntPoint& aPoint) = 0;
+  virtual PRInt32 GetRectLength(const nsIntRect& aRect) = 0;
+  virtual PRInt32 GetRectOffset(const nsIntRect& aRect) = 0;
+
+protected:
+  PRInt32 mPos;
+  PRInt32 mStartPos;
+  float mVelocity;
+  nsRefPtr<AsyncPanZoomController> mAsyncPanZoomController;
+};
+
+class AxisX : public Axis {
+public:
+  AxisX(AsyncPanZoomController* mAsyncPanZoomController);
+  virtual PRInt32 GetPointOffset(const nsIntPoint& aPoint);
+  virtual PRInt32 GetRectLength(const nsIntRect& aRect);
+  virtual PRInt32 GetRectOffset(const nsIntRect& aRect);
+};
+
+class AxisY : public Axis {
+public:
+  AxisY(AsyncPanZoomController* mAsyncPanZoomController);
+  virtual PRInt32 GetPointOffset(const nsIntPoint& aPoint);
+  virtual PRInt32 GetRectLength(const nsIntRect& aRect);
+  virtual PRInt32 GetRectOffset(const nsIntRect& aRect);
+};
+
+}
+}
+
+#endif
new file mode 100644
--- /dev/null
+++ b/gfx/layers/ipc/GeckoContentController.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=4 ts=8 et 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/. */
+
+#ifndef mozilla_layers_GeckoContentController_h
+#define mozilla_layers_GeckoContentController_h
+
+#include "Layers.h"
+
+namespace mozilla {
+namespace layers {
+
+class GeckoContentController {
+public:
+  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GeckoContentController)
+
+  /**
+   * Requests a paint of the given FrameMetrics |aFrameMetrics| from Gecko.
+   * Implementations per-platform are responsible for actually handling this.
+   */
+  virtual void RequestContentRepaint(const FrameMetrics& aFrameMetrics) = 0;
+};
+
+}
+}
+
+#endif // mozilla_layers_GeckoContentController_h
new file mode 100644
--- /dev/null
+++ b/gfx/layers/ipc/GestureEventListener.cpp
@@ -0,0 +1,208 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=4 ts=8 et 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/. */
+
+#include "GestureEventListener.h"
+#include "AsyncPanZoomController.h"
+
+namespace mozilla {
+namespace layers {
+
+GestureEventListener::GestureEventListener(AsyncPanZoomController* aAsyncPanZoomController)
+  : mAsyncPanZoomController(aAsyncPanZoomController),
+    mState(NoGesture)
+{
+}
+
+GestureEventListener::~GestureEventListener()
+{
+}
+
+nsEventStatus GestureEventListener::HandleInputEvent(const InputData& aEvent)
+{
+  if (aEvent.mInputType != MULTITOUCH_INPUT) {
+    return nsEventStatus_eIgnore;
+  }
+
+  const MultiTouchInput& event = static_cast<const MultiTouchInput&>(aEvent);
+  switch (event.mType)
+  {
+  case MultiTouchInput::MULTITOUCH_START:
+  case MultiTouchInput::MULTITOUCH_ENTER: {
+    for (size_t i = 0; i < event.mTouches.Length(); i++) {
+      bool foundAlreadyExistingTouch = false;
+      for (size_t j = 0; j < mTouches.Length(); j++) {
+        if (mTouches[j].mIdentifier == event.mTouches[i].mIdentifier) {
+          foundAlreadyExistingTouch = true;
+        }
+      }
+
+      NS_WARN_IF_FALSE(!foundAlreadyExistingTouch, "Tried to add a touch that already exists");
+
+      // If we didn't find a touch in our list that matches this, then add it.
+      // If it already existed, we don't want to add it twice because that
+      // messes with our touch move/end code.
+      if (!foundAlreadyExistingTouch) {
+        mTouches.AppendElement(event.mTouches[i]);
+      }
+    }
+
+    if (mTouches.Length() == 2) {
+      // Another finger has been added; it can't be a tap anymore.
+      HandleTapCancel(event);
+    }
+
+    break;
+  }
+  case MultiTouchInput::MULTITOUCH_MOVE: {
+    // If we move at all, just bail out of the tap. We need to change this so
+    // that there's some tolerance in the future.
+    HandleTapCancel(event);
+
+    bool foundAlreadyExistingTouch = false;
+    for (size_t i = 0; i < mTouches.Length(); i++) {
+      for (size_t j = 0; j < event.mTouches.Length(); j++) {
+        if (mTouches[i].mIdentifier == event.mTouches[j].mIdentifier) {
+          foundAlreadyExistingTouch = true;
+          mTouches[i] = event.mTouches[j];
+        }
+      }
+    }
+
+    NS_WARN_IF_FALSE(foundAlreadyExistingTouch, "Touch moved, but not in list");
+
+    break;
+  }
+  case MultiTouchInput::MULTITOUCH_END:
+  case MultiTouchInput::MULTITOUCH_LEAVE: {
+    bool foundAlreadyExistingTouch = false;
+    for (size_t i = 0; i < event.mTouches.Length() && !foundAlreadyExistingTouch; i++) {
+      for (size_t j = 0; j < mTouches.Length() && !foundAlreadyExistingTouch; j++) {
+        if (event.mTouches[i].mIdentifier == mTouches[j].mIdentifier) {
+          foundAlreadyExistingTouch = true;
+          mTouches.RemoveElementAt(j);
+        }
+      }
+    }
+
+    NS_WARN_IF_FALSE(foundAlreadyExistingTouch, "Touch ended, but not in list");
+
+    if (event.mTime - mTouchStartTime <= MAX_TAP_TIME) {
+      // XXX: Incorrect use of the tap event. In the future, we want to send this
+      // on NS_TOUCH_END, then have a short timer afterwards which sends
+      // SingleTapConfirmed. Since we don't have double taps yet, this is fine for
+      // now.
+      if (HandleSingleTapUpEvent(event) == nsEventStatus_eConsumeNoDefault) {
+        return nsEventStatus_eConsumeNoDefault;
+      }
+
+      if (HandleSingleTapConfirmedEvent(event) == nsEventStatus_eConsumeNoDefault) {
+        return nsEventStatus_eConsumeNoDefault;
+      }
+    }
+
+    break;
+  }
+  case MultiTouchInput::MULTITOUCH_CANCEL:
+    // This gets called if there's a touch that has to bail for weird reasons
+    // like pinching and then moving away from the window that the pinch was
+    // started in without letting go of the screen.
+    HandlePinchGestureEvent(event, true);
+    break;
+  }
+
+  return HandlePinchGestureEvent(event, false);
+}
+
+nsEventStatus GestureEventListener::HandlePinchGestureEvent(const MultiTouchInput& aEvent, bool aClearTouches)
+{
+  nsEventStatus rv = nsEventStatus_eIgnore;
+
+  if (mTouches.Length() > 1 && !aClearTouches) {
+    const nsIntPoint& firstTouch = mTouches[0].mScreenPoint,
+                      secondTouch = mTouches[mTouches.Length() - 1].mScreenPoint;
+    nsIntPoint focusPoint =
+      nsIntPoint((firstTouch.x + secondTouch.x)/2,
+                 (firstTouch.y + secondTouch.y)/2);
+    float currentSpan =
+      float(NS_hypot(firstTouch.x - secondTouch.x,
+                     firstTouch.y - secondTouch.y));
+
+    if (mState == NoGesture) {
+      PinchGestureInput pinchEvent(PinchGestureInput::PINCHGESTURE_START,
+                                   aEvent.mTime,
+                                   focusPoint,
+                                   currentSpan,
+                                   currentSpan);
+
+      mAsyncPanZoomController->HandleInputEvent(pinchEvent);
+
+      mState = InPinchGesture;
+    } else {
+      PinchGestureInput pinchEvent(PinchGestureInput::PINCHGESTURE_SCALE,
+                                   aEvent.mTime,
+                                   focusPoint,
+                                   currentSpan,
+                                   mPreviousSpan);
+
+      mAsyncPanZoomController->HandleInputEvent(pinchEvent);
+    }
+
+    mPreviousSpan = currentSpan;
+
+    rv = nsEventStatus_eConsumeNoDefault;
+  } else if (mState == InPinchGesture) {
+    PinchGestureInput pinchEvent(PinchGestureInput::PINCHGESTURE_END,
+                                 aEvent.mTime,
+                                 mTouches[0].mScreenPoint,
+                                 1.0f,
+                                 1.0f);
+ 
+    mAsyncPanZoomController->HandleInputEvent(pinchEvent);
+
+    mState = NoGesture;
+
+    rv = nsEventStatus_eConsumeNoDefault;
+  }
+
+  if (aClearTouches) {
+    mTouches.Clear();
+  }
+
+  return rv;
+}
+
+nsEventStatus GestureEventListener::HandleSingleTapUpEvent(const MultiTouchInput& aEvent)
+{
+  TapGestureInput tapEvent(TapGestureInput::TAPGESTURE_UP, aEvent.mTime, aEvent.mTouches[0].mScreenPoint);
+  mAsyncPanZoomController->HandleInputEvent(tapEvent);
+
+  return nsEventStatus_eConsumeDoDefault;
+}
+
+nsEventStatus GestureEventListener::HandleSingleTapConfirmedEvent(const MultiTouchInput& aEvent)
+{
+  TapGestureInput tapEvent(TapGestureInput::TAPGESTURE_CONFIRMED, aEvent.mTime, aEvent.mTouches[0].mScreenPoint);
+  mAsyncPanZoomController->HandleInputEvent(tapEvent);
+
+  return nsEventStatus_eConsumeDoDefault;
+}
+
+nsEventStatus GestureEventListener::HandleTapCancel(const MultiTouchInput& aEvent)
+{
+  // XXX: In the future we will have to actually send a cancel notification to
+  // Gecko, but for now since we're doing both the "SingleUp" and
+  // "SingleConfirmed" notifications together, there's no need to cancel either
+  // one.
+  mTouchStartTime = 0;
+  return nsEventStatus_eConsumeDoDefault;
+}
+
+AsyncPanZoomController* GestureEventListener::GetAsyncPanZoomController() {
+  return mAsyncPanZoomController;
+}
+
+}
+}
new file mode 100644
--- /dev/null
+++ b/gfx/layers/ipc/GestureEventListener.h
@@ -0,0 +1,128 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=4 ts=8 et 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/. */
+
+#ifndef mozilla_layers_GestureEventListener_h
+#define mozilla_layers_GestureEventListener_h
+
+#include "mozilla/RefPtr.h"
+#include "InputData.h"
+#include "Axis.h"
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * Platform-non-specific, generalized gesture event listener. This class
+ * intercepts all touches events on their way to AsyncPanZoomController and
+ * determines whether or not they are part of a gesture.
+ *
+ * For example, seeing that two fingers are on the screen means that the user
+ * wants to do a pinch gesture, so we don't forward the touches along to
+ * AsyncPanZoomController since it will think that they are just trying to pan
+ * the screen. Instead, we generate a PinchGestureInput and send that. If the
+ * touch event is not part of a gesture, we just return nsEventStatus_eIgnore
+ * and AsyncPanZoomController is expected to handle it.
+ *
+ * Android doesn't use this class because it has its own built-in gesture event
+ * listeners that should generally be preferred.
+ */
+class GestureEventListener {
+public:
+  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GestureEventListener)
+
+  GestureEventListener(AsyncPanZoomController* aAsyncPanZoomController);
+  ~GestureEventListener();
+
+  // --------------------------------------------------------------------------
+  // These methods must only be called on the controller/UI thread.
+  //
+
+  /**
+   * General input handler for a touch event. If the touch event is not a part
+   * of a gesture, then we pass it along to AsyncPanZoomController. Otherwise,
+   * it gets consumed here and never forwarded along.
+   */
+  nsEventStatus HandleInputEvent(const InputData& aEvent);
+
+  /**
+   * Returns the AsyncPanZoomController stored on this class and used for
+   * callbacks.
+   */
+  AsyncPanZoomController* GetAsyncPanZoomController();
+
+protected:
+  enum GestureState {
+    NoGesture = 0,
+    InPinchGesture
+  };
+
+  /**
+   * Maximum time for a touch on the screen and corresponding lift of the finger
+   * to be considered a tap.
+   */
+  enum { MAX_TAP_TIME = 500 };
+
+  /**
+   * Attempts to handle the event as a pinch event. If it is not a pinch event,
+   * then we simply tell the next consumer to consume the event instead.
+   *
+   * |aClearTouches| marks whether or not to terminate any pinch currently
+   * happening.
+   */
+  nsEventStatus HandlePinchGestureEvent(const MultiTouchInput& aEvent, bool aClearTouches);
+
+  /**
+   * Attempts to handle the event as a single tap event, which highlights links
+   * before opening them. In general, this will not attempt to block the touch
+   * event from being passed along to AsyncPanZoomController since APZC needs to
+   * know about touches ending (and we only know if a touch was a tap once it
+   * ends).
+   */
+  nsEventStatus HandleSingleTapUpEvent(const MultiTouchInput& aEvent);
+
+  /**
+   * Attempts to handle a single tap confirmation. This is what will actually
+   * open links, etc. In general, this will not attempt to block the touch event
+   * from being passed along to AsyncPanZoomController since APZC needs to know
+   * about touches ending (and we only know if a touch was a tap once it ends).
+   */
+  nsEventStatus HandleSingleTapConfirmedEvent(const MultiTouchInput& aEvent);
+
+  /**
+   * Attempts to handle a tap event cancellation. This happens when we think
+   * something was a tap but it actually wasn't. In general, this will not
+   * attempt to block the touch event from being passed along to
+   * AsyncPanZoomController since APZC needs to know about touches ending (and
+   * we only know if a touch was a tap once it ends).
+   */
+  nsEventStatus HandleTapCancel(const MultiTouchInput& aEvent);
+
+  nsRefPtr<AsyncPanZoomController> mAsyncPanZoomController;
+
+  /**
+   * Array containing all active touches. When a touch happens it, gets added to
+   * this array, even if we choose not to handle it. When it ends, we remove it.
+   */
+  nsTArray<SingleTouchData> mTouches;
+  GestureState mState;
+
+  /**
+   * Previous span calculated for the purposes of setting inside a
+   * PinchGestureInput.
+   */
+  float mPreviousSpan;
+
+  /**
+   * Stores the time a touch started, used for detecting a tap gesture. Only
+   * valid when there's exactly one touch in mTouches.
+   */
+  PRUint64 mTouchStartTime;
+};
+
+}
+}
+
+#endif
new file mode 100644
--- /dev/null
+++ b/widget/InputData.h
@@ -0,0 +1,325 @@
+/* -*- 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 InputData_h__
+#define InputData_h__
+
+#include "nsGUIEvent.h"
+#include "nsDOMTouchEvent.h"
+#include "nsDebug.h"
+
+namespace mozilla {
+
+
+enum InputType
+{
+  MULTITOUCH_INPUT,
+  PINCHGESTURE_INPUT,
+  TAPGESTURE_INPUT
+};
+
+class MultiTouchInput;
+class PinchGestureInput;
+class TapGestureInput;
+
+// This looks unnecessary now, but as we add more and more classes that derive
+// from InputType (eventually probably almost as many as nsGUIEvent.h has), it
+// will be more and more clear what's going on with a macro that shortens the
+// definition of the RTTI functions.
+#define INPUTDATA_AS_CHILD_TYPE(type, enumID) \
+  const type& As##type() const \
+  { \
+    NS_ABORT_IF_FALSE(mInputType == enumID, "Invalid cast of InputData."); \
+    return (const type&) *this; \
+  }
+
+/** Base input data class. Should never be instantiated. */
+class InputData
+{
+public:
+  InputType mInputType;
+  // Time in milliseconds that this data is relevant to. This only really
+  // matters when this data is used as an event. We use PRUint32 instead of
+  // TimeStamp because it is easier to convert from nsInputEvent. The time is
+  // platform-specific but it in the case of B2G and Fennec it is since startup.
+  PRUint32 mTime;
+
+  INPUTDATA_AS_CHILD_TYPE(MultiTouchInput, MULTITOUCH_INPUT)
+  INPUTDATA_AS_CHILD_TYPE(PinchGestureInput, PINCHGESTURE_INPUT)
+  INPUTDATA_AS_CHILD_TYPE(TapGestureInput, TAPGESTURE_INPUT)
+
+protected:
+  InputData(InputType aInputType, PRUint32 aTime)
+    : mInputType(aInputType),
+      mTime(aTime)
+  {
+
+
+  }
+};
+
+/**
+ * Data container for a single touch input. Similar to nsDOMTouch, but used in
+ * off-main-thread situations. This is more for just storing touch data, whereas
+ * nsDOMTouch derives from nsIDOMTouch so it is more useful for dispatching
+ * through the DOM (which can only happen on the main thread). nsDOMTouch also
+ * bears the problem of storing pointers to nsIWidget instances which can only
+ * be used on the main thread, so if instead we used nsDOMTouch and ever set
+ * these pointers off-main-thread, Bad Things Can Happen(tm).
+ *
+ * Note that this doesn't inherit from InputData because this itself is not an
+ * event. It is only a container/struct that should have any number of instances
+ * within a MultiTouchInput.
+ *
+ * fixme/bug 775746: Make nsDOMTouch inherit from this class.
+ */
+class SingleTouchData
+{
+public:
+  SingleTouchData(PRInt32 aIdentifier,
+                  nsIntPoint aScreenPoint,
+                  nsIntPoint aRadius,
+                  float aRotationAngle,
+                  float aForce)
+    : mIdentifier(aIdentifier),
+      mScreenPoint(aScreenPoint),
+      mRadius(aRadius),
+      mRotationAngle(aRotationAngle),
+      mForce(aForce)
+  {
+
+
+  }
+
+  // A unique number assigned to each SingleTouchData within a MultiTouchInput so
+  // that they can be easily distinguished when handling a touch start/move/end.
+  PRInt32 mIdentifier;
+
+  // Point on the screen that the touch hit, in device pixels. They are
+  // coordinates on the screen.
+  nsIntPoint mScreenPoint;
+
+  // Radius that the touch covers, i.e. if you're using your thumb it will
+  // probably be larger than using your pinky, even with the same force.
+  // Radius can be different along x and y. For example, if you press down with
+  // your entire finger vertically, the y radius will be much larger than the x
+  // radius.
+  nsIntPoint mRadius;
+
+  float mRotationAngle;
+
+  // How hard the screen is being pressed.
+  float mForce;
+};
+
+/**
+ * Similar to nsTouchEvent, but for use off-main-thread. Also only stores a
+ * screen touch point instead of the many different coordinate spaces nsTouchEvent
+ * stores its touch point in. This includes a way to initialize itself from an
+ * nsTouchEvent by copying all relevant data over. Note that this copying from
+ * nsTouchEvent functionality can only be used on the main thread.
+ *
+ * Stores an array of SingleTouchData.
+ */
+class MultiTouchInput : public InputData
+{
+public:
+  enum MultiTouchType
+  {
+    MULTITOUCH_START,
+    MULTITOUCH_MOVE,
+    MULTITOUCH_END,
+    MULTITOUCH_ENTER,
+    MULTITOUCH_LEAVE,
+    MULTITOUCH_CANCEL
+  };
+
+  MultiTouchInput(MultiTouchType aType, PRUint32 aTime)
+    : InputData(MULTITOUCH_INPUT, aTime),
+      mType(aType)
+  {
+
+
+  }
+
+  MultiTouchInput(const nsTouchEvent& aTouchEvent)
+    : InputData(MULTITOUCH_INPUT, aTouchEvent.time)
+  {
+    NS_ABORT_IF_FALSE(NS_IsMainThread(),
+                      "Can only copy from nsTouchEvent on main thread");
+
+    switch (aTouchEvent.message) {
+      case NS_TOUCH_START:
+        mType = MULTITOUCH_START;
+        break;
+      case NS_TOUCH_MOVE:
+        mType = MULTITOUCH_MOVE;
+        break;
+      case NS_TOUCH_END:
+        mType = MULTITOUCH_END;
+        break;
+      case NS_TOUCH_ENTER:
+        mType = MULTITOUCH_ENTER;
+        break;
+      case NS_TOUCH_LEAVE:
+        mType = MULTITOUCH_LEAVE;
+        break;
+      case NS_TOUCH_CANCEL:
+        mType = MULTITOUCH_CANCEL;
+        break;
+      default:
+        NS_WARNING("Did not assign a type to a MultiTouchInput");
+        break;
+    }
+
+    for (size_t i = 0; i < aTouchEvent.touches.Length(); i++) {
+      nsDOMTouch* domTouch = (nsDOMTouch*)(aTouchEvent.touches[i].get());
+
+      // Extract data from weird interfaces.
+      PRInt32 identifier, radiusX, radiusY;
+      float rotationAngle, force;
+      domTouch->GetIdentifier(&identifier);
+      domTouch->GetRadiusX(&radiusX);
+      domTouch->GetRadiusY(&radiusY);
+      domTouch->GetRotationAngle(&rotationAngle);
+      domTouch->GetForce(&force);
+
+      SingleTouchData data(identifier,
+                           domTouch->mRefPoint,
+                           nsIntPoint(radiusX, radiusY),
+                           rotationAngle,
+                           force);
+
+      mTouches.AppendElement(data);
+    }
+  }
+
+  // This conversion from nsMouseEvent to MultiTouchInput is needed because on
+  // the B2G emulator we can only receive mouse events, but we need to be able
+  // to pan correctly. To do this, we convert the events into a format that the
+  // panning code can handle. This code is very limited and only supports
+  // SingleTouchData. It also sends garbage for the identifier, radius, force
+  // and rotation angle.
+  MultiTouchInput(const nsMouseEvent& aMouseEvent)
+    : InputData(MULTITOUCH_INPUT, aMouseEvent.time)
+  {
+    NS_ABORT_IF_FALSE(NS_IsMainThread(),
+                      "Can only copy from nsMouseEvent on main thread");
+    switch (aMouseEvent.message) {
+    case NS_MOUSE_BUTTON_DOWN:
+      mType = MULTITOUCH_START;
+      break;
+    case NS_MOUSE_MOVE:
+      mType = MULTITOUCH_MOVE;
+      break;
+    case NS_MOUSE_BUTTON_UP:
+      mType = MULTITOUCH_END;
+      break;
+    // The mouse pointer has been interrupted in an implementation-specific
+    // manner, such as a synchronous event or action cancelling the touch, or a
+    // touch point leaving the document window and going into a non-document
+    // area capable of handling user interactions.
+    case NS_MOUSE_EXIT:
+      mType = MULTITOUCH_CANCEL;
+      break;
+    default:
+      NS_WARNING("Did not assign a type to a MultiTouchInput");
+      break;
+    }
+
+    mTouches.AppendElement(SingleTouchData(0,
+                                           aMouseEvent.refPoint,
+                                           nsIntPoint(1, 1),
+                                           180.0f,
+                                           1.0f));
+  }
+
+  MultiTouchType mType;
+  nsTArray<SingleTouchData> mTouches;
+};
+
+/**
+ * Encapsulation class for pinch events. In general, these will be generated by
+ * a gesture listener by looking at SingleTouchData/MultiTouchInput instances and
+ * determining whether or not the user was trying to do a gesture.
+ */
+class PinchGestureInput : public InputData
+{
+public:
+  enum PinchGestureType
+  {
+    PINCHGESTURE_START,
+    PINCHGESTURE_SCALE,
+    PINCHGESTURE_END
+  };
+
+  PinchGestureInput(PinchGestureType aType,
+                    PRUint32 aTime,
+                    const nsIntPoint& aFocusPoint,
+                    float aCurrentSpan,
+                    float aPreviousSpan)
+    : InputData(PINCHGESTURE_INPUT, aTime),
+      mType(aType),
+      mFocusPoint(aFocusPoint),
+      mCurrentSpan(aCurrentSpan),
+      mPreviousSpan(aPreviousSpan)
+  {
+
+
+  }
+
+  PinchGestureType mType;
+
+  // Center point of the pinch gesture. That is, if there are two fingers on the
+  // screen, it is their midpoint. In the case of more than two fingers, the
+  // point is implementation-specific, but can for example be the midpoint
+  // between the very first and very last touch. This is in device pixels and
+  // are the coordinates on the screen of this midpoint.
+  nsIntPoint mFocusPoint;
+
+  // The distance in device pixels (though as a float for increased precision
+  // and because it is the distance along both the x and y axis) between the
+  // touches responsible for the pinch gesture.
+  float mCurrentSpan;
+
+  // The previous |mCurrentSpan| in the PinchGestureInput preceding this one.
+  // This is only really relevant during a PINCHGESTURE_SCALE because when it is
+  // of this type then there must have been a history of spans.
+  float mPreviousSpan;
+};
+
+/**
+ * Encapsulation class for tap events. In general, these will be generated by
+ * a gesture listener by looking at SingleTouchData/MultiTouchInput instances and
+ * determining whether or not the user was trying to do a gesture.
+ */
+class TapGestureInput : public InputData
+{
+public:
+  enum TapGestureType
+  {
+    TAPGESTURE_LONG,
+    TAPGESTURE_UP,
+    TAPGESTURE_CONFIRMED,
+    TAPGESTURE_DOUBLE,
+    TAPGESTURE_CANCEL
+  };
+
+  TapGestureInput(TapGestureType aType, PRUint32 aTime, const nsIntPoint& aPoint)
+    : InputData(TAPGESTURE_INPUT, aTime),
+      mType(aType),
+      mPoint(aPoint)
+  {
+
+
+  }
+
+  TapGestureType mType;
+  nsIntPoint mPoint;
+};
+
+}
+
+#endif // InputData_h__
--- a/widget/Makefile.in
+++ b/widget/Makefile.in
@@ -57,16 +57,17 @@ EXPORTS_mozilla = \
 
 ifdef MOZ_INSTRUMENT_EVENT_LOOP
 EXPORTS_mozilla += \
 		WidgetTraceEvent.h \
 		$(NULL)
 endif
 
 EXPORTS		= \
+		InputData.h \
 		nsIWidget.h \
 		nsGUIEvent.h \
 		nsEvent.h \
 		nsNativeWidget.h \
 		nsWidgetInitData.h \
 		nsWidgetsCID.h \
 		nsIPluginWidget.h \
 		nsINativeKeyBindings.h \