chat-layout 1.1.0-3 → 1.1.0-4

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/example/chat.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  type Context,
14
14
  type DynValue,
15
15
  type HitTest,
16
+ type InlineSpan,
16
17
  type Node,
17
18
  type RenderFeedback,
18
19
  } from "chat-layout";
@@ -133,7 +134,7 @@ ctx.scale(devicePixelRatio, devicePixelRatio);
133
134
 
134
135
  type ReplyPreview = {
135
136
  sender: string;
136
- content: string;
137
+ content: string | InlineSpan<C>[];
137
138
  };
138
139
 
139
140
  type BaseChatItem = {
@@ -143,7 +144,7 @@ type BaseChatItem = {
143
144
 
144
145
  type MessageItem = BaseChatItem & {
145
146
  kind: "message";
146
- content: string;
147
+ content: string | InlineSpan<C>[];
147
148
  reply?: ReplyPreview;
148
149
  };
149
150
 
@@ -154,6 +155,30 @@ type RevokedItem = BaseChatItem & {
154
155
 
155
156
  type ChatItem = MessageItem | RevokedItem;
156
157
 
158
+ const richTextMessage: InlineSpan<C>[] = [
159
+ { text: "现在这个 chat example 可以直接展示 " },
160
+ { text: "rich text", font: "700 16px system-ui", style: "#0f766e" },
161
+ { text: " 了,支持 " },
162
+ { text: "颜色", style: "#2563eb" },
163
+ { text: "、" },
164
+ { text: "粗体", font: "700 16px system-ui", style: "#b91c1c" },
165
+ { text: ",以及 " },
166
+ { text: "inline code", font: "15px ui-monospace, SFMono-Regular, Consolas, monospace", style: "#7c3aed" },
167
+ { text: " 这样的片段混排。" },
168
+ ];
169
+
170
+ const richReplyPreview: InlineSpan<C>[] = [
171
+ { text: "回复预览里也能用 " },
172
+ { text: "rich text", font: "700 13px system-ui", style: "#0f766e" },
173
+ { text: ",比如 " },
174
+ { text: "关键词高亮", style: "#2563eb" },
175
+ { text: " 和 " },
176
+ { text: "code()", font: "12px ui-monospace, SFMono-Regular, Consolas, monospace", style: "#7c3aed" },
177
+ {
178
+ text: ",超长内容仍然会按原来的两行省略规则收起,不需要额外处理。",
179
+ },
180
+ ];
181
+
157
182
  let currentHover: ChatItem | undefined;
158
183
  const REPLACE_ANIMATION_DURATION = 320;
159
184
 
@@ -377,7 +402,7 @@ const list = new ListState<ChatItem>([
377
402
  id: 2,
378
403
  kind: "message",
379
404
  sender: "B",
380
- content: "aaaa",
405
+ content: richTextMessage,
381
406
  reply: {
382
407
  sender: "A",
383
408
  content: "hello world chat layout message render",
@@ -411,8 +436,7 @@ const list = new ListState<ChatItem>([
411
436
  content: "这里是一条会展示回复预览省略效果的消息。",
412
437
  reply: {
413
438
  sender: "A",
414
- content:
415
- "这是一条非常长的回复预览,用来演示 MultilineText 在 chat example 里的末尾 ellipsis 能力。它应该被限制在两行之内,而不是把整个气泡一路撑到天花板。",
439
+ content: richReplyPreview,
416
440
  },
417
441
  },
418
442
  { id: 9, kind: "message", sender: "B", content: randomText(5) },
package/index.d.mts CHANGED
@@ -81,6 +81,21 @@ interface TextStyleOptions<C extends CanvasRenderingContext2D> {
81
81
  /** Default: break-word; use anywhere when min-content should honor grapheme break opportunities. */
82
82
  overflowWrap?: TextOverflowWrapMode;
83
83
  }
84
+ /**
85
+ * A span-like inline text fragment used by rich multi-line text.
86
+ */
87
+ interface InlineSpan<C extends CanvasRenderingContext2D> {
88
+ /** Source text contained in this inline fragment. */
89
+ text: string;
90
+ /** Canvas font string override for this fragment. Falls back to the node-level font. */
91
+ font?: string;
92
+ /** Fill style override for this fragment. Falls back to the node-level style. */
93
+ style?: DynValue<C, string>;
94
+ /** Optional break hint forwarded to pretext rich-inline layout. */
95
+ break?: "normal" | "never";
96
+ /** Optional extra occupied width forwarded to pretext rich-inline layout. */
97
+ extraWidth?: number;
98
+ }
84
99
  /**
85
100
  * Options for multi-line text nodes.
86
101
  */
@@ -352,15 +367,16 @@ declare class Place<C extends CanvasRenderingContext2D> extends Wrapper<C> {
352
367
  //#region src/nodes/text.d.ts
353
368
  /**
354
369
  * Draws wrapped text using the configured line height and alignment.
370
+ * Accepts either a plain string or an array of `InlineSpan` items for mixed inline styles.
355
371
  */
356
372
  declare class MultilineText<C extends CanvasRenderingContext2D> implements Node<C> {
357
- readonly text: string;
373
+ readonly text: string | InlineSpan<C>[];
358
374
  readonly options: MultilineTextOptions<C>;
359
375
  /**
360
- * @param text Source text to measure and draw.
376
+ * @param text Source text to measure and draw. Pass an `InlineSpan[]` for mixed inline styles.
361
377
  * @param options Text layout and drawing options.
362
378
  */
363
- constructor(text: string, options: MultilineTextOptions<C>);
379
+ constructor(text: string | InlineSpan<C>[], options: MultilineTextOptions<C>);
364
380
  measure(ctx: Context<C>): Box;
365
381
  measureMinContent(ctx: Context<C>): Box;
366
382
  draw(ctx: Context<C>, x: number, y: number): boolean;
@@ -640,5 +656,5 @@ declare class TimelineRenderer<C extends CanvasRenderingContext2D, T extends {}>
640
656
  hittest(test: HitTest): boolean;
641
657
  }
642
658
  //#endregion
643
- export { Axis, BaseRenderer, Box, ChatRenderer, ChildLayoutResult, Context, CrossAxisAlignment, DebugRenderer, DynValue, Fixed, Flex, FlexContainerOptions, FlexItem, FlexItemOptions, FlexLayoutResult, Group, HitTest, JumpToOptions, LayoutConstraints, LayoutRect, ListState, MainAxisAlignment, MainAxisSize, MultilineText, MultilineTextOptions, Node, PaddingBox, PhysicalTextAlign, Place, RenderFeedback, RendererOptions, ReplaceListItemAnimationOptions, Text, TextAlign, TextEllipsisPosition, TextOptions, TextOverflowMode, TextOverflowWrapMode, TextStyleOptions, TextWhiteSpaceMode, TextWordBreakMode, TimelineRenderer, VirtualizedRenderer, Wrapper, memoRenderItem, memoRenderItemBy };
659
+ export { Axis, BaseRenderer, Box, ChatRenderer, ChildLayoutResult, Context, CrossAxisAlignment, DebugRenderer, DynValue, Fixed, Flex, FlexContainerOptions, FlexItem, FlexItemOptions, FlexLayoutResult, Group, HitTest, InlineSpan, JumpToOptions, LayoutConstraints, LayoutRect, ListState, MainAxisAlignment, MainAxisSize, MultilineText, MultilineTextOptions, Node, PaddingBox, PhysicalTextAlign, Place, RenderFeedback, RendererOptions, ReplaceListItemAnimationOptions, Text, TextAlign, TextEllipsisPosition, TextOptions, TextOverflowMode, TextOverflowWrapMode, TextStyleOptions, TextWhiteSpaceMode, TextWordBreakMode, TimelineRenderer, VirtualizedRenderer, Wrapper, memoRenderItem, memoRenderItemBy };
644
660
  //# sourceMappingURL=index.d.mts.map
package/index.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { layoutNextLine, layoutWithLines, measureLineStats, measureNaturalWidth, prepareWithSegments } from "@chenglou/pretext";
2
+ import { materializeRichInlineLineRange, measureRichInlineStats, prepareRichInline, walkRichInlineLineRanges } from "@chenglou/pretext/rich-inline";
2
3
  //#region src/internal/node-registry.ts
3
4
  const registry = /* @__PURE__ */ new WeakMap();
4
5
  const revisions = /* @__PURE__ */ new WeakMap();
@@ -1218,6 +1219,221 @@ function layoutTextWithOverflow(ctx, text, maxWidth, options = {}) {
1218
1219
  overflowed: true
1219
1220
  };
1220
1221
  }
1222
+ const RICH_PREPARED_CACHE_CAPACITY = 256;
1223
+ const richPreparedCache = /* @__PURE__ */ new Map();
1224
+ function getRichPreparedCacheKey(spans, defaultFont) {
1225
+ return spans.map((s) => `${s.font ?? defaultFont}\u0000${s.text}\u0000${s.break ?? ""}\u0000${s.extraWidth ?? 0}`).join("");
1226
+ }
1227
+ function readRichPrepared(spans, defaultFont) {
1228
+ const key = getRichPreparedCacheKey(spans, defaultFont);
1229
+ const cached = readLruValue(richPreparedCache, key);
1230
+ if (cached != null) return cached;
1231
+ return writeLruValue(richPreparedCache, key, prepareRichInline(spans.map((s) => ({
1232
+ text: s.text,
1233
+ font: s.font ?? defaultFont,
1234
+ break: s.break,
1235
+ extraWidth: s.extraWidth
1236
+ }))), RICH_PREPARED_CACHE_CAPACITY);
1237
+ }
1238
+ function materializeRichLine(ctx, spans, defaultFont, defaultStyle, lineRange, overflowed) {
1239
+ const richLine = materializeRichInlineLineRange(readRichPrepared(spans, defaultFont), lineRange);
1240
+ const fragments = richLine.fragments.map((frag) => {
1241
+ const span = spans[frag.itemIndex];
1242
+ const fragFont = span?.font ?? defaultFont;
1243
+ const fragStyle = span?.style ?? defaultStyle;
1244
+ const prevFont = ctx.graphics.font;
1245
+ ctx.graphics.font = fragFont;
1246
+ const shift = measureFontShift(ctx);
1247
+ ctx.graphics.font = prevFont;
1248
+ return {
1249
+ itemIndex: frag.itemIndex,
1250
+ text: frag.text,
1251
+ font: fragFont,
1252
+ style: fragStyle,
1253
+ gapBefore: frag.gapBefore,
1254
+ occupiedWidth: frag.occupiedWidth,
1255
+ shift
1256
+ };
1257
+ });
1258
+ return {
1259
+ width: richLine.width,
1260
+ fragments,
1261
+ overflowed
1262
+ };
1263
+ }
1264
+ function measureRichText(ctx, spans, maxWidth, defaultFont) {
1265
+ if (spans.length === 0) return {
1266
+ width: 0,
1267
+ lineCount: 0
1268
+ };
1269
+ const { maxLineWidth: width, lineCount } = measureRichInlineStats(readRichPrepared(spans, defaultFont), maxWidth);
1270
+ return {
1271
+ width,
1272
+ lineCount
1273
+ };
1274
+ }
1275
+ function measureRichTextIntrinsic(ctx, spans, defaultFont) {
1276
+ if (spans.length === 0) return {
1277
+ width: 0,
1278
+ lineCount: 0
1279
+ };
1280
+ const { maxLineWidth: width, lineCount } = measureRichInlineStats(readRichPrepared(spans, defaultFont), INTRINSIC_MAX_WIDTH);
1281
+ return {
1282
+ width,
1283
+ lineCount
1284
+ };
1285
+ }
1286
+ function measureRichTextMinContent(ctx, spans, defaultFont, overflowWrap = "break-word") {
1287
+ if (spans.length === 0) return {
1288
+ width: 0,
1289
+ lineCount: 0
1290
+ };
1291
+ let maxWidth = 0;
1292
+ for (const span of spans) {
1293
+ if (span.text.trim().length === 0) continue;
1294
+ const font = span.font ?? defaultFont;
1295
+ const spanMin = measurePreparedMinContentWidth(readPreparedText(span.text, font, "normal", "normal"), overflowWrap) + (span.extraWidth ?? 0);
1296
+ if (spanMin > maxWidth) maxWidth = spanMin;
1297
+ }
1298
+ if (maxWidth === 0) return {
1299
+ width: 0,
1300
+ lineCount: 0
1301
+ };
1302
+ const { lineCount } = measureRichInlineStats(readRichPrepared(spans, defaultFont), Math.max(maxWidth, MIN_CONTENT_WIDTH_EPSILON));
1303
+ return {
1304
+ width: maxWidth,
1305
+ lineCount
1306
+ };
1307
+ }
1308
+ function layoutRichText(ctx, spans, maxWidth, defaultFont, defaultStyle) {
1309
+ if (spans.length === 0) return {
1310
+ width: 0,
1311
+ lines: [],
1312
+ overflowed: false
1313
+ };
1314
+ const prepared = readRichPrepared(spans, defaultFont);
1315
+ const lineRanges = [];
1316
+ walkRichInlineLineRanges(prepared, maxWidth, (line) => lineRanges.push(line));
1317
+ if (lineRanges.length === 0) return {
1318
+ width: 0,
1319
+ lines: [],
1320
+ overflowed: false
1321
+ };
1322
+ const lines = lineRanges.map((lr) => materializeRichLine(ctx, spans, defaultFont, defaultStyle, lr, false));
1323
+ return {
1324
+ width: lines.reduce((max, line) => Math.max(max, line.width), 0),
1325
+ lines,
1326
+ overflowed: false
1327
+ };
1328
+ }
1329
+ function layoutRichTextIntrinsic(ctx, spans, defaultFont, defaultStyle) {
1330
+ return layoutRichText(ctx, spans, INTRINSIC_MAX_WIDTH, defaultFont, defaultStyle);
1331
+ }
1332
+ function layoutRichTextWithOverflow(ctx, spans, maxWidth, defaultFont, defaultStyle, maxLines, overflow = "clip") {
1333
+ if (spans.length === 0) return {
1334
+ width: 0,
1335
+ lines: [],
1336
+ overflowed: false
1337
+ };
1338
+ const normalizedMaxLines = normalizeMaxLines(maxLines);
1339
+ const layout = layoutRichText(ctx, spans, maxWidth, defaultFont, defaultStyle);
1340
+ if (normalizedMaxLines == null || layout.lines.length <= normalizedMaxLines) return layout;
1341
+ const visibleLines = layout.lines.slice(0, normalizedMaxLines);
1342
+ if (overflow !== "ellipsis") return {
1343
+ width: visibleLines.reduce((max, line) => Math.max(max, line.width), 0),
1344
+ lines: visibleLines,
1345
+ overflowed: true
1346
+ };
1347
+ const lastLine = visibleLines[visibleLines.length - 1];
1348
+ if (lastLine == null || lastLine.fragments.length === 0) return {
1349
+ width: visibleLines.slice(0, -1).reduce((max, line) => Math.max(max, line.width), 0),
1350
+ lines: visibleLines.slice(0, -1),
1351
+ overflowed: true
1352
+ };
1353
+ const lastFrag = lastLine.fragments[lastLine.fragments.length - 1];
1354
+ const prevFont1 = ctx.graphics.font;
1355
+ ctx.graphics.font = lastFrag.font;
1356
+ const ellipsisWidth = measureEllipsisWidth(ctx);
1357
+ ctx.graphics.font = prevFont1;
1358
+ if (maxWidth <= 0 || ellipsisWidth > maxWidth) {
1359
+ const truncatedLine = {
1360
+ width: 0,
1361
+ fragments: [],
1362
+ overflowed: true
1363
+ };
1364
+ return {
1365
+ width: visibleLines.slice(0, -1).reduce((max, line) => Math.max(max, line.width), 0),
1366
+ lines: [...visibleLines.slice(0, -1), truncatedLine],
1367
+ overflowed: true
1368
+ };
1369
+ }
1370
+ const budget = maxWidth - ellipsisWidth;
1371
+ const resultFragments = [];
1372
+ let usedWidth = 0;
1373
+ let ellipsisFont = lastFrag.font;
1374
+ let ellipsisStyle = lastFrag.style;
1375
+ let truncated = false;
1376
+ for (let fi = 0; fi < lastLine.fragments.length; fi++) {
1377
+ const frag = lastLine.fragments[fi];
1378
+ const neededGap = fi === 0 ? 0 : frag.gapBefore;
1379
+ const fragTotal = neededGap + frag.occupiedWidth;
1380
+ if (usedWidth + fragTotal <= budget) {
1381
+ resultFragments.push({
1382
+ ...frag,
1383
+ gapBefore: fi === 0 ? 0 : frag.gapBefore
1384
+ });
1385
+ usedWidth += fragTotal;
1386
+ } else {
1387
+ ellipsisFont = frag.font;
1388
+ ellipsisStyle = frag.style;
1389
+ const remaining = budget - usedWidth - neededGap;
1390
+ if (remaining > 0 && frag.text.length > 0) {
1391
+ const units = getPreparedUnits(readPreparedText(frag.text, frag.font, "normal", "normal"));
1392
+ let charWidth = 0;
1393
+ let charText = "";
1394
+ for (const unit of units) {
1395
+ if (charWidth + unit.width > remaining) break;
1396
+ charWidth += unit.width;
1397
+ charText += unit.text;
1398
+ }
1399
+ if (charText.length > 0) {
1400
+ resultFragments.push({
1401
+ ...frag,
1402
+ text: charText,
1403
+ occupiedWidth: charWidth,
1404
+ gapBefore: fi === 0 ? 0 : frag.gapBefore
1405
+ });
1406
+ usedWidth += neededGap + charWidth;
1407
+ }
1408
+ }
1409
+ truncated = true;
1410
+ break;
1411
+ }
1412
+ }
1413
+ const prevFont2 = ctx.graphics.font;
1414
+ ctx.graphics.font = ellipsisFont;
1415
+ const ellipsisShift = measureFontShift(ctx);
1416
+ ctx.graphics.font = prevFont2;
1417
+ resultFragments.push({
1418
+ itemIndex: -1,
1419
+ text: ELLIPSIS_GLYPH,
1420
+ font: ellipsisFont,
1421
+ style: ellipsisStyle,
1422
+ gapBefore: 0,
1423
+ occupiedWidth: ellipsisWidth,
1424
+ shift: ellipsisShift
1425
+ });
1426
+ const lastLineResult = {
1427
+ width: usedWidth + ellipsisWidth,
1428
+ fragments: resultFragments,
1429
+ overflowed: truncated || layout.lines.length > normalizedMaxLines
1430
+ };
1431
+ return {
1432
+ width: [...visibleLines.slice(0, -1), lastLineResult].reduce((max, line) => Math.max(max, line.width), 0),
1433
+ lines: [...visibleLines.slice(0, -1), lastLineResult],
1434
+ overflowed: true
1435
+ };
1436
+ }
1221
1437
  //#endregion
1222
1438
  //#region src/nodes/text.ts
1223
1439
  function resolvePhysicalTextAlign(options) {
@@ -1250,12 +1466,21 @@ function getSingleLineLayoutKey(maxWidth) {
1250
1466
  function getMultiLineMeasureLayoutKey(maxWidth) {
1251
1467
  return maxWidth == null ? "multi:measure:intrinsic" : `multi:measure:${maxWidth}`;
1252
1468
  }
1469
+ function getRichMultiLineMeasureLayoutKey(maxWidth) {
1470
+ return maxWidth == null ? "rich:measure:intrinsic" : `rich:measure:${maxWidth}`;
1471
+ }
1253
1472
  function getMultiLineDrawLayoutKey(maxWidth) {
1254
1473
  return maxWidth == null ? "multi:draw:intrinsic" : `multi:draw:${maxWidth}`;
1255
1474
  }
1475
+ function getRichMultiLineDrawLayoutKey(maxWidth) {
1476
+ return maxWidth == null ? "rich:draw:intrinsic" : `rich:draw:${maxWidth}`;
1477
+ }
1256
1478
  function getMultiLineOverflowLayoutKey(maxWidth) {
1257
1479
  return maxWidth == null ? "multi:overflow:intrinsic" : `multi:overflow:${maxWidth}`;
1258
1480
  }
1481
+ function getRichMultiLineOverflowLayoutKey(maxWidth) {
1482
+ return maxWidth == null ? "rich:overflow:intrinsic" : `rich:overflow:${maxWidth}`;
1483
+ }
1259
1484
  function shouldUseMultilineOverflowLayout(options) {
1260
1485
  return options.maxLines != null;
1261
1486
  }
@@ -1265,6 +1490,9 @@ function getSingleLineMinContentLayoutKey() {
1265
1490
  function getMultiLineMinContentLayoutKey() {
1266
1491
  return "multi:min-content";
1267
1492
  }
1493
+ function getRichMultiLineMinContentLayoutKey() {
1494
+ return "rich:min-content";
1495
+ }
1268
1496
  function getSingleLineLayout(node, ctx, text, options) {
1269
1497
  const maxWidth = normalizeTextMaxWidth(ctx.constraints?.maxWidth);
1270
1498
  return readCachedTextLayout(node, ctx, getSingleLineLayoutKey(maxWidth), () => maxWidth == null ? layoutFirstLineIntrinsic(ctx, text, options.whiteSpace, options.wordBreak) : options.overflow === "ellipsis" ? layoutEllipsizedFirstLine(ctx, text, maxWidth, options.ellipsisPosition ?? "end", options.whiteSpace, options.wordBreak) : layoutFirstLine(ctx, text, maxWidth, options.whiteSpace, options.wordBreak));
@@ -1308,12 +1536,36 @@ function getSingleLineMinContentLayout(node, ctx, text, options) {
1308
1536
  function getMultiLineMinContentLayout(node, ctx, text, whiteSpace, wordBreak, overflowWrap) {
1309
1537
  return readCachedTextLayout(node, ctx, getMultiLineMinContentLayoutKey(), () => measureTextMinContent(ctx, text, whiteSpace, wordBreak, overflowWrap));
1310
1538
  }
1539
+ function getRichMultiLineMeasureLayout(node, ctx, spans, options) {
1540
+ const maxWidth = normalizeTextMaxWidth(ctx.constraints?.maxWidth);
1541
+ if (maxWidth != null && shouldUseMultilineOverflowLayout(options)) {
1542
+ const layout = getRichMultiLineOverflowLayout(node, ctx, spans, options);
1543
+ return {
1544
+ width: layout.width,
1545
+ lineCount: layout.lines.length
1546
+ };
1547
+ }
1548
+ return readCachedTextLayout(node, ctx, getRichMultiLineMeasureLayoutKey(maxWidth), () => maxWidth == null ? measureRichTextIntrinsic(ctx, spans, options.font) : measureRichText(ctx, spans, maxWidth, options.font));
1549
+ }
1550
+ function getRichMultiLineOverflowLayout(node, ctx, spans, options) {
1551
+ const maxWidth = normalizeTextMaxWidth(ctx.constraints?.maxWidth);
1552
+ return readCachedTextLayout(node, ctx, getRichMultiLineOverflowLayoutKey(maxWidth), () => layoutRichTextWithOverflow(ctx, spans, maxWidth ?? 0, options.font, options.style, options.maxLines, options.overflow));
1553
+ }
1554
+ function getRichMultiLineDrawLayout(node, ctx, spans, options) {
1555
+ const maxWidth = normalizeTextMaxWidth(ctx.constraints?.maxWidth);
1556
+ if (maxWidth != null && shouldUseMultilineOverflowLayout(options)) return getRichMultiLineOverflowLayout(node, ctx, spans, options);
1557
+ return readCachedTextLayout(node, ctx, getRichMultiLineDrawLayoutKey(maxWidth), () => maxWidth == null ? layoutRichTextIntrinsic(ctx, spans, options.font, options.style) : layoutRichText(ctx, spans, maxWidth, options.font, options.style));
1558
+ }
1559
+ function getRichMultiLineMinContentLayout(node, ctx, spans, options) {
1560
+ return readCachedTextLayout(node, ctx, getRichMultiLineMinContentLayoutKey(), () => measureRichTextMinContent(ctx, spans, options.font, options.overflowWrap));
1561
+ }
1311
1562
  /**
1312
1563
  * Draws wrapped text using the configured line height and alignment.
1564
+ * Accepts either a plain string or an array of `InlineSpan` items for mixed inline styles.
1313
1565
  */
1314
1566
  var MultilineText = class {
1315
1567
  /**
1316
- * @param text Source text to measure and draw.
1568
+ * @param text Source text to measure and draw. Pass an `InlineSpan[]` for mixed inline styles.
1317
1569
  * @param options Text layout and drawing options.
1318
1570
  */
1319
1571
  constructor(text, options) {
@@ -1321,6 +1573,14 @@ var MultilineText = class {
1321
1573
  this.options = options;
1322
1574
  }
1323
1575
  measure(ctx) {
1576
+ if (typeof this.text !== "string") {
1577
+ const spans = this.text;
1578
+ const { width, lineCount } = getRichMultiLineMeasureLayout(this, ctx, spans, this.options);
1579
+ return {
1580
+ width,
1581
+ height: lineCount * this.options.lineHeight
1582
+ };
1583
+ }
1324
1584
  return ctx.with((g) => {
1325
1585
  g.font = this.options.font;
1326
1586
  const { width, lineCount } = getMultiLineMeasureLayout(this, ctx, this.text, this.options);
@@ -1331,6 +1591,14 @@ var MultilineText = class {
1331
1591
  });
1332
1592
  }
1333
1593
  measureMinContent(ctx) {
1594
+ if (typeof this.text !== "string") {
1595
+ const spans = this.text;
1596
+ const { width, lineCount } = getRichMultiLineMinContentLayout(this, ctx, spans, this.options);
1597
+ return {
1598
+ width,
1599
+ height: lineCount * this.options.lineHeight
1600
+ };
1601
+ }
1334
1602
  return ctx.with((g) => {
1335
1603
  g.font = this.options.font;
1336
1604
  const { width, lineCount } = getMultiLineMinContentLayout(this, ctx, this.text, this.options.whiteSpace, this.options.wordBreak, this.options.overflowWrap);
@@ -1341,6 +1609,30 @@ var MultilineText = class {
1341
1609
  });
1342
1610
  }
1343
1611
  draw(ctx, x, y) {
1612
+ if (typeof this.text !== "string") {
1613
+ const spans = this.text;
1614
+ const { width, lines } = getRichMultiLineDrawLayout(this, ctx, spans, this.options);
1615
+ const align = resolvePhysicalTextAlign(this.options);
1616
+ const startX = align === "right" ? x + width : align === "center" ? x + width / 2 : x;
1617
+ for (const line of lines) {
1618
+ let cursorX = startX;
1619
+ for (let fi = 0; fi < line.fragments.length; fi++) {
1620
+ const frag = line.fragments[fi];
1621
+ cursorX += frag.gapBefore;
1622
+ ctx.with((g) => {
1623
+ g.font = frag.font;
1624
+ g.fillStyle = ctx.resolveDynValue(frag.style ?? this.options.style);
1625
+ if (align === "right") g.textAlign = "right";
1626
+ else if (align === "center") g.textAlign = "center";
1627
+ else g.textAlign = "left";
1628
+ g.fillText(frag.text, cursorX, y + (this.options.lineHeight + frag.shift) / 2);
1629
+ });
1630
+ cursorX += frag.occupiedWidth;
1631
+ }
1632
+ y += this.options.lineHeight;
1633
+ }
1634
+ return false;
1635
+ }
1344
1636
  return ctx.with((g) => {
1345
1637
  g.font = this.options.font;
1346
1638
  g.fillStyle = ctx.resolveDynValue(this.options.style);