chat-layout 1.2.0-3 → 1.2.0-5

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
@@ -3639,12 +3639,34 @@ function assertUniqueItemReferences(items, existingItems) {
3639
3639
  seen.add(item);
3640
3640
  }
3641
3641
  }
3642
+ function normalizeAnimationDuration(duration) {
3643
+ if (duration == null) return;
3644
+ return Number.isFinite(duration) ? { duration } : {};
3645
+ }
3642
3646
  function normalizeUpdateAnimation(animation) {
3643
- if (animation == null) return;
3644
- return Number.isFinite(animation.duration) ? { duration: animation.duration } : {};
3647
+ return normalizeAnimationDuration(animation?.duration);
3648
+ }
3649
+ function normalizeDeleteAnimation(animation) {
3650
+ return normalizeAnimationDuration(animation?.duration);
3651
+ }
3652
+ const DEFAULT_INSERT_ALL_ANIMATION_DURATION = 220;
3653
+ function normalizeInsertAnimationDuration(duration, hasAnimationOptions) {
3654
+ if (!hasAnimationOptions) return;
3655
+ const resolvedDuration = duration == null ? DEFAULT_INSERT_ALL_ANIMATION_DURATION : duration;
3656
+ if (!Number.isFinite(resolvedDuration) || resolvedDuration <= 0) return;
3657
+ return resolvedDuration;
3658
+ }
3659
+ function normalizeInsertAnimation(animation) {
3660
+ const duration = normalizeInsertAnimationDuration(animation?.duration, animation != null);
3661
+ if (duration == null) return;
3662
+ 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;
3665
+ return normalizedAnimation;
3645
3666
  }
