ntk 8.1.1 → 8.3.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 +339 -7
- package/lib/drawable.js +10 -0
- package/lib/events_map.js +100 -1
- package/lib/glyphdirectory.js +275 -0
- package/lib/glyphdwire.js +142 -0
- package/lib/glyphset.js +32 -5
- package/lib/index.js +25 -2
- package/lib/pictformat.js +196 -0
- package/lib/pixmap.js +166 -7
- package/lib/region.js +148 -0
- package/lib/renderingcontext_2d.js +830 -235
- package/lib/shapeglyphs.js +94 -26
- package/lib/sharedglyphs.js +433 -0
- package/lib/text/glyphs.js +204 -26
- package/lib/widgets/svgview.js +7 -5
- package/lib/window.js +218 -28
- package/lib/xembed.js +3 -1
- package/package.json +2 -2
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// The `_NTK_GLYPHD` glyph directory (docs/shared-glyphs.md): the naming half
|
|
2
|
+
// of the display-wide shared glyph cache.
|
|
3
|
+
//
|
|
4
|
+
// The directory does not rasterize and knows nothing about fonts. It owns
|
|
5
|
+
// three things and nothing else: the ICCCM manager selection that names it,
|
|
6
|
+
// the map from page tokens to server-side glyphsets, and the compact local
|
|
7
|
+
// id space inside each set — it is the single writer of lids, which is what
|
|
8
|
+
// keeps them dense enough for CompositeGlyphs8. Presence is a bit it tracks
|
|
9
|
+
// per (token, member key): clients report uploads (`_NTK_GLYPHD_ADDED`), and
|
|
10
|
+
// later askers are told "already there".
|
|
11
|
+
//
|
|
12
|
+
// Lifetime contract, load-bearing for the client's XID-reuse fence: while
|
|
13
|
+
// the directory owns the selection it frees nothing. Eviction opens a new
|
|
14
|
+
// *generation* — fresh sets advertised, old ones no longer handed out; the
|
|
15
|
+
// retired sets stay pinned by this connection (their ids are the cheap part;
|
|
16
|
+
// Xorg dedupes the pixel storage by content anyway) and die with it, or
|
|
17
|
+
// when the last client alias drops, whichever is later.
|
|
18
|
+
//
|
|
19
|
+
// Every ntk app embeds one of these behind the same code path: the first app
|
|
20
|
+
// on a display self-elects (lib/sharedglyphs.js calls `claim()` when nobody
|
|
21
|
+
// owns the selection), a standalone directory process would claim it the
|
|
22
|
+
// same way and simply outlive the apps.
|
|
23
|
+
|
|
24
|
+
import x11 from 'x11';
|
|
25
|
+
|
|
26
|
+
import GlyphSet from './glyphset.js';
|
|
27
|
+
import {
|
|
28
|
+
GLYPHD,
|
|
29
|
+
MAX_LIDS,
|
|
30
|
+
encodeGlyphdReply,
|
|
31
|
+
parseGlyphdRequest
|
|
32
|
+
} from './glyphdwire.js';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Directory-side policy: the byte budget across the current generation's
|
|
36
|
+
* pages. Uploads reported by clients accumulate against it; exceeding it
|
|
37
|
+
* opens a new generation. It bounds *growth*, not total server memory —
|
|
38
|
+
* retired generations froze at whatever size they reached.
|
|
39
|
+
*/
|
|
40
|
+
export const DEFAULT_SHARED_GLYPHS_POLICY = {
|
|
41
|
+
budgetBytes: 32 << 20
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const TIME_TIMEOUT = 2000;
|
|
45
|
+
|
|
46
|
+
export default class GlyphDirectory {
|
|
47
|
+
/**
|
|
48
|
+
* @param {App} app the connection the directory rides
|
|
49
|
+
* @param {object} [options] `{ budgetBytes }` over DEFAULT_SHARED_GLYPHS_POLICY
|
|
50
|
+
*/
|
|
51
|
+
constructor(app, options) {
|
|
52
|
+
this.app = app;
|
|
53
|
+
this.X = app.X;
|
|
54
|
+
this.policy = { ...DEFAULT_SHARED_GLYPHS_POLICY, ...options };
|
|
55
|
+
this.window = null;
|
|
56
|
+
this.generation = 1;
|
|
57
|
+
this.pages = new Map(); // token -> { set, members: Map(key -> {lid, present}), bytes }
|
|
58
|
+
this.active = false;
|
|
59
|
+
this._atoms = null;
|
|
60
|
+
this._bytes = 0; // uploads reported into the current generation
|
|
61
|
+
this._retired = []; // GlyphSets of past generations: pinned, never freed
|
|
62
|
+
this._onXEvent = this._onXEvent.bind(this);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** the selection endpoint's window id, 0 before claim() */
|
|
66
|
+
get wid() {
|
|
67
|
+
return this.window ? this.window.id : 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Claim the manager selection (ICCCM 2.8): an unmapped window, a real
|
|
72
|
+
* timestamp, `SetSelectionOwner`, and a `MANAGER` announcement on the root
|
|
73
|
+
* so waiting clients adopt the new directory.
|
|
74
|
+
*
|
|
75
|
+
* @returns {Promise<number>} the window id of whoever owns the selection
|
|
76
|
+
* afterwards — ours when the claim won, the winner's when it raced and
|
|
77
|
+
* lost, 0 when the server refused the whole dance
|
|
78
|
+
*/
|
|
79
|
+
async claim() {
|
|
80
|
+
const X = this.X;
|
|
81
|
+
try {
|
|
82
|
+
const names = [GLYPHD.selection, GLYPHD.ensure, GLYPHD.added, GLYPHD.done, GLYPHD.time];
|
|
83
|
+
const [selection, ensure, added, done, time] = await Promise.all(
|
|
84
|
+
names.map((name) => this._atom(name))
|
|
85
|
+
);
|
|
86
|
+
this._atoms = { selection, ensure, added, done, time };
|
|
87
|
+
this.window = this.app.createWindow({
|
|
88
|
+
width: 1,
|
|
89
|
+
height: 1,
|
|
90
|
+
eventMask: x11.eventMask.PropertyChange
|
|
91
|
+
});
|
|
92
|
+
// ICCCM 2.1: never CurrentTime. The property-append trick gives a real
|
|
93
|
+
// server timestamp; 0 (CurrentTime) is the documented fallback when
|
|
94
|
+
// the server will not answer, which every server ntk targets accepts.
|
|
95
|
+
const stamp = await this._serverTime();
|
|
96
|
+
X.SetSelectionOwner(this.window.id, selection, stamp);
|
|
97
|
+
// SetSelectionOwner is void, and a race is settled by whoever the
|
|
98
|
+
// server says owns the selection now
|
|
99
|
+
const owner = await new Promise((resolve, reject) =>
|
|
100
|
+
X.GetSelectionOwner(selection, (err, wid) => (err ? reject(err) : resolve(wid)))
|
|
101
|
+
);
|
|
102
|
+
if (owner !== this.window.id) {
|
|
103
|
+
this.window.destroy();
|
|
104
|
+
this.window = null;
|
|
105
|
+
return owner;
|
|
106
|
+
}
|
|
107
|
+
this.active = true;
|
|
108
|
+
this.window.on('message', (ev) => this._onMessage(ev));
|
|
109
|
+
// SelectionClear (a successor took over) carries the owner in
|
|
110
|
+
// ev.owner, not ev.wid, so the Window wrapper never sees it — raw
|
|
111
|
+
// listener, the same reason the clipboard has one
|
|
112
|
+
X.on('event', this._onXEvent);
|
|
113
|
+
// announce (ICCCM 2.8): MANAGER on the root with the acquisition
|
|
114
|
+
// timestamp, StructureNotify mask — what surviving clients of a dead
|
|
115
|
+
// directory are listening for
|
|
116
|
+
await this.app
|
|
117
|
+
.rootWindow()
|
|
118
|
+
.sendClientMessage(GLYPHD.manager, [stamp, selection, this.window.id], {
|
|
119
|
+
mask: x11.eventMask.StructureNotify
|
|
120
|
+
});
|
|
121
|
+
return this.window.id;
|
|
122
|
+
} catch {
|
|
123
|
+
if (this.window) {
|
|
124
|
+
this.window.destroy();
|
|
125
|
+
this.window = null;
|
|
126
|
+
}
|
|
127
|
+
this.active = false;
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Stop answering. The sets stay: clients still hold aliases into them, and
|
|
134
|
+
* the fence contract ("frees nothing while it owns the selection") has
|
|
135
|
+
* already ended with the ownership — nothing here may free what a reply
|
|
136
|
+
* has advertised, so everything is simply kept until the connection ends.
|
|
137
|
+
*/
|
|
138
|
+
deactivate() {
|
|
139
|
+
if (!this.active) return;
|
|
140
|
+
this.active = false;
|
|
141
|
+
this.X.removeListener('event', this._onXEvent);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
_onXEvent(ev) {
|
|
145
|
+
// SelectionClear: another manager took the selection with a later
|
|
146
|
+
// timestamp — the ICCCM handover. It answers from here on.
|
|
147
|
+
if (ev.type === 29 && this.window && ev.owner === this.window.id) this.deactivate();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
_onMessage(ev) {
|
|
151
|
+
if (!this.active || !this._atoms) return;
|
|
152
|
+
if (ev.message_type === this._atoms.ensure) {
|
|
153
|
+
this._serve(ev, true).catch(() => {});
|
|
154
|
+
} else if (ev.message_type === this._atoms.added) {
|
|
155
|
+
this._serve(ev, false).catch(() => {});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* One RPC: read the request property off the client's mailbox (the read
|
|
161
|
+
* deletes it), answer, confirm with DONE. Failures on the client's side —
|
|
162
|
+
* a mailbox that vanished mid-request — are its own; every cross-client
|
|
163
|
+
* request here swallows its error rather than warning about a dead peer.
|
|
164
|
+
*/
|
|
165
|
+
async _serve(ev, isEnsure) {
|
|
166
|
+
const X = this.X;
|
|
167
|
+
const [mailbox, serial, property] = ev.data;
|
|
168
|
+
const done = (status) => {
|
|
169
|
+
X.SendClientMessage(mailbox, mailbox, this._atoms.done, 32, [serial, status], 0, () => true);
|
|
170
|
+
};
|
|
171
|
+
const prop = await new Promise((resolve) =>
|
|
172
|
+
X.GetProperty(1, mailbox, property, 0, 0, 0x1fffffff, (err, res) => resolve(err ? null : res))
|
|
173
|
+
);
|
|
174
|
+
const request = prop && prop.data ? parseGlyphdRequest(prop.data) : null;
|
|
175
|
+
if (!request || !request.token) return done(0);
|
|
176
|
+
if (isEnsure) {
|
|
177
|
+
const entries = this._ensure(request);
|
|
178
|
+
if (!entries) return done(0);
|
|
179
|
+
const reply = encodeGlyphdReply({
|
|
180
|
+
serial,
|
|
181
|
+
generation: this.generation,
|
|
182
|
+
gsid: this.pages.get(request.token).set.id,
|
|
183
|
+
entries
|
|
184
|
+
});
|
|
185
|
+
X.ChangeProperty(0, mailbox, property, property, 8, reply, () => true);
|
|
186
|
+
return done(1);
|
|
187
|
+
}
|
|
188
|
+
this._added(request);
|
|
189
|
+
return done(1);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Allocate (or look up) a lid per member key, in request order. The single
|
|
194
|
+
* writer of the lid space is what keeps ids compact — dense from 0 in
|
|
195
|
+
* first-asked order, whichever client asked.
|
|
196
|
+
* @returns {Array<{lid, present}>|null} null refuses the request
|
|
197
|
+
*/
|
|
198
|
+
_ensure({ token, keys }) {
|
|
199
|
+
let page = this.pages.get(token);
|
|
200
|
+
if (!page) {
|
|
201
|
+
page = { set: new GlyphSet(this.app), members: new Map(), bytes: 0 };
|
|
202
|
+
this.pages.set(token, page);
|
|
203
|
+
}
|
|
204
|
+
const entries = new Array(keys.length);
|
|
205
|
+
for (let i = 0; i < keys.length; i++) {
|
|
206
|
+
let member = page.members.get(keys[i]);
|
|
207
|
+
if (!member) {
|
|
208
|
+
if (page.members.size >= MAX_LIDS) return null;
|
|
209
|
+
member = { lid: page.members.size, present: false };
|
|
210
|
+
page.members.set(keys[i], member);
|
|
211
|
+
}
|
|
212
|
+
entries[i] = member;
|
|
213
|
+
}
|
|
214
|
+
return entries;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* A client uploaded: flip the presence bits so later askers skip the
|
|
219
|
+
* rasterization, and count the bytes against the generation budget. A
|
|
220
|
+
* token this generation does not advertise (retired since the client
|
|
221
|
+
* bound) is ignored — the uploader still draws from its own alias, and
|
|
222
|
+
* the next generation simply re-earns those glyphs.
|
|
223
|
+
*/
|
|
224
|
+
_added({ token, keys, bytes }) {
|
|
225
|
+
const page = this.pages.get(token);
|
|
226
|
+
if (!page) return;
|
|
227
|
+
for (const key of keys) {
|
|
228
|
+
const member = page.members.get(key);
|
|
229
|
+
if (member) member.present = true;
|
|
230
|
+
}
|
|
231
|
+
page.bytes += bytes;
|
|
232
|
+
this._bytes += bytes;
|
|
233
|
+
if (this._bytes > this.policy.budgetBytes) this._openGeneration();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Eviction, directory style: no set is freed — the current pages retire
|
|
238
|
+
* (pinned here so not even a GC finalizer can free what was advertised)
|
|
239
|
+
* and the next ensure of each token starts a fresh set in a fresh
|
|
240
|
+
* generation. Clients bound to a retired (gsid, generation) notice the
|
|
241
|
+
* mismatch in their next reply and freeze that page.
|
|
242
|
+
*/
|
|
243
|
+
_openGeneration() {
|
|
244
|
+
for (const page of this.pages.values()) this._retired.push(page.set);
|
|
245
|
+
this.pages.clear();
|
|
246
|
+
this._bytes = 0;
|
|
247
|
+
this.generation++;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// a current server timestamp, via the zero-length property append trick
|
|
251
|
+
// (the same one the clipboard uses): the PropertyNotify it provokes
|
|
252
|
+
// carries the server's clock
|
|
253
|
+
_serverTime() {
|
|
254
|
+
return new Promise((resolve) => {
|
|
255
|
+
const finish = (time) => {
|
|
256
|
+
clearTimeout(timer);
|
|
257
|
+
this.window.removeListener('property', onProperty);
|
|
258
|
+
resolve(time);
|
|
259
|
+
};
|
|
260
|
+
const onProperty = (ev) => {
|
|
261
|
+
if (ev.atom === this._atoms.time && ev.state === 0) finish(ev.time >>> 0);
|
|
262
|
+
};
|
|
263
|
+
const timer = setTimeout(() => finish(0), TIME_TIMEOUT);
|
|
264
|
+
timer.unref?.();
|
|
265
|
+
this.window.on('property', onProperty);
|
|
266
|
+
this.X.ChangeProperty(2, this.window.id, this._atoms.time, this.X.atoms.STRING, 8, Buffer.alloc(0));
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
_atom(name) {
|
|
271
|
+
return new Promise((resolve, reject) =>
|
|
272
|
+
this.X.InternAtom(false, name, (err, atom) => (err ? reject(err) : resolve(atom)))
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// The `_NTK_GLYPHD` wire vocabulary (docs/shared-glyphs.md): the atoms and
|
|
2
|
+
// the property payload codecs shared by the directory (lib/glyphdirectory.js)
|
|
3
|
+
// and the client (lib/sharedglyphs.js). Pure functions, no X in sight —
|
|
4
|
+
// which is what makes the protocol unit-testable without a server.
|
|
5
|
+
|
|
6
|
+
/** selection + message vocabulary */
|
|
7
|
+
export const GLYPHD = {
|
|
8
|
+
selection: '_NTK_GLYPHD',
|
|
9
|
+
ensure: '_NTK_GLYPHD_ENSURE',
|
|
10
|
+
added: '_NTK_GLYPHD_ADDED',
|
|
11
|
+
done: '_NTK_GLYPHD_DONE',
|
|
12
|
+
property: '_NTK_GLYPHD_RPC',
|
|
13
|
+
time: '_NTK_GLYPHD_TIME',
|
|
14
|
+
manager: 'MANAGER'
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** wire format version of the RPC properties */
|
|
18
|
+
export const RPC_VERSION = 1;
|
|
19
|
+
|
|
20
|
+
/** lids are compact so CompositeGlyphs can use narrow encodings; a page
|
|
21
|
+
* cannot outgrow 16-bit ids (fonts cap at 65535 glyphs; corner populations
|
|
22
|
+
* are far smaller). The directory refuses rather than overflows. */
|
|
23
|
+
export const MAX_LIDS = 65536;
|
|
24
|
+
|
|
25
|
+
const FLAG_INDICES = 1;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The ensure/added request property (format 8, little-endian):
|
|
29
|
+
*
|
|
30
|
+
* u8 version (1)
|
|
31
|
+
* u8 flags (bit 0: members are u32 font glyph indices, not strings)
|
|
32
|
+
* u16 token byte length T
|
|
33
|
+
* u32 member count N
|
|
34
|
+
* u32 uploaded bytes (added only; 0 in an ensure)
|
|
35
|
+
* T token bytes
|
|
36
|
+
* N members — u32 each, or (u16 length + bytes) each
|
|
37
|
+
*/
|
|
38
|
+
export function encodeGlyphdRequest({ token, indices, keys, bytes = 0 }) {
|
|
39
|
+
const tokenBuf = Buffer.from(token, 'utf8');
|
|
40
|
+
let size = 12 + tokenBuf.length;
|
|
41
|
+
const keyBufs = indices ? null : keys.map((k) => Buffer.from(String(k), 'utf8'));
|
|
42
|
+
size += indices ? keys.length * 4 : keyBufs.reduce((n, b) => n + 2 + b.length, 0);
|
|
43
|
+
const buf = Buffer.alloc(size);
|
|
44
|
+
buf.writeUInt8(RPC_VERSION, 0);
|
|
45
|
+
buf.writeUInt8(indices ? FLAG_INDICES : 0, 1);
|
|
46
|
+
buf.writeUInt16LE(tokenBuf.length, 2);
|
|
47
|
+
buf.writeUInt32LE(keys.length, 4);
|
|
48
|
+
buf.writeUInt32LE(bytes, 8);
|
|
49
|
+
tokenBuf.copy(buf, 12);
|
|
50
|
+
let o = 12 + tokenBuf.length;
|
|
51
|
+
for (let i = 0; i < keys.length; i++) {
|
|
52
|
+
if (indices) {
|
|
53
|
+
buf.writeUInt32LE(keys[i] >>> 0, o);
|
|
54
|
+
o += 4;
|
|
55
|
+
} else {
|
|
56
|
+
const kb = keyBufs[i];
|
|
57
|
+
buf.writeUInt16LE(kb.length, o);
|
|
58
|
+
kb.copy(buf, o + 2);
|
|
59
|
+
o += 2 + kb.length;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return buf;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** @returns {{token, indices, keys, bytes}|null} null when malformed */
|
|
66
|
+
export function parseGlyphdRequest(buf) {
|
|
67
|
+
try {
|
|
68
|
+
if (buf.length < 12 || buf.readUInt8(0) !== RPC_VERSION) return null;
|
|
69
|
+
const indices = (buf.readUInt8(1) & FLAG_INDICES) !== 0;
|
|
70
|
+
const tokenLen = buf.readUInt16LE(2);
|
|
71
|
+
const count = buf.readUInt32LE(4);
|
|
72
|
+
const bytes = buf.readUInt32LE(8);
|
|
73
|
+
let o = 12;
|
|
74
|
+
if (o + tokenLen > buf.length) return null;
|
|
75
|
+
const token = buf.toString('utf8', o, o + tokenLen);
|
|
76
|
+
o += tokenLen;
|
|
77
|
+
const keys = new Array(count);
|
|
78
|
+
for (let i = 0; i < count; i++) {
|
|
79
|
+
if (indices) {
|
|
80
|
+
if (o + 4 > buf.length) return null;
|
|
81
|
+
keys[i] = buf.readUInt32LE(o);
|
|
82
|
+
o += 4;
|
|
83
|
+
} else {
|
|
84
|
+
if (o + 2 > buf.length) return null;
|
|
85
|
+
const len = buf.readUInt16LE(o);
|
|
86
|
+
if (o + 2 + len > buf.length) return null;
|
|
87
|
+
keys[i] = buf.toString('utf8', o + 2, o + 2 + len);
|
|
88
|
+
o += 2 + len;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { token, indices, keys, bytes };
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The ensure reply property (format 8), member entries in request order:
|
|
99
|
+
*
|
|
100
|
+
* u8 version (1)
|
|
101
|
+
* u8 status (1 — a refusal sends no reply property at all)
|
|
102
|
+
* u16 0
|
|
103
|
+
* u32 serial (echo of the request's)
|
|
104
|
+
* u32 generation
|
|
105
|
+
* u32 gsid
|
|
106
|
+
* u32 member count N
|
|
107
|
+
* N u32 each: bit 31 = present, bits 0..30 = lid
|
|
108
|
+
*/
|
|
109
|
+
export function encodeGlyphdReply({ serial, generation, gsid, entries }) {
|
|
110
|
+
const buf = Buffer.alloc(20 + entries.length * 4);
|
|
111
|
+
buf.writeUInt8(RPC_VERSION, 0);
|
|
112
|
+
buf.writeUInt8(1, 1);
|
|
113
|
+
buf.writeUInt32LE(serial >>> 0, 4);
|
|
114
|
+
buf.writeUInt32LE(generation >>> 0, 8);
|
|
115
|
+
buf.writeUInt32LE(gsid >>> 0, 12);
|
|
116
|
+
buf.writeUInt32LE(entries.length, 16);
|
|
117
|
+
entries.forEach((e, i) => {
|
|
118
|
+
buf.writeUInt32LE(((e.present ? 0x80000000 : 0) | e.lid) >>> 0, 20 + i * 4);
|
|
119
|
+
});
|
|
120
|
+
return buf;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** @returns {{serial, generation, gsid, entries: [{lid, present}]}|null} */
|
|
124
|
+
export function parseGlyphdReply(buf) {
|
|
125
|
+
try {
|
|
126
|
+
if (buf.length < 20 || buf.readUInt8(0) !== RPC_VERSION) return null;
|
|
127
|
+
if (buf.readUInt8(1) !== 1) return null;
|
|
128
|
+
const serial = buf.readUInt32LE(4);
|
|
129
|
+
const generation = buf.readUInt32LE(8);
|
|
130
|
+
const gsid = buf.readUInt32LE(12);
|
|
131
|
+
const count = buf.readUInt32LE(16);
|
|
132
|
+
if (buf.length < 20 + count * 4) return null;
|
|
133
|
+
const entries = new Array(count);
|
|
134
|
+
for (let i = 0; i < count; i++) {
|
|
135
|
+
const word = buf.readUInt32LE(20 + i * 4);
|
|
136
|
+
entries[i] = { lid: word & 0x7fffffff, present: (word & 0x80000000) !== 0 };
|
|
137
|
+
}
|
|
138
|
+
return { serial, generation, gsid, entries };
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
package/lib/glyphset.js
CHANGED
|
@@ -9,14 +9,41 @@ const registry = new FinalizationRegistry(({ Render, X, id }) => {
|
|
|
9
9
|
|
|
10
10
|
export default class GlyphSet {
|
|
11
11
|
constructor(app, format) {
|
|
12
|
-
|
|
13
|
-
this.
|
|
12
|
+
this._bind(app);
|
|
13
|
+
this.Render.CreateGlyphSet(this.id, format || this.Render.a8);
|
|
14
|
+
this._register();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A new id for an existing glyphset — RENDER `ReferenceGlyphSet`. XIDs are
|
|
19
|
+
* display-global, so `gsid` may name a set another connection created; the
|
|
20
|
+
* server refcounts, and the underlying set stays alive as long as any
|
|
21
|
+
* reference to it does — including across its creator's disconnect. That
|
|
22
|
+
* is what a cross-process glyph cache stands on (docs/shared-glyphs.md).
|
|
23
|
+
* `destroy()` drops only this alias.
|
|
24
|
+
*
|
|
25
|
+
* @param {App} app
|
|
26
|
+
* @param {number} gsid existing glyphset XID, ours or another client's
|
|
27
|
+
* @returns {GlyphSet}
|
|
28
|
+
*/
|
|
29
|
+
static referenceTo(app, gsid) {
|
|
30
|
+
const gs = Object.create(GlyphSet.prototype);
|
|
31
|
+
gs._bind(app);
|
|
32
|
+
gs.Render.ReferenceGlyphSet(gs.id, gsid);
|
|
33
|
+
gs._register();
|
|
34
|
+
return gs;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_bind(app) {
|
|
38
|
+
this.X = app.X;
|
|
14
39
|
this.display = app.display;
|
|
15
40
|
this.Render = app.display.Render;
|
|
16
|
-
this.id = X.AllocID();
|
|
17
|
-
|
|
41
|
+
this.id = this.X.AllocID();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_register() {
|
|
18
45
|
this._owned = true;
|
|
19
|
-
registry.register(this, { Render: this.Render, X, id: this.id }, this);
|
|
46
|
+
registry.register(this, { Render: this.Render, X: this.X, id: this.id }, this);
|
|
20
47
|
}
|
|
21
48
|
|
|
22
49
|
addGlyphs(glyphs) {
|
package/lib/index.js
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import { decodeKey, groupForState } from './keyboard.js';
|
|
20
20
|
import Pixmap from './pixmap.js';
|
|
21
21
|
import Picture from './picture.js';
|
|
22
|
+
import Region from './region.js';
|
|
22
23
|
import { Image, decodeImage, loadImage } from './image.js';
|
|
23
24
|
import {
|
|
24
25
|
ImageData,
|
|
@@ -48,6 +49,8 @@ import {
|
|
|
48
49
|
} from './rasterize.js';
|
|
49
50
|
import { DEFAULT_MASK_POLICY } from './maskcluster.js';
|
|
50
51
|
import { DEFAULT_SHAPE_POLICY } from './shapeglyphs.js';
|
|
52
|
+
import { DEFAULT_SHARED_GLYPHS_POLICY } from './glyphdirectory.js';
|
|
53
|
+
import { warmSharedGlyphs } from './text/glyphs.js';
|
|
51
54
|
import { TextLayout } from './text/layout.js';
|
|
52
55
|
import SvgView from './widgets/svgview.js';
|
|
53
56
|
import { cssColor, cssColorStraight, premultiply } from './color.js';
|
|
@@ -95,7 +98,11 @@ const DEFAULT_BUFFER_REQUESTS = { maxSize: 64 * 1024 };
|
|
|
95
98
|
* `'direct'` or `'off'`; an object sets the direct backend's knobs too (see
|
|
96
99
|
* docs/context-gles.md) — and
|
|
97
100
|
* { onXError } — called with X protocol errors no request callback
|
|
98
|
-
* claimed (default: console.warn)
|
|
101
|
+
* claimed (default: console.warn) —
|
|
102
|
+
* { sharedGlyphs } — the cross-process shared glyph cache
|
|
103
|
+
* (docs/shared-glyphs.md): on by default, `false` turns it off (as does
|
|
104
|
+
* the `NTK_NO_SHARED_GLYPHS` environment variable), an object sets the
|
|
105
|
+
* directory-side budget (`{ budgetBytes }`).
|
|
99
106
|
* x11's { bufferRequests } defaults to a 64 KB output buffer here, which
|
|
100
107
|
* makes a frame one socket write; pass false to write per request.
|
|
101
108
|
* @param {function} [callback] optional node-style callback
|
|
@@ -150,6 +157,15 @@ export function createClient(options, callback) {
|
|
|
150
157
|
if (process.env.NTK_STRICT_COLORS) Render.strictColors = true;
|
|
151
158
|
display.GLX = glxError ? null : GLX;
|
|
152
159
|
|
|
160
|
+
const app = new App(display, appOptions);
|
|
161
|
+
// Build the visual -> picture format table (issue #295) now: a
|
|
162
|
+
// picture is bound synchronously, so the table has to be here
|
|
163
|
+
// before anything draws rather than fetched at the first paint.
|
|
164
|
+
// It costs the connection nothing and no wait — node-x11 asked
|
|
165
|
+
// `QueryPictFormats` itself while requiring RENDER and kept the
|
|
166
|
+
// reply, so this only reads it. A server that cannot
|
|
167
|
+
// answer leaves every format picked from the depth, as before.
|
|
168
|
+
app.pictFormats().catch(() => null);
|
|
153
169
|
// RENDER requires the client to declare its version before using
|
|
154
170
|
// requests beyond 0.0, and the reply is what gates them afterwards
|
|
155
171
|
// — CreateSolidFill needs 0.10 (see App#solidPicture). 0.11 is the
|
|
@@ -176,7 +192,6 @@ export function createClient(options, callback) {
|
|
|
176
192
|
});
|
|
177
193
|
updateKeyboardMapping(display.min_keycode, display.max_keycode);
|
|
178
194
|
|
|
179
|
-
const app = new App(display, appOptions);
|
|
180
195
|
// Under a policy that could pick the direct backend, find out
|
|
181
196
|
// during the handshake whether it can: two extension queries,
|
|
182
197
|
// running alongside the round trips already in flight here, so
|
|
@@ -225,6 +240,9 @@ export {
|
|
|
225
240
|
resolveCursorShape,
|
|
226
241
|
Pixmap,
|
|
227
242
|
Picture,
|
|
243
|
+
// server-side XFIXES regions (docs/context-2d.md#region-clips):
|
|
244
|
+
// app.createRegion(rects) makes one, ctx.clipRegion(region) clips to it
|
|
245
|
+
Region,
|
|
228
246
|
Surface,
|
|
229
247
|
// fill/stroke styles the 2d context hands back (docs/context-2d.md) —
|
|
230
248
|
// exported for `instanceof`, not to be constructed directly
|
|
@@ -255,6 +273,11 @@ export {
|
|
|
255
273
|
DEFAULT_RASTER_POLICY,
|
|
256
274
|
DEFAULT_MASK_POLICY,
|
|
257
275
|
DEFAULT_SHAPE_POLICY,
|
|
276
|
+
// cross-process shared glyphs (docs/shared-glyphs.md): the directory-side
|
|
277
|
+
// budget, and the prewarm that makes a first paint of already-shared text
|
|
278
|
+
// rasterize and upload nothing
|
|
279
|
+
DEFAULT_SHARED_GLYPHS_POLICY,
|
|
280
|
+
warmSharedGlyphs,
|
|
258
281
|
TextLayout,
|
|
259
282
|
SvgView,
|
|
260
283
|
cssColor,
|