@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.
- package/README.md +31 -14
- package/bin/freedom.js +5 -1
- package/lib/build.js +49 -8
- package/lib/cli.js +107 -80
- package/lib/config.js +108 -31
- package/lib/shell.js +73 -20
- package/lib/theme.js +105 -0
- package/lib/tui.js +21 -3
- package/lib/update.js +109 -0
- package/lib/utils.js +14 -1
- package/package.json +1 -1
- package/shell/win-x64/freedom-shell.exe +0 -0
- package/templates/go/go.mod +6 -0
- package/templates/go/go.sum +2 -2
- package/templates/go/pkg/freedom/assets/freedom.js +6 -0
- package/templates/go/pkg/freedom/assets/index.html +1 -1
- package/templates/go/pkg/freedom/backend_proc.go +4 -4
- package/templates/go/pkg/freedom/configfile.go +12 -7
- package/templates/go/pkg/freedom/freedom.go +27 -11
- package/templates/go/pkg/freedom/window_other.go +64 -12
- package/templates/go/pkg/freedom/window_windows.go +244 -29
- package/templates/go/webview_go/.github/workflows/ci.yaml +62 -0
- package/templates/go/webview_go/CHANGELOG.md +15 -0
- package/templates/go/webview_go/LICENSE +22 -0
- package/templates/go/webview_go/README.md +50 -0
- package/templates/go/webview_go/examples/basic/main.go +12 -0
- package/templates/go/webview_go/examples/bind/main.go +38 -0
- package/templates/go/webview_go/glue.c +36 -0
- package/templates/go/webview_go/go.mod +3 -0
- package/templates/go/webview_go/go.sum +0 -0
- package/templates/go/webview_go/libs/mswebview2/LICENSE +27 -0
- package/templates/go/webview_go/libs/mswebview2/include/WebView2.h +23568 -0
- package/templates/go/webview_go/libs/mswebview2/include/vendor.go +2 -0
- package/templates/go/webview_go/libs/mswebview2/vendor.go +2 -0
- package/templates/go/webview_go/libs/mswebview2/version.txt +1 -0
- package/templates/go/webview_go/libs/webview/LICENSE +22 -0
- package/templates/go/webview_go/libs/webview/include/vendor.go +2 -0
- package/templates/go/webview_go/libs/webview/include/webview.h +3871 -0
- package/templates/go/webview_go/libs/webview/vendor.go +2 -0
- package/templates/go/webview_go/libs/webview/version.txt +1 -0
- package/templates/go/webview_go/webview.cc +1 -0
- package/templates/go/webview_go/webview.go +379 -0
- package/templates/go/webview_go/webview_test.go +50 -0
- package/templates/project/freedom.config.js +5 -5
- package/templates/project/index.html +45 -5
- package/templates/project/src/main.js +149 -8
- package/tutorial/tutorial.html +4 -5
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
52
|
+
// 文件不存在时视为未配置(返回 nil,保持编译期/默认配置不变);
|
|
53
|
+
// 文件存在但读取/解析失败时返回具体错误,由调用方打印告警,避免用户手改配置出错时无感知。
|
|
51
54
|
func (a *App) loadRuntimeConfig() error {
|
|
52
55
|
dir, err := resourcesDir()
|
|
53
56
|
if err != nil {
|
|
54
|
-
return
|
|
57
|
+
return err
|
|
55
58
|
}
|
|
56
|
-
|
|
59
|
+
cfgPath := filepath.Join(dir, "config.json")
|
|
60
|
+
data, err := os.ReadFile(cfgPath)
|
|
57
61
|
if err != nil {
|
|
58
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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), ¶ms); err != nil {
|
|
@@ -203,11 +212,18 @@ func (a *App) bridge(method string, paramsJSON string) (json.RawMessage, error)
|
|
|
203
212
|
}
|
|
204
213
|
}
|
|
205
214
|
|
|
206
|
-
|
|
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(
|
|
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
|
|
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
|
|
5
|
+
import (
|
|
6
|
+
"fmt"
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
// 前端仍可通过 window.freedom.window.isFrameless() 感知当前是否无边框。
|
|
10
|
-
func (a *App) applyTitleBar() {}
|
|
8
|
+
webview "github.com/webview/webview_go"
|
|
9
|
+
)
|
|
11
10
|
|
|
12
|
-
//
|
|
13
|
-
|
|
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 "
|
|
16
|
-
|
|
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
|
-
|
|
19
|
-
case "
|
|
20
|
-
|
|
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
|
-
|
|
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
|
|
45
|
-
wsSysMenu
|
|
63
|
+
wsCaption = 0x00C00000 // WS_CAPTION = WS_BORDER | WS_DLGFRAME
|
|
64
|
+
wsSysMenu = 0x00080000
|
|
46
65
|
wsThickFrame = 0x00040000
|
|
47
66
|
|
|
48
|
-
wmClose
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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(
|
|
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
|
-
|
|
122
|
+
maximizeWindow(hwnd)
|
|
95
123
|
return nil, nil
|
|
96
124
|
case "unmaximize", "restore":
|
|
97
|
-
|
|
125
|
+
restoreWindow(hwnd)
|
|
98
126
|
return nil, nil
|
|
99
127
|
case "toggleMaximize":
|
|
100
|
-
if
|
|
101
|
-
|
|
128
|
+
if isWindowMaximized(hwnd) {
|
|
129
|
+
restoreWindow(hwnd)
|
|
102
130
|
} else {
|
|
103
|
-
|
|
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
|
|
140
|
+
return isWindowMaximized(hwnd), nil
|
|
113
141
|
case "isFrameless":
|
|
114
|
-
return
|
|
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
|
-
//
|
|
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
|
-
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
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.
|