blob: c620f75a5130ad83191156746d4e14f0433ec436 [file]
// Copyright 2022 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/mobile_metrics/tap_friendliness_checker.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/local_frame_client.h"
#include "third_party/blink/renderer/core/frame/page_scale_constraints_set.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/html/forms/html_form_control_element.h"
#include "third_party/blink/renderer/core/html/html_anchor_element.h"
#include "third_party/blink/renderer/core/page/chrome_client.h"
#include "third_party/blink/renderer/core/page/page.h"
#include "ui/gfx/geometry/rect_conversions.h"
namespace blink {
namespace {
// Considers the |target| is a tap-able element which TapFriendlinessChecker
// focus.
bool ShouldRegister(Element* target) {
// TODO(crbug.com/369219144): Should this be DynamicTo<HTMLAnchorElementBase>?
if (const auto* anchor = DynamicTo<HTMLAnchorElement>(target)) {
return !anchor->Href().IsEmpty();
} else if (auto* element = DynamicTo<HTMLElement>(target);
element && element->WillRespondToMouseClickEvents()) {
return true;
} else {
return IsA<HTMLFormControlElement>(target);
}
}
int ViewportWidthInDIPS(LocalFrameView& view) {
int width = view.ViewportWidth();
return view.FrameToScreen(gfx::Rect(0, 0, width, 0)).width();
}
} // namespace
TapFriendlinessChecker* TapFriendlinessChecker::CreateIfMobile(
LocalFrameView& view) {
if (!view.GetPage()->GetSettings().GetViewportEnabled() ||
!view.GetPage()->GetSettings().GetViewportMetaEnabled()) {
return nullptr;
}
return MakeGarbageCollected<TapFriendlinessChecker>(view, PassKey());
}
void TapFriendlinessChecker::RegisterTapEvent(Element* target) {
if (!ShouldRegister(target))
return;
auto* node = DynamicTo<HTMLElement>(target);
if (!node)
return;
// Here we use definition of Android for DIPS.
// See: https://en.wikipedia.org/wiki/Device-independent_pixel
constexpr float kOneDipInMm = 0.15875;
constexpr float kTooSmallThresholdInMm = 7.0;
constexpr float kTooCloseDisplayEdgeThresholdInMm = 5.0;
constexpr float kZoomThreshold = 1.2;
constexpr float kHighlyZoomThreshold = 2.0;
const float zoom_factor = view_->GetPage()->GetVisualViewport().Scale() /
view_->GetPage()
->GetPageScaleConstraintsSet()
.FinalConstraints()
.initial_scale;
const gfx::Rect dip_rect = view_->FrameToScreen(
gfx::ToEnclosingRect(node->GetBoundingClientRectNoLifecycleUpdate()));
ukm::builders::MobileFriendliness_TappedBadTargets builder(
view_->GetFrame().GetDocument()->UkmSourceID());
if (dip_rect.width() * kOneDipInMm / zoom_factor <= kTooSmallThresholdInMm &&
dip_rect.height() * kOneDipInMm / zoom_factor <= kTooSmallThresholdInMm) {
builder.SetTooSmall(true);
}
const float display_width =
ViewportWidthInDIPS(*view_) * kOneDipInMm / zoom_factor;
const double center_x =
(dip_rect.x() + dip_rect.width()) * kOneDipInMm / zoom_factor / 2.0;
const double center_y =
(dip_rect.y() + dip_rect.height()) * kOneDipInMm / zoom_factor / 2.0;
if (center_x <= kTooCloseDisplayEdgeThresholdInMm ||
display_width - kTooCloseDisplayEdgeThresholdInMm <= center_x ||
center_y <= kTooCloseDisplayEdgeThresholdInMm) {
builder.SetCloseDisplayEdge(true);
}
if (kZoomThreshold <= zoom_factor) {
builder.SetZoomed(true);
if (kHighlyZoomThreshold <= zoom_factor)
builder.SetHighlyZoomed(true);
}
builder.Record(view_->GetFrame().GetDocument()->UkmRecorder());
}
void TapFriendlinessChecker::Trace(Visitor* visitor) const {
visitor->Trace(view_);
}
} // namespace blink