plusui-native-core 0.1.44 → 0.1.47

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,8 @@
3
3
  #include <mutex>
4
4
  #include <filesystem>
5
5
  #include <fstream>
6
+ #include <unordered_map>
7
+ #include <cctype>
6
8
 
7
9
  #ifdef _WIN32
8
10
  #define WIN32_LEAN_AND_MEAN
@@ -171,8 +173,10 @@ static std::string getMimeType(const std::string& filePath) {
171
173
  // Platform-specific helper to handle dropped files
172
174
  #ifdef _WIN32
173
175
  // Windows implementation for handling WM_DROPFILES message
174
- void HandleDropFiles(FileDrop::Impl* impl, HDROP hDrop) {
175
- if (!impl->enabled || !impl->filesDroppedCallback) {
176
+ void HandleDropFiles(bool enabled,
177
+ const std::function<void(const std::vector<FileInfo>&)>& filesDroppedCallback,
178
+ HDROP hDrop) {
179
+ if (!enabled || !filesDroppedCallback) {
176
180
  return;
177
181
  }
178
182
 
@@ -215,8 +219,8 @@ void HandleDropFiles(FileDrop::Impl* impl, HDROP hDrop) {
215
219
 
216
220
  DragFinish(hDrop);
217
221
 
218
- if (!files.empty() && impl->filesDroppedCallback) {
219
- impl->filesDroppedCallback(files);
222
+ if (!files.empty() && filesDroppedCallback) {
223
+ filesDroppedCallback(files);
220
224
  }
221
225
  }
222
226
  #endif
@@ -225,8 +229,10 @@ void HandleDropFiles(FileDrop::Impl* impl, HDROP hDrop) {
225
229
  // macOS implementation using NSPasteboard and drag&drop APIs
226
230
  // This would be implemented with Objective-C++ integration with NSView
227
231
  // For now, providing the interface structure
228
- void HandleMacDropFiles(FileDrop::Impl* impl, NSArray<NSURL*>* urls) {
229
- if (!impl->enabled || !impl->filesDroppedCallback) {
232
+ void HandleMacDropFiles(bool enabled,
233
+ const std::function<void(const std::vector<FileInfo>&)>& filesDroppedCallback,
234
+ NSArray<NSURL*>* urls) {
235
+ if (!enabled || !filesDroppedCallback) {
230
236
  return;
231
237
  }
232
238
 
@@ -256,16 +262,18 @@ void HandleMacDropFiles(FileDrop::Impl* impl, NSArray<NSURL*>* urls) {
256
262
  files.push_back(info);
257
263
  }
258
264
 
259
- if (!files.empty() && impl->filesDroppedCallback) {
260
- impl->filesDroppedCallback(files);
265
+ if (!files.empty() && filesDroppedCallback) {
266
+ filesDroppedCallback(files);
261
267
  }
262
268
  }
263
269
  #endif
264
270
 
265
271
  #ifdef __linux__
266
272
  // Linux/GTK implementation
267
- void HandleGtkDropFiles(FileDrop::Impl* impl, GtkSelectionData* data) {
268
- if (!impl->enabled || !impl->filesDroppedCallback) {
273
+ void HandleGtkDropFiles(bool enabled,
274
+ const std::function<void(const std::vector<FileInfo>&)>& filesDroppedCallback,
275
+ GtkSelectionData* data) {
276
+ if (!enabled || !filesDroppedCallback) {
269
277
  return;
270
278
  }
271
279
 
@@ -307,8 +315,8 @@ void HandleGtkDropFiles(FileDrop::Impl* impl, GtkSelectionData* data) {
307
315
  g_strfreev(uris);
308
316
  }
309
317
 
310
- if (!files.empty() && impl->filesDroppedCallback) {
311
- impl->filesDroppedCallback(files);
318
+ if (!files.empty() && filesDroppedCallback) {
319
+ filesDroppedCallback(files);
312
320
  }
313
321
  }
314
322
  #endif
@@ -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,81 @@ 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
+ std::string eventScript =
244
+ "window.dispatchEvent(new CustomEvent('plusui:fileDrop.filesDropped',"
245
+ " { detail: { files: " +
246
+ filesJson + " } }));";
247
+
248
+ // Also emit to backend event API so C++ listeners can react.
249
+ event::emit("fileDrop.filesDropped", filesJson);
250
+
251
+ targetImpl->webview->ExecuteScript(
252
+ std::wstring(eventScript.begin(), eventScript.end()).c_str(),
253
+ nullptr);
254
+ return 0;
255
+ }
131
256
  case WM_SETFOCUS:
132
257
  impl->state.isFocused = true;
133
258
  for (auto &cb : impl->focusCallbacks)
@@ -151,6 +276,10 @@ struct Window::Impl {
151
276
 
152
277
  Window::Window() : pImpl(std::shared_ptr<Impl>(new Impl())) {}
153
278
 
279
+ #ifdef _WIN32
280
+ std::map<HWND, Window::Impl *> Window::Impl::embeddedWebviewByParent;
281
+ #endif
282
+
154
283
  Window::~Window() = default;
155
284
 
156
285
  Window::Window(Window &&other) noexcept = default;
@@ -207,6 +336,8 @@ Window Window::create(const WindowConfig &config) {
207
336
  w.pImpl->state.width = config.width;
208
337
  w.pImpl->state.height = config.height;
209
338
 
339
+ DragAcceptFiles(w.pImpl->hwnd, config.enableFileDrop ? TRUE : FALSE);
340
+
210
341
  if (config.center) {
211
342
  RECT screen;
212
343
  SystemParametersInfo(SPI_GETWORKAREA, 0, &screen, 0);
@@ -801,6 +932,8 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
801
932
  controller->get_ParentWindow(&parentHwnd);
802
933
  GetClientRect(parentHwnd, &bounds);
803
934
  controller->put_Bounds(bounds);
935
+ Window::Impl::embeddedWebviewByParent[parentHwnd] =
936
+ pImpl.get();
804
937
 
805
938
  pImpl->nativeWebView = pImpl->webview.Get();
806
939
  pImpl->ready = true;
@@ -841,6 +974,11 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
841
974
 
842
975
  var block = function(e) {
843
976
  if (!e) return false;
977
+ if (e.type === 'dragenter') {
978
+ window.dispatchEvent(new CustomEvent('plusui:fileDrop.dragEnter', { detail: {} }));
979
+ } else if (e.type === 'dragleave') {
980
+ window.dispatchEvent(new CustomEvent('plusui:fileDrop.dragLeave', { detail: {} }));
981
+ }
844
982
  e.preventDefault();
845
983
  e.stopPropagation();
846
984
  if (typeof e.stopImmediatePropagation === 'function') {
@@ -1117,6 +1255,48 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1117
1255
  result = canForward ? "true" : "false";
1118
1256
  success = true;
1119
1257
  }
1258
+ } else if (method.find("fileDrop.") == 0) {
1259
+ std::string fileDropMethod = method.substr(9);
1260
+ if (fileDropMethod == "setEnabled") {
1261
+ size_t p1 = msg.find("[");
1262
+ size_t p2 = msg.find("]");
1263
+ if (p1 != std::string::npos &&
1264
+ p2 != std::string::npos) {
1265
+ std::string params =
1266
+ msg.substr(p1 + 1, p2 - p1 - 1);
1267
+ bool enabled =
1268
+ params.find("true") !=
1269
+ std::string::npos;
1270
+ pImpl->config.enableFileDrop = enabled;
1271
+
1272
+ HWND targetHwnd = nullptr;
1273
+ if (pImpl->window) {
1274
+ targetHwnd = static_cast<HWND>(
1275
+ pImpl->window->nativeHandle());
1276
+ }
1277
+ if (!targetHwnd && pImpl->controller) {
1278
+ pImpl->controller->get_ParentWindow(
1279
+ &targetHwnd);
1280
+ }
1281
+ if (targetHwnd) {
1282
+ DragAcceptFiles(targetHwnd,
1283
+ enabled ? TRUE
1284
+ : FALSE);
1285
+ }
1286
+ }
1287
+ success = true;
1288
+ } else if (fileDropMethod == "isEnabled") {
1289
+ result = pImpl->config.enableFileDrop
1290
+ ? "true"
1291
+ : "false";
1292
+ success = true;
1293
+ } else if (fileDropMethod == "startDrag") {
1294
+ result = "false";
1295
+ success = true;
1296
+ } else if (fileDropMethod ==
1297
+ "clearCallbacks") {
1298
+ success = true;
1299
+ }
1120
1300
  }
1121
1301
 
1122
1302
  // Send response back to JS (matches
@@ -1192,6 +1372,8 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1192
1372
  "window.__plusui_nativeFileDropOnly = true;"
1193
1373
  "var block = function(e) {"
1194
1374
  "if (!e) return false;"
1375
+ "if (e.type === 'dragenter') { window.dispatchEvent(new CustomEvent('plusui:fileDrop.dragEnter', { detail: {} })); }"
1376
+ "else if (e.type === 'dragleave') { window.dispatchEvent(new CustomEvent('plusui:fileDrop.dragLeave', { detail: {} })); }"
1195
1377
  "e.preventDefault();"
1196
1378
  "e.stopPropagation();"
1197
1379
  "if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();"
@@ -1672,6 +1854,11 @@ void Window::setWebviewDragDropEnabled(bool enabled) {
1672
1854
 
1673
1855
  var block = function(e) {
1674
1856
  if (!e) return false;
1857
+ if (e.type === 'dragenter') {
1858
+ window.dispatchEvent(new CustomEvent('plusui:fileDrop.dragEnter', { detail: {} }));
1859
+ } else if (e.type === 'dragleave') {
1860
+ window.dispatchEvent(new CustomEvent('plusui:fileDrop.dragLeave', { detail: {} }));
1861
+ }
1675
1862
  e.preventDefault();
1676
1863
  e.stopPropagation();
1677
1864
  if (typeof e.stopImmediatePropagation === 'function') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plusui-native-core",
3
- "version": "0.1.44",
3
+ "version": "0.1.47",
4
4
  "description": "PlusUI Core framework (frontend + backend implementations)",
5
5
  "type": "module",
6
6
  "files": [