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