Repository navigation
Add Uno Platform support: run Reactor on desktop, WebAssembly, Android and iOS - #1079
Martin Zikmund (MartinZikmund) wants to merge 5 commits into
Conversation
…clean Groundwork for the Uno Platform port. This is the entire footprint the port has on existing files — everything else it adds is new. - global.json gains an Uno.Sdk msbuild-sdks pin. Inert for the Windows build: no Windows project declares Sdk="Uno.Sdk". It is required at the *solution* level because SDK resolution for Reactor.Uno.slnx reads the repo-root global.json; the per-project ones are never consulted for a root slnx build. - Reconciler.Mount.cs returns `default` instead of `null` from a generic helper. T is constrained to a reference type, so this is behaviour-identical on Windows; it just satisfies the nullable analysis on the Uno build. Not a conditional — the port compiles the shared source with ZERO #if REACTOR_UNO. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017rs9N4ZH4ss5tDtBwBN1Go
…ework Recompiles the Reactor framework against Uno's Microsoft.UI.Xaml so the same Reactor C# runs on desktop (Win32/X11/macOS/FrameBuffer), WebAssembly, Android and iOS. Ships as assembly `Reactor`, namespace `Microsoft.UI.Reactor`, so consumer code is unchanged. Fundamentally a retargeting, not a rewrite: the reconciler, hooks, DSL, layout engine, charting, markdown and data grid are source-shared verbatim from src/Reactor and src/Reactor.Advanced. Only the Windows-only hosting stack (windowing/shell/DWM/persistence/devtools) and Docking/Win2D are excluded, and a slim Uno hosting layer replaces them: ReactorApp (Run/RunAsync/CreateApplication), ReactorHost (render loop), ReactorWindow, and multi-window support. Notable behaviour this layer gets right rather than stubs: - Real secondary windows on every desktop head, each with its own host and state. - Per-window DPI from XamlRoot.RasterizationScale, tracking live changes. - Window closing guards via AppWindow.Closing; file/folder pickers; declarative caption height via AppWindowTitleBar.PreferredHeightOption. - Hot Reload: the shared HotReloadService's [assembly: MetadataUpdateHandler] is active here too, and the render loop implements the full spec 049 pass — hook-cell migration and HookOrderException recovery, not just a re-render. - A themed window root, because a Skia window paints nothing of its own. - DIP->physical conversion for AppWindow.Resize, which takes pixels. Source-share drift is the main hazard of this design and is guarded: globbed roots and the cherry-picked Hosting/ folder are both diffed against declared lists at build time, so an upstream move fails the build instead of silently dropping a subsystem. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017rs9N4ZH4ss5tDtBwBN1Go
Three samples plus a dedicated solution. ReactorUnoCounter and ReactorUnoShowcase are Uno single-projects targeting all four heads — desktop, WebAssembly, Android, iOS — from one project. The Component source is identical everywhere; only the entry point differs: desktop and iOS use ReactorApp.Run<T>() (Uno gives the Apple heads the same host-builder shape as desktop), wasm uses RunAsync because the browser thread cannot block, and Android — the one target with no console entry point — starts from an Activity that calls ReactorApp.CreateApplication<T>(). file-based/Counter.cs is the whole app in a single .cs file via `#:sdk Uno.Sdk`, run with `dotnet run Counter.cs`. Two things worth knowing, both documented in samples/Uno/README.md: - Reactor apps contain no XAML, so there is no ApplicationDefinition for Uno's iOS Hot Restart helper generator to read and it fails the build with Uno0005. The samples set UnoDisableHotRestartHelperGeneration, which disables only VS's iOS *Hot Restart* deploy helper, not Hot Reload. - A Debug Android build uses Fast Deployment, so the managed assemblies are not in the APK. `adb install` of the APK produces an app that aborts at startup; deploy with `-t:Run` instead. Reactor.Uno.slnx is kept out of the main Reactor.slnx on purpose so the Windows CI never restores Uno.Sdk, and each Uno location carries its own global.json / nuget.config / Directory.Build.props so it does not inherit the repo-root Windows + WinAppSDK build infrastructure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017rs9N4ZH4ss5tDtBwBN1Go
Separate from ci.yml on purpose: the main CI drives the Windows / Windows App SDK build and must never restore Uno.Sdk. This leg restores Uno.Sdk and builds Reactor.Uno.slnx per-TFM across all four heads — desktop and WebAssembly in one job, Android and iOS in a second because they need extra workloads. Path-gated on the port itself AND on the source it shares (src/Reactor, src/Reactor.Advanced), because a change there can break the Uno build without touching a single file under src/Reactor.Uno. That is not hypothetical: moving the charting/markdown/data-grid subsystems into Reactor.Advanced silently dropped 74 files out of the port. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017rs9N4ZH4ss5tDtBwBN1Go
…heads Source-shares ~10k lines from samples/ReactorGallery verbatim rather than forking, so the two galleries cannot drift. Of 108 gallery files, all but three compile against Uno untouched; each exception has a documented Shims/ counterpart so the shared source needs no edits at all: - GalleryActivation / GalleryProtocol — Windows App SDK AppInstance activation and Win32 HKCU protocol registration. The stand-ins report "no activation" and "not registered" honestly rather than faking success. GalleryRoutes.cs and GalleryActivationRouting.cs are pure routing and ARE shared. - DockingPage — docking is Win32-native and excluded from the port; PageRouter still routes the tag, so the stand-in explains the gap instead of 404-ing. - WebView2Page — on Uno `WebView2` resolves to a namespace that shadows Reactor's WebView2(...) factory, and a namespace-vs-method-group shadow cannot be disambiguated by an alias. A naming collision in the sample, NOT a missing Uno feature; worth fixing upstream so this page can be shared too. Builds 0 errors on desktop / wasm / android / ios, and runs on desktop: the nav tree, page routing and the control pages all render. KNOWN RENDERING GAPS (compilation is clean; these are runtime): - NavigationView stays in overlay display mode with a scrim over the content at any window width, instead of switching to Expanded as it does on Windows. - The Home page's category and "Recently Added" cards do not render. Both are unexplained and want investigation before this is used as a showcase. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017rs9N4ZH4ss5tDtBwBN1Go
There was a problem hiding this comment.
Pull request overview
Adds a new Uno Platform port (src/Reactor.Uno) so Reactor can be compiled against Uno’s Microsoft.UI.Xaml API surface and run on desktop Skia, WebAssembly, Android, and iOS, with isolation boundaries to avoid impacting the existing WinAppSDK build.
Changes:
- Introduces
Reactor.Uno(Uno.Sdk multi-targeted library) with a slim hosting/windowing layer plus WinRT shims and source-sharing drift guards. - Adds Uno sample apps (counter, showcase, gallery) and supporting per-subtree build isolation (
global.json,nuget.config,Directory.*.props). - Adds a separate
Reactor.Uno.slnxand a path-gatedci-uno.ymlworkflow to build the Uno solution across TFMs.
Reviewed changes
Copilot reviewed 63 out of 63 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Reactor/Core/Reconciler.Mount.cs | Minor nullable-analysis tweak in shared reconciler helper for Uno builds. |
| src/Reactor.Uno/WinRTShims.cs | Provides Uno-side WinRT.WindowsRuntimeTypeAttribute shim for shared source reflection probes. |
| src/Reactor.Uno/README.md | Documents the Uno port architecture, support matrix, and build/run guidance. |
| src/Reactor.Uno/Reactor.Uno.csproj | New Uno.Sdk multi-targeted port project with shared-source includes + drift/triage guards. |
| src/Reactor.Uno/reactor-uno-explainer.html | HTML explainer showing how directives/hosting differ per target. |
| src/Reactor.Uno/nuget.config | Isolated restore sources for the Uno port subtree. |
| src/Reactor.Uno/Hosting/Windowing.cs | Uno windowing shim types + DPI/closing-guard/titlebar plumbing used by shared core. |
| src/Reactor.Uno/Hosting/UnoBootstrap.cs | UnoPlatformHostBuilder bootstrap + code-only Application host. |
| src/Reactor.Uno/Hosting/ReactorHost.cs | Uno render loop host (reconcile, effects, perf stats, hot reload hooks). |
| src/Reactor.Uno/Hosting/ReactorApp.cs | Uno entrypoints (Run, RunAsync, CreateApplication) + window registry APIs. |
| src/Reactor.Uno/GlobalUsings.cs | Global aliases to disambiguate Uno type name collisions in shared source. |
| src/Reactor.Uno/global.json | Pins Uno.Sdk for the src/Reactor.Uno subtree. |
| src/Reactor.Uno/Directory.Packages.props | Disables CPM for the Uno port subtree. |
| src/Reactor.Uno/Directory.Build.props | Stops inheritance from repo-root Windows build infrastructure for Uno subtree. |
| samples/Uno/README.md | Run/build instructions for Uno samples and platform-specific entrypoints. |
| samples/Uno/ReactorUnoShowcase/ReactorUnoShowcase.csproj | Uno single-project sample showcasing controls + pickers + multi-window (desktop). |
| samples/Uno/ReactorUnoShowcase/Program.cs | Showcase entrypoint with per-platform startup differences. |
| samples/Uno/ReactorUnoShowcase/Platforms/iOS/PrivacyInfo.xcprivacy | iOS privacy manifest for the showcase sample. |
| samples/Uno/ReactorUnoShowcase/Platforms/iOS/Media.xcassets/LaunchImages.launchimage/Contents.json | iOS launch image metadata for the showcase sample. |
| samples/Uno/ReactorUnoShowcase/Platforms/iOS/Info.plist | iOS Info.plist for the showcase sample. |
| samples/Uno/ReactorUnoShowcase/Platforms/iOS/Entitlements.plist | iOS entitlements for the showcase sample. |
| samples/Uno/ReactorUnoShowcase/Platforms/Android/Resources/values/Styles.xml | Minimal Android theme setup for the showcase sample. |
| samples/Uno/ReactorUnoShowcase/Platforms/Android/Resources/values/Strings.xml | Android app name resource for the showcase sample. |
| samples/Uno/ReactorUnoShowcase/Platforms/Android/MainActivity.Android.cs | Android Activity entrypoint for the showcase sample. |
| samples/Uno/ReactorUnoShowcase/Platforms/Android/Main.Android.cs | Android NativeApplication hookup calling ReactorApp.CreateApplication. |
| samples/Uno/ReactorUnoShowcase/Platforms/Android/environment.conf | Android GC tuning for the showcase sample. |
| samples/Uno/ReactorUnoShowcase/Platforms/Android/AndroidManifest.xml | Android manifest for the showcase sample. |
| samples/Uno/ReactorUnoCounter/ReactorUnoCounter.csproj | Minimal Uno single-project counter sample. |
| samples/Uno/ReactorUnoCounter/Program.cs | Counter entrypoint (sync desktop/iOS, async wasm, Activity-driven Android). |
| samples/Uno/ReactorUnoCounter/Platforms/iOS/PrivacyInfo.xcprivacy | iOS privacy manifest for the counter sample. |
| samples/Uno/ReactorUnoCounter/Platforms/iOS/Media.xcassets/LaunchImages.launchimage/Contents.json | iOS launch image metadata for the counter sample. |
| samples/Uno/ReactorUnoCounter/Platforms/iOS/Info.plist | iOS Info.plist for the counter sample. |
| samples/Uno/ReactorUnoCounter/Platforms/iOS/Entitlements.plist | iOS entitlements for the counter sample. |
| samples/Uno/ReactorUnoCounter/Platforms/Android/Resources/values/Styles.xml | Minimal Android theme setup for the counter sample. |
| samples/Uno/ReactorUnoCounter/Platforms/Android/Resources/values/Strings.xml | Android app name resource for the counter sample. |
| samples/Uno/ReactorUnoCounter/Platforms/Android/MainActivity.Android.cs | Android Activity entrypoint for the counter sample. |
| samples/Uno/ReactorUnoCounter/Platforms/Android/Main.Android.cs | Android NativeApplication hookup calling ReactorApp.CreateApplication. |
| samples/Uno/ReactorUnoCounter/Platforms/Android/environment.conf | Android GC tuning for the counter sample. |
| samples/Uno/ReactorUnoCounter/Platforms/Android/AndroidManifest.xml | Android manifest for the counter sample. |
| samples/Uno/ReactorGalleryUno/Shims/WebView2Page.cs | Uno shim page to avoid WebView2 name collision in shared gallery code. |
| samples/Uno/ReactorGalleryUno/Shims/GalleryDeepLinkShims.cs | Uno shims for Windows-only deep-link/protocol activation types. |
| samples/Uno/ReactorGalleryUno/Shims/DockingPage.cs | Uno shim page replacing Windows-only docking page. |
| samples/Uno/ReactorGalleryUno/ReactorGalleryUno.csproj | Uno gallery head that source-shares most of samples/ReactorGallery. |
| samples/Uno/ReactorGalleryUno/Program.cs | Gallery entrypoint using ReactorApp + XamlInterop registration. |
| samples/Uno/ReactorGalleryUno/Platforms/iOS/PrivacyInfo.xcprivacy | iOS privacy manifest for the Uno gallery sample. |
| samples/Uno/ReactorGalleryUno/Platforms/iOS/Media.xcassets/LaunchImages.launchimage/Contents.json | iOS launch image metadata for the Uno gallery sample. |
| samples/Uno/ReactorGalleryUno/Platforms/iOS/Info.plist | iOS Info.plist for the Uno gallery sample. |
| samples/Uno/ReactorGalleryUno/Platforms/iOS/Entitlements.plist | iOS entitlements for the Uno gallery sample. |
| samples/Uno/ReactorGalleryUno/Platforms/Android/Resources/values/Styles.xml | Minimal Android theme setup for the Uno gallery sample. |
| samples/Uno/ReactorGalleryUno/Platforms/Android/Resources/values/Strings.xml | Android app name resource for the Uno gallery sample. |
| samples/Uno/ReactorGalleryUno/Platforms/Android/MainActivity.Android.cs | Android Activity entrypoint for the Uno gallery sample. |
| samples/Uno/ReactorGalleryUno/Platforms/Android/Main.Android.cs | Android NativeApplication hookup calling ReactorApp.CreateApplication. |
| samples/Uno/ReactorGalleryUno/Platforms/Android/environment.conf | Android GC tuning for the Uno gallery sample. |
| samples/Uno/ReactorGalleryUno/Platforms/Android/AndroidManifest.xml | Android manifest for the Uno gallery sample. |
| samples/Uno/ReactorGalleryUno/GlobalUsings.cs | Gallery-side SelectionMode alias to resolve Uno/Reactor ambiguity. |
| samples/Uno/nuget.config | Isolated restore sources for Uno samples subtree. |
| samples/Uno/global.json | Pins Uno.Sdk for Uno samples subtree. |
| samples/Uno/file-based/Counter.cs | Single-file dotnet run Counter.cs Uno-based Reactor app example. |
| samples/Uno/Directory.Packages.props | Disables CPM for Uno samples subtree. |
| samples/Uno/Directory.Build.props | Stops inheritance from repo-root Windows build infrastructure for Uno samples. |
| Reactor.Uno.slnx | Dedicated solution for Uno build isolation from main Windows solution. |
| global.json | Adds repo-root Uno.Sdk msbuild SDK pin. |
| .github/workflows/ci-uno.yml | New path-gated CI workflow to build the Uno solution across TFMs. |
Suppressed comments (3)
src/Reactor.Uno/README.md:125
- This architecture listing says
src/Reactor.Uno/global.jsonpinsUno.Sdk 6.7.0-dev.93, but the actual file in this PR pins6.7.0-dev.117. Updating this keeps the README accurate.
src/Reactor.Uno/README.md:186 - This section claims the shared
src/Reactorsource uses#if !REACTOR_UNO(and thatElements/ElementExtensions.Events.cscontains Docking event fluents), but there are noREACTOR_UNOconditionals insrc/Reactor/**in this branch. Updating the README to avoid pointing readers at a non-existent guard/file content will make the porting story clearer and match the actual source-share approach.
src/Reactor.Uno/reactor-uno-explainer.html:223 - The WebAssembly example here also hardcodes
Uno.Sdk@6.7.0-dev.93, which is out of sync with the repo’s pinned6.7.0-dev.117. Updating keeps the explainer’s copyable directives aligned with the build.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| | Desktop (Win32 / X11 / macOS / Framebuffer) | `net10.0-desktop` | ✅ Builds **and runs** — interactive | | ||
| | WebAssembly | `net10.0-browserwasm` | ✅ Builds **and runs** in the browser | | ||
| | Android | `net10.0-android` | ✅ Builds to a **signed APK**; on-device run not yet verified | | ||
| | iOS | `net10.0-ios` | ✅ **Compiles** (on Windows); device deploy needs a Mac and is not yet verified | |
| ([`samples/Uno/file-based/Counter.cs`](../../samples/Uno/file-based/Counter.cs)): | ||
|
|
||
| ```csharp | ||
| #:sdk Uno.Sdk@6.7.0-dev.93 |
| directives:[ | ||
| ['dir','#:sdk '],['','Uno.Sdk@6.7.0-dev.93'], | ||
| ['dir','\n#:project '],['','../../../src/Reactor.Uno/Reactor.Uno.csproj'], | ||
| ['dir','\n#:property '],['prop','TargetFramework'],['','=net10.0-desktop'], | ||
| ['dir','\n#:property '],['prop','OutputType'],['','=Exe'], | ||
| ['dir','\n#:property '],['prop','UnoSingleProject'],['','=true'], | ||
| ['dir','\n#:property '],['prop','UnoFeatures'],['','=SkiaRenderer'], | ||
| ['dir','\n#:property '],['prop','PublishAot'],['','=false'], | ||
| ['dir','\n#:property '],['prop','PackAsTool'],['','=false'], | ||
| ], | ||
| raw:`#:sdk Uno.Sdk@6.7.0-dev.93 | ||
| #:project ../../../src/Reactor.Uno/Reactor.Uno.csproj | ||
| #:property TargetFramework=net10.0-desktop | ||
| #:property OutputType=Exe | ||
| #:property UnoSingleProject=true | ||
| #:property UnoFeatures=SkiaRenderer | ||
| #:property PublishAot=false | ||
| #:property PackAsTool=false` |
|
Hey Martin Zikmund (@MartinZikmund). As we've discussed offline, could we move forward with this and get it into Uno itself? |
The timing is actually perfect for this, as I was looking to move something I started developing with Reactor to extend beyond Windows. Martin Zikmund (@MartinZikmund) 👀👀 |
Definitely! I will be handing over this draft to another team member and we will recreate it as a new repo (either fork or we will use git submodule). Will let you know once we have updates 👍 |
Adds Reactor.Uno, a port of the framework to Uno Platform's Skia targets, so the same Reactor C# runs on desktop (Win32/X11/macOS/FrameBuffer), WebAssembly, Android and iOS, not only Windows / Windows App SDK.
Opening as a draft: the framework port is in good shape, but the gallery sample has known rendering gaps (below) and there are open questions for maintainers about how this should ship.
Approach
This is a retargeting, not a fork. Reactor renders real
Microsoft.UI.Xamlcontrols, and Uno reimplements that API surface so the reconciler, hooks, DSL, layout engine, charting, markdown and data grid are source-shared verbatim fromsrc/Reactorandsrc/Reactor.Advancedvia<Compile Include>. Only the Windows-only hosting stack (windowing/shell/DWM/persistence/devtools) plus Docking and Win2D are excluded, replaced by a slim Uno hosting layer.Footprint on existing code is deliberately tiny. Outside the new projects, this PR touches exactly three things:
src/Reactor/Core/Reconciler.Mount.csreturn null→return defaultin a generic helper.Tis constrained to a reference type, so this is behaviour-identical on Windows; it only satisfies Uno's nullable analysis.global.jsonUno.Sdkmsbuild-sdkspin. Inert for Windows (no Windows project declaresSdk="Uno.Sdk"), but required because solution-level SDK resolution reads the repo-rootglobal.json.Reactor.Uno.slnxReactor.slnxso Windows CI never restores Uno.Sdk.There are zero
#if REACTOR_UNOconditionals in shared source. Where a Windows-only type was needed, the port declares an Uno-side counterpart instead of branching the shared code (e.g.WinRT.WindowsRuntimeTypeAttribute, which cannot exist on Uno because there is no CsWinRT projection — so the attribute probe is correctly always false).What works
net10.0-desktopnet10.0-browserwasmnet10.0-androidnet10.0-iosBeyond compiling, these are implemented for real rather than stubbed: multi-window on every desktop head (each with its own host, render loop and state), per-window DPI tracking via
XamlRoot.RasterizationScale, window closing guards viaAppWindow.Closing, file/folder pickers, and declarative caption height viaAppWindowTitleBar.PreferredHeightOption.An honest feature-support matrix — including what doesn't work on Uno (implicit animations, RichEditBox, custom title bar chrome, tray/shell, docking, devtools) — is in
src/Reactor.Uno/README.md.Hot Reload
Works, and composes with Uno's by construction rather than by coincidence. Reactor registers the standard
[assembly: MetadataUpdateHandler]; so does Uno'sClientHotReloadProcessor. They are peers on one runtime mechanism. Crucially Uno'sHotReloadAgentdiscovers handlers by scanning all loaded assemblies for the attribute and invokes what it finds — so wherever the Uno Dev Server delivers deltas instead ofdotnet watch(wasm, Android, iOS), Uno drives Reactor's re-render with no opt-in and no Reactor-side work.The Uno render loop implements the full spec 049 pass, not just a re-render: hook-cell migration for edited types, and
HookOrderExceptionrecovery (drop hook state and remount) when an edit changes a component's hook shape.Measured on Skia desktop:
🔥 C# and Razor changes applied in ~0.9s, withUseStatepreserved across the edit.dotnet watch -f <tfm>only hot-reloads a single-targeted head. Against a multi-targeted head the Roslyn workspace loads with no references and every edit dies inCS0518: Predefined type 'System.Object' is not defined. This reproduces on a stock Uno app with no Reactor involved, and IDEs are unaffected (they pick one TFM per session).Guarding source-share drift
Source-sharing's failure mode is silent: an
<Compile Include>glob that matches zero files is not an error in MSBuild. This is not hypothetical — moving charting/markdown/data-grid intoReactor.Advanceddropped 74 files out of the port with a perfectly green build, and a later new file inHosting/broke it another way.Two build-time guards now make both failure modes loud:
VerifySharedSourceRoots— errors if any globbed root stops existing.VerifyHostingTriage—Hosting/is cherry-picked file-by-file, so it diffs the folder against two declared lists and fails on anything untriaged, naming the file and the decision to make.ci-uno.ymlis path-gated onsrc/Reactor/**andsrc/Reactor.Advanced/**as well as the port itself, because a change there can break the Uno build without touching a single ported file.Building
The Windows build is unaffected —
Reactor.slnxandReactor.Advancedstill build clean.Known gaps
ReactorGalleryUno(last commit) has rendering defects. It source-shares ~10k lines fromsamples/ReactorGalleryverbatim (105 of 108 files compile untouched) and builds on all four heads, but at runtime:NavigationViewstays in overlay display mode with a scrim over the content at any window width, instead of switching to Expanded as it does on Windows.Both are pending investigation (hence draft state of the PR).
Three files needed Uno-side stand-ins, each documented in
Shims/:GalleryActivation/GalleryProtocol(WinAppSDKAppInstance+ Win32 registry),DockingPage(docking is excluded from the port for now), andWebView2Page— whereWebView2resolves to a namespace on Uno that shadows Reactor'sWebView2(...)factory. That last one is a naming collision in the sample, not a missing Uno feature, and is worth fixing upstream so the page can be shared too.Open questions for maintainers
DispatcherQueueSynchronizationContext,TitleBar.IsDragRegion*) absent from 6.5 GA, so it pins the preview6.7.0-dev.117. Switch to 6.7 GA when it ships.Microsoft.UI.Reactor.Uno, or stay sample-only for now?android+iosworkloads; happy to drop it to android-only or gate it further.🤖 Generated with Claude Code