ntk 5.4.0 → 6.0.1
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 +46 -0
- package/lib/fontconfig.js +70 -13
- package/lib/index.js +36 -18
- package/lib/renderingcontext_2d.js +901 -266
- package/lib/text/fontmanager.js +52 -14
- package/lib/text/fontsource.js +12 -1
- package/lib/text/layout.js +194 -33
- package/lib/text/shape.js +17 -0
- package/lib/window.js +18 -3
- package/package.json +1 -1
package/lib/app.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Clipboard from './clipboard.js';
|
|
2
2
|
import { CursorCache } from './cursor.js';
|
|
3
3
|
import { chooseGLXConfig } from './glx.js';
|
|
4
|
+
import Picture from './picture.js';
|
|
4
5
|
import Pixmap from './pixmap.js';
|
|
5
6
|
import { DEFAULT_RASTER_POLICY, defaultRasterizer } from './rasterize.js';
|
|
6
7
|
import FontManager from './text/fontmanager.js';
|
|
@@ -42,6 +43,7 @@ export default class App {
|
|
|
42
43
|
this._fonts = null;
|
|
43
44
|
this._clipboard = null;
|
|
44
45
|
this._cursors = null;
|
|
46
|
+
this._solidPictures = new Map();
|
|
45
47
|
this._rasterizer = undefined;
|
|
46
48
|
// node-x11 emits X errors it cannot route to a request callback as
|
|
47
49
|
// 'error' on the client — from inside its packet parser. With no
|
|
@@ -167,12 +169,56 @@ export default class App {
|
|
|
167
169
|
return new Pixmap(this, args);
|
|
168
170
|
}
|
|
169
171
|
|
|
172
|
+
/**
|
|
173
|
+
* A repeating source Picture of one colour, for compositing. Components
|
|
174
|
+
* are 0..1 floats, premultiplied by alpha.
|
|
175
|
+
*
|
|
176
|
+
* Cached per connection, not per context: colours a context uses are the
|
|
177
|
+
* app's palette, and `Surface.render` builds a context per call — a
|
|
178
|
+
* per-context cache made every render recreate its fill colours
|
|
179
|
+
* server-side. Nothing evicts; a solid is a few dozen bytes on the server
|
|
180
|
+
* and an app cycles through few colours. Freed in `close()`.
|
|
181
|
+
*
|
|
182
|
+
* On RENDER >= 0.10 a solid is one CreateSolidFill request; an older
|
|
183
|
+
* server (or a hand-built App that skipped the version handshake in
|
|
184
|
+
* `createClient`) gets a 1x1 repeat pixmap, which composites identically.
|
|
185
|
+
*/
|
|
186
|
+
solidPicture(r, g, b, a) {
|
|
187
|
+
const key = `${r}|${g}|${b}|${a}`;
|
|
188
|
+
let p = this._solidPictures.get(key);
|
|
189
|
+
if (p) return p;
|
|
190
|
+
|
|
191
|
+
const Render = this.display.Render;
|
|
192
|
+
const [major, minor] = Render.version || [0, 0];
|
|
193
|
+
if (major > 0 || minor >= 10) {
|
|
194
|
+
const pid = this.X.AllocID();
|
|
195
|
+
Render.CreateSolidFill(pid, r, g, b, a);
|
|
196
|
+
p = new Picture(this, { id: pid });
|
|
197
|
+
} else {
|
|
198
|
+
const pixmap = this.createPixmap({ depth: 32, width: 1, height: 1 });
|
|
199
|
+
const pid = this.X.AllocID();
|
|
200
|
+
Render.CreatePicture(pid, pixmap.id, Render.rgba32, { repeat: 1 });
|
|
201
|
+
Render.FillRectangles(Render.PictOp.Src, pid, [r, g, b, a], [0, 0, 1, 1]);
|
|
202
|
+
p = new Picture(this, { id: pid });
|
|
203
|
+
p._sourcePixmap = pixmap; // keep the 1x1 pixmap alive alongside the picture
|
|
204
|
+
}
|
|
205
|
+
this._solidPictures.set(key, p);
|
|
206
|
+
return p;
|
|
207
|
+
}
|
|
208
|
+
|
|
170
209
|
// flush pending requests and close the connection
|
|
171
210
|
close() {
|
|
172
211
|
if (this._cursors) {
|
|
173
212
|
this._cursors.dispose();
|
|
174
213
|
this._cursors = null;
|
|
175
214
|
}
|
|
215
|
+
// shared by every context that ever asked (see solidPicture), so their
|
|
216
|
+
// lifetime is the connection's — context teardown must not free them
|
|
217
|
+
for (const picture of this._solidPictures.values()) {
|
|
218
|
+
picture.destroy();
|
|
219
|
+
picture._sourcePixmap?.destroy();
|
|
220
|
+
}
|
|
221
|
+
this._solidPictures.clear();
|
|
176
222
|
return new Promise((resolve) => this.X.close(resolve));
|
|
177
223
|
}
|
|
178
224
|
|
package/lib/fontconfig.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
// node:child_process is fetched lazily (not via static import) so that
|
|
2
2
|
// browser bundles of the package never try to resolve it; in non-node
|
|
3
3
|
// environments use a custom FontSource instead (see text/fontsource.js).
|
|
4
|
+
function childProcess() {
|
|
5
|
+
return globalThis.process?.getBuiltinModule?.('node:child_process');
|
|
6
|
+
}
|
|
7
|
+
|
|
4
8
|
function execFileSync(...args) {
|
|
5
|
-
const cp =
|
|
9
|
+
const cp = childProcess();
|
|
6
10
|
if (!cp) {
|
|
7
11
|
throw noFontsError(
|
|
8
12
|
'fontconfig matching needs node (the fc-match CLI) and this is not a node environment'
|
|
@@ -101,6 +105,69 @@ function patternFor({ family, weight, style }) {
|
|
|
101
105
|
return fc;
|
|
102
106
|
}
|
|
103
107
|
|
|
108
|
+
// One command shared by the sync and async paths, so a prewarmed cache entry
|
|
109
|
+
// is byte-for-byte what the sync call would have computed.
|
|
110
|
+
const fcMatchArgs = ['-s', '--format', '%{file}\t%{postscriptname}\t%{charset}\n'];
|
|
111
|
+
const fcMatchOpts = { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 };
|
|
112
|
+
|
|
113
|
+
/** fc-match -s output -> candidates, filtered to formats fontkit can parse */
|
|
114
|
+
function parseMatches(out) {
|
|
115
|
+
const list = [];
|
|
116
|
+
for (const line of out.split('\n')) {
|
|
117
|
+
const [path, postscriptName, charset] = line.split('\t');
|
|
118
|
+
if (path && supported.test(path)) {
|
|
119
|
+
list.push({ path, postscriptName, charset: charset || '', _ranges: null });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return list;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const prewarming = new Map();
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Seed the match cache for a pattern ahead of time, off the event loop.
|
|
129
|
+
*
|
|
130
|
+
* matchSortedSync is deliberately synchronous — it answers from inside text
|
|
131
|
+
* layout — so the first layout for a pattern pays the fc-match spawn (~50ms)
|
|
132
|
+
* as a first-paint stall. Starting the same command here with a non-blocking
|
|
133
|
+
* execFile, while the X connection is still being set up, moves that cost off
|
|
134
|
+
* the critical path (issue #182).
|
|
135
|
+
*
|
|
136
|
+
* Never rejects and never reports: a prewarm is an optimization, and an app
|
|
137
|
+
* that never renders text must not crash — or even warn — over a missing
|
|
138
|
+
* fc-match. An app that does render text reaches the sync path, which
|
|
139
|
+
* diagnoses the failure properly. For the same reason nothing here touches
|
|
140
|
+
* `unavailable`: the first sync throw keeps its original spawn error as
|
|
141
|
+
* `cause`.
|
|
142
|
+
*
|
|
143
|
+
* A sync call racing this one wins — the child's result is discarded
|
|
144
|
+
* whenever the pattern is already cached by the time it exits.
|
|
145
|
+
*
|
|
146
|
+
* @returns {Promise<void>} resolves once the cache is seeded or the attempt
|
|
147
|
+
* abandoned
|
|
148
|
+
*/
|
|
149
|
+
export function prewarm(pattern = {}) {
|
|
150
|
+
const fc = patternFor(pattern);
|
|
151
|
+
if (sortedCache.has(fc) || unavailable) return Promise.resolve();
|
|
152
|
+
const inflight = prewarming.get(fc);
|
|
153
|
+
if (inflight) return inflight;
|
|
154
|
+
const cp = childProcess();
|
|
155
|
+
if (!cp) return Promise.resolve();
|
|
156
|
+
|
|
157
|
+
const promise = new Promise((resolve) => {
|
|
158
|
+
cp.execFile('fc-match', [...fcMatchArgs, fc], fcMatchOpts, (err, out) => {
|
|
159
|
+
prewarming.delete(fc);
|
|
160
|
+
if (!err && !sortedCache.has(fc)) {
|
|
161
|
+
const list = parseMatches(out);
|
|
162
|
+
if (list.length > 0) sortedCache.set(fc, list);
|
|
163
|
+
}
|
|
164
|
+
resolve();
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
prewarming.set(fc, promise);
|
|
168
|
+
return promise;
|
|
169
|
+
}
|
|
170
|
+
|
|
104
171
|
/**
|
|
105
172
|
* Full fontconfig match list for a pattern, best match first — this is the
|
|
106
173
|
* system's font fallback chain. Each candidate carries the unicode coverage
|
|
@@ -119,11 +186,7 @@ export function matchSortedSync(pattern) {
|
|
|
119
186
|
|
|
120
187
|
let out;
|
|
121
188
|
try {
|
|
122
|
-
out = execFileSync(
|
|
123
|
-
'fc-match',
|
|
124
|
-
['-s', '--format', '%{file}\t%{postscriptname}\t%{charset}\n', fc],
|
|
125
|
-
{ encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }
|
|
126
|
-
);
|
|
189
|
+
out = execFileSync('fc-match', [...fcMatchArgs, fc], fcMatchOpts);
|
|
127
190
|
} catch (err) {
|
|
128
191
|
if (err.code === 'ERR_NTK_NO_FONTS') throw err; // no child_process at all
|
|
129
192
|
if (isSpawnFailure(err)) {
|
|
@@ -140,13 +203,7 @@ export function matchSortedSync(pattern) {
|
|
|
140
203
|
throw err;
|
|
141
204
|
}
|
|
142
205
|
|
|
143
|
-
list =
|
|
144
|
-
for (const line of out.split('\n')) {
|
|
145
|
-
const [path, postscriptName, charset] = line.split('\t');
|
|
146
|
-
if (path && supported.test(path)) {
|
|
147
|
-
list.push({ path, postscriptName, charset: charset || '', _ranges: null });
|
|
148
|
-
}
|
|
149
|
-
}
|
|
206
|
+
list = parseMatches(out);
|
|
150
207
|
if (list.length === 0) {
|
|
151
208
|
throw noFontsError(
|
|
152
209
|
`fontconfig matched no font ntk can parse for "${fc}" (needs ` +
|
package/lib/index.js
CHANGED
|
@@ -106,7 +106,16 @@ export function createClient(options, callback) {
|
|
|
106
106
|
// synchronously, which keeps the legacy callback form working. The
|
|
107
107
|
// caller's options object is copied, never mutated.
|
|
108
108
|
const appOptions = options ? { ...options } : {};
|
|
109
|
-
if (appOptions.fontSource != null)
|
|
109
|
+
if (appOptions.fontSource != null) {
|
|
110
|
+
appOptions.fontSource = createFontSource(appOptions.fontSource);
|
|
111
|
+
} else {
|
|
112
|
+
// An app without a fontSource of its own resolves the process default
|
|
113
|
+
// lazily, at first text layout — too late for the fontconfig source's
|
|
114
|
+
// async fc-match prewarm to hide the ~50ms spawn (issue #182). Touch
|
|
115
|
+
// the default now, while the connection handshake runs; only touch,
|
|
116
|
+
// so a later setDefaultFontSource still takes effect.
|
|
117
|
+
defaultFontSource();
|
|
118
|
+
}
|
|
110
119
|
|
|
111
120
|
x11.createClient(x11Options, (error, display) => {
|
|
112
121
|
if (error) return reject(error);
|
|
@@ -124,26 +133,35 @@ export function createClient(options, callback) {
|
|
|
124
133
|
// rendering too bright.
|
|
125
134
|
if (process.env.NTK_STRICT_COLORS) Render.strictColors = true;
|
|
126
135
|
display.GLX = glxError ? null : GLX;
|
|
127
|
-
const X = display.client;
|
|
128
|
-
X.keycode2keysyms = {};
|
|
129
|
-
|
|
130
|
-
function updateKeyboardMapping(min, max) {
|
|
131
|
-
if (max <= min) return;
|
|
132
|
-
X.GetKeyboardMapping(min, max - min, (err, list) => {
|
|
133
|
-
if (err) return;
|
|
134
|
-
for (let i = 0; i < list.length; ++i) X.keycode2keysyms[i + min] = list[i].slice();
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
136
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
137
|
+
// RENDER requires the client to declare its version before using
|
|
138
|
+
// requests beyond 0.0, and the reply is what gates them afterwards
|
|
139
|
+
// — CreateSolidFill needs 0.10 (see App#solidPicture). 0.11 is the
|
|
140
|
+
// last published protocol revision.
|
|
141
|
+
Render.QueryVersion(0, 11, (versionError, version) => {
|
|
142
|
+
Render.version = versionError ? [0, 0] : version;
|
|
143
|
+
|
|
144
|
+
const X = display.client;
|
|
145
|
+
X.keycode2keysyms = {};
|
|
146
|
+
|
|
147
|
+
function updateKeyboardMapping(min, max) {
|
|
148
|
+
if (max <= min) return;
|
|
149
|
+
X.GetKeyboardMapping(min, max - min, (err, list) => {
|
|
150
|
+
if (err) return;
|
|
151
|
+
for (let i = 0; i < list.length; ++i) X.keycode2keysyms[i + min] = list[i].slice();
|
|
152
|
+
});
|
|
142
153
|
}
|
|
143
|
-
});
|
|
144
|
-
updateKeyboardMapping(display.min_keycode, display.max_keycode);
|
|
145
154
|
|
|
146
|
-
|
|
155
|
+
X.on('event', (ev) => {
|
|
156
|
+
// MappingNotify
|
|
157
|
+
if (ev.type === 34) {
|
|
158
|
+
updateKeyboardMapping(ev.firstKeyCode, ev.firstKeyCode + ev.count);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
updateKeyboardMapping(display.min_keycode, display.max_keycode);
|
|
162
|
+
|
|
163
|
+
resolve(new App(display, appOptions));
|
|
164
|
+
});
|
|
147
165
|
});
|
|
148
166
|
});
|
|
149
167
|
});
|