From 18eb0692f099bf235d0bc717ce8b21fc0e1d9ead Mon Sep 17 00:00:00 2001 From: Luke Street Date: Wed, 6 May 2026 17:34:08 -0600 Subject: [PATCH] UI: "No controller" & menu notifications Resolves #629 Resolves #678 --- res/rml/overlay.rcss | 50 +++++++++++- src/dusk/imgui/ImGuiConsole.cpp | 4 - src/dusk/ui/overlay.cpp | 139 ++++++++++++++++++++++++++++++-- src/dusk/ui/overlay.hpp | 3 + src/dusk/ui/prelaunch.cpp | 3 +- src/dusk/ui/ui.cpp | 11 +++ src/dusk/ui/ui.hpp | 2 + 7 files changed, 200 insertions(+), 12 deletions(-) diff --git a/res/rml/overlay.rcss b/res/rml/overlay.rcss index cf90037f57..0f9e4ffd4e 100644 --- a/res/rml/overlay.rcss +++ b/res/rml/overlay.rcss @@ -110,6 +110,47 @@ toast.achievement heading { color: #C2A42D; } +toast.controller-warning { + top: auto; + right: auto; + bottom: 40dp; + left: 50%; + width: 440dp; + max-width: 90%; + transform: translateX(-50%) scale(0.9); +} + +toast.controller-warning[open] { + transform: translateX(-50%) scale(1); +} + +toast.controller-warning heading { + color: #C2A42D; +} + +toast.menu-notification { + top: 40dp; + right: auto; + bottom: auto; + left: 50%; + max-width: 90%; + transform: translateX(-50%) scale(0.9); +} + +toast.menu-notification[open] { + transform: translateX(-50%) scale(1); +} + +toast.menu-notification message { + align-items: center; + text-align: center; +} + +toast.menu-notification message row { + align-items: center; + gap: 6dp; +} + icon { font-family: "Material Symbols Rounded"; font-weight: normal; @@ -138,6 +179,13 @@ icon.controller { decorator: text("" center center); } +icon.warning { + width: 24dp; + height: 24dp; + font-size: 24dp; + decorator: text("" center center); +} + logo { position: absolute; width: 100dp; @@ -186,4 +234,4 @@ logo img.outer { to { transform: rotate(360deg); } -} \ No newline at end of file +} diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index a18d257690..86e4312b00 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -291,10 +291,6 @@ namespace dusk { ImGui::PopStyleColor(); if (dusk::IsGameLaunched && !m_isLaunchInitialized) { - AddToast(ImGui::GetIO().MouseSource == ImGuiMouseSource_TouchScreen ? - "3-finger tap to toggle menu"s : - "Press F1 to toggle menu"s, - 4.f); m_isLaunchInitialized = true; if (getSettings().game.liveSplitEnabled) { dusk::speedrun::connectLiveSplit(); diff --git a/src/dusk/ui/overlay.cpp b/src/dusk/ui/overlay.cpp index 6e401934ce..c11ad51b78 100644 --- a/src/dusk/ui/overlay.cpp +++ b/src/dusk/ui/overlay.cpp @@ -1,11 +1,13 @@ #include "overlay.hpp" #include "aurora/lib/logging.hpp" -#include "magic_enum.hpp" - -#include - #include "dusk/achievements.h" +#include "magic_enum.hpp" +#include "window.hpp" + +#include +#include +#include namespace dusk::ui { namespace { @@ -27,6 +29,8 @@ constexpr std::array, 3> kAutoSaveLayers{{ {"center", "res/org-icon-center.png"}, }}; +constexpr auto kMenuNotificationDuration = std::chrono::milliseconds(2500); + Rml::Element* create_toast(Rml::Element* parent, const Toast& toast) { if (toast.type == "autosave") { auto* logo = append(parent, "logo"); @@ -75,6 +79,78 @@ Rml::Element* create_toast(Rml::Element* parent, const Toast& toast) { return elem; } +Rml::Element* create_controller_warning(Rml::Element* parent) { + auto* elem = append(parent, "toast"); + elem->SetClass("controller-warning", true); + + auto* heading = append(elem, "heading"); + auto* title = append(heading, "span"); + title->SetInnerRML("No controller assigned"); + auto* icon = append(heading, "icon"); + icon->SetClass("warning", true); + + auto* message = append(elem, "message"); + auto* content = append(message, "span"); + content->SetInnerRML("Configure controller port 1 in Settings."); + + return elem; +} + +SDL_Gamepad* gamepad_for_port(u32 port) noexcept { + const s32 index = PADGetIndexForPort(port); + if (index < 0) { + return nullptr; + } + return PADGetSDLGamepadForIndex(static_cast(index)); +} + +Rml::String back_button_name() { + if (auto* gamepad = gamepad_for_port(PAD_CHAN0)) { + switch (SDL_GetGamepadType(gamepad)) { + case SDL_GAMEPAD_TYPE_PS3: + return "Select"; + case SDL_GAMEPAD_TYPE_PS4: + return "Share"; + case SDL_GAMEPAD_TYPE_PS5: + return "Create"; + case SDL_GAMEPAD_TYPE_XBOX360: + return "Back"; + case SDL_GAMEPAD_TYPE_XBOXONE: + return "View"; + case SDL_GAMEPAD_TYPE_GAMECUBE: + return "R + Start"; + default: + break; + } + } + return "Back"; +} + +Rml::Element* create_menu_notification(Rml::Element* parent) { + auto* elem = append(parent, "toast"); + elem->SetClass("menu-notification", true); + + auto* message = append(elem, "message"); + auto* row = append(message, "row"); + append(row, "span")->SetInnerRML("Press F1 or"); + auto* icon = append(row, "icon"); + icon->SetClass("controller", true); + append(row, "span")->SetInnerRML(escape(back_button_name())); + append(row, "span")->SetInnerRML("to open menu"); + + return elem; +} + +void remove_element(Rml::Element*& elem) noexcept { + if (elem == nullptr) { + return; + } + if (auto* parent = elem->GetParentNode()) { + parent->RemoveChild(elem); + } + elem = nullptr; +} + } // namespace Overlay::Overlay() : Document(kDocumentSource) { @@ -86,6 +162,15 @@ Overlay::Overlay() : Document(kDocumentSource) { { mCurrentToast->SetPseudoClass("done", true); } + } else if (mControllerWarning != nullptr && + event.GetTargetElement() == mControllerWarning && + !mControllerWarning->HasAttribute("open")) + { + mControllerWarning->SetPseudoClass("done", true); + } else if (mMenuNotification != nullptr && event.GetTargetElement() == mMenuNotification && + !mMenuNotification->HasAttribute("open")) + { + mMenuNotification->SetPseudoClass("done", true); } }); } @@ -98,6 +183,49 @@ void Overlay::show() { void Overlay::update() { Document::update(); + if (mDocument == nullptr) { + return; + } + + const bool showControllerWarning = + PADGetIndexForPort(PAD_CHAN0) < 0 && dynamic_cast(top_document()) == nullptr; + if (showControllerWarning && mControllerWarning == nullptr) { + mControllerWarning = create_controller_warning(mDocument); + } else if (showControllerWarning && mControllerWarning != nullptr) { + mControllerWarning->SetAttribute("open", ""); + mControllerWarning->SetPseudoClass("opened", true); + mControllerWarning->SetPseudoClass("done", false); + } else if (!showControllerWarning && mControllerWarning != nullptr) { + if (mControllerWarning->IsPseudoClassSet("done") || + !mControllerWarning->IsPseudoClassSet("opened")) + { + remove_element(mControllerWarning); + } else { + mControllerWarning->RemoveAttribute("open"); + } + } + + if (mMenuNotification != nullptr) { + if (clock::now() >= mMenuNotificationStartTime + kMenuNotificationDuration) { + if (mMenuNotification->IsPseudoClassSet("done") || + !mMenuNotification->IsPseudoClassSet("opened")) + { + remove_element(mMenuNotification); + } else { + mMenuNotification->RemoveAttribute("open"); + } + } else { + mMenuNotification->SetAttribute("open", ""); + mMenuNotification->SetPseudoClass("opened", true); + mMenuNotification->SetPseudoClass("done", false); + } + } + if (consume_menu_notification_request()) { + if (mMenuNotification == nullptr) { + mMenuNotification = create_menu_notification(mDocument); + } + mMenuNotificationStartTime = clock::now(); + } auto& toasts = get_toasts(); if (mCurrentToast == nullptr) { @@ -123,8 +251,7 @@ void Overlay::update() { // Fallback for large gaps in time where we never actually opened it !mCurrentToast->IsPseudoClassSet("opened")) { - mCurrentToast->GetParentNode()->RemoveChild(mCurrentToast); - mCurrentToast = nullptr; + remove_element(mCurrentToast); toasts.pop_front(); } else { mCurrentToast->RemoveAttribute("open"); diff --git a/src/dusk/ui/overlay.hpp b/src/dusk/ui/overlay.hpp index 71e2e72470..6a29ca26e4 100644 --- a/src/dusk/ui/overlay.hpp +++ b/src/dusk/ui/overlay.hpp @@ -17,7 +17,10 @@ protected: bool handle_nav_command(Rml::Event& event, NavCommand cmd) override; Rml::Element* mCurrentToast = nullptr; + Rml::Element* mControllerWarning = nullptr; + Rml::Element* mMenuNotification = nullptr; clock::time_point mCurrentToastStartTime; + clock::time_point mMenuNotificationStartTime; }; } // namespace dusk::ui diff --git a/src/dusk/ui/prelaunch.cpp b/src/dusk/ui/prelaunch.cpp index f4cf78c9d3..6fe9075891 100644 --- a/src/dusk/ui/prelaunch.cpp +++ b/src/dusk/ui/prelaunch.cpp @@ -178,6 +178,7 @@ Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementB } mDoAud_seStartMenu(kSoundPlay); + show_menu_notification(); if (getSettings().audio.menuSounds) { JAISoundHandle* handle = g_mEnvSeMgr.field_0x144.getHandle(); @@ -199,7 +200,7 @@ Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementB }); apply_intro_animation(mMenuButtons.back()->root(), "delay-1"); - mMenuButtons.push_back(std::make_unique