drawnui-react 0.1.0-preview.16 → 0.1.0-preview.17
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/README.md +1 -1
- package/SKIPPED.md +5 -5
- package/dist/controls/AnimatedFramesRenderer.d.ts +7 -0
- package/dist/controls/AnimatedFramesRenderer.js +16 -1
- package/dist/controls/DrawnGame.d.ts +4 -1
- package/dist/controls/DrawnGame.js +7 -3
- package/dist/controls/GridStructure.d.ts +13 -1
- package/dist/controls/GridStructure.js +38 -2
- package/dist/controls/SkiaCarousel.d.ts +1 -1
- package/dist/controls/SkiaCarousel.js +3 -2
- package/dist/controls/SkiaEditor.d.ts +5 -1
- package/dist/controls/SkiaEditor.js +19 -17
- package/dist/controls/SkiaGif.d.ts +3 -0
- package/dist/controls/SkiaGif.js +11 -1
- package/dist/controls/SkiaImage.js +15 -10
- package/dist/controls/SkiaLabel.d.ts +52 -3
- package/dist/controls/SkiaLabel.js +222 -50
- package/dist/controls/SkiaLayout.d.ts +5 -0
- package/dist/controls/SkiaLayout.js +7 -4
- package/dist/controls/SkiaLottie.js +3 -1
- package/dist/controls/SkiaProgress.d.ts +2 -1
- package/dist/controls/SkiaProgress.js +4 -1
- package/dist/controls/SkiaScroll.d.ts +12 -0
- package/dist/controls/SkiaScroll.js +76 -8
- package/dist/controls/SkiaShape.d.ts +13 -2
- package/dist/controls/SkiaShape.js +50 -10
- package/dist/controls/SkiaSlider.d.ts +5 -1
- package/dist/controls/SkiaSlider.js +7 -1
- package/dist/controls/SkiaSprite.d.ts +1 -0
- package/dist/controls/SkiaSprite.js +4 -0
- package/dist/controls/SnappingLayout.d.ts +5 -0
- package/dist/controls/SnappingLayout.js +8 -0
- package/dist/core/Accessibility.d.ts +13 -0
- package/dist/core/Accessibility.js +10 -4
- package/dist/core/Animators.d.ts +28 -1
- package/dist/core/Animators.js +76 -8
- package/dist/core/Canvas.d.ts +21 -4
- package/dist/core/Canvas.js +122 -23
- package/dist/core/Easing.d.ts +12 -1
- package/dist/core/Easing.js +25 -1
- package/dist/core/Gestures.d.ts +24 -1
- package/dist/core/Gestures.js +75 -2
- package/dist/core/Hover.d.ts +14 -4
- package/dist/core/Hover.js +38 -22
- package/dist/core/ScrollAnimators.js +9 -0
- package/dist/core/SkiaControl.d.ts +17 -5
- package/dist/core/SkiaControl.js +69 -37
- package/dist/react/SkiaShell.js +33 -14
- package/dist/react/index.js +29 -2
- package/package.json +15 -1
|
@@ -24,6 +24,8 @@ export class SkiaLabel extends SkiaControl {
|
|
|
24
24
|
verticalTextAlignment = "Start";
|
|
25
25
|
lineSpacing = 1;
|
|
26
26
|
lineHeight = 1;
|
|
27
|
+
paragraphSpacing = 0;
|
|
28
|
+
characterSpacing = 1;
|
|
27
29
|
textTransform = "None";
|
|
28
30
|
padding = Thickness.Zero;
|
|
29
31
|
/** Styled fragments; when not empty they replace `Text` (same precedence as C#). */
|
|
@@ -79,6 +81,18 @@ export class SkiaLabel extends SkiaControl {
|
|
|
79
81
|
/** Multiplier applied to the natural line height (ascent + descent). */
|
|
80
82
|
get LineHeight() { return this.lineHeight; }
|
|
81
83
|
set LineHeight(v) { this.Set("lineHeight", v); }
|
|
84
|
+
/**
|
|
85
|
+
* Space above every paragraph after the first, in lines (C# ParagraphSpacing; 0 here as DrawnUi.Rust, C# 0.25). A
|
|
86
|
+
* paragraph starts after "\n"; U+2028 breaks the line inside a paragraph.
|
|
87
|
+
*/
|
|
88
|
+
get ParagraphSpacing() { return this.paragraphSpacing; }
|
|
89
|
+
set ParagraphSpacing(v) { this.Set("paragraphSpacing", v); }
|
|
90
|
+
/**
|
|
91
|
+
* C# CharacterSpacing: 1 = the font's own spacing; every glyph is followed by (CharacterSpacing - 1) points more
|
|
92
|
+
* (less below 1), the line measures and wraps at the width it draws (C# 7cf1007c, DrawnUi.Rust label).
|
|
93
|
+
*/
|
|
94
|
+
get CharacterSpacing() { return this.characterSpacing; }
|
|
95
|
+
set CharacterSpacing(v) { this.Set("characterSpacing", v); }
|
|
82
96
|
get TextTransform() { return this.textTransform; }
|
|
83
97
|
set TextTransform(v) { this.Set("textTransform", v); }
|
|
84
98
|
get Padding() { return this.padding; }
|
|
@@ -113,6 +127,8 @@ export class SkiaLabel extends SkiaControl {
|
|
|
113
127
|
mainFonts;
|
|
114
128
|
fontsCache = new Map();
|
|
115
129
|
runCache = new Map();
|
|
130
|
+
/** CharacterSpacing in pixels at the measured scale: added after every glyph of a run (its Width includes them). */
|
|
131
|
+
glyphSpacing = 0;
|
|
116
132
|
ResolveFonts(family, weight, italic, sizePx) {
|
|
117
133
|
const key = `${family}|${this.fontFamilyFallback}|${weight}|${italic}|${sizePx}`;
|
|
118
134
|
let f = this.fontsCache.get(key);
|
|
@@ -142,12 +158,24 @@ export class SkiaLabel extends SkiaControl {
|
|
|
142
158
|
const weight = span.FontWeight > 0 ? span.FontWeight : span.IsBold ? 700 : this.fontWeight > 0 ? this.fontWeight : bold ? 700 : 0;
|
|
143
159
|
return this.ResolveFonts(span.FontFamily ?? this.fontFamily, weight, italic, (span.FontSize ?? this.fontSize) * scale);
|
|
144
160
|
}
|
|
145
|
-
|
|
161
|
+
Advance(font, text) {
|
|
146
162
|
let w = 0;
|
|
147
163
|
for (const adv of font.getGlyphWidths(font.getGlyphIDs(text)))
|
|
148
|
-
w += adv;
|
|
164
|
+
w += adv + this.glyphSpacing;
|
|
149
165
|
return w;
|
|
150
166
|
}
|
|
167
|
+
static Ignorable = /[\u200B-\u200F\u2060-\u2064\uFE00-\uFE0F\u{E0000}-\u{E0FFF}]/u;
|
|
168
|
+
/**
|
|
169
|
+
* Default-ignorable code points (zero width space, joiners, direction marks, word joiners, variation selectors such
|
|
170
|
+
* as U+FE0F after an emoji, tags) that none of the fonts has draw nothing and take no room, never the missing-glyph
|
|
171
|
+
* box (DrawnUi.Rust label_ignorables; text is not shaped here, C# hides them by shaping).
|
|
172
|
+
*/
|
|
173
|
+
static DropIgnorables(text, main, fallbacks) {
|
|
174
|
+
if (!SkiaLabel.Ignorable.test(text))
|
|
175
|
+
return text;
|
|
176
|
+
const has = (cp) => main.getGlyphIDs(cp)[0] !== 0 || fallbacks.some((f) => f.getGlyphIDs(cp)[0] !== 0);
|
|
177
|
+
return Array.from(text).filter((cp) => !SkiaLabel.Ignorable.test(cp) || has(cp)).join("");
|
|
178
|
+
}
|
|
151
179
|
/**
|
|
152
180
|
* Splits text into runs by glyph availability: the main font, or the first fallback that has a glyph where the
|
|
153
181
|
* main font has glyph 0. Spaces always stay on the main font (fallback faces often carry very wide spaces).
|
|
@@ -161,8 +189,13 @@ export class SkiaLabel extends SkiaControl {
|
|
|
161
189
|
const main = fonts.Main;
|
|
162
190
|
const fbs = fonts.Fallbacks;
|
|
163
191
|
cached = [];
|
|
192
|
+
text = SkiaLabel.DropIgnorables(text, main, fbs);
|
|
193
|
+
if (text.length === 0) {
|
|
194
|
+
this.runCache.set(cacheKey, cached);
|
|
195
|
+
return cached;
|
|
196
|
+
}
|
|
164
197
|
if (fbs.length === 0) {
|
|
165
|
-
cached.push({ Text: text, Font: main, Width:
|
|
198
|
+
cached.push({ Text: text, Font: main, Width: this.Advance(main, text), Fonts: fonts });
|
|
166
199
|
}
|
|
167
200
|
else {
|
|
168
201
|
const cps = Array.from(text);
|
|
@@ -181,13 +214,13 @@ export class SkiaLabel extends SkiaControl {
|
|
|
181
214
|
const f = fontFor(i);
|
|
182
215
|
if (f !== current) {
|
|
183
216
|
const t = cps.slice(start, i).join("");
|
|
184
|
-
cached.push({ Text: t, Font: current, Width:
|
|
217
|
+
cached.push({ Text: t, Font: current, Width: this.Advance(current, t), Fonts: fonts });
|
|
185
218
|
start = i;
|
|
186
219
|
current = f;
|
|
187
220
|
}
|
|
188
221
|
}
|
|
189
222
|
const t = cps.slice(start).join("");
|
|
190
|
-
cached.push({ Text: t, Font: current, Width:
|
|
223
|
+
cached.push({ Text: t, Font: current, Width: this.Advance(current, t), Fonts: fonts });
|
|
191
224
|
}
|
|
192
225
|
this.runCache.set(cacheKey, cached);
|
|
193
226
|
}
|
|
@@ -195,6 +228,76 @@ export class SkiaLabel extends SkiaControl {
|
|
|
195
228
|
}
|
|
196
229
|
Width(runs) { let w = 0; for (const r of runs)
|
|
197
230
|
w += r.Width; return w; }
|
|
231
|
+
/** A line never starts with these (closing punctuation, small kana, the long vowel mark): JIS X 4051 kinsoku (C#). */
|
|
232
|
+
static NoBreakBefore = "、。,.・:;?!゛゜ヽヾゝゞ々〻ー」』)〕]}〉》】〗〙〟⦆»’”‐゠–〜~ぁぃぅぇぉっゃゅょゎゕゖァィゥェォッャュョヮヵヶㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ。」、・ーァィゥェォャュョッ)]},.!?:;%";
|
|
233
|
+
/** A line never ends with these (opening brackets and quotes). */
|
|
234
|
+
static NoBreakAfter = "「『(〔[{〈《【〖〘〝⦅«‘“「([{";
|
|
235
|
+
/** Chinese / Japanese: ideographs, kana, CJK punctuation, full-width forms (C# IsCjkAt). Korean breaks at spaces only. */
|
|
236
|
+
static Cjk = /[\u3000-\u312F\u3190-\u33FF\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF\uFF00-\uFFEF\u{20000}-\u{3FFFF}]/u;
|
|
237
|
+
static CanBreakBetween(before, after) {
|
|
238
|
+
return (SkiaLabel.Cjk.test(before) || SkiaLabel.Cjk.test(after)) && !SkiaLabel.NoBreakBefore.includes(after) && !SkiaLabel.NoBreakAfter.includes(before);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* C# CanBreakInsideWord: whether a line may break between text[index - 1] and text[index] (UTF-16 indexes) inside a
|
|
242
|
+
* space-free run: next to a Chinese or Japanese character, never before closing punctuation or small kana, never
|
|
243
|
+
* after an opening bracket, never inside a surrogate pair.
|
|
244
|
+
*/
|
|
245
|
+
static CanBreakInsideWord(text, index) {
|
|
246
|
+
if (index <= 0 || index >= text.length)
|
|
247
|
+
return false;
|
|
248
|
+
const hi = (k) => { const c = text.charCodeAt(k); return c >= 0xD800 && c <= 0xDBFF; };
|
|
249
|
+
if (hi(index - 1))
|
|
250
|
+
return false; // inside a surrogate pair
|
|
251
|
+
const before = String.fromCodePoint(text.codePointAt(hi(index - 2) && index >= 2 ? index - 2 : index - 1));
|
|
252
|
+
const after = String.fromCodePoint(text.codePointAt(index));
|
|
253
|
+
return SkiaLabel.CanBreakBetween(before, after);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* C# FitAtBreak: the last place inside the word where a Chinese / Japanese line may break and `prefix` plus the word
|
|
257
|
+
* up to there still fits; code points from the word's start, 0 = none.
|
|
258
|
+
*/
|
|
259
|
+
FitCjk(word, prefix, maxWidth) {
|
|
260
|
+
if (!word.some((r) => SkiaLabel.Cjk.test(r.Text)))
|
|
261
|
+
return 0;
|
|
262
|
+
const sp = this.glyphSpacing;
|
|
263
|
+
let x = prefix, best = 0, before = "", k = 0;
|
|
264
|
+
for (const r of word) {
|
|
265
|
+
const cps = Array.from(r.Text), widths = r.Font.getGlyphWidths(r.Font.getGlyphIDs(r.Text, cps.length));
|
|
266
|
+
for (let i = 0; i < cps.length; i++) {
|
|
267
|
+
if (k > 0 && SkiaLabel.CanBreakBetween(before, cps[i])) {
|
|
268
|
+
if (x - sp > maxWidth)
|
|
269
|
+
return best;
|
|
270
|
+
best = k;
|
|
271
|
+
}
|
|
272
|
+
x += widths[i] + sp;
|
|
273
|
+
before = cps[i];
|
|
274
|
+
k++;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return best;
|
|
278
|
+
}
|
|
279
|
+
/** The word cut after `count` code points: the head goes on a line, the rest follows. */
|
|
280
|
+
SplitWord(word, count) {
|
|
281
|
+
const head = [], rest = [];
|
|
282
|
+
let left = count;
|
|
283
|
+
for (const r of word) {
|
|
284
|
+
const cps = Array.from(r.Text);
|
|
285
|
+
if (left >= cps.length) {
|
|
286
|
+
head.push(r);
|
|
287
|
+
left -= cps.length;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (left > 0) {
|
|
291
|
+
const a = cps.slice(0, left).join(""), b = cps.slice(left).join("");
|
|
292
|
+
head.push({ ...r, Text: a, Width: this.Advance(r.Font, a) });
|
|
293
|
+
rest.push({ ...r, Text: b, Width: this.Advance(r.Font, b) });
|
|
294
|
+
left = 0;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
rest.push(r);
|
|
298
|
+
}
|
|
299
|
+
return [head, rest];
|
|
300
|
+
}
|
|
198
301
|
Transform(text) {
|
|
199
302
|
switch (this.textTransform) {
|
|
200
303
|
case "Uppercase": return text.toUpperCase();
|
|
@@ -210,10 +313,16 @@ export class SkiaLabel extends SkiaControl {
|
|
|
210
313
|
Tokenize(scale) {
|
|
211
314
|
const paragraphs = [[]];
|
|
212
315
|
const add = (text, fonts, span) => {
|
|
213
|
-
|
|
316
|
+
// "\n" starts a paragraph, U+2028 breaks the line inside one
|
|
317
|
+
const pieces = this.Transform(text).split(/(\n|\u2028)/);
|
|
318
|
+
const parts = pieces.filter((_, i) => i % 2 === 0), seps = pieces.filter((_, i) => i % 2 === 1);
|
|
214
319
|
for (let p = 0; p < parts.length; p++) {
|
|
215
|
-
if (p > 0)
|
|
216
|
-
|
|
320
|
+
if (p > 0) {
|
|
321
|
+
const next = [];
|
|
322
|
+
if (seps[p - 1] === "\u2028")
|
|
323
|
+
next.LineBreak = true;
|
|
324
|
+
paragraphs.push(next);
|
|
325
|
+
}
|
|
217
326
|
const para = paragraphs[paragraphs.length - 1];
|
|
218
327
|
const words = parts[p].split(" ");
|
|
219
328
|
for (let w = 0; w < words.length; w++) {
|
|
@@ -259,13 +368,21 @@ export class SkiaLabel extends SkiaControl {
|
|
|
259
368
|
line.Descent = r.Fonts.Descent;
|
|
260
369
|
}
|
|
261
370
|
}
|
|
262
|
-
/**
|
|
263
|
-
|
|
371
|
+
/**
|
|
372
|
+
* Word-wraps tokens into lines that fit maxWidth (Infinity = no wrap); keeps the lines that fit maxHeight (at least one)
|
|
373
|
+
* and MaxLines, the last kept one ending with a tail ellipsis (C# / DrawnUi.Rust: a label in a box too short for its
|
|
374
|
+
* text cuts it, not only MaxLines).
|
|
375
|
+
*/
|
|
376
|
+
LayoutLines(maxWidth, scale, maxHeight = Infinity) {
|
|
264
377
|
const wrap = this.lineBreakMode !== "NoWrap" && isFinite(maxWidth);
|
|
378
|
+
// run widths carry the gap after every glyph; the one after a line's last glyph is not drawn
|
|
379
|
+
const sp = this.glyphSpacing;
|
|
265
380
|
const out = [];
|
|
266
381
|
const empty = (fonts) => { const l = this.NewLine(); l.Ascent = fonts.Ascent; l.Descent = fonts.Descent; return l; };
|
|
267
382
|
for (const para of this.Tokenize(scale)) {
|
|
268
383
|
let line = this.NewLine();
|
|
384
|
+
if (out.length > 0 && !para.LineBreak)
|
|
385
|
+
line.Para = true;
|
|
269
386
|
let lastFonts = this.mainFonts;
|
|
270
387
|
for (let i = 0; i < para.length; i++) {
|
|
271
388
|
const tok = para[i];
|
|
@@ -276,46 +393,71 @@ export class SkiaLabel extends SkiaControl {
|
|
|
276
393
|
i++;
|
|
277
394
|
word = word.concat(this.Segment(para[i].Text, para[i].Fonts, para[i].Span));
|
|
278
395
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
396
|
+
for (;;) {
|
|
397
|
+
const space = tok.SpaceBefore && line.Runs.length > 0 ? this.Segment(" ", tok.Fonts, tok.Span) : [];
|
|
398
|
+
const spaceW = this.Width(space), wordW = this.Width(word);
|
|
399
|
+
if (!wrap || line.Width + spaceW + wordW - sp <= maxWidth || (line.Runs.length === 0 && wordW - sp <= maxWidth)) {
|
|
400
|
+
this.Append(line, space);
|
|
401
|
+
this.Append(line, word);
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
// Chinese / Japanese: the word fills the line up to its last break that fits (C# FitAtBreak)
|
|
405
|
+
const k = this.FitCjk(word, line.Width + spaceW, maxWidth);
|
|
406
|
+
if (k > 0) {
|
|
407
|
+
const [head, rest] = this.SplitWord(word, k);
|
|
408
|
+
this.Append(line, space);
|
|
409
|
+
this.Append(line, head);
|
|
410
|
+
out.push(line);
|
|
411
|
+
line = this.NewLine();
|
|
412
|
+
word = rest;
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (line.Runs.length > 0) {
|
|
416
|
+
out.push(line);
|
|
417
|
+
line = this.NewLine();
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
// word longer than the line: break by code points
|
|
421
|
+
for (const r of word) {
|
|
422
|
+
for (const ch of Array.from(r.Text)) {
|
|
423
|
+
const run = this.Segment(ch, r.Fonts, r.Span);
|
|
424
|
+
const chW = this.Width(run);
|
|
425
|
+
if (line.Runs.length > 0 && line.Width + chW - sp > maxWidth) {
|
|
426
|
+
out.push(line);
|
|
427
|
+
line = this.NewLine();
|
|
428
|
+
}
|
|
429
|
+
this.Append(line, run);
|
|
302
430
|
}
|
|
303
|
-
this.Append(line, run);
|
|
304
431
|
}
|
|
432
|
+
break;
|
|
305
433
|
}
|
|
306
434
|
}
|
|
307
435
|
out.push(line.Runs.length > 0 ? line : empty(lastFonts));
|
|
308
436
|
}
|
|
309
|
-
|
|
310
|
-
|
|
437
|
+
let kept = this.maxLines > 0 ? Math.min(out.length, this.maxLines) : out.length;
|
|
438
|
+
if (isFinite(maxHeight)) {
|
|
439
|
+
let used = 0, fit = 0;
|
|
440
|
+
for (let i = 0; i < out.length; i++) {
|
|
441
|
+
const lh = this.LineHeightPx(out[i]);
|
|
442
|
+
// the block height of the first i + 1 lines: spacing after every line but the last, paragraph space above
|
|
443
|
+
const total = used + lh + (i > 0 ? this.ParagraphSpace(out[i]) : 0);
|
|
444
|
+
if (total > maxHeight && fit > 0)
|
|
445
|
+
break;
|
|
446
|
+
fit++;
|
|
447
|
+
used = total - lh + lh * this.lineSpacing;
|
|
448
|
+
}
|
|
449
|
+
kept = Math.min(kept, fit);
|
|
450
|
+
}
|
|
451
|
+
if (kept < out.length) {
|
|
452
|
+
out.length = kept;
|
|
311
453
|
const truncates = this.lineBreakMode === "TailTruncation" || this.lineBreakMode === "HeadTruncation" || this.lineBreakMode === "MiddleTruncation";
|
|
312
454
|
if (truncates) {
|
|
313
|
-
const last = out[
|
|
455
|
+
const last = out[kept - 1];
|
|
314
456
|
const tailRun = last.Runs[last.Runs.length - 1];
|
|
315
457
|
const fonts = tailRun ? tailRun.Fonts : this.mainFonts;
|
|
316
458
|
const ell = this.Segment("…", fonts, tailRun?.Span);
|
|
317
459
|
const ellW = this.Width(ell);
|
|
318
|
-
while (last.Runs.length > 0 && last.Width + ellW > maxWidth) {
|
|
460
|
+
while (last.Runs.length > 0 && last.Width + ellW - sp > maxWidth) {
|
|
319
461
|
const r = last.Runs[last.Runs.length - 1];
|
|
320
462
|
const cps = Array.from(r.Text);
|
|
321
463
|
cps.pop();
|
|
@@ -327,27 +469,39 @@ export class SkiaLabel extends SkiaControl {
|
|
|
327
469
|
continue;
|
|
328
470
|
}
|
|
329
471
|
r.Text = cps.join("");
|
|
330
|
-
r.Width =
|
|
472
|
+
r.Width = this.Advance(r.Font, r.Text);
|
|
331
473
|
last.Width += r.Width;
|
|
332
474
|
}
|
|
333
475
|
this.Append(last, ell);
|
|
334
476
|
}
|
|
335
477
|
}
|
|
478
|
+
if (sp !== 0)
|
|
479
|
+
for (const l of out)
|
|
480
|
+
if (l.Runs.length > 0)
|
|
481
|
+
l.Width -= sp;
|
|
336
482
|
return out;
|
|
337
483
|
}
|
|
338
484
|
LineHeightPx(line) { return (line.Ascent + line.Descent) * this.lineHeight; }
|
|
485
|
+
/** ParagraphSpacing above a line that starts a paragraph (after the first): that many of its lines, with their spacing. */
|
|
486
|
+
ParagraphSpace(line) { return line.Para && this.paragraphSpacing ? this.LineHeightPx(line) * this.lineSpacing * this.paragraphSpacing : 0; }
|
|
339
487
|
BlockHeight() {
|
|
340
488
|
let h = 0;
|
|
341
489
|
for (let i = 0; i < this.lines.length; i++) {
|
|
342
490
|
const lh = this.LineHeightPx(this.lines[i]);
|
|
343
|
-
h += i < this.lines.length - 1 ? lh * this.lineSpacing : lh;
|
|
491
|
+
h += (i < this.lines.length - 1 ? lh * this.lineSpacing : lh) + this.ParagraphSpace(this.lines[i]);
|
|
344
492
|
}
|
|
345
493
|
return h;
|
|
346
494
|
}
|
|
347
|
-
MeasureAbsolute(widthConstraint,
|
|
495
|
+
MeasureAbsolute(widthConstraint, heightConstraint, scale) {
|
|
348
496
|
this.mainFonts = this.ResolveMainFonts(scale);
|
|
497
|
+
const glyphSpacing = scale * (this.characterSpacing - 1);
|
|
498
|
+
if (glyphSpacing !== this.glyphSpacing) {
|
|
499
|
+
this.glyphSpacing = glyphSpacing;
|
|
500
|
+
this.runCache.clear();
|
|
501
|
+
}
|
|
349
502
|
const px = this.padding.HorizontalThickness * scale, py = this.padding.VerticalThickness * scale;
|
|
350
|
-
|
|
503
|
+
const extraH = this.EffectsExtra(scale).H;
|
|
504
|
+
this.lines = this.text || this.Spans.length > 0 ? this.LayoutLines(widthConstraint - px, scale, heightConstraint - py - extraH) : [];
|
|
351
505
|
let width = 0;
|
|
352
506
|
for (const l of this.lines)
|
|
353
507
|
width = Math.max(width, l.Width);
|
|
@@ -378,6 +532,7 @@ export class SkiaLabel extends SkiaControl {
|
|
|
378
532
|
y += extra.Stroke / 2;
|
|
379
533
|
const out = [];
|
|
380
534
|
for (const line of this.lines) {
|
|
535
|
+
y += this.ParagraphSpace(line);
|
|
381
536
|
const lh = this.LineHeightPx(line);
|
|
382
537
|
const lineW = line.Width + extra.W;
|
|
383
538
|
let x = left;
|
|
@@ -430,7 +585,7 @@ export class SkiaLabel extends SkiaControl {
|
|
|
430
585
|
cps.forEach((cp, k) => {
|
|
431
586
|
const w = widths[k] ?? 0;
|
|
432
587
|
out.push({ Index: index, Length: cp.length, Line: li, Left: x, Top: g.y, Width: w, Height: g.h });
|
|
433
|
-
x += w;
|
|
588
|
+
x += w + this.glyphSpacing;
|
|
434
589
|
index += cp.length;
|
|
435
590
|
});
|
|
436
591
|
}
|
|
@@ -512,9 +667,25 @@ export class SkiaLabel extends SkiaControl {
|
|
|
512
667
|
return paint;
|
|
513
668
|
};
|
|
514
669
|
const canvas = ctx.Context.Canvas;
|
|
670
|
+
const sp = this.glyphSpacing;
|
|
671
|
+
// spaced text: every glyph placed with the gap after it (C# MeasureLineGlyphs positions)
|
|
672
|
+
const drawRun = (run, rx, ry, paint) => {
|
|
673
|
+
if (sp === 0) {
|
|
674
|
+
canvas.drawText(run.Text, rx, ry, paint, run.Font);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
const ids = run.Font.getGlyphIDs(run.Text), widths = run.Font.getGlyphWidths(ids);
|
|
678
|
+
const positions = new Float32Array(ids.length * 2);
|
|
679
|
+
for (let k = 0, gx = 0; k < ids.length; k++) {
|
|
680
|
+
positions[k * 2] = gx;
|
|
681
|
+
gx += widths[k] + sp;
|
|
682
|
+
}
|
|
683
|
+
canvas.drawGlyphs(ids, positions, rx, ry, run.Font, paint);
|
|
684
|
+
};
|
|
515
685
|
const gradient = this.FillGradient;
|
|
516
686
|
const textRect = new SKRect(left, top, right, bottom);
|
|
517
687
|
for (const line of this.lines) {
|
|
688
|
+
y += this.ParagraphSpace(line);
|
|
518
689
|
const lh = this.LineHeightPx(line);
|
|
519
690
|
const lineW = line.Width + extra.W;
|
|
520
691
|
let x = left;
|
|
@@ -530,22 +701,23 @@ export class SkiaLabel extends SkiaControl {
|
|
|
530
701
|
this.SetupGradient(strokePaint, this.StrokeGradient, gradientRect ?? new SKRect(x, y, x + line.Width, y + lh));
|
|
531
702
|
for (const run of line.Runs) {
|
|
532
703
|
const span = run.Span;
|
|
704
|
+
const drawnW = run.Width - sp; // the gap after the run's last glyph is not part of it
|
|
533
705
|
if (span) {
|
|
534
|
-
span.Rects.push(new SKRect(x - d.Left, y - d.Top, x +
|
|
706
|
+
span.Rects.push(new SKRect(x - d.Left, y - d.Top, x + drawnW - d.Left, y + lh - d.Top));
|
|
535
707
|
if (span.BackgroundColor)
|
|
536
|
-
canvas.drawRect(CK.LTRBRect(x, y, x +
|
|
708
|
+
canvas.drawRect(CK.LTRBRect(x, y, x + drawnW, y + lh), paintFor(span.BackgroundColor));
|
|
537
709
|
}
|
|
538
710
|
const color = span?.TextColor ?? this.textColor;
|
|
539
711
|
if (run.Text) {
|
|
540
712
|
// C# DrawText order: drop shadow, stroke, fill
|
|
541
713
|
if (shadowPaint)
|
|
542
|
-
|
|
714
|
+
drawRun(run, x + shadowDx, baseline + shadowDy, shadowPaint);
|
|
543
715
|
if (strokePaint)
|
|
544
|
-
|
|
716
|
+
drawRun(run, x, baseline, strokePaint);
|
|
545
717
|
const paint = paintFor(color);
|
|
546
718
|
if (gradient && gradientRect)
|
|
547
719
|
this.SetupGradient(paint, gradient, gradientRect);
|
|
548
|
-
|
|
720
|
+
drawRun(run, x, baseline, paint);
|
|
549
721
|
}
|
|
550
722
|
if (span?.HasDecorations) {
|
|
551
723
|
// same geometry as C# DrawSpanDecorations; CanvasKit exposes no underline/strikeout/x-height metrics,
|
|
@@ -553,11 +725,11 @@ export class SkiaLabel extends SkiaControl {
|
|
|
553
725
|
if (span.Underline && span.UnderlineWidth !== 0) {
|
|
554
726
|
const w = span.UnderlineWidth > 0 ? span.UnderlineWidth * scale : -span.UnderlineWidth;
|
|
555
727
|
const yl = Math.round(baseline + scale);
|
|
556
|
-
canvas.drawLine(x, yl, x +
|
|
728
|
+
canvas.drawLine(x, yl, x + drawnW, yl, paintFor(color, w));
|
|
557
729
|
}
|
|
558
730
|
if (span.Strikeout) {
|
|
559
731
|
const yl = Math.round(baseline - (run.Fonts.SizePx * 0.52) / 2);
|
|
560
|
-
canvas.drawLine(x, yl, x +
|
|
732
|
+
canvas.drawLine(x, yl, x + drawnW, yl, paintFor(span.StrikeoutColor, span.StrikeoutWidth * scale));
|
|
561
733
|
}
|
|
562
734
|
}
|
|
563
735
|
x += run.Width;
|
|
@@ -165,6 +165,11 @@ export declare class SkiaLayout extends SkiaControl {
|
|
|
165
165
|
private MvScheduleBackground;
|
|
166
166
|
/** Binds a cell to items[index] (through the adapter, so MeasureFirst's cell 0 stays realized) and measures it. */
|
|
167
167
|
private MeasureItem;
|
|
168
|
+
/**
|
|
169
|
+
* The item's offset and height are real, not estimated (C# LastMeasuredIndexLocal >= index): MeasureVisible knows
|
|
170
|
+
* them once the measured prefix passed the item; the other strategies always do.
|
|
171
|
+
*/
|
|
172
|
+
IsItemMeasured(index: number): boolean;
|
|
168
173
|
/** Pixel offset of item index from the top of the layout content (inside padding). */
|
|
169
174
|
GetItemOffsetPixels(index: number): number;
|
|
170
175
|
private ItemHeightPixels;
|
|
@@ -731,6 +731,11 @@ export class SkiaLayout extends SkiaControl {
|
|
|
731
731
|
this.ChildrenFactory.ReleaseViewAt(index);
|
|
732
732
|
return h;
|
|
733
733
|
}
|
|
734
|
+
/**
|
|
735
|
+
* The item's offset and height are real, not estimated (C# LastMeasuredIndexLocal >= index): MeasureVisible knows
|
|
736
|
+
* them once the measured prefix passed the item; the other strategies always do.
|
|
737
|
+
*/
|
|
738
|
+
IsItemMeasured(index) { return this.MeasureItemsStrategy !== "MeasureVisible" || index < this.mvMeasured; }
|
|
734
739
|
/** Pixel offset of item index from the top of the layout content (inside padding). */
|
|
735
740
|
GetItemOffsetPixels(index) {
|
|
736
741
|
const scale = this.RenderingScale;
|
|
@@ -823,12 +828,10 @@ export class SkiaLayout extends SkiaControl {
|
|
|
823
828
|
return;
|
|
824
829
|
}
|
|
825
830
|
const composing = this.IsRenderingWithComposition;
|
|
826
|
-
// inside a composite that redraws only some areas, a child entirely outside them is not even traversed
|
|
827
|
-
const culling = !composing && SkiaControl.CompositionCulling > 0;
|
|
828
831
|
for (const v of this.GetOrderedSubviews()) {
|
|
829
|
-
if (composing
|
|
832
|
+
if (composing && !this.DirtyChildrenInternal.has(v))
|
|
830
833
|
continue;
|
|
831
|
-
v.Render(ctx);
|
|
834
|
+
v.Render(ctx); // a child entirely outside the clip skips itself
|
|
832
835
|
}
|
|
833
836
|
}
|
|
834
837
|
GetCompositeChildren() { return this.IsTemplatedList || this.IsRecycledLayout ? [] : this.GetOrderedSubviews(); }
|
|
@@ -32,7 +32,9 @@ export class SkiaLottie extends AnimatedFramesRenderer {
|
|
|
32
32
|
Animation;
|
|
33
33
|
constructor() {
|
|
34
34
|
super();
|
|
35
|
-
|
|
35
|
+
// C# ImageDoubleBuffered pays off where a thread bakes the bitmap; on the web it is one offscreen pass per frame
|
|
36
|
+
// while playing, so the web engines default to Operations (drawnui-cross rule 3)
|
|
37
|
+
this.UseCache = "Operations";
|
|
36
38
|
}
|
|
37
39
|
get Source() { return this.source; }
|
|
38
40
|
set Source(v) { if (this.source !== v) {
|
|
@@ -36,5 +36,6 @@ export declare class SkiaProgress extends SkiaControl {
|
|
|
36
36
|
get Ratio(): number;
|
|
37
37
|
protected MeasureAbsolute(w: number, _h: number, scale: number): ScaledSize;
|
|
38
38
|
protected Paint(ctx: DrawingContext): void;
|
|
39
|
-
|
|
39
|
+
/** Value between Min and Max, read as "65%"; the name is the app's AccessibilityLabel only (drawnui-cross 6c). */
|
|
40
|
+
get AccessibilityValue(): import("../core/Accessibility").AccessibilityValue;
|
|
40
41
|
}
|
|
@@ -76,5 +76,8 @@ export class SkiaProgress extends SkiaControl {
|
|
|
76
76
|
}
|
|
77
77
|
paint.delete();
|
|
78
78
|
}
|
|
79
|
-
|
|
79
|
+
/** Value between Min and Max, read as "65%"; the name is the app's AccessibilityLabel only (drawnui-cross 6c). */
|
|
80
|
+
get AccessibilityValue() {
|
|
81
|
+
return { Now: this.value, Min: this.min, Max: this.max, Step: 0, Text: `${Math.round(this.Ratio * 100)}%` };
|
|
82
|
+
}
|
|
80
83
|
}
|
|
@@ -218,6 +218,18 @@ export declare class SkiaScroll extends SkiaControl {
|
|
|
218
218
|
* templated layout (the recycled list is the scroll's only child; a header goes above the scroll or in Header).
|
|
219
219
|
*/
|
|
220
220
|
ScrollToIndex(index: number, animate: boolean, option?: RelativePositionType): void;
|
|
221
|
+
/**
|
|
222
|
+
* An open ScrollToIndex (C# OrderedScrollToIndex, DrawnUi.Rust scroll_to_index): a row not measured yet is gone to by
|
|
223
|
+
* the sizes as they are estimated, and the order is aimed again every frame until the row has its real place and the
|
|
224
|
+
* viewport stands on it; an animated move goes on to a moved target in the time it has left, later corrections jump.
|
|
225
|
+
* A press, the wheel or another scroll drops it; while it is open LoadMore waits.
|
|
226
|
+
*/
|
|
227
|
+
private indexOrder?;
|
|
228
|
+
private pursuing;
|
|
229
|
+
/** One step of the open order (C# ExecuteScrollToIndexOrder, Rust `pursue`), at the start of every paint. */
|
|
230
|
+
private PursueIndexOrder;
|
|
231
|
+
/** The offset (points) that puts item `index` where `option` says, by the sizes the list knows now; Exact = its place is measured. */
|
|
232
|
+
private AimIndex;
|
|
221
233
|
private StopAnimators;
|
|
222
234
|
/** DrawnUi DecelerationRatio: FrictionScrolled / 100, floored at 0.1 friction. */
|
|
223
235
|
private get DecelerationRatio();
|