chat-layout 1.2.0-4 → 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
@@ -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?.followIfAtBoundary === true) normalizedAnimation.followIfAtBoundary = 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,287 +3875,826 @@ 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
+ #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;
3902
4004
  }
3903
- this.#visibleItems = nextVisibleItems;
3904
- this.#hasVisibleItemSnapshot = true;
3905
- this.#visibleSnapshotState = readListState();
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;
3906
4016
  }
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;
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;
3920
4023
  }
3921
- return changed;
4024
+ const onComplete = animation.onComplete;
4025
+ this.#cancelJumpAnimation();
4026
+ onComplete?.();
4027
+ return requestRedraw || this.#jumpAnimation != null;
3922
4028
  }
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;
4029
+ commit(state) {
4030
+ this.#lastCommittedState = {
4031
+ position: state.position,
4032
+ offset: state.offset
4033
+ };
3928
4034
  }
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);
4035
+ jumpTo(index, options = {}) {
4036
+ this.#clearAutoFollowLatch();
4037
+ if (this.#options.getItemCount() === 0) {
4038
+ this.#cancelJumpAnimation();
3940
4039
  return;
3941
4040
  }
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;
3950
- }
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);
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
+ });
3956
4059
  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
4060
+ ...followChange.change,
4061
+ animation: void 0
3965
4062
  };
3966
4063
  }
3967
- const slotHeight = this.#sampleReplacementHeight(replacement, now);
3968
- const layers = this.#readReplacementLayers(replacement, now, adapter.measureNode);
3969
- return {
3970
- value: {
3971
- draw: (y) => this.#drawReplacementLayers(layers, slotHeight, y, adapter),
3972
- hittest: () => false
3973
- },
3974
- height: slotHeight
3975
- };
4064
+ return change;
3976
4065
  }
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;
3995
- }
4066
+ #cancelJumpAnimation() {
4067
+ this.#jumpAnimation = void 0;
4068
+ this.#controlledState = void 0;
3996
4069
  }
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);
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?.();
4003
4079
  return;
4004
4080
  }
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);
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;
4019
4087
  }
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);
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?.();
4042
4094
  return;
4043
4095
  }
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;
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;
4063
4116
  }
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
4117
  }
4076
- /** Clears all animation state (e.g., on list reset). */
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
+ }
4077
4234
  reset() {
4078
- this.#replacementAnimations = /* @__PURE__ */ new WeakMap();
4079
- this.#activeReplacementItems.clear();
4235
+ this.#drawnItems.clear();
4080
4236
  this.#visibleItems.clear();
4081
- this.#hasVisibleItemSnapshot = false;
4082
- this.#visibleSnapshotState = void 0;
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;
4083
4334
  }
4084
- #createLayer(node, fromAlpha, toAlpha, startTime, duration) {
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));
4085
4376
  return {
4086
- node,
4087
- fromAlpha,
4088
- toAlpha,
4089
- startTime,
4090
- duration
4377
+ kind: "update",
4378
+ layers,
4379
+ height: createScalarAnimation(params.currentVisualState.height, params.nextHeight, params.now, params.duration),
4380
+ retention: "visible"
4091
4381
  };
4092
4382
  }
4093
- #sampleLayerAlpha(layer, now) {
4094
- return interpolate(layer.fromAlpha, layer.toAlpha, layer.startTime, layer.duration, now);
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);
4095
4399
  }
4096
- #sampleReplacementHeight(animation, now) {
4097
- return interpolate(animation.fromHeight, animation.toHeight, animation.startTime, animation.duration, now);
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
+ });
4098
4416
  }
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);
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
+ });
4105
4458
  }
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();
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 {};
4123
4595
  }
4596
+ store.replace(change.prevItem, change.nextItem, transition);
4597
+ return {};
4124
4598
  }
4125
- return result;
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);
4126
4654
  }
4127
- #isIndexVisible(index, resolveVisibleWindow, readVisibleRange) {
4128
- if (index < 0) return false;
4129
- const solution = resolveVisibleWindow();
4130
- for (const entry of solution.window.drawList) {
4131
- if (entry.idx !== index) continue;
4132
- if (readVisibleRange(entry.offset + solution.window.shift, entry.height) != null) return true;
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;
4133
4668
  }
4134
- return false;
4669
+ if (result.viewportAnimation != null) this.#viewportTranslateAnimation = result.viewportAnimation;
4135
4670
  }
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);
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;
4140
4698
  }
