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/. */
7 #include "AccessibleCaret.h"
9 #include "AccessibleCaretLogger.h"
10 #include "mozilla/FloatingPoint.h"
11 #include "mozilla/PresShell.h"
12 #include "mozilla/StaticPrefs_layout.h"
13 #include "mozilla/dom/Document.h"
14 #include "mozilla/ToString.h"
15 #include "nsCanvasFrame.h"
17 #include "nsCSSFrameConstructor.h"
18 #include "nsDOMTokenList.h"
20 #include "nsLayoutUtils.h"
21 #include "nsPlaceholderFrame.h"
27 #define AC_LOG(message, ...) \
28 AC_LOG_BASE("AccessibleCaret (%p): " message, this, ##__VA_ARGS__);
31 #define AC_LOGV(message, ...) \
32 AC_LOGV_BASE("AccessibleCaret (%p): " message, this, ##__VA_ARGS__);
34 NS_IMPL_ISUPPORTS(AccessibleCaret::DummyTouchListener
, nsIDOMEventListener
)
36 const nsLiteralString
AccessibleCaret::sTextOverlayElementId
=
38 const nsLiteralString
AccessibleCaret::sCaretImageElementId
= u
"image"_ns
;
40 #define AC_PROCESS_ENUM_TO_STREAM(e) \
44 std::ostream
& operator<<(std::ostream
& aStream
,
45 const AccessibleCaret::Appearance
& aAppearance
) {
46 using Appearance
= AccessibleCaret::Appearance
;
47 switch (aAppearance
) {
48 AC_PROCESS_ENUM_TO_STREAM(Appearance::None
);
49 AC_PROCESS_ENUM_TO_STREAM(Appearance::Normal
);
50 AC_PROCESS_ENUM_TO_STREAM(Appearance::NormalNotShown
);
51 AC_PROCESS_ENUM_TO_STREAM(Appearance::Left
);
52 AC_PROCESS_ENUM_TO_STREAM(Appearance::Right
);
57 std::ostream
& operator<<(
58 std::ostream
& aStream
,
59 const AccessibleCaret::PositionChangedResult
& aResult
) {
60 using PositionChangedResult
= AccessibleCaret::PositionChangedResult
;
62 AC_PROCESS_ENUM_TO_STREAM(PositionChangedResult::NotChanged
);
63 AC_PROCESS_ENUM_TO_STREAM(PositionChangedResult::Position
);
64 AC_PROCESS_ENUM_TO_STREAM(PositionChangedResult::Zoom
);
65 AC_PROCESS_ENUM_TO_STREAM(PositionChangedResult::Invisible
);
69 #undef AC_PROCESS_ENUM_TO_STREAM
71 // -----------------------------------------------------------------------------
72 // Implementation of AccessibleCaret methods
74 AccessibleCaret::AccessibleCaret(PresShell
* aPresShell
)
75 : mPresShell(aPresShell
) {
76 // Check all resources required.
78 MOZ_ASSERT(RootFrame());
79 MOZ_ASSERT(mPresShell
->GetDocument());
80 InjectCaretElement(mPresShell
->GetDocument());
84 AccessibleCaret::~AccessibleCaret() {
86 RemoveCaretElement(mPresShell
->GetDocument());
90 void AccessibleCaret::SetAppearance(Appearance aAppearance
) {
91 if (mAppearance
== aAppearance
) {
96 CaretElement().ClassList()->Remove(AppearanceString(mAppearance
), rv
);
97 MOZ_ASSERT(!rv
.Failed(), "Remove old appearance failed!");
99 CaretElement().ClassList()->Add(AppearanceString(aAppearance
), rv
);
100 MOZ_ASSERT(!rv
.Failed(), "Add new appearance failed!");
102 AC_LOG("%s: %s -> %s", __FUNCTION__
, ToString(mAppearance
).c_str(),
103 ToString(aAppearance
).c_str());
105 mAppearance
= aAppearance
;
107 // Need to reset rect since the cached rect will be compared in SetPosition.
108 if (mAppearance
== Appearance::None
) {
114 nsAutoString
AccessibleCaret::AppearanceString(Appearance aAppearance
) {
116 switch (aAppearance
) {
117 case Appearance::None
:
120 case Appearance::NormalNotShown
:
121 string
= u
"hidden"_ns
;
123 case Appearance::Normal
:
124 string
= u
"normal"_ns
;
126 case Appearance::Right
:
127 string
= u
"right"_ns
;
129 case Appearance::Left
:
136 bool AccessibleCaret::Intersects(const AccessibleCaret
& aCaret
) const {
137 MOZ_ASSERT(mPresShell
== aCaret
.mPresShell
);
139 if (!IsVisuallyVisible() || !aCaret
.IsVisuallyVisible()) {
144 nsLayoutUtils::GetRectRelativeToFrame(&CaretElement(), RootFrame());
145 nsRect rhsRect
= nsLayoutUtils::GetRectRelativeToFrame(&aCaret
.CaretElement(),
147 return rect
.Intersects(rhsRect
);
150 bool AccessibleCaret::Contains(const nsPoint
& aPoint
,
151 TouchArea aTouchArea
) const {
152 if (!IsVisuallyVisible()) {
156 nsRect textOverlayRect
=
157 nsLayoutUtils::GetRectRelativeToFrame(TextOverlayElement(), RootFrame());
158 nsRect caretImageRect
=
159 nsLayoutUtils::GetRectRelativeToFrame(CaretImageElement(), RootFrame());
161 if (aTouchArea
== TouchArea::CaretImage
) {
162 return caretImageRect
.Contains(aPoint
);
165 MOZ_ASSERT(aTouchArea
== TouchArea::Full
, "Unexpected TouchArea type!");
166 return textOverlayRect
.Contains(aPoint
) || caretImageRect
.Contains(aPoint
);
169 void AccessibleCaret::EnsureApzAware() {
170 // If the caret element was cloned, the listener might have been lost. So
171 // if that's the case we register a dummy listener if there isn't one on
172 // the element already.
173 if (!CaretElement().IsApzAware()) {
174 // FIXME(emilio): Is this needed anymore?
175 CaretElement().AddEventListener(u
"touchstart"_ns
, mDummyTouchListener
,
180 bool AccessibleCaret::IsInPositionFixedSubtree() const {
181 return nsLayoutUtils::IsInPositionFixedSubtree(
182 mImaginaryCaretReferenceFrame
.GetFrame());
185 void AccessibleCaret::InjectCaretElement(Document
* aDocument
) {
186 IgnoredErrorResult rv
;
187 RefPtr
<Element
> element
= CreateCaretElement(aDocument
);
188 mCaretElementHolder
=
189 aDocument
->InsertAnonymousContent(*element
, /* aForce = */ false, rv
);
191 MOZ_ASSERT(!rv
.Failed(), "Insert anonymous content should not fail!");
192 MOZ_ASSERT(mCaretElementHolder
, "We must have anonymous content!");
194 // InsertAnonymousContent will clone the element to make an AnonymousContent.
195 // Since event listeners are not being cloned when cloning a node, we need to
196 // add the listener here.
200 already_AddRefed
<Element
> AccessibleCaret::CreateCaretElement(
201 Document
* aDocument
) const {
202 // Content structure of AccessibleCaret
203 // <div class="moz-accessiblecaret"> <- CaretElement()
204 // <div id="text-overlay"> <- TextOverlayElement()
205 // <div id="image"> <- CaretImageElement()
208 RefPtr
<Element
> parent
= aDocument
->CreateHTMLElement(nsGkAtoms::div
);
209 parent
->ClassList()->Add(u
"moz-accessiblecaret"_ns
, rv
);
210 parent
->ClassList()->Add(u
"none"_ns
, rv
);
212 auto CreateAndAppendChildElement
=
213 [aDocument
, &parent
](const nsLiteralString
& aElementId
) {
214 RefPtr
<Element
> child
= aDocument
->CreateHTMLElement(nsGkAtoms::div
);
215 child
->SetAttr(kNameSpaceID_None
, nsGkAtoms::id
, aElementId
, true);
216 parent
->AppendChildTo(child
, false, IgnoreErrors());
219 CreateAndAppendChildElement(sTextOverlayElementId
);
220 CreateAndAppendChildElement(sCaretImageElementId
);
222 return parent
.forget();
225 void AccessibleCaret::RemoveCaretElement(Document
* aDocument
) {
226 CaretElement().RemoveEventListener(u
"touchstart"_ns
, mDummyTouchListener
,
229 aDocument
->RemoveAnonymousContent(*mCaretElementHolder
, IgnoreErrors());
232 void AccessibleCaret::ClearCachedData() {
233 mImaginaryCaretRect
= nsRect();
234 mImaginaryCaretRectInContainerFrame
= nsRect();
235 mImaginaryCaretReferenceFrame
= nullptr;
239 AccessibleCaret::PositionChangedResult
AccessibleCaret::SetPosition(
240 nsIFrame
* aFrame
, int32_t aOffset
) {
241 if (!CustomContentContainerFrame()) {
242 return PositionChangedResult::NotChanged
;
245 nsRect imaginaryCaretRectInFrame
=
246 nsCaret::GetGeometryForFrame(aFrame
, aOffset
, nullptr);
248 imaginaryCaretRectInFrame
=
249 nsLayoutUtils::ClampRectToScrollFrames(aFrame
, imaginaryCaretRectInFrame
);
251 if (imaginaryCaretRectInFrame
.IsEmpty()) {
252 // Don't bother to set the caret position since it's invisible.
254 return PositionChangedResult::Invisible
;
257 // SetCaretElementStyle() requires the input rect relative to the custom
258 // content container frame.
259 nsRect imaginaryCaretRectInContainerFrame
= imaginaryCaretRectInFrame
;
260 nsLayoutUtils::TransformRect(aFrame
, CustomContentContainerFrame(),
261 imaginaryCaretRectInContainerFrame
);
262 const float zoomLevel
= GetZoomLevel();
263 const bool isSamePosition
= imaginaryCaretRectInContainerFrame
.IsEqualEdges(
264 mImaginaryCaretRectInContainerFrame
);
265 const bool isSameZoomLevel
= FuzzyEqualsMultiplicative(zoomLevel
, mZoomLevel
);
267 // Always update cached mImaginaryCaretRect (relative to the root frame)
268 // because it can change when the caret is scrolled.
269 mImaginaryCaretRect
= imaginaryCaretRectInFrame
;
270 nsLayoutUtils::TransformRect(aFrame
, RootFrame(), mImaginaryCaretRect
);
272 if (isSamePosition
&& isSameZoomLevel
) {
273 return PositionChangedResult::NotChanged
;
276 mImaginaryCaretRectInContainerFrame
= imaginaryCaretRectInContainerFrame
;
277 mImaginaryCaretReferenceFrame
= aFrame
;
278 mZoomLevel
= zoomLevel
;
280 SetCaretElementStyle(imaginaryCaretRectInContainerFrame
, mZoomLevel
);
282 return isSamePosition
? PositionChangedResult::Zoom
283 : PositionChangedResult::Position
;
286 nsIFrame
* AccessibleCaret::RootFrame() const {
287 return mPresShell
->GetRootFrame();
290 nsIFrame
* AccessibleCaret::CustomContentContainerFrame() const {
291 nsCanvasFrame
* canvasFrame
= mPresShell
->GetCanvasFrame();
292 Element
* container
= canvasFrame
->GetCustomContentContainer();
293 nsIFrame
* containerFrame
= container
->GetPrimaryFrame();
294 return containerFrame
;
297 void AccessibleCaret::SetCaretElementStyle(const nsRect
& aRect
,
299 nsPoint position
= CaretElementPosition(aRect
);
300 nsAutoString styleStr
;
301 // We can't use AppendPrintf here, because it does locale-specific
302 // formatting of floating-point values.
303 styleStr
.AppendLiteral("left: ");
304 styleStr
.AppendFloat(nsPresContext::AppUnitsToFloatCSSPixels(position
.x
));
305 styleStr
.AppendLiteral("px; top: ");
306 styleStr
.AppendFloat(nsPresContext::AppUnitsToFloatCSSPixels(position
.y
));
307 styleStr
.AppendLiteral("px; width: ");
308 styleStr
.AppendFloat(StaticPrefs::layout_accessiblecaret_width() /
310 styleStr
.AppendLiteral("px; margin-left: ");
311 styleStr
.AppendFloat(StaticPrefs::layout_accessiblecaret_margin_left() /
313 styleStr
.AppendLiteral("px; transition-duration: ");
314 styleStr
.AppendFloat(
315 StaticPrefs::layout_accessiblecaret_transition_duration());
316 styleStr
.AppendLiteral("ms");
318 CaretElement().SetAttr(kNameSpaceID_None
, nsGkAtoms::style
, styleStr
, true);
319 AC_LOG("%s: %s", __FUNCTION__
, NS_ConvertUTF16toUTF8(styleStr
).get());
321 // Set style string for children.
322 SetTextOverlayElementStyle(aRect
, aZoomLevel
);
323 SetCaretImageElementStyle(aRect
, aZoomLevel
);
326 void AccessibleCaret::SetTextOverlayElementStyle(const nsRect
& aRect
,
328 nsAutoString styleStr
;
329 styleStr
.AppendLiteral("height: ");
330 styleStr
.AppendFloat(nsPresContext::AppUnitsToFloatCSSPixels(aRect
.height
));
331 styleStr
.AppendLiteral("px;");
332 TextOverlayElement()->SetAttr(kNameSpaceID_None
, nsGkAtoms::style
, styleStr
,
334 AC_LOG("%s: %s", __FUNCTION__
, NS_ConvertUTF16toUTF8(styleStr
).get());
337 void AccessibleCaret::SetCaretImageElementStyle(const nsRect
& aRect
,
339 nsAutoString styleStr
;
340 styleStr
.AppendLiteral("height: ");
341 styleStr
.AppendFloat(StaticPrefs::layout_accessiblecaret_height() /
343 styleStr
.AppendLiteral("px;");
344 CaretImageElement()->SetAttr(kNameSpaceID_None
, nsGkAtoms::style
, styleStr
,
346 AC_LOG("%s: %s", __FUNCTION__
, NS_ConvertUTF16toUTF8(styleStr
).get());
349 float AccessibleCaret::GetZoomLevel() {
350 // Full zoom on desktop.
351 float fullZoom
= mPresShell
->GetPresContext()->GetFullZoom();
353 // Pinch-zoom on fennec.
354 float resolution
= mPresShell
->GetCumulativeResolution();
356 return fullZoom
* resolution
;
359 } // namespace mozilla