ntk 6.4.0 → 6.6.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/app.js +20 -0
- package/lib/builtin.js +47 -0
- package/lib/fontconfig.js +5 -3
- package/lib/image.js +2 -1
- package/lib/renderingcontext_2d.js +123 -14
- package/lib/text/fontsource.js +6 -5
- package/lib/widgets/htmlview.js +3 -3
- package/lib/widgets/tex.js +5 -8
- package/lib/window.js +24 -19
- package/package.json +2 -2
package/lib/app.js
CHANGED
|
@@ -146,6 +146,26 @@ export default class App {
|
|
|
146
146
|
return chooseGLXConfig(this, spec);
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Find a 32-bit TrueColor visual with an alpha channel, for per-pixel
|
|
151
|
+
* transparent windows (ARGB). Returns `{ visual, depth: 32 }` — pass
|
|
152
|
+
* these to `createWindow()` together with `backgroundPixel: 0`. Returns
|
|
153
|
+
* `null` when the server has no such visual (XQuartz, for example).
|
|
154
|
+
*
|
|
155
|
+
* The compositor blends the window's alpha channel automatically; no
|
|
156
|
+
* Composite extension calls or EWMH properties are needed from the client.
|
|
157
|
+
* Without a running compositor, transparent regions render black.
|
|
158
|
+
*/
|
|
159
|
+
findArgbVisual(screen = 0) {
|
|
160
|
+
const depths = this.display.screen[screen].depths;
|
|
161
|
+
const visuals = depths?.[32];
|
|
162
|
+
if (!visuals) return null;
|
|
163
|
+
for (const [id, visual] of Object.entries(visuals)) {
|
|
164
|
+
if (visual.class === 4) return { visual: Number(id), depth: 32 };
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
149
169
|
rootWindow(screen = 0) {
|
|
150
170
|
return new Window(this, { id: this.display.screen[screen].root });
|
|
151
171
|
}
|
package/lib/builtin.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Reach a Node builtin from ESM across Node versions.
|
|
2
|
+
//
|
|
3
|
+
// `process.getBuiltinModule(id)` is the browser-safe accessor — the string is
|
|
4
|
+
// a function argument, so no bundler treats it as a module specifier — but it
|
|
5
|
+
// only exists on Node >= 20.16. ntk's floor is lower (Node 18.19, for
|
|
6
|
+
// react-x11), and there is no browser-safe, synchronous way to reach a builtin
|
|
7
|
+
// from ESM on an older Node: `getBuiltinModule` is the API that was added to
|
|
8
|
+
// fill exactly that gap. So where it is absent we fall back to `createRequire`.
|
|
9
|
+
//
|
|
10
|
+
// The `node:module` import below is a real specifier and the one place in lib/
|
|
11
|
+
// allowed to statically import a builtin other than node:events — a sanctioned
|
|
12
|
+
// exception in test/packaging.test.js, scoped to this file. A *browser* bundle
|
|
13
|
+
// of the text stack must stub this one specifier, but the fallback that uses it
|
|
14
|
+
// never runs in a browser (guarded on process.versions.node) and ntk ships no
|
|
15
|
+
// browser build. It is not a top-level await, so the CJS/SEA bundle path (see
|
|
16
|
+
// docs/packaging.md) is unaffected.
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
|
|
19
|
+
let _require;
|
|
20
|
+
|
|
21
|
+
// One createRequire bound to ntk, made lazily. Builtins ('node:*') resolve
|
|
22
|
+
// regardless of the base URL, so this also serves callers that resolve a
|
|
23
|
+
// package in ntk's own dependency graph (e.g. KaTeX's font directory).
|
|
24
|
+
function req() {
|
|
25
|
+
return (_require ??= createRequire(import.meta.url));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* `process.getBuiltinModule(id)` with a Node-18 fallback.
|
|
30
|
+
* @param {string} id e.g. 'node:child_process'
|
|
31
|
+
* @returns the builtin module, or `undefined` outside Node (a browser
|
|
32
|
+
* embedding of the text stack — supply a custom FontSource / callback there).
|
|
33
|
+
*/
|
|
34
|
+
export function builtin(id) {
|
|
35
|
+
const p = globalThis.process;
|
|
36
|
+
if (!p?.versions?.node) return undefined;
|
|
37
|
+
if (typeof p.getBuiltinModule === 'function') return p.getBuiltinModule(id);
|
|
38
|
+
return req()(id);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A `require` for resolving packages in ntk's dependency graph.
|
|
43
|
+
* @returns {NodeRequire|null} `null` outside Node.
|
|
44
|
+
*/
|
|
45
|
+
export function nodeRequire() {
|
|
46
|
+
return globalThis.process?.versions?.node ? req() : null;
|
|
47
|
+
}
|
package/lib/fontconfig.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
// node:child_process is fetched lazily (not
|
|
2
|
-
// browser bundles of the package never try to resolve it; in non-node
|
|
1
|
+
// node:child_process is fetched lazily (via builtin(), not a static import) so
|
|
2
|
+
// that browser bundles of the package never try to resolve it; in non-node
|
|
3
3
|
// environments use a custom FontSource instead (see text/fontsource.js).
|
|
4
|
+
import { builtin } from './builtin.js';
|
|
5
|
+
|
|
4
6
|
function childProcess() {
|
|
5
|
-
return
|
|
7
|
+
return builtin('node:child_process');
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
function execFileSync(...args) {
|
package/lib/image.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import jpeg from 'jpeg-js';
|
|
2
2
|
import { PNG } from 'pngjs';
|
|
3
3
|
|
|
4
|
+
import { builtin } from './builtin.js';
|
|
4
5
|
import { fromStraightRgba, pixelLayout } from './imagedata.js';
|
|
5
6
|
import Picture from './picture.js';
|
|
6
7
|
|
|
@@ -126,7 +127,7 @@ export function decodeImage(buffer) {
|
|
|
126
127
|
export async function loadImage(source) {
|
|
127
128
|
if (typeof source === 'string' || source instanceof URL) {
|
|
128
129
|
// lazy builtin lookup: browser bundles must not depend on node:fs
|
|
129
|
-
const fsp =
|
|
130
|
+
const fsp = builtin('node:fs/promises');
|
|
130
131
|
if (!fsp) {
|
|
131
132
|
throw new Error('loadImage: file paths need node — pass the encoded bytes instead');
|
|
132
133
|
}
|
|
@@ -284,6 +284,9 @@ class RenderingContext2d {
|
|
|
284
284
|
: depth === 8
|
|
285
285
|
? this.Render.a8
|
|
286
286
|
: this.Render.rgb24;
|
|
287
|
+
// Does the target have a real alpha channel? Only then is "transparent"
|
|
288
|
+
// a colour it can hold, which is what clearRect turns on.
|
|
289
|
+
this._hasAlpha = depth === 32;
|
|
287
290
|
this.picture = new Picture(this.window.app, {
|
|
288
291
|
drawable: target,
|
|
289
292
|
format,
|
|
@@ -325,14 +328,14 @@ class RenderingContext2d {
|
|
|
325
328
|
|
|
326
329
|
this._clips = [];
|
|
327
330
|
this._dropMasks();
|
|
328
|
-
for (const gc of [this._gc, this._fillMaskGC]) {
|
|
331
|
+
for (const gc of [this._gc, this._fillMaskGC, this._clipMaskGC]) {
|
|
329
332
|
if (!gc) continue;
|
|
330
333
|
safeRelease(this.X, () => {
|
|
331
334
|
this.X.FreeGC(gc);
|
|
332
335
|
this.X.ReleaseID(gc);
|
|
333
336
|
});
|
|
334
337
|
}
|
|
335
|
-
this._gc = this._fillMaskGC = null;
|
|
338
|
+
this._gc = this._fillMaskGC = this._clipMaskGC = null;
|
|
336
339
|
this._gcs.length = 0;
|
|
337
340
|
|
|
338
341
|
this._backgroundPicture = this._glyphSource = null;
|
|
@@ -1363,12 +1366,19 @@ class RenderingContext2d {
|
|
|
1363
1366
|
// ------------------------------------------------------------------
|
|
1364
1367
|
// drawing
|
|
1365
1368
|
|
|
1369
|
+
// Clearing means "back to nothing", and what nothing looks like depends on
|
|
1370
|
+
// whether the target can hold transparency. A depth-32 ARGB window gets
|
|
1371
|
+
// transparent black, the canvas spec's answer, and the compositor shows
|
|
1372
|
+
// whatever is behind it. Anything else has no alpha channel to write, so
|
|
1373
|
+
// clearing stays opaque white — the paper an opaque window starts from,
|
|
1374
|
+
// and what every caller predating ARGB windows expects.
|
|
1366
1375
|
clearRect(x, y, w, h) {
|
|
1376
|
+
const alpha = this._hasAlpha;
|
|
1367
1377
|
if (matIsIdentity(this._m) && !this._clips.length) {
|
|
1368
1378
|
this.Render.FillRectangles(
|
|
1369
1379
|
this.Render.PictOp.Src,
|
|
1370
1380
|
this.picture.id,
|
|
1371
|
-
[1, 1, 1, 1],
|
|
1381
|
+
alpha ? [0, 0, 0, 0] : [1, 1, 1, 1],
|
|
1372
1382
|
[x, y, w, h],
|
|
1373
1383
|
);
|
|
1374
1384
|
this._markDirty();
|
|
@@ -1376,11 +1386,18 @@ class RenderingContext2d {
|
|
|
1376
1386
|
}
|
|
1377
1387
|
const tmp = new Path2D();
|
|
1378
1388
|
tmp.rect(x, y, w, h);
|
|
1379
|
-
//
|
|
1380
|
-
//
|
|
1389
|
+
// Transformed or clipped, so the rect is a polygon and the erase has to
|
|
1390
|
+
// run through the coverage mask. OutReverse is `dst OUT src`: against an
|
|
1391
|
+
// opaque source it scales the destination by 1 - coverage, erasing to
|
|
1392
|
+
// transparent with an antialiased edge. PictOpSrc would take the whole
|
|
1393
|
+
// bounding box with it, coverage or not. Opaque targets keep the old
|
|
1394
|
+
// behaviour: paint white over the shape, ignoring alpha and the
|
|
1395
|
+
// composite op but honoring the clip, matching the fast path above.
|
|
1381
1396
|
this._fillPolys(flattenPath(tmp._cmds, this._m), "nonzero", {
|
|
1382
|
-
src:
|
|
1383
|
-
|
|
1397
|
+
src: alpha
|
|
1398
|
+
? this.createSolidPicture(0, 0, 0, 1)
|
|
1399
|
+
: this.createSolidPicture(1, 1, 1, 1),
|
|
1400
|
+
op: alpha ? this.Render.PictOp.OutReverse : this.Render.PictOp.Over,
|
|
1384
1401
|
alpha: 1,
|
|
1385
1402
|
});
|
|
1386
1403
|
}
|
|
@@ -1644,13 +1661,15 @@ class RenderingContext2d {
|
|
|
1644
1661
|
drawable: tmpPixmap,
|
|
1645
1662
|
format: R.a8,
|
|
1646
1663
|
});
|
|
1647
|
-
|
|
1648
|
-
R.
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1664
|
+
if (!this._uploadClipCoverage(tmpPixmap, entry, bb)) {
|
|
1665
|
+
R.FillRectangles(
|
|
1666
|
+
R.PictOp.Src,
|
|
1667
|
+
tmpMask.id,
|
|
1668
|
+
[0, 0, 0, 0],
|
|
1669
|
+
[0, 0, bb.w, bb.h],
|
|
1670
|
+
);
|
|
1671
|
+
this._rasterizePolys(tmpMask, entry.polys, entry.rule, -bb.x, -bb.y);
|
|
1672
|
+
}
|
|
1654
1673
|
R.Composite(
|
|
1655
1674
|
op,
|
|
1656
1675
|
tmpMask.id,
|
|
@@ -1669,6 +1688,96 @@ class RenderingContext2d {
|
|
|
1669
1688
|
tmpPixmap.destroy();
|
|
1670
1689
|
}
|
|
1671
1690
|
|
|
1691
|
+
/**
|
|
1692
|
+
* The clip-mask twin of `_uploadCoverage`: rasterize one clip entry here
|
|
1693
|
+
* and PutImage the coverage into the bbox temp, instead of asking the
|
|
1694
|
+
* server for trapezoids. Returns false — caller falls back to AddTraps —
|
|
1695
|
+
* with no rasterizer, when the policy routes this shape to the server, or
|
|
1696
|
+
* when the rasterizer declines.
|
|
1697
|
+
*
|
|
1698
|
+
* Without this the routing was only half applied. `_uploadCoverage` covers
|
|
1699
|
+
* fills and strokes, so an app that set `rasterPolicy` to keep every
|
|
1700
|
+
* drawing local still emitted an AddTraps per non-rectangular clip, and on
|
|
1701
|
+
* glamor those were the whole remaining cost: a wall of 48 rounded cards
|
|
1702
|
+
* spent 12 AddTraps a frame on clips alone (react-x11#199), 116ms of
|
|
1703
|
+
* server drain per frame, with no policy able to reach them.
|
|
1704
|
+
*
|
|
1705
|
+
* PutImage writes Src, so it replaces the temp's clear as well.
|
|
1706
|
+
*/
|
|
1707
|
+
_uploadClipCoverage(tmpPixmap, entry, bb) {
|
|
1708
|
+
const rasterizer = this.window.app.rasterizer;
|
|
1709
|
+
if (!rasterizer) return false;
|
|
1710
|
+
const flat = [];
|
|
1711
|
+
let edges = 0;
|
|
1712
|
+
for (const p of entry.polys) {
|
|
1713
|
+
if (p.pts.length < 6) continue;
|
|
1714
|
+
flat.push(p.pts);
|
|
1715
|
+
edges += p.pts.length / 2;
|
|
1716
|
+
}
|
|
1717
|
+
if (!flat.length) return false;
|
|
1718
|
+
if (
|
|
1719
|
+
routeRaster(bb.w, bb.h, edges, this.window.app.rasterPolicy) !== "local"
|
|
1720
|
+
) {
|
|
1721
|
+
return false;
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
const coverage = rasterizer.rasterize({
|
|
1725
|
+
polys: flat,
|
|
1726
|
+
rule: entry.rule,
|
|
1727
|
+
dx: -bb.x,
|
|
1728
|
+
dy: -bb.y,
|
|
1729
|
+
width: bb.w,
|
|
1730
|
+
height: bb.h,
|
|
1731
|
+
});
|
|
1732
|
+
if (!coverage) return false;
|
|
1733
|
+
|
|
1734
|
+
// scanlines padded to 4 bytes, as in _uploadCoverage — and as there, a
|
|
1735
|
+
// width that is already a multiple of 4 goes out as a view over the
|
|
1736
|
+
// rasterizer's own bytes with no copy
|
|
1737
|
+
const stride = (bb.w + 3) & ~3;
|
|
1738
|
+
let data;
|
|
1739
|
+
if (stride === bb.w) {
|
|
1740
|
+
data = Buffer.isBuffer(coverage)
|
|
1741
|
+
? coverage
|
|
1742
|
+
: Buffer.from(coverage.buffer, coverage.byteOffset, coverage.length);
|
|
1743
|
+
} else {
|
|
1744
|
+
data = Buffer.alloc(stride * bb.h);
|
|
1745
|
+
for (let y = 0; y < bb.h; ++y) {
|
|
1746
|
+
Buffer.from(coverage.buffer, coverage.byteOffset + y * bb.w, bb.w).copy(
|
|
1747
|
+
data,
|
|
1748
|
+
y * stride,
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
this.X.PutImage(
|
|
1753
|
+
2,
|
|
1754
|
+
tmpPixmap.id,
|
|
1755
|
+
this._clipGC(tmpPixmap),
|
|
1756
|
+
bb.w,
|
|
1757
|
+
bb.h,
|
|
1758
|
+
0,
|
|
1759
|
+
0,
|
|
1760
|
+
0,
|
|
1761
|
+
8,
|
|
1762
|
+
data,
|
|
1763
|
+
);
|
|
1764
|
+
return true;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
/**
|
|
1768
|
+
* GC for uploading coverage into a clip temp. Created against the first
|
|
1769
|
+
* temp and kept: every one of them is depth 8 on the same root, which is
|
|
1770
|
+
* all a GC binds to, and the temps themselves come and go per clip entry.
|
|
1771
|
+
*/
|
|
1772
|
+
_clipGC(tmpPixmap) {
|
|
1773
|
+
if (!this._clipMaskGC) {
|
|
1774
|
+
this._clipMaskGC = this.X.AllocID();
|
|
1775
|
+
this._gcs.push(this._clipMaskGC);
|
|
1776
|
+
this.X.CreateGC(this._clipMaskGC, tmpPixmap.id);
|
|
1777
|
+
}
|
|
1778
|
+
return this._clipMaskGC;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1672
1781
|
_rebuildClipMask() {
|
|
1673
1782
|
// The stack changed shape (restore, resize): drop the mask rather than
|
|
1674
1783
|
// rebuild it eagerly. A stack that went back to rectangles never needs
|
package/lib/text/fontsource.js
CHANGED
|
@@ -30,10 +30,11 @@
|
|
|
30
30
|
// fontconfig: ntk ships no fonts, so the app has to hand over the ones it
|
|
31
31
|
// ships, and the spec is the short way to say so.
|
|
32
32
|
//
|
|
33
|
-
// node:fs is fetched through
|
|
34
|
-
//
|
|
35
|
-
// whole purpose is running the text stack without a filesystem.
|
|
36
|
-
//
|
|
33
|
+
// node:fs is fetched lazily through builtin() inside the path branch, never a
|
|
34
|
+
// direct static import, because this file is the one the browser bundle keeps
|
|
35
|
+
// — its whole purpose is running the text stack without a filesystem. See
|
|
36
|
+
// builtin.js for how the lazy lookup stays version-tolerant.
|
|
37
|
+
import { builtin } from '../builtin.js';
|
|
37
38
|
import { charsetHas, matchSortedSync, noFontsError, prewarm, supported } from '../fontconfig.js';
|
|
38
39
|
import Font from './font.js';
|
|
39
40
|
|
|
@@ -267,7 +268,7 @@ const ACCEPTED =
|
|
|
267
268
|
'docs/fonts.md';
|
|
268
269
|
|
|
269
270
|
function fs() {
|
|
270
|
-
const mod =
|
|
271
|
+
const mod = builtin('node:fs');
|
|
271
272
|
if (!mod) {
|
|
272
273
|
throw new Error(
|
|
273
274
|
'ntk: a font path can only be read in node. In a browser, fetch the font ' +
|
package/lib/widgets/htmlview.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { selectAll } from 'css-select';
|
|
2
2
|
import { textContent } from 'domutils';
|
|
3
3
|
import { parseDocument } from 'htmlparser2';
|
|
4
|
+
import { builtin } from '../builtin.js';
|
|
4
5
|
import Yoga from '../yoga.js';
|
|
5
6
|
|
|
6
7
|
import { decodeImage, Image } from '../image.js';
|
|
@@ -467,15 +468,14 @@ export default class HtmlView {
|
|
|
467
468
|
// default policy: local files, only relative to an explicit baseUrl.
|
|
468
469
|
// node builtins are fetched lazily — in environments without them
|
|
469
470
|
// (browser bundles) pass a `loadResource` callback instead.
|
|
470
|
-
const
|
|
471
|
-
const fs = getBuiltin?.call(process, 'node:fs');
|
|
471
|
+
const fs = builtin('node:fs');
|
|
472
472
|
if (!fs || !this.baseUrl) return null;
|
|
473
473
|
let base = this.baseUrl;
|
|
474
474
|
if (typeof base === 'string' && !/^[a-z][a-z0-9+.-]*:/i.test(base)) {
|
|
475
475
|
// filesystem path; point at the directory itself when given one
|
|
476
476
|
let p = base;
|
|
477
477
|
if (!/[/\\]$/.test(p) && fs.existsSync(p) && fs.statSync(p).isDirectory()) p += '/';
|
|
478
|
-
base =
|
|
478
|
+
base = builtin('node:url').pathToFileURL(p);
|
|
479
479
|
}
|
|
480
480
|
const url = new URL(src, base);
|
|
481
481
|
if (url.protocol !== 'file:') return null;
|
package/lib/widgets/tex.js
CHANGED
|
@@ -21,14 +21,11 @@
|
|
|
21
21
|
import { parseSvgPath } from '../path.js';
|
|
22
22
|
import { flatten } from '../rasterize.js';
|
|
23
23
|
import { trapezoidize } from '../trapezoid.js';
|
|
24
|
+
import { builtin, nodeRequire } from '../builtin.js';
|
|
24
25
|
import Font from '../text/font.js';
|
|
25
26
|
|
|
26
|
-
// node builtins are fetched lazily so browser bundles
|
|
27
|
-
// there, inject the assets with configureTex() instead.
|
|
28
|
-
function nodeRequire() {
|
|
29
|
-
const mod = globalThis.process?.getBuiltinModule?.('node:module');
|
|
30
|
-
return mod ? mod.createRequire(import.meta.url) : null;
|
|
31
|
-
}
|
|
27
|
+
// node builtins are fetched lazily (nodeRequire/builtin) so browser bundles
|
|
28
|
+
// never resolve them; there, inject the assets with configureTex() instead.
|
|
32
29
|
|
|
33
30
|
// katex is sizable — load it on first formula, not on package import
|
|
34
31
|
let katex = null;
|
|
@@ -82,8 +79,8 @@ function katexFontFiles() {
|
|
|
82
79
|
'TeX rendering: KaTeX fonts cannot be read here — inject them with configureTex({ fonts })'
|
|
83
80
|
);
|
|
84
81
|
}
|
|
85
|
-
const { readdirSync } =
|
|
86
|
-
const { dirname, join } =
|
|
82
|
+
const { readdirSync } = builtin('node:fs');
|
|
83
|
+
const { dirname, join } = builtin('node:path');
|
|
87
84
|
fontDir = join(dirname(require.resolve('katex/package.json')), 'dist', 'fonts');
|
|
88
85
|
fontFiles = new Set(readdirSync(fontDir).filter((f) => f.endsWith('.ttf')));
|
|
89
86
|
}
|
package/lib/window.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import x11 from 'x11';
|
|
2
2
|
|
|
3
|
+
import { builtin } from './builtin.js';
|
|
3
4
|
import { safeRelease } from './cleanup.js';
|
|
4
5
|
import Drawable from './drawable.js';
|
|
5
6
|
import { packIcons, unpackIcons } from './imagedata.js';
|
|
@@ -696,27 +697,25 @@ export default class Window extends Drawable {
|
|
|
696
697
|
}
|
|
697
698
|
|
|
698
699
|
// grow-only backing pixmap (re)allocation, with headroom (see
|
|
699
|
-
// BACKING_GRANULARITY); new area is cleared to
|
|
700
|
+
// BACKING_GRANULARITY); new area is cleared to the window's background
|
|
701
|
+
// (white for opaque windows, fully transparent for depth-32 ARGB ones)
|
|
700
702
|
_allocBacking(w, h) {
|
|
701
703
|
const cur = this._backing;
|
|
702
704
|
if (cur && cur.width >= w && cur.height >= h) return;
|
|
703
705
|
const roundUp = (v) => Math.ceil(v / BACKING_GRANULARITY) * BACKING_GRANULARITY;
|
|
704
706
|
const newW = roundUp(Math.max(w, cur ? cur.width : 0));
|
|
705
707
|
const newH = roundUp(Math.max(h, cur ? cur.height : 0));
|
|
708
|
+
const depth = this.depth || this.display.screen[0].root_depth;
|
|
706
709
|
const pixmap = new Pixmap(this.app, {
|
|
707
710
|
parent: this,
|
|
708
711
|
width: newW,
|
|
709
712
|
height: newH,
|
|
710
|
-
|
|
711
|
-
// different depths is a BadMatch
|
|
712
|
-
depth: this.depth || this.display.screen[0].root_depth
|
|
713
|
+
depth
|
|
713
714
|
});
|
|
714
715
|
if (!this._clearGc) {
|
|
715
|
-
// reusable across reallocs: a GC is valid for any drawable of the
|
|
716
|
-
// same screen and depth, so one outlives every backing pixmap
|
|
717
716
|
this._clearGc = this.X.AllocID();
|
|
718
717
|
this.X.CreateGC(this._clearGc, pixmap.id, {
|
|
719
|
-
foreground: this.display.screen[0].white_pixel,
|
|
718
|
+
foreground: depth === 32 ? 0 : this.display.screen[0].white_pixel,
|
|
720
719
|
graphicsExposures: 0
|
|
721
720
|
});
|
|
722
721
|
}
|
|
@@ -1086,29 +1085,35 @@ export default class Window extends Drawable {
|
|
|
1086
1085
|
}
|
|
1087
1086
|
|
|
1088
1087
|
/**
|
|
1089
|
-
* Whether
|
|
1090
|
-
*
|
|
1088
|
+
* Whether a blit this window already owes is still waiting to go out —
|
|
1089
|
+
* because the fence for the last frame is unanswered, or because a present
|
|
1090
|
+
* is deferred behind the minimum inter-blit interval.
|
|
1091
1091
|
*
|
|
1092
1092
|
* The one bit of frame-clock state worth publishing, because it is the
|
|
1093
1093
|
* difference between the two ways a toolkit can answer a discrete input.
|
|
1094
1094
|
* Drawing the response inside the event handler costs a frame less latency:
|
|
1095
1095
|
* the blit goes out with the handler's own requests (see the `_present()`
|
|
1096
1096
|
* at the end of the event dispatch) instead of waiting for the next paced
|
|
1097
|
-
* frame
|
|
1098
|
-
*
|
|
1099
|
-
*
|
|
1100
|
-
* deferred until the ack and coalesces with the one already pending, so only
|
|
1101
|
-
* the last paint is ever seen. So `false`
|
|
1097
|
+
* frame. Drawing it when a blit is already queued costs a frame's *work* for
|
|
1098
|
+
* nothing: the paint lands in the same backing store and coalesces into the
|
|
1099
|
+
* present already pending, so only the last one is ever seen. So `false`
|
|
1102
1100
|
* means "draw it now", `true` means "leave it to the frame clock" — which
|
|
1103
1101
|
* is how a burst of discrete events (a spun wheel) paints its first notch
|
|
1104
1102
|
* immediately and folds the rest into one catch-up frame.
|
|
1105
1103
|
*
|
|
1106
|
-
*
|
|
1107
|
-
*
|
|
1108
|
-
*
|
|
1104
|
+
* Both gates have to be reported, not just the fence. The fence is the one
|
|
1105
|
+
* that bites on a slow connection. The minimum inter-blit interval (#195) is
|
|
1106
|
+
* the one that bites on a fast local server, where the fence for a notch is
|
|
1107
|
+
* answered well before the next notch is even read, and what actually holds
|
|
1108
|
+
* the burst back is the interval. Reporting only the fence there answers
|
|
1109
|
+
* "draw it now" to every notch — the precise work this gate exists to skip.
|
|
1110
|
+
*
|
|
1111
|
+
* `false` whenever nothing gates blits at all: `frameSync: false` sends no
|
|
1112
|
+
* fence and `frameInterval: 0` keeps no interval, so a caller who asked for
|
|
1113
|
+
* both gets the unpaced behaviour those options ask for.
|
|
1109
1114
|
*/
|
|
1110
1115
|
frameInFlight() {
|
|
1111
|
-
return this._frame.inFlight;
|
|
1116
|
+
return this._frame.inFlight || this._presentPending;
|
|
1112
1117
|
}
|
|
1113
1118
|
|
|
1114
1119
|
_present() {
|
|
@@ -1911,7 +1916,7 @@ export default class Window extends Drawable {
|
|
|
1911
1916
|
/** os.hostname(), looked up once, and absent in a browser bundle. */
|
|
1912
1917
|
static _hostname() {
|
|
1913
1918
|
if (Window._cachedHostname === undefined) {
|
|
1914
|
-
const os =
|
|
1919
|
+
const os = builtin('node:os');
|
|
1915
1920
|
Window._cachedHostname = os?.hostname?.() ?? null;
|
|
1916
1921
|
}
|
|
1917
1922
|
return Window._cachedHostname;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ntk",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.6.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",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"lib"
|
|
31
31
|
],
|
|
32
32
|
"engines": {
|
|
33
|
-
"node": ">=
|
|
33
|
+
"node": ">=18.19.0"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"bidi-js": "^1.0.3",
|