chat-layout 1.2.0-5 → 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
- if (animation?.followIfAtBoundary === true) normalizedAnimation.followIfAtBoundary = true;
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,117 +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
- #autoFollowLatch;
3985
- #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;
3986
4072
  #jumpAnimation;
3987
- #lastCommittedState;
3988
- #hasPendingListChange = false;
4073
+ #pendingPostJumpBoundary;
4074
+ #pendingPostJumpBoundaryBlocked = false;
3989
4075
  #options;
3990
4076
  constructor(options) {
3991
4077
  this.#options = options;
4078
+ this.#lastHandledScrollMutationVersion = this.#options.readScrollMutation().version;
3992
4079
  }
3993
4080
  beforeFrame() {
3994
- const currentState = this.#options.readListState();
3995
- if (!this.#hasPendingListChange && this.#jumpAnimation == null && this.#lastCommittedState != null && !sameState(this.#lastCommittedState, currentState.position, currentState.offset)) this.#clearAutoFollowLatch();
3996
- 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");
3997
4094
  }
3998
4095
  prepare(now) {
4096
+ if (this.#handlePendingExternalScrollMutation()) return false;
3999
4097
  const animation = this.#jumpAnimation;
4000
4098
  if (animation == null) return false;
4001
4099
  if (this.#options.getItemCount() === 0) {
4002
4100
  this.#cancelJumpAnimation();
4003
4101
  return false;
4004
4102
  }
4005
- if (this.#controlledState != null && !sameState(this.#controlledState, this.#options.readListState().position, this.#options.readListState().offset)) {
4006
- this.#clearAutoFollowLatch();
4007
- this.#cancelJumpAnimation();
4008
- return false;
4009
- }
4010
4103
  const progress = getProgress(animation.startTime, animation.duration, now);
4011
4104
  const eased = progress >= 1 ? 1 : smoothstep(progress);
4012
4105
  const anchor = getAnchorAtDistance(animation.path, animation.path.totalDistance * eased);
4013
4106
  this.#options.applyAnchor(anchor);
4014
4107
  animation.needsMoreFrames = progress < 1;
4108
+ if (!animation.needsMoreFrames && this.#pendingPostJumpBoundary != null && !this.#pendingPostJumpBoundaryBlocked) this.#armAutoFollowBoundary(this.#pendingPostJumpBoundary, "jump-to-boundary-settle");
4015
4109
  return animation.needsMoreFrames;
4016
4110
  }
4017
4111
  finishFrame(requestRedraw) {
4018
4112
  const animation = this.#jumpAnimation;
4019
4113
  if (animation == null) return requestRedraw;
4020
- if (animation.needsMoreFrames) {
4021
- this.#controlledState = this.#options.readListState();
4022
- return true;
4023
- }
4114
+ if (animation.needsMoreFrames) return true;
4115
+ const boundary = this.#pendingPostJumpBoundaryBlocked === true ? void 0 : this.#pendingPostJumpBoundary;
4024
4116
  const onComplete = animation.onComplete;
4025
4117
  this.#cancelJumpAnimation();
4118
+ this.#clearPendingPostJumpBoundary();
4119
+ if (boundary != null) this.#armAutoFollowBoundary(boundary, "jump-to-boundary-settle");
4026
4120
  onComplete?.();
4027
4121
  return requestRedraw || this.#jumpAnimation != null;
4028
4122
  }
4029
4123
  commit(state) {
4030
- this.#lastCommittedState = {
4031
- position: state.position,
4032
- offset: state.offset
4033
- };
4124
+ this.#lastHandledScrollMutationVersion = this.#options.readScrollMutation().version;
4034
4125
  }
4035
4126
  jumpTo(index, options = {}) {
4036
- this.#clearAutoFollowLatch();
4127
+ this.#clearPendingTransitionSettleReconcile();
4128
+ this.#clearPendingPostJumpBoundary();
4037
4129
  if (this.#options.getItemCount() === 0) {
4038
4130
  this.#cancelJumpAnimation();
4039
4131
  return;
4040
4132
  }
4041
- this.#startJumpToIndex(index, options, { kind: "manual" });
4133
+ this.#startJumpToIndex(index, options);
4134
+ }
4135
+ jumpToBoundary(boundary, options = {}) {
4136
+ this.#clearPendingTransitionSettleReconcile();
4137
+ this.#clearPendingPostJumpBoundary();
4138
+ this.#armAutoFollowBoundary(boundary, "jump-to-boundary");
4139
+ if (this.#options.getItemCount() === 0) {
4140
+ this.#cancelJumpAnimation();
4141
+ return;
4142
+ }
4143
+ this.#startJumpToIndex(boundary === "bottom" ? this.#options.getItemCount() - 1 : 0, {
4144
+ ...options,
4145
+ block: boundary === "bottom" ? "end" : "start"
4146
+ });
4147
+ }
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();
4170
+ }
4171
+ getAutoFollowCapabilities() {
4172
+ return {
4173
+ top: this.#canAutoFollowTop,
4174
+ bottom: this.#canAutoFollowBottom
4175
+ };
4176
+ }
4177
+ reconcileAutoFollowAfterTransitionSettle() {
4178
+ this.#pendingTransitionSettleReconcile = true;
4042
4179
  }
4043
4180
  handleListStateChange(change) {
4044
- 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();
4045
4198
  const followChange = this.#resolveAutoFollowChange(change);
4046
- const canChainAutoFollow = followChange != null ? this.#shouldChainAutoFollow(followChange.direction, followChange.animation) : false;
4047
- const canLatchAutoFollow = followChange != null ? this.#shouldLatchAutoFollow(followChange.direction, followChange.count, followChange.animation) : false;
4048
- const canSnapshotAutoFollow = followChange != null ? this.#shouldAutoFollowFromSnapshot(followChange.direction, followChange.count, followChange.animation) : false;
4049
- if (followChange != null && (canSnapshotAutoFollow || canChainAutoFollow || canLatchAutoFollow)) {
4050
- if (canChainAutoFollow) this.#rebaseJumpAnchorForBoundaryInsert(followChange.direction, followChange.count, getNow());
4051
- this.#autoFollowLatch = followChange.direction;
4052
- this.#startJumpToIndex(followChange.direction === "push" ? this.#options.getItemCount() - 1 : 0, {
4053
- block: followChange.direction === "push" ? "end" : "start",
4054
- duration: followChange.animation?.duration
4055
- }, {
4056
- kind: "auto-follow",
4057
- direction: followChange.direction
4058
- });
4059
- return {
4060
- ...followChange.change,
4061
- animation: void 0
4062
- };
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;
4063
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
+ });
4064
4213
  return change;
4065
4214
  }
4066
4215
  #cancelJumpAnimation() {
4067
4216
  this.#jumpAnimation = void 0;
4068
- this.#controlledState = void 0;
4069
4217
  }
4070
- #startJumpToIndex(index, options, source) {
4218
+ #startJumpToIndex(index, options) {
4071
4219
  const targetIndex = this.#options.clampItemIndex(index);
4072
- const currentState = this.#options.normalizeListState(this.#options.readListState());
4073
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());
4074
4224
  const targetAnchor = this.#options.getTargetAnchor(targetIndex, targetBlock);
4075
4225
  if (!(options.animated ?? true)) {
4076
4226
  this.#cancelJumpAnimation();
4077
4227
  this.#options.applyAnchor(targetAnchor);
4228
+ if (settleBoundary != null) this.#armAutoFollowBoundary(settleBoundary, "jump-to-boundary-instant");
4078
4229
  options.onComplete?.();
4079
4230
  return;
4080
4231
  }
