plusui-native 0.2.73 → 0.2.75

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plusui-native",
3
- "version": "0.2.73",
3
+ "version": "0.2.75",
4
4
  "description": "PlusUI CLI - Build C++ desktop apps modern UI ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -27,11 +27,11 @@
27
27
  "semver": "^7.6.0",
28
28
  "which": "^4.0.0",
29
29
  "execa": "^8.0.1",
30
- "plusui-native-builder": "^0.1.72",
31
- "plusui-native-connect": "^0.1.72"
30
+ "plusui-native-builder": "^0.1.74",
31
+ "plusui-native-connect": "^0.1.74"
32
32
  },
33
33
  "peerDependencies": {
34
- "plusui-native-connect": "^0.1.72"
34
+ "plusui-native-connect": "^0.1.74"
35
35
  },
36
36
  "publishConfig": {
37
37
  "access": "public"
@@ -1,15 +1,8 @@
1
1
  import { useState, useEffect } from 'react';
2
- import plusui from 'plusui';
2
+ import plusui, { formatFileSize } from 'plusui-native-core';
3
3
  // Generated by `plusui connect` — channel objects auto-created from name.on / name.emit usage:
4
4
  import { customFileDrop } from '../Connections/connections.gen';
5
5
 
6
- // Define routes for your app (optional - for SPA routing)
7
- const routes = {
8
- '/': 'http://localhost:5173',
9
- '/settings': 'http://localhost:5173/settings',
10
- '/about': 'http://localhost:5173/about',
11
- };
12
-
13
6
  function App() {
14
7
  const [windowSize, setWindowSize] = useState({ width: 0, height: 0 });
15
8
  const [windowPos, setWindowPos] = useState({ x: 0, y: 0 });
@@ -28,7 +21,10 @@ function App() {
28
21
  });
29
22
 
30
23
  // Setup routes
31
- plusui.router.setRoutes(routes);
24
+ plusui.router.setRoutes({
25
+ '/app': 'http://localhost:5173/app',
26
+ '/about': 'http://localhost:5173/about',
27
+ });
32
28
 
33
29
  // Listen for navigation changes
34
30
  const unsub = plusui.browser.onNavigate((url) => {
@@ -70,10 +66,6 @@ function App() {
70
66
  const handleGoForward = async () => await plusui.browser.goForward();
71
67
  const handleReload = async () => await plusui.browser.reload();
72
68
 
73
- // Router navigation
74
- const handleGoHome = async () => await plusui.router.push('/');
75
- const handleGoSettings = async () => await plusui.router.push('/settings');
76
-
77
69
  // App control
78
70
  const handleQuit = async () => await plusui.app.quit();
79
71
 
@@ -140,17 +132,6 @@ function App() {
140
132
  </div>
141
133
  </div>
142
134
 
143
- <div className="card">
144
- <h2>Router (SPA Navigation)</h2>
145
- <div className="button-group">
146
- <button onClick={handleGoHome} className="button">Home /</button>
147
- <button onClick={handleGoSettings} className="button">Settings</button>
148
- </div>
149
- <p style={{ fontSize: '0.85em', color: '#666', marginTop: '10px' }}>
150
- Define routes with <code>plusui.router.setRoutes({'{ ... }'})</code> then navigate with <code>plusui.router.push('/path')</code>
151
- </p>
152
- </div>
153
-
154
135
  <div className="card">
155
136
  <h2>App Control</h2>
156
137
  <button onClick={handleQuit} className="button button-danger">Quit App</button>
@@ -193,7 +174,7 @@ function App() {
193
174
  <div className="filedrop-file-icon">📄</div>
194
175
  <div className="filedrop-file-info">
195
176
  <div className="filedrop-file-name">{file.name}</div>
196
- <div className="filedrop-file-meta">{plusui.formatFileSize(file.size)} • {file.type}</div>
177
+ <div className="filedrop-file-meta">{formatFileSize(file.size)} • {file.type}</div>
197
178
  </div>
198
179
  </div>
199
180
  ))}
@@ -73,9 +73,9 @@ struct WindowConfig {
73
73
 
74
74
  // --- Routing ---
75
75
  // The frontend route this window opens on.
76
- // Use "/" for root, "/settings" for settings page, etc.
76
+ // Use "/app" for main view, "/settings" for settings page, etc.
77
77
  // Works with any frontend router (React Router, TanStack, Solid Router).
78
- std::string route = "/"; // Starting route (e.g. "/", "/settings", "/profile")
78
+ std::string route = "/app"; // Starting route (e.g. "/app", "/settings")
79
79
  int devServerPort = 5173; // Vite dev server port
80
80
  } windowConfig;
81
81
 
@@ -114,12 +114,13 @@ struct WebGPUConfig {
114
114
  // MAIN - Application Entry Point
115
115
  // ============================================================================
116
116
  // ── Connect instance ─────────────────────────────────────────────────────────
117
- // connect is the bridge between C++ and the frontend.
117
+ // conn is the bridge between C++ and the frontend.
118
118
  // Run `plusui connect` to generate Connections/ from your name.on / name.emit usage.
119
- // Then declare: Connections ch(connect);
120
- // and use: ch.myEvent.on([](const json& p) { ... });
121
- // ch.myEvent.emit({{"value", 42}});
122
- static Connect connect;
119
+ // Call initChannels(conn) once, then use channels directly:
120
+ // customFileDrop.on([](const json& p) { ... });
121
+ // customFileDrop.emit({{"value", 42}});
122
+ static Connect conn;
123
+ using json = nlohmann::json;
123
124
 
124
125
  int main() {
125
126
  // Build the app with configuration
@@ -191,12 +192,12 @@ int main() {
191
192
  // ========================================
192
193
  // CONNECT — bind frontend ↔ backend
193
194
  // ========================================
194
- // Wires the connect object to this window.
195
- // Connections ch gives you named channel objects — same API as TypeScript:
196
- // ch.myEvent.on([](const json& p) { ... }); // receive from frontend
197
- // ch.myEvent.emit({{"value", 42}}); // send to frontend
198
- bindConnect(mainWindow, connect);
199
- Connections ch(connect); // use ch.name.on() / ch.name.emit()
195
+ // Wires the connect object to this window, then initialises
196
+ // the auto-generated channels so you can use them directly:
197
+ // customFileDrop.on([](const json& p) { ... });
198
+ // customFileDrop.emit({{"value", 42}});
199
+ bindConnect(mainWindow, conn);
200
+ initChannels(conn);
200
201
 
201
202
  // ========================================
202
203
  // CUSTOM FILE DROP CHANNEL
@@ -205,7 +206,7 @@ int main() {
205
206
  // Frontend drops files → emits via customFileDrop.emit({ files: [...] })
206
207
  // C++ receives here, processes, then emits back to the frontend.
207
208
  // Frontend receives the reply via customFileDrop.on() in App.tsx.
208
- ch.customFileDrop.on([&ch](const json& payload) {
209
+ customFileDrop.on([](const json& payload) {
209
210
  auto files = payload.value("files", json::array());
210
211
  int count = static_cast<int>(files.size());
211
212
  std::cout << "customFileDrop: received " << count << " file(s) from frontend" << std::endl;
@@ -213,7 +214,7 @@ int main() {
213
214
  std::cout << " - " << f.value("name", "?") << " (" << f.value("size", 0) << " bytes)" << std::endl;
214
215
  }
215
216
  // Reply back to frontend — received by customFileDrop.on() in App.tsx
216
- ch.customFileDrop.emit({
217
+ customFileDrop.emit({
217
218
  {"processed", true},
218
219
  {"count", count},
219
220
  {"message", "C++ received " + std::to_string(count) + " file(s)"}
@@ -237,10 +238,10 @@ int main() {
237
238
  //
238
239
  // CONNECT (custom channels — same API on both sides):
239
240
  // Run `plusui connect` to generate Connections/ from your name.on / name.emit calls.
240
- // C++: ch.myEvent.on([](const json& p) { ... }); // receive
241
- // ch.myEvent.emit({{"value", 42}}); // send
242
- // TS: myEvent.on((data) => { ... }); // receive
243
- // myEvent.emit({ value: 42 }); // send
241
+ // C++: customFileDrop.on([](const json& p) { ... }); // receive
242
+ // customFileDrop.emit({{"value", 42}}); // send
243
+ // TS: customFileDrop.on((data) => { ... }); // receive
244
+ // customFileDrop.emit({ value: 42 }); // send
244
245
  //
245
246
  // WINDOW: win.minimize(), win.maximize(), win.close(), win.center(),
246
247
  // win.setSize(w, h), win.setPosition(x, y), win.setTitle(str),
@@ -1,22 +1,15 @@
1
1
  import { createSignal, onMount, onCleanup, Show, For } from 'solid-js';
2
- import plusui from 'plusui';
2
+ import plusui, { formatFileSize } from 'plusui-native-core';
3
3
  // Generated by `plusui connect` — channel objects auto-created from name.on / name.emit usage:
4
4
  import { customFileDrop } from '../Connections/connections.gen';
5
5
 
6
- // Define routes for your app (optional - for SPA routing)
7
- const routes = {
8
- '/': 'http://localhost:5173',
9
- '/settings': 'http://localhost:5173/settings',
10
- '/about': 'http://localhost:5173/about',
11
- };
12
-
13
6
  function App() {
14
7
  const [windowSize, setWindowSize] = createSignal({ width: 0, height: 0 });
15
8
  const [windowPos, setWindowPos] = createSignal({ x: 0, y: 0 });
16
9
  const [currentUrl, setCurrentUrl] = createSignal('');
17
10
  const [canGoBack, setCanGoBack] = createSignal(false);
18
11
  const [canGoForward, setCanGoForward] = createSignal(false);
19
-
12
+
20
13
  // customFileDrop connect channel state
21
14
  const [isDragging, setIsDragging] = createSignal(false);
22
15
  const [droppedFiles, setDroppedFiles] = createSignal<{ name: string; size: number; type: string }[]>([]);
@@ -24,20 +17,23 @@ function App() {
24
17
 
25
18
  onMount(() => {
26
19
  // Setup routes
27
- plusui.router.setRoutes(routes);
28
-
20
+ plusui.router.setRoutes({
21
+ '/app': 'http://localhost:5173/app',
22
+ '/about': 'http://localhost:5173/about',
23
+ });
24
+
29
25
  // Listen for navigation changes
30
26
  plusui.browser.onNavigate((url) => {
31
27
  setCurrentUrl(url);
32
28
  plusui.browser.canGoBack().then(setCanGoBack);
33
29
  plusui.browser.canGoForward().then(setCanGoForward);
34
30
  });
35
-
31
+
36
32
  // Get initial state
37
33
  plusui.browser.getUrl().then(setCurrentUrl);
38
34
  plusui.browser.canGoBack().then(setCanGoBack);
39
35
  plusui.browser.canGoForward().then(setCanGoForward);
40
-
36
+
41
37
  // Listen for responses emitted from C++ via ch.customFileDrop.emit(...) in main.cpp
42
38
  const unsub = customFileDrop.on((data: any) => {
43
39
  setBackendMsg(data?.message ?? JSON.stringify(data));
@@ -62,10 +58,6 @@ function App() {
62
58
  const handleGoForward = async () => await plusui.browser.goForward();
63
59
  const handleReload = async () => await plusui.browser.reload();
64
60
 
65
- // Router navigation
66
- const handleGoHome = async () => await plusui.router.push('/');
67
- const handleGoSettings = async () => await plusui.router.push('/settings');
68
-
69
61
  // App control
70
62
  const handleQuit = async () => await plusui.app.quit();
71
63
 
@@ -88,7 +80,7 @@ function App() {
88
80
  return (
89
81
  <div class="app">
90
82
  <header class="app-header">
91
- <h1>{{PROJECT_NAME}} App</h1>
83
+ <h1>{{ PROJECT_NAME }} App</h1>
92
84
  <p>Built with PlusUI Framework</p>
93
85
  </header>
94
86
 
@@ -132,17 +124,6 @@ function App() {
132
124
  </div>
133
125
  </div>
134
126
 
135
- <div class="card">
136
- <h2>Router (SPA Navigation)</h2>
137
- <div class="button-group">
138
- <button onClick={handleGoHome} class="button">Home /</button>
139
- <button onClick={handleGoSettings} class="button">Settings</button>
140
- </div>
141
- <p style={{ 'font-size': '0.85em', color: '#666', 'margin-top': '10px' }}>
142
- Define routes with <code>plusui.router.setRoutes({'{ ... }'})</code> then navigate with <code>plusui.router.push('/path')</code>
143
- </p>
144
- </div>
145
-
146
127
  <div class="card">
147
128
  <h2>App Control</h2>
148
129
  <button onClick={handleQuit} class="button button-danger">Quit App</button>
@@ -156,7 +137,7 @@ function App() {
156
137
  The frontend receives the reply via <code>customFileDrop.on()</code>. Run{' '}
157
138
  <code>plusui connect</code> to regenerate the channel bindings from both sides.
158
139
  </p>
159
-
140
+
160
141
  <div
161
142
  class={`filedrop-zone ${isDragging() ? 'filedrop-active' : ''}`}
162
143
  onDragOver={handleDragOver}
@@ -167,7 +148,7 @@ function App() {
167
148
  <div class="filedrop-content">
168
149
  <svg class="filedrop-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
169
150
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width={2}
170
- d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
151
+ d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
171
152
  </svg>
172
153
  <div class="filedrop-text">
173
154
  {isDragging() ? 'Drop files here' : 'Drag & drop files to send to C++'}
@@ -186,7 +167,7 @@ function App() {
186
167
  <div class="filedrop-file-icon">📄</div>
187
168
  <div class="filedrop-file-info">
188
169
  <div class="filedrop-file-name">{file.name}</div>
189
- <div class="filedrop-file-meta">{plusui.formatFileSize(file.size)} • {file.type}</div>
170
+ <div class="filedrop-file-meta">{formatFileSize(file.size)} • {file.type}</div>
190
171
  </div>
191
172
  </div>
192
173
  )}
@@ -72,9 +72,9 @@ struct WindowConfig {
72
72
 
73
73
  // --- Routing ---
74
74
  // The frontend route this window opens on.
75
- // Use "/" for root, "/settings" for settings page, etc.
75
+ // Use "/app" for main view, "/settings" for settings page, etc.
76
76
  // Works with any frontend router (Solid Router, TanStack, etc.).
77
- std::string route = "/"; // Starting route (e.g. "/", "/settings", "/profile")
77
+ std::string route = "/app"; // Starting route (e.g. "/app", "/settings")
78
78
  int devServerPort = 5173; // Vite dev server port
79
79
  } windowConfig;
80
80
 
@@ -113,12 +113,13 @@ struct WebGPUConfig {
113
113
  // MAIN - Application Entry Point
114
114
  // ============================================================================
115
115
  // ── Connect instance ─────────────────────────────────────────────────────────
116
- // connect is the bridge between C++ and the frontend.
116
+ // conn is the bridge between C++ and the frontend.
117
117
  // Run `plusui connect` to generate Connections/ from your name.on / name.emit usage.
118
- // Then declare: Connections ch(connect);
119
- // and use: ch.myEvent.on([](const json& p) { ... });
120
- // ch.myEvent.emit({{"value", 42}});
121
- static Connect connect;
118
+ // Call initChannels(conn) once, then use channels directly:
119
+ // customFileDrop.on([](const json& p) { ... });
120
+ // customFileDrop.emit({{"value", 42}});
121
+ static Connect conn;
122
+ using json = nlohmann::json;
122
123
 
123
124
  int main() {
124
125
  // Build the app with configuration
@@ -185,12 +186,12 @@ int main() {
185
186
  // ========================================
186
187
  // CONNECT — bind frontend ↔ backend
187
188
  // ========================================
188
- // Wires the connect object to this window.
189
- // Connections ch gives you named channel objects — same API as TypeScript:
190
- // ch.myEvent.on([](const json& p) { ... }); // receive from frontend
191
- // ch.myEvent.emit({{"value", 42}}); // send to frontend
192
- bindConnect(mainWindow, connect);
193
- Connections ch(connect); // use ch.name.on() / ch.name.emit()
189
+ // Wires the connect object to this window, then initialises
190
+ // the auto-generated channels so you can use them directly:
191
+ // customFileDrop.on([](const json& p) { ... });
192
+ // customFileDrop.emit({{"value", 42}});
193
+ bindConnect(mainWindow, conn);
194
+ initChannels(conn);
194
195
 
195
196
  // ========================================
196
197
  // CUSTOM FILE DROP CHANNEL
@@ -199,7 +200,7 @@ int main() {
199
200
  // Frontend drops files → emits via customFileDrop.emit({ files: [...] })
200
201
  // C++ receives here, processes, then emits back to the frontend.
201
202
  // Frontend receives the reply via customFileDrop.on() in App.tsx.
202
- ch.customFileDrop.on([&ch](const json& payload) {
203
+ customFileDrop.on([](const json& payload) {
203
204
  auto files = payload.value("files", json::array());
204
205
  int count = static_cast<int>(files.size());
205
206
  std::cout << "customFileDrop: received " << count << " file(s) from frontend" << std::endl;
@@ -207,7 +208,7 @@ int main() {
207
208
  std::cout << " - " << f.value("name", "?") << " (" << f.value("size", 0) << " bytes)" << std::endl;
208
209
  }
209
210
  // Reply back to frontend — received by customFileDrop.on() in App.tsx
210
- ch.customFileDrop.emit({
211
+ customFileDrop.emit({
211
212
  {"processed", true},
212
213
  {"count", count},
213
214
  {"message", "C++ received " + std::to_string(count) + " file(s)"}
@@ -231,10 +232,10 @@ int main() {
231
232
  //
232
233
  // CONNECT (custom channels — same API on both sides):
233
234
  // Run `plusui connect` to generate Connections/ from your name.on / name.emit calls.
234
- // C++: ch.myEvent.on([](const json& p) { ... }); // receive
235
- // ch.myEvent.emit({{"value", 42}}); // send
236
- // TS: myEvent.on((data) => { ... }); // receive
237
- // myEvent.emit({ value: 42 }); // send
235
+ // C++: customFileDrop.on([](const json& p) { ... }); // receive
236
+ // customFileDrop.emit({{"value", 42}}); // send
237
+ // TS: customFileDrop.on((data) => { ... }); // receive
238
+ // customFileDrop.emit({ value: 42 }); // send
238
239
  //
239
240
  // WINDOW: win.minimize(), win.maximize(), win.close(), win.center(),
240
241
  // win.setSize(w, h), win.setPosition(x, y), win.setTitle(str),
@@ -1,837 +0,0 @@
1
-
2
- type InvokeFn = (method: string, args?: unknown[]) => Promise<unknown>;
3
- type PendingMap = Record<string, { resolve: (value: unknown) => void; reject: (reason?: unknown) => void }>;
4
-
5
- type WindowSize = { width: number; height: number };
6
- type WindowPosition = { x: number; y: number };
7
- type RouteMap = Record<string, string>;
8
-
9
- let _invoke: InvokeFn | null = null;
10
- let _pending: PendingMap = {};
11
- let _routes: RouteMap = {};
12
-
13
- function initBridge() {
14
- if (typeof window === 'undefined') return;
15
-
16
- const w = window as any;
17
- if (typeof w.__invoke__ === 'function') {
18
- _invoke = w.__invoke__ as InvokeFn;
19
- return;
20
- }
21
-
22
- _pending = {};
23
- w.__pending__ = _pending;
24
-
25
- w.__invoke__ = (method: string, args?: unknown[]): Promise<unknown> => {
26
- return new Promise((resolve, reject) => {
27
- const id = Math.random().toString(36).slice(2, 11);
28
- const request = JSON.stringify({ id, method, params: args ?? [] });
29
- _pending[id] = { resolve, reject };
30
-
31
- if (typeof w.__native_invoke__ === 'function') {
32
- w.__native_invoke__(request);
33
- } else {
34
- setTimeout(() => { delete _pending[id]; resolve(null); }, 0);
35
- }
36
-
37
- setTimeout(() => {
38
- if (_pending[id]) {
39
- delete _pending[id];
40
- reject(new Error(`${method} timed out`));
41
- }
42
- }, 30000);
43
- });
44
- };
45
-
46
- w.__response__ = (id: string, result: unknown) => {
47
- const pending = _pending[id];
48
- if (pending) { pending.resolve(result); delete _pending[id]; }
49
- };
50
-
51
- _invoke = w.__invoke__ as InvokeFn;
52
- }
53
-
54
- async function invoke(method: string, args?: unknown[]) {
55
- if (!_invoke) {
56
- initBridge();
57
- if (!_invoke) throw new Error('PlusUI bridge not initialized');
58
- }
59
- return _invoke(method, args);
60
- }
61
-
62
- initBridge();
63
-
64
- // ─── Connection (on / emit) ───────────────────────────────────────────────────
65
- //
66
- // TWO METHODS. FIVE PRIMITIVES. EVERYTHING YOU NEED.
67
- //
68
- // connect.emit('myEvent', { value: 42 }); // TS → C++
69
- // connect.on('myEvent', (data) => { ... }); // C++ → TS
70
- //
71
- // Built-in features use their feature name as a scope:
72
- // clipboard.on('changed', (data) => { ... }) // 'clipboard.changed'
73
- // win.on('resized', (data) => { ... }) // 'window.resized'
74
- //
75
- // ─────────────────────────────────────────────────────────────────────────────
76
-
77
- type MessageCallback = (payload: any) => void;
78
-
79
- class ConnectionClient {
80
- private pending = new Map<string, { resolve: (v: any) => void; reject: (e: Error) => void }>();
81
- private listeners = new Map<string, Set<MessageCallback>>();
82
-
83
- constructor() {
84
- const host = globalThis as any;
85
- host.__plusuiConnectionMessage = (message: unknown) => this.handleIncoming(message);
86
- if (typeof window !== 'undefined') {
87
- window.addEventListener('plusui:connection:message', (ev: Event) => {
88
- this.handleIncoming((ev as CustomEvent<unknown>).detail);
89
- });
90
- }
91
- }
92
-
93
- private nextId() { return `${Date.now()}-${Math.random().toString(16).slice(2)}`; }
94
-
95
- private async send(env: { kind: string; id?: string; name: string; payload?: unknown }): Promise<any> {
96
- const host = globalThis as any;
97
- if (typeof host.__invoke__ === 'function') return host.__invoke__('connection.dispatch', env);
98
- if (host.ipc?.postMessage) host.ipc.postMessage(JSON.stringify(env));
99
- return null;
100
- }
101
-
102
- private decode(message: unknown): any | null {
103
- if (!message) return null;
104
- if (typeof message === 'string') { try { return JSON.parse(message); } catch { return null; } }
105
- if (typeof message === 'object') return message;
106
- return null;
107
- }
108
-
109
- private handleIncoming(message: unknown): void {
110
- const env = this.decode(message);
111
- if (!env) return;
112
- if ((env.kind === 'result' || env.kind === 'error') && env.id) {
113
- const entry = this.pending.get(env.id);
114
- if (!entry) return;
115
- this.pending.delete(env.id);
116
- if (env.kind === 'error') entry.reject(new Error(env.error || 'Connection call failed'));
117
- else entry.resolve(env.payload);
118
- return;
119
- }
120
- if (env.kind === 'event' || env.kind === 'stream' || env.kind === 'publish') {
121
- const handlers = this.listeners.get(env.name);
122
- if (handlers) for (const h of handlers) h(env.payload);
123
- }
124
- }
125
-
126
- async call<TOut = unknown, TIn = Record<string, unknown>>(name: string, payload: TIn): Promise<TOut> {
127
- const id = this.nextId();
128
- const promise = new Promise<TOut>((resolve, reject) => this.pending.set(id, { resolve, reject }));
129
- await this.send({ kind: 'call', id, name, payload });
130
- return promise;
131
- }
132
-
133
- fire<TIn = Record<string, unknown>>(name: string, payload: TIn): void {
134
- void this.send({ kind: 'fire', name, payload });
135
- }
136
-
137
- on<TData = unknown>(name: string, callback: (payload: TData) => void): () => void {
138
- const set = this.listeners.get(name) ?? new Set<MessageCallback>();
139
- set.add(callback as MessageCallback);
140
- this.listeners.set(name, set);
141
- return () => {
142
- const cur = this.listeners.get(name);
143
- if (!cur) return;
144
- cur.delete(callback as MessageCallback);
145
- if (cur.size === 0) this.listeners.delete(name);
146
- };
147
- }
148
-
149
- stream<TData = unknown>(name: string) {
150
- return {
151
- subscribe: (cb: (payload: TData) => void): (() => void) => {
152
- void this.send({ kind: 'sub', name });
153
- const off = this.on<TData>(name, cb);
154
- return () => { off(); void this.send({ kind: 'unsub', name }); };
155
- },
156
- };
157
- }
158
-
159
- channel<TData = unknown>(name: string) {
160
- return {
161
- subscribe: (cb: (payload: TData) => void): (() => void) => {
162
- void this.send({ kind: 'sub', name });
163
- const off = this.on<TData>(name, cb);
164
- return () => { off(); void this.send({ kind: 'unsub', name }); };
165
- },
166
- publish: (payload: TData): void => { void this.send({ kind: 'publish', name, payload }); },
167
- };
168
- }
169
- }
170
-
171
- const _client = new ConnectionClient();
172
-
173
- // ─── FeatureConnect ───────────────────────────────────────────────────────────
174
-
175
- export type FeatureConnect = {
176
- on: <TData = unknown>(name: string, cb: (payload: TData) => void) => (() => void);
177
- emit: <TIn = Record<string, unknown>>(name: string, payload: TIn) => void;
178
- call: <TOut = unknown, TIn = Record<string, unknown>>(name: string, payload: TIn) => Promise<TOut>;
179
- stream: <TData = unknown>(name: string) => { subscribe: (cb: (payload: TData) => void) => (() => void) };
180
- channel: <TData = unknown>(name: string) => {
181
- subscribe: (cb: (payload: TData) => void) => (() => void);
182
- publish: (payload: TData) => void;
183
- };
184
- scoped: (scope: string) => FeatureConnect;
185
- };
186
-
187
- function _scopeName(scope: string, name: string): string {
188
- return name.startsWith(`${scope}.`) ? name : `${scope}.${name}`;
189
- }
190
-
191
- async function _invokeScoped(method: string, payload?: unknown): Promise<unknown> {
192
- const host = globalThis as any;
193
- if (typeof host.__invoke__ !== 'function') return undefined;
194
- return host.__invoke__(method, payload === undefined ? [] : [payload]);
195
- }
196
-
197
- export function createFeatureConnect(scope: string): FeatureConnect {
198
- return {
199
- emit<TIn = Record<string, unknown>>(name: string, payload: TIn) {
200
- const s = _scopeName(scope, name);
201
- void _invokeScoped(s, payload);
202
- _client.fire(s, payload);
203
- if (typeof window !== 'undefined') {
204
- window.dispatchEvent(new CustomEvent(`plusui:${s}`, { detail: payload }));
205
- }
206
- },
207
- on<TData = unknown>(name: string, cb: (payload: TData) => void): () => void {
208
- const s = _scopeName(scope, name);
209
- const off = _client.on<TData>(s, cb);
210
- if (typeof window === 'undefined') return off;
211
- const dom = (e: Event) => cb((e as CustomEvent<TData>).detail);
212
- window.addEventListener(`plusui:${s}`, dom as EventListener);
213
- return () => { off(); window.removeEventListener(`plusui:${s}`, dom as EventListener); };
214
- },
215
- call<TOut = unknown, TIn = Record<string, unknown>>(name: string, payload: TIn): Promise<TOut> {
216
- const s = _scopeName(scope, name);
217
- const host = globalThis as any;
218
- if (typeof host.__invoke__ === 'function') return _invokeScoped(s, payload) as Promise<TOut>;
219
- return _client.call<TOut, TIn>(s, payload);
220
- },
221
- stream<TData = unknown>(name: string) { return _client.stream<TData>(_scopeName(scope, name)); },
222
- channel<TData = unknown>(name: string) { return _client.channel<TData>(_scopeName(scope, name)); },
223
- scoped: (child: string) => createFeatureConnect(_scopeName(scope, child)),
224
- };
225
- }
226
-
227
- // ─── connect — custom channels (your app-specific messages) ──────────────────
228
- export const connect = {
229
- /** Send a message to C++ backend */
230
- emit<TIn = Record<string, unknown>>(name: string, payload: TIn): void {
231
- _client.fire(name, payload);
232
- },
233
- /** Listen for messages from C++ backend. Returns unsubscribe fn. */
234
- on<TData = unknown>(name: string, cb: (payload: TData) => void): () => void {
235
- return _client.on<TData>(name, cb);
236
- },
237
- /** Scoped feature connection (auto-prefixes names) */
238
- feature: createFeatureConnect,
239
- };
240
-
241
- /** Advanced: raw connection client — used by generated code */
242
- export { _client, _client as connection };
243
-
244
- // ─── win — window management ──────────────────────────────────────────────────
245
- const _winEvents = createFeatureConnect('window');
246
-
247
- export const win = {
248
- minimize: async () => invoke('window.minimize', []),
249
- maximize: async () => invoke('window.maximize', []),
250
- show: async () => invoke('window.show', []),
251
- hide: async () => invoke('window.hide', []),
252
- close: async () => invoke('window.close', []),
253
- center: async () => invoke('window.center', []),
254
- setTitle: async (title: string) => invoke('window.setTitle', [title]),
255
- setSize: async (w: number, h: number) => invoke('window.setSize', [w, h]),
256
- setMinSize: async (w: number, h: number) => invoke('window.setMinSize', [w, h]),
257
- setMaxSize: async (w: number, h: number) => invoke('window.setMaxSize', [w, h]),
258
- setPosition: async (x: number, y: number) => invoke('window.setPosition', [x, y]),
259
- setAlwaysOnTop: async (v: boolean) => invoke('window.setAlwaysOnTop', [v]),
260
- setFullscreen: async (v: boolean) => invoke('window.setFullscreen', [v]),
261
- setOpacity: async (v: number) => invoke('window.setOpacity', [v]),
262
- getSize: async (): Promise<WindowSize> => invoke('window.getSize', []) as Promise<WindowSize>,
263
- getPosition: async (): Promise<WindowPosition> => invoke('window.getPosition', []) as Promise<WindowPosition>,
264
- isMaximized: async (): Promise<boolean> => invoke('window.isMaximized', []) as Promise<boolean>,
265
- isVisible: async (): Promise<boolean> => invoke('window.isVisible', []) as Promise<boolean>,
266
- on: _winEvents.on.bind(_winEvents),
267
- emit: _winEvents.emit.bind(_winEvents),
268
- };
269
-
270
- // ─── browser ──────────────────────────────────────────────────────────────────
271
- const _browserEvents = createFeatureConnect('browser');
272
-
273
- export const browser = {
274
- getUrl: async (): Promise<string> => invoke('browser.getUrl', []) as Promise<string>,
275
- navigate: async (url: string) => invoke('browser.navigate', [url]),
276
- goBack: async () => invoke('browser.goBack', []),
277
- goForward: async () => invoke('browser.goForward', []),
278
- reload: async () => invoke('browser.reload', []),
279
- canGoBack: async (): Promise<boolean> => invoke('browser.canGoBack', []) as Promise<boolean>,
280
- canGoForward: async (): Promise<boolean> => invoke('browser.canGoForward', []) as Promise<boolean>,
281
- onNavigate: (handler: (url: string) => void) => {
282
- if (typeof window === 'undefined') return () => { };
283
- const h = (e: Event) => handler((e as CustomEvent<{ url?: string }>).detail?.url ?? '');
284
- window.addEventListener('plusui:navigate', h);
285
- return () => window.removeEventListener('plusui:navigate', h);
286
- },
287
- on: _browserEvents.on.bind(_browserEvents),
288
- emit: _browserEvents.emit.bind(_browserEvents),
289
- };
290
-
291
- // ─── router ───────────────────────────────────────────────────────────────────
292
- export const router = {
293
- setRoutes: (routes: RouteMap) => { _routes = routes; },
294
- push: async (path: string) => invoke('browser.navigate', [_routes[path] ?? path]),
295
- };
296
-
297
- // ─── app ──────────────────────────────────────────────────────────────────────
298
- const _appEvents = createFeatureConnect('app');
299
-
300
- export const app = {
301
- quit: async () => invoke('app.quit', []),
302
- on: _appEvents.on.bind(_appEvents),
303
- emit: _appEvents.emit.bind(_appEvents),
304
- };
305
-
306
- // ─── clipboard ────────────────────────────────────────────────────────────────
307
- const _clipboardEvents = createFeatureConnect('clipboard');
308
-
309
- export const clipboard = {
310
- getText: async (): Promise<string> => invoke('clipboard.getText', []) as Promise<string>,
311
- setText: async (text: string) => invoke('clipboard.setText', [text]),
312
- clear: async () => invoke('clipboard.clear', []),
313
- hasText: async (): Promise<boolean> => invoke('clipboard.hasText', []) as Promise<boolean>,
314
- on: _clipboardEvents.on.bind(_clipboardEvents),
315
- emit: _clipboardEvents.emit.bind(_clipboardEvents),
316
- };
317
-
318
- // ─── fileDrop ─────────────────────────────────────────────────────────────────
319
- export interface FileInfo { path: string; name: string; type: string; size: number; }
320
-
321
- export const fileDrop = {
322
- setEnabled: async (enabled: boolean) => invoke('fileDrop.setEnabled', [enabled]),
323
- isEnabled: async (): Promise<boolean> => invoke('fileDrop.isEnabled', []) as Promise<boolean>,
324
- onFilesDropped: (handler: (files: FileInfo[]) => void) => {
325
- if (typeof window === 'undefined') return () => { };
326
- const h = (e: Event) => handler((e as CustomEvent<{ files?: FileInfo[] }>).detail?.files ?? []);
327
- window.addEventListener('plusui:fileDrop.filesDropped', h);
328
- return () => window.removeEventListener('plusui:fileDrop.filesDropped', h);
329
- },
330
- onDragEnter: (handler: () => void) => {
331
- if (typeof window === 'undefined') return () => { };
332
- window.addEventListener('plusui:fileDrop.dragEnter', handler);
333
- return () => window.removeEventListener('plusui:fileDrop.dragEnter', handler);
334
- },
335
- onDragLeave: (handler: () => void) => {
336
- if (typeof window === 'undefined') return () => { };
337
-
338
- // ─── win — window management ──────────────────────────────────────────────────
339
- const _winEvents = createFeatureConnect('window');
340
-
341
- export const win = {
342
- minimize: async () => invoke('window.minimize', []),
343
- maximize: async () => invoke('window.maximize', []),
344
- show: async () => invoke('window.show', []),
345
- hide: async () => invoke('window.hide', []),
346
- close: async () => invoke('window.close', []),
347
- center: async () => invoke('window.center', []),
348
- setTitle: async (title: string) => invoke('window.setTitle', [title]),
349
- setSize: async (w: number, h: number) => invoke('window.setSize', [w, h]),
350
- setMinSize: async (w: number, h: number) => invoke('window.setMinSize', [w, h]),
351
- setMaxSize: async (w: number, h: number) => invoke('window.setMaxSize', [w, h]),
352
- setPosition: async (x: number, y: number) => invoke('window.setPosition', [x, y]),
353
- setAlwaysOnTop: async (v: boolean) => invoke('window.setAlwaysOnTop', [v]),
354
- setFullscreen: async (v: boolean) => invoke('window.setFullscreen', [v]),
355
- setOpacity: async (v: number) => invoke('window.setOpacity', [v]),
356
- getSize: async (): Promise<WindowSize> => invoke('window.getSize', []) as Promise<WindowSize>,
357
- getPosition: async (): Promise<WindowPosition> => invoke('window.getPosition', []) as Promise<WindowPosition>,
358
- isMaximized: async (): Promise<boolean> => invoke('window.isMaximized', []) as Promise<boolean>,
359
- isVisible: async (): Promise<boolean> => invoke('window.isVisible', []) as Promise<boolean>,
360
- on: _winEvents.on.bind(_winEvents),
361
- emit: _winEvents.emit.bind(_winEvents),
362
- };
363
-
364
- // ─── browser ──────────────────────────────────────────────────────────────────
365
- const _browserEvents = createFeatureConnect('browser');
366
-
367
- export const browser = {
368
- getUrl: async (): Promise<string> => invoke('browser.getUrl', []) as Promise<string>,
369
- navigate: async (url: string) => invoke('browser.navigate', [url]),
370
- goBack: async () => invoke('browser.goBack', []),
371
- goForward: async () => invoke('browser.goForward', []),
372
- reload: async () => invoke('browser.reload', []),
373
- canGoBack: async (): Promise<boolean> => invoke('browser.canGoBack', []) as Promise<boolean>,
374
- canGoForward: async (): Promise<boolean> => invoke('browser.canGoForward', []) as Promise<boolean>,
375
- onNavigate: (handler: (url: string) => void) => {
376
- if (typeof window === 'undefined') return () => { };
377
- const h = (e: Event) => handler((e as CustomEvent<{ url?: string }>).detail?.url ?? '');
378
- window.addEventListener('plusui:navigate', h);
379
- return () => window.removeEventListener('plusui:navigate', h);
380
- },
381
- on: _browserEvents.on.bind(_browserEvents),
382
- emit: _browserEvents.emit.bind(_browserEvents),
383
- };
384
-
385
- // ─── router ───────────────────────────────────────────────────────────────────
386
- export const router = {
387
- setRoutes: (routes: RouteMap) => { _routes = routes; },
388
- push: async (path: string) => invoke('browser.navigate', [_routes[path] ?? path]),
389
- };
390
-
391
- // ─── app ──────────────────────────────────────────────────────────────────────
392
- const _appEvents = createFeatureConnect('app');
393
-
394
- export const app = {
395
- quit: async () => invoke('app.quit', []),
396
- on: _appEvents.on.bind(_appEvents),
397
- emit: _appEvents.emit.bind(_appEvents),
398
- };
399
-
400
- // ─── clipboard ────────────────────────────────────────────────────────────────
401
- const _clipboardEvents = createFeatureConnect('clipboard');
402
-
403
- export const clipboard = {
404
- getText: async (): Promise<string> => invoke('clipboard.getText', []) as Promise<string>,
405
- setText: async (text: string) => invoke('clipboard.setText', [text]),
406
- clear: async () => invoke('clipboard.clear', []),
407
- hasText: async (): Promise<boolean> => invoke('clipboard.hasText', []) as Promise<boolean>,
408
- on: _clipboardEvents.on.bind(_clipboardEvents),
409
- emit: _clipboardEvents.emit.bind(_clipboardEvents),
410
- };
411
-
412
- // ─── fileDrop ─────────────────────────────────────────────────────────────────
413
- export interface FileInfo { path: string; name: string; type: string; size: number; }
414
-
415
- export const fileDrop = {
416
- setEnabled: async (enabled: boolean) => invoke('fileDrop.setEnabled', [enabled]),
417
- isEnabled: async (): Promise<boolean> => invoke('fileDrop.isEnabled', []) as Promise<boolean>,
418
- onFilesDropped: (handler: (files: FileInfo[]) => void) => {
419
- if (typeof window === 'undefined') return () => { };
420
- const h = (e: Event) => handler((e as CustomEvent<{ files?: FileInfo[] }>).detail?.files ?? []);
421
- window.addEventListener('plusui:fileDrop.filesDropped', h);
422
- return () => window.removeEventListener('plusui:fileDrop.filesDropped', h);
423
- },
424
- onDragEnter: (handler: () => void) => {
425
- if (typeof window === 'undefined') return () => { };
426
- window.addEventListener('plusui:fileDrop.dragEnter', handler);
427
- return () => window.removeEventListener('plusui:fileDrop.dragEnter', handler);
428
- },
429
- onDragLeave: (handler: () => void) => {
430
- if (typeof window === 'undefined') return () => { };
431
- window.addEventListener('plusui:fileDrop.dragLeave', handler);
432
- return () => window.removeEventListener('plusui:fileDrop.dragLeave', handler);
433
- },
434
- };
435
-
436
- // ─── keyboard ─────────────────────────────────────────────────────────────────
437
- export enum KeyCode {
438
- Unknown = 0, Space = 32, Escape = 256, Enter = 257, Tab = 258,
439
- Backspace = 259, Delete = 261, Right = 262, Left = 263, Down = 264, Up = 265,
440
- F1 = 290, F2 = 291, F3 = 292, F4 = 293, F5 = 294, F6 = 295,
441
- F7 = 296, F8 = 297, F9 = 298, F10 = 299, F11 = 300, F12 = 301,
442
- LeftShift = 340, LeftControl = 341, LeftAlt = 342,
443
- }
444
- export enum KeyMod { None = 0, Shift = 1, Control = 2, Alt = 4, Super = 8 }
445
- export interface KeyEvent { key: KeyCode; scancode: number; mods: KeyMod; pressed: boolean; repeat: boolean; keyName: string; }
446
- export interface Shortcut { key: KeyCode; mods: KeyMod; }
447
-
448
- const _shortcutHandlers = new Map<string, () => void>();
449
-
450
- export const keyboard = {
451
- isKeyPressed: async (key: KeyCode): Promise<boolean> => invoke('keyboard.isKeyPressed', [key]) as Promise<boolean>,
452
- setAutoRepeat: async (enabled: boolean): Promise<void> => { await invoke('keyboard.setAutoRepeat', [enabled]); },
453
- getAutoRepeat: async (): Promise<boolean> => invoke('keyboard.getAutoRepeat') as Promise<boolean>,
454
- async registerShortcut(id: string, shortcut: Shortcut, callback: () => void): Promise<boolean> {
455
- _shortcutHandlers.set(id, callback);
456
- return invoke<boolean>('keyboard.registerShortcut', [id, shortcut]);
457
- },
458
- async unregisterShortcut(id: string): Promise<boolean> {
459
- _shortcutHandlers.delete(id);
460
- return invoke<boolean>('keyboard.unregisterShortcut', [id]);
461
- },
462
- async clearShortcuts(): Promise<void> {
463
- _shortcutHandlers.clear();
464
- await invoke('keyboard.clearShortcuts');
465
- },
466
- onKeyDown(callback: (event: KeyEvent) => void): () => void {
467
- if (typeof window === 'undefined') return () => { };
468
- const h = (e: Event) => callback((e as CustomEvent<KeyEvent>).detail);
469
- window.addEventListener('plusui:keyboard.keydown', h);
470
- return () => window.removeEventListener('plusui:keyboard.keydown', h);
471
- },
472
- onKeyUp(callback: (event: KeyEvent) => void): () => void {
473
- if (typeof window === 'undefined') return () => { };
474
- const h = (e: Event) => callback((e as CustomEvent<KeyEvent>).detail);
475
- window.addEventListener('plusui:keyboard.keyup', h);
476
- return () => window.removeEventListener('plusui:keyboard.keyup', h);
477
- },
478
- onShortcut(callback: (id: string) => void): () => void {
479
- if (typeof window === 'undefined') return () => { };
480
- const h = (e: Event) => callback((e as CustomEvent<{ id: string }>).detail.id);
481
- window.addEventListener('plusui:keyboard.shortcut', h);
482
- return () => window.removeEventListener('plusui:keyboard.shortcut', h);
483
- },
484
- parseShortcut(str: string): Shortcut {
485
- const parts = str.toLowerCase().split('+');
486
- let mods = KeyMod.None;
487
- let key = KeyCode.Unknown;
488
- for (const part of parts) {
489
- const t = part.trim();
490
- if (t === 'ctrl' || t === 'control') mods |= KeyMod.Control;
491
- else if (t === 'alt') mods |= KeyMod.Alt;
492
- else if (t === 'shift') mods |= KeyMod.Shift;
493
- else if (t === 'super' || t === 'win' || t === 'cmd') mods |= KeyMod.Super;
494
- else key = this.keyNameToCode(t);
495
- }
496
- return { key, mods };
497
- },
498
- keyNameToCode(name: string): KeyCode {
499
- const map: Record<string, KeyCode> = {
500
- space: KeyCode.Space, escape: KeyCode.Escape, enter: KeyCode.Enter,
501
- tab: KeyCode.Tab, backspace: KeyCode.Backspace, delete: KeyCode.Delete,
502
- right: KeyCode.Right, left: KeyCode.Left, down: KeyCode.Down, up: KeyCode.Up,
503
- f1: KeyCode.F1, f2: KeyCode.F2, f3: KeyCode.F3, f4: KeyCode.F4,
504
- f5: KeyCode.F5, f6: KeyCode.F6, f7: KeyCode.F7, f8: KeyCode.F8,
505
- f9: KeyCode.F9, f10: KeyCode.F10, f11: KeyCode.F11, f12: KeyCode.F12,
506
- };
507
- return map[name] ?? KeyCode.Unknown;
508
- },
509
- };
510
-
511
- // ─── tray ─────────────────────────────────────────────────────────────────────
512
- export interface TrayMenuItem { id: string; label: string; icon?: string; enabled?: boolean; checked?: boolean; separator?: boolean; submenu?: TrayMenuItem[]; }
513
- export interface TrayIconData { id: number; tooltip: string; iconPath: string; isVisible: boolean; }
514
-
515
- export const tray = {
516
- setIcon: async (iconPath: string): Promise<void> => { await invoke('tray.setIcon', [iconPath]); },
517
- setTooltip: async (tooltip: string): Promise<void> => { await invoke('tray.setTooltip', [tooltip]); },
518
- setVisible: async (visible: boolean): Promise<void> => { await invoke('tray.setVisible', [visible]); },
519
- setMenu: async (items: TrayMenuItem[]): Promise<void> => { await invoke('tray.setMenu', [items]); },
520
- setContextMenu: async (items: TrayMenuItem[]): Promise<void> => { await invoke('tray.setContextMenu', [items]); },
521
- onClick(callback: (x: number, y: number) => void): () => void {
522
- if (typeof window === 'undefined') return () => { };
523
- const h = (e: Event) => { const d = (e as CustomEvent<{ x: number; y: number }>).detail; callback(d.x, d.y); };
524
- window.addEventListener('plusui:tray.click', h);
525
- return () => window.removeEventListener('plusui:tray.click', h);
526
- },
527
- onDoubleClick(callback: () => void): () => void {
528
- if (typeof window === 'undefined') return () => { };
529
- window.addEventListener('plusui:tray.doubleClick', callback);
530
- return () => window.removeEventListener('plusui:tray.doubleClick', callback);
531
- },
532
- onRightClick(callback: (x: number, y: number) => void): () => void {
533
- if (typeof window === 'undefined') return () => { };
534
- const h = (e: Event) => { const d = (e as CustomEvent<{ x: number; y: number }>).detail; callback(d.x, d.y); };
535
- window.addEventListener('plusui:tray.rightClick', h);
536
- return () => window.removeEventListener('plusui:tray.rightClick', h);
537
- },
538
- onMenuItemClick(callback: (id: string) => void): () => void {
539
- if (typeof window === 'undefined') return () => { };
540
- const h = (e: Event) => callback((e as CustomEvent<{ id: string }>).detail.id);
541
- window.addEventListener('plusui:tray.menuItemClick', h);
542
- return () => window.removeEventListener('plusui:tray.menuItemClick', h);
543
- },
544
- };
545
-
546
- // ─── display ──────────────────────────────────────────────────────────────────
547
- export interface DisplayMode { width: number; height: number; refreshRate: number; bitDepth: number; }
548
- export interface DisplayBounds { x: number; y: number; width: number; height: number; }
549
- export interface DisplayResolution { width: number; height: number; }
550
- export interface Display {
551
- id: number; name: string; isPrimary: boolean;
552
- bounds: DisplayBounds; workArea: DisplayBounds; resolution: DisplayResolution;
553
- currentMode: DisplayMode; scaleFactor: number; rotation: number;
554
- isInternal: boolean; isConnected: boolean;
555
- }
556
-
557
- export const display = {
558
- getAllDisplays: async (): Promise<Display[]> => invoke<Display[]>('display.getAllDisplays'),
559
- getPrimaryDisplay: async (): Promise<Display> => invoke<Display>('display.getPrimaryDisplay'),
560
- getDisplayAt: async (x: number, y: number): Promise<Display> => invoke<Display>('display.getDisplayAt', [x, y]),
561
- getDisplayAtCursor: async (): Promise<Display> => invoke<Display>('display.getDisplayAtCursor'),
562
- getDisplayById: async (id: number): Promise<Display> => invoke<Display>('display.getDisplayById', [id]),
563
- setDisplayMode: async (displayId: number, mode: DisplayMode): Promise<boolean> => invoke<boolean>('display.setDisplayMode', [displayId, mode]),
564
- setPosition: async (displayId: number, x: number, y: number): Promise<boolean> => invoke<boolean>('display.setPosition', [displayId, x, y]),
565
- turnOff: async (displayId: number): Promise<boolean> => invoke<boolean>('display.turnOff', [displayId]),
566
- getScreenWidth: async (): Promise<number> => invoke<number>('screen.getWidth'),
567
- getScreenHeight: async (): Promise<number> => invoke<number>('screen.getHeight'),
568
- getScaleFactor: async (): Promise<number> => invoke<number>('screen.getScaleFactor'),
569
- getRefreshRate: async (): Promise<number> => invoke<number>('screen.getRefreshRate'),
570
- onConnected(callback: (d: Display) => void): () => void {
571
- if (typeof window === 'undefined') return () => { };
572
- const h = (e: Event) => callback((e as CustomEvent<Display>).detail);
573
- window.addEventListener('plusui:display.connected', h);
574
- return () => window.removeEventListener('plusui:display.connected', h);
575
- },
576
- onDisconnected(callback: (id: number) => void): () => void {
577
- if (typeof window === 'undefined') return () => { };
578
- const h = (e: Event) => callback((e as CustomEvent<{ id: number }>).detail.id);
579
- window.addEventListener('plusui:display.disconnected', h);
580
- return () => window.removeEventListener('plusui:display.disconnected', h);
581
- },
582
- onChanged(callback: (d: Display) => void): () => void {
583
- if (typeof window === 'undefined') return () => { };
584
- const h = (e: Event) => callback((e as CustomEvent<Display>).detail);
585
- window.addEventListener('plusui:display.changed', h);
586
- return () => window.removeEventListener('plusui:display.changed', h);
587
- },
588
- };
589
-
590
- // ─── menu ─────────────────────────────────────────────────────────────────────
591
- export type MenuItemType = 'normal' | 'separator' | 'submenu' | 'checkbox' | 'radio';
592
- export interface MenuItem {
593
- id: string; label: string; accelerator?: string; icon?: string;
594
- type?: MenuItemType; enabled?: boolean; checked?: boolean;
595
- submenu?: MenuItem[]; click?: (item: MenuItem) => void; data?: Record<string, unknown>;
596
- }
597
- export interface ContextMenuOptions { x?: number; y?: number; selector?: string; context?: Record<string, unknown>; }
598
- export interface ContextInfo { x: number; y: number; clientX: number; clientY: number; selector: string; tagName: string; isEditable: boolean; hasSelection: boolean; selectedText?: string; }
599
-
600
- const _menuClickHandlers = new Map<string, (item: MenuItem) => void>();
601
-
602
- function _registerMenuClicks(items: MenuItem[]): void {
603
- for (const item of items) {
604
- if (item.click) _menuClickHandlers.set(item.id, item.click);
605
- if (item.submenu) _registerMenuClicks(item.submenu);
606
- }
607
- }
608
-
609
- function _stripMenuFunctions(items: MenuItem[]): unknown[] {
610
- return items.map(({ click: _c, submenu, ...rest }) => ({
611
- ...rest,
612
- ...(submenu ? { submenu: _stripMenuFunctions(submenu) } : {}),
613
- }));
614
- }
615
-
616
- export const menu = {
617
- async create(items: MenuItem[]): Promise<string> {
618
- _registerMenuClicks(items);
619
- return invoke<string>('menu.create', [_stripMenuFunctions(items)]);
620
- },
621
- popup: async (menuId: string, x?: number, y?: number): Promise<void> => { await invoke('menu.popup', [menuId, x ?? 0, y ?? 0]); },
622
- popupAtCursor: async (menuId: string): Promise<void> => { await invoke('menu.popupAtCursor', [menuId]); },
623
- close: async (menuId: string): Promise<void> => { await invoke('menu.close', [menuId]); },
624
- destroy: async (menuId: string): Promise<void> => { await invoke('menu.destroy', [menuId]); },
625
- async setApplicationMenu(items: MenuItem[]): Promise<void> {
626
- _registerMenuClicks(items);
627
- await invoke('menu.setApplicationMenu', [_stripMenuFunctions(items)]);
628
- },
629
- getApplicationMenu: async (): Promise<MenuItem[]> => invoke<MenuItem[]>('menu.getApplicationMenu'),
630
- async appendToMenuBar(item: MenuItem): Promise<void> {
631
- _registerMenuClicks([item]);
632
- await invoke('menu.appendToMenuBar', [_stripMenuFunctions([item])[0]]);
633
- },
634
- async showContextMenu(items: MenuItem[], options: ContextMenuOptions = {}): Promise<void> {
635
- const menuId = await menu.create(items);
636
- await menu.popup(menuId, options.x, options.y);
637
- },
638
- onItemClick(callback: (id: string) => void): () => void {
639
- if (typeof window === 'undefined') return () => { };
640
- const h = (e: Event) => callback((e as CustomEvent<{ id: string }>).detail.id);
641
- window.addEventListener('plusui:menu.itemClick', h);
642
- return () => window.removeEventListener('plusui:menu.itemClick', h);
643
- },
644
- onContextOpen(callback: (info: ContextInfo) => void): () => void {
645
- if (typeof window === 'undefined') return () => { };
646
- const h = (e: Event) => callback((e as CustomEvent<ContextInfo>).detail);
647
- window.addEventListener('plusui:menu.contextOpen', h);
648
- return () => window.removeEventListener('plusui:menu.contextOpen', h);
649
- },
650
- createEditMenu(handlers?: Partial<{ undo: () => void; redo: () => void; cut: () => void; copy: () => void; paste: () => void; selectAll: () => void; }>): MenuItem {
651
- return {
652
- id: 'edit', label: '&Edit', submenu: [
653
- { id: 'undo', label: 'Undo', accelerator: 'Ctrl+Z', click: handlers?.undo },
654
- { id: 'redo', label: 'Redo', accelerator: 'Ctrl+Y', click: handlers?.redo },
655
- { id: 'sep1', label: '', type: 'separator' },
656
- { id: 'cut', label: 'Cut', accelerator: 'Ctrl+X', click: handlers?.cut },
657
- { id: 'copy', label: 'Copy', accelerator: 'Ctrl+C', click: handlers?.copy },
658
- { id: 'paste', label: 'Paste', accelerator: 'Ctrl+V', click: handlers?.paste },
659
- { id: 'sep2', label: '', type: 'separator' },
660
- { id: 'selectAll', label: 'Select All', accelerator: 'Ctrl+A', click: handlers?.selectAll },
661
- ]
662
- };
663
- },
664
- createFileMenu(handlers?: Partial<{ new: () => void; open: () => void; save: () => void; saveAs: () => void; exit: () => void; }>): MenuItem {
665
- return {
666
- id: 'file', label: '&File', submenu: [
667
- { id: 'new', label: 'New', accelerator: 'Ctrl+N', click: handlers?.new },
668
- { id: 'open', label: 'Open...', accelerator: 'Ctrl+O', click: handlers?.open },
669
- { id: 'sep1', label: '', type: 'separator' },
670
- { id: 'save', label: 'Save', accelerator: 'Ctrl+S', click: handlers?.save },
671
- { id: 'saveAs', label: 'Save As...', accelerator: 'Ctrl+Shift+S', click: handlers?.saveAs },
672
- { id: 'sep2', label: '', type: 'separator' },
673
- { id: 'exit', label: 'Exit', accelerator: 'Alt+F4', click: handlers?.exit },
674
- ]
675
- };
676
- },
677
- createViewMenu(handlers?: Partial<{ zoomIn: () => void; zoomOut: () => void; resetZoom: () => void; fullscreen: () => void; devtools: () => void; }>): MenuItem {
678
- return {
679
- id: 'view', label: '&View', submenu: [
680
- { id: 'zoomIn', label: 'Zoom In', accelerator: 'Ctrl++', click: handlers?.zoomIn },
681
- { id: 'zoomOut', label: 'Zoom Out', accelerator: 'Ctrl+-', click: handlers?.zoomOut },
682
- { id: 'resetZoom', label: 'Reset Zoom', accelerator: 'Ctrl+0', click: handlers?.resetZoom },
683
- { id: 'sep1', label: '', type: 'separator' },
684
- { id: 'fullscreen', label: 'Toggle Fullscreen', accelerator: 'F11', click: handlers?.fullscreen },
685
- { id: 'sep2', label: '', type: 'separator' },
686
- { id: 'devtools', label: 'Developer Tools', accelerator: 'F12', click: handlers?.devtools },
687
- ]
688
- };
689
- },
690
- createTextContextMenu(): MenuItem[] { return [{ id: 'cut', label: 'Cut', accelerator: 'Ctrl+X' }, { id: 'copy', label: 'Copy', accelerator: 'Ctrl+C' }, { id: 'paste', label: 'Paste', accelerator: 'Ctrl+V' }, { id: 'sep1', label: '', type: 'separator' }, { id: 'selectAll', label: 'Select All', accelerator: 'Ctrl+A' }]; },
691
- createImageContextMenu(): MenuItem[] { return [{ id: 'copyImage', label: 'Copy Image' }, { id: 'saveImage', label: 'Save Image As...' }, { id: 'sep1', label: '', type: 'separator' }, { id: 'openInNewTab', label: 'Open Image in New Tab' }]; },
692
- createLinkContextMenu(): MenuItem[] { return [{ id: 'openLink', label: 'Open Link' }, { id: 'openInNewTab', label: 'Open in New Tab' }, { id: 'sep1', label: '', type: 'separator' }, { id: 'copyLink', label: 'Copy Link Address' }]; },
693
- dispose() { _menuClickHandlers.clear(); },
694
- };
695
-
696
- // ─── gpu ──────────────────────────────────────────────────────────────────────
697
- export interface GPUAdapter { requestDevice(descriptor?: GPUDeviceDescriptor): Promise<GPUDevice>; features: Set<string>; limits: Record<string, number>; info?: GPUAdapterInfo; }
698
- export interface GPUAdapterInfo { vendor?: string; architecture?: string; device?: string; description?: string; }
699
- export interface GPUDevice {
700
- createBuffer(d: GPUBufferDescriptor): GPUBuffer;
701
- createTexture(d: GPUTextureDescriptor): GPUTexture;
702
- createSampler(d?: GPUSamplerDescriptor): GPUSampler;
703
- createShaderModule(d: GPUShaderModuleDescriptor): GPUShaderModule;
704
- createRenderPipeline(d: GPURenderPipelineDescriptor): GPURenderPipeline;
705
- createComputePipeline(d: GPUComputePipelineDescriptor): GPUComputePipeline;
706
- createBindGroupLayout(d: GPUBindGroupLayoutDescriptor): GPUBindGroupLayout;
707
- createBindGroup(d: GPUBindGroupDescriptor): GPUBindGroup;
708
- createCommandEncoder(d?: GPUCommandEncoderDescriptor): GPUCommandEncoder;
709
- queue: GPUQueue; destroy(): void; lost?: Promise<GPUDeviceLostInfo>;
710
- }
711
- export interface GPUDeviceLostInfo { reason: 'unknown' | 'destroyed'; message?: string; }
712
- export interface GPUBuffer { mapAsync(mode: number, offset?: number, size?: number): Promise<void>; getMappedRange(offset?: number, size?: number): ArrayBuffer; unmap(): void; destroy(): void; size: number; usage: number; mapState: 'unmapped' | 'pending' | 'mapped'; }
713
- export interface GPUTexture { createView(d?: GPUTextureViewDescriptor): GPUTextureView; destroy(): void; width: number; height: number; depthOrArrayLayers: number; mipLevelCount: number; sampleCount: number; dimension: string; format: string; usage: number; }
714
- export interface GPUTextureView { }
715
- export interface GPUSampler { }
716
- export interface GPUShaderModule { getCompilationInfo(): Promise<{ messages: Array<{ message: string; type: 'error' | 'warning' | 'info'; lineNum?: number; linePos?: number; }> }>; }
717
- export interface GPURenderPipeline { getBindGroupLayout(index: number): GPUBindGroupLayout; }
718
- export interface GPUComputePipeline { getBindGroupLayout(index: number): GPUBindGroupLayout; }
719
- export interface GPUBindGroupLayout { }
720
- export interface GPUBindGroup { }
721
- export interface GPUQueue { submit(cbs: GPUCommandBuffer[]): void; writeBuffer(b: GPUBuffer, offset: number, data: ArrayBuffer | ArrayBufferView, dataOffset?: number, size?: number): void; onSubmittedWorkDone(): Promise<void>; }
722
- export interface GPUCommandBuffer { }
723
- export interface GPUCommandEncoder {
724
- beginRenderPass(d: GPURenderPassDescriptor): GPURenderPassEncoder;
725
- beginComputePass(d?: GPUComputePassDescriptor): GPUComputePassEncoder;
726
- copyBufferToBuffer(src: GPUBuffer, srcOffset: number, dst: GPUBuffer, dstOffset: number, size: number): void;
727
- finish(d?: { label?: string }): GPUCommandBuffer;
728
- }
729
- export interface GPURenderPassEncoder { setPipeline(p: GPURenderPipeline): void; setVertexBuffer(slot: number, b: GPUBuffer, offset?: number, size?: number): void; setIndexBuffer(b: GPUBuffer, fmt: string, offset?: number, size?: number): void; setBindGroup(index: number, bg: GPUBindGroup, offsets?: number[]): void; draw(vertexCount: number, instanceCount?: number, firstVertex?: number, firstInstance?: number): void; drawIndexed(indexCount: number, instanceCount?: number, firstIndex?: number, baseVertex?: number, firstInstance?: number): void; end(): void; }
730
- export interface GPUComputePassEncoder { setPipeline(p: GPUComputePipeline): void; setBindGroup(index: number, bg: GPUBindGroup, offsets?: number[]): void; dispatchWorkgroups(x: number, y?: number, z?: number): void; end(): void; }
731
- export interface GPUBufferDescriptor { size: number; usage: number; mappedAtCreation?: boolean; label?: string; }
732
- export interface GPUTextureDescriptor { size: { width: number; height?: number; depthOrArrayLayers?: number }; mipLevelCount?: number; sampleCount?: number; dimension?: string; format: string; usage: number; label?: string; }
733
- export interface GPUTextureViewDescriptor { format?: string; dimension?: string; baseMipLevel?: number; mipLevelCount?: number; baseArrayLayer?: number; arrayLayerCount?: number; label?: string; }
734
- export interface GPUSamplerDescriptor { label?: string; addressModeU?: string; addressModeV?: string; magFilter?: string; minFilter?: string; }
735
- export interface GPUShaderModuleDescriptor { code: string; label?: string; }
736
- export interface GPURenderPipelineDescriptor { layout?: GPUPipelineLayout; vertex: { module: GPUShaderModule; entryPoint: string; buffers?: unknown[] }; primitive?: { topology?: string; cullMode?: string }; fragment?: { module: GPUShaderModule; entryPoint: string; targets: unknown[] }; label?: string; }
737
- export interface GPUComputePipelineDescriptor { layout?: GPUPipelineLayout; compute: { module: GPUShaderModule; entryPoint: string }; label?: string; }
738
- export interface GPUBindGroupLayoutDescriptor { entries: unknown[]; label?: string; }
739
- export interface GPUBindGroupDescriptor { layout: GPUBindGroupLayout; entries: { binding: number; resource: unknown }[]; label?: string; }
740
- export interface GPUCommandEncoderDescriptor { label?: string; }
741
- export interface GPURenderPassDescriptor { colorAttachments: unknown[]; depthStencilAttachment?: unknown; label?: string; }
742
- export interface GPUComputePassDescriptor { label?: string; }
743
- export interface GPUPipelineLayout { }
744
- export interface GPURequestAdapterOptions { powerPreference?: 'low-power' | 'high-performance'; forceFallbackAdapter?: boolean; }
745
- export interface GPUDeviceDescriptor { requiredFeatures?: string[]; requiredLimits?: Record<string, number>; label?: string; }
746
- export const GPUBufferUsage = { MAP_READ: 0x0001, MAP_WRITE: 0x0002, COPY_SRC: 0x0004, COPY_DST: 0x0008, INDEX: 0x0010, VERTEX: 0x0020, UNIFORM: 0x0040, STORAGE: 0x0080, INDIRECT: 0x0100, QUERY_RESOLVE: 0x0200 } as const;
747
- export const GPUTextureUsage = { COPY_SRC: 0x0001, COPY_DST: 0x0002, TEXTURE_BINDING: 0x0004, STORAGE_BINDING: 0x0008, RENDER_ATTACHMENT: 0x0010 } as const;
748
- export const GPUMapMode = { READ: 0x0001, WRITE: 0x0002 } as const;
749
- export const GPUShaderStage = { VERTEX: 0x0001, FRAGMENT: 0x0002, COMPUTE: 0x0004 } as const;
750
- export const GPUColorWrite = { RED: 0x1, GREEN: 0x2, BLUE: 0x4, ALPHA: 0x8, ALL: 0xF } as const;
751
-
752
- export const gpu = {
753
- async requestAdapter(options?: GPURequestAdapterOptions): Promise<GPUAdapter | null> {
754
- const result = await invoke<any>('webgpu.requestAdapter', [options || {}]);
755
- if (!result) return null;
756
- return {
757
- features: new Set<string>(result.features || []),
758
- limits: result.limits || {},
759
- info: result.info,
760
- requestDevice: async (descriptor?: GPUDeviceDescriptor): Promise<GPUDevice> =>
761
- invoke<any>('webgpu.requestDevice', [result.id, descriptor || {}]),
762
- } as GPUAdapter;
763
- },
764
- getPreferredCanvasFormat(): string { return 'bgra8unorm'; },
765
- onAdapterLost(callback: (info: GPUAdapterInfo) => void): () => void {
766
- if (typeof window === 'undefined') return () => { };
767
- const h = (e: Event) => callback((e as CustomEvent<GPUAdapterInfo>).detail);
768
- window.addEventListener('plusui:webgpu.adapterLost', h);
769
- return () => window.removeEventListener('plusui:webgpu.adapterLost', h);
770
- },
771
- onDeviceLost(callback: (info: GPUDeviceLostInfo) => void): () => void {
772
- if (typeof window === 'undefined') return () => { };
773
- const h = (e: Event) => callback((e as CustomEvent<GPUDeviceLostInfo>).detail);
774
- window.addEventListener('plusui:webgpu.deviceLost', h);
775
- return () => window.removeEventListener('plusui:webgpu.deviceLost', h);
776
- },
777
- onError(callback: (error: string) => void): () => void {
778
- if (typeof window === 'undefined') return () => { };
779
- const h = (e: Event) => callback((e as CustomEvent<{ error: string }>).detail.error);
780
- window.addEventListener('plusui:webgpu.error', h);
781
- return () => window.removeEventListener('plusui:webgpu.error', h);
782
- },
783
- };
784
-
785
- // ─── Helpers ──────────────────────────────────────────────────────────────────
786
- export function formatFileSize(bytes: number): string {
787
- if (bytes === 0) return '0 Bytes';
788
- const k = 1024;
789
- const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
790
- const i = Math.floor(Math.log(bytes) / Math.log(k));
791
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
792
- }
793
-
794
- export function isImageFile(file: FileInfo): boolean { return file.type.startsWith('image/'); }
795
-
796
- // ─── Top-level on / emit ─────────────────────────────────────────────────────
797
- //
798
- // import plusui from 'plusui';
799
- //
800
- // plusui.emit('myEvent', { value: 42 }); // TS → C++
801
- // plusui.on('myEvent', (data) => { ... }); // C++ → TS
802
- //
803
- // plusui.win.minimize();
804
- // plusui.clipboard.on('changed', (data) => { ... });
805
- //
806
- export const on = connect.on.bind(connect) as typeof connect.on;
807
- export const emit = connect.emit.bind(connect) as typeof connect.emit;
808
-
809
- // ─── Default export — everything under one roof ───────────────────────────────
810
- const plusui = {
811
- feature: createFeatureConnect,
812
- connection: _client,
813
- win,
814
- browser,
815
- router,
816
- app,
817
- clipboard,
818
- fileDrop,
819
- keyboard,
820
- KeyCode,
821
- KeyMod,
822
- tray,
823
- display,
824
- menu,
825
- gpu,
826
- GPUBufferUsage,
827
- GPUTextureUsage,
828
- GPUMapMode,
829
- GPUShaderStage,
830
- GPUColorWrite,
831
- formatFileSize,
832
- isImageFile,
833
- on,
834
- emit,
835
- };
836
-
837
- export default plusui;