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/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 pendingGapBefore = 0;
2284
- for (const atom of atoms) {
2285
- const occupiedWidth = atom.width + atom.extraWidthAfter;
2286
- if (atom.kind === "space" && !atom.preservesLineEnd && atom.atomicGroupId == null) {
2287
- pendingGapBefore += occupiedWidth;
2288
- continue;
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, pendingGapBefore) {
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 pendingGapBefore + occupiedWidth;
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
- if (previous != null && previous.itemIndex === atom.itemIndex && previous.font === font && pendingGapBefore === 0) {
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
- return 0;
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: pendingGapBefore,
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 0;
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 pendingGapBefore = 0;
2441
+ let pendingGap = {
2442
+ gapBefore: 0,
2443
+ gapAtomCount: 0,
2444
+ gapSpaceCount: 0
2445
+ };
2338
2446
  forEachAtomInRange(prepared, start, end, (atom) => {
2339
- pendingGapBefore = appendRichFragment(ctx, spans, defaultColor, fragments, atom, pendingGapBefore);
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 pendingGapBefore = 0;
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
- pendingGapBefore = appendRichFragment(ctx, spans, defaultColor, fragments, atom, pendingGapBefore);
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.fillText(fragment.text, cursorX, y + (lineHeight + fragment.shift) / 2);
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 startX = align === "right" ? x + width : align === "center" ? x + width / 2 : x;
2843
- for (const line of lines) {
2844
- let cursorX = startX;
2845
- for (let fi = 0; fi < line.fragments.length; fi++) {
2846
- const frag = line.fragments[fi];
2847
- cursorX += frag.gapBefore;
2848
- ctx.with((g) => {
2849
- g.font = frag.font;
2850
- g.fillStyle = ctx.resolveDynValue(frag.color ?? this.options.color);
2851
- if (align === "right") g.textAlign = "right";
2852
- else if (align === "center") g.textAlign = "center";
2853
- else g.textAlign = "left";
2854
- g.fillText(frag.text, cursorX, y + (this.options.lineHeight + frag.shift) / 2);
2855
- });
2856
- cursorX += frag.occupiedWidth;
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
- switch (resolvePhysicalTextAlign(this.options)) {
2867
- case "left":
2868
- for (const { text, shift } of lines) {
2869
- g.fillText(text, x, y + (this.options.lineHeight + shift) / 2);
2870
- y += this.options.lineHeight;
2871
- }
2872
- break;
2873
- case "right":
2874
- x += width;
2875
- g.textAlign = "right";
2876
- for (const { text, shift } of lines) {
2877
- g.fillText(text, x, y + (this.options.lineHeight + shift) / 2);
2878
- y += this.options.lineHeight;
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
- break;
2881
- case "center":
2882
- x += width / 2;
2883
- g.textAlign = "center";
2884
- for (const { text, shift } of lines) {
2885
- g.fillText(text, x, y + (this.options.lineHeight + shift) / 2);
2886
- y += this.options.lineHeight;
2887
- }
2888
- break;
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.fillText(layout.text, x, y + (this.options.lineHeight + layout.shift) / 2);
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