3646
3667
  var ListState = class {
3647
3668
  #items;
3669
+ #pendingDeletes = /* @__PURE__ */ new Set();
3648
3670
  /** Pixel offset from the anchored item edge. */
3649
3671
  offset = 0;
3650
3672
  /** Anchor item index, or `undefined` to use the renderer default. */
@@ -3658,6 +3680,7 @@ var ListState = class {
3658
3680
  const nextItems = [...value];
3659
3681
  assertUniqueItemReferences(nextItems);
3660
3682
  this.#items = nextItems;
3683
+ this.#pendingDeletes.clear();
3661
3684
  emitListStateChange(this, { type: "set" });
3662
3685
  }
3663
3686
  /**
@@ -3673,14 +3696,16 @@ var ListState = class {
3673
3696
  this.unshiftAll(items);
3674
3697
  }
3675
3698
  /** Prepends an array of items. */
3676
- unshiftAll(items) {
3699
+ unshiftAll(items, animation) {
3677
3700
  if (items.length === 0) return;
3678
3701
  assertUniqueItemReferences(items, this.#items);
3702
+ const normalizedAnimation = normalizeInsertAnimation(animation);
3679
3703
  if (this.position != null) this.position += items.length;
3680
3704
  this.#items = items.concat(this.#items);
3681
3705
  emitListStateChange(this, {
3682
3706
  type: "unshift",
3683
- count: items.length
3707
+ count: items.length,
3708
+ animation: normalizedAnimation
3684
3709
  });
3685
3710
  }
3686
3711
  /** Appends one or more items. */
@@ -3688,13 +3713,15 @@ var ListState = class {
3688
3713
  this.pushAll(items);
3689
3714
  }
3690
3715
  /** Appends an array of items. */
3691
- pushAll(items) {
3716
+ pushAll(items, animation) {
3692
3717
  if (items.length === 0) return;
3693
3718
  assertUniqueItemReferences(items, this.#items);
3719
+ const normalizedAnimation = normalizeInsertAnimation(animation);
3694
3720
  this.#items.push(...items);
3695
3721
  emitListStateChange(this, {
3696
3722
  type: "push",
3697
- count: items.length
3723
+ count: items.length,
3724
+ animation: normalizedAnimation
3698
3725
  });
3699
3726
  }
3700
3727
  /**
@@ -3705,6 +3732,7 @@ var ListState = class {
3705
3732
  if (targetItem === nextItem) throw new Error("update() requires nextItem to be a new object reference.");
3706
3733
  const index = this.#items.indexOf(targetItem);
3707
3734
  if (index < 0) throw new Error("update() targetItem is not present in the list.");
3735
+ if (this.#pendingDeletes.has(targetItem)) throw new Error("update() targetItem is pending deletion.");
3708
3736
  if (this.#items.includes(nextItem)) throw new Error("update() nextItem is already present in the list.");
3709
3737
  const prevItem = this.#items[index];
3710
3738
  this.#items[index] = nextItem;
@@ -3716,6 +3744,47 @@ var ListState = class {
3716
3744
  });
3717
3745
  }
3718
3746
  /**
3747
+ * Starts deleting an existing item by object identity.
3748
+ */
3749
+ delete(item, animation) {
3750
+ if (!isObjectIdentityCandidate(item)) throw new TypeError("delete() only supports object items.");
3751
+ if (this.#items.indexOf(item) < 0) throw new Error("delete() item is not present in the list.");
3752
+ if (this.#pendingDeletes.has(item)) return;
3753
+ const normalizedAnimation = normalizeDeleteAnimation(animation);
3754
+ if (!((normalizedAnimation?.duration ?? 0) > 0)) {
3755
+ this.#pendingDeletes.add(item);
3756
+ this.finalizeDelete(item);
3757
+ return;
3758
+ }
3759
+ this.#pendingDeletes.add(item);
3760
+ emitListStateChange(this, {
3761
+ type: "delete",
3762
+ item,
3763
+ animation: normalizedAnimation
3764
+ });
3765
+ }
3766
+ /**
3767
+ * Finalizes a pending delete by removing the item from the list.
3768
+ */
3769
+ finalizeDelete(item) {
3770
+ if (!this.#pendingDeletes.has(item)) return;
3771
+ const index = this.#items.indexOf(item);
3772
+ this.#pendingDeletes.delete(item);
3773
+ if (index < 0) return;
3774
+ 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);
3781
+ }
3782
+ emitListStateChange(this, {
3783
+ type: "delete-finalize",
3784
+ item
3785
+ });
3786
+ }
3787
+ /**
3719
3788
  * Sets the current anchor item and pixel offset.
3720
3789
  */
3721
3790
  setAnchor(position, offset = 0) {
@@ -3729,6 +3798,7 @@ var ListState = class {
3729
3798
  const nextItems = [...items];
3730
3799
  assertUniqueItemReferences(nextItems);
3731
3800
  this.#items = nextItems;
3801
+ this.#pendingDeletes.clear();
3732
3802
  this.offset = 0;
3733
3803
  this.position = void 0;
3734
3804
  emitListStateChange(this, { type: "reset" });
@@ -3804,43 +3874,857 @@ function memoRenderItemBy(keyOf, renderItem, options = {}) {
3804
3874
  });
3805
3875
  }
3806
3876
  //#endregion
3807
- //#region src/renderer/virtualized/base.ts
3808
- const ALPHA_EPSILON = .001;
3809
- function clamp$3(value, min, max) {
3877
+ //#region src/renderer/virtualized/base-animation.ts
3878
+ function clamp$1(value, min, max) {
3810
3879
  return Math.min(Math.max(value, min), max);
3811
3880
  }
3812
3881
  function sameState(state, position, offset) {
3813
3882
  return Object.is(state.position, position) && Object.is(state.offset, offset);
3814
3883
  }
3884
+ function resolveJumpSegmentIndex(anchor, direction, itemCount) {
3885
+ if (itemCount <= 0) return;
3886
+ if (direction > 0) {
3887
+ if (anchor >= itemCount) return;
3888
+ return clamp$1(Math.floor(anchor), 0, itemCount - 1);
3889
+ }
3890
+ if (anchor <= 0) return;
3891
+ return clamp$1(Math.ceil(anchor) - 1, 0, itemCount - 1);
3892
+ }
3893
+ function buildJumpPath(itemCount, readItemHeight, startAnchor, targetAnchor) {
3894
+ const clampedStartAnchor = clamp$1(startAnchor, 0, itemCount);
3895
+ const clampedTargetAnchor = clamp$1(targetAnchor, 0, itemCount);
3896
+ if (itemCount <= 0 || !Number.isFinite(clampedStartAnchor) || !Number.isFinite(clampedTargetAnchor) || Math.abs(clampedTargetAnchor - clampedStartAnchor) <= Number.EPSILON) return {
3897
+ startAnchor: clampedStartAnchor,
3898
+ targetAnchor: clampedTargetAnchor,
3899
+ totalDistance: 0,
3900
+ segments: []
3901
+ };
3902
+ const direction = clampedTargetAnchor > clampedStartAnchor ? 1 : -1;
3903
+ const segments = [];
3904
+ let cursor = clampedStartAnchor;
3905
+ let totalDistance = 0;
3906
+ while (direction > 0 ? cursor < clampedTargetAnchor : cursor > clampedTargetAnchor) {
3907
+ const index = resolveJumpSegmentIndex(cursor, direction, itemCount);
3908
+ if (index == null) break;
3909
+ const nextCursor = direction > 0 ? Math.min(clampedTargetAnchor, index + 1) : Math.max(clampedTargetAnchor, index);
3910
+ if (Math.abs(nextCursor - cursor) <= Number.EPSILON) {
3911
+ cursor = nextCursor;
3912
+ continue;
3913
+ }
3914
+ const height = readItemHeight(index);
3915
+ const distance = height > 0 ? Math.abs(nextCursor - cursor) * height : 0;
3916
+ if (distance > 0) {
3917
+ segments.push({
3918
+ anchorStart: cursor,
3919
+ anchorEnd: nextCursor,
3920
+ distanceStart: totalDistance,
3921
+ distanceEnd: totalDistance + distance
3922
+ });
3923
+ totalDistance += distance;
3924
+ }
3925
+ cursor = nextCursor;
3926
+ }
3927
+ return {
3928
+ startAnchor: clampedStartAnchor,
3929
+ targetAnchor: clampedTargetAnchor,
3930
+ totalDistance,
3931
+ segments
3932
+ };
3933
+ }
3815
3934
  function smoothstep(value) {
3816
3935
  return value * value * (3 - 2 * value);
3817
3936
  }
3818
3937
  function getProgress(startTime, duration, now) {
3819
3938
  if (!(duration > 0)) return 1;
3820
- return clamp$3((now - startTime) / duration, 0, 1);
3939
+ return clamp$1((now - startTime) / duration, 0, 1);
3821
3940
  }
3822
3941
  function interpolate(from, to, startTime, duration, now) {
3823
3942
  const progress = getProgress(startTime, duration, now);
3824
3943
  const eased = progress >= 1 ? 1 : smoothstep(progress);
3825
3944
  return from + (to - from) * eased;
3826
3945
  }
3946
+ function getAnchorAtDistance(path, distance) {
3947
+ if (!(path.totalDistance > 0) || path.segments.length === 0) return path.targetAnchor;
3948
+ const clampedDistance = clamp$1(distance, 0, path.totalDistance);
3949
+ if (clampedDistance <= 0) return path.startAnchor;
3950
+ if (clampedDistance >= path.totalDistance) return path.targetAnchor;
3951
+ for (const segment of path.segments) {
3952
+ if (clampedDistance >= segment.distanceEnd) continue;
3953
+ const span = segment.distanceEnd - segment.distanceStart;
3954
+ if (!(span > 0)) continue;
3955
+ const ratio = (clampedDistance - segment.distanceStart) / span;
3956
+ return segment.anchorStart + (segment.anchorEnd - segment.anchorStart) * ratio;
3957
+ }
3958
+ return path.targetAnchor;
3959
+ }
3827
3960
  function getNow() {
3828
3961
  return globalThis.performance?.now() ?? Date.now();
3829
3962
  }
3963
+ //#endregion
3964
+ //#region src/renderer/virtualized/frame-session.ts
3965
+ function prepareFrameSession(params) {
3966
+ let solution = params.resolveVisibleWindow(params.now);
3967
+ let viewportTranslateY = params.getViewportTranslateY(params.now);
3968
+ params.captureVisibleItemSnapshot(solution, viewportTranslateY);
3969
+ const requestSettleRedraw = params.pruneTransitionAnimations(solution.window, params.now);
3970
+ if (requestSettleRedraw) {
3971
+ solution = params.resolveVisibleWindow(params.now);
3972
+ viewportTranslateY = params.getViewportTranslateY(params.now);
3973
+ params.captureVisibleItemSnapshot(solution, viewportTranslateY);
3974
+ }
3975
+ return {
3976
+ solution,
3977
+ viewportTranslateY,
3978
+ requestSettleRedraw
3979
+ };
3980
+ }
3981
+ //#endregion
3982
+ //#region src/renderer/virtualized/jump-controller.ts
3983
+ var JumpController = class {
3984
+ #autoFollowLatch;
3985
+ #controlledState;
3986
+ #jumpAnimation;
3987
+ #lastCommittedState;
3988
+ #hasPendingListChange = false;
3989
+ #options;
3990
+ constructor(options) {
3991
+ this.#options = options;
3992
+ }
3993
+ 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;
3997
+ }
3998
+ prepare(now) {
3999
+ const animation = this.#jumpAnimation;
4000
+ if (animation == null) return false;
4001
+ if (this.#options.getItemCount() === 0) {
4002
+ this.#cancelJumpAnimation();
4003
+ return false;
4004
+ }
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
+ const progress = getProgress(animation.startTime, animation.duration, now);
4011
+ const eased = progress >= 1 ? 1 : smoothstep(progress);
4012
+ const anchor = getAnchorAtDistance(animation.path, animation.path.totalDistance * eased);
4013
+ this.#options.applyAnchor(anchor);
4014
+ animation.needsMoreFrames = progress < 1;
4015
+ return animation.needsMoreFrames;
4016
+ }
4017
+ finishFrame(requestRedraw) {
4018
+ const animation = this.#jumpAnimation;
4019
+ if (animation == null) return requestRedraw;
4020
+ if (animation.needsMoreFrames) {
4021
+ this.#controlledState = this.#options.readListState();
4022
+ return true;
4023
+ }
4024
+ const onComplete = animation.onComplete;
4025
+ this.#cancelJumpAnimation();
4026
+ onComplete?.();
4027
+ return requestRedraw || this.#jumpAnimation != null;
4028
+ }
4029
+ commit(state) {
4030
+ this.#lastCommittedState = {
4031
+ position: state.position,
4032
+ offset: state.offset
4033
+ };
4034
+ }
4035
+ jumpTo(index, options = {}) {
4036
+ this.#clearAutoFollowLatch();
4037
+ if (this.#options.getItemCount() === 0) {
4038
+ this.#cancelJumpAnimation();
4039
+ return;
4040
+ }
4041
+ this.#startJumpToIndex(index, options, { kind: "manual" });
4042
+ }
4043
+ handleListStateChange(change) {
4044
+ this.#hasPendingListChange = true;
4045
+ 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
+ };
4063
+ }
4064
+ return change;
4065
+ }
4066
+ #cancelJumpAnimation() {
4067
+ this.#jumpAnimation = void 0;
4068
+ this.#controlledState = void 0;
4069
+ }
4070
+ #startJumpToIndex(index, options, source) {
4071
+ const targetIndex = this.#options.clampItemIndex(index);
4072
+ const currentState = this.#options.normalizeListState(this.#options.readListState());
4073
+ const targetBlock = options.block ?? this.#options.getDefaultJumpBlock();
4074
+ const targetAnchor = this.#options.getTargetAnchor(targetIndex, targetBlock);
4075
+ if (!(options.animated ?? true)) {
4076
+ this.#cancelJumpAnimation();
4077
+ this.#options.applyAnchor(targetAnchor);
4078
+ options.onComplete?.();
4079
+ return;
4080
+ }
4081
+ const startAnchor = this.#options.readAnchor(currentState);
4082
+ if (!Number.isFinite(startAnchor)) {
4083
+ this.#cancelJumpAnimation();
4084
+ this.#options.applyAnchor(targetAnchor);
4085
+ options.onComplete?.();
4086
+ return;
4087
+ }
4088
+ const path = buildJumpPath(this.#options.getItemCount(), this.#options.getItemHeight, startAnchor, targetAnchor);
4089
+ const duration = clamp$1(options.duration ?? this.#options.minJumpDuration + path.totalDistance * this.#options.jumpDurationPerPixel, 0, this.#options.maxJumpDuration);
4090
+ if (duration <= 0 || path.totalDistance <= Number.EPSILON) {
4091
+ this.#cancelJumpAnimation();
4092
+ this.#options.applyAnchor(targetAnchor);
4093
+ options.onComplete?.();
4094
+ return;
4095
+ }
4096
+ this.#jumpAnimation = {
4097
+ path,
4098
+ startTime: getNow(),
4099
+ duration,
4100
+ needsMoreFrames: true,
4101
+ onComplete: options.onComplete,
4102
+ source
4103
+ };
4104
+ this.#controlledState = this.#options.readListState();
4105
+ }
4106
+ #resolveAutoFollowChange(change) {
4107
+ switch (change.type) {
4108
+ case "push":
4109
+ case "unshift": return change.animation?.followIfAtBoundary === true ? {
4110
+ change,
4111
+ direction: change.type,
4112
+ count: change.count,
4113
+ animation: change.animation
4114
+ } : void 0;
4115
+ default: return;
4116
+ }
4117
+ }
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);
4121
+ }
4122
+ #shouldLatchAutoFollow(direction, count, animation) {
4123
+ if (animation?.followIfAtBoundary !== true) return false;
4124
+ if (!this.#matchesLastCommittedStateAfterBoundaryInsert(direction, count)) {
4125
+ this.#clearAutoFollowLatch();
4126
+ return false;
4127
+ }
4128
+ return this.#autoFollowLatch === direction;
4129
+ }
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;
4133
+ }
4134
+ #rebaseJumpAnchorForBoundaryInsert(direction, count, now) {
4135
+ const animation = this.#jumpAnimation;
4136
+ if (animation == null) return;
4137
+ const progress = getProgress(animation.startTime, animation.duration, now);
4138
+ const eased = progress >= 1 ? 1 : smoothstep(progress);
4139
+ const anchorAtNow = getAnchorAtDistance(animation.path, animation.path.totalDistance * eased);
4140
+ this.#cancelJumpAnimation();
4141
+ this.#options.applyAnchor(direction === "unshift" ? anchorAtNow + count : anchorAtNow);
4142
+ }
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);
4150
+ }
4151
+ #clearAutoFollowLatch() {
4152
+ this.#autoFollowLatch = void 0;
4153
+ }
4154
+ };
4155
+ //#endregion
4156
+ //#region src/renderer/virtualized/transition-snapshot.ts
4157
+ var VisibilitySnapshot = class {
4158
+ #drawnItems = /* @__PURE__ */ new Set();
4159
+ #visibleItems = /* @__PURE__ */ new Set();
4160
+ #hasSnapshot = false;
4161
+ #snapshotState;
4162
+ #emptyState;
4163
+ #coversShortList = false;
4164
+ #topGap = 0;
4165
+ #bottomGap = 0;
4166
+ #atStartBoundary = false;
4167
+ #atEndBoundary = false;
4168
+ get coversShortList() {
4169
+ return this.#hasSnapshot && this.#snapshotState != null && this.#coversShortList;
4170
+ }
4171
+ get topGap() {
4172
+ return this.#topGap;
4173
+ }
4174
+ get bottomGap() {
4175
+ return this.#bottomGap;
4176
+ }
4177
+ capture(window, _resolutionPath, items, viewportHeight, snapshotState, extraShift, readVisibleRange) {
4178
+ const nextDrawnItems = /* @__PURE__ */ new Set();
4179
+ const nextVisibleItems = /* @__PURE__ */ new Set();
4180
+ let minVisibleIndex = Number.POSITIVE_INFINITY;
4181
+ let maxVisibleIndex = Number.NEGATIVE_INFINITY;
4182
+ let topMostY = Number.POSITIVE_INFINITY;
4183
+ let bottomMostY = Number.NEGATIVE_INFINITY;
4184
+ const effectiveShift = window.shift + extraShift;
4185
+ 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;
4189
+ topMostY = Math.min(topMostY, y);
4190
+ bottomMostY = Math.max(bottomMostY, y + height);
4191
+ const item = items[idx];
4192
+ if (item != null) nextDrawnItems.add(item);
4193
+ if (item == null || readVisibleRange(y, height) == null) continue;
4194
+ nextVisibleItems.add(item);
4195
+ }
4196
+ this.#drawnItems = nextDrawnItems;
4197
+ this.#visibleItems = nextVisibleItems;
4198
+ this.#hasSnapshot = true;
4199
+ this.#snapshotState = snapshotState;
4200
+ this.#emptyState = items.length === 0 && window.drawList.length === 0 ? snapshotState : void 0;
4201
+ 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;
4207
+ }
4208
+ matchesCurrentState(position, offset) {
4209
+ return this.#hasSnapshot && this.#snapshotState != null && sameState(this.#snapshotState, position, offset);
4210
+ }
4211
+ matchesBoundaryInsertState(direction, count, position, offset) {
4212
+ if (!this.coversShortList || this.#snapshotState == null) return false;
4213
+ return this.#matchesStateAfterBoundaryInsert(direction, count, position, offset);
4214
+ }
4215
+ matchesFollowBoundaryInsertState(direction, count, position, offset) {
4216
+ if (!this.#hasSnapshot || this.#snapshotState == null) return false;
4217
+ if (direction === "push" ? !this.#atEndBoundary : !this.#atStartBoundary) return false;
4218
+ return this.#matchesStateAfterBoundaryInsert(direction, count, position, offset);
4219
+ }
4220
+ matchesEmptyBoundaryInsertState(direction, count, position, offset) {
4221
+ const emptyState = this.#emptyState;
4222
+ if (!this.#hasSnapshot || emptyState == null) return false;
4223
+ return sameState({
4224
+ position: direction === "unshift" && emptyState.position != null ? emptyState.position + count : emptyState.position,
4225
+ offset: emptyState.offset
4226
+ }, position, offset);
4227
+ }
4228
+ isVisible(item) {
4229
+ return this.#visibleItems.has(item);
4230
+ }
4231
+ tracks(item, retention) {
4232
+ return retention === "drawn" ? this.#drawnItems.has(item) : this.#visibleItems.has(item);
4233
+ }
4234
+ reset() {
4235
+ this.#drawnItems.clear();
4236
+ this.#visibleItems.clear();
4237
+ this.#hasSnapshot = false;
4238
+ this.#snapshotState = void 0;
4239
+ this.#emptyState = void 0;
4240
+ this.#coversShortList = false;
4241
+ this.#topGap = 0;
4242
+ this.#bottomGap = 0;
4243
+ this.#atStartBoundary = false;
4244
+ this.#atEndBoundary = false;
4245
+ }
4246
+ #matchesStateAfterBoundaryInsert(direction, count, position, offset) {
4247
+ const snapshotState = this.#snapshotState;
4248
+ if (snapshotState == null) return false;
4249
+ return sameState({
4250
+ position: direction === "unshift" && snapshotState.position != null ? snapshotState.position + count : snapshotState.position,
4251
+ offset: snapshotState.offset
4252
+ }, position, offset);
4253
+ }
4254
+ };
4255
+ //#endregion
4256
+ //#region src/renderer/virtualized/transition-store.ts
4257
+ var TransitionStore = class {
4258
+ #transitions = /* @__PURE__ */ new Map();
4259
+ get size() {
4260
+ return this.#transitions.size;
4261
+ }
4262
+ has(item) {
4263
+ return this.#transitions.has(item);
4264
+ }
4265
+ set(item, transition) {
4266
+ this.#transitions.set(item, transition);
4267
+ }
4268
+ replace(prevItem, nextItem, transition) {
4269
+ this.#transitions.delete(prevItem);
4270
+ this.#transitions.set(nextItem, transition);
4271
+ }
4272
+ delete(item) {
4273
+ const transition = this.#transitions.get(item);
4274
+ if (transition != null) this.#transitions.delete(item);
4275
+ return transition;
4276
+ }
4277
+ readActive(item, now) {
4278
+ const transition = this.#transitions.get(item);
4279
+ if (transition == null) return;
4280
+ return this.#isComplete(transition, now) ? void 0 : transition;
4281
+ }
4282
+ prepare(now) {
4283
+ for (const transition of this.#transitions.values()) if (!this.#isComplete(transition, now)) return true;
4284
+ return false;
4285
+ }
4286
+ findCompleted(now) {
4287
+ return [...this.#transitions.entries()].filter(([, transition]) => this.#isComplete(transition, now)).map(([item, transition]) => ({
4288
+ item,
4289
+ transition
4290
+ }));
4291
+ }
4292
+ findInvisible(snapshot) {
4293
+ return [...this.#transitions.entries()].filter(([item, transition]) => !snapshot.tracks(item, transition.retention)).map(([item, transition]) => ({
4294
+ item,
4295
+ transition
4296
+ }));
4297
+ }
4298
+ reset() {
4299
+ this.#transitions.clear();
4300
+ }
4301
+ #isComplete(transition, now) {
4302
+ return getProgress(transition.height.startTime, transition.height.duration, now) >= 1;
4303
+ }
4304
+ };
4305
+ //#endregion
4306
+ //#region src/renderer/virtualized/transition-planner.ts
4307
+ function isFinitePositive(value) {
4308
+ return Number.isFinite(value) && value > 0;
4309
+ }
4310
+ function normalizeDuration(duration) {
4311
+ return Math.max(0, typeof duration === "number" && Number.isFinite(duration) ? duration : 0);
4312
+ }
4313
+ function createScalarAnimation(from, to, startTime, duration) {
4314
+ return {
4315
+ from,
4316
+ to,
4317
+ startTime,
4318
+ duration
4319
+ };
4320
+ }
4321
+ function createLayerAnimation(node, fromAlpha, toAlpha, startTime, duration, fromTranslateY, toTranslateY) {
4322
+ return {
4323
+ node,
4324
+ alpha: createScalarAnimation(fromAlpha, toAlpha, startTime, duration),
4325
+ translateY: createScalarAnimation(fromTranslateY, toTranslateY, startTime, duration)
4326
+ };
4327
+ }
4328
+ function findVisibleEntry(index, resolveVisibleWindow, readVisibleRange) {
4329
+ if (index < 0) return;
4330
+ const solution = resolveVisibleWindow();
4331
+ for (const entry of solution.window.drawList) {
4332
+ if (entry.idx !== index) continue;
4333
+ if (readVisibleRange(entry.offset + solution.window.shift, entry.height) != null) return entry;
4334
+ }
4335
+ }
4336
+ function isIndexVisible(index, resolveVisibleWindow, readVisibleRange) {
4337
+ return findVisibleEntry(index, resolveVisibleWindow, readVisibleRange) != null;
4338
+ }
4339
+ function resolveAnimationEligibility(params) {
4340
+ 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);
4343
+ }
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";
4348
+ }
4349
+ function sampleScalarAnimation(animation, now) {
4350
+ return interpolate(animation.from, animation.to, animation.startTime, animation.duration, now);
4351
+ }
4352
+ function sampleLayerAnimation(layer, now) {
4353
+ const alpha = sampleScalarAnimation(layer.alpha, now);
4354
+ if (alpha <= .001) return;
4355
+ return {
4356
+ alpha,
4357
+ node: layer.node,
4358
+ translateY: sampleScalarAnimation(layer.translateY, now)
4359
+ };
4360
+ }
4361
+ function sampleTransition(transition, now) {
4362
+ return {
4363
+ kind: transition.kind,
4364
+ slotHeight: sampleScalarAnimation(transition.height, now),
4365
+ layers: transition.layers.map((layer) => sampleLayerAnimation(layer, now)).filter((layer) => layer != null),
4366
+ retention: transition.retention
4367
+ };
4368
+ }
4369
+ function planExistingItemTransition(params) {
4370
+ if (!params.canAnimate || params.duration <= 0) return;
4371
+ if (params.kind === "update" && !Number.isFinite(params.nextHeight)) return;
4372
+ const layers = [];
4373
+ if (params.currentVisualState.alpha > .001) layers.push(createLayerAnimation(params.currentVisualState.node, params.currentVisualState.alpha, 0, params.now, params.duration, params.currentVisualState.translateY, 0));
4374
+ if (params.kind === "update") {
4375
+ layers.push(createLayerAnimation(params.nextNode, 0, 1, params.now, params.duration, params.currentVisualState.translateY, 0));
4376
+ return {
4377
+ kind: "update",
4378
+ layers,
4379
+ height: createScalarAnimation(params.currentVisualState.height, params.nextHeight, params.now, params.duration),
4380
+ retention: "visible"
4381
+ };
4382
+ }
4383
+ return {
4384
+ kind: "delete",
4385
+ layers,
4386
+ height: createScalarAnimation(params.currentVisualState.height, 0, params.now, params.duration),
4387
+ retention: "visible"
4388
+ };
4389
+ }
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
+ function planBoundaryInsertItems(params) {
4402
+ const entries = [];
4403
+ const signedDistance = params.direction === "push" ? 1 : -1;
4404
+ for (const { item, node, height } of params.measuredItems) {
4405
+ 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
+ entries.push({
4408
+ item,
4409
+ transition: {
4410
+ kind: "insert",
4411
+ layers: [createLayerAnimation(node, 0, 1, params.now, params.duration, signedDistance * resolvedDistance, 0)],
4412
+ height: createScalarAnimation(height, height, params.now, params.duration),
4413
+ retention: "drawn"
4414
+ }
4415
+ });
4416
+ }
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
+ };
4442
+ }
4443
+ function measureBoundaryInsertItems(direction, count, ctx) {
4444
+ const start = direction === "push" ? ctx.items.length - count : 0;
4445
+ const end = direction === "push" ? ctx.items.length : Math.min(count, ctx.items.length);
4446
+ if (start < 0 || end < start) return;
4447
+ const measured = [];
4448
+ for (let index = start; index < end; index += 1) {
4449
+ const item = ctx.items[index];
4450
+ if (item == null) continue;
4451
+ const node = ctx.renderItem(item);
4452
+ const height = ctx.measureNode(node).height;
4453
+ measured.push({
4454
+ item,
4455
+ node,
4456
+ height
4457
+ });
4458
+ }
4459
+ return measured;
4460
+ }
4461
+ function drawSampledLayers(sampled, y, adapter) {
4462
+ if (sampled.slotHeight <= 0) return false;
4463
+ let result = false;
4464
+ for (const layer of sampled.layers) {
4465
+ const alpha = clamp$1(layer.alpha, 0, 1);
4466
+ if (alpha <= .001) continue;
4467
+ adapter.graphics.save();
4468
+ try {
4469
+ if (typeof adapter.graphics.globalAlpha === "number") adapter.graphics.globalAlpha *= alpha;
4470
+ if (adapter.drawNode(layer.node, 0, y + layer.translateY)) result = true;
4471
+ } finally {
4472
+ adapter.graphics.restore();
4473
+ }
4474
+ }
4475
+ return result;
4476
+ }
4477
+ function planUpdateTransition(prevItem, nextItem, duration, now, currentVisualState, ctx, snapshot, store) {
4478
+ const nextIndex = ctx.items.indexOf(nextItem);
4479
+ const nextNode = ctx.renderItem(nextItem);
4480
+ const nextHeight = ctx.measureNode(nextNode).height;
4481
+ return planExistingItemTransition({
4482
+ kind: "update",
4483
+ duration: normalizeDuration(duration),
4484
+ canAnimate: resolveAnimationEligibility({
4485
+ index: nextIndex,
4486
+ item: prevItem,
4487
+ position: ctx.position,
4488
+ offset: ctx.offset,
4489
+ snapshot,
4490
+ hasActiveTransition: store.has(prevItem),
4491
+ resolveVisibleWindow: ctx.resolveVisibleWindow,
4492
+ readVisibleRange: ctx.readVisibleRange
4493
+ }),
4494
+ now,
4495
+ currentVisualState,
4496
+ nextNode,
4497
+ nextHeight
4498
+ });
4499
+ }
4500
+ function planDeleteTransition(item, duration, now, currentVisualState, ctx, snapshot, store) {
4501
+ const index = ctx.items.indexOf(item);
4502
+ return planExistingItemTransition({
4503
+ kind: "delete",
4504
+ duration: normalizeDuration(duration),
4505
+ canAnimate: resolveAnimationEligibility({
4506
+ index,
4507
+ item,
4508
+ position: ctx.position,
4509
+ offset: ctx.offset,
4510
+ snapshot,
4511
+ hasActiveTransition: store.has(item),
4512
+ resolveVisibleWindow: ctx.resolveVisibleWindow,
4513
+ readVisibleRange: ctx.readVisibleRange
4514
+ }),
4515
+ now,
4516
+ currentVisualState
4517
+ });
4518
+ }
4519
+ function planBoundaryInsertTransition(direction, count, duration, distance, now, currentTranslateY, ctx, snapshot) {
4520
+ const normalizedDuration = normalizeDuration(duration);
4521
+ 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;
4524
+ const measuredItems = measureBoundaryInsertItems(direction, count, ctx);
4525
+ if (measuredItems == null) return;
4526
+ return planBoundaryInsert({
4527
+ direction,
4528
+ duration: normalizedDuration,
4529
+ distance,
4530
+ now,
4531
+ strategy,
4532
+ snapshot,
4533
+ currentTranslateY,
4534
+ measuredItems
4535
+ });
4536
+ }
4537
+ function getTransitionedItemHeight(item, now, store, adapter) {
4538
+ const transition = store.readActive(item, now);
4539
+ if (transition != null) return sampleTransition(transition, now).slotHeight;
4540
+ const node = adapter.renderItem(item);
4541
+ return adapter.measureNode(node).height;
4542
+ }
4543
+ function resolveTransitionedItem(item, now, store, adapter, lifecycle) {
4544
+ const transition = store.readActive(item, now);
4545
+ if (transition == null) {
4546
+ const node = adapter.renderItem(item);
4547
+ return {
4548
+ value: {
4549
+ draw: (y) => adapter.drawNode(node, 0, y),
4550
+ hittest: (test, y) => node.hittest(adapter.getRootContext(), {
4551
+ ...test,
4552
+ y: test.y - y
4553
+ })
4554
+ },
4555
+ height: adapter.measureNode(node).height
4556
+ };
4557
+ }
4558
+ const sampled = sampleTransition(transition, now);
4559
+ return {
4560
+ value: {
4561
+ draw: (y) => drawSampledLayers(sampled, y, adapter),
4562
+ hittest: () => false
4563
+ },
4564
+ height: sampled.slotHeight
4565
+ };
4566
+ }
4567
+ function readCurrentVisualState(item, now, store, adapter) {
4568
+ const transition = store.readActive(item, now);
4569
+ if (transition != null && transition.layers.length > 0) {
4570
+ const primaryLayer = transition.layers[transition.layers.length - 1];
4571
+ return {
4572
+ node: primaryLayer.node,
4573
+ alpha: sampleScalarAnimation(primaryLayer.alpha, now),
4574
+ height: sampleScalarAnimation(transition.height, now),
4575
+ translateY: sampleScalarAnimation(primaryLayer.translateY, now)
4576
+ };
4577
+ }
4578
+ const node = adapter.renderItem(item);
4579
+ return {
4580
+ node,
4581
+ alpha: 1,
4582
+ height: adapter.measureNode(node).height,
4583
+ translateY: 0
4584
+ };
4585
+ }
4586
+ function handleTransitionStateChange(store, snapshot, currentViewportTranslateY, change, ctx, lifecycle) {
4587
+ switch (change.type) {
4588
+ case "update": {
4589
+ const now = getNow();
4590
+ const currentVisualState = readCurrentVisualState(change.prevItem, now, store, ctx);
4591
+ const transition = planUpdateTransition(change.prevItem, change.nextItem, change.animation?.duration, now, currentVisualState, ctx, snapshot, store);
4592
+ if (transition == null) {
4593
+ store.delete(change.prevItem);
4594
+ return {};
4595
+ }
4596
+ store.replace(change.prevItem, change.nextItem, transition);
4597
+ return {};
4598
+ }
4599
+ case "delete": {
4600
+ const now = getNow();
4601
+ const currentVisualState = readCurrentVisualState(change.item, now, store, ctx);
4602
+ const transition = planDeleteTransition(change.item, change.animation?.duration, now, currentVisualState, ctx, snapshot, store);
4603
+ if (transition == null) {
4604
+ store.delete(change.item);
4605
+ lifecycle.onDeleteComplete(change.item);
4606
+ return {};
4607
+ }
4608
+ store.set(change.item, transition);
4609
+ return {};
4610
+ }
4611
+ case "delete-finalize":
4612
+ store.delete(change.item);
4613
+ return {};
4614
+ case "unshift":
4615
+ case "push": {
4616
+ 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 };
4620
+ for (const entry of plan.entries) store.set(entry.item, entry.transition);
4621
+ return {};
4622
+ }
4623
+ case "reset":
4624
+ case "set":
4625
+ store.reset();
4626
+ snapshot.reset();
4627
+ return {};
4628
+ }
4629
+ }
4630
+ //#endregion
4631
+ //#region src/renderer/virtualized/base-transition.ts
4632
+ var TransitionController = class {
4633
+ #store = new TransitionStore();
4634
+ #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);
4638
+ }
4639
+ pruneInvisible(lifecycle) {
4640
+ return this.pruneInvisibleAt(getNow(), lifecycle);
4641
+ }
4642
+ prepare(now, lifecycle) {
4643
+ 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);
4651
+ }
4652
+ canAutoFollowBoundaryInsert(direction, count, position, offset) {
4653
+ return this.#snapshot.matchesFollowBoundaryInsertState(direction, count, position, offset);
4654
+ }
4655
+ getItemHeight(item, now, adapter) {
4656
+ return getTransitionedItemHeight(item, now, this.#store, adapter);
4657
+ }
4658
+ resolveItem(item, now, adapter, lifecycle) {
4659
+ return resolveTransitionedItem(item, now, this.#store, adapter, lifecycle);
4660
+ }
4661
+ handleListStateChange(change, ctx, lifecycle) {
4662
+ const now = getNow();
4663
+ 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;
4670
+ }
4671
+ settle(now, lifecycle) {
4672
+ const changed = this.#settleTransitions(this.#store.findCompleted(now), now, lifecycle);
4673
+ this.#cleanupViewportTranslateAnimation(now);
4674
+ return changed;
4675
+ }
4676
+ pruneInvisibleAt(now, lifecycle) {
4677
+ return this.#settleTransitions(this.#store.findInvisible(this.#snapshot), now, lifecycle);
4678
+ }
4679
+ reset() {
4680
+ this.#store.reset();
4681
+ this.#snapshot.reset();
4682
+ this.#viewportTranslateAnimation = void 0;
4683
+ }
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) {
4690
+ if (removals.length === 0) return false;
4691
+ const anchor = lifecycle.captureVisualAnchor(now);
4692
+ for (const { item, transition } of removals) {
4693
+ this.#store.delete(item);
4694
+ if (transition.kind === "delete") lifecycle.onDeleteComplete(item);
4695
+ }
4696
+ if (anchor != null && Number.isFinite(anchor)) lifecycle.restoreVisualAnchor(anchor);
4697
+ return true;
4698
+ }
4699
+ };
4700
+ //#endregion
4701
+ //#region src/renderer/virtualized/base.ts
3830
4702
  /**
3831
4703
  * Shared base class for virtualized list renderers.
3832
4704
  */
