ntk 3.4.0 → 3.5.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.
@@ -736,6 +736,34 @@ class RenderingContext2d {
736
736
  this._markDirty();
737
737
  }
738
738
 
739
+ /**
740
+ * Composite shaped glyph runs onto this context, honouring the clip.
741
+ *
742
+ * CompositeGlyphs writes straight to the destination picture, so it has
743
+ * no way to consult our clip mask — text drawn through it used to spill
744
+ * out of clipped boxes while every fill and stroke stayed inside. With a
745
+ * clip active, render the glyph coverage into the scratch a8 mask
746
+ * instead, intersect that with the clip, and paint the real source
747
+ * through the result — the same shape as _fillPolys.
748
+ */
749
+ drawGlyphs(op, src, positioned) {
750
+ const app = this.window.app;
751
+ const R = this.Render;
752
+ if (!this.clipMask) {
753
+ drawGlyphRuns(app, op, src.id, this.picture.id, positioned);
754
+ this._markDirty();
755
+ return;
756
+ }
757
+ this._ensureFillMask();
758
+ this._glyphSource ??= this.createSolidPicture(1, 1, 1, 1);
759
+ R.FillRectangles(R.PictOp.Src, this.fillMask.id, [0, 0, 0, 0], [0, 0, this.width, this.height]);
760
+ // solid white through the glyphs leaves their coverage in the a8 mask
761
+ drawGlyphRuns(app, R.PictOp.Over, this._glyphSource.id, this.fillMask.id, positioned);
762
+ R.Composite(R.PictOp.In, this.clipMask.id, 0, this.fillMask.id, 0, 0, 0, 0, 0, 0, this.width, this.height);
763
+ R.Composite(op, src.id, this.fillMask.id, this.picture.id, 0, 0, 0, 0, 0, 0, this.width, this.height);
764
+ this._markDirty();
765
+ }
766
+
739
767
  // combined clip ∩ globalAlpha mask for direct composites (rect/image
740
768
  // fast paths); returns a picture id or 0. Reuses the fill scratch mask.
741
769
  _compositeMask() {
@@ -928,8 +956,7 @@ class RenderingContext2d {
928
956
  positioned.push({ run, x: cursor, y: oy });
929
957
  cursor += run.width;
930
958
  }
931
- drawGlyphRuns(app, this.Render.PictOp.Over, this._backgroundPicture.id, this.picture.id, positioned);
932
- this._markDirty();
959
+ this.drawGlyphs(this.Render.PictOp.Over, this._backgroundPicture, positioned);
933
960
  }
934
961
 
