plusui-native-core 0.1.104 → 0.1.106

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/Core/.claude/settings.local.json +7 -0
  2. package/Core/CMakeLists.txt +1 -1
  3. package/Core/Features/API/Connect_API.ts +20 -52
  4. package/Core/Features/API/app-api.ts +28 -28
  5. package/Core/Features/API/browser-api.ts +38 -38
  6. package/Core/Features/API/clipboard-api.ts +21 -21
  7. package/Core/Features/API/display-api.ts +33 -33
  8. package/Core/Features/API/keyboard-api.ts +33 -33
  9. package/Core/Features/API/menu-api.ts +39 -39
  10. package/Core/Features/API/router-api.ts +23 -23
  11. package/Core/Features/API/tray-api.ts +22 -22
  12. package/Core/Features/API/webgpu-api.ts +55 -55
  13. package/Core/Features/App/app.cpp +128 -102
  14. package/Core/Features/Browser/browser.cpp +227 -227
  15. package/Core/Features/Browser/browser.ts +161 -161
  16. package/Core/Features/Clipboard/clipboard.cpp +235 -235
  17. package/Core/Features/Display/display.cpp +212 -212
  18. package/Core/Features/FileDrop/filedrop.cpp +448 -324
  19. package/Core/Features/FileDrop/filedrop.css +421 -421
  20. package/Core/Features/FileDrop/filedrop.ts +5 -9
  21. package/Core/Features/Keyboard/keyboard_linux.cpp +4 -0
  22. package/Core/Features/Router/router.cpp +62 -62
  23. package/Core/Features/Router/router.ts +113 -113
  24. package/Core/Features/Tray/tray.cpp +328 -324
  25. package/Core/Features/WebGPU/webgpu.cpp +948 -948
  26. package/Core/Features/Window/webview.cpp +1026 -1014
  27. package/Core/Features/Window/webview.ts +516 -516
  28. package/Core/Features/Window/window.cpp +2265 -1988
  29. package/Core/include/plusui/api.hpp +237 -237
  30. package/Core/include/plusui/app.hpp +33 -33
  31. package/Core/include/plusui/browser.hpp +67 -67
  32. package/Core/include/plusui/clipboard.hpp +41 -41
  33. package/Core/include/plusui/connect.hpp +340 -340
  34. package/Core/include/plusui/connection.hpp +3 -3
  35. package/Core/include/plusui/display.hpp +90 -90
  36. package/Core/include/plusui/filedrop.hpp +92 -77
  37. package/Core/include/plusui/keyboard.hpp +112 -112
  38. package/Core/include/plusui/menu.hpp +153 -153
  39. package/Core/include/plusui/plusui +18 -18
  40. package/Core/include/plusui/router.hpp +42 -42
  41. package/Core/include/plusui/tray.hpp +94 -94
  42. package/Core/include/plusui/webgpu.hpp +434 -434
  43. package/Core/include/plusui/window.hpp +180 -177
  44. package/Core/scripts/generate-umbrella-header.mjs +77 -77
  45. package/Core/vendor/WebView2EnvironmentOptions.h +406 -406
  46. package/Core/vendor/webview.h +487 -487
  47. package/Core/vendor/webview2.h +52079 -52079
  48. package/README.md +19 -19
  49. package/package.json +1 -1
@@ -1,22 +1,25 @@
1
- #include <fstream>
2
- #include <functional>
3
- #include <iostream>
4
- #include <map>
5
- #include <plusui/display.hpp>
6
- #include <plusui/tray.hpp>
7
- #include <plusui/webgpu.hpp>
8
- #include <plusui/window.hpp>
9
- #include <regex>
10
- #include <sstream>
11
-
12
- #ifdef _WIN32
13
- #pragma warning(push)
14
- #pragma warning(disable : 4996)
15
- #include <WebView2.h>
16
- #include <windows.h>
17
- #include <wrl.h>
18
- using namespace Microsoft::WRL;
19
- #pragma warning(pop)
1
+ #include <fstream>
2
+ #include <functional>
3
+ #include <iostream>
4
+ #include <map>
5
+ #include <plusui/display.hpp>
6
+ #include <plusui/tray.hpp>
7
+ #include <plusui/webgpu.hpp>
8
+ #include <plusui/window.hpp>
9
+ #include <regex>
10
+ #include <sstream>
11
+
12
+ #ifdef _WIN32
13
+ #pragma warning(push)
14
+ #pragma warning(disable : 4996)
15
+ #include <WebView2.h>
16
+ #include <windows.h>
17
+ #include <wrl.h>
18
+ #include <shlobj.h>
19
+ #include <oleidl.h>
20
+ #include <shellapi.h>
21
+ using namespace Microsoft::WRL;
22
+ #pragma warning(pop)
20
23
  #elif defined(__APPLE__)
21
24
  #include <Cocoa/Cocoa.h>
22
25
  #import <AppKit/AppKit.h>
@@ -25,66 +28,67 @@ using namespace Microsoft::WRL;
25
28
  #include <objc/objc-runtime.h>
26
29
 
27
30
  #else
31
+ #include <cstdlib>
28
32
  #include <gdk/gdk.h>
29
33
  #include <gtk/gtk.h>
30
34
  #include <webkit2/webkit2.h>
31
35
  #endif
