[go: nahoru, domu]

blob: ef7a05668c5400183eb7847a39c18896a02799b3 [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/core/view_transition/view_transition.h"
#include <vector>
#include "base/time/time.h"
#include "base/trace_event/trace_event.h"
#include "cc/trees/layer_tree_host.h"
#include "cc/trees/paint_holding_reason.h"
#include "third_party/blink/public/platform/web_content_settings_client.h"
#include "third_party/blink/renderer/bindings/core/v8/capture_source_location.h"
#include "third_party/blink/renderer/bindings/core/v8/script_function.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_property.h"
#include "third_party/blink/renderer/bindings/core/v8/script_value.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_throw_dom_exception.h"
#include "third_party/blink/renderer/core/css/style_change_reason.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/dom/dom_node_ids.h"
#include "third_party/blink/renderer/core/dom/pseudo_element.h"
#include "third_party/blink/renderer/core/events/error_event.h"
#include "third_party/blink/renderer/core/execution_context/agent.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/local_frame_view.h"
#include "third_party/blink/renderer/core/layout/layout_box_model_object.h"
#include "third_party/blink/renderer/core/layout/layout_view.h"
#include "third_party/blink/renderer/core/page/chrome_client.h"
#include "third_party/blink/renderer/core/page/page.h"
#include "third_party/blink/renderer/core/paint/paint_layer.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h"
#include "third_party/blink/renderer/platform/graphics/compositing/paint_artifact_compositor.h"
#include "third_party/blink/renderer/platform/graphics/compositor_element_id.h"
#include "third_party/blink/renderer/platform/graphics/paint/clip_paint_property_node.h"
#include "third_party/blink/renderer/platform/heap/cross_thread_handle.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
#include "third_party/blink/renderer/platform/scheduler/public/event_loop.h"
#include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h"
#include "third_party/blink/renderer/platform/wtf/hash_map.h"
#include "third_party/blink/renderer/platform/wtf/std_lib_extras.h"
#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
#include "third_party/blink/renderer/platform/wtf/wtf_size_t.h"
namespace blink {
namespace {
const char kAbortedMessage[] = "Transition was skipped";
const char kInvalidStateMessage[] =
"Transition was aborted because of invalid state";
const char kTimeoutMessage[] =
"Transition was aborted because of timeout in DOM update";
uint32_t NextDocumentTag() {
static uint32_t next_document_tag = 1u;
return next_document_tag++;
}
} // namespace
// DOMChangeFinishedCallback implementation.
ViewTransition::DOMChangeFinishedCallback::DOMChangeFinishedCallback(
ViewTransition* transition,
bool success)
: transition_(transition), success_(success) {
DCHECK(transition_);
DCHECK(transition_->script_bound_state_);
}
ViewTransition::DOMChangeFinishedCallback::~DOMChangeFinishedCallback() =
default;
ScriptValue ViewTransition::DOMChangeFinishedCallback::Call(
ScriptState* script_state,
ScriptValue value) {
transition_->NotifyDOMCallbackFinished(success_, std::move(value));
return ScriptValue();
}
void ViewTransition::DOMChangeFinishedCallback::Trace(Visitor* visitor) const {
ScriptFunction::Callable::Trace(visitor);
visitor->Trace(transition_);
}
ViewTransition::ScriptBoundState::ScriptBoundState(
ExecutionContext* context,
ScriptState* state,
V8ViewTransitionCallback* callback)
: script_state(state),
update_dom_callback(callback),
dom_updated_promise_property(
MakeGarbageCollected<PromiseProperty>(context)),
ready_promise_property(MakeGarbageCollected<PromiseProperty>(context)),
finished_promise_property(
MakeGarbageCollected<PromiseProperty>(context)) {}
void ViewTransition::ScriptBoundState::Trace(Visitor* visitor) const {
visitor->Trace(script_state);
visitor->Trace(update_dom_callback);
visitor->Trace(dom_updated_promise_property);
visitor->Trace(ready_promise_property);
visitor->Trace(finished_promise_property);
}
void ViewTransition::ScriptBoundState::HandlePromise(
Response response,
PromiseProperty* property) {
DCHECK_EQ(property->GetState(), PromiseProperty::State::kPending);
if (!script_state->ContextIsValid())
return;
switch (response) {
case Response::kResolve:
property->ResolveWithUndefined();
break;
case Response::kRejectAbort: {
ScriptState::Scope scope(script_state);
property->Reject(V8ThrowDOMException::CreateOrEmpty(
script_state->GetIsolate(), DOMExceptionCode::kAbortError,
kAbortedMessage));
break;
}
case Response::kRejectInvalidState: {
ScriptState::Scope scope(script_state);
property->Reject(V8ThrowDOMException::CreateOrEmpty(
script_state->GetIsolate(), DOMExceptionCode::kInvalidStateError,
kInvalidStateMessage));
break;
}
case Response::kRejectTimeout: {
ScriptState::Scope scope(script_state);
property->Reject(V8ThrowDOMException::CreateOrEmpty(
script_state->GetIsolate(), DOMExceptionCode::kTimeoutError,
kTimeoutMessage));
break;
}
}
}
ViewTransition::ScopedPauseRendering::ScopedPauseRendering(
const Document& document) {
if (!document.GetFrame()->IsLocalRoot())
return;
auto& client = document.GetPage()->GetChromeClient();
cc_paused_ = client.PauseRendering(*document.GetFrame());
DCHECK(cc_paused_);
}
ViewTransition::ScopedPauseRendering::~ScopedPauseRendering() = default;
bool ViewTransition::ScopedPauseRendering::ShouldThrottleRendering() const {
return !cc_paused_;
}
// static
const char* ViewTransition::StateToString(State state) {
switch (state) {
case State::kInitial:
return "Initial";
case State::kCaptureTagDiscovery:
return "CaptureTagDiscovery";
case State::kCaptureRequestPending:
return "CaptureRequestPending";
case State::kCapturing:
return "Capturing";
case State::kCaptured:
return "Captured";
case State::kWaitForRenderBlock:
return "WaitForRenderBlock";
case State::kDOMCallbackRunning:
return "DOMCallbackRunning";
case State::kDOMCallbackFinished:
return "DOMCallbackFinished";
case State::kAnimateTagDiscovery:
return "AnimateTagDiscovery";
case State::kAnimateRequestPending:
return "AnimateRequestPending";
case State::kAnimating:
return "Animating";
case State::kFinished:
return "Finished";
case State::kAborted:
return "Aborted";
case State::kTimedOut:
return "TimedOut";
case State::kTransitionStateCallbackDispatched:
return "TransitionStateCallbackDispatched";
};
NOTREACHED();
return "";
}
// static
ViewTransition* ViewTransition::CreateFromScript(
Document* document,
ScriptState* script_state,
V8ViewTransitionCallback* callback,
Delegate* delegate) {
return MakeGarbageCollected<ViewTransition>(document, script_state, callback,
delegate);
}
ViewTransition::ViewTransition(Document* document,
ScriptState* script_state,
V8ViewTransitionCallback* update_dom_callback,
Delegate* delegate)
: ExecutionContextLifecycleObserver(document->GetExecutionContext()),
creation_type_(CreationType::kScript),
document_(document),
delegate_(delegate),
document_tag_(NextDocumentTag()),
script_bound_state_(
MakeGarbageCollected<ScriptBoundState>(GetExecutionContext(),
script_state,
update_dom_callback)),
style_tracker_(
MakeGarbageCollected<ViewTransitionStyleTracker>(*document_)) {
ProcessCurrentState();
}
// static
ViewTransition* ViewTransition::CreateForSnapshotForNavigation(
Document* document,
ViewTransitionStateCallback callback,
Delegate* delegate) {
return MakeGarbageCollected<ViewTransition>(document, std::move(callback),
delegate);
}
ViewTransition::ViewTransition(Document* document,
ViewTransitionStateCallback callback,
Delegate* delegate)
: ExecutionContextLifecycleObserver(document->GetExecutionContext()),
creation_type_(CreationType::kForSnapshot),
document_(document),
delegate_(delegate),
navigation_id_(viz::NavigationID::Create()),
document_tag_(NextDocumentTag()),
style_tracker_(
MakeGarbageCollected<ViewTransitionStyleTracker>(*document_)),
transition_state_callback_(std::move(callback)) {
TRACE_EVENT0("blink", "ViewTransition::ViewTransition - CreatedForSnapshot");
DCHECK(transition_state_callback_);
ProcessCurrentState();
}
// static
ViewTransition* ViewTransition::CreateFromSnapshotForNavigation(
Document* document,
ViewTransitionState transition_state,
Delegate* delegate) {
return MakeGarbageCollected<ViewTransition>(
document, std::move(transition_state), delegate);
}
ViewTransition::ViewTransition(Document* document,
ViewTransitionState transition_state,
Delegate* delegate)
: ExecutionContextLifecycleObserver(document->GetExecutionContext()),
creation_type_(CreationType::kFromSnapshot),
document_(document),
delegate_(delegate),
navigation_id_(transition_state.navigation_id),
document_tag_(NextDocumentTag()),
style_tracker_(MakeGarbageCollected<ViewTransitionStyleTracker>(
*document_,
std::move(transition_state))) {
TRACE_EVENT0("blink",
"ViewTransition::ViewTransition - CreatingFromSnapshot");
bool process_next_state = AdvanceTo(State::kWaitForRenderBlock);
DCHECK(process_next_state);
ProcessCurrentState();
}
void ViewTransition::skipTransition() {
SkipTransitionInternal(ScriptBoundState::Response::kRejectAbort);
}
void ViewTransition::SkipTransitionInternal(
ScriptBoundState::Response response) {
DCHECK_NE(response, ScriptBoundState::Response::kResolve);
if (IsTerminalState(state_))
return;
// Cleanup logic which is tied to ViewTransition objects created using the
// script API. If |context_destroyed_| is false the Document is being torn
// down and the script specific callbacks don't need to be dispatched.
if (!context_destroyed_ && creation_type_ == CreationType::kScript) {
DCHECK(script_bound_state_);
// If the ready promise has not yet been resolved, reject it.
if (script_bound_state_->ready_promise_property->GetState() ==
PromiseProperty::State::kPending) {
AtMicrotask(response, script_bound_state_->ready_promise_property);
}
// If we haven't run the dom change callback yet, schedule a task to do so.
// The finished promise will propagate the result of the updateCallbackDone
// promise when this callback runs.
if (static_cast<int>(state_) <
static_cast<int>(State::kDOMCallbackRunning)) {
DCHECK(!dom_callback_succeeded_);
document_->GetTaskRunner(TaskType::kMiscPlatformAPI)
->PostTask(
FROM_HERE,
WTF::BindOnce(
base::IgnoreResult(&ViewTransition::InvokeDOMChangeCallback),
WrapPersistent(this)));
} else if (static_cast<int>(state_) >=
static_cast<int>(State::kDOMCallbackFinished)) {
// If the DOM callback finished and there was a failure then the finished
// promise should have been rejected with updateCallbackDone.
if (!dom_callback_succeeded_) {
DCHECK_EQ(script_bound_state_->finished_promise_property->GetState(),
PromiseProperty::State::kRejected);
} else {
// But if the callback was successful, we need to resolve the finished
// promise while skipping the transition.
AtMicrotask(ScriptBoundState::Response::kResolve,
script_bound_state_->finished_promise_property);
}
}
}
// If we already started processing the transition (i.e. we're beyond capture
// tag discovery), then send a release directive.
if (static_cast<int>(state_) >
static_cast<int>(State::kCaptureTagDiscovery)) {
delegate_->AddPendingRequest(
ViewTransitionRequest::CreateRelease(document_tag_));
}
// We always need to call the transition state callback (mojo seems to require
// this contract), so do so if we have one and we haven't called it yet.
if (transition_state_callback_) {
DCHECK_EQ(creation_type_, CreationType::kForSnapshot);
ViewTransitionState view_transition_state;
view_transition_state.navigation_id = navigation_id_;
std::move(transition_state_callback_).Run(std::move(view_transition_state));
}
// Resume rendering, and finalize the rest of the state.
ResumeRendering();
style_tracker_->Abort();
delegate_->OnTransitionFinished(this);
// This should be the last call in this function to avoid erroneously checking
// the `state_` against the wrong state.
AdvanceTo(State::kAborted);
}
ScriptPromise ViewTransition::finished() const {
DCHECK(script_bound_state_);
return script_bound_state_->finished_promise_property->Promise(
script_bound_state_->script_state->World());
}
ScriptPromise ViewTransition::ready() const {
DCHECK(script_bound_state_);
return script_bound_state_->ready_promise_property->Promise(
script_bound_state_->script_state->World());
}
ScriptPromise ViewTransition::updateCallbackDone() const {
DCHECK(script_bound_state_);
return script_bound_state_->dom_updated_promise_property->Promise(
script_bound_state_->script_state->World());
}
bool ViewTransition::AdvanceTo(State state) {
DCHECK(CanAdvanceTo(state)) << "Current state " << static_cast<int>(state_)
<< " new state " << static_cast<int>(state);
state_ = state;
// If we need to run in a lifecycle, but we're not in one, then make sure to
// schedule an animation in case we wouldn't get one naturally.
if (StateRunsInViewTransitionStepsDuringMainFrame(state_) !=
in_main_lifecycle_update_) {
if (!in_main_lifecycle_update_) {
DCHECK(!IsTerminalState(state_));
document_->View()->ScheduleAnimation();
} else {
DCHECK(IsTerminalState(state_) || WaitsForNotification(state_));
}
return false;
}
// In all other cases, we should be able to process the state immediately. We
// don't do it in this function so that it's clear what's happening outside of
// this call.
return true;
}
bool ViewTransition::CanAdvanceTo(State state) const {
// This documents valid state transitions. Note that this does not make a
// judgement call about whether the state runs synchronously or not,
// so we allow some transitions that would not be possible in a synchronous
// run, like kCaptured -> kAborted. This isn't possible in a synchronous call,
// because kCaptured will always go to kDOMCallbackRunning.
switch (state_) {
case State::kInitial:
return state == State::kCaptureTagDiscovery ||
state == State::kWaitForRenderBlock;
case State::kCaptureTagDiscovery:
return state == State::kCaptureRequestPending || state == State::kAborted;
case State::kCaptureRequestPending:
return state == State::kCapturing || state == State::kAborted;
case State::kCapturing:
return state == State::kCaptured || state == State::kAborted;
case State::kCaptured:
return state == State::kDOMCallbackRunning ||
state == State::kDOMCallbackFinished || state == State::kAborted ||
state == State::kTransitionStateCallbackDispatched;
case State::kTransitionStateCallbackDispatched:
// This transition must finish on a ViewTransition bound to the new
// Document.
return state == State::kAborted;
case State::kWaitForRenderBlock:
return state == State::kAnimateTagDiscovery || state == State::kAborted;
case State::kDOMCallbackRunning:
return state == State::kDOMCallbackFinished || state == State::kAborted;
case State::kDOMCallbackFinished:
return state == State::kAnimateTagDiscovery || state == State::kAborted;
case State::kAnimateTagDiscovery:
return state == State::kAnimateRequestPending || state == State::kAborted;
case State::kAnimateRequestPending:
return state == State::kAnimating || state == State::kAborted;
case State::kAnimating:
return state == State::kFinished || state == State::kAborted;
case State::kAborted:
// We allow aborted to move to timed out state, so that time out can call
// skipTransition and then change the state to timed out.
return state == State::kTimedOut;
case State::kFinished:
case State::kTimedOut:
return false;
}
NOTREACHED();
return false;
}
// static
bool ViewTransition::StateRunsInViewTransitionStepsDuringMainFrame(
State state) {
switch (state) {
case State::kInitial:
return false;
case State::kCaptureTagDiscovery:
case State::kCaptureRequestPending:
return true;
case State::kCapturing:
case State::kCaptured:
case State::kWaitForRenderBlock:
case State::kDOMCallbackRunning:
case State::kDOMCallbackFinished:
case State::kAnimateTagDiscovery:
case State::kAnimateRequestPending:
return false;
case State::kAnimating:
return true;
case State::kFinished:
case State::kAborted:
case State::kTimedOut:
case State::kTransitionStateCallbackDispatched:
return false;
}
NOTREACHED();
return false;
}
// static
bool ViewTransition::WaitsForNotification(State state) {
return state == State::kCapturing || state == State::kDOMCallbackRunning ||
state == State::kWaitForRenderBlock ||
state == State::kTransitionStateCallbackDispatched;
}
// static
bool ViewTransition::IsTerminalState(State state) {
return state == State::kFinished || state == State::kAborted ||
state == State::kTimedOut;
}
void ViewTransition::ProcessCurrentState() {
bool process_next_state = true;
while (process_next_state) {
DCHECK_EQ(in_main_lifecycle_update_,
StateRunsInViewTransitionStepsDuringMainFrame(state_));
TRACE_EVENT1("blink", "ViewTransition::ProcessCurrentState", "state",
StateToString(state_));
process_next_state = false;
switch (state_) {
// Initial state: nothing to do, just advance the state
case State::kInitial:
process_next_state = AdvanceTo(State::kCaptureTagDiscovery);
DCHECK(!process_next_state);
break;
// Update the lifecycle if needed and discover the elements (deferred to
// AddSharedElementsFromCSS).
case State::kCaptureTagDiscovery:
DCHECK(in_main_lifecycle_update_);
DCHECK_GE(document_->Lifecycle().GetState(),
DocumentLifecycle::kCompositingInputsClean);
style_tracker_->AddSharedElementsFromCSS();
process_next_state = AdvanceTo(State::kCaptureRequestPending);
DCHECK(process_next_state);
break;
// Capture request pending -- create the request
case State::kCaptureRequestPending:
if (!style_tracker_->Capture()) {
SkipTransitionInternal(
ScriptBoundState::Response::kRejectInvalidState);
break;
}
delegate_->AddPendingRequest(ViewTransitionRequest::CreateCapture(
document_tag_, style_tracker_->CapturedTagCount(), navigation_id_,
style_tracker_->TakeCaptureResourceIds(),
ConvertToBaseOnceCallback(
CrossThreadBindOnce(&ViewTransition::NotifyCaptureFinished,
MakeUnwrappingCrossThreadHandle(this)))));
if (document_->GetFrame()->IsLocalRoot()) {
document_->GetPage()->GetChromeClient().StopDeferringCommits(
*document_->GetFrame(),
cc::PaintHoldingCommitTrigger::kViewTransition);
}
document_->GetPage()->GetChromeClient().RegisterForCommitObservation(
this);
process_next_state = AdvanceTo(State::kCapturing);
DCHECK(!process_next_state);
break;
case State::kCapturing:
DCHECK(WaitsForNotification(state_));
break;
case State::kCaptured: {
style_tracker_->CaptureResolved();
if (creation_type_ == CreationType::kForSnapshot) {
DCHECK(transition_state_callback_);
ViewTransitionState view_transition_state =
style_tracker_->GetViewTransitionState();
view_transition_state.navigation_id = navigation_id_;
process_next_state =
AdvanceTo(State::kTransitionStateCallbackDispatched);
DCHECK(process_next_state);
std::move(transition_state_callback_)
.Run(std::move(view_transition_state));
break;
}
// The following logic is only executed for ViewTransition objects
// created by the script API.
DCHECK(script_bound_state_);
switch (InvokeDOMChangeCallback()) {
case DOMCallbackResult::kFinished:
process_next_state = AdvanceTo(State::kDOMCallbackFinished);
DCHECK(process_next_state);
DCHECK(dom_callback_succeeded_);
break;
case DOMCallbackResult::kFailed:
process_next_state = AdvanceTo(State::kDOMCallbackFinished);
DCHECK(process_next_state);
DCHECK(!dom_callback_succeeded_);
SkipTransitionInternal(ScriptBoundState::Response::kRejectAbort);
break;
case DOMCallbackResult::kRunning:
process_next_state = AdvanceTo(State::kDOMCallbackRunning);
DCHECK(process_next_state);
break;
}
break;
}
case State::kWaitForRenderBlock:
DCHECK(WaitsForNotification(state_));
break;
case State::kDOMCallbackRunning:
DCHECK(WaitsForNotification(state_));
break;
case State::kDOMCallbackFinished:
ResumeRendering();
if (!dom_callback_succeeded_) {
SkipTransitionInternal(ScriptBoundState::Response::kRejectAbort);
break;
}
process_next_state = AdvanceTo(State::kAnimateTagDiscovery);
DCHECK(process_next_state);
break;
case State::kAnimateTagDiscovery:
DCHECK(!in_main_lifecycle_update_);
document_->View()->UpdateLifecycleToPrePaintClean(
DocumentUpdateReason::kViewTransition);
DCHECK_GE(document_->Lifecycle().GetState(),
DocumentLifecycle::kPrePaintClean);
style_tracker_->AddSharedElementsFromCSS();
process_next_state = AdvanceTo(State::kAnimateRequestPending);
DCHECK(process_next_state);
break;
case State::kAnimateRequestPending:
if (!style_tracker_->Start()) {
SkipTransitionInternal(
ScriptBoundState::Response::kRejectInvalidState);
break;
}
delegate_->AddPendingRequest(
ViewTransitionRequest::CreateAnimateRenderer(document_tag_,
navigation_id_));
process_next_state = AdvanceTo(State::kAnimating);
DCHECK(!process_next_state);
DCHECK(!in_main_lifecycle_update_);
if (creation_type_ == CreationType::kScript) {
AtMicrotask(ScriptBoundState::Response::kResolve,
script_bound_state_->ready_promise_property);
}
break;
case State::kAnimating: {
if (first_animating_frame_) {
first_animating_frame_ = false;
// We need to schedule an animation frame, in case this is the only
// kAnimating frame we will get, so that we can clean up in the next
// frame.
document_->View()->ScheduleAnimation();
break;
}
if (style_tracker_->HasActiveAnimations())
break;
style_tracker_->StartFinished();
if (creation_type_ == CreationType::kScript) {
AtMicrotask(ScriptBoundState::Response::kResolve,
script_bound_state_->finished_promise_property);
}
delegate_->AddPendingRequest(
ViewTransitionRequest::CreateRelease(document_tag_));
delegate_->OnTransitionFinished(this);
style_tracker_ = nullptr;
process_next_state = AdvanceTo(State::kFinished);
DCHECK(!process_next_state);
break;
}
case State::kFinished:
case State::kAborted:
case State::kTimedOut:
case State::kTransitionStateCallbackDispatched:
break;
}
}
}
void ViewTransition::Trace(Visitor* visitor) const {
visitor->Trace(document_);
visitor->Trace(style_tracker_);
visitor->Trace(script_bound_state_);
ScriptWrappable::Trace(visitor);
ActiveScriptWrappable::Trace(visitor);
ExecutionContextLifecycleObserver::Trace(visitor);
}
bool ViewTransition::MatchForOnlyChild(
PseudoId pseudo_id,
AtomicString view_transition_name) const {
if (!style_tracker_)
return false;
return style_tracker_->MatchForOnlyChild(pseudo_id, view_transition_name);
}
ViewTransition::DOMCallbackResult ViewTransition::InvokeDOMChangeCallback() {
DCHECK(script_bound_state_);
if (!script_bound_state_->update_dom_callback) {
dom_callback_succeeded_ = true;
AtMicrotask(ScriptBoundState::Response::kResolve,
script_bound_state_->dom_updated_promise_property);
// If we're already at the terminal state, the dom update callback was
// scheduled to run after the transition was skipped.
if (IsDone()) {
AtMicrotask(ScriptBoundState::Response::kResolve,
script_bound_state_->finished_promise_property);
}
return DOMCallbackResult::kFinished;
}
v8::Maybe<ScriptPromise> result =
script_bound_state_->update_dom_callback->Invoke(nullptr);
// TODO(vmpstr): Should this be a DCHECK?
if (result.IsNothing()) {
dom_callback_succeeded_ = false;
AtMicrotask(ScriptBoundState::Response::kRejectAbort,
script_bound_state_->dom_updated_promise_property);
AtMicrotask(ScriptBoundState::Response::kRejectAbort,
script_bound_state_->finished_promise_property);
return DOMCallbackResult::kFailed;
}
ScriptState::Scope scope(script_bound_state_->script_state);
result.ToChecked().Then(
MakeGarbageCollected<ScriptFunction>(
script_bound_state_->script_state,
MakeGarbageCollected<DOMChangeFinishedCallback>(this, true)),
MakeGarbageCollected<ScriptFunction>(
script_bound_state_->script_state,
MakeGarbageCollected<DOMChangeFinishedCallback>(this, false)));
return DOMCallbackResult::kRunning;
}
void ViewTransition::ContextDestroyed() {
TRACE_EVENT0("blink", "ViewTransition::ContextDestroyed");
// TODO(khushalsagar): This needs to be called for pages entering BFCache.
context_destroyed_ = true;
SkipTransitionInternal(ScriptBoundState::Response::kRejectAbort);
}
bool ViewTransition::HasPendingActivity() const {
return !IsTerminalState(state_);
}
void ViewTransition::NotifyCaptureFinished() {
if (state_ != State::kCapturing) {
DCHECK(IsTerminalState(state_));
return;
}
bool process_next_state = AdvanceTo(State::kCaptured);
DCHECK(process_next_state);
ProcessCurrentState();
}
void ViewTransition::NotifyDOMCallbackFinished(bool success,
ScriptValue value) {
// Handle all promises which depend on this callback.
if (success) {
script_bound_state_->dom_updated_promise_property->ResolveWithUndefined();
// If we're already at the terminal state, the transition was skipped before
// the callback finished. Also handle the finish promise.
if (IsDone())
script_bound_state_->finished_promise_property->ResolveWithUndefined();
} else {
script_bound_state_->dom_updated_promise_property->Reject(value.V8Value());
// The ready promise rejects with the value of updateCallbackDone callback
// if it's skipped because of an error in the callback.
if (!IsDone())
script_bound_state_->ready_promise_property->Reject(value.V8Value());
// If the domUpdate callback fails the transition is skipped. The finish
// promise should mirror the result of updateCallbackDone.
script_bound_state_->finished_promise_property->Reject(value.V8Value());
}
dom_callback_succeeded_ = success;
if (IsTerminalState(state_))
return;
bool process_next_state = AdvanceTo(State::kDOMCallbackFinished);
DCHECK(process_next_state);
ProcessCurrentState();
}
bool ViewTransition::NeedsSharedElementEffectNode(
const LayoutObject& object) const {
// Layout view always needs an effect node, even if root itself is not
// transitioning. The reason for this is that we want the root to have an
// effect which can be hoisted up be the sibling of the layout view. This
// simplifies calling code to have a consistent stacking context structure.
if (IsA<LayoutView>(object))
return !IsTerminalState(state_);
// Otherwise check if the layout object has an active shared element.
auto* element = DynamicTo<Element>(object.GetNode());
return element && style_tracker_ && style_tracker_->IsSharedElement(element);
}
bool ViewTransition::IsRepresentedViaPseudoElements(
const LayoutObject& object) const {
if (IsTerminalState(state_))
return false;
if (IsA<LayoutView>(object))
return style_tracker_->IsRootTransitioning();
auto* element = DynamicTo<Element>(object.GetNode());
return element && style_tracker_->IsSharedElement(element);
}
PaintPropertyChangeType ViewTransition::UpdateEffect(
const LayoutObject& object,
const EffectPaintPropertyNodeOrAlias& current_effect,
const ClipPaintPropertyNodeOrAlias* current_clip,
const TransformPaintPropertyNodeOrAlias* current_transform) {
DCHECK(NeedsSharedElementEffectNode(object));
DCHECK(current_transform);
DCHECK(current_clip);
EffectPaintPropertyNode::State state;
state.direct_compositing_reasons =
CompositingReason::kViewTransitionSharedElement;
state.local_transform_space = current_transform;
state.output_clip = current_clip;
state.view_transition_shared_element_id =
ViewTransitionElementId(document_tag_);
state.compositor_element_id = CompositorElementIdFromUniqueObjectId(
object.UniqueId(),
CompositorElementIdNamespace::kSharedElementTransition);
auto* element = DynamicTo<Element>(object.GetNode());
if (!element) {
// The only non-element participant is the layout view.
DCHECK(object.IsLayoutView());
style_tracker_->UpdateRootIndexAndSnapshotId(
state.view_transition_shared_element_id,
state.view_transition_element_resource_id);
DCHECK(state.view_transition_shared_element_id.valid() ||
!style_tracker_->IsRootTransitioning());
return style_tracker_->UpdateRootEffect(std::move(state), current_effect);
}
style_tracker_->UpdateElementIndicesAndSnapshotId(
element, state.view_transition_shared_element_id,
state.view_transition_element_resource_id);
return style_tracker_->UpdateEffect(element, std::move(state),
current_effect);
}
EffectPaintPropertyNode* ViewTransition::GetEffect(
const LayoutObject& object) const {
DCHECK(NeedsSharedElementEffectNode(object));
auto* element = DynamicTo<Element>(object.GetNode());
if (!element)
return style_tracker_->GetRootEffect();
return style_tracker_->GetEffect(element);
}
void ViewTransition::RunViewTransitionStepsOutsideMainFrame() {
DCHECK(document_->Lifecycle().GetState() >=
DocumentLifecycle::kPrePaintClean);
DCHECK(!in_main_lifecycle_update_);
if (state_ == State::kAnimating && style_tracker_ &&
!style_tracker_->RunPostPrePaintSteps()) {
SkipTransitionInternal(ScriptBoundState::Response::kRejectInvalidState);
}
}
void ViewTransition::RunViewTransitionStepsDuringMainFrame() {
DCHECK_GE(document_->Lifecycle().GetState(),
DocumentLifecycle::kPrePaintClean);
DCHECK(!in_main_lifecycle_update_);
base::AutoReset<bool> scope(&in_main_lifecycle_update_, true);
if (StateRunsInViewTransitionStepsDuringMainFrame(state_))
ProcessCurrentState();
if (style_tracker_ &&
document_->Lifecycle().GetState() >= DocumentLifecycle::kPrePaintClean &&
!style_tracker_->RunPostPrePaintSteps()) {
SkipTransitionInternal(ScriptBoundState::Response::kRejectInvalidState);
}
}
bool ViewTransition::NeedsUpToDateTags() const {
return state_ == State::kCaptureTagDiscovery ||
state_ == State::kAnimateTagDiscovery;
}
PseudoElement* ViewTransition::CreatePseudoElement(
Element* parent,
PseudoId pseudo_id,
const AtomicString& view_transition_name) {
DCHECK(style_tracker_);
return style_tracker_->CreatePseudoElement(parent, pseudo_id,
view_transition_name);
}
String ViewTransition::UAStyleSheet() const {
// TODO(vmpstr): We can still request getComputedStyle(html,
// "::view-transition-pseudo") outside of a page transition. What should we
// return in that case?
if (!style_tracker_)
return "";
return style_tracker_->UAStyleSheet();
}
void ViewTransition::WillCommitCompositorFrame() {
// There should only be 1 commit when we're in the capturing phase and
// rendering is paused immediately after it finishes.
if (state_ == State::kCapturing)
PauseRendering();
}
gfx::Rect ViewTransition::GetSnapshotViewportRect() const {
if (!style_tracker_)
return gfx::Rect();
return style_tracker_->GetSnapshotViewportRect();
}
gfx::Vector2d ViewTransition::GetRootSnapshotPaintOffset() const {
if (!style_tracker_)
return gfx::Vector2d();
return style_tracker_->GetRootSnapshotPaintOffset();
}
void ViewTransition::PauseRendering() {
DCHECK(!rendering_paused_scope_);
if (!document_->GetPage() || !document_->View())
return;
rendering_paused_scope_.emplace(*document_);
document_->GetPage()->GetChromeClient().UnregisterFromCommitObservation(this);
TRACE_EVENT_NESTABLE_ASYNC_BEGIN0("blink", "ViewTransition::PauseRendering",
this);
const base::TimeDelta kTimeout = [this]() {
if (auto* settings = document_->GetFrame()->GetContentSettingsClient();
settings &&
settings->IncreaseSharedElementTransitionCallbackTimeout()) {
return base::Seconds(15);
} else {
return base::Seconds(4);
}
}();
document_->GetTaskRunner(TaskType::kInternalFrameLifecycleControl)
->PostDelayedTask(FROM_HERE,
WTF::BindOnce(&ViewTransition::OnRenderingPausedTimeout,
WrapWeakPersistent(this)),
kTimeout);
}
void ViewTransition::OnRenderingPausedTimeout() {
if (!rendering_paused_scope_)
return;
ResumeRendering();
SkipTransitionInternal(ScriptBoundState::Response::kRejectTimeout);
AdvanceTo(State::kTimedOut);
}
void ViewTransition::ResumeRendering() {
if (!rendering_paused_scope_)
return;
TRACE_EVENT_NESTABLE_ASYNC_END0("blink", "ViewTransition::PauseRendering",
this);
rendering_paused_scope_.reset();
}
void ViewTransition::AtMicrotask(ScriptBoundState::Response response,
PromiseProperty* property) {
document_->GetAgent().event_loop()->EnqueueMicrotask(
WTF::BindOnce(&ViewTransition::ScriptBoundState::HandlePromise,
WrapPersistent(script_bound_state_.Get()), response,
WrapPersistent(property)));
}
void ViewTransition::WillBeginMainFrame() {
if (state_ != State::kWaitForRenderBlock)
return;
// WillBeginMainFrame() implies that rendering has started. If we were waiting
// for render-blocking resources to be loaded, they must have been fetched (or
// timed out) before rendering is started.
DCHECK(document_->RenderingHasBegun());
bool process_next_state = AdvanceTo(State::kAnimateTagDiscovery);
DCHECK(process_next_state);
ProcessCurrentState();
}
bool ViewTransition::ShouldThrottleRendering() const {
return rendering_paused_scope_ &&
rendering_paused_scope_->ShouldThrottleRendering();
}
} // namespace blink