Add support for hero images with Windows

This commit is contained in:
Pat Hartl 2026-04-02 23:50:59 -05:00
parent 27bbbffb5b
commit e1166c474e
6 changed files with 37 additions and 9 deletions

View file

@ -237,8 +237,9 @@ NOTIFYAPI INT64 WNT_ShowToast(
if (!descriptor || !handler)
return static_cast<INT64>(WinToast::WinToastError::InvalidParameters);
bool hasBody = descriptor->body != nullptr && descriptor->body[0] != L'\0';
bool hasImage = descriptor->imagePath != nullptr && descriptor->imagePath[0] != L'\0';
bool hasBody = descriptor->body != nullptr && descriptor->body[0] != L'\0';
bool hasImage = descriptor->imagePath != nullptr && descriptor->imagePath[0] != L'\0';
bool hasHeroImage = descriptor->heroImagePath != nullptr && descriptor->heroImagePath[0] != L'\0';
WinToastTemplate tmpl(SelectTemplateType(hasImage, hasBody));
@ -249,6 +250,9 @@ NOTIFYAPI INT64 WNT_ShowToast(
if (hasImage)
tmpl.setImagePath(descriptor->imagePath);
if (hasHeroImage)
tmpl.setHeroImagePath(descriptor->heroImagePath);
for (int i = 0; i < descriptor->buttonCount; ++i)
{
if (descriptor->buttonLabels && descriptor->buttonLabels[i])

View file

@ -70,7 +70,8 @@ typedef enum _WNT_AudioOption {
typedef struct _WNT_ToastDescriptor {
const wchar_t* title; /* required */
const wchar_t* body; /* nullable */
const wchar_t* imagePath; /* nullable — absolute path to an image file */
const wchar_t* imagePath; /* nullable — absolute path; displayed as a square thumbnail */
const wchar_t* heroImagePath; /* nullable — absolute path; displayed full-width, aspect ratio preserved */
const wchar_t** buttonLabels; /* nullable — array of buttonCount wchar_t* */
int buttonCount;
long long expirationMs; /* 0 = platform default */

View file

@ -16,9 +16,15 @@ namespace Notify.NET.Abstractions
/// <summary>Optional body text shown beneath the title.</summary>
public string? Body { get; }
/// <summary>Absolute path to an image file to display in the notification.</summary>
/// <summary>Absolute path to an image file displayed as a square thumbnail.</summary>
public string? ImagePath { get; }
/// <summary>
/// Absolute path to an image file displayed full-width above the title, preserving aspect ratio.
/// Windows only — ignored on Linux and macOS.
/// </summary>
public string? HeroImagePath { get; }
/// <summary>Action buttons to display. Maximum platform limits apply (typically 5 on Windows, varies on Linux).</summary>
public IReadOnlyList<NotificationButton> Buttons { get; }
@ -38,6 +44,7 @@ namespace Notify.NET.Abstractions
string title,
string? body,
string? imagePath,
string? heroImagePath,
IReadOnlyList<NotificationButton> buttons,
INotificationHandler? handler,
TimeSpan? expiration,
@ -50,6 +57,7 @@ namespace Notify.NET.Abstractions
Title = title;
Body = body;
ImagePath = imagePath;
HeroImagePath = heroImagePath;
Buttons = buttons;
Handler = handler;
Expiration = expiration;

View file

@ -26,6 +26,7 @@ namespace Notify.NET.Builder
private string _title = string.Empty;
private string? _body;
private string? _imagePath;
private string? _heroImagePath;
private readonly List<NotificationButton> _buttons = new List<NotificationButton>();
private INotificationHandler? _handler;
private TimeSpan? _expiration;
@ -58,13 +59,23 @@ namespace Notify.NET.Builder
return this;
}
/// <summary>Sets the absolute path of an image to display in the notification.</summary>
/// <summary>Sets the absolute path of an image to display as a square thumbnail.</summary>
public NotificationBuilder WithImage(string imagePath)
{
_imagePath = imagePath;
return this;
}
/// <summary>
/// Sets the absolute path of an image to display full-width above the notification title,
/// preserving the image's aspect ratio. Windows only — ignored on Linux and macOS.
/// </summary>
public NotificationBuilder WithHeroImage(string imagePath)
{
_heroImagePath = imagePath;
return this;
}
/// <summary>Adds an action button with an optional click callback.</summary>
/// <param name="label">Text shown on the button.</param>
/// <param name="callback">Called with the notification ID when the button is clicked.</param>
@ -166,6 +177,7 @@ namespace Notify.NET.Builder
title: _title,
body: _body,
imagePath: _imagePath,
heroImagePath: _heroImagePath,
buttons: _buttons.AsReadOnly(),
handler: handler,
expiration: _expiration,

View file

@ -45,7 +45,8 @@ namespace Notify.NET.Platform.Windows
{
public IntPtr title; // wchar_t*
public IntPtr body; // wchar_t* (may be IntPtr.Zero)
public IntPtr imagePath; // wchar_t* (may be IntPtr.Zero)
public IntPtr imagePath; // wchar_t* (may be IntPtr.Zero) — square thumbnail
public IntPtr heroImagePath; // wchar_t* (may be IntPtr.Zero) — full-width, aspect ratio preserved
public IntPtr buttonLabels; // wchar_t** (array of pointers, may be IntPtr.Zero)
public int buttonCount;
public long expirationMs; // 0 = platform default

View file

@ -214,9 +214,10 @@ namespace Notify.NET.Platform.Windows
// Pin managed strings as unmanaged UTF-16 memory for the duration of the call.
// button label pointers are pinned in the IntPtr[] and that array is pinned too.
using var titlePin = new PinnedString(request.Title);
using var bodyPin = new PinnedString(request.Body);
using var imagePin = new PinnedString(ResolveImagePath(request.ImagePath));
using var titlePin = new PinnedString(request.Title);
using var bodyPin = new PinnedString(request.Body);
using var imagePin = new PinnedString(ResolveImagePath(request.ImagePath));
using var heroImagePin = new PinnedString(ResolveImagePath(request.HeroImagePath));
// Build array of pinned button label pointers.
var buttonPins = new PinnedString[request.Buttons.Count];
@ -244,6 +245,7 @@ namespace Notify.NET.Platform.Windows
title = titlePin.Pointer,
body = bodyPin.Pointer,
imagePath = imagePin.Pointer,
heroImagePath = heroImagePin.Pointer,
buttonLabels = buttonArrayPtr,
buttonCount = request.Buttons.Count,
expirationMs = request.Expiration.HasValue ? (long)request.Expiration.Value.TotalMilliseconds : 0L,