From 93ec3c7dbd1f94a230e5da761ad880d67e0d0030 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Tue, 5 May 2026 19:29:42 -0600 Subject: [PATCH] New UI: Achievement toasts (#690) * Rename Overlay -> GraphicsTuner, Popup -> MenuBar * Update GraphicsTuner CSS * WIP Overlay document & toasts * Achievement toasts * Cleanup --- files.cmake | 6 +- include/dusk/achievements.h | 3 - res/rml/overlay.rcss | 211 +++++++--------- res/rml/tuner.rcss | 147 +++++++++++ src/dusk/achievements.cpp | 26 +- src/dusk/imgui/ImGuiConsole.cpp | 12 - src/dusk/imgui/ImGuiMenuTools.cpp | 62 ----- src/dusk/imgui/ImGuiMenuTools.hpp | 8 - src/dusk/ui/graphics_tuner.cpp | 282 +++++++++++++++++++++ src/dusk/ui/graphics_tuner.hpp | 90 +++++++ src/dusk/ui/{popup.cpp => menu_bar.cpp} | 20 +- src/dusk/ui/{popup.hpp => menu_bar.hpp} | 8 +- src/dusk/ui/overlay.cpp | 313 ++++++------------------ src/dusk/ui/overlay.hpp | 75 +----- src/dusk/ui/prelaunch.cpp | 1 - src/dusk/ui/settings.cpp | 26 +- src/dusk/ui/ui.cpp | 50 +++- src/dusk/ui/ui.hpp | 19 +- src/m_Do/m_Do_main.cpp | 13 +- 19 files changed, 798 insertions(+), 574 deletions(-) create mode 100644 res/rml/tuner.rcss create mode 100644 src/dusk/ui/graphics_tuner.cpp create mode 100644 src/dusk/ui/graphics_tuner.hpp rename src/dusk/ui/{popup.cpp => menu_bar.cpp} (91%) rename src/dusk/ui/{popup.hpp => menu_bar.hpp} (82%) diff --git a/files.cmake b/files.cmake index ce4f343e88..e5406ceb02 100644 --- a/files.cmake +++ b/files.cmake @@ -1473,6 +1473,8 @@ set(DUSK_FILES src/dusk/ui/editor.hpp src/dusk/ui/event.cpp src/dusk/ui/event.hpp + src/dusk/ui/graphics_tuner.cpp + src/dusk/ui/graphics_tuner.hpp src/dusk/ui/input.cpp src/dusk/ui/input.hpp src/dusk/ui/modal.cpp @@ -1484,8 +1486,8 @@ set(DUSK_FILES src/dusk/ui/overlay.hpp src/dusk/ui/pane.cpp src/dusk/ui/pane.hpp - src/dusk/ui/popup.cpp - src/dusk/ui/popup.hpp + src/dusk/ui/menu_bar.cpp + src/dusk/ui/menu_bar.hpp src/dusk/ui/prelaunch.cpp src/dusk/ui/prelaunch.hpp src/dusk/ui/preset.cpp diff --git a/include/dusk/achievements.h b/include/dusk/achievements.h index ca4676ab2d..bf0021c9d7 100644 --- a/include/dusk/achievements.h +++ b/include/dusk/achievements.h @@ -50,8 +50,6 @@ public: bool hasSignal(const char* key) const; std::vector getAchievements() const; - bool hasPendingUnlock() const { return !m_pendingUnlocks.empty(); } - std::string consumePendingUnlock(); private: struct Entry { @@ -68,7 +66,6 @@ private: std::unordered_set m_signals; bool m_loaded = false; bool m_dirty = false; - std::queue m_pendingUnlocks; }; } // namespace dusk diff --git a/res/rml/overlay.rcss b/res/rml/overlay.rcss index e18bb436e5..416252a66b 100644 --- a/res/rml/overlay.rcss +++ b/res/rml/overlay.rcss @@ -8,140 +8,107 @@ body { height: 100%; margin: 0; padding: 0; - font-family: "Fira Sans Condensed"; - font-size: 24dp; - color: #FFFFFF; - display: flex; - flex-direction: column; - justify-content: flex-end; - align-items: stretch; -} - -.overlay-root { - width: 100%; - min-height: 45%; - display: flex; - flex-direction: column; - justify-content: flex-end; - align-items: stretch; - decorator: vertical-gradient(#00000000 #151610F2); - filter: opacity(0); - transition: filter 0.2s linear-in-out; -} - -.overlay-root[open] { - filter: opacity(1); -} - -.overlay { - width: 100%; - max-width: 1216dp; - margin-left: auto; - margin-right: auto; - display: flex; - flex-direction: column; - gap: 24dp; - padding: 48dp 64dp; -} - -@media (max-height: 800dp) { - .overlay-root { - min-height: 38%; - } - - .overlay { - gap: 16dp; - padding: 32dp 48dp; - } -} - -.header { - display: flex; - justify-content: space-between; - align-items: center; - gap: 24dp; -} - -.carousel-container { - flex: 1 1 auto; - display: flex; - justify-content: flex-end; - min-width: 0; -} - -.description { - font-size: 18dp; - line-height: 22dp; - color: rgba(255, 255, 255, 50%); -} - -.divider { - margin: 1dp 0; - border-top: 1dp rgba(217, 217, 217, 50%); -} - -.footer { - display: flex; - justify-content: space-between; - align-items: center; - gap: 24dp; -} - -footer-button { - display: block; - width: 100%; - max-width: 220dp; - border: 0; - padding: 0; - background-color: transparent; - font-family: "Fira Sans Condensed"; - font-weight: bold; + font-family: "Fira Sans"; + font-weight: normal; font-size: 20dp; - line-height: 24dp; - text-transform: uppercase; - color: #FFFFFF; - opacity: 1; + color: #E0DBC8; + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: stretch; + z-index: 1; + pointer-events: none; +} + +toast { + position: absolute; + top: 40dp; + right: 40dp; + display: flex; + flex-flow: column; + border-radius: 14dp; + overflow: hidden; + border: 1dp #92875B; + backdrop-filter: blur(5dp); + box-shadow: 0 0 15dp 3dp; + background-color: rgba(21, 22, 16, 80%); + filter: opacity(0); + transform: scale(0.9); + transform-origin: center; + transition: filter transform 0.2s cubic-in-out; + padding: 18dp 24dp; + gap: 8dp; +} + +toast[open] { + filter: opacity(1); + transform: scale(1); +} + +/*toast:hover { cursor: pointer; + background-color: rgba(61, 59, 36, 80%); } -footer-button.return { - text-align: left; +toast:active { + background-color: rgba(45, 43, 26, 80%); +}*/ + +toast heading { + display: flex; + gap: 18dp; + align-items: center; + font-family: "Fira Sans Condensed"; + font-size: 18dp; + font-weight: bold; + text-transform: uppercase; + color: #92875B; } -footer-button.reset { - text-align: right; -} - -.stepped-carousel { +toast message { display: flex; align-items: center; - justify-content: center; - gap: 16dp; - width: auto; - min-width: 246dp; - padding: 0; - background-color: transparent; - font-family: "Fira Sans Condensed"; - font-weight: bold; + justify-content: start; + gap: 8dp; } -.stepped-carousel-value { - line-height: 29dp; - min-width: 166dp; - text-align: center; - white-space: nowrap; - opacity: 0.9; +toast progress { + height: 4dp; + position: absolute; + left: 0; + bottom: 0; + width: 100%; } -.stepped-carousel-arrow { - width: 24dp; - height: 24dp; - min-width: 24dp; - padding: 0; - border: 0; - background-color: transparent; - opacity: 1; - cursor: pointer; +toast progress fill { + background-color: rgba(194, 164, 45, 80%); +} + +toast.achievement { + border: 1dp #C2A42D; +} + +toast.achievement heading { + color: #C2A42D; +} + +icon { font-family: "Material Symbols Rounded"; font-weight: normal; + display: inline-block; + vertical-align: middle; } + +icon.arrow-forward { + width: 24dp; + height: 24dp; + font-size: 24dp; + decorator: text("" center center); +} + +icon.trophy { + width: 24dp; + height: 24dp; + font-size: 24dp; + decorator: text("" center center); +} \ No newline at end of file diff --git a/res/rml/tuner.rcss b/res/rml/tuner.rcss new file mode 100644 index 0000000000..86dd2043f6 --- /dev/null +++ b/res/rml/tuner.rcss @@ -0,0 +1,147 @@ +*, *:before, *:after { + box-sizing: border-box; +} + +body { + overflow: visible; + width: 100%; + height: 100%; + margin: 0; + padding: 0; + font-family: "Fira Sans Condensed"; + font-size: 24dp; + color: #FFFFFF; + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: stretch; +} + +.tuner-root { + width: 100%; + min-height: 45%; + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: stretch; + decorator: vertical-gradient(#00000000 #151610F2); + filter: opacity(0); + transition: filter 0.2s linear-in-out; +} + +.tuner-root[open] { + filter: opacity(1); +} + +.tuner { + width: 100%; + max-width: 1216dp; + margin-left: auto; + margin-right: auto; + display: flex; + flex-direction: column; + gap: 24dp; + padding: 48dp 64dp; +} + +@media (max-height: 800dp) { + .tuner-root { + min-height: 38%; + } + + .tuner { + gap: 16dp; + padding: 32dp 48dp; + } +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 24dp; +} + +.carousel-container { + flex: 1 1 auto; + display: flex; + justify-content: flex-end; + min-width: 0; +} + +.description { + font-size: 18dp; + line-height: 22dp; + color: rgba(255, 255, 255, 50%); +} + +.divider { + margin: 1dp 0; + border-top: 1dp rgba(217, 217, 217, 50%); +} + +.footer { + display: flex; + justify-content: space-between; + align-items: center; + gap: 24dp; +} + +footer-button { + display: block; + width: 100%; + max-width: 220dp; + border: 0; + padding: 0; + background-color: transparent; + font-family: "Fira Sans Condensed"; + font-weight: bold; + font-size: 20dp; + line-height: 24dp; + text-transform: uppercase; + color: #FFFFFF; + opacity: 1; + cursor: pointer; +} + +footer-button.return { + text-align: left; +} + +footer-button.reset { + text-align: right; +} + +.stepped-carousel { + display: flex; + align-items: center; + justify-content: center; + gap: 16dp; + width: auto; + min-width: 246dp; + padding: 0; + background-color: transparent; + font-family: "Fira Sans Condensed"; + font-weight: bold; +} + +.stepped-carousel-value { + line-height: 29dp; + min-width: 166dp; + text-align: center; + white-space: nowrap; + opacity: 0.9; +} + +.stepped-carousel-arrow { + width: 24dp; + height: 24dp; + min-width: 24dp; + padding: 0; + border: 0; + background-color: transparent; + opacity: 1; + cursor: pointer; + font-family: "Material Symbols Rounded"; + font-weight: normal; +} diff --git a/src/dusk/achievements.cpp b/src/dusk/achievements.cpp index 26a302678e..a090a40969 100644 --- a/src/dusk/achievements.cpp +++ b/src/dusk/achievements.cpp @@ -1,14 +1,15 @@ #include "dusk/achievements.h" -#include "dusk/io.hpp" -#include "dusk/main.h" -#include "d/d_com_inf_game.h" -#include "d/d_meter2_info.h" #include "d/actor/d_a_alink.h" #include "d/actor/d_a_npc4.h" #include "d/actor/d_a_player.h" +#include "d/d_com_inf_game.h" #include "d/d_demo.h" -#include "f_pc/f_pc_name.h" +#include "d/d_meter2_info.h" +#include "dusk/io.hpp" +#include "dusk/main.h" +#include "dusk/ui/ui.hpp" #include "f_op/f_op_actor_mng.h" +#include "f_pc/f_pc_name.h" #include #include @@ -454,12 +455,6 @@ AchievementSystem& AchievementSystem::get() { return instance; } -std::string AchievementSystem::consumePendingUnlock() { - std::string msg = std::move(m_pendingUnlocks.front()); - m_pendingUnlocks.pop(); - return msg; -} - std::vector AchievementSystem::getAchievements() const { std::vector result; result.reserve(m_entries.size()); @@ -559,7 +554,14 @@ void AchievementSystem::processEntry(Entry& e) { if (nowUnlocked) { e.achievement.progress = e.achievement.isCounter ? e.achievement.goal : 1; e.achievement.unlocked = true; - m_pendingUnlocks.push(e.achievement.name); + if (getSettings().game.enableAchievementNotifications) { + ui::push_toast({ + .type = "achievement", + .title = "Achievement Unlocked!", + .content = e.achievement.name, + .duration = std::chrono::seconds(5), + }); + } m_dirty = true; } else if (progressChanged) { m_dirty = true; diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index 7530a7f613..fa5f849dbe 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -10,11 +10,9 @@ #include "fmt/format.h" #include "ImGuiConsole.hpp" -#include "dusk/ui/preset.hpp" #include "dusk/ui/ui.hpp" #include "JSystem/JUtility/JUTGamePad.h" #include "SDL3/SDL_mouse.h" -#include "dusk/achievements.h" #include "dusk/audio/DuskAudioSystem.h" #include "dusk/config.hpp" #include "dusk/dusk.h" @@ -254,15 +252,6 @@ namespace dusk { UpdateSettings(); - AchievementSystem::get().tick(); - while (AchievementSystem::get().hasPendingUnlock()) { - if (getSettings().game.enableAchievementNotifications) { - m_menuTools.notifyAchievement(AchievementSystem::get().consumePendingUnlock()); - } else { - AchievementSystem::get().consumePendingUnlock(); - } - } - if (!fpcM_SearchByName(fpcNm_LOGO_SCENE_e) && (ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) && ImGui::IsKeyPressed(ImGuiKey_R)) @@ -338,7 +327,6 @@ namespace dusk { m_menuTools.ShowSaveEditor(); m_menuTools.ShowStateShare(); } - m_menuTools.showAchievementNotification(); DuskDebugPad(); // temporary, remove later // Hide mouse cursor if the F1 menu is not open and the cursor is idle for 3 seconds. diff --git a/src/dusk/imgui/ImGuiMenuTools.cpp b/src/dusk/imgui/ImGuiMenuTools.cpp index 714add87c4..d3c062e0f0 100644 --- a/src/dusk/imgui/ImGuiMenuTools.cpp +++ b/src/dusk/imgui/ImGuiMenuTools.cpp @@ -267,66 +267,4 @@ namespace dusk { ImGui::End(); ImGui::PopFont(); } - - void ImGuiMenuTools::notifyAchievement(std::string name) { - if (m_notifyTimer <= 0.f) { - m_notifyName = std::move(name); - m_notifyTimer = NOTIFY_DURATION; - } else { - m_notifyQueue.push(std::move(name)); - } - } - - void ImGuiMenuTools::showAchievementNotification() { - if (!getSettings().game.enableAchievementNotifications.getValue()) { - return; - } - if (m_notifyTimer <= 0.f) { - if (m_notifyQueue.empty()) { - return; - } - m_notifyName = std::move(m_notifyQueue.front()); - m_notifyQueue.pop(); - m_notifyTimer = NOTIFY_DURATION; - } - - m_notifyTimer -= ImGui::GetIO().DeltaTime; - - const float alpha = std::min({ - m_notifyTimer / NOTIFY_FADE_TIME, - (NOTIFY_DURATION - m_notifyTimer) / NOTIFY_FADE_TIME, - 1.0f - }); - - const ImGuiViewport* viewport = ImGui::GetMainViewport(); - const float padding = 12.0f; - ImGui::SetNextWindowPos( - ImVec2(viewport->WorkPos.x + viewport->WorkSize.x - padding, viewport->WorkPos.y + padding), - ImGuiCond_Always, ImVec2(1.0f, 0.0f) - ); - - ImGui::SetNextWindowBgAlpha(alpha * 0.92f); - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.06f, 0.01f, alpha * 0.92f)); - ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 0.8f, 0.1f, alpha)); - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, alpha)); - ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 2.0f); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(14.0f, 10.0f)); - - constexpr ImGuiWindowFlags flags = - ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | - ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoInputs; - - if (ImGui::Begin("##achievement_notify", nullptr, flags)) { - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.82f, 0.1f, alpha)); - ImGui::TextUnformatted("Achievement Unlocked!"); - ImGui::PopStyleColor(); - ImGui::Spacing(); - ImGui::TextUnformatted(m_notifyName.c_str()); - } - ImGui::End(); - - ImGui::PopStyleVar(2); - ImGui::PopStyleColor(3); - } } diff --git a/src/dusk/imgui/ImGuiMenuTools.hpp b/src/dusk/imgui/ImGuiMenuTools.hpp index 7ae0e5bec6..f2c68494f7 100644 --- a/src/dusk/imgui/ImGuiMenuTools.hpp +++ b/src/dusk/imgui/ImGuiMenuTools.hpp @@ -27,8 +27,6 @@ namespace dusk { void ShowAudioDebug(); void ShowSaveEditor(); void ShowStateShare(); - void notifyAchievement(std::string name); - void showAchievementNotification(); private: bool m_showDebugOverlay = false; @@ -68,12 +66,6 @@ namespace dusk { bool m_showStateShare = false; ImGuiStateShare m_stateShare; - - std::string m_notifyName; - float m_notifyTimer = 0.f; - std::queue m_notifyQueue; - static constexpr float NOTIFY_DURATION = 4.0f; - static constexpr float NOTIFY_FADE_TIME = 0.5f; }; } diff --git a/src/dusk/ui/graphics_tuner.cpp b/src/dusk/ui/graphics_tuner.cpp new file mode 100644 index 0000000000..390d4f9971 --- /dev/null +++ b/src/dusk/ui/graphics_tuner.cpp @@ -0,0 +1,282 @@ +#include "graphics_tuner.hpp" + +#include "Z2AudioLib/Z2SeMgr.h" +#include "m_Do/m_Do_audio.h" + +#include +#include +#include + +#include "dusk/config.hpp" +#include "dusk/settings.h" + +#include +#include + +namespace dusk::ui { +namespace { + +const Rml::String kDocumentSource = R"RML( + + + + + +
+
+
+
+ +
+
+
+ +
+
+ +
+)RML"; + +int get_value(GraphicsOption option) { + switch (option) { + case GraphicsOption::InternalResolution: + return getSettings().game.internalResolutionScale.getValue(); + case GraphicsOption::ShadowResolution: + return getSettings().game.shadowResolutionMultiplier.getValue(); + case GraphicsOption::BloomMode: + return static_cast(getSettings().game.bloomMode.getValue()); + case GraphicsOption::BloomMultiplier: + return std::clamp( + static_cast(getSettings().game.bloomMultiplier.getValue() * 100.0f + 0.5f), 0, + 100); + } + return 0; +} + +void set_value(GraphicsOption option, int value) { + switch (option) { + case GraphicsOption::InternalResolution: + getSettings().game.internalResolutionScale.setValue(value); + VISetFrameBufferScale(static_cast(value)); + break; + case GraphicsOption::ShadowResolution: + getSettings().game.shadowResolutionMultiplier.setValue(value); + break; + case GraphicsOption::BloomMode: + getSettings().game.bloomMode.setValue(static_cast(std::clamp( + value, static_cast(BloomMode::Off), static_cast(BloomMode::Dusk)))); + break; + case GraphicsOption::BloomMultiplier: + getSettings().game.bloomMultiplier.setValue(std::clamp(value, 0, 100) / 100.0f); + break; + } + config::Save(); +} + +Rml::Element* create_stepped_carousel_root(Rml::Element* parent) { + auto* doc = parent->GetOwnerDocument(); + auto root = doc->CreateElement("div"); + root->SetClass("stepped-carousel", true); + root->SetAttribute("tabindex", "0"); + return parent->AppendChild(std::move(root)); +} + +Rml::Element* create_stepped_carousel_arrow( + Rml::Element* parent, const Rml::String& className, const Rml::String& label) { + auto* doc = parent->GetOwnerDocument(); + auto button = doc->CreateElement("button"); + button->SetClass("stepped-carousel-arrow", true); + button->SetClass(className, true); + button->SetInnerRML(label); + return parent->AppendChild(std::move(button)); +} + +} // namespace + +SteppedCarousel::SteppedCarousel(Rml::Element* parent, Props props) + : Component(create_stepped_carousel_root(parent)), mProps(std::move(props)) { + Rml::Element* prevElem = create_stepped_carousel_arrow(mRoot, "prev", ""); + mValueElem = append(mRoot, "div"); + mValueElem->SetClass("stepped-carousel-value", true); + Rml::Element* nextElem = create_stepped_carousel_arrow(mRoot, "next", ""); + + listen(prevElem, Rml::EventId::Click, + [this](Rml::Event&) { handle_nav_command(NavCommand::Left); }); + listen(nextElem, Rml::EventId::Click, + [this](Rml::Event&) { handle_nav_command(NavCommand::Right); }); + listen(mRoot, Rml::EventId::Keydown, [this](Rml::Event& event) { + const auto cmd = map_nav_event(event); + if (cmd != NavCommand::None && handle_nav_command(cmd)) { + event.StopPropagation(); + } + }); +} + +bool SteppedCarousel::focus() { + return Component::focus(); +} + +void SteppedCarousel::update() { + if (mValueElem == nullptr) { + return; + } + const int value = std::clamp(mProps.getValue ? mProps.getValue() : 0, mProps.min, mProps.max); + if (mProps.formatValue) { + mValueElem->SetInnerRML(mProps.formatValue(value)); + } else { + mValueElem->SetInnerRML(std::to_string(value)); + } +} + +bool SteppedCarousel::handle_nav_command(NavCommand cmd) { + if (cmd == NavCommand::Left) { + const int value = mProps.getValue ? mProps.getValue() : 0; + apply(std::clamp(value - mProps.step, mProps.min, mProps.max)); + return true; + } + if (cmd == NavCommand::Right) { + const int value = mProps.getValue ? mProps.getValue() : 0; + apply(std::clamp(value + mProps.step, mProps.min, mProps.max)); + return true; + } + return false; +} + +void SteppedCarousel::apply(int value) { + const int nextValue = std::clamp(value, mProps.min, mProps.max); + const int currentValue = + std::clamp(mProps.getValue ? mProps.getValue() : 0, mProps.min, mProps.max); + if (nextValue == currentValue) { + return; + } + mDoAud_seStartMenu(kSoundItemChange); + if (mProps.onChange) { + mProps.onChange(nextValue); + } +} + +Rml::String format_graphics_setting_value(GraphicsOption option, int value) { + switch (option) { + case GraphicsOption::InternalResolution: + if (value <= 0) { + return "Auto"; + } else { + u32 width = 0; + u32 height = 0; + AuroraGetRenderSize(&width, &height); + return fmt::format("{}× ({}×{})", value, width, height); + } + case GraphicsOption::ShadowResolution: + return fmt::format("{}×", value); + case GraphicsOption::BloomMode: + switch (static_cast(value)) { + case BloomMode::Off: + return "Off"; + case BloomMode::Classic: + return "Classic"; + case BloomMode::Dusk: + return "Dusk"; + } + break; + case GraphicsOption::BloomMultiplier: + return fmt::format("{}%", value); + } + return ""; +} + +GraphicsTuner::GraphicsTuner(GraphicsTunerProps props) + : Document(kDocumentSource), mOption(props.option), mValueMin(props.valueMin), + mValueMax(props.valueMax), mDefaultValue(props.defaultValue) { + if (mDocument == nullptr) { + return; + } + + if (auto* title = mDocument->GetElementById("title")) { + title->SetInnerRML(escape(props.title)); + } + if (auto* description = mDocument->GetElementById("description")) { + description->SetInnerRML(escape(props.helpText)); + } + if (auto* carouselParent = mDocument->GetElementById("carousel-container")) { + add_component(carouselParent, + SteppedCarousel::Props{ + .min = mValueMin, + .max = mValueMax, + .step = 1, + .getValue = [this] { return get_value(mOption); }, + .onChange = [this](int value) { set_value(mOption, value); }, + .formatValue = + [this](int value) { return format_graphics_setting_value(mOption, value); }, + }); + } + + if (auto* footer = mDocument->GetElementById("footer")) { + auto& returnButton = add_component