diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index 278070186d1..f1ffd12d448 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -948,6 +948,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 555728d7980..f9cea5e23b2 100644 --- a/Libraries/LibWeb/CSS/Selector.cpp +++ b/Libraries/LibWeb/CSS/Selector.cpp @@ -262,8 +262,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/DOM/Document.cpp b/Libraries/LibWeb/DOM/Document.cpp index cd3b8953d84..1bd06a79c47 100644 --- a/Libraries/LibWeb/DOM/Document.cpp +++ b/Libraries/LibWeb/DOM/Document.cpp @@ -135,6 +135,7 @@ #include #include #include +#include #include #include #include @@ -465,6 +466,7 @@ Document::Document(JS::Realm& realm, URL::URL const& 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_style_invalidator(realm.heap().allocate()) { m_legacy_platform_object_flags = PlatformObject::LegacyPlatformObjectFlags { @@ -606,6 +608,12 @@ void Document::visit_edges(Cell::Visitor& visitor) visitor.visit(m_adopted_style_sheets); visitor.visit(m_script_blocking_style_sheet_set); + visitor.visit(m_active_view_transition); + visitor.visit(m_dynamic_view_transition_style_sheet); + + for (auto& view_transition : m_update_callback_queue) + visitor.visit(view_transition); + visitor.visit(m_top_layer_elements); visitor.visit(m_top_layer_pending_removals); visitor.visit(m_showing_auto_popover_list); @@ -3376,7 +3384,8 @@ void Document::update_the_visibility_state(HTML::VisibilityState visibility_stat // FIXME: 4. Run the screen orientation change steps with document. - // FIXME: 5. Run the view transition page visibility change steps with document. + // 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. @@ -6038,6 +6047,10 @@ void Document::for_each_active_css_style_sheet(Function Document::start_view_transition(ViewTransition::ViewTransitionUpdateCallback 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 != nullptr) + transition->set_update_callback(update_callback); + + // 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\""_utf16)); + return transition; + } + + // 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"_utf16)); + + // 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.clear(); +} + +// 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\"."_utf16)); + } + } + // 2. Otherwise, assert: active view transition is null. + else { + VERIFY(!m_active_view_transition); + } + })); +} + ElementByIdMap& Document::element_by_id() const { if (!m_element_by_id) diff --git a/Libraries/LibWeb/DOM/Document.h b/Libraries/LibWeb/DOM/Document.h index a6aec7dec3e..4edac037b14 100644 --- a/Libraries/LibWeb/DOM/Document.h +++ b/Libraries/LibWeb/DOM/Document.h @@ -42,6 +42,7 @@ #include #include #include +#include #include #include @@ -860,6 +861,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(ViewTransition::ViewTransitionUpdateCallback 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() { return m_update_callback_queue; } + void reset_cursor_blink_cycle(); GC::Ref editing_host_manager() const { return *m_editing_host_manager; } @@ -1289,6 +1306,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; GC::Ref m_style_invalidator; diff --git a/Libraries/LibWeb/DOM/Document.idl b/Libraries/LibWeb/DOM/Document.idl index efd5f068269..37b12e6762e 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 @@ -157,6 +158,9 @@ interface Document : Node { [LegacyLenientThis] attribute EventHandler onreadystatechange; attribute EventHandler onvisibilitychange; + // https://drafts.csswg.org/css-view-transitions-1/#additions-to-document-api + ViewTransition startViewTransition(optional ViewTransitionUpdateCallback updateCallback); + // https://www.w3.org/TR/SVG2/struct.html#InterfaceDocumentExtensions readonly attribute SVGSVGElement? rootElement; }; diff --git a/Libraries/LibWeb/DOM/Element.cpp b/Libraries/LibWeb/DOM/Element.cpp index da53e433556..923a78fcf12 100644 --- a/Libraries/LibWeb/DOM/Element.cpp +++ b/Libraries/LibWeb/DOM/Element.cpp @@ -85,6 +85,7 @@ #include #include #include +#include #include #include #include @@ -3975,6 +3976,44 @@ Optional Element::lang() const 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() +{ + // FIXME: Actually implement this. + return Gfx::ImmutableBitmap::create(MUST(Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, Gfx::AlphaType::Premultiplied, Gfx::IntSize(1, 1)))); +} + void Element::set_pointer_capture(WebIDL::Long pointer_id) { (void)pointer_id; diff --git a/Libraries/LibWeb/DOM/Element.h b/Libraries/LibWeb/DOM/Element.h index 4d273ae096c..2f67e8a9aad 100644 --- a/Libraries/LibWeb/DOM/Element.h +++ b/Libraries/LibWeb/DOM/Element.h @@ -515,6 +515,18 @@ public: } } + 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(); + void set_pointer_capture(WebIDL::Long pointer_id); void release_pointer_capture(WebIDL::Long pointer_id); bool has_pointer_capture(WebIDL::Long pointer_id); @@ -642,6 +654,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 }; + // https://html.spec.whatwg.org/multipage/grouping-content.html#ordinal-value Optional m_ordinal_value; bool m_is_contained_in_list_subtree { false }; diff --git a/Libraries/LibWeb/DOM/PseudoElement.h b/Libraries/LibWeb/DOM/PseudoElement.h index 18383fb2cd4..1bd3ceae02b 100644 --- a/Libraries/LibWeb/DOM/PseudoElement.h +++ b/Libraries/LibWeb/DOM/PseudoElement.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -53,9 +54,9 @@ private: }; // https://drafts.csswg.org/css-view-transitions/#pseudo-element-tree -class PseudoElementTreeNode final +class PseudoElementTreeNode : public PseudoElement - , TreeNode { + , public TreeNode { GC_CELL(PseudoElementTreeNode, PseudoElement); GC_DECLARE_ALLOCATOR(PseudoElementTreeNode); }; diff --git a/Libraries/LibWeb/Forward.h b/Libraries/LibWeb/Forward.h index 810130ecd65..0b96c9e3bd6 100644 --- a/Libraries/LibWeb/Forward.h +++ b/Libraries/LibWeb/Forward.h @@ -1123,6 +1123,12 @@ class PerformanceMeasure; } +namespace Web::ViewTransition { + +class ViewTransition; + +} + namespace Web::WebAssembly { class Global; diff --git a/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp b/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp index b78f230c91f..b3f910d67df 100644 --- a/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp +++ b/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp @@ -458,7 +458,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 b4566d50e41..aa094c96b79 100644 --- a/Libraries/LibWeb/HTML/Navigable.cpp +++ b/Libraries/LibWeb/HTML/Navigable.cpp @@ -2587,6 +2587,22 @@ 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 +CSSPixelSize Navigable::snapshot_containing_block_size() +{ + return this->snapshot_containing_block().size(); +} + 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 827fc52daa0..962c771c25b 100644 --- a/Libraries/LibWeb/HTML/Navigable.h +++ b/Libraries/LibWeb/HTML/Navigable.h @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -200,6 +201,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 + CSSPixelSize 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..ba66f9fbb5a --- /dev/null +++ b/Libraries/LibWeb/ViewTransition/ViewTransition.cpp @@ -0,0 +1,1050 @@ +/* + * 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(NamedViewTransitionPseudoElement); +GC_DEFINE_ALLOCATOR(ReplacedNamedViewTransitionPseudoElement); +GC_DEFINE_ALLOCATOR(CapturedElement); +GC_DEFINE_ALLOCATOR(ViewTransition); + +NamedViewTransitionPseudoElement::NamedViewTransitionPseudoElement(CSS::PseudoElement type, FlyString view_transition_name) + : m_type(type) + , m_view_transition_name(view_transition_name) +{ +} + +ReplacedNamedViewTransitionPseudoElement::ReplacedNamedViewTransitionPseudoElement(CSS::PseudoElement type, FlyString view_transition_name, RefPtr content = {}) + : NamedViewTransitionPseudoElement(type, view_transition_name) +{ + m_content = content; +} + +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) + , m_transition_root_pseudo_element(heap().allocate()) + +{ +} + +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); + } + visitor.visit(m_update_callback); + visitor.visit(m_ready_promise); + visitor.visit(m_update_callback_done_promise); + visitor.visit(m_finished_promise); + visitor.visit(m_transition_root_pseudo_element); +} + +void CapturedElement::visit_edges(Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + + visitor.visit(new_element); + + visitor.visit(group_keyframes); + visitor.visit(group_animation_name_rule); + visitor.visit(group_styles_rule); + visitor.visit(image_pair_isolation_rule); + visitor.visit(image_animation_name_rule); +} + +// 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"_utf16)); + } +} + +// 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::InvalidStateError::create(realm, "Failed to capture old state"_utf16)); + // 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 != snapshot_containing_block_size) { + skip_the_view_transition(WebIDL::InvalidStateError::create(realm, "Transition's initial snapshot containing block size is not equal to the snapshot containing block size"_utf16)); + 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::InvalidStateError::create(realm, "Failed to capture new state"_utf16)); + // 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::InvalidStateError::create(realm, "Failed to update pseudo-element styles"_utf16)); + // 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 Error::from_string_literal("The snapshot containing block is too large."); + + // 6. Set transition’s initial snapshot containing block size to the snapshot containing block size. + m_initial_snapshot_containing_block_size = CSSPixelSize { snapshot_containing_block.width().raw_value(), snapshot_containing_block.height().raw_value() }; + + // 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) { + // NOTE: Step 1 is handled at the end of this function. + + // 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) + // 1. Set element’s captured in a view transition to false. + 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); + + // 1. If any flat tree ancestor of this element skips its contents, then continue. + if (element.skips_its_contents()) + return TraversalDecision::SkipChildrenAndContinue; + + return TraversalDecision::Continue; + }); + if (result == TraversalDecision::Break) + return Error::from_string_literal("Cannot include multiple elements with the same view-transition-name in a view transition."); + + // 8. For each element in captureElements: + for (auto& element : capture_elements) { + // 1. Let capture be a new captured element struct. + auto capture = heap().allocate(); + + // 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() ? 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 transform 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) { + // NOTE: Step 1 is handled at the end of this function. + + // 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())) { + auto captured_element = heap().allocate(); + 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; + + // 1. If any flat tree ancestor of this element skips its contents, then continue. + if (element.skips_its_contents()) + return TraversalDecision::SkipChildrenAndContinue; + + return TraversalDecision::Continue; + }); + if (result == TraversalDecision::Break) + return Error::from_string_literal("Cannot include multiple elements with the same view-transition-name in a view transition."); + + 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. + auto group = heap().allocate(CSS::PseudoElement::ViewTransitionGroup, transition_name); + + // 2. Append group to transition’s transition root pseudo-element. + m_transition_root_pseudo_element->append_child(group); + + // 3. Let imagePair be a new '::view-transition-image-pair()', with its view transition name set to + // transitionName. + auto image_pair = heap().allocate(CSS::PseudoElement::ViewTransitionImagePair, transition_name); + + // 4. Append imagePair to group. + group->append_child(image_pair); + + // 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. + auto old = heap().allocate(CSS::PseudoElement::ViewTransitionOld, transition_name, captured_element->old_image); + + // 2. Append old to imagePair. + image_pair->append_child(old); + } + + // 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. + auto new_ = heap().allocate(CSS::PseudoElement::ViewTransitionNew, transition_name); + + // 2. Append new to imagePair. + image_pair->append_child(new_); + } + + // 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); + + // 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)), + stylesheet->rules().length())); + 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)), + stylesheet->rules().length())); + 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; + // FIXME: Remove this once tranform gets used in step 5 below. + (void)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; + // FIXME: Remove this once tranform gets used in step 5 below. + (void)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")), + stylesheet->rules().length())); + // FIXME: all the strings above should be the identically named variables, serialized somehow. + 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({0}) {{ + animation-name: -ua-view-transition-group-anim-{0}; + }} + )", + transition_name)), + stylesheet->rules().length())); + 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)), + stylesheet->rules().length())); + 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. + // AD-HOC: We can't use the given CSS exactly since it is two rules, not one. + // Instead we turn it into one rule, with both of them nested inside. + index = MUST(stylesheet->insert_rule(MUST(String::formatted(R"( + :root {{ + &::view-transition-old({0}) {{ + animation-name: -ua-view-transition-fade-out, -ua-mix-blend-mode-plus-lighter; + }} + &::view-transition-new({0}) {{ + animation-name: -ua-view-transition-fade-in, -ua-mix-blend-mode-plus-lighter; + }} + }} + )", + transition_name)), + stylesheet->rules().length())); + 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 || to_underlying(m_phase) < to_underlying(Phase::UpdateCallbackCalled)); + + // 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 { + auto promise = MUST(WebIDL::invoke_callback(*m_update_callback, {}, {})); + // 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 »). + // FIXME: We should not need to push an incumbent realm here, but http://wpt.live/css/css-view-transitions/update-callback-timeout.html crashes without it. + HTML::main_thread_event_loop().push_onto_backup_incumbent_realm_stack(realm); + MUST(JS::call(realm.vm(), *promise_capability->resolve(), JS::js_undefined(), promise)); + HTML::main_thread_event_loop().pop_backup_incumbent_realm_stack(); + // 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 (to_underlying(m_phase) < to_underlying(Phase::UpdateCallbackCalled)) { + 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: + m_transition_root_pseudo_element->for_each_in_inclusive_subtree([&](DOM::PseudoElementTreeNode&) { + // For each animation whose timeline is a document timeline associated with document, and contains at + // least one associated effect whose effect target is element, set hasActiveAnimations to true if any of the + // following conditions are true: + // FIXME: Implement this. + + // - animation’s play state is paused or running. + // FIXME: Implement this. + + // - document’s pending animation event queue has any events associated with animation. + // FIXME: Implement this. + + return TraversalDecision::Continue; + }); + + // 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. + // FIXME: Without this TemporaryExecutionContext, this would fail an assert later on about missing one. + // Figure out why and where this actually needs to be handled. + HTML::TemporaryExecutionContext context(realm); + 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 != snapshot_containing_block_size) { + // 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"_utf16)); + // 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::InvalidStateError::create(realm, "Failed to update pseudo-element styles"_utf16)); + // 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: Implement this once we have 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 Error::from_string_literal("capturedElement’s new element has a flat tree ancestor that skips its contents."); + } + + // - capturedElement’s new element is not rendered. + if (captured_element->new_element->not_rendered()) + return Error::from_string_literal("capturedElement’s new element is not rendered."); + + // - 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. + auto offset = new_rect.location() - captured_element->new_element->navigable()->snapshot_containing_block().location(); + transform = CSS::Transformation(CSS::TransformFunction::Translate, Vector({ CSS::TransformValue(CSS::Length::make_px(offset.x())), CSS::TransformValue(CSS::Length::make_px(offset.y())) })); + + // 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")), + stylesheet->rules().length())); + // FIXME: all the strings above should be the identically named variables, serialized somehow. + 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 { + captured_element->group_styles_rule->set_selector_text(MUST(String::formatted(":root::view-transition-group({0})", transition_name))); + captured_element->group_styles_rule->set_css_text(MUST(String::formatted(R"( + width: {}; + height: {}; + transform: {}; + writing-mode: {}; + direction: {}; + text-orientation: {}; + mix-blend-mode: {}; + backdrop-filter: {}; + color-scheme: {}; + )", + width, height, "transform", "writing_mode", "direction", "text_orientation", "mix_blend_mode", "backdrop_filter", "color_scheme"))); + // FIXME: all the strings above should be the identically named variables, serialized somehow. + } + + // 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. + ReplacedNamedViewTransitionPseudoElement* new_; + m_transition_root_pseudo_element->for_each_in_inclusive_subtree_of_type([&](auto& element) { + if (element.m_type == CSS::PseudoElement::ViewTransitionNew && element.m_view_transition_name == transition_name) { + new_ = &element; + return TraversalDecision::Break; + } + return TraversalDecision::Continue; + }); + + // 2. Set new’s replaced element content to the result of capturing the image of capturedElement’s + // new element. + new_->m_content = captured_element->new_element->capture_the_image(); + } + } + + // 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 = [&](GC::Ptr 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..deb0659001a --- /dev/null +++ b/Libraries/LibWeb/ViewTransition/ViewTransition.h @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2025, Psychpsyo + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Web::ViewTransition { + +// https://drafts.csswg.org/css-view-transitions-1/#named-view-transition-pseudo +class NamedViewTransitionPseudoElement + : public DOM::PseudoElementTreeNode { + GC_CELL(NamedViewTransitionPseudoElement, DOM::PseudoElementTreeNode); + GC_DECLARE_ALLOCATOR(NamedViewTransitionPseudoElement); + + NamedViewTransitionPseudoElement(CSS::PseudoElement, FlyString); + + CSS::PseudoElement m_type; + + // Several of the view transition pseudo-elements are named view transition pseudo-elements, which are + // functional tree-abiding view transition pseudo-elements associated with a view transition name. + FlyString m_view_transition_name; +}; + +// https://drafts.csswg.org/css-view-transitions-1/#::view-transition-old +// https://drafts.csswg.org/css-view-transitions-1/#::view-transition-new +class ReplacedNamedViewTransitionPseudoElement + : public NamedViewTransitionPseudoElement { + GC_CELL(ReplacedNamedViewTransitionPseudoElement, NamedViewTransitionPseudoElement); + GC_DECLARE_ALLOCATOR(ReplacedNamedViewTransitionPseudoElement); + + ReplacedNamedViewTransitionPseudoElement(CSS::PseudoElement, FlyString, RefPtr); + + RefPtr m_content; +}; + +// 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 {}; + CSS::Filter old_backdrop_filter {}; + Optional old_color_scheme {}; + GC::Ptr new_element {}; + + GC::Ptr group_keyframes {}; + GC::Ptr group_animation_name_rule {}; + GC::Ptr group_styles_rule {}; + GC::Ptr image_pair_isolation_rule {}; + GC::Ptr image_animation_name_rule {}; + +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 class Phase : u8 { + 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 + GC::Ref m_transition_root_pseudo_element; + + // 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 403bf29c21d..c40bd9bb0d4 100644 --- a/Libraries/LibWeb/idl_files.cmake +++ b/Libraries/LibWeb/idl_files.cmake @@ -424,6 +424,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 c3790faf0d0..7d0256d7e6d 100644 --- a/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp +++ b/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp @@ -137,6 +137,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 01c3fcfaf15..e026e6b0307 100644 --- a/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/Namespaces.h +++ b/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/Namespaces.h @@ -43,6 +43,7 @@ static constexpr Array libweb_interface_namespaces = { "Streams"sv, "UIEvents"sv, "URLPattern"sv, + "ViewTransition"sv, "WebAudio"sv, "WebGL"sv, "WebIDL"sv, diff --git a/Meta/gn/secondary/Userland/Libraries/LibWeb/DOM/BUILD.gn b/Meta/gn/secondary/Userland/Libraries/LibWeb/DOM/BUILD.gn index 06202f52c45..0e557ed66fc 100644 --- a/Meta/gn/secondary/Userland/Libraries/LibWeb/DOM/BUILD.gn +++ b/Meta/gn/secondary/Userland/Libraries/LibWeb/DOM/BUILD.gn @@ -41,6 +41,7 @@ source_set("DOM") { "ParentNode.cpp", "Position.cpp", "ProcessingInstruction.cpp", + "PseudoElement.cpp", "QualifiedName.cpp", "Range.cpp", "ShadowRoot.cpp", diff --git a/Meta/gn/secondary/Userland/Libraries/LibWeb/idl_files.gni b/Meta/gn/secondary/Userland/Libraries/LibWeb/idl_files.gni index bc7844c0eeb..48de8999a59 100644 --- a/Meta/gn/secondary/Userland/Libraries/LibWeb/idl_files.gni +++ b/Meta/gn/secondary/Userland/Libraries/LibWeb/idl_files.gni @@ -359,6 +359,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", diff --git a/Tests/LibWeb/Ref/input/wpt-import/css/css-view-transitions/update-callback-called-once.html b/Tests/LibWeb/Ref/input/wpt-import/css/css-view-transitions/update-callback-called-once.html new file mode 100644 index 00000000000..56edc34347c --- /dev/null +++ b/Tests/LibWeb/Ref/input/wpt-import/css/css-view-transitions/update-callback-called-once.html @@ -0,0 +1,39 @@ + + +Update callback should only be called once + + + + + +

Test passes if there is a filled green square and no red.

+
+ + diff --git a/Tests/LibWeb/Text/expected/all-window-properties.txt b/Tests/LibWeb/Text/expected/all-window-properties.txt index 45430ca6b8d..b436751380a 100644 --- a/Tests/LibWeb/Text/expected/all-window-properties.txt +++ b/Tests/LibWeb/Text/expected/all-window-properties.txt @@ -476,6 +476,7 @@ VTTRegion ValidityState VideoTrack VideoTrackList +ViewTransition VisualViewport WeakMap WeakRef