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/Assertions.h"
11 #include "mozilla/ErrorResult.h"
12 #include "mozilla/FloatingPoint.h"
13 #include "mozilla/PresShell.h"
14 #include "mozilla/StaticPrefs_layout.h"
15 #include "mozilla/dom/Document.h"
16 #include "mozilla/dom/ShadowRoot.h"
17 #include "mozilla/ToString.h"
18 #include "nsCanvasFrame.h"
20 #include "nsCSSFrameConstructor.h"
21 #include "nsDOMTokenList.h"
22 #include "nsGenericHTMLElement.h"
24 #include "nsLayoutUtils.h"
25 #include "nsPlaceholderFrame.h"
31 #define AC_LOG(message, ...) \
32 AC_LOG_BASE("AccessibleCaret (%p): " message, this, ##__VA_ARGS__);
35 #define AC_LOGV(message, ...) \
36 AC_LOGV_BASE("AccessibleCaret (%p): " message, this, ##__VA_ARGS__);
38 NS_IMPL_ISUPPORTS(AccessibleCaret::DummyTouchListener
, nsIDOMEventListener
)
40 static constexpr auto kTextOverlayElementId
= u
"text-overlay"_ns
;
41 static constexpr auto kCaretImageElementId
= u
"image"_ns
;
43 #define AC_PROCESS_ENUM_TO_STREAM(e) \
47 std::ostream
& operator<<(std::ostream
& aStream
,
48 const AccessibleCaret::Appearance
& aAppearance
) {
49 using Appearance
= AccessibleCaret::Appearance
;
50 switch (aAppearance
) {
51 AC_PROCESS_ENUM_TO_STREAM(Appearance::None
);
52 AC_PROCESS_ENUM_TO_STREAM(Appearance::Normal
);
53 AC_PROCESS_ENUM_TO_STREAM(Appearance::NormalNotShown
);
54 AC_PROCESS_ENUM_TO_STREAM(Appearance::Left
);
55 AC_PROCESS_ENUM_TO_STREAM(Appearance::Right
);
60 std::ostream
& operator<<(
61 std::ostream
& aStream
,
62 const AccessibleCaret::PositionChangedResult
& aResult
) {
63 using PositionChangedResult
= AccessibleCaret::PositionChangedResult
;
65 AC_PROCESS_ENUM_TO_STREAM(PositionChangedResult::NotChanged
);
66 AC_PROCESS_ENUM_TO_STREAM(PositionChangedResult::Position
);
67 AC_PROCESS_ENUM_TO_STREAM(PositionChangedResult::Zoom
);
68 AC_PROCESS_ENUM_TO_STREAM(PositionChangedResult::Invisible
);
72 #undef AC_PROCESS_ENUM_TO_STREAM
74 // -----------------------------------------------------------------------------
75 // Implementation of AccessibleCaret methods
77 AccessibleCaret::AccessibleCaret(PresShell
* aPresShell
)
78 : mPresShell(aPresShell
) {
79 // Check all resources required.
81 MOZ_ASSERT(mPresShell
->GetDocument());
82 InjectCaretElement(mPresShell
->GetDocument());
86 AccessibleCaret::~AccessibleCaret() {
88 RemoveCaretElement(mPresShell
->GetDocument());
92 dom::Element
* AccessibleCaret::TextOverlayElement() const {
93 return mCaretElementHolder
->Root()->GetElementById(kTextOverlayElementId
);
96 dom::Element
* AccessibleCaret::CaretImageElement() const {
97 return mCaretElementHolder
->Root()->GetElementById(kCaretImageElementId
);
100 void AccessibleCaret::SetAppearance(Appearance aAppearance
) {
101 if (mAppearance
== aAppearance
) {
105 IgnoredErrorResult rv
;
106 CaretElement().ClassList()->Remove(AppearanceString(mAppearance
), rv
);
107 MOZ_ASSERT(!rv
.Failed(), "Remove old appearance failed!");
109 CaretElement().ClassList()->Add(AppearanceString(aAppearance
), rv
);
110 MOZ_ASSERT(!rv
.Failed(), "Add new appearance failed!");
112 AC_LOG("%s: %s -> %s", __FUNCTION__
, ToString(mAppearance
).c_str(),
113 ToString(aAppearance
).c_str());
115 mAppearance
= aAppearance
;
117 // Need to reset rect since the cached rect will be compared in SetPosition.
118 if (mAppearance
== Appearance::None
) {
124 nsAutoString
AccessibleCaret::AppearanceString(Appearance aAppearance
) {
126 switch (aAppearance
) {
127 case Appearance::None
:
130 case Appearance::NormalNotShown
:
131 string
= u
"hidden"_ns
;
133 case Appearance::Normal
:
134 string
= u
"normal"_ns
;
136 case Appearance::Right
:
137 string
= u
"right"_ns
;
139 case Appearance::Left
:
146 bool AccessibleCaret::Intersects(const AccessibleCaret
& aCaret
) const {
147 MOZ_ASSERT(mPresShell
== aCaret
.mPresShell
);
149 if (!IsVisuallyVisible() || !aCaret
.IsVisuallyVisible()) {
154 nsLayoutUtils::GetRectRelativeToFrame(&CaretElement(), RootFrame());
155 nsRect rhsRect
= nsLayoutUtils::GetRectRelativeToFrame(&aCaret
.CaretElement(),
157 return rect
.Intersects(rhsRect
);
160 bool AccessibleCaret::Contains(const nsPoint
& aPoint
,
161 TouchArea aTouchArea
) const {
162 if (!IsVisuallyVisible()) {
166 nsRect textOverlayRect
=
167 nsLayoutUtils::GetRectRelativeToFrame(TextOverlayElement(), RootFrame());
168 nsRect caretImageRect
=
169 nsLayoutUtils::GetRectRelativeToFrame(CaretImageElement(), RootFrame());
171 if (aTouchArea
== TouchArea::CaretImage
) {
172 return caretImageRect
.Contains(aPoint
);
175 MOZ_ASSERT(aTouchArea
== TouchArea::Full
, "Unexpected TouchArea type!");
176 return textOverlayRect
.Contains(aPoint
) || caretImageRect
.Contains(aPoint
);
179 void AccessibleCaret::EnsureApzAware() {
180 // If the caret element was cloned, the listener might have been lost. So
181 // if that's the case we register a dummy listener if there isn't one on
182 // the element already.
183 if (!CaretElement().IsApzAware()) {
184 CaretElement().AddEventListener(u
"touchstart"_ns
, mDummyTouchListener
,
189 bool AccessibleCaret::IsInPositionFixedSubtree() const {
190 return nsLayoutUtils::IsInPositionFixedSubtree(
191 mImaginaryCaretReferenceFrame
.GetFrame());
194 void AccessibleCaret::InjectCaretElement(Document
* aDocument
) {
195 mCaretElementHolder
=
196 aDocument
->InsertAnonymousContent(/* aForce = */ false, IgnoreErrors());
197 MOZ_RELEASE_ASSERT(mCaretElementHolder
, "We must have anonymous content!");
199 CreateCaretElement();
203 void AccessibleCaret::CreateCaretElement() const {
204 // Content structure of AccessibleCaret
205 // <div class="moz-accessiblecaret"> <- CaretElement()
207 // <link rel="stylesheet" href="accessiblecaret.css">
208 // <div id="text-overlay"> <- TextOverlayElement()
209 // <div id="image"> <- CaretImageElement()
211 constexpr bool kNotify
= false;
213 Element
& host
= CaretElement();
214 host
.SetAttr(kNameSpaceID_None
, nsGkAtoms::_class
,
215 u
"moz-accessiblecaret none"_ns
, kNotify
);
217 ShadowRoot
* root
= mCaretElementHolder
->Root();
218 Document
* doc
= host
.OwnerDoc();
220 RefPtr
<NodeInfo
> linkNodeInfo
= doc
->NodeInfoManager()->GetNodeInfo(
221 nsGkAtoms::link
, nullptr, kNameSpaceID_XHTML
, nsINode::ELEMENT_NODE
);
222 RefPtr
<nsGenericHTMLElement
> link
=
223 NS_NewHTMLLinkElement(linkNodeInfo
.forget());
224 if (NS_WARN_IF(!link
)) {
227 link
->SetAttr(nsGkAtoms::rel
, u
"stylesheet"_ns
, IgnoreErrors());
228 link
->SetAttr(nsGkAtoms::href
,
229 u
"resource://content-accessible/accessiblecaret.css"_ns
,
231 root
->AppendChildTo(link
, kNotify
, IgnoreErrors());
234 auto CreateAndAppendChildElement
= [&](const nsLiteralString
& aElementId
) {
235 RefPtr
<Element
> child
= doc
->CreateHTMLElement(nsGkAtoms::div
);
236 child
->SetAttr(kNameSpaceID_None
, nsGkAtoms::id
, aElementId
, kNotify
);
237 mCaretElementHolder
->Root()->AppendChildTo(child
, kNotify
, IgnoreErrors());
240 CreateAndAppendChildElement(kTextOverlayElementId
);
241 CreateAndAppendChildElement(kCaretImageElementId
);
244 void AccessibleCaret::RemoveCaretElement(Document
* aDocument
) {
245 CaretElement().RemoveEventListener(u
"touchstart"_ns
, mDummyTouchListener
,
248 aDocument
->RemoveAnonymousContent(*mCaretElementHolder
);
251 void AccessibleCaret::ClearCachedData() {
252 mImaginaryCaretRect
= nsRect();
253 mImaginaryCaretRectInContainerFrame
= nsRect();
254 mImaginaryCaretReferenceFrame
= nullptr;
258 AccessibleCaret::PositionChangedResult
AccessibleCaret::SetPosition(
259 nsIFrame
* aFrame
, int32_t aOffset
) {
260 if (!CustomContentContainerFrame()) {
261 return PositionChangedResult::NotChanged
;
264 nsRect imaginaryCaretRectInFrame
=
265 nsCaret::GetGeometryForFrame(aFrame
, aOffset
, nullptr);
267 imaginaryCaretRectInFrame
=
268 nsLayoutUtils::ClampRectToScrollFrames(aFrame
, imaginaryCaretRectInFrame
);
270 if (imaginaryCaretRectInFrame
.IsEmpty()) {
271 // Don't bother to set the caret position since it's invisible.
273 return PositionChangedResult::Invisible
;
276 // SetCaretElementStyle() requires the input rect relative to the custom
277 // content container frame.
278 nsRect imaginaryCaretRectInContainerFrame
= imaginaryCaretRectInFrame
;
279 nsLayoutUtils::TransformRect(aFrame
, CustomContentContainerFrame(),
280 imaginaryCaretRectInContainerFrame
);
281 const float zoomLevel
= GetZoomLevel();
282 const bool isSamePosition
= imaginaryCaretRectInContainerFrame
.IsEqualEdges(
283 mImaginaryCaretRectInContainerFrame
);
284 const bool isSameZoomLevel
= FuzzyEqualsMultiplicative(zoomLevel
, mZoomLevel
);
286 // Always update cached mImaginaryCaretRect (relative to the root frame)
287 // because it can change when the caret is scrolled.
288 mImaginaryCaretRect
= imaginaryCaretRectInFrame
;
289 nsLayoutUtils::TransformRect(aFrame
, RootFrame(), mImaginaryCaretRect
);
291 if (isSamePosition
&& isSameZoomLevel
) {
292 return PositionChangedResult::NotChanged
;
295 mImaginaryCaretRectInContainerFrame
= imaginaryCaretRectInContainerFrame
;
296 mImaginaryCaretReferenceFrame
= aFrame
;
297 mZoomLevel
= zoomLevel
;
299 SetCaretElementStyle(imaginaryCaretRectInContainerFrame
, mZoomLevel
);
301 return isSamePosition
? PositionChangedResult::Zoom
302 : PositionChangedResult::Position
;
305 nsIFrame
* AccessibleCaret::RootFrame() const {
306 return mPresShell
->GetRootFrame();
309 nsIFrame
* AccessibleCaret::CustomContentContainerFrame() const {
310 nsCanvasFrame
* canvasFrame
= mPresShell
->GetCanvasFrame();
311 Element
* container
= canvasFrame
->GetCustomContentContainer();
312 nsIFrame
* containerFrame
= container
->GetPrimaryFrame();
313 return containerFrame
;
316 void AccessibleCaret::SetCaretElementStyle(const nsRect
& aRect
,
318 nsPoint position
= CaretElementPosition(aRect
);
319 nsAutoString styleStr
;
320 // We can't use AppendPrintf here, because it does locale-specific
321 // formatting of floating-point values.
322 styleStr
.AppendLiteral("left: ");
323 styleStr
.AppendFloat(nsPresContext::AppUnitsToFloatCSSPixels(position
.x
));
324 styleStr
.AppendLiteral("px; top: ");
325 styleStr
.AppendFloat(nsPresContext::AppUnitsToFloatCSSPixels(position
.y
));
326 styleStr
.AppendLiteral("px; width: ");
327 styleStr
.AppendFloat(StaticPrefs::layout_accessiblecaret_width() /
329 styleStr
.AppendLiteral("px; margin-left: ");
330 styleStr
.AppendFloat(StaticPrefs::layout_accessiblecaret_margin_left() /
332 styleStr
.AppendLiteral("px; transition-duration: ");
333 styleStr
.AppendFloat(
334 StaticPrefs::layout_accessiblecaret_transition_duration());
335 styleStr
.AppendLiteral("ms");
337 CaretElement().SetAttr(kNameSpaceID_None
, nsGkAtoms::style
, styleStr
, true);
338 AC_LOG("%s: %s", __FUNCTION__
, NS_ConvertUTF16toUTF8(styleStr
).get());
340 // Set style string for children.
341 SetTextOverlayElementStyle(aRect
, aZoomLevel
);
342 SetCaretImageElementStyle(aRect
, aZoomLevel
);
345 void AccessibleCaret::SetTextOverlayElementStyle(const nsRect
& aRect
,
347 nsAutoString styleStr
;
348 styleStr
.AppendLiteral("height: ");
349 styleStr
.AppendFloat(nsPresContext::AppUnitsToFloatCSSPixels(aRect
.height
));
350 styleStr
.AppendLiteral("px;");
351 TextOverlayElement()->SetAttr(kNameSpaceID_None
, nsGkAtoms::style
, styleStr
,
353 AC_LOG("%s: %s", __FUNCTION__
, NS_ConvertUTF16toUTF8(styleStr
).get());
356 void AccessibleCaret::SetCaretImageElementStyle(const nsRect
& aRect
,
358 nsAutoString styleStr
;
359 styleStr
.AppendLiteral("height: ");
360 styleStr
.AppendFloat(StaticPrefs::layout_accessiblecaret_height() /
362 styleStr
.AppendLiteral("px;");
363 CaretImageElement()->SetAttr(kNameSpaceID_None
, nsGkAtoms::style
, styleStr
,
365 AC_LOG("%s: %s", __FUNCTION__
, NS_ConvertUTF16toUTF8(styleStr
).get());
368 float AccessibleCaret::GetZoomLevel() {
369 // Full zoom on desktop.
370 float fullZoom
= mPresShell
->GetPresContext()->GetFullZoom();
372 // Pinch-zoom on fennec.
373 float resolution
= mPresShell
->GetCumulativeResolution();
375 return fullZoom
* resolution
;
378 } // namespace mozilla