plusui-native-core 0.1.56 → 0.1.59
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/Features/App/app.cpp +0 -56
- package/Core/Features/App/app.ts +3 -0
- package/Core/Features/Browser/browser.ts +24 -0
- package/Core/Features/Clipboard/clipboard.ts +14 -0
- package/Core/Features/Connection/README.md +77 -38
- package/Core/Features/Connection/connect.cpp +27 -50
- package/Core/Features/Connection/connect.ts +86 -0
- package/Core/Features/Display/display.ts +12 -0
- package/Core/Features/FileDrop/filedrop.ts +14 -0
- package/Core/Features/Keyboard/keyboard.ts +11 -0
- package/Core/Features/Menu/menu.ts +11 -0
- package/Core/Features/Tray/tray.ts +12 -0
- package/Core/Features/WebGPU/webgpu.ts +11 -0
- package/Core/Features/Window/window.cpp +176 -7
- package/Core/Features/Window/window.ts +14 -0
- package/Core/include/plusui/connect.hpp +187 -13
- package/Core/include/plusui/connection.hpp +3 -0
- package/Core/include/plusui/plusui.hpp +1 -0
- package/Core/include/plusui/window.hpp +3 -0
- package/package.json +1 -1
|
@@ -7,31 +7,8 @@
|
|
|
7
7
|
#include <plusui/tray.hpp>
|
|
8
8
|
#include <plusui/window.hpp>
|
|
9
9
|
|
|
10
|
-
#include <bridge.hpp> // Generated generated bridge
|
|
11
|
-
|
|
12
10
|
namespace plusui {
|
|
13
11
|
|
|
14
|
-
// --- Bridge Implementation (Demo) ---
|
|
15
|
-
class DemoBridge : public bridge::GeneratedBridge {
|
|
16
|
-
public:
|
|
17
|
-
nlohmann::json handle_greet(const nlohmann::json &args) override {
|
|
18
|
-
std::string name = args.value("name", "Guest");
|
|
19
|
-
return {{"message", "Hello from C++, " + name + "!"}};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
void handle_log(const nlohmann::json &args) override {
|
|
23
|
-
std::string msg = args.value("msg", "");
|
|
24
|
-
std::cout << "[Client Log] " << msg << std::endl;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
void handle_minimize(const nlohmann::json &args) override {
|
|
28
|
-
// We need access to the window to minimize it.
|
|
29
|
-
// For now, we'll just log. In a real app, Bridge might hold a reference to
|
|
30
|
-
// WindowController.
|
|
31
|
-
std::cout << "[Client] Requested minimize" << std::endl;
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
|
|
35
12
|
struct App::Impl {
|
|
36
13
|
bool running = false;
|
|
37
14
|
};
|
|
@@ -151,39 +128,6 @@ Window App::Builder::build() {
|
|
|
151
128
|
}
|
|
152
129
|
}
|
|
153
130
|
|
|
154
|
-
// --- Initialize Bridge ---
|
|
155
|
-
// Create the bridge instance
|
|
156
|
-
auto bridge = std::make_shared<DemoBridge>();
|
|
157
|
-
|
|
158
|
-
// Hook 1: Window -> Bridge (Inbound)
|
|
159
|
-
webviewWin.onMessage(
|
|
160
|
-
[bridge](const std::string &msg) { bridge->dispatch(msg); });
|
|
161
|
-
|
|
162
|
-
// Hook 2: Bridge -> Window (Outbound)
|
|
163
|
-
// We capture webviewWin by value (it's a lightweight handle)
|
|
164
|
-
bridge->setOutboundHandler([webviewWin](const std::string &msg) mutable {
|
|
165
|
-
webviewWin.postMessage(msg);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
// Keep bridge alive?
|
|
169
|
-
// Since webviewWin.onMessage captures 'bridge', the bridge is kept alive by
|
|
170
|
-
// the Window. And 'bridge' outbound handler captures 'webviewWin', keeping
|
|
171
|
-
// Window alive. Cycle exists, but ensures lifetime match.
|
|
172
|
-
|
|
173
|
-
// Optional: Emit an initial event
|
|
174
|
-
bridge->emit_onResize(config.width, config.height);
|
|
175
|
-
|
|
176
|
-
// Hook 3: File Drop
|
|
177
|
-
webviewWin.onFileDrop([bridge](const std::string &jsonStr) {
|
|
178
|
-
if (jsonStr.empty())
|
|
179
|
-
return;
|
|
180
|
-
try {
|
|
181
|
-
auto j = nlohmann::json::parse(jsonStr);
|
|
182
|
-
bridge->emit_onFileDrop(j);
|
|
183
|
-
} catch (...) {
|
|
184
|
-
}
|
|
185
|
-
});
|
|
186
|
-
|
|
187
131
|
return webviewWin;
|
|
188
132
|
}
|
|
189
133
|
|
package/Core/Features/App/app.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { connect } from '../Connection/connect';
|
|
2
|
+
|
|
1
3
|
export interface AppConfig {
|
|
2
4
|
title?: string;
|
|
3
5
|
width?: number;
|
|
@@ -20,6 +22,7 @@ export interface InvokeOptions {
|
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
export class App {
|
|
25
|
+
public readonly connect = connect.feature('app');
|
|
23
26
|
private initialized = false;
|
|
24
27
|
|
|
25
28
|
constructor(
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { connect } from '../Connection/connect';
|
|
2
|
+
|
|
1
3
|
export interface BrowserState {
|
|
2
4
|
url: string;
|
|
3
5
|
title: string;
|
|
@@ -36,6 +38,8 @@ const _loadErrorCallbacks: ErrorCallback[] = [];
|
|
|
36
38
|
let _currentState: BrowserState = { url: '', title: '', canGoBack: false, canGoForward: false, isLoading: false };
|
|
37
39
|
let _routes: Record<string, string> = {};
|
|
38
40
|
let _currentRoute: string = '/';
|
|
41
|
+
const browserFeatureEvents = connect.feature('browser');
|
|
42
|
+
const routerFeatureEvents = connect.feature('router');
|
|
39
43
|
|
|
40
44
|
// Setup event listeners from native backend
|
|
41
45
|
if (typeof window !== 'undefined') {
|
|
@@ -62,6 +66,16 @@ if (typeof window !== 'undefined') {
|
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
export const browser = {
|
|
69
|
+
connect: browserFeatureEvents,
|
|
70
|
+
|
|
71
|
+
on: <TData = unknown>(name: string, callback: (payload: TData) => void): (() => void) => {
|
|
72
|
+
return browserFeatureEvents.on<TData>(name, callback);
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
emit: <TIn = Record<string, unknown>>(name: string, payload: TIn): void => {
|
|
76
|
+
browserFeatureEvents.emit<TIn>(name, payload);
|
|
77
|
+
},
|
|
78
|
+
|
|
65
79
|
async navigate(url: string): Promise<void> {
|
|
66
80
|
await invoke('browser.navigate', [url]);
|
|
67
81
|
},
|
|
@@ -160,6 +174,16 @@ export const browser = {
|
|
|
160
174
|
};
|
|
161
175
|
|
|
162
176
|
export const router = {
|
|
177
|
+
connect: routerFeatureEvents,
|
|
178
|
+
|
|
179
|
+
on: <TData = unknown>(name: string, callback: (payload: TData) => void): (() => void) => {
|
|
180
|
+
return routerFeatureEvents.on<TData>(name, callback);
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
emit: <TIn = Record<string, unknown>>(name: string, payload: TIn): void => {
|
|
184
|
+
routerFeatureEvents.emit<TIn>(name, payload);
|
|
185
|
+
},
|
|
186
|
+
|
|
163
187
|
setRoutes(routes: Record<string, string>): void {
|
|
164
188
|
_routes = routes;
|
|
165
189
|
},
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { connect } from '../Connection/connect';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Clipboard API - Cross-platform clipboard management
|
|
3
5
|
*
|
|
@@ -6,6 +8,9 @@
|
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
export interface ClipboardAPI {
|
|
11
|
+
on<TData = unknown>(name: string, callback: (payload: TData) => void): () => void;
|
|
12
|
+
emit<TIn = Record<string, unknown>>(name: string, payload: TIn): void;
|
|
13
|
+
|
|
9
14
|
// Text operations
|
|
10
15
|
getText(): Promise<string>;
|
|
11
16
|
setText(text: string): Promise<void>;
|
|
@@ -24,6 +29,7 @@ export interface ClipboardAPI {
|
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
export class Clipboard implements ClipboardAPI {
|
|
32
|
+
public readonly connect = connect.feature('clipboard');
|
|
27
33
|
private invokeFn: (name: string, args?: unknown[]) => Promise<unknown>;
|
|
28
34
|
private onFn: (event: string, callback: (data: unknown) => void) => () => void;
|
|
29
35
|
|
|
@@ -35,6 +41,14 @@ export class Clipboard implements ClipboardAPI {
|
|
|
35
41
|
this.onFn = onFn;
|
|
36
42
|
}
|
|
37
43
|
|
|
44
|
+
on<TData = unknown>(name: string, callback: (payload: TData) => void): () => void {
|
|
45
|
+
return this.connect.on<TData>(name, callback);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
emit<TIn = Record<string, unknown>>(name: string, payload: TIn): void {
|
|
49
|
+
this.connect.emit<TIn>(name, payload);
|
|
50
|
+
}
|
|
51
|
+
|
|
38
52
|
/**
|
|
39
53
|
* Get text from clipboard
|
|
40
54
|
* @returns Current clipboard text content
|
|
@@ -9,7 +9,7 @@ The **Connection Feature** is PlusUI's absurdly simple system for frontend ↔ b
|
|
|
9
9
|
Just `emit()` and `on()` handle all communication patterns:
|
|
10
10
|
|
|
11
11
|
| Keyword | Pattern | Direction | Use Cases |
|
|
12
|
-
|
|
12
|
+
| --------- | --------- | ----------- | ----------- |
|
|
13
13
|
| **EVENT** | Fire & forget | One way → | Notifications, commands, logging |
|
|
14
14
|
| **CALL** | Request/response | Two ways ↔ | Get data, save data, RPC |
|
|
15
15
|
| **STREAM** | Continuous data | One way push → | Real-time updates, monitoring |
|
|
@@ -46,29 +46,18 @@ connect.emit('greet', { name: 'World' });
|
|
|
46
46
|
```cpp
|
|
47
47
|
#include <plusui/connect.hpp>
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
protected:
|
|
51
|
-
void handleMessage(const Envelope& env) override {
|
|
52
|
-
// Listen for messages from frontend
|
|
53
|
-
if (env.kind == MessageKind::Fire && env.name == "greet") {
|
|
54
|
-
std::string name = env.payload["name"];
|
|
55
|
-
|
|
56
|
-
// Respond by emitting back
|
|
57
|
-
emit("greetResponse", {{"message", "Hello, " + name + "!"}});
|
|
58
|
-
}
|
|
59
|
-
}
|
|
49
|
+
plusui::Connect connect;
|
|
60
50
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
51
|
+
connect.on("greet", [&connect](const nlohmann::json& payload) {
|
|
52
|
+
std::string user = payload.value("name", "World");
|
|
53
|
+
connect.emit("greetResponse", {{"message", "Hello, " + user + "!"}});
|
|
54
|
+
});
|
|
66
55
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
};
|
|
56
|
+
// Wire backend connect ↔ window bridge once
|
|
57
|
+
plusui::bindConnect(mainWindow, connect);
|
|
58
|
+
|
|
59
|
+
// Example: backend push to frontend
|
|
60
|
+
connect.emit("resize", {{"width", 1280}, {"height", 720}});
|
|
72
61
|
```
|
|
73
62
|
|
|
74
63
|
### Listen on Frontend
|
|
@@ -98,17 +87,69 @@ connect.on('messageName', (data) => {
|
|
|
98
87
|
### Backend
|
|
99
88
|
|
|
100
89
|
```cpp
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
90
|
+
plusui::Connect connect;
|
|
91
|
+
|
|
92
|
+
// Listen for messages
|
|
93
|
+
connect.on("messageName", [](const nlohmann::json& payload) {
|
|
94
|
+
// process payload
|
|
95
|
+
});
|
|
107
96
|
|
|
108
97
|
// Send messages
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
98
|
+
connect.emit("messageName", {{"key", "value"}});
|
|
99
|
+
|
|
100
|
+
// Bridge window <-> connect runtime
|
|
101
|
+
plusui::bindConnect(mainWindow, connect);
|
|
102
|
+
|
|
103
|
+
// Optional: call primitive support
|
|
104
|
+
connect.onCall("getUser", [](const nlohmann::json& payload) {
|
|
105
|
+
return nlohmann::json{{"id", payload.value("id", 0)}, {"name", "Ada"}};
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Optional: stream/query subscription hooks
|
|
109
|
+
connect.onSubscription("cpu", [](bool subscribed, const nlohmann::json&) {
|
|
110
|
+
if (subscribed) {
|
|
111
|
+
startCpuStream();
|
|
112
|
+
} else {
|
|
113
|
+
stopCpuStream();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Feature-Scoped Names
|
|
119
|
+
|
|
120
|
+
Use `connect` for custom global channels, and feature-scoped APIs when you want automatic namespacing:
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
window.emit('resized', { width: 1200, height: 800 }); // -> "window.resized"
|
|
124
|
+
clipboard.emit('changed', { text: 'hello' }); // -> "clipboard.changed"
|
|
125
|
+
|
|
126
|
+
// Also supported when you need explicit feature connect access:
|
|
127
|
+
window.connect.emit('resized', { width: 1200, height: 800 }); // -> "window.resized"
|
|
128
|
+
connect.emit('custom.appEvent', { ok: true }); // custom/global channel
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Backend can listen using fully scoped names, or use a scoped helper:
|
|
132
|
+
|
|
133
|
+
```cpp
|
|
134
|
+
plusui::Connect connect;
|
|
135
|
+
plusui::bindConnect(mainWindow, connect);
|
|
136
|
+
|
|
137
|
+
auto windowFeature = connect.feature("window");
|
|
138
|
+
windowFeature.on("resized", [](const nlohmann::json& payload) {
|
|
139
|
+
// receives "window.resized"
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
connect.on("window.resized", [](const nlohmann::json& payload) {
|
|
143
|
+
// from window.emit('resized', ...) or window.connect.emit('resized', ...)
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
connect.on("clipboard.changed", [](const nlohmann::json& payload) {
|
|
147
|
+
// from clipboard.emit('changed', ...) or clipboard.connect.emit('changed', ...)
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
connect.on("custom.appEvent", [](const nlohmann::json& payload) {
|
|
151
|
+
// from connect.emit('custom.appEvent', ...)
|
|
152
|
+
});
|
|
112
153
|
```
|
|
113
154
|
|
|
114
155
|
## 💡 Design Your Own Patterns
|
|
@@ -124,13 +165,11 @@ connect.emit('getUser', { id: 123 });
|
|
|
124
165
|
```
|
|
125
166
|
|
|
126
167
|
```cpp
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
}
|
|
168
|
+
plusui::Connect connect;
|
|
169
|
+
connect.onCall("getUser", [](const nlohmann::json& payload) {
|
|
170
|
+
auto user = database.findUser(payload["id"]);
|
|
171
|
+
return nlohmann::json{{"user", user}};
|
|
172
|
+
});
|
|
134
173
|
```
|
|
135
174
|
|
|
136
175
|
### Notifications
|
|
@@ -1,57 +1,34 @@
|
|
|
1
1
|
#include <plusui/connect.hpp>
|
|
2
|
+
#include <plusui/window.hpp>
|
|
2
3
|
|
|
3
4
|
namespace plusui {
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// }
|
|
32
|
-
//
|
|
33
|
-
// // STREAM: continuous updates (start streaming)
|
|
34
|
-
// if (name == "subscribeCPU") {
|
|
35
|
-
// startCPUMonitor(); // Will call emit("cpu", ...) repeatedly
|
|
36
|
-
// }
|
|
37
|
-
//
|
|
38
|
-
// // QUERY: request + stream responses
|
|
39
|
-
// if (name == "search") {
|
|
40
|
-
// auto query = data["query"].get<std::string>();
|
|
41
|
-
// for (auto& result : searchDatabase(query)) {
|
|
42
|
-
// emit("searchResult", result); // Multiple results
|
|
43
|
-
// }
|
|
44
|
-
// }
|
|
45
|
-
//
|
|
46
|
-
// // STATE: synced value
|
|
47
|
-
// if (name == "theme") {
|
|
48
|
-
// currentTheme = data["mode"].get<std::string>();
|
|
49
|
-
// emit("theme", data); // Sync back to frontend
|
|
50
|
-
// }
|
|
51
|
-
// }
|
|
52
|
-
// };
|
|
53
|
-
//
|
|
54
|
-
// See Core/Features/Connection/README.md for more details.
|
|
6
|
+
namespace {
|
|
7
|
+
|
|
8
|
+
std::string makeConnectionDispatchScript(const nlohmann::json &message) {
|
|
9
|
+
return "(function(){const m=" + message.dump() +
|
|
10
|
+
";if(typeof globalThis.__plusuiConnectionMessage === 'function'){"
|
|
11
|
+
"globalThis.__plusuiConnectionMessage(m);}"
|
|
12
|
+
"window.dispatchEvent(new CustomEvent('plusui:connection:message',"
|
|
13
|
+
"{detail:m}));})();";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
} // namespace
|
|
17
|
+
|
|
18
|
+
void bindConnect(Window &window, Connect &connect) {
|
|
19
|
+
window.onMessage([&connect](const std::string &message) {
|
|
20
|
+
connect.dispatchMessage(message);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
connect.setOutbound([&window](const std::string &message) {
|
|
24
|
+
try {
|
|
25
|
+
auto parsed = nlohmann::json::parse(message);
|
|
26
|
+
window.executeScript(makeConnectionDispatchScript(parsed));
|
|
27
|
+
} catch (...) {
|
|
28
|
+
// Ignore malformed outbound payloads
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
55
32
|
|
|
56
33
|
}
|
|
57
34
|
|
|
@@ -180,7 +180,93 @@ export const connect = {
|
|
|
180
180
|
on: <TData = unknown>(name: string, callback: (payload: TData) => void): (() => void) => {
|
|
181
181
|
return connectionClient.on<TData>(name, callback);
|
|
182
182
|
},
|
|
183
|
+
|
|
184
|
+
feature: (scope: string): FeatureConnect => {
|
|
185
|
+
return createFeatureConnect(scope);
|
|
186
|
+
},
|
|
183
187
|
};
|
|
184
188
|
|
|
185
189
|
// Export the raw client for advanced use cases (call, fire, stream, channel)
|
|
186
190
|
export const connection = connectionClient;
|
|
191
|
+
|
|
192
|
+
export type FeatureConnect = {
|
|
193
|
+
emit: <TIn = Record<string, unknown>>(name: string, payload: TIn) => void;
|
|
194
|
+
on: <TData = unknown>(name: string, callback: (payload: TData) => void) => (() => void);
|
|
195
|
+
call: <TOut = unknown, TIn = Record<string, unknown>>(name: string, payload: TIn) => Promise<TOut>;
|
|
196
|
+
stream: <TData = unknown>(name: string) => {
|
|
197
|
+
subscribe: (callback: (payload: TData) => void) => (() => void);
|
|
198
|
+
};
|
|
199
|
+
channel: <TData = unknown>(name: string) => {
|
|
200
|
+
subscribe: (callback: (payload: TData) => void) => (() => void);
|
|
201
|
+
publish: (payload: TData) => void;
|
|
202
|
+
};
|
|
203
|
+
scoped: (scope: string) => FeatureConnect;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
function scopeName(scope: string, name: string): string {
|
|
207
|
+
if (name.startsWith(`${scope}.`)) {
|
|
208
|
+
return name;
|
|
209
|
+
}
|
|
210
|
+
return `${scope}.${name}`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function invokeScoped(method: string, payload?: unknown): Promise<unknown> {
|
|
214
|
+
const host = globalThis as any;
|
|
215
|
+
if (typeof host.__invoke__ !== 'function') {
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const args = payload === undefined ? [] : [payload];
|
|
220
|
+
return host.__invoke__(method, args);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function createFeatureConnect(scope: string): FeatureConnect {
|
|
224
|
+
return {
|
|
225
|
+
emit: <TIn = Record<string, unknown>>(name: string, payload: TIn) => {
|
|
226
|
+
const scoped = scopeName(scope, name);
|
|
227
|
+
void invokeScoped(scoped, payload);
|
|
228
|
+
connect.emit(scoped, payload);
|
|
229
|
+
|
|
230
|
+
if (typeof window !== 'undefined') {
|
|
231
|
+
window.dispatchEvent(new CustomEvent(`plusui:${scoped}`, { detail: payload }));
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
on: <TData = unknown>(name: string, callback: (payload: TData) => void): (() => void) => {
|
|
235
|
+
const scoped = scopeName(scope, name);
|
|
236
|
+
const offConnect = connect.on<TData>(scoped, callback);
|
|
237
|
+
|
|
238
|
+
if (typeof window === 'undefined') {
|
|
239
|
+
return offConnect;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const domHandler = (event: Event) => {
|
|
243
|
+
const custom = event as CustomEvent<TData>;
|
|
244
|
+
callback(custom.detail);
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
window.addEventListener(`plusui:${scoped}`, domHandler as EventListener);
|
|
248
|
+
|
|
249
|
+
return () => {
|
|
250
|
+
offConnect();
|
|
251
|
+
window.removeEventListener(`plusui:${scoped}`, domHandler as EventListener);
|
|
252
|
+
};
|
|
253
|
+
},
|
|
254
|
+
call: <TOut = unknown, TIn = Record<string, unknown>>(name: string, payload: TIn): Promise<TOut> => {
|
|
255
|
+
const scoped = scopeName(scope, name);
|
|
256
|
+
const host = globalThis as any;
|
|
257
|
+
|
|
258
|
+
if (typeof host.__invoke__ === 'function') {
|
|
259
|
+
return invokeScoped(scoped, payload) as Promise<TOut>;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return connection.call<TOut, TIn>(scoped, payload);
|
|
263
|
+
},
|
|
264
|
+
stream: <TData = unknown>(name: string) => {
|
|
265
|
+
return connection.stream<TData>(scopeName(scope, name));
|
|
266
|
+
},
|
|
267
|
+
channel: <TData = unknown>(name: string) => {
|
|
268
|
+
return connection.channel<TData>(scopeName(scope, name));
|
|
269
|
+
},
|
|
270
|
+
scoped: (childScope: string) => createFeatureConnect(scopeName(scope, childScope)),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { connect } from '../Connection/connect';
|
|
2
|
+
|
|
1
3
|
export interface DisplayMode {
|
|
2
4
|
width: number;
|
|
3
5
|
height: number;
|
|
@@ -32,10 +34,20 @@ export interface Display {
|
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
export class DisplayAPI {
|
|
37
|
+
public readonly connect = connect.feature('display');
|
|
38
|
+
|
|
35
39
|
constructor(
|
|
36
40
|
private invokeFn: (name: string, args?: unknown[]) => Promise<unknown>
|
|
37
41
|
) {}
|
|
38
42
|
|
|
43
|
+
on<TData = unknown>(name: string, callback: (payload: TData) => void): () => void {
|
|
44
|
+
return this.connect.on<TData>(name, callback);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
emit<TIn = Record<string, unknown>>(name: string, payload: TIn): void {
|
|
48
|
+
this.connect.emit<TIn>(name, payload);
|
|
49
|
+
}
|
|
50
|
+
|
|
39
51
|
async getAllDisplays(): Promise<Display[]> {
|
|
40
52
|
return await this.invokeFn<Display[]>('display.getAllDisplays', []);
|
|
41
53
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { connect } from '../Connection/connect';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* FileDrop API - Cross-platform drag & drop file handling
|
|
3
5
|
*
|
|
@@ -30,6 +32,9 @@ export interface DragEvent {
|
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export interface FileDropAPI {
|
|
35
|
+
on<TData = unknown>(name: string, callback: (payload: TData) => void): () => void;
|
|
36
|
+
emit<TIn = Record<string, unknown>>(name: string, payload: TIn): void;
|
|
37
|
+
|
|
33
38
|
/**
|
|
34
39
|
* Enable or disable file drop into window
|
|
35
40
|
* @param enabled - true to allow files to be dropped into the window
|
|
@@ -77,6 +82,7 @@ export interface FileDropAPI {
|
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
export class FileDrop implements FileDropAPI {
|
|
85
|
+
public readonly connect = connect.feature('fileDrop');
|
|
80
86
|
private invokeFn: (name: string, args?: unknown[]) => Promise<unknown>;
|
|
81
87
|
private onFn: (event: string, callback: (data: unknown) => void) => () => void;
|
|
82
88
|
|
|
@@ -88,6 +94,14 @@ export class FileDrop implements FileDropAPI {
|
|
|
88
94
|
this.onFn = onFn;
|
|
89
95
|
}
|
|
90
96
|
|
|
97
|
+
on<TData = unknown>(name: string, callback: (payload: TData) => void): () => void {
|
|
98
|
+
return this.connect.on<TData>(name, callback);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
emit<TIn = Record<string, unknown>>(name: string, payload: TIn): void {
|
|
102
|
+
this.connect.emit<TIn>(name, payload);
|
|
103
|
+
}
|
|
104
|
+
|
|
91
105
|
/**
|
|
92
106
|
* Enable or disable file drop into window
|
|
93
107
|
* @param enabled - true to allow files to be dropped into the window
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { connect } from '../Connection/connect';
|
|
2
|
+
|
|
1
3
|
export enum KeyCode {
|
|
2
4
|
Unknown = 0,
|
|
3
5
|
Space = 32,
|
|
@@ -53,6 +55,7 @@ export type KeyEventCallback = (event: KeyEvent) => void;
|
|
|
53
55
|
export type ShortcutCallback = () => void;
|
|
54
56
|
|
|
55
57
|
export class KeyboardAPI {
|
|
58
|
+
public readonly connect = connect.feature('keyboard');
|
|
56
59
|
private shortcutHandlers: Map<string, ShortcutCallback> = new Map();
|
|
57
60
|
|
|
58
61
|
constructor(
|
|
@@ -62,6 +65,14 @@ export class KeyboardAPI {
|
|
|
62
65
|
this.setupEventListeners();
|
|
63
66
|
}
|
|
64
67
|
|
|
68
|
+
on<TData = unknown>(name: string, callback: (payload: TData) => void): () => void {
|
|
69
|
+
return this.connect.on<TData>(name, callback);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
emit<TIn = Record<string, unknown>>(name: string, payload: TIn): void {
|
|
73
|
+
this.connect.emit<TIn>(name, payload);
|
|
74
|
+
}
|
|
75
|
+
|
|
65
76
|
private setupEventListeners(): void {
|
|
66
77
|
this.eventFn('keyboard:keydown', (data) => {
|
|
67
78
|
const event = data as KeyEvent;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { connect } from '../Connection/connect';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* PlusUI Menu API
|
|
3
5
|
*
|
|
@@ -68,6 +70,7 @@ export interface ContextInfo {
|
|
|
68
70
|
// ============================================================================
|
|
69
71
|
|
|
70
72
|
export class MenuAPI {
|
|
73
|
+
public readonly connect = connect.feature('menu');
|
|
71
74
|
private clickHandlers: Map<string, (item: MenuItem) => void> = new Map();
|
|
72
75
|
private contextMenuItems: MenuItem[] = [];
|
|
73
76
|
private contextMenuEnabled = false;
|
|
@@ -85,6 +88,14 @@ export class MenuAPI {
|
|
|
85
88
|
});
|
|
86
89
|
}
|
|
87
90
|
|
|
91
|
+
on<TData = unknown>(name: string, callback: (payload: TData) => void): () => void {
|
|
92
|
+
return this.connect.on<TData>(name, callback);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
emit<TIn = Record<string, unknown>>(name: string, payload: TIn): void {
|
|
96
|
+
this.connect.emit<TIn>(name, payload);
|
|
97
|
+
}
|
|
98
|
+
|
|
88
99
|
// ========================================================================
|
|
89
100
|
// Popup Menus
|
|
90
101
|
// ========================================================================
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { connect } from '../Connection/connect';
|
|
2
|
+
|
|
1
3
|
export interface TrayMenuItem {
|
|
2
4
|
id: string;
|
|
3
5
|
label: string;
|
|
@@ -16,11 +18,21 @@ export interface TrayIconData {
|
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export class TrayAPI {
|
|
21
|
+
public readonly connect = connect.feature('tray');
|
|
22
|
+
|
|
19
23
|
constructor(
|
|
20
24
|
private invokeFn: (name: string, args?: unknown[]) => Promise<unknown>,
|
|
21
25
|
private eventFn: (event: string, callback: (...args: unknown[]) => void) => () => void
|
|
22
26
|
) {}
|
|
23
27
|
|
|
28
|
+
on<TData = unknown>(name: string, callback: (payload: TData) => void): () => void {
|
|
29
|
+
return this.connect.on<TData>(name, callback);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
emit<TIn = Record<string, unknown>>(name: string, payload: TIn): void {
|
|
33
|
+
this.connect.emit<TIn>(name, payload);
|
|
34
|
+
}
|
|
35
|
+
|
|
24
36
|
async setIcon(iconPath: string): Promise<void> {
|
|
25
37
|
await this.invokeFn('tray.setIcon', [iconPath]);
|
|
26
38
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { connect } from '../Connection/connect';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* WebGPU Frontend API Wrapper
|
|
3
5
|
*
|
|
@@ -561,6 +563,7 @@ export function createWebGPU(
|
|
|
561
563
|
* Global WebGPU namespace (like navigator.gpu)
|
|
562
564
|
*/
|
|
563
565
|
class WebGPUNamespace {
|
|
566
|
+
public readonly connect = connect.feature('webgpu');
|
|
564
567
|
private invokeFn: (name: string, args?: unknown[]) => Promise<unknown>;
|
|
565
568
|
private onFn: (event: string, callback: (data: unknown) => void) => () => void;
|
|
566
569
|
|
|
@@ -572,6 +575,14 @@ class WebGPUNamespace {
|
|
|
572
575
|
this.onFn = onFn;
|
|
573
576
|
}
|
|
574
577
|
|
|
578
|
+
on<TData = unknown>(name: string, callback: (payload: TData) => void): () => void {
|
|
579
|
+
return this.connect.on<TData>(name, callback);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
emit<TIn = Record<string, unknown>>(name: string, payload: TIn): void {
|
|
583
|
+
this.connect.emit<TIn>(name, payload);
|
|
584
|
+
}
|
|
585
|
+
|
|
575
586
|
async requestAdapter(options?: GPURequestAdapterOptions): Promise<GPUAdapter | null> {
|
|
576
587
|
const result = await this.invokeFn('webgpu.requestAdapter', [options || {}]);
|
|
577
588
|
if (!result) {
|
|
@@ -138,6 +138,7 @@ struct Window::Impl {
|
|
|
138
138
|
ConsoleCallback consoleCallback;
|
|
139
139
|
MessageCallback messageCallback;
|
|
140
140
|
FileDropCallback fileDropCallback;
|
|
141
|
+
std::map<std::string, JSCallback> bindings;
|
|
141
142
|
|
|
142
143
|
std::vector<MoveCallback> moveCallbacks;
|
|
143
144
|
std::vector<ResizeCallback> resizeCallbacks;
|
|
@@ -1107,17 +1108,14 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
|
|
|
1107
1108
|
std::cout << "[PlusUI] Received: " << msg
|
|
1108
1109
|
<< std::endl;
|
|
1109
1110
|
|
|
1111
|
+
bool handledByMessageCallback = false;
|
|
1112
|
+
|
|
1110
1113
|
// New Generic Message Handler
|
|
1111
1114
|
if (pImpl->messageCallback) {
|
|
1112
1115
|
pImpl->messageCallback(msg);
|
|
1113
|
-
// If handled by generic handler, do we
|
|
1114
|
-
// stop? For now, let's allow legacy to run
|
|
1115
|
-
// too for backward compat UNLESS the
|
|
1116
|
-
// message is clearly for the new system.
|
|
1117
|
-
// The new system uses "kind" field.
|
|
1118
1116
|
if (msg.find("\"kind\"") !=
|
|
1119
1117
|
std::string::npos) {
|
|
1120
|
-
|
|
1118
|
+
handledByMessageCallback = true;
|
|
1121
1119
|
}
|
|
1122
1120
|
}
|
|
1123
1121
|
|
|
@@ -1164,8 +1162,126 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
|
|
|
1164
1162
|
id = getId();
|
|
1165
1163
|
method = getMethod();
|
|
1166
1164
|
|
|
1165
|
+
auto extractFirstParam = [&msg]() {
|
|
1166
|
+
size_t paramsPos = msg.find("\"params\"");
|
|
1167
|
+
if (paramsPos == std::string::npos)
|
|
1168
|
+
return std::string("null");
|
|
1169
|
+
size_t colonPos = msg.find(":", paramsPos);
|
|
1170
|
+
if (colonPos == std::string::npos)
|
|
1171
|
+
return std::string("null");
|
|
1172
|
+
size_t arrayStart = msg.find("[", colonPos);
|
|
1173
|
+
if (arrayStart == std::string::npos)
|
|
1174
|
+
return std::string("null");
|
|
1175
|
+
|
|
1176
|
+
size_t i = arrayStart + 1;
|
|
1177
|
+
while (i < msg.size() &&
|
|
1178
|
+
std::isspace(
|
|
1179
|
+
static_cast<unsigned char>(msg[i])))
|
|
1180
|
+
++i;
|
|
1181
|
+
if (i >= msg.size() || msg[i] == ']')
|
|
1182
|
+
return std::string("null");
|
|
1183
|
+
|
|
1184
|
+
size_t start = i;
|
|
1185
|
+
if (msg[i] == '"') {
|
|
1186
|
+
++i;
|
|
1187
|
+
bool escaped = false;
|
|
1188
|
+
while (i < msg.size()) {
|
|
1189
|
+
char c = msg[i];
|
|
1190
|
+
if (escaped) {
|
|
1191
|
+
escaped = false;
|
|
1192
|
+
} else if (c == '\\') {
|
|
1193
|
+
escaped = true;
|
|
1194
|
+
} else if (c == '"') {
|
|
1195
|
+
++i;
|
|
1196
|
+
break;
|
|
1197
|
+
}
|
|
1198
|
+
++i;
|
|
1199
|
+
}
|
|
1200
|
+
return msg.substr(start, i - start);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
int depth = 0;
|
|
1204
|
+
bool inString = false;
|
|
1205
|
+
bool escaped = false;
|
|
1206
|
+
while (i < msg.size()) {
|
|
1207
|
+
char c = msg[i];
|
|
1208
|
+
if (inString) {
|
|
1209
|
+
if (escaped) {
|
|
1210
|
+
escaped = false;
|
|
1211
|
+
} else if (c == '\\') {
|
|
1212
|
+
escaped = true;
|
|
1213
|
+
} else if (c == '"') {
|
|
1214
|
+
inString = false;
|
|
1215
|
+
}
|
|
1216
|
+
} else {
|
|
1217
|
+
if (c == '"') {
|
|
1218
|
+
inString = true;
|
|
1219
|
+
} else if (c == '{' || c == '[') {
|
|
1220
|
+
++depth;
|
|
1221
|
+
} else if (c == '}' || c == ']') {
|
|
1222
|
+
if (depth == 0) {
|
|
1223
|
+
break;
|
|
1224
|
+
}
|
|
1225
|
+
--depth;
|
|
1226
|
+
} else if (c == ',' && depth == 0) {
|
|
1227
|
+
break;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
++i;
|
|
1231
|
+
}
|
|
1232
|
+
return msg.substr(start, i - start);
|
|
1233
|
+
};
|
|
1234
|
+
|
|
1235
|
+
auto decodeJsonString =
|
|
1236
|
+
[](const std::string &input) {
|
|
1237
|
+
if (input.size() < 2 ||
|
|
1238
|
+
input.front() != '"' ||
|
|
1239
|
+
input.back() != '"') {
|
|
1240
|
+
return input;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
std::string decoded;
|
|
1244
|
+
decoded.reserve(input.size() - 2);
|
|
1245
|
+
for (size_t i = 1; i + 1 < input.size();
|
|
1246
|
+
++i) {
|
|
1247
|
+
char c = input[i];
|
|
1248
|
+
if (c == '\\' && i + 1 < input.size() - 1) {
|
|
1249
|
+
char next = input[++i];
|
|
1250
|
+
switch (next) {
|
|
1251
|
+
case '"':
|
|
1252
|
+
decoded.push_back('"');
|
|
1253
|
+
break;
|
|
1254
|
+
case '\\':
|
|
1255
|
+
decoded.push_back('\\');
|
|
1256
|
+
break;
|
|
1257
|
+
case '/':
|
|
1258
|
+
decoded.push_back('/');
|
|
1259
|
+
break;
|
|
1260
|
+
case 'n':
|
|
1261
|
+
decoded.push_back('\n');
|
|
1262
|
+
break;
|
|
1263
|
+
case 'r':
|
|
1264
|
+
decoded.push_back('\r');
|
|
1265
|
+
break;
|
|
1266
|
+
case 't':
|
|
1267
|
+
decoded.push_back('\t');
|
|
1268
|
+
break;
|
|
1269
|
+
default:
|
|
1270
|
+
decoded.push_back(next);
|
|
1271
|
+
break;
|
|
1272
|
+
}
|
|
1273
|
+
} else {
|
|
1274
|
+
decoded.push_back(c);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
return decoded;
|
|
1278
|
+
};
|
|
1279
|
+
|
|
1167
1280
|
// Route to handlers
|
|
1168
|
-
if (
|
|
1281
|
+
if (handledByMessageCallback) {
|
|
1282
|
+
success = true;
|
|
1283
|
+
result = "null";
|
|
1284
|
+
} else if (method.find("window.") == 0) {
|
|
1169
1285
|
std::string winMethod = method.substr(7);
|
|
1170
1286
|
if (winMethod == "minimize") {
|
|
1171
1287
|
if (pImpl->window)
|
|
@@ -1417,6 +1533,30 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
|
|
|
1417
1533
|
"clearCallbacks") {
|
|
1418
1534
|
success = true;
|
|
1419
1535
|
}
|
|
1536
|
+
} else if (method.find("webview.") == 0) {
|
|
1537
|
+
std::string bindingName = method.substr(8);
|
|
1538
|
+
auto bindingIt =
|
|
1539
|
+
pImpl->bindings.find(bindingName);
|
|
1540
|
+
if (bindingIt != pImpl->bindings.end()) {
|
|
1541
|
+
std::string rawParam =
|
|
1542
|
+
extractFirstParam();
|
|
1543
|
+
std::string callbackArg =
|
|
1544
|
+
decodeJsonString(rawParam);
|
|
1545
|
+
try {
|
|
1546
|
+
result =
|
|
1547
|
+
bindingIt->second(callbackArg);
|
|
1548
|
+
if (result.empty()) {
|
|
1549
|
+
result = "null";
|
|
1550
|
+
}
|
|
1551
|
+
success = true;
|
|
1552
|
+
} catch (const std::exception &e) {
|
|
1553
|
+
std::cerr
|
|
1554
|
+
<< "[PlusUI] webview binding error: "
|
|
1555
|
+
<< e.what() << std::endl;
|
|
1556
|
+
result = "null";
|
|
1557
|
+
success = true;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1420
1560
|
}
|
|
1421
1561
|
|
|
1422
1562
|
// Send response back to JS (matches
|
|
@@ -1654,6 +1794,35 @@ void Window::onFileDrop(FileDropCallback callback) {
|
|
|
1654
1794
|
pImpl->fileDropCallback = callback;
|
|
1655
1795
|
}
|
|
1656
1796
|
|
|
1797
|
+
void Window::bind(const std::string &name, JSCallback callback) {
|
|
1798
|
+
pImpl->bindings[name] = callback;
|
|
1799
|
+
|
|
1800
|
+
std::string bridgeScript =
|
|
1801
|
+
"window." + name +
|
|
1802
|
+
R"( = function(...args) {
|
|
1803
|
+
if (window.plusui && typeof window.plusui.invoke === 'function') {
|
|
1804
|
+
const payload = args.length === 0 ? null : (args.length === 1 ? args[0] : args);
|
|
1805
|
+
return window.plusui.invoke('webview.)" +
|
|
1806
|
+
name +
|
|
1807
|
+
R"(', [payload]);
|
|
1808
|
+
}
|
|
1809
|
+
if (window.__invoke__) {
|
|
1810
|
+
const payload = args.length === 0 ? null : (args.length === 1 ? args[0] : args);
|
|
1811
|
+
return window.__invoke__('webview.)" +
|
|
1812
|
+
name +
|
|
1813
|
+
R"(', [payload]);
|
|
1814
|
+
}
|
|
1815
|
+
return Promise.resolve(null);
|
|
1816
|
+
};)";
|
|
1817
|
+
|
|
1818
|
+
executeScript(bridgeScript);
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
void Window::unbind(const std::string &name) {
|
|
1822
|
+
pImpl->bindings.erase(name);
|
|
1823
|
+
executeScript("delete window." + name + ";");
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1657
1826
|
void Window::loadURL(const std::string &url) { navigate(url); }
|
|
1658
1827
|
|
|
1659
1828
|
void Window::loadHTML(const std::string &html) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { connect } from '../Connection/connect';
|
|
2
|
+
|
|
1
3
|
export interface WindowSize {
|
|
2
4
|
width: number;
|
|
3
5
|
height: number;
|
|
@@ -41,7 +43,19 @@ async function invoke(method: string, args?: unknown[]): Promise<unknown> {
|
|
|
41
43
|
return _invoke(method, args);
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
const windowFeatureEvents = connect.feature('window');
|
|
47
|
+
|
|
44
48
|
export const window = {
|
|
49
|
+
connect: windowFeatureEvents,
|
|
50
|
+
|
|
51
|
+
on: <TData = unknown>(name: string, callback: (payload: TData) => void): (() => void) => {
|
|
52
|
+
return windowFeatureEvents.on<TData>(name, callback);
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
emit: <TIn = Record<string, unknown>>(name: string, payload: TIn): void => {
|
|
56
|
+
windowFeatureEvents.emit<TIn>(name, payload);
|
|
57
|
+
},
|
|
58
|
+
|
|
45
59
|
async minimize(id?: WindowId): Promise<void> {
|
|
46
60
|
await invoke('window.minimize', id ? [id] : []);
|
|
47
61
|
},
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#pragma once
|
|
2
2
|
|
|
3
3
|
#include <functional>
|
|
4
|
+
#include <map>
|
|
4
5
|
#include <nlohmann/json.hpp>
|
|
5
6
|
#include <string>
|
|
7
|
+
#include <vector>
|
|
6
8
|
|
|
7
9
|
// ============================================
|
|
8
10
|
// PlusUI Connect Feature
|
|
@@ -60,12 +62,99 @@
|
|
|
60
62
|
|
|
61
63
|
namespace plusui {
|
|
62
64
|
|
|
65
|
+
class Window;
|
|
66
|
+
|
|
63
67
|
class Connect {
|
|
68
|
+
public:
|
|
69
|
+
using EventHandler = std::function<void(const nlohmann::json &)>;
|
|
70
|
+
using CallHandler =
|
|
71
|
+
std::function<nlohmann::json(const nlohmann::json &)>;
|
|
72
|
+
using SubscriptionHandler =
|
|
73
|
+
std::function<void(bool subscribed, const nlohmann::json &)>;
|
|
74
|
+
|
|
75
|
+
class Feature {
|
|
76
|
+
Connect *owner;
|
|
77
|
+
std::string scope;
|
|
78
|
+
|
|
79
|
+
std::string scopedName(const std::string &name) const {
|
|
80
|
+
const std::string prefix = scope + ".";
|
|
81
|
+
if (name.rfind(prefix, 0) == 0) {
|
|
82
|
+
return name;
|
|
83
|
+
}
|
|
84
|
+
return prefix + name;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public:
|
|
88
|
+
Feature(Connect *parent, std::string featureScope)
|
|
89
|
+
: owner(parent), scope(std::move(featureScope)) {}
|
|
90
|
+
|
|
91
|
+
void on(const std::string &name, EventHandler handler) {
|
|
92
|
+
owner->on(scopedName(name), std::move(handler));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
void emit(const std::string &name, const nlohmann::json &payload) {
|
|
96
|
+
owner->emit(scopedName(name), payload);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
void onCall(const std::string &name, CallHandler handler) {
|
|
100
|
+
owner->onCall(scopedName(name), std::move(handler));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
void onSubscription(const std::string &name,
|
|
104
|
+
SubscriptionHandler handler) {
|
|
105
|
+
owner->onSubscription(scopedName(name), std::move(handler));
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
private:
|
|
64
110
|
std::function<void(const std::string &)> outboundHandler;
|
|
111
|
+
std::map<std::string, std::vector<EventHandler>> eventHandlers;
|
|
112
|
+
std::map<std::string, CallHandler> callHandlers;
|
|
113
|
+
std::map<std::string, SubscriptionHandler> subscriptionHandlers;
|
|
114
|
+
|
|
115
|
+
void emitEnvelope(const std::string &kind, const std::string &name,
|
|
116
|
+
const nlohmann::json &payload,
|
|
117
|
+
const std::string &id = std::string(),
|
|
118
|
+
const std::string &error = std::string()) {
|
|
119
|
+
if (!outboundHandler) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
nlohmann::json j;
|
|
124
|
+
j["kind"] = kind;
|
|
125
|
+
j["name"] = name;
|
|
126
|
+
if (!id.empty()) {
|
|
127
|
+
j["id"] = id;
|
|
128
|
+
}
|
|
129
|
+
j["payload"] = payload;
|
|
130
|
+
if (!error.empty()) {
|
|
131
|
+
j["error"] = error;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
outboundHandler(j.dump());
|
|
135
|
+
}
|
|
65
136
|
|
|
66
137
|
public:
|
|
138
|
+
|
|
67
139
|
virtual ~Connect() = default;
|
|
68
140
|
|
|
141
|
+
// Mirror frontend API: register message listener by name
|
|
142
|
+
void on(const std::string &name, EventHandler handler) {
|
|
143
|
+
eventHandlers[name].push_back(std::move(handler));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
Feature feature(const std::string &scope) { return Feature(this, scope); }
|
|
147
|
+
|
|
148
|
+
// Request/response helper for call primitive
|
|
149
|
+
void onCall(const std::string &name, CallHandler handler) {
|
|
150
|
+
callHandlers[name] = std::move(handler);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Subscription helper for stream/query primitives
|
|
154
|
+
void onSubscription(const std::string &name, SubscriptionHandler handler) {
|
|
155
|
+
subscriptionHandlers[name] = std::move(handler);
|
|
156
|
+
}
|
|
157
|
+
|
|
69
158
|
// Setup the outbound message handler
|
|
70
159
|
void setOutbound(std::function<void(const std::string &)> h) {
|
|
71
160
|
outboundHandler = std::move(h);
|
|
@@ -86,28 +175,94 @@ public:
|
|
|
86
175
|
* - STATE: emit("theme", {{"mode", "dark"}})
|
|
87
176
|
*/
|
|
88
177
|
void emit(const std::string &name, const nlohmann::json &payload) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
178
|
+
emitEnvelope("event", name, payload);
|
|
179
|
+
}
|
|
92
180
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
181
|
+
void emitResult(const std::string &id, const std::string &name,
|
|
182
|
+
const nlohmann::json &payload) {
|
|
183
|
+
emitEnvelope("result", name, payload, id);
|
|
184
|
+
}
|
|
97
185
|
|
|
98
|
-
|
|
186
|
+
void emitError(const std::string &id, const std::string &name,
|
|
187
|
+
const std::string &message) {
|
|
188
|
+
emitEnvelope("error", name, nlohmann::json::object(), id, message);
|
|
99
189
|
}
|
|
100
190
|
|
|
101
191
|
// Parse and dispatch incoming messages from frontend
|
|
102
192
|
void dispatchMessage(const std::string &message) {
|
|
103
193
|
try {
|
|
104
194
|
auto j = nlohmann::json::parse(message);
|
|
105
|
-
|
|
106
|
-
|
|
195
|
+
nlohmann::json envelope = j;
|
|
196
|
+
|
|
197
|
+
if (j.is_object() && j.value("method", std::string()) ==
|
|
198
|
+
"connection.dispatch") {
|
|
199
|
+
auto paramsIt = j.find("params");
|
|
200
|
+
if (paramsIt != j.end() && paramsIt->is_array() &&
|
|
201
|
+
!paramsIt->empty() && (*paramsIt)[0].is_object()) {
|
|
202
|
+
envelope = (*paramsIt)[0];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
std::string name = envelope.value("name", "");
|
|
207
|
+
nlohmann::json payload =
|
|
208
|
+
envelope.value("payload", nlohmann::json::object());
|
|
209
|
+
std::string kind = envelope.value("kind", "fire");
|
|
210
|
+
std::string id = envelope.value("id", "");
|
|
107
211
|
|
|
108
|
-
if (
|
|
109
|
-
|
|
212
|
+
if (name.empty()) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (kind == "call") {
|
|
217
|
+
try {
|
|
218
|
+
nlohmann::json responsePayload = nlohmann::json::object();
|
|
219
|
+
auto callIt = callHandlers.find(name);
|
|
220
|
+
if (callIt != callHandlers.end()) {
|
|
221
|
+
responsePayload = callIt->second(payload);
|
|
222
|
+
} else {
|
|
223
|
+
responsePayload = handleCall(name, payload);
|
|
224
|
+
}
|
|
225
|
+
if (!id.empty()) {
|
|
226
|
+
emitResult(id, name, responsePayload);
|
|
227
|
+
}
|
|
228
|
+
} catch (const std::exception &e) {
|
|
229
|
+
if (!id.empty()) {
|
|
230
|
+
emitError(id, name, e.what());
|
|
231
|
+
}
|
|
232
|
+
} catch (...) {
|
|
233
|
+
if (!id.empty()) {
|
|
234
|
+
emitError(id, name, "Unknown error");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (kind == "sub") {
|
|
241
|
+
auto subIt = subscriptionHandlers.find(name);
|
|
242
|
+
if (subIt != subscriptionHandlers.end()) {
|
|
243
|
+
subIt->second(true, payload);
|
|
244
|
+
}
|
|
245
|
+
handleSubscription(name, true, payload);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (kind == "unsub") {
|
|
250
|
+
auto subIt = subscriptionHandlers.find(name);
|
|
251
|
+
if (subIt != subscriptionHandlers.end()) {
|
|
252
|
+
subIt->second(false, payload);
|
|
253
|
+
}
|
|
254
|
+
handleSubscription(name, false, payload);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
auto listenersIt = eventHandlers.find(name);
|
|
259
|
+
if (listenersIt != eventHandlers.end()) {
|
|
260
|
+
for (const auto &listener : listenersIt->second) {
|
|
261
|
+
listener(payload);
|
|
262
|
+
}
|
|
110
263
|
}
|
|
264
|
+
|
|
265
|
+
handleMessage(name, payload);
|
|
111
266
|
} catch (...) {
|
|
112
267
|
// Silently ignore malformed messages
|
|
113
268
|
}
|
|
@@ -116,6 +271,19 @@ public:
|
|
|
116
271
|
void dispatch(const std::string &message) { dispatchMessage(message); }
|
|
117
272
|
|
|
118
273
|
protected:
|
|
274
|
+
virtual nlohmann::json handleCall(const std::string &name,
|
|
275
|
+
const nlohmann::json &payload) {
|
|
276
|
+
handleMessage(name, payload);
|
|
277
|
+
return nlohmann::json::object();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
virtual void handleSubscription(const std::string &name, bool subscribed,
|
|
281
|
+
const nlohmann::json &payload) {
|
|
282
|
+
(void)name;
|
|
283
|
+
(void)subscribed;
|
|
284
|
+
(void)payload;
|
|
285
|
+
}
|
|
286
|
+
|
|
119
287
|
/**
|
|
120
288
|
* handleMessage() - Receive messages from the frontend
|
|
121
289
|
*
|
|
@@ -136,9 +304,15 @@ protected:
|
|
|
136
304
|
* }
|
|
137
305
|
* }
|
|
138
306
|
*/
|
|
139
|
-
virtual void handleMessage(const std::string &name,
|
|
307
|
+
virtual void handleMessage(const std::string &name,
|
|
308
|
+
const nlohmann::json &payload) {
|
|
309
|
+
(void)name;
|
|
310
|
+
(void)payload;
|
|
311
|
+
}
|
|
140
312
|
};
|
|
141
313
|
|
|
314
|
+
void bindConnect(Window &window, Connect &connect);
|
|
315
|
+
|
|
142
316
|
// Legacy alias for backwards compatibility
|
|
143
317
|
using Bridge = Connect;
|
|
144
318
|
using Connection = Connect;
|
|
@@ -161,11 +161,14 @@ public:
|
|
|
161
161
|
bool canGoForward() const;
|
|
162
162
|
|
|
163
163
|
// Scripting
|
|
164
|
+
using JSCallback = std::function<std::string(const std::string &)>;
|
|
164
165
|
void executeScript(const std::string &script);
|
|
165
166
|
void executeScript(const std::string &script, StringCallback callback);
|
|
166
167
|
void eval(const std::string &js) {
|
|
167
168
|
executeScript(js);
|
|
168
169
|
} // Alias for compatibility
|
|
170
|
+
void bind(const std::string &name, JSCallback callback);
|
|
171
|
+
void unbind(const std::string &name);
|
|
169
172
|
|
|
170
173
|
template <typename T> std::future<T> evaluate(const std::string &js) {
|
|
171
174
|
// Implementation in cpp or inline
|