chat-layout 1.2.0-8 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -194,6 +194,24 @@ Type-check:
194
194
  bun run typecheck
195
195
  ```
196
196
 
197
+ Run tests:
198
+
199
+ ```bash
200
+ bun run test
201
+ ```
202
+
203
+ Run tests with coverage:
204
+
205
+ ```bash
206
+ bun run test:coverage
207
+ ```
208
+
209
+ Run the local verification bundle:
210
+
211
+ ```bash
212
+ bun run check
213
+ ```
214
+
197
215
  Build distributable files:
198
216
 
199
217
  ```bash
package/example/chat.ts CHANGED
@@ -10,13 +10,13 @@ import {
10
10
  ShrinkWrap,
11
11
  Text,
12
12
  Wrapper,
13
+ initRenderFeedback,
13
14
  memoRenderItem,
14
15
  type Context,
15
16
  type DynValue,
16
17
  type HitTest,
17
18
  type InlineSpan,
18
19
  type Node,
19
- type RenderFeedback,
20
20
  } from "chat-layout";
21
21
 
22
22
  const sampleWords = [
@@ -507,16 +507,9 @@ const renderer = new ListRenderer(ctx, {
507
507
  });
508
508
  renderer.padding = { top: 32, bottom: 32 };
509
509
  let nextMessageId = list.items.length + 1;
510
+ const feedback = initRenderFeedback();
510
511
 
511
512
  function drawFrame(): void {
512
- const feedback: RenderFeedback = {
513
- minIdx: Number.NaN,
514
- maxIdx: Number.NaN,
515
- min: Number.NaN,
516
- max: Number.NaN,
517
- canAutoFollowTop: false,
518
- canAutoFollowBottom: false,
519
- };
520
513
  renderer.render(feedback);
521
514
 
522
515
  ctx.save();
package/index.d.mts CHANGED
@@ -24,6 +24,10 @@ interface RenderFeedback {
24
24
  /** Whether the current viewport may auto-follow inserts at the visual bottom edge. */
25
25
  canAutoFollowBottom: boolean;
26
26
  }
27
+ /**
28
+ * Creates or resets a render feedback object to its default empty state.
29
+ */
30
+ declare function initRenderFeedback(feedback?: Partial<RenderFeedback>): RenderFeedback;
27
31
  /**
28
32
  * The main axis direction used by flex containers.
29
33
  */
@@ -779,5 +783,5 @@ declare class ListRenderer<C extends CanvasRenderingContext2D, T extends {}> ext
779
783
  protected _getTargetAnchor(index: number, block: NonNullable<JumpToOptions["block"]>): number;
780
784
  }
781
785
  //#endregion
782
- export { Axis, BaseRenderer, Box, ChildLayoutResult, Context, CrossAxisAlignment, DebugRenderer, DeleteListItemAnimationOptions, DynValue, Fixed, Flex, FlexContainerOptions, FlexItem, FlexItemOptions, FlexLayoutResult, Group, HitTest, InlineSpan, InsertListItemsAnimationOptions, JumpToOptions, LayoutConstraints, LayoutRect, ListAnchorMode, ListLayoutOptions, ListPadding, ListRenderer, ListRendererOptions, ListState, ListUnderflowAlign, MainAxisAlignment, MainAxisSize, MultilineText, MultilineTextOptions, Node, PaddingBox, PhysicalTextAlign, Place, PushListItemsAnimationOptions, RenderFeedback, RendererOptions, ShrinkWrap, Text, TextAlign, TextEllipsisPosition, TextJustifyMode, TextJustifyOptions, TextOptions, TextOverflowMode, TextOverflowWrapMode, TextStyleOptions, TextWhiteSpaceMode, TextWordBreakMode, UnshiftListItemsAnimationOptions, UpdateListItemAnimationOptions, VirtualizedRenderer, Wrapper, memoRenderItem, memoRenderItemBy };
786
+ export { Axis, BaseRenderer, Box, ChildLayoutResult, Context, CrossAxisAlignment, DebugRenderer, DeleteListItemAnimationOptions, DynValue, Fixed, Flex, FlexContainerOptions, FlexItem, FlexItemOptions, FlexLayoutResult, Group, HitTest, InlineSpan, InsertListItemsAnimationOptions, JumpToOptions, LayoutConstraints, LayoutRect, ListAnchorMode, ListLayoutOptions, ListPadding, ListRenderer, ListRendererOptions, ListState, ListUnderflowAlign, MainAxisAlignment, MainAxisSize, MultilineText, MultilineTextOptions, Node, PaddingBox, PhysicalTextAlign, Place, PushListItemsAnimationOptions, RenderFeedback, RendererOptions, ShrinkWrap, Text, TextAlign, TextEllipsisPosition, TextJustifyMode, TextJustifyOptions, TextOptions, TextOverflowMode, TextOverflowWrapMode, TextStyleOptions, TextWhiteSpaceMode, TextWordBreakMode, UnshiftListItemsAnimationOptions, UpdateListItemAnimationOptions, VirtualizedRenderer, Wrapper, initRenderFeedback, memoRenderItem, memoRenderItemBy };
783
787
  //# sourceMappingURL=index.d.mts.map
package/index.mjs CHANGED
@@ -3574,31 +3574,13 @@ var DebugRenderer = class extends BaseRenderer {
3574
3574
  }
3575
3575
  };
