From 44e02a9d5384ab98a9c885c99a97766665807e66 Mon Sep 17 00:00:00 2001 From: doop <56421834+dooplecks@users.noreply.github.com> Date: Sat, 22 Aug 2026 01:28:52 -0400 Subject: [PATCH 1/3] Fix Clang builds crashing at launch (#2325) Closes #2323. --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 70cfa1de8f..f3bb07a27a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -425,7 +425,7 @@ elseif (CMAKE_CXX_COMPILER_ID MATCHES "^(AppleClang|Clang)$") if (CMAKE_SYSTEM_PROCESSOR STREQUAL "arm64" OR CMAKE_OSX_ARCHITECTURES MATCHES "arm64") set(DUSK_PATCHABLE_ENTRY_FLAG $<$:-fpatchable-function-entry=2,1>) else () - set(DUSK_PATCHABLE_ENTRY_FLAG $<$:-fpatchable-function-entry=10,5>) + set(DUSK_PATCHABLE_ENTRY_FLAG $<$:-fpatchable-function-entry=10,4>) endif () endif () From 14f9b6c7626d6bdd6f271d99231e93e8de0552ce Mon Sep 17 00:00:00 2001 From: doop <56421834+dooplecks@users.noreply.github.com> Date: Sat, 22 Aug 2026 01:29:43 -0400 Subject: [PATCH 2/3] Add setting for UI scale (#2326) --- src/dusk/settings.cpp | 4 ++++ src/dusk/settings.h | 1 + src/dusk/ui/settings.cpp | 6 ++++++ src/dusk/ui/ui.cpp | 12 ++++++++++++ src/dusk/ui/ui.hpp | 2 ++ src/m_Do/m_Do_main.cpp | 1 + 6 files changed, 26 insertions(+) diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 16bc26e532..24e4ae5305 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -1,6 +1,7 @@ #include "dusk/settings.h" #include #include "dusk/config.hpp" +#include "dusk/ui/ui.hpp" #include "dusk/game_mode.hpp" namespace dusk { @@ -16,6 +17,7 @@ UserSettings g_userSettings = { .rememberWindowSize {"video.rememberWindowSize", false}, .lastWindowWidth {"video.lastWindowWidth", 0}, .lastWindowHeight {"video.lastWindowHeight", 0}, + .uiScale {"video.uiScale", 100}, }, .audio = { @@ -228,6 +230,8 @@ void registerSettings() { Register(g_userSettings.video.rememberWindowSize); Register(g_userSettings.video.lastWindowWidth); Register(g_userSettings.video.lastWindowHeight); + Register(g_userSettings.video.uiScale, + [](const int&, const int&) { dusk::ui::apply_scale(); }); // Audio Register(g_userSettings.audio.masterVolume); diff --git a/src/dusk/settings.h b/src/dusk/settings.h index 2713812ac3..8e9a31a075 100644 --- a/src/dusk/settings.h +++ b/src/dusk/settings.h @@ -145,6 +145,7 @@ struct UserSettings { ConfigVar rememberWindowSize; ConfigVar lastWindowWidth; ConfigVar lastWindowHeight; + ConfigVar uiScale; } video; struct { diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp index 80bcf908e7..05173f79f4 100644 --- a/src/dusk/ui/settings.cpp +++ b/src/dusk/ui/settings.cpp @@ -770,6 +770,12 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { }, .isDisabled = [] { return IsMobile; }, }); + + config_int_select(leftPane, rightPane, getSettings().video.uiScale, + "UI Scale", + "Scales the Dusklight interface relative to the display's DPI scale. Has no effect on the game's UI and HUD.", + 50, 200, 25, {}, {}, "%"); + leftPane.add_section("Resolution"); graphics_tuner_control(*this, leftPane, rightPane, getSettings().game.internalResolutionScale, diff --git a/src/dusk/ui/ui.cpp b/src/dusk/ui/ui.cpp index 079f3186ba..34a13b3180 100644 --- a/src/dusk/ui/ui.cpp +++ b/src/dusk/ui/ui.cpp @@ -214,6 +214,8 @@ void handle_event(const SDL_Event& event) noexcept { }); } sConnectedGamepads.erase(event.gdevice.which); + } else if (event.type == SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED) { + apply_scale(); } input::handle_event(event); } @@ -491,4 +493,14 @@ bool consume_menu_notification_request() noexcept { return requested; } +void apply_scale() noexcept { + const auto userScale = getSettings().video.uiScale.getValue(); + auto scale = 0.0f; + if (userScale != 0) { + const auto displayScale = aurora::window::get_window_size().scale; + scale = static_cast(userScale) / 100.0f * (displayScale > 0.0f ? displayScale : 1.0f); + } + aurora::rmlui::set_ui_scale(scale); +} + } // namespace dusk::ui diff --git a/src/dusk/ui/ui.hpp b/src/dusk/ui/ui.hpp index b0af9ca5a3..27ee39f35a 100644 --- a/src/dusk/ui/ui.hpp +++ b/src/dusk/ui/ui.hpp @@ -114,4 +114,6 @@ bool consume_menu_notification_request() noexcept; const char* battery_icon(SDL_PowerState state, int level) noexcept; const char* connection_state_icon(SDL_JoystickConnectionState state) noexcept; +void apply_scale() noexcept; + } // namespace dusk::ui diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 2312deed48..3ae4af3349 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -776,6 +776,7 @@ int game_main(int argc, char* argv[]) { dusk::texture_replacements::reload(); dusk::ui::initialize(); + dusk::ui::apply_scale(); dusk::ui::push_document(std::make_unique(), true, true); dusk::ui::push_document(std::make_unique(), false, true); dusk::ui::push_document(std::make_unique(), false); From 41d5148793cf67f0e93e79cf3eab8718d6e9849d Mon Sep 17 00:00:00 2001 From: Luke Street Date: Fri, 21 Aug 2026 23:31:35 -0600 Subject: [PATCH 3/3] UiService: Dialog controls (#2332) --- docs/modding.md | 30 +++++++++++++-- res/rml/window.rcss | 80 +++++++++++++++++++++++++++++++++++++-- sdk/include/mods/svc/ui.h | 20 ++++++---- src/dusk/mods/svc/ui.cpp | 19 ++++++++-- src/dusk/ui/modal.cpp | 51 ++++++++++++++++++++++++- src/dusk/ui/modal.hpp | 10 ++++- src/dusk/ui/pane.cpp | 9 +++++ src/dusk/ui/pane.hpp | 1 + src/dusk/ui/popover.cpp | 10 +++-- src/dusk/ui/popover.hpp | 3 +- 10 files changed, 208 insertions(+), 25 deletions(-) diff --git a/docs/modding.md b/docs/modding.md index 06fcb1389a..e98abe0026 100644 --- a/docs/modding.md +++ b/docs/modding.md @@ -516,10 +516,32 @@ svc_ui->window_push(mod_ctx, &desc, &window); ``` **Dialogs:** `dialog_push` shows a modal dialog. `variant` picks the style, `icon` optionally overrides the variant's -default icon, and actions become buttons. After an action's `on_pressed` returns, the dialog closes unless the action -sets `keep_open`. A `keep_open` action can close it later (or immediately) with `dialog_close`. Cancel fires -`on_dismiss` if present and always closes. `dialog_set_body`, `dialog_set_icon`, and `dialog_add_action` mutate a live -dialog. +default icon, and actions become buttons. The optional `build` callback allows you to add controls to a pane between +the body and actions. It uses the same text, progress, and control builders as panels. + +```cpp +ModResult build_dialog(ModContext*, UiElementHandle pane, void*, ModError*) { + UiControlDesc input = UI_CONTROL_DESC_INIT; + input.kind = UI_CONTROL_STRING; + input.label = "Name"; + input.get = get_name; + input.set = set_name; + return svc_ui->pane_add_control(mod_ctx, pane, &input, nullptr); +} + +UiDialogAction action = {"Save", save, nullptr, false}; +UiDialogDesc dialog = UI_DIALOG_DESC_INIT; +dialog.title = "New Preset"; +dialog.body_rml = "Choose a name for the preset."; +dialog.actions = &action; +dialog.action_count = 1; +dialog.build = build_dialog; +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. **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/res/rml/window.rcss b/res/rml/window.rcss index a25b0895f5..364d047c1d 100644 --- a/res/rml/window.rcss +++ b/res/rml/window.rcss @@ -48,6 +48,12 @@ window.modal { max-width: 640dp; } +@media (max-height: 896dp) { + window.modal { + max-height: 100%; + } +} + window[open] { filter: opacity(1); transform: scale(1); @@ -62,6 +68,16 @@ window[open] { } } +@media (max-width: 768dp) { + body { + padding: 16dp; + } + window.modal { + width: 100%; + max-width: 100%; + } +} + window tab-bar { flex: 0 0 64dp; height: 64dp; @@ -419,14 +435,17 @@ progress { margin: 6dp 0 2dp 0; } +progress fill { + background-color: rgba(194, 164, 45, 80%); + border-radius: 3dp; +} + progress.progress-done fill { background-color: #44aa22; - border-radius: 3dp; } progress.progress-ongoing fill { background-color: #2255bb; - border-radius: 3dp; } button.achievement-clear { @@ -466,7 +485,9 @@ button.achievement-clear { padding: 24dp; gap: 20dp; flex: 0 1 auto; + min-height: 0; width: 100%; + overflow: hidden; text-align: left; } @@ -507,7 +528,7 @@ window.modal.danger .modal-header icon { .modal-body { display: block; width: 100%; - flex: 0 1 auto; + flex: 0 0 auto; min-width: 0; font-size: 20dp; color: #FFFFFF; @@ -519,6 +540,37 @@ window.modal.danger .modal-header icon { color: #92875B; } +.modal-content { + display: none; + width: 100%; + flex: 1 1 auto; + min-height: 0; + overflow: hidden; +} + +.modal-content.active { + display: flex; + flex-direction: column; +} + +.modal-content pane { + display: flex; + flex: 1 1 auto; + flex-direction: column; + min-height: 0; + width: 100%; + gap: 8dp; + overflow: hidden auto; +} + +.modal-content pane > * { + flex: 0 0 auto; +} + +.modal-content pane > spacer { + display: none; +} + .verification-progress { display: flex; flex-direction: column; @@ -563,3 +615,25 @@ progress.verification-progress-bar { flex: 0 0 auto; width: 100%; } + +@media (max-height: 640dp) { + .modal-dialog { + padding: 16dp; + gap: 12dp; + } + + .modal-body { + font-size: 17dp; + } +} + +@media (max-width: 640dp) { + .modal-actions { + flex-direction: column; + } + + .modal-actions button.modal-btn { + flex: 0 0 auto; + width: 100%; + } +} diff --git a/sdk/include/mods/svc/ui.h b/sdk/include/mods/svc/ui.h index 397231e833..38d67e3192 100644 --- a/sdk/include/mods/svc/ui.h +++ b/sdk/include/mods/svc/ui.h @@ -9,7 +9,7 @@ #define UI_SERVICE_ID "dev.twilitrealm.dusklight.ui" #define UI_SERVICE_MAJOR 1u -#define UI_SERVICE_MINOR 3u +#define UI_SERVICE_MINOR 4u /* * UI primitives: a panel inside the host Mods window, mod-owned windows, dialogs, toasts, @@ -127,11 +127,14 @@ typedef struct UiControlDesc { {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} +/* Build pane contents. A non-MOD_OK result fails the mod. */ +typedef ModResult (*UiPaneBuildFn)( + ModContext* ctx, UiElementHandle pane, void* user_data, ModError* out_error); + /* Build the panel contents. `panel` accepts the pane_add_* functions; it and * every element created in it are destroyed (handles invalidated) whenever the * panel is rebuilt, e.g. on tab switches. A non-MOD_OK result fails the mod. */ -typedef ModResult (*UiPanelBuildFn)( - ModContext* ctx, UiElementHandle panel, void* user_data, ModError* out_error); +typedef UiPaneBuildFn UiPanelBuildFn; /* Called every frame while the panel is the visible tab. */ typedef ModResult (*UiPanelUpdateFn)(ModContext* ctx, void* user_data, ModError* out_error); @@ -147,8 +150,7 @@ typedef struct UiModsPanelDesc { /* Builds the contents associated with a group button. The target pane is cleared immediately * before this callback and is valid only while its tab remains built. */ -typedef ModResult (*UiGroupBuildFn)( - ModContext* ctx, UiElementHandle target_pane, void* user_data, ModError* out_error); +typedef UiPaneBuildFn UiGroupBuildFn; typedef struct UiGroupDesc { uint32_t struct_size; @@ -216,11 +218,15 @@ typedef struct UiDialogDesc { /* Fired on cancel (B/Escape) before the dialog closes; the dialog always * closes on dismiss. */ UiDialogActionFn on_dismiss; - void* user_data; /* passed to 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. */ + UiPaneBuildFn build; } UiDialogDesc; #define UI_DIALOG_DESC_INIT \ - {sizeof(UiDialogDesc), NULL, NULL, UI_DIALOG_NORMAL, NULL, NULL, 0u, NULL, NULL} + {sizeof(UiDialogDesc), NULL, NULL, UI_DIALOG_NORMAL, NULL, NULL, 0u, NULL, NULL, NULL} /* A tab added to the in-game menu bar. */ typedef struct UiMenuTabDesc { diff --git a/src/dusk/mods/svc/ui.cpp b/src/dusk/mods/svc/ui.cpp index 49b10bd0fc..7296909b67 100644 --- a/src/dusk/mods/svc/ui.cpp +++ b/src/dusk/mods/svc/ui.cpp @@ -808,7 +808,8 @@ ModResult ui_window_close(LoadedMod& mod, uint64_t handle) { return MOD_OK; } -ModResult ui_dialog_push(LoadedMod& mod, const UiDialogDesc& desc, uint64_t& outHandle) { +ModResult ui_dialog_push( + LoadedMod& mod, const UiDialogDesc& desc, UiPaneBuildFn build, uint64_t& outHandle) { outHandle = 0; if (!aurora::rmlui::is_initialized()) { return MOD_UNAVAILABLE; @@ -851,6 +852,16 @@ ModResult ui_dialog_push(LoadedMod& mod, const UiDialogDesc& desc, uint64_t& out auto dialog = std::make_unique( std::move(props), [handle] { on_mod_dialog_destroyed(handle); }); + if (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); + }); + if (!mod.active) { + return MOD_ERROR; + } + } if (auto* slot = slot_from_handle(handle)) { slot->document = dialog.get(); } @@ -1338,7 +1349,8 @@ ModResult ui_dialog_push(ModContext* context, const UiDialogDesc* desc, UiDialog *outDialog = 0; } auto* mod = mod_from_context(context); - if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(UiDialogDesc) || + constexpr size_t kLegacyDescSize = offsetof(UiDialogDesc, build); + if (mod == nullptr || desc == nullptr || desc->struct_size < kLegacyDescSize || desc->title == nullptr || desc->body_rml == nullptr || desc->actions == nullptr || desc->action_count == 0 || desc->variant > UI_DIALOG_DANGER) { @@ -1349,8 +1361,9 @@ ModResult ui_dialog_push(ModContext* context, const UiDialogDesc* desc, UiDialog return MOD_INVALID_ARGUMENT; } } + 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, handle); + const auto result = ui_impl::ui_dialog_push(*mod, *desc, build, handle); if (result == MOD_OK && outDialog != nullptr) { *outDialog = handle; } diff --git a/src/dusk/ui/modal.cpp b/src/dusk/ui/modal.cpp index 8c8dcd5a3b..d4d3e479dc 100644 --- a/src/dusk/ui/modal.cpp +++ b/src/dusk/ui/modal.cpp @@ -1,5 +1,7 @@ #include "modal.hpp" +#include + namespace dusk::ui { Modal::Modal(Props props) : WindowSmall("modal", "modal-dialog"), mProps(std::move(props)) { @@ -23,6 +25,9 @@ Modal::Modal(Props props) : WindowSmall("modal", "modal-dialog"), mProps(std::mo body->SetClass("modal-body", true); body->SetInnerRML(mProps.bodyRml); + mContentRoot = append(mDialog, "div"); + mContentRoot->SetClass("modal-content", true); + auto* actions = append(mDialog, "div"); actions->SetClass("modal-actions", true); if (props.isVertical) { @@ -36,12 +41,36 @@ Modal::Modal(Props props) : WindowSmall("modal", "modal-dialog"), mProps(std::mo mDoAud_seStartMenu(kSoundWindowOpen); } +void Modal::update() { + if (mContentPane != nullptr) { + mContentPane->update(); + } + if (mPendingAction) { + auto action = std::move(mPendingAction); + action(*this); + } + WindowSmall::update(); +} + +Pane& Modal::content_pane() { + if (mContentPane == nullptr) { + mContentRoot->SetClass("active", true); + mContentPane = std::make_unique(mContentRoot, Pane::Type::Uncontrolled); + } + return *mContentPane; +} + void Modal::add_action(ModalAction action) { auto* actions = mDialog->QuerySelector(".modal-actions"); auto btn = std::make_unique