wailsapp / wailsapp/wails

[v3, macOS] Screen.Bounds.X/Y on non-primary screens are off by the screen's scale factor (mixed-DPI)

Open
#5,409 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Bug investigating MacOS P2 v3
Dominant language
Go
Stars
36.3k
Forks
1.9k
Avg merge
2d 11h
Merged PRs (30d)
33

Description

Description

In pkg/application/screen_darwin.go, cScreenToScreen multiplies screen.x and screen.y by the screen's own backingScaleFactor before assigning them to Bounds.X / Bounds.Y:

sf := float64(screen.scaleFactor)
toPhysical := func(points C.int) int { return int(float64(points) * sf) }

return &Screen{
    X: toPhysical(screen.x),
    Y: toPhysical(screen.y),
    Bounds: Rect{
        X: toPhysical(screen.x),
        Y: toPhysical(screen.y),
        ...
    },
    ...
}

The intent (per the code comment) is a round-trip workaround for applyDPIScaling in screenmanager.go, which divides Bounds.Width / Bounds.Height by the scale factor. But Bounds.X / Bounds.Y are not divided again in applyDPIScaling — they're left in points × ownScaleFactor space, while Bounds.Width / Bounds.Height end up in logical points.

After #5304 + the GetScreens normalization, Position() / SetPosition() are in logical points across screens. So on mixed-DPI setups (e.g., a Retina secondary above a 1x primary), SetPosition(s.Bounds.X, s.Bounds.Y) for a non-primary screen lands at the wrong location by a factor of that screen's ScaleFactor.

Steps to Reproduce

  1. Primary monitor: 1x (non-Retina).
  2. Secondary monitor: 2x Retina, positioned above the primary in macOS Displays settings.
  3. Read secondary.Bounds.Y from GetScreens().
  4. Call SetPosition(secondary.Bounds.X, secondary.Bounds.Y).
  5. Window does not land at the top-left of the secondary; it's off by secondary.ScaleFactor.

The same ScaleFactor doubling exists today but only becomes user-visible once Position/SetPosition are normalized to logical points (which is the goal of #5117 / #5304).

Notes

  • The flofreud setup in #5117 (Retina primary + 1x external above) is not affected, because the non-primary screen has ScaleFactor=1 and the multiplication is a no-op.
  • This is a pre-existing macOS-only Bounds unit bug — exposed (not introduced) by the GetScreens normalization landing alongside #5304.
  • The fix is likely: drop the * sf multiplication on X/Y in cScreenToScreen (keep it for PhysicalBounds.X/Y), and verify nothing in screenmanager.go (e.g., move() placement adjustment, getScreenAlignment) regresses on the cross-DPI tests.

Environment

  • Wails v3 (pkg/application/screen_darwin.go, pkg/application/screenmanager.go)
  • macOS, mixed-DPI multi-monitor

Related

  • #5117 — coordinate-system audit
  • #5304 — absolute coordinate normalization PR (where this case became user-visible)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with cScreenToScreen in pkg/application/screen_darwin.go, then inspect applyDPIScaling, move(), and getScreenAlignment in pkg/application/screenmanager.go. Run the cross-DPI tests and verify that Bounds coordinates remain in logical points, PhysicalBounds coordinates retain physical scaling, and SetPosition can round-trip a non-primary screen position.

Written by the indexing model from the issue text.

Assessment

Tech stack
go, macos
Domain
desktop
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.