plusui-native-core 0.1.57 → 0.1.61

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.
@@ -38,6 +38,8 @@ const _loadErrorCallbacks: ErrorCallback[] = [];
38
38
  let _currentState: BrowserState = { url: '', title: '', canGoBack: false, canGoForward: false, isLoading: false };
39
39
  let _routes: Record<string, string> = {};
40
40
  let _currentRoute: string = '/';
41
+ const browserFeatureEvents = connect.feature('browser');
42
+ const routerFeatureEvents = connect.feature('router');
41
43
 
42
44
  // Setup event listeners from native backend
43
45
  if (typeof window !== 'undefined') {
@@ -64,7 +66,15 @@ if (typeof window !== 'undefined') {
64
66
  }
65
67
 
66
68
  export const browser = {
67
- connect: connect.feature('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
+ },
68
78
 
69
79
  async navigate(url: string): Promise<void> {
70
80
  await invoke('browser.navigate', [url]);
@@ -164,7 +174,15 @@ export const browser = {
164
174
  };
165
175
 
166
176
  export const router = {
167
- connect: connect.feature('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
+ },
168
186
 
169
187
  setRoutes(routes: Record<string, string>): void {
170
188
  _routes = routes;
@@ -8,6 +8,9 @@ import { connect } from '../Connection/connect';
8
8
  */
9
9
 
10
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
+
11
14
  // Text operations
12
15
  getText(): Promise<string>;
13
16
  setText(text: string): Promise<void>;
@@ -38,6 +41,14 @@ export class Clipboard implements ClipboardAPI {
38
41
  this.onFn = onFn;
39
42
  }
40
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
+
41
52
  /**
42
53
  * Get text from clipboard
43
54
  * @returns Current clipboard text content
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## 🎯 Overview
4
4
 
5
- The **Connection Feature** is PlusUI's absurdly simple system for frontend ↔ backend communication. Both sides use the same two methods: **`emit()`** and **`on()`**.
5
+ The **Connection Feature** is PlusUI's absurdly simple system for frontend ↔ backend communication. Use **`feature.emit()`** and **`feature.on()`** on frontend, mirrored by scoped channels on backend.
6
6
 
7
7
  ## ✨ The 5 Primitives That Cover Everything
8
8
 
@@ -32,13 +32,15 @@ Just `emit()` and `on()` handle all communication patterns:
32
32
  ```typescript
33
33
  import { connect } from 'plusui-native-core/connect';
34
34
 
35
+ const custom = connect.feature('custom');
36
+
35
37
  // Listen for response
36
- connect.on('greetResponse', (data) => {
38
+ custom.on('greetResponse', (data) => {
37
39
  console.log(data.message); // "Hello, World!"
38
40
  });
39
41
 
40
42
  // Send message to backend
41
- connect.emit('greet', { name: 'World' });
43
+ custom.emit('greet', { name: 'World' });
42
44
  ```
43
45
 
44
46
  ### Receive & Respond
@@ -48,23 +50,25 @@ connect.emit('greet', { name: 'World' });
48
50
 
49
51
  plusui::Connect connect;
50
52
 
51
- connect.on("greet", [&connect](const nlohmann::json& payload) {
53
+ auto customFeature = connect.feature("custom");
54
+
55
+ customFeature.on("greet", [&connect](const nlohmann::json& payload) {
52
56
  std::string user = payload.value("name", "World");
53
- connect.emit("greetResponse", {{"message", "Hello, " + user + "!"}});
57
+ connect.emit("custom.greetResponse", {{"message", "Hello, " + user + "!"}});
54
58
  });
55
59
 
56
60
  // Wire backend connect ↔ window bridge once
57
61
  plusui::bindConnect(mainWindow, connect);
58
62
 
59
63
  // Example: backend push to frontend
60
- connect.emit("resize", {{"width", 1280}, {"height", 720}});
64
+ connect.emit("custom.resize", {{"width", 1280}, {"height", 720}});
61
65
  ```
62
66
 
63
67
  ### Listen on Frontend
64
68
 
65
69
  ```typescript
66
70
  // Listen for messages from backend
67
- connect.on('resize', (data) => {
71
+ custom.on('resize', (data) => {
68
72
  console.log(`${data.width}x${data.height}`);
69
73
  updateLayout(data);
70
74
  });
@@ -117,14 +121,47 @@ connect.onSubscription("cpu", [](bool subscribed, const nlohmann::json&) {
117
121
 
118
122
  ### Feature-Scoped Names
119
123
 
120
- Use `connect` for custom global channels, and `feature.connect` when you want automatic namespacing:
124
+ Use `connect` for custom global channels, and feature-scoped APIs when you want automatic namespacing:
121
125
 
122
126
  ```typescript
127
+ window.emit('resized', { width: 1200, height: 800 }); // -> "window.resized"
128
+ clipboard.emit('changed', { text: 'hello' }); // -> "clipboard.changed"
129
+
130
+ // Also supported when you need explicit feature connect access:
123
131
  window.connect.emit('resized', { width: 1200, height: 800 }); // -> "window.resized"
124
- clipboard.connect.emit('changed', { text: 'hello' }); // -> "clipboard.changed"
125
132
  connect.emit('custom.appEvent', { ok: true }); // custom/global channel
126
133
  ```
127
134
 
135
+ Backend can listen using fully scoped names, or use a scoped helper:
136
+
137
+ ```cpp
138
+ plusui::Connect connect;
139
+ plusui::bindConnect(mainWindow, connect);
140
+
141
+ auto windowFeature = connect.feature("window");
142
+ windowFeature.on("resized", [](const nlohmann::json& payload) {
143
+ // receives "window.resized"
144
+ });
145
+
146
+ connect.on("window.resized", [](const nlohmann::json& payload) {
147
+ // from window.emit('resized', ...) or window.connect.emit('resized', ...)
148
+ });
149
+
150
+ connect.on("clipboard.changed", [](const nlohmann::json& payload) {
151
+ // from clipboard.emit('changed', ...) or clipboard.connect.emit('changed', ...)
152
+ });
153
+
154
+ connect.on("custom.appEvent", [](const nlohmann::json& payload) {
155
+ // from connect.emit('custom.appEvent', ...)
156
+ });
157
+ ```
158
+
159
+ `plusui connect` scans frontend and backend `.emit()` / `.on()` calls and understands feature aliases, including:
160
+
161
+ - `const custom = createFeatureConnect('custom'); custom.on('event', ...)`
162
+ - `const custom = connect.feature('custom'); custom.emit('event', ...)`
163
+ - `auto custom = connect.feature("custom"); custom.on("event", ...)`
164
+
128
165
  ## 💡 Design Your Own Patterns
129
166
 
130
167
  ### Request/Response Pattern
@@ -40,6 +40,14 @@ export class DisplayAPI {
40
40
  private invokeFn: (name: string, args?: unknown[]) => Promise<unknown>
41
41
  ) {}
42
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
+
43
51
  async getAllDisplays(): Promise<Display[]> {
44
52
  return await this.invokeFn<Display[]>('display.getAllDisplays', []);
45
53
  }
@@ -32,6 +32,9 @@ export interface DragEvent {
32
32
  }
33
33
 
34
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
+
35
38
  /**
36
39
  * Enable or disable file drop into window
37
40
  * @param enabled - true to allow files to be dropped into the window
@@ -91,6 +94,14 @@ export class FileDrop implements FileDropAPI {
91
94
  this.onFn = onFn;
92
95
  }
93
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
+
94
105
  /**
95
106
  * Enable or disable file drop into window
96
107
  * @param enabled - true to allow files to be dropped into the window
@@ -65,6 +65,14 @@ export class KeyboardAPI {
65
65
  this.setupEventListeners();
66
66
  }
67
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
+
68
76
  private setupEventListeners(): void {
69
77
  this.eventFn('keyboard:keydown', (data) => {
70
78
  const event = data as KeyEvent;
@@ -88,6 +88,14 @@ export class MenuAPI {
88
88
  });
89
89
  }
90
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
+
91
99
  // ========================================================================
92
100
  // Popup Menus
93
101
  // ========================================================================
@@ -25,6 +25,14 @@ export class TrayAPI {
25
25
  private eventFn: (event: string, callback: (...args: unknown[]) => void) => () => void
26
26
  ) {}
27
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
+
28
36
  async setIcon(iconPath: string): Promise<void> {
29
37
  await this.invokeFn('tray.setIcon', [iconPath]);
30
38
  }
@@ -575,6 +575,14 @@ class WebGPUNamespace {
575
575
  this.onFn = onFn;
576
576
  }
577
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
+
578
586
  async requestAdapter(options?: GPURequestAdapterOptions): Promise<GPUAdapter | null> {
579
587
  const result = await this.invokeFn('webgpu.requestAdapter', [options || {}]);
580
588
  if (!result) {
@@ -43,8 +43,18 @@ async function invoke(method: string, args?: unknown[]): Promise<unknown> {
43
43
  return _invoke(method, args);
44
44
  }
45
45
 
46
+ const windowFeatureEvents = connect.feature('window');
47
+
46
48
  export const window = {
47
- connect: connect.feature('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
+ },
48
58
 
49
59
  async minimize(id?: WindowId): Promise<void> {
50
60
  await invoke('window.minimize', id ? [id] : []);
@@ -72,6 +72,72 @@ public:
72
72
  using SubscriptionHandler =
73
73
  std::function<void(bool subscribed, const nlohmann::json &)>;
74
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
+ // ============================================================
110
+ // Channel — single-name channel object
111
+ //
112
+ // Mirror of the TypeScript createChannel() object:
113
+ // download.on([](const json& p) { ... });
114
+ // download.emit({{"progress", 50}});
115
+ //
116
+ // Obtained via connect.channel("download") or declared in
117
+ // the auto-generated connections.gen.hpp.
118
+ // ============================================================
119
+ class Channel {
120
+ Connect *owner;
121
+ std::string name_;
122
+
123
+ public:
124
+ Channel() : owner(nullptr) {}
125
+ Channel(Connect *parent, std::string channelName)
126
+ : owner(parent), name_(std::move(channelName)) {}
127
+
128
+ // Register a listener — mirrors TypeScript: download.on((d) => { ... })
129
+ void on(EventHandler handler) {
130
+ if (owner) owner->on(name_, std::move(handler));
131
+ }
132
+
133
+ // Send a message — mirrors TypeScript: download.emit({ key: value })
134
+ void emit(const nlohmann::json &payload = nlohmann::json::object()) {
135
+ if (owner) owner->emit(name_, payload);
136
+ }
137
+
138
+ const std::string &name() const { return name_; }
139
+ };
140
+
75
141
  private:
76
142
  std::function<void(const std::string &)> outboundHandler;
77
143
  std::map<std::string, std::vector<EventHandler>> eventHandlers;
@@ -100,6 +166,8 @@ private:
100
166
  outboundHandler(j.dump());
101
167
  }
102
168
 
169
+ public:
170
+
103
171
  virtual ~Connect() = default;
104
172
 
105
173
  // Mirror frontend API: register message listener by name
@@ -107,6 +175,15 @@ private:
107
175
  eventHandlers[name].push_back(std::move(handler));
108
176
  }
109
177
 
178
+ Feature feature(const std::string &scope) { return Feature(this, scope); }
179
+
180
+ // Create a named channel object — mirrors TypeScript createChannel('name')
181
+ // Returns by value; store as a member or local:
182
+ // auto download = connect.channel("download");
183
+ // download.on([](const json& p) { ... });
184
+ // download.emit({{"progress", 50}});
185
+ Channel channel(const std::string &name) { return Channel(this, name); }
186
+
110
187
  // Request/response helper for call primitive
111
188
  void onCall(const std::string &name, CallHandler handler) {
112
189
  callHandlers[name] = std::move(handler);
@@ -31,7 +31,6 @@
31
31
  #include <plusui/filedrop.hpp>
32
32
  #include <plusui/keyboard.hpp>
33
33
  #include <plusui/menu.hpp>
34
- #include <plusui/native_bindings.hpp>
35
34
  #include <plusui/tray.hpp>
36
35
  #include <plusui/webgpu.hpp>
37
36
  #include <plusui/window.hpp>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plusui-native-core",
3
- "version": "0.1.57",
3
+ "version": "0.1.61",
4
4
  "description": "PlusUI Core framework (frontend + backend implementations)",
5
5
  "type": "module",
6
6
  "files": [
@@ -1,19 +0,0 @@
1
- #pragma once
2
-
3
- #include <string>
4
- #include <vector>
5
-
6
- namespace plusui {
7
-
8
- struct BindingEntry {
9
- const char *source;
10
- const char *feature;
11
- const char *method;
12
- };
13
-
14
- class NativeBindings {
15
- public:
16
- static const std::vector<BindingEntry> &getEntries();
17
- };
18
-
19
- } // namespace plusui