chat-layout 1.2.0-1 → 1.2.0-2
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 +26 -0
- package/example/chat.ts +80 -1
- package/index.d.mts +29 -2
- package/index.mjs +366 -78
- package/index.mjs.map +1 -1
- package/package.json +1 -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
|
}
|