plusui-native-core 0.1.105 → 0.1.106

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/Core/.claude/settings.local.json +7 -0
  2. package/Core/CMakeLists.txt +1 -1
  3. package/Core/Features/API/app-api.ts +28 -28
  4. package/Core/Features/API/browser-api.ts +38 -38
  5. package/Core/Features/API/clipboard-api.ts +21 -21
  6. package/Core/Features/API/display-api.ts +33 -33
  7. package/Core/Features/API/keyboard-api.ts +33 -33
  8. package/Core/Features/API/menu-api.ts +39 -39
  9. package/Core/Features/API/router-api.ts +23 -23
  10. package/Core/Features/API/tray-api.ts +22 -22
  11. package/Core/Features/API/webgpu-api.ts +55 -55
  12. package/Core/Features/App/app.cpp +128 -102
  13. package/Core/Features/Browser/browser.cpp +227 -227
  14. package/Core/Features/Browser/browser.ts +161 -161
  15. package/Core/Features/Clipboard/clipboard.cpp +235 -235
  16. package/Core/Features/Display/display.cpp +212 -212
  17. package/Core/Features/FileDrop/filedrop.cpp +448 -324
  18. package/Core/Features/FileDrop/filedrop.css +421 -421
  19. package/Core/Features/FileDrop/filedrop.ts +0 -7
  20. package/Core/Features/Keyboard/keyboard_linux.cpp +4 -0
  21. package/Core/Features/Router/router.cpp +62 -62
  22. package/Core/Features/Router/router.ts +113 -113
  23. package/Core/Features/Tray/tray.cpp +328 -324
  24. package/Core/Features/WebGPU/webgpu.cpp +948 -948
  25. package/Core/Features/Window/webview.cpp +1009 -1009
  26. package/Core/Features/Window/webview.ts +516 -516
  27. package/Core/Features/Window/window.cpp +2240 -1986
  28. package/Core/include/plusui/api.hpp +237 -237
  29. package/Core/include/plusui/app.hpp +33 -33
  30. package/Core/include/plusui/browser.hpp +67 -67
  31. package/Core/include/plusui/clipboard.hpp +41 -41
  32. package/Core/include/plusui/connect.hpp +340 -340
  33. package/Core/include/plusui/connection.hpp +3 -3
  34. package/Core/include/plusui/display.hpp +90 -90
  35. package/Core/include/plusui/filedrop.hpp +92 -77
  36. package/Core/include/plusui/keyboard.hpp +112 -112
  37. package/Core/include/plusui/menu.hpp +153 -153
  38. package/Core/include/plusui/plusui +18 -18
  39. package/Core/include/plusui/router.hpp +42 -42
  40. package/Core/include/plusui/tray.hpp +94 -94
  41. package/Core/include/plusui/webgpu.hpp +434 -434
  42. package/Core/include/plusui/window.hpp +180 -177
  43. package/Core/scripts/generate-umbrella-header.mjs +77 -77
  44. package/Core/vendor/WebView2EnvironmentOptions.h +406 -406
  45. package/Core/vendor/webview.h +487 -487
  46. package/Core/vendor/webview2.h +52079 -52079
  47. package/README.md +19 -19
  48. package/package.json +1 -1
