ElementStateManager.cpp (14589B)
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 "ElementStateManager.h" 8 #include "mozilla/EventStateManager.h" 9 #include "mozilla/PresShell.h" 10 #include "mozilla/StaticPrefs_ui.h" 11 #include "mozilla/dom/Element.h" 12 #include "mozilla/dom/Document.h" 13 #include "mozilla/layers/APZEventState.h" 14 #include "mozilla/layers/APZUtils.h" 15 #include "nsITimer.h" 16 17 static mozilla::LazyLogModule sApzAemLog("apz.elementstate"); 18 #define ESM_LOG(...) MOZ_LOG(sApzAemLog, LogLevel::Debug, (__VA_ARGS__)) 19 20 namespace mozilla { 21 namespace layers { 22 23 class DelayedClearElementActivation final : public nsITimerCallback, 24 public nsINamed { 25 private: 26 explicit DelayedClearElementActivation(RefPtr<dom::Element>& aTarget, 27 const nsCOMPtr<nsITimer>& aTimer) 28 : mTarget(aTarget) 29 // Hold the reference count until we are called back. 30 , 31 mTimer(aTimer), 32 mProcessedSingleTap(false) {} 33 34 public: 35 NS_DECL_ISUPPORTS 36 37 static RefPtr<DelayedClearElementActivation> Create( 38 RefPtr<dom::Element>& aTarget); 39 40 NS_IMETHOD Notify(nsITimer*) override; 41 42 NS_IMETHOD GetName(nsACString& aName) override; 43 44 void MarkSingleTapProcessed(); 45 46 bool ProcessedSingleTap() const { return mProcessedSingleTap; } 47 48 void StartTimer(); 49 50 /** 51 * Clear the Event State Manager's global active content. 52 */ 53 void ClearGlobalActiveContent(); 54 55 void ClearTimer() { 56 if (mTimer) { 57 mTimer->Cancel(); 58 mTimer = nullptr; 59 } 60 } 61 dom::Element* GetTarget() const { return mTarget; } 62 63 private: 64 ~DelayedClearElementActivation() = default; 65 66 RefPtr<dom::Element> mTarget; 67 nsCOMPtr<nsITimer> mTimer; 68 bool mProcessedSingleTap; 69 }; 70 71 static nsPresContext* GetPresContextFor(nsIContent* aContent) { 72 if (!aContent) { 73 return nullptr; 74 } 75 PresShell* presShell = aContent->OwnerDoc()->GetPresShell(); 76 if (!presShell) { 77 return nullptr; 78 } 79 return presShell->GetPresContext(); 80 } 81 82 RefPtr<DelayedClearElementActivation> DelayedClearElementActivation::Create( 83 RefPtr<dom::Element>& aTarget) { 84 nsCOMPtr<nsITimer> timer = NS_NewTimer(); 85 if (!timer) { 86 return nullptr; 87 } 88 RefPtr<DelayedClearElementActivation> event = 89 new DelayedClearElementActivation(aTarget, timer); 90 return event; 91 } 92 93 NS_IMETHODIMP DelayedClearElementActivation::Notify(nsITimer*) { 94 ESM_LOG("DelayedClearElementActivation notification ready=%d", 95 mProcessedSingleTap); 96 // If the single tap has been processed and the timer has expired, 97 // clear the active element state. 98 if (mProcessedSingleTap) { 99 ESM_LOG("DelayedClearElementActivation clearing active content"); 100 ClearGlobalActiveContent(); 101 } 102 mTimer = nullptr; 103 return NS_OK; 104 } 105 106 NS_IMETHODIMP DelayedClearElementActivation::GetName(nsACString& aName) { 107 aName.AssignLiteral("DelayedClearElementActivation"); 108 return NS_OK; 109 } 110 111 void DelayedClearElementActivation::StartTimer() { 112 MOZ_ASSERT(mTimer); 113 // If the timer is null, active content state has already been cleared. 114 if (!mTimer) { 115 return; 116 } 117 nsresult rv = mTimer->InitWithCallback( 118 this, StaticPrefs::ui_touch_activation_duration_ms(), 119 nsITimer::TYPE_ONE_SHOT); 120 if (NS_FAILED(rv)) { 121 ClearTimer(); 122 } 123 } 124 125 void DelayedClearElementActivation::MarkSingleTapProcessed() { 126 mProcessedSingleTap = true; 127 if (!mTimer) { 128 ESM_LOG("Clear activation immediate!"); 129 ClearGlobalActiveContent(); 130 } 131 } 132 133 void DelayedClearElementActivation::ClearGlobalActiveContent() { 134 if (nsPresContext* pc = GetPresContextFor(mTarget)) { 135 EventStateManager::ClearGlobalActiveContent(pc->EventStateManager()); 136 } 137 mTarget = nullptr; 138 } 139 140 NS_IMPL_ISUPPORTS(DelayedClearElementActivation, nsITimerCallback, nsINamed) 141 142 ElementStateManager::ElementStateManager() 143 : mCanBePanOrZoom(false), 144 mCanBePanOrZoomSet(false), 145 mSingleTapBeforeActivation(false), 146 mSingleTapState(apz::SingleTapState::NotClick), 147 mSetActiveTask(nullptr), 148 mSetHoverTask(nullptr) {} 149 150 ElementStateManager::~ElementStateManager() = default; 151 152 void ElementStateManager::SetTargetElement( 153 dom::EventTarget* aTarget, PreventDefault aTouchStartPreventDefault) { 154 if (mTarget) { 155 // Multiple fingers on screen (since HandleTouchEnd clears mTarget). 156 ESM_LOG("Multiple fingers on-screen, clearing target element"); 157 CancelActiveTask(); 158 ResetActive(); 159 ResetTouchBlockState(); 160 return; 161 } 162 163 mTarget = dom::Element::FromEventTargetOrNull(aTarget); 164 ESM_LOG("Setting target element to %p", mTarget.get()); 165 TriggerElementActivation(); 166 if (mTarget && !bool(aTouchStartPreventDefault)) { 167 ScheduleSetHoverTask(); 168 } 169 } 170 171 void ElementStateManager::HandleTouchStart(bool aCanBePanOrZoom) { 172 ESM_LOG("Touch start, aCanBePanOrZoom: %d", aCanBePanOrZoom); 173 if (mCanBePanOrZoomSet) { 174 // Multiple fingers on screen (since HandleTouchEnd clears mCanBePanSet). 175 ESM_LOG("Multiple fingers on-screen, clearing touch block state"); 176 CancelActiveTask(); 177 ResetActive(); 178 ResetTouchBlockState(); 179 return; 180 } 181 182 mCanBePanOrZoom = aCanBePanOrZoom; 183 mCanBePanOrZoomSet = true; 184 TriggerElementActivation(); 185 } 186 187 void ElementStateManager::TriggerElementActivation() { 188 // Reset mSingleTapState here either when HandleTouchStart() or 189 // SetTargetElement() gets called. 190 // NOTE: It's possible that ProcessSingleTap() gets called in between 191 // HandleTouchStart() and SetTargetElement() calls. I.e., 192 // mSingleTapBeforeActivation is true, in such cases it doesn't matter that 193 // mSingleTapState was reset once and referred it in ProcessSingleTap() and 194 // then reset here again because in ProcessSingleTap() `NotYetDetermined` is 195 // the only one state we need to care, and it should NOT happen in the 196 // scenario. In other words the case where we need to care `NotYetDetermined` 197 // is when ProcessSingleTap() gets called later than any other events and 198 // notifications. 199 mSingleTapState = apz::SingleTapState::NotClick; 200 201 // Both HandleTouchStart() and SetTargetElement() call this. They can be 202 // called in either order. One will set mCanBePanOrZoomSet, and the other, 203 // mTarget. We want to actually trigger the activation once both are set. 204 if (!(mTarget && mCanBePanOrZoomSet)) { 205 return; 206 } 207 208 RefPtr<DelayedClearElementActivation> delayedEvent = 209 DelayedClearElementActivation::Create(mTarget); 210 if (mDelayedClearElementActivation) { 211 mDelayedClearElementActivation->ClearTimer(); 212 mDelayedClearElementActivation->ClearGlobalActiveContent(); 213 } 214 mDelayedClearElementActivation = delayedEvent; 215 216 // If the touch cannot be a pan, make mTarget :active right away. 217 // Otherwise, wait a bit to see if the user will pan or not. 218 if (!mCanBePanOrZoom) { 219 SetActive(mTarget); 220 221 if (mDelayedClearElementActivation) { 222 if (mSingleTapBeforeActivation) { 223 mDelayedClearElementActivation->MarkSingleTapProcessed(); 224 } 225 mDelayedClearElementActivation->StartTimer(); 226 } 227 } else { 228 CancelActiveTask(); // this is only needed because of bug 1169802. Fixing 229 // that bug properly should make this unnecessary. 230 ScheduleSetActiveTask(); 231 } 232 ESM_LOG( 233 "Got both touch-end event and end touch notiication, clearing pan " 234 "state"); 235 mCanBePanOrZoomSet = false; 236 } 237 238 void ElementStateManager::ClearActivation() { 239 ESM_LOG("Clearing element activation"); 240 CancelActiveTask(); 241 ResetActive(); 242 } 243 244 bool ElementStateManager::HandleTouchEndEvent(apz::SingleTapState aState) { 245 ESM_LOG("Touch end event, state: %hhu", static_cast<uint8_t>(aState)); 246 247 mTouchEndState += TouchEndState::GotTouchEndEvent; 248 return MaybeChangeActiveState(aState); 249 } 250 251 bool ElementStateManager::HandleTouchEnd(apz::SingleTapState aState) { 252 ESM_LOG("Touch end"); 253 254 mTouchEndState += TouchEndState::GotTouchEndNotification; 255 return MaybeChangeActiveState(aState); 256 } 257 258 bool ElementStateManager::MaybeChangeActiveState(apz::SingleTapState aState) { 259 if (mTouchEndState != 260 TouchEndStates(TouchEndState::GotTouchEndEvent, 261 TouchEndState::GotTouchEndNotification)) { 262 return false; 263 } 264 265 CancelActiveTask(); 266 267 mSingleTapState = aState; 268 269 if (aState == apz::SingleTapState::WasClick) { 270 // Scrollbar thumbs use a different mechanism for their active 271 // highlight (the "active" attribute), so don't set the active state 272 // on them because nothing will clear it. 273 if (mCanBePanOrZoom && 274 !(mTarget && mTarget->IsXULElement(nsGkAtoms::thumb))) { 275 SetActive(mTarget); 276 } 277 } else { 278 // We might reach here if mCanBePanOrZoom was false on touch-start and 279 // so we set the element active right away. Now it turns out the 280 // action was not a click so we need to reset the active element. 281 ResetActive(); 282 } 283 284 ResetTouchBlockState(); 285 return true; 286 } 287 288 void ElementStateManager::ProcessSingleTap() { 289 if (!mDelayedClearElementActivation) { 290 // We have not received touch-start notification yet. We will have to run 291 // MarkSingleTapProcessed() when we receive the touch-start notification. 292 mSingleTapBeforeActivation = true; 293 return; 294 } 295 296 if (mSingleTapState == apz::SingleTapState::NotYetDetermined) { 297 // If we got `NotYetDetermined`, which means at the moment we don't know for 298 // sure whether double-tapping will be incoming or not, but now we are sure 299 // that no double-tapping will happen, thus it's time to activate the target 300 // element. 301 if (auto* target = mDelayedClearElementActivation->GetTarget()) { 302 SetActive(target); 303 } 304 } 305 mDelayedClearElementActivation->MarkSingleTapProcessed(); 306 307 if (mCanBePanOrZoom) { 308 // In the case that we have not started the delayed reset of the element 309 // activation state, start the timer now. 310 mDelayedClearElementActivation->StartTimer(); 311 } 312 313 // We don't need to keep a reference to the element activation 314 // clearing, because the event and its timer keep each other alive 315 // until the timer expires 316 mDelayedClearElementActivation = nullptr; 317 } 318 319 void ElementStateManager::Destroy() { 320 if (mDelayedClearElementActivation) { 321 mDelayedClearElementActivation->ClearTimer(); 322 mDelayedClearElementActivation = nullptr; 323 } 324 CancelActiveTask(); 325 CancelHoverTask(); 326 } 327 328 void ElementStateManager::HandleStartPanning() { 329 ESM_LOG("Start panning"); 330 ClearActivation(); 331 // Unlike :active we don't need to unset :hover state. 332 // We just need to stop the hover task if it hasn't yet been triggered. 333 CancelHoverTask(); 334 } 335 336 void ElementStateManager::SetActive(dom::Element* aTarget) { 337 ESM_LOG("Setting active %p", aTarget); 338 339 if (nsPresContext* pc = GetPresContextFor(aTarget)) { 340 pc->EventStateManager()->SetContentState(aTarget, 341 dom::ElementState::ACTIVE); 342 } 343 } 344 345 void ElementStateManager::SetHover(dom::Element* aTarget) { 346 ESM_LOG("Setting hover %p", aTarget); 347 348 if (nsPresContext* pc = GetPresContextFor(aTarget)) { 349 pc->EventStateManager()->SetContentState(aTarget, dom::ElementState::HOVER); 350 } 351 } 352 353 void ElementStateManager::ResetActive() { 354 ESM_LOG("Resetting active from %p", mTarget.get()); 355 356 // Clear the :active flag from mTarget by setting it on the document root. 357 if (mTarget) { 358 dom::Element* root = mTarget->OwnerDoc()->GetDocumentElement(); 359 if (root) { 360 ESM_LOG("Found root %p, making active", root); 361 SetActive(root); 362 } 363 } 364 } 365 366 void ElementStateManager::ResetTouchBlockState() { 367 mTarget = nullptr; 368 mCanBePanOrZoomSet = false; 369 mTouchEndState.clear(); 370 mSingleTapBeforeActivation = false; 371 // NOTE: Do not reset mSingleTapState here since it will be necessary in 372 // ProcessSingleTap() to tell whether we need to activate the target element 373 // because on environments where double-tap is enabled ProcessSingleTap() 374 // gets called after both of touch-end event and end touch notiication 375 // arrived. 376 } 377 378 void ElementStateManager::ScheduleSetActiveTask() { 379 MOZ_ASSERT(mSetActiveTask == nullptr); 380 381 RefPtr<CancelableRunnable> task = 382 NewCancelableRunnableMethod<nsCOMPtr<dom::Element>>( 383 "layers::ElementStateManager::SetActiveTask", this, 384 &ElementStateManager::SetActiveTask, mTarget); 385 mSetActiveTask = task; 386 NS_GetCurrentThread()->DelayedDispatch( 387 task.forget(), StaticPrefs::ui_touch_activation_delay_ms()); 388 ESM_LOG("Scheduling mSetActiveTask %p\n", mSetActiveTask.get()); 389 } 390 391 void ElementStateManager::SetActiveTask(const nsCOMPtr<dom::Element>& aTarget) { 392 ESM_LOG("mSetActiveTask %p running", mSetActiveTask.get()); 393 394 // This gets called from mSetActiveTask's Run() method. The message loop 395 // deletes the task right after running it, so we need to null out 396 // mSetActiveTask to make sure we're not left with a dangling pointer. 397 mSetActiveTask = nullptr; 398 SetActive(aTarget); 399 } 400 401 void ElementStateManager::CancelActiveTask() { 402 ESM_LOG("Cancelling active task %p", mSetActiveTask.get()); 403 404 if (mSetActiveTask) { 405 mSetActiveTask->Cancel(); 406 mSetActiveTask = nullptr; 407 } 408 } 409 410 void ElementStateManager::ScheduleSetHoverTask() { 411 // Clobber the previous hover task. 412 CancelHoverTask(); 413 414 RefPtr<CancelableRunnable> task = 415 NewCancelableRunnableMethod<nsCOMPtr<dom::Element>>( 416 "layers::ElementStateManager::SetHoverTask", this, 417 &ElementStateManager::SetHoverTask, mTarget); 418 mSetHoverTask = task; 419 int32_t delay = StaticPrefs::ui_touch_hover_delay_ms(); 420 if (delay) { 421 NS_GetCurrentThread()->DelayedDispatch(task.forget(), delay); 422 } else { 423 NS_GetCurrentThread()->Dispatch(task.forget()); 424 } 425 ESM_LOG("Scheduling mSetHoverTask %p", mSetHoverTask.get()); 426 } 427 428 void ElementStateManager::SetHoverTask(const nsCOMPtr<dom::Element>& aTarget) { 429 ESM_LOG("mSetHoverTask %p running", mSetHoverTask.get()); 430 431 // This gets called from mSetHoverTask's Run() method. The message loop 432 // deletes the task right after running it, so we need to null out 433 // mSetHoverTask to make sure we're not left with a dangling pointer. 434 mSetHoverTask = nullptr; 435 SetHover(aTarget); 436 } 437 438 void ElementStateManager::CancelHoverTask() { 439 ESM_LOG("Cancelling task %p", mSetHoverTask.get()); 440 441 if (mSetHoverTask) { 442 mSetHoverTask->Cancel(); 443 mSetHoverTask = nullptr; 444 } 445 } 446 } // namespace layers 447 } // namespace mozilla