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.
- package/Core/CMakeLists.txt +190 -7
- package/Core/Features/App/app.cpp +129 -0
- package/Core/Features/App/app.ts +126 -0
- package/Core/Features/Browser/browser.cpp +181 -0
- package/Core/Features/Browser/browser.ts +182 -0
- package/Core/Features/Clipboard/clipboard.cpp +234 -0
- package/Core/Features/Clipboard/clipboard.ts +113 -0
- package/Core/Features/Display/display.cpp +209 -0
- package/Core/Features/Display/display.ts +104 -0
- package/Core/Features/Event/Events.ts +166 -0
- package/Core/Features/Event/events.cpp +200 -0
- package/Core/Features/Keyboard/keyboard.cpp +186 -0
- package/Core/Features/Keyboard/keyboard.ts +175 -0
- package/Core/Features/Menu/context-menu.css +293 -0
- package/Core/Features/Menu/menu.cpp +481 -0
- package/Core/Features/Menu/menu.ts +439 -0
- package/Core/Features/Tray/tray.cpp +310 -0
- package/Core/Features/Tray/tray.ts +68 -0
- package/Core/Features/WebGPU/webgpu.cpp +937 -0
- package/Core/Features/WebGPU/webgpu.ts +1013 -0
- package/Core/Features/WebView/webview.cpp +1052 -0
- package/Core/Features/WebView/webview.ts +510 -0
- package/Core/Features/Window/window.cpp +664 -0
- package/Core/Features/Window/window.ts +142 -0
- package/Core/Features/WindowManager/window_manager.cpp +341 -0
- package/Core/include/plusui/app.hpp +73 -0
- package/Core/include/plusui/browser.hpp +66 -0
- package/Core/include/plusui/clipboard.hpp +41 -0
- package/Core/include/plusui/events.hpp +58 -0
- package/Core/include/{keyboard.hpp → plusui/keyboard.hpp} +21 -44
- package/Core/include/plusui/menu.hpp +153 -0
- package/Core/include/plusui/tray.hpp +93 -0
- package/Core/include/plusui/webgpu.hpp +434 -0
- package/Core/include/plusui/webview.hpp +142 -0
- package/Core/include/plusui/window.hpp +111 -0
- package/Core/include/plusui/window_manager.hpp +57 -0
- package/Core/vendor/WebView2EnvironmentOptions.h +406 -0
- package/Core/vendor/stb_image.h +7988 -0
- package/Core/vendor/webview.h +618 -510
- package/Core/vendor/webview2.h +52079 -0
- package/README.md +19 -0
- package/package.json +12 -15
- package/Core/include/app.hpp +0 -121
- package/Core/include/menu.hpp +0 -79
- package/Core/include/tray.hpp +0 -81
- package/Core/include/window.hpp +0 -106
- package/Core/src/app.cpp +0 -311
- package/Core/src/display.cpp +0 -424
- package/Core/src/tray.cpp +0 -275
- package/Core/src/window.cpp +0 -528
- package/dist/index.d.ts +0 -205
- package/dist/index.js +0 -198
- package/src/index.ts +0 -574
- /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
|
+
|