Skip to content

fix(windows): use physical pixels in SetRelativePosition to fix DPI scaling#5372

Open
leaanthony wants to merge 6 commits into
masterfrom
agent/engineer-windows/07848f9d
Open

fix(windows): use physical pixels in SetRelativePosition to fix DPI scaling#5372
leaanthony wants to merge 6 commits into
masterfrom
agent/engineer-windows/07848f9d

Conversation

@leaanthony
Copy link
Copy Markdown
Member

@leaanthony leaanthony commented May 8, 2026

Summary

  • Bug: SetRelativePosition on Windows incorrectly treated its (x, y) input as DIP (logical pixels) and applied a DPI scale factor. On a 125%-scaled display, passing coordinates from GetCursorPos/robotgo.Location() (physical pixels) caused the window to be placed 25% further from origin than intended — a deviation that grows linearly with the coordinate value.
  • Fix: Changed setRelativePosition and relativePosition in webview_window_windows.go to work exclusively in physical pixels, consistent with Win32 APIs that return physical screen coordinates on DPI-aware processes. The implementation now adds screen.PhysicalWorkArea.X/Y to compute the absolute physical position and calls setPhysicalBounds directly without any DPI conversion.

Root cause

setRelativePosition previously called relativeToAbsoluteDipPoint (treating input as DIP) then setPosition (which applies DipToPhysicalRect). At 125% scaling this multiplied user-provided coordinates by 1.25, so physical (600, 400) became physical (750, 500). The error scales with the distance from the origin, matching the reported symptom.

Reproduce steps (before fix)

  1. Set Windows display scaling to 125%
  2. Call window.SetRelativePosition(600, 400) (e.g. from mouse cursor coordinates)
  3. Window appears at physical ~(750, 500) instead of (600, 400)

After fix

SetRelativePosition(x, y) places the window at physical position (PhysicalWorkArea.X + x, PhysicalWorkArea.Y + y) — no DPI scaling applied to the user-supplied coordinates. RelativePosition() returns (physX - PhysicalWorkArea.X, physY - PhysicalWorkArea.Y) so the round-trip is correct.

Test plan

  • Build succeeds: go build ./pkg/application/
  • Existing tests pass: go test ./pkg/application/
  • Manual: at 125% scaling, window follows mouse cursor at correct physical position (verified via windows-vm-fix workflow)

Fixes #4300

CC @leaanthony

Summary by CodeRabbit

  • Bug Fixes

    • Window positioning on Windows now uses physical-pixel coordinates anchored to the screen work area for accurate placement.
    • When screen info is unavailable, windows update position using raw physical coordinates while preserving current physical size to avoid unintended moves or resizes.
  • Tests

    • Added Windows-only unit tests covering physical-pixel positioning, nil-screen scenarios, and a regression case for DPI-scaling handling.

Review Change Stack

…tion

On Windows with non-100% display scaling, SetRelativePosition incorrectly
applied DPI scaling to the input coordinates. Since Win32 APIs such as
GetCursorPos return physical pixel coordinates, passing those directly to
SetRelativePosition caused the window to be placed 25% further from the
origin at 125% scaling (error scales linearly with the coordinate value).

The old implementation treated (x, y) as DIP and converted via
DipToPhysicalRect, so a physical position (600, 400) became physical
(750, 500) at 125% - a 25% overshoot growing with distance from origin.

Fix: use physical pixel coordinates exclusively in both setRelativePosition
and relativePosition. The implementation now adds PhysicalWorkArea.X/Y to
get the absolute physical position and calls setPhysicalBounds directly,
bypassing any DPI scaling. relativePosition subtracts PhysicalWorkArea.X/Y
from GetWindowRect output (which is always physical on a DPI-aware process).

Fixes: #4300

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
Copilot AI review requested due to automatic review settings May 8, 2026 14:24
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 8, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 342a9a94-5377-4895-91a2-a7ed83eee077

📥 Commits

Reviewing files that changed from the base of the PR and between c7faef8 and a6bbe06.

📒 Files selected for processing (1)
  • v3/pkg/application/webview_window_windows.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • v3/pkg/application/webview_window_windows.go

Walkthrough