3833
4705
  var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
3834
4706
  static MIN_JUMP_DURATION = 160;
3835
4707
  static MAX_JUMP_DURATION = 420;
3836
- static JUMP_DURATION_PER_ITEM = 28;
3837
- #controlledState;
3838
- #jumpAnimation;
3839
- #replacementAnimations = /* @__PURE__ */ new WeakMap();
3840
- #activeReplacementItems = /* @__PURE__ */ new Set();
3841
- #nextReplacementLayerKey = 0;
4708
+ static JUMP_DURATION_PER_PIXEL = .7;
4709
+ #jumpController;
4710
+ #transitionController = new TransitionController();
3842
4711
  constructor(graphics, options) {
3843
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
+ });
3844
4728
  subscribeListState(options.list, this, (owner, change) => {
3845
4729
  owner.#handleListStateChange(change);
3846
4730
  });
@@ -3869,57 +4753,57 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
3869
4753
  set items(value) {
3870
4754
  this.options.list.items = value;
3871
4755
  }
4756
+ /** Renders the current visible window. */
4757
+ render(feedback) {
4758
+ this.#jumpController.beforeFrame();
4759
+ const now = getNow();
4760
+ const keepAnimating = this._prepareRender(now);
4761
+ const { clientWidth: viewportWidth, clientHeight: viewportHeight } = this.graphics.canvas;
4762
+ this.graphics.clearRect(0, 0, viewportWidth, viewportHeight);
4763
+ const frame = prepareFrameSession({
4764
+ now,
4765
+ resolveVisibleWindow: (frameNow) => this._resolveVisibleWindow(frameNow),
4766
+ getViewportTranslateY: (frameNow) => this.#transitionController.getViewportTranslateY(frameNow),
4767
+ captureVisibleItemSnapshot: (solution, extraShift) => this._captureVisibleItemSnapshot(solution, extraShift),
4768
+ pruneTransitionAnimations: (window, frameNow) => this._pruneTransitionAnimations(window, frameNow)
4769
+ });
4770
+ const requestRedraw = this._renderVisibleWindow(frame.solution.window, feedback, frame.viewportTranslateY);
4771
+ this._commitListState(frame.solution.normalizedState);
4772
+ return this._finishRender(keepAnimating || requestRedraw || frame.requestSettleRedraw);
4773
+ }
4774
+ /** Hit-tests the current visible window. */
4775
+ hittest(test) {
4776
+ this.#jumpController.beforeFrame();
4777
+ const now = getNow();
4778
+ this.#transitionController.settle(now, this.#getTransitionLifecycleAdapter());
4779
+ const frame = prepareFrameSession({
4780
+ now,
4781
+ resolveVisibleWindow: (frameNow) => this._resolveVisibleWindow(frameNow),
4782
+ getViewportTranslateY: (frameNow) => this.#transitionController.getViewportTranslateY(frameNow),
4783
+ captureVisibleItemSnapshot: (solution, extraShift) => this._captureVisibleItemSnapshot(solution, extraShift),
4784
+ pruneTransitionAnimations: (window, frameNow) => this._pruneTransitionAnimations(window, frameNow)
4785
+ });
4786
+ return this._hittestVisibleWindow(frame.solution.window, test, frame.viewportTranslateY);
4787
+ }
3872
4788
  _readListState() {
3873
4789
  return {
3874
4790
  position: this.position,
3875
4791
  offset: this.offset
3876
4792
  };
3877
4793
  }
