chat-layout 1.2.0-1 → 1.2.0-3
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 +45 -15
- package/example/chat.ts +104 -12
- package/example/test.ts +4 -2
- package/index.d.mts +33 -6
- package/index.mjs +437 -117
- package/index.mjs.map +1 -1
- package/package.json +4 -1
package/index.mjs
CHANGED
|
@@ -2386,63 +2386,65 @@ function measureRichFragmentShift(ctx, font) {
|
|
|
2386
2386
|
}
|
|
2387
2387
|
function materializeRichFragments(ctx, spans, defaultColor, atoms) {
|
|
2388
2388
|
const fragments = [];
|
|
2389
|
-
let
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
}
|
|
2396
|
-
const span = spans[atom.itemIndex];
|
|
2397
|
-
const font = span?.font ?? atom.font;
|
|
2398
|
-
const color = span?.color ?? defaultColor;
|
|
2399
|
-
const previous = fragments[fragments.length - 1];
|
|
2400
|
-
if (previous != null && previous.itemIndex === atom.itemIndex && previous.font === font && pendingGapBefore === 0) {
|
|
2401
|
-
previous.text += atom.text;
|
|
2402
|
-
previous.occupiedWidth += occupiedWidth;
|
|
2403
|
-
continue;
|
|
2404
|
-
}
|
|
2405
|
-
fragments.push({
|
|
2406
|
-
itemIndex: atom.itemIndex,
|
|
2407
|
-
text: atom.text,
|
|
2408
|
-
font,
|
|
2409
|
-
color,
|
|
2410
|
-
gapBefore: pendingGapBefore,
|
|
2411
|
-
occupiedWidth,
|
|
2412
|
-
shift: measureRichFragmentShift(ctx, font)
|
|
2413
|
-
});
|
|
2414
|
-
pendingGapBefore = 0;
|
|
2415
|
-
}
|
|
2389
|
+
let pendingGap = {
|
|
2390
|
+
gapBefore: 0,
|
|
2391
|
+
gapAtomCount: 0,
|
|
2392
|
+
gapSpaceCount: 0
|
|
2393
|
+
};
|
|
2394
|
+
for (const atom of atoms) pendingGap = appendRichFragment(ctx, spans, defaultColor, fragments, atom, pendingGap);
|
|
2416
2395
|
return fragments;
|
|
2417
2396
|
}
|
|
2418
|
-
function appendRichFragment(ctx, spans, defaultColor, fragments, atom,
|
|
2397
|
+
function appendRichFragment(ctx, spans, defaultColor, fragments, atom, pendingGap) {
|
|
2419
2398
|
const occupiedWidth = atom.width + atom.extraWidthAfter;
|
|
2420
|
-
if (atom.kind === "space" && !atom.preservesLineEnd && atom.atomicGroupId == null) return
|
|
2399
|
+
if (atom.kind === "space" && !atom.preservesLineEnd && atom.atomicGroupId == null) return {
|
|
2400
|
+
gapBefore: pendingGap.gapBefore + occupiedWidth,
|
|
2401
|
+
gapAtomCount: pendingGap.gapAtomCount + 1,
|
|
2402
|
+
gapSpaceCount: pendingGap.gapSpaceCount + 1
|
|
2403
|
+
};
|
|
2421
2404
|
const span = spans[atom.itemIndex];
|
|
2422
2405
|
const font = span?.font ?? atom.font;
|
|
2423
2406
|
const color = span?.color ?? defaultColor;
|
|
2424
2407
|
const previous = fragments[fragments.length - 1];
|
|
2425
|
-
|
|
2408
|
+
const spaceCount = atom.kind === "space" ? 1 : 0;
|
|
2409
|
+
if (previous != null && previous.itemIndex === atom.itemIndex && previous.font === font && pendingGap.gapBefore === 0) {
|
|
2426
2410
|
previous.text += atom.text;
|
|
2427
2411
|
previous.occupiedWidth += occupiedWidth;
|
|
2428
|
-
|
|
2412
|
+
previous.atomCount += 1;
|
|
2413
|
+
previous.spaceCount += spaceCount;
|
|
2414
|
+
return {
|
|
2415
|
+
gapBefore: 0,
|
|
2416
|
+
gapAtomCount: 0,
|
|
2417
|
+
gapSpaceCount: 0
|
|
2418
|
+
};
|
|
2429
2419
|
}
|
|
2430
2420
|
fragments.push({
|
|
2431
2421
|
itemIndex: atom.itemIndex,
|
|
2432
2422
|
text: atom.text,
|
|
2433
2423
|
font,
|
|
2434
2424
|
color,
|
|
2435
|
-
gapBefore:
|
|
2425
|
+
gapBefore: pendingGap.gapBefore,
|
|
2426
|
+
gapAtomCount: pendingGap.gapAtomCount,
|
|
2427
|
+
gapSpaceCount: pendingGap.gapSpaceCount,
|
|
2436
2428
|
occupiedWidth,
|
|
2429
|
+
atomCount: 1,
|
|
2430
|
+
spaceCount,
|
|
2437
2431
|
shift: measureRichFragmentShift(ctx, font)
|
|
2438
2432
|
});
|
|
2439
|
-
return
|
|
2433
|
+
return {
|
|
2434
|
+
gapBefore: 0,
|
|
2435
|
+
gapAtomCount: 0,
|
|
2436
|
+
gapSpaceCount: 0
|
|
2437
|
+
};
|
|
2440
2438
|
}
|
|
2441
2439
|
function materializeRichFragmentsInRange(ctx, spans, defaultColor, prepared, start, end) {
|
|
2442
2440
|
const fragments = [];
|
|
2443
|
-
let
|
|
2441
|
+
let pendingGap = {
|
|
2442
|
+
gapBefore: 0,
|
|
2443
|
+
gapAtomCount: 0,
|
|
2444
|
+
gapSpaceCount: 0
|
|
2445
|
+
};
|
|
2444
2446
|
forEachAtomInRange(prepared, start, end, (atom) => {
|
|
2445
|
-
|
|
2447
|
+
pendingGap = appendRichFragment(ctx, spans, defaultColor, fragments, atom, pendingGap);
|
|
2446
2448
|
});
|
|
2447
2449
|
return fragments;
|
|
2448
2450
|
}
|
|
@@ -2488,7 +2490,11 @@ function createRichEllipsisFragment(ctx, font, color) {
|
|
|
2488
2490
|
font,
|
|
2489
2491
|
color,
|
|
2490
2492
|
gapBefore: 0,
|
|
2493
|
+
gapAtomCount: 0,
|
|
2494
|
+
gapSpaceCount: 0,
|
|
2491
2495
|
occupiedWidth: measureEllipsisWidth(ctx),
|
|
2496
|
+
atomCount: 1,
|
|
2497
|
+
spaceCount: 0,
|
|
2492
2498
|
shift: measureFontShift(ctx)
|
|
2493
2499
|
}));
|
|
2494
2500
|
}
|
|
@@ -2574,13 +2580,17 @@ function layoutRichEndEllipsisFromCursor(ctx, spans, defaultFont, defaultColor,
|
|
|
2574
2580
|
const { prefixCount, width } = selection;
|
|
2575
2581
|
const fragments = [];
|
|
2576
2582
|
let atomIndex = 0;
|
|
2577
|
-
let
|
|
2583
|
+
let pendingGap = {
|
|
2584
|
+
gapBefore: 0,
|
|
2585
|
+
gapAtomCount: 0,
|
|
2586
|
+
gapSpaceCount: 0
|
|
2587
|
+
};
|
|
2578
2588
|
let lastVisibleAtom;
|
|
2579
2589
|
let lastAtom;
|
|
2580
2590
|
forEachAtomFromCursorToEnd(prepared, start, (atom) => {
|
|
2581
2591
|
lastAtom = atom;
|
|
2582
2592
|
if (atomIndex < prefixCount) {
|
|
2583
|
-
|
|
2593
|
+
pendingGap = appendRichFragment(ctx, spans, defaultColor, fragments, atom, pendingGap);
|
|
2584
2594
|
lastVisibleAtom = atom;
|
|
2585
2595
|
}
|
|
2586
2596
|
atomIndex += 1;
|
|
@@ -2749,6 +2759,154 @@ function layoutRichTextWithOverflow(ctx, spans, maxWidth, defaultFont, defaultCo
|
|
|
2749
2759
|
};
|
|
2750
2760
|
}
|
|
2751
2761
|
//#endregion
|
|
2762
|
+
//#region src/text/justify.ts
|
|
2763
|
+
let _justifySupported;
|
|
2764
|
+
function isJustifySupported(ctx) {
|
|
2765
|
+
if (_justifySupported !== void 0) return _justifySupported;
|
|
2766
|
+
_justifySupported = typeof ctx.wordSpacing === "string" && typeof ctx.letterSpacing === "string";
|
|
2767
|
+
return _justifySupported;
|
|
2768
|
+
}
|
|
2769
|
+
function resolveJustifyMode(justify) {
|
|
2770
|
+
if (justify === true) return "inter-word";
|
|
2771
|
+
if (justify === "inter-word" || justify === "inter-character") return justify;
|
|
2772
|
+
return null;
|
|
2773
|
+
}
|
|
2774
|
+
const HYBRID_WORD_SHARE_CANDIDATES = [
|
|
2775
|
+
.15,
|
|
2776
|
+
.2,
|
|
2777
|
+
.25,
|
|
2778
|
+
.3,
|
|
2779
|
+
.35,
|
|
2780
|
+
.4,
|
|
2781
|
+
.45,
|
|
2782
|
+
.5,
|
|
2783
|
+
.55,
|
|
2784
|
+
.6,
|
|
2785
|
+
.65,
|
|
2786
|
+
.7,
|
|
2787
|
+
.75,
|
|
2788
|
+
.8,
|
|
2789
|
+
.85,
|
|
2790
|
+
1,
|
|
2791
|
+
0
|
|
2792
|
+
];
|
|
2793
|
+
const PUNCTUATION_OR_SYMBOL_PATTERN = /^[\p{P}\p{S}]$/u;
|
|
2794
|
+
const JUSTIFY_SCORE_EPSILON = 1e-9;
|
|
2795
|
+
function analyzeLineForJustify(prepared, line) {
|
|
2796
|
+
let wordGapCount = 0;
|
|
2797
|
+
let wordCount = 0;
|
|
2798
|
+
let renderAtomCount = 0;
|
|
2799
|
+
let spaceCount = 0;
|
|
2800
|
+
let nonSpaceCount = 0;
|
|
2801
|
+
let cjkCount = 0;
|
|
2802
|
+
let latinLikeCount = 0;
|
|
2803
|
+
let punctuationCount = 0;
|
|
2804
|
+
let nonSpaceWidth = 0;
|
|
2805
|
+
let insideWord = false;
|
|
2806
|
+
forEachAtomInRange(prepared, line.start, line.end, (atom) => {
|
|
2807
|
+
if (atom.kind === "space" && !atom.preservesLineEnd && atom.atomicGroupId == null) wordGapCount++;
|
|
2808
|
+
renderAtomCount++;
|
|
2809
|
+
if (atom.kind === "space") {
|
|
2810
|
+
spaceCount++;
|
|
2811
|
+
insideWord = false;
|
|
2812
|
+
return;
|
|
2813
|
+
}
|
|
2814
|
+
nonSpaceCount++;
|
|
2815
|
+
nonSpaceWidth += atom.width + atom.extraWidthAfter;
|
|
2816
|
+
if (!insideWord) {
|
|
2817
|
+
wordCount++;
|
|
2818
|
+
insideWord = true;
|
|
2819
|
+
}
|
|
2820
|
+
if (isCJK(atom.text)) {
|
|
2821
|
+
cjkCount++;
|
|
2822
|
+
return;
|
|
2823
|
+
}
|
|
2824
|
+
if (PUNCTUATION_OR_SYMBOL_PATTERN.test(atom.text)) {
|
|
2825
|
+
punctuationCount++;
|
|
2826
|
+
return;
|
|
2827
|
+
}
|
|
2828
|
+
latinLikeCount++;
|
|
2829
|
+
});
|
|
2830
|
+
return {
|
|
2831
|
+
wordGapCount,
|
|
2832
|
+
wordCount,
|
|
2833
|
+
renderAtomCount,
|
|
2834
|
+
letterGapCount: Math.max(renderAtomCount - 1, 0),
|
|
2835
|
+
spaceCount,
|
|
2836
|
+
nonSpaceCount,
|
|
2837
|
+
cjkCount,
|
|
2838
|
+
latinLikeCount,
|
|
2839
|
+
punctuationCount,
|
|
2840
|
+
lineWidth: line.width,
|
|
2841
|
+
nonSpaceWidth
|
|
2842
|
+
};
|
|
2843
|
+
}
|
|
2844
|
+
function getAverageWordWidth(info) {
|
|
2845
|
+
return info.wordCount > 0 ? info.nonSpaceWidth / info.wordCount : info.lineWidth;
|
|
2846
|
+
}
|
|
2847
|
+
function getAverageCharWidth(info) {
|
|
2848
|
+
return info.renderAtomCount > 0 ? info.lineWidth / info.renderAtomCount : info.lineWidth;
|
|
2849
|
+
}
|
|
2850
|
+
function resolvePerGapSpacing(totalSpace, gapCount) {
|
|
2851
|
+
if (totalSpace === 0) return 0;
|
|
2852
|
+
if (gapCount <= 0) return null;
|
|
2853
|
+
return totalSpace / gapCount;
|
|
2854
|
+
}
|
|
2855
|
+
function exceedsThreshold(perGap, averageWidth, threshold) {
|
|
2856
|
+
if (!Number.isFinite(threshold)) return false;
|
|
2857
|
+
return perGap > threshold * averageWidth;
|
|
2858
|
+
}
|
|
2859
|
+
function createJustifySpacing(wordSpacingPx, letterSpacingPx) {
|
|
2860
|
+
return {
|
|
2861
|
+
wordSpacing: `${wordSpacingPx}px`,
|
|
2862
|
+
letterSpacing: `${letterSpacingPx}px`,
|
|
2863
|
+
wordSpacingPx,
|
|
2864
|
+
letterSpacingPx
|
|
2865
|
+
};
|
|
2866
|
+
}
|
|
2867
|
+
function computeJustifySpacing(lineWidth, maxWidth, info, mode, threshold = Number.POSITIVE_INFINITY) {
|
|
2868
|
+
const extraSpace = maxWidth - lineWidth;
|
|
2869
|
+
if (extraSpace <= 0 || mode == null) return null;
|
|
2870
|
+
if (mode === "inter-word" && info.wordGapCount > 0) {
|
|
2871
|
+
const perGap = extraSpace / info.wordGapCount;
|
|
2872
|
+
if (exceedsThreshold(perGap, Math.max(getAverageWordWidth(info), Number.EPSILON), threshold)) return null;
|
|
2873
|
+
return createJustifySpacing(perGap, 0);
|
|
2874
|
+
}
|
|
2875
|
+
if (mode !== "inter-character" || info.renderAtomCount === 0) return null;
|
|
2876
|
+
const avgCharWidth = Math.max(getAverageCharWidth(info), Number.EPSILON);
|
|
2877
|
+
if (info.wordGapCount === 0) {
|
|
2878
|
+
const perGap = resolvePerGapSpacing(extraSpace, info.letterGapCount);
|
|
2879
|
+
if (perGap == null) return null;
|
|
2880
|
+
if (exceedsThreshold(perGap, avgCharWidth, threshold)) return null;
|
|
2881
|
+
return createJustifySpacing(0, perGap);
|
|
2882
|
+
}
|
|
2883
|
+
const avgWordWidth = Math.max(getAverageWordWidth(info), Number.EPSILON);
|
|
2884
|
+
const nonSpaceCount = Math.max(info.nonSpaceCount, 1);
|
|
2885
|
+
const cjkRatio = info.cjkCount / nonSpaceCount;
|
|
2886
|
+
const latinLikeRatio = info.latinLikeCount / nonSpaceCount;
|
|
2887
|
+
const punctuationRatio = info.punctuationCount / nonSpaceCount;
|
|
2888
|
+
const wordPenalty = 1 + cjkRatio;
|
|
2889
|
+
const letterPenalty = 1 + latinLikeRatio + .5 * punctuationRatio;
|
|
2890
|
+
let bestCandidate = null;
|
|
2891
|
+
for (const wordShare of HYBRID_WORD_SHARE_CANDIDATES) {
|
|
2892
|
+
const wordExtraSpace = extraSpace * wordShare;
|
|
2893
|
+
const letterExtraSpace = extraSpace - wordExtraSpace;
|
|
2894
|
+
const wordSpacingPx = resolvePerGapSpacing(wordExtraSpace, info.wordGapCount);
|
|
2895
|
+
const letterSpacingPx = resolvePerGapSpacing(letterExtraSpace, info.letterGapCount);
|
|
2896
|
+
if (wordSpacingPx == null || letterSpacingPx == null) continue;
|
|
2897
|
+
if (exceedsThreshold(wordSpacingPx, avgWordWidth, threshold) || exceedsThreshold(letterSpacingPx, avgCharWidth, threshold)) continue;
|
|
2898
|
+
const wordRatio = wordSpacingPx / avgWordWidth;
|
|
2899
|
+
const letterRatio = letterSpacingPx / avgCharWidth;
|
|
2900
|
+
const score = wordPenalty * wordRatio ** 2 + letterPenalty * letterRatio ** 2;
|
|
2901
|
+
if (bestCandidate == null || score < bestCandidate.score - JUSTIFY_SCORE_EPSILON || Math.abs(score - bestCandidate.score) <= JUSTIFY_SCORE_EPSILON && wordShare > bestCandidate.wordShare) bestCandidate = {
|
|
2902
|
+
spacing: createJustifySpacing(wordSpacingPx, letterSpacingPx),
|
|
2903
|
+
score,
|
|
2904
|
+
wordShare
|
|
2905
|
+
};
|
|
2906
|
+
}
|
|
2907
|
+
return bestCandidate?.spacing ?? null;
|
|
2908
|
+
}
|
|
2909
|
+
//#endregion
|
|
2752
2910
|
//#region src/nodes/text.ts
|
|
2753
2911
|
function resolvePhysicalTextAlign(options) {
|
|
2754
2912
|
if (options.physicalAlign != null) return options.physicalAlign;
|
|
@@ -2763,6 +2921,26 @@ function normalizeTextMaxWidth(maxWidth) {
|
|
|
2763
2921
|
if (maxWidth == null) return;
|
|
2764
2922
|
return Math.max(0, maxWidth);
|
|
2765
2923
|
}
|
|
2924
|
+
const DEFAULT_TEXT_SPACING = {
|
|
2925
|
+
wordSpacing: "0px",
|
|
2926
|
+
letterSpacing: "0px"
|
|
2927
|
+
};
|
|
2928
|
+
function supportsTextSpacing(g) {
|
|
2929
|
+
return typeof g.wordSpacing === "string" && typeof g.letterSpacing === "string";
|
|
2930
|
+
}
|
|
2931
|
+
function withTextSpacing(g, spacing, cb) {
|
|
2932
|
+
if (!supportsTextSpacing(g)) return cb();
|
|
2933
|
+
const savedWordSpacing = g.wordSpacing;
|
|
2934
|
+
const savedLetterSpacing = g.letterSpacing;
|
|
2935
|
+
try {
|
|
2936
|
+
g.wordSpacing = spacing.wordSpacing;
|
|
2937
|
+
g.letterSpacing = spacing.letterSpacing;
|
|
2938
|
+
return cb();
|
|
2939
|
+
} finally {
|
|
2940
|
+
g.wordSpacing = savedWordSpacing;
|
|
2941
|
+
g.letterSpacing = savedLetterSpacing;
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2766
2944
|
function getTextLayoutContext(ctx) {
|
|
2767
2945
|
return ctx;
|
|
2768
2946
|
}
|
|
@@ -2866,7 +3044,9 @@ function drawRichLine(ctx, line, fallbackColor, x, y, lineHeight) {
|
|
|
2866
3044
|
g.font = fragment.font;
|
|
2867
3045
|
g.fillStyle = ctx.resolveDynValue(fragment.color ?? fallbackColor);
|
|
2868
3046
|
g.textAlign = "left";
|
|
2869
|
-
g
|
|
3047
|
+
withTextSpacing(g, DEFAULT_TEXT_SPACING, () => {
|
|
3048
|
+
g.fillText(fragment.text, cursorX, y + (lineHeight + fragment.shift) / 2);
|
|
3049
|
+
});
|
|
2870
3050
|
});
|
|
2871
3051
|
cursorX += fragment.occupiedWidth;
|
|
2872
3052
|
}
|
|
@@ -2945,23 +3125,83 @@ var MultilineText = class {
|
|
|
2945
3125
|
const spans = this.text;
|
|
2946
3126
|
const { width, lines } = getRichMultiLineDrawLayout(this, ctx, spans, this.options);
|
|
2947
3127
|
const align = resolvePhysicalTextAlign(this.options);
|
|
2948
|
-
const
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
3128
|
+
const maxWidth = normalizeTextMaxWidth(ctx.constraints?.maxWidth);
|
|
3129
|
+
const mode = resolveJustifyMode(this.options.justify);
|
|
3130
|
+
const canJustify = mode != null && maxWidth != null && maxWidth > 0 && isJustifySupported(ctx.graphics);
|
|
3131
|
+
const threshold = this.options.justifyGapThreshold ?? 2;
|
|
3132
|
+
if (canJustify) {
|
|
3133
|
+
const prepared = readPreparedInlineLayout(getRichPreparedKey(spans, this.options.font, this.options.whiteSpace ?? "normal", this.options.wordBreak ?? "normal"), createRichSourceItems(spans, this.options.font), this.options.whiteSpace ?? "normal", this.options.wordBreak ?? "normal");
|
|
3134
|
+
let lineIndex = 0;
|
|
3135
|
+
const totalLines = lines.length;
|
|
3136
|
+
walkPreparedLineRanges(prepared, maxWidth, (lineRange) => {
|
|
3137
|
+
if (lineIndex >= totalLines) return false;
|
|
3138
|
+
const line = lines[lineIndex];
|
|
3139
|
+
const isLastLine = lineIndex === totalLines - 1;
|
|
3140
|
+
if (!(isLastLine && shouldUseMultilineOverflowLayout(this.options) && this.options.overflow === "ellipsis") && (!isLastLine || this.options.justifyLastLine === true)) {
|
|
3141
|
+
const info = analyzeLineForJustify(prepared, lineRange);
|
|
3142
|
+
const spacing = computeJustifySpacing(lineRange.width, maxWidth, info, mode, threshold);
|
|
3143
|
+
if (spacing != null) {
|
|
3144
|
+
let cursorX = x;
|
|
3145
|
+
for (let fi = 0; fi < line.fragments.length; fi++) {
|
|
3146
|
+
const frag = line.fragments[fi];
|
|
3147
|
+
const leadingLetterGapCount = fi > 0 ? frag.gapAtomCount + 1 : 0;
|
|
3148
|
+
const internalLetterGapCount = Math.max(frag.atomCount - 1, 0);
|
|
3149
|
+
cursorX += frag.gapBefore + leadingLetterGapCount * spacing.letterSpacingPx + frag.gapSpaceCount * spacing.wordSpacingPx;
|
|
3150
|
+
ctx.with((g) => {
|
|
3151
|
+
g.font = frag.font;
|
|
3152
|
+
g.fillStyle = ctx.resolveDynValue(frag.color ?? this.options.color);
|
|
3153
|
+
g.textAlign = "left";
|
|
3154
|
+
withTextSpacing(g, spacing, () => {
|
|
3155
|
+
g.fillText(frag.text, cursorX, y + (this.options.lineHeight + frag.shift) / 2);
|
|
3156
|
+
});
|
|
3157
|
+
});
|
|
3158
|
+
cursorX += frag.occupiedWidth + internalLetterGapCount * spacing.letterSpacingPx + frag.spaceCount * spacing.wordSpacingPx;
|
|
3159
|
+
}
|
|
3160
|
+
y += this.options.lineHeight;
|
|
3161
|
+
lineIndex++;
|
|
3162
|
+
return;
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
let cursorX = align === "right" ? x + width : align === "center" ? x + width / 2 : x;
|
|
3166
|
+
for (let fi = 0; fi < line.fragments.length; fi++) {
|
|
3167
|
+
const frag = line.fragments[fi];
|
|
3168
|
+
cursorX += frag.gapBefore;
|
|
3169
|
+
ctx.with((g) => {
|
|
3170
|
+
g.font = frag.font;
|
|
3171
|
+
g.fillStyle = ctx.resolveDynValue(frag.color ?? this.options.color);
|
|
3172
|
+
if (align === "right") g.textAlign = "right";
|
|
3173
|
+
else if (align === "center") g.textAlign = "center";
|
|
3174
|
+
else g.textAlign = "left";
|
|
3175
|
+
withTextSpacing(g, DEFAULT_TEXT_SPACING, () => {
|
|
3176
|
+
g.fillText(frag.text, cursorX, y + (this.options.lineHeight + frag.shift) / 2);
|
|
3177
|
+
});
|
|
3178
|
+
});
|
|
3179
|
+
cursorX += frag.occupiedWidth;
|
|
3180
|
+
}
|
|
3181
|
+
y += this.options.lineHeight;
|
|
3182
|
+
lineIndex++;
|
|
3183
|
+
});
|
|
3184
|
+
} else {
|
|
3185
|
+
const startX = align === "right" ? x + width : align === "center" ? x + width / 2 : x;
|
|
3186
|
+
for (const line of lines) {
|
|
3187
|
+
let cursorX = startX;
|
|
3188
|
+
for (let fi = 0; fi < line.fragments.length; fi++) {
|
|
3189
|
+
const frag = line.fragments[fi];
|
|
3190
|
+
cursorX += frag.gapBefore;
|
|
3191
|
+
ctx.with((g) => {
|
|
3192
|
+
g.font = frag.font;
|
|
3193
|
+
g.fillStyle = ctx.resolveDynValue(frag.color ?? this.options.color);
|
|
3194
|
+
if (align === "right") g.textAlign = "right";
|
|
3195
|
+
else if (align === "center") g.textAlign = "center";
|
|
3196
|
+
else g.textAlign = "left";
|
|
3197
|
+
withTextSpacing(g, DEFAULT_TEXT_SPACING, () => {
|
|
3198
|
+
g.fillText(frag.text, cursorX, y + (this.options.lineHeight + frag.shift) / 2);
|
|
3199
|
+
});
|
|
3200
|
+
});
|
|
3201
|
+
cursorX += frag.occupiedWidth;
|
|
3202
|
+
}
|
|
3203
|
+
y += this.options.lineHeight;
|
|
2963
3204
|
}
|
|
2964
|
-
y += this.options.lineHeight;
|
|
2965
3205
|
}
|
|
2966
3206
|
return false;
|
|
2967
3207
|
}
|
|
@@ -2969,30 +3209,76 @@ var MultilineText = class {
|
|
|
2969
3209
|
g.font = this.options.font;
|
|
2970
3210
|
g.fillStyle = ctx.resolveDynValue(this.options.color);
|
|
2971
3211
|
const { width, lines } = getMultiLineDrawLayout(this, ctx, this.text, this.options);
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
3212
|
+
const maxWidth = normalizeTextMaxWidth(ctx.constraints?.maxWidth);
|
|
3213
|
+
const mode = resolveJustifyMode(this.options.justify);
|
|
3214
|
+
const canJustify = mode != null && maxWidth != null && maxWidth > 0 && isJustifySupported(g);
|
|
3215
|
+
const threshold = this.options.justifyGapThreshold ?? 2;
|
|
3216
|
+
if (canJustify) {
|
|
3217
|
+
const prepared = readPreparedText(this.text, this.options.font, this.options.whiteSpace ?? "normal", this.options.wordBreak ?? "normal");
|
|
3218
|
+
let lineIndex = 0;
|
|
3219
|
+
const totalLines = lines.length;
|
|
3220
|
+
walkPreparedLineRanges(prepared, maxWidth, (lineRange) => {
|
|
3221
|
+
if (lineIndex >= totalLines) return false;
|
|
3222
|
+
const layout = lines[lineIndex];
|
|
3223
|
+
const isLastLine = lineIndex === totalLines - 1;
|
|
3224
|
+
if (!(isLastLine && shouldUseMultilineOverflowLayout(this.options) && this.options.overflow === "ellipsis") && (!isLastLine || this.options.justifyLastLine === true)) {
|
|
3225
|
+
const info = analyzeLineForJustify(prepared, lineRange);
|
|
3226
|
+
const spacing = computeJustifySpacing(lineRange.width, maxWidth, info, mode, threshold);
|
|
3227
|
+
if (spacing != null) {
|
|
3228
|
+
withTextSpacing(g, spacing, () => {
|
|
3229
|
+
g.textAlign = "left";
|
|
3230
|
+
g.fillText(layout.text, x, y + (this.options.lineHeight + layout.shift) / 2);
|
|
3231
|
+
});
|
|
3232
|
+
y += this.options.lineHeight;
|
|
3233
|
+
lineIndex++;
|
|
3234
|
+
return;
|
|
3235
|
+
}
|
|
2977
3236
|
}
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
}
|
|
2994
|
-
|
|
2995
|
-
|
|
3237
|
+
withTextSpacing(g, DEFAULT_TEXT_SPACING, () => {
|
|
3238
|
+
switch (resolvePhysicalTextAlign(this.options)) {
|
|
3239
|
+
case "left":
|
|
3240
|
+
g.textAlign = "left";
|
|
3241
|
+
g.fillText(layout.text, x, y + (this.options.lineHeight + layout.shift) / 2);
|
|
3242
|
+
break;
|
|
3243
|
+
case "right":
|
|
3244
|
+
g.textAlign = "right";
|
|
3245
|
+
g.fillText(layout.text, x + width, y + (this.options.lineHeight + layout.shift) / 2);
|
|
3246
|
+
break;
|
|
3247
|
+
case "center":
|
|
3248
|
+
g.textAlign = "center";
|
|
3249
|
+
g.fillText(layout.text, x + width / 2, y + (this.options.lineHeight + layout.shift) / 2);
|
|
3250
|
+
break;
|
|
3251
|
+
}
|
|
3252
|
+
});
|
|
3253
|
+
y += this.options.lineHeight;
|
|
3254
|
+
lineIndex++;
|
|
3255
|
+
});
|
|
3256
|
+
} else withTextSpacing(g, DEFAULT_TEXT_SPACING, () => {
|
|
3257
|
+
switch (resolvePhysicalTextAlign(this.options)) {
|
|
3258
|
+
case "left":
|
|
3259
|
+
for (const { text, shift } of lines) {
|
|
3260
|
+
g.fillText(text, x, y + (this.options.lineHeight + shift) / 2);
|
|
3261
|
+
y += this.options.lineHeight;
|
|
3262
|
+
}
|
|
3263
|
+
break;
|
|
3264
|
+
case "right":
|
|
3265
|
+
x += width;
|
|
3266
|
+
g.textAlign = "right";
|
|
3267
|
+
for (const { text, shift } of lines) {
|
|
3268
|
+
g.fillText(text, x, y + (this.options.lineHeight + shift) / 2);
|
|
3269
|
+
y += this.options.lineHeight;
|
|
3270
|
+
}
|
|
3271
|
+
break;
|
|
3272
|
+
case "center":
|
|
3273
|
+
x += width / 2;
|
|
3274
|
+
g.textAlign = "center";
|
|
3275
|
+
for (const { text, shift } of lines) {
|
|
3276
|
+
g.fillText(text, x, y + (this.options.lineHeight + shift) / 2);
|
|
3277
|
+
y += this.options.lineHeight;
|
|
3278
|
+
}
|
|
3279
|
+
break;
|
|
3280
|
+
}
|
|
3281
|
+
});
|
|
2996
3282
|
return false;
|
|
2997
3283
|
});
|
|
2998
3284
|
}
|
|
@@ -3055,7 +3341,9 @@ var Text = class {
|
|
|
3055
3341
|
g.font = this.options.font;
|
|
3056
3342
|
g.fillStyle = ctx.resolveDynValue(this.options.color);
|
|
3057
3343
|
const layout = getSingleLineLayout(this, ctx, text, this.options);
|
|
3058
|
-
g
|
|
3344
|
+
withTextSpacing(g, DEFAULT_TEXT_SPACING, () => {
|
|
3345
|
+
g.fillText(layout.text, x, y + (this.options.lineHeight + layout.shift) / 2);
|
|
3346
|
+
});
|
|
3059
3347
|
return false;
|
|
3060
3348
|
});
|
|
3061
3349
|
}
|
|
@@ -3337,6 +3625,24 @@ function subscribeListState(list, owner, listener) {
|
|
|
3337
3625
|
token
|
|
3338
3626
|
});
|
|
3339
3627
|
}
|
|
3628
|
+
function isObjectIdentityCandidate(value) {
|
|
3629
|
+
return typeof value === "object" && value !== null || typeof value === "function";
|
|
3630
|
+
}
|
|
3631
|
+
function assertUniqueItemReferences(items, existingItems) {
|
|
3632
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3633
|
+
if (existingItems != null) {
|
|
3634
|
+
for (const item of existingItems) if (isObjectIdentityCandidate(item)) seen.add(item);
|
|
3635
|
+
}
|
|
3636
|
+
for (const item of items) {
|
|
3637
|
+
if (!isObjectIdentityCandidate(item)) continue;
|
|
3638
|
+
if (seen.has(item)) throw new Error("ListState items must use unique object references.");
|
|
3639
|
+
seen.add(item);
|
|
3640
|
+
}
|
|
3641
|
+
}
|
|
3642
|
+
function normalizeUpdateAnimation(animation) {
|
|
3643
|
+
if (animation == null) return;
|
|
3644
|
+
return Number.isFinite(animation.duration) ? { duration: animation.duration } : {};
|
|
3645
|
+
}
|
|
3340
3646
|
var ListState = class {
|
|
3341
3647
|
#items;
|
|
3342
3648
|
/** Pixel offset from the anchored item edge. */
|
|
@@ -3349,14 +3655,18 @@ var ListState = class {
|
|
|
3349
3655
|
}
|
|
3350
3656
|
/** Replaces the full item collection while preserving scroll state. */
|
|
3351
3657
|
set items(value) {
|
|
3352
|
-
|
|
3658
|
+
const nextItems = [...value];
|
|
3659
|
+
assertUniqueItemReferences(nextItems);
|
|
3660
|
+
this.#items = nextItems;
|
|
3353
3661
|
emitListStateChange(this, { type: "set" });
|
|
3354
3662
|
}
|
|
3355
3663
|
/**
|
|
3356
3664
|
* @param items Initial list items.
|
|
3357
3665
|
*/
|
|
3358
3666
|
constructor(items = []) {
|
|
3359
|
-
|
|
3667
|
+
const nextItems = [...items];
|
|
3668
|
+
assertUniqueItemReferences(nextItems);
|
|
3669
|
+
this.#items = nextItems;
|
|
3360
3670
|
}
|
|
3361
3671
|
/** Prepends one or more items. */
|
|
3362
3672
|
unshift(...items) {
|
|
@@ -3365,6 +3675,7 @@ var ListState = class {
|
|
|
3365
3675
|
/** Prepends an array of items. */
|
|
3366
3676
|
unshiftAll(items) {
|
|
3367
3677
|
if (items.length === 0) return;
|
|
3678
|
+
assertUniqueItemReferences(items, this.#items);
|
|
3368
3679
|
if (this.position != null) this.position += items.length;
|
|
3369
3680
|
this.#items = items.concat(this.#items);
|
|
3370
3681
|
emitListStateChange(this, {
|
|
@@ -3379,6 +3690,7 @@ var ListState = class {
|
|
|
3379
3690
|
/** Appends an array of items. */
|
|
3380
3691
|
pushAll(items) {
|
|
3381
3692
|
if (items.length === 0) return;
|
|
3693
|
+
assertUniqueItemReferences(items, this.#items);
|
|
3382
3694
|
this.#items.push(...items);
|
|
3383
3695
|
emitListStateChange(this, {
|
|
3384
3696
|
type: "push",
|
|
@@ -3386,19 +3698,21 @@ var ListState = class {
|
|
|
3386
3698
|
});
|
|
3387
3699
|
}
|
|
3388
3700
|
/**
|
|
3389
|
-
*
|
|
3701
|
+
* Updates an existing item by object identity.
|
|
3390
3702
|
*/
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
if (
|
|
3394
|
-
const
|
|
3395
|
-
|
|
3703
|
+
update(targetItem, nextItem, animation) {
|
|
3704
|
+
if (!isObjectIdentityCandidate(targetItem) || !isObjectIdentityCandidate(nextItem)) throw new TypeError("update() only supports object items.");
|
|
3705
|
+
if (targetItem === nextItem) throw new Error("update() requires nextItem to be a new object reference.");
|
|
3706
|
+
const index = this.#items.indexOf(targetItem);
|
|
3707
|
+
if (index < 0) throw new Error("update() targetItem is not present in the list.");
|
|
3708
|
+
if (this.#items.includes(nextItem)) throw new Error("update() nextItem is already present in the list.");
|
|
3709
|
+
const prevItem = this.#items[index];
|
|
3710
|
+
this.#items[index] = nextItem;
|
|
3396
3711
|
emitListStateChange(this, {
|
|
3397
|
-
type: "
|
|
3398
|
-
index: normalizedIndex,
|
|
3712
|
+
type: "update",
|
|
3399
3713
|
prevItem,
|
|
3400
|
-
nextItem
|
|
3401
|
-
animation:
|
|
3714
|
+
nextItem,
|
|
3715
|
+
animation: normalizeUpdateAnimation(animation)
|
|
3402
3716
|
});
|
|
3403
3717
|
}
|
|
3404
3718
|
/**
|
|
@@ -3412,7 +3726,9 @@ var ListState = class {
|
|
|
3412
3726
|
* Replaces all items and clears scroll state.
|
|
3413
3727
|
*/
|
|
3414
3728
|
reset(items = []) {
|
|
3415
|
-
|
|
3729
|
+
const nextItems = [...items];
|
|
3730
|
+
assertUniqueItemReferences(nextItems);
|
|
3731
|
+
this.#items = nextItems;
|
|
3416
3732
|
this.offset = 0;
|
|
3417
3733
|
this.position = void 0;
|
|
3418
3734
|
emitListStateChange(this, { type: "reset" });
|
|
@@ -3520,7 +3836,8 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
|
|
|
3520
3836
|
static JUMP_DURATION_PER_ITEM = 28;
|
|
3521
3837
|
#controlledState;
|
|
3522
3838
|
#jumpAnimation;
|
|
3523
|
-
#replacementAnimations = /* @__PURE__ */ new
|
|
3839
|
+
#replacementAnimations = /* @__PURE__ */ new WeakMap();
|
|
3840
|
+
#activeReplacementItems = /* @__PURE__ */ new Set();
|
|
3524
3841
|
#nextReplacementLayerKey = 0;
|
|
3525
3842
|
constructor(graphics, options) {
|
|
3526
3843
|
super(graphics, options);
|
|
@@ -3683,14 +4000,14 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
|
|
|
3683
4000
|
}
|
|
3684
4001
|
_getItemHeight(index) {
|
|
3685
4002
|
const now = getNow();
|
|
3686
|
-
const replacement = this.#readReplacementAnimation(index, now);
|
|
3687
|
-
if (replacement != null) return this.#sampleReplacementHeight(replacement, now);
|
|
3688
4003
|
const item = this.items[index];
|
|
4004
|
+
const replacement = this.#readReplacementAnimation(item, now);
|
|
4005
|
+
if (replacement != null) return this.#sampleReplacementHeight(replacement, now);
|
|
3689
4006
|
const node = this.options.renderItem(item);
|
|
3690
4007
|
return this.measureRootNode(node).height;
|
|
3691
4008
|
}
|
|
3692
|
-
_resolveItem(item,
|
|
3693
|
-
const replacement = this.#readReplacementAnimation(
|
|
4009
|
+
_resolveItem(item, _index, now) {
|
|
4010
|
+
const replacement = this.#readReplacementAnimation(item, now);
|
|
3694
4011
|
if (replacement == null) {
|
|
3695
4012
|
const node = this.options.renderItem(item);
|
|
3696
4013
|
return {
|
|
@@ -3762,24 +4079,26 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
|
|
|
3762
4079
|
#isLayerComplete(layer, now) {
|
|
3763
4080
|
return getProgress(layer.startTime, layer.duration, now) >= 1 && Math.abs(layer.toAlpha - this.#sampleLayerAlpha(layer, now)) <= ALPHA_EPSILON;
|
|
3764
4081
|
}
|
|
3765
|
-
#readReplacementAnimation(
|
|
3766
|
-
const animation = this.#replacementAnimations.get(
|
|
4082
|
+
#readReplacementAnimation(item, now) {
|
|
4083
|
+
const animation = this.#replacementAnimations.get(item);
|
|
3767
4084
|
if (animation == null) return;
|
|
3768
4085
|
const currentLayer = animation.layers.find((layer) => layer.key === animation.currentLayerKey);
|
|
3769
4086
|
if (currentLayer == null) {
|
|
3770
|
-
this.#replacementAnimations.delete(
|
|
4087
|
+
this.#replacementAnimations.delete(item);
|
|
4088
|
+
this.#activeReplacementItems.delete(item);
|
|
3771
4089
|
return;
|
|
3772
4090
|
}
|
|
3773
4091
|
animation.layers = animation.layers.filter((layer) => layer.key === animation.currentLayerKey || !this.#isLayerComplete(layer, now));
|
|
3774
4092
|
if (getProgress(animation.startTime, animation.duration, now) >= 1 && this.#isLayerComplete(currentLayer, now) && animation.layers.length === 1) {
|
|
3775
|
-
this.#replacementAnimations.delete(
|
|
4093
|
+
this.#replacementAnimations.delete(item);
|
|
4094
|
+
this.#activeReplacementItems.delete(item);
|
|
3776
4095
|
return;
|
|
3777
4096
|
}
|
|
3778
4097
|
return animation;
|
|
3779
4098
|
}
|
|
3780
4099
|
#prepareReplacementAnimations(now) {
|
|
3781
4100
|
let keepAnimating = false;
|
|
3782
|
-
for (const
|
|
4101
|
+
for (const item of [...this.#activeReplacementItems]) if (this.#readReplacementAnimation(item, now) != null) keepAnimating = true;
|
|
3783
4102
|
return keepAnimating;
|
|
3784
4103
|
}
|
|
3785
4104
|
#drawReplacementLayers(layers, slotHeight, y) {
|
|
@@ -3805,38 +4124,34 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
|
|
|
3805
4124
|
}
|
|
3806
4125
|
#handleListStateChange(change) {
|
|
3807
4126
|
switch (change.type) {
|
|
3808
|
-
case "
|
|
3809
|
-
this.#
|
|
4127
|
+
case "update":
|
|
4128
|
+
this.#handleUpdate(change.prevItem, change.nextItem, change.animation?.duration);
|
|
3810
4129
|
break;
|
|
3811
|
-
case "unshift":
|
|
3812
|
-
if (change.count <= 0 || this.#replacementAnimations.size === 0) return;
|
|
3813
|
-
const shifted = /* @__PURE__ */ new Map();
|
|
3814
|
-
for (const [index, animation] of this.#replacementAnimations) shifted.set(index + change.count, animation);
|
|
3815
|
-
this.#replacementAnimations = shifted;
|
|
3816
|
-
break;
|
|
3817
|
-
}
|
|
4130
|
+
case "unshift":
|
|
3818
4131
|
case "push": break;
|
|
3819
4132
|
case "reset":
|
|
3820
4133
|
case "set":
|
|
3821
|
-
this.#replacementAnimations
|
|
4134
|
+
this.#replacementAnimations = /* @__PURE__ */ new WeakMap();
|
|
4135
|
+
this.#activeReplacementItems.clear();
|
|
3822
4136
|
break;
|
|
3823
4137
|
}
|
|
3824
4138
|
}
|
|
3825
|
-
#
|
|
3826
|
-
const normalizedDuration = Number.isFinite(duration) ?
|
|
4139
|
+
#handleUpdate(prevItem, nextItem, duration) {
|
|
4140
|
+
const normalizedDuration = Math.max(0, typeof duration === "number" && Number.isFinite(duration) ? duration : 0);
|
|
3827
4141
|
if (normalizedDuration <= 0) {
|
|
3828
|
-
this.#replacementAnimations.delete(
|
|
4142
|
+
this.#replacementAnimations.delete(prevItem);
|
|
4143
|
+
this.#activeReplacementItems.delete(prevItem);
|
|
3829
4144
|
return;
|
|
3830
4145
|
}
|
|
3831
4146
|
const now = getNow();
|
|
3832
4147
|
const nextNode = this.options.renderItem(nextItem);
|
|
3833
4148
|
const nextHeight = this.measureRootNode(nextNode).height;
|
|
3834
|
-
const animation = this.#readReplacementAnimation(
|
|
4149
|
+
const animation = this.#readReplacementAnimation(prevItem, now);
|
|
3835
4150
|
if (animation == null) {
|
|
3836
4151
|
const prevNode = this.options.renderItem(prevItem);
|
|
3837
4152
|
const outgoing = this.#createReplacementLayer(prevNode, 1, 0, now, normalizedDuration);
|
|
3838
4153
|
const incoming = this.#createReplacementLayer(nextNode, 0, 1, now, normalizedDuration);
|
|
3839
|
-
this.#replacementAnimations.set(
|
|
4154
|
+
this.#replacementAnimations.set(nextItem, {
|
|
3840
4155
|
currentLayerKey: incoming.key,
|
|
3841
4156
|
layers: [outgoing, incoming],
|
|
3842
4157
|
fromHeight: this.measureRootNode(prevNode).height,
|
|
@@ -3844,6 +4159,8 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
|
|
|
3844
4159
|
startTime: now,
|
|
3845
4160
|
duration: normalizedDuration
|
|
3846
4161
|
});
|
|
4162
|
+
this.#activeReplacementItems.delete(prevItem);
|
|
4163
|
+
this.#activeReplacementItems.add(nextItem);
|
|
3847
4164
|
return;
|
|
3848
4165
|
}
|
|
3849
4166
|
const currentLayer = animation.layers.find((layer) => layer.key === animation.currentLayerKey);
|
|
@@ -3853,7 +4170,8 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
|
|
|
3853
4170
|
if (currentAlpha > ALPHA_EPSILON) layers.push(this.#createReplacementLayer(currentNode, currentAlpha, 0, now, normalizedDuration));
|
|
3854
4171
|
const incoming = this.#createReplacementLayer(nextNode, 0, 1, now, normalizedDuration);
|
|
3855
4172
|
layers.push(incoming);
|
|
3856
|
-
this.#replacementAnimations.
|
|
4173
|
+
this.#replacementAnimations.delete(prevItem);
|
|
4174
|
+
this.#replacementAnimations.set(nextItem, {
|
|
3857
4175
|
currentLayerKey: incoming.key,
|
|
3858
4176
|
layers,
|
|
3859
4177
|
fromHeight: this.#sampleReplacementHeight(animation, now),
|
|
@@ -3861,6 +4179,8 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
|
|
|
3861
4179
|
startTime: now,
|
|
3862
4180
|
duration: normalizedDuration
|
|
3863
4181
|
});
|
|
4182
|
+
this.#activeReplacementItems.delete(prevItem);
|
|
4183
|
+
this.#activeReplacementItems.add(nextItem);
|
|
3864
4184
|
}
|
|
3865
4185
|
};
|
|
3866
4186
|
//#endregion
|