ntk 8.2.0 → 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 +717 -235
- package/lib/shapeglyphs.js +94 -26
- package/lib/sharedglyphs.js +433 -0
- package/lib/text/glyphs.js +143 -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
package/lib/text/glyphs.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import GlyphSet from '../glyphset.js';
|
|
2
2
|
import Picture from '../picture.js';
|
|
3
3
|
import Pixmap from '../pixmap.js';
|
|
4
|
+
import { fontPageToken, sharedGlyphsFor } from '../sharedglyphs.js';
|
|
4
5
|
import { trapezoidize } from '../trapezoid.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -37,6 +38,25 @@ function policyOf(app) {
|
|
|
37
38
|
return app.textPolicy ? { ...DEFAULT_TEXT_POLICY, ...app.textPolicy } : DEFAULT_TEXT_POLICY;
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
/** a fresh AddGlyphs object — fresh per call because node-x11 mutates what
|
|
42
|
+
* it is given (pads rows, divides offX by 64); the source bitmap survives */
|
|
43
|
+
function uploadGlyph(lid, bitmap, adv) {
|
|
44
|
+
return {
|
|
45
|
+
id: lid,
|
|
46
|
+
width: bitmap ? bitmap.width : 0,
|
|
47
|
+
height: bitmap ? bitmap.height : 0,
|
|
48
|
+
// XRender GLYPHINFO places the image at origin - (x, y); node-x11
|
|
49
|
+
// packs -x and +y, so x here is the bitmap's left bearing and y its
|
|
50
|
+
// (negated, y-down) top — i.e. the ascent above the baseline
|
|
51
|
+
x: bitmap ? bitmap.left : 0,
|
|
52
|
+
y: bitmap ? -bitmap.top : 0,
|
|
53
|
+
// node-x11 AddGlyphs expects 26.6 fixed point and divides by 64
|
|
54
|
+
offX: adv * 64,
|
|
55
|
+
offY: 0,
|
|
56
|
+
image: bitmap ? bitmap.data : Buffer.alloc(0)
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
40
60
|
/**
|
|
41
61
|
* Server-side glyph cache for one (font face, pixel size) pair.
|
|
42
62
|
*
|
|
@@ -51,32 +71,52 @@ function policyOf(app) {
|
|
|
51
71
|
* (`offX`), so the server advances the pen automatically and runs of
|
|
52
72
|
* unkerned text need no per-glyph position data at all.
|
|
53
73
|
* - Bitmaps upload lazily, once per glyph, batched into one AddGlyphs
|
|
54
|
-
* request per draw that introduces new glyphs
|
|
74
|
+
* request per draw that introduces new glyphs — and, when the display's
|
|
75
|
+
* shared glyph directory is live (docs/shared-glyphs.md), re-bind to the
|
|
76
|
+
* display-wide set so other ntk processes skip the work entirely.
|
|
55
77
|
*/
|
|
56
78
|
export class GlyphPage {
|
|
57
79
|
constructor(app, font, size) {
|
|
58
80
|
this.app = app;
|
|
59
81
|
this.font = font;
|
|
60
82
|
this.size = size;
|
|
61
|
-
this
|
|
62
|
-
|
|
63
|
-
|
|
83
|
+
// With shared glyphs off this page is exactly what it always was: a
|
|
84
|
+
// private set from birth. With them on, the set is minted lazily — a
|
|
85
|
+
// page that resolves entirely from the directory never creates one.
|
|
86
|
+
this.glyphset = sharedGlyphsFor(app) ? null : new GlyphSet(app);
|
|
87
|
+
this.entries = new Map(); // font glyph id -> { lid, adv, gs }
|
|
88
|
+
this.bytes = 0; // privately uploaded bitmap bytes, for the LRU budget
|
|
89
|
+
this._lids = 0; // next private lid (dense, upload order)
|
|
90
|
+
this._maxLid = -1; // widest id this page composites, private or shared
|
|
91
|
+
this._privateCount = 0; // entries still bound to the private set
|
|
92
|
+
this._shared = undefined; // SharedPage binding, resolved on first ensure
|
|
64
93
|
}
|
|
65
94
|
|
|
66
95
|
/** number of bits per glyph needed to address this page's ids */
|
|
67
96
|
get bits() {
|
|
68
|
-
return this.
|
|
97
|
+
return this._maxLid > 255 ? 16 : 8;
|
|
69
98
|
}
|
|
70
99
|
|
|
71
100
|
/**
|
|
72
|
-
* Ensure all glyphs of a shaped run are
|
|
73
|
-
*
|
|
101
|
+
* Ensure all glyphs of a shaped run are drawable; returns nothing.
|
|
102
|
+
*
|
|
103
|
+
* New glyphs are rasterized and sent in a single AddGlyphs request to the
|
|
104
|
+
* page's private set — this draw's pixels never wait on anything. When the
|
|
105
|
+
* shared directory is live they are also asked about (docs/shared-glyphs.md):
|
|
106
|
+
* the reply re-binds their entries to the display-wide set — uploading the
|
|
107
|
+
* retained bitmap only when this process was first to need the glyph — and
|
|
108
|
+
* once nothing private remains the private set is freed. Every later draw
|
|
109
|
+
* of those glyphs, in this process and every other, is then pure
|
|
110
|
+
* CompositeGlyphs. A page that wants the shared entries *before* first
|
|
111
|
+
* paint warms up front: see `warmSharedGlyphs`.
|
|
74
112
|
*/
|
|
75
113
|
ensure(glyphs) {
|
|
76
114
|
let batch = null;
|
|
115
|
+
let ask = null;
|
|
116
|
+
const shared = this._sharedBinding();
|
|
77
117
|
for (const g of glyphs) {
|
|
78
118
|
if (this.entries.has(g.id)) continue;
|
|
79
|
-
const lid = this.
|
|
119
|
+
const lid = this._lids++;
|
|
80
120
|
if (lid >= 65536) {
|
|
81
121
|
// 2^16 distinct glyphs at one size — not reachable in practice
|
|
82
122
|
// (fonts cap at 65535 glyphs) but fail loudly rather than corrupt
|
|
@@ -84,36 +124,77 @@ export class GlyphPage {
|
|
|
84
124
|
}
|
|
85
125
|
const adv = Math.round(this.font.advanceOf(g.id, this.size));
|
|
86
126
|
const bitmap = this.font.rasterize(g.id, this.size);
|
|
87
|
-
this.
|
|
127
|
+
if (!this.glyphset) this.glyphset = new GlyphSet(this.app);
|
|
128
|
+
if (lid > this._maxLid) this._maxLid = lid;
|
|
129
|
+
this.entries.set(g.id, { lid, adv, gs: this.glyphset.id, private: true });
|
|
130
|
+
this._privateCount++;
|
|
88
131
|
if (!batch) batch = [];
|
|
89
|
-
batch.push(
|
|
90
|
-
id: lid,
|
|
91
|
-
width: bitmap ? bitmap.width : 0,
|
|
92
|
-
height: bitmap ? bitmap.height : 0,
|
|
93
|
-
// XRender GLYPHINFO places the image at origin - (x, y); node-x11
|
|
94
|
-
// packs -x and +y, so x here is the bitmap's left bearing and y its
|
|
95
|
-
// (negated, y-down) top — i.e. the ascent above the baseline
|
|
96
|
-
x: bitmap ? bitmap.left : 0,
|
|
97
|
-
y: bitmap ? -bitmap.top : 0,
|
|
98
|
-
// node-x11 AddGlyphs expects 26.6 fixed point and divides by 64
|
|
99
|
-
offX: adv * 64,
|
|
100
|
-
offY: 0,
|
|
101
|
-
image: bitmap ? bitmap.data : Buffer.alloc(0)
|
|
102
|
-
});
|
|
132
|
+
batch.push(uploadGlyph(lid, bitmap, adv));
|
|
103
133
|
this.bytes += bitmap ? bitmap.data.length : 0;
|
|
134
|
+
if (shared && shared.open) {
|
|
135
|
+
if (!ask) ask = [];
|
|
136
|
+
ask.push({ key: g.id, payload: { bitmap, adv } });
|
|
137
|
+
}
|
|
104
138
|
}
|
|
105
139
|
if (batch) this.glyphset.addGlyphs(batch);
|
|
140
|
+
if (ask) shared.ask(ask);
|
|
106
141
|
}
|
|
107
142
|
|
|
108
143
|
entry(fontGlyphId) {
|
|
109
144
|
return this.entries.get(fontGlyphId);
|
|
110
145
|
}
|
|
111
146
|
|
|
112
|
-
/**
|
|
147
|
+
/** the shared side of this page, bound on first use; null when the
|
|
148
|
+
* feature is off or the font's bytes cannot be content-addressed */
|
|
149
|
+
_sharedBinding() {
|
|
150
|
+
if (this._shared !== undefined) return this._shared;
|
|
151
|
+
const client = sharedGlyphsFor(this.app);
|
|
152
|
+
const token = client ? fontPageToken(this.font, this.size) : null;
|
|
153
|
+
this._shared = token
|
|
154
|
+
? client.bindPage({
|
|
155
|
+
token,
|
|
156
|
+
indices: true, // member keys are the font glyph indices themselves
|
|
157
|
+
makeGlyph: (key, payload, lid) => {
|
|
158
|
+
// absent from the shared set: upload the bitmap retained at
|
|
159
|
+
// mint time, or rasterize now on the warm path (which retained
|
|
160
|
+
// nothing precisely because the glyph was expected to be there)
|
|
161
|
+
const bitmap =
|
|
162
|
+
payload.bitmap !== undefined ? payload.bitmap : this.font.rasterize(key, this.size);
|
|
163
|
+
return uploadGlyph(lid, bitmap, payload.adv);
|
|
164
|
+
},
|
|
165
|
+
adopt: (key, lid, gsid, payload) => this._adoptShared(key, lid, gsid, payload)
|
|
166
|
+
})
|
|
167
|
+
: null;
|
|
168
|
+
return this._shared;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** re-bind one glyph to the shared set (confirmed present, or uploaded by
|
|
172
|
+
* us); drop the private set once nothing composites from it any more */
|
|
173
|
+
_adoptShared(key, lid, gsid, payload) {
|
|
174
|
+
const prev = this.entries.get(key);
|
|
175
|
+
this.entries.set(key, { lid, adv: payload.adv, gs: gsid });
|
|
176
|
+
if (lid > this._maxLid) this._maxLid = lid;
|
|
177
|
+
if (prev && prev.private && --this._privateCount === 0 && this.glyphset) {
|
|
178
|
+
// ordered after any CompositeGlyphs already issued, like the LRU's
|
|
179
|
+
// frees; new glyphs during a directory outage mint a fresh set
|
|
180
|
+
this.glyphset.destroy();
|
|
181
|
+
this.glyphset = null;
|
|
182
|
+
this.bytes = 0;
|
|
183
|
+
this._lids = 0;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** free the server-side glyphset and shared alias (LRU eviction / shutdown) */
|
|
113
188
|
destroy() {
|
|
114
|
-
this.glyphset.destroy();
|
|
189
|
+
if (this.glyphset) this.glyphset.destroy();
|
|
190
|
+
this.glyphset = null;
|
|
191
|
+
if (this._shared) this._shared.destroy();
|
|
192
|
+
this._shared = undefined;
|
|
115
193
|
this.entries.clear();
|
|
116
194
|
this.bytes = 0;
|
|
195
|
+
this._lids = 0;
|
|
196
|
+
this._maxLid = -1;
|
|
197
|
+
this._privateCount = 0;
|
|
117
198
|
}
|
|
118
199
|
}
|
|
119
200
|
|
|
@@ -133,6 +214,42 @@ export function getGlyphPage(app, font, size) {
|
|
|
133
214
|
return page;
|
|
134
215
|
}
|
|
135
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Warm one (font, size) page from the shared glyph directory before first
|
|
219
|
+
* paint (docs/shared-glyphs.md): resolve `text`'s glyphs into shared-set
|
|
220
|
+
* entries so that the first draw of already-shared text rasterizes nothing
|
|
221
|
+
* and uploads nothing — the cold-start win a synchronous first `fillText`
|
|
222
|
+
* cannot have, since it cannot wait for the directory's answer and falls
|
|
223
|
+
* back to a private upload for exactly one frame instead.
|
|
224
|
+
*
|
|
225
|
+
* Glyphs the directory has never seen are rasterized here and uploaded once,
|
|
226
|
+
* which is the same work the first draw would have done. Resolves `true`
|
|
227
|
+
* when the page ended up bound to the shared cache; `false` means the
|
|
228
|
+
* feature is off or degraded and drawing will use the private path — either
|
|
229
|
+
* way the following draws are correct.
|
|
230
|
+
*
|
|
231
|
+
* @param {App} app
|
|
232
|
+
* @param {Font} font
|
|
233
|
+
* @param {number} size pixel size
|
|
234
|
+
* @param {string} text whose glyphs to warm (shaped with defaults)
|
|
235
|
+
* @returns {Promise<boolean>}
|
|
236
|
+
*/
|
|
237
|
+
export async function warmSharedGlyphs(app, font, size, text) {
|
|
238
|
+
if (!sharedGlyphsFor(app)) return false;
|
|
239
|
+
const page = getGlyphPage(app, font, size);
|
|
240
|
+
const shared = page._sharedBinding();
|
|
241
|
+
if (!shared || !shared.open) return false;
|
|
242
|
+
const ask = [];
|
|
243
|
+
const seen = new Set();
|
|
244
|
+
for (const g of font.shape(String(text), size).glyphs) {
|
|
245
|
+
if (page.entries.has(g.id) || seen.has(g.id)) continue;
|
|
246
|
+
seen.add(g.id);
|
|
247
|
+
ask.push({ key: g.id, payload: { adv: Math.round(font.advanceOf(g.id, size)) } });
|
|
248
|
+
}
|
|
249
|
+
if (ask.length) await shared.ask(ask);
|
|
250
|
+
return shared.open && shared.bound;
|
|
251
|
+
}
|
|
252
|
+
|
|
136
253
|
/**
|
|
137
254
|
* Evict least-recently-used glyph pages until uploaded bitmaps fit the
|
|
138
255
|
* policy budget. `inUse` pages (referenced by requests queued this draw)
|
|
@@ -422,7 +539,7 @@ function drawBitmapGlyphRuns(app, op, srcId, dstId, positioned, policy) {
|
|
|
422
539
|
for (const pos of positionGlyphs(positioned)) {
|
|
423
540
|
const page = pages.get(pos.run);
|
|
424
541
|
const e = page.entry(pos.glyph.id);
|
|
425
|
-
items.push({ gs:
|
|
542
|
+
items.push({ gs: e.gs, lid: e.lid, adv: e.adv, x: pos.x, y: pos.y });
|
|
426
543
|
}
|
|
427
544
|
const encoded = encodeGlyphItems(items, bits);
|
|
428
545
|
if (!encoded) return;
|
package/lib/widgets/svgview.js
CHANGED
|
@@ -220,9 +220,11 @@ function pathBBox(path) {
|
|
|
220
220
|
* inline `style` beats the presentation attribute, the initial fill is black
|
|
221
221
|
* and the initial stroke is none, `<line>` never fills, `<use>` paints its
|
|
222
222
|
* target with the *use* element's style, and non-rendered subtrees
|
|
223
|
-
* contribute nothing. It starts at the root
|
|
224
|
-
*
|
|
225
|
-
*
|
|
223
|
+
* contribute nothing. It starts at the root element itself, because `draw`
|
|
224
|
+
* does: the root `<svg>` is an ordinary element for inheritance, and every
|
|
225
|
+
* mainstream icon set puts `fill`/`stroke` there (issue #306). The two walks
|
|
226
|
+
* have to agree — a scan that skipped the root would disagree with what is
|
|
227
|
+
* painted.
|
|
226
228
|
*
|
|
227
229
|
* @returns {{ kind: 'mono'|'multi', solo: string|null }} `solo` is the one
|
|
228
230
|
* paint a `mono` document uses — a colour, or the literal `'currentColor'`
|
|
@@ -284,7 +286,7 @@ function scanPaints(root, ids) {
|
|
|
284
286
|
}
|
|
285
287
|
};
|
|
286
288
|
|
|
287
|
-
|
|
289
|
+
visit(root, INHERITED.fill, INHERITED.stroke, 0);
|
|
288
290
|
return {
|
|
289
291
|
kind: multi || paints.size > 1 ? 'multi' : 'mono',
|
|
290
292
|
solo: paints.size === 1 ? [...paints][0] : null
|
|
@@ -433,7 +435,7 @@ export default class SvgView {
|
|
|
433
435
|
this._renderChildren(
|
|
434
436
|
this._root,
|
|
435
437
|
ctx,
|
|
436
|
-
{ ...INHERITED, color: opts.color ?? this.color },
|
|
438
|
+
this._style(this._root, { ...INHERITED, color: opts.color ?? this.color }),
|
|
437
439
|
1,
|
|
438
440
|
0
|
|
439
441
|
);
|
package/lib/window.js
CHANGED
|
@@ -332,6 +332,24 @@ function warnHint(key, message) {
|
|
|
332
332
|
console.warn(`ntk: ${message} Further occurrences are not reported.`);
|
|
333
333
|
}
|
|
334
334
|
|
|
335
|
+
/**
|
|
336
|
+
* A GetGeometry reply in ntk's spelling. node-x11 calls the position `xPos`/
|
|
337
|
+
* `yPos` and the reply's root window `windowid`, neither of which is what X
|
|
338
|
+
* calls them; the `root` here is the root of the screen the drawable is on,
|
|
339
|
+
* not this window's parent.
|
|
340
|
+
*/
|
|
341
|
+
function unpackGeometry(res) {
|
|
342
|
+
return {
|
|
343
|
+
x: res.xPos,
|
|
344
|
+
y: res.yPos,
|
|
345
|
+
width: res.width,
|
|
346
|
+
height: res.height,
|
|
347
|
+
depth: res.depth,
|
|
348
|
+
borderWidth: res.borderWidth,
|
|
349
|
+
root: res.windowid
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
335
353
|
/** A Window, a Pixmap, or a bare XID — all of these name a server resource. */
|
|
336
354
|
function resourceId(value) {
|
|
337
355
|
return typeof value === 'number' ? value : (value?.id ?? 0);
|
|
@@ -404,18 +422,37 @@ export default class Window extends Drawable {
|
|
|
404
422
|
// the whole property without dropping what an earlier one set
|
|
405
423
|
this._sizeHints = null;
|
|
406
424
|
this._wmHints = null;
|
|
425
|
+
// `wnd.ready` (see the getter): resolved as soon as this wrapper knows
|
|
426
|
+
// its geometry — at the end of the constructor for a window ntk created,
|
|
427
|
+
// when GetGeometry replies for one adopted by id
|
|
407
428
|
this._readyPromiseResolve = null;
|
|
408
429
|
this._readyPromise = new Promise((resolve) => {
|
|
409
430
|
this._readyPromiseResolve = resolve;
|
|
410
431
|
});
|
|
432
|
+
// constructor round trips still outstanding, for an adopted window
|
|
433
|
+
this._readyPending = 0;
|
|
411
434
|
|
|
412
435
|
const parentId = args.parent ? args.parent.id : app.display.screen[0].root;
|
|
413
436
|
|
|
414
437
|
// double buffering (see docs/window.md): windows we created get a
|
|
415
438
|
// backing pixmap when a 2d context is requested, unless opted out
|
|
416
439
|
this._foreign = !!args.id;
|
|
440
|
+
// Adopting a window is not the same as "another client paints these
|
|
441
|
+
// pixels", though it is the safe default reading of one. The Composite
|
|
442
|
+
// overlay window and a window handed over by an embedding host (XEmbed's
|
|
443
|
+
// plug side, an `--wid` handoff) are ours completely — they are foreign
|
|
444
|
+
// only in the sense that `createWindow` did not make them, and they are
|
|
445
|
+
// exactly the windows that most want an unflickering double buffer and a
|
|
446
|
+
// vblank-paced blit. Saying so is `{ id, backingStore: true }` or
|
|
447
|
+
// `{ id, present: true }`; from there such a window behaves like any
|
|
448
|
+
// other window ntk owns (issue #294).
|
|
449
|
+
this._ownsPixels = !this._foreign || args.backingStore === true || args.present === true;
|
|
417
450
|
this._backingOptOut = args.backingStore === false;
|
|
418
451
|
this._backing = null;
|
|
452
|
+
// an adopted window has no geometry, depth or visual to build a backing
|
|
453
|
+
// pixmap from until its constructor's questions reply — see
|
|
454
|
+
// _enableBackingStoreWhenReady
|
|
455
|
+
this._backingScheduled = false;
|
|
419
456
|
// What the backing store clears to, and the window attribute it mirrors.
|
|
420
457
|
// Undefined means "nothing was asked for" — see _clearColour.
|
|
421
458
|
this._clearPixel = args.backgroundPixel;
|
|
@@ -528,11 +565,19 @@ export default class Window extends Drawable {
|
|
|
528
565
|
for (const name of forwardedXAttributes) {
|
|
529
566
|
if (args[name] !== undefined) values[name] = args[name];
|
|
530
567
|
}
|
|
568
|
+
// what setCursor compares against, so setting the cursor a window was
|
|
569
|
+
// created with is free rather than a redundant round trip
|
|
570
|
+
this._currentCursor = values.cursor;
|
|
531
571
|
// a window on a non-default visual (GLX, ARGB) also needs its own
|
|
532
572
|
// colormap and an explicit border pixel: inheriting either from a
|
|
533
573
|
// parent of a different depth is a BadMatch (X11 CreateWindow)
|
|
534
574
|
this.visual = args.visual ?? 0;
|
|
535
575
|
this.depth = args.depth ?? 0;
|
|
576
|
+
// `visual` is the CreateWindow argument, where 0 means CopyFromParent;
|
|
577
|
+
// `visualId` is the visual the pixels are actually in, which is what a
|
|
578
|
+
// picture format is picked from (issue #295). CopyFromParent is only
|
|
579
|
+
// ever the parent's, and the default parent is the root.
|
|
580
|
+
this.visualId = this.visual || args.parent?.visualId || app.display.screen[0].root_visual;
|
|
536
581
|
if (this.visual) {
|
|
537
582
|
if (values.colormap === undefined) {
|
|
538
583
|
this._ownedColormap = app.createColormap(this.visual);
|
|
@@ -549,23 +594,33 @@ export default class Window extends Drawable {
|
|
|
549
594
|
this.id = args.id;
|
|
550
595
|
this.eventMask = 0;
|
|
551
596
|
this.visual = 0;
|
|
597
|
+
// nothing is known about another client's window until it answers
|
|
598
|
+
this.visualId = 0;
|
|
552
599
|
this.depth = 0;
|
|
553
600
|
// an adopted window's geometry is not known until GetGeometry replies
|
|
554
601
|
this._deliveredGeom = null;
|
|
555
|
-
//
|
|
602
|
+
// Two questions, because a drawable's pixels take both to describe:
|
|
603
|
+
// GetGeometry for width, height and depth, GetWindowAttributes for the
|
|
604
|
+
// visual — depth alone does not name a picture format, and the windows
|
|
605
|
+
// whose format was picked wrongest are exactly these, the ones another
|
|
606
|
+
// client chose the visual for (issue #295). They go out in the same
|
|
607
|
+
// batch, so the pair costs one round trip, not two, and `ready` is the
|
|
608
|
+
// wait for both.
|
|
609
|
+
this._readyPending = 2;
|
|
556
610
|
X.GetGeometry(this.id, (err, res) => {
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
this.
|
|
611
|
+
// A window that was destroyed between the id reaching us and this
|
|
612
|
+
// request reaching the server answers BadWindow, and there is
|
|
613
|
+
// nothing to record. `ready` still resolves: adopting a window that
|
|
614
|
+
// has since gone is ordinary for a window manager, and a wait that
|
|
615
|
+
// never ends is worse than one that ends with width undefined.
|
|
616
|
+
this._readyPending--;
|
|
617
|
+
if (err) this._settleReady();
|
|
618
|
+
else this._applyGeometry(unpackGeometry(res));
|
|
619
|
+
});
|
|
620
|
+
X.GetWindowAttributes(this.id, (err, attrs) => {
|
|
621
|
+
this._readyPending--;
|
|
622
|
+
if (!err) this.visualId = attrs.visual;
|
|
623
|
+
this._settleReady();
|
|
569
624
|
});
|
|
570
625
|
}
|
|
571
626
|
|
|
@@ -630,11 +685,21 @@ export default class Window extends Drawable {
|
|
|
630
685
|
// region and an event selection it never uses. `present: true` is still
|
|
631
686
|
// honoured eagerly, for a caller who wants it up before the first paint.
|
|
632
687
|
this._presentOptOut = args.present === false;
|
|
633
|
-
if (args.present
|
|
688
|
+
if (args.present === true) {
|
|
634
689
|
// fire and forget: until the extensions answer, blits use CopyArea,
|
|
635
|
-
// which is what they would have done anyway
|
|
690
|
+
// which is what they would have done anyway. Present needs nothing of
|
|
691
|
+
// the window but its id, so an adopted one can have it up before its
|
|
692
|
+
// geometry has even replied.
|
|
636
693
|
this.enablePresent().catch((err) => this.app.options?.onXError?.(err));
|
|
637
694
|
}
|
|
695
|
+
// A window adopted with ownership declared starts its backing store now
|
|
696
|
+
// rather than waiting for the first getContext('2d'): the allocation
|
|
697
|
+
// needs the constructor's replies anyway, so starting it here means it
|
|
698
|
+
// is normally already there by the time the caller's `await wnd.ready`
|
|
699
|
+
// returns and it draws.
|
|
700
|
+
if (this._foreign && this._ownsPixels && !this._backingOptOut) {
|
|
701
|
+
this._enableBackingStoreWhenReady();
|
|
702
|
+
}
|
|
638
703
|
|
|
639
704
|
X.event_consumers[this.id] = this;
|
|
640
705
|
this.on('event', (ev) => {
|
|
@@ -717,7 +782,7 @@ export default class Window extends Drawable {
|
|
|
717
782
|
const child = new Window(app, { id: ev.wid });
|
|
718
783
|
const ntkev = { ...ev, parent: this, window: child, target: child };
|
|
719
784
|
// wait until we know that we track correct x,y,w,h values
|
|
720
|
-
child.
|
|
785
|
+
child.ready.then(() => {
|
|
721
786
|
this.emit(eventName, ntkev);
|
|
722
787
|
});
|
|
723
788
|
});
|
|
@@ -773,10 +838,82 @@ export default class Window extends Drawable {
|
|
|
773
838
|
});
|
|
774
839
|
|
|
775
840
|
if (typeof this.width !== 'undefined') {
|
|
776
|
-
this.
|
|
841
|
+
this._settleReady();
|
|
777
842
|
}
|
|
778
843
|
}
|
|
779
844
|
|
|
845
|
+
/**
|
|
846
|
+
* Resolves with this window once its geometry is known — immediately for a
|
|
847
|
+
* window ntk created (it already knows what it asked for), when the
|
|
848
|
+
* GetGeometry sent by the constructor replies for one adopted by id.
|
|
849
|
+
*
|
|
850
|
+
* Adoption is the case that needs it: until the reply lands, `width`,
|
|
851
|
+
* `height`, `x` and `y` are `undefined` and `depth` is `0`, so anything
|
|
852
|
+
* that picks a pixel format from the depth — `getContext('2d')`,
|
|
853
|
+
* `createPattern` — would pick the wrong one. Awaiting is safe
|
|
854
|
+
* unconditionally, so code that takes windows from either source need not
|
|
855
|
+
* ask which it has:
|
|
856
|
+
*
|
|
857
|
+
* const wnd = new Window(app, { id: ev.wid });
|
|
858
|
+
* await wnd.ready;
|
|
859
|
+
* const ctx = wnd.getContext('2d');
|
|
860
|
+
*
|
|
861
|
+
* The promise never rejects. A window destroyed before the reply arrives
|
|
862
|
+
* resolves all the same, with `width` still `undefined` — the request that
|
|
863
|
+
* hits the gone window is the one that reports it.
|
|
864
|
+
*/
|
|
865
|
+
get ready() {
|
|
866
|
+
return this._readyPromise;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* Ask the server where this window is now — `{ x, y, width, height, depth,
|
|
871
|
+
* borderWidth, root }`, with `x`/`y` relative to the parent, as X reports
|
|
872
|
+
* them.
|
|
873
|
+
*
|
|
874
|
+
* `wnd.x`/`y`/`width`/`height`/`depth` are kept current from the event
|
|
875
|
+
* stream and are the cheap answer; this is the round trip for the cases
|
|
876
|
+
* the events cannot cover — a window nobody selected StructureNotify on, a
|
|
877
|
+
* window ntk created with the default `depth: 0` (CopyFromParent, whose
|
|
878
|
+
* real depth only the server knows), or simply the state at a point in
|
|
879
|
+
* time. The reply is written back to those properties, and resolves
|
|
880
|
+
* `ready` if it was still pending.
|
|
881
|
+
*/
|
|
882
|
+
getGeometry() {
|
|
883
|
+
return new Promise((resolve, reject) => {
|
|
884
|
+
this.X.GetGeometry(this.id, (err, res) => {
|
|
885
|
+
if (err) return reject(err);
|
|
886
|
+
const geometry = unpackGeometry(res);
|
|
887
|
+
this._applyGeometry(geometry);
|
|
888
|
+
resolve(geometry);
|
|
889
|
+
});
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/** Record a GetGeometry reply, and let anything waiting on it go. */
|
|
894
|
+
_applyGeometry({ x, y, width, height, depth }) {
|
|
895
|
+
this.x = x;
|
|
896
|
+
this.y = y;
|
|
897
|
+
this.width = width;
|
|
898
|
+
this.height = height;
|
|
899
|
+
this.depth = depth;
|
|
900
|
+
// Only where nothing is known yet: on an adopted window a 'resize'
|
|
901
|
+
// reports both moved and resized until there is a baseline to compare
|
|
902
|
+
// against (see _tagResize), and the event stream may already have set
|
|
903
|
+
// one — that is the delivered geometry, and this reply is not.
|
|
904
|
+
this._deliveredGeom ??= { x, y, width, height };
|
|
905
|
+
this._settleReady();
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Let anything waiting on `ready` go, once the questions the constructor
|
|
910
|
+
* asked have all been answered. A window ntk created asked none.
|
|
911
|
+
*/
|
|
912
|
+
_settleReady() {
|
|
913
|
+
if (this._readyPending > 0) return;
|
|
914
|
+
this._readyPromiseResolve(this);
|
|
915
|
+
}
|
|
916
|
+
|
|
780
917
|
_forget() {
|
|
781
918
|
Window._cacheFor(this.app).delete(this.id);
|
|
782
919
|
delete this.X.event_consumers[this.id];
|
|
@@ -790,14 +927,43 @@ export default class Window extends Drawable {
|
|
|
790
927
|
* an 'expose' (and 'draw') event is emitted only when a real redraw is
|
|
791
928
|
* needed (first paint, resize). Opt out with
|
|
792
929
|
* `createWindow({ backingStore: false })`.
|
|
930
|
+
*
|
|
931
|
+
* A window adopted by id is left alone by default — another client's
|
|
932
|
+
* pixels are not ntk's to buffer — and opts in with
|
|
933
|
+
* `createWindow({ id, backingStore: true })` (see `_ownsPixels`).
|
|
793
934
|
*/
|
|
794
935
|
getContext(name, ...args) {
|
|
795
|
-
if (name === '2d' && !this._backing && !this._backingOptOut &&
|
|
796
|
-
this.
|
|
936
|
+
if (name === '2d' && !this._backing && !this._backingOptOut && this._ownsPixels) {
|
|
937
|
+
if (this._foreign) this._enableBackingStoreWhenReady();
|
|
938
|
+
else this._enableBackingStore();
|
|
797
939
|
}
|
|
798
940
|
return super.getContext(name, ...args);
|
|
799
941
|
}
|
|
800
942
|
|
|
943
|
+
/**
|
|
944
|
+
* The backing store for an adopted window, once there is something to build
|
|
945
|
+
* it out of.
|
|
946
|
+
*
|
|
947
|
+
* A pixmap needs a width, a height, a depth and a visual, and an adopted
|
|
948
|
+
* window knows none of them until `GetGeometry` and `GetWindowAttributes`
|
|
949
|
+
* reply — so the allocation waits on `ready`. A 2d context taken in the
|
|
950
|
+
* meantime draws straight to the window and re-binds itself to the pixmap
|
|
951
|
+
* when it appears (the `_backing` event), which is the same path a resize
|
|
952
|
+
* reallocation takes.
|
|
953
|
+
*/
|
|
954
|
+
_enableBackingStoreWhenReady() {
|
|
955
|
+
if (this._backingScheduled) return;
|
|
956
|
+
this._backingScheduled = true;
|
|
957
|
+
this.ready.then(() => {
|
|
958
|
+
// A window destroyed before the replies arrived still settles `ready`,
|
|
959
|
+
// with its geometry never filled in — there is no pixmap to make for
|
|
960
|
+
// one, and CreatePixmap with an undefined size is not the way to find
|
|
961
|
+
// that out.
|
|
962
|
+
if (this._destroyed || this._backing || !this.width || !this.height) return;
|
|
963
|
+
this._enableBackingStore();
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
|
|
801
967
|
_enableBackingStore() {
|
|
802
968
|
this._dirty = false;
|
|
803
969
|
this._backingValid = false;
|
|
@@ -805,7 +971,7 @@ export default class Window extends Drawable {
|
|
|
805
971
|
// Now there is a pixmap to present. Inert where the extensions are
|
|
806
972
|
// missing, so this costs a window on such a server two extension queries
|
|
807
973
|
// that node-x11 answers from its cache after the first.
|
|
808
|
-
if (!this._presentOptOut &&
|
|
974
|
+
if (!this._presentOptOut && this._ownsPixels) {
|
|
809
975
|
this.enablePresent().catch((err) => this.app.options?.onXError?.(err));
|
|
810
976
|
}
|
|
811
977
|
|
|
@@ -817,7 +983,8 @@ export default class Window extends Drawable {
|
|
|
817
983
|
// server sends them even if user code only listens to 'draw'
|
|
818
984
|
if (!(this.eventMask & x11.eventMask.Exposure)) {
|
|
819
985
|
this.eventMask |= x11.eventMask.Exposure;
|
|
820
|
-
|
|
986
|
+
// no callback: it would only buy node-x11's void-sync round trip, see setCursor
|
|
987
|
+
this.X.ChangeWindowAttributes(this.id, { eventMask: this.eventMask });
|
|
821
988
|
}
|
|
822
989
|
|
|
823
990
|
// a pure move is a ConfigureNotify too, and reallocating a backing
|
|
@@ -863,8 +1030,11 @@ export default class Window extends Drawable {
|
|
|
863
1030
|
this._clearPixel = pixel;
|
|
864
1031
|
if (this._destroyed) return this;
|
|
865
1032
|
if (this._clearGc) this.X.ChangeGC(this._clearGc, { foreground: pixel });
|
|
866
|
-
|
|
867
|
-
|
|
1033
|
+
// The window attribute half is only ours to write on a window whose
|
|
1034
|
+
// pixels are ours — an adopted one has to have said so (see _ownsPixels).
|
|
1035
|
+
if (this._ownsPixels) {
|
|
1036
|
+
// no callback: it would only buy node-x11's void-sync round trip, see setCursor
|
|
1037
|
+
this.X.ChangeWindowAttributes(this.id, { backgroundPixel: pixel });
|
|
868
1038
|
}
|
|
869
1039
|
const backing = this._backing;
|
|
870
1040
|
if (backing && this._clearGc) {
|
|
@@ -896,7 +1066,10 @@ export default class Window extends Drawable {
|
|
|
896
1066
|
parent: this,
|
|
897
1067
|
width: newW,
|
|
898
1068
|
height: newH,
|
|
899
|
-
depth
|
|
1069
|
+
depth,
|
|
1070
|
+
// it holds this window's pixels, so it is read through this window's
|
|
1071
|
+
// visual — a pixmap has none of its own (issue #295)
|
|
1072
|
+
visual: this.visualId
|
|
900
1073
|
});
|
|
901
1074
|
if (!this._clearGc) {
|
|
902
1075
|
this._clearGc = this.X.AllocID();
|
|
@@ -2997,9 +3170,19 @@ export default class Window extends Drawable {
|
|
|
2997
3170
|
* pointer stays visible
|
|
2998
3171
|
*/
|
|
2999
3172
|
setCursor(nameOrShape) {
|
|
3173
|
+
// resolved first: an unknown name has to throw whether or not the memo
|
|
3174
|
+
// below short-circuits the request
|
|
3000
3175
|
const cursor = nameOrShape == null ? 0 : this.app.cursors.get(nameOrShape);
|
|
3176
|
+
if (cursor === this._currentCursor) return this;
|
|
3177
|
+
this._currentCursor = cursor;
|
|
3001
3178
|
safeRelease(this.X, () => {
|
|
3002
|
-
|
|
3179
|
+
// no callback, deliberately: node-x11 guarantees a callback on a *void*
|
|
3180
|
+
// request eventually fires, and buys that by injecting a GetInputFocus
|
|
3181
|
+
// round trip when nothing else in the tick expects a reply (node-x11
|
|
3182
|
+
// issue #85). A callback that ignores its argument costs that round trip
|
|
3183
|
+
// per cursor change and buys nothing — an X error reaches
|
|
3184
|
+
// `client.emit('error')`, and so app's onXError hook, either way.
|
|
3185
|
+
this.X.ChangeWindowAttributes(this.id, { cursor });
|
|
3003
3186
|
});
|
|
3004
3187
|
return this;
|
|
3005
3188
|
}
|
|
@@ -3012,7 +3195,8 @@ export default class Window extends Drawable {
|
|
|
3012
3195
|
} else {
|
|
3013
3196
|
return this;
|
|
3014
3197
|
}
|
|
3015
|
-
|
|
3198
|
+
// no callback: it would only buy node-x11's void-sync round trip, see setCursor
|
|
3199
|
+
this.X.ChangeWindowAttributes(this.id, { eventMask: this.eventMask });
|
|
3016
3200
|
return this;
|
|
3017
3201
|
}
|
|
3018
3202
|
|
|
@@ -3307,7 +3491,13 @@ export default class Window extends Drawable {
|
|
|
3307
3491
|
getAttributes() {
|
|
3308
3492
|
return new Promise((resolve, reject) => {
|
|
3309
3493
|
safeRelease(this.X, () =>
|
|
3310
|
-
this.X.GetWindowAttributes(this.id, (err, attrs) =>
|
|
3494
|
+
this.X.GetWindowAttributes(this.id, (err, attrs) => {
|
|
3495
|
+
if (err) return reject(err);
|
|
3496
|
+
// the visual names the picture format (see `visualId`), and this
|
|
3497
|
+
// reply is the only place the server ever states it
|
|
3498
|
+
this.visualId = attrs.visual;
|
|
3499
|
+
resolve(attrs);
|
|
3500
|
+
})
|
|
3311
3501
|
);
|
|
3312
3502
|
});
|
|
3313
3503
|
}
|
|
@@ -3515,7 +3705,7 @@ export default class Window extends Drawable {
|
|
|
3515
3705
|
// there can be multiple screens (and roots)
|
|
3516
3706
|
const root = new Window(app, { id: tree.root });
|
|
3517
3707
|
const wrappers = [...children, root, ...(parent ? [parent] : [])];
|
|
3518
|
-
Promise.all(wrappers.map((w) => w.
|
|
3708
|
+
Promise.all(wrappers.map((w) => w.ready)).then(() => {
|
|
3519
3709
|
callback(null, { parent, root, children });
|
|
3520
3710
|
});
|
|
3521
3711
|
});
|
package/lib/xembed.js
CHANGED
|
@@ -559,7 +559,9 @@ export class XEmbedSocket extends EventEmitter {
|
|
|
559
559
|
// reparent, so its ReparentNotify does not come back to us
|
|
560
560
|
safeRelease(this.X, () => {
|
|
561
561
|
client.eventMask = 0;
|
|
562
|
-
|
|
562
|
+
// no callback: it would only buy node-x11's void-sync round trip, see
|
|
563
|
+
// Window#setCursor
|
|
564
|
+
this.X.ChangeWindowAttributes(client.id, { eventMask: 0 });
|
|
563
565
|
});
|
|
564
566
|
client.reparentTo(this.app.rootWindow(), x, y);
|
|
565
567
|
client.removeFromSaveSet();
|