@n1h/sweetview 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/LICENSE-0BSD +12 -0
- package/README.md +268 -3
- package/docs/README.md +26 -0
- package/docs/app.md +523 -0
- package/docs/build.md +169 -0
- package/docs/ipc-ws.md +202 -0
- package/docs/ipc.md +323 -0
- package/docs/native.md +1066 -0
- package/docs/page.md +71 -0
- package/docs/settings-store.md +125 -0
- package/docs/webview-library.md +104 -0
- package/docs/window-chrome.md +221 -0
- package/docs/worker.md +72 -0
- package/file-imports.d.ts +19 -0
- package/package.json +66 -4
- package/src/app.ts +350 -0
- package/src/build.ts +118 -0
- package/src/html.ts +71 -0
- package/src/index.ts +8 -0
- package/src/ipc/bridge.ts +41 -0
- package/src/ipc/frame.ts +16 -0
- package/src/ipc/index.ts +16 -0
- package/src/ipc/memory.ts +33 -0
- package/src/ipc/service.ts +75 -0
- package/src/ipc-ws/client.ts +111 -0
- package/src/ipc-ws/index.ts +5 -0
- package/src/ipc-ws/server.ts +133 -0
- package/src/make-app.ts +93 -0
- package/src/messages.ts +16 -0
- package/src/native/app-icon.ts +23 -0
- package/src/native/app-menu.ts +18 -0
- package/src/native/darwin/app-icon.ts +10 -0
- package/src/native/darwin/app-menu.ts +88 -0
- package/src/native/darwin/folder-picker.ts +29 -0
- package/src/native/darwin/image.ts +47 -0
- package/src/native/darwin/menu.ts +89 -0
- package/src/native/darwin/notifier.ts +95 -0
- package/src/native/darwin/objc.ts +246 -0
- package/src/native/darwin/overlay.ts +352 -0
- package/src/native/darwin/tray.ts +129 -0
- package/src/native/darwin/window.ts +64 -0
- package/src/native/folder-picker.ts +15 -0
- package/src/native/hit-regions.ts +162 -0
- package/src/native/icon.ts +530 -0
- package/src/native/index.ts +38 -0
- package/src/native/menu.ts +43 -0
- package/src/native/notifications.ts +40 -0
- package/src/native/overlay.ts +182 -0
- package/src/native/platform.ts +25 -0
- package/src/native/tray.ts +41 -0
- package/src/native/win32/api.ts +234 -0
- package/src/native/win32/app-icon.ts +5 -0
- package/src/native/win32/app-menu.ts +4 -0
- package/src/native/win32/folder-picker.ts +121 -0
- package/src/native/win32/icon.ts +85 -0
- package/src/native/win32/menu.ts +107 -0
- package/src/native/win32/message-window.ts +72 -0
- package/src/native/win32/notifier.ts +28 -0
- package/src/native/win32/overlay.ts +499 -0
- package/src/native/win32/tray.ts +228 -0
- package/src/native/win32/window.ts +77 -0
- package/src/native/window.ts +40 -0
- package/src/page.ts +48 -0
- package/src/ring.ts +110 -0
- package/src/settings-store.ts +101 -0
- package/src/webview-lib.ts +60 -0
- package/src/window-chrome.ts +118 -0
- package/src/worker.ts +72 -0
package/docs/native.md
ADDED
|
@@ -0,0 +1,1066 @@
|
|
|
1
|
+
# @n1h/sweetview/native
|
|
2
|
+
|
|
3
|
+
Native desktop pieces for a Bun app, called over `bun:ffi`: a tray icon, notifications, control of the webview's window, overlay windows with click-through hit regions, a folder dialog, the macOS app menu and Dock icon, menus as data, and icon encoders. Every function that touches the OS is implemented for Windows and macOS and runs on the UI thread, the thread that owns the window and spins the webview's run loop. The menu, icon and hit-region helpers are plain TypeScript; all but `createHitRelay` run on any thread. Install and the overview are in [../README.md](../README.md).
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { createTray, createNotifier, nativeWindow, createOverlayWindow, makeOverlay, convexHull, createHitRelay, flattenRegions, hitRegions, packRegions, unpackRegions, pickFolder, installAppMenu, setAppIcon, resolveMenu, decodePng, dotIcns, dotIcon, dotPng, encodeIcns, encodeIco, encodePng, icnsFromPng, icnsFromRgba, icoFromPng, icoFromRgba, renderDot, trimTransparent, type Tray, type TrayIcon, type TrayOptions, type NotificationOptions, type Notifier, type NotifierOptions, type NativeWindow, type WindowHandle, type Overlay, type OverlayOptions, type OverlayWindowOptions, type HitRefiner, type Point, type Rect, type ResizeEdge, type HitPolygon, type HitProbe, type HitRelay, type AppMenuOptions, type MenuItem, type MenuSource, type MenuTemplate, type DecodedPng, type DotIconOptions, type Icon } from "@n1h/sweetview/native";
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Every function that touches the OS picks its Windows or macOS implementation on its first call. On any other OS that call throws `sweetnative: <platform> is not supported (win32 and darwin are)`. Importing the module does not throw on any OS.
|
|
10
|
+
|
|
11
|
+
## Tray
|
|
12
|
+
|
|
13
|
+
### createTray
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
function createTray(options: TrayOptions): Tray
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Adds an icon to the Windows notification area, or a status item to the macOS menu bar, and returns the handle that changes or removes it.
|
|
20
|
+
|
|
21
|
+
| Field | Type | Default | Meaning |
|
|
22
|
+
|---|---|---|---|
|
|
23
|
+
| `options` | [`TrayOptions`](#trayoptions) | required | Tooltip, icon, menu and click handlers. |
|
|
24
|
+
|
|
25
|
+
Returns: a [`Tray`](#tray-1).
|
|
26
|
+
|
|
27
|
+
Throws:
|
|
28
|
+
|
|
29
|
+
- Windows, when `options.icon.windows.ico` is not a readable .ico file.
|
|
30
|
+
- Windows, when the hidden window that receives the tray's messages cannot be created.
|
|
31
|
+
- MacOS, when the system status bar returns no item.
|
|
32
|
+
|
|
33
|
+
Platforms: Windows: implemented; the menu opens on right click; the icon is added again when Explorer restarts; an icon the shell refuses at login is logged and added once the taskbar appears. macOS: implemented; with a menu set, every click opens the menu and the click handlers never fire. Linux: throws.
|
|
34
|
+
|
|
35
|
+
Thread: UI thread. The handlers and the menu's `click` functions run on the UI thread from inside the OS message loop, while the webview's run loop is on the stack; they must return quickly and cannot await anything that needs Bun's event loop. The single-click delay is an OS timer, so it fires only while the webview's run loop pumps messages.
|
|
36
|
+
|
|
37
|
+
### Tray
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
interface Tray {
|
|
41
|
+
setTooltip(text: string): void;
|
|
42
|
+
setIcon(icon: TrayIcon): void;
|
|
43
|
+
setMenu(menu: MenuSource | undefined): void;
|
|
44
|
+
dispose(): void;
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
| Member | Meaning |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `setTooltip(text)` | Replaces the hover text. Windows keeps the first 127 UTF-16 code units. No-op after `dispose`. |
|
|
51
|
+
| `setIcon(icon)` | Replaces the icon. No-op after `dispose`. Windows: throws when `icon.windows.ico` is not a readable .ico file. |
|
|
52
|
+
| `setMenu(menu)` | Replaces the menu source; `undefined` removes the menu. Takes effect at the next click. |
|
|
53
|
+
| `dispose()` | Removes the icon. Safe to call twice. |
|
|
54
|
+
|
|
55
|
+
### TrayIcon
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
type TrayIcon = Icon;
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The tray's icon, an [`Icon`](#icon). Each platform takes the image it needs from it.
|
|
62
|
+
|
|
63
|
+
| Use | Windows | macOS |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| Tray or menu bar | the .ico entry at the system small-icon size, 16 px at 100 % scale | the image at 18x18 points; a PNG wants 36 px for Retina; a template image unless `template: false` |
|
|
66
|
+
| Notification | the .ico entry at the system large-icon size, 32 px at 100 % scale | not used |
|
|
67
|
+
| No usable image | tray: the stock application icon; notification: the system information icon | a dot character as the item's title |
|
|
68
|
+
|
|
69
|
+
### TrayOptions
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
type TrayOptions = {
|
|
73
|
+
tooltip?: string;
|
|
74
|
+
icon?: TrayIcon;
|
|
75
|
+
menu?: MenuSource;
|
|
76
|
+
onClick?: () => void;
|
|
77
|
+
onDoubleClick?: () => void;
|
|
78
|
+
};
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
| Field | Type | Default | Meaning |
|
|
82
|
+
|---|---|---|---|
|
|
83
|
+
| `tooltip` | `string` | `""` | Hover text. Windows keeps the first 127 UTF-16 code units. |
|
|
84
|
+
| `icon` | [`TrayIcon`](#trayicon) | stock image | The tray's icon. |
|
|
85
|
+
| `menu` | [`MenuSource`](#menusource) | none | Windows: opens on right click. macOS: opens on any click, as menu bar items do. A source that resolves to an empty template opens nothing. |
|
|
86
|
+
| `onClick` | `() => void` | none | Left single click. Fires one system double-click interval after the click, and only when no second click follows. macOS: only when no menu is set. |
|
|
87
|
+
| `onDoubleClick` | `() => void` | none | Left double click. A double click does not also fire `onClick`. macOS: only when no menu is set. |
|
|
88
|
+
|
|
89
|
+
## Notifications
|
|
90
|
+
|
|
91
|
+
### createNotifier
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
function createNotifier(options: NotifierOptions = {}): Notifier
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Creates the object that shows system notifications and reports clicks on them.
|
|
98
|
+
|
|
99
|
+
| Field | Type | Default | Meaning |
|
|
100
|
+
|---|---|---|---|
|
|
101
|
+
| `options` | [`NotifierOptions`](#notifieroptions) | `{}` | The click handler. |
|
|
102
|
+
|
|
103
|
+
Returns: a [`Notifier`](#notifier).
|
|
104
|
+
|
|
105
|
+
Throws: Windows, when no tray exists: `sweetnative: on Windows, create the tray before the notifier (toasts hang off the tray icon)`.
|
|
106
|
+
|
|
107
|
+
Platforms: Windows: implemented as the balloon of the most recently created tray that is still alive when `createNotifier` is called; Windows 10 and 11 show it as a toast with the tray's icon, or with the system information icon when the tray has the stock icon; it respects the system's quiet hours; a tray has one click handler, so a later notifier replaces the earlier one's, and disposing the earlier one leaves the later one's handler in place. macOS: implemented with the user notification center; it needs a bundle identifier, so it shows notifications from an .app and nothing from a bare executable; the first notification asks the user for permission; banners show while the app is frontmost too. Linux: throws.
|
|
108
|
+
|
|
109
|
+
Thread: UI thread. `onClick` runs on the UI thread.
|
|
110
|
+
|
|
111
|
+
### NotificationOptions
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
type NotificationOptions = {
|
|
115
|
+
title: string;
|
|
116
|
+
body?: string;
|
|
117
|
+
silent?: boolean;
|
|
118
|
+
};
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
| Field | Type | Default | Meaning |
|
|
122
|
+
|---|---|---|---|
|
|
123
|
+
| `title` | `string` | required | The heading. Windows keeps the first 63 UTF-16 code units. |
|
|
124
|
+
| `body` | `string` | `""` | The text. Windows keeps the first 255 UTF-16 code units. |
|
|
125
|
+
| `silent` | `boolean` | `false` | No sound. |
|
|
126
|
+
|
|
127
|
+
### Notifier
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
interface Notifier {
|
|
131
|
+
notify(notification: NotificationOptions): boolean;
|
|
132
|
+
dispose(): void;
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
| Member | Meaning |
|
|
137
|
+
|---|---|
|
|
138
|
+
| `notify(notification)` | Shows a notification. Returns false when nothing is shown: after `dispose`; on Windows once the tray is disposed or when the shell refuses; on macOS without a bundle identifier. macOS returns true without waiting for the permission answer, so a denied permission still returns true. |
|
|
139
|
+
| `dispose()` | Stops showing notifications and stops calling `onClick`. Safe to call twice. |
|
|
140
|
+
|
|
141
|
+
### NotifierOptions
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
type NotifierOptions = {
|
|
145
|
+
onClick?: () => void;
|
|
146
|
+
};
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
| Field | Type | Default | Meaning |
|
|
150
|
+
|---|---|---|---|
|
|
151
|
+
| `onClick` | `() => void` | none | The user clicked a notification. |
|
|
152
|
+
|
|
153
|
+
## Windows and overlays
|
|
154
|
+
|
|
155
|
+
Positions and sizes are in window units: physical pixels on Windows when the process is per-monitor DPI aware, which `createOverlayWindow` sets, and points on macOS. The origin is the top-left corner of the primary screen on both. `Overlay.scale()` converts to CSS pixels of the page.
|
|
156
|
+
|
|
157
|
+
### nativeWindow
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
function nativeWindow(handle: WindowHandle): NativeWindow
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Wraps a window that something else created, typically the webview's, to show it, hide it, set its icon and take over its close button. Creating the wrapper changes nothing about the window.
|
|
164
|
+
|
|
165
|
+
| Field | Type | Default | Meaning |
|
|
166
|
+
|---|---|---|---|
|
|
167
|
+
| `handle` | [`WindowHandle`](#windowhandle) | required | The HWND on Windows, the NSWindow pointer on macOS. |
|
|
168
|
+
|
|
169
|
+
Returns: a [`NativeWindow`](#nativewindow-1).
|
|
170
|
+
|
|
171
|
+
Throws: when `handle` is `null` or 0.
|
|
172
|
+
|
|
173
|
+
Platforms: Windows: implemented. macOS: implemented. Linux: throws.
|
|
174
|
+
|
|
175
|
+
Thread: UI thread.
|
|
176
|
+
|
|
177
|
+
### NativeWindow
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
interface NativeWindow {
|
|
181
|
+
isVisible(): boolean;
|
|
182
|
+
show(): void;
|
|
183
|
+
hide(): void;
|
|
184
|
+
toggle(): void;
|
|
185
|
+
setIcon(icon: Icon | undefined): void;
|
|
186
|
+
onCloseRequest(handler: () => void): () => void;
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
| Member | Meaning |
|
|
191
|
+
|---|---|
|
|
192
|
+
| `isVisible()` | Whether the window is shown. |
|
|
193
|
+
| `show()` | Shows the window, brings it to the front and asks for the foreground. Windows: restores it first when it is minimised. macOS: also activates the app. |
|
|
194
|
+
| `hide()` | Hides the window. |
|
|
195
|
+
| `toggle()` | `hide()` when visible, `show()` otherwise. |
|
|
196
|
+
| `setIcon(icon)` | Windows: the title bar, taskbar and Alt+Tab icon, from `icon.windows.ico`; `undefined` restores the stock icon; throws when the bytes are not a readable .ico file. macOS: no-op, because a window has no icon of its own there; [`setAppIcon`](#setappicon) sets the Dock's. |
|
|
197
|
+
| `onCloseRequest(handler)` | Calls `handler` instead of closing the window, which would end the app; `handler` is typically `hide`. Returns a function that restores the default close. A second call for the same window replaces the handler, and the restore function from the earlier call then does nothing. Quitting through a menu still works. An exception from `handler` is logged and swallowed. Windows: every close request to the window is intercepted, Alt+F4 included; throws when the window procedure cannot be hooked. macOS: only the close button is intercepted; throws when the window has no close button. |
|
|
198
|
+
|
|
199
|
+
### WindowHandle
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
type WindowHandle = Pointer | bigint | number | null;
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
A native window handle as the webview hands it out: the HWND on Windows, the NSWindow pointer on macOS. `Pointer` is the `bun:ffi` type. Functions that need a window throw on `null` or 0; [`pickFolder`](#pickfolder) accepts `null`.
|
|
206
|
+
|
|
207
|
+
### createOverlayWindow
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
function createOverlayWindow(options: OverlayWindowOptions = {}): Overlay
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Creates a hidden window, an overlay or with `frame` a normal window, to host the webview: pass `overlay.handle` as the `window` option of [`createWebviewApp`](app.md#createwebviewapp) from the root entry. Use it rather than [`makeOverlay`](#makeoverlay) for a see-through page on Windows, because WebView2 honours transparency only when the host window has it before the browser is created. Call `show()` once the page is set.
|
|
214
|
+
|
|
215
|
+
| Field | Type | Default | Meaning |
|
|
216
|
+
|---|---|---|---|
|
|
217
|
+
| `options` | [`OverlayWindowOptions`](#overlaywindowoptions) | `{}` | The window's look and behaviour, including every [`OverlayOptions`](#overlayoptions) field. |
|
|
218
|
+
|
|
219
|
+
What the call sets up:
|
|
220
|
+
|
|
221
|
+
| Step | Windows | macOS |
|
|
222
|
+
|---|---|---|
|
|
223
|
+
| Thread | initialises COM on the calling thread as a single-threaded apartment, which WebView2 needs | creates the shared application object |
|
|
224
|
+
| Process | sets per-monitor DPI awareness; a process that already set it keeps its setting | nothing |
|
|
225
|
+
| First window | creates one hidden window first, once per process, because the first top-level window a process creates cannot be made topmost | nothing |
|
|
226
|
+
| The window | created hidden; a bare popup, or with `frame` an overlapped window with title bar and borders | created hidden; borderless, or with `frame` titled, closable, miniaturizable and resizable; a frameless window with `noActivate: false` gets a class that can become key and take keyboard input |
|
|
227
|
+
| Close button | hides the window | hides the window |
|
|
228
|
+
| Window destroyed | ends the webview's run loop | not handled here |
|
|
229
|
+
| Window resized | resizes the webview's child window to fill the client area | not handled here |
|
|
230
|
+
| Default bounds | 320x240, centred on the primary work area | 320x240, centred on the primary work area |
|
|
231
|
+
|
|
232
|
+
Returns: an [`Overlay`](#overlay) for the new window.
|
|
233
|
+
|
|
234
|
+
Throws:
|
|
235
|
+
|
|
236
|
+
- Windows, when the calling thread already initialised COM as multi-threaded: `sweetnative: this thread already initialised COM as multi-threaded; WebView2 needs an STA`.
|
|
237
|
+
- Windows, when the window class cannot be registered or the window cannot be created.
|
|
238
|
+
- Windows, when `colorKey` is not six hex digits with an optional leading `#`.
|
|
239
|
+
- Windows, when the layered-window attributes or the transparent composition cannot be set.
|
|
240
|
+
- Windows, when `icon.windows.ico` is not a readable .ico file.
|
|
241
|
+
- MacOS, when the window cannot be created.
|
|
242
|
+
|
|
243
|
+
Platforms: Windows: implemented. macOS: implemented. Linux: throws.
|
|
244
|
+
|
|
245
|
+
Thread: UI thread, the one that later runs the webview. The window procedure and the overlay's timers run only while the webview's run loop pumps messages on that thread.
|
|
246
|
+
|
|
247
|
+
### makeOverlay
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
function makeOverlay(handle: WindowHandle, options: OverlayOptions = {}): Overlay
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Restyles an existing window, typically the webview's own, into an overlay in place, and returns the [`Overlay`](#overlay) that controls it.
|
|
254
|
+
|
|
255
|
+
| Field | Type | Default | Meaning |
|
|
256
|
+
|---|---|---|---|
|
|
257
|
+
| `handle` | [`WindowHandle`](#windowhandle) | required | The window to restyle. |
|
|
258
|
+
| `options` | [`OverlayOptions`](#overlayoptions) | `{}` | The overlay's look and behaviour. |
|
|
259
|
+
|
|
260
|
+
What it does. Windows: replaces the frame with a bare popup style, keeping visibility; sets the extended styles and layered attributes the options ask for; turns on alpha composition for `transparent`; sets the z-order. macOS: makes the window borderless without a shadow; for `transparent` makes it non-opaque with a clear background; sets its alpha and window level; puts it on every Space, keeps it out of window cycling and lets it show over full-screen apps; for `hideFromTaskbar` switches the app to the accessory activation policy.
|
|
261
|
+
|
|
262
|
+
What it does not do. It creates no window. It does not initialise COM, set DPI awareness or create the hidden first window, so on Windows `topmost` has no effect when the restyled window is the first top-level window of the process. It installs no window procedure, so the webview's own close, destroy and resize handling stays as it was. It does not make WebView2 transparent: a browser created before the restyle keeps painting its background. On macOS it gives the window no class that can become key, so `noActivate: false` only makes `show()` activate the app.
|
|
263
|
+
|
|
264
|
+
Returns: an [`Overlay`](#overlay) for the window.
|
|
265
|
+
|
|
266
|
+
Throws:
|
|
267
|
+
|
|
268
|
+
- When `handle` is `null` or 0.
|
|
269
|
+
- Windows, when `colorKey` is not six hex digits with an optional leading `#`.
|
|
270
|
+
- Windows, when the layered-window attributes or the transparent composition cannot be set.
|
|
271
|
+
|
|
272
|
+
Platforms: Windows: implemented. macOS: implemented. Linux: throws.
|
|
273
|
+
|
|
274
|
+
Thread: UI thread.
|
|
275
|
+
|
|
276
|
+
### Overlay
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
interface Overlay {
|
|
280
|
+
readonly handle: WindowHandle;
|
|
281
|
+
show(): void;
|
|
282
|
+
hide(): void;
|
|
283
|
+
setIcon(icon: Icon | undefined): void;
|
|
284
|
+
bounds(): Rect;
|
|
285
|
+
moveTo(x: number, y: number): void;
|
|
286
|
+
resize(width: number, height: number): void;
|
|
287
|
+
setBounds(rect: Rect): void;
|
|
288
|
+
setClickThrough(on: boolean): void;
|
|
289
|
+
setHitRegions(polygons: HitPolygon[] | null, refine?: HitRefiner): void;
|
|
290
|
+
setTopmost(on: boolean): void;
|
|
291
|
+
startDrag(): void;
|
|
292
|
+
startResize(edge: ResizeEdge): void;
|
|
293
|
+
minimize(): void;
|
|
294
|
+
toggleMaximize(): void;
|
|
295
|
+
maximized(): boolean;
|
|
296
|
+
cursor(): Point;
|
|
297
|
+
screen(): Rect;
|
|
298
|
+
screenAt(point: Point): Rect;
|
|
299
|
+
workArea(): Rect;
|
|
300
|
+
virtualScreen(): Rect;
|
|
301
|
+
scale(): number;
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Control of an overlay window. All members run on the UI thread.
|
|
306
|
+
|
|
307
|
+
| Member | Meaning |
|
|
308
|
+
|---|---|
|
|
309
|
+
| `handle` | The HWND or NSWindow pointer as a `bigint`, for the `window` option of `createWebviewApp`. |
|
|
310
|
+
| `show()` | Shows the window. With `noActivate` it does not take focus. Windows: first sizes the webview's child window to the client area; without `noActivate` and without `topmost` it also raises the window and asks for the foreground, which a process the user has not clicked yet would not otherwise get. macOS: with `frame` or `noActivate: false` it makes the window key and activates the app; otherwise it orders the window to the front without activating anything. |
|
|
311
|
+
| `hide()` | Hides the window. |
|
|
312
|
+
| `setIcon(icon)` | Windows: the title bar, taskbar and Alt+Tab icon from `icon.windows.ico`; `undefined` restores the stock one; only a framed window shows it; throws when the bytes are not a readable .ico file. macOS: no-op; [`setAppIcon`](#setappicon) sets the Dock's. |
|
|
313
|
+
| `bounds()` | The window's outer position and size, frame included. |
|
|
314
|
+
| `moveTo(x, y)` | Moves the top-left corner. Windows: rounds to whole pixels. While the window is topmost, every move asserts the topmost order again, so the taskbar does not win a tie. |
|
|
315
|
+
| `resize(width, height)` | Resizes and keeps the top-left corner in place. |
|
|
316
|
+
| `setBounds(rect)` | Moves and resizes in one call. |
|
|
317
|
+
| `setClickThrough(on)` | Whole window: `true` lets every click fall through to what is underneath; `false` takes every click. Clears any hit regions and stops their polling. |
|
|
318
|
+
| `setHitRegions(polygons, refine)` | Makes only the given polygons take clicks; [`setHitRegions`](#sethitregions) below has the details. |
|
|
319
|
+
| `setTopmost(on)` | Above every normal window, or not. Windows: the topmost z-order, remembered for later moves. macOS: the status window level, above the Dock, or the normal level. |
|
|
320
|
+
| `startDrag()` | Hands the mouse press in progress to the OS as a window move; [`startDrag and startResize`](#startdrag-and-startresize) below has the details. |
|
|
321
|
+
| `startResize(edge)` | Resizes from an edge or corner while the button is held; [`startDrag and startResize`](#startdrag-and-startresize) below has the details. |
|
|
322
|
+
| `minimize()` | Minimises the window. |
|
|
323
|
+
| `toggleMaximize()` | Windows: switches between the primary monitor's work area and the bounds before maximising; the popup's own maximise would cover the taskbar. macOS: zooms. |
|
|
324
|
+
| `maximized()` | Windows: true while `toggleMaximize` has the window maximised; the OS's own maximised state is not read. macOS: whether the window is zoomed. |
|
|
325
|
+
| `cursor()` | The cursor's position on the screen. macOS: `{ x: 0, y: 0 }` when the position cannot be read. |
|
|
326
|
+
| `screen()` | The whole primary monitor. The overlay can draw over the taskbar or Dock there. |
|
|
327
|
+
| `screenAt(point)` | The whole monitor that contains `point`, or the nearest one when no monitor does. A window dropped on another monitor finds that monitor's bounds this way. |
|
|
328
|
+
| `workArea()` | The primary monitor minus the taskbar or Dock. |
|
|
329
|
+
| `virtualScreen()` | The rectangle around every monitor together. |
|
|
330
|
+
| `scale()` | Window units per CSS pixel of the page: the window's DPI divided by 96 on Windows, 1 when the DPI is unknown; always 1 on macOS. |
|
|
331
|
+
|
|
332
|
+
#### setHitRegions
|
|
333
|
+
|
|
334
|
+
`setHitRegions(polygons, refine?)` makes only the given polygons take clicks. A click anywhere else in the window falls through to whatever is underneath. The polygons are in window-local units, measured from the window's outer top-left corner. `null` makes the whole window take clicks again and stops the polling. The call repaints nothing, so a page can resend its polygons tens of times a second as its drawing moves. The page computes the polygons from what it draws, and this side tests the cursor itself, so the page is never in the click path.
|
|
335
|
+
|
|
336
|
+
| Aspect | Windows | macOS |
|
|
337
|
+
|---|---|---|
|
|
338
|
+
| Poll | a timer per window every 10 ms, plus one test on every `setHitRegions` call | one timer for every overlay every 10 ms, plus one test on every call; once started it runs for the life of the process |
|
|
339
|
+
| Where the timer runs | inside the webview's message loop on the UI thread | on the main run loop |
|
|
340
|
+
| Pass-through state | the transparent extended style, with the layered style unless `colorKey` or `opacity` already made the window layered | the window ignores mouse events |
|
|
341
|
+
| Buttons that freeze the state | left, right, middle | any |
|
|
342
|
+
|
|
343
|
+
The rules on each test:
|
|
344
|
+
|
|
345
|
+
- Polygons with fewer than three points are dropped, and coordinates are rounded to whole units, as [`packRegions`](#packregions) does.
|
|
346
|
+
- While a mouse button is down nothing changes, so a drag that leaves the polygons keeps going.
|
|
347
|
+
- With the cursor outside the window, or outside every polygon, the window passes clicks through. Entering the window onto empty space is right from the first pixel. Entering onto a polygon takes one tick.
|
|
348
|
+
- With the cursor inside a polygon and no `refine`, the window takes clicks.
|
|
349
|
+
- With `refine`, the polygons are the coarse answer, for example a bounding-box hull that includes the gap between a figure's ears. On entering a polygon the window takes clicks at once. `refine` is then called with the cursor's window-local point whenever that point differs from the last one asked, and again after every `setHitRegions` call under a still cursor. The reply sets the state: `solid` true takes clicks, false passes them through. Only the reply to the latest question counts. A reply is dropped when the cursor has since moved to another point or left the polygons, when `setHitRegions` has been called again, or when `setHitRegions(null)` or `setClickThrough` has cleared the regions. Outside every polygon `refine` is not called.
|
|
350
|
+
- A refiner that never replies leaves the window in its last state while the cursor stays inside the polygons. The next entry into a polygon takes clicks, so a stalled page degrades to polygon precision.
|
|
351
|
+
- [`setClickThrough`](#overlay) clears the regions.
|
|
352
|
+
|
|
353
|
+
[`createHitRelay`](#createhitrelay) does the UI-thread bookkeeping for a page that answers the questions.
|
|
354
|
+
|
|
355
|
+
#### startDrag and startResize
|
|
356
|
+
|
|
357
|
+
`startDrag()` hands the mouse press in progress to the OS as a window move, as a title bar would. Call it from a UI-thread method that the page calls on pointer-down in its own title bar. The window then follows the cursor until the button is released. A page draws its own chrome in a frameless window this way. Windows: the OS's own move loop runs, snapping included, and the call returns when the move ends. macOS: the window drag starts from the application's current event, which must be that mouse-down; with no current event the call does nothing.
|
|
358
|
+
|
|
359
|
+
`startResize(edge)` resizes the window from `edge` for the page's own resize handles. [window-chrome.md](window-chrome.md) builds a page-drawn title bar and resize handles on these two calls.
|
|
360
|
+
|
|
361
|
+
| Aspect | Windows | macOS |
|
|
362
|
+
|---|---|---|
|
|
363
|
+
| Mechanism | this module's own timer moves the edge or corner with the cursor every 10 ms; the OS's size loop needs a thick frame, which brings a visible border | no-op |
|
|
364
|
+
| Minimum size | 160x120 window units | not applicable |
|
|
365
|
+
| Ends | when the left button is released | not applicable |
|
|
366
|
+
| Throws | when the timer cannot be created | never |
|
|
367
|
+
|
|
368
|
+
### OverlayOptions
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
type OverlayOptions = {
|
|
372
|
+
transparent?: boolean;
|
|
373
|
+
colorKey?: string;
|
|
374
|
+
opacity?: number;
|
|
375
|
+
topmost?: boolean;
|
|
376
|
+
hideFromTaskbar?: boolean;
|
|
377
|
+
noActivate?: boolean;
|
|
378
|
+
};
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
| Field | Type | Default | Meaning |
|
|
382
|
+
|---|---|---|---|
|
|
383
|
+
| `transparent` | `boolean` | `false` | Honours the page's alpha: wherever the page is transparent, the desktop shows through, anti-aliased edges included. The browser must paint a transparent background too, which is `createWebviewApp`'s `transparent: true`. Clicks on transparent pixels still land on the window; use `setClickThrough` or `setHitRegions` for pass-through. Windows: the desktop compositor blends the client area by its alpha. macOS: a non-opaque window with a clear background. |
|
|
384
|
+
| `colorKey` | `string` | none | `#rrggbb`; pixels of exactly this colour are fully transparent and click-through. One bit of transparency; not needed with `transparent`. Windows only: throws when it is not six hex digits. macOS: a warning is logged and the field is ignored. |
|
|
385
|
+
| `opacity` | `number` | none | Whole-window opacity, clamped to 0 to 1. Combines with the colour key. |
|
|
386
|
+
| `topmost` | `boolean` | `true` | Above every normal window. macOS: the status window level, above the Dock. |
|
|
387
|
+
| `hideFromTaskbar` | `boolean` | `true` | Windows: no taskbar button and no Alt+Tab entry. macOS: switches the whole app to the accessory activation policy, so the app has no Dock icon and no Cmd+Tab entry; a menu bar status item still works; [`installAppMenu`](#installappmenu) switches it back. |
|
|
388
|
+
| `noActivate` | `boolean` | `true` | Clicking the window never takes focus from the app the user is in. `false` makes a frameless window that takes focus and keyboard input, for a settings window that draws its own chrome. macOS: a frameless window takes keyboard input only when `createOverlayWindow` made it. |
|
|
389
|
+
|
|
390
|
+
### OverlayWindowOptions
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
type OverlayWindowOptions = OverlayOptions & {
|
|
394
|
+
title?: string;
|
|
395
|
+
bounds?: Rect;
|
|
396
|
+
frame?: boolean;
|
|
397
|
+
dark?: boolean;
|
|
398
|
+
icon?: Icon;
|
|
399
|
+
};
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
Options of [`createOverlayWindow`](#createoverlaywindow): every [`OverlayOptions`](#overlayoptions) field plus these.
|
|
403
|
+
|
|
404
|
+
| Field | Type | Default | Meaning |
|
|
405
|
+
|---|---|---|---|
|
|
406
|
+
| `title` | `string` | `""` | The window's title, shown in the title bar of a framed window. |
|
|
407
|
+
| `bounds` | [`Rect`](#rect) | 320x240, centred on the primary work area | Initial bounds, window units. Windows: the outer bounds, frame included. macOS: the content area; with `frame` the title bar is added above it. |
|
|
408
|
+
| `frame` | `boolean` | `false` | A normal window instead of an overlay: title bar and close button, a taskbar or Dock presence, activates when clicked, not above other windows, opaque. Still created hidden, so `show()` is the first time it appears, and its close button hides it rather than ending the app. For a settings window next to an overlay, hosted by the same process and run loop. |
|
|
409
|
+
| `dark` | `boolean` | `false` | Framed windows: a dark title bar and frame for a dark page. Windows: immersive dark mode, ignored where the OS does not support it. macOS: the dark appearance. |
|
|
410
|
+
| `icon` | [`Icon`](#icon) | stock icon | Windows: the title bar, taskbar and Alt+Tab icon, as `Overlay.setIcon`. macOS: ignored. |
|
|
411
|
+
|
|
412
|
+
With `frame: true` the overlay options behave differently per platform:
|
|
413
|
+
|
|
414
|
+
| `OverlayOptions` field | Windows with `frame` | macOS with `frame` |
|
|
415
|
+
|---|---|---|
|
|
416
|
+
| `topmost`, `hideFromTaskbar`, `noActivate` | default to `false`; values given explicitly apply | ignored |
|
|
417
|
+
| `transparent`, `colorKey`, `opacity` | apply | ignored |
|
|
418
|
+
|
|
419
|
+
### HitRefiner
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
type HitRefiner = (local: Point, answer: (solid: boolean) => void) => void;
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
Asked while the cursor is inside a hit polygon, with the cursor in window-local units. Whoever knows the exact shape, typically the page with a triangle pick, calls `answer` with whether that point is really solid. `answer` may be called later from another UI-thread callback. Until it is, the window keeps its current state; right after the cursor enters a polygon that state is to take clicks. A call to `answer` is ignored once a later question was asked, the cursor left the polygons, or the regions were replaced or cleared.
|
|
426
|
+
|
|
427
|
+
Thread: called on the UI thread from the overlay's timer or from `setHitRegions`; `answer` must be called on the UI thread.
|
|
428
|
+
|
|
429
|
+
### Point
|
|
430
|
+
|
|
431
|
+
```ts
|
|
432
|
+
type Point = { x: number; y: number };
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
| Field | Type | Default | Meaning |
|
|
436
|
+
|---|---|---|---|
|
|
437
|
+
| `x` | `number` | required | Horizontal position, growing to the right. |
|
|
438
|
+
| `y` | `number` | required | Vertical position, growing downward. |
|
|
439
|
+
|
|
440
|
+
### Rect
|
|
441
|
+
|
|
442
|
+
```ts
|
|
443
|
+
type Rect = { x: number; y: number; width: number; height: number };
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
| Field | Type | Default | Meaning |
|
|
447
|
+
|---|---|---|---|
|
|
448
|
+
| `x` | `number` | required | Left edge. |
|
|
449
|
+
| `y` | `number` | required | Top edge. |
|
|
450
|
+
| `width` | `number` | required | Width. |
|
|
451
|
+
| `height` | `number` | required | Height. |
|
|
452
|
+
|
|
453
|
+
### ResizeEdge
|
|
454
|
+
|
|
455
|
+
```ts
|
|
456
|
+
type ResizeEdge = "left" | "right" | "top" | "bottom" | "top-left" | "top-right" | "bottom-left" | "bottom-right";
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
An edge or corner of the window, for `Overlay.startResize`.
|
|
460
|
+
|
|
461
|
+
## Hit regions
|
|
462
|
+
|
|
463
|
+
The exports in this section are also available from `@n1h/sweetview/native/hit-regions` alone, which imports no `bun:ffi`, so a page bundle can use them. The usual flow: the page computes polygons, sends them with [`flattenRegions`](#flattenregions) through an IPC method, the UI-thread method turns them back with [`unpackRegions`](#unpackregions) and hands them to a [`HitRelay`](#hitrelay), which calls `Overlay.setHitRegions`.
|
|
464
|
+
|
|
465
|
+
### convexHull
|
|
466
|
+
|
|
467
|
+
```ts
|
|
468
|
+
function convexHull(points: Point[]): Point[]
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
The convex hull of the points, by the monotone chain algorithm. The hull of a mesh's projected bounding-box corners is the exact silhouette of a box-shaped mesh and a coarse polygon for any other.
|
|
472
|
+
|
|
473
|
+
| Field | Type | Default | Meaning |
|
|
474
|
+
|---|---|---|---|
|
|
475
|
+
| `points` | [`Point`](#point)`[]` | required | Any points, in any order. Not modified. |
|
|
476
|
+
|
|
477
|
+
Returns: the hull, counter-clockwise in a y-up frame, which is clockwise on screen. With three or more input points, collinear and duplicate points are left out. Fewer than three distinct points give fewer than three back.
|
|
478
|
+
|
|
479
|
+
Thread: any.
|
|
480
|
+
|
|
481
|
+
### createHitRelay
|
|
482
|
+
|
|
483
|
+
```ts
|
|
484
|
+
function createHitRelay(overlay: Pick<Overlay, "setHitRegions">, probe: (question: HitProbe) => void): HitRelay
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
The UI-thread side of exact click-through. The page sends its coarse polygons; the overlay asks for the exact answer inside one through `probe`, typically an event to the page; the page's reply comes back through `HitRelay.answer`. Each probe replaces the one before, and a reply to anything but the latest probe is dropped. A page that never replies leaves polygon precision.
|
|
488
|
+
|
|
489
|
+
| Field | Type | Default | Meaning |
|
|
490
|
+
|---|---|---|---|
|
|
491
|
+
| `overlay` | `Pick<Overlay, "setHitRegions">` | required | Receives `setHitRegions` with the current regions, or `null` while disabled, and the relay's refiner. |
|
|
492
|
+
| `probe` | `(question: HitProbe) => void` | required | Sends one question to the page. Called on the UI thread from the overlay's poll, and synchronously from inside `setRegions`, `setEnabled` and `apply` when the cursor is inside a polygon; must return quickly. |
|
|
493
|
+
|
|
494
|
+
Returns: a [`HitRelay`](#hitrelay), enabled, with no regions. Nothing reaches the overlay until the first call to `setRegions`, `setEnabled` or `apply`.
|
|
495
|
+
|
|
496
|
+
Thread: UI thread.
|
|
497
|
+
|
|
498
|
+
### flattenRegions
|
|
499
|
+
|
|
500
|
+
```ts
|
|
501
|
+
function flattenRegions(polygons: HitPolygon[]): number[]
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
Polygons as one flat number array, the shape to send from the page in an IPC method: per polygon a point count, then that many `x, y` pairs. Its JSON is a fraction of the size of an array of point objects.
|
|
505
|
+
|
|
506
|
+
| Field | Type | Default | Meaning |
|
|
507
|
+
|---|---|---|---|
|
|
508
|
+
| `polygons` | [`HitPolygon`](#hitpolygon)`[]` | required | The polygons to send. |
|
|
509
|
+
|
|
510
|
+
Returns: the flat array.
|
|
511
|
+
|
|
512
|
+
Thread: any.
|
|
513
|
+
|
|
514
|
+
### hitRegions
|
|
515
|
+
|
|
516
|
+
```ts
|
|
517
|
+
function hitRegions(packed: Int32Array, x: number, y: number): boolean
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
Whether a point is inside any packed polygon. Each polygon's bounds are checked first, then the shape by even-odd ray casting. It allocates nothing, so hundreds of polygons cost microseconds.
|
|
521
|
+
|
|
522
|
+
| Field | Type | Default | Meaning |
|
|
523
|
+
|---|---|---|---|
|
|
524
|
+
| `packed` | `Int32Array` | required | Output of [`packRegions`](#packregions). |
|
|
525
|
+
| `x` | `number` | required | Window-local x. |
|
|
526
|
+
| `y` | `number` | required | Window-local y. |
|
|
527
|
+
|
|
528
|
+
Returns: true when the point is inside at least one polygon.
|
|
529
|
+
|
|
530
|
+
Thread: any.
|
|
531
|
+
|
|
532
|
+
### packRegions
|
|
533
|
+
|
|
534
|
+
```ts
|
|
535
|
+
function packRegions(polygons: HitPolygon[]): Int32Array
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
Packs polygons for [`hitRegions`](#hitregions). Per polygon: `count, minX, minY, maxX, maxY`, then `count` pairs of `x, y`. The minimum bounds are rounded down, the maximum bounds up, and the points to the nearest integer. Polygons with fewer than three points are dropped.
|
|
539
|
+
|
|
540
|
+
| Field | Type | Default | Meaning |
|
|
541
|
+
|---|---|---|---|
|
|
542
|
+
| `polygons` | [`HitPolygon`](#hitpolygon)`[]` | required | The polygons, window-local units. |
|
|
543
|
+
|
|
544
|
+
Returns: the packed array; empty when no polygon has three points.
|
|
545
|
+
|
|
546
|
+
Thread: any.
|
|
547
|
+
|
|
548
|
+
### unpackRegions
|
|
549
|
+
|
|
550
|
+
```ts
|
|
551
|
+
function unpackRegions(flat: readonly number[]): HitPolygon[]
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
The inverse of [`flattenRegions`](#flattenregions), for the UI-thread method that receives the polygons. The array is not validated: a truncated array yields points with `undefined` coordinates.
|
|
555
|
+
|
|
556
|
+
| Field | Type | Default | Meaning |
|
|
557
|
+
|---|---|---|---|
|
|
558
|
+
| `flat` | `readonly number[]` | required | Output of `flattenRegions`. |
|
|
559
|
+
|
|
560
|
+
Returns: the polygons.
|
|
561
|
+
|
|
562
|
+
Thread: any.
|
|
563
|
+
|
|
564
|
+
### HitPolygon
|
|
565
|
+
|
|
566
|
+
```ts
|
|
567
|
+
type HitPolygon = Point[];
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
One region: a simple polygon in window-local units, any winding. A rectangle is a four-point polygon.
|
|
571
|
+
|
|
572
|
+
### HitProbe
|
|
573
|
+
|
|
574
|
+
```ts
|
|
575
|
+
type HitProbe = Point & { id: number };
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
A question for the page: is its drawing solid at this window-local point?
|
|
579
|
+
|
|
580
|
+
| Field | Type | Default | Meaning |
|
|
581
|
+
|---|---|---|---|
|
|
582
|
+
| `x` | `number` | required | Window-local x of the cursor. |
|
|
583
|
+
| `y` | `number` | required | Window-local y of the cursor. |
|
|
584
|
+
| `id` | `number` | required | Grows by one with every probe; the reply carries it back. |
|
|
585
|
+
|
|
586
|
+
### HitRelay
|
|
587
|
+
|
|
588
|
+
```ts
|
|
589
|
+
type HitRelay = {
|
|
590
|
+
setRegions(polygons: HitPolygon[]): void;
|
|
591
|
+
answer(reply: { id: number; solid: boolean }): void;
|
|
592
|
+
setEnabled(on: boolean): void;
|
|
593
|
+
enabled(): boolean;
|
|
594
|
+
apply(): void;
|
|
595
|
+
};
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
| Member | Meaning |
|
|
599
|
+
|---|---|
|
|
600
|
+
| `setRegions(polygons)` | Stores the page's latest polygons, window-local units, and sends them to the overlay at once while enabled. |
|
|
601
|
+
| `answer(reply)` | The page's reply to a probe. Applied only when `reply.id` is the latest probe's id. |
|
|
602
|
+
| `setEnabled(on)` | `false`: the whole window takes clicks, whatever the regions say, as a click-through toggle. `true`: the regions apply again. Default `true`. |
|
|
603
|
+
| `enabled()` | Whether the regions apply. |
|
|
604
|
+
| `apply()` | Sends the current state to the overlay again, for example after the webview is created. Before any `setRegions` that state is an empty region list, so the whole window passes clicks through. |
|
|
605
|
+
|
|
606
|
+
Thread: UI thread for every member.
|
|
607
|
+
|
|
608
|
+
## Folder picker
|
|
609
|
+
|
|
610
|
+
### pickFolder
|
|
611
|
+
|
|
612
|
+
```ts
|
|
613
|
+
function pickFolder(owner: WindowHandle, title: string): string | null
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
Opens the OS folder dialog and blocks until the user picks a folder or cancels.
|
|
617
|
+
|
|
618
|
+
| Field | Type | Default | Meaning |
|
|
619
|
+
|---|---|---|---|
|
|
620
|
+
| `owner` | [`WindowHandle`](#windowhandle) | required | Windows: the window that owns the dialog; `null` for none. macOS: unused; the panel is modal to the app. |
|
|
621
|
+
| `title` | `string` | required | Windows: the dialog's title. macOS: the message shown in the panel. |
|
|
622
|
+
|
|
623
|
+
Returns: the absolute path of the chosen folder, or `null` when the user cancelled.
|
|
624
|
+
|
|
625
|
+
Throws:
|
|
626
|
+
|
|
627
|
+
- Windows, when a COM call fails, including when the calling thread's COM apartment is multi-threaded; the message names the call and its HRESULT.
|
|
628
|
+
- MacOS, when the open panel cannot be created.
|
|
629
|
+
|
|
630
|
+
Platforms: Windows: implemented; only existing file-system folders can be chosen. macOS: implemented; the user can create folders in the panel; the app is activated before the panel opens. Linux: throws.
|
|
631
|
+
|
|
632
|
+
Thread: UI thread. Windows: the call needs a single-threaded COM apartment and initialises one on a thread that has none. The UI thread, and so the webview, waits until the dialog closes.
|
|
633
|
+
|
|
634
|
+
## App menu
|
|
635
|
+
|
|
636
|
+
### installAppMenu
|
|
637
|
+
|
|
638
|
+
```ts
|
|
639
|
+
function installAppMenu(options: AppMenuOptions): void
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
Installs a minimal macOS menu bar, which the webview does not create, so Cmd+Q and the editing shortcuts work. It replaces any main menu the app had.
|
|
643
|
+
|
|
644
|
+
| Field | Type | Default | Meaning |
|
|
645
|
+
|---|---|---|---|
|
|
646
|
+
| `options` | [`AppMenuOptions`](#appmenuoptions) | required | The app's name and About text. |
|
|
647
|
+
|
|
648
|
+
| Menu | Item | Shortcut | Action |
|
|
649
|
+
|---|---|---|---|
|
|
650
|
+
| `appName` | About `appName`… | none | An alert titled "About `appName`" showing `aboutText` |
|
|
651
|
+
| `appName` | Quit `appName` | Cmd+Q | Quits the app |
|
|
652
|
+
| Edit | Undo | Cmd+Z | Sent to the focused view |
|
|
653
|
+
| Edit | Redo | Cmd+Shift+Z | Sent to the focused view |
|
|
654
|
+
| Edit | Cut | Cmd+X | Sent to the focused view |
|
|
655
|
+
| Edit | Copy | Cmd+C | Sent to the focused view |
|
|
656
|
+
| Edit | Paste | Cmd+V | Sent to the focused view |
|
|
657
|
+
| Edit | Select All | Cmd+A | Sent to the focused view |
|
|
658
|
+
|
|
659
|
+
macOS also switches the app to the regular activation policy, which gives it a Dock icon and a Cmd+Tab entry and undoes `hideFromTaskbar` of an overlay restyled before.
|
|
660
|
+
|
|
661
|
+
Platforms: macOS: implemented. Windows: no-op; the tray menu is the app's menu. Linux: throws.
|
|
662
|
+
|
|
663
|
+
Thread: UI thread, before the run loop starts.
|
|
664
|
+
|
|
665
|
+
### AppMenuOptions
|
|
666
|
+
|
|
667
|
+
```ts
|
|
668
|
+
type AppMenuOptions = { appName: string; aboutText: string };
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
| Field | Type | Default | Meaning |
|
|
672
|
+
|---|---|---|---|
|
|
673
|
+
| `appName` | `string` | required | The app menu's title and the name in its items. |
|
|
674
|
+
| `aboutText` | `string` | required | The body text of the About alert. |
|
|
675
|
+
|
|
676
|
+
## App icon
|
|
677
|
+
|
|
678
|
+
### setAppIcon
|
|
679
|
+
|
|
680
|
+
```ts
|
|
681
|
+
function setAppIcon(icon: Icon): void
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
Sets the running app's icon. macOS: the Dock tile and the Cmd+Tab switcher, from `icon.macos`, replacing the bundle's icon or the generic icon of a bare executable. The image is full colour unless `template: true`, at the PNG's own size. An icon without a usable `macos` image clears the custom icon.
|
|
685
|
+
|
|
686
|
+
| Field | Type | Default | Meaning |
|
|
687
|
+
|---|---|---|---|
|
|
688
|
+
| `icon` | [`Icon`](#icon) | required | The app's icon; only the `macos` section is read. |
|
|
689
|
+
|
|
690
|
+
Platforms: macOS: implemented. Windows: no-op; Explorer, the taskbar and Alt+Tab take the app's icon from the executable's resources, set at build time with `bun build --compile --windows-icon=app.ico`, and each window's from `NativeWindow.setIcon` or `OverlayWindowOptions.icon`. Linux: throws.
|
|
691
|
+
|
|
692
|
+
Thread: UI thread.
|
|
693
|
+
|
|
694
|
+
## Menus
|
|
695
|
+
|
|
696
|
+
A menu is data: a template describes the items, and each platform builds its own native menu from it every time the menu opens.
|
|
697
|
+
|
|
698
|
+
### resolveMenu
|
|
699
|
+
|
|
700
|
+
```ts
|
|
701
|
+
const resolveMenu: (source: MenuSource | undefined) => MenuTemplate
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
The template a source stands for: the function form is called afresh each time, `undefined` gives an empty template.
|
|
705
|
+
|
|
706
|
+
| Field | Type | Default | Meaning |
|
|
707
|
+
|---|---|---|---|
|
|
708
|
+
| `source` | `MenuSource \| undefined` | required | The source to resolve. |
|
|
709
|
+
|
|
710
|
+
Returns: the template.
|
|
711
|
+
|
|
712
|
+
Thread: any; the tray calls it on the UI thread when its menu is about to open.
|
|
713
|
+
|
|
714
|
+
### MenuItem
|
|
715
|
+
|
|
716
|
+
```ts
|
|
717
|
+
type MenuItem =
|
|
718
|
+
| {
|
|
719
|
+
type?: "item";
|
|
720
|
+
label: string;
|
|
721
|
+
enabled?: boolean;
|
|
722
|
+
checked?: boolean;
|
|
723
|
+
icon?: Icon;
|
|
724
|
+
click?: () => void;
|
|
725
|
+
}
|
|
726
|
+
| {
|
|
727
|
+
type?: "submenu";
|
|
728
|
+
label: string;
|
|
729
|
+
enabled?: boolean;
|
|
730
|
+
icon?: Icon;
|
|
731
|
+
submenu: MenuTemplate;
|
|
732
|
+
}
|
|
733
|
+
| { type: "separator" };
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
One entry of a menu: a clickable item, a submenu, or a separator. An entry with `type: "separator"` is a separator; any other entry with a `submenu` field is a submenu; the rest are clickable items.
|
|
737
|
+
|
|
738
|
+
| Field | Type | Default | Meaning |
|
|
739
|
+
|---|---|---|---|
|
|
740
|
+
| `type` | `"item" \| "submenu" \| "separator"` | inferred | Required only for a separator. |
|
|
741
|
+
| `label` | `string` | required | The item's text. Windows: an ampersand shows as written. |
|
|
742
|
+
| `enabled` | `boolean` | `true` | `false` greys the item out and makes it unclickable. |
|
|
743
|
+
| `checked` | `boolean` | `false` | Clickable items: shows a check mark. |
|
|
744
|
+
| `icon` | [`Icon`](#icon) | none | An image before the label. Windows: the .ico entry at the system small-icon size, 16 px at 100 % scale, scaled in software when there is no exact entry. macOS: a 16-point image, a template image unless `template: false`; shown on macOS 27 too, where menu item images are hidden by default. An image that cannot be decoded is logged and left out. |
|
|
745
|
+
| `click` | `() => void` | none | Clickable items: runs on the UI thread when the item is chosen. Windows: after the menu has closed. |
|
|
746
|
+
| `submenu` | [`MenuTemplate`](#menutemplate) | required for a submenu | The submenu's items. |
|
|
747
|
+
|
|
748
|
+
### MenuSource
|
|
749
|
+
|
|
750
|
+
```ts
|
|
751
|
+
type MenuSource = MenuTemplate | (() => MenuTemplate);
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
A template, or a function that produces one. The function form is called every time the menu is about to open, so labels, check marks and enabled state reflect the current app state without bookkeeping.
|
|
755
|
+
|
|
756
|
+
### MenuTemplate
|
|
757
|
+
|
|
758
|
+
```ts
|
|
759
|
+
type MenuTemplate = MenuItem[];
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
The items of one menu, top to bottom.
|
|
763
|
+
|
|
764
|
+
## Icons
|
|
765
|
+
|
|
766
|
+
Icons as bytes, with no image library: one [`Icon`](#icon) shape for every surface, encoders for .ico, .icns and PNG, a PNG decoder, and a coloured disc for an app that has no art yet. Every function here is pure and runs on any thread. Pixel data is top-down RGBA, four bytes per pixel, with straight alpha. Resampling is bilinear and scales up as well as down.
|
|
767
|
+
|
|
768
|
+
| Function | Sizes it writes, in px |
|
|
769
|
+
|---|---|
|
|
770
|
+
| `dotIcon` | 16, 24, 32, or `DotIconOptions.sizes` |
|
|
771
|
+
| `dotIcns` | 16, 32, 64, 128, 256, 512, 1024, always |
|
|
772
|
+
| `icoFromPng`, `icoFromRgba` | 16, 24, 32, 48, 64, or `sizes` |
|
|
773
|
+
| `icnsFromPng`, `icnsFromRgba` | 16, 32, 64, 128, 256, or `sizes` |
|
|
774
|
+
| `encodeIcns` | the sizes in its map; only 16, 32, 64, 128, 256, 512 and 1024 have an .icns element type |
|
|
775
|
+
|
|
776
|
+
### decodePng
|
|
777
|
+
|
|
778
|
+
```ts
|
|
779
|
+
function decodePng(bytes: Uint8Array): DecodedPng
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
Decodes a PNG to top-down RGBA. It reads non-interlaced 8-bit greyscale, RGB, palette, greyscale with alpha, and RGBA images, with every row filter. A tRNS chunk gives palette entries their alpha and makes one greyscale or RGB colour transparent. Chunk CRCs are not checked.
|
|
783
|
+
|
|
784
|
+
| Field | Type | Default | Meaning |
|
|
785
|
+
|---|---|---|---|
|
|
786
|
+
| `bytes` | `Uint8Array` | required | The PNG file's bytes. |
|
|
787
|
+
|
|
788
|
+
Returns: a [`DecodedPng`](#decodedpng).
|
|
789
|
+
|
|
790
|
+
Throws:
|
|
791
|
+
|
|
792
|
+
- When the bytes do not start with the PNG signature, or a chunk runs past the end of the bytes.
|
|
793
|
+
- When there is no IHDR chunk, the image is interlaced, or the bit depth is not 8.
|
|
794
|
+
- When the colour type is not one of the five above, or a palette image has no PLTE chunk.
|
|
795
|
+
- When the image data does not inflate, or inflates to fewer bytes than the image needs.
|
|
796
|
+
- When a row uses an unknown filter type.
|
|
797
|
+
|
|
798
|
+
### dotIcns
|
|
799
|
+
|
|
800
|
+
```ts
|
|
801
|
+
function dotIcns(opts: DotIconOptions = {}): Uint8Array
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
The coloured disc as an .icns file with every standard size, for a macOS app bundle's `CFBundleIconFile`. `opts.sizes` is ignored.
|
|
805
|
+
|
|
806
|
+
| Field | Type | Default | Meaning |
|
|
807
|
+
|---|---|---|---|
|
|
808
|
+
| `opts` | [`DotIconOptions`](#doticonoptions) | `{}` | Colour and ring. |
|
|
809
|
+
|
|
810
|
+
Returns: the .icns bytes.
|
|
811
|
+
|
|
812
|
+
### dotIcon
|
|
813
|
+
|
|
814
|
+
```ts
|
|
815
|
+
function dotIcon(opts: DotIconOptions = {}): Uint8Array
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
The coloured disc as a multi-size .ico file, for `Icon.windows.ico`.
|
|
819
|
+
|
|
820
|
+
| Field | Type | Default | Meaning |
|
|
821
|
+
|---|---|---|---|
|
|
822
|
+
| `opts` | [`DotIconOptions`](#doticonoptions) | `{}` | Colour, ring and sizes. |
|
|
823
|
+
|
|
824
|
+
Returns: the .ico bytes.
|
|
825
|
+
|
|
826
|
+
### dotPng
|
|
827
|
+
|
|
828
|
+
```ts
|
|
829
|
+
function dotPng(size: number, opts: DotIconOptions = {}): Uint8Array
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
The coloured disc as a square PNG, for `Icon.macos.png`.
|
|
833
|
+
|
|
834
|
+
| Field | Type | Default | Meaning |
|
|
835
|
+
|---|---|---|---|
|
|
836
|
+
| `size` | `number` | required | Width and height in px. |
|
|
837
|
+
| `opts` | [`DotIconOptions`](#doticonoptions) | `{}` | Colour and ring; `sizes` is ignored. |
|
|
838
|
+
|
|
839
|
+
Returns: the PNG bytes.
|
|
840
|
+
|
|
841
|
+
### encodeIcns
|
|
842
|
+
|
|
843
|
+
```ts
|
|
844
|
+
function encodeIcns(images: Map<number, Uint8Array>): Uint8Array
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
Wraps PNGs as an .icns file, smallest first, for a bundle's `CFBundleIconFile`. The PNG bytes and their sizes are not checked.
|
|
848
|
+
|
|
849
|
+
| Field | Type | Default | Meaning |
|
|
850
|
+
|---|---|---|---|
|
|
851
|
+
| `images` | `Map<number, Uint8Array>` | required | Pixel size to PNG bytes of that size. |
|
|
852
|
+
|
|
853
|
+
Returns: the .icns bytes.
|
|
854
|
+
|
|
855
|
+
Throws: when a size has no .icns element type; the message lists the sizes that do.
|
|
856
|
+
|
|
857
|
+
### encodeIco
|
|
858
|
+
|
|
859
|
+
```ts
|
|
860
|
+
function encodeIco(images: Map<number, Uint8Array>): Uint8Array
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
Wraps square RGBA images as an .ico file of 32-bit entries, smallest first. A 256 px entry is written with the zero size the format uses for it. The pixel arrays' lengths are not checked.
|
|
864
|
+
|
|
865
|
+
Throws: when a size is below 1 or above 256.
|
|
866
|
+
|
|
867
|
+
| Field | Type | Default | Meaning |
|
|
868
|
+
|---|---|---|---|
|
|
869
|
+
| `images` | `Map<number, Uint8Array>` | required | Size in px to top-down RGBA of that size. Sizes up to 256. |
|
|
870
|
+
|
|
871
|
+
Returns: the .ico bytes.
|
|
872
|
+
|
|
873
|
+
### encodePng
|
|
874
|
+
|
|
875
|
+
```ts
|
|
876
|
+
function encodePng(size: number, rgba: Uint8Array): Uint8Array
|
|
877
|
+
```
|
|
878
|
+
|
|
879
|
+
A square RGBA image as an 8-bit RGBA PNG.
|
|
880
|
+
|
|
881
|
+
| Field | Type | Default | Meaning |
|
|
882
|
+
|---|---|---|---|
|
|
883
|
+
| `size` | `number` | required | Width and height in px. |
|
|
884
|
+
| `rgba` | `Uint8Array` | required | Top-down RGBA, `size * size * 4` bytes. |
|
|
885
|
+
|
|
886
|
+
Returns: the PNG bytes.
|
|
887
|
+
|
|
888
|
+
Throws: when `rgba` is not `size * size * 4` bytes long.
|
|
889
|
+
|
|
890
|
+
### icnsFromPng
|
|
891
|
+
|
|
892
|
+
```ts
|
|
893
|
+
function icnsFromPng(png: Uint8Array, sizes?: number[]): Uint8Array
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
A square PNG as an .icns file, resampled to each size, for `CFBundleIconFile`.
|
|
897
|
+
|
|
898
|
+
| Field | Type | Default | Meaning |
|
|
899
|
+
|---|---|---|---|
|
|
900
|
+
| `png` | `Uint8Array` | required | A square PNG that [`decodePng`](#decodepng) reads. |
|
|
901
|
+
| `sizes` | `number[]` | `[16, 32, 64, 128, 256]` | Sizes to write; each needs an .icns element type. |
|
|
902
|
+
|
|
903
|
+
Returns: the .icns bytes.
|
|
904
|
+
|
|
905
|
+
Throws:
|
|
906
|
+
|
|
907
|
+
- When `decodePng` throws.
|
|
908
|
+
- When the PNG is not square.
|
|
909
|
+
- When a size has no .icns element type.
|
|
910
|
+
|
|
911
|
+
### icnsFromRgba
|
|
912
|
+
|
|
913
|
+
```ts
|
|
914
|
+
function icnsFromRgba(size: number, rgba: Uint8Array, sizes: number[] = [16, 32, 64, 128, 256]): Uint8Array
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
Square RGBA as an .icns file, resampled to each size, for `CFBundleIconFile`.
|
|
918
|
+
|
|
919
|
+
| Field | Type | Default | Meaning |
|
|
920
|
+
|---|---|---|---|
|
|
921
|
+
| `size` | `number` | required | Width and height of `rgba` in px. |
|
|
922
|
+
| `rgba` | `Uint8Array` | required | Top-down RGBA, `size * size * 4` bytes. |
|
|
923
|
+
| `sizes` | `number[]` | `[16, 32, 64, 128, 256]` | Sizes to write; each needs an .icns element type. |
|
|
924
|
+
|
|
925
|
+
Returns: the .icns bytes.
|
|
926
|
+
|
|
927
|
+
Throws: when a size has no .icns element type.
|
|
928
|
+
|
|
929
|
+
### icoFromPng
|
|
930
|
+
|
|
931
|
+
```ts
|
|
932
|
+
function icoFromPng(png: Uint8Array, sizes?: number[]): Uint8Array
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
A square PNG as a multi-size .ico file of 32-bit entries, resampled to each size, for `Icon.windows.ico`.
|
|
936
|
+
|
|
937
|
+
| Field | Type | Default | Meaning |
|
|
938
|
+
|---|---|---|---|
|
|
939
|
+
| `png` | `Uint8Array` | required | A square PNG that [`decodePng`](#decodepng) reads. |
|
|
940
|
+
| `sizes` | `number[]` | `[16, 24, 32, 48, 64]` | Sizes to write, up to 256. |
|
|
941
|
+
|
|
942
|
+
Returns: the .ico bytes.
|
|
943
|
+
|
|
944
|
+
Throws:
|
|
945
|
+
|
|
946
|
+
- When `decodePng` throws.
|
|
947
|
+
- When the PNG is not square.
|
|
948
|
+
|
|
949
|
+
### icoFromRgba
|
|
950
|
+
|
|
951
|
+
```ts
|
|
952
|
+
function icoFromRgba(size: number, rgba: Uint8Array, sizes: number[] = [16, 24, 32, 48, 64]): Uint8Array
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
Square RGBA as a multi-size .ico file of 32-bit entries, resampled to each size, for `Icon.windows.ico`.
|
|
956
|
+
|
|
957
|
+
| Field | Type | Default | Meaning |
|
|
958
|
+
|---|---|---|---|
|
|
959
|
+
| `size` | `number` | required | Width and height of `rgba` in px. |
|
|
960
|
+
| `rgba` | `Uint8Array` | required | Top-down RGBA, `size * size * 4` bytes. |
|
|
961
|
+
| `sizes` | `number[]` | `[16, 24, 32, 48, 64]` | Sizes to write, up to 256. |
|
|
962
|
+
|
|
963
|
+
Returns: the .ico bytes.
|
|
964
|
+
|
|
965
|
+
### renderDot
|
|
966
|
+
|
|
967
|
+
```ts
|
|
968
|
+
function renderDot(size: number, opts: DotIconOptions = {}): Uint8Array
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
A filled disc, with an optional outer ring, on a transparent square. The disc's diameter is `size`, so it touches all four edges. The disc's edge is anti-aliased over one pixel. The ring meets the fill with a hard edge.
|
|
972
|
+
|
|
973
|
+
| Field | Type | Default | Meaning |
|
|
974
|
+
|---|---|---|---|
|
|
975
|
+
| `size` | `number` | required | Width and height in px. |
|
|
976
|
+
| `opts` | [`DotIconOptions`](#doticonoptions) | `{}` | Colour and ring; `sizes` is ignored. |
|
|
977
|
+
|
|
978
|
+
Returns: top-down RGBA, `size * size * 4` bytes.
|
|
979
|
+
|
|
980
|
+
### trimTransparent
|
|
981
|
+
|
|
982
|
+
```ts
|
|
983
|
+
function trimTransparent(image: DecodedPng, margin = 1, threshold = 8): DecodedPng
|
|
984
|
+
```
|
|
985
|
+
|
|
986
|
+
Crops an image to its opaque pixels, as a square centred on them with a transparent margin, so art with padding around its subject fills a 16 px menu icon. Where the square runs past the source it is padded with transparency.
|
|
987
|
+
|
|
988
|
+
| Field | Type | Default | Meaning |
|
|
989
|
+
|---|---|---|---|
|
|
990
|
+
| `image` | [`DecodedPng`](#decodedpng) | required | The image to crop. |
|
|
991
|
+
| `margin` | `number` | `1` | Pixels added beyond the subject on its longer axis; the shorter axis gets more, to make the square. Margin pixels inside the source keep the source's pixels, whose alpha is at most `threshold`. |
|
|
992
|
+
| `threshold` | `number` | `8` | A pixel counts as opaque when its alpha is above this. |
|
|
993
|
+
|
|
994
|
+
Returns: a new square image, or `image` itself, not cropped or squared, when no pixel's alpha is above `threshold`.
|
|
995
|
+
|
|
996
|
+
### DecodedPng
|
|
997
|
+
|
|
998
|
+
```ts
|
|
999
|
+
type DecodedPng = { width: number; height: number; rgba: Uint8Array };
|
|
1000
|
+
```
|
|
1001
|
+
|
|
1002
|
+
| Field | Type | Default | Meaning |
|
|
1003
|
+
|---|---|---|---|
|
|
1004
|
+
| `width` | `number` | required | Width in px. |
|
|
1005
|
+
| `height` | `number` | required | Height in px. |
|
|
1006
|
+
| `rgba` | `Uint8Array` | required | Top-down RGBA, `width * height * 4` bytes. |
|
|
1007
|
+
|
|
1008
|
+
### DotIconOptions
|
|
1009
|
+
|
|
1010
|
+
```ts
|
|
1011
|
+
type DotIconOptions = {
|
|
1012
|
+
color?: string;
|
|
1013
|
+
ring?: string;
|
|
1014
|
+
sizes?: number[];
|
|
1015
|
+
};
|
|
1016
|
+
```
|
|
1017
|
+
|
|
1018
|
+
| Field | Type | Default | Meaning |
|
|
1019
|
+
|---|---|---|---|
|
|
1020
|
+
| `color` | `string` | `"#8b5cf6"`, violet | Fill colour, `#rrggbb` or `#rgb`. |
|
|
1021
|
+
| `ring` | `string` | none | Colour of an outer ring, `#rrggbb` or `#rgb`. The ring's inner edge is `max(1, size / 10) + 0.5` px inside the disc's edge. |
|
|
1022
|
+
| `sizes` | `number[]` | `[16, 24, 32]` | Image sizes in px; read by `dotIcon` only. |
|
|
1023
|
+
|
|
1024
|
+
### Icon
|
|
1025
|
+
|
|
1026
|
+
```ts
|
|
1027
|
+
type Icon = {
|
|
1028
|
+
windows?: {
|
|
1029
|
+
ico: Uint8Array;
|
|
1030
|
+
};
|
|
1031
|
+
macos?: {
|
|
1032
|
+
symbol?: string;
|
|
1033
|
+
png?: Uint8Array;
|
|
1034
|
+
template?: boolean;
|
|
1035
|
+
};
|
|
1036
|
+
};
|
|
1037
|
+
```
|
|
1038
|
+
|
|
1039
|
+
An icon with one section per platform; each platform reads only its own. A missing section means that platform's stock image. The same shape serves the tray, menu items, windows and the app icon.
|
|
1040
|
+
|
|
1041
|
+
| Field | Type | Default | Meaning |
|
|
1042
|
+
|---|---|---|---|
|
|
1043
|
+
| `windows.ico` | `Uint8Array` | required in `windows` | The bytes of an .ico file. The best-fitting entry is picked for each use: the small-icon size for the tray, menus and a window's small icon, the large-icon size for notifications and a window's big icon. 32-bit and PNG-compressed entries both work. |
|
|
1044
|
+
| `macos.symbol` | `string` | none | An SF Symbol name. Preferred over `png` when both are given; when no symbol has that name, an error is logged and `png` is used. |
|
|
1045
|
+
| `macos.png` | `Uint8Array` | none | PNG bytes. Sized by the use: 18x18 points in the menu bar, 16x16 in menus, the PNG's own size in the Dock. Supply 2x pixels for Retina. |
|
|
1046
|
+
| `macos.template` | `boolean` | `true` in the menu bar and menus, `false` for the Dock | A monochrome image the system recolours for light and dark. A colour image wants `false`. |
|
|
1047
|
+
|
|
1048
|
+
## Example
|
|
1049
|
+
|
|
1050
|
+
```ts
|
|
1051
|
+
import { createWebviewApp } from "@n1h/sweetview";
|
|
1052
|
+
import { createNotifier, createOverlayWindow, createTray, dotIcon, type MenuTemplate } from "@n1h/sweetview/native";
|
|
1053
|
+
|
|
1054
|
+
const settings = createOverlayWindow({ frame: true, title: "Settings", bounds: { x: 200, y: 200, width: 480, height: 360 } });
|
|
1055
|
+
const app = await createWebviewApp({ title: "Settings", window: settings.handle, assets: { "index.html": "<h1>Settings</h1>" } });
|
|
1056
|
+
let paused = false;
|
|
1057
|
+
const menu = (): MenuTemplate => [
|
|
1058
|
+
{ label: "Settings…", click: () => settings.show() },
|
|
1059
|
+
{ label: "Paused", checked: paused, click: () => { paused = !paused; } },
|
|
1060
|
+
{ type: "separator" },
|
|
1061
|
+
{ label: "Quit", click: () => app.quit() },
|
|
1062
|
+
];
|
|
1063
|
+
createTray({ tooltip: "My app", icon: { windows: { ico: dotIcon() }, macos: { symbol: "circle.fill" } }, menu, onDoubleClick: () => settings.show() });
|
|
1064
|
+
createNotifier({ onClick: () => settings.show() }).notify({ title: "My app", body: "Running in the tray" });
|
|
1065
|
+
app.run();
|
|
1066
|
+
```
|