3576
3576
  //#endregion
3577
- //#region src/renderer/weak-listeners.ts
3578
- function pruneWeakListenerMap(listeners) {
3579
- for (const [token, listener] of listeners) if (listener.ownerRef.deref() == null) listeners.delete(token);
3580
- }
3581
- function emitWeakListeners(listeners, event) {
3582
- for (const [token, listener] of [...listeners]) {
3583
- const owner = listener.ownerRef.deref();
3584
- if (owner == null) {
3585
- listeners.delete(token);
3586
- continue;
3587
- }
3588
- listener.notify(owner, event);
3589
- }
3590
- }
3591
- //#endregion
3592
3577
  //#region src/renderer/list-state.ts
3593
- const listStateListeners = /* @__PURE__ */ new WeakMap();
3594
- const listStateListenerRegistry = typeof FinalizationRegistry === "function" ? new FinalizationRegistry(({ listRef, token }) => {
3595
- const list = listRef.deref();
3596
- if (list == null) return;
3597
- deleteListStateListener(list, token);
3598
- }) : null;
3578
+ const listStateChangeQueues = /* @__PURE__ */ new WeakMap();
3599
3579
  const listScrollMutations = /* @__PURE__ */ new WeakMap();
3600
3580
  const WRITE_LIST_SCROLL_STATE = Symbol("writeListScrollState");
3601
3581
  const FINALIZE_LIST_DELETE = Symbol("finalizeListDelete");
3582
+ const LIST_STATE_CHANGE_TIME = Symbol("listStateChangeTime");
3583
+ const LIST_STATE_CHANGE_SNAPSHOT = Symbol("listStateChangeSnapshot");
3602
3584
  function normalizePosition(value) {
3603
3585
  return typeof value === "number" && Number.isFinite(value) ? Math.trunc(value) : void 0;
3604
3586
  }
