ntk 5.2.0 → 5.4.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/lib/window.js CHANGED
@@ -2,6 +2,7 @@ import x11 from 'x11';
2
2
 
3
3
  import { safeRelease } from './cleanup.js';
4
4
  import Drawable from './drawable.js';
5
+ import { packIcons, unpackIcons } from './imagedata.js';
5
6
  import Pixmap from './pixmap.js';
6
7
  import * as xevents from './events_map.js';
7
8
  import { decodeKey } from './keyboard.js';
@@ -432,6 +433,11 @@ export default class Window extends Drawable {
432
433
  if (args.windowType) {
433
434
  this.setWindowType(args.windowType);
434
435
  }
436
+ if (args.icon) {
437
+ // needs the atom interned first, so unlike the writers above it is not
438
+ // on the wire by the time createWindow returns
439
+ this.setIcon(args.icon).catch((err) => app.options.onXError?.(err));
440
+ }
435
441
  // ICCCM/EWMH hints at creation: `hints: { ... }`, the `sizeHints`/
436
442
  // `resizable` arguments that predate it, or the hint names on their own
437
443
  // at the top level. All of them land in one setHints call so that a
@@ -505,6 +511,9 @@ export default class Window extends Drawable {
505
511
  // them in the order they happened (a drag sees the move, then the up)
506
512
  this._flushCoalesced();
507
513
  this.emit(eventName, ntkev);
514
+ // a WM_DELETE_WINDOW ClientMessage is the window manager *asking*, and
515
+ // 'close' is that question in a form an application can answer
516
+ if (eventName === 'message') this._emitCloseRequest(ntkev);
508
517
  // anything drawn during the handlers becomes visible in one blit
509
518
  if (this._dirty) this._present();
510
519
  });
@@ -527,6 +536,18 @@ export default class Window extends Drawable {
527
536
  });
528
537
 
