blob: 3d460ae1ca8cace58163233b6f0ce7f0dcf29f02 [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// 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_mac.h"
#include "base/bind.h"
#include "base/command_line.h"
#include "base/metrics/histogram_macros.h"
#include "base/numerics/safe_conversions.h"
#include "chrome/browser/themes/theme_properties.h"
#include "chrome/browser/themes/theme_service.h"
#include "chrome/browser/themes/theme_service_factory.h"
#include "chrome/browser/ui/cocoa/fullscreen/fullscreen_menubar_tracker.h"
#include "chrome/browser/ui/cocoa/fullscreen/fullscreen_toolbar_controller_views.h"
#include "chrome/browser/ui/exclusive_access/fullscreen_controller.h"
#include "chrome/browser/ui/extensions/hosted_app_browser_controller.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/views/bookmarks/bookmark_bar_view.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/browser_view_layout.h"
#include "chrome/browser/ui/views/frame/hosted_app_button_container.h"
#include "chrome/browser/ui/views/tabs/tab_strip.h"
#include "chrome/browser/ui/views/toolbar/toolbar_view.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/pref_names.h"
#include "components/prefs/pref_service.h"
#include "ui/base/hit_test.h"
#include "ui/base/theme_provider.h"
#include "ui/gfx/canvas.h"
namespace {
constexpr int kHostedAppMenuMargin = 7;
constexpr int kFramePaddingLeft = 75;
constexpr double kTitlePaddingWidthFraction = 0.1;
FullscreenToolbarStyle GetUserPreferredToolbarStyle(bool always_show) {
// In Kiosk mode, we don't show top Chrome UI.
if (base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kKioskMode))
return FullscreenToolbarStyle::TOOLBAR_NONE;
return always_show ? FullscreenToolbarStyle::TOOLBAR_PRESENT
: FullscreenToolbarStyle::TOOLBAR_HIDDEN;
}
} // namespace
///////////////////////////////////////////////////////////////////////////////
// BrowserNonClientFrameViewMac, public:
BrowserNonClientFrameViewMac::BrowserNonClientFrameViewMac(
BrowserFrame* frame,
BrowserView* browser_view)
: BrowserNonClientFrameView(frame, browser_view) {
show_fullscreen_toolbar_.Init(
prefs::kShowFullscreenToolbar, browser_view->GetProfile()->GetPrefs(),
base::BindRepeating(&BrowserNonClientFrameViewMac::UpdateFullscreenTopUI,
base::Unretained(this), true));
if (!base::FeatureList::IsEnabled(features::kImmersiveFullscreen)) {
fullscreen_toolbar_controller_.reset(
[[FullscreenToolbarControllerViews alloc]
initWithBrowserView:browser_view]);
[fullscreen_toolbar_controller_
setToolbarStyle:GetUserPreferredToolbarStyle(
*show_fullscreen_toolbar_)];
}
if (browser_view->IsBrowserTypeHostedApp()) {
if (browser_view->browser()
->app_controller()
->ShouldShowHostedAppButtonContainer()) {
set_hosted_app_button_container(new HostedAppButtonContainer(
frame, browser_view, GetCaptionColor(kActive),
GetCaptionColor(kInactive), kHostedAppMenuMargin));
AddChildView(hosted_app_button_container());
}
DCHECK(browser_view->ShouldShowWindowTitle());
window_title_ = new views::Label(browser_view->GetWindowTitle());
AddChildView(window_title_);
}
}
BrowserNonClientFrameViewMac::~BrowserNonClientFrameViewMac() {
if ([fullscreen_toolbar_controller_ isInFullscreen])
[fullscreen_toolbar_controller_ exitFullscreenMode];
}
///////////////////////////////////////////////////////////////////////////////
// BrowserNonClientFrameViewMac, BrowserNonClientFrameView implementation:
void BrowserNonClientFrameViewMac::OnFullscreenStateChanged() {
if (base::FeatureList::IsEnabled(features::kImmersiveFullscreen)) {
browser_view()->immersive_mode_controller()->SetEnabled(
browser_view()->IsFullscreen());
return;
}
if (browser_view()->IsFullscreen()) {
[fullscreen_toolbar_controller_ enterFullscreenMode];
} else {
// Exiting tab fullscreen requires updating Top UI.
// Called from here so we can capture exiting tab fullscreen both by
// pressing 'ESC' key and by clicking green traffic light button.
UpdateFullscreenTopUI(false);
[fullscreen_toolbar_controller_ exitFullscreenMode];
}
browser_view()->Layout();
}
bool BrowserNonClientFrameViewMac::CaptionButtonsOnLeadingEdge() const {
return true;
}
gfx::Rect BrowserNonClientFrameViewMac::GetBoundsForTabStripRegion(
const views::View* tabstrip) const {
// TODO(weili): In the future, we should hide the title bar, and show the
// tab strip directly under the menu bar. For now, just lay our content
// under the native title bar. Use the default title bar height to avoid
// calling through private APIs.
DCHECK(tabstrip);
constexpr int kTabstripLeftInset = 70; // Make room for caption buttons.
// Do not draw caption buttons on fullscreen.
const int x = frame()->IsFullscreen() ? 0 : kTabstripLeftInset;
const bool restored = !frame()->IsMaximized() && !frame()->IsFullscreen();
return gfx::Rect(x, GetTopInset(restored), width() - x,
tabstrip->GetPreferredSize().height());
}
int BrowserNonClientFrameViewMac::GetTopInset(bool restored) const {
if (hosted_app_button_container()) {
DCHECK(browser_view()->IsBrowserTypeHostedApp());
if (ShouldHideTopUIForFullscreen())
return 0;
return hosted_app_button_container()->GetPreferredSize().height() +
kHostedAppMenuMargin * 2;
}
if (!browser_view()->IsTabStripVisible())
return 0;
// Mac seems to reserve 1 DIP of the top inset as a resize handle.
constexpr int kResizeHandleHeight = 1;
constexpr int kTabstripTopInset = 8;
int top_inset = kTabstripTopInset;
if (EverHasVisibleBackgroundTabShapes()) {
top_inset =
std::max(top_inset, BrowserNonClientFrameView::kMinimumDragHeight +
kResizeHandleHeight);
}
// Calculate the y offset for the tab strip because in fullscreen mode the tab
// strip may need to move under the slide down menu bar.
CGFloat y_offset = TopUIFullscreenYOffset();
if (y_offset > 0) {
// When menubar shows up, we need to update mouse tracking area.
NSWindow* window = GetWidget()->GetNativeWindow().GetNativeNSWindow();
NSRect content_bounds = [[window contentView] bounds];
// Backing bar tracking area uses native coordinates.
CGFloat tracking_height =
FullscreenBackingBarHeight() + top_inset + y_offset;
NSRect backing_bar_area =
NSMakeRect(0, NSMaxY(content_bounds) - tracking_height,
NSWidth(content_bounds), tracking_height);
[fullscreen_toolbar_controller_ updateToolbarFrame:backing_bar_area];
}
return y_offset + top_inset;
}
int BrowserNonClientFrameViewMac::GetThemeBackgroundXInset() const {
return 0;
}
void BrowserNonClientFrameViewMac::UpdateFullscreenTopUI(
bool needs_check_tab_fullscreen) {
if (base::FeatureList::IsEnabled(features::kImmersiveFullscreen))
return;
FullscreenToolbarStyle old_style =
[fullscreen_toolbar_controller_ toolbarStyle];
// Update to the new toolbar style if needed.
FullscreenToolbarStyle new_style;
FullscreenController* controller =
browser_view()->GetExclusiveAccessManager()->fullscreen_controller();
if ((controller->IsWindowFullscreenForTabOrPending() ||
controller->IsExtensionFullscreenOrPending()) &&
needs_check_tab_fullscreen) {
new_style = FullscreenToolbarStyle::TOOLBAR_NONE;
} else {
new_style = GetUserPreferredToolbarStyle(*show_fullscreen_toolbar_);
}
[fullscreen_toolbar_controller_ setToolbarStyle:new_style];
if (![fullscreen_toolbar_controller_ isInFullscreen] ||
old_style == new_style)
return;
// Notify browser that top ui state has been changed so that we can update
// the bookmark bar state as well.
browser_view()->browser()->FullscreenTopUIStateChanged();
// Re-layout if toolbar style changes in fullscreen mode.
if (frame()->IsFullscreen())
browser_view()->Layout();
}
bool BrowserNonClientFrameViewMac::ShouldHideTopUIForFullscreen() const {
if (frame()->IsFullscreen()) {
return [fullscreen_toolbar_controller_ toolbarStyle] !=
FullscreenToolbarStyle::TOOLBAR_PRESENT;
}
return false;
}
void BrowserNonClientFrameViewMac::UpdateThrobber(bool running) {
}
///////////////////////////////////////////////////////////////////////////////
// BrowserNonClientFrameViewMac, views::NonClientFrameView implementation:
gfx::Rect BrowserNonClientFrameViewMac::GetBoundsForClientView() const {
return bounds();
}
gfx::Rect BrowserNonClientFrameViewMac::GetWindowBoundsForClientBounds(
const gfx::Rect& client_bounds) const {
int top_inset = GetTopInset(false);
// If the operating system is handling drawing the window titlebar then the
// titlebar height will not be included in |GetTopInset|, so we have to
// explicitly add it. If a custom titlebar is being drawn, this calculation
// will be zero.
NSWindow* window = GetWidget()->GetNativeWindow().GetNativeNSWindow();
DCHECK(window);
top_inset += window.frame.size.height -
[window contentRectForFrameRect:window.frame].size.height;
return gfx::Rect(client_bounds.x(), client_bounds.y() - top_inset,
client_bounds.width(), client_bounds.height() + top_inset);
}
int BrowserNonClientFrameViewMac::NonClientHitTest(const gfx::Point& point) {
int super_component = BrowserNonClientFrameView::NonClientHitTest(point);
if (super_component != HTNOWHERE)
return super_component;
// BrowserView::NonClientHitTest will return HTNOWHERE for points that hit
// the native title bar. On Mac, we need to explicitly return HTCAPTION for
// those points.
const int component = frame()->client_view()->NonClientHitTest(point);
return (component == HTNOWHERE && bounds().Contains(point)) ? HTCAPTION
: component;
}
void BrowserNonClientFrameViewMac::GetWindowMask(const gfx::Size& size,
SkPath* window_mask) {}
void BrowserNonClientFrameViewMac::UpdateWindowIcon() {
}
void BrowserNonClientFrameViewMac::UpdateWindowTitle() {
if (window_title_ && !frame()->IsFullscreen()) {
window_title_->SetText(browser_view()->GetWindowTitle());
Layout();
}
}
void BrowserNonClientFrameViewMac::SizeConstraintsChanged() {
}
void BrowserNonClientFrameViewMac::UpdateMinimumSize() {
GetWidget()->OnSizeConstraintsChanged();
}
///////////////////////////////////////////////////////////////////////////////
// BrowserNonClientFrameViewMac, views::View implementation:
gfx::Size BrowserNonClientFrameViewMac::GetMinimumSize() const {
gfx::Size client_size = frame()->client_view()->GetMinimumSize();
if (browser_view()->browser()->is_type_tabbed())
client_size.SetToMax(browser_view()->tabstrip()->GetMinimumSize());
// macOS apps generally don't allow their windows to get shorter than a
// certain height, which empirically seems to be related to their *minimum*
// width rather than their current width. This 4:3 ratio was chosen
// empirically because it looks decent for both tabbed and untabbed browsers.
client_size.SetToMax(gfx::Size(0, (client_size.width() * 3) / 4));
return client_size;
}
///////////////////////////////////////////////////////////////////////////////
// BrowserNonClientFrameViewMac, protected:
// views::View:
void BrowserNonClientFrameViewMac::OnPaint(gfx::Canvas* canvas) {
if (!browser_view()->IsBrowserTypeNormal() &&
!browser_view()->IsBrowserTypeHostedApp()) {
return;
}
SkColor frame_color = GetFrameColor();
canvas->DrawColor(frame_color);
if (window_title_) {
window_title_->SetBackgroundColor(frame_color);
window_title_->SetEnabledColor(GetCaptionColor(kUseCurrent));
}
auto* theme_service =
ThemeServiceFactory::GetForProfile(browser_view()->browser()->profile());
if (!theme_service->UsingSystemTheme())
PaintThemedFrame(canvas);
}
void BrowserNonClientFrameViewMac::Layout() {
const int available_height = GetTopInset(true);
int leading_x = kFramePaddingLeft;
int trailing_x = width();
if (hosted_app_button_container()) {
trailing_x = hosted_app_button_container()->LayoutInContainer(
leading_x, trailing_x, 0, available_height);
const int title_padding = base::checked_cast<int>(
std::round(width() * kTitlePaddingWidthFraction));
window_title_->SetBoundsRect(GetCenteredTitleBounds(
width(), available_height, leading_x + title_padding,
trailing_x - title_padding,
window_title_->CalculatePreferredSize().width()));
}
}
///////////////////////////////////////////////////////////////////////////////
// BrowserNonClientFrameViewMac, private:
gfx::Rect BrowserNonClientFrameViewMac::GetCenteredTitleBounds(
int frame_width,
int frame_height,
int left_inset_x,
int right_inset_x,
int title_width) {
// Center in container.
int title_x = (frame_width - title_width) / 2;
// Align right side to right inset if overlapping.
title_x = std::min(title_x, right_inset_x - title_width);
// Align left side to left inset if overlapping.
title_x = std::max(title_x, left_inset_x);
// Clip width to right inset if overlapping.
title_width = std::min(title_width, right_inset_x - title_x);
return gfx::Rect(title_x, 0, title_width, frame_height);
}
void BrowserNonClientFrameViewMac::PaintThemedFrame(gfx::Canvas* canvas) {
gfx::ImageSkia image = GetFrameImage();
canvas->TileImageInt(image, 0, TopUIFullscreenYOffset(), width(),
image.height());
gfx::ImageSkia overlay = GetFrameOverlayImage();
canvas->DrawImageInt(overlay, 0, 0);
}
CGFloat BrowserNonClientFrameViewMac::FullscreenBackingBarHeight() const {
BrowserView* browser_view = this->browser_view();
DCHECK(browser_view->IsFullscreen());
CGFloat total_height = 0;
if (browser_view->IsTabStripVisible())
total_height += browser_view->GetTabStripHeight();
if (browser_view->IsToolbarVisible())
total_height += browser_view->toolbar()->bounds().height();
return total_height;
}
int BrowserNonClientFrameViewMac::TopUIFullscreenYOffset() const {
if (!browser_view()->IsTabStripVisible() || !browser_view()->IsFullscreen())
return 0;
CGFloat menu_bar_height =
[[[NSApplication sharedApplication] mainMenu] menuBarHeight];
CGFloat title_bar_height =
NSHeight([NSWindow frameRectForContentRect:NSZeroRect
styleMask:NSWindowStyleMaskTitled]);
if (base::FeatureList::IsEnabled(features::kImmersiveFullscreen))
return menu_bar_height == 0 ? 0 : menu_bar_height + title_bar_height;
return [[fullscreen_toolbar_controller_ menubarTracker] menubarFraction] *
(menu_bar_height + title_bar_height);
}