diff --git a/docs/modding.md b/docs/modding.md index 80e1998ea5..86af7ce34c 100644 --- a/docs/modding.md +++ b/docs/modding.md @@ -422,6 +422,22 @@ sets `keep_open`. A `keep_open` action can close it later (or immediately) with `on_dismiss` if present and always closes. `dialog_set_body`, `dialog_set_icon`, and `dialog_add_action` mutate a live dialog. +**Toasts:** `push_toast` enqueues a notification. Titles and bodies accept RML. The optional `type` is applied as an +RCSS class; `warning` uses the built-in warning appearance, and mods can define their own types. A duration of 0 uses +the default of 5 seconds. + +Toasts have a `mod-id` attribute, so `UI_SCOPE_OVERLAY` styles can use selectors such as +`toast[mod-id="com.example.randomizer"].success`. + +```cpp +UiToastDesc toast = UI_TOAST_DESC_INIT; +toast.type = "success"; +toast.title_rml = "Randomizer"; +toast.body_rml = "Seed loaded successfully."; +toast.duration_ms = 3000; +svc_ui->push_toast(mod_ctx, &toast); +``` + **Menu bar tabs:** `register_menu_tab` adds a tab to the in-game menu bar. `on_selected` fires when the user activates the tab: typically you'd push a window from it. The tab is removed by `unregister_menu_tab`, or automatically when the mod is disabled. diff --git a/sdk/include/mods/svc/ui.h b/sdk/include/mods/svc/ui.h index 1635a96c5f..5dd9066421 100644 --- a/sdk/include/mods/svc/ui.h +++ b/sdk/include/mods/svc/ui.h @@ -9,11 +9,11 @@ #define UI_SERVICE_ID "dev.twilitrealm.dusklight.ui" #define UI_SERVICE_MAJOR 1u -#define UI_SERVICE_MINOR 0u +#define UI_SERVICE_MINOR 1u /* - * UI primitives: a panel inside the host Mods window, mod-owned windows, dialogs, scoped - * RCSS stylesheets and menu bar tabs. + * UI primitives: a panel inside the host Mods window, mod-owned windows, dialogs, toasts, + * scoped RCSS stylesheets and menu bar tabs. * * All calls must be made on the game thread from mod callbacks (initialize, update, hooks, or UI * callbacks). Handles are opaque, generation-checked ids; a stale or unknown handle fails with @@ -212,6 +212,19 @@ typedef struct UiMenuTabDesc { #define UI_MENU_TAB_DESC_INIT {sizeof(UiMenuTabDesc), NULL, NULL, NULL} +typedef struct UiToastDesc { + uint32_t struct_size; + /* Optional RCSS class, such as "warning" or a custom mod-defined type. */ + const char* type; + /* Optional RML. At least one of title_rml or body_rml must be non-empty. */ + const char* title_rml; + const char* body_rml; + /* How long the toast remains open; 0 uses the default of 5000 ms. */ + uint32_t duration_ms; +} UiToastDesc; + +#define UI_TOAST_DESC_INIT {sizeof(UiToastDesc), NULL, NULL, NULL, 0u} + typedef struct UiService { ServiceHeader header; @@ -275,6 +288,9 @@ typedef struct UiService { ModResult (*register_menu_tab)( ModContext* ctx, const UiMenuTabDesc* desc, UiMenuTabHandle* out_tab); ModResult (*unregister_menu_tab)(ModContext* ctx, UiMenuTabHandle tab); + + /* Enqueue a toast notification. */ + ModResult (*push_toast)(ModContext* ctx, const UiToastDesc* desc); } UiService; MOD_DECLARE_SERVICE(UiService, svc_ui, UI_SERVICE_ID, UI_SERVICE_MAJOR, UI_SERVICE_MINOR); diff --git a/src/dusk/mods/svc/ui.cpp b/src/dusk/mods/svc/ui.cpp index cf4ea86808..6f0ff297fc 100644 --- a/src/dusk/mods/svc/ui.cpp +++ b/src/dusk/mods/svc/ui.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -1321,6 +1322,27 @@ ModResult ui_unregister_menu_tab(ModContext* context, UiMenuTabHandle tab) { return ui_impl::ui_unregister_menu_tab(*mod, tab); } +ModResult ui_push_toast(ModContext* context, const UiToastDesc* desc) { + auto* mod = mod_from_context(context); + if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(UiToastDesc) || + ((desc->title_rml == nullptr || desc->title_rml[0] == '\0') && + (desc->body_rml == nullptr || desc->body_rml[0] == '\0'))) + { + return MOD_INVALID_ARGUMENT; + } + + constexpr uint32_t kDefaultDurationMs = 5000; + const uint32_t durationMs = desc->duration_ms == 0 ? kDefaultDurationMs : desc->duration_ms; + ui::push_toast({ + .type = desc->type != nullptr ? desc->type : "", + .title = desc->title_rml != nullptr ? desc->title_rml : "", + .content = desc->body_rml != nullptr ? desc->body_rml : "", + .duration = std::chrono::milliseconds{durationMs}, + .modId = mod->metadata.id, + }); + return MOD_OK; +} + ModResult ui_dialog_set_body(ModContext* context, UiDialogHandle dialog, const char* bodyRml) { auto* mod = mod_from_context(context); if (mod == nullptr || dialog == 0 || bodyRml == nullptr) { @@ -1371,6 +1393,7 @@ constexpr UiService s_uiService{ .unregister_styles = ui_unregister_styles, .register_menu_tab = ui_register_menu_tab, .unregister_menu_tab = ui_unregister_menu_tab, + .push_toast = ui_push_toast, }; } // namespace diff --git a/src/dusk/ui/overlay.cpp b/src/dusk/ui/overlay.cpp index d2bd3fe6c1..d0ee19cc6a 100644 --- a/src/dusk/ui/overlay.cpp +++ b/src/dusk/ui/overlay.cpp @@ -71,6 +71,9 @@ Rml::Element* create_toast(Rml::Element* parent, const Toast& toast) { } auto* elem = append(parent, "toast"); + if (!toast.modId.empty()) { + elem->SetAttribute("mod-id", toast.modId); + } if (!toast.type.empty()) { elem->SetClass(toast.type, true); } diff --git a/src/dusk/ui/ui.hpp b/src/dusk/ui/ui.hpp index bd993ea34d..d5db990570 100644 --- a/src/dusk/ui/ui.hpp +++ b/src/dusk/ui/ui.hpp @@ -30,6 +30,7 @@ struct Toast { Rml::String title; Rml::String content; clock::duration duration; + Rml::String modId; }; // Button clicked/pressed