plusui-native-core 0.1.105 → 0.1.107

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 (77) hide show
  1. package/Core/.cache/clangd/index/api.hpp.016B34C8046EF490.idx +0 -0
  2. package/Core/.cache/clangd/index/app.cpp.1E6FAF043D496421.idx +0 -0
  3. package/Core/.cache/clangd/index/app.hpp.FA0E5D412C4E6148.idx +0 -0
  4. package/Core/.cache/clangd/index/browser.cpp.9461A2CAF129F1D9.idx +0 -0
  5. package/Core/.cache/clangd/index/browser.hpp.BE40AE80881B3107.idx +0 -0
  6. package/Core/.cache/clangd/index/clipboard.cpp.2399913537B2A7AD.idx +0 -0
  7. package/Core/.cache/clangd/index/clipboard.hpp.C1095DDACD7149E9.idx +0 -0
  8. package/Core/.cache/clangd/index/connect.cpp.518C66C7C28B30A9.idx +0 -0
  9. package/Core/.cache/clangd/index/connect.hpp.08E2F7CD13B78601.idx +0 -0
  10. package/Core/.cache/clangd/index/connection.hpp.849FAEF1523BF2C3.idx +0 -0
  11. package/Core/.cache/clangd/index/display.cpp.F6F6D932BF9F8D8E.idx +0 -0
  12. package/Core/.cache/clangd/index/display.hpp.0C1A9CAD11EE4404.idx +0 -0
  13. package/Core/.cache/clangd/index/filedrop.cpp.669B524B3C501C52.idx +0 -0
  14. package/Core/.cache/clangd/index/filedrop.hpp.48460099C3F35F2D.idx +0 -0
  15. package/Core/.cache/clangd/index/keyboard.cpp.DC6D34E4A4F798DD.idx +0 -0
  16. package/Core/.cache/clangd/index/keyboard.hpp.F016CB68D7DE5A46.idx +0 -0
  17. package/Core/.cache/clangd/index/keyboard_linux.cpp.B403FDCEA7A6CA53.idx +0 -0
  18. package/Core/.cache/clangd/index/menu.cpp.3059F08D8D2DF265.idx +0 -0
  19. package/Core/.cache/clangd/index/menu.hpp.8716DCCC573910D4.idx +0 -0
  20. package/Core/.cache/clangd/index/plusui.hpp.8CFCDFDC2E3F41DD.idx +0 -0
  21. package/Core/.cache/clangd/index/router.cpp.EAC9EAD34C59E573.idx +0 -0
  22. package/Core/.cache/clangd/index/router.hpp.2B06E2EE9998468D.idx +0 -0
  23. package/Core/.cache/clangd/index/stb_image.h.E26CF48FE089A0D3.idx +0 -0
  24. package/Core/.cache/clangd/index/tray.cpp.92F244E7E1D7F0EC.idx +0 -0
  25. package/Core/.cache/clangd/index/tray.hpp.0D881B0601BBBD25.idx +0 -0
  26. package/Core/.cache/clangd/index/webgpu.cpp.FC656FA8BE10FE15.idx +0 -0
  27. package/Core/.cache/clangd/index/webgpu.hpp.5AF1A5E9DF9E5AE0.idx +0 -0
  28. package/Core/.cache/clangd/index/window.cpp.191D8C9ADF874B22.idx +0 -0
  29. package/Core/.cache/clangd/index/window.hpp.B9811B43AA295697.idx +0 -0
  30. package/Core/.claude/settings.local.json +7 -0
  31. package/Core/CMakeLists.txt +1 -1
  32. package/Core/Features/API/app-api.ts +28 -28
  33. package/Core/Features/API/browser-api.ts +38 -38
  34. package/Core/Features/API/clipboard-api.ts +21 -21
  35. package/Core/Features/API/display-api.ts +33 -33
  36. package/Core/Features/API/keyboard-api.ts +33 -33
  37. package/Core/Features/API/menu-api.ts +39 -39
  38. package/Core/Features/API/router-api.ts +23 -23
  39. package/Core/Features/API/tray-api.ts +22 -22
  40. package/Core/Features/API/webgpu-api.ts +55 -55
  41. package/Core/Features/App/app.cpp +135 -102
  42. package/Core/Features/Browser/browser.cpp +227 -227
  43. package/Core/Features/Browser/browser.ts +161 -161
  44. package/Core/Features/Clipboard/clipboard.cpp +235 -235
  45. package/Core/Features/Display/display.cpp +212 -212
  46. package/Core/Features/FileDrop/filedrop.cpp +448 -324
  47. package/Core/Features/FileDrop/filedrop.css +421 -421
  48. package/Core/Features/FileDrop/filedrop.ts +0 -7
  49. package/Core/Features/Keyboard/keyboard_linux.cpp +4 -0
  50. package/Core/Features/Router/router.cpp +62 -62
  51. package/Core/Features/Router/router.ts +113 -113
  52. package/Core/Features/Tray/tray.cpp +437 -324
  53. package/Core/Features/WebGPU/webgpu.cpp +948 -948
  54. package/Core/Features/Window/webview.cpp +1009 -1009
  55. package/Core/Features/Window/webview.ts +516 -516
  56. package/Core/Features/Window/window.cpp +2240 -1986
  57. package/Core/include/plusui/api.hpp +237 -237
  58. package/Core/include/plusui/app.hpp +36 -34
  59. package/Core/include/plusui/browser.hpp +67 -67
  60. package/Core/include/plusui/clipboard.hpp +41 -41
  61. package/Core/include/plusui/connect.hpp +340 -340
  62. package/Core/include/plusui/connection.hpp +3 -3
  63. package/Core/include/plusui/display.hpp +90 -90
  64. package/Core/include/plusui/filedrop.hpp +92 -77
  65. package/Core/include/plusui/keyboard.hpp +112 -112
  66. package/Core/include/plusui/menu.hpp +153 -153
  67. package/Core/include/plusui/plusui +18 -18
  68. package/Core/include/plusui/router.hpp +42 -42
  69. package/Core/include/plusui/tray.hpp +94 -94
  70. package/Core/include/plusui/webgpu.hpp +434 -434
  71. package/Core/include/plusui/window.hpp +180 -177
  72. package/Core/scripts/generate-umbrella-header.mjs +77 -77
  73. package/Core/vendor/WebView2EnvironmentOptions.h +406 -406
  74. package/Core/vendor/webview.h +487 -487
  75. package/Core/vendor/webview2.h +52079 -52079
  76. package/README.md +19 -19
  77. 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,200 +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
- // Clear any leftover visual feedback from the drag
350
- "document.querySelectorAll('.dropzone-active,.filedrop-active')"
351
- ".forEach(function(z){z.classList.remove('dropzone-active');"
352
- "z.classList.remove('filedrop-active');});"
353
- // Global event — always fires
354
- "window.dispatchEvent(new "
355
- "CustomEvent('plusui:fileDrop.filesDropped',"
356
- " { detail: { files: files } }));"
357
- // Zone-specific delivery via DPI-corrected hit test
358
- "var el=document.elementFromPoint(" +
359
- std::to_string(dpx) + "," + std::to_string(dpy) +
360
- ");"
361
- "var zone=el&&el.closest?el.closest('[data-dropzone]'):null;"
362
- "var zoneName=zone?zone.getAttribute('data-dropzone'):null;"
363
- "if(zoneName&&window.__plusui_fileDrop__){"
364
- "window.__plusui_fileDrop__(zoneName,files);"
365
- "}"
366
- // If no zone matched deliver to all registered zones so a
367
- // single-zone app always works regardless of hit-test accuracy.
368
- "if(!zoneName&&window.__plusui_fileDrop_default__){"
369
- "window.__plusui_fileDrop_default__(files);"
370
- "}"
371
- "})();";
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
+ }
372
252
 
373
- targetImpl->webview->ExecuteScript(
374
- std::wstring(eventScript.begin(), eventScript.end()).c_str(),
375
- nullptr);
376
- return 0;
377
- }
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;
378
277
  case WM_HOTKEY: {
379
278
  int hotKeyId = (int)wp;
380
279
  auto it = impl->hotkeys.find(hotKeyId);
@@ -439,683 +338,719 @@ struct Window::Impl {
439
338
  }
440
339
  return DefWindowProc(hwnd, msg, wp, lp);
441
340
  }
