Skip to content

Commit 67663fe

Browse files
committed
Add optional WinUI 3 backend for menus and message dialogs
1 parent f370b36 commit 67663fe

24 files changed

Lines changed: 1401 additions & 7 deletions

‎README.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,8 @@ These bindings provide native system API access while preserving the library's c
8080
## License
8181

8282
[MIT](./LICENSE)
83+
84+
## Optional Windows UI backend
85+
86+
Build with `NATIVEAPI_ENABLE_WINUI3=ON` to use WinUI 3 menus and message dialogs
87+
automatically. With the option OFF, Windows uses the existing Win32 backend. See [build and usage instructions](docs/winui3.md).

‎cmake/RestoreWinUI3.ps1‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
param([string]$Destination = "$PSScriptRoot/../build/winui3-packages")
2+
$ErrorActionPreference = 'Stop'
3+
$packages = @(
4+
@{ Id = 'microsoft.windowsappsdk'; Version = '1.6.250108002'; Directory = 'winappsdk'; Hash = 'C01C03EF5ADB65D9BE4769311E45A53F03749432FDDB235173824B729C304380' },
5+
@{ Id = 'microsoft.windows.cppwinrt'; Version = '2.0.240405.15'; Directory = 'cppwinrt'; Hash = 'E889007B5D9235931E7340DDF737D2C346EEBDD23C619F1F4F2426A2AAE47180' },
6+
@{ Id = 'microsoft.web.webview2'; Version = '1.0.2903.40'; Directory = 'webview2'; Hash = 'EF128016DD1E51C59178C827ED5B8AA3322C57AFA8675D930F8109505542AD74' }
7+
)
8+
New-Item -ItemType Directory -Path $Destination -Force | Out-Null
9+
$Destination = (Resolve-Path -LiteralPath $Destination).Path
10+
foreach ($package in $packages) {
11+
$archive = Join-Path $Destination "$($package.Directory).zip"
12+
if (!(Test-Path -LiteralPath $archive) -or
13+
(Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash -ne $package.Hash) {
14+
$url = "https://api.nuget.org/v3-flatcontainer/$($package.Id)/$($package.Version)/$($package.Id).$($package.Version).nupkg"
15+
Invoke-WebRequest -Uri $url -OutFile $archive
16+
}
17+
if ((Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash -ne $package.Hash) {
18+
throw "Package hash mismatch: $($package.Id)"
19+
}
20+
Expand-Archive -LiteralPath $archive -DestinationPath (Join-Path $Destination $package.Directory) -Force
21+
}
22+
Write-Output "WinUI3 build packages restored to $Destination"
23+
Write-Output 'Install Windows App Runtime 1.6 separately on machines running the modern backend.'

‎cmake/WinUI3.cmake‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Optional MSVC/C++/WinRT integration. Packages are supplied by the consumer;
2+
# configuring the default library never downloads or requires Windows App SDK.
3+
set(NATIVEAPI_WINAPPSDK_DIR "" CACHE PATH "Extracted Microsoft.WindowsAppSDK NuGet package (1.6)")
4+
set(NATIVEAPI_CPPWINRT_EXE "" CACHE FILEPATH "cppwinrt.exe from Microsoft.Windows.CppWinRT")
5+
set(NATIVEAPI_WEBVIEW2_DIR "" CACHE PATH "Extracted Microsoft.Web.WebView2 NuGet package")
6+
7+
if(NOT WIN32 OR NOT MSVC)
8+
message(FATAL_ERROR "NATIVEAPI_ENABLE_WINUI3 requires Windows and MSVC")
9+
endif()
10+
if(NOT EXISTS "${NATIVEAPI_WINAPPSDK_DIR}/include/WindowsAppSDK-VersionInfo.h" OR
11+
NOT EXISTS "${NATIVEAPI_CPPWINRT_EXE}")
12+
message(FATAL_ERROR "Set NATIVEAPI_WINAPPSDK_DIR and NATIVEAPI_CPPWINRT_EXE; see docs/winui3.md")
13+
endif()
14+
if(NOT EXISTS "${NATIVEAPI_WEBVIEW2_DIR}/lib/Microsoft.Web.WebView2.Core.winmd")
15+
message(FATAL_ERROR "Set NATIVEAPI_WEBVIEW2_DIR (WinUI metadata references WebView2); see docs/winui3.md")
16+
endif()
17+
# This package layout and hosting API have been validated with the 1.6 release.
18+
file(READ "${NATIVEAPI_WINAPPSDK_DIR}/include/WindowsAppSDK-VersionInfo.h" _version)
19+
if(NOT _version MATCHES "WINDOWSAPPSDK_RELEASE_MAJORMINOR[ \t]+0x00010006")
20+
message(FATAL_ERROR "The WinUI3 backend currently supports Windows App SDK 1.6 packages")
21+
endif()
22+
if(CMAKE_GENERATOR_PLATFORM MATCHES "ARM64")
23+
set(_arch arm64)
24+
elseif(CMAKE_SIZEOF_VOID_P EQUAL 8)
25+
set(_arch x64)
26+
else()
27+
set(_arch x86)
28+
endif()
29+
set(_projection "${CMAKE_CURRENT_BINARY_DIR}/winui3-projection")
30+
file(GLOB _metadata "${NATIVEAPI_WINAPPSDK_DIR}/lib/uap10.0/*.winmd"
31+
"${NATIVEAPI_WINAPPSDK_DIR}/lib/uap10.0.17763/*.winmd")
32+
add_custom_command(
33+
OUTPUT "${_projection}/projection.stamp"
34+
COMMAND "${CMAKE_COMMAND}" -E make_directory "${_projection}"
35+
COMMAND "${NATIVEAPI_CPPWINRT_EXE}" -input sdk
36+
-input "${NATIVEAPI_WINAPPSDK_DIR}/lib/uap10.0"
37+
-input "${NATIVEAPI_WINAPPSDK_DIR}/lib/uap10.0.17763"
38+
-input "${NATIVEAPI_WEBVIEW2_DIR}/lib/Microsoft.Web.WebView2.Core.winmd"
39+
-output "${_projection}"
40+
COMMAND "${CMAKE_COMMAND}" -E touch "${_projection}/projection.stamp"
41+
DEPENDS ${_metadata}
42+
VERBATIM)
43+
add_custom_target(nativeapi_winui3_projection DEPENDS "${_projection}/projection.stamp")
44+
add_dependencies(nativeapi nativeapi_winui3_projection)
45+
target_sources(nativeapi PRIVATE platform/windows/menu_winui3_windows.cpp
46+
platform/windows/message_dialog_winui3_windows.cpp
47+
platform/windows/winui3_runtime_windows.cpp)
48+
target_include_directories(nativeapi PRIVATE "${_projection}" "${NATIVEAPI_WINAPPSDK_DIR}/include")
49+
target_compile_definitions(nativeapi PRIVATE NATIVEAPI_ENABLE_WINUI3 NOMINMAX)
50+
target_link_libraries(nativeapi PRIVATE windowsapp)
51+
52+
# Call this for each executable using the optional backend (also for DLL consumers:
53+
# deploy the bootstrap DLL beside the executable, not beside the plugin DLL).
54+
function(nativeapi_deploy_winui3 target)
55+
if(CMAKE_GENERATOR_PLATFORM MATCHES "ARM64")
56+
set(_arch arm64)
57+
elseif(CMAKE_SIZEOF_VOID_P EQUAL 8)
58+
set(_arch x64)
59+
else()
60+
set(_arch x86)
61+
endif()
62+
add_custom_command(TARGET ${target} POST_BUILD
63+
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
64+
"${NATIVEAPI_WINAPPSDK_DIR}/runtimes/win-${_arch}/native/Microsoft.WindowsAppRuntime.Bootstrap.dll"
65+
"$<TARGET_FILE_DIR:${target}>"
66+
VERBATIM)
67+
endfunction()

‎cmake/winui3-example.manifest‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2+
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
3+
<application xmlns="urn:schemas-microsoft-com:asm.v3">
4+
<windowsSettings>
5+
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
6+
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
7+
</windowsSettings>
8+
</application>
9+
</assembly>

‎docs/winui3.md‎

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# Optional WinUI 3 backend on Windows
2+
3+
`NATIVEAPI_ENABLE_WINUI3=ON` selects WinUI 3 for new Windows menus and
4+
message dialogs. Menus use `Microsoft.UI.Xaml.Controls.MenuFlyout`; message
5+
dialogs use `Microsoft.UI.Xaml.Controls.ContentDialog`. Both share a lazy XAML
6+
runtime and use private XAML Islands, including in tray-only applications.
7+
Future WinUI components should use this same option and runtime.
8+
9+
With the option OFF (the default), menus and dialogs retain their Win32
10+
implementations. Public C++ and C interfaces stay platform independent.
11+
12+
## Build
13+
14+
The optional backend requires MSVC, a Windows 10/11 SDK, and Windows App SDK 1.6.
15+
The default build, including MinGW, has no new dependencies. This integration
16+
currently pins the 1.6 package layout; newer SDK versions require validation before
17+
changing the version guard.
18+
19+
From the repository root, in PowerShell:
20+
21+
```powershell
22+
./cmake/RestoreWinUI3.ps1
23+
$packages = (Resolve-Path build/winui3-packages).Path
24+
cmake -S . -B build/menu-modern -G "Visual Studio 17 2022" -A x64 `
25+
-DNATIVEAPI_ENABLE_WINUI3=ON `
26+
"-DNATIVEAPI_WINAPPSDK_DIR=$packages/winappsdk" `
27+
"-DNATIVEAPI_CPPWINRT_EXE=$packages/cppwinrt/bin/cppwinrt.exe" `
28+
"-DNATIVEAPI_WEBVIEW2_DIR=$packages/webview2"
29+
cmake --build build/menu-modern --config Debug --target menu_example message_dialog_example menu_backend_test winui3_dialog_test
30+
./build/menu-modern/examples/menu_example/Debug/menu_example.exe
31+
```
32+
33+
The restore script verifies pinned package hashes. It downloads build dependencies
34+
only, without installing a runtime. WebView2 metadata is required to generate WinUI
35+
projections; the menu does not instantiate WebView2 or require its browser runtime.
36+
37+
Install the matching [Windows App Runtime 1.6](https://learn.microsoft.com/en-us/windows/apps/windows-app-sdk/downloads-archive)
38+
on deployment machines. For each consuming executable, call
39+
`nativeapi_deploy_winui3(your_executable_target)` after `add_subdirectory(nativeapi)`
40+
and creating the target. This copies the bootstrap DLL beside the executable.
41+
For a DLL/plugin, deploy beside the **host executable**. The library dynamically
42+
loads the bootstrap DLL only on first WinUI component use; a missing runtime does not
43+
prevent native menus from working. An existing WinUI host's package graph and
44+
dispatcher are reused.
45+
46+
The host executable must declare **PerMonitorV2** DPI awareness in its application
47+
manifest to render crisp text at high display scaling. The bundled examples and
48+
WinUI smoke test embed `cmake/winui3-example.manifest`. Add equivalent DPI settings
49+
to your own application's manifest (a DLL manifest cannot set the host's DPI mode).
50+
The library does not change process-wide DPI settings, since that would affect
51+
other windows owned by the host. Without DPI awareness Windows bitmap-scales the
52+
finished menu, which makes text and edges blurry.
53+
54+
## C++ and C usage
55+
56+
To try the tray integration, build and run the tray example instead:
57+
58+
```powershell
59+
cmake --build build/menu-modern --config Debug --target tray_icon_example
60+
./build/menu-modern/examples/tray_icon_example/Debug/tray_icon_example.exe
61+
```
62+
63+
Right-click the tray icon whose tooltip is **nativeapi WinUI3 Tray Test - right-click**
64+
(it may be in the tray overflow). Select **Exit** to stop the example.
65+
66+
```cpp
67+
Application::GetInstance(); // Initializes COM on the UI thread.
68+
auto menu = std::make_shared<Menu>();
69+
menu->AddItem(std::make_shared<MenuItem>("Open"));
70+
menu->Open(PositioningStrategy::CursorPosition()); // WinUI3 when compiled ON.
71+
72+
MessageDialog dialog("Information", "This is a real WinUI 3 ContentDialog.");
73+
dialog.SetModality(DialogModality::Application);
74+
dialog.Open();
75+
```
76+
77+
```c
78+
native_menu_t menu = native_menu_create();
79+
/* The build option selects the default backend for the C API too. */
80+
/* Add items, register listeners, and open using the existing C API. */
81+
native_menu_free(menu);
82+
```
83+
84+
`IsBackendSupported()` reports compile-time capability. `SetBackend()` rejects
85+
unsupported backends, changes while open, and modern presentation for menus wrapping
86+
an external `HMENU`. Runtime initialization failure makes `Open()` return false
87+
and logs a diagnostic. Submenus use their root's backend.
88+
89+
## Message dialogs
90+
91+
Run `./build/menu-modern/examples/message_dialog_example/Debug/message_dialog_example.exe`.
92+
No runtime switch or new API is required.
93+
94+
- `None` returns after presentation. Keep the dialog alive and dispatch the UI
95+
message loop (normally `Application::Run()`). `Close()` dismisses it.
96+
- `Application` pumps messages until dismissal and temporarily disables the
97+
application's currently visible, enabled top-level windows.
98+
- `Window` disables the calling thread's active visible window. The public API
99+
has no explicit parent parameter yet; if no active window exists, the dialog
100+
opens without an owner. It never blocks other applications.
101+
- `SetTitle` and `SetMessage` update displayed content. Long messages scroll.
102+
The OK button, title-bar close, and `Close()` dismiss the dialog. Previously
103+
enabled windows are restored on dismissal, initialization failure, or destruction.
104+
- Create, mutate, open, close, and destroy on the same STA UI thread. Reopening
105+
an already open instance is rejected. WinUI runtime errors return false from
106+
`Open()` and are logged, with no silent fallback to Win32.
107+
- Hosts and example executables use PerMonitorV2 sizing, including monitor changes.
108+
109+
The implementation follows Microsoft's [ContentDialog hosting requirements](https://learn.microsoft.com/en-us/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.contentdialog?view=windows-app-sdk-1.6).
110+
111+
## Menu behavior and limits
112+
113+
- Call creation, opening, closing, and mutations on the same STA UI thread. Keep
114+
the root menu alive until `Open()` returns. One modern menu session can run per
115+
thread; nested root opens are rejected.
116+
- `Open()` continues pumping messages until dismissal. `Close()`, clicks outside,
117+
and normal WinUI dismissal end the session. `WM_QUIT` is preserved for the host.
118+
- Normal items, separators, checkbox/radio items, icons, tooltips, disabled items,
119+
and nested submenus use WinUI controls. Checked state remains application-owned,
120+
matching the existing example: update it in the click listener.
121+
- Labels, icons, enabled/checked state, tooltips, and shortcut text update while
122+
displayed. Structural edits (insert/remove/replace submenu) are reflected on the
123+
next root open. Root opening listeners run before constructing the item tree;
124+
submenu lifecycle events follow the child presenter's loading/unloading.
125+
- Keyboard accelerator labels are displayed. This backend does not register
126+
application/global shortcuts; use the existing shortcut API for those.
127+
- WinUI checkbox controls are two-state. Mixed checkbox state is currently shown
128+
as unchecked. Use the native backend if tri-state presentation is required.
129+
- `GetNativeObject()` still returns the library-owned `HMENU`, never a WinRT
130+
pointer. It represents the native mirror; external direct `HMENU` changes do not
131+
update the modern presentation. Do not destroy this borrowed handle.
132+
- Positioning uses the existing screen/DIP conversion followed by WinUI placement
133+
and edge avoidance. Themes and accessibility come from WinUI. Appearance can
134+
differ from File Explorer's shell menu.
135+
136+
## Verification
137+
138+
`ctest --test-dir build/menu-modern -C Debug -R menu_backend_test --output-on-failure`
139+
checks backend selection without requiring the runtime. The explicit interactive
140+
smoke test opens and closes the menu twice and checks lifecycle/reentry:
141+
142+
```powershell
143+
./build/menu-modern/tests/Debug/menu_backend_test.exe --winui3
144+
```
145+
146+
The test also exercises live property updates and cancellation from an opening
147+
listener. Add `--interactive` to keep each popup open for up to a minute in a
148+
preview window.
149+
150+
The same selection test should pass in a default build with the feature disabled.
151+
152+
The explicit desktop dialog integration test covers modal and modeless opening,
153+
reentry rejection, live updates, programmatic/title-bar close, owner restoration,
154+
destruction while open, and sharing the runtime with a menu:
155+
156+
```powershell
157+
./build/menu-modern/tests/Debug/winui3_dialog_test.exe
158+
```

‎examples/menu_example/CMakeLists.txt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ add_executable(menu_example main.cpp)
1111

1212
# Link against the nativeapi library
1313
target_link_libraries(menu_example nativeapi)
14+
if(NATIVEAPI_ENABLE_WINUI3)
15+
target_sources(menu_example PRIVATE "../../cmake/winui3-example.manifest")
16+
nativeapi_deploy_winui3(menu_example)
17+
endif()
1418

1519
# Platform-specific settings
1620
if(CMAKE_SYSTEM_NAME STREQUAL "iOS")

‎examples/menu_example/main.cpp‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
using namespace nativeapi;
88

9-
int main() {
9+
int main(int argc, char** argv) {
1010
std::cout << "=== Menu Event System Example ===" << std::endl;
1111

1212
// Get the Application instance to initialize platform
@@ -15,6 +15,11 @@ int main() {
1515
try {
1616
// Create a menu
1717
auto menu = std::make_shared<Menu>();
18+
if (argc > 1 && std::string(argv[1]) == "--winui3" &&
19+
!menu->SetBackend(MenuBackend::WinUI3)) {
20+
std::cerr << "WinUI3 menu support was not compiled in." << std::endl;
21+
return 1;
22+
}
1823
std::cout << "Created menu with ID: " << menu->GetId() << std::endl;
1924

2025
// Create menu items with different types
@@ -140,6 +145,12 @@ int main() {
140145
std::cout << "Click the Exit menu item to quit the application." << std::endl;
141146
std::cout << "========================================" << std::endl;
142147

148+
// The modern backend pumps its UI queue until dismissal. It can also be used
149+
// by tray-only applications without entering Application::Run first.
150+
if (menu->GetBackend() == MenuBackend::WinUI3) {
151+
return menu->Open(PositioningStrategy::Absolute({100, 100})) ? 0 : 1;
152+
}
153+
143154
// Set up application started listener to open menu after event loop starts
144155
app.AddListener<ApplicationStartedEvent>([menu](const ApplicationStartedEvent& event) {
145156
std::cout << "Application started - opening menu at (100, 100)" << std::endl;

‎examples/message_dialog_example/CMakeLists.txt‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,8 @@ if(APPLE)
2727
)
2828
endif()
2929

30+
31+
if(NATIVEAPI_ENABLE_WINUI3)
32+
target_sources(message_dialog_example PRIVATE "../../cmake/winui3-example.manifest")
33+
nativeapi_deploy_winui3(message_dialog_example)
34+
endif()

‎examples/tray_icon_example/CMakeLists.txt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ add_executable(tray_icon_example
1313

1414
# Link main library
1515
target_link_libraries(tray_icon_example PRIVATE nativeapi)
16+
if(NATIVEAPI_ENABLE_WINUI3)
17+
target_sources(tray_icon_example PRIVATE "../../cmake/winui3-example.manifest")
18+
nativeapi_deploy_winui3(tray_icon_example)
19+
endif()
1620

1721
# Set example program properties
1822
set_target_properties(tray_icon_example PROPERTIES

‎examples/tray_icon_example/main.cpp‎

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include <chrono>
22
#include <iostream>
33
#include <memory>
4+
#include <string>
45
#include <thread>
56

67
#include "../../src/application.h"
@@ -15,7 +16,9 @@ using nativeapi::MenuItem;
1516
using nativeapi::MenuItemClickedEvent;
1617
using nativeapi::MenuItemType;
1718

18-
int main() {
19+
int main(int argc, char** argv) {
20+
const bool use_winui3 = Menu::IsBackendSupported(MenuBackend::WinUI3) ||
21+
(argc > 1 && std::string(argv[1]) == "--winui3");
1922
std::cout << "Starting TrayIcon Example..." << std::endl;
2023

2124
// Get the Application instance - this handles platform initialization
@@ -36,8 +39,19 @@ int main() {
3639
}
3740

3841
// Set up the tray icon
39-
trayIcon->SetTitle("Test App");
40-
trayIcon->SetTooltip("This is a test tray icon");
42+
trayIcon->SetTitle(use_winui3 ? "nativeapi WinUI3 Tray Test" : "Test App");
43+
trayIcon->SetTooltip(use_winui3 ? "nativeapi WinUI3 Tray Test - right-click" : "This is a test tray icon");
44+
// Embedded blue four-pane icon keeps the example independent of asset paths.
45+
if (use_winui3) {
46+
auto icon = Image::FromBase64(
47+
"iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAOElEQVR4nO3RwQkAMAgEQcu205ST1CB5"
48+
"nI9ZuKcwYJVW1eeO9nsHAAAAALAPoPgLAAAAAADiAAV6s6MvzK1rffgAAAAASUVORK5CYII=");
49+
if (!icon) {
50+
std::cerr << "Failed to load the tray test icon." << std::endl;
51+
return 1;
52+
}
53+
trayIcon->SetIcon(icon);
54+
}
4155

4256
// Set up event listeners
4357
trayIcon->AddListener<TrayIconClickedEvent>([](const TrayIconClickedEvent& event) {
@@ -61,6 +75,18 @@ int main() {
6175

6276
// Create context menu
6377
auto context_menu = std::make_shared<Menu>();
78+
if (use_winui3 && !context_menu->SetBackend(MenuBackend::WinUI3)) {
79+
std::cerr << "WinUI3 menu support was not compiled in. Enable NATIVEAPI_ENABLE_WINUI3."
80+
<< std::endl;
81+
return 1;
82+
}
83+
std::cout << "Menu backend: " << (use_winui3 ? "WinUI3" : "Native") << std::endl;
84+
context_menu->AddListener<MenuOpenedEvent>([](const auto&) {
85+
std::cout << "Tray menu opened" << std::endl;
86+
});
87+
context_menu->AddListener<MenuClosedEvent>([](const auto&) {
88+
std::cout << "Tray menu closed" << std::endl;
89+
});
6490

6591
// Add menu items
6692
auto status_item = std::make_shared<MenuItem>("Status: Running", MenuItemType::Normal);

0 commit comments

Comments
 (0)