From 4bdaff01fc2afaa9451e6ec3d88f8e444eaa8ba3 Mon Sep 17 00:00:00 2001 From: Nicholas Bly <73457207+NicholasBly@users.noreply.github.com> Date: Sun, 13 Sep 2026 08:07:54 -0400 Subject: [PATCH] Add Exit button + controller led fix (#221) --- runtime/include/aurora_events.h | 1 + runtime/include/settings_overlay.h | 2 ++ runtime/src/settings_overlay.cpp | 50 ++++++++++++++++++++++++++++-- 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/runtime/include/aurora_events.h b/runtime/include/aurora_events.h index ddb2df8..1fcc911 100644 --- a/runtime/include/aurora_events.h +++ b/runtime/include/aurora_events.h @@ -58,6 +58,7 @@ inline void Flush(bool force = false) { // still active. A window close is an intentional successful exit, so end the // process directly and do not run the crash/atexit paths. [[noreturn]] inline void ExitForAuroraWindowClose() noexcept { + settings_overlay::ReleaseControllers(); WindowPlacementPersistence::Flush(true); #if defined(_WIN32) ::ExitProcess(0); diff --git a/runtime/include/settings_overlay.h b/runtime/include/settings_overlay.h index b706989..631eb3a 100644 --- a/runtime/include/settings_overlay.h +++ b/runtime/include/settings_overlay.h @@ -12,4 +12,6 @@ void Draw() noexcept; bool StartupScreenVisible() noexcept; void NotifyStrapInputAccepted() noexcept; void AdvancePresentedFrame() noexcept; +// Put host controllers back to a neutral state before the process ends. +void ReleaseControllers() noexcept; } // namespace settings_overlay diff --git a/runtime/src/settings_overlay.cpp b/runtime/src/settings_overlay.cpp index aad825d..9354800 100644 --- a/runtime/src/settings_overlay.cpp +++ b/runtime/src/settings_overlay.cpp @@ -1,5 +1,6 @@ #include "settings_overlay.h" #include "audio_backend.h" +#include "aurora_events.h" #include "controller_button_names.h" #include "controller_mapping_wizard.h" #include "input_bindings.h" @@ -15,6 +16,7 @@ #include #include #include +#include #include #include @@ -70,6 +72,7 @@ const char* GraphicsApiDisplayName() { } bool g_topBarVisible = false; +bool g_exitPromptOpen = false; bool g_rumbleEnabled = RuntimeConfigFile::RumbleEnabled(true); int g_controllerPort = 0; float g_resolutionScale = RuntimeConfigFile::ResolutionMultiplier(1.0f); @@ -1175,6 +1178,18 @@ void DrawStartupScreen() { ImGui::PopStyleColor(); } +void DrawExitPrompt() { + constexpr const char* kTitle = "Exit"; + if (g_exitPromptOpen && !ImGui::IsPopupOpen(kTitle)) ImGui::OpenPopup(kTitle); + if (!ImGui::BeginPopupModal(kTitle, &g_exitPromptOpen, ImGuiWindowFlags_AlwaysAutoResize)) return; + ImGui::TextUnformatted("Quit the game?"); + if (ImGui::Button("Exit", ImVec2(120.0f, 0.0f))) ExitForAuroraWindowClose(); + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(120.0f, 0.0f))) g_exitPromptOpen = false; + if (!g_exitPromptOpen) ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); +} + void DrawTopBar() { if (!g_topBarVisible) { return; @@ -1243,11 +1258,17 @@ void DrawTopBar() { ImGui::EndMenu(); } - const float hideWidth = ImGui::CalcTextSize("Hide (F10)").x + ImGui::GetStyle().FramePadding.x * 2.0f; - ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), ImGui::GetWindowWidth() - hideWidth - 8.0f)); + const ImGuiStyle& style = ImGui::GetStyle(); + const float hideWidth = ImGui::CalcTextSize("Hide (F10)").x + style.FramePadding.x * 2.0f; + const float exitWidth = ImGui::CalcTextSize("X").x + style.FramePadding.x * 2.0f; + ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), + ImGui::GetWindowWidth() - hideWidth - exitWidth - style.ItemSpacing.x - 8.0f)); if (ImGui::MenuItem("Hide (F10)")) { SetTopBarVisible(false); } + if (ImGui::MenuItem("X")) { + g_exitPromptOpen = true; + } ImGui::EndMainMenuBar(); } @@ -1342,12 +1363,36 @@ void HandleEvents(const AuroraEvent* events) noexcept { if (!g_rebind.active && IsToggleKey(ev->sdl, SDL_SCANCODE_F10)) { SetTopBarVisible(!g_topBarVisible); } + if (!g_rebind.active && !g_topBarVisible && IsToggleKey(ev->sdl, SDL_SCANCODE_ESCAPE)) { + g_exitPromptOpen = true; + } if (IsMouseActivity(ev->sdl)) { g_lastMouseActivity = Clock::now(); } } } +void ReleaseControllers() noexcept { + // Aurora drives the LED white on first PADRead and never clears it, and the + // exit paths terminate the process outright, so do it here. + bool queued = false; + for (uint32_t port = 0; port < PAD_MAX_CONTROLLERS; ++port) { + const s32 index = PADGetIndexForPort(port); + if (index < 0) continue; + if (SDL_Gamepad* pad = PADGetSDLGamepadForIndex(static_cast(index))) { + SDL_SetGamepadLED(pad, 0, 0, 0); + queued = true; + } + } + constexpr std::array stopAll{ + PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD}; + PADControlAllMotors(stopAll.data()); + // SDL hands LED and rumble reports to its own HIDAPI sender thread rather + // than writing them here, so without this the process dies before the + // controller ever receives them. + if (queued) SDL_Delay(120); +} + void Draw() noexcept { // Wait for the frame worker's DONE phase: it has replayed the previous frame's ImGui draw lists // and started the next ImGui frame, so all overlay callers can now safely issue ImGui commands. @@ -1365,6 +1410,7 @@ void Draw() noexcept { } DrawFpsOverlay(); DrawTopBar(); + DrawExitPrompt(); controller_mapping_wizard::Draw(); // The wizard captures raw presses; keep them out of the game. const bool inputBlocked = controller_mapping_wizard::IsActive() || g_rebind.active;