442
- #elif defined(__APPLE__)
443
- WKWebView *wkWebView = nullptr;
444
- #else
445
- WebKitWebView *gtkWebView = nullptr;
446
- #endif
447
- };
448
-
449
- Window::Window() : pImpl(std::shared_ptr<Impl>(new Impl())) {}
450
-
451
- #ifdef _WIN32
452
- std::map<HWND, Window::Impl *> Window::Impl::embeddedWebviewByParent;
453
- #endif
454
-
455
- Window::~Window() = default;
456
-
457
- Window::Window(Window &&other) noexcept = default;
458
- Window &Window::operator=(Window &&other) noexcept = default;
459
-
460
- Window Window::create(const WindowConfig &config) {
461
- Window w;
462
- w.pImpl->config = config;
463
-
464
- #ifdef _WIN32
465
- WNDCLASSEXW wc = {};
466
- wc.cbSize = sizeof(WNDCLASSEXW);
467
- wc.lpfnWndProc = Impl::wndProc;
468
- wc.hInstance = GetModuleHandle(nullptr);
469
- wc.lpszClassName = L"PLUSUI_WINDOW";
470
- wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
471
- wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
472
- RegisterClassExW(&wc);
473
-
474
- DWORD style = 0;
475
- DWORD exStyle = 0;
476
-
477
- if (config.decorations) {
478
- style = WS_OVERLAPPEDWINDOW;
479
- if (!config.resizable)
480
- style &= ~WS_THICKFRAME;
481
- if (!config.minimizable)
482
- style &= ~WS_MINIMIZEBOX;
483
- if (!config.closable)
484
- style &= ~WS_SYSMENU;
485
- } else {
486
- // Frameless window
487
- style = WS_POPUP;
488
- if (config.resizable)
489
- style |= WS_THICKFRAME;
490
- }
491
-
492
- if (config.transparent) {
493
- exStyle = WS_EX_LAYERED;
494
- }
495
-
496
- if (config.skipTaskbar) {
497
- exStyle |= WS_EX_TOOLWINDOW;
498
- }
499
-
500
- std::wstring wideTitle(config.title.begin(), config.title.end());
501
- w.pImpl->hwnd = CreateWindowExW(
502
- exStyle, L"PLUSUI_WINDOW", wideTitle.c_str(), style,
503
- config.x >= 0 ? config.x : CW_USEDEFAULT,
504
- config.y >= 0 ? config.y : CW_USEDEFAULT, config.width, config.height,
505
- nullptr, nullptr, GetModuleHandle(nullptr), w.pImpl.get());
506
-
507
- w.pImpl->nativeWindow = (void *)w.pImpl->hwnd;
508
- w.pImpl->state.width = config.width;
509
- w.pImpl->state.height = config.height;
510
-
511
- DragAcceptFiles(w.pImpl->hwnd, config.fileDrop ? TRUE : FALSE);
512
-
513
- if (config.center) {
514
- RECT screen;
515
- SystemParametersInfo(SPI_GETWORKAREA, 0, &screen, 0);
516
- int x = (screen.right - config.width) / 2;
517
- int y = (screen.bottom - config.height) / 2;
518
- SetWindowPos(w.pImpl->hwnd, nullptr, x, y, 0, 0, SWP_NOZORDER | SWP_NOSIZE);
519
- }
520
-
521
- if (config.alwaysOnTop) {
522
- SetWindowPos(w.pImpl->hwnd, HWND_TOPMOST, 0, 0, 0, 0,
523
- SWP_NOMOVE | SWP_NOSIZE);
524
- }
525
-
526
- #elif defined(__APPLE__)
527
- NSWindow *nswin = [[NSWindow alloc]
528
- initWithContentRect:NSMakeRect(config.x >= 0 ? config.x : 100,
529
- config.y >= 0 ? config.y : 100,
530
- config.width, config.height)
531
- styleMask:(config.resizable ? NSWindowStyleMaskResizable : 0) |
532
- NSWindowStyleMaskTitled | NSWindowStyleMaskClosable |
533
- (config.minimizable ? NSWindowStyleMaskMiniaturizable
534
- : 0)backing:NSBackingStoreBuffered
535
- defer:NO];
536
-
537
- [nswin setTitle:[NSString stringWithUTF8String:config.title.c_str()]];
538
- [nswin setReleasedWhenClosed:NO];
539
-
540
- if (config.center) {
541
- [nswin center];
542
- }
543
-
544
- if (config.alwaysOnTop) {
545
- [nswin setLevel:NSFloatingWindowLevel];
546
- }
547
-
548
- w.pImpl->nativeWindow = (__bridge void *)nswin;
549
-
550
- #else
551
- gtk_init_check(0, nullptr);
552
-
553
- GtkWindow *gtkwin = GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL));
554
- gtk_window_set_title(gtkwin, config.title.c_str());
555
- gtk_window_set_default_size(gtkwin, config.width, config.height);
556
-
557
- if (config.x >= 0 && config.y >= 0) {
558
- gtk_window_move(gtkwin, config.x, config.y);
559
- } else if (config.center) {
560
- gtk_window_set_position(gtkwin, GTK_WIN_POS_CENTER);
561
- }
562
-
563
- if (!config.resizable) {
564
- gtk_window_set_resizable(gtkwin, FALSE);
565
- }
566
-
567
- if (config.alwaysOnTop) {
568
- gtk_window_set_keep_above(gtkwin, TRUE);
569
- }
570
-
571
- w.pImpl->nativeWindow = (void *)gtkwin;
572
- #endif
573
-
574
- // Initialize tray manager for native windows
575
- w.pImpl->trayManager = std::make_unique<TrayManager>();
576
- #ifdef _WIN32
577
- if (w.pImpl->hwnd) {
578
- w.pImpl->trayManager->setWindowHandle((void *)w.pImpl->hwnd);
579
- }
580
- #elif defined(__APPLE__)
581
- w.pImpl->trayManager->setWindowHandle(w.pImpl->nativeWindow);
582
- #else
583
- w.pImpl->trayManager->setWindowHandle(w.pImpl->nativeWindow);
584
- #endif
585
-
586
- return w;
587
- }
588
-
589
- void Window::setTitle(const std::string &title) {
590
- pImpl->config.title = title;
591
- #ifdef _WIN32
592
- if (pImpl->hwnd) {
593
- std::wstring wideTitle(title.begin(), title.end());
594
- SetWindowTextW(pImpl->hwnd, wideTitle.c_str());
595
- }
596
- #elif defined(__APPLE__)
597
- if (pImpl->nativeWindow) {
598
- NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
599
- [nswin setTitle:[NSString stringWithUTF8String:title.c_str()]];
600
- }
601
- #else
602
- if (pImpl->nativeWindow) {
603
- gtk_window_set_title(GTK_WINDOW(pImpl->nativeWindow), title.c_str());
604
- }
605
- #endif
606
- }
607
-
608
- std::string Window::getTitle() const {
609
- if (!pImpl->currentTitle.empty())
610
- return pImpl->currentTitle;
611
- return pImpl->config.title;
612
- }
613
-
614
- void Window::setSize(int width, int height) {
615
- pImpl->config.width = width;
616
- pImpl->config.height = height;
617
- #ifdef _WIN32
618
- if (pImpl->hwnd)
619
- SetWindowPos(pImpl->hwnd, nullptr, 0, 0, width, height,
620
- SWP_NOZORDER | SWP_NOMOVE);
621
- #elif defined(__APPLE__)
622
- if (pImpl->nativeWindow) {
623
- NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
624
- [nswin setContentSize:NSMakeSize(width, height)];
625
- }
626
- #else
627
- if (pImpl->nativeWindow) {
628
- gtk_window_resize(GTK_WINDOW(pImpl->nativeWindow), width, height);
629
- }
630
- #endif
631
- }
632
-
633
- void Window::getSize(int &width, int &height) const {
634
- width = pImpl->state.width;
635
- height = pImpl->state.height;
636
- }
637
-
638
- void Window::setMinSize(int minWidth, int minHeight) {
639
- pImpl->config.minWidth = minWidth;
640
- pImpl->config.minHeight = minHeight;
641
- #ifdef _WIN32
642
- if (pImpl->hwnd) {
643
- SetWindowLongPtr(pImpl->hwnd, GWL_STYLE,
644
- GetWindowLong(pImpl->hwnd, GWL_STYLE) | WS_THICKFRAME);
645
- }
646
- #endif
647
- }
648
-
649
- void Window::setMaxSize(int maxWidth, int maxHeight) {
650
- pImpl->config.maxWidth = maxWidth;
651
- pImpl->config.maxHeight = maxHeight;
652
- }
653
-
654
- void Window::setPosition(int x, int y) {
655
- pImpl->config.x = x;
656
- pImpl->config.y = y;
657
- #ifdef _WIN32
658
- if (pImpl->hwnd)
659
- SetWindowPos(pImpl->hwnd, nullptr, x, y, 0, 0, SWP_NOZORDER | SWP_NOSIZE);
660
- #elif defined(__APPLE__)
661
- if (pImpl->nativeWindow) {
662
- NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
663
- [nswin setFrameOrigin:NSMakePoint(x, y)];
664
- }
665
- #else
666
- if (pImpl->nativeWindow) {
667
- gtk_window_move(GTK_WINDOW(pImpl->nativeWindow), x, y);
668
- }
669
- #endif
670
- }
671
-
672
- void Window::getPosition(int &x, int &y) const {
673
- x = pImpl->state.x;
674
- y = pImpl->state.y;
675
- }
676
-
677
- void Window::center() {
678
- #ifdef _WIN32
679
- if (pImpl->hwnd) {
680
- RECT rc, screen;
681
- GetWindowRect(pImpl->hwnd, &rc);
682
- SystemParametersInfo(SPI_GETWORKAREA, 0, &screen, 0);
683
- int x = (screen.right - (rc.right - rc.left)) / 2;
684
- int y = (screen.bottom - (rc.bottom - rc.top)) / 2;
685
- SetWindowPos(pImpl->hwnd, nullptr, x, y, 0, 0, SWP_NOZORDER | SWP_NOSIZE);
686
- }
687
- #elif defined(__APPLE__)
688
- if (pImpl->nativeWindow) {
689
- NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
690
- [nswin center];
691
- }
692
- #else
693
- if (pImpl->nativeWindow) {
694
- gtk_window_set_position(GTK_WINDOW(pImpl->nativeWindow),
695
- GTK_WIN_POS_CENTER);
696
- }
697
- #endif
698
- }
699
-
700
- void Window::setFullscreen(bool enabled) {
701
- bool wasFullscreen = pImpl->state.isFullscreen;
702
- pImpl->config.fullscreen = enabled;
703
- pImpl->state.isFullscreen = enabled;
704
- pImpl->state.isHidden = false;
705
-
706
- #ifdef _WIN32
707
- if (pImpl->hwnd) {
708
- if (enabled) {
709
- SetWindowLongPtr(pImpl->hwnd, GWL_STYLE,
710
- GetWindowLong(pImpl->hwnd, GWL_STYLE) &
711
- ~WS_OVERLAPPEDWINDOW);
712
- SetWindowPos(pImpl->hwnd, HWND_TOP, 0, 0, GetSystemMetrics(SM_CXSCREEN),
713
- GetSystemMetrics(SM_CYSCREEN), SWP_SHOWWINDOW);
714
- } else {
715
- SetWindowLongPtr(pImpl->hwnd, GWL_STYLE,
716
- GetWindowLong(pImpl->hwnd, GWL_STYLE) |
717
- WS_OVERLAPPEDWINDOW);
718
- SetWindowPos(pImpl->hwnd, nullptr, 0, 0, 0, 0,
719
- SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED);
720
- }
721
- }
722
- #elif defined(__APPLE__)
723
- if (pImpl->nativeWindow) {
724
- NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
725
- if (enabled) {
726
- [nswin toggleFullScreen:nil];
727
- }
728
- }
729
- #else
730
- if (pImpl->nativeWindow) {
731
- if (enabled) {
732
- gtk_window_fullscreen(GTK_WINDOW(pImpl->nativeWindow));
733
- } else {
734
- gtk_window_unfullscreen(GTK_WINDOW(pImpl->nativeWindow));
735
- }
736
- }
737
- #endif
738
-
739
- if (wasFullscreen != enabled) {
740
- for (auto &cb : pImpl->stateCallbacks)
741
- cb(pImpl->state);
742
- }
743
- }
744
-
745
- bool Window::isFullscreen() const { return pImpl->state.isFullscreen; }
746
-
747
- void Window::minimize() {
748
- bool wasMinimized = pImpl->state.isMinimized;
749
- pImpl->state.isMinimized = true;
750
- pImpl->state.isHidden = false;
751
-
752
- #ifdef _WIN32
753
- if (pImpl->hwnd)
754
- ShowWindow(pImpl->hwnd, SW_MINIMIZE);
755
- #elif defined(__APPLE__)
756
- if (pImpl->nativeWindow) {
757
- NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
758
- [nswin miniaturize:nil];
759
- }
760
- #else
761
- if (pImpl->nativeWindow) {
762
- gtk_window_iconify(GTK_WINDOW(pImpl->nativeWindow));
763
- }
764
- #endif
765
-
766
- if (!wasMinimized) {
767
- for (auto &cb : pImpl->stateCallbacks)
768
- cb(pImpl->state);
769
- }
770
- }
771
-
772
- void Window::maximize() {
773
- bool wasMaximized = pImpl->state.isMaximized;
774
- pImpl->state.isMaximized = true;
775
- pImpl->state.isHidden = false;
776
-
777
- #ifdef _WIN32
778
- if (pImpl->hwnd)
779
- ShowWindow(pImpl->hwnd, SW_MAXIMIZE);
780
- #elif defined(__APPLE__)
781
- if (pImpl->nativeWindow) {
782
- NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
783
- [nswin zoom:nil];
784
- }
785
- #else
786
- if (pImpl->nativeWindow) {
787
- gtk_window_maximize(GTK_WINDOW(pImpl->nativeWindow));
788
- }
789
- #endif
790
-
791
- if (!wasMaximized) {
792
- for (auto &cb : pImpl->stateCallbacks)
793
- cb(pImpl->state);
794
- }
795
- }
796
-
797
- void Window::restore() {
798
- bool wasMinimized = pImpl->state.isMinimized;
799
- bool wasMaximized = pImpl->state.isMaximized;
800
- pImpl->state.isMinimized = false;
801
- pImpl->state.isMaximized = false;
802
- pImpl->state.isHidden = false;
803
-
804
- #ifdef _WIN32
805
- if (pImpl->hwnd)
806
- ShowWindow(pImpl->hwnd, SW_RESTORE);
807
- #elif defined(__APPLE__)
808
- if (pImpl->nativeWindow) {
809
- NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
810
- [nswin deminiaturize:nil];
811
- }
812
- #else
813
- if (pImpl->nativeWindow) {
814
- gtk_window_unmaximize(GTK_WINDOW(pImpl->nativeWindow));
815
- gtk_window_deiconify(GTK_WINDOW(pImpl->nativeWindow));
816
- }
817
- #endif
818
-
819
- if (wasMinimized || wasMaximized) {
820
- for (auto &cb : pImpl->stateCallbacks)
821
- cb(pImpl->state);
822
- }
823
- }
824
-
825
- bool Window::isMaximized() const { return pImpl->state.isMaximized; }
826
-
827
- bool Window::isMinimized() const { return pImpl->state.isMinimized; }
828
-
829
- void Window::show() {
830
- bool wasHidden = pImpl->state.isHidden;
831
- pImpl->state.isVisible = true;
832
- pImpl->state.isHidden = false;
833
-
834
- #ifdef _WIN32
835
- if (pImpl->hwnd)
836
- ShowWindow(pImpl->hwnd, SW_SHOW);
837
- #elif defined(__APPLE__)
838
- if (pImpl->nativeWindow) {
839
- NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
840
- [nswin orderFront:nil];
841
- }
842
- #else
843
- if (pImpl->nativeWindow) {
844
- gtk_widget_show(GTK_WIDGET(pImpl->nativeWindow));
845
- }
846
- #endif
847
-
848
- // Fire state change event if hidden state changed
849
- if (wasHidden) {
850
- for (auto &cb : pImpl->stateCallbacks)
851
- cb(pImpl->state);
852
- }
853
- }
854
-
855
- void Window::hide() {
856
- bool wasVisible = pImpl->state.isVisible;
857
- pImpl->state.isVisible = false;
858
- pImpl->state.isHidden = true;
859
-
860
- #ifdef _WIN32
861
- if (pImpl->hwnd)
862
- ShowWindow(pImpl->hwnd, SW_HIDE);
863
- #elif defined(__APPLE__)
864
- if (pImpl->nativeWindow) {
865
- NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
866
- [nswin orderOut:nil];
867
- }
868
- #else
869
- if (pImpl->nativeWindow) {
870
- gtk_widget_hide(GTK_WIDGET(pImpl->nativeWindow));
871
- }
872
- #endif
873
-
874
- // Fire state change event if visibility changed
875
- if (wasVisible) {
876
- for (auto &cb : pImpl->stateCallbacks)
877
- cb(pImpl->state);
878
- }
879
- }
880
-
881
- bool Window::isVisible() const { return pImpl->state.isVisible; }
882
-
883
- bool Window::isHidden() const { return pImpl->state.isHidden; }
884
-
885
- void Window::focus() {
886
- #ifdef _WIN32
887
- if (pImpl->hwnd)
888
- SetFocus(pImpl->hwnd);
889
- #elif defined(__APPLE__)
890
- if (pImpl->nativeWindow) {
891
- NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
892
- [nswin makeKeyAndOrderFront:nil];
893
- }
894
- #else
895
- if (pImpl->nativeWindow) {
896
- gtk_window_present(GTK_WINDOW(pImpl->nativeWindow));
897
- }
898
- #endif
899
- }
900
-
901
- bool Window::isFocused() const { return pImpl->state.isFocused; }
902
-
903
- void Window::setAlwaysOnTop(bool enabled) {
904
- pImpl->config.alwaysOnTop = enabled;
905
- #ifdef _WIN32
906
- if (pImpl->hwnd) {
907
- SetWindowPos(pImpl->hwnd, enabled ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0,
908
- 0, SWP_NOMOVE | SWP_NOSIZE);
909
- }
910
- #elif defined(__APPLE__)
911
- if (pImpl->nativeWindow) {
912
- NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
913
- [nswin setLevel:enabled ? NSFloatingWindowLevel : NSNormalWindowLevel];
914
- }
915
- #else
916
- if (pImpl->nativeWindow) {
917
- gtk_window_set_keep_above(GTK_WINDOW(pImpl->nativeWindow), enabled);
918
- }
919
- #endif
920
- }
921
-
922
- void Window::setResizable(bool enabled) {
923
- pImpl->config.resizable = enabled;
924
- #ifdef _WIN32
925
- if (pImpl->hwnd) {
926
- SetWindowLongPtr(
927
- pImpl->hwnd, GWL_STYLE,
928
- enabled ? GetWindowLong(pImpl->hwnd, GWL_STYLE) | WS_THICKFRAME
929
- : GetWindowLong(pImpl->hwnd, GWL_STYLE) & ~WS_THICKFRAME);
930
- }
931
- #elif defined(__APPLE__)
932
- // Handled in create
933
- #else
934
- if (pImpl->nativeWindow) {
935
- gtk_window_set_resizable(GTK_WINDOW(pImpl->nativeWindow), enabled);
936
- }
937
- #endif
938
- }
939
-
940
- void Window::setDecorations(bool enabled) {
941
- #ifdef _WIN32
942
- if (pImpl->hwnd) {
943
- SetWindowLongPtr(pImpl->hwnd, GWL_STYLE,
944
- enabled
945
- ? GetWindowLong(pImpl->hwnd, GWL_STYLE) | WS_CAPTION
946
- : GetWindowLong(pImpl->hwnd, GWL_STYLE) & ~WS_CAPTION);
947
- }
948
- #elif defined(__APPLE__)
949
- if (pImpl->nativeWindow) {
950
- NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
951
- [nswin setStyleMask:enabled ? [nswin styleMask] | NSWindowStyleMaskTitled
952
- : [nswin styleMask] & ~NSWindowStyleMaskTitled];
953
- }
954
- #else
955
- if (pImpl->nativeWindow) {
956
- // GTK handles this differently
957
- }
958
- #endif
959
- }
960
-
961
- void Window::setSkipTaskbar(bool enabled) {
962
- #ifdef _WIN32
963
- if (pImpl->hwnd) {
964
- SetWindowLongPtr(
965
- pImpl->hwnd, GWL_EXSTYLE,
966
- enabled ? GetWindowLong(pImpl->hwnd, GWL_EXSTYLE) | WS_EX_TOOLWINDOW
967
- : GetWindowLong(pImpl->hwnd, GWL_EXSTYLE) & ~WS_EX_TOOLWINDOW);
968
- }
969
- #endif
970
- }
971
-
972
- void Window::setOpacity(double opacity) {
973
- pImpl->config.opacity = opacity;
974
- #ifdef _WIN32
975
- if (pImpl->hwnd) {
976
- BYTE alpha = static_cast<BYTE>(opacity * 255);
977
- SetWindowLongPtr(pImpl->hwnd, GWL_EXSTYLE,
978
- GetWindowLong(pImpl->hwnd, GWL_EXSTYLE) | WS_EX_LAYERED);
979
- SetLayeredWindowAttributes(pImpl->hwnd, 0, alpha, LWA_ALPHA);
980
- }
981
- #endif
982
- }
983
-
984
- void Window::setIconFromMemory(const unsigned char *data, size_t size) {
985
- #ifdef _WIN32
986
- if (pImpl->hwnd) {
987
- int width, height, channels;
988
- unsigned char *pixels =
989
- stbi_load_from_memory(data, (int)size, &width, &height, &channels, 4);
990
- if (pixels) {
991
- BITMAPINFO bmi = {};
992
- bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
993
- bmi.bmiHeader.biWidth = width;
994
- bmi.bmiHeader.biHeight = -height;
995
- bmi.bmiHeader.biPlanes = 1;
996
- bmi.bmiHeader.biBitCount = 32;
997
- bmi.bmiHeader.biCompression = BI_RGB;
998
-
999
- void *bits = nullptr;
1000
- HBITMAP hbm =
1001
- CreateDIBSection(nullptr, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0);
1002
- if (hbm && bits) {
1003
- // Convert RGBA to BGRA
1004
- for (int i = 0; i < width * height; ++i) {
1005
- ((uint8_t *)bits)[i * 4 + 0] = pixels[i * 4 + 2];
1006
- ((uint8_t *)bits)[i * 4 + 1] = pixels[i * 4 + 1];
1007
- ((uint8_t *)bits)[i * 4 + 2] = pixels[i * 4 + 0];
1008
- ((uint8_t *)bits)[i * 4 + 3] = pixels[i * 4 + 3];
1009
- }
1010
-
1011
- ICONINFO ii = {};
1012
- ii.fIcon = TRUE;
1013
- ii.hbmColor = hbm;
1014
- ii.hbmMask = hbm;
1015
-
1016
- HICON hIcon = CreateIconIndirect(&ii);
1017
- if (hIcon) {
1018
- SendMessage(pImpl->hwnd, WM_SETICON, ICON_SMALL, (LPARAM)hIcon);
1019
- SendMessage(pImpl->hwnd, WM_SETICON, ICON_BIG, (LPARAM)hIcon);
1020
- }
1021
- DeleteObject(hbm);
1022
- }
1023
- stbi_image_free(pixels);
1024
- }
1025
- }
1026
- #endif
1027
- }
1028
-
1029
- WindowState Window::getState() const { return pImpl->state; }
1030
-
1031
- void Window::close() {
1032
- #ifdef _WIN32
1033
- if (pImpl->hwnd)
1034
- PostMessage(pImpl->hwnd, WM_CLOSE, 0, 0);
1035
- #elif defined(__APPLE__)
1036
- if (pImpl->nativeWindow) {
1037
- NSWindow *nswin = (__bridge NSWindow *)pImpl->nativeWindow;
1038
- [nswin close];
1039
- }
1040
- #else
1041
- if (pImpl->nativeWindow) {
1042
- gtk_widget_destroy(GTK_WIDGET(pImpl->nativeWindow));
1043
- }
1044
- #endif
1045
- }
1046
-
1047
- void Window::onMove(MoveCallback callback) {
1048
- pImpl->moveCallbacks.push_back(callback);
1049
- }
1050
-
1051
- void Window::onResize(ResizeCallback callback) {
1052
- pImpl->resizeCallbacks.push_back(callback);
1053
- }
1054
-
1055
- void Window::onClose(CloseCallback callback) {
1056
- pImpl->closeCallbacks.push_back(callback);
1057
- }
1058
-
1059
- void Window::onFocus(FocusCallback callback) {
1060
- pImpl->focusCallbacks.push_back(callback);
1061
- }
1062
-
1063
- void Window::onStateChange(StateCallback callback) {
1064
- pImpl->stateCallbacks.push_back(callback);
1065
- }
1066
-
1067
- void *Window::nativeHandle() const { return pImpl->nativeWindow; }
1068
-
1069
- // WebView-specific implementations
1070
- Window Window::create(void *windowHandle, const WindowConfig &config) {
1071
- Window win;
1072
- win.pImpl->config = config;
1073
-
1074
- // Pure native file-drop mode: when native FileDrop is enabled,
1075
- // fully disable browser/WebView drag-drop handling.
1076
- if (win.pImpl->config.fileDrop) {
1077
- win.pImpl->config.disableWebviewDragDrop = true;
1078
- }
1079
-
1080
- #ifdef _WIN32
1081
- HWND hwnd = static_cast<HWND>(windowHandle);
1082
-
1083
- // Create WebView2 environment and controller
1084
- auto pImpl = win.pImpl;
1085
- CreateCoreWebView2EnvironmentWithOptions(
1086
- nullptr, nullptr, nullptr,
1087
- Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
1088
- [hwnd, pImpl](HRESULT result,
1089
- ICoreWebView2Environment *env) -> HRESULT {
1090
- if (FAILED(result) || !env)
1091
- return result;
1092
- env->CreateCoreWebView2Controller(
1093
- hwnd,
1094
- Callback<
1095
- ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>(
1096
- [pImpl](HRESULT result,
1097
- ICoreWebView2Controller *controller) -> HRESULT {
1098
- (void)result; // Suppress unused warning
1099
- if (controller != nullptr) {
1100
- pImpl->controller = controller;
1101
- controller->get_CoreWebView2(&pImpl->webview);
1102
-
1103
- RECT bounds;
1104
- HWND parentHwnd;
1105
- controller->get_ParentWindow(&parentHwnd);
1106
- GetClientRect(parentHwnd, &bounds);
1107
- controller->put_Bounds(bounds);
1108
- Window::Impl::embeddedWebviewByParent[parentHwnd] =
1109
- pImpl.get();
1110
-
1111
- // AllowExternalDrop must be TRUE so that drag-
1112
- // related DOM events (dragenter, dragover, dragleave,
1113
- // drop) fire inside the webview. Our injected JS
1114
- // calls preventDefault() on all of them to stop the
1115
- // browser from navigating to the dropped file.
1116
- // The parent HWND still receives WM_DROPFILES via
1117
- // DragAcceptFiles, which is where we extract file
1118
- // metadata and push it into JavaScript.
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__.
1119
1054
  ComPtr<ICoreWebView2Controller4> controller4;
1120
1055
  if (controller &&
1121
1056
  SUCCEEDED(controller->QueryInterface(
@@ -1123,35 +1058,35 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1123
1058
  controller4) {
1124
1059
  controller4->put_AllowExternalDrop(TRUE);
1125
1060
  }
1126
-
1127
- pImpl->nativeWebView = pImpl->webview.Get();
1128
- pImpl->ready = true;
1129
-
1130
- // Inject bridge script that runs on EVERY document
1131
- // (survives navigation)
1132
- std::string bridgeScript = R"(
1133
- window.__native_invoke__ = function(request) {
1134
- if (window.chrome && window.chrome.webview) {
1135
- window.chrome.webview.postMessage(request);
1136
- }
1137
- };
1138
- )";
1139
- pImpl->webview->AddScriptToExecuteOnDocumentCreated(
1140
- std::wstring(bridgeScript.begin(),
1141
- bridgeScript.end())
1142
- .c_str(),
1143
- nullptr);
1144
-
1145
- // Inject scrollbar hiding CSS if disabled
1146
- if (!pImpl->config.scrollbars) {
1147
- std::string scrollbarScript = kHideScrollbarsScript;
1148
- pImpl->webview->AddScriptToExecuteOnDocumentCreated(
1149
- std::wstring(scrollbarScript.begin(),
1150
- scrollbarScript.end())
1151
- .c_str(),
1152
- nullptr);
1153
- }
1154
-
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
+
1155
1090
  // Block browser default drag-drop behavior (prevents
1156
1091
  // the browser from navigating to the dropped file) while
1157
1092
  // still allowing visual feedback on drop zones.
@@ -1227,6 +1162,43 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1227
1162
  e.preventDefault();
1228
1163
  dragDepth = 0;
1229
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
+ }
1230
1202
  }, true);
1231
1203
 
1232
1204
  // Also block at window level as a safety net
@@ -1240,428 +1212,450 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1240
1212
  .c_str(),
1241
1213
  nullptr);
1242
1214
 
1243
- // Set up WebMessageReceived handler for JS->C++ bridge
1244
- pImpl->webview->add_WebMessageReceived(
1245
- Callback<
1246
- ICoreWebView2WebMessageReceivedEventHandler>(
1247
- [pImpl](ICoreWebView2 *sender,
1248
- ICoreWebView2WebMessageReceivedEventArgs
1249
- *args) -> HRESULT {
1250
- (void)sender; // Suppress unused warning
1251
- LPWSTR message = nullptr;
1252
- args->TryGetWebMessageAsString(&message);
1253
- if (!message)
1254
- return S_OK;
1255
- std::wstring wmsg(message);
1256
- #pragma warning(push)
1257
- #pragma warning(disable : 4244) // Suppress wchar_t to char conversion warning
1258
- std::string msg(wmsg.begin(), wmsg.end());
1259
- #pragma warning(pop)
1260
- CoTaskMemFree(message);
1261
-
1262
- // Debug: log received message
1263
- std::cout << "[PlusUI] Received: " << msg
1264
- << std::endl;
1265
-
1266
- bool handledByMessageCallback = false;
1267
-
1268
- // New Generic Message Handler
1269
- if (pImpl->messageCallback) {
1270
- pImpl->messageCallback(msg);
1271
- if (msg.find("\"kind\"") !=
1272
- std::string::npos) {
1273
- handledByMessageCallback = true;
1274
- }
1275
- }
1276
-
1277
- // Parse JSON-RPC: {"id":"...",
1278
- // "method":"window.minimize", "params":[...]}
1279
- std::string id, method;
1280
- std::string result = "null";
1281
- bool success = false;
1282
-
1283
- // Simple JSON parsing
1284
- auto getId = [&msg]() {
1285
- size_t pos = msg.find("\"id\"");
1286
- if (pos == std::string::npos)
1287
- return std::string();
1288
- pos = msg.find(":", pos);
1289
- if (pos == std::string::npos)
1290
- return std::string();
1291
- size_t start = msg.find("\"", pos);
1292
- if (start == std::string::npos)
1293
- return std::string();
1294
- size_t end = msg.find("\"", start + 1);
1295
- if (end == std::string::npos)
1296
- return std::string();
1297
- return msg.substr(start + 1,
1298
- end - start - 1);
1299
- };
1300
- auto getMethod = [&msg]() {
1301
- size_t pos = msg.find("\"method\"");
1302
- if (pos == std::string::npos)
1303
- return std::string();
1304
- pos = msg.find(":", pos);
1305
- if (pos == std::string::npos)
1306
- return std::string();
1307
- size_t start = msg.find("\"", pos);
1308
- if (start == std::string::npos)
1309
- return std::string();
1310
- size_t end = msg.find("\"", start + 1);
1311
- if (end == std::string::npos)
1312
- return std::string();
1313
- return msg.substr(start + 1,
1314
- end - start - 1);
1315
- };
1316
-
1317
- id = getId();
1318
- method = getMethod();
1319
-
1320
- auto extractFirstParam = [&msg]() {
1321
- size_t paramsPos = msg.find("\"params\"");
1322
- if (paramsPos == std::string::npos)
1323
- return std::string("null");
1324
- size_t colonPos = msg.find(":", paramsPos);
1325
- if (colonPos == std::string::npos)
1326
- return std::string("null");
1327
- size_t arrayStart = msg.find("[", colonPos);
1328
- if (arrayStart == std::string::npos)
1329
- return std::string("null");
1330
-
1331
- size_t i = arrayStart + 1;
1332
- while (i < msg.size() &&
1333
- std::isspace(
1334
- static_cast<unsigned char>(msg[i])))
1335
- ++i;
1336
- if (i >= msg.size() || msg[i] == ']')
1337
- return std::string("null");
1338
-
1339
- size_t start = i;
1340
- if (msg[i] == '"') {
1341
- ++i;
1342
- bool escaped = false;
1343
- while (i < msg.size()) {
1344
- char c = msg[i];
1345
- if (escaped) {
1346
- escaped = false;
1347
- } else if (c == '\\') {
1348
- escaped = true;
1349
- } else if (c == '"') {
1350
- ++i;
1351
- break;
1352
- }
1353
- ++i;
1354
- }
1355
- return msg.substr(start, i - start);
1356
- }
1357
-
1358
- int depth = 0;
1359
- bool inString = false;
1360
- bool escaped = false;
1361
- while (i < msg.size()) {
1362
- char c = msg[i];
1363
- if (inString) {
1364
- if (escaped) {
1365
- escaped = false;
1366
- } else if (c == '\\') {
1367
- escaped = true;
1368
- } else if (c == '"') {
1369
- inString = false;
1370
- }
1371
- } else {
1372
- if (c == '"') {
1373
- inString = true;
1374
- } else if (c == '{' || c == '[') {
1375
- ++depth;
1376
- } else if (c == '}' || c == ']') {
1377
- if (depth == 0) {
1378
- break;
1379
- }
1380
- --depth;
1381
- } else if (c == ',' && depth == 0) {
1382
- break;
1383
- }
1384
- }
1385
- ++i;
1386
- }
1387
- return msg.substr(start, i - start);
1388
- };
1389
-
1390
- auto decodeJsonString =
1391
- [](const std::string &input) {
1392
- if (input.size() < 2 ||
1393
- input.front() != '"' ||
1394
- input.back() != '"') {
1395
- return input;
1396
- }
1397
-
1398
- std::string decoded;
1399
- decoded.reserve(input.size() - 2);
1400
- for (size_t i = 1; i + 1 < input.size();
1401
- ++i) {
1402
- char c = input[i];
1403
- if (c == '\\' && i + 1 < input.size() - 1) {
1404
- char next = input[++i];
1405
- switch (next) {
1406
- case '"':
1407
- decoded.push_back('"');
1408
- break;
1409
- case '\\':
1410
- decoded.push_back('\\');
1411
- break;
1412
- case '/':
1413
- decoded.push_back('/');
1414
- break;
1415
- case 'n':
1416
- decoded.push_back('\n');
1417
- break;
1418
- case 'r':
1419
- decoded.push_back('\r');
1420
- break;
1421
- case 't':
1422
- decoded.push_back('\t');
1423
- break;
1424
- default:
1425
- decoded.push_back(next);
1426
- break;
1427
- }
1428
- } else {
1429
- decoded.push_back(c);
1430
- }
1431
- }
1432
- return decoded;
1433
- };
1434
-
1435
- // Route to handlers
1436
- if (handledByMessageCallback) {
1437
- success = true;
1438
- result = "null";
1439
- } else if (method.find("window.") == 0) {
1440
- std::string winMethod = method.substr(7);
1441
- if (winMethod == "minimize") {
1442
- if (pImpl->window)
1443
- pImpl->window->minimize();
1444
- success = true;
1445
- } else if (winMethod == "maximize") {
1446
- if (pImpl->window)
1447
- pImpl->window->maximize();
1448
- success = true;
1449
- } else if (winMethod == "restore") {
1450
- if (pImpl->window)
1451
- pImpl->window->restore();
1452
- success = true;
1453
- } else if (winMethod == "close") {
1454
- if (pImpl->window)
1455
- pImpl->window->close();
1456
- success = true;
1457
- } else if (winMethod == "show") {
1458
- if (pImpl->window)
1459
- pImpl->window->show();
1460
- success = true;
1461
- } else if (winMethod == "hide") {
1462
- if (pImpl->window)
1463
- pImpl->window->hide();
1464
- success = true;
1465
- } else if (winMethod == "getSize") {
1466
- if (pImpl->window) {
1467
- int w, h;
1468
- pImpl->window->getSize(w, h);
1469
- result =
1470
- "{\"width\":" + std::to_string(w) +
1471
- ",\"height\":" + std::to_string(h) +
1472
- "}";
1473
- }
1474
- success = true;
1475
- } else if (winMethod == "getPosition") {
1476
- if (pImpl->window) {
1477
- int x, y;
1478
- pImpl->window->getPosition(x, y);
1479
- result = "{\"x\":" + std::to_string(x) +
1480
- ",\"y\":" + std::to_string(y) +
1481
- "}";
1482
- }
1483
- success = true;
1484
- } else if (winMethod == "setSize") {
1485
- // Parse params [width, height]
1486
- size_t p1 = msg.find("[");
1487
- size_t p2 = msg.find("]");
1488
- if (p1 != std::string::npos &&
1489
- p2 != std::string::npos) {
1490
- std::string params =
1491
- msg.substr(p1 + 1, p2 - p1 - 1);
1492
- int w = 0, h = 0;
1493
- sscanf(params.c_str(), "%d, %d", &w,
1494
- &h);
1495
- if (pImpl->window)
1496
- pImpl->window->setSize(w, h);
1497
- }
1498
- success = true;
1499
- } else if (winMethod == "setPosition") {
1500
- size_t p1 = msg.find("[");
1501
- size_t p2 = msg.find("]");
1502
- if (p1 != std::string::npos &&
1503
- p2 != std::string::npos) {
1504
- std::string params =
1505
- msg.substr(p1 + 1, p2 - p1 - 1);
1506
- int x = 0, y = 0;
1507
- sscanf(params.c_str(), "%d, %d", &x,
1508
- &y);
1509
- if (pImpl->window)
1510
- pImpl->window->setPosition(x, y);
1511
- }
1512
- success = true;
1513
- } else if (winMethod == "setTitle") {
1514
- size_t p1 = msg.find("[\"");
1515
- size_t p2 = msg.find("\"]");
1516
- if (p1 != std::string::npos &&
1517
- p2 != std::string::npos) {
1518
- std::string title =
1519
- msg.substr(p1 + 2, p2 - p1 - 2);
1520
- if (pImpl->window)
1521
- pImpl->window->setTitle(title);
1522
- }
1523
- success = true;
1524
- } else if (winMethod == "setFullscreen") {
1525
- size_t p1 = msg.find("[");
1526
- size_t p2 = msg.find("]");
1527
- if (p1 != std::string::npos &&
1528
- p2 != std::string::npos) {
1529
- std::string params =
1530
- msg.substr(p1 + 1, p2 - p1 - 1);
1531
- if (pImpl->window)
1532
- pImpl->window->setFullscreen(
1533
- params.find("true") !=
1534
- std::string::npos);
1535
- }
1536
- success = true;
1537
- } else if (winMethod == "setAlwaysOnTop") {
1538
- size_t p1 = msg.find("[");
1539
- size_t p2 = msg.find("]");
1540
- if (p1 != std::string::npos &&
1541
- p2 != std::string::npos) {
1542
- std::string params =
1543
- msg.substr(p1 + 1, p2 - p1 - 1);
1544
- if (pImpl->window)
1545
- pImpl->window->setAlwaysOnTop(
1546
- params.find("true") !=
1547
- std::string::npos);
1548
- }
1549
- success = true;
1550
- } else if (winMethod == "setResizable") {
1551
- size_t p1 = msg.find("[");
1552
- size_t p2 = msg.find("]");
1553
- if (p1 != std::string::npos &&
1554
- p2 != std::string::npos) {
1555
- std::string params =
1556
- msg.substr(p1 + 1, p2 - p1 - 1);
1557
- if (pImpl->window)
1558
- pImpl->window->setResizable(
1559
- params.find("true") !=
1560
- std::string::npos);
1561
- }
1562
- success = true;
1563
- } else if (winMethod == "isMaximized") {
1564
- result = (pImpl->window &&
1565
- pImpl->window->isMaximized())
1566
- ? "true"
1567
- : "false";
1568
- success = true;
1569
- } else if (winMethod == "isMinimized") {
1570
- result = (pImpl->window &&
1571
- pImpl->window->isMinimized())
1572
- ? "true"
1573
- : "false";
1574
- success = true;
1575
- } else if (winMethod == "isVisible") {
1576
- result = (pImpl->window &&
1577
- pImpl->window->isVisible())
1578
- ? "true"
1579
- : "false";
1580
- success = true;
1581
- } else if (winMethod == "center") {
1582
- if (pImpl->window)
1583
- pImpl->window->center();
1584
- success = true;
1585
- }
1586
- } else if (method.find("browser.") == 0) {
1587
- std::string browserMethod =
1588
- method.substr(8);
1589
- if (browserMethod == "navigate") {
1590
- size_t p1 = msg.find("[\"");
1591
- size_t p2 = msg.find("\"]");
1592
- if (p1 != std::string::npos &&
1593
- p2 != std::string::npos) {
1594
- std::string url =
1595
- msg.substr(p1 + 2, p2 - p1 - 2);
1596
- pImpl->webview->Navigate(
1597
- std::wstring(url.begin(), url.end())
1598
- .c_str());
1599
- }
1600
- success = true;
1601
- } else if (browserMethod == "goBack") {
1602
- pImpl->webview->GoBack();
1603
- success = true;
1604
- } else if (browserMethod == "goForward") {
1605
- pImpl->webview->GoForward();
1606
- success = true;
1607
- } else if (browserMethod == "reload") {
1608
- pImpl->webview->Reload();
1609
- success = true;
1610
- } else if (browserMethod == "stop") {
1611
- pImpl->webview->Stop();
1612
- success = true;
1613
- } else if (browserMethod == "getUrl") {
1614
- result = "\"" + pImpl->currentURL + "\"";
1615
- success = true;
1616
- } else if (browserMethod == "getTitle") {
1617
- result =
1618
- "\"" + pImpl->currentTitle + "\"";
1619
- success = true;
1620
- } else if (browserMethod == "canGoBack") {
1621
- BOOL canBack;
1622
- pImpl->webview->get_CanGoBack(&canBack);
1623
- result = canBack ? "true" : "false";
1624
- success = true;
1625
- } else if (browserMethod ==
1626
- "canGoForward") {
1627
- BOOL canForward;
1628
- pImpl->webview->get_CanGoForward(
1629
- &canForward);
1630
- result = canForward ? "true" : "false";
1631
- success = true;
1632
- }
1633
- } else if (method.find("fileDrop.") == 0) {
1634
- std::string fileDropMethod =
1635
- method.substr(9);
1636
- if (fileDropMethod == "setEnabled") {
1637
- size_t p1 = msg.find("[");
1638
- size_t p2 = msg.find("]");
1639
- if (p1 != std::string::npos &&
1640
- p2 != std::string::npos) {
1641
- std::string params =
1642
- msg.substr(p1 + 1, p2 - p1 - 1);
1643
- bool enabled = params.find("true") !=
1644
- std::string::npos;
1645
- pImpl->config.fileDrop = enabled;
1646
-
1647
- HWND targetHwnd = nullptr;
1648
- if (pImpl->window) {
1649
- targetHwnd = static_cast<HWND>(
1650
- pImpl->window->nativeHandle());
1651
- }
1652
- if (!targetHwnd && pImpl->controller) {
1653
- pImpl->controller->get_ParentWindow(
1654
- &targetHwnd);
1655
- }
1656
- if (targetHwnd) {
1657
- DragAcceptFiles(targetHwnd,
1658
- enabled ? TRUE
1659
- : FALSE);
1660
- }
1661
-
1662
- // Always keep external drops allowed
1663
- // at the WebView2 level so WM_DROPFILES
1664
- // 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.
1665
1659
  if (pImpl->controller) {
1666
1660
  ComPtr<ICoreWebView2Controller4>
1667
1661
  controller4;
@@ -1669,115 +1663,115 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1669
1663
  &controller4)) &&
1670
1664
  controller4) {
1671
1665
  controller4->put_AllowExternalDrop(
1672
- TRUE);
1666
+ FALSE);
1673
1667
  }
1674
1668
  }
1675
- }
1676
- success = true;
1677
- } else if (fileDropMethod == "isEnabled") {
1678
- result = pImpl->config.fileDrop
1679
- ? "true"
1680
- : "false";
1681
- success = true;
1682
- } else if (fileDropMethod == "startDrag") {
1683
- result = "false";
1684
- success = true;
1685
- } else if (fileDropMethod ==
1686
- "clearCallbacks") {
1687
- success = true;
1688
- }
1689
- } else if (method.find("webview.") == 0) {
1690
- std::string bindingName = method.substr(8);
1691
- auto bindingIt =
1692
- pImpl->bindings.find(bindingName);
1693
- if (bindingIt != pImpl->bindings.end()) {
1694
- std::string rawParam =
1695
- extractFirstParam();
1696
- std::string callbackArg =
1697
- decodeJsonString(rawParam);
1698
- try {
1699
- result =
1700
- bindingIt->second(callbackArg);
1701
- if (result.empty()) {
1702
- result = "null";
1703
- }
1704
- success = true;
1705
- } catch (const std::exception &e) {
1706
- std::cerr
1707
- << "[PlusUI] webview binding error: "
1708
- << e.what() << std::endl;
1709
- result = "null";
1710
- success = true;
1711
- }
1712
- }
1713
- }
1714
-
1715
- // Send response back to JS (matches
1716
- // plusui-native-core SDK bridge)
1717
- std::string response =
1718
- "window.__response__(\"" + id + "\", " +
1719
- result + ");";
1720
- pImpl->webview->ExecuteScript(
1721
- std::wstring(response.begin(),
1722
- response.end())
1723
- .c_str(),
1724
- nullptr);
1725
-
1726
- return S_OK;
1727
- })
1728
- .Get(),
1729
- nullptr);
1730
-
1731
- // Process pending scripts
1732
- for (const auto &script : pImpl->pendingScripts) {
1733
- pImpl->webview->ExecuteScript(
1734
- std::wstring(script.begin(), script.end())
1735
- .c_str(),
1736
- nullptr);
1737
- }
1738
- pImpl->pendingScripts.clear();
1739
-
1740
- // Process pending navigation
1741
- if (!pImpl->pendingNavigation.empty()) {
1742
- pImpl->webview->Navigate(
1743
- std::wstring(pImpl->pendingNavigation.begin(),
1744
- pImpl->pendingNavigation.end())
1745
- .c_str());
1746
- pImpl->pendingNavigation.clear();
1747
- }
1748
-
1749
- // Process pending HTML
1750
- if (!pImpl->pendingHTML.empty()) {
1751
- pImpl->webview->NavigateToString(
1752
- std::wstring(pImpl->pendingHTML.begin(),
1753
- pImpl->pendingHTML.end())
1754
- .c_str());
1755
- pImpl->pendingHTML.clear();
1756
- }
1757
-
1758
- // Process pending File
1759
- if (!pImpl->pendingFile.empty()) {
1760
- // For now, loadFile is implemented via navigate in
1761
- // some versions or direct file reading. We'll handle
1762
- // it via navigate for simplicity if it's already
1763
- // implemented that way.
1764
- }
1765
- }
1766
- return S_OK;
1767
- })
1768
- .Get());
1769
- return S_OK;
1770
- })
1771
- .Get());
1772
-
1773
- #elif defined(__APPLE__)
1774
- WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
1775
- config.preferences.javaScriptEnabled = YES;
1776
-
1777
- if (win.pImpl->config.devtools) {
1778
- [config.preferences setValue:@YES forKey:@"developerExtrasEnabled"];
1779
- }
1780
-
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
+
1781
1775
  // Block browser default drag-drop while allowing drop zone visual feedback.
