plusui-native-core 0.1.45 → 0.1.48

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.
@@ -3,6 +3,7 @@
3
3
  #include <iostream>
4
4
  #include <map>
5
5
  #include <plusui/display.hpp>
6
+ #include <plusui/events.hpp>
6
7
  #include <plusui/tray.hpp>
7
8
  #include <plusui/webgpu.hpp>
8
9
  #include <plusui/window.hpp>
@@ -51,6 +52,54 @@ constexpr char kHideScrollbarsScript[] = R"(
51
52
  window.addEventListener("load", ensureStyle);
52
53
  })();
53
54
  )";
55
+
56
+ #ifdef _WIN32
57
+ static std::string jsonEscape(const std::string &input) {
58
+ std::string out;
59
+ out.reserve(input.size() + 8);
60
+ for (char c : input) {
61
+ switch (c) {
62
+ case '\\':
63
+ out += "\\\\";
64
+ break;
65
+ case '"':
66
+ out += "\\\"";
67
+ break;
68
+ case '\n':
69
+ out += "\\n";
70
+ break;
71
+ case '\r':
72
+ out += "\\r";
73
+ break;
74
+ case '\t':
75
+ out += "\\t";
76
+ break;
77
+ default:
78
+ out += c;
79
+ break;
80
+ }
81
+ }
82
+ return out;
83
+ }
84
+
85
+ static std::string mimeTypeFromPath(const std::string &path) {
86
+ size_t dot = path.find_last_of('.');
87
+ if (dot == std::string::npos)
88
+ return "application/octet-stream";
89
+ std::string ext = path.substr(dot);
90
+ for (char &ch : ext)
91
+ ch = static_cast<char>(tolower(static_cast<unsigned char>(ch)));
92
+ if (ext == ".png") return "image/png";
93
+ if (ext == ".jpg" || ext == ".jpeg") return "image/jpeg";
94
+ if (ext == ".gif") return "image/gif";
95
+ if (ext == ".webp") return "image/webp";
96
+ if (ext == ".svg") return "image/svg+xml";
97
+ if (ext == ".txt") return "text/plain";
98
+ if (ext == ".json") return "application/json";
99
+ if (ext == ".pdf") return "application/pdf";
100
+ return "application/octet-stream";
101
+ }
102
+ #endif
54
103
  } // namespace
55
104
 
