ntk 8.22.0 → 8.23.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.
@@ -565,6 +565,15 @@ export default class FontManager {
565
565
  return new TextLayout(this, content, style, options);
566
566
  }
567
567
 
568
+ /**
569
+ * Whether `layout` takes `justify`, and fills a line with its word
570
+ * separators itself. True; a caller that asks before it passes the option
571
+ * keeps its own way of justifying for an engine without it.
572
+ */
573
+ get justifies() {
574
+ return true;
575
+ }
576
+
568
577
  /**
569
578
  * Memoized shaping used by TextLayout and the canvas text path, bounded at
570
579
  * two generations of `SHAPE_GENERATION` words. `levelsKey` is a compact
@@ -671,7 +680,7 @@ export default class FontManager {
671
680
  */
672
681
  _shapeCachedWhole(text, style) {
673
682
  const emb = embeddingLevels(text, style.direction);
674
- const baseLevel = emb.paragraphs.length ? emb.paragraphs[0].level : 0;
683
+ const baseLevel = emb.paragraphs[0].level;
675
684
  const shaped = this._shapeCached(text, style, normalizedLevels(emb.levels, 0, text.length));
676
685
  return shaped.baseLevel === baseLevel ? shaped : { ...shaped, baseLevel };
677
686
  }
@@ -12,6 +12,29 @@ const WS = new Set([0x20, 0x09]);
12
12
  const HARD_BREAKS = /[\n\r\u2028\u2029]+$/;
13
13
  const TRAILING_WS = /[ \t]+$/;
14
14
 
15
+ // The word separators a justified line's room is shared out at (CSS Text 3,
16
+ // 7.4, `text-justify: auto` as a browser has it for the scripts that space
17
+ // their words): a space and a no-break space.
18
+ const SEPARATORS = new Set([0x20, 0xa0]);
19
+ const separates = (g) => g.codePoints.length === 1 && SEPARATORS.has(g.codePoints[0]);
20
+
21
+ // What ends a line `justify` sets as it does a paragraph's last: a line feed,
22
+ // a carriage return or a paragraph separator, as a browser's forced break
23
+ // does. A line separator, U+2028, is not one, so a caller can ask a line to
24
+ // break there and be justified.
25
+ const JUSTIFY_STOPS = /[\n\r\u2029]$/;
26
+
27
+ // `justify`'s lines as bits: those that go on to another (REST), and the
28
+ // paragraph's last with those a forced break ends (LAST).
29
+ const REST = 1;
30
+ const LAST = 2;
31
+ function justifyBits(justify) {
32
+ if (justify === true || justify === 'rest') return REST;
33
+ if (justify === 'last') return LAST;
34
+ if (justify === 'all') return REST | LAST;
35
+ return 0;
36
+ }
37
+
15
38
  /** Scripts written without spaces between words, whose line breaking
16
39
  * needs a dictionary (UAX #14's SA class, and Javanese, Balinese and
17
40
  * Buginese, which are AL there): a token of theirs is a run of words. */
@@ -162,6 +185,44 @@ function isWsGlyph(g) {
162
185
  return g.codePoints.length > 0 && g.codePoints.every((cp) => WS.has(cp));
163
186
  }
164
187
 
