plusui-native 0.2.66 → 0.2.68

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.
@@ -1,21 +1,3 @@
1
- // ============================================================
2
- // PlusUI — single import entrypoint
3
- //
4
- // import plusui from 'plusui';
5
- //
6
- // plusui.win.minimize();
7
- // plusui.fileDrop.onFilesDropped(...);
8
- // plusui.formatFileSize(bytes);
9
- //
10
- // For TypeScript types only:
11
- // import type { FileInfo } from 'plusui';
12
- //
13
- // After running `plusui connect`, custom channel objects are
14
- // exported from Connections/connections.gen.ts:
15
- // import { myChannel } from '../Connections/connections.gen';
16
- // ============================================================
17
-
18
- // ─── Bridge bootstrap ────────────────────────────────────────────────────────
19
1
 
20
2
  type InvokeFn = (method: string, args?: unknown[]) => Promise<unknown>;
21
3
  type PendingMap = Record<string, { resolve: (value: unknown) => void; reject: (reason?: unknown) => void }>;
@@ -256,32 +238,32 @@ export const connect = {
256
238
  feature: createFeatureConnect,
257
239
  };
258
240
 
259
- /** Advanced: raw connection client — call / stream / channel */
260
- export const connection = _client;
241
+ /** Advanced: raw connection client — used by generated code */
242
+ export { _client, _client as connection };
261
243
 
262
244
  // ─── win — window management ──────────────────────────────────────────────────
263
245
  const _winEvents = createFeatureConnect('window');
264
246
 
265
247
  export const win = {
266
- minimize: async () => invoke('window.minimize', []),
267
- maximize: async () => invoke('window.maximize', []),
268
- show: async () => invoke('window.show', []),
269
- hide: async () => invoke('window.hide', []),
270
- close: async () => invoke('window.close', []),
271
- center: async () => invoke('window.center', []),
272
- setTitle: async (title: string) => invoke('window.setTitle', [title]),
273
- setSize: async (w: number, h: number) => invoke('window.setSize', [w, h]),
274
- setMinSize: async (w: number, h: number) => invoke('window.setMinSize', [w, h]),
275
- setMaxSize: async (w: number, h: number) => invoke('window.setMaxSize', [w, h]),
276
- setPosition: async (x: number, y: number) => invoke('window.setPosition', [x, y]),
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]),
277
259
  setAlwaysOnTop: async (v: boolean) => invoke('window.setAlwaysOnTop', [v]),
278
- setFullscreen: async (v: boolean) => invoke('window.setFullscreen', [v]),
279
- setOpacity: async (v: number) => invoke('window.setOpacity', [v]),
280
- getSize: async (): Promise<WindowSize> => invoke('window.getSize', []) as Promise<WindowSize>,
281
- getPosition: async (): Promise<WindowPosition> => invoke('window.getPosition', []) as Promise<WindowPosition>,
282
- isMaximized: async (): Promise<boolean> => invoke('window.isMaximized', []) as Promise<boolean>,
283
- isVisible: async (): Promise<boolean> => invoke('window.isVisible', []) as Promise<boolean>,
284
- on: _winEvents.on.bind(_winEvents),
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),
285
267
  emit: _winEvents.emit.bind(_winEvents),
286
268
  };
287
269
 