4794
+ _resolveVisibleWindow(now) {
4795
+ return this._resolveVisibleWindowForState(this._readListState(), now);
4796
+ }
3878
4797
  _commitListState(state) {
3879
4798
  this.position = state.position;
3880
4799
  this.offset = state.offset;
4800
+ this.#jumpController.commit(state);
3881
4801
  }
3882
4802
  /**
3883
4803
  * Scrolls the viewport to the requested item index.
3884
4804
  */
3885
4805
  jumpTo(index, options = {}) {
3886
- if (this.items.length === 0) {
3887
- this.#cancelJumpAnimation();
3888
- return;
3889
- }
3890
- const targetIndex = this._clampItemIndex(index);
3891
- const currentState = this._normalizeListState(this._readListState());
3892
- const targetBlock = options.block ?? this._getDefaultJumpBlock();
3893
- const targetAnchor = this._getTargetAnchor(targetIndex, targetBlock);
3894
- if (!(options.animated ?? true)) {
3895
- this.#cancelJumpAnimation();
3896
- this._applyAnchor(targetAnchor);
3897
- options.onComplete?.();
3898
- return;
3899
- }
3900
- const startAnchor = this._readAnchor(currentState);
3901
- if (!Number.isFinite(startAnchor)) {
3902
- this.#cancelJumpAnimation();
3903
- this._applyAnchor(targetAnchor);
3904
- options.onComplete?.();
3905
- return;
3906
- }
3907
- const duration = clamp$3(options.duration ?? VirtualizedRenderer.MIN_JUMP_DURATION + Math.abs(targetAnchor - startAnchor) * VirtualizedRenderer.JUMP_DURATION_PER_ITEM, 0, VirtualizedRenderer.MAX_JUMP_DURATION);
3908
- if (duration <= 0 || Math.abs(targetAnchor - startAnchor) <= Number.EPSILON) {
3909
- this.#cancelJumpAnimation();
3910
- this._applyAnchor(targetAnchor);
3911
- options.onComplete?.();
3912
- return;
3913
- }
3914
- this.#jumpAnimation = {
3915
- startAnchor,
3916
- targetAnchor,
3917
- startTime: getNow(),
3918
- duration,
3919
- needsMoreFrames: true,
3920
- onComplete: options.onComplete
3921
- };
3922
- this.#controlledState = this._readListState();
4806
+ this.#jumpController.jumpTo(index, options);
3923
4807
  }