32
-
33
- #include <stb_image.h>
34
-
35
- namespace plusui {
36
-
37
- namespace {
38
- constexpr char kHideScrollbarsScript[] = R"(
39
- (function() {
40
- var css = "::-webkit-scrollbar{display:none!important;width:0!important;height:0!important;}::-webkit-scrollbar-track{background:transparent!important;}::-webkit-scrollbar-thumb{background:transparent!important;}*{-ms-overflow-style:none!important;scrollbar-width:none!important;scrollbar-color:transparent transparent!important;}";
41
- var styleId = "__plusui_hide_scrollbars";
42
- var ensureStyle = function() {
43
- if (document.getElementById(styleId)) return;
44
- var style = document.createElement("style");
45
- style.id = styleId;
46
- style.type = "text/css";
47
- style.appendChild(document.createTextNode(css));
48
- var container = document.head || document.documentElement || document.body;
49
- if (container) {
50
- container.appendChild(style);
51
- }
52
- };
53
- ensureStyle();
54
- document.addEventListener("DOMContentLoaded", ensureStyle);
55
- window.addEventListener("load", ensureStyle);
56
- })();
57
- )";
58
-
59
- #ifdef _WIN32
60
- static std::string jsonEscape(const std::string &input) {
61
- std::string out;
62
- out.reserve(input.size() + 8);
63
- for (char c : input) {
64
- switch (c) {
65
- case '\\':
66
- out += "\\\\";
67
- break;
68
- case '"':
69
- out += "\\\"";
70
- break;
71
- case '\n':
72
- out += "\\n";
73
- break;
74
- case '\r':
75
- out += "\\r";
76
- break;
77
- case '\t':
78
- out += "\\t";
79
- break;
80
- default:
81
- out += c;
82
- break;
83
- }
84
- }
85
- return out;
86
- }
87
-
36
+
37
+ #include <stb_image.h>
38
+
39
+ namespace plusui {
40
+
41
+ namespace {
42
+ constexpr char kHideScrollbarsScript[] = R"(
43
+ (function() {
44
+ var css = "::-webkit-scrollbar{display:none!important;width:0!important;height:0!important;}::-webkit-scrollbar-track{background:transparent!important;}::-webkit-scrollbar-thumb{background:transparent!important;}*{-ms-overflow-style:none!important;scrollbar-width:none!important;scrollbar-color:transparent transparent!important;}";
45
+ var styleId = "__plusui_hide_scrollbars";
46
+ var ensureStyle = function() {
47
+ if (document.getElementById(styleId)) return;
48
+ var style = document.createElement("style");
49
+ style.id = styleId;
50
+ style.type = "text/css";
51
+ style.appendChild(document.createTextNode(css));
52
+ var container = document.head || document.documentElement || document.body;
53
+ if (container) {
54
+ container.appendChild(style);
55
+ }
56
+ };
57
+ ensureStyle();
58
+ document.addEventListener("DOMContentLoaded", ensureStyle);
59
+ window.addEventListener("load", ensureStyle);
60
+ })();
61
+ )";
62
+
63
+ #ifdef _WIN32
64
+ static std::string jsonEscape(const std::string &input) {
65
+ std::string out;
66
+ out.reserve(input.size() + 8);
67
+ for (char c : input) {
68
+ switch (c) {
69
+ case '\\':
70
+ out += "\\\\";
71
+ break;
72
+ case '"':
73
+ out += "\\\"";
74
+ break;
75
+ case '\n':
76
+ out += "\\n";
77
+ break;
78
+ case '\r':
79
+ out += "\\r";
80
+ break;
81
+ case '\t':
82
+ out += "\\t";
83
+ break;
84
+ default:
85
+ out += c;
86
+ break;
87
+ }
88
+ }
89
+ return out;
90
+ }
91
+
88
92
  // Map Windows VK code to PlusUI KeyCode (GLFW-style values)
89
93
  static int vkToPlusUIKeyCode(WPARAM vk) {
90
94
  if (vk >= 'A' && vk <= 'Z') return (int)vk; // A=65…Z=90
@@ -121,31 +125,41 @@ static int currentKeyMods() {
121
125
  return mods;
122
126
  }
123
127
 
124
- static std::string mimeTypeFromPath(const std::string &path) {
125
- size_t dot = path.find_last_of('.');
126
- if (dot == std::string::npos)
127
- return "application/octet-stream";
128
- std::string ext = path.substr(dot);
129
- for (char &ch : ext)
130
- ch = static_cast<char>(tolower(static_cast<unsigned char>(ch)));
131
- if (ext == ".png")
132
- return "image/png";
133
- if (ext == ".jpg" || ext == ".jpeg")
134
- return "image/jpeg";
135
- if (ext == ".gif")
136
- return "image/gif";
137
- if (ext == ".webp")
138
- return "image/webp";
139
- if (ext == ".svg")
140
- return "image/svg+xml";
141
- if (ext == ".txt")
142
- return "text/plain";
143
- if (ext == ".json")
144
- return "application/json";
145
- if (ext == ".pdf")
146
- return "application/pdf";
128
+ static std::string mimeTypeFromPath(const std::string &path) {
129
+ size_t dot = path.find_last_of('.');
130
+ if (dot == std::string::npos)
131
+ return "application/octet-stream";
132
+ std::string ext = path.substr(dot);
133
+ for (char &ch : ext)
134
+ ch = static_cast<char>(tolower(static_cast<unsigned char>(ch)));
135
+ if (ext == ".png")
136
+ return "image/png";
137
+ if (ext == ".jpg" || ext == ".jpeg")
138
+ return "image/jpeg";
139
+ if (ext == ".gif")
140
+ return "image/gif";
141
+ if (ext == ".webp")
142
+ return "image/webp";
143
+ if (ext == ".svg")
144
+ return "image/svg+xml";
145
+ if (ext == ".txt")
146
+ return "text/plain";
147
+ if (ext == ".json")
148
+ return "application/json";
149
+ if (ext == ".pdf")
150
+ return "application/pdf";
147
151
  return "application/octet-stream";
148
152
  }
153
+ // ---------------------------------------------------------------------------
154
+ //
155
+ // WebView2 has a design problem: AllowExternalDrop(TRUE) makes it consume
156
+ // drops so WM_DROPFILES never fires; AllowExternalDrop(FALSE) makes it
157
+ // reject drags entirely so even WM_DROPFILES never fires. The solution is
158
+ // to keep AllowExternalDrop(FALSE) and register our own IDropTarget on the
159
+ // parent HWND via RegisterDragDrop. This intercepts drops before WebView2
160
+ // ever sees them, giving us full control over visual feedback AND file data.
161
+ // ---------------------------------------------------------------------------
162
+
149
163
  #endif // _WIN32
150
164
 
151
165
  #if !defined(_WIN32)
@@ -181,196 +195,85 @@ static std::string buildShortcutScript(const std::string& id) {
181
195
  #endif // !_WIN32
182
196
 
183
197
  } // namespace
184
-
185
- struct Window::Impl {
186
- void *nativeWindow = nullptr;
187
- void *nativeWebView = nullptr;
188
- WindowConfig config;
189
- WindowState state;
190
-
191
- // WebView-specific members
192
- std::string currentURL;
193
- std::string currentTitle;
194
- std::shared_ptr<Window> window;
195
- std::string userAgent;
196
- bool loading = false;
197
- double zoom = 1.0;
198
- bool ready = false;
199
- std::vector<std::string> pendingScripts;
200
- std::string pendingNavigation;
201
- std::string pendingHTML;
202
- std::string pendingFile;
203
- std::unique_ptr<TrayManager> trayManager;
204
- WebGPU webgpu;
205
- NavigationCallback navigationCallback;
206
- LoadCallback loadStartCallback;
207
- LoadCallback loadEndCallback;
208
- LoadCallback navigationCompleteCallback;
209
- ErrorCallback errorCallback;
210
- ConsoleCallback consoleCallback;
211
- MessageCallback messageCallback;
198
+
199
+ struct Window::Impl {
200
+ void *nativeWindow = nullptr;
201
+ void *nativeWebView = nullptr;
202
+ WindowConfig config;
203
+ WindowState state;
204
+
205
+ // WebView-specific members
206
+ std::string currentURL;
207
+ std::string currentTitle;
208
+ std::shared_ptr<Window> window;
209
+ std::string userAgent;
210
+ bool loading = false;
211
+ double zoom = 1.0;
212
+ bool ready = false;
213
+ std::vector<std::string> pendingScripts;
214
+ std::string pendingNavigation;
215
+ std::string pendingHTML;
216
+ std::string pendingFile;
217
+ std::unique_ptr<TrayManager> trayManager;
218
+ WebGPU webgpu;
219
+ NavigationCallback navigationCallback;
220
+ LoadCallback loadStartCallback;
221
+ LoadCallback loadEndCallback;
222
+ LoadCallback navigationCompleteCallback;
223
+ ErrorCallback errorCallback;
224
+ ConsoleCallback consoleCallback;
225
+ MessageCallback messageCallback;
212
226
  FileDropCallback fileDropCallback;
213
227
  std::map<std::string, JSCallback> bindings;
214
228
  std::map<int, std::string> hotkeys; // hotkey id -> shortcut id
215
-
216
- std::vector<MoveCallback> moveCallbacks;
217
- std::vector<ResizeCallback> resizeCallbacks;
218
- std::vector<CloseCallback> closeCallbacks;
219
- std::vector<FocusCallback> focusCallbacks;
220
- std::vector<StateCallback> stateCallbacks;
221
-
222
- #ifdef _WIN32
223
- HWND hwnd = nullptr;
224
- WNDPROC originalProc = nullptr;
225
- ComPtr<ICoreWebView2Controller> controller;
226
- ComPtr<ICoreWebView2> webview;
227
- static std::map<HWND, Impl *> embeddedWebviewByParent;
228
-
229
- static LRESULT CALLBACK wndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
230
- Impl *impl = nullptr;
231
- if (msg == WM_NCCREATE) {
232
- auto cs = (LPCREATESTRUCT)lp;
233
- impl = (Impl *)cs->lpCreateParams;
234
- SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)impl);
235
- } else {
236
- impl = (Impl *)GetWindowLongPtr(hwnd, GWLP_USERDATA);
237
- }
238
-
239
- if (impl) {
240
- switch (msg) {
241
- case WM_MOVE: {
242
- int x = (int)(short)LOWORD(lp);
243
- int y = (int)(short)HIWORD(lp);
244
- impl->state.x = x;
245
- impl->state.y = y;
246
- for (auto &cb : impl->moveCallbacks)
247
- cb(x, y);
248
- break;
249
- }
250
- case WM_SIZE: {
251
- int width = LOWORD(lp);
252
- int height = HIWORD(lp);
253
- impl->state.width = width;
254
- impl->state.height = height;
255
- for (auto &cb : impl->resizeCallbacks)
256
- cb(width, height);
257
- break;
258
- }
259
- case WM_CLOSE:
260
- for (auto &cb : impl->closeCallbacks)
261
- cb();
262
- break;
263
- case WM_DROPFILES: {
264
- HDROP hDrop = reinterpret_cast<HDROP>(wp);
265
- Impl *targetImpl = impl;
266
- if (!targetImpl->webview) {
267
- auto it = embeddedWebviewByParent.find(hwnd);
268
- if (it != embeddedWebviewByParent.end()) {
269
- targetImpl = it->second;
270
- }
271
- }
272
-
273
- if (!targetImpl || !targetImpl->config.fileDrop ||
274
- !targetImpl->webview) {
275
- DragFinish(hDrop);
276
- return 0;
277
- }
278
-
279
- UINT fileCount = DragQueryFileW(hDrop, 0xFFFFFFFF, nullptr, 0);
280
- std::string filesJson = "[";
281
-
282
- for (UINT i = 0; i < fileCount; ++i) {
283
- UINT pathLen = DragQueryFileW(hDrop, i, nullptr, 0);
284
- std::wstring wpath(pathLen + 1, L'\0');
285
- DragQueryFileW(hDrop, i, &wpath[0], pathLen + 1);
286
- wpath.resize(pathLen);
287
-
288
- int utf8Len = WideCharToMultiByte(CP_UTF8, 0, wpath.c_str(), -1,
289
- nullptr, 0, nullptr, nullptr);
290
- std::string path;
291
- if (utf8Len > 0) {
292
- path.resize(static_cast<size_t>(utf8Len - 1));
293
- WideCharToMultiByte(CP_UTF8, 0, wpath.c_str(), -1, &path[0],
294
- utf8Len, nullptr, nullptr);
295
- }
296
-
297
- std::string name = path;
298
- size_t lastSlash = path.find_last_of("\\/");
299
- if (lastSlash != std::string::npos) {
300
- name = path.substr(lastSlash + 1);
301
- }
302
-
303
- unsigned long long sizeBytes = 0;
304
- WIN32_FILE_ATTRIBUTE_DATA fileAttr;
305
- if (GetFileAttributesExW(wpath.c_str(), GetFileExInfoStandard,
306
- &fileAttr)) {
307
- ULARGE_INTEGER size;
308
- size.HighPart = fileAttr.nFileSizeHigh;
309
- size.LowPart = fileAttr.nFileSizeLow;
310
- sizeBytes = size.QuadPart;
311
- }
312
-
313
- if (i > 0)
314
- filesJson += ",";
315
- filesJson += "{\"path\":\"" + jsonEscape(path) + "\",\"name\":\"" +
316
- jsonEscape(name) + "\",\"type\":\"" +
317
- jsonEscape(mimeTypeFromPath(path)) +
318
- "\",\"size\":" + std::to_string(sizeBytes) + "}";
319
- }
320
- filesJson += "]";
321
-
322
- // Must query drop point BEFORE DragFinish invalidates hDrop
323
- POINT dropPoint = {0, 0};
324
- DragQueryPoint(hDrop, &dropPoint);
325
-
326
- DragFinish(hDrop);
327
-
328
- // Always fire the C++ callback regardless of zone
329
- if (targetImpl->fileDropCallback) {
330
- targetImpl->fileDropCallback(filesJson);
331
- }
332
229
 
333
- // DragQueryPoint returns physical (device) pixels relative to the
334
- // client area. document.elementFromPoint uses CSS (logical) pixels.
335
- // Divide by the DPI scale factor so the hit-test is correct on HiDPI
336
- // displays (e.g. 150% scaling → divide by 1.5).
337
- double dpiScale = 1.0;
338
- UINT dpi = GetDpiForWindow(hwnd);
339
- if (dpi > 0) dpiScale = static_cast<double>(dpi) / 96.0;
230
+ std::vector<MoveCallback> moveCallbacks;
231
+ std::vector<ResizeCallback> resizeCallbacks;
232
+ std::vector<CloseCallback> closeCallbacks;
233
+ std::vector<FocusCallback> focusCallbacks;
234
+ std::vector<StateCallback> stateCallbacks;
340
235
 
341
- int dpx = static_cast<int>(dropPoint.x / dpiScale);
342
- int dpy = static_cast<int>(dropPoint.y / dpiScale);
236
+ #ifdef _WIN32
237
+ HWND hwnd = nullptr;
238
+ WNDPROC originalProc = nullptr;
239
+ ComPtr<ICoreWebView2Controller> controller;
240
+ ComPtr<ICoreWebView2> webview;
241
+ static std::map<HWND, Impl *> embeddedWebviewByParent;
343
242
 
344
- std::string eventScript =
345
- "(function(){"
346
- "var files=" +
347
- filesJson +
348
- ";"
349
- // Global event — always fires
350
- "window.dispatchEvent(new "
351
- "CustomEvent('plusui:fileDrop.filesDropped',"
352
- " { detail: { files: files } }));"
353
- // Zone-specific delivery via DPI-corrected hit test
354
- "var el=document.elementFromPoint(" +
355
- std::to_string(dpx) + "," + std::to_string(dpy) +
356
- ");"
357
- "var zone=el&&el.closest?el.closest('[data-dropzone]'):null;"
358
- "var zoneName=zone?zone.getAttribute('data-dropzone'):null;"
359
- "if(zoneName&&window.__plusui_fileDrop__){"
360
- "window.__plusui_fileDrop__(zoneName,files);"
361
- "}"
362
- // If no zone matched deliver to all registered zones so a
363
- // single-zone app always works regardless of hit-test accuracy.
364
- "if(!zoneName&&window.__plusui_fileDrop_default__){"
365
- "window.__plusui_fileDrop_default__(files);"
366
- "}"
367
- "})();";
243
+ static LRESULT CALLBACK wndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
244
+ Impl *impl = nullptr;
245
+ if (msg == WM_NCCREATE) {
246
+ auto cs = (LPCREATESTRUCT)lp;
247
+ impl = (Impl *)cs->lpCreateParams;
248
+ SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)impl);
249
+ } else {
250
+ impl = (Impl *)GetWindowLongPtr(hwnd, GWLP_USERDATA);
251
+ }
368
252
 
369
- targetImpl->webview->ExecuteScript(
370
- std::wstring(eventScript.begin(), eventScript.end()).c_str(),
371
- nullptr);
372
- return 0;
373
- }
253
+ if (impl) {
254
+ switch (msg) {
255
+ case WM_MOVE: {
256
+ int x = (int)(short)LOWORD(lp);
257
+ int y = (int)(short)HIWORD(lp);
258
+ impl->state.x = x;
259
+ impl->state.y = y;
260
+ for (auto &cb : impl->moveCallbacks)
261
+ cb(x, y);
262
+ break;
263
+ }
264
+ case WM_SIZE: {
265
+ int width = LOWORD(lp);
266
+ int height = HIWORD(lp);
267
+ impl->state.width = width;
268
+ impl->state.height = height;
269
+ for (auto &cb : impl->resizeCallbacks)
270
+ cb(width, height);
271
+ break;
272
+ }
273
+ case WM_CLOSE:
274
+ for (auto &cb : impl->closeCallbacks)
275
+ cb();
276
+ break;
374
277
  case WM_HOTKEY: {
375
278
  int hotKeyId = (int)wp;
376
279
  auto it = impl->hotkeys.find(hotKeyId);
@@ -435,717 +338,755 @@ struct Window::Impl {
435
338
  }
436
339
  return DefWindowProc(hwnd, msg, wp, lp);
437
340
  }
438
- #elif defined(__APPLE__)
439
- WKWebView *wkWebView = nullptr;
440
- #else
441
- WebKitWebView *gtkWebView = nullptr;
442
- #endif
443
- };
444
-
445
- Window::Window() : pImpl(std::shared_ptr<Impl>(new Impl())) {}
446
-
447
- #ifdef _WIN32
448
- std::map<HWND, Window::Impl *> Window::Impl::embeddedWebviewByParent;
449
- #endif
450
-
451
- Window::~Window() = default;
452
-
453
- Window::Window(Window &&other) noexcept = default;
454
- Window &Window::operator=(Window &&other) noexcept = default;
455
-
456
- Window Window::create(const WindowConfig &config) {
457
- Window w;
458
- w.pImpl->config = config;
459
-
460
- #ifdef _WIN32
461
- WNDCLASSEXW wc = {};
462
- wc.cbSize = sizeof(WNDCLASSEXW);
463
- wc.lpfnWndProc = Impl::wndProc;
464
- wc.hInstance = GetModuleHandle(nullptr);
465
- wc.lpszClassName = L"PLUSUI_WINDOW";
466
- wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
467
- wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
468
- RegisterClassExW(&wc);
469
-
470
- DWORD style = 0;
471
- DWORD exStyle = 0;
472
-
473
- if (config.decorations) {
474
- style = WS_OVERLAPPEDWINDOW;
475
- if (!config.resizable)
476
- style &= ~WS_THICKFRAME;
477
- if (!config.minimizable)
478
- style &= ~WS_MINIMIZEBOX;
479
- if (!config.closable)
480
- style &= ~WS_SYSMENU;
481
- } else {
482
- // Frameless window
483
- style = WS_POPUP;
484
- if (config.resizable)
485
- style |= WS_THICKFRAME;
486
- }
487
-
488
- if (config.transparent) {
489
- exStyle = WS_EX_LAYERED;
490
- }
491
-
492
- if (config.skipTaskbar) {
493
- exStyle |= WS_EX_TOOLWINDOW;
494
- }
495
-
496
- std::wstring wideTitle(config.title.begin(), config.title.end());
497
- w.pImpl->hwnd = CreateWindowExW(
498
- exStyle, L"PLUSUI_WINDOW", wideTitle.c_str(), style,
499
- config.x >= 0 ? config.x : CW_USEDEFAULT,
500
- config.y >= 0 ? config.y : CW_USEDEFAULT, config.width, config.height,
501
- nullptr, nullptr, GetModuleHandle(nullptr), w.pImpl.get());
502
-
503
- w.pImpl->nativeWindow = (void *)w.pImpl->hwnd;
504
- w.pImpl->state.width = config.width;
505
- w.pImpl->state.height = config.height;
506
-
507
- DragAcceptFiles(w.pImpl->hwnd, config.fileDrop ? TRUE : FALSE);
508
-
509
- if (config.center) {
510
- RECT screen;
511
- SystemParametersInfo(SPI_GETWORKAREA, 0, &screen, 0);
512
- int x = (screen.right - config.width) / 2;
513
- int y = (screen.bottom - config.height) / 2;
514
- SetWindowPos(w.pImpl->hwnd, nullptr, x, y, 0, 0, SWP_NOZORDER | SWP_NOSIZE);
515
- }
516
-
517
- if (config.alwaysOnTop) {
518
- SetWindowPos(w.pImpl->hwnd, HWND_TOPMOST, 0, 0, 0, 0,
519
- SWP_NOMOVE | SWP_NOSIZE);
520
- }
521
-
522
- #elif defined(__APPLE__)
523
- NSWindow *nswin = [[NSWindow alloc]
524
- initWithContentRect:NSMakeRect(config.x >= 0 ? config.x : 100,
525
- config.y >= 0 ? config.y : 100,
526
- config.width, config.height)
527
- styleMask:(config.resizable ? NSWindowStyleMaskResizable : 0) |
528
- NSWindowStyleMaskTitled | NSWindowStyleMaskClosable |
529
- (config.minimizable ? NSWindowStyleMaskMiniaturizable
530
- : 0)backing:NSBackingStoreBuffered
531
- defer:NO];
532
-
533
- [nswin setTitle:[NSString stringWithUTF8String:config.title.c_str()]];
534
- [nswin setReleasedWhenClosed:NO];
535
-
536
- if (config.center) {
537
- [nswin center];
538
- }
539
-
540
- if (config.alwaysOnTop) {
541
- [nswin setLevel:NSFloatingWindowLevel];
542
- }
543
-
544
- w.pImpl->nativeWindow = (__bridge void *)nswin;
545
-
546
- #else
547
- gtk_init_check(0, nullptr);
548
-
549
- GtkWindow *gtkwin = GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL));
550
- gtk_window_set_title(gtkwin, config.title.c_str());
551
- gtk_window_set_default_size(gtkwin, config.width, config.height);
552
-
553
- if (config.x >= 0 && config.y >= 0) {
554
- gtk_window_move(gtkwin, config.x, config.y);
555
- } else if (config.center) {
556
- gtk_window_set_position(gtkwin, GTK_WIN_POS_CENTER);
557
- }
558
-
559
- if (!config.resizable) {
560
- gtk_window_set_resizable(gtkwin, FALSE);
561
- }
562
-
563
- if (config.alwaysOnTop) {
564
- gtk_window_set_keep_above(gtkwin, TRUE);
565
- }
566
-
567
- w.pImpl->nativeWindow = (void *)gtkwin;
568
- #endif
569
-
570
- // Initialize tray manager for native windows
571
- w.pImpl->trayManager = std::make_unique<TrayManager>();
572
- #ifdef _WIN32
573
- if (w.pImpl->hwnd) {
574
- w.pImpl->trayManager->setWindowHandle((void *)w.pImpl->hwnd);
575
- }
576
- #elif defined(__APPLE__)
577
- w.pImpl->trayManager->setWindowHandle(w.pImpl->nativeWindow);
578
- #else
579
- w.pImpl->trayManager->setWindowHandle(w.pImpl->nativeWindow);
580
- #endif
581
-
582
- return w;
583
- }
584
-
585
- void Window::setTitle(const std::string &title) {
586
- pImpl->config.title = title;
587
- #ifdef _WIN32
588
- if (pImpl->hwnd) {
589
- std::wstring wideTitle(title.begin(), title.end());
590
- SetWindowTextW(pImpl->hwnd, wideTitle.c_str());
591
- }
592
- #elif defined(__APPLE__)
593
- if (pImpl->nativeWindow) {
594
- NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
595
- [nswin setTitle:[NSString stringWithUTF8String:title.c_str()]];
596
- }
597
- #else
598
- if (pImpl->nativeWindow) {
599
- gtk_window_set_title(GTK_WINDOW(pImpl->nativeWindow), title.c_str());
600
- }
601
- #endif
602
- }
603
-
604
- std::string Window::getTitle() const {
605
- if (!pImpl->currentTitle.empty())
606
- return pImpl->currentTitle;
607
- return pImpl->config.title;
608
- }
609
-
610
- void Window::setSize(int width, int height) {
611
- pImpl->config.width = width;
612
- pImpl->config.height = height;
613
- #ifdef _WIN32
614
- if (pImpl->hwnd)
615
- SetWindowPos(pImpl->hwnd, nullptr, 0, 0, width, height,
616
- SWP_NOZORDER | SWP_NOMOVE);
617
- #elif defined(__APPLE__)
618
- if (pImpl->nativeWindow) {
619
- NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
620
- [nswin setContentSize:NSMakeSize(width, height)];
621
- }
622
- #else
623
- if (pImpl->nativeWindow) {
624
- gtk_window_resize(GTK_WINDOW(pImpl->nativeWindow), width, height);
625
- }
626
- #endif
627
- }
628
-
629
- void Window::getSize(int &width, int &height) const {
630
- width = pImpl->state.width;
631
- height = pImpl->state.height;
632
- }
633
-
634
- void Window::setMinSize(int minWidth, int minHeight) {
635
- pImpl->config.minWidth = minWidth;
636
- pImpl->config.minHeight = minHeight;
637
- #ifdef _WIN32
638
- if (pImpl->hwnd) {
639
- SetWindowLongPtr(pImpl->hwnd, GWL_STYLE,
640
- GetWindowLong(pImpl->hwnd, GWL_STYLE) | WS_THICKFRAME);
641
- }
642
- #endif
643
- }
644
-
645
- void Window::setMaxSize(int maxWidth, int maxHeight) {
646
- pImpl->config.maxWidth = maxWidth;
647
- pImpl->config.maxHeight = maxHeight;
648
- }
649
-
650
- void Window::setPosition(int x, int y) {
651
- pImpl->config.x = x;
652
- pImpl->config.y = y;
653
- #ifdef _WIN32
654
- if (pImpl->hwnd)
655
- SetWindowPos(pImpl->hwnd, nullptr, x, y, 0, 0, SWP_NOZORDER | SWP_NOSIZE);
656
- #elif defined(__APPLE__)
657
- if (pImpl->nativeWindow) {
658
- NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
659
- [nswin setFrameOrigin:NSMakePoint(x, y)];
660
- }
661
- #else
662
- if (pImpl->nativeWindow) {
663
- gtk_window_move(GTK_WINDOW(pImpl->nativeWindow), x, y);
664
- }
665
- #endif
666
- }
667
-
668
- void Window::getPosition(int &x, int &y) const {
669
- x = pImpl->state.x;
670
- y = pImpl->state.y;
671
- }
672
-
673
- void Window::center() {
674
- #ifdef _WIN32
675
- if (pImpl->hwnd) {
676
- RECT rc, screen;
677
- GetWindowRect(pImpl->hwnd, &rc);
678
- SystemParametersInfo(SPI_GETWORKAREA, 0, &screen, 0);
679
- int x = (screen.right - (rc.right - rc.left)) / 2;
680
- int y = (screen.bottom - (rc.bottom - rc.top)) / 2;
681
- SetWindowPos(pImpl->hwnd, nullptr, x, y, 0, 0, SWP_NOZORDER | SWP_NOSIZE);
682
- }
683
- #elif defined(__APPLE__)
684
- if (pImpl->nativeWindow) {
685
- NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
686
- [nswin center];
687
- }
688
- #else
689
- if (pImpl->nativeWindow) {
690
- gtk_window_set_position(GTK_WINDOW(pImpl->nativeWindow),
691
- GTK_WIN_POS_CENTER);
692
- }
693
- #endif
694
- }
695
-
696
- void Window::setFullscreen(bool enabled) {
697
- bool wasFullscreen = pImpl->state.isFullscreen;
698
- pImpl->config.fullscreen = enabled;
699
- pImpl->state.isFullscreen = enabled;
700
- pImpl->state.isHidden = false;
701
-
702
- #ifdef _WIN32
703
- if (pImpl->hwnd) {
704
- if (enabled) {
705
- SetWindowLongPtr(pImpl->hwnd, GWL_STYLE,
706
- GetWindowLong(pImpl->hwnd, GWL_STYLE) &
707
- ~WS_OVERLAPPEDWINDOW);
708
- SetWindowPos(pImpl->hwnd, HWND_TOP, 0, 0, GetSystemMetrics(SM_CXSCREEN),
709
- GetSystemMetrics(SM_CYSCREEN), SWP_SHOWWINDOW);
710
- } else {
711
- SetWindowLongPtr(pImpl->hwnd, GWL_STYLE,
712
- GetWindowLong(pImpl->hwnd, GWL_STYLE) |
713
- WS_OVERLAPPEDWINDOW);
714
- SetWindowPos(pImpl->hwnd, nullptr, 0, 0, 0, 0,
715
- SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED);
716
- }
717
- }
718
- #elif defined(__APPLE__)
719
- if (pImpl->nativeWindow) {
720
- NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
721
- if (enabled) {
722
- [nswin toggleFullScreen:nil];
723
- }
724
- }
725
- #else
726
- if (pImpl->nativeWindow) {
727
- if (enabled) {
728
- gtk_window_fullscreen(GTK_WINDOW(pImpl->nativeWindow));
729
- } else {
730
- gtk_window_unfullscreen(GTK_WINDOW(pImpl->nativeWindow));
731
- }
732
- }
733
- #endif
734
-
735
- if (wasFullscreen != enabled) {
736
- for (auto &cb : pImpl->stateCallbacks)
737
- cb(pImpl->state);
738
- }
739
- }
740
-
741
- bool Window::isFullscreen() const { return pImpl->state.isFullscreen; }
742
-
743
- void Window::minimize() {
744
- bool wasMinimized = pImpl->state.isMinimized;
745
- pImpl->state.isMinimized = true;
746
- pImpl->state.isHidden = false;
747
-
748
- #ifdef _WIN32
749
- if (pImpl->hwnd)
750
- ShowWindow(pImpl->hwnd, SW_MINIMIZE);
751
- #elif defined(__APPLE__)
752
- if (pImpl->nativeWindow) {
753
- NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
754
- [nswin miniaturize:nil];
755
- }
756
- #else
757
- if (pImpl->nativeWindow) {
758
- gtk_window_iconify(GTK_WINDOW(pImpl->nativeWindow));
759
- }
760
- #endif
761
-
762
- if (!wasMinimized) {
763
- for (auto &cb : pImpl->stateCallbacks)
764
- cb(pImpl->state);
765
- }
766
- }
767
-
768
- void Window::maximize() {
769
- bool wasMaximized = pImpl->state.isMaximized;
770
- pImpl->state.isMaximized = true;
771
- pImpl->state.isHidden = false;
772
-
773
- #ifdef _WIN32
774
- if (pImpl->hwnd)
775
- ShowWindow(pImpl->hwnd, SW_MAXIMIZE);
776
- #elif defined(__APPLE__)
777
- if (pImpl->nativeWindow) {
778
- NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
779
- [nswin zoom:nil];
780
- }
781
- #else
782
- if (pImpl->nativeWindow) {
783
- gtk_window_maximize(GTK_WINDOW(pImpl->nativeWindow));
784
- }
785
- #endif
786
-
787
- if (!wasMaximized) {
788
- for (auto &cb : pImpl->stateCallbacks)
789
- cb(pImpl->state);
790
- }
791
- }
792
-
793
- void Window::restore() {
794
- bool wasMinimized = pImpl->state.isMinimized;
795
- bool wasMaximized = pImpl->state.isMaximized;
796
- pImpl->state.isMinimized = false;
797
- pImpl->state.isMaximized = false;
798
- pImpl->state.isHidden = false;
799
-
800
- #ifdef _WIN32
801
- if (pImpl->hwnd)
802
- ShowWindow(pImpl->hwnd, SW_RESTORE);
803
- #elif defined(__APPLE__)
804
- if (pImpl->nativeWindow) {
805
- NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
806
- [nswin deminiaturize:nil];
807
- }
808
- #else
809
- if (pImpl->nativeWindow) {
810
- gtk_window_unmaximize(GTK_WINDOW(pImpl->nativeWindow));
811
- gtk_window_deiconify(GTK_WINDOW(pImpl->nativeWindow));
812
- }
813
- #endif
814
-
815
- if (wasMinimized || wasMaximized) {
816
- for (auto &cb : pImpl->stateCallbacks)
817
- cb(pImpl->state);
818
- }
819
- }
820
-
821
- bool Window::isMaximized() const { return pImpl->state.isMaximized; }
822
-
823
- bool Window::isMinimized() const { return pImpl->state.isMinimized; }
824
-
825
- void Window::show() {
826
- bool wasHidden = pImpl->state.isHidden;
827
- pImpl->state.isVisible = true;
828
- pImpl->state.isHidden = false;
829
-
830
- #ifdef _WIN32
831
- if (pImpl->hwnd)
832
- ShowWindow(pImpl->hwnd, SW_SHOW);
833
- #elif defined(__APPLE__)
834
- if (pImpl->nativeWindow) {
835
- NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
836
- [nswin orderFront:nil];
837
- }
838
- #else
839
- if (pImpl->nativeWindow) {
840
- gtk_widget_show(GTK_WIDGET(pImpl->nativeWindow));
841
- }
842
- #endif
843
-
844
- // Fire state change event if hidden state changed
845
- if (wasHidden) {
846
- for (auto &cb : pImpl->stateCallbacks)
847
- cb(pImpl->state);
848
- }
849
- }
850
-
851
- void Window::hide() {
852
- bool wasVisible = pImpl->state.isVisible;
853
- pImpl->state.isVisible = false;
854
- pImpl->state.isHidden = true;
855
-
856
- #ifdef _WIN32
857
- if (pImpl->hwnd)
858
- ShowWindow(pImpl->hwnd, SW_HIDE);
859
- #elif defined(__APPLE__)
860
- if (pImpl->nativeWindow) {
861
- NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
862
- [nswin orderOut:nil];
863
- }
864
- #else
865
- if (pImpl->nativeWindow) {
866
- gtk_widget_hide(GTK_WIDGET(pImpl->nativeWindow));
867
- }
868
- #endif
869
-
870
- // Fire state change event if visibility changed
871
- if (wasVisible) {
872
- for (auto &cb : pImpl->stateCallbacks)
873
- cb(pImpl->state);
874
- }
875
- }
876
-
877
- bool Window::isVisible() const { return pImpl->state.isVisible; }
878
-
879
- bool Window::isHidden() const { return pImpl->state.isHidden; }
880
-
881
- void Window::focus() {
882
- #ifdef _WIN32
883
- if (pImpl->hwnd)
884
- SetFocus(pImpl->hwnd);
885
- #elif defined(__APPLE__)
886
- if (pImpl->nativeWindow) {
887
- NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
888
- [nswin makeKeyAndOrderFront:nil];
889
- }
890
- #else
891
- if (pImpl->nativeWindow) {
892
- gtk_window_present(GTK_WINDOW(pImpl->nativeWindow));
893
- }
894
- #endif
895
- }
896
-
897
- bool Window::isFocused() const { return pImpl->state.isFocused; }
898
-
899
- void Window::setAlwaysOnTop(bool enabled) {
900
- pImpl->config.alwaysOnTop = enabled;
901
- #ifdef _WIN32
902
- if (pImpl->hwnd) {
903
- SetWindowPos(pImpl->hwnd, enabled ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0,
904
- 0, SWP_NOMOVE | SWP_NOSIZE);
905
- }
906
- #elif defined(__APPLE__)
907
- if (pImpl->nativeWindow) {
908
- NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
909
- [nswin setLevel:enabled ? NSFloatingWindowLevel : NSNormalWindowLevel];
910
- }
911
- #else
912
- if (pImpl->nativeWindow) {
913
- gtk_window_set_keep_above(GTK_WINDOW(pImpl->nativeWindow), enabled);
914
- }
915
- #endif
916
- }
917
-
918
- void Window::setResizable(bool enabled) {
919
- pImpl->config.resizable = enabled;
920
- #ifdef _WIN32
921
- if (pImpl->hwnd) {
922
- SetWindowLongPtr(
923
- pImpl->hwnd, GWL_STYLE,
924
- enabled ? GetWindowLong(pImpl->hwnd, GWL_STYLE) | WS_THICKFRAME
925
- : GetWindowLong(pImpl->hwnd, GWL_STYLE) & ~WS_THICKFRAME);
926
- }
927
- #elif defined(__APPLE__)
928
- // Handled in create
929
- #else
930
- if (pImpl->nativeWindow) {
931
- gtk_window_set_resizable(GTK_WINDOW(pImpl->nativeWindow), enabled);
932
- }
933
- #endif
934
- }
935
-
936
- void Window::setDecorations(bool enabled) {
937
- #ifdef _WIN32
938
- if (pImpl->hwnd) {
939
- SetWindowLongPtr(pImpl->hwnd, GWL_STYLE,
940
- enabled
941
- ? GetWindowLong(pImpl->hwnd, GWL_STYLE) | WS_CAPTION
942
- : GetWindowLong(pImpl->hwnd, GWL_STYLE) & ~WS_CAPTION);
943
- }
944
- #elif defined(__APPLE__)
945
- if (pImpl->nativeWindow) {
946
- NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
947
- [nswin setStyleMask:enabled ? [nswin styleMask] | NSWindowStyleMaskTitled
948
- : [nswin styleMask] & ~NSWindowStyleMaskTitled];
949
- }
950
- #else
951
- if (pImpl->nativeWindow) {
952
- // GTK handles this differently
953
- }
954
- #endif
955
- }
956
-
957
- void Window::setSkipTaskbar(bool enabled) {
958
- #ifdef _WIN32
959
- if (pImpl->hwnd) {
960
- SetWindowLongPtr(
961
- pImpl->hwnd, GWL_EXSTYLE,
962
- enabled ? GetWindowLong(pImpl->hwnd, GWL_EXSTYLE) | WS_EX_TOOLWINDOW
963
- : GetWindowLong(pImpl->hwnd, GWL_EXSTYLE) & ~WS_EX_TOOLWINDOW);
964
- }
965
- #endif
966
- }
967
-
968
- void Window::setOpacity(double opacity) {
969
- pImpl->config.opacity = opacity;
970
- #ifdef _WIN32
971
- if (pImpl->hwnd) {
972
- BYTE alpha = static_cast<BYTE>(opacity * 255);
973
- SetWindowLongPtr(pImpl->hwnd, GWL_EXSTYLE,
974
- GetWindowLong(pImpl->hwnd, GWL_EXSTYLE) | WS_EX_LAYERED);
975
- SetLayeredWindowAttributes(pImpl->hwnd, 0, alpha, LWA_ALPHA);
976
- }
977
- #endif
978
- }
979
-
980
- void Window::setIconFromMemory(const unsigned char *data, size_t size) {
981
- #ifdef _WIN32
982
- if (pImpl->hwnd) {
983
- int width, height, channels;
984
- unsigned char *pixels =
985
- stbi_load_from_memory(data, (int)size, &width, &height, &channels, 4);
986
- if (pixels) {
987
- BITMAPINFO bmi = {};
988
- bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
989
- bmi.bmiHeader.biWidth = width;
990
- bmi.bmiHeader.biHeight = -height;
991
- bmi.bmiHeader.biPlanes = 1;
992
- bmi.bmiHeader.biBitCount = 32;
993
- bmi.bmiHeader.biCompression = BI_RGB;
994
-
995
- void *bits = nullptr;
996
- HBITMAP hbm =
997
- CreateDIBSection(nullptr, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0);
998
- if (hbm && bits) {
999
- // Convert RGBA to BGRA
1000
- for (int i = 0; i < width * height; ++i) {
1001
- ((uint8_t *)bits)[i * 4 + 0] = pixels[i * 4 + 2];
1002
- ((uint8_t *)bits)[i * 4 + 1] = pixels[i * 4 + 1];
1003
- ((uint8_t *)bits)[i * 4 + 2] = pixels[i * 4 + 0];
1004
- ((uint8_t *)bits)[i * 4 + 3] = pixels[i * 4 + 3];
1005
- }
1006
-
1007
- ICONINFO ii = {};
1008
- ii.fIcon = TRUE;
1009
- ii.hbmColor = hbm;
1010
- ii.hbmMask = hbm;
1011
-
1012
- HICON hIcon = CreateIconIndirect(&ii);
1013
- if (hIcon) {
1014
- SendMessage(pImpl->hwnd, WM_SETICON, ICON_SMALL, (LPARAM)hIcon);
1015
- SendMessage(pImpl->hwnd, WM_SETICON, ICON_BIG, (LPARAM)hIcon);
1016
- }
1017
- DeleteObject(hbm);
1018
- }
1019
- stbi_image_free(pixels);
1020
- }
1021
- }
1022
- #endif
1023
- }
1024
-
1025
- WindowState Window::getState() const { return pImpl->state; }
1026
-
1027
- void Window::close() {
1028
- #ifdef _WIN32
1029
- if (pImpl->hwnd)
1030
- PostMessage(pImpl->hwnd, WM_CLOSE, 0, 0);
1031
- #elif defined(__APPLE__)
1032
- if (pImpl->nativeWindow) {
1033
- NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
1034
- [nswin close];
1035
- }
1036
- #else
1037
- if (pImpl->nativeWindow) {
1038
- gtk_widget_destroy(GTK_WIDGET(pImpl->nativeWindow));
1039
- }
1040
- #endif
1041
- }
1042
-
1043
- void Window::onMove(MoveCallback callback) {
1044
- pImpl->moveCallbacks.push_back(callback);
1045
- }
1046
-
1047
- void Window::onResize(ResizeCallback callback) {
1048
- pImpl->resizeCallbacks.push_back(callback);
1049
- }
1050
-
1051
- void Window::onClose(CloseCallback callback) {
1052
- pImpl->closeCallbacks.push_back(callback);
1053
- }
1054
-
1055
- void Window::onFocus(FocusCallback callback) {
1056
- pImpl->focusCallbacks.push_back(callback);
1057
- }
1058
-
1059
- void Window::onStateChange(StateCallback callback) {
1060
- pImpl->stateCallbacks.push_back(callback);
1061
- }
1062
-
1063
- void *Window::nativeHandle() const { return pImpl->nativeWindow; }
1064
-
1065
- // WebView-specific implementations
1066
- Window Window::create(void *windowHandle, const WindowConfig &config) {
1067
- Window win;
1068
- win.pImpl->config = config;
1069
-
1070
- // Pure native file-drop mode: when native FileDrop is enabled,
1071
- // fully disable browser/WebView drag-drop handling.
1072
- if (win.pImpl->config.fileDrop) {
1073
- win.pImpl->config.disableWebviewDragDrop = true;
1074
- }
1075
-
1076
- #ifdef _WIN32
1077
- HWND hwnd = static_cast<HWND>(windowHandle);
1078
-
1079
- // Create WebView2 environment and controller
1080
- auto pImpl = win.pImpl;
1081
- CreateCoreWebView2EnvironmentWithOptions(
1082
- nullptr, nullptr, nullptr,
1083
- Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
1084
- [hwnd, pImpl](HRESULT result,
1085
- ICoreWebView2Environment *env) -> HRESULT {
1086
- if (FAILED(result) || !env)
1087
- return result;
1088
- env->CreateCoreWebView2Controller(
1089
- hwnd,
1090
- Callback<
1091
- ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>(
1092
- [pImpl](HRESULT result,
1093
- ICoreWebView2Controller *controller) -> HRESULT {
1094
- (void)result; // Suppress unused warning
1095
- if (controller != nullptr) {
1096
- pImpl->controller = controller;
1097
- controller->get_CoreWebView2(&pImpl->webview);
1098
-
1099
- RECT bounds;
1100
- HWND parentHwnd;
1101
- controller->get_ParentWindow(&parentHwnd);
1102
- GetClientRect(parentHwnd, &bounds);
1103
- controller->put_Bounds(bounds);
1104
- Window::Impl::embeddedWebviewByParent[parentHwnd] =
1105
- pImpl.get();
1106
-
1107
- // Disable WebView2's internal drop handling so the OS
1108
- // drop message (WM_DROPFILES) propagates to our WndProc.
1109
- // When AllowExternalDrop is TRUE, WebView2 consumes the
1110
- // drop event itself and WM_DROPFILES never fires.
1111
- // Visual drag feedback (dropzone-active CSS class) is
1112
- // still handled by the injected JS via dragenter/dragover.
341
+ #elif defined(__APPLE__)
342
+ WKWebView *wkWebView = nullptr;
343
+ #else
344
+ WebKitWebView *gtkWebView = nullptr;
345
+ #endif
346
+ };
347
+
348
+ #ifdef _WIN32
349
+
350
+ #endif // _WIN32
351
+
352
+ Window::Window() : pImpl(std::shared_ptr<Impl>(new Impl())) {}
353
+
354
+ #ifdef _WIN32
355
+ std::map<HWND, Window::Impl *> Window::Impl::embeddedWebviewByParent;
356
+ #endif
357
+
358
+ Window::~Window() = default;
359
+
360
+ Window::Window(Window &&other) noexcept = default;
361
+ Window &Window::operator=(Window &&other) noexcept = default;
362
+
363
+ Window Window::create(const WindowConfig &config) {
364
+ Window w;
365
+ w.pImpl->config = config;
366
+
367
+ #ifdef _WIN32
368
+ WNDCLASSEXW wc = {};
369
+ wc.cbSize = sizeof(WNDCLASSEXW);
370
+ wc.lpfnWndProc = Impl::wndProc;
371
+ wc.hInstance = GetModuleHandle(nullptr);
372
+ wc.lpszClassName = L"PLUSUI_WINDOW";
373
+ wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
374
+ wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
375
+ RegisterClassExW(&wc);
376
+
377
+ DWORD style = 0;
378
+ DWORD exStyle = 0;
379
+
380
+ if (config.decorations) {
381
+ style = WS_OVERLAPPEDWINDOW;
382
+ if (!config.resizable)
383
+ style &= ~WS_THICKFRAME;
384
+ if (!config.minimizable)
385
+ style &= ~WS_MINIMIZEBOX;
386
+ if (!config.closable)
387
+ style &= ~WS_SYSMENU;
388
+ } else {
389
+ // Frameless window
390
+ style = WS_POPUP;
391
+ if (config.resizable)
392
+ style |= WS_THICKFRAME;
393
+ }
394
+
395
+ if (config.transparent) {
396
+ exStyle = WS_EX_LAYERED;
397
+ }
398
+
399
+ if (config.skipTaskbar) {
400
+ exStyle |= WS_EX_TOOLWINDOW;
401
+ }
402
+
403
+ std::wstring wideTitle(config.title.begin(), config.title.end());
404
+ w.pImpl->hwnd = CreateWindowExW(
405
+ exStyle, L"PLUSUI_WINDOW", wideTitle.c_str(), style,
406
+ config.x >= 0 ? config.x : CW_USEDEFAULT,
407
+ config.y >= 0 ? config.y : CW_USEDEFAULT, config.width, config.height,
408
+ nullptr, nullptr, GetModuleHandle(nullptr), w.pImpl.get());
409
+
410
+ w.pImpl->nativeWindow = (void *)w.pImpl->hwnd;
411
+ w.pImpl->state.width = config.width;
412
+ w.pImpl->state.height = config.height;
413
+
414
+ if (config.center) {
415
+ RECT screen;
416
+ SystemParametersInfo(SPI_GETWORKAREA, 0, &screen, 0);
417
+ int x = (screen.right - config.width) / 2;
418
+ int y = (screen.bottom - config.height) / 2;
419
+ SetWindowPos(w.pImpl->hwnd, nullptr, x, y, 0, 0, SWP_NOZORDER | SWP_NOSIZE);
420
+ }
421
+
422
+ if (config.alwaysOnTop) {
423
+ SetWindowPos(w.pImpl->hwnd, HWND_TOPMOST, 0, 0, 0, 0,
424
+ SWP_NOMOVE | SWP_NOSIZE);
425
+ }
426
+
427
+ #elif defined(__APPLE__)
428
+ NSWindow *nswin = [[NSWindow alloc]
429
+ initWithContentRect:NSMakeRect(config.x >= 0 ? config.x : 100,
430
+ config.y >= 0 ? config.y : 100,
431
+ config.width, config.height)
432
+ styleMask:(config.resizable ? NSWindowStyleMaskResizable : 0) |
433
+ NSWindowStyleMaskTitled | NSWindowStyleMaskClosable |
434
+ (config.minimizable ? NSWindowStyleMaskMiniaturizable
435
+ : 0)backing:NSBackingStoreBuffered
436
+ defer:NO];
437
+
438
+ [nswin setTitle:[NSString stringWithUTF8String:config.title.c_str()]];
439
+ [nswin setReleasedWhenClosed:NO];
440
+
441
+ if (config.center) {
442
+ [nswin center];
443
+ }
444
+
445
+ if (config.alwaysOnTop) {
446
+ [nswin setLevel:NSFloatingWindowLevel];
447
+ }
448
+
449
+ w.pImpl->nativeWindow = (__bridge void *)nswin;
450
+
451
+ #else
452
+ // Force the X11 GDK backend so window positioning and XGrabKey shortcuts work.
453
+ // On a Wayland session this uses XWayland transparently.
454
+ // Must be set before gtk_init and must override any compositor-set GDK_BACKEND.
455
+ setenv("GDK_BACKEND", "x11", 1);
456
+ gtk_init_check(0, nullptr);
457
+
458
+ GtkWindow *gtkwin = GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL));
459
+ gtk_window_set_title(gtkwin, config.title.c_str());
460
+ gtk_window_set_default_size(gtkwin, config.width, config.height);
461
+
462
+ if (config.x >= 0 && config.y >= 0) {
463
+ gtk_window_move(gtkwin, config.x, config.y);
464
+ } else if (config.center) {
465
+ gtk_window_set_position(gtkwin, GTK_WIN_POS_CENTER);
466
+ }
467
+
468
+ if (!config.resizable) {
469
+ gtk_window_set_resizable(gtkwin, FALSE);
470
+ }
471
+
472
+ if (config.alwaysOnTop) {
473
+ gtk_window_set_keep_above(gtkwin, TRUE);
474
+ }
475
+
476
+ w.pImpl->nativeWindow = (void *)gtkwin;
477
+ #endif
478
+
479
+ // Initialize tray manager for native windows
480
+ w.pImpl->trayManager = std::make_unique<TrayManager>();
481
+ #ifdef _WIN32
482
+ if (w.pImpl->hwnd) {
483
+ w.pImpl->trayManager->setWindowHandle((void *)w.pImpl->hwnd);
484
+ }
485
+ #elif defined(__APPLE__)
486
+ w.pImpl->trayManager->setWindowHandle(w.pImpl->nativeWindow);
487
+ #else
488
+ w.pImpl->trayManager->setWindowHandle(w.pImpl->nativeWindow);
489
+ #endif
490
+
491
+ return w;
492
+ }
493
+
494
+ void Window::setTitle(const std::string &title) {
495
+ pImpl->config.title = title;
496
+ #ifdef _WIN32
497
+ if (pImpl->hwnd) {
498
+ std::wstring wideTitle(title.begin(), title.end());
499
+ SetWindowTextW(pImpl->hwnd, wideTitle.c_str());
500
+ }
501
+ #elif defined(__APPLE__)
502
+ if (pImpl->nativeWindow) {
503
+ NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
504
+ [nswin setTitle:[NSString stringWithUTF8String:title.c_str()]];
505
+ }
506
+ #else
507
+ if (pImpl->nativeWindow) {
508
+ gtk_window_set_title(GTK_WINDOW(pImpl->nativeWindow), title.c_str());
509
+ }
510
+ #endif
511
+ }
512
+
513
+ std::string Window::getTitle() const {
514
+ if (!pImpl->currentTitle.empty())
515
+ return pImpl->currentTitle;
516
+ return pImpl->config.title;
517
+ }
518
+
519
+ void Window::setSize(int width, int height) {
520
+ pImpl->config.width = width;
521
+ pImpl->config.height = height;
522
+ #ifdef _WIN32
523
+ if (pImpl->hwnd)
524
+ SetWindowPos(pImpl->hwnd, nullptr, 0, 0, width, height,
525
+ SWP_NOZORDER | SWP_NOMOVE);
526
+ #elif defined(__APPLE__)
527
+ if (pImpl->nativeWindow) {
528
+ NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
529
+ [nswin setContentSize:NSMakeSize(width, height)];
530
+ }
531
+ #else
532
+ if (pImpl->nativeWindow) {
533
+ gtk_window_resize(GTK_WINDOW(pImpl->nativeWindow), width, height);
534
+ }
535
+ #endif
536
+ }
537
+
538
+ void Window::getSize(int &width, int &height) const {
539
+ width = pImpl->state.width;
540
+ height = pImpl->state.height;
541
+ }
542
+
543
+ void Window::setMinSize(int minWidth, int minHeight) {
544
+ pImpl->config.minWidth = minWidth;
545
+ pImpl->config.minHeight = minHeight;
546
+ #ifdef _WIN32
547
+ if (pImpl->hwnd) {
548
+ SetWindowLongPtr(pImpl->hwnd, GWL_STYLE,
549
+ GetWindowLong(pImpl->hwnd, GWL_STYLE) | WS_THICKFRAME);
550
+ }
551
+ #endif
552
+ }
553
+
554
+ void Window::setMaxSize(int maxWidth, int maxHeight) {
555
+ pImpl->config.maxWidth = maxWidth;
556
+ pImpl->config.maxHeight = maxHeight;
557
+ }
558
+
559
+ void Window::setPosition(int x, int y) {
560
+ pImpl->config.x = x;
561
+ pImpl->config.y = y;
562
+ #ifdef _WIN32
563
+ if (pImpl->hwnd)
564
+ SetWindowPos(pImpl->hwnd, nullptr, x, y, 0, 0, SWP_NOZORDER | SWP_NOSIZE);
565
+ #elif defined(__APPLE__)
566
+ if (pImpl->nativeWindow) {
567
+ NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
568
+ [nswin setFrameOrigin:NSMakePoint(x, y)];
569
+ }
570
+ #else
571
+ if (pImpl->nativeWindow) {
572
+ gtk_window_move(GTK_WINDOW(pImpl->nativeWindow), x, y);
573
+ }
574
+ #endif
575
+ }
576
+
577
+ void Window::getPosition(int &x, int &y) const {
578
+ x = pImpl->state.x;
579
+ y = pImpl->state.y;
580
+ }
581
+
582
+ void Window::center() {
583
+ #ifdef _WIN32
584
+ if (pImpl->hwnd) {
585
+ RECT rc, screen;
586
+ GetWindowRect(pImpl->hwnd, &rc);
587
+ SystemParametersInfo(SPI_GETWORKAREA, 0, &screen, 0);
588
+ int x = (screen.right - (rc.right - rc.left)) / 2;
589
+ int y = (screen.bottom - (rc.bottom - rc.top)) / 2;
590
+ SetWindowPos(pImpl->hwnd, nullptr, x, y, 0, 0, SWP_NOZORDER | SWP_NOSIZE);
591
+ }
592
+ #elif defined(__APPLE__)
593
+ if (pImpl->nativeWindow) {
594
+ NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
595
+ [nswin center];
596
+ }
597
+ #else
598
+ // gtk_window_set_position(GTK_WIN_POS_CENTER) is a map-time hint only —
599
+ // it does nothing on an already-visible window. Use GDK geometry to
600
+ // calculate the centre and move the window explicitly, matching Windows.
601
+ {
602
+ GtkWindow *gtkwin = nullptr;
603
+ if (pImpl->nativeWindow)
604
+ gtkwin = GTK_WINDOW(pImpl->nativeWindow);
605
+ else if (pImpl->window && pImpl->window->pImpl->nativeWindow)
606
+ gtkwin = GTK_WINDOW(pImpl->window->pImpl->nativeWindow);
607
+ if (gtkwin) {
608
+ GdkDisplay *display = gdk_display_get_default();
609
+ GdkMonitor *monitor = gdk_display_get_primary_monitor(display);
610
+ if (!monitor) monitor = gdk_display_get_monitor(display, 0);
611
+ if (monitor) {
612
+ GdkRectangle geo;
613
+ gdk_monitor_get_geometry(monitor, &geo);
614
+ int w = 0, h = 0;
615
+ gtk_window_get_size(gtkwin, &w, &h);
616
+ gtk_window_move(gtkwin,
617
+ geo.x + (geo.width - w) / 2,
618
+ geo.y + (geo.height - h) / 2);
619
+ }
620
+ }
621
+ }
622
+ #endif
623
+ }
624
+
625
+ void Window::setFullscreen(bool enabled) {
626
+ bool wasFullscreen = pImpl->state.isFullscreen;
627
+ pImpl->config.fullscreen = enabled;
628
+ pImpl->state.isFullscreen = enabled;
629
+ pImpl->state.isHidden = false;
630
+
631
+ #ifdef _WIN32
632
+ if (pImpl->hwnd) {
633
+ if (enabled) {
634
+ SetWindowLongPtr(pImpl->hwnd, GWL_STYLE,
635
+ GetWindowLong(pImpl->hwnd, GWL_STYLE) &
636
+ ~WS_OVERLAPPEDWINDOW);
637
+ SetWindowPos(pImpl->hwnd, HWND_TOP, 0, 0, GetSystemMetrics(SM_CXSCREEN),
638
+ GetSystemMetrics(SM_CYSCREEN), SWP_SHOWWINDOW);
639
+ } else {
640
+ SetWindowLongPtr(pImpl->hwnd, GWL_STYLE,
641
+ GetWindowLong(pImpl->hwnd, GWL_STYLE) |
642
+ WS_OVERLAPPEDWINDOW);
643
+ SetWindowPos(pImpl->hwnd, nullptr, 0, 0, 0, 0,
644
+ SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED);
645
+ }
646
+ }
647
+ #elif defined(__APPLE__)
648
+ if (pImpl->nativeWindow) {
649
+ NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
650
+ if (enabled) {
651
+ [nswin toggleFullScreen:nil];
652
+ }
653
+ }
654
+ #else
655
+ if (pImpl->nativeWindow) {
656
+ if (enabled) {
657
+ gtk_window_fullscreen(GTK_WINDOW(pImpl->nativeWindow));
658
+ } else {
659
+ gtk_window_unfullscreen(GTK_WINDOW(pImpl->nativeWindow));
660
+ }
661
+ }
662
+ #endif
663
+
664
+ if (wasFullscreen != enabled) {
665
+ for (auto &cb : pImpl->stateCallbacks)
666
+ cb(pImpl->state);
667
+ }
668
+ }
669
+
670
+ bool Window::isFullscreen() const { return pImpl->state.isFullscreen; }
671
+
672
+ void Window::minimize() {
673
+ bool wasMinimized = pImpl->state.isMinimized;
674
+ pImpl->state.isMinimized = true;
675
+ pImpl->state.isHidden = false;
676
+
677
+ #ifdef _WIN32
678
+ if (pImpl->hwnd)
679
+ ShowWindow(pImpl->hwnd, SW_MINIMIZE);
680
+ #elif defined(__APPLE__)
681
+ if (pImpl->nativeWindow) {
682
+ NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
683
+ [nswin miniaturize:nil];
684
+ }
685
+ #else
686
+ if (pImpl->nativeWindow) {
687
+ gtk_window_iconify(GTK_WINDOW(pImpl->nativeWindow));
688
+ }
689
+ #endif
690
+
691
+ if (!wasMinimized) {
692
+ for (auto &cb : pImpl->stateCallbacks)
693
+ cb(pImpl->state);
694
+ }
695
+ }
696
+
697
+ void Window::maximize() {
698
+ bool wasMaximized = pImpl->state.isMaximized;
699
+ pImpl->state.isMaximized = true;
700
+ pImpl->state.isHidden = false;
701
+
702
+ #ifdef _WIN32
703
+ if (pImpl->hwnd)
704
+ ShowWindow(pImpl->hwnd, SW_MAXIMIZE);
705
+ #elif defined(__APPLE__)
706
+ if (pImpl->nativeWindow) {
707
+ NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
708
+ [nswin zoom:nil];
709
+ }
710
+ #else
711
+ if (pImpl->nativeWindow) {
712
+ gtk_window_maximize(GTK_WINDOW(pImpl->nativeWindow));
713
+ }
714
+ #endif
715
+
716
+ if (!wasMaximized) {
717
+ for (auto &cb : pImpl->stateCallbacks)
718
+ cb(pImpl->state);
719
+ }
720
+ }
721
+
722
+ void Window::restore() {
723
+ bool wasMinimized = pImpl->state.isMinimized;
724
+ bool wasMaximized = pImpl->state.isMaximized;
725
+ pImpl->state.isMinimized = false;
726
+ pImpl->state.isMaximized = false;
727
+ pImpl->state.isHidden = false;
728
+
729
+ #ifdef _WIN32
730
+ if (pImpl->hwnd)
731
+ ShowWindow(pImpl->hwnd, SW_RESTORE);
732
+ #elif defined(__APPLE__)
733
+ if (pImpl->nativeWindow) {
734
+ NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
735
+ [nswin deminiaturize:nil];
736
+ }
737
+ #else
738
+ if (pImpl->nativeWindow) {
739
+ gtk_window_unmaximize(GTK_WINDOW(pImpl->nativeWindow));
740
+ gtk_window_deiconify(GTK_WINDOW(pImpl->nativeWindow));
741
+ }
742
+ #endif
743
+
744
+ if (wasMinimized || wasMaximized) {
745
+ for (auto &cb : pImpl->stateCallbacks)
746
+ cb(pImpl->state);
747
+ }
748
+ }
749
+
750
+ bool Window::isMaximized() const { return pImpl->state.isMaximized; }
751
+
752
+ bool Window::isMinimized() const { return pImpl->state.isMinimized; }
753
+
754
+ void Window::show() {
755
+ bool wasHidden = pImpl->state.isHidden;
756
+ pImpl->state.isVisible = true;
757
+ pImpl->state.isHidden = false;
758
+
759
+ #ifdef _WIN32
760
+ if (pImpl->hwnd)
761
+ ShowWindow(pImpl->hwnd, SW_SHOW);
762
+ else if (pImpl->window)
763
+ pImpl->window->show();
764
+ #elif defined(__APPLE__)
765
+ if (pImpl->nativeWindow) {
766
+ NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
767
+ [nswin orderFront:nil];
768
+ } else if (pImpl->window) {
769
+ pImpl->window->show();
770
+ }
771
+ #else
772
+ if (pImpl->nativeWindow) {
773
+ // show_all reveals the window and all child widgets (e.g. the webview)
774
+ gtk_widget_show_all(GTK_WIDGET(pImpl->nativeWindow));
775
+ } else if (pImpl->window) {
776
+ pImpl->window->show();
777
+ }
778
+ #endif
779
+
780
+ // Fire state change event if hidden state changed
781
+ if (wasHidden) {
782
+ for (auto &cb : pImpl->stateCallbacks)
783
+ cb(pImpl->state);
784
+ }
785
+ }
786
+
787
+ void Window::hide() {
788
+ bool wasVisible = pImpl->state.isVisible;
789
+ pImpl->state.isVisible = false;
790
+ pImpl->state.isHidden = true;
791
+
792
+ #ifdef _WIN32
793
+ if (pImpl->hwnd)
794
+ ShowWindow(pImpl->hwnd, SW_HIDE);
795
+ else if (pImpl->window)
796
+ pImpl->window->hide();
797
+ #elif defined(__APPLE__)
798
+ if (pImpl->nativeWindow) {
799
+ NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
800
+ [nswin orderOut:nil];
801
+ } else if (pImpl->window) {
802
+ pImpl->window->hide();
803
+ }
804
+ #else
805
+ if (pImpl->nativeWindow) {
806
+ gtk_widget_hide(GTK_WIDGET(pImpl->nativeWindow));
807
+ } else if (pImpl->window) {
808
+ pImpl->window->hide();
809
+ }
810
+ #endif
811
+
812
+ // Fire state change event if visibility changed
813
+ if (wasVisible) {
814
+ for (auto &cb : pImpl->stateCallbacks)
815
+ cb(pImpl->state);
816
+ }
817
+ }
818
+
819
+ bool Window::isVisible() const { return pImpl->state.isVisible; }
820
+
821
+ bool Window::isHidden() const { return pImpl->state.isHidden; }
822
+
823
+ void Window::focus() {
824
+ #ifdef _WIN32
825
+ if (pImpl->hwnd)
826
+ SetFocus(pImpl->hwnd);
827
+ #elif defined(__APPLE__)
828
+ if (pImpl->nativeWindow) {
829
+ NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
830
+ [nswin makeKeyAndOrderFront:nil];
831
+ }
832
+ #else
833
+ if (pImpl->nativeWindow) {
834
+ gtk_window_present(GTK_WINDOW(pImpl->nativeWindow));
835
+ }
836
+ #endif
837
+ }
838
+
839
+ bool Window::isFocused() const { return pImpl->state.isFocused; }
840
+
841
+ void Window::setAlwaysOnTop(bool enabled) {
842
+ pImpl->config.alwaysOnTop = enabled;
843
+ #ifdef _WIN32
844
+ if (pImpl->hwnd) {
845
+ SetWindowPos(pImpl->hwnd, enabled ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0,
846
+ 0, SWP_NOMOVE | SWP_NOSIZE);
847
+ }
848
+ #elif defined(__APPLE__)
849
+ if (pImpl->nativeWindow) {
850
+ NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
851
+ [nswin setLevel:enabled ? NSFloatingWindowLevel : NSNormalWindowLevel];
852
+ }
853
+ #else
854
+ if (pImpl->nativeWindow) {
855
+ gtk_window_set_keep_above(GTK_WINDOW(pImpl->nativeWindow), enabled);
856
+ }
857
+ #endif
858
+ }
859
+
860
+ void Window::setResizable(bool enabled) {
861
+ pImpl->config.resizable = enabled;
862
+ #ifdef _WIN32
863
+ if (pImpl->hwnd) {
864
+ SetWindowLongPtr(
865
+ pImpl->hwnd, GWL_STYLE,
866
+ enabled ? GetWindowLong(pImpl->hwnd, GWL_STYLE) | WS_THICKFRAME
867
+ : GetWindowLong(pImpl->hwnd, GWL_STYLE) & ~WS_THICKFRAME);
868
+ }
869
+ #elif defined(__APPLE__)
870
+ // Handled in create
871
+ #else
872
+ if (pImpl->nativeWindow) {
873
+ gtk_window_set_resizable(GTK_WINDOW(pImpl->nativeWindow), enabled);
874
+ }
875
+ #endif
876
+ }
877
+
878
+ void Window::setDecorations(bool enabled) {
879
+ #ifdef _WIN32
880
+ if (pImpl->hwnd) {
881
+ SetWindowLongPtr(pImpl->hwnd, GWL_STYLE,
882
+ enabled
883
+ ? GetWindowLong(pImpl->hwnd, GWL_STYLE) | WS_CAPTION
884
+ : GetWindowLong(pImpl->hwnd, GWL_STYLE) & ~WS_CAPTION);
885
+ }
886
+ #elif defined(__APPLE__)
887
+ if (pImpl->nativeWindow) {
888
+ NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
889
+ [nswin setStyleMask:enabled ? [nswin styleMask] | NSWindowStyleMaskTitled
890
+ : [nswin styleMask] & ~NSWindowStyleMaskTitled];
891
+ }
892
+ #else
893
+ if (pImpl->nativeWindow) {
894
+ // GTK handles this differently
895
+ }
896
+ #endif
897
+ }
898
+
899
+ void Window::setSkipTaskbar(bool enabled) {
900
+ #ifdef _WIN32
901
+ if (pImpl->hwnd) {
902
+ SetWindowLongPtr(
903
+ pImpl->hwnd, GWL_EXSTYLE,
904
+ enabled ? GetWindowLong(pImpl->hwnd, GWL_EXSTYLE) | WS_EX_TOOLWINDOW
905
+ : GetWindowLong(pImpl->hwnd, GWL_EXSTYLE) & ~WS_EX_TOOLWINDOW);
906
+ }
907
+ #endif
908
+ }
909
+
910
+ void Window::setOpacity(double opacity) {
911
+ pImpl->config.opacity = opacity;
912
+ #ifdef _WIN32
913
+ if (pImpl->hwnd) {
914
+ BYTE alpha = static_cast<BYTE>(opacity * 255);
915
+ SetWindowLongPtr(pImpl->hwnd, GWL_EXSTYLE,
916
+ GetWindowLong(pImpl->hwnd, GWL_EXSTYLE) | WS_EX_LAYERED);
917
+ SetLayeredWindowAttributes(pImpl->hwnd, 0, alpha, LWA_ALPHA);
918
+ }
919
+ #endif
920
+ }
921
+
922
+ void Window::setIconFromMemory(const unsigned char *data, size_t size) {
923
+ #ifdef _WIN32
924
+ if (pImpl->hwnd) {
925
+ int width, height, channels;
926
+ unsigned char *pixels =
927
+ stbi_load_from_memory(data, (int)size, &width, &height, &channels, 4);
928
+ if (pixels) {
929
+ BITMAPINFO bmi = {};
930
+ bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
931
+ bmi.bmiHeader.biWidth = width;
932
+ bmi.bmiHeader.biHeight = -height;
933
+ bmi.bmiHeader.biPlanes = 1;
934
+ bmi.bmiHeader.biBitCount = 32;
935
+ bmi.bmiHeader.biCompression = BI_RGB;
936
+
937
+ void *bits = nullptr;
938
+ HBITMAP hbm =
939
+ CreateDIBSection(nullptr, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0);
940
+ if (hbm && bits) {
941
+ // Convert RGBA to BGRA
942
+ for (int i = 0; i < width * height; ++i) {
943
+ ((uint8_t *)bits)[i * 4 + 0] = pixels[i * 4 + 2];
944
+ ((uint8_t *)bits)[i * 4 + 1] = pixels[i * 4 + 1];
945
+ ((uint8_t *)bits)[i * 4 + 2] = pixels[i * 4 + 0];
946
+ ((uint8_t *)bits)[i * 4 + 3] = pixels[i * 4 + 3];
947
+ }
948
+
949
+ ICONINFO ii = {};
950
+ ii.fIcon = TRUE;
951
+ ii.hbmColor = hbm;
952
+ ii.hbmMask = hbm;
953
+
954
+ HICON hIcon = CreateIconIndirect(&ii);
955
+ if (hIcon) {
956
+ SendMessage(pImpl->hwnd, WM_SETICON, ICON_SMALL, (LPARAM)hIcon);
957
+ SendMessage(pImpl->hwnd, WM_SETICON, ICON_BIG, (LPARAM)hIcon);
958
+ }
959
+ DeleteObject(hbm);
960
+ }
961
+ stbi_image_free(pixels);
962
+ }
963
+ }
964
+ #endif
965
+ }
966
+
967
+ WindowState Window::getState() const { return pImpl->state; }
968
+
969
+ void Window::close() {
970
+ #ifdef _WIN32
971
+ if (pImpl->hwnd)
972
+ PostMessage(pImpl->hwnd, WM_CLOSE, 0, 0);
973
+ #elif defined(__APPLE__)
974
+ if (pImpl->nativeWindow) {
975
+ NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
976
+ [nswin close];
977
+ }
978
+ #else
979
+ if (pImpl->nativeWindow) {
980
+ gtk_widget_destroy(GTK_WIDGET(pImpl->nativeWindow));
981
+ }
982
+ #endif
983
+ }
984
+
985
+ void Window::onMove(MoveCallback callback) {
986
+ pImpl->moveCallbacks.push_back(callback);
987
+ }
988
+
989
+ void Window::onResize(ResizeCallback callback) {
990
+ pImpl->resizeCallbacks.push_back(callback);
991
+ }
992
+
993
+ void Window::onClose(CloseCallback callback) {
994
+ pImpl->closeCallbacks.push_back(callback);
995
+ }
996
+
997
+ void Window::onFocus(FocusCallback callback) {
998
+ pImpl->focusCallbacks.push_back(callback);
999
+ }
1000
+
1001
+ void Window::onStateChange(StateCallback callback) {
1002
+ pImpl->stateCallbacks.push_back(callback);
1003
+ }
1004
+
1005
+ void *Window::nativeHandle() const { return pImpl->nativeWindow; }
1006
+ void *Window::nativeWebView() const { return pImpl->nativeWebView; }
1007
+
1008
+ // WebView-specific implementations
1009
+ Window Window::create(void *windowHandle, const WindowConfig &config) {
1010
+ Window win;
1011
+ win.pImpl->config = config;
1012
+
1013
+ // Pure native file-drop mode: when native FileDrop is enabled,
1014
+ // fully disable browser/WebView drag-drop handling.
1015
+ if (win.pImpl->config.fileDrop) {
1016
+ win.pImpl->config.disableWebviewDragDrop = true;
1017
+ }
1018
+
1019
+ #ifdef _WIN32
1020
+ HWND hwnd = static_cast<HWND>(windowHandle);
1021
+
1022
+ // Create WebView2 environment and controller
1023
+ auto pImpl = win.pImpl;
1024
+ CreateCoreWebView2EnvironmentWithOptions(
1025
+ nullptr, nullptr, nullptr,
1026
+ Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
1027
+ [hwnd, pImpl](HRESULT result,
1028
+ ICoreWebView2Environment *env) -> HRESULT {
1029
+ if (FAILED(result) || !env)
1030
+ return result;
1031
+ env->CreateCoreWebView2Controller(
1032
+ hwnd,
1033
+ Callback<
1034
+ ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>(
1035
+ [pImpl](HRESULT result,
1036
+ ICoreWebView2Controller *controller) -> HRESULT {
1037
+ (void)result; // Suppress unused warning
1038
+ if (controller != nullptr) {
1039
+ pImpl->controller = controller;
1040
+ controller->get_CoreWebView2(&pImpl->webview);
1041
+
1042
+ RECT bounds;
1043
+ HWND parentHwnd;
1044
+ controller->get_ParentWindow(&parentHwnd);
1045
+ GetClientRect(parentHwnd, &bounds);
1046
+ controller->put_Bounds(bounds);
1047
+ Window::Impl::embeddedWebviewByParent[parentHwnd] =
1048
+ pImpl.get();
1049
+
1050
+ // AllowExternalDrop must be TRUE so that we can handle
1051
+ // drops via WebView2's DOM events + chrome.webview messaging.
1052
+ // The JS side catches the drop event and sends file data
1053
+ // to native via window.__native_invoke__.
1113
1054
  ComPtr<ICoreWebView2Controller4> controller4;
1114
1055
  if (controller &&
1115
1056
  SUCCEEDED(controller->QueryInterface(
1116
1057
  IID_PPV_ARGS(&controller4))) &&
1117
1058
  controller4) {
1118
- controller4->put_AllowExternalDrop(FALSE);
1059
+ controller4->put_AllowExternalDrop(TRUE);
1119
1060
  }
1120
-
1121
- pImpl->nativeWebView = pImpl->webview.Get();
1122
- pImpl->ready = true;
1123
-
1124
- // Inject bridge script that runs on EVERY document
1125
- // (survives navigation)
1126
- std::string bridgeScript = R"(
1127
- window.__native_invoke__ = function(request) {
1128
- if (window.chrome && window.chrome.webview) {
1129
- window.chrome.webview.postMessage(request);
1130
- }
1131
- };
1132
- )";
1133
- pImpl->webview->AddScriptToExecuteOnDocumentCreated(
1134
- std::wstring(bridgeScript.begin(),
1135
- bridgeScript.end())
1136
- .c_str(),
1137
- nullptr);
1138
-
1139
- // Inject scrollbar hiding CSS if disabled
1140
- if (!pImpl->config.scrollbars) {
1141
- std::string scrollbarScript = kHideScrollbarsScript;
1142
- pImpl->webview->AddScriptToExecuteOnDocumentCreated(
1143
- std::wstring(scrollbarScript.begin(),
1144
- scrollbarScript.end())
1145
- .c_str(),
1146
- nullptr);
1147
- }
1148
-
1061
+
1062
+ pImpl->nativeWebView = pImpl->webview.Get();
1063
+ pImpl->ready = true;
1064
+
1065
+ // Inject bridge script that runs on EVERY document
1066
+ // (survives navigation)
1067
+ std::string bridgeScript = R"(
1068
+ window.__native_invoke__ = function(request) {
1069
+ if (window.chrome && window.chrome.webview) {
1070
+ window.chrome.webview.postMessage(request);
1071
+ }
1072
+ };
1073
+ )";
1074
+ pImpl->webview->AddScriptToExecuteOnDocumentCreated(
1075
+ std::wstring(bridgeScript.begin(),
1076
+ bridgeScript.end())
1077
+ .c_str(),
1078
+ nullptr);
1079
+
1080
+ // Inject scrollbar hiding CSS if disabled
1081
+ if (!pImpl->config.scrollbars) {
1082
+ std::string scrollbarScript = kHideScrollbarsScript;
1083
+ pImpl->webview->AddScriptToExecuteOnDocumentCreated(
1084
+ std::wstring(scrollbarScript.begin(),
1085
+ scrollbarScript.end())
1086
+ .c_str(),
1087
+ nullptr);
1088
+ }
1089
+
1149
1090
  // Block browser default drag-drop behavior (prevents
1150
1091
  // the browser from navigating to the dropped file) while
1151
1092
  // still allowing visual feedback on drop zones.
@@ -1157,6 +1098,7 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1157
1098
  window.__plusui_dropzone_init = true;
1158
1099
 
1159
1100
  var activeZone = null;
1101
+ var dragDepth = 0;
1160
1102
 
1161
1103
  var findDropZone = function(e) {
1162
1104
  var target = null;
@@ -1170,19 +1112,28 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1170
1112
 
1171
1113
  var updateActiveZone = function(zone) {
1172
1114
  if (activeZone === zone) return;
1173
- if (activeZone) activeZone.classList.remove('dropzone-active');
1115
+ if (activeZone) {
1116
+ activeZone.classList.remove('dropzone-active');
1117
+ activeZone.classList.remove('filedrop-active');
1118
+ }
1174
1119
  activeZone = zone;
1175
- if (activeZone) activeZone.classList.add('dropzone-active');
1120
+ if (activeZone) {
1121
+ activeZone.classList.add('dropzone-active');
1122
+ activeZone.classList.add('filedrop-active');
1123
+ }
1176
1124
  };
1177
1125
 
1178
1126
  // Always preventDefault to stop browser from navigating to file,
1179
- // but show visual feedback when over a drop zone
1127
+ // but show visual feedback when over a drop zone.
1128
+ // dragDepth tracks nested dragenter/dragleave pairs so we know
1129
+ // when the drag truly leaves the window (depth returns to 0).
1180
1130
  document.addEventListener('dragenter', function(e) {
1181
1131
  e.preventDefault();
1132
+ dragDepth++;
1182
1133
  var zone = findDropZone(e);
1183
1134
  updateActiveZone(zone);
1184
1135
  if (e.dataTransfer) {
1185
- try { e.dataTransfer.dropEffect = zone ? 'move' : 'none'; } catch(_) {}
1136
+ try { e.dataTransfer.dropEffect = zone ? 'copy' : 'none'; } catch(_) {}
1186
1137
  }
1187
1138
  }, true);
1188
1139
 
@@ -1191,19 +1142,63 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1191
1142
  var zone = findDropZone(e);
1192
1143
  updateActiveZone(zone);
1193
1144
  if (e.dataTransfer) {
1194
- try { e.dataTransfer.dropEffect = zone ? 'move' : 'none'; } catch(_) {}
1145
+ try { e.dataTransfer.dropEffect = zone ? 'copy' : 'none'; } catch(_) {}
1195
1146
  }
1196
1147
  }, true);
1197
1148
 
1198
1149
  document.addEventListener('dragleave', function(e) {
1199
1150
  e.preventDefault();
1200
- var zone = findDropZone(e);
1201
- updateActiveZone(zone);
1151
+ dragDepth--;
1152
+ if (dragDepth <= 0) {
1153
+ dragDepth = 0;
1154
+ updateActiveZone(null);
1155
+ } else {
1156
+ var zone = findDropZone(e);
1157
+ updateActiveZone(zone);
1158
+ }
1202
1159
  }, true);
1203
1160
 
1204
1161
  document.addEventListener('drop', function(e) {
1205
1162
  e.preventDefault();
1163
+ dragDepth = 0;
1206
1164
  updateActiveZone(null);
1165
+
1166
+ // Process dropped files and send to native
1167
+ if (e.dataTransfer && e.dataTransfer.files) {
1168
+ var files = [];
1169
+ for (var i = 0; i < e.dataTransfer.files.length; i++) {
1170
+ var file = e.dataTransfer.files[i];
1171
+ files.push({
1172
+ name: file.name,
1173
+ size: file.size,
1174
+ type: file.type || 'application/octet-stream'
1175
+ });
1176
+ }
1177
+
1178
+ // Send file info to native via the bridge
1179
+ if (window.__native_invoke__) {
1180
+ var request = JSON.stringify({
1181
+ method: 'plusui:fileDrop',
1182
+ params: { files: files }
1183
+ });
1184
+ window.__native_invoke__(request);
1185
+ }
1186
+
1187
+ // Also dispatch custom event for JS listeners
1188
+ window.dispatchEvent(new CustomEvent('plusui:fileDrop.filesDropped', {
1189
+ detail: { files: files }
1190
+ }));
1191
+
1192
+ // Call zone-specific handler if exists
1193
+ var zone = findDropZone(e);
1194
+ var zoneName = zone ? zone.getAttribute('data-dropzone') : null;
1195
+ if (zoneName && window.__plusui_fileDrop__) {
1196
+ window.__plusui_fileDrop__(zoneName, files);
1197
+ }
1198
+ if (!zoneName && window.__plusui_fileDrop_default__) {
1199
+ window.__plusui_fileDrop_default__(files);
1200
+ }
1201
+ }
1207
1202
  }, true);
1208
1203
 
1209
1204
  // Also block at window level as a safety net
@@ -1217,428 +1212,450 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1217
1212
  .c_str(),
1218
1213
  nullptr);
1219
1214
 
1220
- // Set up WebMessageReceived handler for JS->C++ bridge
1221
- pImpl->webview->add_WebMessageReceived(
1222
- Callback<
1223
- ICoreWebView2WebMessageReceivedEventHandler>(
1224
- [pImpl](ICoreWebView2 *sender,
1225
- ICoreWebView2WebMessageReceivedEventArgs
1226
- *args) -> HRESULT {
1227
- (void)sender; // Suppress unused warning
1228
- LPWSTR message = nullptr;
1229
- args->TryGetWebMessageAsString(&message);
1230
- if (!message)
1231
- return S_OK;
1232
- std::wstring wmsg(message);
1233
- #pragma warning(push)
1234
- #pragma warning(disable : 4244) // Suppress wchar_t to char conversion warning
1235
- std::string msg(wmsg.begin(), wmsg.end());
1236
- #pragma warning(pop)
1237
- CoTaskMemFree(message);
1238
-
1239
- // Debug: log received message
1240
- std::cout << "[PlusUI] Received: " << msg
1241
- << std::endl;
1242
-
1243
- bool handledByMessageCallback = false;
1244
-
1245
- // New Generic Message Handler
1246
- if (pImpl->messageCallback) {
1247
- pImpl->messageCallback(msg);
1248
- if (msg.find("\"kind\"") !=
1249
- std::string::npos) {
1250
- handledByMessageCallback = true;
1251
- }
1252
- }
1253
-
1254
- // Parse JSON-RPC: {"id":"...",
1255
- // "method":"window.minimize", "params":[...]}
1256
- std::string id, method;
1257
- std::string result = "null";
1258
- bool success = false;
1259
-
1260
- // Simple JSON parsing
1261
- auto getId = [&msg]() {
1262
- size_t pos = msg.find("\"id\"");
1263
- if (pos == std::string::npos)
1264
- return std::string();
1265
- pos = msg.find(":", pos);
1266
- if (pos == std::string::npos)
1267
- return std::string();
1268
- size_t start = msg.find("\"", pos);
1269
- if (start == std::string::npos)
1270
- return std::string();
1271
- size_t end = msg.find("\"", start + 1);
1272
- if (end == std::string::npos)
1273
- return std::string();
1274
- return msg.substr(start + 1,
1275
- end - start - 1);
1276
- };
1277
- auto getMethod = [&msg]() {
1278
- size_t pos = msg.find("\"method\"");
1279
- if (pos == std::string::npos)
1280
- return std::string();
1281
- pos = msg.find(":", pos);
1282
- if (pos == std::string::npos)
1283
- return std::string();
1284
- size_t start = msg.find("\"", pos);
1285
- if (start == std::string::npos)
1286
- return std::string();
1287
- size_t end = msg.find("\"", start + 1);
1288
- if (end == std::string::npos)
1289
- return std::string();
1290
- return msg.substr(start + 1,
1291
- end - start - 1);
1292
- };
1293
-
1294
- id = getId();
1295
- method = getMethod();
1296
-
1297
- auto extractFirstParam = [&msg]() {
1298
- size_t paramsPos = msg.find("\"params\"");
1299
- if (paramsPos == std::string::npos)
1300
- return std::string("null");
1301
- size_t colonPos = msg.find(":", paramsPos);
1302
- if (colonPos == std::string::npos)
1303
- return std::string("null");
1304
- size_t arrayStart = msg.find("[", colonPos);
1305
- if (arrayStart == std::string::npos)
1306
- return std::string("null");
1307
-
1308
- size_t i = arrayStart + 1;
1309
- while (i < msg.size() &&
1310
- std::isspace(
1311
- static_cast<unsigned char>(msg[i])))
1312
- ++i;
1313
- if (i >= msg.size() || msg[i] == ']')
1314
- return std::string("null");
1315
-
1316
- size_t start = i;
1317
- if (msg[i] == '"') {
1318
- ++i;
1319
- bool escaped = false;
1320
- while (i < msg.size()) {
1321
- char c = msg[i];
1322
- if (escaped) {
1323
- escaped = false;
1324
- } else if (c == '\\') {
1325
- escaped = true;
1326
- } else if (c == '"') {
1327
- ++i;
1328
- break;
1329
- }
1330
- ++i;
1331
- }
1332
- return msg.substr(start, i - start);
1333
- }
1334
-
1335
- int depth = 0;
1336
- bool inString = false;
1337
- bool escaped = false;
1338
- while (i < msg.size()) {
1339
- char c = msg[i];
1340
- if (inString) {
1341
- if (escaped) {
1342
- escaped = false;
1343
- } else if (c == '\\') {
1344
- escaped = true;
1345
- } else if (c == '"') {
1346
- inString = false;
1347
- }
1348
- } else {
1349
- if (c == '"') {
1350
- inString = true;
1351
- } else if (c == '{' || c == '[') {
1352
- ++depth;
1353
- } else if (c == '}' || c == ']') {
1354
- if (depth == 0) {
1355
- break;
1356
- }
1357
- --depth;
1358
- } else if (c == ',' && depth == 0) {
1359
- break;
1360
- }
1361
- }
1362
- ++i;
1363
- }
1364
- return msg.substr(start, i - start);
1365
- };
1366
-
1367
- auto decodeJsonString =
1368
- [](const std::string &input) {
1369
- if (input.size() < 2 ||
1370
- input.front() != '"' ||
1371
- input.back() != '"') {
1372
- return input;
1373
- }
1374
-
1375
- std::string decoded;
1376
- decoded.reserve(input.size() - 2);
1377
- for (size_t i = 1; i + 1 < input.size();
1378
- ++i) {
1379
- char c = input[i];
1380
- if (c == '\\' && i + 1 < input.size() - 1) {
1381
- char next = input[++i];
1382
- switch (next) {
1383
- case '"':
1384
- decoded.push_back('"');
1385
- break;
1386
- case '\\':
1387
- decoded.push_back('\\');
1388
- break;
1389
- case '/':
1390
- decoded.push_back('/');
1391
- break;
1392
- case 'n':
1393
- decoded.push_back('\n');
1394
- break;
1395
- case 'r':
1396
- decoded.push_back('\r');
1397
- break;
1398
- case 't':
1399
- decoded.push_back('\t');
1400
- break;
1401
- default:
1402
- decoded.push_back(next);
1403
- break;
1404
- }
1405
- } else {
1406
- decoded.push_back(c);
1407
- }
1408
- }
1409
- return decoded;
1410
- };
1411
-
1412
- // Route to handlers
1413
- if (handledByMessageCallback) {
1414
- success = true;
1415
- result = "null";
1416
- } else if (method.find("window.") == 0) {
1417
- std::string winMethod = method.substr(7);
1418
- if (winMethod == "minimize") {
1419
- if (pImpl->window)
1420
- pImpl->window->minimize();
1421
- success = true;
1422
- } else if (winMethod == "maximize") {
1423
- if (pImpl->window)
1424
- pImpl->window->maximize();
1425
- success = true;
1426
- } else if (winMethod == "restore") {
1427
- if (pImpl->window)
1428
- pImpl->window->restore();
1429
- success = true;
1430
- } else if (winMethod == "close") {
1431
- if (pImpl->window)
1432
- pImpl->window->close();
1433
- success = true;
1434
- } else if (winMethod == "show") {
1435
- if (pImpl->window)
1436
- pImpl->window->show();
1437
- success = true;
1438
- } else if (winMethod == "hide") {
1439
- if (pImpl->window)
1440
- pImpl->window->hide();
1441
- success = true;
1442
- } else if (winMethod == "getSize") {
1443
- if (pImpl->window) {
1444
- int w, h;
1445
- pImpl->window->getSize(w, h);
1446
- result =
1447
- "{\"width\":" + std::to_string(w) +
1448
- ",\"height\":" + std::to_string(h) +
1449
- "}";
1450
- }
1451
- success = true;
1452
- } else if (winMethod == "getPosition") {
1453
- if (pImpl->window) {
1454
- int x, y;
1455
- pImpl->window->getPosition(x, y);
1456
- result = "{\"x\":" + std::to_string(x) +
1457
- ",\"y\":" + std::to_string(y) +
1458
- "}";
1459
- }
1460
- success = true;
1461
- } else if (winMethod == "setSize") {
1462
- // Parse params [width, height]
1463
- size_t p1 = msg.find("[");
1464
- size_t p2 = msg.find("]");
1465
- if (p1 != std::string::npos &&
1466
- p2 != std::string::npos) {
1467
- std::string params =
1468
- msg.substr(p1 + 1, p2 - p1 - 1);
1469
- int w = 0, h = 0;
1470
- sscanf(params.c_str(), "%d, %d", &w,
1471
- &h);
1472
- if (pImpl->window)
1473
- pImpl->window->setSize(w, h);
1474
- }
1475
- success = true;
1476
- } else if (winMethod == "setPosition") {
1477
- size_t p1 = msg.find("[");
1478
- size_t p2 = msg.find("]");
1479
- if (p1 != std::string::npos &&
1480
- p2 != std::string::npos) {
1481
- std::string params =
1482
- msg.substr(p1 + 1, p2 - p1 - 1);
1483
- int x = 0, y = 0;
1484
- sscanf(params.c_str(), "%d, %d", &x,
1485
- &y);
1486
- if (pImpl->window)
1487
- pImpl->window->setPosition(x, y);
1488
- }
1489
- success = true;
1490
- } else if (winMethod == "setTitle") {
1491
- size_t p1 = msg.find("[\"");
1492
- size_t p2 = msg.find("\"]");
1493
- if (p1 != std::string::npos &&
1494
- p2 != std::string::npos) {
1495
- std::string title =
1496
- msg.substr(p1 + 2, p2 - p1 - 2);
1497
- if (pImpl->window)
1498
- pImpl->window->setTitle(title);
1499
- }
1500
- success = true;
1501
- } else if (winMethod == "setFullscreen") {
1502
- size_t p1 = msg.find("[");
1503
- size_t p2 = msg.find("]");
1504
- if (p1 != std::string::npos &&
1505
- p2 != std::string::npos) {
1506
- std::string params =
1507
- msg.substr(p1 + 1, p2 - p1 - 1);
1508
- if (pImpl->window)
1509
- pImpl->window->setFullscreen(
1510
- params.find("true") !=
1511
- std::string::npos);
1512
- }
1513
- success = true;
1514
- } else if (winMethod == "setAlwaysOnTop") {
1515
- size_t p1 = msg.find("[");
1516
- size_t p2 = msg.find("]");
1517
- if (p1 != std::string::npos &&
1518
- p2 != std::string::npos) {
1519
- std::string params =
1520
- msg.substr(p1 + 1, p2 - p1 - 1);
1521
- if (pImpl->window)
1522
- pImpl->window->setAlwaysOnTop(
1523
- params.find("true") !=
1524
- std::string::npos);
1525
- }
1526
- success = true;
1527
- } else if (winMethod == "setResizable") {
1528
- size_t p1 = msg.find("[");
1529
- size_t p2 = msg.find("]");
1530
- if (p1 != std::string::npos &&
1531
- p2 != std::string::npos) {
1532
- std::string params =
1533
- msg.substr(p1 + 1, p2 - p1 - 1);
1534
- if (pImpl->window)
1535
- pImpl->window->setResizable(
1536
- params.find("true") !=
1537
- std::string::npos);
1538
- }
1539
- success = true;
1540
- } else if (winMethod == "isMaximized") {
1541
- result = (pImpl->window &&
1542
- pImpl->window->isMaximized())
1543
- ? "true"
1544
- : "false";
1545
- success = true;
1546
- } else if (winMethod == "isMinimized") {
1547
- result = (pImpl->window &&
1548
- pImpl->window->isMinimized())
1549
- ? "true"
1550
- : "false";
1551
- success = true;
1552
- } else if (winMethod == "isVisible") {
1553
- result = (pImpl->window &&
1554
- pImpl->window->isVisible())
1555
- ? "true"
1556
- : "false";
1557
- success = true;
1558
- } else if (winMethod == "center") {
1559
- if (pImpl->window)
1560
- pImpl->window->center();
1561
- success = true;
1562
- }
1563
- } else if (method.find("browser.") == 0) {
1564
- std::string browserMethod =
1565
- method.substr(8);
1566
- if (browserMethod == "navigate") {
1567
- size_t p1 = msg.find("[\"");
1568
- size_t p2 = msg.find("\"]");
1569
- if (p1 != std::string::npos &&
1570
- p2 != std::string::npos) {
1571
- std::string url =
1572
- msg.substr(p1 + 2, p2 - p1 - 2);
1573
- pImpl->webview->Navigate(
1574
- std::wstring(url.begin(), url.end())
1575
- .c_str());
1576
- }
1577
- success = true;
1578
- } else if (browserMethod == "goBack") {
1579
- pImpl->webview->GoBack();
1580
- success = true;
1581
- } else if (browserMethod == "goForward") {
1582
- pImpl->webview->GoForward();
1583
- success = true;
1584
- } else if (browserMethod == "reload") {
1585
- pImpl->webview->Reload();
1586
- success = true;
1587
- } else if (browserMethod == "stop") {
1588
- pImpl->webview->Stop();
1589
- success = true;
1590
- } else if (browserMethod == "getUrl") {
1591
- result = "\"" + pImpl->currentURL + "\"";
1592
- success = true;
1593
- } else if (browserMethod == "getTitle") {
1594
- result =
1595
- "\"" + pImpl->currentTitle + "\"";
1596
- success = true;
1597
- } else if (browserMethod == "canGoBack") {
1598
- BOOL canBack;
1599
- pImpl->webview->get_CanGoBack(&canBack);
1600
- result = canBack ? "true" : "false";
1601
- success = true;
1602
- } else if (browserMethod ==
1603
- "canGoForward") {
1604
- BOOL canForward;
1605
- pImpl->webview->get_CanGoForward(
1606
- &canForward);
1607
- result = canForward ? "true" : "false";
1608
- success = true;
1609
- }
1610
- } else if (method.find("fileDrop.") == 0) {
1611
- std::string fileDropMethod =
1612
- method.substr(9);
1613
- if (fileDropMethod == "setEnabled") {
1614
- size_t p1 = msg.find("[");
1615
- size_t p2 = msg.find("]");
1616
- if (p1 != std::string::npos &&
1617
- p2 != std::string::npos) {
1618
- std::string params =
1619
- msg.substr(p1 + 1, p2 - p1 - 1);
1620
- bool enabled = params.find("true") !=
1621
- std::string::npos;
1622
- pImpl->config.fileDrop = enabled;
1623
-
1624
- HWND targetHwnd = nullptr;
1625
- if (pImpl->window) {
1626
- targetHwnd = static_cast<HWND>(
1627
- pImpl->window->nativeHandle());
1628
- }
1629
- if (!targetHwnd && pImpl->controller) {
1630
- pImpl->controller->get_ParentWindow(
1631
- &targetHwnd);
1632
- }
1633
- if (targetHwnd) {
1634
- DragAcceptFiles(targetHwnd,
1635
- enabled ? TRUE
1636
- : FALSE);
1637
- }
1638
-
1639
- // Always keep external drops allowed
1640
- // at the WebView2 level so WM_DROPFILES
1641
- // fires on the parent HWND
1215
+ // Set up WebMessageReceived handler for JS->C++ bridge
1216
+ pImpl->webview->add_WebMessageReceived(
1217
+ Callback<
1218
+ ICoreWebView2WebMessageReceivedEventHandler>(
1219
+ [pImpl](ICoreWebView2 *sender,
1220
+ ICoreWebView2WebMessageReceivedEventArgs
1221
+ *args) -> HRESULT {
1222
+ (void)sender; // Suppress unused warning
1223
+ LPWSTR message = nullptr;
1224
+ args->TryGetWebMessageAsString(&message);
1225
+ if (!message)
1226
+ return S_OK;
1227
+ std::wstring wmsg(message);
1228
+ #pragma warning(push)
1229
+ #pragma warning(disable : 4244) // Suppress wchar_t to char conversion warning
1230
+ std::string msg(wmsg.begin(), wmsg.end());
1231
+ #pragma warning(pop)
1232
+ CoTaskMemFree(message);
1233
+
1234
+ // Debug: log received message
1235
+ std::cout << "[PlusUI] Received: " << msg
1236
+ << std::endl;
1237
+
1238
+ bool handledByMessageCallback = false;
1239
+
1240
+ // New Generic Message Handler
1241
+ if (pImpl->messageCallback) {
1242
+ pImpl->messageCallback(msg);
1243
+ if (msg.find("\"kind\"") !=
1244
+ std::string::npos) {
1245
+ handledByMessageCallback = true;
1246
+ }
1247
+ }
1248
+
1249
+ // Parse JSON-RPC: {"id":"...",
1250
+ // "method":"window.minimize", "params":[...]}
1251
+ std::string id, method;
1252
+ std::string result = "null";
1253
+ bool success = false;
1254
+
1255
+ // Handle plusui:fileDrop message from JS
1256
+ if (msg.find("plusui:fileDrop") != std::string::npos) {
1257
+ // Extract files from the message
1258
+ size_t filesStart = msg.find("\"files\"");
1259
+ if (filesStart != std::string::npos) {
1260
+ size_t bracketStart = msg.find("[", filesStart);
1261
+ size_t bracketEnd = msg.find("]", bracketStart);
1262
+ if (bracketStart != std::string::npos && bracketEnd != std::string::npos) {
1263
+ std::string filesJson = msg.substr(bracketStart, bracketEnd - bracketStart + 1);
1264
+
1265
+ // Fire the global event in JS
1266
+ std::string eventScript =
1267
+ "window.dispatchEvent(new CustomEvent('plusui:fileDrop.filesDropped', { detail: { files: " + filesJson + " } }));";
1268
+ pImpl->webview->ExecuteScript(
1269
+ std::wstring(eventScript.begin(), eventScript.end()).c_str(), nullptr);
1270
+ }
1271
+ }
1272
+ success = true;
1273
+ }
1274
+
1275
+ // Simple JSON parsing
1276
+ auto getId = [&msg]() {
1277
+ size_t pos = msg.find("\"id\"");
1278
+ if (pos == std::string::npos)
1279
+ return std::string();
1280
+ pos = msg.find(":", pos);
1281
+ if (pos == std::string::npos)
1282
+ return std::string();
1283
+ size_t start = msg.find("\"", pos);
1284
+ if (start == std::string::npos)
1285
+ return std::string();
1286
+ size_t end = msg.find("\"", start + 1);
1287
+ if (end == std::string::npos)
1288
+ return std::string();
1289
+ return msg.substr(start + 1,
1290
+ end - start - 1);
1291
+ };
1292
+ auto getMethod = [&msg]() {
1293
+ size_t pos = msg.find("\"method\"");
1294
+ if (pos == std::string::npos)
1295
+ return std::string();
1296
+ pos = msg.find(":", pos);
1297
+ if (pos == std::string::npos)
1298
+ return std::string();
1299
+ size_t start = msg.find("\"", pos);
1300
+ if (start == std::string::npos)
1301
+ return std::string();
1302
+ size_t end = msg.find("\"", start + 1);
1303
+ if (end == std::string::npos)
1304
+ return std::string();
1305
+ return msg.substr(start + 1,
1306
+ end - start - 1);
1307
+ };
1308
+
1309
+ id = getId();
1310
+ method = getMethod();
1311
+
1312
+ auto extractFirstParam = [&msg]() {
1313
+ size_t paramsPos = msg.find("\"params\"");
1314
+ if (paramsPos == std::string::npos)
1315
+ return std::string("null");
1316
+ size_t colonPos = msg.find(":", paramsPos);
1317
+ if (colonPos == std::string::npos)
1318
+ return std::string("null");
1319
+ size_t arrayStart = msg.find("[", colonPos);
1320
+ if (arrayStart == std::string::npos)
1321
+ return std::string("null");
1322
+
1323
+ size_t i = arrayStart + 1;
1324
+ while (i < msg.size() &&
1325
+ std::isspace(
1326
+ static_cast<unsigned char>(msg[i])))
1327
+ ++i;
1328
+ if (i >= msg.size() || msg[i] == ']')
1329
+ return std::string("null");
1330
+
1331
+ size_t start = i;
1332
+ if (msg[i] == '"') {
1333
+ ++i;
1334
+ bool escaped = false;
1335
+ while (i < msg.size()) {
1336
+ char c = msg[i];
1337
+ if (escaped) {
1338
+ escaped = false;
1339
+ } else if (c == '\\') {
1340
+ escaped = true;
1341
+ } else if (c == '"') {
1342
+ ++i;
1343
+ break;
1344
+ }
1345
+ ++i;
1346
+ }
1347
+ return msg.substr(start, i - start);
1348
+ }
1349
+
1350
+ int depth = 0;
1351
+ bool inString = false;
1352
+ bool escaped = false;
1353
+ while (i < msg.size()) {
1354
+ char c = msg[i];
1355
+ if (inString) {
1356
+ if (escaped) {
1357
+ escaped = false;
1358
+ } else if (c == '\\') {
1359
+ escaped = true;
1360
+ } else if (c == '"') {
1361
+ inString = false;
1362
+ }
1363
+ } else {
1364
+ if (c == '"') {
1365
+ inString = true;
1366
+ } else if (c == '{' || c == '[') {
1367
+ ++depth;
1368
+ } else if (c == '}' || c == ']') {
1369
+ if (depth == 0) {
1370
+ break;
1371
+ }
1372
+ --depth;
1373
+ } else if (c == ',' && depth == 0) {
1374
+ break;
1375
+ }
1376
+ }
1377
+ ++i;
1378
+ }
1379
+ return msg.substr(start, i - start);
1380
+ };
1381
+
1382
+ auto decodeJsonString =
1383
+ [](const std::string &input) {
1384
+ if (input.size() < 2 ||
1385
+ input.front() != '"' ||
1386
+ input.back() != '"') {
1387
+ return input;
1388
+ }
1389
+
1390
+ std::string decoded;
1391
+ decoded.reserve(input.size() - 2);
1392
+ for (size_t i = 1; i + 1 < input.size();
1393
+ ++i) {
1394
+ char c = input[i];
1395
+ if (c == '\\' && i + 1 < input.size() - 1) {
1396
+ char next = input[++i];
1397
+ switch (next) {
1398
+ case '"':
1399
+ decoded.push_back('"');
1400
+ break;
1401
+ case '\\':
1402
+ decoded.push_back('\\');
1403
+ break;
1404
+ case '/':
1405
+ decoded.push_back('/');
1406
+ break;
1407
+ case 'n':
1408
+ decoded.push_back('\n');
1409
+ break;
1410
+ case 'r':
1411
+ decoded.push_back('\r');
1412
+ break;
1413
+ case 't':
1414
+ decoded.push_back('\t');
1415
+ break;
1416
+ default:
1417
+ decoded.push_back(next);
1418
+ break;
1419
+ }
1420
+ } else {
1421
+ decoded.push_back(c);
1422
+ }
1423
+ }
1424
+ return decoded;
1425
+ };
1426
+
1427
+ // Route to handlers
1428
+ if (handledByMessageCallback) {
1429
+ success = true;
1430
+ result = "null";
1431
+ } else if (method.find("window.") == 0) {
1432
+ std::string winMethod = method.substr(7);
1433
+ if (winMethod == "minimize") {
1434
+ if (pImpl->window)
1435
+ pImpl->window->minimize();
1436
+ success = true;
1437
+ } else if (winMethod == "maximize") {
1438
+ if (pImpl->window)
1439
+ pImpl->window->maximize();
1440
+ success = true;
1441
+ } else if (winMethod == "restore") {
1442
+ if (pImpl->window)
1443
+ pImpl->window->restore();
1444
+ success = true;
1445
+ } else if (winMethod == "close") {
1446
+ if (pImpl->window)
1447
+ pImpl->window->close();
1448
+ success = true;
1449
+ } else if (winMethod == "show") {
1450
+ if (pImpl->window)
1451
+ pImpl->window->show();
1452
+ success = true;
1453
+ } else if (winMethod == "hide") {
1454
+ if (pImpl->window)
1455
+ pImpl->window->hide();
1456
+ success = true;
1457
+ } else if (winMethod == "getSize") {
1458
+ if (pImpl->window) {
1459
+ int w, h;
1460
+ pImpl->window->getSize(w, h);
1461
+ result =
1462
+ "{\"width\":" + std::to_string(w) +
1463
+ ",\"height\":" + std::to_string(h) +
1464
+ "}";
1465
+ }
1466
+ success = true;
1467
+ } else if (winMethod == "getPosition") {
1468
+ if (pImpl->window) {
1469
+ int x, y;
1470
+ pImpl->window->getPosition(x, y);
1471
+ result = "{\"x\":" + std::to_string(x) +
1472
+ ",\"y\":" + std::to_string(y) +
1473
+ "}";
1474
+ }
1475
+ success = true;
1476
+ } else if (winMethod == "setSize") {
1477
+ // Parse params [width, height]
1478
+ size_t p1 = msg.find("[");
1479
+ size_t p2 = msg.find("]");
1480
+ if (p1 != std::string::npos &&
1481
+ p2 != std::string::npos) {
1482
+ std::string params =
1483
+ msg.substr(p1 + 1, p2 - p1 - 1);
1484
+ int w = 0, h = 0;
1485
+ sscanf(params.c_str(), "%d, %d", &w,
1486
+ &h);
1487
+ if (pImpl->window)
1488
+ pImpl->window->setSize(w, h);
1489
+ }
1490
+ success = true;
1491
+ } else if (winMethod == "setPosition") {
1492
+ size_t p1 = msg.find("[");
1493
+ size_t p2 = msg.find("]");
1494
+ if (p1 != std::string::npos &&
1495
+ p2 != std::string::npos) {
1496
+ std::string params =
1497
+ msg.substr(p1 + 1, p2 - p1 - 1);
1498
+ int x = 0, y = 0;
1499
+ sscanf(params.c_str(), "%d, %d", &x,
1500
+ &y);
1501
+ if (pImpl->window)
1502
+ pImpl->window->setPosition(x, y);
1503
+ }
1504
+ success = true;
1505
+ } else if (winMethod == "setTitle") {
1506
+ size_t p1 = msg.find("[\"");
1507
+ size_t p2 = msg.find("\"]");
1508
+ if (p1 != std::string::npos &&
1509
+ p2 != std::string::npos) {
1510
+ std::string title =
1511
+ msg.substr(p1 + 2, p2 - p1 - 2);
1512
+ if (pImpl->window)
1513
+ pImpl->window->setTitle(title);
1514
+ }
1515
+ success = true;
1516
+ } else if (winMethod == "setFullscreen") {
1517
+ size_t p1 = msg.find("[");
1518
+ size_t p2 = msg.find("]");
1519
+ if (p1 != std::string::npos &&
1520
+ p2 != std::string::npos) {
1521
+ std::string params =
1522
+ msg.substr(p1 + 1, p2 - p1 - 1);
1523
+ if (pImpl->window)
1524
+ pImpl->window->setFullscreen(
1525
+ params.find("true") !=
1526
+ std::string::npos);
1527
+ }
1528
+ success = true;
1529
+ } else if (winMethod == "setAlwaysOnTop") {
1530
+ size_t p1 = msg.find("[");
1531
+ size_t p2 = msg.find("]");
1532
+ if (p1 != std::string::npos &&
1533
+ p2 != std::string::npos) {
1534
+ std::string params =
1535
+ msg.substr(p1 + 1, p2 - p1 - 1);
1536
+ if (pImpl->window)
1537
+ pImpl->window->setAlwaysOnTop(
1538
+ params.find("true") !=
1539
+ std::string::npos);
1540
+ }
1541
+ success = true;
1542
+ } else if (winMethod == "setResizable") {
1543
+ size_t p1 = msg.find("[");
1544
+ size_t p2 = msg.find("]");
1545
+ if (p1 != std::string::npos &&
1546
+ p2 != std::string::npos) {
1547
+ std::string params =
1548
+ msg.substr(p1 + 1, p2 - p1 - 1);
1549
+ if (pImpl->window)
1550
+ pImpl->window->setResizable(
1551
+ params.find("true") !=
1552
+ std::string::npos);
1553
+ }
1554
+ success = true;
1555
+ } else if (winMethod == "isMaximized") {
1556
+ result = (pImpl->window &&
1557
+ pImpl->window->isMaximized())
1558
+ ? "true"
1559
+ : "false";
1560
+ success = true;
1561
+ } else if (winMethod == "isMinimized") {
1562
+ result = (pImpl->window &&
1563
+ pImpl->window->isMinimized())
1564
+ ? "true"
1565
+ : "false";
1566
+ success = true;
1567
+ } else if (winMethod == "isVisible") {
1568
+ result = (pImpl->window &&
1569
+ pImpl->window->isVisible())
1570
+ ? "true"
1571
+ : "false";
1572
+ success = true;
1573
+ } else if (winMethod == "center") {
1574
+ if (pImpl->window)
1575
+ pImpl->window->center();
1576
+ success = true;
1577
+ }
1578
+ } else if (method.find("browser.") == 0) {
1579
+ std::string browserMethod =
1580
+ method.substr(8);
1581
+ if (browserMethod == "navigate") {
1582
+ size_t p1 = msg.find("[\"");
1583
+ size_t p2 = msg.find("\"]");
1584
+ if (p1 != std::string::npos &&
1585
+ p2 != std::string::npos) {
1586
+ std::string url =
1587
+ msg.substr(p1 + 2, p2 - p1 - 2);
1588
+ pImpl->webview->Navigate(
1589
+ std::wstring(url.begin(), url.end())
1590
+ .c_str());
1591
+ }
1592
+ success = true;
1593
+ } else if (browserMethod == "goBack") {
1594
+ pImpl->webview->GoBack();
1595
+ success = true;
1596
+ } else if (browserMethod == "goForward") {
1597
+ pImpl->webview->GoForward();
1598
+ success = true;
1599
+ } else if (browserMethod == "reload") {
1600
+ pImpl->webview->Reload();
1601
+ success = true;
1602
+ } else if (browserMethod == "stop") {
1603
+ pImpl->webview->Stop();
1604
+ success = true;
1605
+ } else if (browserMethod == "getUrl") {
1606
+ result = "\"" + pImpl->currentURL + "\"";
1607
+ success = true;
1608
+ } else if (browserMethod == "getTitle") {
1609
+ result =
1610
+ "\"" + pImpl->currentTitle + "\"";
1611
+ success = true;
1612
+ } else if (browserMethod == "canGoBack") {
1613
+ BOOL canBack;
1614
+ pImpl->webview->get_CanGoBack(&canBack);
1615
+ result = canBack ? "true" : "false";
1616
+ success = true;
1617
+ } else if (browserMethod ==
1618
+ "canGoForward") {
1619
+ BOOL canForward;
1620
+ pImpl->webview->get_CanGoForward(
1621
+ &canForward);
1622
+ result = canForward ? "true" : "false";
1623
+ success = true;
1624
+ }
1625
+ } else if (method.find("fileDrop.") == 0) {
1626
+ std::string fileDropMethod =
1627
+ method.substr(9);
1628
+ if (fileDropMethod == "setEnabled") {
1629
+ size_t p1 = msg.find("[");
1630
+ size_t p2 = msg.find("]");
1631
+ if (p1 != std::string::npos &&
1632
+ p2 != std::string::npos) {
1633
+ std::string params =
1634
+ msg.substr(p1 + 1, p2 - p1 - 1);
1635
+ bool enabled = params.find("true") !=
1636
+ std::string::npos;
1637
+ pImpl->config.fileDrop = enabled;
1638
+
1639
+ HWND targetHwnd = nullptr;
1640
+ if (pImpl->window) {
1641
+ targetHwnd = static_cast<HWND>(
1642
+ pImpl->window->nativeHandle());
1643
+ }
1644
+ if (!targetHwnd && pImpl->controller) {
1645
+ pImpl->controller->get_ParentWindow(
1646
+ &targetHwnd);
1647
+ }
1648
+ if (targetHwnd) {
1649
+ DragAcceptFiles(targetHwnd,
1650
+ enabled ? TRUE
1651
+ : FALSE);
1652
+ }
1653
+
1654
+ // Keep external drops DISABLED at WebView2 level
1655
+ // so our IDropTarget on the parent HWND can intercept
1656
+ // them and WM_DROPFILES never fires (which would
1657
+ // cause double-handling). We handle everything
1658
+ // in IDropTarget::Drop.
1642
1659
  if (pImpl->controller) {
1643
1660
  ComPtr<ICoreWebView2Controller4>
1644
1661
  controller4;
@@ -1646,115 +1663,115 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1646
1663
  &controller4)) &&
1647
1664
  controller4) {
1648
1665
  controller4->put_AllowExternalDrop(
1649
- TRUE);
1666
+ FALSE);
1650
1667
  }
1651
1668
  }
1652
- }
1653
- success = true;
1654
- } else if (fileDropMethod == "isEnabled") {
1655
- result = pImpl->config.fileDrop
1656
- ? "true"
1657
- : "false";
1658
- success = true;
1659
- } else if (fileDropMethod == "startDrag") {
1660
- result = "false";
1661
- success = true;
1662
- } else if (fileDropMethod ==
1663
- "clearCallbacks") {
1664
- success = true;
1665
- }
1666
- } else if (method.find("webview.") == 0) {
1667
- std::string bindingName = method.substr(8);
1668
- auto bindingIt =
1669
- pImpl->bindings.find(bindingName);
1670
- if (bindingIt != pImpl->bindings.end()) {
1671
- std::string rawParam =
1672
- extractFirstParam();
1673
- std::string callbackArg =
1674
- decodeJsonString(rawParam);
1675
- try {
1676
- result =
1677
- bindingIt->second(callbackArg);
1678
- if (result.empty()) {
1679
- result = "null";
1680
- }
1681
- success = true;
1682
- } catch (const std::exception &e) {
1683
- std::cerr
1684
- << "[PlusUI] webview binding error: "
1685
- << e.what() << std::endl;
1686
- result = "null";
1687
- success = true;
1688
- }
1689
- }
1690
- }
1691
-
1692
- // Send response back to JS (matches
1693
- // plusui-native-core SDK bridge)
1694
- std::string response =
1695
- "window.__response__(\"" + id + "\", " +
1696
- result + ");";
1697
- pImpl->webview->ExecuteScript(
1698
- std::wstring(response.begin(),
1699
- response.end())
1700
- .c_str(),
1701
- nullptr);
1702
-
1703
- return S_OK;
1704
- })
1705
- .Get(),
1706
- nullptr);
1707
-
1708
- // Process pending scripts
1709
- for (const auto &script : pImpl->pendingScripts) {
1710
- pImpl->webview->ExecuteScript(
1711
- std::wstring(script.begin(), script.end())
1712
- .c_str(),
1713
- nullptr);
1714
- }
1715
- pImpl->pendingScripts.clear();
1716
-
1717
- // Process pending navigation
1718
- if (!pImpl->pendingNavigation.empty()) {
1719
- pImpl->webview->Navigate(
1720
- std::wstring(pImpl->pendingNavigation.begin(),
1721
- pImpl->pendingNavigation.end())
1722
- .c_str());
1723
- pImpl->pendingNavigation.clear();
1724
- }
1725
-
1726
- // Process pending HTML
1727
- if (!pImpl->pendingHTML.empty()) {
1728
- pImpl->webview->NavigateToString(
1729
- std::wstring(pImpl->pendingHTML.begin(),
1730
- pImpl->pendingHTML.end())
1731
- .c_str());
1732
- pImpl->pendingHTML.clear();
1733
- }
1734
-
1735
- // Process pending File
1736
- if (!pImpl->pendingFile.empty()) {
1737
- // For now, loadFile is implemented via navigate in
1738
- // some versions or direct file reading. We'll handle
1739
- // it via navigate for simplicity if it's already
1740
- // implemented that way.
1741
- }
1742
- }
1743
- return S_OK;
1744
- })
1745
- .Get());
1746
- return S_OK;
1747
- })
1748
- .Get());
1749
-
1750
- #elif defined(__APPLE__)
1751
- WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
1752
- config.preferences.javaScriptEnabled = YES;
1753
-
1754
- if (win.pImpl->config.devtools) {
1755
- [config.preferences setValue:@YES forKey:@"developerExtrasEnabled"];
1756
- }
1757
-
1669
+ }
1670
+ success = true;
1671
+ } else if (fileDropMethod == "isEnabled") {
1672
+ result = pImpl->config.fileDrop
1673
+ ? "true"
1674
+ : "false";
1675
+ success = true;
1676
+ } else if (fileDropMethod == "startDrag") {
1677
+ result = "false";
1678
+ success = true;
1679
+ } else if (fileDropMethod ==
1680
+ "clearCallbacks") {
1681
+ success = true;
1682
+ }
1683
+ } else if (method.find("webview.") == 0) {
1684
+ std::string bindingName = method.substr(8);
1685
+ auto bindingIt =
1686
+ pImpl->bindings.find(bindingName);
1687
+ if (bindingIt != pImpl->bindings.end()) {
1688
+ std::string rawParam =
1689
+ extractFirstParam();
1690
+ std::string callbackArg =
1691
+ decodeJsonString(rawParam);
1692
+ try {
1693
+ result =
1694
+ bindingIt->second(callbackArg);
1695
+ if (result.empty()) {
1696
+ result = "null";
1697
+ }
1698
+ success = true;
1699
+ } catch (const std::exception &e) {
1700
+ std::cerr
1701
+ << "[PlusUI] webview binding error: "
1702
+ << e.what() << std::endl;
1703
+ result = "null";
1704
+ success = true;
1705
+ }
1706
+ }
1707
+ }
1708
+
1709
+ // Send response back to JS (matches
1710
+ // plusui-native-core SDK bridge)
1711
+ std::string response =
1712
+ "window.__response__(\"" + id + "\", " +
1713
+ result + ");";
1714
+ pImpl->webview->ExecuteScript(
1715
+ std::wstring(response.begin(),
1716
+ response.end())
1717
+ .c_str(),
1718
+ nullptr);
1719
+
1720
+ return S_OK;
1721
+ })
1722
+ .Get(),
1723
+ nullptr);
1724
+
1725
+ // Process pending scripts
1726
+ for (const auto &script : pImpl->pendingScripts) {
1727
+ pImpl->webview->ExecuteScript(
1728
+ std::wstring(script.begin(), script.end())
1729
+ .c_str(),
1730
+ nullptr);
1731
+ }
1732
+ pImpl->pendingScripts.clear();
1733
+
1734
+ // Process pending navigation
1735
+ if (!pImpl->pendingNavigation.empty()) {
1736
+ pImpl->webview->Navigate(
1737
+ std::wstring(pImpl->pendingNavigation.begin(),
1738
+ pImpl->pendingNavigation.end())
1739
+ .c_str());
1740
+ pImpl->pendingNavigation.clear();
1741
+ }
1742
+
1743
+ // Process pending HTML
1744
+ if (!pImpl->pendingHTML.empty()) {
1745
+ pImpl->webview->NavigateToString(
1746
+ std::wstring(pImpl->pendingHTML.begin(),
1747
+ pImpl->pendingHTML.end())
1748
+ .c_str());
1749
+ pImpl->pendingHTML.clear();
1750
+ }
1751
+
1752
+ // Process pending File
1753
+ if (!pImpl->pendingFile.empty()) {
1754
+ // For now, loadFile is implemented via navigate in
1755
+ // some versions or direct file reading. We'll handle
1756
+ // it via navigate for simplicity if it's already
1757
+ // implemented that way.
1758
+ }
1759
+ }
1760
+ return S_OK;
1761
+ })
1762
+ .Get());
1763
+ return S_OK;
1764
+ })
1765
+ .Get());
1766
+
1767
+ #elif defined(__APPLE__)
1768
+ WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
1769
+ config.preferences.javaScriptEnabled = YES;
1770
+
1771
+ if (win.pImpl->config.devtools) {
1772
+ [config.preferences setValue:@YES forKey:@"developerExtrasEnabled"];
1773
+ }
1774
+
1758
1775
  // Block browser default drag-drop while allowing drop zone visual feedback.
