diff options
Diffstat (limited to 'layout/base/AccessibleCaret.cpp')
-rw-r--r-- | layout/base/AccessibleCaret.cpp | 392 |
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 |