relativePosition() and setRelativePosition(x,y) were changed to use physical pixel coordinates relative to the screen's PhysicalWorkArea; setRelativePosition applies via setPhysicalBounds and both functions handle nil-screen by using/preserving physical bounds.

Changes

Window Positioning Coordinate System

Layer / File(s) Summary
Compute relative physical position
v3/pkg/application/webview_window_windows.go
relativePosition() now returns the window's physical X/Y offset from the owning screen's PhysicalWorkArea, with a nil-screen fallback to the window's physical bounds.
Apply relative physical position
v3/pkg/application/webview_window_windows.go
setRelativePosition(x, y) interprets inputs as physical pixels relative to PhysicalWorkArea and sets the window via setPhysicalBounds; when the screen is nil it updates X/Y while preserving physical Width/Height.
Tests: arithmetic & nil-screen
v3/pkg/application/webview_window_windows_test.go
Added table-driven Windows-only tests verifying physical-pixel arithmetic across DPI scales and nil-screen fallbacks for both setRelativePosition and relativePosition.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • wailsapp/wails#5371: Touches the same Windows window code and may interact with changes to setPhysicalBounds.

Suggested reviewers

  • atterpac

Poem

🐰 I hop in pixels crisp and bright,

No DPI fog to hide my sight.
WorkArea offsets now behave,
Windows land where callers gave.
A tiny hop — exact and brave!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main fix: using physical pixels in SetRelativePosition to address DPI scaling issues on Windows.
Description check ✅ Passed The PR description provides comprehensive context including bug summary, root cause analysis, reproduction steps, and the solution, though it does not follow the template checklist format.
Linked Issues check ✅ Passed The code changes directly address issue #4300 by fixing SetRelativePosition to use physical pixels instead of DIP, eliminating incorrect DPI scaling on non-100% displays.
Out of Scope Changes check ✅ Passed All changes are in scope: modifications to setRelativePosition/relativePosition logic and comprehensive unit tests covering the fix and regression scenarios for issue #4300.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/engineer-windows/07848f9d

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.1)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes Windows DPI-scaling misplacement for SetRelativePosition by treating (x, y) as physical pixel coordinates (consistent with Win32 APIs like GetCursorPos) rather than DIP/logical pixels.

Changes:

  • Updated Windows relativePosition() to compute offsets using physicalBounds() and screen.PhysicalWorkArea.
  • Updated Windows setRelativePosition() to compute absolute physical coordinates and call setPhysicalBounds directly (no DPI conversion).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +759 to +763