1759
1776
  // File delivery is handled natively by macOS drag APIs, not browser events.
1760
1777
  if (win.pImpl->config.disableWebviewDragDrop ||
@@ -1812,24 +1829,24 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1812
1829
  forMainFrameOnly:NO];
1813
1830
  [config.userContentController addUserScript:userScript];
1814
1831
  }
1815
-
1816
- // Hide scrollbars if disabled
1817
- if (!win.pImpl->config.scrollbars) {
1818
- NSString *scrollbarScript =
1819
- [NSString stringWithUTF8String:kHideScrollbarsScript];
1820
-
1821
- WKUserScript *scrollScript = [[WKUserScript alloc]
1822
- initWithSource:scrollbarScript
1823
- injectionTime:WKUserScriptInjectionTimeAtDocumentEnd
1824
- forMainFrameOnly:NO];
1825
- [config.userContentController addUserScript:scrollScript];
1826
- }
1827
-
1828
- NSView *parentView = (__bridge NSView *)windowHandle;
1829
- win.pImpl->wkWebView =
1830
- [[WKWebView alloc] initWithFrame:parentView.bounds configuration:config];
1831
- [parentView addSubview:win.pImpl->wkWebView];
1832
-
1832
+
1833
+ // Hide scrollbars if disabled
1834
+ if (!win.pImpl->config.scrollbars) {
1835
+ NSString *scrollbarScript =
1836
+ [NSString stringWithUTF8String:kHideScrollbarsScript];
1837
+
1838
+ WKUserScript *scrollScript = [[WKUserScript alloc]
1839
+ initWithSource:scrollbarScript
1840
+ injectionTime:WKUserScriptInjectionTimeAtDocumentEnd
1841
+ forMainFrameOnly:NO];
1842
+ [config.userContentController addUserScript:scrollScript];
1843
+ }
1844
+
1845
+ NSView *parentView = (__bridge NSView *)windowHandle;
1846
+ win.pImpl->wkWebView =
1847
+ [[WKWebView alloc] initWithFrame:parentView.bounds configuration:config];
1848
+ [parentView addSubview:win.pImpl->wkWebView];
1849
+
1833
1850
  win.pImpl->nativeWebView = (__bridge void *)win.pImpl->wkWebView;
