From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- dom/smil/SMILAnimationController.cpp | 700 +++++++ dom/smil/SMILAnimationController.h | 208 ++ dom/smil/SMILAnimationFunction.cpp | 993 +++++++++ dom/smil/SMILAnimationFunction.h | 444 ++++ dom/smil/SMILAttr.h | 100 + dom/smil/SMILBoolType.cpp | 69 + dom/smil/SMILBoolType.h | 44 + dom/smil/SMILCSSProperty.cpp | 199 ++ dom/smil/SMILCSSProperty.h | 80 + dom/smil/SMILCSSValueType.cpp | 550 +++++ dom/smil/SMILCSSValueType.h | 133 ++ dom/smil/SMILCompositor.cpp | 239 +++ dom/smil/SMILCompositor.h | 132 ++ dom/smil/SMILCompositorTable.h | 28 + dom/smil/SMILEnumType.cpp | 69 + dom/smil/SMILEnumType.h | 44 + dom/smil/SMILFloatType.cpp | 81 + dom/smil/SMILFloatType.h | 44 + dom/smil/SMILInstanceTime.cpp | 188 ++ dom/smil/SMILInstanceTime.h | 166 ++ dom/smil/SMILIntegerType.cpp | 86 + dom/smil/SMILIntegerType.h | 39 + dom/smil/SMILInterval.cpp | 137 ++ dom/smil/SMILInterval.h | 86 + dom/smil/SMILKeySpline.cpp | 127 ++ dom/smil/SMILKeySpline.h | 107 + dom/smil/SMILMilestone.h | 75 + dom/smil/SMILNullType.cpp | 57 + dom/smil/SMILNullType.h | 44 + dom/smil/SMILParserUtils.cpp | 632 ++++++ dom/smil/SMILParserUtils.h | 91 + dom/smil/SMILRepeatCount.cpp | 14 + dom/smil/SMILRepeatCount.h | 62 + dom/smil/SMILSetAnimationFunction.cpp | 26 + dom/smil/SMILSetAnimationFunction.h | 38 + dom/smil/SMILStringType.cpp | 75 + dom/smil/SMILStringType.h | 44 + dom/smil/SMILTargetIdentifier.h | 87 + dom/smil/SMILTimeContainer.cpp | 306 +++ dom/smil/SMILTimeContainer.h | 299 +++ dom/smil/SMILTimeValue.cpp | 52 + dom/smil/SMILTimeValue.h | 145 ++ dom/smil/SMILTimeValueSpec.cpp | 370 ++++ dom/smil/SMILTimeValueSpec.h | 143 ++ dom/smil/SMILTimeValueSpecParams.h | 58 + dom/smil/SMILTimedElement.cpp | 2166 ++++++++++++++++++++ dom/smil/SMILTimedElement.h | 649 ++++++ dom/smil/SMILType.h | 212 ++ dom/smil/SMILTypes.h | 30 + dom/smil/SMILValue.cpp | 142 ++ dom/smil/SMILValue.h | 75 + dom/smil/TimeEvent.cpp | 57 + dom/smil/TimeEvent.h | 61 + dom/smil/crashtests/1010681-1.svg | 23 + dom/smil/crashtests/1322770-1.svg | 3 + dom/smil/crashtests/1322849-1.svg | 2 + dom/smil/crashtests/1343357-1.html | 12 + dom/smil/crashtests/1375596-1.svg | 3 + dom/smil/crashtests/1402547-1.html | 3 + dom/smil/crashtests/1411963-1.html | 10 + dom/smil/crashtests/1413319-1.html | 2 + dom/smil/crashtests/1535388-1.html | 18 + dom/smil/crashtests/1772573-1.html | 18 + dom/smil/crashtests/1780800-1.html | 22 + dom/smil/crashtests/483584-1.svg | 8 + dom/smil/crashtests/483584-2.svg | 133 ++ dom/smil/crashtests/523188-1.svg | 15 + dom/smil/crashtests/525099-1.svg | 7 + dom/smil/crashtests/526536-1.svg | 19 + dom/smil/crashtests/526875-1.svg | 4 + dom/smil/crashtests/526875-2.svg | 4 + dom/smil/crashtests/529387-1-helper.svg | 5 + dom/smil/crashtests/529387-1.xhtml | 7 + dom/smil/crashtests/531550-1.svg | 3 + dom/smil/crashtests/541297-1.svg | 22 + dom/smil/crashtests/547333-1.svg | 22 + dom/smil/crashtests/548899-1.svg | 14 + dom/smil/crashtests/551620-1.svg | 21 + dom/smil/crashtests/554141-1.svg | 12 + dom/smil/crashtests/554202-2.svg | 19 + dom/smil/crashtests/555026-1.svg | 25 + dom/smil/crashtests/556841-1.svg | 16 + dom/smil/crashtests/572938-1.svg | 12 + dom/smil/crashtests/572938-2.svg | 22 + dom/smil/crashtests/572938-3.svg | 10 + dom/smil/crashtests/572938-4.svg | 10 + dom/smil/crashtests/588287-1.svg | 24 + dom/smil/crashtests/588287-2.svg | 26 + dom/smil/crashtests/590425-1.html | 24 + dom/smil/crashtests/594653-1.svg | 26 + dom/smil/crashtests/596796-1.svg | 15 + dom/smil/crashtests/605345-1.svg | 25 + dom/smil/crashtests/606101-1.svg | 23 + dom/smil/crashtests/608295-1.html | 18 + dom/smil/crashtests/608549-1.svg | 29 + dom/smil/crashtests/611927-1.svg | 4 + dom/smil/crashtests/615002-1.svg | 16 + dom/smil/crashtests/615872-1.svg | 21 + dom/smil/crashtests/641388-1.html | 97 + dom/smil/crashtests/641388-2.html | 79 + dom/smil/crashtests/650732-1.svg | 46 + dom/smil/crashtests/665334-1.svg | 13 + dom/smil/crashtests/669225-1.svg | 21 + dom/smil/crashtests/669225-2.svg | 21 + dom/smil/crashtests/670313-1.svg | 20 + dom/smil/crashtests/678822-1.svg | 3 + dom/smil/crashtests/678847-1.svg | 3 + dom/smil/crashtests/678938-1.svg | 11 + dom/smil/crashtests/690994-1.svg | 17 + dom/smil/crashtests/691337-1.svg | 8 + dom/smil/crashtests/691337-2.svg | 11 + dom/smil/crashtests/697640-1.svg | 3 + dom/smil/crashtests/699325-1.svg | 5 + dom/smil/crashtests/709907-1.svg | 3 + dom/smil/crashtests/720103-1.svg | 4 + dom/smil/crashtests/849593-1.xhtml | 34 + dom/smil/crashtests/crashtests.list | 62 + dom/smil/moz.build | 74 + dom/smil/test/db_smilAnimateMotion.js | 309 +++ dom/smil/test/db_smilCSSFromBy.js | 207 ++ dom/smil/test/db_smilCSSFromTo.js | 625 ++++++ dom/smil/test/db_smilCSSPaced.js | 356 ++++ dom/smil/test/db_smilCSSPropertyList.js | 104 + dom/smil/test/db_smilMappedAttrList.js | 148 ++ dom/smil/test/file_smilWithTransition.html | 79 + dom/smil/test/mochitest.toml | 109 + dom/smil/test/smilAnimateMotionValueLists.js | 116 ++ dom/smil/test/smilExtDoc_helper.svg | 7 + dom/smil/test/smilTestUtils.js | 1015 +++++++++ dom/smil/test/smilXHR_helper.svg | 8 + dom/smil/test/test_smilAccessKey.xhtml | 79 + dom/smil/test/test_smilAdditionFallback.html | 30 + dom/smil/test/test_smilAnimateMotion.xhtml | 51 + .../test/test_smilAnimateMotionInvalidValues.xhtml | 176 ++ .../test/test_smilAnimateMotionOverrideRules.xhtml | 215 ++ dom/smil/test/test_smilBackwardsSeeking.xhtml | 191 ++ .../test/test_smilCSSFontStretchRelative.xhtml | 102 + dom/smil/test/test_smilCSSFromBy.xhtml | 49 + dom/smil/test/test_smilCSSFromTo.xhtml | 76 + dom/smil/test/test_smilCSSInherit.xhtml | 85 + dom/smil/test/test_smilCSSInvalidValues.xhtml | 59 + dom/smil/test/test_smilCSSPaced.xhtml | 44 + dom/smil/test/test_smilChangeAfterFrozen.xhtml | 571 ++++++ dom/smil/test/test_smilConditionalProcessing.html | 93 + dom/smil/test/test_smilContainerBinding.xhtml | 101 + dom/smil/test/test_smilCrossContainer.xhtml | 132 ++ .../test/test_smilDynamicDelayedBeginElement.xhtml | 103 + dom/smil/test/test_smilExtDoc.xhtml | 80 + dom/smil/test/test_smilFillMode.xhtml | 86 + dom/smil/test/test_smilGetSimpleDuration.xhtml | 84 + dom/smil/test/test_smilGetStartTime.xhtml | 103 + dom/smil/test/test_smilHyperlinking.xhtml | 229 +++ dom/smil/test/test_smilInvalidValues.html | 122 ++ dom/smil/test/test_smilKeySplines.xhtml | 296 +++ dom/smil/test/test_smilKeyTimes.xhtml | 391 ++++ dom/smil/test/test_smilKeyTimesPacedMode.xhtml | 123 ++ dom/smil/test/test_smilMappedAttrFromBy.xhtml | 51 + dom/smil/test/test_smilMappedAttrFromTo.xhtml | 79 + dom/smil/test/test_smilMappedAttrPaced.xhtml | 46 + dom/smil/test/test_smilMinTiming.html | 93 + dom/smil/test/test_smilRepeatDuration.html | 139 ++ dom/smil/test/test_smilRepeatTiming.xhtml | 96 + dom/smil/test/test_smilReset.xhtml | 82 + dom/smil/test/test_smilRestart.xhtml | 102 + dom/smil/test/test_smilSetCurrentTime.xhtml | 76 + dom/smil/test/test_smilSync.xhtml | 255 +++ dom/smil/test/test_smilSyncTransform.xhtml | 66 + dom/smil/test/test_smilSyncbaseTarget.xhtml | 180 ++ dom/smil/test/test_smilTextZoom.xhtml | 99 + dom/smil/test/test_smilTiming.xhtml | 291 +++ dom/smil/test/test_smilTimingZeroIntervals.xhtml | 285 +++ dom/smil/test/test_smilUpdatedInterval.xhtml | 64 + dom/smil/test/test_smilValues.xhtml | 171 ++ dom/smil/test/test_smilWithTransition.html | 14 + dom/smil/test/test_smilWithXlink.xhtml | 47 + dom/smil/test/test_smilXHR.xhtml | 88 + 176 files changed, 21493 insertions(+) create mode 100644 dom/smil/SMILAnimationController.cpp create mode 100644 dom/smil/SMILAnimationController.h create mode 100644 dom/smil/SMILAnimationFunction.cpp create mode 100644 dom/smil/SMILAnimationFunction.h create mode 100644 dom/smil/SMILAttr.h create mode 100644 dom/smil/SMILBoolType.cpp create mode 100644 dom/smil/SMILBoolType.h create mode 100644 dom/smil/SMILCSSProperty.cpp create mode 100644 dom/smil/SMILCSSProperty.h create mode 100644 dom/smil/SMILCSSValueType.cpp create mode 100644 dom/smil/SMILCSSValueType.h create mode 100644 dom/smil/SMILCompositor.cpp create mode 100644 dom/smil/SMILCompositor.h create mode 100644 dom/smil/SMILCompositorTable.h create mode 100644 dom/smil/SMILEnumType.cpp create mode 100644 dom/smil/SMILEnumType.h create mode 100644 dom/smil/SMILFloatType.cpp create mode 100644 dom/smil/SMILFloatType.h create mode 100644 dom/smil/SMILInstanceTime.cpp create mode 100644 dom/smil/SMILInstanceTime.h create mode 100644 dom/smil/SMILIntegerType.cpp create mode 100644 dom/smil/SMILIntegerType.h create mode 100644 dom/smil/SMILInterval.cpp create mode 100644 dom/smil/SMILInterval.h create mode 100644 dom/smil/SMILKeySpline.cpp create mode 100644 dom/smil/SMILKeySpline.h create mode 100644 dom/smil/SMILMilestone.h create mode 100644 dom/smil/SMILNullType.cpp create mode 100644 dom/smil/SMILNullType.h create mode 100644 dom/smil/SMILParserUtils.cpp create mode 100644 dom/smil/SMILParserUtils.h create mode 100644 dom/smil/SMILRepeatCount.cpp create mode 100644 dom/smil/SMILRepeatCount.h create mode 100644 dom/smil/SMILSetAnimationFunction.cpp create mode 100644 dom/smil/SMILSetAnimationFunction.h create mode 100644 dom/smil/SMILStringType.cpp create mode 100644 dom/smil/SMILStringType.h create mode 100644 dom/smil/SMILTargetIdentifier.h create mode 100644 dom/smil/SMILTimeContainer.cpp create mode 100644 dom/smil/SMILTimeContainer.h create mode 100644 dom/smil/SMILTimeValue.cpp create mode 100644 dom/smil/SMILTimeValue.h create mode 100644 dom/smil/SMILTimeValueSpec.cpp create mode 100644 dom/smil/SMILTimeValueSpec.h create mode 100644 dom/smil/SMILTimeValueSpecParams.h create mode 100644 dom/smil/SMILTimedElement.cpp create mode 100644 dom/smil/SMILTimedElement.h create mode 100644 dom/smil/SMILType.h create mode 100644 dom/smil/SMILTypes.h create mode 100644 dom/smil/SMILValue.cpp create mode 100644 dom/smil/SMILValue.h create mode 100644 dom/smil/TimeEvent.cpp create mode 100644 dom/smil/TimeEvent.h create mode 100644 dom/smil/crashtests/1010681-1.svg create mode 100644 dom/smil/crashtests/1322770-1.svg create mode 100644 dom/smil/crashtests/1322849-1.svg create mode 100644 dom/smil/crashtests/1343357-1.html create mode 100644 dom/smil/crashtests/1375596-1.svg create mode 100644 dom/smil/crashtests/1402547-1.html create mode 100644 dom/smil/crashtests/1411963-1.html create mode 100644 dom/smil/crashtests/1413319-1.html create mode 100644 dom/smil/crashtests/1535388-1.html create mode 100644 dom/smil/crashtests/1772573-1.html create mode 100644 dom/smil/crashtests/1780800-1.html create mode 100644 dom/smil/crashtests/483584-1.svg create mode 100644 dom/smil/crashtests/483584-2.svg create mode 100644 dom/smil/crashtests/523188-1.svg create mode 100644 dom/smil/crashtests/525099-1.svg create mode 100644 dom/smil/crashtests/526536-1.svg create mode 100644 dom/smil/crashtests/526875-1.svg create mode 100644 dom/smil/crashtests/526875-2.svg create mode 100644 dom/smil/crashtests/529387-1-helper.svg create mode 100644 dom/smil/crashtests/529387-1.xhtml create mode 100644 dom/smil/crashtests/531550-1.svg create mode 100644 dom/smil/crashtests/541297-1.svg create mode 100644 dom/smil/crashtests/547333-1.svg create mode 100644 dom/smil/crashtests/548899-1.svg create mode 100644 dom/smil/crashtests/551620-1.svg create mode 100644 dom/smil/crashtests/554141-1.svg create mode 100644 dom/smil/crashtests/554202-2.svg create mode 100644 dom/smil/crashtests/555026-1.svg create mode 100644 dom/smil/crashtests/556841-1.svg create mode 100644 dom/smil/crashtests/572938-1.svg create mode 100644 dom/smil/crashtests/572938-2.svg create mode 100644 dom/smil/crashtests/572938-3.svg create mode 100644 dom/smil/crashtests/572938-4.svg create mode 100644 dom/smil/crashtests/588287-1.svg create mode 100644 dom/smil/crashtests/588287-2.svg create mode 100644 dom/smil/crashtests/590425-1.html create mode 100644 dom/smil/crashtests/594653-1.svg create mode 100644 dom/smil/crashtests/596796-1.svg create mode 100644 dom/smil/crashtests/605345-1.svg create mode 100644 dom/smil/crashtests/606101-1.svg create mode 100644 dom/smil/crashtests/608295-1.html create mode 100644 dom/smil/crashtests/608549-1.svg create mode 100644 dom/smil/crashtests/611927-1.svg create mode 100644 dom/smil/crashtests/615002-1.svg create mode 100644 dom/smil/crashtests/615872-1.svg create mode 100644 dom/smil/crashtests/641388-1.html create mode 100644 dom/smil/crashtests/641388-2.html create mode 100644 dom/smil/crashtests/650732-1.svg create mode 100644 dom/smil/crashtests/665334-1.svg create mode 100644 dom/smil/crashtests/669225-1.svg create mode 100644 dom/smil/crashtests/669225-2.svg create mode 100644 dom/smil/crashtests/670313-1.svg create mode 100644 dom/smil/crashtests/678822-1.svg create mode 100644 dom/smil/crashtests/678847-1.svg create mode 100644 dom/smil/crashtests/678938-1.svg create mode 100644 dom/smil/crashtests/690994-1.svg create mode 100644 dom/smil/crashtests/691337-1.svg create mode 100644 dom/smil/crashtests/691337-2.svg create mode 100644 dom/smil/crashtests/697640-1.svg create mode 100644 dom/smil/crashtests/699325-1.svg create mode 100644 dom/smil/crashtests/709907-1.svg create mode 100644 dom/smil/crashtests/720103-1.svg create mode 100644 dom/smil/crashtests/849593-1.xhtml create mode 100644 dom/smil/crashtests/crashtests.list create mode 100644 dom/smil/moz.build create mode 100644 dom/smil/test/db_smilAnimateMotion.js create mode 100644 dom/smil/test/db_smilCSSFromBy.js create mode 100644 dom/smil/test/db_smilCSSFromTo.js create mode 100644 dom/smil/test/db_smilCSSPaced.js create mode 100644 dom/smil/test/db_smilCSSPropertyList.js create mode 100644 dom/smil/test/db_smilMappedAttrList.js create mode 100644 dom/smil/test/file_smilWithTransition.html create mode 100644 dom/smil/test/mochitest.toml create mode 100644 dom/smil/test/smilAnimateMotionValueLists.js create mode 100644 dom/smil/test/smilExtDoc_helper.svg create mode 100644 dom/smil/test/smilTestUtils.js create mode 100644 dom/smil/test/smilXHR_helper.svg create mode 100644 dom/smil/test/test_smilAccessKey.xhtml create mode 100644 dom/smil/test/test_smilAdditionFallback.html create mode 100644 dom/smil/test/test_smilAnimateMotion.xhtml create mode 100644 dom/smil/test/test_smilAnimateMotionInvalidValues.xhtml create mode 100644 dom/smil/test/test_smilAnimateMotionOverrideRules.xhtml create mode 100644 dom/smil/test/test_smilBackwardsSeeking.xhtml create mode 100644 dom/smil/test/test_smilCSSFontStretchRelative.xhtml create mode 100644 dom/smil/test/test_smilCSSFromBy.xhtml create mode 100644 dom/smil/test/test_smilCSSFromTo.xhtml create mode 100644 dom/smil/test/test_smilCSSInherit.xhtml create mode 100644 dom/smil/test/test_smilCSSInvalidValues.xhtml create mode 100644 dom/smil/test/test_smilCSSPaced.xhtml create mode 100644 dom/smil/test/test_smilChangeAfterFrozen.xhtml create mode 100644 dom/smil/test/test_smilConditionalProcessing.html create mode 100644 dom/smil/test/test_smilContainerBinding.xhtml create mode 100644 dom/smil/test/test_smilCrossContainer.xhtml create mode 100644 dom/smil/test/test_smilDynamicDelayedBeginElement.xhtml create mode 100644 dom/smil/test/test_smilExtDoc.xhtml create mode 100644 dom/smil/test/test_smilFillMode.xhtml create mode 100644 dom/smil/test/test_smilGetSimpleDuration.xhtml create mode 100644 dom/smil/test/test_smilGetStartTime.xhtml create mode 100644 dom/smil/test/test_smilHyperlinking.xhtml create mode 100644 dom/smil/test/test_smilInvalidValues.html create mode 100644 dom/smil/test/test_smilKeySplines.xhtml create mode 100644 dom/smil/test/test_smilKeyTimes.xhtml create mode 100644 dom/smil/test/test_smilKeyTimesPacedMode.xhtml create mode 100644 dom/smil/test/test_smilMappedAttrFromBy.xhtml create mode 100644 dom/smil/test/test_smilMappedAttrFromTo.xhtml create mode 100644 dom/smil/test/test_smilMappedAttrPaced.xhtml create mode 100644 dom/smil/test/test_smilMinTiming.html create mode 100644 dom/smil/test/test_smilRepeatDuration.html create mode 100644 dom/smil/test/test_smilRepeatTiming.xhtml create mode 100644 dom/smil/test/test_smilReset.xhtml create mode 100644 dom/smil/test/test_smilRestart.xhtml create mode 100644 dom/smil/test/test_smilSetCurrentTime.xhtml create mode 100644 dom/smil/test/test_smilSync.xhtml create mode 100644 dom/smil/test/test_smilSyncTransform.xhtml create mode 100644 dom/smil/test/test_smilSyncbaseTarget.xhtml create mode 100644 dom/smil/test/test_smilTextZoom.xhtml create mode 100644 dom/smil/test/test_smilTiming.xhtml create mode 100644 dom/smil/test/test_smilTimingZeroIntervals.xhtml create mode 100644 dom/smil/test/test_smilUpdatedInterval.xhtml create mode 100644 dom/smil/test/test_smilValues.xhtml create mode 100644 dom/smil/test/test_smilWithTransition.html create mode 100644 dom/smil/test/test_smilWithXlink.xhtml create mode 100644 dom/smil/test/test_smilXHR.xhtml (limited to 'dom/smil') diff --git a/dom/smil/SMILAnimationController.cpp b/dom/smil/SMILAnimationController.cpp new file mode 100644 index 0000000000..2c103b0f16 --- /dev/null +++ b/dom/smil/SMILAnimationController.cpp @@ -0,0 +1,700 @@ +/* -*- 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 + +#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.IsEmpty(), + "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 (SMILCompositor& compositor : *mLastCompositorTable) { + 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(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 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 currentCompositorTable( + new SMILCompositorTable(0)); + nsTArray> 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 (SMILCompositor& compositor : *currentCompositorTable) { + SMILCompositor* lastCompositor = + mLastCompositorTable->GetEntry(compositor.GetKey()); + + if (lastCompositor) { + compositor.StealCachedBaseValue(lastCompositor); + if (!lastCompositor->HasSameNumberOfAnimationFunctionsAs(compositor)) { + // If we have multiple animations on the same element, they share a + // compositor. If an active animation ends, it will no longer be in + // the compositor table. We need to force compositing to ensure we + // render the element with any remaining frozen animations even though + // they would not normally trigger compositing. + compositor.ToggleForceCompositing(); + } + } + } + + // * 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 (SMILCompositor& compositor : *mLastCompositorTable) { + compositor.ClearAnimationEffects(); + } + } + + // return early if there are no active animations to avoid a style flush + if (currentCompositorTable->IsEmpty()) { + 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& compositor : *currentCompositorTable) { + compositor.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> elements; + for (SMILTimeContainer* container : mChildContainerTable.Keys()) { + if (container->IsPausedByType(SMILTimeContainer::PAUSE_BEGIN)) { + continue; + } + container->PopMilestoneElementsAtMilestone(nextMilestone, elements); + } + + // 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 (RefPtr& elem : elements) { + 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(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); + aStyleFlushNeeded |= func.ValueNeedsReparsingEverySample(); + 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); + aStyleFlushNeeded |= func.ValueNeedsReparsingEverySample(); + 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(); + } +} + +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 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 diff --git a/dom/smil/SMILAnimationController.h b/dom/smil/SMILAnimationController.h new file mode 100644 index 0000000000..04f2e34cf3 --- /dev/null +++ b/dom/smil/SMILAnimationController.h @@ -0,0 +1,208 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILANIMATIONCONTROLLER_H_ +#define DOM_SMIL_SMILANIMATIONCONTROLLER_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/SMILCompositorTable.h" +#include "mozilla/SMILMilestone.h" +#include "mozilla/SMILTimeContainer.h" +#include "mozilla/UniquePtr.h" +#include "nsCOMPtr.h" +#include "nsTArray.h" +#include "nsTHashtable.h" +#include "nsHashKeys.h" +#include "nsRefreshObservers.h" + +class nsRefreshDriver; + +namespace mozilla { +struct SMILTargetIdentifier; +namespace dom { +class Element; +class SVGAnimationElement; +} // namespace dom + +//---------------------------------------------------------------------- +// SMILAnimationController +// +// The animation controller maintains the animation timer and determines the +// sample times and sample rate for all SMIL animations in a document. There is +// at most one animation controller per document so that frame-rate tuning can +// be performed at a document-level. +// +// The animation controller can contain many child time containers (timed +// document root objects) which may correspond to SVG document fragments within +// a compound document. These time containers can be paused individually or +// here, at the document level. +// +class SMILAnimationController final : public SMILTimeContainer, + public nsARefreshObserver { + public: + explicit SMILAnimationController(mozilla::dom::Document* aDoc); + + // Clears mDocument pointer. (Called by our mozilla::dom::Document when it's + // going away) + void Disconnect(); + + // SMILContainer + void Pause(uint32_t aType) override; + void Resume(uint32_t aType) override; + SMILTime GetParentTime() const override; + + // nsARefreshObserver + NS_IMETHOD_(MozExternalRefCountType) AddRef() override; + NS_IMETHOD_(MozExternalRefCountType) Release() override; + + void WillRefresh(mozilla::TimeStamp aTime) override; + + // Methods for registering and enumerating animation elements + void RegisterAnimationElement( + mozilla::dom::SVGAnimationElement* aAnimationElement); + void UnregisterAnimationElement( + mozilla::dom::SVGAnimationElement* aAnimationElement); + + // Methods for resampling all animations + // (A resample performs the same operations as a sample but doesn't advance + // the current time and doesn't check if the container is paused) + // This will flush pending style changes for the document. + void Resample() { DoSample(false); } + + void SetResampleNeeded() { + if (!mRunningSample && !mResampleNeeded) { + FlagDocumentNeedsFlush(); + mResampleNeeded = true; + } + } + + // This will flush pending style changes for the document. + void FlushResampleRequests() { + if (!mResampleNeeded) return; + + Resample(); + } + + // Methods for handling page transitions + void OnPageShow(); + void OnPageHide(); + + // Methods for supporting cycle-collection + void Traverse(nsCycleCollectionTraversalCallback* aCallback); + void Unlink(); + + // Methods for relaying the availability of the refresh driver + void NotifyRefreshDriverCreated(nsRefreshDriver* aRefreshDriver); + void NotifyRefreshDriverDestroying(nsRefreshDriver* aRefreshDriver); + + // Helper to check if we have any animation elements at all + bool HasRegisteredAnimations() const { + return mAnimationElementTable.Count() != 0; + } + + bool MightHavePendingStyleUpdates() const { + return mMightHavePendingStyleUpdates; + } + + bool PreTraverse(); + bool PreTraverseInSubtree(mozilla::dom::Element* aRoot); + + protected: + ~SMILAnimationController(); + + // alias declarations + using TimeContainerPtrKey = nsPtrHashKey; + using TimeContainerHashtable = nsTHashtable; + using AnimationElementPtrKey = nsPtrHashKey; + using AnimationElementHashtable = nsTHashtable; + + // Returns mDocument's refresh driver, if it's got one. + nsRefreshDriver* GetRefreshDriver(); + + // Methods for controlling whether we're sampling + void UpdateSampling(); + bool ShouldSample() const; + + void StopSampling(nsRefreshDriver* aRefreshDriver); + + // Wrapper for StartSampling that defers if no animations are registered. + void MaybeStartSampling(nsRefreshDriver* aRefreshDriver); + + // Sample-related callbacks and implementation helpers + void DoSample() override; + void DoSample(bool aSkipUnchangedContainers); + + void RewindElements(); + + void DoMilestoneSamples(); + + static void SampleTimedElement(mozilla::dom::SVGAnimationElement* aElement, + TimeContainerHashtable* aActiveContainers); + + static void AddAnimationToCompositorTable( + mozilla::dom::SVGAnimationElement* aElement, + SMILCompositorTable* aCompositorTable, bool& aStyleFlushNeeded); + + static bool GetTargetIdentifierForAnimation( + mozilla::dom::SVGAnimationElement* aAnimElem, + SMILTargetIdentifier& aResult); + + // Methods for adding/removing time containers + nsresult AddChild(SMILTimeContainer& aChild) override; + void RemoveChild(SMILTimeContainer& aChild) override; + + void FlagDocumentNeedsFlush(); + + // Members + nsAutoRefCnt mRefCnt; + NS_DECL_OWNINGTHREAD + + AnimationElementHashtable mAnimationElementTable; + TimeContainerHashtable mChildContainerTable; + mozilla::TimeStamp mCurrentSampleTime; + mozilla::TimeStamp mStartTime; + + // Average time between samples from the refresh driver. This is used to + // detect large unexpected gaps between samples such as can occur when the + // computer sleeps. The nature of the SMIL model means that catching up these + // large gaps can be expensive as, for example, many events may need to be + // dispatched for the intervening time when no samples were received. + // + // In such cases, we ignore the intervening gap and continue sampling from + // when we were expecting the next sample to arrive. + // + // Note that we only do this for SMIL and not CSS transitions (which doesn't + // have so much work to do to catch up) nor scripted animations (which expect + // animation time to follow real time). + // + // This behaviour does not affect pausing (since we're not *expecting* any + // samples then) nor seeking (where the SMIL model behaves somewhat + // differently such as not dispatching events). + SMILTime mAvgTimeBetweenSamples = 0; + + bool mResampleNeeded = false; + bool mRunningSample = false; + + // Are we registered with our document's refresh driver? + bool mRegisteredWithRefreshDriver = false; + + // Have we updated animated values without adding them to the restyle tracker? + bool mMightHavePendingStyleUpdates = false; + + // Store raw ptr to mDocument. It owns the controller, so controller + // shouldn't outlive it + mozilla::dom::Document* mDocument; + + // Contains compositors used in our last sample. We keep this around + // so we can detect when an element/attribute used to be animated, + // but isn't anymore for some reason. (e.g. if its element is + // removed or retargeted) + UniquePtr mLastCompositorTable; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILANIMATIONCONTROLLER_H_ diff --git a/dom/smil/SMILAnimationFunction.cpp b/dom/smil/SMILAnimationFunction.cpp new file mode 100644 index 0000000000..032ebd60c3 --- /dev/null +++ b/dom/smil/SMILAnimationFunction.cpp @@ -0,0 +1,993 @@ +/* -*- 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 "SMILAnimationFunction.h" + +#include + +#include +#include + +#include "mozilla/DebugOnly.h" +#include "mozilla/SMILAttr.h" +#include "mozilla/SMILCSSValueType.h" +#include "mozilla/SMILNullType.h" +#include "mozilla/SMILParserUtils.h" +#include "mozilla/SMILTimedElement.h" +#include "mozilla/dom/SVGAnimationElement.h" +#include "nsAttrValueInlines.h" +#include "nsCOMArray.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsGkAtoms.h" +#include "nsIContent.h" +#include "nsReadableUtils.h" +#include "nsString.h" + +using namespace mozilla::dom; + +namespace mozilla { + +//---------------------------------------------------------------------- +// Static members + +nsAttrValue::EnumTable SMILAnimationFunction::sAccumulateTable[] = { + {"none", false}, {"sum", true}, {nullptr, 0}}; + +nsAttrValue::EnumTable SMILAnimationFunction::sAdditiveTable[] = { + {"replace", false}, {"sum", true}, {nullptr, 0}}; + +nsAttrValue::EnumTable SMILAnimationFunction::sCalcModeTable[] = { + {"linear", CALC_LINEAR}, + {"discrete", CALC_DISCRETE}, + {"paced", CALC_PACED}, + {"spline", CALC_SPLINE}, + {nullptr, 0}}; + +// Any negative number should be fine as a sentinel here, +// because valid distances are non-negative. +#define COMPUTE_DISTANCE_ERROR (-1) + +//---------------------------------------------------------------------- +// Constructors etc. + +SMILAnimationFunction::SMILAnimationFunction() + : mSampleTime(-1), + mRepeatIteration(0), + mBeginTime(INT64_MIN), + mAnimationElement(nullptr), + mErrorFlags(0), + mIsActive(false), + mIsFrozen(false), + mLastValue(false), + mHasChanged(true), + mValueNeedsReparsingEverySample(false), + mPrevSampleWasSingleValueAnimation(false), + mWasSkippedInPrevSample(false) {} + +void SMILAnimationFunction::SetAnimationElement( + SVGAnimationElement* aAnimationElement) { + mAnimationElement = aAnimationElement; +} + +bool SMILAnimationFunction::SetAttr(nsAtom* aAttribute, const nsAString& aValue, + nsAttrValue& aResult, + nsresult* aParseResult) { + // Some elements such as set and discard don't support all possible attributes + if (IsDisallowedAttribute(aAttribute)) { + aResult.SetTo(aValue); + if (aParseResult) { + *aParseResult = NS_OK; + } + return true; + } + + bool foundMatch = true; + nsresult parseResult = NS_OK; + + // The attributes 'by', 'from', 'to', and 'values' may be parsed differently + // depending on the element & attribute we're animating. So instead of + // parsing them now we re-parse them at every sample. + if (aAttribute == nsGkAtoms::by || aAttribute == nsGkAtoms::from || + aAttribute == nsGkAtoms::to || aAttribute == nsGkAtoms::values) { + // We parse to, from, by, values at sample time. + // XXX Need to flag which attribute has changed and then when we parse it at + // sample time, report any errors and reset the flag + mHasChanged = true; + aResult.SetTo(aValue); + } else if (aAttribute == nsGkAtoms::accumulate) { + parseResult = SetAccumulate(aValue, aResult); + } else if (aAttribute == nsGkAtoms::additive) { + parseResult = SetAdditive(aValue, aResult); + } else if (aAttribute == nsGkAtoms::calcMode) { + parseResult = SetCalcMode(aValue, aResult); + } else if (aAttribute == nsGkAtoms::keyTimes) { + parseResult = SetKeyTimes(aValue, aResult); + } else if (aAttribute == nsGkAtoms::keySplines) { + parseResult = SetKeySplines(aValue, aResult); + } else { + foundMatch = false; + } + + if (foundMatch && aParseResult) { + *aParseResult = parseResult; + } + + return foundMatch; +} + +bool SMILAnimationFunction::UnsetAttr(nsAtom* aAttribute) { + if (IsDisallowedAttribute(aAttribute)) { + return true; + } + + bool foundMatch = true; + + if (aAttribute == nsGkAtoms::by || aAttribute == nsGkAtoms::from || + aAttribute == nsGkAtoms::to || aAttribute == nsGkAtoms::values) { + mHasChanged = true; + } else if (aAttribute == nsGkAtoms::accumulate) { + UnsetAccumulate(); + } else if (aAttribute == nsGkAtoms::additive) { + UnsetAdditive(); + } else if (aAttribute == nsGkAtoms::calcMode) { + UnsetCalcMode(); + } else if (aAttribute == nsGkAtoms::keyTimes) { + UnsetKeyTimes(); + } else if (aAttribute == nsGkAtoms::keySplines) { + UnsetKeySplines(); + } else { + foundMatch = false; + } + + return foundMatch; +} + +void SMILAnimationFunction::SampleAt(SMILTime aSampleTime, + const SMILTimeValue& aSimpleDuration, + uint32_t aRepeatIteration) { + // * Update mHasChanged ("Might this sample be different from prev one?") + // Were we previously sampling a fill="freeze" final val? (We're not anymore.) + mHasChanged |= mLastValue; + + // Are we sampling at a new point in simple duration? And does that matter? + mHasChanged |= + (mSampleTime != aSampleTime || mSimpleDuration != aSimpleDuration) && + !IsValueFixedForSimpleDuration(); + + // Are we on a new repeat and accumulating across repeats? + if (!mErrorFlags) { // (can't call GetAccumulate() if we've had parse errors) + mHasChanged |= (mRepeatIteration != aRepeatIteration) && GetAccumulate(); + } + + mSampleTime = aSampleTime; + mSimpleDuration = aSimpleDuration; + mRepeatIteration = aRepeatIteration; + mLastValue = false; +} + +void SMILAnimationFunction::SampleLastValue(uint32_t aRepeatIteration) { + if (!mLastValue || mRepeatIteration != aRepeatIteration) { + mHasChanged = true; + } + + mRepeatIteration = aRepeatIteration; + mLastValue = true; +} + +void SMILAnimationFunction::Activate(SMILTime aBeginTime) { + mBeginTime = aBeginTime; + mIsActive = true; + mIsFrozen = false; + mHasChanged = true; +} + +void SMILAnimationFunction::Inactivate(bool aIsFrozen) { + mIsActive = false; + mIsFrozen = aIsFrozen; + mHasChanged = true; +} + +void SMILAnimationFunction::ComposeResult(const SMILAttr& aSMILAttr, + SMILValue& aResult) { + mHasChanged = false; + mPrevSampleWasSingleValueAnimation = false; + mWasSkippedInPrevSample = false; + + // Skip animations that are inactive or in error + if (!IsActiveOrFrozen() || mErrorFlags != 0) return; + + // Get the animation values + SMILValueArray values; + nsresult rv = GetValues(aSMILAttr, values); + if (NS_FAILED(rv)) return; + + // Check that we have the right number of keySplines and keyTimes + CheckValueListDependentAttrs(values.Length()); + if (mErrorFlags != 0) return; + + // If this interval is active, we must have a non-negative mSampleTime + MOZ_ASSERT(mSampleTime >= 0 || !mIsActive, + "Negative sample time for active animation"); + MOZ_ASSERT(mSimpleDuration.IsResolved() || mLastValue, + "Unresolved simple duration for active or frozen animation"); + + // If we want to add but don't have a base value then just fail outright. + // This can happen when we skipped getting the base value because there's an + // animation function in the sandwich that should replace it but that function + // failed unexpectedly. + bool isAdditive = IsAdditive(); + if (isAdditive && aResult.IsNull()) return; + + SMILValue result; + + if (values.Length() == 1 && !IsToAnimation()) { + // Single-valued animation + result = values[0]; + mPrevSampleWasSingleValueAnimation = true; + + } else if (mLastValue) { + // Sampling last value + const SMILValue& last = values.LastElement(); + result = last; + + // See comment in AccumulateResult: to-animation does not accumulate + if (!IsToAnimation() && GetAccumulate() && mRepeatIteration) { + // If the target attribute type doesn't support addition Add will + // fail leaving result = last + result.Add(last, mRepeatIteration); + } + + } else { + // Interpolation + if (NS_FAILED(InterpolateResult(values, result, aResult))) return; + + if (NS_FAILED(AccumulateResult(values, result))) return; + } + + // If additive animation isn't required or isn't supported, set the value. + if (!isAdditive || NS_FAILED(aResult.SandwichAdd(result))) { + aResult = std::move(result); + } +} + +int8_t SMILAnimationFunction::CompareTo( + const SMILAnimationFunction* aOther) const { + NS_ENSURE_TRUE(aOther, 0); + + NS_ASSERTION(aOther != this, "Trying to compare to self"); + + // Inactive animations sort first + if (!IsActiveOrFrozen() && aOther->IsActiveOrFrozen()) return -1; + + if (IsActiveOrFrozen() && !aOther->IsActiveOrFrozen()) return 1; + + // Sort based on begin time + if (mBeginTime != aOther->GetBeginTime()) + return mBeginTime > aOther->GetBeginTime() ? 1 : -1; + + // Next sort based on syncbase dependencies: the dependent element sorts after + // its syncbase + const SMILTimedElement& thisTimedElement = mAnimationElement->TimedElement(); + const SMILTimedElement& otherTimedElement = + aOther->mAnimationElement->TimedElement(); + if (thisTimedElement.IsTimeDependent(otherTimedElement)) return 1; + if (otherTimedElement.IsTimeDependent(thisTimedElement)) return -1; + + // Animations that appear later in the document sort after those earlier in + // the document + MOZ_ASSERT(mAnimationElement != aOther->mAnimationElement, + "Two animations cannot have the same animation content element!"); + + return (nsContentUtils::PositionIsBefore(mAnimationElement, + aOther->mAnimationElement)) + ? -1 + : 1; +} + +bool SMILAnimationFunction::WillReplace() const { + /* + * In IsAdditive() we don't consider to-animation to be additive as it is + * a special case that is dealt with differently in the compositing method. + * Here, however, we return FALSE for to-animation (i.e. it will NOT replace + * the underlying value) as it builds on the underlying value. + */ + return !mErrorFlags && !(IsAdditive() || IsToAnimation()); +} + +bool SMILAnimationFunction::HasChanged() const { + return mHasChanged || mValueNeedsReparsingEverySample; +} + +bool SMILAnimationFunction::UpdateCachedTarget( + const SMILTargetIdentifier& aNewTarget) { + if (!mLastTarget.Equals(aNewTarget)) { + mLastTarget = aNewTarget; + return true; + } + return false; +} + +//---------------------------------------------------------------------- +// Implementation helpers + +nsresult SMILAnimationFunction::InterpolateResult(const SMILValueArray& aValues, + SMILValue& aResult, + SMILValue& aBaseValue) { + // Sanity check animation values + if ((!IsToAnimation() && aValues.Length() < 2) || + (IsToAnimation() && aValues.Length() != 1)) { + NS_ERROR("Unexpected number of values"); + return NS_ERROR_FAILURE; + } + + if (IsToAnimation() && aBaseValue.IsNull()) { + return NS_ERROR_FAILURE; + } + + // Get the normalised progress through the simple duration. + // + // If we have an indefinite simple duration, just set the progress to be + // 0 which will give us the expected behaviour of the animation being fixed at + // its starting point. + double simpleProgress = 0.0; + + if (mSimpleDuration.IsDefinite()) { + SMILTime dur = mSimpleDuration.GetMillis(); + + MOZ_ASSERT(dur >= 0, "Simple duration should not be negative"); + MOZ_ASSERT(mSampleTime >= 0, "Sample time should not be negative"); + + if (mSampleTime >= dur || mSampleTime < 0) { + NS_ERROR("Animation sampled outside interval"); + return NS_ERROR_FAILURE; + } + + if (dur > 0) { + simpleProgress = (double)mSampleTime / dur; + } // else leave simpleProgress at 0.0 (e.g. if mSampleTime == dur == 0) + } + + nsresult rv = NS_OK; + SMILCalcMode calcMode = GetCalcMode(); + + // Force discrete calcMode for visibility since StyleAnimationValue will + // try to interpolate it using the special clamping behavior defined for + // CSS. + if (SMILCSSValueType::PropertyFromValue(aValues[0]) == + eCSSProperty_visibility) { + calcMode = CALC_DISCRETE; + } + + if (calcMode != CALC_DISCRETE) { + // Get the normalised progress between adjacent values + const SMILValue* from = nullptr; + const SMILValue* to = nullptr; + // Init to -1 to make sure that if we ever forget to set this, the + // MOZ_ASSERT that tests that intervalProgress is in range will fail. + double intervalProgress = -1.f; + if (IsToAnimation()) { + from = &aBaseValue; + to = &aValues[0]; + if (calcMode == CALC_PACED) { + // Note: key[Times/Splines/Points] are ignored for calcMode="paced" + intervalProgress = simpleProgress; + } else { + double scaledSimpleProgress = + ScaleSimpleProgress(simpleProgress, calcMode); + intervalProgress = ScaleIntervalProgress(scaledSimpleProgress, 0); + } + } else if (calcMode == CALC_PACED) { + rv = ComputePacedPosition(aValues, simpleProgress, intervalProgress, from, + to); + // Note: If the above call fails, we'll skip the "from->Interpolate" + // call below, and we'll drop into the CALC_DISCRETE section + // instead. (as the spec says we should, because our failure was + // presumably due to the values being non-additive) + } else { // calcMode == CALC_LINEAR or calcMode == CALC_SPLINE + double scaledSimpleProgress = + ScaleSimpleProgress(simpleProgress, calcMode); + uint32_t index = + (uint32_t)floor(scaledSimpleProgress * (aValues.Length() - 1)); + from = &aValues[index]; + to = &aValues[index + 1]; + intervalProgress = scaledSimpleProgress * (aValues.Length() - 1) - index; + intervalProgress = ScaleIntervalProgress(intervalProgress, index); + } + + if (NS_SUCCEEDED(rv)) { + MOZ_ASSERT(from, "NULL from-value during interpolation"); + MOZ_ASSERT(to, "NULL to-value during interpolation"); + MOZ_ASSERT(0.0f <= intervalProgress && intervalProgress < 1.0f, + "Interval progress should be in the range [0, 1)"); + rv = from->Interpolate(*to, intervalProgress, aResult); + } + } + + // Discrete-CalcMode case + // Note: If interpolation failed (isn't supported for this type), the SVG + // spec says to force discrete mode. + if (calcMode == CALC_DISCRETE || NS_FAILED(rv)) { + double scaledSimpleProgress = + ScaleSimpleProgress(simpleProgress, CALC_DISCRETE); + + // Floating-point errors can mean that, for example, a sample time of 29s in + // a 100s duration animation gives us a simple progress of 0.28999999999 + // instead of the 0.29 we'd expect. Normally this isn't a noticeable + // problem, but when we have sudden jumps in animation values (such as is + // the case here with discrete animation) we can get unexpected results. + // + // To counteract this, before we perform a floor() on the animation + // progress, we add a tiny fudge factor to push us into the correct interval + // in cases where floating-point errors might cause us to fall short. + static const double kFloatingPointFudgeFactor = 1.0e-16; + if (scaledSimpleProgress + kFloatingPointFudgeFactor <= 1.0) { + scaledSimpleProgress += kFloatingPointFudgeFactor; + } + + if (IsToAnimation()) { + // We don't follow SMIL 3, 12.6.4, where discrete to animations + // are the same as animations. Instead, we treat it as a + // discrete animation with two values (the underlying value and + // the to="" value), and honor keyTimes="" as well. + uint32_t index = (uint32_t)floor(scaledSimpleProgress * 2); + aResult = index == 0 ? aBaseValue : aValues[0]; + } else { + uint32_t index = (uint32_t)floor(scaledSimpleProgress * aValues.Length()); + aResult = aValues[index]; + + // For animation of CSS properties, normally when interpolating we perform + // a zero-value fixup which means that empty values (values with type + // SMILCSSValueType but a null pointer value) are converted into + // a suitable zero value based on whatever they're being interpolated + // with. For discrete animation, however, since we don't interpolate, + // that never happens. In some rare cases, such as discrete non-additive + // by-animation, we can arrive here with |aResult| being such an empty + // value so we need to manually perform the fixup. + // + // We could define a generic method for this on SMILValue but its faster + // and simpler to just special case SMILCSSValueType. + if (aResult.mType == &SMILCSSValueType::sSingleton) { + // We have currently only ever encountered this case for the first + // value of a by-animation (which has two values) and since we have no + // way of testing other cases we just skip them (but assert if we + // ever do encounter them so that we can add code to handle them). + if (index + 1 >= aValues.Length()) { + MOZ_ASSERT(aResult.mU.mPtr, "The last value should not be empty"); + } else { + // Base the type of the zero value on the next element in the series. + SMILCSSValueType::FinalizeValue(aResult, aValues[index + 1]); + } + } + } + rv = NS_OK; + } + return rv; +} + +nsresult SMILAnimationFunction::AccumulateResult(const SMILValueArray& aValues, + SMILValue& aResult) { + if (!IsToAnimation() && GetAccumulate() && mRepeatIteration) { + // If the target attribute type doesn't support addition, Add will + // fail and we leave aResult untouched. + aResult.Add(aValues.LastElement(), mRepeatIteration); + } + + return NS_OK; +} + +/* + * Given the simple progress for a paced animation, this method: + * - determines which two elements of the values array we're in between + * (returned as aFrom and aTo) + * - determines where we are between them + * (returned as aIntervalProgress) + * + * Returns NS_OK, or NS_ERROR_FAILURE if our values don't support distance + * computation. + */ +nsresult SMILAnimationFunction::ComputePacedPosition( + const SMILValueArray& aValues, double aSimpleProgress, + double& aIntervalProgress, const SMILValue*& aFrom, const SMILValue*& aTo) { + NS_ASSERTION(0.0f <= aSimpleProgress && aSimpleProgress < 1.0f, + "aSimpleProgress is out of bounds"); + NS_ASSERTION(GetCalcMode() == CALC_PACED, + "Calling paced-specific function, but not in paced mode"); + MOZ_ASSERT(aValues.Length() >= 2, "Unexpected number of values"); + + // Trivial case: If we have just 2 values, then there's only one interval + // for us to traverse, and our progress across that interval is the exact + // same as our overall progress. + if (aValues.Length() == 2) { + aIntervalProgress = aSimpleProgress; + aFrom = &aValues[0]; + aTo = &aValues[1]; + return NS_OK; + } + + double totalDistance = ComputePacedTotalDistance(aValues); + if (totalDistance == COMPUTE_DISTANCE_ERROR) return NS_ERROR_FAILURE; + + // If we have 0 total distance, then it's unclear where our "paced" position + // should be. We can just fail, which drops us into discrete animation mode. + // (That's fine, since our values are apparently indistinguishable anyway.) + if (totalDistance == 0.0) { + return NS_ERROR_FAILURE; + } + + // total distance we should have moved at this point in time. + // (called 'remainingDist' due to how it's used in loop below) + double remainingDist = aSimpleProgress * totalDistance; + + // Must be satisfied, because totalDistance is a sum of (non-negative) + // distances, and aSimpleProgress is non-negative + NS_ASSERTION(remainingDist >= 0, "distance values must be non-negative"); + + // Find where remainingDist puts us in the list of values + // Note: We could optimize this next loop by caching the + // interval-distances in an array, but maybe that's excessive. + for (uint32_t i = 0; i < aValues.Length() - 1; i++) { + // Note: The following assertion is valid because remainingDist should + // start out non-negative, and this loop never shaves off more than its + // current value. + NS_ASSERTION(remainingDist >= 0, "distance values must be non-negative"); + + double curIntervalDist; + + DebugOnly rv = + aValues[i].ComputeDistance(aValues[i + 1], curIntervalDist); + MOZ_ASSERT(NS_SUCCEEDED(rv), + "If we got through ComputePacedTotalDistance, we should " + "be able to recompute each sub-distance without errors"); + + NS_ASSERTION(curIntervalDist >= 0, "distance values must be non-negative"); + // Clamp distance value at 0, just in case ComputeDistance is evil. + curIntervalDist = std::max(curIntervalDist, 0.0); + + if (remainingDist >= curIntervalDist) { + remainingDist -= curIntervalDist; + } else { + // NOTE: If we get here, then curIntervalDist necessarily is not 0. Why? + // Because this clause is only hit when remainingDist < curIntervalDist, + // and if curIntervalDist were 0, that would mean remainingDist would + // have to be < 0. But that can't happen, because remainingDist (as + // a distance) is non-negative by definition. + NS_ASSERTION(curIntervalDist != 0, + "We should never get here with this set to 0..."); + + // We found the right spot -- an interpolated position between + // values i and i+1. + aFrom = &aValues[i]; + aTo = &aValues[i + 1]; + aIntervalProgress = remainingDist / curIntervalDist; + return NS_OK; + } + } + + MOZ_ASSERT_UNREACHABLE( + "shouldn't complete loop & get here -- if we do, " + "then aSimpleProgress was probably out of bounds"); + return NS_ERROR_FAILURE; +} + +/* + * Computes the total distance to be travelled by a paced animation. + * + * Returns the total distance, or returns COMPUTE_DISTANCE_ERROR if + * our values don't support distance computation. + */ +double SMILAnimationFunction::ComputePacedTotalDistance( + const SMILValueArray& aValues) const { + NS_ASSERTION(GetCalcMode() == CALC_PACED, + "Calling paced-specific function, but not in paced mode"); + + double totalDistance = 0.0; + for (uint32_t i = 0; i < aValues.Length() - 1; i++) { + double tmpDist; + nsresult rv = aValues[i].ComputeDistance(aValues[i + 1], tmpDist); + if (NS_FAILED(rv)) { + return COMPUTE_DISTANCE_ERROR; + } + + // Clamp distance value to 0, just in case we have an evil ComputeDistance + // implementation somewhere + MOZ_ASSERT(tmpDist >= 0.0f, "distance values must be non-negative"); + tmpDist = std::max(tmpDist, 0.0); + + totalDistance += tmpDist; + } + + return totalDistance; +} + +double SMILAnimationFunction::ScaleSimpleProgress(double aProgress, + SMILCalcMode aCalcMode) { + if (!HasAttr(nsGkAtoms::keyTimes)) return aProgress; + + uint32_t numTimes = mKeyTimes.Length(); + + if (numTimes < 2) return aProgress; + + uint32_t i = 0; + for (; i < numTimes - 2 && aProgress >= mKeyTimes[i + 1]; ++i) { + } + + if (aCalcMode == CALC_DISCRETE) { + // discrete calcMode behaviour differs in that each keyTime defines the time + // from when the corresponding value is set, and therefore the last value + // needn't be 1. So check if we're in the last 'interval', that is, the + // space between the final value and 1.0. + if (aProgress >= mKeyTimes[i + 1]) { + MOZ_ASSERT(i == numTimes - 2, + "aProgress is not in range of the current interval, yet the " + "current interval is not the last bounded interval either."); + ++i; + } + return (double)i / numTimes; + } + + double& intervalStart = mKeyTimes[i]; + double& intervalEnd = mKeyTimes[i + 1]; + + double intervalLength = intervalEnd - intervalStart; + if (intervalLength <= 0.0) return intervalStart; + + return (i + (aProgress - intervalStart) / intervalLength) / + double(numTimes - 1); +} + +double SMILAnimationFunction::ScaleIntervalProgress(double aProgress, + uint32_t aIntervalIndex) { + if (GetCalcMode() != CALC_SPLINE) return aProgress; + + if (!HasAttr(nsGkAtoms::keySplines)) return aProgress; + + MOZ_ASSERT(aIntervalIndex < mKeySplines.Length(), "Invalid interval index"); + + SMILKeySpline const& spline = mKeySplines[aIntervalIndex]; + return spline.GetSplineValue(aProgress); +} + +bool SMILAnimationFunction::HasAttr(nsAtom* aAttName) const { + if (IsDisallowedAttribute(aAttName)) { + return false; + } + return mAnimationElement->HasAttr(aAttName); +} + +const nsAttrValue* SMILAnimationFunction::GetAttr(nsAtom* aAttName) const { + if (IsDisallowedAttribute(aAttName)) { + return nullptr; + } + return mAnimationElement->GetParsedAttr(aAttName); +} + +bool SMILAnimationFunction::GetAttr(nsAtom* aAttName, + nsAString& aResult) const { + if (IsDisallowedAttribute(aAttName)) { + return false; + } + return mAnimationElement->GetAttr(aAttName, aResult); +} + +/* + * A utility function to make querying an attribute that corresponds to an + * SMILValue a little neater. + * + * @param aAttName The attribute name (in the global namespace). + * @param aSMILAttr The SMIL attribute to perform the parsing. + * @param[out] aResult The resulting SMILValue. + * @param[out] aPreventCachingOfSandwich + * If |aResult| contains dependencies on its context that + * should prevent the result of the animation sandwich from + * being cached and reused in future samples (as reported + * by SMILAttr::ValueFromString), then this outparam + * will be set to true. Otherwise it is left unmodified. + * + * Returns false if a parse error occurred, otherwise returns true. + */ +bool SMILAnimationFunction::ParseAttr(nsAtom* aAttName, + const SMILAttr& aSMILAttr, + SMILValue& aResult, + bool& aPreventCachingOfSandwich) const { + nsAutoString attValue; + if (GetAttr(aAttName, attValue)) { + nsresult rv = aSMILAttr.ValueFromString(attValue, mAnimationElement, + aResult, aPreventCachingOfSandwich); + if (NS_FAILED(rv)) return false; + } + return true; +} + +/* + * SMILANIM specifies the following rules for animation function values: + * + * (1) if values is set, it overrides everything + * (2) for from/to/by animation at least to or by must be specified, from on its + * own (or nothing) is an error--which we will ignore + * (3) if both by and to are specified only to will be used, by will be ignored + * (4) if by is specified without from (by animation), forces additive behaviour + * (5) if to is specified without from (to animation), special care needs to be + * taken when compositing animation as such animations are composited last. + * + * This helper method applies these rules to fill in the values list and to set + * some internal state. + */ +nsresult SMILAnimationFunction::GetValues(const SMILAttr& aSMILAttr, + SMILValueArray& aResult) { + if (!mAnimationElement) return NS_ERROR_FAILURE; + + mValueNeedsReparsingEverySample = false; + SMILValueArray result; + + // If "values" is set, use it + if (HasAttr(nsGkAtoms::values)) { + nsAutoString attValue; + GetAttr(nsGkAtoms::values, attValue); + bool preventCachingOfSandwich = false; + if (!SMILParserUtils::ParseValues(attValue, mAnimationElement, aSMILAttr, + result, preventCachingOfSandwich)) { + return NS_ERROR_FAILURE; + } + + if (preventCachingOfSandwich) { + mValueNeedsReparsingEverySample = true; + } + // Else try to/from/by + } else { + bool preventCachingOfSandwich = false; + bool parseOk = true; + SMILValue to, from, by; + parseOk &= + ParseAttr(nsGkAtoms::to, aSMILAttr, to, preventCachingOfSandwich); + parseOk &= + ParseAttr(nsGkAtoms::from, aSMILAttr, from, preventCachingOfSandwich); + parseOk &= + ParseAttr(nsGkAtoms::by, aSMILAttr, by, preventCachingOfSandwich); + + if (preventCachingOfSandwich) { + mValueNeedsReparsingEverySample = true; + } + + if (!parseOk || !result.SetCapacity(2, fallible)) { + return NS_ERROR_FAILURE; + } + + // AppendElement() below must succeed, because SetCapacity() succeeded. + if (!to.IsNull()) { + if (!from.IsNull()) { + MOZ_ALWAYS_TRUE(result.AppendElement(from, fallible)); + MOZ_ALWAYS_TRUE(result.AppendElement(to, fallible)); + } else { + MOZ_ALWAYS_TRUE(result.AppendElement(to, fallible)); + } + } else if (!by.IsNull()) { + SMILValue effectiveFrom(by.mType); + if (!from.IsNull()) effectiveFrom = from; + // Set values to 'from; from + by' + MOZ_ALWAYS_TRUE(result.AppendElement(effectiveFrom, fallible)); + SMILValue effectiveTo(effectiveFrom); + if (!effectiveTo.IsNull() && NS_SUCCEEDED(effectiveTo.Add(by))) { + MOZ_ALWAYS_TRUE(result.AppendElement(effectiveTo, fallible)); + } else { + // Using by-animation with non-additive type or bad base-value + return NS_ERROR_FAILURE; + } + } else { + // No values, no to, no by -- call it a day + return NS_ERROR_FAILURE; + } + } + + aResult = std::move(result); + + return NS_OK; +} + +void SMILAnimationFunction::CheckValueListDependentAttrs(uint32_t aNumValues) { + CheckKeyTimes(aNumValues); + CheckKeySplines(aNumValues); +} + +/** + * Performs checks for the keyTimes attribute required by the SMIL spec but + * which depend on other attributes and therefore needs to be updated as + * dependent attributes are set. + */ +void SMILAnimationFunction::CheckKeyTimes(uint32_t aNumValues) { + if (!HasAttr(nsGkAtoms::keyTimes)) return; + + SMILCalcMode calcMode = GetCalcMode(); + + // attribute is ignored for calcMode = paced + if (calcMode == CALC_PACED) { + SetKeyTimesErrorFlag(false); + return; + } + + uint32_t numKeyTimes = mKeyTimes.Length(); + if (numKeyTimes < 1) { + // keyTimes isn't set or failed preliminary checks + SetKeyTimesErrorFlag(true); + return; + } + + // no. keyTimes == no. values + // For to-animation the number of values is considered to be 2. + bool matchingNumOfValues = numKeyTimes == (IsToAnimation() ? 2 : aNumValues); + if (!matchingNumOfValues) { + SetKeyTimesErrorFlag(true); + return; + } + + // first value must be 0 + if (mKeyTimes[0] != 0.0) { + SetKeyTimesErrorFlag(true); + return; + } + + // last value must be 1 for linear or spline calcModes + if (calcMode != CALC_DISCRETE && numKeyTimes > 1 && + mKeyTimes.LastElement() != 1.0) { + SetKeyTimesErrorFlag(true); + return; + } + + SetKeyTimesErrorFlag(false); +} + +void SMILAnimationFunction::CheckKeySplines(uint32_t aNumValues) { + // attribute is ignored if calc mode is not spline + if (GetCalcMode() != CALC_SPLINE) { + SetKeySplinesErrorFlag(false); + return; + } + + // calc mode is spline but the attribute is not set + if (!HasAttr(nsGkAtoms::keySplines)) { + SetKeySplinesErrorFlag(false); + return; + } + + if (mKeySplines.Length() < 1) { + // keyTimes isn't set or failed preliminary checks + SetKeySplinesErrorFlag(true); + return; + } + + // ignore splines if there's only one value + if (aNumValues == 1 && !IsToAnimation()) { + SetKeySplinesErrorFlag(false); + return; + } + + // no. keySpline specs == no. values - 1 + uint32_t splineSpecs = mKeySplines.Length(); + if ((splineSpecs != aNumValues - 1 && !IsToAnimation()) || + (IsToAnimation() && splineSpecs != 1)) { + SetKeySplinesErrorFlag(true); + return; + } + + SetKeySplinesErrorFlag(false); +} + +bool SMILAnimationFunction::IsValueFixedForSimpleDuration() const { + return mSimpleDuration.IsIndefinite() || + (!mHasChanged && mPrevSampleWasSingleValueAnimation); +} + +//---------------------------------------------------------------------- +// Property getters + +bool SMILAnimationFunction::GetAccumulate() const { + const nsAttrValue* value = GetAttr(nsGkAtoms::accumulate); + if (!value) return false; + + return value->GetEnumValue(); +} + +bool SMILAnimationFunction::GetAdditive() const { + const nsAttrValue* value = GetAttr(nsGkAtoms::additive); + if (!value) return false; + + return value->GetEnumValue(); +} + +SMILAnimationFunction::SMILCalcMode SMILAnimationFunction::GetCalcMode() const { + const nsAttrValue* value = GetAttr(nsGkAtoms::calcMode); + if (!value) return CALC_LINEAR; + + return SMILCalcMode(value->GetEnumValue()); +} + +//---------------------------------------------------------------------- +// Property setters / un-setters: + +nsresult SMILAnimationFunction::SetAccumulate(const nsAString& aAccumulate, + nsAttrValue& aResult) { + mHasChanged = true; + bool parseResult = + aResult.ParseEnumValue(aAccumulate, sAccumulateTable, true); + SetAccumulateErrorFlag(!parseResult); + return parseResult ? NS_OK : NS_ERROR_FAILURE; +} + +void SMILAnimationFunction::UnsetAccumulate() { + SetAccumulateErrorFlag(false); + mHasChanged = true; +} + +nsresult SMILAnimationFunction::SetAdditive(const nsAString& aAdditive, + nsAttrValue& aResult) { + mHasChanged = true; + bool parseResult = aResult.ParseEnumValue(aAdditive, sAdditiveTable, true); + SetAdditiveErrorFlag(!parseResult); + return parseResult ? NS_OK : NS_ERROR_FAILURE; +} + +void SMILAnimationFunction::UnsetAdditive() { + SetAdditiveErrorFlag(false); + mHasChanged = true; +} + +nsresult SMILAnimationFunction::SetCalcMode(const nsAString& aCalcMode, + nsAttrValue& aResult) { + mHasChanged = true; + bool parseResult = aResult.ParseEnumValue(aCalcMode, sCalcModeTable, true); + SetCalcModeErrorFlag(!parseResult); + return parseResult ? NS_OK : NS_ERROR_FAILURE; +} + +void SMILAnimationFunction::UnsetCalcMode() { + SetCalcModeErrorFlag(false); + mHasChanged = true; +} + +nsresult SMILAnimationFunction::SetKeySplines(const nsAString& aKeySplines, + nsAttrValue& aResult) { + mKeySplines.Clear(); + aResult.SetTo(aKeySplines); + + mHasChanged = true; + + if (!SMILParserUtils::ParseKeySplines(aKeySplines, mKeySplines)) { + mKeySplines.Clear(); + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +void SMILAnimationFunction::UnsetKeySplines() { + mKeySplines.Clear(); + SetKeySplinesErrorFlag(false); + mHasChanged = true; +} + +nsresult SMILAnimationFunction::SetKeyTimes(const nsAString& aKeyTimes, + nsAttrValue& aResult) { + mKeyTimes.Clear(); + aResult.SetTo(aKeyTimes); + + mHasChanged = true; + + if (!SMILParserUtils::ParseSemicolonDelimitedProgressList(aKeyTimes, true, + mKeyTimes)) { + mKeyTimes.Clear(); + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +void SMILAnimationFunction::UnsetKeyTimes() { + mKeyTimes.Clear(); + SetKeyTimesErrorFlag(false); + mHasChanged = true; +} + +} // namespace mozilla diff --git a/dom/smil/SMILAnimationFunction.h b/dom/smil/SMILAnimationFunction.h new file mode 100644 index 0000000000..8e85da76f6 --- /dev/null +++ b/dom/smil/SMILAnimationFunction.h @@ -0,0 +1,444 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILANIMATIONFUNCTION_H_ +#define DOM_SMIL_SMILANIMATIONFUNCTION_H_ + +#include "mozilla/SMILAttr.h" +#include "mozilla/SMILKeySpline.h" +#include "mozilla/SMILTargetIdentifier.h" +#include "mozilla/SMILTimeValue.h" +#include "mozilla/SMILTypes.h" +#include "mozilla/SMILValue.h" +#include "nsAttrValue.h" +#include "nsGkAtoms.h" +#include "nsString.h" +#include "nsTArray.h" + +namespace mozilla { +namespace dom { +class SVGAnimationElement; +} // namespace dom + +//---------------------------------------------------------------------- +// SMILAnimationFunction +// +// The animation function calculates animation values. It it is provided with +// time parameters (sample time, repeat iteration etc.) and it uses this to +// build an appropriate animation value by performing interpolation and +// addition operations. +// +// It is responsible for implementing the animation parameters of an animation +// element (e.g. from, by, to, values, calcMode, additive, accumulate, keyTimes, +// keySplines) +// +class SMILAnimationFunction { + public: + SMILAnimationFunction(); + + /* + * Sets the owning animation element which this class uses to query attribute + * values and compare document positions. + */ + void SetAnimationElement( + mozilla::dom::SVGAnimationElement* aAnimationElement); + + /* + * Sets animation-specific attributes (or marks them dirty, in the case + * of from/to/by/values). + * + * @param aAttribute The attribute being set + * @param aValue The updated value of the attribute. + * @param aResult The nsAttrValue object that may be used for storing the + * parsed result. + * @param aParseResult Outparam used for reporting parse errors. Will be set + * to NS_OK if everything succeeds. + * @return true if aAttribute is a recognized animation-related + * attribute; false otherwise. + */ + virtual bool SetAttr(nsAtom* aAttribute, const nsAString& aValue, + nsAttrValue& aResult, nsresult* aParseResult = nullptr); + + /* + * Unsets the given attribute. + * + * @returns true if aAttribute is a recognized animation-related + * attribute; false otherwise. + */ + virtual bool UnsetAttr(nsAtom* aAttribute); + + /** + * Indicate a new sample has occurred. + * + * @param aSampleTime The sample time for this timed element expressed in + * simple time. + * @param aSimpleDuration The simple duration for this timed element. + * @param aRepeatIteration The repeat iteration for this sample. The first + * iteration has a value of 0. + */ + void SampleAt(SMILTime aSampleTime, const SMILTimeValue& aSimpleDuration, + uint32_t aRepeatIteration); + + /** + * Indicate to sample using the last value defined for the animation function. + * This value is not normally sampled due to the end-point exclusive timing + * model but only occurs when the fill mode is "freeze" and the active + * duration is an even multiple of the simple duration. + * + * @param aRepeatIteration The repeat iteration for this sample. The first + * iteration has a value of 0. + */ + void SampleLastValue(uint32_t aRepeatIteration); + + /** + * Indicate that this animation is now active. This is used to instruct the + * animation function that it should now add its result to the animation + * sandwich. The begin time is also provided for proper prioritization of + * animation functions, and for this reason, this method must be called + * before either of the Sample methods. + * + * @param aBeginTime The begin time for the newly active interval. + */ + void Activate(SMILTime aBeginTime); + + /** + * Indicate that this animation is no longer active. This is used to instruct + * the animation function that it should no longer add its result to the + * animation sandwich. + * + * @param aIsFrozen true if this animation should continue to contribute + * to the animation sandwich using the most recent sample + * parameters. + */ + void Inactivate(bool aIsFrozen); + + /** + * Combines the result of this animation function for the last sample with the + * specified value. + * + * @param aSMILAttr This animation's target attribute. Used here for + * doing attribute-specific parsing of from/to/by/values. + * + * @param aResult The value to compose with. + */ + void ComposeResult(const SMILAttr& aSMILAttr, SMILValue& aResult); + + /** + * Returns the relative priority of this animation to another. The priority is + * used for determining the position of the animation in the animation + * sandwich -- higher priority animations are applied on top of lower + * priority animations. + * + * @return -1 if this animation has lower priority or 1 if this animation has + * higher priority + * + * This method should never return any other value, including 0. + */ + int8_t CompareTo(const SMILAnimationFunction* aOther) const; + + /* + * The following methods are provided so that the compositor can optimize its + * operations by only composing those animation that will affect the final + * result. + */ + + /** + * Indicates if the animation is currently active or frozen. Inactive + * animations will not contribute to the composed result. + * + * @return true if the animation is active or frozen, false otherwise. + */ + bool IsActiveOrFrozen() const { + /* + * - Frozen animations should be considered active for the purposes of + * compositing. + * - This function does not assume that our SMILValues (by/from/to/values) + * have already been parsed. + */ + return (mIsActive || mIsFrozen); + } + + /** + * Indicates if the animation is active. + * + * @return true if the animation is active, false otherwise. + */ + bool IsActive() const { return mIsActive; } + + /** + * Indicates if this animation will replace the passed in result rather than + * adding to it. Animations that replace the underlying value may be called + * without first calling lower priority animations. + * + * @return True if the animation will replace, false if it will add or + * otherwise build on the passed in value. + */ + virtual bool WillReplace() const; + + /** + * Indicates if the parameters for this animation have changed since the last + * time it was composited. This allows rendering to be performed only when + * necessary, particularly when no animations are active. + * + * Note that the caller is responsible for determining if the animation + * target has changed (with help from my UpdateCachedTarget() method). + * + * @return true if the animation parameters have changed, false + * otherwise. + */ + bool HasChanged() const; + + /** + * This method lets us clear the 'HasChanged' flag for inactive animations + * after we've reacted to their change to the 'inactive' state, so that we + * won't needlessly recompose their targets in every sample. + * + * This should only be called on an animation function that is inactive and + * that returns true from HasChanged(). + */ + void ClearHasChanged() { + MOZ_ASSERT(HasChanged(), + "clearing mHasChanged flag, when it's already false"); + MOZ_ASSERT(!IsActiveOrFrozen(), + "clearing mHasChanged flag for active animation"); + mHasChanged = false; + } + + /** + * Updates the cached record of our animation target, and returns a boolean + * that indicates whether the target has changed since the last call to this + * function. (This lets SMILCompositor check whether its animation + * functions have changed value or target since the last sample. If none of + * them have, then the compositor doesn't need to do anything.) + * + * @param aNewTarget A SMILTargetIdentifier representing the animation + * target of this function for this sample. + * @return true if |aNewTarget| is different from the old cached value; + * otherwise, false. + */ + bool UpdateCachedTarget(const SMILTargetIdentifier& aNewTarget); + + /** + * Returns true if this function was skipped in the previous sample (because + * there was a higher-priority non-additive animation). If a skipped animation + * function is later used, then the animation sandwich must be recomposited. + */ + bool WasSkippedInPrevSample() const { return mWasSkippedInPrevSample; } + + /** + * Mark this animation function as having been skipped. By marking the + * function as skipped, if it is used in a subsequent sample we'll know to + * recomposite the sandwich. + */ + void SetWasSkipped() { mWasSkippedInPrevSample = true; } + + /** + * Returns true if we need to recalculate the animation value on every sample. + * (e.g. because it depends on context like the font-size) + */ + bool ValueNeedsReparsingEverySample() const { + return mValueNeedsReparsingEverySample; + } + + // Comparator utility class, used for sorting SMILAnimationFunctions + class Comparator { + public: + bool Equals(const SMILAnimationFunction* aElem1, + const SMILAnimationFunction* aElem2) const { + return (aElem1->CompareTo(aElem2) == 0); + } + bool LessThan(const SMILAnimationFunction* aElem1, + const SMILAnimationFunction* aElem2) const { + return (aElem1->CompareTo(aElem2) < 0); + } + }; + + protected: + // alias declarations + using SMILValueArray = FallibleTArray; + + // Types + enum SMILCalcMode : uint8_t { + CALC_LINEAR, + CALC_DISCRETE, + CALC_PACED, + CALC_SPLINE + }; + + // Used for sorting SMILAnimationFunctions + SMILTime GetBeginTime() const { return mBeginTime; } + + // Property getters + bool GetAccumulate() const; + bool GetAdditive() const; + virtual SMILCalcMode GetCalcMode() const; + + // Property setters + nsresult SetAccumulate(const nsAString& aAccumulate, nsAttrValue& aResult); + nsresult SetAdditive(const nsAString& aAdditive, nsAttrValue& aResult); + nsresult SetCalcMode(const nsAString& aCalcMode, nsAttrValue& aResult); + nsresult SetKeyTimes(const nsAString& aKeyTimes, nsAttrValue& aResult); + nsresult SetKeySplines(const nsAString& aKeySplines, nsAttrValue& aResult); + + // Property un-setters + void UnsetAccumulate(); + void UnsetAdditive(); + void UnsetCalcMode(); + void UnsetKeyTimes(); + void UnsetKeySplines(); + + // Helpers + virtual bool IsDisallowedAttribute(const nsAtom* aAttribute) const { + return false; + } + virtual nsresult InterpolateResult(const SMILValueArray& aValues, + SMILValue& aResult, SMILValue& aBaseValue); + nsresult AccumulateResult(const SMILValueArray& aValues, SMILValue& aResult); + + nsresult ComputePacedPosition(const SMILValueArray& aValues, + double aSimpleProgress, + double& aIntervalProgress, + const SMILValue*& aFrom, const SMILValue*& aTo); + double ComputePacedTotalDistance(const SMILValueArray& aValues) const; + + /** + * Adjust the simple progress, that is, the point within the simple duration, + * by applying any keyTimes. + */ + double ScaleSimpleProgress(double aProgress, SMILCalcMode aCalcMode); + /** + * Adjust the progress within an interval, that is, between two animation + * values, by applying any keySplines. + */ + double ScaleIntervalProgress(double aProgress, uint32_t aIntervalIndex); + + // Convenience attribute getters + bool HasAttr(nsAtom* aAttName) const; + const nsAttrValue* GetAttr(nsAtom* aAttName) const; + bool GetAttr(nsAtom* aAttName, nsAString& aResult) const; + + bool ParseAttr(nsAtom* aAttName, const SMILAttr& aSMILAttr, + SMILValue& aResult, bool& aPreventCachingOfSandwich) const; + + virtual nsresult GetValues(const SMILAttr& aSMILAttr, + SMILValueArray& aResult); + + virtual void CheckValueListDependentAttrs(uint32_t aNumValues); + void CheckKeyTimes(uint32_t aNumValues); + void CheckKeySplines(uint32_t aNumValues); + + virtual bool IsToAnimation() const { + return !HasAttr(nsGkAtoms::values) && HasAttr(nsGkAtoms::to) && + !HasAttr(nsGkAtoms::from); + } + + // Returns true if we know our composited value won't change over the + // simple duration of this animation (for a fixed base value). + virtual bool IsValueFixedForSimpleDuration() const; + + inline bool IsAdditive() const { + /* + * Animation is additive if: + * + * (1) additive = "sum" (GetAdditive() == true), or + * (2) it is 'by animation' (by is set, from and values are not) + * + * Although animation is not additive if it is 'to animation' + */ + bool isByAnimation = (!HasAttr(nsGkAtoms::values) && + HasAttr(nsGkAtoms::by) && !HasAttr(nsGkAtoms::from)); + return !IsToAnimation() && (GetAdditive() || isByAnimation); + } + + // Setters for error flags + // These correspond to bit-indices in mErrorFlags, for tracking parse errors + // in these attributes, when those parse errors should block us from doing + // animation. + enum AnimationAttributeIdx { + BF_ACCUMULATE = 0, + BF_ADDITIVE = 1, + BF_CALC_MODE = 2, + BF_KEY_TIMES = 3, + BF_KEY_SPLINES = 4, + BF_KEY_POINTS = 5 // only + }; + + inline void SetAccumulateErrorFlag(bool aNewValue) { + SetErrorFlag(BF_ACCUMULATE, aNewValue); + } + inline void SetAdditiveErrorFlag(bool aNewValue) { + SetErrorFlag(BF_ADDITIVE, aNewValue); + } + inline void SetCalcModeErrorFlag(bool aNewValue) { + SetErrorFlag(BF_CALC_MODE, aNewValue); + } + inline void SetKeyTimesErrorFlag(bool aNewValue) { + SetErrorFlag(BF_KEY_TIMES, aNewValue); + } + inline void SetKeySplinesErrorFlag(bool aNewValue) { + SetErrorFlag(BF_KEY_SPLINES, aNewValue); + } + inline void SetKeyPointsErrorFlag(bool aNewValue) { + SetErrorFlag(BF_KEY_POINTS, aNewValue); + } + inline void SetErrorFlag(AnimationAttributeIdx aField, bool aValue) { + if (aValue) { + mErrorFlags |= (0x01 << aField); + } else { + mErrorFlags &= ~(0x01 << aField); + } + } + + // Members + // ------- + + static nsAttrValue::EnumTable sAdditiveTable[]; + static nsAttrValue::EnumTable sCalcModeTable[]; + static nsAttrValue::EnumTable sAccumulateTable[]; + + FallibleTArray mKeyTimes; + FallibleTArray mKeySplines; + + // These are the parameters provided by the previous sample. Currently we + // perform lazy calculation. That is, we only calculate the result if and when + // instructed by the compositor. This allows us to apply the result directly + // to the animation value and allows the compositor to filter out functions + // that it determines will not contribute to the final result. + SMILTime mSampleTime; // sample time within simple dur + SMILTimeValue mSimpleDuration; + uint32_t mRepeatIteration; + + SMILTime mBeginTime; // document time + + // The owning animation element. This is used for sorting based on document + // position and for fetching attribute values stored in the element. + // Raw pointer is OK here, because this SMILAnimationFunction can't outlive + // its owning animation element. + mozilla::dom::SVGAnimationElement* mAnimationElement; + + // Which attributes have been set but have had errors. This is not used for + // all attributes but only those which have specified error behaviour + // associated with them. + uint16_t mErrorFlags; + + // Allows us to check whether an animation function has changed target from + // sample to sample (because if neither target nor animated value have + // changed, we don't have to do anything). + SMILWeakTargetIdentifier mLastTarget; + + // Boolean flags + bool mIsActive : 1; + bool mIsFrozen : 1; + bool mLastValue : 1; + bool mHasChanged : 1; + bool mValueNeedsReparsingEverySample : 1; + bool mPrevSampleWasSingleValueAnimation : 1; + bool mWasSkippedInPrevSample : 1; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILANIMATIONFUNCTION_H_ diff --git a/dom/smil/SMILAttr.h b/dom/smil/SMILAttr.h new file mode 100644 index 0000000000..a67921a9ad --- /dev/null +++ b/dom/smil/SMILAttr.h @@ -0,0 +1,100 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILATTR_H_ +#define DOM_SMIL_SMILATTR_H_ + +#include "nscore.h" +#include "nsStringFwd.h" + +class nsIContent; + +namespace mozilla { + +class SMILValue; + +namespace dom { +class SVGAnimationElement; +} // namespace dom + +//////////////////////////////////////////////////////////////////////// +// SMILAttr: A variable targeted by SMIL for animation and can therefore have +// an underlying (base) value and an animated value For example, an attribute of +// a particular SVG element. +// +// These objects only exist during the compositing phase of SMIL animation +// calculations. They have a single owner who is responsible for deleting the +// object. + +class SMILAttr { + public: + /** + * Creates a new SMILValue for this attribute from a string. The string is + * parsed in the context of this attribute so that context-dependent values + * such as em-based units can be resolved into a canonical form suitable for + * animation (including interpolation etc.). + * + * @param aStr A string defining the new value to be created. + * @param aSrcElement The source animation element. This may be needed to + * provided additional context data such as for + * animateTransform where the 'type' attribute is needed to + * parse the value. + * @param[out] aValue Outparam for storing the parsed value. + * @param[out] aPreventCachingOfSandwich + * Outparam to indicate whether the attribute contains + * dependencies on its context that should prevent the + * result of the animation sandwich from being cached and + * reused in future samples. + * @return NS_OK on success or an error code if creation failed. + */ + virtual nsresult ValueFromString( + const nsAString& aStr, + const mozilla::dom::SVGAnimationElement* aSrcElement, SMILValue& aValue, + bool& aPreventCachingOfSandwich) const = 0; + + /** + * Gets the underlying value of this attribute. + * + * @return a SMILValue object. returned_object.IsNull() will be true if an + * error occurred. + */ + virtual SMILValue GetBaseValue() const = 0; + + /** + * Clears the animated value of this attribute. + * + * NOTE: The animation target is not guaranteed to be in a document when this + * method is called. (See bug 523188) + */ + virtual void ClearAnimValue() = 0; + + /** + * Sets the presentation value of this attribute. + * + * @param aValue The value to set. + * @return NS_OK on success or an error code if setting failed. + */ + virtual nsresult SetAnimValue(const SMILValue& aValue) = 0; + + /** + * Returns the targeted content node, for any SMILAttr implementations + * that want to expose that to the animation logic. Otherwise, returns + * null. + * + * @return the targeted content node, if this SMILAttr implementation + * wishes to make it avaiable. Otherwise, nullptr. + */ + virtual const nsIContent* GetTargetNode() const { return nullptr; } + + /** + * Virtual destructor, to make sure subclasses can clean themselves up. + */ + virtual ~SMILAttr() = default; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILATTR_H_ diff --git a/dom/smil/SMILBoolType.cpp b/dom/smil/SMILBoolType.cpp new file mode 100644 index 0000000000..218b7af16f --- /dev/null +++ b/dom/smil/SMILBoolType.cpp @@ -0,0 +1,69 @@ +/* -*- 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 "SMILBoolType.h" + +#include "mozilla/SMILValue.h" +#include "nsDebug.h" +#include + +namespace mozilla { + +void SMILBoolType::Init(SMILValue& aValue) const { + MOZ_ASSERT(aValue.IsNull(), "Unexpected value type"); + aValue.mU.mBool = false; + aValue.mType = this; +} + +void SMILBoolType::Destroy(SMILValue& aValue) const { + MOZ_ASSERT(aValue.mType == this, "Unexpected SMIL value"); + aValue.mU.mBool = false; + aValue.mType = SMILNullType::Singleton(); +} + +nsresult SMILBoolType::Assign(SMILValue& aDest, const SMILValue& aSrc) const { + MOZ_ASSERT(aDest.mType == aSrc.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aDest.mType == this, "Unexpected SMIL value"); + aDest.mU.mBool = aSrc.mU.mBool; + return NS_OK; +} + +bool SMILBoolType::IsEqual(const SMILValue& aLeft, + const SMILValue& aRight) const { + MOZ_ASSERT(aLeft.mType == aRight.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aLeft.mType == this, "Unexpected type for SMIL value"); + + return aLeft.mU.mBool == aRight.mU.mBool; +} + +nsresult SMILBoolType::Add(SMILValue& aDest, const SMILValue& aValueToAdd, + uint32_t aCount) const { + MOZ_ASSERT(aValueToAdd.mType == aDest.mType, "Trying to add invalid types"); + MOZ_ASSERT(aValueToAdd.mType == this, "Unexpected source type"); + return NS_ERROR_FAILURE; // bool values can't be added to each other +} + +nsresult SMILBoolType::ComputeDistance(const SMILValue& aFrom, + const SMILValue& aTo, + double& aDistance) const { + MOZ_ASSERT(aFrom.mType == aTo.mType, "Trying to compare different types"); + MOZ_ASSERT(aFrom.mType == this, "Unexpected source type"); + return NS_ERROR_FAILURE; // there is no concept of distance between bool + // values +} + +nsresult SMILBoolType::Interpolate(const SMILValue& aStartVal, + const SMILValue& aEndVal, + double aUnitDistance, + SMILValue& aResult) const { + MOZ_ASSERT(aStartVal.mType == aEndVal.mType, + "Trying to interpolate different types"); + MOZ_ASSERT(aStartVal.mType == this, "Unexpected types for interpolation"); + MOZ_ASSERT(aResult.mType == this, "Unexpected result type"); + return NS_ERROR_FAILURE; // bool values do not interpolate +} + +} // namespace mozilla diff --git a/dom/smil/SMILBoolType.h b/dom/smil/SMILBoolType.h new file mode 100644 index 0000000000..a8c12a6860 --- /dev/null +++ b/dom/smil/SMILBoolType.h @@ -0,0 +1,44 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILBOOLTYPE_H_ +#define DOM_SMIL_SMILBOOLTYPE_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/SMILType.h" + +namespace mozilla { + +class SMILBoolType : public SMILType { + public: + // Singleton for SMILValue objects to hold onto. + static SMILBoolType* Singleton() { + static SMILBoolType sSingleton; + return &sSingleton; + } + + protected: + // SMILType Methods + // ------------------- + void Init(SMILValue& aValue) const override; + void Destroy(SMILValue& aValue) const override; + nsresult Assign(SMILValue& aDest, const SMILValue& aSrc) const override; + nsresult Add(SMILValue& aDest, const SMILValue& aValueToAdd, + uint32_t aCount) const override; + bool IsEqual(const SMILValue& aLeft, const SMILValue& aRight) const override; + nsresult ComputeDistance(const SMILValue& aFrom, const SMILValue& aTo, + double& aDistance) const override; + nsresult Interpolate(const SMILValue& aStartVal, const SMILValue& aEndVal, + double aUnitDistance, SMILValue& aResult) const override; + + private: + // Private constructor: prevent instances beyond my singleton. + constexpr SMILBoolType() = default; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILBOOLTYPE_H_ diff --git a/dom/smil/SMILCSSProperty.cpp b/dom/smil/SMILCSSProperty.cpp new file mode 100644 index 0000000000..1de2b313c7 --- /dev/null +++ b/dom/smil/SMILCSSProperty.cpp @@ -0,0 +1,199 @@ +/* -*- 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/. */ + +/* representation of a SMIL-animatable CSS property on an element */ + +#include "SMILCSSProperty.h" + +#include + +#include "mozilla/AnimatedPropertyID.h" +#include "mozilla/SMILCSSValueType.h" +#include "mozilla/SMILValue.h" +#include "mozilla/ServoBindings.h" +#include "mozilla/StyleAnimationValue.h" +#include "mozilla/dom/Element.h" +#include "nsCSSProps.h" +#include "nsDOMCSSAttrDeclaration.h" + +namespace mozilla { + +// Class Methods +SMILCSSProperty::SMILCSSProperty(nsCSSPropertyID aPropID, + dom::Element* aElement, + const ComputedStyle* aBaseComputedStyle) + : mPropID(aPropID), + mElement(aElement), + mBaseComputedStyle(aBaseComputedStyle) { + MOZ_ASSERT(IsPropertyAnimatable(mPropID), + "Creating a SMILCSSProperty for a property " + "that's not supported for animation"); +} + +SMILValue SMILCSSProperty::GetBaseValue() const { + // To benefit from Return Value Optimization and avoid copy constructor calls + // due to our use of return-by-value, we must return the exact same object + // from ALL return points. This function must only return THIS variable: + SMILValue baseValue; + + // SPECIAL CASE: (a) Shorthands + // (b) 'display' + // (c) No base ComputedStyle + if (nsCSSProps::IsShorthand(mPropID) || mPropID == eCSSProperty_display || + !mBaseComputedStyle) { + // We can't look up the base (computed-style) value of shorthand + // properties because they aren't guaranteed to have a consistent computed + // value. + // + // Also, although we can look up the base value of the display property, + // doing so involves clearing and resetting the property which can cause + // frames to be recreated which we'd like to avoid. + // + // Furthermore, if we don't (yet) have a base ComputedStyle we obviously + // can't resolve a base value. + // + // In any case, just return a dummy value (initialized with the right + // type, so as not to indicate failure). + SMILValue tmpVal(&SMILCSSValueType::sSingleton); + std::swap(baseValue, tmpVal); + return baseValue; + } + + AnimationValue computedValue; + AnimatedPropertyID property(mPropID); + MOZ_ASSERT(!property.IsCustom(), + "Cannot animate custom properties with SMIL"); + computedValue.mServo = + Servo_ComputedValues_ExtractAnimationValue(mBaseComputedStyle, &property) + .Consume(); + if (!computedValue.mServo) { + return baseValue; + } + + baseValue = SMILCSSValueType::ValueFromAnimationValue(mPropID, mElement, + computedValue); + return baseValue; +} + +nsresult SMILCSSProperty::ValueFromString( + const nsAString& aStr, const dom::SVGAnimationElement* aSrcElement, + SMILValue& aValue, bool& aPreventCachingOfSandwich) const { + NS_ENSURE_TRUE(IsPropertyAnimatable(mPropID), NS_ERROR_FAILURE); + + SMILCSSValueType::ValueFromString(mPropID, mElement, aStr, aValue, + &aPreventCachingOfSandwich); + + if (aValue.IsNull()) { + return NS_ERROR_FAILURE; + } + + // XXX Due to bug 536660 (or at least that seems to be the most likely + // culprit), when we have animation setting display:none on a element, + // if we DON'T set the property every sample, chaos ensues. + if (!aPreventCachingOfSandwich && mPropID == eCSSProperty_display) { + aPreventCachingOfSandwich = true; + } + return NS_OK; +} + +nsresult SMILCSSProperty::SetAnimValue(const SMILValue& aValue) { + NS_ENSURE_TRUE(IsPropertyAnimatable(mPropID), NS_ERROR_FAILURE); + return mElement->SMILOverrideStyle()->SetSMILValue(mPropID, aValue); +} + +void SMILCSSProperty::ClearAnimValue() { + mElement->SMILOverrideStyle()->ClearSMILValue(mPropID); +} + +// Based on http://www.w3.org/TR/SVG/propidx.html +// static +bool SMILCSSProperty::IsPropertyAnimatable(nsCSSPropertyID aPropID) { + // NOTE: Right now, Gecko doesn't recognize the following properties from + // the SVG Property Index: + // alignment-baseline + // baseline-shift + // color-profile + // glyph-orientation-horizontal + // glyph-orientation-vertical + // kerning + // writing-mode + + switch (aPropID) { + case eCSSProperty_clip: + case eCSSProperty_clip_rule: + case eCSSProperty_clip_path: + case eCSSProperty_color: + case eCSSProperty_color_interpolation: + case eCSSProperty_color_interpolation_filters: + case eCSSProperty_cursor: + case eCSSProperty_display: + case eCSSProperty_dominant_baseline: + case eCSSProperty_fill: + case eCSSProperty_fill_opacity: + case eCSSProperty_fill_rule: + case eCSSProperty_filter: + case eCSSProperty_flood_color: + case eCSSProperty_flood_opacity: + case eCSSProperty_font: + case eCSSProperty_font_family: + case eCSSProperty_font_size: + case eCSSProperty_font_size_adjust: + case eCSSProperty_font_stretch: + case eCSSProperty_font_style: + case eCSSProperty_font_variant: + case eCSSProperty_font_weight: + case eCSSProperty_height: + case eCSSProperty_image_rendering: + case eCSSProperty_letter_spacing: + case eCSSProperty_lighting_color: + case eCSSProperty_marker: + case eCSSProperty_marker_end: + case eCSSProperty_marker_mid: + case eCSSProperty_marker_start: + case eCSSProperty_mask: + case eCSSProperty_mask_type: + case eCSSProperty_opacity: + case eCSSProperty_overflow: + case eCSSProperty_pointer_events: + case eCSSProperty_shape_rendering: + case eCSSProperty_stop_color: + case eCSSProperty_stop_opacity: + case eCSSProperty_stroke: + case eCSSProperty_stroke_dasharray: + case eCSSProperty_stroke_dashoffset: + case eCSSProperty_stroke_linecap: + case eCSSProperty_stroke_linejoin: + case eCSSProperty_stroke_miterlimit: + case eCSSProperty_stroke_opacity: + case eCSSProperty_stroke_width: + case eCSSProperty_text_anchor: + case eCSSProperty_text_decoration: + case eCSSProperty_text_decoration_line: + case eCSSProperty_text_rendering: + case eCSSProperty_vector_effect: + case eCSSProperty_width: + case eCSSProperty_visibility: + case eCSSProperty_word_spacing: + return true; + + // EXPLICITLY NON-ANIMATABLE PROPERTIES: + // (Some of these aren't supported at all in Gecko -- I've commented those + // ones out. If/when we add support for them, uncomment their line here) + // ---------------------------------------------------------------------- + // case eCSSProperty_enable_background: + // case eCSSProperty_glyph_orientation_horizontal: + // case eCSSProperty_glyph_orientation_vertical: + // case eCSSProperty_writing_mode: + case eCSSProperty_direction: + case eCSSProperty_unicode_bidi: + return false; + + default: + return false; + } +} + +} // namespace mozilla diff --git a/dom/smil/SMILCSSProperty.h b/dom/smil/SMILCSSProperty.h new file mode 100644 index 0000000000..d83721e56b --- /dev/null +++ b/dom/smil/SMILCSSProperty.h @@ -0,0 +1,80 @@ +/* -*- 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/. */ + +/* representation of a SMIL-animatable CSS property on an element */ + +#ifndef DOM_SMIL_SMILCSSPROPERTY_H_ +#define DOM_SMIL_SMILCSSPROPERTY_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/SMILAttr.h" +#include "nsAtom.h" +#include "nsCSSPropertyID.h" +#include "nsCSSValue.h" + +namespace mozilla { +class ComputedStyle; +namespace dom { +class Element; +} // namespace dom + +/** + * SMILCSSProperty: Implements the SMILAttr interface for SMIL animations + * that target CSS properties. Represents a particular animation-targeted CSS + * property on a particular element. + */ +class SMILCSSProperty : public SMILAttr { + public: + /** + * Constructs a new SMILCSSProperty. + * @param aPropID The CSS property we're interested in animating. + * @param aElement The element whose CSS property is being animated. + * @param aBaseComputedStyle The ComputedStyle to use when getting the base + * value. If this is nullptr and GetBaseValue is + * called, an empty SMILValue initialized with + * the SMILCSSValueType will be returned. + */ + SMILCSSProperty(nsCSSPropertyID aPropID, dom::Element* aElement, + const ComputedStyle* aBaseComputedStyle); + + // SMILAttr methods + nsresult ValueFromString(const nsAString& aStr, + const dom::SVGAnimationElement* aSrcElement, + SMILValue& aValue, + bool& aPreventCachingOfSandwich) const override; + SMILValue GetBaseValue() const override; + nsresult SetAnimValue(const SMILValue& aValue) override; + void ClearAnimValue() override; + + /** + * Utility method - returns true if the given property is supported for + * SMIL animation. + * + * @param aProperty The property to check for animation support. + * @return true if the given property is supported for SMIL animation, or + * false otherwise + */ + static bool IsPropertyAnimatable(nsCSSPropertyID aPropID); + + protected: + nsCSSPropertyID mPropID; + // Using non-refcounted pointer for mElement -- we know mElement will stay + // alive for my lifetime because a SMILAttr (like me) only lives as long + // as the Compositing step, and DOM elements don't get a chance to die during + // that time. + dom::Element* mElement; + + // The style to use when fetching base styles. + // + // As with mElement, since a SMILAttr only lives as long as the + // compositing step and since ComposeAttribute holds an owning reference to + // the base ComputedStyle, we can use a non-owning reference here. + const ComputedStyle* mBaseComputedStyle; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILCSSPROPERTY_H_ diff --git a/dom/smil/SMILCSSValueType.cpp b/dom/smil/SMILCSSValueType.cpp new file mode 100644 index 0000000000..32f19805a6 --- /dev/null +++ b/dom/smil/SMILCSSValueType.cpp @@ -0,0 +1,550 @@ +/* -*- 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/. */ + +/* representation of a value for a SMIL-animated CSS property */ + +#include "SMILCSSValueType.h" + +#include "nsComputedDOMStyle.h" +#include "nsColor.h" +#include "nsCSSProps.h" +#include "nsCSSValue.h" +#include "nsDebug.h" +#include "nsPresContextInlines.h" +#include "nsPresContext.h" +#include "nsString.h" +#include "nsStyleUtil.h" +#include "mozilla/DeclarationBlock.h" +#include "mozilla/PresShell.h" +#include "mozilla/PresShellInlines.h" +#include "mozilla/ServoBindings.h" +#include "mozilla/StyleAnimationValue.h" +#include "mozilla/ServoCSSParser.h" +#include "mozilla/ServoStyleSet.h" +#include "mozilla/SMILParserUtils.h" +#include "mozilla/SMILValue.h" +#include "mozilla/dom/BaseKeyframeTypesBinding.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" + +using namespace mozilla::dom; + +namespace mozilla { + +using ServoAnimationValues = CopyableAutoTArray, 1>; + +/*static*/ +SMILCSSValueType SMILCSSValueType::sSingleton; + +struct ValueWrapper { + ValueWrapper(nsCSSPropertyID aPropID, const AnimationValue& aValue) + : mPropID(aPropID) { + MOZ_ASSERT(!aValue.IsNull()); + mServoValues.AppendElement(aValue.mServo); + } + ValueWrapper(nsCSSPropertyID aPropID, + const RefPtr& aValue) + : mPropID(aPropID), mServoValues{(aValue)} {} + ValueWrapper(nsCSSPropertyID aPropID, ServoAnimationValues&& aValues) + : mPropID(aPropID), mServoValues{std::move(aValues)} {} + + bool operator==(const ValueWrapper& aOther) const { + if (mPropID != aOther.mPropID) { + return false; + } + + MOZ_ASSERT(!mServoValues.IsEmpty()); + size_t len = mServoValues.Length(); + if (len != aOther.mServoValues.Length()) { + return false; + } + for (size_t i = 0; i < len; i++) { + if (!Servo_AnimationValue_DeepEqual(mServoValues[i], + aOther.mServoValues[i])) { + return false; + } + } + return true; + } + + bool operator!=(const ValueWrapper& aOther) const { + return !(*this == aOther); + } + + nsCSSPropertyID mPropID; + ServoAnimationValues mServoValues; +}; + +// Helper Methods +// -------------- + +// If one argument is null, this method updates it to point to "zero" +// for the other argument's Unit (if applicable; otherwise, we return false). +// +// If neither argument is null, this method simply returns true. +// +// If both arguments are null, this method returns false. +// +// |aZeroValueStorage| should be a reference to a +// RefPtr. This is used where we may need to allocate a +// new ServoAnimationValue to represent the appropriate zero value. +// +// Returns true on success, or otherwise. +static bool FinalizeServoAnimationValues( + const RefPtr*& aValue1, + const RefPtr*& aValue2, + RefPtr& aZeroValueStorage) { + if (!aValue1 && !aValue2) { + return false; + } + + // Are we missing either val? (If so, it's an implied 0 in other val's units) + + if (!aValue1) { + aZeroValueStorage = Servo_AnimationValues_GetZeroValue(*aValue2).Consume(); + aValue1 = &aZeroValueStorage; + } else if (!aValue2) { + aZeroValueStorage = Servo_AnimationValues_GetZeroValue(*aValue1).Consume(); + aValue2 = &aZeroValueStorage; + } + return *aValue1 && *aValue2; +} + +static ValueWrapper* ExtractValueWrapper(SMILValue& aValue) { + return static_cast(aValue.mU.mPtr); +} + +static const ValueWrapper* ExtractValueWrapper(const SMILValue& aValue) { + return static_cast(aValue.mU.mPtr); +} + +// Class methods +// ------------- +void SMILCSSValueType::Init(SMILValue& aValue) const { + MOZ_ASSERT(aValue.IsNull(), "Unexpected SMIL value type"); + + aValue.mU.mPtr = nullptr; + aValue.mType = this; +} + +void SMILCSSValueType::Destroy(SMILValue& aValue) const { + MOZ_ASSERT(aValue.mType == this, "Unexpected SMIL value type"); + delete static_cast(aValue.mU.mPtr); + aValue.mType = SMILNullType::Singleton(); +} + +nsresult SMILCSSValueType::Assign(SMILValue& aDest, + const SMILValue& aSrc) const { + MOZ_ASSERT(aDest.mType == aSrc.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aDest.mType == this, "Unexpected SMIL value type"); + const ValueWrapper* srcWrapper = ExtractValueWrapper(aSrc); + ValueWrapper* destWrapper = ExtractValueWrapper(aDest); + + if (srcWrapper) { + if (!destWrapper) { + // barely-initialized dest -- need to alloc & copy + aDest.mU.mPtr = new ValueWrapper(*srcWrapper); + } else { + // both already fully-initialized -- just copy straight across + *destWrapper = *srcWrapper; + } + } else if (destWrapper) { + // fully-initialized dest, barely-initialized src -- clear dest + delete destWrapper; + aDest.mU.mPtr = destWrapper = nullptr; + } // else, both are barely-initialized -- nothing to do. + + return NS_OK; +} + +bool SMILCSSValueType::IsEqual(const SMILValue& aLeft, + const SMILValue& aRight) const { + MOZ_ASSERT(aLeft.mType == aRight.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aLeft.mType == this, "Unexpected SMIL value"); + const ValueWrapper* leftWrapper = ExtractValueWrapper(aLeft); + const ValueWrapper* rightWrapper = ExtractValueWrapper(aRight); + + if (leftWrapper) { + if (rightWrapper) { + // Both non-null + NS_WARNING_ASSERTION(leftWrapper != rightWrapper, + "Two SMILValues with matching ValueWrapper ptr"); + return *leftWrapper == *rightWrapper; + } + // Left non-null, right null + return false; + } + if (rightWrapper) { + // Left null, right non-null + return false; + } + // Both null + return true; +} + +static bool AddOrAccumulate(SMILValue& aDest, const SMILValue& aValueToAdd, + CompositeOperation aCompositeOp, uint64_t aCount) { + MOZ_ASSERT(aValueToAdd.mType == aDest.mType, + "Trying to add mismatching types"); + MOZ_ASSERT(aValueToAdd.mType == &SMILCSSValueType::sSingleton, + "Unexpected SMIL value type"); + MOZ_ASSERT(aCompositeOp == CompositeOperation::Add || + aCompositeOp == CompositeOperation::Accumulate, + "Composite operation should be add or accumulate"); + MOZ_ASSERT(aCompositeOp != CompositeOperation::Add || aCount == 1, + "Count should be 1 if composite operation is add"); + + ValueWrapper* destWrapper = ExtractValueWrapper(aDest); + const ValueWrapper* valueToAddWrapper = ExtractValueWrapper(aValueToAdd); + + // If both of the values are empty just fail. This can happen in rare cases + // such as when the underlying animation produced an empty value. + // + // Technically, it doesn't matter what we return here since in either case it + // will produce the same result: an empty value. + if (!destWrapper && !valueToAddWrapper) { + return false; + } + + nsCSSPropertyID property = + valueToAddWrapper ? valueToAddWrapper->mPropID : destWrapper->mPropID; + // Special case: font-size-adjust and stroke-dasharray are explicitly + // non-additive (even though StyleAnimationValue *could* support adding them) + if (property == eCSSProperty_font_size_adjust || + property == eCSSProperty_stroke_dasharray) { + return false; + } + // Skip font shorthand since it includes font-size-adjust. + if (property == eCSSProperty_font) { + return false; + } + + size_t len = valueToAddWrapper ? valueToAddWrapper->mServoValues.Length() + : destWrapper->mServoValues.Length(); + + MOZ_ASSERT(!valueToAddWrapper || !destWrapper || + valueToAddWrapper->mServoValues.Length() == + destWrapper->mServoValues.Length(), + "Both of values' length in the wrappers should be the same if " + "both of them exist"); + + for (size_t i = 0; i < len; i++) { + const RefPtr* valueToAdd = + valueToAddWrapper ? &valueToAddWrapper->mServoValues[i] : nullptr; + const RefPtr* destValue = + destWrapper ? &destWrapper->mServoValues[i] : nullptr; + RefPtr zeroValueStorage; + if (!FinalizeServoAnimationValues(valueToAdd, destValue, + zeroValueStorage)) { + return false; + } + + // FinalizeServoAnimationValues may have updated destValue so we should make + // sure the aDest and aDestWrapper outparams are up-to-date. + if (destWrapper) { + destWrapper->mServoValues[i] = *destValue; + } else { + // aDest may be a barely-initialized "zero" destination. + aDest.mU.mPtr = destWrapper = new ValueWrapper(property, *destValue); + destWrapper->mServoValues.SetLength(len); + } + + RefPtr result; + if (aCompositeOp == CompositeOperation::Add) { + result = Servo_AnimationValues_Add(*destValue, *valueToAdd).Consume(); + } else { + result = Servo_AnimationValues_Accumulate(*destValue, *valueToAdd, aCount) + .Consume(); + } + + if (!result) { + return false; + } + destWrapper->mServoValues[i] = result; + } + + return true; +} + +nsresult SMILCSSValueType::SandwichAdd(SMILValue& aDest, + const SMILValue& aValueToAdd) const { + return AddOrAccumulate(aDest, aValueToAdd, CompositeOperation::Add, 1) + ? NS_OK + : NS_ERROR_FAILURE; +} + +nsresult SMILCSSValueType::Add(SMILValue& aDest, const SMILValue& aValueToAdd, + uint32_t aCount) const { + return AddOrAccumulate(aDest, aValueToAdd, CompositeOperation::Accumulate, + aCount) + ? NS_OK + : NS_ERROR_FAILURE; +} + +nsresult SMILCSSValueType::ComputeDistance(const SMILValue& aFrom, + const SMILValue& aTo, + double& aDistance) const { + MOZ_ASSERT(aFrom.mType == aTo.mType, "Trying to compare different types"); + MOZ_ASSERT(aFrom.mType == this, "Unexpected source type"); + + const ValueWrapper* fromWrapper = ExtractValueWrapper(aFrom); + const ValueWrapper* toWrapper = ExtractValueWrapper(aTo); + MOZ_ASSERT(toWrapper, "expecting non-null endpoint"); + + size_t len = toWrapper->mServoValues.Length(); + MOZ_ASSERT(!fromWrapper || fromWrapper->mServoValues.Length() == len, + "From and to values length should be the same if " + "The start value exists"); + + double squareDistance = 0; + + for (size_t i = 0; i < len; i++) { + const RefPtr* fromValue = + fromWrapper ? &fromWrapper->mServoValues[i] : nullptr; + const RefPtr* toValue = &toWrapper->mServoValues[i]; + RefPtr zeroValueStorage; + if (!FinalizeServoAnimationValues(fromValue, toValue, zeroValueStorage)) { + return NS_ERROR_FAILURE; + } + + double distance = + Servo_AnimationValues_ComputeDistance(*fromValue, *toValue); + if (distance < 0.0) { + return NS_ERROR_FAILURE; + } + + if (len == 1) { + aDistance = distance; + return NS_OK; + } + squareDistance += distance * distance; + } + + aDistance = sqrt(squareDistance); + + return NS_OK; +} + +nsresult SMILCSSValueType::Interpolate(const SMILValue& aStartVal, + const SMILValue& aEndVal, + double aUnitDistance, + SMILValue& aResult) const { + MOZ_ASSERT(aStartVal.mType == aEndVal.mType, + "Trying to interpolate different types"); + MOZ_ASSERT(aStartVal.mType == this, "Unexpected types for interpolation"); + MOZ_ASSERT(aResult.mType == this, "Unexpected result type"); + MOZ_ASSERT(aUnitDistance >= 0.0 && aUnitDistance <= 1.0, + "unit distance value out of bounds"); + MOZ_ASSERT(!aResult.mU.mPtr, "expecting barely-initialized outparam"); + + const ValueWrapper* startWrapper = ExtractValueWrapper(aStartVal); + const ValueWrapper* endWrapper = ExtractValueWrapper(aEndVal); + MOZ_ASSERT(endWrapper, "expecting non-null endpoint"); + + // For discretely-animated properties Servo_AnimationValues_Interpolate will + // perform the discrete animation (i.e. 50% flip) and return a success result. + // However, SMIL has its own special discrete animation behavior that it uses + // when keyTimes are specified, but we won't run that unless that this method + // returns a failure to indicate that the property cannot be smoothly + // interpolated, i.e. that we need to use a discrete calcMode. + // + // For shorthands, Servo_Property_IsDiscreteAnimatable will always return + // false. That's fine since most shorthands (like 'font' and + // 'text-decoration') include non-discrete components. If authors want to + // treat all components as discrete then they should use calcMode="discrete". + if (Servo_Property_IsDiscreteAnimatable(endWrapper->mPropID)) { + return NS_ERROR_FAILURE; + } + + ServoAnimationValues results; + size_t len = endWrapper->mServoValues.Length(); + results.SetCapacity(len); + MOZ_ASSERT(!startWrapper || startWrapper->mServoValues.Length() == len, + "Start and end values length should be the same if " + "the start value exists"); + for (size_t i = 0; i < len; i++) { + const RefPtr* startValue = + startWrapper ? &startWrapper->mServoValues[i] : nullptr; + const RefPtr* endValue = &endWrapper->mServoValues[i]; + RefPtr zeroValueStorage; + if (!FinalizeServoAnimationValues(startValue, endValue, zeroValueStorage)) { + return NS_ERROR_FAILURE; + } + + RefPtr result = + Servo_AnimationValues_Interpolate(*startValue, *endValue, aUnitDistance) + .Consume(); + if (!result) { + return NS_ERROR_FAILURE; + } + results.AppendElement(result); + } + aResult.mU.mPtr = new ValueWrapper(endWrapper->mPropID, std::move(results)); + + return NS_OK; +} + +static ServoAnimationValues ValueFromStringHelper( + nsCSSPropertyID aPropID, Element* aTargetElement, + nsPresContext* aPresContext, const ComputedStyle* aComputedStyle, + const nsAString& aString) { + ServoAnimationValues result; + + Document* doc = aTargetElement->GetComposedDoc(); + if (!doc) { + return result; + } + + // Parse property + ServoCSSParser::ParsingEnvironment env = + ServoCSSParser::GetParsingEnvironment(doc); + RefPtr servoDeclarationBlock = + ServoCSSParser::ParseProperty( + aPropID, NS_ConvertUTF16toUTF8(aString), env, + StyleParsingMode::ALLOW_UNITLESS_LENGTH | + StyleParsingMode::ALLOW_ALL_NUMERIC_VALUES); + if (!servoDeclarationBlock) { + return result; + } + + // Compute value + aPresContext->StyleSet()->GetAnimationValues( + servoDeclarationBlock, aTargetElement, aComputedStyle, result); + + return result; +} + +// static +void SMILCSSValueType::ValueFromString(nsCSSPropertyID aPropID, + Element* aTargetElement, + const nsAString& aString, + SMILValue& aValue, + bool* aIsContextSensitive) { + MOZ_ASSERT(aValue.IsNull(), "Outparam should be null-typed"); + nsPresContext* presContext = + nsContentUtils::GetContextForContent(aTargetElement); + if (!presContext) { + NS_WARNING("Not parsing animation value; unable to get PresContext"); + return; + } + + Document* doc = aTargetElement->GetComposedDoc(); + if (doc && !nsStyleUtil::CSPAllowsInlineStyle(nullptr, doc, nullptr, 0, 1, + aString, nullptr)) { + return; + } + + RefPtr computedStyle = + nsComputedDOMStyle::GetComputedStyle(aTargetElement); + if (!computedStyle) { + return; + } + + ServoAnimationValues parsedValues = ValueFromStringHelper( + aPropID, aTargetElement, presContext, computedStyle, aString); + if (aIsContextSensitive) { + // FIXME: Bug 1358955 - detect context-sensitive values and set this value + // appropriately. + *aIsContextSensitive = false; + } + + if (!parsedValues.IsEmpty()) { + sSingleton.Init(aValue); + aValue.mU.mPtr = new ValueWrapper(aPropID, std::move(parsedValues)); + } +} + +// static +SMILValue SMILCSSValueType::ValueFromAnimationValue( + nsCSSPropertyID aPropID, Element* aTargetElement, + const AnimationValue& aValue) { + SMILValue result; + + Document* doc = aTargetElement->GetComposedDoc(); + // We'd like to avoid serializing |aValue| if possible, and since the + // string passed to CSPAllowsInlineStyle is only used for reporting violations + // and an intermediate CSS value is not likely to be particularly useful + // in that case, we just use a generic placeholder string instead. + static const nsLiteralString kPlaceholderText = u"[SVG animation of CSS]"_ns; + if (doc && !nsStyleUtil::CSPAllowsInlineStyle(nullptr, doc, nullptr, 0, 1, + kPlaceholderText, nullptr)) { + return result; + } + + sSingleton.Init(result); + result.mU.mPtr = new ValueWrapper(aPropID, aValue); + + return result; +} + +// static +bool SMILCSSValueType::SetPropertyValues(const SMILValue& aValue, + DeclarationBlock& aDecl) { + MOZ_ASSERT(aValue.mType == &SMILCSSValueType::sSingleton, + "Unexpected SMIL value type"); + const ValueWrapper* wrapper = ExtractValueWrapper(aValue); + if (!wrapper) { + return false; + } + + bool changed = false; + for (const auto& value : wrapper->mServoValues) { + changed |= Servo_DeclarationBlock_SetPropertyToAnimationValue(aDecl.Raw(), + value, {}); + } + + return changed; +} + +// static +nsCSSPropertyID SMILCSSValueType::PropertyFromValue(const SMILValue& aValue) { + if (aValue.mType != &SMILCSSValueType::sSingleton) { + return eCSSProperty_UNKNOWN; + } + + const ValueWrapper* wrapper = ExtractValueWrapper(aValue); + if (!wrapper) { + return eCSSProperty_UNKNOWN; + } + + return wrapper->mPropID; +} + +// static +void SMILCSSValueType::FinalizeValue(SMILValue& aValue, + const SMILValue& aValueToMatch) { + MOZ_ASSERT(aValue.mType == aValueToMatch.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aValue.mType == &SMILCSSValueType::sSingleton, + "Unexpected SMIL value type"); + + ValueWrapper* valueWrapper = ExtractValueWrapper(aValue); + // If |aValue| already has a value, there's nothing to do here. + if (valueWrapper) { + return; + } + + const ValueWrapper* valueToMatchWrapper = ExtractValueWrapper(aValueToMatch); + if (!valueToMatchWrapper) { + MOZ_ASSERT_UNREACHABLE("Value to match is empty"); + return; + } + + ServoAnimationValues zeroValues; + zeroValues.SetCapacity(valueToMatchWrapper->mServoValues.Length()); + + for (const auto& valueToMatch : valueToMatchWrapper->mServoValues) { + RefPtr zeroValue = + Servo_AnimationValues_GetZeroValue(valueToMatch).Consume(); + if (!zeroValue) { + return; + } + zeroValues.AppendElement(std::move(zeroValue)); + } + aValue.mU.mPtr = + new ValueWrapper(valueToMatchWrapper->mPropID, std::move(zeroValues)); +} + +} // namespace mozilla diff --git a/dom/smil/SMILCSSValueType.h b/dom/smil/SMILCSSValueType.h new file mode 100644 index 0000000000..72c66bdc43 --- /dev/null +++ b/dom/smil/SMILCSSValueType.h @@ -0,0 +1,133 @@ +/* -*- 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/. */ + +/* representation of a value for a SMIL-animated CSS property */ + +#ifndef DOM_SMIL_SMILCSSVALUETYPE_H_ +#define DOM_SMIL_SMILCSSVALUETYPE_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/SMILType.h" +#include "nsCSSPropertyID.h" +#include "nsStringFwd.h" + +namespace mozilla { +struct AnimationValue; +class DeclarationBlock; +namespace dom { +class Element; +} // namespace dom + +/* + * SMILCSSValueType: Represents a SMIL-animated CSS value. + */ +class SMILCSSValueType : public SMILType { + public: + // Singleton for SMILValue objects to hold onto. + static SMILCSSValueType sSingleton; + + protected: + // SMILType Methods + // ------------------- + void Init(SMILValue& aValue) const override; + void Destroy(SMILValue&) const override; + nsresult Assign(SMILValue& aDest, const SMILValue& aSrc) const override; + bool IsEqual(const SMILValue& aLeft, const SMILValue& aRight) const override; + nsresult Add(SMILValue& aDest, const SMILValue& aValueToAdd, + uint32_t aCount) const override; + nsresult SandwichAdd(SMILValue& aDest, + const SMILValue& aValueToAdd) const override; + nsresult ComputeDistance(const SMILValue& aFrom, const SMILValue& aTo, + double& aDistance) const override; + nsresult Interpolate(const SMILValue& aStartVal, const SMILValue& aEndVal, + double aUnitDistance, SMILValue& aResult) const override; + + public: + // Helper Methods + // -------------- + /** + * Sets up the given SMILValue to represent the given string value. The + * string is interpreted as a value for the given property on the given + * element. + * + * On failure, this method leaves aValue.mType == SMILNullType::sSingleton. + * Otherwise, this method leaves aValue.mType == this class's singleton. + * + * @param aPropID The property for which we're parsing a value. + * @param aTargetElement The target element to whom the property/value + * setting applies. + * @param aString The string to be parsed as a CSS value. + * @param [out] aValue The SMILValue to be populated. Should + * initially be null-typed. + * @param [out] aIsContextSensitive Set to true if |aString| may produce + * a different |aValue| depending on other + * CSS properties on |aTargetElement| + * or its ancestors (e.g. 'inherit). + * false otherwise. May be nullptr. + * Not set if the method fails. + * @pre aValue.IsNull() + * @post aValue.IsNull() || aValue.mType == SMILCSSValueType::sSingleton + */ + static void ValueFromString(nsCSSPropertyID aPropID, + dom::Element* aTargetElement, + const nsAString& aString, SMILValue& aValue, + bool* aIsContextSensitive); + + /** + * Creates a SMILValue to wrap the given animation value. + * + * @param aPropID The property that |aValue| corresponds to. + * @param aTargetElement The target element to which the animation value + * applies. + * @param aValue The animation value to use. + * @return A new SMILValue. On failure, returns a + * SMILValue with the null type (i.e. rv.IsNull() + * returns true). + */ + static SMILValue ValueFromAnimationValue(nsCSSPropertyID aPropID, + dom::Element* aTargetElement, + const AnimationValue& aValue); + + /** + * Sets the relevant property values in the declaration block. + * + * Returns whether the declaration changed. + */ + static bool SetPropertyValues(const SMILValue&, mozilla::DeclarationBlock&); + + /** + * Return the CSS property animated by the specified value. + * + * @param aValue The SMILValue to examine. + * @return The nsCSSPropertyID enum value of the property animated + * by |aValue|, or eCSSProperty_UNKNOWN if the type of + * |aValue| is not SMILCSSValueType. + */ + static nsCSSPropertyID PropertyFromValue(const SMILValue& aValue); + + /** + * If |aValue| is an empty value, converts it to a suitable zero value by + * matching the type of value stored in |aValueToMatch|. + * + * There is no indication if this method fails. If a suitable zero value could + * not be created, |aValue| is simply unmodified. + * + * @param aValue The SMILValue (of type SMILCSSValueType) to + * possibly update. + * @param aValueToMatch A SMILValue (of type SMILCSSValueType) for which + * a corresponding zero value will be created if |aValue| + * is empty. + */ + static void FinalizeValue(SMILValue& aValue, const SMILValue& aValueToMatch); + + private: + // Private constructor: prevent instances beyond my singleton. + constexpr SMILCSSValueType() = default; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILCSSVALUETYPE_H_ diff --git a/dom/smil/SMILCompositor.cpp b/dom/smil/SMILCompositor.cpp new file mode 100644 index 0000000000..6b55267cae --- /dev/null +++ b/dom/smil/SMILCompositor.cpp @@ -0,0 +1,239 @@ +/* -*- 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 "SMILCompositor.h" + +#include "mozilla/dom/SVGSVGElement.h" +#include "nsComputedDOMStyle.h" +#include "nsCSSProps.h" +#include "nsHashKeys.h" +#include "SMILCSSProperty.h" + +namespace mozilla { + +// PLDHashEntryHdr methods +bool SMILCompositor::KeyEquals(KeyTypePointer aKey) const { + return aKey && aKey->Equals(mKey); +} + +/*static*/ +PLDHashNumber SMILCompositor::HashKey(KeyTypePointer aKey) { + // Combine the 3 values into one numeric value, which will be hashed. + // NOTE: We right-shift one of the pointers by 2 to get some randomness in + // its 2 lowest-order bits. (Those shifted-off bits will always be 0 since + // our pointers will be word-aligned.) + return (NS_PTR_TO_UINT32(aKey->mElement.get()) >> 2) + + NS_PTR_TO_UINT32(aKey->mAttributeName.get()); +} + +// Cycle-collection support +void SMILCompositor::Traverse(nsCycleCollectionTraversalCallback* aCallback) { + if (!mKey.mElement) return; + + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(*aCallback, "Compositor mKey.mElement"); + aCallback->NoteXPCOMChild(mKey.mElement); +} + +// Other methods +void SMILCompositor::AddAnimationFunction(SMILAnimationFunction* aFunc) { + if (aFunc) { + mAnimationFunctions.AppendElement(aFunc); + } +} + +void SMILCompositor::ComposeAttribute(bool& aMightHavePendingStyleUpdates) { + if (!mKey.mElement) return; + + // If we might need to resolve base styles, grab a suitable ComputedStyle + // for initializing our SMILAttr with. + RefPtr baseComputedStyle; + if (MightNeedBaseStyle()) { + baseComputedStyle = nsComputedDOMStyle::GetUnanimatedComputedStyleNoFlush( + mKey.mElement, PseudoStyleType::NotPseudo); + } + + // FIRST: Get the SMILAttr (to grab base value from, and to eventually + // give animated value to) + UniquePtr smilAttr = CreateSMILAttr(baseComputedStyle); + if (!smilAttr) { + // Target attribute not found (or, out of memory) + return; + } + if (mAnimationFunctions.IsEmpty()) { + // No active animation functions. (We can still have a SMILCompositor in + // that case if an animation function has *just* become inactive) + smilAttr->ClearAnimValue(); + // Removing the animation effect may require a style update. + aMightHavePendingStyleUpdates = true; + return; + } + + // SECOND: Sort the animationFunctions, to prepare for compositing. + SMILAnimationFunction::Comparator comparator; + mAnimationFunctions.Sort(comparator); + + // THIRD: Step backwards through animation functions to find out + // which ones we actually care about. + uint32_t firstFuncToCompose = GetFirstFuncToAffectSandwich(); + + // FOURTH: Get & cache base value + SMILValue sandwichResultValue; + if (!mAnimationFunctions[firstFuncToCompose]->WillReplace()) { + sandwichResultValue = smilAttr->GetBaseValue(); + } + UpdateCachedBaseValue(sandwichResultValue); + + if (!mForceCompositing) { + return; + } + + // FIFTH: Compose animation functions + aMightHavePendingStyleUpdates = true; + uint32_t length = mAnimationFunctions.Length(); + for (uint32_t i = firstFuncToCompose; i < length; ++i) { + mAnimationFunctions[i]->ComposeResult(*smilAttr, sandwichResultValue); + } + if (sandwichResultValue.IsNull()) { + smilAttr->ClearAnimValue(); + return; + } + + // SIXTH: Set the animated value to the final composited result. + nsresult rv = smilAttr->SetAnimValue(sandwichResultValue); + if (NS_FAILED(rv)) { + NS_WARNING("SMILAttr::SetAnimValue failed"); + } +} + +void SMILCompositor::ClearAnimationEffects() { + if (!mKey.mElement || !mKey.mAttributeName) return; + + UniquePtr smilAttr = CreateSMILAttr(nullptr); + if (!smilAttr) { + // Target attribute not found (or, out of memory) + return; + } + smilAttr->ClearAnimValue(); +} + +// Protected Helper Functions +// -------------------------- +UniquePtr SMILCompositor::CreateSMILAttr( + const ComputedStyle* aBaseComputedStyle) { + nsCSSPropertyID propID = GetCSSPropertyToAnimate(); + + if (propID != eCSSProperty_UNKNOWN) { + return MakeUnique(propID, mKey.mElement.get(), + aBaseComputedStyle); + } + + return mKey.mElement->GetAnimatedAttr(mKey.mAttributeNamespaceID, + mKey.mAttributeName); +} + +nsCSSPropertyID SMILCompositor::GetCSSPropertyToAnimate() const { + if (mKey.mAttributeNamespaceID != kNameSpaceID_None) { + return eCSSProperty_UNKNOWN; + } + + nsCSSPropertyID propID = + nsCSSProps::LookupProperty(nsAtomCString(mKey.mAttributeName)); + + if (!SMILCSSProperty::IsPropertyAnimatable(propID)) { + return eCSSProperty_UNKNOWN; + } + + // If we are animating the 'width' or 'height' of an outer SVG + // element we should animate it as a CSS property, but for other elements + // in SVG namespace (e.g. ) we should animate it as a length attribute. + if ((mKey.mAttributeName == nsGkAtoms::width || + mKey.mAttributeName == nsGkAtoms::height) && + mKey.mElement->GetNameSpaceID() == kNameSpaceID_SVG) { + // Not an element. + if (!mKey.mElement->IsSVGElement(nsGkAtoms::svg)) { + return eCSSProperty_UNKNOWN; + } + + // An inner element + if (static_cast(*mKey.mElement).IsInner()) { + return eCSSProperty_UNKNOWN; + } + + // Indeed an outer element, fall through. + } + + return propID; +} + +bool SMILCompositor::MightNeedBaseStyle() const { + if (GetCSSPropertyToAnimate() == eCSSProperty_UNKNOWN) { + return false; + } + + // We should return true if at least one animation function might build on + // the base value. + for (const SMILAnimationFunction* func : mAnimationFunctions) { + if (!func->WillReplace()) { + return true; + } + } + + return false; +} + +uint32_t SMILCompositor::GetFirstFuncToAffectSandwich() { + // For performance reasons, we throttle most animations on elements in + // display:none subtrees. (We can't throttle animations that target the + // "display" property itself, though -- if we did, display:none elements + // could never be dynamically displayed via animations.) + // To determine whether we're in a display:none subtree, we will check the + // element's primary frame since element in display:none subtree doesn't have + // a primary frame. Before this process, we will construct frame when we + // append an element to subtree. So we will not need to worry about pending + // frame construction in this step. + bool canThrottle = mKey.mAttributeName != nsGkAtoms::display && + !mKey.mElement->GetPrimaryFrame(); + + uint32_t i; + for (i = mAnimationFunctions.Length(); i > 0; --i) { + SMILAnimationFunction* curAnimFunc = mAnimationFunctions[i - 1]; + // In the following, the lack of short-circuit behavior of |= means that we + // will ALWAYS run UpdateCachedTarget (even if mForceCompositing is true) + // but only call HasChanged and WasSkippedInPrevSample if necessary. This + // is important since we need UpdateCachedTarget to run in order to detect + // changes to the target in subsequent samples. + mForceCompositing |= curAnimFunc->UpdateCachedTarget(mKey) || + (curAnimFunc->HasChanged() && !canThrottle) || + curAnimFunc->WasSkippedInPrevSample(); + + if (curAnimFunc->WillReplace()) { + --i; + break; + } + } + + // Mark remaining animation functions as having been skipped so if we later + // use them we'll know to force compositing. + // Note that we only really need to do this if something has changed + // (otherwise we would have set the flag on a previous sample) and if + // something has changed mForceCompositing will be true. + if (mForceCompositing) { + for (uint32_t j = i; j > 0; --j) { + mAnimationFunctions[j - 1]->SetWasSkipped(); + } + } + return i; +} + +void SMILCompositor::UpdateCachedBaseValue(const SMILValue& aBaseValue) { + if (mCachedBaseValue != aBaseValue) { + // Base value has changed since last sample. + mCachedBaseValue = aBaseValue; + mForceCompositing = true; + } +} + +} // namespace mozilla diff --git a/dom/smil/SMILCompositor.h b/dom/smil/SMILCompositor.h new file mode 100644 index 0000000000..5f987d5854 --- /dev/null +++ b/dom/smil/SMILCompositor.h @@ -0,0 +1,132 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILCOMPOSITOR_H_ +#define DOM_SMIL_SMILCOMPOSITOR_H_ + +#include + +#include "PLDHashTable.h" +#include "SMILTargetIdentifier.h" +#include "mozilla/SMILAnimationFunction.h" +#include "mozilla/SMILCompositorTable.h" +#include "mozilla/UniquePtr.h" +#include "nsCSSPropertyID.h" +#include "nsString.h" +#include "nsTHashtable.h" + +namespace mozilla { + +class ComputedStyle; + +//---------------------------------------------------------------------- +// SMILCompositor +// +// Performs the composition of the animation sandwich by combining the results +// of a series animation functions according to the rules of SMIL composition +// including prioritising animations. + +class SMILCompositor : public PLDHashEntryHdr { + public: + using KeyType = SMILTargetIdentifier; + using KeyTypeRef = const KeyType&; + using KeyTypePointer = const KeyType*; + + explicit SMILCompositor(KeyTypePointer aKey) + : mKey(*aKey), mForceCompositing(false) {} + SMILCompositor(SMILCompositor&& toMove) noexcept + : PLDHashEntryHdr(std::move(toMove)), + mKey(std::move(toMove.mKey)), + mAnimationFunctions(std::move(toMove.mAnimationFunctions)), + mForceCompositing(false) {} + + // PLDHashEntryHdr methods + KeyTypeRef GetKey() const { return mKey; } + bool KeyEquals(KeyTypePointer aKey) const; + static KeyTypePointer KeyToPointer(KeyTypeRef aKey) { return &aKey; } + static PLDHashNumber HashKey(KeyTypePointer aKey); + enum { ALLOW_MEMMOVE = false }; + + // Adds the given animation function to this Compositor's list of functions + void AddAnimationFunction(SMILAnimationFunction* aFunc); + + // Composes the attribute's current value with the list of animation + // functions, and assigns the resulting value to this compositor's target + // attribute. If a change is made that might produce style updates, + // aMightHavePendingStyleUpdates is set to true. Otherwise it is not modified. + void ComposeAttribute(bool& aMightHavePendingStyleUpdates); + + // Clears animation effects on my target attribute + void ClearAnimationEffects(); + + // Cycle-collection support + void Traverse(nsCycleCollectionTraversalCallback* aCallback); + + // Toggles a bit that will force us to composite (bypassing early-return + // optimizations) when we hit ComposeAttribute. + void ToggleForceCompositing() { mForceCompositing = true; } + + // Transfers |aOther|'s mCachedBaseValue to |this| + void StealCachedBaseValue(SMILCompositor* aOther) { + mCachedBaseValue = std::move(aOther->mCachedBaseValue); + } + + bool HasSameNumberOfAnimationFunctionsAs(const SMILCompositor& aOther) const { + return mAnimationFunctions.Length() == aOther.mAnimationFunctions.Length(); + } + + private: + // Create a SMILAttr for my target, on the heap. + // + // @param aBaseComputedStyle An optional ComputedStyle which, if set, will be + // used when fetching the base style. + UniquePtr CreateSMILAttr(const ComputedStyle* aBaseComputedStyle); + + // Returns the CSS property this compositor should animate, or + // eCSSProperty_UNKNOWN if this compositor does not animate a CSS property. + nsCSSPropertyID GetCSSPropertyToAnimate() const; + + // Returns true if we might need to refer to base styles (i.e. we are + // targeting a CSS property and have one or more animation functions that + // don't just replace the underlying value). + // + // This might return true in some cases where we don't actually need the base + // style since it doesn't build up the animation sandwich to check if the + // functions that appear to need the base style are actually replaced by + // a function further up the stack. + bool MightNeedBaseStyle() const; + + // Finds the index of the first function that will affect our animation + // sandwich. Also toggles the 'mForceCompositing' flag if it finds that any + // (used) functions have changed. + uint32_t GetFirstFuncToAffectSandwich(); + + // If the passed-in base value differs from our cached base value, this + // method updates the cached value (and toggles the 'mForceCompositing' flag) + void UpdateCachedBaseValue(const SMILValue& aBaseValue); + + // The hash key (tuple of element and attributeName) + KeyType mKey; + + // Hash Value: List of animation functions that animate the specified attr + nsTArray mAnimationFunctions; + + // Member data for detecting when we need to force-recompose + // --------------------------------------------------------- + // Flag for tracking whether we need to compose. Initialized to false, but + // gets flipped to true if we detect that something has changed. + bool mForceCompositing; + + // Cached base value, so we can detect & force-recompose when it changes + // from one sample to the next. (SMILAnimationController moves this + // forward from the previous sample's compositor by calling + // StealCachedBaseValue.) + SMILValue mCachedBaseValue; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILCOMPOSITOR_H_ diff --git a/dom/smil/SMILCompositorTable.h b/dom/smil/SMILCompositorTable.h new file mode 100644 index 0000000000..649c108872 --- /dev/null +++ b/dom/smil/SMILCompositorTable.h @@ -0,0 +1,28 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILCOMPOSITORTABLE_H_ +#define DOM_SMIL_SMILCOMPOSITORTABLE_H_ + +#include "nsTHashtable.h" + +//---------------------------------------------------------------------- +// SMILCompositorTable : A hashmap of SMILCompositors +// +// This is just a forward-declaration because it is included in +// SMILAnimationController which is used in Document. We don't want to +// expose all of SMILCompositor or otherwise any changes to it will mean the +// whole world will need to be rebuilt. + +namespace mozilla { + +class SMILCompositor; + +using SMILCompositorTable = nsTHashtable; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILCOMPOSITORTABLE_H_ diff --git a/dom/smil/SMILEnumType.cpp b/dom/smil/SMILEnumType.cpp new file mode 100644 index 0000000000..fbc5c6d2b8 --- /dev/null +++ b/dom/smil/SMILEnumType.cpp @@ -0,0 +1,69 @@ +/* -*- 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 "SMILEnumType.h" + +#include "mozilla/SMILValue.h" +#include "nsDebug.h" +#include + +namespace mozilla { + +void SMILEnumType::Init(SMILValue& aValue) const { + MOZ_ASSERT(aValue.IsNull(), "Unexpected value type"); + aValue.mU.mUint = 0; + aValue.mType = this; +} + +void SMILEnumType::Destroy(SMILValue& aValue) const { + MOZ_ASSERT(aValue.mType == this, "Unexpected SMIL value"); + aValue.mU.mUint = 0; + aValue.mType = SMILNullType::Singleton(); +} + +nsresult SMILEnumType::Assign(SMILValue& aDest, const SMILValue& aSrc) const { + MOZ_ASSERT(aDest.mType == aSrc.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aDest.mType == this, "Unexpected SMIL value"); + aDest.mU.mUint = aSrc.mU.mUint; + return NS_OK; +} + +bool SMILEnumType::IsEqual(const SMILValue& aLeft, + const SMILValue& aRight) const { + MOZ_ASSERT(aLeft.mType == aRight.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aLeft.mType == this, "Unexpected type for SMIL value"); + + return aLeft.mU.mUint == aRight.mU.mUint; +} + +nsresult SMILEnumType::Add(SMILValue& aDest, const SMILValue& aValueToAdd, + uint32_t aCount) const { + MOZ_ASSERT(aValueToAdd.mType == aDest.mType, "Trying to add invalid types"); + MOZ_ASSERT(aValueToAdd.mType == this, "Unexpected source type"); + return NS_ERROR_FAILURE; // enum values can't be added to each other +} + +nsresult SMILEnumType::ComputeDistance(const SMILValue& aFrom, + const SMILValue& aTo, + double& aDistance) const { + MOZ_ASSERT(aFrom.mType == aTo.mType, "Trying to compare different types"); + MOZ_ASSERT(aFrom.mType == this, "Unexpected source type"); + return NS_ERROR_FAILURE; // there is no concept of distance between enum + // values +} + +nsresult SMILEnumType::Interpolate(const SMILValue& aStartVal, + const SMILValue& aEndVal, + double aUnitDistance, + SMILValue& aResult) const { + MOZ_ASSERT(aStartVal.mType == aEndVal.mType, + "Trying to interpolate different types"); + MOZ_ASSERT(aStartVal.mType == this, "Unexpected types for interpolation"); + MOZ_ASSERT(aResult.mType == this, "Unexpected result type"); + return NS_ERROR_FAILURE; // enum values do not interpolate +} + +} // namespace mozilla diff --git a/dom/smil/SMILEnumType.h b/dom/smil/SMILEnumType.h new file mode 100644 index 0000000000..a380ad21b7 --- /dev/null +++ b/dom/smil/SMILEnumType.h @@ -0,0 +1,44 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILENUMTYPE_H_ +#define DOM_SMIL_SMILENUMTYPE_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/SMILType.h" + +namespace mozilla { + +class SMILEnumType : public SMILType { + public: + // Singleton for SMILValue objects to hold onto. + static SMILEnumType* Singleton() { + static SMILEnumType sSingleton; + return &sSingleton; + } + + protected: + // SMILType Methods + // ------------------- + void Init(SMILValue& aValue) const override; + void Destroy(SMILValue& aValue) const override; + nsresult Assign(SMILValue& aDest, const SMILValue& aSrc) const override; + bool IsEqual(const SMILValue& aLeft, const SMILValue& aRight) const override; + nsresult Add(SMILValue& aDest, const SMILValue& aValueToAdd, + uint32_t aCount) const override; + nsresult ComputeDistance(const SMILValue& aFrom, const SMILValue& aTo, + double& aDistance) const override; + nsresult Interpolate(const SMILValue& aStartVal, const SMILValue& aEndVal, + double aUnitDistance, SMILValue& aResult) const override; + + private: + // Private constructor: prevent instances beyond my singleton. + constexpr SMILEnumType() = default; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILENUMTYPE_H_ diff --git a/dom/smil/SMILFloatType.cpp b/dom/smil/SMILFloatType.cpp new file mode 100644 index 0000000000..ac50cfe8d2 --- /dev/null +++ b/dom/smil/SMILFloatType.cpp @@ -0,0 +1,81 @@ +/* -*- 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 "SMILFloatType.h" + +#include "mozilla/SMILValue.h" +#include "nsDebug.h" +#include + +namespace mozilla { + +void SMILFloatType::Init(SMILValue& aValue) const { + MOZ_ASSERT(aValue.IsNull(), "Unexpected value type"); + aValue.mU.mDouble = 0.0; + aValue.mType = this; +} + +void SMILFloatType::Destroy(SMILValue& aValue) const { + MOZ_ASSERT(aValue.mType == this, "Unexpected SMIL value"); + aValue.mU.mDouble = 0.0; + aValue.mType = SMILNullType::Singleton(); +} + +nsresult SMILFloatType::Assign(SMILValue& aDest, const SMILValue& aSrc) const { + MOZ_ASSERT(aDest.mType == aSrc.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aDest.mType == this, "Unexpected SMIL value"); + aDest.mU.mDouble = aSrc.mU.mDouble; + return NS_OK; +} + +bool SMILFloatType::IsEqual(const SMILValue& aLeft, + const SMILValue& aRight) const { + MOZ_ASSERT(aLeft.mType == aRight.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aLeft.mType == this, "Unexpected type for SMIL value"); + + return aLeft.mU.mDouble == aRight.mU.mDouble; +} + +nsresult SMILFloatType::Add(SMILValue& aDest, const SMILValue& aValueToAdd, + uint32_t aCount) const { + MOZ_ASSERT(aValueToAdd.mType == aDest.mType, "Trying to add invalid types"); + MOZ_ASSERT(aValueToAdd.mType == this, "Unexpected source type"); + aDest.mU.mDouble += aValueToAdd.mU.mDouble * aCount; + return NS_OK; +} + +nsresult SMILFloatType::ComputeDistance(const SMILValue& aFrom, + const SMILValue& aTo, + double& aDistance) const { + MOZ_ASSERT(aFrom.mType == aTo.mType, "Trying to compare different types"); + MOZ_ASSERT(aFrom.mType == this, "Unexpected source type"); + + const double& from = aFrom.mU.mDouble; + const double& to = aTo.mU.mDouble; + + aDistance = fabs(to - from); + + return NS_OK; +} + +nsresult SMILFloatType::Interpolate(const SMILValue& aStartVal, + const SMILValue& aEndVal, + double aUnitDistance, + SMILValue& aResult) const { + MOZ_ASSERT(aStartVal.mType == aEndVal.mType, + "Trying to interpolate different types"); + MOZ_ASSERT(aStartVal.mType == this, "Unexpected types for interpolation"); + MOZ_ASSERT(aResult.mType == this, "Unexpected result type"); + + const double& startVal = aStartVal.mU.mDouble; + const double& endVal = aEndVal.mU.mDouble; + + aResult.mU.mDouble = (startVal + (endVal - startVal) * aUnitDistance); + + return NS_OK; +} + +} // namespace mozilla diff --git a/dom/smil/SMILFloatType.h b/dom/smil/SMILFloatType.h new file mode 100644 index 0000000000..18e3d0fdfa --- /dev/null +++ b/dom/smil/SMILFloatType.h @@ -0,0 +1,44 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILFLOATTYPE_H_ +#define DOM_SMIL_SMILFLOATTYPE_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/SMILType.h" + +namespace mozilla { + +class SMILFloatType : public SMILType { + public: + // Singleton for SMILValue objects to hold onto. + static SMILFloatType* Singleton() { + static SMILFloatType sSingleton; + return &sSingleton; + } + + protected: + // SMILType Methods + // ------------------- + void Init(SMILValue& aValue) const override; + void Destroy(SMILValue& aValue) const override; + nsresult Assign(SMILValue& aDest, const SMILValue& aSrc) const override; + bool IsEqual(const SMILValue& aLeft, const SMILValue& aRight) const override; + nsresult Add(SMILValue& aDest, const SMILValue& aValueToAdd, + uint32_t aCount) const override; + nsresult ComputeDistance(const SMILValue& aFrom, const SMILValue& aTo, + double& aDistance) const override; + nsresult Interpolate(const SMILValue& aStartVal, const SMILValue& aEndVal, + double aUnitDistance, SMILValue& aResult) const override; + + private: + // Private constructor: prevent instances beyond my singleton. + constexpr SMILFloatType() = default; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILFLOATTYPE_H_ diff --git a/dom/smil/SMILInstanceTime.cpp b/dom/smil/SMILInstanceTime.cpp new file mode 100644 index 0000000000..7f38b8c1d7 --- /dev/null +++ b/dom/smil/SMILInstanceTime.cpp @@ -0,0 +1,188 @@ +/* -*- 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 "SMILInstanceTime.h" + +#include "mozilla/AutoRestore.h" +#include "mozilla/SMILInterval.h" +#include "mozilla/SMILTimeValueSpec.h" + +namespace mozilla { + +//---------------------------------------------------------------------- +// Implementation + +SMILInstanceTime::SMILInstanceTime(const SMILTimeValue& aTime, + SMILInstanceTimeSource aSource, + SMILTimeValueSpec* aCreator, + SMILInterval* aBaseInterval) + : mTime(aTime), + mFlags(0), + mVisited(false), + mFixedEndpointRefCnt(0), + mSerial(0), + mCreator(aCreator), + mBaseInterval(nullptr) // This will get set to aBaseInterval in a call to + // SetBaseInterval() at end of constructor +{ + switch (aSource) { + case SOURCE_NONE: + // No special flags + break; + + case SOURCE_DOM: + mFlags = kDynamic | kFromDOM; + break; + + case SOURCE_SYNCBASE: + mFlags = kMayUpdate; + break; + + case SOURCE_EVENT: + mFlags = kDynamic; + break; + } + + SetBaseInterval(aBaseInterval); +} + +SMILInstanceTime::~SMILInstanceTime() { + MOZ_ASSERT(!mBaseInterval, + "Destroying instance time without first calling Unlink()"); + MOZ_ASSERT(mFixedEndpointRefCnt == 0, + "Destroying instance time that is still used as the fixed " + "endpoint of an interval"); +} + +void SMILInstanceTime::Unlink() { + RefPtr deathGrip(this); + if (mBaseInterval) { + mBaseInterval->RemoveDependentTime(*this); + mBaseInterval = nullptr; + } + mCreator = nullptr; +} + +void SMILInstanceTime::HandleChangedInterval( + const SMILTimeContainer* aSrcContainer, bool aBeginObjectChanged, + bool aEndObjectChanged) { + // It's possible a sequence of notifications might cause our base interval to + // be updated and then deleted. Furthermore, the delete might happen whilst + // we're still in the queue to be notified of the change. In any case, if we + // don't have a base interval, just ignore the change. + if (!mBaseInterval) return; + + MOZ_ASSERT(mCreator, "Base interval is set but creator is not."); + + if (mVisited) { + // Break the cycle here + Unlink(); + return; + } + + bool objectChanged = + mCreator->DependsOnBegin() ? aBeginObjectChanged : aEndObjectChanged; + + RefPtr deathGrip(this); + mozilla::AutoRestore setVisited(mVisited); + mVisited = true; + + mCreator->HandleChangedInstanceTime(*GetBaseTime(), aSrcContainer, *this, + objectChanged); +} + +void SMILInstanceTime::HandleDeletedInterval() { + MOZ_ASSERT(mBaseInterval, + "Got call to HandleDeletedInterval on an independent instance " + "time"); + MOZ_ASSERT(mCreator, "Base interval is set but creator is not"); + + mBaseInterval = nullptr; + mFlags &= ~kMayUpdate; // Can't update without a base interval + + RefPtr deathGrip(this); + mCreator->HandleDeletedInstanceTime(*this); + mCreator = nullptr; +} + +void SMILInstanceTime::HandleFilteredInterval() { + MOZ_ASSERT(mBaseInterval, + "Got call to HandleFilteredInterval on an independent instance " + "time"); + + mBaseInterval = nullptr; + mFlags &= ~kMayUpdate; // Can't update without a base interval + mCreator = nullptr; +} + +bool SMILInstanceTime::ShouldPreserve() const { + return mFixedEndpointRefCnt > 0 || (mFlags & kWasDynamicEndpoint); +} + +void SMILInstanceTime::UnmarkShouldPreserve() { + mFlags &= ~kWasDynamicEndpoint; +} + +void SMILInstanceTime::AddRefFixedEndpoint() { + MOZ_ASSERT(mFixedEndpointRefCnt < UINT16_MAX, + "Fixed endpoint reference count upper limit reached"); + ++mFixedEndpointRefCnt; + mFlags &= ~kMayUpdate; // Once fixed, always fixed +} + +void SMILInstanceTime::ReleaseFixedEndpoint() { + MOZ_ASSERT(mFixedEndpointRefCnt > 0, "Duplicate release"); + --mFixedEndpointRefCnt; + if (mFixedEndpointRefCnt == 0 && IsDynamic()) { + mFlags |= kWasDynamicEndpoint; + } +} + +bool SMILInstanceTime::IsDependentOn(const SMILInstanceTime& aOther) const { + if (mVisited) return false; + + const SMILInstanceTime* myBaseTime = GetBaseTime(); + if (!myBaseTime) return false; + + if (myBaseTime == &aOther) return true; + + mozilla::AutoRestore setVisited(mVisited); + mVisited = true; + return myBaseTime->IsDependentOn(aOther); +} + +const SMILInstanceTime* SMILInstanceTime::GetBaseTime() const { + if (!mBaseInterval) { + return nullptr; + } + + MOZ_ASSERT(mCreator, "Base interval is set but there is no creator."); + if (!mCreator) { + return nullptr; + } + + return mCreator->DependsOnBegin() ? mBaseInterval->Begin() + : mBaseInterval->End(); +} + +void SMILInstanceTime::SetBaseInterval(SMILInterval* aBaseInterval) { + MOZ_ASSERT(!mBaseInterval, + "Attempting to reassociate an instance time with a different " + "interval."); + + if (aBaseInterval) { + MOZ_ASSERT(mCreator, + "Attempting to create a dependent instance time without " + "reference to the creating SMILTimeValueSpec object."); + if (!mCreator) return; + + aBaseInterval->AddDependentTime(*this); + } + + mBaseInterval = aBaseInterval; +} + +} // namespace mozilla diff --git a/dom/smil/SMILInstanceTime.h b/dom/smil/SMILInstanceTime.h new file mode 100644 index 0000000000..1224cfc8fe --- /dev/null +++ b/dom/smil/SMILInstanceTime.h @@ -0,0 +1,166 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILINSTANCETIME_H_ +#define DOM_SMIL_SMILINSTANCETIME_H_ + +#include "nsISupportsImpl.h" +#include "mozilla/SMILTimeValue.h" + +namespace mozilla { +class SMILInterval; +class SMILTimeContainer; +class SMILTimeValueSpec; + +//---------------------------------------------------------------------- +// SMILInstanceTime +// +// An instant in document simple time that may be used in creating a new +// interval. +// +// For an overview of how this class is related to other SMIL time classes see +// the documentation in SMILTimeValue.h +// +// These objects are owned by an SMILTimedElement but MAY also be referenced +// by: +// +// a) SMILIntervals that belong to the same SMILTimedElement and which refer +// to the SMILInstanceTimes which form the interval endpoints; and/or +// b) SMILIntervals that belong to other SMILTimedElements but which need to +// update dependent instance times when they change or are deleted. +// E.g. for begin='a.begin', 'a' needs to inform dependent +// SMILInstanceTimes if its begin time changes. This notification is +// performed by the SMILInterval. + +class SMILInstanceTime final { + public: + // Instance time source. Times generated by events, syncbase relationships, + // and DOM calls behave differently in some circumstances such as when a timed + // element is reset. + enum SMILInstanceTimeSource { + // No particularly significant source, e.g. offset time, 'indefinite' + SOURCE_NONE, + // Generated by a DOM call such as beginElement + SOURCE_DOM, + // Generated by a syncbase relationship + SOURCE_SYNCBASE, + // Generated by an event + SOURCE_EVENT + }; + + explicit SMILInstanceTime(const SMILTimeValue& aTime, + SMILInstanceTimeSource aSource = SOURCE_NONE, + SMILTimeValueSpec* aCreator = nullptr, + SMILInterval* aBaseInterval = nullptr); + + void Unlink(); + void HandleChangedInterval(const SMILTimeContainer* aSrcContainer, + bool aBeginObjectChanged, bool aEndObjectChanged); + void HandleDeletedInterval(); + void HandleFilteredInterval(); + + const SMILTimeValue& Time() const { return mTime; } + const SMILTimeValueSpec* GetCreator() const { return mCreator; } + + bool IsDynamic() const { return !!(mFlags & kDynamic); } + bool IsFixedTime() const { return !(mFlags & kMayUpdate); } + bool FromDOM() const { return !!(mFlags & kFromDOM); } + + bool ShouldPreserve() const; + void UnmarkShouldPreserve(); + + void AddRefFixedEndpoint(); + void ReleaseFixedEndpoint(); + + void DependentUpdate(const SMILTimeValue& aNewTime) { + MOZ_ASSERT(!IsFixedTime(), + "Updating an instance time that is not expected to be updated"); + mTime = aNewTime; + } + + bool IsDependent() const { return !!mBaseInterval; } + bool IsDependentOn(const SMILInstanceTime& aOther) const; + const SMILInterval* GetBaseInterval() const { return mBaseInterval; } + const SMILInstanceTime* GetBaseTime() const; + + bool SameTimeAndBase(const SMILInstanceTime& aOther) const { + return mTime == aOther.mTime && GetBaseTime() == aOther.GetBaseTime(); + } + + // Get and set a serial number which may be used by a containing class to + // control the sort order of otherwise similar instance times. + uint32_t Serial() const { return mSerial; } + void SetSerial(uint32_t aIndex) { mSerial = aIndex; } + + NS_INLINE_DECL_REFCOUNTING(SMILInstanceTime) + + private: + // Private destructor, to discourage deletion outside of Release(): + ~SMILInstanceTime(); + + void SetBaseInterval(SMILInterval* aBaseInterval); + + SMILTimeValue mTime; + + // Internal flags used to represent the behaviour of different instance times + enum { + // Indicates that this instance time was generated by an event or a DOM + // call. Such instance times require special handling when (i) the owning + // element is reset, (ii) when they are to be added as a new end instance + // times (as per SMIL's event sensitivity contraints), and (iii) when + // a backwards seek is performed and the timing model is reconstructed. + kDynamic = 1, + + // Indicates that this instance time is referred to by an + // SMILTimeValueSpec and as such may be updated. Such instance time should + // not be filtered out by the SMILTimedElement even if they appear to be + // in the past as they may be updated to a future time. + kMayUpdate = 2, + + // Indicates that this instance time was generated from the DOM as opposed + // to an SMILTimeValueSpec. When a 'begin' or 'end' attribute is set or + // reset we should clear all the instance times that have been generated by + // that attribute (and hence an SMILTimeValueSpec), but not those from the + // DOM. + kFromDOM = 4, + + // Indicates that this instance time was used as the endpoint of an interval + // that has been filtered or removed. However, since it is a dynamic time it + // should be preserved and not filtered. + kWasDynamicEndpoint = 8 + }; + uint8_t mFlags; // Combination of kDynamic, kMayUpdate, etc. + mutable bool mVisited; // Cycle tracking + + // Additional reference count to determine if this instance time is currently + // used as a fixed endpoint in any intervals. Instance times that are used in + // this way should not be removed when the owning SMILTimedElement removes + // instance times in response to a restart or in an attempt to free up memory + // by filtering out old instance times. + // + // Instance times are only shared in a few cases, namely: + // a) early ends, + // b) zero-duration intervals, + // c) momentarily whilst establishing new intervals and updating the current + // interval, and + // d) trimmed intervals + // Hence the limited range of a uint16_t should be more than adequate. + uint16_t mFixedEndpointRefCnt; + + uint32_t mSerial; // A serial number used by the containing class to + // specify the sort order for instance times with the + // same mTime. + + SMILTimeValueSpec* mCreator; // The SMILTimeValueSpec object that created + // us. (currently only needed for syncbase + // instance times.) + SMILInterval* mBaseInterval; // Interval from which this time is derived + // (only used for syncbase instance times) +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILINSTANCETIME_H_ diff --git a/dom/smil/SMILIntegerType.cpp b/dom/smil/SMILIntegerType.cpp new file mode 100644 index 0000000000..c1ac207836 --- /dev/null +++ b/dom/smil/SMILIntegerType.cpp @@ -0,0 +1,86 @@ +/* -*- 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 "SMILIntegerType.h" + +#include "mozilla/SMILValue.h" +#include "nsDebug.h" +#include + +namespace mozilla { + +void SMILIntegerType::Init(SMILValue& aValue) const { + MOZ_ASSERT(aValue.IsNull(), "Unexpected value type"); + aValue.mU.mInt = 0; + aValue.mType = this; +} + +void SMILIntegerType::Destroy(SMILValue& aValue) const { + MOZ_ASSERT(aValue.mType == this, "Unexpected SMIL value"); + aValue.mU.mInt = 0; + aValue.mType = SMILNullType::Singleton(); +} + +nsresult SMILIntegerType::Assign(SMILValue& aDest, + const SMILValue& aSrc) const { + MOZ_ASSERT(aDest.mType == aSrc.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aDest.mType == this, "Unexpected SMIL value"); + aDest.mU.mInt = aSrc.mU.mInt; + return NS_OK; +} + +bool SMILIntegerType::IsEqual(const SMILValue& aLeft, + const SMILValue& aRight) const { + MOZ_ASSERT(aLeft.mType == aRight.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aLeft.mType == this, "Unexpected type for SMIL value"); + + return aLeft.mU.mInt == aRight.mU.mInt; +} + +nsresult SMILIntegerType::Add(SMILValue& aDest, const SMILValue& aValueToAdd, + uint32_t aCount) const { + MOZ_ASSERT(aValueToAdd.mType == aDest.mType, "Trying to add invalid types"); + MOZ_ASSERT(aValueToAdd.mType == this, "Unexpected source type"); + aDest.mU.mInt += aValueToAdd.mU.mInt * aCount; + return NS_OK; +} + +nsresult SMILIntegerType::ComputeDistance(const SMILValue& aFrom, + const SMILValue& aTo, + double& aDistance) const { + MOZ_ASSERT(aFrom.mType == aTo.mType, "Trying to compare different types"); + MOZ_ASSERT(aFrom.mType == this, "Unexpected source type"); + aDistance = fabs(double(aTo.mU.mInt - aFrom.mU.mInt)); + return NS_OK; +} + +nsresult SMILIntegerType::Interpolate(const SMILValue& aStartVal, + const SMILValue& aEndVal, + double aUnitDistance, + SMILValue& aResult) const { + MOZ_ASSERT(aStartVal.mType == aEndVal.mType, + "Trying to interpolate different types"); + MOZ_ASSERT(aStartVal.mType == this, "Unexpected types for interpolation"); + MOZ_ASSERT(aResult.mType == this, "Unexpected result type"); + + const double startVal = double(aStartVal.mU.mInt); + const double endVal = double(aEndVal.mU.mInt); + const double currentVal = startVal + (endVal - startVal) * aUnitDistance; + + // When currentVal is exactly midway between its two nearest integers, we + // jump to the "next" integer to provide simple, easy to remember and + // consistent behaviour (from the SMIL author's point of view). + + if (startVal < endVal) { + aResult.mU.mInt = int64_t(floor(currentVal + 0.5)); // round mid up + } else { + aResult.mU.mInt = int64_t(ceil(currentVal - 0.5)); // round mid down + } + + return NS_OK; +} + +} // namespace mozilla diff --git a/dom/smil/SMILIntegerType.h b/dom/smil/SMILIntegerType.h new file mode 100644 index 0000000000..6bdff63d80 --- /dev/null +++ b/dom/smil/SMILIntegerType.h @@ -0,0 +1,39 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILINTEGERTYPE_H_ +#define DOM_SMIL_SMILINTEGERTYPE_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/SMILType.h" + +namespace mozilla { + +class SMILIntegerType : public SMILType { + public: + void Init(SMILValue& aValue) const override; + void Destroy(SMILValue& aValue) const override; + nsresult Assign(SMILValue& aDest, const SMILValue& aSrc) const override; + bool IsEqual(const SMILValue& aLeft, const SMILValue& aRight) const override; + nsresult Add(SMILValue& aDest, const SMILValue& aValueToAdd, + uint32_t aCount) const override; + nsresult ComputeDistance(const SMILValue& aFrom, const SMILValue& aTo, + double& aDistance) const override; + nsresult Interpolate(const SMILValue& aStartVal, const SMILValue& aEndVal, + double aUnitDistance, SMILValue& aResult) const override; + + static SMILIntegerType* Singleton() { + static SMILIntegerType sSingleton; + return &sSingleton; + } + + private: + constexpr SMILIntegerType() = default; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILINTEGERTYPE_H_ diff --git a/dom/smil/SMILInterval.cpp b/dom/smil/SMILInterval.cpp new file mode 100644 index 0000000000..e0ee99ac00 --- /dev/null +++ b/dom/smil/SMILInterval.cpp @@ -0,0 +1,137 @@ +/* -*- 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 "SMILInterval.h" + +#include "mozilla/DebugOnly.h" + +namespace mozilla { + +SMILInterval::SMILInterval() : mBeginFixed(false), mEndFixed(false) {} + +SMILInterval::SMILInterval(const SMILInterval& aOther) + : mBegin(aOther.mBegin), + mEnd(aOther.mEnd), + mBeginFixed(false), + mEndFixed(false) { + MOZ_ASSERT(aOther.mDependentTimes.IsEmpty(), + "Attempt to copy-construct an interval with dependent times; this " + "will lead to instance times being shared between intervals."); + + // For the time being we don't allow intervals with fixed endpoints to be + // copied since we only ever copy-construct to establish a new current + // interval. If we ever need to copy historical intervals we may need to move + // the ReleaseFixedEndpoint calls from Unlink to the dtor. + MOZ_ASSERT(!aOther.mBeginFixed && !aOther.mEndFixed, + "Attempt to copy-construct an interval with fixed endpoints"); +} + +SMILInterval::~SMILInterval() { + MOZ_ASSERT(mDependentTimes.IsEmpty(), + "Destroying interval without disassociating dependent instance " + "times. Unlink was not called"); +} + +void SMILInterval::Unlink(bool aFiltered) { + for (int32_t i = mDependentTimes.Length() - 1; i >= 0; --i) { + if (aFiltered) { + mDependentTimes[i]->HandleFilteredInterval(); + } else { + mDependentTimes[i]->HandleDeletedInterval(); + } + } + mDependentTimes.Clear(); + if (mBegin && mBeginFixed) { + mBegin->ReleaseFixedEndpoint(); + } + mBegin = nullptr; + if (mEnd && mEndFixed) { + mEnd->ReleaseFixedEndpoint(); + } + mEnd = nullptr; +} + +SMILInstanceTime* SMILInterval::Begin() { + MOZ_ASSERT(mBegin && mEnd, "Requesting Begin() on un-initialized interval."); + return mBegin; +} + +SMILInstanceTime* SMILInterval::End() { + MOZ_ASSERT(mBegin && mEnd, "Requesting End() on un-initialized interval."); + return mEnd; +} + +void SMILInterval::SetBegin(SMILInstanceTime& aBegin) { + MOZ_ASSERT(aBegin.Time().IsDefinite(), + "Attempt to set unresolved or indefinite begin time on interval"); + MOZ_ASSERT(!mBeginFixed, + "Attempt to set begin time but the begin point is fixed"); + // Check that we're not making an instance time dependent on itself. Such an + // arrangement does not make intuitive sense and should be detected when + // creating or updating intervals. + MOZ_ASSERT(!mBegin || aBegin.GetBaseTime() != mBegin, + "Attempt to make self-dependent instance time"); + + mBegin = &aBegin; +} + +void SMILInterval::SetEnd(SMILInstanceTime& aEnd) { + MOZ_ASSERT(!mEndFixed, "Attempt to set end time but the end point is fixed"); + // As with SetBegin, check we're not making an instance time dependent on + // itself. + MOZ_ASSERT(!mEnd || aEnd.GetBaseTime() != mEnd, + "Attempting to make self-dependent instance time"); + + mEnd = &aEnd; +} + +void SMILInterval::FixBegin() { + MOZ_ASSERT(mBegin && mEnd, "Fixing begin point on un-initialized interval"); + MOZ_ASSERT(!mBeginFixed, "Duplicate calls to FixBegin()"); + mBeginFixed = true; + mBegin->AddRefFixedEndpoint(); +} + +void SMILInterval::FixEnd() { + MOZ_ASSERT(mBegin && mEnd, "Fixing end point on un-initialized interval"); + MOZ_ASSERT(mBeginFixed, + "Fixing the end of an interval without a fixed begin"); + MOZ_ASSERT(!mEndFixed, "Duplicate calls to FixEnd()"); + mEndFixed = true; + mEnd->AddRefFixedEndpoint(); +} + +void SMILInterval::AddDependentTime(SMILInstanceTime& aTime) { + RefPtr* inserted = + mDependentTimes.InsertElementSorted(&aTime); + if (!inserted) { + NS_WARNING("Insufficient memory to insert instance time."); + } +} + +void SMILInterval::RemoveDependentTime(const SMILInstanceTime& aTime) { + DebugOnly found = mDependentTimes.RemoveElementSorted(&aTime); + MOZ_ASSERT(found, "Couldn't find instance time to delete."); +} + +void SMILInterval::GetDependentTimes(InstanceTimeList& aTimes) { + aTimes = mDependentTimes.Clone(); +} + +bool SMILInterval::IsDependencyChainLink() const { + if (!mBegin || !mEnd) + return false; // Not yet initialised so it can't be part of a chain + + if (mDependentTimes.IsEmpty()) return false; // No dependents, chain end + + // So we have dependents, but we're still only a link in the chain (as opposed + // to the end of the chain) if one of our endpoints is dependent on an + // interval other than ourselves. + return (mBegin->IsDependent() && mBegin->GetBaseInterval() != this) || + (mEnd->IsDependent() && mEnd->GetBaseInterval() != this); +} + +} // namespace mozilla diff --git a/dom/smil/SMILInterval.h b/dom/smil/SMILInterval.h new file mode 100644 index 0000000000..a569fce147 --- /dev/null +++ b/dom/smil/SMILInterval.h @@ -0,0 +1,86 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILINTERVAL_H_ +#define DOM_SMIL_SMILINTERVAL_H_ + +#include "mozilla/SMILInstanceTime.h" +#include "nsTArray.h" + +namespace mozilla { + +//---------------------------------------------------------------------- +// SMILInterval class +// +// A structure consisting of a begin and end time. The begin time must be +// resolved (i.e. not indefinite or unresolved). +// +// For an overview of how this class is related to other SMIL time classes see +// the documentation in SMILTimeValue.h + +class SMILInterval { + public: + SMILInterval(); + SMILInterval(const SMILInterval& aOther); + ~SMILInterval(); + void Unlink(bool aFiltered = false); + + const SMILInstanceTime* Begin() const { + MOZ_ASSERT(mBegin && mEnd, + "Requesting Begin() on un-initialized instance time"); + return mBegin; + } + SMILInstanceTime* Begin(); + + const SMILInstanceTime* End() const { + MOZ_ASSERT(mBegin && mEnd, + "Requesting End() on un-initialized instance time"); + return mEnd; + } + SMILInstanceTime* End(); + + void SetBegin(SMILInstanceTime& aBegin); + void SetEnd(SMILInstanceTime& aEnd); + void Set(SMILInstanceTime& aBegin, SMILInstanceTime& aEnd) { + SetBegin(aBegin); + SetEnd(aEnd); + } + + void FixBegin(); + void FixEnd(); + + using InstanceTimeList = nsTArray>; + + void AddDependentTime(SMILInstanceTime& aTime); + void RemoveDependentTime(const SMILInstanceTime& aTime); + void GetDependentTimes(InstanceTimeList& aTimes); + + // Cue for assessing if this interval can be filtered + bool IsDependencyChainLink() const; + + private: + RefPtr mBegin; + RefPtr mEnd; + + // SMILInstanceTimes to notify when this interval is changed or deleted. + InstanceTimeList mDependentTimes; + + // Indicates if the end points of the interval are fixed or not. + // + // Note that this is not the same as having an end point whose TIME is fixed + // (i.e. SMILInstanceTime::IsFixed() returns true). This is because it is + // possible to have an end point with a fixed TIME and yet still update the + // end point to refer to a different SMILInstanceTime object. + // + // However, if mBegin/EndFixed is true, then BOTH the SMILInstanceTime + // OBJECT returned for that end point and its TIME value will not change. + bool mBeginFixed; + bool mEndFixed; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILINTERVAL_H_ diff --git a/dom/smil/SMILKeySpline.cpp b/dom/smil/SMILKeySpline.cpp new file mode 100644 index 0000000000..dd508e1bcb --- /dev/null +++ b/dom/smil/SMILKeySpline.cpp @@ -0,0 +1,127 @@ +/* -*- 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 "SMILKeySpline.h" +#include +#include + +namespace mozilla { + +#define NEWTON_ITERATIONS 4 +#define NEWTON_MIN_SLOPE 0.02 +#define SUBDIVISION_PRECISION 0.0000001 +#define SUBDIVISION_MAX_ITERATIONS 10 + +const double SMILKeySpline::kSampleStepSize = + 1.0 / double(kSplineTableSize - 1); + +void SMILKeySpline::Init(double aX1, double aY1, double aX2, double aY2) { + mX1 = aX1; + mY1 = aY1; + mX2 = aX2; + mY2 = aY2; + + if (mX1 != mY1 || mX2 != mY2) CalcSampleValues(); +} + +double SMILKeySpline::GetSplineValue(double aX) const { + if (mX1 == mY1 && mX2 == mY2) return aX; + + return CalcBezier(GetTForX(aX), mY1, mY2); +} + +void SMILKeySpline::GetSplineDerivativeValues(double aX, double& aDX, + double& aDY) const { + double t = GetTForX(aX); + aDX = GetSlope(t, mX1, mX2); + aDY = GetSlope(t, mY1, mY2); +} + +void SMILKeySpline::CalcSampleValues() { + for (uint32_t i = 0; i < kSplineTableSize; ++i) { + mSampleValues[i] = CalcBezier(double(i) * kSampleStepSize, mX1, mX2); + } +} + +/*static*/ +double SMILKeySpline::CalcBezier(double aT, double aA1, double aA2) { + // use Horner's scheme to evaluate the Bezier polynomial + return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT; +} + +/*static*/ +double SMILKeySpline::GetSlope(double aT, double aA1, double aA2) { + return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1); +} + +double SMILKeySpline::GetTForX(double aX) const { + // Early return when aX == 1.0 to avoid floating-point inaccuracies. + if (aX == 1.0) { + return 1.0; + } + // Find interval where t lies + double intervalStart = 0.0; + const double* currentSample = &mSampleValues[1]; + const double* const lastSample = &mSampleValues[kSplineTableSize - 1]; + for (; currentSample != lastSample && *currentSample <= aX; ++currentSample) { + intervalStart += kSampleStepSize; + } + --currentSample; // t now lies between *currentSample and *currentSample+1 + + // Interpolate to provide an initial guess for t + double dist = (aX - *currentSample) / (*(currentSample + 1) - *currentSample); + double guessForT = intervalStart + dist * kSampleStepSize; + + // Check the slope to see what strategy to use. If the slope is too small + // Newton-Raphson iteration won't converge on a root so we use bisection + // instead. + double initialSlope = GetSlope(guessForT, mX1, mX2); + if (initialSlope >= NEWTON_MIN_SLOPE) { + return NewtonRaphsonIterate(aX, guessForT); + } + if (initialSlope == 0.0) { + return guessForT; + } + return BinarySubdivide(aX, intervalStart, intervalStart + kSampleStepSize); +} + +double SMILKeySpline::NewtonRaphsonIterate(double aX, double aGuessT) const { + // Refine guess with Newton-Raphson iteration + for (uint32_t i = 0; i < NEWTON_ITERATIONS; ++i) { + // We're trying to find where f(t) = aX, + // so we're actually looking for a root for: CalcBezier(t) - aX + double currentX = CalcBezier(aGuessT, mX1, mX2) - aX; + double currentSlope = GetSlope(aGuessT, mX1, mX2); + + if (currentSlope == 0.0) return aGuessT; + + aGuessT -= currentX / currentSlope; + } + + return aGuessT; +} + +double SMILKeySpline::BinarySubdivide(double aX, double aA, double aB) const { + double currentX; + double currentT; + uint32_t i = 0; + + do { + currentT = aA + (aB - aA) / 2.0; + currentX = CalcBezier(currentT, mX1, mX2) - aX; + + if (currentX > 0.0) { + aB = currentT; + } else { + aA = currentT; + } + } while (fabs(currentX) > SUBDIVISION_PRECISION && + ++i < SUBDIVISION_MAX_ITERATIONS); + + return currentT; +} + +} // namespace mozilla diff --git a/dom/smil/SMILKeySpline.h b/dom/smil/SMILKeySpline.h new file mode 100644 index 0000000000..d5f180e94a --- /dev/null +++ b/dom/smil/SMILKeySpline.h @@ -0,0 +1,107 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILKEYSPLINE_H_ +#define DOM_SMIL_SMILKEYSPLINE_H_ + +#include "mozilla/ArrayUtils.h" +#include "mozilla/PodOperations.h" + +namespace mozilla { + +/** + * Utility class to provide scaling defined in a keySplines element. + */ +class SMILKeySpline { + public: + constexpr SMILKeySpline() : mX1(0), mY1(0), mX2(0), mY2(0) { + /* caller must call Init later */\ + } + + /** + * Creates a new key spline control point description. + * + * aX1, etc. are the x1, y1, x2, y2 cubic Bezier control points as defined + * by SMILANIM 3.2.3. They must each be in the range 0.0 <= x <= 1.0 + */ + SMILKeySpline(double aX1, double aY1, double aX2, double aY2) + : mX1(0), mY1(0), mX2(0), mY2(0) { + Init(aX1, aY1, aX2, aY2); + } + + double X1() const { return mX1; } + double Y1() const { return mY1; } + double X2() const { return mX2; } + double Y2() const { return mY2; } + + void Init(double aX1, double aY1, double aX2, double aY2); + + /** + * Gets the output (y) value for an input (x). + * + * @param aX The input x value. A floating-point number between 0 and + * 1 (inclusive). + */ + double GetSplineValue(double aX) const; + + void GetSplineDerivativeValues(double aX, double& aDX, double& aDY) const; + + bool operator==(const SMILKeySpline& aOther) const { + return mX1 == aOther.mX1 && mY1 == aOther.mY1 && mX2 == aOther.mX2 && + mY2 == aOther.mY2; + } + bool operator!=(const SMILKeySpline& aOther) const { + return !(*this == aOther); + } + int32_t Compare(const SMILKeySpline& aRhs) const { + if (mX1 != aRhs.mX1) return mX1 < aRhs.mX1 ? -1 : 1; + if (mY1 != aRhs.mY1) return mY1 < aRhs.mY1 ? -1 : 1; + if (mX2 != aRhs.mX2) return mX2 < aRhs.mX2 ? -1 : 1; + if (mY2 != aRhs.mY2) return mY2 < aRhs.mY2 ? -1 : 1; + return 0; + } + + private: + void CalcSampleValues(); + + /** + * Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2. + */ + static double CalcBezier(double aT, double aA1, double aA2); + + /** + * Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2. + */ + static double GetSlope(double aT, double aA1, double aA2); + + double GetTForX(double aX) const; + + double NewtonRaphsonIterate(double aX, double aGuessT) const; + + double BinarySubdivide(double aX, double aA, double aB) const; + + static double A(double aA1, double aA2) { + return 1.0 - 3.0 * aA2 + 3.0 * aA1; + } + + static double B(double aA1, double aA2) { return 3.0 * aA2 - 6.0 * aA1; } + + static double C(double aA1) { return 3.0 * aA1; } + + double mX1; + double mY1; + double mX2; + double mY2; + + enum { kSplineTableSize = 11 }; + double mSampleValues[kSplineTableSize] = {}; + + static const double kSampleStepSize; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILKEYSPLINE_H_ diff --git a/dom/smil/SMILMilestone.h b/dom/smil/SMILMilestone.h new file mode 100644 index 0000000000..8b9372428f --- /dev/null +++ b/dom/smil/SMILMilestone.h @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILMILESTONE_H_ +#define DOM_SMIL_SMILMILESTONE_H_ + +#include "mozilla/SMILTypes.h" + +namespace mozilla { + +/* + * A significant moment in an SMILTimedElement's lifetime where a sample is + * required. + * + * Animations register the next milestone in their lifetime with the time + * container that they belong to. When the animation controller goes to run + * a sample it first visits all the animations that have a registered milestone + * in order of their milestone times. This allows interdependencies between + * animations to be correctly resolved and events to fire in the proper order. + * + * A distinction is made between a milestone representing the end of an interval + * and any other milestone. This is because if animation A ends at time t, and + * animation B begins at the same time t (or has some other significant moment + * such as firing a repeat event), SMIL's endpoint-exclusive timing model + * implies that the interval end occurs first. In fact, interval ends can be + * thought of as ending an infinitesimally small time before t. Therefore, + * A should be sampled before B. + * + * Furthermore, this distinction between sampling the end of an interval and + * a regular sample is used within the timing model (specifically in + * SMILTimedElement) to ensure that all intervals ending at time t are sampled + * before any new intervals are entered so that we have a fully up-to-date set + * of instance times available before committing to a new interval. Once an + * interval is entered, the begin time is fixed. + */ +class SMILMilestone { + public: + SMILMilestone(SMILTime aTime, bool aIsEnd) : mTime(aTime), mIsEnd(aIsEnd) {} + + SMILMilestone() : mTime(0), mIsEnd(false) {} + + bool operator==(const SMILMilestone& aOther) const { + return mTime == aOther.mTime && mIsEnd == aOther.mIsEnd; + } + + bool operator!=(const SMILMilestone& aOther) const { + return !(*this == aOther); + } + + bool operator<(const SMILMilestone& aOther) const { + // Earlier times sort first, and for equal times end milestones sort first + return mTime < aOther.mTime || + (mTime == aOther.mTime && mIsEnd && !aOther.mIsEnd); + } + + bool operator<=(const SMILMilestone& aOther) const { + return *this == aOther || *this < aOther; + } + + bool operator>=(const SMILMilestone& aOther) const { + return !(*this < aOther); + } + + SMILTime mTime; // The milestone time. This may be in container time or + // parent container time depending on where it is used. + bool mIsEnd; // true if this milestone corresponds to an interval + // end, false otherwise. +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILMILESTONE_H_ diff --git a/dom/smil/SMILNullType.cpp b/dom/smil/SMILNullType.cpp new file mode 100644 index 0000000000..571f6f3afe --- /dev/null +++ b/dom/smil/SMILNullType.cpp @@ -0,0 +1,57 @@ +/* -*- 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 "SMILNullType.h" + +#include "mozilla/SMILValue.h" +#include "nsDebug.h" + +namespace mozilla { + +/*static*/ +SMILNullType* SMILNullType::Singleton() { + static SMILNullType sSingleton; + return &sSingleton; +} + +nsresult SMILNullType::Assign(SMILValue& aDest, const SMILValue& aSrc) const { + MOZ_ASSERT(aDest.mType == aSrc.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aSrc.mType == this, "Unexpected source type"); + aDest.mU = aSrc.mU; + aDest.mType = Singleton(); + return NS_OK; +} + +bool SMILNullType::IsEqual(const SMILValue& aLeft, + const SMILValue& aRight) const { + MOZ_ASSERT(aLeft.mType == aRight.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aLeft.mType == this, "Unexpected type for SMIL value"); + + return true; // All null-typed values are equivalent. +} + +nsresult SMILNullType::Add(SMILValue& aDest, const SMILValue& aValueToAdd, + uint32_t aCount) const { + MOZ_ASSERT_UNREACHABLE("Adding NULL type"); + return NS_ERROR_FAILURE; +} + +nsresult SMILNullType::ComputeDistance(const SMILValue& aFrom, + const SMILValue& aTo, + double& aDistance) const { + MOZ_ASSERT_UNREACHABLE("Computing distance for NULL type"); + return NS_ERROR_FAILURE; +} + +nsresult SMILNullType::Interpolate(const SMILValue& aStartVal, + const SMILValue& aEndVal, + double aUnitDistance, + SMILValue& aResult) const { + MOZ_ASSERT_UNREACHABLE("Interpolating NULL type"); + return NS_ERROR_FAILURE; +} + +} // namespace mozilla diff --git a/dom/smil/SMILNullType.h b/dom/smil/SMILNullType.h new file mode 100644 index 0000000000..83f69b2478 --- /dev/null +++ b/dom/smil/SMILNullType.h @@ -0,0 +1,44 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILNULLTYPE_H_ +#define DOM_SMIL_SMILNULLTYPE_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/SMILType.h" + +namespace mozilla { + +class SMILNullType : public SMILType { + public: + // Singleton for SMILValue objects to hold onto. + static SMILNullType* Singleton(); + + protected: + // SMILType Methods + // ------------------- + void Init(SMILValue& aValue) const override {} + void Destroy(SMILValue& aValue) const override {} + nsresult Assign(SMILValue& aDest, const SMILValue& aSrc) const override; + + // The remaining methods should never be called, so although they're very + // simple they don't need to be inline. + bool IsEqual(const SMILValue& aLeft, const SMILValue& aRight) const override; + nsresult Add(SMILValue& aDest, const SMILValue& aValueToAdd, + uint32_t aCount) const override; + nsresult ComputeDistance(const SMILValue& aFrom, const SMILValue& aTo, + double& aDistance) const override; + nsresult Interpolate(const SMILValue& aStartVal, const SMILValue& aEndVal, + double aUnitDistance, SMILValue& aResult) const override; + + private: + // Private constructor: prevent instances beyond my singleton. + constexpr SMILNullType() = default; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILNULLTYPE_H_ diff --git a/dom/smil/SMILParserUtils.cpp b/dom/smil/SMILParserUtils.cpp new file mode 100644 index 0000000000..948192cc82 --- /dev/null +++ b/dom/smil/SMILParserUtils.cpp @@ -0,0 +1,632 @@ +/* -*- 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 "SMILParserUtils.h" + +#include "mozilla/SMILAttr.h" +#include "mozilla/SMILKeySpline.h" +#include "mozilla/SMILRepeatCount.h" +#include "mozilla/SMILTimeValueSpecParams.h" +#include "mozilla/SMILTypes.h" +#include "mozilla/SMILValue.h" +#include "mozilla/SVGContentUtils.h" +#include "mozilla/TextUtils.h" +#include "nsContentUtils.h" +#include "nsCharSeparatedTokenizer.h" + +using namespace mozilla::dom; +//------------------------------------------------------------------------------ +// Helper functions and Constants + +namespace { + +using namespace mozilla; + +const uint32_t MSEC_PER_SEC = 1000; +const uint32_t MSEC_PER_MIN = 1000 * 60; +const uint32_t MSEC_PER_HOUR = 1000 * 60 * 60; + +#define ACCESSKEY_PREFIX_LC u"accesskey("_ns // SMIL2+ +#define ACCESSKEY_PREFIX_CC u"accessKey("_ns // SVG/SMIL ANIM +#define REPEAT_PREFIX u"repeat("_ns +#define WALLCLOCK_PREFIX u"wallclock("_ns + +inline bool SkipWhitespace(RangedPtr& aIter, + const RangedPtr& aEnd) { + while (aIter != aEnd) { + if (!nsContentUtils::IsHTMLWhitespace(*aIter)) { + return true; + } + ++aIter; + } + return false; +} + +inline bool ParseColon(RangedPtr& aIter, + const RangedPtr& aEnd) { + if (aIter == aEnd || *aIter != ':') { + return false; + } + ++aIter; + return true; +} + +/* + * Exactly two digits in the range 00 - 59 are expected. + */ +bool ParseSecondsOrMinutes(RangedPtr& aIter, + const RangedPtr& aEnd, + uint32_t& aValue) { + if (aIter == aEnd || !mozilla::IsAsciiDigit(*aIter)) { + return false; + } + + RangedPtr iter(aIter); + + if (++iter == aEnd || !mozilla::IsAsciiDigit(*iter)) { + return false; + } + + uint32_t value = 10 * mozilla::AsciiAlphanumericToNumber(*aIter) + + mozilla::AsciiAlphanumericToNumber(*iter); + if (value > 59) { + return false; + } + if (++iter != aEnd && mozilla::IsAsciiDigit(*iter)) { + return false; + } + + aValue = value; + aIter = iter; + return true; +} + +inline bool ParseClockMetric(RangedPtr& aIter, + const RangedPtr& aEnd, + uint32_t& aMultiplier) { + if (aIter == aEnd) { + aMultiplier = MSEC_PER_SEC; + return true; + } + + switch (*aIter) { + case 'h': + if (++aIter == aEnd) { + aMultiplier = MSEC_PER_HOUR; + return true; + } + return false; + case 'm': { + const nsAString& metric = Substring(aIter.get(), aEnd.get()); + if (metric.EqualsLiteral("min")) { + aMultiplier = MSEC_PER_MIN; + aIter = aEnd; + return true; + } + if (metric.EqualsLiteral("ms")) { + aMultiplier = 1; + aIter = aEnd; + return true; + } + } + return false; + case 's': + if (++aIter == aEnd) { + aMultiplier = MSEC_PER_SEC; + return true; + } + } + return false; +} + +/** + * See http://www.w3.org/TR/SVG/animate.html#ClockValueSyntax + */ +bool ParseClockValue(RangedPtr& aIter, + const RangedPtr& aEnd, + SMILTimeValue::Rounding aRounding, + SMILTimeValue* aResult) { + if (aIter == aEnd) { + return false; + } + + // TIMECOUNT_VALUE ::= Timecount ("." Fraction)? (Metric)? + // PARTIAL_CLOCK_VALUE ::= Minutes ":" Seconds ("." Fraction)? + // FULL_CLOCK_VALUE ::= Hours ":" Minutes ":" Seconds ("." Fraction)? + enum ClockType { TIMECOUNT_VALUE, PARTIAL_CLOCK_VALUE, FULL_CLOCK_VALUE }; + + int32_t clockType = TIMECOUNT_VALUE; + + RangedPtr iter(aIter); + + // Determine which type of clock value we have by counting the number + // of colons in the string. + do { + switch (*iter) { + case ':': + if (clockType == FULL_CLOCK_VALUE) { + return false; + } + ++clockType; + break; + case 'e': + case 'E': + case '-': + case '+': + // Exclude anything invalid (for clock values) + // that number parsing might otherwise allow. + return false; + } + ++iter; + } while (iter != aEnd); + + iter = aIter; + + int32_t hours = 0, timecount = 0; + double fraction = 0.0; + uint32_t minutes, seconds, multiplier; + + switch (clockType) { + case FULL_CLOCK_VALUE: + if (!SVGContentUtils::ParseInteger(iter, aEnd, hours) || + !ParseColon(iter, aEnd)) { + return false; + } + [[fallthrough]]; + case PARTIAL_CLOCK_VALUE: + if (!ParseSecondsOrMinutes(iter, aEnd, minutes) || + !ParseColon(iter, aEnd) || + !ParseSecondsOrMinutes(iter, aEnd, seconds)) { + return false; + } + if (iter != aEnd && (*iter != '.' || !SVGContentUtils::ParseNumber( + iter, aEnd, fraction))) { + return false; + } + aResult->SetMillis(SMILTime(hours) * MSEC_PER_HOUR + + minutes * MSEC_PER_MIN + + (seconds + fraction) * MSEC_PER_SEC, + aRounding); + aIter = iter; + return true; + case TIMECOUNT_VALUE: + if (*iter != '.' && + !SVGContentUtils::ParseInteger(iter, aEnd, timecount)) { + return false; + } + if (iter != aEnd && *iter == '.' && + !SVGContentUtils::ParseNumber(iter, aEnd, fraction)) { + return false; + } + if (!ParseClockMetric(iter, aEnd, multiplier)) { + return false; + } + aResult->SetMillis((timecount + fraction) * multiplier, aRounding); + aIter = iter; + return true; + } + + return false; +} + +bool ParseOffsetValue(RangedPtr& aIter, + const RangedPtr& aEnd, + SMILTimeValue* aResult) { + RangedPtr iter(aIter); + + int32_t sign; + if (!SVGContentUtils::ParseOptionalSign(iter, aEnd, sign) || + !SkipWhitespace(iter, aEnd) || + !ParseClockValue(iter, aEnd, SMILTimeValue::Rounding::Nearest, aResult)) { + return false; + } + if (sign == -1) { + aResult->SetMillis(-aResult->GetMillis()); + } + aIter = iter; + return true; +} + +bool ParseOffsetValue(const nsAString& aSpec, SMILTimeValue* aResult) { + RangedPtr iter(SVGContentUtils::GetStartRangedPtr(aSpec)); + const RangedPtr end(SVGContentUtils::GetEndRangedPtr(aSpec)); + + return ParseOffsetValue(iter, end, aResult) && iter == end; +} + +bool ParseOptionalOffset(RangedPtr& aIter, + const RangedPtr& aEnd, + SMILTimeValue* aResult) { + if (aIter == aEnd) { + *aResult = SMILTimeValue::Zero(); + return true; + } + + return SkipWhitespace(aIter, aEnd) && ParseOffsetValue(aIter, aEnd, aResult); +} + +void MoveToNextToken(RangedPtr& aIter, + const RangedPtr& aEnd, bool aBreakOnDot, + bool& aIsAnyCharEscaped) { + aIsAnyCharEscaped = false; + + bool isCurrentCharEscaped = false; + + while (aIter != aEnd && !nsContentUtils::IsHTMLWhitespace(*aIter)) { + if (isCurrentCharEscaped) { + isCurrentCharEscaped = false; + } else { + if (*aIter == '+' || *aIter == '-' || (aBreakOnDot && *aIter == '.')) { + break; + } + if (*aIter == '\\') { + isCurrentCharEscaped = true; + aIsAnyCharEscaped = true; + } + } + ++aIter; + } +} + +already_AddRefed ConvertUnescapedTokenToAtom(const nsAString& aToken) { + // Whether the token is an id-ref or event-symbol it should be a valid NCName + if (aToken.IsEmpty() || NS_FAILED(nsContentUtils::CheckQName(aToken, false))) + return nullptr; + return NS_Atomize(aToken); +} + +already_AddRefed ConvertTokenToAtom(const nsAString& aToken, + bool aUnescapeToken) { + // Unescaping involves making a copy of the string which we'd like to avoid if + // possible + if (!aUnescapeToken) { + return ConvertUnescapedTokenToAtom(aToken); + } + + nsAutoString token(aToken); + + const char16_t* read = token.BeginReading(); + const char16_t* const end = token.EndReading(); + char16_t* write = token.BeginWriting(); + bool escape = false; + + while (read != end) { + MOZ_ASSERT(write <= read, "Writing past where we've read"); + if (!escape && *read == '\\') { + escape = true; + ++read; + } else { + *write++ = *read++; + escape = false; + } + } + token.Truncate(write - token.BeginReading()); + + return ConvertUnescapedTokenToAtom(token); +} + +bool ParseElementBaseTimeValueSpec(const nsAString& aSpec, + SMILTimeValueSpecParams& aResult) { + SMILTimeValueSpecParams result; + + // + // The spec will probably look something like one of these + // + // element-name.begin + // element-name.event-name + // event-name + // element-name.repeat(3) + // event\.name + // + // Technically `repeat(3)' is permitted but the behaviour in this case is not + // defined (for SMIL Animation) so we don't support it here. + // + + RangedPtr start(SVGContentUtils::GetStartRangedPtr(aSpec)); + RangedPtr end(SVGContentUtils::GetEndRangedPtr(aSpec)); + + if (start == end) { + return false; + } + + RangedPtr tokenEnd(start); + + bool requiresUnescaping; + MoveToNextToken(tokenEnd, end, true, requiresUnescaping); + + RefPtr atom = ConvertTokenToAtom( + Substring(start.get(), tokenEnd.get()), requiresUnescaping); + if (atom == nullptr) { + return false; + } + + // Parse the second token if there is one + if (tokenEnd != end && *tokenEnd == '.') { + result.mDependentElemID = atom; + + ++tokenEnd; + start = tokenEnd; + MoveToNextToken(tokenEnd, end, false, requiresUnescaping); + + const nsAString& token2 = Substring(start.get(), tokenEnd.get()); + + // element-name.begin + if (token2.EqualsLiteral("begin")) { + result.mType = SMILTimeValueSpecParams::SYNCBASE; + result.mSyncBegin = true; + // element-name.end + } else if (token2.EqualsLiteral("end")) { + result.mType = SMILTimeValueSpecParams::SYNCBASE; + result.mSyncBegin = false; + // element-name.repeat(digit+) + } else if (StringBeginsWith(token2, REPEAT_PREFIX)) { + start += REPEAT_PREFIX.Length(); + int32_t repeatValue; + if (start == tokenEnd || *start == '+' || *start == '-' || + !SVGContentUtils::ParseInteger(start, tokenEnd, repeatValue)) { + return false; + } + if (start == tokenEnd || *start != ')') { + return false; + } + result.mType = SMILTimeValueSpecParams::REPEAT; + result.mRepeatIteration = repeatValue; + // element-name.event-symbol + } else { + atom = ConvertTokenToAtom(token2, requiresUnescaping); + if (atom == nullptr) { + return false; + } + result.mType = SMILTimeValueSpecParams::EVENT; + result.mEventSymbol = atom; + } + } else { + // event-symbol + result.mType = SMILTimeValueSpecParams::EVENT; + result.mEventSymbol = atom; + } + + // We've reached the end of the token, so we should now be either looking at + // a '+', '-' (possibly with whitespace before it), or the end. + if (!ParseOptionalOffset(tokenEnd, end, &result.mOffset) || tokenEnd != end) { + return false; + } + aResult = result; + return true; +} + +} // namespace + +namespace mozilla { + +//------------------------------------------------------------------------------ +// Implementation + +const nsDependentSubstring SMILParserUtils::TrimWhitespace( + const nsAString& aString) { + nsAString::const_iterator start, end; + + aString.BeginReading(start); + aString.EndReading(end); + + // Skip whitespace characters at the beginning + while (start != end && nsContentUtils::IsHTMLWhitespace(*start)) { + ++start; + } + + // Skip whitespace characters at the end. + while (end != start) { + --end; + + if (!nsContentUtils::IsHTMLWhitespace(*end)) { + // Step back to the last non-whitespace character. + ++end; + + break; + } + } + + return Substring(start, end); +} + +bool SMILParserUtils::ParseKeySplines( + const nsAString& aSpec, FallibleTArray& aKeySplines) { + for (const auto& controlPoint : + nsCharSeparatedTokenizerTemplate(aSpec, + ';') + .ToRange()) { + nsCharSeparatedTokenizerTemplate + tokenizer(controlPoint, ','); + + double values[4]; + for (auto& value : values) { + if (!tokenizer.hasMoreTokens() || + !SVGContentUtils::ParseNumber(tokenizer.nextToken(), value) || + value > 1.0 || value < 0.0) { + return false; + } + } + if (tokenizer.hasMoreTokens() || tokenizer.separatorAfterCurrentToken() || + !aKeySplines.AppendElement( + SMILKeySpline(values[0], values[1], values[2], values[3]), + fallible)) { + return false; + } + } + + return !aKeySplines.IsEmpty(); +} + +bool SMILParserUtils::ParseSemicolonDelimitedProgressList( + const nsAString& aSpec, bool aNonDecreasing, + FallibleTArray& aArray) { + nsCharSeparatedTokenizerTemplate tokenizer( + aSpec, ';'); + + double previousValue = -1.0; + + while (tokenizer.hasMoreTokens()) { + double value; + if (!SVGContentUtils::ParseNumber(tokenizer.nextToken(), value)) { + return false; + } + + if (value > 1.0 || value < 0.0 || + (aNonDecreasing && value < previousValue)) { + return false; + } + + if (!aArray.AppendElement(value, fallible)) { + return false; + } + previousValue = value; + } + + return !aArray.IsEmpty(); +} + +// Helper class for ParseValues +class MOZ_STACK_CLASS SMILValueParser + : public SMILParserUtils::GenericValueParser { + public: + SMILValueParser(const SVGAnimationElement* aSrcElement, + const SMILAttr* aSMILAttr, + FallibleTArray* aValuesArray, + bool* aPreventCachingOfSandwich) + : mSrcElement(aSrcElement), + mSMILAttr(aSMILAttr), + mValuesArray(aValuesArray), + mPreventCachingOfSandwich(aPreventCachingOfSandwich) {} + + bool Parse(const nsAString& aValueStr) override { + SMILValue newValue; + if (NS_FAILED(mSMILAttr->ValueFromString(aValueStr, mSrcElement, newValue, + *mPreventCachingOfSandwich))) + return false; + + if (!mValuesArray->AppendElement(newValue, fallible)) { + return false; + } + return true; + } + + protected: + const SVGAnimationElement* mSrcElement; + const SMILAttr* mSMILAttr; + FallibleTArray* mValuesArray; + bool* mPreventCachingOfSandwich; +}; + +bool SMILParserUtils::ParseValues(const nsAString& aSpec, + const SVGAnimationElement* aSrcElement, + const SMILAttr& aAttribute, + FallibleTArray& aValuesArray, + bool& aPreventCachingOfSandwich) { + // Assume all results can be cached, until we find one that can't. + aPreventCachingOfSandwich = false; + SMILValueParser valueParser(aSrcElement, &aAttribute, &aValuesArray, + &aPreventCachingOfSandwich); + return ParseValuesGeneric(aSpec, valueParser); +} + +bool SMILParserUtils::ParseValuesGeneric(const nsAString& aSpec, + GenericValueParser& aParser) { + nsCharSeparatedTokenizerTemplate tokenizer( + aSpec, ';'); + if (!tokenizer.hasMoreTokens()) { // Empty list + return false; + } + + while (tokenizer.hasMoreTokens()) { + if (!aParser.Parse(tokenizer.nextToken())) { + return false; + } + } + + return true; +} + +bool SMILParserUtils::ParseRepeatCount(const nsAString& aSpec, + SMILRepeatCount& aResult) { + const nsAString& spec = SMILParserUtils::TrimWhitespace(aSpec); + + if (spec.EqualsLiteral("indefinite")) { + aResult.SetIndefinite(); + return true; + } + + double value; + if (!SVGContentUtils::ParseNumber(spec, value) || value <= 0.0) { + return false; + } + aResult = value; + return true; +} + +bool SMILParserUtils::ParseTimeValueSpecParams( + const nsAString& aSpec, SMILTimeValueSpecParams& aResult) { + const nsAString& spec = TrimWhitespace(aSpec); + + if (spec.EqualsLiteral("indefinite")) { + aResult.mType = SMILTimeValueSpecParams::INDEFINITE; + return true; + } + + // offset type + if (ParseOffsetValue(spec, &aResult.mOffset)) { + aResult.mType = SMILTimeValueSpecParams::OFFSET; + return true; + } + + // wallclock type + if (StringBeginsWith(spec, WALLCLOCK_PREFIX)) { + return false; // Wallclock times not implemented + } + + // accesskey type + if (StringBeginsWith(spec, ACCESSKEY_PREFIX_LC) || + StringBeginsWith(spec, ACCESSKEY_PREFIX_CC)) { + return false; // accesskey is not supported + } + + // event, syncbase, or repeat + return ParseElementBaseTimeValueSpec(spec, aResult); +} + +bool SMILParserUtils::ParseClockValue(const nsAString& aSpec, + SMILTimeValue::Rounding aRounding, + SMILTimeValue* aResult) { + RangedPtr iter(SVGContentUtils::GetStartRangedPtr(aSpec)); + RangedPtr end(SVGContentUtils::GetEndRangedPtr(aSpec)); + + return ::ParseClockValue(iter, end, aRounding, aResult) && iter == end; +} + +int32_t SMILParserUtils::CheckForNegativeNumber(const nsAString& aStr) { + int32_t absValLocation = -1; + + RangedPtr start(SVGContentUtils::GetStartRangedPtr(aStr)); + RangedPtr iter = start; + RangedPtr end(SVGContentUtils::GetEndRangedPtr(aStr)); + + // Skip initial whitespace + while (iter != end && nsContentUtils::IsHTMLWhitespace(*iter)) { + ++iter; + } + + // Check for dash + if (iter != end && *iter == '-') { + ++iter; + // Check for numeric character + if (iter != end && mozilla::IsAsciiDigit(*iter)) { + absValLocation = iter - start; + } + } + return absValLocation; +} + +} // namespace mozilla diff --git a/dom/smil/SMILParserUtils.h b/dom/smil/SMILParserUtils.h new file mode 100644 index 0000000000..f1ac4ba51d --- /dev/null +++ b/dom/smil/SMILParserUtils.h @@ -0,0 +1,91 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILPARSERUTILS_H_ +#define DOM_SMIL_SMILPARSERUTILS_H_ + +#include "nsTArray.h" +#include "nsStringFwd.h" +#include "SMILTimeValue.h" + +namespace mozilla { + +class SMILAttr; +class SMILKeySpline; +class SMILRepeatCount; +class SMILTimeValueSpecParams; +class SMILValue; + +namespace dom { +class SVGAnimationElement; +} // namespace dom + +/** + * Common parsing utilities for the SMIL module. There is little re-use here; it + * simply serves to simplify other classes by moving parsing outside and to aid + * unit testing. + */ +class SMILParserUtils { + public: + // Abstract helper-class for assisting in parsing |values| attribute + class MOZ_STACK_CLASS GenericValueParser { + public: + virtual bool Parse(const nsAString& aValueStr) = 0; + }; + + static const nsDependentSubstring TrimWhitespace(const nsAString& aString); + + static bool ParseKeySplines(const nsAString& aSpec, + FallibleTArray& aKeySplines); + + // Used for parsing the |keyTimes| and |keyPoints| attributes. + static bool ParseSemicolonDelimitedProgressList( + const nsAString& aSpec, bool aNonDecreasing, + FallibleTArray& aArray); + + static bool ParseValues(const nsAString& aSpec, + const mozilla::dom::SVGAnimationElement* aSrcElement, + const SMILAttr& aAttribute, + FallibleTArray& aValuesArray, + bool& aPreventCachingOfSandwich); + + // Generic method that will run some code on each sub-section of an animation + // element's "values" list. + static bool ParseValuesGeneric(const nsAString& aSpec, + GenericValueParser& aParser); + + static bool ParseRepeatCount(const nsAString& aSpec, + SMILRepeatCount& aResult); + + static bool ParseTimeValueSpecParams(const nsAString& aSpec, + SMILTimeValueSpecParams& aResult); + + /* + * Parses a clock value as defined in the SMIL Animation specification. + * If parsing succeeds the returned value will be a non-negative, definite + * time value i.e. IsDefinite will return true. + * + * @param aSpec The string containing a clock value, e.g. "10s" + * @param aResult The parsed result. [OUT] + * @return true if parsing succeeded, otherwise false. + */ + static bool ParseClockValue(const nsAString& aSpec, + SMILTimeValue::Rounding aRounding, + SMILTimeValue* aResult); + + /* + * This method checks whether the given string looks like a negative number. + * Specifically, it checks whether the string looks matches the pattern + * "[whitespace]*-[numeral].*" If the string matches this pattern, this + * method returns the index of the first character after the '-' sign + * (i.e. the index of the absolute value). If not, this method returns -1. + */ + static int32_t CheckForNegativeNumber(const nsAString& aStr); +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILPARSERUTILS_H_ diff --git a/dom/smil/SMILRepeatCount.cpp b/dom/smil/SMILRepeatCount.cpp new file mode 100644 index 0000000000..9a56a42d45 --- /dev/null +++ b/dom/smil/SMILRepeatCount.cpp @@ -0,0 +1,14 @@ +/* -*- 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 "SMILRepeatCount.h" + +namespace mozilla { + +/*static*/ const double SMILRepeatCount::kNotSet = -1.0; +/*static*/ const double SMILRepeatCount::kIndefinite = -2.0; + +} // namespace mozilla diff --git a/dom/smil/SMILRepeatCount.h b/dom/smil/SMILRepeatCount.h new file mode 100644 index 0000000000..0b93aae994 --- /dev/null +++ b/dom/smil/SMILRepeatCount.h @@ -0,0 +1,62 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILREPEATCOUNT_H_ +#define DOM_SMIL_SMILREPEATCOUNT_H_ + +#include "nsDebug.h" +#include + +namespace mozilla { + +//---------------------------------------------------------------------- +// SMILRepeatCount +// +// A tri-state non-negative floating point number for representing the number of +// times an animation repeat, i.e. the SMIL repeatCount attribute. +// +// The three states are: +// 1. not-set +// 2. set (with non-negative, non-zero count value) +// 3. indefinite +// +class SMILRepeatCount { + public: + SMILRepeatCount() : mCount(kNotSet) {} + explicit SMILRepeatCount(double aCount) : mCount(kNotSet) { + SetCount(aCount); + } + + operator double() const { + MOZ_ASSERT(IsDefinite(), + "Converting indefinite or unset repeat count to double"); + return mCount; + } + bool IsDefinite() const { return mCount != kNotSet && mCount != kIndefinite; } + bool IsIndefinite() const { return mCount == kIndefinite; } + bool IsSet() const { return mCount != kNotSet; } + + SMILRepeatCount& operator=(double aCount) { + SetCount(aCount); + return *this; + } + void SetCount(double aCount) { + NS_ASSERTION(aCount > 0.0, "Negative or zero repeat count"); + mCount = aCount > 0.0 ? aCount : kNotSet; + } + void SetIndefinite() { mCount = kIndefinite; } + void Unset() { mCount = kNotSet; } + + private: + static const double kNotSet; + static const double kIndefinite; + + double mCount; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILREPEATCOUNT_H_ diff --git a/dom/smil/SMILSetAnimationFunction.cpp b/dom/smil/SMILSetAnimationFunction.cpp new file mode 100644 index 0000000000..0ef19a2faa --- /dev/null +++ b/dom/smil/SMILSetAnimationFunction.cpp @@ -0,0 +1,26 @@ +/* -*- 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 "SMILSetAnimationFunction.h" + +namespace mozilla { + +bool SMILSetAnimationFunction::IsDisallowedAttribute( + const nsAtom* aAttribute) const { + // + // A element is similar to but lacks: + // AnimationValue.attrib(calcMode, values, keyTimes, keySplines, from, to, + // by) -- BUT has 'to' + // AnimationAddition.attrib(additive, accumulate) + // + return aAttribute == nsGkAtoms::calcMode || aAttribute == nsGkAtoms::values || + aAttribute == nsGkAtoms::keyTimes || + aAttribute == nsGkAtoms::keySplines || aAttribute == nsGkAtoms::from || + aAttribute == nsGkAtoms::by || aAttribute == nsGkAtoms::additive || + aAttribute == nsGkAtoms::accumulate; +} + +} // namespace mozilla diff --git a/dom/smil/SMILSetAnimationFunction.h b/dom/smil/SMILSetAnimationFunction.h new file mode 100644 index 0000000000..79e94d1e47 --- /dev/null +++ b/dom/smil/SMILSetAnimationFunction.h @@ -0,0 +1,38 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILSETANIMATIONFUNCTION_H_ +#define DOM_SMIL_SMILSETANIMATIONFUNCTION_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/SMILAnimationFunction.h" + +namespace mozilla { + +//---------------------------------------------------------------------- +// SMILSetAnimationFunction +// +// Subclass of SMILAnimationFunction that limits the behaviour to that offered +// by a element. +// +class SMILSetAnimationFunction : public SMILAnimationFunction { + protected: + bool IsDisallowedAttribute(const nsAtom* aAttribute) const override; + + // Although animation might look like to-animation, unlike to-animation, + // it never interpolates values. + // Returning false here will mean this animation function gets treated as + // a single-valued function and no interpolation will be attempted. + bool IsToAnimation() const override { return false; } + + // applies the exact same value across the simple duration. + bool IsValueFixedForSimpleDuration() const override { return true; } + bool WillReplace() const override { return true; } +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILSETANIMATIONFUNCTION_H_ diff --git a/dom/smil/SMILStringType.cpp b/dom/smil/SMILStringType.cpp new file mode 100644 index 0000000000..a79a97eb2c --- /dev/null +++ b/dom/smil/SMILStringType.cpp @@ -0,0 +1,75 @@ +/* -*- 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 "SMILStringType.h" + +#include "mozilla/SMILValue.h" +#include "nsDebug.h" +#include "nsString.h" + +namespace mozilla { + +void SMILStringType::Init(SMILValue& aValue) const { + MOZ_ASSERT(aValue.IsNull(), "Unexpected value type"); + aValue.mU.mPtr = new nsString(); + aValue.mType = this; +} + +void SMILStringType::Destroy(SMILValue& aValue) const { + MOZ_ASSERT(aValue.mType == this, "Unexpected SMIL value"); + delete static_cast(aValue.mU.mPtr); + aValue.mU.mPtr = nullptr; + aValue.mType = SMILNullType::Singleton(); +} + +nsresult SMILStringType::Assign(SMILValue& aDest, const SMILValue& aSrc) const { + MOZ_ASSERT(aDest.mType == aSrc.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aDest.mType == this, "Unexpected SMIL value"); + + const nsAString* src = static_cast(aSrc.mU.mPtr); + nsAString* dst = static_cast(aDest.mU.mPtr); + *dst = *src; + return NS_OK; +} + +bool SMILStringType::IsEqual(const SMILValue& aLeft, + const SMILValue& aRight) const { + MOZ_ASSERT(aLeft.mType == aRight.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aLeft.mType == this, "Unexpected type for SMIL value"); + + const nsAString* leftString = static_cast(aLeft.mU.mPtr); + const nsAString* rightString = static_cast(aRight.mU.mPtr); + return *leftString == *rightString; +} + +nsresult SMILStringType::Add(SMILValue& aDest, const SMILValue& aValueToAdd, + uint32_t aCount) const { + MOZ_ASSERT(aValueToAdd.mType == aDest.mType, "Trying to add invalid types"); + MOZ_ASSERT(aValueToAdd.mType == this, "Unexpected source type"); + return NS_ERROR_FAILURE; // string values can't be added to each other +} + +nsresult SMILStringType::ComputeDistance(const SMILValue& aFrom, + const SMILValue& aTo, + double& aDistance) const { + MOZ_ASSERT(aFrom.mType == aTo.mType, "Trying to compare different types"); + MOZ_ASSERT(aFrom.mType == this, "Unexpected source type"); + return NS_ERROR_FAILURE; // there is no concept of distance between string + // values +} + +nsresult SMILStringType::Interpolate(const SMILValue& aStartVal, + const SMILValue& aEndVal, + double aUnitDistance, + SMILValue& aResult) const { + MOZ_ASSERT(aStartVal.mType == aEndVal.mType, + "Trying to interpolate different types"); + MOZ_ASSERT(aStartVal.mType == this, "Unexpected types for interpolation"); + MOZ_ASSERT(aResult.mType == this, "Unexpected result type"); + return NS_ERROR_FAILURE; // string values do not interpolate +} + +} // namespace mozilla diff --git a/dom/smil/SMILStringType.h b/dom/smil/SMILStringType.h new file mode 100644 index 0000000000..7d33d6dade --- /dev/null +++ b/dom/smil/SMILStringType.h @@ -0,0 +1,44 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILSTRINGTYPE_H_ +#define DOM_SMIL_SMILSTRINGTYPE_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/SMILType.h" + +namespace mozilla { + +class SMILStringType : public SMILType { + public: + // Singleton for SMILValue objects to hold onto. + static SMILStringType* Singleton() { + static SMILStringType sSingleton; + return &sSingleton; + } + + protected: + // SMILType Methods + // ------------------- + void Init(SMILValue& aValue) const override; + void Destroy(SMILValue& aValue) const override; + nsresult Assign(SMILValue& aDest, const SMILValue& aSrc) const override; + bool IsEqual(const SMILValue& aLeft, const SMILValue& aRight) const override; + nsresult Add(SMILValue& aDest, const SMILValue& aValueToAdd, + uint32_t aCount) const override; + nsresult ComputeDistance(const SMILValue& aFrom, const SMILValue& aTo, + double& aDistance) const override; + nsresult Interpolate(const SMILValue& aStartVal, const SMILValue& aEndVal, + double aUnitDistance, SMILValue& aResult) const override; + + private: + // Private constructor: prevent instances beyond my singleton. + constexpr SMILStringType() = default; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILSTRINGTYPE_H_ diff --git a/dom/smil/SMILTargetIdentifier.h b/dom/smil/SMILTargetIdentifier.h new file mode 100644 index 0000000000..14a2ee3152 --- /dev/null +++ b/dom/smil/SMILTargetIdentifier.h @@ -0,0 +1,87 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILTARGETIDENTIFIER_H_ +#define DOM_SMIL_SMILTARGETIDENTIFIER_H_ + +// XXX Avoid including this here by moving function bodies to the cpp file +#include "nsAtom.h" +#include "nsIContent.h" +#include "mozilla/dom/Element.h" + +class nsIContent; + +namespace mozilla { +namespace dom { +class Element; +} + +/** + * Struct: SMILTargetIdentifier + * + * Tuple of: { Animated Element, Attribute Name } + * + * Used in SMILAnimationController as hash key for mapping an animation + * target to the SMILCompositor for that target. + * + * NOTE: Need a nsRefPtr for the element & attribute name, because + * SMILAnimationController retain its hash table for one sample into the + * future, and we need to make sure their target isn't deleted in that time. + */ + +struct SMILTargetIdentifier { + SMILTargetIdentifier() + : mElement(nullptr), + mAttributeName(nullptr), + mAttributeNamespaceID(kNameSpaceID_Unknown) {} + + inline bool Equals(const SMILTargetIdentifier& aOther) const { + return (aOther.mElement == mElement && + aOther.mAttributeName == mAttributeName && + aOther.mAttributeNamespaceID == mAttributeNamespaceID); + } + + RefPtr mElement; + RefPtr mAttributeName; + int32_t mAttributeNamespaceID; +}; + +/** + * Class: SMILWeakTargetIdentifier + * + * Version of the above struct that uses non-owning pointers. These are kept + * private, to ensure that they aren't ever dereferenced (or used at all, + * outside of Equals()). + * + * This is solely for comparisons to determine if a target has changed + * from one sample to the next. + */ +class SMILWeakTargetIdentifier { + public: + // Trivial constructor + SMILWeakTargetIdentifier() : mElement(nullptr), mAttributeName(nullptr) {} + + // Allow us to update a weak identifier to match a given non-weak identifier + SMILWeakTargetIdentifier& operator=(const SMILTargetIdentifier& aOther) { + mElement = aOther.mElement; + mAttributeName = aOther.mAttributeName; + return *this; + } + + // Allow for comparison vs. non-weak identifier + inline bool Equals(const SMILTargetIdentifier& aOther) const { + return (aOther.mElement == mElement && + aOther.mAttributeName == mAttributeName); + } + + private: + const nsIContent* mElement; + const nsAtom* mAttributeName; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILTARGETIDENTIFIER_H_ diff --git a/dom/smil/SMILTimeContainer.cpp b/dom/smil/SMILTimeContainer.cpp new file mode 100644 index 0000000000..321c27ed2c --- /dev/null +++ b/dom/smil/SMILTimeContainer.cpp @@ -0,0 +1,306 @@ +/* -*- 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 "SMILTimeContainer.h" + +#include "mozilla/AutoRestore.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/SMILTimedElement.h" +#include "mozilla/SMILTimeValue.h" +#include + +namespace mozilla { + +SMILTimeContainer::SMILTimeContainer() + : mParent(nullptr), + mCurrentTime(0L), + mParentOffset(0L), + mPauseStart(0L), + mNeedsPauseSample(false), + mNeedsRewind(false), + mIsSeeking(false), +#ifdef DEBUG + mHoldingEntries(false), +#endif + mPauseState(PAUSE_BEGIN) { +} + +SMILTimeContainer::~SMILTimeContainer() { + if (mParent) { + mParent->RemoveChild(*this); + } +} + +SMILTimeValue SMILTimeContainer::ContainerToParentTime( + SMILTime aContainerTime) const { + // If we're paused, then future times are indefinite + if (IsPaused() && aContainerTime > mCurrentTime) + return SMILTimeValue::Indefinite(); + + return SMILTimeValue(aContainerTime + mParentOffset); +} + +SMILTimeValue SMILTimeContainer::ParentToContainerTime( + SMILTime aParentTime) const { + // If we're paused, then any time after when we paused is indefinite + if (IsPaused() && aParentTime > mPauseStart) + return SMILTimeValue::Indefinite(); + + return SMILTimeValue(aParentTime - mParentOffset); +} + +void SMILTimeContainer::Begin() { + Resume(PAUSE_BEGIN); + if (mPauseState) { + mNeedsPauseSample = true; + } + + // This is a little bit complicated here. Ideally we'd just like to call + // Sample() and force an initial sample but this turns out to be a bad idea + // because this may mean that NeedsSample() no longer reports true and so when + // we come to the first real sample our parent will skip us over altogether. + // So we force the time to be updated and adopt the policy to never call + // Sample() ourselves but to always leave that to our parent or client. + + UpdateCurrentTime(); +} + +void SMILTimeContainer::Pause(uint32_t aType) { + bool didStartPause = false; + + if (!mPauseState && aType) { + mPauseStart = GetParentTime(); + mNeedsPauseSample = true; + didStartPause = true; + } + + mPauseState |= aType; + + if (didStartPause) { + NotifyTimeChange(); + } +} + +void SMILTimeContainer::Resume(uint32_t aType) { + if (!mPauseState) return; + + mPauseState &= ~aType; + + if (!mPauseState) { + SMILTime extraOffset = GetParentTime() - mPauseStart; + mParentOffset += extraOffset; + NotifyTimeChange(); + } +} + +SMILTime SMILTimeContainer::GetCurrentTimeAsSMILTime() const { + // The following behaviour is consistent with: + // http://www.w3.org/2003/01/REC-SVG11-20030114-errata + // #getCurrentTime_setCurrentTime_undefined_before_document_timeline_begin + // which says that if GetCurrentTime is called before the document timeline + // has begun we should just return 0. + if (IsPausedByType(PAUSE_BEGIN)) return 0L; + + return mCurrentTime; +} + +void SMILTimeContainer::SetCurrentTime(SMILTime aSeekTo) { + // SVG 1.1 doesn't specify what to do for negative times so we adopt SVGT1.2's + // behaviour of clamping negative times to 0. + aSeekTo = std::max(0, aSeekTo); + + // The following behaviour is consistent with: + // http://www.w3.org/2003/01/REC-SVG11-20030114-errata + // #getCurrentTime_setCurrentTime_undefined_before_document_timeline_begin + // which says that if SetCurrentTime is called before the document timeline + // has begun we should still adjust the offset. + SMILTime parentTime = GetParentTime(); + mParentOffset = parentTime - aSeekTo; + mIsSeeking = true; + + if (IsPaused()) { + mNeedsPauseSample = true; + mPauseStart = parentTime; + } + + if (aSeekTo < mCurrentTime) { + // Backwards seek + mNeedsRewind = true; + ClearMilestones(); + } + + // Force an update to the current time in case we get a call to GetCurrentTime + // before another call to Sample(). + UpdateCurrentTime(); + + NotifyTimeChange(); +} + +SMILTime SMILTimeContainer::GetParentTime() const { + if (mParent) return mParent->GetCurrentTimeAsSMILTime(); + + return 0L; +} + +void SMILTimeContainer::SyncPauseTime() { + if (IsPaused()) { + SMILTime parentTime = GetParentTime(); + SMILTime extraOffset = parentTime - mPauseStart; + mParentOffset += extraOffset; + mPauseStart = parentTime; + } +} + +void SMILTimeContainer::Sample() { + if (!NeedsSample()) return; + + UpdateCurrentTime(); + DoSample(); + + mNeedsPauseSample = false; +} + +nsresult SMILTimeContainer::SetParent(SMILTimeContainer* aParent) { + if (mParent) { + mParent->RemoveChild(*this); + // When we're not attached to a parent time container, GetParentTime() will + // return 0. We need to adjust our pause state information to be relative to + // this new time base. + // Note that since "current time = parent time - parent offset" setting the + // parent offset and pause start as follows preserves our current time even + // while parent time = 0. + mParentOffset = -mCurrentTime; + mPauseStart = 0L; + } + + mParent = aParent; + + nsresult rv = NS_OK; + if (mParent) { + rv = mParent->AddChild(*this); + } + + return rv; +} + +void SMILTimeContainer::AddMilestone( + const SMILMilestone& aMilestone, + mozilla::dom::SVGAnimationElement& aElement) { + // We record the milestone time and store it along with the element but this + // time may change (e.g. if attributes are changed on the timed element in + // between samples). If this happens, then we may do an unecessary sample + // but that's pretty cheap. + MOZ_ASSERT(!mHoldingEntries); + mMilestoneEntries.Push(MilestoneEntry(aMilestone, aElement)); +} + +void SMILTimeContainer::ClearMilestones() { + MOZ_ASSERT(!mHoldingEntries); + mMilestoneEntries.Clear(); +} + +bool SMILTimeContainer::GetNextMilestoneInParentTime( + SMILMilestone& aNextMilestone) const { + if (mMilestoneEntries.IsEmpty()) return false; + + SMILTimeValue parentTime = + ContainerToParentTime(mMilestoneEntries.Top().mMilestone.mTime); + if (!parentTime.IsDefinite()) return false; + + aNextMilestone = SMILMilestone(parentTime.GetMillis(), + mMilestoneEntries.Top().mMilestone.mIsEnd); + + return true; +} + +bool SMILTimeContainer::PopMilestoneElementsAtMilestone( + const SMILMilestone& aMilestone, AnimElemArray& aMatchedElements) { + if (mMilestoneEntries.IsEmpty()) return false; + + SMILTimeValue containerTime = ParentToContainerTime(aMilestone.mTime); + if (!containerTime.IsDefinite()) return false; + + SMILMilestone containerMilestone(containerTime.GetMillis(), + aMilestone.mIsEnd); + + MOZ_ASSERT(mMilestoneEntries.Top().mMilestone >= containerMilestone, + "Trying to pop off earliest times but we have earlier ones that " + "were overlooked"); + + MOZ_ASSERT(!mHoldingEntries); + + bool gotOne = false; + while (!mMilestoneEntries.IsEmpty() && + mMilestoneEntries.Top().mMilestone == containerMilestone) { + aMatchedElements.AppendElement(mMilestoneEntries.Pop().mTimebase); + gotOne = true; + } + + return gotOne; +} + +void SMILTimeContainer::Traverse( + nsCycleCollectionTraversalCallback* aCallback) { +#ifdef DEBUG + AutoRestore saveHolding(mHoldingEntries); + mHoldingEntries = true; +#endif + const MilestoneEntry* p = mMilestoneEntries.Elements(); + while (p < mMilestoneEntries.Elements() + mMilestoneEntries.Length()) { + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(*aCallback, "mTimebase"); + aCallback->NoteXPCOMChild(static_cast(p->mTimebase.get())); + ++p; + } +} + +void SMILTimeContainer::Unlink() { + MOZ_ASSERT(!mHoldingEntries); + mMilestoneEntries.Clear(); +} + +void SMILTimeContainer::UpdateCurrentTime() { + SMILTime now = IsPaused() ? mPauseStart : GetParentTime(); + MOZ_ASSERT(now >= mParentOffset, + "Container has negative time with respect to parent"); + const auto updatedCurrentTime = CheckedInt(now) - mParentOffset; + mCurrentTime = updatedCurrentTime.isValid() + ? updatedCurrentTime.value() + : std::numeric_limits::max(); +} + +void SMILTimeContainer::NotifyTimeChange() { + // Called when the container time is changed with respect to the document + // time. When this happens time dependencies in other time containers need to + // re-resolve their times because begin and end times are stored in container + // time. + // + // To get the list of timed elements with dependencies we simply re-use the + // milestone elements. This is because any timed element with dependents and + // with significant transitions yet to fire should have their next milestone + // registered. Other timed elements don't matter. + + // Copy the timed elements to a separate array before calling + // HandleContainerTimeChange on each of them in case doing so mutates + // mMilestoneEntries. + nsTArray> elems; + + { +#ifdef DEBUG + AutoRestore saveHolding(mHoldingEntries); + mHoldingEntries = true; +#endif + for (const MilestoneEntry* p = mMilestoneEntries.Elements(); + p < mMilestoneEntries.Elements() + mMilestoneEntries.Length(); ++p) { + elems.AppendElement(p->mTimebase.get()); + } + } + + for (auto& elem : elems) { + elem->TimedElement().HandleContainerTimeChange(); + } +} + +} // namespace mozilla diff --git a/dom/smil/SMILTimeContainer.h b/dom/smil/SMILTimeContainer.h new file mode 100644 index 0000000000..62ea7ad2c4 --- /dev/null +++ b/dom/smil/SMILTimeContainer.h @@ -0,0 +1,299 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILTIMECONTAINER_H_ +#define DOM_SMIL_SMILTIMECONTAINER_H_ + +#include "mozilla/dom/SVGAnimationElement.h" +#include "mozilla/SMILMilestone.h" +#include "mozilla/SMILTypes.h" +#include "nscore.h" +#include "nsTPriorityQueue.h" + +namespace mozilla { + +class SMILTimeValue; + +//---------------------------------------------------------------------- +// SMILTimeContainer +// +// Common base class for a time base that can be paused, resumed, and sampled. +// +class SMILTimeContainer { + public: + SMILTimeContainer(); + virtual ~SMILTimeContainer(); + + /* + * Pause request types. + */ + enum { + PAUSE_BEGIN = 1, // Paused because timeline has yet to begin. + PAUSE_SCRIPT = 2, // Paused by script. + PAUSE_PAGEHIDE = 4, // Paused because our doc is hidden. + PAUSE_USERPREF = 8, // Paused because animations are disabled in prefs. + PAUSE_IMAGE = 16 // Paused becuase we're in an image that's suspended. + }; + + /* + * Cause the time container to record its begin time. + */ + void Begin(); + + /* + * Pause this time container + * + * @param aType The source of the pause request. Successive calls to Pause + * with the same aType will be ignored. The container will remain paused until + * each call to Pause of a given aType has been matched by at least one call + * to Resume with the same aType. + */ + virtual void Pause(uint32_t aType); + + /* + * Resume this time container + * + * param @aType The source of the resume request. Clears the pause flag for + * this particular type of pause request. When all pause flags have been + * cleared the time container will be resumed. + */ + virtual void Resume(uint32_t aType); + + /** + * Returns true if this time container is paused by the specified type. + * Note that the time container may also be paused by other types; this method + * does not test if aType is the exclusive pause source. + * + * @param @aType The pause source to test for. + * @return true if this container is paused by aType. + */ + bool IsPausedByType(uint32_t aType) const { return mPauseState & aType; } + + /** + * Returns true if this time container is paused. + * Generally you should test for a specific type of pausing using + * IsPausedByType. + * + * @return true if this container is paused, false otherwise. + */ + bool IsPaused() const { return mPauseState != 0; } + + /* + * Return the time elapsed since this time container's begin time (expressed + * in parent time) minus any accumulated offset from pausing. + */ + SMILTime GetCurrentTimeAsSMILTime() const; + + /* + * Seek the document timeline to the specified time. + * + * @param aSeekTo The time to seek to, expressed in this time container's time + * base (i.e. the same units as GetCurrentTime). + */ + void SetCurrentTime(SMILTime aSeekTo); + + /* + * Return the current time for the parent time container if any. + */ + virtual SMILTime GetParentTime() const; + + /* + * Convert container time to parent time. + * + * @param aContainerTime The container time to convert. + * @return The equivalent parent time or indefinite if the container is + * paused and the time is in the future. + */ + SMILTimeValue ContainerToParentTime(SMILTime aContainerTime) const; + + /* + * Convert from parent time to container time. + * + * @param aParentTime The parent time to convert. + * @return The equivalent container time or indefinite if the container is + * paused and aParentTime is after the time when the pause began. + */ + SMILTimeValue ParentToContainerTime(SMILTime aParentTime) const; + + /* + * If the container is paused, causes the pause time to be updated to the + * current parent time. This should be called before updating + * cross-container dependencies that will call ContainerToParentTime in order + * to provide more intuitive results. + */ + void SyncPauseTime(); + + /* + * Updates the current time of this time container and calls DoSample to + * perform any sample-operations. + */ + void Sample(); + + /* + * Return if this time container should be sampled or can be skipped. + * + * This is most useful as an optimisation for skipping time containers that + * don't require a sample. + */ + bool NeedsSample() const { return !mPauseState || mNeedsPauseSample; } + + /* + * Indicates if the elements of this time container need to be rewound. + * This occurs during a backwards seek. + */ + bool NeedsRewind() const { return mNeedsRewind; } + void ClearNeedsRewind() { mNeedsRewind = false; } + + /* + * Indicates the time container is currently processing a SetCurrentTime + * request and appropriate seek behaviour should be applied by child elements + * (e.g. not firing time events). + */ + bool IsSeeking() const { return mIsSeeking; } + void MarkSeekFinished() { mIsSeeking = false; } + + /* + * Sets the parent time container. + * + * The callee still retains ownership of the time container. + */ + nsresult SetParent(SMILTimeContainer* aParent); + + /* + * Registers an element for a sample at the given time. + * + * @param aMilestone The milestone to register in container time. + * @param aElement The timebase element that needs a sample at + * aMilestone. + */ + void AddMilestone(const SMILMilestone& aMilestone, + mozilla::dom::SVGAnimationElement& aElement); + + /* + * Resets the list of milestones. + */ + void ClearMilestones(); + + /* + * Returns the next significant transition from amongst the registered + * milestones. + * + * @param[out] aNextMilestone The next milestone with time in parent time. + * + * @return true if there exists another milestone, false otherwise in + * which case aNextMilestone will be unmodified. + */ + bool GetNextMilestoneInParentTime(SMILMilestone& aNextMilestone) const; + + using AnimElemArray = nsTArray>; + + /* + * Removes and returns the timebase elements from the start of the list of + * timebase elements that match the given time. + * + * @param aMilestone The milestone time to match in parent time. This + * must be <= GetNextMilestoneInParentTime. + * @param[out] aMatchedElements The array to which matching elements will be + * appended. + * @return true if one or more elements match, false otherwise. + */ + bool PopMilestoneElementsAtMilestone(const SMILMilestone& aMilestone, + AnimElemArray& aMatchedElements); + + // Cycle-collection support + void Traverse(nsCycleCollectionTraversalCallback* aCallback); + void Unlink(); + + protected: + /* + * Per-sample operations to be performed whenever Sample() is called and + * NeedsSample() is true. Called after updating mCurrentTime; + */ + virtual void DoSample() {} + + /* + * Adding and removing child containers is not implemented in the base class + * because not all subclasses need this. + */ + + /* + * Adds a child time container. + */ + virtual nsresult AddChild(SMILTimeContainer& aChild) { + return NS_ERROR_FAILURE; + } + + /* + * Removes a child time container. + */ + virtual void RemoveChild(SMILTimeContainer& aChild) {} + + /* + * Implementation helper to update the current time. + */ + void UpdateCurrentTime(); + + /* + * Implementation helper to notify timed elements with dependencies that the + * container time has changed with respect to the document time. + */ + void NotifyTimeChange(); + + // The parent time container, if any + SMILTimeContainer* mParent; + + // The current time established at the last call to Sample() + SMILTime mCurrentTime; + + // The number of milliseconds for which the container has been paused + // (excluding the current pause interval if the container is currently + // paused). + // + // Current time = parent time - mParentOffset + // + SMILTime mParentOffset; + + // The timestamp in parent time when the container was paused + SMILTime mPauseStart; + + // Whether or not a pause sample is required + bool mNeedsPauseSample; + + bool mNeedsRewind; // Backwards seek performed + bool mIsSeeking; // Currently in the middle of a seek operation + +#ifdef DEBUG + bool mHoldingEntries; // True if there's a raw pointer to mMilestoneEntries + // on the stack. +#endif + + // A bitfield of the pause state for all pause requests + uint32_t mPauseState; + + struct MilestoneEntry { + MilestoneEntry(const SMILMilestone& aMilestone, + mozilla::dom::SVGAnimationElement& aElement) + : mMilestone(aMilestone), mTimebase(&aElement) {} + + bool operator<(const MilestoneEntry& aOther) const { + return mMilestone < aOther.mMilestone; + } + + SMILMilestone mMilestone; // In container time. + RefPtr mTimebase; + }; + + // Queue of elements with registered milestones. Used to update the model with + // significant transitions that occur between two samples. Since timed element + // re-register their milestones when they're sampled this is reset once we've + // taken care of the milestones before the current sample time but before we + // actually do the full sample. + nsTPriorityQueue mMilestoneEntries; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILTIMECONTAINER_H_ diff --git a/dom/smil/SMILTimeValue.cpp b/dom/smil/SMILTimeValue.cpp new file mode 100644 index 0000000000..67676a329f --- /dev/null +++ b/dom/smil/SMILTimeValue.cpp @@ -0,0 +1,52 @@ +/* -*- 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 "SMILTimeValue.h" +#include "nsMathUtils.h" + +namespace mozilla { + +const SMILTime SMILTimeValue::kUnresolvedMillis = + std::numeric_limits::max(); + +//---------------------------------------------------------------------- +// SMILTimeValue methods: + +static inline int8_t Cmp(int64_t aA, int64_t aB) { + return aA == aB ? 0 : (aA > aB ? 1 : -1); +} + +int8_t SMILTimeValue::CompareTo(const SMILTimeValue& aOther) const { + int8_t result; + + if (mState == STATE_DEFINITE) { + result = (aOther.mState == STATE_DEFINITE) + ? Cmp(mMilliseconds, aOther.mMilliseconds) + : -1; + } else if (mState == STATE_INDEFINITE) { + if (aOther.mState == STATE_DEFINITE) + result = 1; + else if (aOther.mState == STATE_INDEFINITE) + result = 0; + else + result = -1; + } else { + result = (aOther.mState != STATE_UNRESOLVED) ? 1 : 0; + } + + return result; +} + +void SMILTimeValue::SetMillis(double aMillis, Rounding aRounding) { + mState = STATE_DEFINITE; + mMilliseconds = NS_round(aMillis); + if (aRounding == Rounding::EnsureNonZero && !mMilliseconds && aMillis) { + // Ensure we don't round small values to zero. + mMilliseconds = std::copysign(1.0, aMillis); + } +} + +} // namespace mozilla diff --git a/dom/smil/SMILTimeValue.h b/dom/smil/SMILTimeValue.h new file mode 100644 index 0000000000..19fd67b536 --- /dev/null +++ b/dom/smil/SMILTimeValue.h @@ -0,0 +1,145 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILTIMEVALUE_H_ +#define DOM_SMIL_SMILTIMEVALUE_H_ + +#include "mozilla/SMILTypes.h" +#include "nsDebug.h" + +namespace mozilla { + +/*---------------------------------------------------------------------- + * SMILTimeValue class + * + * A tri-state time value. + * + * First a quick overview of the SMIL time data types: + * + * SMILTime -- a timestamp in milliseconds. + * SMILTimeValue -- (this class) a timestamp that can take the additional + * states 'indefinite' and 'unresolved' + * SMILInstanceTime -- an SMILTimeValue used for constructing intervals. It + * contains additional fields to govern reset behavior + * and track timing dependencies (e.g. syncbase timing). + * SMILInterval -- a pair of SMILInstanceTimes that defines a begin and + * an end time for animation. + * SMILTimeValueSpec -- a component of a begin or end attribute, such as the + * '5s' or 'a.end+2m' in begin="5s; a.end+2m". Acts as + * a broker between an SMILTimedElement and its + * SMILInstanceTimes by generating new instance times + * and handling changes to existing times. + * + * Objects of this class may be in one of three states: + * + * 1) The time is resolved and has a definite millisecond value + * 2) The time is resolved and indefinite + * 3) The time is unresolved + * + * In summary: + * + * State | GetMillis | IsDefinite | IsIndefinite | IsResolved + * -----------+---------------+------------+--------------+------------ + * Definite | SMILTimeValue | true | false | true + * -----------+---------------+------------+--------------+------------ + * Indefinite | -- | false | true | true + * -----------+---------------+------------+--------------+------------ + * Unresolved | -- | false | false | false + * + */ + +class SMILTimeValue { + public: + // Creates an unresolved time value + SMILTimeValue() + : mMilliseconds(kUnresolvedMillis), mState(STATE_UNRESOLVED) {} + + // Creates a resolved time value + explicit SMILTimeValue(SMILTime aMillis) + : mMilliseconds(aMillis), mState(STATE_DEFINITE) {} + + // Named constructor to create an indefinite time value + static SMILTimeValue Indefinite() { + SMILTimeValue value; + value.SetIndefinite(); + return value; + } + + static SMILTimeValue Zero() { return SMILTimeValue(SMILTime(0L)); } + + bool IsIndefinite() const { return mState == STATE_INDEFINITE; } + void SetIndefinite() { + mState = STATE_INDEFINITE; + mMilliseconds = kUnresolvedMillis; + } + + bool IsResolved() const { return mState != STATE_UNRESOLVED; } + void SetUnresolved() { + mState = STATE_UNRESOLVED; + mMilliseconds = kUnresolvedMillis; + } + + bool IsDefinite() const { return mState == STATE_DEFINITE; } + SMILTime GetMillis() const { + MOZ_ASSERT(mState == STATE_DEFINITE, + "GetMillis() called for unresolved or indefinite time"); + + return mState == STATE_DEFINITE ? mMilliseconds : kUnresolvedMillis; + } + + bool IsZero() const { + return mState == STATE_DEFINITE ? mMilliseconds == 0 : false; + } + + void SetMillis(SMILTime aMillis) { + mState = STATE_DEFINITE; + mMilliseconds = aMillis; + } + + /* + * EnsureNonZero ensures values such as 0.0001s are not represented as 0 + * for values where 0 is invalid. + */ + enum class Rounding : uint8_t { EnsureNonZero, Nearest }; + + void SetMillis(double aMillis, Rounding aRounding); + + int8_t CompareTo(const SMILTimeValue& aOther) const; + + bool operator==(const SMILTimeValue& aOther) const { + return CompareTo(aOther) == 0; + } + + bool operator!=(const SMILTimeValue& aOther) const { + return CompareTo(aOther) != 0; + } + + bool operator<(const SMILTimeValue& aOther) const { + return CompareTo(aOther) < 0; + } + + bool operator>(const SMILTimeValue& aOther) const { + return CompareTo(aOther) > 0; + } + + bool operator<=(const SMILTimeValue& aOther) const { + return CompareTo(aOther) <= 0; + } + + bool operator>=(const SMILTimeValue& aOther) const { + return CompareTo(aOther) >= 0; + } + + private: + static const SMILTime kUnresolvedMillis; + + SMILTime mMilliseconds; + enum { STATE_DEFINITE, STATE_INDEFINITE, STATE_UNRESOLVED } mState; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILTIMEVALUE_H_ diff --git a/dom/smil/SMILTimeValueSpec.cpp b/dom/smil/SMILTimeValueSpec.cpp new file mode 100644 index 0000000000..177c813bbc --- /dev/null +++ b/dom/smil/SMILTimeValueSpec.cpp @@ -0,0 +1,370 @@ +/* -*- 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 "mozilla/EventListenerManager.h" +#include "mozilla/SMILInstanceTime.h" +#include "mozilla/SMILInterval.h" +#include "mozilla/SMILParserUtils.h" +#include "mozilla/SMILTimeContainer.h" +#include "mozilla/SMILTimedElement.h" +#include "mozilla/SMILTimeValueSpec.h" +#include "mozilla/SMILTimeValue.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/SVGAnimationElement.h" +#include "mozilla/dom/TimeEvent.h" +#include "nsString.h" +#include + +using namespace mozilla::dom; + +namespace mozilla { + +//---------------------------------------------------------------------- +// Nested class: EventListener + +NS_IMPL_ISUPPORTS(SMILTimeValueSpec::EventListener, nsIDOMEventListener) + +NS_IMETHODIMP +SMILTimeValueSpec::EventListener::HandleEvent(Event* aEvent) { + if (mSpec) { + mSpec->HandleEvent(aEvent); + } + return NS_OK; +} + +//---------------------------------------------------------------------- +// Implementation + +SMILTimeValueSpec::SMILTimeValueSpec(SMILTimedElement& aOwner, bool aIsBegin) + : mOwner(&aOwner), mIsBegin(aIsBegin), mReferencedElement(this) {} + +SMILTimeValueSpec::~SMILTimeValueSpec() { + UnregisterFromReferencedElement(mReferencedElement.get()); + if (mEventListener) { + mEventListener->Disconnect(); + mEventListener = nullptr; + } +} + +nsresult SMILTimeValueSpec::SetSpec(const nsAString& aStringSpec, + Element& aContextElement) { + SMILTimeValueSpecParams params; + + if (!SMILParserUtils::ParseTimeValueSpecParams(aStringSpec, params)) + return NS_ERROR_FAILURE; + + mParams = params; + + // According to SMIL 3.0: + // The special value "indefinite" does not yield an instance time in the + // begin list. It will, however yield a single instance with the value + // "indefinite" in an end list. This value is not removed by a reset. + if (mParams.mType == SMILTimeValueSpecParams::OFFSET || + (!mIsBegin && mParams.mType == SMILTimeValueSpecParams::INDEFINITE)) { + mOwner->AddInstanceTime(new SMILInstanceTime(mParams.mOffset), mIsBegin); + } + + // Fill in the event symbol to simplify handling later + if (mParams.mType == SMILTimeValueSpecParams::REPEAT) { + mParams.mEventSymbol = nsGkAtoms::repeatEvent; + } + + ResolveReferences(aContextElement); + + return NS_OK; +} + +void SMILTimeValueSpec::ResolveReferences(Element& aContextElement) { + if (mParams.mType != SMILTimeValueSpecParams::SYNCBASE && !IsEventBased()) { + return; + } + + // If we're not bound to the document yet, don't worry, we'll get called again + // when that happens + if (!aContextElement.IsInComposedDoc()) return; + + // Hold ref to the old element so that it isn't destroyed in between resetting + // the referenced element and using the pointer to update the referenced + // element. + RefPtr oldReferencedElement = mReferencedElement.get(); + + if (mParams.mDependentElemID) { + mReferencedElement.ResetWithID(aContextElement, mParams.mDependentElemID); + } else if (mParams.mType == SMILTimeValueSpecParams::EVENT) { + Element* target = mOwner->GetTargetElement(); + mReferencedElement.ResetWithElement(target); + } else { + MOZ_ASSERT(false, "Syncbase or repeat spec without ID"); + } + UpdateReferencedElement(oldReferencedElement, mReferencedElement.get()); +} + +bool SMILTimeValueSpec::IsEventBased() const { + return mParams.mType == SMILTimeValueSpecParams::EVENT || + mParams.mType == SMILTimeValueSpecParams::REPEAT; +} + +void SMILTimeValueSpec::HandleNewInterval( + SMILInterval& aInterval, const SMILTimeContainer* aSrcContainer) { + const SMILInstanceTime& baseInstance = + mParams.mSyncBegin ? *aInterval.Begin() : *aInterval.End(); + SMILTimeValue newTime = + ConvertBetweenTimeContainers(baseInstance.Time(), aSrcContainer); + + // Apply offset + if (!ApplyOffset(newTime)) { + NS_WARNING("New time overflows SMILTime, ignoring"); + return; + } + + // Create the instance time and register it with the interval + RefPtr newInstance = new SMILInstanceTime( + newTime, SMILInstanceTime::SOURCE_SYNCBASE, this, &aInterval); + mOwner->AddInstanceTime(newInstance, mIsBegin); +} + +void SMILTimeValueSpec::HandleTargetElementChange(Element* aNewTarget) { + if (!IsEventBased() || mParams.mDependentElemID) return; + + mReferencedElement.ResetWithElement(aNewTarget); +} + +void SMILTimeValueSpec::HandleChangedInstanceTime( + const SMILInstanceTime& aBaseTime, const SMILTimeContainer* aSrcContainer, + SMILInstanceTime& aInstanceTimeToUpdate, bool aObjectChanged) { + // If the instance time is fixed (e.g. because it's being used as the begin + // time of an active or postactive interval) we just ignore the change. + if (aInstanceTimeToUpdate.IsFixedTime()) return; + + SMILTimeValue updatedTime = + ConvertBetweenTimeContainers(aBaseTime.Time(), aSrcContainer); + + // Apply offset + if (!ApplyOffset(updatedTime)) { + NS_WARNING("Updated time overflows SMILTime, ignoring"); + return; + } + + // The timed element that owns the instance time does the updating so it can + // re-sort its array of instance times more efficiently + if (aInstanceTimeToUpdate.Time() != updatedTime || aObjectChanged) { + mOwner->UpdateInstanceTime(&aInstanceTimeToUpdate, updatedTime, mIsBegin); + } +} + +void SMILTimeValueSpec::HandleDeletedInstanceTime( + SMILInstanceTime& aInstanceTime) { + mOwner->RemoveInstanceTime(&aInstanceTime, mIsBegin); +} + +bool SMILTimeValueSpec::DependsOnBegin() const { return mParams.mSyncBegin; } + +void SMILTimeValueSpec::Traverse( + nsCycleCollectionTraversalCallback* aCallback) { + mReferencedElement.Traverse(aCallback); +} + +void SMILTimeValueSpec::Unlink() { + UnregisterFromReferencedElement(mReferencedElement.get()); + mReferencedElement.Unlink(); +} + +//---------------------------------------------------------------------- +// Implementation helpers + +void SMILTimeValueSpec::UpdateReferencedElement(Element* aFrom, Element* aTo) { + if (aFrom == aTo) return; + + UnregisterFromReferencedElement(aFrom); + + switch (mParams.mType) { + case SMILTimeValueSpecParams::SYNCBASE: { + SMILTimedElement* to = GetTimedElement(aTo); + if (to) { + to->AddDependent(*this); + } + } break; + + case SMILTimeValueSpecParams::EVENT: + case SMILTimeValueSpecParams::REPEAT: + RegisterEventListener(aTo); + break; + + default: + // not a referencing-type + break; + } +} + +void SMILTimeValueSpec::UnregisterFromReferencedElement(Element* aElement) { + if (!aElement) return; + + if (mParams.mType == SMILTimeValueSpecParams::SYNCBASE) { + SMILTimedElement* timedElement = GetTimedElement(aElement); + if (timedElement) { + timedElement->RemoveDependent(*this); + } + mOwner->RemoveInstanceTimesForCreator(this, mIsBegin); + } else if (IsEventBased()) { + UnregisterEventListener(aElement); + } +} + +SMILTimedElement* SMILTimeValueSpec::GetTimedElement(Element* aElement) { + auto* animationElement = SVGAnimationElement::FromNodeOrNull(aElement); + return animationElement ? &animationElement->TimedElement() : nullptr; +} + +// Indicates whether we're allowed to register an event-listener +// when scripting is disabled. +bool SMILTimeValueSpec::IsEventAllowedWhenScriptingIsDisabled() { + // The category of (SMIL-specific) "repeat(n)" events are allowed. + if (mParams.mType == SMILTimeValueSpecParams::REPEAT) { + return true; + } + + // A specific list of other SMIL-related events are allowed, too. + if (mParams.mType == SMILTimeValueSpecParams::EVENT && + (mParams.mEventSymbol == nsGkAtoms::repeat || + mParams.mEventSymbol == nsGkAtoms::repeatEvent || + mParams.mEventSymbol == nsGkAtoms::beginEvent || + mParams.mEventSymbol == nsGkAtoms::endEvent)) { + return true; + } + + return false; +} + +void SMILTimeValueSpec::RegisterEventListener(Element* aTarget) { + MOZ_ASSERT(IsEventBased(), + "Attempting to register event-listener for unexpected " + "SMILTimeValueSpec type"); + MOZ_ASSERT(mParams.mEventSymbol, + "Attempting to register event-listener but there is no event " + "name"); + + if (!aTarget) return; + + // When script is disabled, only allow registration for limited events. + if (!aTarget->GetOwnerDocument()->IsScriptEnabled() && + !IsEventAllowedWhenScriptingIsDisabled()) { + return; + } + + if (!mEventListener) { + mEventListener = new EventListener(this); + } + + EventListenerManager* elm = aTarget->GetOrCreateListenerManager(); + if (!elm) { + return; + } + + elm->AddEventListenerByType(mEventListener, + nsDependentAtomString(mParams.mEventSymbol), + AllEventsAtSystemGroupBubble()); +} + +void SMILTimeValueSpec::UnregisterEventListener(Element* aTarget) { + if (!aTarget || !mEventListener) { + return; + } + + EventListenerManager* elm = aTarget->GetOrCreateListenerManager(); + if (!elm) { + return; + } + + elm->RemoveEventListenerByType(mEventListener, + nsDependentAtomString(mParams.mEventSymbol), + AllEventsAtSystemGroupBubble()); +} + +void SMILTimeValueSpec::HandleEvent(Event* aEvent) { + MOZ_ASSERT(mEventListener, "Got event without an event listener"); + MOZ_ASSERT(IsEventBased(), "Got event for non-event SMILTimeValueSpec"); + MOZ_ASSERT(aEvent, "No event supplied"); + + // XXX In the long run we should get the time from the event itself which will + // store the time in global document time which we'll need to convert to our + // time container + SMILTimeContainer* container = mOwner->GetTimeContainer(); + if (!container) return; + + if (mParams.mType == SMILTimeValueSpecParams::REPEAT && + !CheckRepeatEventDetail(aEvent)) { + return; + } + + SMILTime currentTime = container->GetCurrentTimeAsSMILTime(); + SMILTimeValue newTime(currentTime); + if (!ApplyOffset(newTime)) { + NS_WARNING("New time generated from event overflows SMILTime, ignoring"); + return; + } + + RefPtr newInstance = + new SMILInstanceTime(newTime, SMILInstanceTime::SOURCE_EVENT); + mOwner->AddInstanceTime(newInstance, mIsBegin); +} + +bool SMILTimeValueSpec::CheckRepeatEventDetail(Event* aEvent) { + TimeEvent* timeEvent = aEvent->AsTimeEvent(); + if (!timeEvent) { + NS_WARNING("Received a repeat event that was not a DOMTimeEvent"); + return false; + } + + int32_t detail = timeEvent->Detail(); + return detail > 0 && (uint32_t)detail == mParams.mRepeatIteration; +} + +SMILTimeValue SMILTimeValueSpec::ConvertBetweenTimeContainers( + const SMILTimeValue& aSrcTime, const SMILTimeContainer* aSrcContainer) { + // If the source time is either indefinite or unresolved the result is going + // to be the same + if (!aSrcTime.IsDefinite()) return aSrcTime; + + // Convert from source time container to our parent time container + const SMILTimeContainer* dstContainer = mOwner->GetTimeContainer(); + if (dstContainer == aSrcContainer) return aSrcTime; + + // If one of the elements is not attached to a time container then we can't do + // any meaningful conversion + if (!aSrcContainer || !dstContainer) return SMILTimeValue(); // unresolved + + SMILTimeValue docTime = + aSrcContainer->ContainerToParentTime(aSrcTime.GetMillis()); + + if (docTime.IsIndefinite()) + // This will happen if the source container is paused and we have a future + // time. Just return the indefinite time. + return docTime; + + MOZ_ASSERT(docTime.IsDefinite(), + "ContainerToParentTime gave us an unresolved or indefinite time"); + + return dstContainer->ParentToContainerTime(docTime.GetMillis()); +} + +bool SMILTimeValueSpec::ApplyOffset(SMILTimeValue& aTime) const { + // indefinite + offset = indefinite. Likewise for unresolved times. + if (!aTime.IsDefinite()) { + return true; + } + + double resultAsDouble = + (double)aTime.GetMillis() + mParams.mOffset.GetMillis(); + if (resultAsDouble > double(std::numeric_limits::max()) || + resultAsDouble < double(std::numeric_limits::min())) { + return false; + } + aTime.SetMillis(aTime.GetMillis() + mParams.mOffset.GetMillis()); + return true; +} + +} // namespace mozilla diff --git a/dom/smil/SMILTimeValueSpec.h b/dom/smil/SMILTimeValueSpec.h new file mode 100644 index 0000000000..cb0d2dd9ab --- /dev/null +++ b/dom/smil/SMILTimeValueSpec.h @@ -0,0 +1,143 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILTIMEVALUESPEC_H_ +#define DOM_SMIL_SMILTIMEVALUESPEC_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/SMILTimeValueSpecParams.h" +#include "mozilla/dom/IDTracker.h" +#include "nsStringFwd.h" +#include "nsIDOMEventListener.h" + +// XXX Avoid including this here by moving function bodies to the cpp file +#include "mozilla/dom/Element.h" + +namespace mozilla { + +class EventListenerManager; +class SMILInstanceTime; +class SMILInterval; +class SMILTimeContainer; +class SMILTimedElement; +class SMILTimeValue; + +namespace dom { +class Event; +} // namespace dom + +//---------------------------------------------------------------------- +// SMILTimeValueSpec class +// +// An individual element of a 'begin' or 'end' attribute, e.g. '5s', 'a.end'. +// This class handles the parsing of such specifications and performs the +// necessary event handling (for event and repeat specifications) +// and synchronisation (for syncbase specifications). +// +// For an overview of how this class is related to other SMIL time classes see +// the documentation in SMILTimeValue.h + +class SMILTimeValueSpec { + public: + using Element = dom::Element; + using Event = dom::Event; + using IDTracker = dom::IDTracker; + + SMILTimeValueSpec(SMILTimedElement& aOwner, bool aIsBegin); + ~SMILTimeValueSpec(); + + nsresult SetSpec(const nsAString& aStringSpec, Element& aContextElement); + void ResolveReferences(Element& aContextElement); + bool IsEventBased() const; + + void HandleNewInterval(SMILInterval& aInterval, + const SMILTimeContainer* aSrcContainer); + void HandleTargetElementChange(Element* aNewTarget); + + // For created SMILInstanceTime objects + bool DependsOnBegin() const; + void HandleChangedInstanceTime(const SMILInstanceTime& aBaseTime, + const SMILTimeContainer* aSrcContainer, + SMILInstanceTime& aInstanceTimeToUpdate, + bool aObjectChanged); + void HandleDeletedInstanceTime(SMILInstanceTime& aInstanceTime); + + // Cycle-collection support + void Traverse(nsCycleCollectionTraversalCallback* aCallback); + void Unlink(); + + protected: + void UpdateReferencedElement(Element* aFrom, Element* aTo); + void UnregisterFromReferencedElement(Element* aElement); + SMILTimedElement* GetTimedElement(Element* aElement); + bool IsEventAllowedWhenScriptingIsDisabled(); + void RegisterEventListener(Element* aTarget); + void UnregisterEventListener(Element* aTarget); + void HandleEvent(Event* aEvent); + bool CheckRepeatEventDetail(Event* aEvent); + SMILTimeValue ConvertBetweenTimeContainers( + const SMILTimeValue& aSrcTime, const SMILTimeContainer* aSrcContainer); + bool ApplyOffset(SMILTimeValue& aTime) const; + + SMILTimedElement* mOwner; + bool mIsBegin; // Indicates if *we* are a begin spec, + // not to be confused with + // mParams.mSyncBegin which indicates + // if we're synced with the begin of + // the target. + SMILTimeValueSpecParams mParams; + + /** + * If our SMILTimeValueSpec exists for a 'begin' or 'end' attribute with a + * value that specifies a time that is relative to the animation of some + * other element, it will create an instance of this class to reference and + * track that other element. For example, if the SMILTimeValueSpec is for + * end='a.end+2s', an instance of this class will be created to track the + * element associated with the element ID "a". This class will notify the + * SMILTimeValueSpec if the element that that ID identifies changes to a + * different element (or none). + */ + class TimeReferenceTracker final : public IDTracker { + public: + explicit TimeReferenceTracker(SMILTimeValueSpec* aOwner) : mSpec(aOwner) {} + void ResetWithElement(Element* aTo) { + RefPtr from = get(); + Unlink(); + ElementChanged(from, aTo); + } + + protected: + void ElementChanged(Element* aFrom, Element* aTo) override { + IDTracker::ElementChanged(aFrom, aTo); + mSpec->UpdateReferencedElement(aFrom, aTo); + } + bool IsPersistent() override { return true; } + + private: + SMILTimeValueSpec* mSpec; + }; + + TimeReferenceTracker mReferencedElement; + + class EventListener final : public nsIDOMEventListener { + ~EventListener() {} + + public: + explicit EventListener(SMILTimeValueSpec* aOwner) : mSpec(aOwner) {} + void Disconnect() { mSpec = nullptr; } + + NS_DECL_ISUPPORTS + NS_DECL_NSIDOMEVENTLISTENER + + private: + SMILTimeValueSpec* mSpec; + }; + RefPtr mEventListener; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILTIMEVALUESPEC_H_ diff --git a/dom/smil/SMILTimeValueSpecParams.h b/dom/smil/SMILTimeValueSpecParams.h new file mode 100644 index 0000000000..2487168b7f --- /dev/null +++ b/dom/smil/SMILTimeValueSpecParams.h @@ -0,0 +1,58 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILTIMEVALUESPECPARAMS_H_ +#define DOM_SMIL_SMILTIMEVALUESPECPARAMS_H_ + +#include "mozilla/SMILTimeValue.h" +#include "nsAtom.h" + +namespace mozilla { + +//---------------------------------------------------------------------- +// SMILTimeValueSpecParams +// +// A simple data type for storing the result of parsing a single begin or end +// value (e.g. the '5s' in begin="5s; indefinite; a.begin+2s"). + +class SMILTimeValueSpecParams { + public: + SMILTimeValueSpecParams() + : mType(INDEFINITE), mSyncBegin(false), mRepeatIteration(0) {} + + // The type of value this specification describes + enum { OFFSET, SYNCBASE, EVENT, REPEAT, WALLCLOCK, INDEFINITE } mType; + + // A clock value that is added to: + // - type OFFSET: the document begin + // - type SYNCBASE: the timebase's begin or end time + // - type EVENT: the event time + // - type REPEAT: the repeat time + // It is not used for WALLCLOCK or INDEFINITE times + SMILTimeValue mOffset; + + // The base element that this specification refers to. + // For SYNCBASE types, this is the timebase + // For EVENT and REPEAT types, this is the eventbase + RefPtr mDependentElemID; + + // The event to respond to. + // Only used for EVENT types. + RefPtr mEventSymbol; + + // Indicates if this specification refers to the begin or end of the dependent + // element. + // Only used for SYNCBASE types. + bool mSyncBegin; + + // The repeat iteration to respond to. + // Only used for mType=REPEAT. + uint32_t mRepeatIteration; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILTIMEVALUESPECPARAMS_H_ diff --git a/dom/smil/SMILTimedElement.cpp b/dom/smil/SMILTimedElement.cpp new file mode 100644 index 0000000000..4be4a8840e --- /dev/null +++ b/dom/smil/SMILTimedElement.cpp @@ -0,0 +1,2166 @@ +/* -*- 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 "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 + +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 mTarget; + EventMessage mMsg; + int32_t mDetail; + + public: + AsyncTimeEventRunner(nsIContent* aTarget, EventMessage aMsg, int32_t aDetail) + : mozilla::Runnable("AsyncTimeEventRunner"), + mTarget(aTarget), + mMsg(aMsg), + mDetail(aDetail) {} + + // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398) + MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override { + InternalSMILTimeEvent event(true, mMsg); + event.mDetail = mDetail; + + RefPtr 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 > with O(n) performance. +template +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}}; + +const SMILMilestone SMILTimedElement::sMaxMilestone( + std::numeric_limits::max(), false); + +// 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& instance : mBeginInstances) { + instance->Unlink(); + } + mBeginInstances.Clear(); + for (RefPtr& 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: +// +// http://www.w3.org/2003/01/REC-SVG11-20030114-errata#elementtimecontrol-interface +// +// 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(); + return AddInstanceTimeFromCurrentTime(currentTime, aOffsetSeconds, true); +} + +nsresult SMILTimedElement::EndElementAt(double aOffsetSeconds) { + SMILTimeContainer* container = GetTimeContainer(); + if (!container) return NS_ERROR_FAILURE; + + SMILTime currentTime = container->GetCurrentTimeAsSMILTime(); + return AddInstanceTimeFromCurrentTime(currentTime, aOffsetSeconds, false); +} + +//---------------------------------------------------------------------- +// 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* 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 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 + // (bug 277955) and whilst waiting for those resources (and the animation to + // 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(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(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(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& beginSpec : mBeginSpecs) { + beginSpec->ResolveReferences(aContextElement); + } + + for (UniquePtr& endSpec : mEndSpecs) { + endSpec->ResolveReferences(aContextElement); + } + } + + RegisterMilestone(); +} + +void SMILTimedElement::HandleTargetElementChange(Element* aNewTarget) { + AutoIntervalUpdateBatcher updateBatcher(*this); + + for (UniquePtr& beginSpec : mBeginSpecs) { + beginSpec->HandleTargetElementChange(aNewTarget); + } + + for (UniquePtr& endSpec : mEndSpecs) { + endSpec->HandleTargetElementChange(aNewTarget); + } +} + +void SMILTimedElement::Traverse(nsCycleCollectionTraversalCallback* aCallback) { + for (UniquePtr& beginSpec : mBeginSpecs) { + MOZ_ASSERT(beginSpec, "null SMILTimeValueSpec in list of begin specs"); + beginSpec->Traverse(aCallback); + } + + for (UniquePtr& 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& beginSpec : mBeginSpecs) { + MOZ_ASSERT(beginSpec, "null SMILTimeValueSpec in list of begin specs"); + beginSpec->Unlink(); + } + + for (UniquePtr& 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(*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& 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 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& 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& aTimesToKeep) + : mThreshold(aThreshold), mTimesToKeep(aTimesToKeep) {} + bool operator()(SMILInstanceTime* aInstanceTime, uint32_t aIndex) { + return aIndex < mThreshold && !mTimesToKeep.Contains(aInstanceTime); + } + + private: + uint32_t mThreshold; + nsTArray& 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 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: +// http://www.w3.org/TR/2001/REC-smil-animation-20010904/#Timing-BeginEnd-LC-Start +// +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(INT64_MIN); + } + + RefPtr tempBegin; + RefPtr tempEnd; + + while (true) { + // Calculate begin time + if (aFixedBeginTime) { + if (aFixedBeginTime->Time() < beginAfter) { + return false; + } + // our ref-counting is not const-correct + tempBegin = const_cast(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. + // 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::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; + } + + SMILTimeValue result; + + if (aDuration > mMax) { + result = mMax; + } else if (aDuration < mMin) { + result = mMin; + } else { + result = aDuration; + } + + return result; +} + +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 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(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 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); + } +} + +nsresult SMILTimedElement::AddInstanceTimeFromCurrentTime(SMILTime aCurrentTime, + double aOffsetSeconds, + bool aIsBegin) { + double offset = NS_round(aOffsetSeconds * PR_MSEC_PER_SEC); + + // Check we won't overflow the range of SMILTime + if (aCurrentTime + offset > double(std::numeric_limits::max())) + return NS_ERROR_ILLEGAL_VALUE; + + SMILTimeValue timeVal(aCurrentTime + int64_t(offset)); + + RefPtr instanceTime = + new SMILInstanceTime(timeVal, SMILInstanceTime::SOURCE_DOM); + + AddInstanceTime(instanceTime, aIsBegin); + + return NS_OK; +} + +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& time : times) { + time->HandleChangedInterval(container, aBeginObjectChanged, + aEndObjectChanged); + } +} + +void SMILTimedElement::FireTimeEventAsync(EventMessage aMsg, int32_t aDetail) { + if (!mAnimationElement) return; + + nsCOMPtr event = + new AsyncTimeEventRunner(mAnimationElement, aMsg, aDetail); + mAnimationElement->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& endSpec : mEndSpecs) { + if (endSpec->IsEventBased()) return true; + } + return false; +} + +bool SMILTimedElement::AreEndTimesDependentOn( + const SMILInstanceTime* aBase) const { + if (mEndInstances.IsEmpty()) return false; + + for (const RefPtr& endInstance : mEndInstances) { + if (endInstance->GetBaseTime() != aBase) { + return false; + } + } + return true; +} + +} // namespace mozilla diff --git a/dom/smil/SMILTimedElement.h b/dom/smil/SMILTimedElement.h new file mode 100644 index 0000000000..fc2792d1dc --- /dev/null +++ b/dom/smil/SMILTimedElement.h @@ -0,0 +1,649 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILTIMEDELEMENT_H_ +#define DOM_SMIL_SMILTIMEDELEMENT_H_ + +#include + +#include "mozilla/EventForwards.h" +#include "mozilla/SMILInstanceTime.h" +#include "mozilla/SMILInterval.h" +#include "mozilla/SMILMilestone.h" +#include "mozilla/SMILRepeatCount.h" +#include "mozilla/SMILTimeValueSpec.h" +#include "mozilla/SMILTypes.h" +#include "mozilla/UniquePtr.h" +#include "nsAttrValue.h" +#include "nsHashKeys.h" +#include "nsTArray.h" +#include "nsTHashtable.h" + +class nsAtom; + +namespace mozilla { + +class SMILAnimationFunction; +class SMILTimeContainer; +class SMILTimeValue; + +namespace dom { +class SVGAnimationElement; +} // namespace dom + +//---------------------------------------------------------------------- +// SMILTimedElement + +class SMILTimedElement { + public: + SMILTimedElement(); + ~SMILTimedElement(); + + using Element = dom::Element; + + /* + * Sets the owning animation element which this class uses to convert between + * container times and to register timebase elements. + */ + void SetAnimationElement(mozilla::dom::SVGAnimationElement* aElement); + + /* + * Returns the time container with which this timed element is associated or + * nullptr if it is not associated with a time container. + */ + SMILTimeContainer* GetTimeContainer(); + + /* + * Returns the element targeted by the animation element. Needed for + * registering event listeners against the appropriate element. + */ + Element* GetTargetElement(); + + /** + * Methods for supporting the ElementTimeControl interface. + */ + + /* + * Adds a new begin instance time at the current container time plus or minus + * the specified offset. + * + * @param aOffsetSeconds A real number specifying the number of seconds to add + * to the current container time. + * @return NS_OK if the operation succeeeded, or an error code otherwise. + */ + nsresult BeginElementAt(double aOffsetSeconds); + + /* + * Adds a new end instance time at the current container time plus or minus + * the specified offset. + * + * @param aOffsetSeconds A real number specifying the number of seconds to add + * to the current container time. + * @return NS_OK if the operation succeeeded, or an error code otherwise. + */ + nsresult EndElementAt(double aOffsetSeconds); + + /** + * Methods for supporting the SVGAnimationElement interface. + */ + + /** + * According to SVG 1.1 SE this returns + * + * the begin time, in seconds, for this animation element's current + * interval, if it exists, regardless of whether the interval has begun yet. + * + * @return the start time as defined above in milliseconds or an unresolved + * time if there is no current interval. + */ + SMILTimeValue GetStartTime() const; + + /** + * Returns the simple duration of this element. + * + * @return the simple duration in milliseconds or INDEFINITE. + */ + SMILTimeValue GetSimpleDuration() const { return mSimpleDur; } + + /** + * Methods for supporting hyperlinking + */ + + /** + * Internal SMIL methods + */ + + /** + * Returns the time to seek the document to when this element is targetted by + * a hyperlink. + * + * The behavior is defined here: + * http://www.w3.org/TR/smil-animation/#HyperlinkSemantics + * + * It is very similar to GetStartTime() with the exception that when the + * element is not active, the begin time of the *first* interval is returned. + * + * @return the time to seek the documen to in milliseconds or an unresolved + * time if there is no resolved interval. + */ + SMILTimeValue GetHyperlinkTime() const; + + /** + * Adds an instance time object this element's list of instance times. + * These instance times are used when creating intervals. + * + * This method is typically called by an SMILTimeValueSpec. + * + * @param aInstanceTime The time to add, expressed in container time. + * @param aIsBegin true if the time to be added represents a begin + * time or false if it represents an end time. + */ + void AddInstanceTime(SMILInstanceTime* aInstanceTime, bool aIsBegin); + + /** + * Requests this element update the given instance time. + * + * This method is typically called by a child SMILTimeValueSpec. + * + * @param aInstanceTime The instance time to update. + * @param aUpdatedTime The time to update aInstanceTime with. + * @param aDependentTime The instance time upon which aInstanceTime should be + * based. + * @param aIsBegin true if the time to be updated represents a begin + * instance time or false if it represents an end + * instance time. + */ + void UpdateInstanceTime(SMILInstanceTime* aInstanceTime, + SMILTimeValue& aUpdatedTime, bool aIsBegin); + + /** + * Removes an instance time object from this element's list of instance times. + * + * This method is typically called by a child SMILTimeValueSpec. + * + * @param aInstanceTime The instance time to remove. + * @param aIsBegin true if the time to be removed represents a begin + * time or false if it represents an end time. + */ + void RemoveInstanceTime(SMILInstanceTime* aInstanceTime, bool aIsBegin); + + /** + * Removes all the instance times associated with the given + * SMILTimeValueSpec object. Used when an ID assignment changes and hence + * all the previously associated instance times become invalid. + * + * @param aCreator The SMILTimeValueSpec object whose created + * SMILInstanceTime's should be removed. + * @param aIsBegin true if the times to be removed represent begin + * times or false if they are end times. + */ + void RemoveInstanceTimesForCreator(const SMILTimeValueSpec* aCreator, + bool aIsBegin); + + /** + * Sets the object that will be called by this timed element each time it is + * sampled. + * + * In Schmitz's model it is possible to associate several time clients with + * a timed element but for now we only allow one. + * + * @param aClient The time client to associate. Any previous time client + * will be disassociated and no longer sampled. Setting this + * to nullptr will simply disassociate the previous client, + * if any. + */ + void SetTimeClient(SMILAnimationFunction* aClient); + + /** + * Samples the object at the given container time. Timing intervals are + * updated and if this element is active at the given time the associated time + * client will be sampled with the appropriate simple time. + * + * @param aContainerTime The container time at which to sample. + */ + void SampleAt(SMILTime aContainerTime); + + /** + * Performs a special sample for the end of an interval. Such a sample should + * only advance the timed element (and any dependent elements) to the waiting + * or postactive state. It should not cause a transition to the active state. + * Transition to the active state is only performed on a regular SampleAt. + * + * This allows all interval ends at a given time to be processed first and + * hence the new interval can be established based on full information of the + * available instance times. + * + * @param aContainerTime The container time at which to sample. + */ + void SampleEndAt(SMILTime aContainerTime); + + /** + * Informs the timed element that its time container has changed time + * relative to document time. The timed element therefore needs to update its + * dependent elements (which may belong to a different time container) so they + * can re-resolve their times. + */ + void HandleContainerTimeChange(); + + /** + * Resets this timed element's accumulated times and intervals back to start + * up state. + * + * This is used for backwards seeking where rather than accumulating + * historical timing state and winding it back, we reset the element and seek + * forwards. + */ + void Rewind(); + + /** + * Marks this element as disabled or not. If the element is disabled, it + * will ignore any future samples and discard any accumulated timing state. + * + * This is used by SVG to "turn off" timed elements when the associated + * animation element has failing conditional processing tests. + * + * Returns true if the disabled state of the timed element was changed + * as a result of this call (i.e. it was not a redundant call). + */ + bool SetIsDisabled(bool aIsDisabled); + + /** + * Attempts to set an attribute on this timed element. + * + * @param aAttribute The name of the attribute to set. The namespace of this + * attribute is not specified as it is checked by the host + * element. Only attributes in the namespace defined for + * SMIL attributes in the host language are passed to the + * timed element. + * @param aValue The attribute value. + * @param aResult The nsAttrValue object that may be used for storing the + * parsed result. + * @param aContextElement The element to use for context when resolving + * references to other elements. + * @param[out] aParseResult The result of parsing the attribute. Will be set + * to NS_OK if parsing is successful. + * + * @return true if the given attribute is a timing attribute, false + * otherwise. + */ + bool SetAttr(nsAtom* aAttribute, const nsAString& aValue, + nsAttrValue& aResult, Element& aContextElement, + nsresult* aParseResult = nullptr); + + /** + * Attempts to unset an attribute on this timed element. + * + * @param aAttribute The name of the attribute to set. As with SetAttr the + * namespace of the attribute is not specified (see + * SetAttr). + * + * @return true if the given attribute is a timing attribute, false + * otherwise. + */ + bool UnsetAttr(nsAtom* aAttribute); + + /** + * Adds a syncbase dependency to the list of dependents that will be notified + * when this timed element creates, deletes, or updates its current interval. + * + * @param aDependent The SMILTimeValueSpec object to notify. A raw pointer + * to this object will be stored. Therefore it is necessary + * for the object to be explicitly unregistered (with + * RemoveDependent) when it is destroyed. + */ + void AddDependent(SMILTimeValueSpec& aDependent); + + /** + * Removes a syncbase dependency from the list of dependents that are notified + * when the current interval is modified. + * + * @param aDependent The SMILTimeValueSpec object to unregister. + */ + void RemoveDependent(SMILTimeValueSpec& aDependent); + + /** + * Determines if this timed element is dependent on the given timed element's + * begin time for the interval currently in effect. Whilst the element is in + * the active state this is the current interval and in the postactive or + * waiting state this is the previous interval if one exists. In all other + * cases the element is not considered a time dependent of any other element. + * + * @param aOther The potential syncbase element. + * @return true if this timed element's begin time for the currently + * effective interval is directly or indirectly derived from aOther, false + * otherwise. + */ + bool IsTimeDependent(const SMILTimedElement& aOther) const; + + /** + * Called when the timed element has been bound to the document so that + * references from this timed element to other elements can be resolved. + * + * @param aContextElement The element which provides the necessary context for + * resolving references. This is typically the element + * in the host language that owns this timed element. + */ + void BindToTree(Element& aContextElement); + + /** + * Called when the target of the animation has changed so that event + * registrations can be updated. + */ + void HandleTargetElementChange(Element* aNewTarget); + + /** + * Called when the timed element has been removed from a document so that + * references to other elements can be broken. + */ + void DissolveReferences() { Unlink(); } + + // Cycle collection + void Traverse(nsCycleCollectionTraversalCallback* aCallback); + void Unlink(); + + using RemovalTestFunction = bool (*)(SMILInstanceTime* aInstance); + + protected: + // Typedefs + using TimeValueSpecList = nsTArray>; + using InstanceTimeList = nsTArray>; + using IntervalList = nsTArray>; + using TimeValueSpecPtrKey = nsPtrHashKey; + using TimeValueSpecHashSet = nsTHashtable; + + // Helper classes + class InstanceTimeComparator { + public: + bool Equals(const SMILInstanceTime* aElem1, + const SMILInstanceTime* aElem2) const; + bool LessThan(const SMILInstanceTime* aElem1, + const SMILInstanceTime* aElem2) const; + }; + + // Templated helper functions + template + void RemoveInstanceTimes(InstanceTimeList& aArray, TestFunctor& aTest); + + // + // Implementation helpers + // + + nsresult SetBeginSpec(const nsAString& aBeginSpec, Element& aContextElement, + RemovalTestFunction aRemove); + nsresult SetEndSpec(const nsAString& aEndSpec, Element& aContextElement, + RemovalTestFunction aRemove); + nsresult SetSimpleDuration(const nsAString& aDurSpec); + nsresult SetMin(const nsAString& aMinSpec); + nsresult SetMax(const nsAString& aMaxSpec); + nsresult SetRestart(const nsAString& aRestartSpec); + nsresult SetRepeatCount(const nsAString& aRepeatCountSpec); + nsresult SetRepeatDur(const nsAString& aRepeatDurSpec); + nsresult SetFillMode(const nsAString& aFillModeSpec); + + void UnsetBeginSpec(RemovalTestFunction aRemove); + void UnsetEndSpec(RemovalTestFunction aRemove); + void UnsetSimpleDuration(); + void UnsetMin(); + void UnsetMax(); + void UnsetRestart(); + void UnsetRepeatCount(); + void UnsetRepeatDur(); + void UnsetFillMode(); + + nsresult SetBeginOrEndSpec(const nsAString& aSpec, Element& aContextElement, + bool aIsBegin, RemovalTestFunction aRemove); + void ClearSpecs(TimeValueSpecList& aSpecs, InstanceTimeList& aInstances, + RemovalTestFunction aRemove); + void ClearIntervals(); + void DoSampleAt(SMILTime aContainerTime, bool aEndOnly); + + /** + * Helper function to check for an early end and, if necessary, update the + * current interval accordingly. + * + * See SMIL 3.0, section 5.4.5, Element life cycle, "Active Time - Playing an + * interval" for a description of ending early. + * + * @param aSampleTime The current sample time. Early ends should only be + * applied at the last possible moment (i.e. if they are at + * or before the current sample time) and only if the + * current interval is not already ending. + * @return true if the end time of the current interval was updated, + * false otherwise. + */ + bool ApplyEarlyEnd(const SMILTimeValue& aSampleTime); + + /** + * Clears certain state in response to the element restarting. + * + * This state is described in SMIL 3.0, section 5.4.3, Resetting element state + */ + void Reset(); + + /** + * Clears all accumulated timing state except for those instance times for + * which aRemove does not return true. + * + * Unlike the Reset method which only clears instance times, this clears the + * element's state, intervals (including current interval), and tells the + * client animation function to stop applying a result. In effect, it returns + * the element to its initial state but preserves any instance times excluded + * by the passed-in function. + */ + void ClearTimingState(RemovalTestFunction aRemove); + + /** + * Recreates timing state by re-applying begin/end attributes specified on + * the associated animation element. + * + * Note that this does not completely restore the information cleared by + * ClearTimingState since it leaves the element in the startup state. + * The element state will be updated on the next sample. + */ + void RebuildTimingState(RemovalTestFunction aRemove); + + /** + * Completes a seek operation by sending appropriate events and, in the case + * of a backwards seek, updating the state of timing information that was + * previously considered historical. + */ + void DoPostSeek(); + + /** + * Unmarks instance times that were previously preserved because they were + * considered important historical milestones but are no longer such because + * a backwards seek has been performed. + */ + void UnpreserveInstanceTimes(InstanceTimeList& aList); + + /** + * Helper function to iterate through this element's accumulated timing + * information (specifically old SMILIntervals and SMILTimeInstanceTimes) + * and discard items that are no longer needed or exceed some threshold of + * accumulated state. + */ + void FilterHistory(); + + // Helper functions for FilterHistory to clear old SMILIntervals and + // SMILInstanceTimes respectively. + void FilterIntervals(); + void FilterInstanceTimes(InstanceTimeList& aList); + + /** + * Calculates the next acceptable interval for this element after the + * specified interval, or, if no previous interval is specified, it will be + * the first interval with an end time after t=0. + * + * @see SMILANIM 3.6.8 + * + * @param aPrevInterval The previous interval used. If supplied, the first + * interval that begins after aPrevInterval will be + * returned. May be nullptr. + * @param aReplacedInterval The interval that is being updated (if any). This + * used to ensure we don't return interval endpoints + * that are dependent on themselves. May be nullptr. + * @param aFixedBeginTime The time to use for the start of the interval. This + * is used when only the endpoint of the interval + * should be updated such as when the animation is in + * the ACTIVE state. May be nullptr. + * @param[out] aResult The next interval. Will be unchanged if no suitable + * interval was found (in which case false will be + * returned). + * @return true if a suitable interval was found, false otherwise. + */ + bool GetNextInterval(const SMILInterval* aPrevInterval, + const SMILInterval* aReplacedInterval, + const SMILInstanceTime* aFixedBeginTime, + SMILInterval& aResult) const; + SMILInstanceTime* GetNextGreater(const InstanceTimeList& aList, + const SMILTimeValue& aBase, + int32_t& aPosition) const; + SMILInstanceTime* GetNextGreaterOrEqual(const InstanceTimeList& aList, + const SMILTimeValue& aBase, + int32_t& aPosition) const; + SMILTimeValue CalcActiveEnd(const SMILTimeValue& aBegin, + const SMILTimeValue& aEnd) const; + SMILTimeValue GetRepeatDuration() const; + SMILTimeValue ApplyMinAndMax(const SMILTimeValue& aDuration) const; + SMILTime ActiveTimeToSimpleTime(SMILTime aActiveTime, + uint32_t& aRepeatIteration); + SMILInstanceTime* CheckForEarlyEnd(const SMILTimeValue& aContainerTime) const; + void UpdateCurrentInterval(bool aForceChangeNotice = false); + void SampleSimpleTime(SMILTime aActiveTime); + void SampleFillValue(); + nsresult AddInstanceTimeFromCurrentTime(SMILTime aCurrentTime, + double aOffsetSeconds, bool aIsBegin); + void RegisterMilestone(); + bool GetNextMilestone(SMILMilestone& aNextMilestone) const; + + // Notification methods. Note that these notifications can result in nested + // calls to this same object. Therefore, + // (i) we should not perform notification until this object is in + // a consistent state to receive callbacks, and + // (ii) after calling these methods we must assume that the state of the + // element may have changed. + void NotifyNewInterval(); + void NotifyChangedInterval(SMILInterval* aInterval, bool aBeginObjectChanged, + bool aEndObjectChanged); + + void FireTimeEventAsync(EventMessage aMsg, int32_t aDetail); + const SMILInstanceTime* GetEffectiveBeginInstance() const; + const SMILInterval* GetPreviousInterval() const; + bool HasPlayed() const { return !mOldIntervals.IsEmpty(); } + bool HasClientInFillRange() const; + bool EndHasEventConditions() const; + bool AreEndTimesDependentOn(const SMILInstanceTime* aBase) const; + + // Reset the current interval by first passing ownership to a temporary + // variable so that if Unlink() results in us receiving a callback, + // mCurrentInterval will be nullptr and we will be in a consistent state. + void ResetCurrentInterval() { + if (mCurrentInterval) { + // Transfer ownership to temp var. (This sets mCurrentInterval to null.) + auto interval = std::move(mCurrentInterval); + interval->Unlink(); + } + } + + // + // Members + // + mozilla::dom::SVGAnimationElement* mAnimationElement; // [weak] won't outlive + // owner + TimeValueSpecList mBeginSpecs; // [strong] + TimeValueSpecList mEndSpecs; // [strong] + + SMILTimeValue mSimpleDur; + + SMILRepeatCount mRepeatCount; + SMILTimeValue mRepeatDur; + + SMILTimeValue mMin; + SMILTimeValue mMax; + + enum SMILFillMode : uint8_t { FILL_REMOVE, FILL_FREEZE }; + SMILFillMode mFillMode; + static const nsAttrValue::EnumTable sFillModeTable[]; + + enum SMILRestartMode : uint8_t { + RESTART_ALWAYS, + RESTART_WHENNOTACTIVE, + RESTART_NEVER + }; + SMILRestartMode mRestartMode; + static const nsAttrValue::EnumTable sRestartModeTable[]; + + InstanceTimeList mBeginInstances; + InstanceTimeList mEndInstances; + uint32_t mInstanceSerialIndex; + + SMILAnimationFunction* mClient; + UniquePtr mCurrentInterval; + IntervalList mOldIntervals; + uint32_t mCurrentRepeatIteration; + SMILMilestone mPrevRegisteredMilestone; + static const SMILMilestone sMaxMilestone; + static const uint8_t sMaxNumIntervals; + static const uint8_t sMaxNumInstanceTimes; + + // Set of dependent time value specs to be notified when establishing a new + // current interval. Change notifications and delete notifications are handled + // by the interval. + // + // [weak] The SMILTimeValueSpec objects register themselves and unregister + // on destruction. Likewise, we notify them when we are destroyed. + TimeValueSpecHashSet mTimeDependents; + + /** + * The state of the element in its life-cycle. These states are based on the + * element life-cycle described in SMILANIM 3.6.8 + */ + enum SMILElementState { + STATE_STARTUP, + STATE_WAITING, + STATE_ACTIVE, + STATE_POSTACTIVE + }; + SMILElementState mElementState; + + enum SMILSeekState { + SEEK_NOT_SEEKING, + SEEK_FORWARD_FROM_ACTIVE, + SEEK_FORWARD_FROM_INACTIVE, + SEEK_BACKWARD_FROM_ACTIVE, + SEEK_BACKWARD_FROM_INACTIVE + }; + SMILSeekState mSeekState; + + // Used to batch updates to the timing model + class AutoIntervalUpdateBatcher; + bool mDeferIntervalUpdates; + bool mDoDeferredUpdate; // Set if an update to the current interval was + // requested while mDeferIntervalUpdates was set + bool mIsDisabled; + + // Stack-based helper class to call UpdateCurrentInterval when it is destroyed + class AutoIntervalUpdater; + + // Recursion depth checking + uint8_t mDeleteCount; + uint8_t mUpdateIntervalRecursionDepth; + static const uint8_t sMaxUpdateIntervalRecursionDepth; +}; + +inline void ImplCycleCollectionUnlink(SMILTimedElement& aField) { + aField.Unlink(); +} + +inline void ImplCycleCollectionTraverse( + nsCycleCollectionTraversalCallback& aCallback, SMILTimedElement& aField, + const char* aName, uint32_t aFlags = 0) { + aField.Traverse(&aCallback); +} + +} // namespace mozilla + +#endif // DOM_SMIL_SMILTIMEDELEMENT_H_ diff --git a/dom/smil/SMILType.h b/dom/smil/SMILType.h new file mode 100644 index 0000000000..5aef1e7623 --- /dev/null +++ b/dom/smil/SMILType.h @@ -0,0 +1,212 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILTYPE_H_ +#define DOM_SMIL_SMILTYPE_H_ + +#include "mozilla/Attributes.h" +#include "nscore.h" + +namespace mozilla { + +class SMILValue; + +////////////////////////////////////////////////////////////////////////////// +// SMILType: Interface for defining the basic operations needed for animating +// a particular kind of data (e.g. lengths, colors, transformation matrices). +// +// This interface is never used directly but always through a SMILValue that +// bundles together a pointer to a concrete implementation of this interface and +// the data upon which it should operate. +// +// We keep the data and type separate rather than just providing different +// subclasses of SMILValue. This is so that sizeof(SMILValue) is the same +// for all value types, allowing us to have a type-agnostic nsTArray of +// SMILValue objects (actual objects, not pointers). It also allows most +// SMILValues (except those that need to allocate extra memory for their +// data) to be allocated on the stack and directly assigned to one another +// provided performance benefits for the animation code. +// +// Note that different types have different capabilities. Roughly speaking there +// are probably three main types: +// +// +---------------------+---------------+-------------+------------------+ +// | CATEGORY: | DISCRETE | LINEAR | ADDITIVE | +// +---------------------+---------------+-------------+------------------+ +// | Example: | strings, | path data? | lengths, | +// | | color k/words?| | RGB color values | +// | | | | | +// | -- Assign? | X | X | X | +// | -- Add? | - | X? | X | +// | -- SandwichAdd? | - | -? | X | +// | -- ComputeDistance? | - | - | X? | +// | -- Interpolate? | - | X | X | +// +---------------------+---------------+-------------+------------------+ +// + +class SMILType { + /** + * Only give the SMILValue class access to this interface. + */ + friend class SMILValue; + + protected: + /** + * Initialises aValue and sets it to some identity value such that adding + * aValue to another value of the same type has no effect. + * + * @pre aValue.IsNull() + * @post aValue.mType == this + */ + virtual void Init(SMILValue& aValue) const = 0; + + /** + * Destroys any data associated with a value of this type. + * + * @pre aValue.mType == this + * @post aValue.IsNull() + */ + virtual void Destroy(SMILValue& aValue) const = 0; + + /** + * Assign this object the value of another. Think of this as the assignment + * operator. + * + * @param aDest The left-hand side of the assignment. + * @param aSrc The right-hand side of the assignment. + * @return NS_OK on success, an error code on failure such as when the + * underlying type of the specified object differs. + * + * @pre aDest.mType == aSrc.mType == this + */ + virtual nsresult Assign(SMILValue& aDest, const SMILValue& aSrc) const = 0; + + /** + * Test two SMILValue objects (of this SMILType) for equality. + * + * A return value of true represents a guarantee that aLeft and aRight are + * equal. (That is, they would behave identically if passed to the methods + * Add, SandwichAdd, ComputeDistance, and Interpolate). + * + * A return value of false simply indicates that we make no guarantee + * about equality. + * + * NOTE: It's perfectly legal for implementations of this method to return + * false in all cases. However, smarter implementations will make this + * method more useful for optimization. + * + * @param aLeft The left-hand side of the equality check. + * @param aRight The right-hand side of the equality check. + * @return true if we're sure the values are equal, false otherwise. + * + * @pre aDest.mType == aSrc.mType == this + */ + virtual bool IsEqual(const SMILValue& aLeft, + const SMILValue& aRight) const = 0; + + /** + * Adds two values. + * + * The count parameter facilitates repetition. + * + * By equation, + * + * aDest += aValueToAdd * aCount + * + * Therefore, if aCount == 0, aDest will be unaltered. + * + * This method will fail if this data type is not additive or the value was + * not specified using an additive syntax. + * + * See SVG 1.1, section 19.2.5. In particular, + * + * "If a given attribute or property can take values of keywords (which are + * not additive) or numeric values (which are additive), then additive + * animations are possible if the subsequent animation uses a numeric value + * even if the base animation uses a keyword value; however, if the + * subsequent animation uses a keyword value, additive animation is not + * possible." + * + * If this method fails (e.g. because the data type is not additive), aDest + * will be unaltered. + * + * @param aDest The value to add to. + * @param aValueToAdd The value to add. + * @param aCount The number of times to add aValueToAdd. + * @return NS_OK on success, an error code on failure. + * + * @pre aValueToAdd.mType == aDest.mType == this + */ + virtual nsresult Add(SMILValue& aDest, const SMILValue& aValueToAdd, + uint32_t aCount) const = 0; + + /** + * Adds aValueToAdd to the underlying value in the animation sandwich, aDest. + * + * For most types this operation is identical to a regular Add() but for some + * types (notably ) the operation differs. For + * Add() corresponds to simply adding together the + * transform parameters and is used when calculating cumulative values or + * by-animation values. On the other hand SandwichAdd() is used when adding to + * the underlying value and requires matrix post-multiplication. (This + * distinction is most clearly indicated by the SVGT1.2 test suite. It is not + * obvious within the SMIL specifications.) + * + * @param aDest The value to add to. + * @param aValueToAdd The value to add. + * @return NS_OK on success, an error code on failure. + * + * @pre aValueToAdd.mType == aDest.mType == this + */ + virtual nsresult SandwichAdd(SMILValue& aDest, + const SMILValue& aValueToAdd) const { + return Add(aDest, aValueToAdd, 1); + } + + /** + * Calculates the 'distance' between two values. This is the distance used in + * paced interpolation. + * + * @param aFrom The start of the interval for which the distance should + * be calculated. + * @param aTo The end of the interval for which the distance should be + * calculated. + * @param aDistance The result of the calculation. + * @return NS_OK on success, or an appropriate error code if there is no + * notion of distance for the underlying data type or the distance + * could not be calculated. + * + * @pre aFrom.mType == aTo.mType == this + */ + virtual nsresult ComputeDistance(const SMILValue& aFrom, const SMILValue& aTo, + double& aDistance) const = 0; + + /** + * Calculates an interpolated value between two values using the specified + * proportion. + * + * @param aStartVal The value defining the start of the interval of + * interpolation. + * @param aEndVal The value defining the end of the interval of + * interpolation. + * @param aUnitDistance A number between 0.0 and 1.0 (inclusive) defining + * the distance of the interpolated value in the + * interval. + * @param aResult The interpolated value. + * @return NS_OK on success, NS_ERROR_FAILURE if this data type cannot be + * interpolated or NS_ERROR_OUT_OF_MEMORY if insufficient memory was + * available for storing the result. + * + * @pre aStartVal.mType == aEndVal.mType == aResult.mType == this + */ + virtual nsresult Interpolate(const SMILValue& aStartVal, + const SMILValue& aEndVal, double aUnitDistance, + SMILValue& aResult) const = 0; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILTYPE_H_ diff --git a/dom/smil/SMILTypes.h b/dom/smil/SMILTypes.h new file mode 100644 index 0000000000..6391872f9e --- /dev/null +++ b/dom/smil/SMILTypes.h @@ -0,0 +1,30 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILTYPES_H_ +#define DOM_SMIL_SMILTYPES_H_ + +#include + +namespace mozilla { + +// A timestamp in milliseconds +// +// A time may represent: +// +// simple time -- offset within the simple duration +// active time -- offset within the active duration +// document time -- offset since the document begin +// wallclock time -- "real" time -- offset since the epoch +// +// For an overview of how this class is related to other SMIL time classes see +// the documentation in SMILTimeValue.h +// +using SMILTime = int64_t; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILTYPES_H_ diff --git a/dom/smil/SMILValue.cpp b/dom/smil/SMILValue.cpp new file mode 100644 index 0000000000..4942e3b8b6 --- /dev/null +++ b/dom/smil/SMILValue.cpp @@ -0,0 +1,142 @@ +/* -*- 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 "SMILValue.h" + +#include "nsDebug.h" +#include + +namespace mozilla { + +//---------------------------------------------------------------------- +// Public methods + +SMILValue::SMILValue(const SMILType* aType) : mType(SMILNullType::Singleton()) { + mU.mBool = false; + if (!aType) { + NS_ERROR("Trying to construct SMILValue with null mType pointer"); + return; + } + + InitAndCheckPostcondition(aType); +} + +SMILValue::SMILValue(const SMILValue& aVal) : mType(SMILNullType::Singleton()) { + InitAndCheckPostcondition(aVal.mType); + mType->Assign(*this, aVal); +} + +const SMILValue& SMILValue::operator=(const SMILValue& aVal) { + if (&aVal == this) return *this; + + if (mType != aVal.mType) { + DestroyAndReinit(aVal.mType); + } + + mType->Assign(*this, aVal); + + return *this; +} + +// Move constructor / reassignment operator: +SMILValue::SMILValue(SMILValue&& aVal) noexcept + : mU(aVal.mU), // Copying union is only OK because we clear aVal.mType + // below. + mType(aVal.mType) { + // Leave aVal with a null type, so that it's safely destructible (and won't + // mess with anything referenced by its union, which we've copied). + aVal.mType = SMILNullType::Singleton(); +} + +SMILValue& SMILValue::operator=(SMILValue&& aVal) noexcept { + if (!IsNull()) { + // Clean up any data we're currently tracking. + DestroyAndCheckPostcondition(); + } + + // Copy the union (which could include a pointer to external memory) & mType: + mU = aVal.mU; + mType = aVal.mType; + + // Leave aVal with a null type, so that it's safely destructible (and won't + // mess with anything referenced by its union, which we've now copied). + aVal.mType = SMILNullType::Singleton(); + + return *this; +} + +bool SMILValue::operator==(const SMILValue& aVal) const { + if (&aVal == this) return true; + + return mType == aVal.mType && mType->IsEqual(*this, aVal); +} + +nsresult SMILValue::Add(const SMILValue& aValueToAdd, uint32_t aCount) { + if (aValueToAdd.mType != mType) { + NS_ERROR("Trying to add incompatible types"); + return NS_ERROR_FAILURE; + } + + return mType->Add(*this, aValueToAdd, aCount); +} + +nsresult SMILValue::SandwichAdd(const SMILValue& aValueToAdd) { + if (aValueToAdd.mType != mType) { + NS_ERROR("Trying to add incompatible types"); + return NS_ERROR_FAILURE; + } + + return mType->SandwichAdd(*this, aValueToAdd); +} + +nsresult SMILValue::ComputeDistance(const SMILValue& aTo, + double& aDistance) const { + if (aTo.mType != mType) { + NS_ERROR("Trying to calculate distance between incompatible types"); + return NS_ERROR_FAILURE; + } + + return mType->ComputeDistance(*this, aTo, aDistance); +} + +nsresult SMILValue::Interpolate(const SMILValue& aEndVal, double aUnitDistance, + SMILValue& aResult) const { + if (aEndVal.mType != mType) { + NS_ERROR("Trying to interpolate between incompatible types"); + return NS_ERROR_FAILURE; + } + + if (aResult.mType != mType) { + // Outparam has wrong type + aResult.DestroyAndReinit(mType); + } + + return mType->Interpolate(*this, aEndVal, aUnitDistance, aResult); +} + +//---------------------------------------------------------------------- +// Helper methods + +// Wrappers for SMILType::Init & ::Destroy that verify their postconditions +void SMILValue::InitAndCheckPostcondition(const SMILType* aNewType) { + aNewType->Init(*this); + MOZ_ASSERT(mType == aNewType, + "Post-condition of Init failed. SMILValue is invalid"); +} + +void SMILValue::DestroyAndCheckPostcondition() { + mType->Destroy(*this); + MOZ_ASSERT(IsNull(), + "Post-condition of Destroy failed. " + "SMILValue not null after destroying"); +} + +void SMILValue::DestroyAndReinit(const SMILType* aNewType) { + DestroyAndCheckPostcondition(); + InitAndCheckPostcondition(aNewType); +} + +} // namespace mozilla diff --git a/dom/smil/SMILValue.h b/dom/smil/SMILValue.h new file mode 100644 index 0000000000..d57daa5eb3 --- /dev/null +++ b/dom/smil/SMILValue.h @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILVALUE_H_ +#define DOM_SMIL_SMILVALUE_H_ + +#include "mozilla/SMILNullType.h" +#include "mozilla/SMILType.h" + +namespace mozilla { + +/** + * Although objects of this type are generally only created on the stack and + * only exist during the taking of a new time sample, that's not always the + * case. The SMILValue objects obtained from attributes' base values are + * cached so that the SMIL engine can make certain optimizations during a + * sample if the base value has not changed since the last sample (potentially + * avoiding recomposing). These SMILValue objects typically live much longer + * than a single sample. + */ +class SMILValue { + public: + SMILValue() : mU(), mType(SMILNullType::Singleton()) {} + explicit SMILValue(const SMILType* aType); + SMILValue(const SMILValue& aVal); + + ~SMILValue() { mType->Destroy(*this); } + + const SMILValue& operator=(const SMILValue& aVal); + + // Move constructor / reassignment operator: + SMILValue(SMILValue&& aVal) noexcept; + SMILValue& operator=(SMILValue&& aVal) noexcept; + + // Equality operators. These are allowed to be conservative (return false + // more than you'd expect) - see comment above SMILType::IsEqual. + bool operator==(const SMILValue& aVal) const; + bool operator!=(const SMILValue& aVal) const { return !(*this == aVal); } + + bool IsNull() const { return (mType == SMILNullType::Singleton()); } + + nsresult Add(const SMILValue& aValueToAdd, uint32_t aCount = 1); + nsresult SandwichAdd(const SMILValue& aValueToAdd); + nsresult ComputeDistance(const SMILValue& aTo, double& aDistance) const; + nsresult Interpolate(const SMILValue& aEndVal, double aUnitDistance, + SMILValue& aResult) const; + + union { + bool mBool; + uint64_t mUint; + int64_t mInt; + double mDouble; + struct { + float mAngle; + uint16_t mUnit; + uint16_t mOrientType; + } mOrient; + int32_t mIntPair[2]; + float mNumberPair[2]; + void* mPtr; + } mU; + const SMILType* mType; + + protected: + void InitAndCheckPostcondition(const SMILType* aNewType); + void DestroyAndCheckPostcondition(); + void DestroyAndReinit(const SMILType* aNewType); +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILVALUE_H_ diff --git a/dom/smil/TimeEvent.cpp b/dom/smil/TimeEvent.cpp new file mode 100644 index 0000000000..cbb6e72d92 --- /dev/null +++ b/dom/smil/TimeEvent.cpp @@ -0,0 +1,57 @@ +/* -*- 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 "mozilla/ContentEvents.h" +#include "mozilla/dom/TimeEvent.h" +#include "nsIDocShell.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsPresContext.h" +#include "nsGlobalWindowInner.h" + +namespace mozilla::dom { + +TimeEvent::TimeEvent(EventTarget* aOwner, nsPresContext* aPresContext, + InternalSMILTimeEvent* aEvent) + : Event(aOwner, aPresContext, + aEvent ? aEvent : new InternalSMILTimeEvent(false, eVoidEvent)), + mDetail(mEvent->AsSMILTimeEvent()->mDetail) { + if (aEvent) { + mEventIsInternal = false; + } else { + mEventIsInternal = true; + } + + if (mPresContext) { + nsCOMPtr docShell = mPresContext->GetDocShell(); + if (docShell) { + mView = docShell->GetWindow(); + } + } +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(TimeEvent, Event, mView) + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(TimeEvent, Event) + +void TimeEvent::InitTimeEvent(const nsAString& aType, + nsGlobalWindowInner* aView, int32_t aDetail) { + NS_ENSURE_TRUE_VOID(!mEvent->mFlags.mIsBeingDispatched); + + Event::InitEvent(aType, false /*doesn't bubble*/, false /*can't cancel*/); + mDetail = aDetail; + mView = aView ? aView->GetOuterWindow() : nullptr; +} + +} // namespace mozilla::dom + +using namespace mozilla; +using namespace mozilla::dom; + +already_AddRefed NS_NewDOMTimeEvent(EventTarget* aOwner, + nsPresContext* aPresContext, + InternalSMILTimeEvent* aEvent) { + return do_AddRef(new TimeEvent(aOwner, aPresContext, aEvent)); +} diff --git a/dom/smil/TimeEvent.h b/dom/smil/TimeEvent.h new file mode 100644 index 0000000000..3b82e76378 --- /dev/null +++ b/dom/smil/TimeEvent.h @@ -0,0 +1,61 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_TIMEEVENT_H_ +#define DOM_SMIL_TIMEEVENT_H_ + +#include "nsDocShell.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/TimeEventBinding.h" +#include "mozilla/dom/Nullable.h" +#include "mozilla/dom/WindowProxyHolder.h" + +class nsGlobalWindowInner; + +namespace mozilla::dom { + +class TimeEvent final : public Event { + public: + TimeEvent(EventTarget* aOwner, nsPresContext* aPresContext, + InternalSMILTimeEvent* aEvent); + + // nsISupports interface: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(TimeEvent, Event) + + JSObject* WrapObjectInternal(JSContext* aCx, + JS::Handle aGivenProto) override { + return TimeEvent_Binding::Wrap(aCx, this, aGivenProto); + } + + void InitTimeEvent(const nsAString& aType, nsGlobalWindowInner* aView, + int32_t aDetail); + + int32_t Detail() const { return mDetail; } + + Nullable GetView() const { + if (!mView) { + return nullptr; + } + return WindowProxyHolder(mView->GetBrowsingContext()); + } + + TimeEvent* AsTimeEvent() final { return this; } + + private: + ~TimeEvent() = default; + + nsCOMPtr mView; + int32_t mDetail; +}; + +} // namespace mozilla::dom + +already_AddRefed NS_NewDOMTimeEvent( + mozilla::dom::EventTarget* aOwner, nsPresContext* aPresContext, + mozilla::InternalSMILTimeEvent* aEvent); + +#endif // DOM_SMIL_TIMEEVENT_H_ diff --git a/dom/smil/crashtests/1010681-1.svg b/dom/smil/crashtests/1010681-1.svg new file mode 100644 index 0000000000..882bcb53fe --- /dev/null +++ b/dom/smil/crashtests/1010681-1.svg @@ -0,0 +1,23 @@ + + + diff --git a/dom/smil/crashtests/1322770-1.svg b/dom/smil/crashtests/1322770-1.svg new file mode 100644 index 0000000000..405435184e --- /dev/null +++ b/dom/smil/crashtests/1322770-1.svg @@ -0,0 +1,3 @@ + + + diff --git a/dom/smil/crashtests/1322849-1.svg b/dom/smil/crashtests/1322849-1.svg new file mode 100644 index 0000000000..ea3d629813 --- /dev/null +++ b/dom/smil/crashtests/1322849-1.svg @@ -0,0 +1,2 @@ + + diff --git a/dom/smil/crashtests/1343357-1.html b/dom/smil/crashtests/1343357-1.html new file mode 100644 index 0000000000..8219c31221 --- /dev/null +++ b/dom/smil/crashtests/1343357-1.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dom/smil/crashtests/1375596-1.svg b/dom/smil/crashtests/1375596-1.svg new file mode 100644 index 0000000000..69c1673d11 --- /dev/null +++ b/dom/smil/crashtests/1375596-1.svg @@ -0,0 +1,3 @@ + + + diff --git a/dom/smil/crashtests/1402547-1.html b/dom/smil/crashtests/1402547-1.html new file mode 100644 index 0000000000..28fa7ce185 --- /dev/null +++ b/dom/smil/crashtests/1402547-1.html @@ -0,0 +1,3 @@ + + + diff --git a/dom/smil/crashtests/1411963-1.html b/dom/smil/crashtests/1411963-1.html new file mode 100644 index 0000000000..cc61b73e2a --- /dev/null +++ b/dom/smil/crashtests/1411963-1.html @@ -0,0 +1,10 @@ + + + + + diff --git a/dom/smil/crashtests/1413319-1.html b/dom/smil/crashtests/1413319-1.html new file mode 100644 index 0000000000..9bfeef3bdd --- /dev/null +++ b/dom/smil/crashtests/1413319-1.html @@ -0,0 +1,2 @@ + + diff --git a/dom/smil/crashtests/1535388-1.html b/dom/smil/crashtests/1535388-1.html new file mode 100644 index 0000000000..cdfaba4d90 --- /dev/null +++ b/dom/smil/crashtests/1535388-1.html @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/dom/smil/crashtests/1772573-1.html b/dom/smil/crashtests/1772573-1.html new file mode 100644 index 0000000000..ef7783e51b --- /dev/null +++ b/dom/smil/crashtests/1772573-1.html @@ -0,0 +1,18 @@ + + + + + + diff --git a/dom/smil/crashtests/1780800-1.html b/dom/smil/crashtests/1780800-1.html new file mode 100644 index 0000000000..14129a627c --- /dev/null +++ b/dom/smil/crashtests/1780800-1.html @@ -0,0 +1,22 @@ + + + + + + diff --git a/dom/smil/crashtests/483584-1.svg b/dom/smil/crashtests/483584-1.svg new file mode 100644 index 0000000000..b9ded113ef --- /dev/null +++ b/dom/smil/crashtests/483584-1.svg @@ -0,0 +1,8 @@ + + + + + + + diff --git a/dom/smil/crashtests/483584-2.svg b/dom/smil/crashtests/483584-2.svg new file mode 100644 index 0000000000..f5cbd7d466 --- /dev/null +++ b/dom/smil/crashtests/483584-2.svg @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $Revision: 1.6 $ + + diff --git a/dom/smil/crashtests/523188-1.svg b/dom/smil/crashtests/523188-1.svg new file mode 100644 index 0000000000..c03cea4923 --- /dev/null +++ b/dom/smil/crashtests/523188-1.svg @@ -0,0 +1,15 @@ + + + + + + + diff --git a/dom/smil/crashtests/525099-1.svg b/dom/smil/crashtests/525099-1.svg new file mode 100644 index 0000000000..8eed11489a --- /dev/null +++ b/dom/smil/crashtests/525099-1.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/dom/smil/crashtests/526536-1.svg b/dom/smil/crashtests/526536-1.svg new file mode 100644 index 0000000000..4fcf35d081 --- /dev/null +++ b/dom/smil/crashtests/526536-1.svg @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/dom/smil/crashtests/526875-1.svg b/dom/smil/crashtests/526875-1.svg new file mode 100644 index 0000000000..281454bf61 --- /dev/null +++ b/dom/smil/crashtests/526875-1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/dom/smil/crashtests/526875-2.svg b/dom/smil/crashtests/526875-2.svg new file mode 100644 index 0000000000..73c229da5f --- /dev/null +++ b/dom/smil/crashtests/526875-2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/dom/smil/crashtests/529387-1-helper.svg b/dom/smil/crashtests/529387-1-helper.svg new file mode 100644 index 0000000000..7885ab71fd --- /dev/null +++ b/dom/smil/crashtests/529387-1-helper.svg @@ -0,0 +1,5 @@ + + abc + + + diff --git a/dom/smil/crashtests/529387-1.xhtml b/dom/smil/crashtests/529387-1.xhtml new file mode 100644 index 0000000000..de3dbec34c --- /dev/null +++ b/dom/smil/crashtests/529387-1.xhtml @@ -0,0 +1,7 @@ + + + diff --git a/dom/smil/crashtests/531550-1.svg b/dom/smil/crashtests/531550-1.svg new file mode 100644 index 0000000000..306f41702d --- /dev/null +++ b/dom/smil/crashtests/531550-1.svg @@ -0,0 +1,3 @@ + + + diff --git a/dom/smil/crashtests/541297-1.svg b/dom/smil/crashtests/541297-1.svg new file mode 100644 index 0000000000..4268232ba1 --- /dev/null +++ b/dom/smil/crashtests/541297-1.svg @@ -0,0 +1,22 @@ + + diff --git a/dom/smil/crashtests/547333-1.svg b/dom/smil/crashtests/547333-1.svg new file mode 100644 index 0000000000..bac629b493 --- /dev/null +++ b/dom/smil/crashtests/547333-1.svg @@ -0,0 +1,22 @@ + + + + +abc + + diff --git a/dom/smil/crashtests/548899-1.svg b/dom/smil/crashtests/548899-1.svg new file mode 100644 index 0000000000..c12ed27454 --- /dev/null +++ b/dom/smil/crashtests/548899-1.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/dom/smil/crashtests/551620-1.svg b/dom/smil/crashtests/551620-1.svg new file mode 100644 index 0000000000..2ea83e9c29 --- /dev/null +++ b/dom/smil/crashtests/551620-1.svg @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/dom/smil/crashtests/554141-1.svg b/dom/smil/crashtests/554141-1.svg new file mode 100644 index 0000000000..61ce419f53 --- /dev/null +++ b/dom/smil/crashtests/554141-1.svg @@ -0,0 +1,12 @@ + + + + + + diff --git a/dom/smil/crashtests/554202-2.svg b/dom/smil/crashtests/554202-2.svg new file mode 100644 index 0000000000..a3bbb3195c --- /dev/null +++ b/dom/smil/crashtests/554202-2.svg @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/dom/smil/crashtests/555026-1.svg b/dom/smil/crashtests/555026-1.svg new file mode 100644 index 0000000000..76b4cf0756 --- /dev/null +++ b/dom/smil/crashtests/555026-1.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + diff --git a/dom/smil/crashtests/556841-1.svg b/dom/smil/crashtests/556841-1.svg new file mode 100644 index 0000000000..92712deaa9 --- /dev/null +++ b/dom/smil/crashtests/556841-1.svg @@ -0,0 +1,16 @@ + + + + + + + diff --git a/dom/smil/crashtests/572938-1.svg b/dom/smil/crashtests/572938-1.svg new file mode 100644 index 0000000000..d759944c7d --- /dev/null +++ b/dom/smil/crashtests/572938-1.svg @@ -0,0 +1,12 @@ + + + Used Text Element + + + + + Normal Text Element + + + diff --git a/dom/smil/crashtests/572938-2.svg b/dom/smil/crashtests/572938-2.svg new file mode 100644 index 0000000000..8b9cf7b70e --- /dev/null +++ b/dom/smil/crashtests/572938-2.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + diff --git a/dom/smil/crashtests/572938-3.svg b/dom/smil/crashtests/572938-3.svg new file mode 100644 index 0000000000..642ad32fba --- /dev/null +++ b/dom/smil/crashtests/572938-3.svg @@ -0,0 +1,10 @@ + + + Text A + Text B + + + + + diff --git a/dom/smil/crashtests/572938-4.svg b/dom/smil/crashtests/572938-4.svg new file mode 100644 index 0000000000..549d43dd62 --- /dev/null +++ b/dom/smil/crashtests/572938-4.svg @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/dom/smil/crashtests/588287-1.svg b/dom/smil/crashtests/588287-1.svg new file mode 100644 index 0000000000..cc35cf6b46 --- /dev/null +++ b/dom/smil/crashtests/588287-1.svg @@ -0,0 +1,24 @@ + + + + diff --git a/dom/smil/crashtests/588287-2.svg b/dom/smil/crashtests/588287-2.svg new file mode 100644 index 0000000000..70d8e76391 --- /dev/null +++ b/dom/smil/crashtests/588287-2.svg @@ -0,0 +1,26 @@ + + + + diff --git a/dom/smil/crashtests/590425-1.html b/dom/smil/crashtests/590425-1.html new file mode 100644 index 0000000000..906d348db2 --- /dev/null +++ b/dom/smil/crashtests/590425-1.html @@ -0,0 +1,24 @@ + + + + + + + + + + + + diff --git a/dom/smil/crashtests/594653-1.svg b/dom/smil/crashtests/594653-1.svg new file mode 100644 index 0000000000..76352ce30b --- /dev/null +++ b/dom/smil/crashtests/594653-1.svg @@ -0,0 +1,26 @@ + + + + + + + + + diff --git a/dom/smil/crashtests/596796-1.svg b/dom/smil/crashtests/596796-1.svg new file mode 100644 index 0000000000..52a66fd582 --- /dev/null +++ b/dom/smil/crashtests/596796-1.svg @@ -0,0 +1,15 @@ + + + + + + + diff --git a/dom/smil/crashtests/605345-1.svg b/dom/smil/crashtests/605345-1.svg new file mode 100644 index 0000000000..94887cf713 --- /dev/null +++ b/dom/smil/crashtests/605345-1.svg @@ -0,0 +1,25 @@ + + + + + diff --git a/dom/smil/crashtests/606101-1.svg b/dom/smil/crashtests/606101-1.svg new file mode 100644 index 0000000000..988c86fa33 --- /dev/null +++ b/dom/smil/crashtests/606101-1.svg @@ -0,0 +1,23 @@ + + + diff --git a/dom/smil/crashtests/608295-1.html b/dom/smil/crashtests/608295-1.html new file mode 100644 index 0000000000..354e6f9099 --- /dev/null +++ b/dom/smil/crashtests/608295-1.html @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/dom/smil/crashtests/608549-1.svg b/dom/smil/crashtests/608549-1.svg new file mode 100644 index 0000000000..dd441e0135 --- /dev/null +++ b/dom/smil/crashtests/608549-1.svg @@ -0,0 +1,29 @@ + + + + + + + diff --git a/dom/smil/crashtests/611927-1.svg b/dom/smil/crashtests/611927-1.svg new file mode 100644 index 0000000000..ea60f4ce1c --- /dev/null +++ b/dom/smil/crashtests/611927-1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/dom/smil/crashtests/615002-1.svg b/dom/smil/crashtests/615002-1.svg new file mode 100644 index 0000000000..eb9a293199 --- /dev/null +++ b/dom/smil/crashtests/615002-1.svg @@ -0,0 +1,16 @@ + + + + diff --git a/dom/smil/crashtests/615872-1.svg b/dom/smil/crashtests/615872-1.svg new file mode 100644 index 0000000000..e0cdf21546 --- /dev/null +++ b/dom/smil/crashtests/615872-1.svg @@ -0,0 +1,21 @@ + + + diff --git a/dom/smil/crashtests/641388-1.html b/dom/smil/crashtests/641388-1.html new file mode 100644 index 0000000000..25c941dedb --- /dev/null +++ b/dom/smil/crashtests/641388-1.html @@ -0,0 +1,97 @@ + + +
+ diff --git a/dom/smil/crashtests/641388-2.html b/dom/smil/crashtests/641388-2.html new file mode 100644 index 0000000000..f2ddead7e1 --- /dev/null +++ b/dom/smil/crashtests/641388-2.html @@ -0,0 +1,79 @@ + + diff --git a/dom/smil/crashtests/650732-1.svg b/dom/smil/crashtests/650732-1.svg new file mode 100644 index 0000000000..95be31c16a --- /dev/null +++ b/dom/smil/crashtests/650732-1.svg @@ -0,0 +1,46 @@ + + + + + + + + diff --git a/dom/smil/crashtests/665334-1.svg b/dom/smil/crashtests/665334-1.svg new file mode 100644 index 0000000000..94916d1e0e --- /dev/null +++ b/dom/smil/crashtests/665334-1.svg @@ -0,0 +1,13 @@ + + + + + diff --git a/dom/smil/crashtests/669225-1.svg b/dom/smil/crashtests/669225-1.svg new file mode 100644 index 0000000000..9660105631 --- /dev/null +++ b/dom/smil/crashtests/669225-1.svg @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/dom/smil/crashtests/669225-2.svg b/dom/smil/crashtests/669225-2.svg new file mode 100644 index 0000000000..00d52c1f4c --- /dev/null +++ b/dom/smil/crashtests/669225-2.svg @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/dom/smil/crashtests/670313-1.svg b/dom/smil/crashtests/670313-1.svg new file mode 100644 index 0000000000..97e12f35ac --- /dev/null +++ b/dom/smil/crashtests/670313-1.svg @@ -0,0 +1,20 @@ + + + + + diff --git a/dom/smil/crashtests/678822-1.svg b/dom/smil/crashtests/678822-1.svg new file mode 100644 index 0000000000..a5e81ee10f --- /dev/null +++ b/dom/smil/crashtests/678822-1.svg @@ -0,0 +1,3 @@ + + + diff --git a/dom/smil/crashtests/678847-1.svg b/dom/smil/crashtests/678847-1.svg new file mode 100644 index 0000000000..1fa2718cbb --- /dev/null +++ b/dom/smil/crashtests/678847-1.svg @@ -0,0 +1,3 @@ + + + diff --git a/dom/smil/crashtests/678938-1.svg b/dom/smil/crashtests/678938-1.svg new file mode 100644 index 0000000000..f3f8308fa5 --- /dev/null +++ b/dom/smil/crashtests/678938-1.svg @@ -0,0 +1,11 @@ + + + + diff --git a/dom/smil/crashtests/690994-1.svg b/dom/smil/crashtests/690994-1.svg new file mode 100644 index 0000000000..252fd2c264 --- /dev/null +++ b/dom/smil/crashtests/690994-1.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/dom/smil/crashtests/691337-1.svg b/dom/smil/crashtests/691337-1.svg new file mode 100644 index 0000000000..c341faa6b2 --- /dev/null +++ b/dom/smil/crashtests/691337-1.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/dom/smil/crashtests/691337-2.svg b/dom/smil/crashtests/691337-2.svg new file mode 100644 index 0000000000..f4408ae5ee --- /dev/null +++ b/dom/smil/crashtests/691337-2.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/dom/smil/crashtests/697640-1.svg b/dom/smil/crashtests/697640-1.svg new file mode 100644 index 0000000000..c2e1b89fdb --- /dev/null +++ b/dom/smil/crashtests/697640-1.svg @@ -0,0 +1,3 @@ + + + diff --git a/dom/smil/crashtests/699325-1.svg b/dom/smil/crashtests/699325-1.svg new file mode 100644 index 0000000000..7496c6ae21 --- /dev/null +++ b/dom/smil/crashtests/699325-1.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/dom/smil/crashtests/709907-1.svg b/dom/smil/crashtests/709907-1.svg new file mode 100644 index 0000000000..631911970c --- /dev/null +++ b/dom/smil/crashtests/709907-1.svg @@ -0,0 +1,3 @@ + + + diff --git a/dom/smil/crashtests/720103-1.svg b/dom/smil/crashtests/720103-1.svg new file mode 100644 index 0000000000..a51a3bf0fc --- /dev/null +++ b/dom/smil/crashtests/720103-1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/dom/smil/crashtests/849593-1.xhtml b/dom/smil/crashtests/849593-1.xhtml new file mode 100644 index 0000000000..95b9b2feb8 --- /dev/null +++ b/dom/smil/crashtests/849593-1.xhtml @@ -0,0 +1,34 @@ + + + + + + + + + +
+ + diff --git a/dom/smil/crashtests/crashtests.list b/dom/smil/crashtests/crashtests.list new file mode 100644 index 0000000000..1ca739c9ae --- /dev/null +++ b/dom/smil/crashtests/crashtests.list @@ -0,0 +1,62 @@ +load 483584-1.svg +load 483584-2.svg +load 523188-1.svg +load 525099-1.svg +load 526536-1.svg +load 526875-1.svg +load 526875-2.svg +load 529387-1.xhtml +load 531550-1.svg +load 541297-1.svg +load 547333-1.svg +load 548899-1.svg +load 551620-1.svg +load 554141-1.svg +load 554202-2.svg +load 555026-1.svg +load 556841-1.svg +load 572938-1.svg +load 572938-2.svg +load 572938-3.svg +load 572938-4.svg +load 588287-1.svg +load 588287-2.svg +load 590425-1.html +load 594653-1.svg +load 596796-1.svg +load 605345-1.svg +load 606101-1.svg +load 608295-1.html +load 608549-1.svg +load 611927-1.svg +load 615002-1.svg +load 615872-1.svg +load 641388-1.html +load 641388-2.html +load 650732-1.svg +load 665334-1.svg +load 669225-1.svg +load 669225-2.svg +load 670313-1.svg +load 678822-1.svg +load 678847-1.svg +load 678938-1.svg +load 690994-1.svg +load 691337-1.svg +load 691337-2.svg +load 697640-1.svg +load 699325-1.svg +load 709907-1.svg +load 720103-1.svg +load 849593-1.xhtml +load 1010681-1.svg +load 1322770-1.svg +load 1322849-1.svg +load 1343357-1.html +load 1375596-1.svg +load 1402547-1.html +load 1411963-1.html +load 1413319-1.html +load 1535388-1.html +load 1772573-1.html +load 1780800-1.html diff --git a/dom/smil/moz.build b/dom/smil/moz.build new file mode 100644 index 0000000000..2280cd7aa3 --- /dev/null +++ b/dom/smil/moz.build @@ -0,0 +1,74 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "SVG") + +MOCHITEST_MANIFESTS += ["test/mochitest.toml"] + +EXPORTS.mozilla += [ + "SMILAnimationController.h", + "SMILAnimationFunction.h", + "SMILAttr.h", + "SMILCompositorTable.h", + "SMILCSSValueType.h", + "SMILInstanceTime.h", + "SMILInterval.h", + "SMILKeySpline.h", + "SMILMilestone.h", + "SMILNullType.h", + "SMILParserUtils.h", + "SMILRepeatCount.h", + "SMILSetAnimationFunction.h", + "SMILTargetIdentifier.h", + "SMILTimeContainer.h", + "SMILTimedElement.h", + "SMILTimeValue.h", + "SMILTimeValueSpec.h", + "SMILTimeValueSpecParams.h", + "SMILType.h", + "SMILTypes.h", + "SMILValue.h", +] + +EXPORTS.mozilla.dom += [ + "TimeEvent.h", +] + +UNIFIED_SOURCES += [ + "SMILAnimationController.cpp", + "SMILAnimationFunction.cpp", + "SMILBoolType.cpp", + "SMILCompositor.cpp", + "SMILCSSProperty.cpp", + "SMILCSSValueType.cpp", + "SMILEnumType.cpp", + "SMILFloatType.cpp", + "SMILInstanceTime.cpp", + "SMILIntegerType.cpp", + "SMILInterval.cpp", + "SMILKeySpline.cpp", + "SMILNullType.cpp", + "SMILParserUtils.cpp", + "SMILRepeatCount.cpp", + "SMILSetAnimationFunction.cpp", + "SMILStringType.cpp", + "SMILTimeContainer.cpp", + "SMILTimedElement.cpp", + "SMILTimeValue.cpp", + "SMILTimeValueSpec.cpp", + "SMILValue.cpp", + "TimeEvent.cpp", +] + +LOCAL_INCLUDES += [ + "/dom/base", + "/dom/svg", + "/layout/base", + "/layout/style", +] + +FINAL_LIBRARY = "xul" diff --git a/dom/smil/test/db_smilAnimateMotion.js b/dom/smil/test/db_smilAnimateMotion.js new file mode 100644 index 0000000000..31c338586f --- /dev/null +++ b/dom/smil/test/db_smilAnimateMotion.js @@ -0,0 +1,309 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +/* testcase data for */ + +// Fake motion 'attribute', to satisfy testing code that expects an attribute. +var gMotionAttr = new AdditiveAttribute( + SMILUtil.getMotionFakeAttributeName(), + "XML", + "rect" +); + +// CTM-summary-definitions, for re-use by multiple testcase bundles below. +var _reusedCTMLists = { + pacedBasic: { + ctm0: [100, 200, 0], + ctm1_6: [105, 205, 0], + ctm1_3: [110, 210, 0], + ctm2_3: [120, 220, 0], + ctm1: [130, 210, 0], + }, + pacedR60: { + ctm0: [100, 200, Math.PI / 3], + ctm1_6: [105, 205, Math.PI / 3], + ctm1_3: [110, 210, Math.PI / 3], + ctm2_3: [120, 220, Math.PI / 3], + ctm1: [130, 210, Math.PI / 3], + }, + pacedRAuto: { + ctm0: [100, 200, Math.PI / 4], + ctm1_6: [105, 205, Math.PI / 4], + ctm1_3: [110, 210, Math.PI / 4], + ctm2_3: [120, 220, -Math.PI / 4], + ctm1: [130, 210, -Math.PI / 4], + }, + pacedRAutoReverse: { + ctm0: [100, 200, (5 * Math.PI) / 4], + ctm1_6: [105, 205, (5 * Math.PI) / 4], + ctm1_3: [110, 210, (5 * Math.PI) / 4], + ctm2_3: [120, 220, (3 * Math.PI) / 4], + ctm1: [130, 210, (3 * Math.PI) / 4], + }, + + discreteBasic: { + ctm0: [100, 200, 0], + ctm1_6: [100, 200, 0], + ctm1_3: [120, 220, 0], + ctm2_3: [130, 210, 0], + ctm1: [130, 210, 0], + }, + discreteRAuto: { + ctm0: [100, 200, Math.PI / 4], + ctm1_6: [100, 200, Math.PI / 4], + ctm1_3: [120, 220, -Math.PI / 4], + ctm2_3: [130, 210, -Math.PI / 4], + ctm1: [130, 210, -Math.PI / 4], + }, + justMoveBasic: { + ctm0: [40, 80, 0], + ctm1_6: [40, 80, 0], + ctm1_3: [40, 80, 0], + ctm2_3: [40, 80, 0], + ctm1: [40, 80, 0], + }, + justMoveR60: { + ctm0: [40, 80, Math.PI / 3], + ctm1_6: [40, 80, Math.PI / 3], + ctm1_3: [40, 80, Math.PI / 3], + ctm2_3: [40, 80, Math.PI / 3], + ctm1: [40, 80, Math.PI / 3], + }, + justMoveRAuto: { + ctm0: [40, 80, Math.atan(2)], + ctm1_6: [40, 80, Math.atan(2)], + ctm1_3: [40, 80, Math.atan(2)], + ctm2_3: [40, 80, Math.atan(2)], + ctm1: [40, 80, Math.atan(2)], + }, + justMoveRAutoReverse: { + ctm0: [40, 80, Math.PI + Math.atan(2)], + ctm1_6: [40, 80, Math.PI + Math.atan(2)], + ctm1_3: [40, 80, Math.PI + Math.atan(2)], + ctm2_3: [40, 80, Math.PI + Math.atan(2)], + ctm1: [40, 80, Math.PI + Math.atan(2)], + }, + nullMoveBasic: { + ctm0: [0, 0, 0], + ctm1_6: [0, 0, 0], + ctm1_3: [0, 0, 0], + ctm2_3: [0, 0, 0], + ctm1: [0, 0, 0], + }, + nullMoveRAutoReverse: { + ctm0: [0, 0, Math.PI], + ctm1_6: [0, 0, Math.PI], + ctm1_3: [0, 0, Math.PI], + ctm2_3: [0, 0, Math.PI], + ctm1: [0, 0, Math.PI], + }, +}; + +var gMotionBundles = [ + // Bundle to test basic functionality (using default calcMode='paced') + new TestcaseBundle(gMotionAttr, [ + // Basic paced-mode (default) test, with values/mpath/path + new AnimMotionTestcase( + { values: "100, 200; 120, 220; 130, 210" }, + _reusedCTMLists.pacedBasic + ), + new AnimMotionTestcase( + { path: "M100 200 L120 220 L130 210" }, + _reusedCTMLists.pacedBasic + ), + new AnimMotionTestcase( + { mpath: "M100 200 L120 220 L130 210" }, + _reusedCTMLists.pacedBasic + ), + + // ..and now with rotate=constant value in degrees + new AnimMotionTestcase( + { values: "100,200; 120,220; 130, 210", rotate: "60" }, + _reusedCTMLists.pacedR60 + ), + new AnimMotionTestcase( + { path: "M100 200 L120 220 L130 210", rotate: "60" }, + _reusedCTMLists.pacedR60 + ), + new AnimMotionTestcase( + { mpath: "M100 200 L120 220 L130 210", rotate: "60" }, + _reusedCTMLists.pacedR60 + ), + + // ..and now with rotate=constant value in radians + new AnimMotionTestcase( + { path: "M100 200 L120 220 L130 210", rotate: "1.0471975512rad" }, // pi/3 + _reusedCTMLists.pacedR60 + ), + + // ..and now with rotate=auto + new AnimMotionTestcase( + { values: "100,200; 120,220; 130, 210", rotate: "auto" }, + _reusedCTMLists.pacedRAuto + ), + new AnimMotionTestcase( + { path: "M100 200 L120 220 L130 210", rotate: "auto" }, + _reusedCTMLists.pacedRAuto + ), + new AnimMotionTestcase( + { mpath: "M100 200 L120 220 L130 210", rotate: "auto" }, + _reusedCTMLists.pacedRAuto + ), + + // ..and now with rotate=auto-reverse + new AnimMotionTestcase( + { values: "100,200; 120,220; 130, 210", rotate: "auto-reverse" }, + _reusedCTMLists.pacedRAutoReverse + ), + new AnimMotionTestcase( + { path: "M100 200 L120 220 L130 210", rotate: "auto-reverse" }, + _reusedCTMLists.pacedRAutoReverse + ), + new AnimMotionTestcase( + { mpath: "M100 200 L120 220 L130 210", rotate: "auto-reverse" }, + _reusedCTMLists.pacedRAutoReverse + ), + ]), + + // Bundle to test calcMode='discrete' + new TestcaseBundle(gMotionAttr, [ + new AnimMotionTestcase( + { values: "100, 200; 120, 220; 130, 210", calcMode: "discrete" }, + _reusedCTMLists.discreteBasic + ), + new AnimMotionTestcase( + { path: "M100 200 L120 220 L130 210", calcMode: "discrete" }, + _reusedCTMLists.discreteBasic + ), + new AnimMotionTestcase( + { mpath: "M100 200 L120 220 L130 210", calcMode: "discrete" }, + _reusedCTMLists.discreteBasic + ), + // ..and now with rotate=auto + new AnimMotionTestcase( + { + values: "100, 200; 120, 220; 130, 210", + calcMode: "discrete", + rotate: "auto", + }, + _reusedCTMLists.discreteRAuto + ), + new AnimMotionTestcase( + { + path: "M100 200 L120 220 L130 210", + calcMode: "discrete", + rotate: "auto", + }, + _reusedCTMLists.discreteRAuto + ), + new AnimMotionTestcase( + { + mpath: "M100 200 L120 220 L130 210", + calcMode: "discrete", + rotate: "auto", + }, + _reusedCTMLists.discreteRAuto + ), + ]), + + // Bundle to test relative units ('em') + new TestcaseBundle(gMotionAttr, [ + // First with unitless values from->by... + new AnimMotionTestcase( + { from: "10, 10", by: "30, 60" }, + { + ctm0: [10, 10, 0], + ctm1_6: [15, 20, 0], + ctm1_3: [20, 30, 0], + ctm2_3: [30, 50, 0], + ctm1: [40, 70, 0], + } + ), + // ... then add 'em' units (with 1em=10px) on half the values + new AnimMotionTestcase( + { from: "1em, 10", by: "30, 6em" }, + { + ctm0: [10, 10, 0], + ctm1_6: [15, 20, 0], + ctm1_3: [20, 30, 0], + ctm2_3: [30, 50, 0], + ctm1: [40, 70, 0], + } + ), + ]), + + // Bundle to test a path with just a "move" command and nothing else + new TestcaseBundle(gMotionAttr, [ + new AnimMotionTestcase({ values: "40, 80" }, _reusedCTMLists.justMoveBasic), + new AnimMotionTestcase({ path: "M40 80" }, _reusedCTMLists.justMoveBasic), + new AnimMotionTestcase({ mpath: "m40 80" }, _reusedCTMLists.justMoveBasic), + ]), + // ... and now with a fixed rotate-angle + new TestcaseBundle(gMotionAttr, [ + new AnimMotionTestcase( + { values: "40, 80", rotate: "60" }, + _reusedCTMLists.justMoveR60 + ), + new AnimMotionTestcase( + { path: "M40 80", rotate: "60" }, + _reusedCTMLists.justMoveR60 + ), + new AnimMotionTestcase( + { mpath: "m40 80", rotate: "60" }, + _reusedCTMLists.justMoveR60 + ), + ]), + // ... and now with 'auto' (should use the move itself as + // our tangent angle, I think) + new TestcaseBundle(gMotionAttr, [ + new AnimMotionTestcase( + { values: "40, 80", rotate: "auto" }, + _reusedCTMLists.justMoveRAuto + ), + new AnimMotionTestcase( + { path: "M40 80", rotate: "auto" }, + _reusedCTMLists.justMoveRAuto + ), + new AnimMotionTestcase( + { mpath: "m40 80", rotate: "auto" }, + _reusedCTMLists.justMoveRAuto + ), + ]), + // ... and now with 'auto-reverse' + new TestcaseBundle(gMotionAttr, [ + new AnimMotionTestcase( + { values: "40, 80", rotate: "auto-reverse" }, + _reusedCTMLists.justMoveRAutoReverse + ), + new AnimMotionTestcase( + { path: "M40 80", rotate: "auto-reverse" }, + _reusedCTMLists.justMoveRAutoReverse + ), + new AnimMotionTestcase( + { mpath: "m40 80", rotate: "auto-reverse" }, + _reusedCTMLists.justMoveRAutoReverse + ), + ]), + // ... and now with a null move to make sure 'auto'/'auto-reverse' don't + // blow up + new TestcaseBundle(gMotionAttr, [ + new AnimMotionTestcase( + { values: "0, 0", rotate: "auto" }, + _reusedCTMLists.nullMoveBasic + ), + ]), + new TestcaseBundle(gMotionAttr, [ + new AnimMotionTestcase( + { values: "0, 0", rotate: "auto-reverse" }, + _reusedCTMLists.nullMoveRAutoReverse + ), + ]), +]; + +// XXXdholbert Add more tests: +// - keyPoints/keyTimes +// - paths with curves +// - Control path with from/by/to diff --git a/dom/smil/test/db_smilCSSFromBy.js b/dom/smil/test/db_smilCSSFromBy.js new file mode 100644 index 0000000000..737037271d --- /dev/null +++ b/dom/smil/test/db_smilCSSFromBy.js @@ -0,0 +1,207 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +/* testcase data for simple "from-by" animations of CSS properties */ + +// NOTE: This js file requires db_smilCSSPropertyList.js + +// Lists of testcases for re-use across multiple properties of the same type +var _fromByTestLists = { + color: [ + new AnimTestcaseFromBy("rgb(10, 20, 30)", "currentColor", { + midComp: "rgb(35, 45, 55)", + toComp: "rgb(60, 70, 80)", + }), + new AnimTestcaseFromBy("currentColor", "rgb(30, 20, 10)", { + fromComp: "rgb(50, 50, 50)", + midComp: "rgb(65, 60, 55)", + toComp: "rgb(80, 70, 60)", + }), + new AnimTestcaseFromBy( + "rgba(10, 20, 30, 0.2)", + "rgba(50, 50, 50, 1)", + // (rgb(10, 20, 30) * 0.2 * 0.5 + rgb(52, 54, 56) * 1.0 * 0.5) * (1 / 0.6) + { + midComp: "rgba(45, 48, 52, 0.6)", + // (rgb(10, 20, 30) * 0.2 + rgb(50, 50, 50) * 1) / 1.0 + toComp: "rgb(52, 54, 56)", + } + ), + + // The "from" and "by" values in the test case below overflow the maxium + // color-channel values when added together. + // (e.g. for red [ignoring alpha for now], 100 + 240 = 340 which is > 255) + // + // The SVG Animation spec says we should clamp color values "as late as + // possible" i.e. allow the channel overflow and clamp at paint-time. + // + // That gives us: + // + // to-value = (rgb(100, 100, 100) * 0.6 + rgb(240, 240, 240) * 1.0)) * 1 + // = rgb(300, 300, 300) + // midComp = (rgb(100, 100, 100) * 0.6 * 0.5 + rgb(300, 300, 300) * 1.0 * 0.5) * (1 / 0.8) + // = rgb(225, 225, 225) + // + // + new AnimTestcaseFromBy( + "rgba(100, 100, 100, 0.6)", + "rgba(240, 240, 240, 1)", + { midComp: "rgba(225, 225, 225, 0.8)", toComp: "rgb(255, 255, 255)" } + ), + ], + lengthNoUnits: [ + new AnimTestcaseFromBy("0", "50", { + fromComp: "0px", // 0 acts like 0px + midComp: "25px", + toComp: "50px", + }), + new AnimTestcaseFromBy("30", "10", { + fromComp: "30px", + midComp: "35px", + toComp: "40px", + }), + ], + lengthPx: [ + new AnimTestcaseFromBy("0px", "8px", { + fromComp: "0px", + midComp: "4px", + toComp: "8px", + }), + new AnimTestcaseFromBy("1px", "10px", { + fromComp: "1px", + midComp: "6px", + toComp: "11px", + }), + ], + opacity: [ + new AnimTestcaseFromBy("1", "-1", { midComp: "0.5", toComp: "0" }), + new AnimTestcaseFromBy("0.4", "-0.6", { midComp: "0.1", toComp: "0" }), + new AnimTestcaseFromBy( + "0.8", + "-1.4", + { midComp: "0.1", toComp: "0" }, + "opacities with abs val >1 get clamped too early" + ), + new AnimTestcaseFromBy( + "1.2", + "-0.6", + { midComp: "0.9", toComp: "0.6" }, + "opacities with abs val >1 get clamped too early" + ), + ], + paint: [ + // The "none" keyword & URI values aren't addiditve, so the animations in + // these testcases are expected to have no effect. + new AnimTestcaseFromBy("none", "none", { noEffect: 1 }), + new AnimTestcaseFromBy("url(#gradA)", "url(#gradB)", { noEffect: 1 }), + new AnimTestcaseFromBy("url(#gradA)", "url(#gradB) red", { noEffect: 1 }), + new AnimTestcaseFromBy("url(#gradA)", "none", { noEffect: 1 }), + new AnimTestcaseFromBy("red", "url(#gradA)", { noEffect: 1 }), + ], + URIsAndNone: [ + // No need to specify { noEffect: 1 }, since plain URI-valued properties + // aren't additive + new AnimTestcaseFromBy("url(#idA)", "url(#idB)"), + new AnimTestcaseFromBy("none", "url(#idB)"), + new AnimTestcaseFromBy("url(#idB)", "inherit"), + ], +}; + +// List of attribute/testcase-list bundles to be tested +var gFromByBundles = [ + new TestcaseBundle(gPropList.clip, [ + new AnimTestcaseFromBy( + "rect(1px, 2px, 3px, 4px)", + "rect(10px, 20px, 30px, 40px)", + { + midComp: "rect(6px, 12px, 18px, 24px)", + toComp: "rect(11px, 22px, 33px, 44px)", + } + ), + // Adding "auto" (either as a standalone value or a subcomponent value) + // should cause animation to fail. + new AnimTestcaseFromBy("auto", "auto", { noEffect: 1 }), + new AnimTestcaseFromBy("auto", "rect(auto, auto, auto, auto)", { + noEffect: 1, + }), + new AnimTestcaseFromBy( + "rect(auto, auto, auto, auto)", + "rect(auto, auto, auto, auto)", + { noEffect: 1 } + ), + new AnimTestcaseFromBy("rect(1px, 2px, 3px, 4px)", "auto", { noEffect: 1 }), + new AnimTestcaseFromBy("auto", "rect(1px, 2px, 3px, 4px)", { noEffect: 1 }), + new AnimTestcaseFromBy( + "rect(1px, 2px, 3px, auto)", + "rect(10px, 20px, 30px, 40px)", + { noEffect: 1 } + ), + new AnimTestcaseFromBy( + "rect(1px, auto, 3px, 4px)", + "rect(10px, auto, 30px, 40px)", + { noEffect: 1 } + ), + new AnimTestcaseFromBy( + "rect(1px, 2px, 3px, 4px)", + "rect(10px, auto, 30px, 40px)", + { noEffect: 1 } + ), + ]), + // Check that 'by' animations for 'cursor' has no effect + new TestcaseBundle(gPropList.cursor, [ + new AnimTestcaseFromBy("crosshair", "move"), + ]), + new TestcaseBundle( + gPropList.fill, + [].concat(_fromByTestLists.color, _fromByTestLists.paint) + ), + // Check that 'by' animations involving URIs have no effect + new TestcaseBundle(gPropList.filter, _fromByTestLists.URIsAndNone), + new TestcaseBundle(gPropList.font, [ + new AnimTestcaseFromBy( + "10px serif", + "normal normal 400 100px / 10px monospace" + ), + ]), + new TestcaseBundle( + gPropList.font_size, + [].concat(_fromByTestLists.lengthNoUnits, _fromByTestLists.lengthPx) + ), + new TestcaseBundle(gPropList.font_size_adjust, [ + // These testcases implicitly have no effect, because font-size-adjust is + // non-additive (and is declared as such in db_smilCSSPropertyList.js) + new AnimTestcaseFromBy("0.5", "0.1"), + new AnimTestcaseFromBy("none", "0.1"), + new AnimTestcaseFromBy("0.1", "none"), + ]), + // Bug 1457353: Change from nsColor to StyleComplexColor causes addition + // with currentcolor to break. Bug 1465307 for work to re-enable. + new TestcaseBundle(gPropList.lighting_color, _fromByTestLists.color), + new TestcaseBundle(gPropList.marker, _fromByTestLists.URIsAndNone), + new TestcaseBundle(gPropList.marker_end, _fromByTestLists.URIsAndNone), + new TestcaseBundle(gPropList.marker_mid, _fromByTestLists.URIsAndNone), + new TestcaseBundle(gPropList.marker_start, _fromByTestLists.URIsAndNone), + new TestcaseBundle(gPropList.overflow, [ + new AnimTestcaseFromBy("inherit", "auto"), + new AnimTestcaseFromBy("scroll", "hidden"), + ]), + new TestcaseBundle(gPropList.opacity, _fromByTestLists.opacity), + new TestcaseBundle(gPropList.stroke_miterlimit, [ + new AnimTestcaseFromBy("1", "1", { midComp: "1.5", toComp: "2" }), + new AnimTestcaseFromBy("20.1", "-10", { midComp: "15.1", toComp: "10.1" }), + ]), + new TestcaseBundle(gPropList.stroke_dasharray, [ + // These testcases implicitly have no effect, because stroke-dasharray is + // non-additive (and is declared as such in db_smilCSSPropertyList.js) + new AnimTestcaseFromBy("none", "5"), + new AnimTestcaseFromBy("10", "5"), + new AnimTestcaseFromBy("1", "2, 3"), + ]), + new TestcaseBundle( + gPropList.stroke_width, + [].concat(_fromByTestLists.lengthNoUnits, _fromByTestLists.lengthPx) + ), +]; diff --git a/dom/smil/test/db_smilCSSFromTo.js b/dom/smil/test/db_smilCSSFromTo.js new file mode 100644 index 0000000000..a644c96962 --- /dev/null +++ b/dom/smil/test/db_smilCSSFromTo.js @@ -0,0 +1,625 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +/* testcase data for simple "from-to" animations of CSS properties */ + +// NOTE: This js file requires db_smilCSSPropertyList.js + +// NOTE: I'm Including 'inherit' and 'currentColor' as interpolatable values. +// According to SVG Mobile 1.2 section 16.2.9, "keywords such as inherit which +// yield a numeric computed value may be included in the values list for an +// interpolated animation". + +// Path of test URL (stripping off final slash + filename), for use in +// generating computed value of 'cursor' property +var _testPath = document.URL.substring(0, document.URL.lastIndexOf("/")); + +// Lists of testcases for re-use across multiple properties of the same type +var _fromToTestLists = { + color: [ + new AnimTestcaseFromTo("rgb(100, 100, 100)", "rgb(200, 200, 200)", { + midComp: "rgb(150, 150, 150)", + }), + new AnimTestcaseFromTo("#F02000", "#0080A0", { + fromComp: "rgb(240, 32, 0)", + midComp: "rgb(120, 80, 80)", + toComp: "rgb(0, 128, 160)", + }), + new AnimTestcaseFromTo("crimson", "lawngreen", { + fromComp: "rgb(220, 20, 60)", + midComp: "rgb(172, 136, 30)", + toComp: "rgb(124, 252, 0)", + }), + new AnimTestcaseFromTo("currentColor", "rgb(100, 100, 100)", { + fromComp: "rgb(50, 50, 50)", + midComp: "rgb(75, 75, 75)", + }), + new AnimTestcaseFromTo( + "rgba(10, 20, 30, 0.2)", + "rgba(50, 50, 50, 1)", + // (rgb(10, 20, 30) * 0.2 * 0.5 + rgb(50, 50, 50) * 1.0 * 0.5) * (1 / 0.6) + { midComp: "rgba(43, 45, 47, 0.6)", toComp: "rgb(50, 50, 50)" } + ), + ], + colorFromInheritBlack: [ + new AnimTestcaseFromTo("inherit", "rgb(200, 200, 200)", { + fromComp: "rgb(0, 0, 0)", + midComp: "rgb(100, 100, 100)", + }), + ], + colorFromInheritWhite: [ + new AnimTestcaseFromTo("inherit", "rgb(205, 205, 205)", { + fromComp: "rgb(255, 255, 255)", + midComp: "rgb(230, 230, 230)", + }), + ], + paintServer: [ + new AnimTestcaseFromTo("none", "none"), + new AnimTestcaseFromTo("none", "blue", { toComp: "rgb(0, 0, 255)" }), + new AnimTestcaseFromTo("rgb(50, 50, 50)", "none"), + new AnimTestcaseFromTo( + "url(#gradA)", + "url(#gradB) currentColor", + { + fromComp: 'url("' + document.URL + '#gradA") rgb(0, 0, 0)', + toComp: 'url("' + document.URL + '#gradB") rgb(50, 50, 50)', + }, + "need support for URI-based paints" + ), + new AnimTestcaseFromTo( + "url(#gradA) orange", + "url(#gradB)", + { + fromComp: 'url("' + document.URL + '#gradA") rgb(255, 165, 0)', + toComp: 'url("' + document.URL + '#gradB") rgb(0, 0, 0)', + }, + "need support for URI-based paints" + ), + new AnimTestcaseFromTo( + "url(#no_grad)", + "url(#gradB)", + { + fromComp: 'url("' + document.URL + '#no_grad") ' + "rgb(0, 0, 0)", + toComp: 'url("' + document.URL + '#gradB") rgb(0, 0, 0)', + }, + "need support for URI-based paints" + ), + new AnimTestcaseFromTo( + "url(#no_grad) rgb(1,2,3)", + "url(#gradB) blue", + { + fromComp: 'url("' + document.URL + '#no_grad") ' + "rgb(1, 2, 3)", + toComp: 'url("' + document.URL + '#gradB") rgb(0, 0, 255)', + }, + "need support for URI-based paints" + ), + ], + lengthNoUnits: [ + new AnimTestcaseFromTo("0", "20", { + fromComp: "0px", + midComp: "10px", + toComp: "20px", + }), + new AnimTestcaseFromTo("50", "0", { + fromComp: "50px", + midComp: "25px", + toComp: "0px", + }), + new AnimTestcaseFromTo("30", "80", { + fromComp: "30px", + midComp: "55px", + toComp: "80px", + }), + ], + lengthPx: [ + new AnimTestcaseFromTo("0px", "12px", { + fromComp: "0px", + midComp: "6px", + toComp: "12px", + }), + new AnimTestcaseFromTo("16px", "0px", { + fromComp: "16px", + midComp: "8px", + toComp: "0px", + }), + new AnimTestcaseFromTo("10px", "20px", { + fromComp: "10px", + midComp: "15px", + toComp: "20px", + }), + new AnimTestcaseFromTo("41px", "1px", { + fromComp: "41px", + midComp: "21px", + toComp: "1px", + }), + ], + lengthPctSVG: [new AnimTestcaseFromTo("20.5%", "0.5%", { midComp: "10.5%" })], + lengthPxPctSVG: [ + new AnimTestcaseFromTo( + "10px", + "10%", + { midComp: "15px" }, + "need support for interpolating between " + "px and percent values" + ), + ], + lengthPxNoUnitsSVG: [ + new AnimTestcaseFromTo("10", "20px", { + fromComp: "10px", + midComp: "15px", + toComp: "20px", + }), + new AnimTestcaseFromTo("10px", "20", { + fromComp: "10px", + midComp: "15px", + toComp: "20px", + }), + ], + opacity: [ + new AnimTestcaseFromTo("1", "0", { midComp: "0.5" }), + new AnimTestcaseFromTo("0.2", "0.12", { midComp: "0.16" }), + new AnimTestcaseFromTo("0.5", "0.7", { midComp: "0.6" }), + new AnimTestcaseFromTo("0.5", "inherit", { midComp: "0.75", toComp: "1" }), + // Make sure we don't clamp out-of-range values before interpolation + new AnimTestcaseFromTo( + "0.2", + "1.2", + { midComp: "0.7", toComp: "1" }, + "opacities with abs val >1 get clamped too early" + ), + new AnimTestcaseFromTo("-0.2", "0.6", { fromComp: "0", midComp: "0.2" }), + new AnimTestcaseFromTo( + "-1.2", + "1.6", + { fromComp: "0", midComp: "0.2", toComp: "1" }, + "opacities with abs val >1 get clamped too early" + ), + new AnimTestcaseFromTo( + "-0.6", + "1.4", + { fromComp: "0", midComp: "0.4", toComp: "1" }, + "opacities with abs val >1 get clamped too early" + ), + ], + URIsAndNone: [ + new AnimTestcaseFromTo("url(#idA)", "url(#idB)", { + fromComp: 'url("#idA")', + toComp: 'url("#idB")', + }), + new AnimTestcaseFromTo("none", "url(#idB)", { toComp: 'url("#idB")' }), + new AnimTestcaseFromTo("url(#idB)", "inherit", { + fromComp: 'url("#idB")', + toComp: "none", + }), + ], +}; + +function _tweakForLetterSpacing(testcases) { + return testcases.map(function (t) { + let valMap = Object.assign({}, t.computedValMap); + for (let prop of Object.keys(valMap)) { + if (valMap[prop] == "0px") { + valMap[prop] = "normal"; + } + } + return new AnimTestcaseFromTo(t.from, t.to, valMap); + }); +} + +// List of attribute/testcase-list bundles to be tested +var gFromToBundles = [ + new TestcaseBundle(gPropList.clip, [ + new AnimTestcaseFromTo( + "rect(1px, 2px, 3px, 4px)", + "rect(11px, 22px, 33px, 44px)", + { midComp: "rect(6px, 12px, 18px, 24px)" } + ), + new AnimTestcaseFromTo( + "rect(1px, auto, 3px, 4px)", + "rect(11px, auto, 33px, 44px)", + { midComp: "rect(6px, auto, 18px, 24px)" } + ), + new AnimTestcaseFromTo("auto", "auto"), + new AnimTestcaseFromTo( + "rect(auto, auto, auto, auto)", + "rect(auto, auto, auto, auto)" + ), + // Interpolation not supported in these next cases (with auto --> px-value) + new AnimTestcaseFromTo( + "rect(1px, auto, 3px, auto)", + "rect(11px, auto, 33px, 44px)" + ), + new AnimTestcaseFromTo( + "rect(1px, 2px, 3px, 4px)", + "rect(11px, auto, 33px, 44px)" + ), + new AnimTestcaseFromTo("rect(1px, 2px, 3px, 4px)", "auto"), + new AnimTestcaseFromTo("auto", "rect(1px, 2px, 3px, 4px)"), + ]), + new TestcaseBundle(gPropList.clip_path, _fromToTestLists.URIsAndNone), + new TestcaseBundle(gPropList.clip_rule, [ + new AnimTestcaseFromTo("nonzero", "evenodd"), + new AnimTestcaseFromTo("evenodd", "inherit", { toComp: "nonzero" }), + ]), + new TestcaseBundle( + gPropList.color, + [].concat(_fromToTestLists.color, [ + // Note: inherited value is rgb(50, 50, 50) (set on ) + new AnimTestcaseFromTo("inherit", "rgb(200, 200, 200)", { + fromComp: "rgb(50, 50, 50)", + midComp: "rgb(125, 125, 125)", + }), + ]) + ), + new TestcaseBundle(gPropList.color_interpolation, [ + new AnimTestcaseFromTo("sRGB", "auto", { fromComp: "srgb" }), + new AnimTestcaseFromTo("inherit", "linearRGB", { + fromComp: "srgb", + toComp: "linearrgb", + }), + ]), + new TestcaseBundle(gPropList.color_interpolation_filters, [ + new AnimTestcaseFromTo("sRGB", "auto", { fromComp: "srgb" }), + new AnimTestcaseFromTo("auto", "inherit", { toComp: "linearrgb" }), + ]), + new TestcaseBundle(gPropList.cursor, [ + new AnimTestcaseFromTo("crosshair", "move"), + new AnimTestcaseFromTo( + "url('a.cur'), url('b.cur'), nw-resize", + "sw-resize", + { + fromComp: + 'url("' + + _testPath + + '/a.cur"), ' + + 'url("' + + _testPath + + '/b.cur"), ' + + "nw-resize", + } + ), + ]), + new TestcaseBundle(gPropList.direction, [ + new AnimTestcaseFromTo("ltr", "rtl"), + new AnimTestcaseFromTo("rtl", "inherit"), + ]), + new TestcaseBundle(gPropList.display, [ + // I'm not testing the "inherit" value for "display", because part of + // my test runs with "display: none" on everything, and so the + // inherited value isn't always the same. (i.e. the computed value + // of 'inherit' will be different in different tests) + new AnimTestcaseFromTo("block", "table-cell"), + new AnimTestcaseFromTo("inline", "inline-table"), + new AnimTestcaseFromTo("table-row", "none"), + ]), + new TestcaseBundle(gPropList.dominant_baseline, [ + new AnimTestcaseFromTo("alphabetic", "hanging"), + new AnimTestcaseFromTo("mathematical", "central"), + new AnimTestcaseFromTo("middle", "text-after-edge"), + new AnimTestcaseFromTo("text-before-edge", "auto"), + new AnimTestcaseFromTo("alphabetic", "inherit", { toComp: "auto" }), + ]), + // NOTE: Mozilla doesn't currently support "enable-background", but I'm + // testing it here in case we ever add support for it, because it's + // explicitly not animatable in the SVG spec. + new TestcaseBundle(gPropList.enable_background, [ + new AnimTestcaseFromTo("new", "accumulate"), + ]), + new TestcaseBundle( + gPropList.fill, + [].concat( + _fromToTestLists.color, + _fromToTestLists.paintServer, + _fromToTestLists.colorFromInheritBlack + ) + ), + new TestcaseBundle(gPropList.fill_opacity, _fromToTestLists.opacity), + new TestcaseBundle(gPropList.fill_rule, [ + new AnimTestcaseFromTo("nonzero", "evenodd"), + new AnimTestcaseFromTo("evenodd", "inherit", { toComp: "nonzero" }), + ]), + new TestcaseBundle(gPropList.filter, _fromToTestLists.URIsAndNone), + new TestcaseBundle( + gPropList.flood_color, + [].concat(_fromToTestLists.color, _fromToTestLists.colorFromInheritBlack) + ), + new TestcaseBundle(gPropList.flood_opacity, _fromToTestLists.opacity), + new TestcaseBundle(gPropList.font, [ + // NOTE: 'line-height' is hard-wired at 10px in test_smilCSSFromTo.xhtml + // because if it's not explicitly set, its value varies across platforms. + // NOTE: System font values can't be tested here, because their computed + // values vary from platform to platform. However, they are tested + // visually, in the reftest "anim-css-font-1.svg" + new AnimTestcaseFromTo("10px serif", "30px serif", { + fromComp: "normal normal 400 10px / 10px serif", + toComp: "normal normal 400 30px / 10px serif", + }), + new AnimTestcaseFromTo("10px serif", "30px sans-serif", { + fromComp: "normal normal 400 10px / 10px serif", + toComp: "normal normal 400 30px / 10px sans-serif", + }), + new AnimTestcaseFromTo("1px / 90px cursive", "100px monospace", { + fromComp: "normal normal 400 1px / 10px cursive", + toComp: "normal normal 400 100px / 10px monospace", + }), + new AnimTestcaseFromTo( + "italic small-caps 200 1px cursive", + "100px monospace", + { + fromComp: "italic small-caps 200 1px / 10px cursive", + toComp: "normal normal 400 100px / 10px monospace", + } + ), + new AnimTestcaseFromTo( + "oblique normal 200 30px / 10px cursive", + "normal small-caps 800 40px / 10px serif" + ), + ]), + new TestcaseBundle(gPropList.font_family, [ + new AnimTestcaseFromTo("serif", "sans-serif"), + new AnimTestcaseFromTo("cursive", "monospace"), + ]), + new TestcaseBundle( + gPropList.font_size, + [].concat(_fromToTestLists.lengthNoUnits, _fromToTestLists.lengthPx, [ + new AnimTestcaseFromTo("10px", "40%", { + midComp: "15px", + toComp: "20px", + }), + new AnimTestcaseFromTo("160%", "80%", { + fromComp: "80px", + midComp: "60px", + toComp: "40px", + }), + ]) + ), + new TestcaseBundle(gPropList.font_size_adjust, [ + new AnimTestcaseFromTo("0.9", "0.1", { midComp: "0.5" }), + new AnimTestcaseFromTo("0.5", "0.6", { midComp: "0.55" }), + new AnimTestcaseFromTo("none", "0.4"), + ]), + new TestcaseBundle(gPropList.font_stretch, [ + new AnimTestcaseFromTo( + "normal", + "wider", + {}, + "need support for animating between " + "relative 'font-stretch' values" + ), + new AnimTestcaseFromTo( + "narrower", + "ultra-condensed", + {}, + "need support for animating between " + "relative 'font-stretch' values" + ), + new AnimTestcaseFromTo("ultra-condensed", "condensed", { + fromComp: "50%", + midComp: "62.5%", + toComp: "75%", + }), + new AnimTestcaseFromTo("semi-condensed", "semi-expanded", { + fromComp: "87.5%", + midComp: "100%", + toComp: "112.5%", + }), + new AnimTestcaseFromTo("expanded", "ultra-expanded", { + fromComp: "125%", + midComp: "162.5%", + toComp: "200%", + }), + new AnimTestcaseFromTo("ultra-expanded", "inherit", { + fromComp: "200%", + midComp: "150%", + toComp: "100%", + }), + ]), + new TestcaseBundle(gPropList.font_style, [ + new AnimTestcaseFromTo("italic", "inherit", { toComp: "normal" }), + new AnimTestcaseFromTo("normal", "italic"), + new AnimTestcaseFromTo("italic", "oblique"), + new AnimTestcaseFromTo("oblique", "normal", { midComp: "oblique 7deg" }), + ]), + new TestcaseBundle(gPropList.font_variant, [ + new AnimTestcaseFromTo("inherit", "small-caps", { fromComp: "normal" }), + new AnimTestcaseFromTo("small-caps", "normal"), + ]), + new TestcaseBundle(gPropList.font_weight, [ + new AnimTestcaseFromTo("100", "900", { midComp: "500" }), + new AnimTestcaseFromTo("700", "100", { midComp: "400" }), + new AnimTestcaseFromTo("inherit", "200", { + fromComp: "400", + midComp: "300", + }), + new AnimTestcaseFromTo("normal", "bold", { + fromComp: "400", + midComp: "550", + toComp: "700", + }), + new AnimTestcaseFromTo( + "lighter", + "bolder", + {}, + "need support for animating between " + "relative 'font-weight' values" + ), + ]), + // NOTE: Mozilla doesn't currently support "glyph-orientation-horizontal" or + // "glyph-orientation-vertical", but I'm testing them here in case we ever + // add support for them, because they're explicitly not animatable in the SVG + // spec. + new TestcaseBundle(gPropList.glyph_orientation_horizontal, [ + new AnimTestcaseFromTo("45deg", "60deg"), + ]), + new TestcaseBundle(gPropList.glyph_orientation_vertical, [ + new AnimTestcaseFromTo("45deg", "60deg"), + ]), + new TestcaseBundle(gPropList.image_rendering, [ + new AnimTestcaseFromTo("auto", "optimizeQuality", { + toComp: "optimizequality", + }), + new AnimTestcaseFromTo("optimizeQuality", "optimizeSpeed", { + fromComp: "optimizequality", + toComp: "optimizespeed", + }), + ]), + new TestcaseBundle( + gPropList.letter_spacing, + _tweakForLetterSpacing( + [].concat(_fromToTestLists.lengthNoUnits, _fromToTestLists.lengthPx) + ) + ), + new TestcaseBundle( + gPropList.lighting_color, + [].concat(_fromToTestLists.color, _fromToTestLists.colorFromInheritWhite) + ), + new TestcaseBundle(gPropList.marker, _fromToTestLists.URIsAndNone), + new TestcaseBundle(gPropList.marker_end, _fromToTestLists.URIsAndNone), + new TestcaseBundle(gPropList.marker_mid, _fromToTestLists.URIsAndNone), + new TestcaseBundle(gPropList.marker_start, _fromToTestLists.URIsAndNone), + new TestcaseBundle(gPropList.mask, _fromToTestLists.URIsAndNone), + new TestcaseBundle(gPropList.opacity, _fromToTestLists.opacity), + new TestcaseBundle(gPropList.overflow, [ + new AnimTestcaseFromTo("auto", "visible"), + new AnimTestcaseFromTo("inherit", "visible", { fromComp: "hidden" }), + new AnimTestcaseFromTo("scroll", "auto"), + ]), + new TestcaseBundle(gPropList.pointer_events, [ + new AnimTestcaseFromTo("visibleFill", "stroke", { + fromComp: "visiblefill", + }), + new AnimTestcaseFromTo("none", "visibleStroke", { + toComp: "visiblestroke", + }), + ]), + new TestcaseBundle(gPropList.shape_rendering, [ + new AnimTestcaseFromTo("auto", "optimizeSpeed", { + toComp: "optimizespeed", + }), + new AnimTestcaseFromTo("crispEdges", "geometricPrecision", { + fromComp: "crispedges", + toComp: "geometricprecision", + }), + ]), + new TestcaseBundle( + gPropList.stop_color, + [].concat(_fromToTestLists.color, _fromToTestLists.colorFromInheritBlack) + ), + new TestcaseBundle(gPropList.stop_opacity, _fromToTestLists.opacity), + new TestcaseBundle( + gPropList.stroke, + [].concat(_fromToTestLists.color, _fromToTestLists.paintServer, [ + // Note: inherited value is "none" (the default for "stroke" property) + new AnimTestcaseFromTo("inherit", "rgb(200, 200, 200)", { + fromComp: "none", + }), + ]) + ), + new TestcaseBundle( + gPropList.stroke_dasharray, + [].concat(_fromToTestLists.lengthPctSVG, [ + new AnimTestcaseFromTo("inherit", "20", { fromComp: "none" }), + new AnimTestcaseFromTo("1", "none"), + new AnimTestcaseFromTo("10", "20", { midComp: "15" }), + new AnimTestcaseFromTo("1", "2, 3", { + fromComp: "1, 1", + midComp: "1.5, 2", + }), + new AnimTestcaseFromTo("2, 8", "6", { midComp: "4, 7" }), + new AnimTestcaseFromTo("1, 3", "1, 3, 5, 7, 9", { + fromComp: "1, 3, 1, 3, 1, 3, 1, 3, 1, 3", + midComp: "1, 3, 3, 5, 5, 2, 2, 4, 4, 6", + }), + ]) + ), + new TestcaseBundle( + gPropList.stroke_dashoffset, + [].concat( + _fromToTestLists.lengthNoUnits, + _fromToTestLists.lengthPx, + _fromToTestLists.lengthPxPctSVG, + _fromToTestLists.lengthPctSVG, + _fromToTestLists.lengthPxNoUnitsSVG + ) + ), + new TestcaseBundle(gPropList.stroke_linecap, [ + new AnimTestcaseFromTo("butt", "round"), + new AnimTestcaseFromTo("round", "square"), + ]), + new TestcaseBundle(gPropList.stroke_linejoin, [ + new AnimTestcaseFromTo("miter", "round"), + new AnimTestcaseFromTo("round", "bevel"), + ]), + new TestcaseBundle(gPropList.stroke_miterlimit, [ + new AnimTestcaseFromTo("1", "2", { midComp: "1.5" }), + new AnimTestcaseFromTo("20.1", "10.1", { midComp: "15.1" }), + ]), + new TestcaseBundle(gPropList.stroke_opacity, _fromToTestLists.opacity), + new TestcaseBundle( + gPropList.stroke_width, + [].concat( + _fromToTestLists.lengthNoUnits, + _fromToTestLists.lengthPx, + _fromToTestLists.lengthPxPctSVG, + _fromToTestLists.lengthPctSVG, + _fromToTestLists.lengthPxNoUnitsSVG, + [ + new AnimTestcaseFromTo("inherit", "7px", { + fromComp: "1px", + midComp: "4px", + toComp: "7px", + }), + ] + ) + ), + new TestcaseBundle(gPropList.text_anchor, [ + new AnimTestcaseFromTo("start", "middle"), + new AnimTestcaseFromTo("middle", "end"), + ]), + new TestcaseBundle(gPropList.text_decoration_line, [ + new AnimTestcaseFromTo("none", "underline"), + new AnimTestcaseFromTo("overline", "line-through"), + new AnimTestcaseFromTo("blink", "underline"), + ]), + new TestcaseBundle(gPropList.text_rendering, [ + new AnimTestcaseFromTo("auto", "optimizeSpeed", { + toComp: "optimizespeed", + }), + new AnimTestcaseFromTo("optimizeSpeed", "geometricPrecision", { + fromComp: "optimizespeed", + toComp: "geometricprecision", + }), + new AnimTestcaseFromTo("geometricPrecision", "optimizeLegibility", { + fromComp: "geometricprecision", + toComp: "optimizelegibility", + }), + ]), + new TestcaseBundle(gPropList.unicode_bidi, [ + new AnimTestcaseFromTo("embed", "bidi-override"), + ]), + new TestcaseBundle(gPropList.vector_effect, [ + new AnimTestcaseFromTo("none", "non-scaling-stroke"), + ]), + new TestcaseBundle(gPropList.visibility, [ + new AnimTestcaseFromTo("visible", "hidden"), + new AnimTestcaseFromTo("hidden", "collapse"), + ]), + new TestcaseBundle( + gPropList.word_spacing, + [].concat( + _fromToTestLists.lengthNoUnits, + _fromToTestLists.lengthPx, + _fromToTestLists.lengthPxPctSVG + ) + ), + new TestcaseBundle( + gPropList.word_spacing, + _fromToTestLists.lengthPctSVG, + "pct->pct animations don't currently work for " + "*-spacing properties" + ), + // NOTE: Mozilla doesn't currently support "writing-mode", but I'm + // testing it here in case we ever add support for it, because it's + // explicitly not animatable in the SVG spec. + new TestcaseBundle(gPropList.writing_mode, [ + new AnimTestcaseFromTo("lr", "rl"), + ]), +]; diff --git a/dom/smil/test/db_smilCSSPaced.js b/dom/smil/test/db_smilCSSPaced.js new file mode 100644 index 0000000000..de2896bd2b --- /dev/null +++ b/dom/smil/test/db_smilCSSPaced.js @@ -0,0 +1,356 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */ +/* vim: set shiftwidth=4 tabstop=4 autoindent cindent noexpandtab: */ +/* 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/. */ + +/* testcase data for paced-mode animations of CSS properties */ + +// Lists of testcases for re-use across multiple properties of the same type +var _pacedTestLists = { + color: [ + new AnimTestcasePaced( + "rgb(2, 4, 6); " + "rgb(4, 8, 12); " + "rgb(8, 16, 24)", + { + comp0: "rgb(2, 4, 6)", + comp1_6: "rgb(3, 6, 9)", + comp1_3: "rgb(4, 8, 12)", + comp2_3: "rgb(6, 12, 18)", + comp1: "rgb(8, 16, 24)", + } + ), + new AnimTestcasePaced( + "rgb(10, 10, 10); " + "rgb(20, 10, 8); " + "rgb(20, 30, 4)", + { + comp0: "rgb(10, 10, 10)", + comp1_6: "rgb(15, 10, 9)", + comp1_3: "rgb(20, 10, 8)", + comp2_3: "rgb(20, 20, 6)", + comp1: "rgb(20, 30, 4)", + } + ), + // Use the same RGB component values to make + // premultication effect easier to compute. + new AnimTestcasePaced( + "rgba(20, 40, 60, 0.2); " + + "rgba(20, 40, 60, 0.4); " + + "rgba(20, 40, 60, 0.8)", + { + comp0: "rgba(20, 40, 60, 0.2)", + comp1_6: "rgba(20, 40, 60, 0.3)", + comp1_3: "rgba(20, 40, 60, 0.4)", + comp2_3: "rgba(20, 40, 60, 0.6)", + comp1: "rgba(20, 40, 60, 0.8)", + } + ), + ], + currentColor_color: [ + new AnimTestcasePaced( + "olive; " + // rgb(128, 128, 0) + "currentColor; " + // rgb(50, 50, 50) + "rgb(206, 150, 206)", + { + comp0: "rgb(128, 128, 0)", + comp1_6: "rgb(89, 89, 25)", + comp1_3: "rgb(50, 50, 50)", + comp2_3: "rgb(128, 100, 128)", + comp1: "rgb(206, 150, 206)", + } + ), + ], + currentColor_fill: [ + // Bug 1467622 changed the distance calculation + // involving currentColor, comp values below + // are no longer evenly spaced. + new AnimTestcasePaced( + "olive; " + // rgb(128, 128, 0) + "currentColor; " + // rgb(50, 50, 50) + "rgb(206, 150, 206)", + { + comp0: "rgb(128, 128, 0)", + comp1_6: "rgb(98, 98, 19)", + comp1_3: "rgb(67, 67, 39)", + comp2_3: "rgb(115, 92, 115)", + comp1: "rgb(206, 150, 206)", + } + ), + ], + paintServer: [ + // Sanity check: These aren't interpolatable -- they should end up + // ignoring the calcMode="paced" and falling into discrete-mode. + new AnimTestcasePaced( + "url(#gradA); url(#gradB)", + { + comp0: 'url("' + document.URL + '#gradA") rgb(0, 0, 0)', + comp1_6: 'url("' + document.URL + '#gradA") rgb(0, 0, 0)', + comp1_3: 'url("' + document.URL + '#gradA") rgb(0, 0, 0)', + comp2_3: 'url("' + document.URL + '#gradB") rgb(0, 0, 0)', + comp1: 'url("' + document.URL + '#gradB") rgb(0, 0, 0)', + }, + "need support for URI-based paints" + ), + new AnimTestcasePaced( + "url(#gradA); url(#gradB); url(#gradC)", + { + comp0: 'url("' + document.URL + '#gradA") rgb(0, 0, 0)', + comp1_6: 'url("' + document.URL + '#gradA") rgb(0, 0, 0)', + comp1_3: 'url("' + document.URL + '#gradB") rgb(0, 0, 0)', + comp2_3: 'url("' + document.URL + '#gradC") rgb(0, 0, 0)', + comp1: 'url("' + document.URL + '#gradC") rgb(0, 0, 0)', + }, + "need support for URI-based paints" + ), + ], + lengthNoUnits: [ + new AnimTestcasePaced("2; 0; 4", { + comp0: "2px", + comp1_6: "1px", + comp1_3: "0px", + comp2_3: "2px", + comp1: "4px", + }), + new AnimTestcasePaced("10; 12; 8", { + comp0: "10px", + comp1_6: "11px", + comp1_3: "12px", + comp2_3: "10px", + comp1: "8px", + }), + ], + lengthPx: [ + new AnimTestcasePaced("0px; 2px; 6px", { + comp0: "0px", + comp1_6: "1px", + comp1_3: "2px", + comp2_3: "4px", + comp1: "6px", + }), + new AnimTestcasePaced("10px; 12px; 8px", { + comp0: "10px", + comp1_6: "11px", + comp1_3: "12px", + comp2_3: "10px", + comp1: "8px", + }), + ], + lengthPctSVG: [ + new AnimTestcasePaced("5%; 6%; 4%", { + comp0: "5%", + comp1_6: "5.5%", + comp1_3: "6%", + comp2_3: "5%", + comp1: "4%", + }), + ], + lengthPxPctSVG: [ + new AnimTestcasePaced( + "0px; 1%; 6px", + { + comp0: "0px", + comp1_6: "1px", + comp1_3: "1%", + comp2_3: "4px", + comp1: "6px", + }, + "need support for interpolating between " + "px and percent values" + ), + ], + opacity: [ + new AnimTestcasePaced("0; 0.2; 0.6", { + comp0: "0", + comp1_6: "0.1", + comp1_3: "0.2", + comp2_3: "0.4", + comp1: "0.6", + }), + new AnimTestcasePaced("0.7; 1.0; 0.4", { + comp0: "0.7", + comp1_6: "0.85", + comp1_3: "1", + comp2_3: "0.7", + comp1: "0.4", + }), + ], + rect: [ + new AnimTestcasePaced( + "rect(2px, 4px, 6px, 8px); " + + "rect(4px, 8px, 12px, 16px); " + + "rect(8px, 16px, 24px, 32px)", + { + comp0: "rect(2px, 4px, 6px, 8px)", + comp1_6: "rect(3px, 6px, 9px, 12px)", + comp1_3: "rect(4px, 8px, 12px, 16px)", + comp2_3: "rect(6px, 12px, 18px, 24px)", + comp1: "rect(8px, 16px, 24px, 32px)", + } + ), + new AnimTestcasePaced( + "rect(10px, 10px, 10px, 10px); " + + "rect(20px, 10px, 50px, 8px); " + + "rect(20px, 30px, 130px, 4px)", + { + comp0: "rect(10px, 10px, 10px, 10px)", + comp1_6: "rect(15px, 10px, 30px, 9px)", + comp1_3: "rect(20px, 10px, 50px, 8px)", + comp2_3: "rect(20px, 20px, 90px, 6px)", + comp1: "rect(20px, 30px, 130px, 4px)", + } + ), + new AnimTestcasePaced( + "rect(10px, auto, 10px, 10px); " + + "rect(20px, auto, 50px, 8px); " + + "rect(40px, auto, 130px, 4px)", + { + comp0: "rect(10px, auto, 10px, 10px)", + comp1_6: "rect(15px, auto, 30px, 9px)", + comp1_3: "rect(20px, auto, 50px, 8px)", + comp2_3: "rect(30px, auto, 90px, 6px)", + comp1: "rect(40px, auto, 130px, 4px)", + } + ), + // Paced-mode animation is not supported in these next few cases + // (Can't compute subcomponent distance between 'auto' & px-values) + new AnimTestcasePaced( + "rect(10px, 10px, 10px, auto); " + + "rect(20px, 10px, 50px, 8px); " + + "rect(20px, 30px, 130px, 4px)", + { + comp0: "rect(10px, 10px, 10px, auto)", + comp1_6: "rect(10px, 10px, 10px, auto)", + comp1_3: "rect(20px, 10px, 50px, 8px)", + comp2_3: "rect(20px, 30px, 130px, 4px)", + comp1: "rect(20px, 30px, 130px, 4px)", + } + ), + new AnimTestcasePaced( + "rect(10px, 10px, 10px, 10px); " + + "rect(20px, 10px, 50px, 8px); " + + "auto", + { + comp0: "rect(10px, 10px, 10px, 10px)", + comp1_6: "rect(10px, 10px, 10px, 10px)", + comp1_3: "rect(20px, 10px, 50px, 8px)", + comp2_3: "auto", + comp1: "auto", + } + ), + new AnimTestcasePaced( + "auto; " + "auto; " + "rect(20px, 30px, 130px, 4px)", + { + comp0: "auto", + comp1_6: "auto", + comp1_3: "auto", + comp2_3: "rect(20px, 30px, 130px, 4px)", + comp1: "rect(20px, 30px, 130px, 4px)", + } + ), + new AnimTestcasePaced("auto; auto; auto", { + comp0: "auto", + comp1_6: "auto", + comp1_3: "auto", + comp2_3: "auto", + comp1: "auto", + }), + ], +}; + +// TODO: test more properties here. +var gPacedBundles = [ + new TestcaseBundle(gPropList.clip, _pacedTestLists.rect), + new TestcaseBundle( + gPropList.color, + [].concat(_pacedTestLists.color, _pacedTestLists.currentColor_color) + ), + new TestcaseBundle(gPropList.direction, [ + new AnimTestcasePaced("rtl; ltr; rtl"), + ]), + new TestcaseBundle( + gPropList.fill, + [].concat( + _pacedTestLists.color, + _pacedTestLists.currentColor_fill, + _pacedTestLists.paintServer + ) + ), + new TestcaseBundle( + gPropList.font_size, + [].concat(_pacedTestLists.lengthNoUnits, _pacedTestLists.lengthPx, [ + new AnimTestcasePaced("20%; 24%; 16%", { + comp0: "10px", + comp1_6: "11px", + comp1_3: "12px", + comp2_3: "10px", + comp1: "8px", + }), + new AnimTestcasePaced("0px; 4%; 6px", { + comp0: "0px", + comp1_6: "1px", + comp1_3: "2px", + comp2_3: "4px", + comp1: "6px", + }), + ]) + ), + new TestcaseBundle(gPropList.font_size_adjust, [ + new AnimTestcasePaced("0.2; 0.6; 0.8", { + comp0: "0.2", + comp1_6: "0.3", + comp1_3: "0.4", + comp2_3: "0.6", + comp1: "0.8", + }), + new AnimTestcasePaced("none; none; 0.5", { + comp0: "none", + comp1_6: "none", + comp1_3: "none", + comp2_3: "0.5", + comp1: "0.5", + }), + ]), + new TestcaseBundle(gPropList.font_family, [ + // Sanity check: 'font-family' isn't interpolatable. It should end up + // ignoring the calcMode="paced" and falling into discrete-mode. + new AnimTestcasePaced( + "serif; sans-serif; monospace", + { + comp0: "serif", + comp1_6: "serif", + comp1_3: "sans-serif", + comp2_3: "monospace", + comp1: "monospace", + }, + "need support for more font properties" + ), + ]), + new TestcaseBundle(gPropList.opacity, _pacedTestLists.opacity), + new TestcaseBundle( + gPropList.stroke_dasharray, + [].concat(_pacedTestLists.lengthPctSVG, [ + new AnimTestcasePaced("7, 7, 7; 7, 10, 3; 1, 2, 3", { + comp0: "7px, 7px, 7px", + comp1_6: "7px, 8.5px, 5px", + comp1_3: "7px, 10px, 3px", + comp2_3: "4px, 6px, 3px", + comp1: "1px, 2px, 3px", + }), + ]) + ), + new TestcaseBundle( + gPropList.stroke_dashoffset, + [].concat( + _pacedTestLists.lengthNoUnits, + _pacedTestLists.lengthPx, + _pacedTestLists.lengthPctSVG, + _pacedTestLists.lengthPxPctSVG + ) + ), + new TestcaseBundle( + gPropList.stroke_width, + [].concat( + _pacedTestLists.lengthNoUnits, + _pacedTestLists.lengthPx, + _pacedTestLists.lengthPctSVG, + _pacedTestLists.lengthPxPctSVG + ) + ), +]; diff --git a/dom/smil/test/db_smilCSSPropertyList.js b/dom/smil/test/db_smilCSSPropertyList.js new file mode 100644 index 0000000000..237c9db585 --- /dev/null +++ b/dom/smil/test/db_smilCSSPropertyList.js @@ -0,0 +1,104 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +/* list of CSS properties recognized by SVG 1.1 spec, for use in mochitests */ + +// List of CSS Properties from SVG 1.1 Specification, Appendix N +var gPropList = { + // NOTE: AnimatedAttribute signature is: + // (attrName, attrType, sampleTarget, isAnimatable, isAdditive) + + // SKIP 'alignment-baseline' property: animatable but not supported by Mozilla + // SKIP 'baseline-shift' property: animatable but not supported by Mozilla + clip: new AdditiveAttribute("clip", "CSS", "marker"), + clip_path: new NonAdditiveAttribute("clip-path", "CSS", "rect"), + clip_rule: new NonAdditiveAttribute("clip-rule", "CSS", "circle"), + color: new AdditiveAttribute("color", "CSS", "rect"), + color_interpolation: new NonAdditiveAttribute( + "color-interpolation", + "CSS", + "rect" + ), + color_interpolation_filters: new NonAdditiveAttribute( + "color-interpolation-filters", + "CSS", + "feFlood" + ), + // SKIP 'color-profile' property: animatable but not supported by Mozilla + cursor: new NonAdditiveAttribute("cursor", "CSS", "rect"), + direction: new NonAnimatableAttribute("direction", "CSS", "text"), + display: new NonAdditiveAttribute("display", "CSS", "rect"), + dominant_baseline: new NonAdditiveAttribute( + "dominant-baseline", + "CSS", + "text" + ), + enable_background: + // NOTE: Not supported by Mozilla, but explicitly non-animatable + new NonAnimatableAttribute("enable-background", "CSS", "marker"), + fill: new AdditiveAttribute("fill", "CSS", "rect"), + fill_opacity: new AdditiveAttribute("fill-opacity", "CSS", "rect"), + fill_rule: new NonAdditiveAttribute("fill-rule", "CSS", "rect"), + filter: new NonAdditiveAttribute("filter", "CSS", "rect"), + flood_color: new AdditiveAttribute("flood-color", "CSS", "feFlood"), + flood_opacity: new AdditiveAttribute("flood-opacity", "CSS", "feFlood"), + font: new NonAdditiveAttribute("font", "CSS", "text"), + font_family: new NonAdditiveAttribute("font-family", "CSS", "text"), + font_size: new AdditiveAttribute("font-size", "CSS", "text"), + font_size_adjust: new NonAdditiveAttribute("font-size-adjust", "CSS", "text"), + font_stretch: new NonAdditiveAttribute("font-stretch", "CSS", "text"), + font_style: new NonAdditiveAttribute("font-style", "CSS", "text"), + font_variant: new NonAdditiveAttribute("font-variant", "CSS", "text"), + // XXXdholbert should 'font-weight' be additive? + font_weight: new NonAdditiveAttribute("font-weight", "CSS", "text"), + glyph_orientation_horizontal: + // NOTE: Not supported by Mozilla, but explicitly non-animatable + NonAnimatableAttribute("glyph-orientation-horizontal", "CSS", "text"), + glyph_orientation_vertical: + // NOTE: Not supported by Mozilla, but explicitly non-animatable + NonAnimatableAttribute("glyph-orientation-horizontal", "CSS", "text"), + image_rendering: NonAdditiveAttribute("image-rendering", "CSS", "image"), + // SKIP 'kerning' property: animatable but not supported by Mozilla + letter_spacing: new AdditiveAttribute("letter-spacing", "CSS", "text"), + lighting_color: new AdditiveAttribute( + "lighting-color", + "CSS", + "feDiffuseLighting" + ), + marker: new NonAdditiveAttribute("marker", "CSS", "line"), + marker_end: new NonAdditiveAttribute("marker-end", "CSS", "line"), + marker_mid: new NonAdditiveAttribute("marker-mid", "CSS", "line"), + marker_start: new NonAdditiveAttribute("marker-start", "CSS", "line"), + mask: new NonAdditiveAttribute("mask", "CSS", "line"), + opacity: new AdditiveAttribute("opacity", "CSS", "rect"), + overflow: new NonAdditiveAttribute("overflow", "CSS", "marker"), + pointer_events: new NonAdditiveAttribute("pointer-events", "CSS", "rect"), + shape_rendering: new NonAdditiveAttribute("shape-rendering", "CSS", "rect"), + stop_color: new AdditiveAttribute("stop-color", "CSS", "stop"), + stop_opacity: new AdditiveAttribute("stop-opacity", "CSS", "stop"), + stroke: new AdditiveAttribute("stroke", "CSS", "rect"), + stroke_dasharray: new NonAdditiveAttribute("stroke-dasharray", "CSS", "rect"), + stroke_dashoffset: new AdditiveAttribute("stroke-dashoffset", "CSS", "rect"), + stroke_linecap: new NonAdditiveAttribute("stroke-linecap", "CSS", "rect"), + stroke_linejoin: new NonAdditiveAttribute("stroke-linejoin", "CSS", "rect"), + stroke_miterlimit: new AdditiveAttribute("stroke-miterlimit", "CSS", "rect"), + stroke_opacity: new AdditiveAttribute("stroke-opacity", "CSS", "rect"), + stroke_width: new AdditiveAttribute("stroke-width", "CSS", "rect"), + text_anchor: new NonAdditiveAttribute("text-anchor", "CSS", "text"), + text_decoration_line: new NonAdditiveAttribute( + "text-decoration-line", + "CSS", + "text" + ), + text_rendering: new NonAdditiveAttribute("text-rendering", "CSS", "text"), + unicode_bidi: new NonAnimatableAttribute("unicode-bidi", "CSS", "text"), + vector_effect: new NonAdditiveAttribute("vector-effect", "CSS", "rect"), + visibility: new NonAdditiveAttribute("visibility", "CSS", "rect"), + word_spacing: new AdditiveAttribute("word-spacing", "CSS", "text"), + writing_mode: + // NOTE: Not supported by Mozilla, but explicitly non-animatable + new NonAnimatableAttribute("writing-mode", "CSS", "text"), +}; diff --git a/dom/smil/test/db_smilMappedAttrList.js b/dom/smil/test/db_smilMappedAttrList.js new file mode 100644 index 0000000000..81f71ef32b --- /dev/null +++ b/dom/smil/test/db_smilMappedAttrList.js @@ -0,0 +1,148 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +/* List of SVG presentational attributes in the SVG 1.1 spec, for use in + mochitests. (These are the attributes that are mapped to CSS properties) */ + +var gMappedAttrList = { + // NOTE: The list here should match the MappedAttributeEntry arrays in + // SVGElement.cpp + + // PresentationAttributes-FillStroke + fill: new AdditiveAttribute("fill", "XML", "rect"), + fill_opacity: new AdditiveAttribute("fill-opacity", "XML", "rect"), + fill_rule: new NonAdditiveAttribute("fill-rule", "XML", "rect"), + stroke: new AdditiveAttribute("stroke", "XML", "rect"), + stroke_dasharray: new NonAdditiveAttribute("stroke-dasharray", "XML", "rect"), + stroke_dashoffset: new AdditiveAttribute("stroke-dashoffset", "XML", "rect"), + stroke_linecap: new NonAdditiveAttribute("stroke-linecap", "XML", "rect"), + stroke_linejoin: new NonAdditiveAttribute("stroke-linejoin", "XML", "rect"), + stroke_miterlimit: new AdditiveAttribute("stroke-miterlimit", "XML", "rect"), + stroke_opacity: new AdditiveAttribute("stroke-opacity", "XML", "rect"), + stroke_width: new AdditiveAttribute("stroke-width", "XML", "rect"), + + // PresentationAttributes-Graphics + clip_path: new NonAdditiveAttribute("clip-path", "XML", "rect"), + clip_rule: new NonAdditiveAttribute("clip-rule", "XML", "circle"), + color_interpolation: new NonAdditiveAttribute( + "color-interpolation", + "XML", + "rect" + ), + cursor: new NonAdditiveAttribute("cursor", "XML", "rect"), + display: new NonAdditiveAttribute("display", "XML", "rect"), + filter: new NonAdditiveAttribute("filter", "XML", "rect"), + image_rendering: NonAdditiveAttribute("image-rendering", "XML", "image"), + mask: new NonAdditiveAttribute("mask", "XML", "line"), + pointer_events: new NonAdditiveAttribute("pointer-events", "XML", "rect"), + shape_rendering: new NonAdditiveAttribute("shape-rendering", "XML", "rect"), + text_rendering: new NonAdditiveAttribute("text-rendering", "XML", "text"), + visibility: new NonAdditiveAttribute("visibility", "XML", "rect"), + + // PresentationAttributes-TextContentElements + // SKIP 'alignment-baseline' property: animatable but not supported by Mozilla + // SKIP 'baseline-shift' property: animatable but not supported by Mozilla + direction: new NonAnimatableAttribute("direction", "XML", "text"), + dominant_baseline: new NonAdditiveAttribute( + "dominant-baseline", + "XML", + "text" + ), + glyph_orientation_horizontal: + // NOTE: Not supported by Mozilla, but explicitly non-animatable + NonAnimatableAttribute("glyph-orientation-horizontal", "XML", "text"), + glyph_orientation_vertical: + // NOTE: Not supported by Mozilla, but explicitly non-animatable + NonAnimatableAttribute("glyph-orientation-horizontal", "XML", "text"), + // SKIP 'kerning' property: animatable but not supported by Mozilla + letter_spacing: new AdditiveAttribute("letter-spacing", "XML", "text"), + text_anchor: new NonAdditiveAttribute("text-anchor", "XML", "text"), + text_decoration_line: new NonAdditiveAttribute( + "text-decoration-line", + "XML", + "text" + ), + unicode_bidi: new NonAnimatableAttribute("unicode-bidi", "XML", "text"), + word_spacing: new AdditiveAttribute("word-spacing", "XML", "text"), + + // PresentationAttributes-FontSpecification + font_family: new NonAdditiveAttribute("font-family", "XML", "text"), + font_size: new AdditiveAttribute("font-size", "XML", "text"), + font_size_adjust: new NonAdditiveAttribute("font-size-adjust", "XML", "text"), + font_stretch: new NonAdditiveAttribute("font-stretch", "XML", "text"), + font_style: new NonAdditiveAttribute("font-style", "XML", "text"), + font_variant: new NonAdditiveAttribute("font-variant", "XML", "text"), + font_weight: new NonAdditiveAttribute("font-weight", "XML", "text"), + + // PresentationAttributes-GradientStop + stop_color: new AdditiveAttribute("stop-color", "XML", "stop"), + stop_opacity: new AdditiveAttribute("stop-opacity", "XML", "stop"), + + // PresentationAttributes-Viewports + overflow: new NonAdditiveAttribute("overflow", "XML", "marker"), + clip: new AdditiveAttribute("clip", "XML", "marker"), + + // PresentationAttributes-Makers + marker_end: new NonAdditiveAttribute("marker-end", "XML", "line"), + marker_mid: new NonAdditiveAttribute("marker-mid", "XML", "line"), + marker_start: new NonAdditiveAttribute("marker-start", "XML", "line"), + + // PresentationAttributes-Color + color: new AdditiveAttribute("color", "XML", "rect"), + + // PresentationAttributes-Filters + color_interpolation_filters: new NonAdditiveAttribute( + "color-interpolation-filters", + "XML", + "feFlood" + ), + + // PresentationAttributes-feFlood + flood_color: new AdditiveAttribute("flood-color", "XML", "feFlood"), + flood_opacity: new AdditiveAttribute("flood-opacity", "XML", "feFlood"), + + // PresentationAttributes-LightingEffects + lighting_color: new AdditiveAttribute( + "lighting-color", + "XML", + "feDiffuseLighting" + ), +}; + +// Utility method to copy a list of TestcaseBundle objects for CSS properties +// into a list of TestcaseBundles for the corresponding mapped attributes. +function convertCSSBundlesToMappedAttr(bundleList) { + // Create mapping of property names to the corresponding + // mapped-attribute object in gMappedAttrList. + var propertyNameToMappedAttr = {}; + for (attributeLabel in gMappedAttrList) { + var propName = gMappedAttrList[attributeLabel].attrName; + propertyNameToMappedAttr[propName] = gMappedAttrList[attributeLabel]; + } + + var convertedBundles = []; + for (var bundleIdx in bundleList) { + var origBundle = bundleList[bundleIdx]; + var propName = origBundle.animatedAttribute.attrName; + if (propertyNameToMappedAttr[propName]) { + // There's a mapped attribute by this name! Duplicate the TestcaseBundle, + // using the Mapped Attribute instead of the CSS Property. + is( + origBundle.animatedAttribute.attrType, + "CSS", + "expecting to be converting from CSS to XML" + ); + convertedBundles.push( + new TestcaseBundle( + propertyNameToMappedAttr[propName], + origBundle.testcaseList, + origBundle.skipReason + ) + ); + } + } + return convertedBundles; +} diff --git a/dom/smil/test/file_smilWithTransition.html b/dom/smil/test/file_smilWithTransition.html new file mode 100644 index 0000000000..b91398436b --- /dev/null +++ b/dom/smil/test/file_smilWithTransition.html @@ -0,0 +1,79 @@ + + + + + + Test SMIL does not trigger CSS Transitions (bug 1315874) + + +Mozilla Bug + 1315874 + + + + + + + + + diff --git a/dom/smil/test/mochitest.toml b/dom/smil/test/mochitest.toml new file mode 100644 index 0000000000..7a3e98fa57 --- /dev/null +++ b/dom/smil/test/mochitest.toml @@ -0,0 +1,109 @@ +[DEFAULT] +support-files = [ + "db_smilAnimateMotion.js", + "db_smilCSSFromBy.js", + "db_smilCSSFromTo.js", + "db_smilCSSPaced.js", + "db_smilCSSPropertyList.js", + "db_smilMappedAttrList.js", + "file_smilWithTransition.html", + "smilAnimateMotionValueLists.js", + "smilExtDoc_helper.svg", + "smilTestUtils.js", + "smilXHR_helper.svg", +] + +["test_smilAccessKey.xhtml"] + +["test_smilAdditionFallback.html"] + +["test_smilAnimateMotion.xhtml"] + +["test_smilAnimateMotionInvalidValues.xhtml"] + +["test_smilAnimateMotionOverrideRules.xhtml"] + +["test_smilBackwardsSeeking.xhtml"] + +["test_smilCSSFontStretchRelative.xhtml"] + +["test_smilCSSFromBy.xhtml"] + +["test_smilCSSFromTo.xhtml"] + +["test_smilCSSInherit.xhtml"] +disabled = "until bug 501183 is fixed" + +["test_smilCSSInvalidValues.xhtml"] + +["test_smilCSSPaced.xhtml"] + +["test_smilChangeAfterFrozen.xhtml"] +skip-if = ["true"] # bug 1358955. + +["test_smilConditionalProcessing.html"] + +["test_smilContainerBinding.xhtml"] + +["test_smilCrossContainer.xhtml"] + +["test_smilDynamicDelayedBeginElement.xhtml"] + +["test_smilExtDoc.xhtml"] + +["test_smilFillMode.xhtml"] + +["test_smilGetSimpleDuration.xhtml"] + +["test_smilGetStartTime.xhtml"] + +["test_smilHyperlinking.xhtml"] + +["test_smilInvalidValues.html"] + +["test_smilKeySplines.xhtml"] + +["test_smilKeyTimes.xhtml"] + +["test_smilKeyTimesPacedMode.xhtml"] + +["test_smilMappedAttrFromBy.xhtml"] + +["test_smilMappedAttrFromTo.xhtml"] + +["test_smilMappedAttrPaced.xhtml"] + +["test_smilMinTiming.html"] + +["test_smilRepeatDuration.html"] + +["test_smilRepeatTiming.xhtml"] + +["test_smilReset.xhtml"] + +["test_smilRestart.xhtml"] + +["test_smilSetCurrentTime.xhtml"] + +["test_smilSync.xhtml"] + +["test_smilSyncTransform.xhtml"] + +["test_smilSyncbaseTarget.xhtml"] + +["test_smilTextZoom.xhtml"] + +["test_smilTiming.xhtml"] + +["test_smilTimingZeroIntervals.xhtml"] + +["test_smilUpdatedInterval.xhtml"] + +["test_smilValues.xhtml"] + +["test_smilWithTransition.html"] +skip-if = ["os == 'android'"] + +["test_smilWithXlink.xhtml"] + +["test_smilXHR.xhtml"] diff --git a/dom/smil/test/smilAnimateMotionValueLists.js b/dom/smil/test/smilAnimateMotionValueLists.js new file mode 100644 index 0000000000..6e05ebd7e1 --- /dev/null +++ b/dom/smil/test/smilAnimateMotionValueLists.js @@ -0,0 +1,116 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +/* Lists of valid & invalid values for the various attributes */ +const gValidValues = [ + "10 10", + "10 10;", // Trailing semicolons are allowed + "10 10; ", + " 10 10em ", + "1 2 ; 3,4", + "1,2;3,4", + "0 0", + "0,0", +]; + +const gInvalidValues = [ + ";10 10", + "10 10;;", + "1 2 3", + "1 2 3 4", + "1,2;3,4 ,", + ",", + " , ", + ";", + " ; ", + "a", + " a; ", + ";a;", + "", + " ", + "1,2;3,4,", + "1,,2", + ",1,2", +]; + +const gValidRotate = [ + "10", + "20.1", + "30.5deg", + "0.5rad", + "auto", + "auto-reverse", + " 10 ", + " 10deg", + "10deg ", + " 10.1 ", +]; + +const gInvalidRotate = ["10 deg", "10 rad ", "aaa"]; + +const gValidToBy = ["0 0", "1em,2", "50.3em 0.2in", " 1,2", "1 2 "]; + +const gInvalidToBy = [ + "0 0 0", + "0 0,0", + "0,0,0", + "1emm 2", + "1 2;", + "1 2,", + " 1,2 ,", + "abc", + ",", + "", + "1,,2", + "1,2,", +]; + +const gValidPath = [ + "m0 0 L30 30", + "M20,20L10 10", + "M20,20 L30, 30h20", + "m50 50", + "M50 50", + "m0 0", + "M0, 0", +]; + +// paths must start with at least a valid "M" segment to be valid +const gInvalidPath = ["M20in 20", "h30", "L50 50", "abc"]; + +// paths that at least start with a valid "M" segment are valid - the spec says +// to parse everything up to the first invalid token +const gValidPathWithErrors = ["M20 20em", "m0 0 L30,,30", "M10 10 L50 50 abc"]; + +const gValidKeyPoints = [ + "0; 0.5; 1", + "0;.5;1", + "0; 0; 1", + "0; 1; 1", + "0; 0; 1;", // Trailing semicolons are allowed + "0; 0; 1; ", + "0; 0.000; 1", + "0; 0.000001; 1", +]; + +// Should have 3 values to be valid. +// Same as number of keyTimes values +const gInvalidKeyPoints = [ + "0; 1", + "0; 0.5; 0.75; 1", + "0; 1;", + "0", + "1", + "a", + "", + " ", + "0; -0.1; 1", + "0; 1.1; 1", + "0; 0.1; 1.1", + "-0.1; 0.1; 1", + "0; a; 1", + "0;;1", +]; diff --git a/dom/smil/test/smilExtDoc_helper.svg b/dom/smil/test/smilExtDoc_helper.svg new file mode 100644 index 0000000000..fbd9d091a4 --- /dev/null +++ b/dom/smil/test/smilExtDoc_helper.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/dom/smil/test/smilTestUtils.js b/dom/smil/test/smilTestUtils.js new file mode 100644 index 0000000000..be270a39bf --- /dev/null +++ b/dom/smil/test/smilTestUtils.js @@ -0,0 +1,1015 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +// Note: Class syntax roughly based on: +// https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Inheritance +const SVG_NS = "http://www.w3.org/2000/svg"; +const XLINK_NS = "http://www.w3.org/1999/xlink"; + +const MPATH_TARGET_ID = "smilTestUtilsTestingPath"; + +function extend(child, supertype) { + child.prototype.__proto__ = supertype.prototype; +} + +// General Utility Methods +var SMILUtil = { + // Returns the first matched node in the document + getSVGRoot() { + return SMILUtil.getFirstElemWithTag("svg"); + }, + + // Returns the first element in the document with the matching tag + getFirstElemWithTag(aTargetTag) { + var elemList = document.getElementsByTagName(aTargetTag); + return !elemList.length ? null : elemList[0]; + }, + + // Simple wrapper for getComputedStyle + getComputedStyleSimple(elem, prop) { + return window.getComputedStyle(elem).getPropertyValue(prop); + }, + + getAttributeValue(elem, attr) { + if (attr.attrName == SMILUtil.getMotionFakeAttributeName()) { + // Fake motion "attribute" -- "computed value" is the element's CTM + return elem.getCTM(); + } + if (attr.attrType == "CSS") { + return SMILUtil.getComputedStyleWrapper(elem, attr.attrName); + } + if (attr.attrType == "XML") { + // XXXdholbert This is appropriate for mapped attributes, but not + // for other attributes. + return SMILUtil.getComputedStyleWrapper(elem, attr.attrName); + } + throw new Error(`Unexpected attribute value ${attr.attrType}`); + }, + + // Smart wrapper for getComputedStyle, which will generate a "fake" computed + // style for recognized shorthand properties (font, font-variant, overflow, marker) + getComputedStyleWrapper(elem, propName) { + // Special cases for shorthand properties (which aren't directly queriable + // via getComputedStyle) + var computedStyle; + if (propName == "font") { + var subProps = [ + "font-style", + "font-variant-caps", + "font-weight", + "font-size", + "line-height", + "font-family", + ]; + for (var i in subProps) { + var subPropStyle = SMILUtil.getComputedStyleSimple(elem, subProps[i]); + if (subPropStyle) { + if (subProps[i] == "line-height") { + // There needs to be a "/" before line-height + subPropStyle = "/ " + subPropStyle; + } + if (!computedStyle) { + computedStyle = subPropStyle; + } else { + computedStyle = computedStyle + " " + subPropStyle; + } + } + } + } else if (propName == "font-variant") { + // xxx - this isn't completely correct but it's sufficient for what's + // being tested here + computedStyle = SMILUtil.getComputedStyleSimple( + elem, + "font-variant-caps" + ); + } else if (propName == "marker") { + var subProps = ["marker-end", "marker-mid", "marker-start"]; + for (var i in subProps) { + if (!computedStyle) { + computedStyle = SMILUtil.getComputedStyleSimple(elem, subProps[i]); + } else { + is( + computedStyle, + SMILUtil.getComputedStyleSimple(elem, subProps[i]), + "marker sub-properties should match each other " + + "(they shouldn't be individually set)" + ); + } + } + } else if (propName == "overflow") { + var subProps = ["overflow-x", "overflow-y"]; + for (var i in subProps) { + if (!computedStyle) { + computedStyle = SMILUtil.getComputedStyleSimple(elem, subProps[i]); + } else { + is( + computedStyle, + SMILUtil.getComputedStyleSimple(elem, subProps[i]), + "overflow sub-properties should match each other " + + "(they shouldn't be individually set)" + ); + } + } + } else { + computedStyle = SMILUtil.getComputedStyleSimple(elem, propName); + } + return computedStyle; + }, + + getMotionFakeAttributeName() { + return "_motion"; + }, + + // Return stripped px value from specified value. + stripPx: str => str.replace(/px\s*$/, ""), +}; + +var CTMUtil = { + CTM_COMPONENTS_ALL: ["a", "b", "c", "d", "e", "f"], + CTM_COMPONENTS_ROTATE: ["a", "b", "c", "d"], + + // Function to generate a CTM Matrix from a "summary" + // (a 3-tuple containing [tX, tY, theta]) + generateCTM(aCtmSummary) { + if (!aCtmSummary || aCtmSummary.length != 3) { + ok(false, "Unexpected CTM summary tuple length: " + aCtmSummary.length); + } + var tX = aCtmSummary[0]; + var tY = aCtmSummary[1]; + var theta = aCtmSummary[2]; + var cosTheta = Math.cos(theta); + var sinTheta = Math.sin(theta); + var newCtm = { + a: cosTheta, + c: -sinTheta, + e: tX, + b: sinTheta, + d: cosTheta, + f: tY, + }; + return newCtm; + }, + + /// Helper for isCtmEqual + isWithinDelta(aTestVal, aExpectedVal, aErrMsg, aIsTodo) { + var testFunc = aIsTodo ? todo : ok; + const delta = 0.00001; // allowing margin of error = 10^-5 + ok( + aTestVal >= aExpectedVal - delta && aTestVal <= aExpectedVal + delta, + aErrMsg + " | got: " + aTestVal + ", expected: " + aExpectedVal + ); + }, + + assertCTMEqual(aLeftCtm, aRightCtm, aComponentsToCheck, aErrMsg, aIsTodo) { + var foundCTMDifference = false; + for (var j in aComponentsToCheck) { + var curComponent = aComponentsToCheck[j]; + if (!aIsTodo) { + CTMUtil.isWithinDelta( + aLeftCtm[curComponent], + aRightCtm[curComponent], + aErrMsg + " | component: " + curComponent, + false + ); + } else if (aLeftCtm[curComponent] != aRightCtm[curComponent]) { + foundCTMDifference = true; + } + } + + if (aIsTodo) { + todo(!foundCTMDifference, aErrMsg + " | (currently marked todo)"); + } + }, + + assertCTMNotEqual(aLeftCtm, aRightCtm, aComponentsToCheck, aErrMsg, aIsTodo) { + // CTM should not match initial one + var foundCTMDifference = false; + for (var j in aComponentsToCheck) { + var curComponent = aComponentsToCheck[j]; + if (aLeftCtm[curComponent] != aRightCtm[curComponent]) { + foundCTMDifference = true; + break; // We found a difference, as expected. Success! + } + } + + if (aIsTodo) { + todo(foundCTMDifference, aErrMsg + " | (currently marked todo)"); + } else { + ok(foundCTMDifference, aErrMsg); + } + }, +}; + +// Wrapper for timing information +function SMILTimingData(aBegin, aDur) { + this._begin = aBegin; + this._dur = aDur; +} +SMILTimingData.prototype = { + _begin: null, + _dur: null, + getBeginTime() { + return this._begin; + }, + getDur() { + return this._dur; + }, + getEndTime() { + return this._begin + this._dur; + }, + getFractionalTime(aPortion) { + return this._begin + aPortion * this._dur; + }, +}; + +/** + * Attribute: a container for information about an attribute we'll + * attempt to animate with SMIL in our tests. + * + * See also the factory methods below: NonAnimatableAttribute(), + * NonAdditiveAttribute(), and AdditiveAttribute(). + * + * @param aAttrName The name of the attribute + * @param aAttrType The type of the attribute ("CSS" vs "XML") + * @param aTargetTag The name of an element that this attribute could be + * applied to. + * @param aIsAnimatable A bool indicating whether this attribute is defined as + * animatable in the SVG spec. + * @param aIsAdditive A bool indicating whether this attribute is defined as + * additive (i.e. supports "by" animation) in the SVG spec. + */ +function Attribute( + aAttrName, + aAttrType, + aTargetTag, + aIsAnimatable, + aIsAdditive +) { + this.attrName = aAttrName; + this.attrType = aAttrType; + this.targetTag = aTargetTag; + this.isAnimatable = aIsAnimatable; + this.isAdditive = aIsAdditive; +} +Attribute.prototype = { + // Member variables + attrName: null, + attrType: null, + isAnimatable: null, + testcaseList: null, +}; + +// Generators for Attribute objects. These allow lists of attribute +// definitions to be more human-readible than if we were using Attribute() with +// boolean flags, e.g. "Attribute(..., true, true), Attribute(..., true, false) +function NonAnimatableAttribute(aAttrName, aAttrType, aTargetTag) { + return new Attribute(aAttrName, aAttrType, aTargetTag, false, false); +} +function NonAdditiveAttribute(aAttrName, aAttrType, aTargetTag) { + return new Attribute(aAttrName, aAttrType, aTargetTag, true, false); +} +function AdditiveAttribute(aAttrName, aAttrType, aTargetTag) { + return new Attribute(aAttrName, aAttrType, aTargetTag, true, true); +} + +/** + * TestcaseBundle: a container for a group of tests for a particular attribute + * + * @param aAttribute An Attribute object for the attribute + * @param aTestcaseList An array of AnimTestcase objects + */ +function TestcaseBundle(aAttribute, aTestcaseList, aSkipReason) { + this.animatedAttribute = aAttribute; + this.testcaseList = aTestcaseList; + this.skipReason = aSkipReason; +} +TestcaseBundle.prototype = { + // Member variables + animatedAttribute: null, + testcaseList: null, + skipReason: null, + + // Methods + go(aTimingData) { + if (this.skipReason) { + todo( + false, + "Skipping a bundle for '" + + this.animatedAttribute.attrName + + "' because: " + + this.skipReason + ); + } else { + // Sanity Check: Bundle should have > 0 testcases + if (!this.testcaseList || !this.testcaseList.length) { + ok( + false, + "a bundle for '" + + this.animatedAttribute.attrName + + "' has no testcases" + ); + } + + var targetElem = SMILUtil.getFirstElemWithTag( + this.animatedAttribute.targetTag + ); + + if (!targetElem) { + ok( + false, + "Error: can't find an element of type '" + + this.animatedAttribute.targetTag + + "', so I can't test property '" + + this.animatedAttribute.attrName + + "'" + ); + return; + } + + for (var testcaseIdx in this.testcaseList) { + var testcase = this.testcaseList[testcaseIdx]; + if (testcase.skipReason) { + todo( + false, + "Skipping a testcase for '" + + this.animatedAttribute.attrName + + "' because: " + + testcase.skipReason + ); + } else { + testcase.runTest( + targetElem, + this.animatedAttribute, + aTimingData, + false + ); + testcase.runTest( + targetElem, + this.animatedAttribute, + aTimingData, + true + ); + } + } + } + }, +}; + +/** + * AnimTestcase: an abstract class that represents an animation testcase. + * (e.g. a set of "from"/"to" values to test) + */ +function AnimTestcase() {} // abstract => no constructor +AnimTestcase.prototype = { + // Member variables + _animElementTagName: "animate", // Can be overridden for e.g. animateColor + computedValMap: null, + skipReason: null, + + // Methods + /** + * runTest: Runs this AnimTestcase + * + * @param aTargetElem The node to be targeted in our test animation. + * @param aTargetAttr An Attribute object representing the attribute + * to be targeted in our test animation. + * @param aTimeData A SMILTimingData object with timing information for + * our test animation. + * @param aIsFreeze If true, indicates that our test animation should use + * fill="freeze"; otherwise, we'll default to fill="remove". + */ + runTest(aTargetElem, aTargetAttr, aTimeData, aIsFreeze) { + // SANITY CHECKS + if (!SMILUtil.getSVGRoot().animationsPaused()) { + ok(false, "Should start each test with animations paused"); + } + if (SMILUtil.getSVGRoot().getCurrentTime() != 0) { + ok(false, "Should start each test at time = 0"); + } + + // SET UP + // Cache initial computed value + var baseVal = SMILUtil.getAttributeValue(aTargetElem, aTargetAttr); + + // Create & append animation element + var anim = this.setupAnimationElement(aTargetAttr, aTimeData, aIsFreeze); + aTargetElem.appendChild(anim); + + // Build a list of [seek-time, expectedValue, errorMessage] triplets + var seekList = this.buildSeekList( + aTargetAttr, + baseVal, + aTimeData, + aIsFreeze + ); + + // DO THE ACTUAL TESTING + this.seekAndTest(seekList, aTargetElem, aTargetAttr); + + // CLEAN UP + aTargetElem.removeChild(anim); + SMILUtil.getSVGRoot().setCurrentTime(0); + }, + + // HELPER FUNCTIONS + // setupAnimationElement: element + // Subclasses should extend this parent method + setupAnimationElement(aAnimAttr, aTimeData, aIsFreeze) { + var animElement = document.createElementNS( + SVG_NS, + this._animElementTagName + ); + animElement.setAttribute("attributeName", aAnimAttr.attrName); + animElement.setAttribute("attributeType", aAnimAttr.attrType); + animElement.setAttribute("begin", aTimeData.getBeginTime()); + animElement.setAttribute("dur", aTimeData.getDur()); + if (aIsFreeze) { + animElement.setAttribute("fill", "freeze"); + } + return animElement; + }, + + buildSeekList(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) { + if (!aAnimAttr.isAnimatable) { + return this.buildSeekListStatic( + aAnimAttr, + aBaseVal, + aTimeData, + "defined as non-animatable in SVG spec" + ); + } + if (this.computedValMap.noEffect) { + return this.buildSeekListStatic( + aAnimAttr, + aBaseVal, + aTimeData, + "testcase specified to have no effect" + ); + } + return this.buildSeekListAnimated( + aAnimAttr, + aBaseVal, + aTimeData, + aIsFreeze + ); + }, + + seekAndTest(aSeekList, aTargetElem, aTargetAttr) { + var svg = document.getElementById("svg"); + for (var i in aSeekList) { + var entry = aSeekList[i]; + SMILUtil.getSVGRoot().setCurrentTime(entry[0]); + + // Bug 1379908: The computed value of stroke-* properties should be + // serialized with px units, but currently Gecko and Servo don't do that + // when animating these values. + if ( + ["stroke-width", "stroke-dasharray", "stroke-dashoffset"].includes( + aTargetAttr.attrName + ) + ) { + var attr = SMILUtil.stripPx( + SMILUtil.getAttributeValue(aTargetElem, aTargetAttr) + ); + var expectedVal = SMILUtil.stripPx(entry[1]); + is(attr, expectedVal, entry[2]); + return; + } + is( + SMILUtil.getAttributeValue(aTargetElem, aTargetAttr), + entry[1], + entry[2] + ); + } + }, + + // methods that expect to be overridden in subclasses + buildSeekListStatic(aAnimAttr, aBaseVal, aTimeData, aReasonStatic) {}, + buildSeekListAnimated(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) {}, +}; + +// Abstract parent class to share code between from-to & from-by testcases. +function AnimTestcaseFrom() {} // abstract => no constructor +AnimTestcaseFrom.prototype = { + // Member variables + from: null, + + // Methods + setupAnimationElement(aAnimAttr, aTimeData, aIsFreeze) { + // Call super, and then add my own customization + var animElem = AnimTestcase.prototype.setupAnimationElement.apply(this, [ + aAnimAttr, + aTimeData, + aIsFreeze, + ]); + animElem.setAttribute("from", this.from); + return animElem; + }, + + buildSeekListStatic(aAnimAttr, aBaseVal, aTimeData, aReasonStatic) { + var seekList = new Array(); + var msgPrefix = + aAnimAttr.attrName + ": shouldn't be affected by animation "; + seekList.push([ + aTimeData.getBeginTime(), + aBaseVal, + msgPrefix + "(at animation begin) - " + aReasonStatic, + ]); + seekList.push([ + aTimeData.getFractionalTime(1 / 2), + aBaseVal, + msgPrefix + "(at animation mid) - " + aReasonStatic, + ]); + seekList.push([ + aTimeData.getEndTime(), + aBaseVal, + msgPrefix + "(at animation end) - " + aReasonStatic, + ]); + seekList.push([ + aTimeData.getEndTime() + aTimeData.getDur(), + aBaseVal, + msgPrefix + "(after animation end) - " + aReasonStatic, + ]); + return seekList; + }, + + buildSeekListAnimated(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) { + var seekList = new Array(); + var msgPrefix = aAnimAttr.attrName + ": "; + if (aTimeData.getBeginTime() > 0.1) { + seekList.push([ + aTimeData.getBeginTime() - 0.1, + aBaseVal, + msgPrefix + + "checking that base value is set " + + "before start of animation", + ]); + } + + seekList.push([ + aTimeData.getBeginTime(), + this.computedValMap.fromComp || this.from, + msgPrefix + + "checking that 'from' value is set " + + "at start of animation", + ]); + seekList.push([ + aTimeData.getFractionalTime(1 / 2), + this.computedValMap.midComp || this.computedValMap.toComp || this.to, + msgPrefix + "checking value halfway through animation", + ]); + + var finalMsg; + var expectedEndVal; + if (aIsFreeze) { + expectedEndVal = this.computedValMap.toComp || this.to; + finalMsg = msgPrefix + "[freeze-mode] checking that final value is set "; + } else { + expectedEndVal = aBaseVal; + finalMsg = + msgPrefix + "[remove-mode] checking that animation is cleared "; + } + seekList.push([ + aTimeData.getEndTime(), + expectedEndVal, + finalMsg + "at end of animation", + ]); + seekList.push([ + aTimeData.getEndTime() + aTimeData.getDur(), + expectedEndVal, + finalMsg + "after end of animation", + ]); + return seekList; + }, +}; +extend(AnimTestcaseFrom, AnimTestcase); + +/* + * A testcase for a simple "from-to" animation + * @param aFrom The 'from' value + * @param aTo The 'to' value + * @param aComputedValMap A hash-map that contains some computed values, + * if they're needed, as follows: + * - fromComp: Computed value version of |aFrom| (if different from |aFrom|) + * - midComp: Computed value that we expect to visit halfway through the + * animation (if different from |aTo|) + * - toComp: Computed value version of |aTo| (if different from |aTo|) + * - noEffect: Special flag -- if set, indicates that this testcase is + * expected to have no effect on the computed value. (e.g. the + * given values are invalid.) + * @param aSkipReason If this test-case is known to currently fail, this + * parameter should be a string explaining why. + * Otherwise, this value should be null (or omitted). + * + */ +function AnimTestcaseFromTo(aFrom, aTo, aComputedValMap, aSkipReason) { + this.from = aFrom; + this.to = aTo; + this.computedValMap = aComputedValMap || {}; // Let aComputedValMap be omitted + this.skipReason = aSkipReason; +} +AnimTestcaseFromTo.prototype = { + // Member variables + to: null, + + // Methods + setupAnimationElement(aAnimAttr, aTimeData, aIsFreeze) { + // Call super, and then add my own customization + var animElem = AnimTestcaseFrom.prototype.setupAnimationElement.apply( + this, + [aAnimAttr, aTimeData, aIsFreeze] + ); + animElem.setAttribute("to", this.to); + return animElem; + }, +}; +extend(AnimTestcaseFromTo, AnimTestcaseFrom); + +/* + * A testcase for a simple "from-by" animation. + * + * @param aFrom The 'from' value + * @param aBy The 'by' value + * @param aComputedValMap A hash-map that contains some computed values that + * we expect to visit, as follows: + * - fromComp: Computed value version of |aFrom| (if different from |aFrom|) + * - midComp: Computed value that we expect to visit halfway through the + * animation (|aFrom| + |aBy|/2) + * - toComp: Computed value of the animation endpoint (|aFrom| + |aBy|) + * - noEffect: Special flag -- if set, indicates that this testcase is + * expected to have no effect on the computed value. (e.g. the + * given values are invalid. Or the attribute may be animatable + * and additive, but the particular "from" & "by" values that + * are used don't support addition.) + * @param aSkipReason If this test-case is known to currently fail, this + * parameter should be a string explaining why. + * Otherwise, this value should be null (or omitted). + */ +function AnimTestcaseFromBy(aFrom, aBy, aComputedValMap, aSkipReason) { + this.from = aFrom; + this.by = aBy; + this.computedValMap = aComputedValMap; + this.skipReason = aSkipReason; + if ( + this.computedValMap && + !this.computedValMap.noEffect && + !this.computedValMap.toComp + ) { + ok(false, "AnimTestcaseFromBy needs expected computed final value"); + } +} +AnimTestcaseFromBy.prototype = { + // Member variables + by: null, + + // Methods + setupAnimationElement(aAnimAttr, aTimeData, aIsFreeze) { + // Call super, and then add my own customization + var animElem = AnimTestcaseFrom.prototype.setupAnimationElement.apply( + this, + [aAnimAttr, aTimeData, aIsFreeze] + ); + animElem.setAttribute("by", this.by); + return animElem; + }, + buildSeekList(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) { + if (!aAnimAttr.isAdditive) { + return this.buildSeekListStatic( + aAnimAttr, + aBaseVal, + aTimeData, + "defined as non-additive in SVG spec" + ); + } + // Just use inherited method + return AnimTestcaseFrom.prototype.buildSeekList.apply(this, [ + aAnimAttr, + aBaseVal, + aTimeData, + aIsFreeze, + ]); + }, +}; +extend(AnimTestcaseFromBy, AnimTestcaseFrom); + +/* + * A testcase for a "paced-mode" animation + * @param aValues An array of values, to be used as the "Values" list + * @param aComputedValMap A hash-map that contains some computed values, + * if they're needed, as follows: + * - comp0: The computed value at the start of the animation + * - comp1_6: The computed value exactly 1/6 through animation + * - comp1_3: The computed value exactly 1/3 through animation + * - comp2_3: The computed value exactly 2/3 through animation + * - comp1: The computed value of the animation endpoint + * The math works out easiest if... + * (a) aValuesString has 3 entries in its values list: vA, vB, vC + * (b) dist(vB, vC) = 2 * dist(vA, vB) + * With this setup, we can come up with expected intermediate values according + * to the following rules: + * - comp0 should be vA + * - comp1_6 should be us halfway between vA and vB + * - comp1_3 should be vB + * - comp2_3 should be halfway between vB and vC + * - comp1 should be vC + * @param aSkipReason If this test-case is known to currently fail, this + * parameter should be a string explaining why. + * Otherwise, this value should be null (or omitted). + */ +function AnimTestcasePaced(aValuesString, aComputedValMap, aSkipReason) { + this.valuesString = aValuesString; + this.computedValMap = aComputedValMap; + this.skipReason = aSkipReason; + if ( + this.computedValMap && + (!this.computedValMap.comp0 || + !this.computedValMap.comp1_6 || + !this.computedValMap.comp1_3 || + !this.computedValMap.comp2_3 || + !this.computedValMap.comp1) + ) { + ok(false, "This AnimTestcasePaced has an incomplete computed value map"); + } +} +AnimTestcasePaced.prototype = { + // Member variables + valuesString: null, + + // Methods + setupAnimationElement(aAnimAttr, aTimeData, aIsFreeze) { + // Call super, and then add my own customization + var animElem = AnimTestcase.prototype.setupAnimationElement.apply(this, [ + aAnimAttr, + aTimeData, + aIsFreeze, + ]); + animElem.setAttribute("values", this.valuesString); + animElem.setAttribute("calcMode", "paced"); + return animElem; + }, + buildSeekListAnimated(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) { + var seekList = new Array(); + var msgPrefix = aAnimAttr.attrName + ": checking value "; + seekList.push([ + aTimeData.getBeginTime(), + this.computedValMap.comp0, + msgPrefix + "at start of animation", + ]); + seekList.push([ + aTimeData.getFractionalTime(1 / 6), + this.computedValMap.comp1_6, + msgPrefix + "1/6 of the way through animation.", + ]); + seekList.push([ + aTimeData.getFractionalTime(1 / 3), + this.computedValMap.comp1_3, + msgPrefix + "1/3 of the way through animation.", + ]); + seekList.push([ + aTimeData.getFractionalTime(2 / 3), + this.computedValMap.comp2_3, + msgPrefix + "2/3 of the way through animation.", + ]); + + var finalMsg; + var expectedEndVal; + if (aIsFreeze) { + expectedEndVal = this.computedValMap.comp1; + finalMsg = + aAnimAttr.attrName + + ": [freeze-mode] checking that final value is set "; + } else { + expectedEndVal = aBaseVal; + finalMsg = + aAnimAttr.attrName + + ": [remove-mode] checking that animation is cleared "; + } + seekList.push([ + aTimeData.getEndTime(), + expectedEndVal, + finalMsg + "at end of animation", + ]); + seekList.push([ + aTimeData.getEndTime() + aTimeData.getDur(), + expectedEndVal, + finalMsg + "after end of animation", + ]); + return seekList; + }, + buildSeekListStatic(aAnimAttr, aBaseVal, aTimeData, aReasonStatic) { + var seekList = new Array(); + var msgPrefix = + aAnimAttr.attrName + ": shouldn't be affected by animation "; + seekList.push([ + aTimeData.getBeginTime(), + aBaseVal, + msgPrefix + "(at animation begin) - " + aReasonStatic, + ]); + seekList.push([ + aTimeData.getFractionalTime(1 / 6), + aBaseVal, + msgPrefix + "(1/6 of the way through animation) - " + aReasonStatic, + ]); + seekList.push([ + aTimeData.getFractionalTime(1 / 3), + aBaseVal, + msgPrefix + "(1/3 of the way through animation) - " + aReasonStatic, + ]); + seekList.push([ + aTimeData.getFractionalTime(2 / 3), + aBaseVal, + msgPrefix + "(2/3 of the way through animation) - " + aReasonStatic, + ]); + seekList.push([ + aTimeData.getEndTime(), + aBaseVal, + msgPrefix + "(at animation end) - " + aReasonStatic, + ]); + seekList.push([ + aTimeData.getEndTime() + aTimeData.getDur(), + aBaseVal, + msgPrefix + "(after animation end) - " + aReasonStatic, + ]); + return seekList; + }, +}; +extend(AnimTestcasePaced, AnimTestcase); + +/* + * A testcase for an animation. + * + * @param aAttrValueHash A hash-map mapping attribute names to values. + * Should include at least 'path', 'values', 'to' + * or 'by' to describe the motion path. + * @param aCtmMap A hash-map that contains summaries of the expected resulting + * CTM at various points during the animation. The CTM is + * summarized as a tuple of three numbers: [tX, tY, theta] + (indicating a translate(tX,tY) followed by a rotate(theta)) + * - ctm0: The CTM summary at the start of the animation + * - ctm1_6: The CTM summary at exactly 1/6 through animation + * - ctm1_3: The CTM summary at exactly 1/3 through animation + * - ctm2_3: The CTM summary at exactly 2/3 through animation + * - ctm1: The CTM summary at the animation endpoint + * + * NOTE: For paced-mode animation (the default for animateMotion), the math + * works out easiest if: + * (a) our motion path has 3 points: vA, vB, vC + * (b) dist(vB, vC) = 2 * dist(vA, vB) + * (See discussion in header comment for AnimTestcasePaced.) + * + * @param aSkipReason If this test-case is known to currently fail, this + * parameter should be a string explaining why. + * Otherwise, this value should be null (or omitted). + */ +function AnimMotionTestcase(aAttrValueHash, aCtmMap, aSkipReason) { + this.attrValueHash = aAttrValueHash; + this.ctmMap = aCtmMap; + this.skipReason = aSkipReason; + if ( + this.ctmMap && + (!this.ctmMap.ctm0 || + !this.ctmMap.ctm1_6 || + !this.ctmMap.ctm1_3 || + !this.ctmMap.ctm2_3 || + !this.ctmMap.ctm1) + ) { + ok(false, "This AnimMotionTestcase has an incomplete CTM map"); + } +} +AnimMotionTestcase.prototype = { + // Member variables + _animElementTagName: "animateMotion", + + // Implementations of inherited methods that we need to override: + // -------------------------------------------------------------- + setupAnimationElement(aAnimAttr, aTimeData, aIsFreeze) { + var animElement = document.createElementNS( + SVG_NS, + this._animElementTagName + ); + animElement.setAttribute("begin", aTimeData.getBeginTime()); + animElement.setAttribute("dur", aTimeData.getDur()); + if (aIsFreeze) { + animElement.setAttribute("fill", "freeze"); + } + for (var attrName in this.attrValueHash) { + if (attrName == "mpath") { + this.createPath(this.attrValueHash[attrName]); + this.createMpath(animElement); + } else { + animElement.setAttribute(attrName, this.attrValueHash[attrName]); + } + } + return animElement; + }, + + createPath(aPathDescription) { + var path = document.createElementNS(SVG_NS, "path"); + path.setAttribute("d", aPathDescription); + path.setAttribute("id", MPATH_TARGET_ID); + return SMILUtil.getSVGRoot().appendChild(path); + }, + + createMpath(aAnimElement) { + var mpath = document.createElementNS(SVG_NS, "mpath"); + mpath.setAttributeNS(XLINK_NS, "href", "#" + MPATH_TARGET_ID); + return aAnimElement.appendChild(mpath); + }, + + // Override inherited seekAndTest method since... + // (a) it expects a computedValMap and we have a computed-CTM map instead + // and (b) it expects we might have no effect (for non-animatable attrs) + buildSeekList(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) { + var seekList = new Array(); + var msgPrefix = "CTM mismatch "; + seekList.push([ + aTimeData.getBeginTime(), + CTMUtil.generateCTM(this.ctmMap.ctm0), + msgPrefix + "at start of animation", + ]); + seekList.push([ + aTimeData.getFractionalTime(1 / 6), + CTMUtil.generateCTM(this.ctmMap.ctm1_6), + msgPrefix + "1/6 of the way through animation.", + ]); + seekList.push([ + aTimeData.getFractionalTime(1 / 3), + CTMUtil.generateCTM(this.ctmMap.ctm1_3), + msgPrefix + "1/3 of the way through animation.", + ]); + seekList.push([ + aTimeData.getFractionalTime(2 / 3), + CTMUtil.generateCTM(this.ctmMap.ctm2_3), + msgPrefix + "2/3 of the way through animation.", + ]); + + var finalMsg; + var expectedEndVal; + if (aIsFreeze) { + expectedEndVal = CTMUtil.generateCTM(this.ctmMap.ctm1); + finalMsg = + aAnimAttr.attrName + + ": [freeze-mode] checking that final value is set "; + } else { + expectedEndVal = aBaseVal; + finalMsg = + aAnimAttr.attrName + + ": [remove-mode] checking that animation is cleared "; + } + seekList.push([ + aTimeData.getEndTime(), + expectedEndVal, + finalMsg + "at end of animation", + ]); + seekList.push([ + aTimeData.getEndTime() + aTimeData.getDur(), + expectedEndVal, + finalMsg + "after end of animation", + ]); + return seekList; + }, + + // Override inherited seekAndTest method + // (Have to use assertCTMEqual() instead of is() for comparison, to check each + // component of the CTM and to allow for a small margin of error.) + seekAndTest(aSeekList, aTargetElem, aTargetAttr) { + var svg = document.getElementById("svg"); + for (var i in aSeekList) { + var entry = aSeekList[i]; + SMILUtil.getSVGRoot().setCurrentTime(entry[0]); + CTMUtil.assertCTMEqual( + aTargetElem.getCTM(), + entry[1], + CTMUtil.CTM_COMPONENTS_ALL, + entry[2], + false + ); + } + }, + + // Override "runTest" method so we can remove any element that we + // created at the end of each test. + runTest(aTargetElem, aTargetAttr, aTimeData, aIsFreeze) { + AnimTestcase.prototype.runTest.apply(this, [ + aTargetElem, + aTargetAttr, + aTimeData, + aIsFreeze, + ]); + var pathElem = document.getElementById(MPATH_TARGET_ID); + if (pathElem) { + SMILUtil.getSVGRoot().removeChild(pathElem); + } + }, +}; +extend(AnimMotionTestcase, AnimTestcase); + +// MAIN METHOD +function testBundleList(aBundleList, aTimingData) { + for (var bundleIdx in aBundleList) { + aBundleList[bundleIdx].go(aTimingData); + } +} diff --git a/dom/smil/test/smilXHR_helper.svg b/dom/smil/test/smilXHR_helper.svg new file mode 100644 index 0000000000..cb0b51c360 --- /dev/null +++ b/dom/smil/test/smilXHR_helper.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/dom/smil/test/test_smilAccessKey.xhtml b/dom/smil/test/test_smilAccessKey.xhtml new file mode 100644 index 0000000000..5910dc1c27 --- /dev/null +++ b/dom/smil/test/test_smilAccessKey.xhtml @@ -0,0 +1,79 @@ + + + Test for SMIL accessKey support + + + + +Mozilla Bug + 587910 +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilAdditionFallback.html b/dom/smil/test/test_smilAdditionFallback.html new file mode 100644 index 0000000000..a73457728c --- /dev/null +++ b/dom/smil/test/test_smilAdditionFallback.html @@ -0,0 +1,30 @@ + + + + + + +

+
+ + + + + +
+
+
+
diff --git a/dom/smil/test/test_smilAnimateMotion.xhtml b/dom/smil/test/test_smilAnimateMotion.xhtml new file mode 100644 index 0000000000..68ac7fb96a --- /dev/null +++ b/dom/smil/test/test_smilAnimateMotion.xhtml @@ -0,0 +1,51 @@ + + + + Test for animateMotion behavior + + + + + + +Mozilla Bug 436418 +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilAnimateMotionInvalidValues.xhtml b/dom/smil/test/test_smilAnimateMotionInvalidValues.xhtml new file mode 100644 index 0000000000..c2d72e5435 --- /dev/null +++ b/dom/smil/test/test_smilAnimateMotionInvalidValues.xhtml @@ -0,0 +1,176 @@ + + + + Test for animateMotion acceptance of invalid values + + + + + diff --git a/dom/smil/test/test_smilAnimateMotionOverrideRules.xhtml b/dom/smil/test/test_smilAnimateMotionOverrideRules.xhtml new file mode 100644 index 0000000000..3a147cd094 --- /dev/null +++ b/dom/smil/test/test_smilAnimateMotionOverrideRules.xhtml @@ -0,0 +1,215 @@ + + + + Test for overriding of path-defining attributes for animateMotion + + + + + diff --git a/dom/smil/test/test_smilBackwardsSeeking.xhtml b/dom/smil/test/test_smilBackwardsSeeking.xhtml new file mode 100644 index 0000000000..20974e6de3 --- /dev/null +++ b/dom/smil/test/test_smilBackwardsSeeking.xhtml @@ -0,0 +1,191 @@ + + + Test for backwards seeking behavior + + + + +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilCSSFontStretchRelative.xhtml b/dom/smil/test/test_smilCSSFontStretchRelative.xhtml new file mode 100644 index 0000000000..01988a881b --- /dev/null +++ b/dom/smil/test/test_smilCSSFontStretchRelative.xhtml @@ -0,0 +1,102 @@ + + + Test for Animation Behavior on CSS Properties + + + + + +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilCSSFromBy.xhtml b/dom/smil/test/test_smilCSSFromBy.xhtml new file mode 100644 index 0000000000..586305fb8a --- /dev/null +++ b/dom/smil/test/test_smilCSSFromBy.xhtml @@ -0,0 +1,49 @@ + + + Test for Animation Behavior on CSS Properties + + + + + + + +

+
+ + + + testing 123 + + + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilCSSFromTo.xhtml b/dom/smil/test/test_smilCSSFromTo.xhtml new file mode 100644 index 0000000000..501adbc4a8 --- /dev/null +++ b/dom/smil/test/test_smilCSSFromTo.xhtml @@ -0,0 +1,76 @@ + + + Test for Animation Behavior on CSS Properties + + + + + + + +

+
+ + + + testing 123 + + + + + + + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilCSSInherit.xhtml b/dom/smil/test/test_smilCSSInherit.xhtml new file mode 100644 index 0000000000..4e262d3aa9 --- /dev/null +++ b/dom/smil/test/test_smilCSSInherit.xhtml @@ -0,0 +1,85 @@ + + + Test for Animation Behavior on CSS Properties + + + + + +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilCSSInvalidValues.xhtml b/dom/smil/test/test_smilCSSInvalidValues.xhtml new file mode 100644 index 0000000000..5e8e682af9 --- /dev/null +++ b/dom/smil/test/test_smilCSSInvalidValues.xhtml @@ -0,0 +1,59 @@ + + + Test for Animation Behavior on CSS Properties + + + + + + +

+
+ + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilCSSPaced.xhtml b/dom/smil/test/test_smilCSSPaced.xhtml new file mode 100644 index 0000000000..1f6b3509ae --- /dev/null +++ b/dom/smil/test/test_smilCSSPaced.xhtml @@ -0,0 +1,44 @@ + + + Test for Animation Behavior on CSS Properties + + + + + + + +

+
+ + + testing 123 + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilChangeAfterFrozen.xhtml b/dom/smil/test/test_smilChangeAfterFrozen.xhtml new file mode 100644 index 0000000000..97fe131678 --- /dev/null +++ b/dom/smil/test/test_smilChangeAfterFrozen.xhtml @@ -0,0 +1,571 @@ + + + Test for SMIL when things change after an animation is frozen + + + + + +Mozilla Bug 533291 +

+ + +
+
+
+ + diff --git a/dom/smil/test/test_smilConditionalProcessing.html b/dom/smil/test/test_smilConditionalProcessing.html new file mode 100644 index 0000000000..302c445b6e --- /dev/null +++ b/dom/smil/test/test_smilConditionalProcessing.html @@ -0,0 +1,93 @@ + + + + + Test conditional processing tests applied to animations + + + + +

+
+ + + + + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilContainerBinding.xhtml b/dom/smil/test/test_smilContainerBinding.xhtml new file mode 100644 index 0000000000..20e1013d4b --- /dev/null +++ b/dom/smil/test/test_smilContainerBinding.xhtml @@ -0,0 +1,101 @@ + + + + Test for adding and removing animations from a time container + + + + +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilCrossContainer.xhtml b/dom/smil/test/test_smilCrossContainer.xhtml new file mode 100644 index 0000000000..e4d30ef26d --- /dev/null +++ b/dom/smil/test/test_smilCrossContainer.xhtml @@ -0,0 +1,132 @@ + + + + Test for moving animations between time containers + + + + +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilDynamicDelayedBeginElement.xhtml b/dom/smil/test/test_smilDynamicDelayedBeginElement.xhtml new file mode 100644 index 0000000000..10da496454 --- /dev/null +++ b/dom/smil/test/test_smilDynamicDelayedBeginElement.xhtml @@ -0,0 +1,103 @@ + + + + Test for Bug 699143 + + + + + +Mozilla Bug 699143 +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilExtDoc.xhtml b/dom/smil/test/test_smilExtDoc.xhtml new file mode 100644 index 0000000000..0323cbfb77 --- /dev/null +++ b/dom/smil/test/test_smilExtDoc.xhtml @@ -0,0 +1,80 @@ + + + + Test for Bug 628888 - Animations in external document sometimes don't run + + + + +Mozilla Bug 628888 +

+
+ +
+
+
+ + diff --git a/dom/smil/test/test_smilFillMode.xhtml b/dom/smil/test/test_smilFillMode.xhtml new file mode 100644 index 0000000000..ed270863bd --- /dev/null +++ b/dom/smil/test/test_smilFillMode.xhtml @@ -0,0 +1,86 @@ + + + Test for SMIL fill modes + + + + +

+
+ + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilGetSimpleDuration.xhtml b/dom/smil/test/test_smilGetSimpleDuration.xhtml new file mode 100644 index 0000000000..e36922c34a --- /dev/null +++ b/dom/smil/test/test_smilGetSimpleDuration.xhtml @@ -0,0 +1,84 @@ + + + Test for getSimpleDuration Behavior + + + + +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilGetStartTime.xhtml b/dom/smil/test/test_smilGetStartTime.xhtml new file mode 100644 index 0000000000..f854dd7cd9 --- /dev/null +++ b/dom/smil/test/test_smilGetStartTime.xhtml @@ -0,0 +1,103 @@ + + + Test for getStartTime Behavior + + + + +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilHyperlinking.xhtml b/dom/smil/test/test_smilHyperlinking.xhtml new file mode 100644 index 0000000000..49e9e7f7b0 --- /dev/null +++ b/dom/smil/test/test_smilHyperlinking.xhtml @@ -0,0 +1,229 @@ + + + Test for hyperlinking + + + + +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilInvalidValues.html b/dom/smil/test/test_smilInvalidValues.html new file mode 100644 index 0000000000..29ddfad39a --- /dev/null +++ b/dom/smil/test/test_smilInvalidValues.html @@ -0,0 +1,122 @@ + + + + + + Test invalid values cause the model to be updated (bug 941315) + + + + +Mozilla Bug 941315 +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilKeySplines.xhtml b/dom/smil/test/test_smilKeySplines.xhtml new file mode 100644 index 0000000000..c12f6f1e09 --- /dev/null +++ b/dom/smil/test/test_smilKeySplines.xhtml @@ -0,0 +1,296 @@ + + + Test for SMIL keySplines + + + + +

+
+ + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilKeyTimes.xhtml b/dom/smil/test/test_smilKeyTimes.xhtml new file mode 100644 index 0000000000..43d3c91895 --- /dev/null +++ b/dom/smil/test/test_smilKeyTimes.xhtml @@ -0,0 +1,391 @@ + + + Test for SMIL keyTimes + + + + +Mozilla Bug + 557885 +

+
+ + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilKeyTimesPacedMode.xhtml b/dom/smil/test/test_smilKeyTimesPacedMode.xhtml new file mode 100644 index 0000000000..f2d6571fa8 --- /dev/null +++ b/dom/smil/test/test_smilKeyTimesPacedMode.xhtml @@ -0,0 +1,123 @@ + + + Tests updated intervals + + + + +Mozilla Bug 555026 +

+
+ + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilMappedAttrFromBy.xhtml b/dom/smil/test/test_smilMappedAttrFromBy.xhtml new file mode 100644 index 0000000000..5b70b5b83f --- /dev/null +++ b/dom/smil/test/test_smilMappedAttrFromBy.xhtml @@ -0,0 +1,51 @@ + + + Test for Animation Behavior on CSS Properties + + + + + + + + +

+
+ + + + testing 123 + + + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilMappedAttrFromTo.xhtml b/dom/smil/test/test_smilMappedAttrFromTo.xhtml new file mode 100644 index 0000000000..57ef83800e --- /dev/null +++ b/dom/smil/test/test_smilMappedAttrFromTo.xhtml @@ -0,0 +1,79 @@ + + + Test for Animation Behavior on CSS Properties + + + + + + + + +

+
+ + + + testing 123 + + + + + + + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilMappedAttrPaced.xhtml b/dom/smil/test/test_smilMappedAttrPaced.xhtml new file mode 100644 index 0000000000..81adea6390 --- /dev/null +++ b/dom/smil/test/test_smilMappedAttrPaced.xhtml @@ -0,0 +1,46 @@ + + + Test for Animation Behavior on CSS Properties + + + + + + + + +

+
+ + + testing 123 + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilMinTiming.html b/dom/smil/test/test_smilMinTiming.html new file mode 100644 index 0000000000..f0bdd42502 --- /dev/null +++ b/dom/smil/test/test_smilMinTiming.html @@ -0,0 +1,93 @@ + + + + + + Test for Bug 948245 + + + + +Mozilla Bug 948245 +

+
+ + + + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilRepeatDuration.html b/dom/smil/test/test_smilRepeatDuration.html new file mode 100644 index 0000000000..b746bb45d4 --- /dev/null +++ b/dom/smil/test/test_smilRepeatDuration.html @@ -0,0 +1,139 @@ + + + + + + Test for repeat duration calculation (Bug 948245) + + + + +Mozilla Bug 948245 +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilRepeatTiming.xhtml b/dom/smil/test/test_smilRepeatTiming.xhtml new file mode 100644 index 0000000000..ace63d37b0 --- /dev/null +++ b/dom/smil/test/test_smilRepeatTiming.xhtml @@ -0,0 +1,96 @@ + + + + Test repeat timing + + + + +Mozilla Bug + 485157 +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilReset.xhtml b/dom/smil/test/test_smilReset.xhtml new file mode 100644 index 0000000000..a53f73c112 --- /dev/null +++ b/dom/smil/test/test_smilReset.xhtml @@ -0,0 +1,82 @@ + + + Tests for SMIL Reset Behavior + + + + +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilRestart.xhtml b/dom/smil/test/test_smilRestart.xhtml new file mode 100644 index 0000000000..3e03dfdbcc --- /dev/null +++ b/dom/smil/test/test_smilRestart.xhtml @@ -0,0 +1,102 @@ + + + Test for SMIL Restart Behavior + + + + +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilSetCurrentTime.xhtml b/dom/smil/test/test_smilSetCurrentTime.xhtml new file mode 100644 index 0000000000..91ded84c8c --- /dev/null +++ b/dom/smil/test/test_smilSetCurrentTime.xhtml @@ -0,0 +1,76 @@ + + + Test for setCurrentTime Behavior + + + + +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilSync.xhtml b/dom/smil/test/test_smilSync.xhtml new file mode 100644 index 0000000000..36b2a91198 --- /dev/null +++ b/dom/smil/test/test_smilSync.xhtml @@ -0,0 +1,255 @@ + + + Test for SMIL sync behaviour + + + + +

+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilSyncTransform.xhtml b/dom/smil/test/test_smilSyncTransform.xhtml new file mode 100644 index 0000000000..79c5cbf0b9 --- /dev/null +++ b/dom/smil/test/test_smilSyncTransform.xhtml @@ -0,0 +1,66 @@ + + + Test for SMIL sync behaviour for transform types + + + + +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilSyncbaseTarget.xhtml b/dom/smil/test/test_smilSyncbaseTarget.xhtml new file mode 100644 index 0000000000..496cb6751e --- /dev/null +++ b/dom/smil/test/test_smilSyncbaseTarget.xhtml @@ -0,0 +1,180 @@ + + + + Test for syncbase targetting + + + + +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilTextZoom.xhtml b/dom/smil/test/test_smilTextZoom.xhtml new file mode 100644 index 0000000000..5f65bd778e --- /dev/null +++ b/dom/smil/test/test_smilTextZoom.xhtml @@ -0,0 +1,99 @@ + + + Test for SMIL Animation Behavior with textZoom + + + + + +

+
+ + + abc + + + + + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilTiming.xhtml b/dom/smil/test/test_smilTiming.xhtml new file mode 100644 index 0000000000..0dc8525382 --- /dev/null +++ b/dom/smil/test/test_smilTiming.xhtml @@ -0,0 +1,291 @@ + + + Test for SMIL timing + + + + +

+
+ + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilTimingZeroIntervals.xhtml b/dom/smil/test/test_smilTimingZeroIntervals.xhtml new file mode 100644 index 0000000000..d54b74600b --- /dev/null +++ b/dom/smil/test/test_smilTimingZeroIntervals.xhtml @@ -0,0 +1,285 @@ + + + Test for SMIL timing with zero-duration intervals + + + + +

+
+ + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilUpdatedInterval.xhtml b/dom/smil/test/test_smilUpdatedInterval.xhtml new file mode 100644 index 0000000000..3045a815de --- /dev/null +++ b/dom/smil/test/test_smilUpdatedInterval.xhtml @@ -0,0 +1,64 @@ + + + Tests updated intervals + + + + +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilValues.xhtml b/dom/smil/test/test_smilValues.xhtml new file mode 100644 index 0000000000..b25a153472 --- /dev/null +++ b/dom/smil/test/test_smilValues.xhtml @@ -0,0 +1,171 @@ + + + Test for SMIL values + + + + +Mozilla Bug + 474742 +

+
+ + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilWithTransition.html b/dom/smil/test/test_smilWithTransition.html new file mode 100644 index 0000000000..4378841f50 --- /dev/null +++ b/dom/smil/test/test_smilWithTransition.html @@ -0,0 +1,14 @@ + + + + + + +
+
+
diff --git a/dom/smil/test/test_smilWithXlink.xhtml b/dom/smil/test/test_smilWithXlink.xhtml
new file mode 100644
index 0000000000..46ac0f12ef
--- /dev/null
+++ b/dom/smil/test/test_smilWithXlink.xhtml
@@ -0,0 +1,47 @@
+
+
+  Test for animate with xlink:href attribute.
+  
+  
+  
+
+
+

+
+ + + +
+
+
+ + diff --git a/dom/smil/test/test_smilXHR.xhtml b/dom/smil/test/test_smilXHR.xhtml new file mode 100644 index 0000000000..eb0f84268c --- /dev/null +++ b/dom/smil/test/test_smilXHR.xhtml @@ -0,0 +1,88 @@ + + + Test for SMIL Behavior in Data Documents + + + + + +Mozilla Bug 529387 +

+ +
+
+
+ + -- cgit v1.2.3