Skip to content

Commit 08ec0f0

Browse files
committed
fix: window position drift on Windows multi-monitor setups
On Windows with multiple displays, both the NethLink and PhoneIsland windows would gradually shift position each time they were shown/hidden. NethLink drifted 1-2px upward per cycle, while PhoneIsland drifted on both axes by several pixels, eventually disappearing off-screen. Root causes: - PhoneIsland: showPhoneIsland() made two partial setBounds() calls (first width/height only via resize(), then x/y only), each triggering DPI scaling rounding on Windows. Combined into a single setBounds() call with the full rectangle. - NethLink: the getBounds()/setBounds() save-restore cycle accumulated fractional pixel rounding errors from DPI conversions. Added Math.round() on all saved coordinates. - Drag handler: used getContentSize() (inner area excluding title bar and borders) but passed the result to setBounds() which expects outer window dimensions. Replaced with getBounds() for correct size values. These issues only manifested on Windows with multiple monitors due to per-monitor DPI awareness coordinate translation. macOS, Linux, and single-monitor Windows setups were unaffected.
1 parent 62c2a8d commit 08ec0f0

3 files changed

Lines changed: 75 additions & 19 deletions

File tree

src/main/classes/controllers/PhoneIslandController.ts

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,17 @@ export class PhoneIslandController {
2222
const { w, h } = size
2323
const window = this.window.getWindow()
2424
if (window) {
25-
const bounds = window.getBounds()
26-
if (bounds.height !== h || bounds.width !== w) {
27-
window.setBounds({ width: w, height: h })
25+
const currentBounds = window.getBounds()
26+
if (currentBounds.height !== h || currentBounds.width !== w) {
27+
// Use saved position as authoritative source to prevent DPI drift accumulation.
28+
// getBounds() on Windows multi-monitor returns DPI-converted values that drift
29+
// on each setBounds/getBounds roundtrip. The saved position is the user's intended
30+
// position and should always be used instead.
31+
const savedPosition = AccountController.instance.getAccountPhoneIslandPosition()
32+
const x = savedPosition ? savedPosition.x : currentBounds.x
33+
const y = savedPosition ? savedPosition.y : currentBounds.y
34+
const newBounds = { x, y, width: w, height: h }
35+
window.setBounds(newBounds)
2836
PhoneIslandWindow.currentSize = { width: w, height: h }
2937
}
3038
//make sure the size is equal to [0,0] when you want to close the phone island, otherwise the size will not close and will generate slowness problems.
@@ -48,8 +56,10 @@ export class PhoneIslandController {
4856
try {
4957
const window = this.window.getWindow()
5058
if (window) {
59+
const { w, h } = size
60+
// Target bounds to apply after show() to override any WM adjustments
61+
let targetBounds: Electron.Rectangle | undefined
5162

52-
this.resize(size)
5363
if (process.platform !== 'linux') {
5464
const phoneIslandPosition = AccountController.instance.getAccountPhoneIslandPosition()
5565
if (phoneIslandPosition) {
@@ -59,21 +69,37 @@ export class PhoneIslandController {
5969
result ||
6070
(phoneIslandPosition.x >= area.x &&
6171
phoneIslandPosition.y >= area.y &&
62-
(phoneIslandPosition.x + size.w) < (area.x + area.width) &&
63-
(phoneIslandPosition.y + size.h) < (area.y + area.height))
72+
(phoneIslandPosition.x + w) < (area.x + area.width) &&
73+
(phoneIslandPosition.y + h) < (area.y + area.height))
6474
)
6575
}, false)
6676
if (isPhoneIslandOnDisplay) {
67-
window?.setBounds({ x: phoneIslandPosition.x, y: phoneIslandPosition.y }, false)
77+
targetBounds = { x: phoneIslandPosition.x, y: phoneIslandPosition.y, width: w, height: h }
78+
window.setBounds(targetBounds, false)
79+
PhoneIslandWindow.currentSize = { width: w, height: h }
6880
} else {
69-
window?.center()
81+
this.resize(size)
82+
window.center()
7083
}
7184
}
7285
else {
73-
window?.center()
86+
this.resize(size)
87+
window.center()
7488
}
7589
} else {
76-
window?.center()
90+
this.resize(size)
91+
window.center()
92+
}
93+
94+
if (h === 0 && w === 0) {
95+
window.hide()
96+
} else if (!window.isVisible() && !this.isWarmingUp) {
97+
window.show()
98+
window.setAlwaysOnTop(true, 'screen-saver')
99+
// Re-assert position after show/setAlwaysOnTop to override any WM/DPI adjustment
100+
if (targetBounds) {
101+
window.setBounds(targetBounds, false)
102+
}
77103
}
78104
}
79105
} catch (e) {
@@ -86,10 +112,18 @@ export class PhoneIslandController {
86112
const window = this.window.getWindow()
87113
const phoneIslandBounds = window?.getBounds()
88114
if (phoneIslandBounds) {
89-
AccountController.instance.setAccountPhoneIslandPosition({
90-
x: phoneIslandBounds.x,
91-
y: phoneIslandBounds.y
92-
})
115+
const savedPosition = AccountController.instance.getAccountPhoneIslandPosition()
116+
// Only save if the user actually moved the window (not DPI rounding drift).
117+
// DPI rounding on Windows multi-monitor typically shifts by 1-3px per cycle.
118+
const DPI_DRIFT_THRESHOLD = 15
119+
if (!savedPosition ||
120+
Math.abs(phoneIslandBounds.x - savedPosition.x) > DPI_DRIFT_THRESHOLD ||
121+
Math.abs(phoneIslandBounds.y - savedPosition.y) > DPI_DRIFT_THRESHOLD) {
122+
AccountController.instance.setAccountPhoneIslandPosition({
123+
x: phoneIslandBounds.x,
124+
y: phoneIslandBounds.y
125+
})
126+
}
93127
}
94128
debouncer('hide', () => window?.hide(), 250)
95129
} catch (e) {

src/main/classes/windows/NethLinkWindow.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export class NethLinkWindow extends BaseWindow {
7070

7171
show(): void {
7272
try {
73+
let targetBounds: Electron.Rectangle | undefined
7374
const accountBounds = AccountController.instance.getAccountNethLinkBounds()
7475
if (accountBounds) {
7576
const isAccountBoundsOnDisplay = screen.getAllDisplays().reduce((result, display) => {
@@ -83,6 +84,7 @@ export class NethLinkWindow extends BaseWindow {
8384
)
8485
}, false)
8586
if (isAccountBoundsOnDisplay) {
87+
targetBounds = accountBounds
8688
this._window?.setBounds(accountBounds, false)
8789
} else {
8890
this._setBounds()
@@ -94,6 +96,10 @@ export class NethLinkWindow extends BaseWindow {
9496
this._window?.setVisibleOnAllWorkspaces(true)
9597
this._window?.focus()
9698
this._window?.setVisibleOnAllWorkspaces(false)
99+
// Re-assert position after show/focus to override any WM/DPI adjustment on Windows
100+
if (targetBounds) {
101+
this._window?.setBounds(targetBounds, false)
102+
}
97103
}
98104
catch (e: any) {
99105
if (e.message === 'Object has been destroyed') {
@@ -107,7 +113,7 @@ export class NethLinkWindow extends BaseWindow {
107113

108114
hide(..._args: any): void {
109115
try {
110-
this.saveBounds()
116+
this.saveBoundsIfMoved()
111117
this._window?.hide()
112118
} catch (e) {
113119
Log.warning('during hiding the NethLinkWindow:', e)
@@ -119,11 +125,27 @@ export class NethLinkWindow extends BaseWindow {
119125
AccountController.instance.setAccountNethLinkBounds(nethlinkBounds)
120126
}
121127

128+
// Only save if the user actually moved/resized the window (not DPI rounding drift).
129+
// DPI rounding on Windows multi-monitor typically shifts by 1-3px per cycle.
130+
private saveBoundsIfMoved() {
131+
const currentBounds = this._window?.getBounds()
132+
if (!currentBounds) return
133+
const savedBounds = AccountController.instance.getAccountNethLinkBounds()
134+
const DPI_DRIFT_THRESHOLD = 15
135+
if (!savedBounds ||
136+
Math.abs(currentBounds.x - savedBounds.x) > DPI_DRIFT_THRESHOLD ||
137+
Math.abs(currentBounds.y - savedBounds.y) > DPI_DRIFT_THRESHOLD ||
138+
Math.abs(currentBounds.width - savedBounds.width) > DPI_DRIFT_THRESHOLD ||
139+
Math.abs(currentBounds.height - savedBounds.height) > DPI_DRIFT_THRESHOLD) {
140+
this.saveBounds(currentBounds)
141+
}
142+
}
143+
122144
buildWindow(): void {
123145
super.buildWindow()
124146
this._window?.on('hide', this.toggleVisibility)
125147
this._window?.on('moved', () => {
126-
debouncer('onMoveNethLinkWindow', () => this.saveBounds(), 1000)
148+
debouncer('onMoveNethLinkWindow', () => this.saveBoundsIfMoved(), 1000)
127149
})
128150
this._window?.on('show', this.toggleVisibility)
129151
this._window?.on('closed', this.toggleVisibility)

src/main/lib/ipcEvents.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,12 +213,12 @@ export function registerIpcEvents() {
213213
height
214214
}, false)
215215
} else {
216-
const [w, h] = window.getContentSize()
216+
const bounds = window.getBounds()
217217
window.setBounds({
218218
x: newX,
219219
y: newY,
220-
width: w,
221-
height: h
220+
width: bounds.width,
221+
height: bounds.height
222222
}, false)
223223
}
224224
}

0 commit comments

Comments
 (0)