ntk 8.2.0 → 8.3.1

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
@@ -1,7 +1,7 @@
1
1
  import x11 from 'x11';
2
2
 
3
3
  import { builtin } from './builtin.js';
4
- import { safeRelease } from './cleanup.js';
4
+ import { connectionGone, safeRelease } from './cleanup.js';
5
5
  import Drawable from './drawable.js';
6
6
  import { packIcons, unpackIcons } from './imagedata.js';
7
7
  import Pixmap from './pixmap.js';
@@ -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,24 +594,46 @@ 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
556
- 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();
569
- });
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
+ //
610
+ // Nothing is asked of a connection that is going away: node-x11 throws
611
+ // synchronously at a request once close() has begun, and adoption
612
+ // happens from places with no caller to catch — a routed child-event,
613
+ // XEmbed, the shared glyph cache all build windows from inside the X
614
+ // event dispatch (issue #321). There is nothing to learn there either,
615
+ // so `ready` settles with the geometry unknown, the same outcome as a
616
+ // window destroyed before its reply landed (see the getter).
617
+ this._readyPending = connectionGone(X) ? 0 : 2;
618
+ if (this._readyPending) {
619
+ X.GetGeometry(this.id, (err, res) => {
620
+ // A window that was destroyed between the id reaching us and this
621
+ // request reaching the server answers BadWindow, and there is
622
+ // nothing to record. `ready` still resolves: adopting a window that
623
+ // has since gone is ordinary for a window manager, and a wait that
624
+ // never ends is worse than one that ends with width undefined.
625
+ this._readyPending--;
626
+ if (err) this._settleReady();
627
+ else this._applyGeometry(unpackGeometry(res));
628
+ });
629
+ X.GetWindowAttributes(this.id, (err, attrs) => {
630
+ this._readyPending--;
631
+ if (!err) this.visualId = attrs.visual;
632
+ this._settleReady();
633
+ });
634
+ } else {
635
+ this._settleReady();
636
+ }
570
637
  }
571
638
 
572
639
  Window._cacheFor(app).set(this.id, this);
