drawnui-react 0.1.0-preview.2 → 0.1.0-preview.21
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 +17 -4
- package/SKIPPED.md +33 -27
- package/dist/controls/AnimatedFramesRenderer.d.ts +7 -0
- package/dist/controls/AnimatedFramesRenderer.js +16 -1
- package/dist/controls/DrawnGame.d.ts +40 -0
- package/dist/controls/DrawnGame.js +64 -0
- package/dist/controls/GridStructure.d.ts +16 -2
- package/dist/controls/GridStructure.js +40 -4
- package/dist/controls/SkiaBackdrop.d.ts +8 -2
- package/dist/controls/SkiaBackdrop.js +48 -8
- package/dist/controls/SkiaButton.d.ts +19 -2
- package/dist/controls/SkiaButton.js +41 -11
- package/dist/controls/SkiaCarousel.d.ts +9 -1
- package/dist/controls/SkiaCarousel.js +16 -3
- package/dist/controls/SkiaCheckbox.d.ts +0 -1
- package/dist/controls/SkiaCheckbox.js +0 -6
- package/dist/controls/SkiaDrawer.d.ts +2 -0
- package/dist/controls/SkiaDrawer.js +2 -0
- package/dist/controls/SkiaEditor.d.ts +12 -1
- package/dist/controls/SkiaEditor.js +46 -19
- package/dist/controls/SkiaGif.d.ts +3 -0
- package/dist/controls/SkiaGif.js +12 -2
- package/dist/controls/SkiaImage.js +15 -10
- package/dist/controls/SkiaLabel.d.ts +54 -3
- package/dist/controls/SkiaLabel.js +235 -52
- package/dist/controls/SkiaLabelFps.d.ts +16 -0
- package/dist/controls/SkiaLabelFps.js +41 -0
- package/dist/controls/SkiaLayout.d.ts +64 -7
- package/dist/controls/SkiaLayout.js +316 -19
- package/dist/controls/SkiaLottie.js +4 -2
- package/dist/controls/SkiaProgress.d.ts +2 -1
- package/dist/controls/SkiaProgress.js +6 -3
- package/dist/controls/SkiaRadioButton.js +4 -3
- package/dist/controls/SkiaScroll.d.ts +35 -5
- package/dist/controls/SkiaScroll.js +233 -23
- package/dist/controls/SkiaScrollBar.d.ts +27 -0
- package/dist/controls/SkiaScrollBar.js +51 -2
- package/dist/controls/SkiaShaderCarousel.d.ts +5 -0
- package/dist/controls/SkiaShaderCarousel.js +5 -0
- package/dist/controls/SkiaShape.d.ts +13 -2
- package/dist/controls/SkiaShape.js +50 -10
- package/dist/controls/SkiaSlider.d.ts +20 -2
- package/dist/controls/SkiaSlider.js +59 -5
- package/dist/controls/SkiaSprite.d.ts +1 -0
- package/dist/controls/SkiaSprite.js +4 -0
- package/dist/controls/SkiaSvg.js +1 -1
- package/dist/controls/SkiaSwitch.d.ts +0 -1
- package/dist/controls/SkiaSwitch.js +0 -6
- package/dist/controls/SkiaToggle.d.ts +25 -5
- package/dist/controls/SkiaToggle.js +58 -5
- package/dist/controls/SnappingLayout.d.ts +12 -1
- package/dist/controls/SnappingLayout.js +19 -1
- package/dist/core/Accessibility.d.ts +80 -0
- package/dist/core/Accessibility.js +365 -8
- package/dist/core/Animators.d.ts +37 -1
- package/dist/core/Animators.js +102 -9
- package/dist/core/Canvas.d.ts +108 -7
- package/dist/core/Canvas.js +363 -35
- package/dist/core/Easing.d.ts +12 -1
- package/dist/core/Easing.js +25 -1
- package/dist/core/Gestures.d.ts +48 -2
- package/dist/core/Gestures.js +95 -3
- package/dist/core/Hover.d.ts +47 -0
- package/dist/core/Hover.js +134 -0
- package/dist/core/KeyboardManager.d.ts +13 -0
- package/dist/core/KeyboardManager.js +18 -0
- package/dist/core/ScrollAnimators.js +9 -0
- package/dist/core/SkiaControl.d.ts +216 -7
- package/dist/core/SkiaControl.js +516 -59
- package/dist/core/SkiaEffect.d.ts +17 -0
- package/dist/core/SkiaEffect.js +43 -8
- package/dist/core/SkiaImageManager.js +1 -1
- package/dist/core/Styles.d.ts +51 -0
- package/dist/core/Styles.js +92 -0
- package/dist/core/Super.d.ts +17 -1
- package/dist/core/Super.js +35 -4
- package/dist/core/ViewsAdapter.d.ts +7 -2
- package/dist/core/ViewsAdapter.js +10 -3
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/react/SkiaShell.js +33 -14
- package/dist/react/index.d.ts +15 -4
- package/dist/react/index.js +102 -15
- package/dist/react/reconciler.js +34 -12
- package/dist/vite/index.d.ts +9 -0
- package/dist/vite/index.js +36 -3
- package/package.json +33 -3
|
@@ -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#). */
|
|
@@ -43,12 +45,17 @@ export class SkiaLabel extends SkiaControl {
|
|
|
43
45
|
this[key] = v;
|
|
44
46
|
this.Update();
|
|
45
47
|
} }
|
|
48
|
+
/** A property that changes only the look (a color): no layout change, see UpdateDraw. */
|
|
49
|
+
SetLook(key, v) { if (this[key] !== v) {
|
|
50
|
+
this[key] = v;
|
|
51
|
+
this.UpdateDraw();
|
|
52
|
+
} }
|
|
46
53
|
get Text() { return this.text; }
|
|
47
54
|
set Text(v) { this.Set("text", v); }
|
|
48
55
|
get FontSize() { return this.fontSize; }
|
|
49
56
|
set FontSize(v) { this.Set("fontSize", v); }
|
|
50
57
|
get TextColor() { return this.textColor; }
|
|
51
|
-
set TextColor(v) { this.
|
|
58
|
+
set TextColor(v) { this.SetLook("textColor", v); }
|
|
52
59
|
get FontFamily() { return this.fontFamily; }
|
|
53
60
|
set FontFamily(v) { this.Set("fontFamily", v); }
|
|
54
61
|
/**
|
|
@@ -74,6 +81,18 @@ export class SkiaLabel extends SkiaControl {
|
|
|
74
81
|
/** Multiplier applied to the natural line height (ascent + descent). */
|
|
75
82
|
get LineHeight() { return this.lineHeight; }
|
|
76
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); }
|
|
77
96
|
get TextTransform() { return this.textTransform; }
|
|
78
97
|
set TextTransform(v) { this.Set("textTransform", v); }
|
|
79
98
|
get Padding() { return this.padding; }
|
|
@@ -85,8 +104,14 @@ export class SkiaLabel extends SkiaControl {
|
|
|
85
104
|
InsertSubView(index, control) {
|
|
86
105
|
if (!(control instanceof TextSpan))
|
|
87
106
|
throw new Error("DrawnUi: SkiaLabel children must be <TextSpan>");
|
|
107
|
+
const at = control.Parent === this ? this.Spans.indexOf(control) : -1; // already a span: a move, never a 2nd copy
|
|
108
|
+
if (at >= 0) {
|
|
109
|
+
this.Spans.splice(at, 1);
|
|
110
|
+
if (at < index)
|
|
111
|
+
index--;
|
|
112
|
+
}
|
|
88
113
|
control.Parent = this;
|
|
89
|
-
this.Spans.splice(index, 0, control);
|
|
114
|
+
this.Spans.splice(Math.min(Math.max(0, index), this.Spans.length), 0, control);
|
|
90
115
|
this.Update();
|
|
91
116
|
}
|
|
92
117
|
RemoveSubView(control) {
|
|
@@ -102,6 +127,8 @@ export class SkiaLabel extends SkiaControl {
|
|
|
102
127
|
mainFonts;
|
|
103
128
|
fontsCache = new Map();
|
|
104
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;
|
|
105
132
|
ResolveFonts(family, weight, italic, sizePx) {
|
|
106
133
|
const key = `${family}|${this.fontFamilyFallback}|${weight}|${italic}|${sizePx}`;
|
|
107
134
|
let f = this.fontsCache.get(key);
|
|
@@ -131,12 +158,24 @@ export class SkiaLabel extends SkiaControl {
|
|
|
131
158
|
const weight = span.FontWeight > 0 ? span.FontWeight : span.IsBold ? 700 : this.fontWeight > 0 ? this.fontWeight : bold ? 700 : 0;
|
|
132
159
|
return this.ResolveFonts(span.FontFamily ?? this.fontFamily, weight, italic, (span.FontSize ?? this.fontSize) * scale);
|
|
133
160
|
}
|
|
134
|
-
|
|
161
|
+
Advance(font, text) {
|
|
135
162
|
let w = 0;
|
|
136
163
|
for (const adv of font.getGlyphWidths(font.getGlyphIDs(text)))
|
|
137
|
-
w += adv;
|
|
164
|
+
w += adv + this.glyphSpacing;
|
|
138
165
|
return w;
|
|
139
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
|
+
}
|
|
140
179
|
/**
|
|
141
180
|
* Splits text into runs by glyph availability: the main font, or the first fallback that has a glyph where the
|
|
142
181
|
* main font has glyph 0. Spaces always stay on the main font (fallback faces often carry very wide spaces).
|
|
@@ -150,8 +189,13 @@ export class SkiaLabel extends SkiaControl {
|
|
|
150
189
|
const main = fonts.Main;
|
|
151
190
|
const fbs = fonts.Fallbacks;
|
|
152
191
|
cached = [];
|
|
192
|
+
text = SkiaLabel.DropIgnorables(text, main, fbs);
|
|
193
|
+
if (text.length === 0) {
|
|
194
|
+
this.runCache.set(cacheKey, cached);
|
|
195
|
+
return cached;
|
|
196
|
+
}
|
|
153
197
|
if (fbs.length === 0) {
|
|
154
|
-
cached.push({ Text: text, Font: main, Width:
|
|
198
|
+
cached.push({ Text: text, Font: main, Width: this.Advance(main, text), Fonts: fonts });
|
|
155
199
|
}
|
|
156
200
|
else {
|
|
157
201
|
const cps = Array.from(text);
|
|
@@ -170,13 +214,13 @@ export class SkiaLabel extends SkiaControl {
|
|
|
170
214
|
const f = fontFor(i);
|
|
171
215
|
if (f !== current) {
|
|
172
216
|
const t = cps.slice(start, i).join("");
|
|
173
|
-
cached.push({ Text: t, Font: current, Width:
|
|
217
|
+
cached.push({ Text: t, Font: current, Width: this.Advance(current, t), Fonts: fonts });
|
|
174
218
|
start = i;
|
|
175
219
|
current = f;
|
|
176
220
|
}
|
|
177
221
|
}
|
|
178
222
|
const t = cps.slice(start).join("");
|
|
179
|
-
cached.push({ Text: t, Font: current, Width:
|
|
223
|
+
cached.push({ Text: t, Font: current, Width: this.Advance(current, t), Fonts: fonts });
|
|
180
224
|
}
|
|
181
225
|
this.runCache.set(cacheKey, cached);
|
|
182
226
|
}
|
|
@@ -184,6 +228,76 @@ export class SkiaLabel extends SkiaControl {
|
|
|
184
228
|
}
|
|
185
229
|
Width(runs) { let w = 0; for (const r of runs)
|
|
186
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
|
+
}
|
|
187
301
|
Transform(text) {
|
|
188
302
|
switch (this.textTransform) {
|
|
189
303
|
case "Uppercase": return text.toUpperCase();
|
|
@@ -199,10 +313,16 @@ export class SkiaLabel extends SkiaControl {
|
|
|
199
313
|
Tokenize(scale) {
|
|
200
314
|
const paragraphs = [[]];
|
|
201
315
|
const add = (text, fonts, span) => {
|
|
202
|
-
|
|
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);
|
|
203
319
|
for (let p = 0; p < parts.length; p++) {
|
|
204
|
-
if (p > 0)
|
|
205
|
-
|
|
320
|
+
if (p > 0) {
|
|
321
|
+
const next = [];
|
|
322
|
+
if (seps[p - 1] === "\u2028")
|
|
323
|
+
next.LineBreak = true;
|
|
324
|
+
paragraphs.push(next);
|
|
325
|
+
}
|
|
206
326
|
const para = paragraphs[paragraphs.length - 1];
|
|
207
327
|
const words = parts[p].split(" ");
|
|
208
328
|
for (let w = 0; w < words.length; w++) {
|
|
@@ -248,13 +368,21 @@ export class SkiaLabel extends SkiaControl {
|
|
|
248
368
|
line.Descent = r.Fonts.Descent;
|
|
249
369
|
}
|
|
250
370
|
}
|
|
251
|
-
/**
|
|
252
|
-
|
|
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) {
|
|
253
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;
|
|
254
380
|
const out = [];
|
|
255
381
|
const empty = (fonts) => { const l = this.NewLine(); l.Ascent = fonts.Ascent; l.Descent = fonts.Descent; return l; };
|
|
256
382
|
for (const para of this.Tokenize(scale)) {
|
|
257
383
|
let line = this.NewLine();
|
|
384
|
+
if (out.length > 0 && !para.LineBreak)
|
|
385
|
+
line.Para = true;
|
|
258
386
|
let lastFonts = this.mainFonts;
|
|
259
387
|
for (let i = 0; i < para.length; i++) {
|
|
260
388
|
const tok = para[i];
|
|
@@ -265,46 +393,71 @@ export class SkiaLabel extends SkiaControl {
|
|
|
265
393
|
i++;
|
|
266
394
|
word = word.concat(this.Segment(para[i].Text, para[i].Fonts, para[i].Span));
|
|
267
395
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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);
|
|
291
430
|
}
|
|
292
|
-
this.Append(line, run);
|
|
293
431
|
}
|
|
432
|
+
break;
|
|
294
433
|
}
|
|
295
434
|
}
|
|
296
435
|
out.push(line.Runs.length > 0 ? line : empty(lastFonts));
|
|
297
436
|
}
|
|
298
|
-
|
|
299
|
-
|
|
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;
|
|
300
453
|
const truncates = this.lineBreakMode === "TailTruncation" || this.lineBreakMode === "HeadTruncation" || this.lineBreakMode === "MiddleTruncation";
|
|
301
454
|
if (truncates) {
|
|
302
|
-
const last = out[
|
|
455
|
+
const last = out[kept - 1];
|
|
303
456
|
const tailRun = last.Runs[last.Runs.length - 1];
|
|
304
457
|
const fonts = tailRun ? tailRun.Fonts : this.mainFonts;
|
|
305
458
|
const ell = this.Segment("…", fonts, tailRun?.Span);
|
|
306
459
|
const ellW = this.Width(ell);
|
|
307
|
-
while (last.Runs.length > 0 && last.Width + ellW > maxWidth) {
|
|
460
|
+
while (last.Runs.length > 0 && last.Width + ellW - sp > maxWidth) {
|
|
308
461
|
const r = last.Runs[last.Runs.length - 1];
|
|
309
462
|
const cps = Array.from(r.Text);
|
|
310
463
|
cps.pop();
|
|
@@ -316,27 +469,39 @@ export class SkiaLabel extends SkiaControl {
|
|
|
316
469
|
continue;
|
|
317
470
|
}
|
|
318
471
|
r.Text = cps.join("");
|
|
319
|
-
r.Width =
|
|
472
|
+
r.Width = this.Advance(r.Font, r.Text);
|
|
320
473
|
last.Width += r.Width;
|
|
321
474
|
}
|
|
322
475
|
this.Append(last, ell);
|
|
323
476
|
}
|
|
324
477
|
}
|
|
478
|
+
if (sp !== 0)
|
|
479
|
+
for (const l of out)
|
|
480
|
+
if (l.Runs.length > 0)
|
|
481
|
+
l.Width -= sp;
|
|
325
482
|
return out;
|
|
326
483
|
}
|
|
327
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; }
|
|
328
487
|
BlockHeight() {
|
|
329
488
|
let h = 0;
|
|
330
489
|
for (let i = 0; i < this.lines.length; i++) {
|
|
331
490
|
const lh = this.LineHeightPx(this.lines[i]);
|
|
332
|
-
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]);
|
|
333
492
|
}
|
|
334
493
|
return h;
|
|
335
494
|
}
|
|
336
|
-
MeasureAbsolute(widthConstraint,
|
|
495
|
+
MeasureAbsolute(widthConstraint, heightConstraint, scale) {
|
|
337
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
|
+
}
|
|
338
502
|
const px = this.padding.HorizontalThickness * scale, py = this.padding.VerticalThickness * scale;
|
|
339
|
-
|
|
503
|
+
const extraH = this.EffectsExtra(scale).H;
|
|
504
|
+
this.lines = this.text || this.Spans.length > 0 ? this.LayoutLines(widthConstraint - px, scale, heightConstraint - py - extraH) : [];
|
|
340
505
|
let width = 0;
|
|
341
506
|
for (const l of this.lines)
|
|
342
507
|
width = Math.max(width, l.Width);
|
|
@@ -367,6 +532,7 @@ export class SkiaLabel extends SkiaControl {
|
|
|
367
532
|
y += extra.Stroke / 2;
|
|
368
533
|
const out = [];
|
|
369
534
|
for (const line of this.lines) {
|
|
535
|
+
y += this.ParagraphSpace(line);
|
|
370
536
|
const lh = this.LineHeightPx(line);
|
|
371
537
|
const lineW = line.Width + extra.W;
|
|
372
538
|
let x = left;
|
|
@@ -419,7 +585,7 @@ export class SkiaLabel extends SkiaControl {
|
|
|
419
585
|
cps.forEach((cp, k) => {
|
|
420
586
|
const w = widths[k] ?? 0;
|
|
421
587
|
out.push({ Index: index, Length: cp.length, Line: li, Left: x, Top: g.y, Width: w, Height: g.h });
|
|
422
|
-
x += w;
|
|
588
|
+
x += w + this.glyphSpacing;
|
|
423
589
|
index += cp.length;
|
|
424
590
|
});
|
|
425
591
|
}
|
|
@@ -501,9 +667,25 @@ export class SkiaLabel extends SkiaControl {
|
|
|
501
667
|
return paint;
|
|
502
668
|
};
|
|
503
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
|
+
};
|
|
504
685
|
const gradient = this.FillGradient;
|
|
505
686
|
const textRect = new SKRect(left, top, right, bottom);
|
|
506
687
|
for (const line of this.lines) {
|
|
688
|
+
y += this.ParagraphSpace(line);
|
|
507
689
|
const lh = this.LineHeightPx(line);
|
|
508
690
|
const lineW = line.Width + extra.W;
|
|
509
691
|
let x = left;
|
|
@@ -519,22 +701,23 @@ export class SkiaLabel extends SkiaControl {
|
|
|
519
701
|
this.SetupGradient(strokePaint, this.StrokeGradient, gradientRect ?? new SKRect(x, y, x + line.Width, y + lh));
|
|
520
702
|
for (const run of line.Runs) {
|
|
521
703
|
const span = run.Span;
|
|
704
|
+
const drawnW = run.Width - sp; // the gap after the run's last glyph is not part of it
|
|
522
705
|
if (span) {
|
|
523
|
-
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));
|
|
524
707
|
if (span.BackgroundColor)
|
|
525
|
-
canvas.drawRect(CK.LTRBRect(x, y, x +
|
|
708
|
+
canvas.drawRect(CK.LTRBRect(x, y, x + drawnW, y + lh), paintFor(span.BackgroundColor));
|
|
526
709
|
}
|
|
527
710
|
const color = span?.TextColor ?? this.textColor;
|
|
528
711
|
if (run.Text) {
|
|
529
712
|
// C# DrawText order: drop shadow, stroke, fill
|
|
530
713
|
if (shadowPaint)
|
|
531
|
-
|
|
714
|
+
drawRun(run, x + shadowDx, baseline + shadowDy, shadowPaint);
|
|
532
715
|
if (strokePaint)
|
|
533
|
-
|
|
716
|
+
drawRun(run, x, baseline, strokePaint);
|
|
534
717
|
const paint = paintFor(color);
|
|
535
718
|
if (gradient && gradientRect)
|
|
536
719
|
this.SetupGradient(paint, gradient, gradientRect);
|
|
537
|
-
|
|
720
|
+
drawRun(run, x, baseline, paint);
|
|
538
721
|
}
|
|
539
722
|
if (span?.HasDecorations) {
|
|
540
723
|
// same geometry as C# DrawSpanDecorations; CanvasKit exposes no underline/strikeout/x-height metrics,
|
|
@@ -542,11 +725,11 @@ export class SkiaLabel extends SkiaControl {
|
|
|
542
725
|
if (span.Underline && span.UnderlineWidth !== 0) {
|
|
543
726
|
const w = span.UnderlineWidth > 0 ? span.UnderlineWidth * scale : -span.UnderlineWidth;
|
|
544
727
|
const yl = Math.round(baseline + scale);
|
|
545
|
-
canvas.drawLine(x, yl, x +
|
|
728
|
+
canvas.drawLine(x, yl, x + drawnW, yl, paintFor(color, w));
|
|
546
729
|
}
|
|
547
730
|
if (span.Strikeout) {
|
|
548
731
|
const yl = Math.round(baseline - (run.Fonts.SizePx * 0.52) / 2);
|
|
549
|
-
canvas.drawLine(x, yl, x +
|
|
732
|
+
canvas.drawLine(x, yl, x + drawnW, yl, paintFor(span.StrikeoutColor, span.StrikeoutWidth * scale));
|
|
550
733
|
}
|
|
551
734
|
}
|
|
552
735
|
x += run.Width;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { DrawingContext } from "../core/SkiaControl";
|
|
2
|
+
import type { SKRect } from "../core/Types";
|
|
3
|
+
import { SkiaLabel } from "./SkiaLabel";
|
|
4
|
+
/**
|
|
5
|
+
* Mirrors DrawnUi SkiaLabelFps: a label showing the canvas FPS, drawn above its siblings, input-transparent, never
|
|
6
|
+
* re-laying out its parent. Divergence: C# sets Text on every draw, and each new value asks for another frame, so
|
|
7
|
+
* the counter keeps an idle canvas redrawing. Here the value changes only on frames that running animators already
|
|
8
|
+
* request (scroll inertia, transitions, Lottie, game loops): the counter never asks for a frame itself, and an idle
|
|
9
|
+
* canvas keeps the last value measured. Not ported: ForceRefresh, Format, MonoForDigits.
|
|
10
|
+
*/
|
|
11
|
+
export declare class SkiaLabelFps extends SkiaLabel {
|
|
12
|
+
constructor();
|
|
13
|
+
private arrangedIn?;
|
|
14
|
+
Arrange(destination: SKRect, widthRequest: number, heightRequest: number, scale: number): void;
|
|
15
|
+
Render(ctx: DrawingContext): void;
|
|
16
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Aria } from "../core/Accessibility";
|
|
2
|
+
import { SkiaLabel } from "./SkiaLabel";
|
|
3
|
+
/**
|
|
4
|
+
* Mirrors DrawnUi SkiaLabelFps: a label showing the canvas FPS, drawn above its siblings, input-transparent, never
|
|
5
|
+
* re-laying out its parent. Divergence: C# sets Text on every draw, and each new value asks for another frame, so
|
|
6
|
+
* the counter keeps an idle canvas redrawing. Here the value changes only on frames that running animators already
|
|
7
|
+
* request (scroll inertia, transitions, Lottie, game loops): the counter never asks for a frame itself, and an idle
|
|
8
|
+
* canvas keeps the last value measured. Not ported: ForceRefresh, Format, MonoForDigits.
|
|
9
|
+
*/
|
|
10
|
+
export class SkiaLabelFps extends SkiaLabel {
|
|
11
|
+
constructor() {
|
|
12
|
+
super();
|
|
13
|
+
this.IsParentIndependent = true;
|
|
14
|
+
this.AccessibilityRole = Aria.RolePresentation;
|
|
15
|
+
this.Tag = "FPS";
|
|
16
|
+
this.Text = "FPS --";
|
|
17
|
+
this.MaxLines = 1;
|
|
18
|
+
this.BackgroundColor = "#000000";
|
|
19
|
+
this.TextColor = "#32CD32"; // LimeGreen
|
|
20
|
+
this.InputTransparent = true;
|
|
21
|
+
this.ZIndex = Number.MAX_SAFE_INTEGER;
|
|
22
|
+
}
|
|
23
|
+
arrangedIn;
|
|
24
|
+
Arrange(destination, widthRequest, heightRequest, scale) {
|
|
25
|
+
this.arrangedIn = destination;
|
|
26
|
+
super.Arrange(destination, widthRequest, heightRequest, scale);
|
|
27
|
+
}
|
|
28
|
+
Render(ctx) {
|
|
29
|
+
const view = this.Superview;
|
|
30
|
+
// the frame this asks for is requested anyway by the running animators
|
|
31
|
+
if (view && view.AnimatingControls.size > 0)
|
|
32
|
+
this.Text = `FPS ${view.FPS.toFixed(1).padStart(4, "0")}`;
|
|
33
|
+
// parent-independent: the parent does not re-lay out for it, so it re-measures itself here (as SkiaScroll does for its bars)
|
|
34
|
+
const d = this.arrangedIn;
|
|
35
|
+
if (this.NeedMeasure && d) {
|
|
36
|
+
this.Measure(d.Width, d.Height, ctx.Scale);
|
|
37
|
+
this.Arrange(d, this.WidthRequest, this.HeightRequest, ctx.Scale);
|
|
38
|
+
}
|
|
39
|
+
super.Render(ctx);
|
|
40
|
+
}
|
|
41
|
+
}
|