blob: a3c2628e235d729c99cc201412767390bff3a9ab [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// 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/page/validation_message_overlay_delegate.h"
#include <memory>
#include "base/memory/ptr_util.h"
#include "third_party/blink/public/common/tokens/tokens.h"
#include "third_party/blink/public/resources/grit/blink_resources.h"
#include "third_party/blink/renderer/core/dom/dom_token_list.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/dom/events/event_dispatch_forbidden_scope.h"
#include "third_party/blink/renderer/core/frame/settings.h"
#include "third_party/blink/renderer/core/frame/visual_viewport.h"
#include "third_party/blink/renderer/core/layout/layout_view.h"
#include "third_party/blink/renderer/core/loader/empty_clients.h"
#include "third_party/blink/renderer/core/page/page.h"
#include "third_party/blink/renderer/core/page/page_popup_client.h"
#include "third_party/blink/renderer/platform/data_resource_helper.h"
#include "third_party/blink/renderer/platform/graphics/paint/cull_rect.h"
#include "third_party/blink/renderer/platform/graphics/paint/drawing_recorder.h"
#include "third_party/blink/renderer/platform/graphics/paint/paint_record_builder.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
#include "third_party/blink/renderer/platform/text/platform_locale.h"
#include "third_party/blink/renderer/platform/web_test_support.h"
namespace blink {
// ChromeClient for an internal page of ValidationMessageOverlayDelegate.
class ValidationMessageChromeClient : public EmptyChromeClient {
public:
explicit ValidationMessageChromeClient(ChromeClient& main_chrome_client,
LocalFrameView* anchor_view)
: main_chrome_client_(main_chrome_client), anchor_view_(anchor_view) {}
void Trace(Visitor* visitor) const override {
visitor->Trace(main_chrome_client_);
visitor->Trace(anchor_view_);
EmptyChromeClient::Trace(visitor);
}
void ScheduleAnimation(const LocalFrameView*,
base::TimeDelta delay = base::TimeDelta()) override {
// Need to pass LocalFrameView for the anchor element because the Frame for
// this overlay doesn't have an associated WebFrameWidget, which schedules
// animation.
main_chrome_client_->ScheduleAnimation(anchor_view_, delay);
}
float WindowToViewportScalar(LocalFrame* local_frame,
const float scalar_value) const override {
return main_chrome_client_->WindowToViewportScalar(local_frame,
scalar_value);
}
private:
Member<ChromeClient> main_chrome_client_;
Member<LocalFrameView> anchor_view_;
};
ValidationMessageOverlayDelegate::ValidationMessageOverlayDelegate(
Page& main_page,
const Element& anchor,
const String& message,
TextDirection message_dir,
const String& sub_message,
TextDirection sub_message_dir)
: main_page_(main_page),
anchor_(anchor),
message_(message),
sub_message_(sub_message),
message_dir_(message_dir),
sub_message_dir_(sub_message_dir) {}
ValidationMessageOverlayDelegate::~ValidationMessageOverlayDelegate() {
if (page_) {
// This function can be called in EventDispatchForbiddenScope for the main
// document, and the following operations dispatch some events. It's safe
// because the page can't listen the events.
EventDispatchForbiddenScope::AllowUserAgentEvents allow_events;
page_->WillBeDestroyed();
}
}
LocalFrameView& ValidationMessageOverlayDelegate::FrameView() const {
DCHECK(page_)
<< "Do not call FrameView() before the first call of CreatePage()";
return *To<LocalFrame>(page_->MainFrame())->View();
}
void ValidationMessageOverlayDelegate::PaintFrameOverlay(
const FrameOverlay& overlay,
GraphicsContext& context,
const IntSize& view_size) const {
if (IsHiding() && !page_)
return;
if (DrawingRecorder::UseCachedDrawingIfPossible(context, overlay,
DisplayItem::kFrameOverlay))
return;
DrawingRecorder recorder(context, overlay, DisplayItem::kFrameOverlay,
IntRect(IntPoint(), view_size));
const_cast<ValidationMessageOverlayDelegate*>(this)->UpdateFrameViewState(
overlay, view_size);
if (RuntimeEnabledFeatures::CompositeAfterPaintEnabled()) {
context.DrawRecord(FrameView().GetPaintRecord());
} else {
// The overlay frame is has a standalone paint property tree. Paint it in
// its root space into a paint record, then draw the record into the proper
// target space in the overlaid frame.
PaintRecordBuilder paint_record_builder(context);
FrameView().PaintOutsideOfLifecycle(paint_record_builder.Context(),
kGlobalPaintNormalPhase);
context.DrawRecord(paint_record_builder.EndRecording());
}
}
void ValidationMessageOverlayDelegate::ServiceScriptedAnimations(
base::TimeTicks monotonic_frame_begin_time) {
page_->Animator().ServiceScriptedAnimations(monotonic_frame_begin_time);
}
void ValidationMessageOverlayDelegate::UpdateFrameViewState(
const FrameOverlay& overlay,
const IntSize& view_size) {
if (FrameView().Size() != view_size) {
FrameView().Resize(view_size);
page_->GetVisualViewport().SetSize(view_size);
}
IntRect intersection = overlay.Frame().RemoteViewportIntersection();
AdjustBubblePosition(intersection.IsEmpty() ? IntRect(IntPoint(), view_size)
: intersection);
// This manual invalidation is necessary to avoid a DCHECK failure in
// FindVisualRectNeedingUpdateScopeBase::CheckVisualRect().
FrameView().GetLayoutView()->SetSubtreeShouldCheckForPaintInvalidation();
FrameView().UpdateAllLifecyclePhases(DocumentUpdateReason::kOverlay);
}
void ValidationMessageOverlayDelegate::CreatePage(const FrameOverlay& overlay) {
DCHECK(!page_);
// TODO(tkent): Can we share code with WebPagePopupImpl and
// InspectorOverlayAgent?
IntSize view_size = overlay.Size();
Page::PageClients page_clients;
FillWithEmptyClients(page_clients);
chrome_client_ = MakeGarbageCollected<ValidationMessageChromeClient>(
main_page_->GetChromeClient(), anchor_->GetDocument().View());
page_clients.chrome_client = chrome_client_;
Settings& main_settings = main_page_->GetSettings();
page_ = Page::CreateNonOrdinary(
page_clients, main_page_->GetPageScheduler()->GetAgentGroupScheduler());
page_->GetSettings().SetMinimumFontSize(main_settings.GetMinimumFontSize());
page_->GetSettings().SetMinimumLogicalFontSize(
main_settings.GetMinimumLogicalFontSize());
auto* frame = MakeGarbageCollected<LocalFrame>(
MakeGarbageCollected<EmptyLocalFrameClient>(), *page_, nullptr, nullptr,
nullptr, FrameInsertType::kInsertInConstructor, LocalFrameToken(),
nullptr, nullptr,
/* policy_container */ nullptr);
frame->SetView(MakeGarbageCollected<LocalFrameView>(*frame, view_size));
frame->Init(nullptr);
frame->View()->SetCanHaveScrollbars(false);
frame->View()->SetBaseBackgroundColor(Color::kTransparent);
page_->GetVisualViewport().SetSize(view_size);
scoped_refptr<SharedBuffer> data = SharedBuffer::Create();
WriteDocument(data.get());
float zoom_factor = anchor_->GetDocument().GetFrame()->PageZoomFactor();
frame->SetPageZoomFactor(zoom_factor);
// Propagate deprecated DSF for platforms without use-zoom-for-dsf.
page_->SetDeviceScaleFactorDeprecated(
main_page_->DeviceScaleFactorDeprecated());
frame->ForceSynchronousDocumentInstall("text/html", data);
Element& main_message = GetElementById("main-message");
main_message.setTextContent(message_);
Element& sub_message = GetElementById("sub-message");
sub_message.setTextContent(sub_message_);
Element& container = GetElementById("container");
if (WebTestSupport::IsRunningWebTest()) {
container.SetInlineStyleProperty(CSSPropertyID::kTransition, "none");
GetElementById("icon").SetInlineStyleProperty(CSSPropertyID::kTransition,
"none");
main_message.SetInlineStyleProperty(CSSPropertyID::kTransition, "none");
sub_message.SetInlineStyleProperty(CSSPropertyID::kTransition, "none");
}
// Get the size to decide position later.
// TODO(schenney): This says get size, so we only need to update to layout.
FrameView().UpdateAllLifecyclePhases(DocumentUpdateReason::kOverlay);
bubble_size_ = container.VisibleBoundsInVisualViewport().Size();
// Add one because the content sometimes exceeds the exact width due to
// rounding errors.
bubble_size_.Expand(1, 0);
container.SetInlineStyleProperty(CSSPropertyID::kMinWidth,
bubble_size_.Width() / zoom_factor,
CSSPrimitiveValue::UnitType::kPixels);
container.setAttribute(html_names::kClassAttr, "shown-initially");
FrameView().UpdateAllLifecyclePhases(DocumentUpdateReason::kOverlay);
}
void ValidationMessageOverlayDelegate::WriteDocument(SharedBuffer* data) {
DCHECK(data);
PagePopupClient::AddString("<!DOCTYPE html><html><head><style>", data);
data->Append(UncompressResourceAsBinary(IDR_VALIDATION_BUBBLE_CSS));
PagePopupClient::AddString("</style></head>", data);
PagePopupClient::AddString(
Locale::DefaultLocale().IsRTL() ? "<body dir=rtl>" : "<body dir=ltr>",
data);
PagePopupClient::AddString(
"<div id=container>"
"<div id=outer-arrow-top></div>"
"<div id=inner-arrow-top></div>"
"<div id=spacer-top></div>"
"<main id=bubble-body>",
data);
data->Append(UncompressResourceAsBinary(IDR_VALIDATION_BUBBLE_ICON));
PagePopupClient::AddString(message_dir_ == TextDirection::kLtr
? "<div dir=ltr id=main-message></div>"
: "<div dir=rtl id=main-message></div>",
data);
PagePopupClient::AddString(sub_message_dir_ == TextDirection::kLtr
? "<div dir=ltr id=sub-message></div>"
: "<div dir=rtl id=sub-message></div>",
data);
PagePopupClient::AddString(
"</main>"
"<div id=outer-arrow-bottom></div>"
"<div id=inner-arrow-bottom></div>"
"<div id=spacer-bottom></div>"
"</div></body></html>\n",
data);
}
Element& ValidationMessageOverlayDelegate::GetElementById(
const AtomicString& id) const {
Element* element =
To<LocalFrame>(page_->MainFrame())->GetDocument()->getElementById(id);
DCHECK(element) << "No element with id=" << id
<< ". Failed to load the document?";
return *element;
}
void ValidationMessageOverlayDelegate::AdjustBubblePosition(
const IntRect& view_rect) {
if (IsHiding())
return;
float zoom_factor = To<LocalFrame>(page_->MainFrame())->PageZoomFactor();
IntRect anchor_rect = anchor_->VisibleBoundsInVisualViewport();
bool show_bottom_arrow = false;
double bubble_y = anchor_rect.MaxY();
if (view_rect.MaxY() - anchor_rect.MaxY() < bubble_size_.Height()) {
bubble_y = anchor_rect.Y() - bubble_size_.Height();
show_bottom_arrow = true;
}
double bubble_x =
anchor_rect.X() + anchor_rect.Width() / 2 - bubble_size_.Width() / 2;
if (bubble_x < view_rect.X())
bubble_x = view_rect.X();
else if (bubble_x + bubble_size_.Width() > view_rect.MaxX())
bubble_x = view_rect.MaxX() - bubble_size_.Width();
Element& container = GetElementById("container");
container.SetInlineStyleProperty(CSSPropertyID::kLeft, bubble_x / zoom_factor,
CSSPrimitiveValue::UnitType::kPixels);
container.SetInlineStyleProperty(CSSPropertyID::kTop, bubble_y / zoom_factor,
CSSPrimitiveValue::UnitType::kPixels);
// Should match to --arrow-size in validation_bubble.css.
const int kArrowSize = 8;
const int kArrowMargin = 10;
const int kMinArrowAnchorX = kArrowSize + kArrowMargin;
double max_arrow_anchor_x =
bubble_size_.Width() - (kArrowSize + kArrowMargin) * zoom_factor;
double arrow_anchor_x;
const int kOffsetToAnchorRect = 8;
double anchor_rect_center = anchor_rect.X() + anchor_rect.Width() / 2;
if (!Locale::DefaultLocale().IsRTL()) {
double anchor_rect_left =
anchor_rect.X() + kOffsetToAnchorRect * zoom_factor;
if (anchor_rect_left > anchor_rect_center)
anchor_rect_left = anchor_rect_center;
arrow_anchor_x = kMinArrowAnchorX * zoom_factor;
if (bubble_x + arrow_anchor_x < anchor_rect_left) {
arrow_anchor_x = anchor_rect_left - bubble_x;
if (arrow_anchor_x > max_arrow_anchor_x)
arrow_anchor_x = max_arrow_anchor_x;
}
} else {
double anchor_rect_right =
anchor_rect.MaxX() - kOffsetToAnchorRect * zoom_factor;
if (anchor_rect_right < anchor_rect_center)
anchor_rect_right = anchor_rect_center;
arrow_anchor_x = max_arrow_anchor_x;
if (bubble_x + arrow_anchor_x > anchor_rect_right) {
arrow_anchor_x = anchor_rect_right - bubble_x;
if (arrow_anchor_x < kMinArrowAnchorX * zoom_factor)
arrow_anchor_x = kMinArrowAnchorX * zoom_factor;
}
}
double arrow_x = arrow_anchor_x / zoom_factor - kArrowSize;
double arrow_anchor_percent = arrow_anchor_x * 100 / bubble_size_.Width();
if (show_bottom_arrow) {
GetElementById("outer-arrow-bottom")
.SetInlineStyleProperty(CSSPropertyID::kLeft, arrow_x,
CSSPrimitiveValue::UnitType::kPixels);
GetElementById("inner-arrow-bottom")
.SetInlineStyleProperty(CSSPropertyID::kLeft, arrow_x,
CSSPrimitiveValue::UnitType::kPixels);
container.setAttribute(html_names::kClassAttr, "shown-fully bottom-arrow");
container.SetInlineStyleProperty(
CSSPropertyID::kTransformOrigin,
String::Format("%.2f%% bottom", arrow_anchor_percent));
} else {
GetElementById("outer-arrow-top")
.SetInlineStyleProperty(CSSPropertyID::kLeft, arrow_x,
CSSPrimitiveValue::UnitType::kPixels);
GetElementById("inner-arrow-top")
.SetInlineStyleProperty(CSSPropertyID::kLeft, arrow_x,
CSSPrimitiveValue::UnitType::kPixels);
container.setAttribute(html_names::kClassAttr, "shown-fully");
container.SetInlineStyleProperty(
CSSPropertyID::kTransformOrigin,
String::Format("%.2f%% top", arrow_anchor_percent));
}
}
void ValidationMessageOverlayDelegate::StartToHide() {
anchor_ = nullptr;
if (!page_)
return;
GetElementById("container")
.classList()
.replace("shown-fully", "hiding", ASSERT_NO_EXCEPTION);
}
bool ValidationMessageOverlayDelegate::IsHiding() const {
return !anchor_;
}
} // namespace blink