diff --git a/docs/modding.md b/docs/modding.md index e98abe0026..159311ce18 100644 --- a/docs/modding.md +++ b/docs/modding.md @@ -529,7 +529,11 @@ ModResult build_dialog(ModContext*, UiElementHandle pane, void*, ModError*) { return svc_ui->pane_add_control(mod_ctx, pane, &input, nullptr); } -UiDialogAction action = {"Save", save, nullptr, false}; +UiDialogAction action = UI_DIALOG_ACTION_INIT; +action.label = "Save"; +action.on_pressed = save; +action.is_disabled = is_save_disabled; + UiDialogDesc dialog = UI_DIALOG_DESC_INIT; dialog.title = "New Preset"; dialog.body_rml = "Choose a name for the preset."; @@ -540,8 +544,8 @@ svc_ui->dialog_push(mod_ctx, &dialog, nullptr); ``` After an action's `on_pressed`, the dialog closes unless the action sets `keep_open`. It can then be closed later -(or immediately) with `dialog_close`. Cancel fires `on_dismiss` and always closes. `dialog_set_body`, `dialog_set_icon`, -and `dialog_add_action` mutate a live dialog. +(or immediately) with `dialog_close`. Cancel fires `on_dismiss` and always closes. `dialog_set_body` and +`dialog_set_icon` 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 diff --git a/sdk/include/mods/svc/ui.h b/sdk/include/mods/svc/ui.h index e9cbc3fa65..7d48c0d241 100644 --- a/sdk/include/mods/svc/ui.h +++ b/sdk/include/mods/svc/ui.h @@ -8,8 +8,8 @@ #endif #define UI_SERVICE_ID "dev.twilitrealm.dusklight.ui" -#define UI_SERVICE_MAJOR 1u -#define UI_SERVICE_MINOR 5u +#define UI_SERVICE_MAJOR 2u +#define UI_SERVICE_MINOR 0u /* * UI primitives: a panel inside the host Mods window, mod-owned windows, dialogs, toasts, @@ -120,9 +120,8 @@ typedef struct UiControlDesc { /* COLOR: optional RRGGBB/RRGGBBAA values for presets. "rainbow" is a special value. */ const char* const* color_presets; size_t color_preset_count; - bool color_alpha; /* COLOR: use RRGGBBAA values instead of RRGGBB */ - /* Optional selected state for BUTTON/GROUP. Added in UiService minor version 5. */ - UiPredicateFn is_selected; + bool color_alpha; /* COLOR: use RRGGBBAA values instead of RRGGBB */ + UiPredicateFn is_selected; /* BUTTON/GROUP: pptional selected state */ } UiControlDesc; #define UI_CONTROL_DESC_INIT \ @@ -198,15 +197,17 @@ typedef struct UiWindowDesc { typedef void (*UiDialogActionFn)(ModContext* ctx, UiDialogHandle dialog, void* user_data); -/* Note: array element without struct_size; a future change requires appending - * a v2 desc struct rather than growing this one. */ typedef struct UiDialogAction { + uint32_t struct_size; const char* label; /* required */ UiDialogActionFn on_pressed; void* user_data; - bool keep_open; /* false = the dialog closes after on_pressed returns */ + bool keep_open; /* false = the dialog closes after on_pressed returns */ + UiPredicateFn is_disabled; /* optional; polled every frame while the dialog is visible */ } UiDialogAction; +#define UI_DIALOG_ACTION_INIT {sizeof(UiDialogAction), NULL, NULL, NULL, false, NULL} + typedef struct UiDialogDesc { uint32_t struct_size; const char* title; /* plain text; required */ @@ -222,8 +223,7 @@ typedef struct UiDialogDesc { UiDialogActionFn on_dismiss; void* user_data; /* passed to build and on_dismiss */ /* Optional content builder. The pane is rendered below body_rml and above the actions. Its - * handle and all child element handles remain valid until the dialog closes. - * Added in UiService minor version 4. */ + * handle and all child element handles remain valid until the dialog closes. */ UiPaneBuildFn build; } UiDialogDesc; @@ -270,6 +270,10 @@ typedef struct UiService { ModContext* ctx, UiElementHandle pane, float value, UiElementHandle* out_elem); ModResult (*pane_add_control)(ModContext* ctx, UiElementHandle pane, const UiControlDesc* desc, UiElementHandle* out_elem); + /* Add a group button to one pane that builds controls in its paired pane. + * The panes must be the left and right handles from the same UiTabBuildFn call. */ + ModResult (*pane_add_group)(ModContext* ctx, UiElementHandle group_pane, + UiElementHandle target_pane, const UiGroupDesc* desc, UiElementHandle* out_elem); /* Element updates. The handle kind must match the setter (text/rml on text * rows, progress on progress bars). */ @@ -291,9 +295,6 @@ typedef struct UiService { ModResult (*dialog_set_body)(ModContext* ctx, UiDialogHandle dialog, const char* body_rml); /* Replace the dialog icon ("" removes it; names as in UiDialogDesc.icon). */ ModResult (*dialog_set_icon)(ModContext* ctx, UiDialogHandle dialog, const char* icon); - /* Append one action button (same callback rules as at push). */ - ModResult (*dialog_add_action)( - ModContext* ctx, UiDialogHandle dialog, const UiDialogAction* action); /* Whether any focus-stack document is currently visible (visible documents block gamepad * input). */ @@ -320,18 +321,9 @@ typedef struct UiService { /* Enqueue a toast notification. */ ModResult (*push_toast)(ModContext* ctx, const UiToastDesc* desc); - /* Minor version 2 */ - ModResult (*get_clipboard_text)( ModContext* ctx, char* buffer, size_t bufferSize, size_t* outLength); ModResult (*set_clipboard_text)(ModContext* ctx, const char* text); - - /* Minor version 3 */ - - /* Add a group button to one pane that builds controls in its paired pane. - * The panes must be the left and right handles from the same UiTabBuildFn call. */ - ModResult (*pane_add_group)(ModContext* ctx, UiElementHandle group_pane, - UiElementHandle target_pane, const UiGroupDesc* desc, UiElementHandle* out_elem); } UiService; MOD_DECLARE_SERVICE(UiService, svc_ui, UI_SERVICE_ID, UI_SERVICE_MAJOR, UI_SERVICE_MINOR); diff --git a/src/dusk/mods/svc/registry.cpp b/src/dusk/mods/svc/registry.cpp index 68eeb58771..4f769c2dfe 100644 --- a/src/dusk/mods/svc/registry.cpp +++ b/src/dusk/mods/svc/registry.cpp @@ -214,6 +214,7 @@ void ModLoader::init_services() { &svc::g_overlayModule, &svc::g_textureModule, &svc::g_configModule, + &svc::g_uiModule_v1, &svc::g_uiModule, &svc::g_gameModule, &svc::g_cameraModule, diff --git a/src/dusk/mods/svc/registry.hpp b/src/dusk/mods/svc/registry.hpp index ba693dd958..90c3c44d84 100644 --- a/src/dusk/mods/svc/registry.hpp +++ b/src/dusk/mods/svc/registry.hpp @@ -72,6 +72,7 @@ extern const ServiceModule g_hookModule; extern const ServiceModule g_overlayModule; extern const ServiceModule g_textureModule; extern const ServiceModule g_configModule; +extern const ServiceModule g_uiModule_v1; extern const ServiceModule g_uiModule; extern const ServiceModule g_gameModule; extern const ServiceModule g_cameraModule; diff --git a/src/dusk/mods/svc/ui.cpp b/src/dusk/mods/svc/ui.cpp index 3ac625ab77..debdd26471 100644 --- a/src/dusk/mods/svc/ui.cpp +++ b/src/dusk/mods/svc/ui.cpp @@ -3,10 +3,12 @@ #include "config.hpp" #include "registry.hpp" #include "slot_map.hpp" +#include "ui_v1.hpp" #include #include "dusk/mod_loader.hpp" #include "dusk/mods/loader/loader.hpp" +#include "dusk/mods/log_buffer.hpp" #include "dusk/ui/menu_bar.hpp" #include "dusk/ui/mod_window.hpp" #include "dusk/ui/modal.hpp" @@ -440,10 +442,8 @@ void push_stacked_document(std::unique_ptr document) { } } -// Shared by dialog_push and dialog_add_action; the guard handle keeps a -// pressed callback from calling into a torn-down mod. ui::ModalAction make_dialog_action(LoadedMod& mod, uint64_t handle, const UiDialogAction& action) { - return { + ui::ModalAction result{ .label = action.label, .onPressed = [modPtr = &mod, handle, fn = action.on_pressed, userData = action.user_data, @@ -461,6 +461,17 @@ ui::ModalAction make_dialog_action(LoadedMod& mod, uint64_t handle, const UiDial } }, }; + if (action.is_disabled != nullptr) { + result.isDisabled = [modPtr = &mod, handle, fn = action.is_disabled, + userData = action.user_data] { + if (!dialog_open(handle)) { + return false; + } + return guarded_call(*modPtr, "dialog action is_disabled callback", false, + [&] { return fn(modPtr->context.get(), userData); }); + }; + } + return result; } } // namespace @@ -811,8 +822,7 @@ ModResult ui_window_close(LoadedMod& mod, uint64_t handle) { return MOD_OK; } -ModResult ui_dialog_push( - LoadedMod& mod, const UiDialogDesc& desc, UiPaneBuildFn build, uint64_t& outHandle) { +ModResult ui_dialog_push(LoadedMod& mod, const UiDialogDesc& desc, uint64_t& outHandle) { outHandle = 0; if (!aurora::rmlui::is_initialized()) { return MOD_UNAVAILABLE; @@ -849,17 +859,20 @@ ModResult ui_dialog_push( static_cast(modal).close(); } }; + auto* actionData = reinterpret_cast(desc.actions); for (size_t i = 0; i < desc.action_count; ++i) { - props.actions.push_back(make_dialog_action(mod, handle, desc.actions[i])); + const auto& action = *reinterpret_cast(actionData); + props.actions.push_back(make_dialog_action(mod, handle, action)); + actionData += action.struct_size; } auto dialog = std::make_unique( std::move(props), [handle] { on_mod_dialog_destroyed(handle); }); - if (build != nullptr) { + if (desc.build != nullptr) { auto& pane = dialog->content_pane(); const uint64_t paneHandle = wrap_pane(mod, pane, nullptr); invoke_mod_ui_callback(mod, "mod UI dialog build", [&](ModError* error) { - return build(mod.context.get(), paneHandle, desc.user_data, error); + return desc.build(mod.context.get(), paneHandle, desc.user_data, error); }); if (!mod.active) { return MOD_ERROR; @@ -901,15 +914,6 @@ ModResult ui_dialog_set_icon(LoadedMod& mod, uint64_t handle, const char* icon) return MOD_OK; } -ModResult ui_dialog_add_action(LoadedMod& mod, uint64_t handle, const UiDialogAction& action) { - auto* slot = resolve(mod, handle, UiSlotKind::Dialog, "dialog_add_action"); - if (slot == nullptr || slot->document == nullptr) { - return MOD_INVALID_ARGUMENT; - } - static_cast(slot->document)->add_action(make_dialog_action(mod, handle, action)); - return MOD_OK; -} - ModResult ui_register_menu_tab(LoadedMod& mod, const UiMenuTabDesc& desc, uint64_t& outHandle) { outHandle = 0; for (const auto& [owner, tabs] : s_modMenuTabs) { @@ -1353,21 +1357,24 @@ ModResult ui_dialog_push(ModContext* context, const UiDialogDesc* desc, UiDialog *outDialog = 0; } auto* mod = mod_from_context(context); - constexpr size_t kLegacyDescSize = offsetof(UiDialogDesc, build); - if (mod == nullptr || desc == nullptr || desc->struct_size < kLegacyDescSize || + if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(UiDialogDesc) || desc->title == nullptr || desc->body_rml == nullptr || desc->actions == nullptr || desc->action_count == 0 || desc->variant > UI_DIALOG_DANGER) { return MOD_INVALID_ARGUMENT; } + auto* actionData = reinterpret_cast(desc->actions); for (size_t i = 0; i < desc->action_count; ++i) { - if (desc->actions[i].label == nullptr) { + const auto* action = reinterpret_cast(actionData); + if (action->struct_size < sizeof(UiDialogAction) || + action->struct_size % alignof(UiDialogAction) != 0 || action->label == nullptr) + { return MOD_INVALID_ARGUMENT; } + actionData += action->struct_size; } - const UiPaneBuildFn build = desc->struct_size >= sizeof(UiDialogDesc) ? desc->build : nullptr; uint64_t handle = 0; - const auto result = ui_impl::ui_dialog_push(*mod, *desc, build, handle); + const auto result = ui_impl::ui_dialog_push(*mod, *desc, handle); if (result == MOD_OK && outDialog != nullptr) { *outDialog = handle; } @@ -1499,15 +1506,6 @@ ModResult ui_dialog_set_icon(ModContext* context, UiDialogHandle dialog, const c return ui_impl::ui_dialog_set_icon(*mod, dialog, icon); } -ModResult ui_dialog_add_action( - ModContext* context, UiDialogHandle dialog, const UiDialogAction* action) { - auto* mod = mod_from_context(context); - if (mod == nullptr || dialog == 0 || action == nullptr || action->label == nullptr) { - return MOD_INVALID_ARGUMENT; - } - return ui_impl::ui_dialog_add_action(*mod, dialog, *action); -} - ModResult ui_get_clipboard_text( ModContext* ctx, char* buffer, size_t bufferSize, size_t* outLength) { auto* mod = mod_from_context(ctx); @@ -1526,8 +1524,55 @@ ModResult ui_set_clipboard_text(ModContext* ctx, const char* text) { return ui_impl::ui_set_clipboard_text(*mod, text); } -constexpr UiService s_uiService{ - .header = SERVICE_HEADER(UiService, UI_SERVICE_MAJOR, UI_SERVICE_MINOR), +ModResult ui_v1_dialog_push( + ModContext* context, const ui_v1::UiDialogDesc* desc, UiDialogHandle* outDialog) { + if (outDialog != nullptr) { + *outDialog = 0; + } + constexpr size_t kLegacyDescSize = offsetof(ui_v1::UiDialogDesc, build); + if (desc == nullptr || desc->struct_size < kLegacyDescSize || desc->actions == nullptr || + desc->action_count == 0) + { + return MOD_INVALID_ARGUMENT; + } + + std::vector actions; + actions.reserve(desc->action_count); + for (size_t i = 0; i < desc->action_count; ++i) { + UiDialogAction action = UI_DIALOG_ACTION_INIT; + action.label = desc->actions[i].label; + action.on_pressed = desc->actions[i].on_pressed; + action.user_data = desc->actions[i].user_data; + action.keep_open = desc->actions[i].keep_open; + actions.push_back(action); + } + + UiDialogDesc translated = UI_DIALOG_DESC_INIT; + translated.title = desc->title; + translated.body_rml = desc->body_rml; + translated.variant = desc->variant; + translated.icon = desc->icon; + translated.actions = actions.data(); + translated.action_count = actions.size(); + translated.on_dismiss = desc->on_dismiss; + translated.user_data = desc->user_data; + translated.build = desc->struct_size >= sizeof(ui_v1::UiDialogDesc) ? desc->build : nullptr; + return ui_dialog_push(context, &translated, outDialog); +} + +ModResult ui_v1_dialog_add_action( + ModContext* context, UiDialogHandle, const ui_v1::UiDialogAction*) { + auto* mod = mod_from_context(context); + if (mod == nullptr) { + return MOD_INVALID_ARGUMENT; + } + log::write( + mod->metadata.id, LOG_LEVEL_WARN, "UiService v1 dialog_add_action is no longer supported"); + return MOD_UNSUPPORTED; +} + +constexpr ui_v1::UiService s_uiService_v1{ + .header = SERVICE_HEADER(ui_v1::UiService, ui_v1::kMajorVersion, ui_v1::kMinorVersion), .register_mods_panel = ui_register_mods_panel, .pane_add_section = ui_pane_add_section, .pane_add_text = ui_pane_add_text, @@ -1540,11 +1585,11 @@ constexpr UiService s_uiService{ .elem_set_class = ui_elem_set_class, .window_push = ui_window_push, .window_close = ui_window_close, - .dialog_push = ui_dialog_push, + .dialog_push = ui_v1_dialog_push, .dialog_close = ui_dialog_close, .dialog_set_body = ui_dialog_set_body, .dialog_set_icon = ui_dialog_set_icon, - .dialog_add_action = ui_dialog_add_action, + .dialog_add_action = ui_v1_dialog_add_action, .is_any_document_visible = ui_is_any_document_visible, .register_styles = ui_register_styles, .register_styles_file = ui_register_styles_file, @@ -1557,6 +1602,36 @@ constexpr UiService s_uiService{ .pane_add_group = ui_pane_add_group, }; +constexpr UiService s_uiService{ + .header = SERVICE_HEADER(UiService, UI_SERVICE_MAJOR, UI_SERVICE_MINOR), + .register_mods_panel = ui_register_mods_panel, + .pane_add_section = ui_pane_add_section, + .pane_add_text = ui_pane_add_text, + .pane_add_rml = ui_pane_add_rml, + .pane_add_progress = ui_pane_add_progress, + .pane_add_control = ui_pane_add_control, + .pane_add_group = ui_pane_add_group, + .elem_set_text = ui_elem_set_text, + .elem_set_rml = ui_elem_set_rml, + .elem_set_progress = ui_elem_set_progress, + .elem_set_class = ui_elem_set_class, + .window_push = ui_window_push, + .window_close = ui_window_close, + .dialog_push = ui_dialog_push, + .dialog_close = ui_dialog_close, + .dialog_set_body = ui_dialog_set_body, + .dialog_set_icon = ui_dialog_set_icon, + .is_any_document_visible = ui_is_any_document_visible, + .register_styles = ui_register_styles, + .register_styles_file = ui_register_styles_file, + .unregister_styles = ui_unregister_styles, + .register_menu_tab = ui_register_menu_tab, + .unregister_menu_tab = ui_unregister_menu_tab, + .push_toast = ui_push_toast, + .get_clipboard_text = ui_get_clipboard_text, + .set_clipboard_text = ui_set_clipboard_text, +}; + } // namespace void ui_build_mods_panels(LoadedMod& mod, ui::Pane& pane) { @@ -1571,6 +1646,13 @@ std::vector ui_mod_menu_tabs() { return ui_impl::ui_mod_menu_tabs(); } +constinit const ServiceModule g_uiModule_v1{ + .id = UI_SERVICE_ID, + .majorVersion = ui_v1::kMajorVersion, + .minorVersion = ui_v1::kMinorVersion, + .service = &s_uiService_v1, +}; + constinit const ServiceModule g_uiModule{ .id = UI_SERVICE_ID, .majorVersion = UI_SERVICE_MAJOR, diff --git a/src/dusk/mods/svc/ui_v1.hpp b/src/dusk/mods/svc/ui_v1.hpp new file mode 100644 index 0000000000..ef70479f8e --- /dev/null +++ b/src/dusk/mods/svc/ui_v1.hpp @@ -0,0 +1,82 @@ +#pragma once + +#include "mods/svc/ui.h" + +namespace dusk::mods::svc::ui_v1 { + +constexpr uint16_t kMajorVersion = 1; +constexpr uint16_t kMinorVersion = 5; + +struct UiDialogAction { + const char* label; + UiDialogActionFn on_pressed; + void* user_data; + bool keep_open; +}; + +struct UiDialogDesc { + uint32_t struct_size; + const char* title; + const char* body_rml; + UiDialogVariant variant; + const char* icon; + const UiDialogAction* actions; + size_t action_count; + UiDialogActionFn on_dismiss; + void* user_data; + UiPaneBuildFn build; +}; + +struct UiService { + ServiceHeader header; + + ModResult (*register_mods_panel)(ModContext* ctx, const UiModsPanelDesc* desc); + ModResult (*pane_add_section)(ModContext* ctx, UiElementHandle pane, const char* title); + ModResult (*pane_add_text)( + ModContext* ctx, UiElementHandle pane, const char* text, UiElementHandle* out_elem); + ModResult (*pane_add_rml)( + ModContext* ctx, UiElementHandle pane, const char* rml, UiElementHandle* out_elem); + ModResult (*pane_add_progress)( + ModContext* ctx, UiElementHandle pane, float value, UiElementHandle* out_elem); + ModResult (*pane_add_control)(ModContext* ctx, UiElementHandle pane, const UiControlDesc* desc, + UiElementHandle* out_elem); + + ModResult (*elem_set_text)(ModContext* ctx, UiElementHandle elem, const char* text); + ModResult (*elem_set_rml)(ModContext* ctx, UiElementHandle elem, const char* rml); + ModResult (*elem_set_progress)(ModContext* ctx, UiElementHandle elem, float value); + ModResult (*elem_set_class)( + ModContext* ctx, UiElementHandle elem, const char* name, bool active); + + ModResult (*window_push)(ModContext* ctx, const UiWindowDesc* desc, UiWindowHandle* out_window); + ModResult (*window_close)(ModContext* ctx, UiWindowHandle window); + + ModResult (*dialog_push)(ModContext* ctx, const UiDialogDesc* desc, UiDialogHandle* out_dialog); + ModResult (*dialog_close)(ModContext* ctx, UiDialogHandle dialog); + ModResult (*dialog_set_body)(ModContext* ctx, UiDialogHandle dialog, const char* body_rml); + ModResult (*dialog_set_icon)(ModContext* ctx, UiDialogHandle dialog, const char* icon); + ModResult (*dialog_add_action)( + ModContext* ctx, UiDialogHandle dialog, const UiDialogAction* action); + + ModResult (*is_any_document_visible)(ModContext* ctx, bool* out_visible); + + ModResult (*register_styles)( + ModContext* ctx, UiStyleScope scope, const char* rcss, UiStyleHandle* out_style); + ModResult (*register_styles_file)( + ModContext* ctx, UiStyleScope scope, const char* path, UiStyleHandle* out_style); + ModResult (*unregister_styles)(ModContext* ctx, UiStyleHandle style); + + ModResult (*register_menu_tab)( + ModContext* ctx, const UiMenuTabDesc* desc, UiMenuTabHandle* out_tab); + ModResult (*unregister_menu_tab)(ModContext* ctx, UiMenuTabHandle tab); + + ModResult (*push_toast)(ModContext* ctx, const UiToastDesc* desc); + + ModResult (*get_clipboard_text)( + ModContext* ctx, char* buffer, size_t bufferSize, size_t* outLength); + ModResult (*set_clipboard_text)(ModContext* ctx, const char* text); + + ModResult (*pane_add_group)(ModContext* ctx, UiElementHandle group_pane, + UiElementHandle target_pane, const UiGroupDesc* desc, UiElementHandle* out_elem); +}; + +} // namespace dusk::mods::svc::ui_v1 diff --git a/src/dusk/ui/modal.cpp b/src/dusk/ui/modal.cpp index d4d3e479dc..545a9b63b1 100644 --- a/src/dusk/ui/modal.cpp +++ b/src/dusk/ui/modal.cpp @@ -45,6 +45,9 @@ void Modal::update() { if (mContentPane != nullptr) { mContentPane->update(); } + for (const auto& button : mButtons) { + button->update(); + } if (mPendingAction) { auto action = std::move(mPendingAction); action(*this); @@ -62,7 +65,11 @@ Pane& Modal::content_pane() { void Modal::add_action(ModalAction action) { auto* actions = mDialog->QuerySelector(".modal-actions"); - auto btn = std::make_unique