blob: aba29e4af5e84daeb56e672470a98925fbde64d3 [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 "base/check.h"
#include "base/metrics/histogram_functions.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/glic/glic.mojom.h"
#include "chrome/browser/glic/glic_enabling.h"
#include "chrome/browser/glic/glic_fre_controller.h"
#include "chrome/browser/glic/glic_fre_dialog_view.h"
#include "chrome/browser/glic/glic_keyed_service.h"
#include "chrome/browser/glic/glic_metrics.h"
#include "chrome/browser/glic/glic_pref_names.h"
#include "chrome/browser/glic/glic_view.h"
#include "chrome/browser/glic/glic_window_resize_animation.h"
#include "chrome/browser/glic/scoped_glic_button_indicator.h"
#include "chrome/browser/glic/webui_contents_container.h"
#include "chrome/browser/profiles/keep_alive/profile_keep_alive_types.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/tabs/public/tab_dialog_manager.h"
#include "chrome/browser/ui/tabs/public/tab_features.h"
#include "chrome/browser/ui/tabs/public/tab_interface.h"
#include "chrome/browser/ui/views/chrome_widget_sublevel.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/tabs/glic_button.h"
#include "chrome/browser/ui/views/tabs/tab_strip_action_container.h"
#include "chrome/browser/ui/views/tabs/window_finder.h"
#include "content/public/browser/web_contents.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"
#include "ui/views/interaction/element_tracker_views.h"
#include "ui/views/widget/native_widget.h"
#include "ui/views/widget/widget_observer.h"
#if BUILDFLAG(IS_WIN)
#include "ui/display/win/screen_win.h"
#include "ui/views/win/hwnd_util.h"
#endif // BUILDFLAG(IS_WIN)
namespace glic {
DEFINE_CUSTOM_ELEMENT_EVENT_TYPE(kGlicWidgetAttached);
void* kGlicWidgetIdentifier = &kGlicWidgetIdentifier;
namespace {
// Default value for adding a buffer to the attachment zone.
constexpr static int kAttachmentBuffer = 20;
constexpr static int kDetachYDistance = 36;
constexpr static int kWidgetDefaultWidth = 300;
constexpr static int kWidgetTopBarHeight = 48;
constexpr static int kAnimationDurationMs = 300;
constexpr static int kInitialDetachedYPosition = 48;
constexpr char kHistogramGlicPanelPresentationTime[] =
"Glic.PanelPresentationTime";
constexpr static int kCornerRadius = 12;
constexpr static SkColor kDefaultBackgroundColor =
SkColorSetARGB(255, 27, 28, 29);
mojom::PanelState CreatePanelState(bool widget_visible,
Browser* attached_browser) {
mojom::PanelState panel_state;
if (!widget_visible) {
panel_state.kind = mojom::PanelState_Kind::kHidden;
} else if (attached_browser) {
panel_state.kind = mojom::PanelState_Kind::kAttached;
panel_state.window_id = attached_browser->session_id().id();
} else {
panel_state.kind = mojom::PanelState_Kind::kDetached;
}
return panel_state;
}
GlicButton* GetGlicButton(Browser* browser) {
return browser->window()
->AsBrowserView()
->tab_strip_region_view()
->GetGlicButton();
}
} // namespace
// Helper class for observing mouse and key events from native window.
class GlicWindowController::WindowEventObserver : public ui::EventObserver {
public:
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) {
mouse_down_in_draggable_area_ =
glic_view_->IsPointWithinDraggableArea(mouse_location);
initial_press_loc_ = mouse_location;
}
if (event.type() == ui::EventType::kMouseReleased &&
event.AsMouseEvent()->IsRightMouseButton() &&
mouse_down_in_draggable_area_) {
glic_window_controller_->ShowTitleBarContextMenuAt(mouse_location);
}
if (event.type() == ui::EventType::kMouseReleased ||
event.type() == ui::EventType::kMouseExited) {
mouse_down_in_draggable_area_ = false;
initial_press_loc_ = gfx::Point();
}
// 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_->ShouldStartDrag(initial_press_loc_,
mouse_location)) {
glic_window_controller_->HandleWindowDragWithOffset(
mouse_location.OffsetFromOrigin());
}
}
private:
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_;
// Tracks the initial kMousePressed location of a potential drag.
gfx::Point initial_press_loc_;
};
// This class observes the view and widget the glic widget anchors to and
// notifies the controller whenever their bounds change.
class GlicWindowController::AnchorObserver : public views::ViewObserver,
public views::WidgetObserver {
public:
AnchorObserver(GlicWindowController* controller, views::View* anchor_view)
: controller_(controller) {
view_observation_.Observe(anchor_view);
CHECK(anchor_view->GetWidget());
widget_observation_.Observe(anchor_view->GetWidget());
}
private:
// views::ViewObserver:
void OnViewAddedToWidget(views::View* anchor_view) override {
// This event is fired on entering and exiting mac fullscreen. The anchor
// view will be moved from the browser view to an overlay widget, or the
// other way around.
widget_observation_.Observe(anchor_view->GetWidget());
}
void OnViewRemovedFromWidget(views::View* anchor_view) override {
widget_observation_.Reset();
}
void OnViewBoundsChanged(views::View* anchor_view) override {
CHECK(controller_->attached_browser());
controller_->MovePositionToBrowserGlicButton(
controller_->attached_browser(),
/*animate=*/false);
}
// views::WidgetObserver:
void OnWidgetBoundsChanged(views::Widget* anchor_widget,
const gfx::Rect& bounds) override {
CHECK(controller_->attached_browser());
controller_->MovePositionToBrowserGlicButton(
controller_->attached_browser(),
/*animate=*/false);
}
// No need to observe widget destroy because the observed view will be removed
// from the widget and notifies this class.
base::ScopedObservation<views::View, views::ViewObserver> view_observation_{
this};
base::ScopedObservation<views::Widget, views::WidgetObserver>
widget_observation_{this};
raw_ptr<GlicWindowController> controller_;
};
GlicWindowController::GlicWindowController(Profile* profile,
GlicKeyedService* glic_service)
: profile_(profile),
fre_controller_(std::make_unique<GlicFreController>()),
window_finder_(std::make_unique<WindowFinder>()),
glic_service_(glic_service) {}
GlicWindowController::~GlicWindowController() = default;
void GlicWindowController::WebClientInitializeFailed() {
if (state_ == State::kOpenAnimation ||
state_ == State::kWaitingForGlicToLoad) {
// TODO(crbug.com/388328847): The web client failed to initialize. Decide
// what the fallback behavior is. Additionally, we probably need some kind
// of timeout and/or loading indicator if loading takes too much time. For
// now, show the UI anyway, which should be helpful in development.
LOG(ERROR)
<< "Glic web client failed to initialize, it won't work properly.";
show_start_time_ = base::TimeTicks();
ShowFinish();
}
}
void GlicWindowController::LoginPageCommitted() {
login_page_committed_ = true;
if ((state_ == State::kOpenAnimation ||
state_ == State::kWaitingForGlicToLoad) &&
!web_client_) {
// TODO(crbug.com/388328847): Temporarily allow showing the UI when a login
// page is reached.
show_start_time_ = base::TimeTicks();
ShowFinish();
}
}
void GlicWindowController::SetWebClient(GlicWebClientAccess* web_client) {
// If state_ == kClosed, then store web_client_ for a future call to Open().
// Once we get crash/error flows, this can theoretically happen with state_ ==
// kOpen, but those will those need to handled alongside the crash/error
// flows.
web_client_ = web_client;
// Always reset `glic_loaded_` since the web client has changed.
glic_loaded_ = false;
if (state_ == State::kOpenAnimation ||
state_ == State::kWaitingForGlicToLoad) {
if (web_client_) {
WaitForGlicToLoad();
} else {
// TODO(crbug.com/388328847): The web client could disconnect without a
// WebClientInitializeFailed() call, for example, if the renderer crashes.
// Determine the correct behavior in this case.
LOG(ERROR) << "Glic web client disconnected before showing the window.";
show_start_time_ = base::TimeTicks();
ShowFinish();
}
}
}
// Monitoring the glic widget.
void GlicWindowController::OnWidgetActivationChanged(views::Widget* widget,
bool active) {
if (GetGlicWidget() == widget) {
window_activation_callback_list_.Notify(active);
}
}
// Monitoring the glic widget.
void GlicWindowController::OnWidgetDestroyed(views::Widget* widget) {
// This is used to handle the case where the native window is closed
// directly (e.g., Windows context menu close on the title bar).
// Conceptually this should synchronously call Close(), but the Widget
// implementation currently does not support this.
if (GetGlicWidget() == widget) {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(&GlicWindowController::Close,
weak_ptr_factory_.GetWeakPtr()));
}
}
void GlicWindowController::OnWidgetBoundsChanged(views::Widget* widget,
const gfx::Rect& new_bounds) {
if (!attached_browser_ && in_move_loop_) {
// While in a move loop, look for nearby browsers to toggle the drop to
// attach indicator.
HandleGlicButtonIndicator();
}
}
void GlicWindowController::Toggle(BrowserWindowInterface* bwi,
bool prevent_close) {
// If `bwi` is non-null, the glic button was clicked on a specific window and
// glic should be attached to that window. Otherwise glic was invoked from the
// hotkey or other OS-level entrypoint.
Browser* new_attached_browser =
bwi ? bwi->GetBrowserForMigrationOnly() : nullptr;
if (!fre_controller_) {
// It can be the case that the fre_controller_ is torn down (eg, this
// happens when it is closed via the context menu). Recreate the
// controller here, if needed.
fre_controller_ = std::make_unique<GlicFreController>();
}
// Show the FRE if not yet completed, and if we have a browser to use.
if (fre_controller_->ShouldShowFreDialog(profile_)) {
if (!fre_controller_->CanShowFreDialog(new_attached_browser)) {
return;
}
fre_controller_->ShowFreDialog(profile_, new_attached_browser);
return;
}
// In the case where the user invokes the hotkey, and the most recently used
// window for the glic profile is active, treat this as if the user clicked
// the glic button on that window.
// TODO(392644541): There may be edge cases w.r.t. multi-glic-profile.
if (!new_attached_browser) {
Browser* last_active_browser = chrome::FindLastActiveWithProfile(profile_);
if (last_active_browser && last_active_browser->IsActive()) {
new_attached_browser = last_active_browser;
}
}
auto maybe_close = [this, prevent_close] {
if (!prevent_close) {
Close();
}
};
// Pressing the button or the hotkey when the window is open, or waiting to
// load should close it. The latter is required because otherwise if there
// were an error loading the backend (or if it just took a long time) then the
// button/hotkey would become unresponsive.
//
// In the future, when the WebUI can send its status back to the controller
// via mojom, we could explicitly restrict the second case to loading,
// offline, and error states.
if (state_ == State::kOpen || state_ == State::kWaitingForGlicToLoad) {
if (new_attached_browser) {
if (new_attached_browser == attached_browser_) {
// Button was clicked on same browser: close.
// There are three ways for this to happen. Normally the glic window
// obscures the glic button. Either the user used keyboard navigation to
// click the glic button, the user clicked the button early and the
// button click was eventually processed asynchronously after the button
// was obscured, or the user invokes the glic hotkey while glic is
// attached to the active window.
maybe_close();
} else {
// Button clicked on a different browser: attach to that one.
AttachToBrowser(new_attached_browser);
}
return;
}
// Everything else in this block handles the case where the user invokes the
// hotkey and the most recently used window from the glic profile is not
// active.
// Already attached?
if (attached_browser_) {
if (IsActive()) {
// Hotkey when glic active and attached: close.
maybe_close();
return;
}
// Hotkey when glic is inactive and attached:
if (attached_browser_->IsActive()) {
// Hotkey when glic inactive but attached to active browser: close.
// Note: this should not be possible, since if the attached browser is
// active, new_attached_browser must not have been null.
maybe_close();
} else {
// Hotkey when neither attached browser nor glic are active: open
// detached.
CloseAndReopenDetached();
}
return;
}
// Hotkey invoked when glic is already detached.
maybe_close();
} else if (state_ != State::kClosed) {
// Currently in the process of showing the widget, allow that to finish.
return;
} else {
Show(new_attached_browser);
}
}
void GlicWindowController::ShowDetachedForTesting() {
Show(nullptr);
}
void GlicWindowController::WebUiStateChanged(mojom::WebUiState new_state) {
if (webui_state_ != new_state) {
// UI State has changed
webui_state_ = new_state;
webui_state_observers_.Notify(&WebUiStateObserver::WebUiStateChanged,
webui_state_);
}
}
void GlicWindowController::Show(Browser* browser) {
// At this point State must be kClosed, and all glic window state must be
// unset.
CHECK(!attached_browser_);
state_ = State::kOpenAnimation;
glic_service_->metrics()->OnGlicWindowOpen();
show_start_time_ = base::TimeTicks::Now();
if (!contents_) {
contents_ = std::make_unique<WebUIContentsContainer>(profile_, this);
}
glic_service_->NotifyWindowIntentToShow();
glic_service_->GetAuthController().CheckAuthBeforeShow(
base::BindOnce(&GlicWindowController::AuthCheckDoneBeforeShow,
GetWeakPtr(), browser ? browser->AsWeakPtr() : nullptr));
}
void GlicWindowController::AuthCheckDoneBeforeShow(
base::WeakPtr<Browser> browser_for_attachment,
AuthController::BeforeShowResult result) {
switch (result) {
case AuthController::BeforeShowResult::kShowingReauthSigninPage:
state_ = State::kClosed;
return;
case AuthController::BeforeShowResult::kReady:
case AuthController::BeforeShowResult::kSyncFailed:
break;
}
if (browser_for_attachment) {
OpenAttached(browser_for_attachment.get());
} else {
OpenDetached();
}
// If the web client is already initialized, wait for it to load in parallel.
if (web_client_) {
WaitForGlicToLoad();
} else if (login_page_committed_) {
// This indicates that we've warmed the web client and it has hit a login
// page. See LoginPageCommitted.
ShowFinish();
}
NotifyIfPanelStateChanged();
}
gfx::Rect GlicWindowController::GetInitialDetachedBounds() {
gfx::Size widget_size(kWidgetDefaultWidth, kWidgetTopBarHeight);
if (glic_size_) {
widget_size = *glic_size_;
}
gfx::Rect initial_rect;
// 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.
gfx::Point top_right_point = GetTopRightPositionForDetachedGlicWindow();
int padding = 50;
initial_rect.set_x(top_right_point.x() - widget_size.width() - padding);
initial_rect.set_y(top_right_point.y());
initial_rect.set_size(widget_size);
return initial_rect;
}
void GlicWindowController::OpenAttached(Browser* browser) {
GlicButton* glic_button = browser ? GetGlicButton(browser) : nullptr;
// 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.
gfx::Point top_right_point =
GetTopRightPositionForAttachedGlicWindow(glic_button);
gfx::Rect glic_window_widget_initial_rect = glic_button->GetBoundsWithInset();
glic_widget_ = CreateGlicWidget(profile_, glic_window_widget_initial_rect);
glic_widget_observation_.Observe(glic_widget_.get());
glic_widget_->Show();
AttachToBrowser(browser);
// Set target bounds for animation and run the open attached animation.
gfx::Size widget_size(kWidgetDefaultWidth, kWidgetTopBarHeight);
if (glic_size_) {
widget_size = *glic_size_;
}
gfx::Rect target_bounds = glic_widget_->GetWindowBoundsInScreen();
int final_x = top_right_point.x() - widget_size.width();
target_bounds.set_x(final_x);
target_bounds.set_width(widget_size.width());
target_bounds.set_height(widget_size.height());
// TODO(crbug.com/389982576): Match the background color of the widget with
// the web client background.
GetGlicView()->SetBackground(views::CreateRoundedRectBackground(
kDefaultBackgroundColor, kCornerRadius));
// If there's a browser, then animate.
AnimateBounds(target_bounds, base::Milliseconds(kAnimationDurationMs),
base::BindOnce(&GlicWindowController::OpenAnimationFinished,
GetWeakPtr()));
}
void GlicWindowController::OpenDetached() {
gfx::Rect initial_bounds = GetInitialDetachedBounds();
// Make the widget.
glic_widget_ = CreateGlicWidget(profile_, initial_bounds);
glic_widget_observation_.Observe(glic_widget_.get());
// Be sure to reparent the widget and set its state first before showing it.
MaybeCreateHolderWindowAndReparent();
GetGlicWidget()->Show();
gfx::Rect target_bounds = glic_widget_->GetWindowBoundsInScreen();
target_bounds.set_y(initial_bounds.y() + kInitialDetachedYPosition);
// TODO(crbug.com/389982576): Match the background color of the widget with
// the web client background.
GetGlicView()->SetBackground(
views::CreateRoundedRectBackground(SK_ColorBLACK, 12));
AnimateBounds(target_bounds, base::Milliseconds(kAnimationDurationMs),
base::BindOnce(&GlicWindowController::OpenAnimationFinished,
GetWeakPtr()));
}
// This happens after the web client is initialized. It signals the web client
// that it will be shown, and waits for the response before actually showing the
// widget.
void GlicWindowController::WaitForGlicToLoad() {
DCHECK(web_client_);
// Notify the web client that the panel will open, and wait for the response
// to actually show the window.
web_client_->PanelWillOpen(
CreatePanelState(true, attached_browser_),
base::BindOnce(&GlicWindowController::GlicLoaded, GetWeakPtr()));
}
void GlicWindowController::GlicLoaded(mojom::WebClientMode starting_mode) {
// TODO: Use `starting_mode` to log latency metrics.
DVLOG(1) << "GlicLoaded with " << starting_mode;
starting_mode_ = starting_mode;
glic_loaded_ = true;
if (state_ == State::kWaitingForGlicToLoad) {
ShowFinish();
}
}
void GlicWindowController::OpenAnimationFinished() {
if (state_ == State::kOpenAnimation) {
state_ = State::kWaitingForGlicToLoad;
GetGlicView()->web_view()->SetWebContents(contents_->web_contents());
if (glic_loaded_) {
ShowFinish();
}
}
}
void GlicWindowController::ShowFinish() {
login_page_committed_ = false;
if (state_ == State::kClosed || state_ == State::kOpen) {
return;
}
state_ = State::kOpen;
// Record the presentation time of showing the glic panel in an UMA histogram.
if (web_client_ && !show_start_time_.is_null()) {
std::string input_mode;
if (starting_mode_ == mojom::WebClientMode::kText) {
input_mode = ".Text";
} else if (starting_mode_ == mojom::WebClientMode::kAudio) {
input_mode = ".Audio";
}
base::TimeDelta presentation_time =
base::TimeTicks::Now() - show_start_time_;
base::UmaHistogramCustomTimes(
base::StrCat({kHistogramGlicPanelPresentationTime, ".All"}),
presentation_time, base::Milliseconds(1), base::Seconds(60), 50);
if (starting_mode_ != mojom::WebClientMode::kUnknown) {
base::UmaHistogramCustomTimes(
base::StrCat({kHistogramGlicPanelPresentationTime, input_mode}),
presentation_time, base::Milliseconds(1), base::Seconds(60), 50);
}
ResetPresentationTimingState();
}
window_event_observer_ =
std::make_unique<WindowEventObserver>(this, GetGlicView());
// Set the draggable area to the top bar of the window, by default.
GetGlicView()->SetDraggableAreas(
{{0, 0, GetGlicView()->width(), kWidgetTopBarHeight}});
NotifyIfPanelStateChanged();
}
GlicView* GlicWindowController::GetGlicView() {
if (!GetGlicWidget()) {
return nullptr;
}
return static_cast<GlicView*>(GetGlicWidget()->GetContentsView());
}
views::Widget* GlicWindowController::GetGlicWidget() {
return glic_widget_.get();
}
content::WebContents* GlicWindowController::GetWebContents() {
if (!contents_) {
return nullptr;
}
return contents_->web_contents();
}
content::WebContents* GlicWindowController::GetFreWebContents() {
if (!fre_controller_) {
return nullptr;
}
return fre_controller_->GetWebContents();
}
gfx::Point GlicWindowController::GetTopRightPositionForAttachedGlicWindow(
GlicButton* glic_button) {
// The widget should be placed so its top right corner matches the visible top
// right corner of the glic button.
return glic_button->GetBoundsWithInset().top_right();
}
gfx::Point GlicWindowController::GetTopRightPositionForDetachedGlicWindow() {
// Use the top right corner of the display of the most recently
// active browser. If there was no recently active browser, use the primary
// display.
Browser* last_active_browser = BrowserList::GetInstance()->GetLastActive();
std::optional<display::Display> display_to_use;
if (last_active_browser) {
std::optional<display::Display> widget_display =
last_active_browser->GetBrowserView().GetWidget()->GetNearestDisplay();
if (widget_display) {
display_to_use = widget_display.value();
}
}
if (!display_to_use) {
display_to_use = display::Screen::GetScreen()->GetPrimaryDisplay();
}
gfx::Point top_right_bounds = display_to_use->work_area().top_right();
return top_right_bounds;
}
void GlicWindowController::AttachedBrowserDidClose(
BrowserWindowInterface* browser) {
ForceClose();
}
void GlicWindowController::ResizeFinished() {
window_resize_animation_.reset();
}
void GlicWindowController::Attach() {
if (!GetGlicWidget()) {
return;
}
// TODO (crbug.com/388917542) Determine which browser to attach to. Currently
// attaches to the last focused glic-compatible browser.
for (Browser* browser : BrowserList::GetInstance()->OrderedByActivation()) {
if (!IsBrowserGlicCompatible(browser)) {
continue;
}
AttachToBrowser(browser);
return;
}
}
void GlicWindowController::Detach() {
if (state_ != State::kOpen || !attached_browser_) {
return;
}
state_ = State::kDetaching;
MaybeCreateHolderWindowAndReparent();
// Move down a little bit when detaching.
gfx::Rect new_bounds = glic_widget_->GetWindowBoundsInScreen();
new_bounds.set_y(new_bounds.y() + kDetachYDistance);
AnimateBounds(
new_bounds, base::Milliseconds(kAnimationDurationMs),
base::BindOnce(&GlicWindowController::DetachFinished, GetWeakPtr()));
}
void GlicWindowController::DetachFinished() {
state_ = State::kOpen;
}
void GlicWindowController::AttachToBrowser(Browser* browser) {
CHECK(GetGlicWidget());
attached_browser_ = browser;
MovePositionToBrowserGlicButton(browser, true);
// Close the holder window.
holder_widget_.reset();
BrowserView* browser_view = browser->window()->AsBrowserView();
CHECK(browser_view);
// Although the glic widget is conceptually anchored to the glic button, we
// intentionally observe its parent view, the tab strip region, for bounds
// changes. This is because views bounds changed events when its *local*
// bounds change. When the tab strip resizes, the glic button's local bounds
// (relative to the tab strip) typically remain constant.
views::View* anchor_view = browser_view->tab_strip_region_view();
anchor_observer_ = std::make_unique<AnchorObserver>(this, anchor_view);
// Makes the glic widget a child view of the anchor view's widget, which is
// different from the browser widget in mac immersive fullscreen.
glic_widget_->Reparent(anchor_view->GetWidget());
if (!IsActive()) {
GetGlicWidget()->Activate();
}
NotifyIfPanelStateChanged();
// When attached to a browser window, the glic widget mustn't float and when
// interacted with must behave like any other widget.
GetGlicWidget()->SetZOrderLevel(ui::ZOrderLevel::kNormal);
#if BUILDFLAG(IS_MAC)
GetGlicWidget()->SetActivationIndependence(false);
GetGlicWidget()->SetVisibleOnAllWorkspaces(false);
#endif
browser_close_subscription_ = browser->RegisterBrowserDidClose(
base::BindRepeating(&GlicWindowController::AttachedBrowserDidClose,
base::Unretained(this)));
// Trigger custom event for testing.
views::ElementTrackerViews::GetInstance()->NotifyCustomEvent(
kGlicWidgetAttached, GetGlicButton(browser));
}
void GlicWindowController::Resize(const gfx::Size& size,
base::TimeDelta duration,
base::OnceClosure callback) {
glic_size_ = size;
// If the glic window is not in the ready state, do nothing for now.
// TODO(https://crbug.com/379164689): Drive resize animations for error states
// from the browser. For now, we allow animations during the waiting state.
// TOOD(https://crbug.com/392668958): If the widget is ready and asks for a
// resize before the opening animation is finished, we will stop the current
// animation and resize to the final size. Investigate a smoother way to
// animate this transition.
if (state_ == State::kOpen || state_ == State::kWaitingForGlicToLoad ||
state_ == State::kOpenAnimation) {
AnimateSize(size, duration, std::move(callback));
} else {
// If the glic window is closed, or the widget isn't ready (e.g. because
// it's currently still animating closed) immediately post the callback.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, std::move(callback));
}
}
void GlicWindowController::AnimateBounds(const gfx::Rect& target_bounds,
base::TimeDelta duration,
base::OnceClosure callback) {
CHECK(GetGlicWidget());
// Stop the current animation if any.
if (window_resize_animation_) {
ResizeFinished();
}
if (duration < base::Milliseconds(0)) {
duration = base::Milliseconds(0);
}
window_resize_animation_ = std::make_unique<GlicWindowResizeAnimation>(
this, target_bounds, duration, std::move(callback));
}
void GlicWindowController::AnimateSize(const gfx::Size& target_size,
base::TimeDelta duration,
base::OnceClosure callback) {
// Maintain the top-right corner.
gfx::Rect current_bounds = GetGlicWidget()->GetWindowBoundsInScreen();
int original_top_right = current_bounds.x() + current_bounds.width();
current_bounds.set_size(target_size);
current_bounds.set_x(original_top_right - target_size.width());
AnimateBounds(current_bounds, duration, std::move(callback));
}
std::unique_ptr<views::Widget> GlicWindowController::CreateGlicWidget(
Profile* profile,
const gfx::Rect& initial_bounds) {
views::Widget::InitParams params(
views::Widget::InitParams::CLIENT_OWNS_WIDGET,
views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
#if BUILDFLAG(IS_WIN)
params.dont_show_in_taskbar = true;
params.force_system_menu_for_frameless = true;
params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
#endif
params.bounds = initial_bounds;
params.sublevel = ChromeWidgetSublevel::kSublevelGlic;
params.name = "GlicWidget";
std::unique_ptr<views::Widget> widget =
std::make_unique<views::Widget>(std::move(params));
widget->SetContentsView(
std::make_unique<GlicView>(profile, initial_bounds.size()));
// Mac fullscreen uses this identifier to find this widget and reparent it to
// the overlay widget.
widget->SetNativeWindowProperty(views::kWidgetIdentifierKey,
kGlicWidgetIdentifier);
return widget;
}
gfx::Size GlicWindowController::GetSize() {
if (!GetGlicWidget()) {
return gfx::Size();
}
return GetGlicWidget()->GetSize();
}
void GlicWindowController::SetDraggableAreas(
const std::vector<gfx::Rect>& draggable_areas) {
GlicView* glic_view = GetGlicView();
if (!glic_view) {
return;
}
glic_view->SetDraggableAreas(draggable_areas);
}
void GlicWindowController::Close() {
if (state_ == State::kCloseAnimation || state_ == State::kClosed) {
return;
}
const bool reopen_detached = state_ == State::kClosingToReopenDetached;
if (attached_browser_) {
state_ = State::kCloseAnimation;
GetGlicView()->web_view()->SetWebContents(nullptr);
GlicButton* glic_button = GetGlicButton(attached_browser_);
AnimateBounds(glic_button->GetBoundsWithInset(),
base::Milliseconds(kAnimationDurationMs),
base::BindOnce(&GlicWindowController::CloseFinish,
GetWeakPtr(), reopen_detached));
} else {
CloseFinish(reopen_detached);
}
}
void GlicWindowController::CloseFinish(bool reopen_detached) {
if (state_ == State::kClosed) {
return;
}
glic_service_->metrics()->OnGlicWindowClose();
base::UmaHistogramEnumeration("Glic.PanelWebUiState.FinishState2",
webui_state_);
state_ = State::kClosed;
attached_browser_ = nullptr;
anchor_observer_.reset();
window_resize_animation_.reset();
window_event_observer_.reset();
browser_close_subscription_.reset();
glic_widget_observation_.Reset();
glic_widget_.reset();
scoped_glic_button_indicator_.reset();
NotifyIfPanelStateChanged();
if (web_client_) {
// The webview is kept alive by default, no need to use this callback.
web_client_->PanelWasClosed(base::DoNothing());
}
if (reopen_detached) {
Show(nullptr);
}
}
void GlicWindowController::ForceClose() {
CloseFinish(/*reopen_detached=*/false);
}
void GlicWindowController::CloseAndReopenDetached() {
if (state_ != State::kOpen) {
return;
}
state_ = State::kClosingToReopenDetached;
Close();
}
void GlicWindowController::ShowTitleBarContextMenuAt(gfx::Point event_loc) {
#if BUILDFLAG(IS_WIN)
views::View::ConvertPointToScreen(GetGlicView(), &event_loc);
event_loc = display::win::ScreenWin::DIPToScreenPoint(event_loc);
views::ShowSystemMenuAtScreenPixelLocation(views::HWNDForView(GetGlicView()),
event_loc);
#endif // BUILDFLAG(IS_WIN)
}
bool GlicWindowController::ShouldStartDrag(const gfx::Point& initial_press_loc,
const gfx::Point& mouse_location) {
// Determine if the mouse has moved beyond a minimum elasticity distance
// in any direction from the starting point.
static const int kMinimumDragDistance = 10;
int x_offset = abs(mouse_location.x() - initial_press_loc.x());
int y_offset = abs(mouse_location.y() - initial_press_loc.y());
return sqrt(pow(static_cast<float>(x_offset), 2) +
pow(static_cast<float>(y_offset), 2)) > kMinimumDragDistance;
}
void GlicWindowController::HandleWindowDragWithOffset(
gfx::Vector2d mouse_offset) {
// 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;
const views::Widget::MoveLoopSource move_loop_source =
views::Widget::MoveLoopSource::kMouse;
// Set glic to a floating z-order while dragging so browsers brought into
// focus by HandleGlicButtonIndicator won't show in front of glic.
GetGlicWidget()->SetZOrderLevel(ui::ZOrderLevel::kFloatingWindow);
GetGlicWidget()->RunMoveLoop(
mouse_offset, move_loop_source,
views::Widget::MoveLoopEscapeBehavior::kDontHide);
in_move_loop_ = false;
// set glic z-order back to normal after drag is done.
GetGlicWidget()->SetZOrderLevel(ui::ZOrderLevel::kNormal);
scoped_glic_button_indicator_.reset();
// Check whether `GetGlicWidget()` is in a position to attach to a
// browser window.
HandleAttachmentToBrowserWindows();
}
}
void GlicWindowController::HandleAttachmentToBrowserWindows() {
Browser* browser = FindBrowserForAttachment();
// No browser within attachment range so maybe reparent under an empty holder
// widget.
if (!browser) {
MaybeCreateHolderWindowAndReparent();
return;
}
// Attach to the found browser.
AttachToBrowser(browser);
}
void GlicWindowController::HandleGlicButtonIndicator() {
Browser* browser = FindBrowserForAttachment();
// No browser within attachment range so reset indicators
if (!browser) {
scoped_glic_button_indicator_.reset();
return;
}
GlicButton* glic_button = GetGlicButton(browser);
// If there isn't an existing scoped indicator for this button, create one.
if (!scoped_glic_button_indicator_ ||
scoped_glic_button_indicator_->GetGlicButton() != glic_button) {
// Bring the browser to the front.
browser->GetBrowserView().GetWidget()->Activate();
scoped_glic_button_indicator_ =
std::make_unique<ScopedGlicButtonIndicator>(glic_button);
}
}
Browser* GlicWindowController::FindBrowserForAttachment() {
// The profile must have started off as Glic enabled since a Glic widget is
// open but it may have been disabled at runtime by policy. In this edge-case,
// prevent reattaching back to a window (as it no longer has a GlicButton).
if (!GlicEnabling::IsEnabledForProfile(profile_)) {
return nullptr;
}
gfx::Point glic_top_right =
GetGlicWidget()->GetWindowBoundsInScreen().top_right();
// Loops through all browsers in activation order with the latest accessed
// browser first.
for (Browser* browser : BrowserList::GetInstance()->OrderedByActivation()) {
if (!IsBrowserGlicCompatible(browser)) {
continue;
}
// If the profile is enabled, the Glic button must be available.
auto* tab_strip_region_view =
browser->GetBrowserView().tab_strip_region_view();
CHECK(tab_strip_region_view);
CHECK(tab_strip_region_view->GetGlicButton());
// Define attachment zone as the right of the tab strip. It either is the
// width of the widget or 1/3 of the tab strip, whichever is smaller.
gfx::Rect attachment_zone = tab_strip_region_view->GetBoundsInScreen();
int width = std::min(attachment_zone.width() / 3, kWidgetDefaultWidth);
attachment_zone.SetByBounds(attachment_zone.right() - width,
attachment_zone.y() - kAttachmentBuffer,
attachment_zone.right() + kAttachmentBuffer,
attachment_zone.bottom());
// If both the left center of the attachment zone and glic button right
// center are occluded, don't consider for attachment.
if (IsBrowserOccludedAtPoint(browser, attachment_zone.left_center()) &&
IsBrowserOccludedAtPoint(browser, tab_strip_region_view->GetGlicButton()
->GetBoundsInScreen()
.right_center())) {
continue;
}
if (attachment_zone.Contains(glic_top_right)) {
return browser;
}
}
// No browser found near glic.
return nullptr;
}
void GlicWindowController::MovePositionToBrowserGlicButton(Browser* browser,
bool animate) {
if (!GetGlicWidget()) {
return;
}
// If the profile's been disabled (e.g. by policy) the window's Glic button
// will be removed so we can't anchor to it. We could work around this by
// keeping the button but disabling and making it invisible but this is an
// edge-case, not sure it's worth the effort.
if (!GlicEnabling::IsEnabledForProfile(browser->profile())) {
return;
}
GlicButton* glic_button = GetGlicButton(browser);
CHECK(glic_button);
gfx::Point top_right = GetTopRightPositionForAttachedGlicWindow(glic_button);
gfx::Rect current_bounds = GetGlicWidget()->GetWindowBoundsInScreen();
gfx::Rect new_bounds = current_bounds;
new_bounds.set_x(top_right.x() - current_bounds.width());
new_bounds.set_y(top_right.y());
gfx::Size cur_widget_size(kWidgetDefaultWidth, kWidgetTopBarHeight);
if (glic_size_) {
cur_widget_size = *glic_size_;
}
new_bounds.set_width(cur_widget_size.width());
new_bounds.set_height(cur_widget_size.height());
// Avoid conversions between pixels and DIP on non 1.0 scale factor displays
// changing widget width and height.
if (animate) {
AnimateBounds(new_bounds, base::Milliseconds(kAnimationDurationMs),
base::DoNothing());
} else {
GetGlicWidget()->SetBounds(new_bounds);
}
NotifyIfPanelStateChanged();
}
void GlicWindowController::MaybeCreateHolderWindowAndReparent() {
attached_browser_ = nullptr;
anchor_observer_.reset();
browser_close_subscription_.reset();
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 = glic_widget_->GetWindowBoundsInScreen();
params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
holder_widget_->Init(std::move(params));
holder_widget_->ShowInactive();
}
glic_widget_->Reparent(holder_widget_.get());
NotifyIfPanelStateChanged();
// When the glic window is in a detached state, elevate its z-order to be
// always on top. On the Mac, mark it as "activation independent" so that
// interacting with it does not activate Chrome.
GetGlicWidget()->SetZOrderLevel(ui::ZOrderLevel::kFloatingWindow);
#if BUILDFLAG(IS_MAC)
GetGlicWidget()->SetActivationIndependence(true);
holder_widget_->SetVisibleOnAllWorkspaces(true);
GetGlicWidget()->SetVisibleOnAllWorkspaces(true);
#endif
}
bool GlicWindowController::IsBrowserGlicCompatible(Browser* browser) {
// A browser is not compatible if it:
// - is not a TYPE_NORMAL browser
// - is from a glic-disabled profile
// - is not visible
// - uses a different Profile from glic
if (!GlicEnabling::IsEnabledForProfile(browser->profile()) ||
!browser->is_type_normal() || !browser->window()->IsVisible() ||
browser->profile() != profile_) {
return false;
}
return true;
}
void GlicWindowController::AddStateObserver(StateObserver* observer) {
state_observers_.AddObserver(observer);
}
void GlicWindowController::RemoveStateObserver(StateObserver* observer) {
state_observers_.RemoveObserver(observer);
}
void GlicWindowController::AddWebUiStateObserver(WebUiStateObserver* observer) {
webui_state_observers_.AddObserver(observer);
}
void GlicWindowController::RemoveWebUiStateObserver(
WebUiStateObserver* observer) {
webui_state_observers_.RemoveObserver(observer);
}
void GlicWindowController::NotifyIfPanelStateChanged() {
auto new_state = ComputePanelState();
if (new_state != panel_state_) {
panel_state_ = new_state;
state_observers_.Notify(&StateObserver::PanelStateChanged, panel_state_);
}
}
mojom::PanelState GlicWindowController::ComputePanelState() const {
mojom::PanelState panel_state;
if (state_ == State::kClosed || state_ == State::kCloseAnimation) {
panel_state.kind = mojom::PanelState_Kind::kHidden;
} else if (attached_browser_) {
panel_state.kind = mojom::PanelState_Kind::kAttached;
panel_state.window_id = attached_browser_->session_id().id();
} else {
panel_state.kind = mojom::PanelState_Kind::kDetached;
}
return panel_state;
}
bool GlicWindowController::IsActive() {
return IsShowing() && GetGlicWidget() && GetGlicWidget()->IsActive();
}
bool GlicWindowController::IsShowing() const {
return !(state_ == State::kClosed || state_ == State::kCloseAnimation);
}
bool GlicWindowController::IsAttached() {
return attached_browser_ != nullptr;
}
base::CallbackListSubscription
GlicWindowController::AddWindowActivationChangedCallback(
WindowActivationChangedCallback callback) {
return window_activation_callback_list_.Add(std::move(callback));
}
void GlicWindowController::Preload() {
if (!contents_) {
contents_ = std::make_unique<WebUIContentsContainer>(profile_, this);
}
}
void GlicWindowController::Reload() {
if (GetFreWebContents()) {
GetFreWebContents()->ReloadFocusedFrame();
}
if (contents_) {
contents_->web_contents()->ReloadFocusedFrame();
}
}
bool GlicWindowController::IsWarmed() {
return !!contents_;
}
base::WeakPtr<GlicWindowController> GlicWindowController::GetWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
void GlicWindowController::Shutdown() {
// Hide first, then clean up (but do not animate).
ForceClose();
contents_.reset();
fre_controller_.reset();
}
void GlicWindowController::ResetPresentationTimingState() {
show_start_time_ = base::TimeTicks();
starting_mode_ = mojom::WebClientMode::kUnknown;
}
bool GlicWindowController::IsBrowserOccludedAtPoint(Browser* browser,
gfx::Point point) {
std::set<gfx::NativeWindow> exclude = {
GetGlicView()->GetWidget()->GetNativeWindow()};
gfx::NativeWindow window =
window_finder_->GetLocalProcessWindowAtPoint(point, exclude);
if (browser->GetBrowserView().GetWidget()->GetNativeWindow() != window) {
return true;
}
return false;
}
} // namespace glic