Files
psxrecomp/runtime/src/local_mouse_sdl.cpp
T

218 lines
9.1 KiB
C++

#include "local_mouse_sdl.h"
#include "local_mouse_policy.h"
#include "psx_keybinds.h"
#include "host_osd.h"
#include <cstdio>
namespace {
SDL_Window* window_ = nullptr;
psx::LocalMouseHost host_;
uint64_t now_ns() {
#if defined(PSX_SDL3)
return SDL_GetTicksNS();
#else
#if SDL_VERSION_ATLEAST(2, 0, 18)
return SDL_GetTicks64() * 1000000;
#else
// Preserve the framework's older SDL2 build path without requiring a
// new dependency. This owner-thread clock extends the 32-bit tick era.
static uint32_t previous = 0;
static uint64_t era = 0;
const uint32_t ticks = SDL_GetTicks();
if (ticks < previous) era += uint64_t(1) << 32;
previous = ticks;
return (era + ticks) * 1000000;
#endif
#endif
}
uint64_t event_ns(const SDL_Event& event, uint64_t now) {
#if defined(PSX_SDL3)
return event.common.timestamp;
#else
// Lift wrapping SDL2 millisecond timestamps into the nearest current era.
const uint64_t ms = now / 1000000;
const int32_t delta = static_cast<int32_t>(event.common.timestamp - uint32_t(ms));
if (delta < 0 && uint64_t(-int64_t(delta)) > ms) return UINT64_MAX;
return uint64_t(int64_t(ms) + delta) * 1000000;
#endif
}
SDL_Scancode scancode(psx::MouseControl control) {
if (control == psx::MouseControl::Escape) return SDL_SCANCODE_ESCAPE;
if (control == psx::MouseControl::LeftAlt) return SDL_SCANCODE_LALT;
if (control >= psx::MouseControl::Left && control <= psx::MouseControl::X2)
return PSXKB_MOUSE_SC(int(control));
return SDL_SCANCODE_UNKNOWN;
}
bool capture(void*, bool on) {
if (!window_) return !on;
#if defined(PSX_SDL3)
return SDL_SetWindowRelativeMouseMode(window_, on);
#else
return SDL_SetRelativeMouseMode(on ? SDL_TRUE : SDL_FALSE) == 0;
#endif
}
void suppress(void*, psx::MouseControl control) {
psx_keybinds_suppress_control(1, scancode(control));
}
void notice(void*, const char* message) { host_osd_push(message, 2000); }
psx::LocalMousePolicy& state() {
static psx::LocalMousePolicy policy({nullptr, capture, suppress, notice});
return policy;
}
bool focus() {
if (!window_ || SDL_GetKeyboardFocus() != window_) return false;
const auto flags = SDL_GetWindowFlags(window_);
return (flags & (SDL_WINDOW_HIDDEN | SDL_WINDOW_MINIMIZED)) == 0;
}
bool conflict() {
const SDL_Scancode sc = scancode(state().hold_control());
if (sc == SDL_SCANCODE_UNKNOWN) return false;
for (int i = 0; i < PSX_KB_COUNT; ++i)
if (psx_keybinds_get_button(1, i) == sc || psx_keybinds_get_button_alt(1, i) == sc)
return true;
return false;
}
void update(uint64_t now) {
host_.focused = focus();
host_.hold_conflict = conflict();
state().update(now, host_);
}
bool control(uint64_t t, uint64_t now, psx::MouseControl control, bool down, bool repeat) {
const SDL_Scancode sc = scancode(control);
const bool swallowed = psx_keybinds_control_suppressed(1, sc) != 0;
if (!down) psx_keybinds_release_control(1, sc);
return state().control(t, now, control, down, repeat) || swallowed;
}
} // namespace
extern "C" int psx_mod_set_local_mouse_policy(const PSXModMousePolicy* policy) {
if (state().install(policy)) return 1;
std::fprintf(stderr, "psxrecomp: local mouse policy rejected (null/invalid, "
"struct_size %u expected %u, or one is already registered)\n",
policy ? (unsigned)policy->struct_size : 0u,
(unsigned)sizeof(PSXModMousePolicy));
return 0;
}
extern "C" int psx_mod_set_local_mouse_persistent_capture(int enabled) {
return (enabled == 0 || enabled == 1) && state().persistent_capture(enabled != 0);
}
extern "C" int psx_mod_set_local_mouse_capture_across_transitions(int enabled) {
return (enabled == 0 || enabled == 1) && state().capture_across_transitions(enabled != 0);
}
void psx_local_mouse_clear() { state().clear(); host_ = {}; window_ = nullptr; }
bool psx_local_mouse_installed() { return state().installed(); }
void psx_local_mouse_reset() {
state().reset();
host_.connected = host_.analog = false;
}
void psx_local_mouse_begin(SDL_Window* window, bool live) {
// No policy: no host state to track, no per-frame global mouse polling.
if (!state().installed()) return;
if (window_ && window_ != window) state().reset();
window_ = window;
host_.live = live;
update(now_ns());
// Windows global state survives focus clearing SDL's cached window state.
// Other backends may not support global mouse polling (e.g. Wayland), so
// there reconcile whenever focused (captured or not), so a release made
// while unfocused is caught on focus regain. Neither path manufactures a
// down or unblocks a held activation across focus loss.
#if defined(_WIN32)
const bool left_down = (SDL_GetGlobalMouseState(nullptr, nullptr) & SDL_BUTTON(SDL_BUTTON_LEFT)) != 0;
state().sync_left(now_ns(), left_down);
#else
if (host_.focused)
state().sync_left(now_ns(), (SDL_GetMouseState(nullptr, nullptr) & SDL_BUTTON(SDL_BUTTON_LEFT)) != 0);
#endif
// Catch a lost hold release, including one made outside the game window.
const auto hold = state().hold_control();
bool down = false;
if (hold == psx::MouseControl::Right)
down = (SDL_GetMouseState(nullptr, nullptr) & SDL_BUTTON(SDL_BUTTON_RIGHT)) != 0;
else if (hold == psx::MouseControl::LeftAlt)
down = SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_LALT] != 0;
if (hold != psx::MouseControl::None && !down)
state().control(0, now_ns(), hold, false, false);
}
bool psx_local_mouse_event(const SDL_Event& ev) {
if (!state().installed()) {
// Ownership of an already consumed source lasts through release even
// when its plugin has just been disabled/unregistered.
SDL_Scancode sc = SDL_SCANCODE_UNKNOWN;
bool down = false;
if (ev.type == SDL_MOUSEBUTTONDOWN || ev.type == SDL_MOUSEBUTTONUP) {
if (ev.button.button >= 1 && ev.button.button <= 5)
sc = PSXKB_MOUSE_SC(ev.button.button);
down = ev.type == SDL_MOUSEBUTTONDOWN;
} else if (ev.type == SDL_KEYDOWN || ev.type == SDL_KEYUP) {
#if defined(PSX_SDL3)
sc = ev.key.scancode;
#else
sc = ev.key.keysym.scancode;
#endif
down = ev.type == SDL_KEYDOWN;
}
const bool swallowed = psx_keybinds_control_suppressed(1, sc) != 0;
if (!down) psx_keybinds_release_control(1, sc);
return swallowed;
}
const uint64_t now = now_ns(), t = event_ns(ev, now);
update(now);
#if defined(PSX_SDL3)
if (ev.type == SDL_EVENT_WINDOW_FOCUS_LOST || ev.type == SDL_EVENT_WINDOW_HIDDEN ||
ev.type == SDL_EVENT_WINDOW_MINIMIZED) {
if (window_ && ev.window.windowID == SDL_GetWindowID(window_)) state().reset();
} else if (ev.type == SDL_EVENT_MOUSE_REMOVED) {
state().reset();
}
#else
if (window_ && ev.type == SDL_WINDOWEVENT && ev.window.windowID == SDL_GetWindowID(window_) &&
(ev.window.event == SDL_WINDOWEVENT_FOCUS_LOST || ev.window.event == SDL_WINDOWEVENT_HIDDEN ||
ev.window.event == SDL_WINDOWEVENT_MINIMIZED)) state().reset();
#endif
if (ev.type == SDL_MOUSEMOTION) {
if (!window_ || ev.motion.windowID != SDL_GetWindowID(window_)) return false;
if (ev.motion.which == SDL_TOUCH_MOUSEID) return false;
#if defined(PSX_SDL3)
if (ev.motion.which == SDL_PEN_MOUSEID) return false;
#endif
state().motion(t, now, ev.motion.xrel, ev.motion.yrel);
} else if (ev.type == SDL_MOUSEBUTTONDOWN || ev.type == SDL_MOUSEBUTTONUP) {
if (ev.button.which == SDL_TOUCH_MOUSEID) return false;
#if defined(PSX_SDL3)
if (ev.button.which == SDL_PEN_MOUSEID) return false;
#endif
// An owned activation release stays immediate even outside the game
// window. Only a fresh down in this window may acquire capture.
if (!window_ || (ev.button.windowID != SDL_GetWindowID(window_) &&
ev.type != SDL_MOUSEBUTTONUP)) return false;
if (ev.button.button >= 1 && ev.button.button <= 5)
return control(t, now, psx::MouseControl(ev.button.button), ev.type == SDL_MOUSEBUTTONDOWN, false);
} else if (ev.type == SDL_KEYDOWN || ev.type == SDL_KEYUP) {
#if defined(PSX_SDL3)
if (!window_ || ev.key.windowID != SDL_GetWindowID(window_)) return false;
const auto sc = ev.key.scancode;
#else
if (!window_ || ev.key.windowID != SDL_GetWindowID(window_)) return false;
const auto sc = ev.key.keysym.scancode;
#endif
const auto c = sc == SDL_SCANCODE_ESCAPE ? psx::MouseControl::Escape :
sc == SDL_SCANCODE_LALT ? psx::MouseControl::LeftAlt : psx::MouseControl::None;
if (c != psx::MouseControl::None)
return control(t, now, c, ev.type == SDL_KEYDOWN, ev.key.repeat != 0);
}
return false;
}
void psx_local_mouse_pad(bool connected, bool analog, uint16_t buttons, uint8_t& rx, uint8_t& ry) {
if (!state().installed()) return;
host_.connected = connected;
host_.analog = analog;
host_.native_right = rx != 128 || ry != 128;
host_.start = (buttons & 0x0008) == 0;
host_.buttons = buttons;
const uint64_t now = now_ns();
update(now);
state().sample(now, rx, ry);
}