plusui-native-core 0.1.4 → 0.1.5

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 (54) hide show
  1. package/Core/CMakeLists.txt +190 -7
  2. package/Core/Features/App/app.cpp +129 -0
  3. package/Core/Features/App/app.ts +126 -0
  4. package/Core/Features/Browser/browser.cpp +181 -0
  5. package/Core/Features/Browser/browser.ts +182 -0
  6. package/Core/Features/Clipboard/clipboard.cpp +234 -0
  7. package/Core/Features/Clipboard/clipboard.ts +113 -0
  8. package/Core/Features/Display/display.cpp +209 -0
  9. package/Core/Features/Display/display.ts +104 -0
  10. package/Core/Features/Event/Events.ts +166 -0
  11. package/Core/Features/Event/events.cpp +200 -0
  12. package/Core/Features/Keyboard/keyboard.cpp +186 -0
  13. package/Core/Features/Keyboard/keyboard.ts +175 -0
  14. package/Core/Features/Menu/context-menu.css +293 -0
  15. package/Core/Features/Menu/menu.cpp +481 -0
  16. package/Core/Features/Menu/menu.ts +439 -0
  17. package/Core/Features/Tray/tray.cpp +310 -0
  18. package/Core/Features/Tray/tray.ts +68 -0
  19. package/Core/Features/WebGPU/webgpu.cpp +937 -0
  20. package/Core/Features/WebGPU/webgpu.ts +1013 -0
  21. package/Core/Features/WebView/webview.cpp +1052 -0
  22. package/Core/Features/WebView/webview.ts +510 -0
  23. package/Core/Features/Window/window.cpp +664 -0
  24. package/Core/Features/Window/window.ts +142 -0
  25. package/Core/Features/WindowManager/window_manager.cpp +341 -0
  26. package/Core/include/plusui/app.hpp +73 -0
  27. package/Core/include/plusui/browser.hpp +66 -0
  28. package/Core/include/plusui/clipboard.hpp +41 -0
  29. package/Core/include/plusui/events.hpp +58 -0
  30. package/Core/include/{keyboard.hpp → plusui/keyboard.hpp} +21 -44
  31. package/Core/include/plusui/menu.hpp +153 -0
  32. package/Core/include/plusui/tray.hpp +93 -0
  33. package/Core/include/plusui/webgpu.hpp +434 -0
  34. package/Core/include/plusui/webview.hpp +142 -0
  35. package/Core/include/plusui/window.hpp +111 -0
  36. package/Core/include/plusui/window_manager.hpp +57 -0
  37. package/Core/vendor/WebView2EnvironmentOptions.h +406 -0
  38. package/Core/vendor/stb_image.h +7988 -0
  39. package/Core/vendor/webview.h +618 -510
  40. package/Core/vendor/webview2.h +52079 -0
  41. package/README.md +19 -0
  42. package/package.json +12 -15
  43. package/Core/include/app.hpp +0 -121
  44. package/Core/include/menu.hpp +0 -79
  45. package/Core/include/tray.hpp +0 -81
  46. package/Core/include/window.hpp +0 -106
  47. package/Core/src/app.cpp +0 -311
  48. package/Core/src/display.cpp +0 -424
  49. package/Core/src/tray.cpp +0 -275
  50. package/Core/src/window.cpp +0 -528
  51. package/dist/index.d.ts +0 -205
  52. package/dist/index.js +0 -198
  53. package/src/index.ts +0 -574
  54. /package/Core/include/{display.hpp → plusui/display.hpp} +0 -0
