feat: add WindowManagerMacOS for multi-window support - #3052
Open
realZachi wants to merge 1 commit into
Open
Conversation
react-native-macos has so far had no way to show React content in more
than one window: RCTAppDelegate creates a single NSWindow in
loadReactNativeWindow: and there is no JavaScript API to create more.
Add a WindowManagerMacOS module that opens additional NSWindows from
JavaScript. Each window hosts its own React root, identified by an app
key registered with AppRegistry.registerComponent, while all windows
share a single bridge and JavaScript runtime.
AppRegistry.registerComponent('Settings', () => SettingsScreen);
await WindowManagerMacOS.open({
moduleName: 'Settings',
title: 'Settings',
width: 480,
height: 360,
});
Root views are created through the app delegate's RCTRootViewFactory,
which already guards bridge and host creation internally and can safely
be called more than once, so the same code path works for the old
architecture, Fabric and bridgeless alike. When no factory is available
the module falls back to -[RCTRootView initWithBridge:moduleName:
initialProperties:] so the bridge is still shared.
open() is idempotent per key: opening an already-open key focuses the
existing window instead of creating a duplicate. Windows can be closed,
focused, retitled and enumerated, and lifecycle events are delivered to
JavaScript via RCTEventEmitter.
Also make RCTKeyWindow() fall back to the main window and then to any
visible, focusable window on macOS. It previously returned
[NSApp keyWindow] unconditionally, which is nil whenever no window holds
key focus, causing RCTDeviceInfo to report a zero-sized window through
Dimensions. That is more likely to happen with several windows open.
Adds an RNTester example under the macOS category demonstrating opening,
focusing, retitling and closing secondary windows, and a shared counter
that shows state being observed across windows.
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
Author
|
@microsoft-github-policy-service agree
@microsoft-github-policy-service agree |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary:
react-native-macos currently has no way to show React content in more than one window.
RCTAppDelegatecreates exactly oneNSWindowinloadReactNativeWindow:, and there is no JavaScript API to create additional ones. Multi-window support has been asked for repeatedly (#604, #617), and the scope wiki notes the fork has no window APIs today.This adds
WindowManagerMacOS, a macOS-only module for opening and managing additional windows from JavaScript. Each window hosts its own React root — identified by an app key registered withAppRegistry.registerComponent— while all windows share a single bridge and JavaScript runtime.API:
open,close,focus,setTitle,isOpen,getWindows, pluswindowDidOpen/windowDidClose/windowDidFocus/windowDidBlur/windowDidResizeevents viaRCTEventEmitter.Implementation notes
RCTRootViewFactory. That factory already guards bridge and host creation internally (createBridgeIfNeeded:returns early when a bridge exists), so it can safely be called more than once — the capability was already there, just unused. Going through it means the same code path works for the old architecture, Fabric and bridgeless without the module having to branch on architecture. When no factory is available it falls back to-[RCTRootView initWithBridge:moduleName:initialProperties:], so the bridge is still shared.-initWithBundleURL:is deliberately never used, since that would create a second bridge.RCTRootViewFactorylives inReact-RCTAppDelegate, which depends onReact-Core; importing it fromReact/CoreModuleswould be circular. The module instead redeclares the two members it needs as local protocols and relies on selector dispatch, guarded byrespondsToSelector:.open()is idempotent perkey. Opening an already-open key focuses the existing window and resolves with its info instead of creating a duplicate — the behaviour a "open settings" menu item wants.window.releasedWhenClosed = NO, since the window is retained by the module's registry.RCTKeyWindow()fallback. On macOS it returned[NSApp keyWindow]unconditionally, which isnilwhenever no window holds key focus —RCTDeviceInfothen reports a zero-sized window throughDimensions(useWindowDimensions() and Dimensions.get("window") both return { width: 0, height: 0 } when window does not have keyboard focus [cause and solution provided] #2296). It now falls back to the main window and then to any visible, focusable window. This is more likely to be hit with several windows open, but it is an existing bug independent of this feature.Known limitation
Dimensions/useWindowDimensionsstill report the app's key window rather than the window a given root is rendered into. Making those per-surface is a larger change toRCTDeviceInfoand is not attempted here.This change is macOS-only (
#if TARGET_OS_OSX), so there is no corresponding facebook/react-native PR — the API has no meaning on iOS, where the module is not registered andWindowManagerMacOS.isSupportedisfalse.Test Plan:
Automated — run against this branch:
yarn flow-check→Found 0 errorsyarn lint→ exit 0, no findingsyarn jest packages/react-native/Libraries→ 115 suites / 1330 tests / 207 snapshots passedyarn changeset status --since=origin/main→ exit 0,react-native-macosat patchyarn test-typescript→ the only two errors areDuplicate identifier 'FocusEvent'inLibraries/Types/CoreEventTypes.d.ts, which reproduce identically on a cleanmainand are unrelated to this changeBuild —
packages/rn-tester, schemeRNTester-macOS, Debug:** BUILD SUCCEEDED **, withRCTWindowManager.mmcompiled into the target.Runtime — verified in a separate macOS app on the old architecture, driving the public API directly:
isSupportedtrueopen({moduleName, title, width: 460, height: 520})460x520, window appearswindowDidOpenlistenergetWindows()1open()again with the same keygetWindows()still1(no duplicate)close(key)true,getWindows()→0State declared in a plain module-scope store is observed and mutated from both windows, confirming the shared JavaScript runtime.
Manual — a new RNTester example under the macOS category ("WindowManagerMacOS") exercises opening two windows, focusing, retitling, closing, the idempotent re-open, a live list of open windows driven by the lifecycle events, and a counter shared across windows.