Bug 1110039 - Part 2.2 - Add AccessibleCaret. r=roc
authorTing-Yu Lin <tlin@mozilla.com>
Mon, 04 May 2015 21:25:00 +0200
changeset 243468 2228e70314f8d7311f5ff81f3ee762c62b6b39a3
parent 243467 ae5c8904cae754b22e2b08a9a5a9f3fce2553485
child 243469 e426254bcc6b7f379759cfb09b35abaf0adb2c15
push id28738
push usercbook@mozilla.com
push dateTue, 12 May 2015 14:11:31 +0000
treeherdermozilla-central@bedce1b405a3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersroc
bugs1110039, 1020244
milestone40.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 1110039 - Part 2.2 - Add AccessibleCaret. r=roc See AccessibleCaret.h for the class description. Technical difference between AccessibleCaret and Touch/SelectionCarets: The anonymous dom element containing a caret image will be created by AccessibleCaret by using the API landed in bug 1020244 instead of being created by nsCanvasFrame.
layout/base/AccessibleCaret.cpp
layout/base/AccessibleCaret.h
layout/style/ua.css
new file mode 100644
--- /dev/null
+++ b/layout/base/AccessibleCaret.cpp
@@ -0,0 +1,269 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 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 "AccessibleCaret.h"
+
+#include "AccessibleCaretLogger.h"
+#include "mozilla/Preferences.h"
+#include "nsCanvasFrame.h"
+#include "nsCaret.h"
+#include "nsDOMTokenList.h"
+#include "nsIFrame.h"
+
+namespace mozilla {
+using namespace dom;
+
+#undef AC_LOG
+#define AC_LOG(message, ...)                                                   \
+  AC_LOG_BASE("AccessibleCaret (%p): " message, this, ##__VA_ARGS__);
+
+#undef AC_LOGV
+#define AC_LOGV(message, ...)                                                  \
+  AC_LOGV_BASE("AccessibleCaret (%p): " message, this, ##__VA_ARGS__);
+
+NS_IMPL_ISUPPORTS(AccessibleCaret::DummyTouchListener, nsIDOMEventListener)
+
+// -----------------------------------------------------------------------------
+// Implementation of AccessibleCaret methods
+
+AccessibleCaret::AccessibleCaret(nsIPresShell* aPresShell)
+  : mPresShell(aPresShell)
+{
+  // Check all resources required.
+  MOZ_ASSERT(mPresShell);
+  MOZ_ASSERT(RootFrame());
+  MOZ_ASSERT(mPresShell->GetDocument());
+  MOZ_ASSERT(mPresShell->GetCanvasFrame());
+  MOZ_ASSERT(mPresShell->GetCanvasFrame()->GetCustomContentContainer());
+
+  InjectCaretElement(mPresShell->GetDocument());
+}
+
+AccessibleCaret::~AccessibleCaret()
+{
+  RemoveCaretElement(mPresShell->GetDocument());
+}
+
+void
+AccessibleCaret::SetAppearance(Appearance aAppearance)
+{
+  if (mAppearance == aAppearance) {
+    return;
+  }
+
+  ErrorResult rv;
+  CaretElement()->ClassList()->Remove(AppearanceString(mAppearance), rv);
+  MOZ_ASSERT(!rv.Failed(), "Remove old appearance failed!");
+
+  CaretElement()->ClassList()->Add(AppearanceString(aAppearance), rv);
+  MOZ_ASSERT(!rv.Failed(), "Add new appearance failed!");
+
+  mAppearance = aAppearance;
+
+  // Need to reset rect since the cached rect will be compared in SetPosition.
+  if (mAppearance == Appearance::None) {
+    mImaginaryCaretRect = nsRect();
+  }
+}
+
+void
+AccessibleCaret::SetSelectionBarEnabled(bool aEnabled)
+{
+  if (mSelectionBarEnabled == aEnabled) {
+    return;
+  }
+
+  AC_LOG("%s, enabled %d", __FUNCTION__, aEnabled);
+
+  ErrorResult rv;
+  CaretElement()->ClassList()->Toggle(NS_LITERAL_STRING("no-bar"),
+                                      Optional<bool>(!aEnabled), rv);
+  MOZ_ASSERT(!rv.Failed());
+
+  mSelectionBarEnabled = aEnabled;
+}
+
+/* static */ nsString
+AccessibleCaret::AppearanceString(Appearance aAppearance)
+{
+  nsAutoString string;
+  switch (aAppearance) {
+  case Appearance::None:
+  case Appearance::NormalNotShown:
+    string = NS_LITERAL_STRING("none");
+    break;
+  case Appearance::Normal:
+    string = NS_LITERAL_STRING("normal");
+    break;
+  case Appearance::Right:
+    string = NS_LITERAL_STRING("right");
+    break;
+  case Appearance::Left:
+    string = NS_LITERAL_STRING("left");
+    break;
+  }
+  return string;
+}
+
+bool
+AccessibleCaret::Intersects(const AccessibleCaret& aCaret) const
+{
+  MOZ_ASSERT(mPresShell == aCaret.mPresShell);
+
+  if (!IsVisuallyVisible() || !aCaret.IsVisuallyVisible()) {
+    return false;
+  }
+
+  nsRect rect = nsLayoutUtils::GetRectRelativeToFrame(CaretElement(), RootFrame());
+  nsRect rhsRect = nsLayoutUtils::GetRectRelativeToFrame(aCaret.CaretElement(), RootFrame());
+  return rect.Intersects(rhsRect);
+}
+
+bool
+AccessibleCaret::Contains(const nsPoint& aPoint) const
+{
+  if (!IsVisuallyVisible()) {
+    return false;
+  }
+
+  nsRect rect =
+    nsLayoutUtils::GetRectRelativeToFrame(CaretImageElement(), RootFrame());
+
+  return rect.Contains(aPoint);
+}
+
+void
+AccessibleCaret::InjectCaretElement(nsIDocument* aDocument)
+{
+  ErrorResult rv;
+  nsCOMPtr<Element> element = CreateCaretElement(aDocument);
+  mCaretElementHolder = aDocument->InsertAnonymousContent(*element, rv);
+
+  MOZ_ASSERT(!rv.Failed(), "Insert anonymous content should not fail!");
+  MOZ_ASSERT(mCaretElementHolder.get(), "We must have anonymous content!");
+
+  // InsertAnonymousContent will clone the element to make an AnonymousContent.
+  // Since event listeners are not being cloned when cloning a node, we need to
+  // add the listener here.
+  CaretElement()->AddEventListener(NS_LITERAL_STRING("touchstart"),
+                                   mDummyTouchListener, false);
+}
+
+already_AddRefed<Element>
+AccessibleCaret::CreateCaretElement(nsIDocument* aDocument) const
+{
+  // Content structure of AccessibleCaret
+  // <div class="moz-accessiblecaret">  <- CaretElement()
+  //   <div class="image">              <- CaretImageElement()
+  //   <div class="bar">                <- SelectionBarElement()
+
+  ErrorResult rv;
+  nsCOMPtr<Element> parent = aDocument->CreateHTMLElement(nsGkAtoms::div);
+  parent->ClassList()->Add(NS_LITERAL_STRING("moz-accessiblecaret"), rv);
+  parent->ClassList()->Add(NS_LITERAL_STRING("none"), rv);
+  parent->ClassList()->Add(NS_LITERAL_STRING("no-bar"), rv);
+
+  nsCOMPtr<Element> image = aDocument->CreateHTMLElement(nsGkAtoms::div);
+  image->ClassList()->Add(NS_LITERAL_STRING("image"), rv);
+  parent->AppendChildTo(image, false);
+
+  nsCOMPtr<Element> bar = aDocument->CreateHTMLElement(nsGkAtoms::div);
+  bar->ClassList()->Add(NS_LITERAL_STRING("bar"), rv);
+  parent->AppendChildTo(bar, false);
+
+  return parent.forget();
+}
+
+void
+AccessibleCaret::RemoveCaretElement(nsIDocument* aDocument)
+{
+  CaretElement()->RemoveEventListener(NS_LITERAL_STRING("touchstart"),
+                                      mDummyTouchListener, false);
+
+  ErrorResult rv;
+  aDocument->RemoveAnonymousContent(*mCaretElementHolder, rv);
+  // It's OK rv is failed since nsCanvasFrame might not exists now.
+}
+
+AccessibleCaret::PositionChangedResult
+AccessibleCaret::SetPosition(nsIFrame* aFrame, int32_t aOffset)
+{
+  if (!CustomContentContainerFrame()) {
+    return PositionChangedResult::NotChanged;
+  }
+
+  nsRect imaginaryCaretRectInFrame =
+    nsCaret::GetGeometryForFrame(aFrame, aOffset, nullptr);
+
+  imaginaryCaretRectInFrame =
+    nsLayoutUtils::ClampRectToScrollFrames(aFrame, imaginaryCaretRectInFrame);
+
+  if (imaginaryCaretRectInFrame.IsEmpty()) {
+    // Don't bother to set the caret position since it's invisible.
+    return PositionChangedResult::Invisible;
+  }
+
+  nsRect imaginaryCaretRect = imaginaryCaretRectInFrame;
+  nsLayoutUtils::TransformRect(aFrame, RootFrame(), imaginaryCaretRect);
+
+  if (imaginaryCaretRect.IsEqualEdges(mImaginaryCaretRect)) {
+    return PositionChangedResult::NotChanged;
+  }
+
+  mImaginaryCaretRect = imaginaryCaretRect;
+
+  // SetCaretElementPosition() and SetSelectionBarElementPosition() require the
+  // input rect relative to container frame.
+  nsRect imaginaryCaretRectInContainerFrame = imaginaryCaretRectInFrame;
+  nsLayoutUtils::TransformRect(aFrame, CustomContentContainerFrame(),
+                               imaginaryCaretRectInContainerFrame);
+  SetCaretElementPosition(imaginaryCaretRectInContainerFrame);
+  SetSelectionBarElementPosition(imaginaryCaretRectInContainerFrame);
+
+  return PositionChangedResult::Changed;
+}
+
+nsIFrame*
+AccessibleCaret::CustomContentContainerFrame() const
+{
+  nsCanvasFrame* canvasFrame = mPresShell->GetCanvasFrame();
+  Element* container = canvasFrame->GetCustomContentContainer();
+  nsIFrame* containerFrame = container->GetPrimaryFrame();
+  return containerFrame;
+}
+
+void
+AccessibleCaret::SetCaretElementPosition(const nsRect& aRect)
+{
+  nsPoint position = CaretElementPosition(aRect);
+  nsAutoString styleStr;
+  styleStr.AppendPrintf("left: %dpx; top: %dpx;",
+                        nsPresContext::AppUnitsToIntCSSPixels(position.x),
+                        nsPresContext::AppUnitsToIntCSSPixels(position.y));
+
+  ErrorResult rv;
+  CaretElement()->SetAttribute(NS_LITERAL_STRING("style"), styleStr, rv);
+  MOZ_ASSERT(!rv.Failed());
+
+  AC_LOG("Set caret style: %s", NS_ConvertUTF16toUTF8(styleStr).get());
+}
+
+void
+AccessibleCaret::SetSelectionBarElementPosition(const nsRect& aRect)
+{
+  int32_t height = nsPresContext::AppUnitsToIntCSSPixels(aRect.height);
+  nsAutoString barStyleStr;
+  barStyleStr.AppendPrintf("margin-top: -%dpx; height: %dpx;",
+                           height, height);
+
+  ErrorResult rv;
+  SelectionBarElement()->SetAttribute(NS_LITERAL_STRING("style"), barStyleStr, rv);
+  MOZ_ASSERT(!rv.Failed());
+
+  AC_LOG("Set bar style: %s", NS_ConvertUTF16toUTF8(barStyleStr).get());
+}
+
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/layout/base/AccessibleCaret.h
@@ -0,0 +1,209 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 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 AccessibleCaret_h__
+#define AccessibleCaret_h__
+
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/AnonymousContent.h"
+#include "mozilla/dom/Element.h"
+#include "nsCOMPtr.h"
+#include "nsIDOMEventListener.h"
+#include "nsISupportsBase.h"
+#include "nsISupportsImpl.h"
+#include "nsRect.h"
+#include "nsRefPtr.h"
+#include "nsString.h"
+
+class nsIDocument;
+class nsIFrame;
+class nsIPresShell;
+struct nsPoint;
+
+namespace mozilla {
+
+// -----------------------------------------------------------------------------
+// Upon the creation of AccessibleCaret, it will insert DOM Element as an
+// anonymous content containing the caret image. The caret appearance and
+// position can be controlled by SetAppearance() and SetPosition().
+//
+// All the rect or point are relative to root frame except being specified
+// explicitly.
+//
+// None of the methods in AccessibleCaret will flush layout or style. To ensure
+// that SetPosition() works correctly, the caller must make sure the layout is
+// up to date.
+//
+class AccessibleCaret final
+{
+public:
+  explicit AccessibleCaret(nsIPresShell* aPresShell);
+  ~AccessibleCaret();
+
+  // This enumeration representing the visibility and visual style of an
+  // AccessibleCaret.
+  //
+  // Use SetAppearance() to change the appearance, and use GetAppearance() to
+  // get the current appearance.
+  enum class Appearance : uint8_t {
+    // Do not display the caret at all.
+    None,
+
+    // Display the caret in default style.
+    Normal,
+
+    // The caret should be displayed logically but it is kept invisible to the
+    // user. This enum is the only difference between "logically visible" and
+    // "visually visible". It can be used for reasons such as:
+    // 1. Out of scroll port.
+    // 2. For UX requirement such as hide a caret in an empty text area.
+    NormalNotShown,
+
+    // Display the caret which is tilted to the left.
+    Left,
+
+    // Display the caret which is tilted to the right.
+    Right
+  };
+
+  Appearance GetAppearance() const
+  {
+    return mAppearance;
+  }
+
+  void SetAppearance(Appearance aAppearance);
+
+  // Return true if current appearance is either Normal, NormalNotShown, Left,
+  // or Right.
+  bool IsLogicallyVisible() const
+  {
+      return mAppearance != Appearance::None;
+  }
+
+  // Return true if current appearance is either Normal, Left, or Right.
+  bool IsVisuallyVisible() const
+  {
+    return (mAppearance != Appearance::None) &&
+           (mAppearance != Appearance::NormalNotShown);
+  }
+
+  // Set true to enable the "Text Selection Bar" described in "Text Selection
+  // Visual Spec" in bug 921965.
+  void SetSelectionBarEnabled(bool aEnabled);
+
+  // This enumeration representing the result returned by SetPosition().
+  enum class PositionChangedResult : uint8_t {
+    // Position is not changed.
+    NotChanged,
+
+    // Position is changed.
+    Changed,
+
+    // Position is out of scroll port.
+    Invisible
+  };
+  PositionChangedResult SetPosition(nsIFrame* aFrame, int32_t aOffset);
+
+  // Does two AccessibleCarets overlap?
+  bool Intersects(const AccessibleCaret& aCaret) const;
+
+  // Is the point within the caret's rect? The point should be relative to root
+  // frame.
+  bool Contains(const nsPoint& aPoint) const;
+
+  // The geometry center of the imaginary caret (nsCaret) to which this
+  // AccessibleCaret is attached. It is needed when dragging the caret.
+  nsPoint LogicalPosition() const
+  {
+    return mImaginaryCaretRect.Center();
+  }
+
+  // Element for 'Intersects' test. Container of image and bar elements.
+  dom::Element* CaretElement() const
+  {
+    return mCaretElementHolder->GetContentNode();
+  }
+
+private:
+  // Argument aRect should be relative to CustomContentContainerFrame().
+  void SetCaretElementPosition(const nsRect& aRect);
+  void SetSelectionBarElementPosition(const nsRect& aRect);
+
+  // Element which contains the caret image for 'Contains' test.
+  dom::Element* CaretImageElement() const
+  {
+    return CaretElement()->GetFirstElementChild();
+  }
+
+  // Element which represents the text selection bar.
+  dom::Element* SelectionBarElement() const
+  {
+    return CaretElement()->GetLastElementChild();
+  }
+
+  nsIFrame* RootFrame() const
+  {
+    return mPresShell->GetRootFrame();
+  }
+
+  nsIFrame* CustomContentContainerFrame() const;
+
+  // Transform Appearance to CSS class name in ua.css.
+  static nsString AppearanceString(Appearance aAppearance);
+
+  already_AddRefed<dom::Element> CreateCaretElement(nsIDocument* aDocument) const;
+
+  // Inject caret element into custom content container.
+  void InjectCaretElement(nsIDocument* aDocument);
+
+  // Remove caret element from custom content container.
+  void RemoveCaretElement(nsIDocument* aDocument);
+
+  // The bottom-center of the imaginary caret to which this AccessibleCaret is
+  // attached.
+  static nsPoint CaretElementPosition(const nsRect& aRect)
+  {
+    return aRect.TopLeft() + nsPoint(aRect.width / 2, aRect.height);
+  }
+
+  class DummyTouchListener final : public nsIDOMEventListener
+  {
+  public:
+    NS_DECL_ISUPPORTS
+    NS_IMETHOD HandleEvent(nsIDOMEvent* aEvent) override
+    {
+      return NS_OK;
+    }
+
+  private:
+    virtual ~DummyTouchListener() {};
+  };
+
+  // Member variables
+  Appearance mAppearance = Appearance::None;
+
+  bool mSelectionBarEnabled = false;
+
+  // AccessibleCaretManager owns us. When it's destroyed by
+  // AccessibleCaretEventHub::Terminate() which is called in
+  // PresShell::Destroy(), it frees us automatically. No need to worry we
+  // outlive mPresShell.
+  nsIPresShell* MOZ_NON_OWNING_REF const mPresShell = nullptr;
+
+  nsRefPtr<dom::AnonymousContent> mCaretElementHolder;
+
+  // mImaginaryCaretRect is relative to root frame.
+  nsRect mImaginaryCaretRect;
+
+  // A no-op touch-start listener which prevents APZ from panning when dragging
+  // the caret.
+  nsRefPtr<DummyTouchListener> mDummyTouchListener{new DummyTouchListener()};
+
+}; // class AccessibleCaret
+
+} // namespace mozilla
+
+#endif // AccessibleCaret_h__
--- a/layout/style/ua.css
+++ b/layout/style/ua.css
@@ -309,16 +309,110 @@ parsererror|sourcetext {
   font-family: -moz-fixed;
   margin-top: 2em;
   margin-bottom: 1em;
   color: red;
   font-weight: bold;
   font-size: 12pt;
 }
 
+div:-moz-native-anonymous.moz-accessiblecaret,
+div:-moz-native-anonymous.moz-accessiblecaret > div.image,
+div:-moz-native-anonymous.moz-accessiblecaret > div.bar {
+  position: absolute;
+  z-index: 2147483647;
+}
+
+div:-moz-native-anonymous.moz-accessiblecaret {
+  width: 44px;
+  height: 47px;
+  margin-left: -23px;
+}
+
+div:-moz-native-anonymous.moz-accessiblecaret > div.image {
+  background-position: center center;
+  background-size: 100% 100%;
+  width: 100%;
+  height: 100%;
+
+  /* Override this property in moz-custom-content-container to make dummy touch
+   * listener work. */
+  pointer-events: auto;
+}
+
+div:-moz-native-anonymous.moz-accessiblecaret > div.bar {
+  margin-left: 49%;
+  width: 2px;
+  background-color: #008aa0;
+}
+
+div:-moz-native-anonymous.moz-accessiblecaret.no-bar > div.bar {
+  display: none;
+}
+
+div:-moz-native-anonymous.moz-accessiblecaret.normal > div.image {
+  background-image: url("resource://gre/res/text_caret.png");
+}
+
+div:-moz-native-anonymous.moz-accessiblecaret.left > div.image {
+  background-image: url("resource://gre/res/text_caret_tilt_left.png");
+  margin-left: -39%;
+}
+
+div:-moz-native-anonymous.moz-accessiblecaret.right > div.image {
+  background-image: url("resource://gre/res/text_caret_tilt_right.png");
+  margin-left: 41%;
+}
+
+div:-moz-native-anonymous.moz-accessiblecaret.none {
+  display: none;
+}
+
+@media (min-resolution: 1.5dppx) {
+  div:-moz-native-anonymous.moz-accessiblecaret.normal > div.image {
+    background-image: url("resource://gre/res/text_caret@1.5x.png");
+  }
+
+  div:-moz-native-anonymous.moz-accessiblecaret.left > div.image {
+    background-image: url("resource://gre/res/text_caret_tilt_left@1.5x.png");
+  }
+
+  div:-moz-native-anonymous.moz-accessiblecaret.right > div.image {
+    background-image: url("resource://gre/res/text_caret_tilt_right@1.5x.png");
+  }
+}
+
+@media (min-resolution: 2dppx) {
+  div:-moz-native-anonymous.moz-accessiblecaret.normal > div.image {
+    background-image: url("resource://gre/res/text_caret@2x.png");
+  }
+
+  div:-moz-native-anonymous.moz-accessiblecaret.left > div.image {
+    background-image: url("resource://gre/res/text_caret_tilt_left@2x.png");
+  }
+
+  div:-moz-native-anonymous.moz-accessiblecaret.right > div.image {
+    background-image: url("resource://gre/res/text_caret_tilt_right@2x.png");
+  }
+}
+
+@media (min-resolution: 2.25dppx) {
+  div:-moz-native-anonymous.moz-accessiblecaret.normal > div.image {
+    background-image: url("resource://gre/res/text_caret@2.25x.png");
+  }
+
+  div:-moz-native-anonymous.moz-accessiblecaret.left > div.image {
+    background-image: url("resource://gre/res/text_caret_tilt_left@2.25x.png");
+  }
+
+  div:-moz-native-anonymous.moz-accessiblecaret.right > div.image {
+    background-image: url("resource://gre/res/text_caret_tilt_right@2.25x.png");
+  }
+}
+
 div:-moz-native-anonymous.moz-touchcaret,
 div:-moz-native-anonymous.moz-selectioncaret-left,
 div:-moz-native-anonymous.moz-selectioncaret-right {
   position: fixed;
   width: 44px;
   height: 47px;
 }