diff --git a/VisualC-GDK/SDL/SDL.vcxproj.filters b/VisualC-GDK/SDL/SDL.vcxproj.filters
index 9970a0834c..6843eff2ec 100644
--- a/VisualC-GDK/SDL/SDL.vcxproj.filters
+++ b/VisualC-GDK/SDL/SDL.vcxproj.filters
@@ -4,6 +4,9 @@
+
+
+
@@ -54,6 +57,7 @@
+
@@ -241,6 +245,7 @@
+
@@ -513,4 +518,4 @@
-
+
\ No newline at end of file
diff --git a/VisualC/SDL/SDL.vcxproj b/VisualC/SDL/SDL.vcxproj
index db8a11f436..643bffef8f 100644
--- a/VisualC/SDL/SDL.vcxproj
+++ b/VisualC/SDL/SDL.vcxproj
@@ -466,6 +466,7 @@
+
@@ -522,6 +523,7 @@
+
@@ -624,6 +626,7 @@
+
@@ -639,6 +642,8 @@
+
+
@@ -1000,4 +1005,4 @@
-
\ No newline at end of file
+
diff --git a/VisualC/SDL/SDL.vcxproj.filters b/VisualC/SDL/SDL.vcxproj.filters
index 0798f3d750..655fca14fd 100644
--- a/VisualC/SDL/SDL.vcxproj.filters
+++ b/VisualC/SDL/SDL.vcxproj.filters
@@ -262,6 +262,12 @@
{107d41e6-7c7e-4d9a-a3b3-b6f4abfde0c1}
+
+ {0000b1b2d12877646f6147a5a7510000}
+
+
+ {0000b6590eb19c11945581d0479b0000}
+
@@ -489,6 +495,9 @@
camera
+
+ events
+
filesystem
@@ -501,6 +510,9 @@
main
+
+ notification
+
@@ -1351,6 +1363,9 @@
dialog
+
+ events
+
filesystem
@@ -1378,6 +1393,12 @@
main\windows
+
+ notification
+
+
+ notification\windows
+
diff --git a/src/core/windows/SDL_windows.c b/src/core/windows/SDL_windows.c
index 3f51cbd5fa..4ece1bd46a 100644
--- a/src/core/windows/SDL_windows.c
+++ b/src/core/windows/SDL_windows.c
@@ -379,6 +379,11 @@ BOOL WIN_IsWindows81OrGreater(void)
CHECKWINVER(TRUE, IsWindowsVersionOrGreater(HIBYTE(_WIN32_WINNT_WINBLUE), LOBYTE(_WIN32_WINNT_WINBLUE), 0));
}
+BOOL WIN_IsWindows10OrGreater(void)
+{
+ CHECKWINVER(TRUE, IsWindowsVersionOrGreater(HIBYTE(_WIN32_WINNT_WIN10), LOBYTE(_WIN32_WINNT_WIN10), 0));
+}
+
BOOL WIN_IsWindows11OrGreater(void)
{
return IsWindowsBuildVersionAtLeast(22000);
diff --git a/src/core/windows/SDL_windows.h b/src/core/windows/SDL_windows.h
index e102f200c3..2c18c5cd1b 100644
--- a/src/core/windows/SDL_windows.h
+++ b/src/core/windows/SDL_windows.h
@@ -196,6 +196,9 @@ extern BOOL WIN_IsWindows8OrGreater(void);
// Returns true if we're running on Windows 8.1 and newer
extern BOOL WIN_IsWindows81OrGreater(void);
+// Returns true if we're running on Windows 10 and newer
+extern BOOL WIN_IsWindows10OrGreater(void);
+
// Returns true if we're running on Windows 11 and newer
extern BOOL WIN_IsWindows11OrGreater(void);
diff --git a/src/notification/windows/SDL_windowsnotification.c b/src/notification/windows/SDL_windowsnotification.c
new file mode 100644
index 0000000000..e3441bcbc0
--- /dev/null
+++ b/src/notification/windows/SDL_windowsnotification.c
@@ -0,0 +1,1161 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2026 Sam Lantinga
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+#include "SDL_internal.h"
+
+#include "../../events/SDL_notificationevents_c.h"
+#include "notification/SDL_notification_c.h"
+#include "video/SDL_surface_c.h"
+
+#include "../../core/windows/SDL_windows.h"
+#define COBJMACROS
+#include
+#include
+#include
+#include
+
+typedef HRESULT(WINAPI *RoGetActivationFactory_t)(HSTRING activatableClassId, REFIID iid, void **factory);
+typedef HRESULT(WINAPI *RoActivateInstance_t)(HSTRING activatableClassId, IInspectable **instance);
+typedef HRESULT(WINAPI *WindowsCreateString_t)(PCWSTR sourceString, UINT32 length, HSTRING *string);
+typedef HRESULT(WINAPI *WindowsCreateStringReference_t)(PCWSTR sourceString, UINT32 length, HSTRING_HEADER *hstringHeader, HSTRING *string);
+typedef HRESULT(WINAPI *WindowsDeleteString_t)(HSTRING string);
+typedef PCWSTR(WINAPI *WindowsGetStringRawBuffer_t)(HSTRING string, UINT32 *length);
+typedef NTSTATUS(WINAPI *BCryptGenRandom_t)(BCRYPT_ALG_HANDLE hAlgorithm, PUCHAR pbBuffer, ULONG cbBuffer, ULONG dwFlags);
+typedef LONG(WINAPI *GetCurrentPackageFullName_t)(UINT32 *packageFullNameLength, PWSTR packageFullName);
+
+static RoGetActivationFactory_t WIN_RoGetActivationFactory;
+static RoActivateInstance_t WIN_RoActivateInstance;
+static WindowsCreateString_t WIN_WindowsCreateString;
+static WindowsCreateStringReference_t WIN_WindowsCreateStringReference;
+static WindowsDeleteString_t WIN_WindowsDeleteString;
+static WindowsGetStringRawBuffer_t WIN_WindowsGetStringRawBuffer;
+static GetCurrentPackageFullName_t WIN_GetCurrentPackageFullName;
+
+// The registry key base needed to register the app instance so notifications can be sent.
+#define REG_KEY_BASE L"SOFTWARE\\Classes\\AppUserModelId\\"
+#define GROUP_ID_STR L"SDL_LocalNotification"
+
+// IIDs for the interfaces.
+DEFINE_GUID(IID_IToastNotificationManagerStatics,
+ 0x50ac103f, 0xd235, 0x4598, 0xbb, 0xef, 0x98, 0xfe, 0x4d, 0x1a, 0x3a, 0xd4);
+
+DEFINE_GUID(IID_IToastNotificationManagerStatics2,
+ 0x7ab93c52, 0x0e48, 0x4750, 0xba, 0x9d, 0x1a, 0x41, 0x13, 0x98, 0x18, 0x47);
+
+DEFINE_GUID(IID_IToastNotificationFactory,
+ 0x04124b20, 0x82c6, 0x4229, 0xb1, 0x09, 0xfd, 0x9e, 0xd4, 0x66, 0x2b, 0x53);
+
+DEFINE_GUID(IID_IToastNotification2,
+ 0x9dfb9fd1, 0x143a, 0x490e, 0x90, 0xbf, 0xb9, 0xfb, 0xa7, 0x13, 0x2d, 0xe7);
+
+DEFINE_GUID(IID_IToastNotification4,
+ 0x15154935, 0x28ea, 0x4727, 0x88, 0xe9, 0xc5, 0x86, 0x80, 0xe2, 0xd1, 0x18);
+
+DEFINE_GUID(IID_INotificationActivationCallback,
+ 0x53e31837, 0x6600, 0x4a81, 0x93, 0x95, 0x75, 0xcf, 0xfe, 0x74, 0x6f, 0x94);
+
+DEFINE_GUID(IID_IXmlDocument,
+ 0xf7f3a506, 0x1e87, 0x42d6, 0xbc, 0xfb, 0xb8, 0xc8, 0x09, 0xfa, 0x54, 0x94);
+
+DEFINE_GUID(IID_IXmlDocumentIO,
+ 0x6cd0e74e, 0xee65, 0x4489, 0x9e, 0xbf, 0xca, 0x43, 0xe8, 0x7b, 0xa6, 0x37);
+
+DEFINE_GUID(IID_IToastActivatedEventArgs,
+ 0xe3bf92f3, 0xc197, 0x436f, 0x82, 0x65, 0x06, 0x25, 0x82, 0x4f, 0x8d, 0xac);
+
+DEFINE_GUID(IID_IToastActivatedEventHandler,
+ 0xab54de2d, 0x97d9, 0x5528, 0xb6, 0xad, 0x10, 0x5a, 0xfe, 0x15, 0x65, 0x30);
+
+DEFINE_GUID(IID_IToastDismissedEventHandler,
+ 0x61c2402f, 0x0ed0, 0x5a18, 0xab, 0x69, 0x59, 0xf4, 0xaa, 0x99, 0xa3, 0x68);
+
+static struct Impl_IGeneric *pClassFactory = NULL;
+
+static HSTRING hsGroupId = NULL;
+static HSTRING hsAppId = NULL;
+
+static __x_ABI_CWindows_CUI_CNotifications_CIToastNotificationManagerStatics *pToastNotificationManager = NULL;
+static __x_ABI_CWindows_CUI_CNotifications_CIToastNotifier *pToastNotifier = NULL;
+static __x_ABI_CWindows_CUI_CNotifications_CIToastNotificationFactory *pNotificationFactory = NULL;
+
+static WCHAR *app_reg_key = NULL;
+static WCHAR *app_icon_path = NULL;
+
+static bool ro_initialized = false;
+static bool co_initialized = false;
+
+// IUnknown implementation
+typedef struct Impl_IGeneric
+{
+ IUnknownVtbl *lpVtbl;
+ SDL_AtomicInt refCount;
+} Impl_IGeneric;
+
+static ULONG STDMETHODCALLTYPE Impl_IGeneric_AddRef(Impl_IGeneric *_this)
+{
+ return SDL_AddAtomicInt(&_this->refCount, 1) + 1;
+}
+
+// OnActivated interface
+static HRESULT STDMETHODCALLTYPE Impl_OnActivated_QueryInterface(__FITypedEventHandler_2_Windows__CUI__CNotifications__CToastNotification_IInspectable *_this, REFIID riid, void **ppvObject)
+{
+ if (ppvObject == NULL) {
+ return E_POINTER;
+ }
+ if (IsEqualGUID(riid, &IID_IToastActivatedEventHandler) ||
+ IsEqualGUID(riid, &IID_IAgileObject) ||
+ IsEqualGUID(riid, &IID_IUnknown)) {
+ *ppvObject = _this;
+ _this->lpVtbl->AddRef(_this);
+ return S_OK;
+ }
+ return E_NOINTERFACE;
+}
+
+static HRESULT STDMETHODCALLTYPE Impl_OnActivated_Invoke(__FITypedEventHandler_2_Windows__CUI__CNotifications__CToastNotification_IInspectable *_this, __x_ABI_CWindows_CUI_CNotifications_CIToastNotification *Sender, IInspectable *Args)
+{
+ __x_ABI_CWindows_CUI_CNotifications_CIToastActivatedEventArgs *pEventArgs;
+ HRESULT hr = Args->lpVtbl->QueryInterface(Args, &IID_IToastActivatedEventArgs, (LPVOID *)&pEventArgs);
+ if (SUCCEEDED(hr)) {
+ SDL_NotificationID id = 0;
+
+ __x_ABI_CWindows_CUI_CNotifications_CIToastNotification2 *pToastNotification2;
+ hr = Sender->lpVtbl->QueryInterface(Sender, &IID_IToastNotification2, (LPVOID *)&pToastNotification2);
+ if (SUCCEEDED(hr)) {
+ HSTRING hsTag;
+ hr = pToastNotification2->lpVtbl->get_Tag(pToastNotification2, &hsTag);
+ if (SUCCEEDED(hr)) {
+ PCWSTR tag = WIN_WindowsGetStringRawBuffer(hsTag, NULL);
+ id = (SDL_NotificationID)SDL_wcstoul(tag, NULL, 10);
+ }
+ pToastNotification2->lpVtbl->Release(pToastNotification2);
+ }
+
+ if (id) {
+ HSTRING hsEventString;
+ hr = pEventArgs->lpVtbl->get_Arguments(pEventArgs, &hsEventString);
+ if (SUCCEEDED(hr)) {
+ PCWCHAR wstr = WIN_WindowsGetStringRawBuffer(hsEventString, NULL);
+ if (wstr) {
+ char tmp[512];
+ const int len = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, tmp, sizeof(tmp), NULL, NULL);
+ if (len > 0 && len <= sizeof(tmp)) {
+ SDL_SendNotificationAction(id, tmp);
+ }
+ }
+ }
+ }
+ }
+
+ return S_OK;
+}
+
+static __FITypedEventHandler_2_Windows__CUI__CNotifications__CToastNotification_IInspectableVtbl WindowsToast__OnActivatedVtbl = {
+ .QueryInterface = &Impl_OnActivated_QueryInterface,
+ .AddRef = (void *)Impl_IGeneric_AddRef,
+ .Release = (void *)Impl_IGeneric_AddRef,
+ .Invoke = &Impl_OnActivated_Invoke,
+};
+
+static __FITypedEventHandler_2_Windows__CUI__CNotifications__CToastNotification_IInspectable OnActivated = {
+ .lpVtbl = &WindowsToast__OnActivatedVtbl
+};
+
+// OnDismissed interface
+static HRESULT STDMETHODCALLTYPE Impl_OnDismissed_QueryInterface(__FITypedEventHandler_2_Windows__CUI__CNotifications__CToastNotification_Windows__CUI__CNotifications__CToastDismissedEventArgs *_this, REFIID riid, void **ppvObject)
+{
+ if (ppvObject == NULL) {
+ return E_POINTER;
+ }
+ if (IsEqualGUID(riid, &IID_IToastDismissedEventHandler) ||
+ IsEqualGUID(riid, &IID_IAgileObject) ||
+ IsEqualGUID(riid, &IID_IUnknown)) {
+ *ppvObject = _this;
+ _this->lpVtbl->AddRef(_this);
+ return S_OK;
+ }
+ return E_NOINTERFACE;
+}
+
+static HRESULT STDMETHODCALLTYPE Impl_OnDismissed_Invoke(__FITypedEventHandler_2_Windows__CUI__CNotifications__CToastNotification_Windows__CUI__CNotifications__CToastDismissedEventArgs *_this, __x_ABI_CWindows_CUI_CNotifications_CIToastNotification *Sender, __x_ABI_CWindows_CUI_CNotifications_CIToastDismissedEventArgs *Args)
+{
+ __x_ABI_CWindows_CUI_CNotifications_CToastDismissalReason Reason;
+ Args->lpVtbl->get_Reason(Args, &Reason);
+
+ /* Remove transient notifications that were cancelled or timed out,
+ * so they won't persist in the notification center.
+ */
+ switch (Reason) {
+ case ToastDismissalReason_TimedOut:
+ case ToastDismissalReason_UserCanceled:
+ pToastNotifier->lpVtbl->Hide(pToastNotifier, Sender);
+ break;
+
+ default:
+ break;
+ }
+
+ return S_OK;
+}
+
+static __FITypedEventHandler_2_Windows__CUI__CNotifications__CToastNotification_Windows__CUI__CNotifications__CToastDismissedEventArgsVtbl WindowsToast__OnDismissedVtbl = {
+ .QueryInterface = &Impl_OnDismissed_QueryInterface,
+ .AddRef = (void *)Impl_IGeneric_AddRef,
+ .Release = (void *)Impl_IGeneric_AddRef,
+ .Invoke = &Impl_OnDismissed_Invoke,
+};
+
+static __FITypedEventHandler_2_Windows__CUI__CNotifications__CToastNotification_Windows__CUI__CNotifications__CToastDismissedEventArgs OnDismissed = {
+ .lpVtbl = &WindowsToast__OnDismissedVtbl
+};
+
+static bool IsInPackage()
+{
+ if (!WIN_GetCurrentPackageFullName) {
+ HMODULE kernel32 = GetModuleHandle(TEXT("kernel32.dll"));
+ WIN_GetCurrentPackageFullName = (GetCurrentPackageFullName_t)GetProcAddress(kernel32, "GetCurrentPackageFullName");
+ }
+
+ if (WIN_GetCurrentPackageFullName) {
+ UINT32 length = 0;
+ LONG rc = WIN_GetCurrentPackageFullName(&length, NULL);
+ if (rc != ERROR_INSUFFICIENT_BUFFER) {
+ if (rc == APPMODEL_ERROR_NO_PACKAGE) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+static WCHAR *GetExePath()
+{
+ DWORD buflen = MAX_PATH;
+ WCHAR *path = NULL;
+ DWORD len = 0;
+
+ for (;;) {
+ WCHAR *ptr = SDL_realloc(path, buflen * sizeof(WCHAR));
+ if (!ptr) {
+ SDL_free(path);
+ return NULL;
+ }
+
+ path = ptr;
+
+ len = GetModuleFileNameW(NULL, path, buflen);
+ // If this was truncated, then len >= buflen - 1
+ if (len < buflen - 1) {
+ break;
+ }
+
+ // buffer too small? Try again.
+ buflen *= 2;
+ }
+
+ if (len == 0) {
+ SDL_free(path);
+ return NULL;
+ }
+
+ return path;
+}
+
+/* There is no way to pass an image as a byte stream when creating Windows
+ * notifications, so surfaces are saved as PNG files to temporary storage
+ * while the notification is being shown, then cleaned up a few seconds later.
+ */
+typedef struct ToastIcon
+{
+ struct ToastIcon *next;
+ WCHAR icon_file[1];
+} ToastIcon;
+
+static ToastIcon *toast_icons = NULL;
+static UINT_PTR cleanup_timer_id = 0;
+
+static void CleanupIcons()
+{
+ KillTimer(NULL, cleanup_timer_id);
+ cleanup_timer_id = 0;
+
+ for (ToastIcon *i = toast_icons; i; i = toast_icons) {
+ DeleteFileW(i->icon_file);
+ toast_icons = i->next;
+ SDL_free(i);
+ }
+}
+
+static void CALLBACK IconCleanupCallback(HWND hWnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime)
+{
+ CleanupIcons();
+}
+
+static WCHAR *GetFullPath(const char *path)
+{
+ const size_t conv_len = SDL_strlen(path) + 1;
+
+ WCHAR *conv_path = SDL_malloc(conv_len * sizeof(WCHAR));
+ if (!conv_path) {
+ return NULL;
+ }
+ MultiByteToWideChar(CP_UTF8, 0, path, -1, conv_path, (int)conv_len);
+
+ const DWORD full_len = GetFullPathNameW(conv_path, 0, NULL, NULL);
+ if (!full_len) {
+ SDL_free(conv_path);
+ return NULL;
+ }
+
+ WCHAR *full_path = SDL_malloc(full_len * sizeof(WCHAR));
+ if (!full_path) {
+ SDL_free(conv_path);
+ return NULL;
+ }
+ GetFullPathNameW(conv_path, full_len, full_path, NULL);
+ SDL_free(conv_path);
+
+ // Make sure the file actually exists.
+ WIN32_FILE_ATTRIBUTE_DATA attr;
+ if (GetFileAttributesExW(full_path, GetFileExInfoStandard, &attr)) {
+ return full_path;
+ }
+
+ SDL_free(full_path);
+ return NULL;
+}
+
+static const WCHAR *GetToastIconPath()
+{
+ if (app_icon_path) {
+ return app_icon_path;
+ }
+
+ const char *icon = SDL_GetStringProperty(SDL_GetGlobalProperties(), SDL_PROP_GLOBAL_NOTIFICATION_HEADER_ICON_STRING, NULL);
+ if (icon) {
+ app_icon_path = GetFullPath(icon);
+ }
+
+ return app_icon_path;
+}
+
+static WCHAR *SaveToastImage(SDL_Surface *icon)
+{
+ if (icon) {
+ // The documentation states that the buffers for these should be MAX_PATH.
+ WCHAR *temp_path = NULL;
+ size_t path_len = 0;
+
+ // Stop any timers so they won't fire in the middle of this.
+ if (cleanup_timer_id) {
+ KillTimer(NULL, cleanup_timer_id);
+ }
+
+ path_len = GetTempPath2W(0, NULL);
+ if (!path_len) {
+ return NULL;
+ }
+ temp_path = SDL_realloc(temp_path, (path_len + 1) * sizeof(WCHAR));
+ path_len = GetTempPath2W((DWORD)path_len + 1, temp_path);
+ if (!path_len) {
+ SDL_free(temp_path);
+ return NULL;
+ }
+
+ WCHAR file_name[MAX_PATH];
+ const UINT name_ret = GetTempFileNameW(temp_path, L"SDL", 0, file_name);
+ SDL_free(temp_path);
+ if (!name_ret) {
+ return NULL;
+ }
+
+ path_len += SDL_wcslen(file_name) + 5;
+
+ WCHAR *path_buf = NULL;
+ ToastIcon *toast_icon = SDL_calloc(1, sizeof(ToastIcon) + (path_len * sizeof(WCHAR)));
+ toast_icon->next = toast_icons;
+ toast_icons = toast_icon;
+ path_buf = toast_icon->icon_file;
+
+ SDL_wcslcat(path_buf, file_name, path_len);
+ SDL_wcslcat(path_buf, L".png", path_len);
+
+ const int len = WideCharToMultiByte(CP_UTF8, 0, path_buf, -1, NULL, 0, NULL, NULL);
+ char *png_path = SDL_malloc(len * sizeof(char));
+ WideCharToMultiByte(CP_UTF8, 0, path_buf, -1, png_path, len, NULL, NULL);
+ SDL_SavePNG(icon, png_path);
+ SDL_free(png_path);
+
+ // Duplicate the path, since the source object will be destroyed by a timer.
+ path_buf = SDL_wcsdup(path_buf);
+
+ // Schedule a cleanup of icons 5 seconds from now.
+ cleanup_timer_id = SetTimer(NULL, 0, 5000, IconCleanupCallback);
+
+ return path_buf;
+ }
+
+ return NULL;
+}
+
+static WCHAR *GetAppMetadata(const char *metadata_name)
+{
+ WCHAR *metadata = NULL;
+ int id_len = 0;
+
+ const char *app_metadata = SDL_GetAppMetadataProperty(metadata_name);
+ if (app_metadata && *app_metadata != '\0') {
+ id_len = MultiByteToWideChar(CP_UTF8, 0, app_metadata, -1, NULL, 0);
+ if (id_len > 0) {
+ metadata = SDL_malloc(id_len * sizeof(WCHAR));
+ if (!metadata) {
+ return NULL;
+ }
+
+ MultiByteToWideChar(CP_UTF8, 0, app_metadata, -1, metadata, id_len);
+ return metadata;
+ }
+ } else {
+ WCHAR *wszExePath = GetExePath();
+
+ for (WCHAR *c = wszExePath + SDL_wcslen(wszExePath); c >= wszExePath; --c) {
+ if (*c == L'/' || *c == L'\\') {
+ metadata = c + 1;
+ break;
+ }
+ }
+
+ if (!metadata) {
+ metadata = wszExePath;
+ }
+
+ metadata = SDL_wcsdup(metadata);
+ SDL_free(wszExePath);
+
+ return metadata;
+ }
+
+ return NULL;
+}
+
+static bool InitToastSystem()
+{
+ static bool initialized = false;
+
+ if (initialized) {
+ return true;
+ }
+
+#define RESOLVE(x) \
+ WIN_##x = (x##_t)WIN_LoadComBaseFunction(#x); \
+ if (!WIN_##x) \
+ return WIN_SetError("GetProcAddress failed for " #x)
+ RESOLVE(RoGetActivationFactory);
+ RESOLVE(RoActivateInstance);
+ RESOLVE(WindowsCreateString);
+ RESOLVE(WindowsCreateStringReference);
+ RESOLVE(WindowsGetStringRawBuffer);
+ RESOLVE(WindowsDeleteString);
+#undef RESOLVE
+
+ HSTRING_HEADER hshToastNotificationManager;
+ HSTRING hsToastNotificationManager = NULL;
+ HSTRING_HEADER hshToastNotification;
+ HSTRING hsToastNotification = NULL;
+ WCHAR *image_path = NULL;
+
+ // Initialize COM and Windows runtime with the same threading model.
+ HRESULT hr = WIN_CoInitialize();
+ if (FAILED(hr)) {
+ return false;
+ }
+ co_initialized = true;
+
+ hr = WIN_RoInitialize();
+ if (FAILED(hr)) {
+ return false;
+ }
+ ro_initialized = true;
+
+ // Get the application ID and name.
+ WCHAR *app_id = GetAppMetadata(SDL_PROP_APP_METADATA_IDENTIFIER_STRING);
+ WCHAR *app_name = GetAppMetadata(SDL_PROP_APP_METADATA_NAME_STRING);
+
+ // Create the persistent appID string.
+ hr = WIN_WindowsCreateString(app_id, (UINT32)SDL_wcslen(app_id), &hsAppId);
+ if (FAILED(hr)) {
+ goto cleanup;
+ }
+
+ // Create the persistent groupID string.
+ hr = WIN_WindowsCreateString(GROUP_ID_STR, (UINT32)SDL_wcslen(GROUP_ID_STR), &hsGroupId);
+ if (FAILED(hr)) {
+ goto cleanup;
+ }
+
+ // Build the registry key.
+ {
+ size_t reg_key_len = SDL_wcslen(REG_KEY_BASE) + SDL_wcslen(app_id) + 1;
+ app_reg_key = SDL_malloc(reg_key_len * sizeof(WCHAR));
+ if (!app_reg_key) {
+ goto cleanup;
+ }
+ SDL_swprintf(app_reg_key, reg_key_len, L"%ls%ls", REG_KEY_BASE, app_id);
+ }
+
+ // Set the app name and icon.
+ {
+ const WCHAR *icon_path = GetToastIconPath();
+ if (icon_path) {
+ hr = HRESULT_FROM_WIN32(RegSetKeyValueW(HKEY_CURRENT_USER, app_reg_key, L"IconUri", REG_SZ, icon_path, (DWORD)(SDL_wcslen(icon_path) * sizeof(WCHAR))));
+ if (FAILED(hr)) {
+ goto cleanup;
+ }
+ } else {
+ // This will "fail" if the key already doesn't exist, which is fine.
+ RegDeleteKeyValueW(HKEY_CURRENT_USER, app_reg_key, L"IconUri");
+ }
+ }
+
+ hr = HRESULT_FROM_WIN32(RegSetKeyValueW(HKEY_CURRENT_USER, app_reg_key, L"DisplayName", REG_SZ, app_name, (DWORD)(SDL_wcslen(app_name) * sizeof(WCHAR))));
+ if (FAILED(hr)) {
+ goto cleanup;
+ }
+
+ hr = WIN_WindowsCreateStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager, sizeof(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager) / sizeof(WCHAR) - 1, &hshToastNotificationManager, &hsToastNotificationManager);
+ if (FAILED(hr)) {
+ goto cleanup;
+ }
+
+ hr = WIN_RoGetActivationFactory(hsToastNotificationManager, &IID_IToastNotificationManagerStatics, (LPVOID *)&pToastNotificationManager);
+ if (FAILED(hr)) {
+ goto cleanup;
+ }
+
+ hr = pToastNotificationManager->lpVtbl->CreateToastNotifierWithId(pToastNotificationManager, hsAppId, &pToastNotifier);
+ if (FAILED(hr)) {
+ goto cleanup;
+ }
+
+ hr = WIN_WindowsCreateStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotification, (UINT32)(sizeof(RuntimeClass_Windows_UI_Notifications_ToastNotification) / sizeof(wchar_t) - 1), &hshToastNotification, &hsToastNotification);
+ if (FAILED(hr)) {
+ goto cleanup;
+ }
+
+ hr = WIN_RoGetActivationFactory(hsToastNotification, &IID_IToastNotificationFactory, (LPVOID *)&pNotificationFactory);
+ if (FAILED(hr)) {
+ goto cleanup;
+ }
+
+ initialized = true;
+
+cleanup:
+ WIN_WindowsDeleteString(hsToastNotificationManager);
+ WIN_WindowsDeleteString(hsToastNotification);
+ SDL_free(image_path);
+ SDL_free(app_id);
+ SDL_free(app_name);
+
+ return initialized;
+}
+
+static bool AppendXmlAudio(SDL_IOStream *dst, const char *sound)
+{
+ static const WCHAR *default_sound_path = L"ms-winsoundevent:Notification.Default";
+
+ WCHAR buf[512];
+ WCHAR *buf_ptr = buf;
+ WCHAR *path_format = NULL;
+ const WCHAR *silent_str = L"false";
+ const WCHAR *sound_path = default_sound_path;
+ int buf_len = SDL_arraysize(buf);
+
+ if (SDL_strcmp(sound, "silent") == 0) {
+ silent_str = L"true";
+ } else if (SDL_strcmp(sound, "default") != 0) {
+ /* Windows currently only loads custom notification sounds when the app is
+ * in an MSIX package. We'll prepend a 'file:///' prefix when not in a package
+ * in case this changes in the future, but for now, it just plays the default
+ * sound.
+ */
+ if (IsInPackage()) {
+ const WCHAR *prefix = L"ms-appx:///";
+ const size_t prefix_len = 11;
+
+ const size_t path_len = SDL_strlen(sound) + prefix_len + 1;
+ if (path_len) {
+ path_format = SDL_malloc(sizeof(WCHAR *) * path_len);
+ if (!path_format) {
+ return false;
+ }
+ SDL_wcslcpy(path_format, prefix, path_len);
+
+ if (*sound == '/') {
+ ++sound;
+ }
+ MultiByteToWideChar(CP_UTF8, 0, sound, -1, path_format + prefix_len, (int)(path_len - prefix_len));
+ sound_path = path_format;
+ }
+ } else {
+ SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Windows does not support custom notification sounds outside an MSIX package");
+
+ WCHAR *path = GetFullPath(sound);
+ if (path) {
+ const WCHAR *prefix = L"file:///";
+ const DWORD prefix_len = 8;
+
+ const size_t path_len = SDL_wcslen(path) + prefix_len + 1;
+ sound_path = path_format = SDL_malloc(sizeof(WCHAR *) * path_len);
+ if (!path_format) {
+ SDL_free(path);
+ return false;
+ }
+
+ SDL_wcslcpy(path_format, prefix, path_len);
+ SDL_wcslcat(path_format, path, path_len);
+ SDL_free(path);
+ }
+ }
+ }
+
+ for (;;) {
+ const int len = SDL_swprintf(buf_ptr, buf_len, L"