ntk 7.0.0 → 7.1.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 +51 -0
- package/lib/renderingcontext_2d.js +42 -0
- package/lib/text/font.js +142 -0
- package/lib/text/fontmanager.js +63 -3
- package/lib/text/layout.js +1 -0
- package/lib/window.js +6 -4
- package/package.json +1 -1
package/lib/app.js
CHANGED
|
@@ -8,6 +8,15 @@ import { ShmUploader } from './shm-upload.js';
|
|
|
8
8
|
import FontManager from './text/fontmanager.js';
|
|
9
9
|
import Window from './window.js';
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* The highest predefined atom id in the core protocol, `XA_WM_TRANSIENT_FOR`.
|
|
13
|
+
*
|
|
14
|
+
* Atoms 1 to 68 are named by the protocol itself, so every server agrees on
|
|
15
|
+
* them and no server ever frees them. Anything above is interned at runtime
|
|
16
|
+
* and belongs to the connection that asked for it — see `_isolateAtoms`.
|
|
17
|
+
*/
|
|
18
|
+
const PREDEFINED_ATOM_MAX = 68;
|
|
19
|
+
|
|
11
20
|
/**
|
|
12
21
|
* Frame interval used before the display has been asked, in ms — and the one
|
|
13
22
|
* kept on a server that cannot answer.
|
|
@@ -76,6 +85,7 @@ export default class App {
|
|
|
76
85
|
this.display = display;
|
|
77
86
|
this.X = display.client;
|
|
78
87
|
this.options = options;
|
|
88
|
+
this._isolateAtoms();
|
|
79
89
|
this._fonts = null;
|
|
80
90
|
this._clipboard = null;
|
|
81
91
|
this._cursors = null;
|
|
@@ -94,6 +104,47 @@ export default class App {
|
|
|
94
104
|
});
|
|
95
105
|
}
|
|
96
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Give this connection an atom table of its own.
|
|
109
|
+
*
|
|
110
|
+
* node-x11 hands every client the *same* table object — `this.atoms =
|
|
111
|
+
* stdatoms` in xcore, assigned rather than copied — and `InternAtom`
|
|
112
|
+
* answers out of it without a round trip when the name is already there.
|
|
113
|
+
* So an id interned on one connection is handed to every other connection
|
|
114
|
+
* in the process, which have never asked the server for it.
|
|
115
|
+
*
|
|
116
|
+
* That is safe only while something keeps the server alive. An X server
|
|
117
|
+
* frees every atom it holds when its **last** client disconnects, and
|
|
118
|
+
* hands the same ids out again to whatever is interned next. A process
|
|
119
|
+
* that opens connections in sequence — a test suite, a tool that runs a
|
|
120
|
+
* few apps, anything under Xvfb with no window manager holding the server
|
|
121
|
+
* open — therefore writes properties with ids the server has forgotten.
|
|
122
|
+
* The visible half of that is `BadAtom` on a `ChangeProperty` naming an
|
|
123
|
+
* atom the app never chose; the quiet half is worse, because once the id
|
|
124
|
+
* has been reissued to a different name the write lands under *that* atom
|
|
125
|
+
* and no error is reported at all.
|
|
126
|
+
*
|
|
127
|
+
* Keeping only the predefined atoms is what makes the copy correct: ids 1
|
|
128
|
+
* to 68 are fixed by the core protocol, identical on every server, and
|
|
129
|
+
* never freed. Everything above that has to be interned per connection,
|
|
130
|
+
* which now happens because this table starts without it. Within one
|
|
131
|
+
* connection the caching still works, and it is still correct, because a
|
|
132
|
+
* live connection is exactly what stops the server resetting.
|
|
133
|
+
*/
|
|
134
|
+
_isolateAtoms() {
|
|
135
|
+
const shared = this.X.atoms;
|
|
136
|
+
if (!shared) return;
|
|
137
|
+
const own = {};
|
|
138
|
+
const names = {};
|
|
139
|
+
for (const [name, id] of Object.entries(shared)) {
|
|
140
|
+
if (id > PREDEFINED_ATOM_MAX) continue;
|
|
141
|
+
own[name] = id;
|
|
142
|
+
names[id] = name;
|
|
143
|
+
}
|
|
144
|
+
this.X.atoms = own;
|
|
145
|
+
this.X.atom_names = names;
|
|
146
|
+
}
|
|
147
|
+
|
|
97
148
|
/**
|
|
98
149
|
* The fastest refresh rate any active output is running at, in Hz, or
|
|
99
150
|
* `null` until the display has been asked — and on a server with no RandR
|
|
@@ -60,6 +60,23 @@ function intersectBox(a, b) {
|
|
|
60
60
|
|
|
61
61
|
const DEFAULT_FONT = "20px sans-serif";
|
|
62
62
|
|
|
63
|
+
// `"wght" 460, "wdth" 87.5` — the CSS grammar, quotes optional. An object is
|
|
64
|
+
// passed through, so callers can skip the string entirely.
|
|
65
|
+
const VARIATION_RE = /["']?([a-zA-Z0-9]{4})["']?\s+(-?[\d.]+)/g;
|
|
66
|
+
|
|
67
|
+
function parseVariationSettings(val) {
|
|
68
|
+
if (!val || val === "normal") return null;
|
|
69
|
+
if (typeof val === "object") return val;
|
|
70
|
+
if (typeof val !== "string") return null;
|
|
71
|
+
const out = {};
|
|
72
|
+
let any = false;
|
|
73
|
+
for (const [, tag, value] of val.matchAll(VARIATION_RE)) {
|
|
74
|
+
out[tag] = Number(value);
|
|
75
|
+
any = true;
|
|
76
|
+
}
|
|
77
|
+
return any ? out : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
63
80
|
/**
|
|
64
81
|
* Fallback for a context dropped without `destroy()`, matching Pixmap,
|
|
65
82
|
* Picture and GlyphSet. Only the GCs are freed here: everything else a
|
|
@@ -507,6 +524,7 @@ class RenderingContext2d {
|
|
|
507
524
|
gco: this._gco,
|
|
508
525
|
textStyle: this._textStyle,
|
|
509
526
|
fontString: this._lastFontString,
|
|
527
|
+
fontVariations: this._fontVariations,
|
|
510
528
|
textAlign: this.textAlign,
|
|
511
529
|
textBaseline: this.textBaseline,
|
|
512
530
|
m: this._m.slice(),
|
|
@@ -529,6 +547,7 @@ class RenderingContext2d {
|
|
|
529
547
|
this._gco = s.gco;
|
|
530
548
|
this._textStyle = s.textStyle;
|
|
531
549
|
this._lastFontString = s.fontString;
|
|
550
|
+
this._fontVariations = s.fontVariations;
|
|
532
551
|
this.textAlign = s.textAlign;
|
|
533
552
|
this.textBaseline = s.textBaseline;
|
|
534
553
|
this._m = s.m;
|
|
@@ -2399,6 +2418,7 @@ class RenderingContext2d {
|
|
|
2399
2418
|
weight: parsed.weight,
|
|
2400
2419
|
style: parsed.style,
|
|
2401
2420
|
size: parsed.size,
|
|
2421
|
+
variations: this._fontVariations,
|
|
2402
2422
|
};
|
|
2403
2423
|
style.font = this.window.app.fonts.match(style.family, style);
|
|
2404
2424
|
this._lastFontString = val;
|
|
@@ -2409,6 +2429,28 @@ class RenderingContext2d {
|
|
|
2409
2429
|
return this._lastFontString || DEFAULT_FONT;
|
|
2410
2430
|
}
|
|
2411
2431
|
|
|
2432
|
+
/**
|
|
2433
|
+
* CSS's `font-variation-settings`, for a variable font: `'"wght" 460'`,
|
|
2434
|
+
* or `{ wght: 460 }`. Axes a font does not have are ignored and values are
|
|
2435
|
+
* clamped to their range, so this is safe to set unconditionally.
|
|
2436
|
+
*
|
|
2437
|
+
* The `wght` axis needs none of this — a numeric weight in the `font`
|
|
2438
|
+
* shorthand already drives it (`ctx.font = '460 40px Inter'`). This is for
|
|
2439
|
+
* the rest: `wdth`, `slnt`, `opsz` and whatever a display face invents.
|
|
2440
|
+
*
|
|
2441
|
+
* Order-independent: setting it after `font` re-resolves the face, so the
|
|
2442
|
+
* two can be assigned either way round.
|
|
2443
|
+
*/
|
|
2444
|
+
set fontVariationSettings(val) {
|
|
2445
|
+
this._fontVariations = parseVariationSettings(val);
|
|
2446
|
+
// re-resolve against the font already in force, if there is one
|
|
2447
|
+
if (this._textStyle) this.font = this._lastFontString;
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
get fontVariationSettings() {
|
|
2451
|
+
return this._fontVariations ?? null;
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2412
2454
|
// ------------------------------------------------------------------
|
|
2413
2455
|
// gradients / images
|
|
2414
2456
|
|
package/lib/text/font.js
CHANGED
|
@@ -2,6 +2,60 @@ import * as fontkit from 'fontkit';
|
|
|
2
2
|
|
|
3
3
|
import { flatten, rasterizePath } from '../rasterize.js';
|
|
4
4
|
|
|
5
|
+
// Axis coordinates are rounded to this many decimals before anything is
|
|
6
|
+
// instantiated or cached. Every distinct coordinate is a font in its own
|
|
7
|
+
// right — its own rasterized glyphs, its own server-side glyphset — so the
|
|
8
|
+
// precision of the number that reaches here is the precision of the cache,
|
|
9
|
+
// and a slider handing over 459.9999999997 must not mint a face that
|
|
10
|
+
// 460 will never hit again. Two decimals is far finer than any axis is
|
|
11
|
+
// perceptible at, and makes the coordinate that reaches fontkit and the
|
|
12
|
+
// coordinate in the key the same number by construction.
|
|
13
|
+
const AXIS_DECIMALS = 2;
|
|
14
|
+
const round = (v) => Number(v.toFixed(AXIS_DECIMALS));
|
|
15
|
+
|
|
16
|
+
// Instances kept per base font. Reached only by an app driving an axis
|
|
17
|
+
// continuously — an animation, a drag — where the tail is cold by
|
|
18
|
+
// definition; a design system's handful of weights never approaches it.
|
|
19
|
+
// Server-side glyph pages have their own byte budget (text/glyphs.js), and
|
|
20
|
+
// evicting a Font here does not strand them: the key is derived from the
|
|
21
|
+
// coordinates, so re-instantiating the same point produces the same key and
|
|
22
|
+
// finds the same page.
|
|
23
|
+
const MAX_INSTANCES = 64;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Clamp `settings` into a font's design space: drop axes it does not have,
|
|
27
|
+
* clamp the rest to their range, round, and drop anything left sitting on
|
|
28
|
+
* its own default (the base font already *is* the default instance).
|
|
29
|
+
*
|
|
30
|
+
* @returns {object|null} the coordinates to instantiate, or null for "this
|
|
31
|
+
* font already is that instance"
|
|
32
|
+
*/
|
|
33
|
+
export function normalizeVariations(settings, axes) {
|
|
34
|
+
if (!settings || !axes) return null;
|
|
35
|
+
const coords = {};
|
|
36
|
+
let any = false;
|
|
37
|
+
for (const tag of Object.keys(settings).sort()) {
|
|
38
|
+
const axis = axes[tag];
|
|
39
|
+
if (!axis) continue; // a font that has no such axis is not wrong, just flat
|
|
40
|
+
const value = Number(settings[tag]);
|
|
41
|
+
if (!Number.isFinite(value)) continue;
|
|
42
|
+
const clamped = round(Math.min(axis.max, Math.max(axis.min, value)));
|
|
43
|
+
if (clamped === round(axis.default)) continue;
|
|
44
|
+
coords[tag] = clamped;
|
|
45
|
+
any = true;
|
|
46
|
+
}
|
|
47
|
+
return any ? coords : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Canonical cache key for normalized coordinates: `wdth=87.5,wght=460`. */
|
|
51
|
+
export function variationKey(coords) {
|
|
52
|
+
if (!coords) return '';
|
|
53
|
+
return Object.keys(coords)
|
|
54
|
+
.sort()
|
|
55
|
+
.map((tag) => `${tag}=${coords[tag]}`)
|
|
56
|
+
.join(',');
|
|
57
|
+
}
|
|
58
|
+
|
|
5
59
|
/**
|
|
6
60
|
* A single font face (one entry of a .ttc collection, or a whole .ttf/.otf),
|
|
7
61
|
* parsed with fontkit. Wraps everything the text pipeline needs:
|
|
@@ -71,6 +125,94 @@ export default class Font {
|
|
|
71
125
|
return this.fk.unitsPerEm;
|
|
72
126
|
}
|
|
73
127
|
|
|
128
|
+
/**
|
|
129
|
+
* The variation axes this face offers, keyed by OpenType tag:
|
|
130
|
+
* `{ wght: { name, min, default, max } }`. `{}` for a static font, so
|
|
131
|
+
* `Object.keys(font.variationAxes).length` is the "is this variable?"
|
|
132
|
+
* question and needs no other API.
|
|
133
|
+
*/
|
|
134
|
+
get variationAxes() {
|
|
135
|
+
return this.fk.variationAxes ?? {};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* A static instance of this face at a point in its design space — the
|
|
140
|
+
* thing a variable font is *for*. `font.variation({ wght: 460 })` is the
|
|
141
|
+
* `font-variation-settings: "wght" 460` of CSS.
|
|
142
|
+
*
|
|
143
|
+
* Instances are ordinary `Font`s from here on: they shape, measure and
|
|
144
|
+
* rasterize like any face, and because the key is derived from the
|
|
145
|
+
* coordinates they get their own glyph page and their own server-side
|
|
146
|
+
* glyphset. That is what keeps the axis honest — leave the key alone and
|
|
147
|
+
* every instance of one file collides in the glyph cache, so the whole
|
|
148
|
+
* axis renders as whichever coordinate got there first.
|
|
149
|
+
*
|
|
150
|
+
* Nothing is rasterized here. Instantiating is a table-level operation;
|
|
151
|
+
* glyphs are still rasterized one at a time, on first use, at the size
|
|
152
|
+
* they are drawn at.
|
|
153
|
+
*
|
|
154
|
+
* Settings for axes this font does not have are ignored, values are
|
|
155
|
+
* clamped to each axis's range, and coordinates that land on their own
|
|
156
|
+
* default return **this font** rather than a redundant copy of it.
|
|
157
|
+
*
|
|
158
|
+
* @param {object} settings axis tag -> value, e.g. `{ wght: 460 }`
|
|
159
|
+
* @returns {Font} the instance, or `this` when the settings are a no-op
|
|
160
|
+
*/
|
|
161
|
+
variation(settings) {
|
|
162
|
+
const coords = normalizeVariations(settings, this.variationAxes);
|
|
163
|
+
if (!coords) return this;
|
|
164
|
+
const suffix = variationKey(coords);
|
|
165
|
+
|
|
166
|
+
if (!this._variations) this._variations = new Map();
|
|
167
|
+
const cached = this._variations.get(suffix);
|
|
168
|
+
if (cached) {
|
|
169
|
+
// Map iterates in insertion order: re-inserting a hit moves it to the
|
|
170
|
+
// tail, so the eviction below drops the least recently asked for
|
|
171
|
+
this._variations.delete(suffix);
|
|
172
|
+
this._variations.set(suffix, cached);
|
|
173
|
+
return cached;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const instance = new Font(this._instantiate(coords), this.path, this.postscriptName);
|
|
177
|
+
instance.key = `${this.key}|${suffix}`;
|
|
178
|
+
/** the face this was cut from, for callers walking back to the axes */
|
|
179
|
+
instance.variationOf = this;
|
|
180
|
+
instance.variationCoords = coords;
|
|
181
|
+
|
|
182
|
+
this._variations.set(suffix, instance);
|
|
183
|
+
if (this._variations.size > MAX_INSTANCES) {
|
|
184
|
+
const oldest = this._variations.keys().next().value;
|
|
185
|
+
this._variations.delete(oldest);
|
|
186
|
+
}
|
|
187
|
+
return instance;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* fontkit's instancing, with the one failure it has turned into something
|
|
192
|
+
* that says what to do about it.
|
|
193
|
+
*
|
|
194
|
+
* `getVariation` rebuilds the face from the original stream at the
|
|
195
|
+
* original directory offset. For a WOFF2 that stream is still
|
|
196
|
+
* Brotli-compressed, so the copy comes back with a directory parsed out of
|
|
197
|
+
* compressed bytes — no `cmap`, and a `Cannot read properties of undefined
|
|
198
|
+
* (reading 'tables')` from somewhere deep in shaping, at the first
|
|
199
|
+
* character drawn rather than here. Checking the instance is well-formed
|
|
200
|
+
* costs one property read and moves the report to the call that caused it.
|
|
201
|
+
*/
|
|
202
|
+
_instantiate(coords) {
|
|
203
|
+
const instance = this.fk.getVariation(coords);
|
|
204
|
+
if (!instance?.directory?.tables?.cmap) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`ntk: cannot instantiate a variation of ${this.postscriptName || this.key}` +
|
|
207
|
+
(this.fk.type ? ` (${this.fk.type})` : '') +
|
|
208
|
+
'. fontkit can only instantiate axes from an uncompressed font — ' +
|
|
209
|
+
'ship the .ttf/.otf of a variable font rather than its .woff2. ' +
|
|
210
|
+
'See docs/fonts.md#variable-fonts'
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
return instance;
|
|
214
|
+
}
|
|
215
|
+
|
|
74
216
|
scale(size) {
|
|
75
217
|
return size / this.fk.unitsPerEm;
|
|
76
218
|
}
|
package/lib/text/fontmanager.js
CHANGED
|
@@ -8,6 +8,41 @@ import {
|
|
|
8
8
|
import { embeddingLevels, normalizedLevels, shapeText } from './shape.js';
|
|
9
9
|
import { TextLayout } from './layout.js';
|
|
10
10
|
|
|
11
|
+
// Bound on the resolved-match map. See the sweep in `match()`.
|
|
12
|
+
const MAX_MATCHES = 512;
|
|
13
|
+
|
|
14
|
+
/** Cache-key fragment for a style's `variations`, before any font is known. */
|
|
15
|
+
function variationsKeyOf(variations) {
|
|
16
|
+
if (!variations) return '';
|
|
17
|
+
return Object.keys(variations)
|
|
18
|
+
.sort()
|
|
19
|
+
.map((tag) => `${tag}=${variations[tag]}`)
|
|
20
|
+
.join(',');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Put a resolved face at the point in its design space the style asked for.
|
|
25
|
+
*
|
|
26
|
+
* The interesting half is `weight`. CSS has said for years that
|
|
27
|
+
* `font-weight: 460` on a variable font means the `wght` axis at 460, not
|
|
28
|
+
* "the nearest face"; a face with a `wght` axis therefore takes the
|
|
29
|
+
* requested weight as a coordinate, and an app that hands ntk a variable
|
|
30
|
+
* file gets the weight it asked for without knowing an axis exists. An
|
|
31
|
+
* explicit `variations.wght` wins, because a caller naming the axis
|
|
32
|
+
* directly is being more specific than one naming a weight.
|
|
33
|
+
*
|
|
34
|
+
* Everything here is a no-op for a static face: `variation()` returns the
|
|
35
|
+
* font unchanged when the settings do not apply, so this costs one property
|
|
36
|
+
* read on the path every non-variable app is already on.
|
|
37
|
+
*/
|
|
38
|
+
function instantiate(font, weight, variations) {
|
|
39
|
+
const axes = font.variationAxes;
|
|
40
|
+
if (!axes || (!axes.wght && !variations)) return font;
|
|
41
|
+
const settings = { ...variations };
|
|
42
|
+
if (axes.wght && settings.wght === undefined) settings.wght = weight;
|
|
43
|
+
return font.variation(settings);
|
|
44
|
+
}
|
|
45
|
+
|
|
11
46
|
/**
|
|
12
47
|
* Font lookup, loading and caching — the entry point of the text API,
|
|
13
48
|
* available as `app.fonts` (and constructible standalone for headless
|
|
@@ -122,9 +157,14 @@ export default class FontManager {
|
|
|
122
157
|
match(family = 'sans-serif', opts = {}) {
|
|
123
158
|
const weight = numWeight(opts.weight);
|
|
124
159
|
const italic = !!(opts.style && opts.style.includes('italic'));
|
|
125
|
-
const cacheKey = `${family}|${weight}|${italic}`;
|
|
160
|
+
const cacheKey = `${family}|${weight}|${italic}|${variationsKeyOf(opts.variations)}`;
|
|
126
161
|
let font = this._matches.get(cacheKey);
|
|
127
|
-
if (font)
|
|
162
|
+
if (font) {
|
|
163
|
+
// insertion order is LRU order; re-inserting a hit moves it to the tail
|
|
164
|
+
this._matches.delete(cacheKey);
|
|
165
|
+
this._matches.set(cacheKey, font);
|
|
166
|
+
return font;
|
|
167
|
+
}
|
|
128
168
|
|
|
129
169
|
const families = String(family)
|
|
130
170
|
.split(',')
|
|
@@ -145,7 +185,19 @@ export default class FontManager {
|
|
|
145
185
|
});
|
|
146
186
|
font = this._open(candidates[0]);
|
|
147
187
|
}
|
|
188
|
+
font = instantiate(font, weight, opts.variations);
|
|
148
189
|
this._matches.set(cacheKey, font);
|
|
190
|
+
// The key now carries a point in a continuous space rather than one of a
|
|
191
|
+
// handful of weights, so an app animating an axis walks this map instead
|
|
192
|
+
// of hitting it. Same sweep as the shaping memo: drop the stale half in
|
|
193
|
+
// one pass rather than one entry per insert.
|
|
194
|
+
if (this._matches.size > MAX_MATCHES) {
|
|
195
|
+
let drop = this._matches.size >> 1;
|
|
196
|
+
for (const key of this._matches.keys()) {
|
|
197
|
+
if (drop-- <= 0) break;
|
|
198
|
+
this._matches.delete(key);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
149
201
|
return font;
|
|
150
202
|
}
|
|
151
203
|
|
|
@@ -239,7 +291,15 @@ export default class FontManager {
|
|
|
239
291
|
*/
|
|
240
292
|
_shapeCached(text, style, levelsKey = '0') {
|
|
241
293
|
const font = style.font;
|
|
242
|
-
|
|
294
|
+
// A resolved `font` already carries its coordinates in its key, so the
|
|
295
|
+
// variations fragment only earns its keep on the family path — where two
|
|
296
|
+
// points of one axis would otherwise share a shaped run, and the second
|
|
297
|
+
// would be drawn with the first's advances.
|
|
298
|
+
const key = font
|
|
299
|
+
? `${font.key}|${style.size}|${style.weight}|${style.style}|${levelsKey}|${text}`
|
|
300
|
+
: `${style.family}|${style.size}|${style.weight}|${style.style}|${variationsKeyOf(
|
|
301
|
+
style.variations
|
|
302
|
+
)}|${levelsKey}|${text}`;
|
|
243
303
|
let shaped = this._shapeCache.get(key);
|
|
244
304
|
if (shaped) {
|
|
245
305
|
// Map iterates in insertion order: re-inserting a hit moves it to the
|
package/lib/text/layout.js
CHANGED
|
@@ -66,6 +66,7 @@ export class TextLayout {
|
|
|
66
66
|
size: s.size ?? style.size ?? 16,
|
|
67
67
|
weight: s.weight ?? style.weight,
|
|
68
68
|
style: s.style ?? style.style,
|
|
69
|
+
variations: s.variations ?? style.variations,
|
|
69
70
|
features: s.features ?? style.features,
|
|
70
71
|
language: s.language ?? style.language,
|
|
71
72
|
color: s.color ?? style.color ?? null
|
package/lib/window.js
CHANGED
|
@@ -1902,8 +1902,9 @@ export default class Window extends Drawable {
|
|
|
1902
1902
|
|
|
1903
1903
|
/**
|
|
1904
1904
|
* Intern several atoms and hand them to `cb` as an object keyed by name.
|
|
1905
|
-
*
|
|
1906
|
-
*
|
|
1905
|
+
* Interned atoms are cached per connection — which is true because App
|
|
1906
|
+
* makes it true, see `_isolateAtoms` — so a repeated call costs no round
|
|
1907
|
+
* trip. Same deferred-chain hazards as setTitle: by the
|
|
1907
1908
|
* time the replies land the window may be gone, so callers re-check
|
|
1908
1909
|
* `this._destroyed`.
|
|
1909
1910
|
*/
|
|
@@ -3371,8 +3372,9 @@ export default class Window extends Drawable {
|
|
|
3371
3372
|
|
|
3372
3373
|
_emitCloseRequest(ev) {
|
|
3373
3374
|
if (ev.format !== 32 || !this.listenerCount('close')) return;
|
|
3374
|
-
//
|
|
3375
|
-
// the
|
|
3375
|
+
// interned atoms are cached per connection (App#_isolateAtoms), so these
|
|
3376
|
+
// are the ids this window's own addProtocol interned — an id another
|
|
3377
|
+
// connection happened to intern would not be one this server still knows
|
|
3376
3378
|
const X = this.X;
|
|
3377
3379
|
if (!X.atoms.WM_PROTOCOLS || ev.message_type !== X.atoms.WM_PROTOCOLS) return;
|
|
3378
3380
|
if (!X.atoms.WM_DELETE_WINDOW || ev.data?.[0] !== X.atoms.WM_DELETE_WINDOW) return;
|