@@ -3634,34 +3616,40 @@ function writeInternalListScrollState(list, state) {
3634
3616
  function finalizeInternalListDelete(list, item) {
3635
3617
  list[FINALIZE_LIST_DELETE](item);
3636
3618
  }
3637
- function deleteListStateListener(list, token) {
3638
- const listeners = listStateListeners.get(list);
3639
- if (listeners == null) return;
3640
- listeners.delete(token);
3641
- if (listeners.size === 0) listStateListeners.delete(list);
3642
- }
3643
- function emitListStateChange(list, change) {
3644
- const listeners = listStateListeners.get(list);
3645
- if (listeners == null) return;
3646
- emitWeakListeners(listeners, change);
3647
- if (listeners.size === 0) listStateListeners.delete(list);
3648
- }
3649
- function subscribeListState(list, owner, listener) {
3619
+ function enqueueListStateChange(list, change) {
3650
3620
  const key = list;
3651
- let listeners = listStateListeners.get(key);
3652
- if (listeners == null) {
3653
- listeners = /* @__PURE__ */ new Map();
3654
- listStateListeners.set(key, listeners);
3655
- } else pruneWeakListenerMap(listeners);
3656
- const token = Symbol();
3657
- listeners.set(token, {
3658
- ownerRef: new WeakRef(owner),
3659
- notify: listener
3621
+ let queue = listStateChangeQueues.get(key);
3622
+ if (queue == null) {
3623
+ queue = [];
3624
+ listStateChangeQueues.set(key, queue);
3625
+ }
3626
+ const timestampedChange = change;
3627
+ Object.defineProperty(timestampedChange, LIST_STATE_CHANGE_TIME, {
3628
+ value: performance.now(),
3629
+ configurable: true
3660
3630
  });
3661
- listStateListenerRegistry?.register(owner, {
3662
- listRef: new WeakRef(key),
3663
- token
3631
+ Object.defineProperty(timestampedChange, LIST_STATE_CHANGE_SNAPSHOT, {
3632
+ value: {
3633
+ items: [...list.items],
3634
+ position: list.position,
3635
+ offset: list.offset
3636
+ },
3637
+ configurable: true
3664
3638
  });
3639
+ queue.push(timestampedChange);
3640
+ }
3641
+ function drainInternalListStateChanges(list) {
3642
+ const key = list;
3643
+ const queue = listStateChangeQueues.get(key);
3644
+ if (queue == null || queue.length === 0) return [];
3645
+ listStateChangeQueues.delete(key);
3646
+ return queue;
3647
+ }
3648
+ function readInternalListStateChangeTime(change) {
3649
+ return change[LIST_STATE_CHANGE_TIME];
3650
+ }
3651
+ function readInternalListStateChangeSnapshot(change) {
3652
+ return change[LIST_STATE_CHANGE_SNAPSHOT];
3665
3653
  }
3666
3654
  function isObjectIdentityCandidate(value) {
3667
3655
  return typeof value === "object" && value !== null || typeof value === "function";
@@ -3724,7 +3712,7 @@ var ListState = class {
3724
3712
  assertUniqueItemReferences(nextItems);
3725
3713
  this.#items = nextItems;
3726
3714
  this.#pendingDeletes.clear();
3727
- emitListStateChange(this, { type: "set" });
3715
+ enqueueListStateChange(this, { type: "set" });
3728
3716
  }
3729
3717
  /**
3730
3718
  * @param items Initial list items.
@@ -3745,7 +3733,7 @@ var ListState = class {
3745
3733
  const normalizedAnimation = normalizeInsertAnimation(animation);
3746
3734
  if (this.position != null) this.#writeScrollState({ position: this.position + items.length }, "internal");
3747
3735
  this.#items = items.concat(this.#items);
3748
- emitListStateChange(this, {
3736
+ enqueueListStateChange(this, {
3749
3737
  type: "unshift",
3750
3738
  count: items.length,
3751
3739
  animation: normalizedAnimation
@@ -3761,7 +3749,7 @@ var ListState = class {
3761
3749
  assertUniqueItemReferences(items, this.#items);
3762
3750
  const normalizedAnimation = normalizeInsertAnimation(animation);
3763
3751
  this.#items.push(...items);
3764
- emitListStateChange(this, {
3752
+ enqueueListStateChange(this, {
3765
3753
  type: "push",
3766
3754
  count: items.length,
3767
3755
  animation: normalizedAnimation
@@ -3779,7 +3767,7 @@ var ListState = class {
3779
3767
  if (this.#items.includes(nextItem)) throw new Error("update() nextItem is already present in the list.");
3780
3768
  const prevItem = this.#items[index];
3781
3769
  this.#items[index] = nextItem;
3782
- emitListStateChange(this, {
3770
+ enqueueListStateChange(this, {
3783
3771
  type: "update",
3784
3772
  prevItem,
3785
3773
  nextItem,
@@ -3800,7 +3788,7 @@ var ListState = class {
3800
3788
  return;
3801
3789
  }
3802
3790
  this.#pendingDeletes.add(item);
3803
- emitListStateChange(this, {
3791
+ enqueueListStateChange(this, {
3804
3792
  type: "delete",
3805
3793
  item,
3806
3794
  animation: normalizedAnimation
@@ -3823,7 +3811,7 @@ var ListState = class {
3823
3811
  if (this.position > index) this.#writeScrollState({ position: this.position - 1 }, "internal");
3824
3812
  else if (this.position === index) this.#writeScrollState({ position: Math.min(index, this.#items.length - 1) }, "internal");
3825
3813
  }
3826
- emitListStateChange(this, {
3814
+ enqueueListStateChange(this, {
3827
3815
  type: "delete-finalize",
3828
3816
  item
3829
3817
  });
@@ -3840,7 +3828,7 @@ var ListState = class {
3840
3828
  position: void 0,
3841
3829
  offset: 0
3842
3830
  }, "internal");
3843
- emitListStateChange(this, { type: "reset" });
3831
+ enqueueListStateChange(this, { type: "reset" });
3844
3832
  }
3845
3833
  /** Applies a relative pixel scroll delta. */
3846
3834
  applyScroll(delta) {
@@ -3930,6 +3918,21 @@ function memoRenderItemBy(keyOf, renderItem, options = {}) {
3930
3918
  });
3931
3919
  }
3932
3920
  //#endregion
3921
+ //#region src/types.ts
3922
+ /**
3923
+ * Creates or resets a render feedback object to its default empty state.
3924
+ */
3925
+ function initRenderFeedback(feedback = {}) {
3926
+ const target = feedback;
3927
+ target.minIdx = NaN;
3928
+ target.maxIdx = NaN;
3929
+ target.min = NaN;
3930
+ target.max = NaN;
3931
+ target.canAutoFollowTop = false;
3932
+ target.canAutoFollowBottom = false;
3933
+ return target;
3934
+ }
3935
+ //#endregion
3933
3936
  //#region src/renderer/virtualized/frame-session.ts
3934
3937
  function prepareFrameSession(params) {
3935
3938
  let solution = params.resolveVisibleWindow(params.now);
@@ -4046,6 +4049,10 @@ var JumpController = class JumpController {
4046
4049
  #pendingAutoFollowRecomputeReasonTop = "init";
4047
4050
  #pendingAutoFollowRecomputeReasonBottom = "init";
4048
4051
  #pendingTransitionSettleReconcile = false;
4052
+ #autoFollowObservationCountTop = 0;
4053
+ #autoFollowObservationCountBottom = 0;
4054
+ #pendingAutoFollowInvalidationTop = false;
4055
+ #pendingAutoFollowInvalidationBottom = false;
4049
4056
  #lastArmedAutoFollowBoundary;
4050
4057
  #lastObservedRenderedAutoFollowTop = false;
4051
4058
  #lastObservedRenderedAutoFollowBottom = false;
@@ -4127,12 +4134,32 @@ var JumpController = class JumpController {
4127
4134
  block: boundary === "bottom" ? "end" : "start"
4128
4135
  });
4129
4136
  }
4137
+ beginAutoFollowBoundaryObservation(boundary) {
4138
+ if (boundary === "top") {
4139
+ this.#autoFollowObservationCountTop += 1;
4140
+ return;
4141
+ }
4142
+ this.#autoFollowObservationCountBottom += 1;
4143
+ }
4144
+ endAutoFollowBoundaryObservation(boundary) {
4145
+ if (boundary === "top") {
4146
+ this.#autoFollowObservationCountTop = Math.max(0, this.#autoFollowObservationCountTop - 1);
4147
+ return;
4148
+ }
4149
+ this.#autoFollowObservationCountBottom = Math.max(0, this.#autoFollowObservationCountBottom - 1);
4150
+ }
4151
+ invalidateAutoFollowBoundary(boundary) {
4152
+ if (boundary == null || boundary === "top") this.#pendingAutoFollowInvalidationTop = true;
4153
+ if (boundary == null || boundary === "bottom") this.#pendingAutoFollowInvalidationBottom = true;
4154
+ }
4130
4155
  recomputeAutoFollowCapabilities(capabilities) {
4131
4156
  const previouslyObservedDualBoundary = this.#lastObservedRenderedAutoFollowTop && this.#lastObservedRenderedAutoFollowBottom;
4132
4157
  if (capabilities.top && capabilities.bottom && !previouslyObservedDualBoundary) {
4133
4158
  this.#setAutoFollowBoundary("top", true, "dual-boundary-promotion");
4134
4159
  this.#setAutoFollowBoundary("bottom", true, "dual-boundary-promotion");
4135
4160
  }
4161
+ this.#syncObservedOrInvalidatedBoundary("top", capabilities);
4162
+ this.#syncObservedOrInvalidatedBoundary("bottom", capabilities);
4136
4163
  if (this.#pendingAutoFollowRecomputeTop) {
4137
4164
  this.#setAutoFollowBoundary("top", capabilities.top, `strict-recompute:${this.#pendingAutoFollowRecomputeReasonTop}`);
4138
4165
  this.#pendingAutoFollowRecomputeTop = false;
@@ -4159,22 +4186,23 @@ var JumpController = class JumpController {
4159
4186
  reconcileAutoFollowAfterTransitionSettle() {
4160
4187
  this.#pendingTransitionSettleReconcile = true;
4161
4188
  }
4162
- handleListStateChange(change) {
4189
+ handleListStateChange(change, now = getNow()) {
4163
4190
  switch (change.type) {
4164
4191
  case "reset":
4165
4192
  case "set":
4166
4193
  this.#cancelJumpAnimation();
4167
4194
  this.#clearPendingPostJumpBoundary();
4168
4195
  this.#clearPendingTransitionSettleReconcile();
4196
+ this.#clearAutoFollowObservationState();
4169
4197
  this.#syncScrollMutationVersion();
4170
4198
  this.#markAutoFollowRecompute(void 0, change.type);
4171
4199
  return change;
4172
4200
  case "push":
4173
- case "unshift": return this.#handleBoundaryInsert(change);
4201
+ case "unshift": return this.#handleBoundaryInsert(change, now);
4174
4202
  default: return change;
4175
4203
  }
4176
4204
  }
4177
- #handleBoundaryInsert(change) {
4205
+ #handleBoundaryInsert(change, now) {
4178
4206
  if (this.#handlePendingExternalScrollMutation()) return change;
4179
4207
  this.#clearPendingTransitionSettleReconcile();
4180
4208
  const followChange = this.#resolveAutoFollowChange(change);
@@ -4187,21 +4215,21 @@ var JumpController = class JumpController {
4187
4215
  this.#lastArmedAutoFollowBoundary = followChange.boundary;
4188
4216
  }
4189
4217
  this.#clearPendingPostJumpBoundary();
4190
- this.#materializeAnimatedAnchor(getNow(), followChange.direction, followChange.count);
4218
+ this.#materializeAnimatedAnchor(now, followChange.direction, followChange.count);
4191
4219
  this.#startJumpToIndex(followChange.boundary === "bottom" ? this.#options.getItemCount() - 1 : 0, {
4192
4220
  block: followChange.boundary === "bottom" ? "end" : "start",
4193
4221
  duration: followChange.animation?.duration
4194
- });
4222
+ }, now);
4195
4223
  return change;
4196
4224
  }
4197
4225
  #cancelJumpAnimation() {
4198
4226
  this.#jumpAnimation = void 0;
4199
4227
  }
4200
- #startJumpToIndex(index, options) {
4228
+ #startJumpToIndex(index, options, now = getNow()) {
4201
4229
  const targetIndex = this.#options.clampItemIndex(index);
4202
4230
  const targetBlock = options.block ?? this.#options.getDefaultJumpBlock();
4203
4231
  const settleBoundary = this.#resolveBoundaryLatchTarget(targetIndex, targetBlock);
4204
- this.#materializeAnimatedAnchor(getNow());
4232
+ this.#materializeAnimatedAnchor(now);
4205
4233
  const currentState = this.#options.normalizeListState(this.#options.readListState());
4206
4234
  const targetAnchor = this.#options.getTargetAnchor(targetIndex, targetBlock);
4207
4235
  if (!(options.animated ?? true)) {
@@ -4234,7 +4262,7 @@ var JumpController = class JumpController {
4234
4262
  }
4235
4263
  this.#jumpAnimation = {
4236
4264
  path,
4237
- startTime: getNow(),
4265
+ startTime: now,
4238
4266
  duration,
4239
4267
  needsMoreFrames: true,
4240
4268
  onComplete: options.onComplete
@@ -4288,6 +4316,12 @@ var JumpController = class JumpController {
4288
4316
  #clearPendingTransitionSettleReconcile() {
4289
4317
  this.#pendingTransitionSettleReconcile = false;
4290
4318
  }
4319
+ #clearAutoFollowObservationState() {
4320
+ this.#autoFollowObservationCountTop = 0;
4321
+ this.#autoFollowObservationCountBottom = 0;
4322
+ this.#pendingAutoFollowInvalidationTop = false;
4323
+ this.#pendingAutoFollowInvalidationBottom = false;
4324
+ }
4291
4325
  #materializeAnimatedAnchor(now, direction, count = 0) {
4292
4326
  const animation = this.#jumpAnimation;
4293
4327
  if (animation == null) return;
@@ -4302,6 +4336,20 @@ var JumpController = class JumpController {
4302
4336
  if (boundary === "top") this.#canAutoFollowTop = value;
4303
4337
  else this.#canAutoFollowBottom = value;
4304
4338
  }
4339
+ #syncObservedOrInvalidatedBoundary(boundary, capabilities) {
4340
+ const isObserved = this.#readAutoFollowObservationCount(boundary) > 0;
4341
+ const isInvalidated = boundary === "top" ? this.#pendingAutoFollowInvalidationTop : this.#pendingAutoFollowInvalidationBottom;
4342
+ if (!isObserved && !isInvalidated) return;
4343
+ this.#setAutoFollowBoundary(boundary, this.#readCapabilityForBoundary(capabilities, boundary), "transition-observation");
4344
+ if (boundary === "top") {
4345
+ this.#pendingAutoFollowInvalidationTop = false;
4346
+ return;
4347
+ }
4348
+ this.#pendingAutoFollowInvalidationBottom = false;
4349
+ }
4350
+ #readAutoFollowObservationCount(boundary) {
4351
+ return boundary === "top" ? this.#autoFollowObservationCountTop : this.#autoFollowObservationCountBottom;
4352
+ }
4305
4353
  #syncLastArmedBoundaryFromLatchedState() {
4306
4354
  if (this.#canAutoFollowTop === this.#canAutoFollowBottom) return;
4307
4355
  this.#lastArmedAutoFollowBoundary = this.#canAutoFollowTop ? "top" : "bottom";
@@ -4815,11 +4863,15 @@ var TransitionStore = class {
4815
4863
  return this.#transitions.has(item);
4816
4864
  }
4817
4865
  set(item, transition) {
4866
+ const previous = this.#transitions.get(item);
4818
4867
  this.#transitions.set(item, transition);
4868
+ return previous;
4819
4869
  }
4820
4870
  replace(prevItem, nextItem, transition) {
4871
+ const previous = this.#transitions.get(prevItem);
4821
4872
  this.#transitions.delete(prevItem);
4822
4873
  this.#transitions.set(nextItem, transition);
4874
+ return previous;
4823
4875
  }
4824
4876
  delete(item) {
4825
4877
  const transition = this.#transitions.get(item);
@@ -4847,6 +4899,12 @@ var TransitionStore = class {
4847
4899
  transition
4848
4900
  }));
4849
4901
  }
4902
+ entries() {
4903
+ return [...this.#transitions.entries()].map(([item, transition]) => ({
4904
+ item,
4905
+ transition
4906
+ }));
4907
+ }
4850
4908
  reset() {
4851
4909
  this.#transitions.clear();
4852
4910
  }
