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 <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: Samuel Attard <sam@electronjs.org>
This commit is contained in:
parent
0fe2836151
commit
1f8a46c9c6
16 changed files with 223 additions and 0 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<views::Widget> widget_;
|
||||
|
||||
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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_
|
||||
|
|
|
@ -116,4 +116,8 @@ const NSWindowButton kButtonTypes[] = {
|
|||
[self setNeedsDisplayForButtons];
|
||||
}
|
||||
|
||||
- (gfx::Point)getMargin {
|
||||
return margin_;
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -194,6 +194,8 @@ const char kEnableWebSQL[] = "enableWebSQL";
|
|||
|
||||
const char kEnablePreferredSizeMode[] = "enablePreferredSizeMode";
|
||||
|
||||
const char ktitleBarOverlay[] = "titleBarOverlay";
|
||||
|
||||
} // namespace options
|
||||
|
||||
namespace switches {
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
84
spec-main/fixtures/pages/overlay.html
Normal file
84
spec-main/fixtures/pages/overlay.html
Normal file
|
@ -0,0 +1,84 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<style>
|
||||
:root {
|
||||
--fallback-title-bar-height: 40px;
|
||||
}
|
||||
|
||||
.draggable {
|
||||
app-region: drag;
|
||||
/* Pre-fix app-region during standardization process */
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.nonDraggable {
|
||||
app-region: no-drag;
|
||||
/* Pre-fix app-region during standardization process */
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
|
||||
#titleBarContainer {
|
||||
position: absolute;
|
||||
top: env(titlebar-area-y, 0);
|
||||
height: env(titlebar-area-height, var(--fallback-title-bar-height));
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#titleBar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
display: flex;
|
||||
user-select: none;
|
||||
height: 100%;
|
||||
left: env(titlebar-area-x, 0);
|
||||
width: env(titlebar-area-width, 100%);
|
||||
}
|
||||
|
||||
#mainContent {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: env(titlebar-area-height, var(--fallback-title-bar-height));
|
||||
overflow-y: scroll;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
const {ipcRenderer} = require('electron');
|
||||
navigator.windowControlsOverlay.ongeometrychange = function() {
|
||||
const {x, y, width, height} = navigator.windowControlsOverlay.getBoundingClientRect();
|
||||
ipcRenderer.send('geometrychange', {x, y, width, height});
|
||||
};
|
||||
</script>
|
||||
<div id="titleBarContainer">
|
||||
<div id="titleBar" class=" draggable">
|
||||
<span class="draggable">Title goes here</span>
|
||||
<input class="nonDraggable" type="text" placeholder="Search"></input>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mainContent"><!-- The rest of the webpage --></div>
|
||||
<script>
|
||||
function getCssOverlayProperties() {
|
||||
const cssOverlayProps = {};
|
||||
const titleBarContainer = document.getElementById('titleBarContainer');
|
||||
const titleBar = document.getElementById('titleBar');
|
||||
cssOverlayProps.y = titleBarContainer.computedStyleMap().get('top').value;
|
||||
cssOverlayProps.height = titleBarContainer.computedStyleMap().get('height').value;
|
||||
cssOverlayProps.x = titleBar.computedStyleMap().get('left').value;
|
||||
cssOverlayProps.width = titleBar.computedStyleMap().get('width').value;
|
||||
return cssOverlayProps;
|
||||
}
|
||||
|
||||
function getJSOverlayProperties() {
|
||||
const {x, y, width, height} = navigator.windowControlsOverlay.getBoundingClientRect();
|
||||
return {x, y, width, height};
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in a new issue