ntk 8.1.1 → 8.3.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
@@ -332,6 +332,24 @@ function warnHint(key, message) {
332
332
  console.warn(`ntk: ${message} Further occurrences are not reported.`);
333
333
  }
334
334
 
335
+ /**
336
+ * A GetGeometry reply in ntk's spelling. node-x11 calls the position `xPos`/
337
+ * `yPos` and the reply's root window `windowid`, neither of which is what X
338
+ * calls them; the `root` here is the root of the screen the drawable is on,
339
+ * not this window's parent.
340
+ */
341
+ function unpackGeometry(res) {
342
+ return {
343
+ x: res.xPos,
344
+ y: res.yPos,
345
+ width: res.width,
346
+ height: res.height,
347
+ depth: res.depth,
348
+ borderWidth: res.borderWidth,
349
+ root: res.windowid
350
+ };
351
+ }
352
+
335
353
  /** A Window, a Pixmap, or a bare XID — all of these name a server resource. */
336
354
  function resourceId(value) {
337
355
  return typeof value === 'number' ? value : (value?.id ?? 0);
@@ -404,18 +422,37 @@ export default class Window extends Drawable {
404
422
  // the whole property without dropping what an earlier one set
405
423
  this._sizeHints = null;
406
424
  this._wmHints = null;
425
+ // `wnd.ready` (see the getter): resolved as soon as this wrapper knows
426
+ // its geometry — at the end of the constructor for a window ntk created,
427
+ // when GetGeometry replies for one adopted by id
407
428
  this._readyPromiseResolve = null;
408
429
  this._readyPromise = new Promise((resolve) => {
409
430
  this._readyPromiseResolve = resolve;
410
431
  });
432
+ // constructor round trips still outstanding, for an adopted window
433
+ this._readyPending = 0;
411
434
 
412
435
  const parentId = args.parent ? args.parent.id : app.display.screen[0].root;
413
436
 
414
437
  // double buffering (see docs/window.md): windows we created get a
415
438
  // backing pixmap when a 2d context is requested, unless opted out
416
439
  this._foreign = !!args.id;
440
+ // Adopting a window is not the same as "another client paints these
441
+ // pixels", though it is the safe default reading of one. The Composite
442
+ // overlay window and a window handed over by an embedding host (XEmbed's
443
+ // plug side, an `--wid` handoff) are ours completely — they are foreign
444
+ // only in the sense that `createWindow` did not make them, and they are
445
+ // exactly the windows that most want an unflickering double buffer and a
446
+ // vblank-paced blit. Saying so is `{ id, backingStore: true }` or
447
+ // `{ id, present: true }`; from there such a window behaves like any
448
+ // other window ntk owns (issue #294).
449
+ this._ownsPixels = !this._foreign || args.backingStore === true || args.present === true;
417
450
  this._backingOptOut = args.backingStore === false;
418
451
  this._backing = null;
452
+ // an adopted window has no geometry, depth or visual to build a backing
453
+ // pixmap from until its constructor's questions reply — see
454
+ // _enableBackingStoreWhenReady
455
+ this._backingScheduled = false;
419
456
  // What the backing store clears to, and the window attribute it mirrors.
420
457
  // Undefined means "nothing was asked for" — see _clearColour.
421
458
  this._clearPixel = args.backgroundPixel;
@@ -528,11 +565,19 @@ export default class Window extends Drawable {
528
565
  for (const name of forwardedXAttributes) {
529
566
  if (args[name] !== undefined) values[name] = args[name];
530
567
  }
568
+ // what setCursor compares against, so setting the cursor a window was
569
+ // created with is free rather than a redundant round trip
570
+ this._currentCursor = values.cursor;
531
571
  // a window on a non-default visual (GLX, ARGB) also needs its own
532
572
  // colormap and an explicit border pixel: inheriting either from a
533
573
  // parent of a different depth is a BadMatch (X11 CreateWindow)
534
574
  this.visual = args.visual ?? 0;
535
575
  this.depth = args.depth ?? 0;
576
+ // `visual` is the CreateWindow argument, where 0 means CopyFromParent;
577
+ // `visualId` is the visual the pixels are actually in, which is what a
578
+ // picture format is picked from (issue #295). CopyFromParent is only
579
+ // ever the parent's, and the default parent is the root.
580
+ this.visualId = this.visual || args.parent?.visualId || app.display.screen[0].root_visual;
536
581
  if (this.visual) {
537
582
  if (values.colormap === undefined) {
538
583
  this._ownedColormap = app.createColormap(this.visual);
@@ -549,23 +594,33 @@ export default class Window extends Drawable {
549
594
  this.id = args.id;
550
595
  this.eventMask = 0;
551
596
  this.visual = 0;
597
+ // nothing is known about another client's window until it answers
598
+ this.visualId = 0;
552
599
  this.depth = 0;
553
600
  // an adopted window's geometry is not known until GetGeometry replies
554
601
  this._deliveredGeom = null;
555
- // populate width and height
602
+ // Two questions, because a drawable's pixels take both to describe:
603
+ // GetGeometry for width, height and depth, GetWindowAttributes for the
604
+ // visual — depth alone does not name a picture format, and the windows
605
+ // whose format was picked wrongest are exactly these, the ones another
606
+ // client chose the visual for (issue #295). They go out in the same
607
+ // batch, so the pair costs one round trip, not two, and `ready` is the
608
+ // wait for both.
609
+ this._readyPending = 2;
556
610
  X.GetGeometry(this.id, (err, res) => {
557
- if (!err) {
558
- this.width = res.width;
559
- this.height = res.height;
560
- this.x = res.xPos;
561
- this.y = res.yPos;
562
- this.depth = res.depth;
563
- // nothing was known about this window until now, so until this
564
- // reply lands a 'resize' reports both moved and resized (see
565
- // _tagResize) rather than guessing
566
- this._deliveredGeom ??= { x: this.x, y: this.y, width: this.width, height: this.height };
567
- }
568
- this._readyPromiseResolve();
611
+ // A window that was destroyed between the id reaching us and this
612
+ // request reaching the server answers BadWindow, and there is
613
+ // nothing to record. `ready` still resolves: adopting a window that
614
+ // has since gone is ordinary for a window manager, and a wait that
615
+ // never ends is worse than one that ends with width undefined.
616
+ this._readyPending--;
617
+ if (err) this._settleReady();
618
+ else this._applyGeometry(unpackGeometry(res));
619
+ });
620
+ X.GetWindowAttributes(this.id, (err, attrs) => {
621
+ this._readyPending--;
622
+ if (!err) this.visualId = attrs.visual;
623
+ this._settleReady();
569
624
  });
570
625
  }
571
626
 
@@ -630,11 +685,21 @@ export default class Window extends Drawable {
630
685
  // region and an event selection it never uses. `present: true` is still
631
686
  // honoured eagerly, for a caller who wants it up before the first paint.
632
687
  this._presentOptOut = args.present === false;
633
- if (args.present && !args.id) {
688
+ if (args.present === true) {
634
689
  // fire and forget: until the extensions answer, blits use CopyArea,
635
- // which is what they would have done anyway
690
+ // which is what they would have done anyway. Present needs nothing of
691
+ // the window but its id, so an adopted one can have it up before its
692
+ // geometry has even replied.
636
693
  this.enablePresent().catch((err) => this.app.options?.onXError?.(err));
637
694
  }
695
+ // A window adopted with ownership declared starts its backing store now
696
+ // rather than waiting for the first getContext('2d'): the allocation
697
+ // needs the constructor's replies anyway, so starting it here means it
698
+ // is normally already there by the time the caller's `await wnd.ready`
699
+ // returns and it draws.
700
+ if (this._foreign && this._ownsPixels && !this._backingOptOut) {
701
+ this._enableBackingStoreWhenReady();
702
+ }
638
703
 
639
704
  X.event_consumers[this.id] = this;
640
705
  this.on('event', (ev) => {
@@ -717,7 +782,7 @@ export default class Window extends Drawable {
717
782
  const child = new Window(app, { id: ev.wid });
718
783
  const ntkev = { ...ev, parent: this, window: child, target: child };
719
784
  // wait until we know that we track correct x,y,w,h values
720
- child._readyPromise.then(() => {
785
+ child.ready.then(() => {
721
786
  this.emit(eventName, ntkev);
722
787
  });
723
788
  });
@@ -773,10 +838,82 @@ export default class Window extends Drawable {
773
838
  });
774
839
 
775
840
  if (typeof this.width !== 'undefined') {
776
- this._readyPromiseResolve();
841
+ this._settleReady();
777
842
  }
778
843
  }
779
844
 
845
+ /**
846
+ * Resolves with this window once its geometry is known — immediately for a
847
+ * window ntk created (it already knows what it asked for), when the
848
+ * GetGeometry sent by the constructor replies for one adopted by id.
849
+ *
850
+ * Adoption is the case that needs it: until the reply lands, `width`,
851
+ * `height`, `x` and `y` are `undefined` and `depth` is `0`, so anything
852
+ * that picks a pixel format from the depth — `getContext('2d')`,
853
+ * `createPattern` — would pick the wrong one. Awaiting is safe
854
+ * unconditionally, so code that takes windows from either source need not
855
+ * ask which it has:
856
+ *
857
+ * const wnd = new Window(app, { id: ev.wid });
858
+ * await wnd.ready;
859
+ * const ctx = wnd.getContext('2d');
860
+ *
861
+ * The promise never rejects. A window destroyed before the reply arrives
862
+ * resolves all the same, with `width` still `undefined` — the request that
863
+ * hits the gone window is the one that reports it.
864
+ */
865
+ get ready() {
866
+ return this._readyPromise;
867
+ }
868
+
869
+ /**
870
+ * Ask the server where this window is now — `{ x, y, width, height, depth,
871
+ * borderWidth, root }`, with `x`/`y` relative to the parent, as X reports
872
+ * them.
873
+ *
874
+ * `wnd.x`/`y`/`width`/`height`/`depth` are kept current from the event
875
+ * stream and are the cheap answer; this is the round trip for the cases
876
+ * the events cannot cover — a window nobody selected StructureNotify on, a
877
+ * window ntk created with the default `depth: 0` (CopyFromParent, whose
878
+ * real depth only the server knows), or simply the state at a point in
879
+ * time. The reply is written back to those properties, and resolves
880
+ * `ready` if it was still pending.
881
+ */
882
+ getGeometry() {
883
+ return new Promise((resolve, reject) => {
884
+ this.X.GetGeometry(this.id, (err, res) => {
885
+ if (err) return reject(err);
886
+ const geometry = unpackGeometry(res);
887
+ this._applyGeometry(geometry);
888
+ resolve(geometry);
889
+ });
890
+ });
891
+ }
892
+
893
+ /** Record a GetGeometry reply, and let anything waiting on it go. */
894
+ _applyGeometry({ x, y, width, height, depth }) {
895
+ this.x = x;
896
+ this.y = y;
897
+ this.width = width;
898
+ this.height = height;
899
+ this.depth = depth;
900
+ // Only where nothing is known yet: on an adopted window a 'resize'
901
+ // reports both moved and resized until there is a baseline to compare
902
+ // against (see _tagResize), and the event stream may already have set
903
+ // one — that is the delivered geometry, and this reply is not.
904
+ this._deliveredGeom ??= { x, y, width, height };
905
+ this._settleReady();
906
+ }
907
+
908
+ /**
909
+ * Let anything waiting on `ready` go, once the questions the constructor
910
+ * asked have all been answered. A window ntk created asked none.
911
+ */
912
+ _settleReady() {
913
+ if (this._readyPending > 0) return;
914
+ this._readyPromiseResolve(this);
915
+ }
916
+
780
917
  _forget() {
781
918
  Window._cacheFor(this.app).delete(this.id);
782
919
  delete this.X.event_consumers[this.id];
@@ -790,14 +927,43 @@ export default class Window extends Drawable {
790
927
  * an 'expose' (and 'draw') event is emitted only when a real redraw is
791
928
  * needed (first paint, resize). Opt out with
792
929
  * `createWindow({ backingStore: false })`.
930
+ *
931
+ * A window adopted by id is left alone by default — another client's
932
+ * pixels are not ntk's to buffer — and opts in with
933
+ * `createWindow({ id, backingStore: true })` (see `_ownsPixels`).
793
934
  */
794
935
  getContext(name, ...args) {
795
- if (name === '2d' && !this._backing && !this._backingOptOut && !this._foreign) {
796
- this._enableBackingStore();
936
+ if (name === '2d' && !this._backing && !this._backingOptOut && this._ownsPixels) {
937
+ if (this._foreign) this._enableBackingStoreWhenReady();
938
+ else this._enableBackingStore();
797
939
  }
798
940
  return super.getContext(name, ...args);
799
941
  }
800
942
 
943
+ /**
944
+ * The backing store for an adopted window, once there is something to build
945
+ * it out of.
946
+ *
947
+ * A pixmap needs a width, a height, a depth and a visual, and an adopted
948
+ * window knows none of them until `GetGeometry` and `GetWindowAttributes`
949
+ * reply — so the allocation waits on `ready`. A 2d context taken in the
950
+ * meantime draws straight to the window and re-binds itself to the pixmap
951
+ * when it appears (the `_backing` event), which is the same path a resize
952
+ * reallocation takes.
953
+ */
954
+ _enableBackingStoreWhenReady() {
955
+ if (this._backingScheduled) return;
956
+ this._backingScheduled = true;
957
+ this.ready.then(() => {
958
+ // A window destroyed before the replies arrived still settles `ready`,
959
+ // with its geometry never filled in — there is no pixmap to make for
960
+ // one, and CreatePixmap with an undefined size is not the way to find
961
+ // that out.
962
+ if (this._destroyed || this._backing || !this.width || !this.height) return;
963
+ this._enableBackingStore();
964
+ });
965
+ }
966
+
801
967
  _enableBackingStore() {
802
968
  this._dirty = false;
803
969
  this._backingValid = false;
@@ -805,7 +971,7 @@ export default class Window extends Drawable {
805
971
  // Now there is a pixmap to present. Inert where the extensions are
806
972
  // missing, so this costs a window on such a server two extension queries
807
973
  // that node-x11 answers from its cache after the first.
808
- if (!this._presentOptOut && !this._foreign) {
974
+ if (!this._presentOptOut && this._ownsPixels) {
809
975
  this.enablePresent().catch((err) => this.app.options?.onXError?.(err));
810
976
  }
811
977
 
@@ -817,7 +983,8 @@ export default class Window extends Drawable {
817
983
  // server sends them even if user code only listens to 'draw'
818
984
  if (!(this.eventMask & x11.eventMask.Exposure)) {
819
985
  this.eventMask |= x11.eventMask.Exposure;
820
- this.X.ChangeWindowAttributes(this.id, { eventMask: this.eventMask }, () => {});
986
+ // no callback: it would only buy node-x11's void-sync round trip, see setCursor
987
+ this.X.ChangeWindowAttributes(this.id, { eventMask: this.eventMask });
821
988
  }
822
989
 
823
990
  // a pure move is a ConfigureNotify too, and reallocating a backing
@@ -863,8 +1030,11 @@ export default class Window extends Drawable {
863
1030
  this._clearPixel = pixel;
864
1031
  if (this._destroyed) return this;
865
1032
  if (this._clearGc) this.X.ChangeGC(this._clearGc, { foreground: pixel });
866
- if (!this._foreign) {
867
- this.X.ChangeWindowAttributes(this.id, { backgroundPixel: pixel }, () => {});
1033
+ // The window attribute half is only ours to write on a window whose
1034
+ // pixels are ours — an adopted one has to have said so (see _ownsPixels).
1035
+ if (this._ownsPixels) {
1036
+ // no callback: it would only buy node-x11's void-sync round trip, see setCursor
1037
+ this.X.ChangeWindowAttributes(this.id, { backgroundPixel: pixel });
868
1038
  }
869
1039
  const backing = this._backing;
870
1040
  if (backing && this._clearGc) {
@@ -896,7 +1066,10 @@ export default class Window extends Drawable {
896
1066
  parent: this,
897
1067
  width: newW,
898
1068
  height: newH,
899
- depth
1069
+ depth,
1070
+ // it holds this window's pixels, so it is read through this window's
1071
+ // visual — a pixmap has none of its own (issue #295)
1072
+ visual: this.visualId
900
1073
  });
901
1074
  if (!this._clearGc) {
902
1075
  this._clearGc = this.X.AllocID();
@@ -2997,9 +3170,19 @@ export default class Window extends Drawable {
2997
3170
  * pointer stays visible
2998
3171
  */
2999
3172
  setCursor(nameOrShape) {
3173
+ // resolved first: an unknown name has to throw whether or not the memo
3174
+ // below short-circuits the request
3000
3175
  const cursor = nameOrShape == null ? 0 : this.app.cursors.get(nameOrShape);
3176
+ if (cursor === this._currentCursor) return this;
3177
+ this._currentCursor = cursor;
3001
3178
  safeRelease(this.X, () => {
3002
- this.X.ChangeWindowAttributes(this.id, { cursor }, () => {});
3179
+ // no callback, deliberately: node-x11 guarantees a callback on a *void*
3180
+ // request eventually fires, and buys that by injecting a GetInputFocus
3181
+ // round trip when nothing else in the tick expects a reply (node-x11
3182
+ // issue #85). A callback that ignores its argument costs that round trip
3183
+ // per cursor change and buys nothing — an X error reaches
3184
+ // `client.emit('error')`, and so app's onXError hook, either way.
3185
+ this.X.ChangeWindowAttributes(this.id, { cursor });
3003
3186
  });
3004
3187
  return this;
3005
3188
  }
@@ -3012,7 +3195,8 @@ export default class Window extends Drawable {
3012
3195
  } else {
3013
3196
  return this;
3014
3197
  }
3015
- this.X.ChangeWindowAttributes(this.id, { eventMask: this.eventMask }, () => {});
3198
+ // no callback: it would only buy node-x11's void-sync round trip, see setCursor
3199
+ this.X.ChangeWindowAttributes(this.id, { eventMask: this.eventMask });
3016
3200
  return this;
3017
3201
  }
3018
3202
 
@@ -3307,7 +3491,13 @@ export default class Window extends Drawable {
3307
3491
  getAttributes() {
3308
3492
  return new Promise((resolve, reject) => {
3309
3493
  safeRelease(this.X, () =>
3310
- this.X.GetWindowAttributes(this.id, (err, attrs) => (err ? reject(err) : resolve(attrs)))
3494
+ this.X.GetWindowAttributes(this.id, (err, attrs) => {
3495
+ if (err) return reject(err);
3496
+ // the visual names the picture format (see `visualId`), and this
3497
+ // reply is the only place the server ever states it
3498
+ this.visualId = attrs.visual;
3499
+ resolve(attrs);
3500
+ })
3311
3501
  );
3312
3502
  });
3313
3503
  }
@@ -3515,7 +3705,7 @@ export default class Window extends Drawable {
3515
3705
  // there can be multiple screens (and roots)
3516
3706
  const root = new Window(app, { id: tree.root });
3517
3707
  const wrappers = [...children, root, ...(parent ? [parent] : [])];
3518
- Promise.all(wrappers.map((w) => w._readyPromise)).then(() => {
3708
+ Promise.all(wrappers.map((w) => w.ready)).then(() => {
3519
3709
  callback(null, { parent, root, children });
3520
3710
  });
3521
3711
  });
package/lib/xembed.js CHANGED
@@ -559,7 +559,9 @@ export class XEmbedSocket extends EventEmitter {
559
559
  // reparent, so its ReparentNotify does not come back to us
560
560
  safeRelease(this.X, () => {
561
561
  client.eventMask = 0;
562
- this.X.ChangeWindowAttributes(client.id, { eventMask: 0 }, () => {});
562
+ // no callback: it would only buy node-x11's void-sync round trip, see
563
+ // Window#setCursor
564
+ this.X.ChangeWindowAttributes(client.id, { eventMask: 0 });
563
565
  });
564
566
  client.reparentTo(this.app.rootWindow(), x, y);
565
567
  client.removeFromSaveSet();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ntk",
3
- "version": "8.1.1",
3
+ "version": "8.3.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",
@@ -44,7 +44,7 @@
44
44
  "linebreak": "^1.1.0",
45
45
  "parse-color": "^1.0.0",
46
46
  "pngjs": "^7.0.0",
47
- "x11": "^3.9.0"
47
+ "x11": "^4.0.1"
48
48
  },
49
49
  "optionalDependencies": {
50
50
  "x11-dri": ">=0.2.0 <1"