tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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