blob: 4ebce1d11bc26d648151696eada52cf7d75373cf [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_non_client_frame_view_chromeos.h"
#include <algorithm>
#include "base/containers/cxx20_erase.h"
#include "base/metrics/user_metrics.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/ash/system_web_apps/types/system_web_app_delegate.h"
#include "chrome/browser/platform_util.h"
#include "chrome/browser/profiles/profiles_state.h"
#include "chrome/browser/themes/theme_properties.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/color/chrome_color_id.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/views/frame/browser_frame.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/frame/immersive_mode_controller.h"
#include "chrome/browser/ui/views/frame/tab_search_frame_caption_button.h"
#include "chrome/browser/ui/views/frame/tab_strip_region_view.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/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/views/web_apps/frame_toolbar/web_app_frame_toolbar_view.h"
#include "chrome/browser/ui/web_applications/app_browser_controller.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/ui/base/chromeos_ui_constants.h"
#include "chromeos/ui/base/tablet_state.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 "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_enums.mojom.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/layout.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/image_model.h"
#include "ui/base/ui_base_types.h"
#include "ui/chromeos/styles/cros_styles.h"
#include "ui/events/gestures/gesture_recognizer.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/scoped_canvas.h"
#include "ui/views/animation/animation_builder.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/rect_based_targeting_utils.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)
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "ash/constants/ash_features.h"
#include "ash/wm/window_util.h"
#include "chrome/browser/ui/ash/multi_user/multi_user_window_manager_helper.h"
#include "chrome/browser/ui/ash/session_util.h"
#include "chrome/browser/ui/ash/system_web_apps/system_web_app_ui_utils.h"
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
#if BUILDFLAG(IS_CHROMEOS_LACROS)
#include "chromeos/ui/frame/interior_resize_handler_targeter.h"
#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
namespace {
// The indicator for teleported windows has 8 DIPs before and below it.
constexpr int kProfileIndicatorPadding = 8;
#if BUILDFLAG(IS_CHROMEOS_ASH)
// 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;
}
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
// 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);
}
} // namespace
BrowserNonClientFrameViewChromeOS::BrowserNonClientFrameViewChromeOS(
BrowserFrame* frame,
BrowserView* browser_view)
: BrowserNonClientFrameView(frame, browser_view) {
#if BUILDFLAG(IS_CHROMEOS_ASH)
ash::window_util::InstallResizeHandleWindowTargeterForWindow(
frame->GetNativeWindow());
#endif
#if BUILDFLAG(IS_CHROMEOS_LACROS)
frame->GetNativeWindow()->SetEventTargeter(
std::make_unique<chromeos::InteriorResizeHandleTargeter>());
#endif
}
BrowserNonClientFrameViewChromeOS::~BrowserNonClientFrameViewChromeOS() {
ImmersiveModeController* immersive_controller =
browser_view()->immersive_mode_controller();
if (immersive_controller)
immersive_controller->RemoveObserver(this);
}
void BrowserNonClientFrameViewChromeOS::Init() {
Browser* browser = browser_view()->browser();
std::unique_ptr<TabSearchFrameCaptionButton> tab_search_button;
if (TabSearchFrameCaptionButton::IsTabSearchCaptionButtonEnabled(browser)) {
tab_search_button =
std::make_unique<TabSearchFrameCaptionButton>(browser->profile());
tab_search_bubble_host_ = tab_search_button->tab_search_bubble_host();
}
caption_button_container_ =
AddChildView(std::make_unique<chromeos::FrameCaptionButtonContainerView>(
frame(), std::move(tab_search_button)));
caption_button_container_->UpdateCaptionButtonState(false /*=animate*/);
// 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());
// To preserve privacy, tag incognito windows so that they won't be included
// in screenshot sent to assistant server.
if (browser->profile()->IsOffTheRecord()) {
frame()->GetNativeWindow()->SetProperty(
chromeos::kBlockedForAssistantSnapshotKey, true);
}
display_observer_.emplace(this);
if (frame()->ShouldDrawFrameHeader())
frame_header_ = CreateFrameHeader();
if (browser_view()->GetIsWebAppType() &&
(!browser->is_type_app_popup() ||
browser_view()->AppUsesWindowControlsOverlay() ||
browser_view()->AppUsesBorderlessMode())) {
// Add the container for extra web app buttons (e.g app menu button).
set_web_app_frame_toolbar(
AddChildView(std::make_unique<WebAppFrameToolbarView>(browser_view())));
if (AppIsBorderlessPwa())
UpdateBorderlessModeEnabled();
}
browser_view()->immersive_mode_controller()->AddObserver(this);
}
gfx::Rect BrowserNonClientFrameViewChromeOS::GetBoundsForTabStripRegion(
const gfx::Size& tabstrip_minimum_size) const {
const int left_inset = GetTabStripLeftInset();
const bool restored = !frame()->IsMaximized() && !frame()->IsFullscreen();
return gfx::Rect(left_inset, GetTopInset(restored),
std::max(0, width() - left_inset - GetTabStripRightInset()),
tabstrip_minimum_size.height());
}
int BrowserNonClientFrameViewChromeOS::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() || frame()->IsFullscreen() ||
browser_view()->GetTabStripVisible() ||
browser_view()->webui_tab_strip()) {
return 0;
}
}
Browser* browser = browser_view()->browser();
int header_height = frame_header_ ? frame_header_->GetHeaderHeight() : 0;
if (web_app_frame_toolbar()) {
header_height = std::max(
header_height, web_app_frame_toolbar()->GetPreferredSize().height());
}
if (browser_view()->GetTabStripVisible())
return header_height - browser_view()->GetTabStripHeight();
return UsePackagedAppHeaderStyle(browser)
? header_height
: caption_button_container_->bounds().bottom();
}
int BrowserNonClientFrameViewChromeOS::GetThemeBackgroundXInset() const {
return BrowserFrameHeaderChromeOS::GetThemeBackgroundXInset();
}
void BrowserNonClientFrameViewChromeOS::UpdateFrameColor() {
OnUpdateFrameColor();
BrowserNonClientFrameView::UpdateFrameColor();
}
void BrowserNonClientFrameViewChromeOS::UpdateThrobber(bool running) {
if (window_icon_)
window_icon_->Update();
}
bool BrowserNonClientFrameViewChromeOS::CanUserExitFullscreen() const {
return !platform_util::IsBrowserLockedFullscreen(browser_view()->browser());
}
SkColor BrowserNonClientFrameViewChromeOS::GetCaptionColor(
BrowserFrameActiveState active_state) const {
// Web apps apply a theme color if specified by the extension/manifest.
absl::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 (ShouldPaintAsActive(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 BrowserNonClientFrameViewChromeOS::GetFrameColor(
BrowserFrameActiveState active_state) const {
if (!UsePackagedAppHeaderStyle(browser_view()->browser()))
return BrowserNonClientFrameView::GetFrameColor(active_state);
absl::optional<SkColor> color;
if (browser_view()->GetIsWebAppType())
color = browser_view()->browser()->app_controller()->GetThemeColor();
SkColor fallback_color = chromeos::kDefaultFrameColor;
if (chromeos::features::IsDarkLightModeEnabled() && GetWidget()) {
// TODO(skau): Migrate to ColorProvider.
fallback_color =
cros_styles::ResolveColor(cros_styles::ColorName::kBgColor,
GetNativeTheme()->ShouldUseDarkColors());
}
return color.value_or(fallback_color);
}
TabSearchBubbleHost*
BrowserNonClientFrameViewChromeOS::GetTabSearchBubbleHost() {
return tab_search_bubble_host_;
}
void BrowserNonClientFrameViewChromeOS::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 BrowserNonClientFrameViewChromeOS::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
// BrowserNonClientFrameViewChromeOS.
return bounds();
}
gfx::Rect BrowserNonClientFrameViewChromeOS::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 BrowserNonClientFrameViewChromeOS::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 && !frame()->IsMaximized() &&
!frame()->IsFullscreen() &&
!chromeos::TabletState::Get()->InTabletMode()) {
// TODO(crbug.com/1213133): Tab Strip hit calculation and bounds logic
// should reside in the TabStrip class.
gfx::Point client_point(point);
View::ConvertPointToTarget(this, frame()->client_view(), &client_point);
gfx::Rect tabstrip_shadow_bounds(browser_view()->tabstrip()->bounds());
constexpr int kTabShadowHeight = 4;
tabstrip_shadow_bounds.set_height(kTabShadowHeight);
if (tabstrip_shadow_bounds.Contains(client_point))
return HTCAPTION;
}
return hit_test;
}
void BrowserNonClientFrameViewChromeOS::GetWindowMask(const gfx::Size& size,
SkPath* window_mask) {
#if BUILDFLAG(IS_CHROMEOS_LACROS)
// The opaque region of surface should be set exactly same as the frame header
// path in BrowserFrameHeader.
if (frame()->ShouldDrawFrameHeader())
*window_mask = frame_header_->GetWindowMaskForFrameHeader(size);
#endif
}
void BrowserNonClientFrameViewChromeOS::ResetWindowControls() {
BrowserNonClientFrameView::ResetWindowControls();
caption_button_container_->SetVisible(GetShowCaptionButtons());
caption_button_container_->ResetWindowControls();
}
void BrowserNonClientFrameViewChromeOS::WindowControlsOverlayEnabledChanged() {
bool enabled = browser_view()->IsWindowControlsOverlayEnabled();
web_app_frame_toolbar()->OnWindowControlsOverlayEnabledChanged();
caption_button_container_->OnWindowControlsOverlayEnabledChanged(
enabled, GetFrameHeaderColor(browser_view()->IsActive()));
browser_view()->InvalidateLayout();
}
void BrowserNonClientFrameViewChromeOS::UpdateWindowIcon() {
if (window_icon_)
window_icon_->SchedulePaint();
}
void BrowserNonClientFrameViewChromeOS::UpdateWindowTitle() {
if (!frame()->IsFullscreen() && frame_header_)
frame_header_->SchedulePaintForTitle();
frame()->GetNativeWindow()->SetProperty(
chromeos::kWindowOverviewTitleKey,
browser_view()->browser()->GetWindowTitleForCurrentTab(
/*include_app_name=*/false));
}
void BrowserNonClientFrameViewChromeOS::SizeConstraintsChanged() {}
void BrowserNonClientFrameViewChromeOS::OnPaint(gfx::Canvas* canvas) {
if (!GetShouldPaint())
return;
if (frame_header_)
frame_header_->PaintHeader(canvas);
}
void BrowserNonClientFrameViewChromeOS::LayoutWindowControlsOverlay() {
int overlay_height = caption_button_container_->size().height();
gfx::Rect available_space(caption_button_container_->x(), overlay_height);
web_app_frame_toolbar()->LayoutForWindowControlsOverlay(available_space);
content::WebContents* web_contents = browser_view()->GetActiveWebContents();
// WebContents can be null when an app window is first launched.
if (web_contents) {
int overlay_width = web_app_frame_toolbar()->size().width() +
caption_button_container_->size().width();
int bounding_rect_width = width() - overlay_width;
auto bounding_rect =
GetMirroredRect(gfx::Rect(bounding_rect_width, overlay_height));
web_contents->UpdateWindowControlsOverlay(bounding_rect);
}
}
void BrowserNonClientFrameViewChromeOS::UpdateBorderlessModeEnabled() {
web_app_frame_toolbar()->UpdateBorderlessModeEnabled();
caption_button_container_->UpdateBorderlessModeEnabled(
browser_view()->IsBorderlessModeEnabled());
}
bool BrowserNonClientFrameViewChromeOS::AppIsBorderlessPwa() {
return browser_view()->GetIsWebAppType() &&
browser_view()->AppUsesBorderlessMode() &&
browser_view()->IsBorderlessModeEnabled();
}
void BrowserNonClientFrameViewChromeOS::Layout() {
// 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()->tabstrip()->GetPreferredSize().height();
if (frame_header_)
frame_header_->SetHeaderHeightForPainting(painted_height);
if (profile_indicator_icon_)
LayoutProfileIndicator();
if (web_app_frame_toolbar()) {
if (browser_view()->IsWindowControlsOverlayEnabled()) {
LayoutWindowControlsOverlay();
} else if (AppIsBorderlessPwa()) {
UpdateBorderlessModeEnabled();
} else {
web_app_frame_toolbar()->LayoutInContainer(GetToolbarLeftInset(),
caption_button_container_->x(),
0, painted_height);
}
}
BrowserNonClientFrameView::Layout();
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());
}
}
void BrowserNonClientFrameViewChromeOS::GetAccessibleNodeData(
ui::AXNodeData* node_data) {
node_data->role = ax::mojom::Role::kTitleBar;
}
gfx::Size BrowserNonClientFrameViewChromeOS::GetMinimumSize() const {
#if BUILDFLAG(IS_CHROMEOS_ASH)
// 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;
}
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
gfx::Size min_client_view_size(frame()->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_region_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();
}
return gfx::Size(min_width, min_height);
}
void BrowserNonClientFrameViewChromeOS::OnThemeChanged() {
OnUpdateFrameColor();
caption_button_container_->OnWindowControlsOverlayEnabledChanged(
browser_view()->IsWindowControlsOverlayEnabled(),
GetFrameHeaderColor(browser_view()->IsActive()));
BrowserNonClientFrameView::OnThemeChanged();
MaybeAnimateThemeChanged();
}
void BrowserNonClientFrameViewChromeOS::ChildPreferredSizeChanged(
views::View* child) {
if (browser_view()->initialized()) {
InvalidateLayout();
frame()->GetRootView()->Layout();
}
}
bool BrowserNonClientFrameViewChromeOS::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;
#if BUILDFLAG(IS_CHROMEOS)
// In immersive mode, the caption buttons container is reparented to the
// TopContainerView and hence |rect| should not be claimed here. See
// BrowserNonClientFrameViewChromeOS::OnImmersiveRevealStarted().
should_leave_to_top_container =
browser_view()->immersive_mode_controller()->IsRevealed();
#endif
return !should_leave_to_top_container;
}
views::View::Views BrowserNonClientFrameViewChromeOS::GetChildrenInZOrder() {
if (frame()->ShouldDrawFrameHeader() && frame_header_)
return frame_header_->GetAdjustedChildrenInZOrder(this);
return BrowserNonClientFrameView::GetChildrenInZOrder();
}
SkColor BrowserNonClientFrameViewChromeOS::GetTitleColor() {
return GetColorProvider()->GetColor(kColorCaptionForeground);
}
SkColor BrowserNonClientFrameViewChromeOS::GetFrameHeaderColor(bool active) {
return GetFrameColor(active ? BrowserFrameActiveState::kActive
: BrowserFrameActiveState::kInactive);
}
gfx::ImageSkia BrowserNonClientFrameViewChromeOS::GetFrameHeaderImage(
bool active) {
return GetFrameImage(active ? BrowserFrameActiveState::kActive
: BrowserFrameActiveState::kInactive);
}
int BrowserNonClientFrameViewChromeOS::GetFrameHeaderImageYInset() {
return ThemeProperties::kFrameHeightAboveTabs - GetTopInset(false);
}
gfx::ImageSkia BrowserNonClientFrameViewChromeOS::GetFrameHeaderOverlayImage(
bool active) {
return GetFrameOverlayImage(active ? BrowserFrameActiveState::kActive
: BrowserFrameActiveState::kInactive);
}
void BrowserNonClientFrameViewChromeOS::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 BrowserNonClientFrameViewChromeOS::OnDisplayMetricsChanged(
const display::Display& display,
uint32_t changed_metrics) {
// When the display is rotated, the frame header may have invalid snap icons.
// For example, when |features::kVerticalSnapState| is enabled, 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 BrowserNonClientFrameViewChromeOS::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*/);
if (web_app_frame_toolbar())
web_app_frame_toolbar()->SetVisible(should_show_caption_buttons);
ImmersiveModeController* immersive_mode_controller =
browser_view()->immersive_mode_controller();
const bool was_enabled = immersive_mode_controller->IsEnabled();
immersive_mode_controller->SetEnabled(ShouldEnableImmersiveModeController());
// Do not relayout if immersive mode has not changed.
if (was_enabled == immersive_mode_controller->IsEnabled())
return;
InvalidateLayout();
// Can be null in tests.
if (frame()->client_view())
frame()->client_view()->InvalidateLayout();
if (frame()->GetRootView())
frame()->GetRootView()->Layout();
}
bool BrowserNonClientFrameViewChromeOS::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->IsLoading();
}
ui::ImageModel BrowserNonClientFrameViewChromeOS::GetFaviconForTabIconView() {
views::WidgetDelegate* delegate = frame()->widget_delegate();
return delegate ? delegate->GetWindowIcon() : ui::ImageModel();
}
void BrowserNonClientFrameViewChromeOS::OnWindowDestroying(
aura::Window* window) {
DCHECK(window_observation_.IsObserving());
window_observation_.Reset();
}
void BrowserNonClientFrameViewChromeOS::OnWindowPropertyChanged(
aura::Window* window,
const void* key,
intptr_t old) {
if (key == aura::client::kShowStateKey) {
bool enter_fullscreen = window->GetProperty(aura::client::kShowStateKey) ==
ui::SHOW_STATE_FULLSCREEN;
bool exit_fullscreen =
static_cast<ui::WindowShowState>(old) == ui::SHOW_STATE_FULLSCREEN;
// 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 (frame()->client_view())
frame()->client_view()->InvalidateLayout();
}
if (key == chromeos::kWindowStateTypeKey) {
// Update the window controls if we are entering or exiting float state.
const bool enter_floated = IsFloated();
const bool exit_floated = static_cast<chromeos::WindowStateType>(old) ==
chromeos::WindowStateType::kFloated;
if (!enter_floated && !exit_floated)
return;
if (frame_header_)
frame_header_->OnFloatStateChanged();
ResetWindowControls();
if (!chromeos::TabletState::Get()->InTabletMode())
return;
// Additionally updates immersive mode for PWA/SWA so that we show the title
// bar when floated, and hide the title bar otherwise.
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 BrowserNonClientFrameViewChromeOS::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
// BrowserNonClientFrameViewChromeOS (see TopContainerView::PaintChildren()).
// However, BrowserNonClientFrameViewChromeOS 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);
if (web_app_frame_toolbar())
container->AddChildViewAt(web_app_frame_toolbar(), 0);
container->Layout();
#if BUILDFLAG(IS_CHROMEOS_LACROS)
// In Lacros, when entering in immersive fullscreen, it is possible
// that chromeos::FrameHeader::painted_height_ is set to '0', when
// Layout() is called. This is because the tapstrip gets hidden.
//
// When it happens, PaintFrameImagesInRoundRect() has an empty rect
// to paint onto, and the TabStrip's new theme is not painted.
if (frame_header_ && frame_header_->GetHeaderHeightForPainting() == 0)
frame_header_->LayoutHeader();
#endif
}
void BrowserNonClientFrameViewChromeOS::OnImmersiveRevealEnded() {
ResetWindowControls();
AddChildViewAt(caption_button_container_.get(), 0);
if (web_app_frame_toolbar()) {
views::ClientView* client_view =
GetWidget() ? GetWidget()->client_view() : nullptr;
// Add the web app frame toolbar at the end, but before the client view if
// it exists.
absl::optional<size_t> index;
if (client_view)
index = GetIndexOf(client_view);
if (index.has_value())
AddChildViewAt(web_app_frame_toolbar(), index.value());
else
AddChildView(web_app_frame_toolbar());
}
Layout();
}
void BrowserNonClientFrameViewChromeOS::OnImmersiveFullscreenExited() {
OnImmersiveRevealEnded();
}
void BrowserNonClientFrameViewChromeOS::PaintAsActiveChanged() {
BrowserNonClientFrameView::PaintAsActiveChanged();
UpdateProfileIcons();
if (frame_header_)
frame_header_->SetPaintAsActive(ShouldPaintAsActive());
}
void BrowserNonClientFrameViewChromeOS::OnProfileAvatarChanged(
const base::FilePath& profile_path) {
BrowserNonClientFrameView::OnProfileAvatarChanged(profile_path);
UpdateProfileIcons();
}
void BrowserNonClientFrameViewChromeOS::AddedToWidget() {
if (!chromeos::features::IsDarkLightModeEnabled())
return;
if (highlight_border_overlay_ ||
!GetWidget()->GetNativeWindow()->GetProperty(
chromeos::kShouldHaveHighlightBorderOverlay)) {
return;
}
highlight_border_overlay_ =
std::make_unique<HighlightBorderOverlay>(GetWidget());
}
bool BrowserNonClientFrameViewChromeOS::GetShowCaptionButtons() const {
return GetShowCaptionButtonsWhenNotInOverview() && !GetOverviewMode() &&
!GetHideCaptionButtonsForFullscreen() && !UseWebUITabStrip();
}
bool BrowserNonClientFrameViewChromeOS::GetShowCaptionButtonsWhenNotInOverview()
const {
if (UsePackagedAppHeaderStyle(browser_view()->browser()))
return true;
if (!chromeos::TabletState::Get()->InTabletMode())
return true;
return IsFloated();
}
int BrowserNonClientFrameViewChromeOS::GetToolbarLeftInset() const {
// Include padding on left and right of icon.
return profile_indicator_icon_
? kProfileIndicatorPadding * 2 + profile_indicator_icon_->width()
: 0;
}
int BrowserNonClientFrameViewChromeOS::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 BrowserNonClientFrameViewChromeOS::GetTabStripRightInset() const {
int inset = 0;
if (GetShowCaptionButtonsWhenNotInOverview())
inset += caption_button_container_->GetPreferredSize().width();
if (web_app_frame_toolbar())
inset += web_app_frame_toolbar()->GetPreferredSize().width();
return inset;
}
bool BrowserNonClientFrameViewChromeOS::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 !frame()->IsFullscreen();
}
void BrowserNonClientFrameViewChromeOS::OnAddedToOrRemovedFromOverview() {
const bool should_show_caption_buttons = GetShowCaptionButtons();
caption_button_container_->SetVisible(should_show_caption_buttons);
if (web_app_frame_toolbar())
web_app_frame_toolbar()->SetVisible(should_show_caption_buttons);
}
std::unique_ptr<chromeos::FrameHeader>
BrowserNonClientFrameViewChromeOS::CreateFrameHeader() {
std::unique_ptr<chromeos::FrameHeader> header;
Browser* browser = browser_view()->browser();
if (!UsePackagedAppHeaderStyle(browser)) {
header = std::make_unique<BrowserFrameHeaderChromeOS>(
frame(), this, this, caption_button_container_);
} else {
header = std::make_unique<chromeos::DefaultFrameHeader>(
frame(), this, caption_button_container_);
}
header->SetLeftHeaderView(window_icon_);
return header;
}
void BrowserNonClientFrameViewChromeOS::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 || AppIsBorderlessPwa())
? 0
: GetTopInset(/*restored=*/false);
frame()->GetNativeWindow()->SetProperty(aura::client::kTopViewInset, inset);
}
bool BrowserNonClientFrameViewChromeOS::GetShowProfileIndicatorIcon() const {
#if BUILDFLAG(IS_CHROMEOS_ASH)
// 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 MultiUserWindowManagerHelper::ShouldShowAvatar(
browser_view()->GetNativeWindow());
#else
// Multi-signin support is deprecated in Lacros.
return false;
#endif
}
void BrowserNonClientFrameViewChromeOS::UpdateProfileIcons() {
// Multi-signin support is deprecated in Lacros, so only do this for ash.
#if BUILDFLAG(IS_CHROMEOS_ASH)
View* root_view = frame()->GetRootView();
if (GetShowProfileIndicatorIcon()) {
bool needs_layout = !profile_indicator_icon_;
if (!profile_indicator_icon_) {
profile_indicator_icon_ = new ProfileIndicatorIcon();
AddChildView(profile_indicator_icon_.get());
}
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->Layout();
}
} else if (profile_indicator_icon_) {
delete profile_indicator_icon_;
profile_indicator_icon_ = nullptr;
if (root_view)
root_view->Layout();
}
#endif
}
void BrowserNonClientFrameViewChromeOS::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 BrowserNonClientFrameViewChromeOS::GetOverviewMode() const {
return GetFrameWindow()->GetProperty(chromeos::kIsShowingInOverviewKey);
}
bool BrowserNonClientFrameViewChromeOS::GetHideCaptionButtonsForFullscreen()
const {
if (!frame()->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 BrowserNonClientFrameViewChromeOS::OnUpdateFrameColor() {
aura::Window* window = frame()->GetNativeWindow();
window->SetProperty(chromeos::kFrameActiveColorKey,
GetFrameColor(BrowserFrameActiveState::kActive));
window->SetProperty(chromeos::kFrameInactiveColorKey,
GetFrameColor(BrowserFrameActiveState::kInactive));
if (frame_header_)
frame_header_->UpdateFrameColors();
}
void BrowserNonClientFrameViewChromeOS::MaybeAnimateThemeChanged() {
#if BUILDFLAG(IS_CHROMEOS_ASH)
if (!browser_view())
return;
Browser* browser = browser_view()->browser();
if (!browser)
return;
// 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<BrowserNonClientFrameViewChromeOS>& 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());
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
}
bool BrowserNonClientFrameViewChromeOS::IsFloated() const {
return GetFrameWindow()->GetProperty(chromeos::kWindowStateTypeKey) ==
chromeos::WindowStateType::kFloated;
}
bool BrowserNonClientFrameViewChromeOS::ShouldEnableImmersiveModeController()
const {
if (chromeos::TabletState::Get()->InTabletMode()) {
// Tabbed browsers do not support immersive mode in tablet mode. We use the
// web ui touchable tabstrip, which has its own sliding mechanism to view
// the tabs.
if (browser_view()->GetSupportsTabStrip())
return false;
// No immersive mode for minimized windows as they aren't visible, and
// floated windows need a permanent header to drag.
if (frame()->IsMinimized() || IsFloated())
return false;
return true;
}
// In clamshell mode, we want immersive mode if fullscreen.
return frame()->IsFullscreen();
}
bool BrowserNonClientFrameViewChromeOS::UseWebUITabStrip() const {
return WebUITabStripContainerView::UseTouchableTabStrip(
browser_view()->browser()) &&
browser_view()->GetSupportsTabStrip();
}
const aura::Window* BrowserNonClientFrameViewChromeOS::GetFrameWindow() const {
return const_cast<BrowserNonClientFrameViewChromeOS*>(this)->GetFrameWindow();
}
aura::Window* BrowserNonClientFrameViewChromeOS::GetFrameWindow() {
return frame()->GetNativeWindow();
}
BEGIN_METADATA(BrowserNonClientFrameViewChromeOS, BrowserNonClientFrameView)
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