4141
4699
  };
4142
4700
  //#endregion
@@ -4147,12 +4705,26 @@ var ReplacementController = class {
4147
4705
  var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
4148
4706
  static MIN_JUMP_DURATION = 160;
4149
4707
  static MAX_JUMP_DURATION = 420;
4150
- static JUMP_DURATION_PER_ITEM = 28;
4151
- #controlledState;
4152
- #jumpAnimation;
4153
- #replacementController = new ReplacementController();
4708
+ static JUMP_DURATION_PER_PIXEL = .7;
4709
+ #jumpController;
4710
+ #transitionController = new TransitionController();
4154
4711
  constructor(graphics, options) {
4155
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
+ });
4156
4728
  subscribeListState(options.list, this, (owner, change) => {
4157
4729
  owner.#handleListStateChange(change);
4158
4730
  });
@@ -4183,30 +4755,35 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
4183
4755
  }
4184
4756
  /** Renders the current visible window. */
4185
4757
  render(feedback) {
4758
+ this.#jumpController.beforeFrame();
4186
4759
  const now = getNow();
4187
4760
  const keepAnimating = this._prepareRender(now);
4188
4761
  const { clientWidth: viewportWidth, clientHeight: viewportHeight } = this.graphics.canvas;
4189
4762
  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);
4196
- }
4197
- const requestRedraw = this._renderVisibleWindow(solution.window, feedback);
4198
- this._commitListState(solution.normalizedState);
4199
- return this._finishRender(keepAnimating || requestRedraw || requestSettleRedraw);
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);
4200
4773
  }
4201
4774
  /** Hit-tests the current visible window. */
4202
4775
  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);
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);
4210
4787
  }
4211
4788
  _readListState() {
4212
4789
  return {
@@ -4214,51 +4791,19 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
4214
4791
  offset: this.offset
4215
4792
  };
4216
4793
  }
4794
+ _resolveVisibleWindow(now) {
4795
+ return this._resolveVisibleWindowForState(this._readListState(), now);
4796
+ }
4217
4797
  _commitListState(state) {
4218
4798
  this.position = state.position;
4219
4799
  this.offset = state.offset;
4800
+ this.#jumpController.commit(state);
4220
4801
  }
4221
4802
  /**
4222
4803
  * Scrolls the viewport to the requested item index.
4223
4804
  */
4224
4805
  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();
4806
+ this.#jumpController.jumpTo(index, options);
4262
4807
  }
4263
4808
  _resetRenderFeedback(feedback) {
4264
4809
  if (feedback == null) return;
@@ -4288,178 +4833,231 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
4288
4833
  }
4289
4834
  return result;
4290
4835
  }
4291
- _renderVisibleWindow(window, feedback) {
4836
+ _renderVisibleWindow(window, feedback, extraShift = 0) {
4292
4837
  this._resetRenderFeedback(feedback);
4293
- return this._renderDrawList(window.drawList, window.shift, feedback);
4838
+ return this._renderDrawList(window.drawList, window.shift + extraShift, feedback);
4294
4839
  }
4295
4840
  _readVisibleRange(top, height) {
4296
4841
  if (!Number.isFinite(top) || !Number.isFinite(height) || height <= 0) return;
4297
4842
  const viewportHeight = this.graphics.canvas.clientHeight;
4298
- const visibleTop = clamp$3(-top, 0, height);
4299
- const visibleBottom = clamp$3(viewportHeight - top, 0, height);
4843
+ const visibleTop = clamp$1(-top, 0, height);
4844
+ const visibleBottom = clamp$1(viewportHeight - top, 0, height);
4300
4845
  if (visibleBottom <= visibleTop) return;
4301
4846
  return {
4302
4847
  top: visibleTop,
4303
4848
  bottom: visibleBottom
4304
4849
  };
4305
4850
  }
4306
- _pruneReplacementAnimations(_window) {
4307
- return this.#replacementController.pruneInvisible({ onDeleteComplete: this.#handleDeleteComplete.bind(this) });
4851
+ _pruneTransitionAnimations(_window, now) {
4852
+ return this.#transitionController.pruneInvisibleAt(now, this.#getTransitionLifecycleAdapter());
4308
4853
  }
4309
- _hittestVisibleWindow(window, test) {
4854
+ _hittestVisibleWindow(window, test, extraShift = 0) {
4310
4855
  for (const { value: item, offset, height } of window.drawList) {
4311
- const y = offset + window.shift;
4856
+ const y = offset + window.shift + extraShift;
4312
4857
  if (test.y < y || test.y >= y + height) continue;
4313
4858
  return item.hittest(test, y);
4314
4859
  }
4315
4860
  return false;
4316
4861
  }
4317
- _captureVisibleItemSnapshot(window) {
4318
- this.#replacementController.captureVisibleItemSnapshot(window, this.items, this._readVisibleRange.bind(this), this._readListState.bind(this));
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));
4319
4865
  }
4320
4866
  _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;
4867
+ const keepTransitioning = this.#transitionController.prepare(now, this.#getTransitionLifecycleAdapter());
4868
+ const keepJumping = this.#jumpController.prepare(now);
4869
+ return keepTransitioning || keepJumping;
4337
4870
  }