@@ -1,324 +1,448 @@
1
- #include <plusui/filedrop.hpp>
2
- #include <memory>
3
- #include <mutex>
4
- #include <filesystem>
5
- #include <fstream>
6
- #include <unordered_map>
7
- #include <cctype>
8
-
9
- #ifdef _WIN32
10
- #define WIN32_LEAN_AND_MEAN
11
- #include <windows.h>
12
- #include <shlobj.h>
13
- #include <objidl.h>
14
- #include <oleidl.h>
15
- #include <shellapi.h>
16
- #elif defined(__APPLE__)
17
- #include <AppKit/AppKit.h>
18
- #include <CoreFoundation/CoreFoundation.h>
19
- #else
20
- // Linux/GTK
21
- #include <gtk/gtk.h>
22
- #endif
23
-
24
- namespace PlusUI {
25
-
26
- struct FileDrop::Impl {
27
- std::mutex mtx;
28
- bool enabled = true;
29
- std::function<void(const std::vector<FileInfo>&)> filesDroppedCallback;
30
- std::function<void()> dragEnterCallback;
31
- std::function<void()> dragLeaveCallback;
32
-
33
- #ifdef _WIN32
34
- HWND hwnd = nullptr;
35
- #endif
36
- };
37
-
38
- FileDrop::FileDrop() : pImpl(new Impl()) {}
39
-
40
- FileDrop::~FileDrop() { delete pImpl; }
41
-
42
- void FileDrop::setEnabled(bool enabled) {
43
- std::lock_guard<std::mutex> lock(pImpl->mtx);
44
- pImpl->enabled = enabled;
45
-
46
- #ifdef _WIN32
47
- if (pImpl->hwnd) {
48
- DragAcceptFiles(pImpl->hwnd, enabled ? TRUE : FALSE);
49
- }
50
- #endif
51
- }
52
-
53
- bool FileDrop::isEnabled() const {
54
- std::lock_guard<std::mutex> lock(pImpl->mtx);
55
- return pImpl->enabled;
56
- }
57
-
58
- void FileDrop::onFilesDropped(std::function<void(const std::vector<FileInfo>&)> callback) {
59
- std::lock_guard<std::mutex> lock(pImpl->mtx);
60
- pImpl->filesDroppedCallback = callback;
61
- }
62
-
63
- void FileDrop::onDragEnter(std::function<void()> callback) {
64
- std::lock_guard<std::mutex> lock(pImpl->mtx);
65
- pImpl->dragEnterCallback = callback;
66
- }
67
-
68
- void FileDrop::onDragLeave(std::function<void()> callback) {
69
- std::lock_guard<std::mutex> lock(pImpl->mtx);
70
- pImpl->dragLeaveCallback = callback;
71
- }
72
-
73
- bool FileDrop::startDrag(const std::vector<std::string>& filePaths) {
74
- if (filePaths.empty()) {
75
- return false;
76
- }
77
-
78
- #ifdef _WIN32
79
- // Windows drag source implementation
80
- // This would create an IDropSource and IDataObject to initiate drag
81
- // For now, return false as full implementation requires COM setup
82
- return false;
83
-
84
- #elif defined(__APPLE__)
85
- @autoreleasepool {
86
- // macOS drag source implementation
87
- NSMutableArray* items = [NSMutableArray arrayWithCapacity:filePaths.size()];
88
-
89
- for (const auto& path : filePaths) {
90
- NSString* nsPath = [NSString stringWithUTF8String:path.c_str()];
91
- NSURL* url = [NSURL fileURLWithPath:nsPath];
92
- if (url) {
93
- NSDraggingItem* item = [[NSDraggingItem alloc] initWithPasteboardWriter:url];
94
- [items addObject:item];
95
- }
96
- }
97
-
98
- if ([items count] > 0) {
99
- // Would need NSView reference to begin dragging session
100
- return true;
101
- }
102
- return false;
103
- }
104
-
105
- #else
106
- // Linux/GTK drag source implementation
107
- // Would use gtk_drag_begin_with_coordinates
108
- return false;
109
- #endif
110
- }
111
-
112
- void FileDrop::clearCallbacks() {
113
- std::lock_guard<std::mutex> lock(pImpl->mtx);
114
- pImpl->filesDroppedCallback = nullptr;
115
- pImpl->dragEnterCallback = nullptr;
116
- pImpl->dragLeaveCallback = nullptr;
117
- }
118
-
119
- // Helper function to get MIME type from file extension
120
- static std::string getMimeType(const std::string& filePath) {
121
- namespace fs = std::filesystem;
122
- fs::path p(filePath);
123
- std::string ext = p.extension().string();
124
-
125
- // Convert to lowercase
126
- for (char& c : ext) {
127
- c = tolower(c);
128
- }
129
-
130
- // Common MIME types
131
- static const std::unordered_map<std::string, std::string> mimeTypes = {
132
- {".txt", "text/plain"},
133
- {".html", "text/html"},
134
- {".htm", "text/html"},
135
- {".css", "text/css"},
136
- {".js", "application/javascript"},
137
- {".json", "application/json"},
138
- {".xml", "application/xml"},
139
- {".pdf", "application/pdf"},
140
- {".zip", "application/zip"},
141
- {".tar", "application/x-tar"},
142
- {".gz", "application/gzip"},
143
- {".7z", "application/x-7z-compressed"},
144
- {".png", "image/png"},
145
- {".jpg", "image/jpeg"},
146
- {".jpeg", "image/jpeg"},
147
- {".gif", "image/gif"},
148
- {".bmp", "image/bmp"},
149
- {".svg", "image/svg+xml"},
150
- {".webp", "image/webp"},
151
- {".ico", "image/x-icon"},
152
- {".mp3", "audio/mpeg"},
153
- {".wav", "audio/wav"},
154
- {".ogg", "audio/ogg"},
155
- {".mp4", "video/mp4"},
156
- {".webm", "video/webm"},
157
- {".avi", "video/x-msvideo"},
158
- {".mkv", "video/x-matroska"},
159
- {".ttf", "font/ttf"},
160
- {".otf", "font/otf"},
161
- {".woff", "font/woff"},
162
- {".woff2", "font/woff2"},
163
- };
164
-
165
- auto it = mimeTypes.find(ext);
166
- if (it != mimeTypes.end()) {
167
- return it->second;
168
- }
169
-
170
- return "application/octet-stream";
171
- }
172
-
173
- // Platform-specific helper to handle dropped files
174
- #ifdef _WIN32
175
- // Windows implementation for handling WM_DROPFILES message
176
- void HandleDropFiles(bool enabled,
177
- const std::function<void(const std::vector<FileInfo>&)>& filesDroppedCallback,
178
- HDROP hDrop) {
179
- if (!enabled || !filesDroppedCallback) {
180
- return;
181
- }
182
-
183
- std::vector<FileInfo> files;
184
- UINT fileCount = DragQueryFileW(hDrop, 0xFFFFFFFF, nullptr, 0);
185
-
186
- for (UINT i = 0; i < fileCount; ++i) {
187
- UINT pathLen = DragQueryFileW(hDrop, i, nullptr, 0);
188
- std::wstring wpath(pathLen, L'\0');
189
- DragQueryFileW(hDrop, i, &wpath[0], pathLen + 1);
190
-
191
- // Convert wide string to UTF-8
192
- int size = WideCharToMultiByte(CP_UTF8, 0, wpath.c_str(), -1, nullptr, 0, nullptr, nullptr);
193
- std::string path(size - 1, '\0');
194
- WideCharToMultiByte(CP_UTF8, 0, wpath.c_str(), -1, &path[0], size, nullptr, nullptr);
195
-
196
- FileInfo info;
197
- info.path = path;
198
-
199
- // Get file name
200
- size_t lastSlash = path.find_last_of("\\/");
201
- info.name = (lastSlash != std::string::npos) ? path.substr(lastSlash + 1) : path;
202
-
203
- // Get file size
204
- WIN32_FILE_ATTRIBUTE_DATA fileAttr;
205
- if (GetFileAttributesExW(wpath.c_str(), GetFileExInfoStandard, &fileAttr)) {
206
- LARGE_INTEGER size;
207
- size.HighPart = fileAttr.nFileSizeHigh;
208
- size.LowPart = fileAttr.nFileSizeLow;
209
- info.size = static_cast<std::uint64_t>(size.QuadPart);
210
- } else {
211
- info.size = 0;
212
- }
213
-
214
- // Get MIME type
215
- info.type = getMimeType(path);
216
-
217
- files.push_back(info);
218
- }
219
-
220
- DragFinish(hDrop);
221
-
222
- if (!files.empty() && filesDroppedCallback) {
223
- filesDroppedCallback(files);
224
- }
225
- }
226
- #endif
227
-
228
- #ifdef __APPLE__
229
- // macOS implementation using NSPasteboard and drag&drop APIs
230
- // This would be implemented with Objective-C++ integration with NSView
231
- // For now, providing the interface structure
232
- void HandleMacDropFiles(bool enabled,
233
- const std::function<void(const std::vector<FileInfo>&)>& filesDroppedCallback,
234
- NSArray<NSURL*>* urls) {
235
- if (!enabled || !filesDroppedCallback) {
236
- return;
237
- }
238
-
239
- std::vector<FileInfo> files;
240
-
241
- for (NSURL* url in urls) {
242
- if (![url isFileURL]) continue;
243
-
244
- NSString* path = [url path];
245
- FileInfo info;
246
- info.path = [path UTF8String];
247
- info.name = [[path lastPathComponent] UTF8String];
248
-
249
- // Get file size
250
- NSError* error = nil;
251
- NSDictionary* attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:path error:&error];
252
- if (attrs) {
253
- NSNumber* fileSize = [attrs objectForKey:NSFileSize];
254
- info.size = [fileSize unsignedLongLongValue];
255
- } else {
256
- info.size = 0;
257
- }
258
-
259
- // Get MIME type
260
- info.type = getMimeType(info.path);
261
-
262
- files.push_back(info);
263
- }
264
-
265
- if (!files.empty() && filesDroppedCallback) {
266
- filesDroppedCallback(files);
267
- }
268
- }
269
- #endif
270
-
271
- #ifdef __linux__
272
- // Linux/GTK implementation
273
- void HandleGtkDropFiles(bool enabled,
274
- const std::function<void(const std::vector<FileInfo>&)>& filesDroppedCallback,
275
- GtkSelectionData* data) {
276
- if (!enabled || !filesDroppedCallback) {
277
- return;
278
- }
279
-
280
- std::vector<FileInfo> files;
281
- gchar** uris = gtk_selection_data_get_uris(data);
282
-
283
- if (uris) {
284
- for (int i = 0; uris[i] != nullptr; ++i) {
285
- GFile* file = g_file_new_for_uri(uris[i]);
286
- gchar* path = g_file_get_path(file);
287
-
288
- if (path) {
289
- FileInfo info;
290
- info.path = path;
291
-
292
- gchar* basename = g_file_get_basename(file);
293
- info.name = basename;
294
- g_free(basename);
295
-
296
- // Get file size
297
- GFileInfo* fileInfo = g_file_query_info(file, G_FILE_ATTRIBUTE_STANDARD_SIZE,
298
- G_FILE_QUERY_INFO_NONE, nullptr, nullptr);
299
- if (fileInfo) {
300
- info.size = g_file_info_get_size(fileInfo);
301
- g_object_unref(fileInfo);
302
- } else {
303
- info.size = 0;
304
- }
305
-
306
- // Get MIME type
307
- info.type = getMimeType(info.path);
308
-
309
- files.push_back(info);
310
- g_free(path);
311
- }
312
-
313
- g_object_unref(file);
314
- }
315
- g_strfreev(uris);
316
- }
317
-
318
- if (!files.empty() && filesDroppedCallback) {
319
- filesDroppedCallback(files);
320
- }
321
- }
322
- #endif
323
-
324
- } // namespace PlusUI
1
+ #include <plusui/filedrop.hpp>
2
+ #include <plusui/window.hpp>
3
+ #include <memory>
4
+ #include <mutex>
5
+ #include <filesystem>
6
+ #include <fstream>
7
+ #include <unordered_map>
8
+ #include <cctype>
9
+ #include <iostream>
10
+
11
+ #ifdef _WIN32
12
+ #define WIN32_LEAN_AND_MEAN
13
+ #include <windows.h>
14
+ #include <shlobj.h>
15
+ #include <objidl.h>
16
+ #include <oleidl.h>
17
+ #include <shellapi.h>
18
+ #elif defined(__APPLE__)
19
+ #include <AppKit/AppKit.h>
20
+ #include <CoreFoundation/CoreFoundation.h>
21
+ #import <WebKit/WebKit.h>
22
+ #else
23
+ // Linux/GTK
24
+ #include <gtk/gtk.h>
25
+ #include <webkit2/webkit2.h>
26
+ #endif
27
+
28
+ namespace plusui {
29
+
30
+ static std::string jsonEscape(const std::string &input) {
31
+ std::string out;
32
+ out.reserve(input.size() + 8);
33
+ for (char c : input) {
34
+ switch (c) {
35
+ case '\\': out += "\\\\"; break;
36
+ case '"': out += "\\\""; break;
37
+ case '\n': out += "\\n"; break;
38
+ case '\r': out += "\\r"; break;
39
+ case '\t': out += "\\t"; break;
40
+ default: out += c; break;
41
+ }
42
+ }
43
+ return out;
44
+ }
45
+
46
+ static std::string mimeTypeFromPath(const std::string &path) {
47
+ size_t dot = path.find_last_of(".");
48
+ if (dot == std::string::npos) return "application/octet-stream";
49
+ std::string ext = path.substr(dot + 1);
50
+ for (char &c : ext) c = (char)tolower(c);
51
+ if (ext == "png") return "image/png";
52
+ if (ext == "jpg" || ext == "jpeg") return "image/jpeg";
53
+ if (ext == "gif") return "image/gif";
54
+ if (ext == "html" || ext == "htm") return "text/html";
55
+ if (ext == "js") return "application/javascript";
56
+ if (ext == "json") return "application/json";
57
+ if (ext == "css") return "text/css";
58
+ if (ext == "txt" || ext == "md") return "text/plain";
59
+ if (ext == "svg") return "image/svg+xml";
60
+ if (ext == "pdf") return "application/pdf";
61
+ if (ext == "zip") return "application/zip";
62
+ return "application/octet-stream";
63
+ }
64
+
65
+ struct FileDrop::Impl {
66
+ std::mutex mtx;
67
+ bool enabled = true;
68
+ std::function<void(const std::vector<FileInfo>&)> filesDroppedCallback;
69
+ std::function<void()> dragEnterCallback;
70
+ std::function<void()> dragLeaveCallback;
71
+ void* nativeWindowHandle = nullptr;
72
+ void* nativeWebViewHandle = nullptr;
73
+
74
+ #ifdef _WIN32
75
+ void* dropTarget = nullptr;
76
+ #elif defined(__APPLE__)
77
+ // macOS specific drag context
78
+ #elif defined(__linux__)
79
+ struct DropCtx {
80
+ WebKitWebView *wv;
81
+ bool entered = false;
82
+ gint x = 0, y = 0;
83
+ };
84
+ DropCtx* dc = nullptr;
85
+ #endif
86
+ };
87
+
88
+ FileDrop::FileDrop() : pImpl(new Impl()) {}
89
+
90
+ FileDrop::~FileDrop() {
91
+ #ifdef __linux__
92
+ if (pImpl->dc) delete pImpl->dc;
93
+ #endif
94
+ delete pImpl;
95
+ }
96
+
97
+ #ifdef _WIN32
98
+ class FileDropTarget : public IDropTarget {
99
+ HWND m_hwnd;
100
+ void* m_webview;
101
+ ULONG m_refCount;
102
+ bool m_hasFiles;
103
+
104
+ public:
105
+ FileDropTarget(HWND hwnd, void* webview) : m_hwnd(hwnd), m_webview(webview), m_refCount(1), m_hasFiles(false) {}
106
+
107
+ HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject) override {
108
+ if (riid == IID_IUnknown || riid == IID_IDropTarget) {
109
+ *ppvObject = this;
110
+ AddRef();
111
+ return S_OK;
112
+ }
113
+ *ppvObject = nullptr;
114
+ return E_NOINTERFACE;
115
+ }
116
+ ULONG STDMETHODCALLTYPE AddRef() override { return InterlockedIncrement(&m_refCount); }
117
+ ULONG STDMETHODCALLTYPE Release() override {
118
+ ULONG ref = InterlockedDecrement(&m_refCount);
119
+ if (ref == 0) delete this;
120
+ return ref;
121
+ }
122
+
123
+ void screenToCSS(POINTL pt, int &cssX, int &cssY) {
124
+ POINT ptClient = {pt.x, pt.y};
125
+ ScreenToClient(m_hwnd, &ptClient);
126
+
127
+ double dpiScale = 1.0;
128
+ UINT dpi = GetDpiForWindow(m_hwnd);
129
+ if (dpi > 0) dpiScale = static_cast<double>(dpi) / 96.0;
130
+
131
+ cssX = static_cast<int>(ptClient.x / dpiScale);
132
+ cssY = static_cast<int>(ptClient.y / dpiScale);
133
+ }
134
+
135
+ bool hasFiles(IDataObject *pDataObj) {
136
+ if (!pDataObj) return false;
137
+ FORMATETC fmt = {CF_HDROP, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
138
+ return pDataObj->QueryGetData(&fmt) == S_OK;
139
+ }
140
+
141
+ void execScript(const std::string &js) {
142
+ if (m_webview) {
143
+ std::wstring wjs(js.begin(), js.end());
144
+ ((ICoreWebView2*)m_webview)->ExecuteScript(wjs.c_str(), nullptr);
145
+ }
146
+ }
147
+
148
+ HRESULT STDMETHODCALLTYPE DragEnter(IDataObject *pDataObj, DWORD grfKeyState, POINTL pt, DWORD *pdwEffect) override {
149
+ m_hasFiles = hasFiles(pDataObj);
150
+ if (m_hasFiles) {
151
+ *pdwEffect = DROPEFFECT_COPY;
152
+ int cssX, cssY;
153
+ screenToCSS(pt, cssX, cssY);
154
+ execScript(
155
+ "(function(){"
156
+ "var el=document.elementFromPoint(" + std::to_string(cssX) + "," + std::to_string(cssY) + ");"
157
+ "var zone=el&&el.closest?el.closest('[data-dropzone]'):null;"
158
+ "document.querySelectorAll('.dropzone-active,.filedrop-active')"
159
+ ".forEach(function(z){if(z!==zone){z.classList.remove('dropzone-active');"
160
+ "z.classList.remove('filedrop-active');}});"
161
+ "if(zone){zone.classList.add('dropzone-active');zone.classList.add('filedrop-active');}"
162
+ "})();"
163
+ );
164
+ } else {
165
+ *pdwEffect = DROPEFFECT_NONE;
166
+ }
167
+ return S_OK;
168
+ }
169
+
170
+ HRESULT STDMETHODCALLTYPE DragOver(DWORD grfKeyState, POINTL pt, DWORD *pdwEffect) override {
171
+ if (m_hasFiles) {
172
+ *pdwEffect = DROPEFFECT_COPY;
173
+ int cssX, cssY;
174
+ screenToCSS(pt, cssX, cssY);
175
+ execScript(
176
+ "(function(){"
177
+ "var el=document.elementFromPoint(" + std::to_string(cssX) + "," + std::to_string(cssY) + ");"
178
+ "var zone=el&&el.closest?el.closest('[data-dropzone]'):null;"
179
+ "document.querySelectorAll('.dropzone-active,.filedrop-active')"
180
+ ".forEach(function(z){if(z!==zone){z.classList.remove('dropzone-active');"
181
+ "z.classList.remove('filedrop-active');}});"
182
+ "if(zone){zone.classList.add('dropzone-active');zone.classList.add('filedrop-active');}"
183
+ "})();"
184
+ );
185
+ } else {
186
+ *pdwEffect = DROPEFFECT_NONE;
187
+ }
188
+ return S_OK;
189
+ }
190
+
191
+ HRESULT STDMETHODCALLTYPE DragLeave() override {
192
+ execScript(
193
+ "(function(){"
194
+ "document.querySelectorAll('.dropzone-active,.filedrop-active')"
195
+ ".forEach(function(z){z.classList.remove('dropzone-active');"
196
+ "z.classList.remove('filedrop-active');});"
197
+ "})();"
198
+ );
199
+ return S_OK;
200
+ }
201
+
202
+ HRESULT STDMETHODCALLTYPE Drop(IDataObject *pDataObj, DWORD grfKeyState, POINTL pt, DWORD *pdwEffect) override {
203
+ if (!pDataObj || !m_executeScript) return E_FAIL;
204
+
205
+ FORMATETC fmt = {CF_HDROP, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
206
+ STGMEDIUM stg = {};
207
+
208
+ if (pDataObj->GetData(&fmt, &stg) != S_OK) return E_FAIL;
209
+
210
+ HDROP hDrop = static_cast<HDROP>(stg.hGlobal);
211
+ std::string filesJson = "[";
212
+ UINT fileCount = DragQueryFileW(hDrop, 0xFFFFFFFF, nullptr, 0);
213
+
214
+ for (UINT i = 0; i < fileCount; ++i) {
215
+ UINT pathLen = DragQueryFileW(hDrop, i, nullptr, 0);
216
+ std::wstring wpath(pathLen + 1, L'\0');
217
+ DragQueryFileW(hDrop, i, &wpath[0], pathLen + 1);
218
+ wpath.resize(pathLen);
219
+
220
+ int utf8Len = WideCharToMultiByte(CP_UTF8, 0, wpath.c_str(), -1, nullptr, 0, nullptr, nullptr);
221
+ std::string path;
222
+ if (utf8Len > 0) {
223
+ path.resize(static_cast<size_t>(utf8Len - 1));
224
+ WideCharToMultiByte(CP_UTF8, 0, wpath.c_str(), -1, &path[0], utf8Len, nullptr, nullptr);
225
+ }
226
+
227
+ std::string name = path;
228
+ size_t lastSlash = path.find_last_of("\\/");
229
+ if (lastSlash != std::string::npos) name = path.substr(lastSlash + 1);
230
+
231
+ unsigned long long sizeBytes = 0;
232
+ WIN32_FILE_ATTRIBUTE_DATA fileAttr;
233
+ if (GetFileAttributesExW(wpath.c_str(), GetFileExInfoStandard, &fileAttr)) {
234
+ ULARGE_INTEGER size;
235
+ size.HighPart = fileAttr.nFileSizeHigh;
236
+ size.LowPart = fileAttr.nFileSizeLow;
237
+ sizeBytes = size.QuadPart;
238
+ }
239
+
240
+ if (i > 0) filesJson += ",";
241
+ filesJson += "{\"path\":\"" + jsonEscape(path) + "\",\"name\":\"" +
242
+ jsonEscape(name) + "\",\"type\":\"" +
243
+ jsonEscape(mimeTypeFromPath(path)) +
244
+ "\",\"size\":" + std::to_string(sizeBytes) + "}";
245
+ }
246
+ filesJson += "]";
247
+ ReleaseStgMedium(&stg);
248
+
249
+ int cssX, cssY;
250
+ screenToCSS(pt, cssX, cssY);
251
+
252
+ std::string eventScript =
253
+ "(function(){"
254
+ "var files=" + filesJson + ";"
255
+ "document.querySelectorAll('.dropzone-active,.filedrop-active')"
256
+ ".forEach(function(z){z.classList.remove('dropzone-active');"
257
+ "z.classList.remove('filedrop-active');});"
258
+ "window.dispatchEvent(new CustomEvent('plusui:fileDrop.filesDropped',"
259
+ " { detail: { files: files } }));"
260
+ "var el=document.elementFromPoint(" + std::to_string(cssX) + "," + std::to_string(cssY) + ");"
261
+ "var zone=el&&el.closest?el.closest('[data-dropzone]'):null;"
262
+ "var zoneName=zone?zone.getAttribute('data-dropzone'):null;"
263
+ "if(zoneName&&window.__plusui_fileDrop__){"
264
+ "window.__plusui_fileDrop__(zoneName,files);"
265
+ "}"
266
+ "})();";
267
+
268
+ execScript(eventScript);
269
+ *pdwEffect = DROPEFFECT_COPY;
270
+ return S_OK;
271
+ }
272
+ };
273
+ #endif
274
+
275
+ void FileDrop::attach(void* nativeWindowHandle, void* nativeWebViewHandle) {
276
+ pImpl->nativeWindowHandle = nativeWindowHandle;
277
+ pImpl->nativeWebViewHandle = nativeWebViewHandle;
278
+ if (!pImpl->enabled) return;
279
+
280
+ #ifdef _WIN32
281
+ HWND hwnd = static_cast<HWND>(nativeWindowHandle);
282
+ DragAcceptFiles(hwnd, TRUE);
283
+ FileDropTarget* target = new FileDropTarget(hwnd, nativeWebViewHandle);
284
+ pImpl->dropTarget = target;
285
+ RegisterDragDrop(hwnd, target);
286
+
287
+ #elif defined(__APPLE__)
288
+ // We let the objective-c wrapper handle this via PlusUIView
289
+ // No need to redeclare standard drag drops here if it requires subclassing
290
+ #elif defined(__linux__)
291
+ WebKitWebView* webView = static_cast<WebKitWebView*>(nativeWebViewHandle);
292
+ if (!webView) return;
293
+
294
+ gtk_drag_dest_set(GTK_WIDGET(webView), GTK_DEST_DEFAULT_ALL, nullptr, 0, GDK_ACTION_COPY);
295
+ gtk_drag_dest_add_uri_targets(GTK_WIDGET(webView));
296
+
297
+ pImpl->dc = new Impl::DropCtx{webView, false, 0, 0};
298
+
299
+ // drag-motion
300
+ static auto sDragMotion = +[](GtkWidget *, GdkDragContext *ctx, gint x, gint y, guint time, gpointer data) -> gboolean {
301
+ auto *dc = static_cast<Impl::DropCtx *>(data);
302
+ dc->x = x; dc->y = y;
303
+ if (!dc->entered) {
304
+ dc->entered = true;
305
+ }
306
+ gdk_drag_status(ctx, GDK_ACTION_COPY, time);
307
+
308
+ std::string script = "(function(){"
309
+ "var el=document.elementFromPoint(" + std::to_string(x) + "," + std::to_string(y) + ");"
310
+ "var zone=el&&el.closest?el.closest('[data-dropzone]'):null;"
311
+ "document.querySelectorAll('.dropzone-active,.filedrop-active')"
312
+ ".forEach(function(z){if(z!==zone){z.classList.remove('dropzone-active');"
313
+ "z.classList.remove('filedrop-active');}});"
314
+ "if(zone){zone.classList.add('dropzone-active');zone.classList.add('filedrop-active');}"
315
+ "})();";
316
+ webkit_web_view_run_javascript(dc->wv, script.c_str(), nullptr, nullptr, nullptr);
317
+
318
+ return TRUE;
319
+ };
320
+ g_signal_connect(GTK_WIDGET(webView), "drag-motion", G_CALLBACK(sDragMotion), pImpl->dc);
321
+
322
+ // drag-leave
323
+ static auto sDragLeave = +[](GtkWidget *, GdkDragContext *, guint, gpointer data) -> void {
324
+ auto *dc = static_cast<Impl::DropCtx *>(data);
325
+ dc->entered = false;
326
+ std::string script = "(function(){"
327
+ "document.querySelectorAll('.dropzone-active,.filedrop-active')"
328
+ ".forEach(function(z){z.classList.remove('dropzone-active');"
329
+ "z.classList.remove('filedrop-active');});})();";
330
+ webkit_web_view_run_javascript(dc->wv, script.c_str(), nullptr, nullptr, nullptr);
331
+ };
332
+ g_signal_connect(GTK_WIDGET(webView), "drag-leave", G_CALLBACK(sDragLeave), pImpl->dc);
333
+
334
+ // drag-drop
335
+ static auto sDragDrop = +[](GtkWidget *widget, GdkDragContext *ctx, gint x, gint y, guint time, gpointer data) -> gboolean {
336
+ auto *dc = static_cast<Impl::DropCtx *>(data);
337
+ dc->x = x; dc->y = y;
338
+ GdkAtom target = gtk_drag_dest_find_target(widget, ctx, nullptr);
339
+ if (target == GDK_NONE) { gtk_drag_finish(ctx, FALSE, FALSE, time); return TRUE; }
340
+ gtk_drag_get_data(widget, ctx, target, time);
341
+ return TRUE;
342
+ };
343
+ g_signal_connect(GTK_WIDGET(webView), "drag-drop", G_CALLBACK(sDragDrop), pImpl->dc);
344
+
345
+ // drag-data-received
346
+ static auto sDragDataRcv = +[](GtkWidget *, GdkDragContext *ctx, gint, gint, GtkSelectionData *sel, guint, guint time, gpointer data) -> void {
347
+ auto *dc = static_cast<Impl::DropCtx *>(data);
348
+ dc->entered = false;
349
+ gchar **uris = gtk_selection_data_get_uris(sel);
350
+ if (!uris) { gtk_drag_finish(ctx, FALSE, FALSE, time); return; }
351
+
352
+ std::string filesJson = "[";
353
+ bool first = true;
354
+ for (int i = 0; uris[i]; ++i) {
355
+ GFile *gf = g_file_new_for_uri(uris[i]);
356
+ gchar *path = g_file_get_path(gf);
357
+ if (!path) { g_object_unref(gf); continue; }
358
+
359
+ GFileInfo *fi = g_file_query_info(gf, G_FILE_ATTRIBUTE_STANDARD_SIZE, G_FILE_QUERY_INFO_NONE, nullptr, nullptr);
360
+ guint64 fsize = fi ? (guint64)g_file_info_get_size(fi) : 0;
361
+ if (fi) g_object_unref(fi);
362
+
363
+ std::string fp(path);
364
+ std::string nm = fp.substr(fp.rfind('/') + 1);
365
+
366
+ gboolean uncertain = FALSE;
367
+ gchar *ct = g_content_type_guess(path, nullptr, 0, &uncertain);
368
+ std::string mime = ct ? std::string(ct) : "application/octet-stream";
369
+ g_free(ct); g_free(path); g_object_unref(gf);
370
+
371
+ if (!first) filesJson += ",";
372
+ filesJson += "{\"path\":\"" + jsonEscape(fp) + "\","
373
+ "\"name\":\"" + jsonEscape(nm) + "\","
374
+ "\"type\":\"" + jsonEscape(mime) + "\","
375
+ "\"size\":" + std::to_string(fsize) + "}";
376
+ first = false;
377
+ }
378
+ filesJson += "]";
379
+ g_strfreev(uris);
380
+ if (first) { gtk_drag_finish(ctx, FALSE, FALSE, time); return; }
381
+
382
+ gint x = dc->x, y = dc->y;
383
+ std::string script =
384
+ "(function(){"
385
+ "var files=" + filesJson + ";"
386
+ "document.querySelectorAll('.dropzone-active,.filedrop-active')"
387
+ ".forEach(function(z){z.classList.remove('dropzone-active');"
388
+ "z.classList.remove('filedrop-active');});"
389
+ "window.dispatchEvent(new CustomEvent('plusui:fileDrop.filesDropped',"
390
+ " { detail: { files: files } }));"
391
+ "var el=document.elementFromPoint(" + std::to_string(x) + "," + std::to_string(y) + ");"
392
+ "var zone=el&&el.closest?el.closest('[data-dropzone]'):null;"
393
+ "var zoneName=zone?zone.getAttribute('data-dropzone'):null;"
394
+ "if(zoneName&&window.__plusui_fileDrop__){"
395
+ "window.__plusui_fileDrop__(zoneName,files);"
396
+ "}"
397
+ "})();";
398
+
399
+ webkit_web_view_run_javascript(dc->wv, script.c_str(), nullptr, nullptr, nullptr);
400
+ gtk_drag_finish(ctx, TRUE, FALSE, time);
401
+ };
402
+ g_signal_connect(GTK_WIDGET(webView), "drag-data-received", G_CALLBACK(sDragDataRcv), pImpl->dc);
403
+ #endif
404
+ }
405
+
406
+ void FileDrop::setEnabled(bool enabled) {
407
+ std::lock_guard<std::mutex> lock(pImpl->mtx);
408
+ pImpl->enabled = enabled;
409
+ #ifdef _WIN32
410
+ if (pImpl->nativeWindowHandle) {
411
+ DragAcceptFiles(static_cast<HWND>(pImpl->nativeWindowHandle), enabled ? TRUE : FALSE);
412
+ }
413
+ #endif
414
+ }
415
+
416
+ bool FileDrop::isEnabled() const {
417
+ std::lock_guard<std::mutex> lock(pImpl->mtx);
418
+ return pImpl->enabled;
419
+ }
420
+
421
+ void FileDrop::onFilesDropped(std::function<void(const std::vector<FileInfo>&)> callback) {
422
+ std::lock_guard<std::mutex> lock(pImpl->mtx);
423
+ pImpl->filesDroppedCallback = callback;
424
+ }
425
+
426
+ void FileDrop::onDragEnter(std::function<void()> callback) {
427
+ std::lock_guard<std::mutex> lock(pImpl->mtx);
428
+ pImpl->dragEnterCallback = callback;
429
+ }
430
+
431
+ void FileDrop::onDragLeave(std::function<void()> callback) {
432
+ std::lock_guard<std::mutex> lock(pImpl->mtx);
433
+ pImpl->dragLeaveCallback = callback;
434
+ }
435
+
436
+ bool FileDrop::startDrag(const std::vector<std::string>& filePaths) {
437
+ if (filePaths.empty()) return false;
438
+ return false;
439
+ }
440
+
441
+ void FileDrop::clearCallbacks() {
442
+ std::lock_guard<std::mutex> lock(pImpl->mtx);
443
+ pImpl->filesDroppedCallback = nullptr;
444
+ pImpl->dragEnterCallback = nullptr;
445
+ pImpl->dragLeaveCallback = nullptr;
446
+ }
447
+
448
+ } // namespace plusui