Source code
Revision control
Copy as Markdown
Other Tools
/* -*- 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
#include "SMILTimedElement.h"
#include "mozilla/AutoRestore.h"
#include "mozilla/ContentEvents.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/EventDispatcher.h"
#include "mozilla/SMILAnimationFunction.h"
#include "mozilla/SMILInstanceTime.h"
#include "mozilla/SMILParserUtils.h"
#include "mozilla/SMILTimeContainer.h"
#include "mozilla/SMILTimeValue.h"
#include "mozilla/SMILTimeValueSpec.h"
#include "mozilla/dom/DocumentInlines.h"
#include "mozilla/dom/SVGAnimationElement.h"
#include "nsAttrValueInlines.h"
#include "nsGkAtoms.h"
#include "nsReadableUtils.h"
#include "nsMathUtils.h"
#include "nsThreadUtils.h"
#include "prdtoa.h"
#include "prtime.h"
#include "nsString.h"
#include "nsCharSeparatedTokenizer.h"
#include <algorithm>
using namespace mozilla::dom;
namespace mozilla {
//----------------------------------------------------------------------
// Helper class: InstanceTimeComparator
// Upon inserting an instance time into one of our instance time lists we assign
// it a serial number. This allows us to sort the instance times in such a way
// that where we have several equal instance times, the ones added later will
// sort later. This means that when we call UpdateCurrentInterval during the
// waiting state we won't unnecessarily change the begin instance.
//
// The serial number also means that every instance time has an unambiguous
// position in the array so we can use RemoveElementSorted and the like.
bool SMILTimedElement::InstanceTimeComparator::Equals(
const SMILInstanceTime* aElem1, const SMILInstanceTime* aElem2) const {
MOZ_ASSERT(aElem1 && aElem2, "Trying to compare null instance time pointers");
MOZ_ASSERT(aElem1->Serial() && aElem2->Serial(),
"Instance times have not been assigned serial numbers");
MOZ_ASSERT(aElem1 == aElem2 || aElem1->Serial() != aElem2->Serial(),
"Serial numbers are not unique");
return aElem1->Serial() == aElem2->Serial();
}
bool SMILTimedElement::InstanceTimeComparator::LessThan(
const SMILInstanceTime* aElem1, const SMILInstanceTime* aElem2) const {
MOZ_ASSERT(aElem1 && aElem2, "Trying to compare null instance time pointers");
MOZ_ASSERT(aElem1->Serial() && aElem2->Serial(),
"Instance times have not been assigned serial numbers");
int8_t cmp = aElem1->Time().CompareTo(aElem2->Time());
return cmp == 0 ? aElem1->Serial() < aElem2->Serial() : cmp < 0;
}
//----------------------------------------------------------------------
// Helper class: AsyncTimeEventRunner
namespace {
class AsyncTimeEventRunner : public Runnable {
protected:
const RefPtr<nsIContent> mTarget;
EventMessage mMsg;
int32_t mDetail;
public:
AsyncTimeEventRunner(nsIContent* aTarget, EventMessage aMsg, int32_t aDetail)
: mozilla::Runnable("AsyncTimeEventRunner"),
mTarget(aTarget),
mMsg(aMsg),
mDetail(aDetail) {}
MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override {
nsPIDOMWindowInner* inner = mTarget->OwnerDoc()->GetInnerWindow();
if (inner && !inner->HasSMILTimeEventListeners()) {
return NS_OK;
}
InternalSMILTimeEvent event(true, mMsg);
event.mDetail = mDetail;
RefPtr<nsPresContext> context = nullptr;
Document* doc = mTarget->GetComposedDoc();
if (doc) {
context = doc->GetPresContext();
}
return EventDispatcher::Dispatch(mTarget, context, &event);
}
};
} // namespace
//----------------------------------------------------------------------
// Helper class: AutoIntervalUpdateBatcher
// Stack-based helper class to set the mDeferIntervalUpdates flag on an
// SMILTimedElement and perform the UpdateCurrentInterval when the object is
// destroyed.
//
// If several of these objects are allocated on the stack, the update will not
// be performed until the last object for a given SMILTimedElement is
// destroyed.
class MOZ_STACK_CLASS SMILTimedElement::AutoIntervalUpdateBatcher {
public:
explicit AutoIntervalUpdateBatcher(SMILTimedElement& aTimedElement)
: mTimedElement(aTimedElement),
mDidSetFlag(!aTimedElement.mDeferIntervalUpdates) {
mTimedElement.mDeferIntervalUpdates = true;
}
~AutoIntervalUpdateBatcher() {
if (!mDidSetFlag) return;
mTimedElement.mDeferIntervalUpdates = false;
if (mTimedElement.mDoDeferredUpdate) {
mTimedElement.mDoDeferredUpdate = false;
mTimedElement.UpdateCurrentInterval();
}
}
private:
SMILTimedElement& mTimedElement;
bool mDidSetFlag;
};
//----------------------------------------------------------------------
// Helper class: AutoIntervalUpdater
// Stack-based helper class to call UpdateCurrentInterval when it is destroyed
// which helps avoid bugs where we forget to call UpdateCurrentInterval in the
// case of early returns (e.g. due to parse errors).
//
// This can be safely used in conjunction with AutoIntervalUpdateBatcher; any
// calls to UpdateCurrentInterval made by this class will simply be deferred if
// there is an AutoIntervalUpdateBatcher on the stack.
class MOZ_STACK_CLASS SMILTimedElement::AutoIntervalUpdater {
public:
explicit AutoIntervalUpdater(SMILTimedElement& aTimedElement)
: mTimedElement(aTimedElement) {}
~AutoIntervalUpdater() { mTimedElement.UpdateCurrentInterval(); }
private:
SMILTimedElement& mTimedElement;
};
//----------------------------------------------------------------------
// Templated helper functions
// Selectively remove elements from an array of type
// nsTArray<RefPtr<SMILInstanceTime> > with O(n) performance.
template <class TestFunctor>
void SMILTimedElement::RemoveInstanceTimes(InstanceTimeList& aArray,
TestFunctor& aTest) {
InstanceTimeList newArray;
for (uint32_t i = 0; i < aArray.Length(); ++i) {
SMILInstanceTime* item = aArray[i].get();
if (aTest(item, i)) {
// As per bugs 665334 and 669225 we should be careful not to remove the
// instance time that corresponds to the previous interval's end time.
//
// Most functors supplied here fulfil this condition by checking if the
// instance time is marked as "ShouldPreserve" and if so, not deleting it.
//
// However, when filtering instance times, we sometimes need to drop even
// instance times marked as "ShouldPreserve". In that case we take special
// care not to delete the end instance time of the previous interval.
MOZ_ASSERT(!GetPreviousInterval() || item != GetPreviousInterval()->End(),
"Removing end instance time of previous interval");
item->Unlink();
} else {
newArray.AppendElement(item);
}
}
aArray = std::move(newArray);
}
//----------------------------------------------------------------------
// Static members
const nsAttrValue::EnumTable SMILTimedElement::sFillModeTable[] = {
{"remove", FILL_REMOVE}, {"freeze", FILL_FREEZE}, {nullptr, 0}};
const nsAttrValue::EnumTable SMILTimedElement::sRestartModeTable[] = {
{"always", RESTART_ALWAYS},
{"whenNotActive", RESTART_WHENNOTACTIVE},
{"never", RESTART_NEVER},
{nullptr, 0}};
// The thresholds at which point we start filtering intervals and instance times
// indiscriminately.
// See FilterIntervals and FilterInstanceTimes.
const uint8_t SMILTimedElement::sMaxNumIntervals = 20;
const uint8_t SMILTimedElement::sMaxNumInstanceTimes = 100;
// Detect if we arrive in some sort of undetected recursive syncbase dependency
// relationship
const uint8_t SMILTimedElement::sMaxUpdateIntervalRecursionDepth = 20;
//----------------------------------------------------------------------
// Ctor, dtor
SMILTimedElement::SMILTimedElement()
: mAnimationElement(nullptr),
mFillMode(FILL_REMOVE),
mRestartMode(RESTART_ALWAYS),
mInstanceSerialIndex(0),
mClient(nullptr),
mCurrentInterval(nullptr),
mCurrentRepeatIteration(0),
mPrevRegisteredMilestone(sMaxMilestone),
mElementState(STATE_STARTUP),
mSeekState(SEEK_NOT_SEEKING),
mDeferIntervalUpdates(false),
mDoDeferredUpdate(false),
mIsDisabled(false),
mDeleteCount(0),
mUpdateIntervalRecursionDepth(0) {
mSimpleDur.SetIndefinite();
mMin = SMILTimeValue::Zero();
mMax.SetIndefinite();
}
SMILTimedElement::~SMILTimedElement() {
// Unlink all instance times from dependent intervals
for (RefPtr<SMILInstanceTime>& instance : mBeginInstances) {
instance->Unlink();
}
mBeginInstances.Clear();
for (RefPtr<SMILInstanceTime>& instance : mEndInstances) {
instance->Unlink();
}
mEndInstances.Clear();
// Notify anyone listening to our intervals that they're gone
// (We shouldn't get any callbacks from this because all our instance times
// are now disassociated with any intervals)
ClearIntervals();
// The following assertions are important in their own right (for checking
// correct behavior) but also because AutoIntervalUpdateBatcher holds pointers
// to class so if they fail there's the possibility we might have dangling
// pointers.
MOZ_ASSERT(!mDeferIntervalUpdates,
"Interval updates should no longer be blocked when an "
"SMILTimedElement disappears");
MOZ_ASSERT(!mDoDeferredUpdate,
"There should no longer be any pending updates when an "
"SMILTimedElement disappears");
}
void SMILTimedElement::SetAnimationElement(SVGAnimationElement* aElement) {
MOZ_ASSERT(aElement, "NULL owner element");
MOZ_ASSERT(!mAnimationElement, "Re-setting owner");
mAnimationElement = aElement;
}
SMILTimeContainer* SMILTimedElement::GetTimeContainer() {
return mAnimationElement ? mAnimationElement->GetTimeContainer() : nullptr;
}
dom::Element* SMILTimedElement::GetTargetElement() {
return mAnimationElement ? mAnimationElement->GetTargetElementContent()
: nullptr;
}
//----------------------------------------------------------------------
// ElementTimeControl methods
//
// The definition of the ElementTimeControl interface differs between SMIL
// Animation and SVG 1.1. In SMIL Animation all methods have a void return
// type and the new instance time is simply added to the list and restart
// semantics are applied as with any other instance time. In the SVG definition
// the methods return a bool depending on the restart mode.
//
// This inconsistency has now been addressed by an erratum in SVG 1.1:
//
//
// which favours the definition in SMIL, i.e. instance times are just added
// without first checking the restart mode.
nsresult SMILTimedElement::BeginElementAt(double aOffsetSeconds) {
SMILTimeContainer* container = GetTimeContainer();
if (!container) return NS_ERROR_FAILURE;
SMILTime currentTime = container->GetCurrentTimeAsSMILTime();
AddInstanceTimeFromCurrentTime(currentTime, aOffsetSeconds, true);
return NS_OK;
}
nsresult SMILTimedElement::EndElementAt(double aOffsetSeconds) {
SMILTimeContainer* container = GetTimeContainer();
if (!container) return NS_ERROR_FAILURE;
SMILTime currentTime = container->GetCurrentTimeAsSMILTime();
AddInstanceTimeFromCurrentTime(currentTime, aOffsetSeconds, false);
return NS_OK;
}
//----------------------------------------------------------------------
// SVGAnimationElement methods
SMILTimeValue SMILTimedElement::GetStartTime() const {
return mElementState == STATE_WAITING || mElementState == STATE_ACTIVE
? mCurrentInterval->Begin()->Time()
: SMILTimeValue();
}
//----------------------------------------------------------------------
// Hyperlinking support
SMILTimeValue SMILTimedElement::GetHyperlinkTime() const {
SMILTimeValue hyperlinkTime; // Default ctor creates unresolved time
if (mElementState == STATE_ACTIVE) {
hyperlinkTime = mCurrentInterval->Begin()->Time();
} else if (!mBeginInstances.IsEmpty()) {
hyperlinkTime = mBeginInstances[0]->Time();
}
return hyperlinkTime;
}
//----------------------------------------------------------------------
// SMILTimedElement
void SMILTimedElement::AddInstanceTime(SMILInstanceTime* aInstanceTime,
bool aIsBegin) {
MOZ_ASSERT(aInstanceTime, "Attempting to add null instance time");
// Event-sensitivity: If an element is not active (but the parent time
// container is), then events are only handled for begin specifications.
if (mElementState != STATE_ACTIVE && !aIsBegin &&
aInstanceTime->IsDynamic()) {
// No need to call Unlink here--dynamic instance times shouldn't be linked
// to anything that's going to miss them
MOZ_ASSERT(!aInstanceTime->GetBaseInterval(),
"Dynamic instance time has a base interval--we probably need "
"to unlink it if we're not going to use it");
return;
}
aInstanceTime->SetSerial(++mInstanceSerialIndex);
InstanceTimeList& instanceList = aIsBegin ? mBeginInstances : mEndInstances;
RefPtr<SMILInstanceTime>* inserted =
instanceList.InsertElementSorted(aInstanceTime, InstanceTimeComparator());
if (!inserted) {
NS_WARNING("Insufficient memory to insert instance time");
return;
}
UpdateCurrentInterval();
}
void SMILTimedElement::UpdateInstanceTime(SMILInstanceTime* aInstanceTime,
SMILTimeValue& aUpdatedTime,
bool aIsBegin) {
MOZ_ASSERT(aInstanceTime, "Attempting to update null instance time");
// The reason we update the time here and not in the SMILTimeValueSpec is
// that it means we *could* re-sort more efficiently by doing a sorted remove
// and insert but currently this doesn't seem to be necessary given how
// infrequently we get these change notices.
aInstanceTime->DependentUpdate(aUpdatedTime);
InstanceTimeList& instanceList = aIsBegin ? mBeginInstances : mEndInstances;
instanceList.Sort(InstanceTimeComparator());
// Generally speaking, UpdateCurrentInterval makes changes to the current
// interval and sends changes notices itself. However, in this case because
// instance times are shared between the instance time list and the intervals
// we are effectively changing the current interval outside
// UpdateCurrentInterval so we need to explicitly signal that we've made
// a change.
//
// This wouldn't be necessary if we cloned instance times on adding them to
// the current interval but this introduces other complications (particularly
// detecting which instance time is being used to define the begin of the
// current interval when doing a Reset).
bool changedCurrentInterval =
mCurrentInterval && (mCurrentInterval->Begin() == aInstanceTime ||
mCurrentInterval->End() == aInstanceTime);
UpdateCurrentInterval(changedCurrentInterval);
}
void SMILTimedElement::RemoveInstanceTime(SMILInstanceTime* aInstanceTime,
bool aIsBegin) {
MOZ_ASSERT(aInstanceTime, "Attempting to remove null instance time");
// If the instance time should be kept (because it is or was the fixed end
// point of an interval) then just disassociate it from the creator.
if (aInstanceTime->ShouldPreserve()) {
aInstanceTime->Unlink();
return;
}
InstanceTimeList& instanceList = aIsBegin ? mBeginInstances : mEndInstances;
mozilla::DebugOnly<bool> found =
instanceList.RemoveElementSorted(aInstanceTime, InstanceTimeComparator());
MOZ_ASSERT(found, "Couldn't find instance time to delete");
UpdateCurrentInterval();
}
namespace {
class MOZ_STACK_CLASS RemoveByCreator {
public:
explicit RemoveByCreator(const SMILTimeValueSpec* aCreator)
: mCreator(aCreator) {}
bool operator()(SMILInstanceTime* aInstanceTime, uint32_t /*aIndex*/) {
if (aInstanceTime->GetCreator() != mCreator) return false;
// If the instance time should be kept (because it is or was the fixed end
// point of an interval) then just disassociate it from the creator.
if (aInstanceTime->ShouldPreserve()) {
aInstanceTime->Unlink();
return false;
}
return true;
}
private:
const SMILTimeValueSpec* mCreator;
};
} // namespace
void SMILTimedElement::RemoveInstanceTimesForCreator(
const SMILTimeValueSpec* aCreator, bool aIsBegin) {
MOZ_ASSERT(aCreator, "Creator not set");
InstanceTimeList& instances = aIsBegin ? mBeginInstances : mEndInstances;
RemoveByCreator removeByCreator(aCreator);
RemoveInstanceTimes(instances, removeByCreator);
UpdateCurrentInterval();
}
void SMILTimedElement::SetTimeClient(SMILAnimationFunction* aClient) {
//
// No need to check for nullptr. A nullptr parameter simply means to remove
// the previous client which we do by setting to nullptr anyway.
//
mClient = aClient;
}
void SMILTimedElement::SampleAt(SMILTime aContainerTime) {
if (mIsDisabled) return;
// Milestones are cleared before a sample
mPrevRegisteredMilestone = sMaxMilestone;
DoSampleAt(aContainerTime, false);
}
void SMILTimedElement::SampleEndAt(SMILTime aContainerTime) {
if (mIsDisabled) return;
// Milestones are cleared before a sample
mPrevRegisteredMilestone = sMaxMilestone;
// If the current interval changes, we don't bother trying to remove any old
// milestones we'd registered. So it's possible to get a call here to end an
// interval at a time that no longer reflects the end of the current interval.
//
// For now we just check that we're actually in an interval but note that the
// initial sample we use to initialise the model is an end sample. This is
// because we want to resolve all the instance times before committing to an
// initial interval. Therefore an end sample from the startup state is also
// acceptable.
if (mElementState == STATE_ACTIVE || mElementState == STATE_STARTUP) {
DoSampleAt(aContainerTime, true); // End sample
} else {
// Even if this was an unnecessary milestone sample we want to be sure that
// our next real milestone is registered.
RegisterMilestone();
}
}
void SMILTimedElement::DoSampleAt(SMILTime aContainerTime, bool aEndOnly) {
MOZ_ASSERT(mAnimationElement,
"Got sample before being registered with an animation element");
MOZ_ASSERT(GetTimeContainer(),
"Got sample without being registered with a time container");
// This could probably happen if we later implement externalResourcesRequired
// start) we transfer a node from another document fragment that has already
// started. In such a case we might receive milestone samples registered with
// the already active container.
if (GetTimeContainer()->IsPausedByType(SMILTimeContainer::PAUSE_BEGIN))
return;
// We use an end-sample to start animation since an end-sample lets us
// tentatively create an interval without committing to it (by transitioning
// to the ACTIVE state) and this is necessary because we might have
// dependencies on other animations that are yet to start. After these
// other animations start, it may be necessary to revise our initial interval.
//
// However, sometimes instead of an end-sample we can get a regular sample
// during STARTUP state. This can happen, for example, if we register
// a milestone before time t=0 and are then re-bound to the tree (which sends
// us back to the STARTUP state). In such a case we should just ignore the
// sample and wait for our real initial sample which will be an end-sample.
if (mElementState == STATE_STARTUP && !aEndOnly) return;
bool finishedSeek = false;
if (GetTimeContainer()->IsSeeking() && mSeekState == SEEK_NOT_SEEKING) {
mSeekState = mElementState == STATE_ACTIVE ? SEEK_FORWARD_FROM_ACTIVE
: SEEK_FORWARD_FROM_INACTIVE;
} else if (mSeekState != SEEK_NOT_SEEKING &&
!GetTimeContainer()->IsSeeking()) {
finishedSeek = true;
}
bool stateChanged;
SMILTimeValue sampleTime(aContainerTime);
do {
#ifdef DEBUG
// Check invariant
if (mElementState == STATE_STARTUP || mElementState == STATE_POSTACTIVE) {
MOZ_ASSERT(!mCurrentInterval,
"Shouldn't have current interval in startup or postactive "
"states");
} else {
MOZ_ASSERT(mCurrentInterval,
"Should have current interval in waiting and active states");
}
#endif
stateChanged = false;
switch (mElementState) {
case STATE_STARTUP: {
SMILInterval firstInterval;
mElementState =
GetNextInterval(nullptr, nullptr, nullptr, firstInterval)
? STATE_WAITING
: STATE_POSTACTIVE;
stateChanged = true;
if (mElementState == STATE_WAITING) {
mCurrentInterval = MakeUnique<SMILInterval>(firstInterval);
NotifyNewInterval();
}
} break;
case STATE_WAITING: {
if (mCurrentInterval->Begin()->Time() <= sampleTime) {
mElementState = STATE_ACTIVE;
mCurrentInterval->FixBegin();
if (mClient) {
mClient->Activate(mCurrentInterval->Begin()->Time().GetMillis());
}
if (mSeekState == SEEK_NOT_SEEKING) {
FireTimeEventAsync(eSMILBeginEvent, 0);
}
if (HasPlayed()) {
Reset(); // Apply restart behaviour
// The call to Reset() may mean that the end point of our current
// interval should be changed and so we should update the interval
// now. However, calling UpdateCurrentInterval could result in the
// interval getting deleted (perhaps through some web of syncbase
// dependencies) therefore we make updating the interval the last
// thing we do. There is no guarantee that mCurrentInterval is set
// after this.
UpdateCurrentInterval();
}
stateChanged = true;
}
} break;
case STATE_ACTIVE: {
// Ending early will change the interval but we don't notify dependents
// of the change until we have closed off the current interval (since we
// don't want dependencies to un-end our early end).
bool didApplyEarlyEnd = ApplyEarlyEnd(sampleTime);
if (mCurrentInterval->End()->Time() <= sampleTime) {
SMILInterval newInterval;
mElementState = GetNextInterval(mCurrentInterval.get(), nullptr,
nullptr, newInterval)
? STATE_WAITING
: STATE_POSTACTIVE;
if (mClient) {
mClient->Inactivate(mFillMode == FILL_FREEZE);
}
mCurrentInterval->FixEnd();
if (mSeekState == SEEK_NOT_SEEKING) {
FireTimeEventAsync(eSMILEndEvent, 0);
}
mCurrentRepeatIteration = 0;
mOldIntervals.AppendElement(std::move(mCurrentInterval));
SampleFillValue();
if (mElementState == STATE_WAITING) {
mCurrentInterval = MakeUnique<SMILInterval>(newInterval);
}
// We are now in a consistent state to dispatch notifications
if (didApplyEarlyEnd) {
NotifyChangedInterval(mOldIntervals.LastElement().get(), false,
true);
}
if (mElementState == STATE_WAITING) {
NotifyNewInterval();
}
FilterHistory();
stateChanged = true;
} else if (mCurrentInterval->Begin()->Time() <= sampleTime) {
MOZ_ASSERT(!didApplyEarlyEnd, "We got an early end, but didn't end");
SMILTime beginTime = mCurrentInterval->Begin()->Time().GetMillis();
SMILTime activeTime = aContainerTime - beginTime;
// The 'min' attribute can cause the active interval to be longer than
// the 'repeating interval'.
// In that extended period we apply the fill mode.
if (GetRepeatDuration() <= SMILTimeValue(activeTime)) {
if (mClient && mClient->IsActive()) {
mClient->Inactivate(mFillMode == FILL_FREEZE);
}
SampleFillValue();
} else {
SampleSimpleTime(activeTime);
// We register our repeat times as milestones (except when we're
// seeking) so we should get a sample at exactly the time we repeat.
// (And even when we are seeking we want to update
// mCurrentRepeatIteration so we do that first before testing the
// seek state.)
uint32_t prevRepeatIteration = mCurrentRepeatIteration;
if (ActiveTimeToSimpleTime(activeTime, mCurrentRepeatIteration) ==
0 &&
mCurrentRepeatIteration != prevRepeatIteration &&
mCurrentRepeatIteration && mSeekState == SEEK_NOT_SEEKING) {
FireTimeEventAsync(eSMILRepeatEvent,
static_cast<int32_t>(mCurrentRepeatIteration));
}
}
}
// Otherwise |sampleTime| is *before* the current interval. That
// normally doesn't happen but can happen if we get a stray milestone
// sample (e.g. if we registered a milestone with a time container that
// later got re-attached as a child of a more advanced time container).
// In that case we should just ignore the sample.
} break;
case STATE_POSTACTIVE:
break;
}
// Generally we continue driving the state machine so long as we have
// changed state. However, for end samples we only drive the state machine
// as far as the waiting or postactive state because we don't want to commit
// to any new interval (by transitioning to the active state) until all the
// end samples have finished and we then have complete information about the
// available instance times upon which to base our next interval.
} while (stateChanged && (!aEndOnly || (mElementState != STATE_WAITING &&
mElementState != STATE_POSTACTIVE)));
if (finishedSeek) {
DoPostSeek();
}
RegisterMilestone();
}
void SMILTimedElement::HandleContainerTimeChange() {
// In future we could possibly introduce a separate change notice for time
// container changes and only notify those dependents who live in other time
// containers. For now we don't bother because when we re-resolve the time in
// the SMILTimeValueSpec we'll check if anything has changed and if not, we
// won't go any further.
if (mElementState == STATE_WAITING || mElementState == STATE_ACTIVE) {
NotifyChangedInterval(mCurrentInterval.get(), false, false);
}
}
namespace {
bool RemoveNonDynamic(SMILInstanceTime* aInstanceTime) {
// Generally dynamically-generated instance times (DOM calls, event-based
// times) are not associated with their creator SMILTimeValueSpec since
// they may outlive them.
MOZ_ASSERT(!aInstanceTime->IsDynamic() || !aInstanceTime->GetCreator(),
"Dynamic instance time should be unlinked from its creator");
return !aInstanceTime->IsDynamic() && !aInstanceTime->ShouldPreserve();
}
} // namespace
void SMILTimedElement::Rewind() {
MOZ_ASSERT(mAnimationElement,
"Got rewind request before being attached to an animation "
"element");
// It's possible to get a rewind request whilst we're already in the middle of
// a backwards seek. This can happen when we're performing tree surgery and
// seeking containers at the same time because we can end up requesting
// a local rewind on an element after binding it to a new container and then
// performing a rewind on that container as a whole without sampling in
// between.
//
// However, it should currently be impossible to get a rewind in the middle of
// a forwards seek since forwards seeks are detected and processed within the
// same (re)sample.
if (mSeekState == SEEK_NOT_SEEKING) {
mSeekState = mElementState == STATE_ACTIVE ? SEEK_BACKWARD_FROM_ACTIVE
: SEEK_BACKWARD_FROM_INACTIVE;
}
MOZ_ASSERT(mSeekState == SEEK_BACKWARD_FROM_INACTIVE ||
mSeekState == SEEK_BACKWARD_FROM_ACTIVE,
"Rewind in the middle of a forwards seek?");
ClearTimingState(RemoveNonDynamic);
RebuildTimingState(RemoveNonDynamic);
MOZ_ASSERT(!mCurrentInterval, "Current interval is set at end of rewind");
}
namespace {
bool RemoveAll(SMILInstanceTime* aInstanceTime) { return true; }
} // namespace
bool SMILTimedElement::SetIsDisabled(bool aIsDisabled) {
if (mIsDisabled == aIsDisabled) return false;
if (aIsDisabled) {
mIsDisabled = true;
ClearTimingState(RemoveAll);
} else {
RebuildTimingState(RemoveAll);
mIsDisabled = false;
}
return true;
}
namespace {
bool RemoveNonDOM(SMILInstanceTime* aInstanceTime) {
return !aInstanceTime->FromDOM() && !aInstanceTime->ShouldPreserve();
}
} // namespace
bool SMILTimedElement::SetAttr(nsAtom* aAttribute, const nsAString& aValue,
nsAttrValue& aResult, Element& aContextElement,
nsresult* aParseResult) {
bool foundMatch = true;
nsresult parseResult = NS_OK;
if (aAttribute == nsGkAtoms::begin) {
parseResult = SetBeginSpec(aValue, aContextElement, RemoveNonDOM);
} else if (aAttribute == nsGkAtoms::dur) {
parseResult = SetSimpleDuration(aValue);
} else if (aAttribute == nsGkAtoms::end) {
parseResult = SetEndSpec(aValue, aContextElement, RemoveNonDOM);
} else if (aAttribute == nsGkAtoms::fill) {
parseResult = SetFillMode(aValue);
} else if (aAttribute == nsGkAtoms::max) {
parseResult = SetMax(aValue);
} else if (aAttribute == nsGkAtoms::min) {
parseResult = SetMin(aValue);
} else if (aAttribute == nsGkAtoms::repeatCount) {
parseResult = SetRepeatCount(aValue);
} else if (aAttribute == nsGkAtoms::repeatDur) {
parseResult = SetRepeatDur(aValue);
} else if (aAttribute == nsGkAtoms::restart) {
parseResult = SetRestart(aValue);
} else {
foundMatch = false;
}
if (foundMatch) {
aResult.SetTo(aValue);
if (aParseResult) {
*aParseResult = parseResult;
}
}
return foundMatch;
}
bool SMILTimedElement::UnsetAttr(nsAtom* aAttribute) {
bool foundMatch = true;
if (aAttribute == nsGkAtoms::begin) {
UnsetBeginSpec(RemoveNonDOM);
} else if (aAttribute == nsGkAtoms::dur) {
UnsetSimpleDuration();
} else if (aAttribute == nsGkAtoms::end) {
UnsetEndSpec(RemoveNonDOM);
} else if (aAttribute == nsGkAtoms::fill) {
UnsetFillMode();
} else if (aAttribute == nsGkAtoms::max) {
UnsetMax();
} else if (aAttribute == nsGkAtoms::min) {
UnsetMin();
} else if (aAttribute == nsGkAtoms::repeatCount) {
UnsetRepeatCount();
} else if (aAttribute == nsGkAtoms::repeatDur) {
UnsetRepeatDur();
} else if (aAttribute == nsGkAtoms::restart) {
UnsetRestart();
} else {
foundMatch = false;
}
return foundMatch;
}
//----------------------------------------------------------------------
// Setters and unsetters
nsresult SMILTimedElement::SetBeginSpec(const nsAString& aBeginSpec,
Element& aContextElement,
RemovalTestFunction aRemove) {
return SetBeginOrEndSpec(aBeginSpec, aContextElement, true /*isBegin*/,
aRemove);
}
void SMILTimedElement::UnsetBeginSpec(RemovalTestFunction aRemove) {
ClearSpecs(mBeginSpecs, mBeginInstances, aRemove);
UpdateCurrentInterval();
}
nsresult SMILTimedElement::SetEndSpec(const nsAString& aEndSpec,
Element& aContextElement,
RemovalTestFunction aRemove) {
return SetBeginOrEndSpec(aEndSpec, aContextElement, false /*!isBegin*/,
aRemove);
}
void SMILTimedElement::UnsetEndSpec(RemovalTestFunction aRemove) {
ClearSpecs(mEndSpecs, mEndInstances, aRemove);
UpdateCurrentInterval();
}
nsresult SMILTimedElement::SetSimpleDuration(const nsAString& aDurSpec) {
// Update the current interval before returning
AutoIntervalUpdater updater(*this);
SMILTimeValue duration;
const nsAString& dur = SMILParserUtils::TrimWhitespace(aDurSpec);
// SVG-specific: "For SVG's animation elements, if "media" is specified, the
// attribute will be ignored." (SVG 1.1, section 19.2.6)
if (dur.EqualsLiteral("media") || dur.EqualsLiteral("indefinite")) {
duration.SetIndefinite();
} else {
if (!SMILParserUtils::ParseClockValue(
dur, SMILTimeValue::Rounding::EnsureNonZero, &duration) ||
duration.IsZero()) {
mSimpleDur.SetIndefinite();
return NS_ERROR_FAILURE;
}
}
// mSimpleDur should never be unresolved. ParseClockValue will either set
// duration to resolved or will return false.
MOZ_ASSERT(duration.IsResolved(), "Setting unresolved simple duration");
mSimpleDur = duration;
return NS_OK;
}
void SMILTimedElement::UnsetSimpleDuration() {
mSimpleDur.SetIndefinite();
UpdateCurrentInterval();
}
nsresult SMILTimedElement::SetMin(const nsAString& aMinSpec) {
// Update the current interval before returning
AutoIntervalUpdater updater(*this);
SMILTimeValue duration;
const nsAString& min = SMILParserUtils::TrimWhitespace(aMinSpec);
if (min.EqualsLiteral("media")) {
duration = SMILTimeValue::Zero();
} else {
if (!SMILParserUtils::ParseClockValue(min, SMILTimeValue::Rounding::Nearest,
&duration)) {
mMin = SMILTimeValue::Zero();
return NS_ERROR_FAILURE;
}
}
MOZ_ASSERT(duration.GetMillis() >= 0L, "Invalid duration");
mMin = duration;
return NS_OK;
}
void SMILTimedElement::UnsetMin() {
mMin = SMILTimeValue::Zero();
UpdateCurrentInterval();
}
nsresult SMILTimedElement::SetMax(const nsAString& aMaxSpec) {
// Update the current interval before returning
AutoIntervalUpdater updater(*this);
SMILTimeValue duration;
const nsAString& max = SMILParserUtils::TrimWhitespace(aMaxSpec);
if (max.EqualsLiteral("media") || max.EqualsLiteral("indefinite")) {
duration.SetIndefinite();
} else {
if (!SMILParserUtils::ParseClockValue(
max, SMILTimeValue::Rounding::EnsureNonZero, &duration) ||
duration.IsZero()) {
mMax.SetIndefinite();
return NS_ERROR_FAILURE;
}
MOZ_ASSERT(duration.GetMillis() > 0L, "Invalid duration");
}
mMax = duration;
return NS_OK;
}
void SMILTimedElement::UnsetMax() {
mMax.SetIndefinite();
UpdateCurrentInterval();
}
nsresult SMILTimedElement::SetRestart(const nsAString& aRestartSpec) {
nsAttrValue temp;
bool parseResult = temp.ParseEnumValue(aRestartSpec, sRestartModeTable, true);
mRestartMode =
parseResult ? SMILRestartMode(temp.GetEnumValue()) : RESTART_ALWAYS;
UpdateCurrentInterval();
return parseResult ? NS_OK : NS_ERROR_FAILURE;
}
void SMILTimedElement::UnsetRestart() {
mRestartMode = RESTART_ALWAYS;
UpdateCurrentInterval();
}
nsresult SMILTimedElement::SetRepeatCount(const nsAString& aRepeatCountSpec) {
// Update the current interval before returning
AutoIntervalUpdater updater(*this);
SMILRepeatCount newRepeatCount;
if (SMILParserUtils::ParseRepeatCount(aRepeatCountSpec, newRepeatCount)) {
mRepeatCount = newRepeatCount;
return NS_OK;
}
mRepeatCount.Unset();
return NS_ERROR_FAILURE;
}
void SMILTimedElement::UnsetRepeatCount() {
mRepeatCount.Unset();
UpdateCurrentInterval();
}
nsresult SMILTimedElement::SetRepeatDur(const nsAString& aRepeatDurSpec) {
// Update the current interval before returning
AutoIntervalUpdater updater(*this);
SMILTimeValue duration;
const nsAString& repeatDur = SMILParserUtils::TrimWhitespace(aRepeatDurSpec);
if (repeatDur.EqualsLiteral("indefinite")) {
duration.SetIndefinite();
} else {
if (!SMILParserUtils::ParseClockValue(
repeatDur, SMILTimeValue::Rounding::EnsureNonZero, &duration)) {
mRepeatDur.SetUnresolved();
return NS_ERROR_FAILURE;
}
}
mRepeatDur = duration;
return NS_OK;
}
void SMILTimedElement::UnsetRepeatDur() {
mRepeatDur.SetUnresolved();
UpdateCurrentInterval();
}
nsresult SMILTimedElement::SetFillMode(const nsAString& aFillModeSpec) {
uint16_t previousFillMode = mFillMode;
nsAttrValue temp;
bool parseResult = temp.ParseEnumValue(aFillModeSpec, sFillModeTable, true);
mFillMode = parseResult ? SMILFillMode(temp.GetEnumValue()) : FILL_REMOVE;
// Update fill mode of client
if (mFillMode != previousFillMode && HasClientInFillRange()) {
mClient->Inactivate(mFillMode == FILL_FREEZE);
SampleFillValue();
}
return parseResult ? NS_OK : NS_ERROR_FAILURE;
}
void SMILTimedElement::UnsetFillMode() {
uint16_t previousFillMode = mFillMode;
mFillMode = FILL_REMOVE;
if (previousFillMode == FILL_FREEZE && HasClientInFillRange()) {
mClient->Inactivate(false);
}
}
void SMILTimedElement::AddDependent(SMILTimeValueSpec& aDependent) {
// There's probably no harm in attempting to register a dependent
// SMILTimeValueSpec twice, but we're not expecting it to happen.
MOZ_ASSERT(!mTimeDependents.GetEntry(&aDependent),
"SMILTimeValueSpec is already registered as a dependency");
mTimeDependents.PutEntry(&aDependent);
// Add current interval. We could add historical intervals too but that would
// cause unpredictable results since some intervals may have been filtered.
// SMIL doesn't say what to do here so for simplicity and consistency we
// simply add the current interval if there is one.
//
// It's not necessary to call SyncPauseTime since we're dealing with
// historical instance times not newly added ones.
if (mCurrentInterval) {
aDependent.HandleNewInterval(*mCurrentInterval, GetTimeContainer());
}
}
void SMILTimedElement::RemoveDependent(SMILTimeValueSpec& aDependent) {
mTimeDependents.RemoveEntry(&aDependent);
}
bool SMILTimedElement::IsTimeDependent(const SMILTimedElement& aOther) const {
const SMILInstanceTime* thisBegin = GetEffectiveBeginInstance();
const SMILInstanceTime* otherBegin = aOther.GetEffectiveBeginInstance();
if (!thisBegin || !otherBegin) return false;
return thisBegin->IsDependentOn(*otherBegin);
}
void SMILTimedElement::BindToTree(Element& aContextElement) {
// Reset previously registered milestone since we may be registering with
// a different time container now.
mPrevRegisteredMilestone = sMaxMilestone;
// If we were already active then clear all our timing information and start
// afresh
if (mElementState != STATE_STARTUP) {
mSeekState = SEEK_NOT_SEEKING;
Rewind();
}
// Scope updateBatcher to last only for the ResolveReferences calls:
{
AutoIntervalUpdateBatcher updateBatcher(*this);
// Resolve references to other parts of the tree
for (UniquePtr<SMILTimeValueSpec>& beginSpec : mBeginSpecs) {
beginSpec->ResolveReferences(aContextElement);
}
for (UniquePtr<SMILTimeValueSpec>& endSpec : mEndSpecs) {
endSpec->ResolveReferences(aContextElement);
}
}
RegisterMilestone();
}
void SMILTimedElement::HandleTargetElementChange(Element* aNewTarget) {
AutoIntervalUpdateBatcher updateBatcher(*this);
for (UniquePtr<SMILTimeValueSpec>& beginSpec : mBeginSpecs) {
beginSpec->HandleTargetElementChange(aNewTarget);
}
for (UniquePtr<SMILTimeValueSpec>& endSpec : mEndSpecs) {
endSpec->HandleTargetElementChange(aNewTarget);
}
}
void SMILTimedElement::Traverse(nsCycleCollectionTraversalCallback* aCallback) {
for (UniquePtr<SMILTimeValueSpec>& beginSpec : mBeginSpecs) {
MOZ_ASSERT(beginSpec, "null SMILTimeValueSpec in list of begin specs");
beginSpec->Traverse(aCallback);
}
for (UniquePtr<SMILTimeValueSpec>& endSpec : mEndSpecs) {
MOZ_ASSERT(endSpec, "null SMILTimeValueSpec in list of end specs");
endSpec->Traverse(aCallback);
}
}
void SMILTimedElement::Unlink() {
AutoIntervalUpdateBatcher updateBatcher(*this);
// Remove dependencies on other elements
for (UniquePtr<SMILTimeValueSpec>& beginSpec : mBeginSpecs) {
MOZ_ASSERT(beginSpec, "null SMILTimeValueSpec in list of begin specs");
beginSpec->Unlink();
}
for (UniquePtr<SMILTimeValueSpec>& endSpec : mEndSpecs) {
MOZ_ASSERT(endSpec, "null SMILTimeValueSpec in list of end specs");
endSpec->Unlink();
}
ClearIntervals();
// Make sure we don't notify other elements of new intervals
mTimeDependents.Clear();
}
//----------------------------------------------------------------------
// Implementation helpers
nsresult SMILTimedElement::SetBeginOrEndSpec(const nsAString& aSpec,
Element& aContextElement,
bool aIsBegin,
RemovalTestFunction aRemove) {
TimeValueSpecList& timeSpecsList = aIsBegin ? mBeginSpecs : mEndSpecs;
InstanceTimeList& instances = aIsBegin ? mBeginInstances : mEndInstances;
ClearSpecs(timeSpecsList, instances, aRemove);
AutoIntervalUpdateBatcher updateBatcher(*this);
nsCharSeparatedTokenizer tokenizer(aSpec, ';');
if (!tokenizer.hasMoreTokens()) { // Empty list
return NS_ERROR_FAILURE;
}
bool hadFailure = false;
while (tokenizer.hasMoreTokens()) {
auto spec = MakeUnique<SMILTimeValueSpec>(*this, aIsBegin);
nsresult rv = spec->SetSpec(tokenizer.nextToken(), aContextElement);
if (NS_SUCCEEDED(rv)) {
timeSpecsList.AppendElement(std::move(spec));
} else {
hadFailure = true;
}
}
// The return value from this function is only used to determine if we should
// print a console message or not, so we return failure if we had one or more
// failures but we don't need to differentiate between different types of
// failures or the number of failures.
return hadFailure ? NS_ERROR_FAILURE : NS_OK;
}
namespace {
// Adaptor functor for RemoveInstanceTimes that allows us to use function
// pointers instead.
// Without this we'd have to either templatize ClearSpecs and all its callers
// or pass bool flags around to specify which removal function to use here.
class MOZ_STACK_CLASS RemoveByFunction {
public:
explicit RemoveByFunction(SMILTimedElement::RemovalTestFunction aFunction)
: mFunction(aFunction) {}
bool operator()(SMILInstanceTime* aInstanceTime, uint32_t /*aIndex*/) {
return mFunction(aInstanceTime);
}
private:
SMILTimedElement::RemovalTestFunction mFunction;
};
} // namespace
void SMILTimedElement::ClearSpecs(TimeValueSpecList& aSpecs,
InstanceTimeList& aInstances,
RemovalTestFunction aRemove) {
AutoIntervalUpdateBatcher updateBatcher(*this);
for (UniquePtr<SMILTimeValueSpec>& spec : aSpecs) {
spec->Unlink();
}
aSpecs.Clear();
RemoveByFunction removeByFunction(aRemove);
RemoveInstanceTimes(aInstances, removeByFunction);
}
void SMILTimedElement::ClearIntervals() {
if (mElementState != STATE_STARTUP) {
mElementState = STATE_POSTACTIVE;
}
mCurrentRepeatIteration = 0;
ResetCurrentInterval();
// Remove old intervals
for (int32_t i = mOldIntervals.Length() - 1; i >= 0; --i) {
mOldIntervals[i]->Unlink();
}
mOldIntervals.Clear();
}
bool SMILTimedElement::ApplyEarlyEnd(const SMILTimeValue& aSampleTime) {
// This should only be called within DoSampleAt as a helper function
MOZ_ASSERT(mElementState == STATE_ACTIVE,
"Unexpected state to try to apply an early end");
bool updated = false;
// Only apply an early end if we're not already ending.
if (mCurrentInterval->End()->Time() > aSampleTime) {
SMILInstanceTime* earlyEnd = CheckForEarlyEnd(aSampleTime);
if (earlyEnd) {
if (earlyEnd->IsDependent()) {
// Generate a new instance time for the early end since the
// existing instance time is part of some dependency chain that we
// don't want to participate in.
RefPtr<SMILInstanceTime> newEarlyEnd =
new SMILInstanceTime(earlyEnd->Time());
mCurrentInterval->SetEnd(*newEarlyEnd);
} else {
mCurrentInterval->SetEnd(*earlyEnd);
}
updated = true;
}
}
return updated;
}
namespace {
class MOZ_STACK_CLASS RemoveReset {
public:
explicit RemoveReset(const SMILInstanceTime* aCurrentIntervalBegin)
: mCurrentIntervalBegin(aCurrentIntervalBegin) {}
bool operator()(SMILInstanceTime* aInstanceTime, uint32_t /*aIndex*/) {
// SMIL 3.0 section 5.4.3, 'Resetting element state':
// Any instance times associated with past Event-values, Repeat-values,
// Accesskey-values or added via DOM method calls are removed from the
// dependent begin and end instance times lists. In effect, all events
// and DOM methods calls in the past are cleared. This does not apply to
// an instance time that defines the begin of the current interval.
return aInstanceTime->IsDynamic() && !aInstanceTime->ShouldPreserve() &&
(!mCurrentIntervalBegin || aInstanceTime != mCurrentIntervalBegin);
}
private:
const SMILInstanceTime* mCurrentIntervalBegin;
};
} // namespace
void SMILTimedElement::Reset() {
RemoveReset resetBegin(mCurrentInterval ? mCurrentInterval->Begin()
: nullptr);
RemoveInstanceTimes(mBeginInstances, resetBegin);
RemoveReset resetEnd(nullptr);
RemoveInstanceTimes(mEndInstances, resetEnd);
}
void SMILTimedElement::ClearTimingState(RemovalTestFunction aRemove) {
mElementState = STATE_STARTUP;
ClearIntervals();
UnsetBeginSpec(aRemove);
UnsetEndSpec(aRemove);
if (mClient) {
mClient->Inactivate(false);
}
}
void SMILTimedElement::RebuildTimingState(RemovalTestFunction aRemove) {
MOZ_ASSERT(mAnimationElement,
"Attempting to enable a timed element not attached to an "
"animation element");
MOZ_ASSERT(mElementState == STATE_STARTUP,
"Rebuilding timing state from non-startup state");
if (mAnimationElement->HasAttr(nsGkAtoms::begin)) {
nsAutoString attValue;
mAnimationElement->GetAttr(nsGkAtoms::begin, attValue);
SetBeginSpec(attValue, *mAnimationElement, aRemove);
}
if (mAnimationElement->HasAttr(nsGkAtoms::end)) {
nsAutoString attValue;
mAnimationElement->GetAttr(nsGkAtoms::end, attValue);
SetEndSpec(attValue, *mAnimationElement, aRemove);
}
mPrevRegisteredMilestone = sMaxMilestone;
RegisterMilestone();
}
void SMILTimedElement::DoPostSeek() {
// Finish backwards seek
if (mSeekState == SEEK_BACKWARD_FROM_INACTIVE ||
mSeekState == SEEK_BACKWARD_FROM_ACTIVE) {
// Previously some dynamic instance times may have been marked to be
// preserved because they were endpoints of an historic interval (which may
// or may not have been filtered). Now that we've finished a seek we should
// clear that flag for those instance times whose intervals are no longer
// historic.
UnpreserveInstanceTimes(mBeginInstances);
UnpreserveInstanceTimes(mEndInstances);
// Now that the times have been unmarked perform a reset. This might seem
// counter-intuitive when we're only doing a seek within an interval but
// SMIL seems to require this. SMIL 3.0, 'Hyperlinks and timing':
// Resolved end times associated with events, Repeat-values,
// Accesskey-values or added via DOM method calls are cleared when seeking
// to time earlier than the resolved end time.
Reset();
UpdateCurrentInterval();
}
switch (mSeekState) {
case SEEK_FORWARD_FROM_ACTIVE:
case SEEK_BACKWARD_FROM_ACTIVE:
if (mElementState != STATE_ACTIVE) {
FireTimeEventAsync(eSMILEndEvent, 0);
}
break;
case SEEK_FORWARD_FROM_INACTIVE:
case SEEK_BACKWARD_FROM_INACTIVE:
if (mElementState == STATE_ACTIVE) {
FireTimeEventAsync(eSMILBeginEvent, 0);
}
break;
case SEEK_NOT_SEEKING:
/* Do nothing */
break;
}
mSeekState = SEEK_NOT_SEEKING;
}
void SMILTimedElement::UnpreserveInstanceTimes(InstanceTimeList& aList) {
const SMILInterval* prevInterval = GetPreviousInterval();
const SMILInstanceTime* cutoff = mCurrentInterval ? mCurrentInterval->Begin()
: prevInterval ? prevInterval->Begin()
: nullptr;
for (RefPtr<SMILInstanceTime>& instance : aList) {
if (!cutoff || cutoff->Time().CompareTo(instance->Time()) < 0) {
instance->UnmarkShouldPreserve();
}
}
}
void SMILTimedElement::FilterHistory() {
// We should filter the intervals first, since instance times still used in an
// interval won't be filtered.
FilterIntervals();
FilterInstanceTimes(mBeginInstances);
FilterInstanceTimes(mEndInstances);
}
void SMILTimedElement::FilterIntervals() {
// We can filter old intervals that:
//
// a) are not the previous interval; AND
// b) are not in the middle of a dependency chain; AND
// c) are not the first interval
//
// Condition (a) is necessary since the previous interval is used for applying
// fill effects and updating the current interval.
//
// Condition (b) is necessary since even if this interval itself is not
// active, it may be part of a dependency chain that includes active
// intervals. Such chains are used to establish priorities within the
// animation sandwich.
//
// Condition (c) is necessary to support hyperlinks that target animations
// since in some cases the defined behavior is to seek the document back to
// the first resolved begin time. Presumably the intention here is not
// actually to use the first resolved begin time, the
// _the_first_resolved_begin_time_that_produced_an_interval. That is,
// if we have begin="-5s; -3s; 1s; 3s" with a duration on 1s, we should seek
// to 1s. The spec doesn't say this but I'm pretty sure that is the intention.
// It seems negative times were simply not considered.
//
// Although the above conditions allow us to safely filter intervals for most
// scenarios they do not cover all cases and there will still be scenarios
// that generate intervals indefinitely. In such a case we simply set
// a maximum number of intervals and drop any intervals beyond that threshold.
uint32_t threshold = mOldIntervals.Length() > sMaxNumIntervals
? mOldIntervals.Length() - sMaxNumIntervals
: 0;
IntervalList filteredList;
for (uint32_t i = 0; i < mOldIntervals.Length(); ++i) {
SMILInterval* interval = mOldIntervals[i].get();
if (i != 0 && /*skip first interval*/
i + 1 < mOldIntervals.Length() && /*skip previous interval*/
(i < threshold || !interval->IsDependencyChainLink())) {
interval->Unlink(true /*filtered, not deleted*/);
} else {
filteredList.AppendElement(std::move(mOldIntervals[i]));
}
}
mOldIntervals = std::move(filteredList);
}
namespace {
class MOZ_STACK_CLASS RemoveFiltered {
public:
explicit RemoveFiltered(SMILTimeValue aCutoff) : mCutoff(aCutoff) {}
bool operator()(SMILInstanceTime* aInstanceTime, uint32_t /*aIndex*/) {
// We can filter instance times that:
// a) Precede the end point of the previous interval; AND
// b) Are NOT syncbase times that might be updated to a time after the end
// point of the previous interval; AND
// c) Are NOT fixed end points in any remaining interval.
return aInstanceTime->Time() < mCutoff && aInstanceTime->IsFixedTime() &&
!aInstanceTime->ShouldPreserve();
}
private:
SMILTimeValue mCutoff;
};
class MOZ_STACK_CLASS RemoveBelowThreshold {
public:
RemoveBelowThreshold(uint32_t aThreshold,
nsTArray<const SMILInstanceTime*>& aTimesToKeep)
: mThreshold(aThreshold), mTimesToKeep(aTimesToKeep) {}
bool operator()(SMILInstanceTime* aInstanceTime, uint32_t aIndex) {
return aIndex < mThreshold && !mTimesToKeep.Contains(aInstanceTime);
}
private:
uint32_t mThreshold;
nsTArray<const SMILInstanceTime*>& mTimesToKeep;
};
} // namespace
void SMILTimedElement::FilterInstanceTimes(InstanceTimeList& aList) {
if (GetPreviousInterval()) {
RemoveFiltered removeFiltered(GetPreviousInterval()->End()->Time());
RemoveInstanceTimes(aList, removeFiltered);
}
// As with intervals it is possible to create a document that, even despite
// our most aggressive filtering, will generate instance times indefinitely
// (e.g. cyclic dependencies with TimeEvents---we can't filter such times as
// they're unpredictable due to the possibility of seeking the document which
// may prevent some events from being generated). Therefore we introduce
// a hard cutoff at which point we just drop the oldest instance times.
if (aList.Length() > sMaxNumInstanceTimes) {
uint32_t threshold = aList.Length() - sMaxNumInstanceTimes;
// There are a few instance times we should keep though, notably:
// - the current interval begin time,
// - the previous interval end time (see note in RemoveInstanceTimes)
// - the first interval begin time (see note in FilterIntervals)
nsTArray<const SMILInstanceTime*> timesToKeep;
if (mCurrentInterval) {
timesToKeep.AppendElement(mCurrentInterval->Begin());
}
const SMILInterval* prevInterval = GetPreviousInterval();
if (prevInterval) {
timesToKeep.AppendElement(prevInterval->End());
}
if (!mOldIntervals.IsEmpty()) {
timesToKeep.AppendElement(mOldIntervals[0]->Begin());
}
RemoveBelowThreshold removeBelowThreshold(threshold, timesToKeep);
RemoveInstanceTimes(aList, removeBelowThreshold);
}
}
//
// This method is based on the pseudocode given in the SMILANIM spec.
//
// See:
//
bool SMILTimedElement::GetNextInterval(const SMILInterval* aPrevInterval,
const SMILInterval* aReplacedInterval,
const SMILInstanceTime* aFixedBeginTime,
SMILInterval& aResult) const {
MOZ_ASSERT(!aFixedBeginTime || aFixedBeginTime->Time().IsDefinite(),
"Unresolved or indefinite begin time given for interval start");
static const SMILTimeValue zeroTime(0L);
if (mRestartMode == RESTART_NEVER && aPrevInterval) return false;
// Calc starting point
SMILTimeValue beginAfter;
bool prevIntervalWasZeroDur = false;
if (aPrevInterval) {
beginAfter = aPrevInterval->End()->Time();
prevIntervalWasZeroDur =
aPrevInterval->End()->Time() == aPrevInterval->Begin()->Time();
} else {
beginAfter.SetMillis(std::numeric_limits<SMILTime>::min());
}
RefPtr<SMILInstanceTime> tempBegin;
RefPtr<SMILInstanceTime> tempEnd;
while (true) {
// Calculate begin time
if (aFixedBeginTime) {
if (aFixedBeginTime->Time() < beginAfter) {
return false;
}
// our ref-counting is not const-correct
tempBegin = const_cast<SMILInstanceTime*>(aFixedBeginTime);
} else if ((!mAnimationElement ||
!mAnimationElement->HasAttr(nsGkAtoms::begin)) &&
beginAfter <= zeroTime) {
tempBegin = new SMILInstanceTime(SMILTimeValue(0));
} else {
int32_t beginPos = 0;
do {
tempBegin =
GetNextGreaterOrEqual(mBeginInstances, beginAfter, beginPos);
if (!tempBegin || !tempBegin->Time().IsDefinite()) {
return false;
}
// If we're updating the current interval then skip any begin time that
// is dependent on the current interval's begin time. e.g.
// <animate id="a" begin="b.begin; a.begin+2s"...
// If b's interval disappears whilst 'a' is in the waiting state the
// begin time at "a.begin+2s" should be skipped since 'a' never begun.
} while (aReplacedInterval &&
tempBegin->GetBaseTime() == aReplacedInterval->Begin());
}
MOZ_ASSERT(tempBegin && tempBegin->Time().IsDefinite() &&
tempBegin->Time() >= beginAfter,
"Got a bad begin time while fetching next interval");
// Calculate end time
{
int32_t endPos = 0;
do {
tempEnd =
GetNextGreaterOrEqual(mEndInstances, tempBegin->Time(), endPos);
// SMIL doesn't allow for coincident zero-duration intervals, so if the
// previous interval was zero-duration, and tempEnd is going to give us
// another zero duration interval, then look for another end to use
// instead.
if (tempEnd && prevIntervalWasZeroDur &&
tempEnd->Time() == beginAfter) {
tempEnd = GetNextGreater(mEndInstances, tempBegin->Time(), endPos);
}
// As above with begin times, avoid creating self-referential loops
// between instance times by checking that the newly found end instance
// time is not already dependent on the end of the current interval.
} while (tempEnd && aReplacedInterval &&
tempEnd->GetBaseTime() == aReplacedInterval->End());
if (!tempEnd) {
// If all the ends are before the beginning we have a bad interval
// UNLESS:
// a) We never had any end attribute to begin with (the SMIL pseudocode
// places this condition earlier in the flow but that fails to allow
// for DOM calls when no "indefinite" condition is given), OR
// b) We never had any end instance times to begin with, OR
// c) We have end events which leave the interval open-ended.
bool openEndedIntervalOk = mEndSpecs.IsEmpty() ||
mEndInstances.IsEmpty() ||
EndHasEventConditions();
// The above conditions correspond with the SMIL pseudocode but SMIL
// doesn't address self-dependent instance times which we choose to
// ignore.
//
// Therefore we add a qualification of (b) above that even if
// there are end instance times but they all depend on the end of the
// current interval we should act as if they didn't exist and allow the
// open-ended interval.
//
// In the following condition we don't use |= because it doesn't provide
// short-circuit behavior.
openEndedIntervalOk =
openEndedIntervalOk ||
(aReplacedInterval &&
AreEndTimesDependentOn(aReplacedInterval->End()));
if (!openEndedIntervalOk) {
return false; // Bad interval
}
}
SMILTimeValue intervalEnd = tempEnd ? tempEnd->Time() : SMILTimeValue();
SMILTimeValue activeEnd = CalcActiveEnd(tempBegin->Time(), intervalEnd);
if (!tempEnd || intervalEnd != activeEnd) {
tempEnd = new SMILInstanceTime(activeEnd);
}
}
MOZ_ASSERT(tempEnd, "Failed to get end point for next interval");
// When we choose the interval endpoints, we don't allow coincident
// zero-duration intervals, so if we arrive here and we have a zero-duration
// interval starting at the same point as a previous zero-duration interval,
// then it must be because we've applied constraints to the active duration.
// In that case, we will potentially run into an infinite loop, so we break
// it by searching for the next interval that starts AFTER our current
// zero-duration interval.
if (prevIntervalWasZeroDur && tempEnd->Time() == beginAfter) {
beginAfter.SetMillis(tempBegin->Time().GetMillis() + 1);
prevIntervalWasZeroDur = false;
continue;
}
prevIntervalWasZeroDur = tempBegin->Time() == tempEnd->Time();
// Check for valid interval
if (tempEnd->Time() > zeroTime ||
(tempBegin->Time() == zeroTime && tempEnd->Time() == zeroTime)) {
aResult.Set(*tempBegin, *tempEnd);
return true;
}
if (mRestartMode == RESTART_NEVER) {
// tempEnd <= 0 so we're going to loop which effectively means restarting
return false;
}
beginAfter = tempEnd->Time();
}
MOZ_ASSERT_UNREACHABLE("Hmm... we really shouldn't be here");
return false;
}
SMILInstanceTime* SMILTimedElement::GetNextGreater(
const InstanceTimeList& aList, const SMILTimeValue& aBase,
int32_t& aPosition) const {
SMILInstanceTime* result = nullptr;
while ((result = GetNextGreaterOrEqual(aList, aBase, aPosition)) &&
result->Time() == aBase) {
}
return result;
}
SMILInstanceTime* SMILTimedElement::GetNextGreaterOrEqual(
const InstanceTimeList& aList, const SMILTimeValue& aBase,
int32_t& aPosition) const {
SMILInstanceTime* result = nullptr;
int32_t count = aList.Length();
for (; aPosition < count && !result; ++aPosition) {
SMILInstanceTime* val = aList[aPosition].get();
MOZ_ASSERT(val, "NULL instance time in list");
if (val->Time() >= aBase) {
result = val;
}
}
return result;
}
/**
* @see SMILANIM 3.3.4
*/
SMILTimeValue SMILTimedElement::CalcActiveEnd(const SMILTimeValue& aBegin,
const SMILTimeValue& aEnd) const {
SMILTimeValue result;
MOZ_ASSERT(mSimpleDur.IsResolved(),
"Unresolved simple duration in CalcActiveEnd");
MOZ_ASSERT(aBegin.IsDefinite(),
"Indefinite or unresolved begin time in CalcActiveEnd");
result = GetRepeatDuration();
if (aEnd.IsDefinite()) {
SMILTime activeDur = aEnd.GetMillis() - aBegin.GetMillis();
if (result.IsDefinite()) {
result.SetMillis(std::min(result.GetMillis(), activeDur));
} else {
result.SetMillis(activeDur);
}
}
result = ApplyMinAndMax(result);
if (result.IsDefinite()) {
SMILTime activeEnd = result.GetMillis() + aBegin.GetMillis();
result.SetMillis(activeEnd);
}
return result;
}
SMILTimeValue SMILTimedElement::GetRepeatDuration() const {
SMILTimeValue multipliedDuration;
if (mRepeatCount.IsDefinite() && mSimpleDur.IsDefinite()) {
if (mRepeatCount * double(mSimpleDur.GetMillis()) <
double(std::numeric_limits<SMILTime>::max())) {
multipliedDuration.SetMillis(
SMILTime(mRepeatCount * mSimpleDur.GetMillis()));
}
} else {
multipliedDuration.SetIndefinite();
}
SMILTimeValue repeatDuration;
if (mRepeatDur.IsResolved()) {
repeatDuration = std::min(multipliedDuration, mRepeatDur);
} else if (mRepeatCount.IsSet()) {
repeatDuration = multipliedDuration;
} else {
repeatDuration = mSimpleDur;
}
return repeatDuration;
}
SMILTimeValue SMILTimedElement::ApplyMinAndMax(
const SMILTimeValue& aDuration) const {
if (!aDuration.IsResolved()) {
return aDuration;
}
if (mMax < mMin) {
return aDuration;
}
return std::clamp(aDuration, mMin, mMax);
}
SMILTime SMILTimedElement::ActiveTimeToSimpleTime(SMILTime aActiveTime,
uint32_t& aRepeatIteration) {
SMILTime result;
MOZ_ASSERT(mSimpleDur.IsResolved(),
"Unresolved simple duration in ActiveTimeToSimpleTime");
MOZ_ASSERT(aActiveTime >= 0, "Expecting non-negative active time");
// Note that a negative aActiveTime will give us a negative value for
// aRepeatIteration, which is bad because aRepeatIteration is unsigned
if (mSimpleDur.IsIndefinite() || mSimpleDur.IsZero()) {
aRepeatIteration = 0;
result = aActiveTime;
} else {
result = aActiveTime % mSimpleDur.GetMillis();
aRepeatIteration = (uint32_t)(aActiveTime / mSimpleDur.GetMillis());
}
return result;
}
//
// Although in many cases it would be possible to check for an early end and
// adjust the current interval well in advance the SMIL Animation spec seems to
// indicate that we should only apply an early end at the latest possible
// moment. In particular, this paragraph from section 3.6.8:
//
// 'If restart is set to "always", then the current interval will end early if
// there is an instance time in the begin list that is before (i.e. earlier
// than) the defined end for the current interval. Ending in this manner will
// also send a changed time notice to all time dependents for the current
// interval end.'
//
SMILInstanceTime* SMILTimedElement::CheckForEarlyEnd(
const SMILTimeValue& aContainerTime) const {
MOZ_ASSERT(mCurrentInterval,
"Checking for an early end but the current interval is not set");
if (mRestartMode != RESTART_ALWAYS) return nullptr;
int32_t position = 0;
SMILInstanceTime* nextBegin = GetNextGreater(
mBeginInstances, mCurrentInterval->Begin()->Time(), position);
if (nextBegin && nextBegin->Time() > mCurrentInterval->Begin()->Time() &&
nextBegin->Time() < mCurrentInterval->End()->Time() &&
nextBegin->Time() <= aContainerTime) {
return nextBegin;
}
return nullptr;
}
void SMILTimedElement::UpdateCurrentInterval(bool aForceChangeNotice) {
// Check if updates are currently blocked (batched)
if (mDeferIntervalUpdates) {
mDoDeferredUpdate = true;
return;
}
// We adopt the convention of not resolving intervals until the first
// sample. Otherwise, every time each attribute is set we'll re-resolve the
// current interval and notify all our time dependents of the change.
//
// The disadvantage of deferring resolving the interval is that DOM calls to
// to getStartTime will throw an INVALID_STATE_ERR exception until the
// document timeline begins since the start time has not yet been resolved.
if (mElementState == STATE_STARTUP) return;
// Although SMIL gives rules for detecting cycles in change notifications,
// some configurations can lead to create-delete-create-delete-etc. cycles
// which SMIL does not consider.
//
// In order to provide consistent behavior in such cases, we detect two
// deletes in a row and then refuse to create any further intervals. That is,
// we say the configuration is invalid.
if (mDeleteCount > 1) {
// When we update the delete count we also set the state to post active, so
// if we're not post active here then something other than
// UpdateCurrentInterval has updated the element state in between and all
// bets are off.
MOZ_ASSERT(mElementState == STATE_POSTACTIVE,
"Expected to be in post-active state after performing double "
"delete");
return;
}
// Check that we aren't stuck in infinite recursion updating some syncbase
// dependencies. Generally such situations should be detected in advance and
// the chain broken in a sensible and predictable manner, so if we're hitting
// this assertion we need to work out how to detect the case that's causing
// it. In release builds, just bail out before we overflow the stack.
AutoRestore<uint8_t> depthRestorer(mUpdateIntervalRecursionDepth);
if (++mUpdateIntervalRecursionDepth > sMaxUpdateIntervalRecursionDepth) {
MOZ_ASSERT(false,
"Update current interval recursion depth exceeded threshold");
return;
}
// If the interval is active the begin time is fixed.
const SMILInstanceTime* beginTime =
mElementState == STATE_ACTIVE ? mCurrentInterval->Begin() : nullptr;
SMILInterval updatedInterval;
if (GetNextInterval(GetPreviousInterval(), mCurrentInterval.get(), beginTime,
updatedInterval)) {
if (mElementState == STATE_POSTACTIVE) {
MOZ_ASSERT(!mCurrentInterval,
"In postactive state but the interval has been set");
mCurrentInterval = MakeUnique<SMILInterval>(updatedInterval);
mElementState = STATE_WAITING;
NotifyNewInterval();
} else {
bool beginChanged = false;
bool endChanged = false;
if (mElementState != STATE_ACTIVE &&
!updatedInterval.Begin()->SameTimeAndBase(
*mCurrentInterval->Begin())) {
mCurrentInterval->SetBegin(*updatedInterval.Begin());
beginChanged = true;
}
if (!updatedInterval.End()->SameTimeAndBase(*mCurrentInterval->End())) {
mCurrentInterval->SetEnd(*updatedInterval.End());
endChanged = true;
}
if (beginChanged || endChanged || aForceChangeNotice) {
NotifyChangedInterval(mCurrentInterval.get(), beginChanged, endChanged);
}
}
// There's a chance our next milestone has now changed, so update the time
// container
RegisterMilestone();
} else { // GetNextInterval failed: Current interval is no longer valid
if (mElementState == STATE_ACTIVE) {
// The interval is active so we can't just delete it, instead trim it so
// that begin==end.
if (!mCurrentInterval->End()->SameTimeAndBase(
*mCurrentInterval->Begin())) {
mCurrentInterval->SetEnd(*mCurrentInterval->Begin());
NotifyChangedInterval(mCurrentInterval.get(), false, true);
}
// The transition to the postactive state will take place on the next
// sample (along with firing end events, clearing intervals etc.)
RegisterMilestone();
} else if (mElementState == STATE_WAITING) {
AutoRestore<uint8_t> deleteCountRestorer(mDeleteCount);
++mDeleteCount;
mElementState = STATE_POSTACTIVE;
ResetCurrentInterval();
}
}
}
void SMILTimedElement::SampleSimpleTime(SMILTime aActiveTime) {
if (mClient) {
uint32_t repeatIteration;
SMILTime simpleTime = ActiveTimeToSimpleTime(aActiveTime, repeatIteration);
mClient->SampleAt(simpleTime, mSimpleDur, repeatIteration);
}
}
void SMILTimedElement::SampleFillValue() {
if (mFillMode != FILL_FREEZE || !mClient) return;
SMILTime activeTime;
if (mElementState == STATE_WAITING || mElementState == STATE_POSTACTIVE) {
const SMILInterval* prevInterval = GetPreviousInterval();
MOZ_ASSERT(prevInterval,
"Attempting to sample fill value but there is no previous "
"interval");
MOZ_ASSERT(prevInterval->End()->Time().IsDefinite() &&
prevInterval->End()->IsFixedTime(),
"Attempting to sample fill value but the endpoint of the "
"previous interval is not resolved and fixed");
activeTime = prevInterval->End()->Time().GetMillis() -
prevInterval->Begin()->Time().GetMillis();
// If the interval's repeat duration was shorter than its active duration,
// use the end of the repeat duration to determine the frozen animation's
// state.
SMILTimeValue repeatDuration = GetRepeatDuration();
if (repeatDuration.IsDefinite()) {
activeTime = std::min(repeatDuration.GetMillis(), activeTime);
}
} else {
MOZ_ASSERT(
mElementState == STATE_ACTIVE,
"Attempting to sample fill value when we're in an unexpected state "
"(probably STATE_STARTUP)");
// If we are being asked to sample the fill value while active we *must*
// have a repeat duration shorter than the active duration so use that.
MOZ_ASSERT(GetRepeatDuration().IsDefinite(),
"Attempting to sample fill value of an active animation with "
"an indefinite repeat duration");
activeTime = GetRepeatDuration().GetMillis();
}
uint32_t repeatIteration;
SMILTime simpleTime = ActiveTimeToSimpleTime(activeTime, repeatIteration);
if (simpleTime == 0L && repeatIteration) {
mClient->SampleLastValue(--repeatIteration);
} else {
mClient->SampleAt(simpleTime, mSimpleDur, repeatIteration);
}
}
void SMILTimedElement::AddInstanceTimeFromCurrentTime(SMILTime aCurrentTime,
double aOffsetSeconds,
bool aIsBegin) {
double offset = NS_round(aOffsetSeconds * PR_MSEC_PER_SEC);
SMILTimeValue timeVal(std::clamp<SMILTime>(
aCurrentTime + offset, 0, std::numeric_limits<SMILTime>::max()));
RefPtr<SMILInstanceTime> instanceTime =
new SMILInstanceTime(timeVal, SMILInstanceTime::SOURCE_DOM);
AddInstanceTime(instanceTime, aIsBegin);
}
void SMILTimedElement::RegisterMilestone() {
SMILTimeContainer* container = GetTimeContainer();
if (!container) return;
MOZ_ASSERT(mAnimationElement,
"Got a time container without an owning animation element");
SMILMilestone nextMilestone;
if (!GetNextMilestone(nextMilestone)) return;
// This method is called every time we might possibly have updated our
// current interval, but since SMILTimeContainer makes no attempt to filter
// out redundant milestones we do some rudimentary filtering here. It's not
// perfect, but unnecessary samples are fairly cheap.
if (nextMilestone >= mPrevRegisteredMilestone) return;
container->AddMilestone(nextMilestone, *mAnimationElement);
mPrevRegisteredMilestone = nextMilestone;
}
bool SMILTimedElement::GetNextMilestone(SMILMilestone& aNextMilestone) const {
// Return the next key moment in our lifetime.
//
// XXX It may be possible in future to optimise this so that we only register
// for milestones if:
// a) We have time dependents, or
// b) We are dependent on events or syncbase relationships, or
// c) There are registered listeners for our events
//
// Then for the simple case where everything uses offset values we could
// ignore milestones altogether.
//
// We'd need to be careful, however, that if one of those conditions became
// true in between samples that we registered our next milestone at that
// point.
switch (mElementState) {
case STATE_STARTUP:
// All elements register for an initial end sample at t=0 where we resolve
// our initial interval.
aNextMilestone.mIsEnd = true; // Initial sample should be an end sample
aNextMilestone.mTime = 0;
return true;
case STATE_WAITING:
MOZ_ASSERT(mCurrentInterval,
"In waiting state but the current interval has not been set");
aNextMilestone.mIsEnd = false;
aNextMilestone.mTime = mCurrentInterval->Begin()->Time().GetMillis();
return true;
case STATE_ACTIVE: {
// Work out what comes next: the interval end or the next repeat iteration
SMILTimeValue nextRepeat;
if (mSeekState == SEEK_NOT_SEEKING && mSimpleDur.IsDefinite()) {
SMILTime nextRepeatActiveTime =
(mCurrentRepeatIteration + 1) * mSimpleDur.GetMillis();
// Check that the repeat fits within the repeat duration
if (SMILTimeValue(nextRepeatActiveTime) < GetRepeatDuration()) {
nextRepeat.SetMillis(mCurrentInterval->Begin()->Time().GetMillis() +
nextRepeatActiveTime);
}
}
SMILTimeValue nextMilestone =
std::min(mCurrentInterval->End()->Time(), nextRepeat);
// Check for an early end before that time
SMILInstanceTime* earlyEnd = CheckForEarlyEnd(nextMilestone);
if (earlyEnd) {
aNextMilestone.mIsEnd = true;
aNextMilestone.mTime = earlyEnd->Time().GetMillis();
return true;
}
// Apply the previously calculated milestone
if (nextMilestone.IsDefinite()) {
aNextMilestone.mIsEnd = nextMilestone != nextRepeat;
aNextMilestone.mTime = nextMilestone.GetMillis();
return true;
}
return false;
}
case STATE_POSTACTIVE:
return false;
}
MOZ_CRASH("Invalid element state");
}
void SMILTimedElement::NotifyNewInterval() {
MOZ_ASSERT(mCurrentInterval,
"Attempting to notify dependents of a new interval but the "
"interval is not set");
SMILTimeContainer* container = GetTimeContainer();
if (container) {
container->SyncPauseTime();
}
for (SMILTimeValueSpec* spec : mTimeDependents.Keys()) {
SMILInterval* interval = mCurrentInterval.get();
// It's possible that in notifying one new time dependent of a new interval
// that a chain reaction is triggered which results in the original
// interval disappearing. If that's the case we can skip sending further
// notifications.
if (!interval) {
break;
}
spec->HandleNewInterval(*interval, container);
}
}
void SMILTimedElement::NotifyChangedInterval(SMILInterval* aInterval,
bool aBeginObjectChanged,
bool aEndObjectChanged) {
MOZ_ASSERT(aInterval, "Null interval for change notification");
SMILTimeContainer* container = GetTimeContainer();
if (container) {
container->SyncPauseTime();
}
// Copy the instance times list since notifying the instance times can result
// in a chain reaction whereby our own interval gets deleted along with its
// instance times.
InstanceTimeList times;
aInterval->GetDependentTimes(times);
for (RefPtr<SMILInstanceTime>& time : times) {
time->HandleChangedInterval(container, aBeginObjectChanged,
aEndObjectChanged);
}
}
void SMILTimedElement::FireTimeEventAsync(EventMessage aMsg, int32_t aDetail) {
if (!mAnimationElement) return;
Document* ownerDoc = mAnimationElement->OwnerDoc();
if (ownerDoc->IsBeingUsedAsImage() || !ownerDoc->IsScriptEnabled()) {
// Without scripting the only listeners would be from SMIL itself
// and they would exist for the life of the document.
nsPIDOMWindowInner* inner = ownerDoc->GetInnerWindow();
if (inner && !inner->HasSMILTimeEventListeners()) {
return;
}
}
nsCOMPtr<nsIRunnable> event =
new AsyncTimeEventRunner(mAnimationElement, aMsg, aDetail);
ownerDoc->Dispatch(event.forget());
}
const SMILInstanceTime* SMILTimedElement::GetEffectiveBeginInstance() const {
switch (mElementState) {
case STATE_STARTUP:
return nullptr;
case STATE_ACTIVE:
return mCurrentInterval->Begin();
case STATE_WAITING:
case STATE_POSTACTIVE: {
const SMILInterval* prevInterval = GetPreviousInterval();
return prevInterval ? prevInterval->Begin() : nullptr;
}
}
MOZ_CRASH("Invalid element state");
}
const SMILInterval* SMILTimedElement::GetPreviousInterval() const {
return mOldIntervals.IsEmpty() ? nullptr : mOldIntervals.LastElement().get();
}
bool SMILTimedElement::HasClientInFillRange() const {
// Returns true if we have a client that is in the range where it will fill
return mClient && ((mElementState != STATE_ACTIVE && HasPlayed()) ||
(mElementState == STATE_ACTIVE && !mClient->IsActive()));
}
bool SMILTimedElement::EndHasEventConditions() const {
for (const UniquePtr<SMILTimeValueSpec>& endSpec : mEndSpecs) {
if (endSpec->IsEventBased()) return true;
}
return false;
}
bool SMILTimedElement::AreEndTimesDependentOn(
const SMILInstanceTime* aBase) const {
if (mEndInstances.IsEmpty()) return false;
for (const RefPtr<SMILInstanceTime>& endInstance : mEndInstances) {
if (endInstance->GetBaseTime() != aBase) {
return false;
}
}
return true;
}
} // namespace mozilla