@@ -4082,6 +4233,7 @@ var JumpController = class {
4082
4233
  if (!Number.isFinite(startAnchor)) {
4083
4234
  this.#cancelJumpAnimation();
4084
4235
  this.#options.applyAnchor(targetAnchor);
4236
+ if (settleBoundary != null) this.#armAutoFollowBoundary(settleBoundary, "jump-to-boundary-instant");
4085
4237
  options.onComplete?.();
4086
4238
  return;
4087
4239
  }
@@ -4090,24 +4242,34 @@ var JumpController = class {
4090
4242
  if (duration <= 0 || path.totalDistance <= Number.EPSILON) {
4091
4243
  this.#cancelJumpAnimation();
4092
4244
  this.#options.applyAnchor(targetAnchor);
4245
+ if (settleBoundary != null) this.#armAutoFollowBoundary(settleBoundary, "jump-to-boundary-instant");
4093
4246
  options.onComplete?.();
4094
4247
  return;
4095
4248
  }
4249
+ if (settleBoundary != null) {
4250
+ this.#pendingPostJumpBoundary = settleBoundary;
4251
+ this.#pendingPostJumpBoundaryBlocked = false;
4252
+ }
4096
4253
  this.#jumpAnimation = {
4097
4254
  path,
4098
4255
  startTime: getNow(),
4099
4256
  duration,
4100
4257
  needsMoreFrames: true,
4101
- onComplete: options.onComplete,
4102
- source
4258
+ onComplete: options.onComplete
4103
4259
  };
4104
- 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";
4105
4266
  }
4106
4267
  #resolveAutoFollowChange(change) {
4107
4268
  switch (change.type) {
4108
4269
  case "push":
4109
- case "unshift": return change.animation?.followIfAtBoundary === true ? {
4270
+ case "unshift": return change.animation?.autoFollow === true ? {
4110
4271
  change,
4272
+ boundary: change.type === "push" ? "bottom" : "top",
4111
4273
  direction: change.type,
4112
4274
  count: change.count,
4113
4275
  animation: change.animation
@@ -4115,41 +4277,95 @@ var JumpController = class {
4115
4277
  default: return;
4116
4278
  }
4117
4279
  }
4118
- #shouldAutoFollowFromSnapshot(direction, count, animation) {
4119
- if (animation?.followIfAtBoundary !== true) return false;
4120
- return this.#options.canAutoFollowBoundaryInsert(direction, count, this.#options.readListState().position, this.#options.readListState().offset);
4280
+ #hasAutoFollowCapability(boundary) {
4281
+ return boundary === "top" ? this.#canAutoFollowTop : this.#canAutoFollowBottom;
4121
4282
  }
4122
- #shouldLatchAutoFollow(direction, count, animation) {
4123
- if (animation?.followIfAtBoundary !== true) return false;
4124
- if (!this.#matchesLastCommittedStateAfterBoundaryInsert(direction, count)) {
4125
- this.#clearAutoFollowLatch();
4126
- return false;
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;
4291
+ }
4292
+ #markAutoFollowRecompute(boundary, reason) {
4293
+ if (boundary == null || boundary === "top") {
4294
+ this.#pendingAutoFollowRecomputeTop = true;
4295
+ this.#pendingAutoFollowRecomputeReasonTop = reason;
4127
4296
  }
4128
- return this.#autoFollowLatch === direction;
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;
4129
4305
  }
4130
- #shouldChainAutoFollow(direction, animation) {
4131
- if (animation?.followIfAtBoundary !== true) return false;
4132
- return this.#jumpAnimation?.source.kind === "auto-follow" ? this.#jumpAnimation.source.direction === direction : false;
4306
+ #clearPendingTransitionSettleReconcile() {
4307
+ this.#pendingTransitionSettleReconcile = false;
4133
4308
  }
4134
- #rebaseJumpAnchorForBoundaryInsert(direction, count, now) {
4309
+ #materializeAnimatedAnchor(now, direction, count = 0) {
4135
4310
  const animation = this.#jumpAnimation;
4136
4311
  if (animation == null) return;
4137
4312
  const progress = getProgress(animation.startTime, animation.duration, now);
4138
4313
  const eased = progress >= 1 ? 1 : smoothstep(progress);
4139
- 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;
4140
4316
  this.#cancelJumpAnimation();
4141
- this.#options.applyAnchor(direction === "unshift" ? anchorAtNow + count : anchorAtNow);
4317
+ this.#options.applyAnchor(anchor);
4142
4318
  }
4143
- #matchesLastCommittedStateAfterBoundaryInsert(direction, count) {
4144
- const state = this.#lastCommittedState;
4145
- if (state == null) return false;
4146
- return sameState({
4147
- position: direction === "unshift" && state.position != null ? state.position + count : state.position,
4148
- offset: state.offset
4149
- }, 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";
4345
+ }
4346
+ #readCapabilityForBoundary(capabilities, boundary) {
4347
+ return boundary === "top" ? capabilities.top : capabilities.bottom;
4348
+ }
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
+ });
4150
4355
  }
4151
- #clearAutoFollowLatch() {
4152
- this.#autoFollowLatch = void 0;
4356
+ #syncScrollMutationVersion() {
4357
+ this.#lastHandledScrollMutationVersion = this.#options.readScrollMutation().version;
4358
+ }
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;
4153
4369
  }
4154
4370
  };
4155
4371
  //#endregion
