@yufengtadian/freedom-cli 1.12.1 → 1.12.13

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 (47) hide show
  1. package/README.md +31 -14
  2. package/bin/freedom.js +5 -1
  3. package/lib/build.js +49 -8
  4. package/lib/cli.js +107 -80
  5. package/lib/config.js +108 -31
  6. package/lib/shell.js +73 -20
  7. package/lib/theme.js +105 -0
  8. package/lib/tui.js +21 -3
  9. package/lib/update.js +109 -0
  10. package/lib/utils.js +14 -1
  11. package/package.json +1 -1
  12. package/shell/win-x64/freedom-shell.exe +0 -0
  13. package/templates/go/go.mod +6 -0
  14. package/templates/go/go.sum +2 -2
  15. package/templates/go/pkg/freedom/assets/freedom.js +6 -0
  16. package/templates/go/pkg/freedom/assets/index.html +1 -1
  17. package/templates/go/pkg/freedom/backend_proc.go +4 -4
  18. package/templates/go/pkg/freedom/configfile.go +12 -7
  19. package/templates/go/pkg/freedom/freedom.go +27 -11
  20. package/templates/go/pkg/freedom/window_other.go +64 -12
  21. package/templates/go/pkg/freedom/window_windows.go +244 -29
  22. package/templates/go/webview_go/.github/workflows/ci.yaml +62 -0
  23. package/templates/go/webview_go/CHANGELOG.md +15 -0
  24. package/templates/go/webview_go/LICENSE +22 -0
  25. package/templates/go/webview_go/README.md +50 -0
  26. package/templates/go/webview_go/examples/basic/main.go +12 -0
  27. package/templates/go/webview_go/examples/bind/main.go +38 -0
  28. package/templates/go/webview_go/glue.c +36 -0
  29. package/templates/go/webview_go/go.mod +3 -0
  30. package/templates/go/webview_go/go.sum +0 -0
  31. package/templates/go/webview_go/libs/mswebview2/LICENSE +27 -0
  32. package/templates/go/webview_go/libs/mswebview2/include/WebView2.h +23568 -0
  33. package/templates/go/webview_go/libs/mswebview2/include/vendor.go +2 -0
  34. package/templates/go/webview_go/libs/mswebview2/vendor.go +2 -0
  35. package/templates/go/webview_go/libs/mswebview2/version.txt +1 -0
  36. package/templates/go/webview_go/libs/webview/LICENSE +22 -0
  37. package/templates/go/webview_go/libs/webview/include/vendor.go +2 -0
  38. package/templates/go/webview_go/libs/webview/include/webview.h +3871 -0
  39. package/templates/go/webview_go/libs/webview/vendor.go +2 -0
  40. package/templates/go/webview_go/libs/webview/version.txt +1 -0
  41. package/templates/go/webview_go/webview.cc +1 -0
  42. package/templates/go/webview_go/webview.go +379 -0
  43. package/templates/go/webview_go/webview_test.go +50 -0
  44. package/templates/project/freedom.config.js +5 -5
  45. package/templates/project/index.html +45 -5
  46. package/templates/project/src/main.js +149 -8
  47. package/tutorial/tutorial.html +4 -5
@@ -45,7 +45,7 @@
45
45
  font-size: 13px;
46
46
  }
47
47
 
