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/include/d/d_msg_class.h b/include/d/d_msg_class.h index 2735561c05..c70345ca92 100644 --- a/include/d/d_msg_class.h +++ b/include/d/d_msg_class.h @@ -121,6 +121,25 @@ struct jmessage_tReference : public JMessage::TReference { void setDemoFrame(u32 i_frame) { mDemoFrame = i_frame; } void setTopColorType(u8 i_colorType) { mTopColorType = i_colorType; } void setNowColorType(u8 i_colorType) { mNowColorType = i_colorType; } +#if TARGET_PC + void resetColors() { + mNowCCColor = 0xFFFFFFFF; + mNowGCColor = 0xFFFFFFFF; + mTopCCColor = 0xFFFFFFFF; + mTopGCColor = 0xFFFFFFFF; + mNowFullColor = false; + mTopFullColor = false; + } + void setFullColor(u32 ccColor, u32 gcColor) { + mNowCCColor = ccColor; + mNowGCColor = gcColor; + mNowFullColor = true; + } + void clearNowFullColor() { mNowFullColor = false; } + bool hasTopFullColor() { return mTopFullColor; } + u32 getTopCCColor() { return mTopCCColor; } + u32 getTopGCColor() { return mTopGCColor; } +#endif void setTopTagScale(u16 i_tagScale) { mTopTagScale = i_tagScale; } void setNowTagScale(u16 i_tagScale) { mNowTagScale = i_tagScale; } void setRevoMessageID(u32 i_msgID) { mRevoMessageID = i_msgID; } @@ -365,6 +384,14 @@ struct jmessage_tReference : public JMessage::TReference { /* 0x1274 */ bool mSelectSetCancelFlag; /* 0x1275 */ bool mBombNameUseFlag; /* 0x1276 */ u8 mBatchColorFlag; +#if TARGET_PC + u32 mNowCCColor; + u32 mNowGCColor; + u32 mTopCCColor; + u32 mTopGCColor; + bool mNowFullColor; + bool mTopFullColor; +#endif }; // Size: 0x1278 struct jmessage_tMeasureProcessor : public JMessage::TRenderingProcessor { @@ -462,6 +489,9 @@ struct jmessage_tRenderingProcessor : public JMessage::TRenderingProcessor { void do_selwidthcenter(int); void do_heightcenter(); void do_color(u8); +#if TARGET_PC + void do_fullcolor(u32 ccColor, u32 gcColor); +#endif void do_scale(f32); void do_linedown(s16); void do_transY(s16, bool); @@ -640,6 +670,9 @@ struct jmessage_string_tRenderingProcessor : public JMessage::TRenderingProcesso void do_rubystrcat(char*); void do_outfont(u8); void do_color(u8); +#if TARGET_PC + void do_fullcolor(u32 ccColor, u32 gcColor); +#endif void do_scale(f32); void do_linedown(s16); void do_numset(s16); diff --git a/mods/flow_demo/src/mod.cpp b/mods/flow_demo/src/mod.cpp index e4d18a90f0..a05150458b 100644 --- a/mods/flow_demo/src/mod.cpp +++ b/mods/flow_demo/src/mod.cpp @@ -177,18 +177,20 @@ MOD_EXPORT ModResult mod_initialize(ModError* outError) { if (result == MOD_OK) { result = add_message(mods::flow::MessageBuilder{kResponseStyle} .text_color(MESSAGE_COLOR_DEFAULT) - .text("Hello from ") - .text_color(MESSAGE_COLOR_RED) - .text("FlowService") + .text("Custom ") + .text_color(0xFF0000FF) + .text("colors") + .text_color(MESSAGE_COLOR_DEFAULT) + .text(" and even ") + .text_color(0x00FF00FF, 0x00FFFFFF) + .text("gradients") .text_color(MESSAGE_COLOR_DEFAULT) - .text(", ") - .player_name() .text("!\n") .character_delay(3) .text("This text is slow. ") .character_delay(0) .pause(12) - .text_scale(125) + .text_scale(150) .text("Big") .text_scale(100) .text(" text too.") diff --git a/mods/randomizer/generator/utility/text.cpp b/mods/randomizer/generator/utility/text.cpp index ed72bb5654..5348b3f9e3 100644 --- a/mods/randomizer/generator/utility/text.cpp +++ b/mods/randomizer/generator/utility/text.cpp @@ -4,6 +4,8 @@ #include "yaml.hpp" #include "shiftjis_table.hpp" +#include + #include #include @@ -15,6 +17,20 @@ #endif namespace randomizer { +namespace { + +std::string text_color_code(uint32_t rgba) { + const auto message = + mods::flow::MessageBuilder{}.text_color(rgba).build(MESSAGE_LANGUAGE_ENGLISH); + const auto& bytes = message.text(); + return {reinterpret_cast(bytes.data()), bytes.size() - 1}; +} + +const auto kDarkGreenMessageCode = text_color_code(0x4BBE4BFF); +const auto kBlueMessageCode = text_color_code(0x4B96D7FF); +const auto kSilverMessageCode = text_color_code(0xBFBFBFFF); + +} // namespace Text::Text(const std::string& str) { for (auto& text : mText) { @@ -508,10 +524,9 @@ std::string UTF8ToShiftJIS(const std::string& utf8Str) { {"", "\x1A\x06\xFF\x00\x00\x04"sv}, {"", "\x1A\x06\xFF\x00\x00\x06"sv}, {"", "\x1A\x06\xFF\x00\x00\x08"sv}, - // custom colors - {"", "\x1A\x06\xFF\x00\x00\x09"sv}, - {"", "\x1A\x06\xFF\x00\x00\x0A"sv}, - {"", "\x1A\x06\xFF\x00\x00\x0B"sv}, + {"", kDarkGreenMessageCode}, + {"", kBlueMessageCode}, + {"", kSilverMessageCode}, }; void breakLines(std::string& str, float maxStrLength, int lang) { diff --git a/mods/randomizer/src/hooks.cpp b/mods/randomizer/src/hooks.cpp index 51ff55c62c..7bfd8ce12b 100644 --- a/mods/randomizer/src/hooks.cpp +++ b/mods/randomizer/src/hooks.cpp @@ -202,10 +202,6 @@ DEFINE_HOOK_SYMBOL("dComIfGs_getCollectSmell", u8(), getCollectSmell); DEFINE_HOOK(&dEvt_control_c::skipper, dEvt_control_c__skipper); -DEFINE_HOOK_SYMBOL("getFontCCColorTable", u32(u8, u8), getCCColorTable); -DEFINE_HOOK_SYMBOL("getFontGCColorTable", u32(u8, u8), getGCColorTable); - - namespace randomizer::ui { dialogSelectModeState g_dialogSelectModeState = SelectReady; } @@ -3089,28 +3085,6 @@ void hookReplaceEvtControlSkipper(ModContext*, void* args, void* retval, void*) *static_cast(retval) = doSkip; } -void hookPostGetColorTable(ModContext*, void* args, void* retval, void* userdata) { - auto& returnColor = *static_cast(retval); - if (returnColor != 0xFFFFFFFF) { - return; - } - - auto i_color = mods::arg(args, 0); - switch (i_color) { - case 9: - returnColor = 0x4BBE4BFF; // Dark Green - break; - case 10: - returnColor = 0x4B96D7FF; // Blue - break; - case 11: - returnColor = 0xBFBFBFFF; // Silver - break; - default: - break; - } -} - } ModResult initialize() { @@ -3268,9 +3242,6 @@ ModResult initialize() { ADD_HOOK_REPLACE(dEvt_control_c__skipper, hookReplaceEvtControlSkipper); - ADD_HOOK_POST(getCCColorTable, hookPostGetColorTable); - ADD_HOOK_POST(getGCColorTable, hookPostGetColorTable); - return MOD_OK; } @@ -3402,9 +3373,6 @@ ModResult uninstall() { mods::hook::uninstall(svc_hook); - mods::hook::uninstall(svc_hook); - mods::hook::uninstall(svc_hook); - return MOD_OK; } } diff --git a/sdk/include/mods/svc/flow.hpp b/sdk/include/mods/svc/flow.hpp index e0d6e5c2f1..c7671f69a6 100644 --- a/sdk/include/mods/svc/flow.hpp +++ b/sdk/include/mods/svc/flow.hpp @@ -500,7 +500,20 @@ public: } MessageBuilder& text_color(MessageTextColor color) { const auto index = static_cast(color); - return raw_tag(255, 0, {&index, 1}); + return raw_tag(0xFF, 0, {&index, 1}); + } + /* Full 32-bit text color (Dusklight extension) */ + MessageBuilder& text_color(uint32_t rgba) { + std::array arguments{}; + write_bits(arguments.data(), rgba); + return raw_tag(0xFF, 0, arguments); + } + /* Full 32-bit text vertical gradient colors (Dusklight extension) */ + MessageBuilder& text_color(uint32_t upperRgba, uint32_t lowerRgba) { + std::array arguments{}; + write_bits(arguments.data(), upperRgba); + write_bits(arguments.data() + sizeof(upperRgba), lowerRgba); + return raw_tag(0xFF, 0, arguments); } MessageBuilder& text_scale(uint16_t percent) { return timed_tag(255, 1, percent); } MessageBuilder& character_delay(uint16_t frames) { return timed_tag(0, 6, frames); } diff --git a/sdk/include/mods/svc/message.h b/sdk/include/mods/svc/message.h index 172dce4895..b170e6f09b 100644 --- a/sdk/include/mods/svc/message.h +++ b/sdk/include/mods/svc/message.h @@ -8,7 +8,7 @@ #define MESSAGE_SERVICE_ID "dev.twilitrealm.dusklight.message" #define MESSAGE_SERVICE_MAJOR 1u -#define MESSAGE_SERVICE_MINOR 0u +#define MESSAGE_SERVICE_MINOR 1u typedef uint16_t MessageId; typedef uint64_t MessageHandle; diff --git a/sdk/include/mods/svc/ui.h b/sdk/include/mods/svc/ui.h index 38d67e3192..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 4u +#define UI_SERVICE_MAJOR 2u +#define UI_SERVICE_MINOR 0u /* * UI primitives: a panel inside the host Mods window, mod-owned windows, dialogs, toasts, @@ -102,7 +102,7 @@ typedef struct UiControlDesc { /* Optional override for the modified indicator. CONFIG_VAR controls derive it from value != * default when this is NULL. */ UiPredicateFn is_modified; - /* Passed to every callback above. */ + /* Passed to every callback. */ void* user_data; /* NUMBER: inclusive clamp range and step. min == max means the defaults (0 .. INT32_MAX); step * < 1 means 1. */ @@ -120,12 +120,13 @@ 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 */ + bool color_alpha; /* COLOR: use RRGGBBAA values instead of RRGGBB */ + UiPredicateFn is_selected; /* BUTTON/GROUP: pptional selected state */ } UiControlDesc; #define UI_CONTROL_DESC_INIT \ {sizeof(UiControlDesc), UI_CONTROL_BUTTON, NULL, NULL, UI_BINDING_CALLBACKS, 0u, NULL, NULL, \ - NULL, NULL, NULL, NULL, 0, 0, 1, NULL, NULL, NULL, 0u, 0, NULL, 0u, false} + NULL, NULL, NULL, NULL, 0, 0, 1, NULL, NULL, NULL, 0u, 0, NULL, 0u, false, NULL} /* Build pane contents. A non-MOD_OK result fails the mod. */ typedef ModResult (*UiPaneBuildFn)( @@ -196,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 */ @@ -220,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; @@ -268,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). */ @@ -289,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). */ @@ -318,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/d/d_msg_class.cpp b/src/d/d_msg_class.cpp index 46b246264a..4358d6d5d2 100644 --- a/src/d/d_msg_class.cpp +++ b/src/d/d_msg_class.cpp @@ -16,6 +16,21 @@ #if TARGET_PC #include "dusk/menu_pointer.h" #include "dusk/scope_guard.hpp" +#include "helpers/bits.hpp" + +static bool read_full_color(const void* data, u32 size, u32& ccColor, u32& gcColor) { + if (size == 4) { + ccColor = dusk::read_bits(data); + gcColor = ccColor; + return true; + } + if (size == 8) { + ccColor = dusk::read_bits(data); + gcColor = dusk::read_bits(static_cast(data) + sizeof(ccColor)); + return true; + } + return false; +} #endif #if TARGET_PC @@ -400,6 +415,9 @@ jmessage_tReference::jmessage_tReference() { resetCharCountBuffer(); mNowColorType = 0; mTopColorType = 0; +#if TARGET_PC + resetColors(); +#endif mButtonTagStopFlag = 0; mPageEndCount = 0; mSelectNum = 0; @@ -594,6 +612,13 @@ void jmessage_tReference::pageSend() { if (mNowColorType != mTopColorType) { mTopColorType = mNowColorType; } +#if TARGET_PC + mTopFullColor = mNowFullColor; + if (mNowFullColor) { + mTopCCColor = mNowCCColor; + mTopGCColor = mNowGCColor; + } +#endif mTopWordCount = mNowWordCount; mCharAlpha = 0.0f; @@ -1917,6 +1942,9 @@ void jmessage_tSequenceProcessor::do_begin(void const* pEntry, char const* pszTe pReference->resetCharCnt(); pReference->setNowColorType(0); pReference->setTopColorType(0); +#if TARGET_PC + pReference->resetColors(); +#endif pReference->setNowWordCount(0); pReference->setTopWordCount(0); pReference->setBatchColorFlag(0); @@ -2280,9 +2308,21 @@ bool jmessage_tSequenceProcessor::do_tag(u32 i_tag, void const* i_data, u32 i_si return true; case MSGTAG_GROUP(255): switch (i_tag) { - case MSGTAG_COLOR: + case MSGTAG_COLOR: { +#if TARGET_PC + u32 ccColor; + u32 gcColor; + if (read_full_color(i_data, i_size, ccColor, gcColor)) { + pReference->setFullColor(ccColor, gcColor); + } else if (i_size == 1) { + pReference->clearNowFullColor(); + pReference->setNowColorType(*(u8*)i_data & 0xFF); + } +#else pReference->setNowColorType(*(u8*)i_data & 0xFF); +#endif return true; + } case MSGTAG_SCALE: pReference->setNowTagScale(*(BE(u16)*)i_data & 0xFFFF); return true; @@ -2863,7 +2903,15 @@ void jmessage_tRenderingProcessor::do_begin(void const* pEntry, char const* pszT do_scale(field_0x44); } +#if TARGET_PC + if (pReference->hasTopFullColor()) { + do_fullcolor(pReference->getTopCCColor(), pReference->getTopGCColor()); + } else { + do_color(pReference->getTopColorType()); + } +#else do_color(pReference->getTopColorType()); +#endif pReference->resetDrawLightCount(); do_widthcenter(); @@ -3017,9 +3065,20 @@ bool jmessage_tRenderingProcessor::do_tag(u32 i_tag, void const* i_data, u32 i_s return 1; case MSGTAG_GROUP(255): switch (i_tag) { - case MSGTAG_COLOR: + case MSGTAG_COLOR: { +#if TARGET_PC + u32 ccColor; + u32 gcColor; + if (read_full_color(i_data, i_size, ccColor, gcColor)) { + do_fullcolor(ccColor, gcColor); + } else if (i_size == 1) { + do_color(*(u8*)i_data & 0xFF); + } +#else do_color(*(u8*)i_data & 0xFF); +#endif return 1; + } case MSGTAG_SCALE: field_0x13c = *(BE(u16)*)i_data & 0xFFFF; do_scale(field_0x13c / 100.0f); @@ -3630,6 +3689,18 @@ void jmessage_tRenderingProcessor::do_color(u8 i_colorNo) { do_strcat(buffer, false, false, false); } +#if TARGET_PC +void jmessage_tRenderingProcessor::do_fullcolor(u32 ccColor, u32 gcColor) { + mColorNo = 0xFF; + mCCColor = ccColor; + mGCColor = gcColor; + + char buffer[40]; + SAFE_SPRINTF(buffer, "\x1B" "CC[%08x]" "\x1B" "GC[%08x]", mCCColor, mGCColor); + do_strcat(buffer, false, false, false); +} +#endif + void jmessage_tRenderingProcessor::do_scale(f32 param_1) { jmessage_tReference* pReference = (jmessage_tReference*)getReference(); @@ -4684,9 +4755,20 @@ bool jmessage_string_tRenderingProcessor::do_tag(u32 i_tag, void const* i_data, switch(i_tag & 0xFF0000) { case MSGTAG_GROUP(255): switch(i_tag) { - case MSGTAG_COLOR: + case MSGTAG_COLOR: { +#if TARGET_PC + u32 ccColor; + u32 gcColor; + if (read_full_color(i_data, i_size, ccColor, gcColor)) { + do_fullcolor(ccColor, gcColor); + } else if (i_size == 1) { + do_color(*(u8*)i_data & 0xFF); + } +#else do_color(*(u8*)i_data & 0xFF); +#endif break; + } case MSGTAG_SCALE: do_scale(*(BE(u16)*)i_data / 100.0f); break; @@ -5336,6 +5418,14 @@ void jmessage_string_tRenderingProcessor::do_color(u8 i_colorNo) { do_strcat(buffer); } +#if TARGET_PC +void jmessage_string_tRenderingProcessor::do_fullcolor(u32 ccColor, u32 gcColor) { + char buffer[32]; + SAFE_SPRINTF(buffer, "\x1b" "CC[%08x]" "\x1b" "GC[%08x]", ccColor, gcColor); + do_strcat(buffer); +} +#endif + void jmessage_string_tRenderingProcessor::do_scale(f32 i_scale) { J2DTextBox::TFontSize fontSize; mpReference->getPanePtr()->getFontSize(fontSize); diff --git a/src/dusk/mods/svc/flow.cpp b/src/dusk/mods/svc/flow.cpp index b55006c3b0..9c5af25e4c 100644 --- a/src/dusk/mods/svc/flow.cpp +++ b/src/dusk/mods/svc/flow.cpp @@ -11,6 +11,7 @@ #include "JSystem/JMessage/control.h" #include "JSystem/JMessage/processor.h" #include "JSystem/JMessage/resource.h" +#include "d/d_msg_class.h" #include #include @@ -467,11 +468,19 @@ bool valid_encoded_text(const uint8_t* text, size_t size) { if (text[offset] == 0) { return offset + 1 == size; } - if (text[offset] == 0x1a) { + if (text[offset] == JMessage::data::gcTagBegin) { if (offset + 2 > size || text[offset + 1] < 5 || text[offset + 1] > size - offset) { return false; } - offset += text[offset + 1]; + const size_t tagSize = text[offset + 1]; + const uint32_t tag = + MSGTAG_GROUP(text[offset + 2]) | read_bits(text + offset + 3); + const size_t argumentSize = tagSize - 5; + if (tag == MSGTAG_COLOR && argumentSize != 1 && argumentSize != 4 && argumentSize != 8) + { + return false; + } + offset += tagSize; } else { ++offset; } @@ -490,7 +499,7 @@ size_t encoded_text_size(const ResourceInfo& resource, const char* text) { if (bytes[offset] == 0) { return offset + 1; } - if (bytes[offset] == 0x1a) { + if (bytes[offset] == JMessage::data::gcTagBegin) { if (offset + 2 > maximum || bytes[offset + 1] < 5 || bytes[offset + 1] > maximum - offset) { 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 7296909b67..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 @@ -570,6 +581,9 @@ ModResult ui_pane_add_control( case UI_CONTROL_GROUP: spec.kind = desc.kind == UI_CONTROL_BUTTON ? ui::ModControlSpec::Kind::Button : ui::ModControlSpec::Kind::Group; + if (desc.struct_size >= sizeof(UiControlDesc)) { + spec.isSelected = wrap_predicate(mod, desc.is_selected, desc.user_data, pane); + } spec.onPressed = [modPtr = &mod, fn = desc.on_pressed, userData = desc.user_data, guardHandle = pane] { if (!slot_live(guardHandle)) { @@ -808,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; @@ -846,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; @@ -898,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) { @@ -1147,6 +1154,7 @@ bool valid_color_preset(const char* value, bool alpha) { bool valid_control_desc(const UiControlDesc& desc) { constexpr size_t kLegacyDescSize = offsetof(UiControlDesc, color_presets); + constexpr size_t kColorDescSize = offsetof(UiControlDesc, is_selected); if (desc.struct_size < kLegacyDescSize || desc.label == nullptr) { return false; } @@ -1160,7 +1168,7 @@ bool valid_control_desc(const UiControlDesc& desc) { case UI_CONTROL_SELECT: break; case UI_CONTROL_COLOR: - if (desc.struct_size < sizeof(UiControlDesc)) { + if (desc.struct_size < kColorDescSize) { return false; } break; @@ -1349,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; } @@ -1495,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); @@ -1522,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, @@ -1536,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, @@ -1553,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) { @@ -1567,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/group_button.cpp b/src/dusk/ui/group_button.cpp index 79ae8a4d32..2f9a430d68 100644 --- a/src/dusk/ui/group_button.cpp +++ b/src/dusk/ui/group_button.cpp @@ -4,15 +4,20 @@ namespace dusk::ui { GroupButton::GroupButton(Rml::Element* parent, Props props) : SelectButton{parent, {.key = std::move(props.text)}}, - mIsDisabled{std::move(props.isDisabled)} { + mIsSelected{std::move(props.isSelected)}, mIsDisabled{std::move(props.isDisabled)} { mRoot->SetClass("group-button", true); } void GroupButton::update() { + set_selected(selected()); set_disabled(disabled()); SelectButton::update(); } +bool GroupButton::selected() const { + return mIsSelected ? mIsSelected() : SelectButton::selected(); +} + bool GroupButton::disabled() const { return mIsDisabled ? mIsDisabled() : SelectButton::disabled(); } diff --git a/src/dusk/ui/group_button.hpp b/src/dusk/ui/group_button.hpp index 2287bb9004..8b207318d4 100644 --- a/src/dusk/ui/group_button.hpp +++ b/src/dusk/ui/group_button.hpp @@ -8,15 +8,18 @@ class GroupButton : public SelectButton { public: struct Props { Rml::String text; + std::function isSelected; std::function isDisabled; }; GroupButton(Rml::Element* parent, Props props); void update() override; + bool selected() const override; bool disabled() const override; private: + std::function mIsSelected; std::function mIsDisabled; }; diff --git a/src/dusk/ui/mod_window.cpp b/src/dusk/ui/mod_window.cpp index eebc1e086b..213ac29b7d 100644 --- a/src/dusk/ui/mod_window.cpp +++ b/src/dusk/ui/mod_window.cpp @@ -17,6 +17,7 @@ Component* build_mod_control(Pane& pane, Pane* helpPane, ModControlSpec spec) { case ModControlSpec::Kind::Button: control = &pane.add_button(ControlledButton::Props{ .text = s.label, + .isSelected = s.isSelected, .isDisabled = s.isDisabled, }) .on_pressed([shared] { @@ -28,6 +29,7 @@ Component* build_mod_control(Pane& pane, Pane* helpPane, ModControlSpec spec) { case ModControlSpec::Kind::Group: control = &pane.add_group_button(GroupButton::Props{ .text = s.label, + .isSelected = s.isSelected, .isDisabled = s.isDisabled, }) .on_pressed([shared] { diff --git a/src/dusk/ui/mod_window.hpp b/src/dusk/ui/mod_window.hpp index f8eade44d3..88e59dd68c 100644 --- a/src/dusk/ui/mod_window.hpp +++ b/src/dusk/ui/mod_window.hpp @@ -29,6 +29,7 @@ struct ModControlSpec { std::function setInt; std::function getString; std::function setString; + std::function isSelected; std::function isDisabled; std::function isModified; int min = 0; 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