4338
4871
  _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;
4872
+ return this.#jumpController.finishFrame(requestRedraw);
4349
4873
  }
4350
4874
  _clampItemIndex(index) {
4351
- 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);
4352
4876
  }
4353
4877
  _getItemHeight(index) {
4878
+ return this._getItemHeightAt(index, getNow());
4879
+ }
4880
+ _getItemHeightAt(index, now) {
4354
4881
  const item = this.items[index];
4355
- return this.#replacementController.getItemHeight(item, getNow(), {
4882
+ return this.#transitionController.getItemHeight(item, now, {
4356
4883
  renderItem: this.options.renderItem,
4357
4884
  measureNode: this.measureRootNode.bind(this)
4358
4885
  });
4359
4886
  }
4360
- _resolveItem(item, _index, now) {
4361
- return this.#replacementController.resolveItem(item, now, this.#getReplacementRendererAdapter());
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));
4362
4891
  }
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
- }
4892
+ _restoreAnchor(anchor) {
4893
+ if (!Number.isFinite(anchor) || this.items.length <= 0) return;
4894
+ this._applyAnchor(anchor);
4383
4895
  }
4384
- #cancelJumpAnimation() {
4385
- this.#jumpAnimation = void 0;
4386
- this.#controlledState = void 0;
4896
+ _resolveItem(item, _index, now) {
4897
+ return this.#transitionController.resolveItem(item, now, this.#getTransitionRenderAdapter(), this.#getTransitionLifecycleAdapter());
4387
4898
  }
4388
4899
  #handleDeleteComplete(item) {
4389
4900
  this.options.list.finalizeDelete(item);
4390
4901
  }
4391
- #getReplacementRendererAdapter() {
4902
+ #getTransitionLifecycleAdapter() {
4392
4903
  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)
4904
+ onDeleteComplete: this.#handleDeleteComplete.bind(this),
4905
+ captureVisualAnchor: this._readAnchorAt.bind(this),
4906
+ restoreVisualAnchor: this._restoreAnchor.bind(this)
4400
4907
  };
4401
4908
  }
4402
- #getReplacementUpdateContext() {
4909
+ #getVirtualizedRuntime() {
4403
4910
  return {
4404
- ...this.#getReplacementRendererAdapter(),
4405
4911
  items: this.items,
4406
4912
  position: this.position,
4407
4913
  offset: this.offset,
4408
- readListState: this._readListState.bind(this),
4914
+ renderItem: this.options.renderItem,
4915
+ measureNode: this.measureRootNode.bind(this),
4409
4916
  readVisibleRange: this._readVisibleRange.bind(this),
4410
4917
  resolveVisibleWindow: () => this._resolveVisibleWindow(getNow())
4411
4918
  };
4412
4919
  }
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
+ };
4929
+ }
4930
+ #getTransitionPlanningAdapter() {
4931
+ return {
4932
+ ...this.#getVirtualizedRuntime(),
4933
+ underflowAlign: this._getLayoutOptions().underflowAlign
4934
+ };
4935
+ }
4413
4936
  #handleListStateChange(change) {
4414
- this.#replacementController.handleListStateChange(change, this.#getReplacementUpdateContext());
4937
+ const nextChange = this.#jumpController.handleListStateChange(change);
4938
+ this.#transitionController.handleListStateChange(nextChange, this.#getTransitionPlanningAdapter(), this.#getTransitionLifecycleAdapter());
4415
4939
  }
4416
4940
  };
4417
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;
4984
+ }
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);
5007
+ }
5008
+ }
5009
+ //#endregion
4418
5010
  //#region src/renderer/virtualized/solver.ts
