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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bunite-core",
3
3
  "description": "Uniting UI and Bun",
4
- "version": "0.11.0",
4
+ "version": "0.11.2",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "setup:cef": "bun ../tools/bunite-dev/scripts/setup-cef.ts",
@@ -24,7 +24,7 @@
24
24
  "msgpackr": "^1.11.9"
25
25
  },
26
26
  "optionalDependencies": {
27
- "bunite-native-win-x64": "0.0.4",
27
+ "bunite-native-win-x64": "0.0.6",
28
28
  "bunite-native-mac-arm64": "0.0.1",
29
29
  "bunite-native-linux-x64": "0.0.1"
30
30
  }
@@ -88,9 +88,15 @@ export class AppRuntime {
88
88
  process.env.BUNITE_USER_DATA_DIR = join(appDataDir, name);
89
89
  }
90
90
 
91
+ const envEngine = process.env.BUNITE_ENGINE;
92
+ const engineFromEnv = envEngine === "cef" || envEngine === "webview2"
93
+ ? envEngine
94
+ : undefined;
95
+
91
96
  await initNativeRuntime({
92
97
  hideConsole: options.hideConsole,
93
98
  popupBlocking: options.popupBlocking,
99
+ engine: options.engine ?? engineFromEnv,
94
100
  engineFlags: options.engineFlags
95
101
  });
96
102
 
@@ -126,11 +132,21 @@ export class AppRuntime {
126
132
  }
127
133
 
