@yufengtadian/freedom-cli 1.12.18 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/lib/build.js +1 -1
  2. package/lib/security.js +1 -1
  3. package/package.json +5 -1
  4. package/shell/darwin-arm64/freedom-shell +0 -0
  5. package/shell/linux-x64/freedom-shell +0 -0
  6. package/shell/win-x64/freedom-shell.exe +0 -0
  7. package/templates/go/go.mod +1 -7
  8. package/templates/go/go.sum +2 -2
  9. package/templates/go/main.go +22 -8
  10. package/templates/go/pkg/freedom/anti_debug_other.go +2 -3
  11. package/templates/go/pkg/freedom/anti_debug_windows.go +10 -12
  12. package/templates/go/pkg/freedom/assets/{index.html → default.html} +1 -1
  13. package/templates/go/pkg/freedom/assets/freedom.js +200 -5
  14. package/templates/go/pkg/freedom/assets_embed.go +11 -13
  15. package/templates/go/pkg/freedom/authenticode_other.go +11 -0
  16. package/templates/go/pkg/freedom/authenticode_windows.go +87 -0
  17. package/templates/go/pkg/freedom/backend_proc.go +291 -53
  18. package/templates/go/pkg/freedom/bridge.go +13 -2
  19. package/templates/go/pkg/freedom/capability.go +77 -0
  20. package/templates/go/pkg/freedom/center_other.go +3 -0
  21. package/templates/go/pkg/freedom/center_windows.go +45 -35
  22. package/templates/go/pkg/freedom/dispatch.go +60 -0
  23. package/templates/go/pkg/freedom/events_windows.go +319 -0
  24. package/templates/go/pkg/freedom/freedom.go +193 -99
  25. package/templates/go/pkg/freedom/msgwindow_windows.go +257 -0
  26. package/templates/go/pkg/freedom/osver_other.go +15 -0
  27. package/templates/go/pkg/freedom/osver_windows.go +121 -0
  28. package/templates/go/pkg/freedom/proc_hide_windows.go +2 -2
  29. package/templates/go/pkg/freedom/{configfile.go → resources.go} +38 -35
  30. package/templates/go/pkg/freedom/security.go +19 -12
  31. package/templates/go/pkg/freedom/singleinstance_other.go +10 -0
  32. package/templates/go/pkg/freedom/singleinstance_windows.go +145 -0
  33. package/templates/go/pkg/freedom/spawn_other.go +11 -0
  34. package/templates/go/pkg/freedom/spawn_windows.go +23 -0
  35. package/templates/go/pkg/freedom/store.go +458 -0
  36. package/templates/go/pkg/freedom/syscap_linux.go +278 -0
  37. package/templates/go/pkg/freedom/syscap_other.go +15 -3
  38. package/templates/go/pkg/freedom/syscap_windows.go +228 -36
  39. package/templates/go/pkg/freedom/sysint_common.go +70 -0
  40. package/templates/go/pkg/freedom/sysint_windows.go +426 -0
  41. package/templates/go/pkg/freedom/tray_common.go +12 -0
  42. package/templates/go/pkg/freedom/tray_linux.go +302 -0
  43. package/templates/go/pkg/freedom/tray_other.go +2 -2
  44. package/templates/go/pkg/freedom/tray_windows.go +69 -39
  45. package/templates/go/pkg/freedom/updater.go +372 -0
  46. package/templates/go/pkg/freedom/window_mgr.go +385 -0
  47. package/templates/go/pkg/freedom/window_other.go +32 -66
  48. package/templates/go/pkg/freedom/window_windows.go +397 -254
  49. package/templates/go/build_all.txt +0 -0
  50. package/templates/go/build_err.txt +0 -0
  51. package/templates/go/gen_config.go +0 -22
  52. package/templates/go/pkg/freedom/center_windows_test.go +0 -32
  53. package/templates/go/pkg/freedom/security_test.go +0 -153
  54. package/templates/go/temp_vet_test/main.go +0 -40
  55. package/templates/go/test_err.txt +0 -0
  56. package/templates/go/vet_err.txt +0 -1
  57. package/templates/go/vet_err2.txt +0 -2
  58. package/templates/go/webview_go/.github/workflows/ci.yaml +0 -62
  59. package/templates/go/webview_go/CHANGELOG.md +0 -15
  60. package/templates/go/webview_go/LICENSE +0 -22
  61. package/templates/go/webview_go/README.md +0 -50
  62. package/templates/go/webview_go/examples/basic/main.go +0 -12
  63. package/templates/go/webview_go/examples/bind/main.go +0 -38
  64. package/templates/go/webview_go/glue.c +0 -36
  65. package/templates/go/webview_go/go.mod +0 -3
  66. package/templates/go/webview_go/go.sum +0 -0
  67. package/templates/go/webview_go/libs/mswebview2/LICENSE +0 -27
  68. package/templates/go/webview_go/libs/mswebview2/include/WebView2.h +0 -23568
  69. package/templates/go/webview_go/libs/mswebview2/include/vendor.go +0 -2
  70. package/templates/go/webview_go/libs/mswebview2/vendor.go +0 -2
  71. package/templates/go/webview_go/libs/mswebview2/version.txt +0 -1
  72. package/templates/go/webview_go/libs/webview/LICENSE +0 -22
  73. package/templates/go/webview_go/libs/webview/include/vendor.go +0 -2
  74. package/templates/go/webview_go/libs/webview/include/webview.h +0 -3871
  75. package/templates/go/webview_go/libs/webview/vendor.go +0 -2
  76. package/templates/go/webview_go/libs/webview/version.txt +0 -1
  77. package/templates/go/webview_go/webview.cc +0 -1
  78. package/templates/go/webview_go/webview.go +0 -481
  79. package/templates/go/webview_go/webview_test.go +0 -50
