Skip to content

Commit

Permalink
feat: enable window controls overlay on macOS (#29986)
Browse files Browse the repository at this point in the history
* 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 1b58b5b.

* Add new titleBarOverlay property to manage feature

* spelling fix

* Update docs/api/frameless-window.md

Co-authored-by: Samuel Attard <sam@electronjs.org>

* Update shell/browser/api/electron_api_browser_window.cc

Co-authored-by: Samuel Attard <sam@electronjs.org>

* update per review feedback

Co-authored-by: John Kleinschmidt <jkleinsc@electronjs.org>
Co-authored-by: Samuel Attard <sam@electronjs.org>
  • Loading branch information
3 people committed Jul 8, 2021
1 parent 6699fea commit 9abfbee
Show file tree
Hide file tree
Showing 16 changed files with 223 additions and 0 deletions.
6 changes: 6 additions & 0 deletions docs/api/browser-window.md
Expand Up @@ -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
Expand Down Expand Up @@ -1806,3 +1810,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
17 changes: 17 additions & 0 deletions docs/api/frameless-window.md
Expand Up @@ -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
Expand Down Expand Up @@ -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
16 changes: 16 additions & 0 deletions shell/browser/api/electron_api_browser_window.cc
Expand Up @@ -59,6 +59,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)) {
Expand Down Expand Up @@ -314,6 +325,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());
Expand Down
1 change: 1 addition & 0 deletions shell/browser/api/electron_api_browser_window.h
Expand Up @@ -70,6 +70,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;
Expand Down
4 changes: 4 additions & 0 deletions shell/browser/api/electron_api_web_contents.cc
Expand Up @@ -1673,6 +1673,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();
Expand Down
18 changes: 18 additions & 0 deletions shell/browser/native_window.cc
Expand Up @@ -54,6 +54,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_);
Expand Down Expand Up @@ -395,6 +396,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);
Expand Down Expand Up @@ -493,6 +502,7 @@ void NativeWindow::NotifyWindowWillMove(const gfx::Rect& new_bounds,
}

void NativeWindow::NotifyWindowResize() {
NotifyLayoutWindowControlsOverlay();
for (NativeWindowObserver& observer : observers_)
observer.OnWindowResize();
}
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 8 additions & 0 deletions shell/browser/native_window.h
Expand Up @@ -256,6 +256,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*,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<views::Widget> widget_;

Expand Down Expand Up @@ -391,6 +397,8 @@ class NativeWindow : public base::SupportsUserData,
// Accessible title.
std::u16string accessible_title_;

gfx::Rect overlay_rect_;

base::WeakPtrFactory<NativeWindow> weak_factory_{this};

DISALLOW_COPY_AND_ASSIGN(NativeWindow);
Expand Down
1 change: 1 addition & 0 deletions shell/browser/native_window_mac.h
Expand Up @@ -148,6 +148,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;
Expand Down
23 changes: 23 additions & 0 deletions shell/browser/native_window_mac.mm
Expand Up @@ -1488,6 +1488,7 @@ void ViewDidMoveToSuperview(NSView* self, SEL _cmd) {
void NativeWindowMac::SetWindowButtonVisibility(bool visible) {
window_button_visibility_ = visible;
InternalSetWindowButtonVisibility(visible);
NotifyLayoutWindowControlsOverlay();
}

bool NativeWindowMac::GetWindowButtonVisibility() const {
Expand All @@ -1505,6 +1506,7 @@ void ViewDidMoveToSuperview(NSView* self, SEL _cmd) {
if (buttons_view_) {
[buttons_view_ setMargin:traffic_light_position_];
[buttons_view_ viewDidMoveToWindow];
NotifyLayoutWindowControlsOverlay();
}
}

Expand Down Expand Up @@ -1859,6 +1861,27 @@ void ViewDidMoveToSuperview(NSView* self, SEL _cmd) {
[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) {
Expand Down
2 changes: 2 additions & 0 deletions shell/browser/native_window_observer.h
Expand Up @@ -102,6 +102,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
Expand Down
1 change: 1 addition & 0 deletions shell/browser/ui/cocoa/window_buttons_view.h
Expand Up @@ -26,6 +26,7 @@
- (void)setMargin:(const absl::optional<gfx::Point>&)margin;
- (void)setShowOnHover:(BOOL)yes;
- (void)setNeedsDisplayForButtons;
- (gfx::Point)getMargin;
@end

#endif // SHELL_BROWSER_UI_COCOA_WINDOW_BUTTONS_VIEW_H_
4 changes: 4 additions & 0 deletions shell/browser/ui/cocoa/window_buttons_view.mm
Expand Up @@ -116,4 +116,8 @@ - (void)mouseExited:(NSEvent*)event {
[self setNeedsDisplayForButtons];
}

- (gfx::Point)getMargin {
return margin_;
}

@end
2 changes: 2 additions & 0 deletions shell/common/options_switches.cc
Expand Up @@ -191,6 +191,8 @@ const char kEnableWebSQL[] = "enableWebSQL";

const char kEnablePreferredSizeMode[] = "enablePreferredSizeMode";

const char ktitleBarOverlay[] = "titleBarOverlay";

} // namespace options

namespace switches {
Expand Down
1 change: 1 addition & 0 deletions shell/common/options_switches.h
Expand Up @@ -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[];
Expand Down
35 changes: 35 additions & 0 deletions spec-main/api-browser-window-spec.ts
Expand Up @@ -1877,7 +1877,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,
Expand All @@ -1898,6 +1927,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', () => {
Expand Down

0 comments on commit 9abfbee

Please sign in to comment.