3924
4808
  _resetRenderFeedback(feedback) {
3925
4809
  if (feedback == null) return;
@@ -3929,13 +4813,10 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
3929
4813
  feedback.max = NaN;
3930
4814
  }
3931
4815
  _accumulateRenderFeedback(feedback, idx, top, height) {
3932
- if (!Number.isFinite(top) || !Number.isFinite(height) || height <= 0) return;
3933
- const viewportHeight = this.graphics.canvas.clientHeight;
3934
- const visibleTop = clamp$3(-top, 0, height);
3935
- const visibleBottom = clamp$3(viewportHeight - top, 0, height);
3936
- if (visibleBottom <= visibleTop) return;
3937
- const itemMin = idx + visibleTop / height;
3938
- const itemMax = idx + visibleBottom / height;
4816
+ const visibleRange = this._readVisibleRange(top, height);
4817
+ if (visibleRange == null) return;
4818
+ const itemMin = idx + visibleRange.top / height;
4819
+ const itemMax = idx + visibleRange.bottom / height;
3939
4820
  feedback.minIdx = Number.isNaN(feedback.minIdx) ? idx : Math.min(idx, feedback.minIdx);
3940
4821
  feedback.maxIdx = Number.isNaN(feedback.maxIdx) ? idx : Math.max(idx, feedback.maxIdx);
3941
4822
  feedback.min = Number.isNaN(feedback.min) ? itemMin : Math.min(itemMin, feedback.min);
@@ -3952,367 +4833,296 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
3952
4833
  }
3953
4834
  return result;
3954
4835
  }
3955
- _renderVisibleWindow(window, feedback) {
4836
+ _renderVisibleWindow(window, feedback, extraShift = 0) {
3956
4837
  this._resetRenderFeedback(feedback);
3957
- return this._renderDrawList(window.drawList, window.shift, feedback);
4838
+ return this._renderDrawList(window.drawList, window.shift + extraShift, feedback);
3958
4839
  }
3959
- _hittestVisibleWindow(window, test) {
4840
+ _readVisibleRange(top, height) {
4841
+ if (!Number.isFinite(top) || !Number.isFinite(height) || height <= 0) return;
4842
+ const viewportHeight = this.graphics.canvas.clientHeight;
4843
+ const visibleTop = clamp$1(-top, 0, height);
4844
+ const visibleBottom = clamp$1(viewportHeight - top, 0, height);
4845
+ if (visibleBottom <= visibleTop) return;
4846
+ return {
4847
+ top: visibleTop,
4848
+ bottom: visibleBottom
4849
+ };
4850
+ }
4851
+ _pruneTransitionAnimations(_window, now) {
4852
+ return this.#transitionController.pruneInvisibleAt(now, this.#getTransitionLifecycleAdapter());
4853
+ }
4854
+ _hittestVisibleWindow(window, test, extraShift = 0) {
3960
4855
  for (const { value: item, offset, height } of window.drawList) {
3961
- const y = offset + window.shift;
4856
+ const y = offset + window.shift + extraShift;
3962
4857
  if (test.y < y || test.y >= y + height) continue;
3963
4858
  return item.hittest(test, y);
3964
4859
  }
3965
4860
  return false;
3966
4861
  }
3967
- _prepareRender() {
3968
- const now = getNow();
3969
- const keepReplacing = this.#prepareReplacementAnimations(now);
3970
- const animation = this.#jumpAnimation;
3971
- if (animation == null) return keepReplacing;
3972
- if (this.items.length === 0) {
3973
- this.#cancelJumpAnimation();
3974
- return keepReplacing;
3975
- }
3976
- if (this.#controlledState != null && !sameState(this.#controlledState, this.position, this.offset)) {
3977
- this.#cancelJumpAnimation();
3978
- return keepReplacing;
3979
- }
3980
- const anchor = interpolate(animation.startAnchor, animation.targetAnchor, animation.startTime, animation.duration, now);
3981
- const progress = getProgress(animation.startTime, animation.duration, now);
3982
- this._applyAnchor(anchor);
3983
- animation.needsMoreFrames = progress < 1;
3984
- return keepReplacing || animation.needsMoreFrames;
4862
+ _captureVisibleItemSnapshot(solution, extraShift = 0) {
4863
+ 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));
4865
+ }
4866
+ _prepareRender(now) {
4867
+ const keepTransitioning = this.#transitionController.prepare(now, this.#getTransitionLifecycleAdapter());
4868
+ const keepJumping = this.#jumpController.prepare(now);
4869
+ return keepTransitioning || keepJumping;
3985
4870
  }
3986
4871
  _finishRender(requestRedraw) {
3987
- const animation = this.#jumpAnimation;
3988
- if (animation == null) return requestRedraw;
3989
- if (animation.needsMoreFrames) {
3990
- this.#controlledState = this._readListState();
3991
- return true;
3992
- }
3993
- const onComplete = animation.onComplete;
3994
- this.#cancelJumpAnimation();
3995
- onComplete?.();
3996
- return requestRedraw || this.#jumpAnimation != null;
4872
+ return this.#jumpController.finishFrame(requestRedraw);
3997
4873
  }
3998
4874
  _clampItemIndex(index) {
3999
- return clamp$3(Number.isFinite(index) ? Math.trunc(index) : 0, 0, this.items.length - 1);
4875
+ return clamp$1(Number.isFinite(index) ? Math.trunc(index) : 0, 0, this.items.length - 1);
4000
4876
  }
4001
4877
  _getItemHeight(index) {
4002
- const now = getNow();
4878
+ return this._getItemHeightAt(index, getNow());
4879
+ }
4880
+ _getItemHeightAt(index, now) {
4003
4881
  const item = this.items[index];
4004
- const replacement = this.#readReplacementAnimation(item, now);
4005
- if (replacement != null) return this.#sampleReplacementHeight(replacement, now);
4006
- const node = this.options.renderItem(item);
4007
- return this.measureRootNode(node).height;
4882
+ return this.#transitionController.getItemHeight(item, now, {
4883
+ renderItem: this.options.renderItem,
4884
+ measureNode: this.measureRootNode.bind(this)
4885
+ });
4008
4886
  }