1834
1851
 
1835
1852
  // ── macOS key event forwarding ─────────────────────────────────────────────
@@ -1912,8 +1929,20 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1912
1929
  // ── Linux / GTK + WebKit2 WebView ─────────────────────────────────────────
1913
1930
  {
1914
1931
  GtkWidget *gtkParent = static_cast<GtkWidget*>(windowHandle);
1932
+
1933
+ // Disable GPU hardware acceleration — GBM/DRM buffer creation fails under
1934
+ // XWayland. Software rendering works everywhere and is fine for a UI webview.
1935
+ WebKitSettings *wkSettings = webkit_settings_new();
1936
+ webkit_settings_set_hardware_acceleration_policy(
1937
+ wkSettings, WEBKIT_HARDWARE_ACCELERATION_POLICY_NEVER);
1938
+ if (win.pImpl->config.devtools) {
1939
+ webkit_settings_set_enable_developer_extras(wkSettings, TRUE);
1940
+ }
1941
+
1915
1942
  WebKitWebView *webView =
1916
- WEBKIT_WEB_VIEW(webkit_web_view_new());
1943
+ WEBKIT_WEB_VIEW(webkit_web_view_new_with_settings(wkSettings));
1944
+ g_object_unref(wkSettings);
1945
+
1917
1946
  gtk_container_add(GTK_CONTAINER(gtkParent), GTK_WIDGET(webView));
1918
1947
  gtk_widget_show(GTK_WIDGET(webView));
1919
1948
  win.pImpl->gtkWebView = webView;
@@ -1932,12 +1961,260 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1932
1961
  webkit_user_content_manager_add_script(mgr, bridgeScript);
1933
1962
  webkit_user_script_unref(bridgeScript);
1934
1963
 
1964
+ // Register "plusui" as a named message handler so the JS bridge above works.
1965
+ webkit_user_content_manager_register_script_message_handler(mgr, "plusui");
1966
+
1935
1967
  // Helper to run JS in this WebView
1936
1968
  auto runJS = [webView](const std::string& script) {
1937
1969
  webkit_web_view_run_javascript(webView, script.c_str(),
1938
1970
  nullptr, nullptr, nullptr);
1939
1971
  };
1940
1972
 
1973
+ // ── JS → C++ message dispatch ─────────────────────────────────────────────
1974
+ // Mirror the full Windows dispatch so every plusui.window.*, app.*, etc.
1975
+ // call from the frontend reaches the correct C++ handler.
1976
+ struct MsgCtx {
1977
+ WebKitWebView *webView;
1978
+ Window::Impl *impl;
1979
+ };
1980
+ auto *msgCtx = new MsgCtx{webView, win.pImpl.get()};
1981
+
1982
+ // Assign to a typed pointer so G_CALLBACK receives a single token (not a
1983
+ // multi-line lambda — the preprocessor would split on commas otherwise).
1984
+ static auto sMsgHandler = +[](WebKitUserContentManager *,
1985
+ WebKitJavascriptResult *jsResult,
1986
+ gpointer data) -> void {
1987
+ auto *ctx = static_cast<MsgCtx *>(data);
1988
+ Window::Impl *pImpl = ctx->impl;
1989
+
1990
+ JSCValue *jsVal = webkit_javascript_result_get_js_value(jsResult);
1991
+ gchar *raw = jsc_value_to_string(jsVal);
1992
+ if (!raw) return;
1993
+ std::string msg(raw);
1994
+ g_free(raw);
1995
+
1996
+ auto runJSResp = [ctx](const std::string &script) {
1997
+ webkit_web_view_run_javascript(ctx->webView, script.c_str(),
1998
+ nullptr, nullptr, nullptr);
1999
+ };
2000
+
2001
+ // ── simple JSON field extractors (no external dep) ───────────────────
2002
+ auto getField = [&](const std::string &key) -> std::string {
2003
+ size_t pos = msg.find("\"" + key + "\"");
2004
+ if (pos == std::string::npos) return {};
2005
+ pos = msg.find(':', pos); if (pos == std::string::npos) return {};
2006
+ ++pos;
2007
+ while (pos < msg.size() && std::isspace((unsigned char)msg[pos])) ++pos;
2008
+ if (pos >= msg.size()) return {};
2009
+ if (msg[pos] == '"') {
2010
+ size_t s = pos + 1, e = s;
2011
+ while (e < msg.size() && msg[e] != '"') {
2012
+ if (msg[e] == '\\') ++e;
2013
+ ++e;
2014
+ }
2015
+ return msg.substr(s, e - s);
2016
+ }
2017
+ size_t s = pos;
2018
+ while (pos < msg.size() && msg[pos] != ',' && msg[pos] != '}') ++pos;
2019
+ return msg.substr(s, pos - s);
2020
+ };
2021
+
2022
+ auto extractFirstParam = [&]() -> std::string {
2023
+ size_t pa = msg.find("\"params\"");
2024
+ if (pa == std::string::npos) return "null";
2025
+ size_t ar = msg.find('[', pa);
2026
+ if (ar == std::string::npos) return "null";
2027
+ size_t i = ar + 1;
2028
+ while (i < msg.size() && std::isspace((unsigned char)msg[i])) ++i;
2029
+ if (i >= msg.size() || msg[i] == ']') return "null";
2030
+ size_t start = i;
2031
+ if (msg[i] == '"') {
2032
+ ++i;
2033
+ while (i < msg.size()) {
2034
+ if (msg[i] == '\\') { ++i; } else if (msg[i] == '"') { ++i; break; }
2035
+ ++i;
2036
+ }
2037
+ return msg.substr(start, i - start);
2038
+ }
2039
+ int depth = 0;
2040
+ bool inStr = false, esc = false;
2041
+ while (i < msg.size()) {
2042
+ char c = msg[i];
2043
+ if (inStr) { if (esc) esc=false; else if (c=='\\') esc=true; else if (c=='"') inStr=false; }
2044
+ else {
2045
+ if (c=='"') inStr=true;
2046
+ else if (c=='{' || c=='[') ++depth;
2047
+ else if ((c=='}' || c==']') && depth==0) break;
2048
+ else if (c==',' && depth==0) break;
2049
+ else if (c=='}' || c==']') --depth;
2050
+ }
2051
+ ++i;
2052
+ }
2053
+ return msg.substr(start, i - start);
2054
+ };
2055
+
2056
+ auto decodeStr = [](const std::string &s) -> std::string {
2057
+ if (s.size() < 2 || s.front() != '"' || s.back() != '"') return s;
2058
+ std::string out; out.reserve(s.size()-2);
2059
+ for (size_t i = 1; i+1 < s.size(); ++i) {
2060
+ if (s[i]=='\\' && i+2 < s.size()) {
2061
+ char n = s[++i];
2062
+ switch(n){ case '"': out+='"'; break; case '\\': out+='\\'; break;
2063
+ case 'n': out+='\n'; break; case 'r': out+='\r'; break;
2064
+ case 't': out+='\t'; break; default: out+=n; break; }
2065
+ } else { out += s[i]; }
2066
+ }
2067
+ return out;
2068
+ };
2069
+
2070
+ std::string id = getField("id");
2071
+ std::string method = getField("method");
2072
+ std::string result = "null";
2073
+ bool success = false;
2074
+
2075
+ // ── messageCallback (user-level handler) ──────────────────────────────
2076
+ if (pImpl->messageCallback) {
2077
+ pImpl->messageCallback(msg);
2078
+ if (msg.find("\"kind\"") != std::string::npos) {
2079
+ success = true;
2080
+ }
2081
+ }
2082
+
2083
+ // ── window.* ──────────────────────────────────────────────────────────
2084
+ if (!success && method.rfind("window.", 0) == 0) {
2085
+ std::string wm = method.substr(7);
2086
+ success = true;
2087
+ if (wm == "minimize") { if (pImpl->window) pImpl->window->minimize(); }
2088
+ else if (wm == "maximize") { if (pImpl->window) pImpl->window->maximize(); }
2089
+ else if (wm == "restore") { if (pImpl->window) pImpl->window->restore(); }
2090
+ else if (wm == "close") { if (pImpl->window) pImpl->window->close(); }
2091
+ else if (wm == "show") { if (pImpl->window) pImpl->window->show();
2092
+ else { pImpl->state.isHidden=false; pImpl->state.isVisible=true; } }
2093
+ else if (wm == "hide") { if (pImpl->window) pImpl->window->hide();
2094
+ else { pImpl->state.isHidden=true; pImpl->state.isVisible=false; } }
2095
+ else if (wm == "center") { if (pImpl->window) pImpl->window->center(); }
2096
+ else if (wm == "getSize") {
2097
+ int w=0,h=0; if (pImpl->window) pImpl->window->getSize(w,h);
2098
+ result = "{\"width\":" + std::to_string(w) + ",\"height\":" + std::to_string(h) + "}";
2099
+ }
2100
+ else if (wm == "getPosition") {
2101
+ int x=0,y=0; if (pImpl->window) pImpl->window->getPosition(x,y);
2102
+ result = "{\"x\":" + std::to_string(x) + ",\"y\":" + std::to_string(y) + "}";
2103
+ }
2104
+ else if (wm == "setSize") {
2105
+ size_t p1=msg.find('['), p2=msg.find(']');
2106
+ if (p1!=std::string::npos && p2!=std::string::npos) {
2107
+ int w=0,h=0; sscanf(msg.substr(p1+1,p2-p1-1).c_str(),"%d, %d",&w,&h);
2108
+ if (pImpl->window) pImpl->window->setSize(w,h);
2109
+ }
2110
+ }
2111
+ else if (wm == "setPosition") {
2112
+ size_t p1=msg.find('['), p2=msg.find(']');
2113
+ if (p1!=std::string::npos && p2!=std::string::npos) {
2114
+ int x=0,y=0; sscanf(msg.substr(p1+1,p2-p1-1).c_str(),"%d, %d",&x,&y);
2115
+ if (pImpl->window) pImpl->window->setPosition(x,y);
2116
+ }
2117
+ }
2118
+ else if (wm == "setTitle") {
2119
+ size_t p1=msg.find("[\""), p2=msg.find("\"]");
2120
+ if (p1!=std::string::npos && p2!=std::string::npos) {
2121
+ if (pImpl->window) pImpl->window->setTitle(msg.substr(p1+2,p2-p1-2));
2122
+ }
2123
+ }
2124
+ else if (wm == "setFullscreen") {
2125
+ size_t p1=msg.find('['), p2=msg.find(']');
2126
+ if (p1!=std::string::npos && p2!=std::string::npos)
2127
+ if (pImpl->window) pImpl->window->setFullscreen(msg.find("true",p1)!=std::string::npos && msg.find("true",p1)<p2);
2128
+ }
2129
+ else if (wm == "setAlwaysOnTop") {
2130
+ size_t p1=msg.find('['), p2=msg.find(']');
2131
+ if (p1!=std::string::npos && p2!=std::string::npos)
2132
+ if (pImpl->window) pImpl->window->setAlwaysOnTop(msg.find("true",p1)!=std::string::npos && msg.find("true",p1)<p2);
2133
+ }
2134
+ else if (wm == "setResizable") {
2135
+ size_t p1=msg.find('['), p2=msg.find(']');
2136
+ if (p1!=std::string::npos && p2!=std::string::npos)
2137
+ if (pImpl->window) pImpl->window->setResizable(msg.find("true",p1)!=std::string::npos && msg.find("true",p1)<p2);
2138
+ }
2139
+ else if (wm == "isMaximized") { result = (pImpl->window && pImpl->window->isMaximized()) ? "true":"false"; }
2140
+ else if (wm == "isMinimized") { result = (pImpl->window && pImpl->window->isMinimized()) ? "true":"false"; }
2141
+ else if (wm == "isVisible") { result = (pImpl->window && pImpl->window->isVisible()) ? "true":"false"; }
2142
+ else { success = false; }
2143
+ }
2144
+
2145
+ // ── browser.* ─────────────────────────────────────────────────────────
2146
+ if (!success && method.rfind("browser.", 0) == 0) {
2147
+ std::string bm = method.substr(8); success = true;
2148
+ if (bm == "navigate") {
2149
+ size_t p1=msg.find("[\""), p2=msg.find("\"]");
2150
+ if (p1!=std::string::npos && p2!=std::string::npos)
2151
+ webkit_web_view_load_uri(ctx->webView, msg.substr(p1+2,p2-p1-2).c_str());
2152
+ }
2153
+ else if (bm == "goBack") { webkit_web_view_go_back(ctx->webView); }
2154
+ else if (bm == "goForward") { webkit_web_view_go_forward(ctx->webView); }
2155
+ else if (bm == "reload") { webkit_web_view_reload(ctx->webView); }
2156
+ else if (bm == "stop") { webkit_web_view_stop_loading(ctx->webView); }
2157
+ else if (bm == "getUrl") { result = "\"" + pImpl->currentURL + "\""; }
2158
+ else if (bm == "getTitle") { result = "\"" + pImpl->currentTitle + "\""; }
2159
+ else if (bm == "canGoBack") { result = webkit_web_view_can_go_back(ctx->webView) ? "true":"false"; }
2160
+ else if (bm == "canGoForward") { result = webkit_web_view_can_go_forward(ctx->webView) ? "true":"false"; }
2161
+ else { success = false; }
2162
+ }
2163
+
2164
+ // ── app.* ─────────────────────────────────────────────────────────────
2165
+ if (!success && method.rfind("app.", 0) == 0) {
2166
+ std::string am = method.substr(4); success = true;
2167
+ if (am == "quit") { gtk_main_quit(); }
2168
+ else { success = false; }
2169
+ }
2170
+
2171
+ // ── fileDrop.* ────────────────────────────────────────────────────────
2172
+ if (!success && method.rfind("fileDrop.", 0) == 0) {
2173
+ std::string fm = method.substr(9); success = true;
2174
+ if (fm == "setEnabled") { size_t p1=msg.find('['),p2=msg.find(']');
2175
+ if(p1!=std::string::npos&&p2!=std::string::npos)
2176
+ pImpl->config.fileDrop = msg.find("true",p1)!=std::string::npos&&msg.find("true",p1)<p2; }
2177
+ else if (fm == "isEnabled") { result = pImpl->config.fileDrop ? "true":"false"; }
2178
+ else if (fm == "startDrag" || fm == "clearCallbacks") { /* no-op on Linux */ }
2179
+ else { success = false; }
2180
+ }
2181
+
2182
+ // ── webview.* (custom C++ bindings) ───────────────────────────────────
2183
+ if (!success && method.rfind("webview.", 0) == 0) {
2184
+ std::string bn = method.substr(8);
2185
+ auto it = pImpl->bindings.find(bn);
2186
+ if (it != pImpl->bindings.end()) {
2187
+ try {
2188
+ result = it->second(decodeStr(extractFirstParam()));
2189
+ if (result.empty()) result = "null";
2190
+ success = true;
2191
+ } catch (...) { result = "null"; success = true; }
2192
+ }
2193
+ }
2194
+
2195
+ // ── connect.* (custom semantic bindings via bindConnect) ──────────────
2196
+ if (!success && method.rfind("connect.", 0) == 0) {
2197
+ std::string bn = method.substr(8);
2198
+ auto it = pImpl->bindings.find("connect." + bn);
2199
+ if (it == pImpl->bindings.end()) it = pImpl->bindings.find(bn);
2200
+ if (it != pImpl->bindings.end()) {
2201
+ try {
2202
+ result = it->second(decodeStr(extractFirstParam()));
2203
+ if (result.empty()) result = "null";
2204
+ success = true;
2205
+ } catch (...) { result = "null"; success = true; }
2206
+ }
2207
+ }
2208
+
2209
+ // ── send response back to JS ──────────────────────────────────────────
2210
+ if (!id.empty()) {
2211
+ std::string resp = "window.__response__(\"" + id + "\", " + result + ");";
2212
+ runJSResp(resp);
2213
+ }
2214
+ };
2215
+ g_signal_connect(mgr, "script-message-received::plusui",
2216
+ G_CALLBACK(sMsgHandler), msgCtx);
2217
+
1941
2218
  // GDK keyval → PlusUI KeyCode
1942
2219
  auto gdkKeyToPlusUI = [](guint keyval) -> int {
1943
2220
  if (keyval >= GDK_KEY_a && keyval <= GDK_KEY_z)
@@ -2079,423 +2356,423 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
2079
2356
 
2080
2357
  return win;
2081
2358
  }
2082
-
2083
- TrayManager &Window::tray() { return *pImpl->trayManager; }
2084
-
2085
- void Window::setWindow(std::shared_ptr<Window> win) {
2086
- pImpl->window = win;
2087
- if (!win)
2088
- return;
2089
-
2090
- std::weak_ptr<Impl> weak_pImpl = pImpl;
2091
- win->onResize([weak_pImpl](int w, int h) {
2092
- if (auto pImpl = weak_pImpl.lock()) {
2093
- #ifdef _WIN32
2094
- if (pImpl->controller) {
2095
- RECT bounds = {0, 0, w, h};
2096
- pImpl->controller->put_Bounds(bounds);
2097
- }
2098
- #elif defined(__APPLE__)
2099
- if (pImpl->wkWebView) {
2100
- NSView *view = (__bridge NSView *)pImpl->wkWebView;
2101
- NSView *parent = [view superview];
2102
- if (parent) {
2103
- [view setFrame:[parent bounds]];
2104
- }
2105
- }
2106
- #else
2107
- if (pImpl->gtkWebView) {
2108
- // GTK usually handles this if added to a container with expand=TRUE
2109
- // but we can ensure it here if it's a fixed layout parent
2110
- gtk_widget_set_size_request(GTK_WIDGET(pImpl->gtkWebView), w, h);
2111
- }
2112
- #endif
2113
- }
2114
- });
2115
-
2116
- // Trigger initial resize
2117
- int w, h;
2118
- win->getSize(w, h);
2119
- #ifdef _WIN32
2120
- if (pImpl->controller) {
2121
- RECT bounds = {0, 0, w, h};
2122
- pImpl->controller->put_Bounds(bounds);
2123
- }
2124
- #endif
2125
- }
2126
-
2127
- void Window::navigate(const std::string &url) {
2128
- pImpl->currentURL = url;
2129
- pImpl->loading = true;
2130
-
2131
- #ifdef _WIN32
2132
- if (pImpl->ready && pImpl->webview) {
2133
- pImpl->webview->Navigate(std::wstring(url.begin(), url.end()).c_str());
2134
- } else {
2135
- pImpl->pendingNavigation = url;
2136
- }
2137
- #elif defined(__APPLE__)
2138
- if (pImpl->wkWebView) {
2139
- NSURL *nsurl =
2140
- [NSURL URLWithString:[NSString stringWithUTF8String:url.c_str()]];
2141
- NSURLRequest *request = [NSURLRequest requestWithURL:nsurl];
2142
- [pImpl->wkWebView loadRequest:request];
2143
- }
2144
- #else
2145
- if (pImpl->gtkWebView) {
2146
- webkit_web_view_load_uri(pImpl->gtkWebView, url.c_str());
2147
- }
2148
- #endif
2149
- }
2150
-
2151
- void Window::onMessage(MessageCallback callback) {
2152
- pImpl->messageCallback = callback;
2153
- }
2154
-
2155
- void Window::postMessage(const std::string &message) {
2156
- #ifdef _WIN32
2157
- if (pImpl->webview) {
2158
- std::wstring wmsg(message.begin(), message.end());
2159
- pImpl->webview->PostWebMessageAsJson(wmsg.c_str());
2160
- }
2161
- #elif defined(__APPLE__)
2162
- // TODO: formatting for Apple
2163
- #endif
2164
- }
2165
-
2166
- void Window::onFileDrop(FileDropCallback callback) {
2167
- pImpl->fileDropCallback = callback;
2168
- }
2169
-
2170
- void Window::bind(const std::string &name, JSCallback callback) {
2171
- pImpl->bindings[name] = callback;
2172
-
2173
- std::string bridgeScript =
2174
- "window." + name +
2175
- R"( = function(...args) {
2176
- if (window.plusui && typeof window.plusui.invoke === 'function') {
2177
- const payload = args.length === 0 ? null : (args.length === 1 ? args[0] : args);
2178
- return window.plusui.invoke('webview.)" +
2179
- name +
2180
- R"(', [payload]);
2181
- }
2182
- if (window.__invoke__) {
2183
- const payload = args.length === 0 ? null : (args.length === 1 ? args[0] : args);
2184
- return window.__invoke__('webview.)" +
2185
- name +
2186
- R"(', [payload]);
2187
- }
2188
- return Promise.resolve(null);
2189
- };)";
2190
-
2191
- executeScript(bridgeScript);
2192
- }
2193
-
2194
- void Window::unbind(const std::string &name) {
2195
- pImpl->bindings.erase(name);
2196
- executeScript("delete window." + name + ";");
2197
- }
2198
-
2199
- void Window::loadURL(const std::string &url) { navigate(url); }
2200
-
2201
- void Window::loadHTML(const std::string &html) {
2202
- loadHTML(html, "about:blank");
2203
- }
2204
-
2205
- void Window::loadHTML(const std::string &html, const std::string &baseURL) {
2206
- pImpl->loading = true;
2207
-
2208
- #ifdef _WIN32
2209
- (void)baseURL; // Not used on Windows
2210
- if (pImpl->ready && pImpl->webview) {
2211
- pImpl->webview->NavigateToString(
2212
- std::wstring(html.begin(), html.end()).c_str());
2213
- } else {
2214
- pImpl->pendingHTML = html;
2215
- }
2216
- #elif defined(__APPLE__)
2217
- if (pImpl->wkWebView) {
2218
- NSString *htmlString = [NSString stringWithUTF8String:html.c_str()];
2219
- NSURL *base =
2220
- [NSURL URLWithString:[NSString stringWithUTF8String:baseURL.c_str()]];
2221
- [pImpl->wkWebView loadHTMLString:htmlString baseURL:base];
2222
- }
2223
- #else
2224
- if (pImpl->gtkWebView) {
2225
- webkit_web_view_load_html(pImpl->gtkWebView, html.c_str(), baseURL.c_str());
2226
- }
2227
- #endif
2228
- }
2229
-
2230
- void Window::loadFile(const std::string &filePath) {
2231
- std::ifstream file(filePath);
2232
- if (!file) {
2233
- std::cerr << "PlusUI: Failed to open file: " << filePath << std::endl;
2234
- return;
2235
- }
2236
-
2237
- std::stringstream buffer;
2238
- buffer << file.rdbuf();
2239
-
2240
- // Use file:// URL as base for relative paths
2241
- std::string baseURL =
2242
- "file://" + filePath.substr(0, filePath.find_last_of("/\\") + 1);
2243
- loadHTML(buffer.str(), baseURL);
2244
- }
2245
-
2246
- void Window::reload() {
2247
- #ifdef _WIN32
2248
- if (pImpl->webview) {
2249
- pImpl->webview->Reload();
2250
- }
2251
- #elif defined(__APPLE__)
2252
- if (pImpl->wkWebView) {
2253
- [pImpl->wkWebView reload];
2254
- }
2255
- #else
2256
- if (pImpl->gtkWebView) {
2257
- webkit_web_view_reload(pImpl->gtkWebView);
2258
- }
2259
- #endif
2260
- }
2261
-
2262
- void Window::stop() {
2263
- #ifdef _WIN32
2264
- if (pImpl->webview) {
2265
- pImpl->webview->Stop();
2266
- }
2267
- #elif defined(__APPLE__)
2268
- if (pImpl->wkWebView) {
2269
- [pImpl->wkWebView stopLoading];
2270
- }
2271
- #else
2272
- if (pImpl->gtkWebView) {
2273
- webkit_web_view_stop_loading(pImpl->gtkWebView);
2274
- }
2275
- #endif
2276
- }
2277
-
2278
- void Window::goBack() {
2279
- #ifdef _WIN32
2280
- if (pImpl->webview) {
2281
- pImpl->webview->GoBack();
2282
- }
2283
- #elif defined(__APPLE__)
2284
- if (pImpl->wkWebView) {
2285
- [pImpl->wkWebView goBack];
2286
- }
2287
- #else
2288
- if (pImpl->gtkWebView) {
2289
- webkit_web_view_go_back(pImpl->gtkWebView);
2290
- }
2291
- #endif
2292
- }
2293
-
2294
- void Window::goForward() {
2295
- #ifdef _WIN32
2296
- if (pImpl->webview) {
2297
- pImpl->webview->GoForward();
2298
- }
2299
- #elif defined(__APPLE__)
2300
- if (pImpl->wkWebView) {
2301
- [pImpl->wkWebView goForward];
2302
- }
2303
- #else
2304
- if (pImpl->gtkWebView) {
2305
- webkit_web_view_go_forward(pImpl->gtkWebView);
2306
- }
2307
- #endif
2308
- }
2309
-
2310
- bool Window::canGoBack() const {
2311
- #ifdef _WIN32
2312
- if (pImpl->webview) {
2313
- BOOL canGoBack;
2314
- pImpl->webview->get_CanGoBack(&canGoBack);
2315
- return canGoBack;
2316
- }
2317
- #elif defined(__APPLE__)
2318
- if (pImpl->wkWebView) {
2319
- return [pImpl->wkWebView canGoBack];
2320
- }
2321
- #else
2322
- if (pImpl->gtkWebView) {
2323
- return webkit_web_view_can_go_back(pImpl->gtkWebView);
2324
- }
2325
- #endif
2326
- return false;
2327
- }
2328
-
2329
- bool Window::canGoForward() const {
2330
- #ifdef _WIN32
2331
- if (pImpl->webview) {
2332
- BOOL canGoForward;
2333
- pImpl->webview->get_CanGoForward(&canGoForward);
2334
- return canGoForward;
2335
- }
2336
- #elif defined(__APPLE__)
2337
- if (pImpl->wkWebView) {
2338
- return [pImpl->wkWebView canGoForward];
2339
- }
2340
- #else
2341
- if (pImpl->gtkWebView) {
2342
- return webkit_web_view_can_go_forward(pImpl->gtkWebView);
2343
- }
2344
- #endif
2345
- return false;
2346
- }
2347
-
2348
- void Window::executeScript(const std::string &script) {
2349
- #ifdef _WIN32
2350
- if (pImpl->ready && pImpl->webview) {
2351
- pImpl->webview->ExecuteScript(
2352
- std::wstring(script.begin(), script.end()).c_str(), nullptr);
2353
- } else {
2354
- pImpl->pendingScripts.push_back(script);
2355
- }
2356
- #elif defined(__APPLE__)
2357
- if (pImpl->wkWebView) {
2358
- NSString *js = [NSString stringWithUTF8String:script.c_str()];
2359
- [pImpl->wkWebView evaluateJavaScript:js completionHandler:nil];
2360
- }
2361
- #else
2362
- if (pImpl->gtkWebView) {
2363
- webkit_web_view_run_javascript(pImpl->gtkWebView, script.c_str(), nullptr,
2364
- nullptr, nullptr);
2365
- }
2366
- #endif
2367
- }
2368
-
2369
- void Window::executeScript(const std::string &script,
2370
- std::function<void(const std::string &)> callback) {
2371
- #ifdef _WIN32
2372
- if (pImpl->webview) {
2373
- pImpl->webview->ExecuteScript(
2374
- std::wstring(script.begin(), script.end()).c_str(),
2375
- Callback<ICoreWebView2ExecuteScriptCompletedHandler>(
2376
- [callback](HRESULT error, LPCWSTR result) -> HRESULT {
2377
- (void)error; // Suppress unused warning
2378
- if (result && callback) {
2379
- std::wstring wstr(result);
2380
- callback(std::string(wstr.begin(), wstr.end()));
2381
- }
2382
- return S_OK;
2383
- })
2384
- .Get());
2385
- }
2386
- #elif defined(__APPLE__)
2387
- if (pImpl->wkWebView) {
2388
- NSString *js = [NSString stringWithUTF8String:script.c_str()];
2389
- [pImpl->wkWebView evaluateJavaScript:js
2390
- completionHandler:^(id result, NSError *error) {
2391
- if (result && callback) {
2392
- NSString *resultStr =
2393
- [NSString stringWithFormat:@"%@", result];
2394
- callback([resultStr UTF8String]);
2395
- }
2396
- }];
2397
- }
2398
- #else
2399
- if (pImpl->gtkWebView) {
2400
- // GTK WebKit callback handling would go here
2401
- webkit_web_view_run_javascript(pImpl->gtkWebView, script.c_str(), nullptr,
2402
- nullptr, nullptr);
2403
- }
2404
- #endif
2405
- }
2406
-
2407
- void Window::openDevTools() {
2408
- #ifdef _WIN32
2409
- if (pImpl->webview) {
2410
- pImpl->webview->OpenDevToolsWindow();
2411
- }
2412
- #elif defined(__APPLE__)
2413
- // macOS: Dev tools open in Safari's Web Inspector
2414
- #else
2415
- if (pImpl->gtkWebView) {
2416
- WebKitWebInspector *inspector =
2417
- webkit_web_view_get_inspector(pImpl->gtkWebView);
2418
- webkit_web_inspector_show(inspector);
2419
- }
2420
- #endif
2421
- }
2422
-
2423
- void Window::closeDevTools() {
2424
- #ifdef _WIN32
2425
- // WebView2 doesn't have explicit close
2426
- #elif defined(__APPLE__)
2427
- // macOS: Handled by Web Inspector
2428
- #else
2429
- if (pImpl->gtkWebView) {
2430
- WebKitWebInspector *inspector =
2431
- webkit_web_view_get_inspector(pImpl->gtkWebView);
2432
- webkit_web_inspector_close(inspector);
2433
- }
2434
- #endif
2435
- }
2436
-
2437
- void Window::setUserAgent(const std::string &userAgent) {
2438
- pImpl->userAgent = userAgent;
2439
-
2440
- #ifdef _WIN32
2441
- if (pImpl->webview) {
2442
- ComPtr<ICoreWebView2Settings> settings;
2443
- pImpl->webview->get_Settings(&settings);
2444
- // WebView2 user agent requires settings2 interface
2445
- }
2446
- #elif defined(__APPLE__)
2447
- if (pImpl->wkWebView) {
2448
- [pImpl->wkWebView
2449
- setCustomUserAgent:[NSString stringWithUTF8String:userAgent.c_str()]];
2450
- }
2451
- #else
2452
- if (pImpl->gtkWebView) {
2453
- WebKitSettings *settings = webkit_web_view_get_settings(pImpl->gtkWebView);
2454
- webkit_settings_set_user_agent(settings, userAgent.c_str());
2455
- }
2456
- #endif
2457
- }
2458
-
2459
- std::string Window::getUserAgent() const { return pImpl->userAgent; }
2460
-
2461
- void Window::setZoom(double factor) {
2462
- pImpl->zoom = factor;
2463
-
2464
- #ifdef _WIN32
2465
- if (pImpl->controller) {
2466
- pImpl->controller->put_ZoomFactor(factor);
2467
- }
2468
- #elif defined(__APPLE__)
2469
- if (pImpl->wkWebView) {
2470
- [pImpl->wkWebView setPageZoom:factor];
2471
- }
2472
- #else
2473
- if (pImpl->gtkWebView) {
2474
- webkit_web_view_set_zoom_level(pImpl->gtkWebView, factor);
2475
- }
2476
- #endif
2477
- }
2478
-
2479
- double Window::getZoom() const { return pImpl->zoom; }
2480
-
2481
- void Window::injectCSS(const std::string &css) {
2482
- std::string script = R"(
2483
- (function() {
2484
- const style = document.createElement('style');
2485
- style.id = 'plusui-injected-css-' + Date.now();
2486
- style.textContent = `)" +
2487
- css + R"(`;
2488
- if (document.head) {
2489
- document.head.appendChild(style);
2490
- } else {
2491
- document.documentElement.appendChild(style);
2492
- }
2493
- })();
2494
- )";
2495
-
2496
- executeScript(script);
2497
- }
2498
-
2359
+
2360
+ TrayManager &Window::tray() { return *pImpl->trayManager; }
2361
+
2362
+ void Window::setWindow(std::shared_ptr<Window> win) {
2363
+ pImpl->window = win;
2364
+ if (!win)
2365
+ return;
2366
+
2367
+ std::weak_ptr<Impl> weak_pImpl = pImpl;
2368
+ win->onResize([weak_pImpl](int w, int h) {
2369
+ if (auto pImpl = weak_pImpl.lock()) {
2370
+ #ifdef _WIN32
2371
+ if (pImpl->controller) {
2372
+ RECT bounds = {0, 0, w, h};
2373
+ pImpl->controller->put_Bounds(bounds);
2374
+ }
2375
+ #elif defined(__APPLE__)
2376
+ if (pImpl->wkWebView) {
2377
+ NSView *view = (__bridge NSView *)pImpl->wkWebView;
2378
+ NSView *parent = [view superview];
2379
+ if (parent) {
2380
+ [view setFrame:[parent bounds]];
2381
+ }
2382
+ }
2383
+ #else
2384
+ if (pImpl->gtkWebView) {
2385
+ // GTK usually handles this if added to a container with expand=TRUE
2386
+ // but we can ensure it here if it's a fixed layout parent
2387
+ gtk_widget_set_size_request(GTK_WIDGET(pImpl->gtkWebView), w, h);
2388
+ }
2389
+ #endif
2390
+ }
2391
+ });
2392
+
2393
+ // Trigger initial resize
2394
+ int w, h;
2395
+ win->getSize(w, h);
2396
+ #ifdef _WIN32
2397
+ if (pImpl->controller) {
2398
+ RECT bounds = {0, 0, w, h};
2399
+ pImpl->controller->put_Bounds(bounds);
2400
+ }
2401
+ #endif
2402
+ }
2403
+
2404
+ void Window::navigate(const std::string &url) {
2405
+ pImpl->currentURL = url;
2406
+ pImpl->loading = true;
2407
+
2408
+ #ifdef _WIN32
2409
+ if (pImpl->ready && pImpl->webview) {
2410
+ pImpl->webview->Navigate(std::wstring(url.begin(), url.end()).c_str());
2411
+ } else {
2412
+ pImpl->pendingNavigation = url;
2413
+ }
2414
+ #elif defined(__APPLE__)
2415
+ if (pImpl->wkWebView) {
2416
+ NSURL *nsurl =
2417
+ [NSURL URLWithString:[NSString stringWithUTF8String:url.c_str()]];
2418
+ NSURLRequest *request = [NSURLRequest requestWithURL:nsurl];
2419
+ [pImpl->wkWebView loadRequest:request];
2420
+ }
2421
+ #else
2422
+ if (pImpl->gtkWebView) {
2423
+ webkit_web_view_load_uri(pImpl->gtkWebView, url.c_str());
2424
+ }
2425
+ #endif
2426
+ }
2427
+
2428
+ void Window::onMessage(MessageCallback callback) {
2429
+ pImpl->messageCallback = callback;
2430
+ }
2431
+
2432
+ void Window::postMessage(const std::string &message) {
2433
+ #ifdef _WIN32
2434
+ if (pImpl->webview) {
2435
+ std::wstring wmsg(message.begin(), message.end());
2436
+ pImpl->webview->PostWebMessageAsJson(wmsg.c_str());
2437
+ }
2438
+ #elif defined(__APPLE__)
2439
+ // TODO: formatting for Apple
2440
+ #endif
2441
+ }
2442
+
2443
+ void Window::onFileDrop(FileDropCallback callback) {
2444
+ pImpl->fileDropCallback = callback;
2445
+ }
2446
+
2447
+ void Window::bind(const std::string &name, JSCallback callback) {
2448
+ pImpl->bindings[name] = callback;
2449
+
2450
+ std::string bridgeScript =
2451
+ "window." + name +
2452
+ R"( = function(...args) {
2453
+ if (window.plusui && typeof window.plusui.invoke === 'function') {
2454
+ const payload = args.length === 0 ? null : (args.length === 1 ? args[0] : args);
2455
+ return window.plusui.invoke('webview.)" +
2456
+ name +
2457
+ R"(', [payload]);
2458
+ }
2459
+ if (window.__invoke__) {
2460
+ const payload = args.length === 0 ? null : (args.length === 1 ? args[0] : args);
2461
+ return window.__invoke__('webview.)" +
2462
+ name +
2463
+ R"(', [payload]);
2464
+ }
2465
+ return Promise.resolve(null);
2466
+ };)";
2467
+
2468
+ executeScript(bridgeScript);
2469
+ }
2470
+
2471
+ void Window::unbind(const std::string &name) {
2472
+ pImpl->bindings.erase(name);
2473
+ executeScript("delete window." + name + ";");
2474
+ }
2475
+
2476
+ void Window::loadURL(const std::string &url) { navigate(url); }
2477
+
2478
+ void Window::loadHTML(const std::string &html) {
2479
+ loadHTML(html, "about:blank");
2480
+ }
2481
+
2482
+ void Window::loadHTML(const std::string &html, const std::string &baseURL) {
2483
+ pImpl->loading = true;
2484
+
2485
+ #ifdef _WIN32
2486
+ (void)baseURL; // Not used on Windows
2487
+ if (pImpl->ready && pImpl->webview) {
2488
+ pImpl->webview->NavigateToString(
2489
+ std::wstring(html.begin(), html.end()).c_str());
2490
+ } else {
2491
+ pImpl->pendingHTML = html;
2492
+ }
2493
+ #elif defined(__APPLE__)
2494
+ if (pImpl->wkWebView) {
2495
+ NSString *htmlString = [NSString stringWithUTF8String:html.c_str()];
2496
+ NSURL *base =
2497
+ [NSURL URLWithString:[NSString stringWithUTF8String:baseURL.c_str()]];
2498
+ [pImpl->wkWebView loadHTMLString:htmlString baseURL:base];
2499
+ }
2500
+ #else
2501
+ if (pImpl->gtkWebView) {
2502
+ webkit_web_view_load_html(pImpl->gtkWebView, html.c_str(), baseURL.c_str());
2503
+ }
2504
+ #endif
2505
+ }
2506
+
2507
+ void Window::loadFile(const std::string &filePath) {
2508
+ std::ifstream file(filePath);
2509
+ if (!file) {
2510
+ std::cerr << "PlusUI: Failed to open file: " << filePath << std::endl;
2511
+ return;
2512
+ }
2513
+
2514
+ std::stringstream buffer;
2515
+ buffer << file.rdbuf();
2516
+
2517
+ // Use file:// URL as base for relative paths
2518
+ std::string baseURL =
2519
+ "file://" + filePath.substr(0, filePath.find_last_of("/\\") + 1);
2520
+ loadHTML(buffer.str(), baseURL);
2521
+ }
2522
+
2523
+ void Window::reload() {
2524
+ #ifdef _WIN32
2525
+ if (pImpl->webview) {
2526
+ pImpl->webview->Reload();
2527
+ }
2528
+ #elif defined(__APPLE__)
2529
+ if (pImpl->wkWebView) {
2530
+ [pImpl->wkWebView reload];
2531
+ }
2532
+ #else
2533
+ if (pImpl->gtkWebView) {
2534
+ webkit_web_view_reload(pImpl->gtkWebView);
2535
+ }
2536
+ #endif
2537
+ }
2538
+
2539
+ void Window::stop() {
2540
+ #ifdef _WIN32
2541
+ if (pImpl->webview) {
2542
+ pImpl->webview->Stop();
2543
+ }
2544
+ #elif defined(__APPLE__)
2545
+ if (pImpl->wkWebView) {
2546
+ [pImpl->wkWebView stopLoading];
2547
+ }
2548
+ #else
2549
+ if (pImpl->gtkWebView) {
2550
+ webkit_web_view_stop_loading(pImpl->gtkWebView);
2551
+ }
2552
+ #endif
2553
+ }
2554
+
2555
+ void Window::goBack() {
2556
+ #ifdef _WIN32
2557
+ if (pImpl->webview) {
2558
+ pImpl->webview->GoBack();
2559
+ }
2560
+ #elif defined(__APPLE__)
2561
+ if (pImpl->wkWebView) {
2562
+ [pImpl->wkWebView goBack];
2563
+ }
2564
+ #else
2565
+ if (pImpl->gtkWebView) {
2566
+ webkit_web_view_go_back(pImpl->gtkWebView);
2567
+ }
2568
+ #endif
2569
+ }
2570
+
2571
+ void Window::goForward() {
2572
+ #ifdef _WIN32
2573
+ if (pImpl->webview) {
2574
+ pImpl->webview->GoForward();
2575
+ }
2576
+ #elif defined(__APPLE__)
2577
+ if (pImpl->wkWebView) {
2578
+ [pImpl->wkWebView goForward];
2579
+ }
2580
+ #else
2581
+ if (pImpl->gtkWebView) {
2582
+ webkit_web_view_go_forward(pImpl->gtkWebView);
2583
+ }
2584
+ #endif
2585
+ }
2586
+
2587
+ bool Window::canGoBack() const {
2588
+ #ifdef _WIN32
2589
+ if (pImpl->webview) {
2590
+ BOOL canGoBack;
2591
+ pImpl->webview->get_CanGoBack(&canGoBack);
2592
+ return canGoBack;
2593
+ }
2594
+ #elif defined(__APPLE__)
2595
+ if (pImpl->wkWebView) {
2596
+ return [pImpl->wkWebView canGoBack];
2597
+ }
2598
+ #else
2599
+ if (pImpl->gtkWebView) {
2600
+ return webkit_web_view_can_go_back(pImpl->gtkWebView);
2601
+ }
2602
+ #endif
2603
+ return false;
2604
+ }
2605
+
2606
+ bool Window::canGoForward() const {
2607
+ #ifdef _WIN32
2608
+ if (pImpl->webview) {
2609
+ BOOL canGoForward;
2610
+ pImpl->webview->get_CanGoForward(&canGoForward);
2611
+ return canGoForward;
2612
+ }
2613
+ #elif defined(__APPLE__)
2614
+ if (pImpl->wkWebView) {
2615
+ return [pImpl->wkWebView canGoForward];
2616
+ }
2617
+ #else
2618
+ if (pImpl->gtkWebView) {
2619
+ return webkit_web_view_can_go_forward(pImpl->gtkWebView);
2620
+ }
2621
+ #endif
2622
+ return false;
2623
+ }
2624
+
2625
+ void Window::executeScript(const std::string &script) {
2626
+ #ifdef _WIN32
2627
+ if (pImpl->ready && pImpl->webview) {
2628
+ pImpl->webview->ExecuteScript(
2629
+ std::wstring(script.begin(), script.end()).c_str(), nullptr);
2630
+ } else {
2631
+ pImpl->pendingScripts.push_back(script);
2632
+ }
2633
+ #elif defined(__APPLE__)
2634
+ if (pImpl->wkWebView) {
2635
+ NSString *js = [NSString stringWithUTF8String:script.c_str()];
2636
+ [pImpl->wkWebView evaluateJavaScript:js completionHandler:nil];
2637
+ }
2638
+ #else
2639
+ if (pImpl->gtkWebView) {
2640
+ webkit_web_view_run_javascript(pImpl->gtkWebView, script.c_str(), nullptr,
2641
+ nullptr, nullptr);
2642
+ }
2643
+ #endif
2644
+ }
2645
+
2646
+ void Window::executeScript(const std::string &script,
2647
+ std::function<void(const std::string &)> callback) {
2648
+ #ifdef _WIN32
2649
+ if (pImpl->webview) {
2650
+ pImpl->webview->ExecuteScript(
2651
+ std::wstring(script.begin(), script.end()).c_str(),
2652
+ Callback<ICoreWebView2ExecuteScriptCompletedHandler>(
2653
+ [callback](HRESULT error, LPCWSTR result) -> HRESULT {
2654
+ (void)error; // Suppress unused warning
2655
+ if (result && callback) {
2656
+ std::wstring wstr(result);
2657
+ callback(std::string(wstr.begin(), wstr.end()));
2658
+ }
2659
+ return S_OK;
2660
+ })
2661
+ .Get());
2662
+ }
2663
+ #elif defined(__APPLE__)
2664
+ if (pImpl->wkWebView) {
2665
+ NSString *js = [NSString stringWithUTF8String:script.c_str()];
2666
+ [pImpl->wkWebView evaluateJavaScript:js
2667
+ completionHandler:^(id result, NSError *error) {
2668
+ if (result && callback) {
2669
+ NSString *resultStr =
2670
+ [NSString stringWithFormat:@"%@", result];
2671
+ callback([resultStr UTF8String]);
2672
+ }
2673
+ }];
2674
+ }
2675
+ #else
2676
+ if (pImpl->gtkWebView) {
2677
+ // GTK WebKit callback handling would go here
2678
+ webkit_web_view_run_javascript(pImpl->gtkWebView, script.c_str(), nullptr,
2679
+ nullptr, nullptr);
2680
+ }
2681
+ #endif
2682
+ }
2683
+
2684
+ void Window::openDevTools() {
2685
+ #ifdef _WIN32
2686
+ if (pImpl->webview) {
2687
+ pImpl->webview->OpenDevToolsWindow();
2688
+ }
2689
+ #elif defined(__APPLE__)
2690
+ // macOS: Dev tools open in Safari's Web Inspector
2691
+ #else
2692
+ if (pImpl->gtkWebView) {
2693
+ WebKitWebInspector *inspector =
2694
+ webkit_web_view_get_inspector(pImpl->gtkWebView);
2695
+ webkit_web_inspector_show(inspector);
2696
+ }
2697
+ #endif
2698
+ }
2699
+
2700
+ void Window::closeDevTools() {
2701
+ #ifdef _WIN32
2702
+ // WebView2 doesn't have explicit close
2703
+ #elif defined(__APPLE__)
2704
+ // macOS: Handled by Web Inspector
2705
+ #else
2706
+ if (pImpl->gtkWebView) {
2707
+ WebKitWebInspector *inspector =
2708
+ webkit_web_view_get_inspector(pImpl->gtkWebView);
2709
+ webkit_web_inspector_close(inspector);
2710
+ }
2711
+ #endif
2712
+ }
2713
+
2714
+ void Window::setUserAgent(const std::string &userAgent) {
2715
+ pImpl->userAgent = userAgent;
2716
+
2717
+ #ifdef _WIN32
2718
+ if (pImpl->webview) {
2719
+ ComPtr<ICoreWebView2Settings> settings;
2720
+ pImpl->webview->get_Settings(&settings);
2721
+ // WebView2 user agent requires settings2 interface
2722
+ }
2723
+ #elif defined(__APPLE__)
2724
+ if (pImpl->wkWebView) {
2725
+ [pImpl->wkWebView
2726
+ setCustomUserAgent:[NSString stringWithUTF8String:userAgent.c_str()]];
2727
+ }
2728
+ #else
2729
+ if (pImpl->gtkWebView) {
2730
+ WebKitSettings *settings = webkit_web_view_get_settings(pImpl->gtkWebView);
2731
+ webkit_settings_set_user_agent(settings, userAgent.c_str());
2732
+ }
2733
+ #endif
2734
+ }
2735
+
2736
+ std::string Window::getUserAgent() const { return pImpl->userAgent; }
2737
+
2738
+ void Window::setZoom(double factor) {
2739
+ pImpl->zoom = factor;
2740
+
2741
+ #ifdef _WIN32
2742
+ if (pImpl->controller) {
2743
+ pImpl->controller->put_ZoomFactor(factor);
2744
+ }
2745
+ #elif defined(__APPLE__)
2746
+ if (pImpl->wkWebView) {
2747
+ [pImpl->wkWebView setPageZoom:factor];
2748
+ }
2749
+ #else
2750
+ if (pImpl->gtkWebView) {
2751
+ webkit_web_view_set_zoom_level(pImpl->gtkWebView, factor);
2752
+ }
2753
+ #endif
2754
+ }
2755
+
2756
+ double Window::getZoom() const { return pImpl->zoom; }
2757
+
2758
+ void Window::injectCSS(const std::string &css) {
2759
+ std::string script = R"(
2760
+ (function() {
2761
+ const style = document.createElement('style');
2762
+ style.id = 'plusui-injected-css-' + Date.now();
2763
+ style.textContent = `)" +
2764
+ css + R"(`;
2765
+ if (document.head) {
2766
+ document.head.appendChild(style);
2767
+ } else {
2768
+ document.documentElement.appendChild(style);
2769
+ }
2770
+ })();
2771
+ )";
2772
+
2773
+ executeScript(script);
2774
+ }
2775
+
2499
2776
  void Window::setWebviewDragDropEnabled(bool enabled) {
2500
2777
  if (enabled) {
2501
2778
  // Remove the dropzone init so it can be re-applied if needed later
@@ -2565,33 +2842,33 @@ void Window::setWebviewDragDropEnabled(bool enabled) {
2565
2842
  )");
2566
2843
  }
2567
2844
  }
