Browse Source

feat: enable windows control overlay on Windows (#30887)

* feat: enable window controls overlay on macOS (#29253)

* feat: enable windows control overlay on macOS

* address review feedback

* chore: address review feedback

* Address review feedback

* update doc per review

* only enable WCO when titleBarStyle is overlay

* Revert "only enable WCO when titleBarStyle is overlay"

This reverts commit 1b58b5b1fcb8f091880a4e5d1f8855399c44afad.

* Add new titleBarOverlay property to manage feature

* spelling fix

* Update docs/api/frameless-window.md

Co-authored-by: Samuel Attard <[email protected]>

* Update shell/browser/api/electron_api_browser_window.cc

Co-authored-by: Samuel Attard <[email protected]>

* update per review feedback

Co-authored-by: Samuel Attard <[email protected]>
(cherry picked from commit 1f8a46c9c6d6ec4e597e4ae4abe0b1e898bc78b9)

* feat: enable windows control overlay on Windows (#30678)

cherry-picked from 41646d1

Co-Authored-By: Michaela Laurencin <[email protected]>

Co-authored-by: Michaela Laurencin <[email protected]>

* modify included header files and update patches

* kick off missed ci

* fix lint error

* chore: update patches

* chore: update patches

* remove version control marker

* correct `resizeable_` backport

Co-authored-by: John Kleinschmidt <[email protected]>
Co-authored-by: PatchUp <73610968+patchup[bot]@users.noreply.github.com>
Co-authored-by: Cheng Zhao <[email protected]>
Michaela Laurencin 3 years ago
parent
commit
d0ba8d1f69

+ 3 - 0
chromium_src/BUILD.gn

@@ -66,8 +66,11 @@ static_library("chrome") {
       "//chrome/browser/extensions/global_shortcut_listener_win.cc",
       "//chrome/browser/extensions/global_shortcut_listener_win.h",
       "//chrome/browser/icon_loader_win.cc",
+      "//chrome/browser/ui/frame/window_frame_util.h",
+      "//chrome/browser/ui/view_ids.h",
       "//chrome/browser/win/chrome_process_finder.cc",
       "//chrome/browser/win/chrome_process_finder.h",
+      "//chrome/browser/win/titlebar_config.h",
       "//chrome/child/v8_crashpad_support_win.cc",
       "//chrome/child/v8_crashpad_support_win.h",
     ]

+ 6 - 12
docs/api/browser-window.md

@@ -213,16 +213,13 @@ It creates a new `BrowserWindow` with native properties as set by the `options`.
     * `followWindow` - The backdrop should automatically appear active when the window is active, and inactive when it is not. This is the default.
     * `active` - The backdrop should always appear active.
     * `inactive` - The backdrop should always appear inactive.
-  * `titleBarStyle` String (optional) - The style of window title bar.
+  * `titleBarStyle` String (optional) _macOS_ _Windows_ - The style of window title bar.
     Default is `default`. Possible values are:
-    * `default` - Results in the standard gray opaque Mac title
-      bar.
-    * `hidden` - Results in a hidden title bar and a full size content window, yet
-      the title bar still has the standard window controls ("traffic lights") in
-      the top left.
-    * `hiddenInset` - Results in a hidden title bar with an alternative look
+    * `default` - Results in the standard title bar for macOS or Windows respectively.
+    * `hidden` - Results in a hidden title bar and a full size content window. On macOS, the window still has the standard window controls (“traffic lights”) in the top left. On Windows, when combined with `titleBarOverlay: true` it will activate the Window Controls Overlay (see `titleBarOverlay` for more information), otherwise no window controls will be shown.
+    * `hiddenInset` - Only on macOS, results in a hidden title bar with an alternative look
       where the traffic light buttons are slightly more inset from the window edge.
-    * `customButtonsOnHover` - Results in a hidden title bar and a full size
+    * `customButtonsOnHover` - Only on macOS, results in a hidden title bar and a full size
       content window, the traffic light buttons will display when being hovered
       over in the top left of the window.  **Note:** This option is currently
       experimental.
@@ -403,10 +400,7 @@ It creates a new `BrowserWindow` with native properties as set by the `options`.
       contain the layout of the document—without requiring scrolling. Enabling
       this will cause the `preferred-size-changed` event to be emitted on the
       `WebContents` when the preferred size changes. Default is `false`.
-  * `titleBarOverlay` Boolean (optional) -  On macOS, when using a frameless window in conjunction with
-    `win.setWindowButtonVisibility(true)` or using a `titleBarStyle` so that the traffic lights are visible,
-    this property enables the Window Controls Overlay [JavaScript APIs][overlay-javascript-apis] and
-    [CSS Environment Variables][overlay-css-env-vars].  Default is `false`.
+  * `titleBarOverlay` [OverlayOptions](structures/overlay-options.md) | Boolean (optional) -  When using a frameless window in conjuction with `win.setWindowButtonVisibility(true)` on macOS or using a `titleBarStyle` so that the standard window controls ("traffic lights" on macOS) are visible, this property enables the Window Controls Overlay [JavaScript APIs][overlay-javascript-apis] and [CSS Environment Variables][overlay-css-env-vars]. Specifying `true` will result in an overlay with default system colors. Default is `false`.  On Windows, the [OverlayOptions](structures/overlay-options.md) can be used instead of a boolean to specify colors for the overlay.
 
 When setting minimum or maximum window size with `minWidth`/`maxWidth`/
 `minHeight`/`maxHeight`, it only constrains the users. It won't prevent you from

+ 24 - 8
docs/api/frameless-window.md

@@ -18,17 +18,17 @@ const win = new BrowserWindow({ width: 800, height: 600, frame: false })
 win.show()
 ```
 
-### Alternatives on macOS
+### Alternatives
 
-There's an alternative way to specify a chromeless window.
+There's an alternative way to specify a chromeless window on macOS and Windows.
 Instead of setting `frame` to `false` which disables both the titlebar and window controls,
 you may want to have the title bar hidden and your content extend to the full window size,
-yet still preserve the window controls ("traffic lights") for standard window actions.
+yet still preserve the window controls ("traffic lights" on macOS) for standard window actions.
 You can do so by specifying the `titleBarStyle` option:
 
 #### `hidden`
 
-Results in a hidden title bar and a full size content window, yet the title bar still has the standard window controls (“traffic lights”) in the top left.
+Results in a hidden title bar and a full size content window. On macOS, the title bar still has the standard window controls (“traffic lights”) in the top left.
 
 ```javascript
 const { BrowserWindow } = require('electron')
@@ -36,6 +36,8 @@ const win = new BrowserWindow({ titleBarStyle: 'hidden' })
 win.show()
 ```
 
+### Alternatives on macOS
+
 #### `hiddenInset`
 
 Results in a hidden title bar with an alternative look where the traffic light buttons are slightly more inset from the window edge.
@@ -63,19 +65,33 @@ win.show()
 
 ## Windows Control Overlay
 
-On macOS, when using a frameless window in conjuction with `win.setWindowButtonVisibility(true)` or using one of the `titleBarStyle`s described above so
-that the traffic lights are visible, you can access the Window Controls Overlay [JavaScript APIs][overlay-javascript-apis] and
-[CSS Environment Variables][overlay-css-env-vars] by setting the `titleBarOverlay` option to true:
+When using a frameless window in conjuction with `win.setWindowButtonVisibility(true)` on macOS, using one of the `titleBarStyle`s as described above so
+that the traffic lights are visible, or using `titleBarStyle: hidden` on Windows, you can access the Window Controls Overlay [JavaScript APIs][overlay-javascript-apis] and
+[CSS Environment Variables][overlay-css-env-vars] by setting the `titleBarOverlay` option to true. Specifying `true` will result in an overlay with default system colors.
+
+On Windows, you can also specify the color of the overlay and its symbols by setting `titleBarOverlay` to an object with the options `color` and `symbolColor`. If an option is not specified, the color will default to its system color for the window control buttons:
 
 ```javascript
 const { BrowserWindow } = require('electron')
 const win = new BrowserWindow({
-  titleBarStyle: 'hiddenInset',
+  titleBarStyle: 'hidden',
   titleBarOverlay: true
 })
 win.show()
 ```
 
+```javascript
+const { BrowserWindow } = require('electron')
+const win = new BrowserWindow({
+  titleBarStyle: 'hidden',
+  titleBarOverlay: {
+    color: '#2f3241',
+    symbolColor: '#74b1be'
+  }
+})
+win.show()
+```
+
 ## Transparent window
 
 By setting the `transparent` option to `true`, you can also make the frameless

+ 4 - 0
docs/api/structures/overlay-options.md

@@ -0,0 +1,4 @@
+# OverlayOptions Object
+
+* `color` String (optional) _Windows_ - The CSS color of the Window Controls Overlay when enabled. Default is the system color.
+* `symbolColor` String (optional) _Windows_ - The CSS color of the symbols on the Window Controls Overlay when enabled. Default is the system color.

+ 14 - 0
electron_strings.grdp

@@ -1,5 +1,19 @@
 <?xml version="1.0" encoding="utf-8"?>
 <grit-part>
+  <!-- Windows Caption Buttons -->
+  <message name="IDS_APP_ACCNAME_CLOSE" desc="The accessible name for the Close button.">
+    Close
+  </message>
+  <message name="IDS_APP_ACCNAME_MINIMIZE" desc="The accessible name for the Minimize button.">
+    Minimize
+  </message>
+  <message name="IDS_APP_ACCNAME_MAXIMIZE" desc="The accessible name for the Maximize button.">
+    Maximize
+  </message>
+  <message name="IDS_APP_ACCNAME_RESTORE" desc="The accessible name for the Restore button.">
+    Restore
+  </message>
+
   <!-- Printing Service -->
   <message name="IDS_UTILITY_PROCESS_PRINTING_SERVICE_NAME" desc="The name of the utility process used for printing conversions.">
     Printing Service

+ 1 - 0
filenames.auto.gni

@@ -101,6 +101,7 @@ auto_filenames = {
     "docs/api/structures/new-window-web-contents-event.md",
     "docs/api/structures/notification-action.md",
     "docs/api/structures/notification-response.md",
+    "docs/api/structures/overlay-options.md",
     "docs/api/structures/point.md",
     "docs/api/structures/post-body.md",
     "docs/api/structures/printer-info.md",

+ 4 - 0
filenames.gni

@@ -90,6 +90,10 @@ filenames = {
     "shell/browser/ui/views/electron_views_delegate_win.cc",
     "shell/browser/ui/views/win_frame_view.cc",
     "shell/browser/ui/views/win_frame_view.h",
+    "shell/browser/ui/views/win_caption_button.cc",
+    "shell/browser/ui/views/win_caption_button.h",
+    "shell/browser/ui/views/win_caption_button_container.cc",
+    "shell/browser/ui/views/win_caption_button_container.h",
     "shell/browser/ui/win/dialog_thread.cc",
     "shell/browser/ui/win/dialog_thread.h",
     "shell/browser/ui/win/electron_desktop_native_widget_aura.cc",

+ 2 - 0
patches/chromium/.patches

@@ -133,6 +133,8 @@ attach_to_correct_frame_in.patch
 merge_m92_speculative_fix_for_crash_in.patch
 cherry-pick-d727013bb543.patch
 pa_make_getusablesize_handle_nullptr_gracefully.patch
+dpwas_window_control_overlay_api_values_account_for_page_zoom_factor.patch
+reland_make_clientview_a_child_of_the_nonclientframeview.patch
 content-visibility_force_range_base_extent_when_computing_visual.patch
 cherry-pick-6215793f008f.patch
 cherry-pick-6048fcd52f42.patch

+ 701 - 0
patches/chromium/dpwas_window_control_overlay_api_values_account_for_page_zoom_factor.patch

@@ -0,0 +1,701 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Mike Jackson <[email protected]>
+Date: Wed, 9 Jun 2021 16:48:30 +0000
+Subject: dpwas: Window Control Overlay API values account for page zoom factor
+
+The overlay's bounding rect passed from the browser process
+to the render process doesn't take the page's zoom factor
+(browser zoom - Ctrl+/-) into account. The bounding rect is
+exposed via a JS API/Event and CSS environment variables, so
+we need to convert from Frame space coordinates to unzoomed
+CSS pixels. When calculating the new rect, ensure that we return
+a slightly larger rect if needed to avoid rendering contents
+smaller than the Window Control Overlay. e.g. If the height of
+the Window Control Overlay is 32, and page's zoom factor is 500%
+we will return a height of 7, instead of 6.
+
+LocalFrame is notified of page zoom change via
+SynchronizeVisualProperties, to ensure we are only computing this
+in a single pass, we also add the Window Control Overlay rect
+to the SynchronizeVisualProperties message.
+
+Manual testing:
+
+1) Enable 'Desktop PWA Window Controls Overlay' flags
+2) Install https://amandabaker.github.io/pwa/windowControlsOverlay-newCSSVars/index.html
+3) Toggle Window Control Overlay on
+4) Change zoom level for PWA via the 3 dots menu
+5) As you increase the zoom level, the values returned should decrease
+6) As you decrease the zoom level, the values returned should increase
+
+Screenshots:
+  100%: https://imgur.com/a/L4MV4RW
+   80%: https://imgur.com/a/xH79oZg
+  125%: https://imgur.com/a/CcqlkPV
+
+Explainer: https://github.com/WICG/window-controls-overlay/blob/master/explainer.md
+Design Doc: https://docs.google.com/document/d/1k0YL_-VMLIfjYCgJ2v6cMvuUv2qMKg4BgLI2tJ4qtyo/edit?usp=sharing
+I2P: https://groups.google.com/a/chromium.org/forum/#!msg/blink-dev/cper6nNLFRQ/hU91kfCWBQAJ
+
+Bug: 937121, 1213123
+Change-Id: I6744bb5a64b4021195734464b9a024e15277baa7
+Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2918946
+Commit-Queue: Mike Jackson <[email protected]>
+Reviewed-by: Daniel Cheng <[email protected]>
+Reviewed-by: Avi Drissman <[email protected]>
+Reviewed-by: danakj <[email protected]>
+Cr-Commit-Position: refs/heads/master@{#890815}
+
+diff --git a/content/browser/renderer_host/render_widget_host_delegate.cc b/content/browser/renderer_host/render_widget_host_delegate.cc
+index 26c7a93644bb2b9f58817294265b80de33e9ef1b..3780835536c56f076831aadac63878133f21a0cd 100644
+--- a/content/browser/renderer_host/render_widget_host_delegate.cc
++++ b/content/browser/renderer_host/render_widget_host_delegate.cc
+@@ -91,6 +91,10 @@ blink::mojom::DisplayMode RenderWidgetHostDelegate::GetDisplayMode() const {
+   return blink::mojom::DisplayMode::kBrowser;
+ }
+ 
++gfx::Rect RenderWidgetHostDelegate::GetWindowsControlsOverlayRect() const {
++  return gfx::Rect();
++}
++
+ bool RenderWidgetHostDelegate::HasMouseLock(
+     RenderWidgetHostImpl* render_widget_host) {
+   return false;
+diff --git a/content/browser/renderer_host/render_widget_host_delegate.h b/content/browser/renderer_host/render_widget_host_delegate.h
+index 51bcc78ecd8f5f40e90a5e9077ac59b37c5c3e13..74d81a2a91ef515c3b89e2ceaa197b894c9fd9b7 100644
+--- a/content/browser/renderer_host/render_widget_host_delegate.h
++++ b/content/browser/renderer_host/render_widget_host_delegate.h
+@@ -215,6 +215,10 @@ class CONTENT_EXPORT RenderWidgetHostDelegate {
+   // to frame-based widgets. Other widgets are always kBrowser.
+   virtual blink::mojom::DisplayMode GetDisplayMode() const;
+ 
++  // Returns the Window Control Overlay rectangle. Only applies to an
++  // outermost main frame's widget. Other widgets always returns an empty rect.
++  virtual gfx::Rect GetWindowsControlsOverlayRect() const;
++
+   // Notification that the widget has lost capture.
+   virtual void LostCapture(RenderWidgetHostImpl* render_widget_host) {}
+ 
+diff --git a/content/browser/renderer_host/render_widget_host_impl.cc b/content/browser/renderer_host/render_widget_host_impl.cc
+index fc8916ac6dc76968e0cbd06877ffb80c95f3abf4..18f0ea3b8ea34f6287e92299ef147bccaedd302a 100644
+--- a/content/browser/renderer_host/render_widget_host_impl.cc
++++ b/content/browser/renderer_host/render_widget_host_impl.cc
+@@ -879,6 +879,8 @@ blink::VisualProperties RenderWidgetHostImpl::GetVisualProperties() {
+   auto& current_screen_info = visual_properties.screen_infos.mutable_current();
+ 
+   visual_properties.is_fullscreen_granted = delegate_->IsFullscreen();
++  visual_properties.window_controls_overlay_rect =
++      delegate_->GetWindowsControlsOverlayRect();
+ 
+   if (is_frame_widget)
+     visual_properties.display_mode = delegate_->GetDisplayMode();
+@@ -2659,7 +2661,9 @@ bool RenderWidgetHostImpl::StoredVisualPropertiesNeedsUpdate(
+          old_visual_properties->is_pinch_gesture_active !=
+              new_visual_properties.is_pinch_gesture_active ||
+          old_visual_properties->root_widget_window_segments !=
+-             new_visual_properties.root_widget_window_segments;
++             new_visual_properties.root_widget_window_segments ||
++         old_visual_properties->window_controls_overlay_rect !=
++             new_visual_properties.window_controls_overlay_rect;
+ }
+ 
+ void RenderWidgetHostImpl::AutoscrollStart(const gfx::PointF& position) {
+diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc
+index ca51a8a45570fafc0dfe2b400cbb7172a9be632d..835a100a98882e3fff1e679ed596171ce865a653 100644
+--- a/content/browser/web_contents/web_contents_impl.cc
++++ b/content/browser/web_contents/web_contents_impl.cc
+@@ -7872,10 +7872,22 @@ gfx::Size WebContentsImpl::GetSize() {
+ 
+ #endif  // !defined(OS_MAC)
+ 
++gfx::Rect WebContentsImpl::GetWindowsControlsOverlayRect() const {
++  return window_controls_overlay_rect_;
++}
++
+ void WebContentsImpl::UpdateWindowControlsOverlay(
+     const gfx::Rect& bounding_rect) {
+-  GetMainFrame()->GetAssociatedLocalMainFrame()->UpdateWindowControlsOverlay(
+-      bounding_rect);
++  if (window_controls_overlay_rect_ == bounding_rect)
++    return;
++
++  window_controls_overlay_rect_ = bounding_rect;
++
++  // Updates to the |window_controls_overlay_rect_| are sent via
++  // the VisualProperties message.
++  if (RenderWidgetHost* render_widget_host =
++          GetMainFrame()->GetRenderWidgetHost())
++    render_widget_host->SynchronizeVisualProperties();
+ }
+ 
+ BrowserPluginEmbedder* WebContentsImpl::GetBrowserPluginEmbedder() const {
+diff --git a/content/browser/web_contents/web_contents_impl.h b/content/browser/web_contents/web_contents_impl.h
+index 17034e75d2ab5bd4e716e9c72277c77a53387808..3e32a9b4e17bb515066acaf014d1fe659cc83772 100644
+--- a/content/browser/web_contents/web_contents_impl.h
++++ b/content/browser/web_contents/web_contents_impl.h
+@@ -960,6 +960,7 @@ class CONTENT_EXPORT WebContentsImpl : public WebContents,
+   bool IsWidgetForMainFrame(RenderWidgetHostImpl* render_widget_host) override;
+   bool IsShowingContextMenuOnPage() const override;
+   void DidChangeScreenOrientation() override;
++  gfx::Rect GetWindowsControlsOverlayRect() const override;
+ 
+   // RenderFrameHostManager::Delegate ------------------------------------------
+ 
+@@ -2091,6 +2092,12 @@ class CONTENT_EXPORT WebContentsImpl : public WebContents,
+   // with OOPIF renderers.
+   blink::mojom::TextAutosizerPageInfo text_autosizer_page_info_;
+ 
++  // Stores the rect of the Windows Control Overlay, which contains system UX
++  // affordances (e.g. close), for installed desktop Progress Web Apps (PWAs),
++  // if the app specifies the 'window-controls-overlay' DisplayMode in its
++  // manifest. This is in frame space coordinates.
++  gfx::Rect window_controls_overlay_rect_;
++
+   // Observe native theme for changes to dark mode, preferred color scheme, and
+   // preferred contrast. Used to notify the renderer of preferred color scheme
+   // and preferred contrast changes.
+diff --git a/content/browser/web_contents/web_contents_impl_browsertest.cc b/content/browser/web_contents/web_contents_impl_browsertest.cc
+index 2e75cbf168dbfa48d9f094ed84398197fd0487aa..73dc93a8afd6fa1ff38e900590681e22d43f7ca4 100644
+--- a/content/browser/web_contents/web_contents_impl_browsertest.cc
++++ b/content/browser/web_contents/web_contents_impl_browsertest.cc
+@@ -43,6 +43,7 @@
+ #include "content/public/browser/back_forward_cache.h"
+ #include "content/public/browser/browser_thread.h"
+ #include "content/public/browser/file_select_listener.h"
++#include "content/public/browser/host_zoom_map.h"
+ #include "content/public/browser/invalidate_type.h"
+ #include "content/public/browser/javascript_dialog_manager.h"
+ #include "content/public/browser/load_notification_details.h"
+@@ -88,6 +89,7 @@
+ #include "testing/gmock/include/gmock/gmock.h"
+ #include "third_party/blink/public/common/client_hints/client_hints.h"
+ #include "third_party/blink/public/common/features.h"
++#include "third_party/blink/public/common/page/page_zoom.h"
+ #include "third_party/blink/public/mojom/frame/fullscreen.mojom.h"
+ #include "ui/base/clipboard/clipboard_format_type.h"
+ #include "url/gurl.h"
+@@ -4466,19 +4468,74 @@ class WebContentsImplBrowserTestWindowControlsOverlay
+   }
+ 
+   void ValidateTitlebarAreaCSSValue(const std::string& name,
+-                                    const std::string& expected_result) {
++                                    int expected_result) {
+     SCOPED_TRACE(name);
+-
+     EXPECT_EQ(
+         expected_result,
+         EvalJs(shell()->web_contents(),
+                JsReplace(
+-                   "(() => {const e = document.getElementById('target');const "
+-                   "style = window.getComputedStyle(e, null); return "
+-                   "style.getPropertyValue($1);})();",
++                   "(() => {"
++                   "const e = document.getElementById('target');"
++                   "const style = window.getComputedStyle(e, null);"
++                   "return Math.round(style.getPropertyValue($1).replace('px', "
++                   "''));"
++                   "})();",
+                    name)));
+   }
+ 
++
++  void ValidateWindowsControlOverlayState(WebContents* web_contents,
++                                          const gfx::Rect& expected_rect,
++                                          int css_fallback_value) {
++    EXPECT_EQ(!expected_rect.IsEmpty(),
++              EvalJs(web_contents, "navigator.windowControlsOverlay.visible"));
++    EXPECT_EQ(
++        expected_rect.x(),
++        EvalJs(web_contents,
++               "navigator.windowControlsOverlay.getBoundingClientRect().x"));
++    EXPECT_EQ(
++        expected_rect.y(),
++        EvalJs(web_contents,
++               "navigator.windowControlsOverlay.getBoundingClientRect().y"));
++    EXPECT_EQ(
++        expected_rect.width(),
++        EvalJs(
++            web_contents,
++            "navigator.windowControlsOverlay.getBoundingClientRect().width"));
++    EXPECT_EQ(
++        expected_rect.height(),
++        EvalJs(
++            web_contents,
++            "navigator.windowControlsOverlay.getBoundingClientRect().height"));
++
++    // When the overlay is not visible, the environment variables should be
++    // undefined, and the the fallback value should be used.
++    gfx::Rect css_rect = expected_rect;
++    if (css_rect.IsEmpty()) {
++      css_rect.SetRect(css_fallback_value, css_fallback_value,
++                       css_fallback_value, css_fallback_value);
++    }
++
++    ValidateTitlebarAreaCSSValue("left", css_rect.x());
++    ValidateTitlebarAreaCSSValue("top", css_rect.y());
++    ValidateTitlebarAreaCSSValue("width", css_rect.width());
++    ValidateTitlebarAreaCSSValue("height", css_rect.height());
++  }
++
++  void WaitForWindowControlsOverlayUpdate(
++      WebContents* web_contents,
++      const gfx::Rect& bounding_client_rect) {
++    EXPECT_TRUE(
++        ExecJs(web_contents->GetMainFrame(),
++               "navigator.windowControlsOverlay.ongeometrychange = (e) => {"
++               "  document.title = 'ongeometrychange'"
++               "}"));
++
++    web_contents->UpdateWindowControlsOverlay(bounding_client_rect);
++    TitleWatcher title_watcher(web_contents, u"ongeometrychange");
++    ignore_result(title_watcher.WaitAndGetTitle());
++  }
++
+  private:
+   base::test::ScopedFeatureList scoped_feature_list_;
+ };
+@@ -4500,24 +4557,12 @@ IN_PROC_BROWSER_TEST_F(WebContentsImplBrowserTestWindowControlsOverlay,
+   // empty.
+   int empty_rect_value = 0;
+ 
+-  EXPECT_EQ(false,
+-            EvalJs(web_contents, "navigator.windowControlsOverlay.visible"));
+-  EXPECT_EQ(
+-      empty_rect_value,
+-      EvalJs(web_contents,
+-             "navigator.windowControlsOverlay.getBoundingClientRect().x"));
+-  EXPECT_EQ(
+-      empty_rect_value,
+-      EvalJs(web_contents,
+-             "navigator.windowControlsOverlay.getBoundingClientRect().y"));
+-  EXPECT_EQ(
+-      empty_rect_value,
+-      EvalJs(web_contents,
+-             "navigator.windowControlsOverlay.getBoundingClientRect().width"));
+-  EXPECT_EQ(
+-      empty_rect_value,
+-      EvalJs(web_contents,
+-             "navigator.windowControlsOverlay.getBoundingClientRect().height"));
++
++  // Update bounds and ensure that JS APIs and CSS variables are updated.
++  gfx::Rect bounding_client_rect(1, 2, 3, 4);
++  WaitForWindowControlsOverlayUpdate(web_contents, bounding_client_rect);
++  ValidateWindowsControlOverlayState(web_contents, bounding_client_rect, 50);
++}
+ 
+   // When the overlay is not visble, the environment variables should be
+   // undefined, and the the fallback value of 50px should be used.
+@@ -4535,31 +4580,15 @@ IN_PROC_BROWSER_TEST_F(WebContentsImplBrowserTestWindowControlsOverlay,
+   gfx::Rect bounding_client_rect =
+       gfx::Rect(new_x, new_y, new_width, new_height);
+ 
+-  web_contents->UpdateWindowControlsOverlay(bounding_client_rect);
+-
+-  EXPECT_EQ(true,
+-            EvalJs(web_contents, "navigator.windowControlsOverlay.visible"));
+-  EXPECT_EQ(
+-      new_x,
+-      EvalJs(web_contents,
+-             "navigator.windowControlsOverlay.getBoundingClientRect().x"));
+-  EXPECT_EQ(
+-      new_y,
+-      EvalJs(web_contents,
+-             "navigator.windowControlsOverlay.getBoundingClientRect().y"));
+-  EXPECT_EQ(
+-      new_width,
+-      EvalJs(web_contents,
+-             "navigator.windowControlsOverlay.getBoundingClientRect().width"));
+-  EXPECT_EQ(
+-      new_height,
+-      EvalJs(web_contents,
+-             "navigator.windowControlsOverlay.getBoundingClientRect().height"));
+-
+-  ValidateTitlebarAreaCSSValue("left", "1px");
+-  ValidateTitlebarAreaCSSValue("top", "2px");
+-  ValidateTitlebarAreaCSSValue("width", "3px");
+-  ValidateTitlebarAreaCSSValue("height", "4px");
++  // Update bounds and ensure that JS APIs and CSS variables are updated.
++  gfx::Rect bounding_client_rect(0, 0, 100, 32);
++  WaitForWindowControlsOverlayUpdate(web_contents, bounding_client_rect);
++  ValidateWindowsControlOverlayState(web_contents, bounding_client_rect, 55);
++
++  // Now toggle Windows Controls Overlay off.
++  gfx::Rect empty_rect;
++  WaitForWindowControlsOverlayUpdate(web_contents, empty_rect);
++  ValidateWindowsControlOverlayState(web_contents, empty_rect, 55);
+ }
+ 
+ IN_PROC_BROWSER_TEST_F(WebContentsImplBrowserTestWindowControlsOverlay,
+@@ -4568,14 +4597,16 @@ IN_PROC_BROWSER_TEST_F(WebContentsImplBrowserTestWindowControlsOverlay,
+ 
+   GURL url(url::kAboutBlankURL);
+   EXPECT_TRUE(NavigateToURL(shell(), url));
+-  EXPECT_TRUE(ExecuteScript(
+-      web_contents->GetMainFrame(),
+-      "geometrychangeCount = 0;"
+-      "navigator.windowControlsOverlay.ongeometrychange = (e) => {"
+-      "  geometrychangeCount++;"
+-      "  rect = e.boundingRect;"
+-      "  visible = e.visible;"
+-      "}"));
++
++  EXPECT_TRUE(
++      ExecJs(web_contents->GetMainFrame(),
++             "geometrychangeCount = 0;"
++             "navigator.windowControlsOverlay.ongeometrychange = (e) => {"
++             "  geometrychangeCount++;"
++             "  rect = e.boundingRect;"
++             "  visible = e.visible;"
++             "  document.title = 'ongeometrychange' + geometrychangeCount"
++             "}"));
+ 
+   WaitForLoadStop(web_contents);
+ 
+@@ -4584,23 +4615,107 @@ IN_PROC_BROWSER_TEST_F(WebContentsImplBrowserTestWindowControlsOverlay,
+   EXPECT_EQ(0, EvalJs(web_contents, "geometrychangeCount"));
+ 
+   // Information about the bounds should be updated.
+-  const int x = 2;
+-  const int y = 2;
+-  const int width = 2;
+-  const int height = 2;
+-
+-  gfx::Rect bounding_client_rect = gfx::Rect(x, y, width, height);
+-
++  gfx::Rect bounding_client_rect = gfx::Rect(2, 3, 4, 5);
+   web_contents->UpdateWindowControlsOverlay(bounding_client_rect);
++  TitleWatcher title_watcher(web_contents, u"ongeometrychange1");
++  ignore_result(title_watcher.WaitAndGetTitle());
+ 
+   // Expect the "geometrychange" event to have fired once.
+   EXPECT_EQ(1, EvalJs(web_contents, "geometrychangeCount"));
+ 
+   // Validate the event payload.
+   EXPECT_EQ(true, EvalJs(web_contents, "visible"));
+-  EXPECT_EQ(x, EvalJs(web_contents, "rect.x;"));
+-  EXPECT_EQ(y, EvalJs(web_contents, "rect.y"));
+-  EXPECT_EQ(width, EvalJs(web_contents, "rect.width"));
+-  EXPECT_EQ(height, EvalJs(web_contents, "rect.height"));
++  EXPECT_EQ(bounding_client_rect.x(), EvalJs(web_contents, "rect.x;"));
++  EXPECT_EQ(bounding_client_rect.y(), EvalJs(web_contents, "rect.y"));
++  EXPECT_EQ(bounding_client_rect.width(), EvalJs(web_contents, "rect.width"));
++  EXPECT_EQ(bounding_client_rect.height(), EvalJs(web_contents, "rect.height"));
++}
++
++#if !defined(OS_ANDROID)
++IN_PROC_BROWSER_TEST_F(WebContentsImplBrowserTestWindowControlsOverlay,
++                       ValidatePageScaleChangesInfoAndFiresEvent) {
++  auto* web_contents = shell()->web_contents();
++  GURL url(
++      R"(data:text/html,<body><div id=target style="position=absolute;
++      left: env(titlebar-area-x, 60px);
++      top: env(titlebar-area-y, 60px);
++      width: env(titlebar-area-width, 60px);
++      height: env(titlebar-area-height, 60px);"></div></body>)");
++
++  EXPECT_TRUE(NavigateToURL(shell(), url));
++  WaitForLoadStop(web_contents);
++
++  gfx::Rect bounding_client_rect = gfx::Rect(5, 10, 15, 20);
++  WaitForWindowControlsOverlayUpdate(web_contents, bounding_client_rect);
++
++  // Update zoom level, confirm the "geometrychange" event is fired,
++  // and CSS variables are updated
++  EXPECT_TRUE(
++      ExecJs(web_contents->GetMainFrame(),
++             "geometrychangeCount = 0;"
++             "navigator.windowControlsOverlay.ongeometrychange = (e) => {"
++             "  geometrychangeCount++;"
++             "  rect = e.boundingRect;"
++             "  visible = e.visible;"
++             "  document.title = 'ongeometrychangefromzoomlevel'"
++             "}"));
++  content::HostZoomMap::SetZoomLevel(web_contents, 1.5);
++  TitleWatcher title_watcher(web_contents, u"ongeometrychangefromzoomlevel");
++  ignore_result(title_watcher.WaitAndGetTitle());
++
++  // Validate the event payload.
++  double zoom_factor = blink::PageZoomLevelToZoomFactor(
++      content::HostZoomMap::GetZoomLevel(web_contents));
++  gfx::Rect scaled_rect =
++      gfx::ScaleToEnclosingRectSafe(bounding_client_rect, 1.0f / zoom_factor);
++
++  EXPECT_EQ(true, EvalJs(web_contents, "visible"));
++  EXPECT_EQ(scaled_rect.x(), EvalJs(web_contents, "rect.x"));
++  EXPECT_EQ(scaled_rect.y(), EvalJs(web_contents, "rect.y"));
++  EXPECT_EQ(scaled_rect.width(), EvalJs(web_contents, "rect.width"));
++  EXPECT_EQ(scaled_rect.height(), EvalJs(web_contents, "rect.height"));
++  ValidateWindowsControlOverlayState(web_contents, scaled_rect, 60);
++}
++#endif
++
++class WebContentsImplBrowserTestWindowControlsOverlayNonOneDeviceScaleFactor
++    : public WebContentsImplBrowserTestWindowControlsOverlay {
++ public:
++  void SetUp() override {
++#if defined(OS_MAC)
++    // Device scale factor on MacOSX is always an integer.
++    EnablePixelOutput(2.0f);
++#else
++    EnablePixelOutput(1.25f);
++#endif
++    WebContentsImplBrowserTestWindowControlsOverlay::SetUp();
++  }
++};
++
++IN_PROC_BROWSER_TEST_F(
++    WebContentsImplBrowserTestWindowControlsOverlayNonOneDeviceScaleFactor,
++    ValidateScaledCorrectly) {
++  auto* web_contents = shell()->web_contents();
++  GURL url(
++      R"(data:text/html,<body><div id=target style="position=absolute;
++      left: env(titlebar-area-x, 70px);
++      top: env(titlebar-area-y, 70px);
++      width: env(titlebar-area-width, 70px);
++      height: env(titlebar-area-height, 70px);"></div></body>)");
++
++  EXPECT_TRUE(NavigateToURL(shell(), url));
++  WaitForLoadStop(web_contents);
++#if defined(OS_MAC)
++  // Device scale factor on MacOSX is always an integer.
++  ASSERT_EQ(2.0f,
++            web_contents->GetRenderWidgetHostView()->GetDeviceScaleFactor());
++#else
++  ASSERT_EQ(1.25f,
++            web_contents->GetRenderWidgetHostView()->GetDeviceScaleFactor());
++#endif
++
++  gfx::Rect bounding_client_rect = gfx::Rect(5, 10, 15, 20);
++  WaitForWindowControlsOverlayUpdate(web_contents, bounding_client_rect);
++  ValidateWindowsControlOverlayState(web_contents, bounding_client_rect, 70);
+ }
+ }  // namespace content
+diff --git a/third_party/blink/common/widget/visual_properties.cc b/third_party/blink/common/widget/visual_properties.cc
+index 433ca5954c9f316905f289948ab2e4ebe66b7833..55932091bafe8959c855529d49b9f66cd6e386f0 100644
+--- a/third_party/blink/common/widget/visual_properties.cc
++++ b/third_party/blink/common/widget/visual_properties.cc
+@@ -33,7 +33,8 @@ bool VisualProperties::operator==(const VisualProperties& other) const {
+          page_scale_factor == other.page_scale_factor &&
+          compositing_scale_factor == other.compositing_scale_factor &&
+          root_widget_window_segments == other.root_widget_window_segments &&
+-         is_pinch_gesture_active == other.is_pinch_gesture_active;
++         is_pinch_gesture_active == other.is_pinch_gesture_active &&
++         window_controls_overlay_rect == other.window_controls_overlay_rect;
+ }
+ 
+ bool VisualProperties::operator!=(const VisualProperties& other) const {
+diff --git a/third_party/blink/common/widget/visual_properties_mojom_traits.cc b/third_party/blink/common/widget/visual_properties_mojom_traits.cc
+index d378def431a2643de08951ff861b68868b1d7250..262eec364918a668a2f5e65af2044c24d3380aa7 100644
+--- a/third_party/blink/common/widget/visual_properties_mojom_traits.cc
++++ b/third_party/blink/common/widget/visual_properties_mojom_traits.cc
+@@ -24,6 +24,7 @@ bool StructTraits<
+       !data.ReadBrowserControlsParams(&out->browser_controls_params) ||
+       !data.ReadLocalSurfaceId(&out->local_surface_id) ||
+       !data.ReadRootWidgetWindowSegments(&out->root_widget_window_segments) ||
++      !data.ReadWindowControlsOverlayRect(&out->window_controls_overlay_rect) ||
+       data.page_scale_factor() <= 0 || data.compositing_scale_factor() <= 0)
+     return false;
+   out->auto_resize_enabled = data.auto_resize_enabled();
+diff --git a/third_party/blink/public/common/widget/visual_properties.h b/third_party/blink/public/common/widget/visual_properties.h
+index 3c16c86e704558b40e00b40264a4d7018d89fb5e..e020adae74d1f061bbbfc5bc10e8a40a69f93410 100644
+--- a/third_party/blink/public/common/widget/visual_properties.h
++++ b/third_party/blink/public/common/widget/visual_properties.h
+@@ -129,6 +129,13 @@ struct BLINK_COMMON_EXPORT VisualProperties {
+   // main frame's renderer, and needs to be shared with subframes.
+   bool is_pinch_gesture_active = false;
+ 
++  // The rect of the Windows Control Overlay, which contains system UX
++  // affordances (e.g. close), for installed desktop Progress Web Apps (PWAs),
++  // if the app specifies the 'window-controls-overlay' DisplayMode in its
++  // manifest. This is only valid and to be consumed by the outermost main
++  // frame.
++  gfx::Rect window_controls_overlay_rect;
++
+   VisualProperties();
+   VisualProperties(const VisualProperties& other);
+   ~VisualProperties();
+diff --git a/third_party/blink/public/common/widget/visual_properties_mojom_traits.h b/third_party/blink/public/common/widget/visual_properties_mojom_traits.h
+index f6634310fd17acc7299db892d68aea770578a0f1..8d7ab89e5d434e4098a55c6b78d06bfb6f3faa29 100644
+--- a/third_party/blink/public/common/widget/visual_properties_mojom_traits.h
++++ b/third_party/blink/public/common/widget/visual_properties_mojom_traits.h
+@@ -97,6 +97,11 @@ struct BLINK_COMMON_EXPORT StructTraits<blink::mojom::VisualPropertiesDataView,
+     return r.is_pinch_gesture_active;
+   }
+ 
++  static const gfx::Rect& window_controls_overlay_rect(
++      const blink::VisualProperties& r) {
++    return r.window_controls_overlay_rect;
++  }
++
+   static bool Read(blink::mojom::VisualPropertiesDataView r,
+                    blink::VisualProperties* out);
+ };
+diff --git a/third_party/blink/public/mojom/frame/frame.mojom b/third_party/blink/public/mojom/frame/frame.mojom
+index add4a22fe76818d5fa7c124f85a781da387ba3f4..cbbbdfd799135d8c86f9f2eecd558771627991f2 100644
+--- a/third_party/blink/public/mojom/frame/frame.mojom
++++ b/third_party/blink/public/mojom/frame/frame.mojom
+@@ -1116,10 +1116,6 @@ interface LocalMainFrame {
+   UpdateBrowserControlsState(cc.mojom.BrowserControlsState constraints,
+                              cc.mojom.BrowserControlsState current,
+                              bool animate);
+-
+-  // Notify renderer that the window controls overlay has changed size or
+-  // visibility.
+-  UpdateWindowControlsOverlay(gfx.mojom.Rect window_controls_overlay_rect);
+ };
+ 
+ // Implemented in Blink, this interface defines remote main-frame-specific
+diff --git a/third_party/blink/public/mojom/widget/visual_properties.mojom b/third_party/blink/public/mojom/widget/visual_properties.mojom
+index b2fe7bf659bcfdc183e57ad7c4e45f1c422a246f..43a4874cfae908754c476a508544154e1088634e 100644
+--- a/third_party/blink/public/mojom/widget/visual_properties.mojom
++++ b/third_party/blink/public/mojom/widget/visual_properties.mojom
+@@ -92,4 +92,12 @@ struct VisualProperties {
+   // Indicates whether a pinch gesture is currently active. Originates in the
+   // main frame's renderer, and needs to be shared with subframes.
+   bool is_pinch_gesture_active;
++
++  // The rect of the Windows Control Overlay, which contains system UX
++  // affordances (e.g. close), for installed desktop Progress Web Apps (PWAs),
++  // if the app specifies the 'window-controls-overlay' DisplayMode in its
++  // manifest. This is only valid and to be consumed by the outermost main
++  // frame.
++  gfx.mojom.Rect window_controls_overlay_rect;
++
+ };
+diff --git a/third_party/blink/renderer/core/frame/local_frame.cc b/third_party/blink/renderer/core/frame/local_frame.cc
+index d6cf1cbb0beb65f0003051f67d5fa5b723c77a74..5f991049ea325368543d134cbe95614625b30004 100644
+--- a/third_party/blink/renderer/core/frame/local_frame.cc
++++ b/third_party/blink/renderer/core/frame/local_frame.cc
+@@ -2930,31 +2930,71 @@ void LocalFrame::UpdateBrowserControlsState(
+ }
+ 
+ void LocalFrame::UpdateWindowControlsOverlay(
+-    const gfx::Rect& window_controls_overlay_rect) {
++
++    const gfx::Rect& bounding_rect_in_dips) {
++  if (!RuntimeEnabledFeatures::WebAppWindowControlsOverlayEnabled(nullptr))
++    return;
++
++  // The rect passed to us from content is in DIP screen space, relative to the
++  // main frame, and needs to move to CSS space. This doesn't take the page's
++  // zoom factor into account so we must scale by the inverse of the page zoom
++  // in order to get correct CSS space coordinates. Note that when
++  // use-zoom-for-dsf is enabled, WindowToViewportScalar will be the true device
++  // scale factor, and PageZoomFactor will be the combination of the device
++  // scale factor and the zoom percent of the page. It is preferable to compute
++  // a rect that is slightly larger than one that would render smaller than the
++  // window control overlay.
++  LocalFrame& local_frame_root = LocalFrameRoot();
++  const float window_to_viewport_factor =
++      GetPage()->GetChromeClient().WindowToViewportScalar(&local_frame_root,
++                                                          1.0f);
++  const float zoom_factor = local_frame_root.PageZoomFactor();
++  const float scale_factor = zoom_factor / window_to_viewport_factor;
++  gfx::Rect window_controls_overlay_rect =
++      gfx::ScaleToEnclosingRectSafe(bounding_rect_in_dips, 1.0f / scale_factor);
++
++  bool fire_event =
++      (window_controls_overlay_rect != window_controls_overlay_rect_);
++
+   is_window_controls_overlay_visible_ = !window_controls_overlay_rect.IsEmpty();
+   window_controls_overlay_rect_ = window_controls_overlay_rect;
+ 
+   DocumentStyleEnvironmentVariables& vars =
+       GetDocument()->GetStyleEngine().EnsureEnvironmentVariables();
+-  vars.SetVariable(
+-      UADefinedVariable::kTitlebarAreaX,
+-      StyleEnvironmentVariables::FormatPx(window_controls_overlay_rect_.x()));
+-  vars.SetVariable(
+-      UADefinedVariable::kTitlebarAreaY,
+-      StyleEnvironmentVariables::FormatPx(window_controls_overlay_rect_.y()));
+-  vars.SetVariable(UADefinedVariable::kTitlebarAreaWidth,
+-                   StyleEnvironmentVariables::FormatPx(
+-                       window_controls_overlay_rect_.width()));
+-  vars.SetVariable(UADefinedVariable::kTitlebarAreaHeight,
+-                   StyleEnvironmentVariables::FormatPx(
+-                       window_controls_overlay_rect_.height()));
+-
+-  auto* window_controls_overlay =
+-      WindowControlsOverlay::FromIfExists(*DomWindow()->navigator());
+-
+-  if (window_controls_overlay) {
+-    window_controls_overlay->WindowControlsOverlayChanged(
+-        window_controls_overlay_rect);
++
++  if (is_window_controls_overlay_visible_) {
++    vars.SetVariable(
++        UADefinedVariable::kTitlebarAreaX,
++        StyleEnvironmentVariables::FormatPx(window_controls_overlay_rect_.x()));
++    vars.SetVariable(
++        UADefinedVariable::kTitlebarAreaY,
++        StyleEnvironmentVariables::FormatPx(window_controls_overlay_rect_.y()));
++    vars.SetVariable(UADefinedVariable::kTitlebarAreaWidth,
++                     StyleEnvironmentVariables::FormatPx(
++                         window_controls_overlay_rect_.width()));
++    vars.SetVariable(UADefinedVariable::kTitlebarAreaHeight,
++                     StyleEnvironmentVariables::FormatPx(
++                         window_controls_overlay_rect_.height()));
++  } else {
++    const UADefinedVariable vars_to_remove[] = {
++        UADefinedVariable::kTitlebarAreaX,
++        UADefinedVariable::kTitlebarAreaY,
++        UADefinedVariable::kTitlebarAreaWidth,
++        UADefinedVariable::kTitlebarAreaHeight,
++    };
++    for (auto var_to_remove : vars_to_remove) {
++      vars.RemoveVariable(StyleEnvironmentVariables::GetVariableName(var_to_remove));
++    }
++  }
++
++  if (fire_event) {
++    auto* window_controls_overlay =
++        WindowControlsOverlay::FromIfExists(*DomWindow()->navigator());
++
++    if (window_controls_overlay) {
++      window_controls_overlay->WindowControlsOverlayChanged(
++          window_controls_overlay_rect_);
++    }
+   }
+ }
+ 
+diff --git a/third_party/blink/renderer/core/frame/local_frame.h b/third_party/blink/renderer/core/frame/local_frame.h
+index 38ad1f729ffc7416ae56c771f5b518fb63520c08..d725460b26f25ca5746126f76efdfc8722943492 100644
+--- a/third_party/blink/renderer/core/frame/local_frame.h
++++ b/third_party/blink/renderer/core/frame/local_frame.h
+@@ -732,8 +732,7 @@ class CORE_EXPORT LocalFrame final
+   void UpdateBrowserControlsState(cc::BrowserControlsState constraints,
+                                   cc::BrowserControlsState current,
+                                   bool animate) override;
+-  void UpdateWindowControlsOverlay(
+-      const gfx::Rect& window_controls_overlay_rect) override;
++  void UpdateWindowControlsOverlay(const gfx::Rect& bounding_rect_in_dips);
+ 
+   // mojom::FullscreenVideoElementHandler implementation:
+   void RequestFullscreenVideoElement() final;
+diff --git a/third_party/blink/renderer/core/frame/web_frame_widget_impl.cc b/third_party/blink/renderer/core/frame/web_frame_widget_impl.cc
+index 1f13dc8bce4a41b96bb2bfce776d6b55500db5b6..d33496ce445cb2af4b21cdd23bdc011d6214b352 100644
+--- a/third_party/blink/renderer/core/frame/web_frame_widget_impl.cc
++++ b/third_party/blink/renderer/core/frame/web_frame_widget_impl.cc
+@@ -1516,6 +1516,10 @@ void WebFrameWidgetImpl::ApplyVisualPropertiesSizing(
+               widget_base_->VisibleViewportSizeInDIPs()),
+           visual_properties.browser_controls_params);
+     }
++
++    LocalRootImpl()->GetFrame()->UpdateWindowControlsOverlay(
++        visual_properties.window_controls_overlay_rect);
++
+   } else {
+     // Widgets in a WebView's frame tree without a local main frame
+     // set the size of the WebView to be the |visible_viewport_size|, in order
+diff --git a/third_party/blink/tools/blinkpy/presubmit/audit_non_blink_usage.py b/third_party/blink/tools/blinkpy/presubmit/audit_non_blink_usage.py
+index 2816268f68d8910b11c5b6ea6d0c2a1a92bd2e1a..ac95c2f8e18081cca7a2c14899c9d7a9444fa565 100755
+--- a/third_party/blink/tools/blinkpy/presubmit/audit_non_blink_usage.py
++++ b/third_party/blink/tools/blinkpy/presubmit/audit_non_blink_usage.py
+@@ -279,6 +279,7 @@ _CONFIG = [
+             'gfx::RectF',
+             'gfx::RRectF',
+             'gfx::ScaleToCeiledSize',
++            'gfx::ScaleToEnclosingRectSafe',
+             'gfx::ScaleVector2d',
+             'gfx::Size',
+             'gfx::SizeF',

+ 1727 - 0
patches/chromium/reland_make_clientview_a_child_of_the_nonclientframeview.patch

@@ -0,0 +1,1727 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Amanda Baker <[email protected]>
+Date: Sat, 10 Apr 2021 00:00:15 +0000
+Subject: Reland "Make ClientView a child of the NonClientFrameView"
+
+This is a reland of ec39b1ddb4cca45bf582ca55ad1585ceb51d9306
+
+Original change's description:
+> Make ClientView a child of the NonClientFrameView
+>
+> This refactor changes the hierarchy of views under
+> NonClientView. Previously, NonClientFrameView and ClientView were
+> siblings under NonClientView. This will change the hierarchy to:
+> NonClientView > NonClientFrameView > ClientView.
+>
+> This change also enables a cleaner implementation of the Window
+> Controls Overlay (see before (https://crrev.com/c/2504573) vs
+> after (https://crrev.com/c/2545685))
+>
+> Window controls overlay links:
+> Explainer: https://github.com/WICG/window-controls-overlay/blob/master/explainer.md
+> Design Doc: https://docs.google.com/document/d/1k0YL_-VMLIfjYCgJ2v6cMvuUv2qMKg4BgLI2tJ4qtyo/edit?usp=sharing
+> I2P: https://groups.google.com/a/chromium.org/forum/#!msg/blink-dev/cper6nNLFRQ/hU91kfCWBQAJ
+>
+> Bug: 1175276, 937121
+> Change-Id: If16de54a858f571c628b66c7801ef3777c6cc924
+> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2522616
+> Commit-Queue: Amanda Baker <[email protected]>
+> Reviewed-by: Mitsuru Oshima <[email protected]>
+> Reviewed-by: Matt Giuca <[email protected]>
+> Reviewed-by: David Tseng <[email protected]>
+> Reviewed-by: Leonard Grey <[email protected]>
+> Reviewed-by: Peter Kasting <[email protected]>
+> Cr-Commit-Position: refs/heads/master@{#864068}
+
+Bug: 1175276
+Bug: 937121
+Change-Id: Ib8919739dd685a4ea20b56fd241750678c38a9c0
+Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2780032
+Reviewed-by: Leonard Grey <[email protected]>
+Reviewed-by: Mitsuru Oshima <[email protected]>
+Reviewed-by: Peter Kasting <[email protected]>
+Reviewed-by: Matt Giuca <[email protected]>
+Reviewed-by: David Tseng <[email protected]>
+Commit-Queue: Amanda Baker <[email protected]>
+Cr-Commit-Position: refs/heads/master@{#871173}
+
+diff --git a/apps/ui/views/app_window_frame_view.cc b/apps/ui/views/app_window_frame_view.cc
+index 1a73ae216937d4329ff38f3af8ca2c8f395fa1aa..f96d7bec703431ddc2ba67c4a8e588c361d859c9 100644
+--- a/apps/ui/views/app_window_frame_view.cc
++++ b/apps/ui/views/app_window_frame_view.cc
+@@ -249,8 +249,11 @@ gfx::Size AppWindowFrameView::CalculatePreferredSize() const {
+ }
+ 
+ void AppWindowFrameView::Layout() {
++  NonClientFrameView::Layout();
++
+   if (!draw_frame_)
+     return;
++
+   gfx::Size close_size = close_button_->GetPreferredSize();
+   const int kButtonOffsetY = 0;
+   const int kButtonSpacing = 1;
+diff --git a/ash/app_list/views/search_box_view_unittest.cc b/ash/app_list/views/search_box_view_unittest.cc
+index 8e548da40dd51e15df17bc8550340ebcf0ca4cad..748307a88e512e8237cb124ddb31d1f6129e2ff2 100644
+--- a/ash/app_list/views/search_box_view_unittest.cc
++++ b/ash/app_list/views/search_box_view_unittest.cc
+@@ -92,9 +92,9 @@ class SearchBoxViewTest : public views::test::WidgetTest,
+   }
+ 
+   void TearDown() override {
+-    view_.reset();
+     app_list_view_->GetWidget()->Close();
+     widget_->CloseNow();
++    view_.reset();
+     views::test::WidgetTest::TearDown();
+   }
+ 
+diff --git a/ash/frame/default_frame_header_unittest.cc b/ash/frame/default_frame_header_unittest.cc
+index 1299e1993e7d0986a553e69865bf950a1313afd5..28b5ccfb5900c0c1240e1debdde95174bf752a54 100644
+--- a/ash/frame/default_frame_header_unittest.cc
++++ b/ash/frame/default_frame_header_unittest.cc
+@@ -277,7 +277,7 @@ TEST_F(DefaultFrameHeaderTest, ResizeAndReorderDuringAnimation) {
+     LayerDestroyedChecker checker(animating_layer);
+ 
+     // Change the view's stacking order should stop the animation.
+-    ASSERT_EQ(2u, frame_view_1->children().size());
++    ASSERT_EQ(3u, frame_view_1->children().size());
+     frame_view_1->ReorderChildView(extra_view_1, 0);
+ 
+     EXPECT_EQ(
+diff --git a/ash/frame/non_client_frame_view_ash.cc b/ash/frame/non_client_frame_view_ash.cc
+index 1f8a6931448f9fe7b7e364c02c01f6782c0873a3..bc73d9da6b020b39da1b56a9e42d3aac9ae5ea12 100644
+--- a/ash/frame/non_client_frame_view_ash.cc
++++ b/ash/frame/non_client_frame_view_ash.cc
+@@ -327,16 +327,16 @@ gfx::Size NonClientFrameViewAsh::CalculatePreferredSize() const {
+ }
+ 
+ void NonClientFrameViewAsh::Layout() {
+-  if (!GetEnabled())
+-    return;
+   views::NonClientFrameView::Layout();
++  if (!GetFrameEnabled())
++    return;
+   aura::Window* frame_window = frame_->GetNativeWindow();
+   frame_window->SetProperty(aura::client::kTopViewInset,
+                             NonClientTopBorderHeight());
+ }
+ 
+ gfx::Size NonClientFrameViewAsh::GetMinimumSize() const {
+-  if (!GetEnabled())
++  if (!GetFrameEnabled())
+     return gfx::Size();
+ 
+   gfx::Size min_client_view_size(frame_->client_view()->GetMinimumSize());
+@@ -358,13 +358,6 @@ gfx::Size NonClientFrameViewAsh::GetMaximumSize() const {
+   return gfx::Size(width, height);
+ }
+ 
+-void NonClientFrameViewAsh::SetVisible(bool visible) {
+-  overlay_view_->SetVisible(visible);
+-  views::View::SetVisible(visible);
+-  // We need to re-layout so that client view will occupy entire window.
+-  InvalidateLayout();
+-}
+-
+ void NonClientFrameViewAsh::SetShouldPaintHeader(bool paint) {
+   header_view_->SetShouldPaintHeader(paint);
+ }
+@@ -372,7 +365,7 @@ void NonClientFrameViewAsh::SetShouldPaintHeader(bool paint) {
+ int NonClientFrameViewAsh::NonClientTopBorderHeight() const {
+   // The frame should not occupy the window area when it's in fullscreen,
+   // not visible or disabled.
+-  if (frame_->IsFullscreen() || !GetVisible() || !GetEnabled() ||
++  if (frame_->IsFullscreen() || !GetFrameEnabled() ||
+       header_view_->in_immersive_mode()) {
+     return 0;
+   }
+@@ -395,6 +388,15 @@ SkColor NonClientFrameViewAsh::GetInactiveFrameColorForTest() const {
+   return frame_->GetNativeWindow()->GetProperty(kFrameInactiveColorKey);
+ }
+ 
++void NonClientFrameViewAsh::SetFrameEnabled(bool enabled) {
++  if (enabled == frame_enabled_)
++    return;
++
++  frame_enabled_ = enabled;
++  overlay_view_->SetVisible(frame_enabled_);
++  InvalidateLayout();
++}
++
+ void NonClientFrameViewAsh::OnDidSchedulePaint(const gfx::Rect& r) {
+   // We may end up here before |header_view_| has been added to the Widget.
+   if (header_view_->GetWidget()) {
+@@ -410,9 +412,18 @@ void NonClientFrameViewAsh::OnDidSchedulePaint(const gfx::Rect& r) {
+ bool NonClientFrameViewAsh::DoesIntersectRect(const views::View* target,
+                                               const gfx::Rect& rect) const {
+   CHECK_EQ(target, this);
+-  // NonClientView hit tests the NonClientFrameView first instead of going in
+-  // z-order. Return false so that events get to the OverlayView.
+-  return false;
++
++  // Give the OverlayView the first chance to handle events.
++  if (frame_enabled_ && overlay_view_->HitTestRect(rect))
++    return false;
++
++  // Handle the event if it's within the bounds of the ClientView.
++  gfx::RectF rect_in_client_view_coords_f(rect);
++  View::ConvertRectToTarget(this, frame_->client_view(),
++                            &rect_in_client_view_coords_f);
++  gfx::Rect rect_in_client_view_coords =
++      gfx::ToEnclosingRect(rect_in_client_view_coords_f);
++  return frame_->client_view()->HitTestRect(rect_in_client_view_coords);
+ }
+ 
+ chromeos::FrameCaptionButtonContainerView*
+diff --git a/ash/frame/non_client_frame_view_ash.h b/ash/frame/non_client_frame_view_ash.h
+index ac2633a19af8939d9655a09549d7c6c567e00a6e..c5efb7933d3077f75ccb7296ac873cdd1a1c9ea4 100644
+--- a/ash/frame/non_client_frame_view_ash.h
++++ b/ash/frame/non_client_frame_view_ash.h
+@@ -90,7 +90,6 @@ class ASH_EXPORT NonClientFrameViewAsh : public views::NonClientFrameView {
+   void Layout() override;
+   gfx::Size GetMinimumSize() const override;
+   gfx::Size GetMaximumSize() const override;
+-  void SetVisible(bool visible) override;
+ 
+   // If |paint| is false, we should not paint the header. Used for overview mode
+   // with OnOverviewModeStarting() and OnOverviewModeEnded() to hide/show the
+@@ -111,6 +110,9 @@ class ASH_EXPORT NonClientFrameViewAsh : public views::NonClientFrameView {
+ 
+   views::Widget* frame() { return frame_; }
+ 
++  bool GetFrameEnabled() const { return frame_enabled_; }
++  virtual void SetFrameEnabled(bool enabled);
++
+  protected:
+   // views::View:
+   void OnDidSchedulePaint(const gfx::Rect& r) override;
+@@ -141,6 +143,8 @@ class ASH_EXPORT NonClientFrameViewAsh : public views::NonClientFrameView {
+ 
+   OverlayView* overlay_view_ = nullptr;
+ 
++  bool frame_enabled_ = true;
++
+   std::unique_ptr<NonClientFrameViewAshImmersiveHelper> immersive_helper_;
+ 
+   base::CallbackListSubscription paint_as_active_subscription_ =
+diff --git a/ash/frame/non_client_frame_view_ash_unittest.cc b/ash/frame/non_client_frame_view_ash_unittest.cc
+index 67093a915488503984e01a43d47964ad7eafc89c..b1b0f64e0b47d558ae06d4e444b531efd54ab5e3 100644
+--- a/ash/frame/non_client_frame_view_ash_unittest.cc
++++ b/ash/frame/non_client_frame_view_ash_unittest.cc
+@@ -543,19 +543,19 @@ TEST_F(NonClientFrameViewAshTest, FrameVisibility) {
+       delegate->non_client_frame_view();
+   EXPECT_EQ(client_bounds, widget->client_view()->GetLocalBounds().size());
+ 
+-  non_client_frame_view->SetVisible(false);
++  non_client_frame_view->SetFrameEnabled(false);
+   widget->GetRootView()->Layout();
+   EXPECT_EQ(gfx::Size(200, 100),
+             widget->client_view()->GetLocalBounds().size());
+-  EXPECT_FALSE(widget->non_client_view()->frame_view()->GetVisible());
++  EXPECT_FALSE(non_client_frame_view->GetFrameEnabled());
+   EXPECT_EQ(
+       window_bounds,
+       non_client_frame_view->GetClientBoundsForWindowBounds(window_bounds));
+ 
+-  non_client_frame_view->SetVisible(true);
++  non_client_frame_view->SetFrameEnabled(true);
+   widget->GetRootView()->Layout();
+   EXPECT_EQ(client_bounds, widget->client_view()->GetLocalBounds().size());
+-  EXPECT_TRUE(widget->non_client_view()->frame_view()->GetVisible());
++  EXPECT_TRUE(non_client_frame_view->GetFrameEnabled());
+   EXPECT_EQ(32, delegate->GetNonClientFrameViewTopBorderHeight());
+   EXPECT_EQ(
+       gfx::Rect(gfx::Point(10, 42), client_bounds),
+diff --git a/ash/hud_display/hud_display.cc b/ash/hud_display/hud_display.cc
+index ad7d615f0cd2cf4e897f98deac7aa08915f4434f..d9ba5e54e6de749f846a6376e25e746eafa013cc 100644
+--- a/ash/hud_display/hud_display.cc
++++ b/ash/hud_display/hud_display.cc
+@@ -6,6 +6,7 @@
+ 
+ #include "ash/fast_ink/view_tree_host_root_view.h"
+ #include "ash/fast_ink/view_tree_host_widget.h"
++#include "ash/frame/non_client_frame_view_ash.h"
+ #include "ash/hud_display/graphs_container_view.h"
+ #include "ash/hud_display/hud_constants.h"
+ #include "ash/hud_display/hud_header_view.h"
+@@ -82,12 +83,11 @@ std::unique_ptr<views::ClientView> MakeClientView(views::Widget* widget) {
+ }
+ 
+ void InitializeFrameView(views::WidgetDelegate* delegate) {
+-  auto* frame_view = delegate->GetWidget()->non_client_view()->frame_view();
++  auto* frame_view = static_cast<NonClientFrameViewAsh*>(
++      delegate->GetWidget()->non_client_view()->frame_view());
+   // TODO(oshima): support component type with TYPE_WINDOW_FLAMELESS widget.
+-  if (frame_view) {
+-    frame_view->SetEnabled(false);
+-    frame_view->SetVisible(false);
+-  }
++  if (frame_view)
++    frame_view->SetFrameEnabled(false);
+ }
+ 
+ }  // namespace
+diff --git a/ash/system/holding_space/holding_space_tray_bubble.cc b/ash/system/holding_space/holding_space_tray_bubble.cc
+index 1ffb354110867722be54898d2f045f1707af7f4b..5e55fbe7b361f9e9ca6ffaa1d20fbab79090724a 100644
+--- a/ash/system/holding_space/holding_space_tray_bubble.cc
++++ b/ash/system/holding_space/holding_space_tray_bubble.cc
+@@ -319,6 +319,8 @@ HoldingSpaceTrayBubble::HoldingSpaceTrayBubble(
+ 
+   // Create and customize bubble view.
+   TrayBubbleView* bubble_view = new TrayBubbleView(init_params);
++  // Ensure bubble frame does not draw background behind bubble view.
++  bubble_view->set_color(SK_ColorTRANSPARENT);
+   child_bubble_container_ =
+       bubble_view->AddChildView(std::make_unique<ChildBubbleContainer>());
+   child_bubble_container_->SetMaxHeight(CalculateMaxHeight());
+@@ -339,12 +341,6 @@ HoldingSpaceTrayBubble::HoldingSpaceTrayBubble(
+   bubble_wrapper_ =
+       std::make_unique<TrayBubbleWrapper>(holding_space_tray, bubble_view);
+ 
+-  // Set bubble frame to be invisible.
+-  bubble_wrapper_->GetBubbleWidget()
+-      ->non_client_view()
+-      ->frame_view()
+-      ->SetVisible(false);
+-
+   event_handler_ =
+       std::make_unique<HoldingSpaceTrayBubbleEventHandler>(this, &delegate_);
+ 
+diff --git a/ash/wm/system_modal_container_layout_manager_unittest.cc b/ash/wm/system_modal_container_layout_manager_unittest.cc
+index 1723e7ea3ddaf9dd232765b07ccbce0d88bdf4c4..e8a5f8db3583782e139341d5bf7f791aaa98709e 100644
+--- a/ash/wm/system_modal_container_layout_manager_unittest.cc
++++ b/ash/wm/system_modal_container_layout_manager_unittest.cc
+@@ -547,11 +547,12 @@ TEST_F(SystemModalContainerLayoutManagerTest, ShowModalWhileHidden) {
+ TEST_F(SystemModalContainerLayoutManagerTest, ChangeCapture) {
+   std::unique_ptr<aura::Window> widget_window(ShowToplevelTestWindow(false));
+   views::test::CaptureTrackingView* view = new views::test::CaptureTrackingView;
+-  views::View* contents_view =
++  views::View* client_view =
+       views::Widget::GetWidgetForNativeView(widget_window.get())
+-          ->GetContentsView();
+-  contents_view->AddChildView(view);
+-  view->SetBoundsRect(contents_view->bounds());
++          ->non_client_view()
++          ->client_view();
++  client_view->AddChildView(view);
++  view->SetBoundsRect(client_view->bounds());
+ 
+   gfx::Point center(view->width() / 2, view->height() / 2);
+   views::View::ConvertPointToScreen(view, &center);
+diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/background_test.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/background_test.js
+index c790c2b55c3a1757bb9d6afe200796e4cc66178f..9d6a58dfc034413a36646ea8520ed62faa609e93 100644
+--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/background_test.js
++++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/background_test.js
+@@ -3047,7 +3047,7 @@ TEST_F('ChromeVoxBackgroundTest', 'ImageAnnotations', function() {
+ 
+ TEST_F('ChromeVoxBackgroundTest', 'VolumeChanges', function() {
+   const mockFeedback = this.createMockFeedback();
+-  this.runWithLoadedTree(``, function() {
++  this.runWithLoadedTree('<p>test</p>', function() {
+     const bounds = ChromeVoxState.instance.getFocusBounds();
+     mockFeedback.call(press(KeyCode.VOLUME_UP))
+         .expectSpeech('Volume', 'Slider', /\d+%/)
+diff --git a/chrome/browser/ui/views/apps/app_info_dialog/app_info_dialog_container.cc b/chrome/browser/ui/views/apps/app_info_dialog/app_info_dialog_container.cc
+index 180167b310fdfe5392c8b0f919cc2ce0d6556b3d..c32e8027d855505b5413c884b341bdf542ebcc42 100644
+--- a/chrome/browser/ui/views/apps/app_info_dialog/app_info_dialog_container.cc
++++ b/chrome/browser/ui/views/apps/app_info_dialog/app_info_dialog_container.cc
+@@ -89,21 +89,6 @@ class FullSizeBubbleFrameView : public views::BubbleFrameView {
+   ~FullSizeBubbleFrameView() override = default;
+ 
+  private:
+-  // Overridden from views::ViewTargeterDelegate:
+-  bool DoesIntersectRect(const View* target,
+-                         const gfx::Rect& rect) const override {
+-    // Make sure click events can still reach the close button, even if the
+-    // ClientView overlaps it.
+-    // NOTE: |rect| is in the mirrored coordinate space, so we must use the
+-    // close button's mirrored bounds to correctly target the close button when
+-    // in RTL mode.
+-    if (IsCloseButtonVisible() &&
+-        GetCloseButtonMirroredBounds().Intersects(rect)) {
+-      return true;
+-    }
+-    return views::BubbleFrameView::DoesIntersectRect(target, rect);
+-  }
+-
+   // Overridden from views::BubbleFrameView:
+   bool ExtendClientIntoTitle() const override { return true; }
+ };
+diff --git a/chrome/browser/ui/views/frame/browser_non_client_frame_view.cc b/chrome/browser/ui/views/frame/browser_non_client_frame_view.cc
+index ca4a6ad167418b234bdae1e07ef398a7b6b97ed4..361cab54d78f5c0a9a6c6cdde0d002af6b358a9a 100644
+--- a/chrome/browser/ui/views/frame/browser_non_client_frame_view.cc
++++ b/chrome/browser/ui/views/frame/browser_non_client_frame_view.cc
+@@ -66,6 +66,11 @@ BrowserNonClientFrameView::~BrowserNonClientFrameView() {
+     g_browser_process->profile_manager()->
+         GetProfileAttributesStorage().RemoveObserver(this);
+   }
++
++  // WebAppFrameToolbarView::ToolbarButtonContainer is an
++  // ImmersiveModeController::Observer, so it must be destroyed before the
++  // BrowserView destroys the ImmersiveModeController.
++  delete web_app_frame_toolbar_;
+ }
+ 
+ void BrowserNonClientFrameView::OnBrowserViewInitViewsComplete() {
+@@ -294,68 +299,6 @@ void BrowserNonClientFrameView::ChildPreferredSizeChanged(views::View* child) {
+     Layout();
+ }
+ 
+-bool BrowserNonClientFrameView::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_ASH) || BUILDFLAG(IS_CHROMEOS_LACROS)
+-  // 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
+-
+-  if (!browser_view_->GetTabStripVisible()) {
+-    // Claim |rect| if it is above the top of the topmost client area view.
+-    return !should_leave_to_top_container && (rect.y() < GetTopInset(false));
+-  }
+-
+-  // If the rect is outside the bounds of the client area, claim it.
+-  gfx::RectF rect_in_client_view_coords_f(rect);
+-  View::ConvertRectToTarget(this, frame_->client_view(),
+-                            &rect_in_client_view_coords_f);
+-  gfx::Rect rect_in_client_view_coords =
+-      gfx::ToEnclosingRect(rect_in_client_view_coords_f);
+-  if (!frame_->client_view()->HitTestRect(rect_in_client_view_coords))
+-    return true;
+-
+-  // Otherwise, claim |rect| only if it is above the bottom of the tab strip
+-  // region view in a non-tab portion.
+-  TabStripRegionView* tab_strip_region_view =
+-      browser_view_->tab_strip_region_view();
+-
+-  // The |tab_strip_region_view| may not be in a Widget (e.g. when switching
+-  // into immersive reveal the BrowserView's TopContainerView is reparented).
+-  if (tab_strip_region_view->GetWidget()) {
+-    gfx::RectF rect_in_region_view_coords_f(rect);
+-    View::ConvertRectToTarget(this, tab_strip_region_view,
+-                              &rect_in_region_view_coords_f);
+-    gfx::Rect rect_in_region_view_coords =
+-        gfx::ToEnclosingRect(rect_in_region_view_coords_f);
+-    if (rect_in_region_view_coords.y() >=
+-        tab_strip_region_view->GetLocalBounds().bottom()) {
+-      // |rect| is below the tab_strip_region_view.
+-      return false;
+-    }
+-
+-    if (tab_strip_region_view->HitTestRect(rect_in_region_view_coords)) {
+-      // Claim |rect| if it is in a non-tab portion of the tabstrip.
+-      return tab_strip_region_view->IsRectInWindowCaption(
+-          rect_in_region_view_coords);
+-    }
+-  }
+-
+-  // We claim |rect| because it is above the bottom of the tabstrip, but
+-  // not in the tabstrip itself.
+-  return !should_leave_to_top_container;
+-}
+-
+ void BrowserNonClientFrameView::OnProfileAdded(
+     const base::FilePath& profile_path) {
+   OnProfileAvatarChanged(profile_path);
+diff --git a/chrome/browser/ui/views/frame/browser_non_client_frame_view.h b/chrome/browser/ui/views/frame/browser_non_client_frame_view.h
+index 8760b6b6239e7bc102305ab9280860ac847fedc3..875ba7daf98a8d59390fe3c2f0281f29d43f964d 100644
+--- a/chrome/browser/ui/views/frame/browser_non_client_frame_view.h
++++ b/chrome/browser/ui/views/frame/browser_non_client_frame_view.h
+@@ -157,8 +157,6 @@ class BrowserNonClientFrameView : public views::NonClientFrameView,
+ 
+   // views::NonClientFrameView:
+   void ChildPreferredSizeChanged(views::View* child) override;
+-  bool DoesIntersectRect(const views::View* target,
+-                         const gfx::Rect& rect) const override;
+ 
+   // ProfileAttributesStorage::Observer:
+   void OnProfileAdded(const base::FilePath& profile_path) override;
+diff --git a/chrome/browser/ui/views/frame/browser_non_client_frame_view_chromeos.cc b/chrome/browser/ui/views/frame/browser_non_client_frame_view_chromeos.cc
+index 945848b668620c5f037b219b6c76d35d851af291..05b1a9917da90ac9c2077f9fe39ce1418b04db1f 100644
+--- a/chrome/browser/ui/views/frame/browser_non_client_frame_view_chromeos.cc
++++ b/chrome/browser/ui/views/frame/browser_non_client_frame_view_chromeos.cc
+@@ -401,6 +401,27 @@ void BrowserNonClientFrameViewChromeOS::ChildPreferredSizeChanged(
+   }
+ }
+ 
++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_ASH) || BUILDFLAG(IS_CHROMEOS_LACROS)
++  // 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;
++}
++
+ SkColor BrowserNonClientFrameViewChromeOS::GetTitleColor() {
+   return browser_view()->GetRegularOrGuestSession()
+              ? kNormalWindowTitleTextColor
+@@ -561,7 +582,13 @@ void BrowserNonClientFrameViewChromeOS::OnImmersiveRevealStarted() {
+ }
+ 
+ void BrowserNonClientFrameViewChromeOS::OnImmersiveRevealEnded() {
+-  AddChildViewAt(caption_button_container_, 0);
++  // Ensure the caption button container receives events before the browser view
++  // by placing it higher in the z-order.
++  // [0] - FrameAnimatorView
++  // [1] - BrowserView
++  // [2] - FrameCaptionButtonContainerView
++  const int kCaptionButtonContainerIndex = 2;
++  AddChildViewAt(caption_button_container_, kCaptionButtonContainerIndex);
+   if (web_app_frame_toolbar())
+     AddChildViewAt(web_app_frame_toolbar(), 0);
+   Layout();
+diff --git a/chrome/browser/ui/views/frame/browser_non_client_frame_view_chromeos.h b/chrome/browser/ui/views/frame/browser_non_client_frame_view_chromeos.h
+index 97df69a7209fafdaf2597b46536da0743f87b7fd..4bbc1a599464e3556f0da4da917bae9b9e03884c 100644
+--- a/chrome/browser/ui/views/frame/browser_non_client_frame_view_chromeos.h
++++ b/chrome/browser/ui/views/frame/browser_non_client_frame_view_chromeos.h
+@@ -79,6 +79,8 @@ class BrowserNonClientFrameViewChromeOS
+   gfx::Size GetMinimumSize() const override;
+   void OnThemeChanged() override;
+   void ChildPreferredSizeChanged(views::View* child) override;
++  bool DoesIntersectRect(const views::View* target,
++                         const gfx::Rect& rect) const override;
+ 
+   // BrowserFrameHeaderChromeOS::AppearanceProvider:
+   SkColor GetTitleColor() override;
+diff --git a/chrome/browser/ui/views/frame/browser_non_client_frame_view_mac.mm b/chrome/browser/ui/views/frame/browser_non_client_frame_view_mac.mm
+index 387e4c46a90d3f365fee56388a60805a35f61fa1..1eeda0de0520fba7ef6a6d8606e3aef27601599c 100644
+--- a/chrome/browser/ui/views/frame/browser_non_client_frame_view_mac.mm
++++ b/chrome/browser/ui/views/frame/browser_non_client_frame_view_mac.mm
+@@ -338,6 +338,8 @@ FullscreenToolbarStyle GetUserPreferredToolbarStyle(bool always_show) {
+ }
+ 
+ void BrowserNonClientFrameViewMac::Layout() {
++  NonClientFrameView::Layout();
++
+   const int available_height = GetTopInset(true);
+   int leading_x = kFramePaddingLeft;
+   int trailing_x = width();
+diff --git a/chrome/browser/ui/views/frame/browser_non_client_frame_view_unittest.cc b/chrome/browser/ui/views/frame/browser_non_client_frame_view_unittest.cc
+index 58cb7172a89c054175f04e5d031b3114d638ed26..f62322371d905b5eabd64b0e5bd63c8784c4341b 100644
+--- a/chrome/browser/ui/views/frame/browser_non_client_frame_view_unittest.cc
++++ b/chrome/browser/ui/views/frame/browser_non_client_frame_view_unittest.cc
+@@ -61,12 +61,15 @@ class BrowserNonClientFrameViewPopupTest
+ #define MAYBE_HitTestPopupTopChrome HitTestPopupTopChrome
+ #endif
+ TEST_F(BrowserNonClientFrameViewPopupTest, MAYBE_HitTestPopupTopChrome) {
+-  EXPECT_FALSE(frame_view_->HitTestRect(gfx::Rect(-1, 4, 1, 1)));
+-  EXPECT_FALSE(frame_view_->HitTestRect(gfx::Rect(4, -1, 1, 1)));
++  constexpr gfx::Rect kLeftOfFrame(-1, 4, 1, 1);
++  EXPECT_FALSE(frame_view_->HitTestRect(kLeftOfFrame));
++
++  constexpr gfx::Rect kAboveFrame(4, -1, 1, 1);
++  EXPECT_FALSE(frame_view_->HitTestRect(kAboveFrame));
++
+   const int top_inset = frame_view_->GetTopInset(false);
+-  EXPECT_FALSE(frame_view_->HitTestRect(gfx::Rect(4, top_inset, 1, 1)));
+-  if (top_inset > 0)
+-    EXPECT_TRUE(frame_view_->HitTestRect(gfx::Rect(4, top_inset - 1, 1, 1)));
++  const gfx::Rect in_browser_view(4, top_inset, 1, 1);
++  EXPECT_TRUE(frame_view_->HitTestRect(in_browser_view));
+ }
+ 
+ class BrowserNonClientFrameViewTabbedTest
+@@ -108,7 +111,9 @@ TEST_F(BrowserNonClientFrameViewTabbedTest, MAYBE_HitTestTabstrip) {
+ 
+   // Hits client portions of the tabstrip (near the bottom left corner of the
+   // first tab).
+-  EXPECT_FALSE(frame_view_->HitTestRect(gfx::Rect(
++  EXPECT_TRUE(frame_view_->HitTestRect(gfx::Rect(
++      tabstrip_bounds.x() + 10, tabstrip_bounds.bottom() - 10, 1, 1)));
++  EXPECT_TRUE(frame_view_->browser_view()->HitTestRect(gfx::Rect(
+       tabstrip_bounds.x() + 10, tabstrip_bounds.bottom() - 10, 1, 1)));
+ 
+ // Tabs extend to the top of the tabstrip everywhere in this test context on
+diff --git a/chrome/browser/ui/views/frame/browser_view.cc b/chrome/browser/ui/views/frame/browser_view.cc
+index c33c4327974e7a5d80ac8da0af06a7cc7c271b63..58a0cec1d400fcd987d9a7873df5638c9c29e066 100644
+--- a/chrome/browser/ui/views/frame/browser_view.cc
++++ b/chrome/browser/ui/views/frame/browser_view.cc
+@@ -2928,6 +2928,12 @@ void BrowserView::ViewHierarchyChanged(
+ }
+ 
+ void BrowserView::AddedToWidget() {
++  // BrowserView may be added to a widget more than once if the user changes
++  // themes after starting the browser. Do not re-initialize BrowserView in
++  // this case.
++  if (initialized_)
++    return;
++
+   views::ClientView::AddedToWidget();
+ 
+   widget_observation_.Observe(GetWidget());
+diff --git a/chrome/browser/ui/views/frame/glass_browser_frame_view.cc b/chrome/browser/ui/views/frame/glass_browser_frame_view.cc
+index 3ae9bc2369f3327c258a0ea080ba9c2aee0e1c3b..452070396204ca6fd6b231a8dc98ff132f55aa2f 100644
+--- a/chrome/browser/ui/views/frame/glass_browser_frame_view.cc
++++ b/chrome/browser/ui/views/frame/glass_browser_frame_view.cc
+@@ -403,6 +403,7 @@ void GlassBrowserFrameView::Layout() {
+   LayoutCaptionButtons();
+   LayoutTitleBar();
+   LayoutClientView();
++  NonClientFrameView::Layout();
+ }
+ 
+ ///////////////////////////////////////////////////////////////////////////////
+diff --git a/chrome/browser/ui/views/frame/opaque_browser_frame_view_layout.cc b/chrome/browser/ui/views/frame/opaque_browser_frame_view_layout.cc
+index c788b1cf970cceefb82b36c2a24de5bdf1432119..843f8f87b82862c1030b96976b4a99e29f0f7aa4 100644
+--- a/chrome/browser/ui/views/frame/opaque_browser_frame_view_layout.cc
++++ b/chrome/browser/ui/views/frame/opaque_browser_frame_view_layout.cc
+@@ -18,6 +18,7 @@
+ #include "ui/gfx/font.h"
+ #include "ui/views/controls/button/image_button.h"
+ #include "ui/views/controls/label.h"
++#include "ui/views/view_utils.h"
+ #include "ui/views/window/caption_button_layout_constants.h"
+ #include "ui/views/window/frame_caption_button.h"
+ 
+@@ -606,10 +607,20 @@ gfx::Size OpaqueBrowserFrameViewLayout::GetPreferredSize(
+ 
+ void OpaqueBrowserFrameViewLayout::ViewAdded(views::View* host,
+                                              views::View* view) {
++  if (views::IsViewClass<views::ClientView>(view)) {
++    client_view_ = static_cast<views::ClientView*>(view);
++    return;
++  }
++
+   SetView(view->GetID(), view);
+ }
+ 
+ void OpaqueBrowserFrameViewLayout::ViewRemoved(views::View* host,
+                                                views::View* view) {
++  if (views::IsViewClass<views::ClientView>(view)) {
++    client_view_ = nullptr;
++    return;
++  }
++
+   SetView(view->GetID(), nullptr);
+ }
+diff --git a/chrome/browser/ui/views/frame/opaque_browser_frame_view_layout.h b/chrome/browser/ui/views/frame/opaque_browser_frame_view_layout.h
+index 3a8d8905cb1f68a89d36ac6fe53fb6fe5d581dfc..ec7d845fede82b43de7009ee7eb965097cf3d3f6 100644
+--- a/chrome/browser/ui/views/frame/opaque_browser_frame_view_layout.h
++++ b/chrome/browser/ui/views/frame/opaque_browser_frame_view_layout.h
+@@ -217,6 +217,8 @@ class OpaqueBrowserFrameViewLayout : public views::LayoutManager {
+   std::vector<views::FrameButton> leading_buttons_;
+   std::vector<views::FrameButton> trailing_buttons_;
+ 
++  views::ClientView* client_view_ = nullptr;
++
+   DISALLOW_COPY_AND_ASSIGN(OpaqueBrowserFrameViewLayout);
+ };
+ 
+diff --git a/chrome/browser/ui/views/web_apps/frame_toolbar/web_app_frame_toolbar_view.cc b/chrome/browser/ui/views/web_apps/frame_toolbar/web_app_frame_toolbar_view.cc
+index c0a4ab3705b813fedd0155b37934c807e30119cc..c6d8c62fd09ec82db897fe0bb2bf13ab03c0fc95 100644
+--- a/chrome/browser/ui/views/web_apps/frame_toolbar/web_app_frame_toolbar_view.cc
++++ b/chrome/browser/ui/views/web_apps/frame_toolbar/web_app_frame_toolbar_view.cc
+@@ -29,6 +29,7 @@ WebAppFrameToolbarView::WebAppFrameToolbarView(views::Widget* widget,
+   DCHECK(browser_view_);
+   DCHECK(web_app::AppBrowserController::IsWebApp(browser_view_->browser()));
+   SetID(VIEW_ID_WEB_APP_FRAME_TOOLBAR);
++  SetEventTargeter(std::make_unique<views::ViewTargeter>(this));
+ 
+   {
+     // TODO(tluk) fix the need for both LayoutInContainer() and a layout
+@@ -219,6 +220,24 @@ ReloadButton* WebAppFrameToolbarView::GetReloadButton() {
+   return left_container_ ? left_container_->reload_button() : nullptr;
+ }
+ 
++bool WebAppFrameToolbarView::DoesIntersectRect(const View* target,
++                                               const gfx::Rect& rect) const {
++  DCHECK_EQ(target, this);
++  if (!views::ViewTargeterDelegate::DoesIntersectRect(this, rect))
++    return false;
++
++  // If the rect is inside the bounds of the center_container, do not claim it.
++  // There is no actionable content in the center_container, and it overlaps
++  // tabs in tabbed PWA windows.
++  gfx::RectF rect_in_center_container_coords_f(rect);
++  View::ConvertRectToTarget(this, center_container_,
++                            &rect_in_center_container_coords_f);
++  gfx::Rect rect_in_client_view_coords =
++      gfx::ToEnclosingRect(rect_in_center_container_coords_f);
++
++  return !center_container_->HitTestRect(rect_in_client_view_coords);
++}
++
+ PageActionIconController*
+ WebAppFrameToolbarView::GetPageActionIconControllerForTesting() {
+   return right_container_->page_action_icon_controller();
+diff --git a/chrome/browser/ui/views/web_apps/frame_toolbar/web_app_frame_toolbar_view.h b/chrome/browser/ui/views/web_apps/frame_toolbar/web_app_frame_toolbar_view.h
+index 8d3f6ad1375d86bcca05c603a9dab01481e752e1..0bca228da4e73319f424b2ae2409bcaabe82f775 100644
+--- a/chrome/browser/ui/views/web_apps/frame_toolbar/web_app_frame_toolbar_view.h
++++ b/chrome/browser/ui/views/web_apps/frame_toolbar/web_app_frame_toolbar_view.h
+@@ -15,9 +15,11 @@
+ #include "ui/gfx/color_palette.h"
+ #include "ui/views/accessible_pane_view.h"
+ #include "ui/views/metadata/metadata_header_macros.h"
++#include "ui/views/view_targeter_delegate.h"
+ 
+ namespace views {
+ class View;
++class ViewTargeterDelegate;
+ class Widget;
+ }  // namespace views
+ 
+@@ -29,7 +31,8 @@ class WebAppToolbarButtonContainer;
+ 
+ // A container for web app buttons in the title bar.
+ class WebAppFrameToolbarView : public views::AccessiblePaneView,
+-                               public ToolbarButtonProvider {
++                               public ToolbarButtonProvider,
++                               public views::ViewTargeterDelegate {
+  public:
+   METADATA_HEADER(WebAppFrameToolbarView);
+   WebAppFrameToolbarView(views::Widget* widget, BrowserView* browser_view);
+@@ -71,6 +74,10 @@ class WebAppFrameToolbarView : public views::AccessiblePaneView,
+   ToolbarButton* GetBackButton() override;
+   ReloadButton* GetReloadButton() override;
+ 
++  // views::ViewTargeterDelegate
++  bool DoesIntersectRect(const View* target,
++                         const gfx::Rect& rect) const override;
++
+   WebAppNavigationButtonContainer* get_left_container_for_testing() {
+     return left_container_;
+   }
+diff --git a/components/exo/client_controlled_shell_surface.cc b/components/exo/client_controlled_shell_surface.cc
+index 2efee6195ef3dae4a8cbd4c35ebeb809c88c1bc3..39fb793b30c99b0c6b9ee3ba64c54820d456418f 100644
+--- a/components/exo/client_controlled_shell_surface.cc
++++ b/components/exo/client_controlled_shell_surface.cc
+@@ -1024,7 +1024,7 @@ void ClientControlledShellSurface::SetWidgetBounds(const gfx::Rect& bounds) {
+ gfx::Rect ClientControlledShellSurface::GetShadowBounds() const {
+   gfx::Rect shadow_bounds = ShellSurfaceBase::GetShadowBounds();
+   const ash::NonClientFrameViewAsh* frame_view = GetFrameView();
+-  if (frame_view->GetVisible()) {
++  if (frame_view->GetFrameEnabled()) {
+     // The client controlled geometry is only for the client
+     // area. When the chrome side frame is enabled, the shadow height
+     // has to include the height of the frame, and the total height is
+@@ -1083,7 +1083,7 @@ float ClientControlledShellSurface::GetScale() const {
+ base::Optional<gfx::Rect> ClientControlledShellSurface::GetWidgetBounds()
+     const {
+   const ash::NonClientFrameViewAsh* frame_view = GetFrameView();
+-  if (frame_view->GetVisible()) {
++  if (frame_view->GetFrameEnabled()) {
+     gfx::Rect visible_bounds = ShellSurfaceBase::GetVisibleBounds();
+     if (widget_->IsMaximized() && frame_type_ == SurfaceFrameType::NORMAL) {
+       // When the widget is maximized in clamshell mode, client sends
+@@ -1262,7 +1262,7 @@ void ClientControlledShellSurface::UpdateFrame() {
+           .work_area();
+ 
+   ash::WindowState* window_state = GetWindowState();
+-  bool enable_wide_frame = GetFrameView()->GetVisible() &&
++  bool enable_wide_frame = GetFrameView()->GetFrameEnabled() &&
+                            window_state->IsMaximizedOrFullscreenOrPinned() &&
+                            work_area.width() != geometry().width();
+   bool update_frame = state_changed_;
+diff --git a/components/exo/client_controlled_shell_surface_unittest.cc b/components/exo/client_controlled_shell_surface_unittest.cc
+index dc4756c4a80c0521a4367ad0a76e0bba9e195869..20d4d7cdbf746bfc323c6ecef2e68c22972f1e80 100644
+--- a/components/exo/client_controlled_shell_surface_unittest.cc
++++ b/components/exo/client_controlled_shell_surface_unittest.cc
+@@ -517,7 +517,7 @@ TEST_F(ClientControlledShellSurfaceTest, Frame) {
+ 
+   // Normal state.
+   widget->LayoutRootViewIfNecessary();
+-  EXPECT_TRUE(frame_view->GetVisible());
++  EXPECT_TRUE(frame_view->GetFrameEnabled());
+   EXPECT_EQ(normal_window_bounds, widget->GetWindowBoundsInScreen());
+   EXPECT_EQ(client_bounds,
+             frame_view->GetClientBoundsForWindowBounds(normal_window_bounds));
+@@ -528,7 +528,7 @@ TEST_F(ClientControlledShellSurfaceTest, Frame) {
+   surface->Commit();
+ 
+   widget->LayoutRootViewIfNecessary();
+-  EXPECT_TRUE(frame_view->GetVisible());
++  EXPECT_TRUE(frame_view->GetFrameEnabled());
+   EXPECT_EQ(fullscreen_bounds, widget->GetWindowBoundsInScreen());
+   EXPECT_EQ(
+       gfx::Size(800, 568),
+@@ -541,7 +541,7 @@ TEST_F(ClientControlledShellSurfaceTest, Frame) {
+   surface->Commit();
+ 
+   widget->LayoutRootViewIfNecessary();
+-  EXPECT_TRUE(frame_view->GetVisible());
++  EXPECT_TRUE(frame_view->GetFrameEnabled());
+   EXPECT_EQ(gfx::Rect(0, 200, 800, 400), widget->GetWindowBoundsInScreen());
+ 
+   display_manager->UpdateWorkAreaOfDisplay(display_id, gfx::Insets(0, 0, 0, 0));
+@@ -552,7 +552,7 @@ TEST_F(ClientControlledShellSurfaceTest, Frame) {
+   surface->Commit();
+ 
+   widget->LayoutRootViewIfNecessary();
+-  EXPECT_TRUE(frame_view->GetVisible());
++  EXPECT_TRUE(frame_view->GetFrameEnabled());
+   EXPECT_EQ(fullscreen_bounds, widget->GetWindowBoundsInScreen());
+   EXPECT_EQ(fullscreen_bounds,
+             frame_view->GetClientBoundsForWindowBounds(fullscreen_bounds));
+@@ -562,7 +562,7 @@ TEST_F(ClientControlledShellSurfaceTest, Frame) {
+   surface->Commit();
+ 
+   widget->LayoutRootViewIfNecessary();
+-  EXPECT_TRUE(frame_view->GetVisible());
++  EXPECT_TRUE(frame_view->GetFrameEnabled());
+   EXPECT_EQ(fullscreen_bounds, widget->GetWindowBoundsInScreen());
+   EXPECT_EQ(fullscreen_bounds,
+             frame_view->GetClientBoundsForWindowBounds(fullscreen_bounds));
+@@ -587,7 +587,7 @@ TEST_F(ClientControlledShellSurfaceTest, Frame) {
+   surface->Commit();
+ 
+   widget->LayoutRootViewIfNecessary();
+-  EXPECT_TRUE(frame_view->GetVisible());
++  EXPECT_TRUE(frame_view->GetFrameEnabled());
+   EXPECT_EQ(normal_window_bounds, widget->GetWindowBoundsInScreen());
+   EXPECT_EQ(client_bounds,
+             frame_view->GetClientBoundsForWindowBounds(normal_window_bounds));
+@@ -599,7 +599,7 @@ TEST_F(ClientControlledShellSurfaceTest, Frame) {
+   surface->Commit();
+ 
+   widget->LayoutRootViewIfNecessary();
+-  EXPECT_FALSE(frame_view->GetVisible());
++  EXPECT_FALSE(frame_view->GetFrameEnabled());
+   EXPECT_EQ(client_bounds, widget->GetWindowBoundsInScreen());
+   EXPECT_EQ(client_bounds,
+             frame_view->GetClientBoundsForWindowBounds(client_bounds));
+@@ -611,14 +611,14 @@ TEST_F(ClientControlledShellSurfaceTest, Frame) {
+   surface->Commit();
+ 
+   widget->LayoutRootViewIfNecessary();
+-  EXPECT_TRUE(frame_view->GetVisible());
++  EXPECT_TRUE(frame_view->GetFrameEnabled());
+   EXPECT_TRUE(frame_view->GetHeaderView()->in_immersive_mode());
+ 
+   surface->SetFrame(SurfaceFrameType::NONE);
+   surface->Commit();
+ 
+   widget->LayoutRootViewIfNecessary();
+-  EXPECT_FALSE(frame_view->GetVisible());
++  EXPECT_FALSE(frame_view->GetFrameEnabled());
+   EXPECT_FALSE(frame_view->GetHeaderView()->in_immersive_mode());
+ }
+ 
+@@ -2015,7 +2015,7 @@ TEST_F(ClientControlledShellSurfaceTest, SnappedInTabletMode) {
+   // Snapped window can also use auto hide.
+   surface->SetFrame(SurfaceFrameType::AUTOHIDE);
+   surface->Commit();
+-  EXPECT_TRUE(frame_view->GetVisible());
++  EXPECT_TRUE(frame_view->GetFrameEnabled());
+   EXPECT_TRUE(frame_view->GetHeaderView()->in_immersive_mode());
+ }
+ 
+diff --git a/components/exo/shell_surface_base.cc b/components/exo/shell_surface_base.cc
+index b123d175c82cde0121860c2763f8c2da0c1f3b51..ba83bbd01676e75a65f30743a95622a3c5c0e9fc 100644
+--- a/components/exo/shell_surface_base.cc
++++ b/components/exo/shell_surface_base.cc
+@@ -111,8 +111,7 @@ class CustomFrameView : public ash::NonClientFrameViewAsh {
+                   ShellSurfaceBase* shell_surface,
+                   bool enabled)
+       : NonClientFrameViewAsh(widget), shell_surface_(shell_surface) {
+-    SetEnabled(enabled);
+-    SetVisible(enabled);
++    SetFrameEnabled(enabled);
+     if (!enabled)
+       NonClientFrameViewAsh::SetShouldPaintHeader(false);
+   }
+@@ -121,7 +120,7 @@ class CustomFrameView : public ash::NonClientFrameViewAsh {
+ 
+   // Overridden from ash::NonClientFrameViewAsh:
+   void SetShouldPaintHeader(bool paint) override {
+-    if (GetVisible()) {
++    if (GetFrameEnabled()) {
+       NonClientFrameViewAsh::SetShouldPaintHeader(paint);
+       return;
+     }
+@@ -129,46 +128,46 @@ class CustomFrameView : public ash::NonClientFrameViewAsh {
+ 
+   // Overridden from views::NonClientFrameView:
+   gfx::Rect GetBoundsForClientView() const override {
+-    if (GetVisible())
++    if (GetFrameEnabled())
+       return ash::NonClientFrameViewAsh::GetBoundsForClientView();
+     return bounds();
+   }
+   gfx::Rect GetWindowBoundsForClientBounds(
+       const gfx::Rect& client_bounds) const override {
+-    if (GetVisible()) {
++    if (GetFrameEnabled()) {
+       return ash::NonClientFrameViewAsh::GetWindowBoundsForClientBounds(
+           client_bounds);
+     }
+     return client_bounds;
+   }
+   int NonClientHitTest(const gfx::Point& point) override {
+-    if (GetVisible() || shell_surface_->server_side_resize())
++    if (GetFrameEnabled() || shell_surface_->server_side_resize())
+       return ash::NonClientFrameViewAsh::NonClientHitTest(point);
+     return GetWidget()->client_view()->NonClientHitTest(point);
+   }
+   void GetWindowMask(const gfx::Size& size, SkPath* window_mask) override {
+-    if (GetVisible())
++    if (GetFrameEnabled())
+       return ash::NonClientFrameViewAsh::GetWindowMask(size, window_mask);
+   }
+   void ResetWindowControls() override {
+-    if (GetVisible())
++    if (GetFrameEnabled())
+       return ash::NonClientFrameViewAsh::ResetWindowControls();
+   }
+   void UpdateWindowIcon() override {
+-    if (GetVisible())
++    if (GetFrameEnabled())
+       return ash::NonClientFrameViewAsh::ResetWindowControls();
+   }
+   void UpdateWindowTitle() override {
+-    if (GetVisible())
++    if (GetFrameEnabled())
+       return ash::NonClientFrameViewAsh::UpdateWindowTitle();
+   }
+   void SizeConstraintsChanged() override {
+-    if (GetVisible())
++    if (GetFrameEnabled())
+       return ash::NonClientFrameViewAsh::SizeConstraintsChanged();
+   }
+   gfx::Size GetMinimumSize() const override {
+     gfx::Size minimum_size = shell_surface_->GetMinimumSize();
+-    if (GetVisible()) {
++    if (GetFrameEnabled()) {
+       return ash::NonClientFrameViewAsh::GetWindowBoundsForClientBounds(
+                  gfx::Rect(minimum_size))
+           .size();
+@@ -177,7 +176,7 @@ class CustomFrameView : public ash::NonClientFrameViewAsh {
+   }
+   gfx::Size GetMaximumSize() const override {
+     gfx::Size maximum_size = shell_surface_->GetMaximumSize();
+-    if (GetVisible() && !maximum_size.IsEmpty()) {
++    if (GetFrameEnabled() && !maximum_size.IsEmpty()) {
+       return ash::NonClientFrameViewAsh::GetWindowBoundsForClientBounds(
+                  gfx::Rect(maximum_size))
+           .size();
+@@ -710,11 +709,10 @@ void ShellSurfaceBase::OnSetFrame(SurfaceFrameType frame_type) {
+ 
+   CustomFrameView* frame_view =
+       static_cast<CustomFrameView*>(widget_->non_client_view()->frame_view());
+-  if (frame_view->GetEnabled() == frame_enabled())
++  if (frame_view->GetFrameEnabled() == frame_enabled())
+     return;
+ 
+-  frame_view->SetEnabled(frame_enabled());
+-  frame_view->SetVisible(frame_enabled());
++  frame_view->SetFrameEnabled(frame_enabled());
+   frame_view->SetShouldPaintHeader(frame_enabled());
+   widget_->GetRootView()->Layout();
+   // TODO(oshima): We probably should wait applying these if the
+diff --git a/components/ui_devtools/views/overlay_agent_unittest.cc b/components/ui_devtools/views/overlay_agent_unittest.cc
+index 00c8f36619857e7e7b44a1d0c9382ae74b0dbb2a..9e6494d76eccb835a1b253e779542d9c9941d5e7 100644
+--- a/components/ui_devtools/views/overlay_agent_unittest.cc
++++ b/components/ui_devtools/views/overlay_agent_unittest.cc
+@@ -114,12 +114,14 @@ class OverlayAgentTest : public views::ViewsTestBase {
+   }
+ #endif
+ 
+-  void CreateWidget(const gfx::Rect& bounds) {
++  void CreateWidget(const gfx::Rect& bounds,
++                    views::Widget::InitParams::Type type) {
+     widget_ = std::make_unique<views::Widget>();
+     views::Widget::InitParams params;
+     params.delegate = nullptr;
+     params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
+     params.bounds = bounds;
++    params.type = type;
+ #if defined(USE_AURA)
+     params.parent = GetContext();
+ #endif
+@@ -129,7 +131,8 @@ class OverlayAgentTest : public views::ViewsTestBase {
+ 
+   void CreateWidget() {
+     // Create a widget with default bounds.
+-    return CreateWidget(gfx::Rect(0, 0, 400, 400));
++    return CreateWidget(gfx::Rect(0, 0, 400, 400),
++                        views::Widget::InitParams::Type::TYPE_WINDOW);
+   }
+ 
+   views::Widget* widget() { return widget_.get(); }
+@@ -176,13 +179,14 @@ TEST_F(OverlayAgentTest, FindElementIdTargetedByPointWindow) {
+ #endif
+ 
+ TEST_F(OverlayAgentTest, FindElementIdTargetedByPointViews) {
+-  CreateWidget();
++  // Use a frameless window instead of deleting all children of |contents_view|
++  CreateWidget(gfx::Rect(0, 0, 400, 400),
++               views::Widget::InitParams::Type::TYPE_WINDOW_FRAMELESS);
+ 
+   std::unique_ptr<protocol::DOM::Node> root;
+   dom_agent()->getDocument(&root);
+ 
+-  views::View* contents_view = widget()->GetContentsView();
+-  contents_view->RemoveAllChildViews(true);
++  views::View* contents_view = widget()->GetRootView();
+ 
+   views::View* child_1 = new views::View;
+   views::View* child_2 = new views::View;
+@@ -203,7 +207,7 @@ TEST_F(OverlayAgentTest, FindElementIdTargetedByPointViews) {
+   child_1->SetBounds(20, 20, 100, 100);
+   child_2->SetBounds(90, 50, 100, 100);
+ 
+-  EXPECT_EQ(GetViewAtPoint(1, 1), widget()->GetContentsView());
++  EXPECT_EQ(GetViewAtPoint(1, 1), widget()->GetRootView());
+   EXPECT_EQ(GetViewAtPoint(21, 21), child_1);
+   EXPECT_EQ(GetViewAtPoint(170, 130), child_2);
+   // At the overlap.
+@@ -237,7 +241,7 @@ TEST_F(OverlayAgentTest, HighlightRects) {
+ 
+   for (const auto& test_case : kTestCases) {
+     SCOPED_TRACE(testing::Message() << "Case: " << test_case.name);
+-    CreateWidget(kWidgetBounds);
++    CreateWidget(kWidgetBounds, views::Widget::InitParams::Type::TYPE_WINDOW);
+     // Can't just use kWidgetBounds because of Mac's menu bar.
+     gfx::Vector2d widget_screen_offset =
+         widget()->GetClientAreaBoundsInScreen().OffsetFromOrigin();
+diff --git a/third_party/wayland/features.gni b/third_party/wayland/features.gni
+index ecdc2c72ff2b267ea180ddb04ad2c6e6842653ea..424be6e75be0e0da3d00e42ee82d596912acd7a1 100644
+--- a/third_party/wayland/features.gni
++++ b/third_party/wayland/features.gni
+@@ -18,5 +18,5 @@ declare_args() {
+ 
+   # This may be set by Chromium packagers who do not wish to use the bundled
+   # wayland scanner.
+-  use_system_wayland_scanner = (host_toolchain == default_toolchain && is_msan)
++  use_system_wayland_scanner = host_toolchain == default_toolchain && is_msan
+ }
+diff --git a/ui/views/BUILD.gn b/ui/views/BUILD.gn
+index c0e55a44f12a63a8a4a989b0de96df616cbb0709..d5dd660994324ef282b3affe7ed1d0da8892111f 100644
+--- a/ui/views/BUILD.gn
++++ b/ui/views/BUILD.gn
+@@ -1196,7 +1196,6 @@ test("views_unittests") {
+     "window/dialog_delegate_unittest.cc",
+     "window/frame_caption_button_unittest.cc",
+     "window/hit_test_utils_unittest.cc",
+-    "window/non_client_view_unittest.cc",
+   ]
+ 
+   configs += [ "//build/config:precompiled_headers" ]
+diff --git a/ui/views/accessibility/view_ax_platform_node_delegate_unittest.cc b/ui/views/accessibility/view_ax_platform_node_delegate_unittest.cc
+index d34d57d14514a799eca3a1cef2c07b513c5034fd..0cca80545e2d53caa3999c68529af9d32f20b2f5 100644
+--- a/ui/views/accessibility/view_ax_platform_node_delegate_unittest.cc
++++ b/ui/views/accessibility/view_ax_platform_node_delegate_unittest.cc
+@@ -115,7 +115,8 @@ class ViewAXPlatformNodeDelegateTest : public ViewsTestBase {
+     ui::AXPlatformNode::NotifyAddAXModeFlags(ui::kAXModeComplete);
+ 
+     widget_ = new Widget;
+-    Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW);
++    Widget::InitParams params =
++        CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS);
+     params.bounds = gfx::Rect(0, 0, 200, 200);
+     widget_->Init(std::move(params));
+ 
+@@ -127,7 +128,7 @@ class ViewAXPlatformNodeDelegateTest : public ViewsTestBase {
+     label_->SetID(DEFAULT_VIEW_ID);
+     button_->AddChildView(label_);
+ 
+-    widget_->GetContentsView()->AddChildView(button_);
++    widget_->GetRootView()->AddChildView(button_);
+     widget_->Show();
+   }
+ 
+@@ -163,7 +164,7 @@ class ViewAXPlatformNodeDelegateTest : public ViewsTestBase {
+   // child Views.
+   View::Views SetUpExtraViews() {
+     View* parent_view =
+-        widget_->GetContentsView()->AddChildView(std::make_unique<View>());
++        widget_->GetRootView()->AddChildView(std::make_unique<View>());
+     View::Views views{parent_view};
+     for (int i = 0; i < 4; i++)
+       views.push_back(parent_view->AddChildView(std::make_unique<View>()));
+@@ -223,7 +224,7 @@ class ViewAXPlatformNodeDelegateTableTest
+     auto table =
+         std::make_unique<TableView>(model_.get(), columns, TEXT_ONLY, true);
+     table_ = table.get();
+-    widget_->GetContentsView()->AddChildView(
++    widget_->GetRootView()->AddChildView(
+         TableView::CreateScrollViewWithTable(std::move(table)));
+   }
+ 
+@@ -536,12 +537,12 @@ TEST_F(ViewAXPlatformNodeDelegateTest, TreeNavigation) {
+   ViewAXPlatformNodeDelegate* child_view_3 = view_accessibility(extra_views[3]);
+   ViewAXPlatformNodeDelegate* child_view_4 = view_accessibility(extra_views[4]);
+ 
+-  EXPECT_EQ(view_accessibility(widget_->GetContentsView())->GetNativeObject(),
++  EXPECT_EQ(view_accessibility(widget_->GetRootView())->GetNativeObject(),
+             parent_view->GetParent());
+   EXPECT_EQ(4, parent_view->GetChildCount());
+ 
+-  EXPECT_EQ(2, button_accessibility()->GetIndexInParent());
+-  EXPECT_EQ(3, parent_view->GetIndexInParent());
++  EXPECT_EQ(0, button_accessibility()->GetIndexInParent());
++  EXPECT_EQ(1, parent_view->GetIndexInParent());
+ 
+   EXPECT_EQ(child_view_1->GetNativeObject(), parent_view->ChildAtIndex(0));
+   EXPECT_EQ(child_view_2->GetNativeObject(), parent_view->ChildAtIndex(1));
+@@ -585,8 +586,6 @@ TEST_F(ViewAXPlatformNodeDelegateTest, TreeNavigationWithLeafViews) {
+   // view is added as the next sibling of the already present button view.
+   //
+   // Widget
+-  // ++NonClientView
+-  // ++NonClientFrameView
+   // ++Button
+   // ++++Label
+   // 0 = ++ParentView
+@@ -596,7 +595,7 @@ TEST_F(ViewAXPlatformNodeDelegateTest, TreeNavigationWithLeafViews) {
+   // 4 = ++++ChildView4
+   View::Views extra_views = SetUpExtraViews();
+   ViewAXPlatformNodeDelegate* contents_view =
+-      view_accessibility(widget_->GetContentsView());
++      view_accessibility(widget_->GetRootView());
+   ViewAXPlatformNodeDelegate* parent_view = view_accessibility(extra_views[0]);
+   ViewAXPlatformNodeDelegate* child_view_1 = view_accessibility(extra_views[1]);
+   ViewAXPlatformNodeDelegate* child_view_2 = view_accessibility(extra_views[2]);
+@@ -610,12 +609,12 @@ TEST_F(ViewAXPlatformNodeDelegateTest, TreeNavigationWithLeafViews) {
+   parent_view->OverrideIsLeaf(true);
+   child_view_2->OverrideIsLeaf(true);
+ 
+-  EXPECT_EQ(4, contents_view->GetChildCount());
++  EXPECT_EQ(2, contents_view->GetChildCount());
+   EXPECT_EQ(contents_view->GetNativeObject(), parent_view->GetParent());
+   EXPECT_EQ(0, parent_view->GetChildCount());
+ 
+-  EXPECT_EQ(2, button_accessibility()->GetIndexInParent());
+-  EXPECT_EQ(3, parent_view->GetIndexInParent());
++  EXPECT_EQ(0, button_accessibility()->GetIndexInParent());
++  EXPECT_EQ(1, parent_view->GetIndexInParent());
+ 
+   EXPECT_FALSE(contents_view->IsIgnored());
+   EXPECT_FALSE(parent_view->IsIgnored());
+@@ -647,12 +646,12 @@ TEST_F(ViewAXPlatformNodeDelegateTest, TreeNavigationWithLeafViews) {
+   // have no effect.
+   parent_view->OverrideIsLeaf(false);
+ 
+-  EXPECT_EQ(4, contents_view->GetChildCount());
++  EXPECT_EQ(2, contents_view->GetChildCount());
+   EXPECT_EQ(contents_view->GetNativeObject(), parent_view->GetParent());
+   EXPECT_EQ(4, parent_view->GetChildCount());
+ 
+-  EXPECT_EQ(2, button_accessibility()->GetIndexInParent());
+-  EXPECT_EQ(3, parent_view->GetIndexInParent());
++  EXPECT_EQ(0, button_accessibility()->GetIndexInParent());
++  EXPECT_EQ(1, parent_view->GetIndexInParent());
+ 
+   EXPECT_FALSE(contents_view->IsIgnored());
+   EXPECT_FALSE(parent_view->IsIgnored());
+@@ -691,8 +690,6 @@ TEST_F(ViewAXPlatformNodeDelegateTest, TreeNavigationWithIgnoredViews) {
+   // view is added as the next sibling of the already present button view.
+   //
+   // Widget
+-  // ++NonClientView
+-  // ++NonClientFrameView
+   // ++Button
+   // ++++Label
+   // 0 = ++ParentView
+@@ -702,7 +699,7 @@ TEST_F(ViewAXPlatformNodeDelegateTest, TreeNavigationWithIgnoredViews) {
+   // 4 = ++++ChildView4
+   View::Views extra_views = SetUpExtraViews();
+   ViewAXPlatformNodeDelegate* contents_view =
+-      view_accessibility(widget_->GetContentsView());
++      view_accessibility(widget_->GetRootView());
+   ViewAXPlatformNodeDelegate* parent_view = view_accessibility(extra_views[0]);
+   ViewAXPlatformNodeDelegate* child_view_1 = view_accessibility(extra_views[1]);
+   ViewAXPlatformNodeDelegate* child_view_2 = view_accessibility(extra_views[2]);
+@@ -716,7 +713,7 @@ TEST_F(ViewAXPlatformNodeDelegateTest, TreeNavigationWithIgnoredViews) {
+   EXPECT_EQ(contents_view->GetNativeObject(), parent_view->GetParent());
+   EXPECT_EQ(3, parent_view->GetChildCount());
+ 
+-  EXPECT_EQ(2, button_accessibility()->GetIndexInParent());
++  EXPECT_EQ(0, button_accessibility()->GetIndexInParent());
+   EXPECT_EQ(-1, parent_view->GetIndexInParent());
+ 
+   EXPECT_EQ(child_view_1->GetNativeObject(), parent_view->ChildAtIndex(0));
+@@ -724,17 +721,17 @@ TEST_F(ViewAXPlatformNodeDelegateTest, TreeNavigationWithIgnoredViews) {
+   EXPECT_EQ(child_view_4->GetNativeObject(), parent_view->ChildAtIndex(2));
+ 
+   EXPECT_EQ(button_accessibility()->GetNativeObject(),
+-            contents_view->ChildAtIndex(2));
+-  EXPECT_EQ(child_view_1->GetNativeObject(), contents_view->ChildAtIndex(3));
+-  EXPECT_EQ(child_view_3->GetNativeObject(), contents_view->ChildAtIndex(4));
+-  EXPECT_EQ(child_view_4->GetNativeObject(), contents_view->ChildAtIndex(5));
++            contents_view->ChildAtIndex(0));
++  EXPECT_EQ(child_view_1->GetNativeObject(), contents_view->ChildAtIndex(1));
++  EXPECT_EQ(child_view_3->GetNativeObject(), contents_view->ChildAtIndex(2));
++  EXPECT_EQ(child_view_4->GetNativeObject(), contents_view->ChildAtIndex(3));
+ 
+   EXPECT_EQ(nullptr, parent_view->GetNextSibling());
+   EXPECT_EQ(nullptr, parent_view->GetPreviousSibling());
+ 
+   EXPECT_EQ(contents_view->GetNativeObject(), child_view_1->GetParent());
+   EXPECT_EQ(0, child_view_1->GetChildCount());
+-  EXPECT_EQ(3, child_view_1->GetIndexInParent());
++  EXPECT_EQ(1, child_view_1->GetIndexInParent());
+   EXPECT_EQ(child_view_3->GetNativeObject(), child_view_1->GetNextSibling());
+   EXPECT_EQ(button_accessibility()->GetNativeObject(),
+             child_view_1->GetPreviousSibling());
+@@ -747,14 +744,14 @@ TEST_F(ViewAXPlatformNodeDelegateTest, TreeNavigationWithIgnoredViews) {
+ 
+   EXPECT_EQ(contents_view->GetNativeObject(), child_view_3->GetParent());
+   EXPECT_EQ(0, child_view_3->GetChildCount());
+-  EXPECT_EQ(4, child_view_3->GetIndexInParent());
++  EXPECT_EQ(2, child_view_3->GetIndexInParent());
+   EXPECT_EQ(child_view_4->GetNativeObject(), child_view_3->GetNextSibling());
+   EXPECT_EQ(child_view_1->GetNativeObject(),
+             child_view_3->GetPreviousSibling());
+ 
+   EXPECT_EQ(contents_view->GetNativeObject(), child_view_4->GetParent());
+   EXPECT_EQ(0, child_view_4->GetChildCount());
+-  EXPECT_EQ(5, child_view_4->GetIndexInParent());
++  EXPECT_EQ(3, child_view_4->GetIndexInParent());
+   EXPECT_EQ(nullptr, child_view_4->GetNextSibling());
+   EXPECT_EQ(child_view_3->GetNativeObject(),
+             child_view_4->GetPreviousSibling());
+diff --git a/ui/views/bubble/bubble_frame_view.cc b/ui/views/bubble/bubble_frame_view.cc
+index a2380fa3ac51d01c1858893d88df8ca25ea4adf4..95a4b1df05fba900db6116360bbdedf864f78e2a 100644
+--- a/ui/views/bubble/bubble_frame_view.cc
++++ b/ui/views/bubble/bubble_frame_view.cc
+@@ -391,8 +391,20 @@ void BubbleFrameView::Layout() {
+     header_bottom = header_view_->bounds().bottom();
+   }
+ 
+-  if (bounds.IsEmpty())
++  // Only account for footnote_container_'s height if it's visible, because
++  // content_margins_ adds extra padding even if all child views are invisible.
++  if (footnote_container_ && footnote_container_->GetVisible()) {
++    const int width = contents_bounds.width();
++    const int height = footnote_container_->GetHeightForWidth(width);
++    footnote_container_->SetBounds(
++        contents_bounds.x(), contents_bounds.bottom() - height, width, height);
++  }
++
++  NonClientFrameView::Layout();
++
++  if (bounds.IsEmpty()) {
+     return;
++  }
+ 
+   // The buttons are positioned somewhat closer to the edge of the bubble.
+   const int close_margin =
+@@ -442,15 +454,6 @@ void BubbleFrameView::Layout() {
+ 
+   title_icon_->SetBounds(bounds.x(), bounds.y(), title_icon_pref_size.width(),
+                          title_height);
+-
+-  // Only account for footnote_container_'s height if it's visible, because
+-  // content_margins_ adds extra padding even if all child views are invisible.
+-  if (footnote_container_ && footnote_container_->GetVisible()) {
+-    const int width = contents_bounds.width();
+-    const int height = footnote_container_->GetHeightForWidth(width);
+-    footnote_container_->SetBounds(
+-        contents_bounds.x(), contents_bounds.bottom() - height, width, height);
+-  }
+ }
+ 
+ void BubbleFrameView::OnThemeChanged() {
+diff --git a/ui/views/cocoa/drag_drop_client_mac_unittest.mm b/ui/views/cocoa/drag_drop_client_mac_unittest.mm
+index 0f1881bd1fea1827d8c972c41817bc72b09efcd2..9966491dcd41da16521951424adb14f06134cc0c 100644
+--- a/ui/views/cocoa/drag_drop_client_mac_unittest.mm
++++ b/ui/views/cocoa/drag_drop_client_mac_unittest.mm
+@@ -195,7 +195,7 @@ void SetUp() override {
+     widget_->Show();
+ 
+     target_ = new DragDropView();
+-    widget_->GetContentsView()->AddChildView(target_);
++    widget_->non_client_view()->frame_view()->AddChildView(target_);
+     target_->SetBoundsRect(bounds);
+ 
+     drag_drop_client()->source_operation_ = ui::DragDropTypes::DRAG_COPY;
+@@ -329,7 +329,7 @@ DragOperation OnPerformDrop(const ui::DropTargetEvent& event) override {
+   SetData(data);
+ 
+   target_ = new DragDropCloseView();
+-  widget_->GetContentsView()->AddChildView(target_);
++  widget_->non_client_view()->frame_view()->AddChildView(target_);
+   target_->SetBoundsRect(gfx::Rect(0, 0, 100, 100));
+   target_->set_formats(ui::OSExchangeData::STRING | ui::OSExchangeData::URL);
+ 
+diff --git a/ui/views/controls/table/table_view_unittest.cc b/ui/views/controls/table/table_view_unittest.cc
+index 12cb14b36c6798f7151b7bb67841db53a30e29fb..f377aff61222583149a7fd615e3fc0ef806c13b3 100644
+--- a/ui/views/controls/table/table_view_unittest.cc
++++ b/ui/views/controls/table/table_view_unittest.cc
+@@ -444,12 +444,13 @@ class TableViewTest : public ViewsTestBase,
+     helper_ = std::make_unique<TableViewTestHelper>(table_);
+ 
+     widget_ = std::make_unique<Widget>();
+-    Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW);
++    Widget::InitParams params =
++        CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS);
+     params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
+     params.bounds = gfx::Rect(0, 0, 650, 650);
+     params.delegate = GetWidgetDelegate(widget_.get());
+     widget_->Init(std::move(params));
+-    widget_->GetContentsView()->AddChildView(std::move(scroll_view));
++    widget_->GetRootView()->AddChildView(std::move(scroll_view));
+     widget_->Show();
+   }
+ 
+diff --git a/ui/views/view_unittest_mac.mm b/ui/views/view_unittest_mac.mm
+index f2e3724183b02739983cb1bcce06f7f222efc536..a3f5c3e28fc9ec48017e17dc4c6a1ac1150a57f6 100644
+--- a/ui/views/view_unittest_mac.mm
++++ b/ui/views/view_unittest_mac.mm
+@@ -117,7 +117,7 @@ void SetUp() override {
+ 
+     view_ = new ThreeFingerSwipeView;
+     view_->SetSize(widget_->GetClientAreaBoundsInScreen().size());
+-    widget_->GetContentsView()->AddChildView(view_);
++    widget_->non_client_view()->frame_view()->AddChildView(view_);
+   }
+ 
+   void TearDown() override {
+diff --git a/ui/views/widget/native_widget_mac_unittest.mm b/ui/views/widget/native_widget_mac_unittest.mm
+index 15e1197030b3f1e93ed617b9cb8ff2feaf3d35d8..7233e122869034a0db15908f2c8a8869000c04b5 100644
+--- a/ui/views/widget/native_widget_mac_unittest.mm
++++ b/ui/views/widget/native_widget_mac_unittest.mm
+@@ -573,8 +573,10 @@ void WaitForPaintCount(int target) {
+ 
+   Widget* widget = CreateTopLevelPlatformWidget();
+   widget->SetBounds(gfx::Rect(0, 0, 300, 300));
+-  widget->GetContentsView()->AddChildView(new CursorView(0, hand));
+-  widget->GetContentsView()->AddChildView(new CursorView(100, ibeam));
++  widget->non_client_view()->frame_view()->AddChildView(
++      new CursorView(0, hand));
++  widget->non_client_view()->frame_view()->AddChildView(
++      new CursorView(100, ibeam));
+   widget->Show();
+   NSWindow* widget_window = widget->GetNativeWindow().GetNativeNSWindow();
+ 
+@@ -879,8 +881,8 @@ void WaitForPaintCount(int target) {
+   const std::u16string long_tooltip(2000, 'W');
+ 
+   // Create a nested layout to test corner cases.
+-  LabelButton* back =
+-      widget->GetContentsView()->AddChildView(std::make_unique<LabelButton>());
++  LabelButton* back = widget->non_client_view()->frame_view()->AddChildView(
++      std::make_unique<LabelButton>());
+   back->SetBounds(10, 10, 80, 80);
+   widget->Show();
+ 
+@@ -944,7 +946,7 @@ void WaitForPaintCount(int target) {
+ 
+   CustomTooltipView* view_below = new CustomTooltipView(u"Back", view_above);
+   view_below->SetBoundsRect(widget_below->GetContentsView()->bounds());
+-  widget_below->GetContentsView()->AddChildView(view_below);
++  widget_below->non_client_view()->frame_view()->AddChildView(view_below);
+ 
+   widget_below->Show();
+   widget_above->Show();
+diff --git a/ui/views/widget/widget_unittest.cc b/ui/views/widget/widget_unittest.cc
+index 3ae33afe803df33138861760f455edc10c4022fa..66bdbb121ec2eb170fd2c423cfc21eddea05604c 100644
+--- a/ui/views/widget/widget_unittest.cc
++++ b/ui/views/widget/widget_unittest.cc
+@@ -3808,7 +3808,7 @@ TEST_F(WidgetTest, MouseWheelEvent) {
+   WidgetAutoclosePtr widget(CreateTopLevelPlatformWidget());
+   widget->SetBounds(gfx::Rect(0, 0, 600, 600));
+   EventCountView* event_count_view = new EventCountView();
+-  widget->GetContentsView()->AddChildView(event_count_view);
++  widget->client_view()->AddChildView(event_count_view);
+   event_count_view->SetBounds(0, 0, 600, 600);
+   widget->Show();
+ 
+@@ -3819,81 +3819,6 @@ TEST_F(WidgetTest, MouseWheelEvent) {
+   EXPECT_EQ(1, event_count_view->GetEventCount(ui::ET_MOUSEWHEEL));
+ }
+ 
+-class LayoutCountingView : public View {
+- public:
+-  LayoutCountingView() = default;
+-  ~LayoutCountingView() override = default;
+-
+-  void set_layout_closure(base::OnceClosure layout_closure) {
+-    layout_closure_ = std::move(layout_closure);
+-  }
+-
+-  size_t GetAndClearLayoutCount() {
+-    const size_t count = layout_count_;
+-    layout_count_ = 0u;
+-    return count;
+-  }
+-
+-  // View:
+-  void Layout() override {
+-    ++layout_count_;
+-    View::Layout();
+-    if (layout_closure_)
+-      std::move(layout_closure_).Run();
+-  }
+-
+- private:
+-  size_t layout_count_ = 0u;
+-
+-  // If valid, this is run when Layout() is called.
+-  base::OnceClosure layout_closure_;
+-
+-  DISALLOW_COPY_AND_ASSIGN(LayoutCountingView);
+-};
+-
+-using WidgetInvalidateLayoutTest = ViewsTestBaseWithNativeWidgetType;
+-
+-TEST_P(WidgetInvalidateLayoutTest, InvalidateLayout) {
+-  std::unique_ptr<Widget> widget =
+-      CreateTestWidget(Widget::InitParams::TYPE_WINDOW);
+-  LayoutCountingView* view =
+-      widget->widget_delegate()->GetContentsView()->AddChildView(
+-          std::make_unique<LayoutCountingView>());
+-  view->parent()->SetLayoutManager(std::make_unique<FillLayout>());
+-  // Force an initial Layout().
+-  // TODO(sky): this shouldn't be necessary, adding a child view should trigger
+-  // ScheduleLayout().
+-  view->Layout();
+-  widget->Show();
+-
+-  ui::Compositor* compositor = widget->GetCompositor();
+-  ASSERT_TRUE(compositor);
+-  compositor->ScheduleDraw();
+-  ui::DrawWaiterForTest::WaitForCompositingEnded(compositor);
+-
+-  base::RunLoop run_loop;
+-  view->GetAndClearLayoutCount();
+-  // Don't use WaitForCompositingEnded() here as it's entirely possible nothing
+-  // will be drawn (which means WaitForCompositingEnded() isn't run). Instead
+-  // wait for Layout() to be called.
+-  view->set_layout_closure(run_loop.QuitClosure());
+-  EXPECT_FALSE(ViewTestApi(view).needs_layout());
+-  EXPECT_FALSE(ViewTestApi(widget->GetRootView()).needs_layout());
+-  view->InvalidateLayout();
+-  EXPECT_TRUE(ViewTestApi(view).needs_layout());
+-  EXPECT_TRUE(ViewTestApi(widget->GetRootView()).needs_layout());
+-  run_loop.Run();
+-  EXPECT_EQ(1u, view->GetAndClearLayoutCount());
+-  EXPECT_FALSE(ViewTestApi(view).needs_layout());
+-  EXPECT_FALSE(ViewTestApi(widget->GetRootView()).needs_layout());
+-}
+-
+-INSTANTIATE_TEST_SUITE_P(
+-    WidgetInvalidateLayoutTest,
+-    WidgetInvalidateLayoutTest,
+-    ::testing::Values(ViewsTestBase::NativeWidgetType::kDefault,
+-                      ViewsTestBase::NativeWidgetType::kDesktop));
+-
+ class WidgetShadowTest : public WidgetTest {
+  public:
+   WidgetShadowTest() = default;
+diff --git a/ui/views/window/client_view.cc b/ui/views/window/client_view.cc
+index 5527e26da2710ecfc50d361c5834495d622a935d..227efb29047d87b39e2e9b7e0b45b027000ea761 100644
+--- a/ui/views/window/client_view.cc
++++ b/ui/views/window/client_view.cc
+@@ -87,8 +87,6 @@ void ClientView::ViewHierarchyChanged(
+     // TODO(weili): This seems fragile and can be refactored.
+     // Tracked at https://crbug.com/1012466.
+     AddChildViewAt(contents_view_, 0);
+-  } else if (!details.is_add && details.child == contents_view_) {
+-    contents_view_ = nullptr;
+   }
+ }
+ 
+diff --git a/ui/views/window/custom_frame_view.cc b/ui/views/window/custom_frame_view.cc
+index eda68f689da621422ce17d7e1d50fe0ba4efe771..9c2f931e0e4d37c4653c9a7a7f34ea4d8b8a49b1 100644
+--- a/ui/views/window/custom_frame_view.cc
++++ b/ui/views/window/custom_frame_view.cc
+@@ -211,6 +211,7 @@ void CustomFrameView::Layout() {
+   }
+ 
+   LayoutClientView();
++  NonClientFrameView::Layout();
+ }
+ 
+ gfx::Size CustomFrameView::CalculatePreferredSize() const {
+diff --git a/ui/views/window/non_client_view.cc b/ui/views/window/non_client_view.cc
+index 73170394a794489dd4c409a29d7b0f47a887e1dd..7d8bb5d42ca2730810f8384463058014c2aa2259 100644
+--- a/ui/views/window/non_client_view.cc
++++ b/ui/views/window/non_client_view.cc
+@@ -11,6 +11,7 @@
+ #include "ui/accessibility/ax_node_data.h"
+ #include "ui/base/hit_test.h"
+ #include "ui/gfx/geometry/rect_conversions.h"
++#include "ui/views/layout/fill_layout.h"
+ #include "ui/views/metadata/metadata_impl_macros.h"
+ #include "ui/views/rect_based_targeting_utils.h"
+ #include "ui/views/view_targeter.h"
+@@ -24,18 +25,6 @@
+ 
+ namespace views {
+ 
+-namespace {
+-
+-// The frame view and the client view are always at these specific indices,
+-// because the RootView message dispatch sends messages to items higher in the
+-// z-order first and we always want the client view to have first crack at
+-// handling mouse messages.
+-constexpr int kFrameViewIndex = 0;
+-constexpr int kClientViewIndex = 1;
+-// The overlay view is always on top (view == children().back()).
+-
+-}  // namespace
+-
+ NonClientFrameView::~NonClientFrameView() = default;
+ 
+ bool NonClientFrameView::ShouldPaintAsActive() const {
+@@ -129,18 +118,19 @@ void NonClientFrameView::OnThemeChanged() {
+   SchedulePaint();
+ }
+ 
+-NonClientFrameView::NonClientFrameView() {
+-  SetEventTargeter(std::make_unique<views::ViewTargeter>(this));
+-}
++void NonClientFrameView::Layout() {
++  if (GetLayoutManager())
++    GetLayoutManager()->Layout(this);
+ 
+-// ViewTargeterDelegate:
+-bool NonClientFrameView::DoesIntersectRect(const View* target,
+-                                           const gfx::Rect& rect) const {
+-  CHECK_EQ(target, this);
++  views::ClientView* client_view = GetWidget()->client_view();
++  client_view->SetBoundsRect(GetBoundsForClientView());
++  SkPath client_clip;
++  if (GetClientMask(client_view->size(), &client_clip))
++    client_view->SetClipPath(client_clip);
++}
+ 
+-  // For the default case, we assume the non-client frame view never overlaps
+-  // the client view.
+-  return !GetWidget()->client_view()->bounds().Intersects(rect);
++NonClientFrameView::NonClientFrameView() {
++  SetEventTargeter(std::make_unique<views::ViewTargeter>(this));
+ }
+ 
+ #if defined(OS_WIN)
+@@ -165,13 +155,18 @@ NonClientView::~NonClientView() {
+ 
+ void NonClientView::SetFrameView(
+     std::unique_ptr<NonClientFrameView> frame_view) {
+-  // See comment in header about ownership.
+-  frame_view->set_owned_by_client();
+-  if (frame_view_.get())
++  // If there is an existing frame view, remove the client view before removing
++  // the frame view to prevent the client view from being deleted.
++  if (frame_view_.get()) {
++    frame_view_->RemoveChildView(client_view_);
+     RemoveChildView(frame_view_.get());
++  }
++
+   frame_view_ = std::move(frame_view);
+-  if (parent())
+-    AddChildViewAt(frame_view_.get(), kFrameViewIndex);
++  if (parent()) {
++    AddChildViewAt(frame_view_.get(), 0);
++    frame_view_->AddChildViewAt(client_view_, 0);
++  }
+ }
+ 
+ void NonClientView::SetOverlayView(View* view) {
+@@ -262,11 +257,6 @@ void NonClientView::Layout() {
+   // into a View hierarchy once" ( http://codereview.chromium.org/27317 ), but
+   // where that is still the case it should simply be fixed.
+   frame_view_->SetBoundsRect(GetLocalBounds());
+-  client_view_->SetBoundsRect(frame_view_->GetBoundsForClientView());
+-
+-  SkPath client_clip;
+-  if (frame_view_->GetClientMask(client_view_->size(), &client_clip))
+-    client_view_->SetClipPath(client_clip);
+ 
+   if (overlay_view_)
+     overlay_view_->SetBoundsRect(GetLocalBounds());
+@@ -302,8 +292,8 @@ void NonClientView::ViewHierarchyChanged(
+   // the various setters, and create and add children directly in the
+   // constructor.
+   if (details.is_add && GetWidget() && details.child == this) {
+-    AddChildViewAt(frame_view_.get(), kFrameViewIndex);
+-    AddChildViewAt(client_view_, kClientViewIndex);
++    AddChildViewAt(frame_view_.get(), 0);
++    frame_view_->AddChildViewAt(client_view_, 0);
+     if (overlay_view_)
+       AddChildView(overlay_view_);
+   }
+diff --git a/ui/views/window/non_client_view.h b/ui/views/window/non_client_view.h
+index 58558cc0df457ba3c077784906c5ea4093c301c9..c47c5b6353cbf242e6790651d8382d813cb58250 100644
+--- a/ui/views/window/non_client_view.h
++++ b/ui/views/window/non_client_view.h
+@@ -101,11 +101,7 @@ class VIEWS_EXPORT NonClientFrameView : public View,
+   // View:
+   void GetAccessibleNodeData(ui::AXNodeData* node_data) override;
+   void OnThemeChanged() override;
+-
+- protected:
+-  // ViewTargeterDelegate:
+-  bool DoesIntersectRect(const View* target,
+-                         const gfx::Rect& rect) const override;
++  void Layout() override;
+ 
+  private:
+ #if defined(OS_WIN)
+@@ -121,10 +117,11 @@ class VIEWS_EXPORT NonClientFrameView : public View,
+ //
+ //  The NonClientView is the logical root of all Views contained within a
+ //  Window, except for the RootView which is its parent and of which it is the
+-//  sole child. The NonClientView has two children, the NonClientFrameView which
++//  sole child. The NonClientView has one child, the NonClientFrameView which
+ //  is responsible for painting and responding to events from the non-client
+-//  portions of the window, and the ClientView, which is responsible for the
+-//  same for the client area of the window:
++//  portions of the window, and for forwarding events to its child, the
++//  ClientView, which is responsible for the same for the client area of the
++//  window:
+ //
+ //  +- views::Widget ------------------------------------+
+ //  | +- views::RootView ------------------------------+ |
+@@ -135,23 +132,17 @@ class VIEWS_EXPORT NonClientFrameView : public View,
+ //  | | | | << of the non-client areas of a     >> | | | |
+ //  | | | | << views::Widget.                   >> | | | |
+ //  | | | |                                        | | | |
+-//  | | | +----------------------------------------+ | | |
+-//  | | | +- views::ClientView or subclass --------+ | | |
+-//  | | | |                                        | | | |
+-//  | | | | << all painting and event receiving >> | | | |
+-//  | | | | << of the client areas of a         >> | | | |
+-//  | | | | << views::Widget.                   >> | | | |
+-//  | | | |                                        | | | |
++//  | | | | +- views::ClientView or subclass ----+ | | | |
++//  | | | | |                                    | | | | |
++//  | | | | | << all painting and event       >> | | | | |
++//  | | | | | << receiving of the client      >> | | | | |
++//  | | | | | << areas of a views::Widget.    >> | | | | |
++//  | | | | +----------------------------------+ | | | | |
+ //  | | | +----------------------------------------+ | | |
+ //  | | +--------------------------------------------+ | |
+ //  | +------------------------------------------------+ |
+ //  +----------------------------------------------------+
+ //
+-// The NonClientFrameView and ClientView are siblings because due to theme
+-// changes the NonClientFrameView may be replaced with different
+-// implementations (e.g. during the switch from DWM/Aero-Glass to Vista Basic/
+-// Classic rendering).
+-//
+ class VIEWS_EXPORT NonClientView : public View, public ViewTargeterDelegate {
+  public:
+   METADATA_HEADER(NonClientView);
+diff --git a/ui/views/window/non_client_view_unittest.cc b/ui/views/window/non_client_view_unittest.cc
+deleted file mode 100644
+index fd66cb2c607fe47102fc14f5cc4d946863ac274c..0000000000000000000000000000000000000000
+--- a/ui/views/window/non_client_view_unittest.cc
++++ /dev/null
+@@ -1,102 +0,0 @@
+-// Copyright 2018 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 "ui/views/window/non_client_view.h"
+-
+-#include <memory>
+-
+-#include "ui/views/test/views_test_base.h"
+-#include "ui/views/widget/widget_delegate.h"
+-#include "ui/views/window/client_view.h"
+-#include "ui/views/window/native_frame_view.h"
+-
+-namespace views {
+-namespace test {
+-
+-namespace {
+-
+-class NonClientFrameTestView : public NativeFrameView {
+- public:
+-  using NativeFrameView::NativeFrameView;
+-  int layout_count() const { return layout_count_; }
+-
+-  // NativeFrameView:
+-  void Layout() override {
+-    NativeFrameView::Layout();
+-    ++layout_count_;
+-  }
+-
+- private:
+-  int layout_count_ = 0;
+-};
+-
+-class ClientTestView : public ClientView {
+- public:
+-  using ClientView::ClientView;
+-  int layout_count() const { return layout_count_; }
+-
+-  // ClientView:
+-  void Layout() override {
+-    ClientView::Layout();
+-    ++layout_count_;
+-  }
+-
+- private:
+-  int layout_count_ = 0;
+-};
+-
+-class TestWidgetDelegate : public WidgetDelegateView {
+- public:
+-  // WidgetDelegateView:
+-  std::unique_ptr<NonClientFrameView> CreateNonClientFrameView(
+-      Widget* widget) override {
+-    return std::make_unique<NonClientFrameTestView>(widget);
+-  }
+-
+-  views::ClientView* CreateClientView(Widget* widget) override {
+-    return new ClientTestView(widget, this);
+-  }
+-};
+-
+-class NonClientViewTest : public ViewsTestBase {
+- public:
+-  Widget::InitParams CreateParams(Widget::InitParams::Type type) override {
+-    Widget::InitParams params = ViewsTestBase::CreateParams(type);
+-    params.delegate = new TestWidgetDelegate;
+-    return params;
+-  }
+-};
+-
+-}  // namespace
+-
+-// Ensure Layout() is not called excessively on a ClientView when Widget bounds
+-// are changing.
+-TEST_F(NonClientViewTest, OnlyLayoutChildViewsOnce) {
+-  std::unique_ptr<views::Widget> widget =
+-      CreateTestWidget(Widget::InitParams::TYPE_WINDOW);
+-
+-  NonClientView* non_client_view = widget->non_client_view();
+-  non_client_view->Layout();
+-
+-  auto* frame_view =
+-      static_cast<NonClientFrameTestView*>(non_client_view->frame_view());
+-  auto* client_view =
+-      static_cast<ClientTestView*>(non_client_view->client_view());
+-
+-  int initial_frame_view_layouts = frame_view->layout_count();
+-  int initial_client_view_layouts = client_view->layout_count();
+-
+-  // Make sure it does no layout when nothing has changed.
+-  non_client_view->Layout();
+-  EXPECT_EQ(frame_view->layout_count(), initial_frame_view_layouts);
+-  EXPECT_EQ(client_view->layout_count(), initial_client_view_layouts);
+-
+-  // Ensure changing bounds triggers a (single) layout.
+-  widget->SetBounds(gfx::Rect(0, 0, 161, 100));
+-  EXPECT_EQ(frame_view->layout_count(), initial_frame_view_layouts + 1);
+-  EXPECT_EQ(client_view->layout_count(), initial_client_view_layouts + 1);
+-}
+-
+-}  // namespace test
+-}  // namespace views

+ 41 - 1
shell/browser/native_window.cc

@@ -24,6 +24,34 @@
 #include "ui/display/win/screen_win.h"
 #endif
 
+namespace gin {
+
+template <>
+struct Converter<electron::NativeWindow::TitleBarStyle> {
+  static bool FromV8(v8::Isolate* isolate,
+                     v8::Handle<v8::Value> val,
+                     electron::NativeWindow::TitleBarStyle* out) {
+    using TitleBarStyle = electron::NativeWindow::TitleBarStyle;
+    std::string title_bar_style;
+    if (!ConvertFromV8(isolate, val, &title_bar_style))
+      return false;
+    if (title_bar_style == "hidden") {
+      *out = TitleBarStyle::kHidden;
+#if defined(OS_MAC)
+    } else if (title_bar_style == "hiddenInset") {
+      *out = TitleBarStyle::kHiddenInset;
+    } else if (title_bar_style == "customButtonsOnHover") {
+      *out = TitleBarStyle::kCustomButtonsOnHover;
+#endif
+    } else {
+      return false;
+    }
+    return true;
+  }
+};
+
+}  // namespace gin
+
 namespace electron {
 
 namespace {
@@ -54,7 +82,19 @@ NativeWindow::NativeWindow(const gin_helper::Dictionary& options,
   options.Get(options::kFrame, &has_frame_);
   options.Get(options::kTransparent, &transparent_);
   options.Get(options::kEnableLargerThanScreen, &enable_larger_than_screen_);
-  options.Get(options::ktitleBarOverlay, &titlebar_overlay_);
+  options.Get(options::kTitleBarStyle, &title_bar_style_);
+
+  v8::Local<v8::Value> titlebar_overlay;
+  if (options.Get(options::ktitleBarOverlay, &titlebar_overlay)) {
+    if (titlebar_overlay->IsBoolean()) {
+      options.Get(options::ktitleBarOverlay, &titlebar_overlay_);
+    } else if (titlebar_overlay->IsObject()) {
+      titlebar_overlay_ = true;
+#if !defined(OS_WIN)
+      DCHECK(false);
+#endif
+    }
+  }
 
   if (parent)
     options.Get("modal", &is_modal_);

+ 12 - 0
shell/browser/native_window.h

@@ -315,6 +315,14 @@ class NativeWindow : public base::SupportsUserData,
   views::Widget* widget() const { return widget_.get(); }
   views::View* content_view() const { return content_view_; }
 
+  enum class TitleBarStyle {
+    kNormal,
+    kHidden,
+    kHiddenInset,
+    kCustomButtonsOnHover,
+  };
+  TitleBarStyle title_bar_style() const { return title_bar_style_; }
+
   bool has_frame() const { return has_frame_; }
   void set_has_frame(bool has_frame) { has_frame_ = has_frame; }
 
@@ -346,8 +354,12 @@ class NativeWindow : public base::SupportsUserData,
         [&browser_view](NativeBrowserView* n) { return (n == browser_view); });
   }
 
+  // The boolean parsing of the "titleBarOverlay" option
   bool titlebar_overlay_ = false;
 
+  // The "titleBarStyle" option.
+  TitleBarStyle title_bar_style_ = TitleBarStyle::kNormal;
+
  private:
   std::unique_ptr<views::Widget> widget_;
 

+ 0 - 11
shell/browser/native_window_mac.h

@@ -183,14 +183,6 @@ class NativeWindowMac : public NativeWindow,
     kInactive,
   };
 
-  enum class TitleBarStyle {
-    kNormal,
-    kHidden,
-    kHiddenInset,
-    kCustomButtonsOnHover,
-  };
-  TitleBarStyle title_bar_style() const { return title_bar_style_; }
-
   ElectronPreviewItem* preview_item() const { return preview_item_.get(); }
   ElectronTouchBar* touch_bar() const { return touch_bar_.get(); }
   bool zoom_to_page_width() const { return zoom_to_page_width_; }
@@ -263,9 +255,6 @@ class NativeWindowMac : public NativeWindow,
   // The presentation options before entering kiosk mode.
   NSApplicationPresentationOptions kiosk_options_;
 
-  // The "titleBarStyle" option.
-  TitleBarStyle title_bar_style_ = TitleBarStyle::kNormal;
-
   // The "visualEffectState" option.
   VisualEffectState visual_effect_state_ = VisualEffectState::kFollowWindow;
 

+ 0 - 23
shell/browser/native_window_mac.mm

@@ -164,28 +164,6 @@
 
 namespace gin {
 
-template <>
-struct Converter<electron::NativeWindowMac::TitleBarStyle> {
-  static bool FromV8(v8::Isolate* isolate,
-                     v8::Handle<v8::Value> val,
-                     electron::NativeWindowMac::TitleBarStyle* out) {
-    using TitleBarStyle = electron::NativeWindowMac::TitleBarStyle;
-    std::string title_bar_style;
-    if (!ConvertFromV8(isolate, val, &title_bar_style))
-      return false;
-    if (title_bar_style == "hidden") {
-      *out = TitleBarStyle::kHidden;
-    } else if (title_bar_style == "hiddenInset") {
-      *out = TitleBarStyle::kHiddenInset;
-    } else if (title_bar_style == "customButtonsOnHover") {
-      *out = TitleBarStyle::kCustomButtonsOnHover;
-    } else {
-      return false;
-    }
-    return true;
-  }
-};
-
 template <>
 struct Converter<electron::NativeWindowMac::VisualEffectState> {
   static bool FromV8(v8::Isolate* isolate,
@@ -274,7 +252,6 @@ NativeWindowMac::NativeWindowMac(const gin_helper::Dictionary& options,
                    height);
 
   options.Get(options::kResizable, &resizable_);
-  options.Get(options::kTitleBarStyle, &title_bar_style_);
   options.Get(options::kZoomToPageWidth, &zoom_to_page_width_);
   options.Get(options::kSimpleFullScreen, &always_simple_fullscreen_);
   options.GetOptional(options::kTrafficLightPosition, &traffic_light_position_);

+ 33 - 0
shell/browser/native_window_views.cc

@@ -71,12 +71,14 @@
 
 #elif defined(OS_WIN)
 #include "base/win/win_util.h"
+#include "extensions/common/image_util.h"
 #include "shell/browser/ui/views/win_frame_view.h"
 #include "shell/browser/ui/win/electron_desktop_native_widget_aura.h"
 #include "skia/ext/skia_utils_win.h"
 #include "ui/base/win/shell.h"
 #include "ui/display/screen.h"
 #include "ui/display/win/screen_win.h"
+#include "ui/gfx/color_utils.h"
 #include "ui/views/widget/desktop_aura/desktop_native_widget_aura.h"
 #endif
 
@@ -170,6 +172,37 @@ NativeWindowViews::NativeWindowViews(const gin_helper::Dictionary& options,
   options.Get("thickFrame", &thick_frame_);
   if (transparent())
     thick_frame_ = false;
+
+  overlay_button_color_ = color_utils::GetSysSkColor(COLOR_BTNFACE);
+  overlay_symbol_color_ = color_utils::GetSysSkColor(COLOR_BTNTEXT);
+
+  v8::Local<v8::Value> titlebar_overlay;
+  if (options.Get(options::ktitleBarOverlay, &titlebar_overlay) &&
+      titlebar_overlay->IsObject()) {
+    v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
+    gin_helper::Dictionary titlebar_overlay_obj =
+        gin::Dictionary::CreateEmpty(isolate);
+    options.Get(options::ktitleBarOverlay, &titlebar_overlay_obj);
+
+    std::string overlay_color_string;
+    if (titlebar_overlay_obj.Get(options::kOverlayButtonColor,
+                                 &overlay_color_string)) {
+      bool success = extensions::image_util::ParseCssColorString(
+          overlay_color_string, &overlay_button_color_);
+      DCHECK(success);
+    }
+
+    std::string overlay_symbol_color_string;
+    if (titlebar_overlay_obj.Get(options::kOverlaySymbolColor,
+                                 &overlay_symbol_color_string)) {
+      bool success = extensions::image_util::ParseCssColorString(
+          overlay_symbol_color_string, &overlay_symbol_color_);
+      DCHECK(success);
+    }
+  }
+
+  if (title_bar_style_ != TitleBarStyle::kNormal)
+    set_has_frame(false);
 #endif
 
   if (enable_larger_than_screen())

+ 15 - 0
shell/browser/native_window_views.h

@@ -19,6 +19,7 @@
 #if defined(OS_WIN)
 #include "base/win/scoped_gdi_object.h"
 #include "shell/browser/ui/win/taskbar_host.h"
+
 #endif
 
 namespace views {
@@ -174,6 +175,15 @@ class NativeWindowViews : public NativeWindow,
   TaskbarHost& taskbar_host() { return taskbar_host_; }
 #endif
 
+#if defined(OS_WIN)
+  bool IsWindowControlsOverlayEnabled() const {
+    return (title_bar_style_ == NativeWindowViews::TitleBarStyle::kHidden) &&
+           titlebar_overlay_;
+  }
+  SkColor overlay_button_color() const { return overlay_button_color_; }
+  SkColor overlay_symbol_color() const { return overlay_symbol_color_; }
+#endif
+
  private:
   // views::WidgetObserver:
   void OnWidgetActivationChanged(views::Widget* widget, bool active) override;
@@ -294,6 +304,11 @@ class NativeWindowViews : public NativeWindow,
 
   // Whether the window is currently being moved.
   bool is_moving_ = false;
+
+  // The color to use as the theme and symbol colors respectively for Window
+  // Controls Overlay if enabled on Windows.
+  SkColor overlay_button_color_;
+  SkColor overlay_symbol_color_;
 #endif
 
   // Handles unhandled keyboard messages coming back from the renderer process.

+ 5 - 5
shell/browser/ui/views/frameless_view.cc

@@ -85,17 +85,17 @@ int FramelessView::NonClientHitTest(const gfx::Point& cursor) {
       return HTCAPTION;
   }
 
+  // Support resizing frameless window by dragging the border.
+  int frame_component = ResizingBorderHitTest(cursor);
+  if (frame_component != HTNOWHERE)
+    return frame_component;
+
   // Check for possible draggable region in the client area for the frameless
   // window.
   SkRegion* draggable_region = window_->draggable_region();
   if (draggable_region && draggable_region->contains(cursor.x(), cursor.y()))
     return HTCAPTION;
 
-  // Support resizing frameless window by dragging the border.
-  int frame_component = ResizingBorderHitTest(cursor);
-  if (frame_component != HTNOWHERE)
-    return frame_component;
-
   return HTCLIENT;
 }
 

+ 2 - 0
shell/browser/ui/views/frameless_view.h

@@ -48,6 +48,8 @@ class FramelessView : public views::NonClientFrameView {
   NativeWindowViews* window_ = nullptr;
   views::Widget* frame_ = nullptr;
 
+  friend class NativeWindowsViews;
+
  private:
   DISALLOW_COPY_AND_ASSIGN(FramelessView);
 };

+ 220 - 0
shell/browser/ui/views/win_caption_button.cc

@@ -0,0 +1,220 @@
+// Copyright (c) 2016 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 "shell/browser/ui/views/win_caption_button.h"
+
+#include <utility>
+
+#include "base/i18n/rtl.h"
+#include "base/numerics/safe_conversions.h"
+#include "chrome/browser/ui/frame/window_frame_util.h"
+#include "chrome/grit/theme_resources.h"
+#include "shell/browser/ui/views/win_frame_view.h"
+#include "shell/common/color_util.h"
+#include "ui/base/theme_provider.h"
+#include "ui/gfx/animation/tween.h"
+#include "ui/gfx/color_utils.h"
+#include "ui/gfx/geometry/rect_conversions.h"
+#include "ui/gfx/scoped_canvas.h"
+#include "ui/views/metadata/metadata_impl_macros.h"
+
+namespace electron {
+
+WinCaptionButton::WinCaptionButton(PressedCallback callback,
+                                   WinFrameView* frame_view,
+                                   ViewID button_type,
+                                   const std::u16string& accessible_name)
+    : views::Button(std::move(callback)),
+      frame_view_(frame_view),
+      button_type_(button_type) {
+  SetAnimateOnStateChange(true);
+  // Not focusable by default, only for accessibility.
+  SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY);
+  SetAccessibleName(accessible_name);
+}
+
+gfx::Size WinCaptionButton::CalculatePreferredSize() const {
+  // TODO(bsep): The sizes in this function are for 1x device scale and don't
+  // match Windows button sizes at hidpi.
+  int height = WindowFrameUtil::kWindows10GlassCaptionButtonHeightRestored;
+  int base_width = WindowFrameUtil::kWindows10GlassCaptionButtonWidth;
+  return gfx::Size(base_width + GetBetweenButtonSpacing(), height);
+}
+
+void WinCaptionButton::OnPaintBackground(gfx::Canvas* canvas) {
+  // Paint the background of the button (the semi-transparent rectangle that
+  // appears when you hover or press the button).
+
+  const SkColor bg_color = frame_view_->window()->overlay_button_color();
+  const SkAlpha theme_alpha = SkColorGetA(bg_color);
+
+  gfx::Rect bounds = GetContentsBounds();
+  bounds.Inset(0, 0, 0, 0);
+
+  canvas->FillRect(bounds, SkColorSetA(bg_color, theme_alpha));
+
+  SkColor base_color;
+  SkAlpha hovered_alpha, pressed_alpha;
+  if (button_type_ == VIEW_ID_CLOSE_BUTTON) {
+    base_color = SkColorSetRGB(0xE8, 0x11, 0x23);
+    hovered_alpha = SK_AlphaOPAQUE;
+    pressed_alpha = 0x98;
+  } else {
+    // Match the native buttons.
+    base_color = frame_view_->GetReadableFeatureColor(bg_color);
+    hovered_alpha = 0x1A;
+    pressed_alpha = 0x33;
+
+    if (theme_alpha > 0) {
+      // Theme buttons have slightly increased opacity to make them stand out
+      // against a visually-busy frame image.
+      constexpr float kAlphaScale = 1.3f;
+      hovered_alpha = base::ClampRound<SkAlpha>(hovered_alpha * kAlphaScale);
+      pressed_alpha = base::ClampRound<SkAlpha>(pressed_alpha * kAlphaScale);
+    }
+  }
+
+  SkAlpha alpha;
+  if (GetState() == STATE_PRESSED)
+    alpha = pressed_alpha;
+  else
+    alpha = gfx::Tween::IntValueBetween(hover_animation().GetCurrentValue(),
+                                        SK_AlphaTRANSPARENT, hovered_alpha);
+  canvas->FillRect(bounds, SkColorSetA(base_color, alpha));
+}
+
+void WinCaptionButton::PaintButtonContents(gfx::Canvas* canvas) {
+  PaintSymbol(canvas);
+}
+
+int WinCaptionButton::GetBetweenButtonSpacing() const {
+  const int display_order_index = GetButtonDisplayOrderIndex();
+  return display_order_index == 0
+             ? 0
+             : WindowFrameUtil::kWindows10GlassCaptionButtonVisualSpacing;
+}
+
+int WinCaptionButton::GetButtonDisplayOrderIndex() const {
+  int button_display_order = 0;
+  switch (button_type_) {
+    case VIEW_ID_MINIMIZE_BUTTON:
+      button_display_order = 0;
+      break;
+    case VIEW_ID_MAXIMIZE_BUTTON:
+    case VIEW_ID_RESTORE_BUTTON:
+      button_display_order = 1;
+      break;
+    case VIEW_ID_CLOSE_BUTTON:
+      button_display_order = 2;
+      break;
+    default:
+      NOTREACHED();
+      return 0;
+  }
+
+  // Reverse the ordering if we're in RTL mode
+  if (base::i18n::IsRTL())
+    button_display_order = 2 - button_display_order;
+
+  return button_display_order;
+}
+
+namespace {
+
+// Canvas::DrawRect's stroke can bleed out of |rect|'s bounds, so this draws a
+// rectangle inset such that the result is constrained to |rect|'s size.
+void DrawRect(gfx::Canvas* canvas,
+              const gfx::Rect& rect,
+              const cc::PaintFlags& flags) {
+  gfx::RectF rect_f(rect);
+  float stroke_half_width = flags.getStrokeWidth() / 2;
+  rect_f.Inset(stroke_half_width, stroke_half_width);
+  canvas->DrawRect(rect_f, flags);
+}
+
+}  // namespace
+
+void WinCaptionButton::PaintSymbol(gfx::Canvas* canvas) {
+  SkColor symbol_color = frame_view_->window()->overlay_symbol_color();
+
+  if (button_type_ == VIEW_ID_CLOSE_BUTTON &&
+      hover_animation().is_animating()) {
+    symbol_color = gfx::Tween::ColorValueBetween(
+        hover_animation().GetCurrentValue(), symbol_color, SK_ColorWHITE);
+  } else if (button_type_ == VIEW_ID_CLOSE_BUTTON &&
+             (GetState() == STATE_HOVERED || GetState() == STATE_PRESSED)) {
+    symbol_color = SK_ColorWHITE;
+  }
+
+  gfx::ScopedCanvas scoped_canvas(canvas);
+  const float scale = canvas->UndoDeviceScaleFactor();
+
+  const int symbol_size_pixels = std::round(10 * scale);
+  gfx::RectF bounds_rect(GetContentsBounds());
+  bounds_rect.Scale(scale);
+  gfx::Rect symbol_rect(gfx::ToEnclosingRect(bounds_rect));
+  symbol_rect.ClampToCenteredSize(
+      gfx::Size(symbol_size_pixels, symbol_size_pixels));
+
+  cc::PaintFlags flags;
+  flags.setAntiAlias(false);
+  flags.setColor(symbol_color);
+  flags.setStyle(cc::PaintFlags::kStroke_Style);
+  // Stroke width jumps up a pixel every time we reach a new integral scale.
+  const int stroke_width = std::floor(scale);
+  flags.setStrokeWidth(stroke_width);
+
+  switch (button_type_) {
+    case VIEW_ID_MINIMIZE_BUTTON: {
+      const int y = symbol_rect.CenterPoint().y();
+      const gfx::Point p1 = gfx::Point(symbol_rect.x(), y);
+      const gfx::Point p2 = gfx::Point(symbol_rect.right(), y);
+      canvas->DrawLine(p1, p2, flags);
+      return;
+    }
+
+    case VIEW_ID_MAXIMIZE_BUTTON:
+      DrawRect(canvas, symbol_rect, flags);
+      return;
+
+    case VIEW_ID_RESTORE_BUTTON: {
+      // Bottom left ("in front") square.
+      const int separation = std::floor(2 * scale);
+      symbol_rect.Inset(0, separation, separation, 0);
+      DrawRect(canvas, symbol_rect, flags);
+
+      // Top right ("behind") square.
+      canvas->ClipRect(symbol_rect, SkClipOp::kDifference);
+      symbol_rect.Offset(separation, -separation);
+      DrawRect(canvas, symbol_rect, flags);
+      return;
+    }
+
+    case VIEW_ID_CLOSE_BUTTON: {
+      flags.setAntiAlias(true);
+      // The close button's X is surrounded by a "halo" of transparent pixels.
+      // When the X is white, the transparent pixels need to be a bit brighter
+      // to be visible.
+      const float stroke_halo =
+          stroke_width * (symbol_color == SK_ColorWHITE ? 0.1f : 0.05f);
+      flags.setStrokeWidth(stroke_width + stroke_halo);
+
+      // TODO(bsep): This sometimes draws misaligned at fractional device scales
+      // because the button's origin isn't necessarily aligned to pixels.
+      canvas->ClipRect(symbol_rect);
+      SkPath path;
+      path.moveTo(symbol_rect.x(), symbol_rect.y());
+      path.lineTo(symbol_rect.right(), symbol_rect.bottom());
+      path.moveTo(symbol_rect.right(), symbol_rect.y());
+      path.lineTo(symbol_rect.x(), symbol_rect.bottom());
+      canvas->DrawPath(path, flags);
+      return;
+    }
+
+    default:
+      NOTREACHED();
+      return;
+  }
+}
+}  // namespace electron

+ 54 - 0
shell/browser/ui/views/win_caption_button.h

@@ -0,0 +1,54 @@
+// Copyright (c) 2016 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.
+
+#ifndef SHELL_BROWSER_UI_VIEWS_WIN_CAPTION_BUTTON_H_
+#define SHELL_BROWSER_UI_VIEWS_WIN_CAPTION_BUTTON_H_
+
+#include "chrome/browser/ui/view_ids.h"
+#include "ui/gfx/canvas.h"
+#include "ui/views/controls/button/button.h"
+#include "ui/views/metadata/metadata_header_macros.h"
+
+namespace electron {
+
+class WinFrameView;
+
+class WinCaptionButton : public views::Button {
+ public:
+  WinCaptionButton(PressedCallback callback,
+                   WinFrameView* frame_view,
+                   ViewID button_type,
+                   const std::u16string& accessible_name);
+  WinCaptionButton(const WinCaptionButton&) = delete;
+  WinCaptionButton& operator=(const WinCaptionButton&) = delete;
+
+  // // views::Button:
+  gfx::Size CalculatePreferredSize() const override;
+  void OnPaintBackground(gfx::Canvas* canvas) override;
+  void PaintButtonContents(gfx::Canvas* canvas) override;
+
+  //  private:
+  // Returns the amount we should visually reserve on the left (right in RTL)
+  // for spacing between buttons. We do this instead of repositioning the
+  // buttons to avoid the sliver of deadspace that would result.
+  int GetBetweenButtonSpacing() const;
+
+  // Returns the order in which this button will be displayed (with 0 being
+  // drawn farthest to the left, and larger indices being drawn to the right of
+  // smaller indices).
+  int GetButtonDisplayOrderIndex() const;
+
+  // The base color to use for the button symbols and background blending. Uses
+  // the more readable of black and white.
+  SkColor GetBaseColor() const;
+
+  // Paints the minimize/maximize/restore/close icon for the button.
+  void PaintSymbol(gfx::Canvas* canvas);
+
+  WinFrameView* frame_view_;
+  ViewID button_type_;
+};
+}  // namespace electron
+
+#endif  // SHELL_BROWSER_UI_VIEWS_WIN_CAPTION_BUTTON_H_

+ 143 - 0
shell/browser/ui/views/win_caption_button_container.cc

@@ -0,0 +1,143 @@
+// Copyright 2020 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 "shell/browser/ui/views/win_caption_button_container.h"
+
+#include <memory>
+#include <utility>
+
+#include "shell/browser/ui/views/win_caption_button.h"
+#include "shell/browser/ui/views/win_frame_view.h"
+#include "ui/base/l10n/l10n_util.h"
+#include "ui/strings/grit/ui_strings.h"
+#include "ui/views/layout/flex_layout.h"
+#include "ui/views/view_class_properties.h"
+
+namespace electron {
+
+namespace {
+
+std::unique_ptr<WinCaptionButton> CreateCaptionButton(
+    views::Button::PressedCallback callback,
+    WinFrameView* frame_view,
+    ViewID button_type,
+    int accessible_name_resource_id) {
+  return std::make_unique<WinCaptionButton>(
+      std::move(callback), frame_view, button_type,
+      l10n_util::GetStringUTF16(accessible_name_resource_id));
+}
+
+bool HitTestCaptionButton(WinCaptionButton* button, const gfx::Point& point) {
+  return button && button->GetVisible() && button->bounds().Contains(point);
+}
+
+}  // anonymous namespace
+
+WinCaptionButtonContainer::WinCaptionButtonContainer(WinFrameView* frame_view)
+    : frame_view_(frame_view),
+      minimize_button_(AddChildView(CreateCaptionButton(
+          base::BindRepeating(&views::Widget::Minimize,
+                              base::Unretained(frame_view_->frame())),
+          frame_view_,
+          VIEW_ID_MINIMIZE_BUTTON,
+          IDS_APP_ACCNAME_MINIMIZE))),
+      maximize_button_(AddChildView(CreateCaptionButton(
+          base::BindRepeating(&views::Widget::Maximize,
+                              base::Unretained(frame_view_->frame())),
+          frame_view_,
+          VIEW_ID_MAXIMIZE_BUTTON,
+          IDS_APP_ACCNAME_MAXIMIZE))),
+      restore_button_(AddChildView(CreateCaptionButton(
+          base::BindRepeating(&views::Widget::Restore,
+                              base::Unretained(frame_view_->frame())),
+          frame_view_,
+          VIEW_ID_RESTORE_BUTTON,
+          IDS_APP_ACCNAME_RESTORE))),
+      close_button_(AddChildView(CreateCaptionButton(
+          base::BindRepeating(&views::Widget::CloseWithReason,
+                              base::Unretained(frame_view_->frame()),
+                              views::Widget::ClosedReason::kCloseButtonClicked),
+          frame_view_,
+          VIEW_ID_CLOSE_BUTTON,
+          IDS_APP_ACCNAME_CLOSE))) {
+  // Layout is horizontal, with buttons placed at the trailing end of the view.
+  // This allows the container to expand to become a faux titlebar/drag handle.
+  auto* const layout = SetLayoutManager(std::make_unique<views::FlexLayout>());
+  layout->SetOrientation(views::LayoutOrientation::kHorizontal)
+      .SetMainAxisAlignment(views::LayoutAlignment::kEnd)
+      .SetCrossAxisAlignment(views::LayoutAlignment::kStart)
+      .SetDefault(
+          views::kFlexBehaviorKey,
+          views::FlexSpecification(views::LayoutOrientation::kHorizontal,
+                                   views::MinimumFlexSizeRule::kPreferred,
+                                   views::MaximumFlexSizeRule::kPreferred,
+                                   /* adjust_width_for_height */ false,
+                                   views::MinimumFlexSizeRule::kScaleToZero));
+}
+
+WinCaptionButtonContainer::~WinCaptionButtonContainer() {}
+
+int WinCaptionButtonContainer::NonClientHitTest(const gfx::Point& point) const {
+  DCHECK(HitTestPoint(point))
+      << "should only be called with a point inside this view's bounds";
+  if (HitTestCaptionButton(minimize_button_, point)) {
+    return HTMINBUTTON;
+  }
+  if (HitTestCaptionButton(maximize_button_, point)) {
+    return HTMAXBUTTON;
+  }
+  if (HitTestCaptionButton(restore_button_, point)) {
+    return HTMAXBUTTON;
+  }
+  if (HitTestCaptionButton(close_button_, point)) {
+    return HTCLOSE;
+  }
+  return HTCAPTION;
+}
+
+void WinCaptionButtonContainer::ResetWindowControls() {
+  minimize_button_->SetState(views::Button::STATE_NORMAL);
+  maximize_button_->SetState(views::Button::STATE_NORMAL);
+  restore_button_->SetState(views::Button::STATE_NORMAL);
+  close_button_->SetState(views::Button::STATE_NORMAL);
+  InvalidateLayout();
+}
+
+void WinCaptionButtonContainer::AddedToWidget() {
+  views::Widget* const widget = GetWidget();
+
+  DCHECK(!widget_observation_.IsObserving());
+  widget_observation_.Observe(widget);
+
+  UpdateButtons();
+
+  if (frame_view_->window()->IsWindowControlsOverlayEnabled()) {
+    SetPaintToLayer();
+  }
+}
+
+void WinCaptionButtonContainer::RemovedFromWidget() {
+  DCHECK(widget_observation_.IsObserving());
+  widget_observation_.Reset();
+}
+
+void WinCaptionButtonContainer::OnWidgetBoundsChanged(
+    views::Widget* widget,
+    const gfx::Rect& new_bounds) {
+  UpdateButtons();
+}
+
+void WinCaptionButtonContainer::UpdateButtons() {
+  const bool is_maximized = frame_view_->frame()->IsMaximized();
+  restore_button_->SetVisible(is_maximized);
+  maximize_button_->SetVisible(!is_maximized);
+
+  // In touch mode, windows cannot be taken out of fullscreen or tiled mode, so
+  // the maximize/restore button should be disabled.
+  const bool is_touch = ui::TouchUiController::Get()->touch_ui();
+  restore_button_->SetEnabled(!is_touch);
+  maximize_button_->SetEnabled(!is_touch);
+  InvalidateLayout();
+}
+}  // namespace electron

+ 70 - 0
shell/browser/ui/views/win_caption_button_container.h

@@ -0,0 +1,70 @@
+// Copyright 2020 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.
+
+#ifndef SHELL_BROWSER_UI_VIEWS_WIN_CAPTION_BUTTON_CONTAINER_H_
+#define SHELL_BROWSER_UI_VIEWS_WIN_CAPTION_BUTTON_CONTAINER_H_
+
+#include "base/scoped_observation.h"
+#include "ui/base/pointer/touch_ui_controller.h"
+#include "ui/views/controls/button/button.h"
+#include "ui/views/metadata/metadata_header_macros.h"
+#include "ui/views/view.h"
+#include "ui/views/widget/widget.h"
+#include "ui/views/widget/widget_observer.h"
+
+namespace electron {
+
+class WinFrameView;
+class WinCaptionButton;
+
+// Provides a container for Windows 10 caption buttons that can be moved between
+// frame and browser window as needed. When extended horizontally, becomes a
+// grab bar for moving the window.
+class WinCaptionButtonContainer : public views::View,
+                                  public views::WidgetObserver {
+ public:
+  explicit WinCaptionButtonContainer(WinFrameView* frame_view);
+  ~WinCaptionButtonContainer() override;
+
+  // Tests to see if the specified |point| (which is expressed in this view's
+  // coordinates and which must be within this view's bounds) is within one of
+  // the caption buttons. Returns one of HitTestCompat enum defined in
+  // ui/base/hit_test.h, HTCAPTION if the area hit would be part of the window's
+  // drag handle, and HTNOWHERE otherwise.
+  // See also ClientView::NonClientHitTest.
+  int NonClientHitTest(const gfx::Point& point) const;
+
+ private:
+  // views::View:
+  void AddedToWidget() override;
+  void RemovedFromWidget() override;
+
+  // views::WidgetObserver:
+  void OnWidgetBoundsChanged(views::Widget* widget,
+                             const gfx::Rect& new_bounds) override;
+
+  void ResetWindowControls();
+
+  // Sets caption button visibility and enabled state based on window state.
+  // Only one of maximize or restore button should ever be visible at the same
+  // time, and both are disabled in tablet UI mode.
+  void UpdateButtons();
+
+  WinFrameView* const frame_view_;
+  WinCaptionButton* const minimize_button_;
+  WinCaptionButton* const maximize_button_;
+  WinCaptionButton* const restore_button_;
+  WinCaptionButton* const close_button_;
+
+  base::ScopedObservation<views::Widget, views::WidgetObserver>
+      widget_observation_{this};
+
+  base::CallbackListSubscription subscription_ =
+      ui::TouchUiController::Get()->RegisterCallback(
+          base::BindRepeating(&WinCaptionButtonContainer::UpdateButtons,
+                              base::Unretained(this)));
+};
+}  // namespace electron
+
+#endif  // SHELL_BROWSER_UI_VIEWS_WIN_CAPTION_BUTTON_CONTAINER_H_

+ 219 - 2
shell/browser/ui/views/win_frame_view.cc

@@ -1,11 +1,24 @@
 // Copyright (c) 2014 GitHub, Inc.
 // Use of this source code is governed by the MIT license that can be
 // found in the LICENSE file.
+//
+// Portions of this file are sourced from
+// chrome/browser/ui/views/frame/glass_browser_frame_view.cc,
+// Copyright (c) 2012 The Chromium Authors,
+// which is governed by a BSD-style license
 
 #include "shell/browser/ui/views/win_frame_view.h"
 
+#include <dwmapi.h>
+#include <memory>
+
 #include "base/win/windows_version.h"
 #include "shell/browser/native_window_views.h"
+#include "shell/browser/ui/views/win_caption_button_container.h"
+#include "ui/base/win/hwnd_metrics.h"
+#include "ui/display/win/dpi.h"
+#include "ui/display/win/screen_win.h"
+#include "ui/gfx/geometry/dip_util.h"
 #include "ui/views/widget/widget.h"
 #include "ui/views/win/hwnd_util.h"
 
@@ -17,6 +30,30 @@ WinFrameView::WinFrameView() {}
 
 WinFrameView::~WinFrameView() {}
 
+void WinFrameView::Init(NativeWindowViews* window, views::Widget* frame) {
+  window_ = window;
+  frame_ = frame;
+
+  if (window->IsWindowControlsOverlayEnabled()) {
+    caption_button_container_ =
+        AddChildView(std::make_unique<WinCaptionButtonContainer>(this));
+  } else {
+    caption_button_container_ = nullptr;
+  }
+}
+
+SkColor WinFrameView::GetReadableFeatureColor(SkColor background_color) {
+  // color_utils::GetColorWithMaxContrast()/IsDark() aren't used here because
+  // they switch based on the Chrome light/dark endpoints, while we want to use
+  // the system native behavior below.
+  const auto windows_luma = [](SkColor c) {
+    return 0.25f * SkColorGetR(c) + 0.625f * SkColorGetG(c) +
+           0.125f * SkColorGetB(c);
+  };
+  return windows_luma(background_color) <= 128.0f ? SK_ColorWHITE
+                                                  : SK_ColorBLACK;
+}
+
 gfx::Rect WinFrameView::GetWindowBoundsForClientBounds(
     const gfx::Rect& client_bounds) const {
   return views::GetWindowBoundsForClientBounds(
@@ -24,15 +61,195 @@ gfx::Rect WinFrameView::GetWindowBoundsForClientBounds(
       client_bounds);
 }
 
+int WinFrameView::FrameBorderThickness() const {
+  return (IsMaximized() || frame()->IsFullscreen())
+             ? 0
+             : display::win::ScreenWin::GetSystemMetricsInDIP(SM_CXSIZEFRAME);
+}
+
 int WinFrameView::NonClientHitTest(const gfx::Point& point) {
   if (window_->has_frame())
     return frame_->client_view()->NonClientHitTest(point);
-  else
-    return FramelessView::NonClientHitTest(point);
+
+  if (ShouldCustomDrawSystemTitlebar()) {
+    // See if the point is within any of the window controls.
+    if (caption_button_container_) {
+      gfx::Point local_point = point;
+
+      ConvertPointToTarget(parent(), caption_button_container_, &local_point);
+      if (caption_button_container_->HitTestPoint(local_point)) {
+        const int hit_test_result =
+            caption_button_container_->NonClientHitTest(local_point);
+        if (hit_test_result != HTNOWHERE)
+          return hit_test_result;
+      }
+    }
+
+    // On Windows 8+, the caption buttons are almost butted up to the top right
+    // corner of the window. This code ensures the mouse isn't set to a size
+    // cursor while hovering over the caption buttons, thus giving the incorrect
+    // impression that the user can resize the window.
+    if (base::win::GetVersion() >= base::win::Version::WIN8) {
+      RECT button_bounds = {0};
+      if (SUCCEEDED(DwmGetWindowAttribute(
+              views::HWNDForWidget(frame()), DWMWA_CAPTION_BUTTON_BOUNDS,
+              &button_bounds, sizeof(button_bounds)))) {
+        gfx::RectF button_bounds_in_dips = gfx::ConvertRectToDips(
+            gfx::Rect(button_bounds), display::win::GetDPIScale());
+        // TODO(crbug.com/1131681): GetMirroredRect() requires an integer rect,
+        // but the size in DIPs may not be an integer with a fractional device
+        // scale factor. If we want to keep using integers, the choice to use
+        // ToFlooredRectDeprecated() seems to be doing the wrong thing given the
+        // comment below about insetting 1 DIP instead of 1 physical pixel. We
+        // should probably use ToEnclosedRect() and then we could have inset 1
+        // physical pixel here.
+        gfx::Rect buttons = GetMirroredRect(
+            gfx::ToFlooredRectDeprecated(button_bounds_in_dips));
+
+        // There is a small one-pixel strip right above the caption buttons in
+        // which the resize border "peeks" through.
+        constexpr int kCaptionButtonTopInset = 1;
+        // The sizing region at the window edge above the caption buttons is
+        // 1 px regardless of scale factor. If we inset by 1 before converting
+        // to DIPs, the precision loss might eliminate this region entirely. The
+        // best we can do is to inset after conversion. This guarantees we'll
+        // show the resize cursor when resizing is possible. The cost of which
+        // is also maybe showing it over the portion of the DIP that isn't the
+        // outermost pixel.
+        buttons.Inset(0, kCaptionButtonTopInset, 0, 0);
+        if (buttons.Contains(point))
+          return HTNOWHERE;
+      }
+    }
+
+    int top_border_thickness = FrameTopBorderThickness(false);
+    // At the window corners the resize area is not actually bigger, but the 16
+    // pixels at the end of the top and bottom edges trigger diagonal resizing.
+    constexpr int kResizeCornerWidth = 16;
+    int window_component = GetHTComponentForFrame(
+        point, top_border_thickness, top_border_thickness, top_border_thickness,
+        kResizeCornerWidth - FrameBorderThickness(),
+        frame()->widget_delegate()->CanResize());
+    if (window_component != HTNOWHERE)
+      return window_component;
+  }
+
+  // Use the parent class's hittest last
+  return FramelessView::NonClientHitTest(point);
 }
 
 const char* WinFrameView::GetClassName() const {
   return kViewClassName;
 }
 
+bool WinFrameView::IsMaximized() const {
+  return frame()->IsMaximized();
+}
+
+bool WinFrameView::ShouldCustomDrawSystemTitlebar() const {
+  return window()->IsWindowControlsOverlayEnabled();
+}
+
+void WinFrameView::Layout() {
+  LayoutCaptionButtons();
+  if (window()->IsWindowControlsOverlayEnabled()) {
+    LayoutWindowControlsOverlay();
+  }
+  NonClientFrameView::Layout();
+}
+
+int WinFrameView::FrameTopBorderThickness(bool restored) const {
+  // Mouse and touch locations are floored but GetSystemMetricsInDIP is rounded,
+  // so we need to floor instead or else the difference will cause the hittest
+  // to fail when it ought to succeed.
+  return std::floor(
+      FrameTopBorderThicknessPx(restored) /
+      display::win::ScreenWin::GetScaleFactorForHWND(HWNDForView(this)));
+}
+
+int WinFrameView::FrameTopBorderThicknessPx(bool restored) const {
+  // Distinct from FrameBorderThickness() because we can't inset the top
+  // border, otherwise Windows will give us a standard titlebar.
+  // For maximized windows this is not true, and the top border must be
+  // inset in order to avoid overlapping the monitor above.
+
+  // See comments in BrowserDesktopWindowTreeHostWin::GetClientAreaInsets().
+  const bool needs_no_border =
+      (ShouldCustomDrawSystemTitlebar() && frame()->IsMaximized()) ||
+      frame()->IsFullscreen();
+  if (needs_no_border && !restored)
+    return 0;
+
+  // Note that this method assumes an equal resize handle thickness on all
+  // sides of the window.
+  // TODO(dfried): Consider having it return a gfx::Insets object instead.
+  return ui::GetFrameThickness(
+      MonitorFromWindow(HWNDForView(this), MONITOR_DEFAULTTONEAREST));
+}
+
+int WinFrameView::TitlebarMaximizedVisualHeight() const {
+  int maximized_height =
+      display::win::ScreenWin::GetSystemMetricsInDIP(SM_CYCAPTION);
+  return maximized_height;
+}
+
+int WinFrameView::TitlebarHeight(bool restored) const {
+  if (frame()->IsFullscreen() && !restored)
+    return 0;
+
+  return TitlebarMaximizedVisualHeight() + FrameTopBorderThickness(false);
+}
+
+int WinFrameView::WindowTopY() const {
+  // The window top is SM_CYSIZEFRAME pixels when maximized (see the comment in
+  // FrameTopBorderThickness()) and floor(system dsf) pixels when restored.
+  // Unfortunately we can't represent either of those at hidpi without using
+  // non-integral dips, so we return the closest reasonable values instead.
+  if (IsMaximized())
+    return FrameTopBorderThickness(false);
+
+  return 1;
+}
+
+void WinFrameView::LayoutCaptionButtons() {
+  if (!caption_button_container_)
+    return;
+
+  // Non-custom system titlebar already contains caption buttons.
+  if (!ShouldCustomDrawSystemTitlebar()) {
+    caption_button_container_->SetVisible(false);
+    return;
+  }
+
+  caption_button_container_->SetVisible(true);
+
+  const gfx::Size preferred_size =
+      caption_button_container_->GetPreferredSize();
+  int height = preferred_size.height();
+
+  height = IsMaximized() ? TitlebarMaximizedVisualHeight()
+                         : TitlebarHeight(false) - WindowTopY();
+
+  // TODO(mlaurencin): This -1 creates a 1 pixel gap between the right
+  // edge of the overlay and the edge of the window, allowing for this edge
+  // portion to return the correct hit test and be manually resized properly.
+  // Alternatives can be explored, but the differences in view structures
+  // between Electron and Chromium may result in this as the best option.
+
+  caption_button_container_->SetBounds(width() - preferred_size.width(),
+                                       WindowTopY(), preferred_size.width() - 1,
+                                       height);
+}
+
+void WinFrameView::LayoutWindowControlsOverlay() {
+  int overlay_height = caption_button_container_->size().height();
+  int overlay_width = caption_button_container_->size().width();
+  int bounding_rect_width = width() - overlay_width;
+  auto bounding_rect =
+      GetMirroredRect(gfx::Rect(0, 0, bounding_rect_width, overlay_height));
+
+  window()->SetWindowControlsOverlayRect(bounding_rect);
+  window()->NotifyLayoutWindowControlsOverlay();
+}
+
 }  // namespace electron

+ 58 - 0
shell/browser/ui/views/win_frame_view.h

@@ -1,11 +1,18 @@
 // Copyright (c) 2014 GitHub, Inc.
 // Use of this source code is governed by the MIT license that can be
 // found in the LICENSE file.
+//
+// Portions of this file are sourced from
+// chrome/browser/ui/views/frame/glass_browser_frame_view.h,
+// Copyright (c) 2012 The Chromium Authors,
+// which is governed by a BSD-style license
 
 #ifndef SHELL_BROWSER_UI_VIEWS_WIN_FRAME_VIEW_H_
 #define SHELL_BROWSER_UI_VIEWS_WIN_FRAME_VIEW_H_
 
+#include "shell/browser/native_window_views.h"
 #include "shell/browser/ui/views/frameless_view.h"
+#include "shell/browser/ui/views/win_caption_button.h"
 
 namespace electron {
 
@@ -15,6 +22,14 @@ class WinFrameView : public FramelessView {
   WinFrameView();
   ~WinFrameView() override;
 
+  void Init(NativeWindowViews* window, views::Widget* frame) override;
+
+  // Alpha to use for features in the titlebar (the window title and caption
+  // buttons) when the window is inactive. They are opaque when active.
+  static constexpr SkAlpha kInactiveTitlebarFeatureAlpha = 0x66;
+
+  SkColor GetReadableFeatureColor(SkColor background_color);
+
   // views::NonClientFrameView:
   gfx::Rect GetWindowBoundsForClientBounds(
       const gfx::Rect& client_bounds) const override;
@@ -23,7 +38,50 @@ class WinFrameView : public FramelessView {
   // views::View:
   const char* GetClassName() const override;
 
+  NativeWindowViews* window() const { return window_; }
+  views::Widget* frame() const { return frame_; }
+
+  bool IsMaximized() const;
+
+  bool ShouldCustomDrawSystemTitlebar() const;
+
+  // Visual height of the titlebar when the window is maximized (i.e. excluding
+  // the area above the top of the screen).
+  int TitlebarMaximizedVisualHeight() const;
+
+ protected:
+  // views::View:
+  void Layout() override;
+
  private:
+  friend class WinCaptionButtonContainer;
+
+  int FrameBorderThickness() const;
+
+  // Returns the thickness of the window border for the top edge of the frame,
+  // which is sometimes different than FrameBorderThickness(). Does not include
+  // the titlebar/tabstrip area. If |restored| is true, this is calculated as if
+  // the window was restored, regardless of its current state.
+  int FrameTopBorderThickness(bool restored) const;
+  int FrameTopBorderThicknessPx(bool restored) const;
+
+  // Returns the height of the titlebar for popups or other browser types that
+  // don't have tabs.
+  int TitlebarHeight(bool restored) const;
+
+  // Returns the y coordinate for the top of the frame, which in maximized mode
+  // is the top of the screen and in restored mode is 1 pixel below the top of
+  // the window to leave room for the visual border that Windows draws.
+  int WindowTopY() const;
+
+  void LayoutCaptionButtons();
+  void LayoutWindowControlsOverlay();
+
+  // The container holding the caption buttons (minimize, maximize, close, etc.)
+  // May be null if the caption button container is destroyed before the frame
+  // view. Always check for validity before using!
+  WinCaptionButtonContainer* caption_button_container_;
+
   DISALLOW_COPY_AND_ASSIGN(WinFrameView);
 };
 

+ 5 - 0
shell/common/options_switches.cc

@@ -31,6 +31,11 @@ const char kFullscreen[] = "fullscreen";
 const char kTrafficLightPosition[] = "trafficLightPosition";
 const char kRoundedCorners[] = "roundedCorners";
 
+// The color to use as the theme and symbol colors respectively for Window
+// Controls Overlay if enabled on Windows.
+const char kOverlayButtonColor[] = "color";
+const char kOverlaySymbolColor[] = "symbolColor";
+
 // Whether the window should show in taskbar.
 const char kSkipTaskbar[] = "skipTaskbar";
 

+ 2 - 0
shell/common/options_switches.h

@@ -58,6 +58,8 @@ extern const char kVisualEffectState[];
 extern const char kTrafficLightPosition[];
 extern const char kRoundedCorners[];
 extern const char ktitleBarOverlay[];
+extern const char kOverlayButtonColor[];
+extern const char kOverlaySymbolColor[];
 
 // WebPreferences.
 extern const char kZoomFactor[];

+ 16 - 5
spec-main/api-browser-window-spec.ts

@@ -5,6 +5,7 @@ import * as fs from 'fs';
 import * as os from 'os';
 import * as qs from 'querystring';
 import * as http from 'http';
+import * as semver from 'semver';
 import { AddressInfo } from 'net';
 import { app, BrowserWindow, BrowserView, dialog, ipcMain, OnBeforeSendHeadersListenerDetails, protocol, screen, webContents, session, WebContents, BrowserWindowConstructorOptions } from 'electron/main';
 
@@ -1882,7 +1883,7 @@ describe('BrowserWindow module', () => {
     });
   });
 
-  ifdescribe(process.platform === 'darwin' && parseInt(os.release().split('.')[0]) >= 14)('"titleBarStyle" option', () => {
+  ifdescribe(process.platform === 'win32' || (process.platform === 'darwin' && semver.gte(os.release(), '14.0.0')))('"titleBarStyle" option', () => {
     const testWindowsOverlay = async (style: any) => {
       const w = new BrowserWindow({
         show: false,
@@ -1896,12 +1897,22 @@ describe('BrowserWindow module', () => {
         titleBarOverlay: true
       });
       const overlayHTML = path.join(__dirname, 'fixtures', 'pages', 'overlay.html');
-      await w.loadFile(overlayHTML);
+      if (process.platform === 'darwin') {
+        await w.loadFile(overlayHTML);
+      } else {
+        const overlayReady = emittedOnce(ipcMain, 'geometrychange');
+        await w.loadFile(overlayHTML);
+        await overlayReady;
+      }
       const overlayEnabled = await w.webContents.executeJavaScript('navigator.windowControlsOverlay.visible');
       expect(overlayEnabled).to.be.true('overlayEnabled');
       const overlayRect = await w.webContents.executeJavaScript('getJSOverlayProperties()');
       expect(overlayRect.y).to.equal(0);
-      expect(overlayRect.x).to.be.greaterThan(0);
+      if (process.platform === 'darwin') {
+        expect(overlayRect.x).to.be.greaterThan(0);
+      } else {
+        expect(overlayRect.x).to.equal(0);
+      }
       expect(overlayRect.width).to.be.greaterThan(0);
       expect(overlayRect.height).to.be.greaterThan(0);
       const cssOverlayRect = await w.webContents.executeJavaScript('getCssOverlayProperties();');
@@ -1923,7 +1934,7 @@ describe('BrowserWindow module', () => {
       const contentSize = w.getContentSize();
       expect(contentSize).to.deep.equal([400, 400]);
     });
-    it('creates browser window with hidden inset title bar', () => {
+    ifit(process.platform === 'darwin')('creates browser window with hidden inset title bar', () => {
       const w = new BrowserWindow({
         show: false,
         width: 400,
@@ -1936,7 +1947,7 @@ describe('BrowserWindow module', () => {
     it('sets Window Control Overlay with hidden title bar', async () => {
       await testWindowsOverlay('hidden');
     });
-    it('sets Window Control Overlay with hidden inset title bar', async () => {
+    ifit(process.platform === 'darwin')('sets Window Control Overlay with hidden inset title bar', async () => {
       await testWindowsOverlay('hiddenInset');
     });
   });