blob: c67d078eb796adc52105c920c999d36d9e87b765 [file] [log] [blame]
// Copyright 2012 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/frame/browser_frame_view_chromeos.h"
#include <memory>
#include <optional>
#include "ash/multi_user/multi_user_window_manager.h"
#include "ash/shell.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_util.h"
#include "ash/wm/wm_highlight_border_overlay_delegate.h"
#include "base/check.h"
#include "base/check_op.h"
#include "base/metrics/user_metrics.h"
#include "build/build_config.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/platform_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_attributes_storage.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/themes/theme_properties.h"
#include "chrome/browser/ui/ash/session/session_util.h"
#include "chrome/browser/ui/ash/system_web_apps/system_web_app_ui_utils.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_element_identifiers.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/color/chrome_color_id.h"
#include "chrome/browser/ui/exclusive_access/exclusive_access_manager.h"
#include "chrome/browser/ui/exclusive_access/fullscreen_controller.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/frame/browser_widget.h"
#include "chrome/browser/ui/views/frame/immersive_mode_controller.h"
#include "chrome/browser/ui/views/frame/tab_strip_view_interface.h"
#include "chrome/browser/ui/views/frame/top_container_view.h"
#include "chrome/browser/ui/views/profiles/profile_indicator_icon.h"
#include "chrome/browser/ui/views/side_panel/side_panel.h"
#include "chrome/browser/ui/views/tab_icon_view.h"
#include "chrome/browser/ui/views/tabs/tab_strip.h"
#include "chrome/browser/ui/views/toolbar/toolbar_view.h"
#include "chrome/browser/ui/web_applications/app_browser_controller.h"
#include "chromeos/ash/experiences/system_web_apps/types/system_web_app_delegate.h"
#include "chromeos/components/kiosk/kiosk_utils.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/ui/base/chromeos_ui_constants.h"
#include "chromeos/ui/base/window_properties.h"
#include "chromeos/ui/base/window_state_type.h"
#include "chromeos/ui/frame/caption_buttons/frame_caption_button_container_view.h"
#include "chromeos/ui/frame/default_frame_header.h"
#include "chromeos/ui/frame/frame_utils.h"
#include "components/services/app_service/public/cpp/app_update.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/web_contents.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/env.h"
#include "ui/base/hit_test.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/image_model.h"
#include "ui/base/mojom/window_show_state.mojom.h"
#include "ui/base/ui_base_types.h"
#include "ui/chromeos/styles/cros_styles.h"
#include "ui/display/screen.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rounded_corners_f.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/animation/animation_builder.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/webview/webview.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/rect_based_targeting_utils.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
#include "ui/views/window/caption_button_layout_constants.h"
#if BUILDFLAG(ENABLE_WEBUI_TAB_STRIP)
#include "chrome/browser/ui/views/frame/webui_tab_strip_container_view.h"
#endif // BUILDFLAG(ENABLE_WEBUI_TAB_STRIP)
DEFINE_UI_CLASS_PROPERTY_TYPE(BrowserFrameViewChromeOS*)
namespace {
// The indicator for teleported windows has 8 DIPs before and below it.
constexpr int kProfileIndicatorPadding = 8;
// Returns the layer for the specified `web_view`'s native view.
ui::Layer* GetNativeViewLayer(views::WebView* web_view) {
if (web_view) {
if (views::NativeViewHost* holder = web_view->holder(); holder) {
if (aura::Window* native_view = holder->native_view(); native_view) {
return native_view->layer();
}
}
}
return nullptr;
}
// Returns the render widget host for the specified `web_view`.
content::RenderWidgetHost* GetRenderWidgetHost(views::WebView* web_view) {
if (web_view) {
if (auto* web_contents = web_view->GetWebContents(); web_contents) {
if (auto* rvh = web_contents->GetRenderViewHost(); rvh) {
return rvh->GetWidget();
}
}
}
return nullptr;
}
// Returns whether `browser` should draw its frame header.
// The default is true.
bool ShouldDrawFrameHeader(BrowserWindowInterface* browser) {
// Currently, frame headers are only disabled for custom tab browsers.
return browser->GetType() != BrowserWindowInterface::Type::TYPE_CUSTOM_TAB;
}
DEFINE_UI_CLASS_PROPERTY_KEY(BrowserFrameViewChromeOS*,
kBrowserFrameViewChromeOSKey,
nullptr)
// Returns true if the header should be painted so that it looks the same as
// the header used for packaged apps.
bool UsePackagedAppHeaderStyle(const Browser* browser) {
if (browser->is_type_normal() ||
(browser->is_type_popup() && !browser->is_trusted_source())) {
return false;
}
return !browser->SupportsWindowFeature(Browser::FEATURE_TABSTRIP);
}
// Whether or not the window's title should show the avatar. Practically,
// returns true when the owner of the window is different from the owner of
// the desktop.
bool ShouldShowAvatar(aura::Window* window) {
auto* multi_user_window_manager =
ash::Shell::Get()->multi_user_window_manager();
return !multi_user_window_manager->IsWindowOnDesktopOfUser(
window, multi_user_window_manager->GetWindowOwner(window));
}
} // namespace
class BrowserFrameViewChromeOS::ProfileChangeObserver
: public ProfileAttributesStorage::Observer {
public:
explicit ProfileChangeObserver(BrowserFrameViewChromeOS& frame)
: frame_(frame) {
if (g_browser_process->profile_manager()) {
profile_observation_.Observe(
&g_browser_process->profile_manager()->GetProfileAttributesStorage());
} else {
CHECK_IS_TEST();
}
}
~ProfileChangeObserver() override = default;
// ProfileAttributesStorage::Observer:
void OnProfileAdded(const base::FilePath& profile_path) override {
frame_->UpdateProfileIcons();
}
void OnProfileWasRemoved(const base::FilePath& profile_path,
const std::u16string& profile_name) override {
frame_->UpdateProfileIcons();
}
void OnProfileAvatarChanged(const base::FilePath& profile_path) override {
frame_->UpdateProfileIcons();
}
void OnProfileHighResAvatarLoaded(
const base::FilePath& profile_path) override {
frame_->UpdateProfileIcons();
}
private:
raw_ref<BrowserFrameViewChromeOS> frame_;
base::ScopedObservation<ProfileAttributesStorage,
ProfileAttributesStorage::Observer>
profile_observation_{this};
};
BrowserFrameViewChromeOS::BrowserFrameViewChromeOS(BrowserWidget* widget,
BrowserView* browser_view)
: BrowserFrameView(widget, browser_view) {
ash::window_util::InstallResizeHandleWindowTargeterForWindow(
widget->GetNativeWindow());
aura::Window* frame_window = widget->GetNativeWindow();
frame_window->SetProperty(kBrowserFrameViewChromeOSKey, this);
GetViewAccessibility().SetRole(ax::mojom::Role::kTitleBar);
}
BrowserFrameViewChromeOS::~BrowserFrameViewChromeOS() {
ImmersiveModeController* immersive_controller =
browser_view()->immersive_mode_controller();
if (immersive_controller) {
immersive_controller->RemoveObserver(this);
}
if (profile_indicator_icon_) {
RemoveChildViewT(std::exchange(profile_indicator_icon_, nullptr));
}
}
BrowserFrameViewChromeOS* BrowserFrameViewChromeOS::Get(aura::Window* window) {
return window->GetProperty(kBrowserFrameViewChromeOSKey);
}
void BrowserFrameViewChromeOS::Init() {
Browser* browser = browser_view()->browser();
const bool is_close_button_enabled =
!(browser->app_controller() &&
browser->app_controller()->IsPreventCloseEnabled());
caption_button_container_ =
AddChildView(std::make_unique<chromeos::FrameCaptionButtonContainerView>(
browser_widget(), is_close_button_enabled));
// Initializing the TabIconView is expensive, so only do it if we need to.
if (browser_view()->ShouldShowWindowIcon()) {
AddChildView(views::Builder<TabIconView>()
.CopyAddressTo(&window_icon_)
.SetModel(this)
.Build());
}
UpdateProfileIcons();
window_observation_.Observe(GetFrameWindow());
if (apps::AppServiceProxyFactory::IsAppServiceAvailableForProfile(
browser->profile())) {
app_registry_cache_observation_.Observe(
&apps::AppServiceProxyFactory::GetForProfile(browser->profile())
->AppRegistryCache());
}
// To preserve privacy, tag incognito windows so that they won't be included
// in screenshot sent to assistant server.
if (browser->profile()->IsOffTheRecord()) {
browser_widget()->GetNativeWindow()->SetProperty(
chromeos::kBlockedForAssistantSnapshotKey, true);
}
display_observer_.emplace(this);
if (ShouldDrawFrameHeader(browser)) {
frame_header_ = CreateFrameHeader();
}
if (AppIsPwaWithBorderlessDisplayMode()) {
UpdateBorderlessModeEnabled();
}
browser_view()->immersive_mode_controller()->AddObserver(this);
}
BrowserLayoutParams BrowserFrameViewChromeOS::GetBrowserLayoutParams() const {
BrowserLayoutParams params;
params.visual_client_area = GetLocalBounds();
if (profile_indicator_icon_) {
params.leading_exclusion.content =
gfx::SizeF(profile_indicator_icon_->bounds().right(),
profile_indicator_icon_->bounds().bottom());
params.leading_exclusion.horizontal_padding = kProfileIndicatorPadding;
params.leading_exclusion.vertical_padding =
profile_indicator_icon_->bounds().y();
}
if (GetShowCaptionButtonsWhenNotInOverview()) {
const auto caption_bounds = caption_button_container_->bounds();
params.trailing_exclusion.content =
gfx::SizeF(width() - caption_bounds.x(), caption_bounds.bottom());
}
return params;
}
gfx::Rect BrowserFrameViewChromeOS::GetBoundsForTabStripRegion(
const gfx::Size& tabstrip_minimum_size) const {
const int left_inset = GetTabStripLeftInset();
const bool restored =
!browser_widget()->IsMaximized() && !browser_widget()->IsFullscreen();
return gfx::Rect(left_inset, GetTopInset(restored),
std::max(0, width() - left_inset - GetTabStripRightInset()),
tabstrip_minimum_size.height());
}
gfx::Rect BrowserFrameViewChromeOS::GetBoundsForWebAppFrameToolbar(
const gfx::Size& toolbar_preferred_size) const {
const int x = GetToolbarLeftInset();
const int available_width = caption_button_container_->x() - x;
int painted_height = GetTopInset(false);
if (browser_view()->GetTabStripVisible()) {
painted_height += browser_view()->GetTabStripHeight();
}
return gfx::Rect(x, 0, std::max(0, available_width), painted_height);
}
bool BrowserFrameViewChromeOS::ShouldShowWebAppFrameToolbar() const {
if (!GetShowCaptionButtons()) {
return false;
}
if (browser_view()->browser()->is_type_app_popup() &&
!browser_view()->AppUsesWindowControlsOverlay() &&
!browser_view()->AppUsesBorderlessMode()) {
return false;
}
return true;
}
int BrowserFrameViewChromeOS::GetTopInset(bool restored) const {
// TODO(estade): why do callsites in this class hardcode false for |restored|?
if (!GetShouldPaint()) {
// When immersive fullscreen unrevealed, tabstrip is offscreen with normal
// tapstrip bounds, the top inset should reach this topmost edge.
const ImmersiveModeController* const immersive_controller =
browser_view()->immersive_mode_controller();
if (immersive_controller->IsEnabled() &&
!immersive_controller->IsRevealed()) {
return (-1) * browser_view()->GetTabStripHeight();
}
// The header isn't painted for restored popup/app windows in overview mode,
// but the inset is still calculated below, so the overview code can align
// the window content with a fake header.
if (!GetOverviewMode() || browser_widget()->IsFullscreen() ||
browser_view()->GetTabStripVisible() ||
browser_view()->webui_tab_strip()) {
return 0;
}
}
if (browser_view()->GetTabStripVisible()) {
return 0;
}
Browser* browser = browser_view()->browser();
int header_height = frame_header_ ? frame_header_->GetHeaderHeight() : 0;
const gfx::Size toolbar_size =
browser_view()->GetWebAppFrameToolbarPreferredSize();
if (!toolbar_size.IsEmpty()) {
header_height = std::max(header_height, toolbar_size.height());
}
return UsePackagedAppHeaderStyle(browser)
? header_height
: caption_button_container_->bounds().bottom();
}
void BrowserFrameViewChromeOS::UpdateThrobber(bool running) {
if (window_icon_) {
window_icon_->Update();
}
}
bool BrowserFrameViewChromeOS::CanUserExitFullscreen() const {
return !platform_util::IsBrowserLockedFullscreen(browser_view()->browser());
}
SkColor BrowserFrameViewChromeOS::GetCaptionColor(
BrowserFrameActiveState active_state) const {
// Web apps apply a theme color if specified by the extension/manifest.
std::optional<SkColor> frame_theme_color =
browser_view()->browser()->app_controller()->GetThemeColor();
const SkColor frame_color =
frame_theme_color.value_or(GetFrameColor(active_state));
const SkColor active_caption_color =
views::FrameCaptionButton::GetButtonColor(frame_color);
if (ShouldPaintAsActiveForState(active_state)) {
return active_caption_color;
}
const float inactive_alpha_ratio =
views::FrameCaptionButton::GetInactiveButtonColorAlphaRatio();
return SkColorSetA(active_caption_color,
inactive_alpha_ratio * SK_AlphaOPAQUE);
}
SkColor BrowserFrameViewChromeOS::GetFrameColor(
BrowserFrameActiveState active_state) const {
if (!UsePackagedAppHeaderStyle(browser_view()->browser())) {
return BrowserFrameView::GetFrameColor(active_state);
}
std::optional<SkColor> color;
if (browser_view()->GetIsWebAppType()) {
color = browser_view()->browser()->app_controller()->GetThemeColor();
}
SkColor fallback_color = chromeos::kDefaultFrameColor;
if (GetWidget()) {
// TODO(skau): Migrate to ColorProvider.
fallback_color = cros_styles::ResolveColor(
cros_styles::ColorName::kBgColor,
GetNativeTheme()->preferred_color_scheme() ==
ui::NativeTheme::PreferredColorScheme::kDark);
}
return color.value_or(fallback_color);
}
void BrowserFrameViewChromeOS::UpdateMinimumSize() {
gfx::Size current_min_size = GetMinimumSize();
if (last_minimum_size_ == current_min_size) {
return;
}
last_minimum_size_ = current_min_size;
GetWidget()->OnSizeConstraintsChanged();
}
gfx::Rect BrowserFrameViewChromeOS::GetBoundsForClientView() const {
// The ClientView must be flush with the top edge of the widget so that the
// web contents can take up the entire screen in immersive fullscreen (with
// or without the top-of-window views revealed). When in immersive fullscreen
// and the top-of-window views are revealed, the TopContainerView paints the
// window header by redirecting paints from its background to
// BrowserFrameViewChromeOS.
return bounds();
}
gfx::Rect BrowserFrameViewChromeOS::GetWindowBoundsForClientBounds(
const gfx::Rect& client_bounds) const {
const int top_inset = GetTopInset(false);
return gfx::Rect(client_bounds.x(),
std::max(0, client_bounds.y() - top_inset),
client_bounds.width(), client_bounds.height() + top_inset);
}
int BrowserFrameViewChromeOS::NonClientHitTest(const gfx::Point& point) {
int hit_test = chromeos::FrameBorderNonClientHitTest(this, point);
// When the window is restored (and not in tablet split-view mode) we want a
// large click target above the tabs to drag the window, so redirect clicks in
// the tab's shadow to caption.
if (hit_test == HTCLIENT && !browser_widget()->IsMaximized() &&
!browser_widget()->IsFullscreen() &&
!display::Screen::Get()->InTabletMode()) {
// TODO(crbug.com/40768579): Tab Strip hit calculation and bounds logic
// should reside in the TabStrip class.
gfx::Point client_point(point);
View::ConvertPointToTarget(this, browser_widget()->client_view(),
&client_point);
gfx::Rect tabstrip_shadow_bounds(
browser_view()
->tab_strip_view()
->GetViewByElementId(kTabStripElementId)
->bounds());
constexpr int kTabShadowHeight = 4;
tabstrip_shadow_bounds.set_height(kTabShadowHeight);
if (tabstrip_shadow_bounds.Contains(client_point)) {
return HTCAPTION;
}
}
return hit_test;
}
void BrowserFrameViewChromeOS::ResetWindowControls() {
BrowserFrameView::ResetWindowControls();
caption_button_container_->SetVisible(GetShowCaptionButtons());
caption_button_container_->ResetWindowControls();
}
void BrowserFrameViewChromeOS::WindowControlsOverlayEnabledChanged() {
bool enabled = browser_view()->IsWindowControlsOverlayEnabled();
caption_button_container_->OnWindowControlsOverlayEnabledChanged(
enabled, GetFrameHeaderColor(browser_view()->IsActive()));
}
void BrowserFrameViewChromeOS::UpdateWindowIcon() {
if (window_icon_) {
window_icon_->SchedulePaint();
}
}
void BrowserFrameViewChromeOS::UpdateWindowTitle() {
if (!browser_widget()->IsFullscreen() && frame_header_) {
frame_header_->SchedulePaintForTitle();
}
browser_widget()->GetNativeWindow()->SetProperty(
chromeos::kWindowOverviewTitleKey,
browser_view()->browser()->GetWindowTitleForCurrentTab(
/*include_app_name=*/false));
}
void BrowserFrameViewChromeOS::SizeConstraintsChanged() {}
void BrowserFrameViewChromeOS::OnPaint(gfx::Canvas* canvas) {
if (!GetShouldPaint()) {
return;
}
if (frame_header_) {
frame_header_->PaintHeader(canvas);
}
}
void BrowserFrameViewChromeOS::UpdateBorderlessModeEnabled() {
caption_button_container_->UpdateBorderlessModeEnabled(
browser_view()->IsBorderlessModeEnabled());
}
bool BrowserFrameViewChromeOS::AppIsPwaWithBorderlessDisplayMode() const {
return browser_view()->GetIsWebAppType() &&
browser_view()->AppUsesBorderlessMode();
}
void BrowserFrameViewChromeOS::Layout(PassKey) {
// The header must be laid out before computing |painted_height| because the
// computation of |painted_height| for app and popup windows depends on the
// position of the window controls.
if (frame_header_) {
frame_header_->LayoutHeader();
}
int painted_height = GetTopInset(false);
if (browser_view()->GetTabStripVisible()) {
painted_height += browser_view()->GetTabStripHeight();
}
if (frame_header_) {
frame_header_->SetHeaderHeightForPainting(painted_height);
}
if (profile_indicator_icon_) {
LayoutProfileIndicator();
}
if (AppIsPwaWithBorderlessDisplayMode()) {
UpdateBorderlessModeEnabled();
}
LayoutSuperclass<BrowserFrameView>(this);
UpdateTopViewInset();
if (frame_header_) {
// The top right corner must be occupied by a caption button for easy mouse
// access. This check is agnostic to RTL layout.
DCHECK_EQ(caption_button_container_->y(), 0);
DCHECK_EQ(caption_button_container_->bounds().right(), width());
}
}
gfx::Size BrowserFrameViewChromeOS::GetMinimumSize() const {
// System web apps (e.g. Settings) may have a fixed minimum size.
Browser* browser = browser_view()->browser();
if (ash::IsSystemWebApp(browser)) {
gfx::Size minimum_size = ash::GetSystemWebAppMinimumWindowSize(browser);
if (!minimum_size.IsEmpty()) {
return minimum_size;
}
}
// The minimum size of a borderless window is only limited by the window's
// `highlight_border_overlay_`.
if (browser_view()->IsBorderlessModeEnabled()) {
// `CalculateImageSourceSize()` returns the minimum size needed to draw the
// highlight border, which in turn is the minimum size of a borderless
// window.
return highlight_border_overlay_->CalculateImageSourceSize();
}
gfx::Size min_client_view_size(
browser_widget()->client_view()->GetMinimumSize());
const int min_frame_width =
frame_header_ ? frame_header_->GetMinimumHeaderWidth() : 0;
int min_width = std::max(min_frame_width, min_client_view_size.width());
if (browser_view()->GetTabStripVisible()) {
// Ensure that the minimum width is enough to hold a minimum width tab strip
// at its usual insets.
const int min_tabstrip_width =
browser_view()->tab_strip_view()->GetMinimumSize().width();
min_width =
std::max(min_width, min_tabstrip_width + GetTabStripLeftInset() +
GetTabStripRightInset());
}
int min_height = min_client_view_size.height();
if (browser_view()->IsWindowControlsOverlayEnabled()) {
// Ensure that the minimum height is at least the height of the caption
// button container, which contains the WCO toggle and other windowing
// controls.
min_height = min_height + caption_button_container_->size().height();
}
// Include bottom rounded corners region. See b:294588040.
aura::Window* window = GetWidget()->GetNativeWindow();
const gfx::RoundedCornersF window_radii =
ash::WindowState::Get(window)->GetWindowRoundedCorners();
CHECK_EQ(window_radii.lower_left(), window_radii.lower_right());
min_height = min_height + window_radii.lower_left();
return gfx::Size(min_width, min_height);
}
void BrowserFrameViewChromeOS::OnThemeChanged() {
OnUpdateFrameColor();
caption_button_container_->OnWindowControlsOverlayEnabledChanged(
browser_view()->IsWindowControlsOverlayEnabled(),
GetFrameHeaderColor(browser_view()->IsActive()));
BrowserFrameView::OnThemeChanged();
MaybeAnimateThemeChanged();
}
void BrowserFrameViewChromeOS::ChildPreferredSizeChanged(views::View* child) {
if (browser_view()->initialized()) {
InvalidateLayout();
browser_widget()->GetRootView()->DeprecatedLayoutImmediately();
}
}
bool BrowserFrameViewChromeOS::DoesIntersectRect(const views::View* target,
const gfx::Rect& rect) const {
DCHECK_EQ(target, this);
if (!views::ViewTargeterDelegate::DoesIntersectRect(this, rect)) {
// |rect| is outside the frame's bounds.
return false;
}
bool should_leave_to_top_container = false;
// In immersive mode, the caption buttons container is reparented to the
// TopContainerView and hence |rect| should not be claimed here. See
// BrowserFrameViewChromeOS::OnImmersiveRevealStarted().
should_leave_to_top_container =
browser_view()->immersive_mode_controller()->IsRevealed();
return !should_leave_to_top_container;
}
views::View::Views BrowserFrameViewChromeOS::GetChildrenInZOrder() {
if (ShouldDrawFrameHeader(browser_view()->browser()) && frame_header_) {
return frame_header_->GetAdjustedChildrenInZOrder(this);
}
return BrowserFrameView::GetChildrenInZOrder();
}
SkColor BrowserFrameViewChromeOS::GetTitleColor() {
return GetColorProvider()->GetColor(kColorCaptionForeground);
}
SkColor BrowserFrameViewChromeOS::GetFrameHeaderColor(bool active) {
return GetFrameColor(active ? BrowserFrameActiveState::kActive
: BrowserFrameActiveState::kInactive);
}
gfx::ImageSkia BrowserFrameViewChromeOS::GetFrameHeaderImage(bool active) {
return GetFrameImage(active ? BrowserFrameActiveState::kActive
: BrowserFrameActiveState::kInactive);
}
int BrowserFrameViewChromeOS::GetFrameHeaderImageYInset() {
return browser_view()->GetThemeOffsetFromBrowserView().y();
}
gfx::ImageSkia BrowserFrameViewChromeOS::GetFrameHeaderOverlayImage(
bool active) {
return GetFrameOverlayImage(active ? BrowserFrameActiveState::kActive
: BrowserFrameActiveState::kInactive);
}
void BrowserFrameViewChromeOS::OnDisplayTabletStateChanged(
display::TabletState state) {
switch (state) {
case display::TabletState::kInTabletMode:
OnTabletModeToggled(true);
return;
case display::TabletState::kInClamshellMode:
OnTabletModeToggled(false);
return;
case display::TabletState::kEnteringTabletMode:
case display::TabletState::kExitingTabletMode:
break;
}
}
void BrowserFrameViewChromeOS::OnDisplayMetricsChanged(
const display::Display& display,
uint32_t changed_metrics) {
// When the display is rotated, the frame header may have invalid snap icons.
// For example, rotating from landscape display to portrait display layout
// should update snap icons from left/right arrows to upward/downward arrows
// for top and bottom snaps.
if ((changed_metrics & DISPLAY_METRIC_ROTATION) && frame_header_) {
frame_header_->InvalidateLayout();
}
}
void BrowserFrameViewChromeOS::OnTabletModeToggled(bool enabled) {
if (!enabled && browser_view()->immersive_mode_controller()->IsRevealed()) {
// Before updating the caption buttons state below (which triggers a
// relayout), we want to move the caption buttons from the
// TopContainerView back to this view.
OnImmersiveRevealEnded();
}
const bool should_show_caption_buttons = GetShowCaptionButtons();
caption_button_container_->SetVisible(should_show_caption_buttons);
caption_button_container_->UpdateCaptionButtonState(true /*=animate*/);
ImmersiveModeController* immersive_mode_controller =
browser_view()->immersive_mode_controller();
const bool was_immersive = immersive_mode_controller->IsEnabled();
// Set the immersive mode to what it should be because an immersive mode may
// be used in non fullscreen state in tablet mode.
immersive_mode_controller->SetEnabled(ShouldEnableImmersiveModeController());
// Do not relayout if immersive mode hasn't changed because the non client
// frame area will not change.
if (was_immersive == immersive_mode_controller->IsEnabled()) {
return;
}
InvalidateLayout();
// Can be null in tests.
if (browser_widget()->client_view()) {
browser_widget()->client_view()->InvalidateLayout();
}
if (browser_widget()->GetRootView()) {
browser_widget()->GetRootView()->DeprecatedLayoutImmediately();
}
}
bool BrowserFrameViewChromeOS::ShouldTabIconViewAnimate() const {
// Web apps use their app icon and shouldn't show a throbber.
if (browser_view()->GetIsWebAppType()) {
return false;
}
// This function is queried during the creation of the window as the
// TabIconView we host is initialized, so we need to null check the selected
// WebContents because in this condition there is not yet a selected tab.
content::WebContents* current_tab = browser_view()->GetActiveWebContents();
return current_tab && current_tab->ShouldShowLoadingUI();
}
ui::ImageModel BrowserFrameViewChromeOS::GetFaviconForTabIconView() {
views::WidgetDelegate* delegate = browser_widget()->widget_delegate();
return delegate ? delegate->GetWindowIcon() : ui::ImageModel();
}
void BrowserFrameViewChromeOS::OnWindowDestroying(aura::Window* window) {
DCHECK(window_observation_.IsObserving());
window_observation_.Reset();
display_observer_.reset();
}
void BrowserFrameViewChromeOS::OnWindowPropertyChanged(aura::Window* window,
const void* key,
intptr_t old) {
// ChromeOS has rounded windows for certain window states. If these states
// changes, we need to update the rounded corners of the frame associate with
// the `window`accordingly.
if (key == chromeos::kWindowHasRoundedCornersKey) {
UpdateWindowRoundedCorners();
}
if (key == aura::client::kShowStateKey) {
bool enter_fullscreen = window->GetProperty(aura::client::kShowStateKey) ==
ui::mojom::WindowShowState::kFullscreen;
bool exit_fullscreen = static_cast<ui::mojom::WindowShowState>(old) ==
ui::mojom::WindowShowState::kFullscreen;
// May have to hide caption buttons while in fullscreen mode, or show them
// when exiting fullscreen.
if (enter_fullscreen || exit_fullscreen) {
ResetWindowControls();
}
// The client view (in particular the tab strip) has different layout in
// restored vs. maximized/fullscreen. Invalidate the layout because the
// window bounds may not have changed. https://crbug.com/1342414
if (browser_widget()->client_view()) {
browser_widget()->client_view()->InvalidateLayout();
}
}
if (key == chromeos::kWindowStateTypeKey) {
// Update window controls when window state changes as whether or not these
// are shown can depend on the window state (e.g. hiding the caption buttons
// in non-immersive full screen mode, see crbug.com/1336470).
ResetWindowControls();
// Update the window controls if we are entering or exiting float state.
const bool is_floated = IsFloated();
const bool was_floated = static_cast<chromeos::WindowStateType>(old) ==
chromeos::WindowStateType::kFloated;
if (frame_header_ && (is_floated != was_floated)) {
frame_header_->OnFloatStateChanged();
}
const bool is_fullscreen = browser_widget()->IsFullscreen();
const bool was_fullscreen = chromeos::IsFullscreenOrPinnedWindowStateType(
static_cast<chromeos::WindowStateType>(old));
// Additionally updates immersive mode when the state is transitioning
// between non fullscreen states such as maximzied, snapped or float. When
// switching to or from the fullscreen states, or the state change between
// fullscreen states (fullscreen <> pinneed), the immersive mode is updated
// in `BrowserView::FullscreenStateChanged`.
if (!is_fullscreen && !was_fullscreen) {
browser_view()->immersive_mode_controller()->SetEnabled(
ShouldEnableImmersiveModeController());
}
return;
}
if (key == chromeos::kIsShowingInOverviewKey) {
OnAddedToOrRemovedFromOverview();
return;
}
if (!frame_header_) {
return;
}
if (key == aura::client::kShowStateKey) {
frame_header_->OnShowStateChanged(
window->GetProperty(aura::client::kShowStateKey));
} else if (key == chromeos::kFrameRestoreLookKey) {
frame_header_->view()->InvalidateLayout();
}
}
void BrowserFrameViewChromeOS::OnImmersiveRevealStarted() {
ResetWindowControls();
// The frame caption buttons use ink drop highlights and flood fill effects.
// They make those buttons paint_to_layer. On immersive mode, the browser's
// TopContainerView is also converted to paint_to_layer (see
// ImmersiveModeControllerAsh::OnImmersiveRevealStarted()). In this mode, the
// TopContainerView is responsible for painting this
// BrowserFrameViewChromeOS (see TopContainerView::PaintChildren()).
// However, BrowserFrameViewChromeOS is a sibling of TopContainerView
// not a child. As a result, when the frame caption buttons are set to
// paint_to_layer as a result of an ink drop effect, they will disappear.
// https://crbug.com/840242. To fix this, we'll make the caption buttons
// temporarily children of the TopContainerView while they're all painting to
// their layers.
auto* container = browser_view()->top_container();
container->AddChildViewAt(caption_button_container_.get(), 0);
container->DeprecatedLayoutImmediately();
}
void BrowserFrameViewChromeOS::OnImmersiveRevealEnded() {
ResetWindowControls();
AddChildViewAt(caption_button_container_.get(), 0);
DeprecatedLayoutImmediately();
}
void BrowserFrameViewChromeOS::OnImmersiveFullscreenExited() {
OnImmersiveRevealEnded();
}
void BrowserFrameViewChromeOS::OnAppUpdate(const apps::AppUpdate& update) {
Browser* browser = browser_view()->browser();
if (!browser->app_controller() ||
browser->app_controller()->app_id() != update.AppId() ||
!caption_button_container_) {
return;
}
caption_button_container_->SetCloseButtonEnabled(
update.AllowClose().value_or(true));
}
void BrowserFrameViewChromeOS::OnAppRegistryCacheWillBeDestroyed(
apps::AppRegistryCache* cache) {
app_registry_cache_observation_.Reset();
}
bool BrowserFrameViewChromeOS::ShouldEnableImmersiveModeController() const {
// Do not support immersive mode in kiosk.
if (chromeos::IsKioskSession()) {
return false;
}
if (IsLockedFullscreen() &&
!GetFrameWindow()->GetProperty(chromeos::kUseImmersiveInTrustedPinned)) {
return false;
}
if (display::Screen::Get()->InTabletMode() &&
(IsSnapped() || browser_widget()->IsMaximized())) {
// Snapped or maximized browser windows that doesn't have tabstrip uses
// immersive frame to hide frame in tablet mode.
return !browser_view()->GetSupportsTabStrip();
}
const auto* fullscreen_controller = browser_view()
->browser()
->GetFeatures()
.exclusive_access_manager()
->fullscreen_controller();
// For other scnarios, use immersive if the browser is in fullscreen, and it
// is NOT requested via extension or HTML API `requestFullscreen()`.
return browser_widget()->IsFullscreen() &&
!fullscreen_controller->IsExtensionFullscreenOrPending() &&
fullscreen_controller->IsFullscreenForBrowser();
}
// static
bool BrowserFrameViewChromeOS::ShouldShowAvatarForTesting(
aura::Window* window) {
return ShouldShowAvatar(window);
}
bool BrowserFrameViewChromeOS::IsLockedFullscreen() const {
return ash::WindowState::Get(browser_widget()->GetNativeWindow())
->IsLockedFullscreen();
}
void BrowserFrameViewChromeOS::PaintAsActiveChanged() {
BrowserFrameView::PaintAsActiveChanged();
UpdateProfileIcons();
if (frame_header_) {
frame_header_->SetPaintAsActive(ShouldPaintAsActive());
}
}
void BrowserFrameViewChromeOS::AddedToWidget() {
if (highlight_border_overlay_ ||
!GetWidget()->GetNativeWindow()->GetProperty(
chromeos::kShouldHaveHighlightBorderOverlay)) {
return;
}
highlight_border_overlay_ = std::make_unique<HighlightBorderOverlay>(
GetWidget(), std::make_unique<ash::WmHighlightBorderOverlayDelegate>());
}
BrowserFrameViewChromeOS::BoundsAndMargins
BrowserFrameViewChromeOS::GetCaptionButtonBounds() const {
return BoundsAndMargins{GetShowCaptionButtonsWhenNotInOverview()
? gfx::RectF(caption_button_container_->bounds())
: gfx::RectF()};
}
bool BrowserFrameViewChromeOS::GetShowCaptionButtons() const {
if (GetOverviewMode()) {
return false;
}
return GetShowCaptionButtonsWhenNotInOverview();
}
bool BrowserFrameViewChromeOS::GetShowCaptionButtonsWhenNotInOverview() const {
// Show the caption buttons if an immersive mode is enabled for trusted pined
// state. This is to show the three dot menu which is a part of caption button
// container, rather than showing buttons. Only relevant for non-web browser
// scenarios.
if (IsLockedFullscreen() &&
GetFrameWindow()->GetProperty(chromeos::kUseImmersiveInTrustedPinned)) {
return true;
}
if (GetHideCaptionButtonsForFullscreen()) {
return false;
}
// Show the caption buttons for packaged apps which support immersive mode.
if (UsePackagedAppHeaderStyle(browser_view()->browser())) {
return true;
}
// Browsers in tablet mode still show their caption buttons in float state,
// even with the webUI tab strip.
if (display::Screen::Get()->InTabletMode()) {
return IsFloated();
}
return !UseWebUITabStrip();
}
int BrowserFrameViewChromeOS::GetToolbarLeftInset() const {
// Include padding on left and right of icon.
return profile_indicator_icon_
? kProfileIndicatorPadding * 2 + profile_indicator_icon_->width()
: 0;
}
int BrowserFrameViewChromeOS::GetTabStripLeftInset() const {
// Include padding on left of icon.
// The tab strip has its own 'padding' to the right of the icon.
return profile_indicator_icon_
? kProfileIndicatorPadding + profile_indicator_icon_->width()
: 0;
}
int BrowserFrameViewChromeOS::GetTabStripRightInset() const {
int inset = 0;
if (GetShowCaptionButtonsWhenNotInOverview()) {
inset += caption_button_container_->GetPreferredSize().width();
}
return inset;
}
bool BrowserFrameViewChromeOS::GetShouldPaint() const {
// Floated windows show their frame as they need to be dragged or hidden.
if (IsFloated()) {
return true;
}
#if BUILDFLAG(ENABLE_WEBUI_TAB_STRIP)
// Normal windows that have a WebUI-based tab strip do not need a browser
// frame as no tab strip is drawn on top of the browser frame.
if (UseWebUITabStrip()) {
return false;
}
#endif // BUILDFLAG(ENABLE_WEBUI_TAB_STRIP)
// We need to paint when the top-of-window views are revealed in immersive
// fullscreen.
ImmersiveModeController* immersive_mode_controller =
browser_view()->immersive_mode_controller();
if (immersive_mode_controller->IsEnabled()) {
return immersive_mode_controller->IsRevealed();
}
return !browser_widget()->IsFullscreen();
}
void BrowserFrameViewChromeOS::OnAddedToOrRemovedFromOverview() {
const bool should_show_caption_buttons = GetShowCaptionButtons();
caption_button_container_->SetVisible(should_show_caption_buttons);
if (browser_view()->GetIsWebAppType()) {
// The WebAppFrameToolbarView is part of the BrowserView, so make sure the
// BrowserView is re-layed out to take into account these changes.
browser_view()->InvalidateLayout();
}
}
std::unique_ptr<chromeos::FrameHeader>
BrowserFrameViewChromeOS::CreateFrameHeader() {
std::unique_ptr<chromeos::FrameHeader> header;
Browser* browser = browser_view()->browser();
if (!UsePackagedAppHeaderStyle(browser)) {
header = std::make_unique<BrowserFrameHeaderChromeOS>(
browser_widget(), this, this, caption_button_container_);
} else {
header = std::make_unique<chromeos::DefaultFrameHeader>(
browser_widget(), this, caption_button_container_);
}
header->SetLeftHeaderView(window_icon_);
return header;
}
void BrowserFrameViewChromeOS::UpdateTopViewInset() {
// In immersive fullscreen mode, the top view inset property should be 0.
const bool immersive =
browser_view()->immersive_mode_controller()->IsEnabled();
const bool tab_strip_visible = browser_view()->GetTabStripVisible();
const int inset = (tab_strip_visible || immersive ||
(AppIsPwaWithBorderlessDisplayMode() &&
browser_view()->IsBorderlessModeEnabled()))
? 0
: GetTopInset(/*restored=*/false);
browser_widget()->GetNativeWindow()->SetProperty(aura::client::kTopViewInset,
inset);
}
bool BrowserFrameViewChromeOS::GetShowProfileIndicatorIcon() const {
// We only show the profile indicator for the teleported browser windows
// between multi-user sessions. Note that you can't teleport an incognito
// window.
Browser* browser = browser_view()->browser();
if (browser->profile()->IsIncognitoProfile()) {
return false;
}
if (browser->is_type_popup()) {
return false;
}
#if BUILDFLAG(ENABLE_WEBUI_TAB_STRIP)
// TODO(http://crbug.com/1059514): This check shouldn't be necessary. Provide
// an appropriate affordance for the profile icon with the webUI tabstrip and
// remove this block.
if (!browser_view()->GetTabStripVisible()) {
return false;
}
#endif // BUILDFLAG(ENABLE_WEBUI_TAB_STRIP)
return ShouldShowAvatar(browser_view()->GetNativeWindow());
}
void BrowserFrameViewChromeOS::UpdateProfileIcons() {
View* root_view = browser_widget()->GetRootView();
if (GetShowProfileIndicatorIcon()) {
bool needs_layout = !profile_indicator_icon_;
if (!profile_indicator_icon_) {
profile_indicator_icon_ =
AddChildView(std::make_unique<ProfileIndicatorIcon>());
}
gfx::Image image(
GetAvatarImageForContext(browser_view()->browser()->profile()));
profile_indicator_icon_->SetSize(image.Size());
profile_indicator_icon_->SetIcon(image);
if (needs_layout && root_view) {
// Adding a child does not invalidate the layout.
InvalidateLayout();
root_view->DeprecatedLayoutImmediately();
}
} else if (profile_indicator_icon_) {
RemoveChildViewT(std::exchange(profile_indicator_icon_, nullptr));
if (root_view) {
root_view->DeprecatedLayoutImmediately();
}
}
}
void BrowserFrameViewChromeOS::UpdateWindowRoundedCorners() {
DCHECK(GetWidget());
aura::Window* window = GetWidget()->GetNativeWindow();
auto* window_state = ash::WindowState::Get(window);
// For certain windows, we do not window state associated with them. (See
// `ash::WindowState::Get()` for details)
if (!window_state) {
return;
}
const gfx::RoundedCornersF window_radii =
window_state->GetWindowRoundedCorners();
if (frame_header_) {
CHECK_EQ(window_radii.upper_left(), window_radii.upper_right());
frame_header_->SetHeaderCornerRadius(window_radii.upper_left());
}
if (browser_view()->IsWindowControlsOverlayEnabled()) {
// With window controls overlay enabled, the caption_button_container is
// drawn above the client view. The container has a background that extends
// over the curvature of the top-right corner, requiring its rounding.
caption_button_container_->layer()->SetRoundedCornerRadius(
gfx::RoundedCornersF(0, window_radii.upper_right(), 0, 0));
caption_button_container_->layer()->SetIsFastRoundedCorner(/*enable=*/true);
}
GetWidget()->client_view()->UpdateWindowRoundedCorners(window_radii);
}
void BrowserFrameViewChromeOS::LayoutProfileIndicator() {
DCHECK(profile_indicator_icon_);
const int frame_height =
GetTopInset(false) + browser_view()->GetTabStripHeight();
profile_indicator_icon_->SetPosition(
gfx::Point(kProfileIndicatorPadding,
(frame_height - profile_indicator_icon_->height()) / 2));
profile_indicator_icon_->SetVisible(true);
// The layout size is set along with the image.
DCHECK_LE(profile_indicator_icon_->height(), frame_height);
}
bool BrowserFrameViewChromeOS::GetOverviewMode() const {
return GetFrameWindow()->GetProperty(chromeos::kIsShowingInOverviewKey);
}
bool BrowserFrameViewChromeOS::GetHideCaptionButtonsForFullscreen() const {
if (!browser_widget()->IsFullscreen()) {
return false;
}
auto* immersive_controller = browser_view()->immersive_mode_controller();
// In fullscreen view, but not in immersive mode. Hide the caption buttons.
if (!immersive_controller || !immersive_controller->IsEnabled()) {
return true;
}
return immersive_controller->ShouldHideTopViews();
}
void BrowserFrameViewChromeOS::OnUpdateFrameColor() {
aura::Window* window = browser_widget()->GetNativeWindow();
window->SetProperty(chromeos::kFrameActiveColorKey,
GetFrameColor(BrowserFrameActiveState::kActive));
window->SetProperty(chromeos::kFrameInactiveColorKey,
GetFrameColor(BrowserFrameActiveState::kInactive));
if (frame_header_) {
frame_header_->UpdateFrameColors();
}
}
void BrowserFrameViewChromeOS::MaybeAnimateThemeChanged() {
if (!browser_view()) {
return;
}
Browser* browser = browser_view()->browser();
// Theme change events are only animated for system web apps which explicitly
// request the behavior.
bool animate_theme_change_for_swa =
ash::IsSystemWebApp(browser) &&
browser->app_controller()->system_app()->ShouldAnimateThemeChanges();
if (!animate_theme_change_for_swa) {
return;
}
views::WebView* web_view = browser_view()->contents_web_view();
ui::Layer* layer = GetNativeViewLayer(web_view);
content::RenderWidgetHost* render_widget_host = GetRenderWidgetHost(web_view);
if (!layer || !render_widget_host) {
return;
}
// Immediately hide the layer associated with the `contents_web_view()` native
// view so that repainting of the web contents (which is janky) is hidden from
// user. Note that opacity is set just above `0.f` to pass a DCHECK that
// exists in `aura::Window` that might otherwise be tripped when changing
// window visibility (see https://crbug.com/351553).
layer->SetOpacity(std::nextafter(0.f, 1.f));
// Cache a callback to invoke to animate the layer back in. Note that because
// this is a cancelable callback, any previously created callback will be
// cancelled.
theme_changed_animation_callback_.Reset(base::BindOnce(
[](const base::WeakPtr<BrowserFrameViewChromeOS>& self,
base::TimeTicks theme_changed_time, bool success) {
if (!self || !self->browser_view()) {
return;
}
views::WebView* web_view = self->browser_view()->contents_web_view();
ui::Layer* layer = GetNativeViewLayer(web_view);
if (!layer) {
return;
}
// Delay animating the layer back in at least until the
// `chromeos::DefaultFrameHeader` has had a chance to complete its own
// color change animation.
const base::TimeDelta offset =
chromeos::kDefaultFrameColorChangeAnimationDuration -
(base::TimeTicks::Now() - theme_changed_time);
views::AnimationBuilder()
.SetPreemptionStrategy(ui::LayerAnimator::PreemptionStrategy::
IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
.Once()
.Offset(std::max(offset, base::TimeDelta()))
.SetDuration(chromeos::kDefaultFrameColorChangeAnimationDuration)
.SetOpacity(layer, 1.f);
},
weak_ptr_factory_.GetWeakPtr(), base::TimeTicks::Now()));
// Animate the layer back in only after a round trip through the renderer and
// compositor pipelines. This should ensure that the web contents has finished
// repainting theme changes.
render_widget_host->InsertVisualStateCallback(
theme_changed_animation_callback_.callback());
}
bool BrowserFrameViewChromeOS::IsFloated() const {
return ash::WindowState::Get(browser_widget()->GetNativeWindow())
->IsFloated();
}
bool BrowserFrameViewChromeOS::IsSnapped() const {
return ash::WindowState::Get(browser_widget()->GetNativeWindow())
->IsSnapped();
}
bool BrowserFrameViewChromeOS::UseWebUITabStrip() const {
return WebUITabStripContainerView::UseTouchableTabStrip(
browser_view()->browser()) &&
browser_view()->GetSupportsTabStrip();
}
const aura::Window* BrowserFrameViewChromeOS::GetFrameWindow() const {
return const_cast<BrowserFrameViewChromeOS*>(this)->GetFrameWindow();
}
aura::Window* BrowserFrameViewChromeOS::GetFrameWindow() {
return browser_widget()->GetNativeWindow();
}
BEGIN_METADATA(BrowserFrameViewChromeOS)
ADD_READONLY_PROPERTY_METADATA(bool, ShowCaptionButtons)
ADD_READONLY_PROPERTY_METADATA(bool, ShowCaptionButtonsWhenNotInOverview)
ADD_READONLY_PROPERTY_METADATA(int, ToolbarLeftInset)
ADD_READONLY_PROPERTY_METADATA(int, TabStripLeftInset)
ADD_READONLY_PROPERTY_METADATA(int, TabStripRightInset)
ADD_READONLY_PROPERTY_METADATA(bool, ShouldPaint)
ADD_READONLY_PROPERTY_METADATA(bool, ShowProfileIndicatorIcon)
ADD_READONLY_PROPERTY_METADATA(bool, OverviewMode)
END_METADATA