Bug 987718 - Part 5: Add SelectionCarets; r=roc
authorMorris Tseng <mtseng@mozilla.com>
Wed, 04 Jun 2014 22:57:00 +0200
changeset 194186 533bcb64047560dd81e77932429d0154c1a45880
parent 194185 4883dbd3a0d3c5ed21070b99aa28d3bcc49ed7a5
child 194187 4d8bd0baf6332ccc5428e657d834a9970e1e127a
push id3741
push userasasaki@mozilla.com
push dateMon, 21 Jul 2014 20:25:18 +0000
treeherdermozilla-esr52@4d6f46f5af68 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersroc
bugs987718
milestone32.0a1
Bug 987718 - Part 5: Add SelectionCarets; r=roc
layout/base/SelectionCarets.cpp
layout/base/SelectionCarets.h
layout/base/TouchCaret.cpp
layout/base/TouchCaret.h
layout/base/moz.build
new file mode 100644
--- /dev/null
+++ b/layout/base/SelectionCarets.cpp
@@ -0,0 +1,883 @@
+/* -*- 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 "SelectionCarets.h"
+
+#include "gfxPrefs.h"
+#include "nsBidiPresUtils.h"
+#include "nsCanvasFrame.h"
+#include "nsCaret.h"
+#include "nsContentUtils.h"
+#include "nsDebug.h"
+#include "nsDOMTokenList.h"
+#include "nsFrame.h"
+#include "nsIDocument.h"
+#include "nsIDocShell.h"
+#include "nsIDOMDocument.h"
+#include "nsIDOMNodeFilter.h"
+#include "nsIPresShell.h"
+#include "nsPresContext.h"
+#include "nsRect.h"
+#include "nsView.h"
+#include "mozilla/dom/DOMRect.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Selection.h"
+#include "mozilla/dom/TreeWalker.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/TouchEvents.h"
+#include "TouchCaret.h"
+
+using namespace mozilla;
+
+// We treat mouse/touch move as "REAL" move event once its move distance
+// exceed this value, in CSS pixel.
+static const int32_t kMoveStartTolerancePx = 5;
+// Time for trigger scroll end event, in miliseconds.
+static const int32_t kScrollEndTimerDelay = 300;
+
+NS_IMPL_ISUPPORTS(SelectionCarets,
+                  nsISelectionListener,
+                  nsIScrollObserver,
+                  nsISupportsWeakReference)
+
+/*static*/ int32_t SelectionCarets::sSelectionCaretsInflateSize = 0;
+
+SelectionCarets::SelectionCarets(nsIPresShell *aPresShell)
+  : mActiveTouchId(-1)
+  , mCaretCenterToDownPointOffsetY(0)
+  , mDragMode(NONE)
+  , mVisible(false)
+  , mStartCaretVisible(false)
+  , mEndCaretVisible(false)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  static bool addedPref = false;
+  if (!addedPref) {
+    Preferences::AddIntVarCache(&sSelectionCaretsInflateSize,
+                                "selectioncaret.inflatesize.threshold");
+    addedPref = true;
+  }
+
+  mPresShell = aPresShell;
+}
+
+SelectionCarets::~SelectionCarets()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  if (mLongTapDetectorTimer) {
+    mLongTapDetectorTimer->Cancel();
+    mLongTapDetectorTimer = nullptr;
+  }
+
+  if (mScrollEndDetectorTimer) {
+    mScrollEndDetectorTimer->Cancel();
+    mScrollEndDetectorTimer = nullptr;
+  }
+
+  mPresShell = nullptr;
+}
+
+static bool
+IsOnRect(const nsRect& aRect,
+         const nsPoint& aPoint,
+         int32_t aInflateSize)
+{
+  // Check if the click was in the bounding box of the selection caret
+  nsRect rect = aRect;
+  rect.Inflate(aInflateSize);
+  return rect.Contains(aPoint);
+}
+
+nsEventStatus
+SelectionCarets::HandleEvent(WidgetEvent* aEvent)
+{
+  WidgetMouseEvent *mouseEvent = aEvent->AsMouseEvent();
+  if (mouseEvent && mouseEvent->reason == WidgetMouseEvent::eSynthesized) {
+    return nsEventStatus_eIgnore;
+  }
+
+  WidgetTouchEvent *touchEvent = aEvent->AsTouchEvent();
+  nsIntPoint movePoint;
+  int32_t nowTouchId = -1;
+  if (touchEvent && !touchEvent->touches.IsEmpty()) {
+    // If touch happened, just grab event with same identifier
+    if (mActiveTouchId >= 0) {
+      for (uint32_t i = 0; i < touchEvent->touches.Length(); ++i) {
+        if (touchEvent->touches[i]->Identifier() == mActiveTouchId) {
+          movePoint = touchEvent->touches[i]->mRefPoint;
+          nowTouchId = touchEvent->touches[i]->Identifier();
+          break;
+        }
+      }
+
+      // not found, consume it
+      if (nowTouchId == -1) {
+        return nsEventStatus_eConsumeNoDefault;
+      }
+    } else {
+      movePoint = touchEvent->touches[0]->mRefPoint;
+      nowTouchId = touchEvent->touches[0]->Identifier();
+    }
+  } else if (mouseEvent) {
+    movePoint = LayoutDeviceIntPoint::ToUntyped(mouseEvent->AsGUIEvent()->refPoint);
+  }
+
+  // Get event coordinate relative to canvas frame
+  nsIFrame* canvasFrame = mPresShell->GetCanvasFrame();
+  if (!canvasFrame) {
+    return nsEventStatus_eIgnore;
+  }
+  nsPoint ptInCanvas =
+    nsLayoutUtils::GetEventCoordinatesRelativeTo(aEvent, movePoint, canvasFrame);
+
+  if (aEvent->message == NS_TOUCH_START ||
+      (aEvent->message == NS_MOUSE_BUTTON_DOWN &&
+       mouseEvent->button == WidgetMouseEvent::eLeftButton)) {
+    // If having a active touch, ignore other touch down event
+    if (aEvent->message == NS_TOUCH_START && mActiveTouchId >= 0) {
+      return nsEventStatus_eConsumeNoDefault;
+    }
+
+    mActiveTouchId = nowTouchId;
+    mDownPoint = ptInCanvas;
+    int32_t inflateSize = SelectionCaretsInflateSize();
+    if (mVisible && IsOnRect(GetStartFrameRect(), ptInCanvas, inflateSize)) {
+      mDragMode = START_FRAME;
+      mCaretCenterToDownPointOffsetY = GetCaretYCenterPosition() - ptInCanvas.y;
+      SetSelectionDirection(false);
+      SetMouseDownState(true);
+      return nsEventStatus_eConsumeNoDefault;
+    } else if (mVisible && IsOnRect(GetEndFrameRect(), ptInCanvas, inflateSize)) {
+      mDragMode = END_FRAME;
+      mCaretCenterToDownPointOffsetY = GetCaretYCenterPosition() - ptInCanvas.y;
+      SetSelectionDirection(true);
+      SetMouseDownState(true);
+      return nsEventStatus_eConsumeNoDefault;
+    } else {
+      mDragMode = NONE;
+      mActiveTouchId = -1;
+      SetVisibility(false);
+      LaunchLongTapDetector();
+    }
+  } else if (aEvent->message == NS_TOUCH_END ||
+             aEvent->message == NS_TOUCH_CANCEL ||
+             aEvent->message == NS_MOUSE_BUTTON_UP) {
+    CancelLongTapDetector();
+    if (mDragMode != NONE) {
+      // Only care about same id
+      if (mActiveTouchId == nowTouchId) {
+        SetMouseDownState(false);
+        mDragMode = NONE;
+        mActiveTouchId = -1;
+      }
+      return nsEventStatus_eConsumeNoDefault;
+    }
+  } else if (aEvent->message == NS_TOUCH_MOVE ||
+             aEvent->message == NS_MOUSE_MOVE) {
+    if (mDragMode == START_FRAME || mDragMode == END_FRAME) {
+      if (mActiveTouchId == nowTouchId) {
+        ptInCanvas.y += mCaretCenterToDownPointOffsetY;
+        return DragSelection(ptInCanvas);
+      }
+
+      return nsEventStatus_eConsumeNoDefault;
+    }
+
+    nsPoint delta = mDownPoint - ptInCanvas;
+    if (NS_hypot(delta.x, delta.y) >
+          nsPresContext::AppUnitsPerCSSPixel() * kMoveStartTolerancePx) {
+      CancelLongTapDetector();
+    }
+  } else if (aEvent->message == NS_MOUSE_MOZLONGTAP) {
+    if (!mVisible) {
+      SelectWord();
+      return nsEventStatus_eConsumeNoDefault;
+    }
+  }
+  return nsEventStatus_eIgnore;
+}
+
+static void
+SetElementVisibility(dom::Element* aElement, bool aVisible)
+{
+  NS_ENSURE_TRUE_VOID(aElement);
+  ErrorResult err;
+  aElement->ClassList()->Toggle(NS_LITERAL_STRING("hidden"),
+                                   dom::Optional<bool>(!aVisible), err);
+}
+
+void
+SelectionCarets::SetVisibility(bool aVisible)
+{
+  if (!mPresShell) {
+    return;
+  }
+
+  if (mVisible == aVisible) {
+    return;
+  }
+  mVisible = aVisible;
+
+  dom::Element* startElement = mPresShell->GetSelectionCaretsStartElement();
+  SetElementVisibility(startElement, mVisible && mStartCaretVisible);
+
+  dom::Element* endElement = mPresShell->GetSelectionCaretsEndElement();
+  SetElementVisibility(endElement, mVisible && mEndCaretVisible);
+
+  // We must call SetHasTouchCaret() in order to get APZC to wait until the
+  // event has been round-tripped and check whether it has been handled,
+  // otherwise B2G will end up panning the document when the user tries to drag
+  // selection caret.
+  mPresShell->SetMayHaveTouchCaret(mVisible);
+}
+
+void
+SelectionCarets::SetStartFrameVisibility(bool aVisible)
+{
+  mStartCaretVisible = aVisible;
+  dom::Element* element = mPresShell->GetSelectionCaretsStartElement();
+  SetElementVisibility(element, mVisible && mStartCaretVisible);
+}
+
+void
+SelectionCarets::SetEndFrameVisibility(bool aVisible)
+{
+  mEndCaretVisible = aVisible;
+  dom::Element* element = mPresShell->GetSelectionCaretsEndElement();
+  SetElementVisibility(element, mVisible && mEndCaretVisible);
+}
+
+void
+SelectionCarets::SetTilted(bool aIsTilt)
+{
+  dom::Element* startElement = mPresShell->GetSelectionCaretsStartElement();
+  dom::Element* endElement = mPresShell->GetSelectionCaretsEndElement();
+  NS_ENSURE_TRUE_VOID(startElement && endElement);
+
+  ErrorResult err;
+  startElement->ClassList()->Toggle(NS_LITERAL_STRING("tilt"),
+                                       dom::Optional<bool>(aIsTilt), err);
+
+  endElement->ClassList()->Toggle(NS_LITERAL_STRING("tilt"),
+                                     dom::Optional<bool>(aIsTilt), err);
+}
+
+static void
+SetCaretDirection(dom::Element* aElement, bool aIsRight)
+{
+  MOZ_ASSERT(aElement);
+
+  ErrorResult err;
+  if (aIsRight) {
+    aElement->ClassList()->Add(NS_LITERAL_STRING("moz-selectioncaret-right"), err);
+    aElement->ClassList()->Remove(NS_LITERAL_STRING("moz-selectioncaret-left"), err);
+  } else {
+    aElement->ClassList()->Add(NS_LITERAL_STRING("moz-selectioncaret-left"), err);
+    aElement->ClassList()->Remove(NS_LITERAL_STRING("moz-selectioncaret-right"), err);
+  }
+}
+
+static bool
+IsRightToLeft(nsIFrame* aFrame)
+{
+  MOZ_ASSERT(aFrame);
+
+  return aFrame->IsFrameOfType(nsIFrame::eLineParticipant) ?
+    (nsBidiPresUtils::GetFrameEmbeddingLevel(aFrame) & 1) :
+    aFrame->StyleVisibility()->mDirection == NS_STYLE_DIRECTION_RTL;
+}
+
+/*
+ * Reduce rect to 1 app unit width along either left or right edge base on
+ * aToRightEdge parameter.
+ */
+static void
+ReduceRectToVerticalEdge(nsRect& aRect, bool aToRightEdge)
+{
+  if (aToRightEdge) {
+    aRect.x = aRect.XMost() - 1;
+  }
+  aRect.width = 1;
+}
+
+static nsIFrame*
+FindFirstNodeWithFrame(nsIDocument* aDocument,
+                       nsRange* aRange,
+                       nsFrameSelection* aFrameSelection,
+                       bool aBackward,
+                       int& aOutOffset)
+{
+  NS_ENSURE_TRUE(aDocument && aRange && aFrameSelection, nullptr);
+
+  nsCOMPtr<nsINode> startNode =
+    do_QueryInterface(aBackward ? aRange->GetEndParent() : aRange->GetStartParent());
+  nsCOMPtr<nsINode> endNode =
+    do_QueryInterface(aBackward ? aRange->GetStartParent() : aRange->GetEndParent());
+  int32_t offset = aBackward ? aRange->EndOffset() : aRange->StartOffset();
+
+  nsCOMPtr<nsIContent> startContent = do_QueryInterface(startNode);
+  nsCOMPtr<nsIContent> endContent = do_QueryInterface(endNode);
+  nsFrameSelection::HINT hintStart =
+    nsFrameSelection::GetHintForPosition(startContent, offset);
+  nsIFrame* startFrame = aFrameSelection->GetFrameForNodeOffset(startContent,
+                                                                offset,
+                                                                hintStart,
+                                                                &aOutOffset);
+
+  if (startFrame) {
+    return startFrame;
+  }
+
+  ErrorResult err;
+  nsRefPtr<dom::TreeWalker> walker =
+    aDocument->CreateTreeWalker(*startNode,
+                                nsIDOMNodeFilter::SHOW_ALL,
+                                nullptr,
+                                err);
+
+  NS_ENSURE_TRUE(walker, nullptr);
+  startFrame = startContent ? startContent->GetPrimaryFrame() : nullptr;
+  while (!startFrame && startNode != endNode) {
+    if (aBackward) {
+      startNode = walker->PreviousNode(err);
+    } else {
+      startNode = walker->NextNode(err);
+    }
+    startContent = do_QueryInterface(startNode);
+    startFrame = startContent ? startContent->GetPrimaryFrame() : nullptr;
+  }
+  return startFrame;
+}
+
+void
+SelectionCarets::UpdateSelectionCarets()
+{
+  if (!mPresShell) {
+    return;
+  }
+
+  nsISelection* caretSelection = GetSelection();
+  if (!caretSelection) {
+    SetVisibility(false);
+    return;
+  }
+
+  nsRefPtr<dom::Selection> selection = static_cast<dom::Selection*>(caretSelection);
+  if (selection->GetRangeCount() <= 0) {
+    SetVisibility(false);
+    return;
+  }
+
+  nsRefPtr<nsRange> range = selection->GetRangeAt(0);
+  if (range->Collapsed()) {
+    SetVisibility(false);
+    return;
+  }
+
+  nsLayoutUtils::FirstAndLastRectCollector collector;
+  nsRange::CollectClientRects(&collector, range,
+                              range->GetStartParent(), range->StartOffset(),
+                              range->GetEndParent(), range->EndOffset());
+
+  nsIFrame* canvasFrame = mPresShell->GetCanvasFrame();
+  nsIFrame* rootFrame = mPresShell->GetRootFrame();
+
+  if (!canvasFrame || !rootFrame) {
+    SetVisibility(false);
+    return;
+  }
+
+  // Check if caret inside the scroll frame's boundary
+  nsIFrame* caretFocusFrame = GetCaretFocusFrame();
+  if (!caretFocusFrame) {
+    SetVisibility(false);
+    return;
+  }
+  nsIContent *editableAncestor = caretFocusFrame->GetContent()->GetEditingHost();
+
+  if (!editableAncestor) {
+    SetVisibility(false);
+    return;
+  }
+
+  nsRect resultRect;
+  for (nsIFrame* frame = editableAncestor->GetPrimaryFrame();
+      frame != nullptr;
+      frame = frame->GetNextContinuation()) {
+    nsRect rect = frame->GetRectRelativeToSelf();
+    nsLayoutUtils::TransformRect(frame, rootFrame, rect);
+    resultRect = resultRect.Union(rect);
+  }
+
+  // Check start and end frame is rtl or ltr text
+  nsRefPtr<nsFrameSelection> fs = caretFocusFrame->GetFrameSelection();
+  int32_t startOffset;
+  nsIFrame* startFrame = FindFirstNodeWithFrame(mPresShell->GetDocument(),
+                                                range, fs, false, startOffset);
+
+  int32_t endOffset;
+  nsIFrame* endFrame = FindFirstNodeWithFrame(mPresShell->GetDocument(),
+                                              range, fs, true, endOffset);
+
+  if (!startFrame || !endFrame) {
+    SetVisibility(false);
+    return;
+  }
+
+  // Check if startFrame is after endFrame.
+  if (nsLayoutUtils::CompareTreePosition(startFrame, endFrame) > 0) {
+    SetVisibility(false);
+    return;
+  }
+
+  bool startFrameIsRTL = IsRightToLeft(startFrame);
+  bool endFrameIsRTL = IsRightToLeft(endFrame);
+
+  // If start frame is LTR, then place start caret in first rect's leftmost
+  // otherwise put it to first rect's rightmost.
+  ReduceRectToVerticalEdge(collector.mFirstRect, startFrameIsRTL);
+
+  // Contrary to start frame, if end frame is LTR, put end caret to last
+  // rect's rightmost position, otherwise, put it to last rect's leftmost.
+  ReduceRectToVerticalEdge(collector.mLastRect, !endFrameIsRTL);
+
+  SetStartFrameVisibility(resultRect.Intersects(collector.mFirstRect));
+  SetEndFrameVisibility(resultRect.Intersects(collector.mLastRect));
+
+  nsLayoutUtils::TransformRect(rootFrame, canvasFrame, collector.mFirstRect);
+  nsLayoutUtils::TransformRect(rootFrame, canvasFrame, collector.mLastRect);
+
+  SetStartFramePos(collector.mFirstRect.BottomLeft());
+  SetEndFramePos(collector.mLastRect.BottomRight());
+  SetVisibility(true);
+
+  // If range select only one character, append tilt class name to it.
+  bool isTilt = false;
+  if (startFrame) {
+    nsPeekOffsetStruct pos(eSelectCluster,
+                           eDirNext,
+                           startOffset,
+                           0,
+                           false,
+                           true,  //limit on scrolled views
+                           false,
+                           false);
+    startFrame->PeekOffset(&pos);
+    nsCOMPtr<nsIContent> endContent = do_QueryInterface(range->GetEndParent());
+    if (nsLayoutUtils::CompareTreePosition(pos.mResultContent, endContent) > 0 ||
+        (pos.mResultContent == endContent &&
+         pos.mContentOffset >= range->EndOffset())) {
+      isTilt = true;
+    }
+  }
+
+  SetCaretDirection(mPresShell->GetSelectionCaretsStartElement(), startFrameIsRTL);
+  SetCaretDirection(mPresShell->GetSelectionCaretsEndElement(), !endFrameIsRTL);
+  SetTilted(isTilt);
+}
+
+nsresult
+SelectionCarets::SelectWord()
+{
+  // If caret isn't visible, the word is not selectable
+  if (!GetCaretVisible()) {
+    return NS_OK;
+  }
+
+  NS_ENSURE_TRUE(mPresShell, NS_OK);
+
+  nsIFrame* canvasFrame = mPresShell->GetCanvasFrame();
+  NS_ENSURE_TRUE(canvasFrame, NS_OK);
+
+  // Find content offsets for mouse down point
+  nsIFrame *ptFrame = nsLayoutUtils::GetFrameForPoint(canvasFrame, mDownPoint,
+    nsLayoutUtils::IGNORE_PAINT_SUPPRESSION | nsLayoutUtils::IGNORE_CROSS_DOC);
+  NS_ENSURE_TRUE(ptFrame, NS_OK);
+  nsPoint ptInFrame = mDownPoint;
+  nsLayoutUtils::TransformPoint(canvasFrame, ptFrame, ptInFrame);
+
+  nsIFrame* caretFocusFrame = GetCaretFocusFrame();
+  nsRefPtr<nsFrameSelection> fs = caretFocusFrame->GetFrameSelection();
+  fs->SetMouseDownState(true);
+  nsFrame* frame = static_cast<nsFrame*>(ptFrame);
+  nsresult rs = frame->SelectByTypeAtPoint(mPresShell->GetPresContext(), ptInFrame,
+                                           eSelectWord, eSelectWord, 0);
+  fs->SetMouseDownState(false);
+
+  // Clear maintain selection otherwise we cannot select less than a word
+  fs->MaintainSelection();
+  return rs;
+}
+
+/*
+ * If we're dragging start caret, we do not want to drag over previous
+ * character of end caret. Same as end caret. So we check if content offset
+ * exceed previous/next character of end/start caret base on aDragMode.
+ */
+static bool
+CompareRangeWithContentOffset(nsRange* aRange,
+                              nsFrameSelection* aSelection,
+                              nsIFrame::ContentOffsets& aOffsets,
+                              SelectionCarets::DragMode aDragMode)
+{
+  MOZ_ASSERT(aDragMode != SelectionCarets::NONE);
+  nsINode* node = nullptr;
+  int32_t nodeOffset = 0;
+  nsFrameSelection::HINT hint = nsFrameSelection::HINTLEFT;
+  nsDirection dir;
+
+  if (aDragMode == SelectionCarets::START_FRAME) {
+    // Check previous character of end node offset
+    node = aRange->GetEndParent();
+    nodeOffset = aRange->EndOffset();
+    hint = nsFrameSelection::HINTLEFT;
+    dir = eDirPrevious;
+  } else {
+    // Check next character of start node offset
+    node = aRange->GetStartParent();
+    nodeOffset = aRange->StartOffset();
+    hint = nsFrameSelection::HINTRIGHT;
+    dir = eDirNext;
+  }
+  nsCOMPtr<nsIContent> content = do_QueryInterface(node);
+
+  int32_t offset = 0;
+  nsIFrame* theFrame =
+    aSelection->GetFrameForNodeOffset(content, nodeOffset, hint, &offset);
+
+  NS_ENSURE_TRUE(theFrame, false);
+
+  // Move one character forward/backward from point and get offset
+  nsPeekOffsetStruct pos(eSelectCluster,
+                         dir,
+                         offset,
+                         0,
+                         true,
+                         true,  //limit on scrolled views
+                         false,
+                         false);
+  nsresult rv = theFrame->PeekOffset(&pos);
+  if (NS_FAILED(rv)) {
+    pos.mResultContent = content;
+    pos.mContentOffset = nodeOffset;
+  }
+
+  // Compare with current point
+  int32_t result = nsContentUtils::ComparePoints(aOffsets.content,
+                                                 aOffsets.StartOffset(),
+                                                 pos.mResultContent,
+                                                 pos.mContentOffset);
+  if ((aDragMode == SelectionCarets::START_FRAME && result == 1) ||
+      (aDragMode == SelectionCarets::END_FRAME && result == -1)) {
+    aOffsets.content = pos.mResultContent;
+    aOffsets.offset = pos.mContentOffset;
+    aOffsets.secondaryOffset = pos.mContentOffset;
+  }
+
+  return true;
+}
+
+nsEventStatus
+SelectionCarets::DragSelection(const nsPoint &movePoint)
+{
+  nsIFrame* canvasFrame = mPresShell->GetCanvasFrame();
+  NS_ENSURE_TRUE(canvasFrame, nsEventStatus_eConsumeNoDefault);
+
+  // Find out which content we point to
+  nsIFrame *ptFrame = nsLayoutUtils::GetFrameForPoint(canvasFrame, movePoint,
+    nsLayoutUtils::IGNORE_PAINT_SUPPRESSION | nsLayoutUtils::IGNORE_CROSS_DOC);
+  NS_ENSURE_TRUE(ptFrame, nsEventStatus_eConsumeNoDefault);
+  nsPoint ptInFrame = movePoint;
+  nsLayoutUtils::TransformPoint(canvasFrame, ptFrame, ptInFrame);
+  nsFrame::ContentOffsets offsets =
+    ptFrame->GetContentOffsetsFromPoint(ptInFrame);
+  NS_ENSURE_TRUE(offsets.content, nsEventStatus_eConsumeNoDefault);
+
+  nsISelection* caretSelection = GetSelection();
+  nsRefPtr<dom::Selection> selection = static_cast<dom::Selection*>(caretSelection);
+  if (selection->GetRangeCount() <= 0) {
+    return nsEventStatus_eConsumeNoDefault;
+  }
+
+  nsRefPtr<nsRange> range = selection->GetRangeAt(0);
+  nsIFrame* caretFocusFrame = GetCaretFocusFrame();
+  nsRefPtr<nsFrameSelection> fs = caretFocusFrame->GetFrameSelection();
+  if (!CompareRangeWithContentOffset(range, fs, offsets, mDragMode)) {
+    return nsEventStatus_eConsumeNoDefault;
+  }
+
+  // Move caret postion.
+  nsIFrame *scrollable =
+    nsLayoutUtils::GetClosestFrameOfType(caretFocusFrame, nsGkAtoms::scrollFrame);
+  nsWeakFrame weakScrollable = scrollable;
+  fs->HandleClick(offsets.content, offsets.StartOffset(),
+                  offsets.EndOffset(),
+                  true,
+                  false,
+                  offsets.associateWithNext);
+  if (!weakScrollable.IsAlive()) {
+    return nsEventStatus_eConsumeNoDefault;
+  }
+
+  // Scroll scrolled frame.
+  nsIScrollableFrame *saf = do_QueryFrame(scrollable);
+  nsIFrame *capturingFrame = saf->GetScrolledFrame();
+  nsPoint ptInScrolled = movePoint;
+  nsLayoutUtils::TransformPoint(canvasFrame, capturingFrame, ptInScrolled);
+  fs->StartAutoScrollTimer(capturingFrame, ptInScrolled, TouchCaret::sAutoScrollTimerDelay);
+  UpdateSelectionCarets();
+  return nsEventStatus_eConsumeNoDefault;
+}
+
+nscoord
+SelectionCarets::GetCaretYCenterPosition()
+{
+  nsIFrame* canvasFrame = mPresShell->GetCanvasFrame();
+  nsIFrame* caretFocusFrame = GetCaretFocusFrame();
+
+  if (!canvasFrame || !caretFocusFrame) {
+    return 0;
+  }
+  nsISelection* caretSelection = GetSelection();
+  nsRefPtr<dom::Selection> selection = static_cast<dom::Selection*>(caretSelection);
+  if (selection->GetRangeCount() <= 0) {
+    return 0;
+  }
+  nsRefPtr<nsRange> range = selection->GetRangeAt(0);
+  nsRefPtr<nsFrameSelection> fs = caretFocusFrame->GetFrameSelection();
+
+  MOZ_ASSERT(mDragMode != NONE);
+  nsCOMPtr<nsIContent> node;
+  uint32_t nodeOffset;
+  if (mDragMode == START_FRAME) {
+    node = do_QueryInterface(range->GetStartParent());
+    nodeOffset = range->StartOffset();
+  } else {
+    node = do_QueryInterface(range->GetEndParent());
+    nodeOffset = range->EndOffset();
+  }
+
+  int32_t offset;
+  nsFrameSelection::HINT hint =
+    nsFrameSelection::GetHintForPosition(node, nodeOffset);
+  nsIFrame* theFrame =
+    fs->GetFrameForNodeOffset(node, nodeOffset, hint, &offset);
+
+  if (!theFrame) {
+    return 0;
+  }
+  nsRect frameRect = theFrame->GetRectRelativeToSelf();
+  nsLayoutUtils::TransformRect(theFrame, canvasFrame, frameRect);
+  return frameRect.Center().y;
+}
+
+void
+SelectionCarets::SetMouseDownState(bool aState)
+{
+  nsIFrame* caretFocusFrame = GetCaretFocusFrame();
+  nsRefPtr<nsFrameSelection> fs = caretFocusFrame->GetFrameSelection();
+  if (fs->GetMouseDownState() == aState) {
+    return;
+  }
+  fs->SetMouseDownState(aState);
+
+  if (aState) {
+    fs->StartBatchChanges();
+  } else {
+    fs->EndBatchChanges();
+  }
+}
+
+void
+SelectionCarets::SetSelectionDirection(bool aForward)
+{
+  nsISelection* caretSelection = GetSelection();
+  nsRefPtr<dom::Selection> selection = static_cast<dom::Selection*>(caretSelection);
+  selection->SetDirection(aForward ? eDirNext : eDirPrevious);
+}
+
+static void
+SetFramePos(dom::Element* aElement, const nsPoint& aPosition)
+{
+  NS_ENSURE_TRUE_VOID(aElement);
+
+  nsAutoString styleStr;
+  styleStr.AppendLiteral("left:");
+  styleStr.AppendFloat(nsPresContext::AppUnitsToFloatCSSPixels(aPosition.x));
+  styleStr.AppendLiteral("px;top:");
+  styleStr.AppendFloat(nsPresContext::AppUnitsToFloatCSSPixels(aPosition.y));
+  styleStr.AppendLiteral("px;");
+
+  aElement->SetAttr(kNameSpaceID_None, nsGkAtoms::style, styleStr, true);
+}
+
+void
+SelectionCarets::SetStartFramePos(const nsPoint& aPosition)
+{
+  SetFramePos(mPresShell->GetSelectionCaretsStartElement(), aPosition);
+}
+
+void
+SelectionCarets::SetEndFramePos(const nsPoint& aPosition)
+{
+  SetFramePos(mPresShell->GetSelectionCaretsEndElement(), aPosition);
+}
+
+nsRect
+SelectionCarets::GetStartFrameRect()
+{
+  nsIFrame* canvasFrame = mPresShell->GetCanvasFrame();
+  dom::Element* element = mPresShell->GetSelectionCaretsStartElement();
+  NS_ENSURE_TRUE(element, nsRect());
+
+  nsIFrame* frame = element->GetPrimaryFrame();
+  NS_ENSURE_TRUE(frame, nsRect());
+
+  nsRect frameRect = frame->GetRectRelativeToSelf();
+  nsLayoutUtils::TransformRect(frame, canvasFrame, frameRect);
+  return frameRect;
+}
+
+nsRect
+SelectionCarets::GetEndFrameRect()
+{
+  nsIFrame* canvasFrame = mPresShell->GetCanvasFrame();
+  dom::Element* element = mPresShell->GetSelectionCaretsEndElement();
+  NS_ENSURE_TRUE(element, nsRect());
+
+  nsIFrame* frame = element->GetPrimaryFrame();
+  NS_ENSURE_TRUE(frame, nsRect());
+
+  nsRect frameRect = frame->GetRectRelativeToSelf();
+  nsLayoutUtils::TransformRect(frame, canvasFrame, frameRect);
+  return frameRect;
+}
+
+nsIFrame*
+SelectionCarets::GetCaretFocusFrame()
+{
+  nsRefPtr<nsCaret> caret = mPresShell->GetCaret();
+  NS_ENSURE_TRUE(caret, nullptr);
+
+  nsISelection* caretSelection = caret->GetCaretDOMSelection();
+  nsRect focusRect;
+  return caret->GetGeometry(caretSelection, &focusRect);
+}
+
+bool
+SelectionCarets::GetCaretVisible()
+{
+  NS_ENSURE_TRUE(mPresShell, false);
+  nsRefPtr<nsCaret> caret = mPresShell->GetCaret();
+  NS_ENSURE_TRUE(caret, false);
+
+  bool caretVisible = false;
+  caret->GetCaretVisible(&caretVisible);
+  return caretVisible;
+}
+
+nsISelection*
+SelectionCarets::GetSelection()
+{
+  nsRefPtr<nsCaret> caret = mPresShell->GetCaret();
+  return caret->GetCaretDOMSelection();
+}
+
+nsresult
+SelectionCarets::NotifySelectionChanged(nsIDOMDocument* aDoc,
+                                       nsISelection* aSel,
+                                       int16_t aReason)
+{
+  bool isCollapsed;
+  aSel->GetIsCollapsed(&isCollapsed);
+  if (isCollapsed) {
+    SetVisibility(false);
+    return NS_OK;
+  }
+  if (aReason & nsISelectionListener::KEYPRESS_REASON) {
+    SetVisibility(false);
+  } else {
+    UpdateSelectionCarets();
+  }
+  return NS_OK;
+}
+
+void
+SelectionCarets::ScrollPositionChanged()
+{
+  SetVisibility(false);
+  LaunchScrollEndDetector();
+}
+
+void
+SelectionCarets::LaunchLongTapDetector()
+{
+  if (XRE_GetProcessType() != GeckoProcessType_Default) {
+    return;
+  }
+
+  if (!mLongTapDetectorTimer) {
+    mLongTapDetectorTimer = do_CreateInstance("@mozilla.org/timer;1");
+  }
+
+  MOZ_ASSERT(mLongTapDetectorTimer);
+  CancelLongTapDetector();
+  int32_t longTapDelay = gfxPrefs::UiClickHoldContextMenusDelay();
+  mLongTapDetectorTimer->InitWithFuncCallback(FireLongTap,
+                                              this,
+                                              longTapDelay,
+                                              nsITimer::TYPE_ONE_SHOT);
+}
+
+void
+SelectionCarets::CancelLongTapDetector()
+{
+  if (XRE_GetProcessType() != GeckoProcessType_Default) {
+    return;
+  }
+
+  if (!mLongTapDetectorTimer) {
+    return;
+  }
+
+  mLongTapDetectorTimer->Cancel();
+}
+
+/* static */void
+SelectionCarets::FireLongTap(nsITimer* aTimer, void* aSelectionCarets)
+{
+  nsRefPtr<SelectionCarets> self = static_cast<SelectionCarets*>(aSelectionCarets);
+  NS_PRECONDITION(aTimer == self->mLongTapDetectorTimer,
+                  "Unexpected timer");
+
+  self->SelectWord();
+}
+
+void
+SelectionCarets::LaunchScrollEndDetector()
+{
+  if (!mScrollEndDetectorTimer) {
+    mScrollEndDetectorTimer = do_CreateInstance("@mozilla.org/timer;1");
+  }
+
+  MOZ_ASSERT(mScrollEndDetectorTimer);
+  mScrollEndDetectorTimer->InitWithFuncCallback(FireScrollEnd,
+                                                this,
+                                                kScrollEndTimerDelay,
+                                                nsITimer::TYPE_ONE_SHOT);
+}
+
+/* static */void
+SelectionCarets::FireScrollEnd(nsITimer* aTimer, void* aSelectionCarets)
+{
+  nsRefPtr<SelectionCarets> self = static_cast<SelectionCarets*>(aSelectionCarets);
+  NS_PRECONDITION(aTimer == self->mScrollEndDetectorTimer,
+                  "Unexpected timer");
+  self->SetVisibility(true);
+  self->UpdateSelectionCarets();
+}
new file mode 100644
--- /dev/null
+++ b/layout/base/SelectionCarets.h
@@ -0,0 +1,215 @@
+/* -*- 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 SelectionCarets_h__
+#define SelectionCarets_h__
+
+#include "nsIScrollObserver.h"
+#include "nsISelectionListener.h"
+#include "nsWeakPtr.h"
+#include "nsWeakReference.h"
+#include "Units.h"
+#include "mozilla/EventForwards.h"
+
+class nsCanvasFrame;
+class nsIDocument;
+class nsIFrame;
+class nsIPresShell;
+class nsITimer;
+class nsIWidget;
+class nsPresContext;
+
+namespace mozilla {
+
+/**
+ * The SelectionCarets draw a pair of carets when the selection is not
+ * collapsed, one at each end of the selection.
+ * SelectionCarets also handle visibility, dragging caret and selecting word
+ * when long tap event fired.
+ *
+ * The DOM structure is 2 div elements for showing start and end caret.
+ *
+ * Here is an explanation of the html class names:
+ *   .moz-selectioncaret-left: Indicates start DIV.
+ *   .moz-selectioncaret-right: Indicates end DIV.
+ *   .hidden: This class name is set by SetVisibility,
+ *            SetStartFrameVisibility and SetEndFrameVisibility. Element
+ *            with this class name become hidden.
+ *   .tilt: This class name is set by SetTilted. According to the
+ *          UX spec, when selection contains only one characters, the image of
+ *          caret becomes tilt.
+ */
+class SelectionCarets MOZ_FINAL : public nsISelectionListener,
+                                  public nsIScrollObserver,
+                                  public nsSupportsWeakReference
+{
+public:
+  /**
+   * Indicate which part of caret we are dragging at.
+   */
+  enum DragMode {
+    NONE,
+    START_FRAME,
+    END_FRAME
+  };
+
+  explicit SelectionCarets(nsIPresShell *aPresShell);
+  virtual ~SelectionCarets();
+
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSISELECTIONLISTENER
+
+  // nsIScrollObserver
+  virtual void ScrollPositionChanged() MOZ_OVERRIDE;
+
+  void Terminate()
+  {
+    mPresShell = nullptr;
+  }
+
+  nsEventStatus HandleEvent(WidgetEvent* aEvent);
+
+  /**
+   * Set visibility for selection caret.
+   */
+  void SetVisibility(bool aVisible);
+
+  bool GetVisibility() const
+  {
+    return mVisible;
+  }
+
+  /**
+   * Get from pref "selectioncaret.inflatesize.threshold". This will inflate size of
+   * caret frame when we checking if user click on caret or not. In app units.
+   */
+  static int32_t SelectionCaretsInflateSize()
+  {
+    return sSelectionCaretsInflateSize;
+  }
+
+private:
+  SelectionCarets() MOZ_DELETE;
+
+  /**
+   * Update selection caret position base on current selection range.
+   */
+  void UpdateSelectionCarets();
+
+  /**
+   * Select word base on current position, only active when element
+   * is focused. Triggered by long tap event.
+   */
+  nsresult SelectWord();
+
+  /**
+   * Move selection base on current touch/mouse point
+   */
+  nsEventStatus DragSelection(const nsPoint &movePoint);
+
+  /**
+   * Get the vertical center position of selection caret relative to canvas
+   * frame.
+   */
+  nscoord GetCaretYCenterPosition();
+
+  /**
+   * Simulate mouse down state when we change the selection range.
+   * Hence, the selection change event will fire normally.
+   */
+  void SetMouseDownState(bool aState);
+
+  void SetSelectionDirection(bool aForward);
+
+  /**
+   * Move start frame of selection caret to given position.
+   * In app units.
+   */
+  void SetStartFramePos(const nsPoint& aPosition);
+
+  /**
+   * Move end frame of selection caret to given position.
+   * In app units.
+   */
+  void SetEndFramePos(const nsPoint& aPosition);
+
+  /**
+   * Get rect of selection caret's start frame relative
+   * to document's canvas frame, in app units.
+   */
+  nsRect GetStartFrameRect();
+
+  /**
+   * Get rect of selection caret's end frame relative
+   * to document's canvas frame, in app units.
+   */
+  nsRect GetEndFrameRect();
+
+  /**
+   * Set visibility for start part of selection caret, this function
+   * only affects css property of start frame. So it doesn't change
+   * mVisible member. When caret overflows element's box we'll hide
+   * it by calling this function.
+   */
+  void SetStartFrameVisibility(bool aVisible);
+
+  /**
+   * Same as above function but for end frame of selection caret.
+   */
+  void SetEndFrameVisibility(bool aVisible);
+
+  /**
+   * Set tilt class name to start and end frame of selection caret.
+   */
+  void SetTilted(bool aIsTilt);
+
+  // Utility function
+  nsIFrame* GetCaretFocusFrame();
+  bool GetCaretVisible();
+  nsISelection* GetSelection();
+
+  /**
+   * Detecting long tap using timer
+   */
+  void LaunchLongTapDetector();
+  void CancelLongTapDetector();
+  static void FireLongTap(nsITimer* aTimer, void* aSelectionCarets);
+
+  void LaunchScrollEndDetector();
+  static void FireScrollEnd(nsITimer* aTimer, void* aSelectionCarets);
+
+  nsIPresShell* mPresShell;
+
+  // This timer is used for detecting long tap fire. If content process
+  // has APZC, we'll use APZC for long tap detecting. Otherwise, we use this
+  // timer to detect long tap.
+  nsCOMPtr<nsITimer> mLongTapDetectorTimer;
+
+  // This timer is used for detecting scroll end. We don't have
+  // scroll end event now, so we will fire this event with a
+  // const time when we scroll. So when timer triggers, we treat it
+  // as scroll end event.
+  nsCOMPtr<nsITimer> mScrollEndDetectorTimer;
+
+  // When touch or mouse down, we save the position for detecting
+  // drag distance
+  nsPoint mDownPoint;
+
+  // For filter multitouch event
+  int32_t mActiveTouchId;
+
+  nscoord mCaretCenterToDownPointOffsetY;
+  DragMode mDragMode;
+  bool mVisible;
+  bool mStartCaretVisible;
+  bool mEndCaretVisible;
+
+  // Preference
+  static int32_t sSelectionCaretsInflateSize;
+};
+} // namespace mozilla
+
+#endif //SelectionCarets_h__
--- a/layout/base/TouchCaret.cpp
+++ b/layout/base/TouchCaret.cpp
@@ -33,18 +33,16 @@ 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),
@@ -279,17 +277,17 @@ TouchCaret::MoveCaret(const nsPoint& mov
   }
 
   // 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);
