mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-04-22 12:35:14 +00:00
LibWeb: Implement view transitions
This implements the CSS view-transitions-1 spec.
This commit is contained in:
parent
82b1d5537e
commit
65ebfcb37d
19 changed files with 1413 additions and 2 deletions
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()), {});
|
||||
|
|
|
@ -127,6 +127,7 @@
|
|||
#include <LibWeb/HTML/Scripting/Agent.h>
|
||||
#include <LibWeb/HTML/Scripting/ClassicScript.h>
|
||||
#include <LibWeb/HTML/Scripting/ExceptionReporter.h>
|
||||
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
|
||||
#include <LibWeb/HTML/Scripting/WindowEnvironmentSettingsObject.h>
|
||||
#include <LibWeb/HTML/SharedResourceRequest.h>
|
||||
#include <LibWeb/HTML/Storage.h>
|
||||
|
@ -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<ViewTransition::ViewTransition> Document::start_view_transition(Optional<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.has_value())
|
||||
transition->set_update_callback(update_callback.value());
|
||||
|
||||
// 3. Let document be this’s relevant global object’s associated document.
|
||||
auto& document = as<HTML::Window>(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) {
|
||||
|
|
|
@ -39,6 +39,7 @@
|
|||
#include <LibWeb/HTML/VisibilityState.h>
|
||||
#include <LibWeb/InvalidateDisplayList.h>
|
||||
#include <LibWeb/TrustedTypes/InjectionSink.h>
|
||||
#include <LibWeb/ViewTransition/ViewTransition.h>
|
||||
#include <LibWeb/WebIDL/ExceptionOr.h>
|
||||
#include <LibWeb/WebIDL/ObservableArray.h>
|
||||
|
||||
|
@ -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<ViewTransition::ViewTransition> start_view_transition(Optional<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<ViewTransition::ViewTransition> active_view_transition() const { return m_active_view_transition; }
|
||||
void set_active_view_transition(GC::Ptr<ViewTransition::ViewTransition> 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<CSS::CSSStyleSheet> 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<ViewTransition::ViewTransition*> update_callback_queue() const { return m_update_callback_queue; }
|
||||
|
||||
void reset_cursor_blink_cycle();
|
||||
|
||||
GC::Ref<EditingHostManager> 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<GC::Ref<Element>> m_render_blocking_elements;
|
||||
|
||||
// https://drafts.csswg.org/css-view-transitions-1/#document-active-view-transition
|
||||
GC::Ptr<ViewTransition::ViewTransition> 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<CSS::CSSStyleSheet> 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<ViewTransition::ViewTransition*> m_update_callback_queue = {};
|
||||
|
||||
HashTable<WeakPtr<Node>> m_pending_nodes_for_style_invalidation_due_to_presence_of_has;
|
||||
};
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
#import <HTML/HTMLScriptElement.idl>
|
||||
#import <HTML/Location.idl>
|
||||
#import <Selection/Selection.idl>
|
||||
#import <ViewTransition/ViewTransition.idl>
|
||||
|
||||
// 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 {
|
||||
|
|
|
@ -77,6 +77,7 @@
|
|||
#include <LibWeb/Namespace.h>
|
||||
#include <LibWeb/Page/Page.h>
|
||||
#include <LibWeb/Painting/PaintableBox.h>
|
||||
#include <LibWeb/Painting/StackingContext.h>
|
||||
#include <LibWeb/Painting/ViewportPaintable.h>
|
||||
#include <LibWeb/SVG/SVGAElement.h>
|
||||
#include <LibWeb/Selection/Selection.h>
|
||||
|
@ -3779,4 +3780,93 @@ Optional<String> 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<FlyString> 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<Gfx::Bitmap> 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<FlyString> document_scoped_view_transition_name();
|
||||
|
||||
// https://drafts.csswg.org/css-view-transitions-1/#capture-the-image
|
||||
RefPtr<Gfx::Bitmap> 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<>
|
||||
|
|
|
@ -864,6 +864,10 @@ class PerformanceMark;
|
|||
class PerformanceMeasure;
|
||||
}
|
||||
|
||||
namespace Web::ViewTransition {
|
||||
class ViewTransition;
|
||||
}
|
||||
|
||||
namespace Web::WebAssembly {
|
||||
class Global;
|
||||
class Instance;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<CSSPixels, CSSPixels> 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>, NavigationObserver& navigation_observer)
|
||||
{
|
||||
auto result = m_navigation_observers.set(navigation_observer);
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
#include <AK/HashTable.h>
|
||||
#include <AK/String.h>
|
||||
#include <AK/Tuple.h>
|
||||
#include <LibJS/Heap/Cell.h>
|
||||
#include <LibWeb/Bindings/NavigationPrototype.h>
|
||||
#include <LibWeb/DOM/DocumentLoadEventDelayer.h>
|
||||
|
@ -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<CSSPixels, CSSPixels> 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();
|
||||
|
||||
|
|
970
Libraries/LibWeb/ViewTransition/ViewTransition.cpp
Normal file
970
Libraries/LibWeb/ViewTransition/ViewTransition.cpp
Normal file
|
@ -0,0 +1,970 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Psychpsyo <psychpsyo@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibJS/Runtime/Realm.h>
|
||||
#include <LibWeb/CSS/CSSKeyframesRule.h>
|
||||
#include <LibWeb/HTML/EventLoop/EventLoop.h>
|
||||
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
|
||||
#include <LibWeb/HTML/Window.h>
|
||||
#include <LibWeb/Layout/Node.h>
|
||||
#include <LibWeb/Painting/PaintableBox.h>
|
||||
#include <LibWeb/ViewTransition/ViewTransition.h>
|
||||
#include <LibWeb/WebIDL/AbstractOperations.h>
|
||||
#include <LibWeb/WebIDL/Promise.h>
|
||||
|
||||
namespace Web::ViewTransition {
|
||||
|
||||
GC_DEFINE_ALLOCATOR(ViewTransition);
|
||||
|
||||
GC::Ref<ViewTransition> ViewTransition::create(JS::Realm& realm)
|
||||
{
|
||||
auto const& finished_promise = WebIDL::create_promise(realm);
|
||||
WebIDL::mark_promise_as_handled(finished_promise);
|
||||
return realm.create<ViewTransition>(realm, WebIDL::create_promise(realm), WebIDL::create_promise(realm), finished_promise);
|
||||
}
|
||||
|
||||
ViewTransition::ViewTransition(JS::Realm& realm, GC::Ref<WebIDL::Promise> ready_promise, GC::Ref<WebIDL::Promise> update_callback_done_promise, GC::Ref<WebIDL::Promise> 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::Window>(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::Window>(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<void> 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::Window>(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<FlyString>();
|
||||
|
||||
// 4. Let captureElements be a new list of elements.
|
||||
auto capture_elements = AK::Vector<DOM::Element&>();
|
||||
|
||||
// 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<int>::max() || snapshot_containing_block.height() > NumericLimits<int>::max())
|
||||
return ErrorOr<void> {};
|
||||
|
||||
// 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<DOM::Element>([&](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<void> {};
|
||||
|
||||
// 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 <transform-function> 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::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<void> 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::Window>(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<FlyString>();
|
||||
|
||||
// 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<DOM::Element>([&](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<void> {};
|
||||
|
||||
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::Window>(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<CSS::CSSStyleRule>(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<CSS::CSSStyleRule>(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<CSS::CSSKeyframesRule>(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<CSS::CSSStyleRule>(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<CSS::CSSStyleRule>(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<CSS::CSSStyleRule>(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<JS::Value> {
|
||||
// 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<JS::Value> {
|
||||
// 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::Window>(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::Window>(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::Window>(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<JS::Value> { 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::Window>(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<void> 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<CSSPixels> width = {};
|
||||
Optional<CSSPixels> height = {};
|
||||
Optional<CSS::Transformation> transform = {};
|
||||
Optional<CSS::WritingMode> writing_mode = {};
|
||||
Optional<CSS::Direction> direction = {};
|
||||
// FIXME: text_orientation
|
||||
Optional<CSS::MixBlendMode> mix_blend_mode = {};
|
||||
Optional<Vector<Gfx::Filter>> backdrop_filter = {};
|
||||
Optional<CSS::PreferredColorScheme> 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<DOM::Element>(*ancestor).skips_its_contents())
|
||||
return ErrorOr<void> {};
|
||||
}
|
||||
|
||||
// - capturedElement’s new element is not rendered.
|
||||
if (captured_element->new_element && captured_element->new_element->not_rendered())
|
||||
return ErrorOr<void> {};
|
||||
|
||||
// - 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::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::Window>(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<CSS::CSSStyleRule>(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::Window>(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);
|
||||
}
|
||||
|
||||
}
|
145
Libraries/LibWeb/ViewTransition/ViewTransition.h
Normal file
145
Libraries/LibWeb/ViewTransition/ViewTransition.h
Normal file
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Psychpsyo <psychpsyo@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/HashMap.h>
|
||||
#include <AK/Tuple.h>
|
||||
#include <LibGfx/Bitmap.h>
|
||||
#include <LibGfx/Filter.h>
|
||||
#include <LibWeb/Bindings/PlatformObject.h>
|
||||
#include <LibWeb/Bindings/ViewTransitionPrototype.h>
|
||||
#include <LibWeb/CSS/Enums.h>
|
||||
#include <LibWeb/CSS/PreferredColorScheme.h>
|
||||
#include <LibWeb/CSS/Transformation.h>
|
||||
#include <LibWeb/Forward.h>
|
||||
#include <LibWeb/PixelUnits.h>
|
||||
|
||||
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<Gfx::Bitmap> 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<CSS::TransformValue>());
|
||||
Optional<CSS::WritingMode> old_writing_mode = {};
|
||||
Optional<CSS::Direction> old_direction = {};
|
||||
// FIXME: old_text_orientation
|
||||
Optional<CSS::MixBlendMode> old_mix_blend_mode = {};
|
||||
Optional<Vector<Gfx::Filter>> old_backdrop_filter = {};
|
||||
Optional<CSS::PreferredColorScheme> 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<WebIDL::CallbackType>;
|
||||
|
||||
class ViewTransition final : public Bindings::PlatformObject {
|
||||
WEB_PLATFORM_OBJECT(ViewTransition, Bindings::PlatformObject);
|
||||
GC_DECLARE_ALLOCATOR(ViewTransition);
|
||||
|
||||
public:
|
||||
static GC::Ref<ViewTransition> create(JS::Realm&);
|
||||
virtual ~ViewTransition() override = default;
|
||||
|
||||
// https://drafts.csswg.org/css-view-transitions-1/#dom-viewtransition-updatecallbackdone
|
||||
GC::Ref<WebIDL::Promise> update_callback_done() const { return m_update_callback_done_promise; }
|
||||
// https://drafts.csswg.org/css-view-transitions-1/#dom-viewtransition-ready
|
||||
GC::Ref<WebIDL::Promise> ready() const { return m_ready_promise; }
|
||||
// https://drafts.csswg.org/css-view-transitions-1/#dom-viewtransition-finished
|
||||
GC::Ref<WebIDL::Promise> 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<void> capture_the_old_state();
|
||||
|
||||
// https://drafts.csswg.org/css-view-transitions-1/#capture-the-new-state
|
||||
ErrorOr<void> 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<void> 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<WebIDL::Promise>, GC::Ref<WebIDL::Promise>, GC::Ref<WebIDL::Promise>);
|
||||
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<FlyString, CapturedElement*> 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<WebIDL::Promise> m_ready_promise;
|
||||
|
||||
// https://drafts.csswg.org/css-view-transitions-1/#viewtransition-update-callback-done-promise
|
||||
GC::Ref<WebIDL::Promise> m_update_callback_done_promise;
|
||||
|
||||
// https://drafts.csswg.org/css-view-transitions-1/#viewtransition-finished-promise
|
||||
GC::Ref<WebIDL::Promise> 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<Tuple<CSSPixels, CSSPixels>> m_initial_snapshot_containing_block_size;
|
||||
};
|
||||
|
||||
}
|
11
Libraries/LibWeb/ViewTransition/ViewTransition.idl
Normal file
11
Libraries/LibWeb/ViewTransition/ViewTransition.idl
Normal file
|
@ -0,0 +1,11 @@
|
|||
// https://drafts.csswg.org/css-view-transitions-1/#callbackdef-viewtransitionupdatecallback
|
||||
callback ViewTransitionUpdateCallback = Promise<any> ();
|
||||
|
||||
// https://drafts.csswg.org/css-view-transitions-1/#viewtransition
|
||||
[Exposed=Window]
|
||||
interface ViewTransition {
|
||||
readonly attribute Promise<undefined> updateCallbackDone;
|
||||
readonly attribute Promise<undefined> ready;
|
||||
readonly attribute Promise<undefined> finished;
|
||||
undefined skipTransition();
|
||||
};
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -38,6 +38,7 @@ static constexpr Array libweb_interface_namespaces = {
|
|||
"ServiceWorker"sv,
|
||||
"UIEvents"sv,
|
||||
"URLPattern"sv,
|
||||
"ViewTransition"sv,
|
||||
"WebAudio"sv,
|
||||
"WebGL"sv,
|
||||
"WebIDL"sv,
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Add table
Reference in a new issue