Reconcile a LEFT release missed while unfocused on focus regain

Non-Windows only reconciled while focused AND captured; focus loss drops capture, so a release outside the window left activation_down_ stuck. Decision moved to LocalMousePolicy::sync_left (tested).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit f012cf1693)
This commit is contained in:
Matthew Stanley
2026-10-04 10:52:32 -07:00
parent 93371b1782
commit f7ca7237cd
4 changed files with 34 additions and 7 deletions
+4
View File
@@ -149,6 +149,10 @@ bool LocalMousePolicy::control(uint64_t t, uint64_t now, MouseControl control,
}
return false;
}
void LocalMousePolicy::sync_left(uint64_t now, bool physically_down) {
if (!physically_down && activation_down_)
control(0, now, MouseControl::Left, false, false);
}
void LocalMousePolicy::motion(uint64_t t, uint64_t now, double dx, double dy) {
if (!captured_) return;
if (!allowed() || !std::isfinite(dx) || !std::isfinite(dy) ||
+2
View File
@@ -25,6 +25,8 @@ public:
void reset();
void update(uint64_t now, LocalMouseHost host);
bool control(uint64_t t, uint64_t now, MouseControl control, bool down, bool repeat);
// Reconcile a LEFT release the host never delivered (e.g. made while unfocused).
void sync_left(uint64_t now, bool physically_down);
void motion(uint64_t t, uint64_t now, double dx, double dy);
void sample(uint64_t now, uint8_t& rx, uint8_t& ry);
bool captured() const { return captured_; }
+7 -7
View File
@@ -105,16 +105,16 @@ void psx_local_mouse_begin(SDL_Window* window, bool 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 catch a lost release only while focused and captured. Neither path
// manufactures a down or unblocks a held activation across focus loss.
// 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_released = !(SDL_GetGlobalMouseState(nullptr, nullptr) & SDL_BUTTON(SDL_BUTTON_LEFT));
const bool left_down = (SDL_GetGlobalMouseState(nullptr, nullptr) & SDL_BUTTON(SDL_BUTTON_LEFT)) != 0;
state().sync_left(now_ns(), left_down);
#else
const bool left_released = host_.focused && state().captured() &&
!(SDL_GetMouseState(nullptr, nullptr) & SDL_BUTTON(SDL_BUTTON_LEFT));
if (host_.focused)
state().sync_left(now_ns(), (SDL_GetMouseState(nullptr, nullptr) & SDL_BUTTON(SDL_BUTTON_LEFT)) != 0);
#endif
if (left_released)
state().control(0, now_ns(), psx::MouseControl::Left, false, false);
// Catch a lost hold release, including one made outside the game window.
const auto hold = state().hold_control();
bool down = false;
+21
View File
@@ -240,6 +240,27 @@ int main() {
batch.sample(33*Ms,x,y);CHECK(x==128 && y==128);batch.clear();
}
// LEFT released while unfocused is never delivered; reconcile on regain so
// the next click activates. A still-held button is not a new activation.
{
psx::LocalMousePolicy lost({nullptr,capture,suppress,notice});
CHECK(lost.install(&callbacks));
const uint64_t base=9000*Ms;
lost.update(base,host);
CHECK(lost.control(base,base,psx::MouseControl::Left,true,false) && lost.captured());
auto away=host;away.focused=false;
lost.update(base+10*Ms,away);CHECK(!lost.captured());
lost.update(base+20*Ms,host);
lost.sync_left(base+20*Ms,true);
CHECK(!lost.control(base+21*Ms,base+21*Ms,psx::MouseControl::Left,true,false));
lost.sync_left(base+22*Ms,false);
lost.update(base+23*Ms,host);
CHECK(lost.control(base+23*Ms,base+23*Ms,psx::MouseControl::Left,true,false));
CHECK(lost.captured());
lost.sync_left(base+24*Ms,true);CHECK(lost.captured()); // down: no-op
lost.clear();
}
policy.reset();host.hold_conflict=false;capture_ok=false;CHECK(acquire());CHECK(!policy.captured());
const unsigned failure_notices=notices;CHECK(acquire());CHECK(notices==failure_notices);
policy.clear(); CHECK(!policy.installed() && !policy.captured());