Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- `ComputeParticlesDemo`: keeps drawing the last particle snapshot on frames where no new one has landed, so it renders on the Emscripten WebGPU backend instead of showing nothing. The status label reports the landed-snapshot count alongside the frame count
- `Component`'s effect path now reuses its offscreen `GpuCanvas` across frames while the component size is unchanged, instead of allocating (and freeing) a full-size render target every frame. On a size change the outgoing canvas is released before the replacement is created, so its `RenderContext` lease returns to the pool rather than forcing a second context to be reserved permanently
- `ComponentEffectsDemo`: shader effects now share a common base that compiles the pipeline at most once instead of retrying a failed compile on every frame, reports the compile error in the status label and on the console, and shows the CPU time spent applying the effect next to the paint time
- Added a `Toast Notifications` demo to the graphics example app demonstrating the `ToastNotification` utility: a simple `sendNotification`, a rich `ToastTemplate` (attribution, actions, scenario, duration, expiration, event callbacks), an image template, and hide/clear

#### Rive Runtime Bump

Expand Down Expand Up @@ -208,6 +209,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- UBSAN and ASAN fixes throughout the codebase
- AUv3 plugin host bypass is now connected to the processor: the wrapper-owned bypass parameter is created and drives `processBlockBypassed`, and host bypass state is persisted/restored inside the `YUPProcessorState` blob (legacy raw processor state still loads)
- Added bypass parameter handling tests for the AU, CLAP, and VST3 plugin client wrappers (routing to `processBlockBypassed`, bypass state round-trip, and text/value conversion)
- Windows toasts emit the `scenario` attribute with the spellings the toast schema declares (`reminder` / `alarm` / `incomingCall`) rather than the capitalised WinToast ones, which are not part of the enumeration. Schema conformance only — it is not the cause of the toasts that fail to display on Windows 11, see `docs/Windows Toast 80070490 Analysis.md`
- Windows toasts report a real permission state instead of always claiming `granted`: `ToastNotification::getPermissionState()` / `requestPermission()` now query `IToastNotifier::get_Setting()`, so an application, user, group policy or manifest level block is visible to the caller. The setting is also logged next to the payload. Note that it does not cover Do Not Disturb or the per-app "show notification banners" switch, which suppress the on-screen banner while still delivering the toast to the notification center
- Windows toasts no longer hand `put_ExpirationTime` a stack object that dies at the end of the enclosing `if` block. The notification retains that `IReference<DateTime>` for its whole life, so it was already dangling by the time `Show()` read it; it is now a reference-counted `ComBaseClassHelper` that the notification keeps alive

### Documentation

Expand Down
2 changes: 2 additions & 0 deletions docs/ui/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ windowing, layout, widgets, and theming are still to come.
`snapshotToTexture` for pixel capture.
- [Component paint profiling](component-profiling.md) — measure and
reduce the cost of `Component::paint`.
- [Toast notifications](toast-notifications.md) - the cross-platform `ToastNotification` utility and
its `ToastTemplate`, delivered by the platform notification backend.

## Additional Components

Expand Down
145 changes: 145 additions & 0 deletions docs/ui/toast-notifications.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# Toast Notifications

`ToastNotification` is a cross-platform utility for displaying toast-style
notifications (system popups) from a desktop or mobile app. It wraps each
platform's native notification API behind a single interface:

| Platform | Backend |
| -------- | ------- |
| Windows | Windows toast XML + AUMI/shortcut handling |
| macOS / iOS | `UserNotifications` (`UNUserNotificationCenter`) |
| Android | `NotificationManager` via JNI |
| Linux / BSD | `notify-send`, falling back to `zenity` |
| Emscripten | Browser Web Notifications API |

## Basic usage

`ToastNotification` is a singleton. Set the application name, initialize the
backend, and send a notification:

```cpp
auto& notifications = *ToastNotification::getInstance();

notifications.setAppName ("MyApp");
notifications.setAppUserModelId ("MyCompany.MyApp");

if (auto result = notifications.initialize(); result.failed())
handleError (result.getErrorMessage());

notifications.sendNotification (
"Hello!",
"This is a toast notification.",
[] (const Result& result)
{
if (result.failed())
handleError (result.getErrorMessage());
});
```

