diff options
Diffstat (limited to 'dom/animation/KeyframeEffectReadOnly.cpp')
-rw-r--r-- | dom/animation/KeyframeEffectReadOnly.cpp | 1430 |
1 files changed, 1430 insertions, 0 deletions
diff --git a/dom/animation/KeyframeEffectReadOnly.cpp b/dom/animation/KeyframeEffectReadOnly.cpp new file mode 100644 index 000000000..639e0b2b0 --- /dev/null +++ b/dom/animation/KeyframeEffectReadOnly.cpp @@ -0,0 +1,1430 @@ +/* -*- 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/dom/KeyframeEffectReadOnly.h" + +#include "mozilla/dom/KeyframeAnimationOptionsBinding.h" + // For UnrestrictedDoubleOrKeyframeAnimationOptions; +#include "mozilla/dom/CSSPseudoElement.h" +#include "mozilla/dom/KeyframeEffectBinding.h" +#include "mozilla/AnimationUtils.h" +#include "mozilla/EffectSet.h" +#include "mozilla/FloatingPoint.h" // For IsFinite +#include "mozilla/LookAndFeel.h" // For LookAndFeel::GetInt +#include "mozilla/KeyframeUtils.h" +#include "mozilla/ServoBindings.h" +#include "mozilla/StyleAnimationValue.h" +#include "Layers.h" // For Layer +#include "nsComputedDOMStyle.h" // nsComputedDOMStyle::GetStyleContextForElement +#include "nsContentUtils.h" // nsContentUtils::ReportToConsole +#include "nsCSSPropertyIDSet.h" +#include "nsCSSProps.h" // For nsCSSProps::PropHasFlags +#include "nsCSSPseudoElements.h" // For CSSPseudoElementType +#include "nsIPresShell.h" +#include "nsIScriptError.h" + +namespace mozilla { + +bool +PropertyValuePair::operator==(const PropertyValuePair& aOther) const +{ + if (mProperty != aOther.mProperty || mValue != aOther.mValue) { + return false; + } + if (mServoDeclarationBlock == aOther.mServoDeclarationBlock) { + return true; + } + if (!mServoDeclarationBlock || !aOther.mServoDeclarationBlock) { + return false; + } + return Servo_DeclarationBlock_Equals(mServoDeclarationBlock, + aOther.mServoDeclarationBlock); +} + +namespace dom { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(KeyframeEffectReadOnly, + AnimationEffectReadOnly, + mTarget) + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(KeyframeEffectReadOnly, + AnimationEffectReadOnly) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(KeyframeEffectReadOnly) +NS_INTERFACE_MAP_END_INHERITING(AnimationEffectReadOnly) + +NS_IMPL_ADDREF_INHERITED(KeyframeEffectReadOnly, AnimationEffectReadOnly) +NS_IMPL_RELEASE_INHERITED(KeyframeEffectReadOnly, AnimationEffectReadOnly) + +KeyframeEffectReadOnly::KeyframeEffectReadOnly( + nsIDocument* aDocument, + const Maybe<OwningAnimationTarget>& aTarget, + const TimingParams& aTiming, + const KeyframeEffectParams& aOptions) + : KeyframeEffectReadOnly(aDocument, aTarget, + new AnimationEffectTimingReadOnly(aDocument, + aTiming), + aOptions) +{ +} + +KeyframeEffectReadOnly::KeyframeEffectReadOnly( + nsIDocument* aDocument, + const Maybe<OwningAnimationTarget>& aTarget, + AnimationEffectTimingReadOnly* aTiming, + const KeyframeEffectParams& aOptions) + : AnimationEffectReadOnly(aDocument, aTiming) + , mTarget(aTarget) + , mEffectOptions(aOptions) + , mInEffectOnLastAnimationTimingUpdate(false) + , mCumulativeChangeHint(nsChangeHint(0)) +{ +} + +JSObject* +KeyframeEffectReadOnly::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) +{ + return KeyframeEffectReadOnlyBinding::Wrap(aCx, this, aGivenProto); +} + +IterationCompositeOperation +KeyframeEffectReadOnly::IterationComposite() const +{ + return mEffectOptions.mIterationComposite; +} + +CompositeOperation +KeyframeEffectReadOnly::Composite() const +{ + return CompositeOperation::Replace; +} + +void +KeyframeEffectReadOnly::NotifyAnimationTimingUpdated() +{ + UpdateTargetRegistration(); + + // If the effect is not relevant it will be removed from the target + // element's effect set. However, effects not in the effect set + // will not be included in the set of candidate effects for running on + // the compositor and hence they won't have their compositor status + // updated. As a result, we need to make sure we clear their compositor + // status here. + bool isRelevant = mAnimation && mAnimation->IsRelevant(); + if (!isRelevant) { + ResetIsRunningOnCompositor(); + } + + // Detect changes to "in effect" status since we need to recalculate the + // animation cascade for this element whenever that changes. + bool inEffect = IsInEffect(); + if (inEffect != mInEffectOnLastAnimationTimingUpdate) { + MarkCascadeNeedsUpdate(); + mInEffectOnLastAnimationTimingUpdate = inEffect; + } + + // Request restyle if necessary. + if (mAnimation && !mProperties.IsEmpty() && HasComputedTimingChanged()) { + EffectCompositor::RestyleType restyleType = + CanThrottle() ? + EffectCompositor::RestyleType::Throttled : + EffectCompositor::RestyleType::Standard; + RequestRestyle(restyleType); + } + + // If we're no longer "in effect", our ComposeStyle method will never be + // called and we will never have a chance to update mProgressOnLastCompose + // and mCurrentIterationOnLastCompose. + // We clear them here to ensure that if we later become "in effect" we will + // request a restyle (above). + if (!inEffect) { + mProgressOnLastCompose.SetNull(); + mCurrentIterationOnLastCompose = 0; + } +} + +static bool +KeyframesEqualIgnoringComputedOffsets(const nsTArray<Keyframe>& aLhs, + const nsTArray<Keyframe>& aRhs) +{ + if (aLhs.Length() != aRhs.Length()) { + return false; + } + + for (size_t i = 0, len = aLhs.Length(); i < len; ++i) { + const Keyframe& a = aLhs[i]; + const Keyframe& b = aRhs[i]; + if (a.mOffset != b.mOffset || + a.mTimingFunction != b.mTimingFunction || + a.mPropertyValues != b.mPropertyValues) { + return false; + } + } + return true; +} + +// https://w3c.github.io/web-animations/#dom-keyframeeffect-setkeyframes +void +KeyframeEffectReadOnly::SetKeyframes(JSContext* aContext, + JS::Handle<JSObject*> aKeyframes, + ErrorResult& aRv) +{ + nsTArray<Keyframe> keyframes = + KeyframeUtils::GetKeyframesFromObject(aContext, mDocument, aKeyframes, aRv); + if (aRv.Failed()) { + return; + } + + RefPtr<nsStyleContext> styleContext = GetTargetStyleContext(); + SetKeyframes(Move(keyframes), styleContext); +} + +void +KeyframeEffectReadOnly::SetKeyframes(nsTArray<Keyframe>&& aKeyframes, + nsStyleContext* aStyleContext) +{ + if (KeyframesEqualIgnoringComputedOffsets(aKeyframes, mKeyframes)) { + return; + } + + mKeyframes = Move(aKeyframes); + // Apply distribute spacing irrespective of the spacing mode. We will apply + // the specified spacing mode when we generate computed animation property + // values from the keyframes since both operations require a style context + // and need to be performed whenever the style context changes. + KeyframeUtils::ApplyDistributeSpacing(mKeyframes); + + if (mAnimation && mAnimation->IsRelevant()) { + nsNodeUtils::AnimationChanged(mAnimation); + } + + if (aStyleContext) { + UpdateProperties(aStyleContext); + MaybeUpdateFrameForCompositor(); + } +} + +const AnimationProperty* +KeyframeEffectReadOnly::GetEffectiveAnimationOfProperty( + nsCSSPropertyID aProperty) const +{ + EffectSet* effectSet = + EffectSet::GetEffectSet(mTarget->mElement, mTarget->mPseudoType); + for (size_t propIdx = 0, propEnd = mProperties.Length(); + propIdx != propEnd; ++propIdx) { + if (aProperty == mProperties[propIdx].mProperty) { + const AnimationProperty* result = &mProperties[propIdx]; + // Skip if there is a property of animation level that is overridden + // by !important rules. + if (effectSet && + effectSet->PropertiesWithImportantRules() + .HasProperty(result->mProperty) && + effectSet->PropertiesForAnimationsLevel() + .HasProperty(result->mProperty)) { + result = nullptr; + } + return result; + } + } + return nullptr; +} + +bool +KeyframeEffectReadOnly::HasAnimationOfProperty(nsCSSPropertyID aProperty) const +{ + for (const AnimationProperty& property : mProperties) { + if (property.mProperty == aProperty) { + return true; + } + } + return false; +} + +#ifdef DEBUG +bool +SpecifiedKeyframeArraysAreEqual(const nsTArray<Keyframe>& aA, + const nsTArray<Keyframe>& aB) +{ + if (aA.Length() != aB.Length()) { + return false; + } + + for (size_t i = 0; i < aA.Length(); i++) { + const Keyframe& a = aA[i]; + const Keyframe& b = aB[i]; + if (a.mOffset != b.mOffset || + a.mTimingFunction != b.mTimingFunction || + a.mPropertyValues != b.mPropertyValues) { + return false; + } + } + + return true; +} +#endif + +void +KeyframeEffectReadOnly::UpdateProperties(nsStyleContext* aStyleContext) +{ + MOZ_ASSERT(aStyleContext); + + nsTArray<AnimationProperty> properties = BuildProperties(aStyleContext); + + if (mProperties == properties) { + return; + } + + // Preserve the state of the mIsRunningOnCompositor flag. + nsCSSPropertyIDSet runningOnCompositorProperties; + + for (const AnimationProperty& property : mProperties) { + if (property.mIsRunningOnCompositor) { + runningOnCompositorProperties.AddProperty(property.mProperty); + } + } + + mProperties = Move(properties); + + for (AnimationProperty& property : mProperties) { + property.mIsRunningOnCompositor = + runningOnCompositorProperties.HasProperty(property.mProperty); + } + + // FIXME (bug 1303235): Do this for Servo too + if (aStyleContext->PresContext()->StyleSet()->IsGecko()) { + CalculateCumulativeChangeHint(aStyleContext); + } + + MarkCascadeNeedsUpdate(); + + RequestRestyle(EffectCompositor::RestyleType::Layer); +} + +void +KeyframeEffectReadOnly::ComposeStyle( + RefPtr<AnimValuesStyleRule>& aStyleRule, + const nsCSSPropertyIDSet& aPropertiesToSkip) +{ + ComputedTiming computedTiming = GetComputedTiming(); + mProgressOnLastCompose = computedTiming.mProgress; + mCurrentIterationOnLastCompose = computedTiming.mCurrentIteration; + + // If the progress is null, we don't have fill data for the current + // time so we shouldn't animate. + if (computedTiming.mProgress.IsNull()) { + return; + } + + for (size_t propIdx = 0, propEnd = mProperties.Length(); + propIdx != propEnd; ++propIdx) + { + const AnimationProperty& prop = mProperties[propIdx]; + + MOZ_ASSERT(prop.mSegments[0].mFromKey == 0.0, "incorrect first from key"); + MOZ_ASSERT(prop.mSegments[prop.mSegments.Length() - 1].mToKey == 1.0, + "incorrect last to key"); + + if (aPropertiesToSkip.HasProperty(prop.mProperty)) { + continue; + } + + MOZ_ASSERT(prop.mSegments.Length() > 0, + "property should not be in animations if it has no segments"); + + // FIXME: Maybe cache the current segment? + const AnimationPropertySegment *segment = prop.mSegments.Elements(), + *segmentEnd = segment + prop.mSegments.Length(); + while (segment->mToKey <= computedTiming.mProgress.Value()) { + MOZ_ASSERT(segment->mFromKey <= segment->mToKey, "incorrect keys"); + if ((segment+1) == segmentEnd) { + break; + } + ++segment; + MOZ_ASSERT(segment->mFromKey == (segment-1)->mToKey, "incorrect keys"); + } + MOZ_ASSERT(segment->mFromKey <= segment->mToKey, "incorrect keys"); + MOZ_ASSERT(segment >= prop.mSegments.Elements() && + size_t(segment - prop.mSegments.Elements()) < + prop.mSegments.Length(), + "out of array bounds"); + + if (!aStyleRule) { + // Allocate the style rule now that we know we have animation data. + aStyleRule = new AnimValuesStyleRule(); + } + + StyleAnimationValue fromValue = segment->mFromValue; + StyleAnimationValue toValue = segment->mToValue; + // Iteration composition for accumulate + if (mEffectOptions.mIterationComposite == + IterationCompositeOperation::Accumulate && + computedTiming.mCurrentIteration > 0) { + const AnimationPropertySegment& lastSegment = + prop.mSegments.LastElement(); + // FIXME: Bug 1293492: Add a utility function to calculate both of + // below StyleAnimationValues. + DebugOnly<bool> accumulateResult = + StyleAnimationValue::Accumulate(prop.mProperty, + fromValue, + lastSegment.mToValue, + computedTiming.mCurrentIteration); + // We can't check the accumulation result in case of filter property. + // That's because some filter property can't accumulate, + // e.g. 'contrast(2) brightness(2)' onto 'brightness(1) contrast(1)' + // because of mismatch of the order. + MOZ_ASSERT(accumulateResult || prop.mProperty == eCSSProperty_filter, + "could not accumulate value"); + accumulateResult = + StyleAnimationValue::Accumulate(prop.mProperty, + toValue, + lastSegment.mToValue, + computedTiming.mCurrentIteration); + MOZ_ASSERT(accumulateResult || prop.mProperty == eCSSProperty_filter, + "could not accumulate value"); + } + + // Special handling for zero-length segments + if (segment->mToKey == segment->mFromKey) { + if (computedTiming.mProgress.Value() < 0) { + aStyleRule->AddValue(prop.mProperty, Move(fromValue)); + } else { + aStyleRule->AddValue(prop.mProperty, Move(toValue)); + } + continue; + } + + double positionInSegment = + (computedTiming.mProgress.Value() - segment->mFromKey) / + (segment->mToKey - segment->mFromKey); + double valuePosition = + ComputedTimingFunction::GetPortion(segment->mTimingFunction, + positionInSegment, + computedTiming.mBeforeFlag); + + MOZ_ASSERT(IsFinite(valuePosition), "Position value should be finite"); + StyleAnimationValue val; + if (StyleAnimationValue::Interpolate(prop.mProperty, + fromValue, + toValue, + valuePosition, val)) { + aStyleRule->AddValue(prop.mProperty, Move(val)); + } else if (valuePosition < 0.5) { + aStyleRule->AddValue(prop.mProperty, Move(fromValue)); + } else { + aStyleRule->AddValue(prop.mProperty, Move(toValue)); + } + } +} + +bool +KeyframeEffectReadOnly::IsRunningOnCompositor() const +{ + // We consider animation is running on compositor if there is at least + // one property running on compositor. + // Animation.IsRunningOnCompotitor will return more fine grained + // information in bug 1196114. + for (const AnimationProperty& property : mProperties) { + if (property.mIsRunningOnCompositor) { + return true; + } + } + return false; +} + +void +KeyframeEffectReadOnly::SetIsRunningOnCompositor(nsCSSPropertyID aProperty, + bool aIsRunning) +{ + MOZ_ASSERT(nsCSSProps::PropHasFlags(aProperty, + CSS_PROPERTY_CAN_ANIMATE_ON_COMPOSITOR), + "Property being animated on compositor is a recognized " + "compositor-animatable property"); + for (AnimationProperty& property : mProperties) { + if (property.mProperty == aProperty) { + property.mIsRunningOnCompositor = aIsRunning; + // We currently only set a performance warning message when animations + // cannot be run on the compositor, so if this animation is running + // on the compositor we don't need a message. + if (aIsRunning) { + property.mPerformanceWarning.reset(); + } + return; + } + } +} + +void +KeyframeEffectReadOnly::ResetIsRunningOnCompositor() +{ + for (AnimationProperty& property : mProperties) { + property.mIsRunningOnCompositor = false; + } +} + +static const KeyframeEffectOptions& +KeyframeEffectOptionsFromUnion( + const UnrestrictedDoubleOrKeyframeEffectOptions& aOptions) +{ + MOZ_ASSERT(aOptions.IsKeyframeEffectOptions()); + return aOptions.GetAsKeyframeEffectOptions(); +} + +static const KeyframeEffectOptions& +KeyframeEffectOptionsFromUnion( + const UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions) +{ + MOZ_ASSERT(aOptions.IsKeyframeAnimationOptions()); + return aOptions.GetAsKeyframeAnimationOptions(); +} + +template <class OptionsType> +static KeyframeEffectParams +KeyframeEffectParamsFromUnion(const OptionsType& aOptions, + nsAString& aInvalidPacedProperty, + ErrorResult& aRv) +{ + KeyframeEffectParams result; + if (!aOptions.IsUnrestrictedDouble()) { + const KeyframeEffectOptions& options = + KeyframeEffectOptionsFromUnion(aOptions); + KeyframeEffectParams::ParseSpacing(options.mSpacing, + result.mSpacingMode, + result.mPacedProperty, + aInvalidPacedProperty, + aRv); + // Ignore iterationComposite if the Web Animations API is not enabled, + // then the default value 'Replace' will be used. + if (AnimationUtils::IsCoreAPIEnabledForCaller()) { + result.mIterationComposite = options.mIterationComposite; + } + } + return result; +} + +/* static */ Maybe<OwningAnimationTarget> +KeyframeEffectReadOnly::ConvertTarget( + const Nullable<ElementOrCSSPseudoElement>& aTarget) +{ + // Return value optimization. + Maybe<OwningAnimationTarget> result; + + if (aTarget.IsNull()) { + return result; + } + + const ElementOrCSSPseudoElement& target = aTarget.Value(); + MOZ_ASSERT(target.IsElement() || target.IsCSSPseudoElement(), + "Uninitialized target"); + + if (target.IsElement()) { + result.emplace(&target.GetAsElement()); + } else { + RefPtr<Element> elem = target.GetAsCSSPseudoElement().ParentElement(); + result.emplace(elem, target.GetAsCSSPseudoElement().GetType()); + } + return result; +} + +template <class KeyframeEffectType, class OptionsType> +/* static */ already_AddRefed<KeyframeEffectType> +KeyframeEffectReadOnly::ConstructKeyframeEffect( + const GlobalObject& aGlobal, + const Nullable<ElementOrCSSPseudoElement>& aTarget, + JS::Handle<JSObject*> aKeyframes, + const OptionsType& aOptions, + ErrorResult& aRv) +{ + nsIDocument* doc = AnimationUtils::GetCurrentRealmDocument(aGlobal.Context()); + if (!doc) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + TimingParams timingParams = + TimingParams::FromOptionsUnion(aOptions, doc, aRv); + if (aRv.Failed()) { + return nullptr; + } + + nsAutoString invalidPacedProperty; + KeyframeEffectParams effectOptions = + KeyframeEffectParamsFromUnion(aOptions, invalidPacedProperty, aRv); + if (aRv.Failed()) { + return nullptr; + } + + if (!invalidPacedProperty.IsEmpty()) { + const char16_t* params[] = { invalidPacedProperty.get() }; + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, + NS_LITERAL_CSTRING("Animation"), + doc, + nsContentUtils::eDOM_PROPERTIES, + "UnanimatablePacedProperty", + params, ArrayLength(params)); + } + + Maybe<OwningAnimationTarget> target = ConvertTarget(aTarget); + RefPtr<KeyframeEffectType> effect = + new KeyframeEffectType(doc, target, timingParams, effectOptions); + + effect->SetKeyframes(aGlobal.Context(), aKeyframes, aRv); + if (aRv.Failed()) { + return nullptr; + } + + return effect.forget(); +} + +template<class KeyframeEffectType> +/* static */ already_AddRefed<KeyframeEffectType> +KeyframeEffectReadOnly::ConstructKeyframeEffect(const GlobalObject& aGlobal, + KeyframeEffectReadOnly& aSource, + ErrorResult& aRv) +{ + nsIDocument* doc = AnimationUtils::GetCurrentRealmDocument(aGlobal.Context()); + if (!doc) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + // Create a new KeyframeEffectReadOnly object with aSource's target, + // iteration composite operation, composite operation, and spacing mode. + // The constructor creates a new AnimationEffect(ReadOnly) object by + // aSource's TimingParams. + // Note: we don't need to re-throw exceptions since the value specified on + // aSource's timing object can be assumed valid. + RefPtr<KeyframeEffectType> effect = + new KeyframeEffectType(doc, + aSource.mTarget, + aSource.SpecifiedTiming(), + aSource.mEffectOptions); + // Copy cumulative change hint. mCumulativeChangeHint should be the same as + // the source one because both of targets are the same. + effect->mCumulativeChangeHint = aSource.mCumulativeChangeHint; + + // Copy aSource's keyframes and animation properties. + // Note: We don't call SetKeyframes directly, which might revise the + // computed offsets and rebuild the animation properties. + // FIXME: Bug 1314537: We have to make sure SharedKeyframeList is handled + // properly. + effect->mKeyframes = aSource.mKeyframes; + effect->mProperties = aSource.mProperties; + return effect.forget(); +} + +nsTArray<AnimationProperty> +KeyframeEffectReadOnly::BuildProperties(nsStyleContext* aStyleContext) +{ + MOZ_ASSERT(aStyleContext); + + nsTArray<AnimationProperty> result; + // If mTarget is null, return an empty property array. + if (!mTarget) { + return result; + } + + // When GetComputedKeyframeValues or GetAnimationPropertiesFromKeyframes + // calculate computed values from |mKeyframes|, they could possibly + // trigger a subsequent restyle in which we rebuild animations. If that + // happens we could find that |mKeyframes| is overwritten while it is + // being iterated over. Normally that shouldn't happen but just in case we + // make a copy of |mKeyframes| first and iterate over that instead. + auto keyframesCopy(mKeyframes); + + nsTArray<ComputedKeyframeValues> computedValues = + KeyframeUtils::GetComputedKeyframeValues(keyframesCopy, + mTarget->mElement, + aStyleContext); + + if (mEffectOptions.mSpacingMode == SpacingMode::paced) { + KeyframeUtils::ApplySpacing(keyframesCopy, SpacingMode::paced, + mEffectOptions.mPacedProperty, + computedValues, aStyleContext); + } + + result = KeyframeUtils::GetAnimationPropertiesFromKeyframes(keyframesCopy, + computedValues, + aStyleContext); + +#ifdef DEBUG + MOZ_ASSERT(SpecifiedKeyframeArraysAreEqual(mKeyframes, keyframesCopy), + "Apart from the computed offset members, the keyframes array" + " should not be modified"); +#endif + + mKeyframes.SwapElements(keyframesCopy); + return result; +} + +void +KeyframeEffectReadOnly::UpdateTargetRegistration() +{ + if (!mTarget) { + return; + } + + bool isRelevant = mAnimation && mAnimation->IsRelevant(); + + // Animation::IsRelevant() returns a cached value. It only updates when + // something calls Animation::UpdateRelevance. Whenever our timing changes, + // we should be notifying our Animation before calling this, so + // Animation::IsRelevant() should be up-to-date by the time we get here. + MOZ_ASSERT(isRelevant == IsCurrent() || IsInEffect(), + "Out of date Animation::IsRelevant value"); + + if (isRelevant) { + EffectSet* effectSet = + EffectSet::GetOrCreateEffectSet(mTarget->mElement, mTarget->mPseudoType); + effectSet->AddEffect(*this); + } else { + UnregisterTarget(); + } +} + +void +KeyframeEffectReadOnly::UnregisterTarget() +{ + EffectSet* effectSet = + EffectSet::GetEffectSet(mTarget->mElement, mTarget->mPseudoType); + if (effectSet) { + effectSet->RemoveEffect(*this); + if (effectSet->IsEmpty()) { + EffectSet::DestroyEffectSet(mTarget->mElement, mTarget->mPseudoType); + } + } +} + +void +KeyframeEffectReadOnly::RequestRestyle( + EffectCompositor::RestyleType aRestyleType) +{ + nsPresContext* presContext = GetPresContext(); + if (presContext && mTarget && mAnimation) { + presContext->EffectCompositor()-> + RequestRestyle(mTarget->mElement, mTarget->mPseudoType, + aRestyleType, mAnimation->CascadeLevel()); + } +} + +already_AddRefed<nsStyleContext> +KeyframeEffectReadOnly::GetTargetStyleContext() +{ + nsIPresShell* shell = GetPresShell(); + if (!shell) { + return nullptr; + } + + MOZ_ASSERT(mTarget, + "Should only have a presshell when we have a target element"); + + nsIAtom* pseudo = mTarget->mPseudoType < CSSPseudoElementType::Count + ? nsCSSPseudoElements::GetPseudoAtom(mTarget->mPseudoType) + : nullptr; + return nsComputedDOMStyle::GetStyleContextForElement(mTarget->mElement, + pseudo, shell); +} + +#ifdef DEBUG +void +DumpAnimationProperties(nsTArray<AnimationProperty>& aAnimationProperties) +{ + for (auto& p : aAnimationProperties) { + printf("%s\n", nsCSSProps::GetStringValue(p.mProperty).get()); + for (auto& s : p.mSegments) { + nsString fromValue, toValue; + Unused << StyleAnimationValue::UncomputeValue(p.mProperty, + s.mFromValue, + fromValue); + Unused << StyleAnimationValue::UncomputeValue(p.mProperty, + s.mToValue, + toValue); + printf(" %f..%f: %s..%s\n", s.mFromKey, s.mToKey, + NS_ConvertUTF16toUTF8(fromValue).get(), + NS_ConvertUTF16toUTF8(toValue).get()); + } + } +} +#endif + +/* static */ already_AddRefed<KeyframeEffectReadOnly> +KeyframeEffectReadOnly::Constructor( + const GlobalObject& aGlobal, + const Nullable<ElementOrCSSPseudoElement>& aTarget, + JS::Handle<JSObject*> aKeyframes, + const UnrestrictedDoubleOrKeyframeEffectOptions& aOptions, + ErrorResult& aRv) +{ + return ConstructKeyframeEffect<KeyframeEffectReadOnly>(aGlobal, aTarget, + aKeyframes, aOptions, + aRv); +} + +/* static */ already_AddRefed<KeyframeEffectReadOnly> +KeyframeEffectReadOnly::Constructor(const GlobalObject& aGlobal, + KeyframeEffectReadOnly& aSource, + ErrorResult& aRv) +{ + return ConstructKeyframeEffect<KeyframeEffectReadOnly>(aGlobal, aSource, aRv); +} + +void +KeyframeEffectReadOnly::GetTarget( + Nullable<OwningElementOrCSSPseudoElement>& aRv) const +{ + if (!mTarget) { + aRv.SetNull(); + return; + } + + switch (mTarget->mPseudoType) { + case CSSPseudoElementType::before: + case CSSPseudoElementType::after: + aRv.SetValue().SetAsCSSPseudoElement() = + CSSPseudoElement::GetCSSPseudoElement(mTarget->mElement, + mTarget->mPseudoType); + break; + + case CSSPseudoElementType::NotPseudo: + aRv.SetValue().SetAsElement() = mTarget->mElement; + break; + + default: + NS_NOTREACHED("Animation of unsupported pseudo-type"); + aRv.SetNull(); + } +} + +static void +CreatePropertyValue(nsCSSPropertyID aProperty, + float aOffset, + const Maybe<ComputedTimingFunction>& aTimingFunction, + const StyleAnimationValue& aValue, + AnimationPropertyValueDetails& aResult) +{ + aResult.mOffset = aOffset; + + nsString stringValue; + DebugOnly<bool> uncomputeResult = + StyleAnimationValue::UncomputeValue(aProperty, aValue, stringValue); + MOZ_ASSERT(uncomputeResult, "failed to uncompute value"); + aResult.mValue = stringValue; + + if (aTimingFunction) { + aResult.mEasing.Construct(); + aTimingFunction->AppendToString(aResult.mEasing.Value()); + } else { + aResult.mEasing.Construct(NS_LITERAL_STRING("linear")); + } + + aResult.mComposite = CompositeOperation::Replace; +} + +void +KeyframeEffectReadOnly::GetProperties( + nsTArray<AnimationPropertyDetails>& aProperties, + ErrorResult& aRv) const +{ + for (const AnimationProperty& property : mProperties) { + AnimationPropertyDetails propertyDetails; + propertyDetails.mProperty = + NS_ConvertASCIItoUTF16(nsCSSProps::GetStringValue(property.mProperty)); + propertyDetails.mRunningOnCompositor = property.mIsRunningOnCompositor; + + nsXPIDLString localizedString; + if (property.mPerformanceWarning && + property.mPerformanceWarning->ToLocalizedString(localizedString)) { + propertyDetails.mWarning.Construct(localizedString); + } + + if (!propertyDetails.mValues.SetCapacity(property.mSegments.Length(), + mozilla::fallible)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + for (size_t segmentIdx = 0, segmentLen = property.mSegments.Length(); + segmentIdx < segmentLen; + segmentIdx++) + { + const AnimationPropertySegment& segment = property.mSegments[segmentIdx]; + + binding_detail::FastAnimationPropertyValueDetails fromValue; + CreatePropertyValue(property.mProperty, segment.mFromKey, + segment.mTimingFunction, segment.mFromValue, + fromValue); + // We don't apply timing functions for zero-length segments, so + // don't return one here. + if (segment.mFromKey == segment.mToKey) { + fromValue.mEasing.Reset(); + } + // The following won't fail since we have already allocated the capacity + // above. + propertyDetails.mValues.AppendElement(fromValue, mozilla::fallible); + + // Normally we can ignore the to-value for this segment since it is + // identical to the from-value from the next segment. However, we need + // to add it if either: + // a) this is the last segment, or + // b) the next segment's from-value differs. + if (segmentIdx == segmentLen - 1 || + property.mSegments[segmentIdx + 1].mFromValue != segment.mToValue) { + binding_detail::FastAnimationPropertyValueDetails toValue; + CreatePropertyValue(property.mProperty, segment.mToKey, + Nothing(), segment.mToValue, toValue); + // It doesn't really make sense to have a timing function on the + // last property value or before a sudden jump so we just drop the + // easing property altogether. + toValue.mEasing.Reset(); + propertyDetails.mValues.AppendElement(toValue, mozilla::fallible); + } + } + + aProperties.AppendElement(propertyDetails); + } +} + +void +KeyframeEffectReadOnly::GetKeyframes(JSContext*& aCx, + nsTArray<JSObject*>& aResult, + ErrorResult& aRv) +{ + MOZ_ASSERT(aResult.IsEmpty()); + MOZ_ASSERT(!aRv.Failed()); + + if (!aResult.SetCapacity(mKeyframes.Length(), mozilla::fallible)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + for (const Keyframe& keyframe : mKeyframes) { + // Set up a dictionary object for the explicit members + BaseComputedKeyframe keyframeDict; + if (keyframe.mOffset) { + keyframeDict.mOffset.SetValue(keyframe.mOffset.value()); + } + MOZ_ASSERT(keyframe.mComputedOffset != Keyframe::kComputedOffsetNotSet, + "Invalid computed offset"); + keyframeDict.mComputedOffset.Construct(keyframe.mComputedOffset); + if (keyframe.mTimingFunction) { + keyframeDict.mEasing.Truncate(); + keyframe.mTimingFunction.ref().AppendToString(keyframeDict.mEasing); + } // else if null, leave easing as its default "linear". + + JS::Rooted<JS::Value> keyframeJSValue(aCx); + if (!ToJSValue(aCx, keyframeDict, &keyframeJSValue)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + JS::Rooted<JSObject*> keyframeObject(aCx, &keyframeJSValue.toObject()); + for (const PropertyValuePair& propertyValue : keyframe.mPropertyValues) { + + const char* name = nsCSSProps::PropertyIDLName(propertyValue.mProperty); + + // nsCSSValue::AppendToString does not accept shorthands properties but + // works with token stream values if we pass eCSSProperty_UNKNOWN as + // the property. + nsCSSPropertyID propertyForSerializing = + nsCSSProps::IsShorthand(propertyValue.mProperty) + ? eCSSProperty_UNKNOWN + : propertyValue.mProperty; + + nsAutoString stringValue; + if (propertyValue.mServoDeclarationBlock) { + Servo_DeclarationBlock_SerializeOneValue( + propertyValue.mServoDeclarationBlock, &stringValue); + } else { + propertyValue.mValue.AppendToString( + propertyForSerializing, stringValue, nsCSSValue::eNormalized); + } + + JS::Rooted<JS::Value> value(aCx); + if (!ToJSValue(aCx, stringValue, &value) || + !JS_DefineProperty(aCx, keyframeObject, name, value, + JSPROP_ENUMERATE)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + } + + aResult.AppendElement(keyframeObject); + } +} + +/* static */ const TimeDuration +KeyframeEffectReadOnly::OverflowRegionRefreshInterval() +{ + // The amount of time we can wait between updating throttled animations + // on the main thread that influence the overflow region. + static const TimeDuration kOverflowRegionRefreshInterval = + TimeDuration::FromMilliseconds(200); + + return kOverflowRegionRefreshInterval; +} + +bool +KeyframeEffectReadOnly::CanThrottle() const +{ + // Unthrottle if we are not in effect or current. This will be the case when + // our owning animation has finished, is idle, or when we are in the delay + // phase (but without a backwards fill). In each case the computed progress + // value produced on each tick will be the same so we will skip requesting + // unnecessary restyles in NotifyAnimationTimingUpdated. Any calls we *do* get + // here will be because of a change in state (e.g. we are newly finished or + // newly no longer in effect) in which case we shouldn't throttle the sample. + if (!IsInEffect() || !IsCurrent()) { + return false; + } + + nsIFrame* frame = GetAnimationFrame(); + if (!frame) { + // There are two possible cases here. + // a) No target element + // b) The target element has no frame, e.g. because it is in a display:none + // subtree. + // In either case we can throttle the animation because there is no + // need to update on the main thread. + return true; + } + + // We can throttle the animation if the animation is paint only and + // the target frame is out of view or the document is in background tabs. + if (CanIgnoreIfNotVisible()) { + nsIPresShell* presShell = GetPresShell(); + if ((presShell && !presShell->IsActive()) || + frame->IsScrolledOutOfView()) { + return true; + } + } + + // First we need to check layer generation and transform overflow + // prior to the property.mIsRunningOnCompositor check because we should + // occasionally unthrottle these animations even if the animations are + // already running on compositor. + for (const LayerAnimationInfo::Record& record : + LayerAnimationInfo::sRecords) { + // Skip properties that are overridden by !important rules. + // (GetEffectiveAnimationOfProperty, as called by + // HasEffectiveAnimationOfProperty, only returns a property which is + // neither overridden by !important rules nor overridden by other + // animation.) + if (!HasEffectiveAnimationOfProperty(record.mProperty)) { + continue; + } + + EffectSet* effectSet = EffectSet::GetEffectSet(mTarget->mElement, + mTarget->mPseudoType); + MOZ_ASSERT(effectSet, "CanThrottle should be called on an effect " + "associated with a target element"); + layers::Layer* layer = + FrameLayerBuilder::GetDedicatedLayer(frame, record.mLayerType); + // Unthrottle if the layer needs to be brought up to date + if (!layer || + effectSet->GetAnimationGeneration() != + layer->GetAnimationGeneration()) { + return false; + } + + // If this is a transform animation that affects the overflow region, + // we should unthrottle the animation periodically. + if (record.mProperty == eCSSProperty_transform && + !CanThrottleTransformChanges(*frame)) { + return false; + } + } + + for (const AnimationProperty& property : mProperties) { + if (!property.mIsRunningOnCompositor) { + return false; + } + } + + return true; +} + +bool +KeyframeEffectReadOnly::CanThrottleTransformChanges(nsIFrame& aFrame) const +{ + // If we know that the animation cannot cause overflow, + // we can just disable flushes for this animation. + + // If we don't show scrollbars, we don't care about overflow. + if (LookAndFeel::GetInt(LookAndFeel::eIntID_ShowHideScrollbars) == 0) { + return true; + } + + nsPresContext* presContext = GetPresContext(); + // CanThrottleTransformChanges is only called as part of a refresh driver tick + // in which case we expect to has a pres context. + MOZ_ASSERT(presContext); + + TimeStamp now = + presContext->RefreshDriver()->MostRecentRefresh(); + + EffectSet* effectSet = EffectSet::GetEffectSet(mTarget->mElement, + mTarget->mPseudoType); + MOZ_ASSERT(effectSet, "CanThrottleTransformChanges is expected to be called" + " on an effect in an effect set"); + MOZ_ASSERT(mAnimation, "CanThrottleTransformChanges is expected to be called" + " on an effect with a parent animation"); + TimeStamp animationRuleRefreshTime = + effectSet->AnimationRuleRefreshTime(mAnimation->CascadeLevel()); + // If this animation can cause overflow, we can throttle some of the ticks. + if (!animationRuleRefreshTime.IsNull() && + (now - animationRuleRefreshTime) < OverflowRegionRefreshInterval()) { + return true; + } + + // If the nearest scrollable ancestor has overflow:hidden, + // we don't care about overflow. + nsIScrollableFrame* scrollable = + nsLayoutUtils::GetNearestScrollableFrame(&aFrame); + if (!scrollable) { + return true; + } + + ScrollbarStyles ss = scrollable->GetScrollbarStyles(); + if (ss.mVertical == NS_STYLE_OVERFLOW_HIDDEN && + ss.mHorizontal == NS_STYLE_OVERFLOW_HIDDEN && + scrollable->GetLogicalScrollPosition() == nsPoint(0, 0)) { + return true; + } + + return false; +} + +nsIFrame* +KeyframeEffectReadOnly::GetAnimationFrame() const +{ + if (!mTarget) { + return nullptr; + } + + nsIFrame* frame = mTarget->mElement->GetPrimaryFrame(); + if (!frame) { + return nullptr; + } + + if (mTarget->mPseudoType == CSSPseudoElementType::before) { + frame = nsLayoutUtils::GetBeforeFrame(frame); + } else if (mTarget->mPseudoType == CSSPseudoElementType::after) { + frame = nsLayoutUtils::GetAfterFrame(frame); + } else { + MOZ_ASSERT(mTarget->mPseudoType == CSSPseudoElementType::NotPseudo, + "unknown mTarget->mPseudoType"); + } + if (!frame) { + return nullptr; + } + + return nsLayoutUtils::GetStyleFrame(frame); +} + +nsIDocument* +KeyframeEffectReadOnly::GetRenderedDocument() const +{ + if (!mTarget) { + return nullptr; + } + return mTarget->mElement->GetComposedDoc(); +} + +nsIPresShell* +KeyframeEffectReadOnly::GetPresShell() const +{ + nsIDocument* doc = GetRenderedDocument(); + if (!doc) { + return nullptr; + } + return doc->GetShell(); +} + +nsPresContext* +KeyframeEffectReadOnly::GetPresContext() const +{ + nsIPresShell* shell = GetPresShell(); + if (!shell) { + return nullptr; + } + return shell->GetPresContext(); +} + +/* static */ bool +KeyframeEffectReadOnly::IsGeometricProperty( + const nsCSSPropertyID aProperty) +{ + switch (aProperty) { + case eCSSProperty_bottom: + case eCSSProperty_height: + case eCSSProperty_left: + case eCSSProperty_right: + case eCSSProperty_top: + case eCSSProperty_width: + return true; + default: + return false; + } +} + +/* static */ bool +KeyframeEffectReadOnly::CanAnimateTransformOnCompositor( + const nsIFrame* aFrame, + AnimationPerformanceWarning::Type& aPerformanceWarning) +{ + // Disallow OMTA for preserve-3d transform. Note that we check the style property + // rather than Extend3DContext() since that can recurse back into this function + // via HasOpacity(). See bug 779598. + if (aFrame->Combines3DTransformWithAncestors() || + aFrame->StyleDisplay()->mTransformStyle == NS_STYLE_TRANSFORM_STYLE_PRESERVE_3D) { + aPerformanceWarning = AnimationPerformanceWarning::Type::TransformPreserve3D; + return false; + } + // Note that testing BackfaceIsHidden() is not a sufficient test for + // what we need for animating backface-visibility correctly if we + // remove the above test for Extend3DContext(); that would require + // looking at backface-visibility on descendants as well. See bug 1186204. + if (aFrame->BackfaceIsHidden()) { + aPerformanceWarning = + AnimationPerformanceWarning::Type::TransformBackfaceVisibilityHidden; + return false; + } + // Async 'transform' animations of aFrames with SVG transforms is not + // supported. See bug 779599. + if (aFrame->IsSVGTransformed()) { + aPerformanceWarning = AnimationPerformanceWarning::Type::TransformSVG; + return false; + } + + return true; +} + +bool +KeyframeEffectReadOnly::ShouldBlockAsyncTransformAnimations( + const nsIFrame* aFrame, + AnimationPerformanceWarning::Type& aPerformanceWarning) const +{ + // We currently only expect this method to be called for effects whose + // animations are eligible for the compositor since, Animations that are + // paused, zero-duration, finished etc. should not block other animations from + // running on the compositor. + MOZ_ASSERT(mAnimation && mAnimation->IsPlayableOnCompositor()); + + EffectSet* effectSet = + EffectSet::GetEffectSet(mTarget->mElement, mTarget->mPseudoType); + for (const AnimationProperty& property : mProperties) { + // If there is a property for animations level that is overridden by + // !important rules, it should not block other animations from running + // on the compositor. + // NOTE: We don't currently check for !important rules for properties that + // don't run on the compositor. As result such properties (e.g. margin-left) + // can still block async animations even if they are overridden by + // !important rules. + if (effectSet && + effectSet->PropertiesWithImportantRules() + .HasProperty(property.mProperty) && + effectSet->PropertiesForAnimationsLevel() + .HasProperty(property.mProperty)) { + continue; + } + // Check for geometric properties + if (IsGeometricProperty(property.mProperty)) { + aPerformanceWarning = + AnimationPerformanceWarning::Type::TransformWithGeometricProperties; + return true; + } + + // Check for unsupported transform animations + if (property.mProperty == eCSSProperty_transform) { + if (!CanAnimateTransformOnCompositor(aFrame, + aPerformanceWarning)) { + return true; + } + } + } + + return false; +} + +void +KeyframeEffectReadOnly::SetPerformanceWarning( + nsCSSPropertyID aProperty, + const AnimationPerformanceWarning& aWarning) +{ + for (AnimationProperty& property : mProperties) { + if (property.mProperty == aProperty && + (!property.mPerformanceWarning || + *property.mPerformanceWarning != aWarning)) { + property.mPerformanceWarning = Some(aWarning); + + nsXPIDLString localizedString; + if (nsLayoutUtils::IsAnimationLoggingEnabled() && + property.mPerformanceWarning->ToLocalizedString(localizedString)) { + nsAutoCString logMessage = NS_ConvertUTF16toUTF8(localizedString); + AnimationUtils::LogAsyncAnimationFailure(logMessage, mTarget->mElement); + } + return; + } + } +} + +static already_AddRefed<nsStyleContext> +CreateStyleContextForAnimationValue(nsCSSPropertyID aProperty, + const StyleAnimationValue& aValue, + nsStyleContext* aBaseStyleContext) +{ + MOZ_ASSERT(aBaseStyleContext, + "CreateStyleContextForAnimationValue needs to be called " + "with a valid nsStyleContext"); + + RefPtr<AnimValuesStyleRule> styleRule = new AnimValuesStyleRule(); + styleRule->AddValue(aProperty, aValue); + + nsCOMArray<nsIStyleRule> rules; + rules.AppendObject(styleRule); + + MOZ_ASSERT(aBaseStyleContext->PresContext()->StyleSet()->IsGecko(), + "ServoStyleSet should not use StyleAnimationValue for animations"); + nsStyleSet* styleSet = + aBaseStyleContext->PresContext()->StyleSet()->AsGecko(); + + RefPtr<nsStyleContext> styleContext = + styleSet->ResolveStyleByAddingRules(aBaseStyleContext, rules); + + // We need to call StyleData to generate cached data for the style context. + // Otherwise CalcStyleDifference returns no meaningful result. + styleContext->StyleData(nsCSSProps::kSIDTable[aProperty]); + + return styleContext.forget(); +} + +void +KeyframeEffectReadOnly::CalculateCumulativeChangeHint( + nsStyleContext *aStyleContext) +{ + mCumulativeChangeHint = nsChangeHint(0); + + for (const AnimationProperty& property : mProperties) { + for (const AnimationPropertySegment& segment : property.mSegments) { + RefPtr<nsStyleContext> fromContext = + CreateStyleContextForAnimationValue(property.mProperty, + segment.mFromValue, aStyleContext); + + RefPtr<nsStyleContext> toContext = + CreateStyleContextForAnimationValue(property.mProperty, + segment.mToValue, aStyleContext); + + uint32_t equalStructs = 0; + uint32_t samePointerStructs = 0; + nsChangeHint changeHint = + fromContext->CalcStyleDifference(toContext, + nsChangeHint(0), + &equalStructs, + &samePointerStructs); + + mCumulativeChangeHint |= changeHint; + } + } +} + +void +KeyframeEffectReadOnly::SetAnimation(Animation* aAnimation) +{ + if (mAnimation == aAnimation) { + return; + } + + // Restyle for the old animation. + RequestRestyle(EffectCompositor::RestyleType::Layer); + + mAnimation = aAnimation; + + // The order of these function calls is important: + // NotifyAnimationTimingUpdated() need the updated mIsRelevant flag to check + // if it should create the effectSet or not, and MarkCascadeNeedsUpdate() + // needs a valid effectSet, so we should call them in this order. + if (mAnimation) { + mAnimation->UpdateRelevance(); + } + NotifyAnimationTimingUpdated(); + if (mAnimation) { + MarkCascadeNeedsUpdate(); + } +} + +bool +KeyframeEffectReadOnly::CanIgnoreIfNotVisible() const +{ + if (!AnimationUtils::IsOffscreenThrottlingEnabled()) { + return false; + } + + // FIXME (bug 1303235): We don't calculate mCumulativeChangeHint for + // the Servo backend yet + nsPresContext* presContext = GetPresContext(); + if (!presContext || presContext->StyleSet()->IsServo()) { + return false; + } + + // FIXME: For further sophisticated optimization we need to check + // change hint on the segment corresponding to computedTiming.progress. + return NS_IsHintSubset( + mCumulativeChangeHint, nsChangeHint_Hints_CanIgnoreIfNotVisible); +} + +void +KeyframeEffectReadOnly::MaybeUpdateFrameForCompositor() +{ + nsIFrame* frame = GetAnimationFrame(); + if (!frame) { + return; + } + + // FIXME: Bug 1272495: If this effect does not win in the cascade, the + // NS_FRAME_MAY_BE_TRANSFORMED flag should be removed when the animation + // will be removed from effect set or the transform keyframes are removed + // by setKeyframes. The latter case will be hard to solve though. + for (const AnimationProperty& property : mProperties) { + if (property.mProperty == eCSSProperty_transform) { + frame->AddStateBits(NS_FRAME_MAY_BE_TRANSFORMED); + return; + } + } +} + +void +KeyframeEffectReadOnly::MarkCascadeNeedsUpdate() +{ + if (!mTarget) { + return; + } + + EffectSet* effectSet = EffectSet::GetEffectSet(mTarget->mElement, + mTarget->mPseudoType); + if (!effectSet) { + return; + } + effectSet->MarkCascadeNeedsUpdate(); +} + +bool +KeyframeEffectReadOnly::HasComputedTimingChanged() const +{ + // Typically we don't need to request a restyle if the progress hasn't + // changed since the last call to ComposeStyle. The one exception is if the + // iteration composite mode is 'accumulate' and the current iteration has + // changed, since that will often produce a different result. + ComputedTiming computedTiming = GetComputedTiming(); + return computedTiming.mProgress != mProgressOnLastCompose || + (mEffectOptions.mIterationComposite == + IterationCompositeOperation::Accumulate && + computedTiming.mCurrentIteration != + mCurrentIterationOnLastCompose); +} + +} // namespace dom +} // namespace mozilla |