Bug 924692 - Part 3: Add TouchCaret; r=roc, bugs
authorPhoebe Chang <phchang@mozilla.com>
Tue, 03 Jun 2014 15:08:45 +0800
changeset 205467 a1582fd3bf01933847d6a426b979a10a88e55ac1
parent 205466 b8d910d939131a48fc3e95bf8e3dcd64a126254b
child 205468 1d2b67fc93f795347c1af802d84ce0c931aed7e1
push id3741
push userasasaki@mozilla.com
push dateMon, 21 Jul 2014 20:25:18 +0000
treeherdermozilla-beta@4d6f46f5af68 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersroc, bugs
bugs924692
milestone32.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 924692 - Part 3: Add TouchCaret; r=roc, bugs
layout/base/TouchCaret.cpp
layout/base/TouchCaret.h
layout/base/moz.build
modules/libpref/src/init/all.js
new file mode 100644
--- /dev/null
+++ b/layout/base/TouchCaret.cpp
@@ -0,0 +1,814 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 sw=2 et tw=78: */
+/* 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 "TouchCaret.h"
+
+#include "nsCOMPtr.h"
+#include "nsFrameSelection.h"
+#include "nsIFrame.h"
+#include "nsIScrollableFrame.h"
+#include "nsIDOMNode.h"
+#include "nsISelection.h"
+#include "nsISelectionPrivate.h"
+#include "nsIContent.h"
+#include "nsIPresShell.h"
+#include "nsCanvasFrame.h"
+#include "nsRenderingContext.h"
+#include "nsPresContext.h"
+#include "nsBlockFrame.h"
+#include "nsISelectionController.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/BasicEvents.h"
+#include "nsIDOMWindow.h"
+#include "nsQueryContentEventResult.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsView.h"
+#include "nsDOMTokenList.h"
+#include <algorithm>
+
+using namespace mozilla;
+
+#define TOUCHCARET_LOG(...)
+// #define TOUCHCARET_LOG(...) printf_stderr("TouchCaret: " __VA_ARGS__)
+
+// Click on the boundary of input/textarea will place the caret at the
+// front/end of the content. To advoid this, we need to deflate the content
+// boundary by 61 app units (1 pixel + 1 app unit).
+static const int32_t kBoundaryAppUnits = 61;
+// The auto scroll timer's interval in milliseconds.
+static const int32_t kAutoScrollTimerDelay = 30;
+
+NS_IMPL_ISUPPORTS(TouchCaret, nsISelectionListener)
+
+/*static*/ int32_t TouchCaret::sTouchCaretMaxDistance = 0;
+/*static*/ int32_t TouchCaret::sTouchCaretExpirationTime = 0;
+
+TouchCaret::TouchCaret(nsIPresShell* aPresShell)
+  : mState(TOUCHCARET_NONE),
+    mActiveTouchId(-1),
+    mCaretCenterToDownPointOffsetY(0),
+    mVisible(false)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  static bool addedTouchCaretPref = false;
+  if (!addedTouchCaretPref) {
+    Preferences::AddIntVarCache(&sTouchCaretMaxDistance,
+                                "touchcaret.distance.threshold");
+    Preferences::AddIntVarCache(&sTouchCaretExpirationTime,
+                                "touchcaret.expiration.time");
+    addedTouchCaretPref = true;
+  }
+
+  // The presshell owns us, so no addref.
+  mPresShell = do_GetWeakReference(aPresShell);
+  MOZ_ASSERT(mPresShell, "Hey, pres shell should support weak refs");
+}
+
+TouchCaret::~TouchCaret()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  if (mTouchCaretExpirationTimer) {
+    mTouchCaretExpirationTimer->Cancel();
+    mTouchCaretExpirationTimer = nullptr;
+  }
+}
+
+nsIFrame*
+TouchCaret::GetCanvasFrame()
+{
+  nsCOMPtr<nsIPresShell> presShell = do_QueryReferent(mPresShell);
+  if (!presShell) {
+    return nullptr;
+  }
+  return presShell->GetCanvasFrame();
+}
+
+void
+TouchCaret::SetVisibility(bool aVisible)
+{
+  if (mVisible == aVisible) {
+    return;
+  }
+  mVisible = aVisible;
+
+  nsCOMPtr<nsIPresShell> presShell = do_QueryReferent(mPresShell);
+  if (!presShell) {
+    return;
+  }
+  mozilla::dom::Element* touchCaretElement = presShell->GetTouchCaretElement();
+  if (!touchCaretElement) {
+    return;
+  }
+
+  // Set touch caret visibility.
+  ErrorResult err;
+  touchCaretElement->ClassList()->Toggle(NS_LITERAL_STRING("hidden"),
+                                         dom::Optional<bool>(!mVisible),
+                                         err);
+  // Set touch caret expiration time.
+  mVisible ? LaunchExpirationTimer() : CancelExpirationTimer();
+}
+
+nsRect
+TouchCaret::GetTouchFrameRect()
+{
+  nsCOMPtr<nsIPresShell> presShell = do_QueryReferent(mPresShell);
+  if (!presShell) {
+    return nsRect();
+  }
+
+  dom::Element* touchCaretElement = presShell->GetTouchCaretElement();
+  if (!touchCaretElement) {
+    return nsRect();
+  }
+
+  // Get touch caret position relative to canvas frame.
+  nsIFrame* touchCaretFrame = touchCaretElement->GetPrimaryFrame();
+  nsRect tcRect = touchCaretFrame->GetRectRelativeToSelf();
+  nsIFrame* canvasFrame = GetCanvasFrame();
+
+  nsLayoutUtils::TransformResult rv =
+    nsLayoutUtils::TransformRect(touchCaretFrame, canvasFrame, tcRect);
+  return rv == nsLayoutUtils::TRANSFORM_SUCCEEDED ? tcRect : nsRect();
+}
+
+nsRect
+TouchCaret::GetContentBoundary()
+{
+  nsCOMPtr<nsIPresShell> presShell = do_QueryReferent(mPresShell);
+  if (!presShell) {
+    return nsRect();
+  }
+
+  nsRefPtr<nsCaret> caret = presShell->GetCaret();
+  nsISelection* caretSelection = caret->GetCaretDOMSelection();
+  nsRect focusRect;
+  nsIFrame* focusFrame = caret->GetGeometry(caretSelection, &focusRect);
+  nsIFrame* canvasFrame = GetCanvasFrame();
+
+  // Get the editing host to determine the touch caret dragable boundary.
+  dom::Element* editingHost = focusFrame->GetContent()->GetEditingHost();
+  if (!editingHost) {
+    return nsRect();
+  }
+
+  nsRect resultRect;
+  for (nsIFrame* frame = editingHost->GetPrimaryFrame(); frame;
+       frame = frame->GetNextContinuation()) {
+    nsRect rect = frame->GetContentRectRelativeToSelf();
+    nsLayoutUtils::TransformRect(frame, canvasFrame, rect);
+    resultRect = resultRect.Union(rect);
+
+    mozilla::layout::FrameChildListIterator lists(frame);
+    for (; !lists.IsDone(); lists.Next()) {
+      // Loop over all children to take the overflow rect in to consideration.
+      nsFrameList::Enumerator childFrames(lists.CurrentList());
+      for (; !childFrames.AtEnd(); childFrames.Next()) {
+        nsIFrame* kid = childFrames.get();
+        nsRect overflowRect = kid->GetScrollableOverflowRect();
+        nsLayoutUtils::TransformRect(kid, canvasFrame, overflowRect);
+        resultRect = resultRect.Union(overflowRect);
+      }
+    }
+  }
+  // Shrink rect to make sure we never hit the boundary.
+  resultRect.Deflate(kBoundaryAppUnits);
+
+  return resultRect;
+}
+
+nscoord
+TouchCaret::GetCaretYCenterPosition()
+{
+  nsCOMPtr<nsIPresShell> presShell = do_QueryReferent(mPresShell);
+  if (!presShell) {
+    return 0;
+  }
+
+  nsRefPtr<nsCaret> caret = presShell->GetCaret();
+  nsISelection* caretSelection = caret->GetCaretDOMSelection();
+  nsRect focusRect;
+  nsIFrame* focusFrame = caret->GetGeometry(caretSelection, &focusRect);
+  nsRect caretRect = focusFrame->GetRectRelativeToSelf();
+  nsIFrame *canvasFrame = GetCanvasFrame();
+  nsLayoutUtils::TransformRect(focusFrame, canvasFrame, caretRect);
+
+  return (caretRect.y + caretRect.height / 2);
+}
+
+void
+TouchCaret::SetTouchFramePos(const nsPoint& aOrigin)
+{
+  nsCOMPtr<nsIPresShell> presShell = do_QueryReferent(mPresShell);
+  if (!presShell) {
+    return;
+  }
+
+  mozilla::dom::Element* touchCaretElement = presShell->GetTouchCaretElement();
+  if (!touchCaretElement) {
+    return;
+  }
+
+  // Convert aOrigin to CSS pixels.
+  nsRefPtr<nsPresContext> presContext = presShell->GetPresContext();
+  int32_t x = presContext->AppUnitsToIntCSSPixels(aOrigin.x);
+  int32_t y = presContext->AppUnitsToIntCSSPixels(aOrigin.y);
+
+  nsAutoString styleStr;
+  styleStr.AppendLiteral("left: ");
+  styleStr.AppendInt(x);
+  styleStr.AppendLiteral("px; top: ");
+  styleStr.AppendInt(y);
+  styleStr.AppendLiteral("px;");
+
+  touchCaretElement->SetAttr(kNameSpaceID_None, nsGkAtoms::style,
+                             styleStr, true);
+}
+
+void
+TouchCaret::MoveCaret(const nsPoint& movePoint)
+{
+  nsCOMPtr<nsIPresShell> presShell = do_QueryReferent(mPresShell);
+  if (!presShell) {
+    return;
+  }
+
+  // Get scrollable frame.
+  nsRefPtr<nsCaret> caret = presShell->GetCaret();
+  nsISelection* caretSelection = caret->GetCaretDOMSelection();
+  nsRect focusRect;
+  nsIFrame* focusFrame = caret->GetGeometry(caretSelection, &focusRect);
+  nsIFrame* scrollable =
+    nsLayoutUtils::GetClosestFrameOfType(focusFrame, nsGkAtoms::scrollFrame);
+
+  // Convert touch/mouse position to frame coordinates.
+  nsIFrame* canvasFrame = GetCanvasFrame();
+  if (!canvasFrame) {
+    return;
+  }
+  nsPoint offsetToCanvasFrame = nsPoint(0,0);
+  nsLayoutUtils::TransformPoint(scrollable, canvasFrame, offsetToCanvasFrame);
+  nsPoint pt = movePoint - offsetToCanvasFrame;
+
+  // Evaluate offsets.
+  nsIFrame::ContentOffsets offsets =
+    scrollable->GetContentOffsetsFromPoint(pt, nsIFrame::SKIP_HIDDEN);
+
+  // Move caret position.
+  nsWeakFrame weakScrollable = scrollable;
+  nsRefPtr<nsFrameSelection> fs = scrollable->GetFrameSelection();
+  fs->HandleClick(offsets.content, offsets.StartOffset(),
+                  offsets.EndOffset(),
+                  false,
+                  false,
+                  offsets.associateWithNext);
+
+  if (!weakScrollable.IsAlive()) {
+    return;
+  }
+
+  // Scroll scrolled frame.
+  nsIScrollableFrame* saf = do_QueryFrame(scrollable);
+  nsIFrame* capturingFrame = saf->GetScrolledFrame();
+  offsetToCanvasFrame = nsPoint(0,0);
+  nsLayoutUtils::TransformPoint(capturingFrame, canvasFrame, offsetToCanvasFrame);
+  pt = movePoint - offsetToCanvasFrame;
+  fs->StartAutoScrollTimer(capturingFrame, pt, kAutoScrollTimerDelay);
+}
+
+bool
+TouchCaret::IsOnTouchCaret(const nsPoint& aPoint)
+{
+  // Return false if touch caret is not visible.
+  if (!mVisible) {
+    return false;
+  }
+
+  nsRect tcRect = GetTouchFrameRect();
+
+  // Check if the click was in the bounding box of the touch caret.
+  int32_t distance;
+  if (tcRect.Contains(aPoint.x, aPoint.y)) {
+    distance = 0;
+  } else {
+    // If click is outside the bounding box of the touch caret, check the
+    // distance to the center of the touch caret.
+    int32_t posX = (tcRect.x + (tcRect.width / 2));
+    int32_t posY = (tcRect.y + (tcRect.height / 2));
+    int32_t dx = Abs(aPoint.x - posX);
+    int32_t dy = Abs(aPoint.y - posY);
+    distance = dx + dy;
+  }
+  return (distance <= TouchCaretMaxDistance());
+}
+
+nsresult
+TouchCaret::NotifySelectionChanged(nsIDOMDocument* aDoc, nsISelection* aSel,
+                                     int16_t aReason)
+{
+  // Hide touch caret while no caret exists.
+  nsCOMPtr<nsIPresShell> presShell = do_QueryReferent(mPresShell);
+  if (!presShell) {
+    return NS_OK;
+  }
+
+  nsRefPtr<nsCaret> caret = presShell->GetCaret();
+  if (!caret) {
+    SetVisibility(false);
+    return NS_OK;
+  }
+
+  // The same touch caret is shared amongst the document and any text widgets it
+  // may contain. This means that the touch caret could get notifications from
+  // multiple selections.
+  // If this notification is for a selection that is not the one the
+  // the caret is currently interested in , then there is nothing to do!
+  if (aSel != caret->GetCaretDOMSelection()) {
+    return NS_OK;
+  }
+
+  // Update touch caret position and visibility.
+  // Hide touch caret while key event causes selection change.
+  if ((aReason == nsISelectionListener::NO_REASON) ||
+      (aReason & nsISelectionListener::KEYPRESS_REASON)) {
+    UpdateTouchCaret(false);
+  } else {
+    UpdateTouchCaret(true);
+  }
+
+  return NS_OK;
+}
+
+void
+TouchCaret::UpdateTouchCaret(bool aVisible)
+{
+  // Hide touch caret while no caret exists.
+  nsCOMPtr<nsIPresShell> presShell = do_QueryReferent(mPresShell);
+  if (!presShell) {
+    return;
+  }
+
+  nsRefPtr<nsCaret> caret = presShell->GetCaret();
+  if (!caret) {
+    SetVisibility(false);
+    return;
+  }
+
+  // Hide touch caret while caret is not visible.
+  bool caretVisible = false;
+  caret->GetCaretVisible(&caretVisible);
+  if (!caretVisible) {
+    SetVisibility(false);
+    return;
+  }
+
+  // Caret is visible and shown, update touch caret.
+  nsISelection* caretSelection = caret->GetCaretDOMSelection();
+  nsRect focusRect;
+  nsIFrame* focusFrame = caret->GetGeometry(caretSelection, &focusRect);
+  if (!focusFrame || focusRect.IsEmpty()) {
+    SetVisibility(false);
+    return;
+  }
+
+  // Position of the touch caret relative to focusFrame.
+  nsPoint pos = nsPoint(focusRect.x + (focusRect.width / 2),
+                        focusRect.y + focusRect.height);
+
+  // Transform the position to make it relative to canvas frame.
+  nsIFrame* canvasFrame = GetCanvasFrame();
+  if (!canvasFrame) {
+    return;
+  }
+  nsLayoutUtils::TransformPoint(focusFrame, canvasFrame, pos);
+
+  // Clamp the touch caret position to the scrollframe boundary.
+  nsIFrame* closestScrollFrame =
+    nsLayoutUtils::GetClosestFrameOfType(focusFrame, nsGkAtoms::scrollFrame);
+  while (closestScrollFrame) {
+    nsIScrollableFrame* sf = do_QueryFrame(closestScrollFrame);
+    nsRect visualRect = sf->GetScrollPortRect();
+    // Clamp the touch caret in the scroll port.
+    nsLayoutUtils::TransformRect(closestScrollFrame, canvasFrame, visualRect);
+    pos = visualRect.ClampPoint(pos);
+
+    // Get next ancestor scroll frame.
+    closestScrollFrame =
+      nsLayoutUtils::GetClosestFrameOfType(closestScrollFrame->GetParent(),
+                                           nsGkAtoms::scrollFrame);
+  }
+
+  SetTouchFramePos(pos);
+  SetVisibility(aVisible);
+}
+
+/* static */void
+TouchCaret::DisableTouchCaretCallback(nsITimer* aTimer, void* aTouchCaret)
+{
+  nsRefPtr<TouchCaret> self = static_cast<TouchCaret*>(aTouchCaret);
+  NS_PRECONDITION(aTimer == self->mTouchCaretExpirationTimer,
+                  "Unexpected timer");
+
+  self->SetVisibility(false);
+}
+
+void
+TouchCaret::LaunchExpirationTimer()
+{
+  if (TouchCaretExpirationTime() > 0) {
+    if (!mTouchCaretExpirationTimer) {
+      mTouchCaretExpirationTimer = do_CreateInstance("@mozilla.org/timer;1");
+    }
+
+    if (mTouchCaretExpirationTimer) {
+      mTouchCaretExpirationTimer->Cancel();
+      mTouchCaretExpirationTimer->InitWithFuncCallback(DisableTouchCaretCallback,
+                                                       this,
+                                                       TouchCaretExpirationTime(),
+                                                       nsITimer::TYPE_ONE_SHOT);
+    }
+  }
+}
+
+void
+TouchCaret::CancelExpirationTimer()
+{
+  if (mTouchCaretExpirationTimer) {
+    mTouchCaretExpirationTimer->Cancel();
+  }
+}
+
+nsEventStatus
+TouchCaret::HandleEvent(WidgetEvent* aEvent)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  nsCOMPtr<nsIPresShell> presShell = do_QueryReferent(mPresShell);
+  if (!presShell) {
+    return nsEventStatus_eIgnore;
+  }
+
+  mozilla::dom::Element* touchCaretElement = presShell->GetTouchCaretElement();
+  if (!touchCaretElement) {
+    return nsEventStatus_eIgnore;
+  }
+
+  nsEventStatus status = nsEventStatus_eIgnore;
+
+  switch (aEvent->message) {
+    case NS_TOUCH_START:
+    case NS_TOUCH_ENTER:
+      status = HandleTouchDownEvent(aEvent->AsTouchEvent());
+      break;
+    case NS_MOUSE_BUTTON_DOWN:
+      status = HandleMouseDownEvent(aEvent->AsMouseEvent());
+      break;
+    case NS_TOUCH_END:
+      status = HandleTouchUpEvent(aEvent->AsTouchEvent());
+      break;
+   case NS_MOUSE_BUTTON_UP:
+      status = HandleMouseUpEvent(aEvent->AsMouseEvent());
+      break;
+    case NS_TOUCH_MOVE:
+      status = HandleTouchMoveEvent(aEvent->AsTouchEvent());
+      break;
+    case NS_MOUSE_MOVE:
+      status = HandleMouseMoveEvent(aEvent->AsMouseEvent());
+      break;
+    case NS_TOUCH_CANCEL:
+      mTouchesId.Clear();
+      SetState(TOUCHCARET_NONE);
+      LaunchExpirationTimer();
+      break;
+    case NS_KEY_UP:
+    case NS_KEY_DOWN:
+    case NS_KEY_PRESS:
+    case NS_WHEEL_EVENT_START:
+      // Disable touch caret while key/wheel event is received.
+      SetVisibility(false);
+      break;
+    default:
+      break;
+  }
+
+  return status;
+}
+
+nsPoint
+TouchCaret::GetEventPosition(WidgetTouchEvent* aEvent, int32_t aIdentifier)
+{
+  for (size_t i = 0; i < aEvent->touches.Length(); i++) {
+    if (aEvent->touches[i]->mIdentifier == aIdentifier) {
+      // Get event coordinate relative to canvas frame.
+      nsIFrame* canvasFrame = GetCanvasFrame();
+      nsIntPoint touchIntPoint = aEvent->touches[i]->mRefPoint;
+      return nsLayoutUtils::GetEventCoordinatesRelativeTo(aEvent,
+                                                          touchIntPoint,
+                                                          canvasFrame);
+    }
+  }
+  return nsPoint(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE);
+}
+
+nsPoint
+TouchCaret::GetEventPosition(WidgetMouseEvent* aEvent)
+{
+  // Get event coordinate relative to canvas frame.
+  nsIFrame* canvasFrame = GetCanvasFrame();
+  nsIntPoint mouseIntPoint =
+    LayoutDeviceIntPoint::ToUntyped(aEvent->AsGUIEvent()->refPoint);
+  return nsLayoutUtils::GetEventCoordinatesRelativeTo(aEvent,
+                                                      mouseIntPoint,
+                                                      canvasFrame);
+}
+
+nsEventStatus
+TouchCaret::HandleMouseMoveEvent(WidgetMouseEvent* aEvent)
+{
+  TOUCHCARET_LOG("%p got a mouse-move in state %d\n", this, mState);
+  nsEventStatus status = nsEventStatus_eIgnore;
+
+  switch (mState) {
+    case TOUCHCARET_NONE:
+      break;
+
+    case TOUCHCARET_MOUSEDRAG_ACTIVE:
+      {
+        nsPoint movePoint = GetEventPosition(aEvent);
+        movePoint.y += mCaretCenterToDownPointOffsetY;
+        nsRect contentBoundary = GetContentBoundary();
+        movePoint = contentBoundary.ClampPoint(movePoint);
+
+        MoveCaret(movePoint);
+        status = nsEventStatus_eConsumeNoDefault;
+      }
+      break;
+
+    case TOUCHCARET_TOUCHDRAG_ACTIVE:
+    case TOUCHCARET_TOUCHDRAG_INACTIVE:
+      // Consume mouse event in touch sequence.
+      status = nsEventStatus_eConsumeNoDefault;
+      break;
+  }
+
+  return status;
+}
+
+nsEventStatus
+TouchCaret::HandleTouchMoveEvent(WidgetTouchEvent* aEvent)
+{
+  TOUCHCARET_LOG("%p got a touch-move in state %d\n", this, mState);
+  nsEventStatus status = nsEventStatus_eIgnore;
+
+  switch (mState) {
+    case TOUCHCARET_NONE:
+      break;
+
+    case TOUCHCARET_MOUSEDRAG_ACTIVE:
+      // Consume touch event in mouse sequence.
+      status = nsEventStatus_eConsumeNoDefault;
+      break;
+
+    case TOUCHCARET_TOUCHDRAG_ACTIVE:
+      {
+        nsPoint movePoint = GetEventPosition(aEvent, mActiveTouchId);
+        movePoint.y += mCaretCenterToDownPointOffsetY;
+        nsRect contentBoundary = GetContentBoundary();
+        movePoint = contentBoundary.ClampPoint(movePoint);
+
+        MoveCaret(movePoint);
+        status = nsEventStatus_eConsumeNoDefault;
+      }
+      break;
+
+    case TOUCHCARET_TOUCHDRAG_INACTIVE:
+      // Consume NS_TOUCH_MOVE event in TOUCHCARET_TOUCHDRAG_INACTIVE state.
+      status = nsEventStatus_eConsumeNoDefault;
+      break;
+  }
+
+  return status;
+}
+
+nsEventStatus
+TouchCaret::HandleMouseUpEvent(WidgetMouseEvent* aEvent)
+{
+  TOUCHCARET_LOG("%p got a mouse-up in state %d\n", this, mState);
+  nsEventStatus status = nsEventStatus_eIgnore;
+
+  switch (mState) {
+    case TOUCHCARET_NONE:
+      break;
+
+    case TOUCHCARET_MOUSEDRAG_ACTIVE:
+      if (aEvent->button == WidgetMouseEvent::eLeftButton) {
+        LaunchExpirationTimer();
+        SetState(TOUCHCARET_NONE);
+        status = nsEventStatus_eConsumeNoDefault;
+      }
+      break;
+
+    case TOUCHCARET_TOUCHDRAG_ACTIVE:
+    case TOUCHCARET_TOUCHDRAG_INACTIVE:
+      // Consume mouse event in touch sequence.
+      status = nsEventStatus_eConsumeNoDefault;
+      break;
+  }
+
+  return status;
+}
+
+nsEventStatus
+TouchCaret::HandleTouchUpEvent(WidgetTouchEvent* aEvent)
+{
+  TOUCHCARET_LOG("%p got a touch-end in state %d\n", this, mState);
+  // Remove touches from cache if the stroke is gone in TOUCHDRAG states.
+  if (mState == TOUCHCARET_TOUCHDRAG_ACTIVE ||
+      mState == TOUCHCARET_TOUCHDRAG_INACTIVE) {
+    for (size_t i = 0; i < aEvent->touches.Length(); i++) {
+      nsTArray<int32_t>::index_type index =
+        mTouchesId.IndexOf(aEvent->touches[i]->mIdentifier);
+      MOZ_ASSERT(index != nsTArray<int32_t>::NoIndex);
+      mTouchesId.RemoveElementAt(index);
+    }
+  }
+
+  nsEventStatus status = nsEventStatus_eIgnore;
+
+  switch (mState) {
+    case TOUCHCARET_NONE:
+      break;
+
+    case TOUCHCARET_MOUSEDRAG_ACTIVE:
+      // Consume touch event in mouse sequence.
+      status = nsEventStatus_eConsumeNoDefault;
+      break;
+
+    case TOUCHCARET_TOUCHDRAG_ACTIVE:
+      if (mTouchesId.Length() == 0) {
+        // No more finger on the screen.
+        SetState(TOUCHCARET_NONE);
+        LaunchExpirationTimer();
+      } else {
+        // Still has finger touching on the screen.
+        if (aEvent->touches[0]->mIdentifier == mActiveTouchId) {
+          // Remove finger from the touch caret.
+          SetState(TOUCHCARET_TOUCHDRAG_INACTIVE);
+          LaunchExpirationTimer();
+        } else {
+          // If the finger removed is not the finger on touch caret, remain in
+          // TOUCHCARET_DRAG_ACTIVE state.
+        }
+      }
+      status = nsEventStatus_eConsumeNoDefault;
+      break;
+
+    case TOUCHCARET_TOUCHDRAG_INACTIVE:
+      if (mTouchesId.Length() == 0) {
+        // No more finger on the screen.
+        SetState(TOUCHCARET_NONE);
+      }
+      status = nsEventStatus_eConsumeNoDefault;
+      break;
+  }
+
+  return status;
+}
+
+nsEventStatus
+TouchCaret::HandleMouseDownEvent(WidgetMouseEvent* aEvent)
+{
+  TOUCHCARET_LOG("%p got a mouse-down in state %d\n", this, mState);
+  if (!GetVisibility()) {
+    // If touch caret is invisible, bypass event.
+    return nsEventStatus_eIgnore;
+  }
+
+  nsEventStatus status = nsEventStatus_eIgnore;
+
+  switch (mState) {
+    case TOUCHCARET_NONE:
+      if (aEvent->button == WidgetMouseEvent::eLeftButton) {
+        nsPoint point = GetEventPosition(aEvent);
+        if (IsOnTouchCaret(point)) {
+          // Cache distence of the event point to the center of touch caret.
+          mCaretCenterToDownPointOffsetY = GetCaretYCenterPosition() - point.y;
+          // Enter TOUCHCARET_MOUSEDRAG_ACTIVE state and cancel the timer.
+          SetState(TOUCHCARET_MOUSEDRAG_ACTIVE);
+          CancelExpirationTimer();
+          status = nsEventStatus_eConsumeNoDefault;
+        } else {
+          // Set touch caret invisible if HisTest fails. Bypass event.
+          SetVisibility(false);
+          status = nsEventStatus_eIgnore;
+        }
+      } else {
+        // Set touch caret invisible if not left button down event.
+        SetVisibility(false);
+        status = nsEventStatus_eIgnore;
+      }
+      break;
+
+    case TOUCHCARET_MOUSEDRAG_ACTIVE:
+      SetVisibility(false);
+      SetState(TOUCHCARET_NONE);
+      break;
+
+    case TOUCHCARET_TOUCHDRAG_ACTIVE:
+    case TOUCHCARET_TOUCHDRAG_INACTIVE:
+      // Consume mouse event in touch sequence.
+      status = nsEventStatus_eConsumeNoDefault;
+      break;
+  }
+
+  return status;
+}
+
+nsEventStatus
+TouchCaret::HandleTouchDownEvent(WidgetTouchEvent* aEvent)
+{
+  TOUCHCARET_LOG("%p got a touch-start in state %d\n", this, mState);
+
+  nsEventStatus status = nsEventStatus_eIgnore;
+
+  switch (mState) {
+    case TOUCHCARET_NONE:
+      if (!GetVisibility()) {
+        // If touch caret is invisible, bypass event.
+        status = nsEventStatus_eIgnore;
+      } else {
+        nsPoint point = GetEventPosition(aEvent, 0);
+        if (IsOnTouchCaret(point)) {
+          // Touch start position is contained in touch caret.
+          mActiveTouchId = aEvent->touches[0]->mIdentifier;
+          // Cache distance of the event point to the center of touch caret.
+          mCaretCenterToDownPointOffsetY = GetCaretYCenterPosition() - point.y;
+          // Enter TOUCHCARET_TOUCHDRAG_ACTIVE state and cancel the timer.
+          SetState(TOUCHCARET_TOUCHDRAG_ACTIVE);
+          CancelExpirationTimer();
+          status = nsEventStatus_eConsumeNoDefault;
+        } else {
+          // Set touch caret invisible if HisTest fails. Bypass event.
+          SetVisibility(false);
+          status = nsEventStatus_eIgnore;
+        }
+      }
+      break;
+
+    case TOUCHCARET_MOUSEDRAG_ACTIVE:
+    case TOUCHCARET_TOUCHDRAG_ACTIVE:
+    case TOUCHCARET_TOUCHDRAG_INACTIVE:
+      // Consume NS_TOUCH_START event.
+      status = nsEventStatus_eConsumeNoDefault;
+      break;
+  }
+
+  // Cache active touch IDs in TOUCHDRAG states.
+  if (mState == TOUCHCARET_TOUCHDRAG_ACTIVE ||
+      mState == TOUCHCARET_TOUCHDRAG_INACTIVE) {
+    mTouchesId.Clear();
+    for (size_t i = 0; i < aEvent->touches.Length(); i++) {
+      mTouchesId.AppendElement(aEvent->touches[i]->mIdentifier);
+    }
+  }
+
+  return status;
+}
+
+void
+TouchCaret::SetState(TouchCaretState aState)
+{
+  TOUCHCARET_LOG("%p state changed from %d to %d\n", this, mState, aState);
+  if (mState == TOUCHCARET_NONE) {
+    MOZ_ASSERT(aState != TOUCHCARET_TOUCHDRAG_INACTIVE,
+               "mState: NONE => TOUCHDRAG_INACTIVE isn't allowed!");
+  }
+
+  if (mState == TOUCHCARET_TOUCHDRAG_ACTIVE) {
+    MOZ_ASSERT(aState != TOUCHCARET_MOUSEDRAG_ACTIVE,
+               "mState: TOUCHDRAG_ACTIVE => MOUSEDRAG_ACTIVE isn't allowed!");
+  }
+
+  if (mState == TOUCHCARET_MOUSEDRAG_ACTIVE) {
+    MOZ_ASSERT(aState == TOUCHCARET_MOUSEDRAG_ACTIVE ||
+               aState == TOUCHCARET_NONE,
+               "MOUSEDRAG_ACTIVE allowed next state: NONE!");
+  }
+
+  if (mState == TOUCHCARET_TOUCHDRAG_INACTIVE) {
+    MOZ_ASSERT(aState == TOUCHCARET_TOUCHDRAG_INACTIVE ||
+               aState == TOUCHCARET_NONE,
+               "TOUCHDRAG_INACTIVE allowed next state: NONE!");
+  }
+
+  mState = aState;
+
+  if (mState == TOUCHCARET_NONE) {
+    mActiveTouchId = -1;
+    mCaretCenterToDownPointOffsetY = 0;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/layout/base/TouchCaret.h
@@ -0,0 +1,240 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 sw=2 et tw=78: */
+/* 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_TouchCaret_h__
+#define mozilla_TouchCaret_h__
+
+#include "nsISelectionListener.h"
+#include "nsIScrollObserver.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsFrameSelection.h"
+#include "nsITimer.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/TouchEvents.h"
+#include "Units.h"
+
+namespace mozilla {
+
+/**
+ * The TouchCaret places a touch caret according to caret postion when the
+ * caret is shown.
+ * TouchCaret is also responsible for touch caret visibility. Touch caret
+ * won't be shown when timer expires or while key event causes selection change.
+ */
+class TouchCaret MOZ_FINAL : public nsISelectionListener
+{
+public:
+  explicit TouchCaret(nsIPresShell* aPresShell);
+  ~TouchCaret();
+
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSISELECTIONLISTENER
+
+  void Terminate()
+  {
+    mPresShell = nullptr;
+  }
+
+  /**
+   * Handle mouse and touch event only.
+   * Depends on visibility and position of touch caret, HandleEvent may consume
+   * that input event and return nsEventStatus_eConsumeNoDefault to the caller.
+   * In that case, caller should stop bubble up that input event.
+   */
+  nsEventStatus HandleEvent(WidgetEvent* aEvent);
+
+  /**
+   * By calling this function, touch caret recalculate touch frame position and
+   * update accordingly.
+   */
+  void UpdateTouchCaret(bool aVisible);
+
+  /**
+   * SetVisibility will set the visibility of the touch caret.
+   * SetVisibility performs an attribute-changed notification which could, in
+   * theory, destroy frames.
+   */
+  void SetVisibility(bool aVisible);
+
+  /**
+   * GetVisibility will get the visibility of the touch caret.
+   */
+  bool GetVisibility() const
+  {
+    return mVisible;
+  }
+
+private:
+  // Hide default constructor.
+  TouchCaret() MOZ_DELETE;
+
+  /**
+   * Find the nsCanvasFrame which holds the touch caret.
+   */
+  nsIFrame* GetCanvasFrame();
+
+  /**
+   * Retrieve the bounding rectangle of the touch caret.
+   *
+   * @returns A nsRect representing the bounding rectangle of this touch caret.
+   *          The returned offset is relative to the canvas frame.
+   */
+  nsRect GetTouchFrameRect();
+
+  /**
+   * Retrieve the bounding rectangle where the caret can be positioned.
+   * If we're positioning a caret in an input field, make sure the touch caret
+   * stays within the bounds of the field.
+   *
+   * @returns A nsRect representing the bounding rectangle of this valid area.
+   *          The returned offset is relative to the canvas frame.
+   */
+  nsRect GetContentBoundary();
+
+  /**
+   * Retrieve the center y position of the caret.
+   * The returned point is relative to the canvas frame.
+   */
+  nscoord GetCaretYCenterPosition();
+
+  /**
+   * Set the position of the touch caret.
+   * Touch caret is an absolute positioned div.
+   */
+  void SetTouchFramePos(const nsPoint& aOrigin);
+
+  void LaunchExpirationTimer();
+  void CancelExpirationTimer();
+  static void DisableTouchCaretCallback(nsITimer* aTimer, void* aPresShell);
+
+  /**
+   * Move the caret to movePoint which is relative to the canvas frame.
+   * Caret will be scrolled into view.
+   *
+   * @param movePoint tap location relative to the canvas frame.
+   */
+  void MoveCaret(const nsPoint& movePoint);
+
+  /**
+   * Check if aPoint is inside the touch caret frame.
+   *
+   * @param aPoint tap location relative to the canvas frame.
+   */
+  bool IsOnTouchCaret(const nsPoint& aPoint);
+
+  /**
+   * These Handle* functions comprise input alphabet of the TouchCaret
+   * finite-state machine triggering state transitions.
+   */
+  nsEventStatus HandleMouseMoveEvent(WidgetMouseEvent* aEvent);
+  nsEventStatus HandleMouseUpEvent(WidgetMouseEvent* aEvent);
+  nsEventStatus HandleMouseDownEvent(WidgetMouseEvent* aEvent);
+  nsEventStatus HandleTouchMoveEvent(WidgetTouchEvent* aEvent);
+  nsEventStatus HandleTouchUpEvent(WidgetTouchEvent* aEvent);
+  nsEventStatus HandleTouchDownEvent(WidgetTouchEvent* aEvent);
+
+  /**
+   * Get the coordinates of a given touch event, relative to canvas frame.
+   * @param aEvent the event
+   * @param aIdentifier the mIdentifier of the touch which is to be converted.
+   * @return the point, or (NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE) if
+   * for some reason the coordinates for the touch are not known (e.g.,
+   * the mIdentifier touch is not found).
+   */
+  nsPoint GetEventPosition(WidgetTouchEvent* aEvent, int32_t aIdentifier);
+
+  /**
+   * Get the coordinates of a given mouse event, relative to canvas frame.
+   * @param aEvent the event
+   * @return the point, or (NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE) if
+   * for some reason the coordinates for the mouse are not known.
+   */
+  nsPoint GetEventPosition(WidgetMouseEvent* aEvent);
+
+  /**
+   * States of TouchCaret finite-state machine.
+   */
+  enum TouchCaretState {
+    // In this state, either there is no touch/mouse event going on, or the
+    // first stroke does not hit the touch caret.
+    // Will enter TOUCHCARET_TOUCHDRAG_ACTIVE state if the first touch stroke
+    // hits the touch caret. Will enter TOUCHCARET_MOUSEDRAG_ACTIVE state if
+    // mouse (left button) down hits the touch caret.
+    // Allowed next state: TOUCHCARET_MOUSEDRAG_ACTIVE,
+    //                     TOUCHCARET_TOUCHDRAG_ACTIVE.
+    TOUCHCARET_NONE,
+    // The first (left button) mouse down hits on the touch caret and is
+    // alive. Will enter TOUCHCARET_NONE state if the left button is release.
+    // Allowed next states: TOUCHCARET_NONE.
+    TOUCHCARET_MOUSEDRAG_ACTIVE,
+    // The first touch start event hits on touch caret and is alive.
+    // Will enter TOUCHCARET_NONE state if the finger on touch caret is
+    // removed and there are no more fingers on the screen; will enter
+    // TOUCHCARET_TOUCHDRAG_INACTIVE state if the finger on touch caret is
+    // removed but still has fingers touching on the screen.
+    // Allowed next states: TOUCHCARET_NONE, TOUCHCARET_TOUCHDRAG_INACTIVE.
+    TOUCHCARET_TOUCHDRAG_ACTIVE,
+    // The first touch stroke, which hit on touch caret, is dead, but still has
+    // fingers touching on the screen.
+    // Will enter TOUCHCARET_NONE state if all the fingers are removed from the
+    // screen.
+    // Allowed next state: TOUCHCARET_NONE.
+    TOUCHCARET_TOUCHDRAG_INACTIVE,
+  };
+
+  /**
+   * Do actual state transition and reset substates.
+   */
+  void SetState(TouchCaretState aState);
+
+  /**
+   * Current state we're dealing with.
+   */
+  TouchCaretState mState;
+
+  /**
+   * Array containing all active touch IDs. When a touch happens, it gets added
+   * to this array, even if we choose not to handle it. When it ends, we remove
+   * it. We need to maintain this array in order to detect the end of the
+   * "multitouch" states because touch start events contain all current touches,
+   * but touch end events contain only those touches that have gone.
+   */
+  nsTArray<int32_t> mTouchesId;
+
+  /**
+   * The mIdentifier of the touch which is on the touch caret.
+   */
+  int32_t mActiveTouchId;
+
+  /**
+   * The offset between the tap location and the center of caret along y axis.
+   */
+  nscoord mCaretCenterToDownPointOffsetY;
+
+  static int32_t TouchCaretMaxDistance()
+  {
+    return sTouchCaretMaxDistance;
+  }
+
+  static int32_t TouchCaretExpirationTime()
+  {
+    return sTouchCaretExpirationTime;
+  }
+
+protected:
+  nsWeakPtr mPresShell;
+
+  // Touch caret visibility
+  bool mVisible;
+  // Touch caret timer
+  nsCOMPtr<nsITimer> mTouchCaretExpirationTimer;
+
+  // Preference
+  static int32_t sTouchCaretMaxDistance;
+  static int32_t sTouchCaretExpirationTime;
+};
+} //namespace mozilla
+#endif //mozilla_TouchCaret_h__
--- a/layout/base/moz.build
+++ b/layout/base/moz.build
@@ -86,16 +86,17 @@ UNIFIED_SOURCES += [
     'nsQuoteList.cpp',
     'nsStyleChangeList.cpp',
     'nsStyleSheetService.cpp',
     'PaintTracker.cpp',
     'PositionedEventTargeting.cpp',
     'RestyleManager.cpp',
     'RestyleTracker.cpp',
     'StackArena.cpp',
+    'TouchCaret.cpp',
 ]
 
 # nsDocumentViewer.cpp and nsPresShell.cpp need to be built separately
 # because they force NSPR logging.
 # nsPresArena.cpp needs to be built separately because it uses plarena.h.
 # nsRefreshDriver.cpp needs to be built separately because of name clashes in the OS X headers
 SOURCES += [
     'nsDocumentViewer.cpp',
--- a/modules/libpref/src/init/all.js
+++ b/modules/libpref/src/init/all.js
@@ -4146,16 +4146,26 @@ pref("urlclassifier.downloadAllowTable",
 pref("urlclassifier.disallow_completions", "test-malware-simple,test-phish-simple,goog-downloadwhite-digest256");
 
 // Turn off Spatial navigation by default.
 pref("snav.enabled", false);
 
 // Turn off touch caret by default.
 pref("touchcaret.enabled", false);
 
+// Maximum distance to the center of touch caret (in app unit square) which
+// will be accepted to drag touch caret (0 means only in the bounding box of touch
+// caret is accepted)
+pref("touchcaret.distance.threshold", 1500);
+
+// We'll start to increment time when user release the control of touch caret.
+// When time exceed this expiration time, we'll hide touch caret.
+// In milliseconds. (0 means disable this feature)
+pref("touchcaret.expiration.time", 3000);
+
 // Wakelock is disabled by default.
 pref("dom.wakelock.enabled", false);
 
 // The URL of the Firefox Accounts auth server backend
 pref("identity.fxaccounts.auth.uri", "https://api.accounts.firefox.com/v1");
 
 // disable mozsample size for now
 pref("image.mozsamplesize.enabled", false);