ladybird/Libraries/LibWeb/ViewTransition/ViewTransition.cpp

1047 lines
52 KiB
C++
Raw Normal View History

/*
* 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(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<Gfx::ImmutableBitmap> content = {})
: NamedViewTransitionPseudoElement(type, view_transition_name)
{
m_content = content;
}
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)
, m_transition_root_pseudo_element(heap().allocate<DOM::PseudoElementTreeNode>())
{
}
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 transitions relevant global objects 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 transitions relevant Realm,
skip_the_view_transition(WebIDL::InvalidStateError::create(realm, "Failed to capture old state"_utf16));
// and return.
return;
}
// 4. Set documents 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 transitions 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 transitions 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 transitions phase is "done", then return.
// NOTE: This happens if transition was skipped before this point.
if (m_phase == Phase::Done)
return;
// 2. Set transitions relevant global objects associated documents 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 transitions initial snapshot containing block size is not equal to the snapshot containing block size, then
// skip transition with an "InvalidStateError" DOMException in transitions 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 transitions relevant Realm,
skip_the_view_transition(WebIDL::InvalidStateError::create(realm, "Failed to capture new state"_utf16));
// and return.
return;
}
// 5. For each capturedElement of transitions named elements' values:
for (auto captured_element : m_named_elements) {
// 1. If capturedElements new element is not null, then set capturedElements new elements 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 transitions 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 transitions phase to "animating".
m_phase = Phase::Animating;
// 9. Resolve transitions 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 transitions relevant global objects associated document.
auto& document = as<HTML::Window>(HTML::relevant_global_object(*this)).associated_document();
// 2. Let namedElements be transitions 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 Error::from_string_literal("The snapshot containing block is too large.");
// 6. Set transitions initial snapshot containing block size to the snapshot containing block size.
m_initial_snapshot_containing_block_size = snapshot_containing_block.size();
// 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) {
// 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 elements 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 elements 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 elements 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<CapturedElement>();
// 2. Set captures 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 captures old width to originalRects width.
capture->old_width = original_rect.width();
// 5. Set captures old height to originalRects height.
capture->old_height = original_rect.height();
// 6. Set captures old transform to a <transform-function> that would map elements 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::TransformValue(CSS::Length(0, CSS::LengthUnit::Px)), CSS::TransformValue(CSS::Length(0, CSS::LengthUnit::Px)) }));
// 7. Set captures 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 captures old direction to the computed value of direction on element.
capture->old_direction = element.layout_node()->computed_values().direction();
// 9. Set captures old text-orientation to the computed value of text-orientation on element.
// FIXME: Implement this once we have text-orientation.
// 10. Set captures 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 captures 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 captures 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 elements 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 transitions relevant global objects associated document.
auto& document = as<HTML::Window>(HTML::relevant_global_object(*this)).associated_document();
// 2. Let namedElements be transitions 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) {
// NOTE: Step 1 is handled at the end of this function.
// 2. Let transitionName be the elements 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<CapturedElement>();
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 thiss relevant global objects associated document.
auto& document = as<HTML::Window>(HTML::relevant_global_object(*this)).associated_document();
// 2. Set documents 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 transitions 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<NamedViewTransitionPseudoElement>(CSS::PseudoElement::ViewTransitionGroup, transition_name);
// 2. Append group to transitions 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<NamedViewTransitionPseudoElement>(CSS::PseudoElement::ViewTransitionImagePair, transition_name);
// 4. Append imagePair to group.
group->append_child(image_pair);
// 5. If capturedElements 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 capturedElements old image as its replaced content.
auto old = heap().allocate<ReplacedNamedViewTransitionPseudoElement>(CSS::PseudoElement::ViewTransitionOld, transition_name, captured_element->old_image);
// 2. Append old to imagePair.
image_pair->append_child(old);
}
// 6. If capturedElements 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<ReplacedNamedViewTransitionPseudoElement>(CSS::PseudoElement::ViewTransitionNew, transition_name);
// 2. Append new to imagePair.
image_pair->append_child(new_);
}
// 7. If capturedElements old image is null, then:
if (!captured_element->old_image) {
// 1. Assert: capturedElements new element is not null.
VERIFY(captured_element->new_element);
// 2. Set capturedElements image animation name rule to a new CSSStyleRule representing the
// following CSS, and append it to documents 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<CSS::CSSStyleRule>(stylesheet->css_rules()->item(index));
}
// 8. If capturedElements new element is null, then:
if (!captured_element->new_element) {
// 1. Assert: capturedElements old image is not null.
VERIFY(captured_element->old_image);
// 2. Set capturedElements image animation name rule to a new CSSStyleRule representing the
// following CSS, and append it to documents 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<CSS::CSSStyleRule>(stylesheet->css_rules()->item(index));
}
// 9. If both of capturedElements old image and new element are not null, then:
if (captured_element->old_image && captured_element->new_element) {
// 1. Let transform be capturedElements 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 capturedElements old width.
auto& width = captured_element->old_width;
// 3. Let height be capturedElements old height.
auto& height = captured_element->old_height;
// 4. Let backdropFilter be capturedElements 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 capturedElements group keyframes to a new CSSKeyframesRule representing the following
// CSS, and append it to documents 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<CSS::CSSKeyframesRule>(stylesheet->css_rules()->item(index));
// 6. Set capturedElements group animation name rule to a new CSSStyleRule representing the
// following CSS, and append it to documents 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<CSS::CSSStyleRule>(stylesheet->css_rules()->item(index));
// 7. Set capturedElements image pair isolation rule to a new CSSStyleRule representing the
// following CSS, and append it to documents 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<CSS::CSSStyleRule>(stylesheet->css_rules()->item(index));
// 8. Set capturedElements image animation name rule to a new CSSStyleRule representing the
// following CSS, and append it to documents 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<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: transitions phase is "done", or before "update-callback-called".
VERIFY(m_phase == Phase::Done || to_underlying(m_phase) < to_underlying(Phase::UpdateCallbackCalled));
// 2. If transitions phase is not "done", then set transitions 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 transitions update callback is null, then set callbackPromise to a promise resolved with undefined, in
// transitions 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 transitions 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<JS::Value> {
// 1. Resolve transitions 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 transitions update callback done promise with reason.
WebIDL::reject_promise(realm, m_update_callback_done_promise, reason);
// 2. If transitions 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 transitions ready promise.
// NOTE: transitions 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 transitions relevant settings objects 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 transitions 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 transitions relevant global objects associated document.
auto& document = as<HTML::Window>(HTML::relevant_global_object(*this)).associated_document();
// 2. Assert: transitions phase is not "done".
VERIFY(m_phase != Phase::Done);
// 3. If transitions 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 documents active view transition is transition, Clear view transition transition.
if (document.active_view_transition() == this)
clear_view_transition();
// 6. Set transitions phase to "done".
m_phase = Phase::Done;
// 7. Reject transitions ready promise with reason.
WebIDL::reject_promise(realm, m_ready_promise, reason);
// 8. Resolve transitions finished promise with the result of reacting to transitions 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 transitions relevant global objects 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 transitions transition root pseudo-elements 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.
// - animations play state is paused or running.
// FIXME: Implement this.
// - documents 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 transitions phase to "done".
m_phase = Phase::Done;
// 2. Clear view transition transition.
clear_view_transition();
// 3. Resolve transitions 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 transitions 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 transitions 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 transitions 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<void> ViewTransition::update_pseudo_element_styles()
{
// To update pseudo-element styles for a ViewTransition transition:
// 1. For each transitionName → capturedElement of transitions 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: Implement this once we have text-orientation.
Optional<CSS::MixBlendMode> mix_blend_mode = {};
Optional<CSS::Filter> backdrop_filter = {};
Optional<CSS::PreferredColorScheme> color_scheme = {};
// 2. If capturedElements new element is null, then:
if (!captured_element->new_element) {
// 1. Set width to capturedElements old width.
width = captured_element->old_width;
// 2. Set height to capturedElements old height.
height = captured_element->old_height;
// 3. Set transform to capturedElements old transform.
transform = captured_element->old_transform;
// 4. Set writingMode to capturedElements old writing-mode.
writing_mode = captured_element->old_writing_mode;
// 5. Set direction to capturedElements old direction.
direction = captured_element->old_direction;
// 6. Set textOrientation to capturedElements old text-orientation.
// FIXME: Implement this once we have text-orientation.
// 7. Set mixBlendMode to capturedElements old mix-blend-mode.
mix_blend_mode = captured_element->old_mix_blend_mode;
// 8. Set backdropFilter to capturedElements old backdrop-filter.
backdrop_filter = captured_element->old_backdrop_filter;
// 9. Set colorScheme to capturedElements old color-scheme.
color_scheme = captured_element->old_color_scheme;
}
// 3. Otherwise:
else {
// 1. Return failure if any of the following conditions is true:
// - capturedElements new element has a flat tree ancestor that skips its contents.
for (auto ancestor = captured_element->new_element->parent_element(); ancestor; ancestor = ancestor->parent_element()) {
if (ancestor->skips_its_contents())
return Error::from_string_literal("capturedElements new element has a flat tree ancestor that skips its contents.");
}
// - capturedElements new element is not rendered.
if (captured_element->new_element->not_rendered())
return Error::from_string_literal("capturedElements 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 capturedElements new element being
// captured in a view transition.
// 2. Let newRect be the snapshot containing block if capturedElements new element is the
// document element, otherwise, capturedElements 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::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 capturedElements new element.
writing_mode = captured_element->new_element->layout_node()->computed_values().writing_mode();
// 7. Set direction to the computed value of direction on capturedElements new element.
direction = captured_element->new_element->layout_node()->computed_values().direction();
// 8. Set textOrientation to the computed value of text-orientation on capturedElements new
// element.
// FIXME: Implement this.
// 9. Set mixBlendMode to the computed value of mix-blend-mode on capturedElements 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 capturedElements 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 capturedElements new element.
color_scheme = captured_element->new_element->layout_node()->computed_values().color_scheme();
}
// 4. If capturedElements group styles rule is null, then set capturedElements group styles rule to a new
// CSSStyleRule representing the following CSS, and append it to transitions relevant global objects
// associated documents 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")),
stylesheet->rules().length()));
// FIXME: all the strings above should be the identically named variables, serialized somehow.
captured_element->group_styles_rule = as<CSS::CSSStyleRule>(stylesheet->css_rules()->item(index));
}
// Otherwise, update capturedElements 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 capturedElements 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<ReplacedNamedViewTransitionPseudoElement>([&](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 news replaced element content to the result of capturing the image of capturedElements
// new element.
new_->m_content = captured_element->new_element->capture_the_image();
}
}
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 transitions relevant global objects associated document.
auto& document = as<HTML::Window>(HTML::relevant_global_object(*this)).associated_document();
// 2. Assert: documents active view transition is transition.
VERIFY(document.active_view_transition() == this);
// 3. For each capturedElement of transitions named elements' values:
for (auto captured_element : m_named_elements) {
// 1. If capturedElements new element is not null, then set capturedElements 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 capturedElements style definitions:
auto steps = [&](GC::Ptr<CSS::CSSRule> style) {
// 1. If style is not null, and style is in documents dynamic view transition style sheet, then remove
// style from documents 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 documents show view transition tree to false.
document.set_show_view_transition_tree(false);
// 5. Set documents active view transition to null.
document.set_active_view_transition(nullptr);
}
}