/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ #ifndef mozilla_AnimationEventDispatcher_h #define mozilla_AnimationEventDispatcher_h #include "mozilla/AnimationComparator.h" #include "mozilla/Assertions.h" #include "mozilla/Attributes.h" #include "mozilla/ContentEvents.h" #include "mozilla/EventDispatcher.h" #include "mozilla/EventListenerManager.h" #include "mozilla/Variant.h" #include "mozilla/dom/AnimationPlaybackEvent.h" #include "mozilla/dom/KeyframeEffect.h" #include "mozilla/ProfilerMarkers.h" #include "nsCycleCollectionParticipant.h" #include "nsPresContext.h" class nsRefreshDriver; namespace mozilla { struct AnimationEventInfo { struct CssAnimationOrTransitionData { OwningAnimationTarget mTarget; const EventMessage mMessage; const double mElapsedTime; // The transition generation or animation relative position in the global // animation list. We use this information to determine the order of // cancelled transitions or animations. (i.e. We override the animation // index of the cancelled transitions/animations because their animation // indexes have been changed.) const uint64_t mAnimationIndex; // FIXME(emilio): is this needed? This preserves behavior from before // bug 1847200, but it's unclear what the timeStamp of the event should be. // See also https://github.com/w3c/csswg-drafts/issues/9167 const TimeStamp mEventEnqueueTimeStamp{TimeStamp::Now()}; }; struct CssAnimationData : public CssAnimationOrTransitionData { const RefPtr mAnimationName; }; struct CssTransitionData : public CssAnimationOrTransitionData { // For transition events only. const AnimatedPropertyID mProperty; }; struct WebAnimationData { const RefPtr mOnEvent; const dom::Nullable mCurrentTime; const dom::Nullable mTimelineTime; const TimeStamp mEventEnqueueTimeStamp{TimeStamp::Now()}; }; using Data = Variant; RefPtr mAnimation; TimeStamp mScheduledEventTimeStamp; Data mData; OwningAnimationTarget* GetOwningAnimationTarget() { if (mData.is()) { return &mData.as().mTarget; } if (mData.is()) { return &mData.as().mTarget; } return nullptr; } // Return the event context if the event is animationcancel or // transitioncancel. Maybe GetEventContext() const { if (mData.is()) { const auto& data = mData.as(); return Some(dom::Animation::EventContext{ NonOwningAnimationTarget(data.mTarget), data.mAnimationIndex}); } if (mData.is()) { const auto& data = mData.as(); return Some(dom::Animation::EventContext{ NonOwningAnimationTarget(data.mTarget), data.mAnimationIndex}); } return Nothing(); } void MaybeAddMarker() const; // For CSS animation events AnimationEventInfo(RefPtr aAnimationName, const NonOwningAnimationTarget& aTarget, EventMessage aMessage, double aElapsedTime, uint64_t aAnimationIndex, const TimeStamp& aScheduledEventTimeStamp, dom::Animation* aAnimation) : mAnimation(aAnimation), mScheduledEventTimeStamp(aScheduledEventTimeStamp), mData(CssAnimationData{ {OwningAnimationTarget(aTarget.mElement, aTarget.mPseudoRequest), aMessage, aElapsedTime, aAnimationIndex}, std::move(aAnimationName)}) { if (profiler_thread_is_being_profiled_for_markers()) { MaybeAddMarker(); } } // For CSS transition events AnimationEventInfo(const AnimatedPropertyID& aProperty, const NonOwningAnimationTarget& aTarget, EventMessage aMessage, double aElapsedTime, uint64_t aTransitionGeneration, const TimeStamp& aScheduledEventTimeStamp, dom::Animation* aAnimation) : mAnimation(aAnimation), mScheduledEventTimeStamp(aScheduledEventTimeStamp), mData(CssTransitionData{ {OwningAnimationTarget(aTarget.mElement, aTarget.mPseudoRequest), aMessage, aElapsedTime, aTransitionGeneration}, aProperty}) { if (profiler_thread_is_being_profiled_for_markers()) { MaybeAddMarker(); } } // For web animation events AnimationEventInfo(nsAtom* aOnEvent, const dom::Nullable& aCurrentTime, const dom::Nullable& aTimelineTime, TimeStamp&& aScheduledEventTimeStamp, dom::Animation* aAnimation) : mAnimation(aAnimation), mScheduledEventTimeStamp(std::move(aScheduledEventTimeStamp)), mData(WebAnimationData{RefPtr{aOnEvent}, aCurrentTime, aTimelineTime}) { } AnimationEventInfo(const AnimationEventInfo& aOther) = delete; AnimationEventInfo& operator=(const AnimationEventInfo& aOther) = delete; AnimationEventInfo(AnimationEventInfo&& aOther) = default; AnimationEventInfo& operator=(AnimationEventInfo&& aOther) = default; int32_t Compare(const AnimationEventInfo& aOther, nsContentUtils::NodeIndexCache& aCache) const { if (mScheduledEventTimeStamp != aOther.mScheduledEventTimeStamp) { // Null timestamps sort first if (mScheduledEventTimeStamp.IsNull()) { return -1; } if (aOther.mScheduledEventTimeStamp.IsNull()) { return 1; } return mScheduledEventTimeStamp < aOther.mScheduledEventTimeStamp ? -1 : 1; } // Events in the Web Animations spec are prior to CSS events. if (IsWebAnimationEvent() != aOther.IsWebAnimationEvent()) { return IsWebAnimationEvent() ? -1 : 1; } return mAnimation->CompareCompositeOrder(GetEventContext(), *aOther.mAnimation, aOther.GetEventContext(), aCache); } bool IsWebAnimationEvent() const { return mData.is(); } // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230) MOZ_CAN_RUN_SCRIPT_BOUNDARY void Dispatch(nsPresContext* aPresContext) { if (mData.is()) { const auto& data = mData.as(); EventListenerManager* elm = mAnimation->GetExistingListenerManager(); if (!elm || !elm->HasListenersFor(data.mOnEvent)) { return; } dom::AnimationPlaybackEventInit init; init.mCurrentTime = data.mCurrentTime; init.mTimelineTime = data.mTimelineTime; MOZ_ASSERT(nsDependentAtomString(data.mOnEvent).Find(u"on"_ns) == 0, "mOnEvent atom should start with 'on'!"); RefPtr event = dom::AnimationPlaybackEvent::Constructor( mAnimation, Substring(nsDependentAtomString(data.mOnEvent), 2), init); event->SetTrusted(true); event->WidgetEventPtr()->AssignEventTime( WidgetEventTime(data.mEventEnqueueTimeStamp)); RefPtr target = mAnimation; EventDispatcher::DispatchDOMEvent(target, nullptr /* WidgetEvent */, event, aPresContext, nullptr /* nsEventStatus */); return; } if (mData.is()) { const auto& data = mData.as(); nsPIDOMWindowInner* win = data.mTarget.mElement->OwnerDoc()->GetInnerWindow(); if (win && !win->HasTransitionEventListeners()) { MOZ_ASSERT(data.mMessage == eTransitionStart || data.mMessage == eTransitionRun || data.mMessage == eTransitionEnd || data.mMessage == eTransitionCancel); return; } InternalTransitionEvent event(true, data.mMessage); data.mProperty.ToString(event.mPropertyName); event.mElapsedTime = data.mElapsedTime; event.mPseudoElement = nsCSSPseudoElements::PseudoRequestAsString( data.mTarget.mPseudoRequest); event.AssignEventTime(WidgetEventTime(data.mEventEnqueueTimeStamp)); RefPtr target = data.mTarget.mElement; EventDispatcher::Dispatch(target, aPresContext, &event); return; } const auto& data = mData.as(); InternalAnimationEvent event(true, data.mMessage); data.mAnimationName->ToString(event.mAnimationName); event.mElapsedTime = data.mElapsedTime; event.mPseudoElement = nsCSSPseudoElements::PseudoRequestAsString(data.mTarget.mPseudoRequest); event.AssignEventTime(WidgetEventTime(data.mEventEnqueueTimeStamp)); RefPtr target = data.mTarget.mElement; EventDispatcher::Dispatch(target, aPresContext, &event); } }; class AnimationEventDispatcher final { public: explicit AnimationEventDispatcher(nsPresContext* aPresContext) : mPresContext(aPresContext), mIsSorted(true) {} NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(AnimationEventDispatcher) NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(AnimationEventDispatcher) void Disconnect(); void QueueEvent(AnimationEventInfo&& aEvent); void QueueEvents(nsTArray&& aEvents); // This will call SortEvents automatically if it has not already been // called. void DispatchEvents() { if (!mPresContext || mPendingEvents.IsEmpty()) { return; } SortEvents(); EventArray events = std::move(mPendingEvents); // mIsSorted will be set to true by SortEvents above, and we leave it // that way since mPendingEvents is now empty for (AnimationEventInfo& info : events) { info.Dispatch(mPresContext); // Bail out if our mPresContext was nullified due to destroying the pres // context. if (!mPresContext) { break; } } } void ClearEventQueue() { mPendingEvents.Clear(); mIsSorted = true; } bool HasQueuedEvents() const { return !mPendingEvents.IsEmpty(); } // There shouldn't be a lot of events in the queue, so linear search should be // fine. bool HasQueuedEventsFor(const dom::Animation* aAnimation) const { for (const AnimationEventInfo& info : mPendingEvents) { if (info.mAnimation.get() == aAnimation) { return true; } } return false; } private: ~AnimationEventDispatcher() = default; // Sort all pending CSS animation/transition events by scheduled event time // and composite order. // https://drafts.csswg.org/web-animations/#update-animations-and-send-events void SortEvents() { if (mIsSorted) { return; } struct AnimationEventInfoComparator { mutable nsContentUtils::NodeIndexCache mCache; bool LessThan(const AnimationEventInfo& aOne, const AnimationEventInfo& aOther) const { return aOne.Compare(aOther, mCache) < 0; } }; mPendingEvents.StableSort(AnimationEventInfoComparator()); mIsSorted = true; } void ScheduleDispatch(); nsPresContext* mPresContext; using EventArray = nsTArray; EventArray mPendingEvents; bool mIsSorted; }; } // namespace mozilla #endif // mozilla_AnimationEventDispatcher_h