plusui-native-core 0.1.102 → 0.1.104

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.
@@ -17,15 +17,18 @@
17
17
  #include <wrl.h>
18
18
  using namespace Microsoft::WRL;
19
19
  #pragma warning(pop)
20
- #elif defined(__APPLE__)
21
- #include <Cocoa/Cocoa.h>
22
- #import <WebKit/WebKit.h>
23
- #include <objc/objc-runtime.h>
24
-
25
- #else
26
- #include <gtk/gtk.h>
27
- #include <webkit2/webkit2.h>
28
- #endif
20
+ #elif defined(__APPLE__)
21
+ #include <Cocoa/Cocoa.h>
22
+ #import <AppKit/AppKit.h>
23
+ #import <Carbon/Carbon.h>
24
+ #import <WebKit/WebKit.h>
25
+ #include <objc/objc-runtime.h>
26
+
27
+ #else
28
+ #include <gdk/gdk.h>
29
+ #include <gtk/gtk.h>
30
+ #include <webkit2/webkit2.h>
31
+ #endif
29
32
 
30
33
  #include <stb_image.h>
31
34
 
@@ -82,6 +85,42 @@ static std::string jsonEscape(const std::string &input) {
82
85
  return out;
83
86
  }
84
87
 
88
+ // Map Windows VK code to PlusUI KeyCode (GLFW-style values)
89
+ static int vkToPlusUIKeyCode(WPARAM vk) {
90
+ if (vk >= 'A' && vk <= 'Z') return (int)vk; // A=65…Z=90
91
+ if (vk >= '0' && vk <= '9') return (int)vk; // 0=48…9=57
92
+ if (vk >= VK_F1 && vk <= VK_F12) return 290 + (int)(vk - VK_F1); // F1=290…F12=301
93
+ switch (vk) {
94
+ case VK_SPACE: return 32;
95
+ case VK_ESCAPE: return 256;
96
+ case VK_RETURN: return 257;
97
+ case VK_TAB: return 258;
98
+ case VK_BACK: return 259;
99
+ case VK_DELETE: return 261;
100
+ case VK_RIGHT: return 262;
101
+ case VK_LEFT: return 263;
102
+ case VK_DOWN: return 264;
103
+ case VK_UP: return 265;
104
+ case VK_SHIFT:
105
+ case VK_LSHIFT: return 340;
106
+ case VK_CONTROL:
107
+ case VK_LCONTROL:return 341;
108
+ case VK_MENU:
109
+ case VK_LMENU: return 342;
110
+ default: return 0;
111
+ }
112
+ }
113
+
114
+ // Build the KeyMod flags from current modifier key states
115
+ static int currentKeyMods() {
116
+ int mods = 0;
117
+ if (GetKeyState(VK_SHIFT) & 0x8000) mods |= 1; // KeyMod::Shift
118
+ if (GetKeyState(VK_CONTROL) & 0x8000) mods |= 2; // KeyMod::Control
119
+ if (GetKeyState(VK_MENU) & 0x8000) mods |= 4; // KeyMod::Alt
120
+ if ((GetKeyState(VK_LWIN) | GetKeyState(VK_RWIN)) & 0x8000) mods |= 8; // KeyMod::Super
121
+ return mods;
122
+ }
123
+
85
124
  static std::string mimeTypeFromPath(const std::string &path) {
86
125
  size_t dot = path.find_last_of('.');
87
126
  if (dot == std::string::npos)
@@ -105,10 +144,43 @@ static std::string mimeTypeFromPath(const std::string &path) {
105
144
  return "application/json";
106
145
  if (ext == ".pdf")
107
146
  return "application/pdf";
108
- return "application/octet-stream";
109
- }
110
- #endif
111
- } // namespace
147
+ return "application/octet-stream";
148
+ }
149
+ #endif // _WIN32
150
+
151
+ #if !defined(_WIN32)
152
+ // Build the JS snippet that dispatches plusui:keyboard:keydown/keyup.
153
+ // Used by macOS (NSEvent monitor) and Linux (GDK key signals).
154
+ static std::string buildKeyEventScript(int keyCode, int scancode, int mods,
155
+ bool pressed, bool repeat) {
156
+ std::string evtName = pressed ? "plusui:keyboard:keydown"
157
+ : "plusui:keyboard:keyup";
158
+ return
159
+ "(function(){"
160
+ "var e={key:" + std::to_string(keyCode) +
161
+ ",scancode:" + std::to_string(scancode) +
162
+ ",mods:" + std::to_string(mods) +
163
+ ",pressed:" + (pressed ? "true" : "false") +
164
+ ",repeat:" + (repeat ? "true" : "false") +
165
+ ",keyName:\"\"};"
166
+ "window.dispatchEvent(new CustomEvent('" + evtName + "',{detail:e}));"
167
+ "})();";
168
+ }
169
+
170
+ // Build the JS snippet that dispatches plusui:keyboard:shortcut and calls handler.
171
+ static std::string buildShortcutScript(const std::string& id) {
172
+ return
173
+ "(function(){"
174
+ "var id=\"" + id + "\";"
175
+ "window.dispatchEvent(new CustomEvent('plusui:keyboard:shortcut',{detail:{id:id}}));"
176
+ "if(window.__plusui_shortcut_handlers__&&window.__plusui_shortcut_handlers__[id]){"
177
+ "window.__plusui_shortcut_handlers__[id]();"
178
+ "}"
179
+ "})();";
180
+ }
181
+ #endif // !_WIN32
182
+
183
+ } // namespace
112
184
 
