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 +29 -5
- package/index.d.mts +20 -4
- package/index.mjs +293 -1
- package/index.mjs.map +1 -1
- package/package.json +1 -1
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:
|
|
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);
|