Files
electron/shell/browser/api/electron_api_base_window.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

343 lines
12 KiB
C++

// Copyright (c) 2018 GitHub, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_BROWSER_API_ELECTRON_API_BASE_WINDOW_H_
#define ELECTRON_SHELL_BROWSER_API_ELECTRON_API_BASE_WINDOW_H_
#include <array>
#include <map>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
#include "content/public/browser/browser_thread.h"
#include "shell/browser/native_window_observer.h"
#include "shell/common/api/electron_api_native_image.h"
#include "shell/common/gin_helper/trackable_object.h"
#include "v8/include/cppgc/persistent.h"
#if BUILDFLAG(IS_MAC)
#include <optional>
#include "ui/gfx/geometry/point.h"
#endif
namespace gin {
class Arguments;
} // namespace gin
namespace gin_helper {
class PersistentDictionary;
template <typename T>
class Handle;
} // namespace gin_helper
namespace electron {
class NativeWindow;
namespace api {
class Menu;
class View;
class BaseWindow : public gin_helper::TrackableObject<BaseWindow>,
private NativeWindowObserver {
public:
static gin_helper::WrappableBase* New(gin::Arguments* args);
static void BuildPrototype(v8::Isolate* isolate,
v8::Local<v8::FunctionTemplate> prototype);
// The constructor's template, created on first use; BrowserWindow inherits
// from it.
static v8::Local<v8::FunctionTemplate> GetConstructorTemplate(
v8::Isolate* isolate);
// Clears window state from the Local State JSON file in
// app.getPath('userData') via PrefService.
static void ClearPersistedState(const std::string& window_name);
static bool IsWindowNameValid(const gin_helper::Dictionary& options,
std::string* error_message);
// False when the options name an app shim (macOS) that can't host a window.
static bool IsAppShimValid(const gin_helper::Dictionary& options,
std::string* error_message);
// Returns false, with the reason, when the `parent` in `options` is a
// window that can't have children.
static bool IsParentValid(const gin_helper::Dictionary& options,
std::string* error_message);
const NativeWindow* window() const { return window_.get(); }
NativeWindow* window() { return window_.get(); }
static BaseWindow* GetFocusedWindow();
// Null unless |value| is a live BaseWindow.
static BaseWindow* FromValue(v8::Isolate* isolate,
v8::Local<v8::Value> value);
// |window| may be dangling.
static bool IsLive(const BaseWindow* window);
void SetMenuNatively(Menu* menu);
void RemoveMenu();
protected:
// Common constructor.
BaseWindow(v8::Isolate* isolate, const gin_helper::Dictionary& options);
// Creating independent BaseWindow instance.
BaseWindow(gin::Arguments* args, const gin_helper::Dictionary& options);
~BaseWindow() override;
// gin_helper::WrappableBase:
void OnWrapped(v8::Isolate* isolate) override;
// TrackableObject:
void InitWith(v8::Isolate* isolate, v8::Local<v8::Object> wrapper) override;
// NativeWindowObserver:
void WillCloseWindow(bool* prevent_default) override;
void OnWindowClosed() override;
void OnWindowQueryEndSession(const std::vector<std::string>& reasons,
bool* prevent_default) override;
void OnWindowEndSession(const std::vector<std::string>& reasons) override;
void OnWindowBlur() override;
void OnWindowFocus() override;
void OnWindowShow() override;
void OnWindowHide() override;
void OnWindowMaximize() override;
void OnWindowUnmaximize() override;
void OnWindowMinimize() override;
void OnWindowRestore() override;
void OnWindowWillResize(const gfx::Rect& new_bounds,
gfx::ResizeEdge edge,
bool* prevent_default) override;
void OnWindowResize() override;
void OnWindowResized() override;
void OnWindowWillMove(const gfx::Rect& new_bounds,
bool* prevent_default) override;
void OnWindowMove() override;
void OnWindowMoved() override;
void OnWindowSwipe(const std::string& direction) override;
void OnWindowRotateGesture(float rotation) override;
void OnWindowSheetBegin() override;
void OnWindowSheetEnd() override;
void OnWindowIsKeyChanged(bool is_key) override;
void OnWindowEnterFullScreen() override;
void OnWindowLeaveFullScreen() override;
void OnWindowEnterHtmlFullScreen() override;
void OnWindowLeaveHtmlFullScreen() override;
void OnWindowAlwaysOnTopChanged(bool is_always_on_top) override;
void OnExecuteAppCommand(std::string_view command_name) override;
void OnTouchBarItemResult(const std::string& item_id,
const base::DictValue& details) override;
void OnNewWindowForTab() override;
void OnSystemContextMenu(int x, int y, bool* prevent_default) override;
void OnWindowStateRestored() override;
#if BUILDFLAG(IS_WIN)
void OnWindowMessage(UINT message, WPARAM w_param, LPARAM l_param) override;
#endif
// Public APIs of NativeWindow.
void SetContentView(View* view);
void Close();
virtual void CloseImmediately();
virtual void Focus();
virtual void Blur();
bool IsFocused() const;
virtual void Show();
virtual void ShowInactive();
void Hide();
bool IsVisible() const;
bool IsEnabled() const;
void SetEnabled(bool enable);
void Maximize();
void Unmaximize();
bool IsMaximized() const;
void Minimize();
void Restore();
bool IsMinimized() const;
void SetFullScreen(bool fullscreen);
bool IsFullscreen() const;
void SetBounds(v8::Local<v8::Object> bounds, gin::Arguments* args);
gfx::Rect GetBounds() const;
void SetSize(int width, int height, gin::Arguments* args);
std::array<int, 2U> GetSize() const;
void SetContentSize(int width, int height, gin::Arguments* args);
std::array<int, 2U> GetContentSize() const;
void SetContentBounds(const gfx::Rect& bounds, gin::Arguments* args);
gfx::Rect GetContentBounds() const;
bool IsNormal() const;
gfx::Rect GetNormalBounds() const;
void SetMinimumSize(int width, int height);
std::array<int, 2U> GetMinimumSize() const;
void SetMaximumSize(int width, int height);
std::array<int, 2U> GetMaximumSize() const;
void SetSheetOffset(double offsetY, gin::Arguments* args);
void SetResizable(bool resizable);
bool IsResizable() const;
void SetMovable(bool movable);
void MoveAbove(const std::string& sourceId, gin::Arguments* args);
void MoveTop();
bool IsMovable() const;
void SetMinimizable(bool minimizable);
bool IsMinimizable() const;
void SetMaximizable(bool maximizable);
bool IsMaximizable() const;
void SetFullScreenable(bool fullscreenable);
bool IsFullScreenable() const;
void SetClosable(bool closable);
bool IsClosable() const;
void SetAlwaysOnTop(bool top, gin::Arguments* args);
bool IsAlwaysOnTop() const;
void Center();
void SetPosition(int x, int y, gin::Arguments* args);
std::array<int, 2U> GetPosition() const;
void SetTitle(const std::string& title);
void SetTitleFromPage(const std::string& title);
bool SetTitleFromPageIfNotSetFromApi(const std::string& title);
std::string GetTitle() const;
void SetAccessibleTitle(const std::string& title);
std::string GetAccessibleTitle() const;
void FlashFrame(bool flash);
void SetSkipTaskbar(bool skip);
void SetExcludedFromShownWindowsMenu(bool excluded);
bool IsExcludedFromShownWindowsMenu() const;
void SetSimpleFullScreen(bool simple_fullscreen);
bool IsSimpleFullScreen() const;
void SetKiosk(bool kiosk);
bool IsKiosk() const;
bool IsTabletMode() const;
virtual void SetBackgroundColor(const std::string& color_name);
std::string GetBackgroundColor() const;
void InvalidateShadow();
void SetHasShadow(bool has_shadow);
bool HasShadow() const;
void SetOpacity(const double opacity);
double GetOpacity() const;
void SetShape(const std::vector<gfx::Rect>& rects);
void SetRepresentedFilename(const std::string& filename);
std::string GetRepresentedFilename() const;
void SetDocumentEdited(bool edited);
bool IsDocumentEdited() const;
void SetIgnoreMouseEvents(bool ignore, gin::Arguments* args);
void SetContentProtection(bool enable);
bool IsContentProtected() const;
void SetFocusable(bool focusable);
bool IsFocusable() const;
void SetMenu(v8::Isolate* isolate, v8::Local<v8::Value> menu);
void SetParentWindow(v8::Local<v8::Value> value, gin::Arguments* args);
std::string GetMediaSourceId() const;
v8::Local<v8::Value> GetNativeWindowHandle();
void SetProgressBar(double progress, gin::Arguments* args);
void SetOverlayIcon(const gfx::Image& overlay,
const std::string& description);
void SetVisibleOnAllWorkspaces(bool visible, gin::Arguments* args);
bool IsVisibleOnAllWorkspaces() const;
void SetAutoHideCursor(bool auto_hide);
virtual void SetVibrancy(v8::Isolate* isolate,
v8::Local<v8::Value> value,
gin::Arguments* args);
virtual void SetBackgroundMaterial(const std::string& material);
#if BUILDFLAG(IS_MAC)
std::string GetAlwaysOnTopLevel() const;
void SetWindowButtonVisibility(bool visible);
bool GetWindowButtonVisibility() const;
void SetWindowButtonPosition(std::optional<gfx::Point> position);
std::optional<gfx::Point> GetWindowButtonPosition() const;
bool IsHiddenInMissionControl();
void SetHiddenInMissionControl(bool hidden);
#endif
void SetTouchBar(std::vector<gin_helper::PersistentDictionary> items);
void RefreshTouchBarItem(const std::string& item_id);
void SetEscapeTouchBarItem(gin_helper::PersistentDictionary item);
void SelectPreviousTab();
void SelectNextTab();
void ShowAllTabs();
void MergeAllWindows();
void MoveTabToNewWindow();
void ToggleTabBar();
void AddTabbedWindow(NativeWindow* window, gin::Arguments* args);
v8::Local<v8::Value> GetTabbingIdentifier();
void SetAutoHideMenuBar(bool auto_hide);
bool IsMenuBarAutoHide() const;
void SetMenuBarVisibility(bool visible);
bool IsMenuBarVisible() const;
void SetAspectRatio(double aspect_ratio, gin::Arguments* args);
void PreviewFile(const std::string& path, gin::Arguments* args);
void CloseFilePreview();
// Public getters of NativeWindow.
v8::Local<v8::Value> GetContentView() const;
BaseWindow* GetParentWindow() const;
std::vector<BaseWindow*> GetChildWindows() const;
bool IsModal() const;
// Extra APIs added in JS.
bool SetThumbarButtons(gin::Arguments* args);
#if defined(TOOLKIT_VIEWS)
void SetIcon(v8::Isolate* isolate, v8::Local<v8::Value> icon);
void SetIconImpl(v8::Isolate* isolate,
v8::Local<v8::Value> icon,
NativeImage::OnConvertError on_error);
#endif
#if BUILDFLAG(IS_WIN)
typedef base::RepeatingCallback<void(v8::Local<v8::Value>,
v8::Local<v8::Value>)>
MessageCallback;
bool HookWindowMessage(UINT message, const MessageCallback& callback);
bool IsWindowMessageHooked(UINT message);
void UnhookWindowMessage(UINT message);
void UnhookAllWindowMessages();
bool SetThumbnailClip(const gfx::Rect& region);
bool SetThumbnailToolTip(const std::string& tooltip);
void SetAppDetails(const gin_helper::Dictionary& options);
bool IsSnapped() const;
void SetAccentColor(gin::Arguments* args);
v8::Local<v8::Value> GetAccentColor() const;
#endif
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX)
void SetTitleBarOverlay(const gin_helper::Dictionary& options,
gin::Arguments* args);
#endif
[[nodiscard]] constexpr int32_t GetID() const { return weak_map_id(); }
private:
// Helpers.
template <typename... Args>
void EmitEventSoon(std::string_view eventName) {
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(base::IgnoreResult(&BaseWindow::Emit<Args...>),
weak_factory_.GetWeakPtr(), eventName));
}
#if BUILDFLAG(IS_WIN)
typedef std::map<UINT, MessageCallback> MessageCallbackMap;
MessageCallbackMap messages_callback_map_;
#endif
cppgc::Persistent<View> content_view_;
cppgc::Persistent<Menu> menu_;
v8::Global<v8::Value> parent_window_;
std::unique_ptr<NativeWindow> window_;
// Reference to JS wrapper to prevent garbage collection.
v8::Global<v8::Value> self_ref_;
bool title_set_from_api_ = false;
base::WeakPtrFactory<BaseWindow> weak_factory_{this};
};
} // namespace api
} // namespace electron
#endif // ELECTRON_SHELL_BROWSER_API_ELECTRON_API_BASE_WINDOW_H_