michael@0: /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #include "nsSMILAnimationController.h" michael@0: #include "nsSMILCompositor.h" michael@0: #include "nsSMILCSSProperty.h" michael@0: #include "nsCSSProps.h" michael@0: #include "nsITimer.h" michael@0: #include "mozilla/dom/Element.h" michael@0: #include "nsIDocument.h" michael@0: #include "mozilla/dom/SVGAnimationElement.h" michael@0: #include "nsSMILTimedElement.h" michael@0: #include michael@0: #include "mozilla/AutoRestore.h" michael@0: michael@0: using namespace mozilla; michael@0: using namespace mozilla::dom; michael@0: michael@0: //---------------------------------------------------------------------- michael@0: // nsSMILAnimationController implementation michael@0: michael@0: //---------------------------------------------------------------------- michael@0: // ctors, dtors, factory methods michael@0: michael@0: nsSMILAnimationController::nsSMILAnimationController(nsIDocument* aDoc) michael@0: : mAvgTimeBetweenSamples(0), michael@0: mResampleNeeded(false), michael@0: mDeferredStartSampling(false), michael@0: mRunningSample(false), michael@0: mRegisteredWithRefreshDriver(false), michael@0: mDocument(aDoc) michael@0: { michael@0: NS_ABORT_IF_FALSE(aDoc, "need a non-null document"); michael@0: michael@0: nsRefreshDriver* refreshDriver = GetRefreshDriver(); michael@0: if (refreshDriver) { michael@0: mStartTime = refreshDriver->MostRecentRefresh(); michael@0: } else { michael@0: mStartTime = mozilla::TimeStamp::Now(); michael@0: } michael@0: mCurrentSampleTime = mStartTime; michael@0: michael@0: Begin(); michael@0: } michael@0: michael@0: nsSMILAnimationController::~nsSMILAnimationController() michael@0: { michael@0: NS_ASSERTION(mAnimationElementTable.Count() == 0, michael@0: "Animation controller shouldn't be tracking any animation" michael@0: " elements when it dies"); michael@0: NS_ASSERTION(!mRegisteredWithRefreshDriver, michael@0: "Leaving stale entry in refresh driver's observer list"); michael@0: } michael@0: michael@0: void michael@0: nsSMILAnimationController::Disconnect() michael@0: { michael@0: NS_ABORT_IF_FALSE(mDocument, "disconnecting when we weren't connected...?"); michael@0: NS_ABORT_IF_FALSE(mRefCnt.get() == 1, michael@0: "Expecting to disconnect when doc is sole remaining owner"); michael@0: NS_ASSERTION(mPauseState & nsSMILTimeContainer::PAUSE_PAGEHIDE, michael@0: "Expecting to be paused for pagehide before disconnect"); michael@0: michael@0: StopSampling(GetRefreshDriver()); michael@0: michael@0: mDocument = nullptr; // (raw pointer) michael@0: } michael@0: michael@0: //---------------------------------------------------------------------- michael@0: // nsSMILTimeContainer methods: michael@0: michael@0: void michael@0: nsSMILAnimationController::Pause(uint32_t aType) michael@0: { michael@0: nsSMILTimeContainer::Pause(aType); michael@0: michael@0: if (mPauseState) { michael@0: mDeferredStartSampling = false; michael@0: StopSampling(GetRefreshDriver()); michael@0: } michael@0: } michael@0: michael@0: void michael@0: nsSMILAnimationController::Resume(uint32_t aType) michael@0: { michael@0: bool wasPaused = (mPauseState != 0); michael@0: // Update mCurrentSampleTime so that calls to GetParentTime--used for michael@0: // calculating parent offsets--are accurate michael@0: mCurrentSampleTime = mozilla::TimeStamp::Now(); michael@0: michael@0: nsSMILTimeContainer::Resume(aType); michael@0: michael@0: if (wasPaused && !mPauseState && mChildContainerTable.Count()) { michael@0: MaybeStartSampling(GetRefreshDriver()); michael@0: Sample(); // Run the first sample manually michael@0: } michael@0: } michael@0: michael@0: nsSMILTime michael@0: nsSMILAnimationController::GetParentTime() const michael@0: { michael@0: return (nsSMILTime)(mCurrentSampleTime - mStartTime).ToMilliseconds(); michael@0: } michael@0: michael@0: //---------------------------------------------------------------------- michael@0: // nsARefreshObserver methods: michael@0: NS_IMPL_ADDREF(nsSMILAnimationController) michael@0: NS_IMPL_RELEASE(nsSMILAnimationController) michael@0: michael@0: // nsRefreshDriver Callback function michael@0: void michael@0: nsSMILAnimationController::WillRefresh(mozilla::TimeStamp aTime) michael@0: { michael@0: // Although we never expect aTime to go backwards, when we initialise the michael@0: // animation controller, if we can't get hold of a refresh driver we michael@0: // initialise mCurrentSampleTime to Now(). It may be possible that after michael@0: // doing so we get sampled by a refresh driver whose most recent refresh time michael@0: // predates when we were initialised, so to be safe we make sure to take the michael@0: // most recent time here. michael@0: aTime = std::max(mCurrentSampleTime, aTime); michael@0: michael@0: // Sleep detection: If the time between samples is a whole lot greater than we michael@0: // were expecting then we assume the computer went to sleep or someone's michael@0: // messing with the clock. In that case, fiddle our parent offset and use our michael@0: // average time between samples to calculate the new sample time. This michael@0: // prevents us from hanging while trying to catch up on all the missed time. michael@0: michael@0: // Smoothing of coefficient for the average function. 0.2 should let us track michael@0: // the sample rate reasonably tightly without being overly affected by michael@0: // occasional delays. michael@0: static const double SAMPLE_DUR_WEIGHTING = 0.2; michael@0: // If the elapsed time exceeds our expectation by this number of times we'll michael@0: // initiate special behaviour to basically ignore the intervening time. michael@0: static const double SAMPLE_DEV_THRESHOLD = 200.0; michael@0: michael@0: nsSMILTime elapsedTime = michael@0: (nsSMILTime)(aTime - mCurrentSampleTime).ToMilliseconds(); michael@0: if (mAvgTimeBetweenSamples == 0) { michael@0: // First sample. michael@0: mAvgTimeBetweenSamples = elapsedTime; michael@0: } else { michael@0: if (elapsedTime > SAMPLE_DEV_THRESHOLD * mAvgTimeBetweenSamples) { michael@0: // Unexpectedly long delay between samples. michael@0: NS_WARNING("Detected really long delay between samples, continuing from " michael@0: "previous sample"); michael@0: mParentOffset += elapsedTime - mAvgTimeBetweenSamples; michael@0: } michael@0: // Update the moving average. Due to truncation here the average will michael@0: // normally be a little less than it should be but that's probably ok. michael@0: mAvgTimeBetweenSamples = michael@0: (nsSMILTime)(elapsedTime * SAMPLE_DUR_WEIGHTING + michael@0: mAvgTimeBetweenSamples * (1.0 - SAMPLE_DUR_WEIGHTING)); michael@0: } michael@0: mCurrentSampleTime = aTime; michael@0: michael@0: Sample(); michael@0: } michael@0: michael@0: //---------------------------------------------------------------------- michael@0: // Animation element registration methods: michael@0: michael@0: void michael@0: nsSMILAnimationController::RegisterAnimationElement( michael@0: SVGAnimationElement* aAnimationElement) michael@0: { michael@0: mAnimationElementTable.PutEntry(aAnimationElement); michael@0: if (mDeferredStartSampling) { michael@0: mDeferredStartSampling = false; michael@0: if (mChildContainerTable.Count()) { michael@0: // mAnimationElementTable was empty, but now we've added its 1st element michael@0: NS_ABORT_IF_FALSE(mAnimationElementTable.Count() == 1, michael@0: "we shouldn't have deferred sampling if we already had " michael@0: "animations registered"); michael@0: StartSampling(GetRefreshDriver()); michael@0: Sample(); // Run the first sample manually michael@0: } // else, don't sample until a time container is registered (via AddChild) michael@0: } michael@0: } michael@0: michael@0: void michael@0: nsSMILAnimationController::UnregisterAnimationElement( michael@0: SVGAnimationElement* aAnimationElement) michael@0: { michael@0: mAnimationElementTable.RemoveEntry(aAnimationElement); michael@0: } michael@0: michael@0: //---------------------------------------------------------------------- michael@0: // Page show/hide michael@0: michael@0: void michael@0: nsSMILAnimationController::OnPageShow() michael@0: { michael@0: Resume(nsSMILTimeContainer::PAUSE_PAGEHIDE); michael@0: } michael@0: michael@0: void michael@0: nsSMILAnimationController::OnPageHide() michael@0: { michael@0: Pause(nsSMILTimeContainer::PAUSE_PAGEHIDE); michael@0: } michael@0: michael@0: //---------------------------------------------------------------------- michael@0: // Cycle-collection support michael@0: michael@0: void michael@0: nsSMILAnimationController::Traverse( michael@0: nsCycleCollectionTraversalCallback* aCallback) michael@0: { michael@0: // Traverse last compositor table michael@0: if (mLastCompositorTable) { michael@0: mLastCompositorTable->EnumerateEntries(CompositorTableEntryTraverse, michael@0: aCallback); michael@0: } michael@0: } michael@0: michael@0: /*static*/ PLDHashOperator michael@0: nsSMILAnimationController::CompositorTableEntryTraverse( michael@0: nsSMILCompositor* aCompositor, michael@0: void* aArg) michael@0: { michael@0: nsCycleCollectionTraversalCallback* cb = michael@0: static_cast(aArg); michael@0: aCompositor->Traverse(cb); michael@0: return PL_DHASH_NEXT; michael@0: } michael@0: michael@0: void michael@0: nsSMILAnimationController::Unlink() michael@0: { michael@0: mLastCompositorTable = nullptr; michael@0: } michael@0: michael@0: //---------------------------------------------------------------------- michael@0: // Refresh driver lifecycle related methods michael@0: michael@0: void michael@0: nsSMILAnimationController::NotifyRefreshDriverCreated( michael@0: nsRefreshDriver* aRefreshDriver) michael@0: { michael@0: if (!mPauseState) { michael@0: MaybeStartSampling(aRefreshDriver); michael@0: } michael@0: } michael@0: michael@0: void michael@0: nsSMILAnimationController::NotifyRefreshDriverDestroying( michael@0: nsRefreshDriver* aRefreshDriver) michael@0: { michael@0: if (!mPauseState && !mDeferredStartSampling) { michael@0: StopSampling(aRefreshDriver); michael@0: } michael@0: } michael@0: michael@0: //---------------------------------------------------------------------- michael@0: // Timer-related implementation helpers michael@0: michael@0: void michael@0: nsSMILAnimationController::StartSampling(nsRefreshDriver* aRefreshDriver) michael@0: { michael@0: NS_ASSERTION(mPauseState == 0, "Starting sampling but controller is paused"); michael@0: NS_ASSERTION(!mDeferredStartSampling, michael@0: "Started sampling but the deferred start flag is still set"); michael@0: if (aRefreshDriver) { michael@0: MOZ_ASSERT(!mRegisteredWithRefreshDriver, michael@0: "Redundantly registering with refresh driver"); michael@0: NS_ABORT_IF_FALSE(!GetRefreshDriver() || michael@0: aRefreshDriver == GetRefreshDriver(), michael@0: "Starting sampling with wrong refresh driver"); michael@0: // We're effectively resuming from a pause so update our current sample time michael@0: // or else it will confuse our "average time between samples" calculations. michael@0: mCurrentSampleTime = mozilla::TimeStamp::Now(); michael@0: aRefreshDriver->AddRefreshObserver(this, Flush_Style); michael@0: mRegisteredWithRefreshDriver = true; michael@0: } michael@0: } michael@0: michael@0: void michael@0: nsSMILAnimationController::StopSampling(nsRefreshDriver* aRefreshDriver) michael@0: { michael@0: if (aRefreshDriver && mRegisteredWithRefreshDriver) { michael@0: // NOTE: The document might already have been detached from its PresContext michael@0: // (and RefreshDriver), which would make GetRefreshDriver() return null. michael@0: NS_ABORT_IF_FALSE(!GetRefreshDriver() || michael@0: aRefreshDriver == GetRefreshDriver(), michael@0: "Stopping sampling with wrong refresh driver"); michael@0: aRefreshDriver->RemoveRefreshObserver(this, Flush_Style); michael@0: mRegisteredWithRefreshDriver = false; michael@0: } michael@0: } michael@0: michael@0: void michael@0: nsSMILAnimationController::MaybeStartSampling(nsRefreshDriver* aRefreshDriver) michael@0: { michael@0: if (mDeferredStartSampling) { michael@0: // We've received earlier 'MaybeStartSampling' calls, and we're michael@0: // deferring until we get a registered animation. michael@0: return; michael@0: } michael@0: michael@0: if (mAnimationElementTable.Count()) { michael@0: StartSampling(aRefreshDriver); michael@0: } else { michael@0: mDeferredStartSampling = true; michael@0: } michael@0: } michael@0: michael@0: //---------------------------------------------------------------------- michael@0: // Sample-related methods and callbacks michael@0: michael@0: PLDHashOperator michael@0: TransferCachedBaseValue(nsSMILCompositor* aCompositor, michael@0: void* aData) michael@0: { michael@0: nsSMILCompositorTable* lastCompositorTable = michael@0: static_cast(aData); michael@0: nsSMILCompositor* lastCompositor = michael@0: lastCompositorTable->GetEntry(aCompositor->GetKey()); michael@0: michael@0: if (lastCompositor) { michael@0: aCompositor->StealCachedBaseValue(lastCompositor); michael@0: } michael@0: michael@0: return PL_DHASH_NEXT; michael@0: } michael@0: michael@0: PLDHashOperator michael@0: RemoveCompositorFromTable(nsSMILCompositor* aCompositor, michael@0: void* aData) michael@0: { michael@0: nsSMILCompositorTable* lastCompositorTable = michael@0: static_cast(aData); michael@0: lastCompositorTable->RemoveEntry(aCompositor->GetKey()); michael@0: return PL_DHASH_NEXT; michael@0: } michael@0: michael@0: PLDHashOperator michael@0: DoClearAnimationEffects(nsSMILCompositor* aCompositor, michael@0: void* /*aData*/) michael@0: { michael@0: aCompositor->ClearAnimationEffects(); michael@0: return PL_DHASH_NEXT; michael@0: } michael@0: michael@0: PLDHashOperator michael@0: DoComposeAttribute(nsSMILCompositor* aCompositor, michael@0: void* /*aData*/) michael@0: { michael@0: aCompositor->ComposeAttribute(); michael@0: return PL_DHASH_NEXT; michael@0: } michael@0: michael@0: void michael@0: nsSMILAnimationController::DoSample() michael@0: { michael@0: DoSample(true); // Skip unchanged time containers michael@0: } michael@0: michael@0: void michael@0: nsSMILAnimationController::DoSample(bool aSkipUnchangedContainers) michael@0: { michael@0: if (!mDocument) { michael@0: NS_ERROR("Shouldn't be sampling after document has disconnected"); michael@0: return; michael@0: } michael@0: if (mRunningSample) { michael@0: NS_ERROR("Shouldn't be recursively sampling"); michael@0: return; michael@0: } michael@0: michael@0: mResampleNeeded = false; michael@0: // Set running sample flag -- do this before flushing styles so that when we michael@0: // flush styles we don't end up requesting extra samples michael@0: AutoRestore autoRestoreRunningSample(mRunningSample); michael@0: mRunningSample = true; michael@0: michael@0: // STEP 1: Bring model up to date michael@0: // (i) Rewind elements where necessary michael@0: // (ii) Run milestone samples michael@0: RewindElements(); michael@0: DoMilestoneSamples(); michael@0: michael@0: // STEP 2: Sample the child time containers michael@0: // michael@0: // When we sample the child time containers they will simply record the sample michael@0: // time in document time. michael@0: TimeContainerHashtable activeContainers(mChildContainerTable.Count()); michael@0: SampleTimeContainerParams tcParams = { &activeContainers, michael@0: aSkipUnchangedContainers }; michael@0: mChildContainerTable.EnumerateEntries(SampleTimeContainer, &tcParams); michael@0: michael@0: // STEP 3: (i) Sample the timed elements AND michael@0: // (ii) Create a table of compositors michael@0: // michael@0: // (i) Here we sample the timed elements (fetched from the michael@0: // SVGAnimationElements) which determine from the active time if the michael@0: // element is active and what its simple time etc. is. This information is michael@0: // then passed to its time client (nsSMILAnimationFunction). michael@0: // michael@0: // (ii) During the same loop we also build up a table that contains one michael@0: // compositor for each animated attribute and which maps animated elements to michael@0: // the corresponding compositor for their target attribute. michael@0: // michael@0: // Note that this compositor table needs to be allocated on the heap so we can michael@0: // store it until the next sample. This lets us find out which elements were michael@0: // animated in sample 'n-1' but not in sample 'n' (and hence need to have michael@0: // their animation effects removed in sample 'n'). michael@0: // michael@0: // Parts (i) and (ii) are not functionally related but we combine them here to michael@0: // save iterating over the animation elements twice. michael@0: michael@0: // Create the compositor table michael@0: nsAutoPtr michael@0: currentCompositorTable(new nsSMILCompositorTable(0)); michael@0: michael@0: SampleAnimationParams saParams = { &activeContainers, michael@0: currentCompositorTable }; michael@0: mAnimationElementTable.EnumerateEntries(SampleAnimation, michael@0: &saParams); michael@0: activeContainers.Clear(); michael@0: michael@0: // STEP 4: Compare previous sample's compositors against this sample's. michael@0: // (Transfer cached base values across, & remove animation effects from michael@0: // no-longer-animated targets.) michael@0: if (mLastCompositorTable) { michael@0: // * Transfer over cached base values, from last sample's compositors michael@0: currentCompositorTable->EnumerateEntries(TransferCachedBaseValue, michael@0: mLastCompositorTable); michael@0: michael@0: // * For each compositor in current sample's hash table, remove entry from michael@0: // prev sample's hash table -- we don't need to clear animation michael@0: // effects of those compositors, since they're still being animated. michael@0: currentCompositorTable->EnumerateEntries(RemoveCompositorFromTable, michael@0: mLastCompositorTable); michael@0: michael@0: // * For each entry that remains in prev sample's hash table (i.e. for michael@0: // every target that's no longer animated), clear animation effects. michael@0: mLastCompositorTable->EnumerateEntries(DoClearAnimationEffects, nullptr); michael@0: } michael@0: michael@0: // return early if there are no active animations to avoid a style flush michael@0: if (currentCompositorTable->Count() == 0) { michael@0: mLastCompositorTable = nullptr; michael@0: return; michael@0: } michael@0: michael@0: nsCOMPtr kungFuDeathGrip(mDocument); // keeps 'this' alive too michael@0: mDocument->FlushPendingNotifications(Flush_Style); michael@0: michael@0: // WARNING: michael@0: // WARNING: the above flush may have destroyed the pres shell and/or michael@0: // WARNING: frames and other layout related objects. michael@0: // WARNING: michael@0: michael@0: // STEP 5: Compose currently-animated attributes. michael@0: // XXXdholbert: This step traverses our animation targets in an effectively michael@0: // random order. For animation from/to 'inherit' values to work correctly michael@0: // when the inherited value is *also* being animated, we really should be michael@0: // traversing our animated nodes in an ancestors-first order (bug 501183) michael@0: currentCompositorTable->EnumerateEntries(DoComposeAttribute, nullptr); michael@0: michael@0: // Update last compositor table michael@0: mLastCompositorTable = currentCompositorTable.forget(); michael@0: michael@0: NS_ASSERTION(!mResampleNeeded, "Resample dirty flag set during sample!"); michael@0: } michael@0: michael@0: void michael@0: nsSMILAnimationController::RewindElements() michael@0: { michael@0: bool rewindNeeded = false; michael@0: mChildContainerTable.EnumerateEntries(RewindNeeded, &rewindNeeded); michael@0: if (!rewindNeeded) michael@0: return; michael@0: michael@0: mAnimationElementTable.EnumerateEntries(RewindAnimation, nullptr); michael@0: mChildContainerTable.EnumerateEntries(ClearRewindNeeded, nullptr); michael@0: } michael@0: michael@0: /*static*/ PLDHashOperator michael@0: nsSMILAnimationController::RewindNeeded(TimeContainerPtrKey* aKey, michael@0: void* aData) michael@0: { michael@0: NS_ABORT_IF_FALSE(aData, michael@0: "Null data pointer during time container enumeration"); michael@0: bool* rewindNeeded = static_cast(aData); michael@0: michael@0: nsSMILTimeContainer* container = aKey->GetKey(); michael@0: if (container->NeedsRewind()) { michael@0: *rewindNeeded = true; michael@0: return PL_DHASH_STOP; michael@0: } michael@0: michael@0: return PL_DHASH_NEXT; michael@0: } michael@0: michael@0: /*static*/ PLDHashOperator michael@0: nsSMILAnimationController::RewindAnimation(AnimationElementPtrKey* aKey, michael@0: void* aData) michael@0: { michael@0: SVGAnimationElement* animElem = aKey->GetKey(); michael@0: nsSMILTimeContainer* timeContainer = animElem->GetTimeContainer(); michael@0: if (timeContainer && timeContainer->NeedsRewind()) { michael@0: animElem->TimedElement().Rewind(); michael@0: } michael@0: michael@0: return PL_DHASH_NEXT; michael@0: } michael@0: michael@0: /*static*/ PLDHashOperator michael@0: nsSMILAnimationController::ClearRewindNeeded(TimeContainerPtrKey* aKey, michael@0: void* aData) michael@0: { michael@0: aKey->GetKey()->ClearNeedsRewind(); michael@0: return PL_DHASH_NEXT; michael@0: } michael@0: michael@0: void michael@0: nsSMILAnimationController::DoMilestoneSamples() michael@0: { michael@0: // We need to sample the timing model but because SMIL operates independently michael@0: // of the frame-rate, we can get one sample at t=0s and the next at t=10min. michael@0: // michael@0: // In between those two sample times a whole string of significant events michael@0: // might be expected to take place: events firing, new interdependencies michael@0: // between animations resolved and dissolved, etc. michael@0: // michael@0: // Furthermore, at any given time, we want to sample all the intervals that michael@0: // end at that time BEFORE any that begin. This behaviour is implied by SMIL's michael@0: // endpoint-exclusive timing model. michael@0: // michael@0: // So we have the animations (specifically the timed elements) register the michael@0: // next significant moment (called a milestone) in their lifetime and then we michael@0: // step through the model at each of these moments and sample those animations michael@0: // registered for those times. This way events can fire in the correct order, michael@0: // dependencies can be resolved etc. michael@0: michael@0: nsSMILTime sampleTime = INT64_MIN; michael@0: michael@0: while (true) { michael@0: // We want to find any milestones AT OR BEFORE the current sample time so we michael@0: // initialise the next milestone to the moment after (1ms after, to be michael@0: // precise) the current sample time and see if there are any milestones michael@0: // before that. Any other milestones will be dealt with in a subsequent michael@0: // sample. michael@0: nsSMILMilestone nextMilestone(GetCurrentTime() + 1, true); michael@0: mChildContainerTable.EnumerateEntries(GetNextMilestone, &nextMilestone); michael@0: michael@0: if (nextMilestone.mTime > GetCurrentTime()) { michael@0: break; michael@0: } michael@0: michael@0: GetMilestoneElementsParams params; michael@0: params.mMilestone = nextMilestone; michael@0: mChildContainerTable.EnumerateEntries(GetMilestoneElements, ¶ms); michael@0: uint32_t length = params.mElements.Length(); michael@0: michael@0: // During the course of a sampling we don't want to actually go backwards. michael@0: // Due to negative offsets, early ends and the like, a timed element might michael@0: // register a milestone that is actually in the past. That's fine, but it's michael@0: // still only going to get *sampled* with whatever time we're up to and no michael@0: // earlier. michael@0: // michael@0: // Because we're only performing this clamping at the last moment, the michael@0: // animations will still all get sampled in the correct order and michael@0: // dependencies will be appropriately resolved. michael@0: sampleTime = std::max(nextMilestone.mTime, sampleTime); michael@0: michael@0: for (uint32_t i = 0; i < length; ++i) { michael@0: SVGAnimationElement* elem = params.mElements[i].get(); michael@0: NS_ABORT_IF_FALSE(elem, "nullptr animation element in list"); michael@0: nsSMILTimeContainer* container = elem->GetTimeContainer(); michael@0: if (!container) michael@0: // The container may be nullptr if the element has been detached from its michael@0: // parent since registering a milestone. michael@0: continue; michael@0: michael@0: nsSMILTimeValue containerTimeValue = michael@0: container->ParentToContainerTime(sampleTime); michael@0: if (!containerTimeValue.IsDefinite()) michael@0: continue; michael@0: michael@0: // Clamp the converted container time to non-negative values. michael@0: nsSMILTime containerTime = std::max(0, containerTimeValue.GetMillis()); michael@0: michael@0: if (nextMilestone.mIsEnd) { michael@0: elem->TimedElement().SampleEndAt(containerTime); michael@0: } else { michael@0: elem->TimedElement().SampleAt(containerTime); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: /*static*/ PLDHashOperator michael@0: nsSMILAnimationController::GetNextMilestone(TimeContainerPtrKey* aKey, michael@0: void* aData) michael@0: { michael@0: NS_ABORT_IF_FALSE(aKey, "Null hash key for time container hash table"); michael@0: NS_ABORT_IF_FALSE(aKey->GetKey(), "Null time container key in hash table"); michael@0: NS_ABORT_IF_FALSE(aData, michael@0: "Null data pointer during time container enumeration"); michael@0: michael@0: nsSMILMilestone* nextMilestone = static_cast(aData); michael@0: michael@0: nsSMILTimeContainer* container = aKey->GetKey(); michael@0: if (container->IsPausedByType(nsSMILTimeContainer::PAUSE_BEGIN)) michael@0: return PL_DHASH_NEXT; michael@0: michael@0: nsSMILMilestone thisMilestone; michael@0: bool didGetMilestone = michael@0: container->GetNextMilestoneInParentTime(thisMilestone); michael@0: if (didGetMilestone && thisMilestone < *nextMilestone) { michael@0: *nextMilestone = thisMilestone; michael@0: } michael@0: michael@0: return PL_DHASH_NEXT; michael@0: } michael@0: michael@0: /*static*/ PLDHashOperator michael@0: nsSMILAnimationController::GetMilestoneElements(TimeContainerPtrKey* aKey, michael@0: void* aData) michael@0: { michael@0: NS_ABORT_IF_FALSE(aKey, "Null hash key for time container hash table"); michael@0: NS_ABORT_IF_FALSE(aKey->GetKey(), "Null time container key in hash table"); michael@0: NS_ABORT_IF_FALSE(aData, michael@0: "Null data pointer during time container enumeration"); michael@0: michael@0: GetMilestoneElementsParams* params = michael@0: static_cast(aData); michael@0: michael@0: nsSMILTimeContainer* container = aKey->GetKey(); michael@0: if (container->IsPausedByType(nsSMILTimeContainer::PAUSE_BEGIN)) michael@0: return PL_DHASH_NEXT; michael@0: michael@0: container->PopMilestoneElementsAtMilestone(params->mMilestone, michael@0: params->mElements); michael@0: michael@0: return PL_DHASH_NEXT; michael@0: } michael@0: michael@0: /*static*/ PLDHashOperator michael@0: nsSMILAnimationController::SampleTimeContainer(TimeContainerPtrKey* aKey, michael@0: void* aData) michael@0: { michael@0: NS_ENSURE_TRUE(aKey, PL_DHASH_NEXT); michael@0: NS_ENSURE_TRUE(aKey->GetKey(), PL_DHASH_NEXT); michael@0: NS_ENSURE_TRUE(aData, PL_DHASH_NEXT); michael@0: michael@0: SampleTimeContainerParams* params = michael@0: static_cast(aData); michael@0: michael@0: nsSMILTimeContainer* container = aKey->GetKey(); michael@0: if (!container->IsPausedByType(nsSMILTimeContainer::PAUSE_BEGIN) && michael@0: (container->NeedsSample() || !params->mSkipUnchangedContainers)) { michael@0: container->ClearMilestones(); michael@0: container->Sample(); michael@0: container->MarkSeekFinished(); michael@0: params->mActiveContainers->PutEntry(container); michael@0: } michael@0: michael@0: return PL_DHASH_NEXT; michael@0: } michael@0: michael@0: /*static*/ PLDHashOperator michael@0: nsSMILAnimationController::SampleAnimation(AnimationElementPtrKey* aKey, michael@0: void* aData) michael@0: { michael@0: NS_ENSURE_TRUE(aKey, PL_DHASH_NEXT); michael@0: NS_ENSURE_TRUE(aKey->GetKey(), PL_DHASH_NEXT); michael@0: NS_ENSURE_TRUE(aData, PL_DHASH_NEXT); michael@0: michael@0: SVGAnimationElement* animElem = aKey->GetKey(); michael@0: if (animElem->PassesConditionalProcessingTests()) { michael@0: SampleAnimationParams* params = static_cast(aData); michael@0: michael@0: SampleTimedElement(animElem, params->mActiveContainers); michael@0: AddAnimationToCompositorTable(animElem, params->mCompositorTable); michael@0: } michael@0: michael@0: return PL_DHASH_NEXT; michael@0: } michael@0: michael@0: /*static*/ void michael@0: nsSMILAnimationController::SampleTimedElement( michael@0: SVGAnimationElement* aElement, TimeContainerHashtable* aActiveContainers) michael@0: { michael@0: nsSMILTimeContainer* timeContainer = aElement->GetTimeContainer(); michael@0: if (!timeContainer) michael@0: return; michael@0: michael@0: // We'd like to call timeContainer->NeedsSample() here and skip all timed michael@0: // elements that belong to paused time containers that don't need a sample, michael@0: // but that doesn't work because we've already called Sample() on all the time michael@0: // containers so the paused ones don't need a sample any more and they'll michael@0: // return false. michael@0: // michael@0: // Instead we build up a hashmap of active time containers during the previous michael@0: // step (SampleTimeContainer) and then test here if the container for this michael@0: // timed element is in the list. michael@0: if (!aActiveContainers->GetEntry(timeContainer)) michael@0: return; michael@0: michael@0: nsSMILTime containerTime = timeContainer->GetCurrentTime(); michael@0: michael@0: NS_ABORT_IF_FALSE(!timeContainer->IsSeeking(), michael@0: "Doing a regular sample but the time container is still seeking"); michael@0: aElement->TimedElement().SampleAt(containerTime); michael@0: } michael@0: michael@0: /*static*/ void michael@0: nsSMILAnimationController::AddAnimationToCompositorTable( michael@0: SVGAnimationElement* aElement, nsSMILCompositorTable* aCompositorTable) michael@0: { michael@0: // Add a compositor to the hash table if there's not already one there michael@0: nsSMILTargetIdentifier key; michael@0: if (!GetTargetIdentifierForAnimation(aElement, key)) michael@0: // Something's wrong/missing about animation's target; skip this animation michael@0: return; michael@0: michael@0: nsSMILAnimationFunction& func = aElement->AnimationFunction(); michael@0: michael@0: // Only add active animation functions. If there are no active animations michael@0: // targeting an attribute, no compositor will be created and any previously michael@0: // applied animations will be cleared. michael@0: if (func.IsActiveOrFrozen()) { michael@0: // Look up the compositor for our target, & add our animation function michael@0: // to its list of animation functions. michael@0: nsSMILCompositor* result = aCompositorTable->PutEntry(key); michael@0: result->AddAnimationFunction(&func); michael@0: michael@0: } else if (func.HasChanged()) { michael@0: // Look up the compositor for our target, and force it to skip the michael@0: // "nothing's changed so don't bother compositing" optimization for this michael@0: // sample. |func| is inactive, but it's probably *newly* inactive (since michael@0: // it's got HasChanged() == true), so we need to make sure to recompose michael@0: // its target. michael@0: nsSMILCompositor* result = aCompositorTable->PutEntry(key); michael@0: result->ToggleForceCompositing(); michael@0: michael@0: // We've now made sure that |func|'s inactivity will be reflected as of michael@0: // this sample. We need to clear its HasChanged() flag so that it won't michael@0: // trigger this same clause in future samples (until it changes again). michael@0: func.ClearHasChanged(); michael@0: } michael@0: } michael@0: michael@0: static inline bool michael@0: IsTransformAttribute(int32_t aNamespaceID, nsIAtom *aAttributeName) michael@0: { michael@0: return aNamespaceID == kNameSpaceID_None && michael@0: (aAttributeName == nsGkAtoms::transform || michael@0: aAttributeName == nsGkAtoms::patternTransform || michael@0: aAttributeName == nsGkAtoms::gradientTransform); michael@0: } michael@0: michael@0: // Helper function that, given a SVGAnimationElement, looks up its target michael@0: // element & target attribute and populates a nsSMILTargetIdentifier michael@0: // for this target. michael@0: /*static*/ bool michael@0: nsSMILAnimationController::GetTargetIdentifierForAnimation( michael@0: SVGAnimationElement* aAnimElem, nsSMILTargetIdentifier& aResult) michael@0: { michael@0: // Look up target (animated) element michael@0: Element* targetElem = aAnimElem->GetTargetElementContent(); michael@0: if (!targetElem) michael@0: // Animation has no target elem -- skip it. michael@0: return false; michael@0: michael@0: // Look up target (animated) attribute michael@0: // SMILANIM section 3.1, attributeName may michael@0: // have an XMLNS prefix to indicate the XML namespace. michael@0: nsCOMPtr attributeName; michael@0: int32_t attributeNamespaceID; michael@0: if (!aAnimElem->GetTargetAttributeName(&attributeNamespaceID, michael@0: getter_AddRefs(attributeName))) michael@0: // Animation has no target attr -- skip it. michael@0: return false; michael@0: michael@0: // animateTransform can only animate transforms, conversely transforms michael@0: // can only be animated by animateTransform michael@0: if (IsTransformAttribute(attributeNamespaceID, attributeName) != michael@0: (aAnimElem->Tag() == nsGkAtoms::animateTransform)) michael@0: return false; michael@0: michael@0: // Look up target (animated) attribute-type michael@0: nsSMILTargetAttrType attributeType = aAnimElem->GetTargetAttributeType(); michael@0: michael@0: // Check if an 'auto' attributeType refers to a CSS property or XML attribute. michael@0: // Note that SMIL requires we search for CSS properties first. So if they michael@0: // overlap, 'auto' = 'CSS'. (SMILANIM 3.1) michael@0: bool isCSS = false; michael@0: if (attributeType == eSMILTargetAttrType_auto) { michael@0: if (attributeNamespaceID == kNameSpaceID_None) { michael@0: // width/height are special as they may be attributes or for michael@0: // outer- elements, mapped into style. michael@0: if (attributeName == nsGkAtoms::width || michael@0: attributeName == nsGkAtoms::height) { michael@0: isCSS = targetElem->GetNameSpaceID() != kNameSpaceID_SVG; michael@0: } else { michael@0: nsCSSProperty prop = michael@0: nsCSSProps::LookupProperty(nsDependentAtomString(attributeName), michael@0: nsCSSProps::eEnabledForAllContent); michael@0: isCSS = nsSMILCSSProperty::IsPropertyAnimatable(prop); michael@0: } michael@0: } michael@0: } else { michael@0: isCSS = (attributeType == eSMILTargetAttrType_CSS); michael@0: } michael@0: michael@0: // Construct the key michael@0: aResult.mElement = targetElem; michael@0: aResult.mAttributeName = attributeName; michael@0: aResult.mAttributeNamespaceID = attributeNamespaceID; michael@0: aResult.mIsCSS = isCSS; michael@0: michael@0: return true; michael@0: } michael@0: michael@0: //---------------------------------------------------------------------- michael@0: // Add/remove child time containers michael@0: michael@0: nsresult michael@0: nsSMILAnimationController::AddChild(nsSMILTimeContainer& aChild) michael@0: { michael@0: TimeContainerPtrKey* key = mChildContainerTable.PutEntry(&aChild); michael@0: NS_ENSURE_TRUE(key, NS_ERROR_OUT_OF_MEMORY); michael@0: michael@0: if (!mPauseState && mChildContainerTable.Count() == 1) { michael@0: MaybeStartSampling(GetRefreshDriver()); michael@0: Sample(); // Run the first sample manually michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: void michael@0: nsSMILAnimationController::RemoveChild(nsSMILTimeContainer& aChild) michael@0: { michael@0: mChildContainerTable.RemoveEntry(&aChild); michael@0: michael@0: if (!mPauseState && mChildContainerTable.Count() == 0) { michael@0: StopSampling(GetRefreshDriver()); michael@0: } michael@0: } michael@0: michael@0: // Helper method michael@0: nsRefreshDriver* michael@0: nsSMILAnimationController::GetRefreshDriver() michael@0: { michael@0: if (!mDocument) { michael@0: NS_ERROR("Requesting refresh driver after document has disconnected!"); michael@0: return nullptr; michael@0: } michael@0: michael@0: nsIPresShell* shell = mDocument->GetShell(); michael@0: if (!shell) { michael@0: return nullptr; michael@0: } michael@0: michael@0: nsPresContext* context = shell->GetPresContext(); michael@0: return context ? context->RefreshDriver() : nullptr; michael@0: } michael@0: michael@0: void michael@0: nsSMILAnimationController::FlagDocumentNeedsFlush() michael@0: { michael@0: mDocument->SetNeedStyleFlush(); michael@0: }