Skip to content

Commit

Permalink
LibWeb: Add pseudoElement parameter to GetAnimationsOptions
Browse files Browse the repository at this point in the history
This corresponds to: w3c/csswg-drafts#11050

For now, we don't do anything with this parameter, because we don't yet
support animating pseudo-elements.
  • Loading branch information
AtkinsSJ committed Nov 8, 2024
1 parent 635d484 commit 6029b07
Show file tree
Hide file tree
Showing 14 changed files with 141 additions and 78 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,7 @@ source_set("Animations") {
"DocumentTimeline.h",
"KeyframeEffect.cpp",
"KeyframeEffect.h",
"PseudoElementParsing.cpp",
"PseudoElementParsing.h",
]
}
51 changes: 32 additions & 19 deletions Userland/Libraries/LibWeb/Animations/Animatable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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;
}

Expand Down
11 changes: 6 additions & 5 deletions Userland/Libraries/LibWeb/Animations/Animatable.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -40,8 +41,8 @@ class Animatable {
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>);
Expand Down
7 changes: 4 additions & 3 deletions Userland/Libraries/LibWeb/Animations/Animatable.idl
Original file line number Diff line number Diff line change
@@ -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;
};
42 changes: 7 additions & 35 deletions Userland/Libraries/LibWeb/Animations/KeyframeEffect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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 {};
}

Expand Down
38 changes: 38 additions & 0 deletions Userland/Libraries/LibWeb/Animations/PseudoElementParsing.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright (c) 2024, Sam Atkins <[email protected]>
*
* 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;
}

}
19 changes: 19 additions & 0 deletions Userland/Libraries/LibWeb/Animations/PseudoElementParsing.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright (c) 2024, Sam Atkins <[email protected]>
*
* 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&);

}
1 change: 1 addition & 0 deletions Userland/Libraries/LibWeb/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 10 additions & 6 deletions Userland/Libraries/LibWeb/CSS/StyleComputer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}

Expand Down
8 changes: 4 additions & 4 deletions Userland/Libraries/LibWeb/DOM/Document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
2 changes: 1 addition & 1 deletion Userland/Libraries/LibWeb/DOM/Document.h
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,7 @@ class Document
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; }
Expand Down
12 changes: 12 additions & 0 deletions Userland/Libraries/LibWeb/DOM/Node.h
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,18 @@ class Node : public EventTarget {
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
{
Expand Down
8 changes: 4 additions & 4 deletions Userland/Libraries/LibWeb/DOM/ShadowRoot.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
2 changes: 1 addition & 1 deletion Userland/Libraries/LibWeb/DOM/ShadowRoot.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class ShadowRoot final : public DocumentFragment {

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;

Expand Down

0 comments on commit 6029b07

Please sign in to comment.