ntk 8.15.0 → 8.15.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/text/layout.js +84 -11
- package/package.json +1 -1
package/lib/text/layout.js
CHANGED
|
@@ -25,15 +25,88 @@ const UNSPACED =
|
|
|
25
25
|
* 12.8px measures 410.0125px in the 410px column it was written for — and
|
|
26
26
|
* a browser takes the line as fitting, comparing in 64ths of a pixel with
|
|
27
27
|
* that much to spare (Blink's line breaker adds a LayoutUnit's epsilon to
|
|
28
|
-
* the width it fits to).
|
|
28
|
+
* the width it fits to). What it compares is the line's items, each
|
|
29
|
+
* rounded up to a 64th first (`fittedWidth`).
|
|
29
30
|
*/
|
|
30
31
|
const FIT_SLACK = 1 / 64;
|
|
31
32
|
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
/** A width rounded up to a 64th of a pixel, as Blink rounds an item's
|
|
34
|
+
* shaped width up to a LayoutUnit (`ShapeResult::SnappedWidth`). The
|
|
35
|
+
* nudge keeps a sum that lands on a 64th, give or take the last bit of a
|
|
36
|
+
* double, where it lands. */
|
|
37
|
+
function snapUp(width) {
|
|
38
|
+
return Math.ceil(width * 64 - 1e-7) / 64;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A line's width as a browser fits it: its spans' parts of it each rounded
|
|
43
|
+
* up to a 64th of a pixel and summed, its trailing white space left out.
|
|
44
|
+
*
|
|
45
|
+
* A browser measures a line in items — the text of one element between its
|
|
46
|
+
* edges, shaped — and rounds each one's width up to a LayoutUnit before it
|
|
47
|
+
* adds it (Blink's `ShapeResult::SnappedWidth`, which every text item's
|
|
48
|
+
* `inline_size` on a line is), so a line of three items is up to three 64ths
|
|
49
|
+
* wider than its advances. Compared with the width and its 64th to spare
|
|
50
|
+
* (`FIT_SLACK`), a line of Verdana 0.002px past its 529px, the `<abbr>` in
|
|
51
|
+
* it making three items, is 2 64ths past and breaks, as it does in Chrome;
|
|
52
|
+
* a line that is one item and a hair over still fits. A span is an item,
|
|
53
|
+
* and so is its part of a line; a `kernAcross` span is not one of its own
|
|
54
|
+
* — it is a space a justified line or `word-spacing` spaces out, which a
|
|
55
|
+
* browser spaces inside the item — and is measured with the span before it.
|
|
56
|
+
*
|
|
57
|
+
* Kept a token at a time: `add` puts one on the line, `with` is the line's
|
|
58
|
+
* width were the next one put on it.
|
|
59
|
+
*/
|
|
60
|
+
class LineFit {
|
|
61
|
+
constructor() {
|
|
62
|
+
this.reset();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
reset() {
|
|
66
|
+
/** the items closed on the line, each rounded up */
|
|
67
|
+
this.closed = 0;
|
|
68
|
+
/** the width of the item the line ends in, as far as it goes */
|
|
69
|
+
this.open = 0;
|
|
70
|
+
this.span = null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
add(token, kern) {
|
|
74
|
+
const frags = token.fragments;
|
|
75
|
+
for (let i = 0; i < frags.length; i++) {
|
|
76
|
+
const frag = frags[i];
|
|
77
|
+
const width = frag.shaped.width + (frag.kern ?? 0) + (i === 0 ? kern : 0);
|
|
78
|
+
if (this.span === null || this.span === frag.span || frag.span.kernAcross || this.span.kernAcross) {
|
|
79
|
+
this.open += width;
|
|
80
|
+
} else {
|
|
81
|
+
this.closed += snapUp(this.open);
|
|
82
|
+
this.open = width;
|
|
83
|
+
}
|
|
84
|
+
this.span = frag.span;
|
|
85
|
+
}
|
|
86
|
+
// a token of no text, a forced break's, adds nothing but its kerning
|
|
87
|
+
if (!frags.length) this.open += kern;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The line's width with `token` on it, the white space it ends on left
|
|
91
|
+
* out. */
|
|
92
|
+
with(token, kern) {
|
|
93
|
+
const { closed, open, span } = this;
|
|
94
|
+
this.add(token, kern);
|
|
95
|
+
const width = this.closed + snapUp(this.open - token.wsWidth);
|
|
96
|
+
this.closed = closed;
|
|
97
|
+
this.open = open;
|
|
98
|
+
this.span = span;
|
|
99
|
+
return width;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A line's tokens' width as a browser fits it (`LineFit`). */
|
|
104
|
+
function fittedWidth(toks) {
|
|
105
|
+
if (!toks.length) return 0;
|
|
106
|
+
const fit = new LineFit();
|
|
107
|
+
for (let i = 0; i < toks.length - 1; i++) fit.add(toks[i], i ? kernBefore(toks[i]) : 0);
|
|
108
|
+
const last = toks.length - 1;
|
|
109
|
+
return fit.with(toks[last], last ? kernBefore(toks[last]) : 0);
|
|
37
110
|
}
|
|
38
111
|
|
|
39
112
|
/** The kerning between a token and the one before it on its line
|
|
@@ -136,16 +209,16 @@ export class TextLayout {
|
|
|
136
209
|
const wraps = options.wrap !== false;
|
|
137
210
|
{
|
|
138
211
|
let cur = [];
|
|
139
|
-
|
|
212
|
+
const fit = new LineFit();
|
|
140
213
|
const flush = () => {
|
|
141
214
|
lineTokens.push(cur);
|
|
142
215
|
cur = [];
|
|
143
|
-
|
|
216
|
+
fit.reset();
|
|
144
217
|
};
|
|
145
218
|
for (let token of tokens) {
|
|
146
219
|
while (
|
|
147
220
|
wraps &&
|
|
148
|
-
|
|
221
|
+
fit.with(token, cur.length ? kernBefore(token) : 0) > maxWidth + FIT_SLACK
|
|
149
222
|
) {
|
|
150
223
|
if (cur.length > 0) {
|
|
151
224
|
flush();
|
|
@@ -160,7 +233,7 @@ export class TextLayout {
|
|
|
160
233
|
token = rest;
|
|
161
234
|
}
|
|
162
235
|
}
|
|
163
|
-
|
|
236
|
+
fit.add(token, cur.length ? kernBefore(token) : 0);
|
|
164
237
|
cur.push(token);
|
|
165
238
|
if (token.required) flush();
|
|
166
239
|
}
|
|
@@ -199,7 +272,7 @@ export class TextLayout {
|
|
|
199
272
|
// Elision is the last kept line's business, or a line's that does not
|
|
200
273
|
// wrap and runs past the width, and it happens before entries exist,
|
|
201
274
|
// because dropping content means re-shaping the tail.
|
|
202
|
-
const cut = (elide && li === lineTokens.length - 1) || (cutWide &&
|
|
275
|
+
const cut = (elide && li === lineTokens.length - 1) || (cutWide && fittedWidth(toks) > maxWidth + FIT_SLACK);
|
|
203
276
|
const ellipsis = cut ? this._elide(toks, baseSpan) : null;
|
|
204
277
|
if (ellipsis) {
|
|
205
278
|
toks = this._fitBefore(toks, maxWidth - ellipsis.width);
|