Skip to content

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

Closed
leaanthony wants to merge 7 commits into
masterfrom
agent/engineer-windows/07848f9d
Closed

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

Conversation

@leaanthony

@leaanthony leaanthony commented May 8, 2026 •

Copy link
Copy Markdown
Member

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

    • Improved window positioning on Windows by using physical-pixel coordinates relative to the screen work area, providing more accurate placement across display scaling settings.
    • When screen information is unavailable, windows now retain their physical size and use raw coordinates to avoid unexpected movement or resizing.
  • Tests

    • Added Windows-specific coverage for display scaling, work-area offsets, physical dimensions, and positioning when screen information is unavailable.

…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

coderabbitai Bot commented May 8, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 03f5f929-05dc-46bb-bdb8-175aa0aeb297

📥 Commits

Reviewing files that changed from the base of the PR and between af7026b and ecae4c0.

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

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


Walkthrough

relativePosition() and setRelativePosition(x, y) now use physical pixels relative to PhysicalWorkArea. Nil-screen paths use absolute physical coordinates and preserve window dimensions.

Changes

Window Positioning Coordinate System

Layer / File(s) Summary
Compute relative physical position
v3/pkg/application/webview_window_windows.go
relativePosition() returns the physical X/Y offset from PhysicalWorkArea, or absolute physical bounds when no screen exists.
Apply relative physical position
v3/pkg/application/webview_window_windows.go
setRelativePosition(x, y) uses physical coordinates, applies them through setPhysicalBounds, and preserves physical dimensions.
Validate arithmetic and nil-screen behavior
v3/pkg/application/webview_window_windows_test.go
Windows-only tests cover DPI scales, work-area offsets, unscaled inputs, and nil-screen fallbacks.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to ecae4

This change updates Windows relative window positioning to use physical pixels, preventing DPI scaling from being applied twice. No remaining merge-readiness risk is identified.

🚥 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%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 2 files. 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 identifies the Windows physical-pixel and DPI-scaling fix in SetRelativePosition.
Description check ✅ Passed The description provides the bug, root cause, fix, reproduction steps, expected behavior, testing summary, and link to issue #4300. It does not complete every template checkbox or provide wails doctor…
Linked Issues check ✅ Passed The implementation addresses issue #4300 by treating SetRelativePosition and RelativePosition coordinates as physical pixels, using PhysicalWorkArea offsets, and avoiding duplicate DPI scaling. The ad…
Out of Scope Changes check ✅ Passed The changes are limited to the Windows relative-positioning implementation and focused regression tests. They directly support the requirements of issue #4300.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 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

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

I hop through pixels, crisp and bright,
DPI no longer shifts my flight.
Work-area offsets guide the way,
Windows land where callers say.
Nil screens keep their bounds in sight.

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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

@github-actions github-actions Bot added Bug Something isn't working v3 labels Sep 9, 2026
@coderabbitai

coderabbitai Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@leaanthony

Copy link
Copy Markdown
Member Author

Thank you for investigating this and putting together the fix. After reviewing the coordinate contract and testing the production methods on Windows, we're closing this PR because changing SetRelativePosition and RelativePosition to physical pixels would break existing callers.

Wails' window positions, dimensions, and screen work areas use device-independent (logical) pixels. This change makes the relative-position APIs inconsistent with SetPosition, window initialization, and calculations such as the built-in bottom-right positioning example. A native Windows probe using controlled 100%, 125%, and 200% screen metadata passed with the existing implementation, but failed at 125% and 200% with this change. The added tests also passed with the production change reverted, because they reproduce the arithmetic rather than exercise the changed methods.

For the mouse-following case in #4300, the workaround is to convert the absolute physical mouse coordinates to logical coordinates and use the absolute-position API:

x, y := robotgo.Location()
p := application.PhysicalToDipPoint(application.Point{X: x, Y: y})
wd.SetPosition(p.X, p.Y)

This preserves the existing API contract and avoids passing absolute desktop coordinates to a work-area-relative API. Please validate this adjustment in the original reproducer, including displays with nonzero origins; #4300 can remain open for that follow-up.

Thanks again for the contribution. The reported symptom deserves addressing, but changing the units of these existing APIs is not a compatible fix.

@leaanthony leaanthony closed this Sep 9, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in Wails Release Train Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Something isn't working P1 High priority v3 Windows

Projects

Status: Done

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