2568
-
2569
- void Window::onNavigationStart(NavigationCallback callback) {
2570
- pImpl->navigationCallback = callback;
2571
- }
2572
-
2573
- void Window::onNavigationComplete(LoadCallback callback) {
2574
- pImpl->navigationCompleteCallback = callback;
2575
- }
2576
-
2577
- void Window::onLoadStart(LoadCallback callback) {
2578
- pImpl->loadStartCallback = callback;
2579
- }
2580
-
2581
- void Window::onLoadEnd(LoadCallback callback) {
2582
- pImpl->loadEndCallback = callback;
2583
- }
2584
-
2585
- void Window::onLoadError(ErrorCallback callback) {
2586
- pImpl->errorCallback = callback;
2587
- }
2588
-
2589
- void Window::onConsoleMessage(ConsoleCallback callback) {
2590
- pImpl->consoleCallback = callback;
2591
- }
2592
-
2593
- bool Window::isLoading() const { return pImpl->loading; }
2594
-
2595
- std::string Window::getURL() const { return pImpl->currentURL; }
2596
-
2597
- } // namespace plusui
2845
+
2846
+ void Window::onNavigationStart(NavigationCallback callback) {
2847
+ pImpl->navigationCallback = callback;
2848
+ }
2849
+
2850
+ void Window::onNavigationComplete(LoadCallback callback) {
2851
+ pImpl->navigationCompleteCallback = callback;
2852
+ }
2853
+
2854
+ void Window::onLoadStart(LoadCallback callback) {
2855
+ pImpl->loadStartCallback = callback;
2856
+ }
2857
+
2858
+ void Window::onLoadEnd(LoadCallback callback) {
2859
+ pImpl->loadEndCallback = callback;
2860
+ }
2861
+
2862
+ void Window::onLoadError(ErrorCallback callback) {
2863
+ pImpl->errorCallback = callback;
2864
+ }
2865
+
2866
+ void Window::onConsoleMessage(ConsoleCallback callback) {
2867
+ pImpl->consoleCallback = callback;
2868
+ }
2869
+
2870
+ bool Window::isLoading() const { return pImpl->loading; }
2871
+
2872
+ std::string Window::getURL() const { return pImpl->currentURL; }
2873
+
2874
+ } // namespace plusui