4419
- function clamp$2(value, min, max) {
5011
+ function clamp(value, min, max) {
4420
5012
  return Math.min(Math.max(value, min), max);
4421
5013
  }
4422
5014
  function normalizeOffset(offset) {
4423
5015
  return Number.isFinite(offset) ? offset : 0;
4424
5016
  }
4425
- function normalizeVisibleState(itemCount, state, direction) {
5017
+ function resolveListLayoutOptions(options = {}) {
5018
+ return {
5019
+ anchorMode: options.anchorMode ?? "top",
5020
+ underflowAlign: options.underflowAlign ?? "top"
5021
+ };
5022
+ }
5023
+ function normalizeVisibleState(itemCount, state, layout) {
4426
5024
  if (itemCount <= 0) return {
4427
5025
  position: 0,
4428
5026
  offset: 0
4429
5027
  };
4430
5028
  const position = state.position;
4431
- const fallbackPosition = direction === "forward" ? 0 : itemCount - 1;
5029
+ const fallbackPosition = layout.anchorMode === "top" ? 0 : itemCount - 1;
4432
5030
  if (typeof position !== "number" || !Number.isFinite(position)) return {
4433
5031
  position: fallbackPosition,
4434
5032
  offset: normalizeOffset(state.offset)
4435
5033
  };
4436
5034
  return {
4437
- position: clamp$2(Math.trunc(position), 0, itemCount - 1),
5035
+ position: clamp(Math.trunc(position), 0, itemCount - 1),
4438
5036
  offset: normalizeOffset(state.offset)
4439
5037
  };
4440
5038
  }
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);
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
+ };
4449
5046
  if (items.length === 0) return {
4450
5047
  normalizedState,
5048
+ resolutionPath: [],
4451
5049
  window: {
4452
5050
  drawList: [],
4453
5051
  shift: 0
4454
5052
  }
4455
5053
  };
