1// Copyright 2015 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#include "chrome/browser/ui/views/frame/browser_non_client_frame_view_mac.h" 6 7#include "base/bind.h" 8#include "base/command_line.h" 9#include "base/metrics/histogram_macros.h" 10#include "base/numerics/safe_conversions.h" 11#include "chrome/browser/themes/theme_properties.h" 12#include "chrome/browser/themes/theme_service.h" 13#include "chrome/browser/themes/theme_service_factory.h" 14#include "chrome/browser/ui/cocoa/fullscreen/fullscreen_menubar_tracker.h" 15#include "chrome/browser/ui/cocoa/fullscreen/fullscreen_toolbar_controller.h" 16#include "chrome/browser/ui/exclusive_access/fullscreen_controller.h" 17#include "chrome/browser/ui/layout_constants.h" 18#include "chrome/browser/ui/view_ids.h" 19#include "chrome/browser/ui/views/bookmarks/bookmark_bar_view.h" 20#include "chrome/browser/ui/views/frame/browser_frame.h" 21#include "chrome/browser/ui/views/frame/browser_view.h" 22#include "chrome/browser/ui/views/frame/browser_view_layout.h" 23#include "chrome/browser/ui/views/frame/tab_strip_region_view.h" 24#include "chrome/browser/ui/views/tabs/tab_strip.h" 25#include "chrome/browser/ui/views/toolbar/toolbar_view.h" 26#include "chrome/browser/ui/views/web_apps/web_app_frame_toolbar_view.h" 27#include "chrome/browser/ui/web_applications/app_browser_controller.h" 28#include "chrome/common/chrome_features.h" 29#include "chrome/common/chrome_switches.h" 30#include "chrome/common/pref_names.h" 31#include "components/prefs/pref_service.h" 32#include "ui/base/hit_test.h" 33#include "ui/base/theme_provider.h" 34#include "ui/gfx/canvas.h" 35 36namespace { 37 38constexpr int kFramePaddingLeft = 75; 39// Keep in sync with web_app_frame_toolbar_browsertest.cc 40constexpr double kTitlePaddingWidthFraction = 0.1; 41 42FullscreenToolbarStyle GetUserPreferredToolbarStyle(bool always_show) { 43 // In Kiosk mode, we don't show top Chrome UI. 44 if (base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kKioskMode)) 45 return FullscreenToolbarStyle::TOOLBAR_NONE; 46 return always_show ? FullscreenToolbarStyle::TOOLBAR_PRESENT 47 : FullscreenToolbarStyle::TOOLBAR_HIDDEN; 48} 49 50} // namespace 51 52/////////////////////////////////////////////////////////////////////////////// 53// BrowserNonClientFrameViewMac, public: 54 55BrowserNonClientFrameViewMac::BrowserNonClientFrameViewMac( 56 BrowserFrame* frame, 57 BrowserView* browser_view) 58 : BrowserNonClientFrameView(frame, browser_view) { 59 show_fullscreen_toolbar_.Init( 60 prefs::kShowFullscreenToolbar, browser_view->GetProfile()->GetPrefs(), 61 base::BindRepeating(&BrowserNonClientFrameViewMac::UpdateFullscreenTopUI, 62 base::Unretained(this))); 63 if (!base::FeatureList::IsEnabled(features::kImmersiveFullscreen)) { 64 fullscreen_toolbar_controller_.reset( 65 [[FullscreenToolbarController alloc] initWithBrowserView:browser_view]); 66 [fullscreen_toolbar_controller_ 67 setToolbarStyle:GetUserPreferredToolbarStyle( 68 *show_fullscreen_toolbar_)]; 69 } 70 71 if (browser_view->IsBrowserTypeWebApp()) { 72 if (browser_view->browser()->app_controller()) { 73 set_web_app_frame_toolbar(AddChildView( 74 std::make_unique<WebAppFrameToolbarView>(frame, browser_view))); 75 } 76 77 // The window title appears above the web app frame toolbar (if present), 78 // which surrounds the title with minimal-ui buttons on the left, 79 // and other controls (such as the app menu button) on the right. 80 if (browser_view->ShouldShowWindowTitle()) { 81 window_title_ = AddChildView( 82 std::make_unique<views::Label>(browser_view->GetWindowTitle())); 83 window_title_->SetID(VIEW_ID_WINDOW_TITLE); 84 } 85 } 86} 87 88BrowserNonClientFrameViewMac::~BrowserNonClientFrameViewMac() { 89 if ([fullscreen_toolbar_controller_ isInFullscreen]) 90 [fullscreen_toolbar_controller_ exitFullscreenMode]; 91} 92 93/////////////////////////////////////////////////////////////////////////////// 94// BrowserNonClientFrameViewMac, BrowserNonClientFrameView implementation: 95 96void BrowserNonClientFrameViewMac::OnFullscreenStateChanged() { 97 if (base::FeatureList::IsEnabled(features::kImmersiveFullscreen)) { 98 browser_view()->immersive_mode_controller()->SetEnabled( 99 browser_view()->IsFullscreen()); 100 return; 101 } 102 if (browser_view()->IsFullscreen()) { 103 [fullscreen_toolbar_controller_ enterFullscreenMode]; 104 } else { 105 // Exiting tab fullscreen requires updating Top UI. 106 // Called from here so we can capture exiting tab fullscreen both by 107 // pressing 'ESC' key and by clicking green traffic light button. 108 UpdateFullscreenTopUI(); 109 [fullscreen_toolbar_controller_ exitFullscreenMode]; 110 } 111 browser_view()->Layout(); 112} 113 114bool BrowserNonClientFrameViewMac::CaptionButtonsOnLeadingEdge() const { 115 // In OSX 10.10 and 10.11, caption buttons always get drawn on the left side 116 // of the browser frame instead of the leading edge. This causes a discrepancy 117 // in RTL mode. 118 return !base::i18n::IsRTL() || base::mac::IsAtLeastOS10_12(); 119} 120 121gfx::Rect BrowserNonClientFrameViewMac::GetBoundsForTabStripRegion( 122 const gfx::Size& tabstrip_minimum_size) const { 123 // TODO(weili): In the future, we should hide the title bar, and show the 124 // tab strip directly under the menu bar. For now, just lay our content 125 // under the native title bar. Use the default title bar height to avoid 126 // calling through private APIs. 127 const bool restored = !frame()->IsMaximized() && !frame()->IsFullscreen(); 128 gfx::Rect bounds(0, GetTopInset(restored), width(), 129 tabstrip_minimum_size.height()); 130 131 // Do not draw caption buttons on fullscreen. 132 if (!frame()->IsFullscreen()) { 133 const int kCaptionWidth = base::mac::IsAtMostOS10_15() ? 70 : 85; 134 if (CaptionButtonsOnLeadingEdge()) 135 bounds.Inset(gfx::Insets(0, kCaptionWidth, 0, 0)); 136 else 137 bounds.Inset(gfx::Insets(0, 0, 0, kCaptionWidth)); 138 } 139 140 return bounds; 141} 142 143int BrowserNonClientFrameViewMac::GetTopInset(bool restored) const { 144 if (web_app_frame_toolbar()) { 145 DCHECK(browser_view()->IsBrowserTypeWebApp()); 146 if (ShouldHideTopUIForFullscreen()) 147 return 0; 148 return web_app_frame_toolbar()->GetPreferredSize().height() + 149 kWebAppMenuMargin * 2; 150 } 151 152 if (!browser_view()->IsTabStripVisible()) 153 return 0; 154 155 // Mac seems to reserve 1 DIP of the top inset as a resize handle. 156 constexpr int kResizeHandleHeight = 1; 157 constexpr int kTabstripTopInset = 8; 158 int top_inset = kTabstripTopInset; 159 if (EverHasVisibleBackgroundTabShapes()) { 160 top_inset = 161 std::max(top_inset, BrowserNonClientFrameView::kMinimumDragHeight + 162 kResizeHandleHeight); 163 } 164 165 // Calculate the y offset for the tab strip because in fullscreen mode the tab 166 // strip may need to move under the slide down menu bar. 167 CGFloat y_offset = TopUIFullscreenYOffset(); 168 if (y_offset > 0) { 169 // When menubar shows up, we need to update mouse tracking area. 170 NSWindow* window = GetWidget()->GetNativeWindow().GetNativeNSWindow(); 171 NSRect content_bounds = [[window contentView] bounds]; 172 // Backing bar tracking area uses native coordinates. 173 CGFloat tracking_height = 174 FullscreenBackingBarHeight() + top_inset + y_offset; 175 NSRect backing_bar_area = 176 NSMakeRect(0, NSMaxY(content_bounds) - tracking_height, 177 NSWidth(content_bounds), tracking_height); 178 [fullscreen_toolbar_controller_ updateToolbarFrame:backing_bar_area]; 179 } 180 181 return y_offset + top_inset; 182} 183 184int BrowserNonClientFrameViewMac::GetThemeBackgroundXInset() const { 185 return 0; 186} 187 188void BrowserNonClientFrameViewMac::UpdateFullscreenTopUI() { 189 if (base::FeatureList::IsEnabled(features::kImmersiveFullscreen)) 190 return; 191 192 FullscreenToolbarStyle old_style = 193 [fullscreen_toolbar_controller_ toolbarStyle]; 194 195 // Update to the new toolbar style if needed. 196 FullscreenToolbarStyle new_style; 197 FullscreenController* controller = 198 browser_view()->GetExclusiveAccessManager()->fullscreen_controller(); 199 if ((controller->IsWindowFullscreenForTabOrPending() || 200 controller->IsExtensionFullscreenOrPending())) { 201 browser_view()->HideDownloadShelf(); 202 new_style = FullscreenToolbarStyle::TOOLBAR_NONE; 203 } else { 204 new_style = GetUserPreferredToolbarStyle(*show_fullscreen_toolbar_); 205 browser_view()->UnhideDownloadShelf(); 206 } 207 [fullscreen_toolbar_controller_ setToolbarStyle:new_style]; 208 if (![fullscreen_toolbar_controller_ isInFullscreen] || 209 old_style == new_style) 210 return; 211 212 // Notify browser that top ui state has been changed so that we can update 213 // the bookmark bar state as well. 214 browser_view()->browser()->FullscreenTopUIStateChanged(); 215 216 // Re-layout if toolbar style changes in fullscreen mode. 217 if (frame()->IsFullscreen()) { 218 browser_view()->Layout(); 219 // The web frame toolbar is visible in fullscreen mode on Mac and thus 220 // requires a re-layout when in fullscreen and shown. 221 if (web_app_frame_toolbar() && !ShouldHideTopUIForFullscreen()) 222 InvalidateLayout(); 223 } 224} 225 226bool BrowserNonClientFrameViewMac::ShouldHideTopUIForFullscreen() const { 227 if (frame()->IsFullscreen()) { 228 return [fullscreen_toolbar_controller_ toolbarStyle] != 229 FullscreenToolbarStyle::TOOLBAR_PRESENT; 230 } 231 return false; 232} 233 234void BrowserNonClientFrameViewMac::UpdateThrobber(bool running) { 235} 236 237/////////////////////////////////////////////////////////////////////////////// 238// BrowserNonClientFrameViewMac, views::NonClientFrameView implementation: 239 240gfx::Rect BrowserNonClientFrameViewMac::GetBoundsForClientView() const { 241 return bounds(); 242} 243 244gfx::Rect BrowserNonClientFrameViewMac::GetWindowBoundsForClientBounds( 245 const gfx::Rect& client_bounds) const { 246 int top_inset = GetTopInset(false); 247 248 // If the operating system is handling drawing the window titlebar then the 249 // titlebar height will not be included in |GetTopInset|, so we have to 250 // explicitly add it. If a custom titlebar is being drawn, this calculation 251 // will be zero. 252 NSWindow* window = GetWidget()->GetNativeWindow().GetNativeNSWindow(); 253 DCHECK(window); 254 top_inset += window.frame.size.height - 255 [window contentRectForFrameRect:window.frame].size.height; 256 257 return gfx::Rect(client_bounds.x(), client_bounds.y() - top_inset, 258 client_bounds.width(), client_bounds.height() + top_inset); 259} 260 261int BrowserNonClientFrameViewMac::NonClientHitTest(const gfx::Point& point) { 262 int super_component = BrowserNonClientFrameView::NonClientHitTest(point); 263 if (super_component != HTNOWHERE) 264 return super_component; 265 266 // BrowserView::NonClientHitTest will return HTNOWHERE for points that hit 267 // the native title bar. On Mac, we need to explicitly return HTCAPTION for 268 // those points. 269 const int component = frame()->client_view()->NonClientHitTest(point); 270 return (component == HTNOWHERE && bounds().Contains(point)) ? HTCAPTION 271 : component; 272} 273 274void BrowserNonClientFrameViewMac::GetWindowMask(const gfx::Size& size, 275 SkPath* window_mask) {} 276 277void BrowserNonClientFrameViewMac::UpdateWindowIcon() { 278} 279 280void BrowserNonClientFrameViewMac::UpdateWindowTitle() { 281 if (window_title_) { 282 DCHECK(browser_view()->IsBrowserTypeWebApp()); 283 window_title_->SetText(browser_view()->GetWindowTitle()); 284 Layout(); 285 } 286} 287 288void BrowserNonClientFrameViewMac::SizeConstraintsChanged() { 289} 290 291void BrowserNonClientFrameViewMac::UpdateMinimumSize() { 292 GetWidget()->OnSizeConstraintsChanged(); 293} 294 295/////////////////////////////////////////////////////////////////////////////// 296// BrowserNonClientFrameViewMac, views::View implementation: 297 298gfx::Size BrowserNonClientFrameViewMac::GetMinimumSize() const { 299 gfx::Size client_size = frame()->client_view()->GetMinimumSize(); 300 if (browser_view()->browser()->is_type_normal()) 301 client_size.SetToMax( 302 browser_view()->tab_strip_region_view()->GetMinimumSize()); 303 304 // macOS apps generally don't allow their windows to get shorter than a 305 // certain height, which empirically seems to be related to their *minimum* 306 // width rather than their current width. This 4:3 ratio was chosen 307 // empirically because it looks decent for both tabbed and untabbed browsers. 308 client_size.SetToMax(gfx::Size(0, (client_size.width() * 3) / 4)); 309 310 return client_size; 311} 312 313/////////////////////////////////////////////////////////////////////////////// 314// BrowserNonClientFrameViewMac, protected: 315 316// views::View: 317 318void BrowserNonClientFrameViewMac::OnPaint(gfx::Canvas* canvas) { 319 if (!browser_view()->IsBrowserTypeNormal() && 320 !browser_view()->IsBrowserTypeWebApp()) { 321 return; 322 } 323 324 SkColor frame_color = GetFrameColor(); 325 canvas->DrawColor(frame_color); 326 327 if (window_title_) { 328 window_title_->SetBackgroundColor(frame_color); 329 window_title_->SetEnabledColor( 330 GetCaptionColor(BrowserFrameActiveState::kUseCurrent)); 331 } 332 333 auto* theme_service = 334 ThemeServiceFactory::GetForProfile(browser_view()->browser()->profile()); 335 if (!theme_service->UsingSystemTheme()) 336 PaintThemedFrame(canvas); 337} 338 339void BrowserNonClientFrameViewMac::Layout() { 340 const int available_height = GetTopInset(true); 341 int leading_x = kFramePaddingLeft; 342 int trailing_x = width(); 343 344 if (web_app_frame_toolbar()) { 345 std::pair<int, int> remaining_bounds = 346 web_app_frame_toolbar()->LayoutInContainer(leading_x, trailing_x, 0, 347 available_height); 348 leading_x = remaining_bounds.first; 349 trailing_x = remaining_bounds.second; 350 351 const int title_padding = base::checked_cast<int>( 352 std::round(width() * kTitlePaddingWidthFraction)); 353 window_title_->SetBoundsRect(GetCenteredTitleBounds( 354 width(), available_height, leading_x + title_padding, 355 trailing_x - title_padding, 356 window_title_->CalculatePreferredSize().width())); 357 } 358} 359 360/////////////////////////////////////////////////////////////////////////////// 361// BrowserNonClientFrameViewMac, private: 362 363gfx::Rect BrowserNonClientFrameViewMac::GetCenteredTitleBounds( 364 int frame_width, 365 int frame_height, 366 int left_inset_x, 367 int right_inset_x, 368 int title_width) { 369 // Center in container. 370 int title_x = (frame_width - title_width) / 2; 371 372 // Align right side to right inset if overlapping. 373 title_x = std::min(title_x, right_inset_x - title_width); 374 375 // Align left side to left inset if overlapping. 376 title_x = std::max(title_x, left_inset_x); 377 378 // Clip width to right inset if overlapping. 379 title_width = std::min(title_width, right_inset_x - title_x); 380 381 return gfx::Rect(title_x, 0, title_width, frame_height); 382} 383 384void BrowserNonClientFrameViewMac::PaintThemedFrame(gfx::Canvas* canvas) { 385 gfx::ImageSkia image = GetFrameImage(); 386 canvas->TileImageInt(image, 0, TopUIFullscreenYOffset(), width(), 387 image.height()); 388 gfx::ImageSkia overlay = GetFrameOverlayImage(); 389 canvas->DrawImageInt(overlay, 0, 0); 390} 391 392CGFloat BrowserNonClientFrameViewMac::FullscreenBackingBarHeight() const { 393 BrowserView* browser_view = this->browser_view(); 394 DCHECK(browser_view->IsFullscreen()); 395 396 CGFloat total_height = 0; 397 if (browser_view->IsTabStripVisible()) 398 total_height += browser_view->GetTabStripHeight(); 399 400 if (browser_view->IsToolbarVisible()) 401 total_height += browser_view->toolbar()->bounds().height(); 402 403 return total_height; 404} 405 406int BrowserNonClientFrameViewMac::TopUIFullscreenYOffset() const { 407 if (!browser_view()->IsTabStripVisible() || !browser_view()->IsFullscreen()) 408 return 0; 409 410 CGFloat menu_bar_height = 411 [[[NSApplication sharedApplication] mainMenu] menuBarHeight]; 412 CGFloat title_bar_height = 413 NSHeight([NSWindow frameRectForContentRect:NSZeroRect 414 styleMask:NSWindowStyleMaskTitled]); 415 if (base::FeatureList::IsEnabled(features::kImmersiveFullscreen)) 416 return menu_bar_height == 0 ? 0 : menu_bar_height + title_bar_height; 417 return [[fullscreen_toolbar_controller_ menubarTracker] menubarFraction] * 418 (menu_bar_height + title_bar_height); 419} 420