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