@@ -4157,53 +4373,92 @@ var JumpController = class {
4157
4373
  var VisibilitySnapshot = class {
4158
4374
  #drawnItems = /* @__PURE__ */ new Set();
4159
4375
  #visibleItems = /* @__PURE__ */ new Set();
4376
+ #previousVisibleItems = /* @__PURE__ */ new Set();
4160
4377
  #hasSnapshot = false;
4161
4378
  #snapshotState;
4379
+ #previousSnapshotState;
4162
4380
  #emptyState;
4163
4381
  #coversShortList = false;
4164
- #topGap = 0;
4165
- #bottomGap = 0;
4166
4382
  #atStartBoundary = false;
4167
4383
  #atEndBoundary = false;
4384
+ #minDrawnIndex = Number.POSITIVE_INFINITY;
4385
+ #maxDrawnIndex = Number.NEGATIVE_INFINITY;
4386
+ #topBoundaryItem;
4387
+ #bottomBoundaryItem;
4168
4388
  get coversShortList() {
4169
4389
  return this.#hasSnapshot && this.#snapshotState != null && this.#coversShortList;
4170
4390
  }
4171
- get topGap() {
4172
- return this.#topGap;
4391
+ get hasSnapshot() {
4392
+ return this.#hasSnapshot;
4393
+ }
4394
+ get previousState() {
4395
+ return this.#previousSnapshotState;
4173
4396
  }
4174
- get bottomGap() {
4175
- return this.#bottomGap;
4397
+ readDrawnIndexRange() {
4398
+ if (!Number.isFinite(this.#minDrawnIndex) || !Number.isFinite(this.#maxDrawnIndex)) return;
4399
+ return {
4400
+ minIndex: this.#minDrawnIndex,
4401
+ maxIndex: this.#maxDrawnIndex
4402
+ };
4176
4403
  }
4177
- capture(window, _resolutionPath, items, viewportHeight, snapshotState, extraShift, readVisibleRange) {
4404
+ readBoundaryItem(boundary) {
4405
+ return boundary === "top" ? this.#topBoundaryItem : this.#bottomBoundaryItem;
4406
+ }
4407
+ capture(window, _resolutionPath, items, viewport, snapshotState, readVisibleRange, readOuterVisibleRange) {
4408
+ this.#previousVisibleItems = this.#visibleItems;
4409
+ this.#previousSnapshotState = this.#snapshotState;
4178
4410
  const nextDrawnItems = /* @__PURE__ */ new Set();
4179
4411
  const nextVisibleItems = /* @__PURE__ */ new Set();
4180
4412
  let minVisibleIndex = Number.POSITIVE_INFINITY;
4181
4413
  let maxVisibleIndex = Number.NEGATIVE_INFINITY;
4182
4414
  let topMostY = Number.POSITIVE_INFINITY;
4183
4415
  let bottomMostY = Number.NEGATIVE_INFINITY;
4184
- const effectiveShift = window.shift + extraShift;
4416
+ let nextMinDrawnIndex = Number.POSITIVE_INFINITY;
4417
+ let nextMaxDrawnIndex = Number.NEGATIVE_INFINITY;
4418
+ let nextTopBoundaryItem;
4419
+ let nextBottomBoundaryItem;
4420
+ let nextTopBoundaryY = Number.POSITIVE_INFINITY;
4421
+ let nextBottomBoundaryY = Number.NEGATIVE_INFINITY;
4422
+ const effectiveShift = window.shift;
4423
+ const contentOriginY = viewport.contentTop;
4185
4424
  for (const { idx, offset, height } of window.drawList) {
4186
- minVisibleIndex = Math.min(minVisibleIndex, idx);
4187
- maxVisibleIndex = Math.max(maxVisibleIndex, idx);
4188
- const y = offset + effectiveShift;
4425
+ const y = offset + effectiveShift + contentOriginY;
4189
4426
  topMostY = Math.min(topMostY, y);
4190
4427
  bottomMostY = Math.max(bottomMostY, y + height);
4191
4428
  const item = items[idx];
4192
- if (item != null) nextDrawnItems.add(item);
4193
- if (item == null || readVisibleRange(y, height) == null) continue;
4194
- nextVisibleItems.add(item);
4429
+ if (item != null && readOuterVisibleRange(y, height) != null) {
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);
4439
+ if (y < nextTopBoundaryY) {
4440
+ nextTopBoundaryY = y;
4441
+ nextTopBoundaryItem = item;
4442
+ }
4443
+ if (y + height > nextBottomBoundaryY) {
4444
+ nextBottomBoundaryY = y + height;
4445
+ nextBottomBoundaryItem = item;
4446
+ }
4447
+ }
4195
4448
  }
4196
4449
  this.#drawnItems = nextDrawnItems;
4197
4450
  this.#visibleItems = nextVisibleItems;
4198
4451
  this.#hasSnapshot = true;
4199
4452
  this.#snapshotState = snapshotState;
4453
+ this.#minDrawnIndex = nextMinDrawnIndex;
4454
+ this.#maxDrawnIndex = nextMaxDrawnIndex;
4455
+ this.#topBoundaryItem = nextTopBoundaryItem;
4456
+ this.#bottomBoundaryItem = nextBottomBoundaryItem;
4200
4457
  this.#emptyState = items.length === 0 && window.drawList.length === 0 ? snapshotState : void 0;
4201
4458
  const contentHeight = bottomMostY - topMostY;
4202
- 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;
4203
- this.#topGap = this.#coversShortList ? Math.max(0, topMostY) : 0;
4204
- this.#bottomGap = this.#coversShortList ? Math.max(0, viewportHeight - bottomMostY) : 0;
4205
- this.#atStartBoundary = window.drawList.length > 0 && items.length > 0 && minVisibleIndex === 0 && topMostY >= -Number.EPSILON;
4206
- 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;
4207
4462
  }
4208
4463
  matchesCurrentState(position, offset) {
4209
4464
  return this.#hasSnapshot && this.#snapshotState != null && sameState(this.#snapshotState, position, offset);
@@ -4228,20 +4483,27 @@ var VisibilitySnapshot = class {
4228
4483
  isVisible(item) {
4229
4484
  return this.#visibleItems.has(item);
4230
4485
  }
4486
+ wasVisible(item) {
4487
+ return this.#previousVisibleItems.has(item);
4488
+ }
4231
4489
  tracks(item, retention) {
4232
4490
  return retention === "drawn" ? this.#drawnItems.has(item) : this.#visibleItems.has(item);
4233
4491
  }
4234
4492
  reset() {
4235
4493
  this.#drawnItems.clear();
4236
4494
  this.#visibleItems.clear();
4495
+ this.#previousVisibleItems.clear();
4237
4496
  this.#hasSnapshot = false;
4238
4497
  this.#snapshotState = void 0;
4498
+ this.#previousSnapshotState = void 0;
4239
4499
  this.#emptyState = void 0;
4240
4500
  this.#coversShortList = false;
4241
- this.#topGap = 0;
4242
- this.#bottomGap = 0;
4243
4501
  this.#atStartBoundary = false;
4244
4502
  this.#atEndBoundary = false;
4503
+ this.#minDrawnIndex = Number.POSITIVE_INFINITY;
4504
+ this.#maxDrawnIndex = Number.NEGATIVE_INFINITY;
4505
+ this.#topBoundaryItem = void 0;
4506
+ this.#bottomBoundaryItem = void 0;
4245
4507
  }
4246
4508
  #matchesStateAfterBoundaryInsert(direction, count, position, offset) {
4247
4509
  const snapshotState = this.#snapshotState;
@@ -4290,7 +4552,7 @@ var TransitionStore = class {
4290
4552
  }));
4291
4553
  }
4292
4554
  findInvisible(snapshot) {
4293
- 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]) => ({
4294
4556
  item,
4295
4557
  transition
4296
4558
  }));
@@ -4304,9 +4566,6 @@ var TransitionStore = class {
4304
4566
  };
4305
4567
  //#endregion
4306
4568
  //#region src/renderer/virtualized/transition-planner.ts
4307
- function isFinitePositive(value) {
4308
- return Number.isFinite(value) && value > 0;
4309
- }
4310
4569
  function normalizeDuration(duration) {
4311
4570
  return Math.max(0, typeof duration === "number" && Number.isFinite(duration) ? duration : 0);
4312
4571
  }
@@ -4338,13 +4597,16 @@ function isIndexVisible(index, resolveVisibleWindow, readVisibleRange) {
4338
4597
  }
4339
4598
  function resolveAnimationEligibility(params) {
4340
4599
  if (params.index < 0) return false;
4341
- if (params.snapshot.matchesCurrentState(params.position, params.offset)) return params.snapshot.isVisible(params.item);
4342
- 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);
4343
4602
  }
4344
- function resolveBoundaryInsertStrategy(direction, underflowAlign, coversShortListSnapshot) {
4345
- if (!coversShortListSnapshot) return "hard-cut";
4346
- if (direction === "push" && underflowAlign === "bottom" || direction === "unshift" && underflowAlign === "top") return "viewport-slide";
4347
- 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);
4348
4610
  }
4349
4611
  function sampleScalarAnimation(animation, now) {
4350
4612
  return interpolate(animation.from, animation.to, animation.startTime, animation.duration, now);
@@ -4377,68 +4639,31 @@ function planExistingItemTransition(params) {
4377
4639
  kind: "update",
4378
4640
  layers,
4379
4641
  height: createScalarAnimation(params.currentVisualState.height, params.nextHeight, params.now, params.duration),
4380
- retention: "visible"
4642
+ retention: "drawn"
4381
4643
  };
4382
4644
  }
4383
4645
  return {
4384
4646
  kind: "delete",
4385
4647
  layers,
4386
4648
  height: createScalarAnimation(params.currentVisualState.height, 0, params.now, params.duration),
4387
- retention: "visible"
4649
+ retention: "drawn"
4388
4650
  };
4389
4651
  }
4390
- function planViewportShift(params) {
4391
- if (!isFinitePositive(params.travel) || params.duration <= 0) return;
4392
- return createScalarAnimation(params.direction === "positive" ? params.currentTranslateY + params.travel : params.currentTranslateY - params.travel, 0, params.now, params.duration);
4393
- }
4394
- function planBoundaryInsert(params) {
4395
- switch (params.strategy) {
4396
- case "hard-cut": return;
4397
- case "item-enter": return planBoundaryInsertItems(params);
4398
- case "viewport-slide": return planBoundaryInsertViewportShift(params);
4399
- }
4400
- }
4401
4652
  function planBoundaryInsertItems(params) {
4402
4653
  const entries = [];
4403
- const signedDistance = params.direction === "push" ? 1 : -1;
4404
4654
  for (const { item, node, height } of params.measuredItems) {
4405
4655
  if (!Number.isFinite(height) || height < 0) return;
4406
- const resolvedDistance = typeof params.distance === "number" && Number.isFinite(params.distance) ? Math.max(0, params.distance) : Math.min(24, height);
4407
4656
  entries.push({
4408
4657
  item,
4409
4658
  transition: {
4410
4659
  kind: "insert",
4411
- layers: [createLayerAnimation(node, 0, 1, params.now, params.duration, signedDistance * resolvedDistance, 0)],
4412
- 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),
4413
4662
  retention: "drawn"
4414
4663
  }
4415
4664
  });
4416
4665
  }
