LibWeb: Add pseudoElement parameter to GetAnimationsOptions

This corresponds to: https://github.com/w3c/csswg-drafts/pull/11050

For now, we don't do anything useful with this parameter, because we
don't yet support animating pseudo-elements.
This commit is contained in:
Sam Atkins 2024-11-08 15:41:49 +00:00 committed by Andreas Kling
parent c5c5d97544
commit b0e58054aa
Notes: github-actions[bot] 2024-11-09 14:46:39 +00:00
14 changed files with 141 additions and 78 deletions

View file

@ -16,5 +16,7 @@ source_set("Animations") {
"DocumentTimeline.h",
"KeyframeEffect.cpp",
"KeyframeEffect.h",
"PseudoElementParsing.cpp",
"PseudoElementParsing.h",
]
}

View file

@ -9,6 +9,7 @@
#include <LibWeb/Animations/Animatable.h>
#include <LibWeb/Animations/Animation.h>
#include <LibWeb/Animations/DocumentTimeline.h>
#include <LibWeb/Animations/PseudoElementParsing.h>
#include <LibWeb/CSS/CSSTransition.h>
#include <LibWeb/CSS/StyleValues/EasingStyleValue.h>
#include <LibWeb/CSS/StyleValues/TimeStyleValue.h>
@ -58,43 +59,55 @@ WebIDL::ExceptionOr<JS::NonnullGCPtr<Animation>> Animatable::animate(Optional<JS
return animation;
}
// https://www.w3.org/TR/web-animations-1/#dom-animatable-getanimations
Vector<JS::NonnullGCPtr<Animation>> Animatable::get_animations(GetAnimationsOptions options)
// https://drafts.csswg.org/web-animations-1/#dom-animatable-getanimations
WebIDL::ExceptionOr<Vector<JS::NonnullGCPtr<Animation>>> Animatable::get_animations(Optional<GetAnimationsOptions> options)
{
verify_cast<DOM::Element>(*this).document().update_style();
return get_animations_internal(options);
}
Vector<JS::NonnullGCPtr<Animation>> Animatable::get_animations_internal(GetAnimationsOptions options)
WebIDL::ExceptionOr<Vector<JS::NonnullGCPtr<Animation>>> Animatable::get_animations_internal(Optional<GetAnimationsOptions> options)
{
// Returns the set of relevant animations for this object, or, if an options parameter is passed with subtree set to
// true, returns the set of relevant animations for a subtree for this object.
// 1. Let object be the object on which this method was called.
// The returned list is sorted using the composite order described for the associated animations of effects in
// §5.4.2 The effect stack.
if (!m_is_sorted_by_composite_order) {
quick_sort(m_associated_animations, [](JS::NonnullGCPtr<Animation>& a, JS::NonnullGCPtr<Animation>& b) {
auto& a_effect = verify_cast<KeyframeEffect>(*a->effect());
auto& b_effect = verify_cast<KeyframeEffect>(*b->effect());
return KeyframeEffect::composite_order(a_effect, b_effect) < 0;
});
m_is_sorted_by_composite_order = true;
// 2. Let pseudoElement be the result of pseudo-element parsing applied to pseudoElement of options, or null if options is not passed.
// FIXME: Currently only DOM::Element includes Animatable, but that might not always be true.
Optional<CSS::Selector::PseudoElement> pseudo_element;
if (options.has_value() && options->pseudo_element.has_value()) {
auto& realm = static_cast<DOM::Element&>(*this).realm();
pseudo_element = TRY(pseudo_element_parsing(realm, options->pseudo_element));
}
// 3. If pseudoElement is not null, then let target be the pseudo-element identified by pseudoElement with object as the originating element.
// Otherwise, let target be object.
// FIXME: We can't refer to pseudo-elements directly, and they also can't be animated yet.
(void)pseudo_element;
JS::NonnullGCPtr target { *static_cast<DOM::Element*>(this) };
// 4. If options is passed with subtree set to true, then return the set of relevant animations for a subtree of target.
// Otherwise, return the set of relevant animations for target.
Vector<JS::NonnullGCPtr<Animation>> relevant_animations;
for (auto const& animation : m_associated_animations) {
if (animation->is_relevant())
relevant_animations.append(*animation);
}
if (options.subtree) {
JS::NonnullGCPtr target { *static_cast<DOM::Element*>(this) };
target->for_each_child_of_type<DOM::Element>([&](auto& child) {
relevant_animations.extend(child.get_animations(options));
if (options.has_value() && options->subtree) {
Optional<WebIDL::Exception> exception;
TRY(target->for_each_child_of_type_fallible<DOM::Element>([&](auto& child) -> WebIDL::ExceptionOr<IterationDecision> {
relevant_animations.extend(TRY(child.get_animations(options)));
return IterationDecision::Continue;
});
}));
}
// The returned list is sorted using the composite order described for the associated animations of effects in
// §5.4.2 The effect stack.
quick_sort(relevant_animations, [](JS::NonnullGCPtr<Animation>& a, JS::NonnullGCPtr<Animation>& b) {
auto& a_effect = verify_cast<KeyframeEffect>(*a->effect());
auto& b_effect = verify_cast<KeyframeEffect>(*b->effect());
return KeyframeEffect::composite_order(a_effect, b_effect) < 0;
});
return relevant_animations;
}

View file

@ -17,18 +17,19 @@ class CSSTransition;
namespace Web::Animations {
// https://www.w3.org/TR/web-animations-1/#dictdef-keyframeanimationoptions
// https://drafts.csswg.org/web-animations-1/#dictdef-keyframeanimationoptions
struct KeyframeAnimationOptions : public KeyframeEffectOptions {
FlyString id { ""_fly_string };
Optional<JS::GCPtr<AnimationTimeline>> timeline;
};
// https://www.w3.org/TR/web-animations-1/#dictdef-getanimationsoptions
// https://drafts.csswg.org/web-animations-1/#dictdef-getanimationsoptions
struct GetAnimationsOptions {
bool subtree { false };
Optional<String> pseudo_element {};
};
// https://www.w3.org/TR/web-animations-1/#animatable
// https://drafts.csswg.org/web-animations-1/#animatable
class Animatable {
public:
struct TransitionAttributes {
@ -40,8 +41,8 @@ public:
virtual ~Animatable() = default;
WebIDL::ExceptionOr<JS::NonnullGCPtr<Animation>> animate(Optional<JS::Handle<JS::Object>> keyframes, Variant<Empty, double, KeyframeAnimationOptions> options = {});
Vector<JS::NonnullGCPtr<Animation>> get_animations(GetAnimationsOptions options = {});
Vector<JS::NonnullGCPtr<Animation>> get_animations_internal(GetAnimationsOptions options = {});
WebIDL::ExceptionOr<Vector<JS::NonnullGCPtr<Animation>>> get_animations(Optional<GetAnimationsOptions> options = {});
WebIDL::ExceptionOr<Vector<JS::NonnullGCPtr<Animation>>> get_animations_internal(Optional<GetAnimationsOptions> options = {});
void associate_with_animation(JS::NonnullGCPtr<Animation>);
void disassociate_with_animation(JS::NonnullGCPtr<Animation>);

View file

@ -1,19 +1,20 @@
#import <Animations/Animation.idl>
#import <Animations/KeyframeEffect.idl>
// https://www.w3.org/TR/web-animations-1/#the-animatable-interface-mixin
// https://drafts.csswg.org/web-animations-1/#the-animatable-interface-mixin
interface mixin Animatable {
Animation animate(object? keyframes, optional (unrestricted double or KeyframeAnimationOptions) options = {});
sequence<Animation> getAnimations(optional GetAnimationsOptions options = {});
};
// https://www.w3.org/TR/web-animations-1/#dictdef-keyframeanimationoptions
// https://drafts.csswg.org/web-animations-1/#dictdef-keyframeanimationoptions
dictionary KeyframeAnimationOptions : KeyframeEffectOptions {
DOMString id = "";
AnimationTimeline? timeline;
};
// https://www.w3.org/TR/web-animations-1/#dictdef-getanimationsoptions
// https://drafts.csswg.org/web-animations-1/#dictdef-getanimationsoptions
dictionary GetAnimationsOptions {
boolean subtree = false;
CSSOMString? pseudoElement = null;
};

View file

@ -8,6 +8,7 @@
#include <LibJS/Runtime/Iterator.h>
#include <LibWeb/Animations/Animation.h>
#include <LibWeb/Animations/KeyframeEffect.h>
#include <LibWeb/Animations/PseudoElementParsing.h>
#include <LibWeb/Bindings/KeyframeEffectPrototype.h>
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/CSS/StyleComputer.h>
@ -783,42 +784,13 @@ Optional<String> KeyframeEffect::pseudo_element() const
return MUST(String::formatted("::{}", m_target_pseudo_selector->name()));
}
// https://www.w3.org/TR/web-animations-1/#dom-keyframeeffect-pseudoelement
WebIDL::ExceptionOr<void> KeyframeEffect::set_pseudo_element(Optional<String> pseudo_element)
// https://drafts.csswg.org/web-animations-1/#dom-keyframeeffect-pseudoelement
WebIDL::ExceptionOr<void> KeyframeEffect::set_pseudo_element(Optional<String> value)
{
auto& realm = this->realm();
// On setting, sets the target pseudo-selector of the animation effect to the provided value after applying the
// following exceptions:
// FIXME:
// - If one of the legacy Selectors Level 2 single-colon selectors (':before', ':after', ':first-letter', or
// ':first-line') is specified, the target pseudo-selector must be set to the equivalent two-colon selector
// (e.g. '::before').
if (pseudo_element.has_value()) {
auto value = pseudo_element.value();
if (value == ":before" || value == ":after" || value == ":first-letter" || value == ":first-line") {
m_target_pseudo_selector = CSS::Selector::PseudoElement::from_string(MUST(value.substring_from_byte_offset(1)));
return {};
}
}
// - If the provided value is not null and is an invalid <pseudo-element-selector>, the user agent must throw a
// DOMException with error name SyntaxError and leave the target pseudo-selector of this animation effect
// unchanged.
if (pseudo_element.has_value()) {
if (pseudo_element->starts_with_bytes("::"sv)) {
if (auto value = CSS::Selector::PseudoElement::from_string(MUST(pseudo_element->substring_from_byte_offset(2))); value.has_value()) {
m_target_pseudo_selector = value;
return {};
}
}
return WebIDL::SyntaxError::create(realm, MUST(String::formatted("Invalid pseudo-element selector: \"{}\"", pseudo_element.value())));
}
m_target_pseudo_selector = {};
// On setting, sets the target pseudo-selector of the animation effect to the result of
// pseudo-element parsing on the provided value, defined as the following:
// NOTE: The actual definition is in pseudo_element_parsing().
m_target_pseudo_selector = TRY(pseudo_element_parsing(realm(), value));
return {};
}

View file

@ -0,0 +1,38 @@
/*
* Copyright (c) 2024, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "PseudoElementParsing.h"
#include <LibWeb/CSS/Parser/Parser.h>
namespace Web::Animations {
// https://drafts.csswg.org/web-animations-1/#dom-keyframeeffect-pseudo-element-parsing
WebIDL::ExceptionOr<Optional<CSS::Selector::PseudoElement>> pseudo_element_parsing(JS::Realm& realm, Optional<String> const& value)
{
// 1. Given the value value, perform the following steps:
// 2. If value is not null and is an invalid <pseudo-element-selector>,
Optional<CSS::Selector::PseudoElement> pseudo_element;
if (value.has_value()) {
pseudo_element = parse_pseudo_element_selector(CSS::Parser::ParsingContext { realm }, *value);
if (!pseudo_element.has_value()) {
// 1. Throw a DOMException with error name "SyntaxError".
// 2. Abort.
return WebIDL::SyntaxError::create(realm, MUST(String::formatted("Invalid pseudo-element selector: \"{}\"", value.value())));
}
}
// 3. If value is one of the legacy Selectors Level 2 single-colon selectors (':before', ':after', ':first-letter', or ':first-line'),
// then return the equivalent two-colon selector (e.g. '::before').
if (value.has_value() && value->is_one_of(":before", ":after", ":first-letter", ":first-line")) {
return CSS::Selector::PseudoElement::from_string(MUST(value->substring_from_byte_offset(1)));
}
// 4. Otherwise, return value.
return pseudo_element;
}
}

View file

@ -0,0 +1,19 @@
/*
* Copyright (c) 2024, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Optional.h>
#include <AK/String.h>
#include <LibWeb/CSS/Selector.h>
#include <LibWeb/WebIDL/ExceptionOr.h>
namespace Web::Animations {
// https://drafts.csswg.org/web-animations-1/#dom-keyframeeffect-pseudo-element-parsing
WebIDL::ExceptionOr<Optional<CSS::Selector::PseudoElement>> pseudo_element_parsing(JS::Realm&, Optional<String> const&);
}

View file

@ -10,6 +10,7 @@ set(SOURCES
Animations/AnimationTimeline.cpp
Animations/DocumentTimeline.cpp
Animations/KeyframeEffect.cpp
Animations/PseudoElementParsing.cpp
ARIA/AriaData.cpp
ARIA/ARIAMixin.cpp
ARIA/Roles.cpp

View file

@ -1579,12 +1579,16 @@ void StyleComputer::compute_cascaded_values(StyleProperties& style, DOM::Element
}
}
auto animations = element.get_animations_internal({ .subtree = false });
for (auto& animation : animations) {
if (auto effect = animation->effect(); effect && effect->is_keyframe_effect()) {
auto& keyframe_effect = *static_cast<Animations::KeyframeEffect*>(effect.ptr());
if (keyframe_effect.pseudo_element_type() == pseudo_element)
collect_animation_into(element, pseudo_element, keyframe_effect, style);
auto animations = element.get_animations_internal(Animations::GetAnimationsOptions { .subtree = false });
if (animations.is_exception()) {
dbgln("Error getting animations for element {}", element.debug_description());
} else {
for (auto& animation : animations.value()) {
if (auto effect = animation->effect(); effect && effect->is_keyframe_effect()) {
auto& keyframe_effect = *static_cast<Animations::KeyframeEffect*>(effect.ptr());
if (keyframe_effect.pseudo_element_type() == pseudo_element)
collect_animation_into(element, pseudo_element, keyframe_effect, style);
}
}
}

View file

@ -4695,13 +4695,13 @@ void Document::remove_replaced_animations()
}
}
Vector<JS::NonnullGCPtr<Animations::Animation>> Document::get_animations()
WebIDL::ExceptionOr<Vector<JS::NonnullGCPtr<Animations::Animation>>> Document::get_animations()
{
Vector<JS::NonnullGCPtr<Animations::Animation>> relevant_animations;
for_each_child_of_type<Element>([&](auto& child) {
relevant_animations.extend(child.get_animations({ .subtree = true }));
TRY(for_each_child_of_type_fallible<Element>([&](auto& child) -> WebIDL::ExceptionOr<IterationDecision> {
relevant_animations.extend(TRY(child.get_animations(Animations::GetAnimationsOptions { .subtree = true })));
return IterationDecision::Continue;
});
}));
return relevant_animations;
}

View file

@ -636,7 +636,7 @@ public:
void update_animations_and_send_events(Optional<double> const& timestamp);
void remove_replaced_animations();
Vector<JS::NonnullGCPtr<Animations::Animation>> get_animations();
WebIDL::ExceptionOr<Vector<JS::NonnullGCPtr<Animations::Animation>>> get_animations();
bool ready_to_run_scripts() const { return m_ready_to_run_scripts; }
void set_ready_to_run_scripts() { m_ready_to_run_scripts = true; }

View file

@ -637,6 +637,18 @@ public:
return const_cast<Node*>(this)->template for_each_child_of_type<U>(move(callback));
}
template<typename U, typename Callback>
WebIDL::ExceptionOr<void> for_each_child_of_type_fallible(Callback callback)
{
for (auto* node = first_child(); node; node = node->next_sibling()) {
if (is<U>(node)) {
if (TRY(callback(verify_cast<U>(*node))) == IterationDecision::Break)
return {};
}
}
return {};
}
template<typename U>
U const* next_sibling_of_type() const
{

View file

@ -174,13 +174,13 @@ void ShadowRoot::for_each_css_style_sheet(Function<void(CSS::CSSStyleSheet&)>&&
}
}
Vector<JS::NonnullGCPtr<Animations::Animation>> ShadowRoot::get_animations()
WebIDL::ExceptionOr<Vector<JS::NonnullGCPtr<Animations::Animation>>> ShadowRoot::get_animations()
{
Vector<JS::NonnullGCPtr<Animations::Animation>> relevant_animations;
for_each_child_of_type<Element>([&](auto& child) {
relevant_animations.extend(child.get_animations({ .subtree = true }));
TRY(for_each_child_of_type_fallible<Element>([&](auto& child) -> WebIDL::ExceptionOr<IterationDecision> {
relevant_animations.extend(TRY(child.get_animations(Animations::GetAnimationsOptions { .subtree = true })));
return IterationDecision::Continue;
});
}));
return relevant_animations;
}

View file

@ -60,7 +60,7 @@ public:
void for_each_css_style_sheet(Function<void(CSS::CSSStyleSheet&)>&& callback) const;
Vector<JS::NonnullGCPtr<Animations::Animation>> get_animations();
WebIDL::ExceptionOr<Vector<JS::NonnullGCPtr<Animations::Animation>>> get_animations();
virtual void finalize() override;