bunite-core 0.11.0 → 0.11.2

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.
@@ -0,0 +1,887 @@
1
+ #include "webview2_internal.h"
2
+
3
+ #include <algorithm>
4
+
5
+ using Microsoft::WRL::Callback;
6
+ using Microsoft::WRL::ComPtr;
7
+ using Microsoft::WRL::Make;
8
+
9
+ namespace bunite_webview2 {
10
+
11
+ RuntimeState g_runtime;
12
+
13
+ static HINSTANCE g_module = nullptr;
14
+ static bool g_co_initialized = false;
15
+
16
+ HINSTANCE getCurrentModuleHandle() {
17
+ if (g_module) return g_module;
18
+ HMODULE mod = nullptr;
19
+ GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
20
+ GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
21
+ reinterpret_cast<LPCWSTR>(&getCurrentModuleHandle), &mod);
22
+ g_module = static_cast<HINSTANCE>(mod);
23
+ return g_module;
24
+ }
25
+
26
+ // ---- task queue + pump ---------------------------------------------------
27
+ //
28
+ // Wake-message coalescing: one outstanding `kRunQueuedTaskMessage` is enough
29
+ // to guarantee a drain. Clearing the flag inside the message handler (before
30
+ // draining) preserves liveness for tasks posted during the drain itself.
31
+
32
+ static std::atomic<bool> g_wake_pending{false};
33
+
34
+ void postUiTask(std::function<void()> task) {
35
+ {
36
+ std::lock_guard<std::mutex> g(g_runtime.task_mutex);
37
+ g_runtime.queued_tasks.push_back(std::move(task));
38
+ }
39
+ bool expected = false;
40
+ if (g_runtime.message_window &&
41
+ g_wake_pending.compare_exchange_strong(expected, true)) {
42
+ PostMessageW(g_runtime.message_window, kRunQueuedTaskMessage, 0, 0);
43
+ }
44
+ }
45
+
46
+ void executeQueuedUiTasks() {
47
+ std::deque<std::function<void()>> drained;
48
+ {
49
+ std::lock_guard<std::mutex> g(g_runtime.task_mutex);
50
+ drained.swap(g_runtime.queued_tasks);
51
+ }
52
+ for (auto& t : drained) {
53
+ if (t) t();
54
+ }
55
+ }
56
+
57
+ void pumpOnce() {
58
+ MSG msg;
59
+ while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
60
+ if (msg.message == WM_QUIT) {
61
+ g_runtime.shutting_down.store(true);
62
+ return;
63
+ }
64
+ TranslateMessage(&msg);
65
+ DispatchMessageW(&msg);
66
+ }
67
+ // Drain queued tasks even if no message arrived.
68
+ executeQueuedUiTasks();
69
+ }
70
+
71
+ // ---- window class + message window ---------------------------------------
72
+
73
+ LRESULT CALLBACK windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp);
74
+ LRESULT CALLBACK messageProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp);
75
+
76
+ static bool ensureClassRegistered(WNDCLASSEXW& cls) {
77
+ WNDCLASSEXW probe{};
78
+ probe.cbSize = sizeof(probe);
79
+ if (GetClassInfoExW(cls.hInstance, cls.lpszClassName, &probe)) return true;
80
+ if (RegisterClassExW(&cls) == 0) {
81
+ DWORD err = GetLastError();
82
+ if (err != ERROR_CLASS_ALREADY_EXISTS) {
83
+ BUNITE_ERROR("webview2: RegisterClassExW(%ls) failed err=%lu",
84
+ cls.lpszClassName, err);
85
+ return false;
86
+ }
87
+ }
88
+ return true;
89
+ }
90
+
91
+ bool registerWindowClasses() {
92
+ WNDCLASSEXW wc{};
93
+ wc.cbSize = sizeof(wc);
94
+ wc.lpfnWndProc = windowProc;
95
+ wc.hInstance = getCurrentModuleHandle();
96
+ wc.hCursor = LoadCursorW(nullptr, IDC_ARROW);
97
+ wc.hbrBackground = nullptr;
98
+ wc.lpszClassName = kWindowClass;
99
+ if (!ensureClassRegistered(wc)) return false;
100
+
101
+ // View container — a plain child window we own. WebView2 controllers parent
102
+ // to one of these, so SetWindowPos / EnableWindow on the container never
103
+ // round-trips through the Edge GPU process (which deadlocks under load).
104
+ WNDCLASSEXW vc{};
105
+ vc.cbSize = sizeof(vc);
106
+ vc.lpfnWndProc = DefWindowProcW;
107
+ vc.hInstance = getCurrentModuleHandle();
108
+ vc.hCursor = LoadCursorW(nullptr, IDC_ARROW);
109
+ vc.hbrBackground = nullptr;
110
+ vc.lpszClassName = kViewContainerClass;
111
+ if (!ensureClassRegistered(vc)) return false;
112
+
113
+ WNDCLASSEXW mc{};
114
+ mc.cbSize = sizeof(mc);
115
+ mc.lpfnWndProc = messageProc;
116
+ mc.hInstance = getCurrentModuleHandle();
117
+ mc.lpszClassName = L"BuniteWebView2MessageWindow";
118
+ if (!ensureClassRegistered(mc)) return false;
119
+
120
+ g_runtime.message_window = CreateWindowExW(0, mc.lpszClassName, L"",
121
+ 0, 0, 0, 0, 0,
122
+ HWND_MESSAGE, nullptr,
123
+ getCurrentModuleHandle(), nullptr);
124
+ return g_runtime.message_window != nullptr;
125
+ }
126
+
127
+ // ---- environment bootstrap -----------------------------------------------
128
+
129
+ static void ensureEnvironment(std::function<void()> on_ready) {
130
+ if (g_runtime.env_ready) { on_ready(); return; }
131
+ g_runtime.env_waiters.push_back(std::move(on_ready));
132
+ if (g_runtime.env_pending) return;
133
+ g_runtime.env_pending = true;
134
+
135
+ auto opts = Make<CoreWebView2EnvironmentOptions>();
136
+ if (!g_runtime.additional_browser_arguments.empty()) {
137
+ opts->put_AdditionalBrowserArguments(g_runtime.additional_browser_arguments.c_str());
138
+ }
139
+ if (!g_runtime.language.empty()) {
140
+ opts->put_Language(g_runtime.language.c_str());
141
+ }
142
+ configureSchemes(opts.Get());
143
+
144
+ std::wstring user_data = g_runtime.user_data_folder;
145
+ if (user_data.empty()) {
146
+ user_data = utf8ToWide(defaultUserDataFolder());
147
+ }
148
+
149
+ auto lifetime = g_runtime.lifetime;
150
+ HRESULT hr = CreateCoreWebView2EnvironmentWithOptions(
151
+ nullptr,
152
+ user_data.empty() ? nullptr : user_data.c_str(),
153
+ opts.Get(),
154
+ Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
155
+ [lifetime](HRESULT cr, ICoreWebView2Environment* env) -> HRESULT {
156
+ if (!lifetime || !lifetime->alive.load()) return S_OK;
157
+ g_runtime.env_pending = false;
158
+ auto waiters = std::move(g_runtime.env_waiters);
159
+ if (FAILED(cr) || !env) {
160
+ BUNITE_ERROR("webview2: env create failed hr=0x%08x — dropping %zu waiter(s)",
161
+ static_cast<unsigned>(cr), waiters.size());
162
+ return S_OK;
163
+ }
164
+ g_runtime.env = env;
165
+ g_runtime.env_ready = true;
166
+ for (auto& w : waiters) w();
167
+ return S_OK;
168
+ }).Get());
169
+ if (FAILED(hr)) {
170
+ BUNITE_ERROR("webview2: CreateCoreWebView2EnvironmentWithOptions failed hr=0x%08x",
171
+ static_cast<unsigned>(hr));
172
+ g_runtime.env_pending = false;
173
+ }
174
+ }
175
+
176
+ // ---- event emission -----------------------------------------------------
177
+
178
+ // The JS side calls bunite_free_cstring on both pointers, so each must point
179
+ // at heap-owned memory we allocated with malloc/strdup.
180
+ void emitWindowEvent(uint32_t window_id, const char* name, const std::string& payload) {
181
+ if (!g_runtime.window_event_handler) return;
182
+ const char* body = payload.empty() ? "{}" : payload.c_str();
183
+ g_runtime.window_event_handler(window_id, _strdup(name ? name : ""), _strdup(body));
184
+ }
185
+
186
+ void emitWebviewEvent(uint32_t view_id, const char* name, const std::string& payload) {
187
+ if (!g_runtime.webview_event_handler) return;
188
+ const char* body = payload.empty() ? "{}" : payload.c_str();
189
+ g_runtime.webview_event_handler(view_id, _strdup(name ? name : ""), _strdup(body));
190
+ }
191
+
192
+ // ---- lookups ------------------------------------------------------------
193
+
194
+ WindowHost* getWindow(uint32_t id) {
195
+ std::lock_guard<std::mutex> g(g_runtime.object_mutex);
196
+ auto it = g_runtime.windows_by_id.find(id);
197
+ return it == g_runtime.windows_by_id.end() ? nullptr : it->second;
198
+ }
199
+
200
+ ViewHost* getView(uint32_t id) {
201
+ std::lock_guard<std::mutex> g(g_runtime.object_mutex);
202
+ auto it = g_runtime.views_by_id.find(id);
203
+ return it == g_runtime.views_by_id.end() ? nullptr : it->second;
204
+ }
205
+
206
+ // ---- window proc --------------------------------------------------------
207
+
208
+ static WindowHost* findWindowByHwnd(HWND hwnd) {
209
+ std::lock_guard<std::mutex> g(g_runtime.object_mutex);
210
+ for (auto& [id, w] : g_runtime.windows_by_id) {
211
+ if (w->hwnd == hwnd) return w;
212
+ }
213
+ return nullptr;
214
+ }
215
+
216
+ static void applyViewLayout(ViewHost* v) {
217
+ if (!v || !v->container_hwnd) return;
218
+ RECT target = v->bounds;
219
+ if (v->auto_resize && v->window && v->window->hwnd) {
220
+ GetClientRect(v->window->hwnd, &target);
221
+ }
222
+ SetWindowPos(v->container_hwnd, nullptr,
223
+ target.left, target.top,
224
+ target.right - target.left, target.bottom - target.top,
225
+ SWP_NOZORDER | SWP_NOACTIVATE);
226
+ if (v->controller) {
227
+ RECT inner{ 0, 0, target.right - target.left, target.bottom - target.top };
228
+ v->controller->put_Bounds(inner);
229
+ }
230
+ }
231
+
232
+ static void layoutViewsForWindow(WindowHost* w) {
233
+ if (!w) return;
234
+ for (ViewHost* v : w->views) applyViewLayout(v);
235
+ }
236
+
237
+ LRESULT CALLBACK windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
238
+ switch (msg) {
239
+ case WM_SIZE: {
240
+ WindowHost* w = findWindowByHwnd(hwnd);
241
+ if (w) layoutViewsForWindow(w);
242
+ return 0;
243
+ }
244
+ case WM_CLOSE: {
245
+ WindowHost* w = findWindowByHwnd(hwnd);
246
+ if (w && !w->close_pending.load()) {
247
+ w->close_pending.store(true);
248
+ emitWindowEvent(w->id, "close-requested");
249
+ return 0; // wait for bunite_window_close
250
+ }
251
+ DestroyWindow(hwnd);
252
+ return 0;
253
+ }
254
+ case WM_SETFOCUS:
255
+ case WM_ACTIVATE: {
256
+ WindowHost* w = findWindowByHwnd(hwnd);
257
+ if (w) emitWindowEvent(w->id, "focus");
258
+ break;
259
+ }
260
+ case WM_KILLFOCUS: {
261
+ WindowHost* w = findWindowByHwnd(hwnd);
262
+ if (w) emitWindowEvent(w->id, "blur");
263
+ break;
264
+ }
265
+ case WM_DESTROY:
266
+ return 0;
267
+ }
268
+ return DefWindowProcW(hwnd, msg, wp, lp);
269
+ }
270
+
271
+ LRESULT CALLBACK messageProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
272
+ if (msg == kRunQueuedTaskMessage) {
273
+ // Reset the wake flag here, but DO NOT drain queued tasks from inside the
274
+ // message dispatch — a task that invokes a WebView2 async COM method (e.g.
275
+ // CreateCoreWebView2Controller) would otherwise enter a nested STA pump
276
+ // and deadlock the Edge helper. Drain happens in pumpOnce() instead.
277
+ g_wake_pending.store(false);
278
+ return 0;
279
+ }
280
+ return DefWindowProcW(hwnd, msg, wp, lp);
281
+ }
282
+
283
+ // ---- init / shutdown ----------------------------------------------------
284
+
285
+ bool initRuntime(const char* engine_dir, bool /*hide_console*/,
286
+ bool popup_blocking, const char* engine_config_json) {
287
+ buniteApplyEnvLogLevel();
288
+ BUNITE_INFO("webview2: bunite_init enter engine_dir=%s",
289
+ (engine_dir && *engine_dir) ? engine_dir : "(null)");
290
+ if (g_runtime.initialized.load()) return true;
291
+
292
+ HRESULT co = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
293
+ if (SUCCEEDED(co)) g_co_initialized = true;
294
+ else if (co != RPC_E_CHANGED_MODE) {
295
+ BUNITE_ERROR("webview2: CoInitializeEx failed hr=0x%08x", static_cast<unsigned>(co));
296
+ return false;
297
+ }
298
+
299
+ g_runtime.lifetime = std::make_shared<HostLifetime>();
300
+ g_runtime.popup_blocking = popup_blocking;
301
+ if (engine_dir && *engine_dir) g_runtime.user_data_folder = utf8ToWide(engine_dir);
302
+ parseEngineConfig(engine_config_json ? engine_config_json : "",
303
+ g_runtime.user_data_folder,
304
+ g_runtime.additional_browser_arguments,
305
+ g_runtime.language);
306
+
307
+ if (!registerWindowClasses()) {
308
+ BUNITE_ERROR("webview2: window class registration failed");
309
+ return false;
310
+ }
311
+
312
+ g_runtime.initialized.store(true);
313
+ BUNITE_INFO("webview2: runtime initialized");
314
+ return true;
315
+ }
316
+
317
+ void shutdownRuntime() {
318
+ if (!g_runtime.initialized.load()) return;
319
+ if (g_runtime.shutting_down.load()) return;
320
+ g_runtime.shutting_down.store(true);
321
+
322
+ cancelAllRouteRequests();
323
+
324
+ std::vector<ViewHost*> views;
325
+ std::vector<WindowHost*> windows;
326
+ {
327
+ std::lock_guard<std::mutex> g(g_runtime.object_mutex);
328
+ for (auto& [_, v] : g_runtime.views_by_id) views.push_back(v);
329
+ for (auto& [_, w] : g_runtime.windows_by_id) windows.push_back(w);
330
+ }
331
+
332
+ // Staged teardown — controller->Close() has no completion callback.
333
+ // Each stage pumps so async Edge work can settle before the parent goes away.
334
+ auto drain = [](int ms) {
335
+ auto t0 = std::chrono::steady_clock::now();
336
+ while (std::chrono::duration_cast<std::chrono::milliseconds>(
337
+ std::chrono::steady_clock::now() - t0).count() < ms) {
338
+ pumpOnce();
339
+ Sleep(1);
340
+ }
341
+ };
342
+
343
+ for (auto* v : views) {
344
+ if (v->controller) {
345
+ v->closing.store(true);
346
+ v->controller->Close();
347
+ }
348
+ }
349
+ drain(250);
350
+
351
+ for (auto* v : views) {
352
+ if (v->container_hwnd) {
353
+ DestroyWindow(v->container_hwnd);
354
+ v->container_hwnd = nullptr;
355
+ }
356
+ }
357
+ drain(100);
358
+
359
+ for (auto* w : windows) {
360
+ if (w->hwnd) DestroyWindow(w->hwnd);
361
+ }
362
+ drain(250);
363
+
364
+ // Flip alive BEFORE deleting view/window structs so any straggling COM
365
+ // callback that fires during the deletes short-circuits.
366
+ if (g_runtime.lifetime) g_runtime.lifetime->alive.store(false);
367
+
368
+ {
369
+ std::lock_guard<std::mutex> g(g_runtime.object_mutex);
370
+ for (auto* v : views) delete v;
371
+ for (auto* w : windows) delete w;
372
+ g_runtime.views_by_id.clear();
373
+ g_runtime.windows_by_id.clear();
374
+ }
375
+
376
+ // Reset mutable runtime state so a re-init starts from a clean slate.
377
+ {
378
+ std::lock_guard<std::mutex> g(g_runtime.task_mutex);
379
+ g_runtime.queued_tasks.clear();
380
+ }
381
+ {
382
+ std::lock_guard<std::mutex> g(g_runtime.route_mutex);
383
+ g_runtime.pending_routes.clear();
384
+ g_runtime.registered_routes.clear();
385
+ g_runtime.next_route_request_id = 1;
386
+ }
387
+ {
388
+ std::lock_guard<std::mutex> g(g_runtime.permission_mutex);
389
+ g_runtime.pending_permissions.clear();
390
+ g_runtime.next_permission_request_id = 1;
391
+ }
392
+ g_runtime.env_waiters.clear();
393
+ g_runtime.env_pending = false;
394
+ g_runtime.env_ready = false;
395
+ g_runtime.env.Reset();
396
+
397
+ if (g_runtime.message_window) {
398
+ DestroyWindow(g_runtime.message_window);
399
+ g_runtime.message_window = nullptr;
400
+ }
401
+ if (g_co_initialized) { CoUninitialize(); g_co_initialized = false; }
402
+ g_runtime.shutting_down.store(false);
403
+ g_runtime.initialized.store(false);
404
+ }
405
+
406
+ // ---- window CRUD --------------------------------------------------------
407
+
408
+ static DWORD styleForTitleBar(const std::wstring& tbs) {
409
+ if (tbs == L"hidden" || tbs == L"hiddenInset") {
410
+ return WS_POPUP | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX;
411
+ }
412
+ return WS_OVERLAPPEDWINDOW;
413
+ }
414
+
415
+ bool createWindow(uint32_t window_id, double x, double y, double w, double h,
416
+ const char* title, const char* title_bar_style,
417
+ bool transparent, bool hidden, bool minimized, bool maximized) {
418
+ BUNITE_INFO("webview2: createWindow id=%u xy=(%g,%g) size=(%g,%g) hidden=%d trans=%d",
419
+ window_id, x, y, w, h, hidden, transparent);
420
+ WindowHost* host = new WindowHost();
421
+ host->id = window_id;
422
+ host->title = title ? utf8ToWide(title) : L"";
423
+ host->title_bar_style = title_bar_style ? utf8ToWide(title_bar_style) : L"";
424
+ host->transparent = transparent;
425
+ host->hidden = hidden;
426
+ host->minimized = minimized;
427
+ host->maximized = maximized;
428
+
429
+ DWORD style = styleForTitleBar(host->title_bar_style);
430
+ DWORD ex_style = 0;
431
+ if (transparent) ex_style |= WS_EX_LAYERED | WS_EX_NOREDIRECTIONBITMAP;
432
+
433
+ int ix = (x == 0 && y == 0) ? CW_USEDEFAULT : static_cast<int>(x);
434
+ int iy = (x == 0 && y == 0) ? CW_USEDEFAULT : static_cast<int>(y);
435
+ int iw = static_cast<int>(w > 0 ? w : 800);
436
+ int ih = static_cast<int>(h > 0 ? h : 600);
437
+
438
+ HWND hwnd = CreateWindowExW(ex_style, kWindowClass, host->title.c_str(),
439
+ style, ix, iy, iw, ih, nullptr, nullptr,
440
+ getCurrentModuleHandle(), nullptr);
441
+ if (!hwnd) {
442
+ BUNITE_ERROR("webview2: CreateWindowExW failed err=%lu (class=%ls)",
443
+ GetLastError(), kWindowClass);
444
+ delete host;
445
+ return false;
446
+ }
447
+ BUNITE_INFO("webview2: createWindow hwnd=%p id=%u", hwnd, window_id);
448
+ host->hwnd = hwnd;
449
+ if (transparent) {
450
+ // Fully transparent layered window — pixels are sourced from the WebView2
451
+ // controller (Stage 3 sets DefaultBackgroundColor to {0,0,0,0}).
452
+ SetLayeredWindowAttributes(hwnd, 0, 255, LWA_ALPHA);
453
+ }
454
+
455
+ {
456
+ std::lock_guard<std::mutex> g(g_runtime.object_mutex);
457
+ g_runtime.windows_by_id[window_id] = host;
458
+ }
459
+
460
+ if (maximized) ShowWindow(hwnd, SW_MAXIMIZE);
461
+ else if (minimized) ShowWindow(hwnd, SW_MINIMIZE);
462
+ else if (!hidden) ShowWindow(hwnd, SW_SHOW);
463
+ return true;
464
+ }
465
+
466
+ void destroyWindow(uint32_t window_id) {
467
+ WindowHost* w = nullptr;
468
+ bool last_window = false;
469
+ {
470
+ std::lock_guard<std::mutex> g(g_runtime.object_mutex);
471
+ auto it = g_runtime.windows_by_id.find(window_id);
472
+ if (it == g_runtime.windows_by_id.end()) return;
473
+ w = it->second;
474
+ }
475
+ if (!w) return;
476
+ std::vector<ViewHost*> views = w->views;
477
+ for (auto* v : views) destroyView(v->id);
478
+ if (w->hwnd) {
479
+ DestroyWindow(w->hwnd);
480
+ w->hwnd = nullptr;
481
+ }
482
+ {
483
+ std::lock_guard<std::mutex> g(g_runtime.object_mutex);
484
+ g_runtime.windows_by_id.erase(window_id);
485
+ last_window = g_runtime.windows_by_id.empty();
486
+ }
487
+ emitWindowEvent(window_id, "close");
488
+ if (last_window) emitWindowEvent(0, "all-windows-closed");
489
+ delete w;
490
+ }
491
+
492
+ // ---- view CRUD ----------------------------------------------------------
493
+
494
+ static void attachControllerCallbacks(ViewHost* view);
495
+
496
+ // Synchronous teardown for a half-initialized view — called when wireView fails
497
+ // before the controller is alive. Cleans HWND + map entries + emits a signal so
498
+ // the JS side's `whenReady()` rejects instead of hanging.
499
+ static void abortView(uint32_t view_id) {
500
+ ViewHost* v = nullptr;
501
+ {
502
+ std::lock_guard<std::mutex> g(g_runtime.object_mutex);
503
+ auto it = g_runtime.views_by_id.find(view_id);
504
+ if (it == g_runtime.views_by_id.end()) return;
505
+ v = it->second;
506
+ g_runtime.views_by_id.erase(it);
507
+ }
508
+ if (!v) return;
509
+ if (v->window) {
510
+ auto& vs = v->window->views;
511
+ vs.erase(std::remove(vs.begin(), vs.end(), v), vs.end());
512
+ }
513
+ if (v->container_hwnd) {
514
+ DestroyWindow(v->container_hwnd);
515
+ v->container_hwnd = nullptr;
516
+ }
517
+ emitWebviewEvent(view_id, "view-init-failed");
518
+ delete v;
519
+ }
520
+
521
+ static void wireView(ViewHost* view, std::function<void()> on_attached) {
522
+ if (!g_runtime.env) {
523
+ BUNITE_ERROR("webview2: wireView with no env");
524
+ abortView(view->id);
525
+ return;
526
+ }
527
+ auto lifetime = g_runtime.lifetime;
528
+ uint32_t view_id = view->id;
529
+ HWND host_hwnd = view->window->hwnd;
530
+
531
+ // Create per-view container HWND (parent = host window). Start hidden so a
532
+ // controller that fails to materialise doesn't flash an empty rectangle, and
533
+ // so renderer-driven `setHidden(true)` can land before the surface appears.
534
+ RECT initial = view->bounds;
535
+ if (view->auto_resize) GetClientRect(host_hwnd, &initial);
536
+ view->container_hwnd = CreateWindowExW(
537
+ 0, kViewContainerClass, L"",
538
+ WS_CHILD | WS_CLIPCHILDREN,
539
+ initial.left, initial.top,
540
+ initial.right - initial.left, initial.bottom - initial.top,
541
+ host_hwnd, nullptr, getCurrentModuleHandle(), nullptr);
542
+ if (!view->container_hwnd) {
543
+ BUNITE_ERROR("webview2: container HWND creation failed view=%u err=%lu",
544
+ view_id, GetLastError());
545
+ abortView(view_id);
546
+ return;
547
+ }
548
+ HRESULT hr = g_runtime.env->CreateCoreWebView2Controller(
549
+ view->container_hwnd,
550
+ Callback<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>(
551
+ [lifetime, view_id, on_attached](HRESULT cr, ICoreWebView2Controller* ctl) -> HRESULT {
552
+ BUNITE_INFO("webview2: controller-create completion view=%u hr=0x%08x",
553
+ view_id, static_cast<unsigned>(cr));
554
+ if (!lifetime || !lifetime->alive.load()) return S_OK;
555
+ ViewHost* v = getView(view_id);
556
+ if (!v) return S_OK;
557
+ if (FAILED(cr) || !ctl) {
558
+ BUNITE_ERROR("webview2: controller create failed hr=0x%08x",
559
+ static_cast<unsigned>(cr));
560
+ abortView(view_id);
561
+ return S_OK;
562
+ }
563
+ v->controller = ctl;
564
+ ctl->QueryInterface(IID_PPV_ARGS(&v->controller2));
565
+ ctl->get_CoreWebView2(&v->webview);
566
+ if (v->webview) v->webview->QueryInterface(IID_PPV_ARGS(&v->webview2));
567
+
568
+ if (v->window->transparent && v->controller2) {
569
+ COREWEBVIEW2_COLOR clear{0, 0, 0, 0};
570
+ v->controller2->put_DefaultBackgroundColor(clear);
571
+ }
572
+
573
+ // Controller bounds are container-relative.
574
+ RECT cont{};
575
+ GetClientRect(v->container_hwnd, &cont);
576
+ ctl->put_Bounds(cont);
577
+ ctl->put_IsVisible(v->pending_visible);
578
+ if (v->pending_visible) ShowWindow(v->container_hwnd, SW_SHOWNA);
579
+
580
+ attachControllerCallbacks(v);
581
+ attachAppResFilter(v);
582
+ v->ready.store(true);
583
+
584
+ // Inject preload script. Wrapper enforces:
585
+ // - main frame only (matching CEF's OnContextCreated main-frame gate)
586
+ // - origin allowlist when `preload_origins` is non-empty
587
+ // Empty allowlist = inject on every main frame (CEF parity default).
588
+ if (!v->preload_script.empty() && v->webview) {
589
+ std::string allowlist = "[";
590
+ for (size_t i = 0; i < v->preload_origins.size(); ++i) {
591
+ if (i) allowlist += ",";
592
+ allowlist += "\"" + escapeJsonString(v->preload_origins[i]) + "\"";
593
+ }
594
+ allowlist += "]";
595
+ std::string body =
596
+ "(function(){if(window.self!==window.top)return;"
597
+ "var __a=" + allowlist +
598
+ ",__o=location.origin;"
599
+ "if(__a.length){var __m=function(p,v){var i=0,j=0,s=-1,k=0,L=function(c){return c.charCodeAt(0)|32;};"
600
+ "while(j<v.length){if(i<p.length&&(p[i]===\"?\"||L(p[i])===L(v[j]))){i++;j++;}"
601
+ "else if(i<p.length&&p[i]===\"*\"){s=i++;k=j;}"
602
+ "else if(s>=0){i=s+1;j=++k;}else{return false;}}"
603
+ "while(i<p.length&&p[i]===\"*\")i++;return i===p.length;};"
604
+ "var __ok=false;for(var i=0;i<__a.length;i++){if(__m(__a[i],__o)){__ok=true;break;}}"
605
+ "if(!__ok)return;}"
606
+ + v->preload_script +
607
+ "})();";
608
+ std::wstring wpreload = utf8ToWide(body);
609
+ auto lt = lifetime;
610
+ v->webview->AddScriptToExecuteOnDocumentCreated(
611
+ wpreload.c_str(),
612
+ Callback<ICoreWebView2AddScriptToExecuteOnDocumentCreatedCompletedHandler>(
613
+ [lt, view_id](HRESULT, LPCWSTR id) -> HRESULT {
614
+ if (!lt || !lt->alive.load()) return S_OK;
615
+ ViewHost* vv = getView(view_id);
616
+ if (vv && id) vv->add_script_id = id;
617
+ return S_OK;
618
+ }).Get());
619
+ }
620
+
621
+ // Initial navigation.
622
+ if (!v->url.empty()) {
623
+ v->webview->Navigate(utf8ToWide(v->url).c_str());
624
+ } else if (!v->html.empty()) {
625
+ v->webview->NavigateToString(utf8ToWide(v->html).c_str());
626
+ }
627
+
628
+ emitWebviewEvent(v->id, "view-ready");
629
+ if (on_attached) on_attached();
630
+ return S_OK;
631
+ }).Get());
632
+ if (FAILED(hr)) {
633
+ BUNITE_ERROR("webview2: CreateCoreWebView2Controller failed hr=0x%08x",
634
+ static_cast<unsigned>(hr));
635
+ abortView(view_id);
636
+ }
637
+ }
638
+
639
+ bool createView(uint32_t view_id, uint32_t window_id,
640
+ const char* url, const char* html,
641
+ const char* preload, const char* appres_root,
642
+ const char* navigation_rules_json,
643
+ double x, double y, double w, double h,
644
+ bool auto_resize, bool sandbox,
645
+ const char* preload_origins_json) {
646
+ BUNITE_INFO("webview2: createView view=%u window=%u url=%s",
647
+ view_id, window_id, url && *url ? url : "(none)");
648
+ WindowHost* window = getWindow(window_id);
649
+ if (!window) {
650
+ BUNITE_ERROR("webview2: createView for unknown window_id=%u", window_id);
651
+ return false;
652
+ }
653
+
654
+ ViewHost* v = new ViewHost();
655
+ v->id = view_id;
656
+ v->window = window;
657
+ v->url = url ? url : "";
658
+ v->html = html ? html : "";
659
+ v->preload_script = preload ? preload : "";
660
+ v->appres_root = appres_root ? appres_root : "";
661
+ v->sandbox = sandbox;
662
+ v->auto_resize = auto_resize;
663
+ v->bounds = { static_cast<LONG>(x), static_cast<LONG>(y),
664
+ static_cast<LONG>(x + w), static_cast<LONG>(y + h) };
665
+ if (navigation_rules_json && *navigation_rules_json) {
666
+ v->navigation_rules = parseNavigationRulesJson(navigation_rules_json);
667
+ }
668
+ if (preload_origins_json && *preload_origins_json) {
669
+ v->preload_origins = parsePreloadOriginsJson(preload_origins_json);
670
+ }
671
+
672
+ {
673
+ std::lock_guard<std::mutex> g(g_runtime.object_mutex);
674
+ g_runtime.views_by_id[view_id] = v;
675
+ }
676
+ window->views.push_back(v);
677
+
678
+ // Defer the WebView2 controller bootstrap. Calling CreateCoreWebView2Controller
679
+ // synchronously from inside a Win32 message dispatch (e.g. a renderer-driven
680
+ // SurfaceCap.init RPC) enters a nested STA pump that can deadlock the Edge
681
+ // helper's GPU IPC. Hopping through the queued-task loop guarantees the call
682
+ // runs at top of the next pump iteration.
683
+ postUiTask([view_id]() {
684
+ ensureEnvironment([view_id]() {
685
+ ViewHost* v = getView(view_id);
686
+ if (!v) return;
687
+ wireView(v, nullptr);
688
+ });
689
+ });
690
+ return true;
691
+ }
692
+
693
+ void destroyView(uint32_t id) {
694
+ ViewHost* v = nullptr;
695
+ {
696
+ std::lock_guard<std::mutex> g(g_runtime.object_mutex);
697
+ auto it = g_runtime.views_by_id.find(id);
698
+ if (it == g_runtime.views_by_id.end()) return;
699
+ v = it->second;
700
+ g_runtime.views_by_id.erase(it);
701
+ }
702
+ if (!v) return;
703
+
704
+ if (v->window) {
705
+ auto& vs = v->window->views;
706
+ vs.erase(std::remove(vs.begin(), vs.end(), v), vs.end());
707
+ }
708
+
709
+ // Defer Close() → container destroy → delete across three pump ticks. Edge
710
+ // gets at least one tick after Close() to settle before its parent HWND
711
+ // vanishes; see shutdownRuntime's staged drains for the same reason.
712
+ postUiTask([v]() {
713
+ if (v->controller) {
714
+ v->closing.store(true);
715
+ v->controller->Close();
716
+ }
717
+ postUiTask([v]() {
718
+ if (v->container_hwnd) {
719
+ DestroyWindow(v->container_hwnd);
720
+ v->container_hwnd = nullptr;
721
+ }
722
+ postUiTask([v]() { delete v; });
723
+ });
724
+ });
725
+ }
726
+
727
+ // ---- per-view event wiring ----------------------------------------------
728
+
729
+ static bool originAllowedForPreload(const ViewHost* v, const std::string& origin) {
730
+ if (v->preload_origins.empty()) return true; // engine-agnostic default
731
+ for (auto& o : v->preload_origins) {
732
+ if (globMatchCaseInsensitive(o, origin)) return true;
733
+ }
734
+ return false;
735
+ }
736
+
737
+ static void enumerateChildHwnds(HWND root, std::vector<HWND>& out) {
738
+ EnumChildWindows(root, [](HWND child, LPARAM lp) -> BOOL {
739
+ reinterpret_cast<std::vector<HWND>*>(lp)->push_back(child);
740
+ return TRUE;
741
+ }, reinterpret_cast<LPARAM>(&out));
742
+ }
743
+
744
+ static void applyInputPassthrough(ViewHost* v, bool passthrough) {
745
+ if (!v->container_hwnd) return;
746
+ // Disable our container (Bunite-owned). This also gates input to every
747
+ // descendant — including the Edge-owned controller HWNDs — without ever
748
+ // touching those cross-process windows, which was the original deadlock path.
749
+ EnableWindow(v->container_hwnd, passthrough ? FALSE : TRUE);
750
+ }
751
+
752
+ static void attachControllerCallbacks(ViewHost* view) {
753
+ if (!view->webview) return;
754
+ auto lifetime = g_runtime.lifetime;
755
+ uint32_t view_id = view->id;
756
+ EventRegistrationToken tok;
757
+
758
+ // NavigationStarting — enforce navigation rules, emit "will-navigate" event.
759
+ view->webview->add_NavigationStarting(
760
+ Callback<ICoreWebView2NavigationStartingEventHandler>(
761
+ [lifetime, view_id](ICoreWebView2*, ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT {
762
+ if (!lifetime || !lifetime->alive.load()) return S_OK;
763
+ ViewHost* v = getView(view_id);
764
+ if (!v) return S_OK;
765
+ LPWSTR uri_raw = nullptr;
766
+ args->get_Uri(&uri_raw);
767
+ std::string url = wideToUtf8(uri_raw);
768
+ if (uri_raw) CoTaskMemFree(uri_raw);
769
+ if (!shouldAllowNavigation(v, url)) {
770
+ args->put_Cancel(TRUE);
771
+ return S_OK;
772
+ }
773
+ emitWebviewEvent(v->id, "will-navigate", url);
774
+ return S_OK;
775
+ }).Get(),
776
+ &tok);
777
+
778
+ // NavigationCompleted — surfaced as did-navigate + dom-ready (CEF parity).
779
+ view->webview->add_NavigationCompleted(
780
+ Callback<ICoreWebView2NavigationCompletedEventHandler>(
781
+ [lifetime, view_id](ICoreWebView2* wv, ICoreWebView2NavigationCompletedEventArgs* args) -> HRESULT {
782
+ if (!lifetime || !lifetime->alive.load()) return S_OK;
783
+ BOOL ok = FALSE;
784
+ args->get_IsSuccess(&ok);
785
+ if (!ok) return S_OK;
786
+ LPWSTR src_raw = nullptr;
787
+ if (wv) wv->get_Source(&src_raw);
788
+ std::string url = wideToUtf8(src_raw);
789
+ if (src_raw) CoTaskMemFree(src_raw);
790
+ emitWebviewEvent(view_id, "did-navigate", url);
791
+ emitWebviewEvent(view_id, "dom-ready", url);
792
+ return S_OK;
793
+ }).Get(),
794
+ &tok);
795
+
796
+ // PermissionRequested — map to bunite kind and stash deferral.
797
+ view->webview->add_PermissionRequested(
798
+ Callback<ICoreWebView2PermissionRequestedEventHandler>(
799
+ [lifetime, view_id](ICoreWebView2*, ICoreWebView2PermissionRequestedEventArgs* args) -> HRESULT {
800
+ if (!lifetime || !lifetime->alive.load()) return S_OK;
801
+ ViewHost* v = getView(view_id);
802
+ if (!v) return S_OK;
803
+ COREWEBVIEW2_PERMISSION_KIND kind;
804
+ args->get_PermissionKind(&kind);
805
+ uint32_t bit = permissionKindToBuniteBit(kind);
806
+
807
+ ComPtr<ICoreWebView2Deferral> deferral;
808
+ args->GetDeferral(&deferral);
809
+
810
+ uint32_t req_id;
811
+ {
812
+ std::lock_guard<std::mutex> g(g_runtime.permission_mutex);
813
+ req_id = g_runtime.next_permission_request_id++;
814
+ PendingPermissionRequest p;
815
+ p.view_id = v->id;
816
+ p.bunite_kind = bit;
817
+ p.args = args;
818
+ p.deferral = deferral;
819
+ g_runtime.pending_permissions[req_id] = std::move(p);
820
+ }
821
+ std::string payload = "{\"requestId\":" + std::to_string(req_id) +
822
+ ",\"kind\":" + std::to_string(bit) + "}";
823
+ emitWebviewEvent(v->id, "permission-requested", payload);
824
+ return S_OK;
825
+ }).Get(),
826
+ &tok);
827
+
828
+ // NewWindowRequested — block by default (matches plan), bubble event so the
829
+ // host can decide to open externally.
830
+ view->webview->add_NewWindowRequested(
831
+ Callback<ICoreWebView2NewWindowRequestedEventHandler>(
832
+ [lifetime, view_id](ICoreWebView2*, ICoreWebView2NewWindowRequestedEventArgs* args) -> HRESULT {
833
+ if (!lifetime || !lifetime->alive.load()) return S_OK;
834
+ args->put_Handled(TRUE);
835
+ LPWSTR uri_raw = nullptr;
836
+ args->get_Uri(&uri_raw);
837
+ std::string url = wideToUtf8(uri_raw);
838
+ if (uri_raw) CoTaskMemFree(uri_raw);
839
+ std::string payload = "{\"url\":\"" + escapeJsonString(url) + "\"}";
840
+ emitWebviewEvent(view_id, "new-window-open", payload);
841
+ return S_OK;
842
+ }).Get(),
843
+ &tok);
844
+
845
+ // WindowCloseRequested — surfaced as window-level close-requested.
846
+ view->webview->add_WindowCloseRequested(
847
+ Callback<ICoreWebView2WindowCloseRequestedEventHandler>(
848
+ [lifetime, view_id](ICoreWebView2*, IUnknown*) -> HRESULT {
849
+ if (!lifetime || !lifetime->alive.load()) return S_OK;
850
+ ViewHost* v = getView(view_id);
851
+ if (v && v->window) emitWindowEvent(v->window->id, "close-requested");
852
+ return S_OK;
853
+ }).Get(),
854
+ &tok);
855
+
856
+ // DownloadStarting (ICoreWebView2_4) — suppress by default.
857
+ ComPtr<ICoreWebView2_4> wv4;
858
+ view->webview->QueryInterface(IID_PPV_ARGS(&wv4));
859
+ if (wv4) {
860
+ wv4->add_DownloadStarting(
861
+ Callback<ICoreWebView2DownloadStartingEventHandler>(
862
+ [lifetime, view_id](ICoreWebView2*, ICoreWebView2DownloadStartingEventArgs* args) -> HRESULT {
863
+ if (!lifetime || !lifetime->alive.load()) return S_OK;
864
+ ComPtr<ICoreWebView2DownloadOperation> op;
865
+ args->get_DownloadOperation(&op);
866
+ LPWSTR uri_raw = nullptr;
867
+ if (op) op->get_Uri(&uri_raw);
868
+ std::string url = wideToUtf8(uri_raw);
869
+ if (uri_raw) CoTaskMemFree(uri_raw);
870
+ args->put_Cancel(TRUE);
871
+ std::string payload = "{\"url\":\"" + escapeJsonString(url) + "\"}";
872
+ emitWebviewEvent(view_id, "download-blocked", payload);
873
+ return S_OK;
874
+ }).Get(),
875
+ &tok);
876
+ }
877
+
878
+ if (view->pending_passthrough) applyInputPassthrough(view, true);
879
+ }
880
+
881
+ // Re-exported helper for ffi.cpp.
882
+ void setViewInputPassthrough(ViewHost* v, bool passthrough) {
883
+ v->pending_passthrough = passthrough;
884
+ if (v->ready.load()) applyInputPassthrough(v, passthrough);
885
+ }
886
+
887
+ } // namespace bunite_webview2