4009
- _resolveItem(item, _index, now) {
4010
- const replacement = this.#readReplacementAnimation(item, now);
4011
- if (replacement == null) {
4012
- const node = this.options.renderItem(item);
4013
- return {
4014
- value: {
4015
- draw: (y) => this.drawRootNode(node, 0, y),
4016
- hittest: (test, y) => node.hittest(this.getRootContext(), {
4017
- ...test,
4018
- y: test.y - y
4019
- })
4020
- },
4021
- height: this.measureRootNode(node).height
4022
- };
4023
- }
4024
- const slotHeight = this.#sampleReplacementHeight(replacement, now);
4025
- const layers = replacement.layers.map((layer) => ({
4026
- alpha: this.#sampleLayerAlpha(layer, now),
4027
- node: layer.node,
4028
- nodeHeight: this.measureRootNode(layer.node).height
4029
- })).filter((layer) => layer.alpha > ALPHA_EPSILON);
4030
- return {
4031
- value: {
4032
- draw: (y) => this.#drawReplacementLayers(layers, slotHeight, y),
4033
- hittest: () => false
4034
- },
4035
- height: slotHeight
4036
- };
4887
+ _readAnchorAt(now) {
4888
+ if (this.items.length <= 0) return;
4889
+ const state = this._normalizeListState(this._readListState());
4890
+ return this._readAnchor(state, (index) => this._getItemHeightAt(index, now));
4037
4891
  }
4038
- _getAnchorAtOffset(index, offset) {
4039
- if (this.items.length === 0) return 0;
4040
- let currentIndex = this._clampItemIndex(index);
4041
- let remaining = Number.isFinite(offset) ? offset : 0;
4042
- while (true) {
4043
- if (remaining < 0) {
4044
- if (currentIndex === 0) return 0;
4045
- currentIndex -= 1;
4046
- const height = this._getItemHeight(currentIndex);
4047
- if (height > 0) remaining += height;
4048
- continue;
4049
- }
4050
- const height = this._getItemHeight(currentIndex);
4051
- if (height > 0) {
4052
- if (remaining <= height) return currentIndex + remaining / height;
4053
- remaining -= height;
4054
- } else if (remaining === 0) return currentIndex;
4055
- if (currentIndex === this.items.length - 1) return this.items.length;
4056
- currentIndex += 1;
4057
- }
4892
+ _restoreAnchor(anchor) {
4893
+ if (!Number.isFinite(anchor) || this.items.length <= 0) return;
4894
+ this._applyAnchor(anchor);
4058
4895
  }
4059
- #cancelJumpAnimation() {
4060
- this.#jumpAnimation = void 0;
4061
- this.#controlledState = void 0;
4896
+ _resolveItem(item, _index, now) {
4897
+ return this.#transitionController.resolveItem(item, now, this.#getTransitionRenderAdapter(), this.#getTransitionLifecycleAdapter());
4062
4898
  }
4063
- #createReplacementLayer(node, fromAlpha, toAlpha, startTime, duration) {
4899
+ #handleDeleteComplete(item) {
4900
+ this.options.list.finalizeDelete(item);
4901
+ }
4902
+ #getTransitionLifecycleAdapter() {
4064
4903
  return {
4065
- key: ++this.#nextReplacementLayerKey,
4066
- node,
4067
- fromAlpha,
4068
- toAlpha,
4069
- startTime,
4070
- duration
4904
+ onDeleteComplete: this.#handleDeleteComplete.bind(this),
4905
+ captureVisualAnchor: this._readAnchorAt.bind(this),
4906
+ restoreVisualAnchor: this._restoreAnchor.bind(this)
4071
4907
  };
4072
4908
  }
4073
- #sampleLayerAlpha(layer, now) {
4074
- return interpolate(layer.fromAlpha, layer.toAlpha, layer.startTime, layer.duration, now);
4075
- }
4076
- #sampleReplacementHeight(animation, now) {
4077
- return interpolate(animation.fromHeight, animation.toHeight, animation.startTime, animation.duration, now);
4078
- }
4079
- #isLayerComplete(layer, now) {
4080
- return getProgress(layer.startTime, layer.duration, now) >= 1 && Math.abs(layer.toAlpha - this.#sampleLayerAlpha(layer, now)) <= ALPHA_EPSILON;
4081
- }
4082
- #readReplacementAnimation(item, now) {
4083
- const animation = this.#replacementAnimations.get(item);
4084
- if (animation == null) return;
4085
- const currentLayer = animation.layers.find((layer) => layer.key === animation.currentLayerKey);
4086
- if (currentLayer == null) {
4087
- this.#replacementAnimations.delete(item);
4088
- this.#activeReplacementItems.delete(item);
4089
- return;
4090
- }
4091
- animation.layers = animation.layers.filter((layer) => layer.key === animation.currentLayerKey || !this.#isLayerComplete(layer, now));
4092
- if (getProgress(animation.startTime, animation.duration, now) >= 1 && this.#isLayerComplete(currentLayer, now) && animation.layers.length === 1) {
4093
- this.#replacementAnimations.delete(item);
4094
- this.#activeReplacementItems.delete(item);
4095
- return;
4096
- }
4097
- return animation;
4909
+ #getVirtualizedRuntime() {
4910
+ return {
4911
+ items: this.items,
4912
+ position: this.position,
4913
+ offset: this.offset,
4914
+ renderItem: this.options.renderItem,
4915
+ measureNode: this.measureRootNode.bind(this),
4916
+ readVisibleRange: this._readVisibleRange.bind(this),
4917
+ resolveVisibleWindow: () => this._resolveVisibleWindow(getNow())
4918
+ };
4098
4919
  }
4099
- #prepareReplacementAnimations(now) {
4100
- let keepAnimating = false;
4101
- for (const item of [...this.#activeReplacementItems]) if (this.#readReplacementAnimation(item, now) != null) keepAnimating = true;
4102
- return keepAnimating;
4920
+ #getTransitionRenderAdapter() {
4921
+ const runtime = this.#getVirtualizedRuntime();
4922
+ return {
4923
+ renderItem: runtime.renderItem,
4924
+ measureNode: runtime.measureNode,
4925
+ drawNode: this.drawRootNode.bind(this),
4926
+ getRootContext: this.getRootContext.bind(this),
4927
+ graphics: this.graphics
4928
+ };
4103
4929
  }
4104
- #drawReplacementLayers(layers, slotHeight, y) {
4105
- if (slotHeight <= 0) return false;
4106
- let result = false;
4107
- const width = this.graphics.canvas.clientWidth;
4108
- for (const layer of layers) {
4109
- const alpha = clamp$3(layer.alpha, 0, 1);
4110
- if (alpha <= ALPHA_EPSILON) continue;
4111
- this.graphics.save();
4112
- try {
4113
- this.graphics.beginPath?.();
4114
- this.graphics.rect?.(0, y, width, slotHeight);
4115
- this.graphics.clip?.();
4116
- if (typeof this.graphics.globalAlpha === "number") this.graphics.globalAlpha *= alpha;
4117
- const layerY = y + this._getAnimatedLayerOffset(slotHeight, layer.nodeHeight);
4118
- if (this.drawRootNode(layer.node, 0, layerY)) result = true;
4119
- } finally {
4120
- this.graphics.restore();
4121
- }
4122
- }
4123
- return result;
4930
+ #getTransitionPlanningAdapter() {
4931
+ return {
4932
+ ...this.#getVirtualizedRuntime(),
4933
+ underflowAlign: this._getLayoutOptions().underflowAlign
4934
+ };
4124
4935
  }
4125
4936
  #handleListStateChange(change) {
4126
- switch (change.type) {
4127
- case "update":
4128
- this.#handleUpdate(change.prevItem, change.nextItem, change.animation?.duration);
4129
- break;
4130
- case "unshift":
4131
- case "push": break;
4132
- case "reset":
4133
- case "set":
4134
- this.#replacementAnimations = /* @__PURE__ */ new WeakMap();
4135
- this.#activeReplacementItems.clear();
4136
- break;
4137
- }
4937
+ const nextChange = this.#jumpController.handleListStateChange(change);
4938
+ this.#transitionController.handleListStateChange(nextChange, this.#getTransitionPlanningAdapter(), this.#getTransitionLifecycleAdapter());
4138
4939
  }
