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 105885 67bb92bf9989e9aec99316cd2571cf6721b87ad0
parent 105884 ecb9536114625eb610b6506dc13e33362acfc624
child 105886 19b28e14df61d51ff1666b1a64ca44a26e093231
push id214
push userakeybl@mozilla.com
push dateWed, 14 Nov 2012 20:38:59 +0000
treeherdermozilla-release@c8b08ec8e1aa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerscjones, roc, smaug
bugs750974
milestone17.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 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 \