chat-layout 1.1.0-4 → 1.1.0-5
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/README.md +3 -3
- package/example/chat.ts +12 -12
- package/example/test.ts +5 -5
- package/index.d.mts +4 -4
- package/index.mjs +17 -17
- package/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ const bubble = new RoundedBox(
|
|
|
20
20
|
new MultilineText(item.content, {
|
|
21
21
|
lineHeight: 20,
|
|
22
22
|
font: "16px system-ui",
|
|
23
|
-
|
|
23
|
+
color: "black",
|
|
24
24
|
align: "start",
|
|
25
25
|
}),
|
|
26
26
|
{ top: 6, bottom: 6, left: 10, right: 10, radii: 8, fill: "#ccc" },
|
|
@@ -64,7 +64,7 @@ Single-line `Text` can ellipsize at the start, end, or middle when a finite widt
|
|
|
64
64
|
const title = new Text("Extremely long thread title that should not blow out the row", {
|
|
65
65
|
lineHeight: 20,
|
|
66
66
|
font: "16px system-ui",
|
|
67
|
-
|
|
67
|
+
color: "#111",
|
|
68
68
|
overflow: "ellipsis",
|
|
69
69
|
ellipsisPosition: "middle",
|
|
70
70
|
});
|
|
@@ -76,7 +76,7 @@ Multi-line `MultilineText` can cap the visible line count and convert the last v
|
|
|
76
76
|
const preview = new MultilineText(reply.content, {
|
|
77
77
|
lineHeight: 16,
|
|
78
78
|
font: "13px system-ui",
|
|
79
|
-
|
|
79
|
+
color: "#444",
|
|
80
80
|
align: "start",
|
|
81
81
|
overflowWrap: "anywhere",
|
|
82
82
|
overflow: "ellipsis",
|
package/example/chat.ts
CHANGED
|
@@ -157,23 +157,23 @@ type ChatItem = MessageItem | RevokedItem;
|
|
|
157
157
|
|
|
158
158
|
const richTextMessage: InlineSpan<C>[] = [
|
|
159
159
|
{ text: "现在这个 chat example 可以直接展示 " },
|
|
160
|
-
{ text: "rich text", font: "700 16px system-ui",
|
|
160
|
+
{ text: "rich text", font: "700 16px system-ui", color: "#0f766e" },
|
|
161
161
|
{ text: " 了,支持 " },
|
|
162
|
-
{ text: "颜色",
|
|
162
|
+
{ text: "颜色", color: "#2563eb" },
|
|
163
163
|
{ text: "、" },
|
|
164
|
-
{ text: "粗体", font: "700 16px system-ui",
|
|
164
|
+
{ text: "粗体", font: "700 16px system-ui", color: "#b91c1c" },
|
|
165
165
|
{ text: ",以及 " },
|
|
166
|
-
{ text: "inline code", font: "15px ui-monospace, SFMono-Regular, Consolas, monospace",
|
|
166
|
+
{ text: "inline code", font: "15px ui-monospace, SFMono-Regular, Consolas, monospace", color: "#7c3aed" },
|
|
167
167
|
{ text: " 这样的片段混排。" },
|
|
168
168
|
];
|
|
169
169
|
|
|
170
170
|
const richReplyPreview: InlineSpan<C>[] = [
|
|
171
171
|
{ text: "回复预览里也能用 " },
|
|
172
|
-
{ text: "rich text", font: "700 13px system-ui",
|
|
172
|
+
{ text: "rich text", font: "700 13px system-ui", color: "#0f766e" },
|
|
173
173
|
{ text: ",比如 " },
|
|
174
|
-
{ text: "关键词高亮",
|
|
174
|
+
{ text: "关键词高亮", color: "#2563eb" },
|
|
175
175
|
{ text: " 和 " },
|
|
176
|
-
{ text: "code()", font: "12px ui-monospace, SFMono-Regular, Consolas, monospace",
|
|
176
|
+
{ text: "code()", font: "12px ui-monospace, SFMono-Regular, Consolas, monospace", color: "#7c3aed" },
|
|
177
177
|
{
|
|
178
178
|
text: ",超长内容仍然会按原来的两行省略规则收起,不需要额外处理。",
|
|
179
179
|
},
|
|
@@ -225,7 +225,7 @@ const renderItem = memoRenderItem((item: ChatItem): Node<C> => {
|
|
|
225
225
|
new Text(`${item.sender}已撤回一条消息`, {
|
|
226
226
|
lineHeight: 18,
|
|
227
227
|
font: "14px system-ui",
|
|
228
|
-
|
|
228
|
+
color: () => (currentHover?.id === item.id ? "#525252" : "#666"),
|
|
229
229
|
overflow: "ellipsis",
|
|
230
230
|
}),
|
|
231
231
|
{
|
|
@@ -262,7 +262,7 @@ const renderItem = memoRenderItem((item: ChatItem): Node<C> => {
|
|
|
262
262
|
new Text(item.sender, {
|
|
263
263
|
lineHeight: 15,
|
|
264
264
|
font: "12px system-ui",
|
|
265
|
-
|
|
265
|
+
color: "black",
|
|
266
266
|
}),
|
|
267
267
|
{
|
|
268
268
|
top: 0,
|
|
@@ -286,7 +286,7 @@ const renderItem = memoRenderItem((item: ChatItem): Node<C> => {
|
|
|
286
286
|
new MultilineText(item.content, {
|
|
287
287
|
lineHeight: 20,
|
|
288
288
|
font: "16px system-ui",
|
|
289
|
-
|
|
289
|
+
color: "black",
|
|
290
290
|
align: "start",
|
|
291
291
|
overflowWrap: "anywhere",
|
|
292
292
|
}),
|
|
@@ -302,12 +302,12 @@ const renderItem = memoRenderItem((item: ChatItem): Node<C> => {
|
|
|
302
302
|
new Text(item.reply.sender, {
|
|
303
303
|
lineHeight: 14,
|
|
304
304
|
font: "11px system-ui",
|
|
305
|
-
|
|
305
|
+
color: () => (currentHover?.id === item.id ? "#4d4d4d" : "#666"),
|
|
306
306
|
}),
|
|
307
307
|
new MultilineText(item.reply.content, {
|
|
308
308
|
lineHeight: 16,
|
|
309
309
|
font: "13px system-ui",
|
|
310
|
-
|
|
310
|
+
color: () => (currentHover?.id === item.id ? "#222" : "#444"),
|
|
311
311
|
align: "start",
|
|
312
312
|
overflow: "ellipsis",
|
|
313
313
|
overflowWrap: "anywhere",
|
package/example/test.ts
CHANGED
|
@@ -98,7 +98,7 @@ const node = new RoundedBox(
|
|
|
98
98
|
lineHeight: 20,
|
|
99
99
|
font: "400 16px monospace",
|
|
100
100
|
align: "center",
|
|
101
|
-
|
|
101
|
+
color: "black",
|
|
102
102
|
}),
|
|
103
103
|
{ align: "center" },
|
|
104
104
|
),
|
|
@@ -108,10 +108,10 @@ const node = new RoundedBox(
|
|
|
108
108
|
[
|
|
109
109
|
new ClickDetect(
|
|
110
110
|
new RoundedBox(
|
|
111
|
-
new Text("测试3".repeat(2), {
|
|
111
|
+
new Text("测试 3".repeat(2), {
|
|
112
112
|
lineHeight: 20,
|
|
113
113
|
font: "400 16px monospace",
|
|
114
|
-
|
|
114
|
+
color: () => color,
|
|
115
115
|
}),
|
|
116
116
|
{
|
|
117
117
|
left: 14,
|
|
@@ -128,7 +128,7 @@ const node = new RoundedBox(
|
|
|
128
128
|
lineHeight: 16,
|
|
129
129
|
font: "400 12px monospace",
|
|
130
130
|
align: "center",
|
|
131
|
-
|
|
131
|
+
color: "black",
|
|
132
132
|
}),
|
|
133
133
|
{
|
|
134
134
|
left: 10,
|
|
@@ -163,7 +163,7 @@ const node = new RoundedBox(
|
|
|
163
163
|
lineHeight: 20,
|
|
164
164
|
font: "400 16px monospace",
|
|
165
165
|
physicalAlign: "right",
|
|
166
|
-
|
|
166
|
+
color: "black",
|
|
167
167
|
}),
|
|
168
168
|
{
|
|
169
169
|
left: 10,
|
package/index.d.mts
CHANGED
|
@@ -72,8 +72,8 @@ interface TextStyleOptions<C extends CanvasRenderingContext2D> {
|
|
|
72
72
|
lineHeight: number;
|
|
73
73
|
/** Canvas font string used for measurement and drawing. */
|
|
74
74
|
font: string;
|
|
75
|
-
/**
|
|
76
|
-
|
|
75
|
+
/** Color or resolver used when drawing the text. */
|
|
76
|
+
color: DynValue<C, string>;
|
|
77
77
|
/** Default: normal; matches pretext and CSS-style collapsible whitespace behavior. */
|
|
78
78
|
whiteSpace?: TextWhiteSpaceMode;
|
|
79
79
|
/** Default: normal; use keep-all to match pretext's CJK-friendly line breaking mode. */
|
|
@@ -89,8 +89,8 @@ interface InlineSpan<C extends CanvasRenderingContext2D> {
|
|
|
89
89
|
text: string;
|
|
90
90
|
/** Canvas font string override for this fragment. Falls back to the node-level font. */
|
|
91
91
|
font?: string;
|
|
92
|
-
/**
|
|
93
|
-
|
|
92
|
+
/** Color override for this fragment. Falls back to the node-level color. */
|
|
93
|
+
color?: DynValue<C, string>;
|
|
94
94
|
/** Optional break hint forwarded to pretext rich-inline layout. */
|
|
95
95
|
break?: "normal" | "never";
|
|
96
96
|
/** Optional extra occupied width forwarded to pretext rich-inline layout. */
|
package/index.mjs
CHANGED
|
@@ -1235,12 +1235,12 @@ function readRichPrepared(spans, defaultFont) {
|
|
|
1235
1235
|
extraWidth: s.extraWidth
|
|
1236
1236
|
}))), RICH_PREPARED_CACHE_CAPACITY);
|
|
1237
1237
|
}
|
|
1238
|
-
function materializeRichLine(ctx, spans, defaultFont,
|
|
1238
|
+
function materializeRichLine(ctx, spans, defaultFont, defaultColor, lineRange, overflowed) {
|
|
1239
1239
|
const richLine = materializeRichInlineLineRange(readRichPrepared(spans, defaultFont), lineRange);
|
|
1240
1240
|
const fragments = richLine.fragments.map((frag) => {
|
|
1241
1241
|
const span = spans[frag.itemIndex];
|
|
1242
1242
|
const fragFont = span?.font ?? defaultFont;
|
|
1243
|
-
const
|
|
1243
|
+
const fragColor = span?.color ?? defaultColor;
|
|
1244
1244
|
const prevFont = ctx.graphics.font;
|
|
1245
1245
|
ctx.graphics.font = fragFont;
|
|
1246
1246
|
const shift = measureFontShift(ctx);
|
|
@@ -1249,7 +1249,7 @@ function materializeRichLine(ctx, spans, defaultFont, defaultStyle, lineRange, o
|
|
|
1249
1249
|
itemIndex: frag.itemIndex,
|
|
1250
1250
|
text: frag.text,
|
|
1251
1251
|
font: fragFont,
|
|
1252
|
-
|
|
1252
|
+
color: fragColor,
|
|
1253
1253
|
gapBefore: frag.gapBefore,
|
|
1254
1254
|
occupiedWidth: frag.occupiedWidth,
|
|
1255
1255
|
shift
|
|
@@ -1305,7 +1305,7 @@ function measureRichTextMinContent(ctx, spans, defaultFont, overflowWrap = "brea
|
|
|
1305
1305
|
lineCount
|
|
1306
1306
|
};
|
|
1307
1307
|
}
|
|
1308
|
-
function layoutRichText(ctx, spans, maxWidth, defaultFont,
|
|
1308
|
+
function layoutRichText(ctx, spans, maxWidth, defaultFont, defaultColor) {
|
|
1309
1309
|
if (spans.length === 0) return {
|
|
1310
1310
|
width: 0,
|
|
1311
1311
|
lines: [],
|
|
@@ -1319,24 +1319,24 @@ function layoutRichText(ctx, spans, maxWidth, defaultFont, defaultStyle) {
|
|
|
1319
1319
|
lines: [],
|
|
1320
1320
|
overflowed: false
|
|
1321
1321
|
};
|
|
1322
|
-
const lines = lineRanges.map((lr) => materializeRichLine(ctx, spans, defaultFont,
|
|
1322
|
+
const lines = lineRanges.map((lr) => materializeRichLine(ctx, spans, defaultFont, defaultColor, lr, false));
|
|
1323
1323
|
return {
|
|
1324
1324
|
width: lines.reduce((max, line) => Math.max(max, line.width), 0),
|
|
1325
1325
|
lines,
|
|
1326
1326
|
overflowed: false
|
|
1327
1327
|
};
|
|
1328
1328
|
}
|
|
1329
|
-
function layoutRichTextIntrinsic(ctx, spans, defaultFont,
|
|
1330
|
-
return layoutRichText(ctx, spans, INTRINSIC_MAX_WIDTH, defaultFont,
|
|
1329
|
+
function layoutRichTextIntrinsic(ctx, spans, defaultFont, defaultColor) {
|
|
1330
|
+
return layoutRichText(ctx, spans, INTRINSIC_MAX_WIDTH, defaultFont, defaultColor);
|
|
1331
1331
|
}
|
|
1332
|
-
function layoutRichTextWithOverflow(ctx, spans, maxWidth, defaultFont,
|
|
1332
|
+
function layoutRichTextWithOverflow(ctx, spans, maxWidth, defaultFont, defaultColor, maxLines, overflow = "clip") {
|
|
1333
1333
|
if (spans.length === 0) return {
|
|
1334
1334
|
width: 0,
|
|
1335
1335
|
lines: [],
|
|
1336
1336
|
overflowed: false
|
|
1337
1337
|
};
|
|
1338
1338
|
const normalizedMaxLines = normalizeMaxLines(maxLines);
|
|
1339
|
-
const layout = layoutRichText(ctx, spans, maxWidth, defaultFont,
|
|
1339
|
+
const layout = layoutRichText(ctx, spans, maxWidth, defaultFont, defaultColor);
|
|
1340
1340
|
if (normalizedMaxLines == null || layout.lines.length <= normalizedMaxLines) return layout;
|
|
1341
1341
|
const visibleLines = layout.lines.slice(0, normalizedMaxLines);
|
|
1342
1342
|
if (overflow !== "ellipsis") return {
|
|
@@ -1371,7 +1371,7 @@ function layoutRichTextWithOverflow(ctx, spans, maxWidth, defaultFont, defaultSt
|
|
|
1371
1371
|
const resultFragments = [];
|
|
1372
1372
|
let usedWidth = 0;
|
|
1373
1373
|
let ellipsisFont = lastFrag.font;
|
|
1374
|
-
let
|
|
1374
|
+
let ellipsisColor = lastFrag.color;
|
|
1375
1375
|
let truncated = false;
|
|
1376
1376
|
for (let fi = 0; fi < lastLine.fragments.length; fi++) {
|
|
1377
1377
|
const frag = lastLine.fragments[fi];
|
|
@@ -1385,7 +1385,7 @@ function layoutRichTextWithOverflow(ctx, spans, maxWidth, defaultFont, defaultSt
|
|
|
1385
1385
|
usedWidth += fragTotal;
|
|
1386
1386
|
} else {
|
|
1387
1387
|
ellipsisFont = frag.font;
|
|
1388
|
-
|
|
1388
|
+
ellipsisColor = frag.color;
|
|
1389
1389
|
const remaining = budget - usedWidth - neededGap;
|
|
1390
1390
|
if (remaining > 0 && frag.text.length > 0) {
|
|
1391
1391
|
const units = getPreparedUnits(readPreparedText(frag.text, frag.font, "normal", "normal"));
|
|
@@ -1418,7 +1418,7 @@ function layoutRichTextWithOverflow(ctx, spans, maxWidth, defaultFont, defaultSt
|
|
|
1418
1418
|
itemIndex: -1,
|
|
1419
1419
|
text: ELLIPSIS_GLYPH,
|
|
1420
1420
|
font: ellipsisFont,
|
|
1421
|
-
|
|
1421
|
+
color: ellipsisColor,
|
|
1422
1422
|
gapBefore: 0,
|
|
1423
1423
|
occupiedWidth: ellipsisWidth,
|
|
1424
1424
|
shift: ellipsisShift
|
|
@@ -1549,12 +1549,12 @@ function getRichMultiLineMeasureLayout(node, ctx, spans, options) {
|
|
|
1549
1549
|
}
|
|
1550
1550
|
function getRichMultiLineOverflowLayout(node, ctx, spans, options) {
|
|
1551
1551
|
const maxWidth = normalizeTextMaxWidth(ctx.constraints?.maxWidth);
|
|
1552
|
-
return readCachedTextLayout(node, ctx, getRichMultiLineOverflowLayoutKey(maxWidth), () => layoutRichTextWithOverflow(ctx, spans, maxWidth ?? 0, options.font, options.
|
|
1552
|
+
return readCachedTextLayout(node, ctx, getRichMultiLineOverflowLayoutKey(maxWidth), () => layoutRichTextWithOverflow(ctx, spans, maxWidth ?? 0, options.font, options.color, options.maxLines, options.overflow));
|
|
1553
1553
|
}
|
|
1554
1554
|
function getRichMultiLineDrawLayout(node, ctx, spans, options) {
|
|
1555
1555
|
const maxWidth = normalizeTextMaxWidth(ctx.constraints?.maxWidth);
|
|
1556
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.
|
|
1557
|
+
return readCachedTextLayout(node, ctx, getRichMultiLineDrawLayoutKey(maxWidth), () => maxWidth == null ? layoutRichTextIntrinsic(ctx, spans, options.font, options.color) : layoutRichText(ctx, spans, maxWidth, options.font, options.color));
|
|
1558
1558
|
}
|
|
1559
1559
|
function getRichMultiLineMinContentLayout(node, ctx, spans, options) {
|
|
1560
1560
|
return readCachedTextLayout(node, ctx, getRichMultiLineMinContentLayoutKey(), () => measureRichTextMinContent(ctx, spans, options.font, options.overflowWrap));
|
|
@@ -1621,7 +1621,7 @@ var MultilineText = class {
|
|
|
1621
1621
|
cursorX += frag.gapBefore;
|
|
1622
1622
|
ctx.with((g) => {
|
|
1623
1623
|
g.font = frag.font;
|
|
1624
|
-
g.fillStyle = ctx.resolveDynValue(frag.
|
|
1624
|
+
g.fillStyle = ctx.resolveDynValue(frag.color ?? this.options.color);
|
|
1625
1625
|
if (align === "right") g.textAlign = "right";
|
|
1626
1626
|
else if (align === "center") g.textAlign = "center";
|
|
1627
1627
|
else g.textAlign = "left";
|
|
@@ -1635,7 +1635,7 @@ var MultilineText = class {
|
|
|
1635
1635
|
}
|
|
1636
1636
|
return ctx.with((g) => {
|
|
1637
1637
|
g.font = this.options.font;
|
|
1638
|
-
g.fillStyle = ctx.resolveDynValue(this.options.
|
|
1638
|
+
g.fillStyle = ctx.resolveDynValue(this.options.color);
|
|
1639
1639
|
const { width, lines } = getMultiLineDrawLayout(this, ctx, this.text, this.options);
|
|
1640
1640
|
switch (resolvePhysicalTextAlign(this.options)) {
|
|
1641
1641
|
case "left":
|
|
@@ -1703,7 +1703,7 @@ var Text = class {
|
|
|
1703
1703
|
draw(ctx, x, y) {
|
|
1704
1704
|
return ctx.with((g) => {
|
|
1705
1705
|
g.font = this.options.font;
|
|
1706
|
-
g.fillStyle = ctx.resolveDynValue(this.options.
|
|
1706
|
+
g.fillStyle = ctx.resolveDynValue(this.options.color);
|
|
1707
1707
|
const { text, shift } = getSingleLineLayout(this, ctx, this.text, this.options);
|
|
1708
1708
|
g.fillText(text, x, y + (this.options.lineHeight + shift) / 2);
|
|
1709
1709
|
return false;
|