1782
1776
  // File delivery is handled natively by macOS drag APIs, not browser events.
1783
1777
  if (win.pImpl->config.disableWebviewDragDrop ||
@@ -1835,24 +1829,24 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1835
1829
  forMainFrameOnly:NO];
1836
1830
  [config.userContentController addUserScript:userScript];
1837
1831
  }
1838
-
1839
- // Hide scrollbars if disabled
1840
- if (!win.pImpl->config.scrollbars) {
1841
- NSString *scrollbarScript =
1842
- [NSString stringWithUTF8String:kHideScrollbarsScript];
1843
-
1844
- WKUserScript *scrollScript = [[WKUserScript alloc]
1845
- initWithSource:scrollbarScript
1846
- injectionTime:WKUserScriptInjectionTimeAtDocumentEnd
1847
- forMainFrameOnly:NO];
1848
- [config.userContentController addUserScript:scrollScript];
1849
- }
1850
-
1851
- NSView *parentView = (__bridge NSView *)windowHandle;
1852
- win.pImpl->wkWebView =
1853
- [[WKWebView alloc] initWithFrame:parentView.bounds configuration:config];
1854
- [parentView addSubview:win.pImpl->wkWebView];
1855
-
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
+
1856
1850
  win.pImpl->nativeWebView = (__bridge void *)win.pImpl->wkWebView;
