chat-layout 1.1.0-4 → 1.1.0

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
@@ -1,5 +1,5 @@
1
1
  import { layoutNextLine, layoutWithLines, measureLineStats, measureNaturalWidth, prepareWithSegments } from "@chenglou/pretext";
2
- import { materializeRichInlineLineRange, measureRichInlineStats, prepareRichInline, walkRichInlineLineRanges } from "@chenglou/pretext/rich-inline";
2
+ import { layoutNextRichInlineLineRange, materializeRichInlineLineRange, measureRichInlineStats, prepareRichInline, walkRichInlineLineRanges } from "@chenglou/pretext/rich-inline";
3
3
  //#region src/internal/node-registry.ts
4
4
  const registry = /* @__PURE__ */ new WeakMap();
5
5
  const revisions = /* @__PURE__ */ new WeakMap();
@@ -816,23 +816,10 @@ var Place = class extends Wrapper {
816
816
  });
817
817
  }
818
818
  };
819
- //#endregion
820
- //#region src/text.ts
821
- const FONT_SHIFT_PROBE = "M";
822
- const ELLIPSIS_GLYPH = "…";
823
819
  const INTRINSIC_MAX_WIDTH = Number.POSITIVE_INFINITY;
824
- const PREPARED_TEXT_CACHE_CAPACITY = 512;
825
- const FONT_SHIFT_CACHE_CAPACITY = 64;
826
- const ELLIPSIS_WIDTH_CACHE_CAPACITY = 64;
827
- const LINE_START_CURSOR = {
828
- segmentIndex: 0,
829
- graphemeIndex: 0
830
- };
831
820
  const MIN_CONTENT_WIDTH_EPSILON = .001;
832
- const preparedTextCache = /* @__PURE__ */ new Map();
833
821
  const fontShiftCache = /* @__PURE__ */ new Map();
834
822
  const ellipsisWidthCache = /* @__PURE__ */ new Map();
835
- const preparedUnitCache = /* @__PURE__ */ new WeakMap();
836
823
  let sharedGraphemeSegmenter;
837
824
  function readLruValue(cache, key) {
838
825
  const cached = cache.get(key);
@@ -850,6 +837,106 @@ function writeLruValue(cache, key, value, capacity) {
850
837
  cache.set(key, value);
851
838
  return value;
852
839
  }
840
+ function measureFontShift(ctx) {
841
+ const font = ctx.graphics.font;
842
+ const cached = readLruValue(fontShiftCache, font);
843
+ if (cached != null) return cached;
844
+ const { fontBoundingBoxAscent: ascent = 0, fontBoundingBoxDescent: descent = 0 } = ctx.graphics.measureText("M");
845
+ return writeLruValue(fontShiftCache, font, ascent - descent, 64);
846
+ }
847
+ function measureEllipsisWidth(ctx) {
848
+ const font = ctx.graphics.font;
849
+ const cached = readLruValue(ellipsisWidthCache, font);
850
+ if (cached != null) return cached;
851
+ return writeLruValue(ellipsisWidthCache, font, ctx.graphics.measureText("…").width, 64);
852
+ }
853
+ function getGraphemeSegmenter() {
854
+ if (sharedGraphemeSegmenter !== void 0) return sharedGraphemeSegmenter;
855
+ sharedGraphemeSegmenter = typeof Intl.Segmenter === "function" ? new Intl.Segmenter(void 0, { granularity: "grapheme" }) : null;
856
+ return sharedGraphemeSegmenter;
857
+ }
858
+ function splitGraphemes(text) {
859
+ const segmenter = getGraphemeSegmenter();
860
+ if (segmenter == null) return Array.from(text);
861
+ const graphemes = [];
862
+ for (const part of segmenter.segment(text)) graphemes.push(part.segment);
863
+ return graphemes;
864
+ }
865
+ function buildPrefixWidths(widths) {
866
+ const cumulativeWidths = [0];
867
+ let total = 0;
868
+ for (const width of widths) {
869
+ total += width;
870
+ cumulativeWidths.push(total);
871
+ }
872
+ return cumulativeWidths;
873
+ }
874
+ function buildSuffixWidths(widths) {
875
+ const cumulativeWidths = [0];
876
+ let total = 0;
877
+ for (let index = widths.length - 1; index >= 0; index -= 1) {
878
+ total += widths[index] ?? 0;
879
+ cumulativeWidths.push(total);
880
+ }
881
+ return cumulativeWidths;
882
+ }
883
+ function findMaxFittingCount(cumulativeWidths, maxWidth) {
884
+ if (maxWidth <= 0) return 0;
885
+ let low = 0;
886
+ let high = cumulativeWidths.length - 1;
887
+ while (low < high) {
888
+ const mid = Math.floor((low + high + 1) / 2);
889
+ if ((cumulativeWidths[mid] ?? 0) <= maxWidth) low = mid;
890
+ else high = mid - 1;
891
+ }
892
+ return low;
893
+ }
894
+ function normalizeMaxLines(maxLines) {
895
+ if (maxLines == null || !Number.isFinite(maxLines)) return;
896
+ return Math.max(1, Math.trunc(maxLines));
897
+ }
898
+ function selectEllipsisUnitCounts({ position, prefixWidths, suffixWidths, unitCount, availableWidth, getMaxSuffixCount = (prefixCount) => unitCount - prefixCount }) {
899
+ let prefixCount = 0;
900
+ let suffixCount = 0;
901
+ switch (position) {
902
+ case "start":
903
+ suffixCount = Math.min(unitCount, findMaxFittingCount(suffixWidths, availableWidth));
904
+ break;
905
+ case "middle": {
906
+ let bestVisibleUnits = -1;
907
+ let bestBalanceScore = Number.NEGATIVE_INFINITY;
908
+ for (let nextPrefixCount = 0; nextPrefixCount <= unitCount; nextPrefixCount += 1) {
909
+ const prefixWidth = prefixWidths[nextPrefixCount] ?? 0;
910
+ if (prefixWidth > availableWidth) break;
911
+ const remainingWidth = availableWidth - prefixWidth;
912
+ const maxSuffixCount = Math.max(0, getMaxSuffixCount(nextPrefixCount));
913
+ const nextSuffixCount = Math.min(maxSuffixCount, findMaxFittingCount(suffixWidths, remainingWidth));
914
+ const visibleUnits = nextPrefixCount + nextSuffixCount;
915
+ const balanceScore = -Math.abs(nextPrefixCount - nextSuffixCount);
916
+ if (visibleUnits > bestVisibleUnits || visibleUnits === bestVisibleUnits && balanceScore > bestBalanceScore || visibleUnits === bestVisibleUnits && balanceScore === bestBalanceScore && nextPrefixCount > prefixCount) {
917
+ prefixCount = nextPrefixCount;
918
+ suffixCount = nextSuffixCount;
919
+ bestVisibleUnits = visibleUnits;
920
+ bestBalanceScore = balanceScore;
921
+ }
922
+ }
923
+ break;
924
+ }
925
+ case "end":
926
+ prefixCount = Math.min(unitCount, findMaxFittingCount(prefixWidths, availableWidth));
927
+ break;
928
+ }
929
+ return {
930
+ prefixCount,
931
+ suffixCount
932
+ };
933
+ }
934
+ const LINE_START_CURSOR$1 = {
935
+ segmentIndex: 0,
936
+ graphemeIndex: 0
937
+ };
938
+ const preparedTextCache = /* @__PURE__ */ new Map();
939
+ const preparedUnitCache = /* @__PURE__ */ new WeakMap();
853
940
  function getPreparedTextCacheKey(text, font, whiteSpace, wordBreak) {
854
941
  return `${font}\u0000${whiteSpace}\u0000${wordBreak}\u0000${text}`;
855
942
  }
@@ -860,64 +947,45 @@ function readPreparedText(text, font, whiteSpace, wordBreak) {
860
947
  return writeLruValue(preparedTextCache, key, prepareWithSegments(text, font, {
861
948
  whiteSpace,
862
949
  wordBreak
863
- }), PREPARED_TEXT_CACHE_CAPACITY);
950
+ }), 512);
864
951
  }
