From 1f8a46c9c6d6ec4e597e4ae4abe0b1e898bc78b9 Mon Sep 17 00:00:00 2001 From: John Kleinschmidt Date: Thu, 1 Jul 2021 15:25:40 -0400 Subject: [PATCH] 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 * Update shell/browser/api/electron_api_browser_window.cc Co-authored-by: Samuel Attard * update per review feedback Co-authored-by: Samuel Attard --- docs/api/browser-window.md | 6 ++ docs/api/frameless-window.md | 17 ++++ .../api/electron_api_browser_window.cc | 16 ++++ .../browser/api/electron_api_browser_window.h | 1 + .../browser/api/electron_api_web_contents.cc | 4 + shell/browser/native_window.cc | 18 ++++ shell/browser/native_window.h | 8 ++ shell/browser/native_window_mac.h | 1 + shell/browser/native_window_mac.mm | 23 +++++ shell/browser/native_window_observer.h | 2 + shell/browser/ui/cocoa/window_buttons_view.h | 1 + shell/browser/ui/cocoa/window_buttons_view.mm | 4 + shell/common/options_switches.cc | 2 + shell/common/options_switches.h | 1 + spec-main/api-browser-window-spec.ts | 35 ++++++++ spec-main/fixtures/pages/overlay.html | 84 +++++++++++++++++++ 16 files changed, 223 insertions(+) create mode 100644 spec-main/fixtures/pages/overlay.html diff --git a/docs/api/browser-window.md b/docs/api/browser-window.md index d202ce94948d..61c4b00e03a9 100644 --- a/docs/api/browser-window.md +++ b/docs/api/browser-window.md @@ -392,6 +392,10 @@ 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`. When setting minimum or maximum window size with `minWidth`/`maxWidth`/ `minHeight`/`maxHeight`, it only constrains the users. It won't prevent you from @@ -1815,3 +1819,5 @@ removed in future Electron releases. [window-levels]: https://developer.apple.com/documentation/appkit/nswindow/level [chrome-content-scripts]: https://developer.chrome.com/extensions/content_scripts#execution-environment [event-emitter]: https://nodejs.org/api/events.html#events_class_eventemitter +[overlay-javascript-apis]: https://github.com/WICG/window-controls-overlay/blob/main/explainer.md#javascript-apis +[overlay-css-env-vars]: https://github.com/WICG/window-controls-overlay/blob/main/explainer.md#css-environment-variables diff --git a/docs/api/frameless-window.md b/docs/api/frameless-window.md index 8bb253e1948c..7a73c8925e20 100644 --- a/docs/api/frameless-window.md +++ b/docs/api/frameless-window.md @@ -61,6 +61,21 @@ const win = new BrowserWindow({ titleBarStyle: 'customButtonsOnHover', frame: fa 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: + +```javascript +const { BrowserWindow } = require('electron') +const win = new BrowserWindow({ + titleBarStyle: 'hiddenInset', + titleBarOverlay: true +}) +win.show() +``` + ## Transparent window By setting the `transparent` option to `true`, you can also make the frameless @@ -186,3 +201,5 @@ behave correctly on all platforms you should never use a custom context menu on draggable areas. [ignore-mouse-events]: browser-window.md#winsetignoremouseeventsignore-options +[overlay-javascript-apis]: https://github.com/WICG/window-controls-overlay/blob/main/explainer.md#javascript-apis +[overlay-css-env-vars]: https://github.com/WICG/window-controls-overlay/blob/main/explainer.md#css-environment-variables diff --git a/shell/browser/api/electron_api_browser_window.cc b/shell/browser/api/electron_api_browser_window.cc index 2f96326874f2..5b02132af542 100644 --- a/shell/browser/api/electron_api_browser_window.cc +++ b/shell/browser/api/electron_api_browser_window.cc @@ -57,6 +57,17 @@ BrowserWindow::BrowserWindow(gin::Arguments* args, web_preferences.Set(options::kShow, show); } + bool titleBarOverlay = false; + options.Get(options::ktitleBarOverlay, &titleBarOverlay); + if (titleBarOverlay) { + std::string enabled_features = ""; + if (web_preferences.Get(options::kEnableBlinkFeatures, &enabled_features)) { + enabled_features += ","; + } + enabled_features += features::kWebAppWindowControlsOverlay.name; + web_preferences.Set(options::kEnableBlinkFeatures, enabled_features); + } + // Copy the webContents option to webPreferences. This is only used internally // to implement nativeWindowOpen option. if (options.Get("webContents", &value)) { @@ -312,6 +323,11 @@ void BrowserWindow::OnWindowLeaveFullScreen() { BaseWindow::OnWindowLeaveFullScreen(); } +void BrowserWindow::UpdateWindowControlsOverlay( + const gfx::Rect& bounding_rect) { + web_contents()->UpdateWindowControlsOverlay(bounding_rect); +} + void BrowserWindow::CloseImmediately() { // Close all child windows before closing current window. v8::Locker locker(isolate()); diff --git a/shell/browser/api/electron_api_browser_window.h b/shell/browser/api/electron_api_browser_window.h index fb5a96a5cca5..33fdcece7828 100644 --- a/shell/browser/api/electron_api_browser_window.h +++ b/shell/browser/api/electron_api_browser_window.h @@ -69,6 +69,7 @@ class BrowserWindow : public BaseWindow, void RequestPreferredWidth(int* width) override; void OnCloseButtonClicked(bool* prevent_default) override; void OnWindowIsKeyChanged(bool is_key) override; + void UpdateWindowControlsOverlay(const gfx::Rect& bounding_rect) override; // BaseWindow: void OnWindowBlur() override; diff --git a/shell/browser/api/electron_api_web_contents.cc b/shell/browser/api/electron_api_web_contents.cc index 566515804e9d..4c27745ae710 100644 --- a/shell/browser/api/electron_api_web_contents.cc +++ b/shell/browser/api/electron_api_web_contents.cc @@ -1675,6 +1675,10 @@ void WebContents::ReadyToCommitNavigation( void WebContents::DidFinishNavigation( content::NavigationHandle* navigation_handle) { + if (owner_window_) { + owner_window_->NotifyLayoutWindowControlsOverlay(); + } + if (!navigation_handle->HasCommitted()) return; bool is_main_frame = navigation_handle->IsInMainFrame(); diff --git a/shell/browser/native_window.cc b/shell/browser/native_window.cc index 9bfd5a36f7dd..435b00783568 100644 --- a/shell/browser/native_window.cc +++ b/shell/browser/native_window.cc @@ -53,6 +53,7 @@ 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_); if (parent) options.Get("modal", &is_modal_); @@ -394,6 +395,14 @@ void NativeWindow::PreviewFile(const std::string& path, void NativeWindow::CloseFilePreview() {} +gfx::Rect NativeWindow::GetWindowControlsOverlayRect() { + return overlay_rect_; +} + +void NativeWindow::SetWindowControlsOverlayRect(const gfx::Rect& overlay_rect) { + overlay_rect_ = overlay_rect; +} + void NativeWindow::NotifyWindowRequestPreferredWith(int* width) { for (NativeWindowObserver& observer : observers_) observer.RequestPreferredWidth(width); @@ -493,6 +502,7 @@ void NativeWindow::NotifyWindowWillMove(const gfx::Rect& new_bounds, } void NativeWindow::NotifyWindowResize() { + NotifyLayoutWindowControlsOverlay(); for (NativeWindowObserver& observer : observers_) observer.OnWindowResize(); } @@ -591,6 +601,14 @@ void NativeWindow::NotifyWindowSystemContextMenu(int x, observer.OnSystemContextMenu(x, y, prevent_default); } +void NativeWindow::NotifyLayoutWindowControlsOverlay() { + gfx::Rect bounding_rect = GetWindowControlsOverlayRect(); + if (!bounding_rect.IsEmpty()) { + for (NativeWindowObserver& observer : observers_) + observer.UpdateWindowControlsOverlay(bounding_rect); + } +} + #if defined(OS_WIN) void NativeWindow::NotifyWindowMessage(UINT message, WPARAM w_param, diff --git a/shell/browser/native_window.h b/shell/browser/native_window.h index 0731f5c0b327..1241d70f8cef 100644 --- a/shell/browser/native_window.h +++ b/shell/browser/native_window.h @@ -255,6 +255,9 @@ class NativeWindow : public base::SupportsUserData, return weak_factory_.GetWeakPtr(); } + virtual gfx::Rect GetWindowControlsOverlayRect(); + virtual void SetWindowControlsOverlayRect(const gfx::Rect& overlay_rect); + // Methods called by the WebContents. virtual void HandleKeyboardEvent( content::WebContents*, @@ -299,6 +302,7 @@ class NativeWindow : public base::SupportsUserData, const base::DictionaryValue& details); void NotifyNewWindowForTab(); void NotifyWindowSystemContextMenu(int x, int y, bool* prevent_default); + void NotifyLayoutWindowControlsOverlay(); #if defined(OS_WIN) void NotifyWindowMessage(UINT message, WPARAM w_param, LPARAM l_param); @@ -343,6 +347,8 @@ class NativeWindow : public base::SupportsUserData, [&browser_view](NativeBrowserView* n) { return (n == browser_view); }); } + bool titlebar_overlay_ = false; + private: std::unique_ptr widget_; @@ -391,6 +397,8 @@ class NativeWindow : public base::SupportsUserData, // Accessible title. std::u16string accessible_title_; + gfx::Rect overlay_rect_; + base::WeakPtrFactory weak_factory_{this}; DISALLOW_COPY_AND_ASSIGN(NativeWindow); diff --git a/shell/browser/native_window_mac.h b/shell/browser/native_window_mac.h index 1bbe3862f7ae..343acd4adbe6 100644 --- a/shell/browser/native_window_mac.h +++ b/shell/browser/native_window_mac.h @@ -147,6 +147,7 @@ class NativeWindowMac : public NativeWindow, void CloseFilePreview() override; gfx::Rect ContentBoundsToWindowBounds(const gfx::Rect& bounds) const override; gfx::Rect WindowBoundsToContentBounds(const gfx::Rect& bounds) const override; + gfx::Rect GetWindowControlsOverlayRect() override; void NotifyWindowEnterFullScreen() override; void NotifyWindowLeaveFullScreen() override; void SetActive(bool is_key) override; diff --git a/shell/browser/native_window_mac.mm b/shell/browser/native_window_mac.mm index 0f46ea9fd71e..f89b25bd9def 100644 --- a/shell/browser/native_window_mac.mm +++ b/shell/browser/native_window_mac.mm @@ -1488,6 +1488,7 @@ void NativeWindowMac::SetVibrancy(const std::string& type) { void NativeWindowMac::SetWindowButtonVisibility(bool visible) { window_button_visibility_ = visible; InternalSetWindowButtonVisibility(visible); + NotifyLayoutWindowControlsOverlay(); } bool NativeWindowMac::GetWindowButtonVisibility() const { @@ -1505,6 +1506,7 @@ void NativeWindowMac::SetTrafficLightPosition( if (buttons_view_) { [buttons_view_ setMargin:traffic_light_position_]; [buttons_view_ viewDidMoveToWindow]; + NotifyLayoutWindowControlsOverlay(); } } @@ -1859,6 +1861,27 @@ void NativeWindowMac::SetForwardMouseMessages(bool forward) { [window_ setAcceptsMouseMovedEvents:forward]; } +gfx::Rect NativeWindowMac::GetWindowControlsOverlayRect() { + gfx::Rect bounding_rect; + if (titlebar_overlay_ && !has_frame() && buttons_view_ && + ![buttons_view_ isHidden]) { + NSRect button_frame = [buttons_view_ frame]; + gfx::Point buttons_view_margin = [buttons_view_ getMargin]; + const int overlay_width = GetContentSize().width() - NSWidth(button_frame) - + buttons_view_margin.x(); + CGFloat overlay_height = + NSHeight(button_frame) + buttons_view_margin.y() * 2; + if (base::i18n::IsRTL()) { + bounding_rect = gfx::Rect(0, 0, overlay_width, overlay_height); + } else { + bounding_rect = + gfx::Rect(button_frame.size.width + buttons_view_margin.x(), 0, + overlay_width, overlay_height); + } + } + return bounding_rect; +} + // static NativeWindow* NativeWindow::Create(const gin_helper::Dictionary& options, NativeWindow* parent) { diff --git a/shell/browser/native_window_observer.h b/shell/browser/native_window_observer.h index 4f33570de4b0..caa38e4ffa12 100644 --- a/shell/browser/native_window_observer.h +++ b/shell/browser/native_window_observer.h @@ -104,6 +104,8 @@ class NativeWindowObserver : public base::CheckedObserver { // Called on Windows when App Commands arrive (WM_APPCOMMAND) // Some commands are implemented on on other platforms as well virtual void OnExecuteAppCommand(const std::string& command_name) {} + + virtual void UpdateWindowControlsOverlay(const gfx::Rect& bounding_rect) {} }; } // namespace electron diff --git a/shell/browser/ui/cocoa/window_buttons_view.h b/shell/browser/ui/cocoa/window_buttons_view.h index 0adb34ac1be3..d1941bcb4276 100644 --- a/shell/browser/ui/cocoa/window_buttons_view.h +++ b/shell/browser/ui/cocoa/window_buttons_view.h @@ -26,6 +26,7 @@ - (void)setMargin:(const absl::optional&)margin; - (void)setShowOnHover:(BOOL)yes; - (void)setNeedsDisplayForButtons; +- (gfx::Point)getMargin; @end #endif // SHELL_BROWSER_UI_COCOA_WINDOW_BUTTONS_VIEW_H_ diff --git a/shell/browser/ui/cocoa/window_buttons_view.mm b/shell/browser/ui/cocoa/window_buttons_view.mm index 5ed4a2d14c69..c51aff3f5d3a 100644 --- a/shell/browser/ui/cocoa/window_buttons_view.mm +++ b/shell/browser/ui/cocoa/window_buttons_view.mm @@ -116,4 +116,8 @@ const NSWindowButton kButtonTypes[] = { [self setNeedsDisplayForButtons]; } +- (gfx::Point)getMargin { + return margin_; +} + @end diff --git a/shell/common/options_switches.cc b/shell/common/options_switches.cc index 1f036963feef..da619b9f5fe1 100644 --- a/shell/common/options_switches.cc +++ b/shell/common/options_switches.cc @@ -194,6 +194,8 @@ const char kEnableWebSQL[] = "enableWebSQL"; const char kEnablePreferredSizeMode[] = "enablePreferredSizeMode"; +const char ktitleBarOverlay[] = "titleBarOverlay"; + } // namespace options namespace switches { diff --git a/shell/common/options_switches.h b/shell/common/options_switches.h index e26ad9071297..47d0db155bfb 100644 --- a/shell/common/options_switches.h +++ b/shell/common/options_switches.h @@ -57,6 +57,7 @@ extern const char kVibrancyType[]; extern const char kVisualEffectState[]; extern const char kTrafficLightPosition[]; extern const char kRoundedCorners[]; +extern const char ktitleBarOverlay[]; // WebPreferences. extern const char kZoomFactor[]; diff --git a/spec-main/api-browser-window-spec.ts b/spec-main/api-browser-window-spec.ts index 11af61872372..f297d4b32e17 100644 --- a/spec-main/api-browser-window-spec.ts +++ b/spec-main/api-browser-window-spec.ts @@ -1876,7 +1876,36 @@ describe('BrowserWindow module', () => { }); ifdescribe(process.platform === 'darwin' && parseInt(os.release().split('.')[0]) >= 14)('"titleBarStyle" option', () => { + const testWindowsOverlay = async (style: any) => { + const w = new BrowserWindow({ + show: false, + width: 400, + height: 400, + titleBarStyle: style, + webPreferences: { + nodeIntegration: true, + contextIsolation: false + }, + titleBarOverlay: true + }); + const overlayHTML = path.join(__dirname, 'fixtures', 'pages', 'overlay.html'); + await w.loadFile(overlayHTML); + 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); + expect(overlayRect.width).to.be.greaterThan(0); + expect(overlayRect.height).to.be.greaterThan(0); + const cssOverlayRect = await w.webContents.executeJavaScript('getCssOverlayProperties();'); + expect(cssOverlayRect).to.deep.equal(overlayRect); + const geometryChange = emittedOnce(ipcMain, 'geometrychange'); + w.setBounds({ width: 800 }); + const [, newOverlayRect] = await geometryChange; + expect(newOverlayRect.width).to.equal(overlayRect.width + 400); + }; afterEach(closeAllWindows); + afterEach(() => { ipcMain.removeAllListeners('geometrychange'); }); it('creates browser window with hidden title bar', () => { const w = new BrowserWindow({ show: false, @@ -1897,6 +1926,12 @@ describe('BrowserWindow module', () => { const contentSize = w.getContentSize(); expect(contentSize).to.deep.equal([400, 400]); }); + it('sets Window Control Overlay with hidden title bar', async () => { + await testWindowsOverlay('hidden'); + }); + it('sets Window Control Overlay with hidden inset title bar', async () => { + await testWindowsOverlay('hiddenInset'); + }); }); ifdescribe(process.platform === 'darwin')('"enableLargerThanScreen" option', () => { diff --git a/spec-main/fixtures/pages/overlay.html b/spec-main/fixtures/pages/overlay.html new file mode 100644 index 000000000000..ef7408e53331 --- /dev/null +++ b/spec-main/fixtures/pages/overlay.html @@ -0,0 +1,84 @@ + + + + + + + + + +
+
+ Title goes here + +
+
+
+ + +