48
- /* 无边框模式:前端自绘标题栏(hidden / frameless 时显示) */
48
+ /* 无边框模式:前端自绘标题栏(frameless 时显示) */
49
49
  .titlebar {
50
50
  display: none;
51
51
  height: 36px;
@@ -134,12 +134,12 @@ func (p *ProcBackend) Handle(method string, params []json.RawMessage) (interface
134
134
  id := p.nextID
135
135
  ch := make(chan procResp, 1)
136
136
  p.pending[id] = ch
137
- msg := procMessage{ID: id, Method: method, Params: paramsJSON}
138
- line, _ := json.Marshal(msg)
139
- _, err = p.stdin.Write(append(line, '\n'))
137
+ w := p.stdin // 锁内取引用,锁外写,避免持锁阻塞(后端不消费 stdin Write 可能挂起)
140
138
  p.mu.Unlock()
141
139
 
142
- if err != nil {
140
+ msg := procMessage{ID: id, Method: method, Params: paramsJSON}
141
+ line, _ := json.Marshal(msg)
142
+ if _, err := w.Write(append(line, '\n')); err != nil {
143
143
  p.cancel(id, fmt.Errorf("freedom: proc backend write: %w", err))
144
144
  return nil, err
145
145
  }
@@ -12,6 +12,8 @@ package freedom
12
12
 
13
13
  import (
14
14
  "encoding/json"
15
+ "errors"
16
+ "fmt"
15
17
  "os"
16
18
  "path/filepath"
17
19
  )
@@ -47,19 +49,24 @@ func resourcesDir() (string, error) {
47
49
  }
48
50
 
49
51
  // loadRuntimeConfig 读取 exe 同目录 resources/config.json,将命中的字段覆盖到应用配置。
50
- // 文件不存在或内容非法时静默回退(返回 nil),保持编译期/默认配置不变。
52
+ // 文件不存在时视为未配置(返回 nil,保持编译期/默认配置不变);
53
+ // 文件存在但读取/解析失败时返回具体错误,由调用方打印告警,避免用户手改配置出错时无感知。
51
54
  func (a *App) loadRuntimeConfig() error {
52
55
  dir, err := resourcesDir()
53
56
  if err != nil {
54
- return nil
57
+ return err
55
58
  }
56
- data, err := os.ReadFile(filepath.Join(dir, "config.json"))
59
+ cfgPath := filepath.Join(dir, "config.json")
60
+ data, err := os.ReadFile(cfgPath)
57
61
  if err != nil {
58
- return nil
62
+ if errors.Is(err, os.ErrNotExist) {
63
+ return nil // 未配置:正常回退默认
64
+ }
65
+ return fmt.Errorf("read %s: %w", cfgPath, err)
59
66
  }
60
67
  var rc runtimeConfigFile
61
68
  if err := json.Unmarshal(data, &rc); err != nil {
62
- return nil
69
+ return fmt.Errorf("parse %s: %w", cfgPath, err)
63
70
  }
64
71
  if rc.Title != "" {
65
72
  a.cfg.Title = rc.Title
@@ -69,8 +76,6 @@ func (a *App) loadRuntimeConfig() error {
69
76
  switch rc.TitleBar {
70
77
  case "native":
71
78
  a.cfg.TitleBar = TitleBarNative
72
- case "hidden":
73
- a.cfg.TitleBar = TitleBarHidden
74
79
  case "frameless":
75
80
  a.cfg.TitleBar = TitleBarFrameless
76
81
  }
@@ -29,14 +29,11 @@ import (
29
29
  type TitleBarMode string
30
30
 
31
31
  const (
32
- // TitleBarNative 保留系统原生标题栏(默认)。
32
+ // TitleBarNative 保留系统原生标题栏(默认),标题栏图标与 exe 图标一致。
33
33
  TitleBarNative TitleBarMode = "native"
34
- // TitleBarHidden 隐藏标题栏视觉,但保留系统原生最小化 / 最大化 / 关闭按钮。
35
- // 当前仅 Windows 生效(基于 DWM 扩展实现);macOS / Linux 上回退为原生标题栏。
36
- TitleBarHidden TitleBarMode = "hidden"
37
34
  // TitleBarFrameless 完全无边框,客户区铺满整个窗口。
38
- // 最小化 / 最大化 / 关闭按钮需由前端自绘(通过 window.freedom.window.* 控制),
39
- // 三平台行为一致。
35
+ // 标题栏与系统原生最小化 / 最大化 / 关闭按钮均不存在,
36
+ // 需由前端自绘(通过 window.freedom.window.* 控制),三平台行为一致。
40
37
  TitleBarFrameless TitleBarMode = "frameless"
41
38
  )
42
39
 
@@ -125,7 +122,10 @@ func (a *App) Unbind(name string) {
125
122
  func (a *App) Run() {
126
123
  // 通用壳:先加载 exe 同目录 resources/config.json 覆盖窗口与后端配置
127
124
  //(CLI build 时写入;缺失则使用编译期/默认配置)。
128
- _ = a.loadRuntimeConfig()
125
+ // 配置存在但非法时打印告警(不中断启动),避免用户手改配置出错时静默无感。
126
+ if err := a.loadRuntimeConfig(); err != nil {
127
+ fmt.Printf("freedom: warning: %v\n", err)
128
+ }
129
129
 
130
130
  html, err := a.resolveHTML()
131
131
  if err != nil {
@@ -162,6 +162,7 @@ func (a *App) Run() {
162
162
  }
163
163
  a.applyCenter()
164
164
  a.applyTitleBar()
165
+ a.setWindowIcon()
165
166
 
166
167
  // 注入前端 SDK:window.freedom 全局对象。
167
168
  w.Init(jsSDK)
@@ -195,7 +196,15 @@ func (a *App) Run() {
195
196
  // bridge 是前端调用后端的统一入口(JSON-RPC 风格)。
196
197
  // 前端 SDK 通过 window.__freedom_bridge(method, paramsJson) 调用,
197
198
  // 桥接层把请求转发给当前绑定的后端(内嵌 Go 方法或任意语言进程)。
198
- func (a *App) bridge(method string, paramsJSON string) (json.RawMessage, error) {
199
+ func (a *App) bridge(method string, paramsJSON string) (result json.RawMessage, err error) {
200
+ // 防御后端方法 panic 导致整个窗口崩溃:统一转为错误回传前端。
201
+ defer func() {
202
+ if r := recover(); r != nil {
203
+ result = nil
204
+ err = fmt.Errorf("freedom: method %q panicked: %v", method, r)
205
+ }
206
+ }()
207
+
199
208
  var params []json.RawMessage
200
209
  if len(paramsJSON) > 0 && paramsJSON != "null" {
201
210
  if err := json.Unmarshal([]byte(paramsJSON), &params); err != nil {
@@ -203,11 +212,18 @@ func (a *App) bridge(method string, paramsJSON string) (json.RawMessage, error)
203
212
  }
204
213
  }
205
214
 
206
- result, err := a.backend.Handle(method, params)
215
+ // 框架内置方法特判:__freedom__ping webview Bind 注册为全局函数
216
+ // window.__freedom__ping(),同时兼容经 freedom.call()/__freedom_bridge 路由的旧写法,
217
+ // 避免模板"测试桥接"自检报"method not bound"。
218
+ if method == "__freedom__ping" {
219
+ return json.RawMessage(`"pong"`), nil
220
+ }
221
+
222
+ raw, err := a.backend.Handle(method, params)
207
223
  if err != nil {
208
224
  return nil, err
209
225
  }
210
- data, err := json.Marshal(result)
226
+ data, err := json.Marshal(raw)
211
227
  if err != nil {
212
228
  return nil, fmt.Errorf("freedom: method %q: cannot marshal result: %w", method, err)
213
229
  }
@@ -251,7 +267,7 @@ func (a *App) WindowHandle() uintptr {
251
267
  // windowControl 处理前端 window.freedom.window.* 的窗口控制请求。
252
268
  // 具体实现按平台分文件:window_windows.go(Windows)/ window_other.go(macOS、Linux)。
253
269
  func (a *App) windowControl(action string) (interface{}, error) {
254
- return windowControl(a.WindowHandle(), action, a.cfg.TitleBar)
270
+ return windowControl(a, action)
255
271
  }
256
272
 
257
273
  // resolveHTML 依据配置返回页面内容。
@@ -2,24 +2,76 @@
2
2
 
3
3
  package freedom
4
4
 
5
- import "fmt"
5
+ import (
6
+ "fmt"
6
7
 
7
- // applyTitleBar 在 macOS / Linux 上为空实现:
8
- // 原生标题栏由窗口管理器接管,hidden / frameless 模式暂回退为原生标题栏,
9
- // 前端仍可通过 window.freedom.window.isFrameless() 感知当前是否无边框。
10
- func (a *App) applyTitleBar() {}
8
+ webview "github.com/webview/webview_go"
9
+ )
11
10
 
12
- // windowControl 处理前端 window.freedom.window.* 请求(macOS / Linux 占位实现)。
13
- func windowControl(hwnd uintptr, action string, mode TitleBarMode) (interface{}, error) {
11
+ // applyTitleBar macOS / Linux 上通过 webview 层的 set_decorated 控制原生标题栏:
12
+ // - frameless:GTK gtk_window_set_decorated(FALSE),Cocoa 走隐藏标题栏 + 透明标题栏
13
+ // + 移除原生最小化/关闭按钮,彻底去除"UI 上方残留原生标题栏"问题;
14
+ // - native:恢复系统原生标题栏。
15
+ //
16
+ // 窗口在 webview.New() 时已创建,故此处可安全调用(须在 UI 线程,即 Run 流程内)。
17
+ func (a *App) applyTitleBar() {
18
+ if a.view == nil {
19
+ return
20
+ }
21
+ a.view.SetDecorated(a.cfg.TitleBar != TitleBarFrameless)
22
+ }
23
+
24
+ // setWindowIcon 在 macOS / Linux 上为空实现(图标由应用包 / 窗口管理器决定)。
25
+ func (a *App) setWindowIcon() {}
26
+
27
+ // windowControl 处理前端 window.freedom.window.* 请求(macOS / Linux 实现)。
28
+ //
29
+ // 窗口控制类动作(最小化/最大化/还原/关闭/查询)直接转发到 webview 层的原生
30
+ // 窗口控制(GTK GtkWindow / Cocoa NSWindow),前端自绘标题栏三按钮因此可用:
31
+ // - GTK: gtk_window_iconify / maximize / unmaximize / close / is_maximized
32
+ // - Cocoa: performMiniaturize: / zoom: / performClose: / isZoomed
33
+ //
34
+ // 注意:webview 层窗口控制动作在 UI 线程执行(前端桥接本身运行于 UI 线程回调中,
35
+ // webview_go 的 binding 回调在主线程派发),无需额外 Dispatch。
36
+ func windowControl(a *App, action string) (interface{}, error) {
37
+ if a.view == nil {
38
+ return nil, fmt.Errorf("window not ready")
39
+ }
40
+ var act webview.WindowAction
14
41
  switch action {
15
- case "isFrameless":
16
- return mode == TitleBarFrameless || mode == TitleBarHidden, nil
42
+ case "minimize":
43
+ act = webview.WindowMinimize
44
+ case "maximize":
45
+ act = webview.WindowMaximize
46
+ case "unmaximize", "restore":
47
+ act = webview.WindowRestore
48
+ case "toggleMaximize":
49
+ act = webview.WindowToggleMaximize
50
+ case "close":
51
+ act = webview.WindowClose
17
52
  case "isMaximized":
18
- return false, nil
19
- case "minimize", "maximize", "unmaximize", "restore", "toggleMaximize", "close":
20
- // Windows 平台暂不提供底层窗口控制,前端自绘按钮可对事件静默处理
53
+ act = webview.WindowIsMaximized
54
+ case "isFrameless":
55
+ return a.cfg.TitleBar == TitleBarFrameless, nil
56
+ case "appIcon":
57
+ // macOS / Linux 暂不提供 exe 内嵌图标提取,前端隐藏标题栏图标。
58
+ return "", nil
59
+ case "startDrag":
60
+ // 无边框窗口拖动:GTK 走 gtk_window_begin_move_drag(root_x/y=-1 用当前指针),
61
+ // Cocoa 走 performWindowDragWithEvent:(用当前鼠标位置合成事件)。
62
+ // 与 Windows 的 WM_NCLBUTTONDOWN+HTCAPTION 语义等价,保留页面双击/右键事件。
63
+ if a.view.BeginMoveDrag() != 0 {
64
+ return nil, fmt.Errorf("window drag is not supported on this platform")
65
+ }
21
66
  return nil, nil
22
67
  default:
23
68
  return nil, fmt.Errorf("unknown window action %q", action)
24
69
  }
70
+ if act == webview.WindowIsMaximized {
71
+ return a.view.WindowControl(act) == 1, nil
72
+ }
73
+ if r := a.view.WindowControl(act); r != 0 {
74
+ return nil, fmt.Errorf("window action %q is not supported on this platform", action)
75
+ }
76
+ return nil, nil
25
77
  }
@@ -3,9 +3,16 @@
3
3
  package freedom
4
4
 
5
5
  import (
6
+ "bytes"
7
+ "encoding/base64"
8
+ "encoding/binary"
6
9
  "fmt"
10
+ "image"
11
+ "image/color"
12
+ "image/png"
7
13
  "syscall"
8
- "unsafe"
14
+
15
+ "golang.org/x/sys/windows"
9
16
  )
10
17
 
11
18
  // 进程级 DPI 感知:让窗口坐标 / WebView 渲染统一按物理像素工作。
@@ -27,6 +34,7 @@ func init() {
27
34
 
28
35
  var (
29
36
  user32win = syscall.NewLazyDLL("user32.dll")
37
+ kernel32 = syscall.NewLazyDLL("kernel32.dll")
30
38
 
31
39
  procGetWindowLongPtr = user32win.NewProc("GetWindowLongPtrW")
32
40
  procSetWindowLongPtr = user32win.NewProc("SetWindowLongPtrW")
@@ -35,22 +43,41 @@ var (
35
43
  procSetWindowPos = user32win.NewProc("SetWindowPos")
36
44
  procSetWindowText = user32win.NewProc("SetWindowTextW")
37
45
  procPostMessage = user32win.NewProc("PostMessageW")
46
+ procGetModuleHandle = kernel32.NewProc("GetModuleHandleW")
47
+ procLoadImage = user32win.NewProc("LoadImageW")
48
+ procGetSystemMetrics = user32win.NewProc("GetSystemMetrics")
49
+ procSendMessage = user32win.NewProc("SendMessageW")
50
+ procReleaseCapture = user32win.NewProc("ReleaseCapture")
51
+ procGetWindowRect = user32win.NewProc("GetWindowRect")
52
+ procMonitorFromWindow = user32win.NewProc("MonitorFromWindow")
53
+ procGetMonitorInfo = user32win.NewProc("GetMonitorInfoW")
54
+ procMoveWindow = user32win.NewProc("MoveWindow")
55
+
56
+ procDestroyIcon = user32win.NewProc("DestroyIcon")
38
57
 
39
58
  dwmapi = syscall.NewLazyDLL("dwmapi.dll")
40
59
  procDwmExtendFrameIntoArea = dwmapi.NewProc("DwmExtendFrameIntoClientArea")
41
60
  )
42
61
 
43
62
  const (
44
- wsCaption = 0x00C00000 // WS_CAPTION = WS_BORDER | WS_DLGFRAME
45
- wsSysMenu = 0x00080000
63
+ wsCaption = 0x00C00000 // WS_CAPTION = WS_BORDER | WS_DLGFRAME
64
+ wsSysMenu = 0x00080000
46
65
  wsThickFrame = 0x00040000
47
66
 
48
- wmClose = 0x0010 // WM_CLOSE:请求窗口正常关闭(触发 GoWnd 的关闭回调)
49
- swHide = 0
50
- swShow = 5
51
- swMinimize = 6
52
- swRestore = 9
53
- swMaximize = 3
67
+ wmClose = 0x0010 // WM_CLOSE:请求窗口正常关闭(触发 GoWnd 的关闭回调)
68
+ wmSetIcon = 0x0080 // WM_SETICON:设置窗口大/小图标(ICON_BIG=1 / ICON_SMALL=0
69
+
70
+ rtIcon = 3 // RT_ICON 资源类型(rcedit 注入图标二进制)
71
+ swHide = 0
72
+ swShow = 5
73
+ swMinimize = 6
74
+ swRestore = 9
75
+ swMaximize = 3
76
+
77
+ smCXIcon = 11 // SM_CXICON
78
+ smCYIcon = 12 // SM_CYICON
79
+ smCXSmall = 13 // SM_CXSMICON
80
+ smCYSmall = 14 // SM_CYSMICON
54
81
 
55
82
  swpFrameChanged = 0x0020
56
83
  swpNoMove = 0x0002
@@ -82,7 +109,8 @@ func refreshFrame(hwnd uintptr) {
82
109
  }
83
110
 
84
111
  // windowControl 处理前端 window.freedom.window.* 请求(Windows 实现)。
85
- func windowControl(hwnd uintptr, action string, mode TitleBarMode) (interface{}, error) {
112
+ func windowControl(a *App, action string) (interface{}, error) {
113
+ hwnd := a.WindowHandle()
86
114
  if hwnd == 0 {
87
115
  return nil, fmt.Errorf("window not ready")
88
116
  }
@@ -91,16 +119,16 @@ func windowControl(hwnd uintptr, action string, mode TitleBarMode) (interface{},
91
119
  procShowWindow.Call(hwnd, swMinimize)
92
120
  return nil, nil
93
121
  case "maximize":
94
- procShowWindow.Call(hwnd, swMaximize)
122
+ maximizeWindow(hwnd)
95
123
  return nil, nil
96
124
  case "unmaximize", "restore":
97
- procShowWindow.Call(hwnd, swRestore)
125
+ restoreWindow(hwnd)
98
126
  return nil, nil
99
127
  case "toggleMaximize":
100
- if isZoomed(hwnd) {
101
- procShowWindow.Call(hwnd, swRestore)
128
+ if isWindowMaximized(hwnd) {
129
+ restoreWindow(hwnd)
102
130
  } else {
103
- procShowWindow.Call(hwnd, swMaximize)
131
+ maximizeWindow(hwnd)
104
132
  }
105
133
  return nil, nil
106
134
  case "close":
@@ -109,14 +137,101 @@ func windowControl(hwnd uintptr, action string, mode TitleBarMode) (interface{},
109
137
  procPostMessage.Call(hwnd, wmClose, 0, 0)
110
138
  return nil, nil
111
139
  case "isMaximized":
112
- return isZoomed(hwnd), nil
140
+ return isWindowMaximized(hwnd), nil
113
141
  case "isFrameless":
114
- return mode == TitleBarFrameless || mode == TitleBarHidden, nil
142
+ return a.cfg.TitleBar == TitleBarFrameless, nil
143
+ case "appIcon":
144
+ // 从 exe 内嵌图标(PE 资源 ID=1)提取,转为 PNG data URL 供前端标题栏显示。
145
+ // 不依赖 resources 文件夹里的任何图标文件。
146
+ return exeAppIconDataURL(), nil
147
+ case "startDrag":
148
+ // 前端标题栏 mousedown 时发起窗口拖动(WM_NCLBUTTONDOWN + HTCAPTION)。
149
+ // 相比 -webkit-app-region: drag,可保留双击最大化 / 右键菜单等页面事件。
150
+ procReleaseCapture.Call()
151
+ procSendMessage.Call(hwnd, 0x00A1 /*WM_NCLBUTTONDOWN*/, 2 /*HTCAPTION*/, 0)
152
+ return nil, nil
115
153
  default:
116
154
  return nil, fmt.Errorf("unknown window action %q", action)
117
155
  }
118
156
  }
119
157
 
158
+ // maximizeWindow 最大化窗口。
159
+ // 直接走系统 SW_MAXIMIZE:壳层(webview_go fork)在 WndProc 的 WM_GETMINMAXINFO 中
160
+ // 已把无边框窗口的最大化边界限定到监视器工作区(rcWork),
161
+ // 因此"最大化 = 铺满任务栏上方的工作区",而非 F11 式整屏全屏。
162
+ func maximizeWindow(hwnd uintptr) {
163
+ procShowWindow.Call(hwnd, swMaximize)
164
+ }
165
+
166
+ // restoreWindow 还原窗口(系统 SW_RESTORE,恢复到最大化前的位置与尺寸)。
167
+ func restoreWindow(hwnd uintptr) {
168
+ procShowWindow.Call(hwnd, swRestore)
169
+ }
170
+
171
+ // isWindowMaximized 判断窗口当前是否处于最大化状态。
172
+ func isWindowMaximized(hwnd uintptr) bool {
173
+ return isZoomed(hwnd)
174
+ }
175
+
176
+ // exeIconData 读取当前 exe 内嵌的 RT_ICON 资源原始字节(rcedit 注入时资源 ID=1)。
177
+ // 不依赖 RT_GROUP_ICON 的 ID(rcedit 固定写入 0,而 LoadImageW 按硬编码 ID 加载会失败),
178
+ // 直接取 RT_ICON 二进制,兼容任意注入工具。
179
+ // 使用 x/sys/windows 类型化 API(LoadResourceData 直接返回字节副本,无需 unsafe 指针转换)。
180
+ func exeIconData() []byte {
181
+ var hInst windows.Handle
182
+ if err := windows.GetModuleHandleEx(0, nil, &hInst); err != nil {
183
+ return nil
184
+ }
185
+ // FindResourceW(hModule, MAKEINTRESOURCE(1), MAKEINTRESOURCE(RT_ICON))
186
+ resInfo, err := windows.FindResource(hInst, windows.ResourceID(1), windows.ResourceID(rtIcon))
187
+ if err != nil {
188
+ return nil
189
+ }
190
+ data, err := windows.LoadResourceData(hInst, resInfo)
191
+ if err != nil {
192
+ return nil
193
+ }
194
+ return data
195
+ }
196
+
197
+ // isPNG 判断字节流是否为 PNG 编码(rcedit 注入的 .ico 内嵌 PNG 时 RT_ICON 数据即 PNG)。
198
+ func isPNG(b []byte) bool {
199
+ return len(b) >= 8 && b[0] == 0x89 && b[1] == 'P' && b[2] == 'N' && b[3] == 'G'
200
+ }
201
+
202
+ // loadExeIcon 从 exe 内嵌的 RT_GROUP_ICON 资源加载 HICON。
203
+ // rcedit 注入的 group icon 资源 ID 固定为 0(LoadImageW 需按该 ID 加载;
204
+ // 历史按 ID=1 硬编码导致加载失败,故此处显式用 0)。
205
+ // 调用方用完须 procDestroyIcon 释放;加载失败返回 0。
206
+ func loadExeIcon(cx, cy uintptr) uintptr {
207
+ hInst, _, _ := procGetModuleHandle.Call(0)
208
+ if hInst == 0 {
209
+ return 0
210
+ }
211
+ // LoadImageW(hInst, MAKEINTRESOURCE(0), IMAGE_ICON, cx, cy, LR_DEFAULTCOLOR)
212
+ r, _, _ := procLoadImage.Call(hInst, 0, 1 /*IMAGE_ICON*/, cx, cy, 0 /*LR_DEFAULTCOLOR*/)
213
+ return r
214
+ }
215
+
216
+ // exeAppIconDataURL 从当前 exe 的 PE 资源图标(rcedit 注入的 RT_ICON)提取为 PNG data URL。
217
+ // 无法提取时返回空字符串(前端隐藏图标)。
218
+ func exeAppIconDataURL() string {
219
+ data := exeIconData()
220
+ if len(data) == 0 {
221
+ return ""
222
+ }
223
+ // rcedit 注入 .ico 内嵌 PNG 时,RT_ICON 数据即 PNG 字节,直接编码
224
+ if isPNG(data) {
225
+ return "data:image/png;base64," + base64.StdEncoding.EncodeToString(data)
226
+ }
227
+ // 非 PNG(BMP/DIB):纯 Go 解析像素并编码为 PNG,无需 GDI 绘制
228
+ img, err := dibToPNG(data)
229
+ if err != nil {
230
+ return ""
231
+ }
232
+ return "data:image/png;base64," + base64.StdEncoding.EncodeToString(img)
233
+ }
234
+
120
235
  func isZoomed(hwnd uintptr) bool {
121
236
  r, _, _ := procIsZoomed.Call(hwnd)
122
237
  return r != 0
@@ -130,21 +245,121 @@ func (a *App) applyTitleBar() {
130
245
  }
131
246
  switch a.cfg.TitleBar {
132
247
  case TitleBarFrameless:
133
- // 完全无边框:去掉标题栏 / 系统菜单,客户区铺满整个窗口。
134
- // 最小化 / 最大化 / 关闭按钮由前端自绘(window.freedom.window.*)。
248
+ // 完全无边框:去掉标题栏 / 系统菜单 / 可调边框(WS_THICKFRAME)。
249
+ // - 标题栏与系统原生最小化 / 最大化 / 关闭按钮均不存在,由前端自绘
250
+ // (window.freedom.window.*)接管;
251
+ // - 去掉 WS_THICKFRAME 是最大化行为正确的关键:只要该位存在,系统在
252
+ // WM_GETMINMAXINFO 之后仍会按 AdjustWindowRectEx 追加标题栏 + 边框空间,
253
+ // 导致最大化窗口溢出工作区、盖住任务栏(F11 全屏感);
254
+ // - 边缘拖拽 resize 由壳层 WndProc 的 WM_NCHITTEST 手动实现(返回 HT* 边界值),
255
+ // 不依赖 WS_THICKFRAME。
135
256
  style := getWindowStyle(hwnd)
136
- style &^= wsCaption | wsSysMenu
257
+ style &^= wsCaption | wsSysMenu | wsThickFrame
137
258
  setWindowStyle(hwnd, style)
138
259
  refreshFrame(hwnd)
139
- case TitleBarHidden:
140
- // 隐藏标题栏视觉但保留系统原生按钮:DWM 玻璃扩展。
141
- // 标题栏区域透明化并并入客户区,右上角的最小化 / 最大化 / 关闭按钮
142
- // 由 DWM 继续原生绘制,标题文字置空。
143
- m := margins{cxLeftWidth: 0, cxRightWidth: 0, cyTopHeight: 0, cyBottomHeight: 1}
144
- procDwmExtendFrameIntoArea.Call(hwnd, uintptr(unsafe.Pointer(&m)))
145
- // 标题文字一并清除,标题栏区域只保留系统按钮
146
- procSetWindowText.Call(hwnd, uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(""))))
260
+ default: // TitleBarNative:补齐系统原生标题栏与可调边框。
261
+ // webview_go 底层创建的窗口默认 style 不含 WS_CAPTION(见 WebView2 窗口创建),
262
+ // 因此 native 模式必须显式补回标题栏 / 系统菜单 / 可调边框,
263
+ // 否则窗口会表现为无边框(无原生三按钮、不可拖拽调整大小)。
264
+ style := getWindowStyle(hwnd)
265
+ style |= wsCaption | wsSysMenu | wsThickFrame
266
+ setWindowStyle(hwnd, style)
147
267
  refreshFrame(hwnd)
148
- default: // TitleBarNative:不处理
149
268
  }
150
269
  }
270
+
271
+ // setWindowIcon 把 exe 内嵌的应用程序图标同步到窗口标题栏与任务栏:
272
+ // - native 模式:标题栏图标(ICON_SMALL)与任务栏 / Alt-Tab 图标(ICON_BIG)均来自 exe 图标,
273
+ // 保证「标题栏图标 = exe 图标」;
274
+ // - frameless 模式:无标题栏,但任务栏 / Alt-Tab 仍显示 exe 图标。
275
+ //
276
+ // 图标经 LoadImageW 从 PE 资源 RT_GROUP_ICON(ID=0) 加载(rcedit 注入的 group icon ID=0),
277
+ // 不依赖 resources 文件夹里的任何图标文件。
278
+ func (a *App) setWindowIcon() {
279
+ hwnd := a.WindowHandle()
280
+ if hwnd == 0 {
281
+ return
282
+ }
283
+ cxIcon, _, _ := procGetSystemMetrics.Call(smCXIcon)
284
+ cyIcon, _, _ := procGetSystemMetrics.Call(smCYIcon)
285
+ cxSmall, _, _ := procGetSystemMetrics.Call(smCXSmall)
286
+ cySmall, _, _ := procGetSystemMetrics.Call(smCYSmall)
287
+ big := loadExeIcon(cxIcon, cyIcon)
288
+ small := loadExeIcon(cxSmall, cySmall)
289
+ if big != 0 {
290
+ procSendMessage.Call(hwnd, wmSetIcon, 1 /* ICON_BIG */, big)
291
+ procDestroyIcon.Call(big)
292
+ }
293
+ if small != 0 {
294
+ procSendMessage.Call(hwnd, wmSetIcon, 0 /* ICON_SMALL */, small)
295
+ procDestroyIcon.Call(small)
296
+ }
297
+ }
298
+
299
+ // dibToPNG 将 ICO 内嵌的 DIB 图像(BITMAPINFOHEADER 起始的裸 DIB,即 rcedit 注入
300
+ // 非 PNG .ico 时的 RT_ICON 数据)纯 Go 解析为 PNG 字节,替代原 GDI
301
+ // CreateDIBSection+DrawIconEx 绘制路径(消除对 gdi32/user32 绘图 API 的依赖)。
302
+ // 支持 32/24 bpp;透明由 AND mask 决定(对应位=1 表示透明)。
303
+ func dibToPNG(data []byte) ([]byte, error) {
304
+ if len(data) < 40 {
305
+ return nil, fmt.Errorf("DIB too short: %d bytes", len(data))
306
+ }
307
+ biSize := binary.LittleEndian.Uint32(data[0:4])
308
+ if biSize < 40 {
309
+ return nil, fmt.Errorf("unsupported BITMAPINFOHEADER size %d", biSize)
310
+ }
311
+ width := int(binary.LittleEndian.Uint32(data[4:8]))
312
+ heightRaw := int32(binary.LittleEndian.Uint32(data[8:12]))
313
+ bitCount := binary.LittleEndian.Uint16(data[14:16])
314
+ if width <= 0 || heightRaw == 0 {
315
+ return nil, fmt.Errorf("bad dimensions %dx%d", width, heightRaw)
316
+ }
317
+ bpp := (int(bitCount) + 7) / 8 // bytes per pixel
318
+ if bpp != 3 && bpp != 4 {
319
+ return nil, fmt.Errorf("unsupported bitcount %d", bitCount)
320
+ }
321
+ height := int(heightRaw)
322
+ if height < 0 {
323
+ height = -height
324
+ }
325
+ // ICO 的 DIB 高度可能为实际像素高度的 2 倍(上方 XOR + 下方 AND mask),
326
+ // 此时需折半,否则图像会被纵向拉伸。
327
+ pixelRowBytes := (width*bpp + 3) &^ 3 // 每行按 4 字节对齐
328
+ andRowBytes := ((width + 31) / 32) * 4
329
+ pxLen := pixelRowBytes * height
330
+ if pxLen > len(data)-int(biSize) && height%2 == 0 {
331
+ height /= 2
332
+ pxLen = pixelRowBytes * height
333
+ }
334
+ pxStart := int(biSize)
335
+ andStart := pxStart + pxLen
336
+ hasAndMask := heightRaw > 0 // 正高度表示 DIB 末尾带 AND mask
337
+ if !hasAndMask {
338
+ hasAndMask = andStart+andRowBytes*height <= len(data)
339
+ }
340
+ img := image.NewRGBA(image.Rect(0, 0, width, height))
341
+ for y := 0; y < height; y++ {
342
+ srcY := height - 1 - y // DIB 自底向上:数据首行是图像末行
343
+ row := pxStart + srcY*pixelRowBytes
344
+ andRow := andStart + srcY*andRowBytes
345
+ for x := 0; x < width; x++ {
346
+ off := row + x*bpp
347
+ if off+bpp > len(data) {
348
+ return nil, fmt.Errorf("DIB data truncated at x=%d,y=%d", x, y)
349
+ }
350
+ b := data[off]
351
+ g := data[off+1]
352
+ r := data[off+2]
353
+ a := uint8(255)
354
+ if hasAndMask && (data[andRow+x/8]>>uint(7-x%8))&1 == 1 {
355
+ a = 0
356
+ }
357
+ img.SetRGBA(x, y, color.RGBA{r, g, b, a})
358
+ }
359
+ }
360
+ var buf bytes.Buffer
361
+ if err := png.Encode(&buf, img); err != nil {
362
+ return nil, err
363
+ }
364
+ return buf.Bytes(), nil
365
+ }
@@ -0,0 +1,62 @@
1
+ name: CI Pipeline
2
+ on: [push, pull_request]
3
+
4
+ jobs:
5
+ build:
6
+ runs-on: ${{ matrix.image }}
7
+ strategy:
8
+ fail-fast: false
9
+ matrix:
10
+ # Go 1.23 was the latest version as of 2024-08-31 but the action output a "warning":
11
+ # Failed to restore: "/usr/bin/tar" failed with error: The process '/usr/bin/tar' failed with exit code 2
12
+ # Go 1.22 didn't output the above "warning" so we're using this version.
13
+ # Go 1.14 made -mod=vendor default.
14
+ # Go 1.13 is the version set in go.mod.
15
+ include:
16
+ - { image: macos-14, go: '1.22' }
17
+ - { image: macos-14, go: '1.21' }
18
+ - { image: macos-14, go: '1.20' }
19
+ # Go 1.19 is the earliest version available for ARM64.
20
+ - { image: macos-14, go: '1.19' }
21
+ # macos-12 runner has x86_64 support which is needed for old versions of Go
22
+ - { image: macos-12, go: '1.18' }
23
+ - { image: macos-12, go: '1.17' }
24
+ - { image: macos-12, go: '1.16' }
25
+ - { image: macos-12, go: '1.15' }
26
+ - { image: macos-12, go: '1.14' }
27
+ - { image: macos-12, go: '1.13' }
28
+ - { image: ubuntu-22.04, go: '1.22', apt: libgtk-4-dev libwebkitgtk-6.0-dev }
29
+ - { image: ubuntu-22.04, go: '1.22', apt: libgtk-3-dev libwebkit2gtk-4.1-dev }
30
+ - { image: ubuntu-22.04, go: '1.22', apt: libgtk-3-dev libwebkit2gtk-4.0-dev }
31
+ - { image: ubuntu-22.04, go: '1.21', apt: libgtk-3-dev libwebkit2gtk-4.1-dev }
32
+ - { image: ubuntu-22.04, go: '1.20', apt: libgtk-3-dev libwebkit2gtk-4.1-dev }
33
+ - { image: ubuntu-22.04, go: '1.19', apt: libgtk-3-dev libwebkit2gtk-4.1-dev }
34
+ - { image: ubuntu-22.04, go: '1.18', apt: libgtk-3-dev libwebkit2gtk-4.1-dev }
35
+ - { image: ubuntu-22.04, go: '1.17', apt: libgtk-3-dev libwebkit2gtk-4.1-dev }
36
+ - { image: ubuntu-22.04, go: '1.16', apt: libgtk-3-dev libwebkit2gtk-4.1-dev }
37
+ - { image: ubuntu-22.04, go: '1.15', apt: libgtk-3-dev libwebkit2gtk-4.1-dev }
38
+ - { image: ubuntu-22.04, go: '1.14', apt: libgtk-3-dev libwebkit2gtk-4.1-dev }
39
+ - { image: ubuntu-22.04, go: '1.13', apt: libgtk-3-dev libwebkit2gtk-4.1-dev }
40
+ - { image: windows-2022, go: '1.22' }
41
+ - { image: windows-2022, go: '1.21' }
42
+ # Go 1.20 is the earliest version that is usable in this environment as of 2024-08-31.
43
+ # Compilation fails with older versions:
44
+ # [...]/bin/ld.exe: [...]\go-link-3209855222\000008.o: in function `x_cgo_thread_start':
45
+ # \\_\_\runtime\cgo/gcc_util.c:18: undefined reference to `__imp___iob_func'
46
+ - { image: windows-2022, go: '1.20' }
47
+ name: Build (${{ matrix.image }}, go ${{ matrix.go }})
48
+ steps:
49
+ - uses: actions/checkout@v4
50
+ - name: Install dependencies
51
+ if: runner.os == 'Linux'
52
+ run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.0-dev
53
+ - uses: actions/setup-go@v5
54
+ with:
55
+ go-version: ${{ matrix.go }}
56
+ - name: Build examples
57
+ run: >
58
+ go build
59
+ ./examples/basic
60
+ ./examples/bind
61
+ - name: Run tests
62
+ run: go test
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ ### Migrating from v0.1.1 to v0.10.0
4
+
5
+ 1. `Webview.Open()` has been removed. Use other webview APIs to create a window, open a link and run main UI loop.
6
+ 2. `Webview.Debug()` and `webview.Debugf()` have been removed. Use your favorite logging library to debug webview apps.
7
+ 3. `Webview.Settings` struct has been removed. Title, URL and size are controlled via other API setters and can be updated at any time, not only when webview is created.
8
+ 4. `Webview.Loop()` has been removed. Use `Run()` instead.
9
+ 5. `WebView.Run()`, `WebView.Terminate()`, `WebView.SetTitle()`, `WebView.Dispatch()` stayed the same.
10
+ 6. `WebView.Exit()` has been renamed to `WebView.Destroy()`
11
+ 7. `WebView.SetColor()` and `WebView.SetFullScreen()` have been removed. Use `Window()` to get native window handle and probably write some Cgo code to adjust native window to your taste.
12
+ 8. `Webview.Dialog` has been removed. But it is likely to be brought back as a standalone module.
13
+ 9. `WebView.Eval()` remained the same.
14
+ 10. `WebView.InjectCSS()` has been removed. Use eval to inject style tag with CSS inside.
15
+ 11. `WebView.Bind()` kept the name, but changed the semantics. Only functions can be bound. Not the structs, like in Lorca.