From 1c61ccef40c12840ef50d9ff82a7dde6e1689108 Mon Sep 17 00:00:00 2001 From: Lucas CHOLLET Date: Sun, 22 Dec 2024 18:26:52 -0500 Subject: [PATCH] LibWeb/DOM: Fire transition[cancel,start,run,end] events --- Libraries/LibWeb/Animations/Animation.h | 3 +- Libraries/LibWeb/Animations/AnimationEffect.h | 1 + Libraries/LibWeb/CSS/CSSTransition.h | 13 ++ Libraries/LibWeb/DOM/Document.cpp | 132 ++++++++++++++++++ Libraries/LibWeb/DOM/Document.h | 1 + Libraries/LibWeb/HTML/EventNames.h | 3 + 6 files changed, 152 insertions(+), 1 deletion(-) diff --git a/Libraries/LibWeb/Animations/Animation.h b/Libraries/LibWeb/Animations/Animation.h index 7491e4c21d2..57e5ad704da 100644 --- a/Libraries/LibWeb/Animations/Animation.h +++ b/Libraries/LibWeb/Animations/Animation.h @@ -111,6 +111,8 @@ public: auto release_saved_cancel_time() { return move(m_saved_cancel_time); } + double associated_effect_end() const; + protected: Animation(JS::Realm&); @@ -133,7 +135,6 @@ private: No, }; - double associated_effect_end() const; double effective_playback_rate() const; void apply_any_pending_playback_rate(); diff --git a/Libraries/LibWeb/Animations/AnimationEffect.h b/Libraries/LibWeb/Animations/AnimationEffect.h index 371d3b740d5..aa4514d1862 100644 --- a/Libraries/LibWeb/Animations/AnimationEffect.h +++ b/Libraries/LibWeb/Animations/AnimationEffect.h @@ -118,6 +118,7 @@ public: bool is_in_the_active_phase() const; bool is_in_the_idle_phase() const; + // Keep this enum up to date with CSSTransition::Phase. enum class Phase { Before, Active, diff --git a/Libraries/LibWeb/CSS/CSSTransition.h b/Libraries/LibWeb/CSS/CSSTransition.h index e8b1cc91819..9fee3411975 100644 --- a/Libraries/LibWeb/CSS/CSSTransition.h +++ b/Libraries/LibWeb/CSS/CSSTransition.h @@ -39,6 +39,17 @@ public: double timing_function_output_at_time(double t) const; NonnullRefPtr value_at_time(double t) const; + // This is designed to be created from AnimationEffect::Phase. + enum class Phase : u8 { + Before, + Active, + After, + Idle, + Pending, + }; + Phase previous_phase() const { return m_previous_phase; } + void set_previous_phase(Phase phase) { m_previous_phase = phase; } + private: CSSTransition(JS::Realm&, DOM::Element&, PropertyID, size_t transition_generation, double start_time, double end_time, NonnullRefPtr start_value, NonnullRefPtr end_value, @@ -75,6 +86,8 @@ private: GC::Ref m_keyframe_effect; GC::Ptr m_cached_declaration; + + Phase m_previous_phase { Phase::Idle }; }; } diff --git a/Libraries/LibWeb/DOM/Document.cpp b/Libraries/LibWeb/DOM/Document.cpp index 78c8ac5be3c..108c16e6526 100644 --- a/Libraries/LibWeb/DOM/Document.cpp +++ b/Libraries/LibWeb/DOM/Document.cpp @@ -32,12 +32,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -2204,9 +2206,139 @@ Element* Document::find_a_potential_indicated_element(FlyString const& fragment) return nullptr; } +// https://drafts.csswg.org/css-transitions-2/#event-dispatch +void Document::dispatch_events_for_transition(GC::Ref transition) +{ + auto previous_phase = transition->previous_phase(); + + using Phase = CSS::CSSTransition::Phase; + // The transition phase of a transition is initially ‘idle’ and is updated on each + // animation frame according to the first matching condition from below: + auto transition_phase = Phase::Idle; + + if (!transition->effect()) { + // If the transition has no associated effect, + if (!transition->current_time().has_value()) { + // If the transition has an unresolved current time, + // The transition phase is ‘idle’. + } else if (transition->current_time().value() < 0.0) { + // If the transition has a current time < 0, + // The transition phase is ‘before’. + transition_phase = Phase::Before; + } else { + // Otherwise, + // The transition phase is ‘after’. + transition_phase = Phase::After; + } + } else if (transition->pending() && (previous_phase == Phase::Idle || previous_phase == Phase::Pending)) { + // If the transition has a pending play task or a pending pause task + // and its phase was previously ‘idle’ or ‘pending’, + // The transition phase is ‘pending’. + transition_phase = Phase::Pending; + } else { + // Otherwise, + // The transition phase is the phase of its associated effect. + transition_phase = Phase(to_underlying(transition->effect()->phase())); + } + + enum class Interval : u8 { + Start, + End, + ActiveTime, + }; + + auto dispatch_event = [&](FlyString const& type, Interval interval) { + // The target for a transition event is the transition’s owning element. If there is no owning element, + // no transition events are dispatched. + if (!transition->effect() || !transition->owning_element()) + return; + + auto effect = transition->effect(); + + double elapsed_time = [&]() { + if (interval == Interval::Start) + return max(min(-effect->start_delay(), effect->active_duration()), 0) / 1000; + if (interval == Interval::End) + return max(min(transition->associated_effect_end() - effect->start_delay(), effect->active_duration()), 0) / 1000; + if (interval == Interval::ActiveTime) { + // The active time of the animation at the moment it was canceled calculated using a fill mode of both. + // FIXME: Compute this properly. + return 0.0; + } + VERIFY_NOT_REACHED(); + }(); + + append_pending_animation_event({ + .event = CSS::TransitionEvent::create( + transition->owning_element()->realm(), + type, + CSS::TransitionEventInit { + { .bubbles = true }, + // FIXME: Correctly set property_name and pseudo_element + String {}, + elapsed_time, + String {}, + }), + .animation = transition, + .target = *transition->owning_element(), + .scheduled_event_time = HighResolutionTime::unsafe_shared_current_time(), + }); + }; + + if (previous_phase == Phase::Idle) { + if (transition_phase == Phase::Pending || transition_phase == Phase::Before) + dispatch_event(HTML::EventNames::transitionrun, Interval::Start); + + if (transition_phase == Phase::Active) { + dispatch_event(HTML::EventNames::transitionrun, Interval::Start); + dispatch_event(HTML::EventNames::transitionstart, Interval::Start); + } + + if (transition_phase == Phase::After) { + dispatch_event(HTML::EventNames::transitionrun, Interval::Start); + dispatch_event(HTML::EventNames::transitionstart, Interval::Start); + dispatch_event(HTML::EventNames::transitionend, Interval::End); + } + } else if (previous_phase == Phase::Pending || previous_phase == Phase::Before) { + if (transition_phase == Phase::Active) + dispatch_event(HTML::EventNames::transitionstart, Interval::Start); + + if (transition_phase == Phase::After) { + dispatch_event(HTML::EventNames::transitionstart, Interval::Start); + dispatch_event(HTML::EventNames::transitionend, Interval::End); + } + } else if (previous_phase == Phase::Active) { + if (transition_phase == Phase::After) + dispatch_event(HTML::EventNames::transitionend, Interval::End); + + if (transition_phase == Phase::Before) + dispatch_event(HTML::EventNames::transitionend, Interval::Start); + } else if (previous_phase == Phase::After) { + if (transition_phase == Phase::Active) + dispatch_event(HTML::EventNames::transitionstart, Interval::End); + + if (transition_phase == Phase::Before) { + dispatch_event(HTML::EventNames::transitionstart, Interval::End); + dispatch_event(HTML::EventNames::transitionend, Interval::Start); + } + } + + if (transition_phase == Phase::Idle) { + if (previous_phase != Phase::Idle && previous_phase != Phase::After) + dispatch_event(HTML::EventNames::animationstart, Interval::ActiveTime); + } + + transition->set_previous_phase(transition_phase); +} + // https://www.w3.org/TR/css-animations-2/#event-dispatch void Document::dispatch_events_for_animation_if_necessary(GC::Ref animation) { + if (animation->is_css_transition()) { + dispatch_events_for_transition(verify_cast(*animation)); + return; + } + // Each time a new animation frame is established and the animation does not have a pending play task or pending // pause task, the events to dispatch are determined by comparing the animation’s phase before and after // establishing the new animation frame as follows: diff --git a/Libraries/LibWeb/DOM/Document.h b/Libraries/LibWeb/DOM/Document.h index 261e2b620f7..58e190fcd36 100644 --- a/Libraries/LibWeb/DOM/Document.h +++ b/Libraries/LibWeb/DOM/Document.h @@ -785,6 +785,7 @@ private: Element* find_a_potential_indicated_element(FlyString const& fragment) const; + void dispatch_events_for_transition(GC::Ref); void dispatch_events_for_animation_if_necessary(GC::Ref); template diff --git a/Libraries/LibWeb/HTML/EventNames.h b/Libraries/LibWeb/HTML/EventNames.h index 015ba071a6b..9bb6304bcd6 100644 --- a/Libraries/LibWeb/HTML/EventNames.h +++ b/Libraries/LibWeb/HTML/EventNames.h @@ -116,6 +116,9 @@ namespace Web::HTML::EventNames { __ENUMERATE_HTML_EVENT(suspend) \ __ENUMERATE_HTML_EVENT(timeupdate) \ __ENUMERATE_HTML_EVENT(toggle) \ + __ENUMERATE_HTML_EVENT(transitioncancel) \ + __ENUMERATE_HTML_EVENT(transitionrun) \ + __ENUMERATE_HTML_EVENT(transitionstart) \ __ENUMERATE_HTML_EVENT(transitionend) \ __ENUMERATE_HTML_EVENT(unhandledrejection) \ __ENUMERATE_HTML_EVENT(unload) \