1857
1851
 
1858
1852
  // ── macOS key event forwarding ─────────────────────────────────────────────
@@ -1935,8 +1929,20 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1935
1929
  // ── Linux / GTK + WebKit2 WebView ─────────────────────────────────────────
1936
1930
  {
1937
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
+
1938
1942
  WebKitWebView *webView =
1939
- WEBKIT_WEB_VIEW(webkit_web_view_new());
1943
+ WEBKIT_WEB_VIEW(webkit_web_view_new_with_settings(wkSettings));
1944
+ g_object_unref(wkSettings);
1945
+
1940
1946
  gtk_container_add(GTK_CONTAINER(gtkParent), GTK_WIDGET(webView));
1941
1947
  gtk_widget_show(GTK_WIDGET(webView));
1942
1948
  win.pImpl->gtkWebView = webView;
@@ -1955,12 +1961,260 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1955
1961
  webkit_user_content_manager_add_script(mgr, bridgeScript);
1956
1962
  webkit_user_script_unref(bridgeScript);
1957
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
+
1958
1967
  // Helper to run JS in this WebView
1959
1968
  auto runJS = [webView](const std::string& script) {
1960
1969
  webkit_web_view_run_javascript(webView, script.c_str(),
1961
1970
  nullptr, nullptr, nullptr);
1962
1971
  };
1963
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
+
1964
2218
  // GDK keyval → PlusUI KeyCode
1965
2219
  auto gdkKeyToPlusUI = [](guint keyval) -> int {
1966
2220
  if (keyval >= GDK_KEY_a && keyval <= GDK_KEY_z)
@@ -2102,423 +2356,423 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
2102
2356
 
2103
2357
  return win;
2104
2358
  }
2105
-
2106
- TrayManager &Window::tray() { return *pImpl->trayManager; }
2107
-
2108
- void Window::setWindow(std::shared_ptr<Window> win) {
2109
- pImpl->window = win;
2110
- if (!win)
2111
- return;
2112
-
2113
- std::weak_ptr<Impl> weak_pImpl = pImpl;
2114
- win->onResize([weak_pImpl](int w, int h) {
2115
- if (auto pImpl = weak_pImpl.lock()) {
2116
- #ifdef _WIN32
2117
- if (pImpl->controller) {
2118
- RECT bounds = {0, 0, w, h};
2119
- pImpl->controller->put_Bounds(bounds);
2120
- }
2121
- #elif defined(__APPLE__)
2122
- if (pImpl->wkWebView) {
2123
- NSView *view = (__bridge NSView *)pImpl->wkWebView;
2124
- NSView *parent = [view superview];
2125
- if (parent) {
2126
- [view setFrame:[parent bounds]];
2127
- }
2128
- }
2129
- #else
2130
- if (pImpl->gtkWebView) {
2131
- // GTK usually handles this if added to a container with expand=TRUE
2132
- // but we can ensure it here if it's a fixed layout parent
2133
- gtk_widget_set_size_request(GTK_WIDGET(pImpl->gtkWebView), w, h);
2134
- }
2135
- #endif
2136
- }
2137
- });
2138
-
2139
- // Trigger initial resize
2140
- int w, h;
2141
- win->getSize(w, h);
2142
- #ifdef _WIN32
2143
- if (pImpl->controller) {
2144
- RECT bounds = {0, 0, w, h};
2145
- pImpl->controller->put_Bounds(bounds);
2146
- }
2147
- #endif
2148
- }
2149
-
2150
- void Window::navigate(const std::string &url) {
2151
- pImpl->currentURL = url;
2152
- pImpl->loading = true;
2153
-
2154
- #ifdef _WIN32
2155
- if (pImpl->ready && pImpl->webview) {
2156
- pImpl->webview->Navigate(std::wstring(url.begin(), url.end()).c_str());
2157
- } else {
2158
- pImpl->pendingNavigation = url;
2159
- }
2160
- #elif defined(__APPLE__)
2161
- if (pImpl->wkWebView) {
2162
- NSURL *nsurl =
2163
- [NSURL URLWithString:[NSString stringWithUTF8String:url.c_str()]];
2164
- NSURLRequest *request = [NSURLRequest requestWithURL:nsurl];
2165
- [pImpl->wkWebView loadRequest:request];
2166
- }
2167
- #else
2168
- if (pImpl->gtkWebView) {
2169
- webkit_web_view_load_uri(pImpl->gtkWebView, url.c_str());
2170
- }
2171
- #endif
2172
- }
2173
-
2174
- void Window::onMessage(MessageCallback callback) {
2175
- pImpl->messageCallback = callback;
2176
- }
2177
-
2178
- void Window::postMessage(const std::string &message) {
2179
- #ifdef _WIN32
2180
- if (pImpl->webview) {
2181
- std::wstring wmsg(message.begin(), message.end());
2182
- pImpl->webview->PostWebMessageAsJson(wmsg.c_str());
2183
- }
2184
- #elif defined(__APPLE__)
2185
- // TODO: formatting for Apple
2186
- #endif
2187
- }
2188
-
2189
- void Window::onFileDrop(FileDropCallback callback) {
2190
- pImpl->fileDropCallback = callback;
2191
- }
2192
-
2193
- void Window::bind(const std::string &name, JSCallback callback) {
2194
- pImpl->bindings[name] = callback;
2195
-
2196
- std::string bridgeScript =
2197
- "window." + name +
2198
- R"( = function(...args) {
2199
- if (window.plusui && typeof window.plusui.invoke === 'function') {
2200
- const payload = args.length === 0 ? null : (args.length === 1 ? args[0] : args);
2201
- return window.plusui.invoke('webview.)" +
2202
- name +
2203
- R"(', [payload]);
2204
- }
2205
- if (window.__invoke__) {
2206
- const payload = args.length === 0 ? null : (args.length === 1 ? args[0] : args);
2207
- return window.__invoke__('webview.)" +
2208
- name +
2209
- R"(', [payload]);
2210
- }
2211
- return Promise.resolve(null);
2212
- };)";
2213
-
2214
- executeScript(bridgeScript);
2215
- }
2216
-
2217
- void Window::unbind(const std::string &name) {
2218
- pImpl->bindings.erase(name);
2219
- executeScript("delete window." + name + ";");
2220
- }
2221
-
2222
- void Window::loadURL(const std::string &url) { navigate(url); }
2223
-
2224
- void Window::loadHTML(const std::string &html) {
2225
- loadHTML(html, "about:blank");
2226
- }
2227
-
2228
- void Window::loadHTML(const std::string &html, const std::string &baseURL) {
2229
- pImpl->loading = true;
2230
-
2231
- #ifdef _WIN32
2232
- (void)baseURL; // Not used on Windows
2233
- if (pImpl->ready && pImpl->webview) {
2234
- pImpl->webview->NavigateToString(
2235
- std::wstring(html.begin(), html.end()).c_str());
2236
- } else {
2237
- pImpl->pendingHTML = html;
2238
- }
2239
- #elif defined(__APPLE__)
2240
- if (pImpl->wkWebView) {
2241
- NSString *htmlString = [NSString stringWithUTF8String:html.c_str()];
2242
- NSURL *base =
2243
- [NSURL URLWithString:[NSString stringWithUTF8String:baseURL.c_str()]];
2244
- [pImpl->wkWebView loadHTMLString:htmlString baseURL:base];
2245
- }
2246
- #else
2247
- if (pImpl->gtkWebView) {
2248
- webkit_web_view_load_html(pImpl->gtkWebView, html.c_str(), baseURL.c_str());
2249
- }
2250
- #endif
2251
- }
2252
-
2253
- void Window::loadFile(const std::string &filePath) {
2254
- std::ifstream file(filePath);
2255
- if (!file) {
2256
- std::cerr << "PlusUI: Failed to open file: " << filePath << std::endl;
2257
- return;
2258
- }
2259
-
2260
- std::stringstream buffer;
2261
- buffer << file.rdbuf();
2262
-
2263
- // Use file:// URL as base for relative paths
2264
- std::string baseURL =
2265
- "file://" + filePath.substr(0, filePath.find_last_of("/\\") + 1);
2266
- loadHTML(buffer.str(), baseURL);
2267
- }
2268
-
2269
- void Window::reload() {
2270
- #ifdef _WIN32
2271
- if (pImpl->webview) {
2272
- pImpl->webview->Reload();
2273
- }
2274
- #elif defined(__APPLE__)
2275
- if (pImpl->wkWebView) {
2276
- [pImpl->wkWebView reload];
2277
- }
2278
- #else
2279
- if (pImpl->gtkWebView) {
2280
- webkit_web_view_reload(pImpl->gtkWebView);
2281
- }
2282
- #endif
2283
- }
2284
-
2285
- void Window::stop() {
2286
- #ifdef _WIN32
2287
- if (pImpl->webview) {
2288
- pImpl->webview->Stop();
2289
- }
2290
- #elif defined(__APPLE__)
2291
- if (pImpl->wkWebView) {
2292
- [pImpl->wkWebView stopLoading];
2293
- }
2294
- #else
2295
- if (pImpl->gtkWebView) {
2296
- webkit_web_view_stop_loading(pImpl->gtkWebView);
2297
- }
2298
- #endif
2299
- }
2300
-
2301
- void Window::goBack() {
2302
- #ifdef _WIN32
2303
- if (pImpl->webview) {
2304
- pImpl->webview->GoBack();
2305
- }
2306
- #elif defined(__APPLE__)
2307
- if (pImpl->wkWebView) {
2308
- [pImpl->wkWebView goBack];
2309
- }
2310
- #else
2311
- if (pImpl->gtkWebView) {
2312
- webkit_web_view_go_back(pImpl->gtkWebView);
2313
- }
2314
- #endif
2315
- }
2316
-
2317
- void Window::goForward() {
2318
- #ifdef _WIN32
2319
- if (pImpl->webview) {
2320
- pImpl->webview->GoForward();
2321
- }
2322
- #elif defined(__APPLE__)
2323
- if (pImpl->wkWebView) {
2324
- [pImpl->wkWebView goForward];
2325
- }
2326
- #else
2327
- if (pImpl->gtkWebView) {
2328
- webkit_web_view_go_forward(pImpl->gtkWebView);
2329
- }
2330
- #endif
2331
- }
2332
-
2333
- bool Window::canGoBack() const {
2334
- #ifdef _WIN32
2335
- if (pImpl->webview) {
2336
- BOOL canGoBack;
2337
- pImpl->webview->get_CanGoBack(&canGoBack);
2338
- return canGoBack;
2339
- }
2340
- #elif defined(__APPLE__)
2341
- if (pImpl->wkWebView) {
2342
- return [pImpl->wkWebView canGoBack];
2343
- }
2344
- #else
2345
- if (pImpl->gtkWebView) {
2346
- return webkit_web_view_can_go_back(pImpl->gtkWebView);
2347
- }
2348
- #endif
2349
- return false;
2350
- }
2351
-
2352
- bool Window::canGoForward() const {
2353
- #ifdef _WIN32
2354
- if (pImpl->webview) {
2355
- BOOL canGoForward;
2356
- pImpl->webview->get_CanGoForward(&canGoForward);
2357
- return canGoForward;
2358
- }
2359
- #elif defined(__APPLE__)
2360
- if (pImpl->wkWebView) {
2361
- return [pImpl->wkWebView canGoForward];
2362
- }
2363
- #else
2364
- if (pImpl->gtkWebView) {
2365
- return webkit_web_view_can_go_forward(pImpl->gtkWebView);
2366
- }
2367
- #endif
2368
- return false;
2369
- }
2370
-
2371
- void Window::executeScript(const std::string &script) {
2372
- #ifdef _WIN32
2373
- if (pImpl->ready && pImpl->webview) {
2374
- pImpl->webview->ExecuteScript(
2375
- std::wstring(script.begin(), script.end()).c_str(), nullptr);
2376
- } else {
2377
- pImpl->pendingScripts.push_back(script);
2378
- }
2379
- #elif defined(__APPLE__)
2380
- if (pImpl->wkWebView) {
2381
- NSString *js = [NSString stringWithUTF8String:script.c_str()];
2382
- [pImpl->wkWebView evaluateJavaScript:js completionHandler:nil];
2383
- }
2384
- #else
2385
- if (pImpl->gtkWebView) {
2386
- webkit_web_view_run_javascript(pImpl->gtkWebView, script.c_str(), nullptr,
2387
- nullptr, nullptr);
2388
- }
2389
- #endif
2390
- }
2391
-
2392
- void Window::executeScript(const std::string &script,
2393
- std::function<void(const std::string &)> callback) {
2394
- #ifdef _WIN32
2395
- if (pImpl->webview) {
2396
- pImpl->webview->ExecuteScript(
2397
- std::wstring(script.begin(), script.end()).c_str(),
2398
- Callback<ICoreWebView2ExecuteScriptCompletedHandler>(
2399
- [callback](HRESULT error, LPCWSTR result) -> HRESULT {
2400
- (void)error; // Suppress unused warning
2401
- if (result && callback) {
2402
- std::wstring wstr(result);
2403
- callback(std::string(wstr.begin(), wstr.end()));
2404
- }
2405
- return S_OK;
2406
- })
2407
- .Get());
2408
- }
2409
- #elif defined(__APPLE__)
2410
- if (pImpl->wkWebView) {
2411
- NSString *js = [NSString stringWithUTF8String:script.c_str()];
2412
- [pImpl->wkWebView evaluateJavaScript:js
2413
- completionHandler:^(id result, NSError *error) {
2414
- if (result && callback) {
2415
- NSString *resultStr =
2416
- [NSString stringWithFormat:@"%@", result];
2417
- callback([resultStr UTF8String]);
2418
- }
2419
- }];
2420
- }
2421
- #else
2422
- if (pImpl->gtkWebView) {
2423
- // GTK WebKit callback handling would go here
2424
- webkit_web_view_run_javascript(pImpl->gtkWebView, script.c_str(), nullptr,
2425
- nullptr, nullptr);
2426
- }
2427
- #endif
2428
- }
2429
-
2430
- void Window::openDevTools() {
2431
- #ifdef _WIN32
2432
- if (pImpl->webview) {
2433
- pImpl->webview->OpenDevToolsWindow();
2434
- }
2435
- #elif defined(__APPLE__)
2436
- // macOS: Dev tools open in Safari's Web Inspector
2437
- #else
2438
- if (pImpl->gtkWebView) {
2439
- WebKitWebInspector *inspector =
2440
- webkit_web_view_get_inspector(pImpl->gtkWebView);
2441
- webkit_web_inspector_show(inspector);
2442
- }
2443
- #endif
2444
- }
2445
-
2446
- void Window::closeDevTools() {
2447
- #ifdef _WIN32
2448
- // WebView2 doesn't have explicit close
2449
- #elif defined(__APPLE__)
2450
- // macOS: Handled by Web Inspector
2451
- #else
2452
- if (pImpl->gtkWebView) {
2453
- WebKitWebInspector *inspector =
2454
- webkit_web_view_get_inspector(pImpl->gtkWebView);
2455
- webkit_web_inspector_close(inspector);
2456
- }
2457
- #endif
2458
- }
2459
-
2460
- void Window::setUserAgent(const std::string &userAgent) {
2461
- pImpl->userAgent = userAgent;
2462
-
2463
- #ifdef _WIN32
2464
- if (pImpl->webview) {
2465
- ComPtr<ICoreWebView2Settings> settings;
2466
- pImpl->webview->get_Settings(&settings);
2467
- // WebView2 user agent requires settings2 interface
2468
- }
2469
- #elif defined(__APPLE__)
2470
- if (pImpl->wkWebView) {
2471
- [pImpl->wkWebView
2472
- setCustomUserAgent:[NSString stringWithUTF8String:userAgent.c_str()]];
2473
- }
2474
- #else
2475
- if (pImpl->gtkWebView) {
2476
- WebKitSettings *settings = webkit_web_view_get_settings(pImpl->gtkWebView);
2477
- webkit_settings_set_user_agent(settings, userAgent.c_str());
2478
- }
2479
- #endif
2480
- }
2481
-
2482
- std::string Window::getUserAgent() const { return pImpl->userAgent; }
2483
-
2484
- void Window::setZoom(double factor) {
2485
- pImpl->zoom = factor;
2486
-
2487
- #ifdef _WIN32
2488
- if (pImpl->controller) {
2489
- pImpl->controller->put_ZoomFactor(factor);
2490
- }
2491
- #elif defined(__APPLE__)
2492
- if (pImpl->wkWebView) {
2493
- [pImpl->wkWebView setPageZoom:factor];
2494
- }
2495
- #else
2496
- if (pImpl->gtkWebView) {
2497
- webkit_web_view_set_zoom_level(pImpl->gtkWebView, factor);
2498
- }
2499
- #endif
2500
- }
2501
-
2502
- double Window::getZoom() const { return pImpl->zoom; }
2503
-
2504
- void Window::injectCSS(const std::string &css) {
2505
- std::string script = R"(
2506
- (function() {
2507
- const style = document.createElement('style');
2508
- style.id = 'plusui-injected-css-' + Date.now();
2509
- style.textContent = `)" +
2510
- css + R"(`;
2511
- if (document.head) {
2512
- document.head.appendChild(style);
2513
- } else {
2514
- document.documentElement.appendChild(style);
2515
- }
2516
- })();
2517
- )";
2518
-
2519
- executeScript(script);
2520
- }
2521
-
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
+
2522
2776
  void Window::setWebviewDragDropEnabled(bool enabled) {
2523
2777
  if (enabled) {
2524
2778
  // Remove the dropzone init so it can be re-applied if needed later
@@ -2588,33 +2842,33 @@ void Window::setWebviewDragDropEnabled(bool enabled) {
2588
2842
  )");
2589
2843
  }
2590
2844
  }
2591
-
2592
- void Window::onNavigationStart(NavigationCallback callback) {
2593
- pImpl->navigationCallback = callback;
2594
- }
2595
-
2596
- void Window::onNavigationComplete(LoadCallback callback) {
2597
- pImpl->navigationCompleteCallback = callback;
2598
- }
2599
-
2600
- void Window::onLoadStart(LoadCallback callback) {
2601
- pImpl->loadStartCallback = callback;
2602
- }
2603
-
2604
- void Window::onLoadEnd(LoadCallback callback) {
2605
- pImpl->loadEndCallback = callback;
2606
- }
2607
-
2608
- void Window::onLoadError(ErrorCallback callback) {
2609
- pImpl->errorCallback = callback;
2610
- }
2611
-
2612
- void Window::onConsoleMessage(ConsoleCallback callback) {
2613
- pImpl->consoleCallback = callback;
2614
- }
2615
-
2616
- bool Window::isLoading() const { return pImpl->loading; }
2617
-
2618
- std::string Window::getURL() const { return pImpl->currentURL; }
2619
-
2620
- } // 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