UI: 3-finger tap to toggle menu on mobile

This commit is contained in:
Luke Street
2026-05-03 22:39:31 -06:00
parent 95e6ac54cf
commit 43b603e70b
6 changed files with 166 additions and 72 deletions
+1 -1
-1
View File
@@ -154,7 +154,6 @@ struct UserSettings {
ConfigVar<bool> showPipelineCompilation;
ConfigVar<bool> wasPresetChosen;
ConfigVar<bool> enableCrashReporting;
ConfigVar<bool> duskMenuOpen;
ConfigVar<int> cardFileType;
} backend;
};
+2 -58
View File
@@ -11,9 +11,7 @@
#include "fmt/format.h"
#include "ImGuiConsole.hpp"
#include "JSystem/JUtility/JUTGamePad.h"
#include "SDL3/SDL_events.h"
#include "SDL3/SDL_mouse.h"
#include "aurora/lib/window.hpp"
#include "dusk/achievements.h"
#include "dusk/audio/DuskAudioSystem.h"
#include "dusk/config.hpp"
@@ -35,14 +33,6 @@ using namespace std::string_literals;
using namespace std::string_view_literals;
namespace {
ImVec2 TouchEventToScreenPos(const SDL_TouchFingerEvent& touch) {
const AuroraWindowSize size = aurora::window::get_window_size();
return ImVec2{
touch.x * static_cast<float>(size.width),
touch.y * static_cast<float>(size.height),
};
}
ImGuiWindow* FindDragScrollWindow(ImGuiWindow* window) {
while (window != nullptr) {
const bool canScrollX = window->ScrollMax.x > 0.0f;
@@ -241,48 +231,7 @@ namespace dusk {
ImGuiConsole::ImGuiConsole() {}
void ImGuiConsole::HandleSDLEvent(const SDL_Event& event) {
if (!IsGameLaunched) {
return;
}
switch (event.type) {
case SDL_EVENT_FINGER_DOWN:
if (!m_touchTapActive) {
m_touchTapActive = true;
m_touchTapMoved = false;
m_touchTapFingerId = event.tfinger.fingerID;
m_touchTapStartPos = TouchEventToScreenPos(event.tfinger);
}
break;
case SDL_EVENT_FINGER_MOTION:
if (m_touchTapActive && m_touchTapFingerId == event.tfinger.fingerID) {
const auto currentPos = TouchEventToScreenPos(event.tfinger);
const auto delta = currentPos - m_touchTapStartPos;
if (ImLengthSqr(delta) > 144.0f) {
m_touchTapMoved = true;
}
}
break;
case SDL_EVENT_FINGER_UP:
if (m_touchTapActive && m_touchTapFingerId == event.tfinger.fingerID) {
const bool shouldToggle =
!m_touchTapMoved && (m_isHidden || !ImGui::GetIO().WantCaptureMouse);
m_touchTapActive = false;
m_touchTapMoved = false;
if (shouldToggle) {
m_isHidden = !m_isHidden;
getSettings().backend.duskMenuOpen.setValue(!m_isHidden);
Save();
}
}
break;
case SDL_EVENT_FINGER_CANCELED:
m_touchTapActive = false;
m_touchTapMoved = false;
break;
default:
break;
}
(void)event;
}
void ImGuiConsole::UpdateSettings() {
@@ -324,15 +273,10 @@ namespace dusk {
// m_preLaunchWindow.draw();
// }
m_isHidden = !getSettings().backend.duskMenuOpen;
if (ImGui::GetIO().KeyShift && ImGui::IsKeyPressed(ImGuiKey_F1)) {
m_isHidden = !m_isHidden;
}
bool showMenu = !m_isHidden;
if (getSettings().backend.duskMenuOpen != showMenu) {
getSettings().backend.duskMenuOpen.setValue(showMenu);
Save();
}
// The menu bar renders with ImGuiCol_WindowBg behind it. We just want ImGuiCol_MenuBarBg,
// so make the window bg fully transparent temporarily
@@ -362,7 +306,7 @@ namespace dusk {
if (dusk::IsGameLaunched && !m_isLaunchInitialized) {
AddToast(ImGui::GetIO().MouseSource == ImGuiMouseSource_TouchScreen ?
"Tap to toggle menu"s :
"3-finger tap to toggle menu"s :
"Press F1 to toggle menu"s,
4.f);
m_isLaunchInitialized = true;
-5
View File
@@ -6,7 +6,6 @@
#include <string_view>
#include <aurora/aurora.h>
#include <SDL3/SDL_touch.h>
#include "ImGuiFirstRunPreset.hpp"
#include "ImGuiMenuGame.hpp"
@@ -42,10 +41,6 @@ private:
bool m_isHidden = true;
bool m_isLaunchInitialized = false;
bool m_touchTapActive = false;
bool m_touchTapMoved = false;
SDL_FingerID m_touchTapFingerId = 0;
ImVec2 m_touchTapStartPos = {};
ImGuiWindow* m_dragScrollWindow = nullptr;
ImVec2 m_dragScrollLastMousePos = {};
std::deque<Toast> m_toasts;
-2
View File
@@ -113,7 +113,6 @@ UserSettings g_userSettings = {
.showPipelineCompilation {"backend.showPipelineCompilation", false},
.wasPresetChosen {"backend.wasPresetChosen", false},
.enableCrashReporting {"backend.enableCrashReporting", true},
.duskMenuOpen {"backend.duskMenuOpen", false},
.cardFileType {"backend.cardFileType", static_cast<int>(CARD_GCIFOLDER)}
}
};
@@ -210,7 +209,6 @@ void registerSettings() {
Register(g_userSettings.backend.showPipelineCompilation);
Register(g_userSettings.backend.wasPresetChosen);
Register(g_userSettings.backend.enableCrashReporting);
Register(g_userSettings.backend.duskMenuOpen);
Register(g_userSettings.backend.cardFileType);
}
+163 -5
View File
@@ -5,6 +5,7 @@
#include <RmlUi/Core.h>
#include <SDL3/SDL_gamepad.h>
#include <SDL3/SDL_timer.h>
#include <SDL3/SDL_touch.h>
#include <aurora/rmlui.hpp>
#include <dolphin/pad.h>
@@ -22,6 +23,10 @@ constexpr double kGamepadMenuChordGraceDuration = 0.12;
constexpr Sint16 kGamepadAxisPressThreshold = 16384;
constexpr Sint16 kGamepadAxisReleaseThreshold = 12000;
constexpr int kGamepadAxisDirectionCount = SDL_GAMEPAD_AXIS_COUNT * 2;
constexpr int kMenuTapFingerCount = 3;
constexpr float kMenuTapMoveThreshold = 12.0f;
constexpr double kMenuTapMaxDownSpan = 0.18;
constexpr double kMenuTapMaxDuration = 0.55;
struct GamepadRepeatState {
Rml::Input::KeyIdentifier key = Rml::Input::KI_UNKNOWN;
@@ -32,11 +37,26 @@ struct GamepadRepeatState {
bool pending = false;
};
struct TouchTapFinger {
SDL_FingerID id = 0;
Rml::Vector2f startPosition;
bool active = false;
};
struct TouchTapState {
std::array<TouchTapFinger, kMenuTapFingerCount> fingers;
int activeCount = 0;
double firstDownAt = 0.0;
bool candidate = false;
bool failed = false;
};
bool sPadInputBlocked = false;
std::array<GamepadRepeatState, SDL_GAMEPAD_BUTTON_COUNT> sGamepadButtonRepeats;
std::array<GamepadRepeatState, kGamepadAxisDirectionCount> sGamepadAxisRepeats;
std::array<u32, PAD_MAX_CONTROLLERS> sPadHoldMasks;
std::array<bool, PAD_MAX_CONTROLLERS> sMenuChordConsumed;
TouchTapState sTouchMenuTap;
double now_seconds() noexcept {
return static_cast<double>(SDL_GetTicksNS()) / 1000000000.0;
@@ -389,6 +409,133 @@ void clear_gamepad_repeats() noexcept {
sMenuChordConsumed.fill(false);
}
void reset_touch_menu_tap() noexcept {
sTouchMenuTap = {};
}
Rml::Vector2f touch_position(const SDL_TouchFingerEvent& event, Rml::Context& context) noexcept {
const auto dimensions = context.GetDimensions();
return {
event.x * static_cast<float>(dimensions.x),
event.y * static_cast<float>(dimensions.y),
};
}
TouchTapFinger* find_touch_finger(SDL_FingerID id) noexcept {
for (auto& finger : sTouchMenuTap.fingers) {
if (finger.active && finger.id == id) {
return &finger;
}
}
return nullptr;
}
TouchTapFinger* find_free_touch_finger() noexcept {
for (auto& finger : sTouchMenuTap.fingers) {
if (!finger.active) {
return &finger;
}
}
return nullptr;
}
bool touch_moved_too_far(
const TouchTapFinger& finger, Rml::Vector2f position, Rml::Context& context) noexcept {
const Rml::Vector2f delta = position - finger.startPosition;
const float threshold =
kMenuTapMoveThreshold * std::max(context.GetDensityIndependentPixelRatio(), 1.0f);
return delta.SquaredMagnitude() > threshold * threshold;
}
void dispatch_menu_key(Rml::Context& context) noexcept {
context.ProcessMouseLeave();
context.ProcessKeyDown(Rml::Input::KI_F1, 0);
context.ProcessKeyUp(Rml::Input::KI_F1, 0);
}
bool handle_touch_menu_tap(Rml::Context& context, const SDL_Event& event) noexcept {
switch (event.type) {
case SDL_EVENT_FINGER_DOWN: {
const double now = now_seconds();
if (sTouchMenuTap.activeCount == 0) {
reset_touch_menu_tap();
sTouchMenuTap.firstDownAt = now;
}
if (sTouchMenuTap.candidate || sTouchMenuTap.activeCount >= kMenuTapFingerCount ||
find_touch_finger(event.tfinger.fingerID) != nullptr)
{
sTouchMenuTap.failed = true;
return false;
}
auto* finger = find_free_touch_finger();
if (finger == nullptr) {
sTouchMenuTap.failed = true;
return false;
}
*finger = TouchTapFinger{
.id = event.tfinger.fingerID,
.startPosition = touch_position(event.tfinger, context),
.active = true,
};
sTouchMenuTap.activeCount++;
if (now - sTouchMenuTap.firstDownAt > kMenuTapMaxDownSpan) {
sTouchMenuTap.failed = true;
}
if (sTouchMenuTap.activeCount == kMenuTapFingerCount) {
sTouchMenuTap.candidate = true;
}
return false;
}
case SDL_EVENT_FINGER_MOTION: {
auto* finger = find_touch_finger(event.tfinger.fingerID);
if (finger == nullptr) {
return false;
}
if (touch_moved_too_far(*finger, touch_position(event.tfinger, context), context)) {
sTouchMenuTap.failed = true;
}
return false;
}
case SDL_EVENT_FINGER_UP: {
auto* finger = find_touch_finger(event.tfinger.fingerID);
if (finger == nullptr) {
return false;
}
const double now = now_seconds();
if (!sTouchMenuTap.candidate ||
touch_moved_too_far(*finger, touch_position(event.tfinger, context), context))
{
sTouchMenuTap.failed = true;
}
*finger = {};
sTouchMenuTap.activeCount--;
if (sTouchMenuTap.activeCount > 0) {
return false;
}
const bool shouldDispatch = sTouchMenuTap.candidate && !sTouchMenuTap.failed &&
now - sTouchMenuTap.firstDownAt <= kMenuTapMaxDuration;
reset_touch_menu_tap();
if (shouldDispatch) {
dispatch_menu_key(context);
return true;
}
return false;
}
case SDL_EVENT_FINGER_CANCELED:
reset_touch_menu_tap();
return false;
default:
return false;
}
}
void begin_gamepad_key(GamepadRepeatState& repeat, Rml::Input::KeyIdentifier key) noexcept {
if (repeat.held) {
return;
@@ -530,6 +677,7 @@ void release_input_block() noexcept {
void reset_input_state() noexcept {
clear_gamepad_repeats();
reset_touch_menu_tap();
}
void handle_event(const SDL_Event& event) noexcept {
@@ -541,17 +689,27 @@ void handle_event(const SDL_Event& event) noexcept {
}
}
dispatch_controller_change_event(event);
if (event.type != SDL_EVENT_GAMEPAD_BUTTON_DOWN && event.type != SDL_EVENT_GAMEPAD_BUTTON_UP &&
event.type != SDL_EVENT_GAMEPAD_AXIS_MOTION)
{
return;
}
auto* context = aurora::rmlui::get_context();
if (context == nullptr) {
return;
}
if (event.type == SDL_EVENT_FINGER_DOWN || event.type == SDL_EVENT_FINGER_MOTION ||
event.type == SDL_EVENT_FINGER_UP || event.type == SDL_EVENT_FINGER_CANCELED)
{
if (handle_touch_menu_tap(*context, event)) {
sync_input_block();
}
return;
}
if (event.type != SDL_EVENT_GAMEPAD_BUTTON_DOWN && event.type != SDL_EVENT_GAMEPAD_BUTTON_UP &&
event.type != SDL_EVENT_GAMEPAD_AXIS_MOTION)
{
return;
}
if (event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION) {
process_axis_direction(*context, event.gaxis, AXIS_SIGN_POSITIVE);
process_axis_direction(*context, event.gaxis, AXIS_SIGN_NEGATIVE);