fix(window): check the saved position against the monitors, not their bounding box

Reported from a multi-monitor station: window.json held x=-7680 and the
window opened where nobody could see it, with no way back short of
editing the file — which nobody knows to do.

The guard existed but asked the wrong question. It tested the position
against the VIRTUAL SCREEN, the rectangle spanning every monitor, and
monitors rarely tile that rectangle: a wide screen beside a tall one, or
one mounted higher, leaves gaps inside the box that belong to no monitor.
A window in a gap passes a bounding-box test and is invisible. The test
now walks the monitors themselves, through EnumDisplayMonitors, and uses
each one's WORK area — a title bar under the taskbar cannot be dragged
either.

A position that is genuinely lost is now MOVED onto the nearest monitor,
keeping the window's size. Handing it back to Windows lost the size too
and dropped the window on the primary screen wherever Windows chose.

The arithmetic is in screenclamp.go with no Win32 in it, and tested
against the reporter's four-monitor layout and against a gap between two:
this fault is invisible by definition and cannot be reproduced without
the reporter's screens, so a table test is the only place it can be held.
The layout is also logged at every start, since the first question after
'OpsLog does not open' is what the screens looked like.
This commit is contained in:
2026-08-25 09:53:34 +02:00
parent b2b93e2839
commit 6536d140ba
6 changed files with 271 additions and 19 deletions
+96 -6
View File
@@ -2,12 +2,17 @@
package main
import "syscall"
import (
"fmt"
"strings"
"syscall"
"unsafe"
"hamlog/internal/applog"
)
// GetSystemMetrics indices for the virtual desktop — the rectangle spanning
// every attached monitor. Wails' ScreenGetAll reports each monitor's size but
// not its offset, so it cannot answer "is this coordinate on any screen?"; the
// Win32 metrics can.
// every attached monitor.
const (
smXVirtualScreen = 76
smYVirtualScreen = 77
@@ -16,8 +21,10 @@ const (
)
var (
user32Dll = syscall.NewLazyDLL("user32.dll")
procGetSystemMetrics = user32Dll.NewProc("GetSystemMetrics")
user32Dll = syscall.NewLazyDLL("user32.dll")
procGetSystemMetrics = user32Dll.NewProc("GetSystemMetrics")
procEnumDisplayMonitors = user32Dll.NewProc("EnumDisplayMonitors")
procGetMonitorInfoW = user32Dll.NewProc("GetMonitorInfoW")
)
func systemMetric(index int) int {
@@ -34,3 +41,86 @@ func virtualScreenBounds() (x, y, w, h int, ok bool) {
}
return systemMetric(smXVirtualScreen), systemMetric(smYVirtualScreen), w, h, true
}
// winRect is Win32's RECT.
type winRect struct{ Left, Top, Right, Bottom int32 }
// monitorInfo is MONITORINFO: the monitor's whole rectangle and its work area
// (what is left once the taskbar is taken out).
type monitorInfo struct {
CbSize uint32
RcMonitor winRect
RcWork winRect
DwFlags uint32
}
// monitorRects enumerates the attached monitors.
//
// THE BOUNDING BOX IS NOT THE DESKTOP. The virtual screen is the rectangle that
// spans every monitor, and monitors are rarely arranged to fill it: a wide
// screen beside a tall one, or one offset vertically, leaves rectangular HOLES
// inside the box that belong to no monitor at all. A window placed in a hole
// passes a bounding-box test and is invisible — which is exactly what was
// reported from a four-monitor station whose window.json held x=-7680.
//
// So the test has to be against the monitors themselves.
func monitorRects() []screenRect {
var out []screenRect
cb := syscall.NewCallback(func(hMonitor, hdc uintptr, lprc *winRect, data uintptr) uintptr {
var mi monitorInfo
mi.CbSize = uint32(unsafe.Sizeof(mi))
if r, _, _ := procGetMonitorInfoW.Call(hMonitor, uintptr(unsafe.Pointer(&mi))); r != 0 {
// The WORK area, not the full rectangle: a title bar under the
// taskbar is a window that cannot be dragged, which is the fault
// being guarded against in the first place.
out = append(out, screenRect{
X: int(mi.RcWork.Left), Y: int(mi.RcWork.Top),
W: int(mi.RcWork.Right - mi.RcWork.Left),
H: int(mi.RcWork.Bottom - mi.RcWork.Top),
})
}
return 1 // keep enumerating
})
procEnumDisplayMonitors.Call(0, 0, cb, 0)
return out
}
// describeMonitors renders the layout for the log. A window that opens where
// nobody can see it is reported as "OpsLog did not start", and the first
// question is what the screens looked like at that moment.
func describeMonitors(rects []screenRect) string {
if len(rects) == 0 {
return "none detected"
}
parts := make([]string, 0, len(rects))
for _, r := range rects {
parts = append(parts, fmt.Sprintf("%dx%d at %d,%d", r.W, r.H, r.X, r.Y))
}
return strings.Join(parts, " · ")
}
// onSomeMonitorImpl reports whether a window at these coordinates would land
// where it can be seen and grabbed.
func onSomeMonitorImpl(x, y, w, h int) bool {
rects := monitorRects()
if len(rects) == 0 {
// Enumeration failed. Fall back to the bounding box rather than refuse
// the operator's own saved position on the strength of a failed call.
vx, vy, vw, vh, ok := virtualScreenBounds()
if !ok {
return true
}
return overlapsEnough(x, y, w, h, vx, vy, vw, vh)
}
return onAnyScreen(x, y, w, h, rects)
}
// clampToVisible moves a window rectangle onto the monitor it is closest to.
func clampToVisible(x, y, w, h int) (int, int, bool) {
return clampRectToScreens(x, y, w, h, monitorRects())
}
// logMonitorLayout writes the current screen arrangement once at startup.
func logMonitorLayout() {
applog.Printf("window: monitors — %s", describeMonitors(monitorRects()))
}