diff --git a/Libraries/LibWeb/Animations/Animatable.cpp b/Libraries/LibWeb/Animations/Animatable.cpp index 29602d50aab..189529c0fcd 100644 --- a/Libraries/LibWeb/Animations/Animatable.cpp +++ b/Libraries/LibWeb/Animations/Animatable.cpp @@ -1,6 +1,7 @@ /* * Copyright (c) 2024, Matthew Olsson * Copyright (c) 2024, Sam Atkins + * Copyright (c) 2025, Aliaksandr Kalenik * * SPDX-License-Identifier: BSD-2-Clause */ @@ -19,6 +20,15 @@ namespace Web::Animations { +struct Animatable::Transition { + HashMap transition_attribute_indices; + Vector transition_attributes; + GC::Ptr cached_transition_property_source; + HashMap> associated_transitions; +}; + +Animatable::Impl::~Impl() = default; + // https://www.w3.org/TR/web-animations-1/#dom-animatable-animate WebIDL::ExceptionOr> Animatable::animate(Optional> keyframes, Variant options) { @@ -128,86 +138,100 @@ void Animatable::disassociate_with_animation(GC::Ref animation) impl.associated_animations.remove_first_matching([&](auto element) { return animation == element; }); } -void Animatable::add_transitioned_properties(Vector> properties, CSS::StyleValueVector delays, CSS::StyleValueVector durations, CSS::StyleValueVector timing_functions, CSS::StyleValueVector transition_behaviors) +void Animatable::add_transitioned_properties(Optional pseudo_element, Vector> 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::property_transition_attributes(CSS::PropertyID property) const +Optional Animatable::property_transition_attributes(Optional 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 Animatable::property_transition(CSS::PropertyID property) const +GC::Ptr Animatable::property_transition(Optional 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 animation) +void Animatable::set_transition(Optional pseudo_element, CSS::PropertyID property, GC::Ref 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 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 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 Animatable::cached_animation_name_source(Optional pseudo_element) const @@ -267,24 +291,43 @@ void Animatable::set_cached_animation_name_animation(GC::Ptr Animatable::cached_transition_property_source() const +GC::Ptr Animatable::cached_transition_property_source(Optional 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 value) +void Animatable::set_cached_transition_property_source(Optional pseudo_element, GC::Ptr 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(); return *m_impl; } +Animatable::Transition* Animatable::ensure_transition(Optional 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(); + return impl.transitions[pseudo_element_index]; +} + } diff --git a/Libraries/LibWeb/Animations/Animatable.h b/Libraries/LibWeb/Animations/Animatable.h index 95e30b0f540..09afeb8f4d7 100644 --- a/Libraries/LibWeb/Animations/Animatable.h +++ b/Libraries/LibWeb/Animations/Animatable.h @@ -1,6 +1,7 @@ /* * Copyright (c) 2024, Matthew Olsson * Copyright (c) 2024, Sam Atkins + * Copyright (c) 2025, Aliaksandr Kalenik * * SPDX-License-Identifier: BSD-2-Clause */ @@ -56,20 +57,22 @@ public: GC::Ptr cached_animation_name_animation(Optional) const; void set_cached_animation_name_animation(GC::Ptr value, Optional); - GC::Ptr cached_transition_property_source() const; - void set_cached_transition_property_source(GC::Ptr value); + GC::Ptr cached_transition_property_source(Optional) const; + void set_cached_transition_property_source(Optional, GC::Ptr value); - void add_transitioned_properties(Vector> properties, CSS::StyleValueVector delays, CSS::StyleValueVector durations, CSS::StyleValueVector timing_functions, CSS::StyleValueVector transition_behaviors); - Optional property_transition_attributes(CSS::PropertyID) const; - void set_transition(CSS::PropertyID, GC::Ref); - void remove_transition(CSS::PropertyID); - GC::Ptr property_transition(CSS::PropertyID) const; - void clear_transitions(); + void add_transitioned_properties(Optional, Vector> properties, CSS::StyleValueVector delays, CSS::StyleValueVector durations, CSS::StyleValueVector timing_functions, CSS::StyleValueVector transition_behaviors); + Optional property_transition_attributes(Optional, CSS::PropertyID) const; + void set_transition(Optional, CSS::PropertyID, GC::Ref); + void remove_transition(Optional, CSS::PropertyID); + GC::Ptr property_transition(Optional, CSS::PropertyID) const; + void clear_transitions(Optional); protected: void visit_edges(JS::Cell::Visitor&); private: + struct Transition; + struct Impl { Vector> associated_animations; bool is_sorted_by_composite_order { true }; @@ -77,14 +80,14 @@ private: Array, to_underlying(CSS::PseudoElement::KnownPseudoElementCount) + 1> cached_animation_name_source; Array, to_underlying(CSS::PseudoElement::KnownPseudoElementCount) + 1> cached_animation_name_animation; - HashMap transition_attribute_indices; - Vector transition_attributes; - GC::Ptr cached_transition_property_source; - HashMap> associated_transitions; - }; - Impl& ensure_impl(); + mutable Array, to_underlying(CSS::PseudoElement::KnownPseudoElementCount) + 1> transitions; - OwnPtr m_impl; + ~Impl(); + }; + Impl& ensure_impl() const; + Transition* ensure_transition(Optional) const; + + mutable OwnPtr m_impl; }; } diff --git a/Libraries/LibWeb/CSS/CSSTransition.cpp b/Libraries/LibWeb/CSS/CSSTransition.cpp index 3f70ef4a2f9..d3061615bec 100644 --- a/Libraries/LibWeb/CSS/CSSTransition.cpp +++ b/Libraries/LibWeb/CSS/CSSTransition.cpp @@ -19,12 +19,12 @@ namespace Web::CSS { GC_DEFINE_ALLOCATOR(CSSTransition); -GC::Ref CSSTransition::start_a_transition(DOM::Element& element, PropertyID property_id, size_t transition_generation, - double start_time, double end_time, NonnullRefPtr start_value, NonnullRefPtr end_value, - NonnullRefPtr reversing_adjusted_start_value, double reversing_shortening_factor) +GC::Ref CSSTransition::start_a_transition(DOM::Element& element, Optional pseudo_element, PropertyID property_id, + size_t transition_generation, double start_time, double end_time, NonnullRefPtr start_value, + NonnullRefPtr end_value, NonnullRefPtr reversing_adjusted_start_value, double reversing_shortening_factor) { auto& realm = element.realm(); - return realm.create(realm, element, property_id, transition_generation, start_time, end_time, start_value, end_value, reversing_adjusted_start_value, reversing_shortening_factor); + return realm.create(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 CSSTransition::class_specific_composite_order(GC::Ref pseudo_element, PropertyID property_id, size_t transition_generation, double start_time, double end_time, NonnullRefPtr start_value, NonnullRefPtr end_value, NonnullRefPtr 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(); diff --git a/Libraries/LibWeb/CSS/CSSTransition.h b/Libraries/LibWeb/CSS/CSSTransition.h index ea4bd8ed13a..22cfc3e325b 100644 --- a/Libraries/LibWeb/CSS/CSSTransition.h +++ b/Libraries/LibWeb/CSS/CSSTransition.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -21,9 +22,9 @@ class CSSTransition : public Animations::Animation { GC_DECLARE_ALLOCATOR(CSSTransition); public: - static GC::Ref start_a_transition(DOM::Element&, PropertyID, size_t transition_generation, - double start_time, double end_time, NonnullRefPtr start_value, NonnullRefPtr end_value, - NonnullRefPtr reversing_adjusted_start_value, double reversing_shortening_factor); + static GC::Ref start_a_transition(DOM::Element&, Optional, PropertyID, + size_t transition_generation, double start_time, double end_time, NonnullRefPtr start_value, + NonnullRefPtr end_value, NonnullRefPtr 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, PropertyID, size_t transition_generation, double start_time, double end_time, NonnullRefPtr start_value, NonnullRefPtr end_value, NonnullRefPtr reversing_adjusted_start_value, double reversing_shortening_factor); diff --git a/Libraries/LibWeb/CSS/StyleComputer.cpp b/Libraries/LibWeb/CSS/StyleComputer.cpp index 6a55580ec00..cbe7dd8e7d1 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -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 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 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(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(*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;