diff --git a/runtime/include/runtime_config.h b/runtime/include/runtime_config.h index c8cdf80..4167572 100644 --- a/runtime/include/runtime_config.h +++ b/runtime/include/runtime_config.h @@ -91,6 +91,7 @@ struct RuntimeUserConfig { // "dpad_up,left_shoulder") as values; pressing either bound button counts. std::array, 12> controllerButtons; std::optional rumbleEnabled; + std::optional muteHotkey; std::map controllerExpressions; }; @@ -411,6 +412,9 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) { } config.rumbleEnabled = FindConfigValue(document, "controller", "rumble"); + if (auto value = FindConfigInt(document, "audio", "mute_key")) { + config.muteHotkey = *value; + } if (const auto* section = document.contains("controller") ? &document.at("controller") : nullptr; section != nullptr && section->is_table()) { @@ -710,6 +714,15 @@ inline bool SetRumbleEnabled(bool value) { return WriteSetting("controller", "rumble", value ? "true" : "false"); } +inline int32_t MuteHotkey(int32_t fallback) { + return Get().muteHotkey.value_or(fallback); +} + +inline bool SetMuteHotkey(int32_t value) { + Mutable().muteHotkey = value; + return WriteSetting("audio", "mute_key", std::to_string(value)); +} + inline bool SetAudioVolume(float value) { value = std::clamp(value, 0.0f, 1.0f); Mutable().audioVolume = value; diff --git a/runtime/src/settings_overlay.cpp b/runtime/src/settings_overlay.cpp index 9354800..8127eae 100644 --- a/runtime/src/settings_overlay.cpp +++ b/runtime/src/settings_overlay.cpp @@ -83,6 +83,7 @@ int g_soundEffectsVolumePercent = int g_uiVolumePercent = static_cast(std::lround(RuntimeConfigFile::UiVolume(1.0f) * 100.0f)); int g_voicesVolumePercent = static_cast(std::lround(RuntimeConfigFile::VoicesVolume(1.0f) * 100.0f)); bool g_audioMuted = RuntimeConfigFile::AudioMuted(false); +int32_t g_muteHotkey = RuntimeConfigFile::MuteHotkey(SDL_SCANCODE_BACKSLASH); bool g_audioMixWorker = RuntimeConfigFile::AudioMixWorkerEnabled(true); bool g_attenuateMusicWhenMediaPlays = RuntimeConfigFile::AttenuateMusicWhenMediaPlays(false); int g_frameInterpolationMode = [] { @@ -432,7 +433,7 @@ const char* KeyBindingName(int scancode) { } } -enum class RebindKind { KeyboardButton, KeyboardAxis, Controller }; +enum class RebindKind { KeyboardButton, KeyboardAxis, Controller, MuteHotkey }; struct RebindState { bool active = false; bool openPopup = false; @@ -498,6 +499,11 @@ void CompleteRebind(uint32_t value) { if (alternateValue != PAD_NATIVE_BUTTON_INVALID) config += ',' + NativeBindingConfig(alternateValue); for (size_t i = 0; i < kControllerButtons.size(); ++i) if (kControllerButtons[i].padButton == capture.target) RuntimeConfigFile::SetControllerButton(i, config); + } else if (capture.kind == RebindKind::MuteHotkey) { + g_muteHotkey = static_cast(value); + RuntimeConfigFile::SetMuteHotkey(g_muteHotkey); + g_rebind.active = false; + return; } else if (capture.kind == RebindKind::KeyboardButton) { PADSetKeyButtonBinding(capture.port, {static_cast(value), capture.target}); } else { @@ -520,7 +526,9 @@ void DrawRebindPrompt() { ImGui::Text("Rebind: %s", g_rebind.label.c_str()); ImGui::TextUnformatted(g_rebind.kind == RebindKind::Controller ? "Press a controller button, pull a trigger, or move a stick." - : "Press a keyboard key or click a mouse button."); + : g_rebind.kind == RebindKind::MuteHotkey + ? "Press a keyboard key." + : "Press a keyboard key or click a mouse button."); ImGui::TextUnformatted("Release any held input first. Backspace or Delete clears the mapping."); ImGui::TextUnformatted("Escape can be bound. F10 is reserved for settings."); const float remaining = std::chrono::duration(g_rebind.deadline - Clock::now()).count(); @@ -542,7 +550,8 @@ void DrawRebindPrompt() { } const uint32_t mouse = SDL_GetMouseState(nullptr, nullptr); for (int i = 1; i <= 5 && g_rebind.active; ++i) - if (!overControl && (mouse & ~g_rebind.mouse & (1u << (i - 1))) != 0) CompleteRebind(static_cast(-i - 1)); + if (!overControl && g_rebind.kind != RebindKind::MuteHotkey && + (mouse & ~g_rebind.mouse & (1u << (i - 1))) != 0) CompleteRebind(static_cast(-i - 1)); g_rebind.mouse = mouse; } else if (g_rebind.active && SDL_GetKeyboardFocus() != nullptr && g_rebind.kind == RebindKind::Controller) { auto* pad = SDL_GetGamepadFromID(g_rebind.instance); @@ -565,10 +574,11 @@ void DrawRebindPrompt() { ImGui::EndPopup(); } -void DrawKeyBinding(const char* label, int scancode, RebindKind kind, uint16_t target) { +void DrawKeyBinding(const char* label, int scancode, RebindKind kind, uint16_t target, + float width = 220.0f) { const std::string caption = std::string(KeyBindingName(scancode)) + "##binding"; - if (ImGui::Button(caption.c_str(), ImVec2(220.0f, 0.0f))) BeginRebind(kind, target, label); - ImGui::SameLine(); + if (ImGui::Button(caption.c_str(), ImVec2(width, 0.0f))) BeginRebind(kind, target, label); + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); ImGui::TextUnformatted(label); } @@ -955,10 +965,14 @@ void DrawAudioSettings() { MusicAttenuation::SetVoicesVolume(volume); RuntimeConfigFile::SetVoicesVolume(volume); } + const float labelColumn = ImGui::GetCursorPosX() + ImGui::CalcItemWidth(); if (ImGui::Checkbox("Mute", &g_audioMuted)) { AudioBackend::Instance().SetMuted(g_audioMuted); RuntimeConfigFile::SetAudioMuted(g_audioMuted); } + ImGui::SameLine(); + DrawKeyBinding("Mute shortcut", g_muteHotkey, RebindKind::MuteHotkey, 0, + std::max(60.0f, labelColumn - ImGui::GetCursorPosX())); ImGui::Separator(); if (ImGui::Checkbox("Mix audio on a worker thread", &g_audioMixWorker)) { // Applies immediately: SetMixWorkerEnabled joins any in-flight mix @@ -1255,6 +1269,7 @@ void DrawTopBar() { const std::string audioMenuLabel = audioLabel + "###AudioSettingsMenu"; if (ImGui::BeginMenu(audioMenuLabel.c_str())) { DrawAudioSettings(); + DrawRebindPrompt(); ImGui::EndMenu(); } @@ -1297,9 +1312,12 @@ void UpdateCursorAutoHide() { return; } g_cursorHidden = shouldHide; + // ImGui_ImplSDL3_NewFrame calls SDL_ShowCursor every frame unless this flag is set. if (shouldHide) { + ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange; SDL_HideCursor(); } else { + ImGui::GetIO().ConfigFlags &= ~ImGuiConfigFlags_NoMouseCursorChange; SDL_ShowCursor(); } } @@ -1363,6 +1381,12 @@ void HandleEvents(const AuroraEvent* events) noexcept { if (!g_rebind.active && IsToggleKey(ev->sdl, SDL_SCANCODE_F10)) { SetTopBarVisible(!g_topBarVisible); } + if (!g_rebind.active && g_muteHotkey != PAD_KEY_INVALID && + IsToggleKey(ev->sdl, static_cast(g_muteHotkey))) { + g_audioMuted = !g_audioMuted; + AudioBackend::Instance().SetMuted(g_audioMuted); + RuntimeConfigFile::SetAudioMuted(g_audioMuted); + } if (!g_rebind.active && !g_topBarVisible && IsToggleKey(ev->sdl, SDL_SCANCODE_ESCAPE)) { g_exitPromptOpen = true; }