plusui-native 0.2.74 → 0.2.76
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.76",
|
|
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.75",
|
|
31
|
+
"plusui-native-connect": "^0.1.75"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"plusui-native-connect": "^0.1.
|
|
34
|
+
"plusui-native-connect": "^0.1.75"
|
|
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
|
|
|
@@ -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
|
|
|
@@ -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;
|