//go:build windows package main import ( "strings" "sync/atomic" "syscall" "time" "unsafe" "golang.org/x/sys/windows" "u-desk/internal/common" ) // Chrome_WidgetWin 系列类名是 WebView2 的宿主窗口层级 const webviewHostClassFragment = "Chrome_WidgetWin" // wmSize Win32 窗口尺寸变化消息 const wmSizeMsg uint32 = 0x0005 var ( // user32 对这三个 API 只导出无后缀名(无 A/W 变体,带 W 后缀找不到 proc 会 panic) procGetClientRectFix = windows.NewLazySystemDLL("user32.dll").NewProc("GetClientRect") procSetWindowPosFix = windows.NewLazySystemDLL("user32.dll").NewProc("SetWindowPos") procGetAncestorFix = windows.NewLazySystemDLL("user32.dll").NewProc("GetAncestor") ) // 修复去重:同一时刻只允许一个校验在跑(回调写共享切片要求互斥) var webviewFixRunning atomic.Bool // WM_SIZE 去重:脏标记 + 单 worker(每条 WM_SIZE 只置位,不再批量 spawn goroutine) var ( webviewSizeDirty atomic.Bool webviewSizeWorkerActive atomic.Bool ) // 当前校验的目标主窗口(枚举回调过滤直属子窗口用) var webviewTargetHwnd uintptr // webviewHosts 收集结果容器;enumChildWebviewHosts 回调写入 var webviewHosts []windows.HWND // 包级变量:syscall.NewCallback 指向的 Go 闭包必须存活于整个进程生命周期 var enumChildWebviewHosts = syscall.NewCallback(func(h uintptr, _ uintptr) uintptr { // GA_PARENT(1):只收直属子窗口,避免动到更深层的 WebView2 内部窗口 parent, _, _ := procGetAncestorFix.Call(h, 1) if parent != webviewTargetHwnd { return 1 } var buf [256]uint16 if _, err := windows.GetClassName(windows.HWND(h), &buf[0], int32(len(buf))); err != nil { return 1 } if strings.Contains(syscall.UTF16ToString(buf[:]), webviewHostClassFragment) { webviewHosts = append(webviewHosts, windows.HWND(h)) } return 1 }) // checkAndFixFill 校验 WebView2 宿主是否铺满窗口客户区,未铺满则强制铺满。 // wails alpha 的 WM_SIZE 处理存在竞态:主线程忙时消息被 resizePending 标志丢弃, // 迟到执行的重铺又使用闭包捕获的过期 bounds,webview 会停在中间尺寸且不再有 // 新消息来纠正,表现为整个应用内容收窄、四周露出窗口底色。 func checkAndFixFill(hwnd uintptr) { if hwnd == 0 { return } var rc struct{ Left, Top, Right, Bottom int32 } if r, _, _ := procGetClientRectFix.Call(hwnd, uintptr(unsafe.Pointer(&rc))); r == 0 { return } cw, ch := int(rc.Right-rc.Left), int(rc.Bottom-rc.Top) if cw <= 0 || ch <= 0 { return } webviewTargetHwnd = hwnd webviewHosts = webviewHosts[:0] windows.EnumChildWindows(windows.HWND(hwnd), enumChildWebviewHosts, nil) for _, h := range webviewHosts { // SWP_NOZORDER(0x0004):只改尺寸位置,不干预 wails 管理的层级 procSetWindowPosFix.Call(uintptr(h), 0, 0, 0, uintptr(cw), uintptr(ch), 0x0004) } } // runGuarded 同步调用点的 panic 兜底:panic 取证落盘后不外抛, // 让调用方(如周期循环)能继续跑下一轮 func runGuarded(fn func()) { defer func() { if r := recover(); r != nil { common.AppendPanicLog("webview-fix", r) } }() fn() } // fixWebviewFill WM_SIZE 路径:等 resize 序列结束、wails 内部去抖落定后校验一次。 // WM_SIZE 只置脏标记;活跃期间的后续 WM_SIZE 合并进去(worker 循环消费), // 拖拽缩放的风暴从"每条消息一个 goroutine"降为全程单 worker func fixWebviewFill(hwnd uintptr) { webviewTargetHwnd = hwnd webviewSizeDirty.Store(true) if !webviewSizeWorkerActive.CompareAndSwap(false, true) { return } common.Go("webview-fix", func() { defer webviewSizeWorkerActive.Store(false) for webviewSizeDirty.CompareAndSwap(true, false) { time.Sleep(150 * time.Millisecond) if webviewFixRunning.CompareAndSwap(false, true) { runGuarded(func() { checkAndFixFill(webviewTargetHwnd) }) webviewFixRunning.Store(false) } } }) } // stopWebviewWatch 周期兜底循环的停止信号;StartWebviewFillWatch 重入时先关旧的(重启语义) var stopWebviewWatch chan struct{} // StartWebviewFillWatch 周期兜底:覆盖"无新 WM_SIZE 的迟到重放"场景 // (主线程忙时排队的过期重绘在窗口静止时才执行,此时不会产生任何 WM_SIZE) func StartWebviewFillWatch() { if stopWebviewWatch != nil { select { case <-stopWebviewWatch: // 旧循环已自行退出 default: close(stopWebviewWatch) } } stop := make(chan struct{}) stopWebviewWatch = stop common.Go("webview-watch", func() { for { select { case <-stop: return case <-time.After(3 * time.Second): } if webviewFixRunning.CompareAndSwap(false, true) { runGuarded(func() { checkAndFixFill(findMainWindowHwnd()) }) webviewFixRunning.Store(false) } } }) } // findMainWindowHwnd 找本进程的可见主窗口 // 回调必须进程级复用(同 enumChildWebviewHosts):runtime 的 Windows callback // 硬上限 2000 个且按闭包指针去重,每 3 秒新建一个约 100 分钟即耗尽, // 触发 runtime throw("too many callback functions") 直接静默退出(无法 recover) var mainWindowHwndResult uintptr var enumMainWindowHwnd = syscall.NewCallback(func(h uintptr, _ uintptr) uintptr { var winPid uint32 windows.GetWindowThreadProcessId(windows.HWND(h), &winPid) if winPid == windows.GetCurrentProcessId() && windows.IsWindowVisible(windows.HWND(h)) { mainWindowHwndResult = h return 0 } return 1 }) func findMainWindowHwnd() uintptr { mainWindowHwndResult = 0 windows.EnumWindows(enumMainWindowHwnd, nil) return mainWindowHwndResult }