ntk 5.2.0 → 5.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/clipboard.js +576 -74
- package/lib/image.js +10 -17
- package/lib/imagedata.js +388 -0
- package/lib/index.js +16 -0
- package/lib/renderingcontext_2d.js +362 -28
- package/lib/surface.js +118 -0
- package/lib/widgets/svgview.js +118 -2
- package/lib/window.js +136 -0
- package/package.json +1 -1
package/lib/widgets/svgview.js
CHANGED
|
@@ -202,6 +202,95 @@ function pathBBox(path) {
|
|
|
202
202
|
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
/**
|
|
206
|
+
* How many distinct colours a document actually commits to, decided once at
|
|
207
|
+
* parse time.
|
|
208
|
+
*
|
|
209
|
+
* `mono` means every fill and stroke that reaches a shape is `none` or the
|
|
210
|
+
* *same* paint — one literal colour, or `currentColor` throughout. Such a
|
|
211
|
+
* drawing is really a coverage mask plus a colour, so a caller can render it
|
|
212
|
+
* once and recolour it on every draw. `multi` is everything else: a second
|
|
213
|
+
* distinct paint, or a gradient/pattern reference, whose colours belong to
|
|
214
|
+
* the drawing rather than to the UI around it.
|
|
215
|
+
*
|
|
216
|
+
* Opacity does not enter into it: `opacity`, `fill-opacity` and
|
|
217
|
+
* `stroke-opacity` scale coverage, which a mask carries perfectly well.
|
|
218
|
+
*
|
|
219
|
+
* The walk mirrors `_style` and `_renderNode` — fill and stroke inherit,
|
|
220
|
+
* inline `style` beats the presentation attribute, the initial fill is black
|
|
221
|
+
* and the initial stroke is none, `<line>` never fills, `<use>` paints its
|
|
222
|
+
* target with the *use* element's style, and non-rendered subtrees
|
|
223
|
+
* contribute nothing. It starts at the root's children rather than the root,
|
|
224
|
+
* because `draw` does: presentation attributes on the root `<svg>` are not
|
|
225
|
+
* applied, and a scan that applied them would disagree with what is painted.
|
|
226
|
+
*
|
|
227
|
+
* @returns {{ kind: 'mono'|'multi', solo: string|null }} `solo` is the one
|
|
228
|
+
* paint a `mono` document uses — a colour, or the literal `'currentColor'`
|
|
229
|
+
* when the document defers to its caller — and null when nothing paints.
|
|
230
|
+
*/
|
|
231
|
+
function scanPaints(root, ids) {
|
|
232
|
+
const paints = new Set();
|
|
233
|
+
let multi = false;
|
|
234
|
+
|
|
235
|
+
const paintOf = (node, name, inherited) => {
|
|
236
|
+
let v = attr(node, name);
|
|
237
|
+
for (const decl of (node.attribs?.style || '').split(';')) {
|
|
238
|
+
const idx = decl.indexOf(':');
|
|
239
|
+
if (idx > 0 && decl.slice(0, idx).trim() === name) v = decl.slice(idx + 1);
|
|
240
|
+
}
|
|
241
|
+
if (v === undefined || v === '' || v === 'inherit') return inherited;
|
|
242
|
+
return String(v).trim();
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const note = (paint) => {
|
|
246
|
+
if (paint === 'none') return;
|
|
247
|
+
if (/^url\(/i.test(paint)) multi = true;
|
|
248
|
+
else paints.add(paint);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const kids = (node, fill, stroke, depth) => {
|
|
252
|
+
for (const child of node.children || []) {
|
|
253
|
+
if (child.type === 'tag') visit(child, fill, stroke, depth + 1);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const visit = (node, fill, stroke, depth) => {
|
|
258
|
+
if (multi || depth > 32) return;
|
|
259
|
+
const name = tag(node);
|
|
260
|
+
if (NON_RENDERED.has(name)) return;
|
|
261
|
+
const f = paintOf(node, 'fill', fill);
|
|
262
|
+
const s = paintOf(node, 'stroke', stroke);
|
|
263
|
+
switch (name) {
|
|
264
|
+
case 'svg':
|
|
265
|
+
case 'g':
|
|
266
|
+
case 'a':
|
|
267
|
+
kids(node, f, s, depth);
|
|
268
|
+
return;
|
|
269
|
+
case 'use': {
|
|
270
|
+
const href = node.attribs?.href || node.attribs?.['xlink:href'] || '';
|
|
271
|
+
const target = href.startsWith('#') ? ids.get(href.slice(1)) : null;
|
|
272
|
+
if (!target) return;
|
|
273
|
+
// <symbol> is non-rendered on its own but renders through <use>
|
|
274
|
+
if (tag(target) === 'symbol') kids(target, f, s, depth);
|
|
275
|
+
else visit(target, f, s, depth + 1);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
case 'text':
|
|
279
|
+
note(f);
|
|
280
|
+
return;
|
|
281
|
+
default:
|
|
282
|
+
if (name !== 'line') note(f);
|
|
283
|
+
note(s);
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
kids(root, INHERITED.fill, INHERITED.stroke, 0);
|
|
288
|
+
return {
|
|
289
|
+
kind: multi || paints.size > 1 ? 'multi' : 'mono',
|
|
290
|
+
solo: paints.size === 1 ? [...paints][0] : null
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
205
294
|
/**
|
|
206
295
|
* Widget rendering a static SVG document into a window (or any 2d context
|
|
207
296
|
* via `draw()`). Scripting, CSS stylesheets, filters, masks and external
|
|
@@ -213,12 +302,22 @@ export default class SvgView {
|
|
|
213
302
|
this.theme = { background: 'white', ...(opts.theme || {}) };
|
|
214
303
|
/** fit mode in window mode: 'contain' (default) | 'fill' */
|
|
215
304
|
this.fit = opts.fit || 'contain';
|
|
305
|
+
/**
|
|
306
|
+
* What `currentColor` resolves to, for documents that defer their colour
|
|
307
|
+
* to the surrounding UI the way an icon set does. Per-draw `opts.color`
|
|
308
|
+
* overrides it; both fall back to the CSS initial value, black.
|
|
309
|
+
*/
|
|
310
|
+
this.color = opts.color ?? INHERITED.color;
|
|
216
311
|
|
|
217
312
|
this._root = null;
|
|
218
313
|
this._ids = new Map();
|
|
219
314
|
this.naturalWidth = 0;
|
|
220
315
|
this.naturalHeight = 0;
|
|
221
316
|
this.viewBox = null;
|
|
317
|
+
/** see `scanPaints`: 'mono' | 'multi', and the single paint of a mono
|
|
318
|
+
* document — `'currentColor'` when it defers its colour to the caller */
|
|
319
|
+
this.paintKind = 'mono';
|
|
320
|
+
this.soloPaint = null;
|
|
222
321
|
|
|
223
322
|
if (this.window) {
|
|
224
323
|
this._ctx = this.window.getContext('2d');
|
|
@@ -263,6 +362,10 @@ export default class SvgView {
|
|
|
263
362
|
};
|
|
264
363
|
collect(this._root);
|
|
265
364
|
|
|
365
|
+
const scanned = scanPaints(this._root, this._ids);
|
|
366
|
+
this.paintKind = scanned.kind;
|
|
367
|
+
this.soloPaint = scanned.solo;
|
|
368
|
+
|
|
266
369
|
const a = this._root.attribs || {};
|
|
267
370
|
const vb = (attr(this._root, 'viewBox') || '')
|
|
268
371
|
.split(/[\s,]+/)
|
|
@@ -305,8 +408,15 @@ export default class SvgView {
|
|
|
305
408
|
/**
|
|
306
409
|
* Draw the document into any 2d context. `w`/`h` default to the
|
|
307
410
|
* document's natural size; the viewBox (when present) is scaled to fit.
|
|
411
|
+
*
|
|
412
|
+
* `opts.color` is what `fill="currentColor"` and `stroke="currentColor"`
|
|
413
|
+
* resolve to for this draw, overriding the view's own `color`. That is how
|
|
414
|
+
* an icon takes its colour from the UI around it — the document itself
|
|
415
|
+
* names no colour, so the same parsed document paints in whatever the
|
|
416
|
+
* caller is using, and a caller caching the result can recolour a cached
|
|
417
|
+
* `paintKind === 'mono'` drawing without re-rendering it.
|
|
308
418
|
*/
|
|
309
|
-
draw(ctx, x = 0, y = 0, w = this.naturalWidth, h = this.naturalHeight) {
|
|
419
|
+
draw(ctx, x = 0, y = 0, w = this.naturalWidth, h = this.naturalHeight, opts = {}) {
|
|
310
420
|
if (!this._root) return;
|
|
311
421
|
ctx.save();
|
|
312
422
|
ctx.translate(x, y);
|
|
@@ -319,7 +429,13 @@ export default class SvgView {
|
|
|
319
429
|
} else if (this.naturalWidth > 0 && this.naturalHeight > 0) {
|
|
320
430
|
ctx.scale(w / this.naturalWidth, h / this.naturalHeight);
|
|
321
431
|
}
|
|
322
|
-
this._renderChildren(
|
|
432
|
+
this._renderChildren(
|
|
433
|
+
this._root,
|
|
434
|
+
ctx,
|
|
435
|
+
{ ...INHERITED, color: opts.color ?? this.color },
|
|
436
|
+
1,
|
|
437
|
+
0
|
|
438
|
+
);
|
|
323
439
|
ctx.restore();
|
|
324
440
|
}
|
|
325
441
|
|
package/lib/window.js
CHANGED
|
@@ -2,6 +2,7 @@ import x11 from 'x11';
|
|
|
2
2
|
|
|
3
3
|
import { safeRelease } from './cleanup.js';
|
|
4
4
|
import Drawable from './drawable.js';
|
|
5
|
+
import { packIcons, unpackIcons } from './imagedata.js';
|
|
5
6
|
import Pixmap from './pixmap.js';
|
|
6
7
|
import * as xevents from './events_map.js';
|
|
7
8
|
import { decodeKey } from './keyboard.js';
|
|
@@ -432,6 +433,11 @@ export default class Window extends Drawable {
|
|
|
432
433
|
if (args.windowType) {
|
|
433
434
|
this.setWindowType(args.windowType);
|
|
434
435
|
}
|
|
436
|
+
if (args.icon) {
|
|
437
|
+
// needs the atom interned first, so unlike the writers above it is not
|
|
438
|
+
// on the wire by the time createWindow returns
|
|
439
|
+
this.setIcon(args.icon).catch((err) => app.options.onXError?.(err));
|
|
440
|
+
}
|
|
435
441
|
// ICCCM/EWMH hints at creation: `hints: { ... }`, the `sizeHints`/
|
|
436
442
|
// `resizable` arguments that predate it, or the hint names on their own
|
|
437
443
|
// at the top level. All of them land in one setHints call so that a
|
|
@@ -505,6 +511,9 @@ export default class Window extends Drawable {
|
|
|
505
511
|
// them in the order they happened (a drag sees the move, then the up)
|
|
506
512
|
this._flushCoalesced();
|
|
507
513
|
this.emit(eventName, ntkev);
|
|
514
|
+
// a WM_DELETE_WINDOW ClientMessage is the window manager *asking*, and
|
|
515
|
+
// 'close' is that question in a form an application can answer
|
|
516
|
+
if (eventName === 'message') this._emitCloseRequest(ntkev);
|
|
508
517
|
// anything drawn during the handlers becomes visible in one blit
|
|
509
518
|
if (this._dirty) this._present();
|
|
510
519
|
});
|
|
@@ -527,6 +536,18 @@ export default class Window extends Drawable {
|
|
|
527
536
|
});
|
|
528
537
|
|
|
529
538
|
this.on('newListener', (name) => {
|
|
539
|
+
// Listening for 'close' is the opt-in. WM_DELETE_WINDOW only reaches a
|
|
540
|
+
// client that advertised it in WM_PROTOCOLS — a window manager kills
|
|
541
|
+
// anyone else outright — and having to know that, on top of decoding a
|
|
542
|
+
// ClientMessage by hand, is the protocol showing through the toolkit.
|
|
543
|
+
if (name === 'close' && !this._closeArmed) {
|
|
544
|
+
this._closeArmed = true;
|
|
545
|
+
// interning WM_PROTOCOLS here as well as in addProtocol: the
|
|
546
|
+
// dispatch path matches against both atoms and node-x11 caches them
|
|
547
|
+
// per connection, so this costs one round trip and no more
|
|
548
|
+
this.atom('WM_PROTOCOLS').catch(() => {});
|
|
549
|
+
this.addProtocol('WM_DELETE_WINDOW').catch((err) => this.app.options.onXError?.(err));
|
|
550
|
+
}
|
|
530
551
|
// 'statechange' is derived from a PropertyNotify, so it needs the atom
|
|
531
552
|
// to compare against as well as the mask the table below selects
|
|
532
553
|
if (name === 'statechange' && !this._netWmStateAtom) {
|
|
@@ -1415,6 +1436,9 @@ export default class Window extends Drawable {
|
|
|
1415
1436
|
* atom name: `'WM_DELETE_WINDOW'`, `'WM_TAKE_FOCUS'`, `'_NET_WM_PING'`,
|
|
1416
1437
|
* `'_NET_WM_SYNC_REQUEST'`. Each arrives as a `'message'` event.
|
|
1417
1438
|
*
|
|
1439
|
+
* `WM_DELETE_WINDOW` needs none of this by hand: listening for the
|
|
1440
|
+
* `'close'` event adds the protocol and decodes the message for you.
|
|
1441
|
+
*
|
|
1418
1442
|
* Replaces the whole list. `addProtocol`/`removeProtocol` are the
|
|
1419
1443
|
* accumulating forms, and the ones to reach for: the property is a *set*,
|
|
1420
1444
|
* and a plain write of one atom silently drops the rest.
|
|
@@ -1528,6 +1552,82 @@ export default class Window extends Drawable {
|
|
|
1528
1552
|
});
|
|
1529
1553
|
}
|
|
1530
1554
|
|
|
1555
|
+
/**
|
|
1556
|
+
* EWMH `_NET_WM_ICON` — the icon a taskbar, alt-tab switcher or titlebar
|
|
1557
|
+
* draws for this window.
|
|
1558
|
+
*
|
|
1559
|
+
* wnd.setIcon(await loadImage('icon-48.png'));
|
|
1560
|
+
* wnd.setIcon([icon16, icon32, icon48]);
|
|
1561
|
+
* wnd.setIcon(await ctx.getImageData(0, 0, 64, 64));
|
|
1562
|
+
* wnd.setIcon(null); // remove it
|
|
1563
|
+
*
|
|
1564
|
+
* An image is an ntk [`Image`](images.md), an `ImageData`, or anything
|
|
1565
|
+
* with `{ width, height, data }` where `data` is straight
|
|
1566
|
+
* (non-premultiplied) RGBA — the same contract as the rest of ntk, so
|
|
1567
|
+
* whatever `loadImage()` or `getImageData()` gives you goes straight in.
|
|
1568
|
+
*
|
|
1569
|
+
* Passing several is the useful case: supply the sizes you have and the
|
|
1570
|
+
* window manager picks whichever suits the slot it is filling, instead of
|
|
1571
|
+
* scaling a single one badly. Nothing requires a particular order or a
|
|
1572
|
+
* particular set of sizes; 16, 32 and 48 cover most desktops.
|
|
1573
|
+
*
|
|
1574
|
+
* This is the modern mechanism. `setWmHints({ icon })` writes the ICCCM
|
|
1575
|
+
* pixmap instead, which is 1-bit-or-matched-depth and has no alpha; a
|
|
1576
|
+
* window manager that understands both prefers this one.
|
|
1577
|
+
*
|
|
1578
|
+
* @param {object|object[]|null} images
|
|
1579
|
+
* @returns {Promise<Window>}
|
|
1580
|
+
*/
|
|
1581
|
+
async setIcon(images) {
|
|
1582
|
+
const list = images == null ? [] : Array.isArray(images) ? images : [images];
|
|
1583
|
+
if (!list.length) return this.deleteProperty('_NET_WM_ICON');
|
|
1584
|
+
|
|
1585
|
+
const data = packIcons(list, this.app.display);
|
|
1586
|
+
const property = await this.atom('_NET_WM_ICON');
|
|
1587
|
+
const cardinal = this.X.atoms.CARDINAL;
|
|
1588
|
+
if (this._destroyed) return this;
|
|
1589
|
+
|
|
1590
|
+
// A ChangeProperty carries 24 bytes of header, so a full icon set can
|
|
1591
|
+
// outrun the request limit on a connection created with
|
|
1592
|
+
// `disableBigRequests` — 256x256 alone is 262152 bytes against a 262140
|
|
1593
|
+
// cap. Replace with the first chunk, append the rest.
|
|
1594
|
+
const maxBytes = ((this.app.display.max_request_length ?? 65535) - 6) * 4;
|
|
1595
|
+
safeRelease(this.X, () => {
|
|
1596
|
+
for (let o = 0; o < data.length; o += maxBytes) {
|
|
1597
|
+
this.X.ChangeProperty(
|
|
1598
|
+
o === 0 ? 0 : 2, // Replace, then Append
|
|
1599
|
+
this.id,
|
|
1600
|
+
property,
|
|
1601
|
+
cardinal,
|
|
1602
|
+
32,
|
|
1603
|
+
data.subarray(o, Math.min(o + maxBytes, data.length))
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1606
|
+
});
|
|
1607
|
+
return this;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
/**
|
|
1611
|
+
* Read `_NET_WM_ICON` back — every size the window advertises, largest
|
|
1612
|
+
* last or in whatever order its client wrote them, as `ImageData`.
|
|
1613
|
+
*
|
|
1614
|
+
* Useful on your own window, and the point of it on someone else's: a
|
|
1615
|
+
* window manager drawing titlebars reads this off each client it frames.
|
|
1616
|
+
* Property bytes from another client are not to be trusted, so a
|
|
1617
|
+
* truncated or nonsensical run yields the images that parsed cleanly
|
|
1618
|
+
* rather than throwing.
|
|
1619
|
+
*
|
|
1620
|
+
* Resolves to `null` when the window has no icon.
|
|
1621
|
+
*
|
|
1622
|
+
* @returns {Promise<ImageData[]|null>}
|
|
1623
|
+
*/
|
|
1624
|
+
async getIcon() {
|
|
1625
|
+
const prop = await this.getProperty('_NET_WM_ICON');
|
|
1626
|
+
if (!prop || !prop.data.length) return null;
|
|
1627
|
+
const icons = unpackIcons(prop.data, this.app.display);
|
|
1628
|
+
return icons.length ? icons : null;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1531
1631
|
/** os.hostname(), looked up once, and absent in a browser bundle. */
|
|
1532
1632
|
static _hostname() {
|
|
1533
1633
|
if (Window._cachedHostname === undefined) {
|
|
@@ -2363,6 +2463,42 @@ export default class Window extends Drawable {
|
|
|
2363
2463
|
return this;
|
|
2364
2464
|
}
|
|
2365
2465
|
|
|
2466
|
+
/**
|
|
2467
|
+
* Turn a WM_DELETE_WINDOW ClientMessage into the `close` event.
|
|
2468
|
+
*
|
|
2469
|
+
* Only fires when something is listening, so a window that handles the
|
|
2470
|
+
* raw `message` event itself — the only way to do this before `close`
|
|
2471
|
+
* existed — keeps behaving exactly as it did, rather than suddenly
|
|
2472
|
+
* acquiring a second handler that destroys it.
|
|
2473
|
+
*/
|
|
2474
|
+
_emitCloseRequest(ev) {
|
|
2475
|
+
if (ev.format !== 32 || !this.listenerCount('close')) return;
|
|
2476
|
+
// node-x11 caches interned atoms per connection, so these are the ids
|
|
2477
|
+
// the 'close' listener's addProtocol already interned
|
|
2478
|
+
const X = this.X;
|
|
2479
|
+
if (!X.atoms.WM_PROTOCOLS || ev.message_type !== X.atoms.WM_PROTOCOLS) return;
|
|
2480
|
+
if (!X.atoms.WM_DELETE_WINDOW || ev.data?.[0] !== X.atoms.WM_DELETE_WINDOW) return;
|
|
2481
|
+
|
|
2482
|
+
let prevented = false;
|
|
2483
|
+
this.emit('close', {
|
|
2484
|
+
name: 'close',
|
|
2485
|
+
window: this,
|
|
2486
|
+
target: this,
|
|
2487
|
+
// the timestamp the window manager stamped the request with, for
|
|
2488
|
+
// passing back to setInputFocus and friends
|
|
2489
|
+
time: ev.data[1],
|
|
2490
|
+
preventDefault() {
|
|
2491
|
+
prevented = true;
|
|
2492
|
+
},
|
|
2493
|
+
get defaultPrevented() {
|
|
2494
|
+
return prevented;
|
|
2495
|
+
}
|
|
2496
|
+
});
|
|
2497
|
+
// the default action, and the reason preventDefault has to be called
|
|
2498
|
+
// synchronously: there is no point at which it could be awaited
|
|
2499
|
+
if (!prevented && !this._destroyed) this.destroy();
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2366
2502
|
destroy() {
|
|
2367
2503
|
this._destroyed = true;
|
|
2368
2504
|
this._forget();
|