blob: 98cf8e9eec122beab6e364a4fdb8a1f5a6fdfa86 [file] [log] [blame]
/*
* Copyright (C) 2021 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "Styleable.h"
#include "AnimationEffect.h"
#include "AnimationTimeline.h"
#include "CSSAnimation.h"
#include "CSSCustomPropertyValue.h"
#include "CSSTransition.h"
#include "CommonAtomStrings.h"
#include "ContainerNodeInlines.h"
#include "DocumentQuirks.h"
#include "DocumentTimeline.h"
#include "DocumentView.h"
#include "Element.h"
#include "KeyframeEffect.h"
#include "KeyframeEffectStack.h"
#include "RenderChildIterator.h"
#include "RenderDescendantIterator.h"
#include "RenderElement.h"
#include "RenderElementInlines.h"
#include "RenderListItem.h"
#include "RenderListMarker.h"
#include "RenderStyle+GettersInlines.h"
#include "RenderView.h"
#include "StyleAnimations.h"
#include "StylableInlines.h"
#include "StyleChangedAnimatableProperties.h"
#include "StyleCustomPropertyData.h"
#include "StyleInterpolation.h"
#include "StyleOriginatedAnimation.h"
#include "StyleOriginatedTimelinesController.h"
#include "StylePropertyShorthand.h"
#include "StyleResolver.h"
#include "StyleScope.h"
#include "StyleTreeResolver.h"
#include "ViewTransition.h"
#include "WebAnimation.h"
#include "WebAnimationUtilities.h"
#include <ranges>
namespace WebCore {
const std::optional<const Styleable> Styleable::fromRenderer(const RenderElement& renderer)
{
if (!renderer.style().pseudoElementType()) {
if (RefPtr element = renderer.element())
return fromElement(*element);
return { };
}
switch (*renderer.style().pseudoElementType()) {
case PseudoElementType::Backdrop:
for (auto& topLayerElement : renderer.document().topLayerElements()) {
if (topLayerElement->renderer() && topLayerElement->renderer()->backdropRenderer() == &renderer)
return Styleable(topLayerElement.get(), Style::PseudoElementIdentifier { PseudoElementType::Backdrop });
}
break;
case PseudoElementType::Marker: {
auto* ancestor = renderer.parent();
while (ancestor) {
auto* renderListItem = dynamicDowncast<RenderListItem>(ancestor);
if (renderListItem && ancestor->element() && renderListItem->markerRenderer() == &renderer)
return Styleable(*ancestor->element(), Style::PseudoElementIdentifier { PseudoElementType::Marker });
ancestor = ancestor->parent();
}
break;
}
case PseudoElementType::ViewTransitionGroup:
case PseudoElementType::ViewTransitionImagePair:
case PseudoElementType::ViewTransitionNew:
case PseudoElementType::ViewTransitionOld:
if (RefPtr documentElement = renderer.document().documentElement())
return Styleable(*documentElement, renderer.style().pseudoElementIdentifier());
break;
case PseudoElementType::ViewTransition:
if (RefPtr documentElement = renderer.document().documentElement())
return Styleable(*documentElement, Style::PseudoElementIdentifier { PseudoElementType::ViewTransition });
break;
case PseudoElementType::After:
case PseudoElementType::Before:
if (RefPtr element = renderer.element())
return fromElement(*element);
break;
default:
return std::nullopt;
}
return std::nullopt;
}
RenderElement* Styleable::renderer() const
{
if (!pseudoElementIdentifier)
return element.renderer();
switch (pseudoElementIdentifier->type) {
case PseudoElementType::After:
if (auto* afterPseudoElement = element.afterPseudoElement())
return afterPseudoElement->renderer();
break;
case PseudoElementType::Backdrop:
if (auto* hostRenderer = element.renderer())
return hostRenderer->backdropRenderer().get();
break;
case PseudoElementType::Before:
if (auto* beforePseudoElement = element.beforePseudoElement())
return beforePseudoElement->renderer();
break;
case PseudoElementType::Marker:
if (auto* renderListItem = dynamicDowncast<RenderListItem>(element.renderer())) {
auto* markerRenderer = renderListItem->markerRenderer();
if (markerRenderer && !markerRenderer->style().hasUsedContentNone())
return markerRenderer;
}
break;
case PseudoElementType::ViewTransition:
if (element.renderer() && element.renderer()->isDocumentElementRenderer()) {
if (WeakPtr containingBlock = element.renderer()->view().viewTransitionContainingBlock())
return containingBlock->firstChildBox();
}
break;
case PseudoElementType::ViewTransitionGroup:
case PseudoElementType::ViewTransitionImagePair:
case PseudoElementType::ViewTransitionNew:
case PseudoElementType::ViewTransitionOld: {
if (!element.renderer() || !element.renderer()->isDocumentElementRenderer())
return nullptr;
// Find the right ::view-transition-group().
WeakPtr correctGroup = element.renderer()->view().viewTransitionGroupForName(pseudoElementIdentifier->nameArgument);
if (!correctGroup)
return nullptr;
// Return early if we're looking for ::view-transition-group().
if (pseudoElementIdentifier->type == PseudoElementType::ViewTransitionGroup)
return correctGroup.get();
// Go through all descendants until we find the relevant pseudo element otherwise.
for (auto& descendant : descendantsOfType<RenderBox>(CheckedRef { *correctGroup }.get())) {
if (descendant.style().pseudoElementType() == pseudoElementIdentifier->type)
return &descendant;
}
break;
}
default:
return nullptr;
}
return nullptr;
}
std::unique_ptr<RenderStyle> Styleable::computeAnimatedStyle() const
{
std::unique_ptr<RenderStyle> animatedStyle;
auto* effectStack = keyframeEffectStack();
if (!effectStack)
return animatedStyle;
for (const auto& effect : effectStack->sortedEffects())
effect->getAnimatedStyle(animatedStyle);
return animatedStyle;
}
bool Styleable::computeAnimationExtent(LayoutRect& bounds) const
{
auto* animations = this->animations();
if (!animations)
return false;
RefPtr<KeyframeEffect> matchingEffect = nullptr;
for (const auto& animation : *animations) {
if (RefPtr keyframeEffect = animation->keyframeEffect()) {
if (animatablePropertiesContainTransformRelatedProperty(keyframeEffect->blendingKeyframes().properties()))
matchingEffect = keyframeEffect;
}
}
if (matchingEffect)
return matchingEffect->computeExtentOfTransformAnimation(bounds);
return true;
}
bool Styleable::mayHaveNonZeroOpacity() const
{
auto* renderer = this->renderer();
if (!renderer)
return false;
if (!renderer->style().opacity().isZero())
return true;
if (renderer->style().willChange().containsProperty(CSSPropertyOpacity))
return true;
auto* effectStack = keyframeEffectStack();
return effectStack && effectStack->containsProperty(CSSPropertyOpacity);
}
bool Styleable::isRunningAcceleratedAnimationOfProperty(CSSPropertyID property) const
{
auto* effectStack = keyframeEffectStack();
return effectStack && effectStack->hasMatchingEffect([property](auto& effect) {
return effect.isCurrentlyAffectingProperty(property, KeyframeEffect::Accelerated::Yes);
});
}
bool Styleable::isRunningAcceleratedTransformRelatedAnimation() const
{
auto* effectStack = keyframeEffectStack();
return effectStack && effectStack->hasMatchingEffect([](auto& effect) {
return effect.isRunningAcceleratedTransformRelatedAnimation();
});
}
bool Styleable::hasRunningAcceleratedAnimations() const
{
if (auto* effectStack = keyframeEffectStack()) {
if (effectStack->hasAcceleratedEffects(element.document().settings()))
return true;
}
if (auto* animations = this->animations()) {
for (const auto& animation : *animations) {
if (RefPtr keyframeEffect = animation->keyframeEffect()) {
if (keyframeEffect->isRunningAccelerated())
return true;
}
}
}
return false;
}
void Styleable::animationWasAdded(WebAnimation& animation) const
{
ensureAnimations().add(animation);
}
static inline bool removeCSSTransitionFromMap(CSSTransition& transition, AnimatableCSSPropertyToTransitionMap& cssTransitionsByProperty)
{
auto transitionIterator = cssTransitionsByProperty.find(transition.property());
if (transitionIterator == cssTransitionsByProperty.end() || transitionIterator->value.ptr() != &transition)
return false;
cssTransitionsByProperty.remove(transitionIterator);
return true;
}
void Styleable::removeStyleOriginatedAnimationFromListsForOwningElement(WebAnimation& animation) const
{
ASSERT(is<StyleOriginatedAnimation>(animation));
if (auto* transition = dynamicDowncast<CSSTransition>(animation)) {
if (!removeCSSTransitionFromMap(*transition, ensureRunningTransitionsByProperty()))
removeCSSTransitionFromMap(*transition, ensureCompletedTransitionsByProperty());
}
}
void Styleable::animationWasRemoved(WebAnimation& animation) const
{
ensureAnimations().remove(animation);
// Now, if we're dealing with a CSS Transition, we remove it from the m_elementToRunningCSSTransitionByAnimatableCSSProperty map.
// We don't need to do this for CSS Animations because their timing can be set via CSS to end, which would cause this
// function to be called, but they should remain associated with their owning element until this is changed via a call
// to the JS API or changing the target element's animation-name property.
if (is<CSSTransition>(animation))
removeStyleOriginatedAnimationFromListsForOwningElement(animation);
}
void Styleable::elementWasRemoved() const
{
cancelStyleOriginatedAnimations();
if (CheckedPtr styleOriginatedTimelinesController = element.protectedDocument()->styleOriginatedTimelinesController())
styleOriginatedTimelinesController->styleableWasRemoved(*this);
}
void Styleable::willChangeRenderer() const
{
if (auto* animations = this->animations()) {
for (const auto& animation : *animations)
animation->willChangeRenderer();
}
}
OptionSet<AnimationImpact> Styleable::applyKeyframeEffects(RenderStyle& targetStyle, HashSet<AnimatableCSSProperty>& affectedProperties, const RenderStyle* previousLastStyleChangeEventStyle, const Style::ResolutionContext& resolutionContext) const
{
return element.ensureKeyframeEffectStack(pseudoElementIdentifier).applyKeyframeEffects(targetStyle, affectedProperties, previousLastStyleChangeEventStyle, resolutionContext);
}
void Styleable::cancelStyleOriginatedAnimations() const
{
// It is important we don't cancel style-originated animations when entering the page cache
// since any JS wrapper that is kept alive in the page cache could be associated with an animation
// that itself has not been kept alive (or rather canceled) when entering the page cache.
if (element.protectedDocument()->backForwardCacheState() != Document::NotInBackForwardCache)
return;
cancelStyleOriginatedAnimations({ });
if (CheckedPtr styleOriginatedTimelinesController = element.protectedDocument()->styleOriginatedTimelinesController())
styleOriginatedTimelinesController->unregisterNamedTimelinesAssociatedWithElement(*this);
}
void Styleable::cancelStyleOriginatedAnimations(const WeakStyleOriginatedAnimations& animationsToCancelSilently) const
{
if (auto* animations = this->animations()) {
for (auto& animation : *animations) {
if (RefPtr styleOriginatedAnimation = dynamicDowncast<StyleOriginatedAnimation>(animation.get())) {
styleOriginatedAnimation->cancelFromStyle(animationsToCancelSilently.contains(styleOriginatedAnimation.get()) ? WebAnimation::Silently::Yes : WebAnimation::Silently::No);
setLastStyleChangeEventStyle(nullptr);
}
}
}
if (auto* effectStack = keyframeEffectStack())
effectStack->setCSSAnimationList(std::nullopt);
setAnimationsCreatedByMarkup({ });
}
static bool keyframesRuleExistsForAnimation(Element& element, const Style::ScopedName& animationName)
{
return Style::Scope::resolveTreeScopedReference(element, animationName, [](const Style::Scope& scope, const AtomString& name) -> bool {
if (RefPtr resolver = scope.resolverIfExists())
return resolver->isAnimationNameValid(name);
return false;
});
}
bool Styleable::animationListContainsNewlyValidAnimation(const Style::Animations& animations) const
{
auto& keyframeEffectStack = ensureKeyframeEffectStack();
if (!keyframeEffectStack.hasInvalidCSSAnimationNames())
return false;
for (auto& currentAnimation : animations.usedValues()) {
if (auto keyframesName = currentAnimation.name().tryKeyframesName(); keyframesName && !keyframesName->name.isEmpty() && keyframeEffectStack.containsInvalidCSSAnimationName(keyframesName->name) && keyframesRuleExistsForAnimation(element, *keyframesName))
return true;
}
return false;
}
void Styleable::updateCSSAnimations(const RenderStyle* currentStyle, const RenderStyle& newStyle, const Style::ResolutionContext& resolutionContext, WeakStyleOriginatedAnimations& newStyleOriginatedAnimations, Style::IsInDisplayNoneTree isInDisplayNoneTree) const
{
auto& keyframeEffectStack = ensureKeyframeEffectStack();
// In case this element is newly getting a "display: none" we need to cancel all of its animations and disregard new ones.
if ((!currentStyle || currentStyle->display() != DisplayType::None) && newStyle.display() == DisplayType::None) {
for (auto& cssAnimation : animationsCreatedByMarkup())
cssAnimation->cancelFromStyle();
keyframeEffectStack.setCSSAnimationList(std::nullopt);
setAnimationsCreatedByMarkup({ });
return;
}
auto& currentAnimationList = newStyle.animations();
auto& previousAnimationList = keyframeEffectStack.cssAnimationList();
if (!element.hasPendingKeyframesUpdate(pseudoElementIdentifier) && previousAnimationList && !previousAnimationList->isInitial() && newStyle.hasAnimations() && *previousAnimationList == newStyle.animations() && !animationListContainsNewlyValidAnimation(newStyle.animations()))
return;
CSSAnimationCollection newAnimations;
auto& previousAnimations = animationsCreatedByMarkup();
keyframeEffectStack.clearInvalidCSSAnimationNames();
// https://www.w3.org/TR/css-animations-1/#animations
// The same @keyframes rule name may be repeated within an animation-name. Changes to the animation-name update existing
// animations by iterating over the new list of animations from last to first, and, for each animation, finding the last
// matching animation in the list of existing animations. If a match is found, the existing animation is updated using the
// animation properties corresponding to its position in the new list of animations, whilst maintaining its current playback
// time as described above. The matching animation is removed from the existing list of animations such that it will not match
// twice. If a match is not found, a new animation is created. As a result, updating animation-name from ‘a’ to ‘a, a’ will
// cause the existing animation for ‘a’ to become the second animation in the list and a new animation will be created for the
// first item in the list.
if (!currentAnimationList.isInitial()) {
for (auto& currentAnimation : currentAnimationList.usedValues() | std::views::reverse) {
auto keyframesName = currentAnimation.name().tryKeyframesName();
if (!keyframesName || keyframesName->name.isEmpty())
continue;
if (!keyframesRuleExistsForAnimation(element, *keyframesName)) {
keyframeEffectStack.addInvalidCSSAnimationName(keyframesName->name);
continue;
}
auto& currentAnimationName = keyframesName->name;
bool foundMatchingAnimation = false;
for (auto& previousAnimation : previousAnimations) {
if (previousAnimation->animationName() == currentAnimationName) {
// Timing properties or play state may have changed so we need to update the backing animation with
// the Animation found in the current style.
previousAnimation->setBackingStyleAnimation(currentAnimation);
// Keyframes may have been cleared if the @keyframes rules was changed since
// the last style update, so we must ensure keyframes are picked up.
previousAnimation->updateKeyframesIfNeeded(currentStyle, newStyle, resolutionContext);
newStyleOriginatedAnimations.append(previousAnimation.ptr());
newAnimations.add(previousAnimation);
// Remove the matched animation from the list of previous animations so we may not match it again.
previousAnimations.remove(previousAnimation);
foundMatchingAnimation = true;
break;
}
}
if (!foundMatchingAnimation && isInDisplayNoneTree == Style::IsInDisplayNoneTree::No) {
auto cssAnimation = CSSAnimation::create(*this, Style::Animation { currentAnimation }, currentStyle, newStyle, resolutionContext);
newStyleOriginatedAnimations.append(cssAnimation.ptr());
newAnimations.add(WTF::move(cssAnimation));
}
}
}
// Any animation found in previousAnimations but not found in newAnimations is not longer current and should be canceled.
for (auto& previousAnimation : previousAnimations) {
if (!newAnimations.contains(previousAnimation)) {
if (previousAnimation->owningElement())
previousAnimation->cancelFromStyle();
}
}
setAnimationsCreatedByMarkup(WTF::move(newAnimations));
keyframeEffectStack.setCSSAnimationList(Style::Animations { currentAnimationList });
element.cssAnimationsDidUpdate(pseudoElementIdentifier);
}
static KeyframeEffect* keyframeEffectForElementAndProperty(const Styleable& styleable, const AnimatableCSSProperty& property)
{
if (auto* keyframeEffectStack = styleable.keyframeEffectStack()) {
auto& effects = keyframeEffectStack->sortedEffects();
for (const auto& effect : effects | std::views::reverse) {
if (effect->animatesProperty(property))
return effect.get();
}
}
return nullptr;
}
static bool propertyInStyleMatchesValueForTransitionInMap(const AnimatableCSSProperty& property, const RenderStyle& style, AnimatableCSSPropertyToTransitionMap& transitions, const Document& document)
{
if (RefPtr transition = transitions.get(property)) {
if (Style::Interpolation::equals(property, style, transition->targetStyle(), document))
return true;
}
return false;
}
static bool transitionMatchesProperty(const Style::Transition& transition, const AnimatableCSSProperty& property, const RenderStyle& style)
{
if (transition.isPropertyFilled())
return false;
return WTF::switchOn(transition.property(),
[&](const CSS::Keyword::All&) {
return true;
},
[&](const CSS::Keyword::None&) {
return false;
},
[&](const Style::SingleTransitionProperty::UnknownProperty&) {
return false;
},
[&](const Style::SingleTransitionProperty::SingleProperty& singleProperty) {
return WTF::switchOn(singleProperty.value,
[&](CSSPropertyID propertyId) {
if (!std::holds_alternative<CSSPropertyID>(property))
return false;
auto propertyIdToMatch = std::get<CSSPropertyID>(property);
auto resolvedPropertyId = CSSProperty::resolveDirectionAwareProperty(propertyId, style.writingMode());
if (resolvedPropertyId == propertyIdToMatch)
return true;
for (auto longhand : shorthandForProperty(resolvedPropertyId)) {
auto resolvedLonghand = CSSProperty::resolveDirectionAwareProperty(longhand, style.writingMode());
if (resolvedLonghand == propertyIdToMatch)
return true;
}
return false;
},
[&](const AtomString& customProperty) {
if (!std::holds_alternative<AtomString>(property))
return false;
return std::get<AtomString>(property) == customProperty;
}
);
}
);
}
static void compileTransitionPropertiesInStyle(const RenderStyle& style, CSSPropertiesBitSet& transitionProperties, HashSet<AtomString>& transitionCustomProperties, bool& transitionPropertiesContainAll)
{
auto& transitions = style.transitions();
if (transitions.isInitial()) {
// If we don't have any transitions in the map, this means that the initial value "all 0s" was set.
transitionPropertiesContainAll = true;
return;
}
for (const auto& transition : transitions.usedValues()) {
WTF::switchOn(transition.property(),
[&](const CSS::Keyword::All&) {
transitionPropertiesContainAll = true;
},
[&](const CSS::Keyword::None&) {
// Do nothing.
},
[&](const Style::SingleTransitionProperty::UnknownProperty&) {
// Do nothing.
},
[&](const Style::SingleTransitionProperty::SingleProperty& property) {
WTF::switchOn(property.value,
[&](CSSPropertyID propertyId) {
auto resolvedPropertyId = CSSProperty::resolveDirectionAwareProperty(propertyId, style.writingMode());
if (isShorthand(resolvedPropertyId)) {
for (auto longhand : shorthandForProperty(resolvedPropertyId))
transitionProperties.m_properties.set(longhand);
} else if (resolvedPropertyId != CSSPropertyInvalid)
transitionProperties.m_properties.set(resolvedPropertyId);
},
[&](const AtomString& customProperty) {
transitionCustomProperties.add(customProperty);
}
);
}
);
}
}
static void updateCSSTransitionsForStyleableAndProperty(const Styleable& styleable, const AnimatableCSSProperty& property, const RenderStyle& currentStyle, const RenderStyle& newStyle, const MonotonicTime generationTime, WeakStyleOriginatedAnimations& newStyleOriginatedAnimations)
{
RefPtr keyframeEffect = keyframeEffectForElementAndProperty(styleable, property);
RefPtr animation = keyframeEffect ? keyframeEffect->animation() : nullptr;
bool isDeclarative = false;
if (RefPtr styleOriginatedAnimation = dynamicDowncast<StyleOriginatedAnimation>(animation.get())) {
if (auto owningElement = styleOriginatedAnimation->owningElement())
isDeclarative = *owningElement == styleable;
}
if (animation && !isDeclarative)
return;
Ref document = styleable.element.document();
auto hasMatchingTransitionProperty = false;
auto matchingTransitionDuration = 0.0;
std::optional<Style::Transition> matchingTransition;
if (auto& transitions = newStyle.transitions(); !transitions.isInitial()) {
for (auto& transition : transitions.usedValues()) {
if (transitionMatchesProperty(transition, property, newStyle)) {
hasMatchingTransitionProperty = true;
matchingTransition = transition;
matchingTransitionDuration = std::max(0.0, matchingTransition->duration().value) + matchingTransition->delay().value;
}
}
} else if (!document->quirks().needsResettingTransitionCancelsRunningTransitionQuirk()) {
// If we don't have any transitions in the map, this means that the initial value "all 0s" was set
// and thus all properties match.
hasMatchingTransitionProperty = true;
}
// https://drafts.csswg.org/css-transitions-1/#before-change-style
// Define the before-change style as the computed values of all properties on the element as of the previous style change event, except with
// any styles derived from declarative animations such as CSS Transitions, CSS Animations, and SMIL Animations updated to the current time.
auto beforeChangeStyle = [&]() -> const RenderStyle {
if (auto* lastStyleChangeEventStyle = styleable.lastStyleChangeEventStyle()) {
auto style = RenderStyle::clone(*lastStyleChangeEventStyle);
if (auto* keyframeEffectStack = styleable.keyframeEffectStack()) {
for (const auto& effect : keyframeEffectStack->sortedEffects()) {
if (effect->animatesProperty(property))
Ref { *effect }->apply(style, { nullptr });
}
}
return style;
}
return RenderStyle::clone(currentStyle);
}();
// https://drafts.csswg.org/css-transitions-1/#after-change-style
// Likewise, define the after-change style as the computed values of all properties on the element based on the information known at the start
// of that style change event, but using the computed values of the animation-* properties from the before-change style, excluding any styles
// from CSS Transitions in the computation, and inheriting from the after-change style of the parent. Note that this means the after-change
// style does not differ from the before-change style due to newly created or canceled CSS Animations.
auto afterChangeStyle = [&]() -> const RenderStyle {
if (is<CSSAnimation>(animation) && animation->isRelevant()) {
auto animatedStyle = RenderStyle::clone(newStyle);
animation->resolve(animatedStyle, { nullptr });
return animatedStyle;
}
return RenderStyle::clone(newStyle);
}();
auto allowsDiscreteTransitions = matchingTransition && matchingTransition->behavior() == TransitionBehavior::AllowDiscrete;
auto propertyCanBeInterpolated = [&](const AnimatableCSSProperty& property, const RenderStyle& a, const RenderStyle& b) {
return allowsDiscreteTransitions || Style::Interpolation::canInterpolate(property, a, b, document.get());
};
auto createCSSTransition = [&](const RenderStyle& oldStyle, Seconds delay, Seconds duration, const RenderStyle& reversingAdjustedStartStyle, double reversingShorteningFactor) {
auto cssTransition = CSSTransition::create(styleable, property, generationTime, *matchingTransition, oldStyle, afterChangeStyle, delay, duration, reversingAdjustedStartStyle, reversingShorteningFactor);
newStyleOriginatedAnimations.append(cssTransition.ptr());
styleable.ensureRunningTransitionsByProperty().set(property, WTF::move(cssTransition));
};
bool hasRunningTransition = styleable.hasRunningTransitionForProperty(property);
if (!hasRunningTransition
&& hasMatchingTransitionProperty && matchingTransitionDuration > 0
&& !Style::Interpolation::equals(property, beforeChangeStyle, afterChangeStyle, document.get())
&& propertyCanBeInterpolated(property, beforeChangeStyle, afterChangeStyle)
&& !propertyInStyleMatchesValueForTransitionInMap(property, afterChangeStyle, styleable.ensureCompletedTransitionsByProperty(), document.get())) {
// 1. If all of the following are true:
// - the element does not have a running transition for the property,
// - the before-change style is different from and can be interpolated with the after-change style for that property,
// - the element does not have a completed transition for the property or the end value of the completed transition is different from the after-change style for the property,
// - there is a matching transition-property value, and
// - the combined duration is greater than 0s,
// then implementations must remove the completed transition (if present) from the set of completed transitions
styleable.ensureCompletedTransitionsByProperty().remove(property);
// and start a transition whose:
// - start time is the time of the style change event plus the matching transition delay,
// - end time is the start time plus the matching transition duration,
// - start value is the value of the transitioning property in the before-change style,
// - end value is the value of the transitioning property in the after-change style,
// - reversing-adjusted start value is the same as the start value, and
// - reversing shortening factor is 1.
ASSERT(matchingTransition);
auto delay = Seconds(matchingTransition->delay().value);
auto duration = Seconds(matchingTransition->duration().value);
auto& reversingAdjustedStartStyle = beforeChangeStyle;
auto reversingShorteningFactor = 1;
createCSSTransition(beforeChangeStyle, delay, duration, reversingAdjustedStartStyle, reversingShorteningFactor);
ASSERT(styleable.hasRunningTransitionForProperty(property));
hasRunningTransition = true;
} else if (styleable.hasCompletedTransitionForProperty(property) && !propertyInStyleMatchesValueForTransitionInMap(property, afterChangeStyle, styleable.ensureCompletedTransitionsByProperty(), document)) {
// 2. Otherwise, if the element has a completed transition for the property and the end value of the completed transition is different from
// the after-change style for the property, then implementations must remove the completed transition from the set of completed transitions.
styleable.ensureCompletedTransitionsByProperty().remove(property);
}
if (!hasMatchingTransitionProperty) {
// 3. If the element has a running transition or completed transition for the property, and there is not a matching transition-property
// value, then implementations must cancel the running transition or remove the completed transition from the set of completed transitions.
if (hasRunningTransition)
styleable.ensureRunningTransitionsByProperty().take(property)->cancelFromStyle();
else if (styleable.hasCompletedTransitionForProperty(property))
styleable.ensureCompletedTransitionsByProperty().remove(property);
return;
}
ASSERT(hasMatchingTransitionProperty);
if (hasRunningTransition && !propertyInStyleMatchesValueForTransitionInMap(property, afterChangeStyle, styleable.ensureRunningTransitionsByProperty(), document)) {
auto previouslyRunningTransition = styleable.ensureRunningTransitionsByProperty().take(property);
auto previouslyRunningTransitionCurrentStyle = [&] {
if (auto* lastStyleChangeEventStyle = styleable.lastStyleChangeEventStyle()) {
auto style = RenderStyle::clone(*lastStyleChangeEventStyle);
ASSERT(previouslyRunningTransition->keyframeEffect());
Ref { *previouslyRunningTransition->keyframeEffect() }->apply(style, { nullptr });
return style;
}
return RenderStyle::clone(currentStyle);
}();
// 4. If the element has a running transition for the property, there is a matching transition-property value, and the end value of the running
// transition is not equal to the value of the property in the after-change style, then:
if (Style::Interpolation::equals(property, previouslyRunningTransitionCurrentStyle, afterChangeStyle, document) || !propertyCanBeInterpolated(property, currentStyle, afterChangeStyle)) {
// 1. If the current value of the property in the running transition is equal to the value of the property in the after-change style,
// or if these two values cannot be interpolated, then implementations must cancel the running transition.
previouslyRunningTransition->cancelFromStyle();
} else if (matchingTransitionDuration <= 0.0 || !propertyCanBeInterpolated(property, previouslyRunningTransitionCurrentStyle, afterChangeStyle)) {
// 2. Otherwise, if the combined duration is less than or equal to 0s, or if the current value of the property in the running transition
// cannot be interpolated with the value of the property in the after-change style, then implementations must cancel the running transition.
previouslyRunningTransition->cancelFromStyle();
} else if (Style::Interpolation::equals(property, previouslyRunningTransition->reversingAdjustedStartStyle(), afterChangeStyle, document)) {
// 3. Otherwise, if the reversing-adjusted start value of the running transition is the same as the value of the property in the after-change
// style (see the section on reversing of transitions for why these case exists), implementations must cancel the running transition
// and start a new transition whose:
// - reversing-adjusted start value is the end value of the running transition,
// - reversing shortening factor is the absolute value, clamped to the range [0, 1], of the sum of:
// 1. the output of the timing function of the old transition at the time of the style change event, times the reversing shortening factor of the old transition
// 2. 1 minus the reversing shortening factor of the old transition.
// - start time is the time of the style change event plus:
// 1. if the matching transition delay is nonnegative, the matching transition delay, or
// 2. if the matching transition delay is negative, the product of the new transition’s reversing shortening factor and the matching transition delay,
// - end time is the start time plus the product of the matching transition duration and the new transition’s reversing shortening factor,
// - start value is the current value of the property in the running transition,
// - end value is the value of the property in the after-change style
ASSERT(matchingTransition);
auto& reversingAdjustedStartStyle = previouslyRunningTransition->targetStyle();
double transformedProgress = 1;
if (RefPtr effect = previouslyRunningTransition->effect()) {
if (auto computedTimingProgress = effect->getComputedTiming().progress)
transformedProgress = *computedTimingProgress;
}
auto reversingShorteningFactor = std::max(std::min(((transformedProgress * previouslyRunningTransition->reversingShorteningFactor()) + (1 - previouslyRunningTransition->reversingShorteningFactor())), 1.0), 0.0);
auto delay = matchingTransition->delay().value < 0 ? Seconds(matchingTransition->delay().value) * reversingShorteningFactor : Seconds(matchingTransition->delay().value);
auto duration = Seconds(matchingTransition->duration().value) * reversingShorteningFactor;
previouslyRunningTransition->cancelFromStyle();
createCSSTransition(beforeChangeStyle, delay, duration, reversingAdjustedStartStyle, reversingShorteningFactor);
} else {
// 4. Otherwise, implementations must cancel the running transition and start a new transition whose:
// - start time is the time of the style change event plus the matching transition delay,
// - end time is the start time plus the matching transition duration,
// - start value is the current value of the property in the running transition,
// - end value is the value of the property in the after-change style,
// - reversing-adjusted start value is the same as the start value, and
// - reversing shortening factor is 1.
ASSERT(matchingTransition);
auto delay = Seconds(matchingTransition->delay().value);
auto duration = Seconds(matchingTransition->duration().value);
auto& reversingAdjustedStartStyle = currentStyle;
auto reversingShorteningFactor = 1;
previouslyRunningTransition->cancelFromStyle();
createCSSTransition(previouslyRunningTransitionCurrentStyle, delay, duration, reversingAdjustedStartStyle, reversingShorteningFactor);
}
}
}
void Styleable::updateCSSTransitions(const RenderStyle& currentStyle, const RenderStyle& newStyle, WeakStyleOriginatedAnimations& newStyleOriginatedAnimations) const
{
// In case this element previous had "display: none" we can stop considering transitions altogether.
if (currentStyle.display() == DisplayType::None)
return;
// In case this element is newly getting a "display: none" we need to cancel all of its transitions and disregard new ones,
// unless it will transition the "display" property itself.
if (currentStyle.hasTransitions() && currentStyle.display() != DisplayType::None && newStyle.display() == DisplayType::None && !styleHasDisplayTransition(newStyle)) {
if (hasRunningTransitions()) {
auto runningTransitions = ensureRunningTransitionsByProperty();
for (const auto& cssTransitionsByAnimatableCSSPropertyMapItem : runningTransitions)
cssTransitionsByAnimatableCSSPropertyMapItem.value->cancelFromStyle();
}
return;
}
// Section 3 "Starting of transitions" from the CSS Transitions Level 1 specification.
// https://drafts.csswg.org/css-transitions-1/#starting
auto generationTime = MonotonicTime::now();
// First, let's compile the list of all CSS properties found in the current style and the after-change style.
bool transitionPropertiesContainAll = false;
CSSPropertiesBitSet transitionProperties;
HashSet<AtomString> transitionCustomProperties;
compileTransitionPropertiesInStyle(currentStyle, transitionProperties, transitionCustomProperties, transitionPropertiesContainAll);
compileTransitionPropertiesInStyle(newStyle, transitionProperties, transitionCustomProperties, transitionPropertiesContainAll);
if (transitionPropertiesContainAll) {
auto addProperty = [&](const AnimatableCSSProperty& property) {
WTF::switchOn(property,
[&](CSSPropertyID propertyId) {
if (isShorthand(propertyId)) {
for (auto longhand : shorthandForProperty(propertyId))
transitionProperties.m_properties.set(longhand);
} else if (propertyId != CSSPropertyInvalid)
transitionProperties.m_properties.set(propertyId);
},
[&](const AtomString&) { }
);
};
const RenderStyle* targetStyle = &currentStyle;
if (auto* lastStyleChangeEventStyle = this->lastStyleChangeEventStyle())
targetStyle = lastStyleChangeEventStyle;
Style::conservativelyCollectChangedAnimatableProperties(*targetStyle, newStyle, transitionProperties);
// When we have keyframeEffectStack, it can affect on properties. So we just add them.
if (keyframeEffectStack()) {
for (const auto& effect : keyframeEffectStack()->sortedEffects()) {
for (const auto& property : effect->animatedProperties())
addProperty(property);
if (RefPtr transition = dynamicDowncast<CSSTransition>(effect->animation()))
addProperty(transition->property());
}
}
if (auto* properties = element.runningTransitionsByProperty(pseudoElementIdentifier)) {
for (const auto& [property, transition] : *properties)
addProperty(property);
}
if (auto* properties = element.completedTransitionsByProperty(pseudoElementIdentifier)) {
for (const auto& [property, transition] : *properties)
addProperty(property);
}
auto gatherAnimatableCustomProperties = [&](const Style::CustomPropertyData& customPropertyData) {
if (!customPropertyData.mayHaveAnimatableProperties())
return;
customPropertyData.forEach([&](auto& customPropertyAndValuePair) {
auto [customProperty, customPropertyValue] = customPropertyAndValuePair;
if (customPropertyValue->isAnimatable())
transitionCustomProperties.add(customProperty);
return IterationStatus::Continue;
});
};
gatherAnimatableCustomProperties(currentStyle.inheritedCustomProperties());
gatherAnimatableCustomProperties(currentStyle.nonInheritedCustomProperties());
gatherAnimatableCustomProperties(newStyle.inheritedCustomProperties());
gatherAnimatableCustomProperties(newStyle.nonInheritedCustomProperties());
}
transitionProperties.m_properties.forEachSetBit([&](unsigned index) {
CSSPropertyID propertyId = static_cast<CSSPropertyID>(index);
if (isShorthand(propertyId))
return;
updateCSSTransitionsForStyleableAndProperty(*this, propertyId, currentStyle, newStyle, generationTime, newStyleOriginatedAnimations);
});
for (auto& customProperty : transitionCustomProperties)
updateCSSTransitionsForStyleableAndProperty(*this, customProperty, currentStyle, newStyle, generationTime, newStyleOriginatedAnimations);
}
void Styleable::updateCSSScrollTimelines(const RenderStyle* currentStyle, const RenderStyle& afterChangeStyle) const
{
auto updateAnonymousScrollTimelines = [&]() {
if (currentStyle && currentStyle->scrollTimelines() == afterChangeStyle.scrollTimelines())
return;
auto& currentTimelines = afterChangeStyle.scrollTimelines();
for (auto& currentTimeline : currentTimelines)
currentTimeline->setSource(&element);
if (!currentStyle)
return;
for (auto& previousTimeline : currentStyle->scrollTimelines()) {
if (!currentTimelines.contains(previousTimeline) && previousTimeline->source() == &element)
previousTimeline->setSource(nullptr);
}
};
auto updateNamedScrollTimelines = [&]() {
if (currentStyle && currentStyle->scrollTimelineNames() == afterChangeStyle.scrollTimelineNames() && currentStyle->scrollTimelineAxes() == afterChangeStyle.scrollTimelineAxes())
return;
CheckedRef styleOriginatedTimelinesController = element.protectedDocument()->ensureStyleOriginatedTimelinesController();
auto& currentTimelineNames = afterChangeStyle.scrollTimelineNames();
auto& currentTimelineAxes = afterChangeStyle.scrollTimelineAxes();
auto numberOfAxes = currentTimelineAxes.size();
for (auto [i, name] : indexedRange(currentTimelineNames))
styleOriginatedTimelinesController->registerNamedScrollTimeline(name.value.value, *this, currentTimelineAxes[i % numberOfAxes]);
if (!currentStyle)
return;
for (auto& previousTimelineName : currentStyle->scrollTimelineNames()) {
if (!currentTimelineNames.contains(previousTimelineName))
styleOriginatedTimelinesController->unregisterNamedTimeline(previousTimelineName.value.value, *this);
}
};
updateAnonymousScrollTimelines();
updateNamedScrollTimelines();
};
void Styleable::updateCSSViewTimelines(const RenderStyle* currentStyle, const RenderStyle& afterChangeStyle) const
{
auto updateAnonymousViewTimelines = [&]() {
if (currentStyle && currentStyle->viewTimelines() == afterChangeStyle.viewTimelines())
return;
auto& currentTimelines = afterChangeStyle.viewTimelines();
for (auto& currentTimeline : currentTimelines)
currentTimeline->setSubject(&element);
if (!currentStyle)
return;
for (auto& previousTimeline : currentStyle->viewTimelines()) {
if (!currentTimelines.contains(previousTimeline) && previousTimeline->subject() == &element)
previousTimeline->setSubject(nullptr);
}
};
auto updateNamedViewTimelines = [&]() {
if ((currentStyle && currentStyle->viewTimelineNames() == afterChangeStyle.viewTimelineNames()) && (currentStyle && currentStyle->viewTimelineAxes() == afterChangeStyle.viewTimelineAxes()) && (currentStyle && currentStyle->viewTimelineInsets() == afterChangeStyle.viewTimelineInsets()))
return;
CheckedRef styleOriginatedTimelinesController = element.protectedDocument()->ensureStyleOriginatedTimelinesController();
auto& currentTimelineNames = afterChangeStyle.viewTimelineNames();
auto& currentTimelineAxes = afterChangeStyle.viewTimelineAxes();
auto& currentTimelineInsets = afterChangeStyle.viewTimelineInsets();
auto numberOfAxes = currentTimelineAxes.size();
auto numberOfInsets = currentTimelineInsets.size();
for (auto [i, name] : indexedRange(currentTimelineNames))
styleOriginatedTimelinesController->registerNamedViewTimeline(name.value.value, *this, currentTimelineAxes[i % numberOfAxes], currentTimelineInsets[i % numberOfInsets]);
if (!currentStyle)
return;
for (auto& previousTimelineName : currentStyle->viewTimelineNames()) {
if (!currentTimelineNames.contains(previousTimelineName))
styleOriginatedTimelinesController->unregisterNamedTimeline(previousTimelineName.value.value, *this);
}
};
updateAnonymousViewTimelines();
updateNamedViewTimelines();
};
void Styleable::queryContainerDidChange() const
{
auto* animations = this->animations();
if (!animations)
return;
for (auto& animation : *animations) {
RefPtr keyframeEffect = animation->keyframeEffect();
if (keyframeEffect && keyframeEffect->blendingKeyframes().usesContainerUnits()) {
if (RefPtr cssAnimation = dynamicDowncast<CSSAnimation>(animation))
cssAnimation->keyframesRuleDidChange();
else
keyframeEffect->recomputeKeyframesAtNextOpportunity();
}
}
}
bool Styleable::capturedInViewTransition() const
{
return !element.viewTransitionCapturedName(pseudoElementIdentifier).isNull();
}
void Styleable::setCapturedInViewTransition(AtomString captureName)
{
element.setViewTransitionCapturedName(pseudoElementIdentifier, captureName);
if (CheckedPtr renderer = this->renderer()) {
bool changed = renderer->setCapturedInViewTransition(!captureName.isNull());
if (changed)
element.invalidateStyleAndLayerComposition();
}
}
WTF::TextStream& operator<<(WTF::TextStream& ts, const Styleable& styleable)
{
ts << styleable.element << ", "_s << styleable.pseudoElementIdentifier;
return ts;
}
WTF::TextStream& operator<<(WTF::TextStream& ts, const WeakStyleable& styleable)
{
ts << styleable.element() << ", "_s << styleable.pseudoElementIdentifier();
return ts;
}
} // namespace WebCore