@@ -4,20 +4,22 @@ package freedom
4
4
 
5
5
  import (
6
6
  "unsafe"
7
+
8
+ webview "github.com/webview/webview_go"
7
9
  )
8
10
 
9
11
  const (
10
- smCxScreen = 16
11
- smCyScreen = 17
12
- monitorDefaultToNear = 2 // MONITOR_DEFAULTTONEAREST
12
+ smCxScreen = 16
13
+ smCyScreen = 17
14
+
15
+ monitorDefaultTonearest = 2 // MONITOR_DEFAULTTONEAREST
13
16
  )
14
17
 
18
+ // rect / monitorInfo 对应 Win32 的 RECT 与 MONITORINFO 结构。
15
19
  type rect struct {
16
20
  left, top, right, bottom int32
17
21
  }
18
22
 
19
- // monitorInfo 对应 Win32 MONITORINFO(cbSize 置 0 时用 struct 大小填充,
20
- // 32 位下与 64 位下布局一致,字段对齐与 cbSize 校验兼容)。
21
23
  type monitorInfo struct {
22
24
  cbSize uint32
23
25
  rcMonitor rect
@@ -25,55 +27,63 @@ type monitorInfo struct {
25
27
  dwFlags uint32
26
28
  }
27
29
 
28
- // applyCenter 在 Windows 上把窗口置于屏幕中央(需在 SetSize 之后调用)。
30
+ // applyCenter 在 Windows 上把窗口置于所在显示器的工作区中央(需在 SetSize 之后调用)。
29
31
  // webview_go 未提供 SetPosition,这里通过原生 HWND + MoveWindow 定位。
30
- // 优先居中到窗口当前所在监视器的工作区(rcWork):多屏副屏(负坐标 /
31
- // 不同分辨率 / 任务栏遮挡)下也能正确居中;API 失败时回退主屏全屏居中。
32
- //
33
- // user32 过程句柄(procGetSystemMetrics / procMoveWindow / procMonitorFromWindow /
34
- // procGetMonitorInfo)统一声明在 window_windows.go,避免两套重复 NewProc。
32
+ // 使用 MonitorFromWindow + GetMonitorInfo:多显示器下相对窗口实际所在屏幕居中,
33
+ // 且基于工作区(扣除任务栏)计算,不会遮挡任务栏;查询失败时回退主屏全屏尺寸估算。
35
34
  func (a *App) applyCenter() {
36
35
  if !a.cfg.Center {
37
36
  return
38
37
  }
39
- hwnd := a.WindowHandle()
38
+ // 与 Emit/Quit/WindowHandle 一致经 withView 访问,维持 view 字段的并发约定。
39
+ a.withView(func(view webview.WebView) {
40
+ if hwnd := uintptr(view.Window()); hwnd != 0 {
41
+ a.centerHWND(hwnd)
42
+ }
43
+ })
44
+ }
45
+
46
+ // centerHWND 把指定原生窗口按其所在显示器工作区居中(M2:次级窗口复用)。
47
+ // 尺寸用窗口当前外框(次级窗口尺寸可能与主窗口配置不同)。
48
+ func (a *App) centerHWND(hwnd uintptr) {
40
49
  if hwnd == 0 {
41
50
  return
42
51
  }
43
- mon, _, _ := procMonitorFromWindow.Call(hwnd, monitorDefaultToNear)
44
- if mon != 0 {
52
+ var r rect
53
+ if rr, _, _ := procGetWindowRect.Call(hwnd, uintptr(unsafe.Pointer(&r))); rr == 0 {
54
+ return
55
+ }
56
+ x, y := centeredForWindow(hwnd, int(r.right-r.left), int(r.bottom-r.top))
57
+ procMoveWindow.Call(hwnd, uintptr(x), uintptr(y), uintptr(r.right-r.left), uintptr(r.bottom-r.top), 1)
58
+ }
59
+
60
+ // centeredForWindow 供运行时 window.center 动作使用:以窗口当前实际外框尺寸
61
+ // (而非 Config 初始尺寸)在所在显示器工作区居中。
62
+ func centeredForWindow(hwnd uintptr, w, h int) (int, int) {
63
+ if mon, _, _ := procMonitorFromWindow.Call(hwnd, monitorDefaultTonearest); mon != 0 {
45
64
  var mi monitorInfo
46
65
  mi.cbSize = uint32(unsafe.Sizeof(mi))
47
- if r1, _, _ := procGetMonitorInfo.Call(mon, uintptr(unsafe.Pointer(&mi))); r1 != 0 {
48
- work := mi.rcWork
49
- sw := int(work.right - work.left)
50
- sh := int(work.bottom - work.top)
51
- // M5:窗口居中坐标双向 clamp。逻辑抽到 clampCentered 便于无头单测。
52
- x, y := clampCentered(a.cfg.Width, a.cfg.Height, int(work.left), int(work.top), sw, sh)
53
- procMoveWindow.Call(hwnd, uintptr(x), uintptr(y), uintptr(a.cfg.Width), uintptr(a.cfg.Height), 1)
54
- return
66
+ if r, _, _ := procGetMonitorInfo.Call(mon, uintptr(unsafe.Pointer(&mi))); r != 0 {
67
+ return centeredInRect(int(mi.rcWork.left), int(mi.rcWork.top),
68
+ int(mi.rcWork.right-mi.rcWork.left), int(mi.rcWork.bottom-mi.rcWork.top), w, h)
55
69
  }
56
70
  }
57
- // 回退:主屏全屏尺寸居中(M5 同双向 clamp,上界不小于下界 0)
58
71
  sw, _, _ := procGetSystemMetrics.Call(smCxScreen)
59
72
  sh, _, _ := procGetSystemMetrics.Call(smCyScreen)
60
- // 主屏全屏居中:等价于左上 (0,0) 的工作区,同走 clampCentered 双向 clamp。
61
- x, y := clampCentered(a.cfg.Width, a.cfg.Height, 0, 0, int(sw), int(sh))
62
- procMoveWindow.Call(hwnd, uintptr(x), uintptr(y), uintptr(a.cfg.Width), uintptr(a.cfg.Height), 1)
73
+ return centeredInRect(0, 0, int(sw), int(sh), w, h)
63
74
  }
64
75
 
65
- // clampCentered 计算窗口在其所在工作区内的居中左上角坐标,并做双向 clamp:
66
- // 窗口小于工作区时严格居中;窗口大于工作区时贴左上缘,保证窗口整体落在
67
- // 工作区内、可被拖拽恢复(M5——此前只 clamp 下界,窗口超屏时上界 work.right-w
68
- // 越过下界 work.left,上下界互打架会把坐标钳回负值;现上界不小于下界)。
69
- func clampCentered(winW, winH, left, top, workW, workH int) (int, int) {
70
- x := left + (workW-winW)/2
71
- y := top + (workH-winH)/2
72
- upperX := left + workW - winW
76
+ // centeredInRect 在矩形 (left,top,ww,wh) 内居中 w×h,双向 clamp 保证整体落入矩形。
77
+ func centeredInRect(left, top, ww, wh, w, h int) (int, int) {
78
+ x := left + (ww-w)/2
79
+ y := top + (wh-h)/2
80
+ // 上界不得小于下界:窗口大于工作区时 left+ww-w 会越过 left,
81
+ // 上下界互打架会把坐标钳回负值。此时贴左上缘即可。
82
+ upperX := left + ww - w
73
83
  if upperX < left {
74
84
  upperX = left
75
85
  }
76
- upperY := top + workH - winH
86
+ upperY := top + wh - h
77
87
  if upperY < top {
78
88
  upperY = top
79
89
  }
@@ -0,0 +1,60 @@
1
+ package freedom
2
+
3
+ import (
4
+ "encoding/json"
5
+ "strconv"
6
+
7
+ webview "github.com/webview/webview_go"
8
+ )
9
+
10
+ // M1 异步桥接:前端 __freedom_bridge(id, method, params) 只做投递,handler 在
11
+ // worker goroutine 中执行(不再占 UI 线程消息泵),结果经 __freedom__resolve
12
+ // 回写前端 pending Promise。并发契约:不同/相同方法的调用之间可并发执行,
13
+ // 完成顺序不保证——与 Tauri command / Electron IPC 同语义。
14
+ // window/sys/tray 内置桥保持同步(原生操作快、模态对话框语义本就需要泵)。
15
+
16
+ // bridgeAsync 是 __freedom_bridge 的绑定入口,立即返回。
17
+ func (a *App) bridgeAsync(id float64, method string, paramsJSON string) {
18
+ a.dispatchBridge(id, method, paramsJSON, a.pushResolve)
19
+ }
20
+
21
+ // dispatchBridge 把一次桥接调用投进 goroutine,完成时把回写脚本交给 onDone。
22
+ // onDone 参数即测试缝:单测注入捕获函数验证时序,无需真实 webview。
23
+ func (a *App) dispatchBridge(id float64, method, paramsJSON string, onDone func(js string)) {
24
+ go func() {
25
+ result, err := a.bridge(method, paramsJSON)
26
+ onDone(resolveJS(id, result, err))
27
+ }()
28
+ }
29
+
30
+ // pushResolve 把 resolve 脚本投递回 UI 线程执行;窗口已销毁时静默丢弃。
31
+ // 判空与 Dispatch 同在 withView 读锁临界区内,与 Run 拆除(写锁内 Destroy)互斥。
32
+ func (a *App) pushResolve(js string) {
33
+ a.withView(func(view webview.WebView) {
34
+ view.Dispatch(func() {
35
+ if !a.viewTearing.Load() {
36
+ view.Eval(js)
37
+ }
38
+ })
39
+ })
40
+ }
41
+
42
+ // resolveJS 构造回写脚本。错误按字符串回传(保持 webview_go 旧契约:
43
+ // 前端 catch 到的就是 error message 字符串)。
44
+ func resolveJS(id float64, result json.RawMessage, callErr error) string {
45
+ env := struct {
46
+ OK bool `json:"ok"`
47
+ Result json.RawMessage `json:"result,omitempty"`
48
+ Error string `json:"error,omitempty"`
49
+ }{OK: callErr == nil, Result: result}
50
+ if callErr != nil {
51
+ env.Error = callErr.Error()
52
+ }
53
+ body, err := json.Marshal(env)
54
+ if err != nil {
55
+ env.Result = nil
56
+ env.Error = "freedom: cannot marshal result: " + err.Error()
57
+ body, _ = json.Marshal(env)
58
+ }
59
+ return "window.freedom&&window.freedom.__resolve(" + strconv.FormatFloat(id, 'f', -1, 64) + "," + string(body) + ")"
60
+ }
@@ -0,0 +1,319 @@
1
+ //go:build windows
2
+
3
+ package freedom
4
+
5
+ import (
6
+ "fmt"
7
+ "sync"
8
+ "syscall"
9
+ "unsafe"
10
+ )
11
+
12
+ // Windows 原生窗口子系统(W1 对标 Tauri):
13
+ // - 窗口事件:经 comctl32 SetWindowSubclass 子类化 WebView 主窗口,
14
+ // 把 resize/move/focus/blur 转发为前端事件(window.resized 等)。
15
+ // - 关闭拦截:interceptClose 开启后 WM_CLOSE 不销毁窗口,改为向前端
16
+ // 发送 window.closeRequested;前端确认后以 close({force:true}) 放行。
17
+ // - 全屏状态:setFullscreen 记录进入前的样式与矩形,退出时还原。
18
+
19
+ const (
20
+ wmSize = 0x0005
21
+ wmMove = 0x0003
22
+ wmExitSizeMove = 0x0212
23
+ wmSetFocus = 0x0007
24
+ wmKillFocus = 0x0008
25
+
26
+ sizeMaximized = 1
27
+ sizeMinimized = 2
28
+
29
+ wsPopup = 0x80000000
30
+ wsMinimizeBox = 0x00020000
31
+ wsMaximizeBox = 0x00010000
32
+ wsExTopmost = 0x00000008
33
+ wsExToolwindow = 0x00000080
34
+
35
+ hwndTopmost = uintptr(^uintptr(0)) // HWND_TOPMOST (-1)
36
+ hwndNotopmost = uintptr(^uintptr(1)) // HWND_NOTOPMOST (-2)
37
+
38
+ swpShowWindow = 0x0040
39
+ swpHideWindow = 0x0080
40
+ )
41
+
42
+ // windowRuntime 是每个 HWND 的事件/拦截/全屏运行态。
43
+ type windowRuntime struct {
44
+ app *App
45
+ interceptClose atomicBool
46
+ fsMu sync.Mutex
47
+ fsActive bool
48
+ fsStyle uintptr
49
+ fsExStyle uintptr
50
+ fsRect rect
51
+ }
52
+
53
+ // atomicBool 用 mutex 保证跨线程读写(桥接 goroutine 写、UI 线程读)。
54
+ type atomicBool struct {
55
+ mu sync.Mutex
56
+ v bool
57
+ }
58
+
59
+ func (b *atomicBool) Store(v bool) {
60
+ b.mu.Lock()
61
+ b.v = v
62
+ b.mu.Unlock()
63
+ }
64
+
65
+ func (b *atomicBool) Load() bool {
66
+ b.mu.Lock()
67
+ v := b.v
68
+ b.mu.Unlock()
69
+ return v
70
+ }
71
+
72
+ // windowRuntimes 映射 hwnd → 运行态;子类化回调经此找回 App。
73
+ var windowRuntimes sync.Map
74
+
75
+ // 回调地址在初始化时一次性固化:syscall.NewCallback 每次调用都占用全局句柄表,
76
+ // 在热路径(每次子类化/枚举显示器)里重复 NewCallback 会泄漏且可能溢出。
77
+ var (
78
+ windowSubclassCB = syscall.NewCallback(windowSubclassProc)
79
+ enumMonitorsCB = syscall.NewCallback(enumMonitorsProc)
80
+ )
81
+
82
+ // freedomSubclassID 是 SetWindowSubclass 的子类标识。
83
+ const freedomSubclassID = 0x46524431 // "FRD1"
84
+
85
+ // gwlExStyle = GWL_EXSTYLE(-20)。用变量声明,避免 uintptr 常量转换溢出(同 gwlStyle)。
86
+ var gwlExStyle = -20
87
+
88
+ // windowSubclassProc 是 comctl32 子类化回调:转发窗口事件、实施关闭拦截。
89
+ func windowSubclassProc(hwnd, msg, wParam, lParam, subID, ref uintptr) uintptr {
90
+ v, ok := windowRuntimes.Load(hwnd)
91
+ if !ok {
92
+ r, _, _ := procDefSubclassProc.Call(hwnd, msg, wParam, lParam)
93
+ return r
94
+ }
95
+ rt := v.(*windowRuntime)
96
+ switch msg {
97
+ case wmSize:
98
+ w := int32(uint32(lParam & 0xFFFF))
99
+ h := int32(uint32((lParam >> 16) & 0xFFFF))
100
+ rt.app.Emit("window.resized", map[string]any{
101
+ "width": int(w), "height": int(h),
102
+ "maximized": wParam == sizeMaximized,
103
+ "minimized": wParam == sizeMinimized,
104
+ })
105
+ case wmMove:
106
+ x := int32(int16(uint16(lParam & 0xFFFF)))
107
+ y := int32(int16(uint16((lParam >> 16) & 0xFFFF)))
108
+ rt.app.Emit("window.moved", map[string]any{"x": int(x), "y": int(y)})
109
+ case wmSetFocus:
110
+ rt.app.Emit("window.focused", nil)
111
+ case wmKillFocus:
112
+ rt.app.Emit("window.blurred", nil)
113
+ case wmExitSizeMove:
114
+ // 用户完成拖拽/缩放/最大化切换——window-state 记忆的落点。
115
+ // 异步落盘,避免文件 IO 卡 UI 线程。
116
+ app := rt.app
117
+ go app.saveWindowStateNow()
118
+ case wmDestroy:
119
+ // 同步保存:窗口销毁后几何不可得,且此处仍在 UI 线程消息循环内,
120
+ // 分发清理尚未开始(cleanup defer 在消息循环退出之后)。
121
+ rt.app.saveWindowStateNow()
122
+ case wmClose:
123
+ if rt.interceptClose.Load() {
124
+ // 吞掉默认关闭流程(不进 DefSubclassProc → webview 收不到),
125
+ // 改由前端决策;前端以 close({force:true}) 再次触发时放行。
126
+ rt.app.Emit("window.closeRequested", nil)
127
+ return 0
128
+ }
129
+ case wmCommand:
130
+ // 主窗口原生菜单栏的条目点击:WM_COMMAND 发给菜单属主窗口,
131
+ // 与 tray 隐藏窗口的右键 popup 共用同一 id→前端 id 映射。
132
+ // 注意:不得在此锁 trayProcMu——属主线程可能正持锁跨线程建窗口,会互等死锁。
133
+ id := uint32(wParam & 0xFFFF)
134
+ if ts := trayStateInst; ts != nil {
135
+ ts.mu.Lock()
136
+ label, ok := ts.items[id]
137
+ ts.mu.Unlock()
138
+ if ok {
139
+ ts.emit("tray:menu", map[string]interface{}{"id": label})
140
+ }
141
+ }
142
+ }
143
+ r, _, _ := procDefSubclassProc.Call(hwnd, msg, wParam, lParam)
144
+ return r
145
+ }
146
+
147
+ // installWindowEvents 子类化窗口以接收事件与关闭拦截。须在窗口创建后调用。
148
+ func (a *App) installWindowEvents() {
149
+ hwnd := a.WindowHandle()
150
+ if hwnd == 0 {
151
+ return
152
+ }
153
+ rt := &windowRuntime{app: a}
154
+ windowRuntimes.Store(hwnd, rt)
155
+ // SetWindowSubclass(hwnd, pfnSubclass, uIdSubclass, dwRefData)
156
+ if r, _, _ := procSetWindowSubclass.Call(hwnd, windowSubclassCB, freedomSubclassID, 0); r == 0 {
157
+ windowRuntimes.Delete(hwnd)
158
+ }
159
+ }
160
+
161
+ // uninstallWindowEvents 摘除子类化并清理运行态映射。
162
+ func (a *App) uninstallWindowEvents() {
163
+ if hwnd := a.WindowHandle(); hwnd != 0 {
164
+ procRemoveWindowSubclass.Call(hwnd, windowSubclassCB, freedomSubclassID)
165
+ windowRuntimes.Delete(hwnd)
166
+ }
167
+ }
168
+
169
+ // runtimeFor 返回 hwnd 的运行态(调用方保证窗口已创建)。
170
+ func runtimeFor(hwnd uintptr) (*windowRuntime, error) {
171
+ v, ok := windowRuntimes.Load(hwnd)
172
+ if !ok {
173
+ return nil, fmt.Errorf("freedom: window runtime not installed for hwnd %v", hwnd)
174
+ }
175
+ return v.(*windowRuntime), nil
176
+ }
177
+
178
+ // getFullscreen 读取全屏标志(与 setFullscreen 共用 fsMu)。
179
+ func (rt *windowRuntime) getFullscreen() bool {
180
+ rt.fsMu.Lock()
181
+ defer rt.fsMu.Unlock()
182
+ return rt.fsActive
183
+ }
184
+
185
+ // getWindowRect / getClientRect 返回窗口外框与客户区矩形(物理像素)。
186
+ func getWindowRect(hwnd uintptr) (rect, error) {
187
+ var r rect
188
+ if r0, _, _ := procGetWindowRect.Call(hwnd, uintptr(unsafe.Pointer(&r))); r0 == 0 {
189
+ return rect{}, fmt.Errorf("GetWindowRect failed")
190
+ }
191
+ return r, nil
192
+ }
193
+
194
+ func getClientRect(hwnd uintptr) rect {
195
+ var r rect
196
+ procGetClientRect.Call(hwnd, uintptr(unsafe.Pointer(&r)))
197
+ return r
198
+ }
199
+
200
+ // windowScaleFactor 返回 DPI 缩放(GetDpiForWindow / 96;旧系统回退 1)。
201
+ func windowScaleFactor(hwnd uintptr) float64 {
202
+ if procGetDpiForWindow.Find() != nil {
203
+ return 1
204
+ }
205
+ dpi, _, _ := procGetDpiForWindow.Call(hwnd, 2 /*DPI_AWARENESS_EFFECTIVE*/)
206
+ if dpi == 0 {
207
+ return 1
208
+ }
209
+ return float64(dpi) / 96
210
+ }
211
+
212
+ // monitorEntry 描述一个显示器的几何与缩放(物理像素)。
213
+ type monitorEntry struct {
214
+ X int `json:"x"`
215
+ Y int `json:"y"`
216
+ Width int `json:"width"`
217
+ Height int `json:"height"`
218
+ WorkX int `json:"workX"`
219
+ WorkY int `json:"workY"`
220
+ WorkWidth int `json:"workWidth"`
221
+ WorkHeight int `json:"workHeight"`
222
+ ScaleFactor float64 `json:"scaleFactor"`
223
+ IsPrimary bool `json:"isPrimary"`
224
+ }
225
+
226
+ const monitorInfofPrimary = 1
227
+
228
+ // enumMonitorsProc 是 EnumDisplayMonitors 回调(返回 1 继续枚举)。
229
+ func enumMonitorsProc(hmon, hdc, lprci, data uintptr) uintptr {
230
+ var mi monitorInfo
231
+ mi.cbSize = uint32(unsafe.Sizeof(mi))
232
+ if r, _, _ := procGetMonitorInfo.Call(hmon, uintptr(unsafe.Pointer(&mi))); r != 0 {
233
+ scale := 1.0
234
+ if procGetDpiForMonitor.Find() == nil {
235
+ var dx uint32
236
+ // GetDpiForMonitor(hmonitor, MDT_EFFECTIVE_DPI=0, &dpiX, &dpiY);S_OK==0。
237
+ if hr, _, _ := procGetDpiForMonitor.Call(hmon, 0,
238
+ uintptr(unsafe.Pointer(&dx)), 0); hr == 0 && dx > 0 {
239
+ scale = float64(dx) / 96
240
+ }
241
+ }
242
+ monitorScan.list = append(monitorScan.list, monitorEntry{
243
+ X: int(mi.rcMonitor.left), Y: int(mi.rcMonitor.top),
244
+ Width: int(mi.rcMonitor.right - mi.rcMonitor.left),
245
+ Height: int(mi.rcMonitor.bottom - mi.rcMonitor.top),
246
+ WorkX: int(mi.rcWork.left), WorkY: int(mi.rcWork.top),
247
+ WorkWidth: int(mi.rcWork.right - mi.rcWork.left),
248
+ WorkHeight: int(mi.rcWork.bottom - mi.rcWork.top),
249
+ ScaleFactor: scale,
250
+ IsPrimary: mi.dwFlags&monitorInfofPrimary != 0,
251
+ })
252
+ }
253
+ return 1
254
+ }
255
+
256
+ var monitorScan struct {
257
+ mu sync.Mutex
258
+ list []monitorEntry
259
+ }
260
+
261
+ // listMonitors 枚举全部显示器。
262
+ func listMonitors() []monitorEntry {
263
+ monitorScan.mu.Lock()
264
+ defer monitorScan.mu.Unlock()
265
+ monitorScan.list = nil
266
+ procEnumDisplayMonitors.Call(0, 0, enumMonitorsCB, 0)
267
+ return monitorScan.list
268
+ }
269
+
270
+ // setFullscreen 进入/退出无边框全屏(覆盖整个显示器,含任务栏区域)。
271
+ func setFullscreen(hwnd uintptr, on bool) error {
272
+ rt, err := runtimeFor(hwnd)
273
+ if err != nil {
274
+ return err
275
+ }
276
+ rt.fsMu.Lock()
277
+ defer rt.fsMu.Unlock()
278
+ if on == rt.fsActive {
279
+ return nil
280
+ }
281
+ mon, _, _ := procMonitorFromWindow.Call(hwnd, monitorDefaultTonearest)
282
+ if mon == 0 {
283
+ return fmt.Errorf("MonitorFromWindow failed")
284
+ }
285
+ var mi monitorInfo
286
+ mi.cbSize = uint32(unsafe.Sizeof(mi))
287
+ if r, _, _ := procGetMonitorInfo.Call(mon, uintptr(unsafe.Pointer(&mi))); r == 0 {
288
+ return fmt.Errorf("GetMonitorInfo failed")
289
+ }
290
+ if !rt.fsActive {
291
+ // 进入:存档当前样式与矩形;最大化中的窗口先恢复,避免还原矩形失真。
292
+ if isZoomed(hwnd) {
293
+ procShowWindow.Call(hwnd, swRestore)
294
+ }
295
+ rt.fsStyle = getWindowStyle(hwnd)
296
+ rt.fsExStyle, _, _ = procGetWindowLongPtr.Call(hwnd, uintptr(int(gwlExStyle)))
297
+ if cur, err := getWindowRect(hwnd); err == nil {
298
+ rt.fsRect = cur
299
+ }
300
+ style := (rt.fsStyle &^ (wsCaption | wsThickFrame)) | wsPopup
301
+ setWindowStyle(hwnd, style)
302
+ procSetWindowLongPtr.Call(hwnd, uintptr(int(gwlExStyle)), rt.fsExStyle|wsExTopmost)
303
+ procSetWindowPos.Call(hwnd, hwndTopmost,
304
+ uintptr(int32(mi.rcMonitor.left)), uintptr(int32(mi.rcMonitor.top)),
305
+ uintptr(mi.rcMonitor.right-mi.rcMonitor.left), uintptr(mi.rcMonitor.bottom-mi.rcMonitor.top),
306
+ swpFrameChanged|swpShowWindow)
307
+ rt.fsActive = true
308
+ return nil
309
+ }
310
+ // 退出:还原样式与矩形。
311
+ setWindowStyle(hwnd, rt.fsStyle)
312
+ procSetWindowLongPtr.Call(hwnd, uintptr(int(gwlExStyle)), rt.fsExStyle)
313
+ procSetWindowPos.Call(hwnd, hwndNotopmost,
314
+ uintptr(int32(rt.fsRect.left)), uintptr(int32(rt.fsRect.top)),
315
+ uintptr(rt.fsRect.right-rt.fsRect.left), uintptr(rt.fsRect.bottom-rt.fsRect.top),
316
+ swpFrameChanged|swpShowWindow|swpNoActivate)
317
+ rt.fsActive = false
318
+ return nil
319
+ }