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.
|
|
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.
|
|
31
|
-
"plusui-native-connect": "^0.1.
|
|
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.
|
|
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(
|
|
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">{
|
|
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
|
|
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 = "/";
|
|
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
|
-
//
|
|
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
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
static 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
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
bindConnect(mainWindow,
|
|
199
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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++:
|
|
241
|
-
//
|
|
242
|
-
// TS:
|
|
243
|
-
//
|
|
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(
|
|
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
|
-
|
|
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">{
|
|
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
|
|
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 = "/";
|
|
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
|
-
//
|
|
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
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
static 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
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
bindConnect(mainWindow,
|
|
193
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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++:
|
|
235
|
-
//
|
|
236
|
-
// TS:
|
|
237
|
-
//
|
|
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;
|