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 "SMILAnimationController.h"
8 
9 #include <algorithm>
10 
11 #include "mozilla/AutoRestore.h"
12 #include "mozilla/PresShell.h"
13 #include "mozilla/PresShellInlines.h"
14 #include "mozilla/RestyleManager.h"
15 #include "mozilla/SMILTimedElement.h"
16 #include "mozilla/dom/DocumentInlines.h"
17 #include "mozilla/dom/Element.h"
18 #include "mozilla/dom/SVGAnimationElement.h"
19 #include "nsContentUtils.h"
20 #include "nsCSSProps.h"
21 #include "nsRefreshDriver.h"
22 #include "mozilla/dom/Document.h"
23 #include "SMILCompositor.h"
24 #include "SMILCSSProperty.h"
25 
26 using namespace mozilla::dom;
27 
28 namespace mozilla {
29 
30 //----------------------------------------------------------------------
31 // SMILAnimationController implementation
32 
33 //----------------------------------------------------------------------
34 // ctors, dtors, factory methods
35 
SMILAnimationController(Document * aDoc)36 SMILAnimationController::SMILAnimationController(Document* aDoc)
37     : mAvgTimeBetweenSamples(0),
38       mResampleNeeded(false),
39       mDeferredStartSampling(false),
40       mRunningSample(false),
41       mRegisteredWithRefreshDriver(false),
42       mMightHavePendingStyleUpdates(false),
43       mDocument(aDoc) {
44   MOZ_ASSERT(aDoc, "need a non-null document");
45 
46   nsRefreshDriver* refreshDriver = GetRefreshDriver();
47   if (refreshDriver) {
48     mStartTime = refreshDriver->MostRecentRefresh();
49   } else {
50     mStartTime = mozilla::TimeStamp::Now();
51   }
52   mCurrentSampleTime = mStartTime;
53 
54   Begin();
55 }
56 
~SMILAnimationController()57 SMILAnimationController::~SMILAnimationController() {
58   NS_ASSERTION(mAnimationElementTable.Count() == 0,
59                "Animation controller shouldn't be tracking any animation"
60                " elements when it dies");
61   NS_ASSERTION(!mRegisteredWithRefreshDriver,
62                "Leaving stale entry in refresh driver's observer list");
63 }
64 
Disconnect()65 void SMILAnimationController::Disconnect() {
66   MOZ_ASSERT(mDocument, "disconnecting when we weren't connected...?");
67   MOZ_ASSERT(mRefCnt.get() == 1,
68              "Expecting to disconnect when doc is sole remaining owner");
69   NS_ASSERTION(mPauseState & SMILTimeContainer::PAUSE_PAGEHIDE,
70                "Expecting to be paused for pagehide before disconnect");
71 
72   StopSampling(GetRefreshDriver());
73 
74   mDocument = nullptr;  // (raw pointer)
75 }
76 
77 //----------------------------------------------------------------------
78 // SMILTimeContainer methods:
79 
Pause(uint32_t aType)80 void SMILAnimationController::Pause(uint32_t aType) {
81   SMILTimeContainer::Pause(aType);
82 
83   if (mPauseState) {
84     mDeferredStartSampling = false;
85     StopSampling(GetRefreshDriver());
86   }
87 }
88 
Resume(uint32_t aType)89 void SMILAnimationController::Resume(uint32_t aType) {
90   bool wasPaused = (mPauseState != 0);
91   // Update mCurrentSampleTime so that calls to GetParentTime--used for
92   // calculating parent offsets--are accurate
93   mCurrentSampleTime = mozilla::TimeStamp::Now();
94 
95   SMILTimeContainer::Resume(aType);
96 
97   if (wasPaused && !mPauseState && !mChildContainerTable.IsEmpty()) {
98     MaybeStartSampling(GetRefreshDriver());
99     Sample();  // Run the first sample manually
100   }
101 }
102 
GetParentTime() const103 SMILTime SMILAnimationController::GetParentTime() const {
104   return (SMILTime)(mCurrentSampleTime - mStartTime).ToMilliseconds();
105 }
106 
107 //----------------------------------------------------------------------
108 // nsARefreshObserver methods:
109 NS_IMPL_ADDREF(SMILAnimationController)
NS_IMPL_RELEASE(SMILAnimationController)110 NS_IMPL_RELEASE(SMILAnimationController)
111 
112 // nsRefreshDriver Callback function
113 void SMILAnimationController::WillRefresh(mozilla::TimeStamp aTime) {
114   // Although we never expect aTime to go backwards, when we initialise the
115   // animation controller, if we can't get hold of a refresh driver we
116   // initialise mCurrentSampleTime to Now(). It may be possible that after
117   // doing so we get sampled by a refresh driver whose most recent refresh time
118   // predates when we were initialised, so to be safe we make sure to take the
119   // most recent time here.
120   aTime = std::max(mCurrentSampleTime, aTime);
121 
122   // Sleep detection: If the time between samples is a whole lot greater than we
123   // were expecting then we assume the computer went to sleep or someone's
124   // messing with the clock. In that case, fiddle our parent offset and use our
125   // average time between samples to calculate the new sample time. This
126   // prevents us from hanging while trying to catch up on all the missed time.
127 
128   // Smoothing of coefficient for the average function. 0.2 should let us track
129   // the sample rate reasonably tightly without being overly affected by
130   // occasional delays.
131   static const double SAMPLE_DUR_WEIGHTING = 0.2;
132   // If the elapsed time exceeds our expectation by this number of times we'll
133   // initiate special behaviour to basically ignore the intervening time.
134   static const double SAMPLE_DEV_THRESHOLD = 200.0;
135 
136   SMILTime elapsedTime =
137       (SMILTime)(aTime - mCurrentSampleTime).ToMilliseconds();
138   if (mAvgTimeBetweenSamples == 0) {
139     // First sample.
140     mAvgTimeBetweenSamples = elapsedTime;
141   } else {
142     if (elapsedTime > SAMPLE_DEV_THRESHOLD * mAvgTimeBetweenSamples) {
143       // Unexpectedly long delay between samples.
144       NS_WARNING(
145           "Detected really long delay between samples, continuing from "
146           "previous sample");
147       mParentOffset += elapsedTime - mAvgTimeBetweenSamples;
148     }
149     // Update the moving average. Due to truncation here the average will
150     // normally be a little less than it should be but that's probably ok.
151     mAvgTimeBetweenSamples =
152         (SMILTime)(elapsedTime * SAMPLE_DUR_WEIGHTING +
153                    mAvgTimeBetweenSamples * (1.0 - SAMPLE_DUR_WEIGHTING));
154   }
155   mCurrentSampleTime = aTime;
156 
157   Sample();
158 }
159 
160 //----------------------------------------------------------------------
161 // Animation element registration methods:
162 
RegisterAnimationElement(SVGAnimationElement * aAnimationElement)163 void SMILAnimationController::RegisterAnimationElement(
164     SVGAnimationElement* aAnimationElement) {
165   mAnimationElementTable.PutEntry(aAnimationElement);
166   if (mDeferredStartSampling) {
167     mDeferredStartSampling = false;
168     if (!mChildContainerTable.IsEmpty()) {
169       // mAnimationElementTable was empty, but now we've added its 1st element
170       MOZ_ASSERT(mAnimationElementTable.Count() == 1,
171                  "we shouldn't have deferred sampling if we already had "
172                  "animations registered");
173       StartSampling(GetRefreshDriver());
174       Sample();  // Run the first sample manually
175     }  // else, don't sample until a time container is registered (via AddChild)
176   }
177 }
178 
UnregisterAnimationElement(SVGAnimationElement * aAnimationElement)179 void SMILAnimationController::UnregisterAnimationElement(
180     SVGAnimationElement* aAnimationElement) {
181   mAnimationElementTable.RemoveEntry(aAnimationElement);
182 }
183 
184 //----------------------------------------------------------------------
185 // Page show/hide
186 
OnPageShow()187 void SMILAnimationController::OnPageShow() {
188   Resume(SMILTimeContainer::PAUSE_PAGEHIDE);
189 }
190 
OnPageHide()191 void SMILAnimationController::OnPageHide() {
192   Pause(SMILTimeContainer::PAUSE_PAGEHIDE);
193 }
194 
195 //----------------------------------------------------------------------
196 // Cycle-collection support
197 
Traverse(nsCycleCollectionTraversalCallback * aCallback)198 void SMILAnimationController::Traverse(
199     nsCycleCollectionTraversalCallback* aCallback) {
200   // Traverse last compositor table
201   if (mLastCompositorTable) {
202     for (auto iter = mLastCompositorTable->Iter(); !iter.Done(); iter.Next()) {
203       SMILCompositor* compositor = iter.Get();
204       compositor->Traverse(aCallback);
205     }
206   }
207 }
208 
Unlink()209 void SMILAnimationController::Unlink() { mLastCompositorTable = nullptr; }
210 
211 //----------------------------------------------------------------------
212 // Refresh driver lifecycle related methods
213 
NotifyRefreshDriverCreated(nsRefreshDriver * aRefreshDriver)214 void SMILAnimationController::NotifyRefreshDriverCreated(
215     nsRefreshDriver* aRefreshDriver) {
216   if (!mPauseState && !mChildContainerTable.IsEmpty()) {
217     MaybeStartSampling(aRefreshDriver);
218   }
219 }
220 
NotifyRefreshDriverDestroying(nsRefreshDriver * aRefreshDriver)221 void SMILAnimationController::NotifyRefreshDriverDestroying(
222     nsRefreshDriver* aRefreshDriver) {
223   if (!mPauseState && !mDeferredStartSampling) {
224     StopSampling(aRefreshDriver);
225   }
226 }
227 
228 //----------------------------------------------------------------------
229 // Timer-related implementation helpers
230 
StartSampling(nsRefreshDriver * aRefreshDriver)231 void SMILAnimationController::StartSampling(nsRefreshDriver* aRefreshDriver) {
232   NS_ASSERTION(mPauseState == 0, "Starting sampling but controller is paused");
233   NS_ASSERTION(!mDeferredStartSampling,
234                "Started sampling but the deferred start flag is still set");
235   if (aRefreshDriver) {
236     MOZ_ASSERT(!mRegisteredWithRefreshDriver,
237                "Redundantly registering with refresh driver");
238     MOZ_ASSERT(!GetRefreshDriver() || aRefreshDriver == GetRefreshDriver(),
239                "Starting sampling with wrong refresh driver");
240     // We're effectively resuming from a pause so update our current sample time
241     // or else it will confuse our "average time between samples" calculations.
242     mCurrentSampleTime = mozilla::TimeStamp::Now();
243     aRefreshDriver->AddRefreshObserver(this, FlushType::Style,
244                                        "SMIL animations");
245     mRegisteredWithRefreshDriver = true;
246   }
247 }
248 
StopSampling(nsRefreshDriver * aRefreshDriver)249 void SMILAnimationController::StopSampling(nsRefreshDriver* aRefreshDriver) {
250   if (aRefreshDriver && mRegisteredWithRefreshDriver) {
251     // NOTE: The document might already have been detached from its PresContext
252     // (and RefreshDriver), which would make GetRefreshDriver() return null.
253     MOZ_ASSERT(!GetRefreshDriver() || aRefreshDriver == GetRefreshDriver(),
254                "Stopping sampling with wrong refresh driver");
255     aRefreshDriver->RemoveRefreshObserver(this, FlushType::Style);
256     mRegisteredWithRefreshDriver = false;
257   }
258 }
259 
MaybeStartSampling(nsRefreshDriver * aRefreshDriver)260 void SMILAnimationController::MaybeStartSampling(
261     nsRefreshDriver* aRefreshDriver) {
262   if (mDeferredStartSampling) {
263     // We've received earlier 'MaybeStartSampling' calls, and we're
264     // deferring until we get a registered animation.
265     return;
266   }
267 
268   if (mAnimationElementTable.Count()) {
269     StartSampling(aRefreshDriver);
270   } else {
271     mDeferredStartSampling = true;
272   }
273 }
274 
275 //----------------------------------------------------------------------
276 // Sample-related methods and callbacks
277 
DoSample()278 void SMILAnimationController::DoSample() {
279   DoSample(true);  // Skip unchanged time containers
280 }
281 
DoSample(bool aSkipUnchangedContainers)282 void SMILAnimationController::DoSample(bool aSkipUnchangedContainers) {
283   if (!mDocument) {
284     NS_ERROR("Shouldn't be sampling after document has disconnected");
285     return;
286   }
287   if (mRunningSample) {
288     NS_ERROR("Shouldn't be recursively sampling");
289     return;
290   }
291 
292   bool isStyleFlushNeeded = mResampleNeeded;
293   mResampleNeeded = false;
294 
295   nsCOMPtr<Document> document(mDocument);  // keeps 'this' alive too
296 
297   // Set running sample flag -- do this before flushing styles so that when we
298   // flush styles we don't end up requesting extra samples
299   AutoRestore<bool> autoRestoreRunningSample(mRunningSample);
300   mRunningSample = true;
301 
302   // STEP 1: Bring model up to date
303   // (i)  Rewind elements where necessary
304   // (ii) Run milestone samples
305   RewindElements();
306   DoMilestoneSamples();
307 
308   // STEP 2: Sample the child time containers
309   //
310   // When we sample the child time containers they will simply record the sample
311   // time in document time.
312   TimeContainerHashtable activeContainers(mChildContainerTable.Count());
313   for (SMILTimeContainer* container : mChildContainerTable.Keys()) {
314     if (!container) {
315       continue;
316     }
317 
318     if (!container->IsPausedByType(SMILTimeContainer::PAUSE_BEGIN) &&
319         (container->NeedsSample() || !aSkipUnchangedContainers)) {
320       container->ClearMilestones();
321       container->Sample();
322       container->MarkSeekFinished();
323       activeContainers.PutEntry(container);
324     }
325   }
326 
327   // STEP 3: (i)  Sample the timed elements AND
328   //         (ii) Create a table of compositors
329   //
330   // (i) Here we sample the timed elements (fetched from the
331   // SVGAnimationElements) which determine from the active time if the
332   // element is active and what its simple time etc. is. This information is
333   // then passed to its time client (SMILAnimationFunction).
334   //
335   // (ii) During the same loop we also build up a table that contains one
336   // compositor for each animated attribute and which maps animated elements to
337   // the corresponding compositor for their target attribute.
338   //
339   // Note that this compositor table needs to be allocated on the heap so we can
340   // store it until the next sample. This lets us find out which elements were
341   // animated in sample 'n-1' but not in sample 'n' (and hence need to have
342   // their animation effects removed in sample 'n').
343   //
344   // Parts (i) and (ii) are not functionally related but we combine them here to
345   // save iterating over the animation elements twice.
346 
347   // Create the compositor table
348   UniquePtr<SMILCompositorTable> currentCompositorTable(
349       new SMILCompositorTable(0));
350   nsTArray<RefPtr<SVGAnimationElement>> animElems(
351       mAnimationElementTable.Count());
352 
353   for (SVGAnimationElement* animElem : mAnimationElementTable.Keys()) {
354     SampleTimedElement(animElem, &activeContainers);
355     AddAnimationToCompositorTable(animElem, currentCompositorTable.get(),
356                                   isStyleFlushNeeded);
357     animElems.AppendElement(animElem);
358   }
359   activeContainers.Clear();
360 
361   // STEP 4: Compare previous sample's compositors against this sample's.
362   // (Transfer cached base values across, & remove animation effects from
363   // no-longer-animated targets.)
364   if (mLastCompositorTable) {
365     // * Transfer over cached base values, from last sample's compositors
366     for (auto iter = currentCompositorTable->Iter(); !iter.Done();
367          iter.Next()) {
368       SMILCompositor* compositor = iter.Get();
369       SMILCompositor* lastCompositor =
370           mLastCompositorTable->GetEntry(compositor->GetKey());
371 
372       if (lastCompositor) {
373         compositor->StealCachedBaseValue(lastCompositor);
374       }
375     }
376 
377     // * For each compositor in current sample's hash table, remove entry from
378     // prev sample's hash table -- we don't need to clear animation
379     // effects of those compositors, since they're still being animated.
380     for (const auto& key : currentCompositorTable->Keys()) {
381       mLastCompositorTable->RemoveEntry(key);
382     }
383 
384     // * For each entry that remains in prev sample's hash table (i.e. for
385     // every target that's no longer animated), clear animation effects.
386     for (auto iter = mLastCompositorTable->Iter(); !iter.Done(); iter.Next()) {
387       iter.Get()->ClearAnimationEffects();
388     }
389   }
390 
391   // return early if there are no active animations to avoid a style flush
392   if (currentCompositorTable->Count() == 0) {
393     mLastCompositorTable = nullptr;
394     return;
395   }
396 
397   if (isStyleFlushNeeded) {
398     document->FlushPendingNotifications(FlushType::Style);
399   }
400 
401   // WARNING:
402   // WARNING: the above flush may have destroyed the pres shell and/or
403   // WARNING: frames and other layout related objects.
404   // WARNING:
405 
406   // STEP 5: Compose currently-animated attributes.
407   // XXXdholbert: This step traverses our animation targets in an effectively
408   // random order. For animation from/to 'inherit' values to work correctly
409   // when the inherited value is *also* being animated, we really should be
410   // traversing our animated nodes in an ancestors-first order (bug 501183)
411   bool mightHavePendingStyleUpdates = false;
412   for (auto iter = currentCompositorTable->Iter(); !iter.Done(); iter.Next()) {
413     iter.Get()->ComposeAttribute(mightHavePendingStyleUpdates);
414   }
415 
416   // Update last compositor table
417   mLastCompositorTable = std::move(currentCompositorTable);
418   mMightHavePendingStyleUpdates = mightHavePendingStyleUpdates;
419 
420   NS_ASSERTION(!mResampleNeeded, "Resample dirty flag set during sample!");
421 }
422 
RewindElements()423 void SMILAnimationController::RewindElements() {
424   const bool rewindNeeded = std::any_of(
425       mChildContainerTable.Keys().cbegin(), mChildContainerTable.Keys().cend(),
426       [](SMILTimeContainer* container) { return container->NeedsRewind(); });
427 
428   if (!rewindNeeded) return;
429 
430   for (SVGAnimationElement* animElem : mAnimationElementTable.Keys()) {
431     SMILTimeContainer* timeContainer = animElem->GetTimeContainer();
432     if (timeContainer && timeContainer->NeedsRewind()) {
433       animElem->TimedElement().Rewind();
434     }
435   }
436 
437   for (SMILTimeContainer* container : mChildContainerTable.Keys()) {
438     container->ClearNeedsRewind();
439   }
440 }
441 
DoMilestoneSamples()442 void SMILAnimationController::DoMilestoneSamples() {
443   // We need to sample the timing model but because SMIL operates independently
444   // of the frame-rate, we can get one sample at t=0s and the next at t=10min.
445   //
446   // In between those two sample times a whole string of significant events
447   // might be expected to take place: events firing, new interdependencies
448   // between animations resolved and dissolved, etc.
449   //
450   // Furthermore, at any given time, we want to sample all the intervals that
451   // end at that time BEFORE any that begin. This behaviour is implied by SMIL's
452   // endpoint-exclusive timing model.
453   //
454   // So we have the animations (specifically the timed elements) register the
455   // next significant moment (called a milestone) in their lifetime and then we
456   // step through the model at each of these moments and sample those animations
457   // registered for those times. This way events can fire in the correct order,
458   // dependencies can be resolved etc.
459 
460   SMILTime sampleTime = INT64_MIN;
461 
462   while (true) {
463     // We want to find any milestones AT OR BEFORE the current sample time so we
464     // initialise the next milestone to the moment after (1ms after, to be
465     // precise) the current sample time and see if there are any milestones
466     // before that. Any other milestones will be dealt with in a subsequent
467     // sample.
468     SMILMilestone nextMilestone(GetCurrentTimeAsSMILTime() + 1, true);
469     for (SMILTimeContainer* container : mChildContainerTable.Keys()) {
470       if (container->IsPausedByType(SMILTimeContainer::PAUSE_BEGIN)) {
471         continue;
472       }
473       SMILMilestone thisMilestone;
474       bool didGetMilestone =
475           container->GetNextMilestoneInParentTime(thisMilestone);
476       if (didGetMilestone && thisMilestone < nextMilestone) {
477         nextMilestone = thisMilestone;
478       }
479     }
480 
481     if (nextMilestone.mTime > GetCurrentTimeAsSMILTime()) {
482       break;
483     }
484 
485     nsTArray<RefPtr<mozilla::dom::SVGAnimationElement>> elements;
486     for (SMILTimeContainer* container : mChildContainerTable.Keys()) {
487       if (container->IsPausedByType(SMILTimeContainer::PAUSE_BEGIN)) {
488         continue;
489       }
490       container->PopMilestoneElementsAtMilestone(nextMilestone, elements);
491     }
492 
493     uint32_t length = elements.Length();
494 
495     // During the course of a sampling we don't want to actually go backwards.
496     // Due to negative offsets, early ends and the like, a timed element might
497     // register a milestone that is actually in the past. That's fine, but it's
498     // still only going to get *sampled* with whatever time we're up to and no
499     // earlier.
500     //
501     // Because we're only performing this clamping at the last moment, the
502     // animations will still all get sampled in the correct order and
503     // dependencies will be appropriately resolved.
504     sampleTime = std::max(nextMilestone.mTime, sampleTime);
505 
506     for (uint32_t i = 0; i < length; ++i) {
507       SVGAnimationElement* elem = elements[i].get();
508       MOZ_ASSERT(elem, "nullptr animation element in list");
509       SMILTimeContainer* container = elem->GetTimeContainer();
510       if (!container)
511         // The container may be nullptr if the element has been detached from
512         // its parent since registering a milestone.
513         continue;
514 
515       SMILTimeValue containerTimeValue =
516           container->ParentToContainerTime(sampleTime);
517       if (!containerTimeValue.IsDefinite()) continue;
518 
519       // Clamp the converted container time to non-negative values.
520       SMILTime containerTime =
521           std::max<SMILTime>(0, containerTimeValue.GetMillis());
522 
523       if (nextMilestone.mIsEnd) {
524         elem->TimedElement().SampleEndAt(containerTime);
525       } else {
526         elem->TimedElement().SampleAt(containerTime);
527       }
528     }
529   }
530 }
531 
532 /*static*/
SampleTimedElement(SVGAnimationElement * aElement,TimeContainerHashtable * aActiveContainers)533 void SMILAnimationController::SampleTimedElement(
534     SVGAnimationElement* aElement, TimeContainerHashtable* aActiveContainers) {
535   SMILTimeContainer* timeContainer = aElement->GetTimeContainer();
536   if (!timeContainer) return;
537 
538   // We'd like to call timeContainer->NeedsSample() here and skip all timed
539   // elements that belong to paused time containers that don't need a sample,
540   // but that doesn't work because we've already called Sample() on all the time
541   // containers so the paused ones don't need a sample any more and they'll
542   // return false.
543   //
544   // Instead we build up a hashmap of active time containers during the previous
545   // step (SampleTimeContainer) and then test here if the container for this
546   // timed element is in the list.
547   if (!aActiveContainers->GetEntry(timeContainer)) return;
548 
549   SMILTime containerTime = timeContainer->GetCurrentTimeAsSMILTime();
550 
551   MOZ_ASSERT(!timeContainer->IsSeeking(),
552              "Doing a regular sample but the time container is still seeking");
553   aElement->TimedElement().SampleAt(containerTime);
554 }
555 
556 /*static*/
AddAnimationToCompositorTable(SVGAnimationElement * aElement,SMILCompositorTable * aCompositorTable,bool & aStyleFlushNeeded)557 void SMILAnimationController::AddAnimationToCompositorTable(
558     SVGAnimationElement* aElement, SMILCompositorTable* aCompositorTable,
559     bool& aStyleFlushNeeded) {
560   // Add a compositor to the hash table if there's not already one there
561   SMILTargetIdentifier key;
562   if (!GetTargetIdentifierForAnimation(aElement, key))
563     // Something's wrong/missing about animation's target; skip this animation
564     return;
565 
566   SMILAnimationFunction& func = aElement->AnimationFunction();
567 
568   // Only add active animation functions. If there are no active animations
569   // targeting an attribute, no compositor will be created and any previously
570   // applied animations will be cleared.
571   if (func.IsActiveOrFrozen()) {
572     // Look up the compositor for our target, & add our animation function
573     // to its list of animation functions.
574     SMILCompositor* result = aCompositorTable->PutEntry(key);
575     result->AddAnimationFunction(&func);
576 
577   } else if (func.HasChanged()) {
578     // Look up the compositor for our target, and force it to skip the
579     // "nothing's changed so don't bother compositing" optimization for this
580     // sample. |func| is inactive, but it's probably *newly* inactive (since
581     // it's got HasChanged() == true), so we need to make sure to recompose
582     // its target.
583     SMILCompositor* result = aCompositorTable->PutEntry(key);
584     result->ToggleForceCompositing();
585 
586     // We've now made sure that |func|'s inactivity will be reflected as of
587     // this sample. We need to clear its HasChanged() flag so that it won't
588     // trigger this same clause in future samples (until it changes again).
589     func.ClearHasChanged();
590   }
591   aStyleFlushNeeded |= func.ValueNeedsReparsingEverySample();
592 }
593 
IsTransformAttribute(int32_t aNamespaceID,nsAtom * aAttributeName)594 static inline bool IsTransformAttribute(int32_t aNamespaceID,
595                                         nsAtom* aAttributeName) {
596   return aNamespaceID == kNameSpaceID_None &&
597          (aAttributeName == nsGkAtoms::transform ||
598           aAttributeName == nsGkAtoms::patternTransform ||
599           aAttributeName == nsGkAtoms::gradientTransform);
600 }
601 
602 // Helper function that, given a SVGAnimationElement, looks up its target
603 // element & target attribute and populates a SMILTargetIdentifier
604 // for this target.
605 /*static*/
GetTargetIdentifierForAnimation(SVGAnimationElement * aAnimElem,SMILTargetIdentifier & aResult)606 bool SMILAnimationController::GetTargetIdentifierForAnimation(
607     SVGAnimationElement* aAnimElem, SMILTargetIdentifier& aResult) {
608   // Look up target (animated) element
609   Element* targetElem = aAnimElem->GetTargetElementContent();
610   if (!targetElem)
611     // Animation has no target elem -- skip it.
612     return false;
613 
614   // Look up target (animated) attribute
615   // SMILANIM section 3.1, attributeName may
616   // have an XMLNS prefix to indicate the XML namespace.
617   RefPtr<nsAtom> attributeName;
618   int32_t attributeNamespaceID;
619   if (!aAnimElem->GetTargetAttributeName(&attributeNamespaceID,
620                                          getter_AddRefs(attributeName)))
621     // Animation has no target attr -- skip it.
622     return false;
623 
624   // animateTransform can only animate transforms, conversely transforms
625   // can only be animated by animateTransform
626   if (IsTransformAttribute(attributeNamespaceID, attributeName) !=
627       (aAnimElem->IsSVGElement(nsGkAtoms::animateTransform)))
628     return false;
629 
630   // Construct the key
631   aResult.mElement = targetElem;
632   aResult.mAttributeName = attributeName;
633   aResult.mAttributeNamespaceID = attributeNamespaceID;
634 
635   return true;
636 }
637 
PreTraverse()638 bool SMILAnimationController::PreTraverse() {
639   return PreTraverseInSubtree(nullptr);
640 }
641 
PreTraverseInSubtree(Element * aRoot)642 bool SMILAnimationController::PreTraverseInSubtree(Element* aRoot) {
643   MOZ_ASSERT(NS_IsMainThread());
644 
645   if (!mMightHavePendingStyleUpdates) {
646     return false;
647   }
648 
649   nsPresContext* context = mDocument->GetPresContext();
650   if (!context) {
651     return false;
652   }
653 
654   bool foundElementsNeedingRestyle = false;
655   for (SVGAnimationElement* animElement : mAnimationElementTable.Keys()) {
656     SMILTargetIdentifier key;
657     if (!GetTargetIdentifierForAnimation(animElement, key)) {
658       // Something's wrong/missing about animation's target; skip this animation
659       continue;
660     }
661 
662     // Ignore restyles that aren't in the flattened tree subtree rooted at
663     // aRoot.
664     if (aRoot && !nsContentUtils::ContentIsFlattenedTreeDescendantOf(
665                      key.mElement, aRoot)) {
666       continue;
667     }
668 
669     context->RestyleManager()->PostRestyleEventForAnimations(
670         key.mElement, PseudoStyleType::NotPseudo, RestyleHint::RESTYLE_SMIL);
671 
672     foundElementsNeedingRestyle = true;
673   }
674 
675   // Only clear the mMightHavePendingStyleUpdates flag if we definitely posted
676   // all restyles.
677   if (!aRoot) {
678     mMightHavePendingStyleUpdates = false;
679   }
680 
681   return foundElementsNeedingRestyle;
682 }
683 
684 //----------------------------------------------------------------------
685 // Add/remove child time containers
686 
AddChild(SMILTimeContainer & aChild)687 nsresult SMILAnimationController::AddChild(SMILTimeContainer& aChild) {
688   TimeContainerPtrKey* key = mChildContainerTable.PutEntry(&aChild);
689   NS_ENSURE_TRUE(key, NS_ERROR_OUT_OF_MEMORY);
690 
691   if (!mPauseState && mChildContainerTable.Count() == 1) {
692     MaybeStartSampling(GetRefreshDriver());
693     Sample();  // Run the first sample manually
694   }
695 
696   return NS_OK;
697 }
698 
RemoveChild(SMILTimeContainer & aChild)699 void SMILAnimationController::RemoveChild(SMILTimeContainer& aChild) {
700   mChildContainerTable.RemoveEntry(&aChild);
701 
702   if (!mPauseState && mChildContainerTable.IsEmpty()) {
703     StopSampling(GetRefreshDriver());
704   }
705 }
706 
707 // Helper method
GetRefreshDriver()708 nsRefreshDriver* SMILAnimationController::GetRefreshDriver() {
709   if (!mDocument) {
710     NS_ERROR("Requesting refresh driver after document has disconnected!");
711     return nullptr;
712   }
713 
714   nsPresContext* context = mDocument->GetPresContext();
715   return context ? context->RefreshDriver() : nullptr;
716 }
717 
FlagDocumentNeedsFlush()718 void SMILAnimationController::FlagDocumentNeedsFlush() {
719   if (PresShell* presShell = mDocument->GetPresShell()) {
720     presShell->SetNeedStyleFlush();
721   }
722 }
723 
724 }  // namespace mozilla
725