plusui-native-core 0.1.56 → 0.1.57

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.
@@ -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
 
@@ -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;
@@ -62,6 +64,8 @@ if (typeof window !== 'undefined') {
62
64
  }
63
65
 
64
66
  export const browser = {
67
+ connect: connect.feature('browser'),
68
+
65
69
  async navigate(url: string): Promise<void> {
66
70
  await invoke('browser.navigate', [url]);
67
71
  },
@@ -160,6 +164,8 @@ export const browser = {
160
164
  };
161
165
 
162
166
  export const router = {
167
+ connect: connect.feature('router'),
168
+
163
169
  setRoutes(routes: Record<string, string>): void {
164
170
  _routes = routes;
165
171
  },
@@ -1,3 +1,5 @@
1
+ import { connect } from '../Connection/connect';
2
+
1
3
  /**
2
4
  * Clipboard API - Cross-platform clipboard management
3
5
  *
@@ -24,6 +26,7 @@ export interface ClipboardAPI {
24
26
  }
25
27
 
26
28
  export class Clipboard implements ClipboardAPI {
29
+ public readonly connect = connect.feature('clipboard');
27
30
  private invokeFn: (name: string, args?: unknown[]) => Promise<unknown>;
28
31
  private onFn: (event: string, callback: (data: unknown) => void) => () => void;
29
32
 
@@ -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
- class MyConnect : public plusui::Connect {
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
- public:
62
- // Emit messages to frontend
63
- void emit(const std::string& name, const nlohmann::json& data) {
64
- event(name, data);
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
- // Example: notify frontend of window resize
68
- void onWindowResize(int w, int h) {
69
- emit("resize", {{"width", w}, {"height", h}});
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,42 @@ connect.on('messageName', (data) => {
98
87
  ### Backend
99
88
 
100
89
  ```cpp
101
- // Listen for messages (in handleMessage)
102
- void handleMessage(const Envelope& env) override {
103
- if (env.kind == MessageKind::Fire && env.name == "messageName") {
104
- // process env.payload
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
- void emit(const std::string& name, const nlohmann::json& data) {
110
- event(name, data);
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.connect` when you want automatic namespacing:
121
+
122
+ ```typescript
123
+ window.connect.emit('resized', { width: 1200, height: 800 }); // -> "window.resized"
124
+ clipboard.connect.emit('changed', { text: 'hello' }); // -> "clipboard.changed"
125
+ connect.emit('custom.appEvent', { ok: true }); // custom/global channel
112
126
  ```
113
127
 
114
128
  ## 💡 Design Your Own Patterns
@@ -124,13 +138,11 @@ connect.emit('getUser', { id: 123 });
124
138
  ```
125
139
 
126
140
  ```cpp
127
- // Backend
128
- void handleMessage(const Envelope& env) override {
129
- if (env.name == "getUser") {
130
- auto user = database.findUser(env.payload["id"]);
131
- emit("getUserResponse", {{"user", user}});
132
- }
133
- }
141
+ plusui::Connect connect;
142
+ connect.onCall("getUser", [](const nlohmann::json& payload) {
143
+ auto user = database.findUser(payload["id"]);
144
+ return nlohmann::json{{"user", user}};
145
+ });
134
146
  ```
135
147
 
136
148
  ### 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
- // Connect implementation
6
- //
7
- // The Connect class is header-only (see plusui/connect.hpp).
8
- // This file is reserved for future non-template implementation details.
9
- //
10
- // TWO METHODS. FIVE PRIMITIVES. EVERYTHING YOU NEED.
11
- //
12
- // To use Connect:
13
- // 1. Create a class that inherits from plusui::Connect
14
- // 2. Override handleMessage(name, payload) to receive messages
15
- // 3. Use emit(name, payload) to send messages to frontend
16
- //
17
- // Example - All 5 Primitives:
18
- //
19
- // class MyApp : public plusui::Connect {
20
- // protected:
21
- // void handleMessage(const std::string& name, const nlohmann::json& data) override {
22
- // // EVENT: fire & forget
23
- // if (name == "notification") {
24
- // showNotification(data["message"].get<std::string>());
25
- // }
26
- //
27
- // // CALL: request/response
28
- // if (name == "getUser") {
29
- // auto userId = data["id"].get<int>();
30
- // emit("userData", {{"name", "John"}, {"id", userId}});
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,6 +34,8 @@ 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
  ) {}
@@ -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
  *
@@ -77,6 +79,7 @@ export interface FileDropAPI {
77
79
  }
78
80
 
79
81
  export class FileDrop implements FileDropAPI {
82
+ public readonly connect = connect.feature('fileDrop');
80
83
  private invokeFn: (name: string, args?: unknown[]) => Promise<unknown>;
81
84
  private onFn: (event: string, callback: (data: unknown) => void) => () => void;
82
85
 
@@ -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(
@@ -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;
@@ -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,6 +18,8 @@ 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
@@ -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
 
@@ -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
- return S_OK;
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 (method.find("window.") == 0) {
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;
@@ -42,6 +44,8 @@ async function invoke(method: string, args?: unknown[]): Promise<unknown> {
42
44
  }
43
45
 
44
46
  export const window = {
47
+ connect: connect.feature('window'),
48
+
45
49
  async minimize(id?: WindowId): Promise<void> {
46
50
  await invoke('window.minimize', id ? [id] : []);
47
51
  },
@@ -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,61 @@
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
+ private:
64
76
  std::function<void(const std::string &)> outboundHandler;
77
+ std::map<std::string, std::vector<EventHandler>> eventHandlers;
78
+ std::map<std::string, CallHandler> callHandlers;
79
+ std::map<std::string, SubscriptionHandler> subscriptionHandlers;
80
+
81
+ void emitEnvelope(const std::string &kind, const std::string &name,
82
+ const nlohmann::json &payload,
83
+ const std::string &id = std::string(),
84
+ const std::string &error = std::string()) {
85
+ if (!outboundHandler) {
86
+ return;
87
+ }
88
+
89
+ nlohmann::json j;
90
+ j["kind"] = kind;
91
+ j["name"] = name;
92
+ if (!id.empty()) {
93
+ j["id"] = id;
94
+ }
95
+ j["payload"] = payload;
96
+ if (!error.empty()) {
97
+ j["error"] = error;
98
+ }
99
+
100
+ outboundHandler(j.dump());
101
+ }
65
102
 
66
- public:
67
103
  virtual ~Connect() = default;
68
104
 
105
+ // Mirror frontend API: register message listener by name
106
+ void on(const std::string &name, EventHandler handler) {
107
+ eventHandlers[name].push_back(std::move(handler));
108
+ }
109
+
110
+ // Request/response helper for call primitive
111
+ void onCall(const std::string &name, CallHandler handler) {
112
+ callHandlers[name] = std::move(handler);
113
+ }
114
+
115
+ // Subscription helper for stream/query primitives
116
+ void onSubscription(const std::string &name, SubscriptionHandler handler) {
117
+ subscriptionHandlers[name] = std::move(handler);
118
+ }
119
+
69
120
  // Setup the outbound message handler
70
121
  void setOutbound(std::function<void(const std::string &)> h) {
71
122
  outboundHandler = std::move(h);
@@ -86,28 +137,94 @@ public:
86
137
  * - STATE: emit("theme", {{"mode", "dark"}})
87
138
  */
88
139
  void emit(const std::string &name, const nlohmann::json &payload) {
89
- if (!outboundHandler) {
90
- return;
91
- }
140
+ emitEnvelope("event", name, payload);
141
+ }
92
142
 
93
- nlohmann::json j;
94
- j["kind"] = "event";
95
- j["name"] = name;
96
- j["payload"] = payload;
143
+ void emitResult(const std::string &id, const std::string &name,
144
+ const nlohmann::json &payload) {
145
+ emitEnvelope("result", name, payload, id);
146
+ }
97
147
 
98
- outboundHandler(j.dump());
148
+ void emitError(const std::string &id, const std::string &name,
149
+ const std::string &message) {
150
+ emitEnvelope("error", name, nlohmann::json::object(), id, message);
99
151
  }
100
152
 
101
153
  // Parse and dispatch incoming messages from frontend
102
154
  void dispatchMessage(const std::string &message) {
103
155
  try {
104
156
  auto j = nlohmann::json::parse(message);
105
- std::string name = j.value("name", "");
106
- nlohmann::json payload = j.value("payload", nlohmann::json::object());
157
+ nlohmann::json envelope = j;
158
+
159
+ if (j.is_object() && j.value("method", std::string()) ==
160
+ "connection.dispatch") {
161
+ auto paramsIt = j.find("params");
162
+ if (paramsIt != j.end() && paramsIt->is_array() &&
163
+ !paramsIt->empty() && (*paramsIt)[0].is_object()) {
164
+ envelope = (*paramsIt)[0];
165
+ }
166
+ }
167
+
168
+ std::string name = envelope.value("name", "");
169
+ nlohmann::json payload =
170
+ envelope.value("payload", nlohmann::json::object());
171
+ std::string kind = envelope.value("kind", "fire");
172
+ std::string id = envelope.value("id", "");
107
173
 
108
- if (!name.empty()) {
109
- handleMessage(name, payload);
174
+ if (name.empty()) {
175
+ return;
110
176
  }
177
+
178
+ if (kind == "call") {
179
+ try {
180
+ nlohmann::json responsePayload = nlohmann::json::object();
181
+ auto callIt = callHandlers.find(name);
182
+ if (callIt != callHandlers.end()) {
183
+ responsePayload = callIt->second(payload);
184
+ } else {
185
+ responsePayload = handleCall(name, payload);
186
+ }
187
+ if (!id.empty()) {
188
+ emitResult(id, name, responsePayload);
189
+ }
190
+ } catch (const std::exception &e) {
191
+ if (!id.empty()) {
192
+ emitError(id, name, e.what());
193
+ }
194
+ } catch (...) {
195
+ if (!id.empty()) {
196
+ emitError(id, name, "Unknown error");
197
+ }
198
+ }
199
+ return;
200
+ }
201
+
202
+ if (kind == "sub") {
203
+ auto subIt = subscriptionHandlers.find(name);
204
+ if (subIt != subscriptionHandlers.end()) {
205
+ subIt->second(true, payload);
206
+ }
207
+ handleSubscription(name, true, payload);
208
+ return;
209
+ }
210
+
211
+ if (kind == "unsub") {
212
+ auto subIt = subscriptionHandlers.find(name);
213
+ if (subIt != subscriptionHandlers.end()) {
214
+ subIt->second(false, payload);
215
+ }
216
+ handleSubscription(name, false, payload);
217
+ return;
218
+ }
219
+
220
+ auto listenersIt = eventHandlers.find(name);
221
+ if (listenersIt != eventHandlers.end()) {
222
+ for (const auto &listener : listenersIt->second) {
223
+ listener(payload);
224
+ }
225
+ }
226
+
227
+ handleMessage(name, payload);
111
228
  } catch (...) {
112
229
  // Silently ignore malformed messages
113
230
  }
@@ -116,6 +233,19 @@ public:
116
233
  void dispatch(const std::string &message) { dispatchMessage(message); }
117
234
 
118
235
  protected:
236
+ virtual nlohmann::json handleCall(const std::string &name,
237
+ const nlohmann::json &payload) {
238
+ handleMessage(name, payload);
239
+ return nlohmann::json::object();
240
+ }
241
+
242
+ virtual void handleSubscription(const std::string &name, bool subscribed,
243
+ const nlohmann::json &payload) {
244
+ (void)name;
245
+ (void)subscribed;
246
+ (void)payload;
247
+ }
248
+
119
249
  /**
120
250
  * handleMessage() - Receive messages from the frontend
121
251
  *
@@ -136,9 +266,15 @@ protected:
136
266
  * }
137
267
  * }
138
268
  */
139
- virtual void handleMessage(const std::string &name, const nlohmann::json &payload) = 0;
269
+ virtual void handleMessage(const std::string &name,
270
+ const nlohmann::json &payload) {
271
+ (void)name;
272
+ (void)payload;
273
+ }
140
274
  };
141
275
 
276
+ void bindConnect(Window &window, Connect &connect);
277
+
142
278
  // Legacy alias for backwards compatibility
143
279
  using Bridge = Connect;
144
280
  using Connection = Connect;
@@ -0,0 +1,3 @@
1
+ #pragma once
2
+
3
+ #include <plusui/connect.hpp>
@@ -26,6 +26,7 @@
26
26
  #include <plusui/browser.hpp>
27
27
  #include <plusui/clipboard.hpp>
28
28
  #include <plusui/connect.hpp>
29
+ #include <plusui/connection.hpp>
29
30
  #include <plusui/display.hpp>
30
31
  #include <plusui/filedrop.hpp>
31
32
  #include <plusui/keyboard.hpp>
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plusui-native-core",
3
- "version": "0.1.56",
3
+ "version": "0.1.57",
4
4
  "description": "PlusUI Core framework (frontend + backend implementations)",
5
5
  "type": "module",
6
6
  "files": [