blob: 119d5fd1dc4642dbd452824d5fb0494ad6c4f318 [file] [log] [blame]
// Copyright 2018 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/omnibox/rounded_omnibox_results_frame.h"
#include <memory>
#include <utility>
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "build/build_config.h"
#include "chrome/browser/themes/theme_service.h"
#include "chrome/browser/ui/color/chrome_color_id.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/location_bar/location_bar_view.h"
#include "chrome/browser/ui/views/omnibox/omnibox_aim_popup_webui_content.h"
#include "components/omnibox/common/omnibox_features.h"
#include "ui/base/cursor/cursor.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/pointer/touch_ui_controller.h"
#include "ui/base/ui_base_features.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/geometry/rounded_corners_f.h"
#include "ui/views/bubble/bubble_border.h"
#include "ui/views/layout/layout_provider.h"
#include "ui/views/metadata/view_factory.h"
#if defined(USE_AURA)
#include "ui/aura/window.h"
#include "ui/aura/window_targeter.h"
#endif
#if BUILDFLAG(IS_WIN)
#include "ui/views/widget/native_widget_aura.h"
#endif
namespace {
// Value from the spec controlling appearance of the shadow.
constexpr int kElevation = 16;
#if !defined(USE_AURA)
struct WidgetEventPair {
raw_ptr<views::Widget> widget;
std::unique_ptr<ui::MouseEvent> event;
};
#if BUILDFLAG(IS_MAC)
views::Widget* GetImmersiveFullscreenWidgetForEvent(
views::View* this_view,
const ui::MouseEvent* this_event) {
const views::Widget* parent_widget = this_view->GetWidget()->parent();
BrowserView* browser_view = BrowserView::GetBrowserViewForNativeWindow(
parent_widget->GetNativeWindow());
// If the results window is not a child of the overlay widget we are not in
// immersive fullscreen.
if (browser_view->overlay_widget() != parent_widget) {
return nullptr;
}
{
// If the event is located in the location bar send the event to the overlay
// widget to handle text selection.
gfx::Point event_location = this_event->location();
views::View::ConvertPointToScreen(this_view, &event_location);
views::View::ConvertPointFromScreen(browser_view->GetLocationBarView(),
&event_location);
if (browser_view->GetLocationBarView()->HitTestPoint(event_location)) {
return browser_view->overlay_widget();
}
}
{
// If the event is located in the content view send the event to the browser
// widget.
gfx::Point event_location = this_event->location();
views::View::ConvertPointToScreen(this_view, &event_location);
views::View::ConvertPointFromScreen(browser_view->contents_container(),
&event_location);
if (browser_view->contents_container()->HitTestPoint(event_location)) {
return browser_view->GetWidget();
}
}
// In immersive fullscreen with tabs enabled the floating results shadow
// spreads into the tab strip area which is hosted in yet another separate
// widget, the tab widget. Send the rest of the events to the tab widget. This
// will allow for tab strip interaction in the area covered by the shadow and
// accurate tab hover card dismissal.
if (browser_view->tab_overlay_widget()) {
return browser_view->tab_overlay_widget();
}
// If immersive fullscreen with tabs is not enabled, send events to the
// overlay widget for tab strip interaction in the area covered by the shadow.
return browser_view->overlay_widget();
}
#endif
WidgetEventPair GetParentWidgetAndEvent(views::View* this_view,
const ui::MouseEvent* this_event) {
// Note that the floating results view is a top-level widget, so hop up a
// level before looking for the parent's top-level widget for event
// forwarding.
views::Widget* this_widget = this_view->GetWidget();
views::Widget* parent_widget = this_widget->parent();
std::unique_ptr<ui::MouseEvent> event(
static_cast<ui::MouseEvent*>(this_event->Clone().release()));
if (!parent_widget) {
return {nullptr, std::move(event)};
}
// On macOS if the parent widget is the overlay widget we are in immersive
// fullscreen. Don't walk any higher up the tree. The overlay or tab widget will
// handle the event.
// TODO(http://crbug.com/1462791): Remove custom event handling.
#if BUILDFLAG(IS_MAC)
views::Widget* top_level =
GetImmersiveFullscreenWidgetForEvent(this_view, this_event)
?: parent_widget->GetTopLevelWidgetForNativeView(
parent_widget->GetNativeView());
#else
views::Widget* top_level = parent_widget->GetTopLevelWidgetForNativeView(
parent_widget->GetNativeView());
#endif
DCHECK_NE(this_widget, top_level);
if (!top_level) {
return {nullptr, std::move(event)};
}
gfx::Point event_location = this_event->location();
views::View::ConvertPointToScreen(this_view, &event_location);
views::View::ConvertPointFromScreen(top_level->GetRootView(),
&event_location);
// Convert location to top level widget coordinate.
event->set_location(event_location);
return {top_level, std::move(event)};
}
#endif // !USE_AURA
// View at the top of the frame which paints transparent pixels to make a hole
// so that the location bar shows through.
class TopBackgroundView : public views::View {
METADATA_HEADER(TopBackgroundView, views::View)
public:
explicit TopBackgroundView(const LocationBarView* location_bar)
: location_bar_(location_bar) {}
void OnThemeChanged() override {
views::View::OnThemeChanged();
const SkColor background_color =
GetColorProvider()->GetColor(kColorOmniboxResultsBackground);
// Paint a stroke of the background color as a 1 px border to hide the
// underlying antialiased location bar/toolbar edge. The round rect here is
// not antialiased, since the goal is to completely cover the underlying
// pixels, and AA would let those on the edge partly bleed through.
SetBackground(location_bar_->CreateRoundRectBackground(
SK_ColorTRANSPARENT, background_color, SkBlendMode::kSrc, false));
}
#if !defined(USE_AURA)
// For non-Aura platforms, forward mouse events and cursor requests intended
// for the omnibox to the proper Widgets/Views. For Aura platforms, this is
// done with an event targeter set up in
// RoundedOmniboxResultsFrame::AddedToWidget(), below.
private:
// Note that mouse moved events can be dispatched through OnMouseEvent, but
// RootView directly calls OnMouseMoved as well, so override OnMouseMoved as
// well to catch 'em all.
void OnMouseMoved(const ui::MouseEvent& event) override {
auto pair = GetParentWidgetAndEvent(this, &event);
if (pair.widget) {
pair.widget->OnMouseEvent(pair.event.get());
}
}
void OnMouseEvent(ui::MouseEvent* event) override {
auto pair = GetParentWidgetAndEvent(this, event);
if (pair.widget) {
pair.widget->OnMouseEvent(pair.event.get());
}
// If the original event isn't marked as "handled" then it will propagate up
// the view hierarchy and might be double-handled. https://crbug.com/870341
event->SetHandled();
}
ui::Cursor GetCursor(const ui::MouseEvent& event) override {
const auto pair = GetParentWidgetAndEvent(this, &event);
if (pair.widget) {
views::View* omnibox_view =
pair.widget->GetRootView()->GetEventHandlerForPoint(
pair.event->location());
return omnibox_view->GetCursor(*pair.event);
}
return ui::Cursor();
}
#endif // !USE_AURA
private:
raw_ptr<const LocationBarView> location_bar_;
};
BEGIN_METADATA(TopBackgroundView)
END_METADATA
BEGIN_VIEW_BUILDER(/* no export*/, TopBackgroundView, views::View)
END_VIEW_BUILDER
} // namespace
DEFINE_VIEW_BUILDER(/* no export */, TopBackgroundView)
RoundedOmniboxResultsFrame::RoundedOmniboxResultsFrame(
views::View* contents,
LocationBarView* location_bar,
bool forward_mouse_events)
: contents_(contents), forward_mouse_events_(forward_mouse_events) {
const int corner_radius = views::LayoutProvider::Get()->GetCornerRadiusMetric(
views::ShapeContextTokens::kOmniboxExpandedRadius);
// Host the contents in its own View to simplify layout and customization.
auto contents_host_builder =
views::Builder<views::View>()
.CopyAddressTo(&contents_host_)
.SetBackground(
views::CreateSolidBackground(kColorOmniboxResultsBackground))
.SetPaintToLayer()
.CustomConfigure(base::BindOnce(
[](const int corner_radius, views::View* view) {
view->layer()->SetFillsBoundsOpaquely(false);
// Use rounded corners.
view->layer()->SetRoundedCornerRadius(
gfx::RoundedCornersF(corner_radius));
view->layer()->SetIsFastRoundedCorner(true);
},
corner_radius))
.AddChild(views::Builder<TopBackgroundView>(
std::make_unique<TopBackgroundView>(location_bar))
.CopyAddressTo(&top_background_));
auto contents_host = std::move(contents_host_builder).Build();
contents_host->AddChildViewRaw(contents_.get());
// Initialize the shadow.
auto border = std::make_unique<views::BubbleBorder>(
views::BubbleBorder::Arrow::NONE,
views::BubbleBorder::Shadow::STANDARD_SHADOW);
border->set_rounded_corners(gfx::RoundedCornersF(corner_radius));
border->set_md_shadow_elevation(kElevation);
SetBorder(std::move(border));
AddChildView(std::move(contents_host));
}
RoundedOmniboxResultsFrame::~RoundedOmniboxResultsFrame() = default;
// static
void RoundedOmniboxResultsFrame::OnBeforeWidgetInit(
views::Widget::InitParams* params,
views::Widget* widget) {
#if BUILDFLAG(IS_WIN)
// On Windows, use an Aura window instead of a native window, because the
// native window does not support clicking through translucent shadows to the
// underyling content. Linux and ChromeOS do not need this because they
// already use Aura for the suggestions dropdown.
//
// TODO(sdy): Mac does not support Aura at the moment, and needs a different
// platform-specific solution.
params->native_widget = new views::NativeWidgetAura(widget);
#endif
params->name = "RoundedOmniboxResultsFrameWindow";
// Since we are drawing the shadow in Views via the BubbleBorder, we never
// want our widget to have its own window-manager drawn shadow.
params->shadow_type = views::Widget::InitParams::ShadowType::kNone;
}
// static
int RoundedOmniboxResultsFrame::GetNonResultSectionHeight(bool include_cutout) {
return include_cutout ? GetLayoutConstant(LOCATION_BAR_HEIGHT) +
GetLocationBarAlignmentInsets().height()
: 0;
}
// static
gfx::Insets RoundedOmniboxResultsFrame::GetLocationBarAlignmentInsets() {
if (ui::TouchUiController::Get()->touch_ui()) {
return gfx::Insets::TLBR(6, 1, 5, 1);
}
return gfx::Insets::VH(5, 6);
}
// static
gfx::Insets RoundedOmniboxResultsFrame::GetShadowInsets() {
return views::BubbleBorder::GetBorderAndShadowInsets(kElevation);
}
std::unique_ptr<views::View> RoundedOmniboxResultsFrame::ExtractContents() {
auto contents = std::exchange(contents_, nullptr);
return contents_host_->RemoveChildViewT<views::View>(contents);
}
views::View* RoundedOmniboxResultsFrame::GetContents() {
return contents_;
}
void RoundedOmniboxResultsFrame::SetCutoutVisibility(bool visible) {
if (visible == top_background_->GetVisible()) {
return;
}
top_background_->SetVisible(visible);
}
void RoundedOmniboxResultsFrame::Layout(PassKey) {
// This is called when the Widget resizes due to results changing. Resizing
// the Widget is fast on ChromeOS, but slow on other platforms, and can't be
// animated smoothly.
// TODO(tapted): Investigate using a static Widget size.
gfx::Rect bounds = GetContentsBounds();
contents_host_->SetBoundsRect(bounds);
if (top_background_->GetVisible()) {
gfx::Rect top_bounds(contents_host_->GetContentsBounds());
top_bounds.set_height(GetNonResultSectionHeight(true));
top_bounds.Inset(GetLocationBarAlignmentInsets());
top_background_->SetBoundsRect(top_bounds);
}
gfx::Rect results_bounds(contents_host_->GetContentsBounds());
results_bounds.Inset(GetContentInsets());
// Workaround for 1px visual artifact. The WebUI requests a 1px minimum height
// when empty, creating a visual artifact. Clamping to 0 hides the widget and
// breaks future resize events. Instead, clamp to a 1x1 centered rect to keep
// the widget active while making the artifact unnoticeable.
// TODO(crbug.com/460908495) WebUI should not be sending min height resize
// requests.
if (results_bounds.height() <= 1) {
results_bounds.ClampToCenteredSize(gfx::Size(1, 1));
}
contents_->SetBoundsRect(results_bounds);
}
void RoundedOmniboxResultsFrame::VisibilityChanged(View* starting_from,
bool is_visible) {
views::View::VisibilityChanged(starting_from, is_visible);
#if defined(USE_AURA)
if (!forward_mouse_events_) {
return;
}
if (is_visible) {
// Use a ui::EventTargeter that allows mouse and touch events in the top
// portion of the Widget to pass through to the omnibox beneath it.
auto results_targeter = std::make_unique<aura::WindowTargeter>();
results_targeter->SetInsets(GetContentInsets());
GetWidget()->GetNativeWindow()->SetEventTargeter(
std::move(results_targeter));
} else {
GetWidget()->GetNativeWindow()->SetEventTargeter(nullptr);
}
#endif // USE_AURA
}
// Note: The OnMouseMoved function is only called for the shadow area, as mouse-
// moved events are not dispatched through the view hierarchy but are direct-
// dispatched by RootView. This OnMouseEvent function is on the dispatch path
// for all mouse events of the window, so be careful to correctly mark events as
// "handled" above in subviews.
#if !defined(USE_AURA)
// Note that mouse moved events can be dispatched through OnMouseEvent, but
// RootView directly calls OnMouseMoved as well, so override OnMouseMoved as
// well to catch 'em all.
void RoundedOmniboxResultsFrame::OnMouseMoved(const ui::MouseEvent& event) {
auto pair = GetParentWidgetAndEvent(this, &event);
if (pair.widget) {
pair.widget->OnMouseEvent(pair.event.get());
}
}
void RoundedOmniboxResultsFrame::OnMouseEvent(ui::MouseEvent* event) {
auto pair = GetParentWidgetAndEvent(this, event);
if (pair.widget) {
pair.widget->OnMouseEvent(pair.event.get());
}
}
#endif // !USE_AURA
// Insets used to position |contents_| within |contents_host_|.
gfx::Insets RoundedOmniboxResultsFrame::GetContentInsets() {
return gfx::Insets::TLBR(
GetNonResultSectionHeight(top_background_->GetVisible()), 0, 0, 0);
}
BEGIN_METADATA(RoundedOmniboxResultsFrame)
END_METADATA