4139
- #handleUpdate(prevItem, nextItem, duration) {
4140
- const normalizedDuration = Math.max(0, typeof duration === "number" && Number.isFinite(duration) ? duration : 0);
4141
- if (normalizedDuration <= 0) {
4142
- this.#replacementAnimations.delete(prevItem);
4143
- this.#activeReplacementItems.delete(prevItem);
4144
- return;
4145
- }
4146
- const now = getNow();
4147
- const nextNode = this.options.renderItem(nextItem);
4148
- const nextHeight = this.measureRootNode(nextNode).height;
4149
- const animation = this.#readReplacementAnimation(prevItem, now);
4150
- if (animation == null) {
4151
- const prevNode = this.options.renderItem(prevItem);
4152
- const outgoing = this.#createReplacementLayer(prevNode, 1, 0, now, normalizedDuration);
4153
- const incoming = this.#createReplacementLayer(nextNode, 0, 1, now, normalizedDuration);
4154
- this.#replacementAnimations.set(nextItem, {
4155
- currentLayerKey: incoming.key,
4156
- layers: [outgoing, incoming],
4157
- fromHeight: this.measureRootNode(prevNode).height,
4158
- toHeight: nextHeight,
4159
- startTime: now,
4160
- duration: normalizedDuration
4161
- });
4162
- this.#activeReplacementItems.delete(prevItem);
4163
- this.#activeReplacementItems.add(nextItem);
4164
- return;
4940
+ };
4941
+ //#endregion
4942
+ //#region src/renderer/virtualized/anchor-model.ts
4943
+ function clampItemIndex(index, itemCount) {
4944
+ if (itemCount <= 0) return 0;
4945
+ return clamp$1(Number.isFinite(index) ? Math.trunc(index) : 0, 0, itemCount - 1);
4946
+ }
4947
+ function readAnchorFromState(itemCount, state, anchorMode, readItemHeight) {
4948
+ if (itemCount <= 0) return 0;
4949
+ const height = readItemHeight(state.position);
4950
+ if (anchorMode === "top") return height > 0 ? state.position - state.offset / height : state.position;
4951
+ return height > 0 ? state.position + 1 - state.offset / height : state.position + 1;
4952
+ }
4953
+ function applyAnchorToState(itemCount, anchor, anchorMode, readItemHeight) {
4954
+ if (itemCount <= 0) return;
4955
+ const clampedAnchor = clamp$1(anchor, 0, itemCount);
4956
+ if (anchorMode === "top") {
4957
+ const position = clamp$1(Math.floor(clampedAnchor), 0, itemCount - 1);
4958
+ const height = readItemHeight(position);
4959
+ const offset = height > 0 ? -(clampedAnchor - position) * height : 0;
4960
+ return {
4961
+ position,
4962
+ offset: Object.is(offset, -0) ? 0 : offset
4963
+ };
4964
+ }
4965
+ const position = clamp$1(Math.ceil(clampedAnchor) - 1, 0, itemCount - 1);
4966
+ const height = readItemHeight(position);
4967
+ const offset = height > 0 ? (position + 1 - clampedAnchor) * height : 0;
4968
+ return {
4969
+ position,
4970
+ offset: Object.is(offset, -0) ? 0 : offset
4971
+ };
4972
+ }
4973
+ function getAnchorAtOffset(itemCount, index, offset, readItemHeight) {
4974
+ if (itemCount <= 0) return 0;
4975
+ let currentIndex = clampItemIndex(index, itemCount);
4976
+ let remaining = Number.isFinite(offset) ? offset : 0;
4977
+ while (true) {
4978
+ if (remaining < 0) {
4979
+ if (currentIndex === 0) return 0;
4980
+ currentIndex -= 1;
4981
+ const height = readItemHeight(currentIndex);
4982
+ if (height > 0) remaining += height;
4983
+ continue;
4165
4984
  }
4166
- const currentLayer = animation.layers.find((layer) => layer.key === animation.currentLayerKey);
4167
- const currentNode = currentLayer?.node ?? this.options.renderItem(prevItem);
4168
- const currentAlpha = currentLayer == null ? 1 : this.#sampleLayerAlpha(currentLayer, now);
4169
- const layers = animation.layers.filter((layer) => layer.key !== animation.currentLayerKey && !this.#isLayerComplete(layer, now));
4170
- if (currentAlpha > ALPHA_EPSILON) layers.push(this.#createReplacementLayer(currentNode, currentAlpha, 0, now, normalizedDuration));
4171
- const incoming = this.#createReplacementLayer(nextNode, 0, 1, now, normalizedDuration);
4172
- layers.push(incoming);
4173
- this.#replacementAnimations.delete(prevItem);
4174
- this.#replacementAnimations.set(nextItem, {
4175
- currentLayerKey: incoming.key,
4176
- layers,
4177
- fromHeight: this.#sampleReplacementHeight(animation, now),
4178
- toHeight: nextHeight,
4179
- startTime: now,
4180
- duration: normalizedDuration
4181
- });
4182
- this.#activeReplacementItems.delete(prevItem);
4183
- this.#activeReplacementItems.add(nextItem);
4985
+ const height = readItemHeight(currentIndex);
4986
+ if (height > 0) {
4987
+ if (remaining <= height) return currentIndex + remaining / height;
4988
+ remaining -= height;
4989
+ } else if (remaining === 0) return currentIndex;
4990
+ if (currentIndex === itemCount - 1) return itemCount;
4991
+ currentIndex += 1;
4992
+ }
4993
+ }
4994
+ function getTargetAnchorForItem(itemCount, index, block, anchorMode, viewportHeight, readItemHeight) {
4995
+ if (itemCount <= 0) return 0;
4996
+ const targetIndex = clampItemIndex(index, itemCount);
4997
+ const height = readItemHeight(targetIndex);
4998
+ if (anchorMode === "top") switch (block) {
4999
+ case "start": return getAnchorAtOffset(itemCount, targetIndex, 0, readItemHeight);
5000
+ case "center": return getAnchorAtOffset(itemCount, targetIndex, height / 2 - viewportHeight / 2, readItemHeight);
5001
+ case "end": return getAnchorAtOffset(itemCount, targetIndex, height - viewportHeight, readItemHeight);
5002
+ }
5003
+ switch (block) {
5004
+ case "start": return getAnchorAtOffset(itemCount, targetIndex, viewportHeight, readItemHeight);
5005
+ case "center": return getAnchorAtOffset(itemCount, targetIndex, height / 2 + viewportHeight / 2, readItemHeight);
5006
+ case "end": return getAnchorAtOffset(itemCount, targetIndex, height, readItemHeight);
4184
5007
  }
4185
- };
5008
+ }
4186
5009
  //#endregion
4187
5010
  //#region src/renderer/virtualized/solver.ts
