react-x11 2.16.0 → 2.17.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.
Files changed (62) hide show
  1. package/README.md +38 -23
  2. package/package.json +3 -1
  3. package/src/Reconciler.js +82 -23
  4. package/src/a11y.js +18 -1
  5. package/src/acceleratorhooks.js +40 -6
  6. package/src/anchor.js +20 -2
  7. package/src/appcontext.js +8 -0
  8. package/src/appearance.js +36 -0
  9. package/src/{cocoa → backend}/context2d.js +27 -7
  10. package/src/capabilities.js +99 -1
  11. package/src/cocoa/app.js +204 -6
  12. package/src/cocoa/fonts.js +1 -1
  13. package/src/cocoa/overlay.js +2 -2
  14. package/src/cocoa/panewindow.js +2 -2
  15. package/src/cocoa/presenter.js +2 -2
  16. package/src/cocoa/surface.js +3 -3
  17. package/src/cocoa/window.js +2 -2
  18. package/src/events.js +21 -0
  19. package/src/foreignnodes.js +8 -3
  20. package/src/frame/index.js +30 -4
  21. package/src/glnodes.js +12 -1
  22. package/src/idle.js +59 -1
  23. package/src/index.d.ts +51 -1
  24. package/src/index.js +30 -3
  25. package/src/keysymchars.js +47 -0
  26. package/src/keysyms.d.ts +19 -1
  27. package/src/keysyms.js +107 -8
  28. package/src/launcher.js +17 -8
  29. package/src/launcherhooks.js +24 -10
  30. package/src/node.d.ts +1 -1
  31. package/src/nodes/cascade.js +9 -0
  32. package/src/nodes/node.js +6 -1
  33. package/src/nodes/window/hints.js +21 -2
  34. package/src/nodes/window/window.js +2 -2
  35. package/src/notifications.js +39 -14
  36. package/src/screens.js +159 -24
  37. package/src/taskbarhooks.js +164 -0
  38. package/src/transfer.js +20 -1
  39. package/src/trayhooks.js +1 -1
  40. package/src/types/capabilities.d.ts +32 -3
  41. package/src/types/elements.d.ts +23 -1
  42. package/src/types/events.d.ts +21 -0
  43. package/src/types/filedialog.d.ts +3 -1
  44. package/src/types/launcher.d.ts +20 -6
  45. package/src/types/taskbar.d.ts +79 -0
  46. package/src/wayland/context2d.js +1 -1
  47. package/src/wayland/xkb.js +170 -59
  48. package/src/win32/a11y.js +604 -0
  49. package/src/win32/app.js +768 -0
  50. package/src/win32/bezels.js +158 -0
  51. package/src/win32/dnd.js +283 -0
  52. package/src/win32/fonts.js +497 -0
  53. package/src/win32/glarea.js +548 -0
  54. package/src/win32/ime.js +267 -0
  55. package/src/win32/keymap.js +116 -0
  56. package/src/win32/native.js +54 -0
  57. package/src/win32/panehost.js +106 -0
  58. package/src/win32/panewindow.js +343 -0
  59. package/src/win32/shell.js +426 -0
  60. package/src/win32/surface.js +192 -0
  61. package/src/win32/window.js +659 -0
  62. package/src/windowid.js +128 -20