113
185
  struct Window::Impl {
114
186
  void *nativeWindow = nullptr;
@@ -137,8 +209,9 @@ struct Window::Impl {
137
209
  ErrorCallback errorCallback;
138
210
  ConsoleCallback consoleCallback;
139
211
  MessageCallback messageCallback;
140
- FileDropCallback fileDropCallback;
141
- std::map<std::string, JSCallback> bindings;
212
+ FileDropCallback fileDropCallback;
213
+ std::map<std::string, JSCallback> bindings;
214
+ std::map<int, std::string> hotkeys; // hotkey id -> shortcut id
142
215
 
143
216
  std::vector<MoveCallback> moveCallbacks;
144
217
  std::vector<ResizeCallback> resizeCallbacks;
@@ -197,7 +270,7 @@ struct Window::Impl {
197
270
  }
198
271
  }
199
272
 
200
- if (!targetImpl || !targetImpl->config.enableFileDrop ||
273
+ if (!targetImpl || !targetImpl->config.fileDrop ||
201
274
  !targetImpl->webview) {
202
275
  DragFinish(hDrop);
203
276
  return 0;
@@ -257,12 +330,16 @@ struct Window::Impl {
257
330
  targetImpl->fileDropCallback(filesJson);
258
331
  }
259
332
 
260
- // Build the event script that dispatches to the frontend.
261
- // We always fire the global CustomEvent so any listener can react.
262
- // If the drop lands on a [data-dropzone] element we also call the
263
- // zone-specific callback (__plusui_fileDrop__).
264
- int dpx = dropPoint.x;
265
- int dpy = dropPoint.y;
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;
340
+
341
+ int dpx = static_cast<int>(dropPoint.x / dpiScale);
342
+ int dpy = static_cast<int>(dropPoint.y / dpiScale);
266
343
 
267
344
  std::string eventScript =
268
345
  "(function(){"
@@ -273,7 +350,7 @@ struct Window::Impl {
273
350
  "window.dispatchEvent(new "
274
351
  "CustomEvent('plusui:fileDrop.filesDropped',"
275
352
  " { detail: { files: files } }));"
276
- // Zone-specific delivery
353
+ // Zone-specific delivery via DPI-corrected hit test
277
354
  "var el=document.elementFromPoint(" +
278
355
  std::to_string(dpx) + "," + std::to_string(dpy) +
279
356
  ");"
@@ -282,10 +359,9 @@ struct Window::Impl {
282
359
  "if(zoneName&&window.__plusui_fileDrop__){"
283
360
  "window.__plusui_fileDrop__(zoneName,files);"
284
361
  "}"
285
- // If no zone matched but there is only one registered zone,
286
- // deliver there anyway so a simple single-zone app always works.
287
- "if(!zoneName&&window.__plusui_fileDrop__&&"
288
- "window.__plusui_fileDrop_default__){"
362
+ // If no zone matched deliver to all registered zones so a
363
+ // single-zone app always works regardless of hit-test accuracy.
364
+ "if(!zoneName&&window.__plusui_fileDrop_default__){"
289
365
  "window.__plusui_fileDrop_default__(files);"
290
366
  "}"
291
367
  "})();";
@@ -295,20 +371,70 @@ struct Window::Impl {
295
371
  nullptr);
296
372
  return 0;
297
373
  }
298
- case WM_SETFOCUS:
299
- impl->state.isFocused = true;
300
- for (auto &cb : impl->focusCallbacks)
301
- cb(true);
302
- break;
303
- case WM_KILLFOCUS:
304
- impl->state.isFocused = false;
305
- for (auto &cb : impl->focusCallbacks)
306
- cb(false);
307
- break;
308
- }
309
- }
310
- return DefWindowProc(hwnd, msg, wp, lp);
311
- }
374
+ case WM_HOTKEY: {
375
+ int hotKeyId = (int)wp;
376
+ auto it = impl->hotkeys.find(hotKeyId);
377
+ if (it != impl->hotkeys.end() && impl->webview) {
378
+ std::string shortcutId = it->second;
379
+ // Fire keyboard:shortcut event and also trigger any registered
380
+ // shortcutHandlers on the JS side
381
+ std::string script =
382
+ "(function(){"
383
+ "var id=" + std::string("\"") + shortcutId + "\";"
384
+ "window.dispatchEvent(new CustomEvent('plusui:keyboard:shortcut',{detail:{id:id}}));"
385
+ "if(window.__plusui_shortcut_handlers__&&window.__plusui_shortcut_handlers__[id]){"
386
+ "window.__plusui_shortcut_handlers__[id]();"
387
+ "}"
388
+ "})();";
389
+ impl->webview->ExecuteScript(
390
+ std::wstring(script.begin(), script.end()).c_str(), nullptr);
391
+ }
392
+ return 0;
393
+ }
394
+ case WM_KEYDOWN:
395
+ case WM_SYSKEYDOWN:
396
+ case WM_KEYUP:
397
+ case WM_SYSKEYUP: {
398
+ if (impl->webview) {
399
+ bool pressed = (msg == WM_KEYDOWN || msg == WM_SYSKEYDOWN);
400
+ bool repeat = pressed && ((lp & 0x40000000) != 0);
401
+ int keyCode = vkToPlusUIKeyCode(wp);
402
+ if (keyCode != 0) {
403
+ int mods = currentKeyMods();
404
+ // Build a KeyEvent JSON object and dispatch as CustomEvent
405
+ std::string evtName = pressed
406
+ ? "plusui:keyboard:keydown"
407
+ : "plusui:keyboard:keyup";
408
+ std::string script =
409
+ "(function(){"
410
+ "var e={key:" + std::to_string(keyCode) +
411
+ ",scancode:" + std::to_string((int)((lp >> 16) & 0xFF)) +
412
+ ",mods:" + std::to_string(mods) +
413
+ ",pressed:" + (pressed ? "true" : "false") +
414
+ ",repeat:" + (repeat ? "true" : "false") +
415
+ ",keyName:\"\"};"
416
+ "window.dispatchEvent(new CustomEvent('" + evtName + "',{detail:e}));"
417
+ "})();";
418
+ impl->webview->ExecuteScript(
419
+ std::wstring(script.begin(), script.end()).c_str(), nullptr);
420
+ }
421
+ }
422
+ break;
423
+ }
424
+ case WM_SETFOCUS:
425
+ impl->state.isFocused = true;
426
+ for (auto &cb : impl->focusCallbacks)
427
+ cb(true);
428
+ break;
429
+ case WM_KILLFOCUS:
430
+ impl->state.isFocused = false;
431
+ for (auto &cb : impl->focusCallbacks)
432
+ cb(false);
433
+ break;
434
+ }
435
+ }
436
+ return DefWindowProc(hwnd, msg, wp, lp);
437
+ }
312
438
  #elif defined(__APPLE__)