56
105
  struct Window::Impl {
@@ -93,6 +142,7 @@ struct Window::Impl {
93
142
  WNDPROC originalProc = nullptr;
94
143
  ComPtr<ICoreWebView2Controller> controller;
95
144
  ComPtr<ICoreWebView2> webview;
145
+ static std::map<HWND, Impl *> embeddedWebviewByParent;
96
146
 
97
147
  static LRESULT CALLBACK wndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
98
148
  Impl *impl = nullptr;
@@ -128,6 +178,113 @@ struct Window::Impl {
128
178
  for (auto &cb : impl->closeCallbacks)
129
179
  cb();
130
180
  break;
181
+ case WM_DROPFILES: {
182
+ HDROP hDrop = reinterpret_cast<HDROP>(wp);
183
+ Impl *targetImpl = impl;
184
+ if (!targetImpl->webview) {
185
+ auto it = embeddedWebviewByParent.find(hwnd);
186
+ if (it != embeddedWebviewByParent.end()) {
187
+ targetImpl = it->second;
188
+ }
189
+ }
190
+
191
+ if (!targetImpl || !targetImpl->config.enableFileDrop ||
192
+ !targetImpl->webview) {
193
+ DragFinish(hDrop);
194
+ return 0;
195
+ }
196
+
197
+ UINT fileCount = DragQueryFileW(hDrop, 0xFFFFFFFF, nullptr, 0);
198
+ std::string filesJson = "[";
199
+
200
+ for (UINT i = 0; i < fileCount; ++i) {
201
+ UINT pathLen = DragQueryFileW(hDrop, i, nullptr, 0);
202
+ std::wstring wpath(pathLen + 1, L'\0');
203
+ DragQueryFileW(hDrop, i, &wpath[0], pathLen + 1);
204
+ wpath.resize(pathLen);
205
+
206
+ int utf8Len = WideCharToMultiByte(CP_UTF8, 0, wpath.c_str(), -1,
207
+ nullptr, 0, nullptr, nullptr);
208
+ std::string path;
209
+ if (utf8Len > 0) {
210
+ path.resize(static_cast<size_t>(utf8Len - 1));
211
+ WideCharToMultiByte(CP_UTF8, 0, wpath.c_str(), -1, &path[0],
212
+ utf8Len, nullptr, nullptr);
213
+ }
214
+
215
+ std::string name = path;
216
+ size_t lastSlash = path.find_last_of("\\/");
217
+ if (lastSlash != std::string::npos) {
218
+ name = path.substr(lastSlash + 1);
219
+ }
220
+
221
+ unsigned long long sizeBytes = 0;
222
+ WIN32_FILE_ATTRIBUTE_DATA fileAttr;
223
+ if (GetFileAttributesExW(wpath.c_str(), GetFileExInfoStandard,
224
+ &fileAttr)) {
225
+ ULARGE_INTEGER size;
226
+ size.HighPart = fileAttr.nFileSizeHigh;
227
+ size.LowPart = fileAttr.nFileSizeLow;
228
+ sizeBytes = size.QuadPart;
229
+ }
230
+
231
+ if (i > 0)
232
+ filesJson += ",";
233
+ filesJson +=
234
+ "{\"path\":\"" + jsonEscape(path) + "\",\"name\":\"" +
235
+ jsonEscape(name) + "\",\"type\":\"" +
236
+ jsonEscape(mimeTypeFromPath(path)) + "\",\"size\":" +
237
+ std::to_string(sizeBytes) + "}";
238
+ }
239
+ filesJson += "]";
240
+
241
+ DragFinish(hDrop);
242
+
243
+ POINT dropPoint = {0, 0};
244
+ DragQueryPoint(hDrop, &dropPoint);
245
+
246
+ std::string zoneHitScript =
247
+ "(function(){try{"
248
+ "var el=document.elementFromPoint(" +
249
+ std::to_string(dropPoint.x) + "," + std::to_string(dropPoint.y) +
250
+ ");"
251
+ "if(!el) return false;"
252
+ "if(!el.closest) return false;"
253
+ "return !!el.closest('[data-dropzone=\\\"true\\\"], .filedrop-zone, .dropzone');"
254
+ "}catch(_){return false;}})();";
255
+
256
+ targetImpl->webview->ExecuteScript(
257
+ std::wstring(zoneHitScript.begin(), zoneHitScript.end()).c_str(),
258
+ Callback<ICoreWebView2ExecuteScriptCompletedHandler>(
259
+ [targetImpl, filesJson](HRESULT errorCode,
260
+ LPCWSTR resultJson) -> HRESULT {
261
+ if (FAILED(errorCode) || !targetImpl || !targetImpl->webview)
262
+ return S_OK;
263
+
264
+ bool overDropZone = false;
265
+ if (resultJson) {
266
+ std::wstring wres(resultJson);
267
+ overDropZone = (wres.find(L"true") != std::wstring::npos);
268
+ }
269
+ if (!overDropZone)
270
+ return S_OK;
271
+
272
+ std::string eventScript =
273
+ "window.dispatchEvent(new CustomEvent('plusui:fileDrop.filesDropped',"
274
+ " { detail: { files: " +
275
+ filesJson + " } }));";
276
+
277
+ event::emit("fileDrop.filesDropped", filesJson);
278
+
279
+ targetImpl->webview->ExecuteScript(
280
+ std::wstring(eventScript.begin(), eventScript.end())
281
+ .c_str(),
282
+ nullptr);
283
+ return S_OK;
284
+ })
285
+ .Get());
286
+ return 0;
287
+ }
131
288
  case WM_SETFOCUS:
132
289
  impl->state.isFocused = true;
133
290
  for (auto &cb : impl->focusCallbacks)
@@ -151,6 +308,10 @@ struct Window::Impl {
151
308
 
152
309
  Window::Window() : pImpl(std::shared_ptr<Impl>(new Impl())) {}
153
310
 
311
+ #ifdef _WIN32
312
+ std::map<HWND, Window::Impl *> Window::Impl::embeddedWebviewByParent;
313
+ #endif
314
+
154
315
  Window::~Window() = default;
155
316
 
156
317
  Window::Window(Window &&other) noexcept = default;
@@ -207,6 +368,8 @@ Window Window::create(const WindowConfig &config) {
207
368
  w.pImpl->state.width = config.width;
208
369
  w.pImpl->state.height = config.height;
209
370
 
371
+ DragAcceptFiles(w.pImpl->hwnd, config.enableFileDrop ? TRUE : FALSE);
372
+
210
373
  if (config.center) {
211
374
  RECT screen;
212
375
  SystemParametersInfo(SPI_GETWORKAREA, 0, &screen, 0);
@@ -801,6 +964,20 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
801
964
  controller->get_ParentWindow(&parentHwnd);
802
965
  GetClientRect(parentHwnd, &bounds);
803
966
  controller->put_Bounds(bounds);
967
+ Window::Impl::embeddedWebviewByParent[parentHwnd] =
968
+ pImpl.get();
969
+
970
+ ComPtr<ICoreWebView2Controller4> controller4;
971
+ if (controller &&
972
+ SUCCEEDED(controller->QueryInterface(
973
+ IID_PPV_ARGS(&controller4))) &&
974
+ controller4) {
975
+ bool allowExternalDrop =
976
+ !(pImpl->config.enableFileDrop ||
977
+ pImpl->config.disableWebviewDragDrop);
978
+ controller4->put_AllowExternalDrop(
979
+ allowExternalDrop ? TRUE : FALSE);
980
+ }
804
981
 
805
982
  pImpl->nativeWebView = pImpl->webview.Get();
806
983
  pImpl->ready = true;
@@ -839,15 +1016,41 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
839
1016
  if (window.__plusui_nativeFileDropOnly) return;
840
1017
  window.__plusui_nativeFileDropOnly = true;
841
1018
 
1019
+ var isOverDropZone = function(e) {
1020
+ if (!e) return false;
1021
+ var target = null;
1022
+ if (typeof e.clientX === 'number' && typeof e.clientY === 'number' && document.elementFromPoint) {
1023
+ target = document.elementFromPoint(e.clientX, e.clientY);
1024
+ }
1025
+ if (!target && e.target && e.target.nodeType === 1) {
1026
+ target = e.target;
1027
+ }
1028
+ if (!target || !target.closest) return false;
1029
+ return !!target.closest('[data-dropzone="true"], .filedrop-zone, .dropzone');
1030
+ };
1031
+
1032
+ var zoneActive = false;
1033
+ var emitZoneState = function(next) {
1034
+ if (next === zoneActive) return;
1035
+ zoneActive = next;
1036
+ window.dispatchEvent(new CustomEvent(next ? 'plusui:fileDrop.dragEnter' : 'plusui:fileDrop.dragLeave', { detail: {} }));
1037
+ };
1038
+
842
1039
  var block = function(e) {
843
1040
  if (!e) return false;
1041
+ var overDropZone = isOverDropZone(e);
1042
+ if (e.type === 'drop') {
1043
+ emitZoneState(false);
1044
+ } else {
1045
+ emitZoneState(overDropZone);
1046
+ }
844
1047
  e.preventDefault();
845
1048
  e.stopPropagation();
846
1049
  if (typeof e.stopImmediatePropagation === 'function') {
847
1050
  e.stopImmediatePropagation();
848
1051
  }
849
1052
  if (e.dataTransfer) {
850
- try { e.dataTransfer.dropEffect = 'none'; } catch (_) {}
1053
+ try { e.dataTransfer.dropEffect = overDropZone ? 'copy' : 'none'; } catch (_) {}
851
1054
  }
852
1055
  return false;
853
1056
  };
@@ -1117,6 +1320,64 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1117
1320
  result = canForward ? "true" : "false";
1118
1321
  success = true;
1119
1322
  }
1323
+ } else if (method.find("fileDrop.") == 0) {
1324
+ std::string fileDropMethod = method.substr(9);
1325
+ if (fileDropMethod == "setEnabled") {
1326
+ size_t p1 = msg.find("[");
1327
+ size_t p2 = msg.find("]");
1328
+ if (p1 != std::string::npos &&
1329
+ p2 != std::string::npos) {
1330
+ std::string params =
1331
+ msg.substr(p1 + 1, p2 - p1 - 1);
1332
+ bool enabled =
1333
+ params.find("true") !=
1334
+ std::string::npos;
1335
+ pImpl->config.enableFileDrop = enabled;
1336
+
1337
+ HWND targetHwnd = nullptr;
1338
+ if (pImpl->window) {
1339
+ targetHwnd = static_cast<HWND>(
1340
+ pImpl->window->nativeHandle());
1341
+ }
1342
+ if (!targetHwnd && pImpl->controller) {
1343
+ pImpl->controller->get_ParentWindow(
1344
+ &targetHwnd);
1345
+ }
1346
+ if (targetHwnd) {
1347
+ DragAcceptFiles(targetHwnd,
1348
+ enabled ? TRUE
1349
+ : FALSE);
1350
+ }
1351
+
1352
+ if (pImpl->controller) {
1353
+ ComPtr<ICoreWebView2Controller4>
1354
+ controller4;
1355
+ if (SUCCEEDED(pImpl->controller.As(
1356
+ &controller4)) &&
1357
+ controller4) {
1358
+ bool allowExternalDrop =
1359
+ !(pImpl->config.enableFileDrop ||
1360
+ pImpl->config
1361
+ .disableWebviewDragDrop);
1362
+ controller4->put_AllowExternalDrop(
1363
+ allowExternalDrop ? TRUE
1364
+ : FALSE);
1365
+ }
1366
+ }
1367
+ }
1368
+ success = true;
1369
+ } else if (fileDropMethod == "isEnabled") {
1370
+ result = pImpl->config.enableFileDrop
1371
+ ? "true"
1372
+ : "false";
1373
+ success = true;
1374
+ } else if (fileDropMethod == "startDrag") {
1375
+ result = "false";
1376
+ success = true;
1377
+ } else if (fileDropMethod ==
1378
+ "clearCallbacks") {
1379
+ success = true;
1380
+ }
1120
1381
  }
1121
1382
 
1122
1383
  // Send response back to JS (matches
@@ -1190,12 +1451,28 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1190
1451
  NSString *disableDragDropScript = @"(function() {"
1191
1452
  "if (window.__plusui_nativeFileDropOnly) return;"
1192
1453
  "window.__plusui_nativeFileDropOnly = true;"
1454
+ "var isOverDropZone = function(e) {"
1455
+ "if (!e) return false;"
1456
+ "var target = null;"
1457
+ "if (typeof e.clientX === 'number' && typeof e.clientY === 'number' && document.elementFromPoint) { target = document.elementFromPoint(e.clientX, e.clientY); }"
1458
+ "if (!target && e.target && e.target.nodeType === 1) { target = e.target; }"
1459
+ "if (!target || !target.closest) return false;"
1460
+ "return !!target.closest('[data-dropzone=\\\"true\\\"], .filedrop-zone, .dropzone');"
1461
+ "};"
1462
+ "var zoneActive = false;"
1463
+ "var emitZoneState = function(next) {"
1464
+ "if (next === zoneActive) return;"
1465
+ "zoneActive = next;"
1466
+ "window.dispatchEvent(new CustomEvent(next ? 'plusui:fileDrop.dragEnter' : 'plusui:fileDrop.dragLeave', { detail: {} }));"
1467
+ "};"
1193
1468
  "var block = function(e) {"
1194
1469
  "if (!e) return false;"
1470
+ "var overDropZone = isOverDropZone(e);"
1471
+ "if (e.type === 'drop') { emitZoneState(false); } else { emitZoneState(overDropZone); }"
1195
1472
  "e.preventDefault();"
1196
1473
  "e.stopPropagation();"
1197
1474
  "if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();"
1198
- "if (e.dataTransfer) { try { e.dataTransfer.dropEffect = 'none'; } catch (_) {} }"
1475
+ "if (e.dataTransfer) { try { e.dataTransfer.dropEffect = overDropZone ? 'copy' : 'none'; } catch (_) {} }"
1199
1476
  "return false;"
1200
1477
  "};"
1201
1478
  "window.__plusui_dragDropEvents = ['dragenter','dragover','dragleave','drop'];"
@@ -1670,15 +1947,41 @@ void Window::setWebviewDragDropEnabled(bool enabled) {
1670
1947
  window.__plusui_nativeFileDropOnly = true;
1671
1948
  window.__plusui_dragDropDisabled = true;
1672
1949
 
1950
+ var isOverDropZone = function(e) {
1951
+ if (!e) return false;
1952
+ var target = null;
1953
+ if (typeof e.clientX === 'number' && typeof e.clientY === 'number' && document.elementFromPoint) {
1954
+ target = document.elementFromPoint(e.clientX, e.clientY);
1955
+ }
1956
+ if (!target && e.target && e.target.nodeType === 1) {
1957
+ target = e.target;
1958
+ }
1959
+ if (!target || !target.closest) return false;
1960
+ return !!target.closest('[data-dropzone="true"], .filedrop-zone, .dropzone');
1961
+ };
1962
+
1963
+ var zoneActive = false;
1964
+ var emitZoneState = function(next) {
1965
+ if (next === zoneActive) return;
1966
+ zoneActive = next;
1967
+ window.dispatchEvent(new CustomEvent(next ? 'plusui:fileDrop.dragEnter' : 'plusui:fileDrop.dragLeave', { detail: {} }));
1968
+ };
1969
+
1673
1970
  var block = function(e) {
1674
1971
  if (!e) return false;
1972
+ var overDropZone = isOverDropZone(e);
1973
+ if (e.type === 'drop') {
1974
+ emitZoneState(false);
1975
+ } else {
1976
+ emitZoneState(overDropZone);
1977
+ }
1675
1978
  e.preventDefault();
1676
1979
  e.stopPropagation();
1677
1980
  if (typeof e.stopImmediatePropagation === 'function') {
1678
1981
  e.stopImmediatePropagation();
1679
1982
  }
1680
1983
  if (e.dataTransfer) {
1681
- try { e.dataTransfer.dropEffect = 'none'; } catch (_) {}
1984
+ try { e.dataTransfer.dropEffect = overDropZone ? 'copy' : 'none'; } catch (_) {}
1682
1985
  }
1683
1986
  return false;
1684
1987
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plusui-native-core",
3
- "version": "0.1.45",
3
+ "version": "0.1.48",
4
4
  "description": "PlusUI Core framework (frontend + backend implementations)",
5
5
  "type": "module",
6
6
  "files": [