1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include "AccessibleCaret.h"
8 
9 #include "AccessibleCaretLogger.h"
10 #include "mozilla/FloatingPoint.h"
11 #include "mozilla/PresShell.h"
12 #include "mozilla/StaticPrefs_layout.h"
13 #include "mozilla/ToString.h"
14 #include "nsCanvasFrame.h"
15 #include "nsCaret.h"
16 #include "nsCSSFrameConstructor.h"
17 #include "nsDOMTokenList.h"
18 #include "nsIFrame.h"
19 #include "nsLayoutUtils.h"
20 #include "nsPlaceholderFrame.h"
21 
22 namespace mozilla {
23 using namespace dom;
24 
25 #undef AC_LOG
26 #define AC_LOG(message, ...) \
27   AC_LOG_BASE("AccessibleCaret (%p): " message, this, ##__VA_ARGS__);
28 
29 #undef AC_LOGV
30 #define AC_LOGV(message, ...) \
31   AC_LOGV_BASE("AccessibleCaret (%p): " message, this, ##__VA_ARGS__);
32 
33 NS_IMPL_ISUPPORTS(AccessibleCaret::DummyTouchListener, nsIDOMEventListener)
34 
35 const nsLiteralString AccessibleCaret::sTextOverlayElementId =
36     u"text-overlay"_ns;
37 const nsLiteralString AccessibleCaret::sCaretImageElementId = u"image"_ns;
38 
39 #define AC_PROCESS_ENUM_TO_STREAM(e) \
40   case (e):                          \
41     aStream << #e;                   \
42     break;
operator <<(std::ostream & aStream,const AccessibleCaret::Appearance & aAppearance)43 std::ostream& operator<<(std::ostream& aStream,
44                          const AccessibleCaret::Appearance& aAppearance) {
45   using Appearance = AccessibleCaret::Appearance;
46   switch (aAppearance) {
47     AC_PROCESS_ENUM_TO_STREAM(Appearance::None);
48     AC_PROCESS_ENUM_TO_STREAM(Appearance::Normal);
49     AC_PROCESS_ENUM_TO_STREAM(Appearance::NormalNotShown);
50     AC_PROCESS_ENUM_TO_STREAM(Appearance::Left);
51     AC_PROCESS_ENUM_TO_STREAM(Appearance::Right);
52   }
53   return aStream;
54 }
55 
operator <<(std::ostream & aStream,const AccessibleCaret::PositionChangedResult & aResult)56 std::ostream& operator<<(
57     std::ostream& aStream,
58     const AccessibleCaret::PositionChangedResult& aResult) {
59   using PositionChangedResult = AccessibleCaret::PositionChangedResult;
60   switch (aResult) {
61     AC_PROCESS_ENUM_TO_STREAM(PositionChangedResult::NotChanged);
62     AC_PROCESS_ENUM_TO_STREAM(PositionChangedResult::Position);
63     AC_PROCESS_ENUM_TO_STREAM(PositionChangedResult::Zoom);
64     AC_PROCESS_ENUM_TO_STREAM(PositionChangedResult::Invisible);
65   }
66   return aStream;
67 }
68 #undef AC_PROCESS_ENUM_TO_STREAM
69 
70 // -----------------------------------------------------------------------------
71 // Implementation of AccessibleCaret methods
72 
AccessibleCaret(PresShell * aPresShell)73 AccessibleCaret::AccessibleCaret(PresShell* aPresShell)
74     : mPresShell(aPresShell) {
75   // Check all resources required.
76   if (mPresShell) {
77     MOZ_ASSERT(RootFrame());
78     MOZ_ASSERT(mPresShell->GetDocument());
79     InjectCaretElement(mPresShell->GetDocument());
80   }
81 }
82 
~AccessibleCaret()83 AccessibleCaret::~AccessibleCaret() {
84   if (mPresShell) {
85     RemoveCaretElement(mPresShell->GetDocument());
86   }
87 }
88 
SetAppearance(Appearance aAppearance)89 void AccessibleCaret::SetAppearance(Appearance aAppearance) {
90   if (mAppearance == aAppearance) {
91     return;
92   }
93 
94   ErrorResult rv;
95   CaretElement().ClassList()->Remove(AppearanceString(mAppearance), rv);
96   MOZ_ASSERT(!rv.Failed(), "Remove old appearance failed!");
97 
98   CaretElement().ClassList()->Add(AppearanceString(aAppearance), rv);
99   MOZ_ASSERT(!rv.Failed(), "Add new appearance failed!");
100 
101   AC_LOG("%s: %s -> %s", __FUNCTION__, ToString(mAppearance).c_str(),
102          ToString(aAppearance).c_str());
103 
104   mAppearance = aAppearance;
105 
106   // Need to reset rect since the cached rect will be compared in SetPosition.
107   if (mAppearance == Appearance::None) {
108     ClearCachedData();
109   }
110 }
111 
112 /* static */
AppearanceString(Appearance aAppearance)113 nsAutoString AccessibleCaret::AppearanceString(Appearance aAppearance) {
114   nsAutoString string;
115   switch (aAppearance) {
116     case Appearance::None:
117       string = u"none"_ns;
118       break;
119     case Appearance::NormalNotShown:
120       string = u"hidden"_ns;
121       break;
122     case Appearance::Normal:
123       string = u"normal"_ns;
124       break;
125     case Appearance::Right:
126       string = u"right"_ns;
127       break;
128     case Appearance::Left:
129       string = u"left"_ns;
130       break;
131   }
132   return string;
133 }
134 
Intersects(const AccessibleCaret & aCaret) const135 bool AccessibleCaret::Intersects(const AccessibleCaret& aCaret) const {
136   MOZ_ASSERT(mPresShell == aCaret.mPresShell);
137 
138   if (!IsVisuallyVisible() || !aCaret.IsVisuallyVisible()) {
139     return false;
140   }
141 
142   nsRect rect =
143       nsLayoutUtils::GetRectRelativeToFrame(&CaretElement(), RootFrame());
144   nsRect rhsRect = nsLayoutUtils::GetRectRelativeToFrame(&aCaret.CaretElement(),
145                                                          RootFrame());
146   return rect.Intersects(rhsRect);
147 }
148 
Contains(const nsPoint & aPoint,TouchArea aTouchArea) const149 bool AccessibleCaret::Contains(const nsPoint& aPoint,
150                                TouchArea aTouchArea) const {
151   if (!IsVisuallyVisible()) {
152     return false;
153   }
154 
155   nsRect textOverlayRect =
156       nsLayoutUtils::GetRectRelativeToFrame(TextOverlayElement(), RootFrame());
157   nsRect caretImageRect =
158       nsLayoutUtils::GetRectRelativeToFrame(CaretImageElement(), RootFrame());
159 
160   if (aTouchArea == TouchArea::CaretImage) {
161     return caretImageRect.Contains(aPoint);
162   }
163 
164   MOZ_ASSERT(aTouchArea == TouchArea::Full, "Unexpected TouchArea type!");
165   return textOverlayRect.Contains(aPoint) || caretImageRect.Contains(aPoint);
166 }
167 
EnsureApzAware()168 void AccessibleCaret::EnsureApzAware() {
169   // If the caret element was cloned, the listener might have been lost. So
170   // if that's the case we register a dummy listener if there isn't one on
171   // the element already.
172   if (!CaretElement().IsApzAware()) {
173     // FIXME(emilio): Is this needed anymore?
174     CaretElement().AddEventListener(u"touchstart"_ns, mDummyTouchListener,
175                                     false);
176   }
177 }
178 
IsInPositionFixedSubtree() const179 bool AccessibleCaret::IsInPositionFixedSubtree() const {
180   return nsLayoutUtils::IsInPositionFixedSubtree(
181       mImaginaryCaretReferenceFrame.GetFrame());
182 }
183 
InjectCaretElement(Document * aDocument)184 void AccessibleCaret::InjectCaretElement(Document* aDocument) {
185   ErrorResult rv;
186   RefPtr<Element> element = CreateCaretElement(aDocument);
187   mCaretElementHolder = aDocument->InsertAnonymousContent(*element, rv);
188 
189   MOZ_ASSERT(!rv.Failed(), "Insert anonymous content should not fail!");
190   MOZ_ASSERT(mCaretElementHolder, "We must have anonymous content!");
191 
192   // InsertAnonymousContent will clone the element to make an AnonymousContent.
193   // Since event listeners are not being cloned when cloning a node, we need to
194   // add the listener here.
195   EnsureApzAware();
196 }
197 
CreateCaretElement(Document * aDocument) const198 already_AddRefed<Element> AccessibleCaret::CreateCaretElement(
199     Document* aDocument) const {
200   // Content structure of AccessibleCaret
201   // <div class="moz-accessiblecaret">  <- CaretElement()
202   //   <div id="text-overlay">          <- TextOverlayElement()
203   //   <div id="image">                 <- CaretImageElement()
204 
205   ErrorResult rv;
206   RefPtr<Element> parent = aDocument->CreateHTMLElement(nsGkAtoms::div);
207   parent->ClassList()->Add(u"moz-accessiblecaret"_ns, rv);
208   parent->ClassList()->Add(u"none"_ns, rv);
209 
210   auto CreateAndAppendChildElement =
211       [aDocument, &parent](const nsLiteralString& aElementId) {
212         RefPtr<Element> child = aDocument->CreateHTMLElement(nsGkAtoms::div);
213         child->SetAttr(kNameSpaceID_None, nsGkAtoms::id, aElementId, true);
214         parent->AppendChildTo(child, false, IgnoreErrors());
215       };
216 
217   CreateAndAppendChildElement(sTextOverlayElementId);
218   CreateAndAppendChildElement(sCaretImageElementId);
219 
220   return parent.forget();
221 }
222 
RemoveCaretElement(Document * aDocument)223 void AccessibleCaret::RemoveCaretElement(Document* aDocument) {
224   CaretElement().RemoveEventListener(u"touchstart"_ns, mDummyTouchListener,
225                                      false);
226 
227   aDocument->RemoveAnonymousContent(*mCaretElementHolder, IgnoreErrors());
228 }
229 
ClearCachedData()230 void AccessibleCaret::ClearCachedData() {
231   mImaginaryCaretRect = nsRect();
232   mImaginaryCaretRectInContainerFrame = nsRect();
233   mImaginaryCaretReferenceFrame = nullptr;
234   mZoomLevel = 0.0f;
235 }
236 
SetPosition(nsIFrame * aFrame,int32_t aOffset)237 AccessibleCaret::PositionChangedResult AccessibleCaret::SetPosition(
238     nsIFrame* aFrame, int32_t aOffset) {
239   if (!CustomContentContainerFrame()) {
240     return PositionChangedResult::NotChanged;
241   }
242 
243   nsRect imaginaryCaretRectInFrame =
244       nsCaret::GetGeometryForFrame(aFrame, aOffset, nullptr);
245 
246   imaginaryCaretRectInFrame =
247       nsLayoutUtils::ClampRectToScrollFrames(aFrame, imaginaryCaretRectInFrame);
248 
249   if (imaginaryCaretRectInFrame.IsEmpty()) {
250     // Don't bother to set the caret position since it's invisible.
251     ClearCachedData();
252     return PositionChangedResult::Invisible;
253   }
254 
255   // SetCaretElementStyle() requires the input rect relative to the custom
256   // content container frame.
257   nsRect imaginaryCaretRectInContainerFrame = imaginaryCaretRectInFrame;
258   nsLayoutUtils::TransformRect(aFrame, CustomContentContainerFrame(),
259                                imaginaryCaretRectInContainerFrame);
260   const float zoomLevel = GetZoomLevel();
261   const bool isSamePosition = imaginaryCaretRectInContainerFrame.IsEqualEdges(
262       mImaginaryCaretRectInContainerFrame);
263   const bool isSameZoomLevel = FuzzyEqualsMultiplicative(zoomLevel, mZoomLevel);
264 
265   // Always update cached mImaginaryCaretRect (relative to the root frame)
266   // because it can change when the caret is scrolled.
267   mImaginaryCaretRect = imaginaryCaretRectInFrame;
268   nsLayoutUtils::TransformRect(aFrame, RootFrame(), mImaginaryCaretRect);
269 
270   if (isSamePosition && isSameZoomLevel) {
271     return PositionChangedResult::NotChanged;
272   }
273 
274   mImaginaryCaretRectInContainerFrame = imaginaryCaretRectInContainerFrame;
275   mImaginaryCaretReferenceFrame = aFrame;
276   mZoomLevel = zoomLevel;
277 
278   SetCaretElementStyle(imaginaryCaretRectInContainerFrame, mZoomLevel);
279 
280   return isSamePosition ? PositionChangedResult::Zoom
281                         : PositionChangedResult::Position;
282 }
283 
RootFrame() const284 nsIFrame* AccessibleCaret::RootFrame() const {
285   return mPresShell->GetRootFrame();
286 }
287 
CustomContentContainerFrame() const288 nsIFrame* AccessibleCaret::CustomContentContainerFrame() const {
289   nsCanvasFrame* canvasFrame = mPresShell->GetCanvasFrame();
290   Element* container = canvasFrame->GetCustomContentContainer();
291   nsIFrame* containerFrame = container->GetPrimaryFrame();
292   return containerFrame;
293 }
294 
SetCaretElementStyle(const nsRect & aRect,float aZoomLevel)295 void AccessibleCaret::SetCaretElementStyle(const nsRect& aRect,
296                                            float aZoomLevel) {
297   nsPoint position = CaretElementPosition(aRect);
298   nsAutoString styleStr;
299   // We can't use AppendPrintf here, because it does locale-specific
300   // formatting of floating-point values.
301   styleStr.AppendLiteral("left: ");
302   styleStr.AppendFloat(nsPresContext::AppUnitsToFloatCSSPixels(position.x));
303   styleStr.AppendLiteral("px; top: ");
304   styleStr.AppendFloat(nsPresContext::AppUnitsToFloatCSSPixels(position.y));
305   styleStr.AppendLiteral("px; width: ");
306   styleStr.AppendFloat(StaticPrefs::layout_accessiblecaret_width() /
307                        aZoomLevel);
308   styleStr.AppendLiteral("px; margin-left: ");
309   styleStr.AppendFloat(StaticPrefs::layout_accessiblecaret_margin_left() /
310                        aZoomLevel);
311   styleStr.AppendLiteral("px; transition-duration: ");
312   styleStr.AppendFloat(
313       StaticPrefs::layout_accessiblecaret_transition_duration());
314   styleStr.AppendLiteral("ms");
315 
316   CaretElement().SetAttr(kNameSpaceID_None, nsGkAtoms::style, styleStr, true);
317   AC_LOG("%s: %s", __FUNCTION__, NS_ConvertUTF16toUTF8(styleStr).get());
318 
319   // Set style string for children.
320   SetTextOverlayElementStyle(aRect, aZoomLevel);
321   SetCaretImageElementStyle(aRect, aZoomLevel);
322 }
323 
SetTextOverlayElementStyle(const nsRect & aRect,float aZoomLevel)324 void AccessibleCaret::SetTextOverlayElementStyle(const nsRect& aRect,
325                                                  float aZoomLevel) {
326   nsAutoString styleStr;
327   styleStr.AppendLiteral("height: ");
328   styleStr.AppendFloat(nsPresContext::AppUnitsToFloatCSSPixels(aRect.height));
329   styleStr.AppendLiteral("px;");
330   TextOverlayElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::style, styleStr,
331                                 true);
332   AC_LOG("%s: %s", __FUNCTION__, NS_ConvertUTF16toUTF8(styleStr).get());
333 }
334 
SetCaretImageElementStyle(const nsRect & aRect,float aZoomLevel)335 void AccessibleCaret::SetCaretImageElementStyle(const nsRect& aRect,
336                                                 float aZoomLevel) {
337   nsAutoString styleStr;
338   styleStr.AppendLiteral("height: ");
339   styleStr.AppendFloat(StaticPrefs::layout_accessiblecaret_height() /
340                        aZoomLevel);
341   styleStr.AppendLiteral("px;");
342   CaretImageElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::style, styleStr,
343                                true);
344   AC_LOG("%s: %s", __FUNCTION__, NS_ConvertUTF16toUTF8(styleStr).get());
345 }
346 
GetZoomLevel()347 float AccessibleCaret::GetZoomLevel() {
348   // Full zoom on desktop.
349   float fullZoom = mPresShell->GetPresContext()->GetFullZoom();
350 
351   // Pinch-zoom on fennec.
352   float resolution = mPresShell->GetCumulativeResolution();
353 
354   return fullZoom * resolution;
355 }
356 
357 }  // namespace mozilla
358