865
952
  function readPreparedFirstLine(ctx, text, whiteSpace, wordBreak) {
866
- const line = layoutNextLine(readPreparedText(text, ctx.graphics.font, whiteSpace, wordBreak), LINE_START_CURSOR, INTRINSIC_MAX_WIDTH);
953
+ const line = layoutNextLine(readPreparedText(text, ctx.graphics.font, whiteSpace, wordBreak), LINE_START_CURSOR$1, INTRINSIC_MAX_WIDTH);
867
954
  if (line == null) return;
868
955
  return {
869
956
  text: line.text,
870
957
  prepared: readPreparedText(line.text, ctx.graphics.font, whiteSpace, wordBreak)
871
958
  };
872
959
  }
873
- function measureFontShift(ctx) {
874
- const font = ctx.graphics.font;
875
- const cached = readLruValue(fontShiftCache, font);
876
- if (cached != null) return cached;
877
- const { fontBoundingBoxAscent: ascent = 0, fontBoundingBoxDescent: descent = 0 } = ctx.graphics.measureText(FONT_SHIFT_PROBE);
878
- return writeLruValue(fontShiftCache, font, ascent - descent, FONT_SHIFT_CACHE_CAPACITY);
879
- }
880
960
  function measurePreparedMinContentWidth(prepared, overflowWrap = "break-word") {
881
961
  let maxWidth = 0;
882
962
  let maxAnyWidth = 0;
883
- for (let i = 0; i < prepared.widths.length; i += 1) {
884
- const segmentWidth = prepared.widths[i] ?? 0;
963
+ for (let index = 0; index < prepared.widths.length; index += 1) {
964
+ const segmentWidth = prepared.widths[index] ?? 0;
885
965
  maxAnyWidth = Math.max(maxAnyWidth, segmentWidth);
886
- const segment = prepared.segments[i];
966
+ const segment = prepared.segments[index];
887
967
  if (segment != null && segment.trim().length > 0) {
888
- const breakableWidths = prepared.breakableFitAdvances[i];
968
+ const breakableWidths = prepared.breakableFitAdvances[index];
889
969
  const minContentWidth = overflowWrap === "anywhere" && breakableWidths != null && breakableWidths.length > 0 ? breakableWidths.reduce((widest, width) => Math.max(widest, width), 0) : segmentWidth;
890
970
  maxWidth = Math.max(maxWidth, minContentWidth);
891
971
  }
892
972
  }
893
973
  return maxWidth > 0 ? maxWidth : maxAnyWidth;
894
974
  }
895
- function getGraphemeSegmenter() {
896
- if (sharedGraphemeSegmenter !== void 0) return sharedGraphemeSegmenter;
897
- sharedGraphemeSegmenter = typeof Intl.Segmenter === "function" ? new Intl.Segmenter(void 0, { granularity: "grapheme" }) : null;
898
- return sharedGraphemeSegmenter;
899
- }
900
- function splitGraphemes(text) {
901
- const segmenter = getGraphemeSegmenter();
902
- if (segmenter == null) return Array.from(text);
903
- const graphemes = [];
904
- for (const part of segmenter.segment(text)) graphemes.push(part.segment);
905
- return graphemes;
906
- }
907
975
  function getPreparedUnits(prepared) {
908
976
  const cached = preparedUnitCache.get(prepared);
909
977
  if (cached != null) return cached;
910
978
  const units = [];
911
- for (let i = 0; i < prepared.segments.length; i += 1) {
912
- const segment = prepared.segments[i] ?? "";
913
- const segmentWidth = prepared.widths[i] ?? 0;
914
- const breakableWidths = prepared.breakableFitAdvances[i];
979
+ for (let index = 0; index < prepared.segments.length; index += 1) {
980
+ const segment = prepared.segments[index] ?? "";
981
+ const segmentWidth = prepared.widths[index] ?? 0;
982
+ const breakableWidths = prepared.breakableFitAdvances[index];
915
983
  if (breakableWidths != null && segment.length > 0) {
916
984
  const graphemes = splitGraphemes(segment);
917
985
  if (graphemes.length === breakableWidths.length) {
918
- for (let j = 0; j < graphemes.length; j += 1) units.push({
919
- text: graphemes[j] ?? "",
920
- width: breakableWidths[j] ?? 0
986
+ for (let graphemeIndex = 0; graphemeIndex < graphemes.length; graphemeIndex += 1) units.push({
987
+ text: graphemes[graphemeIndex] ?? "",
988
+ width: breakableWidths[graphemeIndex] ?? 0
921
989
  });
922
990
  continue;
923
991
  }
@@ -930,44 +998,18 @@ function getPreparedUnits(prepared) {
930
998
  preparedUnitCache.set(prepared, units);
931
999
  return units;
932
1000
  }
933
- function buildUnitPrefixWidths(units) {
934
- const widths = [0];
935
- let total = 0;
936
- for (const unit of units) {
937
- total += unit.width;
938
- widths.push(total);
939
- }
940
- return widths;
941
- }
942
- function buildUnitSuffixWidths(units) {
943
- const widths = [0];
944
- let total = 0;
945
- for (let i = units.length - 1; i >= 0; i -= 1) {
946
- total += units[i]?.width ?? 0;
947
- widths.push(total);
948
- }
949
- return widths;
950
- }
951
- function findMaxFittingCount(cumulativeWidths, maxWidth) {
952
- if (maxWidth <= 0) return 0;
953
- let low = 0;
954
- let high = cumulativeWidths.length - 1;
955
- while (low < high) {
956
- const mid = Math.floor((low + high + 1) / 2);
957
- if ((cumulativeWidths[mid] ?? 0) <= maxWidth) low = mid;
958
- else high = mid - 1;
959
- }
960
- return low;
961
- }
962
1001
  function joinUnitText(units, start, end) {
963
1002
  if (start >= end) return "";
964
1003
  return units.slice(start, end).map((unit) => unit.text).join("");
965
1004
  }
966
- function measureEllipsisWidth(ctx) {
967
- const font = ctx.graphics.font;
968
- const cached = readLruValue(ellipsisWidthCache, font);
969
- if (cached != null) return cached;
970
- return writeLruValue(ellipsisWidthCache, font, ctx.graphics.measureText(ELLIPSIS_GLYPH).width, ELLIPSIS_WIDTH_CACHE_CAPACITY);
1005
+ //#endregion
1006
+ //#region src/text/plain.ts
1007
+ const LINE_START_CURSOR = {
1008
+ segmentIndex: 0,
1009
+ graphemeIndex: 0
1010
+ };
1011
+ function clampMaxWidth(maxWidth) {
1012
+ return Math.max(0, maxWidth);
971
1013
  }
972
1014
  function createEllipsisOnlyLayout(ctx, maxWidth, shift) {
973
1015
  const ellipsisWidth = measureEllipsisWidth(ctx);
@@ -979,11 +1021,22 @@ function createEllipsisOnlyLayout(ctx, maxWidth, shift) {
979
1021
  };
980
1022
  return {
981
1023
  width: ellipsisWidth,
982
- text: ELLIPSIS_GLYPH,
1024
+ text: "…",
983
1025
  shift,
984
1026
  overflowed: true
985
1027
  };
986
1028
  }
1029
+ function toTextBlockLayout(lines, shift) {
1030
+ const mappedLines = lines.map((line) => ({
1031
+ width: line.width,
1032
+ text: line.text,
1033
+ shift
1034
+ }));
1035
+ return {
1036
+ width: mappedLines.reduce((maxLineWidth, line) => Math.max(maxLineWidth, line.width), 0),
1037
+ lines: mappedLines
1038
+ };
1039
+ }
987
1040
  function layoutPreparedEllipsis(ctx, prepared, text, maxWidth, shift, position, forceEllipsis = false) {
988
1041
  const intrinsicWidth = measureNaturalWidth(prepared);
989
1042
  if (!forceEllipsis && intrinsicWidth <= maxWidth) return {
@@ -1002,52 +1055,29 @@ function layoutPreparedEllipsis(ctx, prepared, text, maxWidth, shift, position,
1002
1055
  const units = getPreparedUnits(prepared);
1003
1056
  if (units.length === 0) return createEllipsisOnlyLayout(ctx, maxWidth, shift);
1004
1057
  const availableWidth = Math.max(0, maxWidth - ellipsisWidth);
1005
- const prefixWidths = buildUnitPrefixWidths(units);
1006
- const suffixWidths = buildUnitSuffixWidths(units);
1007
- let prefixCount = 0;
1008
- let suffixCount = 0;
1009
- switch (position) {
1010
- case "start":
1011
- suffixCount = Math.min(units.length, findMaxFittingCount(suffixWidths, availableWidth));
1012
- break;
1013
- case "middle": {
1014
- let bestVisibleUnits = -1;
1015
- let bestBalanceScore = Number.NEGATIVE_INFINITY;
1016
- for (let nextPrefixCount = 0; nextPrefixCount <= units.length; nextPrefixCount += 1) {
1017
- const prefixWidth = prefixWidths[nextPrefixCount] ?? 0;
1018
- if (prefixWidth > availableWidth) break;
1019
- const remainingWidth = availableWidth - prefixWidth;
1020
- const maxSuffixCount = units.length - nextPrefixCount;
1021
- const nextSuffixCount = Math.min(maxSuffixCount, findMaxFittingCount(suffixWidths, remainingWidth));
1022
- const visibleUnits = nextPrefixCount + nextSuffixCount;
1023
- const balanceScore = -Math.abs(nextPrefixCount - nextSuffixCount);
1024
- if (visibleUnits > bestVisibleUnits || visibleUnits === bestVisibleUnits && balanceScore > bestBalanceScore || visibleUnits === bestVisibleUnits && balanceScore === bestBalanceScore && nextPrefixCount > prefixCount) {
1025
- prefixCount = nextPrefixCount;
1026
- suffixCount = nextSuffixCount;
1027
- bestVisibleUnits = visibleUnits;
1028
- bestBalanceScore = balanceScore;
1029
- }
1030
- }
1031
- break;
1032
- }
1033
- case "end":
1034
- prefixCount = Math.min(units.length, findMaxFittingCount(prefixWidths, availableWidth));
1035
- break;
1036
- }
1058
+ const prefixWidths = buildPrefixWidths(units.map((unit) => unit.width));
1059
+ const suffixWidths = buildSuffixWidths(units.map((unit) => unit.width));
1060
+ const { prefixCount, suffixCount } = selectEllipsisUnitCounts({
1061
+ position,
1062
+ prefixWidths,
1063
+ suffixWidths,
1064
+ unitCount: units.length,
1065
+ availableWidth
1066
+ });
1037
1067
  const prefixWidth = prefixWidths[prefixCount] ?? 0;
1038
1068
  const suffixWidth = suffixWidths[suffixCount] ?? 0;
1039
1069
  const prefixText = joinUnitText(units, 0, prefixCount);
1040
1070
  const suffixText = joinUnitText(units, units.length - suffixCount, units.length);
1041
1071
  return {
1042
1072
  width: prefixWidth + ellipsisWidth + suffixWidth,
1043
- text: `${prefixText}${ELLIPSIS_GLYPH}${suffixText}`,
1073
+ text: `${prefixText}…${suffixText}`,
1044
1074
  shift,
1045
1075
  overflowed: true
1046
1076
  };
1047
1077
  }
1048
- function normalizeMaxLines(maxLines) {
1049
- if (maxLines == null || !Number.isFinite(maxLines)) return;
1050
- return Math.max(1, Math.trunc(maxLines));
1078
+ function layoutForcedEllipsizedLine(ctx, text, maxWidth, shift, whiteSpace = "normal", wordBreak = "normal") {
1079
+ if (text.length === 0) return createEllipsisOnlyLayout(ctx, maxWidth, shift);
1080
+ return layoutPreparedEllipsis(ctx, readPreparedText(text, ctx.graphics.font, whiteSpace, wordBreak), text, maxWidth, shift, "end", true);
1051
1081
  }
1052
1082
  function layoutFirstLineIntrinsic(ctx, text, whiteSpace = "normal", wordBreak = "normal") {
1053
1083
  const firstLine = readPreparedFirstLine(ctx, text, whiteSpace, wordBreak);
@@ -1080,26 +1110,17 @@ function layoutTextIntrinsic(ctx, text, whiteSpace = "normal", wordBreak = "norm
1080
1110
  width: 0,
1081
1111
  lines: []
1082
1112
  };
1083
- const shift = measureFontShift(ctx);
1084
- const lines = intrinsic.lines.map((line) => ({
1085
- width: line.width,
1086
- text: line.text,
1087
- shift
1088
- }));
1089
- return {
1090
- width: lines.reduce((maxLineWidth, line) => Math.max(maxLineWidth, line.width), 0),
1091
- lines
1092
- };
1113
+ return toTextBlockLayout(intrinsic.lines, measureFontShift(ctx));
1093
1114
  }
1094
1115
  function layoutFirstLine(ctx, text, maxWidth, whiteSpace = "normal", wordBreak = "normal") {
1095
- if (maxWidth < 0) maxWidth = 0;
1116
+ const clampedMaxWidth = clampMaxWidth(maxWidth);
1096
1117
  const shift = measureFontShift(ctx);
1097
- if (maxWidth === 0) return {
1118
+ if (clampedMaxWidth === 0) return {
1098
1119
  width: 0,
1099
1120
  text: "",
1100
1121
  shift
1101
1122
  };
1102
- const line = layoutNextLine(readPreparedText(text, ctx.graphics.font, whiteSpace, wordBreak), LINE_START_CURSOR, maxWidth);
1123
+ const line = layoutNextLine(readPreparedText(text, ctx.graphics.font, whiteSpace, wordBreak), LINE_START_CURSOR, clampedMaxWidth);
1103
1124
  if (line == null) return {
1104
1125
  width: 0,
1105
1126
  text: "",
@@ -1112,7 +1133,7 @@ function layoutFirstLine(ctx, text, maxWidth, whiteSpace = "normal", wordBreak =
1112
1133
  };
1113
1134
  }
1114
1135
  function layoutEllipsizedFirstLine(ctx, text, maxWidth, ellipsisPosition = "end", whiteSpace = "normal", wordBreak = "normal") {
1115
- if (maxWidth < 0) maxWidth = 0;
1136
+ const clampedMaxWidth = clampMaxWidth(maxWidth);
1116
1137
  const firstLine = readPreparedFirstLine(ctx, text, whiteSpace, wordBreak);
1117
1138
  if (firstLine == null) return {
1118
1139
  width: 0,
@@ -1121,25 +1142,21 @@ function layoutEllipsizedFirstLine(ctx, text, maxWidth, ellipsisPosition = "end"
1121
1142
  overflowed: false
1122
1143
  };
1123
1144
  const shift = measureFontShift(ctx);
1124
- if (maxWidth === 0) return {
1145
+ if (clampedMaxWidth === 0) return {
1125
1146
  width: 0,
1126
1147
  text: "",
1127
1148
  shift,
1128
1149
  overflowed: true
1129
1150
  };
1130
- return layoutPreparedEllipsis(ctx, firstLine.prepared, firstLine.text, maxWidth, shift, ellipsisPosition);
1131
- }
1132
- function layoutForcedEllipsizedLine(ctx, text, maxWidth, shift, whiteSpace = "normal", wordBreak = "normal") {
1133
- if (text.length === 0) return createEllipsisOnlyLayout(ctx, maxWidth, shift);
1134
- return layoutPreparedEllipsis(ctx, readPreparedText(text, ctx.graphics.font, whiteSpace, wordBreak), text, maxWidth, shift, "end", true);
1151
+ return layoutPreparedEllipsis(ctx, firstLine.prepared, firstLine.text, clampedMaxWidth, shift, ellipsisPosition);
1135
1152
  }
1136
1153
  function measureText(ctx, text, maxWidth, whiteSpace = "normal", wordBreak = "normal") {
1137
- if (maxWidth < 0) maxWidth = 0;
1138
- if (maxWidth === 0) return {
1154
+ const clampedMaxWidth = clampMaxWidth(maxWidth);
1155
+ if (clampedMaxWidth === 0) return {
1139
1156
  width: 0,
1140
1157
  lineCount: 0
1141
1158
  };
1142
- const { maxLineWidth: width, lineCount } = measureLineStats(readPreparedText(text, ctx.graphics.font, whiteSpace, wordBreak), maxWidth);
1159
+ const { maxLineWidth: width, lineCount } = measureLineStats(readPreparedText(text, ctx.graphics.font, whiteSpace, wordBreak), clampedMaxWidth);
1143
1160
  return {
1144
1161
  width,
1145
1162
  lineCount
@@ -1159,33 +1176,25 @@ function measureTextMinContent(ctx, text, whiteSpace = "normal", wordBreak = "no
1159
1176
  };
1160
1177
  }
1161
1178
  function layoutText(ctx, text, maxWidth, whiteSpace = "normal", wordBreak = "normal") {
1162
- if (maxWidth < 0) maxWidth = 0;
1163
- if (maxWidth === 0) return {
1179
+ const clampedMaxWidth = clampMaxWidth(maxWidth);
1180
+ if (clampedMaxWidth === 0) return {
1164
1181
  width: 0,
1165
1182
  lines: []
1166
1183
  };
1167
- const layout = layoutWithLines(readPreparedText(text, ctx.graphics.font, whiteSpace, wordBreak), maxWidth, 0);
1184
+ const layout = layoutWithLines(readPreparedText(text, ctx.graphics.font, whiteSpace, wordBreak), clampedMaxWidth, 0);
1168
1185
  if (layout.lines.length === 0) return {
1169
1186
  width: 0,
1170
1187
  lines: []
1171
1188
  };
1172
- const shift = measureFontShift(ctx);
1173
- const lines = layout.lines.map((line) => ({
1174
- width: line.width,
1175
- text: line.text,
1176
- shift
1177
- }));
1178
- return {
1179
- width: lines.reduce((maxLineWidth, line) => Math.max(maxLineWidth, line.width), 0),
1180
- lines
1181
- };
1189
+ return toTextBlockLayout(layout.lines, measureFontShift(ctx));
1182
1190
  }
1183
1191
  function layoutTextWithOverflow(ctx, text, maxWidth, options = {}) {
1192
+ const clampedMaxWidth = clampMaxWidth(maxWidth);
1184
1193
  const whiteSpace = options.whiteSpace ?? "normal";
1185
1194
  const wordBreak = options.wordBreak ?? "normal";
1186
1195
  const overflow = options.overflow ?? "clip";
1187
1196
  const normalizedMaxLines = normalizeMaxLines(options.maxLines);
1188
- const layout = layoutText(ctx, text, maxWidth, whiteSpace, wordBreak);
1197
+ const layout = layoutText(ctx, text, clampedMaxWidth, whiteSpace, wordBreak);
1189
1198
  if (normalizedMaxLines == null || layout.lines.length <= normalizedMaxLines) return {
1190
1199
  width: layout.width,
1191
1200
  lines: layout.lines.map((line) => ({
@@ -1205,7 +1214,7 @@ function layoutTextWithOverflow(ctx, text, maxWidth, options = {}) {
1205
1214
  };
1206
1215
  const shift = visibleLines[visibleLines.length - 1]?.shift ?? measureFontShift(ctx);
1207
1216
  const lastVisibleLine = visibleLines[visibleLines.length - 1];
1208
- const ellipsizedLastLine = lastVisibleLine == null || lastVisibleLine.text.length === 0 ? createEllipsisOnlyLayout(ctx, Math.max(0, maxWidth), shift) : layoutForcedEllipsizedLine(ctx, lastVisibleLine.text, maxWidth, shift, whiteSpace, wordBreak);
1217
+ const ellipsizedLastLine = lastVisibleLine == null || lastVisibleLine.text.length === 0 ? createEllipsisOnlyLayout(ctx, clampedMaxWidth, shift) : layoutForcedEllipsizedLine(ctx, lastVisibleLine.text, clampedMaxWidth, shift, whiteSpace, wordBreak);
1209
1218
  const lines = [...visibleLines.slice(0, -1).map((line) => ({
1210
1219
  ...line,
1211
1220
  overflowed: false
@@ -1219,40 +1228,50 @@ function layoutTextWithOverflow(ctx, text, maxWidth, options = {}) {
1219
1228
  overflowed: true
1220
1229
  };
1221
1230
  }
1231
+ //#endregion
1232
+ //#region src/text/rich.ts
1222
1233
  const RICH_PREPARED_CACHE_CAPACITY = 256;
1223
1234
  const richPreparedCache = /* @__PURE__ */ new Map();
1235
+ function withFont(ctx, font, cb) {
1236
+ const previousFont = ctx.graphics.font;
1237
+ ctx.graphics.font = font;
1238
+ try {
1239
+ return cb();
1240
+ } finally {
1241
+ ctx.graphics.font = previousFont;
1242
+ }
1243
+ }
1224
1244
  function getRichPreparedCacheKey(spans, defaultFont) {
1225
- return spans.map((s) => `${s.font ?? defaultFont}\u0000${s.text}\u0000${s.break ?? ""}\u0000${s.extraWidth ?? 0}`).join("");
1245
+ return spans.map((span) => `${span.font ?? defaultFont}\u0000${span.text}\u0000${span.break ?? ""}\u0000${span.extraWidth ?? 0}`).join("");
1226
1246
  }
1227
1247
  function readRichPrepared(spans, defaultFont) {
1228
1248
  const key = getRichPreparedCacheKey(spans, defaultFont);
1229
1249
  const cached = readLruValue(richPreparedCache, key);
1230
1250
  if (cached != null) return cached;
1231
- return writeLruValue(richPreparedCache, key, prepareRichInline(spans.map((s) => ({
1232
- text: s.text,
1233
- font: s.font ?? defaultFont,
1234
- break: s.break,
1235
- extraWidth: s.extraWidth
1251
+ return writeLruValue(richPreparedCache, key, prepareRichInline(spans.map((span) => ({
1252
+ text: span.text,
1253
+ font: span.font ?? defaultFont,
1254
+ break: span.break,
1255
+ extraWidth: span.extraWidth
1236
1256
  }))), RICH_PREPARED_CACHE_CAPACITY);
1237
1257
  }
1238
- function materializeRichLine(ctx, spans, defaultFont, defaultStyle, lineRange, overflowed) {
1258
+ function measureRichFragmentShift(ctx, font) {
1259
+ return withFont(ctx, font, () => measureFontShift(ctx));
1260
+ }
1261
+ function materializeRichLine(ctx, spans, defaultFont, defaultColor, lineRange, overflowed) {
1239
1262
  const richLine = materializeRichInlineLineRange(readRichPrepared(spans, defaultFont), lineRange);
1240
- const fragments = richLine.fragments.map((frag) => {
1241
- const span = spans[frag.itemIndex];
1242
- const fragFont = span?.font ?? defaultFont;
1243
- const fragStyle = span?.style ?? defaultStyle;
1244
- const prevFont = ctx.graphics.font;
1245
- ctx.graphics.font = fragFont;
1246
- const shift = measureFontShift(ctx);
1247
- ctx.graphics.font = prevFont;
1263
+ const fragments = richLine.fragments.map((fragment) => {
1264
+ const span = spans[fragment.itemIndex];
1265
+ const font = span?.font ?? defaultFont;
1266
+ const color = span?.color ?? defaultColor;
1248
1267
  return {
1249
- itemIndex: frag.itemIndex,
1250
- text: frag.text,
1251
- font: fragFont,
1252
- style: fragStyle,
1253
- gapBefore: frag.gapBefore,
1254
- occupiedWidth: frag.occupiedWidth,
1255
- shift
1268
+ itemIndex: fragment.itemIndex,
1269
+ text: fragment.text,
1270
+ font,
1271
+ color,
1272
+ gapBefore: fragment.gapBefore,
1273
+ occupiedWidth: fragment.occupiedWidth,
1274
+ shift: measureRichFragmentShift(ctx, font)
1256
1275
  };
1257
1276
  });
1258
1277
  return {
@@ -1261,7 +1280,176 @@ function materializeRichLine(ctx, spans, defaultFont, defaultStyle, lineRange, o
1261
1280
  overflowed
1262
1281
  };
1263
1282
  }
1264
- function measureRichText(ctx, spans, maxWidth, defaultFont) {
1283
+ function flattenRichLineUnits(line) {
1284
+ const units = [];
1285
+ for (let fragmentIndex = 0; fragmentIndex < line.fragments.length; fragmentIndex += 1) {
1286
+ const fragment = line.fragments[fragmentIndex];
1287
+ const fragmentUnits = getPreparedUnits(readPreparedText(fragment.text, fragment.font, "normal", "normal"));
1288
+ if (fragmentUnits.length === 0) continue;
1289
+ const textWidth = fragmentUnits.reduce((total, unit) => total + unit.width, 0);
1290
+ const trailingExtraWidth = Math.max(0, fragment.occupiedWidth - textWidth);
1291
+ for (let unitIndex = 0; unitIndex < fragmentUnits.length; unitIndex += 1) {
1292
+ const unit = fragmentUnits[unitIndex];
1293
+ units.push({
1294
+ fragmentIndex,
1295
+ itemIndex: fragment.itemIndex,
1296
+ text: unit.text,
1297
+ width: unit.width + (unitIndex === fragmentUnits.length - 1 ? trailingExtraWidth : 0),
1298
+ font: fragment.font,
1299
+ color: fragment.color,
1300
+ leadingGap: unitIndex === 0 ? fragment.gapBefore : 0
1301
+ });
1302
+ }
1303
+ }
1304
+ return units;
1305
+ }
1306
+ function buildRichPrefixWidths(units) {
1307
+ return buildPrefixWidths(units.map((unit) => unit.leadingGap + unit.width));
1308
+ }
1309
+ function buildRichSuffixWidths(units) {
1310
+ const widths = [0];
1311
+ let total = 0;
1312
+ for (let index = units.length - 1; index >= 0; index -= 1) {
1313
+ const unit = units[index];
1314
+ total += unit.width;
1315
+ if (widths.length > 1) total += unit.leadingGap;
1316
+ widths.push(total);
1317
+ }
1318
+ return widths;
1319
+ }
1320
+ function materializeRichFragmentsFromUnits(units, start, end, suppressLeadingGap) {
1321
+ const fragments = [];
1322
+ for (let index = start; index < end; index += 1) {
1323
+ const unit = units[index];
1324
+ const previous = fragments[fragments.length - 1];
1325
+ const previousUnit = units[index - 1];
1326
+ if (previous != null && previousUnit != null && previousUnit.fragmentIndex === unit.fragmentIndex) {
1327
+ previous.text += unit.text;
1328
+ previous.occupiedWidth += unit.width;
1329
+ continue;
1330
+ }
1331
+ fragments.push({
1332
+ itemIndex: unit.itemIndex,
1333
+ text: unit.text,
1334
+ font: unit.font,
1335
+ color: unit.color,
1336
+ gapBefore: fragments.length === 0 && suppressLeadingGap ? 0 : unit.leadingGap,
1337
+ occupiedWidth: unit.width,
1338
+ shift: 0
1339
+ });
1340
+ }
1341
+ return fragments;
1342
+ }
1343
+ function measureRichFragmentsShift(ctx, fragments) {
1344
+ return fragments.map((fragment) => ({
1345
+ ...fragment,
1346
+ shift: measureRichFragmentShift(ctx, fragment.font)
1347
+ }));
1348
+ }
1349
+ function createRichEllipsisFragment(ctx, font, color) {
1350
+ return withFont(ctx, font, () => ({
1351
+ itemIndex: -1,
1352
+ text: "…",
1353
+ font,
1354
+ color,
1355
+ gapBefore: 0,
1356
+ occupiedWidth: measureEllipsisWidth(ctx),
1357
+ shift: measureFontShift(ctx)
1358
+ }));
1359
+ }
1360
+ function createRichEllipsisOnlyLayout(ctx, maxWidth, font, color) {
1361
+ const ellipsis = createRichEllipsisFragment(ctx, font, color);
1362
+ if (ellipsis.occupiedWidth > maxWidth) return {
1363
+ width: 0,
1364
+ fragments: [],
1365
+ overflowed: true
1366
+ };
1367
+ return {
1368
+ width: ellipsis.occupiedWidth,
1369
+ fragments: [ellipsis],
1370
+ overflowed: true
1371
+ };
1372
+ }
1373
+ function layoutPreparedRichEllipsis(ctx, line, maxWidth, defaultFont, defaultColor, position) {
1374
+ if (!line.overflowed && line.width <= maxWidth) return {
1375
+ ...line,
1376
+ overflowed: false
1377
+ };
1378
+ const units = flattenRichLineUnits(line);
1379
+ const fallbackFragment = line.fragments[0];
1380
+ const ellipsisOnly = createRichEllipsisOnlyLayout(ctx, maxWidth, fallbackFragment?.font ?? defaultFont, fallbackFragment?.color ?? defaultColor);
1381
+ if (ellipsisOnly.fragments.length === 0 || units.length === 0) return ellipsisOnly;
1382
+ const ellipsisWidth = ellipsisOnly.width;
1383
+ const availableWidth = Math.max(0, maxWidth - ellipsisWidth);
1384
+ const prefixWidths = buildRichPrefixWidths(units);
1385
+ const suffixWidths = buildRichSuffixWidths(units);
1386
+ const { prefixCount, suffixCount } = selectEllipsisUnitCounts({
1387
+ position,
1388
+ prefixWidths,
1389
+ suffixWidths,
1390
+ unitCount: units.length,
1391
+ availableWidth,
1392
+ getMaxSuffixCount: position === "middle" ? (nextPrefixCount) => Math.max(0, units.length - nextPrefixCount - 1) : void 0
1393
+ });
1394
+ const prefixFragments = measureRichFragmentsShift(ctx, materializeRichFragmentsFromUnits(units, 0, prefixCount, false));
1395
+ const suffixFragments = measureRichFragmentsShift(ctx, materializeRichFragmentsFromUnits(units, units.length - suffixCount, units.length, true));
1396
+ const ellipsisSource = position === "start" ? suffixFragments[0] ?? line.fragments[0] : position === "middle" ? prefixFragments[prefixFragments.length - 1] ?? suffixFragments[0] ?? line.fragments[line.fragments.length - 1] : prefixFragments[prefixFragments.length - 1] ?? line.fragments[line.fragments.length - 1];
1397
+ const ellipsis = createRichEllipsisFragment(ctx, ellipsisSource?.font ?? defaultFont, ellipsisSource?.color ?? defaultColor);
1398
+ const fragments = position === "start" ? [ellipsis, ...suffixFragments] : position === "middle" ? [
1399
+ ...prefixFragments,
1400
+ ellipsis,
1401
+ ...suffixFragments
1402
+ ] : [...prefixFragments, ellipsis];
1403
+ return {
1404
+ width: position === "start" ? ellipsis.occupiedWidth + (suffixWidths[suffixCount] ?? 0) : position === "middle" ? (prefixWidths[prefixCount] ?? 0) + ellipsis.occupiedWidth + (suffixWidths[suffixCount] ?? 0) : (prefixWidths[prefixCount] ?? 0) + ellipsis.occupiedWidth,
1405
+ fragments,
1406
+ overflowed: true
1407
+ };
1408
+ }
1409
+ function layoutRichFirstLineIntrinsic(ctx, spans, defaultFont, defaultColor) {
1410
+ if (spans.length === 0) return {
1411
+ width: 0,
1412
+ fragments: [],
1413
+ overflowed: false
1414
+ };
1415
+ const lineRange = layoutNextRichInlineLineRange(readRichPrepared(spans, defaultFont), INTRINSIC_MAX_WIDTH);
1416
+ if (lineRange == null) return {
1417
+ width: 0,
1418
+ fragments: [],
1419
+ overflowed: false
1420
+ };
1421
+ return materializeRichLine(ctx, spans, defaultFont, defaultColor, lineRange, false);
1422
+ }
1423
+ function layoutRichFirstLine(ctx, spans, maxWidth, defaultFont, defaultColor) {
1424
+ const clampedMaxWidth = Math.max(0, maxWidth);
1425
+ if (spans.length === 0 || clampedMaxWidth === 0) return {
1426
+ width: 0,
1427
+ fragments: [],
1428
+ overflowed: false
1429
+ };
1430
+ const lineRange = layoutNextRichInlineLineRange(readRichPrepared(spans, defaultFont), clampedMaxWidth);
1431
+ if (lineRange == null) return {
1432
+ width: 0,
1433
+ fragments: [],
1434
+ overflowed: false
1435
+ };
1436
+ return materializeRichLine(ctx, spans, defaultFont, defaultColor, lineRange, false);
1437
+ }
1438
+ function layoutRichEllipsizedFirstLine(ctx, spans, maxWidth, defaultFont, defaultColor, ellipsisPosition = "end") {
1439
+ const clampedMaxWidth = Math.max(0, maxWidth);
1440
+ const intrinsicLine = layoutRichFirstLineIntrinsic(ctx, spans, defaultFont, defaultColor);
1441
+ if (intrinsicLine.fragments.length === 0) return {
1442
+ ...intrinsicLine,
1443
+ overflowed: false
1444
+ };
1445
+ if (clampedMaxWidth === 0) return {
1446
+ width: 0,
1447
+ fragments: [],
1448
+ overflowed: true
1449
+ };
1450
+ return layoutPreparedRichEllipsis(ctx, intrinsicLine, clampedMaxWidth, defaultFont, defaultColor, ellipsisPosition);
1451
+ }
1452
+ function measureRichText(_ctx, spans, maxWidth, defaultFont) {
1265
1453
  if (spans.length === 0) return {
1266
1454
  width: 0,
1267
1455
  lineCount: 0
@@ -1272,7 +1460,7 @@ function measureRichText(ctx, spans, maxWidth, defaultFont) {
1272
1460
  lineCount
1273
1461
  };
1274
1462
  }
1275
- function measureRichTextIntrinsic(ctx, spans, defaultFont) {
1463
+ function measureRichTextIntrinsic(_ctx, spans, defaultFont) {
1276
1464
  if (spans.length === 0) return {
1277
1465
  width: 0,
1278
1466
  lineCount: 0
@@ -1283,7 +1471,7 @@ function measureRichTextIntrinsic(ctx, spans, defaultFont) {
1283
1471
  lineCount
1284
1472
  };
1285
1473
  }
1286
- function measureRichTextMinContent(ctx, spans, defaultFont, overflowWrap = "break-word") {
1474
+ function measureRichTextMinContent(_ctx, spans, defaultFont, overflowWrap = "break-word") {
1287
1475
  if (spans.length === 0) return {
1288
1476
  width: 0,
1289
1477
  lineCount: 0
@@ -1292,8 +1480,8 @@ function measureRichTextMinContent(ctx, spans, defaultFont, overflowWrap = "brea
1292
1480
  for (const span of spans) {
1293
1481
  if (span.text.trim().length === 0) continue;
1294
1482
  const font = span.font ?? defaultFont;
1295
- const spanMin = measurePreparedMinContentWidth(readPreparedText(span.text, font, "normal", "normal"), overflowWrap) + (span.extraWidth ?? 0);
1296
- if (spanMin > maxWidth) maxWidth = spanMin;
1483
+ const spanMinWidth = measurePreparedMinContentWidth(readPreparedText(span.text, font, "normal", "normal"), overflowWrap) + (span.extraWidth ?? 0);
1484
+ if (spanMinWidth > maxWidth) maxWidth = spanMinWidth;
1297
1485
  }
1298
1486
  if (maxWidth === 0) return {
1299
1487
  width: 0,
@@ -1305,7 +1493,7 @@ function measureRichTextMinContent(ctx, spans, defaultFont, overflowWrap = "brea
1305
1493
  lineCount
1306
1494
  };
1307
1495
  }
1308
- function layoutRichText(ctx, spans, maxWidth, defaultFont, defaultStyle) {
1496
+ function layoutRichText(ctx, spans, maxWidth, defaultFont, defaultColor) {
1309
1497
  if (spans.length === 0) return {
1310
1498
  width: 0,
1311
1499
  lines: [],
@@ -1313,124 +1501,50 @@ function layoutRichText(ctx, spans, maxWidth, defaultFont, defaultStyle) {
1313
1501
  };
1314
1502
  const prepared = readRichPrepared(spans, defaultFont);
1315
1503
  const lineRanges = [];
1316
- walkRichInlineLineRanges(prepared, maxWidth, (line) => lineRanges.push(line));
1504
+ walkRichInlineLineRanges(prepared, maxWidth, (lineRange) => lineRanges.push(lineRange));
1317
1505
  if (lineRanges.length === 0) return {
1318
1506
  width: 0,
1319
1507
  lines: [],
1320
1508
  overflowed: false
1321
1509
  };
1322
- const lines = lineRanges.map((lr) => materializeRichLine(ctx, spans, defaultFont, defaultStyle, lr, false));
1510
+ const lines = lineRanges.map((lineRange) => materializeRichLine(ctx, spans, defaultFont, defaultColor, lineRange, false));
1323
1511
  return {
1324
- width: lines.reduce((max, line) => Math.max(max, line.width), 0),
1512
+ width: lines.reduce((maxLineWidth, line) => Math.max(maxLineWidth, line.width), 0),
1325
1513
  lines,
1326
1514
  overflowed: false
1327
1515
  };
1328
1516
  }
1329
- function layoutRichTextIntrinsic(ctx, spans, defaultFont, defaultStyle) {
1330
- return layoutRichText(ctx, spans, INTRINSIC_MAX_WIDTH, defaultFont, defaultStyle);
1517
+ function layoutRichTextIntrinsic(ctx, spans, defaultFont, defaultColor) {
1518
+ return layoutRichText(ctx, spans, INTRINSIC_MAX_WIDTH, defaultFont, defaultColor);
1331
1519
  }
1332
- function layoutRichTextWithOverflow(ctx, spans, maxWidth, defaultFont, defaultStyle, maxLines, overflow = "clip") {
1520
+ function layoutRichTextWithOverflow(ctx, spans, maxWidth, defaultFont, defaultColor, maxLines, overflow = "clip") {
1333
1521
  if (spans.length === 0) return {
1334
1522
  width: 0,
1335
1523
  lines: [],
1336
1524
  overflowed: false
1337
1525
  };
1338
1526
  const normalizedMaxLines = normalizeMaxLines(maxLines);
1339
- const layout = layoutRichText(ctx, spans, maxWidth, defaultFont, defaultStyle);
1527
+ const layout = layoutRichText(ctx, spans, maxWidth, defaultFont, defaultColor);
1340
1528
  if (normalizedMaxLines == null || layout.lines.length <= normalizedMaxLines) return layout;
1341
1529
  const visibleLines = layout.lines.slice(0, normalizedMaxLines);
1342
1530
  if (overflow !== "ellipsis") return {
1343
- width: visibleLines.reduce((max, line) => Math.max(max, line.width), 0),
1531
+ width: visibleLines.reduce((maxLineWidth, line) => Math.max(maxLineWidth, line.width), 0),
1344
1532
  lines: visibleLines,
1345
1533
  overflowed: true
1346
1534
  };
1347
- const lastLine = visibleLines[visibleLines.length - 1];
1348
- if (lastLine == null || lastLine.fragments.length === 0) return {
1349
- width: visibleLines.slice(0, -1).reduce((max, line) => Math.max(max, line.width), 0),
1350
- lines: visibleLines.slice(0, -1),
1535
+ const lastVisibleLine = visibleLines[visibleLines.length - 1];
1536
+ const ellipsizedLastLine = lastVisibleLine == null ? {
1537
+ width: 0,
1538
+ fragments: [],
1351
1539
  overflowed: true
1352
- };
1353
- const lastFrag = lastLine.fragments[lastLine.fragments.length - 1];
1354
- const prevFont1 = ctx.graphics.font;
1355
- ctx.graphics.font = lastFrag.font;
1356
- const ellipsisWidth = measureEllipsisWidth(ctx);
1357
- ctx.graphics.font = prevFont1;
1358
- if (maxWidth <= 0 || ellipsisWidth > maxWidth) {
1359
- const truncatedLine = {
1360
- width: 0,
1361
- fragments: [],
1362
- overflowed: true
1363
- };
1364
- return {
1365
- width: visibleLines.slice(0, -1).reduce((max, line) => Math.max(max, line.width), 0),
1366
- lines: [...visibleLines.slice(0, -1), truncatedLine],
1367
- overflowed: true
1368
- };
1369
- }
1370
- const budget = maxWidth - ellipsisWidth;
1371
- const resultFragments = [];
1372
- let usedWidth = 0;
1373
- let ellipsisFont = lastFrag.font;
1374
- let ellipsisStyle = lastFrag.style;
1375
- let truncated = false;
1376
- for (let fi = 0; fi < lastLine.fragments.length; fi++) {
1377
- const frag = lastLine.fragments[fi];
1378
- const neededGap = fi === 0 ? 0 : frag.gapBefore;
1379
- const fragTotal = neededGap + frag.occupiedWidth;
1380
- if (usedWidth + fragTotal <= budget) {
1381
- resultFragments.push({
1382
- ...frag,
1383
- gapBefore: fi === 0 ? 0 : frag.gapBefore
1384
- });
1385
- usedWidth += fragTotal;
1386
- } else {
1387
- ellipsisFont = frag.font;
1388
- ellipsisStyle = frag.style;
1389
- const remaining = budget - usedWidth - neededGap;
1390
- if (remaining > 0 && frag.text.length > 0) {
1391
- const units = getPreparedUnits(readPreparedText(frag.text, frag.font, "normal", "normal"));
1392
- let charWidth = 0;
1393
- let charText = "";
1394
- for (const unit of units) {
1395
- if (charWidth + unit.width > remaining) break;
1396
- charWidth += unit.width;
1397
- charText += unit.text;
1398
- }
1399
- if (charText.length > 0) {
1400
- resultFragments.push({
1401
- ...frag,
1402
- text: charText,
1403
- occupiedWidth: charWidth,
1404
- gapBefore: fi === 0 ? 0 : frag.gapBefore
1405
- });
1406
- usedWidth += neededGap + charWidth;
1407
- }
1408
- }
1409
- truncated = true;
1410
- break;
1411
- }
1412
- }
1413
- const prevFont2 = ctx.graphics.font;
1414
- ctx.graphics.font = ellipsisFont;
1415
- const ellipsisShift = measureFontShift(ctx);
1416
- ctx.graphics.font = prevFont2;
1417
- resultFragments.push({
1418
- itemIndex: -1,
1419
- text: ELLIPSIS_GLYPH,
1420
- font: ellipsisFont,
1421
- style: ellipsisStyle,
1422
- gapBefore: 0,
1423
- occupiedWidth: ellipsisWidth,
1424
- shift: ellipsisShift
1425
- });
1426
- const lastLineResult = {
1427
- width: usedWidth + ellipsisWidth,
1428
- fragments: resultFragments,
1429
- overflowed: truncated || layout.lines.length > normalizedMaxLines
1430
- };
1540
+ } : layoutPreparedRichEllipsis(ctx, {
1541
+ ...lastVisibleLine,
1542
+ overflowed: true
1543
+ }, maxWidth, defaultFont, defaultColor, "end");
1544
+ const lines = [...visibleLines.slice(0, -1), ellipsizedLastLine];
1431
1545
  return {
1432
- width: [...visibleLines.slice(0, -1), lastLineResult].reduce((max, line) => Math.max(max, line.width), 0),
1433
- lines: [...visibleLines.slice(0, -1), lastLineResult],
1546
+ width: lines.reduce((maxLineWidth, line) => Math.max(maxLineWidth, line.width), 0),
1547
+ lines,
1434
1548
  overflowed: true
1435
1549
  };
1436
1550
  }
@@ -1484,6 +1598,15 @@ function getRichMultiLineOverflowLayoutKey(maxWidth) {
1484
1598
  function shouldUseMultilineOverflowLayout(options) {
1485
1599
  return options.maxLines != null;
1486
1600
  }
1601
+ function shouldReadConstrainedOverflowLayout(maxWidth, options) {
1602
+ return maxWidth != null && shouldUseMultilineOverflowLayout(options);
1603
+ }
1604
+ function measureBlockLayout(layout) {
1605
+ return {
1606
+ width: layout.width,
1607
+ lineCount: layout.lines.length
1608
+ };
1609
+ }
1487
1610
  function getSingleLineMinContentLayoutKey() {
1488
1611
  return "single:min-content";
1489
1612
  }
@@ -1497,6 +1620,10 @@ function getSingleLineLayout(node, ctx, text, options) {
1497
1620
  const maxWidth = normalizeTextMaxWidth(ctx.constraints?.maxWidth);
1498
1621
  return readCachedTextLayout(node, ctx, getSingleLineLayoutKey(maxWidth), () => maxWidth == null ? layoutFirstLineIntrinsic(ctx, text, options.whiteSpace, options.wordBreak) : options.overflow === "ellipsis" ? layoutEllipsizedFirstLine(ctx, text, maxWidth, options.ellipsisPosition ?? "end", options.whiteSpace, options.wordBreak) : layoutFirstLine(ctx, text, maxWidth, options.whiteSpace, options.wordBreak));
1499
1622
  }
1623
+ function getRichSingleLineLayout(node, ctx, spans, options) {
1624
+ const maxWidth = normalizeTextMaxWidth(ctx.constraints?.maxWidth);
1625
+ return readCachedTextLayout(node, ctx, getSingleLineLayoutKey(maxWidth), () => maxWidth == null ? layoutRichFirstLineIntrinsic(ctx, spans, options.font, options.color) : options.overflow === "ellipsis" ? layoutRichEllipsizedFirstLine(ctx, spans, maxWidth, options.font, options.color, options.ellipsisPosition ?? "end") : layoutRichFirstLine(ctx, spans, maxWidth, options.font, options.color));
1626
+ }
1500
1627
  function getMultiLineOverflowLayout(node, ctx, text, options) {
1501
1628
  const maxWidth = normalizeTextMaxWidth(ctx.constraints?.maxWidth);
1502
1629
  return readCachedTextLayout(node, ctx, getMultiLineOverflowLayoutKey(maxWidth), () => layoutTextWithOverflow(ctx, text, maxWidth ?? 0, {
@@ -1508,18 +1635,12 @@ function getMultiLineOverflowLayout(node, ctx, text, options) {
1508
1635
  }
1509
1636
  function getMultiLineMeasureLayout(node, ctx, text, options) {
1510
1637
  const maxWidth = normalizeTextMaxWidth(ctx.constraints?.maxWidth);
1511
- if (maxWidth != null && shouldUseMultilineOverflowLayout(options)) {
1512
- const layout = getMultiLineOverflowLayout(node, ctx, text, options);
1513
- return {
1514
- width: layout.width,
1515
- lineCount: layout.lines.length
1516
- };
1517
- }
1638
+ if (shouldReadConstrainedOverflowLayout(maxWidth, options)) return measureBlockLayout(getMultiLineOverflowLayout(node, ctx, text, options));
1518
1639
  return readCachedTextLayout(node, ctx, getMultiLineMeasureLayoutKey(maxWidth), () => maxWidth == null ? measureTextIntrinsic(ctx, text, options.whiteSpace, options.wordBreak) : measureText(ctx, text, maxWidth, options.whiteSpace, options.wordBreak));
1519
1640
  }
1520
1641
  function getMultiLineDrawLayout(node, ctx, text, options) {
1521
1642
  const maxWidth = normalizeTextMaxWidth(ctx.constraints?.maxWidth);
1522
- if (maxWidth != null && shouldUseMultilineOverflowLayout(options)) return getMultiLineOverflowLayout(node, ctx, text, options);
1643
+ if (shouldReadConstrainedOverflowLayout(maxWidth, options)) return getMultiLineOverflowLayout(node, ctx, text, options);
1523
1644
  return readCachedTextLayout(node, ctx, getMultiLineDrawLayoutKey(maxWidth), () => maxWidth == null ? layoutTextIntrinsic(ctx, text, options.whiteSpace, options.wordBreak) : layoutText(ctx, text, maxWidth, options.whiteSpace, options.wordBreak));
1524
1645
  }
1525
1646
  function getSingleLineMinContentLayout(node, ctx, text, options) {
@@ -1533,28 +1654,39 @@ function getSingleLineMinContentLayout(node, ctx, text, options) {
1533
1654
  };
1534
1655
  });
1535
1656
  }
1657
+ function getRichSingleLineMinContentWidth(node, ctx, spans, options) {
1658
+ return readCachedTextLayout(node, ctx, getSingleLineMinContentLayoutKey(), () => measureRichTextMinContent(ctx, spans, options.font, options.overflowWrap).width);
1659
+ }
1660
+ function drawRichLine(ctx, line, fallbackColor, x, y, lineHeight) {
1661
+ let cursorX = x;
1662
+ for (let fragmentIndex = 0; fragmentIndex < line.fragments.length; fragmentIndex += 1) {
1663
+ const fragment = line.fragments[fragmentIndex];
1664
+ cursorX += fragment.gapBefore;
1665
+ ctx.with((g) => {
1666
+ g.font = fragment.font;
1667
+ g.fillStyle = ctx.resolveDynValue(fragment.color ?? fallbackColor);
1668
+ g.textAlign = "left";
1669
+ g.fillText(fragment.text, cursorX, y + (lineHeight + fragment.shift) / 2);
1670
+ });
1671
+ cursorX += fragment.occupiedWidth;
1672
+ }
1673
+ }
1536
1674
  function getMultiLineMinContentLayout(node, ctx, text, whiteSpace, wordBreak, overflowWrap) {
1537
1675
  return readCachedTextLayout(node, ctx, getMultiLineMinContentLayoutKey(), () => measureTextMinContent(ctx, text, whiteSpace, wordBreak, overflowWrap));
1538
1676
  }
1539
1677
  function getRichMultiLineMeasureLayout(node, ctx, spans, options) {
1540
1678
  const maxWidth = normalizeTextMaxWidth(ctx.constraints?.maxWidth);
1541
- if (maxWidth != null && shouldUseMultilineOverflowLayout(options)) {
1542
- const layout = getRichMultiLineOverflowLayout(node, ctx, spans, options);
1543
- return {
1544
- width: layout.width,
1545
- lineCount: layout.lines.length
1546
- };
1547
- }
1679
+ if (shouldReadConstrainedOverflowLayout(maxWidth, options)) return measureBlockLayout(getRichMultiLineOverflowLayout(node, ctx, spans, options));
1548
1680
  return readCachedTextLayout(node, ctx, getRichMultiLineMeasureLayoutKey(maxWidth), () => maxWidth == null ? measureRichTextIntrinsic(ctx, spans, options.font) : measureRichText(ctx, spans, maxWidth, options.font));
1549
1681
  }
1550
1682
  function getRichMultiLineOverflowLayout(node, ctx, spans, options) {
1551
1683
  const maxWidth = normalizeTextMaxWidth(ctx.constraints?.maxWidth);
1552
- return readCachedTextLayout(node, ctx, getRichMultiLineOverflowLayoutKey(maxWidth), () => layoutRichTextWithOverflow(ctx, spans, maxWidth ?? 0, options.font, options.style, options.maxLines, options.overflow));
1684
+ return readCachedTextLayout(node, ctx, getRichMultiLineOverflowLayoutKey(maxWidth), () => layoutRichTextWithOverflow(ctx, spans, maxWidth ?? 0, options.font, options.color, options.maxLines, options.overflow));
1553
1685
  }
1554
1686
  function getRichMultiLineDrawLayout(node, ctx, spans, options) {
1555
1687
  const maxWidth = normalizeTextMaxWidth(ctx.constraints?.maxWidth);
1556
- if (maxWidth != null && shouldUseMultilineOverflowLayout(options)) return getRichMultiLineOverflowLayout(node, ctx, spans, options);
1557
- return readCachedTextLayout(node, ctx, getRichMultiLineDrawLayoutKey(maxWidth), () => maxWidth == null ? layoutRichTextIntrinsic(ctx, spans, options.font, options.style) : layoutRichText(ctx, spans, maxWidth, options.font, options.style));
1688
+ if (shouldReadConstrainedOverflowLayout(maxWidth, options)) return getRichMultiLineOverflowLayout(node, ctx, spans, options);
1689
+ return readCachedTextLayout(node, ctx, getRichMultiLineDrawLayoutKey(maxWidth), () => maxWidth == null ? layoutRichTextIntrinsic(ctx, spans, options.font, options.color) : layoutRichText(ctx, spans, maxWidth, options.font, options.color));
1558
1690
  }
1559
1691
  function getRichMultiLineMinContentLayout(node, ctx, spans, options) {
1560
1692
  return readCachedTextLayout(node, ctx, getRichMultiLineMinContentLayoutKey(), () => measureRichTextMinContent(ctx, spans, options.font, options.overflowWrap));
@@ -1621,7 +1753,7 @@ var MultilineText = class {
1621
1753
  cursorX += frag.gapBefore;
1622
1754
  ctx.with((g) => {
1623
1755
  g.font = frag.font;
1624
- g.fillStyle = ctx.resolveDynValue(frag.style ?? this.options.style);
1756
+ g.fillStyle = ctx.resolveDynValue(frag.color ?? this.options.color);
1625
1757
  if (align === "right") g.textAlign = "right";
1626
1758
  else if (align === "center") g.textAlign = "center";
1627
1759
  else g.textAlign = "left";
@@ -1635,7 +1767,7 @@ var MultilineText = class {
1635
1767
  }
1636
1768
  return ctx.with((g) => {
1637
1769
  g.font = this.options.font;
1638
- g.fillStyle = ctx.resolveDynValue(this.options.style);
1770
+ g.fillStyle = ctx.resolveDynValue(this.options.color);
1639
1771
  const { width, lines } = getMultiLineDrawLayout(this, ctx, this.text, this.options);
1640
1772
  switch (resolvePhysicalTextAlign(this.options)) {
1641
1773
  case "left":
@@ -1673,7 +1805,7 @@ var MultilineText = class {
1673
1805
  */
1674
1806
  var Text = class {
1675
1807
  /**
1676
- * @param text Source text to measure and draw.
1808
+ * @param text Source text to measure and draw. Pass an `InlineSpan[]` for mixed inline styles.
1677
1809
  * @param options Text layout and drawing options.
1678
1810
  */
1679
1811
  constructor(text, options) {
@@ -1681,9 +1813,17 @@ var Text = class {
1681
1813
  this.options = options;
1682
1814
  }
1683
1815
  measure(ctx) {
1816
+ if (typeof this.text !== "string") {
1817
+ const { width } = getRichSingleLineLayout(this, ctx, this.text, this.options);
1818
+ return {
1819
+ width,
1820
+ height: this.options.lineHeight
1821
+ };
1822
+ }
1823
+ const text = this.text;
1684
1824
  return ctx.with((g) => {
1685
1825
  g.font = this.options.font;
1686
- const { width } = getSingleLineLayout(this, ctx, this.text, this.options);
1826
+ const { width } = getSingleLineLayout(this, ctx, text, this.options);
1687
1827
  return {
1688
1828
  width,
1689
1829
  height: this.options.lineHeight
@@ -1691,9 +1831,14 @@ var Text = class {
1691
1831
  });
1692
1832
  }
1693
1833
  measureMinContent(ctx) {
1834
+ if (typeof this.text !== "string") return {
1835
+ width: getRichSingleLineMinContentWidth(this, ctx, this.text, this.options),
1836
+ height: this.options.lineHeight
1837
+ };
1838
+ const text = this.text;
1694
1839
  return ctx.with((g) => {
1695
1840
  g.font = this.options.font;
1696
- const { width } = getSingleLineMinContentLayout(this, ctx, this.text, this.options);
1841
+ const { width } = getSingleLineMinContentLayout(this, ctx, text, this.options);
1697
1842
  return {
1698
1843
  width,
1699
1844
  height: this.options.lineHeight
@@ -1701,11 +1846,16 @@ var Text = class {
1701
1846
  });
1702
1847
  }
1703
1848
  draw(ctx, x, y) {
1849
+ if (typeof this.text !== "string") {
1850
+ drawRichLine(ctx, getRichSingleLineLayout(this, ctx, this.text, this.options), this.options.color, x, y, this.options.lineHeight);
1851
+ return false;
1852
+ }
1853
+ const text = this.text;
1704
1854
  return ctx.with((g) => {
1705
1855
  g.font = this.options.font;
1706
- g.fillStyle = ctx.resolveDynValue(this.options.style);
1707
- const { text, shift } = getSingleLineLayout(this, ctx, this.text, this.options);
1708
- g.fillText(text, x, y + (this.options.lineHeight + shift) / 2);
1856
+ g.fillStyle = ctx.resolveDynValue(this.options.color);
1857
+ const layout = getSingleLineLayout(this, ctx, text, this.options);
1858
+ g.fillText(layout.text, x, y + (this.options.lineHeight + layout.shift) / 2);
1709
1859
  return false;
1710
1860
  });
1711
1861
  }