From 65ebfcb37dd49279a3aa1c6d126112f7728f1e7b Mon Sep 17 00:00:00 2001 From: Psychpsyo Date: Sun, 23 Mar 2025 12:38:21 +0100 Subject: [PATCH] LibWeb: Implement view transitions This implements the CSS view-transitions-1 spec. --- Libraries/LibWeb/CMakeLists.txt | 1 + Libraries/LibWeb/CSS/Selector.cpp | 5 + Libraries/LibWeb/CSS/StyleComputer.cpp | 1 + Libraries/LibWeb/DOM/Document.cpp | 105 +- Libraries/LibWeb/DOM/Document.h | 32 + Libraries/LibWeb/DOM/Document.idl | 4 + Libraries/LibWeb/DOM/Element.cpp | 90 ++ Libraries/LibWeb/DOM/Element.h | 15 + Libraries/LibWeb/Forward.h | 4 + Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp | 5 +- Libraries/LibWeb/HTML/Navigable.cpp | 17 + Libraries/LibWeb/HTML/Navigable.h | 6 + .../LibWeb/ViewTransition/ViewTransition.cpp | 970 ++++++++++++++++++ .../LibWeb/ViewTransition/ViewTransition.h | 145 +++ .../LibWeb/ViewTransition/ViewTransition.idl | 11 + Libraries/LibWeb/idl_files.cmake | 1 + .../BindingsGenerator/IDLGenerators.cpp | 1 + .../LibWeb/BindingsGenerator/Namespaces.h | 1 + .../Userland/Libraries/LibWeb/idl_files.gni | 1 + 19 files changed, 1413 insertions(+), 2 deletions(-) create mode 100644 Libraries/LibWeb/ViewTransition/ViewTransition.cpp create mode 100644 Libraries/LibWeb/ViewTransition/ViewTransition.h create mode 100644 Libraries/LibWeb/ViewTransition/ViewTransition.idl diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index 09009f99d40..b5d8247ca94 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -809,6 +809,7 @@ set(SOURCES URLPattern/URLPattern.cpp UserTiming/PerformanceMark.cpp UserTiming/PerformanceMeasure.cpp + ViewTransition/ViewTransition.cpp WebAssembly/Global.cpp WebAssembly/Instance.cpp WebAssembly/Memory.cpp diff --git a/Libraries/LibWeb/CSS/Selector.cpp b/Libraries/LibWeb/CSS/Selector.cpp index 2e095ef77da..b7dbfe3def5 100644 --- a/Libraries/LibWeb/CSS/Selector.cpp +++ b/Libraries/LibWeb/CSS/Selector.cpp @@ -256,8 +256,13 @@ u32 Selector::specificity() const break; } case SimpleSelector::Type::TagName: + // count the number of type selectors and pseudo-elements in the selector (= C) + ++tag_names; + break; case SimpleSelector::Type::PseudoElement: // count the number of type selectors and pseudo-elements in the selector (= C) + // FIXME: This needs special handling for view transition pseudos: + // https://drafts.csswg.org/css-view-transitions-1/#named-view-transition-pseudo ++tag_names; break; case SimpleSelector::Type::Universal: diff --git a/Libraries/LibWeb/CSS/StyleComputer.cpp b/Libraries/LibWeb/CSS/StyleComputer.cpp index 41f5d189615..9ef7b888be2 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -372,6 +372,7 @@ void StyleComputer::for_each_stylesheet(CascadeOrigin cascade_origin, Callback c { if (cascade_origin == CascadeOrigin::UserAgent) { callback(default_stylesheet(document()), {}); + document().dynamic_view_transition_style_sheet(); if (document().in_quirks_mode()) callback(quirks_mode_stylesheet(document()), {}); callback(mathml_stylesheet(document()), {}); diff --git a/Libraries/LibWeb/DOM/Document.cpp b/Libraries/LibWeb/DOM/Document.cpp index 9d6f8659018..3b60de6158c 100644 --- a/Libraries/LibWeb/DOM/Document.cpp +++ b/Libraries/LibWeb/DOM/Document.cpp @@ -127,6 +127,7 @@ #include #include #include +#include #include #include #include @@ -443,6 +444,7 @@ Document::Document(JS::Realm& realm, const URL::URL& url, TemporaryDocumentForFr , m_url(url) , m_temporary_document_for_fragment_parsing(temporary_document_for_fragment_parsing) , m_editing_host_manager(EditingHostManager::create(realm, *this)) + , m_dynamic_view_transition_style_sheet(parse_css_stylesheet(CSS::Parser::ParsingParams(realm), ""sv, {})) { m_legacy_platform_object_flags = PlatformObject::LegacyPlatformObjectFlags { .supports_named_properties = true, @@ -593,6 +595,8 @@ void Document::visit_edges(Cell::Visitor& visitor) visitor.visit(m_local_storage_holder); visitor.visit(m_session_storage_holder); visitor.visit(m_render_blocking_elements); + visitor.visit(m_active_view_transition); + visitor.visit(m_dynamic_view_transition_style_sheet); visitor.visit(m_policy_container); } @@ -3284,7 +3288,16 @@ void Document::update_the_visibility_state(HTML::VisibilityState visibility_stat }, m_visibility_state); - // 4. Fire an event named visibilitychange at document, with its bubbles attribute initialized to true. + // 4. Run the screen orientation change steps with document. [SCREENORIENTATION] + // FIXME: Implement this. + + // 5. Run the view transition page visibility change steps with document. + view_transition_page_visibility_change_steps(); + + // 6. Run any page visibility change steps which may be defined in other specifications, with visibility state and document. + // FIXME: Implement this. + + // 7. Fire an event named visibilitychange at document, with its bubbles attribute initialized to true. auto event = DOM::Event::create(realm(), HTML::EventNames::visibilitychange); event->set_bubbles(true); dispatch_event(event); @@ -6441,6 +6454,96 @@ void Document::set_onvisibilitychange(WebIDL::CallbackType* value) set_event_handler_attribute(HTML::EventNames::visibilitychange, value); } +// https://drafts.csswg.org/css-view-transitions-1/#dom-document-startviewtransition +// FIXME: Calling document.startViewTransition() without arguments throws TypeError instead of calling this. +GC::Ptr Document::start_view_transition(Optional update_callback) +{ + // The method steps for startViewTransition(updateCallback) are as follows: + + // 1. Let transition be a new ViewTransition object in this’s relevant Realm. + auto& realm = this->realm(); + auto transition = ViewTransition::ViewTransition::create(realm); + + // 2. If updateCallback is provided, set transition’s update callback to updateCallback. + if (update_callback.has_value()) + transition->set_update_callback(update_callback.value()); + + // 3. Let document be this’s relevant global object’s associated document. + auto& document = as(relevant_global_object(*this)).associated_document(); + + // 4. If document’s visibility state is "hidden", then skip transition with an "InvalidStateError" DOMException, + // and return transition. + if (m_visibility_state == HTML::VisibilityState::Hidden) + transition->skip_the_view_transition(WebIDL::InvalidStateError::create(realm, "The document's visibility state is \"hidden\""_string)); + + // 5. If document’s active view transition is not null, then skip that view transition with an "AbortError" + // DOMException in this’s relevant Realm. + if (document.m_active_view_transition) + document.m_active_view_transition->skip_the_view_transition(WebIDL::AbortError::create(realm, "Document.startViewTransition() was called"_string)); + + // 6. Set document’s active view transition to transition. + m_active_view_transition = transition; + + // 7. Return transition. + return transition; +} + +// https://drafts.csswg.org/css-view-transitions-1/#perform-pending-transition-operations +void Document::perform_pending_transition_operations() +{ + // To perform pending transition operations given a Document document, perform the following steps: + + // 1. If document’s active view transition is not null, then: + if (m_active_view_transition) { + // 1. If document’s active view transition’s phase is "pending-capture", then setup view transition for + // document’s active view transition. + if (m_active_view_transition->phase() == ViewTransition::ViewTransition::Phase::PendingCapture) + m_active_view_transition->setup_view_transition(); + // 2. Otherwise, if document’s active view transition’s phase is "animating", then handle transition frame for + // document’s active view transition. + else if (m_active_view_transition->phase() == ViewTransition::ViewTransition::Phase::Animating) + m_active_view_transition->handle_transition_frame(); + } +} + +// https://drafts.csswg.org/css-view-transitions-1/#flush-the-update-callback-queue +void Document::flush_the_update_callback_queue() +{ + // To flush the update callback queue given a Document document: + + // 1. For each transition in document’s update callback queue, call the update callback given transition. + for (auto& transition : m_update_callback_queue) { + transition->call_the_update_callback(); + } + + // 2. Set document’s update callback queue to an empty list. + m_update_callback_queue = {}; +} + +// https://drafts.csswg.org/css-view-transitions-1/#view-transition-page-visibility-change-steps +void Document::view_transition_page_visibility_change_steps() +{ + // The view transition page-visibility change steps given Document document are: + + // 1. Queue a global task on the DOM manipulation task source, given document’s relevant global object, to + // perform the following steps: + HTML::queue_global_task(HTML::Task::Source::DOMManipulation, HTML::relevant_global_object(*this), GC::create_function(realm().heap(), [&] { + HTML::TemporaryExecutionContext context(realm()); + // 1. If document’s visibility state is "hidden", then: + if (m_visibility_state == HTML::VisibilityState::Hidden) { + // 1. If document’s active view transition is not null, then skip document’s active view transition with an + // "InvalidStateError" DOMException. + if (m_active_view_transition) { + m_active_view_transition->skip_the_view_transition(WebIDL::InvalidStateError::create(realm(), "The document's visibility state is \"hidden\"."_string)); + } + } + // 2. Otherwise, assert: active view transition is null. + else { + VERIFY(!m_active_view_transition); + } + })); +} + StringView to_string(SetNeedsLayoutReason reason) { switch (reason) { diff --git a/Libraries/LibWeb/DOM/Document.h b/Libraries/LibWeb/DOM/Document.h index dbce147a627..00859c79c80 100644 --- a/Libraries/LibWeb/DOM/Document.h +++ b/Libraries/LibWeb/DOM/Document.h @@ -39,6 +39,7 @@ #include #include #include +#include #include #include @@ -851,6 +852,22 @@ public: [[nodiscard]] WebIDL::CallbackType* onvisibilitychange(); void set_onvisibilitychange(WebIDL::CallbackType*); + // https://drafts.csswg.org/css-view-transitions-1/#dom-document-startviewtransition + GC::Ptr start_view_transition(Optional update_callback); + // https://drafts.csswg.org/css-view-transitions-1/#perform-pending-transition-operations + void perform_pending_transition_operations(); + // https://drafts.csswg.org/css-view-transitions-1/#flush-the-update-callback-queue + void flush_the_update_callback_queue(); + // https://drafts.csswg.org/css-view-transitions-1/#view-transition-page-visibility-change-steps + void view_transition_page_visibility_change_steps(); + + GC::Ptr active_view_transition() const { return m_active_view_transition; } + void set_active_view_transition(GC::Ptr view_transition) { m_active_view_transition = view_transition; } + void set_rendering_suppression_for_view_transitions(bool value) { m_rendering_suppression_for_view_transitions = value; } + GC::Ptr dynamic_view_transition_style_sheet() const { return m_dynamic_view_transition_style_sheet; } + void set_show_view_transition_tree(bool value) { m_show_view_transition_tree = value; } + Vector update_callback_queue() const { return m_update_callback_queue; } + void reset_cursor_blink_cycle(); GC::Ref editing_host_manager() const { return *m_editing_host_manager; } @@ -1248,6 +1265,21 @@ private: // https://html.spec.whatwg.org/multipage/dom.html#render-blocking-element-set HashTable> m_render_blocking_elements; + // https://drafts.csswg.org/css-view-transitions-1/#document-active-view-transition + GC::Ptr m_active_view_transition; + + // https://drafts.csswg.org/css-view-transitions-1/#document-rendering-suppression-for-view-transitions + bool m_rendering_suppression_for_view_transitions { false }; + + // https://drafts.csswg.org/css-view-transitions-1/#document-dynamic-view-transition-style-sheet + GC::Ptr m_dynamic_view_transition_style_sheet; + + // https://drafts.csswg.org/css-view-transitions-1/#document-show-view-transition-tree + bool m_show_view_transition_tree { false }; + + // https://drafts.csswg.org/css-view-transitions-1/#document-update-callback-queue + Vector m_update_callback_queue = {}; + HashTable> m_pending_nodes_for_style_invalidation_due_to_presence_of_has; }; diff --git a/Libraries/LibWeb/DOM/Document.idl b/Libraries/LibWeb/DOM/Document.idl index 9915ae7d6bc..b3ed586e6bf 100644 --- a/Libraries/LibWeb/DOM/Document.idl +++ b/Libraries/LibWeb/DOM/Document.idl @@ -26,6 +26,7 @@ #import #import #import +#import // https://dom.spec.whatwg.org/#document // https://html.spec.whatwg.org/multipage/dom.html#the-document-object @@ -156,6 +157,9 @@ interface Document : Node { // special event handler IDL attributes that only apply to Document objects [LegacyLenientThis] attribute EventHandler onreadystatechange; attribute EventHandler onvisibilitychange; + + // https://drafts.csswg.org/css-view-transitions-1/#additions-to-document-api + ViewTransition startViewTransition(optional ViewTransitionUpdateCallback updateCallback); }; dictionary ElementCreationOptions { diff --git a/Libraries/LibWeb/DOM/Element.cpp b/Libraries/LibWeb/DOM/Element.cpp index 6782f0c8176..68fbb217972 100644 --- a/Libraries/LibWeb/DOM/Element.cpp +++ b/Libraries/LibWeb/DOM/Element.cpp @@ -77,6 +77,7 @@ #include #include #include +#include #include #include #include @@ -3779,4 +3780,93 @@ Optional Element::lang() const return {}; return maybe_lang.release_value(); } + +// https://drafts.csswg.org/css-images-4/#element-not-rendered +bool Element::not_rendered() const +{ + // An element is not rendered if it does not have an associated box. + if (!layout_node() || !paintable_box()) + return true; + + return false; +} + +// https://drafts.csswg.org/css-view-transitions-1/#document-scoped-view-transition-name +Optional Element::document_scoped_view_transition_name() +{ + // To get the document-scoped view transition name for an Element element: + + // 1. Let scopedViewTransitionName be the computed value of view-transition-name for element. + auto scoped_view_transition_name = computed_properties()->view_transition_name(); + + // 2. If scopedViewTransitionName is associated with element’s node document, then return + // scopedViewTransitionName. + // FIXME: Properly handle tree-scoping of the name here. + // (see https://drafts.csswg.org/css-view-transitions-1/#propdef-view-transition-name , "Each view transition name is a tree-scoped name.") + if (true) { + return scoped_view_transition_name; + } + + // 3. Otherwise, return none. + return {}; +} + +// https://drafts.csswg.org/css-view-transitions-1/#capture-the-image +// To capture the image given an element element, perform the following steps. They return an image. +RefPtr Element::capture_the_image() +{ + // 1. If element is the document element, then: + if (is_document_element()) { + // 1. Render the region of document (including its canvas background and any top layer content) that + // intersects the snapshot containing block, on a transparent canvas the size of the snapshot containing + // block, following the capture rendering characteristics, and these additional characteristics: + // + // - Areas outside element’s scrolling box should be rendered as if they were scrolled to, without + // moving or resizing the layout viewport. This must not trigger events related to scrolling or resizing, + // such as IntersectionObservers. + // - Areas that cannot be scrolled to (i.e. they are out of scrolling bounds), should render the canvas + // background. + + // FIXME: Make this obey all the rules given above. + auto snapshot_containing_block_size = document().navigable()->snapshot_containing_block_size(); + auto size = Gfx::IntSize(snapshot_containing_block_size.get<0>(), snapshot_containing_block_size.get<1>()); + auto image = MUST(Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, Gfx::AlphaType::Premultiplied, size)); + auto painting_surface = Gfx::PaintingSurface::wrap_bitmap(image); + + auto display_list = Painting::DisplayList::create(); + Painting::DisplayListRecorder display_list_recorder(display_list); + PaintContext paint_context { display_list_recorder, document().page().palette(), document().page().client().device_pixels_per_css_pixel() }; + + auto viewport_paintable = document().paintable(); + viewport_paintable->refresh_scroll_state(); + + // NOTE: Painting the stacking context is fine here since the fact that this element is captured in a view transition makes it form a stacking context. + paintable_box()->stacking_context()->paint(paint_context); + + display_list->set_scroll_state(viewport_paintable->scroll_state()); + + Painting::DisplayListPlayerSkia display_list_player; + display_list_player.set_surface(painting_surface); + display_list_player.execute(display_list); + + // 2. Return this canvas as an image. The natural size of the image is equal to the snapshot containing + // block. + return image; + } + + // 2. Otherwise: + else { + // 1. Render element and its descendants, at the same size it appears in its node document, over an infinite + // transparent canvas, following the capture rendering characteristics. + // FIXME: Implement this. + auto image = MUST(Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, Gfx::AlphaType::Premultiplied, Gfx::IntSize(0, 0))); + + // 2. Return the portion of this canvas that includes element’s ink overflow rectangle as an image. The + // natural dimensions of this image must be those of its principal border box, and its origin must + // correspond to that border box’s origin, such that the image represents the contents of this border box + // and any captured ink overflow is represented outside these bounds. + return image; + } +} + } diff --git a/Libraries/LibWeb/DOM/Element.h b/Libraries/LibWeb/DOM/Element.h index fb4494cb14a..df5d789bb8a 100644 --- a/Libraries/LibWeb/DOM/Element.h +++ b/Libraries/LibWeb/DOM/Element.h @@ -464,6 +464,18 @@ public: Element const* list_owner() const; size_t ordinal_value() const; + bool captured_in_a_view_transition() const { return m_captured_in_a_view_transition; } + void set_captured_in_a_view_transition(bool value) { m_captured_in_a_view_transition = value; } + + // https://drafts.csswg.org/css-images-4/#element-not-rendered + bool not_rendered() const; + + // https://drafts.csswg.org/css-view-transitions-1/#document-scoped-view-transition-name + Optional document_scoped_view_transition_name(); + + // https://drafts.csswg.org/css-view-transitions-1/#capture-the-image + RefPtr capture_the_image(); + protected: Element(Document&, DOM::QualifiedName); virtual void initialize(JS::Realm&) override; @@ -570,6 +582,9 @@ private: // https://drafts.csswg.org/css-contain/#proximity-to-the-viewport ProximityToTheViewport m_proximity_to_the_viewport { ProximityToTheViewport::NotDetermined }; + + // https://drafts.csswg.org/css-view-transitions-1/#captured-in-a-view-transition + bool m_captured_in_a_view_transition { false }; }; template<> diff --git a/Libraries/LibWeb/Forward.h b/Libraries/LibWeb/Forward.h index d85324a960c..41a47595277 100644 --- a/Libraries/LibWeb/Forward.h +++ b/Libraries/LibWeb/Forward.h @@ -864,6 +864,10 @@ class PerformanceMark; class PerformanceMeasure; } +namespace Web::ViewTransition { +class ViewTransition; +} + namespace Web::WebAssembly { class Global; class Instance; diff --git a/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp b/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp index a22f5454de5..82790ead6ad 100644 --- a/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp +++ b/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp @@ -457,7 +457,10 @@ void EventLoop::update_the_rendering() // FIXME: 17. For each doc of docs, if the focused area of doc is not a focusable area, then run the focusing steps for doc's viewport, and set doc's relevant global object's navigation API's focus changed during ongoing navigation to false. - // FIXME: 18. For each doc of docs, perform pending transition operations for doc. [CSSVIEWTRANSITIONS] + // 18. For each doc of docs, perform pending transition operations for doc. [CSSVIEWTRANSITIONS] + for (auto& document : docs) { + document->perform_pending_transition_operations(); + } // 19. For each doc of docs, run the update intersection observations steps for doc, passing in the relative high resolution time given now and doc's relevant global object as the timestamp. [INTERSECTIONOBSERVER] for (auto& document : docs) { diff --git a/Libraries/LibWeb/HTML/Navigable.cpp b/Libraries/LibWeb/HTML/Navigable.cpp index 0ee4e46da5c..6a545a35b5e 100644 --- a/Libraries/LibWeb/HTML/Navigable.cpp +++ b/Libraries/LibWeb/HTML/Navigable.cpp @@ -2437,6 +2437,23 @@ void Navigable::paste(String const& text) m_event_handler.handle_paste(text); } +// https://drafts.csswg.org/css-view-transitions-1/#snapshot-containing-block +CSSPixelRect Navigable::snapshot_containing_block() +{ + // The snapshot containing block is a rectangle that covers all areas of the window that could potentially display + // page content (and is therefore consistent regardless of root scrollbars or interactive widgets). + + // Within a child navigable, the snapshot containing block is the union of the navigable’s viewport with any scrollbar gutters. + // FIXME: Actually get the correct rectangle here. + return viewport_rect(); +} +// https://drafts.csswg.org/css-view-transitions-1/#snapshot-containing-block-size +Tuple Navigable::snapshot_containing_block_size() +{ + auto snapshot_containing_block = this->snapshot_containing_block(); + return Tuple { snapshot_containing_block.width(), snapshot_containing_block.height() }; +} + void Navigable::register_navigation_observer(Badge, NavigationObserver& navigation_observer) { auto result = m_navigation_observers.set(navigation_observer); diff --git a/Libraries/LibWeb/HTML/Navigable.h b/Libraries/LibWeb/HTML/Navigable.h index 9b77704bd63..f0045098464 100644 --- a/Libraries/LibWeb/HTML/Navigable.h +++ b/Libraries/LibWeb/HTML/Navigable.h @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -182,6 +183,11 @@ public: Web::EventHandler& event_handler() { return m_event_handler; } Web::EventHandler const& event_handler() const { return m_event_handler; } + // https://drafts.csswg.org/css-view-transitions-1/#snapshot-containing-block + CSSPixelRect snapshot_containing_block(); + // https://drafts.csswg.org/css-view-transitions-1/#snapshot-containing-block-size + Tuple snapshot_containing_block_size(); + bool has_session_history_entry_and_ready_for_navigation() const { return m_has_session_history_entry_and_ready_for_navigation; } void set_has_session_history_entry_and_ready_for_navigation(); diff --git a/Libraries/LibWeb/ViewTransition/ViewTransition.cpp b/Libraries/LibWeb/ViewTransition/ViewTransition.cpp new file mode 100644 index 00000000000..910319d625a --- /dev/null +++ b/Libraries/LibWeb/ViewTransition/ViewTransition.cpp @@ -0,0 +1,970 @@ +/* + * Copyright (c) 2025, Psychpsyo + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Web::ViewTransition { + +GC_DEFINE_ALLOCATOR(ViewTransition); + +GC::Ref ViewTransition::create(JS::Realm& realm) +{ + auto const& finished_promise = WebIDL::create_promise(realm); + WebIDL::mark_promise_as_handled(finished_promise); + return realm.create(realm, WebIDL::create_promise(realm), WebIDL::create_promise(realm), finished_promise); +} + +ViewTransition::ViewTransition(JS::Realm& realm, GC::Ref ready_promise, GC::Ref update_callback_done_promise, GC::Ref finished_promise) + : PlatformObject(realm) + , m_ready_promise(ready_promise) + , m_update_callback_done_promise(update_callback_done_promise) + , m_finished_promise(finished_promise) + +{ +} + +void ViewTransition::initialize(JS::Realm& realm) +{ + Base::initialize(realm); + WEB_SET_PROTOTYPE_FOR_INTERFACE(ViewTransition); +} + +void ViewTransition::visit_edges(Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + + for (auto captured_element : m_named_elements) { + visitor.visit(captured_element.value); + } +} + +void CapturedElement::visit_edges(Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); +} + +// https://drafts.csswg.org/css-view-transitions-1/#dom-viewtransition-skiptransition +void ViewTransition::skip_transition() +{ + // The method steps for skipTransition() are: + + // 1. If this's phase is not "done", then skip the view transition for this with an "AbortError" DOMException. + if (m_phase != Phase::Done) { + skip_the_view_transition(WebIDL::AbortError::create(realm(), "ViewTransition.skip_transition() was called"_string)); + } +} + +// https://drafts.csswg.org/css-view-transitions-1/#setup-view-transition +void ViewTransition::setup_view_transition() +{ + auto& realm = this->realm(); + // To setup view transition for a ViewTransition transition, perform the following steps: + + // 1. Let document be transition’s relevant global object’s associated document. + auto& document = as(HTML::relevant_global_object(*this)).associated_document(); + + // 2. Flush the update callback queue. + // AD-HOC: Spec doesn't say what document to flush it for. + // Lets just use the one we have. + // (see https://github.com/w3c/csswg-drafts/issues/11986 ) + document.flush_the_update_callback_queue(); + + // 3. Capture the old state for transition. + auto result = capture_the_old_state(); + // If failure is returned, + if (result.is_error()) { + // then skip the view transition for transition with an "InvalidStateError" DOMException in transition’s relevant Realm, + skip_the_view_transition(WebIDL::AbortError::create(realm, "Failed to capture old state"_string)); + // and return. + return; + } + + // 4. Set document’s rendering suppression for view transitions to true. + document.set_rendering_suppression_for_view_transitions(true); + + // 5. Queue a global task on the DOM manipulation task source, given transition’s relevant global object, to + // perform the following steps: + HTML::queue_global_task(HTML::Task::Source::DOMManipulation, HTML::relevant_global_object(*this), GC::create_function(realm.heap(), [&] { + HTML::TemporaryExecutionContext context(realm); + // 1. If transition’s phase is "done", then abort these steps. + if (m_phase == Phase::Done) + return; + + // 2. schedule the update callback for transition. + schedule_the_update_callback(); + + // 3. Flush the update callback queue. + // AD-HOC: Spec doesn't say what document to flush it for. + // Lets just use the one we have. + // (see https://github.com/w3c/csswg-drafts/issues/11986 ) + // Also, scheduling the update callback should already do this, see https://github.com/w3c/csswg-drafts/issues/11987 + document.flush_the_update_callback_queue(); + })); +} + +// https://drafts.csswg.org/css-view-transitions-1/#activate-view-transition +void ViewTransition::activate_view_transition() +{ + auto& realm = this->realm(); + // To activate view transition for a ViewTransition transition, perform the following steps: + + // 1. If transition’s phase is "done", then return. + // NOTE: This happens if transition was skipped before this point. + if (m_phase == Phase::Done) + return; + + // 2. Set transition’s relevant global object’s associated document’s rendering suppression for view transitions to + // false. + auto& document = as(HTML::relevant_global_object(*this)).associated_document(); + document.set_rendering_suppression_for_view_transitions(false); + + // 3. If transition’s initial snapshot containing block size is not equal to the snapshot containing block size, then + // skip transition with an "InvalidStateError" DOMException in transition’s relevant Realm, and return. + auto snapshot_containing_block_size = document.navigable()->snapshot_containing_block_size(); + if (!m_initial_snapshot_containing_block_size.has_value() || m_initial_snapshot_containing_block_size.value().get<0>() != snapshot_containing_block_size.get<0>() || m_initial_snapshot_containing_block_size.value().get<1>() != snapshot_containing_block_size.get<1>()) { + skip_the_view_transition(WebIDL::InvalidStateError::create(realm, "Transition's initial snapshot containing block size is not equal to the snapshot containing block size"_string)); + return; + } + + // 4. Capture the new state for transition. + auto result = capture_the_new_state(); + // If failure is returned, + if (result.is_error()) { + // then skip the view transition for transition with an "InvalidStateError" DOMException in transition’s relevant Realm, + skip_the_view_transition(WebIDL::AbortError::create(realm, "Failed to capture old state"_string)); + // and return. + return; + } + + // 5. For each capturedElement of transition’s named elements' values: + for (auto captured_element : m_named_elements) { + // 1. If capturedElement’s new element is not null, then set capturedElement’s new element’s captured in a + // view transition to true. + if (captured_element.value->new_element) { + captured_element.value->new_element->set_captured_in_a_view_transition(true); + } + } + + // 6. Setup transition pseudo-elements for transition. + setup_transition_pseudo_elements(); + + // 7. Update pseudo-element styles for transition. + result = update_pseudo_element_styles(); + // If failure is returned, + if (result.is_error()) { + // then skip the view transition for transition with an "InvalidStateError" DOMException in transition’s relevant Realm, + skip_the_view_transition(WebIDL::AbortError::create(realm, "Failed to capture old state"_string)); + // and return. + return; + } + // NOTE: The above steps will require running document lifecycle phases, to compute information + // calculated during style/layout. + // FIXME: Figure out what this entails. + + // 8. Set transition’s phase to "animating". + m_phase = Phase::Animating; + + // 9. Resolve transition’s ready promise. + WebIDL::resolve_promise(realm, m_ready_promise); +} + +// https://drafts.csswg.org/css-view-transitions-1/#capture-the-old-state +ErrorOr ViewTransition::capture_the_old_state() +{ + // To capture the old state for ViewTransition transition: + + // 1. Let document be transition’s relevant global object’s associated document. + auto& document = as(HTML::relevant_global_object(*this)).associated_document(); + + // 2. Let namedElements be transition’s named elements. + auto named_elements = m_named_elements; + + // 3. Let usedTransitionNames be a new set of strings. + auto used_transition_names = AK::OrderedHashTable(); + + // 4. Let captureElements be a new list of elements. + auto capture_elements = AK::Vector(); + + // 5. If the snapshot containing block size exceeds an implementation-defined maximum, then return failure. + auto snapshot_containing_block = document.navigable()->snapshot_containing_block(); + if (snapshot_containing_block.width() > NumericLimits::max() || snapshot_containing_block.height() > NumericLimits::max()) + return ErrorOr {}; + + // 6. Set transition’s initial snapshot containing block size to the snapshot containing block size. + m_initial_snapshot_containing_block_size = Tuple { snapshot_containing_block.width(), snapshot_containing_block.height() }; + + // 7. For each element of every element that is connected, and has a node document equal to document, in paint + // order: + // FIXME: Actually do this in paint order + auto result = document.document_element()->for_each_in_inclusive_subtree_of_type([&](auto& element) { + // 1. If any flat tree ancestor of this element skips its contents, then continue. + if (element.skips_its_contents()) + return TraversalDecision::SkipChildrenAndContinue; + + // 2. If element has more than one box fragment, then continue. + // FIXME: Implement this once we have fragments. + + // 3. Let transitionName be the element’s document-scoped view transition name. + auto transition_name = element.document_scoped_view_transition_name(); + + // 4. If transitionName is none, or element is not rendered, then continue. + if (!transition_name.has_value() || element.not_rendered()) + return TraversalDecision::Continue; + + // 5. If usedTransitionNames contains transitionName, then: + if (used_transition_names.contains(transition_name.value())) { + // 1. For each element in captureElements: + for (auto& element : capture_elements) + element.set_captured_in_a_view_transition(false); + + // 2. Return failure + return TraversalDecision::Break; + } + + // 6. Append transitionName to usedTransitionNames. + used_transition_names.set(transition_name.value()); + + // 7. Set element’s captured in a view transition to true. + element.set_captured_in_a_view_transition(true); + + // 8. Append element to captureElements. + capture_elements.append(element); + + return TraversalDecision::Continue; + }); + if (result == TraversalDecision::Break) + return ErrorOr {}; + + // 8. For each element in captureElements: + for (auto& element : capture_elements) { + // 1. Let capture be a new captured element struct. + CapturedElement capture = {}; + + // 2. Set capture’s old image to the result of capturing the image of element. + capture.old_image = element.capture_the_image(); + + // 3. Let originalRect be snapshot containing block if element is the document element, otherwise, the + // element's border box. + auto original_rect = element.is_document_element() ? element.navigable()->snapshot_containing_block() : element.paintable_box()->absolute_border_box_rect(); + + // 4. Set capture’s old width to originalRect’s width. + capture.old_width = original_rect.width(); + + // 5. Set capture’s old height to originalRect’s height. + capture.old_height = original_rect.height(); + + // 6. Set capture’s old transform to a that would map element’s border box from the + // snapshot containing block origin to its current visual position. + // FIXME: Actually compute the right offset here. + capture.old_transform = CSS::Transformation(CSS::TransformFunction::Translate, Vector({ CSS::TransformValue(CSS::Length(0, CSS::Length::Type::Px)), CSS::TransformValue(CSS::Length(0, CSS::Length::Type::Px)) })); + + // 7. Set capture’s old writing-mode to the computed value of writing-mode on element. + capture.old_writing_mode = element.layout_node()->computed_values().writing_mode(); + + // 8. Set capture’s old direction to the computed value of direction on element. + capture.old_direction = element.layout_node()->computed_values().direction(); + + // 9. Set capture’s old text-orientation to the computed value of text-orientation on element. + // FIXME: Implement this once we have text-orientation. + + // 10. Set capture’s old mix-blend-mode to the computed value of mix-blend-mode on element. + capture.old_mix_blend_mode = element.layout_node()->computed_values().mix_blend_mode(); + + // 11. Set capture’s old backdrop-filter to the computed value of backdrop-filter on element. + capture.old_backdrop_filter = element.layout_node()->computed_values().backdrop_filter(); + + // 12. Set capture’s old color-scheme to the computed value of color-scheme on element. + capture.old_color_scheme = element.layout_node()->computed_values().color_scheme(); + + // 13. Let transitionName be the computed value of view-transition-name for element. + auto transition_name = element.layout_node()->computed_values().view_transition_name(); + + // 14. Set namedElements[transitionName] to capture. + named_elements.set(transition_name.value(), &capture); + } + + // 9. For each element in captureElements: + for (auto& element : capture_elements) { + // 1. Set element’s captured in a view transition to false. + element.set_captured_in_a_view_transition(false); + } + + return {}; +} + +// https://drafts.csswg.org/css-view-transitions-1/#capture-the-new-state +ErrorOr ViewTransition::capture_the_new_state() +{ + // To capture the new state for ViewTransition transition: + + // 1. Let document be transition’s relevant global object’s associated document. + auto& document = as(HTML::relevant_global_object(*this)).associated_document(); + + // 2. Let namedElements be transition’s named elements. + // NOTE: We just use m_named_elements + + // 3. Let usedTransitionNames be a new set of strings. + auto used_transition_names = AK::OrderedHashTable(); + + // 4. For each element of every element that is connected, and has a node document equal to document, in paint + // order: + // FIXME: Actually do this in paint order + auto result = document.document_element()->for_each_in_inclusive_subtree_of_type([&](auto& element) { + // 1. If any flat tree ancestor of this element skips its contents, then continue. + if (element.skips_its_contents()) + return TraversalDecision::SkipChildrenAndContinue; + + // 2. Let transitionName be the element’s document-scoped view transition name. + auto transition_name = element.document_scoped_view_transition_name(); + + // 3. If transitionName is none, or element is not rendered, then continue. + if (!transition_name.has_value() || !element.not_rendered()) + return TraversalDecision::Continue; + + // 4. If element has more than one box fragment, then continue. + // FIXME: Implement this once we have fragments + + // 5. If usedTransitionNames contains transitionName, then return failure. + if (used_transition_names.contains(transition_name.value())) + return TraversalDecision::Break; + + // 6. Append transitionName to usedTransitionNames. + used_transition_names.set(transition_name.value()); + + // 7. If namedElements[transitionName] does not exist, then set namedElements[transitionName] to a new captured element struct. + if (!m_named_elements.contains(transition_name.value())) { + CapturedElement captured_element = {}; + m_named_elements.set(transition_name.value(), &captured_element); + } + + // 8. Set namedElements[transitionName]'s new element to element. + m_named_elements.get(transition_name.value()).value()->new_element = &element; + + return TraversalDecision::Continue; + }); + if (result == TraversalDecision::Break) + return ErrorOr {}; + + return {}; +} + +// https://drafts.csswg.org/css-view-transitions-1/#setup-transition-pseudo-elements +void ViewTransition::setup_transition_pseudo_elements() +{ + // To setup transition pseudo-elements for a ViewTransition transition: + + // 1. Let document be this’s relevant global object’s associated document. + auto& document = as(HTML::relevant_global_object(*this)).associated_document(); + + // 2. Set document’s show view transition tree to true. + document.set_show_view_transition_tree(true); + // Note: stylesheet is not a variable in the spec but ends up being referenced a lot in this algorithm. + auto stylesheet = document.dynamic_view_transition_style_sheet(); + + // 3. For each transitionName → capturedElement of transition’s named elements: + for (auto [transition_name, captured_element] : m_named_elements) { + // 1. Let group be a new '::view-transition-group()', with its view transition name set to transitionName. + // FIXME: + + // 2. Append group to transition’s transition root pseudo-element. + // FIXME: + + // 3. Let imagePair be a new '::view-transition-image-pair()', with its view transition name set to + // transitionName. + // FIXME: + + // 4. Append imagePair to group. + // FIXME: + + // 5. If capturedElement’s old image is not null, then: + if (captured_element->old_image) { + // 1. Let old be a new '::view-transition-old()', with its view transition name set to transitionName, + // displaying capturedElement’s old image as its replaced content. + // FIXME: + + // 2. Append old to imagePair. + // FIXME: + } + + // 6. If capturedElement’s new element is not null, then: + if (captured_element->new_element) { + // 1. Let new be a new ::view-transition-new(), with its view transition name set to transitionName. + // NOTE: The styling of this pseudo is handled in update pseudo-element styles. + // FIXME: + + // 2. Append new to imagePair. + // FIXME: + } + + // 7. If capturedElement’s old image is null, then: + if (!captured_element->old_image) { + // 1. Assert: capturedElement’s new element is not null. + VERIFY(captured_element->new_element != nullptr); + + // 2. Set capturedElement’s image animation name rule to a new CSSStyleRule representing the + // following CSS, and append it to document’s dynamic view transition style sheet: + // :root::view-transition-new(transitionName) { + // animation-name: -ua-view-transition-fade-in; + // } + // NOTE: The above code example contains variables to be replaced. + unsigned index = MUST(stylesheet->insert_rule(MUST(String::formatted(R"( + :root::view-transition-new({}) {{ + animation-name: -ua-view-transition-fade-in; + }} + )", + transition_name)), + 0)); + captured_element->image_animation_name_rule = as(stylesheet->css_rules()->item(index)); + } + + // 8. If capturedElement’s new element is null, then: + if (!captured_element->new_element) { + // 1. Assert: capturedElement’s old image is not null. + VERIFY(captured_element->old_image); + + // 2. Set capturedElement’s image animation name rule to a new CSSStyleRule representing the + // following CSS, and append it to document’s dynamic view transition style sheet: + // :root::view-transition-old(transitionName) { + // animation-name: -ua-view-transition-fade-out; + // } + // NOTE: The above code example contains variables to be replaced. + unsigned index = MUST(stylesheet->insert_rule(MUST(String::formatted(R"( + :root::view-transition-old({}) {{ + animation-name: -ua-view-transition-fade-out; + }} + )", + transition_name)), + 0)); + captured_element->image_animation_name_rule = as(stylesheet->css_rules()->item(index)); + } + + // 9. If both of capturedElement’s old image and new element are not null, then: + if (captured_element->old_image && captured_element->new_element) { + // 1. Let transform be capturedElement’s old transform. + auto transform = captured_element->old_transform; + + // 2. Let width be capturedElement’s old width. + auto width = captured_element->old_width; + + // 3. Let height be capturedElement’s old height. + auto height = captured_element->old_height; + + // 4. Let backdropFilter be capturedElement’s old backdrop-filter. + auto backdrop_filter = captured_element->old_backdrop_filter; + + // 5. Set capturedElement’s group keyframes to a new CSSKeyframesRule representing the following + // CSS, and append it to document’s dynamic view transition style sheet: + // @keyframes -ua-view-transition-group-anim-transitionName { + // from { + // transform: transform; + // width: width; + // height: height; + // backdrop-filter: backdropFilter; + // } + // } + // NOTE: The above code example contains variables to be replaced. + unsigned index = MUST(stylesheet->insert_rule(MUST(String::formatted(R"( + @keyframes -ua-view-transition-group-anim-{} {{ + from {{ + transform: {}; + width: {}; + height: {}; + backdrop-filter: {}; + }} + }} + )", + transition_name, "transform", width, height, "backdrop_filter")), + index)); + // FIXME: all the strings above should be the identically named variables + captured_element->group_keyframes = as(stylesheet->css_rules()->item(0)); + + // 6. Set capturedElement’s group animation name rule to a new CSSStyleRule representing the + // following CSS, and append it to document’s dynamic view transition style sheet: + // :root::view-transition-group(transitionName) { + // animation-name: -ua-view-transition-group-anim-transitionName; + // } + // NOTE: The above code example contains variables to be replaced. + index = MUST(stylesheet->insert_rule(MUST(String::formatted(R"( + :root::view-transition-group({}) {{ + animation-name: -ua-view-transition-group-anim-{}; + }} + )", + transition_name, transition_name)), + 0)); + captured_element->group_animation_name_rule = as(stylesheet->css_rules()->item(index)); + + // 7. Set capturedElement’s image pair isolation rule to a new CSSStyleRule representing the + // following CSS, and append it to document’s dynamic view transition style sheet: + // :root::view-transition-image-pair(transitionName) { + // isolation: isolate; + // } + // NOTE: The above code example contains variables to be replaced. + index = MUST(stylesheet->insert_rule(MUST(String::formatted(R"( + :root::view-transition-image-pair({}) {{ + isolation: isolate; + }} + )", + transition_name)), + 0)); + captured_element->image_pair_isolation_rule = as(stylesheet->css_rules()->item(index)); + + // 8. Set capturedElement’s image animation name rule to a new CSSStyleRule representing the + // following CSS, and append it to document’s dynamic view transition style sheet: + // :root::view-transition-old(transitionName) { + // animation-name: -ua-view-transition-fade-out, -ua-mix-blend-mode-plus-lighter; + // } + // :root::view-transition-new(transitionName) { + // animation-name: -ua-view-transition-fade-in, -ua-mix-blend-mode-plus-lighter; + // } + // NOTE: The above code example contains variables to be replaced. + // NOTE: mix-blend-mode: plus-lighter ensures that the blending of identical pixels from the + // old and new images results in the same color value as those pixels, and achieves a “correct” + // cross-fade. + index = MUST(stylesheet->insert_rule(MUST(String::formatted(R"( + :root::view-transition-old({}) {{ + animation-name: -ua-view-transition-fade-out, -ua-mix-blend-mode-plus-lighter; + }} + :root::view-transition-new({}) {{ + animation-name: -ua-view-transition-fade-in, -ua-mix-blend-mode-plus-lighter; + }} + )", + transition_name, transition_name)), + 0)); + captured_element->image_animation_name_rule = as(stylesheet->css_rules()->item(index)); + } + } +} + +// https://drafts.csswg.org/css-view-transitions-1/#call-the-update-callback +void ViewTransition::call_the_update_callback() +{ + auto& realm = this->realm(); + // To call the update callback of a ViewTransition transition: + + // 1. Assert: transition’s phase is "done", or before "update-callback-called". + VERIFY(m_phase == Phase::Done || m_phase == Phase::PendingCapture); + + // 2. If transition’s phase is not "done", then set transition’s phase to "update-callback-called". + if (m_phase != Phase::Done) + m_phase = Phase::UpdateCallbackCalled; + + // 3. Let callbackPromise be null. + WebIDL::Promise* callback_promise; + + // 4. If transition’s update callback is null, then set callbackPromise to a promise resolved with undefined, in + // transition’s relevant Realm. + if (!m_update_callback) { + auto& relevant_realm = HTML::relevant_realm(*this); + callback_promise = WebIDL::create_promise(relevant_realm); + WebIDL::resolve_promise(relevant_realm, *callback_promise, JS::js_undefined()); + } + + // 5. Otherwise, set callbackPromise to the result of invoking transition’s update callback. + else { + dbgln("HELLO!"); + auto promise = MUST(WebIDL::invoke_callback(*m_update_callback, {})).value(); + // FIXME: since WebIDL::invoke_callback does not yet convert the value for us, + // We need to do it here manually. + // https://webidl.spec.whatwg.org/#js-promise + + // 1. Let promiseCapability be ? NewPromiseCapability(%Promise%). + auto promise_capability = WebIDL::create_promise(realm); + // 2. Perform ? Call(promiseCapability.[[Resolve]], undefined, « V »). + MUST(JS::call(realm.vm(), *promise_capability->resolve(), JS::js_undefined(), promise)); + // 3. Return promiseCapability. + callback_promise = GC::make_root(promise_capability); + } + + // 6. Let fulfillSteps be to following steps: + auto fulfill_steps = GC::create_function(realm.heap(), [this, &realm](JS::Value) -> WebIDL::ExceptionOr { + // 1. Resolve transition’s update callback done promise with undefined. + WebIDL::resolve_promise(realm, m_update_callback_done_promise, JS::js_undefined()); + + // 2. Activate transition. + activate_view_transition(); + + return JS::js_undefined(); + }); + + // 7. Let rejectSteps be the following steps given reason: + auto reject_steps = GC::create_function(realm.heap(), [this, &realm](JS::Value reason) -> WebIDL::ExceptionOr { + // 1. Reject transition’s update callback done promise with reason. + WebIDL::reject_promise(realm, m_update_callback_done_promise, reason); + + // 2. If transition’s phase is "done", then return. + // NOTE: This happens if transition was skipped before this point. + if (m_phase == Phase::Done) + return JS::js_undefined(); + + // 3. Mark as handled transition’s ready promise. + // NOTE: transition’s update callback done promise will provide the unhandledrejection. This + // step avoids a duplicate. + WebIDL::mark_promise_as_handled(m_update_callback_done_promise); + + // 4. Skip the view transition transition with reason. + skip_the_view_transition(reason); + + return JS::js_undefined(); + }); + + // 8. React to callbackPromise with fulfillSteps and rejectSteps. + // AD-HOC: This can cause an assertion failure when the reaction algorithm ends up accessing the incumbent realm, which may not exist here. + // For now, lets just manually push something onto the incumbent realm stack here as a hack. + // A spec bug for this has been filed at https://github.com/w3c/csswg-drafts/issues/11990 + HTML::main_thread_event_loop().push_onto_backup_incumbent_realm_stack(realm); + WebIDL::react_to_promise(*callback_promise, fulfill_steps, reject_steps); + HTML::main_thread_event_loop().pop_backup_incumbent_realm_stack(); + + // 9. To skip a transition after a timeout, the user agent may perform the following steps in parallel: + // FIXME: Figure out if we want to do this. +} + +// https://drafts.csswg.org/css-view-transitions-1/#schedule-the-update-callback +void ViewTransition::schedule_the_update_callback() +{ + // To schedule the update callback given a ViewTransition transition: + + // 1. Append transition to transition’s relevant settings object’s update callback queue. + // AD-HOC: The update callback queue is a property on document, not a settings object. + // For now we'll just put it on the relevant global object's associated document. + // Spec bug is filed at https://github.com/w3c/csswg-drafts/issues/11986 + as(HTML::relevant_global_object(*this)).associated_document().update_callback_queue().append(this); + + // 2. Queue a global task on the DOM manipulation task source, given transition’s relevant global object, to flush + // the update callback queue. + HTML::queue_global_task(HTML::Task::Source::DOMManipulation, HTML::relevant_global_object(*this), GC::create_function(realm().heap(), [&] { + // AD-HOC: Spec doesn't say what document to flush it for. + // Lets just use the one we use elsewhere. + // (see https://github.com/w3c/csswg-drafts/issues/11986 ) + as(HTML::relevant_global_object(*this)).associated_document().flush_the_update_callback_queue(); + })); +} + +// https://drafts.csswg.org/css-view-transitions-1/#skip-the-view-transition +void ViewTransition::skip_the_view_transition(JS::Value reason) +{ + auto& realm = this->realm(); + // To skip the view transition for ViewTransition transition with reason reason: + + // 1. Let document be transition’s relevant global object’s associated document. + auto& document = as(HTML::relevant_global_object(*this)).associated_document(); + + // 2. Assert: transition’s phase is not "done". + VERIFY(m_phase != Phase::Done); + + // 3. If transition’s phase is before "update-callback-called", then schedule the update callback for transition. + if (m_phase == Phase::PendingCapture) { + schedule_the_update_callback(); + } + + // 4. Set rendering suppression for view transitions to false. + document.set_rendering_suppression_for_view_transitions(false); + + // 5. If document’s active view transition is transition, Clear view transition transition. + if (document.active_view_transition() == this) + clear_view_transition(); + + // 6. Set transition’s phase to "done". + m_phase = Phase::Done; + + // 7. Reject transition’s ready promise with reason. + WebIDL::reject_promise(realm, m_ready_promise, reason); + + // 8. Resolve transition’s finished promise with the result of reacting to transition’s update callback done promise: + // - If the promise was fulfilled, then return undefined. + // AD-HOC: This can cause an assertion failure when the reaction algorithm ends up accessing the incumbent realm, which may not exist here. + // For now, lets just manually push something onto the incumbent realm stack here as a hack. + // A spec bug for this has been filed at https://github.com/w3c/csswg-drafts/issues/11990 + HTML::main_thread_event_loop().push_onto_backup_incumbent_realm_stack(realm); + WebIDL::resolve_promise(realm, m_finished_promise, WebIDL::react_to_promise(m_update_callback_done_promise, GC::create_function(realm.heap(), [](JS::Value) -> WebIDL::ExceptionOr { return JS::js_undefined(); }), nullptr)->promise()); + HTML::main_thread_event_loop().pop_backup_incumbent_realm_stack(); +} + +// https://drafts.csswg.org/css-view-transitions-1/#handle-transition-frame +void ViewTransition::handle_transition_frame() +{ + auto& realm = this->realm(); + // To handle transition frame given a ViewTransition transition + + // 1. Let document be transition’s relevant global object’s associated document. + auto& document = as(HTML::relevant_global_object(*this)).associated_document(); + + // 2. Let hasActiveAnimations be a boolean, initially false. + bool has_active_animations = false; + + // 3. For each element of transition’s transition root pseudo-element’s inclusive descendants: + // FIXME: + + // 4. If hasActiveAnimations is false: + if (!has_active_animations) { + // 1. Set transition’s phase to "done". + m_phase = Phase::Done; + + // 2. Clear view transition transition. + clear_view_transition(); + + // 3. Resolve transition’s finished promise. + WebIDL::resolve_promise(realm, m_finished_promise); + + // 4. Return. + return; + } + + // 5. If transition’s initial snapshot containing block size is not equal to the snapshot containing block size, + auto snapshot_containing_block_size = document.navigable()->snapshot_containing_block_size(); + if (!m_initial_snapshot_containing_block_size.has_value() || m_initial_snapshot_containing_block_size.value().get<0>() != snapshot_containing_block_size.get<0>() || m_initial_snapshot_containing_block_size.value().get<1>() != snapshot_containing_block_size.get<1>()) { + // then skip the view transition for transition with an "InvalidStateError" DOMException in transition’s relevant Realm, + skip_the_view_transition(WebIDL::InvalidStateError::create(realm, "Transition's initial snapshot containing block size is not equal to the snapshot containing block size"_string)); + // and return. + return; + } + + // 6. Update pseudo-element styles for transition. + auto result = update_pseudo_element_styles(); + // If failure is returned, + if (result.is_error()) { + // then skip the view transition for transition with an "InvalidStateError" DOMException in transition’s relevant Realm, + skip_the_view_transition(WebIDL::AbortError::create(realm, "Failed to capture old state"_string)); + // and return. + return; + } +} + +// https://drafts.csswg.org/css-view-transitions-1/#update-pseudo-element-styles +ErrorOr ViewTransition::update_pseudo_element_styles() +{ + // To update pseudo-element styles for a ViewTransition transition: + + // 1. For each transitionName → capturedElement of transition’s named elements: + for (auto [transition_name, captured_element] : m_named_elements) { + // 1. Let width, height, transform, writingMode, direction, textOrientation, mixBlendMode, backdropFilter and + // colorScheme be null. + Optional width = {}; + Optional height = {}; + Optional transform = {}; + Optional writing_mode = {}; + Optional direction = {}; + // FIXME: text_orientation + Optional mix_blend_mode = {}; + Optional> backdrop_filter = {}; + Optional color_scheme = {}; + + // 2. If capturedElement’s new element is null, then: + if (!captured_element->new_element) { + // 1. Set width to capturedElement’s old width. + width = captured_element->old_width; + + // 2. Set height to capturedElement’s old height. + height = captured_element->old_height; + + // 3. Set transform to capturedElement’s old transform. + transform = captured_element->old_transform; + + // 4. Set writingMode to capturedElement’s old writing-mode. + writing_mode = captured_element->old_writing_mode; + + // 5. Set direction to capturedElement’s old direction. + direction = captured_element->old_direction; + + // 6. Set textOrientation to capturedElement’s old text-orientation. + // FIXME: Implement this once we have text-orientation. + + // 7. Set mixBlendMode to capturedElement’s old mix-blend-mode. + mix_blend_mode = captured_element->old_mix_blend_mode; + + // 8. Set backdropFilter to capturedElement’s old backdrop-filter. + backdrop_filter = captured_element->old_backdrop_filter; + + // 9. Set colorScheme to capturedElement’s old color-scheme. + color_scheme = captured_element->old_color_scheme; + } + + // 3. Otherwise: + else { + // 1. Return failure if any of the following conditions is true: + + // - capturedElement’s new element has a flat tree ancestor that skips its contents. + for (auto* ancestor = captured_element->new_element->parent(); ancestor; ancestor = ancestor->parent()) { + if (as(*ancestor).skips_its_contents()) + return ErrorOr {}; + } + + // - capturedElement’s new element is not rendered. + if (captured_element->new_element && captured_element->new_element->not_rendered()) + return ErrorOr {}; + + // - capturedElement has more than one box fragment. + // FIXME: Implement this once we have fragments. + // FIXME: capturedElement would not have box fragments. Update this once the spec issue for that has been resolved: + // https://github.com/w3c/csswg-drafts/issues/11991 + + // NOTE: Other rendering constraints are enforced via capturedElement’s new element being + // captured in a view transition. + + // 2. Let newRect be the snapshot containing block if capturedElement’s new element is the + // document element, otherwise, capturedElement’s border box. + auto new_rect = captured_element->new_element->is_document_element() ? captured_element->new_element->navigable()->snapshot_containing_block() : captured_element->new_element->paintable_box()->absolute_border_box_rect(); + + // 3. Set width to the current width of newRect. + width = new_rect.width(); + + // 4. Set height to the current height of newRect. + height = new_rect.height(); + + // 5. Set transform to a transform that would map newRect from the snapshot containing block origin + // to its current visual position. + // FIXME: Actually compute the right offset here. + transform = CSS::Transformation(CSS::TransformFunction::Translate, Vector({ CSS::TransformValue(CSS::Length(0, CSS::Length::Type::Px)), CSS::TransformValue(CSS::Length(0, CSS::Length::Type::Px)) })); + + // 6. Set writingMode to the computed value of writing-mode on capturedElement’s new element. + writing_mode = captured_element->new_element->layout_node()->computed_values().writing_mode(); + + // 7. Set direction to the computed value of direction on capturedElement’s new element. + direction = captured_element->new_element->layout_node()->computed_values().direction(); + + // 8. Set textOrientation to the computed value of text-orientation on capturedElement’s new + // element. + // FIXME: Implement this. + + // 9. Set mixBlendMode to the computed value of mix-blend-mode on capturedElement’s new + // element. + mix_blend_mode = captured_element->new_element->layout_node()->computed_values().mix_blend_mode(); + + // 10. Set backdropFilter to the computed value of backdrop-filter on capturedElement’s new element. + backdrop_filter = captured_element->new_element->layout_node()->computed_values().backdrop_filter(); + + // 11. Set colorScheme to the computed value of color-scheme on capturedElement’s new element. + color_scheme = captured_element->new_element->layout_node()->computed_values().color_scheme(); + } + + // 4. If capturedElement’s group styles rule is null, then set capturedElement’s group styles rule to a new + // CSSStyleRule representing the following CSS, and append it to transition’s relevant global object’s + // associated document’s dynamic view transition style sheet. + if (!captured_element->group_styles_rule) { + // :root::view-transition-group(transitionName) { + // width: width; + // height: height; + // transform: transform; + // writing-mode: writingMode; + // direction: direction; + // text-orientation: textOrientation; + // mix-blend-mode: mixBlendMode; + // backdrop-filter: backdropFilter; + // color-scheme: colorScheme; + // } + // NOTE: The above code example contains variables to be replaced. + auto stylesheet = as(HTML::relevant_global_object(*this)).associated_document().dynamic_view_transition_style_sheet(); + unsigned index = MUST(stylesheet->insert_rule(MUST(String::formatted(R"( + :root::view-transition-group({}) {{ + width: {}; + height: {}; + transform: {}; + writing-mode: {}; + direction: {}; + text-orientation: {}; + mix-blend-mode: {}; + backdrop-filter: {}; + color-scheme: {}; + }} + )", + transition_name, width, height, "transform", "writing_mode", "direction", "text_orientation", "mix_blend_mode", "backdrop_filter", "color_scheme")), + 0)); + // FIXME: all the strings above should be the identically named variables + captured_element->group_styles_rule = as(stylesheet->css_rules()->item(index)); + } + // Otherwise, update capturedElement’s group styles rule to match the following CSS: + // :root::view-transition-group(transitionName) { + // width: width; + // height: height; + // transform: transform; + // writing-mode: writingMode; + // direction: direction; + // text-orientation: textOrientation; + // mix-blend-mode: mixBlendMode; + // backdrop-filter: backdropFilter; + // color-scheme: colorScheme; + // } + // NOTE: The above code example contains variables to be replaced. + else { + // FIXME: + } + + // 5. If capturedElement’s new element is not null, then: + if (captured_element->new_element) { + // 1. Let new be the ::view-transition-new() with the view transition name transitionName. + // FIXME: + + // 2. Set new’s replaced element content to the result of capturing the image of capturedElement’s + // new element. + // FIXME: + } + } + + // This algorithm must be executed to update styles in user-agent origin if its effects can be observed by a web API. + // FIXME: Find all the places where this is relevant. + + return {}; +} + +// https://drafts.csswg.org/css-view-transitions-1/#clear-view-transition +void ViewTransition::clear_view_transition() +{ + // To clear view transition of a ViewTransition transition: + + // 1. Let document be transition’s relevant global object’s associated document. + auto& document = as(HTML::relevant_global_object(*this)).associated_document(); + + // 2. Assert: document’s active view transition is transition. + VERIFY(document.active_view_transition() == this); + + // 3. For each capturedElement of transition’s named elements' values: + for (auto captured_element : m_named_elements) { + // 1. If capturedElement’s new element is not null, then set capturedElement’s new element's captured in a + // view transition to false. + if (captured_element.value->new_element) { + captured_element.value->new_element->set_captured_in_a_view_transition(false); + } + + // 2. For each style of capturedElement’s style definitions: + auto steps = [&](CSS::CSSRule* style) { + // 1. If style is not null, and style is in document’s dynamic view transition style sheet, then remove + // style from document’s dynamic view transition style sheet. + if (style) { + auto stylesheet = document.dynamic_view_transition_style_sheet(); + auto rules = stylesheet->css_rules(); + for (u32 i = 0; i < rules->length(); i++) { + if (rules->item(i) == style) { + MUST(stylesheet->delete_rule(i)); + break; + } + } + } + }; + + steps(captured_element.value->group_keyframes); + steps(captured_element.value->group_animation_name_rule); + steps(captured_element.value->group_styles_rule); + steps(captured_element.value->image_pair_isolation_rule); + steps(captured_element.value->image_animation_name_rule); + } + + // 4. Set document’s show view transition tree to false. + document.set_show_view_transition_tree(false); + + // 5. Set document’s active view transition to null. + document.set_active_view_transition(nullptr); +} + +} diff --git a/Libraries/LibWeb/ViewTransition/ViewTransition.h b/Libraries/LibWeb/ViewTransition/ViewTransition.h new file mode 100644 index 00000000000..65e9b68e26b --- /dev/null +++ b/Libraries/LibWeb/ViewTransition/ViewTransition.h @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2025, Psychpsyo + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Web::ViewTransition { + +// https://drafts.csswg.org/css-view-transitions-1/#captured-element +struct CapturedElement : public JS::Cell { + GC_CELL(CapturedElement, JS::Cell) + GC_DECLARE_ALLOCATOR(CapturedElement); + + RefPtr old_image = {}; + CSSPixels old_width = 0; + CSSPixels old_height = 0; + // FIXME: Make this an identity transform function by default. + CSS::Transformation old_transform = CSS::Transformation(CSS::TransformFunction::Translate, Vector()); + Optional old_writing_mode = {}; + Optional old_direction = {}; + // FIXME: old_text_orientation + Optional old_mix_blend_mode = {}; + Optional> old_backdrop_filter = {}; + Optional old_color_scheme = {}; + DOM::Element* new_element = nullptr; + + CSS::CSSKeyframesRule* group_keyframes = nullptr; + CSS::CSSStyleRule* group_animation_name_rule = nullptr; + CSS::CSSStyleRule* group_styles_rule = nullptr; + CSS::CSSStyleRule* image_pair_isolation_rule = nullptr; + CSS::CSSStyleRule* image_animation_name_rule = nullptr; + +private: + virtual void visit_edges(JS::Cell::Visitor&) override; +}; + +// https://drafts.csswg.org/css-view-transitions-1/#callbackdef-viewtransitionupdatecallback +using ViewTransitionUpdateCallback = GC::Ptr; + +class ViewTransition final : public Bindings::PlatformObject { + WEB_PLATFORM_OBJECT(ViewTransition, Bindings::PlatformObject); + GC_DECLARE_ALLOCATOR(ViewTransition); + +public: + static GC::Ref create(JS::Realm&); + virtual ~ViewTransition() override = default; + + // https://drafts.csswg.org/css-view-transitions-1/#dom-viewtransition-updatecallbackdone + GC::Ref update_callback_done() const { return m_update_callback_done_promise; } + // https://drafts.csswg.org/css-view-transitions-1/#dom-viewtransition-ready + GC::Ref ready() const { return m_ready_promise; } + // https://drafts.csswg.org/css-view-transitions-1/#dom-viewtransition-finished + GC::Ref finished() const { return m_finished_promise; } + // https://drafts.csswg.org/css-view-transitions-1/#dom-viewtransition-skiptransition + void skip_transition(); + + // https://drafts.csswg.org/css-view-transitions-1/#setup-view-transition + void setup_view_transition(); + + // https://drafts.csswg.org/css-view-transitions-1/#activate-view-transition + void activate_view_transition(); + + // https://drafts.csswg.org/css-view-transitions-1/#capture-the-old-state + ErrorOr capture_the_old_state(); + + // https://drafts.csswg.org/css-view-transitions-1/#capture-the-new-state + ErrorOr capture_the_new_state(); + + // https://drafts.csswg.org/css-view-transitions-1/#setup-transition-pseudo-elements + void setup_transition_pseudo_elements(); + + // https://drafts.csswg.org/css-view-transitions-1/#call-the-update-callback + void call_the_update_callback(); + + // https://drafts.csswg.org/css-view-transitions-1/#schedule-the-update-callback + void schedule_the_update_callback(); + + // https://drafts.csswg.org/css-view-transitions-1/#skip-the-view-transition + void skip_the_view_transition(JS::Value reason); + + // https://drafts.csswg.org/css-view-transitions-1/#handle-transition-frame + void handle_transition_frame(); + + // https://drafts.csswg.org/css-view-transitions-1/#update-pseudo-element-styles + ErrorOr update_pseudo_element_styles(); + + // https://drafts.csswg.org/css-view-transitions-1/#clear-view-transition + void clear_view_transition(); + + // https://drafts.csswg.org/css-view-transitions-1/#viewtransition-phase + enum Phase { + PendingCapture, + UpdateCallbackCalled, + Animating, + Done, + }; + Phase phase() const { return m_phase; } + void set_update_callback(ViewTransitionUpdateCallback callback) { m_update_callback = callback; } + +private: + ViewTransition(JS::Realm&, GC::Ref, GC::Ref, GC::Ref); + virtual void initialize(JS::Realm&) override; + + virtual void visit_edges(JS::Cell::Visitor&) override; + + // https://drafts.csswg.org/css-view-transitions-1/#viewtransition-named-elements + HashMap m_named_elements = {}; + + // https://drafts.csswg.org/css-view-transitions-1/#viewtransition-phase + Phase m_phase = Phase::PendingCapture; + + // https://drafts.csswg.org/css-view-transitions-1/#viewtransition-update-callback + ViewTransitionUpdateCallback m_update_callback = {}; + + // https://drafts.csswg.org/css-view-transitions-1/#viewtransition-ready-promise + GC::Ref m_ready_promise; + + // https://drafts.csswg.org/css-view-transitions-1/#viewtransition-update-callback-done-promise + GC::Ref m_update_callback_done_promise; + + // https://drafts.csswg.org/css-view-transitions-1/#viewtransition-finished-promise + GC::Ref m_finished_promise; + + // https://drafts.csswg.org/css-view-transitions-1/#viewtransition-transition-root-pseudo-element + // FIXME: Implement this once we have these pseudos. + + // https://drafts.csswg.org/css-view-transitions-1/#viewtransition-initial-snapshot-containing-block-size + Optional> m_initial_snapshot_containing_block_size; +}; + +} diff --git a/Libraries/LibWeb/ViewTransition/ViewTransition.idl b/Libraries/LibWeb/ViewTransition/ViewTransition.idl new file mode 100644 index 00000000000..3112a1490e4 --- /dev/null +++ b/Libraries/LibWeb/ViewTransition/ViewTransition.idl @@ -0,0 +1,11 @@ +// https://drafts.csswg.org/css-view-transitions-1/#callbackdef-viewtransitionupdatecallback +callback ViewTransitionUpdateCallback = Promise (); + +// https://drafts.csswg.org/css-view-transitions-1/#viewtransition +[Exposed=Window] +interface ViewTransition { + readonly attribute Promise updateCallbackDone; + readonly attribute Promise ready; + readonly attribute Promise finished; + undefined skipTransition(); +}; diff --git a/Libraries/LibWeb/idl_files.cmake b/Libraries/LibWeb/idl_files.cmake index 6962eb5a474..7996ea6b099 100644 --- a/Libraries/LibWeb/idl_files.cmake +++ b/Libraries/LibWeb/idl_files.cmake @@ -364,6 +364,7 @@ libweb_js_bindings(UIEvents/WheelEvent) libweb_js_bindings(URLPattern/URLPattern) libweb_js_bindings(UserTiming/PerformanceMark) libweb_js_bindings(UserTiming/PerformanceMeasure) +libweb_js_bindings(ViewTransition/ViewTransition) libweb_js_bindings(WebAssembly/Global) libweb_js_bindings(WebAssembly/Instance) libweb_js_bindings(WebAssembly/Memory) diff --git a/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp b/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp index 5763a99856a..920be77ece0 100644 --- a/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp +++ b/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp @@ -113,6 +113,7 @@ static bool is_platform_object(Type const& type) "VTTRegion"sv, "VideoTrack"sv, "VideoTrackList"sv, + "ViewTransition"sv, "WebGL2RenderingContext"sv, "WebGLActiveInfo"sv, "WebGLBuffer"sv, diff --git a/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/Namespaces.h b/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/Namespaces.h index c9dbf2167c5..99eebacc603 100644 --- a/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/Namespaces.h +++ b/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/Namespaces.h @@ -38,6 +38,7 @@ static constexpr Array libweb_interface_namespaces = { "ServiceWorker"sv, "UIEvents"sv, "URLPattern"sv, + "ViewTransition"sv, "WebAudio"sv, "WebGL"sv, "WebIDL"sv, diff --git a/Meta/gn/secondary/Userland/Libraries/LibWeb/idl_files.gni b/Meta/gn/secondary/Userland/Libraries/LibWeb/idl_files.gni index 988a00992f0..b4ed1e2ebb8 100644 --- a/Meta/gn/secondary/Userland/Libraries/LibWeb/idl_files.gni +++ b/Meta/gn/secondary/Userland/Libraries/LibWeb/idl_files.gni @@ -353,6 +353,7 @@ standard_idl_files = [ "//Userland/Libraries/LibWeb/UIEvents/WheelEvent.idl", "//Userland/Libraries/LibWeb/UserTiming/PerformanceMark.idl", "//Userland/Libraries/LibWeb/UserTiming/PerformanceMeasure.idl", + "//Userland/Libraries/LibWeb/ViewTransition/ViewTransition.idl", "//Userland/Libraries/LibWeb/WebAssembly/Instance.idl", "//Userland/Libraries/LibWeb/WebAssembly/Memory.idl", "//Userland/Libraries/LibWeb/WebAssembly/Module.idl",