diff --git a/app/capture.js b/app/capture.js index 31e8379..5fd2701 100644 --- a/app/capture.js +++ b/app/capture.js @@ -1706,20 +1706,32 @@ public static class SFHook { * scaled away from 100% and on secondary monitors. */ osPointToDip(osPoint) { - if (this.screen && typeof this.screen.screenToDipPoint === 'function') { - try { - const dip = this.screen.screenToDipPoint(osPoint); - if (dip && Number.isFinite(dip.x) && Number.isFinite(dip.y)) return dip; - } catch { /* fall through to manual conversion */ } - } + let geometryDip = null; try { const displays = this.screen && typeof this.screen.getAllDisplays === 'function' ? this.screen.getAllDisplays() : []; - const dip = physicalToDip(osPoint, displays); - if (dip) return dip; - } catch { /* no display geometry available */ } - return osPoint; + geometryDip = physicalToDip(osPoint, displays); + } catch { + geometryDip = null; + } + if (this.screen && typeof this.screen.screenToDipPoint === 'function') { + try { + const dip = this.screen.screenToDipPoint(osPoint); + if (dip && Number.isFinite(dip.x) && Number.isFinite(dip.y)) { + if (!geometryDip) return dip; + const offByX = Math.abs(dip.x - geometryDip.x); + const offByY = Math.abs(dip.y - geometryDip.y); + // Some Windows/Electron combinations have been observed to return a + // raw physical point here. That keeps the click marker off-screen on + // scaled displays, so trust the geometry path when the two disagree + // by more than a tiny rounding margin. + if (offByX <= 1 && offByY <= 1) return dip; + return geometryDip; + } + } catch { /* fall through to manual conversion */ } + } + return geometryDip || osPoint; } /** diff --git a/tests/unit/capture.test.js b/tests/unit/capture.test.js index e2e990f..0c5e55c 100644 --- a/tests/unit/capture.test.js +++ b/tests/unit/capture.test.js @@ -736,6 +736,27 @@ test('hook coordinates are converted physical → DIP via screenToDipPoint when assert.deepEqual(seen, [{ x: 640, y: 320 }]); }); +test('bogus Windows screenToDipPoint results fall back to display geometry', () => { + const service = makeService({ + screenApi: { + screenToDipPoint: (p) => ({ x: p.x, y: p.y }), // raw physical point: wrong on scaled displays + getAllDisplays: () => [ + { id: 1, scaleFactor: 2, bounds: { x: 0, y: 0, width: 1440, height: 900 } }, + ], + getCursorScreenPoint: () => { throw new Error('must not fall back to a cursor read'); }, + }, + }); + service.session = { guideId: 'guide-dip-fallback', paused: false, count: 0, intervalSec: 0 }; + const seen = []; + service.enqueueClickCapture = (clickPos) => { + seen.push(clickPos); + }; + + service.onOsClick(1770000000000, { x: 1500, y: 900 }, 'left'); + + assert.deepEqual(seen, [{ x: 750, y: 450 }]); +}); + test('without screenToDipPoint, coordinates convert via display geometry (Linux/X11)', () => { const service = makeService({ screenApi: {