chat-layout 1.2.0-6 → 1.2.0-7

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
@@ -3596,6 +3596,40 @@ const listStateListenerRegistry = typeof FinalizationRegistry === "function" ? n
3596
3596
  if (list == null) return;
3597
3597
  deleteListStateListener(list, token);
3598
3598
  }) : null;
3599
+ const listScrollMutations = /* @__PURE__ */ new WeakMap();
3600
+ const WRITE_LIST_SCROLL_STATE = Symbol("writeListScrollState");
3601
+ function normalizePosition(value) {
3602
+ return typeof value === "number" && Number.isFinite(value) ? Math.trunc(value) : void 0;
3603
+ }
3604
+ function normalizeOffset$1(value) {
3605
+ return Number.isFinite(value) ? value : 0;
3606
+ }
3607
+ function getListScrollMutationRecord(list) {
3608
+ let record = listScrollMutations.get(list);
3609
+ if (record == null) {
3610
+ record = {
3611
+ version: 0,
3612
+ source: "internal"
3613
+ };
3614
+ listScrollMutations.set(list, record);
3615
+ }
3616
+ return record;
3617
+ }
3618
+ function markListScrollMutation(list, source) {
3619
+ const record = getListScrollMutationRecord(list);
3620
+ record.version += 1;
3621
+ record.source = source;
3622
+ }
3623
+ function readListScrollMutation(list) {
3624
+ const record = getListScrollMutationRecord(list);
3625
+ return {
3626
+ version: record.version,
3627
+ source: record.source
3628
+ };
3629
+ }
3630
+ function writeInternalListScrollState(list, state) {
3631
+ list[WRITE_LIST_SCROLL_STATE](state, "internal");
3632
+ }
3599
3633
  function deleteListStateListener(list, token) {
3600
3634
  const listeners = listStateListeners.get(list);
3601
3635
  if (listeners == null) return;
@@ -3660,17 +3694,28 @@ function normalizeInsertAnimation(animation) {
3660
3694
  const duration = normalizeInsertAnimationDuration(animation?.duration, animation != null);
3661
3695
  if (duration == null) return;
3662
3696
  const normalizedAnimation = { duration };
3663
- if (typeof animation?.distance === "number" && Number.isFinite(animation.distance)) normalizedAnimation.distance = Math.max(0, animation.distance);
3664
3697
  if (animation?.autoFollow === true) normalizedAnimation.autoFollow = true;
3665
3698
  return normalizedAnimation;
3666
3699
  }
3667
3700
  var ListState = class {
3668
3701
  #items;
3669
3702
  #pendingDeletes = /* @__PURE__ */ new Set();
3703
+ #offset = 0;
3704
+ #position;
3670
3705
  /** Pixel offset from the anchored item edge. */
3671
- offset = 0;
3706
+ get offset() {
3707
+ return this.#offset;
3708
+ }
3709
+ set offset(value) {
3710
+ this.#writeScrollState({ offset: normalizeOffset$1(value) }, "external");
3711
+ }
3672
3712
  /** Anchor item index, or `undefined` to use the renderer default. */
3673
- position;
3713
+ get position() {
3714
+ return this.#position;
3715
+ }
3716
+ set position(value) {
3717
+ this.#writeScrollState({ position: normalizePosition(value) }, "external");
3718
+ }
3674
3719
  /** Items currently managed by the renderer. */
3675
3720
  get items() {
3676
3721
  return this.#items;
@@ -3700,7 +3745,7 @@ var ListState = class {
3700
3745
  if (items.length === 0) return;
3701
3746
  assertUniqueItemReferences(items, this.#items);
3702
3747
  const normalizedAnimation = normalizeInsertAnimation(animation);
3703
- if (this.position != null) this.position += items.length;
3748
+ if (this.position != null) this.#writeScrollState({ position: this.position + items.length }, "internal");
3704
3749
  this.#items = items.concat(this.#items);
3705
3750
  emitListStateChange(this, {
3706
3751
  type: "unshift",
@@ -3772,12 +3817,13 @@ var ListState = class {
3772
3817
  this.#pendingDeletes.delete(item);
3773
3818
  if (index < 0) return;
3774
3819
  this.#items.splice(index, 1);
3775
- if (this.#items.length === 0) {
3776
- this.position = void 0;
3777
- this.offset = 0;
3778
- } else if (this.position != null) {
3779
- if (this.position > index) this.position -= 1;
3780
- else if (this.position === index) this.position = Math.min(index, this.#items.length - 1);
3820
+ if (this.#items.length === 0) this.#writeScrollState({
3821
+ position: void 0,
3822
+ offset: 0
3823
+ }, "internal");
3824
+ else if (this.position != null) {
3825
+ if (this.position > index) this.#writeScrollState({ position: this.position - 1 }, "internal");
3826
+ else if (this.position === index) this.#writeScrollState({ position: Math.min(index, this.#items.length - 1) }, "internal");
3781
3827
  }
3782
3828
  emitListStateChange(this, {
3783
3829
  type: "delete-finalize",
@@ -3788,8 +3834,10 @@ var ListState = class {
3788
3834
  * Sets the current anchor item and pixel offset.
3789
3835
  */
3790
3836
  setAnchor(position, offset = 0) {
3791
- this.position = Number.isFinite(position) ? Math.trunc(position) : void 0;
3792
- this.offset = Number.isFinite(offset) ? offset : 0;
3837
+ this.#writeScrollState({
3838
+ position: normalizePosition(position),
3839
+ offset: normalizeOffset$1(offset)
3840
+ }, "external");
3793
3841
  }
3794
3842
  /**
3795
3843
  * Replaces all items and clears scroll state.
@@ -3799,18 +3847,44 @@ var ListState = class {
3799
3847
  assertUniqueItemReferences(nextItems);
3800
3848
  this.#items = nextItems;
3801
3849
  this.#pendingDeletes.clear();
3802
- this.offset = 0;
3803
- this.position = void 0;
3850
+ this.#writeScrollState({
3851
+ position: void 0,
3852
+ offset: 0
3853
+ }, "internal");
3804
3854
  emitListStateChange(this, { type: "reset" });
3805
3855
  }
3806
3856
  /** Clears the current scroll anchor while keeping the items. */
3807
3857
  resetScroll() {
3808
- this.offset = 0;
3809
- this.position = void 0;
3858
+ this.#writeScrollState({
3859
+ position: void 0,
3860
+ offset: 0
3861
+ }, "external");
3810
3862
  }
3811
3863
  /** Applies a relative pixel scroll delta. */
3812
3864
  applyScroll(delta) {
3813
- this.offset += delta;
3865
+ this.#writeScrollState({ offset: this.#offset + delta }, "external");
3866
+ }
3867
+ [WRITE_LIST_SCROLL_STATE](patch, source) {
3868
+ this.#writeScrollState(patch, source);
3869
+ }
3870
+ #writeScrollState(patch, source) {
3871
+ let changed = false;
3872
+ if ("position" in patch) {
3873
+ const nextPosition = normalizePosition(patch.position);
3874
+ if (!Object.is(this.#position, nextPosition)) {
3875
+ this.#position = nextPosition;
3876
+ changed = true;
3877
+ }
3878
+ }
3879
+ if ("offset" in patch) {
3880
+ const nextOffset = normalizeOffset$1(patch.offset ?? 0);
3881
+ if (!Object.is(this.#offset, nextOffset)) {
3882
+ this.#offset = nextOffset;
3883
+ changed = true;
3884
+ }
3885
+ }
3886
+ if (!changed) return;
3887
+ markListScrollMutation(this, source);
3814
3888
  }
3815
3889
  };
3816
3890
  //#endregion
@@ -3875,11 +3949,15 @@ function memoRenderItemBy(keyOf, renderItem, options = {}) {
3875
3949
  }
3876
3950
  //#endregion
3877
3951
  //#region src/renderer/virtualized/base-animation.ts
3952
+ const CONTROLLED_STATE_OFFSET_EPSILON = 1e-9;
3878
3953
  function clamp$1(value, min, max) {
3879
3954
  return Math.min(Math.max(value, min), max);
3880
3955
  }
3881
3956
  function sameState(state, position, offset) {
3882
- return Object.is(state.position, position) && Object.is(state.offset, offset);
3957
+ if (!Object.is(state.position, position)) return false;
3958
+ if (Object.is(state.offset, offset)) return true;
3959
+ if (!Number.isFinite(state.offset) || !Number.isFinite(offset)) return false;
3960
+ return Math.abs(state.offset - offset) <= CONTROLLED_STATE_OFFSET_EPSILON;
3883
3961
  }
3884
3962
  function resolveJumpSegmentIndex(anchor, direction, itemCount) {
3885
3963
  if (itemCount <= 0) return;
@@ -3964,145 +4042,190 @@ function getNow() {
3964
4042
  //#region src/renderer/virtualized/frame-session.ts
3965
4043
  function prepareFrameSession(params) {
3966
4044
  let solution = params.resolveVisibleWindow(params.now);
3967
- let viewportTranslateY = params.getViewportTranslateY(params.now);
3968
- params.captureVisibleItemSnapshot(solution, viewportTranslateY);
4045
+ params.captureVisibleItemSnapshot(solution);
3969
4046
  const requestSettleRedraw = params.pruneTransitionAnimations(solution.window, params.now);
3970
4047
  if (requestSettleRedraw) {
3971
4048
  solution = params.resolveVisibleWindow(params.now);
3972
- viewportTranslateY = params.getViewportTranslateY(params.now);
3973
- params.captureVisibleItemSnapshot(solution, viewportTranslateY);
4049
+ params.captureVisibleItemSnapshot(solution);
3974
4050
  }
3975
4051
  return {
3976
4052
  solution,
3977
- viewportTranslateY,
3978
4053
  requestSettleRedraw
3979
4054
  };
3980
4055
  }
3981
4056
  //#endregion
3982
4057
  //#region src/renderer/virtualized/jump-controller.ts
3983
- var JumpController = class {
3984
- #confirmedAutoFollowTop = false;
3985
- #confirmedAutoFollowBottom = false;
3986
- #controlledState;
4058
+ var JumpController = class JumpController {
4059
+ static TRANSITION_SETTLE_SNAP_DURATION = 120;
4060
+ #canAutoFollowTop = false;
4061
+ #canAutoFollowBottom = false;
4062
+ #pendingAutoFollowRecomputeTop = true;
4063
+ #pendingAutoFollowRecomputeBottom = true;
4064
+ #pendingAutoFollowRecomputeReasonTop = "init";
4065
+ #pendingAutoFollowRecomputeReasonBottom = "init";
4066
+ #pendingTransitionSettleReconcile = false;
4067
+ #lastArmedAutoFollowBoundary;
4068
+ #lastObservedRenderedAutoFollowTop = false;
4069
+ #lastObservedRenderedAutoFollowBottom = false;
4070
+ #lastViewportWidth;
4071
+ #lastHandledScrollMutationVersion;
3987
4072
  #jumpAnimation;
3988
- #lastCommittedState;
3989
- #hasPendingListChange = false;
3990
- #pendingBoundaryJumpTop = false;
3991
- #pendingBoundaryJumpBottom = false;
4073
+ #pendingPostJumpBoundary;
4074
+ #pendingPostJumpBoundaryBlocked = false;
3992
4075
  #options;
3993
4076
  constructor(options) {
3994
4077
  this.#options = options;
4078
+ this.#lastHandledScrollMutationVersion = this.#options.readScrollMutation().version;
3995
4079
  }
3996
4080
  beforeFrame() {
3997
- const currentState = this.#options.readListState();
3998
- if (!this.#hasPendingListChange && this.#jumpAnimation == null && this.#lastCommittedState != null && !sameState(this.#lastCommittedState, currentState.position, currentState.offset)) this.#clearPendingBoundaryJumps();
3999
- this.#hasPendingListChange = false;
4081
+ this.#handlePendingExternalScrollMutation();
4082
+ }
4083
+ noteViewportWidth(width) {
4084
+ if (!Number.isFinite(width)) return;
4085
+ if (this.#lastViewportWidth == null) {
4086
+ this.#lastViewportWidth = width;
4087
+ return;
4088
+ }
4089
+ if (Object.is(this.#lastViewportWidth, width)) return;
4090
+ this.#lastViewportWidth = width;
4091
+ this.#clearPendingPostJumpBoundary();
4092
+ this.#clearPendingTransitionSettleReconcile();
4093
+ this.#markAutoFollowRecompute(void 0, "viewport-width-change");
4000
4094
  }
4001
4095
  prepare(now) {
4096
+ if (this.#handlePendingExternalScrollMutation()) return false;
4002
4097
  const animation = this.#jumpAnimation;
4003
4098
  if (animation == null) return false;
4004
4099
  if (this.#options.getItemCount() === 0) {
4005
4100
  this.#cancelJumpAnimation();
4006
4101
  return false;
4007
4102
  }
4008
- if (this.#controlledState != null && !sameState(this.#controlledState, this.#options.readListState().position, this.#options.readListState().offset)) {
4009
- this.#clearPendingBoundaryJumps();
4010
- this.#cancelJumpAnimation();
4011
- return false;
4012
- }
4013
4103
  const progress = getProgress(animation.startTime, animation.duration, now);
4014
4104
  const eased = progress >= 1 ? 1 : smoothstep(progress);
4015
4105
  const anchor = getAnchorAtDistance(animation.path, animation.path.totalDistance * eased);
4016
4106
  this.#options.applyAnchor(anchor);
4017
4107
  animation.needsMoreFrames = progress < 1;
4108
+ if (!animation.needsMoreFrames && this.#pendingPostJumpBoundary != null && !this.#pendingPostJumpBoundaryBlocked) this.#armAutoFollowBoundary(this.#pendingPostJumpBoundary, "jump-to-boundary-settle");
4018
4109
  return animation.needsMoreFrames;
4019
4110
  }
4020
4111
  finishFrame(requestRedraw) {
4021
4112
  const animation = this.#jumpAnimation;
4022
4113
  if (animation == null) return requestRedraw;
4023
- if (animation.needsMoreFrames) {
4024
- this.#controlledState = this.#options.readListState();
4025
- return true;
4026
- }
4114
+ if (animation.needsMoreFrames) return true;
4115
+ const boundary = this.#pendingPostJumpBoundaryBlocked === true ? void 0 : this.#pendingPostJumpBoundary;
4027
4116
  const onComplete = animation.onComplete;
4028
4117
  this.#cancelJumpAnimation();
4118
+ this.#clearPendingPostJumpBoundary();
4119
+ if (boundary != null) this.#armAutoFollowBoundary(boundary, "jump-to-boundary-settle");
4029
4120
  onComplete?.();
4030
4121
  return requestRedraw || this.#jumpAnimation != null;
4031
4122
  }
4032
4123
  commit(state) {
4033
- this.#lastCommittedState = {
4034
- position: state.position,
4035
- offset: state.offset
4036
- };
4124
+ this.#lastHandledScrollMutationVersion = this.#options.readScrollMutation().version;
4037
4125
  }
4038
4126
  jumpTo(index, options = {}) {
4039
- this.#clearPendingBoundaryJumps();
4127
+ this.#clearPendingTransitionSettleReconcile();
4128
+ this.#clearPendingPostJumpBoundary();
4040
4129
  if (this.#options.getItemCount() === 0) {
4041
4130
  this.#cancelJumpAnimation();
4042
4131
  return;
4043
4132
  }
4044
- this.#startJumpToIndex(index, options, { kind: "manual" });
4133
+ this.#startJumpToIndex(index, options);
4045
4134
  }
4046
4135
  jumpToBoundary(boundary, options = {}) {
4047
- this.#clearPendingBoundaryJumps();
4136
+ this.#clearPendingTransitionSettleReconcile();
4137
+ this.#clearPendingPostJumpBoundary();
4138
+ this.#armAutoFollowBoundary(boundary, "jump-to-boundary");
4048
4139
  if (this.#options.getItemCount() === 0) {
4049
4140
  this.#cancelJumpAnimation();
4050
4141
  return;
4051
4142
  }
4052
- this.#armBoundaryJump(boundary);
4053
4143
  this.#startJumpToIndex(boundary === "bottom" ? this.#options.getItemCount() - 1 : 0, {
4054
4144
  ...options,
4055
4145
  block: boundary === "bottom" ? "end" : "start"
4056
- }, {
4057
- kind: "boundary-jump",
4058
- boundary
4059
4146
  });
4060
4147
  }
4061
- syncAutoFollowCapabilities(capabilities) {
4062
- this.#confirmedAutoFollowTop = capabilities.top;
4063
- this.#confirmedAutoFollowBottom = capabilities.bottom;
4064
- this.#clearPendingBoundaryJumps();
4065
- return this.getEffectiveAutoFollowCapabilities();
4148
+ recomputeAutoFollowCapabilities(capabilities) {
4149
+ const previouslyObservedDualBoundary = this.#lastObservedRenderedAutoFollowTop && this.#lastObservedRenderedAutoFollowBottom;
4150
+ if (capabilities.top && capabilities.bottom && !previouslyObservedDualBoundary) {
4151
+ this.#setAutoFollowBoundary("top", true, "dual-boundary-promotion");
4152
+ this.#setAutoFollowBoundary("bottom", true, "dual-boundary-promotion");
4153
+ }
4154
+ if (this.#pendingAutoFollowRecomputeTop) {
4155
+ this.#setAutoFollowBoundary("top", capabilities.top, `strict-recompute:${this.#pendingAutoFollowRecomputeReasonTop}`);
4156
+ this.#pendingAutoFollowRecomputeTop = false;
4157
+ }
4158
+ if (this.#pendingAutoFollowRecomputeBottom) {
4159
+ this.#setAutoFollowBoundary("bottom", capabilities.bottom, `strict-recompute:${this.#pendingAutoFollowRecomputeReasonBottom}`);
4160
+ this.#pendingAutoFollowRecomputeBottom = false;
4161
+ }
4162
+ this.#syncLastArmedBoundaryFromLatchedState();
4163
+ if (this.#pendingTransitionSettleReconcile) {
4164
+ this.#reconcileLatchedAutoFollowAfterTransitionSettle(capabilities);
4165
+ this.#pendingTransitionSettleReconcile = false;
4166
+ }
4167
+ this.#lastObservedRenderedAutoFollowTop = capabilities.top;
4168
+ this.#lastObservedRenderedAutoFollowBottom = capabilities.bottom;
4169
+ return this.getAutoFollowCapabilities();
4066
4170
  }
4067
- getEffectiveAutoFollowCapabilities() {
4171
+ getAutoFollowCapabilities() {
4068
4172
  return {
4069
- top: this.#hasEffectiveAutoFollowCapability("top"),
4070
- bottom: this.#hasEffectiveAutoFollowCapability("bottom")
4173
+ top: this.#canAutoFollowTop,
4174
+ bottom: this.#canAutoFollowBottom
4071
4175
  };
4072
4176
  }
4177
+ reconcileAutoFollowAfterTransitionSettle() {
4178
+ this.#pendingTransitionSettleReconcile = true;
4179
+ }
4073
4180
  handleListStateChange(change) {
4074
- this.#hasPendingListChange = true;
4181
+ switch (change.type) {
4182
+ case "reset":
4183
+ case "set":
4184
+ this.#cancelJumpAnimation();
4185
+ this.#clearPendingPostJumpBoundary();
4186
+ this.#clearPendingTransitionSettleReconcile();
4187
+ this.#syncScrollMutationVersion();
4188
+ this.#markAutoFollowRecompute(void 0, change.type);
4189
+ return change;
4190
+ case "push":
4191
+ case "unshift": return this.#handleBoundaryInsert(change);
4192
+ default: return change;
4193
+ }
4194
+ }
4195
+ #handleBoundaryInsert(change) {
4196
+ if (this.#handlePendingExternalScrollMutation()) return change;
4197
+ this.#clearPendingTransitionSettleReconcile();
4075
4198
  const followChange = this.#resolveAutoFollowChange(change);
4076
- const canChainAutoFollow = followChange != null ? this.#shouldChainAutoFollow(followChange.boundary) : false;
4077
- const canCapabilityAutoFollow = followChange != null ? this.#shouldAutoFollowFromCapability(followChange.boundary, followChange.direction, followChange.count) : false;
4078
- if (followChange != null && (canChainAutoFollow || canCapabilityAutoFollow)) {
4079
- if (canChainAutoFollow) this.#rebaseJumpAnchorForBoundaryInsert(followChange.direction, followChange.count, getNow());
4080
- this.#startJumpToIndex(followChange.boundary === "bottom" ? this.#options.getItemCount() - 1 : 0, {
4081
- block: followChange.boundary === "bottom" ? "end" : "start",
4082
- duration: followChange.animation?.duration
4083
- }, {
4084
- kind: "auto-follow",
4085
- boundary: followChange.boundary
4086
- });
4087
- return {
4088
- ...followChange.change,
4089
- animation: void 0
4090
- };
4199
+ const boundary = change.type === "push" ? "bottom" : "top";
4200
+ if (this.#pendingPostJumpBoundary === boundary) this.#pendingPostJumpBoundaryBlocked = true;
4201
+ if (followChange == null || !this.#hasAutoFollowCapability(followChange.boundary)) return change;
4202
+ if (this.#canAutoFollowTop && this.#canAutoFollowBottom && this.#lastObservedRenderedAutoFollowTop && this.#lastObservedRenderedAutoFollowBottom) {
4203
+ const otherBoundary = followChange.boundary === "top" ? "bottom" : "top";
4204
+ this.#setAutoFollowBoundary(otherBoundary, false, "boundary-insert-narrow");
4205
+ this.#lastArmedAutoFollowBoundary = followChange.boundary;
4091
4206
  }
4207
+ this.#clearPendingPostJumpBoundary();
4208
+ this.#materializeAnimatedAnchor(getNow(), followChange.direction, followChange.count);
4209
+ this.#startJumpToIndex(followChange.boundary === "bottom" ? this.#options.getItemCount() - 1 : 0, {
4210
+ block: followChange.boundary === "bottom" ? "end" : "start",
4211
+ duration: followChange.animation?.duration
4212
+ });
4092
4213
  return change;
4093
4214
  }
4094
4215
  #cancelJumpAnimation() {
4095
4216
  this.#jumpAnimation = void 0;
4096
- this.#controlledState = void 0;
4097
4217
  }
4098
- #startJumpToIndex(index, options, source) {
4218
+ #startJumpToIndex(index, options) {
4099
4219
  const targetIndex = this.#options.clampItemIndex(index);
4100
- const currentState = this.#options.normalizeListState(this.#options.readListState());
4101
4220
  const targetBlock = options.block ?? this.#options.getDefaultJumpBlock();
4221
+ const settleBoundary = this.#resolveBoundaryLatchTarget(targetIndex, targetBlock);
4222
+ this.#materializeAnimatedAnchor(getNow());
4223
+ const currentState = this.#options.normalizeListState(this.#options.readListState());
4102
4224
  const targetAnchor = this.#options.getTargetAnchor(targetIndex, targetBlock);
4103
4225
  if (!(options.animated ?? true)) {
4104
4226
  this.#cancelJumpAnimation();
4105
4227
  this.#options.applyAnchor(targetAnchor);
4228
+ if (settleBoundary != null) this.#armAutoFollowBoundary(settleBoundary, "jump-to-boundary-instant");
4106
4229
  options.onComplete?.();
4107
4230
  return;
4108
4231
  }
@@ -4110,6 +4233,7 @@ var JumpController = class {
4110
4233
  if (!Number.isFinite(startAnchor)) {
4111
4234
  this.#cancelJumpAnimation();
4112
4235
  this.#options.applyAnchor(targetAnchor);
4236
+ if (settleBoundary != null) this.#armAutoFollowBoundary(settleBoundary, "jump-to-boundary-instant");
4113
4237
  options.onComplete?.();
4114
4238
  return;
4115
4239
  }
@@ -4118,18 +4242,27 @@ var JumpController = class {
4118
4242
  if (duration <= 0 || path.totalDistance <= Number.EPSILON) {
4119
4243
  this.#cancelJumpAnimation();
4120
4244
  this.#options.applyAnchor(targetAnchor);
4245
+ if (settleBoundary != null) this.#armAutoFollowBoundary(settleBoundary, "jump-to-boundary-instant");
4121
4246
  options.onComplete?.();
4122
4247
  return;
4123
4248
  }
4249
+ if (settleBoundary != null) {
4250
+ this.#pendingPostJumpBoundary = settleBoundary;
4251
+ this.#pendingPostJumpBoundaryBlocked = false;
4252
+ }
4124
4253
  this.#jumpAnimation = {
4125
4254
  path,
4126
4255
  startTime: getNow(),
4127
4256
  duration,
4128
4257
  needsMoreFrames: true,
4129
- onComplete: options.onComplete,
4130
- source
4258
+ onComplete: options.onComplete
4131
4259
  };
4132
- this.#controlledState = this.#options.readListState();
4260
+ }
4261
+ #resolveBoundaryLatchTarget(index, block) {
4262
+ const itemCount = this.#options.getItemCount();
4263
+ if (itemCount <= 0) return;
4264
+ if (index === 0 && block === "start") return "top";
4265
+ if (index === itemCount - 1 && block === "end") return "bottom";
4133
4266
  }
4134
4267
  #resolveAutoFollowChange(change) {
4135
4268
  switch (change.type) {
@@ -4144,45 +4277,95 @@ var JumpController = class {
4144
4277
  default: return;
4145
4278
  }
4146
4279
  }
4147
- #shouldAutoFollowFromCapability(boundary, direction, count) {
4148
- return this.#hasEffectiveAutoFollowCapability(boundary) && this.#matchesLastCommittedStateAfterBoundaryInsert(direction, count);
4280
+ #hasAutoFollowCapability(boundary) {
4281
+ return boundary === "top" ? this.#canAutoFollowTop : this.#canAutoFollowBottom;
4149
4282
  }
4150
- #shouldChainAutoFollow(boundary) {
4151
- return this.#readJumpBoundary() === boundary;
4283
+ #armAutoFollowBoundary(boundary, reason) {
4284
+ this.#setAutoFollowBoundary(boundary, true, reason);
4285
+ this.#lastArmedAutoFollowBoundary = boundary;
4286
+ if (boundary === "top") {
4287
+ this.#pendingAutoFollowRecomputeTop = false;
4288
+ return;
4289
+ }
4290
+ this.#pendingAutoFollowRecomputeBottom = false;
4152
4291
  }
4153
- #rebaseJumpAnchorForBoundaryInsert(direction, count, now) {
4292
+ #markAutoFollowRecompute(boundary, reason) {
4293
+ if (boundary == null || boundary === "top") {
4294
+ this.#pendingAutoFollowRecomputeTop = true;
4295
+ this.#pendingAutoFollowRecomputeReasonTop = reason;
4296
+ }
4297
+ if (boundary == null || boundary === "bottom") {
4298
+ this.#pendingAutoFollowRecomputeBottom = true;
4299
+ this.#pendingAutoFollowRecomputeReasonBottom = reason;
4300
+ }
4301
+ }
4302
+ #clearPendingPostJumpBoundary() {
4303
+ this.#pendingPostJumpBoundary = void 0;
4304
+ this.#pendingPostJumpBoundaryBlocked = false;
4305
+ }
4306
+ #clearPendingTransitionSettleReconcile() {
4307
+ this.#pendingTransitionSettleReconcile = false;
4308
+ }
4309
+ #materializeAnimatedAnchor(now, direction, count = 0) {
4154
4310
  const animation = this.#jumpAnimation;
4155
4311
  if (animation == null) return;
4156
4312
  const progress = getProgress(animation.startTime, animation.duration, now);
4157
4313
  const eased = progress >= 1 ? 1 : smoothstep(progress);
4158
- const anchorAtNow = getAnchorAtDistance(animation.path, animation.path.totalDistance * eased);
4314
+ let anchor = getAnchorAtDistance(animation.path, animation.path.totalDistance * eased);
4315
+ if (direction === "unshift") anchor += count;
4159
4316
  this.#cancelJumpAnimation();
4160
- this.#options.applyAnchor(direction === "unshift" ? anchorAtNow + count : anchorAtNow);
4317
+ this.#options.applyAnchor(anchor);
4161
4318
  }
4162
- #matchesLastCommittedStateAfterBoundaryInsert(direction, count) {
4163
- const state = this.#lastCommittedState;
4164
- if (state == null) return false;
4165
- return sameState({
4166
- position: direction === "unshift" && state.position != null ? state.position + count : state.position,
4167
- offset: state.offset
4168
- }, this.#options.readListState().position, this.#options.readListState().offset);
4319
+ #setAutoFollowBoundary(boundary, value, reason) {
4320
+ if (boundary === "top") this.#canAutoFollowTop = value;
4321
+ else this.#canAutoFollowBottom = value;
4322
+ }
4323
+ #syncLastArmedBoundaryFromLatchedState() {
4324
+ if (this.#canAutoFollowTop === this.#canAutoFollowBottom) return;
4325
+ this.#lastArmedAutoFollowBoundary = this.#canAutoFollowTop ? "top" : "bottom";
4326
+ }
4327
+ #reconcileLatchedAutoFollowAfterTransitionSettle(capabilities) {
4328
+ if (!this.#canAutoFollowTop && !this.#canAutoFollowBottom) return;
4329
+ const preferredBoundary = this.#resolvePreferredLatchedBoundary(capabilities);
4330
+ if (preferredBoundary == null) return;
4331
+ const otherBoundary = preferredBoundary === "top" ? "bottom" : "top";
4332
+ if (this.#hasAutoFollowCapability(otherBoundary)) this.#setAutoFollowBoundary(otherBoundary, false, "strict-recompute:set");
4333
+ if (!this.#readCapabilityForBoundary(capabilities, preferredBoundary)) this.#startTransitionSettleSnap(preferredBoundary);
4334
+ this.#syncLastArmedBoundaryFromLatchedState();
4335
+ }
4336
+ #resolvePreferredLatchedBoundary(capabilities) {
4337
+ if (this.#canAutoFollowTop && this.#canAutoFollowBottom) {
4338
+ if (capabilities.top && capabilities.bottom) return;
4339
+ if (this.#lastArmedAutoFollowBoundary != null) return this.#lastArmedAutoFollowBoundary;
4340
+ if (capabilities.top !== capabilities.bottom) return capabilities.top ? "top" : "bottom";
4341
+ return "bottom";
4342
+ }
4343
+ if (this.#canAutoFollowTop) return "top";
4344
+ if (this.#canAutoFollowBottom) return "bottom";
4169
4345
  }
4170
- #hasEffectiveAutoFollowCapability(boundary) {
4171
- const animationBoundary = this.#readJumpBoundary();
4172
- return boundary === "top" ? this.#confirmedAutoFollowTop || this.#pendingBoundaryJumpTop || animationBoundary === "top" : this.#confirmedAutoFollowBottom || this.#pendingBoundaryJumpBottom || animationBoundary === "bottom";
4346
+ #readCapabilityForBoundary(capabilities, boundary) {
4347
+ return boundary === "top" ? capabilities.top : capabilities.bottom;
4173
4348
  }
4174
- #readJumpBoundary() {
4175
- const source = this.#jumpAnimation?.source;
4176
- if (source == null || source.kind === "manual") return;
4177
- return source.boundary;
4349
+ #startTransitionSettleSnap(boundary) {
4350
+ if (this.#options.getItemCount() <= 0) return;
4351
+ this.#startJumpToIndex(boundary === "bottom" ? this.#options.getItemCount() - 1 : 0, {
4352
+ block: boundary === "bottom" ? "end" : "start",
4353
+ duration: JumpController.TRANSITION_SETTLE_SNAP_DURATION
4354
+ });
4178
4355
  }
4179
- #armBoundaryJump(boundary) {
4180
- this.#pendingBoundaryJumpTop = boundary === "top";
4181
- this.#pendingBoundaryJumpBottom = boundary === "bottom";
4356
+ #syncScrollMutationVersion() {
4357
+ this.#lastHandledScrollMutationVersion = this.#options.readScrollMutation().version;
4182
4358
  }
4183
- #clearPendingBoundaryJumps() {
4184
- this.#pendingBoundaryJumpTop = false;
4185
- this.#pendingBoundaryJumpBottom = false;
4359
+ #handlePendingExternalScrollMutation() {
4360
+ const mutation = this.#options.readScrollMutation();
4361
+ if (mutation.version === this.#lastHandledScrollMutationVersion) return false;
4362
+ this.#lastHandledScrollMutationVersion = mutation.version;
4363
+ if (mutation.source !== "external") return false;
4364
+ this.#cancelJumpAnimation();
4365
+ this.#clearPendingPostJumpBoundary();
4366
+ this.#clearPendingTransitionSettleReconcile();
4367
+ this.#markAutoFollowRecompute(void 0, "manual-scroll");
4368
+ return true;
4186
4369
  }
4187
4370
  };
4188
4371
  //#endregion
@@ -4196,11 +4379,8 @@ var VisibilitySnapshot = class {
4196
4379
  #previousSnapshotState;
4197
4380
  #emptyState;
4198
4381
  #coversShortList = false;
4199
- #topGap = 0;
4200
- #bottomGap = 0;
4201
4382
  #atStartBoundary = false;
4202
4383
  #atEndBoundary = false;
4203
- #currentExtraShift = 0;
4204
4384
  #minDrawnIndex = Number.POSITIVE_INFINITY;
4205
4385
  #maxDrawnIndex = Number.NEGATIVE_INFINITY;
4206
4386
  #topBoundaryItem;
@@ -4208,18 +4388,12 @@ var VisibilitySnapshot = class {
4208
4388
  get coversShortList() {
4209
4389
  return this.#hasSnapshot && this.#snapshotState != null && this.#coversShortList;
4210
4390
  }
4211
- get topGap() {
4212
- return this.#topGap;
4213
- }
4214
- get bottomGap() {
4215
- return this.#bottomGap;
4391
+ get hasSnapshot() {
4392
+ return this.#hasSnapshot;
4216
4393
  }
4217
4394
  get previousState() {
4218
4395
  return this.#previousSnapshotState;
4219
4396
  }
4220
- get currentExtraShift() {
4221
- return this.#currentExtraShift;
4222
- }
4223
4397
  readDrawnIndexRange() {
4224
4398
  if (!Number.isFinite(this.#minDrawnIndex) || !Number.isFinite(this.#maxDrawnIndex)) return;
4225
4399
  return {
@@ -4230,7 +4404,7 @@ var VisibilitySnapshot = class {
4230
4404
  readBoundaryItem(boundary) {
4231
4405
  return boundary === "top" ? this.#topBoundaryItem : this.#bottomBoundaryItem;
4232
4406
  }
4233
- capture(window, _resolutionPath, items, viewportHeight, snapshotState, extraShift, readVisibleRange) {
4407
+ capture(window, _resolutionPath, items, viewport, snapshotState, readVisibleRange, readOuterVisibleRange) {
4234
4408
  this.#previousVisibleItems = this.#visibleItems;
4235
4409
  this.#previousSnapshotState = this.#snapshotState;
4236
4410
  const nextDrawnItems = /* @__PURE__ */ new Set();
@@ -4245,18 +4419,23 @@ var VisibilitySnapshot = class {
4245
4419
  let nextBottomBoundaryItem;
4246
4420
  let nextTopBoundaryY = Number.POSITIVE_INFINITY;
4247
4421
  let nextBottomBoundaryY = Number.NEGATIVE_INFINITY;
4248
- const effectiveShift = window.shift + extraShift;
4422
+ const effectiveShift = window.shift;
4423
+ const contentOriginY = viewport.contentTop;
4249
4424
  for (const { idx, offset, height } of window.drawList) {
4250
- minVisibleIndex = Math.min(minVisibleIndex, idx);
4251
- maxVisibleIndex = Math.max(maxVisibleIndex, idx);
4252
- nextMinDrawnIndex = Math.min(nextMinDrawnIndex, idx);
4253
- nextMaxDrawnIndex = Math.max(nextMaxDrawnIndex, idx);
4254
- const y = offset + effectiveShift;
4425
+ const y = offset + effectiveShift + contentOriginY;
4255
4426
  topMostY = Math.min(topMostY, y);
4256
4427
  bottomMostY = Math.max(bottomMostY, y + height);
4257
4428
  const item = items[idx];
4258
- if (item != null) {
4429
+ if (item != null && readOuterVisibleRange(y, height) != null) {
4259
4430
  nextDrawnItems.add(item);
4431
+ nextMinDrawnIndex = Math.min(nextMinDrawnIndex, idx);
4432
+ nextMaxDrawnIndex = Math.max(nextMaxDrawnIndex, idx);
4433
+ }
4434
+ if (item == null) continue;
4435
+ if (readVisibleRange(y, height) != null) {
4436
+ minVisibleIndex = Math.min(minVisibleIndex, idx);
4437
+ maxVisibleIndex = Math.max(maxVisibleIndex, idx);
4438
+ nextVisibleItems.add(item);
4260
4439
  if (y < nextTopBoundaryY) {
4261
4440
  nextTopBoundaryY = y;
4262
4441
  nextTopBoundaryItem = item;
@@ -4266,25 +4445,20 @@ var VisibilitySnapshot = class {
4266
4445
  nextBottomBoundaryItem = item;
4267
4446
  }
4268
4447
  }
4269
- if (item == null || readVisibleRange(y, height) == null) continue;
4270
- nextVisibleItems.add(item);
4271
4448
  }
4272
4449
  this.#drawnItems = nextDrawnItems;
4273
4450
  this.#visibleItems = nextVisibleItems;
4274
4451
  this.#hasSnapshot = true;
4275
4452
  this.#snapshotState = snapshotState;
4276
- this.#currentExtraShift = extraShift;
4277
4453
  this.#minDrawnIndex = nextMinDrawnIndex;
4278
4454
  this.#maxDrawnIndex = nextMaxDrawnIndex;
4279
4455
  this.#topBoundaryItem = nextTopBoundaryItem;
4280
4456
  this.#bottomBoundaryItem = nextBottomBoundaryItem;
4281
4457
  this.#emptyState = items.length === 0 && window.drawList.length === 0 ? snapshotState : void 0;
4282
4458
  const contentHeight = bottomMostY - topMostY;
4283
- this.#coversShortList = window.drawList.length > 0 && items.length > 0 && window.drawList.length === items.length && minVisibleIndex === 0 && maxVisibleIndex === items.length - 1 && topMostY >= -Number.EPSILON && bottomMostY <= viewportHeight + Number.EPSILON && contentHeight < viewportHeight - Number.EPSILON;
4284
- this.#topGap = this.#coversShortList ? Math.max(0, topMostY) : 0;
4285
- this.#bottomGap = this.#coversShortList ? Math.max(0, viewportHeight - bottomMostY) : 0;
4286
- this.#atStartBoundary = window.drawList.length > 0 && items.length > 0 && minVisibleIndex === 0 && topMostY >= -Number.EPSILON;
4287
- this.#atEndBoundary = window.drawList.length > 0 && items.length > 0 && maxVisibleIndex === items.length - 1 && bottomMostY <= viewportHeight + Number.EPSILON;
4459
+ this.#coversShortList = window.drawList.length > 0 && items.length > 0 && window.drawList.length === items.length && minVisibleIndex === 0 && maxVisibleIndex === items.length - 1 && topMostY >= viewport.contentTop - 1e-6 && bottomMostY <= viewport.contentBottom + 1e-6 && contentHeight < viewport.contentHeight - 1e-6;
4460
+ this.#atStartBoundary = window.drawList.length > 0 && items.length > 0 && minVisibleIndex === 0 && topMostY >= viewport.contentTop - 1e-6;
4461
+ this.#atEndBoundary = window.drawList.length > 0 && items.length > 0 && maxVisibleIndex === items.length - 1 && bottomMostY <= viewport.contentBottom + 1e-6;
4288
4462
  }
4289
4463
  matchesCurrentState(position, offset) {
4290
4464
  return this.#hasSnapshot && this.#snapshotState != null && sameState(this.#snapshotState, position, offset);
@@ -4324,11 +4498,8 @@ var VisibilitySnapshot = class {
4324
4498
  this.#previousSnapshotState = void 0;
4325
4499
  this.#emptyState = void 0;
4326
4500
  this.#coversShortList = false;
4327
- this.#topGap = 0;
4328
- this.#bottomGap = 0;
4329
4501
  this.#atStartBoundary = false;
4330
4502
  this.#atEndBoundary = false;
4331
- this.#currentExtraShift = 0;
4332
4503
  this.#minDrawnIndex = Number.POSITIVE_INFINITY;
4333
4504
  this.#maxDrawnIndex = Number.NEGATIVE_INFINITY;
4334
4505
  this.#topBoundaryItem = void 0;
@@ -4381,7 +4552,7 @@ var TransitionStore = class {
4381
4552
  }));
4382
4553
  }
4383
4554
  findInvisible(snapshot) {
4384
- return [...this.#transitions.entries()].filter(([item, transition]) => !snapshot.tracks(item, transition.retention)).map(([item, transition]) => ({
4555
+ return [...this.#transitions.entries()].filter(([item, transition]) => !snapshot.tracks(item, transition.retention) && !(transition.kind === "insert" && !snapshot.wasVisible(item))).map(([item, transition]) => ({
4385
4556
  item,
4386
4557
  transition
4387
4558
  }));
@@ -4395,9 +4566,6 @@ var TransitionStore = class {
4395
4566
  };
4396
4567
  //#endregion
4397
4568
  //#region src/renderer/virtualized/transition-planner.ts
4398
- function isFinitePositive(value) {
4399
- return Number.isFinite(value) && value > 0;
4400
- }
4401
4569
  function normalizeDuration(duration) {
4402
4570
  return Math.max(0, typeof duration === "number" && Number.isFinite(duration) ? duration : 0);
4403
4571
  }
@@ -4429,13 +4597,16 @@ function isIndexVisible(index, resolveVisibleWindow, readVisibleRange) {
4429
4597
  }
4430
4598
  function resolveAnimationEligibility(params) {
4431
4599
  if (params.index < 0) return false;
4432
- if (params.snapshot.matchesCurrentState(params.position, params.offset)) return params.snapshot.isVisible(params.item);
4433
- return isIndexVisible(params.index, params.resolveVisibleWindow, params.readVisibleRange);
4600
+ if (params.snapshot.matchesCurrentState(params.position, params.offset)) return params.snapshot.tracks(params.item, "drawn");
4601
+ return isIndexVisible(params.index, params.resolveVisibleWindow, params.readOuterVisibleRange);
4434
4602
  }
4435
- function resolveBoundaryInsertStrategy(direction, underflowAlign, coversShortListSnapshot) {
4436
- if (!coversShortListSnapshot) return "hard-cut";
4437
- if (direction === "push" && underflowAlign === "bottom" || direction === "unshift" && underflowAlign === "top") return "viewport-slide";
4438
- return "item-enter";
4603
+ function hasVisibleBoundaryInsertItems(direction, count, ctx) {
4604
+ if (count <= 0) return false;
4605
+ const start = direction === "push" ? ctx.items.length - count : 0;
4606
+ const end = direction === "push" ? ctx.items.length : Math.min(count, ctx.items.length);
4607
+ if (start < 0 || end <= start) return false;
4608
+ const solution = ctx.resolveVisibleWindow();
4609
+ return solution.window.drawList.some((entry) => entry.idx >= start && entry.idx < end && ctx.readOuterVisibleRange(entry.offset + solution.window.shift, entry.height) != null);
4439
4610
  }
4440
4611
  function sampleScalarAnimation(animation, now) {
4441
4612
  return interpolate(animation.from, animation.to, animation.startTime, animation.duration, now);
@@ -4468,68 +4639,31 @@ function planExistingItemTransition(params) {
4468
4639
  kind: "update",
4469
4640
  layers,
4470
4641
  height: createScalarAnimation(params.currentVisualState.height, params.nextHeight, params.now, params.duration),
4471
- retention: "visible"
4642
+ retention: "drawn"
4472
4643
  };
4473
4644
  }
4474
4645
  return {
4475
4646
  kind: "delete",
4476
4647
  layers,
4477
4648
  height: createScalarAnimation(params.currentVisualState.height, 0, params.now, params.duration),
4478
- retention: "visible"
4649
+ retention: "drawn"
4479
4650
  };
4480
4651
  }
4481
- function planViewportShift(params) {
4482
- if (!isFinitePositive(params.travel) || params.duration <= 0) return;
4483
- return createScalarAnimation(params.direction === "positive" ? params.currentTranslateY + params.travel : params.currentTranslateY - params.travel, 0, params.now, params.duration);
4484
- }
4485
- function planBoundaryInsert(params) {
4486
- switch (params.strategy) {
4487
- case "hard-cut": return;
4488
- case "item-enter": return planBoundaryInsertItems(params);
4489
- case "viewport-slide": return planBoundaryInsertViewportShift(params);
4490
- }
4491
- }
4492
4652
  function planBoundaryInsertItems(params) {
4493
4653
  const entries = [];
4494
- const signedDistance = params.direction === "push" ? 1 : -1;
4495
4654
  for (const { item, node, height } of params.measuredItems) {
4496
4655
  if (!Number.isFinite(height) || height < 0) return;
4497
- const resolvedDistance = typeof params.distance === "number" && Number.isFinite(params.distance) ? Math.max(0, params.distance) : Math.min(24, height);
4498
4656
  entries.push({
4499
4657
  item,
4500
4658
  transition: {
4501
4659
  kind: "insert",
4502
- layers: [createLayerAnimation(node, 0, 1, params.now, params.duration, signedDistance * resolvedDistance, 0)],
4503
- height: createScalarAnimation(height, height, params.now, params.duration),
4660
+ layers: [createLayerAnimation(node, 0, 1, params.now, params.duration, 0, 0)],
4661
+ height: createScalarAnimation(params.animateHeight ? 0 : height, height, params.now, params.duration),
4504
4662
  retention: "drawn"
4505
4663
  }
4506
4664
  });
4507
4665
  }
4508
- return entries.length === 0 ? void 0 : {
4509
- kind: "item-enter",
4510
- entries
4511
- };
4512
- }
4513
- function planBoundaryInsertViewportShift(params) {
4514
- let insertedHeight = 0;
4515
- for (const { height } of params.measuredItems) {
4516
- if (!Number.isFinite(height) || height <= 0) return;
4517
- insertedHeight += height;
4518
- }
4519
- if (!isFinitePositive(insertedHeight)) return;
4520
- const gap = params.direction === "push" ? params.snapshot.topGap : params.snapshot.bottomGap;
4521
- const travel = Math.min(insertedHeight, gap);
4522
- const animation = planViewportShift({
4523
- currentTranslateY: params.currentTranslateY,
4524
- travel,
4525
- direction: params.direction === "push" ? "positive" : "negative",
4526
- now: params.now,
4527
- duration: params.duration
4528
- });
4529
- return animation == null ? void 0 : {
4530
- kind: "viewport-slide",
4531
- animation
4532
- };
4666
+ return entries.length === 0 ? void 0 : { entries };
4533
4667
  }
4534
4668
  function measureBoundaryInsertItems(direction, count, ctx) {
4535
4669
  const start = direction === "push" ? ctx.items.length - count : 0;
@@ -4557,6 +4691,11 @@ function drawSampledLayers(sampled, y, adapter) {
4557
4691
  if (alpha <= .001) continue;
4558
4692
  adapter.graphics.save();
4559
4693
  try {
4694
+ if (sampled.kind === "insert") {
4695
+ adapter.graphics.beginPath();
4696
+ adapter.graphics.rect(0, y, adapter.graphics.canvas.clientWidth, sampled.slotHeight);
4697
+ adapter.graphics.clip();
4698
+ }
4560
4699
  if (typeof adapter.graphics.globalAlpha === "number") adapter.graphics.globalAlpha *= alpha;
4561
4700
  if (adapter.drawNode(layer.node, 0, y + layer.translateY)) result = true;
4562
4701
  } finally {
@@ -4580,7 +4719,7 @@ function planUpdateTransition(prevItem, nextItem, duration, now, currentVisualSt
4580
4719
  snapshot,
4581
4720
  hasActiveTransition: store.has(prevItem),
4582
4721
  resolveVisibleWindow: ctx.resolveVisibleWindow,
4583
- readVisibleRange: ctx.readVisibleRange
4722
+ readOuterVisibleRange: ctx.readOuterVisibleRange
4584
4723
  }),
4585
4724
  now,
4586
4725
  currentVisualState,
@@ -4601,27 +4740,26 @@ function planDeleteTransition(item, duration, now, currentVisualState, ctx, snap
4601
4740
  snapshot,
4602
4741
  hasActiveTransition: store.has(item),
4603
4742
  resolveVisibleWindow: ctx.resolveVisibleWindow,
4604
- readVisibleRange: ctx.readVisibleRange
4743
+ readOuterVisibleRange: ctx.readOuterVisibleRange
4605
4744
  }),
4606
4745
  now,
4607
4746
  currentVisualState
4608
4747
  });
4609
4748
  }
4610
- function planBoundaryInsertTransition(direction, count, duration, distance, now, currentTranslateY, ctx, snapshot) {
4749
+ function planBoundaryInsertTransition(direction, count, duration, now, ctx, snapshot) {
4611
4750
  const normalizedDuration = normalizeDuration(duration);
4612
4751
  if (count <= 0 || normalizedDuration <= 0) return;
4613
- const strategy = snapshot.matchesBoundaryInsertState(direction, count, ctx.position, ctx.offset) ? resolveBoundaryInsertStrategy(direction, ctx.underflowAlign, true) : snapshot.matchesEmptyBoundaryInsertState(direction, count, ctx.position, ctx.offset) ? "item-enter" : "hard-cut";
4614
- if (strategy === "hard-cut") return;
4752
+ const matchesBoundaryState = snapshot.matchesBoundaryInsertState(direction, count, ctx.position, ctx.offset);
4753
+ const matchesFollowState = snapshot.matchesFollowBoundaryInsertState(direction, count, ctx.position, ctx.offset);
4754
+ const matchesEmptyState = snapshot.matchesEmptyBoundaryInsertState(direction, count, ctx.position, ctx.offset);
4755
+ if (!(matchesBoundaryState || matchesFollowState || matchesEmptyState || snapshot.hasSnapshot && hasVisibleBoundaryInsertItems(direction, count, ctx))) return;
4756
+ const animateHeight = !(direction === "unshift" && matchesFollowState && !matchesBoundaryState && !matchesEmptyState);
4615
4757
  const measuredItems = measureBoundaryInsertItems(direction, count, ctx);
4616
4758
  if (measuredItems == null) return;
4617
- return planBoundaryInsert({
4618
- direction,
4759
+ return planBoundaryInsertItems({
4619
4760
  duration: normalizedDuration,
4620
- distance,
4761
+ animateHeight,
4621
4762
  now,
4622
- strategy,
4623
- snapshot,
4624
- currentTranslateY,
4625
4763
  measuredItems
4626
4764
  });
4627
4765
  }
@@ -4674,7 +4812,7 @@ function readCurrentVisualState(item, now, store, adapter) {
4674
4812
  translateY: 0
4675
4813
  };
4676
4814
  }
4677
- function handleTransitionStateChange(store, snapshot, currentViewportTranslateY, change, ctx, lifecycle) {
4815
+ function handleTransitionStateChange(store, snapshot, change, ctx, lifecycle) {
4678
4816
  switch (change.type) {
4679
4817
  case "update": {
4680
4818
  const now = getNow();
@@ -4682,10 +4820,10 @@ function handleTransitionStateChange(store, snapshot, currentViewportTranslateY,
4682
4820
  const transition = planUpdateTransition(change.prevItem, change.nextItem, change.animation?.duration, now, currentVisualState, ctx, snapshot, store);
4683
4821
  if (transition == null) {
4684
4822
  store.delete(change.prevItem);
4685
- return {};
4823
+ return;
4686
4824
  }
4687
4825
  store.replace(change.prevItem, change.nextItem, transition);
4688
- return {};
4826
+ return;
4689
4827
  }
4690
4828
  case "delete": {
4691
4829
  const now = getNow();
@@ -4694,28 +4832,32 @@ function handleTransitionStateChange(store, snapshot, currentViewportTranslateY,
4694
4832
  if (transition == null) {
4695
4833
  store.delete(change.item);
4696
4834
  lifecycle.onDeleteComplete(change.item);
4697
- return {};
4835
+ return;
4698
4836
  }
4699
4837
  store.set(change.item, transition);
4700
- return {};
4838
+ return;
4701
4839
  }
4702
4840
  case "delete-finalize":
4703
4841
  store.delete(change.item);
4704
- return {};
4842
+ return;
4705
4843
  case "unshift":
4706
4844
  case "push": {
4707
4845
  const now = getNow();
4708
- const plan = planBoundaryInsertTransition(change.type, change.count, change.animation?.duration, change.animation?.distance, now, currentViewportTranslateY, ctx, snapshot);
4709
- if (plan == null) return {};
4710
- if (plan.kind === "viewport-slide") return { viewportAnimation: plan.animation };
4846
+ const plan = planBoundaryInsertTransition(change.type, change.count, change.animation?.duration, now, ctx, snapshot);
4847
+ if (plan == null) return;
4711
4848
  for (const entry of plan.entries) store.set(entry.item, entry.transition);
4712
- return {};
4849
+ if (ctx.position == null && snapshot.coversShortList && (change.type === "push" && ctx.anchorMode === "bottom" || change.type === "unshift" && ctx.anchorMode === "top")) {
4850
+ const boundary = change.type === "push" ? "bottom" : "top";
4851
+ const boundaryItem = snapshot.readBoundaryItem(boundary);
4852
+ if (boundaryItem != null) lifecycle.snapItemToViewportBoundary(boundaryItem, boundary);
4853
+ }
4854
+ return;
4713
4855
  }
4714
4856
  case "reset":
4715
4857
  case "set":
4716
4858
  store.reset();
4717
4859
  snapshot.reset();
4718
- return {};
4860
+ return;
4719
4861
  }
4720
4862
  }
4721
4863
  //#endregion
@@ -4736,22 +4878,15 @@ function remapAnchorAfterDeletes(anchor, deletedIndices) {
4736
4878
  var TransitionController = class {
4737
4879
  #store = new TransitionStore();
4738
4880
  #snapshot = new VisibilitySnapshot();
4739
- #viewportTranslateAnimation;
4740
- captureVisibilitySnapshot(window, resolutionPath, items, viewportHeight, snapshotState, extraShift, readVisibleRange) {
4741
- this.#snapshot.capture(window, resolutionPath, items, viewportHeight, snapshotState, extraShift, readVisibleRange);
4881
+ captureVisibilitySnapshot(window, resolutionPath, items, viewport, snapshotState, readVisibleRange, readOuterVisibleRange) {
4882
+ this.#snapshot.capture(window, resolutionPath, items, viewport, snapshotState, readVisibleRange, readOuterVisibleRange);
4742
4883
  }
4743
4884
  pruneInvisible(ctx, lifecycle) {
4744
4885
  return this.pruneInvisibleAt(getNow(), ctx, lifecycle);
4745
4886
  }
4746
4887
  prepare(now, lifecycle) {
4747
4888
  this.settle(now, lifecycle);
4748
- this.#cleanupViewportTranslateAnimation(now);
4749
- const keepViewportAnimating = this.#viewportTranslateAnimation != null;
4750
- return this.#store.prepare(now) || keepViewportAnimating;
4751
- }
4752
- getViewportTranslateY(now) {
4753
- this.#cleanupViewportTranslateAnimation(now);
4754
- return this.#viewportTranslateAnimation == null ? 0 : sampleScalarAnimation(this.#viewportTranslateAnimation, now);
4889
+ return this.#store.prepare(now);
4755
4890
  }
4756
4891
  canAutoFollowBoundaryInsert(direction, count, position, offset) {
4757
4892
  return this.#snapshot.matchesFollowBoundaryInsertState(direction, count, position, offset);
@@ -4765,17 +4900,10 @@ var TransitionController = class {
4765
4900
  handleListStateChange(change, ctx, lifecycle) {
4766
4901
  const now = getNow();
4767
4902
  this.settle(now, lifecycle);
4768
- const result = handleTransitionStateChange(this.#store, this.#snapshot, this.getViewportTranslateY(now), change, ctx, lifecycle);
4769
- if (change.type === "reset" || change.type === "set") {
4770
- this.#viewportTranslateAnimation = void 0;
4771
- return;
4772
- }
4773
- if (result.viewportAnimation != null) this.#viewportTranslateAnimation = result.viewportAnimation;
4903
+ handleTransitionStateChange(this.#store, this.#snapshot, change, ctx, lifecycle);
4774
4904
  }
4775
4905
  settle(now, lifecycle) {
4776
- const changed = this.#settleTransitions(this.#store.findCompleted(now), now, lifecycle);
4777
- this.#cleanupViewportTranslateAnimation(now);
4778
- return changed;
4906
+ return this.#settleTransitions(this.#store.findCompleted(now), now, lifecycle);
4779
4907
  }
4780
4908
  pruneInvisibleAt(now, ctx, lifecycle) {
4781
4909
  const removals = this.#store.findInvisible(this.#snapshot);
@@ -4784,16 +4912,11 @@ var TransitionController = class {
4784
4912
  reset() {
4785
4913
  this.#store.reset();
4786
4914
  this.#snapshot.reset();
4787
- this.#viewportTranslateAnimation = void 0;
4788
- }
4789
- #cleanupViewportTranslateAnimation(now) {
4790
- const animation = this.#viewportTranslateAnimation;
4791
- if (animation == null) return;
4792
- if (getProgress(animation.startTime, animation.duration, now) >= 1) this.#viewportTranslateAnimation = void 0;
4793
4915
  }
4794
4916
  #settleTransitions(removals, now, lifecycle, boundarySnap) {
4795
4917
  if (removals.length === 0) return false;
4796
4918
  const anchor = lifecycle.captureVisualAnchor(now);
4919
+ const beforeState = lifecycle.readScrollState();
4797
4920
  const completedDeleteIndices = [];
4798
4921
  for (const { item, transition } of removals) {
4799
4922
  if (transition.kind === "delete") {
@@ -4805,6 +4928,8 @@ var TransitionController = class {
4805
4928
  }
4806
4929
  if (anchor != null && Number.isFinite(anchor)) lifecycle.restoreVisualAnchor(remapAnchorAfterDeletes(anchor, completedDeleteIndices));
4807
4930
  if (boundarySnap != null) lifecycle.snapItemToViewportBoundary(boundarySnap.item, boundarySnap.boundary);
4931
+ const afterState = lifecycle.readScrollState();
4932
+ if (!sameState(beforeState, afterState.position, afterState.offset)) lifecycle.onTransitionSettleScrollAdjusted();
4808
4933
  return true;
4809
4934
  }
4810
4935
  #resolveNaturalBoundarySnap(removals, now, ctx, lifecycle) {
@@ -4816,7 +4941,7 @@ var TransitionController = class {
4816
4941
  if (transition.kind !== "update" && transition.kind !== "delete") continue;
4817
4942
  const index = lifecycle.readItemIndex(item);
4818
4943
  if (index < 0 || !this.#snapshot.wasVisible(item)) return;
4819
- if (this.#isTransitionVisibleInState(index, previousState, now, this.#snapshot.currentExtraShift, ctx)) return;
4944
+ if (this.#isTransitionVisibleInState(index, previousState, now, ctx)) return;
4820
4945
  naturalIndices.push(index);
4821
4946
  }
4822
4947
  if (naturalIndices.length === 0) return;
@@ -4835,16 +4960,324 @@ var TransitionController = class {
4835
4960
  };
4836
4961
  }
4837
4962
  }
4838
- #isTransitionVisibleInState(index, state, now, extraShift, ctx) {
4963
+ #isTransitionVisibleInState(index, state, now, ctx) {
4839
4964
  const solution = ctx.resolveVisibleWindowForState(state, now);
4840
4965
  for (const entry of solution.window.drawList) {
4841
4966
  if (entry.idx !== index) continue;
4842
- return ctx.readVisibleRange(entry.offset + solution.window.shift + extraShift, entry.height) != null;
4967
+ return ctx.readOuterVisibleRange(entry.offset + solution.window.shift, entry.height) != null;
4843
4968
  }
4844
4969
  return false;
4845
4970
  }
4846
4971
  };
4847
4972
  //#endregion
4973
+ //#region src/renderer/virtualized/solver.ts
4974
+ function clamp(value, min, max) {
4975
+ return Math.min(Math.max(value, min), max);
4976
+ }
4977
+ function normalizeOffset(offset) {
4978
+ return Number.isFinite(offset) ? offset : 0;
4979
+ }
4980
+ function normalizeListPadding(padding) {
4981
+ return {
4982
+ top: typeof padding?.top === "number" && Number.isFinite(padding.top) ? Math.max(0, padding.top) : 0,
4983
+ bottom: typeof padding?.bottom === "number" && Number.isFinite(padding.bottom) ? Math.max(0, padding.bottom) : 0
4984
+ };
4985
+ }
4986
+ function resolveListViewport(outerHeight, padding) {
4987
+ const height = typeof outerHeight === "number" && Number.isFinite(outerHeight) ? Math.max(0, outerHeight) : 0;
4988
+ const resolvedPadding = normalizeListPadding(padding);
4989
+ const contentTop = resolvedPadding.top;
4990
+ const contentBottom = Math.max(contentTop, height - resolvedPadding.bottom);
4991
+ return {
4992
+ outerHeight: height,
4993
+ contentTop,
4994
+ contentBottom,
4995
+ contentHeight: contentBottom - contentTop,
4996
+ outerContentTop: -contentTop,
4997
+ outerContentBottom: height - contentTop
4998
+ };
4999
+ }
5000
+ function resolveListLayoutOptions(options = {}) {
5001
+ return {
5002
+ anchorMode: options.anchorMode ?? "top",
5003
+ underflowAlign: options.underflowAlign ?? "top",
5004
+ padding: normalizeListPadding(options.padding)
5005
+ };
5006
+ }
5007
+ function normalizeVisibleState(itemCount, state, layout) {
5008
+ if (itemCount <= 0) return {
5009
+ position: 0,
5010
+ offset: 0
5011
+ };
5012
+ const position = state.position;
5013
+ const fallbackPosition = layout.anchorMode === "top" ? 0 : itemCount - 1;
5014
+ if (typeof position !== "number" || !Number.isFinite(position)) return {
5015
+ position: fallbackPosition,
5016
+ offset: normalizeOffset(state.offset)
5017
+ };
5018
+ return {
5019
+ position: clamp(Math.trunc(position), 0, itemCount - 1),
5020
+ offset: normalizeOffset(state.offset)
5021
+ };
5022
+ }
5023
+ function resolveVisibleWindow(items, state, viewportHeight, resolveItem, layout) {
5024
+ const viewport = typeof viewportHeight === "number" ? resolveListViewport(viewportHeight, layout.padding) : viewportHeight;
5025
+ const contentHeight = viewport.contentHeight;
5026
+ const normalizedState = normalizeVisibleState(items.length, state, layout);
5027
+ const resolutionPath = /* @__PURE__ */ new Set();
5028
+ const readResolvedItem = (item, idx) => {
5029
+ resolutionPath.add(idx);
5030
+ return resolveItem(item, idx);
5031
+ };
5032
+ if (items.length === 0) return {
5033
+ normalizedState,
5034
+ resolutionPath: [],
5035
+ window: {
5036
+ drawList: [],
5037
+ shift: 0
5038
+ }
5039
+ };
5040
+ if (layout.anchorMode === "top") {
5041
+ let { position, offset } = normalizedState;
5042
+ let drawLength = 0;
5043
+ if (offset > 0) if (position === 0) offset = 0;
5044
+ else {
5045
+ for (let i = position - 1; i >= 0; i -= 1) {
5046
+ const { height } = readResolvedItem(items[i], i);
5047
+ position = i;
5048
+ offset -= height;
5049
+ if (offset <= 0) break;
5050
+ }
5051
+ if (position === 0 && offset > 0) offset = 0;
5052
+ }
5053
+ let y = offset;
5054
+ const drawList = [];
5055
+ for (let i = position; i < items.length; i += 1) {
5056
+ const { value, height } = readResolvedItem(items[i], i);
5057
+ if (y + height > 0) {
5058
+ drawList.push({
5059
+ idx: i,
5060
+ value,
5061
+ offset: y,
5062
+ height
5063
+ });
5064
+ drawLength += height;
5065
+ } else {
5066
+ offset += height;
5067
+ position = i + 1;
5068
+ }
5069
+ y += height;
5070
+ if (y >= contentHeight) break;
5071
+ }
5072
+ let shift = 0;
5073
+ if (y < contentHeight) {
5074
+ if (drawList.length > 0 && drawList.at(-1)?.idx === items.length - 1 && !(drawList.at(-1)?.height > Number.EPSILON)) return finalizeVisibleWindowResult(items.length, viewport, layout, {
5075
+ position,
5076
+ offset
5077
+ }, Array.from(resolutionPath), extendVisibleWindowToOuterBounds(items, {
5078
+ drawList,
5079
+ shift
5080
+ }, viewport, readResolvedItem));
5081
+ if (position === 0 && drawLength < contentHeight) {
5082
+ shift = -offset;
5083
+ offset = 0;
5084
+ } else {
5085
+ shift = contentHeight - y;
5086
+ y = offset += shift;
5087
+ let lastIdx = -1;
5088
+ for (let i = position - 1; i >= 0; i -= 1) {
5089
+ const { value, height } = readResolvedItem(items[i], i);
5090
+ drawLength += height;
5091
+ y -= height;
5092
+ drawList.push({
5093
+ idx: i,
5094
+ value,
5095
+ offset: y - shift,
5096
+ height
5097
+ });
5098
+ lastIdx = i;
5099
+ if (y < 0) break;
5100
+ }
5101
+ if (lastIdx === 0 && drawLength < contentHeight) {
5102
+ shift = drawList.at(-1)?.offset == null ? 0 : -drawList.at(-1).offset;
5103
+ position = 0;
5104
+ offset = 0;
5105
+ }
5106
+ }
5107
+ }
5108
+ return finalizeVisibleWindowResult(items.length, viewport, layout, {
5109
+ position,
5110
+ offset
5111
+ }, Array.from(resolutionPath), extendVisibleWindowToOuterBounds(items, {
5112
+ drawList,
5113
+ shift
5114
+ }, viewport, readResolvedItem));
5115
+ }
5116
+ let { position, offset } = normalizedState;
5117
+ let drawLength = 0;
5118
+ if (offset < 0) if (position === items.length - 1) offset = 0;
5119
+ else for (let i = position + 1; i < items.length; i += 1) {
5120
+ const { height } = readResolvedItem(items[i], i);
5121
+ position = i;
5122
+ offset += height;
5123
+ if (offset > 0) break;
5124
+ }
5125
+ let y = contentHeight + offset;
5126
+ const drawList = [];
5127
+ for (let i = position; i >= 0; i -= 1) {
5128
+ const { value, height } = readResolvedItem(items[i], i);
5129
+ y -= height;
5130
+ if (y <= contentHeight) {
5131
+ drawList.push({
5132
+ idx: i,
5133
+ value,
5134
+ offset: y,
5135
+ height
5136
+ });
5137
+ drawLength += height;
5138
+ } else {
5139
+ offset -= height;
5140
+ position = i - 1;
5141
+ }
5142
+ if (y < 0) break;
5143
+ }
5144
+ let shift = 0;
5145
+ if (y > 0) {
5146
+ shift = -y;
5147
+ if (drawLength < contentHeight) {
5148
+ y = drawLength;
5149
+ for (let i = position + 1; i < items.length; i += 1) {
5150
+ const { value, height } = readResolvedItem(items[i], i);
5151
+ drawList.push({
5152
+ idx: i,
5153
+ value,
5154
+ offset: y - shift,
5155
+ height
5156
+ });
5157
+ y = drawLength += height;
5158
+ if (height > Number.EPSILON) position = i;
5159
+ if (y >= contentHeight) break;
5160
+ }
5161
+ offset = drawLength < contentHeight ? 0 : drawLength - contentHeight;
5162
+ } else offset = drawLength - contentHeight;
5163
+ }
5164
+ return finalizeVisibleWindowResult(items.length, viewport, layout, {
5165
+ position,
5166
+ offset
5167
+ }, Array.from(resolutionPath), extendVisibleWindowToOuterBounds(items, {
5168
+ drawList,
5169
+ shift
5170
+ }, viewport, readResolvedItem));
5171
+ }
5172
+ function finalizeVisibleWindowResult(itemCount, viewport, layout, normalizedState, resolutionPath, window) {
5173
+ const viewportHeight = viewport.contentHeight;
5174
+ if (window.drawList.length !== itemCount || itemCount <= 0) return {
5175
+ normalizedState,
5176
+ resolutionPath,
5177
+ window
5178
+ };
5179
+ let minIndex = Number.POSITIVE_INFINITY;
5180
+ let maxIndex = Number.NEGATIVE_INFINITY;
5181
+ let minOffset = Number.POSITIVE_INFINITY;
5182
+ let maxBottom = Number.NEGATIVE_INFINITY;
5183
+ let hasDeferredSlots = false;
5184
+ for (const entry of window.drawList) {
5185
+ if (!(entry.height > Number.EPSILON)) hasDeferredSlots = true;
5186
+ else {
5187
+ minOffset = Math.min(minOffset, entry.offset);
5188
+ maxBottom = Math.max(maxBottom, entry.offset + entry.height);
5189
+ }
5190
+ minIndex = Math.min(minIndex, entry.idx);
5191
+ maxIndex = Math.max(maxIndex, entry.idx);
5192
+ }
5193
+ if (!Number.isFinite(minOffset) || !Number.isFinite(maxBottom)) return {
5194
+ normalizedState,
5195
+ resolutionPath,
5196
+ window
5197
+ };
5198
+ const contentHeight = maxBottom - minOffset;
5199
+ if (minIndex !== 0 || maxIndex !== itemCount - 1 || !(contentHeight < viewportHeight - Number.EPSILON)) return {
5200
+ normalizedState,
5201
+ resolutionPath,
5202
+ window
5203
+ };
5204
+ const desiredTop = layout.underflowAlign === "bottom" ? viewportHeight - contentHeight : 0;
5205
+ return {
5206
+ normalizedState: hasDeferredSlots ? normalizedState : layout.anchorMode === "top" ? {
5207
+ position: 0,
5208
+ offset: 0
5209
+ } : {
5210
+ position: itemCount - 1,
5211
+ offset: 0
5212
+ },
5213
+ resolutionPath,
5214
+ window: {
5215
+ drawList: window.drawList,
5216
+ shift: desiredTop - minOffset
5217
+ }
5218
+ };
5219
+ }
5220
+ function extendVisibleWindowToOuterBounds(items, window, viewport, resolveItem) {
5221
+ if (window.drawList.length === 0 || items.length === 0) return window;
5222
+ const drawList = [...window.drawList];
5223
+ const existingIndices = new Set(drawList.map((entry) => entry.idx));
5224
+ let topEntry = drawList[0];
5225
+ let bottomEntry = drawList[0];
5226
+ for (const entry of drawList) {
5227
+ if (entry.offset < topEntry.offset) topEntry = entry;
5228
+ if (entry.offset + entry.height > bottomEntry.offset + bottomEntry.height) bottomEntry = entry;
5229
+ }
5230
+ let topIdx = topEntry.idx;
5231
+ let topY = topEntry.offset + window.shift;
5232
+ while (topIdx > 0) {
5233
+ const prevIdx = topIdx - 1;
5234
+ if (existingIndices.has(prevIdx)) {
5235
+ const existing = drawList.find((entry) => entry.idx === prevIdx);
5236
+ topIdx = prevIdx;
5237
+ if (existing != null) topY = existing.offset + window.shift;
5238
+ continue;
5239
+ }
5240
+ const { value, height } = resolveItem(items[prevIdx], prevIdx);
5241
+ const prevY = topY - height;
5242
+ if (prevY + height <= viewport.outerContentTop) break;
5243
+ drawList.push({
5244
+ idx: prevIdx,
5245
+ value,
5246
+ offset: prevY - window.shift,
5247
+ height
5248
+ });
5249
+ existingIndices.add(prevIdx);
5250
+ topIdx = prevIdx;
5251
+ topY = prevY;
5252
+ }
5253
+ let bottomIdx = bottomEntry.idx;
5254
+ let bottomY = bottomEntry.offset + window.shift + bottomEntry.height;
5255
+ while (bottomIdx < items.length - 1) {
5256
+ const nextIdx = bottomIdx + 1;
5257
+ if (existingIndices.has(nextIdx)) {
5258
+ const existing = drawList.find((entry) => entry.idx === nextIdx);
5259
+ bottomIdx = nextIdx;
5260
+ if (existing != null) bottomY = Math.max(bottomY, existing.offset + window.shift + existing.height);
5261
+ continue;
5262
+ }
5263
+ const { value, height } = resolveItem(items[nextIdx], nextIdx);
5264
+ if (bottomY >= viewport.outerContentBottom) break;
5265
+ drawList.push({
5266
+ idx: nextIdx,
5267
+ value,
5268
+ offset: bottomY - window.shift,
5269
+ height
5270
+ });
5271
+ existingIndices.add(nextIdx);
5272
+ bottomIdx = nextIdx;
5273
+ bottomY += height;
5274
+ }
5275
+ return {
5276
+ drawList,
5277
+ shift: window.shift
5278
+ };
5279
+ }
5280
+ //#endregion
4848
5281
  //#region src/renderer/virtualized/base.ts
4849
5282
  /**
4850
5283
  * Shared base class for virtualized list renderers.
@@ -4863,6 +5296,7 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
4863
5296
  jumpDurationPerPixel: VirtualizedRenderer.JUMP_DURATION_PER_PIXEL,
4864
5297
  getItemCount: () => this.items.length,
4865
5298
  readListState: this._readListState.bind(this),
5299
+ readScrollMutation: () => readListScrollMutation(this.options.list),
4866
5300
  normalizeListState: this._normalizeListState.bind(this),
4867
5301
  readAnchor: (state) => this._readAnchor(state, this._getItemHeight.bind(this)),
4868
5302
  applyAnchor: this._applyAnchor.bind(this),
@@ -4902,6 +5336,7 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
4902
5336
  /** Renders the current visible window. */
4903
5337
  render(feedback) {
4904
5338
  this.#jumpController.beforeFrame();
5339
+ this.#jumpController.noteViewportWidth(this.graphics.canvas.clientWidth);
4905
5340
  const now = getNow();
4906
5341
  const keepAnimating = this._prepareRender(now);
4907
5342
  const { clientWidth: viewportWidth, clientHeight: viewportHeight } = this.graphics.canvas;
@@ -4909,12 +5344,11 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
4909
5344
  const frame = prepareFrameSession({
4910
5345
  now,
4911
5346
  resolveVisibleWindow: (frameNow) => this._resolveVisibleWindow(frameNow),
4912
- getViewportTranslateY: (frameNow) => this.#transitionController.getViewportTranslateY(frameNow),
4913
- captureVisibleItemSnapshot: (solution, extraShift) => this._captureVisibleItemSnapshot(solution, extraShift),
5347
+ captureVisibleItemSnapshot: (solution) => this._captureVisibleItemSnapshot(solution),
4914
5348
  pruneTransitionAnimations: (window, frameNow) => this._pruneTransitionAnimations(window, frameNow)
4915
5349
  });
4916
- const autoFollowCapabilities = this.#jumpController.syncAutoFollowCapabilities(this._readAutoFollowCapabilities(frame.solution.window, frame.viewportTranslateY));
4917
- const requestRedraw = this._renderVisibleWindow(frame.solution.window, feedback, frame.viewportTranslateY);
5350
+ const autoFollowCapabilities = this.#jumpController.recomputeAutoFollowCapabilities(this._readAutoFollowCapabilities(frame.solution.window));
5351
+ const requestRedraw = this._renderVisibleWindow(frame.solution.window, feedback);
4918
5352
  if (feedback != null) {
4919
5353
  feedback.canAutoFollowTop = autoFollowCapabilities.top;
4920
5354
  feedback.canAutoFollowBottom = autoFollowCapabilities.bottom;
@@ -4925,17 +5359,17 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
4925
5359
  /** Hit-tests the current visible window. */
4926
5360
  hittest(test) {
4927
5361
  this.#jumpController.beforeFrame();
5362
+ this.#jumpController.noteViewportWidth(this.graphics.canvas.clientWidth);
4928
5363
  const now = getNow();
4929
5364
  this.#transitionController.settle(now, this.#getTransitionLifecycleAdapter());
4930
5365
  const frame = prepareFrameSession({
4931
5366
  now,
4932
5367
  resolveVisibleWindow: (frameNow) => this._resolveVisibleWindow(frameNow),
4933
- getViewportTranslateY: (frameNow) => this.#transitionController.getViewportTranslateY(frameNow),
4934
- captureVisibleItemSnapshot: (solution, extraShift) => this._captureVisibleItemSnapshot(solution, extraShift),
5368
+ captureVisibleItemSnapshot: (solution) => this._captureVisibleItemSnapshot(solution),
4935
5369
  pruneTransitionAnimations: (window, frameNow) => this._pruneTransitionAnimations(window, frameNow)
4936
5370
  });
4937
- this.#jumpController.syncAutoFollowCapabilities(this._readAutoFollowCapabilities(frame.solution.window, frame.viewportTranslateY));
4938
- return this._hittestVisibleWindow(frame.solution.window, test, frame.viewportTranslateY);
5371
+ this.#jumpController.recomputeAutoFollowCapabilities(this._readAutoFollowCapabilities(frame.solution.window));
5372
+ return this._hittestVisibleWindow(frame.solution.window, test);
4939
5373
  }
4940
5374
  _readListState() {
4941
5375
  return {
@@ -4947,8 +5381,7 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
4947
5381
  return this._resolveVisibleWindowForState(this._readListState(), now);
4948
5382
  }
4949
5383
  _commitListState(state) {
4950
- this.position = state.position;
4951
- this.offset = state.offset;
5384
+ writeInternalListScrollState(this.options.list, state);
4952
5385
  this.#jumpController.commit(state);
4953
5386
  }
4954
5387
  /**
@@ -4990,20 +5423,20 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
4990
5423
  }
4991
5424
  _renderDrawList(list, shift, feedback) {
4992
5425
  let result = false;
4993
- const viewportHeight = this.graphics.canvas.clientHeight;
5426
+ const viewport = this._getViewportMetrics();
4994
5427
  for (const { idx, value: item, offset, height } of list) {
4995
- const y = offset + shift;
5428
+ const y = offset + shift + viewport.contentTop;
4996
5429
  if (feedback != null) this._accumulateRenderFeedback(feedback, idx, y, height);
4997
- if (y + height < 0 || y > viewportHeight) continue;
5430
+ if (y + height < 0 || y > viewport.outerHeight) continue;
4998
5431
  if (item.draw(y)) result = true;
4999
5432
  }
5000
5433
  return result;
5001
5434
  }
5002
- _renderVisibleWindow(window, feedback, extraShift = 0) {
5435
+ _renderVisibleWindow(window, feedback) {
5003
5436
  this._resetRenderFeedback(feedback);
5004
- return this._renderDrawList(window.drawList, window.shift + extraShift, feedback);
5437
+ return this._renderDrawList(window.drawList, window.shift, feedback);
5005
5438
  }
5006
- _readAutoFollowCapabilities(window, extraShift = 0) {
5439
+ _readAutoFollowCapabilities(window) {
5007
5440
  if (window.drawList.length === 0 || this.items.length === 0) return {
5008
5441
  top: false,
5009
5442
  bottom: false
@@ -5012,21 +5445,31 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
5012
5445
  let maxIndex = Number.NEGATIVE_INFINITY;
5013
5446
  let topMostY = Number.POSITIVE_INFINITY;
5014
5447
  let bottomMostY = Number.NEGATIVE_INFINITY;
5015
- const effectiveShift = window.shift + extraShift;
5448
+ const viewport = this._getViewportMetrics();
5016
5449
  for (const { idx, offset, height } of window.drawList) {
5017
5450
  minIndex = Math.min(minIndex, idx);
5018
5451
  maxIndex = Math.max(maxIndex, idx);
5019
- const y = offset + effectiveShift;
5452
+ const y = offset + window.shift + viewport.contentTop;
5020
5453
  topMostY = Math.min(topMostY, y);
5021
5454
  bottomMostY = Math.max(bottomMostY, y + height);
5022
5455
  }
5023
- const viewportHeight = this.graphics.canvas.clientHeight;
5024
5456
  return {
5025
- top: minIndex === 0 && topMostY >= -Number.EPSILON,
5026
- bottom: maxIndex === this.items.length - 1 && bottomMostY <= viewportHeight + Number.EPSILON
5457
+ top: minIndex === 0 && topMostY >= viewport.contentTop - 1e-6,
5458
+ bottom: maxIndex === this.items.length - 1 && bottomMostY <= viewport.contentBottom + 1e-6
5027
5459
  };
5028
5460
  }
5029
5461
  _readVisibleRange(top, height) {
5462
+ if (!Number.isFinite(top) || !Number.isFinite(height) || height <= 0) return;
5463
+ const viewport = this._getViewportMetrics();
5464
+ const visibleTop = clamp$1(viewport.contentTop - top, 0, height);
5465
+ const visibleBottom = clamp$1(viewport.contentBottom - top, 0, height);
5466
+ if (visibleBottom <= visibleTop) return;
5467
+ return {
5468
+ top: visibleTop,
5469
+ bottom: visibleBottom
5470
+ };
5471
+ }
5472
+ _readOuterVisibleRange(top, height) {
5030
5473
  if (!Number.isFinite(top) || !Number.isFinite(height) || height <= 0) return;
5031
5474
  const viewportHeight = this.graphics.canvas.clientHeight;
5032
5475
  const visibleTop = clamp$1(-top, 0, height);
@@ -5040,17 +5483,19 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
5040
5483
  _pruneTransitionAnimations(_window, now) {
5041
5484
  return this.#transitionController.pruneInvisibleAt(now, this.#getTransitionPlanningAdapter(), this.#getTransitionLifecycleAdapter());
5042
5485
  }
5043
- _hittestVisibleWindow(window, test, extraShift = 0) {
5486
+ _hittestVisibleWindow(window, test) {
5487
+ const viewport = this._getViewportMetrics();
5044
5488
  for (const { value: item, offset, height } of window.drawList) {
5045
- const y = offset + window.shift + extraShift;
5489
+ const y = offset + window.shift + viewport.contentTop;
5046
5490
  if (test.y < y || test.y >= y + height) continue;
5047
5491
  return item.hittest(test, y);
5048
5492
  }
5049
5493
  return false;
5050
5494
  }
5051
- _captureVisibleItemSnapshot(solution, extraShift = 0) {
5495
+ _captureVisibleItemSnapshot(solution) {
5052
5496
  const normalizedState = this._normalizeListState(this._readListState());
5053
- this.#transitionController.captureVisibilitySnapshot(solution.window, solution.resolutionPath, this.items, this.graphics.canvas.clientHeight, normalizedState, extraShift, this._readVisibleRange.bind(this));
5497
+ const viewport = this._getViewportMetrics();
5498
+ this.#transitionController.captureVisibilitySnapshot(solution.window, solution.resolutionPath, this.items, viewport, normalizedState, this._readVisibleRange.bind(this), this._readOuterVisibleRange.bind(this));
5054
5499
  }
5055
5500
  _prepareRender(now) {
5056
5501
  const keepTransitioning = this.#transitionController.prepare(now, this.#getTransitionLifecycleAdapter());
@@ -5090,6 +5535,9 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
5090
5535
  _resolveItem(item, _index, now) {
5091
5536
  return this.#transitionController.resolveItem(item, now, this.#getTransitionRenderAdapter(), this.#getTransitionLifecycleAdapter());
5092
5537
  }
5538
+ _getViewportMetrics() {
5539
+ return resolveListViewport(this.graphics.canvas.clientHeight, this._getLayoutOptions().padding);
5540
+ }
5093
5541
  #handleDeleteComplete(item) {
5094
5542
  this.options.list.finalizeDelete(item);
5095
5543
  }
@@ -5098,18 +5546,23 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
5098
5546
  onDeleteComplete: this.#handleDeleteComplete.bind(this),
5099
5547
  captureVisualAnchor: this._readAnchorAt.bind(this),
5100
5548
  restoreVisualAnchor: this._restoreAnchor.bind(this),
5549
+ readScrollState: this._readListState.bind(this),
5101
5550
  readItemIndex: (item) => this.items.indexOf(item),
5102
- snapItemToViewportBoundary: this.#snapItemToViewportBoundary.bind(this)
5551
+ snapItemToViewportBoundary: this.#snapItemToViewportBoundary.bind(this),
5552
+ onTransitionSettleScrollAdjusted: () => this.#jumpController.reconcileAutoFollowAfterTransitionSettle()
5103
5553
  };
5104
5554
  }
5105
5555
  #getVirtualizedRuntime() {
5556
+ const viewport = this._getViewportMetrics();
5106
5557
  return {
5107
5558
  items: this.items,
5108
5559
  position: this.position,
5109
5560
  offset: this.offset,
5110
5561
  renderItem: this.options.renderItem,
5111
5562
  measureNode: this.measureRootNode.bind(this),
5563
+ viewport,
5112
5564
  readVisibleRange: this._readVisibleRange.bind(this),
5565
+ readOuterVisibleRange: this._readOuterVisibleRange.bind(this),
5113
5566
  resolveVisibleWindow: () => this._resolveVisibleWindow(getNow()),
5114
5567
  resolveVisibleWindowForState: (state, now) => this._resolveVisibleWindowForState(state, now)
5115
5568
  };
@@ -5127,7 +5580,7 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
5127
5580
  #getTransitionPlanningAdapter() {
5128
5581
  return {
5129
5582
  ...this.#getVirtualizedRuntime(),
5130
- underflowAlign: this._getLayoutOptions().underflowAlign
5583
+ anchorMode: this._getLayoutOptions().anchorMode
5131
5584
  };
5132
5585
  }
5133
5586
  #handleListStateChange(change) {
@@ -5204,212 +5657,6 @@ function getTargetAnchorForItem(itemCount, index, block, anchorMode, viewportHei
5204
5657
  }
5205
5658
  }
5206
5659
  //#endregion
5207
- //#region src/renderer/virtualized/solver.ts
5208
- function clamp(value, min, max) {
5209
- return Math.min(Math.max(value, min), max);
5210
- }
5211
- function normalizeOffset(offset) {
5212
- return Number.isFinite(offset) ? offset : 0;
5213
- }
5214
- function resolveListLayoutOptions(options = {}) {
5215
- return {
5216
- anchorMode: options.anchorMode ?? "top",
5217
- underflowAlign: options.underflowAlign ?? "top"
5218
- };
5219
- }
5220
- function normalizeVisibleState(itemCount, state, layout) {
5221
- if (itemCount <= 0) return {
5222
- position: 0,
5223
- offset: 0
5224
- };
5225
- const position = state.position;
5226
- const fallbackPosition = layout.anchorMode === "top" ? 0 : itemCount - 1;
5227
- if (typeof position !== "number" || !Number.isFinite(position)) return {
5228
- position: fallbackPosition,
5229
- offset: normalizeOffset(state.offset)
5230
- };
5231
- return {
5232
- position: clamp(Math.trunc(position), 0, itemCount - 1),
5233
- offset: normalizeOffset(state.offset)
5234
- };
5235
- }
5236
- function resolveVisibleWindow(items, state, viewportHeight, resolveItem, layout) {
5237
- const normalizedState = normalizeVisibleState(items.length, state, layout);
5238
- const resolutionPath = /* @__PURE__ */ new Set();
5239
- const readResolvedItem = (item, idx) => {
5240
- resolutionPath.add(idx);
5241
- return resolveItem(item, idx);
5242
- };
5243
- if (items.length === 0) return {
5244
- normalizedState,
5245
- resolutionPath: [],
5246
- window: {
5247
- drawList: [],
5248
- shift: 0
5249
- }
5250
- };
5251
- if (layout.anchorMode === "top") {
5252
- let { position, offset } = normalizedState;
5253
- let drawLength = 0;
5254
- if (offset > 0) if (position === 0) offset = 0;
5255
- else {
5256
- for (let i = position - 1; i >= 0; i -= 1) {
5257
- const { height } = readResolvedItem(items[i], i);
5258
- position = i;
5259
- offset -= height;
5260
- if (offset <= 0) break;
5261
- }
5262
- if (position === 0 && offset > 0) offset = 0;
5263
- }
5264
- let y = offset;
5265
- const drawList = [];
5266
- for (let i = position; i < items.length; i += 1) {
5267
- const { value, height } = readResolvedItem(items[i], i);
5268
- if (y + height > 0) {
5269
- drawList.push({
5270
- idx: i,
5271
- value,
5272
- offset: y,
5273
- height
5274
- });
5275
- drawLength += height;
5276
- } else {
5277
- offset += height;
5278
- position = i + 1;
5279
- }
5280
- y += height;
5281
- if (y >= viewportHeight) break;
5282
- }
5283
- let shift = 0;
5284
- if (y < viewportHeight) if (position === 0 && drawLength < viewportHeight) {
5285
- shift = -offset;
5286
- offset = 0;
5287
- } else {
5288
- shift = viewportHeight - y;
5289
- y = offset += shift;
5290
- let lastIdx = -1;
5291
- for (let i = position - 1; i >= 0; i -= 1) {
5292
- const { value, height } = readResolvedItem(items[i], i);
5293
- drawLength += height;
5294
- y -= height;
5295
- drawList.push({
5296
- idx: i,
5297
- value,
5298
- offset: y - shift,
5299
- height
5300
- });
5301
- lastIdx = i;
5302
- if (y < 0) break;
5303
- }
5304
- if (lastIdx === 0 && drawLength < viewportHeight) {
5305
- shift = drawList.at(-1)?.offset == null ? 0 : -drawList.at(-1).offset;
5306
- position = 0;
5307
- offset = 0;
5308
- }
5309
- }
5310
- return finalizeVisibleWindowResult(items.length, viewportHeight, layout, {
5311
- position,
5312
- offset
5313
- }, Array.from(resolutionPath), {
5314
- drawList,
5315
- shift
5316
- });
5317
- }
5318
- let { position, offset } = normalizedState;
5319
- let drawLength = 0;
5320
- if (offset < 0) if (position === items.length - 1) offset = 0;
5321
- else for (let i = position + 1; i < items.length; i += 1) {
5322
- const { height } = readResolvedItem(items[i], i);
5323
- position = i;
5324
- offset += height;
5325
- if (offset > 0) break;
5326
- }
5327
- let y = viewportHeight + offset;
5328
- const drawList = [];
5329
- for (let i = position; i >= 0; i -= 1) {
5330
- const { value, height } = readResolvedItem(items[i], i);
5331
- y -= height;
5332
- if (y <= viewportHeight) {
5333
- drawList.push({
5334
- idx: i,
5335
- value,
5336
- offset: y,
5337
- height
5338
- });
5339
- drawLength += height;
5340
- } else {
5341
- offset -= height;
5342
- position = i - 1;
5343
- }
5344
- if (y < 0) break;
5345
- }
5346
- let shift = 0;
5347
- if (y > 0) {
5348
- shift = -y;
5349
- if (drawLength < viewportHeight) {
5350
- y = drawLength;
5351
- for (let i = position + 1; i < items.length; i += 1) {
5352
- const { value, height } = readResolvedItem(items[i], i);
5353
- drawList.push({
5354
- idx: i,
5355
- value,
5356
- offset: y - shift,
5357
- height
5358
- });
5359
- y = drawLength += height;
5360
- position = i;
5361
- if (y >= viewportHeight) break;
5362
- }
5363
- offset = drawLength < viewportHeight ? 0 : drawLength - viewportHeight;
5364
- } else offset = drawLength - viewportHeight;
5365
- }
5366
- return finalizeVisibleWindowResult(items.length, viewportHeight, layout, {
5367
- position,
5368
- offset
5369
- }, Array.from(resolutionPath), {
5370
- drawList,
5371
- shift
5372
- });
5373
- }
5374
- function finalizeVisibleWindowResult(itemCount, viewportHeight, layout, normalizedState, resolutionPath, window) {
5375
- if (window.drawList.length !== itemCount || itemCount <= 0) return {
5376
- normalizedState,
5377
- resolutionPath,
5378
- window
5379
- };
5380
- let minIndex = Number.POSITIVE_INFINITY;
5381
- let maxIndex = Number.NEGATIVE_INFINITY;
5382
- let minOffset = Number.POSITIVE_INFINITY;
5383
- let maxBottom = Number.NEGATIVE_INFINITY;
5384
- for (const entry of window.drawList) {
5385
- minIndex = Math.min(minIndex, entry.idx);
5386
- maxIndex = Math.max(maxIndex, entry.idx);
5387
- minOffset = Math.min(minOffset, entry.offset);
5388
- maxBottom = Math.max(maxBottom, entry.offset + entry.height);
5389
- }
5390
- const contentHeight = maxBottom - minOffset;
5391
- if (minIndex !== 0 || maxIndex !== itemCount - 1 || !(contentHeight < viewportHeight - Number.EPSILON)) return {
5392
- normalizedState,
5393
- resolutionPath,
5394
- window
5395
- };
5396
- const desiredTop = layout.underflowAlign === "bottom" ? viewportHeight - contentHeight : 0;
5397
- return {
5398
- normalizedState: layout.anchorMode === "top" ? {
5399
- position: 0,
5400
- offset: 0
5401
- } : {
5402
- position: itemCount - 1,
5403
- offset: 0
5404
- },
5405
- resolutionPath,
5406
- window: {
5407
- drawList: window.drawList,
5408
- shift: desiredTop - minOffset
5409
- }
5410
- };
5411
- }
5412
- //#endregion
5413
5660
  //#region src/renderer/virtualized/list.ts
5414
5661
  /**
5415
5662
  * Virtualized list renderer with configurable anchor semantics.
@@ -5420,11 +5667,24 @@ var ListRenderer = class extends VirtualizedRenderer {
5420
5667
  super(graphics, options);
5421
5668
  this.#layout = resolveListLayoutOptions(options);
5422
5669
  }
5670
+ get padding() {
5671
+ return { ...this.#layout.padding };
5672
+ }
5673
+ set padding(value) {
5674
+ const nextPadding = normalizeListPadding(value);
5675
+ if (nextPadding.top === this.#layout.padding.top && nextPadding.bottom === this.#layout.padding.bottom) return;
5676
+ const anchor = this._readAnchorAt(performance.now());
5677
+ this.#layout = {
5678
+ ...this.#layout,
5679
+ padding: nextPadding
5680
+ };
5681
+ if (anchor != null) this._restoreAnchor(anchor);
5682
+ }
5423
5683
  _getLayoutOptions() {
5424
5684
  return this.#layout;
5425
5685
  }
5426
5686
  _resolveVisibleWindowForState(state, now) {
5427
- return resolveVisibleWindow(this.items, state, this.graphics.canvas.clientHeight, (item, idx) => this._resolveItem(item, idx, now), this.#layout);
5687
+ return resolveVisibleWindow(this.items, state, resolveListViewport(this.graphics.canvas.clientHeight, this.#layout.padding), (item, idx) => this._resolveItem(item, idx, now), this.#layout);
5428
5688
  }
5429
5689
  _getDefaultJumpBlock() {
5430
5690
  return this.#layout.anchorMode === "top" ? "start" : "end";
@@ -5441,7 +5701,7 @@ var ListRenderer = class extends VirtualizedRenderer {
5441
5701
  this._commitListState(state);
5442
5702
  }
5443
5703
  _getTargetAnchor(index, block) {
5444
- return getTargetAnchorForItem(this.items.length, index, block, this.#layout.anchorMode, this.graphics.canvas.clientHeight, this._getItemHeight.bind(this));
5704
+ return getTargetAnchorForItem(this.items.length, index, block, this.#layout.anchorMode, resolveListViewport(this.graphics.canvas.clientHeight, this.#layout.padding).contentHeight, this._getItemHeight.bind(this));
5445
5705
  }
5446
5706
  };
5447
5707
  //#endregion