+  fs->StartAutoScrollTimer(capturingFrame, pt, sAutoScrollTimerDelay);
 }
 
 bool
 TouchCaret::IsOnTouchCaret(const nsPoint& aPoint)
 {
   // Return false if touch caret is not visible.
   if (!mVisible) {
     return false;
--- a/layout/base/TouchCaret.h
+++ b/layout/base/TouchCaret.h
@@ -230,11 +230,15 @@ protected:
   // Touch caret visibility
   bool mVisible;
   // Touch caret timer
   nsCOMPtr<nsITimer> mTouchCaretExpirationTimer;
 
   // Preference
   static int32_t sTouchCaretMaxDistance;
   static int32_t sTouchCaretExpirationTime;
+
+  // The auto scroll timer's interval in miliseconds.
+  friend class SelectionCarets;
+  static const int32_t sAutoScrollTimerDelay = 30;
 };
 } //namespace mozilla
 #endif //mozilla_TouchCaret_h__
--- a/layout/base/moz.build
+++ b/layout/base/moz.build
@@ -85,16 +85,17 @@ UNIFIED_SOURCES += [
     'nsPresContext.cpp',
     'nsQuoteList.cpp',
     'nsStyleChangeList.cpp',
     'nsStyleSheetService.cpp',
     'PaintTracker.cpp',
     'PositionedEventTargeting.cpp',
     'RestyleManager.cpp',
     'RestyleTracker.cpp',
+    'SelectionCarets.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