blob: 1cb5d86f5eb82ccbd7c13021b8d54c74b7726bf6 [file] [log] [blame]
/*
* Copyright (C) 2019-2025 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 "WebAnimationUtilities.h"
#include "Animation.h"
#include "AnimationEventBase.h"
#include "AnimationList.h"
#include "AnimationPlaybackEvent.h"
#include "CSSAnimation.h"
#include "CSSAnimationEvent.h"
#include "CSSParserContext.h"
#include "CSSPropertyNames.h"
#include "CSSSelectorParser.h"
#include "CSSTransition.h"
#include "CSSTransitionEvent.h"
#include "Element.h"
#include "EventTargetInlines.h"
#include "KeyframeEffectStack.h"
#include "ScriptExecutionContext.h"
#include "StyleOriginatedAnimation.h"
#include "ViewTransition.h"
#include "WebAnimation.h"
#include <wtf/text/MakeString.h>
namespace WebCore {
static bool compareStyleOriginatedAnimationOwningElementPositionsInDocumentTreeOrder(const Styleable& a, const Styleable& b)
{
// We should not ever be calling this function with two Elements that are the same. If that were the case,
// then comparing objects of this kind would yield inconsistent results when comparing A == B and B == A.
// As such, this function should be called with std::stable_sort().
RELEASE_ASSERT_WITH_SECURITY_IMPLICATION(a != b);
// With regard to pseudo-elements, the sort order is as follows:
//
// - element
// - ::marker
// - ::before
// - any other pseudo-elements not mentioned specifically in this list, sorted in ascending order by the Unicode codepoints that make up each selector
// - ::after
// - element children
enum SortingIndex : uint8_t { NotPseudo, Marker, Before, FirstLetter, FirstLine, GrammarError, Highlight, WebKitScrollbar, Selection, SpellingError, TargetText, After, ViewTransition, ViewTransitionGroup, ViewTransitionImagePair, ViewTransitionOld, ViewTransitionNew, Other };
auto sortingIndex = [](const std::optional<Style::PseudoElementIdentifier>& pseudoElementIdentifier) -> SortingIndex {
if (!pseudoElementIdentifier)
return NotPseudo;
switch (pseudoElementIdentifier->pseudoId) {
case PseudoId::Marker:
return Marker;
case PseudoId::Before:
return Before;
case PseudoId::FirstLetter:
return FirstLetter;
case PseudoId::FirstLine:
return FirstLine;
case PseudoId::GrammarError:
return GrammarError;
case PseudoId::Highlight:
return Highlight;
case PseudoId::WebKitScrollbar:
return WebKitScrollbar;
case PseudoId::Selection:
return Selection;
case PseudoId::SpellingError:
return SpellingError;
case PseudoId::TargetText:
return TargetText;
case PseudoId::After:
return After;
case PseudoId::ViewTransition:
return ViewTransition;
case PseudoId::ViewTransitionGroup:
return ViewTransitionGroup;
case PseudoId::ViewTransitionImagePair:
return ViewTransitionImagePair;
case PseudoId::ViewTransitionOld:
return ViewTransitionOld;
case PseudoId::ViewTransitionNew:
return ViewTransitionNew;
default:
ASSERT_NOT_REACHED();
return Other;
}
};
Ref aReferenceElement = a.element;
Ref bReferenceElement = b.element;
if (aReferenceElement.ptr() == bReferenceElement.ptr()) {
if (isNamedViewTransitionPseudoElement(a.pseudoElementIdentifier) && isNamedViewTransitionPseudoElement(b.pseudoElementIdentifier) && a.pseudoElementIdentifier->nameArgument != b.pseudoElementIdentifier->nameArgument) {
RefPtr activeViewTransition = aReferenceElement->document().activeViewTransition();
ASSERT(activeViewTransition);
for (auto& key : activeViewTransition->namedElements().keys()) {
if (key == a.pseudoElementIdentifier->nameArgument)
return true;
if (key == b.pseudoElementIdentifier->nameArgument)
return false;
}
return false;
}
auto aSortingIndex = sortingIndex(a.pseudoElementIdentifier);
auto bSortingIndex = sortingIndex(b.pseudoElementIdentifier);
ASSERT(aSortingIndex != bSortingIndex);
return aSortingIndex < bSortingIndex;
}
return is_lt(treeOrder<Tree>(aReferenceElement.get(), bReferenceElement.get()));
}
static bool compareCSSTransitions(const CSSTransition& a, const CSSTransition& b)
{
ASSERT(a.owningElement());
ASSERT(b.owningElement());
auto& aOwningElement = a.owningElement();
auto& bOwningElement = b.owningElement();
// If the owning element of A and B differs, sort A and B by tree order of their corresponding owning elements.
if (*aOwningElement != *bOwningElement)
return compareStyleOriginatedAnimationOwningElementPositionsInDocumentTreeOrder(*aOwningElement, *bOwningElement);
// Otherwise, if A and B have different transition generation values, sort by their corresponding transition generation in ascending order.
if (a.generationTime() != b.generationTime())
return a.generationTime() < b.generationTime();
// Otherwise, sort A and B in ascending order by the Unicode codepoints that make up the expanded transition property name of each transition
// (i.e. without attempting case conversion and such that ‘-moz-column-width’ sorts before ‘column-width’).
return codePointCompareLessThan(a.transitionProperty(), b.transitionProperty());
}
static bool compareCSSAnimations(const CSSAnimation& a, const CSSAnimation& b)
{
// https://drafts.csswg.org/css-animations-2/#animation-composite-order
ASSERT(a.owningElement());
ASSERT(b.owningElement());
auto& aOwningElement = a.owningElement();
auto& bOwningElement = b.owningElement();
// If the owning element of A and B differs, sort A and B by tree order of their corresponding owning elements.
if (*aOwningElement != *bOwningElement)
return compareStyleOriginatedAnimationOwningElementPositionsInDocumentTreeOrder(*aOwningElement, *bOwningElement);
// Sort A and B based on their position in the computed value of the animation-name property of the (common) owning element.
RefPtr cssAnimationList = aOwningElement->ensureKeyframeEffectStack().cssAnimationList();
ASSERT(cssAnimationList);
ASSERT(!cssAnimationList->isEmpty());
Ref aBackingAnimation = a.backingAnimation();
Ref bBackingAnimation = b.backingAnimation();
for (auto& animation : *cssAnimationList) {
if (animation.ptr() == aBackingAnimation.ptr())
return true;
if (animation.ptr() == bBackingAnimation.ptr())
return false;
}
// We should have found either of those CSS animations in the CSS animations list.
RELEASE_ASSERT_NOT_REACHED();
}
bool compareAnimationsByCompositeOrder(const WebAnimation& a, const WebAnimation& b)
{
// We should not ever be calling this function with two WebAnimation objects that are the same. If that were the case,
// then comparing objects of this kind would yield inconsistent results when comparing A == B and B == A. As such,
// this function should be called with std::stable_sort().
RELEASE_ASSERT_WITH_SECURITY_IMPLICATION(&a != &b);
auto* aAsStyleOriginatedAnimation = dynamicDowncast<StyleOriginatedAnimation>(a);
bool aHasOwningElement = aAsStyleOriginatedAnimation && aAsStyleOriginatedAnimation->owningElement();
auto* bAsStyleOriginatedAnimation = dynamicDowncast<StyleOriginatedAnimation>(b);
bool bHasOwningElement = bAsStyleOriginatedAnimation && bAsStyleOriginatedAnimation->owningElement();
// CSS Transitions sort first.
auto* aAsCSSTransition = aHasOwningElement ? dynamicDowncast<CSSTransition>(a) : nullptr;
auto* bAsCSSTransition = bHasOwningElement ? dynamicDowncast<CSSTransition>(b) : nullptr;
if (aAsCSSTransition || bAsCSSTransition) {
if (!!aAsCSSTransition == !!bAsCSSTransition)
return compareCSSTransitions(*aAsCSSTransition, *bAsCSSTransition);
return !bAsCSSTransition;
}
// CSS Animations sort next.
auto* aAsCSSAnimation = aHasOwningElement ? dynamicDowncast<CSSAnimation>(a) : nullptr;
auto* bAsCSSAnimation = bHasOwningElement ? dynamicDowncast<CSSAnimation>(b) : nullptr;
if (aAsCSSAnimation || bAsCSSAnimation) {
if (!!aAsCSSAnimation == !!bAsCSSAnimation)
return compareCSSAnimations(*aAsCSSAnimation, *bAsCSSAnimation);
return !bAsCSSAnimation;
}
// JS-originated animations sort last based on their position in the global animation list.
// https://drafts.csswg.org/web-animations-1/#animation-composite-order
RELEASE_ASSERT(a.globalPosition() != b.globalPosition());
return a.globalPosition() < b.globalPosition();
}
template <typename T>
static std::optional<bool> compareStyleOriginatedAnimationEvents(const AnimationEventBase& a, const AnimationEventBase& b)
{
auto* aAsDStyleOriginatedAnimationEventAnimationEvent = dynamicDowncast<T>(a);
auto* bAsDStyleOriginatedAnimationEventAnimationEvent = dynamicDowncast<T>(b);
if (!aAsDStyleOriginatedAnimationEventAnimationEvent && !bAsDStyleOriginatedAnimationEventAnimationEvent)
return std::nullopt;
if (!!aAsDStyleOriginatedAnimationEventAnimationEvent != !!bAsDStyleOriginatedAnimationEventAnimationEvent)
return !bAsDStyleOriginatedAnimationEventAnimationEvent;
auto aScheduledTime = a.scheduledTime();
auto bScheduledTime = b.scheduledTime();
if (aScheduledTime != bScheduledTime)
return aScheduledTime < bScheduledTime;
RefPtr aTarget = a.target();
RefPtr bTarget = b.target();
if (aTarget == bTarget)
return false;
auto aStyleable = Styleable(*downcast<Element>(aTarget), aAsDStyleOriginatedAnimationEventAnimationEvent->pseudoElementIdentifier());
auto bStyleable = Styleable(*downcast<Element>(bTarget), bAsDStyleOriginatedAnimationEventAnimationEvent->pseudoElementIdentifier());
return compareStyleOriginatedAnimationOwningElementPositionsInDocumentTreeOrder(aStyleable, bStyleable);
}
bool compareAnimationEventsByCompositeOrder(const AnimationEventBase& a, const AnimationEventBase& b)
{
// AnimationPlaybackEvent instances sort first.
bool aIsPlaybackEvent = is<AnimationPlaybackEvent>(a);
bool bIsPlaybackEvent = is<AnimationPlaybackEvent>(b);
if (aIsPlaybackEvent || bIsPlaybackEvent) {
if (aIsPlaybackEvent != bIsPlaybackEvent)
return !bIsPlaybackEvent;
if (a.animation() == b.animation())
return false;
// https://drafts.csswg.org/web-animations-1/#update-animations-and-send-events
// 1. Sort the events by their scheduled event time such that events that were scheduled to occur earlier, sort before
// events scheduled to occur later and events whose scheduled event time is unresolved sort before events with a
// resolved scheduled event time.
auto aScheduledTime = a.scheduledTime();
auto bScheduledTime = b.scheduledTime();
if (aScheduledTime != bScheduledTime) {
if (aScheduledTime && bScheduledTime)
return *aScheduledTime < *bScheduledTime;
return !bScheduledTime;
}
// 2. Within events with equal scheduled event times, sort by their composite order.
// CSS Transitions sort first.
bool aIsCSSTransition = is<CSSTransition>(a.animation());
bool bIsCSSTransition = is<CSSTransition>(b.animation());
if (aIsCSSTransition || bIsCSSTransition) {
if (aIsCSSTransition == bIsCSSTransition)
return false;
return !bIsCSSTransition;
}
// CSS Animations sort next.
bool aIsCSSAnimation = is<CSSAnimation>(a.animation());
bool bIsCSSAnimation = is<CSSAnimation>(b.animation());
if (aIsCSSAnimation || bIsCSSAnimation) {
if (aIsCSSAnimation == bIsCSSAnimation)
return false;
return !bIsCSSAnimation;
}
// JS-originated animations sort last based on their position in the global animation list.
RefPtr aAnimation = a.animation();
RefPtr bAnimation = b.animation();
if (aAnimation == bAnimation)
return false;
RELEASE_ASSERT(aAnimation);
RELEASE_ASSERT(bAnimation);
RELEASE_ASSERT(aAnimation->globalPosition() != bAnimation->globalPosition());
return aAnimation->globalPosition() < bAnimation->globalPosition();
}
// CSSTransitionEvent instances sort next.
if (auto sorted = compareStyleOriginatedAnimationEvents<CSSTransitionEvent>(a, b))
return *sorted;
// CSSAnimationEvent instances sort last.
if (auto sorted = compareStyleOriginatedAnimationEvents<CSSAnimationEvent>(a, b))
return *sorted;
return false;
}
// FIXME: This should be owned by CSSSelector.
// FIXME: Generate this function.
String pseudoElementIdentifierAsString(const std::optional<Style::PseudoElementIdentifier>& pseudoElementIdentifier)
{
if (!pseudoElementIdentifier)
return emptyString();
static NeverDestroyed<const String> after(MAKE_STATIC_STRING_IMPL("::after"));
static NeverDestroyed<const String> before(MAKE_STATIC_STRING_IMPL("::before"));
static NeverDestroyed<const String> firstLetter(MAKE_STATIC_STRING_IMPL("::first-letter"));
static NeverDestroyed<const String> firstLine(MAKE_STATIC_STRING_IMPL("::first-line"));
static NeverDestroyed<const String> grammarError(MAKE_STATIC_STRING_IMPL("::grammar-error"));
static NeverDestroyed<const String> marker(MAKE_STATIC_STRING_IMPL("::marker"));
static NeverDestroyed<const String> selection(MAKE_STATIC_STRING_IMPL("::selection"));
static NeverDestroyed<const String> spellingError(MAKE_STATIC_STRING_IMPL("::spelling-error"));
static NeverDestroyed<const String> targetText(MAKE_STATIC_STRING_IMPL("::target-text"));
static NeverDestroyed<const String> viewTransition(MAKE_STATIC_STRING_IMPL("::view-transition"));
static NeverDestroyed<const String> webkitScrollbar(MAKE_STATIC_STRING_IMPL("::-webkit-scrollbar"));
switch (pseudoElementIdentifier->pseudoId) {
case PseudoId::After:
return after;
case PseudoId::Before:
return before;
case PseudoId::FirstLetter:
return firstLetter;
case PseudoId::FirstLine:
return firstLine;
case PseudoId::GrammarError:
return grammarError;
case PseudoId::Highlight:
return makeString("::highlight"_s, '(', pseudoElementIdentifier->nameArgument, ')');
case PseudoId::Marker:
return marker;
case PseudoId::Selection:
return selection;
case PseudoId::SpellingError:
return spellingError;
case PseudoId::TargetText:
return targetText;
case PseudoId::ViewTransition:
return viewTransition;
case PseudoId::ViewTransitionGroup:
return makeString("::view-transition-group"_s, '(', pseudoElementIdentifier->nameArgument, ')');
case PseudoId::ViewTransitionImagePair:
return makeString("::view-transition-image-pair"_s, '(', pseudoElementIdentifier->nameArgument, ')');
case PseudoId::ViewTransitionOld:
return makeString("::view-transition-old"_s, '(', pseudoElementIdentifier->nameArgument, ')');
case PseudoId::ViewTransitionNew:
return makeString("::view-transition-new"_s, '(', pseudoElementIdentifier->nameArgument, ')');
case PseudoId::WebKitScrollbar:
return webkitScrollbar;
default:
return emptyString();
}
}
// bool represents whether parsing was successful, std::optional<Style::PseudoElementIdentifier> is the result of the parsing when successful.
std::pair<bool, std::optional<Style::PseudoElementIdentifier>> pseudoElementIdentifierFromString(const String& pseudoElement, Document* document)
{
// https://drafts.csswg.org/web-animations-1/#dom-keyframeeffect-pseudoelement
if (pseudoElement.isNull())
return { true, std::nullopt };
// FIXME: We should always have a document for accurate settings.
auto parserContext = document ? CSSSelectorParserContext { *document } : CSSSelectorParserContext { CSSParserContext { HTMLStandardMode } };
return CSSSelectorParser::parsePseudoElement(pseudoElement, parserContext);
}
AtomString animatablePropertyAsString(AnimatableCSSProperty property)
{
return WTF::switchOn(property,
[] (CSSPropertyID propertyId) {
return nameString(propertyId);
},
[] (const AtomString& customProperty) {
return customProperty;
}
);
}
} // namespace WebCore