313
439
  WKWebView *wkWebView = nullptr;
314
440
  #else
@@ -378,7 +504,7 @@ Window Window::create(const WindowConfig &config) {
378
504
  w.pImpl->state.width = config.width;
379
505
  w.pImpl->state.height = config.height;
380
506
 
381
- DragAcceptFiles(w.pImpl->hwnd, config.enableFileDrop ? TRUE : FALSE);
507
+ DragAcceptFiles(w.pImpl->hwnd, config.fileDrop ? TRUE : FALSE);
382
508
 
383
509
  if (config.center) {
384
510
  RECT screen;
@@ -943,7 +1069,7 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
943
1069
 
944
1070
  // Pure native file-drop mode: when native FileDrop is enabled,
945
1071
  // fully disable browser/WebView drag-drop handling.
946
- if (win.pImpl->config.enableFileDrop) {
1072
+ if (win.pImpl->config.fileDrop) {
947
1073
  win.pImpl->config.disableWebviewDragDrop = true;
948
1074
  }
949
1075
 
@@ -978,16 +1104,18 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
978
1104
  Window::Impl::embeddedWebviewByParent[parentHwnd] =
979
1105
  pImpl.get();
980
1106
 
981
- // Always allow external drops at the WebView2 level
982
- // so that WM_DROPFILES on the parent HWND fires.
983
- // Browser-level drop behavior (navigating to the file)
984
- // is blocked by the injected JS below instead.
1107
+ // Disable WebView2's internal drop handling so the OS
1108
+ // drop message (WM_DROPFILES) propagates to our WndProc.
1109
+ // When AllowExternalDrop is TRUE, WebView2 consumes the
1110
+ // drop event itself and WM_DROPFILES never fires.
1111
+ // Visual drag feedback (dropzone-active CSS class) is
1112
+ // still handled by the injected JS via dragenter/dragover.
985
1113
  ComPtr<ICoreWebView2Controller4> controller4;
986
1114
  if (controller &&
987
1115
  SUCCEEDED(controller->QueryInterface(
988
1116
  IID_PPV_ARGS(&controller4))) &&
989
1117
  controller4) {
990
- controller4->put_AllowExternalDrop(TRUE);
1118
+ controller4->put_AllowExternalDrop(FALSE);
991
1119
  }
992
1120
 
993
1121
  pImpl->nativeWebView = pImpl->webview.Get();
@@ -1054,7 +1182,7 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1054
1182
  var zone = findDropZone(e);
1055
1183
  updateActiveZone(zone);
1056
1184
  if (e.dataTransfer) {
1057
- try { e.dataTransfer.dropEffect = zone ? 'copy' : 'none'; } catch(_) {}
1185
+ try { e.dataTransfer.dropEffect = zone ? 'move' : 'none'; } catch(_) {}
1058
1186
  }
1059
1187
  }, true);
1060
1188
 
@@ -1063,7 +1191,7 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1063
1191
  var zone = findDropZone(e);
1064
1192
  updateActiveZone(zone);
1065
1193
  if (e.dataTransfer) {
1066
- try { e.dataTransfer.dropEffect = zone ? 'copy' : 'none'; } catch(_) {}
1194
+ try { e.dataTransfer.dropEffect = zone ? 'move' : 'none'; } catch(_) {}
1067
1195
  }
1068
1196
  }, true);
