blob: fab6e16cef65eee0d2060fe98bc5082af868fdfb [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_supplement.h"
#include "cc/trees/layer_tree_host.h"
#include "cc/view_transition/view_transition_request.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_view_transition_callback.h"
#include "third_party/blink/renderer/core/dom/document.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/style/computed_style.h"
#include "third_party/blink/renderer/core/view_transition/view_transition.h"
#include "third_party/blink/renderer/core/view_transition/view_transition_utils.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/graphics/compositing/paint_artifact_compositor.h"
#include "third_party/blink/renderer/platform/scheduler/public/thread_scheduler.h"
namespace blink {
namespace {
bool HasActiveTransitionInAncestorFrame(LocalFrame* frame) {
auto* parent = frame ? frame->Parent() : nullptr;
while (parent && parent->IsLocalFrame()) {
if (To<LocalFrame>(parent)->GetDocument() &&
ViewTransitionUtils::GetActiveTransition(
*To<LocalFrame>(parent)->GetDocument())) {
return true;
}
parent = parent->Parent();
}
return false;
}
// Skips transitions in all local frames underneath |curr_frame|'s local root
// except |curr_frame| itself.
void SkipTransitionInAllLocalFrames(LocalFrame* curr_frame) {
auto* root_view = curr_frame ? curr_frame->LocalFrameRoot().View() : nullptr;
if (!root_view)
return;
root_view->ForAllChildLocalFrameViews([curr_frame](LocalFrameView& child) {
if (child.GetFrame() == *curr_frame)
return;
auto* document = child.GetFrame().GetDocument();
auto* transition = document
? ViewTransitionUtils::GetActiveTransition(*document)
: nullptr;
if (!transition)
return;
transition->skipTransition();
DCHECK(!ViewTransitionUtils::GetActiveTransition(*document));
});
}
} // namespace
// static
const char ViewTransitionSupplement::kSupplementName[] = "ViewTransition";
// static
ViewTransitionSupplement* ViewTransitionSupplement::FromIfExists(
const Document& document) {
return Supplement<Document>::From<ViewTransitionSupplement>(document);
}
// static
ViewTransitionSupplement* ViewTransitionSupplement::From(Document& document) {
auto* supplement =
Supplement<Document>::From<ViewTransitionSupplement>(document);
if (!supplement) {
supplement = MakeGarbageCollected<ViewTransitionSupplement>(document);
Supplement<Document>::ProvideTo(document, supplement);
}
return supplement;
}
// static
ViewTransition* ViewTransitionSupplement::startViewTransition(
ScriptState* script_state,
Document& document,
V8ViewTransitionCallback* callback,
ExceptionState& exception_state) {
DCHECK(script_state);
DCHECK(ThreadScheduler::Current());
auto* supplement = From(document);
if (callback) {
auto* tracker = ThreadScheduler::Current()->GetTaskAttributionTracker();
// Set the parent task ID if we're not in an extension task (as extensions
// are not currently supported in TaskAttributionTracker).
if (tracker && script_state->World().IsMainWorld()) {
auto id = tracker->RunningTaskAttributionId(script_state);
callback->SetParentTaskId(id);
}
}
return supplement->StartTransition(script_state, document, callback,
exception_state);
}
ViewTransition* ViewTransitionSupplement::StartTransition(
ScriptState* script_state,
Document& document,
V8ViewTransitionCallback* callback,
ExceptionState& exception_state) {
// Disallow script initiated transitions during a navigation initiated
// transition.
if (transition_ && !transition_->IsCreatedViaScriptAPI())
return nullptr;
if (transition_)
transition_->skipTransition();
DCHECK(!transition_)
<< "skipTransition() should finish existing |transition_|";
// We need to be connected to a view to have a transition. We also need a
// document element, since that's the originating element for the pseudo tree.
if (!document.View() || !document.documentElement()) {
return nullptr;
}
transition_ =
ViewTransition::CreateFromScript(&document, script_state, callback, this);
// If there is a transition in a parent frame, give that precedence over a
// transition in a child frame.
if (HasActiveTransitionInAncestorFrame(document.GetFrame())) {
auto skipped_transition = transition_;
skipped_transition->skipTransition();
DCHECK(!transition_);
return skipped_transition;
}
// Skip transitions in all frames associated with this widget. We can only
// have one transition per widget/CC.
SkipTransitionInAllLocalFrames(document.GetFrame());
DCHECK(transition_);
return transition_;
}
// static
void ViewTransitionSupplement::SnapshotDocumentForNavigation(
Document& document,
ViewTransition::ViewTransitionStateCallback callback) {
DCHECK(RuntimeEnabledFeatures::ViewTransitionOnNavigationEnabled());
auto* supplement = From(document);
supplement->StartTransition(document, std::move(callback));
}
void ViewTransitionSupplement::StartTransition(
Document& document,
ViewTransition::ViewTransitionStateCallback callback) {
if (transition_) {
// We should skip a transition if one exists, regardless of how it was
// created, since navigation transition takes precedence.
transition_->skipTransition();
}
DCHECK(!transition_)
<< "skipTransition() should finish existing |transition_|";
transition_ = ViewTransition::CreateForSnapshotForNavigation(
&document, std::move(callback), this);
}
// static
void ViewTransitionSupplement::CreateFromSnapshotForNavigation(
Document& document,
ViewTransitionState transition_state) {
DCHECK(RuntimeEnabledFeatures::ViewTransitionOnNavigationEnabled());
auto* supplement = From(document);
supplement->StartTransition(document, std::move(transition_state));
}
// static
void ViewTransitionSupplement::AbortTransition(Document& document) {
auto* supplement = FromIfExists(document);
if (supplement && supplement->transition_) {
supplement->transition_->skipTransition();
DCHECK(!supplement->transition_);
}
}
void ViewTransitionSupplement::StartTransition(
Document& document,
ViewTransitionState transition_state) {
DCHECK(!transition_) << "Existing transition on new Document";
transition_ = ViewTransition::CreateFromSnapshotForNavigation(
&document, std::move(transition_state), this);
// We may already be past the render blocking if this page is coming back from
// a BFCache or has been pre-rendered. In that case, let the transition know
// to advance the state. Note that this has to be done outside of
// `CreateFromSnapshotForNavigation`, because future phases will cause parts
// of the code (layout & paint specifically) to try and access the transition
// object, which wouldn't have been set yet if the following code is done in
// the constructor.
if (document.RenderingHasBegun()) {
transition_->NotifyRenderingHasBegun();
}
}
void ViewTransitionSupplement::OnTransitionFinished(
ViewTransition* transition) {
// TODO(vmpstr): Do we need to explicitly reset transition state?
if (transition == transition_)
transition_ = nullptr;
}
ViewTransition* ViewTransitionSupplement::GetActiveTransition() {
return transition_;
}
ViewTransitionSupplement::ViewTransitionSupplement(Document& document)
: Supplement<Document>(document) {}
ViewTransitionSupplement::~ViewTransitionSupplement() = default;
void ViewTransitionSupplement::Trace(Visitor* visitor) const {
visitor->Trace(transition_);
Supplement<Document>::Trace(visitor);
}
void ViewTransitionSupplement::AddPendingRequest(
std::unique_ptr<ViewTransitionRequest> request) {
pending_requests_.push_back(std::move(request));
auto* document = GetSupplementable();
if (!document || !document->GetPage() || !document->View())
return;
// Schedule a new frame.
document->View()->ScheduleAnimation();
// Ensure paint artifact compositor does an update, since that's the mechanism
// we use to pass transition requests to the compositor.
document->View()->SetPaintArtifactCompositorNeedsUpdate();
}
VectorOf<std::unique_ptr<ViewTransitionRequest>>
ViewTransitionSupplement::TakePendingRequests() {
return std::move(pending_requests_);
}
void ViewTransitionSupplement::OnMetaTagChanged(
const AtomicString& content_value) {
auto same_origin_opt_in =
EqualIgnoringASCIICase(content_value, "same-origin")
? mojom::ViewTransitionSameOriginOptIn::kEnabled
: mojom::ViewTransitionSameOriginOptIn::kDisabled;
if (same_origin_opt_in_ == same_origin_opt_in) {
return;
}
same_origin_opt_in_ = same_origin_opt_in;
// If we have a frame, notify the frame host that the opt-in has changed.
if (auto* document = GetSupplementable(); document->GetFrame()) {
document->GetFrame()
->GetLocalFrameHostRemote()
.OnViewTransitionOptInChanged(same_origin_opt_in);
}
if (same_origin_opt_in_ == mojom::ViewTransitionSameOriginOptIn::kDisabled &&
transition_ && !transition_->IsCreatedViaScriptAPI()) {
transition_->skipTransition();
DCHECK(!transition_)
<< "skipTransition() should finish existing |transition_|";
}
}
void ViewTransitionSupplement::WillInsertBody() {
// If the opt-in is enabled, then there's nothing to do in this function.
if (same_origin_opt_in_ == mojom::ViewTransitionSameOriginOptIn::kEnabled) {
return;
}
// Since we don't have an opt-in, skip a navigation transition if it exists.
if (transition_ && transition_->IsForNavigationOnNewDocument()) {
transition_->skipTransition();
DCHECK(!transition_)
<< "skipTransition() should finish existing |transition_|";
}
}
} // namespace blink