ntk 3.3.0 → 3.5.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/widgets/htmlview.js +16 -4
- package/lib/widgets/markdownview.js +16 -3
- package/lib/window.js +258 -0
- package/package.json +2 -2
package/lib/widgets/htmlview.js
CHANGED
|
@@ -65,6 +65,9 @@ const VOID_SKIP = new Set(['script', 'style', 'head', 'title', 'meta', 'link', '
|
|
|
65
65
|
* Standalone (windowless) use mirrors MarkdownView:
|
|
66
66
|
* const view = new HtmlView(null, { fonts });
|
|
67
67
|
* view.setHtml(html); view.layout(width); view.draw(ctx, x, y);
|
|
68
|
+
* Content that arrives asynchronously (images) invalidates the layout —
|
|
69
|
+
* pass `onInvalidate` to re-layout/redraw when that happens (window mode
|
|
70
|
+
* repaints automatically).
|
|
68
71
|
*/
|
|
69
72
|
export default class HtmlView {
|
|
70
73
|
constructor(window, opts = {}) {
|
|
@@ -74,6 +77,10 @@ export default class HtmlView {
|
|
|
74
77
|
|
|
75
78
|
/** called with (href, event, element) when a link is activated */
|
|
76
79
|
this.onLink = opts.onLink ?? null;
|
|
80
|
+
/** called when async content (an image) invalidates the layout;
|
|
81
|
+
* standalone hosts re-layout/redraw here — window mode also repaints
|
|
82
|
+
* on its own */
|
|
83
|
+
this.onInvalidate = opts.onInvalidate ?? null;
|
|
77
84
|
/** extra author stylesheet(s) applied after document <style>s */
|
|
78
85
|
this._userSheets = [].concat(opts.stylesheet ?? []);
|
|
79
86
|
/**
|
|
@@ -407,13 +414,11 @@ export default class HtmlView {
|
|
|
407
414
|
if (res instanceof SvgView) entry.svg = res;
|
|
408
415
|
else entry.image = res;
|
|
409
416
|
if (!res) entry.failed = true;
|
|
410
|
-
this.
|
|
411
|
-
if (this.window && this.window._mapped) this.render();
|
|
417
|
+
this._invalidate();
|
|
412
418
|
},
|
|
413
419
|
() => {
|
|
414
420
|
entry.failed = true;
|
|
415
|
-
this.
|
|
416
|
-
if (this.window && this.window._mapped) this.render();
|
|
421
|
+
this._invalidate();
|
|
417
422
|
}
|
|
418
423
|
);
|
|
419
424
|
}
|
|
@@ -422,6 +427,13 @@ export default class HtmlView {
|
|
|
422
427
|
for (const child of box.children) this._collectImages(child);
|
|
423
428
|
}
|
|
424
429
|
|
|
430
|
+
/** async content arrived: drop the cached layout, repaint and/or notify */
|
|
431
|
+
_invalidate() {
|
|
432
|
+
this._layoutWidth = -1;
|
|
433
|
+
if (this.window && this.window._mapped) this.render();
|
|
434
|
+
if (this.onInvalidate) this.onInvalidate();
|
|
435
|
+
}
|
|
436
|
+
|
|
425
437
|
// decoded bytes -> Image (PNG/JPEG) or SvgView (sniffed SVG markup)
|
|
426
438
|
_decodeResource(buf) {
|
|
427
439
|
const head = buf.subarray(0, 1024).toString('utf8');
|
|
@@ -51,7 +51,10 @@ const DEFAULT_THEME = {
|
|
|
51
51
|
* wnd.map();
|
|
52
52
|
*
|
|
53
53
|
* Standalone use (no window attach): `view.layout(width)` then
|
|
54
|
-
* `view.draw(ctx, x, y)`; `view.contentHeight` after layout.
|
|
54
|
+
* `view.draw(ctx, x, y)`; `view.contentHeight` after layout. Content that
|
|
55
|
+
* arrives asynchronously (mermaid models) invalidates the layout — pass
|
|
56
|
+
* `onInvalidate` to re-layout/redraw when that happens (window mode
|
|
57
|
+
* repaints automatically).
|
|
55
58
|
*/
|
|
56
59
|
export default class MarkdownView {
|
|
57
60
|
constructor(window, opts = {}) {
|
|
@@ -70,6 +73,10 @@ export default class MarkdownView {
|
|
|
70
73
|
/** called with (href, event) when a rendered link is clicked (window
|
|
71
74
|
* mode wires mousedown automatically; standalone callers use linkAt) */
|
|
72
75
|
this.onLink = opts.onLink ?? null;
|
|
76
|
+
/** called when async content (a mermaid model) invalidates the layout;
|
|
77
|
+
* standalone hosts re-layout/redraw here — window mode also repaints
|
|
78
|
+
* on its own */
|
|
79
|
+
this.onInvalidate = opts.onInvalidate ?? null;
|
|
73
80
|
|
|
74
81
|
if (this.window) {
|
|
75
82
|
this._ctx = this.window.getContext('2d');
|
|
@@ -116,8 +123,7 @@ export default class MarkdownView {
|
|
|
116
123
|
parseMermaid(text).then(
|
|
117
124
|
(model) => {
|
|
118
125
|
entry.model = model;
|
|
119
|
-
this.
|
|
120
|
-
if (this.window && this.window._mapped) this.render();
|
|
126
|
+
this._invalidate();
|
|
121
127
|
},
|
|
122
128
|
() => {
|
|
123
129
|
entry.failed = true; // unsupported/invalid: stays a code block
|
|
@@ -127,6 +133,13 @@ export default class MarkdownView {
|
|
|
127
133
|
return entry;
|
|
128
134
|
}
|
|
129
135
|
|
|
136
|
+
/** async content arrived: drop the cached layout, repaint and/or notify */
|
|
137
|
+
_invalidate() {
|
|
138
|
+
this._layoutWidth = -1;
|
|
139
|
+
if (this.window && this.window._mapped) this.render();
|
|
140
|
+
if (this.onInvalidate) this.onInvalidate();
|
|
141
|
+
}
|
|
142
|
+
|
|
130
143
|
/** lay content out for a container width; returns total content height */
|
|
131
144
|
layout(width) {
|
|
132
145
|
const fonts = this._fonts();
|
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.
|
|
3
|
+
"version": "3.5.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",
|
|
@@ -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.
|
|
54
|
+
"x11": "^3.1.2",
|
|
55
55
|
"yoga-layout": "^3.2.1"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|