4417
- return entries.length === 0 ? void 0 : {
4418
- kind: "item-enter",
4419
- entries
4420
- };
4421
- }
4422
- function planBoundaryInsertViewportShift(params) {
4423
- let insertedHeight = 0;
4424
- for (const { height } of params.measuredItems) {
4425
- if (!Number.isFinite(height) || height <= 0) return;
4426
- insertedHeight += height;
4427
- }
4428
- if (!isFinitePositive(insertedHeight)) return;
4429
- const gap = params.direction === "push" ? params.snapshot.topGap : params.snapshot.bottomGap;
4430
- const travel = Math.min(insertedHeight, gap);
4431
- const animation = planViewportShift({
4432
- currentTranslateY: params.currentTranslateY,
4433
- travel,
4434
- direction: params.direction === "push" ? "positive" : "negative",
4435
- now: params.now,
4436
- duration: params.duration
4437
- });
4438
- return animation == null ? void 0 : {
4439
- kind: "viewport-slide",
4440
- animation
4441
- };
4666
+ return entries.length === 0 ? void 0 : { entries };
4442
4667
  }
4443
4668
  function measureBoundaryInsertItems(direction, count, ctx) {
4444
4669
  const start = direction === "push" ? ctx.items.length - count : 0;
@@ -4466,6 +4691,11 @@ function drawSampledLayers(sampled, y, adapter) {
4466
4691
  if (alpha <= .001) continue;
4467
4692
  adapter.graphics.save();
4468
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
+ }
4469
4699
  if (typeof adapter.graphics.globalAlpha === "number") adapter.graphics.globalAlpha *= alpha;
4470
4700
  if (adapter.drawNode(layer.node, 0, y + layer.translateY)) result = true;
4471
4701
  } finally {
@@ -4489,7 +4719,7 @@ function planUpdateTransition(prevItem, nextItem, duration, now, currentVisualSt
4489
4719
  snapshot,
4490
4720
  hasActiveTransition: store.has(prevItem),
4491
4721
  resolveVisibleWindow: ctx.resolveVisibleWindow,
4492
- readVisibleRange: ctx.readVisibleRange
4722
+ readOuterVisibleRange: ctx.readOuterVisibleRange
4493
4723
  }),
4494
4724
  now,
4495
4725
  currentVisualState,
@@ -4510,27 +4740,26 @@ function planDeleteTransition(item, duration, now, currentVisualState, ctx, snap
4510
4740
  snapshot,
4511
4741
  hasActiveTransition: store.has(item),
4512
4742
  resolveVisibleWindow: ctx.resolveVisibleWindow,
4513
- readVisibleRange: ctx.readVisibleRange
4743
+ readOuterVisibleRange: ctx.readOuterVisibleRange
4514
4744
  }),
4515
4745
  now,
4516
4746
  currentVisualState
4517
4747
  });
4518
4748
  }