128
134
  run() {
135
+ if (!getNativeRuntimeState()) {
136
+ throw new Error("AppRuntime.run() called before await app.ready completed");
137
+ }
129
138
  const lib = getNativeLibrary();
139
+ // CEF: bunite_run_loop blocks here. WebView2 / mac / linux: returns immediately
140
+ // (cooperative pump kicks in below).
130
141
  lib?.symbols.bunite_run_loop();
131
142
 
132
- if (process.platform === "darwin" || process.platform === "linux") {
133
- // mac/linux: cooperative pump on Bun's main thread (NSApp/GTK first-thread constraint).
143
+ const engine = getNativeEngineName();
144
+ const cooperative =
145
+ process.platform === "darwin" ||
146
+ process.platform === "linux" ||
147
+ (process.platform === "win32" && engine !== "cef");
148
+
149
+ if (cooperative) {
134
150
  this.pumpActive = true;
135
151
  const pump = () => {
136
152
  if (!this.pumpActive) return;
@@ -139,8 +155,6 @@ export class AppRuntime {
139
155
  };
140
156
  pump();
141
157
  }
142
- // Windows: native UI thread is separate. Bun's loop stays alive via the RPC
143
- // listening socket started by ensureRpcServer() in the constructor.
144
158
  }
145
159
 
146
160
  quit(code = 0) {
@@ -175,6 +175,12 @@ export class BrowserWindow {
175
175
  Boolean(this.frame.minimized),
176
176
  Boolean(this.frame.maximized)
177
177
  ) ?? false;
178
+ if (!this.nativeAttached) {
179
+ console.error(
180
+ `[bunite] bunite_window_create returned false for window ${this.id} — ` +
181
+ `window will be unusable. Check native log (BUNITE_LOG_LEVEL=info).`
182
+ );
183
+ }
178
184
 
179
185
  BrowserWindowMap[this.id] = this;
180
186
  buniteEventEmitter.on(`focus-${this.id}`, () => { lastFocusedWindowId = this.id; });
package/src/host/index.ts CHANGED
@@ -8,7 +8,7 @@ import { acquireSingleInstanceLock, type SingleInstanceLock } from "./core/singl
8
8
  import { log, type LogLevel } from "./log";
9
9
 
10
10
  export { serveWeb } from "./serveWeb";
11
- export type { WebRpcMount } from "./serveWeb";
11
+ export type { WebRpcMount, WsData, ServeWebOptions } from "./serveWeb";
12
12
 
13
13
  export {
14
14
  acquireSingleInstanceLock,
@@ -2,16 +2,20 @@ import { CString, dlopen, FFIType, JSCallback, ptr, type Pointer } from "bun:ffi
2
2
  import { existsSync } from "node:fs";
3
3
  import { delimiter, join } from "node:path";
4
4
  import { buniteEventEmitter } from "./events/eventEmitter";
5
- import { resolveNativeArtifacts, type ResolvedNativeArtifacts } from "./paths";
5
+ import { resolveNativeArtifacts, type ResolvedNativeArtifacts, type WindowsEngine } from "./paths";
6
+ import { resolvePackageRoot } from "./paths";
6
7
  import { log } from "./log";
7
8
 
8
9
  export type NativeBootstrapOptions = {
9
10
  hideConsole?: boolean;
10
11
  popupBlocking?: boolean;
12
+ /** Windows-only engine selection. Default "webview2"; "cef" requires bunite-cef-win-x64. */
13
+ engine?: WindowsEngine;
11
14
  /**
12
15
  * Engine-specific opaque config. Each adapter parses its own keys.
13
16
  * - CEF (Windows): Chromium command-line flags as `Record<flag, value | true>`.
14
- * - WKWebView, WebKitGTK: defined per adapter; refer to the adapter's bootstrap docs.
17
+ * - WebView2 (Windows): `{ userDataFolder?, additionalBrowserArguments?, language? }`.
18
+ * - WKWebView, WebKitGTK: defined per adapter.
15
19
  */
16
20
  engineFlags?: Record<string, string | boolean>;
17
21
  };
@@ -356,7 +360,8 @@ export function toCString(value: string): CStringPointer {
356
360
  }
357
361
 
358
362
  function applyEnvironment(artifacts: ResolvedNativeArtifacts) {
359
- // CEF needs engine dir on PATH (libcef.dll) and ICU_DATA pointing at resources. Null for mac/linux.
363
+ // CEF needs engine dir on PATH (libcef.dll) and ICU_DATA pointing at resources.
364
+ // WebView2 needs the directory containing WebView2Loader.dll on PATH.
360
365
  const engineBinaryDir = artifacts.cefDir && existsSync(join(artifacts.cefDir, "Release", "libcef.dll"))
361
366
  ? join(artifacts.cefDir, "Release")
362
367
  : artifacts.cefDir;
@@ -367,10 +372,18 @@ function applyEnvironment(artifacts: ResolvedNativeArtifacts) {
367
372
  if (engineResourceDir && !process.env.ICU_DATA) {
368
373
  process.env.ICU_DATA = engineResourceDir;
369
374
  }
370
- if (engineBinaryDir) {
375
+
376
+ const pathDirs: string[] = [];
377
+ if (engineBinaryDir) pathDirs.push(engineBinaryDir);
378
+ if (artifacts.engine === "webview2" && artifacts.nativeLibPath) {
379
+ const dir = join(artifacts.nativeLibPath, "..");
380
+ if (existsSync(join(dir, "WebView2Loader.dll"))) pathDirs.push(dir);
381
+ }
382
+ if (pathDirs.length > 0) {
371
383
  const pathEntries = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
372
- if (!pathEntries.includes(engineBinaryDir)) {
373
- process.env.PATH = [engineBinaryDir, ...pathEntries].join(delimiter);
384
+ const newDirs = pathDirs.filter((d) => !pathEntries.includes(d));
385
+ if (newDirs.length > 0) {
386
+ process.env.PATH = [...newDirs, ...pathEntries].join(delimiter);
374
387
  }
375
388
  }
376
389
  }
@@ -575,18 +588,33 @@ export async function initNativeRuntime(
575
588
  return state;
576
589
  }
577
590
 
578
- const artifacts = resolveNativeArtifacts();
591
+ const artifacts = resolveNativeArtifacts(options.engine);
579
592
  const hasNativeArtifacts = Boolean(
580
593
  artifacts.nativeLibPath && existsSync(artifacts.nativeLibPath)
581
594
  );
582
595
 
583
596
  applyEnvironment(artifacts);
584
597
 
598
+ // Migration nudge: pre-existing CEF deps + unset engine ⇒ default flipped to WebView2.
599
+ if (
600
+ process.platform === "win32" &&
601
+ options.engine === undefined &&
602
+ resolvePackageRoot("bunite-cef-win-x64") != null
603
+ ) {
604
+ log.warn(
605
+ "[bunite] Detected bunite-cef-win-x64. Windows default engine is now \"webview2\" — " +
606
+ "this app is currently running on WebView2. " +
607
+ "To stay on CEF: pass engine: \"cef\" to AppRuntime. " +
608
+ "To accept the new default: drop bunite-cef-win-x64 from dependencies."
609
+ );
610
+ }
611
+
585
612
  if (!hasNativeArtifacts) {
613
+ const engineSuffix = artifacts.engine === "cef" ? " (engine=cef requires bunite-cef-win-x64)" : "";
586
614
  throw new Error(
587
615
  "bunite: native runtime not found. Install the platform package " +
588
- `(bunite-native-${process.platform === "win32" ? "win" : process.platform === "darwin" ? "mac" : "linux"}-<arch>) ` +
589
- "or set BUNITE_CEF_DIR to a CEF runtime directory."
616
+ `(bunite-native-${process.platform === "win32" ? "win" : process.platform === "darwin" ? "mac" : "linux"}-<arch>)` +
617
+ engineSuffix + "."
590
618
  );
591
619
  }
592
620
 
package/src/host/paths.ts CHANGED
@@ -6,6 +6,8 @@ import { CEF_VERSION } from "./cefVersion";
6
6
 
7
7
  const require = createRequire(import.meta.url);
8
8
 
9
+ export type WindowsEngine = "webview2" | "cef";
10
+
9
11
  export type ResolvedNativeArtifacts = {
10
12
  packageRoot: string;
11
13
  source: "optional-package" | "local-build" | "missing";
@@ -14,8 +16,17 @@ export type ResolvedNativeArtifacts = {
14
16
  nativeLibPath: string | null;
15
17
  /** CEF framework dir containing libcef.dll. Null on macOS/Linux (system framework). */
16
18
  cefDir: string | null;
19
+ /** Selected Windows engine. Undefined on mac/linux. */
20
+ engine?: WindowsEngine;
17
21
  };
18
22
 
23
+ function nativeLibBasename(engine: WindowsEngine | undefined): string {
24
+ if (PLATFORM_TAG === "win" && engine === "webview2") {
25
+ return `libBuniteNativeWebView2${NATIVE_LIB_EXT}`;
26
+ }
27
+ return `libBuniteNative${NATIVE_LIB_EXT}`;
28
+ }
29
+
19
30
  export function resolvePackageRoot(packageName: string): string | null {
20
31
  try {
21
32
  const packageJsonPath = require.resolve(`${packageName}/package.json`);
@@ -106,12 +117,14 @@ export function resolveDefaultAppResRoot(): string | null {
106
117
  return existsSync(candidate) ? candidate : null;
107
118
  }
108
119
 
109
- export function resolveNativeArtifacts(): ResolvedNativeArtifacts {
120
+ export function resolveNativeArtifacts(engine?: WindowsEngine): ResolvedNativeArtifacts {
110
121
  const exeDir = getBaseDir();
122
+ const resolvedEngine: WindowsEngine | undefined =
123
+ PLATFORM_TAG === "win" ? (engine ?? "webview2") : undefined;
124
+ const libName = nativeLibBasename(resolvedEngine);
111
125
 
112
- // 1. Entry-script-dir / executable-relative — covers both `bun dist/main.js`
113
- // and a compiled standalone binary, where artifacts ship alongside the entry.
114
- const exeNativeLib = join(exeDir, `libBuniteNative${NATIVE_LIB_EXT}`);
126
+ // 1. Entry-script-dir / executable-relative.
127
+ const exeNativeLib = join(exeDir, libName);
115
128
  if (existsSync(exeNativeLib)) {
116
129
  return {
117
130
  packageRoot: exeDir,
@@ -119,21 +132,22 @@ export function resolveNativeArtifacts(): ResolvedNativeArtifacts {
119
132
  nativePackageName: null,
120
133
  enginePackageName: null,
121
134
  nativeLibPath: exeNativeLib,
122
- cefDir: resolveCefDir([exeDir])
135
+ cefDir: resolvedEngine === "cef" ? resolveCefDir([exeDir]) : null,
136
+ engine: resolvedEngine
123
137
  };
124
138
  }
125
139
 
126
140
  const packageRoot = resolveBunitePackageRoot();
127
141
 
128
- // 2. Optional npm packages (bunite-native-*, bunite-cef-* on Windows only)
142
+ // 2. Optional npm packages.
129
143
  const nativePackageName = `bunite-native-${PLATFORM_TAG}-${ARCH}`;
130
- const enginePackageName = PLATFORM_TAG === "win" ? `bunite-cef-${PLATFORM_TAG}-${ARCH}` : null;
144
+ const enginePackageName = PLATFORM_TAG === "win" && resolvedEngine === "cef"
145
+ ? `bunite-cef-${PLATFORM_TAG}-${ARCH}`
146
+ : null;
131
147
  const nativePackageRoot = resolvePackageRoot(nativePackageName);
132
148
  const enginePackageRoot = enginePackageName ? resolvePackageRoot(enginePackageName) : null;
133
149
 
134
- const packagedNativeLibPath = nativePackageRoot
135
- ? join(nativePackageRoot, `libBuniteNative${NATIVE_LIB_EXT}`)
136
- : null;
150
+ const packagedNativeLibPath = nativePackageRoot ? join(nativePackageRoot, libName) : null;
137
151
  const packagedEngineDir = enginePackageRoot ?? null;
138
152
 
139
153
  if (packagedNativeLibPath && existsSync(packagedNativeLibPath)) {
@@ -143,16 +157,19 @@ export function resolveNativeArtifacts(): ResolvedNativeArtifacts {
143
157
  nativePackageName,
144
158
  enginePackageName: packagedEngineDir && existsSync(packagedEngineDir) ? enginePackageName : null,
145
159
  nativeLibPath: packagedNativeLibPath,
146
- cefDir: (packagedEngineDir && existsSync(packagedEngineDir))
147
- ? packagedEngineDir
148
- : resolveCefDir([nativePackageRoot, packageRoot].filter(Boolean) as string[])
160
+ cefDir: resolvedEngine === "cef"
161
+ ? ((packagedEngineDir && existsSync(packagedEngineDir))
162
+ ? packagedEngineDir
163
+ : resolveCefDir([nativePackageRoot, packageRoot].filter(Boolean) as string[]))
164
+ : null,
165
+ engine: resolvedEngine
149
166
  };
150
167
  }
151
168
 
152
- // 3. Local build (development)
169
+ // 3. Local build (development).
153
170
  if (packageRoot) {
154
171
  const localBuildRoot = join(packageRoot, "native-build", `${PLATFORM_TAG}-${ARCH}`);
155
- const directLib = join(localBuildRoot, `libBuniteNative${NATIVE_LIB_EXT}`);
172
+ const directLib = join(localBuildRoot, libName);
156
173
 
157
174
  if (existsSync(directLib)) {
158
175
  return {
@@ -161,12 +178,12 @@ export function resolveNativeArtifacts(): ResolvedNativeArtifacts {
161
178
  nativePackageName: null,
162
179
  enginePackageName: null,
163
180
  nativeLibPath: directLib,
164
- cefDir: resolveCefDir([localBuildRoot])
181
+ cefDir: resolvedEngine === "cef" ? resolveCefDir([localBuildRoot]) : null,
182
+ engine: resolvedEngine
165
183
  };
166
184
  }
167
185
 
168
- const releaseLib = join(localBuildRoot, "Release", `libBuniteNative${NATIVE_LIB_EXT}`);
169
-
186
+ const releaseLib = join(localBuildRoot, "Release", libName);
170
187
  if (existsSync(releaseLib)) {
171
188
  return {
172
189
  packageRoot,
@@ -174,7 +191,8 @@ export function resolveNativeArtifacts(): ResolvedNativeArtifacts {
174
191
  nativePackageName: null,
175
192
  enginePackageName: null,
176
193
  nativeLibPath: releaseLib,
177
- cefDir: resolveCefDir([localBuildRoot])
194
+ cefDir: resolvedEngine === "cef" ? resolveCefDir([localBuildRoot]) : null,
195
+ engine: resolvedEngine
178
196
  };
179
197
  }
180
198
  }
@@ -185,6 +203,7 @@ export function resolveNativeArtifacts(): ResolvedNativeArtifacts {
185
203
  nativePackageName: nativePackageRoot ? nativePackageName : null,
186
204
  enginePackageName: enginePackageRoot ? enginePackageName : null,
187
205
  nativeLibPath: null,
188
- cefDir: null
206
+ cefDir: null,
207
+ engine: resolvedEngine
189
208
  };
190
209
  }
@@ -1,6 +1,8 @@
1
1
  #pragma once
2
2
 
3
3
  #include <cstdio>
4
+ #include <cstdlib>
5
+ #include <cstring>
4
6
  #include <atomic>
5
7
 
6
8
  enum class BuniteLogLevel : int { Debug = 0, Info = 1, Warn = 2, Error = 3, Silent = 4 };
@@ -11,6 +13,18 @@ inline void buniteSetLogLevel(BuniteLogLevel level) {
11
13
  g_bunite_log_level.store(level, std::memory_order_relaxed);
12
14
  }
13
15
 
16
+ /** Read BUNITE_LOG_LEVEL env once and apply it. Call early in bunite_init so
17
+ * init-time logs land before the TS side's setNativeLogLevel arrives. */
18
+ inline void buniteApplyEnvLogLevel() {
19
+ const char* v = std::getenv("BUNITE_LOG_LEVEL");
20
+ if (!v || !*v) return;
21
+ if (std::strcmp(v, "debug") == 0) buniteSetLogLevel(BuniteLogLevel::Debug);
22
+ else if (std::strcmp(v, "info") == 0) buniteSetLogLevel(BuniteLogLevel::Info);
23
+ else if (std::strcmp(v, "warn") == 0) buniteSetLogLevel(BuniteLogLevel::Warn);
24
+ else if (std::strcmp(v, "error") == 0) buniteSetLogLevel(BuniteLogLevel::Error);
25
+ else if (std::strcmp(v, "silent") == 0) buniteSetLogLevel(BuniteLogLevel::Silent);
26
+ }
27
+
14
28
  inline bool buniteShouldLog(BuniteLogLevel level) {
15
29
  return static_cast<int>(level) >= static_cast<int>(g_bunite_log_level.load(std::memory_order_relaxed));
16
30
  }
@@ -0,0 +1,294 @@
1
+ #include "webview2_internal.h"
2
+
3
+ #include <cstring>
4
+
5
+ using namespace bunite_webview2;
6
+
7
+ // Forward declaration of helper defined in webview2_runtime.cpp.
8
+ namespace bunite_webview2 {
9
+ void setViewInputPassthrough(ViewHost* v, bool passthrough);
10
+ }
11
+
12
+ extern "C" {
13
+
14
+ BUNITE_EXPORT int32_t bunite_abi_version(void) { return 4; }
15
+
16
+ BUNITE_EXPORT void bunite_set_log_level(int32_t level) {
17
+ if (level < 0) level = 0;
18
+ if (level > 4) level = 4;
19
+ buniteSetLogLevel(static_cast<BuniteLogLevel>(level));
20
+ }
21
+
22
+ BUNITE_EXPORT bool bunite_init(const char* engine_dir, bool hide_console,
23
+ bool popup_blocking, const char* engine_config_json) {
24
+ return initRuntime(engine_dir, hide_console, popup_blocking, engine_config_json);
25
+ }
26
+
27
+ BUNITE_EXPORT const char* bunite_engine_name(void) { return "webview2"; }
28
+
29
+ BUNITE_EXPORT const char* bunite_engine_version(void) {
30
+ // Cache only on success — env is created lazily on first view, so early
31
+ // callers (e.g. window-title formatting) would otherwise pin "unknown".
32
+ static std::string cached;
33
+ if (!cached.empty()) return cached.c_str();
34
+ if (!g_runtime.env) return "unknown";
35
+ LPWSTR ver = nullptr;
36
+ if (FAILED(g_runtime.env->get_BrowserVersionString(&ver)) || !ver) {
37
+ if (ver) CoTaskMemFree(ver);
38
+ return "unknown";
39
+ }
40
+ cached = wideToUtf8(ver);
41
+ CoTaskMemFree(ver);
42
+ return cached.c_str();
43
+ }
44
+
45
+ BUNITE_EXPORT void bunite_run_loop(void) {
46
+ // Cooperative engine — TS drives via bunite_pump_once.
47
+ }
48
+
49
+ BUNITE_EXPORT void bunite_pump_once(void) { pumpOnce(); }
50
+
51
+ BUNITE_EXPORT void bunite_quit(void) { shutdownRuntime(); }
52
+
53
+ BUNITE_EXPORT void bunite_free_cstring(const char* value) {
54
+ if (value) free(const_cast<char*>(value));
55
+ }
56
+
57
+ BUNITE_EXPORT void bunite_set_webview_event_handler(BuniteWebviewEventHandler h) {
58
+ g_runtime.webview_event_handler = h;
59
+ }
60
+
61
+ BUNITE_EXPORT void bunite_set_window_event_handler(BuniteWindowEventHandler h) {
62
+ g_runtime.window_event_handler = h;
63
+ }
64
+
65
+ // ---- windows ----------------------------------------------------------
66
+
67
+ BUNITE_EXPORT bool bunite_window_create(
68
+ uint32_t window_id, double x, double y, double w, double h,
69
+ const char* title, const char* title_bar_style,
70
+ bool transparent, bool hidden, bool minimized, bool maximized) {
71
+ return createWindow(window_id, x, y, w, h, title, title_bar_style,
72
+ transparent, hidden, minimized, maximized);
73
+ }
74
+
75
+ BUNITE_EXPORT void bunite_window_destroy(uint32_t window_id) { destroyWindow(window_id); }
76
+
77
+ BUNITE_EXPORT void bunite_window_reset_close_pending(uint32_t window_id) {
78
+ WindowHost* w = getWindow(window_id);
79
+ if (w) w->close_pending.store(false);
80
+ }
81
+
82
+ BUNITE_EXPORT void bunite_window_show(uint32_t window_id) {
83
+ WindowHost* w = getWindow(window_id);
84
+ if (w && w->hwnd) ShowWindow(w->hwnd, SW_SHOW);
85
+ }
86
+
87
+ BUNITE_EXPORT void bunite_window_close(uint32_t window_id) {
88
+ WindowHost* w = getWindow(window_id);
89
+ if (w && w->hwnd) DestroyWindow(w->hwnd);
90
+ }
91
+
92
+ BUNITE_EXPORT void bunite_window_set_title(uint32_t window_id, const char* title) {
93
+ WindowHost* w = getWindow(window_id);
94
+ if (w && w->hwnd && title) SetWindowTextW(w->hwnd, utf8ToWide(title).c_str());
95
+ }
96
+
97
+ BUNITE_EXPORT void bunite_window_minimize(uint32_t window_id) {
98
+ WindowHost* w = getWindow(window_id);
99
+ if (w && w->hwnd) ShowWindow(w->hwnd, SW_MINIMIZE);
100
+ }
101
+
102
+ BUNITE_EXPORT void bunite_window_unminimize(uint32_t window_id) {
103
+ WindowHost* w = getWindow(window_id);
104
+ if (w && w->hwnd) ShowWindow(w->hwnd, SW_RESTORE);
105
+ }
106
+
107
+ BUNITE_EXPORT bool bunite_window_is_minimized(uint32_t window_id) {
108
+ WindowHost* w = getWindow(window_id);
109
+ return w && w->hwnd && IsIconic(w->hwnd);
110
+ }
111
+
112
+ BUNITE_EXPORT void bunite_window_maximize(uint32_t window_id) {
113
+ WindowHost* w = getWindow(window_id);
114
+ if (w && w->hwnd) ShowWindow(w->hwnd, SW_MAXIMIZE);
115
+ }
116
+
117
+ BUNITE_EXPORT void bunite_window_unmaximize(uint32_t window_id) {
118
+ WindowHost* w = getWindow(window_id);
119
+ if (w && w->hwnd) ShowWindow(w->hwnd, SW_RESTORE);
120
+ }
121
+
122
+ BUNITE_EXPORT bool bunite_window_is_maximized(uint32_t window_id) {
123
+ WindowHost* w = getWindow(window_id);
124
+ return w && w->hwnd && IsZoomed(w->hwnd);
125
+ }
126
+
127
+ BUNITE_EXPORT void bunite_window_set_frame(uint32_t window_id,
128
+ double x, double y, double w, double h) {
129
+ WindowHost* win = getWindow(window_id);
130
+ if (win && win->hwnd) {
131
+ SetWindowPos(win->hwnd, nullptr,
132
+ static_cast<int>(x), static_cast<int>(y),
133
+ static_cast<int>(w), static_cast<int>(h),
134
+ SWP_NOZORDER | SWP_NOACTIVATE);
135
+ }
136
+ }
137
+
138
+ // ---- views ------------------------------------------------------------
139
+
140
+ BUNITE_EXPORT bool bunite_view_create(
141
+ uint32_t view_id, uint32_t window_id,
142
+ const char* url, const char* html,
143
+ const char* preload, const char* appres_root,
144
+ const char* navigation_rules_json,
145
+ double x, double y, double w, double h,
146
+ bool auto_resize, bool sandbox, const char* preload_origins_json) {
147
+ return createView(view_id, window_id, url, html, preload, appres_root,
148
+ navigation_rules_json, x, y, w, h,
149
+ auto_resize, sandbox, preload_origins_json);
150
+ }
151
+
152
+ BUNITE_EXPORT void bunite_view_execute_javascript(uint32_t view_id, const char* script) {
153
+ ViewHost* v = getView(view_id);
154
+ if (!v || !v->webview || !script) return;
155
+ v->webview->ExecuteScript(utf8ToWide(script).c_str(), nullptr);
156
+ }
157
+
158
+ BUNITE_EXPORT void bunite_view_load_url(uint32_t view_id, const char* url) {
159
+ ViewHost* v = getView(view_id);
160
+ if (!v || !url) return;
161
+ if (v->webview) v->webview->Navigate(utf8ToWide(url).c_str());
162
+ else v->url = url;
163
+ }
164
+
165
+ BUNITE_EXPORT void bunite_view_load_html(uint32_t view_id, const char* html) {
166
+ ViewHost* v = getView(view_id);
167
+ if (!v || !html) return;
168
+ if (v->webview) v->webview->NavigateToString(utf8ToWide(html).c_str());
169
+ else v->html = html;
170
+ }
171
+
172
+ BUNITE_EXPORT void bunite_register_appres_route(const char* path) {
173
+ registerAppResRoute(path);
174
+ }
175
+
176
+ BUNITE_EXPORT void bunite_unregister_appres_route(const char* path) {
177
+ unregisterAppResRoute(path);
178
+ }
179
+
180
+ BUNITE_EXPORT void bunite_complete_route_request(uint32_t request_id, const char* html) {
181
+ completeRouteRequest(request_id, html);
182
+ }
183
+
184
+ BUNITE_EXPORT void bunite_view_set_visible(uint32_t view_id, bool visible) {
185
+ ViewHost* v = getView(view_id);
186
+ if (!v) return;
187
+ v->pending_visible = visible;
188
+ if (v->container_hwnd) ShowWindow(v->container_hwnd, visible ? SW_SHOW : SW_HIDE);
189
+ if (v->controller) v->controller->put_IsVisible(visible);
190
+ }
191
+
192
+ BUNITE_EXPORT void bunite_view_set_input_passthrough(uint32_t view_id, bool passthrough) {
193
+ ViewHost* v = getView(view_id);
194
+ if (v) setViewInputPassthrough(v, passthrough);
195
+ }
196
+
197
+ BUNITE_EXPORT void bunite_view_set_mask_region(uint32_t /*view_id*/,
198
+ const double* /*rects*/, uint32_t /*count*/) {
199
+ static bool warned = false;
200
+ if (!warned) {
201
+ BUNITE_WARN("webview2: set_mask_region ignored — WebView2 D3D surface cannot be masked. "
202
+ "Use engine \"cef\" + bunite-cef-win-x64 if mask region is required.");
203
+ warned = true;
204
+ }
205
+ }
206
+
207
+ BUNITE_EXPORT void bunite_view_bring_to_front(uint32_t view_id) {
208
+ ViewHost* v = getView(view_id);
209
+ if (!v || !v->container_hwnd) return;
210
+ // Raise our own container HWND. WebView2's internal HWND tree under the
211
+ // container moves with it; we never touch the Edge-owned HWNDs directly.
212
+ SetWindowPos(v->container_hwnd, HWND_TOP, 0, 0, 0, 0,
213
+ SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
214
+ }
215
+
216
+ static void applyBounds(ViewHost* v, double x, double y, double w, double h) {
217
+ v->bounds = { static_cast<LONG>(x), static_cast<LONG>(y),
218
+ static_cast<LONG>(x + w), static_cast<LONG>(y + h) };
219
+ if (v->container_hwnd) {
220
+ SetWindowPos(v->container_hwnd, nullptr,
221
+ v->bounds.left, v->bounds.top,
222
+ v->bounds.right - v->bounds.left,
223
+ v->bounds.bottom - v->bounds.top,
224
+ SWP_NOZORDER | SWP_NOACTIVATE);
225
+ }
226
+ if (v->controller) {
227
+ RECT inner{ 0, 0,
228
+ v->bounds.right - v->bounds.left,
229
+ v->bounds.bottom - v->bounds.top };
230
+ v->controller->put_Bounds(inner);
231
+ }
232
+ }
233
+
234
+ BUNITE_EXPORT void bunite_view_set_bounds(uint32_t view_id,
235
+ double x, double y, double w, double h) {
236
+ ViewHost* v = getView(view_id);
237
+ if (v) applyBounds(v, x, y, w, h);
238
+ }
239
+
240
+ BUNITE_EXPORT void bunite_view_set_bounds_async(uint32_t view_id,
241
+ double x, double y, double w, double h) {
242
+ postUiTask([view_id, x, y, w, h]() {
243
+ ViewHost* v = getView(view_id);
244
+ if (v) applyBounds(v, x, y, w, h);
245
+ });
246
+ }
247
+
248
+ BUNITE_EXPORT void bunite_view_set_anchor(uint32_t view_id, int mode, double /*inset*/) {
249
+ ViewHost* v = getView(view_id);
250
+ if (!v) return;
251
+ v->auto_resize = (mode == 1); // ViewAnchorMode::Fill
252
+ }
253
+
254
+ BUNITE_EXPORT void bunite_view_go_back(uint32_t view_id) {
255
+ ViewHost* v = getView(view_id);
256
+ if (v && v->webview) v->webview->GoBack();
257
+ }
258
+
259
+ BUNITE_EXPORT void bunite_view_reload(uint32_t view_id) {
260
+ ViewHost* v = getView(view_id);
261
+ if (v && v->webview) v->webview->Reload();
262
+ }
263
+
264
+ BUNITE_EXPORT void bunite_view_remove(uint32_t view_id) { destroyView(view_id); }
265
+
266
+ BUNITE_EXPORT void bunite_view_open_devtools(uint32_t view_id) {
267
+ ViewHost* v = getView(view_id);
268
+ if (v && v->webview) v->webview->OpenDevToolsWindow();
269
+ }
270
+
271
+ BUNITE_EXPORT void bunite_view_close_devtools(uint32_t /*view_id*/) {
272
+ // WebView2 doesn't expose a "close devtools" API; the user closes the panel.
273
+ }
274
+
275
+ BUNITE_EXPORT void bunite_view_toggle_devtools(uint32_t view_id) {
276
+ bunite_view_open_devtools(view_id);
277
+ }
278
+
279
+ // ---- permissions ------------------------------------------------------
280
+
281
+ BUNITE_EXPORT void bunite_complete_permission_request(uint32_t request_id, uint32_t state) {
282
+ PendingPermissionRequest p;
283
+ {
284
+ std::lock_guard<std::mutex> g(g_runtime.permission_mutex);
285
+ auto it = g_runtime.pending_permissions.find(request_id);
286
+ if (it == g_runtime.pending_permissions.end()) return;
287
+ p = std::move(it->second);
288
+ g_runtime.pending_permissions.erase(it);
289
+ }
290
+ if (p.args) p.args->put_State(buniteStateToWebView2(state));
291
+ if (p.deferral) p.deferral->Complete();
292
+ }
293
+
294
+ } // extern "C"