summaryrefslogtreecommitdiffstats
path: root/layout/base/AccessibleCaret.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'layout/base/AccessibleCaret.cpp')
-rw-r--r--layout/base/AccessibleCaret.cpp392
1 files changed, 392 insertions, 0 deletions
diff --git a/layout/base/AccessibleCaret.cpp b/layout/base/AccessibleCaret.cpp
new file mode 100644
index 000000000..165f385f5
--- /dev/null
+++ b/layout/base/AccessibleCaret.cpp
@@ -0,0 +1,392 @@
+/* -*- 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/FloatingPoint.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/ToString.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)
+
+float AccessibleCaret::sWidth = 0.0f;
+float AccessibleCaret::sHeight = 0.0f;
+float AccessibleCaret::sMarginLeft = 0.0f;
+float AccessibleCaret::sBarWidth = 0.0f;
+
+NS_NAMED_LITERAL_STRING(AccessibleCaret::sTextOverlayElementId, "text-overlay");
+NS_NAMED_LITERAL_STRING(AccessibleCaret::sCaretImageElementId, "image");
+NS_NAMED_LITERAL_STRING(AccessibleCaret::sSelectionBarElementId, "bar");
+
+#define AC_PROCESS_ENUM_TO_STREAM(e) case(e): aStream << #e; break;
+std::ostream&
+operator<<(std::ostream& aStream, const AccessibleCaret::Appearance& aAppearance)
+{
+ using Appearance = AccessibleCaret::Appearance;
+ switch (aAppearance) {
+ AC_PROCESS_ENUM_TO_STREAM(Appearance::None);
+ AC_PROCESS_ENUM_TO_STREAM(Appearance::Normal);
+ AC_PROCESS_ENUM_TO_STREAM(Appearance::NormalNotShown);
+ AC_PROCESS_ENUM_TO_STREAM(Appearance::Left);
+ AC_PROCESS_ENUM_TO_STREAM(Appearance::Right);
+ }
+ return aStream;
+}
+
+std::ostream&
+operator<<(std::ostream& aStream,
+ const AccessibleCaret::PositionChangedResult& aResult)
+{
+ using PositionChangedResult = AccessibleCaret::PositionChangedResult;
+ switch (aResult) {
+ AC_PROCESS_ENUM_TO_STREAM(PositionChangedResult::NotChanged);
+ AC_PROCESS_ENUM_TO_STREAM(PositionChangedResult::Changed);
+ AC_PROCESS_ENUM_TO_STREAM(PositionChangedResult::Invisible);
+ }
+ return aStream;
+}
+#undef AC_PROCESS_ENUM_TO_STREAM
+
+// -----------------------------------------------------------------------------
+// Implementation of AccessibleCaret methods
+
+AccessibleCaret::AccessibleCaret(nsIPresShell* aPresShell)
+ : mPresShell(aPresShell)
+{
+ // Check all resources required.
+ if (mPresShell) {
+ MOZ_ASSERT(RootFrame());
+ MOZ_ASSERT(mPresShell->GetDocument());
+ MOZ_ASSERT(mPresShell->GetCanvasFrame());
+ MOZ_ASSERT(mPresShell->GetCanvasFrame()->GetCustomContentContainer());
+
+ InjectCaretElement(mPresShell->GetDocument());
+ }
+
+ static bool prefsAdded = false;
+ if (!prefsAdded) {
+ Preferences::AddFloatVarCache(&sWidth, "layout.accessiblecaret.width");
+ Preferences::AddFloatVarCache(&sHeight, "layout.accessiblecaret.height");
+ Preferences::AddFloatVarCache(&sMarginLeft, "layout.accessiblecaret.margin-left");
+ Preferences::AddFloatVarCache(&sBarWidth, "layout.accessiblecaret.bar.width");
+ prefsAdded = true;
+ }
+}
+
+AccessibleCaret::~AccessibleCaret()
+{
+ if (mPresShell) {
+ 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!");
+
+ AC_LOG("%s: %s -> %s", __FUNCTION__, ToString(mAppearance).c_str(),
+ ToString(aAppearance).c_str());
+
+ mAppearance = aAppearance;
+
+ // Need to reset rect since the cached rect will be compared in SetPosition.
+ if (mAppearance == Appearance::None) {
+ mImaginaryCaretRect = nsRect();
+ mZoomLevel = 0.0f;
+ }
+}
+
+void
+AccessibleCaret::SetSelectionBarEnabled(bool aEnabled)
+{
+ if (mSelectionBarEnabled == aEnabled) {
+ return;
+ }
+
+ AC_LOG("Set selection bar %s", aEnabled ? "Enabled" : "Disabled");
+
+ ErrorResult rv;
+ CaretElement()->ClassList()->Toggle(NS_LITERAL_STRING("no-bar"),
+ Optional<bool>(!aEnabled), rv);
+ MOZ_ASSERT(!rv.Failed());
+
+ mSelectionBarEnabled = aEnabled;
+}
+
+/* static */ nsAutoString
+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, TouchArea aTouchArea) const
+{
+ if (!IsVisuallyVisible()) {
+ return false;
+ }
+
+ nsRect textOverlayRect =
+ nsLayoutUtils::GetRectRelativeToFrame(TextOverlayElement(), RootFrame());
+ nsRect caretImageRect =
+ nsLayoutUtils::GetRectRelativeToFrame(CaretImageElement(), RootFrame());
+
+ if (aTouchArea == TouchArea::CaretImage) {
+ return caretImageRect.Contains(aPoint);
+ }
+
+ MOZ_ASSERT(aTouchArea == TouchArea::Full, "Unexpected TouchArea type!");
+ return textOverlayRect.Contains(aPoint) || caretImageRect.Contains(aPoint);
+}
+
+void
+AccessibleCaret::EnsureApzAware()
+{
+ // If the caret element was cloned, the listener might have been lost. So
+ // if that's the case we register a dummy listener if there isn't one on
+ // the element already.
+ if (!CaretElement()->IsApzAware()) {
+ CaretElement()->AddEventListener(NS_LITERAL_STRING("touchstart"),
+ mDummyTouchListener, false);
+ }
+}
+
+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.
+ EnsureApzAware();
+}
+
+already_AddRefed<Element>
+AccessibleCaret::CreateCaretElement(nsIDocument* aDocument) const
+{
+ // Content structure of AccessibleCaret
+ // <div class="moz-accessiblecaret"> <- CaretElement()
+ // <div id="text-overlay" <- TextOverlayElement()
+ // <div id="image"> <- CaretImageElement()
+ // <div id="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);
+
+ auto CreateAndAppendChildElement = [aDocument, &parent](
+ const nsLiteralString& aElementId)
+ {
+ nsCOMPtr<Element> child = aDocument->CreateHTMLElement(nsGkAtoms::div);
+ child->SetAttr(kNameSpaceID_None, nsGkAtoms::id, aElementId, true);
+ parent->AppendChildTo(child, false);
+ };
+
+ CreateAndAppendChildElement(sTextOverlayElementId);
+ CreateAndAppendChildElement(sCaretImageElementId);
+ CreateAndAppendChildElement(sSelectionBarElementId);
+
+ 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.
+ rv.SuppressException();
+}
+
+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.
+ mImaginaryCaretRect = nsRect();
+ mZoomLevel = 0.0f;
+ return PositionChangedResult::Invisible;
+ }
+
+ nsRect imaginaryCaretRect = imaginaryCaretRectInFrame;
+ nsLayoutUtils::TransformRect(aFrame, RootFrame(), imaginaryCaretRect);
+ float zoomLevel = GetZoomLevel();
+
+ if (imaginaryCaretRect.IsEqualEdges(mImaginaryCaretRect) &&
+ FuzzyEqualsMultiplicative(zoomLevel, mZoomLevel)) {
+ return PositionChangedResult::NotChanged;
+ }
+
+ mImaginaryCaretRect = imaginaryCaretRect;
+ mZoomLevel = zoomLevel;
+
+ // SetCaretElementStyle() requires the input rect relative to container frame.
+ nsRect imaginaryCaretRectInContainerFrame = imaginaryCaretRectInFrame;
+ nsLayoutUtils::TransformRect(aFrame, CustomContentContainerFrame(),
+ imaginaryCaretRectInContainerFrame);
+ SetCaretElementStyle(imaginaryCaretRectInContainerFrame, mZoomLevel);
+
+ return PositionChangedResult::Changed;
+}
+
+nsIFrame*
+AccessibleCaret::CustomContentContainerFrame() const
+{
+ nsCanvasFrame* canvasFrame = mPresShell->GetCanvasFrame();
+ Element* container = canvasFrame->GetCustomContentContainer();
+ nsIFrame* containerFrame = container->GetPrimaryFrame();
+ return containerFrame;
+}
+
+void
+AccessibleCaret::SetCaretElementStyle(const nsRect& aRect, float aZoomLevel)
+{
+ nsPoint position = CaretElementPosition(aRect);
+ nsAutoString styleStr;
+ styleStr.AppendPrintf("left: %dpx; top: %dpx; "
+ "width: %.2fpx; height: %.2fpx; margin-left: %.2fpx",
+ nsPresContext::AppUnitsToIntCSSPixels(position.x),
+ nsPresContext::AppUnitsToIntCSSPixels(position.y),
+ sWidth / aZoomLevel,
+ sHeight / aZoomLevel,
+ sMarginLeft / aZoomLevel);
+
+ CaretElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::style, styleStr, true);
+ AC_LOG("%s: %s", __FUNCTION__, NS_ConvertUTF16toUTF8(styleStr).get());
+
+ // Set style string for children.
+ SetTextOverlayElementStyle(aRect, aZoomLevel);
+ SetCaretImageElementStyle(aRect, aZoomLevel);
+ SetSelectionBarElementStyle(aRect, aZoomLevel);
+}
+
+void
+AccessibleCaret::SetTextOverlayElementStyle(const nsRect& aRect,
+ float aZoomLevel)
+{
+ nsAutoString styleStr;
+ styleStr.AppendPrintf("height: %dpx;",
+ nsPresContext::AppUnitsToIntCSSPixels(aRect.height));
+ TextOverlayElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::style, styleStr,
+ true);
+ AC_LOG("%s: %s", __FUNCTION__, NS_ConvertUTF16toUTF8(styleStr).get());
+}
+
+void
+AccessibleCaret::SetCaretImageElementStyle(const nsRect& aRect,
+ float aZoomLevel)
+{
+ nsAutoString styleStr;
+ styleStr.AppendPrintf("margin-top: %dpx;",
+ nsPresContext::AppUnitsToIntCSSPixels(aRect.height));
+ CaretImageElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::style, styleStr,
+ true);
+ AC_LOG("%s: %s", __FUNCTION__, NS_ConvertUTF16toUTF8(styleStr).get());
+}
+
+void
+AccessibleCaret::SetSelectionBarElementStyle(const nsRect& aRect,
+ float aZoomLevel)
+{
+ nsAutoString styleStr;
+ styleStr.AppendPrintf("height: %dpx; width: %.2fpx;",
+ nsPresContext::AppUnitsToIntCSSPixels(aRect.height),
+ sBarWidth / aZoomLevel);
+ SelectionBarElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::style, styleStr,
+ true);
+ AC_LOG("%s: %s", __FUNCTION__, NS_ConvertUTF16toUTF8(styleStr).get());
+}
+
+float
+AccessibleCaret::GetZoomLevel()
+{
+ // Full zoom on desktop.
+ float fullZoom = mPresShell->GetPresContext()->GetFullZoom();
+
+ // Pinch-zoom on B2G or fennec.
+ float resolution = mPresShell->GetCumulativeResolution();
+
+ return fullZoom * resolution;
+}
+
+} // namespace mozilla