diff --git a/docs/architecture/07-cursor.md b/docs/architecture/07-cursor.md index 6230888a..8f1c47ea 100644 --- a/docs/architecture/07-cursor.md +++ b/docs/architecture/07-cursor.md @@ -123,6 +123,10 @@ slightly during zoom ramps, nearest keeps it pixelated and steady. Pure rules: cursor event back on DWM's (a 5-10 px shift at ~5x that snapped back when the zoom stopped). While DWM centres, every level write is followed by a pixel-and-back nudge, so DWM re-centres by its own rule in the same frame (`NudgeAfterLevelWrite`). Field-verified: shift gone, pans steady. +- **No write without its nudge (#381).** The nudge is skipped while a mouse button is held (it made + some clicks fail), so a pan write during a held click would leave Wind's centre on screen until the + next cursor event: drag-selects and held clicks shook. Pan-only writes are held for the click + window instead (`HoldWriteForClick`); level changes and forced writes still go out. - `txDwmCentre=0` keeps the native pointer but lets Wind write the view itself (kill switch; pans then wobble by speed x tick x zoom). - **DWM's learned offset.** DWM learns the gap between `GetCursorPos` and its own cursor point and diff --git a/src/native_cursor.h b/src/native_cursor.h index bf36d67c..ea0e2daf 100644 --- a/src/native_cursor.h +++ b/src/native_cursor.h @@ -96,4 +96,16 @@ inline bool NudgeAfterWrite(bool dwmCentring, bool wrote) { return dwmCentring && wrote; } +// NO WRITE WITHOUT ITS NUDGE (issue #381, field report 2026-10-08: zoomed, holding the left button +// for a drag-select or a held click made the view shake; plain panning was fine). The nudge is +// skipped while a click is in progress (it made some clicks fail), but the write it belongs to +// still went out, so during a hold every changed tick put Wind's centre on screen and the next +// cursor event put DWM's back: the view alternated between the two. While DWM centres, a pan-only +// write is held for the click instead; DWM keeps centring on every cursor event, and the first tick +// after the click window writes and nudges as usual. Level changes and forced writes still go out +// (a zoom during a drag must not freeze, and a centring switch needs its write). +inline bool HoldWriteForClick(bool dwmCentring, bool clickInProgress, bool levelMoved, bool forced) { + return dwmCentring && clickInProgress && !levelMoved && !forced; +} + } // namespace wind diff --git a/src/transform_model.cpp b/src/transform_model.cpp index 7af7bbff..f93f96cf 100644 --- a/src/transform_model.cpp +++ b/src/transform_model.cpp @@ -815,7 +815,10 @@ void TransformModel::present(const MapResult& r, double level, const Config& cfg ci.minOffsetPx = cfg.txMinOffsetPx; ci.settleMs = kSettleMs; const bool forceWrite = forceWrite_; - const bool writeNow = ShouldWriteTransform(ci) || forceWrite; + // A pan write while DWM centres must be followed by its nudge, which a click in progress + // forbids: hold it until the click window ends (HoldWriteForClick, issue #381). + const bool clickHold = HoldWriteForClick(dwmCentreOn_, ClickInProgress(), ci.levelMoved, forceWrite); + const bool writeNow = (ShouldWriteTransform(ci) || forceWrite) && !clickHold; if (writeNow) { lastOffX_ = m.offX; lastOffY_ = m.offY; lastTxX_ = m.txX; lastTxY_ = m.txY; diff --git a/tests/test_native_cursor.cpp b/tests/test_native_cursor.cpp index 39d08d45..63ec2720 100644 --- a/tests/test_native_cursor.cpp +++ b/tests/test_native_cursor.cpp @@ -64,6 +64,14 @@ TEST_CASE("while DWM centres, every write is followed by a cursor event (one cen CHECK(NudgeAfterWrite(true, false) == false); // nothing written } +TEST_CASE("A pan write is held while a click forbids its nudge (#381)") { + CHECK(HoldWriteForClick(true, true, false, false) == true); // held button, DWM centres: hold + CHECK(HoldWriteForClick(true, false, false, false) == false); // no click: write and nudge + CHECK(HoldWriteForClick(false, true, false, false) == false); // Wind owns the view: write + CHECK(HoldWriteForClick(true, true, true, false) == false); // a zoom during a drag still writes + CHECK(HoldWriteForClick(true, true, false, true) == false); // a forced write (centring switch) +} + TEST_CASE("DWM centring yields only near an armed wall, not everywhere it is reachable") { // 12x: wall at 32000/12 = 2666.7 source px. CHECK(NearWall(true, 1000.0, 500.0, 12.0, 32000.0, 64.0) == false); // middle of the screen