chat-layout 1.1.2 → 1.2.0-1

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,3 @@
1
- import { layoutNextLine, layoutWithLines, measureLineStats, measureNaturalWidth, prepareWithSegments } from "@chenglou/pretext";
2
- import { layoutNextRichInlineLineRange, materializeRichInlineLineRange, measureRichInlineStats, prepareRichInline } from "@chenglou/pretext/rich-inline";
3
1
  //#region src/internal/node-registry.ts
4
2
  const registry = /* @__PURE__ */ new WeakMap();
5
3
  const revisions = /* @__PURE__ */ new WeakMap();
@@ -816,9 +814,117 @@ var Place = class extends Wrapper {
816
814
  });
817
815
  }
818
816
  };
819
- const INTRINSIC_MAX_WIDTH = Number.POSITIVE_INFINITY;
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
+ };
923
+ Number.POSITIVE_INFINITY;
820
924
  const MIN_CONTENT_WIDTH_EPSILON = .001;
821
925
  let sharedGraphemeSegmenter;
926
+ const fontShiftCache = /* @__PURE__ */ new Map();
927
+ const ellipsisWidthCache = /* @__PURE__ */ new Map();
822
928
  function readLruValue$1(cache, key) {
823
929
  const cached = cache.get(key);
824
930
  if (cached == null) return;
@@ -836,11 +942,21 @@ function writeLruValue$1(cache, key, value, capacity) {
836
942
  return value;
837
943
  }
838
944
  function measureFontShift(ctx) {
945
+ const font = ctx.graphics.font;
946
+ const cached = fontShiftCache.get(font);
947
+ if (cached != null) return cached;
839
948
  const { fontBoundingBoxAscent: ascent = 0, fontBoundingBoxDescent: descent = 0 } = ctx.graphics.measureText("M");
840
- return ascent - descent;
949
+ const shift = ascent - descent;
950
+ fontShiftCache.set(font, shift);
951
+ return shift;
841
952
  }
842
953
  function measureEllipsisWidth(ctx) {
843
- return ctx.graphics.measureText("…").width;
954
+ const font = ctx.graphics.font;
955
+ const cached = ellipsisWidthCache.get(font);
956
+ if (cached != null) return cached;
957
+ const width = ctx.graphics.measureText("…").width;
958
+ ellipsisWidthCache.set(font, width);
959
+ return width;
844
960
  }
845
961
  function getGraphemeSegmenter() {
846
962
  if (sharedGraphemeSegmenter !== void 0) return sharedGraphemeSegmenter;
@@ -923,79 +1039,1025 @@ function selectEllipsisUnitCounts({ position, prefixWidths, suffixWidths, unitCo
923
1039
  suffixCount
924
1040
  };
925
1041
  }
926
- const LINE_START_CURSOR$1 = {
927
- segmentIndex: 0,
928
- graphemeIndex: 0
929
- };
930
- const preparedTextCache = /* @__PURE__ */ new Map();
931
- function getPreparedTextCacheKey(text, font, whiteSpace, wordBreak) {
932
- return `${font}\u0000${whiteSpace}\u0000${wordBreak}\u0000${text}`;
1042
+ function resolveEllipsisSelection({ widths, ellipsisWidth, maxWidth, position }) {
1043
+ if (ellipsisWidth > maxWidth) return;
1044
+ const prefixWidths = buildPrefixWidths(widths);
1045
+ const suffixWidths = buildSuffixWidths(widths);
1046
+ const { prefixCount, suffixCount } = selectEllipsisUnitCounts({
1047
+ position,
1048
+ prefixWidths,
1049
+ suffixWidths,
1050
+ unitCount: widths.length,
1051
+ availableWidth: Math.max(0, maxWidth - ellipsisWidth)
1052
+ });
1053
+ return {
1054
+ prefixCount,
1055
+ suffixCount,
1056
+ width: position === "start" ? ellipsisWidth + (suffixWidths[suffixCount] ?? 0) : position === "middle" ? (prefixWidths[prefixCount] ?? 0) + ellipsisWidth + (suffixWidths[suffixCount] ?? 0) : (prefixWidths[prefixCount] ?? 0) + ellipsisWidth
1057
+ };
933
1058
  }
934
- function readPreparedText(text, font, whiteSpace, wordBreak) {
935
- const key = getPreparedTextCacheKey(text, font, whiteSpace, wordBreak);
936
- const cached = readLruValue$1(preparedTextCache, key);
1059
+ //#endregion
1060
+ //#region src/text/inline-engine.ts
1061
+ const LINE_FIT_EPSILON = .005;
1062
+ const PREPARED_INLINE_CACHE_CAPACITY = 512;
1063
+ const PREPARED_LINE_STATS_CACHE_CAPACITY = 16;
1064
+ const kinsokuStart = new Set([
1065
+ ",",
1066
+ ".",
1067
+ "!",
1068
+ ":",
1069
+ ";",
1070
+ "?",
1071
+ "、",
1072
+ "。",
1073
+ "・",
1074
+ ")",
1075
+ "〕",
1076
+ "〉",
1077
+ "》",
1078
+ "」",
1079
+ "』",
1080
+ "】",
1081
+ "〗",
1082
+ "〙",
1083
+ "〛",
1084
+ "ー",
1085
+ "々",
1086
+ "〻",
1087
+ "ゝ",
1088
+ "ゞ",
1089
+ "ヽ",
1090
+ "ヾ"
1091
+ ]);
1092
+ const kinsokuEnd = new Set([
1093
+ "\"",
1094
+ "(",
1095
+ "[",
1096
+ "{",
1097
+ "“",
1098
+ "‘",
1099
+ "«",
1100
+ "‹",
1101
+ "(",
1102
+ "〔",
1103
+ "〈",
1104
+ "《",
1105
+ "「",
1106
+ "『",
1107
+ "【",
1108
+ "〖",
1109
+ "〘",
1110
+ "〚"
1111
+ ]);
1112
+ const leftStickyPunctuation = new Set([
1113
+ ".",
1114
+ ",",
1115
+ "!",
1116
+ "?",
1117
+ ":",
1118
+ ";",
1119
+ "،",
1120
+ "؛",
1121
+ "؟",
1122
+ "।",
1123
+ "॥",
1124
+ "၊",
1125
+ "။",
1126
+ "၌",
1127
+ "၍",
1128
+ "၏",
1129
+ ")",
1130
+ "]",
1131
+ "}",
1132
+ "%",
1133
+ "\"",
1134
+ "”",
1135
+ "’",
1136
+ "»",
1137
+ "›",
1138
+ "…"
1139
+ ]);
1140
+ const keepAllGlueChars = new Set([
1141
+ "\xA0",
1142
+ " ",
1143
+ "⁠",
1144
+ ""
1145
+ ]);
1146
+ const closingQuoteChars = new Set([
1147
+ "”",
1148
+ "’",
1149
+ "»",
1150
+ "›",
1151
+ "」",
1152
+ "』",
1153
+ "】"
1154
+ ]);
1155
+ const cjkCodePointRanges = [
1156
+ [19968, 40959],
1157
+ [13312, 19903],
1158
+ [131072, 173791],
1159
+ [173824, 177983],
1160
+ [177984, 178207],
1161
+ [178208, 183983],
1162
+ [183984, 191471],
1163
+ [191472, 192093],
1164
+ [194560, 195103],
1165
+ [196608, 201551],
1166
+ [201552, 205743],
1167
+ [205744, 210041],
1168
+ [63744, 64255],
1169
+ [12288, 12351],
1170
+ [12352, 12447],
1171
+ [12448, 12543],
1172
+ [44032, 55215],
1173
+ [65280, 65519]
1174
+ ];
1175
+ let sharedMeasureContext = null;
1176
+ let sharedWordSegmenter;
1177
+ const textWidthCache = /* @__PURE__ */ new Map();
1178
+ const preparedInlineCache = /* @__PURE__ */ new Map();
1179
+ function getMeasureContext() {
1180
+ if (sharedMeasureContext != null) return sharedMeasureContext;
1181
+ if (typeof OffscreenCanvas !== "undefined") {
1182
+ const ctx = new OffscreenCanvas(1, 1).getContext("2d");
1183
+ if (ctx != null) {
1184
+ sharedMeasureContext = ctx;
1185
+ return ctx;
1186
+ }
1187
+ }
1188
+ if (typeof document !== "undefined") {
1189
+ const ctx = document.createElement("canvas").getContext("2d");
1190
+ if (ctx != null) {
1191
+ sharedMeasureContext = ctx;
1192
+ return ctx;
1193
+ }
1194
+ }
1195
+ throw new Error("Text measurement requires OffscreenCanvas or a DOM canvas context.");
1196
+ }
1197
+ function getTextWidthCache(font) {
1198
+ let cache = textWidthCache.get(font);
1199
+ if (cache == null) {
1200
+ cache = /* @__PURE__ */ new Map();
1201
+ textWidthCache.set(font, cache);
1202
+ }
1203
+ return cache;
1204
+ }
1205
+ function measureTextWidth(font, text) {
1206
+ const cache = getTextWidthCache(font);
1207
+ const cached = cache.get(text);
937
1208
  if (cached != null) return cached;
938
- return writeLruValue$1(preparedTextCache, key, prepareWithSegments(text, font, {
939
- whiteSpace,
940
- wordBreak
941
- }), 512);
1209
+ const ctx = getMeasureContext();
1210
+ ctx.font = font;
1211
+ const width = ctx.measureText(text).width;
1212
+ cache.set(text, width);
1213
+ return width;
1214
+ }
1215
+ function isCollapsibleWhitespace(text) {
1216
+ return /^[ \t\n\f\r]+$/u.test(text);
1217
+ }
1218
+ function isPreservedWhitespaceGrapheme(text) {
1219
+ return text === " " || text === " ";
1220
+ }
1221
+ function normalizePreWrapText(text) {
1222
+ if (!/[\r\f]/.test(text)) return text.replace(/\r\n/g, "\n");
1223
+ return text.replace(/\r\n/g, "\n").replace(/[\r\f]/g, "\n");
1224
+ }
1225
+ function getLastCodePoint(text) {
1226
+ if (text.length === 0) return null;
1227
+ const codePoints = Array.from(text);
1228
+ return codePoints[codePoints.length - 1] ?? null;
1229
+ }
1230
+ function isCJKCodePoint(codePoint) {
1231
+ for (const [start, end] of cjkCodePointRanges) if (codePoint >= start && codePoint <= end) return true;
1232
+ return false;
1233
+ }
1234
+ function isCJK(text) {
1235
+ for (const char of text) {
1236
+ const codePoint = char.codePointAt(0);
1237
+ if (codePoint != null && isCJKCodePoint(codePoint)) return true;
1238
+ }
1239
+ return false;
1240
+ }
1241
+ function endsWithClosingQuote(text) {
1242
+ const last = getLastCodePoint(text);
1243
+ return last != null && closingQuoteChars.has(last);
1244
+ }
1245
+ function endsWithKeepAllGlueText(text) {
1246
+ const last = getLastCodePoint(text);
1247
+ return last != null && keepAllGlueChars.has(last);
1248
+ }
1249
+ function endsWithLineStartProhibitedText(text) {
1250
+ const last = getLastCodePoint(text);
1251
+ return last != null && (kinsokuStart.has(last) || leftStickyPunctuation.has(last));
1252
+ }
1253
+ function canContinueKeepAllTextRun(text) {
1254
+ return !endsWithLineStartProhibitedText(text) && !endsWithKeepAllGlueText(text);
1255
+ }
1256
+ function getSharedWordSegmenter() {
1257
+ if (sharedWordSegmenter !== void 0) return sharedWordSegmenter;
1258
+ sharedWordSegmenter = typeof Intl.Segmenter === "function" ? new Intl.Segmenter(void 0, { granularity: "word" }) : null;
1259
+ return sharedWordSegmenter;
1260
+ }
1261
+ function joinAtomText(atoms, start = 0, end = atoms.length) {
1262
+ let text = "";
1263
+ for (let index = start; index < end; index += 1) {
1264
+ const atom = atoms[index];
1265
+ if (atom != null) text += atom.text;
1266
+ }
1267
+ return text;
1268
+ }
1269
+ function measureAtomSequenceWidth(atoms) {
1270
+ if (atoms.length === 0) return 0;
1271
+ let width = 0;
1272
+ let currentFont = atoms[0].font;
1273
+ let currentText = "";
1274
+ for (const atom of atoms) {
1275
+ if (atom.font !== currentFont && currentText.length > 0) {
1276
+ width += measureTextWidth(currentFont, currentText);
1277
+ currentFont = atom.font;
1278
+ currentText = "";
1279
+ }
1280
+ currentText += atom.text;
1281
+ width += atom.extraWidthAfter;
1282
+ }
1283
+ if (currentText.length > 0) width += measureTextWidth(currentFont, currentText);
1284
+ return width;
1285
+ }
1286
+ function pushTextPartAtoms(target, text, font, itemIndex, atomicGroupId, extraWidth) {
1287
+ const graphemes = splitGraphemes(text);
1288
+ for (let index = 0; index < graphemes.length; index += 1) {
1289
+ const grapheme = graphemes[index] ?? "";
1290
+ target.push({
1291
+ text: grapheme,
1292
+ font,
1293
+ itemIndex,
1294
+ width: measureTextWidth(font, grapheme),
1295
+ extraWidthAfter: index === graphemes.length - 1 ? extraWidth : 0,
1296
+ kind: "text",
1297
+ preservesLineEnd: false,
1298
+ atomicGroupId
1299
+ });
1300
+ }
1301
+ }
1302
+ function buildCollapsedWhitespaceAtoms(items) {
1303
+ const chunks = [[]];
1304
+ const atoms = chunks[0];
1305
+ let pendingSpace = null;
1306
+ let lastVisible = null;
1307
+ for (const item of items) {
1308
+ const atomicGroupId = item.breakMode === "never" ? item.itemIndex + 1 : null;
1309
+ const parts = item.text.match(/[ \t\n\f\r]+|[^ \t\n\f\r]+/gu) ?? [];
1310
+ for (let partIndex = 0; partIndex < parts.length; partIndex += 1) {
1311
+ const part = parts[partIndex] ?? "";
1312
+ if (isCollapsibleWhitespace(part)) {
1313
+ if (lastVisible != null) pendingSpace = {
1314
+ font: lastVisible.font,
1315
+ itemIndex: lastVisible.itemIndex,
1316
+ atomicGroupId
1317
+ };
1318
+ continue;
1319
+ }
1320
+ if (pendingSpace != null) {
1321
+ atoms.push({
1322
+ text: " ",
1323
+ font: pendingSpace.font,
1324
+ itemIndex: pendingSpace.itemIndex,
1325
+ width: measureTextWidth(pendingSpace.font, " "),
1326
+ extraWidthAfter: 0,
1327
+ kind: "space",
1328
+ preservesLineEnd: false,
1329
+ atomicGroupId: pendingSpace.atomicGroupId
1330
+ });
1331
+ pendingSpace = null;
1332
+ }
1333
+ pushTextPartAtoms(atoms, part, item.font, item.itemIndex, atomicGroupId, partIndex === parts.length - 1 ? item.extraWidth : 0);
1334
+ lastVisible = {
1335
+ font: item.font,
1336
+ itemIndex: item.itemIndex,
1337
+ atomicGroupId
1338
+ };
1339
+ }
1340
+ }
1341
+ return chunks;
1342
+ }
1343
+ function buildPreWrapAtoms(items) {
1344
+ const chunks = [[]];
1345
+ let currentChunk = chunks[0];
1346
+ for (const item of items) {
1347
+ const atomicGroupId = item.breakMode === "never" ? item.itemIndex + 1 : null;
1348
+ const graphemes = splitGraphemes(normalizePreWrapText(item.text));
1349
+ for (let index = 0; index < graphemes.length; index += 1) {
1350
+ const grapheme = graphemes[index] ?? "";
1351
+ if (grapheme === "\n") {
1352
+ currentChunk = [];
1353
+ chunks.push(currentChunk);
1354
+ continue;
1355
+ }
1356
+ const isSpace = isPreservedWhitespaceGrapheme(grapheme);
1357
+ currentChunk.push({
1358
+ text: grapheme,
1359
+ font: item.font,
1360
+ itemIndex: item.itemIndex,
1361
+ width: measureTextWidth(item.font, grapheme),
1362
+ extraWidthAfter: index === graphemes.length - 1 ? item.extraWidth : 0,
1363
+ kind: isSpace ? "space" : "text",
1364
+ preservesLineEnd: isSpace,
1365
+ atomicGroupId
1366
+ });
1367
+ }
1368
+ }
1369
+ return chunks;
1370
+ }
1371
+ function buildBaseCjkUnits(atoms) {
1372
+ const units = [];
1373
+ let current = [];
1374
+ let currentContainsCJK = false;
1375
+ let currentEndsWithClosingQuote = false;
1376
+ let currentIsSingleKinsokuEnd = false;
1377
+ const flushCurrent = () => {
1378
+ if (current.length === 0) return;
1379
+ units.push(current);
1380
+ current = [];
1381
+ currentContainsCJK = false;
1382
+ currentEndsWithClosingQuote = false;
1383
+ currentIsSingleKinsokuEnd = false;
1384
+ };
1385
+ for (const atom of atoms) {
1386
+ const atomContainsCJK = isCJK(atom.text);
1387
+ if (current.length === 0) {
1388
+ current = [atom];
1389
+ currentContainsCJK = atomContainsCJK;
1390
+ currentEndsWithClosingQuote = endsWithClosingQuote(atom.text);
1391
+ currentIsSingleKinsokuEnd = kinsokuEnd.has(atom.text);
1392
+ continue;
1393
+ }
1394
+ if (currentIsSingleKinsokuEnd || kinsokuStart.has(atom.text) || leftStickyPunctuation.has(atom.text) || atomContainsCJK && currentEndsWithClosingQuote) {
1395
+ current.push(atom);
1396
+ currentContainsCJK = currentContainsCJK || atomContainsCJK;
1397
+ currentEndsWithClosingQuote = leftStickyPunctuation.has(atom.text) ? currentEndsWithClosingQuote || endsWithClosingQuote(atom.text) : endsWithClosingQuote(atom.text);
1398
+ currentIsSingleKinsokuEnd = false;
1399
+ continue;
1400
+ }
1401
+ if (!currentContainsCJK && !atomContainsCJK) {
1402
+ current.push(atom);
1403
+ currentEndsWithClosingQuote = endsWithClosingQuote(atom.text);
1404
+ currentIsSingleKinsokuEnd = false;
1405
+ continue;
1406
+ }
1407
+ flushCurrent();
1408
+ current = [atom];
1409
+ currentContainsCJK = atomContainsCJK;
1410
+ currentEndsWithClosingQuote = endsWithClosingQuote(atom.text);
1411
+ currentIsSingleKinsokuEnd = kinsokuEnd.has(atom.text);
1412
+ }
1413
+ flushCurrent();
1414
+ return units;
942
1415
  }
943
- function readPreparedFirstLine(ctx, text, whiteSpace, wordBreak) {
944
- const line = layoutNextLine(readPreparedText(text, ctx.graphics.font, whiteSpace, wordBreak), LINE_START_CURSOR$1, INTRINSIC_MAX_WIDTH);
945
- if (line == null) return;
1416
+ function mergeKeepAllUnits(units) {
1417
+ if (units.length <= 1) return units.slice();
1418
+ const merged = [];
1419
+ let current = units[0].slice();
1420
+ let currentText = joinAtomText(current);
1421
+ let currentContainsCJK = isCJK(currentText);
1422
+ let currentCanContinue = canContinueKeepAllTextRun(currentText);
1423
+ const flush = () => {
1424
+ merged.push(current);
1425
+ };
1426
+ for (let index = 1; index < units.length; index += 1) {
1427
+ const next = units[index];
1428
+ const nextText = joinAtomText(next);
1429
+ const nextContainsCJK = isCJK(nextText);
1430
+ const nextCanContinue = canContinueKeepAllTextRun(nextText);
1431
+ if (currentContainsCJK && currentCanContinue) {
1432
+ current = [...current, ...next];
1433
+ currentText += nextText;
1434
+ currentContainsCJK = currentContainsCJK || nextContainsCJK;
1435
+ currentCanContinue = nextCanContinue;
1436
+ continue;
1437
+ }
1438
+ flush();
1439
+ current = next.slice();
1440
+ currentText = nextText;
1441
+ currentContainsCJK = nextContainsCJK;
1442
+ currentCanContinue = nextCanContinue;
1443
+ }
1444
+ flush();
1445
+ return merged;
1446
+ }
1447
+ function splitAtomsByWordSegments(atoms) {
1448
+ if (atoms.length <= 1) return atoms.length === 0 ? [] : [atoms.slice()];
1449
+ const segmenter = getSharedWordSegmenter();
1450
+ if (segmenter == null) return [atoms.slice()];
1451
+ const text = joinAtomText(atoms);
1452
+ const offsets = [0];
1453
+ for (let index = 0; index < atoms.length; index += 1) offsets.push((offsets[index] ?? 0) + (atoms[index]?.text.length ?? 0));
1454
+ const units = [];
1455
+ let atomStart = 0;
1456
+ let atomEnd = 0;
1457
+ for (const segment of segmenter.segment(text)) {
1458
+ const start = segment.index;
1459
+ const end = start + segment.segment.length;
1460
+ while ((offsets[atomStart] ?? 0) < start) atomStart += 1;
1461
+ atomEnd = atomStart;
1462
+ while ((offsets[atomEnd] ?? 0) < end) atomEnd += 1;
1463
+ if (atomStart < atomEnd) units.push(atoms.slice(atomStart, atomEnd));
1464
+ atomStart = atomEnd;
1465
+ }
1466
+ return units.length > 0 ? units : [atoms.slice()];
1467
+ }
1468
+ function makeTextUnit(atoms, atomic = false) {
1469
+ const width = measureAtomSequenceWidth(atoms);
946
1470
  return {
947
- text: line.text,
948
- prepared: readPreparedText(line.text, ctx.graphics.font, whiteSpace, wordBreak)
1471
+ kind: "text",
1472
+ atoms,
1473
+ width,
1474
+ fitEndWidth: width,
1475
+ paintEndWidth: width,
1476
+ breakAfter: true,
1477
+ breakable: !atomic && atoms.length > 1,
1478
+ atomic
949
1479
  };
950
1480
  }
951
- function measurePreparedMinContentWidth(prepared, overflowWrap = "break-word") {
952
- let maxWidth = 0;
953
- let maxAnyWidth = 0;
954
- for (let index = 0; index < prepared.widths.length; index += 1) {
955
- const segmentWidth = prepared.widths[index] ?? 0;
956
- maxAnyWidth = Math.max(maxAnyWidth, segmentWidth);
957
- const segment = prepared.segments[index];
958
- if (segment != null && segment.trim().length > 0) {
959
- const breakableWidths = prepared.breakableFitAdvances[index];
960
- const minContentWidth = overflowWrap === "anywhere" && breakableWidths != null && breakableWidths.length > 0 ? breakableWidths.reduce((widest, width) => Math.max(widest, width), 0) : segmentWidth;
961
- maxWidth = Math.max(maxWidth, minContentWidth);
1481
+ function makeSpaceUnit(atoms) {
1482
+ const width = measureAtomSequenceWidth(atoms);
1483
+ return {
1484
+ kind: "space",
1485
+ atoms,
1486
+ width,
1487
+ fitEndWidth: 0,
1488
+ paintEndWidth: atoms.every((atom) => atom.preservesLineEnd) ? width : 0,
1489
+ breakAfter: true,
1490
+ breakable: atoms.length > 1,
1491
+ atomic: false
1492
+ };
1493
+ }
1494
+ function tokenizeChunkAtoms(chunkAtoms, wordBreak) {
1495
+ const units = [];
1496
+ let index = 0;
1497
+ while (index < chunkAtoms.length) {
1498
+ const atom = chunkAtoms[index];
1499
+ if (atom.atomicGroupId != null) {
1500
+ const start = index;
1501
+ const atomicGroupId = atom.atomicGroupId;
1502
+ while (index < chunkAtoms.length && chunkAtoms[index]?.atomicGroupId === atomicGroupId) index += 1;
1503
+ units.push(makeTextUnit(chunkAtoms.slice(start, index), true));
1504
+ continue;
962
1505
  }
1506
+ if (atom.kind === "space") {
1507
+ const start = index;
1508
+ while (index < chunkAtoms.length && chunkAtoms[index]?.kind === "space" && chunkAtoms[index]?.atomicGroupId == null) index += 1;
1509
+ units.push(makeSpaceUnit(chunkAtoms.slice(start, index)));
1510
+ continue;
1511
+ }
1512
+ const start = index;
1513
+ while (index < chunkAtoms.length && chunkAtoms[index]?.kind === "text" && chunkAtoms[index]?.atomicGroupId == null) index += 1;
1514
+ const textRunAtoms = chunkAtoms.slice(start, index);
1515
+ const runText = joinAtomText(textRunAtoms);
1516
+ const rawUnits = isCJK(runText) ? buildBaseCjkUnits(textRunAtoms) : splitAtomsByWordSegments(textRunAtoms);
1517
+ const normalizedUnits = wordBreak === "keep-all" && isCJK(runText) ? mergeKeepAllUnits(rawUnits) : rawUnits;
1518
+ for (const unitAtoms of normalizedUnits) units.push(makeTextUnit(unitAtoms.slice(), false));
963
1519
  }
964
- return maxWidth > 0 ? maxWidth : maxAnyWidth;
1520
+ return units;
965
1521
  }
966
- function getPreparedUnits(prepared) {
1522
+ function buildPreparedInlineLayout(items, whiteSpace, wordBreak) {
1523
+ const atomChunks = whiteSpace === "pre-wrap" ? buildPreWrapAtoms(items) : buildCollapsedWhitespaceAtoms(items);
1524
+ const chunks = [];
967
1525
  const units = [];
968
- for (let index = 0; index < prepared.segments.length; index += 1) {
969
- const segment = prepared.segments[index] ?? "";
970
- const segmentWidth = prepared.widths[index] ?? 0;
971
- const breakableWidths = prepared.breakableFitAdvances[index];
972
- if (breakableWidths != null && segment.length > 0) {
973
- const graphemes = splitGraphemes(segment);
974
- if (graphemes.length === breakableWidths.length) {
975
- for (let graphemeIndex = 0; graphemeIndex < graphemes.length; graphemeIndex += 1) units.push({
976
- text: graphemes[graphemeIndex] ?? "",
977
- width: breakableWidths[graphemeIndex] ?? 0
978
- });
1526
+ for (const atomChunk of atomChunks) {
1527
+ const startUnit = units.length;
1528
+ const chunkUnits = tokenizeChunkAtoms(atomChunk, wordBreak);
1529
+ units.push(...chunkUnits);
1530
+ chunks.push({
1531
+ startUnit,
1532
+ endUnit: units.length
1533
+ });
1534
+ }
1535
+ return {
1536
+ units,
1537
+ chunks,
1538
+ whiteSpace,
1539
+ wordBreak,
1540
+ lineStatsCache: /* @__PURE__ */ new Map(),
1541
+ minContentWidthCache: /* @__PURE__ */ new Map()
1542
+ };
1543
+ }
1544
+ function readNumberLruValue(cache, key) {
1545
+ const cached = cache.get(key);
1546
+ if (cached == null) return;
1547
+ cache.delete(key);
1548
+ cache.set(key, cached);
1549
+ return cached;
1550
+ }
1551
+ function writeNumberLruValue(cache, key, value, capacity) {
1552
+ if (cache.has(key)) cache.delete(key);
1553
+ else if (cache.size >= capacity) {
1554
+ const firstKey = cache.keys().next().value;
1555
+ if (firstKey != null) cache.delete(firstKey);
1556
+ }
1557
+ cache.set(key, value);
1558
+ return value;
1559
+ }
1560
+ function cloneCursor(cursor) {
1561
+ return {
1562
+ chunkIndex: cursor.chunkIndex,
1563
+ unitIndex: cursor.unitIndex,
1564
+ atomIndex: cursor.atomIndex
1565
+ };
1566
+ }
1567
+ function cursorEquals(a, b) {
1568
+ return a.chunkIndex === b.chunkIndex && a.unitIndex === b.unitIndex && a.atomIndex === b.atomIndex;
1569
+ }
1570
+ function fits(width, maxWidth) {
1571
+ return width <= maxWidth + LINE_FIT_EPSILON;
1572
+ }
1573
+ function getVisibleEndAfterBreak(chunkIndex, unitIndex, unit) {
1574
+ if (unit.kind === "space" && unit.paintEndWidth === 0) return {
1575
+ chunkIndex,
1576
+ unitIndex,
1577
+ atomIndex: 0
1578
+ };
1579
+ return {
1580
+ chunkIndex,
1581
+ unitIndex: unitIndex + 1,
1582
+ atomIndex: 0
1583
+ };
1584
+ }
1585
+ function fitPartialUnit(unit, startAtomIndex, lineWidth, maxWidth) {
1586
+ let width = 0;
1587
+ let count = 0;
1588
+ for (let index = startAtomIndex; index < unit.atoms.length; index += 1) {
1589
+ const atom = unit.atoms[index];
1590
+ const atomWidth = atom.width + atom.extraWidthAfter;
1591
+ if (count > 0 && !fits(lineWidth + width + atomWidth, maxWidth)) break;
1592
+ if (count === 0 || fits(lineWidth + width + atomWidth, maxWidth)) {
1593
+ width += atomWidth;
1594
+ count += 1;
1595
+ continue;
1596
+ }
1597
+ break;
1598
+ }
1599
+ return {
1600
+ count,
1601
+ width
1602
+ };
1603
+ }
1604
+ function stepChunkLine(prepared, chunkIndex, startUnitIndex, startAtomIndex, maxWidth) {
1605
+ const chunk = prepared.chunks[chunkIndex];
1606
+ if (chunk == null || startUnitIndex >= chunk.endUnit) return null;
1607
+ const start = {
1608
+ chunkIndex,
1609
+ unitIndex: startUnitIndex,
1610
+ atomIndex: startAtomIndex
1611
+ };
1612
+ let unitIndex = startUnitIndex;
1613
+ let atomIndex = startAtomIndex;
1614
+ let lineWidth = 0;
1615
+ let hasContent = false;
1616
+ let pendingBreak = null;
1617
+ while (unitIndex < chunk.endUnit) {
1618
+ const unit = prepared.units[unitIndex];
1619
+ if (atomIndex > 0) {
1620
+ const fitted = fitPartialUnit(unit, atomIndex, lineWidth, maxWidth);
1621
+ if (fitted.count === 0) {
1622
+ if (hasContent) return {
1623
+ width: lineWidth,
1624
+ start,
1625
+ end: {
1626
+ chunkIndex,
1627
+ unitIndex,
1628
+ atomIndex
1629
+ },
1630
+ next: {
1631
+ chunkIndex,
1632
+ unitIndex,
1633
+ atomIndex
1634
+ }
1635
+ };
1636
+ const atom = unit.atoms[atomIndex];
1637
+ return {
1638
+ width: atom.width + atom.extraWidthAfter,
1639
+ start,
1640
+ end: {
1641
+ chunkIndex,
1642
+ unitIndex,
1643
+ atomIndex: atomIndex + 1
1644
+ },
1645
+ next: {
1646
+ chunkIndex,
1647
+ unitIndex,
1648
+ atomIndex: atomIndex + 1
1649
+ }
1650
+ };
1651
+ }
1652
+ if (atomIndex + fitted.count >= unit.atoms.length) {
1653
+ lineWidth += fitted.width;
1654
+ hasContent = true;
1655
+ atomIndex = 0;
1656
+ unitIndex += 1;
1657
+ pendingBreak = {
1658
+ end: {
1659
+ chunkIndex,
1660
+ unitIndex,
1661
+ atomIndex: 0
1662
+ },
1663
+ next: {
1664
+ chunkIndex,
1665
+ unitIndex,
1666
+ atomIndex: 0
1667
+ },
1668
+ width: lineWidth
1669
+ };
1670
+ continue;
1671
+ }
1672
+ return {
1673
+ width: lineWidth + fitted.width,
1674
+ start,
1675
+ end: {
1676
+ chunkIndex,
1677
+ unitIndex,
1678
+ atomIndex: atomIndex + fitted.count
1679
+ },
1680
+ next: {
1681
+ chunkIndex,
1682
+ unitIndex,
1683
+ atomIndex: atomIndex + fitted.count
1684
+ }
1685
+ };
1686
+ }
1687
+ if (!hasContent) {
1688
+ if (fits(unit.width, maxWidth) || unit.atomic || !unit.breakable) {
1689
+ lineWidth = unit.width;
1690
+ hasContent = true;
1691
+ const next = {
1692
+ chunkIndex,
1693
+ unitIndex: unitIndex + 1,
1694
+ atomIndex: 0
1695
+ };
1696
+ pendingBreak = {
1697
+ end: getVisibleEndAfterBreak(chunkIndex, unitIndex, unit),
1698
+ next,
1699
+ width: unit.paintEndWidth
1700
+ };
1701
+ unitIndex += 1;
1702
+ continue;
1703
+ }
1704
+ const fitted = fitPartialUnit(unit, 0, 0, maxWidth);
1705
+ if (fitted.count >= unit.atoms.length) {
1706
+ lineWidth = fitted.width;
1707
+ hasContent = true;
1708
+ const next = {
1709
+ chunkIndex,
1710
+ unitIndex: unitIndex + 1,
1711
+ atomIndex: 0
1712
+ };
1713
+ pendingBreak = {
1714
+ end: next,
1715
+ next,
1716
+ width: lineWidth
1717
+ };
1718
+ unitIndex += 1;
979
1719
  continue;
980
1720
  }
1721
+ return {
1722
+ width: fitted.width,
1723
+ start,
1724
+ end: {
1725
+ chunkIndex,
1726
+ unitIndex,
1727
+ atomIndex: fitted.count
1728
+ },
1729
+ next: {
1730
+ chunkIndex,
1731
+ unitIndex,
1732
+ atomIndex: fitted.count
1733
+ }
1734
+ };
981
1735
  }
982
- if (segment.length > 0 || segmentWidth > 0) units.push({
983
- text: segment,
984
- width: segmentWidth
985
- });
1736
+ if (fits(lineWidth + unit.width, maxWidth)) {
1737
+ const nextWidth = lineWidth + unit.width;
1738
+ lineWidth = nextWidth;
1739
+ const next = {
1740
+ chunkIndex,
1741
+ unitIndex: unitIndex + 1,
1742
+ atomIndex: 0
1743
+ };
1744
+ pendingBreak = {
1745
+ end: getVisibleEndAfterBreak(chunkIndex, unitIndex, unit),
1746
+ next,
1747
+ width: nextWidth - unit.width + unit.paintEndWidth
1748
+ };
1749
+ unitIndex += 1;
1750
+ continue;
1751
+ }
1752
+ if (fits(lineWidth + unit.fitEndWidth, maxWidth)) {
1753
+ const next = {
1754
+ chunkIndex,
1755
+ unitIndex: unitIndex + 1,
1756
+ atomIndex: 0
1757
+ };
1758
+ return {
1759
+ width: lineWidth + unit.paintEndWidth,
1760
+ start,
1761
+ end: getVisibleEndAfterBreak(chunkIndex, unitIndex, unit),
1762
+ next
1763
+ };
1764
+ }
1765
+ if (pendingBreak != null) return {
1766
+ width: pendingBreak.width,
1767
+ start,
1768
+ end: pendingBreak.end,
1769
+ next: pendingBreak.next
1770
+ };
1771
+ return {
1772
+ width: lineWidth,
1773
+ start,
1774
+ end: {
1775
+ chunkIndex,
1776
+ unitIndex,
1777
+ atomIndex: 0
1778
+ },
1779
+ next: {
1780
+ chunkIndex,
1781
+ unitIndex,
1782
+ atomIndex: 0
1783
+ }
1784
+ };
986
1785
  }
987
- return units;
1786
+ if (!hasContent) return null;
1787
+ if (pendingBreak != null && pendingBreak.next.unitIndex === chunk.endUnit && pendingBreak.next.atomIndex === 0) return {
1788
+ width: pendingBreak.width,
1789
+ start,
1790
+ end: pendingBreak.end,
1791
+ next: pendingBreak.next
1792
+ };
1793
+ return {
1794
+ width: lineWidth,
1795
+ start,
1796
+ end: {
1797
+ chunkIndex,
1798
+ unitIndex,
1799
+ atomIndex: 0
1800
+ },
1801
+ next: {
1802
+ chunkIndex,
1803
+ unitIndex,
1804
+ atomIndex: 0
1805
+ }
1806
+ };
988
1807
  }
989
- function joinUnitText(units, start, end) {
990
- if (start >= end) return "";
991
- return units.slice(start, end).map((unit) => unit.text).join("");
1808
+ function getPreparedLineStart(prepared) {
1809
+ for (let chunkIndex = 0; chunkIndex < prepared.chunks.length; chunkIndex += 1) {
1810
+ const chunk = prepared.chunks[chunkIndex];
1811
+ if (chunk.startUnit === chunk.endUnit) return {
1812
+ chunkIndex,
1813
+ unitIndex: chunk.startUnit,
1814
+ atomIndex: 0
1815
+ };
1816
+ return {
1817
+ chunkIndex,
1818
+ unitIndex: chunk.startUnit,
1819
+ atomIndex: 0
1820
+ };
1821
+ }
1822
+ }
1823
+ function layoutNextPreparedLine(prepared, start, maxWidth) {
1824
+ const chunk = prepared.chunks[start.chunkIndex];
1825
+ if (chunk == null) return null;
1826
+ if (chunk.startUnit === chunk.endUnit) return cursorEquals(start, {
1827
+ chunkIndex: start.chunkIndex,
1828
+ unitIndex: chunk.endUnit,
1829
+ atomIndex: 0
1830
+ }) ? null : {
1831
+ width: 0,
1832
+ start: cloneCursor(start),
1833
+ end: {
1834
+ chunkIndex: start.chunkIndex,
1835
+ unitIndex: chunk.endUnit,
1836
+ atomIndex: 0
1837
+ },
1838
+ next: {
1839
+ chunkIndex: start.chunkIndex,
1840
+ unitIndex: chunk.endUnit,
1841
+ atomIndex: 0
1842
+ }
1843
+ };
1844
+ return stepChunkLine(prepared, start.chunkIndex, start.unitIndex, start.atomIndex, maxWidth);
1845
+ }
1846
+ function walkPreparedLineRanges(prepared, maxWidth, onLine) {
1847
+ let lineCount = 0;
1848
+ for (let chunkIndex = 0; chunkIndex < prepared.chunks.length; chunkIndex += 1) {
1849
+ const chunk = prepared.chunks[chunkIndex];
1850
+ if (chunk.startUnit === chunk.endUnit) {
1851
+ const cursor = {
1852
+ chunkIndex,
1853
+ unitIndex: chunk.startUnit,
1854
+ atomIndex: 0
1855
+ };
1856
+ if (onLine({
1857
+ width: 0,
1858
+ start: cursor,
1859
+ end: cursor,
1860
+ next: cursor
1861
+ }) === false) {
1862
+ lineCount += 1;
1863
+ return lineCount;
1864
+ }
1865
+ lineCount += 1;
1866
+ continue;
1867
+ }
1868
+ let cursor = {
1869
+ chunkIndex,
1870
+ unitIndex: chunk.startUnit,
1871
+ atomIndex: 0
1872
+ };
1873
+ while (true) {
1874
+ const line = layoutNextPreparedLine(prepared, cursor, maxWidth);
1875
+ if (line == null) break;
1876
+ if (onLine(line) === false) {
1877
+ lineCount += 1;
1878
+ return lineCount;
1879
+ }
1880
+ lineCount += 1;
1881
+ if (cursorEquals(line.next, cursor)) break;
1882
+ cursor = line.next;
1883
+ if (cursor.unitIndex >= chunk.endUnit && cursor.atomIndex === 0) break;
1884
+ }
1885
+ }
1886
+ return lineCount;
1887
+ }
1888
+ function forEachAtomFromCursorToEnd(prepared, start, cb) {
1889
+ for (let chunkIndex = start.chunkIndex; chunkIndex < prepared.chunks.length; chunkIndex += 1) {
1890
+ const chunk = prepared.chunks[chunkIndex];
1891
+ if (chunk == null) continue;
1892
+ forEachAtomInRange(prepared, chunkIndex === start.chunkIndex ? start : {
1893
+ chunkIndex,
1894
+ unitIndex: chunk.startUnit,
1895
+ atomIndex: 0
1896
+ }, {
1897
+ chunkIndex,
1898
+ unitIndex: chunk.endUnit,
1899
+ atomIndex: 0
1900
+ }, cb);
1901
+ }
1902
+ }
1903
+ function measurePreparedLineStats(prepared, maxWidth) {
1904
+ const cached = readNumberLruValue(prepared.lineStatsCache, maxWidth);
1905
+ if (cached != null) return cached;
1906
+ let lineCount = 0;
1907
+ let maxLineWidth = 0;
1908
+ walkPreparedLineRanges(prepared, maxWidth, (line) => {
1909
+ lineCount += 1;
1910
+ if (line.width > maxLineWidth) maxLineWidth = line.width;
1911
+ });
1912
+ return writeNumberLruValue(prepared.lineStatsCache, maxWidth, {
1913
+ lineCount,
1914
+ maxLineWidth
1915
+ }, PREPARED_LINE_STATS_CACHE_CAPACITY);
1916
+ }
1917
+ function forEachAtomInRange(prepared, start, end, cb) {
1918
+ if (start.chunkIndex !== end.chunkIndex) throw new Error("Atom range iteration only supports a single chunk.");
1919
+ for (let unitIndex = start.unitIndex; unitIndex < end.unitIndex; unitIndex += 1) {
1920
+ const unit = prepared.units[unitIndex];
1921
+ const atomStart = unitIndex === start.unitIndex ? start.atomIndex : 0;
1922
+ const atomEnd = unitIndex === end.unitIndex ? end.atomIndex : unit.atoms.length;
1923
+ for (let atomIndex = atomStart; atomIndex < atomEnd; atomIndex += 1) {
1924
+ const atom = unit.atoms[atomIndex];
1925
+ if (atom != null) cb(atom);
1926
+ }
1927
+ }
1928
+ if (end.unitIndex < prepared.units.length) {
1929
+ const unit = prepared.units[end.unitIndex];
1930
+ if (unit != null && start.unitIndex === end.unitIndex) for (let atomIndex = start.atomIndex; atomIndex < end.atomIndex; atomIndex += 1) {
1931
+ const atom = unit.atoms[atomIndex];
1932
+ if (atom != null) cb(atom);
1933
+ }
1934
+ }
1935
+ }
1936
+ function collectAtomsInRange(prepared, start, end) {
1937
+ const atoms = [];
1938
+ if (start.unitIndex === end.unitIndex) {
1939
+ const unit = prepared.units[start.unitIndex];
1940
+ if (unit == null) return atoms;
1941
+ for (let atomIndex = start.atomIndex; atomIndex < end.atomIndex; atomIndex += 1) {
1942
+ const atom = unit.atoms[atomIndex];
1943
+ if (atom != null) atoms.push(atom);
1944
+ }
1945
+ return atoms;
1946
+ }
1947
+ if (prepared.chunks[start.chunkIndex] == null) return atoms;
1948
+ for (let unitIndex = start.unitIndex; unitIndex < end.unitIndex; unitIndex += 1) {
1949
+ const unit = prepared.units[unitIndex];
1950
+ const atomStart = unitIndex === start.unitIndex ? start.atomIndex : 0;
1951
+ for (let atomIndex = atomStart; atomIndex < unit.atoms.length; atomIndex += 1) {
1952
+ const atom = unit.atoms[atomIndex];
1953
+ if (atom != null) atoms.push(atom);
1954
+ }
1955
+ }
1956
+ const endUnit = prepared.units[end.unitIndex];
1957
+ if (endUnit != null) for (let atomIndex = 0; atomIndex < end.atomIndex; atomIndex += 1) {
1958
+ const atom = endUnit.atoms[atomIndex];
1959
+ if (atom != null) atoms.push(atom);
1960
+ }
1961
+ return atoms;
1962
+ }
1963
+ function collectLineAtoms(prepared, line) {
1964
+ return {
1965
+ atoms: collectAtomsInRange(prepared, line.start, line.end),
1966
+ width: line.width
1967
+ };
1968
+ }
1969
+ function materializePreparedLineText(prepared, line) {
1970
+ return joinAtomText(collectLineAtoms(prepared, line).atoms);
1971
+ }
1972
+ function flattenPreparedLineAtoms(prepared, line) {
1973
+ return collectLineAtoms(prepared, line).atoms;
1974
+ }
1975
+ function measurePreparedMinContentWidth$1(prepared, overflowWrap = "break-word") {
1976
+ const cached = prepared.minContentWidthCache.get(overflowWrap);
1977
+ if (cached != null) return cached;
1978
+ let maxWidth = 0;
1979
+ let maxAnyWidth = 0;
1980
+ for (const unit of prepared.units) {
1981
+ if (unit.width > maxAnyWidth) maxAnyWidth = unit.width;
1982
+ if (unit.kind !== "text") continue;
1983
+ const candidateWidth = overflowWrap === "anywhere" && unit.breakable ? unit.atoms.reduce((widest, atom) => Math.max(widest, atom.width + atom.extraWidthAfter), 0) : unit.width;
1984
+ if (candidateWidth > maxWidth) maxWidth = candidateWidth;
1985
+ }
1986
+ const resolvedWidth = maxWidth > 0 ? maxWidth : maxAnyWidth;
1987
+ prepared.minContentWidthCache.set(overflowWrap, resolvedWidth);
1988
+ return resolvedWidth;
1989
+ }
1990
+ function measureAtomsWidth(atoms) {
1991
+ let total = 0;
1992
+ for (let index = 0; index < atoms.length; index += 1) {
1993
+ const atom = atoms[index];
1994
+ if (atom != null) total += atom.width + atom.extraWidthAfter;
1995
+ }
1996
+ return total;
1997
+ }
1998
+ function readPreparedInlineLayout(key, items, whiteSpace, wordBreak) {
1999
+ const cached = readLruValue$1(preparedInlineCache, key);
2000
+ if (cached != null) return cached;
2001
+ return writeLruValue$1(preparedInlineCache, key, buildPreparedInlineLayout(items, whiteSpace, wordBreak), PREPARED_INLINE_CACHE_CAPACITY);
2002
+ }
2003
+ function getPlainPreparedKey(text, font, whiteSpace, wordBreak) {
2004
+ return `plain\u0000${font}\u0000${whiteSpace}\u0000${wordBreak}\u0000${text}`;
2005
+ }
2006
+ function getRichPreparedKey(spans, defaultFont, whiteSpace, wordBreak) {
2007
+ let key = "";
2008
+ for (let index = 0; index < spans.length; index += 1) {
2009
+ const span = spans[index];
2010
+ if (index > 0) key += "";
2011
+ key += span.font ?? defaultFont;
2012
+ key += "\0";
2013
+ key += span.text;
2014
+ key += "\0";
2015
+ key += span.break ?? "";
2016
+ key += "\0";
2017
+ key += span.extraWidth ?? 0;
2018
+ }
2019
+ key += "";
2020
+ key += whiteSpace;
2021
+ key += "";
2022
+ key += wordBreak;
2023
+ return key;
2024
+ }
2025
+ function createPlainSourceItems(text, font) {
2026
+ return [{
2027
+ text,
2028
+ font,
2029
+ itemIndex: 0,
2030
+ breakMode: "normal",
2031
+ extraWidth: 0
2032
+ }];
2033
+ }
2034
+ function createRichSourceItems(spans, defaultFont) {
2035
+ return spans.map((span, index) => ({
2036
+ text: span.text,
2037
+ font: span.font ?? defaultFont,
2038
+ itemIndex: index,
2039
+ breakMode: span.break ?? "normal",
2040
+ extraWidth: span.extraWidth ?? 0
2041
+ }));
2042
+ }
2043
+ //#endregion
2044
+ //#region src/text/plain-core.ts
2045
+ function readPreparedText(text, font, whiteSpace, wordBreak) {
2046
+ return readPreparedInlineLayout(getPlainPreparedKey(text, font, whiteSpace, wordBreak), createPlainSourceItems(text, font), whiteSpace, wordBreak);
2047
+ }
2048
+ function measurePreparedMinContentWidth(prepared, overflowWrap = "break-word") {
2049
+ return measurePreparedMinContentWidth$1(prepared, overflowWrap);
992
2050
  }
993
2051
  //#endregion
994
2052
  //#region src/text/plain.ts
995
- const LINE_START_CURSOR = {
996
- segmentIndex: 0,
997
- graphemeIndex: 0
998
- };
2053
+ function measureTextLayoutWidth(lines) {
2054
+ let width = 0;
2055
+ for (let index = 0; index < lines.length; index += 1) {
2056
+ const line = lines[index];
2057
+ if (line != null && line.width > width) width = line.width;
2058
+ }
2059
+ return width;
2060
+ }
999
2061
  function clampMaxWidth(maxWidth) {
1000
2062
  return Math.max(0, maxWidth);
1001
2063
  }
@@ -1014,22 +2076,33 @@ function createEllipsisOnlyLayout(ctx, maxWidth, shift) {
1014
2076
  overflowed: true
1015
2077
  };
1016
2078
  }
1017
- function toTextBlockLayout(lines, shift) {
1018
- const mappedLines = lines.map((line) => ({
1019
- width: line.width,
1020
- text: line.text,
1021
- shift
1022
- }));
2079
+ function toTextBlockLayout(lines, prepared, shift) {
2080
+ const mappedLines = [];
2081
+ for (let index = 0; index < lines.length; index += 1) {
2082
+ const line = lines[index];
2083
+ mappedLines.push({
2084
+ width: line.width,
2085
+ text: materializePreparedLineText(prepared, line),
2086
+ shift
2087
+ });
2088
+ }
1023
2089
  return {
1024
- width: mappedLines.reduce((maxLineWidth, line) => Math.max(maxLineWidth, line.width), 0),
2090
+ width: measureTextLayoutWidth(mappedLines),
1025
2091
  lines: mappedLines
1026
2092
  };
1027
2093
  }
1028
- function layoutPreparedEllipsis(ctx, prepared, text, maxWidth, shift, position, forceEllipsis = false) {
1029
- const intrinsicWidth = measureNaturalWidth(prepared);
2094
+ function collectEllipsisLayout(ctx, atoms, maxWidth, shift, position, forceEllipsis = false) {
2095
+ const widths = [];
2096
+ let intrinsicWidth = 0;
2097
+ for (let index = 0; index < atoms.length; index += 1) {
2098
+ const atom = atoms[index];
2099
+ const width = atom.width + atom.extraWidthAfter;
2100
+ widths.push(width);
2101
+ intrinsicWidth += width;
2102
+ }
1030
2103
  if (!forceEllipsis && intrinsicWidth <= maxWidth) return {
1031
2104
  width: intrinsicWidth,
1032
- text,
2105
+ text: atoms.map((atom) => atom.text).join(""),
1033
2106
  shift,
1034
2107
  overflowed: false
1035
2108
  };
@@ -1040,65 +2113,120 @@ function layoutPreparedEllipsis(ctx, prepared, text, maxWidth, shift, position,
1040
2113
  shift,
1041
2114
  overflowed: true
1042
2115
  };
1043
- const units = getPreparedUnits(prepared);
1044
- if (units.length === 0) return createEllipsisOnlyLayout(ctx, maxWidth, shift);
1045
- const availableWidth = Math.max(0, maxWidth - ellipsisWidth);
1046
- const prefixWidths = buildPrefixWidths(units.map((unit) => unit.width));
1047
- const suffixWidths = buildSuffixWidths(units.map((unit) => unit.width));
1048
- const { prefixCount, suffixCount } = selectEllipsisUnitCounts({
1049
- position,
1050
- prefixWidths,
1051
- suffixWidths,
1052
- unitCount: units.length,
1053
- availableWidth
2116
+ if (atoms.length === 0) return createEllipsisOnlyLayout(ctx, maxWidth, shift);
2117
+ const selection = resolveEllipsisSelection({
2118
+ widths,
2119
+ ellipsisWidth,
2120
+ maxWidth,
2121
+ position
1054
2122
  });
1055
- const prefixWidth = prefixWidths[prefixCount] ?? 0;
1056
- const suffixWidth = suffixWidths[suffixCount] ?? 0;
1057
- const prefixText = joinUnitText(units, 0, prefixCount);
1058
- const suffixText = joinUnitText(units, units.length - suffixCount, units.length);
2123
+ if (selection == null) return {
2124
+ width: 0,
2125
+ text: "",
2126
+ shift,
2127
+ overflowed: true
2128
+ };
2129
+ const { prefixCount, suffixCount, width } = selection;
2130
+ const prefixText = atoms.slice(0, prefixCount).map((atom) => atom.text).join("");
2131
+ const suffixText = atoms.slice(atoms.length - suffixCount).map((atom) => atom.text).join("");
1059
2132
  return {
1060
- width: prefixWidth + ellipsisWidth + suffixWidth,
1061
- text: `${prefixText}…${suffixText}`,
2133
+ width,
2134
+ text: position === "start" ? `…${suffixText}` : position === "middle" ? `${prefixText}…${suffixText}` : `${prefixText}…`,
1062
2135
  shift,
1063
2136
  overflowed: true
1064
2137
  };
1065
2138
  }
1066
- function layoutForcedEllipsizedLine(ctx, text, maxWidth, shift, whiteSpace = "normal", wordBreak = "normal") {
1067
- if (text.length === 0) return createEllipsisOnlyLayout(ctx, maxWidth, shift);
1068
- return layoutPreparedEllipsis(ctx, readPreparedText(text, ctx.graphics.font, whiteSpace, wordBreak), text, maxWidth, shift, "end", true);
2139
+ function getFirstLineRange$1(prepared) {
2140
+ const start = getPreparedLineStart(prepared);
2141
+ if (start == null) return;
2142
+ return layoutNextPreparedLine(prepared, start, Number.POSITIVE_INFINITY) ?? void 0;
2143
+ }
2144
+ function walkLines$1(prepared, maxWidth) {
2145
+ const lines = [];
2146
+ walkPreparedLineRanges(prepared, maxWidth, (line) => {
2147
+ lines.push(line);
2148
+ });
2149
+ return lines;
2150
+ }
2151
+ function collectVisibleLines$1(prepared, maxWidth, maxLines) {
2152
+ const lines = [];
2153
+ let overflowed = false;
2154
+ walkPreparedLineRanges(prepared, maxWidth, (line) => {
2155
+ if (lines.length < maxLines) {
2156
+ lines.push(line);
2157
+ return true;
2158
+ }
2159
+ overflowed = true;
2160
+ return false;
2161
+ });
2162
+ return {
2163
+ lines,
2164
+ overflowed
2165
+ };
2166
+ }
2167
+ function collectEndEllipsisLayoutFromCursor(ctx, prepared, start, maxWidth, shift) {
2168
+ const ellipsisWidth = measureEllipsisWidth(ctx);
2169
+ if (ellipsisWidth > maxWidth) return {
2170
+ width: 0,
2171
+ text: "",
2172
+ shift,
2173
+ overflowed: true
2174
+ };
2175
+ const widths = [];
2176
+ forEachAtomFromCursorToEnd(prepared, start, (atom) => {
2177
+ widths.push(atom.width + atom.extraWidthAfter);
2178
+ });
2179
+ if (widths.length === 0) return createEllipsisOnlyLayout(ctx, maxWidth, shift);
2180
+ const selection = resolveEllipsisSelection({
2181
+ widths,
2182
+ ellipsisWidth,
2183
+ maxWidth,
2184
+ position: "end"
2185
+ });
2186
+ if (selection == null) return {
2187
+ width: 0,
2188
+ text: "",
2189
+ shift,
2190
+ overflowed: true
2191
+ };
2192
+ const { prefixCount, width } = selection;
2193
+ let text = "";
2194
+ let atomIndex = 0;
2195
+ forEachAtomFromCursorToEnd(prepared, start, (atom) => {
2196
+ if (atomIndex < prefixCount) text += atom.text;
2197
+ atomIndex += 1;
2198
+ });
2199
+ return {
2200
+ width,
2201
+ text: `${text}…`,
2202
+ shift,
2203
+ overflowed: true
2204
+ };
1069
2205
  }
1070
2206
  function layoutFirstLineIntrinsic(ctx, text, whiteSpace = "normal", wordBreak = "normal") {
1071
- const firstLine = readPreparedFirstLine(ctx, text, whiteSpace, wordBreak);
1072
- if (firstLine == null) return {
2207
+ const prepared = readPreparedText(text, ctx.graphics.font, whiteSpace, wordBreak);
2208
+ const line = getFirstLineRange$1(prepared);
2209
+ if (line == null) return {
1073
2210
  width: 0,
1074
2211
  text: "",
1075
2212
  shift: 0
1076
2213
  };
1077
- const shift = measureFontShift(ctx);
1078
2214
  return {
1079
- width: measureNaturalWidth(firstLine.prepared),
1080
- text: firstLine.text,
1081
- shift
2215
+ width: line.width,
2216
+ text: materializePreparedLineText(prepared, line),
2217
+ shift: measureFontShift(ctx)
1082
2218
  };
1083
2219
  }
1084
2220
  function measureTextIntrinsic(ctx, text, whiteSpace = "normal", wordBreak = "normal") {
1085
- const { maxLineWidth: width, lineCount } = measureLineStats(readPreparedText(text, ctx.graphics.font, whiteSpace, wordBreak), INTRINSIC_MAX_WIDTH);
1086
- if (lineCount === 0) return {
1087
- width: 0,
1088
- lineCount: 0
1089
- };
2221
+ const { maxLineWidth: width, lineCount } = measurePreparedLineStats(readPreparedText(text, ctx.graphics.font, whiteSpace, wordBreak), Number.POSITIVE_INFINITY);
1090
2222
  return {
1091
2223
  width,
1092
2224
  lineCount
1093
2225
  };
1094
2226
  }
1095
2227
  function layoutTextIntrinsic(ctx, text, whiteSpace = "normal", wordBreak = "normal") {
1096
- const intrinsic = layoutWithLines(readPreparedText(text, ctx.graphics.font, whiteSpace, wordBreak), INTRINSIC_MAX_WIDTH, 0);
1097
- if (intrinsic.lines.length === 0) return {
1098
- width: 0,
1099
- lines: []
1100
- };
1101
- return toTextBlockLayout(intrinsic.lines, measureFontShift(ctx));
2228
+ const prepared = readPreparedText(text, ctx.graphics.font, whiteSpace, wordBreak);
2229
+ return toTextBlockLayout(walkLines$1(prepared, Number.POSITIVE_INFINITY), prepared, measureFontShift(ctx));
1102
2230
  }
1103
2231
  function layoutFirstLine(ctx, text, maxWidth, whiteSpace = "normal", wordBreak = "normal") {
1104
2232
  const clampedMaxWidth = clampMaxWidth(maxWidth);
@@ -1108,7 +2236,14 @@ function layoutFirstLine(ctx, text, maxWidth, whiteSpace = "normal", wordBreak =
1108
2236
  text: "",
1109
2237
  shift
1110
2238
  };
1111
- const line = layoutNextLine(readPreparedText(text, ctx.graphics.font, whiteSpace, wordBreak), LINE_START_CURSOR, clampedMaxWidth);
2239
+ const prepared = readPreparedText(text, ctx.graphics.font, whiteSpace, wordBreak);
2240
+ const start = getPreparedLineStart(prepared);
2241
+ if (start == null) return {
2242
+ width: 0,
2243
+ text: "",
2244
+ shift
2245
+ };
2246
+ const line = layoutNextPreparedLine(prepared, start, clampedMaxWidth);
1112
2247
  if (line == null) return {
1113
2248
  width: 0,
1114
2249
  text: "",
@@ -1116,19 +2251,12 @@ function layoutFirstLine(ctx, text, maxWidth, whiteSpace = "normal", wordBreak =
1116
2251
  };
1117
2252
  return {
1118
2253
  width: line.width,
1119
- text: line.text,
2254
+ text: materializePreparedLineText(prepared, line),
1120
2255
  shift
1121
2256
  };
1122
2257
  }
1123
2258
  function layoutEllipsizedFirstLine(ctx, text, maxWidth, ellipsisPosition = "end", whiteSpace = "normal", wordBreak = "normal") {
1124
2259
  const clampedMaxWidth = clampMaxWidth(maxWidth);
1125
- const firstLine = readPreparedFirstLine(ctx, text, whiteSpace, wordBreak);
1126
- if (firstLine == null) return {
1127
- width: 0,
1128
- text: "",
1129
- shift: 0,
1130
- overflowed: false
1131
- };
1132
2260
  const shift = measureFontShift(ctx);
1133
2261
  if (clampedMaxWidth === 0) return {
1134
2262
  width: 0,
@@ -1136,7 +2264,15 @@ function layoutEllipsizedFirstLine(ctx, text, maxWidth, ellipsisPosition = "end"
1136
2264
  shift,
1137
2265
  overflowed: true
1138
2266
  };
1139
- return layoutPreparedEllipsis(ctx, firstLine.prepared, firstLine.text, clampedMaxWidth, shift, ellipsisPosition);
2267
+ const prepared = readPreparedText(text, ctx.graphics.font, whiteSpace, wordBreak);
2268
+ const intrinsicLine = getFirstLineRange$1(prepared);
2269
+ if (intrinsicLine == null) return {
2270
+ width: 0,
2271
+ text: "",
2272
+ shift: 0,
2273
+ overflowed: false
2274
+ };
2275
+ return collectEllipsisLayout(ctx, flattenPreparedLineAtoms(prepared, intrinsicLine), clampedMaxWidth, shift, ellipsisPosition);
1140
2276
  }
1141
2277
  function measureText(ctx, text, maxWidth, whiteSpace = "normal", wordBreak = "normal") {
1142
2278
  const clampedMaxWidth = clampMaxWidth(maxWidth);
@@ -1144,7 +2280,7 @@ function measureText(ctx, text, maxWidth, whiteSpace = "normal", wordBreak = "no
1144
2280
  width: 0,
1145
2281
  lineCount: 0
1146
2282
  };
1147
- const { maxLineWidth: width, lineCount } = measureLineStats(readPreparedText(text, ctx.graphics.font, whiteSpace, wordBreak), clampedMaxWidth);
2283
+ const { maxLineWidth: width, lineCount } = measurePreparedLineStats(readPreparedText(text, ctx.graphics.font, whiteSpace, wordBreak), clampedMaxWidth);
1148
2284
  return {
1149
2285
  width,
1150
2286
  lineCount
@@ -1152,12 +2288,12 @@ function measureText(ctx, text, maxWidth, whiteSpace = "normal", wordBreak = "no
1152
2288
  }
1153
2289
  function measureTextMinContent(ctx, text, whiteSpace = "normal", wordBreak = "normal", overflowWrap = "break-word") {
1154
2290
  const prepared = readPreparedText(text, ctx.graphics.font, whiteSpace, wordBreak);
1155
- if (prepared.widths.length === 0) return {
2291
+ const width = measurePreparedMinContentWidth(prepared, overflowWrap);
2292
+ if (width === 0) return {
1156
2293
  width: 0,
1157
2294
  lineCount: 0
1158
2295
  };
1159
- const width = measurePreparedMinContentWidth(prepared, overflowWrap);
1160
- const { lineCount } = measureLineStats(prepared, Math.max(width, MIN_CONTENT_WIDTH_EPSILON));
2296
+ const { lineCount } = measurePreparedLineStats(prepared, Math.max(width, MIN_CONTENT_WIDTH_EPSILON));
1161
2297
  return {
1162
2298
  width,
1163
2299
  lineCount
@@ -1169,12 +2305,8 @@ function layoutText(ctx, text, maxWidth, whiteSpace = "normal", wordBreak = "nor
1169
2305
  width: 0,
1170
2306
  lines: []
1171
2307
  };
1172
- const layout = layoutWithLines(readPreparedText(text, ctx.graphics.font, whiteSpace, wordBreak), clampedMaxWidth, 0);
1173
- if (layout.lines.length === 0) return {
1174
- width: 0,
1175
- lines: []
1176
- };
1177
- return toTextBlockLayout(layout.lines, measureFontShift(ctx));
2308
+ const prepared = readPreparedText(text, ctx.graphics.font, whiteSpace, wordBreak);
2309
+ return toTextBlockLayout(walkLines$1(prepared, clampedMaxWidth), prepared, measureFontShift(ctx));
1178
2310
  }
1179
2311
  function layoutTextWithOverflow(ctx, text, maxWidth, options = {}) {
1180
2312
  const clampedMaxWidth = clampMaxWidth(maxWidth);
@@ -1182,46 +2314,61 @@ function layoutTextWithOverflow(ctx, text, maxWidth, options = {}) {
1182
2314
  const wordBreak = options.wordBreak ?? "normal";
1183
2315
  const overflow = options.overflow ?? "clip";
1184
2316
  const normalizedMaxLines = normalizeMaxLines(options.maxLines);
1185
- const layout = layoutText(ctx, text, clampedMaxWidth, whiteSpace, wordBreak);
1186
- if (normalizedMaxLines == null || layout.lines.length <= normalizedMaxLines) return {
1187
- width: layout.width,
1188
- lines: layout.lines.map((line) => ({
1189
- ...line,
2317
+ const prepared = readPreparedText(text, ctx.graphics.font, whiteSpace, wordBreak);
2318
+ const shift = measureFontShift(ctx);
2319
+ if (normalizedMaxLines == null) {
2320
+ const layout = toTextBlockLayout(walkLines$1(prepared, clampedMaxWidth), prepared, shift);
2321
+ return {
2322
+ width: layout.width,
2323
+ lines: layout.lines.map((line) => ({
2324
+ ...line,
2325
+ overflowed: false
2326
+ })),
2327
+ overflowed: false
2328
+ };
2329
+ }
2330
+ const { lines: visibleRanges, overflowed } = collectVisibleLines$1(prepared, clampedMaxWidth, normalizedMaxLines);
2331
+ if (!overflowed) {
2332
+ const layout = toTextBlockLayout(visibleRanges, prepared, shift);
2333
+ return {
2334
+ width: layout.width,
2335
+ lines: layout.lines.map((line) => ({
2336
+ ...line,
2337
+ overflowed: false
2338
+ })),
1190
2339
  overflowed: false
1191
- })),
2340
+ };
2341
+ }
2342
+ const visibleLines = visibleRanges.map((line) => ({
2343
+ width: line.width,
2344
+ text: materializePreparedLineText(prepared, line),
2345
+ shift,
1192
2346
  overflowed: false
1193
- };
1194
- const visibleLines = layout.lines.slice(0, normalizedMaxLines);
2347
+ }));
1195
2348
  if (overflow !== "ellipsis") return {
1196
- width: visibleLines.reduce((lineWidth, line) => Math.max(lineWidth, line.width), 0),
1197
- lines: visibleLines.map((line) => ({
1198
- ...line,
1199
- overflowed: false
1200
- })),
2349
+ width: measureTextLayoutWidth(visibleLines),
2350
+ lines: visibleLines,
1201
2351
  overflowed: true
1202
2352
  };
1203
- const shift = visibleLines[visibleLines.length - 1]?.shift ?? measureFontShift(ctx);
1204
- const lastVisibleLine = visibleLines[visibleLines.length - 1];
1205
- const ellipsizedLastLine = lastVisibleLine == null || lastVisibleLine.text.length === 0 ? createEllipsisOnlyLayout(ctx, clampedMaxWidth, shift) : layoutForcedEllipsizedLine(ctx, lastVisibleLine.text, clampedMaxWidth, shift, whiteSpace, wordBreak);
1206
- const lines = [...visibleLines.slice(0, -1).map((line) => ({
1207
- ...line,
1208
- overflowed: false
1209
- })), {
1210
- ...ellipsizedLastLine,
1211
- shift
1212
- }];
2353
+ const lastVisibleRange = visibleRanges[visibleRanges.length - 1];
2354
+ const ellipsizedLastLine = lastVisibleRange == null ? createEllipsisOnlyLayout(ctx, clampedMaxWidth, shift) : collectEndEllipsisLayoutFromCursor(ctx, prepared, lastVisibleRange.start, clampedMaxWidth, shift);
2355
+ const mergedLines = [...visibleLines.slice(0, -1), ellipsizedLastLine];
1213
2356
  return {
1214
- width: lines.reduce((lineWidth, line) => Math.max(lineWidth, line.width), 0),
1215
- lines,
2357
+ width: measureTextLayoutWidth(mergedLines),
2358
+ lines: mergedLines,
1216
2359
  overflowed: true
1217
2360
  };
1218
2361
  }
1219
2362
  //#endregion
1220
2363
  //#region src/text/rich.ts
1221
- const RICH_PREPARED_CACHE_CAPACITY = 256;
1222
- const LEADING_COLLAPSIBLE_BOUNDARY_RE = /^[ \t\n\f\r]+/;
1223
- const TRAILING_COLLAPSIBLE_BOUNDARY_RE = /[ \t\n\f\r]+$/;
1224
- const richPreparedCache = /* @__PURE__ */ new Map();
2364
+ function measureRichBlockWidth(lines) {
2365
+ let width = 0;
2366
+ for (let index = 0; index < lines.length; index += 1) {
2367
+ const line = lines[index];
2368
+ if (line != null && line.width > width) width = line.width;
2369
+ }
2370
+ return width;
2371
+ }
1225
2372
  function withFont(ctx, font, cb) {
1226
2373
  const previousFont = ctx.graphics.font;
1227
2374
  ctx.graphics.font = font;
@@ -1231,177 +2378,108 @@ function withFont(ctx, font, cb) {
1231
2378
  ctx.graphics.font = previousFont;
1232
2379
  }
1233
2380
  }
1234
- function getRichPreparedCacheKey(spans, defaultFont) {
1235
- return spans.map((span) => `${span.font ?? defaultFont}\u0000${span.text}\u0000${span.break ?? ""}\u0000${span.extraWidth ?? 0}`).join("");
1236
- }
1237
- function readRichPrepared(spans, defaultFont) {
1238
- const key = getRichPreparedCacheKey(spans, defaultFont);
1239
- const cached = readLruValue$1(richPreparedCache, key);
1240
- if (cached != null) return cached;
1241
- const items = spans.map((span) => ({
1242
- text: span.text,
1243
- font: span.font ?? defaultFont,
1244
- break: span.break,
1245
- extraWidth: span.extraWidth
1246
- }));
1247
- const preparedItemIndexBySourceItemIndex = buildPreparedItemIndexBySourceItemIndex(spans);
1248
- return writeLruValue$1(richPreparedCache, key, {
1249
- prepared: prepareRichInline(items),
1250
- preparedItemIndexBySourceItemIndex
1251
- }, RICH_PREPARED_CACHE_CAPACITY);
1252
- }
1253
- function trimRichInlineBoundaryWhitespace(text) {
1254
- return text.replace(LEADING_COLLAPSIBLE_BOUNDARY_RE, "").replace(TRAILING_COLLAPSIBLE_BOUNDARY_RE, "");
1255
- }
1256
- function buildPreparedItemIndexBySourceItemIndex(spans) {
1257
- const preparedItemIndexBySourceItemIndex = Array.from({ length: spans.length });
1258
- let preparedItemIndex = 0;
1259
- for (let index = 0; index < spans.length; index += 1) {
1260
- if (trimRichInlineBoundaryWhitespace(spans[index].text).length === 0) continue;
1261
- preparedItemIndexBySourceItemIndex[index] = preparedItemIndex;
1262
- preparedItemIndex += 1;
1263
- }
1264
- return preparedItemIndexBySourceItemIndex;
1265
- }
1266
- function getRichFragmentStartCursor(prepared, fragment) {
1267
- const itemIndex = prepared.preparedItemIndexBySourceItemIndex[fragment.itemIndex];
1268
- if (itemIndex == null) return null;
1269
- return {
1270
- itemIndex,
1271
- segmentIndex: fragment.start.segmentIndex,
1272
- graphemeIndex: fragment.start.graphemeIndex
1273
- };
1274
- }
1275
- function splitOverflowingRichLineRange(prepared, lineRange, maxWidth) {
1276
- if (lineRange.width <= maxWidth || lineRange.fragments.length <= 1) return lineRange;
1277
- const trailingFragment = lineRange.fragments[lineRange.fragments.length - 1];
1278
- const splitCursor = getRichFragmentStartCursor(prepared, trailingFragment);
1279
- if (splitCursor == null) return lineRange;
1280
- const fragments = lineRange.fragments.slice(0, -1);
1281
- return {
1282
- fragments,
1283
- width: fragments.reduce((total, fragment) => total + fragment.gapBefore + fragment.occupiedWidth, 0),
1284
- end: splitCursor
1285
- };
1286
- }
1287
- function layoutNextConstrainedRichInlineLineRange(prepared, maxWidth, start) {
1288
- const lineRange = layoutNextRichInlineLineRange(prepared.prepared, maxWidth, start);
1289
- if (lineRange == null) return null;
1290
- return splitOverflowingRichLineRange(prepared, lineRange, maxWidth);
1291
- }
1292
- function walkConstrainedRichInlineLineRanges(prepared, maxWidth, onLine) {
1293
- let lineCount = 0;
1294
- let cursor;
1295
- while (true) {
1296
- const lineRange = layoutNextConstrainedRichInlineLineRange(prepared, maxWidth, cursor);
1297
- if (lineRange == null) return lineCount;
1298
- onLine(lineRange);
1299
- lineCount += 1;
1300
- cursor = lineRange.end;
1301
- }
1302
- }
1303
- function measureConstrainedRichInlineStats(prepared, maxWidth) {
1304
- let lineCount = 0;
1305
- let maxLineWidth = 0;
1306
- walkConstrainedRichInlineLineRanges(prepared, maxWidth, (lineRange) => {
1307
- lineCount += 1;
1308
- if (lineRange.width > maxLineWidth) maxLineWidth = lineRange.width;
1309
- });
1310
- return {
1311
- lineCount,
1312
- maxLineWidth
1313
- };
2381
+ function readRichPrepared(spans, defaultFont, whiteSpace, wordBreak) {
2382
+ return readPreparedInlineLayout(getRichPreparedKey(spans, defaultFont, whiteSpace, wordBreak), createRichSourceItems(spans, defaultFont), whiteSpace, wordBreak);
1314
2383
  }
1315
2384
  function measureRichFragmentShift(ctx, font) {
1316
2385
  return withFont(ctx, font, () => measureFontShift(ctx));
1317
2386
  }
1318
- function materializeRichLine(ctx, spans, defaultFont, defaultColor, lineRange, overflowed) {
1319
- const richLine = materializeRichInlineLineRange(readRichPrepared(spans, defaultFont).prepared, lineRange);
1320
- const fragments = richLine.fragments.map((fragment) => {
1321
- const span = spans[fragment.itemIndex];
1322
- const font = span?.font ?? defaultFont;
2387
+ function materializeRichFragments(ctx, spans, defaultColor, atoms) {
2388
+ const fragments = [];
2389
+ let pendingGapBefore = 0;
2390
+ for (const atom of atoms) {
2391
+ const occupiedWidth = atom.width + atom.extraWidthAfter;
2392
+ if (atom.kind === "space" && !atom.preservesLineEnd && atom.atomicGroupId == null) {
2393
+ pendingGapBefore += occupiedWidth;
2394
+ continue;
2395
+ }
2396
+ const span = spans[atom.itemIndex];
2397
+ const font = span?.font ?? atom.font;
1323
2398
  const color = span?.color ?? defaultColor;
1324
- return {
1325
- itemIndex: fragment.itemIndex,
1326
- text: fragment.text,
2399
+ const previous = fragments[fragments.length - 1];
2400
+ if (previous != null && previous.itemIndex === atom.itemIndex && previous.font === font && pendingGapBefore === 0) {
2401
+ previous.text += atom.text;
2402
+ previous.occupiedWidth += occupiedWidth;
2403
+ continue;
2404
+ }
2405
+ fragments.push({
2406
+ itemIndex: atom.itemIndex,
2407
+ text: atom.text,
1327
2408
  font,
1328
2409
  color,
1329
- gapBefore: fragment.gapBefore,
1330
- occupiedWidth: fragment.occupiedWidth,
2410
+ gapBefore: pendingGapBefore,
2411
+ occupiedWidth,
1331
2412
  shift: measureRichFragmentShift(ctx, font)
1332
- };
1333
- });
1334
- return {
1335
- width: richLine.width,
1336
- fragments,
1337
- overflowed
1338
- };
1339
- }
1340
- function flattenRichLineUnits(line) {
1341
- const units = [];
1342
- for (let fragmentIndex = 0; fragmentIndex < line.fragments.length; fragmentIndex += 1) {
1343
- const fragment = line.fragments[fragmentIndex];
1344
- const fragmentUnits = getPreparedUnits(readPreparedText(fragment.text, fragment.font, "normal", "normal"));
1345
- if (fragmentUnits.length === 0) continue;
1346
- const textWidth = fragmentUnits.reduce((total, unit) => total + unit.width, 0);
1347
- const trailingExtraWidth = Math.max(0, fragment.occupiedWidth - textWidth);
1348
- for (let unitIndex = 0; unitIndex < fragmentUnits.length; unitIndex += 1) {
1349
- const unit = fragmentUnits[unitIndex];
1350
- units.push({
1351
- fragmentIndex,
1352
- itemIndex: fragment.itemIndex,
1353
- text: unit.text,
1354
- width: unit.width + (unitIndex === fragmentUnits.length - 1 ? trailingExtraWidth : 0),
1355
- font: fragment.font,
1356
- color: fragment.color,
1357
- leadingGap: unitIndex === 0 ? fragment.gapBefore : 0
1358
- });
1359
- }
2413
+ });
2414
+ pendingGapBefore = 0;
1360
2415
  }
1361
- return units;
1362
- }
1363
- function buildRichPrefixWidths(units) {
1364
- return buildPrefixWidths(units.map((unit) => unit.leadingGap + unit.width));
2416
+ return fragments;
1365
2417
  }
1366
- function buildRichSuffixWidths(units) {
1367
- const widths = [0];
1368
- let total = 0;
1369
- for (let index = units.length - 1; index >= 0; index -= 1) {
1370
- const unit = units[index];
1371
- total += unit.width;
1372
- if (widths.length > 1) total += unit.leadingGap;
1373
- widths.push(total);
2418
+ function appendRichFragment(ctx, spans, defaultColor, fragments, atom, pendingGapBefore) {
2419
+ const occupiedWidth = atom.width + atom.extraWidthAfter;
2420
+ if (atom.kind === "space" && !atom.preservesLineEnd && atom.atomicGroupId == null) return pendingGapBefore + occupiedWidth;
2421
+ const span = spans[atom.itemIndex];
2422
+ const font = span?.font ?? atom.font;
2423
+ const color = span?.color ?? defaultColor;
2424
+ const previous = fragments[fragments.length - 1];
2425
+ if (previous != null && previous.itemIndex === atom.itemIndex && previous.font === font && pendingGapBefore === 0) {
2426
+ previous.text += atom.text;
2427
+ previous.occupiedWidth += occupiedWidth;
2428
+ return 0;
1374
2429
  }
1375
- return widths;
2430
+ fragments.push({
2431
+ itemIndex: atom.itemIndex,
2432
+ text: atom.text,
2433
+ font,
2434
+ color,
2435
+ gapBefore: pendingGapBefore,
2436
+ occupiedWidth,
2437
+ shift: measureRichFragmentShift(ctx, font)
2438
+ });
2439
+ return 0;
1376
2440
  }
1377
- function materializeRichFragmentsFromUnits(units, start, end, suppressLeadingGap) {
2441
+ function materializeRichFragmentsInRange(ctx, spans, defaultColor, prepared, start, end) {
1378
2442
  const fragments = [];
1379
- for (let index = start; index < end; index += 1) {
1380
- const unit = units[index];
1381
- const previous = fragments[fragments.length - 1];
1382
- const previousUnit = units[index - 1];
1383
- if (previous != null && previousUnit != null && previousUnit.fragmentIndex === unit.fragmentIndex) {
1384
- previous.text += unit.text;
1385
- previous.occupiedWidth += unit.width;
1386
- continue;
1387
- }
1388
- fragments.push({
1389
- itemIndex: unit.itemIndex,
1390
- text: unit.text,
1391
- font: unit.font,
1392
- color: unit.color,
1393
- gapBefore: fragments.length === 0 && suppressLeadingGap ? 0 : unit.leadingGap,
1394
- occupiedWidth: unit.width,
1395
- shift: 0
1396
- });
1397
- }
2443
+ let pendingGapBefore = 0;
2444
+ forEachAtomInRange(prepared, start, end, (atom) => {
2445
+ pendingGapBefore = appendRichFragment(ctx, spans, defaultColor, fragments, atom, pendingGapBefore);
2446
+ });
1398
2447
  return fragments;
1399
2448
  }
1400
- function measureRichFragmentsShift(ctx, fragments) {
1401
- return fragments.map((fragment) => ({
1402
- ...fragment,
1403
- shift: measureRichFragmentShift(ctx, fragment.font)
1404
- }));
2449
+ function materializeRichLine(ctx, spans, defaultColor, prepared, line, overflowed) {
2450
+ return {
2451
+ width: line.width,
2452
+ fragments: materializeRichFragmentsInRange(ctx, spans, defaultColor, prepared, line.start, line.end),
2453
+ overflowed
2454
+ };
2455
+ }
2456
+ function getFirstLineRange(prepared) {
2457
+ const start = getPreparedLineStart(prepared);
2458
+ if (start == null) return;
2459
+ return layoutNextPreparedLine(prepared, start, Number.POSITIVE_INFINITY) ?? void 0;
2460
+ }
2461
+ function walkLines(prepared, maxWidth) {
2462
+ const lines = [];
2463
+ walkPreparedLineRanges(prepared, maxWidth, (line) => {
2464
+ lines.push(line);
2465
+ });
2466
+ return lines;
2467
+ }
2468
+ function collectVisibleLines(prepared, maxWidth, maxLines) {
2469
+ const lines = [];
2470
+ let overflowed = false;
2471
+ walkPreparedLineRanges(prepared, maxWidth, (line) => {
2472
+ if (lines.length < maxLines) {
2473
+ lines.push(line);
2474
+ return true;
2475
+ }
2476
+ overflowed = true;
2477
+ return false;
2478
+ });
2479
+ return {
2480
+ lines,
2481
+ overflowed
2482
+ };
1405
2483
  }
1406
2484
  function createRichEllipsisFragment(ctx, font, color) {
1407
2485
  return withFont(ctx, font, () => ({
@@ -1415,88 +2493,146 @@ function createRichEllipsisFragment(ctx, font, color) {
1415
2493
  }));
1416
2494
  }
1417
2495
  function createRichEllipsisOnlyLayout(ctx, maxWidth, font, color) {
1418
- const ellipsis = createRichEllipsisFragment(ctx, font, color);
1419
- if (ellipsis.occupiedWidth > maxWidth) return {
2496
+ const fragment = createRichEllipsisFragment(ctx, font, color);
2497
+ if (fragment.occupiedWidth > maxWidth) return {
1420
2498
  width: 0,
1421
2499
  fragments: [],
1422
2500
  overflowed: true
1423
2501
  };
1424
2502
  return {
1425
- width: ellipsis.occupiedWidth,
1426
- fragments: [ellipsis],
2503
+ width: fragment.occupiedWidth,
2504
+ fragments: [fragment],
1427
2505
  overflowed: true
1428
2506
  };
1429
2507
  }
1430
- function layoutPreparedRichEllipsis(ctx, line, maxWidth, defaultFont, defaultColor, position) {
1431
- if (!line.overflowed && line.width <= maxWidth) return {
1432
- ...line,
2508
+ function layoutRichEllipsisFromAtoms(ctx, spans, defaultFont, defaultColor, atoms, maxWidth, position, forceEllipsis = false) {
2509
+ const intrinsicWidth = measureAtomsWidth(atoms);
2510
+ if (!forceEllipsis && intrinsicWidth <= maxWidth) return {
2511
+ width: intrinsicWidth,
2512
+ fragments: materializeRichFragments(ctx, spans, defaultColor, atoms),
1433
2513
  overflowed: false
1434
2514
  };
1435
- const units = flattenRichLineUnits(line);
1436
- const fallbackFragment = line.fragments[0];
1437
- const ellipsisOnly = createRichEllipsisOnlyLayout(ctx, maxWidth, fallbackFragment?.font ?? defaultFont, fallbackFragment?.color ?? defaultColor);
1438
- if (ellipsisOnly.fragments.length === 0 || units.length === 0) return ellipsisOnly;
1439
- const ellipsisWidth = ellipsisOnly.width;
1440
- const availableWidth = Math.max(0, maxWidth - ellipsisWidth);
1441
- const prefixWidths = buildRichPrefixWidths(units);
1442
- const suffixWidths = buildRichSuffixWidths(units);
1443
- const { prefixCount, suffixCount } = selectEllipsisUnitCounts({
1444
- position,
1445
- prefixWidths,
1446
- suffixWidths,
1447
- unitCount: units.length,
1448
- availableWidth,
1449
- getMaxSuffixCount: position === "middle" ? (nextPrefixCount) => Math.max(0, units.length - nextPrefixCount - 1) : void 0
2515
+ const ellipsisWidth = measureEllipsisWidth(ctx);
2516
+ if (ellipsisWidth > maxWidth) return {
2517
+ width: 0,
2518
+ fragments: [],
2519
+ overflowed: true
2520
+ };
2521
+ if (atoms.length === 0) return createRichEllipsisOnlyLayout(ctx, maxWidth, defaultFont, defaultColor);
2522
+ const selection = resolveEllipsisSelection({
2523
+ widths: atoms.map((atom) => atom.width + atom.extraWidthAfter),
2524
+ ellipsisWidth,
2525
+ maxWidth,
2526
+ position
1450
2527
  });
1451
- const prefixFragments = measureRichFragmentsShift(ctx, materializeRichFragmentsFromUnits(units, 0, prefixCount, false));
1452
- const suffixFragments = measureRichFragmentsShift(ctx, materializeRichFragmentsFromUnits(units, units.length - suffixCount, units.length, true));
1453
- 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];
1454
- const ellipsis = createRichEllipsisFragment(ctx, ellipsisSource?.font ?? defaultFont, ellipsisSource?.color ?? defaultColor);
1455
- const fragments = position === "start" ? [ellipsis, ...suffixFragments] : position === "middle" ? [
1456
- ...prefixFragments,
1457
- ellipsis,
1458
- ...suffixFragments
1459
- ] : [...prefixFragments, ellipsis];
2528
+ if (selection == null) return {
2529
+ width: 0,
2530
+ fragments: [],
2531
+ overflowed: true
2532
+ };
2533
+ const { prefixCount, suffixCount, width } = selection;
2534
+ const prefixAtoms = atoms.slice(0, prefixCount);
2535
+ const suffixAtoms = atoms.slice(atoms.length - suffixCount);
2536
+ const ellipsisSource = position === "start" ? suffixAtoms[0] ?? atoms[0] : position === "middle" ? prefixAtoms[prefixAtoms.length - 1] ?? suffixAtoms[0] ?? atoms[atoms.length - 1] : prefixAtoms[prefixAtoms.length - 1] ?? atoms[atoms.length - 1];
2537
+ const ellipsisSpan = ellipsisSource == null ? void 0 : spans[ellipsisSource.itemIndex];
2538
+ const ellipsisFragment = createRichEllipsisFragment(ctx, ellipsisSpan?.font ?? ellipsisSource?.font ?? defaultFont, ellipsisSpan?.color ?? defaultColor);
2539
+ const prefixFragments = materializeRichFragments(ctx, spans, defaultColor, prefixAtoms);
2540
+ const suffixFragments = materializeRichFragments(ctx, spans, defaultColor, suffixAtoms);
1460
2541
  return {
1461
- width: position === "start" ? ellipsis.occupiedWidth + (suffixWidths[suffixCount] ?? 0) : position === "middle" ? (prefixWidths[prefixCount] ?? 0) + ellipsis.occupiedWidth + (suffixWidths[suffixCount] ?? 0) : (prefixWidths[prefixCount] ?? 0) + ellipsis.occupiedWidth,
1462
- fragments,
2542
+ width,
2543
+ fragments: position === "start" ? [ellipsisFragment, ...suffixFragments] : position === "middle" ? [
2544
+ ...prefixFragments,
2545
+ ellipsisFragment,
2546
+ ...suffixFragments
2547
+ ] : [...prefixFragments, ellipsisFragment],
1463
2548
  overflowed: true
1464
2549
  };
1465
2550
  }
1466
- function layoutRichFirstLineIntrinsic(ctx, spans, defaultFont, defaultColor) {
1467
- if (spans.length === 0) return {
2551
+ function layoutRichEndEllipsisFromCursor(ctx, spans, defaultFont, defaultColor, prepared, start, maxWidth) {
2552
+ const ellipsisWidth = measureEllipsisWidth(ctx);
2553
+ if (ellipsisWidth > maxWidth) return {
1468
2554
  width: 0,
1469
2555
  fragments: [],
1470
- overflowed: false
2556
+ overflowed: true
2557
+ };
2558
+ const widths = [];
2559
+ forEachAtomFromCursorToEnd(prepared, start, (atom) => {
2560
+ widths.push(atom.width + atom.extraWidthAfter);
2561
+ });
2562
+ if (widths.length === 0) return createRichEllipsisOnlyLayout(ctx, maxWidth, defaultFont, defaultColor);
2563
+ const selection = resolveEllipsisSelection({
2564
+ widths,
2565
+ ellipsisWidth,
2566
+ maxWidth,
2567
+ position: "end"
2568
+ });
2569
+ if (selection == null) return {
2570
+ width: 0,
2571
+ fragments: [],
2572
+ overflowed: true
2573
+ };
2574
+ const { prefixCount, width } = selection;
2575
+ const fragments = [];
2576
+ let atomIndex = 0;
2577
+ let pendingGapBefore = 0;
2578
+ let lastVisibleAtom;
2579
+ let lastAtom;
2580
+ forEachAtomFromCursorToEnd(prepared, start, (atom) => {
2581
+ lastAtom = atom;
2582
+ if (atomIndex < prefixCount) {
2583
+ pendingGapBefore = appendRichFragment(ctx, spans, defaultColor, fragments, atom, pendingGapBefore);
2584
+ lastVisibleAtom = atom;
2585
+ }
2586
+ atomIndex += 1;
2587
+ });
2588
+ const ellipsisSource = lastVisibleAtom ?? lastAtom;
2589
+ const ellipsisSpan = ellipsisSource == null ? void 0 : spans[ellipsisSource.itemIndex];
2590
+ fragments.push(createRichEllipsisFragment(ctx, ellipsisSpan?.font ?? ellipsisSource?.font ?? defaultFont, ellipsisSpan?.color ?? defaultColor));
2591
+ return {
2592
+ width,
2593
+ fragments,
2594
+ overflowed: true
1471
2595
  };
1472
- const lineRange = layoutNextRichInlineLineRange(readRichPrepared(spans, defaultFont).prepared, INTRINSIC_MAX_WIDTH);
1473
- if (lineRange == null) return {
2596
+ }
2597
+ function layoutRichFirstLineIntrinsic(ctx, spans, defaultFont, defaultColor, whiteSpace = "normal", wordBreak = "normal") {
2598
+ const prepared = readRichPrepared(spans, defaultFont, whiteSpace, wordBreak);
2599
+ const line = getFirstLineRange(prepared);
2600
+ if (line == null) return {
1474
2601
  width: 0,
1475
2602
  fragments: [],
1476
2603
  overflowed: false
1477
2604
  };
1478
- return materializeRichLine(ctx, spans, defaultFont, defaultColor, lineRange, false);
2605
+ return materializeRichLine(ctx, spans, defaultColor, prepared, line, false);
1479
2606
  }
1480
- function layoutRichFirstLine(ctx, spans, maxWidth, defaultFont, defaultColor) {
2607
+ function layoutRichFirstLine(ctx, spans, maxWidth, defaultFont, defaultColor, whiteSpace = "normal", wordBreak = "normal") {
1481
2608
  const clampedMaxWidth = Math.max(0, maxWidth);
1482
- if (spans.length === 0 || clampedMaxWidth === 0) return {
2609
+ if (clampedMaxWidth === 0 || spans.length === 0) return {
2610
+ width: 0,
2611
+ fragments: [],
2612
+ overflowed: false
2613
+ };
2614
+ const prepared = readRichPrepared(spans, defaultFont, whiteSpace, wordBreak);
2615
+ const start = getPreparedLineStart(prepared);
2616
+ if (start == null) return {
1483
2617
  width: 0,
1484
2618
  fragments: [],
1485
2619
  overflowed: false
1486
2620
  };
1487
- const lineRange = layoutNextConstrainedRichInlineLineRange(readRichPrepared(spans, defaultFont), clampedMaxWidth);
1488
- if (lineRange == null) return {
2621
+ const line = layoutNextPreparedLine(prepared, start, clampedMaxWidth);
2622
+ if (line == null) return {
1489
2623
  width: 0,
1490
2624
  fragments: [],
1491
2625
  overflowed: false
1492
2626
  };
1493
- return materializeRichLine(ctx, spans, defaultFont, defaultColor, lineRange, false);
2627
+ return materializeRichLine(ctx, spans, defaultColor, prepared, line, false);
1494
2628
  }
1495
- function layoutRichEllipsizedFirstLine(ctx, spans, maxWidth, defaultFont, defaultColor, ellipsisPosition = "end") {
2629
+ function layoutRichEllipsizedFirstLine(ctx, spans, maxWidth, defaultFont, defaultColor, ellipsisPosition = "end", whiteSpace = "normal", wordBreak = "normal") {
1496
2630
  const clampedMaxWidth = Math.max(0, maxWidth);
1497
- const intrinsicLine = layoutRichFirstLineIntrinsic(ctx, spans, defaultFont, defaultColor);
1498
- if (intrinsicLine.fragments.length === 0) return {
1499
- ...intrinsicLine,
2631
+ const prepared = readRichPrepared(spans, defaultFont, whiteSpace, wordBreak);
2632
+ const intrinsicLine = getFirstLineRange(prepared);
2633
+ if (intrinsicLine == null) return {
2634
+ width: 0,
2635
+ fragments: [],
1500
2636
  overflowed: false
1501
2637
  };
1502
2638
  if (clampedMaxWidth === 0) return {
@@ -1504,103 +2640,110 @@ function layoutRichEllipsizedFirstLine(ctx, spans, maxWidth, defaultFont, defaul
1504
2640
  fragments: [],
1505
2641
  overflowed: true
1506
2642
  };
1507
- return layoutPreparedRichEllipsis(ctx, intrinsicLine, clampedMaxWidth, defaultFont, defaultColor, ellipsisPosition);
2643
+ return layoutRichEllipsisFromAtoms(ctx, spans, defaultFont, defaultColor, collectAtomsInRange(prepared, intrinsicLine.start, intrinsicLine.end), clampedMaxWidth, ellipsisPosition);
1508
2644
  }
1509
- function measureRichText(_ctx, spans, maxWidth, defaultFont) {
1510
- if (spans.length === 0) return {
2645
+ function measureRichText(_ctx, spans, maxWidth, defaultFont, whiteSpace = "normal", wordBreak = "normal") {
2646
+ if (spans.length === 0 || maxWidth <= 0) return {
1511
2647
  width: 0,
1512
2648
  lineCount: 0
1513
2649
  };
1514
- const { maxLineWidth: width, lineCount } = measureConstrainedRichInlineStats(readRichPrepared(spans, defaultFont), maxWidth);
2650
+ const { maxLineWidth: width, lineCount } = measurePreparedLineStats(readRichPrepared(spans, defaultFont, whiteSpace, wordBreak), maxWidth);
1515
2651
  return {
1516
2652
  width,
1517
2653
  lineCount
1518
2654
  };
1519
2655
  }
1520
- function measureRichTextIntrinsic(_ctx, spans, defaultFont) {
2656
+ function measureRichTextIntrinsic(_ctx, spans, defaultFont, whiteSpace = "normal", wordBreak = "normal") {
1521
2657
  if (spans.length === 0) return {
1522
2658
  width: 0,
1523
2659
  lineCount: 0
1524
2660
  };
1525
- const { maxLineWidth: width, lineCount } = measureRichInlineStats(readRichPrepared(spans, defaultFont).prepared, INTRINSIC_MAX_WIDTH);
2661
+ const { maxLineWidth: width, lineCount } = measurePreparedLineStats(readRichPrepared(spans, defaultFont, whiteSpace, wordBreak), Number.POSITIVE_INFINITY);
1526
2662
  return {
1527
2663
  width,
1528
2664
  lineCount
1529
2665
  };
1530
2666
  }
1531
- function measureRichTextMinContent(_ctx, spans, defaultFont, overflowWrap = "break-word") {
2667
+ function measureRichTextMinContent(_ctx, spans, defaultFont, overflowWrap = "break-word", whiteSpace = "normal", wordBreak = "normal") {
1532
2668
  if (spans.length === 0) return {
1533
2669
  width: 0,
1534
2670
  lineCount: 0
1535
2671
  };
1536
- let maxWidth = 0;
1537
- for (const span of spans) {
1538
- if (span.text.trim().length === 0) continue;
1539
- const font = span.font ?? defaultFont;
1540
- const spanMinWidth = measurePreparedMinContentWidth(readPreparedText(span.text, font, "normal", "normal"), overflowWrap) + (span.extraWidth ?? 0);
1541
- if (spanMinWidth > maxWidth) maxWidth = spanMinWidth;
1542
- }
1543
- if (maxWidth === 0) return {
2672
+ const prepared = readRichPrepared(spans, defaultFont, whiteSpace, wordBreak);
2673
+ const width = measurePreparedMinContentWidth$1(prepared, overflowWrap);
2674
+ if (width === 0) return {
1544
2675
  width: 0,
1545
2676
  lineCount: 0
1546
2677
  };
1547
- const { lineCount } = measureConstrainedRichInlineStats(readRichPrepared(spans, defaultFont), Math.max(maxWidth, MIN_CONTENT_WIDTH_EPSILON));
2678
+ const { lineCount } = measurePreparedLineStats(prepared, Math.max(width, MIN_CONTENT_WIDTH_EPSILON));
1548
2679
  return {
1549
- width: maxWidth,
2680
+ width,
1550
2681
  lineCount
1551
2682
  };
1552
2683
  }
1553
- function layoutRichText(ctx, spans, maxWidth, defaultFont, defaultColor) {
1554
- if (spans.length === 0) return {
2684
+ function layoutRichText(ctx, spans, maxWidth, defaultFont, defaultColor, whiteSpace = "normal", wordBreak = "normal") {
2685
+ if (spans.length === 0 || maxWidth <= 0) return {
1555
2686
  width: 0,
1556
2687
  lines: [],
1557
2688
  overflowed: false
1558
2689
  };
1559
- const prepared = readRichPrepared(spans, defaultFont);
1560
- const lineRanges = [];
1561
- walkConstrainedRichInlineLineRanges(prepared, maxWidth, (lineRange) => lineRanges.push(lineRange));
1562
- if (lineRanges.length === 0) return {
1563
- width: 0,
1564
- lines: [],
2690
+ const prepared = readRichPrepared(spans, defaultFont, whiteSpace, wordBreak);
2691
+ const lines = walkLines(prepared, maxWidth).map((line) => materializeRichLine(ctx, spans, defaultColor, prepared, line, false));
2692
+ return {
2693
+ width: measureRichBlockWidth(lines),
2694
+ lines,
1565
2695
  overflowed: false
1566
2696
  };
1567
- const lines = lineRanges.map((lineRange) => materializeRichLine(ctx, spans, defaultFont, defaultColor, lineRange, false));
2697
+ }
2698
+ function layoutRichTextIntrinsic(ctx, spans, defaultFont, defaultColor, whiteSpace = "normal", wordBreak = "normal") {
2699
+ const prepared = readRichPrepared(spans, defaultFont, whiteSpace, wordBreak);
2700
+ const lines = walkLines(prepared, Number.POSITIVE_INFINITY).map((line) => materializeRichLine(ctx, spans, defaultColor, prepared, line, false));
1568
2701
  return {
1569
- width: lines.reduce((maxLineWidth, line) => Math.max(maxLineWidth, line.width), 0),
2702
+ width: measureRichBlockWidth(lines),
1570
2703
  lines,
1571
2704
  overflowed: false
1572
2705
  };
1573
2706
  }
1574
- function layoutRichTextIntrinsic(ctx, spans, defaultFont, defaultColor) {
1575
- return layoutRichText(ctx, spans, INTRINSIC_MAX_WIDTH, defaultFont, defaultColor);
1576
- }
1577
- function layoutRichTextWithOverflow(ctx, spans, maxWidth, defaultFont, defaultColor, maxLines, overflow = "clip") {
1578
- if (spans.length === 0) return {
2707
+ function layoutRichTextWithOverflow(ctx, spans, maxWidth, defaultFont, defaultColor, maxLines, overflow = "clip", whiteSpace = "normal", wordBreak = "normal") {
2708
+ if (spans.length === 0 || maxWidth <= 0) return {
1579
2709
  width: 0,
1580
2710
  lines: [],
1581
2711
  overflowed: false
1582
2712
  };
1583
2713
  const normalizedMaxLines = normalizeMaxLines(maxLines);
1584
- const layout = layoutRichText(ctx, spans, maxWidth, defaultFont, defaultColor);
1585
- if (normalizedMaxLines == null || layout.lines.length <= normalizedMaxLines) return layout;
1586
- const visibleLines = layout.lines.slice(0, normalizedMaxLines);
2714
+ const prepared = readRichPrepared(spans, defaultFont, whiteSpace, wordBreak);
2715
+ if (normalizedMaxLines == null) {
2716
+ const lines = walkLines(prepared, maxWidth).map((line) => materializeRichLine(ctx, spans, defaultColor, prepared, line, false));
2717
+ return {
2718
+ width: measureRichBlockWidth(lines),
2719
+ lines,
2720
+ overflowed: false
2721
+ };
2722
+ }
2723
+ const { lines: visibleRanges, overflowed } = collectVisibleLines(prepared, maxWidth, normalizedMaxLines);
2724
+ if (!overflowed) {
2725
+ const lines = visibleRanges.map((line) => materializeRichLine(ctx, spans, defaultColor, prepared, line, false));
2726
+ return {
2727
+ width: measureRichBlockWidth(lines),
2728
+ lines,
2729
+ overflowed: false
2730
+ };
2731
+ }
2732
+ const visibleLines = visibleRanges.map((line) => materializeRichLine(ctx, spans, defaultColor, prepared, line, false));
1587
2733
  if (overflow !== "ellipsis") return {
1588
- width: visibleLines.reduce((maxLineWidth, line) => Math.max(maxLineWidth, line.width), 0),
2734
+ width: measureRichBlockWidth(visibleLines),
1589
2735
  lines: visibleLines,
1590
2736
  overflowed: true
1591
2737
  };
1592
- const lastVisibleLine = visibleLines[visibleLines.length - 1];
1593
- const ellipsizedLastLine = lastVisibleLine == null ? {
2738
+ const lastVisibleRange = visibleRanges[visibleRanges.length - 1];
2739
+ const ellipsizedLastLine = lastVisibleRange == null ? {
1594
2740
  width: 0,
1595
2741
  fragments: [],
1596
2742
  overflowed: true
1597
- } : layoutPreparedRichEllipsis(ctx, {
1598
- ...lastVisibleLine,
1599
- overflowed: true
1600
- }, maxWidth, defaultFont, defaultColor, "end");
2743
+ } : layoutRichEndEllipsisFromCursor(ctx, spans, defaultFont, defaultColor, prepared, lastVisibleRange.start, maxWidth);
1601
2744
  const lines = [...visibleLines.slice(0, -1), ellipsizedLastLine];
1602
2745
  return {
1603
- width: lines.reduce((maxLineWidth, line) => Math.max(maxLineWidth, line.width), 0),
2746
+ width: measureRichBlockWidth(lines),
1604
2747
  lines,
1605
2748
  overflowed: true
1606
2749
  };
@@ -1679,7 +2822,7 @@ function getSingleLineLayout(node, ctx, text, options) {
1679
2822
  }
1680
2823
  function getRichSingleLineLayout(node, ctx, spans, options) {
1681
2824
  const maxWidth = normalizeTextMaxWidth(ctx.constraints?.maxWidth);
1682
- 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));
2825
+ return readCachedTextLayout(node, ctx, getSingleLineLayoutKey(maxWidth), () => maxWidth == null ? layoutRichFirstLineIntrinsic(ctx, spans, options.font, options.color, options.whiteSpace, options.wordBreak) : options.overflow === "ellipsis" ? layoutRichEllipsizedFirstLine(ctx, spans, maxWidth, options.font, options.color, options.ellipsisPosition ?? "end", options.whiteSpace, options.wordBreak) : layoutRichFirstLine(ctx, spans, maxWidth, options.font, options.color, options.whiteSpace, options.wordBreak));
1683
2826
  }
1684
2827
  function getMultiLineOverflowLayout(node, ctx, text, options) {
1685
2828
  const maxWidth = normalizeTextMaxWidth(ctx.constraints?.maxWidth);
@@ -1712,7 +2855,7 @@ function getSingleLineMinContentLayout(node, ctx, text, options) {
1712
2855
  });
1713
2856
  }
1714
2857
  function getRichSingleLineMinContentWidth(node, ctx, spans, options) {
1715
- return readCachedTextLayout(node, ctx, getSingleLineMinContentLayoutKey(), () => measureRichTextMinContent(ctx, spans, options.font, options.overflowWrap).width);
2858
+ return readCachedTextLayout(node, ctx, getSingleLineMinContentLayoutKey(), () => measureRichTextMinContent(ctx, spans, options.font, options.overflowWrap, options.whiteSpace, options.wordBreak).width);
1716
2859
  }
1717
2860
  function drawRichLine(ctx, line, fallbackColor, x, y, lineHeight) {
1718
2861
  let cursorX = x;
@@ -1734,19 +2877,19 @@ function getMultiLineMinContentLayout(node, ctx, text, whiteSpace, wordBreak, ov
1734
2877
  function getRichMultiLineMeasureLayout(node, ctx, spans, options) {
1735
2878
  const maxWidth = normalizeTextMaxWidth(ctx.constraints?.maxWidth);
1736
2879
  if (shouldReadConstrainedOverflowLayout(maxWidth, options)) return measureBlockLayout(getRichMultiLineOverflowLayout(node, ctx, spans, options));
1737
- return readCachedTextLayout(node, ctx, getRichMultiLineMeasureLayoutKey(maxWidth), () => maxWidth == null ? measureRichTextIntrinsic(ctx, spans, options.font) : measureRichText(ctx, spans, maxWidth, options.font));
2880
+ return readCachedTextLayout(node, ctx, getRichMultiLineMeasureLayoutKey(maxWidth), () => maxWidth == null ? measureRichTextIntrinsic(ctx, spans, options.font, options.whiteSpace, options.wordBreak) : measureRichText(ctx, spans, maxWidth, options.font, options.whiteSpace, options.wordBreak));
1738
2881
  }
1739
2882
  function getRichMultiLineOverflowLayout(node, ctx, spans, options) {
1740
2883
  const maxWidth = normalizeTextMaxWidth(ctx.constraints?.maxWidth);
1741
- return readCachedTextLayout(node, ctx, getRichMultiLineOverflowLayoutKey(maxWidth), () => layoutRichTextWithOverflow(ctx, spans, maxWidth ?? 0, options.font, options.color, options.maxLines, options.overflow));
2884
+ return readCachedTextLayout(node, ctx, getRichMultiLineOverflowLayoutKey(maxWidth), () => layoutRichTextWithOverflow(ctx, spans, maxWidth ?? 0, options.font, options.color, options.maxLines, options.overflow, options.whiteSpace, options.wordBreak));
1742
2885
  }
1743
2886
  function getRichMultiLineDrawLayout(node, ctx, spans, options) {
1744
2887
  const maxWidth = normalizeTextMaxWidth(ctx.constraints?.maxWidth);
1745
2888
  if (shouldReadConstrainedOverflowLayout(maxWidth, options)) return getRichMultiLineOverflowLayout(node, ctx, spans, options);
1746
- return readCachedTextLayout(node, ctx, getRichMultiLineDrawLayoutKey(maxWidth), () => maxWidth == null ? layoutRichTextIntrinsic(ctx, spans, options.font, options.color) : layoutRichText(ctx, spans, maxWidth, options.font, options.color));
2889
+ return readCachedTextLayout(node, ctx, getRichMultiLineDrawLayoutKey(maxWidth), () => maxWidth == null ? layoutRichTextIntrinsic(ctx, spans, options.font, options.color, options.whiteSpace, options.wordBreak) : layoutRichText(ctx, spans, maxWidth, options.font, options.color, options.whiteSpace, options.wordBreak));
1747
2890
  }
1748
2891
  function getRichMultiLineMinContentLayout(node, ctx, spans, options) {
1749
- return readCachedTextLayout(node, ctx, getRichMultiLineMinContentLayoutKey(), () => measureRichTextMinContent(ctx, spans, options.font, options.overflowWrap));
2892
+ return readCachedTextLayout(node, ctx, getRichMultiLineMinContentLayoutKey(), () => measureRichTextMinContent(ctx, spans, options.font, options.overflowWrap, options.whiteSpace, options.wordBreak));
1750
2893
  }
1751
2894
  /**
1752
2895
  * Draws wrapped text using the configured line height and alignment.
@@ -3029,6 +4172,6 @@ var TimelineRenderer = class extends VirtualizedRenderer {
3029
4172
  }
3030
4173
  };
3031
4174
  //#endregion
3032
- export { BaseRenderer, ChatRenderer, DebugRenderer, Fixed, Flex, FlexItem, Group, ListState, MultilineText, PaddingBox, Place, Text, TimelineRenderer, VirtualizedRenderer, Wrapper, memoRenderItem, memoRenderItemBy };
4175
+ export { BaseRenderer, ChatRenderer, DebugRenderer, Fixed, Flex, FlexItem, Group, ListState, MultilineText, PaddingBox, Place, ShrinkWrap, Text, TimelineRenderer, VirtualizedRenderer, Wrapper, memoRenderItem, memoRenderItemBy };
3033
4176
 
3034
4177
  //# sourceMappingURL=index.mjs.map