935
962
  /**
@@ -334,7 +334,13 @@ export class TextLayout {
334
334
  const flush = () => {
335
335
  if (batch.length === 0) return;
336
336
  const src = batchColor ? ctx._stylePicture(batchColor) : ctx._backgroundPicture;
337
- drawGlyphRuns(app, Render.PictOp.Over, src.id, ctx.picture.id, batch);
337
+ // via the context so the clip mask is applied (drawGlyphRuns
338
+ // composites straight onto the picture and cannot see it)
339
+ if (typeof ctx.drawGlyphs === 'function') {
340
+ ctx.drawGlyphs(Render.PictOp.Over, src, batch);
341
+ } else {
342
+ drawGlyphRuns(app, Render.PictOp.Over, src.id, ctx.picture.id, batch);
343
+ }
338
344
  batch = [];
339
345
  };
340
346
  for (const r of line.runs) {
package/lib/window.js CHANGED
@@ -134,6 +134,21 @@ export default class Window extends Drawable {
134
134
  if (args.title) {
135
135
  this.setTitle(args.title);
136
136
  }
137
+ if (args.wmClass) {
138
+ const c = args.wmClass;
139
+ if (Array.isArray(c)) this.setClass(c[0], c[1]);
140
+ else if (typeof c === 'object') this.setClass(c.instance, c.class ?? c.instance);
141
+ else this.setClass(c);
142
+ }
143
+ if (args.windowType) {
144
+ this.setWindowType(args.windowType);
145
+ }
146
+ if (args.sizeHints || args.resizable === false) {
147
+ this.setSizeHints({ ...args.sizeHints, ...(args.resizable === false && { resizable: false }) });
148
+ }
149
+ if (args.alwaysOnTop) {
150
+ this.setAlwaysOnTop(true);
151
+ }
137
152
 
138
153
  X.event_consumers[this.id] = this;
139
154
  this.on('event', (ev) => {
@@ -607,6 +622,249 @@ export default class Window extends Drawable {
607
622
  return this;
608
623
  }
609
624
 
625
+ /**
626
+ * Intern several atoms and hand them to `cb` as an object keyed by name.
627
+ * node-x11 caches interned atoms per connection, so a repeated call
628
+ * costs no round trip. Same deferred-chain hazards as setTitle: by the
629
+ * time the replies land the window may be gone, so callers re-check
630
+ * `this._destroyed`.
631
+ */
632
+ _withAtoms(names, cb) {
633
+ const X = this.X;
634
+ const out = {};
635
+ let pending = names.length;
636
+ let failed = false;
637
+ for (const name of names) {
638
+ safeRelease(X, () => {
639
+ X.InternAtom(false, name, (err, atom) => {
640
+ if (failed) return;
641
+ if (err) {
642
+ failed = true;
643
+ return;
644
+ }
645
+ out[name] = atom;
646
+ if (--pending === 0 && !this._destroyed) cb(out);
647
+ });
648
+ });
649
+ }
650
+ return this;
651
+ }
652
+
653
+ /**
654
+ * ICCCM WM_NORMAL_HINTS — how the window manager may resize this window.
655
+ *
656
+ * setSizeHints({ minWidth, minHeight, maxWidth, maxHeight,
657
+ * widthInc, heightInc, baseWidth, baseHeight,
658
+ * minAspect: [num, den], maxAspect: [num, den],
659
+ * gravity })
660
+ *
661
+ * `resizable: false` is shorthand for pinning min and max to the current
662
+ * size. Without this property a WM lets the user resize a window to any
663
+ * size at all, which is why fixed-size dialogs need it.
664
+ */
665
+ setSizeHints(hints = {}) {
666
+ const {
667
+ minWidth,
668
+ minHeight,
669
+ maxWidth,
670
+ maxHeight,
671
+ widthInc,
672
+ heightInc,
673
+ baseWidth,
674
+ baseHeight,
675
+ minAspect,
676
+ maxAspect,
677
+ gravity,
678
+ resizable
679
+ } = hints;
680
+
681
+ let minW = minWidth;
682
+ let minH = minHeight;
683
+ let maxW = maxWidth;
684
+ let maxH = maxHeight;
685
+ if (resizable === false) {
686
+ minW = minW ?? this.width;
687
+ minH = minH ?? this.height;
688
+ maxW = maxW ?? this.width;
689
+ maxH = maxH ?? this.height;
690
+ }
691
+
692
+ // XSizeHints: 18 CARD32s, flags first (ICCCM 4.1.2.3)
693
+ const PMinSize = 16;
694
+ const PMaxSize = 32;
695
+ const PResizeInc = 64;
696
+ const PAspect = 128;
697
+ const PBaseSize = 256;
698
+ const PWinGravity = 512;
699
+
700
+ const v = new Uint32Array(18);
701
+ let flags = 0;
702
+ if (minW !== undefined || minH !== undefined) {
703
+ flags |= PMinSize;
704
+ v[5] = minW ?? 0;
705
+ v[6] = minH ?? 0;
706
+ }
707
+ if (maxW !== undefined || maxH !== undefined) {
708
+ flags |= PMaxSize;
709
+ v[7] = maxW ?? 0;
710
+ v[8] = maxH ?? 0;
711
+ }
712
+ if (widthInc !== undefined || heightInc !== undefined) {
713
+ flags |= PResizeInc;
714
+ v[9] = widthInc ?? 1;
715
+ v[10] = heightInc ?? 1;
716
+ }
717
+ if (minAspect || maxAspect) {
718
+ flags |= PAspect;
719
+ v[11] = minAspect?.[0] ?? 0;
720
+ v[12] = minAspect?.[1] ?? 1;
721
+ v[13] = maxAspect?.[0] ?? 0;
722
+ v[14] = maxAspect?.[1] ?? 1;
723
+ }
724
+ if (baseWidth !== undefined || baseHeight !== undefined) {
725
+ flags |= PBaseSize;
726
+ v[15] = baseWidth ?? 0;
727
+ v[16] = baseHeight ?? 0;
728
+ }
729
+ if (gravity !== undefined) {
730
+ flags |= PWinGravity;
731
+ v[17] = gravity;
732
+ }
733
+ v[0] = flags;
734
+
735
+ safeRelease(this.X, () => {
736
+ this.X.ChangeProperty(
737
+ 0,
738
+ this.id,
739
+ this.X.atoms.WM_NORMAL_HINTS,
740
+ this.X.atoms.WM_SIZE_HINTS,
741
+ 32,
742
+ Buffer.from(v.buffer, v.byteOffset, v.byteLength)
743
+ );
744
+ });
745
+ return this;
746
+ }
747
+
748
+ /**
749
+ * ICCCM WM_CLASS — the instance/class pair taskbars and WMs use to group
750
+ * windows, match icons and apply per-application rules. Two NUL-
751
+ * terminated latin-1 strings.
752
+ */
753
+ setClass(instance, className = instance) {
754
+ const data = Buffer.from(`${instance}\0${className}\0`, 'latin1');
755
+ safeRelease(this.X, () => {
756
+ this.X.ChangeProperty(0, this.id, this.X.atoms.WM_CLASS, this.X.atoms.STRING, 8, data);
757
+ });
758
+ return this;
759
+ }
760
+
761
+ /**
762
+ * EWMH _NET_WM_WINDOW_TYPE — what kind of window this is, e.g. 'dialog',
763
+ * 'utility', 'tooltip', 'dropdown_menu', 'notification'. Window managers
764
+ * use it to pick decorations, stacking and whether the window belongs in
765
+ * the taskbar. Accepts a short name or a full atom name, and a list when
766
+ * you want fallbacks (most-preferred first, per the spec).
767
+ *
768
+ * This is the WM-cooperative alternative to override-redirect: a menu
769
+ * marked 'dropdown_menu' still gets shadows and correct stacking, while
770
+ * an override-redirect window bypasses the WM entirely.
771
+ */
772
+ setWindowType(type) {
773
+ const names = (Array.isArray(type) ? type : [type]).map((t) =>
774
+ t.startsWith('_NET_WM_WINDOW_TYPE_') ? t : `_NET_WM_WINDOW_TYPE_${t.toUpperCase()}`
775
+ );
776
+ return this._withAtoms(['_NET_WM_WINDOW_TYPE', ...names], (atoms) => {
777
+ const v = new Uint32Array(names.map((n) => atoms[n]));
778
+ safeRelease(this.X, () => {
779
+ this.X.ChangeProperty(
780
+ 0,
781
+ this.id,
782
+ atoms._NET_WM_WINDOW_TYPE,
783
+ this.X.atoms.ATOM,
784
+ 32,
785
+ Buffer.from(v.buffer, v.byteOffset, v.byteLength)
786
+ );
787
+ });
788
+ });
789
+ }
790
+
791
+ /**
792
+ * Keep this window above normal windows.
793
+ *
794
+ * Prefers EWMH `_NET_WM_STATE_ABOVE`. quartz-wm (XQuartz) does not
795
+ * advertise that state, so on macOS this falls back to the Apple-WM
796
+ * extension's window levels, which are the only always-on-top mechanism
797
+ * there. Apple-WM addresses the window the WM manages, so once we have
798
+ * been reparented the request must name our frame — the ancestor whose
799
+ * parent is root — not our own id.
800
+ */
801
+ setAlwaysOnTop(on = true) {
802
+ const X = this.X;
803
+ const _NET_WM_STATE_ADD = 1;
804
+ const _NET_WM_STATE_REMOVE = 0;
805
+
806
+ this._withAtoms(['_NET_WM_STATE', '_NET_WM_STATE_ABOVE', '_NET_SUPPORTED'], (atoms) => {
807
+ const root = this.app.display.screen[0].root;
808
+ safeRelease(X, () => {
809
+ X.GetProperty(0, root, atoms._NET_SUPPORTED, X.atoms.ATOM, 0, 1024, (err, prop) => {
810
+ if (err || this._destroyed) return;
811
+ let supported = false;
812
+ const data = prop?.data;
813
+ for (let i = 0; data && i + 4 <= data.length; i += 4) {
814
+ if (data.readUInt32LE(i) === atoms._NET_WM_STATE_ABOVE) {
815
+ supported = true;
816
+ break;
817
+ }
818
+ }
819
+ if (supported) {
820
+ // a mapped window changes state by asking the WM, not by
821
+ // writing the property (EWMH 7.7). SendEvent takes raw event
822
+ // bytes, so pack the 32-byte ClientMessage by hand.
823
+ const ev = Buffer.alloc(32);
824
+ ev.writeUInt8(33, 0); // ClientMessage
825
+ ev.writeUInt8(32, 1); // format
826
+ ev.writeUInt32LE(this.id >>> 0, 4);
827
+ ev.writeUInt32LE(atoms._NET_WM_STATE >>> 0, 8);
828
+ ev.writeUInt32LE(on ? _NET_WM_STATE_ADD : _NET_WM_STATE_REMOVE, 12);
829
+ ev.writeUInt32LE(atoms._NET_WM_STATE_ABOVE >>> 0, 16);
830
+ ev.writeUInt32LE(0, 20); // second property: none
831
+ ev.writeUInt32LE(1, 24); // source indication: application
832
+ safeRelease(X, () => {
833
+ X.SendEvent(
834
+ root,
835
+ false,
836
+ x11.eventMask.SubstructureRedirect | x11.eventMask.SubstructureNotify,
837
+ ev
838
+ );
839
+ });
840
+ } else {
841
+ this._appleWMSetLevel(on);
842
+ }
843
+ });
844
+ });
845
+ });
846
+ return this;
847
+ }
848
+
849
+ /** Apple-WM fallback for setAlwaysOnTop; no-op where the extension is absent. */
850
+ _appleWMSetLevel(on) {
851
+ const X = this.X;
852
+ safeRelease(X, () => {
853
+ X.require('apple-wm', (err, ext) => {
854
+ if (err || this._destroyed) return;
855
+ // the level belongs on the window quartz-wm manages: our frame
856
+ this.queryTree((treeErr, tree) => {
857
+ if (treeErr || this._destroyed) return;
858
+ const rootId = tree.root.id;
859
+ const target = tree.parent.id === rootId ? this.id : tree.parent.id;
860
+ safeRelease(X, () => {
861
+ ext.SetWindowLevel(target, on ? ext.WindowLevel.Floating : ext.WindowLevel.Normal);
862
+ });
863
+ });
864
+ });
865
+ });
866
+ }
867
+
610
868
  /**
611
869
  * Set the mouse cursor shown over this window. Accepts a friendly name
612
870
  * ('text', 'pointer', 'wait', ... — see cursorShapes in lib/cursor.js and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ntk",
3
- "version": "3.4.0",
3
+ "version": "3.5.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",
@@ -51,7 +51,7 @@
51
51
  "parse-color": "^1.0.0",
52
52
  "pngjs": "^7.0.0",
53
53
  "postcss": "^8.5.23",
54
- "x11": "^3.1.1",
54
+ "x11": "^3.1.2",
55
55
  "yoga-layout": "^3.2.1"
56
56
  },
57
57
  "scripts": {