4519
- function planBoundaryInsertTransition(direction, count, duration, distance, now, currentTranslateY, ctx, snapshot) {
4749
+ function planBoundaryInsertTransition(direction, count, duration, now, ctx, snapshot) {
4520
4750
  const normalizedDuration = normalizeDuration(duration);
4521
4751
  if (count <= 0 || normalizedDuration <= 0) return;
4522
- 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";
4523
- 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);
4524
4757
  const measuredItems = measureBoundaryInsertItems(direction, count, ctx);
4525
4758
  if (measuredItems == null) return;
4526
- return planBoundaryInsert({
4527
- direction,
4759
+ return planBoundaryInsertItems({
4528
4760
  duration: normalizedDuration,
4529
- distance,
4761
+ animateHeight,
4530
4762
  now,
4531
- strategy,
4532
- snapshot,
4533
- currentTranslateY,
4534
4763
  measuredItems
4535
4764
  });
4536
4765
  }
@@ -4583,7 +4812,7 @@ function readCurrentVisualState(item, now, store, adapter) {
4583
4812
  translateY: 0
4584
4813
  };
4585
4814
  }
4586
- function handleTransitionStateChange(store, snapshot, currentViewportTranslateY, change, ctx, lifecycle) {
4815
+ function handleTransitionStateChange(store, snapshot, change, ctx, lifecycle) {
4587
4816
  switch (change.type) {
4588
4817
  case "update": {
4589
4818
  const now = getNow();
@@ -4591,10 +4820,10 @@ function handleTransitionStateChange(store, snapshot, currentViewportTranslateY,
4591
4820
  const transition = planUpdateTransition(change.prevItem, change.nextItem, change.animation?.duration, now, currentVisualState, ctx, snapshot, store);
4592
4821
  if (transition == null) {
4593
4822
  store.delete(change.prevItem);
4594
- return {};
4823
+ return;
4595
4824
  }
4596
4825
  store.replace(change.prevItem, change.nextItem, transition);
4597
- return {};
4826
+ return;
4598
4827
  }
4599
4828
  case "delete": {
4600
4829
  const now = getNow();
@@ -4603,51 +4832,61 @@ function handleTransitionStateChange(store, snapshot, currentViewportTranslateY,
4603
4832
  if (transition == null) {
4604
4833
  store.delete(change.item);
4605
4834
  lifecycle.onDeleteComplete(change.item);
4606
- return {};
4835
+ return;
4607
4836
  }
4608
4837
  store.set(change.item, transition);
4609
- return {};
4838
+ return;
4610
4839
  }
4611
4840
  case "delete-finalize":
4612
4841
  store.delete(change.item);
4613
- return {};
4842
+ return;
4614
4843
  case "unshift":
4615
4844
  case "push": {
4616
4845
  const now = getNow();
4617
- const plan = planBoundaryInsertTransition(change.type, change.count, change.animation?.duration, change.animation?.distance, now, currentViewportTranslateY, ctx, snapshot);
4618
- if (plan == null) return {};
4619
- 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;
4620
4848
  for (const entry of plan.entries) store.set(entry.item, entry.transition);
4621
- 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;
4622
4855
  }
4623
4856
  case "reset":
4624
4857
  case "set":
4625
4858
  store.reset();
4626
4859
  snapshot.reset();
4627
- return {};
4860
+ return;
4628
4861
  }
4629
4862
  }
4630
4863
  //#endregion
4631
4864
  //#region src/renderer/virtualized/base-transition.ts
4865
+ function remapAnchorAfterDeletes(anchor, deletedIndices) {
4866
+ if (!Number.isFinite(anchor) || deletedIndices.length === 0) return anchor;
4867
+ const sortedIndices = [...deletedIndices].filter((index) => Number.isFinite(index) && index >= 0).sort((a, b) => a - b);
4868
+ let removedBeforeAnchor = 0;
4869
+ for (const index of sortedIndices) {
4870
+ if (anchor > index + 1) {
4871
+ removedBeforeAnchor += 1;
4872
+ continue;
4873
+ }
4874
+ if (anchor >= index) return index - removedBeforeAnchor;
4875
+ }
4876
+ return anchor - removedBeforeAnchor;
4877
+ }
4632
4878
  var TransitionController = class {
4633
4879
  #store = new TransitionStore();
4634
4880
  #snapshot = new VisibilitySnapshot();
4635
- #viewportTranslateAnimation;
4636
- captureVisibilitySnapshot(window, resolutionPath, items, viewportHeight, snapshotState, extraShift, readVisibleRange) {
4637
- 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);
4638
4883
  }
4639
- pruneInvisible(lifecycle) {
4640
- return this.pruneInvisibleAt(getNow(), lifecycle);
4884
+ pruneInvisible(ctx, lifecycle) {
4885
+ return this.pruneInvisibleAt(getNow(), ctx, lifecycle);
4641
4886
  }
4642
4887
  prepare(now, lifecycle) {
4643
4888
  this.settle(now, lifecycle);
4644
- this.#cleanupViewportTranslateAnimation(now);
4645
- const keepViewportAnimating = this.#viewportTranslateAnimation != null;
4646
- return this.#store.prepare(now) || keepViewportAnimating;
4647
- }
4648
- getViewportTranslateY(now) {
4649
- this.#cleanupViewportTranslateAnimation(now);
4650
- return this.#viewportTranslateAnimation == null ? 0 : sampleScalarAnimation(this.#viewportTranslateAnimation, now);
4889
+ return this.#store.prepare(now);
4651
4890
  }
4652
4891
  canAutoFollowBoundaryInsert(direction, count, position, offset) {
4653
4892
  return this.#snapshot.matchesFollowBoundaryInsertState(direction, count, position, offset);
@@ -4661,81 +4900,422 @@ var TransitionController = class {
4661
4900
  handleListStateChange(change, ctx, lifecycle) {
4662
4901
  const now = getNow();
4663
4902
  this.settle(now, lifecycle);
4664
- const result = handleTransitionStateChange(this.#store, this.#snapshot, this.getViewportTranslateY(now), change, ctx, lifecycle);
4665
- if (change.type === "reset" || change.type === "set") {
4666
- this.#viewportTranslateAnimation = void 0;
4667
- return;
4668
- }
4669
- if (result.viewportAnimation != null) this.#viewportTranslateAnimation = result.viewportAnimation;
4903
+ handleTransitionStateChange(this.#store, this.#snapshot, change, ctx, lifecycle);
4670
4904
  }
4671
4905
  settle(now, lifecycle) {
4672
- const changed = this.#settleTransitions(this.#store.findCompleted(now), now, lifecycle);
4673
- this.#cleanupViewportTranslateAnimation(now);
4674
- return changed;
4906
+ return this.#settleTransitions(this.#store.findCompleted(now), now, lifecycle);
4675
4907
  }
4676
- pruneInvisibleAt(now, lifecycle) {
4677
- return this.#settleTransitions(this.#store.findInvisible(this.#snapshot), now, lifecycle);
4908
+ pruneInvisibleAt(now, ctx, lifecycle) {
4909
+ const removals = this.#store.findInvisible(this.#snapshot);
4910
+ return this.#settleTransitions(removals, now, lifecycle, this.#resolveNaturalBoundarySnap(removals, now, ctx, lifecycle));
4678
4911
  }
4679
4912
  reset() {
4680
4913
  this.#store.reset();
4681
4914
  this.#snapshot.reset();
4682
- this.#viewportTranslateAnimation = void 0;
4683
4915
  }
4684
- #cleanupViewportTranslateAnimation(now) {
4685
- const animation = this.#viewportTranslateAnimation;
4686
- if (animation == null) return;
4687
- if (getProgress(animation.startTime, animation.duration, now) >= 1) this.#viewportTranslateAnimation = void 0;
4688
- }
4689
- #settleTransitions(removals, now, lifecycle) {
4916
+ #settleTransitions(removals, now, lifecycle, boundarySnap) {
4690
4917
  if (removals.length === 0) return false;
4691
4918
  const anchor = lifecycle.captureVisualAnchor(now);
4919
+ const beforeState = lifecycle.readScrollState();
4920
+ const completedDeleteIndices = [];
4692
4921
  for (const { item, transition } of removals) {
4922
+ if (transition.kind === "delete") {
4923
+ const index = lifecycle.readItemIndex(item);
4924
+ if (index >= 0) completedDeleteIndices.push(index);
4925
+ }
4693
4926
  this.#store.delete(item);
4694
4927
  if (transition.kind === "delete") lifecycle.onDeleteComplete(item);
4695
4928
  }
4696
- if (anchor != null && Number.isFinite(anchor)) lifecycle.restoreVisualAnchor(anchor);
4929
+ if (anchor != null && Number.isFinite(anchor)) lifecycle.restoreVisualAnchor(remapAnchorAfterDeletes(anchor, completedDeleteIndices));
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();
4697
4933
  return true;
4698
4934
  }
4935
+ #resolveNaturalBoundarySnap(removals, now, ctx, lifecycle) {
4936
+ const previousState = this.#snapshot.previousState;
4937
+ const drawnRange = this.#snapshot.readDrawnIndexRange();
4938
+ if (previousState == null || drawnRange == null) return;
4939
+ const naturalIndices = [];
4940
+ for (const { item, transition } of removals) {
4941
+ if (transition.kind !== "update" && transition.kind !== "delete") continue;
4942
+ const index = lifecycle.readItemIndex(item);
4943
+ if (index < 0 || !this.#snapshot.wasVisible(item)) return;
4944
+ if (this.#isTransitionVisibleInState(index, previousState, now, ctx)) return;
4945
+ naturalIndices.push(index);
4946
+ }
4947
+ if (naturalIndices.length === 0) return;
4948
+ if (naturalIndices.every((index) => index < drawnRange.minIndex)) {
4949
+ const item = this.#snapshot.readBoundaryItem("top");
4950
+ return item == null ? void 0 : {
4951
+ item,
4952
+ boundary: "top"
4953
+ };
4954
+ }
4955
+ if (naturalIndices.every((index) => index > drawnRange.maxIndex)) {
4956
+ const item = this.#snapshot.readBoundaryItem("bottom");
4957
+ return item == null ? void 0 : {
4958
+ item,
4959
+ boundary: "bottom"
4960
+ };
4961
+ }
4962
+ }
4963
+ #isTransitionVisibleInState(index, state, now, ctx) {
4964
+ const solution = ctx.resolveVisibleWindowForState(state, now);
4965
+ for (const entry of solution.window.drawList) {
4966
+ if (entry.idx !== index) continue;
4967
+ return ctx.readOuterVisibleRange(entry.offset + solution.window.shift, entry.height) != null;
4968
+ }
4969
+ return false;
4970
+ }
4699
4971
  };
4700
4972
  //#endregion
4701
- //#region src/renderer/virtualized/base.ts
4702
- /**
4703
- * Shared base class for virtualized list renderers.
4704
- */
4705
- var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
4706
- static MIN_JUMP_DURATION = 160;
4707
- static MAX_JUMP_DURATION = 420;
4708
- static JUMP_DURATION_PER_PIXEL = .7;
4709
- #jumpController;
4710
- #transitionController = new TransitionController();
4711
- constructor(graphics, options) {
4712
- super(graphics, options);
4713
- this.#jumpController = new JumpController({
4714
- minJumpDuration: VirtualizedRenderer.MIN_JUMP_DURATION,
4715
- maxJumpDuration: VirtualizedRenderer.MAX_JUMP_DURATION,
4716
- jumpDurationPerPixel: VirtualizedRenderer.JUMP_DURATION_PER_PIXEL,
4717
- getItemCount: () => this.items.length,
4718
- readListState: this._readListState.bind(this),
4719
- normalizeListState: this._normalizeListState.bind(this),
4720
- readAnchor: (state) => this._readAnchor(state, this._getItemHeight.bind(this)),
4721
- applyAnchor: this._applyAnchor.bind(this),
4722
- getDefaultJumpBlock: this._getDefaultJumpBlock.bind(this),
4723
- getTargetAnchor: this._getTargetAnchor.bind(this),
4724
- clampItemIndex: this._clampItemIndex.bind(this),
4725
- getItemHeight: this._getItemHeight.bind(this),
4726
- canAutoFollowBoundaryInsert: (direction, count, position, offset) => this.#transitionController.canAutoFollowBoundaryInsert(direction, count, position, offset)
4727
- });
4728
- subscribeListState(options.list, this, (owner, change) => {
4729
- owner.#handleListStateChange(change);
4730
- });
4731
- }
4732
- /** Current anchor item index. */
4733
- get position() {
4734
- return this.options.list.position;
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));
4735
5115
  }
4736
- /** Updates the current anchor item index. */
4737
- set position(value) {
4738
- this.options.list.position = value;
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
5281
+ //#region src/renderer/virtualized/base.ts
5282
+ /**
5283
+ * Shared base class for virtualized list renderers.
5284
+ */
5285
+ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
5286
+ static MIN_JUMP_DURATION = 160;
5287
+ static MAX_JUMP_DURATION = 420;
5288
+ static JUMP_DURATION_PER_PIXEL = .7;
5289
+ #jumpController;
5290
+ #transitionController = new TransitionController();
5291
+ constructor(graphics, options) {
5292
+ super(graphics, options);
5293
+ this.#jumpController = new JumpController({
5294
+ minJumpDuration: VirtualizedRenderer.MIN_JUMP_DURATION,
5295
+ maxJumpDuration: VirtualizedRenderer.MAX_JUMP_DURATION,
5296
+ jumpDurationPerPixel: VirtualizedRenderer.JUMP_DURATION_PER_PIXEL,
5297
+ getItemCount: () => this.items.length,
5298
+ readListState: this._readListState.bind(this),
5299
+ readScrollMutation: () => readListScrollMutation(this.options.list),
5300
+ normalizeListState: this._normalizeListState.bind(this),
5301
+ readAnchor: (state) => this._readAnchor(state, this._getItemHeight.bind(this)),
5302
+ applyAnchor: this._applyAnchor.bind(this),
5303
+ getDefaultJumpBlock: this._getDefaultJumpBlock.bind(this),
5304
+ getTargetAnchor: this._getTargetAnchor.bind(this),
5305
+ clampItemIndex: this._clampItemIndex.bind(this),
5306
+ getItemHeight: this._getItemHeight.bind(this)
5307
+ });
5308
+ subscribeListState(options.list, this, (owner, change) => {
5309
+ owner.#handleListStateChange(change);
5310
+ });
5311
+ }
5312
+ /** Current anchor item index. */
5313
+ get position() {
5314
+ return this.options.list.position;
5315
+ }
5316
+ /** Updates the current anchor item index. */
5317
+ set position(value) {
5318
+ this.options.list.position = value;
4739
5319
  }
4740
5320
  /** Pixel offset from the anchored item edge. */
4741
5321
  get offset() {
@@ -4756,6 +5336,7 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
4756
5336
  /** Renders the current visible window. */
4757
5337
  render(feedback) {
4758
5338
  this.#jumpController.beforeFrame();
5339
+ this.#jumpController.noteViewportWidth(this.graphics.canvas.clientWidth);
4759
5340
  const now = getNow();
4760
5341
  const keepAnimating = this._prepareRender(now);
4761
5342
  const { clientWidth: viewportWidth, clientHeight: viewportHeight } = this.graphics.canvas;
@@ -4763,27 +5344,32 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
4763
5344
  const frame = prepareFrameSession({
4764
5345
  now,
4765
5346
  resolveVisibleWindow: (frameNow) => this._resolveVisibleWindow(frameNow),
4766
- getViewportTranslateY: (frameNow) => this.#transitionController.getViewportTranslateY(frameNow),
4767
- captureVisibleItemSnapshot: (solution, extraShift) => this._captureVisibleItemSnapshot(solution, extraShift),
5347
+ captureVisibleItemSnapshot: (solution) => this._captureVisibleItemSnapshot(solution),
4768
5348
  pruneTransitionAnimations: (window, frameNow) => this._pruneTransitionAnimations(window, frameNow)
4769
5349
  });
4770
- 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);
5352
+ if (feedback != null) {
5353
+ feedback.canAutoFollowTop = autoFollowCapabilities.top;
5354
+ feedback.canAutoFollowBottom = autoFollowCapabilities.bottom;
5355
+ }
4771
5356
  this._commitListState(frame.solution.normalizedState);
4772
5357
  return this._finishRender(keepAnimating || requestRedraw || frame.requestSettleRedraw);
4773
5358
  }
4774
5359
  /** Hit-tests the current visible window. */
4775
5360
  hittest(test) {
4776
5361
  this.#jumpController.beforeFrame();
5362
+ this.#jumpController.noteViewportWidth(this.graphics.canvas.clientWidth);
4777
5363
  const now = getNow();
4778
5364
  this.#transitionController.settle(now, this.#getTransitionLifecycleAdapter());
4779
5365
  const frame = prepareFrameSession({
4780
5366
  now,
4781
5367
  resolveVisibleWindow: (frameNow) => this._resolveVisibleWindow(frameNow),
4782
- getViewportTranslateY: (frameNow) => this.#transitionController.getViewportTranslateY(frameNow),
4783
- captureVisibleItemSnapshot: (solution, extraShift) => this._captureVisibleItemSnapshot(solution, extraShift),
5368
+ captureVisibleItemSnapshot: (solution) => this._captureVisibleItemSnapshot(solution),
4784
5369
  pruneTransitionAnimations: (window, frameNow) => this._pruneTransitionAnimations(window, frameNow)
4785
5370
  });
4786
- 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);
4787
5373
  }
4788
5374
  _readListState() {
4789
5375
  return {
@@ -4795,8 +5381,7 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
4795
5381
  return this._resolveVisibleWindowForState(this._readListState(), now);
4796
5382
  }
4797
5383
  _commitListState(state) {
4798
- this.position = state.position;
4799
- this.offset = state.offset;
5384
+ writeInternalListScrollState(this.options.list, state);
4800
5385
  this.#jumpController.commit(state);
4801
5386
  }
4802
5387
  /**
@@ -4805,12 +5390,26 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
4805
5390
  jumpTo(index, options = {}) {
4806
5391
  this.#jumpController.jumpTo(index, options);
4807
5392
  }
5393
+ /**
5394
+ * Scrolls the viewport to the visual top edge and arms top auto-follow immediately.
5395
+ */
5396
+ jumpToTop(options = {}) {
5397
+ this.#jumpController.jumpToBoundary("top", options);
5398
+ }
5399
+ /**
5400
+ * Scrolls the viewport to the visual bottom edge and arms bottom auto-follow immediately.
5401
+ */
5402
+ jumpToBottom(options = {}) {
5403
+ this.#jumpController.jumpToBoundary("bottom", options);
5404
+ }
4808
5405
  _resetRenderFeedback(feedback) {
4809
5406
  if (feedback == null) return;
4810
5407
  feedback.minIdx = NaN;
4811
5408
  feedback.maxIdx = NaN;
4812
5409
  feedback.min = NaN;
4813
5410
  feedback.max = NaN;
5411
+ feedback.canAutoFollowTop = false;
5412
+ feedback.canAutoFollowBottom = false;
4814
5413
  }
4815
5414
  _accumulateRenderFeedback(feedback, idx, top, height) {
4816
5415
  const visibleRange = this._readVisibleRange(top, height);
@@ -4824,20 +5423,53 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
4824
5423
  }
4825
5424
  _renderDrawList(list, shift, feedback) {
4826
5425
  let result = false;
4827
- const viewportHeight = this.graphics.canvas.clientHeight;
5426
+ const viewport = this._getViewportMetrics();
4828
5427
  for (const { idx, value: item, offset, height } of list) {
4829
- const y = offset + shift;
5428
+ const y = offset + shift + viewport.contentTop;
4830
5429
  if (feedback != null) this._accumulateRenderFeedback(feedback, idx, y, height);
4831
- if (y + height < 0 || y > viewportHeight) continue;
5430
+ if (y + height < 0 || y > viewport.outerHeight) continue;
4832
5431
  if (item.draw(y)) result = true;
4833
5432
  }
4834
5433
  return result;
4835
5434
  }
4836
- _renderVisibleWindow(window, feedback, extraShift = 0) {
5435
+ _renderVisibleWindow(window, feedback) {
4837
5436
  this._resetRenderFeedback(feedback);
4838
- return this._renderDrawList(window.drawList, window.shift + extraShift, feedback);
5437
+ return this._renderDrawList(window.drawList, window.shift, feedback);
5438
+ }
5439
+ _readAutoFollowCapabilities(window) {
5440
+ if (window.drawList.length === 0 || this.items.length === 0) return {
5441
+ top: false,
5442
+ bottom: false
5443
+ };
5444
+ let minIndex = Number.POSITIVE_INFINITY;
5445
+ let maxIndex = Number.NEGATIVE_INFINITY;
5446
+ let topMostY = Number.POSITIVE_INFINITY;
5447
+ let bottomMostY = Number.NEGATIVE_INFINITY;
5448
+ const viewport = this._getViewportMetrics();
5449
+ for (const { idx, offset, height } of window.drawList) {
5450
+ minIndex = Math.min(minIndex, idx);
5451
+ maxIndex = Math.max(maxIndex, idx);
5452
+ const y = offset + window.shift + viewport.contentTop;
5453
+ topMostY = Math.min(topMostY, y);
5454
+ bottomMostY = Math.max(bottomMostY, y + height);
5455
+ }
5456
+ return {
5457
+ top: minIndex === 0 && topMostY >= viewport.contentTop - 1e-6,
5458
+ bottom: maxIndex === this.items.length - 1 && bottomMostY <= viewport.contentBottom + 1e-6
5459
+ };
4839
5460
  }
4840
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) {
4841
5473
  if (!Number.isFinite(top) || !Number.isFinite(height) || height <= 0) return;
4842
5474
  const viewportHeight = this.graphics.canvas.clientHeight;
4843
5475
  const visibleTop = clamp$1(-top, 0, height);
@@ -4849,19 +5481,21 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
4849
5481
  };
4850
5482
  }
4851
5483
  _pruneTransitionAnimations(_window, now) {
4852
- return this.#transitionController.pruneInvisibleAt(now, this.#getTransitionLifecycleAdapter());
5484
+ return this.#transitionController.pruneInvisibleAt(now, this.#getTransitionPlanningAdapter(), this.#getTransitionLifecycleAdapter());
4853
5485
  }
4854
- _hittestVisibleWindow(window, test, extraShift = 0) {
5486
+ _hittestVisibleWindow(window, test) {
5487
+ const viewport = this._getViewportMetrics();
4855
5488
  for (const { value: item, offset, height } of window.drawList) {
4856
- const y = offset + window.shift + extraShift;
5489
+ const y = offset + window.shift + viewport.contentTop;
4857
5490
  if (test.y < y || test.y >= y + height) continue;
4858
5491
  return item.hittest(test, y);
4859
5492
  }
4860
5493
  return false;
4861
5494
  }
4862
- _captureVisibleItemSnapshot(solution, extraShift = 0) {
5495
+ _captureVisibleItemSnapshot(solution) {
4863
5496
  const normalizedState = this._normalizeListState(this._readListState());
4864
- 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));
4865
5499
  }
4866
5500
  _prepareRender(now) {
4867
5501
  const keepTransitioning = this.#transitionController.prepare(now, this.#getTransitionLifecycleAdapter());
@@ -4893,9 +5527,17 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
4893
5527
  if (!Number.isFinite(anchor) || this.items.length <= 0) return;
4894
5528
  this._applyAnchor(anchor);
4895
5529
  }
5530
+ #snapItemToViewportBoundary(item, boundary) {
5531
+ const index = this.items.indexOf(item);
5532
+ if (index < 0) return;
5533
+ this._applyAnchor(this._getTargetAnchor(index, boundary === "top" ? "start" : "end"));
5534
+ }
4896
5535
  _resolveItem(item, _index, now) {
4897
5536
  return this.#transitionController.resolveItem(item, now, this.#getTransitionRenderAdapter(), this.#getTransitionLifecycleAdapter());
4898
5537
  }