@@ -289,20 +271,20 @@ export const win = {
289
271
  const _browserEvents = createFeatureConnect('browser');
290
272
 
291
273
  export const browser = {
292
- getUrl: async (): Promise<string> => invoke('browser.getUrl', []) as Promise<string>,
293
- navigate: async (url: string) => invoke('browser.navigate', [url]),
294
- goBack: async () => invoke('browser.goBack', []),
295
- goForward: async () => invoke('browser.goForward', []),
296
- reload: async () => invoke('browser.reload', []),
297
- canGoBack: async (): Promise<boolean> => invoke('browser.canGoBack', []) as Promise<boolean>,
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>,
298
280
  canGoForward: async (): Promise<boolean> => invoke('browser.canGoForward', []) as Promise<boolean>,
299
281
  onNavigate: (handler: (url: string) => void) => {
300
- if (typeof window === 'undefined') return () => {};
282
+ if (typeof window === 'undefined') return () => { };
301
283
  const h = (e: Event) => handler((e as CustomEvent<{ url?: string }>).detail?.url ?? '');
302
284
  window.addEventListener('plusui:navigate', h);
303
285
  return () => window.removeEventListener('plusui:navigate', h);
304
286
  },
305
- on: _browserEvents.on.bind(_browserEvents),
287
+ on: _browserEvents.on.bind(_browserEvents),
306
288
  emit: _browserEvents.emit.bind(_browserEvents),
307
289
  };
308
290
 
@@ -316,8 +298,8 @@ export const router = {
316
298
  const _appEvents = createFeatureConnect('app');
317
299
 
318
300
  export const app = {
319
- quit: async () => invoke('app.quit', []),
320
- on: _appEvents.on.bind(_appEvents),
301
+ quit: async () => invoke('app.quit', []),
302
+ on: _appEvents.on.bind(_appEvents),
321
303
  emit: _appEvents.emit.bind(_appEvents),
322
304
  };
323
305
 
@@ -325,11 +307,11 @@ export const app = {
325
307
  const _clipboardEvents = createFeatureConnect('clipboard');
326
308
 
327
309
  export const clipboard = {
328
- getText: async (): Promise<string> => invoke('clipboard.getText', []) as Promise<string>,
329
- setText: async (text: string) => invoke('clipboard.setText', [text]),
330
- clear: async () => invoke('clipboard.clear', []),
331
- hasText: async (): Promise<boolean> => invoke('clipboard.hasText', []) as Promise<boolean>,
332
- on: _clipboardEvents.on.bind(_clipboardEvents),
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),
333
315
  emit: _clipboardEvents.emit.bind(_clipboardEvents),
334
316
  };
335
317
 
@@ -338,64 +320,518 @@ export interface FileInfo { path: string; name: string; type: string; size: numb
338
320
 
339
321
  export const fileDrop = {
340
322
  setEnabled: async (enabled: boolean) => invoke('fileDrop.setEnabled', [enabled]),
341
- isEnabled: async (): Promise<boolean> => invoke('fileDrop.isEnabled', []) as Promise<boolean>,
323
+ isEnabled: async (): Promise<boolean> => invoke('fileDrop.isEnabled', []) as Promise<boolean>,
342
324
  onFilesDropped: (handler: (files: FileInfo[]) => void) => {
343
- if (typeof window === 'undefined') return () => {};
325
+ if (typeof window === 'undefined') return () => { };
344
326
  const h = (e: Event) => handler((e as CustomEvent<{ files?: FileInfo[] }>).detail?.files ?? []);
345
327
  window.addEventListener('plusui:fileDrop.filesDropped', h);
346
328
  return () => window.removeEventListener('plusui:fileDrop.filesDropped', h);
347
329
  },
348
330
  onDragEnter: (handler: () => void) => {
349
- if (typeof window === 'undefined') return () => {};
331
+ if (typeof window === 'undefined') return () => { };
350
332
  window.addEventListener('plusui:fileDrop.dragEnter', handler);
351
333
  return () => window.removeEventListener('plusui:fileDrop.dragEnter', handler);
352
334
  },
353
335
  onDragLeave: (handler: () => void) => {
354
- if (typeof window === 'undefined') return () => {};
355
- window.addEventListener('plusui:fileDrop.dragLeave', handler);
356
- return () => window.removeEventListener('plusui:fileDrop.dragLeave', handler);
357
- },
358
- };
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
+ };
359
363
 
360
- // ─── Helpers ──────────────────────────────────────────────────────────────────
361
- export function formatFileSize(bytes: number): string {
362
- if (bytes === 0) return '0 Bytes';
363
- const k = 1024;
364
- const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
365
- const i = Math.floor(Math.log(bytes) / Math.log(k));
366
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
367
- }
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
+ };
368
384
 
369
- export function isImageFile(file: FileInfo): boolean { return file.type.startsWith('image/'); }
385
+ // ─── router ───────────────────────────────────────────────────────────────────
386
+ export const router = {
387
+ setRoutes: (routes: RouteMap) => { _routes = routes; },
388
+ push: async (path: string) => invoke('browser.navigate', [_routes[path] ?? path]),
389
+ };
370
390
 
371
- // ─── Top-level on / emit ─────────────────────────────────────────────────────
372
- //
373
- // import plusui from 'plusui';
374
- //
375
- // plusui.emit('myEvent', { value: 42 }); // TS → C++
376
- // plusui.on('myEvent', (data) => { ... }); // C++ → TS
377
- //
378
- // plusui.win.minimize();
379
- // plusui.clipboard.on('changed', (data) => { ... });
380
- //
381
- export const on = connect.on.bind(connect) as typeof connect.on;
382
- export const emit = connect.emit.bind(connect) as typeof connect.emit;
391
+ // ─── app ──────────────────────────────────────────────────────────────────────
392
+ const _appEvents = createFeatureConnect('app');
383
393
 
384
- // ─── Default export everything under one roof ───────────────────────────────
385
- const plusui = {
386
- /** Create a named custom scope: const search = feature('search'); search.on/emit(...) */
387
- feature: createFeatureConnect,
388
- connection,
389
- win,
390
- browser,
391
- router,
392
- app,
393
- clipboard,
394
- fileDrop,
395
- formatFileSize,
396
- isImageFile,
397
- on,
398
- emit,
399
- };
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
+ };
400
836
 
401
- export default plusui;
837
+ export default plusui;