bun-webview2 0.3.1 → 0.4.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.
- package/CHANGELOG.md +8 -0
- package/LICENSE +1 -1
- package/README.md +12 -16
- package/native/WebView2Loader.dll +0 -0
- package/native/runner.cc +349 -0
- package/native/runner.exe +0 -0
- package/package.json +1 -1
- package/src/window.ts +17 -5
- package/native/webview.dll +0 -0
- package/src/host-ffi.ts +0 -264
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 — 2026-10-14
|
|
4
|
+
|
|
5
|
+
- **The window host is now a native executable.** After the 2026-10-14 Windows security updates, WebView2 environment creation faults at address 0x0 inside `EmbeddedBrowserWebView.dll` when hosted by `bun.exe` — any thread, any loader mode — while plain native executables work (verified by differential testing: C# WinForms ✅, plain C++ statically linked ✅, plain C++ via `LoadLibrary` ✅, Bun ❌). The host (`src/host-ffi.ts`, a Bun process driving the C library via `bun:ffi`) is replaced by `native/runner.exe`: a ~250-line C++ executable compiled with `zig c++` against the vendored webview library.
|
|
6
|
+
- The runner owns the window, the Win32 message loop, the WndProc subclass, and the native command handler; events flow as JSON lines on stdout. `src/host-ffi.ts` and `native/webview.dll` are gone — the runner statically links the C library. `native/` now ships `runner.exe`, `WebView2Loader.dll`, and the `runner.cc` source (build command in its header).
|
|
7
|
+
- Vendored C library patches, required for the updated OS/runtime combo: a real `ICoreWebView2EnvironmentOptions` object (null options fault after the update; the object must also report a `TargetCompatibleBrowserVersion`, e.g. the SDK's, or the runtime rejects creation with `E_INVALIDARG`), and the standalone WebView2 runtime (`EdgeWebView\Application`) preferred over the Edge channel's stale copy when the user hasn't chosen one explicitly.
|
|
8
|
+
- Public API unchanged: verify 21/21. Benchmarks: cold start ~0.85–0.9 s, evaluate ~0.8 ms, IPC ~0.75 ms, navigation ~140 ms — same class as the previous architecture, and the runner process is ~4 MB instead of a ~40 MB Bun child per window.
|
|
9
|
+
- `icon` window option: path to an `.ico` file, applied by the runner via `WM_SETICON` + class icon + an AppUserModelID derived from the title (taskbar groups/labels your app, not "runner"). The window belongs to the runner process, so application icons must be set this way — `rcedit` on your own exe no longer reaches the window.
|
|
10
|
+
|
|
3
11
|
## 0.3.1 — 2026-10-13
|
|
4
12
|
|
|
5
13
|
- `minWidth`/`minHeight` window options: minimum window size in client
|
package/LICENSE
CHANGED
|
@@ -2,7 +2,7 @@ MIT License
|
|
|
2
2
|
|
|
3
3
|
Copyright (c) 2026 bun-webview2 contributors
|
|
4
4
|
|
|
5
|
-
The bundled native/
|
|
5
|
+
The bundled native/runner.exe statically incorporates source code from the webview
|
|
6
6
|
project (https://github.com/webview/webview), Copyright (c) 2017 Serge
|
|
7
7
|
Zaitsev, Copyright (c) 2022 Steffen André Langnes, licensed under the MIT
|
|
8
8
|
License. The WebView2 SDK headers used to compile it are Copyright (c)
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Desktop apps with Bun on Windows — native **WebView2** windows you fully contr
|
|
|
4
4
|
|
|
5
5
|
No browser UI. No tab strip, no address bar, no Edge "App bar", no first-run nags. Your window, your icon, your process — and sub-millisecond control of the page inside it.
|
|
6
6
|
|
|
7
|
-
Zero npm runtime dependencies: the window layer is the [C `webview` library](https://github.com/webview/webview)
|
|
7
|
+
Zero npm runtime dependencies: the window layer is the [C `webview` library](https://github.com/webview/webview) compiled into a ~1 MB native runner, built once with `zig c++` (no MSVC, no cmake).
|
|
8
8
|
|
|
9
9
|
## Quick start
|
|
10
10
|
|
|
@@ -50,23 +50,23 @@ Run it with `bun run`. Close the window (or call `win.close()`) and the process
|
|
|
50
50
|
|
|
51
51
|
## How it works
|
|
52
52
|
|
|
53
|
-
Each window is owned by a dedicated **host process** — a
|
|
53
|
+
Each window is owned by a dedicated **host process** — `runner.exe`, a ~250-line native executable shipped with the library that creates one window through the C webview library and nothing else. Your process drives the page over CDP:
|
|
54
54
|
|
|
55
55
|
```mermaid
|
|
56
56
|
graph TB
|
|
57
|
-
A["Your Bun app"] -- "spawns one per window" --> H1["
|
|
58
|
-
A -- "spawns one per window" --> H2["
|
|
57
|
+
A["Your Bun app"] -- "spawns one per window" --> H1["runner.exe #1\n(native, owns the message loop)"]
|
|
58
|
+
A -- "spawns one per window" --> H2["runner.exe #2\n(native, owns the message loop)"]
|
|
59
59
|
A -- "CDP over ws://127.0.0.1:<port>" --> P1["page target #1"]
|
|
60
60
|
A -- "CDP over ws://127.0.0.1:<port>" --> P2["page target #2"]
|
|
61
61
|
H1 -- "renders" --> P1
|
|
62
62
|
H2 -- "renders" --> P2
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
- **One
|
|
65
|
+
- **One native runner per window** — `runner.exe` (C++, ~1 MB, built with `zig c++`) owns the window and the Win32 message loop. Per-window crash isolation: a dying window never takes your app down. Native by necessity: after the 2026-10-14 Windows security updates, WebView2 environment creation faults inside `bun.exe` processes (any thread, any loader mode) while plain native executables work — verified by differential testing.
|
|
66
66
|
- **Debug port** — the library passes `--remote-debugging-port` and the data folder to each host via the WebView2 loader environment variables (`WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS` / `WEBVIEW2_USER_DATA_FOLDER`). All windows of an app share one WebView2 browser process and one port; each window claims its own page target.
|
|
67
67
|
- **Everything else is CDP.** Navigation, evaluation, IPC, and screenshots flow over a loopback WebSocket using Bun's built-in client. No native module is loaded in your process.
|
|
68
|
-
- **Window commands ride the page.**
|
|
69
|
-
- **Events are split by layer.** Window-level events (`move`, `resize`, `focus`/`blur` via `WM_ACTIVATE`, `theme-changed`) are forwarded from the
|
|
68
|
+
- **Window commands ride the page.** Commands are CDP-evaluated as `bwv2_host_cmd(...)` in the page; the C library delivers them to the runner's native bind callback, and `webview_return` resolves the page-side promise the parent awaits — a resolved call IS the delivery confirmation. The message loop lives in pure native code: no JS event loop exists in the window-owning process at all, so neither the wry/tao pump freeze (the original architecture's failure mode) nor the Bun-process incompatibility can occur.
|
|
69
|
+
- **Events are split by layer.** Window-level events (`move`, `resize`, `focus`/`blur` via `WM_ACTIVATE`, `theme-changed`) are forwarded from the runner's subclassed window procedure over stdout. Page-surface events (`key-down`/`key-up`, `mouse-*`, `scroll`, `file-drop`) are captured as DOM events by an injected bridge and forwarded over the existing CDP binding — richer than Win32 could report (layout-aware `key`/`code`, readable `File` objects on drop) and working everywhere input actually lands, including cross-origin documents.
|
|
70
70
|
|
|
71
71
|
### Why all content flows through CDP (and why windows are "primed")
|
|
72
72
|
|
|
@@ -85,6 +85,7 @@ Related, and also handled for you: on a never-navigated webview target, CDP comm
|
|
|
85
85
|
| `title` | `string` | `"bun-webview2"` | Window title |
|
|
86
86
|
| `width` / `height` | `number` | `800` / `600` | Initial window size |
|
|
87
87
|
| `minWidth` / `minHeight` | `number` | — | Minimum window size (client coordinates); enforced on drags and `setSize` alike. Both must be set together. |
|
|
88
|
+
| `icon` | `string` | — | Path to an `.ico` file for the window/taskbar icon. The runner (which owns the window) applies it along with an AppUserModelID derived from the title, so taskbar entries group and label as your app. |
|
|
88
89
|
| `url` | `string` | — | Initial URL to load |
|
|
89
90
|
| `html` | `string` | — | Initial inline HTML (loaded as a `data:` URL) |
|
|
90
91
|
| `contextMenu` | `boolean` | `false` | Allow the native Chromium right-click menu (blocked by default) |
|
|
@@ -157,19 +158,14 @@ route "new window" to the running instance instead of spawning again.
|
|
|
157
158
|
|
|
158
159
|
## Distribution
|
|
159
160
|
|
|
160
|
-
- Each window spawns
|
|
161
|
-
-
|
|
162
|
-
|
|
163
|
-
bun build --compile --minify src/host-ffi.ts --outfile host.exe # inside node_modules: copy out first
|
|
164
|
-
BWV2_HOST_EXE=./host.exe ./your-app.exe
|
|
165
|
-
```
|
|
166
|
-
- `native/webview.dll` (and the host script, unless compiled) must ship **alongside** your binary — `bun build --compile` embeds neither.
|
|
167
|
-
- Give the exe a custom icon with [rcedit](https://github.com/electron/rcedit); the window inherits the process icon.
|
|
161
|
+
- Each window spawns `runner.exe` from the package's `native/` directory. End-user machines need only the WebView2 runtime (preinstalled on Windows 11 and current Windows 10).
|
|
162
|
+
- `native/runner.exe` and `native/WebView2Loader.dll` must ship **alongside** your binary — `bun build --compile` embeds neither. For custom builds, compile `native/runner.cc` yourself (the build command is in its header comment) and point `BWV2_HOST_EXE` at it.
|
|
163
|
+
- Give the app a custom icon with the `icon` option — the window belongs to the runner process, so `rcedit`-ing your own exe does not reach it.
|
|
168
164
|
- WebView2 writes its user-data folder per app; if you need to control where (e.g. Program Files installs), patch `userDataFolder` at the window layer.
|
|
169
165
|
|
|
170
166
|
## Costs, honestly
|
|
171
167
|
|
|
172
|
-
- Every window costs one
|
|
168
|
+
- Every window costs one native runner process (~4 MB) plus its share of the shared WebView2 browser tree. For 1–3 windows this is desktop-app normal.
|
|
173
169
|
- One WebView2 browser process per app means one app instance at a time (see the data-folder section above).
|
|
174
170
|
- The CDP debug port is a local-loopback control channel: fine for desktop apps, not for hostile multi-user machines (see security notes).
|
|
175
171
|
|
|
Binary file
|
package/native/runner.cc
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
// bun-webview2 native runner — owns one window + webview + message loop.
|
|
2
|
+
//
|
|
3
|
+
// This is the process that MUST NOT be a Bun process: after the 2026-10-14
|
|
4
|
+
// Windows security updates, WebView2 environment creation faults at address
|
|
5
|
+
// 0x0 inside EmbeddedBrowserWebView.dll when hosted by bun.exe (any thread,
|
|
6
|
+
// any loader mode), while plain native executables work. The runner is a
|
|
7
|
+
// plain C++ executable; the parent Bun process keeps full control of the
|
|
8
|
+
// page over CDP and receives window events as JSON lines on stdout.
|
|
9
|
+
//
|
|
10
|
+
// Protocol (identical to the TypeScript host it replaces):
|
|
11
|
+
// argv[last] = JSON config {title, width, height, minWidth?, minHeight?}
|
|
12
|
+
// stdout = one JSON object per line: window events + {"event":"closed"}
|
|
13
|
+
// page -> runner = bwv2_host_cmd("<command json>") via webview_bind;
|
|
14
|
+
// reply flows through webview_return, so the page-side
|
|
15
|
+
// promise the parent awaits resolves = command applied
|
|
16
|
+
//
|
|
17
|
+
// Build (zig c++, no MSVC, no cmake):
|
|
18
|
+
// zig c++ -std=c++17 -O2 -DWEBVIEW_MSWEBVIEW2_BUILTIN_IMPL=0 \
|
|
19
|
+
// -I <webview-src>/core/include -I <webview-src>/compatibility/mingw/include \
|
|
20
|
+
// -I <webview2-sdk>/build/native/include \
|
|
21
|
+
// native/runner.cc <webview-src>/core/src/webview.cc \
|
|
22
|
+
// <webview2-sdk>/build/native/x64/WebView2Loader.dll.lib \
|
|
23
|
+
// -ladvapi32 -lole32 -lshell32 -lshlwapi -luser32 -lversion \
|
|
24
|
+
// -o native/runner.exe
|
|
25
|
+
|
|
26
|
+
#include "webview/webview.h"
|
|
27
|
+
#include "webview/detail/json.hh"
|
|
28
|
+
|
|
29
|
+
#include <windows.h>
|
|
30
|
+
#include <shellapi.h>
|
|
31
|
+
#include <propsys.h>
|
|
32
|
+
#include <shobjidl.h>
|
|
33
|
+
#include <propkey.h>
|
|
34
|
+
#include <shlwapi.h>
|
|
35
|
+
#include <cstdio>
|
|
36
|
+
#include <cstring>
|
|
37
|
+
#include <filesystem>
|
|
38
|
+
#include <string>
|
|
39
|
+
|
|
40
|
+
namespace {
|
|
41
|
+
|
|
42
|
+
// --- window events via WndProc subclassing ---------------------------------
|
|
43
|
+
// Only non-client, window-level messages reach this procedure: input over the
|
|
44
|
+
// page surface goes to the WebView2 child HWND and is delivered to apps as
|
|
45
|
+
// page DOM events by the parent's injected bridge.
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
void emit(const std::string &line) {
|
|
68
|
+
std::fputs(line.c_str(), stdout);
|
|
69
|
+
std::fputc('\n', stdout);
|
|
70
|
+
std::fflush(stdout);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
void emit_event(const std::string &name, const std::string &fields = "") {
|
|
74
|
+
emit("{\"event\":\"" + name + "\"" + (fields.empty() ? "" : "," + fields) + "}");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
std::string number(double v) {
|
|
78
|
+
auto as_long = static_cast<long long>(v);
|
|
79
|
+
if (v == static_cast<double>(as_long)) {
|
|
80
|
+
return std::to_string(as_long);
|
|
81
|
+
}
|
|
82
|
+
return std::to_string(v);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
webview_t g_handle = nullptr;
|
|
86
|
+
HWND g_hwnd = nullptr;
|
|
87
|
+
WNDPROC g_original_proc = nullptr;
|
|
88
|
+
|
|
89
|
+
std::string theme_text() {
|
|
90
|
+
DWORD value = 0;
|
|
91
|
+
DWORD size = sizeof(value);
|
|
92
|
+
if (RegGetValueW(HKEY_CURRENT_USER,
|
|
93
|
+
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
|
94
|
+
L"AppsUseLightTheme", RRF_RT_REG_DWORD, nullptr, &value, &size) == 0 &&
|
|
95
|
+
value == 1) {
|
|
96
|
+
return "light";
|
|
97
|
+
}
|
|
98
|
+
return "dark";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
LRESULT CALLBACK runner_wndproc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
|
102
|
+
switch (msg) {
|
|
103
|
+
case WM_MOVE:
|
|
104
|
+
emit_event("move",
|
|
105
|
+
"\"x\":" + std::to_string(static_cast<short>(LOWORD(lp))) +
|
|
106
|
+
",\"y\":" + std::to_string(static_cast<short>(HIWORD(lp))));
|
|
107
|
+
break;
|
|
108
|
+
case WM_SIZE:
|
|
109
|
+
emit_event("resize",
|
|
110
|
+
"\"width\":" + std::to_string(LOWORD(lp)) +
|
|
111
|
+
",\"height\":" + std::to_string(HIWORD(lp)));
|
|
112
|
+
break;
|
|
113
|
+
case WM_ACTIVATE:
|
|
114
|
+
// window activation: clicks on the page activate the top window too,
|
|
115
|
+
// unlike WM_SETFOCUS which the WebView2 child HWND claims
|
|
116
|
+
emit_event(LOWORD(wp) != 0 ? "focus" : "blur");
|
|
117
|
+
break;
|
|
118
|
+
case WM_THEMECHANGED:
|
|
119
|
+
emit_event("theme-changed", "\"text\":\"" + theme_text() + "\"");
|
|
120
|
+
break;
|
|
121
|
+
default:
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
if (g_original_proc != nullptr) {
|
|
125
|
+
return CallWindowProcW(g_original_proc, hwnd, msg, wp, lp);
|
|
126
|
+
}
|
|
127
|
+
return DefWindowProcW(hwnd, msg, wp, lp);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// --- command channel: page -> webview_bind -> here ------------------------
|
|
131
|
+
|
|
132
|
+
std::string apply_command(const std::string &op, const std::string &args_json) {
|
|
133
|
+
const auto arg = [&args_json](int index) {
|
|
134
|
+
return webview::detail::json_parse(args_json, "", index);
|
|
135
|
+
};
|
|
136
|
+
if (op == "hello") {
|
|
137
|
+
return "true";
|
|
138
|
+
}
|
|
139
|
+
if (op == "setTitle") {
|
|
140
|
+
webview_set_title(g_handle, arg(0).c_str());
|
|
141
|
+
return "0";
|
|
142
|
+
}
|
|
143
|
+
if (op == "setSize") {
|
|
144
|
+
webview_set_size(g_handle, std::atoi(arg(0).c_str()), std::atoi(arg(1).c_str()),
|
|
145
|
+
WEBVIEW_HINT_NONE);
|
|
146
|
+
return "0";
|
|
147
|
+
}
|
|
148
|
+
if (op == "setPosition") {
|
|
149
|
+
SetWindowPos(g_hwnd, nullptr, std::atoi(arg(0).c_str()), std::atoi(arg(1).c_str()), 0, 0,
|
|
150
|
+
SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
|
|
151
|
+
return "0";
|
|
152
|
+
}
|
|
153
|
+
if (op == "setDecorations") {
|
|
154
|
+
auto current = GetWindowLongPtrW(g_hwnd, GWL_STYLE);
|
|
155
|
+
auto masked = current & ~WS_OVERLAPPEDWINDOW;
|
|
156
|
+
auto updated = arg(0) == "true" ? masked | WS_OVERLAPPEDWINDOW : masked;
|
|
157
|
+
SetWindowLongPtrW(g_hwnd, GWL_STYLE, updated);
|
|
158
|
+
SetWindowPos(g_hwnd, nullptr, 0, 0, 0, 0,
|
|
159
|
+
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
|
|
160
|
+
return "0";
|
|
161
|
+
}
|
|
162
|
+
if (op == "setAlwaysOnTop") {
|
|
163
|
+
SetWindowPos(g_hwnd, arg(0) == "true" ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0,
|
|
164
|
+
SWP_NOMOVE | SWP_NOSIZE);
|
|
165
|
+
return "0";
|
|
166
|
+
}
|
|
167
|
+
if (op == "focus") {
|
|
168
|
+
return std::to_string(SetForegroundWindow(g_hwnd));
|
|
169
|
+
}
|
|
170
|
+
if (op == "minimize") {
|
|
171
|
+
return std::to_string(ShowWindow(g_hwnd, SW_MINIMIZE));
|
|
172
|
+
}
|
|
173
|
+
if (op == "maximize") {
|
|
174
|
+
return std::to_string(ShowWindow(g_hwnd, SW_MAXIMIZE));
|
|
175
|
+
}
|
|
176
|
+
if (op == "close") {
|
|
177
|
+
// restore the original procedure before tearing the window down
|
|
178
|
+
if (g_original_proc != nullptr) {
|
|
179
|
+
SetWindowLongPtrW(g_hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(g_original_proc));
|
|
180
|
+
g_original_proc = nullptr;
|
|
181
|
+
}
|
|
182
|
+
webview_terminate(g_handle);
|
|
183
|
+
return "true";
|
|
184
|
+
}
|
|
185
|
+
return "null";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
void on_command(const char *id, const char *req, void * /*arg*/) {
|
|
189
|
+
// req is a JSON array of the call arguments; element 0 is our command payload
|
|
190
|
+
auto payload = webview::detail::json_parse(req, "", 0);
|
|
191
|
+
const auto op = webview::detail::json_parse(payload, "op", 0);
|
|
192
|
+
const auto args = webview::detail::json_parse(payload, "args", 0);
|
|
193
|
+
std::string result = "null";
|
|
194
|
+
try {
|
|
195
|
+
result = apply_command(op, args);
|
|
196
|
+
} catch (const std::exception &e) {
|
|
197
|
+
std::fprintf(stderr, "[runner] command '%s' failed: %s\n", op.c_str(), e.what());
|
|
198
|
+
}
|
|
199
|
+
webview_return(g_handle, id, 0, result.c_str());
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// --- app icon and taskbar identity -----------------------------------------
|
|
203
|
+
// The runner owns the HWND, so the app's icon must be applied here: the C
|
|
204
|
+
// library sets a generic stock icon on its window class. An AppUserModelID
|
|
205
|
+
// makes Windows group and label the window as the app, not as "runner".
|
|
206
|
+
|
|
207
|
+
bool apply_icon(const std::string &icon_path, const std::string &app_name) {
|
|
208
|
+
if (icon_path.empty()) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
std::wstring wide_path(icon_path.size(), L'\0');
|
|
212
|
+
auto path_len = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, icon_path.c_str(),
|
|
213
|
+
static_cast<int>(icon_path.size()), wide_path.data(),
|
|
214
|
+
static_cast<int>(icon_path.size()));
|
|
215
|
+
if (path_len <= 0) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
wide_path.resize(path_len);
|
|
219
|
+
auto big = LoadImageW(nullptr, wide_path.c_str(), IMAGE_ICON, GetSystemMetrics(SM_CXICON),
|
|
220
|
+
GetSystemMetrics(SM_CYICON), LR_LOADFROMFILE);
|
|
221
|
+
auto small = LoadImageW(nullptr, wide_path.c_str(), IMAGE_ICON, 16, 16, LR_LOADFROMFILE);
|
|
222
|
+
if (big == nullptr && small == nullptr) {
|
|
223
|
+
std::fprintf(stderr, "[runner] icon load failed: %s (GetLastError=%lu)\n",
|
|
224
|
+
icon_path.c_str(), GetLastError());
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
if (big != nullptr) {
|
|
228
|
+
SendMessageW(g_hwnd, WM_SETICON, ICON_BIG, reinterpret_cast<LPARAM>(big));
|
|
229
|
+
}
|
|
230
|
+
if (small != nullptr) {
|
|
231
|
+
SendMessageW(g_hwnd, WM_SETICON, ICON_SMALL, reinterpret_cast<LPARAM>(small));
|
|
232
|
+
}
|
|
233
|
+
// update the class icon so future windows of the same class inherit it
|
|
234
|
+
if (big != nullptr) {
|
|
235
|
+
SetClassLongPtrW(g_hwnd, GCLP_HICON, reinterpret_cast<LONG_PTR>(static_cast<HICON>(big)));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// AppUserModelID: taskbar groups by this, so set it per app (falls back to
|
|
239
|
+
// the runner's identity when absent)
|
|
240
|
+
if (!app_name.empty()) {
|
|
241
|
+
std::wstring aumid(L"bun-webview2.");
|
|
242
|
+
aumid += std::wstring(app_name.begin(), app_name.end());
|
|
243
|
+
for (auto &c : aumid) {
|
|
244
|
+
if (c == L' ' || c == L'.' || c == L'\\' || c == L'/') {
|
|
245
|
+
c = L'_';
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// the shell reads AUMID from the property store — the only mechanism
|
|
249
|
+
// that matters; SetPropW would be a no-op that leaks a global atom
|
|
250
|
+
IPropertyStore *store = nullptr;
|
|
251
|
+
auto hwnd_ptr = static_cast<HWND>(g_hwnd);
|
|
252
|
+
if (SUCCEEDED(SHGetPropertyStoreForWindow(hwnd_ptr, IID_IPropertyStore,
|
|
253
|
+
reinterpret_cast<void **>(&store)))) {
|
|
254
|
+
PROPVARIANT variant;
|
|
255
|
+
PropVariantInit(&variant);
|
|
256
|
+
variant.vt = VT_LPWSTR;
|
|
257
|
+
variant.pwszVal = const_cast<LPWSTR>(aumid.c_str());
|
|
258
|
+
store->SetValue(PKEY_AppUserModel_ID, variant);
|
|
259
|
+
store->Release();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// --- standalone runtime resolution -----------------------------------------
|
|
266
|
+
// Windows Update services the standalone WebView2 runtime
|
|
267
|
+
// (EdgeWebView\Application) while the Edge channel's copy can go stale and
|
|
268
|
+
// crash on environment creation. Prefer the standalone tree when present.
|
|
269
|
+
|
|
270
|
+
void resolve_standalone_runtime() {
|
|
271
|
+
if (GetEnvironmentVariableW(L"WEBVIEW2_BROWSER_EXECUTABLE_FOLDER", nullptr, 0) != 0) {
|
|
272
|
+
return; // explicit user choice
|
|
273
|
+
}
|
|
274
|
+
const std::wstring root = L"C:\\Program Files (x86)\\Microsoft\\EdgeWebView\\Application";
|
|
275
|
+
std::error_code ec;
|
|
276
|
+
if (!std::filesystem::is_directory(root, ec)) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
std::wstring newest;
|
|
280
|
+
for (const auto &entry : std::filesystem::directory_iterator(root, ec)) {
|
|
281
|
+
if (!entry.is_directory()) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const auto dir = entry.path().filename().wstring();
|
|
285
|
+
if (dir.find_first_not_of(L"0123456789.") != std::wstring::npos) {
|
|
286
|
+
continue; // version-shaped names only
|
|
287
|
+
}
|
|
288
|
+
if (!std::filesystem::exists(entry.path() / L"msedgewebview2.exe", ec)) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (newest.empty() || dir > newest) {
|
|
292
|
+
newest = dir;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (!newest.empty()) {
|
|
296
|
+
const auto folder = root + L"\\" + newest;
|
|
297
|
+
SetEnvironmentVariableW(L"WEBVIEW2_BROWSER_EXECUTABLE_FOLDER", folder.c_str());
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
} // namespace
|
|
302
|
+
|
|
303
|
+
int main(int argc, char **argv) {
|
|
304
|
+
const std::string config =
|
|
305
|
+
argc > 1 ? argv[argc - 1] : "{}";
|
|
306
|
+
const auto title = webview::detail::json_parse(config, "title", 0);
|
|
307
|
+
const auto width = webview::detail::json_parse(config, "width", 0);
|
|
308
|
+
const auto height = webview::detail::json_parse(config, "height", 0);
|
|
309
|
+
const auto min_width = webview::detail::json_parse(config, "minWidth", 0);
|
|
310
|
+
const auto min_height = webview::detail::json_parse(config, "minHeight", 0);
|
|
311
|
+
const auto icon_path = webview::detail::json_parse(config, "icon", 0);
|
|
312
|
+
|
|
313
|
+
resolve_standalone_runtime();
|
|
314
|
+
|
|
315
|
+
g_handle = webview_create(0, nullptr);
|
|
316
|
+
if (g_handle == nullptr) {
|
|
317
|
+
std::fprintf(stderr, "[runner] webview_create failed\n");
|
|
318
|
+
return 1;
|
|
319
|
+
}
|
|
320
|
+
g_hwnd = static_cast<HWND>(webview_get_window(g_handle));
|
|
321
|
+
g_original_proc = reinterpret_cast<WNDPROC>(SetWindowLongPtrW(
|
|
322
|
+
g_hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(runner_wndproc)));
|
|
323
|
+
|
|
324
|
+
webview_set_title(g_handle,
|
|
325
|
+
(title.empty() ? "bun-webview2" : title).c_str());
|
|
326
|
+
webview_set_size(g_handle, width.empty() ? 800 : std::atoi(width.c_str()),
|
|
327
|
+
height.empty() ? 600 : std::atoi(height.c_str()), WEBVIEW_HINT_NONE);
|
|
328
|
+
if (!min_width.empty() && !min_height.empty()) {
|
|
329
|
+
// the track-size minimum is in OUTER coordinates; compensate the frame
|
|
330
|
+
// so the minimum applies to the client area
|
|
331
|
+
RECT outer{};
|
|
332
|
+
RECT inner{};
|
|
333
|
+
GetWindowRect(g_hwnd, &outer);
|
|
334
|
+
GetClientRect(g_hwnd, &inner);
|
|
335
|
+
const auto frame_w = (outer.right - outer.left) - (inner.right - inner.left);
|
|
336
|
+
const auto frame_h = (outer.bottom - outer.top) - (inner.bottom - inner.top);
|
|
337
|
+
webview_set_size(g_handle, std::atoi(min_width.c_str()) + frame_w,
|
|
338
|
+
std::atoi(min_height.c_str()) + frame_h, WEBVIEW_HINT_MIN);
|
|
339
|
+
}
|
|
340
|
+
webview_bind(g_handle, "bwv2_host_cmd", on_command, nullptr);
|
|
341
|
+
apply_icon(icon_path, title.empty() ? "app" : title);
|
|
342
|
+
// inert initial document; the parent drives all real content over CDP
|
|
343
|
+
webview_navigate(g_handle, "about:blank");
|
|
344
|
+
|
|
345
|
+
webview_run(g_handle);
|
|
346
|
+
// run() returning means the window closed (user close or the "close" op)
|
|
347
|
+
emit_event("closed");
|
|
348
|
+
return 0;
|
|
349
|
+
}
|
|
Binary file
|
package/package.json
CHANGED
package/src/window.ts
CHANGED
|
@@ -33,6 +33,8 @@ export interface Webview2WindowOptions extends HardeningOptions {
|
|
|
33
33
|
readonly minWidth?: number;
|
|
34
34
|
/** @see minWidth */
|
|
35
35
|
readonly minHeight?: number;
|
|
36
|
+
/** Path to an .ico file shown as the window/taskbar icon. The runner owns the HWND, so it applies it there. */
|
|
37
|
+
readonly icon?: string;
|
|
36
38
|
/** Initial URL to load. */
|
|
37
39
|
readonly url?: string;
|
|
38
40
|
/** Initial inline HTML to load (delivered as a data: URL). */
|
|
@@ -128,21 +130,30 @@ class WindowHost {
|
|
|
128
130
|
readonly #eventWaiters = new Set<(event: string, payload: unknown) => void>();
|
|
129
131
|
|
|
130
132
|
constructor(
|
|
131
|
-
readonly options: {
|
|
133
|
+
readonly options: {
|
|
134
|
+
title: string;
|
|
135
|
+
width: number;
|
|
136
|
+
height: number;
|
|
137
|
+
minWidth?: number;
|
|
138
|
+
minHeight?: number;
|
|
139
|
+
icon?: string;
|
|
140
|
+
},
|
|
132
141
|
) {
|
|
133
142
|
const config = JSON.stringify(options);
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
143
|
+
// The runner is a plain native executable: after the 2026-10-14 Windows
|
|
144
|
+
// security updates, WebView2 environment creation faults inside bun.exe
|
|
145
|
+
// processes (any thread, any loader mode) while native executables work.
|
|
146
|
+
// Shipped apps can point BWV2_HOST_EXE at their own runner build.
|
|
137
147
|
const command = Bun.env.BWV2_HOST_EXE
|
|
138
148
|
? [Bun.env.BWV2_HOST_EXE]
|
|
139
|
-
: [
|
|
149
|
+
: [`${import.meta.dir}/../native/runner.exe`];
|
|
140
150
|
this.#child = Bun.spawn([...command, config], {
|
|
141
151
|
stdin: "ignore",
|
|
142
152
|
stdout: "pipe",
|
|
143
153
|
stderr: "inherit",
|
|
144
154
|
env: {
|
|
145
155
|
...Bun.env,
|
|
156
|
+
// runner.exe resolves WebView2Loader.dll from its own directory
|
|
146
157
|
WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS: `--remote-debugging-port=${activePort}`,
|
|
147
158
|
WEBVIEW2_USER_DATA_FOLDER: activeDataDir,
|
|
148
159
|
},
|
|
@@ -278,6 +289,7 @@ export class Webview2Window {
|
|
|
278
289
|
height: opts.height ?? 600,
|
|
279
290
|
minWidth: opts.minWidth,
|
|
280
291
|
minHeight: opts.minHeight,
|
|
292
|
+
icon: opts.icon,
|
|
281
293
|
});
|
|
282
294
|
|
|
283
295
|
const target: CdpTargetInfo = await waitForPageTarget(activePort, claimedTargets, 15_000, activeDataDir);
|
package/native/webview.dll
DELETED
|
Binary file
|
package/src/host-ffi.ts
DELETED
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Child host process (FFI flavor): owns exactly one native window + webview
|
|
3
|
-
* driven directly through the C webview library via bun:ffi — no wry, no
|
|
4
|
-
* tao, no napi addons.
|
|
5
|
-
*
|
|
6
|
-
* Command channel: the parent CDP-evaluates bwv2_host_cmd("<json>") in the
|
|
7
|
-
* page; the C library delivers it to the bind callback re-entrantly on the
|
|
8
|
-
* GUI thread (the only JS that runs here — webview_run blocks the JS loop by
|
|
9
|
-
* design, and this host deliberately has no other JS machinery to lose to a
|
|
10
|
-
* wedged loop). Replies flow back synchronously: webview_return resolves the
|
|
11
|
-
* page-side promise the parent awaits, so a resolved call IS the ack.
|
|
12
|
-
*
|
|
13
|
-
* Lifecycle: user close button or the "close" command ends webview_run; we
|
|
14
|
-
* emit "closed" on stdout and exit. Stdout JSON lines are the only
|
|
15
|
-
* host→parent channel needed.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { dlopen, FFIType, JSCallback, ptr } from "bun:ffi";
|
|
19
|
-
|
|
20
|
-
interface HostConfig {
|
|
21
|
-
readonly title: string;
|
|
22
|
-
readonly width: number;
|
|
23
|
-
readonly height: number;
|
|
24
|
-
readonly minWidth?: number;
|
|
25
|
-
readonly minHeight?: number;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
interface HostCommand {
|
|
29
|
-
readonly op: string;
|
|
30
|
-
readonly args?: unknown[];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Config is always the last argv element: [bun, host-ffi.ts, config].
|
|
34
|
-
const config = JSON.parse(process.argv[process.argv.length - 1] ?? "{}") as Partial<HostConfig>;
|
|
35
|
-
|
|
36
|
-
const dllPath = Bun.env.BWV2_NATIVE_LIB ?? `${import.meta.dir}/../native/webview.dll`;
|
|
37
|
-
|
|
38
|
-
const webview = dlopen(dllPath, {
|
|
39
|
-
webview_create: { args: [FFIType.int, FFIType.pointer], returns: FFIType.pointer },
|
|
40
|
-
webview_run: { args: [FFIType.pointer], returns: FFIType.int },
|
|
41
|
-
webview_terminate: { args: [FFIType.pointer], returns: FFIType.int },
|
|
42
|
-
webview_navigate: { args: [FFIType.pointer, FFIType.pointer], returns: FFIType.int },
|
|
43
|
-
webview_set_title: { args: [FFIType.pointer, FFIType.pointer], returns: FFIType.int },
|
|
44
|
-
webview_set_size: { args: [FFIType.pointer, FFIType.int, FFIType.int, FFIType.int], returns: FFIType.int },
|
|
45
|
-
webview_get_window: { args: [FFIType.pointer], returns: FFIType.pointer },
|
|
46
|
-
webview_bind: { args: [FFIType.pointer, FFIType.pointer, FFIType.pointer, FFIType.pointer], returns: FFIType.int },
|
|
47
|
-
webview_return: { args: [FFIType.pointer, FFIType.pointer, FFIType.int, FFIType.pointer], returns: FFIType.int },
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// Win32 window controls the C API doesn't cover.
|
|
51
|
-
const user32 = dlopen("user32.dll", {
|
|
52
|
-
SetWindowPos: { args: [FFIType.pointer, FFIType.i64, FFIType.i32, FFIType.i32, FFIType.i32, FFIType.i32, FFIType.u32], returns: FFIType.int },
|
|
53
|
-
GetWindowLongPtrW: { args: [FFIType.pointer, FFIType.i32], returns: FFIType.i64 },
|
|
54
|
-
SetWindowLongPtrW: { args: [FFIType.pointer, FFIType.i32, FFIType.i64], returns: FFIType.i64 },
|
|
55
|
-
GetWindowRect: { args: [FFIType.pointer, FFIType.pointer], returns: FFIType.int },
|
|
56
|
-
GetClientRect: { args: [FFIType.pointer, FFIType.pointer], returns: FFIType.int },
|
|
57
|
-
ShowWindow: { args: [FFIType.pointer, FFIType.i32], returns: FFIType.int },
|
|
58
|
-
SetForegroundWindow: { args: [FFIType.pointer], returns: FFIType.int },
|
|
59
|
-
CallWindowProcW: { args: [FFIType.pointer, FFIType.pointer, FFIType.u32, FFIType.u64, FFIType.i64], returns: FFIType.i64 },
|
|
60
|
-
TrackMouseEvent: { args: [FFIType.pointer], returns: FFIType.int },
|
|
61
|
-
GetKeyState: { args: [FFIType.i32], returns: FFIType.i16 },
|
|
62
|
-
});
|
|
63
|
-
const advapi32 = dlopen("advapi32.dll", {
|
|
64
|
-
RegGetValueW: { args: [FFIType.pointer, FFIType.pointer, FFIType.pointer, FFIType.u32, FFIType.u32, FFIType.pointer, FFIType.pointer], returns: FFIType.i32 },
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
const cstr = (s: string) => ptr(Buffer.from(`${s}\0`, "utf8"));
|
|
68
|
-
|
|
69
|
-
const WEBVIEW_HINT_NONE = 0;
|
|
70
|
-
const WEBVIEW_HINT_MIN = 1;
|
|
71
|
-
const SWP_NOSIZE = 0x0001;
|
|
72
|
-
const SWP_NOMOVE = 0x0002;
|
|
73
|
-
const SWP_NOZORDER = 0x0004;
|
|
74
|
-
const SWP_NOACTIVATE = 0x0010;
|
|
75
|
-
const SWP_FRAMECHANGED = 0x0020;
|
|
76
|
-
const GWL_STYLE = -16;
|
|
77
|
-
const WS_OVERLAPPEDWINDOW = 0x00cf0000;
|
|
78
|
-
const SW_MINIMIZE = 6;
|
|
79
|
-
const SW_MAXIMIZE = 3;
|
|
80
|
-
const HWND_TOPMOST = -1;
|
|
81
|
-
const HWND_NOTOPMOST = -2;
|
|
82
|
-
|
|
83
|
-
if (Bun.env.BWV2_DEBUG === "1") console.error("[host-ffi] started");
|
|
84
|
-
|
|
85
|
-
const handle = webview.symbols.webview_create(0, null);
|
|
86
|
-
if (handle === 0n || handle === 0) {
|
|
87
|
-
console.error("[host-ffi] webview_create failed");
|
|
88
|
-
process.exit(1);
|
|
89
|
-
}
|
|
90
|
-
const hwnd = webview.symbols.webview_get_window(handle);
|
|
91
|
-
|
|
92
|
-
const emit = (payload: Record<string, unknown>): void => {
|
|
93
|
-
console.log(JSON.stringify(payload));
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
// --- Window events via WndProc subclassing -------------------------------
|
|
97
|
-
// The flat C API has no event subscriptions, so the window's own procedure
|
|
98
|
-
// is replaced: interesting messages become stdout events (the parent's
|
|
99
|
-
// generic routing picks them up); everything chains to the original proc.
|
|
100
|
-
|
|
101
|
-
const GWL_WNDPROC = -4;
|
|
102
|
-
const WM_MOVE = 0x0003;
|
|
103
|
-
const WM_SIZE = 0x0005;
|
|
104
|
-
// Only non-client, window-level messages reach this procedure: input over
|
|
105
|
-
// the page surface goes to the WebView2 child HWND and is delivered to apps
|
|
106
|
-
// as page DOM events instead (src/inject.ts page-event bridge). Keys,
|
|
107
|
-
// mouse, wheel, and file drops therefore have no WndProc cases at all.
|
|
108
|
-
const WM_ACTIVATE = 0x0006;
|
|
109
|
-
const WM_THEMECHANGED = 0x031a;
|
|
110
|
-
|
|
111
|
-
// personalization: 1 = apps light theme, 0 = dark
|
|
112
|
-
const themeText = (): "light" | "dark" => {
|
|
113
|
-
const value = Buffer.alloc(4);
|
|
114
|
-
const size = Buffer.alloc(4);
|
|
115
|
-
size.writeUInt32LE(4, 0);
|
|
116
|
-
const subkey = Buffer.from("Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize\0", "utf16le");
|
|
117
|
-
const valueName = Buffer.from("AppsUseLightTheme\0", "utf16le");
|
|
118
|
-
const ok = advapi32.symbols.RegGetValueW(0x80000001n /* HKEY_CURRENT_USER */, ptr(subkey), ptr(valueName), 0x0002 /* RRF_RT_REG_DWORD */, 0, ptr(value), ptr(size));
|
|
119
|
-
return ok === 0 && value.readUInt32LE(0) === 1 ? "light" : "dark";
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
const originalProc: { handle: bigint | undefined } = { handle: undefined };
|
|
123
|
-
|
|
124
|
-
const wndProc = new JSCallback(
|
|
125
|
-
(hwndArg: number | bigint, msg: number | bigint, wParam: number | bigint, lParam: number | bigint) => {
|
|
126
|
-
// FFI delivers u64/i64 as number or bigint; normalize once at the boundary
|
|
127
|
-
const hwnd = BigInt(hwndArg);
|
|
128
|
-
const msgN = Number(msg);
|
|
129
|
-
const wp = BigInt.asUintN(64, BigInt(wParam));
|
|
130
|
-
const lp = BigInt.asIntN(64, BigInt(lParam));
|
|
131
|
-
switch (msgN) {
|
|
132
|
-
case WM_MOVE: {
|
|
133
|
-
const l = BigInt.asUintN(32, lp);
|
|
134
|
-
emit({ event: "move", x: Number(BigInt.asIntN(16, l & 0xffffn)), y: Number(BigInt.asIntN(16, (l >> 16n) & 0xffffn)) });
|
|
135
|
-
break;
|
|
136
|
-
}
|
|
137
|
-
case WM_SIZE: {
|
|
138
|
-
const l = BigInt.asUintN(32, lp);
|
|
139
|
-
emit({ event: "resize", width: Number(l & 0xffffn), height: Number((l >> 16n) & 0xffffn) });
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
|
-
case WM_ACTIVATE: {
|
|
143
|
-
// window activation: clicks on the page activate the top window too,
|
|
144
|
-
// unlike WM_SETFOCUS which the WebView2 child HWND claims.
|
|
145
|
-
const active = (BigInt.asUintN(16, wp) & 0xffffn) !== 0n;
|
|
146
|
-
emit({ event: active ? "focus" : "blur" });
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
case WM_THEMECHANGED:
|
|
150
|
-
emit({ event: "theme-changed", text: themeText() });
|
|
151
|
-
break;
|
|
152
|
-
}
|
|
153
|
-
// chain everything (handled or not) to the original procedure
|
|
154
|
-
if (originalProc.handle !== undefined) {
|
|
155
|
-
return user32.symbols.CallWindowProcW(originalProc.handle, hwnd, msgN, wp, lp);
|
|
156
|
-
}
|
|
157
|
-
return 0n;
|
|
158
|
-
},
|
|
159
|
-
{ args: [FFIType.pointer, FFIType.u32, FFIType.u64, FFIType.i64], returns: FFIType.i64 },
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
// Subclass the window so the parent receives native window events.
|
|
163
|
-
originalProc.handle = user32.symbols.SetWindowLongPtrW(hwnd, GWL_WNDPROC, BigInt(wndProc.ptr ?? 0));
|
|
164
|
-
|
|
165
|
-
function applyCommand(command: HostCommand): string {
|
|
166
|
-
const args = command.args ?? [];
|
|
167
|
-
switch (command.op) {
|
|
168
|
-
case "hello":
|
|
169
|
-
return "true";
|
|
170
|
-
case "setTitle":
|
|
171
|
-
return String(webview.symbols.webview_set_title(handle, cstr(String(args[0]))));
|
|
172
|
-
case "setSize":
|
|
173
|
-
return String(webview.symbols.webview_set_size(handle, Number(args[0]), Number(args[1]), WEBVIEW_HINT_NONE));
|
|
174
|
-
case "setPosition": {
|
|
175
|
-
const result = user32.symbols.SetWindowPos(
|
|
176
|
-
hwnd,
|
|
177
|
-
0n,
|
|
178
|
-
Number(args[0]),
|
|
179
|
-
Number(args[1]),
|
|
180
|
-
0,
|
|
181
|
-
0,
|
|
182
|
-
SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE,
|
|
183
|
-
);
|
|
184
|
-
return String(result);
|
|
185
|
-
}
|
|
186
|
-
case "setDecorations": {
|
|
187
|
-
const current = user32.symbols.GetWindowLongPtrW(hwnd, GWL_STYLE);
|
|
188
|
-
const decorationsOn = Boolean(args[0]);
|
|
189
|
-
const masked = BigInt(current) & ~BigInt(WS_OVERLAPPEDWINDOW);
|
|
190
|
-
const updated = decorationsOn ? masked | BigInt(WS_OVERLAPPEDWINDOW) : masked;
|
|
191
|
-
const result = user32.symbols.SetWindowLongPtrW(hwnd, GWL_STYLE, updated);
|
|
192
|
-
user32.symbols.SetWindowPos(hwnd, 0n, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
|
|
193
|
-
return String(result);
|
|
194
|
-
}
|
|
195
|
-
case "setAlwaysOnTop": {
|
|
196
|
-
const insertAfter = args[0] ? BigInt(HWND_TOPMOST) : BigInt(HWND_NOTOPMOST);
|
|
197
|
-
const result = user32.symbols.SetWindowPos(hwnd, insertAfter, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
|
|
198
|
-
return String(result);
|
|
199
|
-
}
|
|
200
|
-
case "focus":
|
|
201
|
-
return String(user32.symbols.SetForegroundWindow(hwnd));
|
|
202
|
-
case "minimize":
|
|
203
|
-
return String(user32.symbols.ShowWindow(hwnd, SW_MINIMIZE));
|
|
204
|
-
case "maximize":
|
|
205
|
-
return String(user32.symbols.ShowWindow(hwnd, SW_MAXIMIZE));
|
|
206
|
-
|
|
207
|
-
case "close":
|
|
208
|
-
// restore the original procedure before tearing the window down
|
|
209
|
-
if (originalProc.handle !== undefined) {
|
|
210
|
-
user32.symbols.SetWindowLongPtrW(hwnd, GWL_WNDPROC, BigInt(originalProc.handle));
|
|
211
|
-
originalProc.handle = undefined;
|
|
212
|
-
}
|
|
213
|
-
webview.symbols.webview_terminate(handle);
|
|
214
|
-
return "true";
|
|
215
|
-
default:
|
|
216
|
-
return "null";
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const bindCallback = new JSCallback(
|
|
221
|
-
(id: string, req: string) => {
|
|
222
|
-
if (Bun.env.BWV2_DEBUG === "1") console.error(`[host-ffi] op: ${req.slice(0, 80)}`);
|
|
223
|
-
let result = "null";
|
|
224
|
-
try {
|
|
225
|
-
const parsed = JSON.parse(req) as unknown[];
|
|
226
|
-
const payload = typeof parsed[0] === "string" ? (JSON.parse(parsed[0]) as HostCommand) : (parsed[0] as HostCommand);
|
|
227
|
-
result = applyCommand(payload);
|
|
228
|
-
} catch (err) {
|
|
229
|
-
console.error("[host-ffi] command failed:", err);
|
|
230
|
-
}
|
|
231
|
-
webview.symbols.webview_return(handle, cstr(id), 0, cstr(result));
|
|
232
|
-
},
|
|
233
|
-
{ args: [FFIType.cstring, FFIType.cstring, FFIType.pointer], returns: FFIType.void },
|
|
234
|
-
);
|
|
235
|
-
|
|
236
|
-
webview.symbols.webview_set_title(handle, cstr(config.title ?? "bun-webview2"));
|
|
237
|
-
webview.symbols.webview_set_size(handle, config.width ?? 800, config.height ?? 600, WEBVIEW_HINT_NONE);
|
|
238
|
-
if (typeof config.minWidth === "number" && typeof config.minHeight === "number") {
|
|
239
|
-
// WEBVIEW_HINT_MIN stores m_minsz; WM_GETMINMAXINFO enforces it on drags
|
|
240
|
-
// and programmatic resizes alike. The track size is in OUTER coordinates,
|
|
241
|
-
// so the frame delta is added to keep the minimum a CLIENT-size minimum.
|
|
242
|
-
const outer = Buffer.alloc(16);
|
|
243
|
-
const inner = Buffer.alloc(16);
|
|
244
|
-
user32.symbols.GetWindowRect(hwnd, ptr(outer));
|
|
245
|
-
user32.symbols.GetClientRect(hwnd, ptr(inner));
|
|
246
|
-
const frameW = outer.readInt32LE(8) - outer.readInt32LE(0) - (inner.readInt32LE(8) - inner.readInt32LE(0));
|
|
247
|
-
const frameH = outer.readInt32LE(12) - outer.readInt32LE(4) - (inner.readInt32LE(12) - inner.readInt32LE(4));
|
|
248
|
-
webview.symbols.webview_set_size(
|
|
249
|
-
handle,
|
|
250
|
-
Math.max(1, config.minWidth + frameW),
|
|
251
|
-
Math.max(1, config.minHeight + frameH),
|
|
252
|
-
WEBVIEW_HINT_MIN,
|
|
253
|
-
);
|
|
254
|
-
}
|
|
255
|
-
webview.symbols.webview_bind(handle, cstr("bwv2_host_cmd"), bindCallback.ptr, null);
|
|
256
|
-
|
|
257
|
-
// about:blank keeps the initial document inert; the parent drives all real
|
|
258
|
-
// content over CDP.
|
|
259
|
-
webview.symbols.webview_navigate(handle, cstr("about:blank"));
|
|
260
|
-
|
|
261
|
-
webview.symbols.webview_run(handle);
|
|
262
|
-
// run() returning means the window closed (user X or the "close" command).
|
|
263
|
-
emit({ event: "closed" });
|
|
264
|
-
process.exit(0);
|