chat-layout 1.2.0-4 → 1.2.0-6

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
@@ -3649,6 +3649,21 @@ function normalizeUpdateAnimation(animation) {
3649
3649
  function normalizeDeleteAnimation(animation) {
3650
3650
  return normalizeAnimationDuration(animation?.duration);
3651
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?.autoFollow === true) normalizedAnimation.autoFollow = true;
3665
+ return normalizedAnimation;
3666
+ }
3652
3667
  var ListState = class {
3653
3668
  #items;
3654
3669
  #pendingDeletes = /* @__PURE__ */ new Set();
@@ -3681,14 +3696,16 @@ var ListState = class {
3681
3696
  this.unshiftAll(items);
3682
3697
  }
3683
3698
  /** Prepends an array of items. */
3684
- unshiftAll(items) {
3699
+ unshiftAll(items, animation) {
3685
3700
  if (items.length === 0) return;
3686
3701
  assertUniqueItemReferences(items, this.#items);
3702
+ const normalizedAnimation = normalizeInsertAnimation(animation);
3687
3703
  if (this.position != null) this.position += items.length;
3688
3704
  this.#items = items.concat(this.#items);
3689
3705
  emitListStateChange(this, {
3690
3706
  type: "unshift",
3691
- count: items.length
3707
+ count: items.length,
3708
+ animation: normalizedAnimation
3692
3709
  });
3693
3710
  }
3694
3711
  /** Appends one or more items. */
@@ -3696,13 +3713,15 @@ var ListState = class {
3696
3713
  this.pushAll(items);
3697
3714
  }
3698
3715
  /** Appends an array of items. */
3699
- pushAll(items) {
3716
+ pushAll(items, animation) {
3700
3717
  if (items.length === 0) return;
3701
3718
  assertUniqueItemReferences(items, this.#items);
3719
+ const normalizedAnimation = normalizeInsertAnimation(animation);
3702
3720
  this.#items.push(...items);
3703
3721
  emitListStateChange(this, {
3704
3722
  type: "push",
3705
- count: items.length
3723
+ count: items.length,
3724
+ animation: normalizedAnimation
3706
3725
  });
3707
3726
  }
3708
3727
  /**
@@ -3856,288 +3875,974 @@ function memoRenderItemBy(keyOf, renderItem, options = {}) {
3856
3875
  }
3857
3876
  //#endregion
3858
3877
  //#region src/renderer/virtualized/base-animation.ts
3859
- function clamp$3(value, min, max) {
3878
+ function clamp$1(value, min, max) {
3860
3879
  return Math.min(Math.max(value, min), max);
3861
3880
  }
3862
3881
  function sameState(state, position, offset) {
3863
3882
  return Object.is(state.position, position) && Object.is(state.offset, offset);
3864
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
+ }
3865
3934
  function smoothstep(value) {
3866
3935
  return value * value * (3 - 2 * value);
3867
3936
  }
3868
3937
  function getProgress(startTime, duration, now) {
3869
3938
  if (!(duration > 0)) return 1;
3870
- return clamp$3((now - startTime) / duration, 0, 1);
3939
+ return clamp$1((now - startTime) / duration, 0, 1);
3871
3940
  }
3872
3941
  function interpolate(from, to, startTime, duration, now) {
3873
3942
  const progress = getProgress(startTime, duration, now);
3874
3943
  const eased = progress >= 1 ? 1 : smoothstep(progress);
3875
3944
  return from + (to - from) * eased;
3876
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
+ }
3877
3960
  function getNow() {
3878
3961
  return globalThis.performance?.now() ?? Date.now();
3879
3962
  }
3880
3963
  //#endregion
3881
- //#region src/renderer/virtualized/base-types.ts
3882
- /** Alpha values below this threshold are treated as fully transparent. */
3883
- const ALPHA_EPSILON = .001;
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
+ }
3884
3981
  //#endregion
3885
- //#region src/renderer/virtualized/base-replacement.ts
3886
- /**
3887
- * Self-contained subsystem that manages item replacement (cross-fade + height)
3888
- * animations for a VirtualizedRenderer.
3889
- */
3890
- var ReplacementController = class {
3891
- #replacementAnimations = /* @__PURE__ */ new WeakMap();
3892
- #activeReplacementItems = /* @__PURE__ */ new Set();
3893
- #visibleItems = /* @__PURE__ */ new Set();
3894
- #hasVisibleItemSnapshot = false;
3895
- #visibleSnapshotState;
3896
- captureVisibleItemSnapshot(window, items, readVisibleRange, readListState) {
3897
- const nextVisibleItems = /* @__PURE__ */ new Set();
3898
- for (const { idx, offset, height } of window.drawList) {
3899
- if (readVisibleRange(offset + window.shift, height) == null) continue;
3900
- const item = items[idx];
3901
- if (item != null) nextVisibleItems.add(item);
3982
+ //#region src/renderer/virtualized/jump-controller.ts
3983
+ var JumpController = class {
3984
+ #confirmedAutoFollowTop = false;
3985
+ #confirmedAutoFollowBottom = false;
3986
+ #controlledState;
3987
+ #jumpAnimation;
3988
+ #lastCommittedState;
3989
+ #hasPendingListChange = false;
3990
+ #pendingBoundaryJumpTop = false;
3991
+ #pendingBoundaryJumpBottom = false;
3992
+ #options;
3993
+ constructor(options) {
3994
+ this.#options = options;
3995
+ }
3996
+ beforeFrame() {
3997
+ const currentState = this.#options.readListState();
3998
+ if (!this.#hasPendingListChange && this.#jumpAnimation == null && this.#lastCommittedState != null && !sameState(this.#lastCommittedState, currentState.position, currentState.offset)) this.#clearPendingBoundaryJumps();
3999
+ this.#hasPendingListChange = false;
4000
+ }
4001
+ prepare(now) {
4002
+ const animation = this.#jumpAnimation;
4003
+ if (animation == null) return false;
4004
+ if (this.#options.getItemCount() === 0) {
4005
+ this.#cancelJumpAnimation();
4006
+ return false;
3902
4007
  }
3903
- this.#visibleItems = nextVisibleItems;
3904
- this.#hasVisibleItemSnapshot = true;
3905
- this.#visibleSnapshotState = readListState();
4008
+ if (this.#controlledState != null && !sameState(this.#controlledState, this.#options.readListState().position, this.#options.readListState().offset)) {
4009
+ this.#clearPendingBoundaryJumps();
4010
+ this.#cancelJumpAnimation();
4011
+ return false;
4012
+ }
4013
+ const progress = getProgress(animation.startTime, animation.duration, now);
4014
+ const eased = progress >= 1 ? 1 : smoothstep(progress);
4015
+ const anchor = getAnchorAtDistance(animation.path, animation.path.totalDistance * eased);
4016
+ this.#options.applyAnchor(anchor);
4017
+ animation.needsMoreFrames = progress < 1;
4018
+ return animation.needsMoreFrames;
3906
4019
  }
3907
- /**
3908
- * Removes animations for items that are no longer visible.
3909
- * Returns true if any animation was canceled or finalized.
3910
- */
3911
- pruneInvisible(adapter) {
3912
- let changed = false;
3913
- for (const item of [...this.#activeReplacementItems]) {
3914
- if (this.#visibleItems.has(item)) continue;
3915
- const animation = this.#replacementAnimations.get(item);
3916
- this.#replacementAnimations.delete(item);
3917
- this.#activeReplacementItems.delete(item);
3918
- if (animation?.kind === "delete") adapter.onDeleteComplete(item);
3919
- changed = true;
4020
+ finishFrame(requestRedraw) {
4021
+ const animation = this.#jumpAnimation;
4022
+ if (animation == null) return requestRedraw;
4023
+ if (animation.needsMoreFrames) {
4024
+ this.#controlledState = this.#options.readListState();
4025
+ return true;
3920
4026
  }
3921
- return changed;
4027
+ const onComplete = animation.onComplete;
4028
+ this.#cancelJumpAnimation();
4029
+ onComplete?.();
4030
+ return requestRedraw || this.#jumpAnimation != null;
3922
4031
  }
3923
- /** Advance all active animations and return true if any are still running. */
3924
- prepare(now, adapter) {
3925
- let keepAnimating = false;
3926
- for (const item of [...this.#activeReplacementItems]) if (this.readAnimation(item, now, adapter) != null) keepAnimating = true;
3927
- return keepAnimating;
4032
+ commit(state) {
4033
+ this.#lastCommittedState = {
4034
+ position: state.position,
4035
+ offset: state.offset
4036
+ };
3928
4037
  }
3929
- /**
3930
- * Returns the active animation for an item, or undefined if none / already
3931
- * completed (and cleans up completed animations as a side effect).
3932
- */
3933
- readAnimation(item, now, adapter) {
3934
- const animation = this.#replacementAnimations.get(item);
3935
- if (animation == null) return;
3936
- if (getProgress(animation.startTime, animation.duration, now) >= 1) {
3937
- this.#replacementAnimations.delete(item);
3938
- this.#activeReplacementItems.delete(item);
3939
- if (animation.kind === "delete") adapter?.onDeleteComplete(item);
4038
+ jumpTo(index, options = {}) {
4039
+ this.#clearPendingBoundaryJumps();
4040
+ if (this.#options.getItemCount() === 0) {
4041
+ this.#cancelJumpAnimation();
3940
4042
  return;
3941
4043
  }
3942
- return animation;
3943
- }
3944
- /** Returns the effective rendered height for an item, accounting for animations. */
3945
- getItemHeight(item, now, adapter) {
3946
- const replacement = this.readAnimation(item, now);
3947
- if (replacement != null) return this.#sampleReplacementHeight(replacement, now);
3948
- const node = adapter.renderItem(item);
3949
- return adapter.measureNode(node).height;
4044
+ this.#startJumpToIndex(index, options, { kind: "manual" });
3950
4045
  }
3951
- /** Resolves an item to its draw/hittest callbacks for the current frame. */
3952
- resolveItem(item, now, adapter) {
3953
- const replacement = this.readAnimation(item, now, adapter);
3954
- if (replacement == null) {
3955
- const node = adapter.renderItem(item);
3956
- return {
3957
- value: {
3958
- draw: (y) => adapter.drawNode(node, 0, y),
3959
- hittest: (test, y) => node.hittest(adapter.getRootContext(), {
3960
- ...test,
3961
- y: test.y - y
3962
- })
3963
- },
3964
- height: adapter.measureNode(node).height
3965
- };
4046
+ jumpToBoundary(boundary, options = {}) {
4047
+ this.#clearPendingBoundaryJumps();
4048
+ if (this.#options.getItemCount() === 0) {
4049
+ this.#cancelJumpAnimation();
4050
+ return;
3966
4051
  }
3967
- const slotHeight = this.#sampleReplacementHeight(replacement, now);
3968
- const layers = this.#readReplacementLayers(replacement, now, adapter.measureNode);
4052
+ this.#armBoundaryJump(boundary);
4053
+ this.#startJumpToIndex(boundary === "bottom" ? this.#options.getItemCount() - 1 : 0, {
4054
+ ...options,
4055
+ block: boundary === "bottom" ? "end" : "start"
4056
+ }, {
4057
+ kind: "boundary-jump",
4058
+ boundary
4059
+ });
4060
+ }
4061
+ syncAutoFollowCapabilities(capabilities) {
4062
+ this.#confirmedAutoFollowTop = capabilities.top;
4063
+ this.#confirmedAutoFollowBottom = capabilities.bottom;
4064
+ this.#clearPendingBoundaryJumps();
4065
+ return this.getEffectiveAutoFollowCapabilities();
4066
+ }
4067
+ getEffectiveAutoFollowCapabilities() {
3969
4068
  return {
3970
- value: {
3971
- draw: (y) => this.#drawReplacementLayers(layers, slotHeight, y, adapter),
3972
- hittest: () => false
3973
- },
3974
- height: slotHeight
4069
+ top: this.#hasEffectiveAutoFollowCapability("top"),
4070
+ bottom: this.#hasEffectiveAutoFollowCapability("bottom")
3975
4071
  };
3976
4072
  }
3977
- handleListStateChange(change, ctx) {
3978
- switch (change.type) {
3979
- case "update":
3980
- this.handleUpdate(change.prevItem, change.nextItem, change.animation?.duration, ctx);
3981
- break;
3982
- case "delete":
3983
- this.handleDelete(change.item, change.animation?.duration, ctx);
3984
- break;
3985
- case "delete-finalize":
3986
- this.#replacementAnimations.delete(change.item);
3987
- this.#activeReplacementItems.delete(change.item);
3988
- break;
3989
- case "unshift":
3990
- case "push": break;
3991
- case "reset":
3992
- case "set":
3993
- this.reset();
3994
- break;
4073
+ handleListStateChange(change) {
4074
+ this.#hasPendingListChange = true;
4075
+ const followChange = this.#resolveAutoFollowChange(change);
4076
+ const canChainAutoFollow = followChange != null ? this.#shouldChainAutoFollow(followChange.boundary) : false;
4077
+ const canCapabilityAutoFollow = followChange != null ? this.#shouldAutoFollowFromCapability(followChange.boundary, followChange.direction, followChange.count) : false;
4078
+ if (followChange != null && (canChainAutoFollow || canCapabilityAutoFollow)) {
4079
+ if (canChainAutoFollow) this.#rebaseJumpAnchorForBoundaryInsert(followChange.direction, followChange.count, getNow());
4080
+ this.#startJumpToIndex(followChange.boundary === "bottom" ? this.#options.getItemCount() - 1 : 0, {
4081
+ block: followChange.boundary === "bottom" ? "end" : "start",
4082
+ duration: followChange.animation?.duration
4083
+ }, {
4084
+ kind: "auto-follow",
4085
+ boundary: followChange.boundary
4086
+ });
4087
+ return {
4088
+ ...followChange.change,
4089
+ animation: void 0
4090
+ };
3995
4091
  }
4092
+ return change;
3996
4093
  }
3997
- handleUpdate(prevItem, nextItem, duration, ctx) {
3998
- const normalizedDuration = Math.max(0, typeof duration === "number" && Number.isFinite(duration) ? duration : 0);
3999
- const nextIndex = ctx.items.indexOf(nextItem);
4000
- if (normalizedDuration <= 0 || nextIndex < 0 || !this.#canAnimateUpdate(nextIndex, prevItem, ctx)) {
4001
- this.#replacementAnimations.delete(prevItem);
4002
- this.#activeReplacementItems.delete(prevItem);
4094
+ #cancelJumpAnimation() {
4095
+ this.#jumpAnimation = void 0;
4096
+ this.#controlledState = void 0;
4097
+ }
4098
+ #startJumpToIndex(index, options, source) {
4099
+ const targetIndex = this.#options.clampItemIndex(index);
4100
+ const currentState = this.#options.normalizeListState(this.#options.readListState());
4101
+ const targetBlock = options.block ?? this.#options.getDefaultJumpBlock();
4102
+ const targetAnchor = this.#options.getTargetAnchor(targetIndex, targetBlock);
4103
+ if (!(options.animated ?? true)) {
4104
+ this.#cancelJumpAnimation();
4105
+ this.#options.applyAnchor(targetAnchor);
4106
+ options.onComplete?.();
4003
4107
  return;
4004
4108
  }
4005
- const now = getNow();
4006
- const nextNode = ctx.renderItem(nextItem);
4007
- const nextHeight = ctx.measureNode(nextNode).height;
4008
- const animation = this.readAnimation(prevItem, now, ctx);
4009
- let currentNode;
4010
- let currentAlpha = 1;
4011
- let fromHeight;
4012
- if (animation == null || animation.incoming == null) {
4013
- currentNode = ctx.renderItem(prevItem);
4014
- fromHeight = ctx.measureNode(currentNode).height;
4015
- } else {
4016
- currentNode = animation.incoming.node;
4017
- currentAlpha = this.#sampleLayerAlpha(animation.incoming, now);
4018
- fromHeight = this.#sampleReplacementHeight(animation, now);
4109
+ const startAnchor = this.#options.readAnchor(currentState);
4110
+ if (!Number.isFinite(startAnchor)) {
4111
+ this.#cancelJumpAnimation();
4112
+ this.#options.applyAnchor(targetAnchor);
4113
+ options.onComplete?.();
4114
+ return;
4019
4115
  }
4020
- const outgoing = currentAlpha > .001 ? this.#createLayer(currentNode, currentAlpha, 0, now, normalizedDuration) : void 0;
4021
- const incoming = this.#createLayer(nextNode, 0, 1, now, normalizedDuration);
4022
- this.#replacementAnimations.delete(prevItem);
4023
- this.#replacementAnimations.set(nextItem, {
4024
- kind: "update",
4025
- outgoing,
4026
- incoming,
4027
- fromHeight,
4028
- toHeight: nextHeight,
4029
- startTime: now,
4030
- duration: normalizedDuration
4031
- });
4032
- this.#activeReplacementItems.delete(prevItem);
4033
- this.#activeReplacementItems.add(nextItem);
4034
- }
4035
- handleDelete(item, duration, ctx) {
4036
- const normalizedDuration = Math.max(0, typeof duration === "number" && Number.isFinite(duration) ? duration : 0);
4037
- const index = ctx.items.indexOf(item);
4038
- if (normalizedDuration <= 0 || index < 0 || !this.#canAnimateUpdate(index, item, ctx)) {
4039
- this.#replacementAnimations.delete(item);
4040
- this.#activeReplacementItems.delete(item);
4041
- ctx.onDeleteComplete(item);
4116
+ const path = buildJumpPath(this.#options.getItemCount(), this.#options.getItemHeight, startAnchor, targetAnchor);
4117
+ const duration = clamp$1(options.duration ?? this.#options.minJumpDuration + path.totalDistance * this.#options.jumpDurationPerPixel, 0, this.#options.maxJumpDuration);
4118
+ if (duration <= 0 || path.totalDistance <= Number.EPSILON) {
4119
+ this.#cancelJumpAnimation();
4120
+ this.#options.applyAnchor(targetAnchor);
4121
+ options.onComplete?.();
4042
4122
  return;
4043
4123
  }
4044
- const now = getNow();
4045
- const animation = this.readAnimation(item, now, ctx);
4046
- let currentNode;
4047
- let currentAlpha = 1;
4048
- let fromHeight;
4049
- if (animation == null) {
4050
- currentNode = ctx.renderItem(item);
4051
- fromHeight = ctx.measureNode(currentNode).height;
4052
- } else if (animation.incoming != null) {
4053
- currentNode = animation.incoming.node;
4054
- currentAlpha = this.#sampleLayerAlpha(animation.incoming, now);
4055
- fromHeight = this.#sampleReplacementHeight(animation, now);
4056
- } else if (animation.outgoing != null) {
4057
- currentNode = animation.outgoing.node;
4058
- currentAlpha = this.#sampleLayerAlpha(animation.outgoing, now);
4059
- fromHeight = this.#sampleReplacementHeight(animation, now);
4060
- } else {
4061
- currentNode = ctx.renderItem(item);
4062
- fromHeight = ctx.measureNode(currentNode).height;
4124
+ this.#jumpAnimation = {
4125
+ path,
4126
+ startTime: getNow(),
4127
+ duration,
4128
+ needsMoreFrames: true,
4129
+ onComplete: options.onComplete,
4130
+ source
4131
+ };
4132
+ this.#controlledState = this.#options.readListState();
4133
+ }
4134
+ #resolveAutoFollowChange(change) {
4135
+ switch (change.type) {
4136
+ case "push":
4137
+ case "unshift": return change.animation?.autoFollow === true ? {
4138
+ change,
4139
+ boundary: change.type === "push" ? "bottom" : "top",
4140
+ direction: change.type,
4141
+ count: change.count,
4142
+ animation: change.animation
4143
+ } : void 0;
4144
+ default: return;
4063
4145
  }
4064
- const outgoing = currentAlpha > .001 ? this.#createLayer(currentNode, currentAlpha, 0, now, normalizedDuration) : void 0;
4065
- this.#replacementAnimations.set(item, {
4066
- kind: "delete",
4067
- outgoing,
4068
- incoming: void 0,
4069
- fromHeight,
4070
- toHeight: 0,
4071
- startTime: now,
4072
- duration: normalizedDuration
4073
- });
4074
- this.#activeReplacementItems.add(item);
4075
4146
  }
4076
- /** Clears all animation state (e.g., on list reset). */
4147
+ #shouldAutoFollowFromCapability(boundary, direction, count) {
4148
+ return this.#hasEffectiveAutoFollowCapability(boundary) && this.#matchesLastCommittedStateAfterBoundaryInsert(direction, count);
4149
+ }
4150
+ #shouldChainAutoFollow(boundary) {
4151
+ return this.#readJumpBoundary() === boundary;
4152
+ }
4153
+ #rebaseJumpAnchorForBoundaryInsert(direction, count, now) {
4154
+ const animation = this.#jumpAnimation;
4155
+ if (animation == null) return;
4156
+ const progress = getProgress(animation.startTime, animation.duration, now);
4157
+ const eased = progress >= 1 ? 1 : smoothstep(progress);
4158
+ const anchorAtNow = getAnchorAtDistance(animation.path, animation.path.totalDistance * eased);
4159
+ this.#cancelJumpAnimation();
4160
+ this.#options.applyAnchor(direction === "unshift" ? anchorAtNow + count : anchorAtNow);
4161
+ }
4162
+ #matchesLastCommittedStateAfterBoundaryInsert(direction, count) {
4163
+ const state = this.#lastCommittedState;
4164
+ if (state == null) return false;
4165
+ return sameState({
4166
+ position: direction === "unshift" && state.position != null ? state.position + count : state.position,
4167
+ offset: state.offset
4168
+ }, this.#options.readListState().position, this.#options.readListState().offset);
4169
+ }
4170
+ #hasEffectiveAutoFollowCapability(boundary) {
4171
+ const animationBoundary = this.#readJumpBoundary();
4172
+ return boundary === "top" ? this.#confirmedAutoFollowTop || this.#pendingBoundaryJumpTop || animationBoundary === "top" : this.#confirmedAutoFollowBottom || this.#pendingBoundaryJumpBottom || animationBoundary === "bottom";
4173
+ }
4174
+ #readJumpBoundary() {
4175
+ const source = this.#jumpAnimation?.source;
4176
+ if (source == null || source.kind === "manual") return;
4177
+ return source.boundary;
4178
+ }
4179
+ #armBoundaryJump(boundary) {
4180
+ this.#pendingBoundaryJumpTop = boundary === "top";
4181
+ this.#pendingBoundaryJumpBottom = boundary === "bottom";
4182
+ }
4183
+ #clearPendingBoundaryJumps() {
4184
+ this.#pendingBoundaryJumpTop = false;
4185
+ this.#pendingBoundaryJumpBottom = false;
4186
+ }
4187
+ };
4188
+ //#endregion
4189
+ //#region src/renderer/virtualized/transition-snapshot.ts
4190
+ var VisibilitySnapshot = class {
4191
+ #drawnItems = /* @__PURE__ */ new Set();
4192
+ #visibleItems = /* @__PURE__ */ new Set();
4193
+ #previousVisibleItems = /* @__PURE__ */ new Set();
4194
+ #hasSnapshot = false;
4195
+ #snapshotState;
4196
+ #previousSnapshotState;
4197
+ #emptyState;
4198
+ #coversShortList = false;
4199
+ #topGap = 0;
4200
+ #bottomGap = 0;
4201
+ #atStartBoundary = false;
4202
+ #atEndBoundary = false;
4203
+ #currentExtraShift = 0;
4204
+ #minDrawnIndex = Number.POSITIVE_INFINITY;
4205
+ #maxDrawnIndex = Number.NEGATIVE_INFINITY;
4206
+ #topBoundaryItem;
4207
+ #bottomBoundaryItem;
4208
+ get coversShortList() {
4209
+ return this.#hasSnapshot && this.#snapshotState != null && this.#coversShortList;
4210
+ }
4211
+ get topGap() {
4212
+ return this.#topGap;
4213
+ }
4214
+ get bottomGap() {
4215
+ return this.#bottomGap;
4216
+ }
4217
+ get previousState() {
4218
+ return this.#previousSnapshotState;
4219
+ }
4220
+ get currentExtraShift() {
4221
+ return this.#currentExtraShift;
4222
+ }
4223
+ readDrawnIndexRange() {
4224
+ if (!Number.isFinite(this.#minDrawnIndex) || !Number.isFinite(this.#maxDrawnIndex)) return;
4225
+ return {
4226
+ minIndex: this.#minDrawnIndex,
4227
+ maxIndex: this.#maxDrawnIndex
4228
+ };
4229
+ }
4230
+ readBoundaryItem(boundary) {
4231
+ return boundary === "top" ? this.#topBoundaryItem : this.#bottomBoundaryItem;
4232
+ }
4233
+ capture(window, _resolutionPath, items, viewportHeight, snapshotState, extraShift, readVisibleRange) {
4234
+ this.#previousVisibleItems = this.#visibleItems;
4235
+ this.#previousSnapshotState = this.#snapshotState;
4236
+ const nextDrawnItems = /* @__PURE__ */ new Set();
4237
+ const nextVisibleItems = /* @__PURE__ */ new Set();
4238
+ let minVisibleIndex = Number.POSITIVE_INFINITY;
4239
+ let maxVisibleIndex = Number.NEGATIVE_INFINITY;
4240
+ let topMostY = Number.POSITIVE_INFINITY;
4241
+ let bottomMostY = Number.NEGATIVE_INFINITY;
4242
+ let nextMinDrawnIndex = Number.POSITIVE_INFINITY;
4243
+ let nextMaxDrawnIndex = Number.NEGATIVE_INFINITY;
4244
+ let nextTopBoundaryItem;
4245
+ let nextBottomBoundaryItem;
4246
+ let nextTopBoundaryY = Number.POSITIVE_INFINITY;
4247
+ let nextBottomBoundaryY = Number.NEGATIVE_INFINITY;
4248
+ const effectiveShift = window.shift + extraShift;
4249
+ for (const { idx, offset, height } of window.drawList) {
4250
+ minVisibleIndex = Math.min(minVisibleIndex, idx);
4251
+ maxVisibleIndex = Math.max(maxVisibleIndex, idx);
4252
+ nextMinDrawnIndex = Math.min(nextMinDrawnIndex, idx);
4253
+ nextMaxDrawnIndex = Math.max(nextMaxDrawnIndex, idx);
4254
+ const y = offset + effectiveShift;
4255
+ topMostY = Math.min(topMostY, y);
4256
+ bottomMostY = Math.max(bottomMostY, y + height);
4257
+ const item = items[idx];
4258
+ if (item != null) {
4259
+ nextDrawnItems.add(item);
4260
+ if (y < nextTopBoundaryY) {
4261
+ nextTopBoundaryY = y;
4262
+ nextTopBoundaryItem = item;
4263
+ }
4264
+ if (y + height > nextBottomBoundaryY) {
4265
+ nextBottomBoundaryY = y + height;
4266
+ nextBottomBoundaryItem = item;
4267
+ }
4268
+ }
4269
+ if (item == null || readVisibleRange(y, height) == null) continue;
4270
+ nextVisibleItems.add(item);
4271
+ }
4272
+ this.#drawnItems = nextDrawnItems;
4273
+ this.#visibleItems = nextVisibleItems;
4274
+ this.#hasSnapshot = true;
4275
+ this.#snapshotState = snapshotState;
4276
+ this.#currentExtraShift = extraShift;
4277
+ this.#minDrawnIndex = nextMinDrawnIndex;
4278
+ this.#maxDrawnIndex = nextMaxDrawnIndex;
4279
+ this.#topBoundaryItem = nextTopBoundaryItem;
4280
+ this.#bottomBoundaryItem = nextBottomBoundaryItem;
4281
+ this.#emptyState = items.length === 0 && window.drawList.length === 0 ? snapshotState : void 0;
4282
+ const contentHeight = bottomMostY - topMostY;
4283
+ this.#coversShortList = window.drawList.length > 0 && items.length > 0 && window.drawList.length === items.length && minVisibleIndex === 0 && maxVisibleIndex === items.length - 1 && topMostY >= -Number.EPSILON && bottomMostY <= viewportHeight + Number.EPSILON && contentHeight < viewportHeight - Number.EPSILON;
4284
+ this.#topGap = this.#coversShortList ? Math.max(0, topMostY) : 0;
4285
+ this.#bottomGap = this.#coversShortList ? Math.max(0, viewportHeight - bottomMostY) : 0;
4286
+ this.#atStartBoundary = window.drawList.length > 0 && items.length > 0 && minVisibleIndex === 0 && topMostY >= -Number.EPSILON;
4287
+ this.#atEndBoundary = window.drawList.length > 0 && items.length > 0 && maxVisibleIndex === items.length - 1 && bottomMostY <= viewportHeight + Number.EPSILON;
4288
+ }
4289
+ matchesCurrentState(position, offset) {
4290
+ return this.#hasSnapshot && this.#snapshotState != null && sameState(this.#snapshotState, position, offset);
4291
+ }
4292
+ matchesBoundaryInsertState(direction, count, position, offset) {
4293
+ if (!this.coversShortList || this.#snapshotState == null) return false;
4294
+ return this.#matchesStateAfterBoundaryInsert(direction, count, position, offset);
4295
+ }
4296
+ matchesFollowBoundaryInsertState(direction, count, position, offset) {
4297
+ if (!this.#hasSnapshot || this.#snapshotState == null) return false;
4298
+ if (direction === "push" ? !this.#atEndBoundary : !this.#atStartBoundary) return false;
4299
+ return this.#matchesStateAfterBoundaryInsert(direction, count, position, offset);
4300
+ }
4301
+ matchesEmptyBoundaryInsertState(direction, count, position, offset) {
4302
+ const emptyState = this.#emptyState;
4303
+ if (!this.#hasSnapshot || emptyState == null) return false;
4304
+ return sameState({
4305
+ position: direction === "unshift" && emptyState.position != null ? emptyState.position + count : emptyState.position,
4306
+ offset: emptyState.offset
4307
+ }, position, offset);
4308
+ }
4309
+ isVisible(item) {
4310
+ return this.#visibleItems.has(item);
4311
+ }
4312
+ wasVisible(item) {
4313
+ return this.#previousVisibleItems.has(item);
4314
+ }
4315
+ tracks(item, retention) {
4316
+ return retention === "drawn" ? this.#drawnItems.has(item) : this.#visibleItems.has(item);
4317
+ }
4077
4318
  reset() {
4078
- this.#replacementAnimations = /* @__PURE__ */ new WeakMap();
4079
- this.#activeReplacementItems.clear();
4319
+ this.#drawnItems.clear();
4080
4320
  this.#visibleItems.clear();
4081
- this.#hasVisibleItemSnapshot = false;
4082
- this.#visibleSnapshotState = void 0;
4321
+ this.#previousVisibleItems.clear();
4322
+ this.#hasSnapshot = false;
4323
+ this.#snapshotState = void 0;
4324
+ this.#previousSnapshotState = void 0;
4325
+ this.#emptyState = void 0;
4326
+ this.#coversShortList = false;
4327
+ this.#topGap = 0;
4328
+ this.#bottomGap = 0;
4329
+ this.#atStartBoundary = false;
4330
+ this.#atEndBoundary = false;
4331
+ this.#currentExtraShift = 0;
4332
+ this.#minDrawnIndex = Number.POSITIVE_INFINITY;
4333
+ this.#maxDrawnIndex = Number.NEGATIVE_INFINITY;
4334
+ this.#topBoundaryItem = void 0;
4335
+ this.#bottomBoundaryItem = void 0;
4336
+ }
4337
+ #matchesStateAfterBoundaryInsert(direction, count, position, offset) {
4338
+ const snapshotState = this.#snapshotState;
4339
+ if (snapshotState == null) return false;
4340
+ return sameState({
4341
+ position: direction === "unshift" && snapshotState.position != null ? snapshotState.position + count : snapshotState.position,
4342
+ offset: snapshotState.offset
4343
+ }, position, offset);
4083
4344
  }
4084
- #createLayer(node, fromAlpha, toAlpha, startTime, duration) {
4345
+ };
4346
+ //#endregion
4347
+ //#region src/renderer/virtualized/transition-store.ts
4348
+ var TransitionStore = class {
4349
+ #transitions = /* @__PURE__ */ new Map();
4350
+ get size() {
4351
+ return this.#transitions.size;
4352
+ }
4353
+ has(item) {
4354
+ return this.#transitions.has(item);
4355
+ }
4356
+ set(item, transition) {
4357
+ this.#transitions.set(item, transition);
4358
+ }
4359
+ replace(prevItem, nextItem, transition) {
4360
+ this.#transitions.delete(prevItem);
4361
+ this.#transitions.set(nextItem, transition);
4362
+ }
4363
+ delete(item) {
4364
+ const transition = this.#transitions.get(item);
4365
+ if (transition != null) this.#transitions.delete(item);
4366
+ return transition;
4367
+ }
4368
+ readActive(item, now) {
4369
+ const transition = this.#transitions.get(item);
4370
+ if (transition == null) return;
4371
+ return this.#isComplete(transition, now) ? void 0 : transition;
4372
+ }
4373
+ prepare(now) {
4374
+ for (const transition of this.#transitions.values()) if (!this.#isComplete(transition, now)) return true;
4375
+ return false;
4376
+ }
4377
+ findCompleted(now) {
4378
+ return [...this.#transitions.entries()].filter(([, transition]) => this.#isComplete(transition, now)).map(([item, transition]) => ({
4379
+ item,
4380
+ transition
4381
+ }));
4382
+ }
4383
+ findInvisible(snapshot) {
4384
+ return [...this.#transitions.entries()].filter(([item, transition]) => !snapshot.tracks(item, transition.retention)).map(([item, transition]) => ({
4385
+ item,
4386
+ transition
4387
+ }));
4388
+ }
4389
+ reset() {
4390
+ this.#transitions.clear();
4391
+ }
4392
+ #isComplete(transition, now) {
4393
+ return getProgress(transition.height.startTime, transition.height.duration, now) >= 1;
4394
+ }
4395
+ };
4396
+ //#endregion
4397
+ //#region src/renderer/virtualized/transition-planner.ts
4398
+ function isFinitePositive(value) {
4399
+ return Number.isFinite(value) && value > 0;
4400
+ }
4401
+ function normalizeDuration(duration) {
4402
+ return Math.max(0, typeof duration === "number" && Number.isFinite(duration) ? duration : 0);
4403
+ }
4404
+ function createScalarAnimation(from, to, startTime, duration) {
4405
+ return {
4406
+ from,
4407
+ to,
4408
+ startTime,
4409
+ duration
4410
+ };
4411
+ }
4412
+ function createLayerAnimation(node, fromAlpha, toAlpha, startTime, duration, fromTranslateY, toTranslateY) {
4413
+ return {
4414
+ node,
4415
+ alpha: createScalarAnimation(fromAlpha, toAlpha, startTime, duration),
4416
+ translateY: createScalarAnimation(fromTranslateY, toTranslateY, startTime, duration)
4417
+ };
4418
+ }
4419
+ function findVisibleEntry(index, resolveVisibleWindow, readVisibleRange) {
4420
+ if (index < 0) return;
4421
+ const solution = resolveVisibleWindow();
4422
+ for (const entry of solution.window.drawList) {
4423
+ if (entry.idx !== index) continue;
4424
+ if (readVisibleRange(entry.offset + solution.window.shift, entry.height) != null) return entry;
4425
+ }
4426
+ }
4427
+ function isIndexVisible(index, resolveVisibleWindow, readVisibleRange) {
4428
+ return findVisibleEntry(index, resolveVisibleWindow, readVisibleRange) != null;
4429
+ }
4430
+ function resolveAnimationEligibility(params) {
4431
+ if (params.index < 0) return false;
4432
+ if (params.snapshot.matchesCurrentState(params.position, params.offset)) return params.snapshot.isVisible(params.item);
4433
+ return isIndexVisible(params.index, params.resolveVisibleWindow, params.readVisibleRange);
4434
+ }
4435
+ function resolveBoundaryInsertStrategy(direction, underflowAlign, coversShortListSnapshot) {
4436
+ if (!coversShortListSnapshot) return "hard-cut";
4437
+ if (direction === "push" && underflowAlign === "bottom" || direction === "unshift" && underflowAlign === "top") return "viewport-slide";
4438
+ return "item-enter";
4439
+ }
4440
+ function sampleScalarAnimation(animation, now) {
4441
+ return interpolate(animation.from, animation.to, animation.startTime, animation.duration, now);
4442
+ }
4443
+ function sampleLayerAnimation(layer, now) {
4444
+ const alpha = sampleScalarAnimation(layer.alpha, now);
4445
+ if (alpha <= .001) return;
4446
+ return {
4447
+ alpha,
4448
+ node: layer.node,
4449
+ translateY: sampleScalarAnimation(layer.translateY, now)
4450
+ };
4451
+ }
4452
+ function sampleTransition(transition, now) {
4453
+ return {
4454
+ kind: transition.kind,
4455
+ slotHeight: sampleScalarAnimation(transition.height, now),
4456
+ layers: transition.layers.map((layer) => sampleLayerAnimation(layer, now)).filter((layer) => layer != null),
4457
+ retention: transition.retention
4458
+ };
4459
+ }
4460
+ function planExistingItemTransition(params) {
4461
+ if (!params.canAnimate || params.duration <= 0) return;
4462
+ if (params.kind === "update" && !Number.isFinite(params.nextHeight)) return;
4463
+ const layers = [];
4464
+ if (params.currentVisualState.alpha > .001) layers.push(createLayerAnimation(params.currentVisualState.node, params.currentVisualState.alpha, 0, params.now, params.duration, params.currentVisualState.translateY, 0));
4465
+ if (params.kind === "update") {
4466
+ layers.push(createLayerAnimation(params.nextNode, 0, 1, params.now, params.duration, params.currentVisualState.translateY, 0));
4085
4467
  return {
4468
+ kind: "update",
4469
+ layers,
4470
+ height: createScalarAnimation(params.currentVisualState.height, params.nextHeight, params.now, params.duration),
4471
+ retention: "visible"
4472
+ };
4473
+ }
4474
+ return {
4475
+ kind: "delete",
4476
+ layers,
4477
+ height: createScalarAnimation(params.currentVisualState.height, 0, params.now, params.duration),
4478
+ retention: "visible"
4479
+ };
4480
+ }
4481
+ function planViewportShift(params) {
4482
+ if (!isFinitePositive(params.travel) || params.duration <= 0) return;
4483
+ return createScalarAnimation(params.direction === "positive" ? params.currentTranslateY + params.travel : params.currentTranslateY - params.travel, 0, params.now, params.duration);
4484
+ }
4485
+ function planBoundaryInsert(params) {
4486
+ switch (params.strategy) {
4487
+ case "hard-cut": return;
4488
+ case "item-enter": return planBoundaryInsertItems(params);
4489
+ case "viewport-slide": return planBoundaryInsertViewportShift(params);
4490
+ }
4491
+ }
4492
+ function planBoundaryInsertItems(params) {
4493
+ const entries = [];
4494
+ const signedDistance = params.direction === "push" ? 1 : -1;
4495
+ for (const { item, node, height } of params.measuredItems) {
4496
+ if (!Number.isFinite(height) || height < 0) return;
4497
+ const resolvedDistance = typeof params.distance === "number" && Number.isFinite(params.distance) ? Math.max(0, params.distance) : Math.min(24, height);
4498
+ entries.push({
4499
+ item,
4500
+ transition: {
4501
+ kind: "insert",
4502
+ layers: [createLayerAnimation(node, 0, 1, params.now, params.duration, signedDistance * resolvedDistance, 0)],
4503
+ height: createScalarAnimation(height, height, params.now, params.duration),
4504
+ retention: "drawn"
4505
+ }
4506
+ });
4507
+ }
4508
+ return entries.length === 0 ? void 0 : {
4509
+ kind: "item-enter",
4510
+ entries
4511
+ };
4512
+ }
4513
+ function planBoundaryInsertViewportShift(params) {
4514
+ let insertedHeight = 0;
4515
+ for (const { height } of params.measuredItems) {
4516
+ if (!Number.isFinite(height) || height <= 0) return;
4517
+ insertedHeight += height;
4518
+ }
4519
+ if (!isFinitePositive(insertedHeight)) return;
4520
+ const gap = params.direction === "push" ? params.snapshot.topGap : params.snapshot.bottomGap;
4521
+ const travel = Math.min(insertedHeight, gap);
4522
+ const animation = planViewportShift({
4523
+ currentTranslateY: params.currentTranslateY,
4524
+ travel,
4525
+ direction: params.direction === "push" ? "positive" : "negative",
4526
+ now: params.now,
4527
+ duration: params.duration
4528
+ });
4529
+ return animation == null ? void 0 : {
4530
+ kind: "viewport-slide",
4531
+ animation
4532
+ };
4533
+ }
4534
+ function measureBoundaryInsertItems(direction, count, ctx) {
4535
+ const start = direction === "push" ? ctx.items.length - count : 0;
4536
+ const end = direction === "push" ? ctx.items.length : Math.min(count, ctx.items.length);
4537
+ if (start < 0 || end < start) return;
4538
+ const measured = [];
4539
+ for (let index = start; index < end; index += 1) {
4540
+ const item = ctx.items[index];
4541
+ if (item == null) continue;
4542
+ const node = ctx.renderItem(item);
4543
+ const height = ctx.measureNode(node).height;
4544
+ measured.push({
4545
+ item,
4086
4546
  node,
4087
- fromAlpha,
4088
- toAlpha,
4089
- startTime,
4090
- duration
4547
+ height
4548
+ });
4549
+ }
4550
+ return measured;
4551
+ }
4552
+ function drawSampledLayers(sampled, y, adapter) {
4553
+ if (sampled.slotHeight <= 0) return false;
4554
+ let result = false;
4555
+ for (const layer of sampled.layers) {
4556
+ const alpha = clamp$1(layer.alpha, 0, 1);
4557
+ if (alpha <= .001) continue;
4558
+ adapter.graphics.save();
4559
+ try {
4560
+ if (typeof adapter.graphics.globalAlpha === "number") adapter.graphics.globalAlpha *= alpha;
4561
+ if (adapter.drawNode(layer.node, 0, y + layer.translateY)) result = true;
4562
+ } finally {
4563
+ adapter.graphics.restore();
4564
+ }
4565
+ }
4566
+ return result;
4567
+ }
4568
+ function planUpdateTransition(prevItem, nextItem, duration, now, currentVisualState, ctx, snapshot, store) {
4569
+ const nextIndex = ctx.items.indexOf(nextItem);
4570
+ const nextNode = ctx.renderItem(nextItem);
4571
+ const nextHeight = ctx.measureNode(nextNode).height;
4572
+ return planExistingItemTransition({
4573
+ kind: "update",
4574
+ duration: normalizeDuration(duration),
4575
+ canAnimate: resolveAnimationEligibility({
4576
+ index: nextIndex,
4577
+ item: prevItem,
4578
+ position: ctx.position,
4579
+ offset: ctx.offset,
4580
+ snapshot,
4581
+ hasActiveTransition: store.has(prevItem),
4582
+ resolveVisibleWindow: ctx.resolveVisibleWindow,
4583
+ readVisibleRange: ctx.readVisibleRange
4584
+ }),
4585
+ now,
4586
+ currentVisualState,
4587
+ nextNode,
4588
+ nextHeight
4589
+ });
4590
+ }
4591
+ function planDeleteTransition(item, duration, now, currentVisualState, ctx, snapshot, store) {
4592
+ const index = ctx.items.indexOf(item);
4593
+ return planExistingItemTransition({
4594
+ kind: "delete",
4595
+ duration: normalizeDuration(duration),
4596
+ canAnimate: resolveAnimationEligibility({
4597
+ index,
4598
+ item,
4599
+ position: ctx.position,
4600
+ offset: ctx.offset,
4601
+ snapshot,
4602
+ hasActiveTransition: store.has(item),
4603
+ resolveVisibleWindow: ctx.resolveVisibleWindow,
4604
+ readVisibleRange: ctx.readVisibleRange
4605
+ }),
4606
+ now,
4607
+ currentVisualState
4608
+ });
4609
+ }
4610
+ function planBoundaryInsertTransition(direction, count, duration, distance, now, currentTranslateY, ctx, snapshot) {
4611
+ const normalizedDuration = normalizeDuration(duration);
4612
+ if (count <= 0 || normalizedDuration <= 0) return;
4613
+ const strategy = snapshot.matchesBoundaryInsertState(direction, count, ctx.position, ctx.offset) ? resolveBoundaryInsertStrategy(direction, ctx.underflowAlign, true) : snapshot.matchesEmptyBoundaryInsertState(direction, count, ctx.position, ctx.offset) ? "item-enter" : "hard-cut";
4614
+ if (strategy === "hard-cut") return;
4615
+ const measuredItems = measureBoundaryInsertItems(direction, count, ctx);
4616
+ if (measuredItems == null) return;
4617
+ return planBoundaryInsert({
4618
+ direction,
4619
+ duration: normalizedDuration,
4620
+ distance,
4621
+ now,
4622
+ strategy,
4623
+ snapshot,
4624
+ currentTranslateY,
4625
+ measuredItems
4626
+ });
4627
+ }
4628
+ function getTransitionedItemHeight(item, now, store, adapter) {
4629
+ const transition = store.readActive(item, now);
4630
+ if (transition != null) return sampleTransition(transition, now).slotHeight;
4631
+ const node = adapter.renderItem(item);
4632
+ return adapter.measureNode(node).height;
4633
+ }
4634
+ function resolveTransitionedItem(item, now, store, adapter, lifecycle) {
4635
+ const transition = store.readActive(item, now);
4636
+ if (transition == null) {
4637
+ const node = adapter.renderItem(item);
4638
+ return {
4639
+ value: {
4640
+ draw: (y) => adapter.drawNode(node, 0, y),
4641
+ hittest: (test, y) => node.hittest(adapter.getRootContext(), {
4642
+ ...test,
4643
+ y: test.y - y
4644
+ })
4645
+ },
4646
+ height: adapter.measureNode(node).height
4647
+ };
4648
+ }
4649
+ const sampled = sampleTransition(transition, now);
4650
+ return {
4651
+ value: {
4652
+ draw: (y) => drawSampledLayers(sampled, y, adapter),
4653
+ hittest: () => false
4654
+ },
4655
+ height: sampled.slotHeight
4656
+ };
4657
+ }
4658
+ function readCurrentVisualState(item, now, store, adapter) {
4659
+ const transition = store.readActive(item, now);
4660
+ if (transition != null && transition.layers.length > 0) {
4661
+ const primaryLayer = transition.layers[transition.layers.length - 1];
4662
+ return {
4663
+ node: primaryLayer.node,
4664
+ alpha: sampleScalarAnimation(primaryLayer.alpha, now),
4665
+ height: sampleScalarAnimation(transition.height, now),
4666
+ translateY: sampleScalarAnimation(primaryLayer.translateY, now)
4091
4667
  };
4092
4668
  }
4093
- #sampleLayerAlpha(layer, now) {
4094
- return interpolate(layer.fromAlpha, layer.toAlpha, layer.startTime, layer.duration, now);
4669
+ const node = adapter.renderItem(item);
4670
+ return {
4671
+ node,
4672
+ alpha: 1,
4673
+ height: adapter.measureNode(node).height,
4674
+ translateY: 0
4675
+ };
4676
+ }
4677
+ function handleTransitionStateChange(store, snapshot, currentViewportTranslateY, change, ctx, lifecycle) {
4678
+ switch (change.type) {
4679
+ case "update": {
4680
+ const now = getNow();
4681
+ const currentVisualState = readCurrentVisualState(change.prevItem, now, store, ctx);
4682
+ const transition = planUpdateTransition(change.prevItem, change.nextItem, change.animation?.duration, now, currentVisualState, ctx, snapshot, store);
4683
+ if (transition == null) {
4684
+ store.delete(change.prevItem);
4685
+ return {};
4686
+ }
4687
+ store.replace(change.prevItem, change.nextItem, transition);
4688
+ return {};
4689
+ }
4690
+ case "delete": {
4691
+ const now = getNow();
4692
+ const currentVisualState = readCurrentVisualState(change.item, now, store, ctx);
4693
+ const transition = planDeleteTransition(change.item, change.animation?.duration, now, currentVisualState, ctx, snapshot, store);
4694
+ if (transition == null) {
4695
+ store.delete(change.item);
4696
+ lifecycle.onDeleteComplete(change.item);
4697
+ return {};
4698
+ }
4699
+ store.set(change.item, transition);
4700
+ return {};
4701
+ }
4702
+ case "delete-finalize":
4703
+ store.delete(change.item);
4704
+ return {};
4705
+ case "unshift":
4706
+ case "push": {
4707
+ const now = getNow();
4708
+ const plan = planBoundaryInsertTransition(change.type, change.count, change.animation?.duration, change.animation?.distance, now, currentViewportTranslateY, ctx, snapshot);
4709
+ if (plan == null) return {};
4710
+ if (plan.kind === "viewport-slide") return { viewportAnimation: plan.animation };
4711
+ for (const entry of plan.entries) store.set(entry.item, entry.transition);
4712
+ return {};
4713
+ }
4714
+ case "reset":
4715
+ case "set":
4716
+ store.reset();
4717
+ snapshot.reset();
4718
+ return {};
4095
4719
  }
4096
- #sampleReplacementHeight(animation, now) {
4097
- return interpolate(animation.fromHeight, animation.toHeight, animation.startTime, animation.duration, now);
4720
+ }
4721
+ //#endregion
4722
+ //#region src/renderer/virtualized/base-transition.ts
4723
+ function remapAnchorAfterDeletes(anchor, deletedIndices) {
4724
+ if (!Number.isFinite(anchor) || deletedIndices.length === 0) return anchor;
4725
+ const sortedIndices = [...deletedIndices].filter((index) => Number.isFinite(index) && index >= 0).sort((a, b) => a - b);
4726
+ let removedBeforeAnchor = 0;
4727
+ for (const index of sortedIndices) {
4728
+ if (anchor > index + 1) {
4729
+ removedBeforeAnchor += 1;
4730
+ continue;
4731
+ }
4732
+ if (anchor >= index) return index - removedBeforeAnchor;
4098
4733
  }
4099
- #readReplacementLayers(animation, now, measureNode) {
4100
- return [animation.outgoing, animation.incoming].filter((layer) => layer != null).map((layer) => ({
4101
- alpha: this.#sampleLayerAlpha(layer, now),
4102
- node: layer.node,
4103
- nodeHeight: measureNode(layer.node).height
4104
- })).filter((layer) => layer.alpha > ALPHA_EPSILON);
4734
+ return anchor - removedBeforeAnchor;
4735
+ }
4736
+ var TransitionController = class {
4737
+ #store = new TransitionStore();
4738
+ #snapshot = new VisibilitySnapshot();
4739
+ #viewportTranslateAnimation;
4740
+ captureVisibilitySnapshot(window, resolutionPath, items, viewportHeight, snapshotState, extraShift, readVisibleRange) {
4741
+ this.#snapshot.capture(window, resolutionPath, items, viewportHeight, snapshotState, extraShift, readVisibleRange);
4105
4742
  }
4106
- #drawReplacementLayers(layers, slotHeight, y, adapter) {
4107
- if (slotHeight <= 0) return false;
4108
- let result = false;
4109
- const width = adapter.graphics.canvas.clientWidth;
4110
- for (const layer of layers) {
4111
- const alpha = clamp$3(layer.alpha, 0, 1);
4112
- if (alpha <= .001) continue;
4113
- adapter.graphics.save();
4114
- try {
4115
- adapter.graphics.beginPath?.();
4116
- adapter.graphics.rect?.(0, y, width, slotHeight);
4117
- adapter.graphics.clip?.();
4118
- if (typeof adapter.graphics.globalAlpha === "number") adapter.graphics.globalAlpha *= alpha;
4119
- const layerY = y + adapter.getAnimatedLayerOffset(slotHeight, layer.nodeHeight);
4120
- if (adapter.drawNode(layer.node, 0, layerY)) result = true;
4121
- } finally {
4122
- adapter.graphics.restore();
4743
+ pruneInvisible(ctx, lifecycle) {
4744
+ return this.pruneInvisibleAt(getNow(), ctx, lifecycle);
4745
+ }
4746
+ prepare(now, lifecycle) {
4747
+ this.settle(now, lifecycle);
4748
+ this.#cleanupViewportTranslateAnimation(now);
4749
+ const keepViewportAnimating = this.#viewportTranslateAnimation != null;
4750
+ return this.#store.prepare(now) || keepViewportAnimating;
4751
+ }
4752
+ getViewportTranslateY(now) {
4753
+ this.#cleanupViewportTranslateAnimation(now);
4754
+ return this.#viewportTranslateAnimation == null ? 0 : sampleScalarAnimation(this.#viewportTranslateAnimation, now);
4755
+ }
4756
+ canAutoFollowBoundaryInsert(direction, count, position, offset) {
4757
+ return this.#snapshot.matchesFollowBoundaryInsertState(direction, count, position, offset);
4758
+ }
4759
+ getItemHeight(item, now, adapter) {
4760
+ return getTransitionedItemHeight(item, now, this.#store, adapter);
4761
+ }
4762
+ resolveItem(item, now, adapter, lifecycle) {
4763
+ return resolveTransitionedItem(item, now, this.#store, adapter, lifecycle);
4764
+ }
4765
+ handleListStateChange(change, ctx, lifecycle) {
4766
+ const now = getNow();
4767
+ this.settle(now, lifecycle);
4768
+ const result = handleTransitionStateChange(this.#store, this.#snapshot, this.getViewportTranslateY(now), change, ctx, lifecycle);
4769
+ if (change.type === "reset" || change.type === "set") {
4770
+ this.#viewportTranslateAnimation = void 0;
4771
+ return;
4772
+ }
4773
+ if (result.viewportAnimation != null) this.#viewportTranslateAnimation = result.viewportAnimation;
4774
+ }
4775
+ settle(now, lifecycle) {
4776
+ const changed = this.#settleTransitions(this.#store.findCompleted(now), now, lifecycle);
4777
+ this.#cleanupViewportTranslateAnimation(now);
4778
+ return changed;
4779
+ }
4780
+ pruneInvisibleAt(now, ctx, lifecycle) {
4781
+ const removals = this.#store.findInvisible(this.#snapshot);
4782
+ return this.#settleTransitions(removals, now, lifecycle, this.#resolveNaturalBoundarySnap(removals, now, ctx, lifecycle));
4783
+ }
4784
+ reset() {
4785
+ this.#store.reset();
4786
+ this.#snapshot.reset();
4787
+ this.#viewportTranslateAnimation = void 0;
4788
+ }
4789
+ #cleanupViewportTranslateAnimation(now) {
4790
+ const animation = this.#viewportTranslateAnimation;
4791
+ if (animation == null) return;
4792
+ if (getProgress(animation.startTime, animation.duration, now) >= 1) this.#viewportTranslateAnimation = void 0;
4793
+ }
4794
+ #settleTransitions(removals, now, lifecycle, boundarySnap) {
4795
+ if (removals.length === 0) return false;
4796
+ const anchor = lifecycle.captureVisualAnchor(now);
4797
+ const completedDeleteIndices = [];
4798
+ for (const { item, transition } of removals) {
4799
+ if (transition.kind === "delete") {
4800
+ const index = lifecycle.readItemIndex(item);
4801
+ if (index >= 0) completedDeleteIndices.push(index);
4123
4802
  }
4803
+ this.#store.delete(item);
4804
+ if (transition.kind === "delete") lifecycle.onDeleteComplete(item);
4805
+ }
4806
+ if (anchor != null && Number.isFinite(anchor)) lifecycle.restoreVisualAnchor(remapAnchorAfterDeletes(anchor, completedDeleteIndices));
4807
+ if (boundarySnap != null) lifecycle.snapItemToViewportBoundary(boundarySnap.item, boundarySnap.boundary);
4808
+ return true;
4809
+ }
4810
+ #resolveNaturalBoundarySnap(removals, now, ctx, lifecycle) {
4811
+ const previousState = this.#snapshot.previousState;
4812
+ const drawnRange = this.#snapshot.readDrawnIndexRange();
4813
+ if (previousState == null || drawnRange == null) return;
4814
+ const naturalIndices = [];
4815
+ for (const { item, transition } of removals) {
4816
+ if (transition.kind !== "update" && transition.kind !== "delete") continue;
4817
+ const index = lifecycle.readItemIndex(item);
4818
+ if (index < 0 || !this.#snapshot.wasVisible(item)) return;
4819
+ if (this.#isTransitionVisibleInState(index, previousState, now, this.#snapshot.currentExtraShift, ctx)) return;
4820
+ naturalIndices.push(index);
4821
+ }
4822
+ if (naturalIndices.length === 0) return;
4823
+ if (naturalIndices.every((index) => index < drawnRange.minIndex)) {
4824
+ const item = this.#snapshot.readBoundaryItem("top");
4825
+ return item == null ? void 0 : {
4826
+ item,
4827
+ boundary: "top"
4828
+ };
4829
+ }
4830
+ if (naturalIndices.every((index) => index > drawnRange.maxIndex)) {
4831
+ const item = this.#snapshot.readBoundaryItem("bottom");
4832
+ return item == null ? void 0 : {
4833
+ item,
4834
+ boundary: "bottom"
4835
+ };
4124
4836
  }
4125
- return result;
4126
4837
  }
4127
- #isIndexVisible(index, resolveVisibleWindow, readVisibleRange) {
4128
- if (index < 0) return false;
4129
- const solution = resolveVisibleWindow();
4838
+ #isTransitionVisibleInState(index, state, now, extraShift, ctx) {
4839
+ const solution = ctx.resolveVisibleWindowForState(state, now);
4130
4840
  for (const entry of solution.window.drawList) {
4131
4841
  if (entry.idx !== index) continue;
4132
- if (readVisibleRange(entry.offset + solution.window.shift, entry.height) != null) return true;
4842
+ return ctx.readVisibleRange(entry.offset + solution.window.shift + extraShift, entry.height) != null;
4133
4843
  }
4134
4844
  return false;
4135
4845
  }
4136
- #canAnimateUpdate(nextIndex, prevItem, ctx) {
4137
- if (nextIndex < 0) return false;
4138
- if (this.#hasVisibleItemSnapshot && this.#visibleSnapshotState != null && sameState(this.#visibleSnapshotState, ctx.position, ctx.offset)) return this.#visibleItems.has(prevItem) || this.#activeReplacementItems.has(prevItem);
4139
- return this.#isIndexVisible(nextIndex, ctx.resolveVisibleWindow, ctx.readVisibleRange);
4140
- }
4141
4846
  };
4142
4847
  //#endregion
4143
4848
  //#region src/renderer/virtualized/base.ts
@@ -4147,12 +4852,25 @@ var ReplacementController = class {
4147
4852
  var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
4148
4853
  static MIN_JUMP_DURATION = 160;
4149
4854
  static MAX_JUMP_DURATION = 420;
4150
- static JUMP_DURATION_PER_ITEM = 28;
4151
- #controlledState;
4152
- #jumpAnimation;
4153
- #replacementController = new ReplacementController();
4855
+ static JUMP_DURATION_PER_PIXEL = .7;
4856
+ #jumpController;
4857
+ #transitionController = new TransitionController();
4154
4858
  constructor(graphics, options) {
4155
4859
  super(graphics, options);
4860
+ this.#jumpController = new JumpController({
4861
+ minJumpDuration: VirtualizedRenderer.MIN_JUMP_DURATION,
4862
+ maxJumpDuration: VirtualizedRenderer.MAX_JUMP_DURATION,
4863
+ jumpDurationPerPixel: VirtualizedRenderer.JUMP_DURATION_PER_PIXEL,
4864
+ getItemCount: () => this.items.length,
4865
+ readListState: this._readListState.bind(this),
4866
+ normalizeListState: this._normalizeListState.bind(this),
4867
+ readAnchor: (state) => this._readAnchor(state, this._getItemHeight.bind(this)),
4868
+ applyAnchor: this._applyAnchor.bind(this),
4869
+ getDefaultJumpBlock: this._getDefaultJumpBlock.bind(this),
4870
+ getTargetAnchor: this._getTargetAnchor.bind(this),
4871
+ clampItemIndex: this._clampItemIndex.bind(this),
4872
+ getItemHeight: this._getItemHeight.bind(this)
4873
+ });
4156
4874
  subscribeListState(options.list, this, (owner, change) => {
4157
4875
  owner.#handleListStateChange(change);
4158
4876
  });
@@ -4183,30 +4901,41 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
4183
4901
  }
4184
4902
  /** Renders the current visible window. */
4185
4903
  render(feedback) {
4904
+ this.#jumpController.beforeFrame();
4186
4905
  const now = getNow();
4187
4906
  const keepAnimating = this._prepareRender(now);
4188
4907
  const { clientWidth: viewportWidth, clientHeight: viewportHeight } = this.graphics.canvas;
4189
4908
  this.graphics.clearRect(0, 0, viewportWidth, viewportHeight);
4190
- let solution = this._resolveVisibleWindow(now);
4191
- this._captureVisibleItemSnapshot(solution.window);
4192
- const requestSettleRedraw = this._pruneReplacementAnimations(solution.window);
4193
- if (requestSettleRedraw) {
4194
- solution = this._resolveVisibleWindow(now);
4195
- this._captureVisibleItemSnapshot(solution.window);
4909
+ const frame = prepareFrameSession({
4910
+ now,
4911
+ resolveVisibleWindow: (frameNow) => this._resolveVisibleWindow(frameNow),
4912
+ getViewportTranslateY: (frameNow) => this.#transitionController.getViewportTranslateY(frameNow),
4913
+ captureVisibleItemSnapshot: (solution, extraShift) => this._captureVisibleItemSnapshot(solution, extraShift),
4914
+ pruneTransitionAnimations: (window, frameNow) => this._pruneTransitionAnimations(window, frameNow)
4915
+ });
4916
+ const autoFollowCapabilities = this.#jumpController.syncAutoFollowCapabilities(this._readAutoFollowCapabilities(frame.solution.window, frame.viewportTranslateY));
4917
+ const requestRedraw = this._renderVisibleWindow(frame.solution.window, feedback, frame.viewportTranslateY);
4918
+ if (feedback != null) {
4919
+ feedback.canAutoFollowTop = autoFollowCapabilities.top;
4920
+ feedback.canAutoFollowBottom = autoFollowCapabilities.bottom;
4196
4921
  }
4197
- const requestRedraw = this._renderVisibleWindow(solution.window, feedback);
4198
- this._commitListState(solution.normalizedState);
4199
- return this._finishRender(keepAnimating || requestRedraw || requestSettleRedraw);
4922
+ this._commitListState(frame.solution.normalizedState);
4923
+ return this._finishRender(keepAnimating || requestRedraw || frame.requestSettleRedraw);
4200
4924
  }
4201
4925
  /** Hit-tests the current visible window. */
4202
4926
  hittest(test) {
4203
- let solution = this._resolveVisibleWindow(getNow());
4204
- this._captureVisibleItemSnapshot(solution.window);
4205
- if (this._pruneReplacementAnimations(solution.window)) {
4206
- solution = this._resolveVisibleWindow(getNow());
4207
- this._captureVisibleItemSnapshot(solution.window);
4208
- }
4209
- return this._hittestVisibleWindow(solution.window, test);
4927
+ this.#jumpController.beforeFrame();
4928
+ const now = getNow();
4929
+ this.#transitionController.settle(now, this.#getTransitionLifecycleAdapter());
4930
+ const frame = prepareFrameSession({
4931
+ now,
4932
+ resolveVisibleWindow: (frameNow) => this._resolveVisibleWindow(frameNow),
4933
+ getViewportTranslateY: (frameNow) => this.#transitionController.getViewportTranslateY(frameNow),
4934
+ captureVisibleItemSnapshot: (solution, extraShift) => this._captureVisibleItemSnapshot(solution, extraShift),
4935
+ pruneTransitionAnimations: (window, frameNow) => this._pruneTransitionAnimations(window, frameNow)
4936
+ });
4937
+ this.#jumpController.syncAutoFollowCapabilities(this._readAutoFollowCapabilities(frame.solution.window, frame.viewportTranslateY));
4938
+ return this._hittestVisibleWindow(frame.solution.window, test, frame.viewportTranslateY);
4210
4939
  }
4211
4940
  _readListState() {
4212
4941
  return {
@@ -4214,51 +4943,31 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
4214
4943
  offset: this.offset
4215
4944
  };
4216
4945
  }
4946
+ _resolveVisibleWindow(now) {
4947
+ return this._resolveVisibleWindowForState(this._readListState(), now);
4948
+ }
4217
4949
  _commitListState(state) {
4218
4950
  this.position = state.position;
4219
4951
  this.offset = state.offset;
4952
+ this.#jumpController.commit(state);
4220
4953
  }
4221
4954
  /**
4222
4955
  * Scrolls the viewport to the requested item index.
4223
4956
  */
4224
4957
  jumpTo(index, options = {}) {
4225
- if (this.items.length === 0) {
4226
- this.#cancelJumpAnimation();
4227
- return;
4228
- }
4229
- const targetIndex = this._clampItemIndex(index);
4230
- const currentState = this._normalizeListState(this._readListState());
4231
- const targetBlock = options.block ?? this._getDefaultJumpBlock();
4232
- const targetAnchor = this._getTargetAnchor(targetIndex, targetBlock);
4233
- if (!(options.animated ?? true)) {
4234
- this.#cancelJumpAnimation();
4235
- this._applyAnchor(targetAnchor);
4236
- options.onComplete?.();
4237
- return;
4238
- }
4239
- const startAnchor = this._readAnchor(currentState);
4240
- if (!Number.isFinite(startAnchor)) {
4241
- this.#cancelJumpAnimation();
4242
- this._applyAnchor(targetAnchor);
4243
- options.onComplete?.();
4244
- return;
4245
- }
4246
- const duration = clamp$3(options.duration ?? VirtualizedRenderer.MIN_JUMP_DURATION + Math.abs(targetAnchor - startAnchor) * VirtualizedRenderer.JUMP_DURATION_PER_ITEM, 0, VirtualizedRenderer.MAX_JUMP_DURATION);
4247
- if (duration <= 0 || Math.abs(targetAnchor - startAnchor) <= Number.EPSILON) {
4248
- this.#cancelJumpAnimation();
4249
- this._applyAnchor(targetAnchor);
4250
- options.onComplete?.();
4251
- return;
4252
- }
4253
- this.#jumpAnimation = {
4254
- startAnchor,
4255
- targetAnchor,
4256
- startTime: getNow(),
4257
- duration,
4258
- needsMoreFrames: true,
4259
- onComplete: options.onComplete
4260
- };
4261
- this.#controlledState = this._readListState();
4958
+ this.#jumpController.jumpTo(index, options);
4959
+ }
4960
+ /**
4961
+ * Scrolls the viewport to the visual top edge and arms top auto-follow immediately.
4962
+ */
4963
+ jumpToTop(options = {}) {
4964
+ this.#jumpController.jumpToBoundary("top", options);
4965
+ }
4966
+ /**
4967
+ * Scrolls the viewport to the visual bottom edge and arms bottom auto-follow immediately.
4968
+ */
4969
+ jumpToBottom(options = {}) {
4970
+ this.#jumpController.jumpToBoundary("bottom", options);
4262
4971
  }
4263
4972
  _resetRenderFeedback(feedback) {
4264
4973
  if (feedback == null) return;
@@ -4266,6 +4975,8 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
4266
4975
  feedback.maxIdx = NaN;
4267
4976
  feedback.min = NaN;
4268
4977
  feedback.max = NaN;
4978
+ feedback.canAutoFollowTop = false;
4979
+ feedback.canAutoFollowBottom = false;
4269
4980
  }
4270
4981
  _accumulateRenderFeedback(feedback, idx, top, height) {
4271
4982
  const visibleRange = this._readVisibleRange(top, height);
@@ -4288,178 +4999,262 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
4288
4999
  }
4289
5000
  return result;
4290
5001
  }
4291
- _renderVisibleWindow(window, feedback) {
5002
+ _renderVisibleWindow(window, feedback, extraShift = 0) {
4292
5003
  this._resetRenderFeedback(feedback);
4293
- return this._renderDrawList(window.drawList, window.shift, feedback);
5004
+ return this._renderDrawList(window.drawList, window.shift + extraShift, feedback);
5005
+ }
5006
+ _readAutoFollowCapabilities(window, extraShift = 0) {
5007
+ if (window.drawList.length === 0 || this.items.length === 0) return {
5008
+ top: false,
5009
+ bottom: false
5010
+ };
5011
+ let minIndex = Number.POSITIVE_INFINITY;
5012
+ let maxIndex = Number.NEGATIVE_INFINITY;
5013
+ let topMostY = Number.POSITIVE_INFINITY;
5014
+ let bottomMostY = Number.NEGATIVE_INFINITY;
5015
+ const effectiveShift = window.shift + extraShift;
5016
+ for (const { idx, offset, height } of window.drawList) {
5017
+ minIndex = Math.min(minIndex, idx);
5018
+ maxIndex = Math.max(maxIndex, idx);
5019
+ const y = offset + effectiveShift;
5020
+ topMostY = Math.min(topMostY, y);
5021
+ bottomMostY = Math.max(bottomMostY, y + height);
5022
+ }
5023
+ const viewportHeight = this.graphics.canvas.clientHeight;
5024
+ return {
5025
+ top: minIndex === 0 && topMostY >= -Number.EPSILON,
5026
+ bottom: maxIndex === this.items.length - 1 && bottomMostY <= viewportHeight + Number.EPSILON
5027
+ };
4294
5028
  }
4295
5029
  _readVisibleRange(top, height) {
4296
5030
  if (!Number.isFinite(top) || !Number.isFinite(height) || height <= 0) return;
4297
5031
  const viewportHeight = this.graphics.canvas.clientHeight;
4298
- const visibleTop = clamp$3(-top, 0, height);
4299
- const visibleBottom = clamp$3(viewportHeight - top, 0, height);
5032
+ const visibleTop = clamp$1(-top, 0, height);
5033
+ const visibleBottom = clamp$1(viewportHeight - top, 0, height);
4300
5034
  if (visibleBottom <= visibleTop) return;
4301
5035
  return {
4302
5036
  top: visibleTop,
4303
5037
  bottom: visibleBottom
4304
5038
  };
4305
5039
  }
4306
- _pruneReplacementAnimations(_window) {
4307
- return this.#replacementController.pruneInvisible({ onDeleteComplete: this.#handleDeleteComplete.bind(this) });
5040
+ _pruneTransitionAnimations(_window, now) {
5041
+ return this.#transitionController.pruneInvisibleAt(now, this.#getTransitionPlanningAdapter(), this.#getTransitionLifecycleAdapter());
4308
5042
  }
4309
- _hittestVisibleWindow(window, test) {
5043
+ _hittestVisibleWindow(window, test, extraShift = 0) {
4310
5044
  for (const { value: item, offset, height } of window.drawList) {
4311
- const y = offset + window.shift;
5045
+ const y = offset + window.shift + extraShift;
4312
5046
  if (test.y < y || test.y >= y + height) continue;
4313
5047
  return item.hittest(test, y);
4314
5048
  }
4315
5049
  return false;
4316
5050
  }
4317
- _captureVisibleItemSnapshot(window) {
4318
- this.#replacementController.captureVisibleItemSnapshot(window, this.items, this._readVisibleRange.bind(this), this._readListState.bind(this));
5051
+ _captureVisibleItemSnapshot(solution, extraShift = 0) {
5052
+ const normalizedState = this._normalizeListState(this._readListState());
5053
+ this.#transitionController.captureVisibilitySnapshot(solution.window, solution.resolutionPath, this.items, this.graphics.canvas.clientHeight, normalizedState, extraShift, this._readVisibleRange.bind(this));
4319
5054
  }
4320
5055
  _prepareRender(now) {
4321
- const keepReplacing = this.#replacementController.prepare(now, { onDeleteComplete: this.#handleDeleteComplete.bind(this) });
4322
- const animation = this.#jumpAnimation;
4323
- if (animation == null) return keepReplacing;
4324
- if (this.items.length === 0) {
4325
- this.#cancelJumpAnimation();
4326
- return keepReplacing;
4327
- }
4328
- if (this.#controlledState != null && !sameState(this.#controlledState, this.position, this.offset)) {
4329
- this.#cancelJumpAnimation();
4330
- return keepReplacing;
4331
- }
4332
- const anchor = interpolate(animation.startAnchor, animation.targetAnchor, animation.startTime, animation.duration, now);
4333
- const progress = getProgress(animation.startTime, animation.duration, now);
4334
- this._applyAnchor(anchor);
4335
- animation.needsMoreFrames = progress < 1;
4336
- return keepReplacing || animation.needsMoreFrames;
5056
+ const keepTransitioning = this.#transitionController.prepare(now, this.#getTransitionLifecycleAdapter());
5057
+ const keepJumping = this.#jumpController.prepare(now);
5058
+ return keepTransitioning || keepJumping;
4337
5059
  }
4338
5060
  _finishRender(requestRedraw) {
4339
- const animation = this.#jumpAnimation;
4340
- if (animation == null) return requestRedraw;
4341
- if (animation.needsMoreFrames) {
4342
- this.#controlledState = this._readListState();
4343
- return true;
4344
- }
4345
- const onComplete = animation.onComplete;
4346
- this.#cancelJumpAnimation();
4347
- onComplete?.();
4348
- return requestRedraw || this.#jumpAnimation != null;
5061
+ return this.#jumpController.finishFrame(requestRedraw);
4349
5062
  }
4350
5063
  _clampItemIndex(index) {
4351
- return clamp$3(Number.isFinite(index) ? Math.trunc(index) : 0, 0, this.items.length - 1);
5064
+ return clamp$1(Number.isFinite(index) ? Math.trunc(index) : 0, 0, this.items.length - 1);
4352
5065
  }
4353
5066
  _getItemHeight(index) {
5067
+ return this._getItemHeightAt(index, getNow());
5068
+ }
5069
+ _getItemHeightAt(index, now) {
4354
5070
  const item = this.items[index];
4355
- return this.#replacementController.getItemHeight(item, getNow(), {
5071
+ return this.#transitionController.getItemHeight(item, now, {
4356
5072
  renderItem: this.options.renderItem,
4357
5073
  measureNode: this.measureRootNode.bind(this)
4358
5074
  });
4359
5075
  }
4360
- _resolveItem(item, _index, now) {
4361
- return this.#replacementController.resolveItem(item, now, this.#getReplacementRendererAdapter());
5076
+ _readAnchorAt(now) {
5077
+ if (this.items.length <= 0) return;
5078
+ const state = this._normalizeListState(this._readListState());
5079
+ return this._readAnchor(state, (index) => this._getItemHeightAt(index, now));
4362
5080
  }
4363
- _getAnchorAtOffset(index, offset) {
4364
- if (this.items.length === 0) return 0;
4365
- let currentIndex = this._clampItemIndex(index);
4366
- let remaining = Number.isFinite(offset) ? offset : 0;
4367
- while (true) {
4368
- if (remaining < 0) {
4369
- if (currentIndex === 0) return 0;
4370
- currentIndex -= 1;
4371
- const height = this._getItemHeight(currentIndex);
4372
- if (height > 0) remaining += height;
4373
- continue;
4374
- }
4375
- const height = this._getItemHeight(currentIndex);
4376
- if (height > 0) {
4377
- if (remaining <= height) return currentIndex + remaining / height;
4378
- remaining -= height;
4379
- } else if (remaining === 0) return currentIndex;
4380
- if (currentIndex === this.items.length - 1) return this.items.length;
4381
- currentIndex += 1;
4382
- }
5081
+ _restoreAnchor(anchor) {
5082
+ if (!Number.isFinite(anchor) || this.items.length <= 0) return;
5083
+ this._applyAnchor(anchor);
4383
5084
  }
4384
- #cancelJumpAnimation() {
4385
- this.#jumpAnimation = void 0;
4386
- this.#controlledState = void 0;
5085
+ #snapItemToViewportBoundary(item, boundary) {
5086
+ const index = this.items.indexOf(item);
5087
+ if (index < 0) return;
5088
+ this._applyAnchor(this._getTargetAnchor(index, boundary === "top" ? "start" : "end"));
5089
+ }
5090
+ _resolveItem(item, _index, now) {
5091
+ return this.#transitionController.resolveItem(item, now, this.#getTransitionRenderAdapter(), this.#getTransitionLifecycleAdapter());
4387
5092
  }
4388
5093
  #handleDeleteComplete(item) {
4389
5094
  this.options.list.finalizeDelete(item);
4390
5095
  }
4391
- #getReplacementRendererAdapter() {
5096
+ #getTransitionLifecycleAdapter() {
4392
5097
  return {
4393
- renderItem: this.options.renderItem,
4394
- measureNode: this.measureRootNode.bind(this),
4395
- drawNode: this.drawRootNode.bind(this),
4396
- getRootContext: this.getRootContext.bind(this),
4397
- graphics: this.graphics,
4398
- getAnimatedLayerOffset: this._getAnimatedLayerOffset.bind(this),
4399
- onDeleteComplete: this.#handleDeleteComplete.bind(this)
5098
+ onDeleteComplete: this.#handleDeleteComplete.bind(this),
5099
+ captureVisualAnchor: this._readAnchorAt.bind(this),
5100
+ restoreVisualAnchor: this._restoreAnchor.bind(this),
5101
+ readItemIndex: (item) => this.items.indexOf(item),
5102
+ snapItemToViewportBoundary: this.#snapItemToViewportBoundary.bind(this)
4400
5103
  };
4401
5104
  }
4402
- #getReplacementUpdateContext() {
5105
+ #getVirtualizedRuntime() {
4403
5106
  return {
4404
- ...this.#getReplacementRendererAdapter(),
4405
5107
  items: this.items,
4406
5108
  position: this.position,
4407
5109
  offset: this.offset,
4408
- readListState: this._readListState.bind(this),
5110
+ renderItem: this.options.renderItem,
5111
+ measureNode: this.measureRootNode.bind(this),
4409
5112
  readVisibleRange: this._readVisibleRange.bind(this),
4410
- resolveVisibleWindow: () => this._resolveVisibleWindow(getNow())
5113
+ resolveVisibleWindow: () => this._resolveVisibleWindow(getNow()),
5114
+ resolveVisibleWindowForState: (state, now) => this._resolveVisibleWindowForState(state, now)
5115
+ };
5116
+ }
5117
+ #getTransitionRenderAdapter() {
5118
+ const runtime = this.#getVirtualizedRuntime();
5119
+ return {
5120
+ renderItem: runtime.renderItem,
5121
+ measureNode: runtime.measureNode,
5122
+ drawNode: this.drawRootNode.bind(this),
5123
+ getRootContext: this.getRootContext.bind(this),
5124
+ graphics: this.graphics
5125
+ };
5126
+ }
5127
+ #getTransitionPlanningAdapter() {
5128
+ return {
5129
+ ...this.#getVirtualizedRuntime(),
5130
+ underflowAlign: this._getLayoutOptions().underflowAlign
4411
5131
  };
4412
5132
  }
4413
5133
  #handleListStateChange(change) {
4414
- this.#replacementController.handleListStateChange(change, this.#getReplacementUpdateContext());
5134
+ const nextChange = this.#jumpController.handleListStateChange(change);
5135
+ this.#transitionController.handleListStateChange(nextChange, this.#getTransitionPlanningAdapter(), this.#getTransitionLifecycleAdapter());
4415
5136
  }
4416
5137
  };
4417
5138
  //#endregion
5139
+ //#region src/renderer/virtualized/anchor-model.ts
5140
+ function clampItemIndex(index, itemCount) {
5141
+ if (itemCount <= 0) return 0;
5142
+ return clamp$1(Number.isFinite(index) ? Math.trunc(index) : 0, 0, itemCount - 1);
5143
+ }
5144
+ function readAnchorFromState(itemCount, state, anchorMode, readItemHeight) {
5145
+ if (itemCount <= 0) return 0;
5146
+ const height = readItemHeight(state.position);
5147
+ if (anchorMode === "top") return height > 0 ? state.position - state.offset / height : state.position;
5148
+ return height > 0 ? state.position + 1 - state.offset / height : state.position + 1;
5149
+ }
5150
+ function applyAnchorToState(itemCount, anchor, anchorMode, readItemHeight) {
5151
+ if (itemCount <= 0) return;
5152
+ const clampedAnchor = clamp$1(anchor, 0, itemCount);
5153
+ if (anchorMode === "top") {
5154
+ const position = clamp$1(Math.floor(clampedAnchor), 0, itemCount - 1);
5155
+ const height = readItemHeight(position);
5156
+ const offset = height > 0 ? -(clampedAnchor - position) * height : 0;
5157
+ return {
5158
+ position,
5159
+ offset: Object.is(offset, -0) ? 0 : offset
5160
+ };
5161
+ }
5162
+ const position = clamp$1(Math.ceil(clampedAnchor) - 1, 0, itemCount - 1);
5163
+ const height = readItemHeight(position);
5164
+ const offset = height > 0 ? (position + 1 - clampedAnchor) * height : 0;
5165
+ return {
5166
+ position,
5167
+ offset: Object.is(offset, -0) ? 0 : offset
5168
+ };
5169
+ }
5170
+ function getAnchorAtOffset(itemCount, index, offset, readItemHeight) {
5171
+ if (itemCount <= 0) return 0;
5172
+ let currentIndex = clampItemIndex(index, itemCount);
5173
+ let remaining = Number.isFinite(offset) ? offset : 0;
5174
+ while (true) {
5175
+ if (remaining < 0) {
5176
+ if (currentIndex === 0) return 0;
5177
+ currentIndex -= 1;
5178
+ const height = readItemHeight(currentIndex);
5179
+ if (height > 0) remaining += height;
5180
+ continue;
5181
+ }
5182
+ const height = readItemHeight(currentIndex);
5183
+ if (height > 0) {
5184
+ if (remaining <= height) return currentIndex + remaining / height;
5185
+ remaining -= height;
5186
+ } else if (remaining === 0) return currentIndex;
5187
+ if (currentIndex === itemCount - 1) return itemCount;
5188
+ currentIndex += 1;
5189
+ }
5190
+ }
5191
+ function getTargetAnchorForItem(itemCount, index, block, anchorMode, viewportHeight, readItemHeight) {
5192
+ if (itemCount <= 0) return 0;
5193
+ const targetIndex = clampItemIndex(index, itemCount);
5194
+ const height = readItemHeight(targetIndex);
5195
+ if (anchorMode === "top") switch (block) {
5196
+ case "start": return getAnchorAtOffset(itemCount, targetIndex, 0, readItemHeight);
5197
+ case "center": return getAnchorAtOffset(itemCount, targetIndex, height / 2 - viewportHeight / 2, readItemHeight);
5198
+ case "end": return getAnchorAtOffset(itemCount, targetIndex, height - viewportHeight, readItemHeight);
5199
+ }
5200
+ switch (block) {
5201
+ case "start": return getAnchorAtOffset(itemCount, targetIndex, viewportHeight, readItemHeight);
5202
+ case "center": return getAnchorAtOffset(itemCount, targetIndex, height / 2 + viewportHeight / 2, readItemHeight);
5203
+ case "end": return getAnchorAtOffset(itemCount, targetIndex, height, readItemHeight);
5204
+ }
5205
+ }
5206
+ //#endregion
4418
5207
  //#region src/renderer/virtualized/solver.ts
4419
- function clamp$2(value, min, max) {
5208
+ function clamp(value, min, max) {
4420
5209
  return Math.min(Math.max(value, min), max);
4421
5210
  }
4422
5211
  function normalizeOffset(offset) {
4423
5212
  return Number.isFinite(offset) ? offset : 0;
4424
5213
  }
4425
- function normalizeVisibleState(itemCount, state, direction) {
5214
+ function resolveListLayoutOptions(options = {}) {
5215
+ return {
5216
+ anchorMode: options.anchorMode ?? "top",
5217
+ underflowAlign: options.underflowAlign ?? "top"
5218
+ };
5219
+ }
5220
+ function normalizeVisibleState(itemCount, state, layout) {
4426
5221
  if (itemCount <= 0) return {
4427
5222
  position: 0,
4428
5223
  offset: 0
4429
5224
  };
4430
5225
  const position = state.position;
4431
- const fallbackPosition = direction === "forward" ? 0 : itemCount - 1;
5226
+ const fallbackPosition = layout.anchorMode === "top" ? 0 : itemCount - 1;
4432
5227
  if (typeof position !== "number" || !Number.isFinite(position)) return {
4433
5228
  position: fallbackPosition,
4434
5229
  offset: normalizeOffset(state.offset)
4435
5230
  };
4436
5231
  return {
4437
- position: clamp$2(Math.trunc(position), 0, itemCount - 1),
5232
+ position: clamp(Math.trunc(position), 0, itemCount - 1),
4438
5233
  offset: normalizeOffset(state.offset)
4439
5234
  };
4440
5235
  }
4441
- function normalizeTimelineState(itemCount, state) {
4442
- return normalizeVisibleState(itemCount, state, "forward");
4443
- }
4444
- function normalizeChatState(itemCount, state) {
4445
- return normalizeVisibleState(itemCount, state, "backward");
4446
- }
4447
- function resolveVisibleWindow(items, state, viewportHeight, resolveItem, direction) {
4448
- const normalizedState = normalizeVisibleState(items.length, state, direction);
5236
+ function resolveVisibleWindow(items, state, viewportHeight, resolveItem, layout) {
5237
+ const normalizedState = normalizeVisibleState(items.length, state, layout);
5238
+ const resolutionPath = /* @__PURE__ */ new Set();
5239
+ const readResolvedItem = (item, idx) => {
5240
+ resolutionPath.add(idx);
5241
+ return resolveItem(item, idx);
5242
+ };
4449
5243
  if (items.length === 0) return {
4450
5244
  normalizedState,
5245
+ resolutionPath: [],
4451
5246
  window: {
4452
5247
  drawList: [],
4453
5248
  shift: 0
4454
5249
  }
4455
5250
  };
4456
- if (direction === "forward") {
5251
+ if (layout.anchorMode === "top") {
4457
5252
  let { position, offset } = normalizedState;
4458
5253
  let drawLength = 0;
4459
5254
  if (offset > 0) if (position === 0) offset = 0;
4460
5255
  else {
4461
5256
  for (let i = position - 1; i >= 0; i -= 1) {
4462
- const { height } = resolveItem(items[i], i);
5257
+ const { height } = readResolvedItem(items[i], i);
4463
5258
  position = i;
4464
5259
  offset -= height;
4465
5260
  if (offset <= 0) break;
@@ -4469,7 +5264,7 @@ function resolveVisibleWindow(items, state, viewportHeight, resolveItem, directi
4469
5264
  let y = offset;
4470
5265
  const drawList = [];
4471
5266
  for (let i = position; i < items.length; i += 1) {
4472
- const { value, height } = resolveItem(items[i], i);
5267
+ const { value, height } = readResolvedItem(items[i], i);
4473
5268
  if (y + height > 0) {
4474
5269
  drawList.push({
4475
5270
  idx: i,
@@ -4494,7 +5289,7 @@ function resolveVisibleWindow(items, state, viewportHeight, resolveItem, directi
4494
5289
  y = offset += shift;
4495
5290
  let lastIdx = -1;
4496
5291
  for (let i = position - 1; i >= 0; i -= 1) {
4497
- const { value, height } = resolveItem(items[i], i);
5292
+ const { value, height } = readResolvedItem(items[i], i);
4498
5293
  drawLength += height;
4499
5294
  y -= height;
4500
5295
  drawList.push({
@@ -4512,22 +5307,19 @@ function resolveVisibleWindow(items, state, viewportHeight, resolveItem, directi
4512
5307
  offset = 0;
4513
5308
  }
4514
5309
  }
4515
- return {
4516
- normalizedState: {
4517
- position,
4518
- offset
4519
- },
4520
- window: {
4521
- drawList,
4522
- shift
4523
- }
4524
- };
5310
+ return finalizeVisibleWindowResult(items.length, viewportHeight, layout, {
5311
+ position,
5312
+ offset
5313
+ }, Array.from(resolutionPath), {
5314
+ drawList,
5315
+ shift
5316
+ });
4525
5317
  }
4526
5318
  let { position, offset } = normalizedState;
4527
5319
  let drawLength = 0;
4528
5320
  if (offset < 0) if (position === items.length - 1) offset = 0;
4529
5321
  else for (let i = position + 1; i < items.length; i += 1) {
4530
- const { height } = resolveItem(items[i], i);
5322
+ const { height } = readResolvedItem(items[i], i);
4531
5323
  position = i;
4532
5324
  offset += height;
4533
5325
  if (offset > 0) break;
@@ -4535,7 +5327,7 @@ function resolveVisibleWindow(items, state, viewportHeight, resolveItem, directi
4535
5327
  let y = viewportHeight + offset;
4536
5328
  const drawList = [];
4537
5329
  for (let i = position; i >= 0; i -= 1) {
4538
- const { value, height } = resolveItem(items[i], i);
5330
+ const { value, height } = readResolvedItem(items[i], i);
4539
5331
  y -= height;
4540
5332
  if (y <= viewportHeight) {
4541
5333
  drawList.push({
@@ -4557,7 +5349,7 @@ function resolveVisibleWindow(items, state, viewportHeight, resolveItem, directi
4557
5349
  if (drawLength < viewportHeight) {
4558
5350
  y = drawLength;
4559
5351
  for (let i = position + 1; i < items.length; i += 1) {
4560
- const { value, height } = resolveItem(items[i], i);
5352
+ const { value, height } = readResolvedItem(items[i], i);
4561
5353
  drawList.push({
4562
5354
  idx: i,
4563
5355
  value,
@@ -4571,122 +5363,88 @@ function resolveVisibleWindow(items, state, viewportHeight, resolveItem, directi
4571
5363
  offset = drawLength < viewportHeight ? 0 : drawLength - viewportHeight;
4572
5364
  } else offset = drawLength - viewportHeight;
4573
5365
  }
5366
+ return finalizeVisibleWindowResult(items.length, viewportHeight, layout, {
5367
+ position,
5368
+ offset
5369
+ }, Array.from(resolutionPath), {
5370
+ drawList,
5371
+ shift
5372
+ });
5373
+ }
5374
+ function finalizeVisibleWindowResult(itemCount, viewportHeight, layout, normalizedState, resolutionPath, window) {
5375
+ if (window.drawList.length !== itemCount || itemCount <= 0) return {
5376
+ normalizedState,
5377
+ resolutionPath,
5378
+ window
5379
+ };
5380
+ let minIndex = Number.POSITIVE_INFINITY;
5381
+ let maxIndex = Number.NEGATIVE_INFINITY;
5382
+ let minOffset = Number.POSITIVE_INFINITY;
5383
+ let maxBottom = Number.NEGATIVE_INFINITY;
5384
+ for (const entry of window.drawList) {
5385
+ minIndex = Math.min(minIndex, entry.idx);
5386
+ maxIndex = Math.max(maxIndex, entry.idx);
5387
+ minOffset = Math.min(minOffset, entry.offset);
5388
+ maxBottom = Math.max(maxBottom, entry.offset + entry.height);
5389
+ }
5390
+ const contentHeight = maxBottom - minOffset;
5391
+ if (minIndex !== 0 || maxIndex !== itemCount - 1 || !(contentHeight < viewportHeight - Number.EPSILON)) return {
5392
+ normalizedState,
5393
+ resolutionPath,
5394
+ window
5395
+ };
5396
+ const desiredTop = layout.underflowAlign === "bottom" ? viewportHeight - contentHeight : 0;
4574
5397
  return {
4575
- normalizedState: {
4576
- position,
4577
- offset
5398
+ normalizedState: layout.anchorMode === "top" ? {
5399
+ position: 0,
5400
+ offset: 0
5401
+ } : {
5402
+ position: itemCount - 1,
5403
+ offset: 0
4578
5404
  },
5405
+ resolutionPath,
4579
5406
  window: {
4580
- drawList,
4581
- shift
5407
+ drawList: window.drawList,
5408
+ shift: desiredTop - minOffset
4582
5409
  }
4583
5410
  };
4584
5411
  }
4585
- function resolveTimelineVisibleWindow(items, state, viewportHeight, resolveItem) {
4586
- return resolveVisibleWindow(items, state, viewportHeight, resolveItem, "forward");
4587
- }
4588
- function resolveChatVisibleWindow(items, state, viewportHeight, resolveItem) {
4589
- return resolveVisibleWindow(items, state, viewportHeight, resolveItem, "backward");
4590
- }
4591
5412
  //#endregion
4592
- //#region src/renderer/virtualized/chat.ts
4593
- function clamp$1(value, min, max) {
4594
- return Math.min(Math.max(value, min), max);
4595
- }
5413
+ //#region src/renderer/virtualized/list.ts
4596
5414
  /**
4597
- * Virtualized renderer anchored to the bottom, suitable for chat-style UIs.
5415
+ * Virtualized list renderer with configurable anchor semantics.
4598
5416
  */
4599
- var ChatRenderer = class extends VirtualizedRenderer {
4600
- _resolveVisibleWindow(now) {
4601
- return resolveChatVisibleWindow(this.items, this._readListState(), this.graphics.canvas.clientHeight, (item, idx) => {
4602
- return this._resolveItem(item, idx, now);
4603
- });
4604
- }
4605
- _getDefaultJumpBlock() {
4606
- return "end";
4607
- }
4608
- _normalizeListState(state) {
4609
- return normalizeChatState(this.items.length, state);
4610
- }
4611
- _readAnchor(state) {
4612
- if (this.items.length === 0) return 0;
4613
- const height = this._getItemHeight(state.position);
4614
- return height > 0 ? state.position + 1 - state.offset / height : state.position + 1;
4615
- }
4616
- _applyAnchor(anchor) {
4617
- if (this.items.length === 0) return;
4618
- const clampedAnchor = clamp$1(anchor, 0, this.items.length);
4619
- const position = clamp$1(Math.ceil(clampedAnchor) - 1, 0, this.items.length - 1);
4620
- const height = this._getItemHeight(position);
4621
- const offset = height > 0 ? (position + 1 - clampedAnchor) * height : 0;
4622
- this._commitListState({
4623
- position,
4624
- offset: Object.is(offset, -0) ? 0 : offset
4625
- });
4626
- }
4627
- _getTargetAnchor(index, block) {
4628
- const height = this._getItemHeight(index);
4629
- const viewportHeight = this.graphics.canvas.clientHeight;
4630
- switch (block) {
4631
- case "start": return this._getAnchorAtOffset(index, viewportHeight);
4632
- case "center": return this._getAnchorAtOffset(index, height / 2 + viewportHeight / 2);
4633
- case "end": return this._getAnchorAtOffset(index, height);
4634
- }
5417
+ var ListRenderer = class extends VirtualizedRenderer {
5418
+ #layout;
5419
+ constructor(graphics, options) {
5420
+ super(graphics, options);
5421
+ this.#layout = resolveListLayoutOptions(options);
4635
5422
  }
4636
- _getAnimatedLayerOffset(slotHeight, nodeHeight) {
4637
- return slotHeight - nodeHeight;
5423
+ _getLayoutOptions() {
5424
+ return this.#layout;
4638
5425
  }
4639
- };
4640
- //#endregion
4641
- //#region src/renderer/virtualized/timeline.ts
4642
- function clamp(value, min, max) {
4643
- return Math.min(Math.max(value, min), max);
4644
- }
4645
- /**
4646
- * Virtualized renderer anchored to the top, suitable for timeline-style UIs.
4647
- */
4648
- var TimelineRenderer = class extends VirtualizedRenderer {
4649
- _resolveVisibleWindow(now) {
4650
- return resolveTimelineVisibleWindow(this.items, this._readListState(), this.graphics.canvas.clientHeight, (item, idx) => {
4651
- return this._resolveItem(item, idx, now);
4652
- });
5426
+ _resolveVisibleWindowForState(state, now) {
5427
+ return resolveVisibleWindow(this.items, state, this.graphics.canvas.clientHeight, (item, idx) => this._resolveItem(item, idx, now), this.#layout);
4653
5428
  }
4654
5429
  _getDefaultJumpBlock() {
4655
- return "start";
5430
+ return this.#layout.anchorMode === "top" ? "start" : "end";
4656
5431
  }
4657
5432
  _normalizeListState(state) {
4658
- return normalizeTimelineState(this.items.length, state);
5433
+ return normalizeVisibleState(this.items.length, state, this.#layout);
4659
5434
  }
4660
- _readAnchor(state) {
4661
- if (this.items.length === 0) return 0;
4662
- const height = this._getItemHeight(state.position);
4663
- return height > 0 ? state.position - state.offset / height : state.position;
5435
+ _readAnchor(state, readItemHeight) {
5436
+ return readAnchorFromState(this.items.length, state, this.#layout.anchorMode, readItemHeight);
4664
5437
  }
4665
5438
  _applyAnchor(anchor) {
4666
- if (this.items.length === 0) return;
4667
- const clampedAnchor = clamp(anchor, 0, this.items.length);
4668
- const position = clamp(Math.floor(clampedAnchor), 0, this.items.length - 1);
4669
- const height = this._getItemHeight(position);
4670
- const offset = height > 0 ? -(clampedAnchor - position) * height : 0;
4671
- this._commitListState({
4672
- position,
4673
- offset: Object.is(offset, -0) ? 0 : offset
4674
- });
5439
+ const state = applyAnchorToState(this.items.length, anchor, this.#layout.anchorMode, this._getItemHeight.bind(this));
5440
+ if (state == null) return;
5441
+ this._commitListState(state);
4675
5442
  }
4676
5443
  _getTargetAnchor(index, block) {
4677
- const height = this._getItemHeight(index);
4678
- const viewportHeight = this.graphics.canvas.clientHeight;
4679
- switch (block) {
4680
- case "start": return this._getAnchorAtOffset(index, 0);
4681
- case "center": return this._getAnchorAtOffset(index, height / 2 - viewportHeight / 2);
4682
- case "end": return this._getAnchorAtOffset(index, height - viewportHeight);
4683
- }
4684
- }
4685
- _getAnimatedLayerOffset(_slotHeight, _nodeHeight) {
4686
- return 0;
5444
+ return getTargetAnchorForItem(this.items.length, index, block, this.#layout.anchorMode, this.graphics.canvas.clientHeight, this._getItemHeight.bind(this));
4687
5445
  }
4688
5446
  };
4689
5447
  //#endregion
4690
- export { BaseRenderer, ChatRenderer, DebugRenderer, Fixed, Flex, FlexItem, Group, ListState, MultilineText, PaddingBox, Place, ShrinkWrap, Text, TimelineRenderer, VirtualizedRenderer, Wrapper, memoRenderItem, memoRenderItemBy };
5448
+ export { BaseRenderer, DebugRenderer, Fixed, Flex, FlexItem, Group, ListRenderer, ListState, MultilineText, PaddingBox, Place, ShrinkWrap, Text, VirtualizedRenderer, Wrapper, memoRenderItem, memoRenderItemBy };
4691
5449
 
4692
5450
  //# sourceMappingURL=index.mjs.map