// Get window physical pixel position relative to the screen WorkArea on which it is
func (w *windowsWebviewWindow) relativePosition() (int, int) {
screen, _ := w.getScreen()
pos := screen.absoluteToRelativeDipPoint(w.bounds().Origin())
// Relative to WorkArea origin
pos.X -= (screen.WorkArea.X - screen.X)
pos.Y -= (screen.WorkArea.Y - screen.Y)
return pos.X, pos.Y
physBounds := w.physicalBounds()
return physBounds.X - screen.PhysicalWorkArea.X, physBounds.Y - screen.PhysicalWorkArea.Y
Comment on lines +766 to +768
// Set window position using physical pixel coordinates relative to the screen WorkArea.
// Using physical pixels matches Win32 APIs (e.g. GetCursorPos) and avoids DPI scaling
// errors at non-100% display scaling factors.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@v3/pkg/application/webview_window_windows.go`:
- Around line 759-778: Both functions call screen, _ := w.getScreen() and assume
screen is non-nil; guard against a nil screen to avoid panics. In
relativePosition(), if getScreen() returns an error or nil screen, return the
window's absolute physical X,Y (from physBounds) or (0,0) as a safe fallback
instead of dereferencing screen.PhysicalWorkArea; in setRelativePosition(), if
screen is nil, treat the provided x,y as absolute physical coordinates (or keep
current physBounds if you prefer no-op) and call w.setPhysicalBounds with a safe
X/Y (do not access screen.PhysicalWorkArea when screen==nil). Update the
functions relativePosition() and setRelativePosition() accordingly to check for
nil and handle the fallback.
- Around line 759-778: The Windows implementation is using physical pixels while
other platforms use DIPs; update windowsWebviewWindow.relativePosition and
windowsWebviewWindow.setRelativePosition to convert between DIPs and physical
pixels using the display scale returned by getScreen(): compute scale :=
screen.Scale (or ScaleFactor) and return/consume DIP coords (relativePosition
should divide physical offsets by scale before returning; setRelativePosition
should multiply the DIP x,y by scale before adding screen.PhysicalWorkArea and
calling setPhysicalBounds). Keep references to getScreen(), physicalBounds(),
setPhysicalBounds(Rect{}) and ensure both methods consistently accept/return
DIPs like macOS/Linux.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 75f2ef3a-c2f2-4e9e-a3c4-e47f29cc4363

📥 Commits

Reviewing files that changed from the base of the PR and between d1be17b and 36377d3.

📒 Files selected for processing (1)
  • v3/pkg/application/webview_window_windows.go

Comment thread v3/pkg/application/webview_window_windows.go Outdated
taliesin-ai and others added 2 commits May 9, 2026 06:47
…ePosition

Add nil check for screen returned by getScreen() to prevent a potential
panic when the window is not found on any known monitor. Also clarify
doc comments to explicitly reference PhysicalWorkArea.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
…ePosition arithmetic

Covers the DPI regression (GitHub #4300) and the nil-screen guard added
in the previous commit. Tests verify that physical-pixel coordinates are
not re-scaled at 100%, 125%, 150%, and 200% DPI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
v3/pkg/application/webview_window_windows_test.go (3)

62-75: 🏗️ Heavy lift

Tests validate inlined arithmetic, not the production functions

Both arithmetic suites replicate the formula (PhysicalWorkArea.X + inputX / physBounds.X - PhysicalWorkArea.X) directly in the test body rather than calling the actual setRelativePosition / relativePosition methods. If the production code ever drifts from this formula, these tests will stay green while the bug re-appears.

This is likely an intentional trade-off because both methods call Win32 APIs (GetWindowRect, SetWindowPos, etc.) that are hard to stub in pure Go unit tests. If that's the case, a short comment acknowledging the constraint would prevent future contributors from wondering why the real functions aren't invoked. Alternatively, consider extracting the coordinate-translation logic into small, unexported pure functions (e.g., physToRelative, relativeToPhys) that take only Rect / Screen arguments — those could be tested directly with zero Win32 dependency.

Also applies to: 145-159

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/pkg/application/webview_window_windows_test.go` around lines 62 - 75,
Tests are inlining the arithmetic instead of exercising the production functions
(setRelativePosition, relativePosition) which can mask regressions; either add a
short comment in webview_window_windows_test.go explaining the Win32 dependency
prevents calling those functions directly, or better yet extract the pure
coordinate-translation logic into small unexported helpers (e.g.,
physToRelative, relativeToPhys) that accept Screen/Rect and perform the
arithmetic, update setRelativePosition/relativePosition to call those helpers,
and change the tests to call the new helpers directly to verify translation
behavior without Win32 stubs.

17-26: ⚡ Quick win

scaleFactor field is populated but never read in the test body

In both TestSetRelativePositionArithmetic and TestRelativePositionArithmetic, scaleFactor is set in every case but tt.scaleFactor is never referenced inside the loop body. The intent — demonstrating that scale is not applied — is valid and meaningful, but without a comment the field looks like a mistake or an incomplete assertion. A future reader may add tt.scaleFactor to the arithmetic thinking they're "fixing" it.

✏️ Suggested clarification (same change applies to the analogous field in TestRelativePositionArithmetic)
 	tests := []struct {
 		name        string
-		scaleFactor float32
+		scaleFactor float32 // intentionally unused: proves the fix — scale must NOT be re-applied
 		workAreaX   int
 		workAreaY   int
 		inputX      int
 		inputY      int
 		wantX       int
 		wantY       int
 	}{

Also applies to: 103-112

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/pkg/application/webview_window_windows_test.go` around lines 17 - 26, The
table-driven tests define a scaleFactor field that is never used (see the tests
slice in TestSetRelativePositionArithmetic and TestRelativePositionArithmetic
and the tt.scaleFactor identifier), which looks like a leftover; either remove
the unused scaleFactor field from the test struct in both tests or explicitly
document its intentional omission by adding a brief comment where the table is
defined (e.g., “scaleFactor included to show scale is not applied —
intentionally unused”) so future readers won't introduce tt.scaleFactor into the
assertions by mistake.

84-91: ⚡ Quick win

Dead else branches in nil-screen tests make it appear both paths are exercised

In both TestSetRelativePositionNilScreen and TestRelativePositionNilScreen, var screen *Screen is always nil, so the else block is unreachable dead code. A reader could reasonably believe the else path is being covered here, which it isn't.

Since the non-nil path is already covered by the arithmetic table tests, the cleanest fix is to drop the unreachable branch entirely:

✏️ Proposed fix for TestSetRelativePositionNilScreen
 	var screen *Screen
 	var result Rect
-	if screen == nil {
-		result = Rect{X: x, Y: y, Width: physBounds.Width, Height: physBounds.Height}
-	} else {
-		result = Rect{X: screen.PhysicalWorkArea.X + x, Y: screen.PhysicalWorkArea.Y + y,
-			Width: physBounds.Width, Height: physBounds.Height}
-	}
+	_ = screen // always nil; nil-path is the case under test
+	result = Rect{X: x, Y: y, Width: physBounds.Width, Height: physBounds.Height}
✏️ Proposed fix for TestRelativePositionNilScreen
 	var screen *Screen
 	var gotX, gotY int
-	if screen == nil {
-		gotX, gotY = physBounds.X, physBounds.Y
-	} else {
-		gotX = physBounds.X - screen.PhysicalWorkArea.X
-		gotY = physBounds.Y - screen.PhysicalWorkArea.Y
-	}
+	_ = screen // always nil; nil-path is the case under test
+	gotX, gotY = physBounds.X, physBounds.Y

Also applies to: 167-174

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/pkg/application/webview_window_windows_test.go` around lines 84 - 91, The
nil-screen tests (TestSetRelativePositionNilScreen and
TestRelativePositionNilScreen) declare var screen *Screen which is always nil,
so the else branch that uses screen.PhysicalWorkArea is dead code and should be
removed; update the code that sets result to unconditionally assign Rect{X: x,
Y: y, Width: physBounds.Width, Height: physBounds.Height} (remove the
conditional and the else block), and apply the same cleanup to the duplicate
occurrence in the other test (the second nil-screen test around the later block)
so only the reachable nil-screen path remains.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@v3/pkg/application/webview_window_windows_test.go`:
- Around line 62-75: Tests are inlining the arithmetic instead of exercising the
production functions (setRelativePosition, relativePosition) which can mask
regressions; either add a short comment in webview_window_windows_test.go
explaining the Win32 dependency prevents calling those functions directly, or
better yet extract the pure coordinate-translation logic into small unexported
helpers (e.g., physToRelative, relativeToPhys) that accept Screen/Rect and
perform the arithmetic, update setRelativePosition/relativePosition to call
those helpers, and change the tests to call the new helpers directly to verify
translation behavior without Win32 stubs.
- Around line 17-26: The table-driven tests define a scaleFactor field that is
never used (see the tests slice in TestSetRelativePositionArithmetic and
TestRelativePositionArithmetic and the tt.scaleFactor identifier), which looks
like a leftover; either remove the unused scaleFactor field from the test struct
in both tests or explicitly document its intentional omission by adding a brief
comment where the table is defined (e.g., “scaleFactor included to show scale is
not applied — intentionally unused”) so future readers won't introduce
tt.scaleFactor into the assertions by mistake.
- Around line 84-91: The nil-screen tests (TestSetRelativePositionNilScreen and
TestRelativePositionNilScreen) declare var screen *Screen which is always nil,
so the else branch that uses screen.PhysicalWorkArea is dead code and should be
removed; update the code that sets result to unconditionally assign Rect{X: x,
Y: y, Width: physBounds.Width, Height: physBounds.Height} (remove the
conditional and the else block), and apply the same cleanup to the duplicate
occurrence in the other test (the second nil-screen test around the later block)
so only the reachable nil-screen path remains.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fadd97f4-e346-461d-b7a8-e62351c66387

📥 Commits

Reviewing files that changed from the base of the PR and between 3a0bbe2 and c7faef8.

📒 Files selected for processing (1)
  • v3/pkg/application/webview_window_windows_test.go

@leaanthony leaanthony added this to the v3.0.0-beta.2 milestone May 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

When the screen definition is not 100%, the window position set by SetRelativePosition is incorrect

3 participants