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/ToString.h"
14 #include "nsCanvasFrame.h"
16 #include "nsCSSFrameConstructor.h"
17 #include "nsDOMTokenList.h"
19 #include "nsLayoutUtils.h"
20 #include "nsPlaceholderFrame.h"
26 #define AC_LOG(message, ...) \
27 AC_LOG_BASE("AccessibleCaret (%p): " message, this, ##__VA_ARGS__);
30 #define AC_LOGV(message, ...) \
31 AC_LOGV_BASE("AccessibleCaret (%p): " message, this, ##__VA_ARGS__);
33 NS_IMPL_ISUPPORTS(AccessibleCaret::DummyTouchListener
, nsIDOMEventListener
)
35 const nsLiteralString
AccessibleCaret::sTextOverlayElementId
=
37 const nsLiteralString
AccessibleCaret::sCaretImageElementId
= u
"image"_ns
;
39 #define AC_PROCESS_ENUM_TO_STREAM(e) \
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
);
56 std::ostream
& operator<<(
57 std::ostream
& aStream
,
58 const AccessibleCaret::PositionChangedResult
& aResult
) {
59 using PositionChangedResult
= AccessibleCaret::PositionChangedResult
;
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
);
68 #undef AC_PROCESS_ENUM_TO_STREAM
70 // -----------------------------------------------------------------------------
71 // Implementation of AccessibleCaret methods
73 AccessibleCaret::AccessibleCaret(PresShell
* aPresShell
)
74 : mPresShell(aPresShell
) {
75 // Check all resources required.
77 MOZ_ASSERT(RootFrame());
78 MOZ_ASSERT(mPresShell
->GetDocument());
79 InjectCaretElement(mPresShell
->GetDocument());
83 AccessibleCaret::~AccessibleCaret() {
85 RemoveCaretElement(mPresShell
->GetDocument());
89 void AccessibleCaret::SetAppearance(Appearance aAppearance
) {
90 if (mAppearance
== aAppearance
) {
95 CaretElement().ClassList()->Remove(AppearanceString(mAppearance
), rv
);
96 MOZ_ASSERT(!rv
.Failed(), "Remove old appearance failed!");
98 CaretElement().ClassList()->Add(AppearanceString(aAppearance
), rv
);
99 MOZ_ASSERT(!rv
.Failed(), "Add new appearance failed!");
101 AC_LOG("%s: %s -> %s", __FUNCTION__
, ToString(mAppearance
).c_str(),
102 ToString(aAppearance
).c_str());
104 mAppearance
= aAppearance
;
106 // Need to reset rect since the cached rect will be compared in SetPosition.
107 if (mAppearance
== Appearance::None
) {
113 nsAutoString
AccessibleCaret::AppearanceString(Appearance aAppearance
) {
115 switch (aAppearance
) {
116 case Appearance::None
:
119 case Appearance::NormalNotShown
:
120 string
= u
"hidden"_ns
;
122 case Appearance::Normal
:
123 string
= u
"normal"_ns
;
125 case Appearance::Right
:
126 string
= u
"right"_ns
;
128 case Appearance::Left
:
135 bool AccessibleCaret::Intersects(const AccessibleCaret
& aCaret
) const {
136 MOZ_ASSERT(mPresShell
== aCaret
.mPresShell
);
138 if (!IsVisuallyVisible() || !aCaret
.IsVisuallyVisible()) {
143 nsLayoutUtils::GetRectRelativeToFrame(&CaretElement(), RootFrame());
144 nsRect rhsRect
= nsLayoutUtils::GetRectRelativeToFrame(&aCaret
.CaretElement(),
146 return rect
.Intersects(rhsRect
);
149 bool AccessibleCaret::Contains(const nsPoint
& aPoint
,
150 TouchArea aTouchArea
) const {
151 if (!IsVisuallyVisible()) {
155 nsRect textOverlayRect
=
156 nsLayoutUtils::GetRectRelativeToFrame(TextOverlayElement(), RootFrame());
157 nsRect caretImageRect
=
158 nsLayoutUtils::GetRectRelativeToFrame(CaretImageElement(), RootFrame());
160 if (aTouchArea
== TouchArea::CaretImage
) {
161 return caretImageRect
.Contains(aPoint
);
164 MOZ_ASSERT(aTouchArea
== TouchArea::Full
, "Unexpected TouchArea type!");
165 return textOverlayRect
.Contains(aPoint
) || caretImageRect
.Contains(aPoint
);
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
,
179 bool AccessibleCaret::IsInPositionFixedSubtree() const {
180 return nsLayoutUtils::IsInPositionFixedSubtree(
181 mImaginaryCaretReferenceFrame
.GetFrame());
184 void AccessibleCaret::InjectCaretElement(Document
* aDocument
) {
185 IgnoredErrorResult rv
;
186 RefPtr
<Element
> element
= CreateCaretElement(aDocument
);
187 mCaretElementHolder
=
188 aDocument
->InsertAnonymousContent(*element
, /* aForce = */ false, rv
);
190 MOZ_ASSERT(!rv
.Failed(), "Insert anonymous content should not fail!");
191 MOZ_ASSERT(mCaretElementHolder
, "We must have anonymous content!");
193 // InsertAnonymousContent will clone the element to make an AnonymousContent.
194 // Since event listeners are not being cloned when cloning a node, we need to
195 // add the listener here.
199 already_AddRefed
<Element
> AccessibleCaret::CreateCaretElement(
200 Document
* aDocument
) const {
201 // Content structure of AccessibleCaret
202 // <div class="moz-accessiblecaret"> <- CaretElement()
203 // <div id="text-overlay"> <- TextOverlayElement()
204 // <div id="image"> <- CaretImageElement()
207 RefPtr
<Element
> parent
= aDocument
->CreateHTMLElement(nsGkAtoms::div
);
208 parent
->ClassList()->Add(u
"moz-accessiblecaret"_ns
, rv
);
209 parent
->ClassList()->Add(u
"none"_ns
, rv
);
211 auto CreateAndAppendChildElement
=
212 [aDocument
, &parent
](const nsLiteralString
& aElementId
) {
213 RefPtr
<Element
> child
= aDocument
->CreateHTMLElement(nsGkAtoms::div
);
214 child
->SetAttr(kNameSpaceID_None
, nsGkAtoms::id
, aElementId
, true);
215 parent
->AppendChildTo(child
, false, IgnoreErrors());
218 CreateAndAppendChildElement(sTextOverlayElementId
);
219 CreateAndAppendChildElement(sCaretImageElementId
);
221 return parent
.forget();
224 void AccessibleCaret::RemoveCaretElement(Document
* aDocument
) {
225 CaretElement().RemoveEventListener(u
"touchstart"_ns
, mDummyTouchListener
,
228 aDocument
->RemoveAnonymousContent(*mCaretElementHolder
, IgnoreErrors());
231 void AccessibleCaret::ClearCachedData() {
232 mImaginaryCaretRect
= nsRect();
233 mImaginaryCaretRectInContainerFrame
= nsRect();
234 mImaginaryCaretReferenceFrame
= nullptr;
238 AccessibleCaret::PositionChangedResult
AccessibleCaret::SetPosition(
239 nsIFrame
* aFrame
, int32_t aOffset
) {
240 if (!CustomContentContainerFrame()) {
241 return PositionChangedResult::NotChanged
;
244 nsRect imaginaryCaretRectInFrame
=
245 nsCaret::GetGeometryForFrame(aFrame
, aOffset
, nullptr);
247 imaginaryCaretRectInFrame
=
248 nsLayoutUtils::ClampRectToScrollFrames(aFrame
, imaginaryCaretRectInFrame
);
250 if (imaginaryCaretRectInFrame
.IsEmpty()) {
251 // Don't bother to set the caret position since it's invisible.
253 return PositionChangedResult::Invisible
;
256 // SetCaretElementStyle() requires the input rect relative to the custom
257 // content container frame.
258 nsRect imaginaryCaretRectInContainerFrame
= imaginaryCaretRectInFrame
;
259 nsLayoutUtils::TransformRect(aFrame
, CustomContentContainerFrame(),
260 imaginaryCaretRectInContainerFrame
);
261 const float zoomLevel
= GetZoomLevel();
262 const bool isSamePosition
= imaginaryCaretRectInContainerFrame
.IsEqualEdges(
263 mImaginaryCaretRectInContainerFrame
);
264 const bool isSameZoomLevel
= FuzzyEqualsMultiplicative(zoomLevel
, mZoomLevel
);
266 // Always update cached mImaginaryCaretRect (relative to the root frame)
267 // because it can change when the caret is scrolled.
268 mImaginaryCaretRect
= imaginaryCaretRectInFrame
;
269 nsLayoutUtils::TransformRect(aFrame
, RootFrame(), mImaginaryCaretRect
);
271 if (isSamePosition
&& isSameZoomLevel
) {
272 return PositionChangedResult::NotChanged
;
275 mImaginaryCaretRectInContainerFrame
= imaginaryCaretRectInContainerFrame
;
276 mImaginaryCaretReferenceFrame
= aFrame
;
277 mZoomLevel
= zoomLevel
;
279 SetCaretElementStyle(imaginaryCaretRectInContainerFrame
, mZoomLevel
);
281 return isSamePosition
? PositionChangedResult::Zoom
282 : PositionChangedResult::Position
;
285 nsIFrame
* AccessibleCaret::RootFrame() const {
286 return mPresShell
->GetRootFrame();
289 nsIFrame
* AccessibleCaret::CustomContentContainerFrame() const {
290 nsCanvasFrame
* canvasFrame
= mPresShell
->GetCanvasFrame();
291 Element
* container
= canvasFrame
->GetCustomContentContainer();
292 nsIFrame
* containerFrame
= container
->GetPrimaryFrame();
293 return containerFrame
;
296 void AccessibleCaret::SetCaretElementStyle(const nsRect
& aRect
,
298 nsPoint position
= CaretElementPosition(aRect
);
299 nsAutoString styleStr
;
300 // We can't use AppendPrintf here, because it does locale-specific
301 // formatting of floating-point values.
302 styleStr
.AppendLiteral("left: ");
303 styleStr
.AppendFloat(nsPresContext::AppUnitsToFloatCSSPixels(position
.x
));
304 styleStr
.AppendLiteral("px; top: ");
305 styleStr
.AppendFloat(nsPresContext::AppUnitsToFloatCSSPixels(position
.y
));
306 styleStr
.AppendLiteral("px; width: ");
307 styleStr
.AppendFloat(StaticPrefs::layout_accessiblecaret_width() /
309 styleStr
.AppendLiteral("px; margin-left: ");
310 styleStr
.AppendFloat(StaticPrefs::layout_accessiblecaret_margin_left() /
312 styleStr
.AppendLiteral("px; transition-duration: ");
313 styleStr
.AppendFloat(
314 StaticPrefs::layout_accessiblecaret_transition_duration());
315 styleStr
.AppendLiteral("ms");
317 CaretElement().SetAttr(kNameSpaceID_None
, nsGkAtoms::style
, styleStr
, true);
318 AC_LOG("%s: %s", __FUNCTION__
, NS_ConvertUTF16toUTF8(styleStr
).get());
320 // Set style string for children.
321 SetTextOverlayElementStyle(aRect
, aZoomLevel
);
322 SetCaretImageElementStyle(aRect
, aZoomLevel
);
325 void AccessibleCaret::SetTextOverlayElementStyle(const nsRect
& aRect
,
327 nsAutoString styleStr
;
328 styleStr
.AppendLiteral("height: ");
329 styleStr
.AppendFloat(nsPresContext::AppUnitsToFloatCSSPixels(aRect
.height
));
330 styleStr
.AppendLiteral("px;");
331 TextOverlayElement()->SetAttr(kNameSpaceID_None
, nsGkAtoms::style
, styleStr
,
333 AC_LOG("%s: %s", __FUNCTION__
, NS_ConvertUTF16toUTF8(styleStr
).get());
336 void AccessibleCaret::SetCaretImageElementStyle(const nsRect
& aRect
,
338 nsAutoString styleStr
;
339 styleStr
.AppendLiteral("height: ");
340 styleStr
.AppendFloat(StaticPrefs::layout_accessiblecaret_height() /
342 styleStr
.AppendLiteral("px;");
343 CaretImageElement()->SetAttr(kNameSpaceID_None
, nsGkAtoms::style
, styleStr
,
345 AC_LOG("%s: %s", __FUNCTION__
, NS_ConvertUTF16toUTF8(styleStr
).get());
348 float AccessibleCaret::GetZoomLevel() {
349 // Full zoom on desktop.
350 float fullZoom
= mPresShell
->GetPresContext()->GetFullZoom();
352 // Pinch-zoom on fennec.
353 float resolution
= mPresShell
->GetCumulativeResolution();
355 return fullZoom
* resolution
;
358 } // namespace mozilla