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 #29253

Merged
merged 12 commits into from Jul 1, 2021
11 changes: 10 additions & 1 deletion docs/api/browser-window.md
Expand Up @@ -219,9 +219,16 @@ It creates a new `BrowserWindow` with native properties as set by the `options`.
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.
the top left. If `enableBlinkFeatures: 'WebAppWindowControlsOverlay'` is set,
using this value will also enable the Window Controls Overlay
[JavaScript APIs][overlay-javascript-apis] and
zcbenz marked this conversation as resolved.
Show resolved Hide resolved
[CSS Environment Variables][overlay-css-env-vars].
* `hiddenInset` - Results in a hidden title bar with an alternative look
where the traffic light buttons are slightly more inset from the window edge.
If `enableBlinkFeatures: 'WebAppWindowControlsOverlay'` is set, using this
value will also enable the Window Controls Overlay
[JavaScript APIs][overlay-javascript-apis] and
[CSS Environment Variables][overlay-css-env-vars].
* `customButtonsOnHover` - 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
Expand Down Expand Up @@ -1815,3 +1822,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
9 changes: 9 additions & 0 deletions docs/api/frameless-window.md
Expand Up @@ -61,6 +61,13 @@ 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
jkleinsc marked this conversation as resolved.
Show resolved Hide resolved
that the traffic lights are visibile, you can access the Window Controls Overlay [JavaScript APIs][overlay-javascript-apis] and
[CSS Environment Variables][overlay-css-env-vars] by setting the blink feature `'WebAppWindowControlsOverlay`
(eg `enableBlinkFeatures: 'WebAppWindowControlsOverlay'`).

## Transparent window

By setting the `transparent` option to `true`, you can also make the frameless
Expand Down Expand Up @@ -186,3 +193,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
5 changes: 5 additions & 0 deletions shell/browser/api/electron_api_browser_window.cc
Expand Up @@ -312,6 +312,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 @@ -71,6 +71,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 @@ -1677,6 +1677,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
17 changes: 17 additions & 0 deletions shell/browser/native_window.cc
Expand Up @@ -394,6 +394,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) {
zcbenz marked this conversation as resolved.
Show resolved Hide resolved
overlay_rect_ = overlay_rect;
}

void NativeWindow::NotifyWindowRequestPreferredWith(int* width) {
for (NativeWindowObserver& observer : observers_)
observer.RequestPreferredWidth(width);
Expand Down Expand Up @@ -493,6 +501,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 +600,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
6 changes: 6 additions & 0 deletions shell/browser/native_window.h
Expand Up @@ -254,6 +254,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 @@ -298,6 +301,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 @@ -390,6 +394,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 @@ -146,6 +146,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
22 changes: 22 additions & 0 deletions shell/browser/native_window_mac.mm
Expand Up @@ -1453,6 +1453,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 @@ -1470,6 +1471,7 @@ void ViewDidMoveToSuperview(NSView* self, SEL _cmd) {
if (buttons_view_) {
[buttons_view_ setMargin:traffic_light_position_];
[buttons_view_ viewDidMoveToWindow];
NotifyLayoutWindowControlsOverlay();
}
}

Expand Down Expand Up @@ -1809,6 +1811,26 @@ void ViewDidMoveToSuperview(NSView* self, SEL _cmd) {
[window_ setAcceptsMouseMovedEvents:forward];
}

gfx::Rect NativeWindowMac::GetWindowControlsOverlayRect() {
gfx::Rect bounding_rect;
if (!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 @@ -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
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
35 changes: 35 additions & 0 deletions spec-main/api-browser-window-spec.ts
Expand Up @@ -1863,7 +1863,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: {
enableBlinkFeatures: 'WebAppWindowControlsOverlay',
nodeIntegration: true,
contextIsolation: false
}
});
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 @@ -1884,6 +1913,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
84 changes: 84 additions & 0 deletions spec-main/fixtures/pages/overlay.html
@@ -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>