ntk 8.13.0 → 8.13.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 CHANGED
@@ -345,12 +345,16 @@ export default class App {
345
345
  const X = this.X;
346
346
  if (connectionGone(X)) return;
347
347
  const name = '_NET_WM_CM_S0';
348
+ // Each answer can arrive after the app has closed — a window that
349
+ // presented once and was torn down at once — and a request made from it
350
+ // then throws out of the reply dispatch, where no caller can catch it.
348
351
  X.QueryExtension('XWAYLAND', (xwErr, ext) => {
349
352
  this._xwayland = !xwErr && !!ext?.present;
353
+ if (connectionGone(X)) return;
350
354
  X.InternAtom(false, name, (atomErr, atom) => {
351
- if (atomErr || !atom) return;
355
+ if (atomErr || !atom || connectionGone(X)) return;
352
356
  X.GetSelectionOwner(atom, (ownerErr, owner) => {
353
- if (ownerErr) return;
357
+ if (ownerErr || connectionGone(X)) return;
354
358
  this._setCompositing(owner !== 0);
355
359
  this.clipboard
356
360
  .watch(name, (ev) => this._setCompositing(ev.reason === 'new-owner' && ev.owner !== 0))
package/lib/clipboard.js CHANGED
@@ -63,6 +63,7 @@ export default class Clipboard {
63
63
  this._fixes = null; // XFixes extension, required on first watch()
64
64
  this._fixesFirstEvent = -1; // its event base, to recognise its events
65
65
  this._selectionWatchers = new Map(); // selection atom -> { name, handlers }
66
+ this._listening = false; // _onXEvent is on the raw event stream
66
67
  this._onXEvent = this._onXEvent.bind(this);
67
68
  }
68
69
 
@@ -328,11 +329,17 @@ export default class Clipboard {
328
329
  // never delivers them to the Window wrapper — listen on the raw
329
330
  // client instead. INCR chunk pacing needs PropertyNotify for the
330
331
  // requestor's window, which is not ours either.
331
- this.X.on('event', this._onXEvent);
332
+ this._listen();
332
333
  })();
333
334
  return this._ready;
334
335
  }
335
336
 
337
+ _listen() {
338
+ if (this._listening) return;
339
+ this._listening = true;
340
+ this.X.on('event', this._onXEvent);
341
+ }
342
+
336
343
  /**
337
344
  * Call `handler` whenever a selection changes hands.
338
345
  *
@@ -355,6 +362,9 @@ export default class Clipboard {
355
362
  *
356
363
  * Watchers share one server-side registration per selection, so watching
357
364
  * the same selection twice costs one extra callback and no extra protocol.
365
+ * The registration is on the root window, as GDK's is, so watching makes
366
+ * no window of the clipboard's own: an app that only watches — every app
367
+ * does, through `App#compositing` — has none.
358
368
  *
359
369
  * @param {string} selection selection atom name, e.g. 'CLIPBOARD' or 'PRIMARY'
360
370
  * @param {function} handler called with the event
@@ -364,9 +374,10 @@ export default class Clipboard {
364
374
  if (typeof handler !== 'function') {
365
375
  throw new TypeError('clipboard: watch needs a handler function');
366
376
  }
367
- await this._ensure();
368
377
  const fixes = await this._ensureFixes();
369
378
  const sel = await this._atom(selection);
379
+ this._listen();
380
+ const root = this.app.display.screen[0].root;
370
381
 
371
382
  let entry = this._selectionWatchers.get(sel);
372
383
  if (!entry) {
@@ -376,7 +387,7 @@ export default class Clipboard {
376
387
  fixes.SelectionEventMask.SetSelectionOwner |
377
388
  fixes.SelectionEventMask.SelectionWindowDestroy |
378
389
  fixes.SelectionEventMask.SelectionClientClose;
379
- safeRelease(this.X, () => fixes.SelectSelectionInput(this._window.id, sel, mask));
390
+ safeRelease(this.X, () => fixes.SelectSelectionInput(root, sel, mask));
380
391
  }
381
392
  entry.handlers.add(handler);
382
393
 
@@ -390,7 +401,7 @@ export default class Clipboard {
390
401
  if (current.handlers.size) return;
391
402
  // last watcher for this selection: drop the server-side registration
392
403
  this._selectionWatchers.delete(sel);
393
- safeRelease(this.X, () => fixes.SelectSelectionInput(this._window.id, sel, 0));
404
+ safeRelease(this.X, () => fixes.SelectSelectionInput(root, sel, 0));
394
405
  };
395
406
  }
396
407
 
@@ -522,7 +533,6 @@ export default class Clipboard {
522
533
  }
523
534
 
524
535
  _onXEvent(ev) {
525
- if (!this._window) return;
526
536
  // XFixes events carry a server-assigned type above the core range, so
527
537
  // this cannot be confused with core SelectionNotify, which is 31
528
538
  if (ev.type === this._fixesFirstEvent && this._selectionWatchers.size) {
@@ -532,6 +542,7 @@ export default class Clipboard {
532
542
  );
533
543
  return;
534
544
  }
545
+ if (!this._window) return;
535
546
  if (ev.type === 28) {
536
547
  // PropertyNotify, state 1 = Deleted: a requestor consumed the INCR
537
548
  // chunk we gave it and is asking for the next one
@@ -0,0 +1,117 @@
1
+ // Geometry clipped to a rectangle, for what goes to the server in XRender's
2
+ // 16.16 fixed point: trapezoids and triangles. A coordinate past 32,767 there
3
+ // overflows the 32-bit word node-x11 writes it into, and the RangeError comes
4
+ // out of the paint that drew it. A box of a code block twenty thousand lines
5
+ // tall in a scroll pane was one, filled as a rounded rectangle.
6
+ //
7
+ // Only what reaches past the rectangle is clipped: everything else goes out
8
+ // as it came, so a drawing that fits is sent exactly as before.
9
+
10
+ /** Whether every point of a flat `[x, y, …]` list is inside the rectangle. */
11
+ export function withinRect(pts, x0, y0, x1, y1) {
12
+ for (let i = 0; i < pts.length; i += 2) {
13
+ const x = pts[i];
14
+ const y = pts[i + 1];
15
+ if (x < x0 || x > x1 || y < y0 || y > y1) return false;
16
+ }
17
+ return true;
18
+ }
19
+
20
+ /**
21
+ * A closed ring clipped to the rectangle (Sutherland–Hodgman), as a flat
22
+ * `[x, y, …]` list, empty when nothing of it is inside. Clipping each ring of
23
+ * a shape on its own keeps its fill, nonzero or even-odd, the same at every
24
+ * point inside a convex region.
25
+ */
26
+ export function clipRingToRect(pts, x0, y0, x1, y1) {
27
+ let ring = pts;
28
+ // each edge of the rectangle: which coordinate, which bound, which side
29
+ for (const [axis, bound, keepAbove] of [
30
+ [0, x0, true],
31
+ [0, x1, false],
32
+ [1, y0, true],
33
+ [1, y1, false],
34
+ ]) {
35
+ const n = ring.length / 2;
36
+ if (n === 0) break;
37
+ const out = [];
38
+ const inside = (i) =>
39
+ keepAbove ? ring[i * 2 + axis] >= bound : ring[i * 2 + axis] <= bound;
40
+ for (let i = 0; i < n; i++) {
41
+ const j = (i + n - 1) % n; // the previous point
42
+ const iin = inside(i);
43
+ const jin = inside(j);
44
+ if (iin !== jin) {
45
+ // where the segment from j to i crosses the bound
46
+ const a = ring[j * 2 + axis];
47
+ const b = ring[i * 2 + axis];
48
+ const t = (bound - a) / (b - a);
49
+ const other = 1 - axis;
50
+ const oa = ring[j * 2 + other];
51
+ const ob = ring[i * 2 + other];
52
+ const p = [0, 0];
53
+ p[axis] = bound;
54
+ p[other] = oa + (ob - oa) * t;
55
+ out.push(p[0], p[1]);
56
+ }
57
+ if (iin) out.push(ring[i * 2], ring[i * 2 + 1]);
58
+ }
59
+ ring = out;
60
+ }
61
+ return ring;
62
+ }
63
+
64
+ /**
65
+ * A flat list of triangles, six numbers each, with every one that reaches
66
+ * past the rectangle clipped to it and cut back into triangles (a fan: what
67
+ * is left of a triangle inside a rectangle is convex).
68
+ */
69
+ export function clipTrianglesToRect(tris, x0, y0, x1, y1) {
70
+ const out = [];
71
+ for (let i = 0; i + 5 < tris.length; i += 6) {
72
+ const tri = tris.slice(i, i + 6);
73
+ if (withinRect(tri, x0, y0, x1, y1)) {
74
+ out.push(...tri);
75
+ continue;
76
+ }
77
+ const poly = clipRingToRect(tri, x0, y0, x1, y1);
78
+ for (let k = 2; k * 2 + 1 < poly.length; k++) {
79
+ out.push(
80
+ poly[0],
81
+ poly[1],
82
+ poly[(k - 1) * 2],
83
+ poly[(k - 1) * 2 + 1],
84
+ poly[k * 2],
85
+ poly[k * 2 + 1],
86
+ );
87
+ }
88
+ }
89
+ return out;
90
+ }
91
+
92
+ /**
93
+ * A flat `[x, y, w, h, …]` list of rectangles cut to `[0, width) × [0,
94
+ * height)`, dropping the ones with nothing left: core X's rectangles are 16
95
+ * bits. The list itself when every rectangle is already on the surface.
96
+ */
97
+ export function rectsOnSurface(flat, width, height) {
98
+ let out = null;
99
+ for (let i = 0; i + 3 < flat.length; i += 4) {
100
+ const x = flat[i];
101
+ const y = flat[i + 1];
102
+ const w = flat[i + 2];
103
+ const h = flat[i + 3];
104
+ const inside = x >= 0 && y >= 0 && x + w <= width && y + h <= height;
105
+ if (inside) {
106
+ if (out) out.push(x, y, w, h);
107
+ continue;
108
+ }
109
+ if (!out) out = flat.slice(0, i);
110
+ const x0 = Math.max(x, 0);
111
+ const y0 = Math.max(y, 0);
112
+ const x1 = Math.min(x + w, width);
113
+ const y1 = Math.min(y + h, height);
114
+ if (x1 > x0 && y1 > y0) out.push(x0, y0, x1 - x0, y1 - y0);
115
+ }
116
+ return out ?? flat;
117
+ }
@@ -29,6 +29,12 @@ import { formatForDepth } from "./pictformat.js";
29
29
  import Pixmap from "./pixmap.js";
30
30
  import { REGION_DOCS, regionId } from "./region.js";
31
31
  import { routeRaster } from "./rasterize.js";
32
+ import {
33
+ clipRingToRect,
34
+ clipTrianglesToRect,
35
+ rectsOnSurface,
36
+ withinRect,
37
+ } from "./cliprect.js";
32
38
  import {
33
39
  blurCoverage,
34
40
  cachedShadow,
@@ -1868,9 +1874,36 @@ class RenderingContext2d {
1868
1874
  });
1869
1875
  }
1870
1876
 
1871
- _rasterizePolys(picture, shapes, rule, dx = 0, dy = 0) {
1877
+ _rasterizePolys(
1878
+ picture,
1879
+ shapes,
1880
+ rule,
1881
+ dx = 0,
1882
+ dy = 0,
1883
+ w = this.width,
1884
+ h = this.height,
1885
+ ) {
1872
1886
  if (!shapes.length) return;
1873
- const traps = trapezoidize(shapes, dx, dy, [], rule);
1887
+ // Trapezoids are 16.16 fixed point, and a coordinate past 32,767 throws
1888
+ // out of the paint (lib/cliprect.js). A shape that reaches past the
1889
+ // picture is cut to it first, a pixel to spare on every side: nothing
1890
+ // outside the picture is ever composited.
1891
+ const x0 = -dx - 1;
1892
+ const y0 = -dy - 1;
1893
+ const x1 = w - dx + 1;
1894
+ const y1 = h - dy + 1;
1895
+ let fitted = null;
1896
+ for (let i = 0; i < shapes.length; i++) {
1897
+ const shape = shapes[i];
1898
+ if (withinRect(shape, x0, y0, x1, y1)) {
1899
+ if (fitted) fitted.push(shape);
1900
+ continue;
1901
+ }
1902
+ if (!fitted) fitted = shapes.slice(0, i);
1903
+ const ring = clipRingToRect(shape, x0, y0, x1, y1);
1904
+ if (ring.length >= 6) fitted.push(ring);
1905
+ }
1906
+ const traps = trapezoidize(fitted ?? shapes, dx, dy, [], rule);
1874
1907
  // stay under the server's maximum request size
1875
1908
  const chunk = 4000 * 6;
1876
1909
  for (let i = 0; i < traps.length; i += chunk) {
@@ -2480,6 +2513,18 @@ class RenderingContext2d {
2480
2513
  }
2481
2514
  if (w < tris.length) tris.length = w;
2482
2515
  if (!tris.length) return;
2516
+ // Triangles are 16.16 fixed point, and the source origin a direct stroke
2517
+ // hands the server is 16 bits: a stroke that reaches past the surface is
2518
+ // cut to it first (lib/cliprect.js), a pixel to spare on every side, so
2519
+ // what the surface shows is drawn exactly as before.
2520
+ const edgeX = this.width + 1;
2521
+ const edgeY = this.height + 1;
2522
+ if (!withinRect(tris, -1, -1, edgeX, edgeY)) {
2523
+ const cut = clipTrianglesToRect(tris, -1, -1, edgeX, edgeY);
2524
+ tris.length = 0;
2525
+ for (let i = 0; i < cut.length; i++) tris.push(cut[i]);
2526
+ if (!tris.length) return;
2527
+ }
2483
2528
 
2484
2529
  const op = this._op();
2485
2530
  // round-cap/join disks overlap the stroke body; overlapping coverage
@@ -3018,6 +3063,20 @@ class RenderingContext2d {
3018
3063
  * for the whole list and then draws the rectangles through here */
3019
3064
  _fillRect(x, y, w, h) {
3020
3065
  if (matIsIdentity(this._m)) {
3066
+ // X coordinates and sizes are 16 bits, and a coordinate past 32,767
3067
+ // throws out of the paint (lib/cliprect.js): a rectangle that reaches
3068
+ // past the surface is filled as the part of it the surface has.
3069
+ if (x < 0 || y < 0 || x + w > this.width || y + h > this.height) {
3070
+ const x0 = Math.max(x, 0);
3071
+ const y0 = Math.max(y, 0);
3072
+ const x1 = Math.min(x + w, this.width);
3073
+ const y1 = Math.min(y + h, this.height);
3074
+ if (!(x1 > x0 && y1 > y0)) return;
3075
+ x = x0;
3076
+ y = y0;
3077
+ w = x1 - x0;
3078
+ h = y1 - y0;
3079
+ }
3021
3080
  if (!prepareStyle(this._backgroundPicture, this._m)) return;
3022
3081
  const op = this._op();
3023
3082
  // A rectangular clip is a smaller rectangle to fill, not a mask — this
@@ -3113,6 +3172,10 @@ class RenderingContext2d {
3113
3172
  }
3114
3173
  if (this._hasPolyClip) return;
3115
3174
  if (clip && (clip.w === 0 || clip.h === 0)) return; // clipped away
3175
+ // the part of each the surface has, as `_fillRect` fills it: the wire's
3176
+ // coordinates are 16 bits
3177
+ const onSurface = rectsOnSurface(flat, this.width, this.height);
3178
+ if (!onSurface.length) return;
3116
3179
  const R = this.Render;
3117
3180
  if (clip) this._setPictureClip(clip);
3118
3181
  // globalAlpha folds into the premultiplied colour — scaling all four
@@ -3122,12 +3185,14 @@ class RenderingContext2d {
3122
3185
  const color = this._foldedColor(this._fillStyle);
3123
3186
  // stay under the server's maximum request size
3124
3187
  const chunk = 10000 * 4;
3125
- for (let i = 0; i < flat.length; i += chunk) {
3188
+ for (let i = 0; i < onSurface.length; i += chunk) {
3126
3189
  R.FillRectangles(
3127
3190
  this._op(),
3128
3191
  this._dst(),
3129
3192
  color,
3130
- i === 0 && flat.length <= chunk ? flat : flat.slice(i, i + chunk),
3193
+ i === 0 && onSurface.length <= chunk
3194
+ ? onSurface
3195
+ : onSurface.slice(i, i + chunk),
3131
3196
  );
3132
3197
  }
3133
3198
  if (clip) this._invalidatePictureClip();
@@ -3166,10 +3231,13 @@ class RenderingContext2d {
3166
3231
  let x1 = -Infinity;
3167
3232
  let y1 = -Infinity;
3168
3233
  for (let i = 0; i < flat.length; i += 4) {
3169
- const x = Math.trunc(flat[i]);
3170
- const y = Math.trunc(flat[i + 1]);
3171
- const w = Math.trunc(flat[i + 2]);
3172
- const h = Math.trunc(flat[i + 3]);
3234
+ // the part of it on the surface: the wire's coordinates are 16 bits
3235
+ const left = Math.trunc(flat[i]);
3236
+ const top = Math.trunc(flat[i + 1]);
3237
+ const x = Math.max(0, left);
3238
+ const y = Math.max(0, top);
3239
+ const w = Math.min(this.width, left + Math.trunc(flat[i + 2])) - x;
3240
+ const h = Math.min(this.height, top + Math.trunc(flat[i + 3])) - y;
3173
3241
  if (w <= 0 || h <= 0) continue;
3174
3242
  rects.push(x, y, w, h);
3175
3243
  if (x < x0) x0 = x;
@@ -3953,6 +4021,8 @@ class RenderingContext2d {
3953
4021
  entry.rule,
3954
4022
  -bb.x,
3955
4023
  -bb.y,
4024
+ bb.w,
4025
+ bb.h,
3956
4026
  );
3957
4027
  }
3958
4028
  R.Composite(
@@ -310,6 +310,13 @@ export class TextLayout {
310
310
  // and every line still needs a style to take its metrics from, so
311
311
  // stand one in rather than crashing on the first empty line.
312
312
  const given = typeof content === 'string' ? [{ text: content }] : content;
313
+ // What a span is shaped with, one per distinct style rather than one a
314
+ // span (`shapingStyleOf`): the spans of a highlighted source file are its
315
+ // tokens, half a million for twenty thousand lines over a handful of
316
+ // styles, and the font lookup and the shaping memo's key, worked out
317
+ // afresh for each, were most of what laying such a file out cost.
318
+ const styles = [];
319
+ const shapings = [];
313
320
  const spans = (given.length ? given : [{ text: '' }]).map((s) => {
314
321
  const merged = {
315
322
  ...s,
@@ -327,7 +334,9 @@ export class TextLayout {
327
334
  letterSpacing: s.letterSpacing ?? style.letterSpacing,
328
335
  color: s.color ?? style.color ?? null
329
336
  };
330
- merged.font = s.font ?? style.font ?? fonts.match(merged.family, merged);
337
+ const shaping = shapingStyleOf(styles, merged, s.font ?? style.font, fonts);
338
+ merged.font = shaping.font;
339
+ shapings.push(shaping);
331
340
  return merged;
332
341
  });
333
342
 
@@ -355,6 +364,7 @@ export class TextLayout {
355
364
  }
356
365
  return {
357
366
  span: spans[lo],
367
+ shaping: shapings[lo],
358
368
  end: lo + 1 < spanStarts.length ? spanStarts[lo + 1] : text.length
359
369
  };
360
370
  };
@@ -388,13 +398,13 @@ export class TextLayout {
388
398
  let width = 0;
389
399
  let pos = start;
390
400
  while (pos < end) {
391
- const { span, end: spanEnd } = spanAt(pos);
401
+ const { span, shaping, end: spanEnd } = spanAt(pos);
392
402
  const fragEnd = Math.min(end, spanEnd);
393
403
  // hard-break controls only terminate the line; never shape them
394
404
  const fragText = text.slice(pos, fragEnd).replace(HARD_BREAKS, '');
395
405
  if (fragText.length > 0) {
396
406
  const fragLevels = normalizedLevels(levels, pos, pos + fragText.length);
397
- const shaped = this.fonts._shapeCached(fragText, span, fragLevels);
407
+ const shaped = this.fonts._shapeCached(fragText, shaping, fragLevels);
398
408
  // the levels ride along so a later split can re-shape a piece at the
399
409
  // level it actually has, rather than assuming ltr
400
410
  fragments.push({ text: fragText, span, shaped, start: pos, levels: fragLevels });
@@ -595,7 +605,20 @@ export class TextLayout {
595
605
  }
596
606
  batch = [];
597
607
  };
608
+ // Only the lines the context can show. A layout taller than the view it
609
+ // is drawn in — a code block of twenty thousand lines in a scroll pane —
610
+ // built every line's glyphs, sent them, and had the server clip them
611
+ // away, on every paint. A line is skipped when its box, grown by its
612
+ // height and its font's extent again on either side for ink that
613
+ // overhangs the box, is outside the rows the clip lets through: nothing
614
+ // of it could have landed on a pixel.
615
+ const rows = visibleRows(ctx, y);
598
616
  for (const line of this.lines) {
617
+ if (rows) {
618
+ const reach = line.height + line.ascent + line.descent;
619
+ if (line.y + line.height + reach < rows.top) continue;
620
+ if (line.y - reach > rows.bottom) continue;
621
+ }
599
622
  for (const r of line.runs) {
600
623
  const color = r.span.color;
601
624
  if (batch.length && color !== batchColor) flush();
@@ -1015,3 +1038,77 @@ function sliceLevels(key, start, end) {
1015
1038
  if (!key.includes(',')) return key;
1016
1039
  return key.split(',').slice(start, end).join(',');
1017
1040
  }
1041
+
1042
+ /**
1043
+ * The rows of a layout drawn at `y` that the context can show — its clip's
1044
+ * box, or the whole surface with no clip — in the layout's own coordinates.
1045
+ * Null where that cannot be said, and the whole layout is drawn: a context
1046
+ * that is not ntk's, a transform that is more than a translation, or a
1047
+ * shadow, which lands where the text is not.
1048
+ */
1049
+ function visibleRows(ctx, y) {
1050
+ if (typeof ctx.drawGlyphs !== 'function') return null;
1051
+ if (typeof ctx._clipExtents !== 'function') return null;
1052
+ const m = ctx._m;
1053
+ if (!m || m[0] !== 1 || m[1] !== 0 || m[2] !== 0 || m[3] !== 1) return null;
1054
+ if (typeof ctx._shadowed !== 'function' || ctx._shadowed()) return null;
1055
+ const height = ctx.height;
1056
+ if (!(height > 0)) return null;
1057
+ // the clip's extents skip a region clip, so they are never smaller than
1058
+ // what the server lets through
1059
+ const box = ctx._clipExtents() ?? { y: 0, h: height };
1060
+ const top = box.y - (y + m[5]);
1061
+ return { top, bottom: top + box.h };
1062
+ }
1063
+
1064
+ /**
1065
+ * How many distinct styles one layout shares shaping styles among. Past it a
1066
+ * span is shaped with a style of its own, as every span once was, rather
1067
+ * than the search growing with the document.
1068
+ */
1069
+ const MAX_SHAPING_STYLES = 16;
1070
+
1071
+ /**
1072
+ * The style `span` is shaped with: one already made for a span whose every
1073
+ * property shaping reads is the same (`===`), or a new one. Its font is the
1074
+ * one the span or the layout named (`given`), else the one its family
1075
+ * matches, looked up once for all the spans that share it. What shaping
1076
+ * reads is what `shapeText`, `fallbackFor` and the memo key read: the face
1077
+ * and what picks it, the size, and features, language and spacing.
1078
+ */
1079
+ function shapingStyleOf(known, span, given, fonts) {
1080
+ for (let i = 0; i < known.length; i++) {
1081
+ const k = known[i];
1082
+ if (
1083
+ k.given === given &&
1084
+ k.family === span.family &&
1085
+ k.size === span.size &&
1086
+ k.weight === span.weight &&
1087
+ k.style === span.style &&
1088
+ k.variations === span.variations &&
1089
+ k.opticalSize === span.opticalSize &&
1090
+ k.opticalSizing === span.opticalSizing &&
1091
+ k.features === span.features &&
1092
+ k.language === span.language &&
1093
+ k.letterSpacing === span.letterSpacing
1094
+ ) {
1095
+ return k;
1096
+ }
1097
+ }
1098
+ const k = {
1099
+ given,
1100
+ font: given ?? fonts.match(span.family, span),
1101
+ family: span.family,
1102
+ size: span.size,
1103
+ weight: span.weight,
1104
+ style: span.style,
1105
+ variations: span.variations,
1106
+ opticalSize: span.opticalSize,
1107
+ opticalSizing: span.opticalSizing,
1108
+ features: span.features,
1109
+ language: span.language,
1110
+ letterSpacing: span.letterSpacing
1111
+ };
1112
+ if (known.length < MAX_SHAPING_STYLES) known.push(k);
1113
+ return k;
1114
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ntk",
3
- "version": "8.13.0",
3
+ "version": "8.13.1",
4
4
  "description": "Desktop UI toolkit for X11 with canvas-like 2d and OpenGL rendering",
5
5
  "author": "Andrey Sidorov <sidorares@yandex.ru>",
6
6
  "license": "MIT",