From ce78231d9b2c8e8986f75a9ef81806f04f93bae5 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Sat, 21 Mar 2026 08:53:41 -0700
Subject: [PATCH] Gamepad mouse mode
---
screenlib/SDL_FrameBuf.cpp | 193 +++++++++++++++++++++++++++++++++++--
screenlib/SDL_FrameBuf.h | 13 +++
screenlib/UIManager.cpp | 6 ++
3 files changed, 206 insertions(+), 6 deletions(-)
diff --git a/screenlib/SDL_FrameBuf.cpp b/screenlib/SDL_FrameBuf.cpp
index 5cffc523..0fcfe5af 100644
--- a/screenlib/SDL_FrameBuf.cpp
+++ b/screenlib/SDL_FrameBuf.cpp
@@ -25,12 +25,6 @@
#include "SDL_FrameBuf.h"
-#define LOWER_PREC(X) ((X)/16) /* Lower the precision of a value */
-#define RAISE_PREC(X) ((X)/16) /* Raise the precision of a value */
-
-#define MIN(A, B) ((A < B) ? A : B)
-#define MAX(A, B) ((A > B) ? A : B)
-
/* Constructors cannot fail. :-/ */
FrameBuf::FrameBuf() : ErrorBase()
{
@@ -98,6 +92,9 @@ FrameBuf::Init(int width, int height, Uint32 window_flags, const char *title, SD
FrameBuf::~FrameBuf()
{
+ for (unsigned int i = 0; i < m_gamepads.length(); ++i) {
+ SDL_CloseGamepad(m_gamepads[i]);
+ }
if (m_target) {
SDL_DestroyTexture(m_target);
}
@@ -113,6 +110,188 @@ void
FrameBuf::ProcessEvent(SDL_Event *event)
{
SDL_ConvertEventToRenderCoordinates(m_renderer, event);
+ ProcessGamepadEvent(event);
+}
+
+void
+FrameBuf::OpenGamepad(SDL_JoystickID id)
+{
+ SDL_Gamepad *gamepad = SDL_OpenGamepad(id);
+ if (gamepad) {
+ m_gamepads.add(gamepad);
+ }
+}
+
+void
+FrameBuf::CloseGamepad(SDL_JoystickID id)
+{
+ for (unsigned int i = 0; i < m_gamepads.length(); ++i) {
+ SDL_Gamepad *gamepad = m_gamepads[i];
+ if (SDL_GetGamepadID(gamepad) == id) {
+ SDL_CloseGamepad(gamepad);
+ m_gamepads.removeAt(i);
+ break;
+ }
+ }
+}
+
+void
+FrameBuf::ProcessGamepadEvent(SDL_Event *event)
+{
+ SDL_Gamepad *gamepad = nullptr;
+
+ if (!GamepadMouseEnabled()) {
+ return;
+ }
+
+ const int DEADZONE = 8000;
+ switch (event->type) {
+ case SDL_EVENT_GAMEPAD_ADDED:
+ OpenGamepad(event->gdevice.which);
+ break;
+ case SDL_EVENT_GAMEPAD_REMOVED:
+ CloseGamepad(event->gdevice.which);
+ break;
+ case SDL_EVENT_GAMEPAD_AXIS_MOTION:
+ if (event->gaxis.axis == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) {
+ if (event->gaxis.value > 0) {
+ if (!m_gamepadMouseDown) {
+ SDL_Event mouse_event;
+ SDL_zero(mouse_event);
+ mouse_event.type = SDL_EVENT_MOUSE_BUTTON_DOWN;
+ mouse_event.button.windowID = SDL_GetWindowID(m_window);
+ mouse_event.button.button = SDL_BUTTON_LEFT;
+ mouse_event.button.down = true;
+ SDL_GetMouseState(&mouse_event.button.x, &mouse_event.button.y);
+ SDL_PushEvent(&mouse_event);
+ m_gamepadMouseDown = true;
+ }
+ } else {
+ if (m_gamepadMouseDown) {
+ SDL_Event mouse_event;
+ SDL_zero(mouse_event);
+ mouse_event.type = SDL_EVENT_MOUSE_BUTTON_UP;
+ mouse_event.button.windowID = SDL_GetWindowID(m_window);
+ mouse_event.button.button = SDL_BUTTON_LEFT;
+ mouse_event.button.down = false;
+ SDL_GetMouseState(&mouse_event.button.x, &mouse_event.button.y);
+ SDL_PushEvent(&mouse_event);
+ m_gamepadMouseDown = false;
+ }
+ }
+ }
+ break;
+ case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
+ case SDL_EVENT_GAMEPAD_BUTTON_UP:
+ gamepad = SDL_OpenGamepad(event->gbutton.which);
+ if (gamepad) {
+ SDL_Keycode key = SDLK_UNKNOWN;
+ switch (SDL_GetGamepadButtonLabel(gamepad, (SDL_GamepadButton)event->gbutton.button)) {
+ case SDL_GAMEPAD_BUTTON_LABEL_A:
+ case SDL_GAMEPAD_BUTTON_LABEL_CROSS:
+ key = SDLK_RETURN;
+ break;
+ case SDL_GAMEPAD_BUTTON_LABEL_B:
+ case SDL_GAMEPAD_BUTTON_LABEL_CIRCLE:
+ key = SDLK_ESCAPE;
+ break;
+ default:
+ break;
+ }
+ if (key != SDLK_UNKNOWN) {
+ SDL_Event keyboard_event;
+ SDL_zero(keyboard_event);
+ bool down = (event->type == SDL_EVENT_GAMEPAD_BUTTON_DOWN);
+ keyboard_event.type = down ? SDL_EVENT_KEY_DOWN : SDL_EVENT_KEY_UP;
+ keyboard_event.key.windowID = SDL_GetWindowID(m_window);
+ keyboard_event.key.key = key;
+ keyboard_event.key.down = down;
+ SDL_PushEvent(&keyboard_event);
+ }
+ SDL_CloseGamepad(gamepad);
+ }
+ break;
+ default:
+ break;
+ }
+}
+
+void
+FrameBuf::SetGamepadMouse(bool enabled)
+{
+ if (enabled) {
+ ++m_gamepadMouse;
+ } else {
+ --m_gamepadMouse;
+ SDL_assert(m_gamepadMouse >= 0);
+ }
+
+ if (!GamepadMouseEnabled()) {
+ m_mouseRemainderX = 0.0f;
+ m_mouseRemainderY = 0.0f;
+ }
+}
+
+static float VectorLengthSquared(const SDL_FPoint &point)
+{
+ return (point.x * point.x + point.y * point.y);
+}
+
+void
+FrameBuf::UpdateGamepadMouseMovement()
+{
+ const float MOUSE_SPEED = 16.0f;
+ SDL_FPoint velocity = { 0.0f, 0.0f };
+ SDL_FPoint mouse;
+ SDL_FPoint delta;
+
+ if (!GamepadMouseEnabled()) {
+ return;
+ }
+
+ // Get the thumbstick with the most deflection
+ const int DEADZONE = 8000;
+ float max_length = 0.0f;
+ for (unsigned int i = 0; i < m_gamepads.length(); ++i) {
+ SDL_Gamepad *gamepad = m_gamepads[i];
+ SDL_FPoint axis;
+ axis.x = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTX);
+ axis.y = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY);
+
+ float length = VectorLengthSquared(axis);
+ if (length > max_length) {
+ velocity = axis;
+ max_length = length;
+ }
+ }
+ if (max_length <= (DEADZONE * DEADZONE)) {
+ return;
+ }
+
+ // Normalize the velocity
+ float length = SDL_sqrtf(VectorLengthSquared(velocity));
+ velocity.x /= length;
+ velocity.y /= length;
+
+ // Remove the deadzone and scale to 1.0
+ float scale = (length - DEADZONE) / (SDL_JOYSTICK_AXIS_MAX - DEADZONE);
+ velocity.x *= scale;
+ velocity.y *= scale;
+
+ SDL_GetMouseState(&mouse.x, &mouse.y);
+ SDL_RenderCoordinatesFromWindow(m_renderer, mouse.x, mouse.y, &mouse.x, &mouse.y);
+
+ delta.x = (velocity.x * MOUSE_SPEED) + m_mouseRemainderX;
+ delta.y = (velocity.y * MOUSE_SPEED) + m_mouseRemainderY;
+ m_mouseRemainderX = SDL_modff(delta.x, &delta.x);
+ m_mouseRemainderY = SDL_modff(delta.y, &delta.y);
+
+ mouse.x += delta.x;
+ mouse.x = SDL_clamp(mouse.x, 0.0f, (float)m_width);
+ mouse.y += delta.y;
+ mouse.y = SDL_clamp(mouse.y, 0.0f, (float)m_height);
+ SDL_RenderCoordinatesToWindow(m_renderer, mouse.x, mouse.y, &mouse.x, &mouse.y);
+ SDL_WarpMouseInWindow(m_window, mouse.x, mouse.y);
}
bool
@@ -208,6 +387,8 @@ FrameBuf::StretchBlit(const SDL_Rect *_dstrect, SDL_Texture *src, const SDL_Rect
void
FrameBuf::Update(void)
{
+ UpdateGamepadMouseMovement();
+
if (Fading() || m_faded) {
return;
}
diff --git a/screenlib/SDL_FrameBuf.h b/screenlib/SDL_FrameBuf.h
index 24fd152a..c181bfc2 100644
--- a/screenlib/SDL_FrameBuf.h
+++ b/screenlib/SDL_FrameBuf.h
@@ -34,6 +34,7 @@
#include <stdio.h>
#include <SDL3/SDL.h>
+#include "../utils/array.h"
#include "../utils/ErrorBase.h"
typedef enum {
@@ -73,6 +74,8 @@ class FrameBuf : public ErrorBase {
void EnableTextInput();
void DisableTextInput();
+ void SetGamepadMouse(bool enabled);
+ bool GamepadMouseEnabled() { return (m_gamepadMouse > 0); }
void ToggleFullScreen(void) {
if (SDL_GetWindowFlags(m_window) & SDL_WINDOW_FULLSCREEN) {
@@ -198,6 +201,11 @@ class FrameBuf : public ErrorBase {
SDL_FRect m_clip;
int m_width;
int m_height;
+ int m_gamepadMouse = 0;
+ float m_mouseRemainderX = 0.0f;
+ float m_mouseRemainderY = 0.0f;
+ bool m_gamepadMouseDown = false;
+ array<SDL_Gamepad *> m_gamepads;
void UpdateDrawColor(Uint32 color) {
Uint8 r, g, b;
@@ -206,6 +214,11 @@ class FrameBuf : public ErrorBase {
b = (color >> 0) & 0xFF;
SDL_SetRenderDrawColor(m_renderer, r, g, b, 0xFF);
}
+
+ void OpenGamepad(SDL_JoystickID id);
+ void CloseGamepad(SDL_JoystickID id);
+ void ProcessGamepadEvent(SDL_Event *event);
+ void UpdateGamepadMouseMovement();
};
#endif /* _SDL_FrameBuf_h */
diff --git a/screenlib/UIManager.cpp b/screenlib/UIManager.cpp
index a00aa5d1..6d33c860 100644
--- a/screenlib/UIManager.cpp
+++ b/screenlib/UIManager.cpp
@@ -269,6 +269,9 @@ UIManager::ShowPanel(UIPanel *panel)
if (!panel->IsCursorVisible()) {
m_screen->HideCursor();
}
+ if (panel->IsCursorVisible()) {
+ m_screen->SetGamepadMouse(true);
+ }
}
}
@@ -285,6 +288,9 @@ UIManager::HidePanel(UIPanel *panel)
if (!panel->IsCursorVisible()) {
m_screen->ShowCursor();
}
+ if (panel->IsCursorVisible()) {
+ m_screen->SetGamepadMouse(false);
+ }
#ifdef FAST_ITERATION
// This is useful for iteration, panels are reloaded