529
538
  this.on('newListener', (name) => {
539
+ // Listening for 'close' is the opt-in. WM_DELETE_WINDOW only reaches a
540
+ // client that advertised it in WM_PROTOCOLS — a window manager kills
541
+ // anyone else outright — and having to know that, on top of decoding a
542
+ // ClientMessage by hand, is the protocol showing through the toolkit.
543
+ if (name === 'close' && !this._closeArmed) {
544
+ this._closeArmed = true;
545
+ // interning WM_PROTOCOLS here as well as in addProtocol: the
546
+ // dispatch path matches against both atoms and node-x11 caches them
547
+ // per connection, so this costs one round trip and no more
548
+ this.atom('WM_PROTOCOLS').catch(() => {});
549
+ this.addProtocol('WM_DELETE_WINDOW').catch((err) => this.app.options.onXError?.(err));
550
+ }
530
551
  // 'statechange' is derived from a PropertyNotify, so it needs the atom
531
552
  // to compare against as well as the mask the table below selects
532
553
  if (name === 'statechange' && !this._netWmStateAtom) {
@@ -634,7 +655,7 @@ export default class Window extends Drawable {
634
655
  });
635
656
  if (!this._clearGc) {
636
657
  // reusable across reallocs: a GC is valid for any drawable of the
637
- // same screen and depth (node-x11 has no FreeGC request)
658
+ // same screen and depth, so one outlives every backing pixmap
638
659
  this._clearGc = this.X.AllocID();
639
660
  this.X.CreateGC(this._clearGc, pixmap.id, {
640
661
  foreground: this.display.screen[0].white_pixel,
@@ -1415,6 +1436,9 @@ export default class Window extends Drawable {
1415
1436
  * atom name: `'WM_DELETE_WINDOW'`, `'WM_TAKE_FOCUS'`, `'_NET_WM_PING'`,
1416
1437
  * `'_NET_WM_SYNC_REQUEST'`. Each arrives as a `'message'` event.
1417
1438
  *
1439
+ * `WM_DELETE_WINDOW` needs none of this by hand: listening for the
1440
+ * `'close'` event adds the protocol and decodes the message for you.
1441
+ *
1418
1442
  * Replaces the whole list. `addProtocol`/`removeProtocol` are the
1419
1443
  * accumulating forms, and the ones to reach for: the property is a *set*,
1420
1444
  * and a plain write of one atom silently drops the rest.
@@ -1528,6 +1552,82 @@ export default class Window extends Drawable {
1528
1552
  });
1529
1553
  }
1530
1554
 
1555
+ /**
1556
+ * EWMH `_NET_WM_ICON` — the icon a taskbar, alt-tab switcher or titlebar
1557
+ * draws for this window.
1558
+ *
1559
+ * wnd.setIcon(await loadImage('icon-48.png'));
1560
+ * wnd.setIcon([icon16, icon32, icon48]);
1561
+ * wnd.setIcon(await ctx.getImageData(0, 0, 64, 64));
1562
+ * wnd.setIcon(null); // remove it
1563
+ *
1564
+ * An image is an ntk [`Image`](images.md), an `ImageData`, or anything
1565
+ * with `{ width, height, data }` where `data` is straight
1566
+ * (non-premultiplied) RGBA — the same contract as the rest of ntk, so
1567
+ * whatever `loadImage()` or `getImageData()` gives you goes straight in.
1568
+ *
1569
+ * Passing several is the useful case: supply the sizes you have and the
1570
+ * window manager picks whichever suits the slot it is filling, instead of
1571
+ * scaling a single one badly. Nothing requires a particular order or a
1572
+ * particular set of sizes; 16, 32 and 48 cover most desktops.
1573
+ *
1574
+ * This is the modern mechanism. `setWmHints({ icon })` writes the ICCCM
1575
+ * pixmap instead, which is 1-bit-or-matched-depth and has no alpha; a
1576
+ * window manager that understands both prefers this one.
1577
+ *
1578
+ * @param {object|object[]|null} images
1579
+ * @returns {Promise<Window>}
1580
+ */
1581
+ async setIcon(images) {
1582
+ const list = images == null ? [] : Array.isArray(images) ? images : [images];
1583
+ if (!list.length) return this.deleteProperty('_NET_WM_ICON');
1584
+
1585
+ const data = packIcons(list, this.app.display);
1586
+ const property = await this.atom('_NET_WM_ICON');
1587
+ const cardinal = this.X.atoms.CARDINAL;
1588
+ if (this._destroyed) return this;
1589
+
1590
+ // A ChangeProperty carries 24 bytes of header, so a full icon set can
1591
+ // outrun the request limit on a connection created with
1592
+ // `disableBigRequests` — 256x256 alone is 262152 bytes against a 262140
1593
+ // cap. Replace with the first chunk, append the rest.
1594
+ const maxBytes = ((this.app.display.max_request_length ?? 65535) - 6) * 4;
1595
+ safeRelease(this.X, () => {
1596
+ for (let o = 0; o < data.length; o += maxBytes) {
1597
+ this.X.ChangeProperty(
1598
+ o === 0 ? 0 : 2, // Replace, then Append
1599
+ this.id,
1600
+ property,
1601
+ cardinal,
1602
+ 32,
1603
+ data.subarray(o, Math.min(o + maxBytes, data.length))
1604
+ );
1605
+ }
1606
+ });
1607
+ return this;
1608
+ }
1609
+
1610
+ /**
1611
+ * Read `_NET_WM_ICON` back — every size the window advertises, largest
1612
+ * last or in whatever order its client wrote them, as `ImageData`.
1613
+ *
1614
+ * Useful on your own window, and the point of it on someone else's: a
1615
+ * window manager drawing titlebars reads this off each client it frames.
1616
+ * Property bytes from another client are not to be trusted, so a
1617
+ * truncated or nonsensical run yields the images that parsed cleanly
1618
+ * rather than throwing.
1619
+ *
1620
+ * Resolves to `null` when the window has no icon.
1621
+ *
1622
+ * @returns {Promise<ImageData[]|null>}
1623
+ */
1624
+ async getIcon() {
1625
+ const prop = await this.getProperty('_NET_WM_ICON');
1626
+ if (!prop || !prop.data.length) return null;
1627
+ const icons = unpackIcons(prop.data, this.app.display);
1628
+ return icons.length ? icons : null;
1629
+ }
1630
+
1531
1631
  /** os.hostname(), looked up once, and absent in a browser bundle. */
1532
1632
  static _hostname() {
1533
1633
  if (Window._cachedHostname === undefined) {
@@ -2363,6 +2463,42 @@ export default class Window extends Drawable {
2363
2463
  return this;
2364
2464
  }
2365
2465
 
2466
+ /**
2467
+ * Turn a WM_DELETE_WINDOW ClientMessage into the `close` event.
2468
+ *
2469
+ * Only fires when something is listening, so a window that handles the
2470
+ * raw `message` event itself — the only way to do this before `close`
2471
+ * existed — keeps behaving exactly as it did, rather than suddenly
2472
+ * acquiring a second handler that destroys it.
2473
+ */
2474
+ _emitCloseRequest(ev) {
2475
+ if (ev.format !== 32 || !this.listenerCount('close')) return;
2476
+ // node-x11 caches interned atoms per connection, so these are the ids
2477
+ // the 'close' listener's addProtocol already interned
2478
+ const X = this.X;
2479
+ if (!X.atoms.WM_PROTOCOLS || ev.message_type !== X.atoms.WM_PROTOCOLS) return;
2480
+ if (!X.atoms.WM_DELETE_WINDOW || ev.data?.[0] !== X.atoms.WM_DELETE_WINDOW) return;
2481
+
2482
+ let prevented = false;
2483
+ this.emit('close', {
2484
+ name: 'close',
2485
+ window: this,
2486
+ target: this,
2487
+ // the timestamp the window manager stamped the request with, for
2488
+ // passing back to setInputFocus and friends
2489
+ time: ev.data[1],
2490
+ preventDefault() {
2491
+ prevented = true;
2492
+ },
2493
+ get defaultPrevented() {
2494
+ return prevented;
2495
+ }
2496
+ });
2497
+ // the default action, and the reason preventDefault has to be called
2498
+ // synchronously: there is no point at which it could be awaited
2499
+ if (!prevented && !this._destroyed) this.destroy();
2500
+ }
2501
+
2366
2502
  destroy() {
2367
2503
  this._destroyed = true;
2368
2504
  this._forget();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ntk",
3
- "version": "5.2.0",
3
+ "version": "5.4.0",
4
4
  "description": "Desktop UI toolkit for X11 with canvas-like 2d and OpenGL rendering",
5
5
  "author": "Andrey Sidorov <sidorares@yandex.ru>",
6
6
  "license": "MIT",