chat-layout 0.1.5 → 1.0.0-2

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,62 +1,54 @@
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
- };
11
+ function bumpRevision(node) {
12
+ revisions.set(node, (revisions.get(node) ?? 0) + 1);
13
+ }
14
+ function getNodeRevision(node) {
15
+ return revisions.get(node) ?? 0;
16
+ }
17
+ function attachNodeToParent(node, parent) {
18
+ if (registry.has(node)) throw getOwnershipError();
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);
29
+ }
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);
48
+ }
49
+ function forEachNodeAncestor(node, visitor) {
50
+ let current = node;
51
+ while (current = registry.get(current)) visitor(current);
60
52
  }
61
53
  //#endregion
62
54
  //#region src/utils.ts
@@ -70,218 +62,53 @@ function shallowMerge(object, other) {
70
62
  };
71
63
  }
72
64
  //#endregion
73
- //#region src/registry.ts
74
- const registry = /* @__PURE__ */ new WeakMap();
75
- function registerNodeParent(node, parent) {
76
- registry.set(node, parent);
65
+ //#region src/nodes/base.ts
66
+ function withNodeConstraints(ctx, constraints) {
67
+ if (constraints === ctx.constraints) return ctx;
68
+ const next = shallow(ctx);
69
+ next.constraints = constraints;
70
+ return next;
77
71
  }
78
- function unregisterNodeParent(node) {
79
- registry.delete(node);
72
+ function measureNodeMinContent(ctx, node, constraints = ctx.constraints) {
73
+ const nextCtx = withNodeConstraints(ctx, constraints);
74
+ if (node.measureMinContent != null) return node.measureMinContent(nextCtx);
75
+ return node.measure(nextCtx);
80
76
  }
81
- function getNodeParent(node) {
82
- return registry.get(node);
83
- }
84
- //#endregion
85
- //#region src/nodes.ts
86
77
  var Group = class {
78
+ #children;
87
79
  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;
80
+ this.#children = [...children];
81
+ replaceNodesParent([], this.#children, this);
164
82
  }
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
- };
192
- }
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;
83
+ get children() {
84
+ return this.#children;
224
85
  }
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;
86
+ replaceChildren(nextChildren) {
87
+ const nextSnapshot = [...nextChildren];
88
+ replaceNodesParent(this.#children, nextSnapshot, this);
89
+ this.#children = nextSnapshot;
262
90
  }
263
91
  };
264
92
  var Wrapper = class {
265
93
  #inner;
266
94
  constructor(inner) {
267
95
  this.#inner = inner;
268
- registerNodeParent(this.#inner, this);
96
+ attachNodeToParent(this.#inner, this);
269
97
  }
270
98
  get inner() {
271
99
  return this.#inner;
272
100
  }
273
101
  set inner(newNode) {
274
102
  if (newNode === this.#inner) return;
275
- unregisterNodeParent(this.#inner);
103
+ replaceNodeParent(this.#inner, newNode, this);
276
104
  this.#inner = newNode;
277
- registerNodeParent(newNode, this);
278
- }
279
- get flex() {
280
- return this.inner.flex;
281
105
  }
282
106
  measure(ctx) {
283
107
  return this.inner.measure(ctx);
284
108
  }
109
+ measureMinContent(ctx) {
110
+ return measureNodeMinContent(ctx, this.inner);
111
+ }
285
112
  draw(ctx, x, y) {
286
113
  return this.inner.draw(ctx, x, y);
287
114
  }
@@ -289,6 +116,117 @@ var Wrapper = class {
289
116
  return this.inner.hittest(ctx, test);
290
117
  }
291
118
  };
119
+ //#endregion
120
+ //#region src/layout.ts
121
+ /**
122
+ * 创建 LayoutRect 的辅助函数
123
+ */
124
+ function createRect(x, y, width, height) {
125
+ return {
126
+ x,
127
+ y,
128
+ width,
129
+ height
130
+ };
131
+ }
132
+ /**
133
+ * 合并多个 rect 得到包含所有 rect 的最小外接矩形
134
+ */
135
+ function mergeRects(rects) {
136
+ if (rects.length === 0) return createRect(0, 0, 0, 0);
137
+ let minX = Infinity;
138
+ let minY = Infinity;
139
+ let maxX = -Infinity;
140
+ let maxY = -Infinity;
141
+ for (const rect of rects) {
142
+ minX = Math.min(minX, rect.x);
143
+ minY = Math.min(minY, rect.y);
144
+ maxX = Math.max(maxX, rect.x + rect.width);
145
+ maxY = Math.max(maxY, rect.y + rect.height);
146
+ }
147
+ return createRect(minX, minY, maxX - minX, maxY - minY);
148
+ }
149
+ /**
150
+ * 从子节点布局结果计算容器的 contentBox
151
+ */
152
+ function computeContentBox(children) {
153
+ return mergeRects(children.map((child) => child.contentBox));
154
+ }
155
+ /**
156
+ * 检查点是否在 rect 内
157
+ */
158
+ function pointInRect(x, y, rect) {
159
+ return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height;
160
+ }
161
+ /**
162
+ * 读取单子节点布局结果中的唯一 child。
163
+ */
164
+ function getSingleChildLayout(layout) {
165
+ return layout.children[0];
166
+ }
167
+ /**
168
+ * 在布局结果中按指定盒模型查找命中的 child,并返回局部坐标。
169
+ */
170
+ function findChildAtPoint(children, x, y, box = "contentBox") {
171
+ for (let i = children.length - 1; i >= 0; i -= 1) {
172
+ const child = children[i];
173
+ const target = box === "rect" ? child.rect : child.contentBox;
174
+ if (!pointInRect(x, y, target)) continue;
175
+ return {
176
+ child,
177
+ localX: x - target.x,
178
+ localY: y - target.y
179
+ };
180
+ }
181
+ }
182
+ //#endregion
183
+ //#region src/nodes/shared.ts
184
+ function withConstraints(ctx, constraints) {
185
+ const next = shallow(ctx);
186
+ next.constraints = constraints;
187
+ return next;
188
+ }
189
+ function getLayoutContext(ctx) {
190
+ return ctx;
191
+ }
192
+ function readLayoutResult(node, ctx) {
193
+ return getLayoutContext(ctx).getLayoutResult(node, ctx.constraints);
194
+ }
195
+ function writeLayoutResult(node, ctx, result) {
196
+ getLayoutContext(ctx).setLayoutResult(node, result, ctx.constraints);
197
+ }
198
+ function ensureLayoutResult(node, ctx) {
199
+ return readLayoutResult(node, ctx);
200
+ }
201
+ function drawLayoutChildren(node, ctx, x, y) {
202
+ const layoutResult = ensureLayoutResult(node, ctx);
203
+ if (!layoutResult) return false;
204
+ let result = false;
205
+ for (const childResult of layoutResult.children) result ||= childResult.node.draw(withConstraints(ctx, childResult.constraints), x + childResult.contentBox.x, y + childResult.contentBox.y);
206
+ return result;
207
+ }
208
+ function hittestLayoutChildren(node, ctx, test, box = "contentBox") {
209
+ const layoutResult = ensureLayoutResult(node, ctx);
210
+ if (!layoutResult) return false;
211
+ const hit = findChildAtPoint(layoutResult.children, test.x, test.y, box);
212
+ if (!hit) return false;
213
+ return hit.child.node.hittest(withConstraints(ctx, hit.child.constraints), shallowMerge(test, {
214
+ x: hit.localX,
215
+ y: hit.localY
216
+ }));
217
+ }
218
+ //#endregion
219
+ //#region src/nodes/box.ts
220
+ function clampToConstraints$1(value, min, max) {
221
+ let result = value;
222
+ if (min != null) result = Math.max(result, min);
223
+ if (max != null) result = Math.min(result, max);
224
+ return result;
225
+ }
226
+ function shrinkConstraint(value, padding) {
227
+ if (value == null) return;
228
+ return Math.max(0, value - padding);
229
+ }
292
230
  var PaddingBox = class extends Wrapper {
293
231
  constructor(inner, padding = {}) {
294
232
  super(inner);
@@ -307,80 +245,831 @@ var PaddingBox = class extends Wrapper {
307
245
  return this.padding.right ?? 0;
308
246
  }
309
247
  measure(ctx) {
310
- ctx.remainingWidth -= this.#left + this.#right;
311
- const { width, height } = ctx.measureNode(this.inner);
248
+ const paddingLeft = this.#left;
249
+ const paddingRight = this.#right;
250
+ const paddingTop = this.#top;
251
+ const paddingBottom = this.#bottom;
252
+ const horizontalPadding = paddingLeft + paddingRight;
253
+ const verticalPadding = paddingTop + paddingBottom;
254
+ const childConstraints = ctx.constraints ? {
255
+ ...ctx.constraints,
256
+ minWidth: shrinkConstraint(ctx.constraints.minWidth, horizontalPadding),
257
+ maxWidth: shrinkConstraint(ctx.constraints.maxWidth, horizontalPadding),
258
+ minHeight: shrinkConstraint(ctx.constraints.minHeight, verticalPadding),
259
+ maxHeight: shrinkConstraint(ctx.constraints.maxHeight, verticalPadding)
260
+ } : void 0;
261
+ const { width, height } = ctx.measureNode(this.inner, childConstraints);
262
+ 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));
263
+ const childRect = createRect(paddingLeft, paddingTop, width, height);
264
+ writeLayoutResult(this, ctx, {
265
+ containerBox,
266
+ contentBox: childRect,
267
+ children: [{
268
+ node: this.inner,
269
+ rect: childRect,
270
+ contentBox: childRect,
271
+ constraints: childConstraints
272
+ }],
273
+ constraints: ctx.constraints
274
+ });
275
+ return {
276
+ width: containerBox.width,
277
+ height: containerBox.height
278
+ };
279
+ }
280
+ measureMinContent(ctx) {
281
+ const paddingLeft = this.#left;
282
+ const paddingRight = this.#right;
283
+ const paddingTop = this.#top;
284
+ const paddingBottom = this.#bottom;
285
+ const horizontalPadding = paddingLeft + paddingRight;
286
+ const verticalPadding = paddingTop + paddingBottom;
287
+ const childConstraints = ctx.constraints ? {
288
+ ...ctx.constraints,
289
+ minWidth: shrinkConstraint(ctx.constraints.minWidth, horizontalPadding),
290
+ maxWidth: shrinkConstraint(ctx.constraints.maxWidth, horizontalPadding),
291
+ minHeight: shrinkConstraint(ctx.constraints.minHeight, verticalPadding),
292
+ maxHeight: shrinkConstraint(ctx.constraints.maxHeight, verticalPadding)
293
+ } : void 0;
294
+ const { width, height } = measureNodeMinContent(ctx, this.inner, childConstraints);
312
295
  return {
313
- width: width + this.#left + this.#right,
314
- height: height + this.#top + this.#bottom
296
+ width: width + horizontalPadding,
297
+ height: height + verticalPadding
315
298
  };
316
299
  }
317
300
  draw(ctx, x, y) {
318
- ctx.remainingWidth -= this.#left + this.#right;
319
- return this.inner.draw(ctx, x + this.#left, y + this.#top);
301
+ const layoutResult = readLayoutResult(this, ctx);
302
+ if (!layoutResult) return this.inner.draw(ctx, x + this.#left, y + this.#top);
303
+ const childResult = getSingleChildLayout(layoutResult);
304
+ if (!childResult) return false;
305
+ return childResult.node.draw(withConstraints(ctx, childResult.constraints), x + childResult.rect.x, y + childResult.rect.y);
320
306
  }
321
307
  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
308
+ const layoutResult = readLayoutResult(this, ctx);
309
+ if (!layoutResult) return false;
310
+ const hit = findChildAtPoint(layoutResult.children, test.x, test.y, "rect");
311
+ if (!hit) return false;
312
+ return hit.child.node.hittest(withConstraints(ctx, hit.child.constraints), shallowMerge(test, {
313
+ x: hit.localX,
314
+ y: hit.localY
327
315
  }));
316
+ }
317
+ };
318
+ var Fixed = class {
319
+ constructor(width, height) {
320
+ this.width = width;
321
+ this.height = height;
322
+ }
323
+ measure(_ctx) {
324
+ return {
325
+ width: this.width,
326
+ height: this.height
327
+ };
328
+ }
329
+ measureMinContent(_ctx) {
330
+ return {
331
+ width: this.width,
332
+ height: this.height
333
+ };
334
+ }
335
+ draw(_ctx, _x, _y) {
336
+ return false;
337
+ }
338
+ hittest(_ctx, _test) {
328
339
  return false;
329
340
  }
330
341
  };
331
- var AlignBox = class extends Wrapper {
332
- #shift = 0;
333
- constructor(inner, options) {
342
+ //#endregion
343
+ //#region src/nodes/flex.ts
344
+ function getMainSize(axis, box) {
345
+ return axis === "row" ? box.width : box.height;
346
+ }
347
+ function getCrossSize(axis, box) {
348
+ return axis === "row" ? box.height : box.width;
349
+ }
350
+ function getMinMain(axis, constraints) {
351
+ return axis === "row" ? constraints?.minWidth : constraints?.minHeight;
352
+ }
353
+ function getMaxMain(axis, constraints) {
354
+ return axis === "row" ? constraints?.maxWidth : constraints?.maxHeight;
355
+ }
356
+ function getMinCross(axis, constraints) {
357
+ return axis === "row" ? constraints?.minHeight : constraints?.minWidth;
358
+ }
359
+ function getMaxCross(axis, constraints) {
360
+ return axis === "row" ? constraints?.maxHeight : constraints?.maxWidth;
361
+ }
362
+ function createAxisConstraints(axis, constraints, main, cross = {}) {
363
+ if (constraints == null && main.min == null && main.max == null && cross.min == null && cross.max == null) return;
364
+ const next = { ...constraints };
365
+ if (axis === "row") {
366
+ next.minWidth = main.min;
367
+ next.maxWidth = main.max;
368
+ next.minHeight = cross.min;
369
+ next.maxHeight = cross.max;
370
+ } else {
371
+ next.minHeight = main.min;
372
+ next.maxHeight = main.max;
373
+ next.minWidth = cross.min;
374
+ next.maxWidth = cross.max;
375
+ }
376
+ return next;
377
+ }
378
+ function clampToConstraints(value, min, max) {
379
+ let result = value;
380
+ if (min != null) result = Math.max(result, min);
381
+ if (max != null) result = Math.min(result, max);
382
+ return result;
383
+ }
384
+ function constraintsEqual(left, right) {
385
+ if (left === right) return true;
386
+ if (left == null || right == null) return left == null && right == null;
387
+ return left.minWidth === right.minWidth && left.maxWidth === right.maxWidth && left.minHeight === right.minHeight && left.maxHeight === right.maxHeight;
388
+ }
389
+ function getCrossAlignment(alignSelf, alignItems) {
390
+ if (alignSelf == null || alignSelf === "auto") return alignItems;
391
+ return alignSelf;
392
+ }
393
+ function getJustifySpacing(justifyContent, freeSpace, itemCount, gap) {
394
+ switch (justifyContent) {
395
+ case "center": return {
396
+ leading: freeSpace / 2,
397
+ between: gap
398
+ };
399
+ case "end": return {
400
+ leading: freeSpace,
401
+ between: gap
402
+ };
403
+ case "space-between": return {
404
+ leading: 0,
405
+ between: itemCount > 1 ? gap + freeSpace / (itemCount - 1) : gap
406
+ };
407
+ case "space-around": return {
408
+ leading: itemCount > 0 ? freeSpace / itemCount / 2 : 0,
409
+ between: itemCount > 0 ? gap + freeSpace / itemCount : gap
410
+ };
411
+ case "space-evenly": return {
412
+ leading: itemCount > 0 ? freeSpace / (itemCount + 1) : 0,
413
+ between: itemCount > 0 ? gap + freeSpace / (itemCount + 1) : gap
414
+ };
415
+ default: return {
416
+ leading: 0,
417
+ between: gap
418
+ };
419
+ }
420
+ }
421
+ function getCrossOffset(align, frameCross, contentCross) {
422
+ switch (align) {
423
+ case "center": return (frameCross - contentCross) / 2;
424
+ case "end": return frameCross - contentCross;
425
+ default: return 0;
426
+ }
427
+ }
428
+ function createRectFromAxis(axis, main, cross, mainSize, crossSize) {
429
+ return axis === "row" ? createRect(main, cross, mainSize, crossSize) : createRect(cross, main, crossSize, mainSize);
430
+ }
431
+ const SHRINK_EPSILON = 1e-6;
432
+ function readFlexItemOptions(child) {
433
+ if (child instanceof FlexItem) return child.item;
434
+ return {};
435
+ }
436
+ function computeFlexLayout(children, options, constraints, measureChild, measureChildMinContent) {
437
+ const axis = options.direction ?? "row";
438
+ const gap = options.gap ?? 0;
439
+ const justifyContent = options.justifyContent ?? "start";
440
+ const alignItems = options.alignItems ?? "start";
441
+ const reverse = options.reverse ?? false;
442
+ const mainAxisSize = options.mainAxisSize ?? "fill";
443
+ const orderedChildren = reverse ? [...children].reverse() : children;
444
+ const maxMain = getMaxMain(axis, constraints);
445
+ const minMain = getMinMain(axis, constraints);
446
+ const maxCross = getMaxCross(axis, constraints);
447
+ const minCross = getMinCross(axis, constraints);
448
+ const gapTotal = orderedChildren.length > 1 ? gap * (orderedChildren.length - 1) : 0;
449
+ const finiteMain = maxMain != null;
450
+ const finiteCross = maxCross != null;
451
+ const availableMain = finiteMain ? Math.max(0, maxMain - gapTotal) : void 0;
452
+ let totalGrow = 0;
453
+ let totalBasis = 0;
454
+ let nonGrowBasis = 0;
455
+ const measurements = /* @__PURE__ */ new Map();
456
+ const basisConstraints = createAxisConstraints(axis, constraints, {
457
+ min: void 0,
458
+ max: void 0
459
+ }, {
460
+ min: void 0,
461
+ max: maxCross
462
+ });
463
+ for (const child of orderedChildren) {
464
+ const item = readFlexItemOptions(child);
465
+ const grow = item.grow ?? 0;
466
+ const shrink = item.shrink ?? 0;
467
+ totalGrow += grow;
468
+ const effectiveAlign = getCrossAlignment(item.alignSelf, alignItems);
469
+ const stretch = effectiveAlign === "stretch";
470
+ const basisMeasured = measureChild(child, basisConstraints);
471
+ const basis = getMainSize(axis, basisMeasured);
472
+ totalBasis += basis;
473
+ if (grow <= 0) nonGrowBasis += basis;
474
+ measurements.set(child, {
475
+ child,
476
+ item,
477
+ basisMeasured,
478
+ measured: basisMeasured,
479
+ basisConstraints,
480
+ initialConstraints: basisConstraints,
481
+ finalConstraints: basisConstraints,
482
+ allocatedMain: void 0,
483
+ grow,
484
+ shrink,
485
+ effectiveAlign,
486
+ stretch,
487
+ basis,
488
+ minContentMain: basis,
489
+ finalMain: basis,
490
+ frozen: false,
491
+ frameMain: basis,
492
+ frameCross: getCrossSize(axis, basisMeasured)
493
+ });
494
+ }
495
+ if (finiteMain && availableMain != null && totalBasis - availableMain > SHRINK_EPSILON) {
496
+ const totalDeficit = totalBasis - availableMain;
497
+ let remainingDeficit = totalDeficit;
498
+ for (const child of orderedChildren) {
499
+ const measurement = measurements.get(child);
500
+ const minContentMeasured = measureChildMinContent(child, measurement.basisConstraints);
501
+ measurement.minContentMain = Math.min(measurement.basis, getMainSize(axis, minContentMeasured));
502
+ measurement.finalMain = measurement.basis;
503
+ measurement.frozen = measurement.shrink <= 0 || measurement.basis - measurement.minContentMain <= SHRINK_EPSILON;
504
+ }
505
+ while (remainingDeficit > SHRINK_EPSILON) {
506
+ const active = orderedChildren.map((child) => measurements.get(child)).filter((measurement) => !measurement.frozen && measurement.shrink > 0);
507
+ const totalScaled = active.reduce((sum, measurement) => sum + measurement.shrink * measurement.basis, 0);
508
+ if (active.length === 0 || totalScaled <= SHRINK_EPSILON) break;
509
+ let frozeAny = false;
510
+ for (const measurement of active) {
511
+ const tentative = measurement.basis - remainingDeficit * (measurement.shrink * measurement.basis / totalScaled);
512
+ if (tentative <= measurement.minContentMain + SHRINK_EPSILON) {
513
+ measurement.finalMain = measurement.minContentMain;
514
+ measurement.frozen = true;
515
+ frozeAny = true;
516
+ } else measurement.finalMain = tentative;
517
+ }
518
+ if (!frozeAny) {
519
+ remainingDeficit = 0;
520
+ break;
521
+ }
522
+ let absorbedDeficit = 0;
523
+ for (const child of orderedChildren) {
524
+ const measurement = measurements.get(child);
525
+ if (measurement.frozen) absorbedDeficit += Math.max(0, measurement.basis - measurement.finalMain);
526
+ }
527
+ remainingDeficit = Math.max(0, totalDeficit - absorbedDeficit);
528
+ }
529
+ for (const child of orderedChildren) {
530
+ const measurement = measurements.get(child);
531
+ measurement.measured = measurement.basisMeasured;
532
+ measurement.initialConstraints = measurement.basisConstraints;
533
+ measurement.finalConstraints = createAxisConstraints(axis, constraints, {
534
+ min: void 0,
535
+ max: measurement.finalMain
536
+ }, {
537
+ min: void 0,
538
+ max: maxCross
539
+ });
540
+ measurement.allocatedMain = void 0;
541
+ measurement.frameMain = measurement.finalMain;
542
+ measurement.frameCross = getCrossSize(axis, measurement.measured);
543
+ }
544
+ } else {
545
+ const remainingMain = finiteMain && availableMain != null ? Math.max(0, availableMain - nonGrowBasis) : void 0;
546
+ for (const child of orderedChildren) {
547
+ const measurement = measurements.get(child);
548
+ if (!(measurement.grow > 0 && finiteMain && remainingMain != null && totalGrow > 0)) {
549
+ measurement.measured = measurement.basisMeasured;
550
+ measurement.initialConstraints = measurement.basisConstraints;
551
+ measurement.finalConstraints = finiteMain ? createAxisConstraints(axis, constraints, {
552
+ min: void 0,
553
+ max: measurement.finalMain
554
+ }, {
555
+ min: void 0,
556
+ max: maxCross
557
+ }) : measurement.basisConstraints;
558
+ measurement.allocatedMain = void 0;
559
+ measurement.finalMain = measurement.basis;
560
+ measurement.frameMain = measurement.basis;
561
+ measurement.frameCross = getCrossSize(axis, measurement.measured);
562
+ continue;
563
+ }
564
+ const allocatedMain = remainingMain * measurement.grow / totalGrow;
565
+ const childConstraints = createAxisConstraints(axis, constraints, { max: allocatedMain }, {
566
+ min: void 0,
567
+ max: maxCross
568
+ });
569
+ const measured = measureChild(child, childConstraints);
570
+ measurement.measured = measured;
571
+ measurement.initialConstraints = childConstraints;
572
+ measurement.finalConstraints = childConstraints;
573
+ measurement.allocatedMain = allocatedMain;
574
+ measurement.finalMain = allocatedMain;
575
+ measurement.frameMain = allocatedMain;
576
+ measurement.frameCross = getCrossSize(axis, measured);
577
+ }
578
+ }
579
+ for (const child of orderedChildren) {
580
+ const measurement = measurements.get(child);
581
+ if (!constraintsEqual(measurement.initialConstraints, measurement.finalConstraints)) measurement.measured = measureChild(child, measurement.finalConstraints);
582
+ measurement.frameMain = measurement.finalMain;
583
+ measurement.frameCross = getCrossSize(axis, measurement.measured);
584
+ }
585
+ let contentMain = gapTotal;
586
+ let contentCross = 0;
587
+ for (const child of orderedChildren) {
588
+ const measurement = measurements.get(child);
589
+ contentMain += measurement.frameMain;
590
+ contentCross = Math.max(contentCross, measurement.frameCross);
591
+ }
592
+ finiteMain && mainAxisSize === "fill" || clampToConstraints(contentMain, minMain, maxMain);
593
+ const containerCross = clampToConstraints(contentCross, minCross, maxCross);
594
+ if (finiteCross) {
595
+ for (const child of orderedChildren) {
596
+ const measurement = measurements.get(child);
597
+ if (!measurement.stretch) continue;
598
+ const finalConstraints = createAxisConstraints(axis, measurement.finalConstraints, {
599
+ min: getMinMain(axis, measurement.finalConstraints),
600
+ max: getMaxMain(axis, measurement.finalConstraints)
601
+ }, {
602
+ min: containerCross,
603
+ max: containerCross
604
+ });
605
+ const remeasured = measureChild(child, finalConstraints);
606
+ measurement.measured = remeasured;
607
+ measurement.finalConstraints = finalConstraints;
608
+ measurement.frameCross = containerCross;
609
+ measurement.frameMain = measurement.allocatedMain ?? getMainSize(axis, remeasured);
610
+ }
611
+ contentMain = gapTotal;
612
+ contentCross = 0;
613
+ for (const child of orderedChildren) {
614
+ const measurement = measurements.get(child);
615
+ contentMain += measurement.frameMain;
616
+ contentCross = Math.max(contentCross, getCrossSize(axis, measurement.measured));
617
+ }
618
+ }
619
+ const finalContainerMain = finiteMain && mainAxisSize === "fill" ? Math.max(maxMain, contentMain) : clampToConstraints(contentMain, minMain, maxMain);
620
+ const spacing = getJustifySpacing(justifyContent, Math.max(0, finalContainerMain - contentMain), orderedChildren.length, gap);
621
+ const childResults = [];
622
+ let cursor = spacing.leading;
623
+ for (const child of orderedChildren) {
624
+ const measurement = measurements.get(child);
625
+ const frameCross = measurement.stretch && finiteCross ? containerCross : measurement.frameCross;
626
+ const contentMainSize = getMainSize(axis, measurement.measured);
627
+ const contentCrossSize = getCrossSize(axis, measurement.measured);
628
+ const rectCross = measurement.stretch ? 0 : getCrossOffset(measurement.effectiveAlign, containerCross, frameCross);
629
+ const contentCrossOffset = rectCross + getCrossOffset(measurement.effectiveAlign, frameCross, contentCrossSize);
630
+ const rect = createRectFromAxis(axis, cursor, rectCross, measurement.frameMain, frameCross);
631
+ const contentBox = createRectFromAxis(axis, cursor, contentCrossOffset, contentMainSize, contentCrossSize);
632
+ childResults.push({
633
+ node: child,
634
+ rect,
635
+ contentBox,
636
+ constraints: measurement.finalConstraints
637
+ });
638
+ cursor += measurement.frameMain + spacing.between;
639
+ }
640
+ const containerBox = axis === "row" ? createRect(0, 0, finalContainerMain, containerCross) : createRect(0, 0, containerCross, finalContainerMain);
641
+ const finalContentBox = childResults.length > 0 ? computeContentBox(childResults) : createRect(0, 0, 0, 0);
642
+ return {
643
+ box: {
644
+ width: containerBox.width,
645
+ height: containerBox.height
646
+ },
647
+ layout: {
648
+ containerBox,
649
+ contentBox: finalContentBox,
650
+ children: childResults,
651
+ constraints
652
+ }
653
+ };
654
+ }
655
+ var FlexItem = class extends Wrapper {
656
+ constructor(inner, item = {}) {
334
657
  super(inner);
658
+ this.item = item;
659
+ }
660
+ };
661
+ var Flex = class extends Group {
662
+ constructor(children, options = {}) {
663
+ super(children);
335
664
  this.options = options;
336
665
  }
337
666
  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;
667
+ const result = computeFlexLayout(this.children, this.options, ctx.constraints, (node, constraints) => ctx.measureNode(node, constraints), (node, constraints) => measureNodeMinContent(ctx, node, constraints));
668
+ writeLayoutResult(this, ctx, result.layout);
669
+ return result.box;
670
+ }
671
+ measureMinContent(ctx) {
672
+ const axis = this.options.direction ?? "row";
673
+ const gap = this.options.gap ?? 0;
674
+ const orderedChildren = this.options.reverse ? [...this.children].reverse() : this.children;
675
+ const gapTotal = orderedChildren.length > 1 ? gap * (orderedChildren.length - 1) : 0;
676
+ const childConstraints = createAxisConstraints(axis, ctx.constraints, {
677
+ min: void 0,
678
+ max: void 0
679
+ }, {
680
+ min: void 0,
681
+ max: getMaxCross(axis, ctx.constraints)
682
+ });
683
+ let width = axis === "row" ? gapTotal : 0;
684
+ let height = axis === "column" ? gapTotal : 0;
685
+ for (const child of orderedChildren) {
686
+ const measured = measureNodeMinContent(ctx, child, childConstraints);
687
+ if (axis === "row") {
688
+ width += measured.width;
689
+ height = Math.max(height, measured.height);
690
+ } else {
691
+ width = Math.max(width, measured.width);
692
+ height += measured.height;
693
+ }
348
694
  }
349
695
  return {
350
- width: ctx.remainingWidth,
696
+ width,
351
697
  height
352
698
  };
353
699
  }
354
700
  draw(ctx, x, y) {
355
- ctx.alignment = this.options.alignment;
356
- return this.inner.draw(ctx, x + this.#shift, y);
701
+ return drawLayoutChildren(this, ctx, x, y);
357
702
  }
358
703
  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;
704
+ return hittestLayoutChildren(this, ctx, test, "contentBox");
363
705
  }
364
706
  };
707
+ //#endregion
708
+ //#region src/nodes/place.ts
709
+ function resolveHorizontalOffset(align, availableWidth, childWidth) {
710
+ switch (align) {
711
+ case "center": return (availableWidth - childWidth) / 2;
712
+ case "end": return availableWidth - childWidth;
713
+ case "start": return 0;
714
+ }
715
+ }
716
+ var Place = class extends Wrapper {
717
+ constructor(inner, options = {}) {
718
+ super(inner);
719
+ this.options = options;
720
+ }
721
+ measure(ctx) {
722
+ const availableWidth = ctx.constraints?.maxWidth;
723
+ const expand = this.options.expand ?? true;
724
+ const childConstraints = ctx.constraints ? { ...ctx.constraints } : void 0;
725
+ const childBox = ctx.measureNode(this.inner, childConstraints);
726
+ let width = expand && availableWidth != null ? availableWidth : childBox.width;
727
+ if (ctx.constraints?.minWidth != null) width = Math.max(width, ctx.constraints.minWidth);
728
+ if (ctx.constraints?.maxWidth != null) width = Math.min(width, ctx.constraints.maxWidth);
729
+ const childRect = createRect(resolveHorizontalOffset(this.options.align ?? "start", width, childBox.width), 0, childBox.width, childBox.height);
730
+ writeLayoutResult(this, ctx, {
731
+ containerBox: createRect(0, 0, width, childBox.height),
732
+ contentBox: childRect,
733
+ children: [{
734
+ node: this.inner,
735
+ rect: childRect,
736
+ contentBox: createRect(0, 0, childBox.width, childBox.height),
737
+ constraints: childConstraints
738
+ }],
739
+ constraints: ctx.constraints
740
+ });
741
+ return {
742
+ width,
743
+ height: childBox.height
744
+ };
745
+ }
746
+ measureMinContent(ctx) {
747
+ return measureNodeMinContent(ctx, this.inner);
748
+ }
749
+ draw(ctx, x, y) {
750
+ const layoutResult = readLayoutResult(this, ctx);
751
+ if (!layoutResult) return this.inner.draw(ctx, x, y);
752
+ const childResult = getSingleChildLayout(layoutResult);
753
+ if (!childResult) return false;
754
+ const childCtx = withConstraints(ctx, childResult.constraints);
755
+ return childResult.node.draw(childCtx, x + childResult.rect.x, y + childResult.rect.y);
756
+ }
757
+ hittest(ctx, test) {
758
+ const layoutResult = readLayoutResult(this, ctx);
759
+ if (!layoutResult) return false;
760
+ const hit = findChildAtPoint(layoutResult.children, test.x, test.y, "rect");
761
+ if (!hit) return false;
762
+ return hit.child.node.hittest(withConstraints(ctx, hit.child.constraints), shallowMerge(test, {
763
+ x: hit.localX,
764
+ y: hit.localY
765
+ }));
766
+ }
767
+ };
768
+ //#endregion
769
+ //#region src/text.ts
770
+ const FONT_SHIFT_PROBE = "M";
771
+ const PREPARED_SEGMENT_CACHE_CAPACITY = 512;
772
+ const FONT_SHIFT_CACHE_CAPACITY = 64;
773
+ const LINE_START_CURSOR = {
774
+ segmentIndex: 0,
775
+ graphemeIndex: 0
776
+ };
777
+ const MIN_CONTENT_WIDTH_EPSILON = .001;
778
+ const preparedSegmentCache = /* @__PURE__ */ new Map();
779
+ const fontShiftCache = /* @__PURE__ */ new Map();
780
+ function preprocessSegments(text, whitespace = "preserve") {
781
+ const segments = text.split("\n");
782
+ if (whitespace === "trim-and-collapse") return segments.map((line) => line.trim()).filter((line) => line.length > 0);
783
+ return segments;
784
+ }
785
+ function readLruValue(cache, key) {
786
+ const cached = cache.get(key);
787
+ if (cached == null) return;
788
+ cache.delete(key);
789
+ cache.set(key, cached);
790
+ return cached;
791
+ }
792
+ function writeLruValue(cache, key, value, capacity) {
793
+ if (cache.has(key)) cache.delete(key);
794
+ else if (cache.size >= capacity) {
795
+ const firstKey = cache.keys().next().value;
796
+ if (firstKey != null) cache.delete(firstKey);
797
+ }
798
+ cache.set(key, value);
799
+ return value;
800
+ }
801
+ function getPreparedSegmentCacheKey(segment, font) {
802
+ return `${font}\u0000${segment}`;
803
+ }
804
+ function readPreparedSegment(segment, font) {
805
+ const key = getPreparedSegmentCacheKey(segment, font);
806
+ const cached = readLruValue(preparedSegmentCache, key);
807
+ if (cached != null) return cached;
808
+ return writeLruValue(preparedSegmentCache, key, prepareWithSegments(segment, font), PREPARED_SEGMENT_CACHE_CAPACITY);
809
+ }
810
+ function measureFontShift(ctx) {
811
+ const font = ctx.graphics.font;
812
+ const cached = readLruValue(fontShiftCache, font);
813
+ if (cached != null) return cached;
814
+ const { fontBoundingBoxAscent: ascent = 0, fontBoundingBoxDescent: descent = 0 } = ctx.graphics.measureText(FONT_SHIFT_PROBE);
815
+ return writeLruValue(fontShiftCache, font, ascent - descent, FONT_SHIFT_CACHE_CAPACITY);
816
+ }
817
+ function measurePreparedMinContentWidth(prepared) {
818
+ let maxWidth = 0;
819
+ let maxAnyWidth = 0;
820
+ for (let i = 0; i < prepared.widths.length; i += 1) {
821
+ const segmentWidth = prepared.widths[i] ?? 0;
822
+ maxAnyWidth = Math.max(maxAnyWidth, segmentWidth);
823
+ const segment = prepared.segments[i];
824
+ if (segment != null && segment.trim().length > 0) maxWidth = Math.max(maxWidth, segmentWidth);
825
+ }
826
+ return maxWidth > 0 ? maxWidth : maxAnyWidth;
827
+ }
828
+ function layoutFirstLineIntrinsic(ctx, text, whitespace = "preserve") {
829
+ const segment = preprocessSegments(text, whitespace)[0];
830
+ if (!segment) return {
831
+ width: 0,
832
+ text: "",
833
+ shift: 0
834
+ };
835
+ const shift = measureFontShift(ctx);
836
+ return {
837
+ width: ctx.graphics.measureText(segment).width,
838
+ text: segment,
839
+ shift
840
+ };
841
+ }
842
+ function measureTextIntrinsic(ctx, text, whitespace = "preserve") {
843
+ const segments = preprocessSegments(text, whitespace);
844
+ if (segments.length === 0) return {
845
+ width: 0,
846
+ lineCount: 0
847
+ };
848
+ let width = 0;
849
+ for (const segment of segments) width = Math.max(width, ctx.graphics.measureText(segment).width);
850
+ return {
851
+ width,
852
+ lineCount: segments.length
853
+ };
854
+ }
855
+ function layoutTextIntrinsic(ctx, text, whitespace = "preserve") {
856
+ const segments = preprocessSegments(text, whitespace);
857
+ if (segments.length === 0) return {
858
+ width: 0,
859
+ lines: []
860
+ };
861
+ const shift = measureFontShift(ctx);
862
+ let width = 0;
863
+ const lines = [];
864
+ for (const segment of segments) {
865
+ const measuredWidth = ctx.graphics.measureText(segment).width;
866
+ width = Math.max(width, measuredWidth);
867
+ lines.push({
868
+ width: measuredWidth,
869
+ text: segment,
870
+ shift
871
+ });
872
+ }
873
+ return {
874
+ width,
875
+ lines
876
+ };
877
+ }
878
+ function layoutFirstLine(ctx, text, maxWidth, whitespace = "preserve") {
879
+ if (maxWidth < 0) maxWidth = 0;
880
+ const segment = preprocessSegments(text, whitespace)[0];
881
+ if (!segment) return {
882
+ width: 0,
883
+ text: "",
884
+ shift: 0
885
+ };
886
+ const shift = measureFontShift(ctx);
887
+ if (maxWidth === 0) return {
888
+ width: 0,
889
+ text: "",
890
+ shift
891
+ };
892
+ const line = layoutNextLine(readPreparedSegment(segment, ctx.graphics.font), LINE_START_CURSOR, maxWidth);
893
+ if (line == null) return {
894
+ width: 0,
895
+ text: "",
896
+ shift
897
+ };
898
+ return {
899
+ width: line.width,
900
+ text: line.text,
901
+ shift
902
+ };
903
+ }
904
+ function measureText(ctx, text, maxWidth, whitespace = "preserve") {
905
+ if (maxWidth < 0) maxWidth = 0;
906
+ const segments = preprocessSegments(text, whitespace);
907
+ if (segments.length === 0 || maxWidth === 0) return {
908
+ width: 0,
909
+ lineCount: 0
910
+ };
911
+ const font = ctx.graphics.font;
912
+ let width = 0;
913
+ let lineCount = 0;
914
+ for (const segment of segments) {
915
+ if (segment.length === 0) {
916
+ lineCount += 1;
917
+ continue;
918
+ }
919
+ const prepared = readPreparedSegment(segment, font);
920
+ lineCount += walkLineRanges(prepared, maxWidth, (line) => {
921
+ width = Math.max(width, line.width);
922
+ });
923
+ }
924
+ return {
925
+ width,
926
+ lineCount
927
+ };
928
+ }
929
+ function measureTextMinContent(ctx, text, whitespace = "preserve") {
930
+ const segments = preprocessSegments(text, whitespace);
931
+ if (segments.length === 0) return {
932
+ width: 0,
933
+ lineCount: 0
934
+ };
935
+ const font = ctx.graphics.font;
936
+ let width = 0;
937
+ for (const segment of segments) {
938
+ if (segment.length === 0) continue;
939
+ const prepared = readPreparedSegment(segment, font);
940
+ width = Math.max(width, measurePreparedMinContentWidth(prepared));
941
+ }
942
+ let lineCount = 0;
943
+ const lineMaxWidth = Math.max(width, MIN_CONTENT_WIDTH_EPSILON);
944
+ for (const segment of segments) {
945
+ if (segment.length === 0) {
946
+ lineCount += 1;
947
+ continue;
948
+ }
949
+ const prepared = readPreparedSegment(segment, font);
950
+ lineCount += walkLineRanges(prepared, lineMaxWidth, () => {});
951
+ }
952
+ return {
953
+ width,
954
+ lineCount
955
+ };
956
+ }
957
+ function layoutText(ctx, text, maxWidth, whitespace = "preserve") {
958
+ if (maxWidth < 0) maxWidth = 0;
959
+ const segments = preprocessSegments(text, whitespace);
960
+ if (segments.length === 0 || maxWidth === 0) return {
961
+ width: 0,
962
+ lines: []
963
+ };
964
+ const font = ctx.graphics.font;
965
+ const shift = measureFontShift(ctx);
966
+ let width = 0;
967
+ const lines = [];
968
+ for (const segment of segments) {
969
+ if (segment.length === 0) {
970
+ lines.push({
971
+ width: 0,
972
+ text: "",
973
+ shift
974
+ });
975
+ continue;
976
+ }
977
+ const { lines: segLines } = layoutWithLines(readPreparedSegment(segment, font), maxWidth, 0);
978
+ for (const segLine of segLines) {
979
+ width = Math.max(width, segLine.width);
980
+ lines.push({
981
+ width: segLine.width,
982
+ text: segLine.text,
983
+ shift
984
+ });
985
+ }
986
+ }
987
+ return {
988
+ width,
989
+ lines
990
+ };
991
+ }
992
+ //#endregion
993
+ //#region src/nodes/text.ts
994
+ function resolvePhysicalTextAlign(options) {
995
+ if (options.physicalAlign != null) return options.physicalAlign;
996
+ if (options.align != null) switch (options.align) {
997
+ case "start": return "left";
998
+ case "center": return "center";
999
+ case "end": return "right";
1000
+ }
1001
+ return "left";
1002
+ }
1003
+ function normalizeTextMaxWidth(maxWidth) {
1004
+ if (maxWidth == null) return;
1005
+ return Math.max(0, maxWidth);
1006
+ }
1007
+ function getTextLayoutContext(ctx) {
1008
+ return ctx;
1009
+ }
1010
+ function readCachedTextLayout(node, ctx, key, compute) {
1011
+ const textCtx = getTextLayoutContext(ctx);
1012
+ const cached = textCtx.getTextLayout(node, key);
1013
+ if (cached != null) return cached;
1014
+ const layout = compute();
1015
+ textCtx.setTextLayout(node, key, layout);
1016
+ return layout;
1017
+ }
1018
+ function getSingleLineLayoutKey(maxWidth) {
1019
+ return maxWidth == null ? "single:intrinsic" : `single:${maxWidth}`;
1020
+ }
1021
+ function getMultiLineMeasureLayoutKey(maxWidth) {
1022
+ return maxWidth == null ? "multi:measure:intrinsic" : `multi:measure:${maxWidth}`;
1023
+ }
1024
+ function getMultiLineDrawLayoutKey(maxWidth) {
1025
+ return maxWidth == null ? "multi:draw:intrinsic" : `multi:draw:${maxWidth}`;
1026
+ }
1027
+ function getSingleLineMinContentLayoutKey() {
1028
+ return "single:min-content";
1029
+ }
1030
+ function getMultiLineMinContentLayoutKey() {
1031
+ return "multi:min-content";
1032
+ }
1033
+ function getSingleLineLayout(node, ctx, text, whitespace) {
1034
+ const maxWidth = normalizeTextMaxWidth(ctx.constraints?.maxWidth);
1035
+ return readCachedTextLayout(node, ctx, getSingleLineLayoutKey(maxWidth), () => maxWidth == null ? layoutFirstLineIntrinsic(ctx, text, whitespace) : layoutFirstLine(ctx, text, maxWidth, whitespace));
1036
+ }
1037
+ function getMultiLineMeasureLayout(node, ctx, text, whitespace) {
1038
+ const maxWidth = normalizeTextMaxWidth(ctx.constraints?.maxWidth);
1039
+ return readCachedTextLayout(node, ctx, getMultiLineMeasureLayoutKey(maxWidth), () => maxWidth == null ? measureTextIntrinsic(ctx, text, whitespace) : measureText(ctx, text, maxWidth, whitespace));
1040
+ }
1041
+ function getMultiLineDrawLayout(node, ctx, text, whitespace) {
1042
+ const maxWidth = normalizeTextMaxWidth(ctx.constraints?.maxWidth);
1043
+ return readCachedTextLayout(node, ctx, getMultiLineDrawLayoutKey(maxWidth), () => maxWidth == null ? layoutTextIntrinsic(ctx, text, whitespace) : layoutText(ctx, text, maxWidth, whitespace));
1044
+ }
1045
+ function getSingleLineMinContentLayout(node, ctx, text, whitespace) {
1046
+ return readCachedTextLayout(node, ctx, getSingleLineMinContentLayoutKey(), () => layoutFirstLineIntrinsic(ctx, text, whitespace));
1047
+ }
1048
+ function getMultiLineMinContentLayout(node, ctx, text, whitespace) {
1049
+ return readCachedTextLayout(node, ctx, getMultiLineMinContentLayoutKey(), () => measureTextMinContent(ctx, text, whitespace));
1050
+ }
365
1051
  var MultilineText = class {
366
- #width = 0;
367
- #lines = [];
368
1052
  constructor(text, options) {
369
1053
  this.text = text;
370
1054
  this.options = options;
371
1055
  }
372
- get flex() {
373
- return true;
374
- }
375
1056
  measure(ctx) {
376
1057
  return ctx.with((g) => {
377
1058
  g.font = this.options.font;
378
- const { width, lines } = layoutText(ctx, this.text, ctx.remainingWidth);
379
- this.#width = width;
380
- this.#lines = lines;
1059
+ const { width, lineCount } = getMultiLineMeasureLayout(this, ctx, this.text, this.options.whitespace);
381
1060
  return {
382
- width: this.#width,
383
- height: this.#lines.length * this.options.lineHeight
1061
+ width,
1062
+ height: lineCount * this.options.lineHeight
1063
+ };
1064
+ });
1065
+ }
1066
+ measureMinContent(ctx) {
1067
+ return ctx.with((g) => {
1068
+ g.font = this.options.font;
1069
+ const { width, lineCount } = getMultiLineMinContentLayout(this, ctx, this.text, this.options.whitespace);
1070
+ return {
1071
+ width,
1072
+ height: lineCount * this.options.lineHeight
384
1073
  };
385
1074
  });
386
1075
  }
@@ -388,25 +1077,26 @@ var MultilineText = class {
388
1077
  return ctx.with((g) => {
389
1078
  g.font = this.options.font;
390
1079
  g.fillStyle = ctx.resolveDynValue(this.options.style);
391
- switch (this.options.alignment) {
1080
+ const { width, lines } = getMultiLineDrawLayout(this, ctx, this.text, this.options.whitespace);
1081
+ switch (resolvePhysicalTextAlign(this.options)) {
392
1082
  case "left":
393
- for (const { text, shift } of this.#lines) {
1083
+ for (const { text, shift } of lines) {
394
1084
  g.fillText(text, x, y + (this.options.lineHeight + shift) / 2);
395
1085
  y += this.options.lineHeight;
396
1086
  }
397
1087
  break;
398
1088
  case "right":
399
- x += this.#width;
1089
+ x += width;
400
1090
  g.textAlign = "right";
401
- for (const { text, shift } of this.#lines) {
1091
+ for (const { text, shift } of lines) {
402
1092
  g.fillText(text, x, y + (this.options.lineHeight + shift) / 2);
403
1093
  y += this.options.lineHeight;
404
1094
  }
405
1095
  break;
406
1096
  case "center":
407
- x += this.#width / 2;
1097
+ x += width / 2;
408
1098
  g.textAlign = "center";
409
- for (const { text, shift } of this.#lines) {
1099
+ for (const { text, shift } of lines) {
410
1100
  g.fillText(text, x, y + (this.options.lineHeight + shift) / 2);
411
1101
  y += this.options.lineHeight;
412
1102
  }
@@ -420,25 +1110,26 @@ var MultilineText = class {
420
1110
  }
421
1111
  };
422
1112
  var Text = class {
423
- #width = 0;
424
- #text = "";
425
- #shift = 0;
426
1113
  constructor(text, options) {
427
1114
  this.text = text;
428
1115
  this.options = options;
429
1116
  }
430
- get flex() {
431
- return false;
432
- }
433
1117
  measure(ctx) {
434
1118
  return ctx.with((g) => {
435
1119
  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;
1120
+ const { width } = getSingleLineLayout(this, ctx, this.text, this.options.whitespace);
440
1121
  return {
441
- width: this.#width,
1122
+ width,
1123
+ height: this.options.lineHeight
1124
+ };
1125
+ });
1126
+ }
1127
+ measureMinContent(ctx) {
1128
+ return ctx.with((g) => {
1129
+ g.font = this.options.font;
1130
+ const { width } = getSingleLineMinContentLayout(this, ctx, this.text, this.options.whitespace);
1131
+ return {
1132
+ width,
442
1133
  height: this.options.lineHeight
443
1134
  };
444
1135
  });
@@ -447,7 +1138,8 @@ var Text = class {
447
1138
  return ctx.with((g) => {
448
1139
  g.font = this.options.font;
449
1140
  g.fillStyle = ctx.resolveDynValue(this.options.style);
450
- g.fillText(this.#text, x, y + (this.options.lineHeight + this.#shift) / 2);
1141
+ const { text, shift } = getSingleLineLayout(this, ctx, this.text, this.options.whitespace);
1142
+ g.fillText(text, x, y + (this.options.lineHeight + shift) / 2);
451
1143
  return false;
452
1144
  });
453
1145
  }
@@ -455,34 +1147,20 @@ var Text = class {
455
1147
  return false;
456
1148
  }
457
1149
  };
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
1150
  //#endregion
480
- //#region src/renderer.ts
1151
+ //#region src/renderer/base.ts
1152
+ const MAX_CONSTRAINT_VARIANTS = 8;
1153
+ function constraintKey(constraints) {
1154
+ if (constraints == null) return "";
1155
+ return `${constraints.minWidth ?? ""},${constraints.maxWidth ?? ""},${constraints.minHeight ?? ""},${constraints.maxHeight ?? ""}`;
1156
+ }
481
1157
  var BaseRenderer = class {
482
1158
  graphics;
483
1159
  #ctx;
484
1160
  #lastWidth;
485
1161
  #cache = /* @__PURE__ */ new WeakMap();
1162
+ #layoutCache = /* @__PURE__ */ new WeakMap();
1163
+ #textLayoutCache = /* @__PURE__ */ new WeakMap();
486
1164
  get context() {
487
1165
  return shallow(this.#ctx);
488
1166
  }
@@ -493,19 +1171,20 @@ var BaseRenderer = class {
493
1171
  const self = this;
494
1172
  this.#ctx = {
495
1173
  graphics: this.graphics,
496
- get remainingWidth() {
497
- return this.graphics.canvas.clientWidth;
1174
+ measureNode(node, constraints) {
1175
+ return self.measureNode(node, constraints);
498
1176
  },
499
- set remainingWidth(value) {
500
- Object.defineProperty(this, "remainingWidth", {
501
- value,
502
- writable: true
503
- });
1177
+ getLayoutResult(node, constraints) {
1178
+ return self.getLayoutResult(node, constraints);
1179
+ },
1180
+ setLayoutResult(node, result, constraints) {
1181
+ self.setLayoutResult(node, result, constraints);
1182
+ },
1183
+ getTextLayout(node, key) {
1184
+ return self.getTextLayout(node, key);
504
1185
  },
505
- alignment: "left",
506
- reverse: false,
507
- measureNode(node) {
508
- return self.measureNode(node, this);
1186
+ setTextLayout(node, key, layout) {
1187
+ self.setTextLayout(node, key, layout);
509
1188
  },
510
1189
  invalidateNode: this.invalidateNode.bind(this),
511
1190
  resolveDynValue(value) {
@@ -523,21 +1202,131 @@ var BaseRenderer = class {
523
1202
  };
524
1203
  this.#lastWidth = this.graphics.canvas.clientWidth;
525
1204
  }
1205
+ #clearAllCaches() {
1206
+ this.#cache = /* @__PURE__ */ new WeakMap();
1207
+ this.#layoutCache = /* @__PURE__ */ new WeakMap();
1208
+ this.#textLayoutCache = /* @__PURE__ */ new WeakMap();
1209
+ }
1210
+ #syncCachesToViewportWidth() {
1211
+ const width = this.graphics.canvas.clientWidth;
1212
+ if (this.#lastWidth === width) return;
1213
+ this.#clearAllCaches();
1214
+ this.#lastWidth = width;
1215
+ }
1216
+ getRootConstraints() {
1217
+ return { maxWidth: this.graphics.canvas.clientWidth };
1218
+ }
1219
+ getRootContext() {
1220
+ const ctx = this.context;
1221
+ ctx.constraints = this.getRootConstraints();
1222
+ return ctx;
1223
+ }
1224
+ measureRootNode(node) {
1225
+ return this.measureNode(node, this.getRootConstraints());
1226
+ }
1227
+ drawRootNode(node, x = 0, y = 0) {
1228
+ this.measureRootNode(node);
1229
+ return node.draw(this.getRootContext(), x, y);
1230
+ }
1231
+ hittestRootNode(node, test) {
1232
+ this.measureRootNode(node);
1233
+ return node.hittest(this.getRootContext(), test);
1234
+ }
526
1235
  invalidateNode(node) {
1236
+ this.#syncCachesToViewportWidth();
527
1237
  this.#cache.delete(node);
528
- let it = node;
529
- while (it = getNodeParent(it)) this.#cache.delete(it);
1238
+ this.#layoutCache.delete(node);
1239
+ this.#textLayoutCache.delete(node);
1240
+ forEachNodeAncestor(node, (ancestor) => {
1241
+ this.#cache.delete(ancestor);
1242
+ this.#layoutCache.delete(ancestor);
1243
+ this.#textLayoutCache.delete(ancestor);
1244
+ });
530
1245
  }
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;
1246
+ getLayoutResult(node, constraints) {
1247
+ this.#syncCachesToViewportWidth();
1248
+ const nodeCache = this.#layoutCache.get(node);
1249
+ if (nodeCache == null) return;
1250
+ const key = constraintKey(constraints);
1251
+ const cached = nodeCache.get(key);
1252
+ if (cached == null) return;
1253
+ if (cached.revision !== getNodeRevision(node)) {
1254
+ nodeCache.delete(key);
1255
+ return;
1256
+ }
1257
+ return cached.layout;
1258
+ }
1259
+ setLayoutResult(node, result, constraints) {
1260
+ this.#syncCachesToViewportWidth();
1261
+ let nodeCache = this.#layoutCache.get(node);
1262
+ if (nodeCache == null) {
1263
+ nodeCache = /* @__PURE__ */ new Map();
1264
+ this.#layoutCache.set(node, nodeCache);
1265
+ } else if (nodeCache.size >= MAX_CONSTRAINT_VARIANTS) {
1266
+ const firstKey = nodeCache.keys().next().value;
1267
+ nodeCache.delete(firstKey);
1268
+ }
1269
+ nodeCache.set(constraintKey(constraints), {
1270
+ revision: getNodeRevision(node),
1271
+ layout: result
1272
+ });
1273
+ }
1274
+ getTextLayout(node, key) {
1275
+ this.#syncCachesToViewportWidth();
1276
+ const nodeCache = this.#textLayoutCache.get(node);
1277
+ if (nodeCache == null) return;
1278
+ const cached = nodeCache.get(key);
1279
+ if (cached == null) return;
1280
+ if (cached.revision !== getNodeRevision(node)) {
1281
+ nodeCache.delete(key);
1282
+ return;
1283
+ }
1284
+ return cached.layout;
1285
+ }
1286
+ setTextLayout(node, key, layout) {
1287
+ this.#syncCachesToViewportWidth();
1288
+ let nodeCache = this.#textLayoutCache.get(node);
1289
+ if (nodeCache == null) {
1290
+ nodeCache = /* @__PURE__ */ new Map();
1291
+ this.#textLayoutCache.set(node, nodeCache);
1292
+ } else if (nodeCache.size >= MAX_CONSTRAINT_VARIANTS) {
1293
+ const firstKey = nodeCache.keys().next().value;
1294
+ nodeCache.delete(firstKey);
538
1295
  }
539
- const result = node.measure(ctx ?? this.context);
540
- this.#cache.set(node, result);
1296
+ nodeCache.set(key, {
1297
+ revision: getNodeRevision(node),
1298
+ layout
1299
+ });
1300
+ }
1301
+ measureNode(node, constraints) {
1302
+ this.#syncCachesToViewportWidth();
1303
+ {
1304
+ const nodeCache = this.#cache.get(node);
1305
+ if (nodeCache != null) {
1306
+ const key = constraintKey(constraints);
1307
+ const cached = nodeCache.get(key);
1308
+ if (cached != null) {
1309
+ if (cached.revision === getNodeRevision(node)) return cached.box;
1310
+ nodeCache.delete(key);
1311
+ }
1312
+ }
1313
+ }
1314
+ const ctx = this.context;
1315
+ if (constraints != null) ctx.constraints = constraints;
1316
+ const result = node.measure(ctx);
1317
+ const key = constraintKey(constraints);
1318
+ let nodeCache = this.#cache.get(node);
1319
+ if (nodeCache == null) {
1320
+ nodeCache = /* @__PURE__ */ new Map();
1321
+ this.#cache.set(node, nodeCache);
1322
+ } else if (nodeCache.size >= MAX_CONSTRAINT_VARIANTS) {
1323
+ const firstKey = nodeCache.keys().next().value;
1324
+ nodeCache.delete(firstKey);
1325
+ }
1326
+ nodeCache.set(key, {
1327
+ revision: getNodeRevision(node),
1328
+ box: result
1329
+ });
541
1330
  return result;
542
1331
  }
543
1332
  };
@@ -545,33 +1334,26 @@ var DebugRenderer = class extends BaseRenderer {
545
1334
  draw(node) {
546
1335
  const { clientWidth: viewportWidth, clientHeight: viewportHeight } = this.graphics.canvas;
547
1336
  this.graphics.clearRect(0, 0, viewportWidth, viewportHeight);
548
- return node.draw(this.context, 0, 0);
1337
+ return this.drawRootNode(node);
549
1338
  }
550
1339
  hittest(node, test) {
551
- return node.hittest(this.context, test);
1340
+ return this.hittestRootNode(node, test);
552
1341
  }
553
1342
  };
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
- }
1343
+ //#endregion
1344
+ //#region src/renderer/list-state.ts
566
1345
  var ListState = class {
567
1346
  offset = 0;
568
- position = NaN;
1347
+ position;
569
1348
  items = [];
1349
+ constructor(items = []) {
1350
+ this.items = [...items];
1351
+ }
570
1352
  unshift(...items) {
571
1353
  this.unshiftAll(items);
572
1354
  }
573
1355
  unshiftAll(items) {
574
- this.position += items.length;
1356
+ if (this.position != null) this.position += items.length;
575
1357
  this.items = items.concat(this.items);
576
1358
  }
577
1359
  push(...items) {
@@ -580,20 +1362,59 @@ var ListState = class {
580
1362
  pushAll(items) {
581
1363
  this.items.push(...items);
582
1364
  }
583
- reset() {
584
- this.items = [];
1365
+ setAnchor(position, offset = 0) {
1366
+ this.position = Number.isFinite(position) ? Math.trunc(position) : void 0;
1367
+ this.offset = Number.isFinite(offset) ? offset : 0;
1368
+ }
1369
+ reset(items = []) {
1370
+ this.items = [...items];
585
1371
  this.offset = 0;
586
- this.position = NaN;
1372
+ this.position = void 0;
587
1373
  }
588
1374
  resetScroll() {
589
1375
  this.offset = 0;
590
- this.position = NaN;
1376
+ this.position = void 0;
591
1377
  }
592
1378
  applyScroll(delta) {
593
1379
  this.offset += delta;
594
1380
  }
595
1381
  };
596
- function clamp(value, min, max) {
1382
+ //#endregion
1383
+ //#region src/renderer/memo.ts
1384
+ function isWeakMapKey(value) {
1385
+ return typeof value === "object" && value !== null || typeof value === "function";
1386
+ }
1387
+ function memoRenderItem(renderItem) {
1388
+ const cache = /* @__PURE__ */ new WeakMap();
1389
+ function fn(item) {
1390
+ if (!isWeakMapKey(item)) throw new TypeError("memoRenderItem() only supports object items. Use memoRenderItemBy() for primitive keys.");
1391
+ const key = item;
1392
+ const cached = cache.get(key);
1393
+ if (cached != null) return cached;
1394
+ const result = renderItem(item);
1395
+ cache.set(key, result);
1396
+ return result;
1397
+ }
1398
+ return Object.assign(fn, { reset: (key) => cache.delete(key) });
1399
+ }
1400
+ function memoRenderItemBy(keyOf, renderItem) {
1401
+ const cache = /* @__PURE__ */ new Map();
1402
+ function fn(item) {
1403
+ const key = keyOf(item);
1404
+ const cached = cache.get(key);
1405
+ if (cached != null) return cached;
1406
+ const result = renderItem(item);
1407
+ cache.set(key, result);
1408
+ return result;
1409
+ }
1410
+ return Object.assign(fn, {
1411
+ reset: (item) => cache.delete(keyOf(item)),
1412
+ resetKey: (key) => cache.delete(key)
1413
+ });
1414
+ }
1415
+ //#endregion
1416
+ //#region src/renderer/virtualized/base.ts
1417
+ function clamp$3(value, min, max) {
597
1418
  return Math.min(Math.max(value, min), max);
598
1419
  }
599
1420
  function sameState(state, position, offset) {
@@ -629,13 +1450,23 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
629
1450
  set items(value) {
630
1451
  this.options.list.items = value;
631
1452
  }
1453
+ _readListState() {
1454
+ return {
1455
+ position: this.position,
1456
+ offset: this.offset
1457
+ };
1458
+ }
1459
+ _commitListState(state) {
1460
+ this.position = state.position;
1461
+ this.offset = state.offset;
1462
+ }
632
1463
  jumpTo(index, options = {}) {
633
1464
  if (this.items.length === 0) {
634
1465
  this.#cancelJumpAnimation();
635
1466
  return;
636
1467
  }
637
1468
  const targetIndex = this._clampItemIndex(index);
638
- this._prepareAnchorState();
1469
+ const currentState = this._normalizeListState(this._readListState());
639
1470
  const targetBlock = options.block ?? this._getDefaultJumpBlock();
640
1471
  const targetAnchor = this._getTargetAnchor(targetIndex, targetBlock);
641
1472
  if (!(options.animated ?? true)) {
@@ -644,14 +1475,14 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
644
1475
  options.onComplete?.();
645
1476
  return;
646
1477
  }
647
- const startAnchor = this._readAnchor();
1478
+ const startAnchor = this._readAnchor(currentState);
648
1479
  if (!Number.isFinite(startAnchor)) {
649
1480
  this.#cancelJumpAnimation();
650
1481
  this._applyAnchor(targetAnchor);
651
1482
  options.onComplete?.();
652
1483
  return;
653
1484
  }
654
- const duration = clamp(options.duration ?? VirtualizedRenderer.MIN_JUMP_DURATION + Math.abs(targetAnchor - startAnchor) * VirtualizedRenderer.JUMP_DURATION_PER_ITEM, 0, VirtualizedRenderer.MAX_JUMP_DURATION);
1485
+ const duration = clamp$3(options.duration ?? VirtualizedRenderer.MIN_JUMP_DURATION + Math.abs(targetAnchor - startAnchor) * VirtualizedRenderer.JUMP_DURATION_PER_ITEM, 0, VirtualizedRenderer.MAX_JUMP_DURATION);
655
1486
  if (duration <= 0 || Math.abs(targetAnchor - startAnchor) <= Number.EPSILON) {
656
1487
  this.#cancelJumpAnimation();
657
1488
  this._applyAnchor(targetAnchor);
@@ -666,10 +1497,7 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
666
1497
  needsMoreFrames: true,
667
1498
  onComplete: options.onComplete
668
1499
  };
669
- this.#controlledState = {
670
- position: this.position,
671
- offset: this.offset
672
- };
1500
+ this.#controlledState = this._readListState();
673
1501
  }
674
1502
  _resetRenderFeedback(feedback) {
675
1503
  if (feedback == null) return;
@@ -681,8 +1509,8 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
681
1509
  _accumulateRenderFeedback(feedback, idx, top, height) {
682
1510
  if (!Number.isFinite(top) || !Number.isFinite(height) || height <= 0) return;
683
1511
  const viewportHeight = this.graphics.canvas.clientHeight;
684
- const visibleTop = clamp(-top, 0, height);
685
- const visibleBottom = clamp(viewportHeight - top, 0, height);
1512
+ const visibleTop = clamp$3(-top, 0, height);
1513
+ const visibleBottom = clamp$3(viewportHeight - top, 0, height);
686
1514
  if (visibleBottom <= visibleTop) return;
687
1515
  const itemMin = idx + visibleTop / height;
688
1516
  const itemMax = idx + visibleBottom / height;
@@ -694,14 +1522,26 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
694
1522
  _renderDrawList(list, shift, feedback) {
695
1523
  let result = false;
696
1524
  const viewportHeight = this.graphics.canvas.clientHeight;
697
- for (const { idx, node, offset, height } of list) {
1525
+ for (const { idx, value: node, offset, height } of list) {
698
1526
  const y = offset + shift;
699
1527
  if (feedback != null) this._accumulateRenderFeedback(feedback, idx, y, height);
700
1528
  if (y + height < 0 || y > viewportHeight) continue;
701
- if (node.draw(this.context, 0, y)) result = true;
1529
+ if (this.drawRootNode(node, 0, y)) result = true;
702
1530
  }
703
1531
  return result;
704
1532
  }
1533
+ _renderVisibleWindow(window, feedback) {
1534
+ this._resetRenderFeedback(feedback);
1535
+ return this._renderDrawList(window.drawList, window.shift, feedback);
1536
+ }
1537
+ _hittestVisibleWindow(window, test) {
1538
+ for (const { value: node, offset, height } of window.drawList) {
1539
+ const y = offset + window.shift;
1540
+ if (test.y < y || test.y >= y + height) continue;
1541
+ return node.hittest(this.getRootContext(), shallowMerge(test, { y: test.y - y }));
1542
+ }
1543
+ return false;
1544
+ }
705
1545
  _prepareRender() {
706
1546
  const animation = this.#jumpAnimation;
707
1547
  if (animation == null) return false;
@@ -713,7 +1553,7 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
713
1553
  this.#cancelJumpAnimation();
714
1554
  return false;
715
1555
  }
716
- const progress = clamp((getNow() - animation.startTime) / animation.duration, 0, 1);
1556
+ const progress = clamp$3((getNow() - animation.startTime) / animation.duration, 0, 1);
717
1557
  const eased = progress >= 1 ? 1 : smoothstep(progress);
718
1558
  const anchor = animation.startAnchor + (animation.targetAnchor - animation.startAnchor) * eased;
719
1559
  this._applyAnchor(anchor);
@@ -724,10 +1564,7 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
724
1564
  const animation = this.#jumpAnimation;
725
1565
  if (animation == null) return requestRedraw;
726
1566
  if (animation.needsMoreFrames) {
727
- this.#controlledState = {
728
- position: this.position,
729
- offset: this.offset
730
- };
1567
+ this.#controlledState = this._readListState();
731
1568
  return true;
732
1569
  }
733
1570
  const onComplete = animation.onComplete;
@@ -736,12 +1573,12 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
736
1573
  return requestRedraw || this.#jumpAnimation != null;
737
1574
  }
738
1575
  _clampItemIndex(index) {
739
- return clamp(Number.isFinite(index) ? Math.trunc(index) : 0, 0, this.items.length - 1);
1576
+ return clamp$3(Number.isFinite(index) ? Math.trunc(index) : 0, 0, this.items.length - 1);
740
1577
  }
741
1578
  _getItemHeight(index) {
742
1579
  const item = this.items[index];
743
1580
  const node = this.options.renderItem(item);
744
- return this.measureNode(node).height;
1581
+ return this.measureRootNode(node).height;
745
1582
  }
746
1583
  _getAnchorAtOffset(index, offset) {
747
1584
  if (this.items.length === 0) return 0;
@@ -769,251 +1606,309 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
769
1606
  this.#controlledState = void 0;
770
1607
  }
771
1608
  };
772
- var TimelineRenderer = class extends VirtualizedRenderer {
773
- _getDefaultJumpBlock() {
774
- return "start";
1609
+ //#endregion
1610
+ //#region src/renderer/virtualized/solver.ts
1611
+ function clamp$2(value, min, max) {
1612
+ return Math.min(Math.max(value, min), max);
1613
+ }
1614
+ function normalizeOffset(offset) {
1615
+ return Number.isFinite(offset) ? offset : 0;
1616
+ }
1617
+ function normalizeTimelineState(itemCount, state) {
1618
+ if (itemCount <= 0) return {
1619
+ position: 0,
1620
+ offset: 0
1621
+ };
1622
+ const position = state.position;
1623
+ if (typeof position !== "number" || !Number.isFinite(position)) return {
1624
+ position: 0,
1625
+ offset: normalizeOffset(state.offset)
1626
+ };
1627
+ return {
1628
+ position: clamp$2(Math.trunc(position), 0, itemCount - 1),
1629
+ offset: normalizeOffset(state.offset)
1630
+ };
1631
+ }
1632
+ function normalizeChatState(itemCount, state) {
1633
+ if (itemCount <= 0) return {
1634
+ position: 0,
1635
+ offset: 0
1636
+ };
1637
+ const position = state.position;
1638
+ if (typeof position !== "number" || !Number.isFinite(position)) return {
1639
+ position: itemCount - 1,
1640
+ offset: normalizeOffset(state.offset)
1641
+ };
1642
+ return {
1643
+ position: clamp$2(Math.trunc(position), 0, itemCount - 1),
1644
+ offset: normalizeOffset(state.offset)
1645
+ };
1646
+ }
1647
+ function resolveTimelineVisibleWindow(items, state, viewportHeight, resolveItem) {
1648
+ const normalizedState = normalizeTimelineState(items.length, state);
1649
+ if (items.length === 0) return {
1650
+ normalizedState,
1651
+ window: {
1652
+ drawList: [],
1653
+ shift: 0
1654
+ }
1655
+ };
1656
+ let { position, offset } = normalizedState;
1657
+ let drawLength = 0;
1658
+ if (offset > 0) if (position === 0) offset = 0;
1659
+ else {
1660
+ for (let i = position - 1; i >= 0; i -= 1) {
1661
+ const { height } = resolveItem(items[i], i);
1662
+ position = i;
1663
+ offset -= height;
1664
+ if (offset <= 0) break;
1665
+ }
1666
+ if (position === 0 && offset > 0) offset = 0;
1667
+ }
1668
+ let y = offset;
1669
+ const drawList = [];
1670
+ for (let i = position; i < items.length; i += 1) {
1671
+ const { value, height } = resolveItem(items[i], i);
1672
+ if (y + height > 0) {
1673
+ drawList.push({
1674
+ idx: i,
1675
+ value,
1676
+ offset: y,
1677
+ height
1678
+ });
1679
+ drawLength += height;
1680
+ } else {
1681
+ offset += height;
1682
+ position = i + 1;
1683
+ }
1684
+ y += height;
1685
+ if (y >= viewportHeight) break;
1686
+ }
1687
+ let shift = 0;
1688
+ if (y < viewportHeight) if (position === 0 && drawLength < viewportHeight) {
1689
+ shift = -offset;
1690
+ offset = 0;
1691
+ } else {
1692
+ shift = viewportHeight - y;
1693
+ y = offset += shift;
1694
+ let lastIdx = -1;
1695
+ for (let i = position - 1; i >= 0; i -= 1) {
1696
+ const { value, height } = resolveItem(items[i], i);
1697
+ drawLength += height;
1698
+ y -= height;
1699
+ drawList.push({
1700
+ idx: i,
1701
+ value,
1702
+ offset: y - shift,
1703
+ height
1704
+ });
1705
+ lastIdx = i;
1706
+ if (y < 0) break;
1707
+ }
1708
+ if (lastIdx === 0 && drawLength < viewportHeight) {
1709
+ shift = drawList.at(-1)?.offset == null ? 0 : -drawList.at(-1).offset;
1710
+ position = 0;
1711
+ offset = 0;
1712
+ }
775
1713
  }
776
- _prepareAnchorState() {
777
- if (this.items.length === 0) return;
778
- if (!Number.isFinite(this.position)) {
779
- this.position = 0;
780
- this.offset = 0;
781
- return;
1714
+ return {
1715
+ normalizedState: {
1716
+ position,
1717
+ offset
1718
+ },
1719
+ window: {
1720
+ drawList,
1721
+ shift
1722
+ }
1723
+ };
1724
+ }
1725
+ function resolveChatVisibleWindow(items, state, viewportHeight, resolveItem) {
1726
+ const normalizedState = normalizeChatState(items.length, state);
1727
+ if (items.length === 0) return {
1728
+ normalizedState,
1729
+ window: {
1730
+ drawList: [],
1731
+ shift: 0
1732
+ }
1733
+ };
1734
+ let { position, offset } = normalizedState;
1735
+ let drawLength = 0;
1736
+ if (offset < 0) if (position === items.length - 1) offset = 0;
1737
+ else for (let i = position + 1; i < items.length; i += 1) {
1738
+ const { height } = resolveItem(items[i], i);
1739
+ position = i;
1740
+ offset += height;
1741
+ if (offset > 0) break;
1742
+ }
1743
+ let y = viewportHeight + offset;
1744
+ const drawList = [];
1745
+ for (let i = position; i >= 0; i -= 1) {
1746
+ const { value, height } = resolveItem(items[i], i);
1747
+ y -= height;
1748
+ if (y <= viewportHeight) {
1749
+ drawList.push({
1750
+ idx: i,
1751
+ value,
1752
+ offset: y,
1753
+ height
1754
+ });
1755
+ drawLength += height;
1756
+ } else {
1757
+ offset -= height;
1758
+ position = i - 1;
1759
+ }
1760
+ if (y < 0) break;
1761
+ }
1762
+ let shift = 0;
1763
+ if (y > 0) {
1764
+ shift = -y;
1765
+ if (drawLength < viewportHeight) {
1766
+ y = drawLength;
1767
+ for (let i = position + 1; i < items.length; i += 1) {
1768
+ const { value, height } = resolveItem(items[i], i);
1769
+ drawList.push({
1770
+ idx: i,
1771
+ value,
1772
+ offset: y - shift,
1773
+ height
1774
+ });
1775
+ y = drawLength += height;
1776
+ position = i;
1777
+ if (y >= viewportHeight) break;
1778
+ }
1779
+ offset = drawLength < viewportHeight ? 0 : drawLength - viewportHeight;
1780
+ } else offset = drawLength - viewportHeight;
1781
+ }
1782
+ return {
1783
+ normalizedState: {
1784
+ position,
1785
+ offset
1786
+ },
1787
+ window: {
1788
+ drawList,
1789
+ shift
782
1790
  }
783
- this.position = this._clampItemIndex(this.position);
784
- if (!Number.isFinite(this.offset)) this.offset = 0;
1791
+ };
1792
+ }
1793
+ //#endregion
1794
+ //#region src/renderer/virtualized/chat.ts
1795
+ function clamp$1(value, min, max) {
1796
+ return Math.min(Math.max(value, min), max);
1797
+ }
1798
+ var ChatRenderer = class extends VirtualizedRenderer {
1799
+ #resolveVisibleWindow() {
1800
+ return resolveChatVisibleWindow(this.items, this._readListState(), this.graphics.canvas.clientHeight, (item) => {
1801
+ const node = this.options.renderItem(item);
1802
+ return {
1803
+ value: node,
1804
+ height: this.measureRootNode(node).height
1805
+ };
1806
+ });
1807
+ }
1808
+ _getDefaultJumpBlock() {
1809
+ return "end";
1810
+ }
1811
+ _normalizeListState(state) {
1812
+ return normalizeChatState(this.items.length, state);
785
1813
  }
786
- _readAnchor() {
787
- this._prepareAnchorState();
1814
+ _readAnchor(state) {
788
1815
  if (this.items.length === 0) return 0;
789
- const height = this._getItemHeight(this.position);
790
- return height > 0 ? this.position - this.offset / height : this.position;
1816
+ const height = this._getItemHeight(state.position);
1817
+ return height > 0 ? state.position + 1 - state.offset / height : state.position + 1;
791
1818
  }
792
1819
  _applyAnchor(anchor) {
793
1820
  if (this.items.length === 0) return;
794
- const clampedAnchor = clamp(anchor, 0, this.items.length);
795
- const position = clamp(Math.floor(clampedAnchor), 0, this.items.length - 1);
1821
+ const clampedAnchor = clamp$1(anchor, 0, this.items.length);
1822
+ const position = clamp$1(Math.ceil(clampedAnchor) - 1, 0, this.items.length - 1);
796
1823
  const height = this._getItemHeight(position);
797
- this.position = position;
798
- const offset = height > 0 ? -(clampedAnchor - position) * height : 0;
799
- this.offset = Object.is(offset, -0) ? 0 : offset;
1824
+ const offset = height > 0 ? (position + 1 - clampedAnchor) * height : 0;
1825
+ this._commitListState({
1826
+ position,
1827
+ offset: Object.is(offset, -0) ? 0 : offset
1828
+ });
800
1829
  }
801
1830
  _getTargetAnchor(index, block) {
802
1831
  const height = this._getItemHeight(index);
803
1832
  const viewportHeight = this.graphics.canvas.clientHeight;
804
1833
  switch (block) {
805
- case "start": return this._getAnchorAtOffset(index, 0);
806
- case "center": return this._getAnchorAtOffset(index, height / 2 - viewportHeight / 2);
807
- case "end": return this._getAnchorAtOffset(index, height - viewportHeight);
1834
+ case "start": return this._getAnchorAtOffset(index, viewportHeight);
1835
+ case "center": return this._getAnchorAtOffset(index, height / 2 + viewportHeight / 2);
1836
+ case "end": return this._getAnchorAtOffset(index, height);
808
1837
  }
809
1838
  }
810
1839
  render(feedback) {
811
1840
  const keepAnimating = this._prepareRender();
812
1841
  const { clientWidth: viewportWidth, clientHeight: viewportHeight } = this.graphics.canvas;
813
1842
  this.graphics.clearRect(0, 0, viewportWidth, viewportHeight);
814
- this._resetRenderFeedback(feedback);
815
- let drawLength = 0;
816
- if (Number.isNaN(this.position)) this.position = 0;
817
- if (this.offset > 0) if (this.position === 0) this.offset = 0;
818
- else {
819
- for (let i = this.position - 1; i >= 0; i -= 1) {
820
- const item = this.items[i];
821
- const node = this.options.renderItem(item);
822
- const { height } = this.measureNode(node);
823
- this.position = i;
824
- this.offset -= height;
825
- if (this.offset <= 0) break;
826
- }
827
- if (this.position === 0 && this.offset > 0) this.offset = 0;
828
- }
829
- let y = this.offset;
830
- const drawList = [];
831
- for (let i = this.position; i < this.items.length; i += 1) {
832
- const item = this.items[i];
833
- const node = this.options.renderItem(item);
834
- const { height } = this.measureNode(node);
835
- if (y + height > 0) {
836
- drawList.push({
837
- idx: i,
838
- node,
839
- offset: y,
840
- height
841
- });
842
- drawLength += height;
843
- } else {
844
- this.offset += height;
845
- this.position = i + 1;
846
- }
847
- y += height;
848
- if (y >= viewportHeight) break;
849
- }
850
- let shift = 0;
851
- if (y < viewportHeight) if (this.position === 0 && drawLength < viewportHeight) {
852
- shift = -this.offset;
853
- this.offset = 0;
854
- } else {
855
- shift = viewportHeight - y;
856
- y = this.offset += shift;
857
- let lastIdx = -1;
858
- for (let i = this.position - 1; i >= 0; i -= 1) {
859
- const item = this.items[lastIdx = i];
860
- const node = this.options.renderItem(item);
861
- const { height } = this.measureNode(node);
862
- drawLength += height;
863
- y -= height;
864
- drawList.push({
865
- idx: i,
866
- node,
867
- offset: y - shift,
868
- height
869
- });
870
- if (y < 0) break;
871
- }
872
- if (lastIdx === 0 && drawLength < viewportHeight) {
873
- shift = -drawList[drawList.length - 1].offset;
874
- this.position = 0;
875
- this.offset = 0;
876
- }
877
- }
878
- const requestRedraw = this._renderDrawList(drawList, shift, feedback);
1843
+ const solution = this.#resolveVisibleWindow();
1844
+ const requestRedraw = this._renderVisibleWindow(solution.window, feedback);
1845
+ this._commitListState(solution.normalizedState);
879
1846
  return this._finishRender(keepAnimating || requestRedraw);
880
1847
  }
881
1848
  hittest(test) {
882
- const viewportHeight = this.graphics.canvas.clientHeight;
883
- let y = this.offset;
884
- for (let i = this.position; i < this.items.length; i += 1) {
885
- const item = this.items[i];
886
- const node = this.options.renderItem(item);
887
- const { height } = this.measureNode(node);
888
- if (test.y < y + height) return node.hittest(this.context, shallowMerge(test, { y: test.y - y }));
889
- y += height;
890
- if (y >= viewportHeight) break;
891
- }
892
- return false;
1849
+ return this._hittestVisibleWindow(this.#resolveVisibleWindow().window, test);
893
1850
  }
894
1851
  };
895
- var ChatRenderer = class extends VirtualizedRenderer {
1852
+ //#endregion
1853
+ //#region src/renderer/virtualized/timeline.ts
1854
+ function clamp(value, min, max) {
1855
+ return Math.min(Math.max(value, min), max);
1856
+ }
1857
+ var TimelineRenderer = class extends VirtualizedRenderer {
1858
+ #resolveVisibleWindow() {
1859
+ return resolveTimelineVisibleWindow(this.items, this._readListState(), this.graphics.canvas.clientHeight, (item) => {
1860
+ const node = this.options.renderItem(item);
1861
+ return {
1862
+ value: node,
1863
+ height: this.measureRootNode(node).height
1864
+ };
1865
+ });
1866
+ }
896
1867
  _getDefaultJumpBlock() {
897
- return "end";
1868
+ return "start";
898
1869
  }
899
- _prepareAnchorState() {
900
- if (this.items.length === 0) return;
901
- if (!Number.isFinite(this.position)) {
902
- this.position = this.items.length - 1;
903
- this.offset = 0;
904
- return;
905
- }
906
- this.position = this._clampItemIndex(this.position);
907
- if (!Number.isFinite(this.offset)) this.offset = 0;
1870
+ _normalizeListState(state) {
1871
+ return normalizeTimelineState(this.items.length, state);
908
1872
  }
909
- _readAnchor() {
910
- this._prepareAnchorState();
1873
+ _readAnchor(state) {
911
1874
  if (this.items.length === 0) return 0;
912
- const height = this._getItemHeight(this.position);
913
- return height > 0 ? this.position + 1 - this.offset / height : this.position + 1;
1875
+ const height = this._getItemHeight(state.position);
1876
+ return height > 0 ? state.position - state.offset / height : state.position;
914
1877
  }
915
1878
  _applyAnchor(anchor) {
916
1879
  if (this.items.length === 0) return;
917
1880
  const clampedAnchor = clamp(anchor, 0, this.items.length);
918
- const position = clamp(Math.ceil(clampedAnchor) - 1, 0, this.items.length - 1);
1881
+ const position = clamp(Math.floor(clampedAnchor), 0, this.items.length - 1);
919
1882
  const height = this._getItemHeight(position);
920
- this.position = position;
921
- const offset = height > 0 ? (position + 1 - clampedAnchor) * height : 0;
922
- this.offset = Object.is(offset, -0) ? 0 : offset;
1883
+ const offset = height > 0 ? -(clampedAnchor - position) * height : 0;
1884
+ this._commitListState({
1885
+ position,
1886
+ offset: Object.is(offset, -0) ? 0 : offset
1887
+ });
923
1888
  }
924
1889
  _getTargetAnchor(index, block) {
925
1890
  const height = this._getItemHeight(index);
926
1891
  const viewportHeight = this.graphics.canvas.clientHeight;
927
1892
  switch (block) {
928
- case "start": return this._getAnchorAtOffset(index, viewportHeight);
929
- case "center": return this._getAnchorAtOffset(index, height / 2 + viewportHeight / 2);
930
- case "end": return this._getAnchorAtOffset(index, height);
1893
+ case "start": return this._getAnchorAtOffset(index, 0);
1894
+ case "center": return this._getAnchorAtOffset(index, height / 2 - viewportHeight / 2);
1895
+ case "end": return this._getAnchorAtOffset(index, height - viewportHeight);
931
1896
  }
932
1897
  }
933
1898
  render(feedback) {
934
1899
  const keepAnimating = this._prepareRender();
935
1900
  const { clientWidth: viewportWidth, clientHeight: viewportHeight } = this.graphics.canvas;
936
1901
  this.graphics.clearRect(0, 0, viewportWidth, viewportHeight);
937
- this._resetRenderFeedback(feedback);
938
- let drawLength = 0;
939
- if (Number.isNaN(this.position)) this.position = this.items.length - 1;
940
- if (this.offset < 0) if (this.position === this.items.length - 1) this.offset = 0;
941
- else for (let i = this.position + 1; i < this.items.length; i += 1) {
942
- const item = this.items[i];
943
- const node = this.options.renderItem(item);
944
- const { height } = this.measureNode(node);
945
- this.position = i;
946
- this.offset += height;
947
- if (this.offset > 0) break;
948
- }
949
- let y = viewportHeight + this.offset;
950
- const drawList = [];
951
- for (let i = this.position; i >= 0; i -= 1) {
952
- const item = this.items[i];
953
- const node = this.options.renderItem(item);
954
- const { height } = this.measureNode(node);
955
- y -= height;
956
- if (y <= viewportHeight) {
957
- drawList.push({
958
- idx: i,
959
- node,
960
- offset: y,
961
- height
962
- });
963
- drawLength += height;
964
- } else {
965
- this.offset -= height;
966
- this.position = i - 1;
967
- }
968
- if (y < 0) break;
969
- }
970
- let shift = 0;
971
- if (y > 0) {
972
- shift = -y;
973
- if (drawLength < viewportHeight) {
974
- y = drawLength;
975
- for (let i = this.position + 1; i < this.items.length; i += 1) {
976
- const item = this.items[i];
977
- const node = this.options.renderItem(item);
978
- const { height } = this.measureNode(node);
979
- drawList.push({
980
- idx: i,
981
- node,
982
- offset: y - shift,
983
- height
984
- });
985
- y = drawLength += height;
986
- this.position = i;
987
- if (y >= viewportHeight) break;
988
- }
989
- if (drawLength < viewportHeight) this.offset = 0;
990
- else this.offset = drawLength - viewportHeight;
991
- } else this.offset = drawLength - viewportHeight;
992
- }
993
- const requestRedraw = this._renderDrawList(drawList, shift, feedback);
1902
+ const solution = this.#resolveVisibleWindow();
1903
+ const requestRedraw = this._renderVisibleWindow(solution.window, feedback);
1904
+ this._commitListState(solution.normalizedState);
994
1905
  return this._finishRender(keepAnimating || requestRedraw);
995
1906
  }
996
1907
  hittest(test) {
997
- const viewportHeight = this.graphics.canvas.clientHeight;
998
- let drawLength = 0;
999
- const heights = [];
1000
- for (let i = this.position; i >= 0; i -= 1) {
1001
- const item = this.items[i];
1002
- const node = this.options.renderItem(item);
1003
- const { height } = this.measureNode(node);
1004
- drawLength += height;
1005
- heights.push([node, height]);
1006
- }
1007
- let y = drawLength < viewportHeight ? drawLength : viewportHeight + this.offset;
1008
- if (test.y > y) return false;
1009
- for (const [node, height] of heights) {
1010
- y -= height;
1011
- if (test.y > y) return node.hittest(this.context, shallowMerge(test, { y: test.y - y }));
1012
- }
1013
- return false;
1908
+ return this._hittestVisibleWindow(this.#resolveVisibleWindow().window, test);
1014
1909
  }
1015
1910
  };
1016
1911
  //#endregion
1017
- export { AlignBox, BaseRenderer, ChatRenderer, DebugRenderer, Fixed, Group, HStack, ListState, MultilineText, PaddingBox, Text, TimelineRenderer, VStack, VirtualizedRenderer, Wrapper, getNodeParent, memoRenderItem, registerNodeParent, unregisterNodeParent };
1912
+ export { BaseRenderer, ChatRenderer, DebugRenderer, Fixed, Flex, FlexItem, Group, ListState, MultilineText, PaddingBox, Place, Text, TimelineRenderer, VirtualizedRenderer, Wrapper, memoRenderItem, memoRenderItemBy };
1018
1913
 
1019
1914
  //# sourceMappingURL=index.mjs.map