From bc56c6987ff549a8cdc5ff89abd0afb28fc5934f Mon Sep 17 00:00:00 2001 From: Calvin Date: Sun, 10 Aug 2025 13:55:40 -0600 Subject: [PATCH] fix: offscreen mode under `window.open` creation (#48026) fix: offscreen mode under `window.open` creation (#47868) fix: offscreen mode under new window creation Co-authored-by: Shelley Vohr --- patches/chromium/can_create_window.patch | 32 ++++++++-- .../chromium/chore_partial_revert_of.patch | 2 +- ...screationoverridden_with_full_params.patch | 6 +- ...board_hides_on_input_blur_in_webview.patch | 4 +- ...r_changes_to_the_webcontentsobserver.patch | 4 +- ...efactor_unfilter_unresponsive_events.patch | 4 +- patches/chromium/web_contents.patch | 2 +- patches/chromium/webview_fullscreen.patch | 2 +- .../browser/api/electron_api_web_contents.cc | 33 +++++++++- shell/browser/api/electron_api_web_contents.h | 2 + spec/guest-window-manager-spec.ts | 61 +++++++++++++++++++ 11 files changed, 133 insertions(+), 19 deletions(-) diff --git a/patches/chromium/can_create_window.patch b/patches/chromium/can_create_window.patch index ab16a6274364..67d6623cca43 100644 --- a/patches/chromium/can_create_window.patch +++ b/patches/chromium/can_create_window.patch @@ -21,10 +21,21 @@ index 378e3eb2f8b9d4daaf39ef213dec88d86cf90a5c..0ace2e0c7073ee97ebb274db4b184a07 &no_javascript_access); diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc -index 5c319d0b7c81cb7335170a23e2863750ae9c6aa0..336bc9c329aed83293e6a802c8504a73cdf15b20 100644 +index 5c319d0b7c81cb7335170a23e2863750ae9c6aa0..ec2989ca2c736140c9be6b78591798ac733e752e 100644 --- a/content/browser/web_contents/web_contents_impl.cc +++ b/content/browser/web_contents/web_contents_impl.cc -@@ -5336,6 +5336,12 @@ FrameTree* WebContentsImpl::CreateNewWindow( +@@ -5281,6 +5281,10 @@ FrameTree* WebContentsImpl::CreateNewWindow( + create_params.initially_hidden = renderer_started_hidden; + create_params.initial_popup_url = params.target_url; + ++ // Potentially allow the delegate to override the create_params. ++ if (delegate_) ++ delegate_->MaybeOverrideCreateParamsForNewWindow(&create_params); ++ + // Even though all codepaths leading here are in response to a renderer + // trying to open a new window, if the new window ends up in a different + // browsing instance, then the RenderViewHost, RenderWidgetHost, +@@ -5336,6 +5340,12 @@ FrameTree* WebContentsImpl::CreateNewWindow( // Sets the newly created WebContents WindowOpenDisposition. new_contents_impl->original_window_open_disposition_ = params.disposition; @@ -37,7 +48,7 @@ index 5c319d0b7c81cb7335170a23e2863750ae9c6aa0..336bc9c329aed83293e6a802c8504a73 // If the new frame has a name, make sure any SiteInstances that can find // this named frame have proxies for it. Must be called after // SetSessionStorageNamespace, since this calls CreateRenderView, which uses -@@ -5377,12 +5383,6 @@ FrameTree* WebContentsImpl::CreateNewWindow( +@@ -5377,12 +5387,6 @@ FrameTree* WebContentsImpl::CreateNewWindow( AddWebContentsDestructionObserver(new_contents_impl); } @@ -122,7 +133,7 @@ index ca92e2ddf78d8f386b5ab23a09876d3b44e21334..33be50ce93dd998df5244f9ade391943 WebContents* source, const OpenURLParams& params, diff --git a/content/public/browser/web_contents_delegate.h b/content/public/browser/web_contents_delegate.h -index a4b8c5f950549e018c0d09522ff8890a1a774966..e364ae0f9bfa6321f3a3be598b36eb07fb5bca7a 100644 +index a4b8c5f950549e018c0d09522ff8890a1a774966..6c4c6265c26f5304b8f77d7fc5a4fad5dffc831d 100644 --- a/content/public/browser/web_contents_delegate.h +++ b/content/public/browser/web_contents_delegate.h @@ -18,6 +18,7 @@ @@ -133,7 +144,15 @@ index a4b8c5f950549e018c0d09522ff8890a1a774966..e364ae0f9bfa6321f3a3be598b36eb07 #include "content/public/browser/eye_dropper.h" #include "content/public/browser/fullscreen_types.h" #include "content/public/browser/invalidate_type.h" -@@ -384,6 +385,13 @@ class CONTENT_EXPORT WebContentsDelegate { +@@ -29,6 +30,7 @@ + #include "content/public/browser/select_audio_output_request.h" + #include "content/public/browser/serial_chooser.h" + #include "content/public/browser/storage_partition_config.h" ++#include "content/public/browser/web_contents.h" + #include "content/public/common/window_container_type.mojom-forward.h" + #include "third_party/blink/public/common/input/web_mouse_event.h" + #include "third_party/blink/public/common/mediastream/media_stream_request.h" +@@ -384,6 +386,16 @@ class CONTENT_EXPORT WebContentsDelegate { const StoragePartitionConfig& partition_config, SessionStorageNamespace* session_storage_namespace); @@ -143,6 +162,9 @@ index a4b8c5f950549e018c0d09522ff8890a1a774966..e364ae0f9bfa6321f3a3be598b36eb07 + int opener_render_frame_id, + const mojom::CreateNewWindowParams& params, + WebContents* new_contents); ++ ++ virtual void MaybeOverrideCreateParamsForNewWindow( ++ content::WebContents::CreateParams* create_params) {} + // Notifies the delegate about the creation of a new WebContents. This // typically happens when popups are created. diff --git a/patches/chromium/chore_partial_revert_of.patch b/patches/chromium/chore_partial_revert_of.patch index 53c34c98428d..267b939513d7 100644 --- a/patches/chromium/chore_partial_revert_of.patch +++ b/patches/chromium/chore_partial_revert_of.patch @@ -14,7 +14,7 @@ track down the source of this problem & figure out if we can fix it by changing something in Electron. diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc -index cc7042ef657b5b248869c9fe560fbece07e1d7d6..b286ce081a19a3ca02cfffaa8ac32e407bbff02a 100644 +index 65d5726b06a24d6ffca55413fd767cb46c87a137..3f68fa06dd2864e047d12f0c59a3c649494f0bf9 100644 --- a/content/browser/web_contents/web_contents_impl.cc +++ b/content/browser/web_contents/web_contents_impl.cc @@ -5252,7 +5252,7 @@ FrameTree* WebContentsImpl::CreateNewWindow( diff --git a/patches/chromium/chore_provide_iswebcontentscreationoverridden_with_full_params.patch b/patches/chromium/chore_provide_iswebcontentscreationoverridden_with_full_params.patch index 1b622788dde7..b76b08b787c0 100644 --- a/patches/chromium/chore_provide_iswebcontentscreationoverridden_with_full_params.patch +++ b/patches/chromium/chore_provide_iswebcontentscreationoverridden_with_full_params.patch @@ -222,7 +222,7 @@ index b969f1d97b7e3396119b579cfbe61e19ff7d2dd4..b8d6169652da28266a514938b45b39c5 content::WebContents* AddNewContents( content::WebContents* source, diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc -index c093f8b4ba872969a88923f2f96597bb8ffc8817..c1c1b4974339e1f08845815ef2d87b213e8597d9 100644 +index e1f9c3070f0de3ee2d54791bd18fcdf35de6b7b5..2004582d7041b94a3232c3c160b2bfd9351f6658 100644 --- a/content/browser/web_contents/web_contents_impl.cc +++ b/content/browser/web_contents/web_contents_impl.cc @@ -5215,8 +5215,7 @@ FrameTree* WebContentsImpl::CreateNewWindow( @@ -250,10 +250,10 @@ index 33be50ce93dd998df5244f9ade391943f06978ad..3bb9baf76d331351d23d59fc2b9eb82d } diff --git a/content/public/browser/web_contents_delegate.h b/content/public/browser/web_contents_delegate.h -index e364ae0f9bfa6321f3a3be598b36eb07fb5bca7a..c75fdf6bd7cb6b4d6bcfbb23da952adce4dd90ac 100644 +index 6c4c6265c26f5304b8f77d7fc5a4fad5dffc831d..d0842904102fee982bc8502478a0a9067bb77904 100644 --- a/content/public/browser/web_contents_delegate.h +++ b/content/public/browser/web_contents_delegate.h -@@ -363,8 +363,7 @@ class CONTENT_EXPORT WebContentsDelegate { +@@ -364,8 +364,7 @@ class CONTENT_EXPORT WebContentsDelegate { SiteInstance* source_site_instance, mojom::WindowContainerType window_container_type, const GURL& opener_url, diff --git a/patches/chromium/fix_on-screen-keyboard_hides_on_input_blur_in_webview.patch b/patches/chromium/fix_on-screen-keyboard_hides_on_input_blur_in_webview.patch index 6798da0aaf17..2112fbebe91d 100644 --- a/patches/chromium/fix_on-screen-keyboard_hides_on_input_blur_in_webview.patch +++ b/patches/chromium/fix_on-screen-keyboard_hides_on_input_blur_in_webview.patch @@ -87,10 +87,10 @@ index 75df43e3cd2721a92c90c18154d53d5c203e2465..ce42c75c8face36d21f53f44c0201ac4 // The view with active text input state, i.e., a focused element. // It will be nullptr if no such view exists. Note that the active view diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc -index 7aa1e2d3b0cbc8055cd60987ad3cbc96e09c25f4..cc7042ef657b5b248869c9fe560fbece07e1d7d6 100644 +index f49eefa91c94e85091879c0e521a5e27d1165d74..65d5726b06a24d6ffca55413fd767cb46c87a137 100644 --- a/content/browser/web_contents/web_contents_impl.cc +++ b/content/browser/web_contents/web_contents_impl.cc -@@ -10068,7 +10068,7 @@ void WebContentsImpl::OnFocusedElementChangedInFrame( +@@ -10072,7 +10072,7 @@ void WebContentsImpl::OnFocusedElementChangedInFrame( "WebContentsImpl::OnFocusedElementChangedInFrame", "render_frame_host", frame); RenderWidgetHostViewBase* root_view = diff --git a/patches/chromium/refactor_expose_cursor_changes_to_the_webcontentsobserver.patch b/patches/chromium/refactor_expose_cursor_changes_to_the_webcontentsobserver.patch index d20f084adcf1..91f757e9670c 100644 --- a/patches/chromium/refactor_expose_cursor_changes_to_the_webcontentsobserver.patch +++ b/patches/chromium/refactor_expose_cursor_changes_to_the_webcontentsobserver.patch @@ -44,10 +44,10 @@ index 0c5aa1c0e4c344f807cf0fcb7cc3cf532c1eaf23..ecbfaf2e7fd842a6f55002975e5bb4c4 void RenderWidgetHostImpl::ShowContextMenuAtPoint( diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc -index 336bc9c329aed83293e6a802c8504a73cdf15b20..c093f8b4ba872969a88923f2f96597bb8ffc8817 100644 +index ec2989ca2c736140c9be6b78591798ac733e752e..e1f9c3070f0de3ee2d54791bd18fcdf35de6b7b5 100644 --- a/content/browser/web_contents/web_contents_impl.cc +++ b/content/browser/web_contents/web_contents_impl.cc -@@ -6089,6 +6089,11 @@ TextInputManager* WebContentsImpl::GetTextInputManager() { +@@ -6093,6 +6093,11 @@ TextInputManager* WebContentsImpl::GetTextInputManager() { return text_input_manager_.get(); } diff --git a/patches/chromium/refactor_unfilter_unresponsive_events.patch b/patches/chromium/refactor_unfilter_unresponsive_events.patch index 89c5eb713c14..ae098c65c6e6 100644 --- a/patches/chromium/refactor_unfilter_unresponsive_events.patch +++ b/patches/chromium/refactor_unfilter_unresponsive_events.patch @@ -15,10 +15,10 @@ This CL removes these filters so the unresponsive event can still be accessed from our JS event. The filtering is moved into Electron's code. diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc -index b286ce081a19a3ca02cfffaa8ac32e407bbff02a..e17f75be3878b9ba7a7a33babc88fcc105ddc267 100644 +index 3f68fa06dd2864e047d12f0c59a3c649494f0bf9..4cd0c02dda57e1d718dbac953c937b492d06150c 100644 --- a/content/browser/web_contents/web_contents_impl.cc +++ b/content/browser/web_contents/web_contents_impl.cc -@@ -10205,25 +10205,13 @@ void WebContentsImpl::RendererUnresponsive( +@@ -10209,25 +10209,13 @@ void WebContentsImpl::RendererUnresponsive( base::RepeatingClosure hang_monitor_restarter) { OPTIONAL_TRACE_EVENT1("content", "WebContentsImpl::RendererUnresponsive", "render_widget_host", render_widget_host); diff --git a/patches/chromium/web_contents.patch b/patches/chromium/web_contents.patch index 84e659baf2e3..c79585c2173d 100644 --- a/patches/chromium/web_contents.patch +++ b/patches/chromium/web_contents.patch @@ -9,7 +9,7 @@ is needed for OSR. Originally landed in https://github.com/electron/libchromiumcontent/pull/226. diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc -index c1c1b4974339e1f08845815ef2d87b213e8597d9..4b309a0af2a747e46a9231eb35c00a825f4a0712 100644 +index 2004582d7041b94a3232c3c160b2bfd9351f6658..2a70e261f9ec17be3d666a06b7dad3abae093aaf 100644 --- a/content/browser/web_contents/web_contents_impl.cc +++ b/content/browser/web_contents/web_contents_impl.cc @@ -4133,6 +4133,13 @@ void WebContentsImpl::Init(const WebContents::CreateParams& params, diff --git a/patches/chromium/webview_fullscreen.patch b/patches/chromium/webview_fullscreen.patch index 86b8fec39bcc..e8f9de966f2f 100644 --- a/patches/chromium/webview_fullscreen.patch +++ b/patches/chromium/webview_fullscreen.patch @@ -37,7 +37,7 @@ index 0ace2e0c7073ee97ebb274db4b184a074f6ae544..ee3015214724be708ca15d480d105bbf if (had_fullscreen_token && !GetView()->HasFocus()) GetView()->Focus(); diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc -index 4b309a0af2a747e46a9231eb35c00a825f4a0712..7aa1e2d3b0cbc8055cd60987ad3cbc96e09c25f4 100644 +index 2a70e261f9ec17be3d666a06b7dad3abae093aaf..f49eefa91c94e85091879c0e521a5e27d1165d74 100644 --- a/content/browser/web_contents/web_contents_impl.cc +++ b/content/browser/web_contents/web_contents_impl.cc @@ -4422,21 +4422,25 @@ KeyboardEventProcessingResult WebContentsImpl::PreHandleKeyboardEvent( diff --git a/shell/browser/api/electron_api_web_contents.cc b/shell/browser/api/electron_api_web_contents.cc index 5e14a9f4d344..a0a77d17f522 100644 --- a/shell/browser/api/electron_api_web_contents.cc +++ b/shell/browser/api/electron_api_web_contents.cc @@ -1211,6 +1211,31 @@ content::WebContents* WebContents::CreateCustomWebContents( return nullptr; } +void WebContents::MaybeOverrideCreateParamsForNewWindow( + content::WebContents::CreateParams* create_params) { + v8::Isolate* isolate = JavascriptEnvironment::GetIsolate(); + v8::HandleScope handle_scope(isolate); + gin_helper::Dictionary dict; + gin::ConvertFromV8(isolate, pending_child_web_preferences_.Get(isolate), + &dict); + + v8::Local use_offscreen; + if (dict.Get(options::kOffscreen, &use_offscreen)) { + bool is_offscreen = + use_offscreen->IsObject() || + (use_offscreen->IsBoolean() && + dict.Get(options::kOffscreen, &is_offscreen) && is_offscreen); + + if (is_offscreen) { + auto* view = new OffScreenWebContentsView( + false, offscreen_use_shared_texture_, + base::BindRepeating(&WebContents::OnPaint, base::Unretained(this))); + create_params->view = view; + create_params->delegate_view = view; + } + } +} + content::WebContents* WebContents::AddNewContents( content::WebContents* source, std::unique_ptr new_contents, @@ -1224,9 +1249,13 @@ content::WebContents* WebContents::AddNewContents( v8::Isolate* isolate = JavascriptEnvironment::GetIsolate(); + Type type = Type::kBrowserWindow; + auto* web_preferences = WebContentsPreferences::From(new_contents.get()); + if (web_preferences && web_preferences->IsOffscreen()) + type = Type::kOffScreen; + v8::HandleScope handle_scope(isolate); - auto api_web_contents = - CreateAndTake(isolate, std::move(new_contents), Type::kBrowserWindow); + auto api_web_contents = CreateAndTake(isolate, std::move(new_contents), type); // We call RenderFrameCreated here as at this point the empty "about:blank" // render frame has already been created. If the window never navigates again diff --git a/shell/browser/api/electron_api_web_contents.h b/shell/browser/api/electron_api_web_contents.h index 09262988ea6a..390d0e37c5fd 100644 --- a/shell/browser/api/electron_api_web_contents.h +++ b/shell/browser/api/electron_api_web_contents.h @@ -510,6 +510,8 @@ class WebContents final : public ExclusiveAccessContext, int opener_render_frame_id, const content::mojom::CreateNewWindowParams& params, content::WebContents* new_contents) override; + void MaybeOverrideCreateParamsForNewWindow( + content::WebContents::CreateParams* create_params) override; content::WebContents* AddNewContents( content::WebContents* source, std::unique_ptr new_contents, diff --git a/spec/guest-window-manager-spec.ts b/spec/guest-window-manager-spec.ts index 88daa4cfae2a..c614f6e42c89 100644 --- a/spec/guest-window-manager-spec.ts +++ b/spec/guest-window-manager-spec.ts @@ -4,6 +4,7 @@ import { expect, assert } from 'chai'; import { once } from 'node:events'; import * as http from 'node:http'; +import * as nodePath from 'node:path'; import { HexColors, ScreenCapture, hasCapturableScreen } from './lib/screen-helpers'; import { ifit, listen } from './lib/spec-helpers'; @@ -203,6 +204,66 @@ describe('webContents.setWindowOpenHandler', () => { expect(await browserWindow.webContents.executeJavaScript('42')).to.equal(42); }); + it('can open an offscreen child window from an onscreen parent', async () => { + browserWindow.webContents.setWindowOpenHandler(() => ({ + action: 'allow', + overrideBrowserWindowOptions: { + webPreferences: { + offscreen: true + } + } + })); + + const didCreateWindow = once(browserWindow.webContents, 'did-create-window'); + const url = `file://${nodePath.join('fixtures', 'pages', 'content.html')}`; + browserWindow.webContents.executeJavaScript(`window.open('${JSON.stringify(url)}') && true`); + const [childWindow] = await didCreateWindow; + expect(childWindow.webContents.isOffscreen()).to.be.true('Child window should be offscreen'); + }); + + it('can open an onscreen child window from an offscreen parent', async () => { + const obw = new BrowserWindow({ + show: false, + webPreferences: { + offscreen: true + } + }); + + await obw.loadURL('about:blank'); + obw.webContents.setWindowOpenHandler(() => ({ action: 'allow' })); + + const didCreateWindow = once(obw.webContents, 'did-create-window'); + const url = `file://${nodePath.join('fixtures', 'pages', 'content.html')}`; + obw.webContents.executeJavaScript(`window.open('${JSON.stringify(url)}') && true`); + const [childWindow] = await didCreateWindow; + expect(childWindow.webContents.isOffscreen()).to.be.false('Child window should not be offscreen'); + }); + + it('can open an offscreen child window from an offscreen parent', async () => { + const obw = new BrowserWindow({ + show: false, + webPreferences: { + offscreen: true + } + }); + + await obw.loadURL('about:blank'); + obw.webContents.setWindowOpenHandler(() => ({ + action: 'allow', + overrideBrowserWindowOptions: { + webPreferences: { + offscreen: true + } + } + })); + + const didCreateWindow = once(obw.webContents, 'did-create-window'); + const url = `file://${nodePath.join('fixtures', 'pages', 'content.html')}`; + obw.webContents.executeJavaScript(`window.open('${JSON.stringify(url)}') && true`); + const [childWindow] = await didCreateWindow; + expect(childWindow.webContents.isOffscreen()).to.be.true('Child window should be offscreen'); + }); + ifit(hasCapturableScreen())('should not make child window background transparent', async () => { browserWindow.webContents.setWindowOpenHandler(() => ({ action: 'allow' })); const didCreateWindow = once(browserWindow.webContents, 'did-create-window');