188
+ /**
189
+ * A line set to fill `width`: what it leaves of it shared equally among its
190
+ * word separators (`SEPARATORS`), each glyph's advance widened by its share
191
+ * — the white space it ends on hangs, and takes none. Its runs are made
192
+ * anew, those with a separator in them over glyphs of their own, since a
193
+ * shaped run is the paragraph's and shared by every layout of it; caret
194
+ * positions, hit testing, drawing and coverage all read the advances, and
195
+ * agree. A line with no separator, or with no room left, keeps its
196
+ * alignment. Answers the line's width.
197
+ */
198
+ function justifyLine(line, width) {
199
+ const free = width - line.width;
200
+ if (!(free > 0)) return line.width;
201
+ let count = 0;
202
+ for (const r of line.runs) {
203
+ for (const g of r.run.glyphs) if (separates(g)) count++;
204
+ }
205
+ if (count === 0) return line.width;
206
+ const extra = free / count;
207
+ let shift = 0;
208
+ line.runs = line.runs.map((r) => {
209
+ const moved = shift ? { ...r, x: r.x + shift } : r;
210
+ let k = 0;
211
+ for (const g of r.run.glyphs) if (separates(g)) k++;
212
+ if (k === 0) return moved;
213
+ shift += k * extra;
214
+ const glyphs = r.run.glyphs.map((g) => (separates(g) ? { ...g, ax: g.ax + extra } : g));
215
+ return {
216
+ ...moved,
217
+ width: r.width + k * extra,
218
+ run: { ...r.run, glyphs, width: r.run.width + k * extra }
219
+ };
220
+ });
221
+ line.width += count * extra;
222
+ line.advance += count * extra;
223
+ return line.width;
224
+ }
225
+
165
226
  /**
166
227
  * Multi-line text layout: breaks (possibly styled) text into lines for a
167
228
  * target container width, with full shaping — kerning, ligatures, complex
@@ -205,6 +266,13 @@ function isWsGlyph(g) {
205
266
  * past the line's end, as CSS's `overflow-wrap: normal` does — but for
206
267
  * text in a script written without spaces (Thai, Khmer, Javanese…), whose
207
268
  * words the breaker cannot find, which is cut as a fallback
269
+ * - `justify` — the lines set to fill `maxWidth`, what each leaves of it
270
+ * shared equally among its word separators, a space and a no-break space:
271
+ * `true` (or `'rest'`) for every line but the paragraph's last and each a
272
+ * forced break ends, as CSS's `text-align: justify`; `'last'` for only
273
+ * those; `'all'` for every line. A line with no separator, or none of the
274
+ * width to spare, keeps `align`'s place, as do the lines not justified.
275
+ * The breaks are the layout's at its width; justifying moves none
208
276
  * - `fit` — 'items' fits and measures a line as a browser does: each span's
209
277
  * part of it rounded up to a 64th of a pixel before it is added, as Blink
210
278
  * rounds an element's text on a line up to a LayoutUnit; a line's `width`
@@ -436,6 +504,11 @@ export class TextLayout {
436
504
  natural = m.lineHeight;
437
505
  }
438
506
  if (runs.length) fitted += round(item);
507
+ // a line's place in the paragraph, for `justify`: its last, which a
508
+ // `maxLines` cut leaves no line, or one a forced break ends
509
+ const ends =
510
+ (li === lineTokens.length - 1 && !this.truncated) ||
511
+ JUSTIFY_STOPS.test(text.slice(Math.max(lineStart, lineEnd - 1), lineEnd));
439
512
  if (fitted > layoutWidth) layoutWidth = fitted;
440
513
  // Half-leading (CSS Inline Layout 3): the slack between the glyphs and
441
514
  // the line box is split evenly above and below, rather than all of it
@@ -462,11 +535,27 @@ export class TextLayout {
462
535
  start: lineStart,
463
536
  end: lineEnd,
464
537
  _contentEnd: contentEnd,
465
- _trailing: trailing
538
+ _trailing: trailing,
539
+ _ends: ends,
540
+ _elided: !!ellipsis
466
541
  });
467
542
  y += box;
468
543
  }
469
544
 
545
+ // ---- justification ----
546
+ // After the lines are broken, so a width the lines are set at is no
547
+ // shaping: a paragraph laid out again at another width breaks the glyphs
548
+ // it has into other lines and spaces them, where one that spaced its
549
+ // words through letter spacing was shaped again at every width.
550
+ const justify = justifyBits(options.justify);
551
+ if (justify && Number.isFinite(maxWidth)) {
552
+ for (const line of this.lines) {
553
+ if (line._elided || !(justify & (line._ends ? LAST : REST))) continue;
554
+ const width = justifyLine(line, maxWidth);
555
+ if (width > layoutWidth) layoutWidth = width;
556
+ }
557
+ }
558
+
470
559
  this.width = layoutWidth;
471
560
  this.height = y;
472
561
 
@@ -537,7 +626,7 @@ export class TextLayout {
537
626
  const text = spans.map((s) => s.text).join('');
538
627
  const emb = embeddingLevels(text, direction);
539
628
  const levels = emb.levels;
540
- const baseLevel = emb.paragraphs.length ? emb.paragraphs[0].level & 1 : 0;
629
+ const baseLevel = emb.paragraphs[0].level & 1;
541
630
 
542
631
  // span boundaries, for binary search by char position
543
632
  const spanStarts = [];
package/lib/text/shape.js CHANGED
@@ -21,17 +21,30 @@ function neverSwitches(cp) {
21
21
 
22
22
  /**
23
23
  * UAX#9 embedding levels for a string. `direction` is 'ltr', 'rtl' or 'auto'
24
- * (first-strong detection). Returns bidi-js's result object.
24
+ * (first-strong detection). Returns bidi-js's result object, which always
25
+ * has a first paragraph: its level is the text's base level.
25
26
  */
26
27
  export function embeddingLevels(text, direction = 'auto') {
27
28
  const explicit = direction === 'ltr' || direction === 'rtl' ? direction : null;
29
+ // An empty text is a paragraph too, at the level its direction names.
30
+ // bidi-js finds no paragraph in it, and a base level read as 0 for want
31
+ // of one set an empty right-to-left field's line, and the caret on it,
32
+ // at the left — where the same field holding only a line break had it
33
+ // at the right. Under 'auto' there is no strong character to find, and
34
+ // the paragraph is left-to-right (UAX#9 P3).
35
+ if (!text.length) {
36
+ return {
37
+ levels: new Uint8Array(0),
38
+ paragraphs: [{ start: 0, end: -1, level: explicit === 'rtl' ? 1 : 0 }]
39
+ };
40
+ }
28
41
  // Text that nothing can make right-to-left is level 0 throughout, which
29
42
  // is what the algorithm would work out a character at a time: every
30
43
  // layout asks, and most text is Latin, CJK or digits with no bidi at all.
31
44
  if (explicit !== 'rtl' && !MAY_REVERSE.test(text)) {
32
45
  return {
33
46
  levels: new Uint8Array(text.length),
34
- paragraphs: text.length ? [{ start: 0, end: text.length - 1, level: 0 }] : []
47
+ paragraphs: [{ start: 0, end: text.length - 1, level: 0 }]
35
48
  };
36
49
  }
37
50
  return bidi.getEmbeddingLevels(text, explicit);
@@ -161,7 +174,7 @@ export function shapeText(fonts, text, style, levels) {
161
174
  if (!levels) {
162
175
  const emb = embeddingLevels(text, style.direction);
163
176
  levels = emb.levels;
164
- baseLevel = emb.paragraphs.length ? emb.paragraphs[0].level : 0;
177
+ baseLevel = emb.paragraphs[0].level;
165
178
  } else {
166
179
  baseLevel = levels.length ? levels[0] & ~1 : 0;
167
180
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ntk",
3
- "version": "8.22.0",
3
+ "version": "8.23.0",
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",