5538
+ _getViewportMetrics() {
5539
+ return resolveListViewport(this.graphics.canvas.clientHeight, this._getLayoutOptions().padding);
5540
+ }
4899
5541
  #handleDeleteComplete(item) {
4900
5542
  this.options.list.finalizeDelete(item);
4901
5543
  }
@@ -4903,18 +5545,26 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
4903
5545
  return {
4904
5546
  onDeleteComplete: this.#handleDeleteComplete.bind(this),
4905
5547
  captureVisualAnchor: this._readAnchorAt.bind(this),
4906
- restoreVisualAnchor: this._restoreAnchor.bind(this)
5548
+ restoreVisualAnchor: this._restoreAnchor.bind(this),
5549
+ readScrollState: this._readListState.bind(this),
5550
+ readItemIndex: (item) => this.items.indexOf(item),
5551
+ snapItemToViewportBoundary: this.#snapItemToViewportBoundary.bind(this),
5552
+ onTransitionSettleScrollAdjusted: () => this.#jumpController.reconcileAutoFollowAfterTransitionSettle()
4907
5553
  };
4908
5554
  }
4909
5555
  #getVirtualizedRuntime() {
5556
+ const viewport = this._getViewportMetrics();
4910
5557
  return {
4911
5558
  items: this.items,
4912
5559
  position: this.position,
4913
5560
  offset: this.offset,
4914
5561
  renderItem: this.options.renderItem,
4915
5562
  measureNode: this.measureRootNode.bind(this),
5563
+ viewport,
4916
5564
  readVisibleRange: this._readVisibleRange.bind(this),
4917
- resolveVisibleWindow: () => this._resolveVisibleWindow(getNow())
5565
+ readOuterVisibleRange: this._readOuterVisibleRange.bind(this),
5566
+ resolveVisibleWindow: () => this._resolveVisibleWindow(getNow()),
5567
+ resolveVisibleWindowForState: (state, now) => this._resolveVisibleWindowForState(state, now)
4918
5568
  };
