Let Chromium manage document.visibilityState
and document.hidden
Chromium already includes the necessary plumbing to manage the visibility properties and `visibilitychange` event so this gets rid of most of our custom logic for `BrowserWindow` and `BrowserView`. Note that `webview` remains unchanged and is still affected by the issues listed below. User facing changes: - The `document` visibility properties and `visibilitychange` event are now also updated/fired in response to occlusion changes on macOS. In other words, `document.visibilityState` will now be `hidden` on macOS if the window is occluded by another window. - Previously, `visibilitychange` was also fired by *both* Electron and Chromium in some cases (e.g. when hiding the window). Now it is only fired by Chromium so you no longer get duplicate events. - The visiblity state of `BrowserWindow`s created with `{ show: false }` is now initially `visible` until the window is shown and hidden. - The visibility state of `BrowserWindow`s with `backgroundThrottling` disabled is now permanently `visible`. This should also fix #6860 (but not for `webview`).
This commit is contained in:
parent
d40a7569cc
commit
7d2226e05e
11 changed files with 175 additions and 90 deletions
|
@ -186,25 +186,22 @@ void WebContentsPreferences::AppendExtraCommandLineSwitches(
|
||||||
command_line->AppendSwitchASCII(::switches::kDisableBlinkFeatures,
|
command_line->AppendSwitchASCII(::switches::kDisableBlinkFeatures,
|
||||||
disable_blink_features);
|
disable_blink_features);
|
||||||
|
|
||||||
// The initial visibility state.
|
if (guest_instance_id) {
|
||||||
NativeWindow* window = NativeWindow::FromWebContents(web_contents);
|
// Webview `document.visibilityState` tracks window visibility so we need
|
||||||
|
// to let it know if the window happens to be hidden right now.
|
||||||
// Use embedder window for webviews
|
|
||||||
if (guest_instance_id && !window) {
|
|
||||||
auto manager = WebViewManager::GetWebViewManager(web_contents);
|
auto manager = WebViewManager::GetWebViewManager(web_contents);
|
||||||
if (manager) {
|
if (manager) {
|
||||||
auto embedder = manager->GetEmbedder(guest_instance_id);
|
auto embedder = manager->GetEmbedder(guest_instance_id);
|
||||||
if (embedder)
|
if (embedder) {
|
||||||
window = NativeWindow::FromWebContents(embedder);
|
auto window = NativeWindow::FromWebContents(embedder);
|
||||||
}
|
const bool visible = window->IsVisible() && !window->IsMinimized();
|
||||||
}
|
if (!visible) {
|
||||||
|
|
||||||
if (window) {
|
|
||||||
bool visible = window->IsVisible() && !window->IsMinimized();
|
|
||||||
if (!visible) // Default state is visible.
|
|
||||||
command_line->AppendSwitch(switches::kHiddenPage);
|
command_line->AppendSwitch(switches::kHiddenPage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool WebContentsPreferences::IsPreferenceEnabled(
|
bool WebContentsPreferences::IsPreferenceEnabled(
|
||||||
const std::string& attribute_name,
|
const std::string& attribute_name,
|
||||||
|
|
|
@ -97,6 +97,25 @@ child.once('ready-to-show', () => {
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Page visibility
|
||||||
|
|
||||||
|
The [Page Visibility API][page-visibility-api] works as follows:
|
||||||
|
|
||||||
|
* On all platforms, the visibility state tracks whether the window is
|
||||||
|
hidden/minimized or not.
|
||||||
|
* Additionally, on macOS, the visibility state also tracks the window
|
||||||
|
occlusion state. If the window is occluded (i.e. fully covered) by another
|
||||||
|
window, the visibility state will be `hidden`. On other platforms, the
|
||||||
|
visibility state will be `hidden` only when the window is minimized or
|
||||||
|
explicitly hidden with `win.hide()`.
|
||||||
|
* If a `BrowserWindow` is created with `show: false`, the initial visibility
|
||||||
|
state will be `visible` despite the window actually being hidden.
|
||||||
|
* If `backgroundThrottling` is disabled, the visibility state will remain
|
||||||
|
`visible` even if the window is minimized, occluded, or hidden.
|
||||||
|
|
||||||
|
It is recommended that you pause expensive operations when the visibility
|
||||||
|
state is `hidden` in order to minimize power consumption.
|
||||||
|
|
||||||
### Platform notices
|
### Platform notices
|
||||||
|
|
||||||
* On macOS modal windows will be displayed as sheets attached to the parent window.
|
* On macOS modal windows will be displayed as sheets attached to the parent window.
|
||||||
|
@ -294,7 +313,8 @@ It creates a new `BrowserWindow` with native properties as set by the `options`.
|
||||||
* `minimumFontSize` Integer (optional) - Defaults to `0`.
|
* `minimumFontSize` Integer (optional) - Defaults to `0`.
|
||||||
* `defaultEncoding` String (optional) - Defaults to `ISO-8859-1`.
|
* `defaultEncoding` String (optional) - Defaults to `ISO-8859-1`.
|
||||||
* `backgroundThrottling` Boolean (optional) - Whether to throttle animations and timers
|
* `backgroundThrottling` Boolean (optional) - Whether to throttle animations and timers
|
||||||
when the page becomes background. Defaults to `true`.
|
when the page becomes background. This also affects the
|
||||||
|
[Page Visibility API][#page-visibility]. Defaults to `true`.
|
||||||
* `offscreen` Boolean (optional) - Whether to enable offscreen rendering for the browser
|
* `offscreen` Boolean (optional) - Whether to enable offscreen rendering for the browser
|
||||||
window. Defaults to `false`. See the
|
window. Defaults to `false`. See the
|
||||||
[offscreen rendering tutorial](../tutorial/offscreen-rendering.md) for
|
[offscreen rendering tutorial](../tutorial/offscreen-rendering.md) for
|
||||||
|
@ -1326,6 +1346,7 @@ removed in future Electron releases.
|
||||||
removed in future Electron releases.
|
removed in future Electron releases.
|
||||||
|
|
||||||
[blink-feature-string]: https://cs.chromium.org/chromium/src/third_party/WebKit/Source/platform/RuntimeEnabledFeatures.json5?l=62
|
[blink-feature-string]: https://cs.chromium.org/chromium/src/third_party/WebKit/Source/platform/RuntimeEnabledFeatures.json5?l=62
|
||||||
|
[page-visibility-api]: https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API
|
||||||
[quick-look]: https://en.wikipedia.org/wiki/Quick_Look
|
[quick-look]: https://en.wikipedia.org/wiki/Quick_Look
|
||||||
[vibrancy-docs]: https://developer.apple.com/reference/appkit/nsvisualeffectview?language=objc
|
[vibrancy-docs]: https://developer.apple.com/reference/appkit/nsvisualeffectview?language=objc
|
||||||
[window-levels]: https://developer.apple.com/reference/appkit/nswindow/1664726-window_levels
|
[window-levels]: https://developer.apple.com/reference/appkit/nswindow/1664726-window_levels
|
||||||
|
|
|
@ -60,12 +60,11 @@ container when used with traditional and flexbox layouts (since v0.36.11). Pleas
|
||||||
do not overwrite the default `display:flex;` CSS property, unless specifying
|
do not overwrite the default `display:flex;` CSS property, unless specifying
|
||||||
`display:inline-flex;` for inline layout.
|
`display:inline-flex;` for inline layout.
|
||||||
|
|
||||||
`webview` has issues being hidden using the `hidden` attribute or using `display: none;`.
|
`webview` has issues being hidden using the `hidden` attribute or using
|
||||||
It can cause unusual rendering behaviour within its child `browserplugin` object
|
`display: none;`. It can cause unusual rendering behaviour within its child
|
||||||
and the web page is reloaded, when the `webview` is un-hidden, as opposed to just
|
`browserplugin` object and the web page is reloaded when the `webview` is
|
||||||
becoming visible again. The recommended approach is to hide the `webview` using
|
un-hidden. The recommended approach is to hide the `webview` using
|
||||||
CSS by zeroing the `width` & `height` and allowing the element to shrink to the 0px
|
`visibility: hidden`.
|
||||||
dimensions via `flex`.
|
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<style>
|
<style>
|
||||||
|
@ -75,9 +74,7 @@ dimensions via `flex`.
|
||||||
height:480px;
|
height:480px;
|
||||||
}
|
}
|
||||||
webview.hide {
|
webview.hide {
|
||||||
flex: 0 1;
|
visibility: hidden;
|
||||||
width: 0px;
|
|
||||||
height: 0px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
```
|
```
|
||||||
|
|
|
@ -109,7 +109,6 @@ BrowserWindow.prototype._init = function () {
|
||||||
if (isVisible !== newState) {
|
if (isVisible !== newState) {
|
||||||
isVisible = newState
|
isVisible = newState
|
||||||
const visibilityState = isVisible ? 'visible' : 'hidden'
|
const visibilityState = isVisible ? 'visible' : 'hidden'
|
||||||
this.webContents.send('ELECTRON_RENDERER_WINDOW_VISIBILITY_CHANGE', visibilityState)
|
|
||||||
this.webContents.emit('-window-visibility-change', visibilityState)
|
this.webContents.emit('-window-visibility-change', visibilityState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -281,7 +281,7 @@ const watchEmbedder = function (embedder) {
|
||||||
for (const guestInstanceId of Object.keys(guestInstances)) {
|
for (const guestInstanceId of Object.keys(guestInstances)) {
|
||||||
const guestInstance = guestInstances[guestInstanceId]
|
const guestInstance = guestInstances[guestInstanceId]
|
||||||
if (guestInstance.embedder === embedder) {
|
if (guestInstance.embedder === embedder) {
|
||||||
guestInstance.guest.send('ELECTRON_RENDERER_WINDOW_VISIBILITY_CHANGE', visibilityState)
|
guestInstance.guest.send('ELECTRON_GUEST_INSTANCE_VISIBILITY_CHANGE', visibilityState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -175,11 +175,18 @@ module.exports = (ipcRenderer, guestInstanceId, openerId, hiddenPage, usesNative
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// The initial visibilityState.
|
if (guestInstanceId != null) {
|
||||||
|
// Webview `document.visibilityState` tracks window visibility (and ignores
|
||||||
|
// the actual <webview> element visibility) for backwards compatibility.
|
||||||
|
// See discussion in #9178.
|
||||||
|
//
|
||||||
|
// Note that this results in duplicate visibilitychange events (since
|
||||||
|
// Chromium also fires them) and potentially incorrect visibility change.
|
||||||
|
// We should reconsider this decision for Electron 2.0.
|
||||||
let cachedVisibilityState = hiddenPage ? 'hidden' : 'visible'
|
let cachedVisibilityState = hiddenPage ? 'hidden' : 'visible'
|
||||||
|
|
||||||
// Subscribe to visibilityState changes.
|
// Subscribe to visibilityState changes.
|
||||||
ipcRenderer.on('ELECTRON_RENDERER_WINDOW_VISIBILITY_CHANGE', function (event, visibilityState) {
|
ipcRenderer.on('ELECTRON_GUEST_INSTANCE_VISIBILITY_CHANGE', function (event, visibilityState) {
|
||||||
if (cachedVisibilityState !== visibilityState) {
|
if (cachedVisibilityState !== visibilityState) {
|
||||||
cachedVisibilityState = visibilityState
|
cachedVisibilityState = visibilityState
|
||||||
document.dispatchEvent(new Event('visibilitychange'))
|
document.dispatchEvent(new Event('visibilitychange'))
|
||||||
|
@ -199,3 +206,4 @@ module.exports = (ipcRenderer, guestInstanceId, openerId, hiddenPage, usesNative
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1412,6 +1412,111 @@ describe('BrowserWindow module', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('document.visibilityState', function () {
|
||||||
|
afterEach(function () {
|
||||||
|
ipcMain.removeAllListeners('pong')
|
||||||
|
})
|
||||||
|
|
||||||
|
function onNextVisibleEvent (callback) {
|
||||||
|
ipcMain.on('pong', function listener (event, visibilityState, hidden) {
|
||||||
|
if (visibilityState === 'visible' && hidden === false) {
|
||||||
|
ipcMain.removeListener('pong', listener)
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onNextHiddenEvent (callback) {
|
||||||
|
ipcMain.on('pong', function listener (event, visibilityState, hidden) {
|
||||||
|
if (visibilityState === 'hidden' && hidden === true) {
|
||||||
|
ipcMain.removeListener('pong', listener)
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
it('visibilityState is initially visible despite window being hidden', function (done) {
|
||||||
|
w.destroy()
|
||||||
|
w = new BrowserWindow({ show: false, width: 100, height: 100 })
|
||||||
|
|
||||||
|
let readyToShow = false
|
||||||
|
w.on('ready-to-show', function () {
|
||||||
|
readyToShow = true
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.once('pong', function (event, visibilityState, hidden) {
|
||||||
|
assert.ok(!readyToShow)
|
||||||
|
assert.equal(visibilityState, 'visible')
|
||||||
|
assert.equal(hidden, false)
|
||||||
|
|
||||||
|
ipcMain.once('pong', function (event, visibilityState, hidden) {
|
||||||
|
assert.ok(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout(done, 1000)
|
||||||
|
w.show()
|
||||||
|
})
|
||||||
|
|
||||||
|
w.loadURL('file://' + path.join(fixtures, 'pages', 'visibilitychange.html'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('visibilityState changes when window is shown and hidden', function (done) {
|
||||||
|
w.destroy()
|
||||||
|
w = new BrowserWindow({
|
||||||
|
width: 100,
|
||||||
|
height: 100
|
||||||
|
})
|
||||||
|
|
||||||
|
onNextVisibleEvent(() => {
|
||||||
|
onNextHiddenEvent(() => {
|
||||||
|
onNextVisibleEvent(() => {
|
||||||
|
onNextHiddenEvent(done)
|
||||||
|
w.minimize()
|
||||||
|
})
|
||||||
|
w.show()
|
||||||
|
w.focus()
|
||||||
|
})
|
||||||
|
w.hide()
|
||||||
|
})
|
||||||
|
|
||||||
|
w.loadURL('file://' + path.join(fixtures, 'pages', 'visibilitychange.html'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('visibilityState remains visible if backgroundThrottling is disabled', function (done) {
|
||||||
|
w.destroy()
|
||||||
|
w = new BrowserWindow({
|
||||||
|
show: false,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
webPreferences: {
|
||||||
|
backgroundThrottling: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onNextVisibleEvent(() => {
|
||||||
|
onNextVisibleEvent(() => {
|
||||||
|
done(new Error('Unexpected visibility change event to visible'))
|
||||||
|
})
|
||||||
|
onNextHiddenEvent(() => {
|
||||||
|
done(new Error('Unexpected visibility change event to hidden'))
|
||||||
|
})
|
||||||
|
|
||||||
|
w.once('show', () => {
|
||||||
|
w.once('hide', () => {
|
||||||
|
w.once('show', () => {
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
w.show()
|
||||||
|
})
|
||||||
|
w.hide()
|
||||||
|
})
|
||||||
|
w.show()
|
||||||
|
})
|
||||||
|
|
||||||
|
w.loadURL('file://' + path.join(fixtures, 'pages', 'visibilitychange.html'))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('new-window event', function () {
|
describe('new-window event', function () {
|
||||||
if (isCI && process.platform === 'darwin') {
|
if (isCI && process.platform === 'darwin') {
|
||||||
return
|
return
|
||||||
|
@ -2318,9 +2423,7 @@ describe('BrowserWindow module', function () {
|
||||||
typeofArrayPush: 'number',
|
typeofArrayPush: 'number',
|
||||||
typeofFunctionApply: 'boolean',
|
typeofFunctionApply: 'boolean',
|
||||||
typeofPreloadExecuteJavaScriptProperty: 'number',
|
typeofPreloadExecuteJavaScriptProperty: 'number',
|
||||||
typeofOpenedWindow: 'object',
|
typeofOpenedWindow: 'object'
|
||||||
documentHidden: true,
|
|
||||||
documentVisibilityState: 'hidden'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,35 +48,6 @@ describe('chromium feature', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('document.hidden', function () {
|
|
||||||
var url = 'file://' + fixtures + '/pages/document-hidden.html'
|
|
||||||
|
|
||||||
it('is set correctly when window is not shown', function (done) {
|
|
||||||
w = new BrowserWindow({
|
|
||||||
show: false
|
|
||||||
})
|
|
||||||
w.webContents.once('ipc-message', function (event, args) {
|
|
||||||
assert.deepEqual(args, ['hidden', true])
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
w.loadURL(url)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('is set correctly when window is inactive', function (done) {
|
|
||||||
if (isCI && process.platform === 'win32') return done()
|
|
||||||
|
|
||||||
w = new BrowserWindow({
|
|
||||||
show: false
|
|
||||||
})
|
|
||||||
w.webContents.once('ipc-message', function (event, args) {
|
|
||||||
assert.deepEqual(args, ['hidden', false])
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
w.showInactive()
|
|
||||||
w.loadURL(url)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
xdescribe('navigator.webkitGetUserMedia', function () {
|
xdescribe('navigator.webkitGetUserMedia', function () {
|
||||||
it('calls its callbacks', function (done) {
|
it('calls its callbacks', function (done) {
|
||||||
navigator.webkitGetUserMedia({
|
navigator.webkitGetUserMedia({
|
||||||
|
|
4
spec/fixtures/api/isolated.html
vendored
4
spec/fixtures/api/isolated.html
vendored
|
@ -19,9 +19,7 @@
|
||||||
typeofArrayPush: typeof Array.prototype.push,
|
typeofArrayPush: typeof Array.prototype.push,
|
||||||
typeofFunctionApply: typeof Function.prototype.apply,
|
typeofFunctionApply: typeof Function.prototype.apply,
|
||||||
typeofPreloadExecuteJavaScriptProperty: typeof window.preloadExecuteJavaScriptProperty,
|
typeofPreloadExecuteJavaScriptProperty: typeof window.preloadExecuteJavaScriptProperty,
|
||||||
typeofOpenedWindow: typeof opened,
|
typeofOpenedWindow: typeof opened
|
||||||
documentHidden: document.hidden,
|
|
||||||
documentVisibilityState: document.visibilityState
|
|
||||||
}, '*')
|
}, '*')
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|
7
spec/fixtures/pages/document-hidden.html
vendored
7
spec/fixtures/pages/document-hidden.html
vendored
|
@ -1,7 +0,0 @@
|
||||||
<html>
|
|
||||||
<body>
|
|
||||||
<script type="text/javascript" charset="utf-8">
|
|
||||||
require('electron').ipcRenderer.send('hidden', document.hidden);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -485,9 +485,7 @@ describe('<webview> tag', function () {
|
||||||
typeofArrayPush: 'number',
|
typeofArrayPush: 'number',
|
||||||
typeofFunctionApply: 'boolean',
|
typeofFunctionApply: 'boolean',
|
||||||
typeofPreloadExecuteJavaScriptProperty: 'number',
|
typeofPreloadExecuteJavaScriptProperty: 'number',
|
||||||
typeofOpenedWindow: 'object',
|
typeofOpenedWindow: 'object'
|
||||||
documentHidden: isCI,
|
|
||||||
documentVisibilityState: isCI ? 'hidden' : 'visible'
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
done()
|
done()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue