Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enable window controls overlay on macOS #29986

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 @@ -1672,6 +1672,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 @@ -1856,6 +1858,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