summaryrefslogtreecommitdiffstats
path: root/dom/smil/SMILAnimationController.cpp
blob: fd288ce907ea1634065f631c76c036f18b748129 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
/* -*- 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/. */

#include "SMILAnimationController.h"

#include <algorithm>

#include "mozilla/AutoRestore.h"
#include "mozilla/PresShell.h"
#include "mozilla/PresShellInlines.h"
#include "mozilla/RestyleManager.h"
#include "mozilla/SMILTimedElement.h"
#include "mozilla/dom/DocumentInlines.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/SVGAnimationElement.h"
#include "nsContentUtils.h"
#include "nsCSSProps.h"
#include "nsRefreshDriver.h"
#include "mozilla/dom/Document.h"
#include "SMILCompositor.h"
#include "SMILCSSProperty.h"

using namespace mozilla::dom;

namespace mozilla {

//----------------------------------------------------------------------
// SMILAnimationController implementation

//----------------------------------------------------------------------
// ctors, dtors, factory methods

SMILAnimationController::SMILAnimationController(Document* aDoc)
    : mDocument(aDoc) {
  MOZ_ASSERT(aDoc, "need a non-null document");

  if (nsRefreshDriver* refreshDriver = GetRefreshDriver()) {
    mStartTime = refreshDriver->MostRecentRefresh();
  } else {
    mStartTime = mozilla::TimeStamp::Now();
  }
  mCurrentSampleTime = mStartTime;

  Begin();
}

SMILAnimationController::~SMILAnimationController() {
  NS_ASSERTION(mAnimationElementTable.Count() == 0,
               "Animation controller shouldn't be tracking any animation"
               " elements when it dies");
  NS_ASSERTION(!mRegisteredWithRefreshDriver,
               "Leaving stale entry in refresh driver's observer list");
}

void SMILAnimationController::Disconnect() {
  MOZ_ASSERT(mDocument, "disconnecting when we weren't connected...?");
  MOZ_ASSERT(mRefCnt.get() == 1,
             "Expecting to disconnect when doc is sole remaining owner");
  NS_ASSERTION(mPauseState & SMILTimeContainer::PAUSE_PAGEHIDE,
               "Expecting to be paused for pagehide before disconnect");

  StopSampling(GetRefreshDriver());

  mDocument = nullptr;  // (raw pointer)
}

//----------------------------------------------------------------------
// SMILTimeContainer methods:

void SMILAnimationController::Pause(uint32_t aType) {
  SMILTimeContainer::Pause(aType);
  UpdateSampling();
}

void SMILAnimationController::Resume(uint32_t aType) {
  bool wasPaused = !!mPauseState;
  // Update mCurrentSampleTime so that calls to GetParentTime--used for
  // calculating parent offsets--are accurate
  mCurrentSampleTime = mozilla::TimeStamp::Now();

  SMILTimeContainer::Resume(aType);

  if (wasPaused && !mPauseState) {
    UpdateSampling();
  }
}

SMILTime SMILAnimationController::GetParentTime() const {
  return (SMILTime)(mCurrentSampleTime - mStartTime).ToMilliseconds();
}

//----------------------------------------------------------------------
// nsARefreshObserver methods:
NS_IMPL_ADDREF(SMILAnimationController)
NS_IMPL_RELEASE(SMILAnimationController)

// nsRefreshDriver Callback function
void SMILAnimationController::WillRefresh(mozilla::TimeStamp aTime) {
  // Although we never expect aTime to go backwards, when we initialise the
  // animation controller, if we can't get hold of a refresh driver we
  // initialise mCurrentSampleTime to Now(). It may be possible that after
  // doing so we get sampled by a refresh driver whose most recent refresh time
  // predates when we were initialised, so to be safe we make sure to take the
  // most recent time here.
  aTime = std::max(mCurrentSampleTime, aTime);

  // Sleep detection: If the time between samples is a whole lot greater than we
  // were expecting then we assume the computer went to sleep or someone's
  // messing with the clock. In that case, fiddle our parent offset and use our
  // average time between samples to calculate the new sample time. This
  // prevents us from hanging while trying to catch up on all the missed time.

  // Smoothing of coefficient for the average function. 0.2 should let us track
  // the sample rate reasonably tightly without being overly affected by
  // occasional delays.
  static const double SAMPLE_DUR_WEIGHTING = 0.2;
  // If the elapsed time exceeds our expectation by this number of times we'll
  // initiate special behaviour to basically ignore the intervening time.
  static const double SAMPLE_DEV_THRESHOLD = 200.0;

  SMILTime elapsedTime =
      (SMILTime)(aTime - mCurrentSampleTime).ToMilliseconds();
  if (mAvgTimeBetweenSamples == 0) {
    // First sample.
    mAvgTimeBetweenSamples = elapsedTime;
  } else {
    if (elapsedTime > SAMPLE_DEV_THRESHOLD * mAvgTimeBetweenSamples) {
      // Unexpectedly long delay between samples.
      NS_WARNING(
          "Detected really long delay between samples, continuing from "
          "previous sample");
      mParentOffset += elapsedTime - mAvgTimeBetweenSamples;
    }
    // Update the moving average. Due to truncation here the average will
    // normally be a little less than it should be but that's probably ok.
    mAvgTimeBetweenSamples =
        (SMILTime)(elapsedTime * SAMPLE_DUR_WEIGHTING +
                   mAvgTimeBetweenSamples * (1.0 - SAMPLE_DUR_WEIGHTING));
  }
  mCurrentSampleTime = aTime;

  Sample();
}

//----------------------------------------------------------------------
// Animation element registration methods:

void SMILAnimationController::RegisterAnimationElement(
    SVGAnimationElement* aAnimationElement) {
  const bool wasEmpty = mAnimationElementTable.IsEmpty();
  mAnimationElementTable.PutEntry(aAnimationElement);
  if (wasEmpty) {
    UpdateSampling();
  }
}

void SMILAnimationController::UnregisterAnimationElement(
    SVGAnimationElement* aAnimationElement) {
  mAnimationElementTable.RemoveEntry(aAnimationElement);
  if (mAnimationElementTable.IsEmpty()) {
    UpdateSampling();
  }
}

//----------------------------------------------------------------------
// Page show/hide

void SMILAnimationController::OnPageShow() {
  Resume(SMILTimeContainer::PAUSE_PAGEHIDE);
}

void SMILAnimationController::OnPageHide() {
  Pause(SMILTimeContainer::PAUSE_PAGEHIDE);
}

//----------------------------------------------------------------------
// Cycle-collection support

void SMILAnimationController::Traverse(
    nsCycleCollectionTraversalCallback* aCallback) {
  // Traverse last compositor table
  if (mLastCompositorTable) {
    for (auto iter = mLastCompositorTable->Iter(); !iter.Done(); iter.Next()) {
      SMILCompositor* compositor = iter.Get();
      compositor->Traverse(aCallback);
    }
  }
}

void SMILAnimationController::Unlink() { mLastCompositorTable = nullptr; }

//----------------------------------------------------------------------
// Refresh driver lifecycle related methods

void SMILAnimationController::NotifyRefreshDriverCreated(
    nsRefreshDriver* aRefreshDriver) {
  UpdateSampling();
}

void SMILAnimationController::NotifyRefreshDriverDestroying(
    nsRefreshDriver* aRefreshDriver) {
  StopSampling(aRefreshDriver);
}

//----------------------------------------------------------------------
// Timer-related implementation helpers

bool SMILAnimationController::ShouldSample() const {
  return !mPauseState && !mAnimationElementTable.IsEmpty() &&
         !mChildContainerTable.IsEmpty();
}

void SMILAnimationController::UpdateSampling() {
  const bool shouldSample = ShouldSample();
  const bool isSampling = mRegisteredWithRefreshDriver;
  if (shouldSample == isSampling) {
    return;
  }

  nsRefreshDriver* driver = GetRefreshDriver();
  if (!driver) {
    return;
  }

  if (shouldSample) {
    // We're effectively resuming from a pause so update our current sample time
    // or else it will confuse our "average time between samples" calculations.
    mCurrentSampleTime = mozilla::TimeStamp::Now();
    driver->AddRefreshObserver(this, FlushType::Style, "SMIL animations");
    mRegisteredWithRefreshDriver = true;
    Sample();  // Run the first sample manually.
  } else {
    StopSampling(driver);
  }
}

void SMILAnimationController::StopSampling(nsRefreshDriver* aRefreshDriver) {
  if (aRefreshDriver && mRegisteredWithRefreshDriver) {
    // NOTE: The document might already have been detached from its PresContext
    // (and RefreshDriver), which would make GetRefreshDriver() return null.
    MOZ_ASSERT(!GetRefreshDriver() || aRefreshDriver == GetRefreshDriver(),
               "Stopping sampling with wrong refresh driver");
    aRefreshDriver->RemoveRefreshObserver(this, FlushType::Style);
    mRegisteredWithRefreshDriver = false;
  }
}

//----------------------------------------------------------------------
// Sample-related methods and callbacks

void SMILAnimationController::DoSample() {
  DoSample(true);  // Skip unchanged time containers
}

void SMILAnimationController::DoSample(bool aSkipUnchangedContainers) {
  if (!mDocument) {
    NS_ERROR("Shouldn't be sampling after document has disconnected");
    return;
  }
  if (mRunningSample) {
    NS_ERROR("Shouldn't be recursively sampling");
    return;
  }

  bool isStyleFlushNeeded = mResampleNeeded;
  mResampleNeeded = false;

  nsCOMPtr<Document> document(mDocument);  // keeps 'this' alive too

  // Set running sample flag -- do this before flushing styles so that when we
  // flush styles we don't end up requesting extra samples
  AutoRestore<bool> autoRestoreRunningSample(mRunningSample);
  mRunningSample = true;

  // STEP 1: Bring model up to date
  // (i)  Rewind elements where necessary
  // (ii) Run milestone samples
  RewindElements();
  DoMilestoneSamples();

  // STEP 2: Sample the child time containers
  //
  // When we sample the child time containers they will simply record the sample
  // time in document time.
  TimeContainerHashtable activeContainers(mChildContainerTable.Count());
  for (SMILTimeContainer* container : mChildContainerTable.Keys()) {
    if (!container) {
      continue;
    }

    if (!container->IsPausedByType(SMILTimeContainer::PAUSE_BEGIN) &&
        (container->NeedsSample() || !aSkipUnchangedContainers)) {
      container->ClearMilestones();
      container->Sample();
      container->MarkSeekFinished();
      activeContainers.PutEntry(container);
    }
  }

  // STEP 3: (i)  Sample the timed elements AND
  //         (ii) Create a table of compositors
  //
  // (i) Here we sample the timed elements (fetched from the
  // SVGAnimationElements) which determine from the active time if the
  // element is active and what its simple time etc. is. This information is
  // then passed to its time client (SMILAnimationFunction).
  //
  // (ii) During the same loop we also build up a table that contains one
  // compositor for each animated attribute and which maps animated elements to
  // the corresponding compositor for their target attribute.
  //
  // Note that this compositor table needs to be allocated on the heap so we can
  // store it until the next sample. This lets us find out which elements were
  // animated in sample 'n-1' but not in sample 'n' (and hence need to have
  // their animation effects removed in sample 'n').
  //
  // Parts (i) and (ii) are not functionally related but we combine them here to
  // save iterating over the animation elements twice.

  // Create the compositor table
  UniquePtr<SMILCompositorTable> currentCompositorTable(
      new SMILCompositorTable(0));
  nsTArray<RefPtr<SVGAnimationElement>> animElems(
      mAnimationElementTable.Count());

  for (SVGAnimationElement* animElem : mAnimationElementTable.Keys()) {
    SampleTimedElement(animElem, &activeContainers);
    AddAnimationToCompositorTable(animElem, currentCompositorTable.get(),
                                  isStyleFlushNeeded);
    animElems.AppendElement(animElem);
  }
  activeContainers.Clear();

  // STEP 4: Compare previous sample's compositors against this sample's.
  // (Transfer cached base values across, & remove animation effects from
  // no-longer-animated targets.)
  if (mLastCompositorTable) {
    // * Transfer over cached base values, from last sample's compositors
    for (auto iter = currentCompositorTable->Iter(); !iter.Done();
         iter.Next()) {
      SMILCompositor* compositor = iter.Get();
      SMILCompositor* lastCompositor =
          mLastCompositorTable->GetEntry(compositor->GetKey());

      if (lastCompositor) {
        compositor->StealCachedBaseValue(lastCompositor);
      }
    }

    // * For each compositor in current sample's hash table, remove entry from
    // prev sample's hash table -- we don't need to clear animation
    // effects of those compositors, since they're still being animated.
    for (const auto& key : currentCompositorTable->Keys()) {
      mLastCompositorTable->RemoveEntry(key);
    }

    // * For each entry that remains in prev sample's hash table (i.e. for
    // every target that's no longer animated), clear animation effects.
    for (auto iter = mLastCompositorTable->Iter(); !iter.Done(); iter.Next()) {
      iter.Get()->ClearAnimationEffects();
    }
  }

  // return early if there are no active animations to avoid a style flush
  if (currentCompositorTable->Count() == 0) {
    mLastCompositorTable = nullptr;
    return;
  }

  if (isStyleFlushNeeded) {
    document->FlushPendingNotifications(FlushType::Style);
  }

  // WARNING:
  // WARNING: the above flush may have destroyed the pres shell and/or
  // WARNING: frames and other layout related objects.
  // WARNING:

  // STEP 5: Compose currently-animated attributes.
  // XXXdholbert: This step traverses our animation targets in an effectively
  // random order. For animation from/to 'inherit' values to work correctly
  // when the inherited value is *also* being animated, we really should be
  // traversing our animated nodes in an ancestors-first order (bug 501183)
  bool mightHavePendingStyleUpdates = false;
  for (auto iter = currentCompositorTable->Iter(); !iter.Done(); iter.Next()) {
    iter.Get()->ComposeAttribute(mightHavePendingStyleUpdates);
  }

  // Update last compositor table
  mLastCompositorTable = std::move(currentCompositorTable);
  mMightHavePendingStyleUpdates = mightHavePendingStyleUpdates;

  NS_ASSERTION(!mResampleNeeded, "Resample dirty flag set during sample!");
}

void SMILAnimationController::RewindElements() {
  const bool rewindNeeded = std::any_of(
      mChildContainerTable.Keys().cbegin(), mChildContainerTable.Keys().cend(),
      [](SMILTimeContainer* container) { return container->NeedsRewind(); });

  if (!rewindNeeded) return;

  for (SVGAnimationElement* animElem : mAnimationElementTable.Keys()) {
    SMILTimeContainer* timeContainer = animElem->GetTimeContainer();
    if (timeContainer && timeContainer->NeedsRewind()) {
      animElem->TimedElement().Rewind();
    }
  }

  for (SMILTimeContainer* container : mChildContainerTable.Keys()) {
    container->ClearNeedsRewind();
  }
}

void SMILAnimationController::DoMilestoneSamples() {
  // We need to sample the timing model but because SMIL operates independently
  // of the frame-rate, we can get one sample at t=0s and the next at t=10min.
  //
  // In between those two sample times a whole string of significant events
  // might be expected to take place: events firing, new interdependencies
  // between animations resolved and dissolved, etc.
  //
  // Furthermore, at any given time, we want to sample all the intervals that
  // end at that time BEFORE any that begin. This behaviour is implied by SMIL's
  // endpoint-exclusive timing model.
  //
  // So we have the animations (specifically the timed elements) register the
  // next significant moment (called a milestone) in their lifetime and then we
  // step through the model at each of these moments and sample those animations
  // registered for those times. This way events can fire in the correct order,
  // dependencies can be resolved etc.

  SMILTime sampleTime = INT64_MIN;

  while (true) {
    // We want to find any milestones AT OR BEFORE the current sample time so we
    // initialise the next milestone to the moment after (1ms after, to be
    // precise) the current sample time and see if there are any milestones
    // before that. Any other milestones will be dealt with in a subsequent
    // sample.
    SMILMilestone nextMilestone(GetCurrentTimeAsSMILTime() + 1, true);
    for (SMILTimeContainer* container : mChildContainerTable.Keys()) {
      if (container->IsPausedByType(SMILTimeContainer::PAUSE_BEGIN)) {
        continue;
      }
      SMILMilestone thisMilestone;
      bool didGetMilestone =
          container->GetNextMilestoneInParentTime(thisMilestone);
      if (didGetMilestone && thisMilestone < nextMilestone) {
        nextMilestone = thisMilestone;
      }
    }

    if (nextMilestone.mTime > GetCurrentTimeAsSMILTime()) {
      break;
    }

    nsTArray<RefPtr<mozilla::dom::SVGAnimationElement>> elements;
    for (SMILTimeContainer* container : mChildContainerTable.Keys()) {
      if (container->IsPausedByType(SMILTimeContainer::PAUSE_BEGIN)) {
        continue;
      }
      container->PopMilestoneElementsAtMilestone(nextMilestone, elements);
    }

    uint32_t length = elements.Length();

    // During the course of a sampling we don't want to actually go backwards.
    // Due to negative offsets, early ends and the like, a timed element might
    // register a milestone that is actually in the past. That's fine, but it's
    // still only going to get *sampled* with whatever time we're up to and no
    // earlier.
    //
    // Because we're only performing this clamping at the last moment, the
    // animations will still all get sampled in the correct order and
    // dependencies will be appropriately resolved.
    sampleTime = std::max(nextMilestone.mTime, sampleTime);

    for (uint32_t i = 0; i < length; ++i) {
      SVGAnimationElement* elem = elements[i].get();
      MOZ_ASSERT(elem, "nullptr animation element in list");
      SMILTimeContainer* container = elem->GetTimeContainer();
      if (!container)
        // The container may be nullptr if the element has been detached from
        // its parent since registering a milestone.
        continue;

      SMILTimeValue containerTimeValue =
          container->ParentToContainerTime(sampleTime);
      if (!containerTimeValue.IsDefinite()) continue;

      // Clamp the converted container time to non-negative values.
      SMILTime containerTime =
          std::max<SMILTime>(0, containerTimeValue.GetMillis());

      if (nextMilestone.mIsEnd) {
        elem->TimedElement().SampleEndAt(containerTime);
      } else {
        elem->TimedElement().SampleAt(containerTime);
      }
    }
  }
}

/*static*/
void SMILAnimationController::SampleTimedElement(
    SVGAnimationElement* aElement, TimeContainerHashtable* aActiveContainers) {
  SMILTimeContainer* timeContainer = aElement->GetTimeContainer();
  if (!timeContainer) return;

  // We'd like to call timeContainer->NeedsSample() here and skip all timed
  // elements that belong to paused time containers that don't need a sample,
  // but that doesn't work because we've already called Sample() on all the time
  // containers so the paused ones don't need a sample any more and they'll
  // return false.
  //
  // Instead we build up a hashmap of active time containers during the previous
  // step (SampleTimeContainer) and then test here if the container for this
  // timed element is in the list.
  if (!aActiveContainers->GetEntry(timeContainer)) return;

  SMILTime containerTime = timeContainer->GetCurrentTimeAsSMILTime();

  MOZ_ASSERT(!timeContainer->IsSeeking(),
             "Doing a regular sample but the time container is still seeking");
  aElement->TimedElement().SampleAt(containerTime);
}

/*static*/
void SMILAnimationController::AddAnimationToCompositorTable(
    SVGAnimationElement* aElement, SMILCompositorTable* aCompositorTable,
    bool& aStyleFlushNeeded) {
  // Add a compositor to the hash table if there's not already one there
  SMILTargetIdentifier key;
  if (!GetTargetIdentifierForAnimation(aElement, key))
    // Something's wrong/missing about animation's target; skip this animation
    return;

  SMILAnimationFunction& func = aElement->AnimationFunction();

  // Only add active animation functions. If there are no active animations
  // targeting an attribute, no compositor will be created and any previously
  // applied animations will be cleared.
  if (func.IsActiveOrFrozen()) {
    // Look up the compositor for our target, & add our animation function
    // to its list of animation functions.
    SMILCompositor* result = aCompositorTable->PutEntry(key);
    result->AddAnimationFunction(&func);

  } else if (func.HasChanged()) {
    // Look up the compositor for our target, and force it to skip the
    // "nothing's changed so don't bother compositing" optimization for this
    // sample. |func| is inactive, but it's probably *newly* inactive (since
    // it's got HasChanged() == true), so we need to make sure to recompose
    // its target.
    SMILCompositor* result = aCompositorTable->PutEntry(key);
    result->ToggleForceCompositing();

    // We've now made sure that |func|'s inactivity will be reflected as of
    // this sample. We need to clear its HasChanged() flag so that it won't
    // trigger this same clause in future samples (until it changes again).
    func.ClearHasChanged();
  }
  aStyleFlushNeeded |= func.ValueNeedsReparsingEverySample();
}

static inline bool IsTransformAttribute(int32_t aNamespaceID,
                                        nsAtom* aAttributeName) {
  return aNamespaceID == kNameSpaceID_None &&
         (aAttributeName == nsGkAtoms::transform ||
          aAttributeName == nsGkAtoms::patternTransform ||
          aAttributeName == nsGkAtoms::gradientTransform);
}

// Helper function that, given a SVGAnimationElement, looks up its target
// element & target attribute and populates a SMILTargetIdentifier
// for this target.
/*static*/
bool SMILAnimationController::GetTargetIdentifierForAnimation(
    SVGAnimationElement* aAnimElem, SMILTargetIdentifier& aResult) {
  // Look up target (animated) element
  Element* targetElem = aAnimElem->GetTargetElementContent();
  if (!targetElem)
    // Animation has no target elem -- skip it.
    return false;

  // Look up target (animated) attribute
  // SMILANIM section 3.1, attributeName may
  // have an XMLNS prefix to indicate the XML namespace.
  RefPtr<nsAtom> attributeName;
  int32_t attributeNamespaceID;
  if (!aAnimElem->GetTargetAttributeName(&attributeNamespaceID,
                                         getter_AddRefs(attributeName)))
    // Animation has no target attr -- skip it.
    return false;

  // animateTransform can only animate transforms, conversely transforms
  // can only be animated by animateTransform
  if (IsTransformAttribute(attributeNamespaceID, attributeName) !=
      (aAnimElem->IsSVGElement(nsGkAtoms::animateTransform)))
    return false;

  // Construct the key
  aResult.mElement = targetElem;
  aResult.mAttributeName = attributeName;
  aResult.mAttributeNamespaceID = attributeNamespaceID;

  return true;
}

bool SMILAnimationController::PreTraverse() {
  return PreTraverseInSubtree(nullptr);
}

bool SMILAnimationController::PreTraverseInSubtree(Element* aRoot) {
  MOZ_ASSERT(NS_IsMainThread());

  if (!mMightHavePendingStyleUpdates) {
    return false;
  }

  nsPresContext* context = mDocument->GetPresContext();
  if (!context) {
    return false;
  }

  bool foundElementsNeedingRestyle = false;
  for (SVGAnimationElement* animElement : mAnimationElementTable.Keys()) {
    SMILTargetIdentifier key;
    if (!GetTargetIdentifierForAnimation(animElement, key)) {
      // Something's wrong/missing about animation's target; skip this animation
      continue;
    }

    // Ignore restyles that aren't in the flattened tree subtree rooted at
    // aRoot.
    if (aRoot && !nsContentUtils::ContentIsFlattenedTreeDescendantOf(
                     key.mElement, aRoot)) {
      continue;
    }

    context->RestyleManager()->PostRestyleEventForAnimations(
        key.mElement, PseudoStyleType::NotPseudo, RestyleHint::RESTYLE_SMIL);

    foundElementsNeedingRestyle = true;
  }

  // Only clear the mMightHavePendingStyleUpdates flag if we definitely posted
  // all restyles.
  if (!aRoot) {
    mMightHavePendingStyleUpdates = false;
  }

  return foundElementsNeedingRestyle;
}

//----------------------------------------------------------------------
// Add/remove child time containers

nsresult SMILAnimationController::AddChild(SMILTimeContainer& aChild) {
  const bool wasEmpty = mChildContainerTable.IsEmpty();
  TimeContainerPtrKey* key = mChildContainerTable.PutEntry(&aChild);
  NS_ENSURE_TRUE(key, NS_ERROR_OUT_OF_MEMORY);
  if (wasEmpty) {
    UpdateSampling();
  }
  return NS_OK;
}

void SMILAnimationController::RemoveChild(SMILTimeContainer& aChild) {
  mChildContainerTable.RemoveEntry(&aChild);
  if (mChildContainerTable.IsEmpty()) {
    UpdateSampling();
  }
}

// Helper method
nsRefreshDriver* SMILAnimationController::GetRefreshDriver() {
  if (!mDocument) {
    NS_ERROR("Requesting refresh driver after document has disconnected!");
    return nullptr;
  }

  nsPresContext* context = mDocument->GetPresContext();
  return context ? context->RefreshDriver() : nullptr;
}

void SMILAnimationController::FlagDocumentNeedsFlush() {
  if (PresShell* presShell = mDocument->GetPresShell()) {
    presShell->SetNeedStyleFlush();
  }
}

}  // namespace mozilla