`sendNotification` is a convenience for the common title + message case. For
more control, build a `ToastTemplate` and call `showToast`:

```cpp
ToastTemplate toast;
toast.setFirstLine ("Backup complete");
toast.setSecondLine ("42 files were synced.");
toast.setAttributionText ("via MyApp");
toast.addAction ("View");
toast.setExpiration (5000); // hide after 5 seconds

toast.onActivated = [] { openViewer(); };

auto result = notifications.showToast (toast);

if (result.wasOk())
notifications.hideToast (result.getValue()); // hide it on demand
```

## ToastTemplate

`ToastTemplate` describes the content and behavior of one notification:

- **Text fields** - `setFirstLine` / `setSecondLine` / `setThirdLine`
(`setTextField`), limited by the template `TemplateType`
(`text01`..`text04`, `imageAndText01`..`imageAndText04`).
- **Attribution** - `setAttributionText`, a small line below the content.
- **Images** - `setImagePath` (with a `CropHint`) and `setHeroImagePath`.
- **Actions** - `addAction` adds a button; activation is reported through
`onActivatedWithAction (index)`.
- **Audio** - `setAudioPath (AudioSystemFile)` or a raw path/URI, plus an
`AudioOption` (`default_`, `silent`, `loop`).
- **Behavior** - `setScenario` (`default_`, `alarm`, `incomingCall`,
`reminder`), `setDuration` (`system`, `short_`, `long_`), and
`setExpiration` (milliseconds from now).

Event callbacks (`onActivated`, `onActivatedWithAction`, `onDismissed`,
`onFailed`) are `std::function`s stored in the template; the backend keeps its
own copy until the notification is gone. They may be invoked on a
platform-dependent thread - marshal back to your UI thread if needed.

## Permissions

On platforms with a user-facing notification permission (Apple, Android 13+,
and the browser on Emscripten), permission is independent of `initialize()`:
query it with `getPermissionState()` and ask for it with `requestPermission()`
at any time:

```cpp
ToastNotification::getPermissionState ([] (ToastNotification::PermissionState state)
{
// notDetermined | granted | denied
});

ToastNotification::requestPermission ([] (ToastNotification::PermissionState state)
{
if (state == ToastNotification::PermissionState::granted)
sendIt();
});
```

`showToast()` and `sendNotification()` re-check the permission before sending
and request it automatically when it hasn't been decided yet, so the simple
one-call flow keeps working. Because the user can deny or revoke permission at
any time (including from the system settings), the synchronous return value of
`showToast()` only reports that the request was accepted - the id is valid for
`hideToast()` - while the authoritative outcome arrives through the optional
completion callback:

```cpp
auto result = notifications.showToast (toast,
[] (const ResultValue<int64>& outcome)
{
if (outcome.failed())
handleError (outcome.getErrorMessage()); // e.g. "permission denied"
});

if (result.failed())
handleError (result.getErrorMessage()); // immediate failure (not initialized, ...)
```

`setPermissionStateChangedCallback()` can observe permission changes (delivered
on Apple macOS 12+ / iOS 15+ only; elsewhere treat `getPermissionState()` as
the source of truth).

## Platform notes

Not every field maps onto every backend:

- **Windows** - full parity, including the App User Model ID
(`setAppUserModelId`, `configureAUMI`) and the Start-menu shortcut policy
(`setShortcutPolicy`).
- **Apple** - everything except expiration, which has no native equivalent; the
backend removes the delivered notification after the timeout instead. Images
are copied to a temporary file before creating the attachment, because
`UNUserNotificationCenter` deletes the file at the URL it is given - the
original image is preserved.
- **Android** - action activation callbacks are not delivered (action buttons
fire broadcasts); notifications need `POST_NOTIFICATIONS` on Android 13+
(`RuntimePermissions::postNotifications`).
- **Linux / BSD** - `notify-send` must be installed (falls back to `zenity`);
`hideToast` and `clear` are no-ops, and actions are not delivered.
- **Emscripten** - permission must be granted by the user (browsers require a
user gesture); event callbacks are not delivered.

`ToastNotification::clear()` hides all displayed notifications and is also
called when the singleton is destroyed.
Loading
Loading