4456
- if (direction === "forward") {
5054
+ if (layout.anchorMode === "top") {
4457
5055
  let { position, offset } = normalizedState;
4458
5056
  let drawLength = 0;
4459
5057
  if (offset > 0) if (position === 0) offset = 0;
4460
5058
  else {
4461
5059
  for (let i = position - 1; i >= 0; i -= 1) {
4462
- const { height } = resolveItem(items[i], i);
5060
+ const { height } = readResolvedItem(items[i], i);
4463
5061
  position = i;
4464
5062
  offset -= height;
4465
5063
  if (offset <= 0) break;
@@ -4469,7 +5067,7 @@ function resolveVisibleWindow(items, state, viewportHeight, resolveItem, directi
4469
5067
  let y = offset;
4470
5068
  const drawList = [];
4471
5069
  for (let i = position; i < items.length; i += 1) {
4472
- const { value, height } = resolveItem(items[i], i);
5070
+ const { value, height } = readResolvedItem(items[i], i);
4473
5071
  if (y + height > 0) {
4474
5072
  drawList.push({
4475
5073
  idx: i,
@@ -4494,7 +5092,7 @@ function resolveVisibleWindow(items, state, viewportHeight, resolveItem, directi
4494
5092
  y = offset += shift;
4495
5093
  let lastIdx = -1;
4496
5094
  for (let i = position - 1; i >= 0; i -= 1) {
4497
- const { value, height } = resolveItem(items[i], i);
5095
+ const { value, height } = readResolvedItem(items[i], i);
4498
5096
  drawLength += height;
4499
5097
  y -= height;
4500
5098
  drawList.push({
@@ -4512,22 +5110,19 @@ function resolveVisibleWindow(items, state, viewportHeight, resolveItem, directi
4512
5110
  offset = 0;
4513
5111
  }
4514
5112
  }
4515
- return {
4516
- normalizedState: {
4517
- position,
4518
- offset
4519
- },
4520
- window: {
4521
- drawList,
4522
- shift
4523
- }
4524
- };
5113
+ return finalizeVisibleWindowResult(items.length, viewportHeight, layout, {
5114
+ position,
5115
+ offset
5116
+ }, Array.from(resolutionPath), {
5117
+ drawList,
5118
+ shift
5119
+ });
4525
5120
  }
4526
5121
  let { position, offset } = normalizedState;
4527
5122
  let drawLength = 0;
4528
5123
  if (offset < 0) if (position === items.length - 1) offset = 0;
4529
5124
  else for (let i = position + 1; i < items.length; i += 1) {
4530
- const { height } = resolveItem(items[i], i);
5125
+ const { height } = readResolvedItem(items[i], i);
4531
5126
  position = i;
4532
5127
  offset += height;
4533
5128
  if (offset > 0) break;
@@ -4535,7 +5130,7 @@ function resolveVisibleWindow(items, state, viewportHeight, resolveItem, directi
4535
5130
  let y = viewportHeight + offset;
4536
5131
  const drawList = [];
4537
5132
  for (let i = position; i >= 0; i -= 1) {
4538
- const { value, height } = resolveItem(items[i], i);
5133
+ const { value, height } = readResolvedItem(items[i], i);
4539
5134
  y -= height;
4540
5135
  if (y <= viewportHeight) {
4541
5136
  drawList.push({
@@ -4557,7 +5152,7 @@ function resolveVisibleWindow(items, state, viewportHeight, resolveItem, directi
4557
5152
  if (drawLength < viewportHeight) {
4558
5153
  y = drawLength;
4559
5154
  for (let i = position + 1; i < items.length; i += 1) {
4560
- const { value, height } = resolveItem(items[i], i);
5155
+ const { value, height } = readResolvedItem(items[i], i);
4561
5156
  drawList.push({
4562
5157
  idx: i,
4563
5158
  value,
@@ -4571,122 +5166,88 @@ function resolveVisibleWindow(items, state, viewportHeight, resolveItem, directi
4571
5166
  offset = drawLength < viewportHeight ? 0 : drawLength - viewportHeight;
4572
5167
  } else offset = drawLength - viewportHeight;
4573
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;
4574
5200
  return {
4575
- normalizedState: {
4576
- position,
4577
- offset
5201
+ normalizedState: layout.anchorMode === "top" ? {
5202
+ position: 0,
5203
+ offset: 0
5204
+ } : {
5205
+ position: itemCount - 1,
5206
+ offset: 0
4578
5207
  },
5208
+ resolutionPath,
4579
5209
  window: {
4580
- drawList,
4581
- shift
5210
+ drawList: window.drawList,
5211
+ shift: desiredTop - minOffset
4582
5212
  }
4583
5213
  };
4584
5214
  }
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
5215
  //#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
- }
5216
+ //#region src/renderer/virtualized/list.ts
4596
5217
  /**
4597
- * Virtualized renderer anchored to the bottom, suitable for chat-style UIs.
5218
+ * Virtualized list renderer with configurable anchor semantics.
4598
5219
  */
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
- });
5220
+ var ListRenderer = class extends VirtualizedRenderer {
5221
+ #layout;
5222
+ constructor(graphics, options) {
5223
+ super(graphics, options);
5224
+ this.#layout = resolveListLayoutOptions(options);
4626
5225
  }
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
- }
5226
+ _getLayoutOptions() {
5227
+ return this.#layout;
4635
5228
  }
4636
- _getAnimatedLayerOffset(slotHeight, nodeHeight) {
4637
- return slotHeight - nodeHeight;
4638
- }
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
- });
5229
+ _resolveVisibleWindowForState(state, now) {
5230
+ return resolveVisibleWindow(this.items, state, this.graphics.canvas.clientHeight, (item, idx) => this._resolveItem(item, idx, now), this.#layout);
4653
5231
  }
4654
5232
  _getDefaultJumpBlock() {
4655
- return "start";
5233
+ return this.#layout.anchorMode === "top" ? "start" : "end";
4656
5234
  }
4657
5235
  _normalizeListState(state) {
4658
- return normalizeTimelineState(this.items.length, state);
5236
+ return normalizeVisibleState(this.items.length, state, this.#layout);
4659
5237
  }
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;
5238
+ _readAnchor(state, readItemHeight) {
5239
+ return readAnchorFromState(this.items.length, state, this.#layout.anchorMode, readItemHeight);
4664
5240
  }
4665
5241
  _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
- });
5242
+ const state = applyAnchorToState(this.items.length, anchor, this.#layout.anchorMode, this._getItemHeight.bind(this));
5243
+ if (state == null) return;
5244
+ this._commitListState(state);
4675
5245
  }
4676
5246
  _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;
5247
+ return getTargetAnchorForItem(this.items.length, index, block, this.#layout.anchorMode, this.graphics.canvas.clientHeight, this._getItemHeight.bind(this));
4687
5248
  }
4688
5249
  };
4689
5250
  //#endregion
4690
- 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 };
4691
5252
 
4692
5253
  //# sourceMappingURL=index.mjs.map