Browse Source

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

* rebase "feat: enable windows control overlay on Windows"

* correct compilation error

* fix linting errors

* modify includes and build file

* change `hidden` option to `overlay`

* add patch to fix visual layout

* add button background color parameter

* add button text color parameter

* modify `overlay` in docs and modify button hover/press transition color

* change `text` to `symbol`

* remove todo and fix `text` replacement

* add new titleBarOverlay property and remove titleBarStyle `overlay`

* update browser and frameless window docs

* remove chromium patches

* chore: update patches

* change button hover color, update trailing `_`, update test file

* add dchecks, update title bar drawing checks, update test file

* modify for mac and linux builds

* update docs with overlayColor and overlaySymbolColor

* add corner and side hit test info

* modify docs and copyright info

* modify `titlebar_overlay_` as boolean or object

* move `title_bar_style_ to `NativeWindow`

* update docs with boolean and object titlebar_overlay_

* add `IsEmpty` checks

* move get options for boolean and object checks

* fix linting error

* disable `use_lld` for macos

* Update docs/api/frameless-window.md

Co-authored-by: John Kleinschmidt <[email protected]>

* Update docs/api/frameless-window.md

Co-authored-by: John Kleinschmidt <[email protected]>

* Update docs/api/frameless-window.md

Co-authored-by: John Kleinschmidt <[email protected]>

* Apply docs suggestions from code review

Co-authored-by: Jeremy Rose <[email protected]>

* modify `true` option description `titleBarOverlay`

* ci: cleanup keychain after tests on arm64 mac (#30472)

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

+ 3 - 0
chromium_src/BUILD.gn

@@ -76,8 +76,11 @@ static_library("chrome") {
       "//chrome/browser/extensions/global_shortcut_listener_win.h",
       "//chrome/browser/icon_loader_win.cc",
       "//chrome/browser/media/webrtc/window_icon_util_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",
     ]

+ 8 - 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.
@@ -392,10 +389,9 @@ 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` Object | 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`.
+    * `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.
 
 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

+ 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

+ 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",

+ 1 - 1
package.json

@@ -5,7 +5,7 @@
   "description": "Build cross platform desktop apps with JavaScript, HTML, and CSS",
   "devDependencies": {
     "@electron/docs-parser": "^0.12.1",
-    "@electron/typescript-definitions": "^8.9.4",
+    "@electron/typescript-definitions": "^8.9.5",
     "@octokit/auth-app": "^2.10.0",
     "@octokit/rest": "^18.0.3",
     "@primer/octicons": "^10.0.0",

+ 1 - 0
patches/chromium/.patches

@@ -103,3 +103,4 @@ hack_to_allow_gclient_sync_with_host_os_mac_on_linux_in_ci.patch
 don_t_run_pcscan_notifythreadcreated_if_pcscan_is_disabled.patch
 add_gin_wrappable_crash_key.patch
 logging_win32_only_create_a_console_if_logging_to_stderr.patch
+disable_use_lld_for_macos.patch

+ 21 - 0
patches/chromium/disable_use_lld_for_macos.patch

@@ -0,0 +1,21 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: mlaurencin <[email protected]>
+Date: Fri, 6 Aug 2021 15:29:48 -0700
+Subject: disable use_lld for macOS
+
+This patch disables use_lld on macOS, in order to prevent a linking bug
+that occurs when building for arm64.
+
+diff --git a/build/config/compiler/compiler.gni b/build/config/compiler/compiler.gni
+index 4a714ab0a5cdafded76bba553205f94599c27a30..d2723dd224a8afd2b6dd99ec11e9e71b7f76fe4a 100644
+--- a/build/config/compiler/compiler.gni
++++ b/build/config/compiler/compiler.gni
+@@ -201,7 +201,7 @@ declare_args() {
+   # In late bring-up on macOS (see docs/mac_lld.md), and not functional at all for
+   # iOS. The default linker everywhere else.
+   use_lld =
+-      is_clang && (!is_apple || (target_os == "mac" && chrome_pgo_phase != 1))
++      is_clang && !is_apple
+ }
+ 
+ declare_args() {

+ 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

@@ -316,6 +316,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; }
 
@@ -347,8 +355,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_; }
@@ -248,9 +240,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

@@ -165,28 +165,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,
@@ -276,7 +254,6 @@ NativeWindowMac::NativeWindowMac(const gin_helper::Dictionary& options,
 
   bool resizable = true;
   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

@@ -70,12 +70,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
 
@@ -165,6 +167,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

@@ -18,6 +18,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;
@@ -292,6 +302,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/metadata/metadata_impl_macros.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"
+
+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/base/metadata/metadata_header_macros.h"
+#include "ui/gfx/canvas.h"
+#include "ui/views/controls/button/button.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/metadata/metadata_header_macros.h"
+#include "ui/base/pointer/touch_ui_controller.h"
+#include "ui/views/controls/button/button.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_

+ 218 - 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() = default;
 
 WinFrameView::~WinFrameView() = default;
 
+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,194 @@ 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, gfx::Insets(top_border_thickness, 0, 0, 0), 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';
 
@@ -1876,7 +1877,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,
@@ -1890,12 +1891,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();');
@@ -1917,7 +1928,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,
@@ -1930,7 +1941,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');
     });
   });

+ 4 - 4
yarn.lock

@@ -33,10 +33,10 @@
     ora "^4.0.3"
     pretty-ms "^5.1.0"
 
-"@electron/typescript-definitions@^8.9.4":
-  version "8.9.4"
-  resolved "https://registry.yarnpkg.com/@electron/typescript-definitions/-/typescript-definitions-8.9.4.tgz#ec5c47aad3d45f2da2c40e22536720a9de64c2aa"
-  integrity sha512-KL3ohLe4D5lhJagBEj1Jpoh4BBiJMoCMlc6RSLPT+DaP0odgjEK+Ky6whRDW1cDDpgaSfMisdbN+CdOx3Y7JOg==
+"@electron/typescript-definitions@^8.9.5":
+  version "8.9.5"
+  resolved "https://registry.yarnpkg.com/@electron/typescript-definitions/-/typescript-definitions-8.9.5.tgz#e6cb08e0e7c9656e178b892eab50866a8a80bf7a"
+  integrity sha512-xDLFl6joGpA8c9cGSPWC3DFHyIGf9+OWZmDrPbGJW1URt6C1ukdQWKSmjb1Rttb94QQxBrGuUlSyz27IQgLFsw==
   dependencies:
     "@types/node" "^11.13.7"
     chalk "^2.4.2"