chat-layout 0.1.4 → 1.0.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,283 +1,85 @@
1
- import { layoutWithLines, prepareWithSegments } from "@chenglou/pretext";
2
- //#region src/text.ts
3
- function preprocessSegments(text) {
4
- return text.split("\n").map((line) => line.trim()).filter(Boolean);
1
+ import { layoutNextLine, layoutWithLines, prepareWithSegments, walkLineRanges } from "@chenglou/pretext";
2
+ //#region src/internal/node-registry.ts
3
+ const registry = /* @__PURE__ */ new WeakMap();
4
+ const revisions = /* @__PURE__ */ new WeakMap();
5
+ function getOwnershipError() {
6
+ return /* @__PURE__ */ new Error("A node can only be attached to one parent. Shared nodes are not supported.");
5
7
  }
6
- function layoutFirstLine(ctx, text, maxWidth) {
7
- if (maxWidth < 0) maxWidth = 0;
8
- const segment = preprocessSegments(text)[0];
9
- if (!segment) return {
10
- width: 0,
11
- text: "",
12
- shift: 0
13
- };
14
- const { fontBoundingBoxAscent: ascent = 0, fontBoundingBoxDescent: descent = 0 } = ctx.graphics.measureText(segment);
15
- const shift = ascent - descent;
16
- if (maxWidth === 0) return {
17
- width: 0,
18
- text: "",
19
- shift
20
- };
21
- const { lines } = layoutWithLines(prepareWithSegments(segment, ctx.graphics.font), maxWidth, 0);
22
- if (lines.length === 0) return {
23
- width: 0,
24
- text: "",
25
- shift
26
- };
27
- return {
28
- width: lines[0].width,
29
- text: lines[0].text,
30
- shift
31
- };
8
+ function getDetachOwnershipError() {
9
+ return /* @__PURE__ */ new Error("Cannot detach or replace a node from a parent that does not own it.");
32
10
  }
33
- function layoutText(ctx, text, maxWidth) {
34
- if (maxWidth < 0) maxWidth = 0;
35
- const segments = preprocessSegments(text);
36
- if (segments.length === 0 || maxWidth === 0) return {
37
- width: 0,
38
- lines: []
39
- };
40
- const font = ctx.graphics.font;
41
- let width = 0;
42
- const lines = [];
43
- for (const segment of segments) {
44
- const { fontBoundingBoxAscent: ascent = 0, fontBoundingBoxDescent: descent = 0 } = ctx.graphics.measureText(segment);
45
- const shift = ascent - descent;
46
- const { lines: segLines } = layoutWithLines(prepareWithSegments(segment, font), maxWidth, 0);
47
- for (const segLine of segLines) {
48
- width = Math.max(width, segLine.width);
49
- lines.push({
50
- width: segLine.width,
51
- text: segLine.text,
52
- shift
53
- });
54
- }
55
- }
56
- return {
57
- width,
58
- lines
59
- };
60
- }
61
- //#endregion
62
- //#region src/utils.ts
63
- function shallow(object) {
64
- return Object.create(object);
11
+ function bumpRevision(node) {
12
+ revisions.set(node, (revisions.get(node) ?? 0) + 1);
65
13
  }
66
- function shallowMerge(object, other) {
67
- return {
68
- __proto__: object,
69
- ...other
70
- };
14
+ function getNodeRevision(node) {
15
+ return revisions.get(node) ?? 0;
71
16
  }
72
- //#endregion
73
- //#region src/registry.ts
74
- const registry = /* @__PURE__ */ new WeakMap();
75
- function registerNodeParent(node, parent) {
17
+ function attachNodeToParent(node, parent) {
18
+ if (registry.has(node)) throw getOwnershipError();
76
19
  registry.set(node, parent);
20
+ bumpRevision(parent);
21
+ }
22
+ function replaceNodeParent(previousNode, nextNode, parent) {
23
+ if (previousNode === nextNode) return;
24
+ if (registry.get(previousNode) !== parent) throw getDetachOwnershipError();
25
+ if (registry.has(nextNode)) throw getOwnershipError();
26
+ registry.delete(previousNode);
27
+ registry.set(nextNode, parent);
28
+ bumpRevision(parent);
77
29
  }
78
- function unregisterNodeParent(node) {
79
- registry.delete(node);
30
+ function replaceNodesParent(previousNodes, nextNodes, parent) {
31
+ const previousSnapshot = Array.from(previousNodes);
32
+ const nextSnapshot = Array.from(nextNodes);
33
+ if (previousSnapshot.length === nextSnapshot.length && previousSnapshot.every((node, index) => node === nextSnapshot[index])) return;
34
+ const previousSet = new Set(previousSnapshot);
35
+ const nextSet = /* @__PURE__ */ new Set();
36
+ for (const node of nextSnapshot) {
37
+ if (nextSet.has(node)) throw getOwnershipError();
38
+ nextSet.add(node);
39
+ const currentParent = registry.get(node);
40
+ if (currentParent != null && currentParent !== parent) throw getOwnershipError();
41
+ }
42
+ for (const node of previousSnapshot) if (!nextSet.has(node)) {
43
+ if (registry.get(node) !== parent) throw getDetachOwnershipError();
44
+ registry.delete(node);
45
+ }
46
+ for (const node of nextSnapshot) if (!previousSet.has(node)) registry.set(node, parent);
47
+ bumpRevision(parent);
80
48
  }
81
- function getNodeParent(node) {
82
- return registry.get(node);
49
+ function forEachNodeAncestor(node, visitor) {
50
+ let current = node;
51
+ while (current = registry.get(current)) visitor(current);
83
52
  }
84
53
  //#endregion
85
- //#region src/nodes.ts
54
+ //#region src/nodes/base.ts
86
55
  var Group = class {
56
+ #children;
87
57
  constructor(children) {
88
- this.children = children;
89
- for (const child of children) registerNodeParent(child, this);
90
- }
91
- get flex() {
92
- return this.children.some((item) => item.flex);
93
- }
94
- };
95
- var VStack = class extends Group {
96
- constructor(children, options = {}) {
97
- super(children);
98
- this.options = options;
99
- }
100
- measure(ctx) {
101
- let width = 0;
102
- let height = 0;
103
- if (this.options.alignment != null) ctx.alignment = this.options.alignment;
104
- for (const [index, child] of this.children.entries()) {
105
- if (this.options.gap != null && index !== 0) height += this.options.gap;
106
- const result = shallow(ctx).measureNode(child);
107
- height += result.height;
108
- width = Math.max(width, result.width);
109
- }
110
- ctx.remainingWidth -= width;
111
- return {
112
- width,
113
- height
114
- };
115
- }
116
- draw(ctx, x, y) {
117
- let result = false;
118
- const fullWidth = ctx.measureNode(this).width;
119
- const alignment = this.options.alignment ?? ctx.alignment;
120
- if (this.options.alignment != null) ctx.alignment = this.options.alignment;
121
- for (const [index, child] of this.children.entries()) {
122
- if (this.options.gap != null && index !== 0) y += this.options.gap;
123
- const { width, height } = shallow(ctx).measureNode(child);
124
- const curCtx = shallow(ctx);
125
- let requestRedraw;
126
- if (alignment === "right") requestRedraw = child.draw(curCtx, x + fullWidth - width, y);
127
- else if (alignment === "center") requestRedraw = child.draw(curCtx, x + (fullWidth - width) / 2, y);
128
- else requestRedraw = child.draw(curCtx, x, y);
129
- result ||= requestRedraw;
130
- y += height;
131
- }
132
- return result;
133
- }
134
- hittest(ctx, test) {
135
- let y = 0;
136
- const fullWidth = ctx.measureNode(this).width;
137
- const alignment = this.options.alignment ?? ctx.alignment;
138
- if (this.options.alignment != null) ctx.alignment = this.options.alignment;
139
- for (const [index, child] of this.children.entries()) {
140
- if (this.options.gap != null && index !== 0) y += this.options.gap;
141
- const { width, height } = shallow(ctx).measureNode(child);
142
- const curCtx = shallow(ctx);
143
- if (test.y >= y && test.y < y + height) {
144
- let x;
145
- if (alignment === "right") x = test.x - fullWidth + width;
146
- else if (alignment === "center") x = test.x - (fullWidth - width) / 2;
147
- else x = test.x;
148
- if (x < 0 || x >= width) return false;
149
- return child.hittest(curCtx, shallowMerge(test, {
150
- x,
151
- y: test.y - y
152
- }));
153
- }
154
- y += height;
155
- }
156
- return false;
157
- }
158
- };
159
- var HStack = class extends Group {
160
- constructor(children, options = {}) {
161
- super(children);
162
- this.children = children;
163
- this.options = options;
164
- }
165
- measure(ctx) {
166
- let width = 0;
167
- let height = 0;
168
- let firstFlex;
169
- for (const [index, child] of this.children.entries()) {
170
- if (this.options.gap != null && index !== 0) width += this.options.gap;
171
- if (firstFlex == null && child.flex) {
172
- firstFlex = child;
173
- continue;
174
- }
175
- const curCtx = shallow(ctx);
176
- curCtx.remainingWidth = ctx.remainingWidth - width;
177
- const result = curCtx.measureNode(child);
178
- width += result.width;
179
- height = Math.max(height, result.height);
180
- }
181
- if (firstFlex != null) {
182
- const curCtx = shallow(ctx);
183
- curCtx.remainingWidth = ctx.remainingWidth - width;
184
- const result = curCtx.measureNode(firstFlex);
185
- width += result.width;
186
- height = Math.max(height, result.height);
187
- }
188
- return {
189
- width,
190
- height
191
- };
58
+ this.#children = [...children];
59
+ replaceNodesParent([], this.#children, this);
192
60
  }
193
- draw(ctx, x, y) {
194
- let result = false;
195
- const reverse = this.options.reverse ?? ctx.reverse;
196
- if (this.options.reverse) ctx.reverse = this.options.reverse;
197
- if (reverse) {
198
- x += ctx.measureNode(this).width;
199
- for (const [index, child] of this.children.entries()) {
200
- const gap = this.options.gap != null && index !== 0 ? this.options.gap : void 0;
201
- if (gap) {
202
- x -= gap;
203
- ctx.remainingWidth -= gap;
204
- }
205
- const { width } = shallow(ctx).measureNode(child);
206
- x -= width;
207
- const requestRedraw = child.draw(shallow(ctx), x, y);
208
- result ||= requestRedraw;
209
- ctx.remainingWidth -= width;
210
- }
211
- } else for (const [index, child] of this.children.entries()) {
212
- const gap = this.options.gap != null && index !== 0 ? this.options.gap : void 0;
213
- if (gap) {
214
- x += gap;
215
- ctx.remainingWidth -= gap;
216
- }
217
- const requestRedraw = child.draw(shallow(ctx), x, y);
218
- result ||= requestRedraw;
219
- const { width } = shallow(ctx).measureNode(child);
220
- ctx.remainingWidth -= width;
221
- x += width;
222
- }
223
- return result;
61
+ get children() {
62
+ return this.#children;
224
63
  }
225
- hittest(ctx, test) {
226
- const reverse = this.options.reverse ?? ctx.reverse;
227
- if (this.options.reverse) ctx.reverse = this.options.reverse;
228
- if (reverse) {
229
- let x = ctx.measureNode(this).width;
230
- for (const [index, child] of this.children.entries()) {
231
- const gap = this.options.gap != null && index !== 0 ? this.options.gap : void 0;
232
- if (gap) {
233
- x -= gap;
234
- ctx.remainingWidth -= gap;
235
- }
236
- const { width, height } = shallow(ctx).measureNode(child);
237
- x -= width;
238
- if (x <= test.x && test.x < x + width) {
239
- if (test.y >= height) return false;
240
- return child.hittest(shallow(ctx), shallowMerge(test, { x: test.x - x }));
241
- }
242
- ctx.remainingWidth -= width;
243
- }
244
- } else {
245
- let x = 0;
246
- for (const [index, child] of this.children.entries()) {
247
- const gap = this.options.gap != null && index !== 0 ? this.options.gap : void 0;
248
- if (gap) {
249
- x += gap;
250
- ctx.remainingWidth -= gap;
251
- }
252
- const { width, height } = shallow(ctx).measureNode(child);
253
- if (x <= test.x && test.x < x + width) {
254
- if (test.y >= height) return false;
255
- return child.hittest(shallow(ctx), shallowMerge(test, { x: test.x - x }));
256
- }
257
- x += width;
258
- ctx.remainingWidth -= width;
259
- }
260
- }
261
- return false;
64
+ replaceChildren(nextChildren) {
65
+ const nextSnapshot = [...nextChildren];
66
+ replaceNodesParent(this.#children, nextSnapshot, this);
67
+ this.#children = nextSnapshot;
262
68
  }
263
69
  };
264
70
  var Wrapper = class {
265
71
  #inner;
266
72
  constructor(inner) {
267
73
  this.#inner = inner;
268
- registerNodeParent(this.#inner, this);
74
+ attachNodeToParent(this.#inner, this);
269
75
  }
270
76
  get inner() {
271
77
  return this.#inner;
272
78
  }
273
79
  set inner(newNode) {
274
80
  if (newNode === this.#inner) return;
275
- unregisterNodeParent(this.#inner);
81
+ replaceNodeParent(this.#inner, newNode, this);
276
82
  this.#inner = newNode;
277
- registerNodeParent(newNode, this);
278
- }
279
- get flex() {
280
- return this.inner.flex;
281
83
  }
282
84
  measure(ctx) {
283
85
  return this.inner.measure(ctx);
@@ -289,6 +91,128 @@ var Wrapper = class {
289
91
  return this.inner.hittest(ctx, test);
290
92
  }
291
93
  };
94
+ //#endregion
95
+ //#region src/layout.ts
96
+ /**
97
+ * 创建 LayoutRect 的辅助函数
98
+ */
99
+ function createRect(x, y, width, height) {
100
+ return {
101
+ x,
102
+ y,
103
+ width,
104
+ height
105
+ };
106
+ }
107
+ /**
108
+ * 合并多个 rect 得到包含所有 rect 的最小外接矩形
109
+ */
110
+ function mergeRects(rects) {
111
+ if (rects.length === 0) return createRect(0, 0, 0, 0);
112
+ let minX = Infinity;
113
+ let minY = Infinity;
114
+ let maxX = -Infinity;
115
+ let maxY = -Infinity;
116
+ for (const rect of rects) {
117
+ minX = Math.min(minX, rect.x);
118
+ minY = Math.min(minY, rect.y);
119
+ maxX = Math.max(maxX, rect.x + rect.width);
120
+ maxY = Math.max(maxY, rect.y + rect.height);
121
+ }
122
+ return createRect(minX, minY, maxX - minX, maxY - minY);
123
+ }
124
+ /**
125
+ * 从子节点布局结果计算容器的 contentBox
126
+ */
127
+ function computeContentBox(children) {
128
+ return mergeRects(children.map((child) => child.contentBox));
129
+ }
130
+ /**
131
+ * 检查点是否在 rect 内
132
+ */
133
+ function pointInRect(x, y, rect) {
134
+ return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height;
135
+ }
136
+ /**
137
+ * 读取单子节点布局结果中的唯一 child。
138
+ */
139
+ function getSingleChildLayout(layout) {
140
+ return layout.children[0];
141
+ }
142
+ /**
143
+ * 在布局结果中按指定盒模型查找命中的 child,并返回局部坐标。
144
+ */
145
+ function findChildAtPoint(children, x, y, box = "contentBox") {
146
+ for (let i = children.length - 1; i >= 0; i -= 1) {
147
+ const child = children[i];
148
+ const target = box === "rect" ? child.rect : child.contentBox;
149
+ if (!pointInRect(x, y, target)) continue;
150
+ return {
151
+ child,
152
+ localX: x - target.x,
153
+ localY: y - target.y
154
+ };
155
+ }
156
+ }
157
+ //#endregion
158
+ //#region src/utils.ts
159
+ function shallow(object) {
160
+ return Object.create(object);
161
+ }
162
+ function shallowMerge(object, other) {
163
+ return {
164
+ __proto__: object,
165
+ ...other
166
+ };
167
+ }
168
+ //#endregion
169
+ //#region src/nodes/shared.ts
170
+ function withConstraints(ctx, constraints) {
171
+ const next = shallow(ctx);
172
+ next.constraints = constraints;
173
+ return next;
174
+ }
175
+ function getLayoutContext(ctx) {
176
+ return ctx;
177
+ }
178
+ function readLayoutResult(node, ctx) {
179
+ return getLayoutContext(ctx).getLayoutResult(node, ctx.constraints);
180
+ }
181
+ function writeLayoutResult(node, ctx, result) {
182
+ getLayoutContext(ctx).setLayoutResult(node, result, ctx.constraints);
183
+ }
184
+ function ensureLayoutResult(node, ctx) {
185
+ return readLayoutResult(node, ctx);
186
+ }
187
+ function drawLayoutChildren(node, ctx, x, y) {
188
+ const layoutResult = ensureLayoutResult(node, ctx);
189
+ if (!layoutResult) return false;
190
+ let result = false;
191
+ for (const childResult of layoutResult.children) result ||= childResult.node.draw(withConstraints(ctx, childResult.constraints), x + childResult.contentBox.x, y + childResult.contentBox.y);
192
+ return result;
193
+ }
194
+ function hittestLayoutChildren(node, ctx, test, box = "contentBox") {
195
+ const layoutResult = ensureLayoutResult(node, ctx);
196
+ if (!layoutResult) return false;
197
+ const hit = findChildAtPoint(layoutResult.children, test.x, test.y, box);
198
+ if (!hit) return false;
199
+ return hit.child.node.hittest(withConstraints(ctx, hit.child.constraints), shallowMerge(test, {
200
+ x: hit.localX,
201
+ y: hit.localY
202
+ }));
203
+ }
204
+ //#endregion
205
+ //#region src/nodes/box.ts
206
+ function clampToConstraints$1(value, min, max) {
207
+ let result = value;
208
+ if (min != null) result = Math.max(result, min);
209
+ if (max != null) result = Math.min(result, max);
210
+ return result;
211
+ }
212
+ function shrinkConstraint(value, padding) {
213
+ if (value == null) return;
214
+ return Math.max(0, value - padding);
215
+ }
292
216
  var PaddingBox = class extends Wrapper {
293
217
  constructor(inner, padding = {}) {
294
218
  super(inner);
@@ -307,80 +231,634 @@ var PaddingBox = class extends Wrapper {
307
231
  return this.padding.right ?? 0;
308
232
  }
309
233
  measure(ctx) {
310
- ctx.remainingWidth -= this.#left + this.#right;
311
- const { width, height } = ctx.measureNode(this.inner);
234
+ const paddingLeft = this.#left;
235
+ const paddingRight = this.#right;
236
+ const paddingTop = this.#top;
237
+ const paddingBottom = this.#bottom;
238
+ const horizontalPadding = paddingLeft + paddingRight;
239
+ const verticalPadding = paddingTop + paddingBottom;
240
+ const childConstraints = ctx.constraints ? {
241
+ ...ctx.constraints,
242
+ minWidth: shrinkConstraint(ctx.constraints.minWidth, horizontalPadding),
243
+ maxWidth: shrinkConstraint(ctx.constraints.maxWidth, horizontalPadding),
244
+ minHeight: shrinkConstraint(ctx.constraints.minHeight, verticalPadding),
245
+ maxHeight: shrinkConstraint(ctx.constraints.maxHeight, verticalPadding)
246
+ } : void 0;
247
+ const { width, height } = ctx.measureNode(this.inner, childConstraints);
248
+ const containerBox = createRect(0, 0, clampToConstraints$1(width + horizontalPadding, ctx.constraints?.minWidth, ctx.constraints?.maxWidth), clampToConstraints$1(height + verticalPadding, ctx.constraints?.minHeight, ctx.constraints?.maxHeight));
249
+ const childRect = createRect(paddingLeft, paddingTop, width, height);
250
+ writeLayoutResult(this, ctx, {
251
+ containerBox,
252
+ contentBox: childRect,
253
+ children: [{
254
+ node: this.inner,
255
+ rect: childRect,
256
+ contentBox: childRect,
257
+ constraints: childConstraints
258
+ }],
259
+ constraints: ctx.constraints
260
+ });
312
261
  return {
313
- width: width + this.#left + this.#right,
314
- height: height + this.#top + this.#bottom
262
+ width: containerBox.width,
263
+ height: containerBox.height
315
264
  };
316
265
  }
317
266
  draw(ctx, x, y) {
318
- ctx.remainingWidth -= this.#left + this.#right;
319
- return this.inner.draw(ctx, x + this.#left, y + this.#top);
267
+ const layoutResult = readLayoutResult(this, ctx);
268
+ if (!layoutResult) return this.inner.draw(ctx, x + this.#left, y + this.#top);
269
+ const childResult = getSingleChildLayout(layoutResult);
270
+ if (!childResult) return false;
271
+ return childResult.node.draw(withConstraints(ctx, childResult.constraints), x + childResult.rect.x, y + childResult.rect.y);
320
272
  }
321
273
  hittest(ctx, test) {
322
- ctx.remainingWidth -= this.#left + this.#right;
323
- const { width, height } = shallow(ctx).measureNode(this.inner);
324
- if (0 <= test.x - this.#left && test.x - this.#left < width && 0 <= test.y - this.#top && test.y - this.#top < height) return this.inner.hittest(shallow(ctx), shallowMerge(test, {
325
- x: test.x - this.#left,
326
- y: test.y - this.#top
274
+ const layoutResult = readLayoutResult(this, ctx);
275
+ if (!layoutResult) return false;
276
+ const hit = findChildAtPoint(layoutResult.children, test.x, test.y, "rect");
277
+ if (!hit) return false;
278
+ return hit.child.node.hittest(withConstraints(ctx, hit.child.constraints), shallowMerge(test, {
279
+ x: hit.localX,
280
+ y: hit.localY
327
281
  }));
282
+ }
283
+ };
284
+ var Fixed = class {
285
+ constructor(width, height) {
286
+ this.width = width;
287
+ this.height = height;
288
+ }
289
+ measure(_ctx) {
290
+ return {
291
+ width: this.width,
292
+ height: this.height
293
+ };
294
+ }
295
+ draw(_ctx, _x, _y) {
296
+ return false;
297
+ }
298
+ hittest(_ctx, _test) {
328
299
  return false;
329
300
  }
330
301
  };
331
- var AlignBox = class extends Wrapper {
332
- #shift = 0;
333
- constructor(inner, options) {
302
+ //#endregion
303
+ //#region src/nodes/flex.ts
304
+ function getMainSize(axis, box) {
305
+ return axis === "row" ? box.width : box.height;
306
+ }
307
+ function getCrossSize(axis, box) {
308
+ return axis === "row" ? box.height : box.width;
309
+ }
310
+ function getMinMain(axis, constraints) {
311
+ return axis === "row" ? constraints?.minWidth : constraints?.minHeight;
312
+ }
313
+ function getMaxMain(axis, constraints) {
314
+ return axis === "row" ? constraints?.maxWidth : constraints?.maxHeight;
315
+ }
316
+ function getMinCross(axis, constraints) {
317
+ return axis === "row" ? constraints?.minHeight : constraints?.minWidth;
318
+ }
319
+ function getMaxCross(axis, constraints) {
320
+ return axis === "row" ? constraints?.maxHeight : constraints?.maxWidth;
321
+ }
322
+ function createAxisConstraints(axis, constraints, main, cross = {}) {
323
+ if (constraints == null && main.min == null && main.max == null && cross.min == null && cross.max == null) return;
324
+ const next = { ...constraints };
325
+ if (axis === "row") {
326
+ next.minWidth = main.min;
327
+ next.maxWidth = main.max;
328
+ next.minHeight = cross.min;
329
+ next.maxHeight = cross.max;
330
+ } else {
331
+ next.minHeight = main.min;
332
+ next.maxHeight = main.max;
333
+ next.minWidth = cross.min;
334
+ next.maxWidth = cross.max;
335
+ }
336
+ return next;
337
+ }
338
+ function clampToConstraints(value, min, max) {
339
+ let result = value;
340
+ if (min != null) result = Math.max(result, min);
341
+ if (max != null) result = Math.min(result, max);
342
+ return result;
343
+ }
344
+ function getCrossAlignment(alignSelf, alignItems) {
345
+ if (alignSelf == null || alignSelf === "auto") return alignItems;
346
+ return alignSelf;
347
+ }
348
+ function getJustifySpacing(justifyContent, freeSpace, itemCount, gap) {
349
+ switch (justifyContent) {
350
+ case "center": return {
351
+ leading: freeSpace / 2,
352
+ between: gap
353
+ };
354
+ case "end": return {
355
+ leading: freeSpace,
356
+ between: gap
357
+ };
358
+ case "space-between": return {
359
+ leading: 0,
360
+ between: itemCount > 1 ? gap + freeSpace / (itemCount - 1) : gap
361
+ };
362
+ case "space-around": return {
363
+ leading: itemCount > 0 ? freeSpace / itemCount / 2 : 0,
364
+ between: itemCount > 0 ? gap + freeSpace / itemCount : gap
365
+ };
366
+ case "space-evenly": return {
367
+ leading: itemCount > 0 ? freeSpace / (itemCount + 1) : 0,
368
+ between: itemCount > 0 ? gap + freeSpace / (itemCount + 1) : gap
369
+ };
370
+ default: return {
371
+ leading: 0,
372
+ between: gap
373
+ };
374
+ }
375
+ }
376
+ function getCrossOffset(align, frameCross, contentCross) {
377
+ switch (align) {
378
+ case "center": return (frameCross - contentCross) / 2;
379
+ case "end": return frameCross - contentCross;
380
+ default: return 0;
381
+ }
382
+ }
383
+ function createRectFromAxis(axis, main, cross, mainSize, crossSize) {
384
+ return axis === "row" ? createRect(main, cross, mainSize, crossSize) : createRect(cross, main, crossSize, mainSize);
385
+ }
386
+ function readFlexItemOptions(child) {
387
+ if (child instanceof FlexItem) return child.item;
388
+ return {};
389
+ }
390
+ function computeFlexLayout(children, options, constraints, measureChild) {
391
+ const axis = options.direction ?? "row";
392
+ const gap = options.gap ?? 0;
393
+ const justifyContent = options.justifyContent ?? "start";
394
+ const alignItems = options.alignItems ?? "start";
395
+ const reverse = options.reverse ?? false;
396
+ const mainAxisSize = options.mainAxisSize ?? "fill";
397
+ const orderedChildren = reverse ? [...children].reverse() : children;
398
+ const maxMain = getMaxMain(axis, constraints);
399
+ const minMain = getMinMain(axis, constraints);
400
+ const maxCross = getMaxCross(axis, constraints);
401
+ const minCross = getMinCross(axis, constraints);
402
+ const gapTotal = orderedChildren.length > 1 ? gap * (orderedChildren.length - 1) : 0;
403
+ const finiteMain = maxMain != null;
404
+ const finiteCross = maxCross != null;
405
+ const availableMain = finiteMain ? Math.max(0, maxMain - gapTotal) : void 0;
406
+ let consumedMain = 0;
407
+ let totalGrow = 0;
408
+ const measurements = /* @__PURE__ */ new Map();
409
+ for (const child of orderedChildren) {
410
+ const item = readFlexItemOptions(child);
411
+ const grow = item.grow ?? 0;
412
+ totalGrow += grow;
413
+ if (grow > 0 && finiteMain) continue;
414
+ const effectiveAlign = getCrossAlignment(item.alignSelf, alignItems);
415
+ const stretch = effectiveAlign === "stretch";
416
+ const childConstraints = createAxisConstraints(axis, constraints, { max: finiteMain && availableMain != null ? Math.max(0, availableMain - consumedMain) : maxMain }, {
417
+ min: void 0,
418
+ max: maxCross
419
+ });
420
+ const measured = measureChild(child, childConstraints);
421
+ const frameMain = getMainSize(axis, measured);
422
+ const frameCross = getCrossSize(axis, measured);
423
+ measurements.set(child, {
424
+ child,
425
+ item,
426
+ measured,
427
+ initialConstraints: childConstraints,
428
+ finalConstraints: childConstraints,
429
+ allocatedMain: void 0,
430
+ grow,
431
+ effectiveAlign,
432
+ stretch,
433
+ frameMain,
434
+ frameCross
435
+ });
436
+ consumedMain += frameMain;
437
+ }
438
+ const remainingMain = finiteMain && availableMain != null ? Math.max(0, availableMain - consumedMain) : void 0;
439
+ for (const child of orderedChildren) {
440
+ if (measurements.has(child)) continue;
441
+ const item = readFlexItemOptions(child);
442
+ const grow = item.grow ?? 0;
443
+ const effectiveAlign = getCrossAlignment(item.alignSelf, alignItems);
444
+ const stretch = effectiveAlign === "stretch";
445
+ const allocatedMain = finiteMain && remainingMain != null && totalGrow > 0 ? remainingMain * grow / totalGrow : void 0;
446
+ const childConstraints = createAxisConstraints(axis, constraints, { max: allocatedMain }, {
447
+ min: void 0,
448
+ max: maxCross
449
+ });
450
+ const measured = measureChild(child, childConstraints);
451
+ const measuredMain = getMainSize(axis, measured);
452
+ const frameMain = allocatedMain ?? measuredMain;
453
+ const frameCross = getCrossSize(axis, measured);
454
+ measurements.set(child, {
455
+ child,
456
+ item,
457
+ measured,
458
+ initialConstraints: childConstraints,
459
+ finalConstraints: childConstraints,
460
+ allocatedMain,
461
+ grow,
462
+ effectiveAlign,
463
+ stretch,
464
+ frameMain,
465
+ frameCross
466
+ });
467
+ }
468
+ let contentMain = gapTotal;
469
+ let contentCross = 0;
470
+ for (const child of orderedChildren) {
471
+ const measurement = measurements.get(child);
472
+ contentMain += measurement.frameMain;
473
+ contentCross = Math.max(contentCross, measurement.frameCross);
474
+ }
475
+ finiteMain && mainAxisSize === "fill" || clampToConstraints(contentMain, minMain, maxMain);
476
+ const containerCross = clampToConstraints(contentCross, minCross, maxCross);
477
+ if (finiteCross) {
478
+ for (const child of orderedChildren) {
479
+ const measurement = measurements.get(child);
480
+ if (!measurement.stretch) continue;
481
+ const finalConstraints = createAxisConstraints(axis, measurement.initialConstraints, {
482
+ min: getMinMain(axis, measurement.initialConstraints),
483
+ max: getMaxMain(axis, measurement.initialConstraints)
484
+ }, {
485
+ min: containerCross,
486
+ max: containerCross
487
+ });
488
+ const remeasured = measureChild(child, finalConstraints);
489
+ measurement.measured = remeasured;
490
+ measurement.finalConstraints = finalConstraints;
491
+ measurement.frameCross = containerCross;
492
+ measurement.frameMain = measurement.allocatedMain ?? getMainSize(axis, remeasured);
493
+ }
494
+ contentMain = gapTotal;
495
+ contentCross = 0;
496
+ for (const child of orderedChildren) {
497
+ const measurement = measurements.get(child);
498
+ contentMain += measurement.frameMain;
499
+ contentCross = Math.max(contentCross, getCrossSize(axis, measurement.measured));
500
+ }
501
+ }
502
+ const finalContainerMain = finiteMain && mainAxisSize === "fill" ? Math.max(maxMain, contentMain) : clampToConstraints(contentMain, minMain, maxMain);
503
+ const spacing = getJustifySpacing(justifyContent, Math.max(0, finalContainerMain - contentMain), orderedChildren.length, gap);
504
+ const childResults = [];
505
+ let cursor = spacing.leading;
506
+ for (const child of orderedChildren) {
507
+ const measurement = measurements.get(child);
508
+ const frameCross = measurement.stretch && finiteCross ? containerCross : measurement.frameCross;
509
+ const contentMainSize = getMainSize(axis, measurement.measured);
510
+ const contentCrossSize = getCrossSize(axis, measurement.measured);
511
+ const rectCross = measurement.stretch ? 0 : getCrossOffset(measurement.effectiveAlign, containerCross, frameCross);
512
+ const contentCrossOffset = rectCross + getCrossOffset(measurement.effectiveAlign, frameCross, contentCrossSize);
513
+ const rect = createRectFromAxis(axis, cursor, rectCross, measurement.frameMain, frameCross);
514
+ const contentBox = createRectFromAxis(axis, cursor, contentCrossOffset, contentMainSize, contentCrossSize);
515
+ childResults.push({
516
+ node: child,
517
+ rect,
518
+ contentBox,
519
+ constraints: measurement.finalConstraints
520
+ });
521
+ cursor += measurement.frameMain + spacing.between;
522
+ }
523
+ const containerBox = axis === "row" ? createRect(0, 0, finalContainerMain, containerCross) : createRect(0, 0, containerCross, finalContainerMain);
524
+ const finalContentBox = childResults.length > 0 ? computeContentBox(childResults) : createRect(0, 0, 0, 0);
525
+ return {
526
+ box: {
527
+ width: containerBox.width,
528
+ height: containerBox.height
529
+ },
530
+ layout: {
531
+ containerBox,
532
+ contentBox: finalContentBox,
533
+ children: childResults,
534
+ constraints
535
+ }
536
+ };
537
+ }
538
+ var FlexItem = class extends Wrapper {
539
+ constructor(inner, item = {}) {
334
540
  super(inner);
541
+ this.item = item;
542
+ }
543
+ };
544
+ var Flex = class extends Group {
545
+ constructor(children, options = {}) {
546
+ super(children);
335
547
  this.options = options;
336
548
  }
337
549
  measure(ctx) {
338
- ctx.alignment = this.options.alignment;
339
- const { width, height } = ctx.measureNode(this.inner);
340
- switch (this.options.alignment) {
341
- case "center":
342
- this.#shift = (ctx.remainingWidth - width) / 2;
343
- break;
344
- case "right":
345
- this.#shift = ctx.remainingWidth - width;
346
- break;
347
- default: this.#shift = 0;
348
- }
550
+ const result = computeFlexLayout(this.children, this.options, ctx.constraints, (node, constraints) => ctx.measureNode(node, constraints));
551
+ writeLayoutResult(this, ctx, result.layout);
552
+ return result.box;
553
+ }
554
+ draw(ctx, x, y) {
555
+ return drawLayoutChildren(this, ctx, x, y);
556
+ }
557
+ hittest(ctx, test) {
558
+ return hittestLayoutChildren(this, ctx, test, "contentBox");
559
+ }
560
+ };
561
+ //#endregion
562
+ //#region src/nodes/place.ts
563
+ function resolveHorizontalOffset(align, availableWidth, childWidth) {
564
+ switch (align) {
565
+ case "center": return (availableWidth - childWidth) / 2;
566
+ case "end": return availableWidth - childWidth;
567
+ case "start": return 0;
568
+ }
569
+ }
570
+ var Place = class extends Wrapper {
571
+ constructor(inner, options = {}) {
572
+ super(inner);
573
+ this.options = options;
574
+ }
575
+ measure(ctx) {
576
+ const availableWidth = ctx.constraints?.maxWidth;
577
+ const expand = this.options.expand ?? true;
578
+ const childConstraints = ctx.constraints ? { ...ctx.constraints } : void 0;
579
+ const childBox = ctx.measureNode(this.inner, childConstraints);
580
+ let width = expand && availableWidth != null ? availableWidth : childBox.width;
581
+ if (ctx.constraints?.minWidth != null) width = Math.max(width, ctx.constraints.minWidth);
582
+ if (ctx.constraints?.maxWidth != null) width = Math.min(width, ctx.constraints.maxWidth);
583
+ const childRect = createRect(resolveHorizontalOffset(this.options.align ?? "start", width, childBox.width), 0, childBox.width, childBox.height);
584
+ writeLayoutResult(this, ctx, {
585
+ containerBox: createRect(0, 0, width, childBox.height),
586
+ contentBox: childRect,
587
+ children: [{
588
+ node: this.inner,
589
+ rect: childRect,
590
+ contentBox: createRect(0, 0, childBox.width, childBox.height),
591
+ constraints: childConstraints
592
+ }],
593
+ constraints: ctx.constraints
594
+ });
349
595
  return {
350
- width: ctx.remainingWidth,
351
- height
596
+ width,
597
+ height: childBox.height
352
598
  };
353
599
  }
354
600
  draw(ctx, x, y) {
355
- ctx.alignment = this.options.alignment;
356
- return this.inner.draw(ctx, x + this.#shift, y);
601
+ const layoutResult = readLayoutResult(this, ctx);
602
+ if (!layoutResult) return this.inner.draw(ctx, x, y);
603
+ const childResult = getSingleChildLayout(layoutResult);
604
+ if (!childResult) return false;
605
+ const childCtx = withConstraints(ctx, childResult.constraints);
606
+ return childResult.node.draw(childCtx, x + childResult.rect.x, y + childResult.rect.y);
357
607
  }
358
608
  hittest(ctx, test) {
359
- ctx.alignment = this.options.alignment;
360
- const { width } = shallow(ctx).measureNode(this.inner);
361
- if (0 <= test.x - this.#shift && test.x - this.#shift < width) return this.inner.hittest(shallow(ctx), shallowMerge(test, { x: test.x - this.#shift }));
362
- return false;
609
+ const layoutResult = readLayoutResult(this, ctx);
610
+ if (!layoutResult) return false;
611
+ const hit = findChildAtPoint(layoutResult.children, test.x, test.y, "rect");
612
+ if (!hit) return false;
613
+ return hit.child.node.hittest(withConstraints(ctx, hit.child.constraints), shallowMerge(test, {
614
+ x: hit.localX,
615
+ y: hit.localY
616
+ }));
363
617
  }
364
618
  };
619
+ //#endregion
620
+ //#region src/text.ts
621
+ const FONT_SHIFT_PROBE = "M";
622
+ const PREPARED_SEGMENT_CACHE_CAPACITY = 512;
623
+ const FONT_SHIFT_CACHE_CAPACITY = 64;
624
+ const LINE_START_CURSOR = {
625
+ segmentIndex: 0,
626
+ graphemeIndex: 0
627
+ };
628
+ const preparedSegmentCache = /* @__PURE__ */ new Map();
629
+ const fontShiftCache = /* @__PURE__ */ new Map();
630
+ function preprocessSegments(text, whitespace = "preserve") {
631
+ const segments = text.split("\n");
632
+ if (whitespace === "trim-and-collapse") return segments.map((line) => line.trim()).filter((line) => line.length > 0);
633
+ return segments;
634
+ }
635
+ function readLruValue(cache, key) {
636
+ const cached = cache.get(key);
637
+ if (cached == null) return;
638
+ cache.delete(key);
639
+ cache.set(key, cached);
640
+ return cached;
641
+ }
642
+ function writeLruValue(cache, key, value, capacity) {
643
+ if (cache.has(key)) cache.delete(key);
644
+ else if (cache.size >= capacity) {
645
+ const firstKey = cache.keys().next().value;
646
+ if (firstKey != null) cache.delete(firstKey);
647
+ }
648
+ cache.set(key, value);
649
+ return value;
650
+ }
651
+ function getPreparedSegmentCacheKey(segment, font) {
652
+ return `${font}\u0000${segment}`;
653
+ }
654
+ function readPreparedSegment(segment, font) {
655
+ const key = getPreparedSegmentCacheKey(segment, font);
656
+ const cached = readLruValue(preparedSegmentCache, key);
657
+ if (cached != null) return cached;
658
+ return writeLruValue(preparedSegmentCache, key, prepareWithSegments(segment, font), PREPARED_SEGMENT_CACHE_CAPACITY);
659
+ }
660
+ function measureFontShift(ctx) {
661
+ const font = ctx.graphics.font;
662
+ const cached = readLruValue(fontShiftCache, font);
663
+ if (cached != null) return cached;
664
+ const { fontBoundingBoxAscent: ascent = 0, fontBoundingBoxDescent: descent = 0 } = ctx.graphics.measureText(FONT_SHIFT_PROBE);
665
+ return writeLruValue(fontShiftCache, font, ascent - descent, FONT_SHIFT_CACHE_CAPACITY);
666
+ }
667
+ function layoutFirstLineIntrinsic(ctx, text, whitespace = "preserve") {
668
+ const segment = preprocessSegments(text, whitespace)[0];
669
+ if (!segment) return {
670
+ width: 0,
671
+ text: "",
672
+ shift: 0
673
+ };
674
+ const shift = measureFontShift(ctx);
675
+ return {
676
+ width: ctx.graphics.measureText(segment).width,
677
+ text: segment,
678
+ shift
679
+ };
680
+ }
681
+ function measureTextIntrinsic(ctx, text, whitespace = "preserve") {
682
+ const segments = preprocessSegments(text, whitespace);
683
+ if (segments.length === 0) return {
684
+ width: 0,
685
+ lineCount: 0
686
+ };
687
+ let width = 0;
688
+ for (const segment of segments) width = Math.max(width, ctx.graphics.measureText(segment).width);
689
+ return {
690
+ width,
691
+ lineCount: segments.length
692
+ };
693
+ }
694
+ function layoutTextIntrinsic(ctx, text, whitespace = "preserve") {
695
+ const segments = preprocessSegments(text, whitespace);
696
+ if (segments.length === 0) return {
697
+ width: 0,
698
+ lines: []
699
+ };
700
+ const shift = measureFontShift(ctx);
701
+ let width = 0;
702
+ const lines = [];
703
+ for (const segment of segments) {
704
+ const measuredWidth = ctx.graphics.measureText(segment).width;
705
+ width = Math.max(width, measuredWidth);
706
+ lines.push({
707
+ width: measuredWidth,
708
+ text: segment,
709
+ shift
710
+ });
711
+ }
712
+ return {
713
+ width,
714
+ lines
715
+ };
716
+ }
717
+ function layoutFirstLine(ctx, text, maxWidth, whitespace = "preserve") {
718
+ if (maxWidth < 0) maxWidth = 0;
719
+ const segment = preprocessSegments(text, whitespace)[0];
720
+ if (!segment) return {
721
+ width: 0,
722
+ text: "",
723
+ shift: 0
724
+ };
725
+ const shift = measureFontShift(ctx);
726
+ if (maxWidth === 0) return {
727
+ width: 0,
728
+ text: "",
729
+ shift
730
+ };
731
+ const line = layoutNextLine(readPreparedSegment(segment, ctx.graphics.font), LINE_START_CURSOR, maxWidth);
732
+ if (line == null) return {
733
+ width: 0,
734
+ text: "",
735
+ shift
736
+ };
737
+ return {
738
+ width: line.width,
739
+ text: line.text,
740
+ shift
741
+ };
742
+ }
743
+ function measureText(ctx, text, maxWidth, whitespace = "preserve") {
744
+ if (maxWidth < 0) maxWidth = 0;
745
+ const segments = preprocessSegments(text, whitespace);
746
+ if (segments.length === 0 || maxWidth === 0) return {
747
+ width: 0,
748
+ lineCount: 0
749
+ };
750
+ const font = ctx.graphics.font;
751
+ let width = 0;
752
+ let lineCount = 0;
753
+ for (const segment of segments) {
754
+ if (segment.length === 0) {
755
+ lineCount += 1;
756
+ continue;
757
+ }
758
+ const prepared = readPreparedSegment(segment, font);
759
+ lineCount += walkLineRanges(prepared, maxWidth, (line) => {
760
+ width = Math.max(width, line.width);
761
+ });
762
+ }
763
+ return {
764
+ width,
765
+ lineCount
766
+ };
767
+ }
768
+ function layoutText(ctx, text, maxWidth, whitespace = "preserve") {
769
+ if (maxWidth < 0) maxWidth = 0;
770
+ const segments = preprocessSegments(text, whitespace);
771
+ if (segments.length === 0 || maxWidth === 0) return {
772
+ width: 0,
773
+ lines: []
774
+ };
775
+ const font = ctx.graphics.font;
776
+ const shift = measureFontShift(ctx);
777
+ let width = 0;
778
+ const lines = [];
779
+ for (const segment of segments) {
780
+ if (segment.length === 0) {
781
+ lines.push({
782
+ width: 0,
783
+ text: "",
784
+ shift
785
+ });
786
+ continue;
787
+ }
788
+ const { lines: segLines } = layoutWithLines(readPreparedSegment(segment, font), maxWidth, 0);
789
+ for (const segLine of segLines) {
790
+ width = Math.max(width, segLine.width);
791
+ lines.push({
792
+ width: segLine.width,
793
+ text: segLine.text,
794
+ shift
795
+ });
796
+ }
797
+ }
798
+ return {
799
+ width,
800
+ lines
801
+ };
802
+ }
803
+ //#endregion
804
+ //#region src/nodes/text.ts
805
+ function resolvePhysicalTextAlign(options) {
806
+ if (options.physicalAlign != null) return options.physicalAlign;
807
+ if (options.align != null) switch (options.align) {
808
+ case "start": return "left";
809
+ case "center": return "center";
810
+ case "end": return "right";
811
+ }
812
+ return "left";
813
+ }
814
+ function normalizeTextMaxWidth(maxWidth) {
815
+ if (maxWidth == null) return;
816
+ return Math.max(0, maxWidth);
817
+ }
818
+ function getTextLayoutContext(ctx) {
819
+ return ctx;
820
+ }
821
+ function readCachedTextLayout(node, ctx, key, compute) {
822
+ const textCtx = getTextLayoutContext(ctx);
823
+ const cached = textCtx.getTextLayout(node, key);
824
+ if (cached != null) return cached;
825
+ const layout = compute();
826
+ textCtx.setTextLayout(node, key, layout);
827
+ return layout;
828
+ }
829
+ function getSingleLineLayoutKey(maxWidth) {
830
+ return maxWidth == null ? "single:intrinsic" : `single:${maxWidth}`;
831
+ }
832
+ function getMultiLineMeasureLayoutKey(maxWidth) {
833
+ return maxWidth == null ? "multi:measure:intrinsic" : `multi:measure:${maxWidth}`;
834
+ }
835
+ function getMultiLineDrawLayoutKey(maxWidth) {
836
+ return maxWidth == null ? "multi:draw:intrinsic" : `multi:draw:${maxWidth}`;
837
+ }
838
+ function getSingleLineLayout(node, ctx, text, whitespace) {
839
+ const maxWidth = normalizeTextMaxWidth(ctx.constraints?.maxWidth);
840
+ return readCachedTextLayout(node, ctx, getSingleLineLayoutKey(maxWidth), () => maxWidth == null ? layoutFirstLineIntrinsic(ctx, text, whitespace) : layoutFirstLine(ctx, text, maxWidth, whitespace));
841
+ }
842
+ function getMultiLineMeasureLayout(node, ctx, text, whitespace) {
843
+ const maxWidth = normalizeTextMaxWidth(ctx.constraints?.maxWidth);
844
+ return readCachedTextLayout(node, ctx, getMultiLineMeasureLayoutKey(maxWidth), () => maxWidth == null ? measureTextIntrinsic(ctx, text, whitespace) : measureText(ctx, text, maxWidth, whitespace));
845
+ }
846
+ function getMultiLineDrawLayout(node, ctx, text, whitespace) {
847
+ const maxWidth = normalizeTextMaxWidth(ctx.constraints?.maxWidth);
848
+ return readCachedTextLayout(node, ctx, getMultiLineDrawLayoutKey(maxWidth), () => maxWidth == null ? layoutTextIntrinsic(ctx, text, whitespace) : layoutText(ctx, text, maxWidth, whitespace));
849
+ }
365
850
  var MultilineText = class {
366
- #width = 0;
367
- #lines = [];
368
851
  constructor(text, options) {
369
852
  this.text = text;
370
853
  this.options = options;
371
854
  }
372
- get flex() {
373
- return true;
374
- }
375
855
  measure(ctx) {
376
856
  return ctx.with((g) => {
377
857
  g.font = this.options.font;
378
- const { width, lines } = layoutText(ctx, this.text, ctx.remainingWidth);
379
- this.#width = width;
380
- this.#lines = lines;
858
+ const { width, lineCount } = getMultiLineMeasureLayout(this, ctx, this.text, this.options.whitespace);
381
859
  return {
382
- width: this.#width,
383
- height: this.#lines.length * this.options.lineHeight
860
+ width,
861
+ height: lineCount * this.options.lineHeight
384
862
  };
385
863
  });
386
864
  }
@@ -388,25 +866,26 @@ var MultilineText = class {
388
866
  return ctx.with((g) => {
389
867
  g.font = this.options.font;
390
868
  g.fillStyle = ctx.resolveDynValue(this.options.style);
391
- switch (this.options.alignment) {
869
+ const { width, lines } = getMultiLineDrawLayout(this, ctx, this.text, this.options.whitespace);
870
+ switch (resolvePhysicalTextAlign(this.options)) {
392
871
  case "left":
393
- for (const { text, shift } of this.#lines) {
872
+ for (const { text, shift } of lines) {
394
873
  g.fillText(text, x, y + (this.options.lineHeight + shift) / 2);
395
874
  y += this.options.lineHeight;
396
875
  }
397
876
  break;
398
877
  case "right":
399
- x += this.#width;
878
+ x += width;
400
879
  g.textAlign = "right";
401
- for (const { text, shift } of this.#lines) {
880
+ for (const { text, shift } of lines) {
402
881
  g.fillText(text, x, y + (this.options.lineHeight + shift) / 2);
403
882
  y += this.options.lineHeight;
404
883
  }
405
884
  break;
406
885
  case "center":
407
- x += this.#width / 2;
886
+ x += width / 2;
408
887
  g.textAlign = "center";
409
- for (const { text, shift } of this.#lines) {
888
+ for (const { text, shift } of lines) {
410
889
  g.fillText(text, x, y + (this.options.lineHeight + shift) / 2);
411
890
  y += this.options.lineHeight;
412
891
  }
@@ -420,25 +899,16 @@ var MultilineText = class {
420
899
  }
421
900
  };
422
901
  var Text = class {
423
- #width = 0;
424
- #text = "";
425
- #shift = 0;
426
902
  constructor(text, options) {
427
903
  this.text = text;
428
904
  this.options = options;
429
905
  }
430
- get flex() {
431
- return false;
432
- }
433
906
  measure(ctx) {
434
907
  return ctx.with((g) => {
435
908
  g.font = this.options.font;
436
- const { width, text, shift } = layoutFirstLine(ctx, this.text, ctx.remainingWidth);
437
- this.#width = width;
438
- this.#text = text;
439
- this.#shift = shift;
909
+ const { width } = getSingleLineLayout(this, ctx, this.text, this.options.whitespace);
440
910
  return {
441
- width: this.#width,
911
+ width,
442
912
  height: this.options.lineHeight
443
913
  };
444
914
  });
@@ -447,7 +917,8 @@ var Text = class {
447
917
  return ctx.with((g) => {
448
918
  g.font = this.options.font;
449
919
  g.fillStyle = ctx.resolveDynValue(this.options.style);
450
- g.fillText(this.#text, x, y + (this.options.lineHeight + this.#shift) / 2);
920
+ const { text, shift } = getSingleLineLayout(this, ctx, this.text, this.options.whitespace);
921
+ g.fillText(text, x, y + (this.options.lineHeight + shift) / 2);
451
922
  return false;
452
923
  });
453
924
  }
@@ -455,34 +926,20 @@ var Text = class {
455
926
  return false;
456
927
  }
457
928
  };
458
- var Fixed = class {
459
- constructor(width, height) {
460
- this.width = width;
461
- this.height = height;
462
- }
463
- get flex() {
464
- return false;
465
- }
466
- measure(_ctx) {
467
- return {
468
- width: this.width,
469
- height: this.height
470
- };
471
- }
472
- draw(_ctx, _x, _y) {
473
- return false;
474
- }
475
- hittest(_ctx, _test) {
476
- return false;
477
- }
478
- };
479
929
  //#endregion
480
- //#region src/renderer.ts
930
+ //#region src/renderer/base.ts
931
+ const MAX_CONSTRAINT_VARIANTS = 8;
932
+ function constraintKey(constraints) {
933
+ if (constraints == null) return "";
934
+ return `${constraints.minWidth ?? ""},${constraints.maxWidth ?? ""},${constraints.minHeight ?? ""},${constraints.maxHeight ?? ""}`;
935
+ }
481
936
  var BaseRenderer = class {
482
937
  graphics;
483
938
  #ctx;
484
939
  #lastWidth;
485
940
  #cache = /* @__PURE__ */ new WeakMap();
941
+ #layoutCache = /* @__PURE__ */ new WeakMap();
942
+ #textLayoutCache = /* @__PURE__ */ new WeakMap();
486
943
  get context() {
487
944
  return shallow(this.#ctx);
488
945
  }
@@ -493,19 +950,20 @@ var BaseRenderer = class {
493
950
  const self = this;
494
951
  this.#ctx = {
495
952
  graphics: this.graphics,
496
- get remainingWidth() {
497
- return this.graphics.canvas.clientWidth;
953
+ measureNode(node, constraints) {
954
+ return self.measureNode(node, constraints);
498
955
  },
499
- set remainingWidth(value) {
500
- Object.defineProperty(this, "remainingWidth", {
501
- value,
502
- writable: true
503
- });
956
+ getLayoutResult(node, constraints) {
957
+ return self.getLayoutResult(node, constraints);
958
+ },
959
+ setLayoutResult(node, result, constraints) {
960
+ self.setLayoutResult(node, result, constraints);
504
961
  },
505
- alignment: "left",
506
- reverse: false,
507
- measureNode(node) {
508
- return self.measureNode(node, this);
962
+ getTextLayout(node, key) {
963
+ return self.getTextLayout(node, key);
964
+ },
965
+ setTextLayout(node, key, layout) {
966
+ self.setTextLayout(node, key, layout);
509
967
  },
510
968
  invalidateNode: this.invalidateNode.bind(this),
511
969
  resolveDynValue(value) {
@@ -523,21 +981,131 @@ var BaseRenderer = class {
523
981
  };
524
982
  this.#lastWidth = this.graphics.canvas.clientWidth;
525
983
  }
984
+ #clearAllCaches() {
985
+ this.#cache = /* @__PURE__ */ new WeakMap();
986
+ this.#layoutCache = /* @__PURE__ */ new WeakMap();
987
+ this.#textLayoutCache = /* @__PURE__ */ new WeakMap();
988
+ }
989
+ #syncCachesToViewportWidth() {
990
+ const width = this.graphics.canvas.clientWidth;
991
+ if (this.#lastWidth === width) return;
992
+ this.#clearAllCaches();
993
+ this.#lastWidth = width;
994
+ }
995
+ getRootConstraints() {
996
+ return { maxWidth: this.graphics.canvas.clientWidth };
997
+ }
998
+ getRootContext() {
999
+ const ctx = this.context;
1000
+ ctx.constraints = this.getRootConstraints();
1001
+ return ctx;
1002
+ }
1003
+ measureRootNode(node) {
1004
+ return this.measureNode(node, this.getRootConstraints());
1005
+ }
1006
+ drawRootNode(node, x = 0, y = 0) {
1007
+ this.measureRootNode(node);
1008
+ return node.draw(this.getRootContext(), x, y);
1009
+ }
1010
+ hittestRootNode(node, test) {
1011
+ this.measureRootNode(node);
1012
+ return node.hittest(this.getRootContext(), test);
1013
+ }
526
1014
  invalidateNode(node) {
1015
+ this.#syncCachesToViewportWidth();
527
1016
  this.#cache.delete(node);
528
- let it = node;
529
- while (it = getNodeParent(it)) this.#cache.delete(it);
1017
+ this.#layoutCache.delete(node);
1018
+ this.#textLayoutCache.delete(node);
1019
+ forEachNodeAncestor(node, (ancestor) => {
1020
+ this.#cache.delete(ancestor);
1021
+ this.#layoutCache.delete(ancestor);
1022
+ this.#textLayoutCache.delete(ancestor);
1023
+ });
530
1024
  }
531
- measureNode(node, ctx) {
532
- if (this.#lastWidth !== this.graphics.canvas.clientWidth) {
533
- this.#cache = /* @__PURE__ */ new WeakMap();
534
- this.#lastWidth = this.graphics.canvas.clientWidth;
535
- } else {
536
- const result = this.#cache.get(node);
537
- if (result != null) return result;
1025
+ getLayoutResult(node, constraints) {
1026
+ this.#syncCachesToViewportWidth();
1027
+ const nodeCache = this.#layoutCache.get(node);
1028
+ if (nodeCache == null) return;
1029
+ const key = constraintKey(constraints);
1030
+ const cached = nodeCache.get(key);
1031
+ if (cached == null) return;
1032
+ if (cached.revision !== getNodeRevision(node)) {
1033
+ nodeCache.delete(key);
1034
+ return;
1035
+ }
1036
+ return cached.layout;
1037
+ }
1038
+ setLayoutResult(node, result, constraints) {
1039
+ this.#syncCachesToViewportWidth();
1040
+ let nodeCache = this.#layoutCache.get(node);
1041
+ if (nodeCache == null) {
1042
+ nodeCache = /* @__PURE__ */ new Map();
1043
+ this.#layoutCache.set(node, nodeCache);
1044
+ } else if (nodeCache.size >= MAX_CONSTRAINT_VARIANTS) {
1045
+ const firstKey = nodeCache.keys().next().value;
1046
+ nodeCache.delete(firstKey);
1047
+ }
1048
+ nodeCache.set(constraintKey(constraints), {
1049
+ revision: getNodeRevision(node),
1050
+ layout: result
1051
+ });
1052
+ }
1053
+ getTextLayout(node, key) {
1054
+ this.#syncCachesToViewportWidth();
1055
+ const nodeCache = this.#textLayoutCache.get(node);
1056
+ if (nodeCache == null) return;
1057
+ const cached = nodeCache.get(key);
1058
+ if (cached == null) return;
1059
+ if (cached.revision !== getNodeRevision(node)) {
1060
+ nodeCache.delete(key);
1061
+ return;
538
1062
  }
539
- const result = node.measure(ctx ?? this.context);
540
- this.#cache.set(node, result);
1063
+ return cached.layout;
1064
+ }
1065
+ setTextLayout(node, key, layout) {
1066
+ this.#syncCachesToViewportWidth();
1067
+ let nodeCache = this.#textLayoutCache.get(node);
1068
+ if (nodeCache == null) {
1069
+ nodeCache = /* @__PURE__ */ new Map();
1070
+ this.#textLayoutCache.set(node, nodeCache);
1071
+ } else if (nodeCache.size >= MAX_CONSTRAINT_VARIANTS) {
1072
+ const firstKey = nodeCache.keys().next().value;
1073
+ nodeCache.delete(firstKey);
1074
+ }
1075
+ nodeCache.set(key, {
1076
+ revision: getNodeRevision(node),
1077
+ layout
1078
+ });
1079
+ }
1080
+ measureNode(node, constraints) {
1081
+ this.#syncCachesToViewportWidth();
1082
+ {
1083
+ const nodeCache = this.#cache.get(node);
1084
+ if (nodeCache != null) {
1085
+ const key = constraintKey(constraints);
1086
+ const cached = nodeCache.get(key);
1087
+ if (cached != null) {
1088
+ if (cached.revision === getNodeRevision(node)) return cached.box;
1089
+ nodeCache.delete(key);
1090
+ }
1091
+ }
1092
+ }
1093
+ const ctx = this.context;
1094
+ if (constraints != null) ctx.constraints = constraints;
1095
+ const result = node.measure(ctx);
1096
+ const key = constraintKey(constraints);
1097
+ let nodeCache = this.#cache.get(node);
1098
+ if (nodeCache == null) {
1099
+ nodeCache = /* @__PURE__ */ new Map();
1100
+ this.#cache.set(node, nodeCache);
1101
+ } else if (nodeCache.size >= MAX_CONSTRAINT_VARIANTS) {
1102
+ const firstKey = nodeCache.keys().next().value;
1103
+ nodeCache.delete(firstKey);
1104
+ }
1105
+ nodeCache.set(key, {
1106
+ revision: getNodeRevision(node),
1107
+ box: result
1108
+ });
541
1109
  return result;
542
1110
  }
543
1111
  };
@@ -545,33 +1113,26 @@ var DebugRenderer = class extends BaseRenderer {
545
1113
  draw(node) {
546
1114
  const { clientWidth: viewportWidth, clientHeight: viewportHeight } = this.graphics.canvas;
547
1115
  this.graphics.clearRect(0, 0, viewportWidth, viewportHeight);
548
- return node.draw(this.context, 0, 0);
1116
+ return this.drawRootNode(node);
549
1117
  }
550
1118
  hittest(node, test) {
551
- return node.hittest(this.context, test);
1119
+ return this.hittestRootNode(node, test);
552
1120
  }
553
1121
  };
554
- function memoRenderItem(renderItem) {
555
- const cache = /* @__PURE__ */ new WeakMap();
556
- function fn(item) {
557
- const key = item;
558
- const cached = cache.get(key);
559
- if (cached != null) return cached;
560
- const result = renderItem(item);
561
- cache.set(key, result);
562
- return result;
563
- }
564
- return Object.assign(fn, { reset: (key) => cache.delete(key) });
565
- }
1122
+ //#endregion
1123
+ //#region src/renderer/list-state.ts
566
1124
  var ListState = class {
567
1125
  offset = 0;
568
- position = NaN;
1126
+ position;
569
1127
  items = [];
1128
+ constructor(items = []) {
1129
+ this.items = [...items];
1130
+ }
570
1131
  unshift(...items) {
571
1132
  this.unshiftAll(items);
572
1133
  }
573
1134
  unshiftAll(items) {
574
- this.position += items.length;
1135
+ if (this.position != null) this.position += items.length;
575
1136
  this.items = items.concat(this.items);
576
1137
  }
577
1138
  push(...items) {
@@ -580,20 +1141,59 @@ var ListState = class {
580
1141
  pushAll(items) {
581
1142
  this.items.push(...items);
582
1143
  }
583
- reset() {
584
- this.items = [];
1144
+ setAnchor(position, offset = 0) {
1145
+ this.position = Number.isFinite(position) ? Math.trunc(position) : void 0;
1146
+ this.offset = Number.isFinite(offset) ? offset : 0;
1147
+ }
1148
+ reset(items = []) {
1149
+ this.items = [...items];
585
1150
  this.offset = 0;
586
- this.position = NaN;
1151
+ this.position = void 0;
587
1152
  }
588
1153
  resetScroll() {
589
1154
  this.offset = 0;
590
- this.position = NaN;
1155
+ this.position = void 0;
591
1156
  }
592
1157
  applyScroll(delta) {
593
1158
  this.offset += delta;
594
1159
  }
595
1160
  };
596
- function clamp(value, min, max) {
1161
+ //#endregion
1162
+ //#region src/renderer/memo.ts
1163
+ function isWeakMapKey(value) {
1164
+ return typeof value === "object" && value !== null || typeof value === "function";
1165
+ }
1166
+ function memoRenderItem(renderItem) {
1167
+ const cache = /* @__PURE__ */ new WeakMap();
1168
+ function fn(item) {
1169
+ if (!isWeakMapKey(item)) throw new TypeError("memoRenderItem() only supports object items. Use memoRenderItemBy() for primitive keys.");
1170
+ const key = item;
1171
+ const cached = cache.get(key);
1172
+ if (cached != null) return cached;
1173
+ const result = renderItem(item);
1174
+ cache.set(key, result);
1175
+ return result;
1176
+ }
1177
+ return Object.assign(fn, { reset: (key) => cache.delete(key) });
1178
+ }
1179
+ function memoRenderItemBy(keyOf, renderItem) {
1180
+ const cache = /* @__PURE__ */ new Map();
1181
+ function fn(item) {
1182
+ const key = keyOf(item);
1183
+ const cached = cache.get(key);
1184
+ if (cached != null) return cached;
1185
+ const result = renderItem(item);
1186
+ cache.set(key, result);
1187
+ return result;
1188
+ }
1189
+ return Object.assign(fn, {
1190
+ reset: (item) => cache.delete(keyOf(item)),
1191
+ resetKey: (key) => cache.delete(key)
1192
+ });
1193
+ }
1194
+ //#endregion
1195
+ //#region src/renderer/virtualized/base.ts
1196
+ function clamp$3(value, min, max) {
597
1197
  return Math.min(Math.max(value, min), max);
598
1198
  }
599
1199
  function sameState(state, position, offset) {
@@ -629,28 +1229,39 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
629
1229
  set items(value) {
630
1230
  this.options.list.items = value;
631
1231
  }
1232
+ _readListState() {
1233
+ return {
1234
+ position: this.position,
1235
+ offset: this.offset
1236
+ };
1237
+ }
1238
+ _commitListState(state) {
1239
+ this.position = state.position;
1240
+ this.offset = state.offset;
1241
+ }
632
1242
  jumpTo(index, options = {}) {
633
1243
  if (this.items.length === 0) {
634
1244
  this.#cancelJumpAnimation();
635
1245
  return;
636
1246
  }
637
1247
  const targetIndex = this._clampItemIndex(index);
638
- this._prepareAnchorState();
639
- const targetAnchor = this._getTargetAnchor(targetIndex);
1248
+ const currentState = this._normalizeListState(this._readListState());
1249
+ const targetBlock = options.block ?? this._getDefaultJumpBlock();
1250
+ const targetAnchor = this._getTargetAnchor(targetIndex, targetBlock);
640
1251
  if (!(options.animated ?? true)) {
641
1252
  this.#cancelJumpAnimation();
642
1253
  this._applyAnchor(targetAnchor);
643
1254
  options.onComplete?.();
644
1255
  return;
645
1256
  }
646
- const startAnchor = this._readAnchor();
1257
+ const startAnchor = this._readAnchor(currentState);
647
1258
  if (!Number.isFinite(startAnchor)) {
648
1259
  this.#cancelJumpAnimation();
649
1260
  this._applyAnchor(targetAnchor);
650
1261
  options.onComplete?.();
651
1262
  return;
652
1263
  }
653
- const duration = clamp(options.duration ?? VirtualizedRenderer.MIN_JUMP_DURATION + Math.abs(targetAnchor - startAnchor) * VirtualizedRenderer.JUMP_DURATION_PER_ITEM, 0, VirtualizedRenderer.MAX_JUMP_DURATION);
1264
+ const duration = clamp$3(options.duration ?? VirtualizedRenderer.MIN_JUMP_DURATION + Math.abs(targetAnchor - startAnchor) * VirtualizedRenderer.JUMP_DURATION_PER_ITEM, 0, VirtualizedRenderer.MAX_JUMP_DURATION);
654
1265
  if (duration <= 0 || Math.abs(targetAnchor - startAnchor) <= Number.EPSILON) {
655
1266
  this.#cancelJumpAnimation();
656
1267
  this._applyAnchor(targetAnchor);
@@ -665,10 +1276,7 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
665
1276
  needsMoreFrames: true,
666
1277
  onComplete: options.onComplete
667
1278
  };
668
- this.#controlledState = {
669
- position: this.position,
670
- offset: this.offset
671
- };
1279
+ this.#controlledState = this._readListState();
672
1280
  }
673
1281
  _resetRenderFeedback(feedback) {
674
1282
  if (feedback == null) return;
@@ -680,8 +1288,8 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
680
1288
  _accumulateRenderFeedback(feedback, idx, top, height) {
681
1289
  if (!Number.isFinite(top) || !Number.isFinite(height) || height <= 0) return;
682
1290
  const viewportHeight = this.graphics.canvas.clientHeight;
683
- const visibleTop = clamp(-top, 0, height);
684
- const visibleBottom = clamp(viewportHeight - top, 0, height);
1291
+ const visibleTop = clamp$3(-top, 0, height);
1292
+ const visibleBottom = clamp$3(viewportHeight - top, 0, height);
685
1293
  if (visibleBottom <= visibleTop) return;
686
1294
  const itemMin = idx + visibleTop / height;
687
1295
  const itemMax = idx + visibleBottom / height;
@@ -693,14 +1301,26 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
693
1301
  _renderDrawList(list, shift, feedback) {
694
1302
  let result = false;
695
1303
  const viewportHeight = this.graphics.canvas.clientHeight;
696
- for (const { idx, node, offset, height } of list) {
1304
+ for (const { idx, value: node, offset, height } of list) {
697
1305
  const y = offset + shift;
698
1306
  if (feedback != null) this._accumulateRenderFeedback(feedback, idx, y, height);
699
1307
  if (y + height < 0 || y > viewportHeight) continue;
700
- if (node.draw(this.context, 0, y)) result = true;
1308
+ if (this.drawRootNode(node, 0, y)) result = true;
701
1309
  }
702
1310
  return result;
703
1311
  }
1312
+ _renderVisibleWindow(window, feedback) {
1313
+ this._resetRenderFeedback(feedback);
1314
+ return this._renderDrawList(window.drawList, window.shift, feedback);
1315
+ }
1316
+ _hittestVisibleWindow(window, test) {
1317
+ for (const { value: node, offset, height } of window.drawList) {
1318
+ const y = offset + window.shift;
1319
+ if (test.y < y || test.y >= y + height) continue;
1320
+ return node.hittest(this.getRootContext(), shallowMerge(test, { y: test.y - y }));
1321
+ }
1322
+ return false;
1323
+ }
704
1324
  _prepareRender() {
705
1325
  const animation = this.#jumpAnimation;
706
1326
  if (animation == null) return false;
@@ -712,7 +1332,7 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
712
1332
  this.#cancelJumpAnimation();
713
1333
  return false;
714
1334
  }
715
- const progress = clamp((getNow() - animation.startTime) / animation.duration, 0, 1);
1335
+ const progress = clamp$3((getNow() - animation.startTime) / animation.duration, 0, 1);
716
1336
  const eased = progress >= 1 ? 1 : smoothstep(progress);
717
1337
  const anchor = animation.startAnchor + (animation.targetAnchor - animation.startAnchor) * eased;
718
1338
  this._applyAnchor(anchor);
@@ -723,10 +1343,7 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
723
1343
  const animation = this.#jumpAnimation;
724
1344
  if (animation == null) return requestRedraw;
725
1345
  if (animation.needsMoreFrames) {
726
- this.#controlledState = {
727
- position: this.position,
728
- offset: this.offset
729
- };
1346
+ this.#controlledState = this._readListState();
730
1347
  return true;
731
1348
  }
732
1349
  const onComplete = animation.onComplete;
@@ -735,245 +1352,342 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
735
1352
  return requestRedraw || this.#jumpAnimation != null;
736
1353
  }
737
1354
  _clampItemIndex(index) {
738
- return clamp(Number.isFinite(index) ? Math.trunc(index) : 0, 0, this.items.length - 1);
1355
+ return clamp$3(Number.isFinite(index) ? Math.trunc(index) : 0, 0, this.items.length - 1);
739
1356
  }
740
1357
  _getItemHeight(index) {
741
1358
  const item = this.items[index];
742
1359
  const node = this.options.renderItem(item);
743
- return this.measureNode(node).height;
1360
+ return this.measureRootNode(node).height;
1361
+ }
1362
+ _getAnchorAtOffset(index, offset) {
1363
+ if (this.items.length === 0) return 0;
1364
+ let currentIndex = this._clampItemIndex(index);
1365
+ let remaining = Number.isFinite(offset) ? offset : 0;
1366
+ while (true) {
1367
+ if (remaining < 0) {
1368
+ if (currentIndex === 0) return 0;
1369
+ currentIndex -= 1;
1370
+ const height = this._getItemHeight(currentIndex);
1371
+ if (height > 0) remaining += height;
1372
+ continue;
1373
+ }
1374
+ const height = this._getItemHeight(currentIndex);
1375
+ if (height > 0) {
1376
+ if (remaining <= height) return currentIndex + remaining / height;
1377
+ remaining -= height;
1378
+ } else if (remaining === 0) return currentIndex;
1379
+ if (currentIndex === this.items.length - 1) return this.items.length;
1380
+ currentIndex += 1;
1381
+ }
744
1382
  }
745
1383
  #cancelJumpAnimation() {
746
1384
  this.#jumpAnimation = void 0;
747
1385
  this.#controlledState = void 0;
748
1386
  }
749
1387
  };
750
- var TimelineRenderer = class extends VirtualizedRenderer {
751
- _prepareAnchorState() {
752
- if (this.items.length === 0) return;
753
- if (!Number.isFinite(this.position)) {
754
- this.position = 0;
755
- this.offset = 0;
756
- return;
1388
+ //#endregion
1389
+ //#region src/renderer/virtualized/solver.ts
1390
+ function clamp$2(value, min, max) {
1391
+ return Math.min(Math.max(value, min), max);
1392
+ }
1393
+ function normalizeOffset(offset) {
1394
+ return Number.isFinite(offset) ? offset : 0;
1395
+ }
1396
+ function normalizeTimelineState(itemCount, state) {
1397
+ if (itemCount <= 0) return {
1398
+ position: 0,
1399
+ offset: 0
1400
+ };
1401
+ const position = state.position;
1402
+ if (typeof position !== "number" || !Number.isFinite(position)) return {
1403
+ position: 0,
1404
+ offset: normalizeOffset(state.offset)
1405
+ };
1406
+ return {
1407
+ position: clamp$2(Math.trunc(position), 0, itemCount - 1),
1408
+ offset: normalizeOffset(state.offset)
1409
+ };
1410
+ }
1411
+ function normalizeChatState(itemCount, state) {
1412
+ if (itemCount <= 0) return {
1413
+ position: 0,
1414
+ offset: 0
1415
+ };
1416
+ const position = state.position;
1417
+ if (typeof position !== "number" || !Number.isFinite(position)) return {
1418
+ position: itemCount - 1,
1419
+ offset: normalizeOffset(state.offset)
1420
+ };
1421
+ return {
1422
+ position: clamp$2(Math.trunc(position), 0, itemCount - 1),
1423
+ offset: normalizeOffset(state.offset)
1424
+ };
1425
+ }
1426
+ function resolveTimelineVisibleWindow(items, state, viewportHeight, resolveItem) {
1427
+ const normalizedState = normalizeTimelineState(items.length, state);
1428
+ if (items.length === 0) return {
1429
+ normalizedState,
1430
+ window: {
1431
+ drawList: [],
1432
+ shift: 0
1433
+ }
1434
+ };
1435
+ let { position, offset } = normalizedState;
1436
+ let drawLength = 0;
1437
+ if (offset > 0) if (position === 0) offset = 0;
1438
+ else {
1439
+ for (let i = position - 1; i >= 0; i -= 1) {
1440
+ const { height } = resolveItem(items[i], i);
1441
+ position = i;
1442
+ offset -= height;
1443
+ if (offset <= 0) break;
1444
+ }
1445
+ if (position === 0 && offset > 0) offset = 0;
1446
+ }
1447
+ let y = offset;
1448
+ const drawList = [];
1449
+ for (let i = position; i < items.length; i += 1) {
1450
+ const { value, height } = resolveItem(items[i], i);
1451
+ if (y + height > 0) {
1452
+ drawList.push({
1453
+ idx: i,
1454
+ value,
1455
+ offset: y,
1456
+ height
1457
+ });
1458
+ drawLength += height;
1459
+ } else {
1460
+ offset += height;
1461
+ position = i + 1;
1462
+ }
1463
+ y += height;
1464
+ if (y >= viewportHeight) break;
1465
+ }
1466
+ let shift = 0;
1467
+ if (y < viewportHeight) if (position === 0 && drawLength < viewportHeight) {
1468
+ shift = -offset;
1469
+ offset = 0;
1470
+ } else {
1471
+ shift = viewportHeight - y;
1472
+ y = offset += shift;
1473
+ let lastIdx = -1;
1474
+ for (let i = position - 1; i >= 0; i -= 1) {
1475
+ const { value, height } = resolveItem(items[i], i);
1476
+ drawLength += height;
1477
+ y -= height;
1478
+ drawList.push({
1479
+ idx: i,
1480
+ value,
1481
+ offset: y - shift,
1482
+ height
1483
+ });
1484
+ lastIdx = i;
1485
+ if (y < 0) break;
1486
+ }
1487
+ if (lastIdx === 0 && drawLength < viewportHeight) {
1488
+ shift = drawList.at(-1)?.offset == null ? 0 : -drawList.at(-1).offset;
1489
+ position = 0;
1490
+ offset = 0;
1491
+ }
1492
+ }
1493
+ return {
1494
+ normalizedState: {
1495
+ position,
1496
+ offset
1497
+ },
1498
+ window: {
1499
+ drawList,
1500
+ shift
1501
+ }
1502
+ };
1503
+ }
1504
+ function resolveChatVisibleWindow(items, state, viewportHeight, resolveItem) {
1505
+ const normalizedState = normalizeChatState(items.length, state);
1506
+ if (items.length === 0) return {
1507
+ normalizedState,
1508
+ window: {
1509
+ drawList: [],
1510
+ shift: 0
1511
+ }
1512
+ };
1513
+ let { position, offset } = normalizedState;
1514
+ let drawLength = 0;
1515
+ if (offset < 0) if (position === items.length - 1) offset = 0;
1516
+ else for (let i = position + 1; i < items.length; i += 1) {
1517
+ const { height } = resolveItem(items[i], i);
1518
+ position = i;
1519
+ offset += height;
1520
+ if (offset > 0) break;
1521
+ }
1522
+ let y = viewportHeight + offset;
1523
+ const drawList = [];
1524
+ for (let i = position; i >= 0; i -= 1) {
1525
+ const { value, height } = resolveItem(items[i], i);
1526
+ y -= height;
1527
+ if (y <= viewportHeight) {
1528
+ drawList.push({
1529
+ idx: i,
1530
+ value,
1531
+ offset: y,
1532
+ height
1533
+ });
1534
+ drawLength += height;
1535
+ } else {
1536
+ offset -= height;
1537
+ position = i - 1;
1538
+ }
1539
+ if (y < 0) break;
1540
+ }
1541
+ let shift = 0;
1542
+ if (y > 0) {
1543
+ shift = -y;
1544
+ if (drawLength < viewportHeight) {
1545
+ y = drawLength;
1546
+ for (let i = position + 1; i < items.length; i += 1) {
1547
+ const { value, height } = resolveItem(items[i], i);
1548
+ drawList.push({
1549
+ idx: i,
1550
+ value,
1551
+ offset: y - shift,
1552
+ height
1553
+ });
1554
+ y = drawLength += height;
1555
+ position = i;
1556
+ if (y >= viewportHeight) break;
1557
+ }
1558
+ offset = drawLength < viewportHeight ? 0 : drawLength - viewportHeight;
1559
+ } else offset = drawLength - viewportHeight;
1560
+ }
1561
+ return {
1562
+ normalizedState: {
1563
+ position,
1564
+ offset
1565
+ },
1566
+ window: {
1567
+ drawList,
1568
+ shift
757
1569
  }
758
- this.position = this._clampItemIndex(this.position);
759
- if (!Number.isFinite(this.offset)) this.offset = 0;
1570
+ };
1571
+ }
1572
+ //#endregion
1573
+ //#region src/renderer/virtualized/chat.ts
1574
+ function clamp$1(value, min, max) {
1575
+ return Math.min(Math.max(value, min), max);
1576
+ }
1577
+ var ChatRenderer = class extends VirtualizedRenderer {
1578
+ #resolveVisibleWindow() {
1579
+ return resolveChatVisibleWindow(this.items, this._readListState(), this.graphics.canvas.clientHeight, (item) => {
1580
+ const node = this.options.renderItem(item);
1581
+ return {
1582
+ value: node,
1583
+ height: this.measureRootNode(node).height
1584
+ };
1585
+ });
1586
+ }
1587
+ _getDefaultJumpBlock() {
1588
+ return "end";
760
1589
  }
761
- _readAnchor() {
762
- this._prepareAnchorState();
1590
+ _normalizeListState(state) {
1591
+ return normalizeChatState(this.items.length, state);
1592
+ }
1593
+ _readAnchor(state) {
763
1594
  if (this.items.length === 0) return 0;
764
- const height = this._getItemHeight(this.position);
765
- return height > 0 ? this.position - this.offset / height : this.position;
1595
+ const height = this._getItemHeight(state.position);
1596
+ return height > 0 ? state.position + 1 - state.offset / height : state.position + 1;
766
1597
  }
767
1598
  _applyAnchor(anchor) {
768
1599
  if (this.items.length === 0) return;
769
- const clampedAnchor = clamp(anchor, 0, this.items.length);
770
- const position = clamp(Math.floor(clampedAnchor), 0, this.items.length - 1);
1600
+ const clampedAnchor = clamp$1(anchor, 0, this.items.length);
1601
+ const position = clamp$1(Math.ceil(clampedAnchor) - 1, 0, this.items.length - 1);
771
1602
  const height = this._getItemHeight(position);
772
- this.position = position;
773
- const offset = height > 0 ? -(clampedAnchor - position) * height : 0;
774
- this.offset = Object.is(offset, -0) ? 0 : offset;
1603
+ const offset = height > 0 ? (position + 1 - clampedAnchor) * height : 0;
1604
+ this._commitListState({
1605
+ position,
1606
+ offset: Object.is(offset, -0) ? 0 : offset
1607
+ });
775
1608
  }
776
- _getTargetAnchor(index) {
777
- return index;
1609
+ _getTargetAnchor(index, block) {
1610
+ const height = this._getItemHeight(index);
1611
+ const viewportHeight = this.graphics.canvas.clientHeight;
1612
+ switch (block) {
1613
+ case "start": return this._getAnchorAtOffset(index, viewportHeight);
1614
+ case "center": return this._getAnchorAtOffset(index, height / 2 + viewportHeight / 2);
1615
+ case "end": return this._getAnchorAtOffset(index, height);
1616
+ }
778
1617
  }
779
1618
  render(feedback) {
780
1619
  const keepAnimating = this._prepareRender();
781
1620
  const { clientWidth: viewportWidth, clientHeight: viewportHeight } = this.graphics.canvas;
782
1621
  this.graphics.clearRect(0, 0, viewportWidth, viewportHeight);
783
- this._resetRenderFeedback(feedback);
784
- let drawLength = 0;
785
- if (Number.isNaN(this.position)) this.position = 0;
786
- if (this.offset > 0) if (this.position === 0) this.offset = 0;
787
- else {
788
- for (let i = this.position - 1; i >= 0; i -= 1) {
789
- const item = this.items[i];
790
- const node = this.options.renderItem(item);
791
- const { height } = this.measureNode(node);
792
- this.position = i;
793
- this.offset -= height;
794
- if (this.offset <= 0) break;
795
- }
796
- if (this.position === 0 && this.offset > 0) this.offset = 0;
797
- }
798
- let y = this.offset;
799
- const drawList = [];
800
- for (let i = this.position; i < this.items.length; i += 1) {
801
- const item = this.items[i];
802
- const node = this.options.renderItem(item);
803
- const { height } = this.measureNode(node);
804
- if (y + height > 0) {
805
- drawList.push({
806
- idx: i,
807
- node,
808
- offset: y,
809
- height
810
- });
811
- drawLength += height;
812
- } else {
813
- this.offset += height;
814
- this.position = i + 1;
815
- }
816
- y += height;
817
- if (y >= viewportHeight) break;
818
- }
819
- let shift = 0;
820
- if (y < viewportHeight) if (this.position === 0 && drawLength < viewportHeight) {
821
- shift = -this.offset;
822
- this.offset = 0;
823
- } else {
824
- shift = viewportHeight - y;
825
- y = this.offset += shift;
826
- let lastIdx = -1;
827
- for (let i = this.position - 1; i >= 0; i -= 1) {
828
- const item = this.items[lastIdx = i];
829
- const node = this.options.renderItem(item);
830
- const { height } = this.measureNode(node);
831
- drawLength += height;
832
- y -= height;
833
- drawList.push({
834
- idx: i,
835
- node,
836
- offset: y - shift,
837
- height
838
- });
839
- if (y < 0) break;
840
- }
841
- if (lastIdx === 0 && drawLength < viewportHeight) {
842
- shift = -drawList[drawList.length - 1].offset;
843
- this.position = 0;
844
- this.offset = 0;
845
- }
846
- }
847
- const requestRedraw = this._renderDrawList(drawList, shift, feedback);
1622
+ const solution = this.#resolveVisibleWindow();
1623
+ const requestRedraw = this._renderVisibleWindow(solution.window, feedback);
1624
+ this._commitListState(solution.normalizedState);
848
1625
  return this._finishRender(keepAnimating || requestRedraw);
849
1626
  }
850
1627
  hittest(test) {
851
- const viewportHeight = this.graphics.canvas.clientHeight;
852
- let y = this.offset;
853
- for (let i = this.position; i < this.items.length; i += 1) {
854
- const item = this.items[i];
855
- const node = this.options.renderItem(item);
856
- const { height } = this.measureNode(node);
857
- if (test.y < y + height) return node.hittest(this.context, shallowMerge(test, { y: test.y - y }));
858
- y += height;
859
- if (y >= viewportHeight) break;
860
- }
861
- return false;
1628
+ return this._hittestVisibleWindow(this.#resolveVisibleWindow().window, test);
862
1629
  }
863
1630
  };
864
- var ChatRenderer = class extends VirtualizedRenderer {
865
- _prepareAnchorState() {
866
- if (this.items.length === 0) return;
867
- if (!Number.isFinite(this.position)) {
868
- this.position = this.items.length - 1;
869
- this.offset = 0;
870
- return;
871
- }
872
- this.position = this._clampItemIndex(this.position);
873
- if (!Number.isFinite(this.offset)) this.offset = 0;
1631
+ //#endregion
1632
+ //#region src/renderer/virtualized/timeline.ts
1633
+ function clamp(value, min, max) {
1634
+ return Math.min(Math.max(value, min), max);
1635
+ }
1636
+ var TimelineRenderer = class extends VirtualizedRenderer {
1637
+ #resolveVisibleWindow() {
1638
+ return resolveTimelineVisibleWindow(this.items, this._readListState(), this.graphics.canvas.clientHeight, (item) => {
1639
+ const node = this.options.renderItem(item);
1640
+ return {
1641
+ value: node,
1642
+ height: this.measureRootNode(node).height
1643
+ };
1644
+ });
874
1645
  }
875
- _readAnchor() {
876
- this._prepareAnchorState();
1646
+ _getDefaultJumpBlock() {
1647
+ return "start";
1648
+ }
1649
+ _normalizeListState(state) {
1650
+ return normalizeTimelineState(this.items.length, state);
1651
+ }
1652
+ _readAnchor(state) {
877
1653
  if (this.items.length === 0) return 0;
878
- const height = this._getItemHeight(this.position);
879
- return height > 0 ? this.position + 1 - this.offset / height : this.position + 1;
1654
+ const height = this._getItemHeight(state.position);
1655
+ return height > 0 ? state.position - state.offset / height : state.position;
880
1656
  }
881
1657
  _applyAnchor(anchor) {
882
1658
  if (this.items.length === 0) return;
883
1659
  const clampedAnchor = clamp(anchor, 0, this.items.length);
884
- const position = clamp(Math.ceil(clampedAnchor) - 1, 0, this.items.length - 1);
1660
+ const position = clamp(Math.floor(clampedAnchor), 0, this.items.length - 1);
885
1661
  const height = this._getItemHeight(position);
886
- this.position = position;
887
- const offset = height > 0 ? (position + 1 - clampedAnchor) * height : 0;
888
- this.offset = Object.is(offset, -0) ? 0 : offset;
1662
+ const offset = height > 0 ? -(clampedAnchor - position) * height : 0;
1663
+ this._commitListState({
1664
+ position,
1665
+ offset: Object.is(offset, -0) ? 0 : offset
1666
+ });
889
1667
  }
890
- _getTargetAnchor(index) {
891
- return index + 1;
1668
+ _getTargetAnchor(index, block) {
1669
+ const height = this._getItemHeight(index);
1670
+ const viewportHeight = this.graphics.canvas.clientHeight;
1671
+ switch (block) {
1672
+ case "start": return this._getAnchorAtOffset(index, 0);
1673
+ case "center": return this._getAnchorAtOffset(index, height / 2 - viewportHeight / 2);
1674
+ case "end": return this._getAnchorAtOffset(index, height - viewportHeight);
1675
+ }
892
1676
  }
893
1677
  render(feedback) {
894
1678
  const keepAnimating = this._prepareRender();
895
1679
  const { clientWidth: viewportWidth, clientHeight: viewportHeight } = this.graphics.canvas;
896
1680
  this.graphics.clearRect(0, 0, viewportWidth, viewportHeight);
897
- this._resetRenderFeedback(feedback);
898
- let drawLength = 0;
899
- if (Number.isNaN(this.position)) this.position = this.items.length - 1;
900
- if (this.offset < 0) if (this.position === this.items.length - 1) this.offset = 0;
901
- else for (let i = this.position + 1; i < this.items.length; i += 1) {
902
- const item = this.items[i];
903
- const node = this.options.renderItem(item);
904
- const { height } = this.measureNode(node);
905
- this.position = i;
906
- this.offset += height;
907
- if (this.offset > 0) break;
908
- }
909
- let y = viewportHeight + this.offset;
910
- const drawList = [];
911
- for (let i = this.position; i >= 0; i -= 1) {
912
- const item = this.items[i];
913
- const node = this.options.renderItem(item);
914
- const { height } = this.measureNode(node);
915
- y -= height;
916
- if (y <= viewportHeight) {
917
- drawList.push({
918
- idx: i,
919
- node,
920
- offset: y,
921
- height
922
- });
923
- drawLength += height;
924
- } else {
925
- this.offset -= height;
926
- this.position = i - 1;
927
- }
928
- if (y < 0) break;
929
- }
930
- let shift = 0;
931
- if (y > 0) {
932
- shift = -y;
933
- if (drawLength < viewportHeight) {
934
- y = drawLength;
935
- for (let i = this.position + 1; i < this.items.length; i += 1) {
936
- const item = this.items[i];
937
- const node = this.options.renderItem(item);
938
- const { height } = this.measureNode(node);
939
- drawList.push({
940
- idx: i,
941
- node,
942
- offset: y - shift,
943
- height
944
- });
945
- y = drawLength += height;
946
- this.position = i;
947
- if (y >= viewportHeight) break;
948
- }
949
- if (drawLength < viewportHeight) this.offset = 0;
950
- else this.offset = drawLength - viewportHeight;
951
- } else this.offset = drawLength - viewportHeight;
952
- }
953
- const requestRedraw = this._renderDrawList(drawList, shift, feedback);
1681
+ const solution = this.#resolveVisibleWindow();
1682
+ const requestRedraw = this._renderVisibleWindow(solution.window, feedback);
1683
+ this._commitListState(solution.normalizedState);
954
1684
  return this._finishRender(keepAnimating || requestRedraw);
955
1685
  }
956
1686
  hittest(test) {
957
- const viewportHeight = this.graphics.canvas.clientHeight;
958
- let drawLength = 0;
959
- const heights = [];
960
- for (let i = this.position; i >= 0; i -= 1) {
961
- const item = this.items[i];
962
- const node = this.options.renderItem(item);
963
- const { height } = this.measureNode(node);
964
- drawLength += height;
965
- heights.push([node, height]);
966
- }
967
- let y = drawLength < viewportHeight ? drawLength : viewportHeight + this.offset;
968
- if (test.y > y) return false;
969
- for (const [node, height] of heights) {
970
- y -= height;
971
- if (test.y > y) return node.hittest(this.context, shallowMerge(test, { y: test.y - y }));
972
- }
973
- return false;
1687
+ return this._hittestVisibleWindow(this.#resolveVisibleWindow().window, test);
974
1688
  }
975
1689
  };
976
1690
  //#endregion
977
- export { AlignBox, BaseRenderer, ChatRenderer, DebugRenderer, Fixed, Group, HStack, ListState, MultilineText, PaddingBox, Text, TimelineRenderer, VStack, VirtualizedRenderer, Wrapper, getNodeParent, memoRenderItem, registerNodeParent, unregisterNodeParent };
1691
+ export { BaseRenderer, ChatRenderer, DebugRenderer, Fixed, Flex, FlexItem, Group, ListState, MultilineText, PaddingBox, Place, Text, TimelineRenderer, VirtualizedRenderer, Wrapper, memoRenderItem, memoRenderItemBy };
978
1692
 
979
1693
  //# sourceMappingURL=index.mjs.map