@@ -0,0 +1,426 @@
1
+ // The shell integrations, as the seams react-x11's ladders look for.
2
+ //
3
+ // Each one is found by capability rather than by platform — `app.createStatusItem`,
4
+ // `app.setDockBadge`, `app.filePanels` — which is the rule in AGENTS.md and
5
+ // what lets the same hook answer from a portal on Linux, from AppKit on a Mac
6
+ // and from Shell_NotifyIcon here.
7
+ //
8
+ // Everything below is asynchronous because the bridge is: a tray icon, a
9
+ // taskbar button and a file dialog all live on the UI thread, where their
10
+ // modal loops belong.
11
+
12
+ /** A tray icon. `useTray()` drives this shape through `app.createStatusItem`. */
13
+ export class Win32StatusItem {
14
+ constructor(app, options = {}) {
15
+ this.app = app;
16
+ this._native = app._native;
17
+ this._listeners = { click: new Set(), action: new Set() };
18
+ this._menu = [];
19
+ this.handle = this._native.trayCreate({
20
+ tooltip: options.tooltip ?? options.title ?? '',
21
+ ...iconOf(options),
22
+ });
23
+ app._statusItems.set(this.handle, this);
24
+ if (options.menu) this.setMenu(options.menu);
25
+ }
26
+
27
+ update(options = {}) {
28
+ if (options.tooltip !== undefined || options.title !== undefined) {
29
+ this._native.trayUpdate(this.handle, {
30
+ tooltip: options.tooltip ?? options.title ?? '',
31
+ });
32
+ }
33
+ if (options.menu) this.setMenu(options.menu);
34
+ }
35
+
36
+ /**
37
+ * The menu behind a right-click. Flattened to `{ label, action }` because
38
+ * the bridge holds no item model: a separator is a label of `-`, and the
39
+ * action is the string that comes back, which keeps the menu the app wrote
40
+ * the menu that answers.
41
+ */
42
+ setMenu(items) {
43
+ this._menu = items ?? [];
44
+ this._native.trayMenu(
45
+ this.handle,
46
+ this._menu.map((item, at) => ({
47
+ label:
48
+ item.separator || item.type === 'separator'
49
+ ? '-'
50
+ : (item.label ?? ''),
51
+ action: String(item.id ?? item.action ?? at),
52
+ })),
53
+ );
54
+ }
55
+
56
+ on(name, fn) {
57
+ this._listeners[name]?.add(fn);
58
+ return () => this._listeners[name]?.delete(fn);
59
+ }
60
+
61
+ _emit(name, value) {
62
+ for (const fn of [...(this._listeners[name] ?? [])]) {
63
+ try {
64
+ fn(value);
65
+ } catch (err) {
66
+ if (process.env.NODE_ENV !== 'production') console.error(err);
67
+ }
68
+ }
69
+ }
70
+
71
+ /** The item the action belongs to, found by the id `setMenu` handed over. */
72
+ /**
73
+ * A menu item was chosen. `action` is the string this item was registered
74
+ * under — the bridge holds no item model, so the menu the app wrote is
75
+ * found again here by that string.
76
+ *
77
+ * `onSelect`, given the item, is the contract every backend keeps
78
+ * (src/cocoa/statusitem.js, src/cocoa/dock.js). This used to call
79
+ * `onClick()` with no argument, which is a different callback — the one
80
+ * for a click on the *icon* — so every menu command did nothing at all.
81
+ */
82
+ _activate(action) {
83
+ const item = this._menu.find(
84
+ (entry, at) => String(entry.id ?? entry.action ?? at) === action,
85
+ );
86
+ item?.onSelect?.(item);
87
+ this._emit('action', item ?? action);
88
+ }
89
+
90
+ remove() {
91
+ this.app._statusItems.delete(this.handle);
92
+ this._native.trayRemove(this.handle);
93
+ }
94
+ }
95
+
96
+ /** An `{ icon, iconWidth, iconHeight }` triple from whatever the caller gave. */
97
+ function iconOf(options) {
98
+ const icon = options.icon ?? options.image;
99
+ if (icon && icon.data && icon.width && icon.height) {
100
+ return {
101
+ icon: Buffer.from(icon.data.buffer ?? icon.data),
102
+ iconWidth: icon.width,
103
+ iconHeight: icon.height,
104
+ };
105
+ }
106
+ // A string icon is a name on the freedesktop rung and an SF Symbol on the
107
+ // Cocoa one. Windows has no icon theme to look a name up in, so it is
108
+ // ignored rather than guessed at, and the app's own icon is used.
109
+ return {};
110
+ }
111
+
112
+ /**
113
+ * The native open/save panels — `app.filePanels`, whose *presence* is what
114
+ * puts the top rung on src/filedialog.js's ladder.
115
+ */
116
+ export class Win32FilePanels {
117
+ constructor(app) {
118
+ this.app = app;
119
+ this._native = app._native;
120
+ this._pending = new Map();
121
+ }
122
+
123
+ /**
124
+ * `show(kind, opts, wnd)` — the contract src/filedialog.js calls. Resolves
125
+ * with the chosen paths, or with an empty list when the user cancelled,
126
+ * which is not an error and must not be one.
127
+ */
128
+ show(kind, opts = {}, wnd = null) {
129
+ const request = this._native.fileDialog({
130
+ kind: kind === 'save' ? 'save' : 'open',
131
+ title: opts.title,
132
+ buttonLabel: opts.buttonLabel ?? opts.prompt,
133
+ defaultPath: opts.defaultPath ?? opts.directory,
134
+ defaultName: opts.defaultName ?? opts.nameFieldStringValue,
135
+ multiple: Boolean(opts.multiple ?? opts.allowsMultipleSelection),
136
+ directory: Boolean(opts.chooseDirectories ?? opts.canChooseDirectories),
137
+ filters: (opts.filters ?? []).map((filter) => ({
138
+ name: filter.name ?? 'Files',
139
+ extensions: filter.extensions ?? [],
140
+ })),
141
+ ownerHwnd: wnd ? this._native.windowHandle(wnd.id) : 0,
142
+ });
143
+ return new Promise((resolve) => this._pending.set(request, resolve));
144
+ }
145
+
146
+ _answer(request, chose, text) {
147
+ const resolve = this._pending.get(request);
148
+ if (!resolve) return;
149
+ this._pending.delete(request);
150
+ resolve({
151
+ canceled: !chose,
152
+ filePaths: chose && text ? text.split('\n') : [],
153
+ });
154
+ }
155
+ }
156
+
157
+ /**
158
+ * The taskbar button: Windows' answer to the Dock tile.
159
+ *
160
+ * Windows has **no count badge**. `ITaskbarList3::SetOverlayIcon` takes a
161
+ * 16x16 icon, so a number has to be drawn into one — which the renderer can
162
+ * do and this cannot, having no text engine of its own down here. Until that
163
+ * is wired, a badge request sets the flashing state and says so, which is
164
+ * more honest than silently doing nothing.
165
+ */
166
+ /**
167
+ * The count drawn into a taskbar overlay icon.
168
+ *
169
+ * `ITaskbarList3::SetOverlayIcon` takes an icon and nothing else: there is no
170
+ * badge API on Windows that draws a number for you, the way the Dock has one.
171
+ * So the number is drawn here, through the same Direct2D verbs everything else
172
+ * draws through, and handed over as pixels.
173
+ *
174
+ * 32×32 because that is the size the shell asks for at every scale it renders
175
+ * the taskbar at, and a smaller icon is scaled up rather than re-rendered.
176
+ * Long labels are clamped to `99+`, which is what every other badge on every
177
+ * other desktop does with them — a taskbar overlay is ~16 device pixels on a
178
+ * 100% display and three glyphs is already generous.
179
+ */
180
+ async function badgePixels(app, label, accent) {
181
+ const SIZE = 32;
182
+ const text = String(label);
183
+ const shown = text.length > 3 ? '99+' : text;
184
+ let surface = null;
185
+ try {
186
+ surface = app.createSurface({
187
+ width: SIZE,
188
+ height: SIZE,
189
+ format: 'argb32',
190
+ });
191
+ const ctx = surface.getContext('2d');
192
+ ctx.clearRect(0, 0, SIZE, SIZE);
193
+ ctx.fillStyle = accent;
194
+ ctx.beginPath();
195
+ ctx.arc(SIZE / 2, SIZE / 2, SIZE / 2 - 1, 0, Math.PI * 2);
196
+ ctx.fill();
197
+ ctx.fillStyle = '#ffffff';
198
+ // Sized to the label: three glyphs in a 32px circle need the smaller face.
199
+ const size = shown.length > 2 ? 15 : 19;
200
+ ctx.font = `600 ${size}px "Segoe UI", sans-serif`;
201
+ // Centred by hand. This context takes `fillText(text, x, baselineY)` and
202
+ // has no `textAlign`/`textBaseline` — it is the verb table's shape, not
203
+ // the canvas API's, and measuring is the part that was never optional.
204
+ const width = ctx.measureText(shown).width;
205
+ ctx.fillText(shown, (SIZE - width) / 2, SIZE / 2 + size * 0.36);
206
+ // A promise: this context's `getImageData` is the asynchronous form, a
207
+ // read being a round trip on the backend it was written for. Here it
208
+ // resolves on a microtask, and it is what makes the badge land a tick
209
+ // after it was asked for rather than in the same call.
210
+ const image = await ctx.getImageData(0, 0, SIZE, SIZE);
211
+ return {
212
+ pixels: Buffer.from(
213
+ image.data.buffer,
214
+ image.data.byteOffset,
215
+ image.data.length,
216
+ ),
217
+ size: SIZE,
218
+ };
219
+ } catch {
220
+ // A badge that could not be drawn is not worth an error path: the caller
221
+ // asked to decorate an icon, and the icon is still there.
222
+ return null;
223
+ } finally {
224
+ surface?.destroy?.();
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Notifications, as a balloon on a tray icon.
230
+ *
231
+ * Shaped like the Cocoa backend's notification centre — `kind`, `available()`,
232
+ * `post()` — because src/notifications.js asks the backend for one and does
233
+ * not care which platform answered. `kind` is what makes the rung report
234
+ * itself honestly in `notificationBackend()`.
235
+ *
236
+ * What this rung cannot do, and says so by not implementing it: actions,
237
+ * replacing a notification in place, and an icon of the app's own. Those want
238
+ * the Windows App SDK's notification manager and an AppUserModelID the system
239
+ * knows — docs/windows.md §"The desktop around the app". Until then the
240
+ * balloon is shown by the shell in the same place, with the same sound, and
241
+ * kept in the same action centre.
242
+ */
243
+ export function installNotifications(app) {
244
+ app.notifications = {
245
+ kind: 'win32',
246
+ // Always: Shell_NotifyIcon is in every Windows session, and a user who
247
+ // has turned notifications off has turned off the display of it rather
248
+ // than the call. There is no ask-first here the way there is on macOS.
249
+ available: async () => true,
250
+ post: async (options) => {
251
+ // The app's own tray icon if it has one, so the balloon points at
252
+ // something the user recognises instead of at a second icon.
253
+ const tray = app._statusItems?.values?.().next?.().value ?? null;
254
+ const ok = app._native.trayNotify(tray?.handle ?? 0, {
255
+ title: options.summary ?? '',
256
+ body: options.body ?? '',
257
+ urgency: options.urgency ?? 'normal',
258
+ });
259
+ if (!ok) return null;
260
+ // The handle src/notifications.js hands back. `close` and `update` are
261
+ // the two a caller may reach for; both are no-ops rather than missing,
262
+ // because a balloon is gone when the shell says so and there is nothing
263
+ // to close or replace.
264
+ return {
265
+ backend: 'win32',
266
+ id: 0,
267
+ close: async () => {},
268
+ update: async () => false,
269
+ closed: Promise.resolve('expired'),
270
+ };
271
+ },
272
+ };
273
+ }
274
+
275
+ /**
276
+ * Two things the desktop knows and this process does not: whether anybody is
277
+ * at the keyboard, and whether the screen may go dark.
278
+ *
279
+ * Both are session-wide on Windows and neither needs a window, so they are on
280
+ * the app rather than on a window — the same shape `src/idle.js` reaches for.
281
+ */
282
+ export function installIdle(app) {
283
+ app.lastInputMs = () => app._native.lastInputMs();
284
+ // Counted, because two callers can want the screen awake at once and the
285
+ // first to finish must not speak for the second — a video player and a
286
+ // long upload in the same app is the ordinary case, not the exotic one.
287
+ // ES_CONTINUOUS is a thread state rather than a counted resource, so the
288
+ // count is kept here and the state is set once and cleared once.
289
+ let held = 0;
290
+ app.keepAwake = (display = true) => {
291
+ if (held === 0) app._native.keepAwake(display);
292
+ held += 1;
293
+ let released = false;
294
+ return () => {
295
+ if (released) return;
296
+ released = true;
297
+ held -= 1;
298
+ if (held === 0) app._native.keepAwake(null);
299
+ };
300
+ };
301
+ }
302
+
303
+ /**
304
+ * The three surfaces the Windows taskbar has that no other desktop does.
305
+ *
306
+ * They are installed as methods on the app, and their presence is what the
307
+ * launcher capability reports as `features.tasks`,
308
+ * `features.thumbnailToolbar` and `features.recentDocuments`
309
+ * (src/capabilities.js) — so a component branches on whether this desktop has
310
+ * the surface, and every other backend answers false without knowing anything
311
+ * about Windows. The probe reads these same methods, which is what keeps the
312
+ * prediction and the hook from ever disagreeing.
313
+ *
314
+ * None of them is a rung on an existing ladder, deliberately. A jump list is
315
+ * not a launcher menu — `useLauncherMenu`'s items carry a callback and a jump-list
316
+ * task starts a *new process* with arguments, so mapping one to the other
317
+ * would quietly change what a click does. Until a second launch can hand its
318
+ * arguments to the first (docs/windows-integrations.md) they are different
319
+ * features and are named differently.
320
+ */
321
+ export function installTaskbarSurfaces(app) {
322
+ /**
323
+ * Up to seven buttons under the taskbar's hover preview.
324
+ *
325
+ * The ids the shell sends back are indices, so the labels are kept here and
326
+ * the click is reported with the button's own `id` — what the caller named
327
+ * it, not where it happened to sit.
328
+ */
329
+ app._thumbButtons = new Map(); // window id -> [caller ids]
330
+ app.thumbnailToolbar = (windowId, buttons) => {
331
+ const list = (buttons ?? []).slice(0, 7);
332
+ app._thumbButtons.set(
333
+ windowId,
334
+ list.map((button) => button.id ?? null),
335
+ );
336
+ app._native.thumbnailToolbar(
337
+ windowId,
338
+ list.map((button) => ({
339
+ tooltip: button.tooltip ?? button.label ?? '',
340
+ enabled: button.enabled !== false,
341
+ dismissOnClick: button.dismissOnClick === true,
342
+ ...iconOf(button),
343
+ })),
344
+ );
345
+ };
346
+
347
+ /**
348
+ * The Tasks category of this application's jump list, shown on a right
349
+ * click of its taskbar button. `null` takes it away.
350
+ */
351
+ app.jumpList = (tasks) => {
352
+ app._native.jumpList(
353
+ (tasks ?? []).map((task) => ({
354
+ title: String(task.title ?? ''),
355
+ arguments: String(task.arguments ?? ''),
356
+ description: String(task.description ?? ''),
357
+ })),
358
+ );
359
+ };
360
+
361
+ /**
362
+ * A document this app just opened, for the shell's Recent lists — the jump
363
+ * list's own, and Explorer's quick access. `null` clears the list.
364
+ *
365
+ * The same act as `NSDocumentController.noteNewRecentDocumentURL:`, and the
366
+ * reason it is here rather than in a cross-platform hook is that only this
367
+ * backend has one to call today.
368
+ */
369
+ app.noteRecentDocument = (path) => {
370
+ app._native.recentDocument(path == null ? null : String(path));
371
+ };
372
+ }
373
+
374
+ export function installTaskbar(app) {
375
+ const handleOf = () => {
376
+ const [first] = [...app._windows.values()];
377
+ return first ? app._native.windowHandle(first.id) : 0;
378
+ };
379
+
380
+ // Which badge was asked for last. Drawing one is asynchronous, so two
381
+ // quick calls can finish in the order the GPU felt like; the counter is
382
+ // what keeps the icon showing the newest answer rather than the slowest.
383
+ let badgeSeq = 0;
384
+
385
+ app.setDockBadge = async (label) => {
386
+ const seq = ++badgeSeq;
387
+ const hwnd = handleOf();
388
+ if (!hwnd) return;
389
+ if (label == null || String(label) === '') {
390
+ app._native.taskbarOverlay(hwnd, null);
391
+ return;
392
+ }
393
+ // The accent, so the badge belongs to the desktop it sits on rather than
394
+ // to a colour this file picked. A theme with none falls back to the blue
395
+ // Windows ships with.
396
+ const accent = app.systemAppearance?.()?.accent ?? '#0078D4';
397
+ const badge = await badgePixels(app, label, accent);
398
+ if (!badge || seq !== badgeSeq) return;
399
+ // The description is the accessible name of the overlay — what a screen
400
+ // reader says about the taskbar button, and the one part of a badge that
401
+ // is not a picture.
402
+ app._native.taskbarOverlay(
403
+ hwnd,
404
+ badge.pixels,
405
+ badge.size,
406
+ badge.size,
407
+ String(label),
408
+ );
409
+ };
410
+
411
+ app.setTaskbarProgress = (value, { indeterminate = false } = {}) => {
412
+ const hwnd = handleOf();
413
+ if (hwnd) app._native.taskbarProgress(hwnd, value, indeterminate);
414
+ };
415
+
416
+ app.requestAttention = () => {
417
+ const hwnd = handleOf();
418
+ if (hwnd) app._native.taskbarFlash(hwnd, true);
419
+ return 1;
420
+ };
421
+
422
+ app.cancelAttention = () => {
423
+ const hwnd = handleOf();
424
+ if (hwnd) app._native.taskbarFlash(hwnd, false);
425
+ };
426
+ }
@@ -0,0 +1,192 @@
1
+ // An offscreen drawing surface on the win32 backend — ntk's `Surface`
2
+ // contract (draw once, composite many) over one Direct2D bitmap.
3
+ // `react-x11/ntk`'s `Surface` hands out one of these when the app it is
4
+ // given is a win32 app (the app's `createSurface` seam, src/win32/app.js),
5
+ // so a component allocates its buffer the same way on every backend and
6
+ // names none of them:
7
+ //
8
+ // const surface = new Surface(app, { width, height }); // device pixels
9
+ // const ctx = surface.getContext('2d'); // a BackendContext2D
10
+ // ctx.fillRect(0, 0, width, height);
11
+ // windowCtx.drawImage(surface, x, y); // one composite
12
+ //
13
+ // What is the same as ntk's: the constructor, `width`/`height`/`format`/
14
+ // `depth`/`bytes`, `getContext`, `render`, `clear`, `destroy`/
15
+ // `Symbol.dispose`, and `drawImage` taking the surface as a source. What
16
+ // differs is stated here, because this is where it lives:
17
+ //
18
+ // - **One graphics state per surface.** Direct2D keeps the transform on the
19
+ // device context and the clip as a stack on it, where an X connection
20
+ // keeps them per Picture/GC. So `getContext('2d')` answers the same
21
+ // context every time — a JS object over that one state, nothing to free,
22
+ // and `destroy()` on it is a no-op — and `render()` brackets its callback
23
+ // in save/restore from the identity transform, so a one-shot draw leaves
24
+ // no residue for the next painter, which is what ntk gets from building a
25
+ // fresh context per call.
26
+ // - **`format: 'a8'` is not here yet.** Every consumer that allocates a
27
+ // surface of its own asks for argb32. Asking for a8 throws rather than
28
+ // answering a colour surface that would composite differently.
29
+ // - **No Picture.** `picture()` is X's compositing handle; here a surface
30
+ // composites through `ctx.drawImage`, and asking for the picture says so.
31
+ // - **`copyWithin` refuses.** The bridge has `IDCompositionSurface::Scroll`
32
+ // for a *window*, which is a composition call and not a bitmap one, and
33
+ // no in-place blit for an offscreen bitmap. The contract already has an
34
+ // answer for that — false means "nothing survives the shift here" and the
35
+ // caller repaints the rect exactly as it would have without the method —
36
+ // so refusing is correct rather than merely convenient. A real scroll
37
+ // belongs here when the bridge grows one.
38
+ import { BackendContext2D } from '../backend/context2d.js';
39
+
40
+ export class Win32Surface {
41
+ constructor(app, { width, height, format = 'argb32' } = {}) {
42
+ if (
43
+ !Number.isInteger(width) ||
44
+ !Number.isInteger(height) ||
45
+ width <= 0 ||
46
+ height <= 0
47
+ ) {
48
+ throw new Error('Surface: width and height must be positive integers');
49
+ }
50
+ if (format === 'a8') {
51
+ throw new Error(
52
+ "Surface: format 'a8' (a coverage surface) is not on the win32 " +
53
+ 'backend yet — allocate argb32, which every backend has, and ' +
54
+ 'tint through fillStyle/globalAlpha.',
55
+ );
56
+ }
57
+ if (format !== 'argb32') {
58
+ throw new Error(
59
+ `Surface: unknown format ${JSON.stringify(format)} (argb32 or a8)`,
60
+ );
61
+ }
62
+ this.app = app;
63
+ this.width = width;
64
+ this.height = height;
65
+ this.format = format;
66
+ this.depth = 32;
67
+ this._native = app._native;
68
+ this._fonts = app.fonts ?? null;
69
+ this._ctx = null;
70
+ this._destroyed = false;
71
+ // The bridge clears the bitmap as it makes it: a fresh one's contents
72
+ // are the allocator's, and a surface that is only partly drawn must
73
+ // composite nothing where nothing was drawn.
74
+ this._surfaceHandle = this._native.createSurface(
75
+ width,
76
+ height,
77
+ app.scale ?? 1,
78
+ );
79
+ }
80
+
81
+ /** bytes of backing storage — what a cache budgets against */
82
+ get bytes() {
83
+ return this.width * this.height * 4;
84
+ }
85
+
86
+ /** X's compositing handle, which this backend does not have. */
87
+ picture() {
88
+ throw new Error(
89
+ 'Surface: a surface on the win32 backend has no XRender Picture — ' +
90
+ 'composite it with ctx.drawImage(surface, x, y), which takes a ' +
91
+ 'surface directly on every backend.',
92
+ );
93
+ }
94
+
95
+ _handle() {
96
+ if (this._destroyed) {
97
+ throw new Error(
98
+ 'Surface: destroyed — a context on a destroyed surface cannot ' +
99
+ 'draw; allocate a new Surface and draw into that.',
100
+ );
101
+ }
102
+ return this._surfaceHandle;
103
+ }
104
+
105
+ _context() {
106
+ if (!this._ctx) {
107
+ this._ctx = new BackendContext2D(
108
+ this._native,
109
+ () => this._handle(),
110
+ () => 1,
111
+ );
112
+ this._ctx._fonts = this._fonts;
113
+ }
114
+ return this._ctx;
115
+ }
116
+
117
+ /**
118
+ * The 2d context on the bitmap — the same one every time, since the
119
+ * bitmap has one graphics state (see the header). ntk's contract has the
120
+ * caller owning it and owing it a `destroy()`; that call is honoured as a
121
+ * no-op, so a caller written against ntk needs no branch.
122
+ */
123
+ getContext(name = '2d') {
124
+ this._handle();
125
+ if (name !== '2d') {
126
+ throw new Error(
127
+ `Surface: getContext(${JSON.stringify(name)}) — a surface on the ` +
128
+ "win32 backend has a '2d' context and nothing else.",
129
+ );
130
+ }
131
+ return this._context();
132
+ }
133
+
134
+ /**
135
+ * Draw into the surface through a context that starts clean — identity
136
+ * transform, the fill and line state as they were — and leaves the
137
+ * surface's state as it found it: the save/restore bracket stands in for
138
+ * the per-call context ntk builds and destroys.
139
+ */
140
+ render(fn) {
141
+ const ctx = this.getContext('2d');
142
+ ctx.save();
143
+ try {
144
+ ctx.resetTransform();
145
+ fn(ctx);
146
+ } finally {
147
+ ctx.restore();
148
+ }
149
+ return this;
150
+ }
151
+
152
+ /** Reset every pixel to fully transparent, whatever transform a live
153
+ * context holds — the clear is issued from the identity. */
154
+ clear() {
155
+ if (this._destroyed) return this;
156
+ const ctx = this._context();
157
+ ctx.save();
158
+ try {
159
+ ctx.resetTransform();
160
+ ctx.clearRect(0, 0, this.width, this.height);
161
+ } finally {
162
+ ctx.restore();
163
+ }
164
+ return this;
165
+ }
166
+
167
+ /**
168
+ * Shift the pixels of `src` in place. Always refused here, which is a
169
+ * value the contract already has: false means "nothing survives the shift"
170
+ * and the caller repaints `src` as it would have anyway. See the header.
171
+ */
172
+ copyWithin() {
173
+ return false;
174
+ }
175
+
176
+ destroy() {
177
+ if (this._destroyed) return;
178
+ this._destroyed = true;
179
+ const handle = this._surfaceHandle;
180
+ this._surfaceHandle = null;
181
+ this._ctx = null;
182
+ if (typeof this._native.releaseSurface === 'function') {
183
+ this._native.releaseSurface(handle);
184
+ }
185
+ }
186
+
187
+ [Symbol.dispose]() {
188
+ this.destroy();
189
+ }
190
+ }
191
+
192
+ export default Win32Surface;