4188
- function clamp$2(value, min, max) {
5011
+ function clamp(value, min, max) {
4189
5012
  return Math.min(Math.max(value, min), max);
4190
5013
  }
4191
5014
  function normalizeOffset(offset) {
4192
5015
  return Number.isFinite(offset) ? offset : 0;
4193
5016
  }
4194
- function normalizeTimelineState(itemCount, state) {
4195
- if (itemCount <= 0) return {
4196
- position: 0,
4197
- offset: 0
4198
- };
4199
- const position = state.position;
4200
- if (typeof position !== "number" || !Number.isFinite(position)) return {
4201
- position: 0,
4202
- offset: normalizeOffset(state.offset)
4203
- };
5017
+ function resolveListLayoutOptions(options = {}) {
4204
5018
  return {
4205
- position: clamp$2(Math.trunc(position), 0, itemCount - 1),
4206
- offset: normalizeOffset(state.offset)
5019
+ anchorMode: options.anchorMode ?? "top",
5020
+ underflowAlign: options.underflowAlign ?? "top"
4207
5021
  };
4208
5022
  }
4209
- function normalizeChatState(itemCount, state) {
5023
+ function normalizeVisibleState(itemCount, state, layout) {
4210
5024
  if (itemCount <= 0) return {
4211
5025
  position: 0,
4212
5026
  offset: 0
4213
5027
  };
4214
5028
  const position = state.position;
5029
+ const fallbackPosition = layout.anchorMode === "top" ? 0 : itemCount - 1;
4215
5030
  if (typeof position !== "number" || !Number.isFinite(position)) return {
4216
- position: itemCount - 1,
5031
+ position: fallbackPosition,
4217
5032
  offset: normalizeOffset(state.offset)
4218
5033
  };
4219
5034
  return {
4220
- position: clamp$2(Math.trunc(position), 0, itemCount - 1),
5035
+ position: clamp(Math.trunc(position), 0, itemCount - 1),
4221
5036
  offset: normalizeOffset(state.offset)
4222
5037
  };
4223
5038
  }
4224
- function resolveTimelineVisibleWindow(items, state, viewportHeight, resolveItem) {
4225
- const normalizedState = normalizeTimelineState(items.length, state);
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
+ };
4226
5046
  if (items.length === 0) return {
4227
5047
  normalizedState,
5048
+ resolutionPath: [],
4228
5049
  window: {
4229
5050
  drawList: [],
4230
5051
  shift: 0
4231
5052
  }
4232
5053
  };
4233
- let { position, offset } = normalizedState;
4234
- let drawLength = 0;
4235
- if (offset > 0) if (position === 0) offset = 0;
4236
- else {
4237
- for (let i = position - 1; i >= 0; i -= 1) {
4238
- const { height } = resolveItem(items[i], i);
4239
- position = i;
4240
- offset -= height;
4241
- if (offset <= 0) break;
4242
- }
4243
- if (position === 0 && offset > 0) offset = 0;
4244
- }
4245
- let y = offset;
4246
- const drawList = [];
4247
- for (let i = position; i < items.length; i += 1) {
4248
- const { value, height } = resolveItem(items[i], i);
4249
- if (y + height > 0) {
4250
- drawList.push({
4251
- idx: i,
4252
- value,
4253
- offset: y,
4254
- height
4255
- });
4256
- drawLength += height;
4257
- } else {
4258
- offset += height;
4259
- position = i + 1;
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;
4260
5066
  }
4261
- y += height;
4262
- if (y >= viewportHeight) break;
4263
- }
4264
- let shift = 0;
4265
- if (y < viewportHeight) if (position === 0 && drawLength < viewportHeight) {
4266
- shift = -offset;
4267
- offset = 0;
4268
- } else {
4269
- shift = viewportHeight - y;
4270
- y = offset += shift;
4271
- let lastIdx = -1;
4272
- for (let i = position - 1; i >= 0; i -= 1) {
4273
- const { value, height } = resolveItem(items[i], i);
4274
- drawLength += height;
4275
- y -= height;
4276
- drawList.push({
4277
- idx: i,
4278
- value,
4279
- offset: y - shift,
4280
- height
4281
- });
4282
- lastIdx = i;
4283
- if (y < 0) break;
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;
4284
5085
  }
4285
- if (lastIdx === 0 && drawLength < viewportHeight) {
4286
- shift = drawList.at(-1)?.offset == null ? 0 : -drawList.at(-1).offset;
4287
- position = 0;
5086
+ let shift = 0;
5087
+ if (y < viewportHeight) if (position === 0 && drawLength < viewportHeight) {
5088
+ shift = -offset;
4288
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
+ }
4289
5112
  }
4290
- }
4291
- return {
4292
- normalizedState: {
5113
+ return finalizeVisibleWindowResult(items.length, viewportHeight, layout, {
4293
5114
  position,
4294
5115
  offset
4295
- },
4296
- window: {
5116
+ }, Array.from(resolutionPath), {
4297
5117
  drawList,
4298
5118
  shift
4299
- }
4300
- };
4301
- }
4302
- function resolveChatVisibleWindow(items, state, viewportHeight, resolveItem) {
4303
- const normalizedState = normalizeChatState(items.length, state);
4304
- if (items.length === 0) return {
4305
- normalizedState,
4306
- window: {
4307
- drawList: [],
4308
- shift: 0
4309
- }
4310
- };
5119
+ });
5120
+ }
4311
5121
  let { position, offset } = normalizedState;
4312
5122
  let drawLength = 0;
4313
5123
  if (offset < 0) if (position === items.length - 1) offset = 0;
4314
5124
  else for (let i = position + 1; i < items.length; i += 1) {
4315
- const { height } = resolveItem(items[i], i);
5125
+ const { height } = readResolvedItem(items[i], i);
4316
5126
  position = i;
4317
5127
  offset += height;
4318
5128
  if (offset > 0) break;
@@ -4320,7 +5130,7 @@ function resolveChatVisibleWindow(items, state, viewportHeight, resolveItem) {
4320
5130
  let y = viewportHeight + offset;
4321
5131
  const drawList = [];
4322
5132
  for (let i = position; i >= 0; i -= 1) {
4323
- const { value, height } = resolveItem(items[i], i);
5133
+ const { value, height } = readResolvedItem(items[i], i);
4324
5134
  y -= height;
4325
5135
  if (y <= viewportHeight) {
4326
5136
  drawList.push({
@@ -4342,7 +5152,7 @@ function resolveChatVisibleWindow(items, state, viewportHeight, resolveItem) {
4342
5152
  if (drawLength < viewportHeight) {
4343
5153
  y = drawLength;
4344
5154
  for (let i = position + 1; i < items.length; i += 1) {
4345
- const { value, height } = resolveItem(items[i], i);
5155
+ const { value, height } = readResolvedItem(items[i], i);
4346
5156
  drawList.push({
4347
5157
  idx: i,
4348
5158
  value,
@@ -4356,142 +5166,88 @@ function resolveChatVisibleWindow(items, state, viewportHeight, resolveItem) {
4356
5166
  offset = drawLength < viewportHeight ? 0 : drawLength - viewportHeight;
4357
5167
  } else offset = drawLength - viewportHeight;
4358
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;
4359
5200
  return {
4360
- normalizedState: {
4361
- position,
4362
- offset
5201
+ normalizedState: layout.anchorMode === "top" ? {
5202
+ position: 0,
5203
+ offset: 0
5204
+ } : {
5205
+ position: itemCount - 1,
5206
+ offset: 0
4363
5207
  },
5208
+ resolutionPath,
4364
5209
  window: {
4365
- drawList,
4366
- shift
5210
+ drawList: window.drawList,
5211
+ shift: desiredTop - minOffset
4367
5212
  }
4368
5213
  };
4369
5214
  }
4370
5215
  //#endregion
4371
- //#region src/renderer/virtualized/chat.ts
4372
- function clamp$1(value, min, max) {
4373
- return Math.min(Math.max(value, min), max);
4374
- }
5216
+ //#region src/renderer/virtualized/list.ts
4375
5217
  /**
4376
- * Virtualized renderer anchored to the bottom, suitable for chat-style UIs.
5218
+ * Virtualized list renderer with configurable anchor semantics.
4377
5219
  */
4378
- var ChatRenderer = class extends VirtualizedRenderer {
4379
- #resolveVisibleWindow() {
4380
- const now = globalThis.performance?.now() ?? Date.now();
4381
- return resolveChatVisibleWindow(this.items, this._readListState(), this.graphics.canvas.clientHeight, (item, idx) => {
4382
- return this._resolveItem(item, idx, now);
4383
- });
4384
- }
4385
- _getDefaultJumpBlock() {
4386
- return "end";
4387
- }
4388
- _normalizeListState(state) {
4389
- return normalizeChatState(this.items.length, state);
4390
- }
4391
- _readAnchor(state) {
4392
- if (this.items.length === 0) return 0;
4393
- const height = this._getItemHeight(state.position);
4394
- return height > 0 ? state.position + 1 - state.offset / height : state.position + 1;
4395
- }
4396
- _applyAnchor(anchor) {
4397
- if (this.items.length === 0) return;
4398
- const clampedAnchor = clamp$1(anchor, 0, this.items.length);
4399
- const position = clamp$1(Math.ceil(clampedAnchor) - 1, 0, this.items.length - 1);
4400
- const height = this._getItemHeight(position);
4401
- const offset = height > 0 ? (position + 1 - clampedAnchor) * height : 0;
4402
- this._commitListState({
4403
- position,
4404
- offset: Object.is(offset, -0) ? 0 : offset
4405
- });
4406
- }
4407
- _getTargetAnchor(index, block) {
4408
- const height = this._getItemHeight(index);
4409
- const viewportHeight = this.graphics.canvas.clientHeight;
4410
- switch (block) {
4411
- case "start": return this._getAnchorAtOffset(index, viewportHeight);
4412
- case "center": return this._getAnchorAtOffset(index, height / 2 + viewportHeight / 2);
4413
- case "end": return this._getAnchorAtOffset(index, height);
4414
- }
4415
- }
4416
- _getAnimatedLayerOffset(slotHeight, nodeHeight) {
4417
- return slotHeight - nodeHeight;
5220
+ var ListRenderer = class extends VirtualizedRenderer {
5221
+ #layout;
5222
+ constructor(graphics, options) {
5223
+ super(graphics, options);
5224
+ this.#layout = resolveListLayoutOptions(options);
4418
5225
  }
4419
- render(feedback) {
4420
- const keepAnimating = this._prepareRender();
4421
- const { clientWidth: viewportWidth, clientHeight: viewportHeight } = this.graphics.canvas;
4422
- this.graphics.clearRect(0, 0, viewportWidth, viewportHeight);
4423
- const solution = this.#resolveVisibleWindow();
4424
- const requestRedraw = this._renderVisibleWindow(solution.window, feedback);
4425
- this._commitListState(solution.normalizedState);
4426
- return this._finishRender(keepAnimating || requestRedraw);
5226
+ _getLayoutOptions() {
5227
+ return this.#layout;
4427
5228
  }
4428
- hittest(test) {
4429
- return this._hittestVisibleWindow(this.#resolveVisibleWindow().window, test);
4430
- }
4431
- };
4432
- //#endregion
4433
- //#region src/renderer/virtualized/timeline.ts
4434
- function clamp(value, min, max) {
4435
- return Math.min(Math.max(value, min), max);
4436
- }
4437
- /**
4438
- * Virtualized renderer anchored to the top, suitable for timeline-style UIs.
4439
- */
4440
- var TimelineRenderer = class extends VirtualizedRenderer {
4441
- #resolveVisibleWindow() {
4442
- const now = globalThis.performance?.now() ?? Date.now();
4443
- return resolveTimelineVisibleWindow(this.items, this._readListState(), this.graphics.canvas.clientHeight, (item, idx) => {
4444
- return this._resolveItem(item, idx, now);
4445
- });
5229
+ _resolveVisibleWindowForState(state, now) {
5230
+ return resolveVisibleWindow(this.items, state, this.graphics.canvas.clientHeight, (item, idx) => this._resolveItem(item, idx, now), this.#layout);
4446
5231
  }
4447
5232
  _getDefaultJumpBlock() {
4448
- return "start";
5233
+ return this.#layout.anchorMode === "top" ? "start" : "end";
4449
5234
  }
4450
5235
  _normalizeListState(state) {
4451
- return normalizeTimelineState(this.items.length, state);
5236
+ return normalizeVisibleState(this.items.length, state, this.#layout);
4452
5237
  }
4453
- _readAnchor(state) {
4454
- if (this.items.length === 0) return 0;
4455
- const height = this._getItemHeight(state.position);
4456
- return height > 0 ? state.position - state.offset / height : state.position;
5238
+ _readAnchor(state, readItemHeight) {
5239
+ return readAnchorFromState(this.items.length, state, this.#layout.anchorMode, readItemHeight);
4457
5240
  }
4458
5241
  _applyAnchor(anchor) {
4459
- if (this.items.length === 0) return;
4460
- const clampedAnchor = clamp(anchor, 0, this.items.length);
4461
- const position = clamp(Math.floor(clampedAnchor), 0, this.items.length - 1);
4462
- const height = this._getItemHeight(position);
4463
- const offset = height > 0 ? -(clampedAnchor - position) * height : 0;
4464
- this._commitListState({
4465
- position,
4466
- offset: Object.is(offset, -0) ? 0 : offset
4467
- });
5242
+ const state = applyAnchorToState(this.items.length, anchor, this.#layout.anchorMode, this._getItemHeight.bind(this));
5243
+ if (state == null) return;
5244
+ this._commitListState(state);
4468
5245
  }
4469
5246
  _getTargetAnchor(index, block) {
4470
- const height = this._getItemHeight(index);
4471
- const viewportHeight = this.graphics.canvas.clientHeight;
4472
- switch (block) {
4473
- case "start": return this._getAnchorAtOffset(index, 0);
4474
- case "center": return this._getAnchorAtOffset(index, height / 2 - viewportHeight / 2);
4475
- case "end": return this._getAnchorAtOffset(index, height - viewportHeight);
4476
- }
4477
- }
4478
- _getAnimatedLayerOffset(_slotHeight, _nodeHeight) {
4479
- return 0;
4480
- }
4481
- render(feedback) {
4482
- const keepAnimating = this._prepareRender();
4483
- const { clientWidth: viewportWidth, clientHeight: viewportHeight } = this.graphics.canvas;
4484
- this.graphics.clearRect(0, 0, viewportWidth, viewportHeight);
4485
- const solution = this.#resolveVisibleWindow();
4486
- const requestRedraw = this._renderVisibleWindow(solution.window, feedback);
4487
- this._commitListState(solution.normalizedState);
4488
- return this._finishRender(keepAnimating || requestRedraw);
4489
- }
4490
- hittest(test) {
4491
- return this._hittestVisibleWindow(this.#resolveVisibleWindow().window, test);
5247
+ return getTargetAnchorForItem(this.items.length, index, block, this.#layout.anchorMode, this.graphics.canvas.clientHeight, this._getItemHeight.bind(this));
4492
5248
  }
4493
5249
  };
4494
5250
  //#endregion
4495
- export { BaseRenderer, ChatRenderer, DebugRenderer, Fixed, Flex, FlexItem, Group, ListState, MultilineText, PaddingBox, Place, ShrinkWrap, Text, TimelineRenderer, VirtualizedRenderer, Wrapper, memoRenderItem, memoRenderItemBy };
5251
+ export { BaseRenderer, DebugRenderer, Fixed, Flex, FlexItem, Group, ListRenderer, ListState, MultilineText, PaddingBox, Place, ShrinkWrap, Text, VirtualizedRenderer, Wrapper, memoRenderItem, memoRenderItemBy };
4496
5252
 
4497
5253
  //# sourceMappingURL=index.mjs.map