Files
electron/shell/app_shim/app_shim_controller.h
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

164 lines
6.3 KiB
Objective-C

// Copyright (c) 2026 Anthropic, PBC.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_APP_SHIM_APP_SHIM_CONTROLLER_H_
#define ELECTRON_SHELL_APP_SHIM_APP_SHIM_CONTROLLER_H_
#import <Foundation/Foundation.h>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include "base/files/file_path.h"
#include "base/memory/weak_ptr.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "electron/shell/common/app_shim.mojom.h"
#include "mojo/public/cpp/bindings/pending_associated_receiver.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "mojo/public/cpp/platform/platform_channel_endpoint.h"
#include "mojo/public/cpp/system/isolated_connection.h"
#include "shell/app_shim/app_shim_menu.h"
class GURL;
namespace display {
class ScopedNativeScreen;
} // namespace display
@class ElectronAppShimDelegate;
@class ElectronMenuController;
namespace electron {
// Runs an app shim process: finds (or starts) the host Electron app, connects
// to it, and then serves the windows the host assigns to this process.
class AppShimController : public mojom::AppShim, private AppShimMenu::Client {
public:
explicit AppShimController(const base::FilePath& host_bundle_path);
~AppShimController() override;
// The process's one controller, or null before it is made.
static AppShimController* Get();
AppShimController(const AppShimController&) = delete;
AppShimController& operator=(const AppShimController&) = delete;
// From the NSApplication delegate.
void OnReopen(bool has_visible_windows);
void OnOpenFiles(std::vector<base::FilePath> files);
void OnOpenUrls(std::vector<GURL> urls);
void OnDidBecomeActive();
void OnNewWindowForTab(uint64_t window_id);
// From -[NSApplication terminate:]. False when the host was asked instead;
// Quit(), or no answer in time, then terminates the app.
bool ShouldTerminate();
// From the NSApplication. Each mode is asked for at most once.
void EnableAccessibilitySupport(mojom::AppShimAccessibilityMode mode);
private:
// The name the host publishes its bootstrap server under.
std::string ServerName() const;
bool IsHostRunning() const;
void LaunchHost();
void PollForHost();
// Hands the host, whose bootstrap server `server_endpoint` is, one end of
// a new channel; invalid when that fails.
static mojo::PlatformChannelEndpoint ConnectToHost(
mojo::PlatformChannelEndpoint server_endpoint);
// The host is running another build of Electron than this process loaded.
// Tells the host, which tells the person, and quits.
void OnBuildMismatch(mojo::PlatformChannelEndpoint server_endpoint);
void OnConnected(mojo::PlatformChannelEndpoint endpoint);
void OnShimConnectedResponse(
mojo::PendingReceiver<mojom::AppShim> app_shim_receiver);
void Terminate();
void OnQuitAnswerTimeout();
void SetUpMenu();
// AppShimMenu::Client:
void OnMenuItemActivated(const std::vector<uint32_t>& path,
int command_id,
int event_flags) override;
void OnMenuWillShow() override;
void OnMenuWillClose() override;
// mojom::AppShim:
void CreateRemoteCocoaApplication(
mojo::PendingAssociatedReceiver<remote_cocoa::mojom::Application>
application) override;
void SetBadgeLabel(const std::string& label) override;
void SetUserAttention(mojom::AppShimAttentionType type) override;
void Quit() override;
void CancelQuit() override;
void SetApplicationMenu(
std::optional<std::vector<mojom::AppShimMenuItemPtr>> items) override;
void InitWindowChrome(uint64_t window_id,
mojom::AppShimWindowChromePtr chrome) override;
void SetWindowButtonVisibility(uint64_t window_id, bool visible) override;
void SetWindowButtonPosition(
uint64_t window_id,
const std::optional<gfx::Point>& position) override;
void SetWindowVibrancy(uint64_t window_id,
const std::string& type,
int32_t duration_ms,
int32_t state) override;
void PerformWindowTabAction(uint64_t window_id,
mojom::AppShimTabAction action) override;
void AddTabbedWindow(uint64_t window_id, uint64_t tabbed_window_id) override;
void PreviewFile(uint64_t window_id,
const base::FilePath& path,
const std::string& display_name) override;
void CloseFilePreview(uint64_t window_id) override;
void ShowMessageBox(uint64_t window_id,
mojom::AppShimMessageBoxOptionsPtr options,
ShowMessageBoxCallback callback) override;
void CloseMessageBox(int32_t id) override;
void ShowOpenDialog(uint64_t window_id,
mojom::AppShimFileDialogOptionsPtr options,
ShowOpenDialogCallback callback) override;
void ShowSaveDialog(uint64_t window_id,
mojom::AppShimFileDialogOptionsPtr options,
ShowSaveDialogCallback callback) override;
const base::FilePath host_bundle_path_;
ElectronAppShimDelegate* __strong delegate_;
// The windows this process hosts ask it about the displays.
std::unique_ptr<display::ScopedNativeScreen> screen_;
base::TimeTicks poll_deadline_;
bool launched_host_ = false;
// The host's menu, while it has given the shim one. The controller only
// refers to the menu's model.
std::unique_ptr<AppShimMenu> menu_;
ElectronMenuController* __strong menu_controller_;
bool terminating_ = false;
// The host has been asked whether the shim may quit.
bool waiting_for_quit_answer_ = false;
base::OneShotTimer quit_answer_timer_;
bool asked_for_partial_accessibility_ = false;
bool asked_for_complete_accessibility_ = false;
NSInteger attention_request_id_ = 0;
mojo::IsolatedConnection bootstrap_connection_;
mojo::Remote<mojom::AppShimHostBootstrap> host_bootstrap_;
mojo::Receiver<mojom::AppShim> shim_receiver_{this};
mojo::Remote<mojom::AppShimHost> host_;
// Calls on `host_` made before the host binds this are delivered once it
// does.
mojo::PendingReceiver<mojom::AppShimHost> host_receiver_;
base::WeakPtrFactory<AppShimController> weak_factory_{this};
};
} // namespace electron
#endif // ELECTRON_SHELL_APP_SHIM_APP_SHIM_CONTROLLER_H_