blob: 9aed59617b3f8a235c08ff8e43c52f66acfb17b4 [file] [log] [blame]
// Copyright 2024 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/glic/glic_window_controller.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/frame/tab_strip_region_view.h"
#include "chrome/browser/ui/views/glic/glic_view.h"
#include "chrome/browser/ui/views/tabs/glic_button.h"
#include "ui/display/screen.h"
#include "ui/events/event_observer.h"
#include "ui/views/controls/webview/webview.h"
#include "ui/views/event_monitor.h"
namespace {
// Default value for how close the corner of glic has to be from a browser's
// glic button to snap.
constexpr static int kSnapDistanceThreshold = 50;
constexpr static int kWidgetWidth = 400;
constexpr static int kWidgetHeight = 800;
constexpr static int kWidgetTopBarHeight = 80;
// Helper class for observing mouse and key events from native window.
class WindowEventObserver : public ui::EventObserver {
public:
explicit WindowEventObserver(
glic::GlicWindowController* glic_window_controller,
glic::GlicView* glic_view)
: glic_window_controller_(glic_window_controller), glic_view_(glic_view) {
event_monitor_ = views::EventMonitor::CreateWindowMonitor(
this, glic_view->GetWidget()->GetNativeWindow(),
{ui::EventType::kMousePressed, ui::EventType::kMouseReleased,
ui::EventType::kMouseDragged});
}
WindowEventObserver(const WindowEventObserver&) = delete;
WindowEventObserver& operator=(const WindowEventObserver&) = delete;
~WindowEventObserver() override = default;
void OnEvent(const ui::Event& event) override {
if (!event.IsMouseEvent()) {
return;
}
gfx::Point mouse_location = event_monitor_->GetLastMouseLocation();
views::View::ConvertPointFromScreen(glic_view_, &mouse_location);
if (event.type() == ui::EventType::kMousePressed &&
glic_view_->IsPointWithinDraggableArea(mouse_location)) {
mouse_down_in_draggable_area_ = true;
}
if (event.type() == ui::EventType::kMouseReleased ||
event.type() == ui::EventType::kMouseExited) {
mouse_down_in_draggable_area_ = false;
}
// Window should only be dragged if a corresponding mouse drag event was
// initiated in the draggable area.
if (mouse_down_in_draggable_area_ &&
event.type() == ui::EventType::kMouseDragged) {
glic_window_controller_->DragFromPoint(mouse_location.OffsetFromOrigin());
}
}
raw_ptr<glic::GlicWindowController> glic_window_controller_;
raw_ptr<glic::GlicView> glic_view_;
std::unique_ptr<views::EventMonitor> event_monitor_;
// Tracks whether the mouse is pressed and was initially within a draggable
// area of the window.
bool mouse_down_in_draggable_area_;
};
} // namespace
namespace glic {
GlicWindowController::GlicWindowController(Profile* profile)
: profile_(profile) {}
void GlicWindowController::Show(views::View* glic_button_view) {
// TODO(crbug.com/379943498): If a glic window already exists, handle showing
// by bringing to front or activating.
if (widget_) {
return;
}
int padding;
gfx::Point top_right_point;
if (!glic_button_view) {
// Right now this only detects whether the glic widget is summoned from the
// OS entrypoint and positions itself detached from the browser.
// TODO(crbug.com/384061064): Add more logic for when the glic window should
// show up in a detached state.
top_right_point = GetTopRightPositionForDetachedWindow();
padding = 50;
} else {
// If summoned from the tab strip button. This will always show up attached
// because it is tied to a views::View object within the current browser
// window.
top_right_point = GetTopRightPositionForAttachedWindow(glic_button_view);
padding = GetLayoutConstant(TAB_STRIP_PADDING);
}
std::tie(widget_, glic_view_) = glic::GlicView::CreateWidget(
profile_, {top_right_point.x() - kWidgetWidth - padding,
top_right_point.y() + padding, kWidgetWidth, kWidgetHeight});
widget_->Show();
window_event_observer_ =
std::make_unique<WindowEventObserver>(this, glic_view_);
// Set the draggable area to the top bar of the window, by default.
glic_view_->SetDraggableAreas({{0, 0, kWidgetWidth, kWidgetTopBarHeight}});
}
gfx::Point GlicWindowController::GetTopRightPositionForAttachedWindow(
views::View* glic_button_view) {
// Initial position determined by glic button bounds. Returns the top right
// point of the button.
gfx::Point top_right_bounds =
glic_button_view->GetBoundsInScreen().top_right();
views::Widget* glic_button_widget = glic_button_view->GetWidget();
AttachToBrowser(glic_button_widget);
return top_right_bounds;
}
gfx::Point GlicWindowController::GetTopRightPositionForDetachedWindow() {
// Position determined by screen bounds. Returns the top right point of the
// screen.
display::Screen* screen = display::Screen::GetScreen();
gfx::Point top_right_bounds =
screen->GetPrimaryDisplay().work_area().top_right();
return top_right_bounds;
}
void GlicWindowController::AttachToBrowser(views::Widget* widget) {
// Makes the glic widget a child view of the given widget's browser.
if (widget && widget_) {
// Add observer to new parent.
pinned_target_widget_observer_.SetPinnedTargetWidget(widget);
views::Widget::ReparentNativeView(widget_->GetNativeView(),
widget->GetNativeView());
}
}
bool GlicWindowController::Resize(const gfx::Size& size) {
if (!widget_) {
return false;
}
widget_->SetSize(size);
glic_view_->web_view()->SetSize(size);
return true;
}
gfx::Size GlicWindowController::GetSize() {
if (!widget_) {
return gfx::Size();
}
return widget_->GetSize();
}
void GlicWindowController::SetDraggableAreas(
const std::vector<gfx::Rect>& draggable_areas) {
if (!glic_view_) {
return;
}
glic_view_->SetDraggableAreas(draggable_areas);
}
void GlicWindowController::Close() {
if (!widget_) {
return;
}
widget_->CloseWithReason(views::Widget::ClosedReason::kCloseButtonClicked);
widget_.reset();
glic_view_ = nullptr;
}
void GlicWindowController::DragFromPoint(gfx::Vector2d mouse_location) {
// This code isn't set up to handle nested run loops. Nested run loops will
// lead to crashes.
if (!in_move_loop_) {
in_move_loop_ = true;
gfx::Vector2d drag_offset = mouse_location;
const views::Widget::MoveLoopSource move_loop_source =
views::Widget::MoveLoopSource::kMouse;
widget_->RunMoveLoop(drag_offset, move_loop_source,
views::Widget::MoveLoopEscapeBehavior::kDontHide);
HandleBrowserPinning(widget_->GetWindowBoundsInScreen().OffsetFromOrigin() +
mouse_location);
in_move_loop_ = false;
}
}
void GlicWindowController::HandleBrowserPinning(gfx::Vector2d mouse_location) {
// Loops through all browsers in activation order with the latest accessed
// browser first.
for (Browser* browser : BrowserList::GetInstance()->OrderedByActivation()) {
views::Widget* window_widget =
browser->window()->AsBrowserView()->GetWidget();
// Skips if the browser:
// - is incognito
// - is not visible
// - uses the same widget as glic
// - uses a different profile from glic
if (browser->profile()->IsOffTheRecord() ||
!browser->window()->IsVisible() || window_widget == widget_.get() ||
browser->GetWebView()->GetBrowserContext() !=
glic_view_->web_view()->GetBrowserContext()) {
continue;
}
auto* tab_strip_region_view =
browser->window()->AsBrowserView()->tab_strip_region_view();
if (!tab_strip_region_view || !tab_strip_region_view->glic_button()) {
continue;
}
gfx::Rect glic_button_rect =
tab_strip_region_view->glic_button()->GetBoundsInScreen();
float glic_button_mouse_distance =
(glic_button_rect.CenterPoint() -
gfx::PointAtOffsetFromOrigin(mouse_location))
.Length();
if (glic_button_mouse_distance < kSnapDistanceThreshold) {
MoveToBrowserPinTarget(browser);
// Close holder window if existing
if (holder_widget_) {
holder_widget_->CloseWithReason(
views::Widget::ClosedReason::kLostFocus);
holder_widget_.reset();
}
AttachToBrowser(window_widget);
} else if (widget_->parent() == window_widget) {
// If farther than the snapping threshold from the current parent
// widget, open a blank holder window to reparent to
MaybeCreateHolderWindowAndReparent();
}
}
}
void GlicWindowController::MoveToBrowserPinTarget(Browser* browser) {
if (!widget_) {
return;
}
gfx::Rect glic_rect = widget_->GetWindowBoundsInScreen();
// TODO(andreaxg): Fix exact snap location.
gfx::Rect glic_button_rect = browser->window()
->AsBrowserView()
->tab_strip_region_view()
->glic_button()
->GetBoundsInScreen();
gfx::Point top_right = glic_button_rect.top_right();
int tab_strip_padding = GetLayoutConstant(TAB_STRIP_PADDING);
glic_rect.set_x(top_right.x() - glic_rect.width() - tab_strip_padding);
glic_rect.set_y(top_right.y() + tab_strip_padding);
widget_->SetBounds(glic_rect);
}
void GlicWindowController::MaybeCreateHolderWindowAndReparent() {
pinned_target_widget_observer_.SetPinnedTargetWidget(nullptr);
if (!holder_widget_) {
holder_widget_ = std::make_unique<views::Widget>();
views::Widget::InitParams params(
views::Widget::InitParams::CLIENT_OWNS_WIDGET,
views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
params.activatable = views::Widget::InitParams::Activatable::kNo;
params.accept_events = false;
// Widget name is specified for debug purposes.
params.name = "HolderWindow";
params.bounds = gfx::Rect(0, 0, 0, 0);
params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
holder_widget_->Init(std::move(params));
}
views::Widget::ReparentNativeView(widget_->GetNativeView(),
holder_widget_->GetNativeView());
}
///////////////////////////////////////////////////////////////////////////////
// PinnedTargetWidgetObserver implementations:
GlicWindowController::PinnedTargetWidgetObserver::PinnedTargetWidgetObserver(
glic::GlicWindowController* glic_window_controller)
: glic_window_controller_(glic_window_controller) {}
GlicWindowController::PinnedTargetWidgetObserver::
~PinnedTargetWidgetObserver() {
SetPinnedTargetWidget(nullptr);
}
void GlicWindowController::PinnedTargetWidgetObserver::SetPinnedTargetWidget(
views::Widget* widget) {
if (widget == pinned_target_widget_) {
return;
}
if (pinned_target_widget_ && pinned_target_widget_->HasObserver(this)) {
pinned_target_widget_->RemoveObserver(this);
pinned_target_widget_ = nullptr;
}
if (widget && !widget->HasObserver(this)) {
widget->AddObserver(this);
pinned_target_widget_ = widget;
}
}
void GlicWindowController::PinnedTargetWidgetObserver::OnWidgetBoundsChanged(
views::Widget* widget,
const gfx::Rect& new_bounds) {
glic_window_controller_->MoveToBrowserPinTarget(
chrome::FindBrowserWithWindow(widget->GetNativeWindow()));
}
void GlicWindowController::PinnedTargetWidgetObserver::OnWidgetDestroying(
views::Widget* widget) {
SetPinnedTargetWidget(nullptr);
}
base::WeakPtr<GlicWindowController> GlicWindowController::GetWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
GlicWindowController::~GlicWindowController() = default;
} // namespace glic