@@ -4939,6 +4997,30 @@ function planExistingItemTransition(params) {
4939
4997
  retention: "drawn"
4940
4998
  };
4941
4999
  }
5000
+ function resolveAutoFollowBoundaryRisk(index, ctx, snapshot) {
5001
+ const drawnRange = snapshot.readDrawnIndexRange();
5002
+ if (index < 0 || !snapshot.hasSnapshot || drawnRange == null || !Number.isFinite(drawnRange.minIndex) || !Number.isFinite(drawnRange.maxIndex)) return;
5003
+ if (ctx.anchorMode === "bottom") return index <= drawnRange.minIndex ? "top" : void 0;
5004
+ return index >= drawnRange.maxIndex ? "bottom" : void 0;
5005
+ }
5006
+ function canClassifyAutoFollowBoundaryRisk(index, snapshot) {
5007
+ return index >= 0 && snapshot.hasSnapshot && snapshot.readDrawnIndexRange() != null;
5008
+ }
5009
+ function beginTransitionAutoFollowObservation(transition, lifecycle) {
5010
+ if (transition.observedAutoFollowBoundary == null) return;
5011
+ lifecycle.beginAutoFollowBoundaryObservation(transition.observedAutoFollowBoundary);
5012
+ }
5013
+ function endTransitionAutoFollowObservation(transition, lifecycle) {
5014
+ if (transition?.observedAutoFollowBoundary == null) return;
5015
+ lifecycle.endAutoFollowBoundaryObservation(transition.observedAutoFollowBoundary);
5016
+ }
5017
+ function invalidateAutoFollowBoundaryRisk(boundary, canClassify, lifecycle) {
5018
+ if (boundary != null) {
5019
+ lifecycle.invalidateAutoFollowBoundary(boundary);
5020
+ return;
5021
+ }
5022
+ if (!canClassify) lifecycle.invalidateAutoFollowBoundary(void 0);
5023
+ }
4942
5024
  function planBoundaryInsertItems(params) {
4943
5025
  const entries = [];
4944
5026
  for (const { item, node, height } of params.measuredItems) {
@@ -5102,40 +5184,50 @@ function readCurrentVisualState(item, now, store, adapter) {
5102
5184
  translateY: 0
5103
5185
  };
5104
5186
  }
5105
- function handleTransitionStateChange(store, snapshot, change, ctx, lifecycle) {
5187
+ function handleTransitionStateChange(store, snapshot, change, ctx, lifecycle, now = getNow()) {
5106
5188
  switch (change.type) {
5107
5189
  case "update": {
5108
- const now = getNow();
5190
+ const nextIndex = ctx.items.indexOf(change.nextItem);
5191
+ const canClassifyRisk = canClassifyAutoFollowBoundaryRisk(nextIndex, snapshot);
5192
+ const observedBoundary = resolveAutoFollowBoundaryRisk(nextIndex, ctx, snapshot);
5109
5193
  const currentVisualState = readCurrentVisualState(change.prevItem, now, store, ctx);
5110
5194
  const transition = planUpdateTransition(change.prevItem, change.nextItem, change.animation?.duration, now, currentVisualState, ctx, snapshot, store);
5111
5195
  if (transition == null) {
5112
- store.delete(change.prevItem);
5196
+ endTransitionAutoFollowObservation(store.delete(change.prevItem), lifecycle);
5197
+ invalidateAutoFollowBoundaryRisk(observedBoundary, canClassifyRisk, lifecycle);
5113
5198
  return;
5114
5199
  }
5115
- store.replace(change.prevItem, change.nextItem, transition);
5200
+ transition.observedAutoFollowBoundary = observedBoundary;
5201
+ endTransitionAutoFollowObservation(store.replace(change.prevItem, change.nextItem, transition), lifecycle);
5202
+ beginTransitionAutoFollowObservation(transition, lifecycle);
5116
5203
  return;
5117
5204
  }
5118
5205
  case "delete": {
5119
- const now = getNow();
5206
+ const index = ctx.items.indexOf(change.item);
5207
+ const canClassifyRisk = canClassifyAutoFollowBoundaryRisk(index, snapshot);
5208
+ const observedBoundary = resolveAutoFollowBoundaryRisk(index, ctx, snapshot);
5120
5209
  const currentVisualState = readCurrentVisualState(change.item, now, store, ctx);
5121
5210
  const transition = planDeleteTransition(change.item, change.animation?.duration, now, currentVisualState, ctx, snapshot, store);
5122
5211
  if (transition == null) {
5123
- store.delete(change.item);
5212
+ endTransitionAutoFollowObservation(store.delete(change.item), lifecycle);
5213
+ invalidateAutoFollowBoundaryRisk(observedBoundary, canClassifyRisk, lifecycle);
5124
5214
  lifecycle.onDeleteComplete(change.item);
5125
5215
  return;
5126
5216
  }
5127
- store.set(change.item, transition);
5217
+ transition.observedAutoFollowBoundary = observedBoundary;
5218
+ endTransitionAutoFollowObservation(store.set(change.item, transition), lifecycle);
5219
+ beginTransitionAutoFollowObservation(transition, lifecycle);
5128
5220
  return;
5129
5221
  }
5130
5222
  case "delete-finalize":
5131
- store.delete(change.item);
5223
+ endTransitionAutoFollowObservation(store.delete(change.item), lifecycle);
5224
+ lifecycle.invalidateAutoFollowBoundary(void 0);
5132
5225
  return;
5133
5226
  case "unshift":
5134
5227
  case "push": {
5135
- const now = getNow();
5136
5228
  const plan = planBoundaryInsertTransition(change.type, change.count, change.animation?.duration, now, ctx, snapshot);
5137
5229
  if (plan == null) return;
5138
- for (const entry of plan.entries) store.set(entry.item, entry.transition);
5230
+ for (const entry of plan.entries) endTransitionAutoFollowObservation(store.set(entry.item, entry.transition), lifecycle);
5139
5231
  if (ctx.position == null && snapshot.coversShortList && (change.type === "push" && ctx.anchorMode === "bottom" || change.type === "unshift" && ctx.anchorMode === "top")) {
5140
5232
  const boundary = change.type === "push" ? "bottom" : "top";
5141
5233
  const boundaryItem = snapshot.readBoundaryItem(boundary);
@@ -5145,6 +5237,7 @@ function handleTransitionStateChange(store, snapshot, change, ctx, lifecycle) {
5145
5237
  }
5146
5238
  case "reset":
5147
5239
  case "set":
5240
+ for (const entry of store.entries()) endTransitionAutoFollowObservation(entry.transition, lifecycle);
5148
5241
  store.reset();
5149
5242
  snapshot.reset();
5150
5243
  return;
@@ -5187,10 +5280,9 @@ var TransitionController = class {
5187
5280
  resolveItem(item, now, adapter, lifecycle) {
5188
5281
  return resolveTransitionedItem(item, now, this.#store, adapter, lifecycle);
5189
5282
  }
5190
- handleListStateChange(change, ctx, lifecycle) {
5191
- const now = getNow();
5283
+ handleListStateChange(change, ctx, lifecycle, now = getNow()) {
5192
5284
  this.settle(now, lifecycle);
5193
- handleTransitionStateChange(this.#store, this.#snapshot, change, ctx, lifecycle);
5285
+ handleTransitionStateChange(this.#store, this.#snapshot, change, ctx, lifecycle, now);
5194
5286
  }
5195
5287
  settle(now, lifecycle) {
5196
5288
  return this.#settleTransitions(this.#store.findCompleted(now), now, lifecycle);
@@ -5213,7 +5305,11 @@ var TransitionController = class {
5213
5305
  const index = lifecycle.readItemIndex(item);
5214
5306
  if (index >= 0) completedDeleteIndices.push(index);
5215
5307
  }
5216
- this.#store.delete(item);
5308
+ const removedTransition = this.#store.delete(item);
5309
+ if (removedTransition?.observedAutoFollowBoundary != null) {
5310
+ lifecycle.endAutoFollowBoundaryObservation(removedTransition.observedAutoFollowBoundary);
5311
+ lifecycle.invalidateAutoFollowBoundary(removedTransition.observedAutoFollowBoundary);
5312
+ }
5217
5313
  if (transition.kind === "delete") lifecycle.onDeleteComplete(item);
5218
5314
  }
5219
5315
  if (anchor != null && Number.isFinite(anchor)) lifecycle.restoreVisualAnchor(remapAnchorAfterDeletes(anchor, completedDeleteIndices));
@@ -5270,6 +5366,7 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
5270
5366
  static JUMP_DURATION_PER_PIXEL = .7;
5271
5367
  #jumpController;
5272
5368
  #transitionController = new TransitionController();
5369
+ #listStateOverride;
5273
5370
  constructor(graphics, options) {
5274
5371
  super(graphics, options);
5275
5372
  this.#jumpController = new JumpController({
@@ -5287,21 +5384,18 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
5287
5384
  clampItemIndex: this._clampItemIndex.bind(this),
5288
5385
  getItemHeight: this._getItemHeight.bind(this)
5289
5386
  });
5290
- subscribeListState(options.list, this, (owner, change) => {
5291
- owner.#handleListStateChange(change);
5292
- });
5293
5387
  }
5294
5388
  /** Current anchor item index. */
5295
5389
  get position() {
5296
- return this.options.list.position;
5390
+ return this.#listStateOverride?.position ?? this.options.list.position;
5297
5391
  }
5298
5392
  /** Pixel offset from the anchored item edge. */
5299
5393
  get offset() {
5300
- return this.options.list.offset;
5394
+ return this.#listStateOverride?.offset ?? this.options.list.offset;
5301
5395
  }
5302
5396
  /** Items currently available to the renderer. */
5303
5397
  get items() {
5304
- return this.options.list.items;
5398
+ return this.#listStateOverride?.items ?? this.options.list.items;
5305
5399
  }
5306
5400
  /** Replaces the current item collection. */
5307
5401
  set items(value) {
@@ -5309,6 +5403,7 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
5309
5403
  }
5310
5404
  /** Renders the current visible window. */
5311
5405
  render(feedback) {
5406
+ this.#drainPendingListStateChanges();
5312
5407
  this.#jumpController.beforeFrame();
5313
5408
  this.#jumpController.noteViewportWidth(this.graphics.canvas.clientWidth);
5314
5409
  const now = getNow();
@@ -5332,6 +5427,7 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
5332
5427
  }
5333
5428
  /** Hit-tests the current visible window. */
5334
5429
  hittest(test) {
5430
+ this.#drainPendingListStateChanges();
5335
5431
  this.#jumpController.beforeFrame();
5336
5432
  this.#jumpController.noteViewportWidth(this.graphics.canvas.clientWidth);
5337
5433
  const now = getNow();
@@ -5378,12 +5474,7 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
5378
5474
  }
5379
5475
  _resetRenderFeedback(feedback) {
5380
5476
  if (feedback == null) return;
5381
- feedback.minIdx = NaN;
5382
- feedback.maxIdx = NaN;
5383
- feedback.min = NaN;
5384
- feedback.max = NaN;
5385
- feedback.canAutoFollowTop = false;
5386
- feedback.canAutoFollowBottom = false;
5477
+ initRenderFeedback(feedback);
5387
5478
  }
5388
5479
  _accumulateRenderFeedback(feedback, idx, top, height) {
5389
5480
  const visibleRange = this._readVisibleRange(top, height);
@@ -5514,6 +5605,7 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
5514
5605
  }
5515
5606
  #handleDeleteComplete(item) {
5516
5607
  finalizeInternalListDelete(this.options.list, item);
5608
+ this.#drainPendingListStateChanges();
5517
5609
  }
5518
5610
  #getTransitionLifecycleAdapter() {
5519
5611
  return {
@@ -5523,7 +5615,10 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
5523
5615
  readScrollState: this._readListState.bind(this),
5524
5616
  readItemIndex: (item) => this.items.indexOf(item),
5525
5617
  snapItemToViewportBoundary: this.#snapItemToViewportBoundary.bind(this),
5526
- onTransitionSettleScrollAdjusted: () => this.#jumpController.reconcileAutoFollowAfterTransitionSettle()
5618
+ onTransitionSettleScrollAdjusted: () => this.#jumpController.reconcileAutoFollowAfterTransitionSettle(),
5619
+ beginAutoFollowBoundaryObservation: (boundary) => this.#jumpController.beginAutoFollowBoundaryObservation(boundary),
5620
+ endAutoFollowBoundaryObservation: (boundary) => this.#jumpController.endAutoFollowBoundaryObservation(boundary),
5621
+ invalidateAutoFollowBoundary: (boundary) => this.#jumpController.invalidateAutoFollowBoundary(boundary)
5527
5622
  };
5528
5623
  }
5529
5624
  #getVirtualizedRuntime() {
@@ -5557,9 +5652,25 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
5557
5652
  anchorMode: this._getLayoutOptions().anchorMode
5558
5653
  };
5559
5654
  }
5560
- #handleListStateChange(change) {
5561
- const nextChange = this.#jumpController.handleListStateChange(change);
5562
- this.#transitionController.handleListStateChange(nextChange, this.#getTransitionPlanningAdapter(), this.#getTransitionLifecycleAdapter());
5655
+ #handleListStateChange(change, now = getNow(), snapshot) {
5656
+ this.#listStateOverride = snapshot == null ? void 0 : {
5657
+ items: [...snapshot.items],
5658
+ position: snapshot.position,
5659
+ offset: snapshot.offset
5660
+ };
5661
+ try {
5662
+ const nextChange = this.#jumpController.handleListStateChange(change, now);
5663
+ this.#transitionController.handleListStateChange(nextChange, this.#getTransitionPlanningAdapter(), this.#getTransitionLifecycleAdapter(), now);
5664
+ } finally {
5665
+ this.#listStateOverride = void 0;
5666
+ }
5667
+ }
5668
+ #drainPendingListStateChanges() {
5669
+ while (true) {
5670
+ const changes = drainInternalListStateChanges(this.options.list);
5671
+ if (changes.length === 0) return;
5672
+ for (const change of changes) this.#handleListStateChange(change, readInternalListStateChangeTime(change), readInternalListStateChangeSnapshot(change));
5673
+ }
5563
5674
  }
5564
5675
  };
5565
5676
  //#endregion
@@ -5679,6 +5790,6 @@ var ListRenderer = class extends VirtualizedRenderer {
5679
5790
  }
5680
5791
  };
5681
5792
  //#endregion
5682
- export { BaseRenderer, DebugRenderer, Fixed, Flex, FlexItem, Group, ListRenderer, ListState, MultilineText, PaddingBox, Place, ShrinkWrap, Text, VirtualizedRenderer, Wrapper, memoRenderItem, memoRenderItemBy };
5793
+ export { BaseRenderer, DebugRenderer, Fixed, Flex, FlexItem, Group, ListRenderer, ListState, MultilineText, PaddingBox, Place, ShrinkWrap, Text, VirtualizedRenderer, Wrapper, initRenderFeedback, memoRenderItem, memoRenderItemBy };
5683
5794
 
5684
5795
  //# sourceMappingURL=index.mjs.map