| // 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 |