LibWeb: Implement CSS transitions support for pseudo elements

We already had all necessary things for pseudo elements support in place
except ability to save transition properties in Animatable. This commit
adds the missing part.
This commit is contained in:
Aliaksandr Kalenik 2025-05-28 22:12:13 +02:00 committed by Alexander Kalenik
commit 3178679f0b
Notes: github-actions[bot] 2025-05-30 13:49:19 +00:00
5 changed files with 136 additions and 93 deletions

View file

@ -1,6 +1,7 @@
/*
* Copyright (c) 2024, Matthew Olsson <mattco@serenityos.org>
* Copyright (c) 2024, Sam Atkins <sam@ladybird.org>
* Copyright (c) 2025, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -19,6 +20,15 @@
namespace Web::Animations {
struct Animatable::Transition {
HashMap<CSS::PropertyID, size_t> transition_attribute_indices;
Vector<TransitionAttributes> transition_attributes;
GC::Ptr<CSS::CSSStyleDeclaration const> cached_transition_property_source;
HashMap<CSS::PropertyID, GC::Ref<CSS::CSSTransition>> associated_transitions;
};
Animatable::Impl::~Impl() = default;
// https://www.w3.org/TR/web-animations-1/#dom-animatable-animate
WebIDL::ExceptionOr<GC::Ref<Animation>> Animatable::animate(Optional<GC::Root<JS::Object>> keyframes, Variant<Empty, double, KeyframeAnimationOptions> options)
{
@ -128,86 +138,100 @@ void Animatable::disassociate_with_animation(GC::Ref<Animation> animation)
impl.associated_animations.remove_first_matching([&](auto element) { return animation == element; });
}
void Animatable::add_transitioned_properties(Vector<Vector<CSS::PropertyID>> properties, CSS::StyleValueVector delays, CSS::StyleValueVector durations, CSS::StyleValueVector timing_functions, CSS::StyleValueVector transition_behaviors)
void Animatable::add_transitioned_properties(Optional<CSS::PseudoElement> pseudo_element, Vector<Vector<CSS::PropertyID>> properties, CSS::StyleValueVector delays, CSS::StyleValueVector durations, CSS::StyleValueVector timing_functions, CSS::StyleValueVector transition_behaviors)
{
auto& impl = ensure_impl();
VERIFY(properties.size() == delays.size());
VERIFY(properties.size() == durations.size());
VERIFY(properties.size() == timing_functions.size());
VERIFY(properties.size() == transition_behaviors.size());
auto* maybe_transition = ensure_transition(pseudo_element);
if (!maybe_transition)
return;
auto& transition = *maybe_transition;
for (size_t i = 0; i < properties.size(); i++) {
size_t index_of_this_transition = impl.transition_attributes.size();
size_t index_of_this_transition = transition.transition_attributes.size();
auto delay = delays[i]->is_time() ? delays[i]->as_time().time().to_milliseconds() : 0;
auto duration = durations[i]->is_time() ? durations[i]->as_time().time().to_milliseconds() : 0;
auto timing_function = timing_functions[i]->is_easing() ? timing_functions[i]->as_easing().function() : CSS::EasingStyleValue::CubicBezier::ease();
auto transition_behavior = CSS::keyword_to_transition_behavior(transition_behaviors[i]->to_keyword()).value_or(CSS::TransitionBehavior::Normal);
VERIFY(timing_functions[i]->is_easing());
impl.transition_attributes.empend(delay, duration, timing_function, transition_behavior);
transition.transition_attributes.empend(delay, duration, timing_function, transition_behavior);
for (auto const& property : properties[i])
impl.transition_attribute_indices.set(property, index_of_this_transition);
transition.transition_attribute_indices.set(property, index_of_this_transition);
}
}
Optional<Animatable::TransitionAttributes const&> Animatable::property_transition_attributes(CSS::PropertyID property) const
Optional<Animatable::TransitionAttributes const&> Animatable::property_transition_attributes(Optional<CSS::PseudoElement> pseudo_element, CSS::PropertyID property) const
{
if (!m_impl)
auto* maybe_transition = ensure_transition(pseudo_element);
if (!maybe_transition)
return {};
auto& impl = *m_impl;
if (auto maybe_index = impl.transition_attribute_indices.get(property); maybe_index.has_value())
return impl.transition_attributes[maybe_index.value()];
auto& transition = *maybe_transition;
if (auto maybe_attr_index = transition.transition_attribute_indices.get(property); maybe_attr_index.has_value())
return transition.transition_attributes[maybe_attr_index.value()];
return {};
}
GC::Ptr<CSS::CSSTransition> Animatable::property_transition(CSS::PropertyID property) const
GC::Ptr<CSS::CSSTransition> Animatable::property_transition(Optional<CSS::PseudoElement> pseudo_element, CSS::PropertyID property) const
{
if (!m_impl)
auto* maybe_transition = ensure_transition(pseudo_element);
if (!maybe_transition)
return {};
auto& impl = *m_impl;
if (auto maybe_animation = impl.associated_transitions.get(property); maybe_animation.has_value())
auto& transition = *maybe_transition;
if (auto maybe_animation = transition.associated_transitions.get(property); maybe_animation.has_value())
return maybe_animation.value();
return {};
}
void Animatable::set_transition(CSS::PropertyID property, GC::Ref<CSS::CSSTransition> animation)
void Animatable::set_transition(Optional<CSS::PseudoElement> pseudo_element, CSS::PropertyID property, GC::Ref<CSS::CSSTransition> animation)
{
auto& impl = ensure_impl();
VERIFY(!impl.associated_transitions.contains(property));
impl.associated_transitions.set(property, animation);
}
void Animatable::remove_transition(CSS::PropertyID property_id)
{
auto& impl = *m_impl;
VERIFY(impl.associated_transitions.contains(property_id));
impl.associated_transitions.remove(property_id);
}
void Animatable::clear_transitions()
{
if (!m_impl)
auto maybe_transition = ensure_transition(pseudo_element);
if (!maybe_transition)
return;
auto& impl = *m_impl;
impl.associated_transitions.clear();
impl.transition_attribute_indices.clear();
impl.transition_attributes.clear();
auto& transition = *maybe_transition;
VERIFY(!transition.associated_transitions.contains(property));
transition.associated_transitions.set(property, animation);
}
void Animatable::remove_transition(Optional<CSS::PseudoElement> pseudo_element, CSS::PropertyID property_id)
{
auto maybe_transition = ensure_transition(pseudo_element);
if (!maybe_transition)
return;
auto& transition = *maybe_transition;
VERIFY(transition.associated_transitions.contains(property_id));
transition.associated_transitions.remove(property_id);
}
void Animatable::clear_transitions(Optional<CSS::PseudoElement> pseudo_element)
{
auto maybe_transition = ensure_transition(pseudo_element);
if (!maybe_transition)
return;
auto& transition = *maybe_transition;
transition.associated_transitions.clear();
transition.transition_attribute_indices.clear();
transition.transition_attributes.clear();
}
void Animatable::visit_edges(JS::Cell::Visitor& visitor)
{
if (!m_impl)
return;
auto& impl = *m_impl;
auto& impl = ensure_impl();
visitor.visit(impl.associated_animations);
for (auto const& cached_animation_source : impl.cached_animation_name_source)
visitor.visit(cached_animation_source);
for (auto const& cached_animation_name : impl.cached_animation_name_animation)
visitor.visit(cached_animation_name);
visitor.visit(impl.cached_transition_property_source);
visitor.visit(impl.associated_transitions);
for (auto const& transition : impl.transitions) {
if (transition) {
visitor.visit(transition->cached_transition_property_source);
visitor.visit(transition->associated_transitions);
}
}
}
GC::Ptr<CSS::CSSStyleDeclaration const> Animatable::cached_animation_name_source(Optional<CSS::PseudoElement> pseudo_element) const
@ -267,24 +291,43 @@ void Animatable::set_cached_animation_name_animation(GC::Ptr<Animations::Animati
}
}
GC::Ptr<CSS::CSSStyleDeclaration const> Animatable::cached_transition_property_source() const
GC::Ptr<CSS::CSSStyleDeclaration const> Animatable::cached_transition_property_source(Optional<CSS::PseudoElement> pseudo_element) const
{
if (!m_impl)
auto* maybe_transition = ensure_transition(pseudo_element);
if (!maybe_transition)
return {};
return m_impl->cached_transition_property_source;
return maybe_transition->cached_transition_property_source;
}
void Animatable::set_cached_transition_property_source(GC::Ptr<CSS::CSSStyleDeclaration const> value)
void Animatable::set_cached_transition_property_source(Optional<CSS::PseudoElement> pseudo_element, GC::Ptr<CSS::CSSStyleDeclaration const> value)
{
ensure_impl();
m_impl->cached_transition_property_source = value;
auto* maybe_transition = ensure_transition(pseudo_element);
if (!maybe_transition)
return;
maybe_transition->cached_transition_property_source = value;
}
Animatable::Impl& Animatable::ensure_impl()
Animatable::Impl& Animatable::ensure_impl() const
{
if (!m_impl)
m_impl = make<Impl>();
return *m_impl;
}
Animatable::Transition* Animatable::ensure_transition(Optional<CSS::PseudoElement> pseudo_element) const
{
auto& impl = ensure_impl();
size_t pseudo_element_index = 0;
if (pseudo_element.has_value()) {
if (!CSS::Selector::PseudoElementSelector::is_known_pseudo_element_type(pseudo_element.value()))
return nullptr;
pseudo_element_index = to_underlying(pseudo_element.value()) + 1;
}
if (!impl.transitions[pseudo_element_index])
impl.transitions[pseudo_element_index] = make<Transition>();
return impl.transitions[pseudo_element_index];
}
}

View file

@ -1,6 +1,7 @@
/*
* Copyright (c) 2024, Matthew Olsson <mattco@serenityos.org>
* Copyright (c) 2024, Sam Atkins <sam@ladybird.org>
* Copyright (c) 2025, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -56,20 +57,22 @@ public:
GC::Ptr<Animations::Animation> cached_animation_name_animation(Optional<CSS::PseudoElement>) const;
void set_cached_animation_name_animation(GC::Ptr<Animations::Animation> value, Optional<CSS::PseudoElement>);
GC::Ptr<CSS::CSSStyleDeclaration const> cached_transition_property_source() const;
void set_cached_transition_property_source(GC::Ptr<CSS::CSSStyleDeclaration const> value);
GC::Ptr<CSS::CSSStyleDeclaration const> cached_transition_property_source(Optional<CSS::PseudoElement>) const;
void set_cached_transition_property_source(Optional<CSS::PseudoElement>, GC::Ptr<CSS::CSSStyleDeclaration const> value);
void add_transitioned_properties(Vector<Vector<CSS::PropertyID>> properties, CSS::StyleValueVector delays, CSS::StyleValueVector durations, CSS::StyleValueVector timing_functions, CSS::StyleValueVector transition_behaviors);
Optional<TransitionAttributes const&> property_transition_attributes(CSS::PropertyID) const;
void set_transition(CSS::PropertyID, GC::Ref<CSS::CSSTransition>);
void remove_transition(CSS::PropertyID);
GC::Ptr<CSS::CSSTransition> property_transition(CSS::PropertyID) const;
void clear_transitions();
void add_transitioned_properties(Optional<CSS::PseudoElement>, Vector<Vector<CSS::PropertyID>> properties, CSS::StyleValueVector delays, CSS::StyleValueVector durations, CSS::StyleValueVector timing_functions, CSS::StyleValueVector transition_behaviors);
Optional<TransitionAttributes const&> property_transition_attributes(Optional<CSS::PseudoElement>, CSS::PropertyID) const;
void set_transition(Optional<CSS::PseudoElement>, CSS::PropertyID, GC::Ref<CSS::CSSTransition>);
void remove_transition(Optional<CSS::PseudoElement>, CSS::PropertyID);
GC::Ptr<CSS::CSSTransition> property_transition(Optional<CSS::PseudoElement>, CSS::PropertyID) const;
void clear_transitions(Optional<CSS::PseudoElement>);
protected:
void visit_edges(JS::Cell::Visitor&);
private:
struct Transition;
struct Impl {
Vector<GC::Ref<Animation>> associated_animations;
bool is_sorted_by_composite_order { true };
@ -77,14 +80,14 @@ private:
Array<GC::Ptr<CSS::CSSStyleDeclaration const>, to_underlying(CSS::PseudoElement::KnownPseudoElementCount) + 1> cached_animation_name_source;
Array<GC::Ptr<Animation>, to_underlying(CSS::PseudoElement::KnownPseudoElementCount) + 1> cached_animation_name_animation;
HashMap<CSS::PropertyID, size_t> transition_attribute_indices;
Vector<TransitionAttributes> transition_attributes;
GC::Ptr<CSS::CSSStyleDeclaration const> cached_transition_property_source;
HashMap<CSS::PropertyID, GC::Ref<CSS::CSSTransition>> associated_transitions;
};
Impl& ensure_impl();
mutable Array<OwnPtr<Transition>, to_underlying(CSS::PseudoElement::KnownPseudoElementCount) + 1> transitions;
OwnPtr<Impl> m_impl;
~Impl();
};
Impl& ensure_impl() const;
Transition* ensure_transition(Optional<CSS::PseudoElement>) const;
mutable OwnPtr<Impl> m_impl;
};
}

View file

@ -19,12 +19,12 @@ namespace Web::CSS {
GC_DEFINE_ALLOCATOR(CSSTransition);
GC::Ref<CSSTransition> CSSTransition::start_a_transition(DOM::Element& element, PropertyID property_id, size_t transition_generation,
double start_time, double end_time, NonnullRefPtr<CSSStyleValue const> start_value, NonnullRefPtr<CSSStyleValue const> end_value,
NonnullRefPtr<CSSStyleValue const> reversing_adjusted_start_value, double reversing_shortening_factor)
GC::Ref<CSSTransition> CSSTransition::start_a_transition(DOM::Element& element, Optional<PseudoElement> pseudo_element, PropertyID property_id,
size_t transition_generation, double start_time, double end_time, NonnullRefPtr<CSSStyleValue const> start_value,
NonnullRefPtr<CSSStyleValue const> end_value, NonnullRefPtr<CSSStyleValue const> reversing_adjusted_start_value, double reversing_shortening_factor)
{
auto& realm = element.realm();
return realm.create<CSSTransition>(realm, element, property_id, transition_generation, start_time, end_time, start_value, end_value, reversing_adjusted_start_value, reversing_shortening_factor);
return realm.create<CSSTransition>(realm, element, pseudo_element, property_id, transition_generation, start_time, end_time, start_value, end_value, reversing_adjusted_start_value, reversing_shortening_factor);
}
Animations::AnimationClass CSSTransition::animation_class() const
@ -75,7 +75,7 @@ Optional<int> CSSTransition::class_specific_composite_order(GC::Ref<Animations::
return {};
}
CSSTransition::CSSTransition(JS::Realm& realm, DOM::Element& element, PropertyID property_id, size_t transition_generation,
CSSTransition::CSSTransition(JS::Realm& realm, DOM::Element& element, Optional<PseudoElement> pseudo_element, PropertyID property_id, size_t transition_generation,
double start_time, double end_time, NonnullRefPtr<CSSStyleValue const> start_value, NonnullRefPtr<CSSStyleValue const> end_value,
NonnullRefPtr<CSSStyleValue const> reversing_adjusted_start_value, double reversing_shortening_factor)
: Animations::Animation(realm)
@ -99,9 +99,11 @@ CSSTransition::CSSTransition(JS::Realm& realm, DOM::Element& element, PropertyID
// Construct a KeyframesEffect for our animation
m_keyframe_effect->set_target(&element);
if (pseudo_element.has_value())
m_keyframe_effect->set_pseudo_element(Selector::PseudoElementSelector { pseudo_element.value() });
m_keyframe_effect->set_start_delay(start_time);
m_keyframe_effect->set_iteration_duration(m_end_time - start_time);
m_keyframe_effect->set_timing_function(element.property_transition_attributes(property_id)->timing_function);
m_keyframe_effect->set_timing_function(element.property_transition_attributes(pseudo_element, property_id)->timing_function);
auto key_frame_set = adopt_ref(*new Animations::KeyframeEffect::KeyFrameSet);
Animations::KeyframeEffect::KeyFrameSet::ResolvedKeyFrame initial_keyframe;
@ -118,7 +120,7 @@ CSSTransition::CSSTransition(JS::Realm& realm, DOM::Element& element, PropertyID
set_owning_element(element);
set_effect(m_keyframe_effect);
element.associate_with_animation(*this);
element.set_transition(m_transition_property, *this);
element.set_transition(pseudo_element, m_transition_property, *this);
HTML::TemporaryExecutionContext context(realm);
play().release_value_but_fixme_should_propagate_errors();

View file

@ -11,6 +11,7 @@
#include <LibWeb/CSS/CSSStyleValue.h>
#include <LibWeb/CSS/Interpolation.h>
#include <LibWeb/CSS/PropertyID.h>
#include <LibWeb/CSS/PseudoElement.h>
#include <LibWeb/CSS/StyleValues/EasingStyleValue.h>
#include <LibWeb/CSS/Time.h>
@ -21,9 +22,9 @@ class CSSTransition : public Animations::Animation {
GC_DECLARE_ALLOCATOR(CSSTransition);
public:
static GC::Ref<CSSTransition> start_a_transition(DOM::Element&, PropertyID, size_t transition_generation,
double start_time, double end_time, NonnullRefPtr<CSSStyleValue const> start_value, NonnullRefPtr<CSSStyleValue const> end_value,
NonnullRefPtr<CSSStyleValue const> reversing_adjusted_start_value, double reversing_shortening_factor);
static GC::Ref<CSSTransition> start_a_transition(DOM::Element&, Optional<PseudoElement>, PropertyID,
size_t transition_generation, double start_time, double end_time, NonnullRefPtr<CSSStyleValue const> start_value,
NonnullRefPtr<CSSStyleValue const> end_value, NonnullRefPtr<CSSStyleValue const> reversing_adjusted_start_value, double reversing_shortening_factor);
StringView transition_property() const { return string_from_property_id(m_transition_property); }
@ -51,7 +52,7 @@ public:
void set_previous_phase(Phase phase) { m_previous_phase = phase; }
private:
CSSTransition(JS::Realm&, DOM::Element&, PropertyID, size_t transition_generation,
CSSTransition(JS::Realm&, DOM::Element&, Optional<PseudoElement>, PropertyID, size_t transition_generation,
double start_time, double end_time, NonnullRefPtr<CSSStyleValue const> start_value, NonnullRefPtr<CSSStyleValue const> end_value,
NonnullRefPtr<CSSStyleValue const> reversing_adjusted_start_value, double reversing_shortening_factor);

View file

@ -1366,19 +1366,16 @@ static void apply_dimension_attribute(CascadedProperties& cascaded_properties, D
static void compute_transitioned_properties(ComputedProperties const& style, DOM::Element& element, Optional<PseudoElement> pseudo_element)
{
// FIXME: Implement transitioning for pseudo-elements
(void)pseudo_element;
auto const source_declaration = style.transition_property_source();
if (!source_declaration)
return;
if (!element.computed_properties())
return;
if (source_declaration == element.cached_transition_property_source())
if (source_declaration == element.cached_transition_property_source(pseudo_element))
return;
// Reparse this transition property
element.clear_transitions();
element.set_cached_transition_property_source(*source_declaration);
element.clear_transitions(pseudo_element);
element.set_cached_transition_property_source(pseudo_element, *source_declaration);
auto const& transition_properties_value = style.property(PropertyID::TransitionProperty);
auto transition_properties = transition_properties_value.is_value_list()
@ -1453,15 +1450,12 @@ static void compute_transitioned_properties(ComputedProperties const& style, DOM
PropertyID::TransitionBehavior,
[] { return CSSKeywordValue::create(Keyword::None); });
element.add_transitioned_properties(move(properties), move(delays), move(durations), move(timing_functions), move(transition_behaviors));
element.add_transitioned_properties(pseudo_element, move(properties), move(delays), move(durations), move(timing_functions), move(transition_behaviors));
}
// https://drafts.csswg.org/css-transitions/#starting
void StyleComputer::start_needed_transitions(ComputedProperties const& previous_style, ComputedProperties& new_style, DOM::Element& element, Optional<PseudoElement> pseudo_element) const
{
// FIXME: Implement transitions for pseudo-elements
if (pseudo_element.has_value())
return;
// https://drafts.csswg.org/css-transitions/#transition-combined-duration
auto combined_duration = [](Animations::Animatable::TransitionAttributes const& transition_attributes) {
@ -1474,19 +1468,19 @@ void StyleComputer::start_needed_transitions(ComputedProperties const& previous_
for (auto i = to_underlying(CSS::first_longhand_property_id); i <= to_underlying(CSS::last_longhand_property_id); ++i) {
auto property_id = static_cast<CSS::PropertyID>(i);
auto matching_transition_properties = element.property_transition_attributes(property_id);
auto matching_transition_properties = element.property_transition_attributes(pseudo_element, property_id);
auto const& before_change_value = previous_style.property(property_id, ComputedProperties::WithAnimationsApplied::Yes);
auto const& after_change_value = new_style.property(property_id, ComputedProperties::WithAnimationsApplied::No);
auto existing_transition = element.property_transition(property_id);
auto existing_transition = element.property_transition(pseudo_element, property_id);
bool has_running_transition = existing_transition && !existing_transition->is_finished();
bool has_completed_transition = existing_transition && existing_transition->is_finished();
auto start_a_transition = [&](auto start_time, auto end_time, auto const& start_value, auto const& end_value, auto const& reversing_adjusted_start_value, auto reversing_shortening_factor) {
dbgln_if(CSS_TRANSITIONS_DEBUG, "Starting a transition of {} from {} to {}", string_from_property_id(property_id), start_value->to_string(), end_value->to_string());
auto transition = CSSTransition::start_a_transition(element, property_id, document().transition_generation(),
start_time, end_time, start_value, end_value, reversing_adjusted_start_value, reversing_shortening_factor);
auto transition = CSSTransition::start_a_transition(element, pseudo_element, property_id,
document().transition_generation(), start_time, end_time, start_value, end_value, reversing_adjusted_start_value, reversing_shortening_factor);
// Immediately set the property's value to the transition's current value, to prevent single-frame jumps.
collect_animation_into(element, {}, as<Animations::KeyframeEffect>(*transition->effect()), new_style, AnimationRefresh::No);
};
@ -1509,7 +1503,7 @@ void StyleComputer::start_needed_transitions(ComputedProperties const& previous_
// then implementations must remove the completed transition (if present) from the set of completed transitions
if (has_completed_transition)
element.remove_transition(property_id);
element.remove_transition(pseudo_element, property_id);
// and start a transition whose:
// - start time is the time of the style change event plus the matching transition delay,
@ -1538,7 +1532,7 @@ void StyleComputer::start_needed_transitions(ComputedProperties const& previous_
// then implementations must remove the completed transition from the set of completed transitions.
else if (has_completed_transition && !existing_transition->transition_end_value()->equals(after_change_value)) {
dbgln_if(CSS_TRANSITIONS_DEBUG, "Transition step 2.");
element.remove_transition(property_id);
element.remove_transition(pseudo_element, property_id);
}
// 3. If the element has a running transition or completed transition for the property,
@ -1549,7 +1543,7 @@ void StyleComputer::start_needed_transitions(ComputedProperties const& previous_
if (has_running_transition)
existing_transition->cancel();
else
element.remove_transition(property_id);
element.remove_transition(pseudo_element, property_id);
}
// 4. If the element has a running transition for the property,
@ -1583,7 +1577,7 @@ void StyleComputer::start_needed_transitions(ComputedProperties const& previous_
existing_transition->cancel();
// AD-HOC: Remove the cancelled transition, otherwise it breaks the invariant that there is only one
// running or completed transition for a property at once.
element.remove_transition(property_id);
element.remove_transition(pseudo_element, property_id);
// - reversing-adjusted start value is the end value of the running transition,
auto reversing_adjusted_start_value = existing_transition->transition_end_value();
@ -1623,7 +1617,7 @@ void StyleComputer::start_needed_transitions(ComputedProperties const& previous_
existing_transition->cancel();
// AD-HOC: Remove the cancelled transition, otherwise it breaks the invariant that there is only one
// running or completed transition for a property at once.
element.remove_transition(property_id);
element.remove_transition(pseudo_element, property_id);
// - start time is the time of the style change event plus the matching transition delay,
auto start_time = style_change_event_time + matching_transition_properties->delay;