ntk 8.13.0 → 8.14.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 +30 -2
- package/lib/clipboard.js +16 -5
- package/lib/cliprect.js +128 -0
- package/lib/color.js +21 -0
- package/lib/fontconfig.js +273 -55
- package/lib/renderingcontext_2d.js +111 -16
- package/lib/text/font.js +31 -1
- package/lib/text/fontmanager.js +34 -3
- package/lib/text/fontsource.js +25 -0
- package/lib/text/layout.js +167 -26
- package/lib/text/marks.js +212 -0
- package/package.json +1 -1
package/lib/app.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { connectionGone } from './cleanup.js';
|
|
2
|
+
import { cssColor } from './color.js';
|
|
2
3
|
import Clipboard from './clipboard.js';
|
|
3
4
|
import { CursorCache } from './cursor.js';
|
|
4
5
|
import {
|
|
@@ -147,6 +148,8 @@ export default class App {
|
|
|
147
148
|
sharedGlyphsFor(this);
|
|
148
149
|
this._cursors = null;
|
|
149
150
|
this._solidPictures = new Map();
|
|
151
|
+
// the same pictures by the CSS colour that asked for them (solidPictureOf)
|
|
152
|
+
this._solidByName = new Map();
|
|
150
153
|
this._rasterizer = undefined;
|
|
151
154
|
this._shm = undefined;
|
|
152
155
|
this._extensionPromises = new Map();
|
|
@@ -345,12 +348,16 @@ export default class App {
|
|
|
345
348
|
const X = this.X;
|
|
346
349
|
if (connectionGone(X)) return;
|
|
347
350
|
const name = '_NET_WM_CM_S0';
|
|
351
|
+
// Each answer can arrive after the app has closed — a window that
|
|
352
|
+
// presented once and was torn down at once — and a request made from it
|
|
353
|
+
// then throws out of the reply dispatch, where no caller can catch it.
|
|
348
354
|
X.QueryExtension('XWAYLAND', (xwErr, ext) => {
|
|
349
355
|
this._xwayland = !xwErr && !!ext?.present;
|
|
356
|
+
if (connectionGone(X)) return;
|
|
350
357
|
X.InternAtom(false, name, (atomErr, atom) => {
|
|
351
|
-
if (atomErr || !atom) return;
|
|
358
|
+
if (atomErr || !atom || connectionGone(X)) return;
|
|
352
359
|
X.GetSelectionOwner(atom, (ownerErr, owner) => {
|
|
353
|
-
if (ownerErr) return;
|
|
360
|
+
if (ownerErr || connectionGone(X)) return;
|
|
354
361
|
this._setCompositing(owner !== 0);
|
|
355
362
|
this.clipboard
|
|
356
363
|
.watch(name, (ev) => this._setCompositing(ev.reason === 'new-owner' && ev.owner !== 0))
|
|
@@ -988,6 +995,26 @@ export default class App {
|
|
|
988
995
|
return p;
|
|
989
996
|
}
|
|
990
997
|
|
|
998
|
+
/**
|
|
999
|
+
* The solid picture a CSS colour string paints with. A UI sets the same few
|
|
1000
|
+
* strings again on every paint — every box's background, every border —
|
|
1001
|
+
* and the way to `solidPicture` is a parse, a premultiply and a key made of
|
|
1002
|
+
* four numbers turned into text; by the string it is one lookup. Throws, as
|
|
1003
|
+
* `fillStyle` always has, for a string that is not a colour.
|
|
1004
|
+
*/
|
|
1005
|
+
solidPictureOf(value) {
|
|
1006
|
+
let p = this._solidByName.get(value);
|
|
1007
|
+
if (p !== undefined) return p;
|
|
1008
|
+
const c = cssColor(value);
|
|
1009
|
+
if (!c) throw new Error(`Not a color: ${JSON.stringify(value)}`);
|
|
1010
|
+
p = this.solidPicture(c[0], c[1], c[2], c[3]);
|
|
1011
|
+
// spellings are unbounded where solids are not: an app animating a
|
|
1012
|
+
// colour writes a new string every frame
|
|
1013
|
+
if (this._solidByName.size >= 1024) this._solidByName.clear();
|
|
1014
|
+
this._solidByName.set(value, p);
|
|
1015
|
+
return p;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
991
1018
|
// flush pending requests and close the connection
|
|
992
1019
|
close() {
|
|
993
1020
|
if (this._cursors) {
|
|
@@ -1008,6 +1035,7 @@ export default class App {
|
|
|
1008
1035
|
picture._sourcePixmap?.destroy();
|
|
1009
1036
|
}
|
|
1010
1037
|
this._solidPictures.clear();
|
|
1038
|
+
this._solidByName.clear();
|
|
1011
1039
|
// GPU contexts are shared by every direct GL surface on this connection
|
|
1012
1040
|
// (see renderingcontext_gles.js), so they outlive individual contexts and
|
|
1013
1041
|
// are the connection's to release. Each holds an EGL display, a GBM device
|
package/lib/clipboard.js
CHANGED
|
@@ -63,6 +63,7 @@ export default class Clipboard {
|
|
|
63
63
|
this._fixes = null; // XFixes extension, required on first watch()
|
|
64
64
|
this._fixesFirstEvent = -1; // its event base, to recognise its events
|
|
65
65
|
this._selectionWatchers = new Map(); // selection atom -> { name, handlers }
|
|
66
|
+
this._listening = false; // _onXEvent is on the raw event stream
|
|
66
67
|
this._onXEvent = this._onXEvent.bind(this);
|
|
67
68
|
}
|
|
68
69
|
|
|
@@ -328,11 +329,17 @@ export default class Clipboard {
|
|
|
328
329
|
// never delivers them to the Window wrapper — listen on the raw
|
|
329
330
|
// client instead. INCR chunk pacing needs PropertyNotify for the
|
|
330
331
|
// requestor's window, which is not ours either.
|
|
331
|
-
this.
|
|
332
|
+
this._listen();
|
|
332
333
|
})();
|
|
333
334
|
return this._ready;
|
|
334
335
|
}
|
|
335
336
|
|
|
337
|
+
_listen() {
|
|
338
|
+
if (this._listening) return;
|
|
339
|
+
this._listening = true;
|
|
340
|
+
this.X.on('event', this._onXEvent);
|
|
341
|
+
}
|
|
342
|
+
|
|
336
343
|
/**
|
|
337
344
|
* Call `handler` whenever a selection changes hands.
|
|
338
345
|
*
|
|
@@ -355,6 +362,9 @@ export default class Clipboard {
|
|
|
355
362
|
*
|
|
356
363
|
* Watchers share one server-side registration per selection, so watching
|
|
357
364
|
* the same selection twice costs one extra callback and no extra protocol.
|
|
365
|
+
* The registration is on the root window, as GDK's is, so watching makes
|
|
366
|
+
* no window of the clipboard's own: an app that only watches — every app
|
|
367
|
+
* does, through `App#compositing` — has none.
|
|
358
368
|
*
|
|
359
369
|
* @param {string} selection selection atom name, e.g. 'CLIPBOARD' or 'PRIMARY'
|
|
360
370
|
* @param {function} handler called with the event
|
|
@@ -364,9 +374,10 @@ export default class Clipboard {
|
|
|
364
374
|
if (typeof handler !== 'function') {
|
|
365
375
|
throw new TypeError('clipboard: watch needs a handler function');
|
|
366
376
|
}
|
|
367
|
-
await this._ensure();
|
|
368
377
|
const fixes = await this._ensureFixes();
|
|
369
378
|
const sel = await this._atom(selection);
|
|
379
|
+
this._listen();
|
|
380
|
+
const root = this.app.display.screen[0].root;
|
|
370
381
|
|
|
371
382
|
let entry = this._selectionWatchers.get(sel);
|
|
372
383
|
if (!entry) {
|
|
@@ -376,7 +387,7 @@ export default class Clipboard {
|
|
|
376
387
|
fixes.SelectionEventMask.SetSelectionOwner |
|
|
377
388
|
fixes.SelectionEventMask.SelectionWindowDestroy |
|
|
378
389
|
fixes.SelectionEventMask.SelectionClientClose;
|
|
379
|
-
safeRelease(this.X, () => fixes.SelectSelectionInput(
|
|
390
|
+
safeRelease(this.X, () => fixes.SelectSelectionInput(root, sel, mask));
|
|
380
391
|
}
|
|
381
392
|
entry.handlers.add(handler);
|
|
382
393
|
|
|
@@ -390,7 +401,7 @@ export default class Clipboard {
|
|
|
390
401
|
if (current.handlers.size) return;
|
|
391
402
|
// last watcher for this selection: drop the server-side registration
|
|
392
403
|
this._selectionWatchers.delete(sel);
|
|
393
|
-
safeRelease(this.X, () => fixes.SelectSelectionInput(
|
|
404
|
+
safeRelease(this.X, () => fixes.SelectSelectionInput(root, sel, 0));
|
|
394
405
|
};
|
|
395
406
|
}
|
|
396
407
|
|
|
@@ -522,7 +533,6 @@ export default class Clipboard {
|
|
|
522
533
|
}
|
|
523
534
|
|
|
524
535
|
_onXEvent(ev) {
|
|
525
|
-
if (!this._window) return;
|
|
526
536
|
// XFixes events carry a server-assigned type above the core range, so
|
|
527
537
|
// this cannot be confused with core SelectionNotify, which is 31
|
|
528
538
|
if (ev.type === this._fixesFirstEvent && this._selectionWatchers.size) {
|
|
@@ -532,6 +542,7 @@ export default class Clipboard {
|
|
|
532
542
|
);
|
|
533
543
|
return;
|
|
534
544
|
}
|
|
545
|
+
if (!this._window) return;
|
|
535
546
|
if (ev.type === 28) {
|
|
536
547
|
// PropertyNotify, state 1 = Deleted: a requestor consumed the INCR
|
|
537
548
|
// chunk we gave it and is asking for the next one
|
package/lib/cliprect.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Geometry clipped to a rectangle, for what goes to the server in XRender's
|
|
2
|
+
// 16.16 fixed point: trapezoids and triangles. A coordinate past 32,767 there
|
|
3
|
+
// overflows the 32-bit word node-x11 writes it into, and the RangeError comes
|
|
4
|
+
// out of the paint that drew it. A box of a code block twenty thousand lines
|
|
5
|
+
// tall in a scroll pane was one, filled as a rounded rectangle.
|
|
6
|
+
//
|
|
7
|
+
// Only what reaches past what the wire can carry is clipped (`WIRE_REACH`):
|
|
8
|
+
// everything else goes out as it came, so a drawing that fits is sent exactly
|
|
9
|
+
// as before.
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* How far from a picture's origin a coordinate may reach and still be
|
|
13
|
+
* carried: half of what 16.16 fixed point and a 16-bit origin hold, so an
|
|
14
|
+
* offset the size of a surface on top of it still fits. Geometry within it
|
|
15
|
+
* goes to the server as it came, and the server clips it — cutting it here
|
|
16
|
+
* costs more than that, and a zoomed graph has hundreds of edges a little
|
|
17
|
+
* past the window every frame. Only geometry past it is cut.
|
|
18
|
+
*/
|
|
19
|
+
export const WIRE_REACH = 16384;
|
|
20
|
+
|
|
21
|
+
/** Whether every point of a flat `[x, y, …]` list is inside the rectangle. */
|
|
22
|
+
export function withinRect(pts, x0, y0, x1, y1) {
|
|
23
|
+
for (let i = 0; i < pts.length; i += 2) {
|
|
24
|
+
const x = pts[i];
|
|
25
|
+
const y = pts[i + 1];
|
|
26
|
+
if (x < x0 || x > x1 || y < y0 || y > y1) return false;
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A closed ring clipped to the rectangle (Sutherland–Hodgman), as a flat
|
|
33
|
+
* `[x, y, …]` list, empty when nothing of it is inside. Clipping each ring of
|
|
34
|
+
* a shape on its own keeps its fill, nonzero or even-odd, the same at every
|
|
35
|
+
* point inside a convex region.
|
|
36
|
+
*/
|
|
37
|
+
export function clipRingToRect(pts, x0, y0, x1, y1) {
|
|
38
|
+
let ring = pts;
|
|
39
|
+
// each edge of the rectangle: which coordinate, which bound, which side
|
|
40
|
+
for (const [axis, bound, keepAbove] of [
|
|
41
|
+
[0, x0, true],
|
|
42
|
+
[0, x1, false],
|
|
43
|
+
[1, y0, true],
|
|
44
|
+
[1, y1, false],
|
|
45
|
+
]) {
|
|
46
|
+
const n = ring.length / 2;
|
|
47
|
+
if (n === 0) break;
|
|
48
|
+
const out = [];
|
|
49
|
+
const inside = (i) =>
|
|
50
|
+
keepAbove ? ring[i * 2 + axis] >= bound : ring[i * 2 + axis] <= bound;
|
|
51
|
+
for (let i = 0; i < n; i++) {
|
|
52
|
+
const j = (i + n - 1) % n; // the previous point
|
|
53
|
+
const iin = inside(i);
|
|
54
|
+
const jin = inside(j);
|
|
55
|
+
if (iin !== jin) {
|
|
56
|
+
// where the segment from j to i crosses the bound
|
|
57
|
+
const a = ring[j * 2 + axis];
|
|
58
|
+
const b = ring[i * 2 + axis];
|
|
59
|
+
const t = (bound - a) / (b - a);
|
|
60
|
+
const other = 1 - axis;
|
|
61
|
+
const oa = ring[j * 2 + other];
|
|
62
|
+
const ob = ring[i * 2 + other];
|
|
63
|
+
const p = [0, 0];
|
|
64
|
+
p[axis] = bound;
|
|
65
|
+
p[other] = oa + (ob - oa) * t;
|
|
66
|
+
out.push(p[0], p[1]);
|
|
67
|
+
}
|
|
68
|
+
if (iin) out.push(ring[i * 2], ring[i * 2 + 1]);
|
|
69
|
+
}
|
|
70
|
+
ring = out;
|
|
71
|
+
}
|
|
72
|
+
return ring;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A flat list of triangles, six numbers each, with every one that reaches
|
|
77
|
+
* past the rectangle clipped to it and cut back into triangles (a fan: what
|
|
78
|
+
* is left of a triangle inside a rectangle is convex).
|
|
79
|
+
*/
|
|
80
|
+
export function clipTrianglesToRect(tris, x0, y0, x1, y1) {
|
|
81
|
+
const out = [];
|
|
82
|
+
for (let i = 0; i + 5 < tris.length; i += 6) {
|
|
83
|
+
const tri = tris.slice(i, i + 6);
|
|
84
|
+
if (withinRect(tri, x0, y0, x1, y1)) {
|
|
85
|
+
out.push(...tri);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const poly = clipRingToRect(tri, x0, y0, x1, y1);
|
|
89
|
+
for (let k = 2; k * 2 + 1 < poly.length; k++) {
|
|
90
|
+
out.push(
|
|
91
|
+
poly[0],
|
|
92
|
+
poly[1],
|
|
93
|
+
poly[(k - 1) * 2],
|
|
94
|
+
poly[(k - 1) * 2 + 1],
|
|
95
|
+
poly[k * 2],
|
|
96
|
+
poly[k * 2 + 1],
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A flat `[x, y, w, h, …]` list of rectangles cut to `[0, width) × [0,
|
|
105
|
+
* height)`, dropping the ones with nothing left: core X's rectangles are 16
|
|
106
|
+
* bits. The list itself when every rectangle is already on the surface.
|
|
107
|
+
*/
|
|
108
|
+
export function rectsOnSurface(flat, width, height) {
|
|
109
|
+
let out = null;
|
|
110
|
+
for (let i = 0; i + 3 < flat.length; i += 4) {
|
|
111
|
+
const x = flat[i];
|
|
112
|
+
const y = flat[i + 1];
|
|
113
|
+
const w = flat[i + 2];
|
|
114
|
+
const h = flat[i + 3];
|
|
115
|
+
const inside = x >= 0 && y >= 0 && x + w <= width && y + h <= height;
|
|
116
|
+
if (inside) {
|
|
117
|
+
if (out) out.push(x, y, w, h);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (!out) out = flat.slice(0, i);
|
|
121
|
+
const x0 = Math.max(x, 0);
|
|
122
|
+
const y0 = Math.max(y, 0);
|
|
123
|
+
const x1 = Math.min(x + w, width);
|
|
124
|
+
const y1 = Math.min(y + h, height);
|
|
125
|
+
if (x1 > x0 && y1 > y0) out.push(x0, y0, x1 - x0, y1 - y0);
|
|
126
|
+
}
|
|
127
|
+
return out ?? flat;
|
|
128
|
+
}
|
package/lib/color.js
CHANGED
|
@@ -42,6 +42,17 @@ function parseHex(value) {
|
|
|
42
42
|
return [part(0) / 255, part(1) / 255, part(2) / 255, hasAlpha ? part(3) / 255 : 1];
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
// Each spelling is parsed once. A UI draws with a few dozen colours and sets
|
|
46
|
+
// them again on every paint — every box's background, every border, every
|
|
47
|
+
// edge of a graph — and a parse is a regular expression and an allocation
|
|
48
|
+
// or two: 6% of a 2D graph pan's frame, 3% of a table's. Bounded, and
|
|
49
|
+
// dropped whole rather than evicted, since a palette is a few dozen strings
|
|
50
|
+
// (react-x11's Cocoa context keeps the same cache for the same reason). The
|
|
51
|
+
// caller gets a copy, as it always has: an array handed out is its to keep
|
|
52
|
+
// or scale.
|
|
53
|
+
const parsed = new Map();
|
|
54
|
+
const PARSED_MAX = 512;
|
|
55
|
+
|
|
45
56
|
/**
|
|
46
57
|
* Parse a CSS colour to `[r, g, b, a]` floats in 0..1 with **straight**
|
|
47
58
|
* (unassociated) alpha, or null if it is not a colour.
|
|
@@ -57,6 +68,16 @@ function parseHex(value) {
|
|
|
57
68
|
*/
|
|
58
69
|
export function cssColorStraight(value) {
|
|
59
70
|
if (typeof value !== 'string') return null;
|
|
71
|
+
let rgba = parsed.get(value);
|
|
72
|
+
if (rgba === undefined) {
|
|
73
|
+
rgba = parseStraight(value);
|
|
74
|
+
if (parsed.size >= PARSED_MAX) parsed.clear();
|
|
75
|
+
parsed.set(value, rgba);
|
|
76
|
+
}
|
|
77
|
+
return rgba && rgba.slice();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseStraight(value) {
|
|
60
81
|
const v = value.trim();
|
|
61
82
|
if (!v) return null;
|
|
62
83
|
if (v.toLowerCase() === 'transparent') return [0, 0, 0, 0];
|