Files
electron/shell/browser/api/electron_api_browser_window.cc
T
Samuel AttardandClaude 0edcdc47c0 feat: add app shims on macOS (#54932)
* feat: add app shims on macOS

An app shim is a small app, in a bundle of its own, that shows windows on
behalf of a host Electron app. The host still creates each window, loads
its page and handles its events, but the NSWindow belongs to the shim's
process, so the Dock, the app switcher, Mission Control and Stage Manager
attribute the window to the shim and not to the host.

- `app.startAppShimServer()` / `app.stopAppShimServer()` and the
  `app-shim-connected` event, which hands over an `AppShimHost`.
- A `BaseWindow` / `BrowserWindow` created with the `appShim` option has
  its native window made in that shim's process, through Chromium's
  remote_cocoa. Such a window is a new NativeWindow implementation that
  only goes through views::Widget; what remote_cocoa has no call for is
  a no-op (see docs/api/app-shim-host.md).
- The shim's executable is `Helpers/app_shim_loader` from the framework
  bundle. It links nothing from Electron: it loads the host's framework
  and calls the new `ElectronAppShimMain` export, which runs an
  `app-shim` process that connects back to the host over a Mach
  bootstrap server.
- Electron Framework gains an rpath to the folder it is in, so that it
  finds its sibling frameworks when an executable in another bundle
  loads it.

Not available in Mac App Store builds.

Co-Authored-By: Claude <noreply@anthropic.com>

* feat: app shim menus, dialogs, window chrome and review fixes

Follow-up to the first app shim commit.

Fixes from review:
- A window torn down from one of its own listeners (shim.quit() or
  win.destroy() in `show`) no longer frees the native host that is
  notifying.
- The native close button no longer leaves the shim waiting on an answer
  that was dropped with the window.
- Keys the page does not handle go back to the shim, whose menu they are
  shortcuts of, and not to the host's menu.
- stopAppShimServer() drops shims that have connected and not said hello.
- Quitting a shim asks the host without blocking and without cancelling a
  logout; quit-requested can now be answered with "no".
- Size limits and useContentSize are in content sizes; restore() and
  unmaximize() each do one thing; webContents.isFocused() works.

New:
- shim.setMenu(): a menu bar of the shim's own, built in the shim from a
  description of the Menu, with clicks sent back.
- Message boxes and open and save dialogs are sheets of the window in the
  shim's process.
- titleBarStyle, traffic light position and visibility, vibrancy, tabs and
  Quick Look, applied in the shim to the NSWindow that remote_cocoa made.
- A Chromium patch for what remote_cocoa has no call for: movable,
  closable, minimizable and fullscreenable, clearing an aspect ratio, and
  yielding activation to the hosting process by pid.
- A shim that loaded another build of Electron than the running host
  tells the host and quits: app-shim-version-mismatch.

The API is marked experimental in the docs.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: address review of app shims

- A window in an app shim can't be given a parent, at construction or
  with setParentWindow(), as the docs say.
- shim.setMenu() is now shim.setApplicationMenu().
- A message box that is showing under an id makes way for a new one with
  that id, whichever process shows it.
- Mac App Store builds no longer export the shim's entry point; they
  already had no loader.
- setProgressBar() and setHasShadow() are listed among what does nothing
  on a window in a shim.
- The spec no longer asserts inside an event listener.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: clang-tidy findings in app shim code

* docs: reword the app shim docs for clarity

* chore: update patches

Re-exported after rebasing onto Chromium 158.0.8105.0; only line offsets
changed.

Co-Authored-By: Claude <noreply@anthropic.com>

* test: use vitest matchers in the app shim spec

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-16 14:03:19 -07:00

429 lines
14 KiB
C++

// Copyright (c) 2013 GitHub, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/browser/api/electron_api_browser_window.h"
#include "base/containers/fixed_flat_set.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "shell/browser/api/electron_api_web_contents_view.h"
#include "shell/browser/browser.h"
#include "shell/browser/native_window.h"
#include "shell/browser/ui/inspectable_web_contents.h"
#include "shell/browser/ui/inspectable_web_contents_view.h"
#include "shell/browser/web_contents_preferences.h"
#include "shell/browser/window_list.h"
#include "shell/common/color_util.h"
#include "shell/common/gin_helper/constructor.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/error_thrower.h"
#include "shell/common/gin_helper/object_template_builder.h"
#include "shell/common/node_includes.h"
#include "shell/common/options_switches.h"
#include "ui/views/focus/focus_manager.h"
#include "ui/views/widget/widget.h"
namespace electron::api {
BrowserWindow::BrowserWindow(gin::Arguments* args,
const gin_helper::Dictionary& options)
: BaseWindow(args->isolate(), options) {
// Use options.webPreferences in WebContents.
v8::Isolate* isolate = args->isolate();
auto web_preferences = gin_helper::Dictionary::CreateEmpty(isolate);
options.Get(options::kWebPreferences, &web_preferences);
// Copy the backgroundColor to webContents.
std::string color;
if (options.Get(options::kBackgroundColor, &color)) {
web_preferences.SetHidden(options::kBackgroundColor, color);
} else if (window()->IsTranslucent()) {
// If the BrowserWindow is transparent or a vibrancy type has been set,
// also propagate transparency to the WebContents unless a separate
// backgroundColor has been set.
web_preferences.SetHidden(options::kBackgroundColor,
ToRGBAHex(SK_ColorTRANSPARENT));
}
// Copy the show setting to webContents, but only if we don't want to paint
// when initially hidden
bool paint_when_initially_hidden = true;
options.Get(options::kPaintWhenInitiallyHidden, &paint_when_initially_hidden);
if (!paint_when_initially_hidden) {
bool show = true;
options.Get(options::kShow, &show);
web_preferences.Set(options::kShow, show);
}
// Copy the webContents option to webPreferences.
v8::Local<v8::Value> value;
if (options.Get("webContents", &value)) {
web_preferences.SetHidden("webContents", value);
}
if (!web_preferences.Has(options::kShow))
web_preferences.Set(options::kShow, true);
// Creates the WebContentsView.
WebContentsView* web_contents_view =
WebContentsView::Create(isolate, web_preferences);
CHECK(web_contents_view);
web_contents_view->RegisterDraggableRegionProvider(window());
auto* web_contents = web_contents_view->GetWebContents();
window()->InitPrimaryWebContentsView(
web_contents->inspectable_web_contents()->GetView());
web_contents_view_ = web_contents_view;
// Save a reference of the WebContents.
v8::Local<v8::Object> wrapper =
web_contents->GetWrapper(isolate).ToLocalChecked();
web_contents_.Reset(isolate, wrapper);
api_web_contents_ = web_contents;
api_web_contents_->AddObserver(this);
Observe(api_web_contents_->web_contents());
// Associate with BrowserWindow.
web_contents->SetOwnerWindow(window());
InitWithArgs(args);
// Install the content view after BaseWindow's JS code is initialized.
// The WebContentsView is added a sibling of BaseWindow's contentView (before
// it in the paint order) so that any views added to BrowserWindow's
// contentView will be painted on top of the BrowserWindow's WebContentsView.
// See https://github.com/electron/electron/pull/41256.
// Note that |GetContentsView|, confusingly, does not refer to the same thing
// as |BaseWindow::GetContentView|.
window()->GetContentsView()->AddChildViewAt(web_contents_view->view(), 0);
window()->GetContentsView()->DeprecatedLayoutImmediately();
// Init window after everything has been setup.
window()->InitFromOptions(options);
}
BrowserWindow::~BrowserWindow() {
if (auto* web_contents = api_web_contents_.Get()) {
// Cleanup the observers if user destroyed this instance directly instead of
// gracefully closing content::WebContents.
web_contents->RemoveObserver(this);
if (!web_contents->IsDestroyed() && web_contents->web_contents())
web_contents->Destroy();
}
}
WebContents* BrowserWindow::GetLiveWebContents() const {
WebContents* web_contents = api_web_contents_.Get();
return web_contents && !web_contents->IsDestroyed() &&
web_contents->web_contents()
? web_contents
: nullptr;
}
void BrowserWindow::BeforeUnloadDialogCancelled() {
WindowList::WindowCloseCancelled(window());
}
void BrowserWindow::WebContentsDestroyed() {
api_web_contents_ = nullptr;
CloseImmediately();
}
void BrowserWindow::OnSetContentBounds(const gfx::Rect& rect) {
// window.resizeTo(...)
// window.moveTo(...)
window()->SetBounds(rect, false);
}
void BrowserWindow::OnActivateContents() {
// Hide the auto-hide menu when webContents is focused.
#if !BUILDFLAG(IS_MAC)
if (IsMenuBarAutoHide() && IsMenuBarVisible())
window()->SetMenuBarVisibility(false);
#endif
}
void BrowserWindow::OnPageTitleUpdated(
const std::u16string& title,
bool explicit_set,
bool from_same_document_history_navigation) {
// Change window title to page title.
auto self = weak_factory_.GetWeakPtr();
if (!Emit("page-title-updated", title, explicit_set)) {
// |this| might be deleted, or marked as destroyed by close().
if (self && !IsDestroyed()) {
if (from_same_document_history_navigation) {
SetTitleFromPageIfNotSetFromApi(base::UTF16ToUTF8(title));
} else {
SetTitleFromPage(base::UTF16ToUTF8(title));
}
}
}
}
void BrowserWindow::RequestPreferredWidth(int* width) {
WebContents* api_web_contents = GetLiveWebContents();
content::WebContents* contents =
api_web_contents ? api_web_contents->web_contents() : nullptr;
*width = contents ? contents->GetPreferredSize().width() : 0;
}
void BrowserWindow::OnCloseButtonClicked(bool* prevent_default) {
// When user tries to close the window by clicking the close button, we do
// not close the window immediately, instead we try to close the web page
// first, and when the web page is closed the window will also be closed.
*prevent_default = true;
// Already closed by renderer.
WebContents* api_web_contents = GetLiveWebContents();
content::WebContents* contents =
api_web_contents ? api_web_contents->web_contents() : nullptr;
if (!contents)
return;
// Required to make beforeunload handler work.
api_web_contents->NotifyUserActivation();
if (contents->NeedToFireBeforeUnloadOrUnloadEvents()) {
contents->DispatchBeforeUnload(false /* auto_cancel */);
} else {
contents->Close();
}
}
void BrowserWindow::OnWindowBlur() {
if (auto* api_web_contents = GetLiveWebContents())
api_web_contents->web_contents()->StoreFocus();
BaseWindow::OnWindowBlur();
}
void BrowserWindow::OnWindowFocus() {
// focus/blur events might be emitted while closing window. When the views
// focus manager already restored focus to another view in this window (a
// child WebContentsView the user clicked into), leave it there.
if (auto* api_web_contents = GetLiveWebContents();
api_web_contents && !FocusIsInAnotherView()) {
content::WebContents* contents = api_web_contents->web_contents();
contents->RestoreFocus();
#if !BUILDFLAG(IS_MAC)
if (!api_web_contents->IsDevToolsOpened())
contents->Focus();
#endif
}
BaseWindow::OnWindowFocus();
}
bool BrowserWindow::FocusIsInAnotherView() {
#if BUILDFLAG(IS_MAC)
return false;
#else
auto* focus_manager = window()->widget()->GetFocusManager();
auto* focused = focus_manager ? focus_manager->GetFocusedView() : nullptr;
auto* primary = window()->primary_web_contents_view();
return focused && primary && !primary->Contains(focused);
#endif
}
void BrowserWindow::OnWindowIsKeyChanged(bool is_key) {
#if BUILDFLAG(IS_MAC)
if (auto* api_web_contents = GetLiveWebContents()) {
auto* rwhv = api_web_contents->web_contents()->GetRenderWidgetHostView();
if (rwhv)
rwhv->SetActive(is_key);
}
window()->SetActive(is_key);
#endif
}
void BrowserWindow::OnWindowLeaveFullScreen() {
#if BUILDFLAG(IS_MAC)
if (auto* api_web_contents = GetLiveWebContents()) {
content::WebContents* contents = api_web_contents->web_contents();
if (contents->IsFullscreen())
contents->ExitFullscreen(true);
}
#endif
BaseWindow::OnWindowLeaveFullScreen();
}
void BrowserWindow::CloseImmediately() {
// Close all child windows before closing current window.
for (BaseWindow* child : GetChildWindows())
child->window()->CloseImmediately();
BaseWindow::CloseImmediately();
}
void BrowserWindow::Focus() {
if (auto* api_web_contents = GetLiveWebContents();
api_web_contents && api_web_contents->IsOffScreen())
FocusOnWebView();
else
BaseWindow::Focus();
}
void BrowserWindow::Blur() {
if (auto* api_web_contents = GetLiveWebContents();
api_web_contents && api_web_contents->IsOffScreen())
BlurWebView();
else
BaseWindow::Blur();
}
void BrowserWindow::SetBackgroundColor(const std::string& color_name) {
BaseWindow::SetBackgroundColor(color_name);
SkColor color = ParseCSSColor(color_name).value_or(SK_ColorWHITE);
if (auto* api_web_contents = GetLiveWebContents()) {
api_web_contents->SetBackgroundColor(color);
// Also update the web preferences object otherwise the view will be reset
// on the next load URL call
auto* web_preferences =
WebContentsPreferences::From(api_web_contents->web_contents());
if (web_preferences) {
web_preferences->SetBackgroundColor(color);
}
}
}
void BrowserWindow::SetBackgroundMaterial(const std::string& material) {
BaseWindow::SetBackgroundMaterial(material);
static constexpr auto materialTypes =
base::MakeFixedFlatSet<std::string_view>({"tabbed", "mica", "acrylic"});
if (materialTypes.contains(material)) {
SetBackgroundColor(ToRGBAHex(SK_ColorTRANSPARENT));
} else if (material == "none") {
SetBackgroundColor(ToRGBAHex(SK_ColorWHITE));
}
}
void BrowserWindow::FocusOnWebView() {
if (auto* api_web_contents = GetLiveWebContents()) {
api_web_contents->web_contents()->GetRenderViewHost()->GetWidget()->Focus();
}
}
void BrowserWindow::BlurWebView() {
if (auto* api_web_contents = GetLiveWebContents())
api_web_contents->web_contents()->GetRenderViewHost()->GetWidget()->Blur();
}
v8::Local<v8::Value> BrowserWindow::GetWebContents(v8::Isolate* isolate) {
if (web_contents_.IsEmpty())
return v8::Null(isolate);
return v8::Local<v8::Value>::New(isolate, web_contents_);
}
void BrowserWindow::OnWindowShow() {
if (auto* api_web_contents = GetLiveWebContents();
api_web_contents && !web_contents_shown_) {
api_web_contents->web_contents()->WasShown();
web_contents_shown_ = true;
}
BaseWindow::OnWindowShow();
}
void BrowserWindow::OnWindowHide() {
if (auto* api_web_contents = GetLiveWebContents())
api_web_contents->web_contents()->WasOccluded();
web_contents_shown_ = false;
BaseWindow::OnWindowHide();
}
void BrowserWindow::Show() {
if (auto* api_web_contents = GetLiveWebContents()) {
api_web_contents->web_contents()->WasShown();
web_contents_shown_ = true;
}
BaseWindow::Show();
}
void BrowserWindow::ShowInactive() {
// This method doesn't make sense for modal window.
if (IsModal())
return;
if (auto* api_web_contents = GetLiveWebContents()) {
api_web_contents->web_contents()->WasShown();
web_contents_shown_ = true;
}
BaseWindow::ShowInactive();
}
// static
gin_helper::WrappableBase* BrowserWindow::New(gin_helper::ErrorThrower thrower,
gin::Arguments* args) {
if (!Browser::Get()->is_ready()) {
thrower.ThrowError("Cannot create BrowserWindow before app is ready");
return nullptr;
}
if (args->Length() > 1) {
args->ThrowError();
return nullptr;
}
gin_helper::Dictionary options;
if (!(args->Length() == 1 && args->GetNext(&options))) {
options = gin::Dictionary::CreateEmpty(args->isolate());
}
std::string error_message;
if (!IsWindowNameValid(options, &error_message)) {
// Window name is already in use throw an error and do not create the window
thrower.ThrowError(error_message);
return nullptr;
}
if (!IsAppShimValid(options, &error_message) ||
!IsParentValid(options, &error_message)) {
thrower.ThrowTypeError(error_message);
return nullptr;
}
return new BrowserWindow(args, options);
}
// static
void BrowserWindow::BuildPrototype(v8::Isolate* isolate,
v8::Local<v8::FunctionTemplate> prototype) {
prototype->SetClassName(gin::StringToV8(isolate, "BrowserWindow"));
gin_helper::ObjectTemplateBuilder(isolate, prototype->PrototypeTemplate())
.SetMethod<&BrowserWindow::FocusOnWebView>("focusOnWebView")
.SetMethod<&BrowserWindow::BlurWebView>("blurWebView")
.SetProperty<&BrowserWindow::GetWebContents>("webContents");
}
// static
v8::Local<v8::Value> BrowserWindow::From(v8::Isolate* isolate,
NativeWindow* native_window) {
if (native_window != nullptr) {
if (auto* base = FromWeakMapID(isolate, native_window->base_window_id()))
return base->GetWrapper();
}
return v8::Null(isolate);
}
} // namespace electron::api
namespace {
using electron::api::BrowserWindow;
void Initialize(v8::Local<v8::Object> exports,
v8::Local<v8::Value> unused,
v8::Local<v8::Context> context,
void* priv) {
v8::Isolate* const isolate = electron::JavascriptEnvironment::GetIsolate();
gin_helper::Dictionary dict{isolate, exports};
dict.Set("BrowserWindow",
gin_helper::CreateConstructor<BrowserWindow>(
isolate, base::BindRepeating(&BrowserWindow::New),
electron::api::BaseWindow::GetConstructorTemplate(isolate)));
}
} // namespace
NODE_LINKED_BINDING_CONTEXT_AWARE(electron_browser_window, Initialize)