@@ -630,11 +697,21 @@ export default class Window extends Drawable {
630
697
  // region and an event selection it never uses. `present: true` is still
631
698
  // honoured eagerly, for a caller who wants it up before the first paint.
632
699
  this._presentOptOut = args.present === false;
633
- if (args.present && !args.id) {
700
+ if (args.present === true) {
634
701
  // fire and forget: until the extensions answer, blits use CopyArea,
635
- // which is what they would have done anyway
702
+ // which is what they would have done anyway. Present needs nothing of
703
+ // the window but its id, so an adopted one can have it up before its
704
+ // geometry has even replied.
636
705
  this.enablePresent().catch((err) => this.app.options?.onXError?.(err));
637
706
  }
707
+ // A window adopted with ownership declared starts its backing store now
708
+ // rather than waiting for the first getContext('2d'): the allocation
709
+ // needs the constructor's replies anyway, so starting it here means it
710
+ // is normally already there by the time the caller's `await wnd.ready`
711
+ // returns and it draws.
712
+ if (this._foreign && this._ownsPixels && !this._backingOptOut) {
713
+ this._enableBackingStoreWhenReady();
714
+ }
638
715
 
639
716
  X.event_consumers[this.id] = this;
640
717
  this.on('event', (ev) => {
@@ -714,15 +791,24 @@ export default class Window extends Drawable {
714
791
  this.on('child-event', (ev) => {
715
792
  const eventName = xevents.eventName[ev.type];
716
793
  if (!eventName) return;
794
+ // Events buffered before `close()` keep being dispatched after it, and
795
+ // a window this client already saw destroyed has no substructure left
796
+ // to report. Neither is worth building a child for — and the questions
797
+ // the constructor would ask on the way out are what used to throw into
798
+ // the dispatcher (issue #321).
799
+ if (this._destroyed || connectionGone(X)) return;
717
800
  const child = new Window(app, { id: ev.wid });
718
801
  const ntkev = { ...ev, parent: this, window: child, target: child };
719
802
  // wait until we know that we track correct x,y,w,h values
720
- child._readyPromise.then(() => {
803
+ child.ready.then(() => {
721
804
  this.emit(eventName, ntkev);
722
805
  });
723
806
  });
724
807
 
725
808
  this.on('newListener', (name) => {
809
+ // one of ntk's own handlers going on, not a caller asking for an
810
+ // event: nothing to arm and nothing to select — see _listenInternal
811
+ if (this._internalListen) return;
726
812
  // Listening for 'close' is the opt-in. WM_DELETE_WINDOW only reaches a
727
813
  // client that advertised it in WM_PROTOCOLS — a window manager kills
728
814
  // anyone else outright — and having to know that, on top of decoding a
@@ -749,6 +835,13 @@ export default class Window extends Drawable {
749
835
  const eventMask = xevents.mask[name];
750
836
  if (!eventMask) return;
751
837
  if ((eventMask & this.eventMask) === 0) {
838
+ // A connection on its way out can select nothing, and node-x11 throws
839
+ // synchronously at a request from then on. This hook runs from
840
+ // `.on()` — including the calls made while adopting a window from
841
+ // inside the X event dispatch, where there is no caller to catch it
842
+ // (issue #321). Before the mask is touched, so it goes on saying what
843
+ // this connection holds.
844
+ if (connectionGone(X)) return;
752
845
  this.eventMask |= eventMask;
753
846
  // the selection can legitimately fail — SubstructureRedirect is
754
847
  // one-client-only, so `on('map_request')` is how you find out
@@ -756,14 +849,29 @@ export default class Window extends Drawable {
756
849
  // error hook rather than dropping it; selectInput() is the
757
850
  // explicit form that hands the error straight back.
758
851
  X.ChangeWindowAttributes(this.id, { eventMask: this.eventMask }, (err) => {
759
- if (err) this.app.options.onXError?.(err);
852
+ if (!err) return;
853
+ // the request failed as a whole, so the server kept the mask it
854
+ // had: drop the bits again rather than leave `eventMask` claiming
855
+ // a selection this connection does not hold. selectInput() trusts
856
+ // it to decide whether a write can be skipped.
857
+ this.eventMask &= ~eventMask;
858
+ this.app.options.onXError?.(err);
760
859
  });
761
860
  }
762
861
  });
763
862
 
764
- this.on('map', () => (this._mapped = true));
765
- this.on('unmap', () => (this._mapped = false));
766
- this.on('destroy', () => {
863
+ // ntk's own bookkeeping listens for the same events an app does, and the
864
+ // hook above reads a listener as a caller expressing interest and selects
865
+ // for it. For a window ntk created that is free — StructureNotify is in
866
+ // the CreateWindow value list already — but for an adopted one the
867
+ // tracked mask starts at zero and the write is absolute, so it would
868
+ // replace whatever this connection had selected on a window another
869
+ // client owns (issue #322). Subscribe past the hook: adopting a window
870
+ // costs no requests and changes no server-side state, and a caller that
871
+ // wants these events still asks for them, with on('map') or selectInput.
872
+ this._listenInternal('map', () => (this._mapped = true));
873
+ this._listenInternal('unmap', () => (this._mapped = false));
874
+ this._listenInternal('destroy', () => {
767
875
  this._destroyed = true;
768
876
  this._forget();
769
877
  this._teardownFrame(false); // the server has already taken the window
@@ -773,10 +881,100 @@ export default class Window extends Drawable {
773
881
  });
774
882
 
775
883
  if (typeof this.width !== 'undefined') {
776
- this._readyPromiseResolve();
884
+ this._settleReady();
885
+ }
886
+ }
887
+
888
+ /**
889
+ * Attach one of ntk's own handlers without the `newListener` hook reading
890
+ * it as a caller asking for the event. The hook's job is to turn expressed
891
+ * interest into a server-side selection; ntk's internal bookkeeping
892
+ * expresses none — it only wants the events that arrive anyway — and on an
893
+ * adopted window a selection nobody asked for overwrites another
894
+ * subsystem's (issue #322). The flag is read synchronously by the hook,
895
+ * which EventEmitter emits before the listener is stored.
896
+ */
897
+ _listenInternal(name, fn) {
898
+ this._internalListen = true;
899
+ try {
900
+ this.on(name, fn);
901
+ } finally {
902
+ this._internalListen = false;
777
903
  }
778
904
  }
779
905
 
906
+ /**
907
+ * Resolves with this window once its geometry is known — immediately for a
908
+ * window ntk created (it already knows what it asked for), when the
909
+ * GetGeometry sent by the constructor replies for one adopted by id.
910
+ *
911
+ * Adoption is the case that needs it: until the reply lands, `width`,
912
+ * `height`, `x` and `y` are `undefined` and `depth` is `0`, so anything
913
+ * that picks a pixel format from the depth — `getContext('2d')`,
914
+ * `createPattern` — would pick the wrong one. Awaiting is safe
915
+ * unconditionally, so code that takes windows from either source need not
916
+ * ask which it has:
917
+ *
918
+ * const wnd = new Window(app, { id: ev.wid });
919
+ * await wnd.ready;
920
+ * const ctx = wnd.getContext('2d');
921
+ *
922
+ * The promise never rejects. A window destroyed before the reply arrives
923
+ * resolves all the same, with `width` still `undefined` — the request that
924
+ * hits the gone window is the one that reports it.
925
+ */
926
+ get ready() {
927
+ return this._readyPromise;
928
+ }
929
+
930
+ /**
931
+ * Ask the server where this window is now — `{ x, y, width, height, depth,
932
+ * borderWidth, root }`, with `x`/`y` relative to the parent, as X reports
933
+ * them.
934
+ *
935
+ * `wnd.x`/`y`/`width`/`height`/`depth` are kept current from the event
936
+ * stream and are the cheap answer; this is the round trip for the cases
937
+ * the events cannot cover — a window nobody selected StructureNotify on, a
938
+ * window ntk created with the default `depth: 0` (CopyFromParent, whose
939
+ * real depth only the server knows), or simply the state at a point in
940
+ * time. The reply is written back to those properties, and resolves
941
+ * `ready` if it was still pending.
942
+ */
943
+ getGeometry() {
944
+ return new Promise((resolve, reject) => {
945
+ this.X.GetGeometry(this.id, (err, res) => {
946
+ if (err) return reject(err);
947
+ const geometry = unpackGeometry(res);
948
+ this._applyGeometry(geometry);
949
+ resolve(geometry);
950
+ });
951
+ });
952
+ }
953
+
954
+ /** Record a GetGeometry reply, and let anything waiting on it go. */
955
+ _applyGeometry({ x, y, width, height, depth }) {
956
+ this.x = x;
957
+ this.y = y;
958
+ this.width = width;
959
+ this.height = height;
960
+ this.depth = depth;
961
+ // Only where nothing is known yet: on an adopted window a 'resize'
962
+ // reports both moved and resized until there is a baseline to compare
963
+ // against (see _tagResize), and the event stream may already have set
964
+ // one — that is the delivered geometry, and this reply is not.
965
+ this._deliveredGeom ??= { x, y, width, height };
966
+ this._settleReady();
967
+ }
968
+
969
+ /**
970
+ * Let anything waiting on `ready` go, once the questions the constructor
971
+ * asked have all been answered. A window ntk created asked none.
972
+ */
973
+ _settleReady() {
974
+ if (this._readyPending > 0) return;
975
+ this._readyPromiseResolve(this);
976
+ }
977
+
780
978
  _forget() {
781
979
  Window._cacheFor(this.app).delete(this.id);
782
980
  delete this.X.event_consumers[this.id];
@@ -790,14 +988,43 @@ export default class Window extends Drawable {
790
988
  * an 'expose' (and 'draw') event is emitted only when a real redraw is
791
989
  * needed (first paint, resize). Opt out with
792
990
  * `createWindow({ backingStore: false })`.
991
+ *
992
+ * A window adopted by id is left alone by default — another client's
993
+ * pixels are not ntk's to buffer — and opts in with
994
+ * `createWindow({ id, backingStore: true })` (see `_ownsPixels`).
793
995
  */
794
996
  getContext(name, ...args) {
795
- if (name === '2d' && !this._backing && !this._backingOptOut && !this._foreign) {
796
- this._enableBackingStore();
997
+ if (name === '2d' && !this._backing && !this._backingOptOut && this._ownsPixels) {
998
+ if (this._foreign) this._enableBackingStoreWhenReady();
999
+ else this._enableBackingStore();
797
1000
  }
798
1001
  return super.getContext(name, ...args);
799
1002
  }
800
1003
 
1004
+ /**
1005
+ * The backing store for an adopted window, once there is something to build
1006
+ * it out of.
1007
+ *
1008
+ * A pixmap needs a width, a height, a depth and a visual, and an adopted
1009
+ * window knows none of them until `GetGeometry` and `GetWindowAttributes`
1010
+ * reply — so the allocation waits on `ready`. A 2d context taken in the
1011
+ * meantime draws straight to the window and re-binds itself to the pixmap
1012
+ * when it appears (the `_backing` event), which is the same path a resize
1013
+ * reallocation takes.
1014
+ */
1015
+ _enableBackingStoreWhenReady() {
1016
+ if (this._backingScheduled) return;
1017
+ this._backingScheduled = true;
1018
+ this.ready.then(() => {
1019
+ // A window destroyed before the replies arrived still settles `ready`,
1020
+ // with its geometry never filled in — there is no pixmap to make for
1021
+ // one, and CreatePixmap with an undefined size is not the way to find
1022
+ // that out.
1023
+ if (this._destroyed || this._backing || !this.width || !this.height) return;
1024
+ this._enableBackingStore();
1025
+ });
1026
+ }
1027
+
801
1028
  _enableBackingStore() {
802
1029
  this._dirty = false;
803
1030
  this._backingValid = false;
@@ -805,7 +1032,7 @@ export default class Window extends Drawable {
805
1032
  // Now there is a pixmap to present. Inert where the extensions are
806
1033
  // missing, so this costs a window on such a server two extension queries
807
1034
  // that node-x11 answers from its cache after the first.
808
- if (!this._presentOptOut && !this._foreign) {
1035
+ if (!this._presentOptOut && this._ownsPixels) {
809
1036
  this.enablePresent().catch((err) => this.app.options?.onXError?.(err));
810
1037
  }
811
1038
 
@@ -817,7 +1044,8 @@ export default class Window extends Drawable {
817
1044
  // server sends them even if user code only listens to 'draw'
818
1045
  if (!(this.eventMask & x11.eventMask.Exposure)) {
819
1046
  this.eventMask |= x11.eventMask.Exposure;
820
- this.X.ChangeWindowAttributes(this.id, { eventMask: this.eventMask }, () => {});
1047
+ // no callback: it would only buy node-x11's void-sync round trip, see setCursor
1048
+ this.X.ChangeWindowAttributes(this.id, { eventMask: this.eventMask });
821
1049
  }
822
1050
 
823
1051
  // a pure move is a ConfigureNotify too, and reallocating a backing
@@ -863,8 +1091,11 @@ export default class Window extends Drawable {
863
1091
  this._clearPixel = pixel;
864
1092
  if (this._destroyed) return this;
865
1093
  if (this._clearGc) this.X.ChangeGC(this._clearGc, { foreground: pixel });
866
- if (!this._foreign) {
867
- this.X.ChangeWindowAttributes(this.id, { backgroundPixel: pixel }, () => {});
1094
+ // The window attribute half is only ours to write on a window whose
1095
+ // pixels are ours — an adopted one has to have said so (see _ownsPixels).
1096
+ if (this._ownsPixels) {
1097
+ // no callback: it would only buy node-x11's void-sync round trip, see setCursor
1098
+ this.X.ChangeWindowAttributes(this.id, { backgroundPixel: pixel });
868
1099
  }
869
1100
  const backing = this._backing;
870
1101
  if (backing && this._clearGc) {
@@ -896,7 +1127,10 @@ export default class Window extends Drawable {
896
1127
  parent: this,
897
1128
  width: newW,
898
1129
  height: newH,
899
- depth
1130
+ depth,
1131
+ // it holds this window's pixels, so it is read through this window's
1132
+ // visual — a pixmap has none of its own (issue #295)
1133
+ visual: this.visualId
900
1134
  });
901
1135
  if (!this._clearGc) {
902
1136
  this._clearGc = this.X.AllocID();
@@ -925,7 +1159,11 @@ export default class Window extends Drawable {
925
1159
 
926
1160
  _handleExpose(ev) {
927
1161
  if (this._backingValid) {
928
- this.X.CopyArea(this._backing.id, this.id, this._presentGc, ev.x, ev.y, ev.x, ev.y, ev.width, ev.height);
1162
+ // an expose can arrive after the connection started closing, and this
1163
+ // runs inside the event dispatch where a throw has no caller (#321)
1164
+ safeRelease(this.X, () => {
1165
+ this.X.CopyArea(this._backing.id, this.id, this._presentGc, ev.x, ev.y, ev.x, ev.y, ev.width, ev.height);
1166
+ });
929
1167
  } else {
930
1168
  this._requestRedraw();
931
1169
  }
@@ -2997,9 +3235,19 @@ export default class Window extends Drawable {
2997
3235
  * pointer stays visible
2998
3236
  */
2999
3237
  setCursor(nameOrShape) {
3238
+ // resolved first: an unknown name has to throw whether or not the memo
3239
+ // below short-circuits the request
3000
3240
  const cursor = nameOrShape == null ? 0 : this.app.cursors.get(nameOrShape);
3241
+ if (cursor === this._currentCursor) return this;
3242
+ this._currentCursor = cursor;
3001
3243
  safeRelease(this.X, () => {
3002
- this.X.ChangeWindowAttributes(this.id, { cursor }, () => {});
3244
+ // no callback, deliberately: node-x11 guarantees a callback on a *void*
3245
+ // request eventually fires, and buys that by injecting a GetInputFocus
3246
+ // round trip when nothing else in the tick expects a reply (node-x11
3247
+ // issue #85). A callback that ignores its argument costs that round trip
3248
+ // per cursor change and buys nothing — an X error reaches
3249
+ // `client.emit('error')`, and so app's onXError hook, either way.
3250
+ this.X.ChangeWindowAttributes(this.id, { cursor });
3003
3251
  });
3004
3252
  return this;
3005
3253
  }
@@ -3012,7 +3260,8 @@ export default class Window extends Drawable {
3012
3260
  } else {
3013
3261
  return this;
3014
3262
  }
3015
- this.X.ChangeWindowAttributes(this.id, { eventMask: this.eventMask }, () => {});
3263
+ // no callback: it would only buy node-x11's void-sync round trip, see setCursor
3264
+ this.X.ChangeWindowAttributes(this.id, { eventMask: this.eventMask });
3016
3265
  return this;
3017
3266
  }
3018
3267
 
@@ -3307,7 +3556,13 @@ export default class Window extends Drawable {
3307
3556
  getAttributes() {
3308
3557
  return new Promise((resolve, reject) => {
3309
3558
  safeRelease(this.X, () =>
3310
- this.X.GetWindowAttributes(this.id, (err, attrs) => (err ? reject(err) : resolve(attrs)))
3559
+ this.X.GetWindowAttributes(this.id, (err, attrs) => {
3560
+ if (err) return reject(err);
3561
+ // the visual names the picture format (see `visualId`), and this
3562
+ // reply is the only place the server ever states it
3563
+ this.visualId = attrs.visual;
3564
+ resolve(attrs);
3565
+ })
3311
3566
  );
3312
3567
  });
3313
3568
  }
@@ -3327,13 +3582,26 @@ export default class Window extends Drawable {
3327
3582
  * either makes you the window manager or rejects with BadAccess because
3328
3583
  * something else already is. The mask is OR-ed into whatever handlers
3329
3584
  * have already asked for.
3585
+ *
3586
+ * A mask this connection already holds is a request that would change
3587
+ * nothing, so it resolves without one — `eventMask` is what this
3588
+ * connection selected and nothing else adds to it behind the caller's
3589
+ * back. It resolves rather than returning undefined because the promise is
3590
+ * the whole interface: an awaited no-op still has to mean "you have it".
3330
3591
  */
3331
3592
  selectInput(mask) {
3332
- this.eventMask |= mask;
3593
+ const added = mask & ~this.eventMask;
3594
+ if (added === 0) return Promise.resolve(this);
3595
+ this.eventMask |= added;
3333
3596
  return new Promise((resolve, reject) => {
3334
- this.X.ChangeWindowAttributes(this.id, { eventMask: this.eventMask }, (err) =>
3335
- err ? reject(err) : resolve(this)
3336
- );
3597
+ this.X.ChangeWindowAttributes(this.id, { eventMask: this.eventMask }, (err) => {
3598
+ if (!err) return resolve(this);
3599
+ // a refused write left the server's mask as it was — give back the
3600
+ // bits, or the next call would find them held and skip a request
3601
+ // that has not happened yet
3602
+ this.eventMask &= ~added;
3603
+ reject(err);
3604
+ });
3337
3605
  });
3338
3606
  }
3339
3607
 
@@ -3515,7 +3783,7 @@ export default class Window extends Drawable {
3515
3783
  // there can be multiple screens (and roots)
3516
3784
  const root = new Window(app, { id: tree.root });
3517
3785
  const wrappers = [...children, root, ...(parent ? [parent] : [])];
3518
- Promise.all(wrappers.map((w) => w._readyPromise)).then(() => {
3786
+ Promise.all(wrappers.map((w) => w.ready)).then(() => {
3519
3787
  callback(null, { parent, root, children });
3520
3788
  });
3521
3789
  });
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.2.0",
3
+ "version": "8.3.1",
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"