chat-layout 1.2.0-0 → 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 +44 -5
- package/example/chat.ts +86 -2
- package/index.d.mts +47 -2
- package/index.mjs +473 -79
- package/index.mjs.map +1 -1
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -814,6 +814,112 @@ var Place = class extends Wrapper {
|
|
|
814
814
|
});
|
|
815
815
|
}
|
|
816
816
|
};
|
|
817
|
+
//#endregion
|
|
818
|
+
//#region src/nodes/shrinkwrap.ts
|
|
819
|
+
const DEFAULT_TOLERANCE = .5;
|
|
820
|
+
const HEIGHT_EPSILON = 1e-6;
|
|
821
|
+
function withMaxWidth(constraints, maxWidth) {
|
|
822
|
+
return {
|
|
823
|
+
...constraints,
|
|
824
|
+
maxWidth
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
function computeShrinkwrapWidth(measure, lowerBound, upperBound, referenceHeight, tolerance = DEFAULT_TOLERANCE) {
|
|
828
|
+
const minWidth = Math.min(lowerBound, upperBound);
|
|
829
|
+
const maxWidth = Math.max(lowerBound, upperBound);
|
|
830
|
+
const effectiveTolerance = Math.max(tolerance, HEIGHT_EPSILON);
|
|
831
|
+
const lowerBoundBox = measure(minWidth);
|
|
832
|
+
if (lowerBoundBox.height <= referenceHeight + HEIGHT_EPSILON) return {
|
|
833
|
+
maxWidth: minWidth,
|
|
834
|
+
box: lowerBoundBox
|
|
835
|
+
};
|
|
836
|
+
let lo = minWidth;
|
|
837
|
+
let hi = maxWidth;
|
|
838
|
+
let hiBox = measure(maxWidth);
|
|
839
|
+
while (hi - lo > effectiveTolerance) {
|
|
840
|
+
const probeWidth = (lo + hi) / 2;
|
|
841
|
+
const probeBox = measure(probeWidth);
|
|
842
|
+
if (probeBox.height <= referenceHeight + HEIGHT_EPSILON) {
|
|
843
|
+
hi = probeWidth;
|
|
844
|
+
hiBox = probeBox;
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
lo = probeWidth;
|
|
848
|
+
}
|
|
849
|
+
return {
|
|
850
|
+
maxWidth: hi,
|
|
851
|
+
box: hiBox
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Shrinks a single child to the narrowest width that does not increase its reference height.
|
|
856
|
+
*/
|
|
857
|
+
var ShrinkWrap = class extends Wrapper {
|
|
858
|
+
constructor(inner, options = {}) {
|
|
859
|
+
super(inner);
|
|
860
|
+
this.options = options;
|
|
861
|
+
}
|
|
862
|
+
measure(ctx) {
|
|
863
|
+
const constraints = ctx.constraints;
|
|
864
|
+
const availableWidth = constraints?.maxWidth;
|
|
865
|
+
if (availableWidth == null) {
|
|
866
|
+
const childConstraints = constraints == null ? void 0 : { ...constraints };
|
|
867
|
+
const childBox = ctx.measureNode(this.inner, childConstraints);
|
|
868
|
+
this.#writeLayout(ctx, childBox, childConstraints);
|
|
869
|
+
return childBox;
|
|
870
|
+
}
|
|
871
|
+
const boundedConstraints = constraints == null ? { maxWidth: availableWidth } : constraints;
|
|
872
|
+
const referenceConstraints = { ...boundedConstraints };
|
|
873
|
+
const referenceBox = ctx.measureNode(this.inner, referenceConstraints);
|
|
874
|
+
let lowerBound = measureNodeMinContent(ctx, this.inner, boundedConstraints).width;
|
|
875
|
+
const preferredMinWidth = this.options.preferredMinWidth == null ? void 0 : Math.max(0, this.options.preferredMinWidth);
|
|
876
|
+
if (preferredMinWidth != null && preferredMinWidth <= availableWidth) lowerBound = Math.max(lowerBound, preferredMinWidth);
|
|
877
|
+
if (boundedConstraints.minWidth != null) lowerBound = Math.max(lowerBound, boundedConstraints.minWidth);
|
|
878
|
+
if (lowerBound >= availableWidth) {
|
|
879
|
+
this.#writeLayout(ctx, referenceBox, referenceConstraints);
|
|
880
|
+
return referenceBox;
|
|
881
|
+
}
|
|
882
|
+
const finalConstraints = withMaxWidth(boundedConstraints, computeShrinkwrapWidth((maxWidth) => ctx.measureNode(this.inner, withMaxWidth(boundedConstraints, maxWidth)), lowerBound, availableWidth, referenceBox.height, this.options.tolerance ?? DEFAULT_TOLERANCE).maxWidth);
|
|
883
|
+
const finalBox = ctx.measureNode(this.inner, finalConstraints);
|
|
884
|
+
this.#writeLayout(ctx, finalBox, finalConstraints);
|
|
885
|
+
return finalBox;
|
|
886
|
+
}
|
|
887
|
+
measureMinContent(ctx) {
|
|
888
|
+
return measureNodeMinContent(ctx, this.inner);
|
|
889
|
+
}
|
|
890
|
+
draw(ctx, x, y) {
|
|
891
|
+
const layoutResult = readLayoutResult(this, ctx);
|
|
892
|
+
if (!layoutResult) return this.inner.draw(ctx, x, y);
|
|
893
|
+
const childResult = getSingleChildLayout(layoutResult);
|
|
894
|
+
if (!childResult) return false;
|
|
895
|
+
return childResult.node.draw(withConstraints(ctx, childResult.constraints), x + childResult.rect.x, y + childResult.rect.y);
|
|
896
|
+
}
|
|
897
|
+
hittest(ctx, test) {
|
|
898
|
+
const layoutResult = readLayoutResult(this, ctx);
|
|
899
|
+
if (!layoutResult) return false;
|
|
900
|
+
const hit = findChildAtPoint(layoutResult.children, test.x, test.y, "rect");
|
|
901
|
+
if (!hit) return false;
|
|
902
|
+
return hit.child.node.hittest(withConstraints(ctx, hit.child.constraints), {
|
|
903
|
+
...test,
|
|
904
|
+
x: hit.localX,
|
|
905
|
+
y: hit.localY
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
#writeLayout(ctx, childBox, childConstraints) {
|
|
909
|
+
const childRect = createRect(0, 0, childBox.width, childBox.height);
|
|
910
|
+
writeLayoutResult(this, ctx, {
|
|
911
|
+
containerBox: childRect,
|
|
912
|
+
contentBox: childRect,
|
|
913
|
+
children: [{
|
|
914
|
+
node: this.inner,
|
|
915
|
+
rect: childRect,
|
|
916
|
+
contentBox: childRect,
|
|
917
|
+
constraints: childConstraints
|
|
918
|
+
}],
|
|
919
|
+
constraints: ctx.constraints
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
};
|
|
817
923
|
Number.POSITIVE_INFINITY;
|
|
818
924
|
const MIN_CONTENT_WIDTH_EPSILON = .001;
|
|
819
925
|
let sharedGraphemeSegmenter;
|
|
@@ -2280,63 +2386,65 @@ function measureRichFragmentShift(ctx, font) {
|
|
|
2280
2386
|
}
|
|
2281
2387
|
function materializeRichFragments(ctx, spans, defaultColor, atoms) {
|
|
2282
2388
|
const fragments = [];
|
|
2283
|
-
let
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
}
|
|
2290
|
-
const span = spans[atom.itemIndex];
|
|
2291
|
-
const font = span?.font ?? atom.font;
|
|
2292
|
-
const color = span?.color ?? defaultColor;
|
|
2293
|
-
const previous = fragments[fragments.length - 1];
|
|
2294
|
-
if (previous != null && previous.itemIndex === atom.itemIndex && previous.font === font && pendingGapBefore === 0) {
|
|
2295
|
-
previous.text += atom.text;
|
|
2296
|
-
previous.occupiedWidth += occupiedWidth;
|
|
2297
|
-
continue;
|
|
2298
|
-
}
|
|
2299
|
-
fragments.push({
|
|
2300
|
-
itemIndex: atom.itemIndex,
|
|
2301
|
-
text: atom.text,
|
|
2302
|
-
font,
|
|
2303
|
-
color,
|
|
2304
|
-
gapBefore: pendingGapBefore,
|
|
2305
|
-
occupiedWidth,
|
|
2306
|
-
shift: measureRichFragmentShift(ctx, font)
|
|
2307
|
-
});
|
|
2308
|
-
pendingGapBefore = 0;
|
|
2309
|
-
}
|
|
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);
|
|
2310
2395
|
return fragments;
|
|
2311
2396
|
}
|
|
2312
|
-
function appendRichFragment(ctx, spans, defaultColor, fragments, atom,
|
|
2397
|
+
function appendRichFragment(ctx, spans, defaultColor, fragments, atom, pendingGap) {
|
|
2313
2398
|
const occupiedWidth = atom.width + atom.extraWidthAfter;
|
|
2314
|
-
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
|
+
};
|
|
2315
2404
|
const span = spans[atom.itemIndex];
|
|
2316
2405
|
const font = span?.font ?? atom.font;
|
|
2317
2406
|
const color = span?.color ?? defaultColor;
|
|
2318
2407
|
const previous = fragments[fragments.length - 1];
|
|
2319
|
-
|
|
2408
|
+
const spaceCount = atom.kind === "space" ? 1 : 0;
|
|
2409
|
+
if (previous != null && previous.itemIndex === atom.itemIndex && previous.font === font && pendingGap.gapBefore === 0) {
|
|
2320
2410
|
previous.text += atom.text;
|
|
2321
2411
|
previous.occupiedWidth += occupiedWidth;
|
|
2322
|
-
|
|
2412
|
+
previous.atomCount += 1;
|
|
2413
|
+
previous.spaceCount += spaceCount;
|
|
2414
|
+
return {
|
|
2415
|
+
gapBefore: 0,
|
|
2416
|
+
gapAtomCount: 0,
|
|
2417
|
+
gapSpaceCount: 0
|
|
2418
|
+
};
|
|
2323
2419
|
}
|
|
2324
2420
|
fragments.push({
|
|
2325
2421
|
itemIndex: atom.itemIndex,
|
|
2326
2422
|
text: atom.text,
|
|
2327
2423
|
font,
|
|
2328
2424
|
color,
|
|
2329
|
-
gapBefore:
|
|
2425
|
+
gapBefore: pendingGap.gapBefore,
|
|
2426
|
+
gapAtomCount: pendingGap.gapAtomCount,
|
|
2427
|
+
gapSpaceCount: pendingGap.gapSpaceCount,
|
|
2330
2428
|
occupiedWidth,
|
|
2429
|
+
atomCount: 1,
|
|
2430
|
+
spaceCount,
|
|
2331
2431
|
shift: measureRichFragmentShift(ctx, font)
|
|
2332
2432
|
});
|
|
2333
|
-
return
|
|
2433
|
+
return {
|
|
2434
|
+
gapBefore: 0,
|
|
2435
|
+
gapAtomCount: 0,
|
|
2436
|
+
gapSpaceCount: 0
|
|
2437
|
+
};
|
|
2334
2438
|
}
|
|
2335
2439
|
function materializeRichFragmentsInRange(ctx, spans, defaultColor, prepared, start, end) {
|
|
2336
2440
|
const fragments = [];
|
|
2337
|
-
let
|
|
2441
|
+
let pendingGap = {
|
|
2442
|
+
gapBefore: 0,
|
|
2443
|
+
gapAtomCount: 0,
|
|
2444
|
+
gapSpaceCount: 0
|
|
2445
|
+
};
|
|
2338
2446
|
forEachAtomInRange(prepared, start, end, (atom) => {
|
|
2339
|
-
|
|
2447
|
+
pendingGap = appendRichFragment(ctx, spans, defaultColor, fragments, atom, pendingGap);
|
|
2340
2448
|
});
|
|
2341
2449
|
return fragments;
|
|
2342
2450
|
}
|
|
@@ -2382,7 +2490,11 @@ function createRichEllipsisFragment(ctx, font, color) {
|
|
|
2382
2490
|
font,
|
|
2383
2491
|
color,
|
|
2384
2492
|
gapBefore: 0,
|
|
2493
|
+
gapAtomCount: 0,
|
|
2494
|
+
gapSpaceCount: 0,
|
|
2385
2495
|
occupiedWidth: measureEllipsisWidth(ctx),
|
|
2496
|
+
atomCount: 1,
|
|
2497
|
+
spaceCount: 0,
|
|
2386
2498
|
shift: measureFontShift(ctx)
|
|
2387
2499
|
}));
|
|
2388
2500
|
}
|
|
@@ -2468,13 +2580,17 @@ function layoutRichEndEllipsisFromCursor(ctx, spans, defaultFont, defaultColor,
|
|
|
2468
2580
|
const { prefixCount, width } = selection;
|
|
2469
2581
|
const fragments = [];
|
|
2470
2582
|
let atomIndex = 0;
|
|
2471
|
-
let
|
|
2583
|
+
let pendingGap = {
|
|
2584
|
+
gapBefore: 0,
|
|
2585
|
+
gapAtomCount: 0,
|
|
2586
|
+
gapSpaceCount: 0
|
|
2587
|
+
};
|
|
2472
2588
|
let lastVisibleAtom;
|
|
2473
2589
|
let lastAtom;
|
|
2474
2590
|
forEachAtomFromCursorToEnd(prepared, start, (atom) => {
|
|
2475
2591
|
lastAtom = atom;
|
|
2476
2592
|
if (atomIndex < prefixCount) {
|
|
2477
|
-
|
|
2593
|
+
pendingGap = appendRichFragment(ctx, spans, defaultColor, fragments, atom, pendingGap);
|
|
2478
2594
|
lastVisibleAtom = atom;
|
|
2479
2595
|
}
|
|
2480
2596
|
atomIndex += 1;
|
|
@@ -2643,6 +2759,154 @@ function layoutRichTextWithOverflow(ctx, spans, maxWidth, defaultFont, defaultCo
|
|
|
2643
2759
|
};
|
|
2644
2760
|
}
|
|
2645
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
|
|
2646
2910
|
//#region src/nodes/text.ts
|
|
2647
2911
|
function resolvePhysicalTextAlign(options) {
|
|
2648
2912
|
if (options.physicalAlign != null) return options.physicalAlign;
|
|
@@ -2657,6 +2921,26 @@ function normalizeTextMaxWidth(maxWidth) {
|
|
|
2657
2921
|
if (maxWidth == null) return;
|
|
2658
2922
|
return Math.max(0, maxWidth);
|
|
2659
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
|
+
}
|
|
2660
2944
|
function getTextLayoutContext(ctx) {
|
|
2661
2945
|
return ctx;
|
|
2662
2946
|
}
|
|
@@ -2760,7 +3044,9 @@ function drawRichLine(ctx, line, fallbackColor, x, y, lineHeight) {
|
|
|
2760
3044
|
g.font = fragment.font;
|
|
2761
3045
|
g.fillStyle = ctx.resolveDynValue(fragment.color ?? fallbackColor);
|
|
2762
3046
|
g.textAlign = "left";
|
|
2763
|
-
g
|
|
3047
|
+
withTextSpacing(g, DEFAULT_TEXT_SPACING, () => {
|
|
3048
|
+
g.fillText(fragment.text, cursorX, y + (lineHeight + fragment.shift) / 2);
|
|
3049
|
+
});
|
|
2764
3050
|
});
|
|
2765
3051
|
cursorX += fragment.occupiedWidth;
|
|
2766
3052
|
}
|
|
@@ -2839,23 +3125,83 @@ var MultilineText = class {
|
|
|
2839
3125
|
const spans = this.text;
|
|
2840
3126
|
const { width, lines } = getRichMultiLineDrawLayout(this, ctx, spans, this.options);
|
|
2841
3127
|
const align = resolvePhysicalTextAlign(this.options);
|
|
2842
|
-
const
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
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;
|
|
2857
3204
|
}
|
|
2858
|
-
y += this.options.lineHeight;
|
|
2859
3205
|
}
|
|
2860
3206
|
return false;
|
|
2861
3207
|
}
|
|
@@ -2863,30 +3209,76 @@ var MultilineText = class {
|
|
|
2863
3209
|
g.font = this.options.font;
|
|
2864
3210
|
g.fillStyle = ctx.resolveDynValue(this.options.color);
|
|
2865
3211
|
const { width, lines } = getMultiLineDrawLayout(this, ctx, this.text, this.options);
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
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
|
+
}
|
|
2879
3236
|
}
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
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
|
+
});
|
|
2890
3282
|
return false;
|
|
2891
3283
|
});
|
|
2892
3284
|
}
|
|
@@ -2949,7 +3341,9 @@ var Text = class {
|
|
|
2949
3341
|
g.font = this.options.font;
|
|
2950
3342
|
g.fillStyle = ctx.resolveDynValue(this.options.color);
|
|
2951
3343
|
const layout = getSingleLineLayout(this, ctx, text, this.options);
|
|
2952
|
-
g
|
|
3344
|
+
withTextSpacing(g, DEFAULT_TEXT_SPACING, () => {
|
|
3345
|
+
g.fillText(layout.text, x, y + (this.options.lineHeight + layout.shift) / 2);
|
|
3346
|
+
});
|
|
2953
3347
|
return false;
|
|
2954
3348
|
});
|
|
2955
3349
|
}
|
|
@@ -4066,6 +4460,6 @@ var TimelineRenderer = class extends VirtualizedRenderer {
|
|
|
4066
4460
|
}
|
|
4067
4461
|
};
|
|
4068
4462
|
//#endregion
|
|
4069
|
-
export { BaseRenderer, ChatRenderer, DebugRenderer, Fixed, Flex, FlexItem, Group, ListState, MultilineText, PaddingBox, Place, Text, TimelineRenderer, VirtualizedRenderer, Wrapper, memoRenderItem, memoRenderItemBy };
|
|
4463
|
+
export { BaseRenderer, ChatRenderer, DebugRenderer, Fixed, Flex, FlexItem, Group, ListState, MultilineText, PaddingBox, Place, ShrinkWrap, Text, TimelineRenderer, VirtualizedRenderer, Wrapper, memoRenderItem, memoRenderItemBy };
|
|
4070
4464
|
|
|
4071
4465
|
//# sourceMappingURL=index.mjs.map
|