From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- layout/style/nsTransitionManager.cpp | 519 +++++++++++++++++++++++++++++++++++ 1 file changed, 519 insertions(+) create mode 100644 layout/style/nsTransitionManager.cpp (limited to 'layout/style/nsTransitionManager.cpp') diff --git a/layout/style/nsTransitionManager.cpp b/layout/style/nsTransitionManager.cpp new file mode 100644 index 0000000000..014c0375da --- /dev/null +++ b/layout/style/nsTransitionManager.cpp @@ -0,0 +1,519 @@ +/* -*- 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/. */ + +/* Code to start and animate CSS transitions. */ + +#include "nsTransitionManager.h" +#include "mozilla/dom/Document.h" +#include "nsAnimationManager.h" + +#include "nsIContent.h" +#include "mozilla/ComputedStyle.h" +#include "mozilla/MemoryReporting.h" +#include "nsCSSPropertyIDSet.h" +#include "mozilla/EffectSet.h" +#include "mozilla/ElementAnimationData.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/ServoBindings.h" +#include "mozilla/StyleAnimationValue.h" +#include "mozilla/dom/DocumentTimeline.h" +#include "mozilla/dom/Element.h" +#include "nsIFrame.h" +#include "nsCSSProps.h" +#include "nsCSSPseudoElements.h" +#include "nsDisplayList.h" +#include "nsRFPService.h" +#include "nsStyleChangeList.h" +#include "mozilla/RestyleManager.h" + +using mozilla::dom::CSSTransition; +using mozilla::dom::DocumentTimeline; +using mozilla::dom::KeyframeEffect; + +using namespace mozilla; +using namespace mozilla::css; + +static inline bool ExtractNonDiscreteComputedValue( + nsCSSPropertyID aProperty, const ComputedStyle& aComputedStyle, + AnimationValue& aAnimationValue) { + if (Servo_Property_IsDiscreteAnimatable(aProperty) && + aProperty != eCSSProperty_visibility) { + return false; + } + + aAnimationValue.mServo = + Servo_ComputedValues_ExtractAnimationValue(&aComputedStyle, aProperty) + .Consume(); + return !!aAnimationValue.mServo; +} + +bool nsTransitionManager::UpdateTransitions(dom::Element* aElement, + PseudoStyleType aPseudoType, + const ComputedStyle& aOldStyle, + const ComputedStyle& aNewStyle) { + if (mPresContext->Medium() == nsGkAtoms::print) { + // For print or print preview, ignore transitions. + return false; + } + + MOZ_ASSERT(mPresContext->IsDynamic()); + if (aNewStyle.StyleDisplay()->mDisplay == StyleDisplay::None) { + StopAnimationsForElement(aElement, aPseudoType); + return false; + } + + auto* collection = CSSTransitionCollection::Get(aElement, aPseudoType); + return DoUpdateTransitions(*aNewStyle.StyleUIReset(), aElement, aPseudoType, + collection, aOldStyle, aNewStyle); +} + +// This function expands the shorthands and "all" keyword specified in +// transition-property, and then execute |aHandler| on the expanded longhand. +// |aHandler| should be a lamda function which accepts nsCSSPropertyID. +template +static void ExpandTransitionProperty(nsCSSPropertyID aProperty, T aHandler) { + if (aProperty == eCSSPropertyExtra_no_properties || + aProperty == eCSSPropertyExtra_variable || + aProperty == eCSSProperty_UNKNOWN) { + // Nothing to do. + return; + } + + // FIXME(emilio): This should probably just use the "all" shorthand id, and we + // should probably remove eCSSPropertyExtra_all_properties. + if (aProperty == eCSSPropertyExtra_all_properties) { + for (nsCSSPropertyID p = nsCSSPropertyID(0); + p < eCSSProperty_COUNT_no_shorthands; p = nsCSSPropertyID(p + 1)) { + if (!nsCSSProps::IsEnabled(p, CSSEnabledState::ForAllContent)) { + continue; + } + aHandler(p); + } + } else if (nsCSSProps::IsShorthand(aProperty)) { + CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(subprop, aProperty, + CSSEnabledState::ForAllContent) { + aHandler(*subprop); + } + } else { + aHandler(aProperty); + } +} + +bool nsTransitionManager::DoUpdateTransitions( + const nsStyleUIReset& aStyle, dom::Element* aElement, + PseudoStyleType aPseudoType, CSSTransitionCollection*& aElementTransitions, + const ComputedStyle& aOldStyle, const ComputedStyle& aNewStyle) { + MOZ_ASSERT(!aElementTransitions || &aElementTransitions->mElement == aElement, + "Element mismatch"); + + // Per http://lists.w3.org/Archives/Public/www-style/2009Aug/0109.html + // I'll consider only the transitions from the number of items in + // 'transition-property' on down, and later ones will override earlier + // ones (tracked using |propertiesChecked|). + bool startedAny = false; + nsCSSPropertyIDSet propertiesChecked; + for (uint32_t i = aStyle.mTransitionPropertyCount; i--;) { + // We're not going to look at any further transitions, so we can just avoid + // looking at this if we know it will not start any transitions. + if (i == 0 && aStyle.GetTransitionCombinedDuration(i).seconds <= 0.0f) { + continue; + } + + ExpandTransitionProperty( + aStyle.GetTransitionProperty(i), [&](nsCSSPropertyID aProperty) { + // We might have something to transition. See if any of the + // properties in question changed and are animatable. + startedAny |= ConsiderInitiatingTransition( + aProperty, aStyle, i, aElement, aPseudoType, aElementTransitions, + aOldStyle, aNewStyle, propertiesChecked); + }); + } + + // Stop any transitions for properties that are no longer in + // 'transition-property', including finished transitions. + // Also stop any transitions (and remove any finished transitions) + // for properties that just changed (and are still in the set of + // properties to transition), but for which we didn't just start the + // transition. This can happen delay and duration are both zero, or + // because the new value is not interpolable. + if (aElementTransitions) { + bool checkProperties = + aStyle.GetTransitionProperty(0) != eCSSPropertyExtra_all_properties; + nsCSSPropertyIDSet allTransitionProperties; + if (checkProperties) { + for (uint32_t i = aStyle.mTransitionPropertyCount; i-- != 0;) { + ExpandTransitionProperty( + aStyle.GetTransitionProperty(i), [&](nsCSSPropertyID aProperty) { + allTransitionProperties.AddProperty( + nsCSSProps::Physicalize(aProperty, aNewStyle)); + }); + } + } + + OwningCSSTransitionPtrArray& animations = aElementTransitions->mAnimations; + size_t i = animations.Length(); + MOZ_ASSERT(i != 0, "empty transitions list?"); + AnimationValue currentValue; + do { + --i; + CSSTransition* anim = animations[i]; + const nsCSSPropertyID property = anim->TransitionProperty(); + if ( + // Properties no longer in `transition-property`. + (checkProperties && !allTransitionProperties.HasProperty(property)) || + // Properties whose computed values changed but for which we did not + // start a new transition (because delay and duration are both zero, + // or because the new value is not interpolable); a new transition + // would have anim->ToValue() matching currentValue. + !ExtractNonDiscreteComputedValue(property, aNewStyle, currentValue) || + currentValue != anim->ToValue()) { + // Stop the transition. + DoCancelTransition(aElement, aPseudoType, aElementTransitions, i); + } + } while (i != 0); + } + + return startedAny; +} + +static Keyframe& AppendKeyframe(double aOffset, nsCSSPropertyID aProperty, + AnimationValue&& aValue, + nsTArray& aKeyframes) { + Keyframe& frame = *aKeyframes.AppendElement(); + frame.mOffset.emplace(aOffset); + MOZ_ASSERT(aValue.mServo); + RefPtr decl = + Servo_AnimationValue_Uncompute(aValue.mServo).Consume(); + frame.mPropertyValues.AppendElement( + PropertyValuePair(aProperty, std::move(decl))); + return frame; +} + +static nsTArray GetTransitionKeyframes(nsCSSPropertyID aProperty, + AnimationValue&& aStartValue, + AnimationValue&& aEndValue) { + nsTArray keyframes(2); + + AppendKeyframe(0.0, aProperty, std::move(aStartValue), keyframes); + AppendKeyframe(1.0, aProperty, std::move(aEndValue), keyframes); + + return keyframes; +} + +static bool IsTransitionable(nsCSSPropertyID aProperty) { + return Servo_Property_IsTransitionable(aProperty); +} + +static Maybe +GetReplacedTransitionProperties(const CSSTransition* aTransition, + const DocumentTimeline* aTimelineToMatch) { + Maybe result; + + // Transition needs to be currently running on the compositor to be + // replaceable. + if (!aTransition || !aTransition->HasCurrentEffect() || + !aTransition->IsRunningOnCompositor() || + aTransition->GetStartTime().IsNull()) { + return result; + } + + // Transition needs to be running on the same timeline. + if (aTransition->GetTimeline() != aTimelineToMatch) { + return result; + } + + // The transition needs to have a keyframe effect. + const KeyframeEffect* keyframeEffect = + aTransition->GetEffect() ? aTransition->GetEffect()->AsKeyframeEffect() + : nullptr; + if (!keyframeEffect) { + return result; + } + + // The keyframe effect needs to be a simple transition of the original + // transition property (i.e. not replaced with something else). + if (keyframeEffect->Properties().Length() != 1 || + keyframeEffect->Properties()[0].mSegments.Length() != 1 || + keyframeEffect->Properties()[0].mProperty != + aTransition->TransitionProperty()) { + return result; + } + + const AnimationPropertySegment& segment = + keyframeEffect->Properties()[0].mSegments[0]; + + result.emplace(CSSTransition::ReplacedTransitionProperties( + {aTransition->GetStartTime().Value(), aTransition->PlaybackRate(), + keyframeEffect->SpecifiedTiming(), segment.mTimingFunction, + segment.mFromValue, segment.mToValue})); + + return result; +} + +bool nsTransitionManager::ConsiderInitiatingTransition( + nsCSSPropertyID aProperty, const nsStyleUIReset& aStyle, + uint32_t transitionIdx, dom::Element* aElement, PseudoStyleType aPseudoType, + CSSTransitionCollection*& aElementTransitions, + const ComputedStyle& aOldStyle, const ComputedStyle& aNewStyle, + nsCSSPropertyIDSet& aPropertiesChecked) { + // IsShorthand itself will assert if aProperty is not a property. + MOZ_ASSERT(!nsCSSProps::IsShorthand(aProperty), "property out of range"); + NS_ASSERTION( + !aElementTransitions || &aElementTransitions->mElement == aElement, + "Element mismatch"); + + aProperty = nsCSSProps::Physicalize(aProperty, aNewStyle); + + // A later item in transition-property already specified a transition for + // this property, so we ignore this one. + // + // See http://lists.w3.org/Archives/Public/www-style/2009Aug/0109.html . + if (aPropertiesChecked.HasProperty(aProperty)) { + return false; + } + + aPropertiesChecked.AddProperty(aProperty); + + if (!IsTransitionable(aProperty)) { + return false; + } + + float delay = aStyle.GetTransitionDelay(transitionIdx).ToMilliseconds(); + + // The spec says a negative duration is treated as zero. + float duration = std::max( + aStyle.GetTransitionDuration(transitionIdx).ToMilliseconds(), 0.0f); + + // If the combined duration of this transition is 0 or less don't start a + // transition. + if (delay + duration <= 0.0f) { + return false; + } + + AnimationValue startValue, endValue; + bool haveValues = + ExtractNonDiscreteComputedValue(aProperty, aOldStyle, startValue) && + ExtractNonDiscreteComputedValue(aProperty, aNewStyle, endValue); + + bool haveChange = startValue != endValue; + + bool shouldAnimate = haveValues && haveChange && + startValue.IsInterpolableWith(aProperty, endValue); + + bool haveCurrentTransition = false; + size_t currentIndex = nsTArray::NoIndex; + const CSSTransition* oldTransition = nullptr; + if (aElementTransitions) { + OwningCSSTransitionPtrArray& animations = aElementTransitions->mAnimations; + for (size_t i = 0, i_end = animations.Length(); i < i_end; ++i) { + if (animations[i]->TransitionProperty() == aProperty) { + haveCurrentTransition = true; + currentIndex = i; + oldTransition = animations[i]; + break; + } + } + } + + // If we got a style change that changed the value to the endpoint + // of the currently running transition, we don't want to interrupt + // its timing function. + // This needs to be before the !shouldAnimate && haveCurrentTransition + // case below because we might be close enough to the end of the + // transition that the current value rounds to the final value. In + // this case, we'll end up with shouldAnimate as false (because + // there's no value change), but we need to return early here rather + // than cancel the running transition because shouldAnimate is false! + // + // Likewise, if we got a style change that changed the value to the + // endpoint of our finished transition, we also don't want to start + // a new transition for the reasons described in + // https://lists.w3.org/Archives/Public/www-style/2015Jan/0444.html . + if (haveCurrentTransition && haveValues && + aElementTransitions->mAnimations[currentIndex]->ToValue() == endValue) { + // GetAnimationRule already called RestyleForAnimation. + return false; + } + + if (!shouldAnimate) { + if (haveCurrentTransition) { + // We're in the middle of a transition, and just got a non-transition + // style change to something that we can't animate. This might happen + // because we got a non-transition style change changing to the current + // in-progress value (which is particularly easy to cause when we're + // currently in the 'transition-delay'). It also might happen because we + // just got a style change to a value that can't be interpolated. + DoCancelTransition(aElement, aPseudoType, aElementTransitions, + currentIndex); + } + return false; + } + + AnimationValue startForReversingTest = startValue; + double reversePortion = 1.0; + + // If the new transition reverses an existing one, we'll need to + // handle the timing differently. + if (haveCurrentTransition && + aElementTransitions->mAnimations[currentIndex]->HasCurrentEffect() && + oldTransition && oldTransition->StartForReversingTest() == endValue) { + // Compute the appropriate negative transition-delay such that right + // now we'd end up at the current position. + double valuePortion = + oldTransition->CurrentValuePortion() * oldTransition->ReversePortion() + + (1.0 - oldTransition->ReversePortion()); + // A timing function with negative y1 (or y2!) might make + // valuePortion negative. In this case, we still want to apply our + // reversing logic based on relative distances, not make duration + // negative. + if (valuePortion < 0.0) { + valuePortion = -valuePortion; + } + // A timing function with y2 (or y1!) greater than one might + // advance past its terminal value. It's probably a good idea to + // clamp valuePortion to be at most one to preserve the invariant + // that a transition will complete within at most its specified + // time. + if (valuePortion > 1.0) { + valuePortion = 1.0; + } + + // Negative delays are essentially part of the transition + // function, so reduce them along with the duration, but don't + // reduce positive delays. + if (delay < 0.0f && std::isfinite(delay)) { + delay *= valuePortion; + } + + if (std::isfinite(duration)) { + duration *= valuePortion; + } + + startForReversingTest = oldTransition->ToValue(); + reversePortion = valuePortion; + } + + TimingParams timing = TimingParamsFromCSSParams( + duration, delay, 1.0 /* iteration count */, + dom::PlaybackDirection::Normal, dom::FillMode::Backwards); + + const StyleComputedTimingFunction& tf = + aStyle.GetTransitionTimingFunction(transitionIdx); + if (!tf.IsLinearKeyword()) { + timing.SetTimingFunction(Some(tf)); + } + + RefPtr transition = DoCreateTransition( + aProperty, aElement, aPseudoType, aNewStyle, aElementTransitions, + std::move(timing), std::move(startValue), std::move(endValue), + std::move(startForReversingTest), reversePortion); + if (!transition) { + return false; + } + + OwningCSSTransitionPtrArray& transitions = aElementTransitions->mAnimations; +#ifdef DEBUG + for (size_t i = 0, i_end = transitions.Length(); i < i_end; ++i) { + MOZ_ASSERT( + i == currentIndex || transitions[i]->TransitionProperty() != aProperty, + "duplicate transitions for property"); + } +#endif + if (haveCurrentTransition) { + // If this new transition is replacing an existing transition that is + // running on the compositor, we store select parameters from the replaced + // transition so that later, once all scripts have run, we can update the + // start value of the transition using TimeStamp::Now(). This allows us to + // avoid a large jump when starting a new transition when the main thread + // lags behind the compositor. + const dom::DocumentTimeline* timeline = aElement->OwnerDoc()->Timeline(); + auto replacedTransitionProperties = + GetReplacedTransitionProperties(oldTransition, timeline); + if (replacedTransitionProperties) { + transition->SetReplacedTransition( + std::move(replacedTransitionProperties.ref())); + } + + transitions[currentIndex]->CancelFromStyle(PostRestyleMode::IfNeeded); + oldTransition = nullptr; // Clear pointer so it doesn't dangle + transitions[currentIndex] = transition; + } else { + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + transitions.AppendElement(transition); + } + + if (auto* effectSet = EffectSet::Get(aElement, aPseudoType)) { + effectSet->UpdateAnimationGeneration(mPresContext); + } + + return true; +} + +already_AddRefed nsTransitionManager::DoCreateTransition( + nsCSSPropertyID aProperty, dom::Element* aElement, + PseudoStyleType aPseudoType, const mozilla::ComputedStyle& aNewStyle, + CSSTransitionCollection*& aElementTransitions, TimingParams&& aTiming, + AnimationValue&& aStartValue, AnimationValue&& aEndValue, + AnimationValue&& aStartForReversingTest, double aReversePortion) { + dom::DocumentTimeline* timeline = aElement->OwnerDoc()->Timeline(); + KeyframeEffectParams effectOptions; + RefPtr keyframeEffect = new KeyframeEffect( + aElement->OwnerDoc(), OwningAnimationTarget(aElement, aPseudoType), + std::move(aTiming), effectOptions); + + keyframeEffect->SetKeyframes( + GetTransitionKeyframes(aProperty, std::move(aStartValue), + std::move(aEndValue)), + &aNewStyle, timeline); + + if (NS_WARN_IF(MOZ_UNLIKELY(!keyframeEffect->IsValidTransition()))) { + return nullptr; + } + + RefPtr animation = + new CSSTransition(mPresContext->Document()->GetScopeObject()); + animation->SetOwningElement(OwningElementRef(*aElement, aPseudoType)); + animation->SetTimelineNoUpdate(timeline); + animation->SetCreationSequence( + mPresContext->RestyleManager()->GetAnimationGeneration()); + animation->SetEffectFromStyle(keyframeEffect); + animation->SetReverseParameters(std::move(aStartForReversingTest), + aReversePortion); + animation->PlayFromStyle(); + + if (!aElementTransitions) { + aElementTransitions = + &aElement->EnsureAnimationData().EnsureTransitionCollection( + *aElement, aPseudoType); + if (!aElementTransitions->isInList()) { + AddElementCollection(aElementTransitions); + } + } + return animation.forget(); +} + +void nsTransitionManager::DoCancelTransition( + dom::Element* aElement, PseudoStyleType aPseudoType, + CSSTransitionCollection*& aElementTransitions, size_t aIndex) { + MOZ_ASSERT(aElementTransitions); + OwningCSSTransitionPtrArray& transitions = aElementTransitions->mAnimations; + CSSTransition* transition = transitions[aIndex]; + + if (transition->HasCurrentEffect()) { + if (auto* effectSet = EffectSet::Get(aElement, aPseudoType)) { + effectSet->UpdateAnimationGeneration(mPresContext); + } + } + transition->CancelFromStyle(PostRestyleMode::IfNeeded); + transitions.RemoveElementAt(aIndex); + + if (transitions.IsEmpty()) { + aElementTransitions->Destroy(); + // |aElementTransitions| is now a dangling pointer! + aElementTransitions = nullptr; + } +} -- cgit v1.2.3