4919
5569
  }
4920
5570
  #getTransitionRenderAdapter() {
@@ -4930,7 +5580,7 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
4930
5580
  #getTransitionPlanningAdapter() {
4931
5581
  return {
4932
5582
  ...this.#getVirtualizedRuntime(),
4933
- underflowAlign: this._getLayoutOptions().underflowAlign
5583
+ anchorMode: this._getLayoutOptions().anchorMode
4934
5584
  };
4935
5585
  }
4936
5586
  #handleListStateChange(change) {
@@ -5007,212 +5657,6 @@ function getTargetAnchorForItem(itemCount, index, block, anchorMode, viewportHei
5007
5657
  }
5008
5658
  }
5009
5659
  //#endregion
5010
- //#region src/renderer/virtualized/solver.ts
5011
- function clamp(value, min, max) {
5012
- return Math.min(Math.max(value, min), max);
5013
- }
5014
- function normalizeOffset(offset) {
5015
- return Number.isFinite(offset) ? offset : 0;
5016
- }
5017
- function resolveListLayoutOptions(options = {}) {
5018
- return {
5019
- anchorMode: options.anchorMode ?? "top",
5020
- underflowAlign: options.underflowAlign ?? "top"
5021
- };
5022
- }
5023
- function normalizeVisibleState(itemCount, state, layout) {
5024
- if (itemCount <= 0) return {
5025
- position: 0,
5026
- offset: 0
5027
- };
5028
- const position = state.position;
5029
- const fallbackPosition = layout.anchorMode === "top" ? 0 : itemCount - 1;
5030
- if (typeof position !== "number" || !Number.isFinite(position)) return {
5031
- position: fallbackPosition,
5032
- offset: normalizeOffset(state.offset)
5033
- };
5034
- return {
5035
- position: clamp(Math.trunc(position), 0, itemCount - 1),
5036
- offset: normalizeOffset(state.offset)
5037
- };
5038
- }
5039
- function resolveVisibleWindow(items, state, viewportHeight, resolveItem, layout) {
5040
- const normalizedState = normalizeVisibleState(items.length, state, layout);
5041
- const resolutionPath = /* @__PURE__ */ new Set();
5042
- const readResolvedItem = (item, idx) => {
5043
- resolutionPath.add(idx);
5044
- return resolveItem(item, idx);
5045
- };
5046
- if (items.length === 0) return {
5047
- normalizedState,
5048
- resolutionPath: [],
5049
- window: {
5050
- drawList: [],
5051
- shift: 0
5052
- }
5053
- };
5054
- if (layout.anchorMode === "top") {
5055
- let { position, offset } = normalizedState;
5056
- let drawLength = 0;
5057
- if (offset > 0) if (position === 0) offset = 0;
5058
- else {
5059
- for (let i = position - 1; i >= 0; i -= 1) {
5060
- const { height } = readResolvedItem(items[i], i);
5061
- position = i;
5062
- offset -= height;
5063
- if (offset <= 0) break;
5064
- }
5065
- if (position === 0 && offset > 0) offset = 0;
5066
- }
5067
- let y = offset;
5068
- const drawList = [];
5069
- for (let i = position; i < items.length; i += 1) {
5070
- const { value, height } = readResolvedItem(items[i], i);
5071
- if (y + height > 0) {
5072
- drawList.push({
5073
- idx: i,
5074
- value,
5075
- offset: y,
5076
- height
5077
- });
5078
- drawLength += height;
5079
- } else {
5080
- offset += height;
5081
- position = i + 1;
5082
- }
5083
- y += height;
5084
- if (y >= viewportHeight) break;
5085
- }
5086
- let shift = 0;
5087
- if (y < viewportHeight) if (position === 0 && drawLength < viewportHeight) {
5088
- shift = -offset;
5089
- offset = 0;
5090
- } else {
5091
- shift = viewportHeight - y;
5092
- y = offset += shift;
5093
- let lastIdx = -1;
5094
- for (let i = position - 1; i >= 0; i -= 1) {
5095
- const { value, height } = readResolvedItem(items[i], i);
5096
- drawLength += height;
5097
- y -= height;
5098
- drawList.push({
5099
- idx: i,
5100
- value,
5101
- offset: y - shift,
5102
- height
5103
- });
5104
- lastIdx = i;
5105
- if (y < 0) break;
5106
- }
5107
- if (lastIdx === 0 && drawLength < viewportHeight) {
5108
- shift = drawList.at(-1)?.offset == null ? 0 : -drawList.at(-1).offset;
5109
- position = 0;
5110
- offset = 0;
5111
- }
5112
- }
5113
- return finalizeVisibleWindowResult(items.length, viewportHeight, layout, {
5114
- position,
5115
- offset
5116
- }, Array.from(resolutionPath), {
5117
- drawList,
5118
- shift
5119
- });
5120
- }
5121
- let { position, offset } = normalizedState;
5122
- let drawLength = 0;
5123
- if (offset < 0) if (position === items.length - 1) offset = 0;
5124
- else for (let i = position + 1; i < items.length; i += 1) {
5125
- const { height } = readResolvedItem(items[i], i);
5126
- position = i;
5127
- offset += height;
5128
- if (offset > 0) break;
5129
- }
5130
- let y = viewportHeight + offset;
5131
- const drawList = [];
5132
- for (let i = position; i >= 0; i -= 1) {
5133
- const { value, height } = readResolvedItem(items[i], i);
5134
- y -= height;
5135
- if (y <= viewportHeight) {
5136
- drawList.push({
5137
- idx: i,
5138
- value,
5139
- offset: y,
5140
- height
5141
- });
5142
- drawLength += height;
5143
- } else {
5144
- offset -= height;
5145
- position = i - 1;
5146
- }
5147
- if (y < 0) break;
5148
- }
5149
- let shift = 0;
5150
- if (y > 0) {
5151
- shift = -y;
5152
- if (drawLength < viewportHeight) {
5153
- y = drawLength;
5154
- for (let i = position + 1; i < items.length; i += 1) {
5155
- const { value, height } = readResolvedItem(items[i], i);
5156
- drawList.push({
5157
- idx: i,
5158
- value,
5159
- offset: y - shift,
5160
- height
5161
- });
5162
- y = drawLength += height;
5163
- position = i;
5164
- if (y >= viewportHeight) break;
5165
- }
5166
- offset = drawLength < viewportHeight ? 0 : drawLength - viewportHeight;
5167
- } else offset = drawLength - viewportHeight;
5168
- }
5169
- return finalizeVisibleWindowResult(items.length, viewportHeight, layout, {
5170
- position,
5171
- offset
5172
- }, Array.from(resolutionPath), {
5173
- drawList,
5174
- shift
5175
- });
5176
- }
5177
- function finalizeVisibleWindowResult(itemCount, viewportHeight, layout, normalizedState, resolutionPath, window) {
5178
- if (window.drawList.length !== itemCount || itemCount <= 0) return {
5179
- normalizedState,
5180
- resolutionPath,
5181
- window
5182
- };
5183
- let minIndex = Number.POSITIVE_INFINITY;
5184
- let maxIndex = Number.NEGATIVE_INFINITY;
5185
- let minOffset = Number.POSITIVE_INFINITY;
5186
- let maxBottom = Number.NEGATIVE_INFINITY;
5187
- for (const entry of window.drawList) {
5188
- minIndex = Math.min(minIndex, entry.idx);
5189
- maxIndex = Math.max(maxIndex, entry.idx);
5190
- minOffset = Math.min(minOffset, entry.offset);
5191
- maxBottom = Math.max(maxBottom, entry.offset + entry.height);
5192
- }
5193
- const contentHeight = maxBottom - minOffset;
5194
- if (minIndex !== 0 || maxIndex !== itemCount - 1 || !(contentHeight < viewportHeight - Number.EPSILON)) return {
5195
- normalizedState,
5196
- resolutionPath,
5197
- window
5198
- };
5199
- const desiredTop = layout.underflowAlign === "bottom" ? viewportHeight - contentHeight : 0;
5200
- return {
5201
- normalizedState: layout.anchorMode === "top" ? {
5202
- position: 0,
5203
- offset: 0
5204
- } : {
5205
- position: itemCount - 1,
5206
- offset: 0
5207
- },
5208
- resolutionPath,
5209
- window: {
5210
- drawList: window.drawList,
5211
- shift: desiredTop - minOffset
5212
- }
5213
- };
5214
- }
5215
- //#endregion
5216
5660
  //#region src/renderer/virtualized/list.ts
5217
5661
  /**
5218
5662
  * Virtualized list renderer with configurable anchor semantics.
@@ -5223,11 +5667,24 @@ var ListRenderer = class extends VirtualizedRenderer {
5223
5667
  super(graphics, options);
5224
5668
  this.#layout = resolveListLayoutOptions(options);
5225
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
+ }
5226
5683
  _getLayoutOptions() {
5227
5684
  return this.#layout;
5228
5685
  }
5229
5686
  _resolveVisibleWindowForState(state, now) {
5230
- 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);
5231
5688
  }
5232
5689
  _getDefaultJumpBlock() {
5233
5690
  return this.#layout.anchorMode === "top" ? "start" : "end";
@@ -5244,7 +5701,7 @@ var ListRenderer = class extends VirtualizedRenderer {
5244
5701
  this._commitListState(state);
5245
5702
  }
5246
5703
  _getTargetAnchor(index, block) {
5247
- 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));
5248
5705
  }
5249
5706
  };
5250
5707
  //#endregion