1069
1197
 
@@ -1491,7 +1619,7 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1491
1619
  msg.substr(p1 + 1, p2 - p1 - 1);
1492
1620
  bool enabled = params.find("true") !=
1493
1621
  std::string::npos;
1494
- pImpl->config.enableFileDrop = enabled;
1622
+ pImpl->config.fileDrop = enabled;
1495
1623
 
1496
1624
  HWND targetHwnd = nullptr;
1497
1625
  if (pImpl->window) {
@@ -1524,7 +1652,7 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1524
1652
  }
1525
1653
  success = true;
1526
1654
  } else if (fileDropMethod == "isEnabled") {
1527
- result = pImpl->config.enableFileDrop
1655
+ result = pImpl->config.fileDrop
1528
1656
  ? "true"
1529
1657
  : "false";
1530
1658
  success = true;
@@ -1630,7 +1758,7 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1630
1758
  // Block browser default drag-drop while allowing drop zone visual feedback.
1631
1759
  // File delivery is handled natively by macOS drag APIs, not browser events.
1632
1760
  if (win.pImpl->config.disableWebviewDragDrop ||
1633
- win.pImpl->config.enableFileDrop) {
1761
+ win.pImpl->config.fileDrop) {
1634
1762
  NSString *disableDragDropScript =
1635
1763
  @"(function() {"
1636
1764
  "if (window.__plusui_dropzone_init) return;"
@@ -1666,7 +1794,9 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1666
1794
  "document.addEventListener('dragleave', function(e) {"
1667
1795
  "e.preventDefault();"
1668
1796
  "var zone = findDropZone(e);"
1669
- "updateActiveZone(zone);"
1797
+ "if (e.relatedTarget && zone && !zone.contains(e.relatedTarget)) {"
1798
+ "updateActiveZone(null);"
1799
+ "}"
1670
1800
  "}, true);"
1671
1801
  "document.addEventListener('drop', function(e) {"
1672
1802
  "e.preventDefault();"
@@ -1700,15 +1830,255 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1700
1830
  [[WKWebView alloc] initWithFrame:parentView.bounds configuration:config];
1701
1831
  [parentView addSubview:win.pImpl->wkWebView];
1702
1832
 
1703
- win.pImpl->nativeWebView = (__bridge void *)win.pImpl->wkWebView;
1704
-
1705
- #endif
1706
-
1707
- win.pImpl->trayManager = std::make_unique<TrayManager>();
1708
- win.pImpl->trayManager->setWindowHandle(windowHandle);
1709
-
1710
- return win;
1711
- }
1833
+ win.pImpl->nativeWebView = (__bridge void *)win.pImpl->wkWebView;
1834
+
1835
+ // ── macOS key event forwarding ─────────────────────────────────────────────
1836
+ // NSEvent local monitor: fires for every keyDown/keyUp that goes through
1837
+ // the app's event queue while any app window is key. We convert to
1838
+ // PlusUI KeyCode and dispatch as CustomEvents into the WebView.
1839
+ {
1840
+ WKWebView * __weak wv = win.pImpl->wkWebView;
1841
+
1842
+ // Map macOS virtual keycode + character to PlusUI KeyCode (GLFW style)
1843
+ auto macVKToPlusUI = [](unsigned short vk, unsigned short ch) -> int {
1844
+ if (ch >= 'a' && ch <= 'z') return (int)(ch - 'a' + 65); // A-Z
1845
+ if (ch >= 'A' && ch <= 'Z') return (int)ch;
1846
+ if (ch >= '0' && ch <= '9') return (int)ch; // 0-9
1847
+ static const struct { unsigned short vk; int code; } fkeys[] = {
1848
+ {122,290},{120,291},{99,292},{118,293},{96,294},{97,295},
1849
+ {98,296},{100,297},{101,298},{109,299},{103,300},{111,301}
1850
+ };
1851
+ for (auto& f : fkeys) if (vk == f.vk) return f.code;
1852
+ switch (vk) {
1853
+ case 49: return 32; // Space
1854
+ case 53: return 256; // Escape
1855
+ case 36: return 257; // Return
1856
+ case 48: return 258; // Tab
1857
+ case 51: return 259; // Backspace
1858
+ case 117: return 261; // Forward Delete
1859
+ case 124: return 262; // Right
1860
+ case 123: return 263; // Left
1861
+ case 125: return 264; // Down
1862
+ case 126: return 265; // Up
1863
+ case 56: return 340; // Left Shift
1864
+ case 59: return 341; // Left Control
1865
+ case 58: return 342; // Left Option/Alt
1866
+ default: return 0;
1867
+ }
1868
+ };
1869
+
1870
+ auto buildMods = [](NSEventModifierFlags f) -> int {
1871
+ int m = 0;
1872
+ if (f & NSEventModifierFlagShift) m |= 1;
1873
+ if (f & NSEventModifierFlagControl) m |= 2;
1874
+ if (f & NSEventModifierFlagOption) m |= 4;
1875
+ if (f & NSEventModifierFlagCommand) m |= 8;
1876
+ return m;
1877
+ };
1878
+
1879
+ [NSEvent addLocalMonitorForEventsMatchingMask:
1880
+ NSEventMaskKeyDown | NSEventMaskKeyUp
1881
+ handler:^NSEvent*(NSEvent* event) {
1882
+ if (!wv) return event;
1883
+ unsigned short vk = [event keyCode];
1884
+ unsigned short ch = 0;
1885
+ NSString *chars = [event charactersIgnoringModifiers];
1886
+ if (chars.length > 0) ch = (unsigned short)[chars characterAtIndex:0];
1887
+ int keyCode = macVKToPlusUI(vk, ch);
1888
+ if (keyCode != 0) {
1889
+ bool pressed = ([event type] == NSEventTypeKeyDown);
1890
+ bool repeat = pressed && [event isARepeat];
1891
+ int mods = buildMods([event modifierFlags]);
1892
+ std::string script = buildKeyEventScript(keyCode, (int)vk, mods, pressed, repeat);
1893
+ NSString* js = [NSString stringWithUTF8String:script.c_str()];
1894
+ [wv evaluateJavaScript:js completionHandler:nil];
1895
+ }
1896
+ return event;
1897
+ }];
1898
+
1899
+ // Wire Carbon global hotkey fired callback → evaluateJavaScript.
1900
+ // setShortcutFiredCallback is defined in keyboard_macos.cpp (compiled via
1901
+ // #include in keyboard.cpp) and lives in the same plusui namespace.
1902
+ void setShortcutFiredCallback(std::function<void(const std::string&)>);
1903
+ setShortcutFiredCallback([wv](const std::string& id) {
1904
+ if (!wv) return;
1905
+ std::string script = buildShortcutScript(id);
1906
+ NSString* js = [NSString stringWithUTF8String:script.c_str()];
1907
+ [wv evaluateJavaScript:js completionHandler:nil];
1908
+ });
1909
+ }
1910
+
1911
+ #else
1912
+ // ── Linux / GTK + WebKit2 WebView ─────────────────────────────────────────
1913
+ {
1914
+ GtkWidget *gtkParent = static_cast<GtkWidget*>(windowHandle);
1915
+ WebKitWebView *webView =
1916
+ WEBKIT_WEB_VIEW(webkit_web_view_new());
1917
+ gtk_container_add(GTK_CONTAINER(gtkParent), GTK_WIDGET(webView));
1918
+ gtk_widget_show(GTK_WIDGET(webView));
1919
+ win.pImpl->gtkWebView = webView;
1920
+ win.pImpl->nativeWebView = static_cast<void*>(webView);
1921
+
1922
+ // Inject the native bridge script on every page load
1923
+ WebKitUserContentManager *mgr =
1924
+ webkit_web_view_get_user_content_manager(webView);
1925
+ WebKitUserScript *bridgeScript = webkit_user_script_new(
1926
+ "window.__native_invoke__ = function(req) {"
1927
+ " window.webkit.messageHandlers.plusui.postMessage(req);"
1928
+ "};",
1929
+ WEBKIT_USER_CONTENT_INJECT_TOP_FRAME,
1930
+ WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START,
1931
+ nullptr, nullptr);
1932
+ webkit_user_content_manager_add_script(mgr, bridgeScript);
1933
+ webkit_user_script_unref(bridgeScript);
1934
+
1935
+ // Helper to run JS in this WebView
1936
+ auto runJS = [webView](const std::string& script) {
1937
+ webkit_web_view_run_javascript(webView, script.c_str(),
1938
+ nullptr, nullptr, nullptr);
1939
+ };
1940
+
1941
+ // GDK keyval → PlusUI KeyCode
1942
+ auto gdkKeyToPlusUI = [](guint keyval) -> int {
1943
+ if (keyval >= GDK_KEY_a && keyval <= GDK_KEY_z)
1944
+ return (int)(keyval - GDK_KEY_a + 65);
1945
+ if (keyval >= GDK_KEY_A && keyval <= GDK_KEY_Z)
1946
+ return (int)(keyval - GDK_KEY_A + 65);
1947
+ if (keyval >= GDK_KEY_0 && keyval <= GDK_KEY_9)
1948
+ return (int)(keyval - GDK_KEY_0 + 48);
1949
+ if (keyval >= GDK_KEY_F1 && keyval <= GDK_KEY_F12)
1950
+ return 290 + (int)(keyval - GDK_KEY_F1);
1951
+ switch (keyval) {
1952
+ case GDK_KEY_space: return 32;
1953
+ case GDK_KEY_Escape: return 256;
1954
+ case GDK_KEY_Return: return 257;
1955
+ case GDK_KEY_Tab: return 258;
1956
+ case GDK_KEY_BackSpace: return 259;
1957
+ case GDK_KEY_Delete: return 261;
1958
+ case GDK_KEY_Right: return 262;
1959
+ case GDK_KEY_Left: return 263;
1960
+ case GDK_KEY_Down: return 264;
1961
+ case GDK_KEY_Up: return 265;
1962
+ case GDK_KEY_Shift_L:
1963
+ case GDK_KEY_Shift_R: return 340;
1964
+ case GDK_KEY_Control_L:
1965
+ case GDK_KEY_Control_R: return 341;
1966
+ case GDK_KEY_Alt_L:
1967
+ case GDK_KEY_Alt_R: return 342;
1968
+ default: return 0;
1969
+ }
1970
+ };
1971
+
1972
+ auto buildGdkMods = [](GdkModifierType state) -> int {
1973
+ int m = 0;
1974
+ if (state & GDK_SHIFT_MASK) m |= 1;
1975
+ if (state & GDK_CONTROL_MASK) m |= 2;
1976
+ if (state & GDK_MOD1_MASK) m |= 4; // Alt
1977
+ if (state & GDK_SUPER_MASK) m |= 8;
1978
+ return m;
1979
+ };
1980
+
1981
+ // Key-press callback: capture by value via malloc'd context struct
1982
+ struct KeyCtx {
1983
+ WebKitWebView* webView;
1984
+ std::function<int(guint, int, int, bool, bool)> dispatch;
1985
+ };
1986
+ auto* ctx = new KeyCtx();
1987
+ ctx->webView = webView;
1988
+
1989
+ // key-press-event
1990
+ g_signal_connect(gtkParent, "key-press-event",
1991
+ G_CALLBACK(+[](GtkWidget*, GdkEventKey* ev, gpointer data) -> gboolean {
1992
+ auto* c = static_cast<KeyCtx*>(data);
1993
+ // gdkKeyToPlusUI re-implemented inline (lambda not capturable in C cb)
1994
+ guint kv = ev->keyval;
1995
+ int kc = 0;
1996
+ if (kv >= GDK_KEY_a && kv <= GDK_KEY_z) kc = (int)(kv-GDK_KEY_a+65);
1997
+ else if (kv >= GDK_KEY_A && kv <= GDK_KEY_Z) kc = (int)(kv-GDK_KEY_A+65);
1998
+ else if (kv >= GDK_KEY_0 && kv <= GDK_KEY_9) kc = (int)(kv-GDK_KEY_0+48);
1999
+ else if (kv >= GDK_KEY_F1 && kv <= GDK_KEY_F12) kc = 290+(int)(kv-GDK_KEY_F1);
2000
+ else switch (kv) {
2001
+ case GDK_KEY_space: kc=32; break;
2002
+ case GDK_KEY_Escape: kc=256; break;
2003
+ case GDK_KEY_Return: kc=257; break;
2004
+ case GDK_KEY_Tab: kc=258; break;
2005
+ case GDK_KEY_BackSpace: kc=259; break;
2006
+ case GDK_KEY_Delete: kc=261; break;
2007
+ case GDK_KEY_Right: kc=262; break;
2008
+ case GDK_KEY_Left: kc=263; break;
2009
+ case GDK_KEY_Down: kc=264; break;
2010
+ case GDK_KEY_Up: kc=265; break;
2011
+ case GDK_KEY_Shift_L: case GDK_KEY_Shift_R: kc=340; break;
2012
+ case GDK_KEY_Control_L: case GDK_KEY_Control_R: kc=341; break;
2013
+ case GDK_KEY_Alt_L: case GDK_KEY_Alt_R: kc=342; break;
2014
+ }
2015
+ if (!kc) return FALSE;
2016
+ int mods = 0;
2017
+ if (ev->state & GDK_SHIFT_MASK) mods |= 1;
2018
+ if (ev->state & GDK_CONTROL_MASK) mods |= 2;
2019
+ if (ev->state & GDK_MOD1_MASK) mods |= 4;
2020
+ if (ev->state & GDK_SUPER_MASK) mods |= 8;
2021
+ bool repeat = (ev->send_event == FALSE &&
2022
+ (ev->state & GDK_KEY_PRESS) != 0); // best-effort
2023
+ std::string script = buildKeyEventScript(kc, (int)ev->hardware_keycode, mods, true, repeat);
2024
+ webkit_web_view_run_javascript(c->webView, script.c_str(), nullptr, nullptr, nullptr);
2025
+ return FALSE; // don't consume — let WebKit still receive it
2026
+ }),
2027
+ ctx);
2028
+
2029
+ // key-release-event
2030
+ g_signal_connect(gtkParent, "key-release-event",
2031
+ G_CALLBACK(+[](GtkWidget*, GdkEventKey* ev, gpointer data) -> gboolean {
2032
+ auto* c = static_cast<KeyCtx*>(data);
2033
+ guint kv = ev->keyval;
2034
+ int kc = 0;
2035
+ if (kv >= GDK_KEY_a && kv <= GDK_KEY_z) kc = (int)(kv-GDK_KEY_a+65);
2036
+ else if (kv >= GDK_KEY_A && kv <= GDK_KEY_Z) kc = (int)(kv-GDK_KEY_A+65);
2037
+ else if (kv >= GDK_KEY_0 && kv <= GDK_KEY_9) kc = (int)(kv-GDK_KEY_0+48);
2038
+ else if (kv >= GDK_KEY_F1 && kv <= GDK_KEY_F12) kc = 290+(int)(kv-GDK_KEY_F1);
2039
+ else switch (kv) {
2040
+ case GDK_KEY_space: kc=32; break;
2041
+ case GDK_KEY_Escape: kc=256; break;
2042
+ case GDK_KEY_Return: kc=257; break;
2043
+ case GDK_KEY_Tab: kc=258; break;
2044
+ case GDK_KEY_BackSpace: kc=259; break;
2045
+ case GDK_KEY_Delete: kc=261; break;
2046
+ case GDK_KEY_Right: kc=262; break;
2047
+ case GDK_KEY_Left: kc=263; break;
2048
+ case GDK_KEY_Down: kc=264; break;
2049
+ case GDK_KEY_Up: kc=265; break;
2050
+ case GDK_KEY_Shift_L: case GDK_KEY_Shift_R: kc=340; break;
2051
+ case GDK_KEY_Control_L: case GDK_KEY_Control_R: kc=341; break;
2052
+ case GDK_KEY_Alt_L: case GDK_KEY_Alt_R: kc=342; break;
2053
+ }
2054
+ if (!kc) return FALSE;
2055
+ int mods = 0;
2056
+ if (ev->state & GDK_SHIFT_MASK) mods |= 1;
2057
+ if (ev->state & GDK_CONTROL_MASK) mods |= 2;
2058
+ if (ev->state & GDK_MOD1_MASK) mods |= 4;
2059
+ if (ev->state & GDK_SUPER_MASK) mods |= 8;
2060
+ std::string script = buildKeyEventScript(kc, (int)ev->hardware_keycode, mods, false, false);
2061
+ webkit_web_view_run_javascript(c->webView, script.c_str(), nullptr, nullptr, nullptr);
2062
+ return FALSE;
2063
+ }),
2064
+ ctx);
2065
+
2066
+ // Wire XGrabKey global shortcut fired callback → webkit_web_view_run_javascript.
2067
+ // g_shortcutFiredFn is defined in keyboard_linux.cpp (compiled via #include).
2068
+ extern std::function<void(const std::string&)> g_shortcutFiredFn;
2069
+ g_shortcutFiredFn = [webView](const std::string& id) {
2070
+ std::string script = buildShortcutScript(id);
2071
+ webkit_web_view_run_javascript(webView, script.c_str(), nullptr, nullptr, nullptr);
2072
+ };
2073
+ }
2074
+
2075
+ #endif
2076
+
2077
+ win.pImpl->trayManager = std::make_unique<TrayManager>();
2078
+ win.pImpl->trayManager->setWindowHandle(windowHandle);
2079
+
2080
+ return win;
2081
+ }
1712
2082
 
1713
2083
  TrayManager &Window::tray() { return *pImpl->trayManager; }
1714
2084
 
@@ -74,7 +74,7 @@
74
74
  * .width(1200) .width(1200)
75
75
  * .height(800) .height(800)
76
76
  * .devtools(true) .devtools(true)
77
- * .enableFileDrop(true); .enableFileDrop(true)
77
+ * .fileDrop(true); .fileDrop(true)
78
78
  * .build() ← returns Window
79
79
  * TS: app.quit() C++: app.quit()
80
80
  * TS: app.onReady(cb) C++: (start after build())
@@ -201,7 +201,7 @@
201
201
  * .title("My App")
202
202
  * .width(1200)
203
203
  * .height(800)
204
- * .enableFileDrop(true)
204
+ * .fileDrop(true)
205
205
  * .build();
206
206
  *
207
207
  * MyApp connect;