blob: 9ab5d76c2bb1627bd0d715ce4444c54bc0615876 [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 "chrome/browser/ui/views/autofill/popup/popup_view_utils.h"
#include <algorithm>
#include "chrome/browser/picture_in_picture/picture_in_picture_window_manager.h"
#include "chrome/browser/platform_util.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/views/extensions/extension_popup.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/permissions/permission_prompt_bubble_base_view.h"
#include "components/autofill/core/browser/ui/suggestion_type.h"
#include "components/autofill/core/common/autofill_features.h"
#include "content/public/browser/web_contents.h"
#include "extensions/common/constants.h"
#include "ui/base/interaction/element_tracker.h"
#include "ui/views/interaction/element_tracker_views.h"
#include "ui/views/widget/widget.h"
using views::BubbleBorder;
namespace autofill {
namespace {
// The minimum number of pixels the popup should be distanced from the edge of
// the content area.
constexpr int kMinimalPopupDistanceToContentAreaEdge = 8;
// Returns true if the arrow is either located on top or on the bottom of the
// popup.
bool IsVerticalArrowSide(views::BubbleArrowSide side) {
return side == views::BubbleArrowSide::kTop ||
side == views::BubbleArrowSide::kBottom;
}
// Returns false if the element is not sufficiently visible to place an arrow.
bool IsElementSufficientlyVisibleForAVerticalArrow(
const gfx::Rect& content_area_bounds,
const gfx::Rect& element_bounds,
views::BubbleArrowSide side) {
// Only consider the visible size of the element for vertical arrows.
if (!IsVerticalArrowSide(side)) {
return true;
}
int visible_width =
std::clamp(element_bounds.right(), content_area_bounds.x(),
content_area_bounds.right()) -
std::clamp(element_bounds.x(), content_area_bounds.x(),
content_area_bounds.right());
return visible_width > 3 * BubbleBorder::kVisibleArrowRadius;
}
// Returns a BubbleBorder::Arrow that is suitable for the supplied |side| and
// text direction.
BubbleBorder::Arrow GetBubbleArrowForBubbleArrowSide(
views::BubbleArrowSide side,
bool right_to_left) {
switch (side) {
case views::BubbleArrowSide::kTop:
return right_to_left ? BubbleBorder::Arrow::TOP_RIGHT
: BubbleBorder::Arrow::TOP_LEFT;
case views::BubbleArrowSide::kBottom:
return right_to_left ? BubbleBorder::Arrow::BOTTOM_RIGHT
: BubbleBorder::Arrow::BOTTOM_LEFT;
case views::BubbleArrowSide::kLeft:
return BubbleBorder::Arrow::LEFT_TOP;
case views::BubbleArrowSide::kRight:
return BubbleBorder::Arrow::RIGHT_TOP;
}
}
// Returns the size of popup placed on the |side| of the |element_bounds| once
// the popup is expanded to its |popup_preferred_size| or the maximum size
// available on the |content_area_bounds|.
gfx::Size GetExpandedPopupSize(const gfx::Rect& content_area_bounds,
const gfx::Rect& element_bounds,
const gfx::Size& popup_preferred_size,
int scrollbar_width,
views::BubbleArrowSide side) {
// Get the maximum available space for the popup
int available_height = GetAvailableVerticalSpaceOnSideOfElement(
content_area_bounds, element_bounds, side);
int available_width = GetAvailableHorizontalSpaceOnSideOfElement(
content_area_bounds, element_bounds, side);
int height = std::min(available_height, popup_preferred_size.height());
int width = std::min(
available_width,
popup_preferred_size.width() +
(height < popup_preferred_size.height() ? scrollbar_width : 0));
return {width, height};
}
} // namespace
void CalculatePopupYAndHeight(int popup_preferred_height,
const gfx::Rect& content_area_bounds,
const gfx::Rect& element_bounds,
gfx::Rect* popup_bounds) {
int top_growth_end = std::clamp(element_bounds.y(), content_area_bounds.y(),
content_area_bounds.bottom());
int bottom_growth_start =
std::clamp(element_bounds.bottom(), content_area_bounds.y(),
content_area_bounds.bottom());
int top_available = top_growth_end - content_area_bounds.y();
int bottom_available = content_area_bounds.bottom() - bottom_growth_start;
popup_bounds->set_height(popup_preferred_height);
popup_bounds->set_y(top_growth_end);
int y_adjustment = (bottom_available >= popup_preferred_height ||
bottom_available >= top_available)
? element_bounds.bottom()
: content_area_bounds.y();
popup_bounds->AdjustToFit(gfx::Rect(popup_bounds->x(), y_adjustment,
popup_bounds->width(),
content_area_bounds.height()));
}
bool CanShowDropdownHere(int item_height,
const gfx::Rect& content_area_bounds,
const gfx::Rect& element_bounds) {
// Ensure that at least one row of the popup will be displayed within the
// bounds of the content area so that the user notices the presence of the
// popup.
bool enough_space_for_one_item_in_content_area_above_element =
element_bounds.y() - content_area_bounds.y() >= item_height;
bool element_top_is_within_content_area_bounds =
element_bounds.y() >= content_area_bounds.y() &&
element_bounds.y() < content_area_bounds.bottom();
bool enough_space_for_one_item_in_content_area_below_element =
content_area_bounds.bottom() - element_bounds.bottom() >= item_height;
bool element_bottom_is_within_content_area_bounds =
element_bounds.bottom() > content_area_bounds.y() &&
element_bounds.bottom() <= content_area_bounds.bottom();
// TODO(crbug.com/40272733): Test the space on the left/right or forbid it
// explicitly.
return (enough_space_for_one_item_in_content_area_above_element &&
element_top_is_within_content_area_bounds) ||
(enough_space_for_one_item_in_content_area_below_element &&
element_bottom_is_within_content_area_bounds);
}
// Keep in sync with TryToCloseAllPrompts() from autofill_uitest.cc.
bool BoundsOverlapWithAnyOpenPrompt(const gfx::Rect& screen_bounds,
content::WebContents* web_contents) {
gfx::NativeWindow top_level_window = web_contents->GetTopLevelNativeWindow();
// `top_level_window` can be `nullptr` if `web_contents` is not attached to
// a window, e.g. in unit test runs.
if (!top_level_window) {
return false;
}
gfx::NativeView top_level_view =
platform_util::GetViewForWindow(top_level_window);
// We generally want to ensure that no prompt overlaps with |screen_bounds|.
// It is possible, however, that a <datalist> is part of a prompt (e.g. an
// extension popup can render a <datalist>). Therefore, we exclude the widget
// that hosts the |web_contents| from the prompts that are considered for
// overlaps.
views::Widget* web_contents_widget =
views::Widget::GetTopLevelWidgetForNativeView(
web_contents->GetContentNativeView());
// On Aura-based systems, prompts are siblings to the top level native window,
// and hence we need to go one level up to start searching from the root
// window.
top_level_view = platform_util::GetParent(top_level_view)
? platform_util::GetParent(top_level_view)
: top_level_view;
views::Widget::Widgets all_widgets;
views::Widget::GetAllChildWidgets(top_level_view, &all_widgets);
return base::ranges::any_of(
all_widgets, [&screen_bounds, web_contents_widget](views::Widget* w) {
return w->IsDialogBox() &&
w->GetWindowBoundsInScreen().Intersects(screen_bounds) &&
w != web_contents_widget;
});
}
bool BoundsOverlapWithOpenPermissionsPrompt(
const gfx::Rect& screen_bounds,
content::WebContents* web_contents) {
Browser* browser = chrome::FindBrowserWithTab(web_contents);
if (!browser) {
return false;
}
BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser);
if (!browser_view) {
return false;
}
views::View* const permission_bubble_view =
views::ElementTrackerViews::GetInstance()->GetFirstMatchingView(
PermissionPromptBubbleBaseView::kMainViewId,
views::ElementTrackerViews::GetInstance()->GetContextForView(
browser_view));
if (!permission_bubble_view) {
return false;
}
return permission_bubble_view->GetWidget()
->GetWindowBoundsInScreen()
.Intersects(screen_bounds);
}
bool BoundsOverlapWithPictureInPictureWindow(const gfx::Rect& screen_bounds) {
std::optional<gfx::Rect> pip_window_bounds =
PictureInPictureWindowManager::GetInstance()
->GetPictureInPictureWindowBounds();
return pip_window_bounds && pip_window_bounds->Intersects(screen_bounds);
}
bool PopupMayExceedContentAreaBounds(content::WebContents* web_contents) {
if (!web_contents) { // May be null for tests.
return false;
}
const GURL& url = web_contents->GetLastCommittedURL();
// Extensions may want to show <datalist> form controls whose popups cannot be
// rendered within the bounds of an extension popup. For that reason they are
// allow-listed to draw popups outside the boundary of the extension popup.
if (url.SchemeIs(extensions::kExtensionScheme)) {
views::Widget* widget = views::Widget::GetTopLevelWidgetForNativeView(
web_contents->GetContentNativeView());
return widget && widget->GetName() == ExtensionPopup::kViewClassName;
}
return false;
}
int GetAvailableVerticalSpaceOnSideOfElement(
const gfx::Rect& content_area_bounds,
const gfx::Rect& element_bounds,
views::BubbleArrowSide side) {
// Note that the side of the arrow is opposite to the side of the element the
// bubble is located on.
switch (side) {
case views::BubbleArrowSide::kLeft:
case views::BubbleArrowSide::kRight:
// For a bubble that is either on the left of the right side of the
// element, the height of the content area is the total available space.
return content_area_bounds.height();
case views::BubbleArrowSide::kBottom:
// If the bubble sits above the element, return the space between the
// upper edge of the element and the content area.
return element_bounds.y() - content_area_bounds.y();
case views::BubbleArrowSide::kTop:
// If the bubble sits below the element, return the space between the
// lower edge of the element and the content area.
return content_area_bounds.bottom() - element_bounds.bottom();
}
}
int GetAvailableHorizontalSpaceOnSideOfElement(
const gfx::Rect& content_area_bounds,
const gfx::Rect& element_bounds,
views::BubbleArrowSide side) {
// Note that the side of the arrow is opposite to the side of the element the
// popup is located on.
switch (side) {
case views::BubbleArrowSide::kRight:
return element_bounds.x() - content_area_bounds.x() -
kMinimalPopupDistanceToContentAreaEdge;
case views::BubbleArrowSide::kLeft:
return content_area_bounds.right() - element_bounds.right() -
kMinimalPopupDistanceToContentAreaEdge;
case views::BubbleArrowSide::kTop:
case views::BubbleArrowSide::kBottom:
return content_area_bounds.width() -
2 * kMinimalPopupDistanceToContentAreaEdge;
}
}
bool IsPopupPlaceableOnSideOfElement(const gfx::Rect& content_area_bounds,
const gfx::Rect& element_bounds,
const gfx::Size& popup_preferred_size,
int additional_spacing,
views::BubbleArrowSide side) {
switch (side) {
case views::BubbleArrowSide::kLeft:
case views::BubbleArrowSide::kRight:
return popup_preferred_size.width() + additional_spacing <=
GetAvailableHorizontalSpaceOnSideOfElement(content_area_bounds,
element_bounds, side);
case views::BubbleArrowSide::kTop:
case views::BubbleArrowSide::kBottom:
return popup_preferred_size.height() + additional_spacing <=
GetAvailableVerticalSpaceOnSideOfElement(content_area_bounds,
element_bounds, side);
}
}
views::BubbleArrowSide GetOptimalArrowSide(
const gfx::Rect& content_area_bounds,
const gfx::Rect& element_bounds,
const gfx::Size& popup_preferred_size,
base::span<const views::BubbleArrowSide> popup_preferred_sides) {
// Probe for a side of the element on which the popup can be shown entirely.
for (views::BubbleArrowSide possible_side : popup_preferred_sides) {
if (IsPopupPlaceableOnSideOfElement(
content_area_bounds, element_bounds, popup_preferred_size,
BubbleBorder::kVisibleArrowLength, possible_side) &&
IsElementSufficientlyVisibleForAVerticalArrow(
content_area_bounds, element_bounds, possible_side)) {
return possible_side;
}
}
// As a fallback, render the popup on top of the element if there is more
// space than below the element.
if (element_bounds.y() - content_area_bounds.y() >
content_area_bounds.bottom() - element_bounds.bottom()) {
return views::BubbleArrowSide::kBottom;
}
return views::BubbleArrowSide::kTop;
}
BubbleBorder::Arrow GetOptimalPopupPlacement(
const gfx::Rect& content_area_bounds,
const gfx::Rect& element_bounds,
const gfx::Size& popup_preferred_size,
bool right_to_left,
int scrollbar_width,
int maximum_pixel_offset_to_center,
int maximum_width_percentage_to_center,
gfx::Rect& popup_bounds,
base::span<const views::BubbleArrowSide> popup_preferred_sides) {
// Determine the best side of the element to put the popup and get a
// corresponding arrow.
views::BubbleArrowSide side =
GetOptimalArrowSide(content_area_bounds, element_bounds,
popup_preferred_size, popup_preferred_sides);
BubbleBorder::Arrow arrow =
GetBubbleArrowForBubbleArrowSide(side, right_to_left);
// Set the actual size of the popup.
popup_bounds.set_size(
GetExpandedPopupSize(content_area_bounds, element_bounds,
popup_preferred_size, scrollbar_width, side));
// Move the origin of the popup to the anchor position on the element
// corresponding to |arrow|.
// ------------------
// For TOP_LEFT | element |
// anchor_point -> ==============----
// | |
// | popup |
// | |
// | |
// --------------
popup_bounds += views::GetContentBoundsOffsetToArrowAnchorPoint(
popup_bounds, arrow,
views::GetArrowAnchorPointFromAnchorRect(arrow, element_bounds));
if (!IsVerticalArrowSide(side)) {
// For a horizontal arrow, move the popup to the top if it leaves the
// lower part of the screen. Note, that by default, the popup's top is
// aligned with the field.
// The popup top can never go above the content area since the popup size
// computed to fit in the screen by GetExpandedPopupSize.
popup_bounds.Offset(0, -1 * std::max(0, popup_bounds.bottom() -
content_area_bounds.bottom()));
return arrow;
}
// The horizontal offset is the minimum of a fixed number of pixels
// |maximum_pixel_offset_to_center| and a percentage of the element width.
int horizontal_offset_pixels = std::min(
maximum_pixel_offset_to_center,
maximum_width_percentage_to_center * element_bounds.width() / 100);
// In addition, the offset is shifted by the distance of the popup's arrow to
// the popup's edge. By this, the arrow of the popup is aligned with the
// targeted pixel and not the edge of the popup.
horizontal_offset_pixels -=
(BubbleBorder::kVisibleArrowBuffer + BubbleBorder::kVisibleArrowRadius);
// Give the offset a direction.
int horizontal_offset = horizontal_offset_pixels * (right_to_left ? -1 : 1);
// Move the popup bounds towards to center of the field.
// Note that for |right_to_left|, this will be a negative value.
// ------------------
// | element |
// ----------========-------
// | |
// |---------| popup |
// horizontal offset | |
// | |
// ---------------
popup_bounds.Offset(horizontal_offset, 0);
// In case the popup the exceeds the right edge of the view port, move it
// back until it completely fits.
// ------------------ |---| shift back
// | element | |
// ----------========---+---
// | | |
// | popup | |
// | | |
// | | |
// -----------+---
// |
// content_area.right()
popup_bounds.Offset(
std::min(0, content_area_bounds.right() - popup_bounds.right() -
kMinimalPopupDistanceToContentAreaEdge),
0);
// Analogously, make move the popup to the right if it exceeds the left edge
// of the content area.
popup_bounds.Offset(std::max(0, content_area_bounds.x() - popup_bounds.x() +
kMinimalPopupDistanceToContentAreaEdge),
0);
return arrow;
}
bool IsExpandableSuggestionType(SuggestionType type) {
switch (type) {
case SuggestionType::kAddressEntry:
case SuggestionType::kAddressFieldByFieldFilling:
case SuggestionType::kCreditCardEntry:
case SuggestionType::kCreditCardFieldByFieldFilling:
case SuggestionType::kCompose:
case SuggestionType::kComposeSavedStateNotification:
case SuggestionType::kDevtoolsTestAddresses:
case SuggestionType::kFillFullAddress:
case SuggestionType::kFillFullEmail:
case SuggestionType::kFillFullName:
case SuggestionType::kFillFullPhoneNumber:
case SuggestionType::kPasswordEntry:
return true;
case SuggestionType::kAccountStoragePasswordEntry:
case SuggestionType::kAllSavedPasswordsEntry:
case SuggestionType::kAutocompleteEntry:
case SuggestionType::kAutofillOptions:
case SuggestionType::kClearForm:
case SuggestionType::kComposeDisable:
case SuggestionType::kComposeGoToSettings:
case SuggestionType::kComposeNeverShowOnThisSiteAgain:
case SuggestionType::kCreateNewPlusAddress:
case SuggestionType::kDatalistEntry:
case SuggestionType::kDeleteAddressProfile:
case SuggestionType::kDevtoolsTestAddressEntry:
case SuggestionType::kEditAddressProfile:
case SuggestionType::kFillEverythingFromAddressProfile:
case SuggestionType::kFillExistingPlusAddress:
case SuggestionType::kFillPassword:
case SuggestionType::kGeneratePasswordEntry:
case SuggestionType::kIbanEntry:
case SuggestionType::kInsecureContextPaymentDisabledMessage:
case SuggestionType::kMerchantPromoCodeEntry:
case SuggestionType::kMixedFormMessage:
case SuggestionType::kPasswordAccountStorageEmpty:
case SuggestionType::kPasswordAccountStorageOptIn:
case SuggestionType::kPasswordAccountStorageOptInAndGenerate:
case SuggestionType::kPasswordAccountStorageReSignin:
case SuggestionType::kPasswordFieldByFieldFilling:
case SuggestionType::kScanCreditCard:
case SuggestionType::kSeePromoCodeDetails:
case SuggestionType::kSeparator:
case SuggestionType::kShowAccountCards:
case SuggestionType::kTitle:
case SuggestionType::kViewPasswordDetails:
case SuggestionType::kVirtualCreditCardEntry:
case SuggestionType::kWebauthnCredential:
case SuggestionType::kWebauthnSignInWithAnotherDevice:
return false;
}
}
views::style::TextStyle GetPrimaryTextStyle() {
return views::style::TextStyle::STYLE_BODY_3_MEDIUM;
}
views::style::TextStyle GetSecondaryTextStyle() {
return views::style::TextStyle::STYLE_BODY_4;
}
} // namespace autofill