@@ -0,0 +1,209 @@
1
+ #ifdef _WIN32
2
+ #ifndef _WIN32_WINNT
3
+ #define _WIN32_WINNT 0x0601
4
+ #endif
5
+ #include <windows.h>
6
+
7
+ #ifndef EXTERN_C
8
+ #ifdef __cplusplus
9
+ #define EXTERN_C extern "C"
10
+ #else
11
+ #define EXTERN_C extern
12
+ #endif
13
+ #endif
14
+
15
+ #include <shellapi.h>
16
+ #include <shlobj.h>
17
+ #include <winuser.h>
18
+
19
+ #ifndef DISPLAY_DEVICE_ATTACHED_TO_SESSION
20
+ #define DISPLAY_DEVICE_ATTACHED_TO_SESSION 0x00000001
21
+ #endif
22
+ #ifndef SM_CXMM
23
+ #define SM_CXMM 43
24
+ #endif
25
+ #ifndef SM_CYMM
26
+ #define SM_CYMM 44
27
+ #endif
28
+
29
+ #pragma comment(lib, "user32.lib")
30
+ #pragma comment(lib, "shell32.lib")
31
+ #endif
32
+
33
+ #include <algorithm>
34
+ #include <plusui/display.hpp>
35
+ #include <vector>
36
+
37
+ #ifdef __APPLE__
38
+ #include <Cocoa/Cocoa.h>
39
+ #include <CoreGraphics/CoreGraphics.h>
40
+ #elif !defined(_WIN32)
41
+ #include <gdk/gdk.h>
42
+ #include <gtk/gtk.h>
43
+ #endif
44
+
45
+ namespace plusui {
46
+
47
+ struct DisplayManager::Impl {
48
+ std::vector<Display> displays;
49
+ std::function<void(const Display &)> onConnectCallback;
50
+ std::function<void(int)> onDisconnectCallback;
51
+ std::function<void(const Display &)> onChangeCallback;
52
+ };
53
+
54
+ DisplayManager::DisplayManager() : pImpl(std::make_unique<Impl>()) {
55
+ refresh();
56
+ }
57
+
58
+ DisplayManager::~DisplayManager() = default;
59
+
60
+ DisplayManager &DisplayManager::instance() {
61
+ static DisplayManager inst;
62
+ return inst;
63
+ }
64
+
65
+ std::vector<Display> DisplayManager::getAllDisplays() {
66
+ #ifdef _WIN32
67
+ pImpl->displays.clear();
68
+
69
+ int i = 0;
70
+ DISPLAY_DEVICEW dd = {};
71
+ dd.cb = sizeof(dd);
72
+
73
+ while (EnumDisplayDevicesW(nullptr, i, &dd, 0)) {
74
+ if (dd.StateFlags & DISPLAY_DEVICE_MIRRORING_DRIVER) {
75
+ i++;
76
+ continue;
77
+ }
78
+
79
+ Display d;
80
+ d.id = i;
81
+ // Widestring to string conversion
82
+ std::wstring wname(dd.DeviceName);
83
+ d.name = std::string(wname.begin(), wname.end());
84
+
85
+ DEVMODEW dm = {};
86
+ dm.dmSize = sizeof(dm);
87
+ if (EnumDisplaySettingsW(dd.DeviceName, ENUM_CURRENT_SETTINGS, &dm)) {
88
+ d.bounds.x = dm.dmPosition.x;
89
+ d.bounds.y = dm.dmPosition.y;
90
+ d.bounds.width = dm.dmPelsWidth;
91
+ d.bounds.height = dm.dmPelsHeight;
92
+ d.resolution.width = dm.dmPelsWidth;
93
+ d.resolution.height = dm.dmPelsHeight;
94
+ d.currentMode.width = dm.dmPelsWidth;
95
+ d.currentMode.height = dm.dmPelsHeight;
96
+ d.currentMode.refreshRate = dm.dmDisplayFrequency;
97
+ }
98
+
99
+ d.isPrimary = (dd.StateFlags & DISPLAY_DEVICE_PRIMARY_DEVICE);
100
+
101
+ HMONITOR hMon = MonitorFromWindow(nullptr, MONITOR_DEFAULTTOPRIMARY);
102
+ MONITORINFO mi = {};
103
+ mi.cbSize = sizeof(mi);
104
+ if (GetMonitorInfoW(hMon, &mi)) {
105
+ d.workArea.x = mi.rcWork.left;
106
+ d.workArea.y = mi.rcWork.top;
107
+ d.workArea.width = mi.rcWork.right - mi.rcWork.left;
108
+ d.workArea.height = mi.rcWork.bottom - mi.rcWork.top;
109
+ }
110
+
111
+ d.scaleFactor = 1.0;
112
+ d.isConnected = (dd.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_SESSION);
113
+
114
+ pImpl->displays.push_back(d);
115
+ i++;
116
+ }
117
+
118
+ #elif defined(__APPLE__)
119
+ pImpl->displays.clear();
120
+
121
+ CGDirectDisplayID mainDisplay = CGMainDisplayID();
122
+ CGDisplayCount count;
123
+ CGGetActiveDisplayList(0, nullptr, &count);
124
+
125
+ std::vector<CGDirectDisplayID> displays(count);
126
+ CGGetActiveDisplayList(count, displays.data(), &count);
127
+
128
+ for (auto displayID : displays) {
129
+ Display d;
130
+ d.id = (int)displayID;
131
+ d.isPrimary = (displayID == mainDisplay);
132
+
133
+ CGRect rect = CGDisplayBounds(displayID);
134
+ d.bounds.x = rect.origin.x;
135
+ d.bounds.y = rect.origin.y;
136
+ d.bounds.width = rect.size.width;
137
+ d.bounds.height = rect.size.height;
138
+ d.resolution.width = rect.size.width;
139
+ d.resolution.height = rect.size.height;
140
+
141
+ // Work area on macOS is tricky without AppKit, but we can use NSScreen in
142
+ // window.cpp if needed. For now, we'll just use the full bounds.
143
+ d.workArea = d.bounds;
144
+
145
+ CGDisplayModeRef mode = CGDisplayCopyDisplayMode(displayID);
146
+ if (mode) {
147
+ d.currentMode.width = CGDisplayModeGetWidth(mode);
148
+ d.currentMode.height = CGDisplayModeGetHeight(mode);
149
+ d.currentMode.refreshRate = CGDisplayModeGetRefreshRate(mode);
150
+ CGDisplayModeRelease(mode);
151
+ }
152
+
153
+ d.scaleFactor = 1.0; // TODO: Get backing scale factor
154
+ d.isConnected = true;
155
+
156
+ pImpl->displays.push_back(d);
157
+ }
158
+ #else
159
+ pImpl->displays.clear();
160
+ GdkDisplay *display = gdk_display_get_default();
161
+ if (display) {
162
+ int n = gdk_display_get_n_monitors(display);
163
+ for (int i = 0; i < n; i++) {
164
+ GdkMonitor *monitor = gdk_display_get_monitor(display, i);
165
+ GdkRectangle rect;
166
+ gdk_monitor_get_geometry(monitor, &rect);
167
+
168
+ Display d;
169
+ d.id = i;
170
+ d.isPrimary = gdk_monitor_is_primary(monitor);
171
+ d.bounds.x = rect.x;
172
+ d.bounds.y = rect.y;
173
+ d.bounds.width = rect.width;
174
+ d.bounds.height = rect.height;
175
+ d.resolution.width = rect.width;
176
+ d.resolution.height = rect.height;
177
+
178
+ gdk_monitor_get_workarea(monitor, &rect);
179
+ d.workArea.x = rect.x;
180
+ d.workArea.y = rect.y;
181
+ d.workArea.width = rect.width;
182
+ d.workArea.height = rect.height;
183
+
184
+ d.scaleFactor = gdk_monitor_get_scale_factor(monitor);
185
+ d.isConnected = true;
186
+ pImpl->displays.push_back(d);
187
+ }
188
+ }
189
+ #endif
190
+ return pImpl->displays;
191
+ }
192
+
193
+ void DisplayManager::refresh() { getAllDisplays(); }
194
+
195
+ void DisplayManager::onDisplayConnected(
196
+ std::function<void(const Display &)> callback) {
197
+ pImpl->onConnectCallback = callback;
198
+ }
199
+
200
+ void DisplayManager::onDisplayDisconnected(std::function<void(int)> callback) {
201
+ pImpl->onDisconnectCallback = callback;
202
+ }
203
+
204
+ void DisplayManager::onDisplayChanged(
205
+ std::function<void(const Display &)> callback) {
206
+ pImpl->onChangeCallback = callback;
207
+ }
208
+
209
+ } // namespace plusui
@@ -0,0 +1,104 @@
1
+ export interface DisplayMode {
2
+ width: number;
3
+ height: number;
4
+ refreshRate: number;
5
+ bitDepth: number;
6
+ }
7
+
8
+ export interface DisplayBounds {
9
+ x: number;
10
+ y: number;
11
+ width: number;
12
+ height: number;
13
+ }
14
+
15
+ export interface DisplayResolution {
16
+ width: number;
17
+ height: number;
18
+ }
19
+
20
+ export interface Display {
21
+ id: number;
22
+ name: string;
23
+ isPrimary: boolean;
24
+ bounds: DisplayBounds;
25
+ workArea: DisplayBounds;
26
+ resolution: DisplayResolution;
27
+ currentMode: DisplayMode;
28
+ scaleFactor: number;
29
+ rotation: number;
30
+ isInternal: boolean;
31
+ isConnected: boolean;
32
+ }
33
+
34
+ export class DisplayAPI {
35
+ constructor(
36
+ private invokeFn: (name: string, args?: unknown[]) => Promise<unknown>
37
+ ) {}
38
+
39
+ async getAllDisplays(): Promise<Display[]> {
40
+ return await this.invokeFn<Display[]>('display.getAllDisplays', []);
41
+ }
42
+
43
+ async getPrimaryDisplay(): Promise<Display> {
44
+ return await this.invokeFn<Display>('display.getPrimaryDisplay', []);
45
+ }
46
+
47
+ async getDisplayAt(x: number, y: number): Promise<Display> {
48
+ return await this.invokeFn<Display>('display.getDisplayAt', [x, y]);
49
+ }
50
+
51
+ async getDisplayAtCursor(): Promise<Display> {
52
+ return await this.invokeFn<Display>('display.getDisplayAtCursor', []);
53
+ }
54
+
55
+ async getDisplayById(id: number): Promise<Display> {
56
+ return await this.invokeFn<Display>('display.getDisplayById', [id]);
57
+ }
58
+
59
+ async setDisplayMode(displayId: number, mode: DisplayMode): Promise<boolean> {
60
+ return await this.invokeFn<boolean>('display.setDisplayMode', [displayId, mode]);
61
+ }
62
+
63
+ async setPosition(displayId: number, x: number, y: number): Promise<boolean> {
64
+ return await this.invokeFn<boolean>('display.setPosition', [displayId, x, y]);
65
+ }
66
+
67
+ async turnOff(displayId: number): Promise<boolean> {
68
+ return await this.invokeFn<boolean>('display.turnOff', [displayId]);
69
+ }
70
+
71
+ async getScreenWidth(): Promise<number> {
72
+ return await this.invokeFn<number>('screen.getWidth', []);
73
+ }
74
+
75
+ async getScreenHeight(): Promise<number> {
76
+ return await this.invokeFn<number>('screen.getHeight', []);
77
+ }
78
+
79
+ async getScaleFactor(): Promise<number> {
80
+ return await this.invokeFn<number>('screen.getScaleFactor', []);
81
+ }
82
+
83
+ async getRefreshRate(): Promise<number> {
84
+ return await this.invokeFn<number>('screen.getRefreshRate', []);
85
+ }
86
+
87
+ onDisplayConnected(callback: (display: Display) => void): () => void {
88
+ return () => {};
89
+ }
90
+
91
+ onDisplayDisconnected(callback: (displayId: number) => void): () => void {
92
+ return () => {};
93
+ }
94
+
95
+ onDisplayChanged(callback: (display: Display) => void): () => void {
96
+ return () => {};
97
+ }
98
+ }
99
+
100
+ export function createDisplayAPI(invokeFn: (name: string, args?: unknown[]) => Promise<unknown>): DisplayAPI {
101
+ return new DisplayAPI(invokeFn);
102
+ }
103
+
104
+ export default DisplayAPI;
@@ -0,0 +1,166 @@
1
+ /**
2
+ * PlusUI Event System - scalable bidirectional events
3
+ *
4
+ * Backend:
5
+ * event::on("myEvent", callback) // Listen for events from frontend
6
+ * event::once("myEvent", callback) // Listen once
7
+ * event::emit("myEvent", data) // Send events to frontend
8
+ *
9
+ * Frontend:
10
+ * const stop = event.on("myEvent", callback) // Listen + unsubscribe
11
+ * event.once("myEvent", callback) // Listen once
12
+ * event.emit("myEvent", data) // Send events to backend
13
+ *
14
+ * Users create custom event names anywhere.
15
+ */
16
+
17
+ type EventCallback<T = unknown> = (data: T) => void;
18
+ type AnyEventCallback = (eventName: string, data: unknown) => void;
19
+ export type Unsubscribe = () => void;
20
+
21
+ const listeners = new Map<string, Set<EventCallback>>();
22
+ const anyListeners = new Set<AnyEventCallback>();
23
+ const attachedDomBridges = new Set<string>();
24
+
25
+ let _invoke: ((method: string, args?: unknown[]) => Promise<unknown>) | null = null;
26
+
27
+ export function setInvokeFn(fn: (method: string, args?: unknown[]) => Promise<unknown>) {
28
+ _invoke = fn;
29
+ }
30
+
31
+ async function invoke(method: string, args?: unknown[]): Promise<unknown> {
32
+ if (!_invoke) {
33
+ if (typeof window !== 'undefined' && (window as any).__invoke__) {
34
+ _invoke = (window as any).__invoke__;
35
+ } else {
36
+ throw new Error('Event API not initialized');
37
+ }
38
+ }
39
+ return _invoke!(method, args);
40
+ }
41
+
42
+ function validateEventName(name: string): void {
43
+ if (!name || !name.trim()) {
44
+ throw new Error('Event name must be a non-empty string');
45
+ }
46
+ }
47
+
48
+ function emitToLocalListeners(name: string, data: unknown): void {
49
+ const eventListeners = listeners.get(name);
50
+ if (eventListeners && eventListeners.size > 0) {
51
+ for (const callback of Array.from(eventListeners)) {
52
+ callback(data);
53
+ }
54
+ }
55
+
56
+ for (const callback of Array.from(anyListeners)) {
57
+ callback(name, data);
58
+ }
59
+ }
60
+
61
+ function ensureDomBridge(name: string): void {
62
+ if (typeof window === 'undefined' || attachedDomBridges.has(name)) {
63
+ return;
64
+ }
65
+
66
+ const domEvent = `plusui:event:${name}`;
67
+ const handler = (event: Event) => {
68
+ emitToLocalListeners(name, (event as CustomEvent).detail);
69
+ };
70
+
71
+ window.addEventListener(domEvent, handler as EventListener);
72
+ attachedDomBridges.add(name);
73
+ }
74
+
75
+ export const event = {
76
+ /**
77
+ * Listen for events from backend
78
+ * Returns an unsubscribe function.
79
+ */
80
+ on: <T = unknown>(name: string, callback: EventCallback<T>): Unsubscribe => {
81
+ validateEventName(name);
82
+
83
+ if (!listeners.has(name)) {
84
+ listeners.set(name, new Set());
85
+ }
86
+ listeners.get(name)!.add(callback as EventCallback);
87
+
88
+ ensureDomBridge(name);
89
+
90
+ return () => {
91
+ event.off(name, callback as EventCallback);
92
+ };
93
+ },
94
+
95
+ /**
96
+ * Listen once for an event from backend.
97
+ */
98
+ once: <T = unknown>(name: string, callback: EventCallback<T>): Unsubscribe => {
99
+ validateEventName(name);
100
+
101
+ let unsubscribe: Unsubscribe = () => {};
102
+ const wrapped = (data: unknown) => {
103
+ unsubscribe();
104
+ callback(data as T);
105
+ };
106
+
107
+ unsubscribe = event.on(name, wrapped);
108
+ return unsubscribe;
109
+ },
110
+
111
+ /**
112
+ * Listen to every incoming backend event that has a registered bridge.
113
+ */
114
+ onAny: (callback: AnyEventCallback): Unsubscribe => {
115
+ if (typeof callback !== 'function') {
116
+ throw new Error('onAny callback must be a function');
117
+ }
118
+
119
+ anyListeners.add(callback);
120
+
121
+ return () => {
122
+ anyListeners.delete(callback);
123
+ };
124
+ },
125
+
126
+ /**
127
+ * Remove one callback from a named event.
128
+ */
129
+ off: (name: string, callback: EventCallback): boolean => {
130
+ validateEventName(name);
131
+
132
+ const set = listeners.get(name);
133
+ if (!set) {
134
+ return false;
135
+ }
136
+
137
+ const removed = set.delete(callback);
138
+ if (set.size === 0) {
139
+ listeners.delete(name);
140
+ }
141
+
142
+ return removed;
143
+ },
144
+
145
+ /**
146
+ * Remove all listeners. If name is set, clear only that event.
147
+ */
148
+ clear: (name?: string): void => {
149
+ if (name === undefined) {
150
+ listeners.clear();
151
+ anyListeners.clear();
152
+ return;
153
+ }
154
+
155
+ validateEventName(name);
156
+ listeners.delete(name);
157
+ },
158
+
159
+ /**
160
+ * Emit events to backend.
161
+ */
162
+ emit: async (name: string, data: unknown): Promise<void> => {
163
+ validateEventName(name);
164
+ await invoke('event.emit', [name, data]);
165
+ },
166
+ };
@@ -0,0 +1,200 @@
1
+ // Event API implementation
2
+ #include <plusui/events.hpp>
3
+ #include <functional>
4
+ #include <unordered_map>
5
+ #include <vector>
6
+ #include <mutex>
7
+ #include <atomic>
8
+ #include <algorithm>
9
+ #include <utility>
10
+ #include <iostream>
11
+
12
+ namespace plusui {
13
+ namespace event {
14
+
15
+ struct ListenerEntry {
16
+ ListenerId id;
17
+ EventCallback callback;
18
+ bool once;
19
+ };
20
+
21
+ struct AnyListenerEntry {
22
+ ListenerId id;
23
+ AnyEventCallback callback;
24
+ bool once;
25
+ };
26
+
27
+ // Event listeners registry
28
+ static std::unordered_map<std::string, std::vector<ListenerEntry>> listeners;
29
+ static std::vector<AnyListenerEntry> any_listeners;
30
+ static std::mutex mutex;
31
+ static std::atomic<ListenerId> next_listener_id{1};
32
+
33
+ // Bridge to frontend (set by webview initialization)
34
+ std::function<void(const std::string&, const EventData&)> __bridge_to_frontend__;
35
+
36
+ // Listen for events from frontend
37
+ ListenerId on(const std::string& name, EventCallback callback) {
38
+ if (name.empty() || !callback) {
39
+ return 0;
40
+ }
41
+
42
+ const ListenerId id = next_listener_id.fetch_add(1);
43
+ std::lock_guard<std::mutex> lock(mutex);
44
+ listeners[name].push_back({id, std::move(callback), false});
45
+ return id;
46
+ }
47
+
48
+ ListenerId once(const std::string& name, EventCallback callback) {
49
+ if (name.empty() || !callback) {
50
+ return 0;
51
+ }
52
+
53
+ const ListenerId id = next_listener_id.fetch_add(1);
54
+ std::lock_guard<std::mutex> lock(mutex);
55
+ listeners[name].push_back({id, std::move(callback), true});
56
+ return id;
57
+ }
58
+
59
+ ListenerId on_any(AnyEventCallback callback) {
60
+ if (!callback) {
61
+ return 0;
62
+ }
63
+
64
+ const ListenerId id = next_listener_id.fetch_add(1);
65
+ std::lock_guard<std::mutex> lock(mutex);
66
+ any_listeners.push_back({id, std::move(callback), false});
67
+ return id;
68
+ }
69
+
70
+ bool off(const std::string& name, ListenerId id) {
71
+ if (name.empty() || id == 0) {
72
+ return false;
73
+ }
74
+
75
+ std::lock_guard<std::mutex> lock(mutex);
76
+ auto it = listeners.find(name);
77
+ if (it == listeners.end()) {
78
+ return false;
79
+ }
80
+
81
+ auto& bucket = it->second;
82
+ const auto old_size = bucket.size();
83
+ bucket.erase(
84
+ std::remove_if(bucket.begin(), bucket.end(), [id](const ListenerEntry& entry) {
85
+ return entry.id == id;
86
+ }),
87
+ bucket.end()
88
+ );
89
+ const auto new_size = bucket.size();
90
+
91
+ if (bucket.empty()) {
92
+ listeners.erase(it);
93
+ }
94
+
95
+ return new_size != old_size;
96
+ }
97
+
98
+ bool off_any(ListenerId id) {
99
+ if (id == 0) {
100
+ return false;
101
+ }
102
+
103
+ std::lock_guard<std::mutex> lock(mutex);
104
+ const auto old_size = any_listeners.size();
105
+ any_listeners.erase(
106
+ std::remove_if(any_listeners.begin(), any_listeners.end(), [id](const AnyListenerEntry& entry) {
107
+ return entry.id == id;
108
+ }),
109
+ any_listeners.end()
110
+ );
111
+
112
+ return any_listeners.size() != old_size;
113
+ }
114
+
115
+ std::size_t off_all(const std::string& name) {
116
+ if (name.empty()) {
117
+ return 0;
118
+ }
119
+
120
+ std::lock_guard<std::mutex> lock(mutex);
121
+ auto it = listeners.find(name);
122
+ if (it == listeners.end()) {
123
+ return 0;
124
+ }
125
+
126
+ const auto removed = it->second.size();
127
+ listeners.erase(it);
128
+ return removed;
129
+ }
130
+
131
+ void clear() {
132
+ std::lock_guard<std::mutex> lock(mutex);
133
+ listeners.clear();
134
+ any_listeners.clear();
135
+ }
136
+
137
+ // Emit events to frontend
138
+ void emit(const std::string& name, const EventData& data) {
139
+ if (__bridge_to_frontend__) {
140
+ __bridge_to_frontend__(name, data);
141
+ }
142
+ }
143
+
144
+ // Internal: Receive event from frontend (called by binding system)
145
+ // This is registered as "event.emit" in the IPC handlers
146
+ void __dispatch_from_frontend__(const std::string& name, const EventData& data) {
147
+ std::vector<ListenerEntry> named_callbacks;
148
+ std::vector<AnyListenerEntry> global_callbacks;
149
+
150
+ {
151
+ std::lock_guard<std::mutex> lock(mutex);
152
+ auto it = listeners.find(name);
153
+ if (it != listeners.end()) {
154
+ named_callbacks = it->second;
155
+ it->second.erase(
156
+ std::remove_if(it->second.begin(), it->second.end(), [](const ListenerEntry& entry) {
157
+ return entry.once;
158
+ }),
159
+ it->second.end()
160
+ );
161
+
162
+ if (it->second.empty()) {
163
+ listeners.erase(it);
164
+ }
165
+ }
166
+
167
+ global_callbacks = any_listeners;
168
+ any_listeners.erase(
169
+ std::remove_if(any_listeners.begin(), any_listeners.end(), [](const AnyListenerEntry& entry) {
170
+ return entry.once;
171
+ }),
172
+ any_listeners.end()
173
+ );
174
+ }
175
+
176
+ for (const auto& entry : named_callbacks) {
177
+ try {
178
+ entry.callback(data);
179
+ } catch (const std::exception& ex) {
180
+ std::cerr << "[PlusUI:event] listener error for '" << name << "': " << ex.what() << std::endl;
181
+ } catch (...) {
182
+ std::cerr << "[PlusUI:event] listener error for '" << name << "': unknown exception" << std::endl;
183
+ }
184
+ }
185
+
186
+ for (const auto& entry : global_callbacks) {
187
+ try {
188
+ entry.callback(name, data);
189
+ } catch (const std::exception& ex) {
190
+ std::cerr << "[PlusUI:event] on_any listener error for '" << name << "': " << ex.what() << std::endl;
191
+ } catch (...) {
192
+ std::cerr << "[PlusUI:event] on_any listener error for '" << name << "': unknown exception" << std::endl;
193
+ }
194
+ }
195
+ }
196
+
197
+ } // namespace event
198
+ } // namespace plusui
199
+
200
+