chat-layout 1.2.0-6 → 1.2.0-8

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
@@ -2041,6 +2041,154 @@ function createRichSourceItems(spans, defaultFont) {
2041
2041
  }));
2042
2042
  }
2043
2043
  //#endregion
2044
+ //#region src/text/justify.ts
2045
+ let _justifySupported;
2046
+ function isJustifySupported(ctx) {
2047
+ if (_justifySupported !== void 0) return _justifySupported;
2048
+ _justifySupported = typeof ctx.wordSpacing === "string" && typeof ctx.letterSpacing === "string";
2049
+ return _justifySupported;
2050
+ }
2051
+ function resolveJustifyMode(justify) {
2052
+ if (justify === true) return "inter-word";
2053
+ if (justify === "inter-word" || justify === "inter-character") return justify;
2054
+ return null;
2055
+ }
2056
+ const HYBRID_WORD_SHARE_CANDIDATES = [
2057
+ .15,
2058
+ .2,
2059
+ .25,
2060
+ .3,
2061
+ .35,
2062
+ .4,
2063
+ .45,
2064
+ .5,
2065
+ .55,
2066
+ .6,
2067
+ .65,
2068
+ .7,
2069
+ .75,
2070
+ .8,
2071
+ .85,
2072
+ 1,
2073
+ 0
2074
+ ];
2075
+ const PUNCTUATION_OR_SYMBOL_PATTERN = /^[\p{P}\p{S}]$/u;
2076
+ const JUSTIFY_SCORE_EPSILON = 1e-9;
2077
+ function analyzeLineForJustify(prepared, line) {
2078
+ let wordGapCount = 0;
2079
+ let wordCount = 0;
2080
+ let renderAtomCount = 0;
2081
+ let spaceCount = 0;
2082
+ let nonSpaceCount = 0;
2083
+ let cjkCount = 0;
2084
+ let latinLikeCount = 0;
2085
+ let punctuationCount = 0;
2086
+ let nonSpaceWidth = 0;
2087
+ let insideWord = false;
2088
+ forEachAtomInRange(prepared, line.start, line.end, (atom) => {
2089
+ if (atom.kind === "space" && !atom.preservesLineEnd && atom.atomicGroupId == null) wordGapCount++;
2090
+ renderAtomCount++;
2091
+ if (atom.kind === "space") {
2092
+ spaceCount++;
2093
+ insideWord = false;
2094
+ return;
2095
+ }
2096
+ nonSpaceCount++;
2097
+ nonSpaceWidth += atom.width + atom.extraWidthAfter;
2098
+ if (!insideWord) {
2099
+ wordCount++;
2100
+ insideWord = true;
2101
+ }
2102
+ if (isCJK(atom.text)) {
2103
+ cjkCount++;
2104
+ return;
2105
+ }
2106
+ if (PUNCTUATION_OR_SYMBOL_PATTERN.test(atom.text)) {
2107
+ punctuationCount++;
2108
+ return;
2109
+ }
2110
+ latinLikeCount++;
2111
+ });
2112
+ return {
2113
+ wordGapCount,
2114
+ wordCount,
2115
+ renderAtomCount,
2116
+ letterGapCount: Math.max(renderAtomCount - 1, 0),
2117
+ spaceCount,
2118
+ nonSpaceCount,
2119
+ cjkCount,
2120
+ latinLikeCount,
2121
+ punctuationCount,
2122
+ lineWidth: line.width,
2123
+ nonSpaceWidth
2124
+ };
2125
+ }
2126
+ function getAverageWordWidth(info) {
2127
+ return info.wordCount > 0 ? info.nonSpaceWidth / info.wordCount : info.lineWidth;
2128
+ }
2129
+ function getAverageCharWidth(info) {
2130
+ return info.renderAtomCount > 0 ? info.lineWidth / info.renderAtomCount : info.lineWidth;
2131
+ }
2132
+ function resolvePerGapSpacing(totalSpace, gapCount) {
2133
+ if (totalSpace === 0) return 0;
2134
+ if (gapCount <= 0) return null;
2135
+ return totalSpace / gapCount;
2136
+ }
2137
+ function exceedsThreshold(perGap, averageWidth, threshold) {
2138
+ if (!Number.isFinite(threshold)) return false;
2139
+ return perGap > threshold * averageWidth;
2140
+ }
2141
+ function createJustifySpacing(wordSpacingPx, letterSpacingPx) {
2142
+ return {
2143
+ wordSpacing: `${wordSpacingPx}px`,
2144
+ letterSpacing: `${letterSpacingPx}px`,
2145
+ wordSpacingPx,
2146
+ letterSpacingPx
2147
+ };
2148
+ }
2149
+ function computeJustifySpacing(lineWidth, maxWidth, info, mode, threshold = Number.POSITIVE_INFINITY) {
2150
+ const extraSpace = maxWidth - lineWidth;
2151
+ if (extraSpace <= 0 || mode == null) return null;
2152
+ if (mode === "inter-word" && info.wordGapCount > 0) {
2153
+ const perGap = extraSpace / info.wordGapCount;
2154
+ if (exceedsThreshold(perGap, Math.max(getAverageWordWidth(info), Number.EPSILON), threshold)) return null;
2155
+ return createJustifySpacing(perGap, 0);
2156
+ }
2157
+ if (mode !== "inter-character" || info.renderAtomCount === 0) return null;
2158
+ const avgCharWidth = Math.max(getAverageCharWidth(info), Number.EPSILON);
2159
+ if (info.wordGapCount === 0) {
2160
+ const perGap = resolvePerGapSpacing(extraSpace, info.letterGapCount);
2161
+ if (perGap == null) return null;
2162
+ if (exceedsThreshold(perGap, avgCharWidth, threshold)) return null;
2163
+ return createJustifySpacing(0, perGap);
2164
+ }
2165
+ const avgWordWidth = Math.max(getAverageWordWidth(info), Number.EPSILON);
2166
+ const nonSpaceCount = Math.max(info.nonSpaceCount, 1);
2167
+ const cjkRatio = info.cjkCount / nonSpaceCount;
2168
+ const latinLikeRatio = info.latinLikeCount / nonSpaceCount;
2169
+ const punctuationRatio = info.punctuationCount / nonSpaceCount;
2170
+ const wordPenalty = 1 + cjkRatio;
2171
+ const letterPenalty = 1 + latinLikeRatio + .5 * punctuationRatio;
2172
+ let bestCandidate = null;
2173
+ for (const wordShare of HYBRID_WORD_SHARE_CANDIDATES) {
2174
+ const wordExtraSpace = extraSpace * wordShare;
2175
+ const letterExtraSpace = extraSpace - wordExtraSpace;
2176
+ const wordSpacingPx = resolvePerGapSpacing(wordExtraSpace, info.wordGapCount);
2177
+ const letterSpacingPx = resolvePerGapSpacing(letterExtraSpace, info.letterGapCount);
2178
+ if (wordSpacingPx == null || letterSpacingPx == null) continue;
2179
+ if (exceedsThreshold(wordSpacingPx, avgWordWidth, threshold) || exceedsThreshold(letterSpacingPx, avgCharWidth, threshold)) continue;
2180
+ const wordRatio = wordSpacingPx / avgWordWidth;
2181
+ const letterRatio = letterSpacingPx / avgCharWidth;
2182
+ const score = wordPenalty * wordRatio ** 2 + letterPenalty * letterRatio ** 2;
2183
+ if (bestCandidate == null || score < bestCandidate.score - JUSTIFY_SCORE_EPSILON || Math.abs(score - bestCandidate.score) <= JUSTIFY_SCORE_EPSILON && wordShare > bestCandidate.wordShare) bestCandidate = {
2184
+ spacing: createJustifySpacing(wordSpacingPx, letterSpacingPx),
2185
+ score,
2186
+ wordShare
2187
+ };
2188
+ }
2189
+ return bestCandidate?.spacing ?? null;
2190
+ }
2191
+ //#endregion
2044
2192
  //#region src/text/plain-core.ts
2045
2193
  function readPreparedText(text, font, whiteSpace, wordBreak) {
2046
2194
  return readPreparedInlineLayout(getPlainPreparedKey(text, font, whiteSpace, wordBreak), createPlainSourceItems(text, font), whiteSpace, wordBreak);
@@ -2759,174 +2907,26 @@ function layoutRichTextWithOverflow(ctx, spans, maxWidth, defaultFont, defaultCo
2759
2907
  };
2760
2908
  }
2761
2909
  //#endregion
2762
- //#region src/text/justify.ts
2763
- let _justifySupported;
2764
- function isJustifySupported(ctx) {
2765
- if (_justifySupported !== void 0) return _justifySupported;
2766
- _justifySupported = typeof ctx.wordSpacing === "string" && typeof ctx.letterSpacing === "string";
2767
- return _justifySupported;
2768
- }
2769
- function resolveJustifyMode(justify) {
2770
- if (justify === true) return "inter-word";
2771
- if (justify === "inter-word" || justify === "inter-character") return justify;
2772
- return null;
2773
- }
2774
- const HYBRID_WORD_SHARE_CANDIDATES = [
2775
- .15,
2776
- .2,
2777
- .25,
2778
- .3,
2779
- .35,
2780
- .4,
2781
- .45,
2782
- .5,
2783
- .55,
2784
- .6,
2785
- .65,
2786
- .7,
2787
- .75,
2788
- .8,
2789
- .85,
2790
- 1,
2791
- 0
2792
- ];
2793
- const PUNCTUATION_OR_SYMBOL_PATTERN = /^[\p{P}\p{S}]$/u;
2794
- const JUSTIFY_SCORE_EPSILON = 1e-9;
2795
- function analyzeLineForJustify(prepared, line) {
2796
- let wordGapCount = 0;
2797
- let wordCount = 0;
2798
- let renderAtomCount = 0;
2799
- let spaceCount = 0;
2800
- let nonSpaceCount = 0;
2801
- let cjkCount = 0;
2802
- let latinLikeCount = 0;
2803
- let punctuationCount = 0;
2804
- let nonSpaceWidth = 0;
2805
- let insideWord = false;
2806
- forEachAtomInRange(prepared, line.start, line.end, (atom) => {
2807
- if (atom.kind === "space" && !atom.preservesLineEnd && atom.atomicGroupId == null) wordGapCount++;
2808
- renderAtomCount++;
2809
- if (atom.kind === "space") {
2810
- spaceCount++;
2811
- insideWord = false;
2812
- return;
2813
- }
2814
- nonSpaceCount++;
2815
- nonSpaceWidth += atom.width + atom.extraWidthAfter;
2816
- if (!insideWord) {
2817
- wordCount++;
2818
- insideWord = true;
2819
- }
2820
- if (isCJK(atom.text)) {
2821
- cjkCount++;
2822
- return;
2823
- }
2824
- if (PUNCTUATION_OR_SYMBOL_PATTERN.test(atom.text)) {
2825
- punctuationCount++;
2826
- return;
2827
- }
2828
- latinLikeCount++;
2829
- });
2830
- return {
2831
- wordGapCount,
2832
- wordCount,
2833
- renderAtomCount,
2834
- letterGapCount: Math.max(renderAtomCount - 1, 0),
2835
- spaceCount,
2836
- nonSpaceCount,
2837
- cjkCount,
2838
- latinLikeCount,
2839
- punctuationCount,
2840
- lineWidth: line.width,
2841
- nonSpaceWidth
2842
- };
2843
- }
2844
- function getAverageWordWidth(info) {
2845
- return info.wordCount > 0 ? info.nonSpaceWidth / info.wordCount : info.lineWidth;
2910
+ //#region src/nodes/text.ts
2911
+ function resolvePhysicalTextAlign(options) {
2912
+ if (options.physicalAlign != null) return options.physicalAlign;
2913
+ if (options.align != null) switch (options.align) {
2914
+ case "start": return "left";
2915
+ case "center": return "center";
2916
+ case "end": return "right";
2917
+ }
2918
+ return "left";
2846
2919
  }
2847
- function getAverageCharWidth(info) {
2848
- return info.renderAtomCount > 0 ? info.lineWidth / info.renderAtomCount : info.lineWidth;
2920
+ function normalizeTextMaxWidth(maxWidth) {
2921
+ if (maxWidth == null) return;
2922
+ return Math.max(0, maxWidth);
2849
2923
  }
2850
- function resolvePerGapSpacing(totalSpace, gapCount) {
2851
- if (totalSpace === 0) return 0;
2852
- if (gapCount <= 0) return null;
2853
- return totalSpace / gapCount;
2854
- }
2855
- function exceedsThreshold(perGap, averageWidth, threshold) {
2856
- if (!Number.isFinite(threshold)) return false;
2857
- return perGap > threshold * averageWidth;
2858
- }
2859
- function createJustifySpacing(wordSpacingPx, letterSpacingPx) {
2860
- return {
2861
- wordSpacing: `${wordSpacingPx}px`,
2862
- letterSpacing: `${letterSpacingPx}px`,
2863
- wordSpacingPx,
2864
- letterSpacingPx
2865
- };
2866
- }
2867
- function computeJustifySpacing(lineWidth, maxWidth, info, mode, threshold = Number.POSITIVE_INFINITY) {
2868
- const extraSpace = maxWidth - lineWidth;
2869
- if (extraSpace <= 0 || mode == null) return null;
2870
- if (mode === "inter-word" && info.wordGapCount > 0) {
2871
- const perGap = extraSpace / info.wordGapCount;
2872
- if (exceedsThreshold(perGap, Math.max(getAverageWordWidth(info), Number.EPSILON), threshold)) return null;
2873
- return createJustifySpacing(perGap, 0);
2874
- }
2875
- if (mode !== "inter-character" || info.renderAtomCount === 0) return null;
2876
- const avgCharWidth = Math.max(getAverageCharWidth(info), Number.EPSILON);
2877
- if (info.wordGapCount === 0) {
2878
- const perGap = resolvePerGapSpacing(extraSpace, info.letterGapCount);
2879
- if (perGap == null) return null;
2880
- if (exceedsThreshold(perGap, avgCharWidth, threshold)) return null;
2881
- return createJustifySpacing(0, perGap);
2882
- }
2883
- const avgWordWidth = Math.max(getAverageWordWidth(info), Number.EPSILON);
2884
- const nonSpaceCount = Math.max(info.nonSpaceCount, 1);
2885
- const cjkRatio = info.cjkCount / nonSpaceCount;
2886
- const latinLikeRatio = info.latinLikeCount / nonSpaceCount;
2887
- const punctuationRatio = info.punctuationCount / nonSpaceCount;
2888
- const wordPenalty = 1 + cjkRatio;
2889
- const letterPenalty = 1 + latinLikeRatio + .5 * punctuationRatio;
2890
- let bestCandidate = null;
2891
- for (const wordShare of HYBRID_WORD_SHARE_CANDIDATES) {
2892
- const wordExtraSpace = extraSpace * wordShare;
2893
- const letterExtraSpace = extraSpace - wordExtraSpace;
2894
- const wordSpacingPx = resolvePerGapSpacing(wordExtraSpace, info.wordGapCount);
2895
- const letterSpacingPx = resolvePerGapSpacing(letterExtraSpace, info.letterGapCount);
2896
- if (wordSpacingPx == null || letterSpacingPx == null) continue;
2897
- if (exceedsThreshold(wordSpacingPx, avgWordWidth, threshold) || exceedsThreshold(letterSpacingPx, avgCharWidth, threshold)) continue;
2898
- const wordRatio = wordSpacingPx / avgWordWidth;
2899
- const letterRatio = letterSpacingPx / avgCharWidth;
2900
- const score = wordPenalty * wordRatio ** 2 + letterPenalty * letterRatio ** 2;
2901
- if (bestCandidate == null || score < bestCandidate.score - JUSTIFY_SCORE_EPSILON || Math.abs(score - bestCandidate.score) <= JUSTIFY_SCORE_EPSILON && wordShare > bestCandidate.wordShare) bestCandidate = {
2902
- spacing: createJustifySpacing(wordSpacingPx, letterSpacingPx),
2903
- score,
2904
- wordShare
2905
- };
2906
- }
2907
- return bestCandidate?.spacing ?? null;
2908
- }
2909
- //#endregion
2910
- //#region src/nodes/text.ts
2911
- function resolvePhysicalTextAlign(options) {
2912
- if (options.physicalAlign != null) return options.physicalAlign;
2913
- if (options.align != null) switch (options.align) {
2914
- case "start": return "left";
2915
- case "center": return "center";
2916
- case "end": return "right";
2917
- }
2918
- return "left";
2919
- }
2920
- function normalizeTextMaxWidth(maxWidth) {
2921
- if (maxWidth == null) return;
2922
- return Math.max(0, maxWidth);
2923
- }
2924
- const DEFAULT_TEXT_SPACING = {
2925
- wordSpacing: "0px",
2926
- letterSpacing: "0px"
2927
- };
2928
- function supportsTextSpacing(g) {
2929
- return typeof g.wordSpacing === "string" && typeof g.letterSpacing === "string";
2924
+ const DEFAULT_TEXT_SPACING = {
2925
+ wordSpacing: "0px",
2926
+ letterSpacing: "0px"
2927
+ };
2928
+ function supportsTextSpacing(g) {
2929
+ return typeof g.wordSpacing === "string" && typeof g.letterSpacing === "string";
2930
2930
  }
2931
2931
  function withTextSpacing(g, spacing, cb) {
2932
2932
  if (!supportsTextSpacing(g)) return cb();
@@ -3596,6 +3596,44 @@ const listStateListenerRegistry = typeof FinalizationRegistry === "function" ? n
3596
3596
  if (list == null) return;
3597
3597
  deleteListStateListener(list, token);
3598
3598
  }) : null;
3599
+ const listScrollMutations = /* @__PURE__ */ new WeakMap();
3600
+ const WRITE_LIST_SCROLL_STATE = Symbol("writeListScrollState");
3601
+ const FINALIZE_LIST_DELETE = Symbol("finalizeListDelete");
3602
+ function normalizePosition(value) {
3603
+ return typeof value === "number" && Number.isFinite(value) ? Math.trunc(value) : void 0;
3604
+ }
3605
+ function normalizeOffset$1(value) {
3606
+ return Number.isFinite(value) ? value : 0;
3607
+ }
3608
+ function getListScrollMutationRecord(list) {
3609
+ let record = listScrollMutations.get(list);
3610
+ if (record == null) {
3611
+ record = {
3612
+ version: 0,
3613
+ source: "internal"
3614
+ };
3615
+ listScrollMutations.set(list, record);
3616
+ }
3617
+ return record;
3618
+ }
3619
+ function markListScrollMutation(list, source) {
3620
+ const record = getListScrollMutationRecord(list);
3621
+ record.version += 1;
3622
+ record.source = source;
3623
+ }
3624
+ function readListScrollMutation(list) {
3625
+ const record = getListScrollMutationRecord(list);
3626
+ return {
3627
+ version: record.version,
3628
+ source: record.source
3629
+ };
3630
+ }
3631
+ function writeInternalListScrollState(list, state) {
3632
+ list[WRITE_LIST_SCROLL_STATE](state, "internal");
3633
+ }
3634
+ function finalizeInternalListDelete(list, item) {
3635
+ list[FINALIZE_LIST_DELETE](item);
3636
+ }
3599
3637
  function deleteListStateListener(list, token) {
3600
3638
  const listeners = listStateListeners.get(list);
3601
3639
  if (listeners == null) return;
@@ -3660,17 +3698,22 @@ function normalizeInsertAnimation(animation) {
3660
3698
  const duration = normalizeInsertAnimationDuration(animation?.duration, animation != null);
3661
3699
  if (duration == null) return;
3662
3700
  const normalizedAnimation = { duration };
3663
- if (typeof animation?.distance === "number" && Number.isFinite(animation.distance)) normalizedAnimation.distance = Math.max(0, animation.distance);
3664
3701
  if (animation?.autoFollow === true) normalizedAnimation.autoFollow = true;
3665
3702
  return normalizedAnimation;
3666
3703
  }
3667
3704
  var ListState = class {
3668
3705
  #items;
3669
3706
  #pendingDeletes = /* @__PURE__ */ new Set();
3707
+ #offset = 0;
3708
+ #position;
3670
3709
  /** Pixel offset from the anchored item edge. */
3671
- offset = 0;
3710
+ get offset() {
3711
+ return this.#offset;
3712
+ }
3672
3713
  /** Anchor item index, or `undefined` to use the renderer default. */
3673
- position;
3714
+ get position() {
3715
+ return this.#position;
3716
+ }
3674
3717
  /** Items currently managed by the renderer. */
3675
3718
  get items() {
3676
3719
  return this.#items;
@@ -3700,7 +3743,7 @@ var ListState = class {
3700
3743
  if (items.length === 0) return;
3701
3744
  assertUniqueItemReferences(items, this.#items);
3702
3745
  const normalizedAnimation = normalizeInsertAnimation(animation);
3703
- if (this.position != null) this.position += items.length;
3746
+ if (this.position != null) this.#writeScrollState({ position: this.position + items.length }, "internal");
3704
3747
  this.#items = items.concat(this.#items);
3705
3748
  emitListStateChange(this, {
3706
3749
  type: "unshift",
@@ -3753,7 +3796,7 @@ var ListState = class {
3753
3796
  const normalizedAnimation = normalizeDeleteAnimation(animation);
3754
3797
  if (!((normalizedAnimation?.duration ?? 0) > 0)) {
3755
3798
  this.#pendingDeletes.add(item);
3756
- this.finalizeDelete(item);
3799
+ this[FINALIZE_LIST_DELETE](item);
3757
3800
  return;
3758
3801
  }
3759
3802
  this.#pendingDeletes.add(item);
@@ -3766,18 +3809,19 @@ var ListState = class {
3766
3809
  /**
3767
3810
  * Finalizes a pending delete by removing the item from the list.
3768
3811
  */
3769
- finalizeDelete(item) {
3812
+ [FINALIZE_LIST_DELETE](item) {
3770
3813
  if (!this.#pendingDeletes.has(item)) return;
3771
3814
  const index = this.#items.indexOf(item);
3772
3815
  this.#pendingDeletes.delete(item);
3773
3816
  if (index < 0) return;
3774
3817
  this.#items.splice(index, 1);
3775
- if (this.#items.length === 0) {
3776
- this.position = void 0;
3777
- this.offset = 0;
3778
- } else if (this.position != null) {
3779
- if (this.position > index) this.position -= 1;
3780
- else if (this.position === index) this.position = Math.min(index, this.#items.length - 1);
3818
+ if (this.#items.length === 0) this.#writeScrollState({
3819
+ position: void 0,
3820
+ offset: 0
3821
+ }, "internal");
3822
+ else if (this.position != null) {
3823
+ if (this.position > index) this.#writeScrollState({ position: this.position - 1 }, "internal");
3824
+ else if (this.position === index) this.#writeScrollState({ position: Math.min(index, this.#items.length - 1) }, "internal");
3781
3825
  }
3782
3826
  emitListStateChange(this, {
3783
3827
  type: "delete-finalize",
@@ -3785,13 +3829,6 @@ var ListState = class {
3785
3829
  });
3786
3830
  }
3787
3831
  /**
3788
- * Sets the current anchor item and pixel offset.
3789
- */
3790
- setAnchor(position, offset = 0) {
3791
- this.position = Number.isFinite(position) ? Math.trunc(position) : void 0;
3792
- this.offset = Number.isFinite(offset) ? offset : 0;
3793
- }
3794
- /**
3795
3832
  * Replaces all items and clears scroll state.
3796
3833
  */
3797
3834
  reset(items = []) {
@@ -3799,18 +3836,37 @@ var ListState = class {
3799
3836
  assertUniqueItemReferences(nextItems);
3800
3837
  this.#items = nextItems;
3801
3838
  this.#pendingDeletes.clear();
3802
- this.offset = 0;
3803
- this.position = void 0;
3839
+ this.#writeScrollState({
3840
+ position: void 0,
3841
+ offset: 0
3842
+ }, "internal");
3804
3843
  emitListStateChange(this, { type: "reset" });
3805
3844
  }
3806
- /** Clears the current scroll anchor while keeping the items. */
3807
- resetScroll() {
3808
- this.offset = 0;
3809
- this.position = void 0;
3810
- }
3811
3845
  /** Applies a relative pixel scroll delta. */
3812
3846
  applyScroll(delta) {
3813
- this.offset += delta;
3847
+ this.#writeScrollState({ offset: this.#offset + delta }, "external");
3848
+ }
3849
+ [WRITE_LIST_SCROLL_STATE](patch, source) {
3850
+ this.#writeScrollState(patch, source);
3851
+ }
3852
+ #writeScrollState(patch, source) {
3853
+ let changed = false;
3854
+ if ("position" in patch) {
3855
+ const nextPosition = normalizePosition(patch.position);
3856
+ if (!Object.is(this.#position, nextPosition)) {
3857
+ this.#position = nextPosition;
3858
+ changed = true;
3859
+ }
3860
+ }
3861
+ if ("offset" in patch) {
3862
+ const nextOffset = normalizeOffset$1(patch.offset ?? 0);
3863
+ if (!Object.is(this.#offset, nextOffset)) {
3864
+ this.#offset = nextOffset;
3865
+ changed = true;
3866
+ }
3867
+ }
3868
+ if (!changed) return;
3869
+ markListScrollMutation(this, source);
3814
3870
  }
3815
3871
  };
3816
3872
  //#endregion
@@ -3874,12 +3930,31 @@ function memoRenderItemBy(keyOf, renderItem, options = {}) {
3874
3930
  });
3875
3931
  }
3876
3932
  //#endregion
3877
- //#region src/renderer/virtualized/base-animation.ts
3933
+ //#region src/renderer/virtualized/frame-session.ts
3934
+ function prepareFrameSession(params) {
3935
+ let solution = params.resolveVisibleWindow(params.now);
3936
+ params.captureVisibleItemSnapshot(solution);
3937
+ const requestSettleRedraw = params.pruneTransitionAnimations(solution.window, params.now);
3938
+ if (requestSettleRedraw) {
3939
+ solution = params.resolveVisibleWindow(params.now);
3940
+ params.captureVisibleItemSnapshot(solution);
3941
+ }
3942
+ return {
3943
+ solution,
3944
+ requestSettleRedraw
3945
+ };
3946
+ }
3947
+ //#endregion
3948
+ //#region src/renderer/virtualized/virtualized-animation.ts
3949
+ const CONTROLLED_STATE_OFFSET_EPSILON = 1e-9;
3878
3950
  function clamp$1(value, min, max) {
3879
3951
  return Math.min(Math.max(value, min), max);
3880
3952
  }
3881
3953
  function sameState(state, position, offset) {
3882
- return Object.is(state.position, position) && Object.is(state.offset, offset);
3954
+ if (!Object.is(state.position, position)) return false;
3955
+ if (Object.is(state.offset, offset)) return true;
3956
+ if (!Number.isFinite(state.offset) || !Number.isFinite(offset)) return false;
3957
+ return Math.abs(state.offset - offset) <= CONTROLLED_STATE_OFFSET_EPSILON;
3883
3958
  }
3884
3959
  function resolveJumpSegmentIndex(anchor, direction, itemCount) {
3885
3960
  if (itemCount <= 0) return;
@@ -3961,148 +4036,178 @@ function getNow() {
3961
4036
  return globalThis.performance?.now() ?? Date.now();
3962
4037
  }
3963
4038
  //#endregion
3964
- //#region src/renderer/virtualized/frame-session.ts
3965
- function prepareFrameSession(params) {
3966
- let solution = params.resolveVisibleWindow(params.now);
3967
- let viewportTranslateY = params.getViewportTranslateY(params.now);
3968
- params.captureVisibleItemSnapshot(solution, viewportTranslateY);
3969
- const requestSettleRedraw = params.pruneTransitionAnimations(solution.window, params.now);
3970
- if (requestSettleRedraw) {
3971
- solution = params.resolveVisibleWindow(params.now);
3972
- viewportTranslateY = params.getViewportTranslateY(params.now);
3973
- params.captureVisibleItemSnapshot(solution, viewportTranslateY);
3974
- }
3975
- return {
3976
- solution,
3977
- viewportTranslateY,
3978
- requestSettleRedraw
3979
- };
3980
- }
3981
- //#endregion
3982
4039
  //#region src/renderer/virtualized/jump-controller.ts
3983
- var JumpController = class {
3984
- #confirmedAutoFollowTop = false;
3985
- #confirmedAutoFollowBottom = false;
3986
- #controlledState;
4040
+ var JumpController = class JumpController {
4041
+ static TRANSITION_SETTLE_SNAP_DURATION = 120;
4042
+ #canAutoFollowTop = false;
4043
+ #canAutoFollowBottom = false;
4044
+ #pendingAutoFollowRecomputeTop = true;
4045
+ #pendingAutoFollowRecomputeBottom = true;
4046
+ #pendingAutoFollowRecomputeReasonTop = "init";
4047
+ #pendingAutoFollowRecomputeReasonBottom = "init";
4048
+ #pendingTransitionSettleReconcile = false;
4049
+ #lastArmedAutoFollowBoundary;
4050
+ #lastObservedRenderedAutoFollowTop = false;
4051
+ #lastObservedRenderedAutoFollowBottom = false;
4052
+ #lastViewportWidth;
4053
+ #lastHandledScrollMutationVersion;
3987
4054
  #jumpAnimation;
3988
- #lastCommittedState;
3989
- #hasPendingListChange = false;
3990
- #pendingBoundaryJumpTop = false;
3991
- #pendingBoundaryJumpBottom = false;
4055
+ #pendingPostJumpBoundary;
4056
+ #pendingPostJumpBoundaryBlocked = false;
3992
4057
  #options;
3993
4058
  constructor(options) {
3994
4059
  this.#options = options;
4060
+ this.#lastHandledScrollMutationVersion = this.#options.readScrollMutation().version;
3995
4061
  }
3996
4062
  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;
4063
+ this.#handlePendingExternalScrollMutation();
4064
+ }
4065
+ noteViewportWidth(width) {
4066
+ if (!Number.isFinite(width)) return;
4067
+ if (this.#lastViewportWidth == null) {
4068
+ this.#lastViewportWidth = width;
4069
+ return;
4070
+ }
4071
+ if (Object.is(this.#lastViewportWidth, width)) return;
4072
+ this.#lastViewportWidth = width;
4073
+ this.#clearPendingPostJumpBoundary();
4074
+ this.#clearPendingTransitionSettleReconcile();
4075
+ this.#markAutoFollowRecompute(void 0, "viewport-width-change");
4000
4076
  }
4001
4077
  prepare(now) {
4078
+ if (this.#handlePendingExternalScrollMutation()) return false;
4002
4079
  const animation = this.#jumpAnimation;
4003
4080
  if (animation == null) return false;
4004
4081
  if (this.#options.getItemCount() === 0) {
4005
4082
  this.#cancelJumpAnimation();
4006
4083
  return false;
4007
4084
  }
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
4085
  const progress = getProgress(animation.startTime, animation.duration, now);
4014
4086
  const eased = progress >= 1 ? 1 : smoothstep(progress);
4015
4087
  const anchor = getAnchorAtDistance(animation.path, animation.path.totalDistance * eased);
4016
4088
  this.#options.applyAnchor(anchor);
4017
4089
  animation.needsMoreFrames = progress < 1;
4090
+ if (!animation.needsMoreFrames && this.#pendingPostJumpBoundary != null && !this.#pendingPostJumpBoundaryBlocked) this.#armAutoFollowBoundary(this.#pendingPostJumpBoundary, "jump-to-boundary-settle");
4018
4091
  return animation.needsMoreFrames;
4019
4092
  }
4020
4093
  finishFrame(requestRedraw) {
4021
4094
  const animation = this.#jumpAnimation;
4022
4095
  if (animation == null) return requestRedraw;
4023
- if (animation.needsMoreFrames) {
4024
- this.#controlledState = this.#options.readListState();
4025
- return true;
4026
- }
4096
+ if (animation.needsMoreFrames) return true;
4097
+ const boundary = this.#pendingPostJumpBoundaryBlocked === true ? void 0 : this.#pendingPostJumpBoundary;
4027
4098
  const onComplete = animation.onComplete;
4028
4099
  this.#cancelJumpAnimation();
4100
+ this.#clearPendingPostJumpBoundary();
4101
+ if (boundary != null) this.#armAutoFollowBoundary(boundary, "jump-to-boundary-settle");
4029
4102
  onComplete?.();
4030
4103
  return requestRedraw || this.#jumpAnimation != null;
4031
4104
  }
4032
4105
  commit(state) {
4033
- this.#lastCommittedState = {
4034
- position: state.position,
4035
- offset: state.offset
4036
- };
4106
+ this.#lastHandledScrollMutationVersion = this.#options.readScrollMutation().version;
4037
4107
  }
4038
4108
  jumpTo(index, options = {}) {
4039
- this.#clearPendingBoundaryJumps();
4109
+ this.#clearPendingTransitionSettleReconcile();
4110
+ this.#clearPendingPostJumpBoundary();
4040
4111
  if (this.#options.getItemCount() === 0) {
4041
4112
  this.#cancelJumpAnimation();
4042
4113
  return;
4043
4114
  }
4044
- this.#startJumpToIndex(index, options, { kind: "manual" });
4115
+ this.#startJumpToIndex(index, options);
4045
4116
  }
4046
4117
  jumpToBoundary(boundary, options = {}) {
4047
- this.#clearPendingBoundaryJumps();
4118
+ this.#clearPendingTransitionSettleReconcile();
4119
+ this.#clearPendingPostJumpBoundary();
4120
+ this.#armAutoFollowBoundary(boundary, "jump-to-boundary");
4048
4121
  if (this.#options.getItemCount() === 0) {
4049
4122
  this.#cancelJumpAnimation();
4050
4123
  return;
4051
4124
  }
4052
- this.#armBoundaryJump(boundary);
4053
4125
  this.#startJumpToIndex(boundary === "bottom" ? this.#options.getItemCount() - 1 : 0, {
4054
4126
  ...options,
4055
4127
  block: boundary === "bottom" ? "end" : "start"
4056
- }, {
4057
- kind: "boundary-jump",
4058
- boundary
4059
4128
  });
4060
4129
  }
4061
- syncAutoFollowCapabilities(capabilities) {
4062
- this.#confirmedAutoFollowTop = capabilities.top;
4063
- this.#confirmedAutoFollowBottom = capabilities.bottom;
4064
- this.#clearPendingBoundaryJumps();
4065
- return this.getEffectiveAutoFollowCapabilities();
4130
+ recomputeAutoFollowCapabilities(capabilities) {
4131
+ const previouslyObservedDualBoundary = this.#lastObservedRenderedAutoFollowTop && this.#lastObservedRenderedAutoFollowBottom;
4132
+ if (capabilities.top && capabilities.bottom && !previouslyObservedDualBoundary) {
4133
+ this.#setAutoFollowBoundary("top", true, "dual-boundary-promotion");
4134
+ this.#setAutoFollowBoundary("bottom", true, "dual-boundary-promotion");
4135
+ }
4136
+ if (this.#pendingAutoFollowRecomputeTop) {
4137
+ this.#setAutoFollowBoundary("top", capabilities.top, `strict-recompute:${this.#pendingAutoFollowRecomputeReasonTop}`);
4138
+ this.#pendingAutoFollowRecomputeTop = false;
4139
+ }
4140
+ if (this.#pendingAutoFollowRecomputeBottom) {
4141
+ this.#setAutoFollowBoundary("bottom", capabilities.bottom, `strict-recompute:${this.#pendingAutoFollowRecomputeReasonBottom}`);
4142
+ this.#pendingAutoFollowRecomputeBottom = false;
4143
+ }
4144
+ this.#syncLastArmedBoundaryFromLatchedState();
4145
+ if (this.#pendingTransitionSettleReconcile) {
4146
+ this.#reconcileLatchedAutoFollowAfterTransitionSettle(capabilities);
4147
+ this.#pendingTransitionSettleReconcile = false;
4148
+ }
4149
+ this.#lastObservedRenderedAutoFollowTop = capabilities.top;
4150
+ this.#lastObservedRenderedAutoFollowBottom = capabilities.bottom;
4151
+ return this.getAutoFollowCapabilities();
4066
4152
  }
4067
- getEffectiveAutoFollowCapabilities() {
4153
+ getAutoFollowCapabilities() {
4068
4154
  return {
4069
- top: this.#hasEffectiveAutoFollowCapability("top"),
4070
- bottom: this.#hasEffectiveAutoFollowCapability("bottom")
4155
+ top: this.#canAutoFollowTop,
4156
+ bottom: this.#canAutoFollowBottom
4071
4157
  };
4072
4158
  }
4159
+ reconcileAutoFollowAfterTransitionSettle() {
4160
+ this.#pendingTransitionSettleReconcile = true;
4161
+ }
4073
4162
  handleListStateChange(change) {
4074
- this.#hasPendingListChange = true;
4163
+ switch (change.type) {
4164
+ case "reset":
4165
+ case "set":
4166
+ this.#cancelJumpAnimation();
4167
+ this.#clearPendingPostJumpBoundary();
4168
+ this.#clearPendingTransitionSettleReconcile();
4169
+ this.#syncScrollMutationVersion();
4170
+ this.#markAutoFollowRecompute(void 0, change.type);
4171
+ return change;
4172
+ case "push":
4173
+ case "unshift": return this.#handleBoundaryInsert(change);
4174
+ default: return change;
4175
+ }
4176
+ }
4177
+ #handleBoundaryInsert(change) {
4178
+ if (this.#handlePendingExternalScrollMutation()) return change;
4179
+ this.#clearPendingTransitionSettleReconcile();
4075
4180
  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
- };
4181
+ const boundary = change.type === "push" ? "bottom" : "top";
4182
+ if (this.#pendingPostJumpBoundary === boundary) this.#pendingPostJumpBoundaryBlocked = true;
4183
+ if (followChange == null || !this.#hasAutoFollowCapability(followChange.boundary)) return change;
4184
+ if (this.#canAutoFollowTop && this.#canAutoFollowBottom && this.#lastObservedRenderedAutoFollowTop && this.#lastObservedRenderedAutoFollowBottom) {
4185
+ const otherBoundary = followChange.boundary === "top" ? "bottom" : "top";
4186
+ this.#setAutoFollowBoundary(otherBoundary, false, "boundary-insert-narrow");
4187
+ this.#lastArmedAutoFollowBoundary = followChange.boundary;
4091
4188
  }
4189
+ this.#clearPendingPostJumpBoundary();
4190
+ this.#materializeAnimatedAnchor(getNow(), followChange.direction, followChange.count);
4191
+ this.#startJumpToIndex(followChange.boundary === "bottom" ? this.#options.getItemCount() - 1 : 0, {
4192
+ block: followChange.boundary === "bottom" ? "end" : "start",
4193
+ duration: followChange.animation?.duration
4194
+ });
4092
4195
  return change;
4093
4196
  }
4094
4197
  #cancelJumpAnimation() {
4095
4198
  this.#jumpAnimation = void 0;
4096
- this.#controlledState = void 0;
4097
4199
  }
4098
- #startJumpToIndex(index, options, source) {
4200
+ #startJumpToIndex(index, options) {
4099
4201
  const targetIndex = this.#options.clampItemIndex(index);
4100
- const currentState = this.#options.normalizeListState(this.#options.readListState());
4101
4202
  const targetBlock = options.block ?? this.#options.getDefaultJumpBlock();
4203
+ const settleBoundary = this.#resolveBoundaryLatchTarget(targetIndex, targetBlock);
4204
+ this.#materializeAnimatedAnchor(getNow());
4205
+ const currentState = this.#options.normalizeListState(this.#options.readListState());
4102
4206
  const targetAnchor = this.#options.getTargetAnchor(targetIndex, targetBlock);
4103
4207
  if (!(options.animated ?? true)) {
4104
4208
  this.#cancelJumpAnimation();
4105
4209
  this.#options.applyAnchor(targetAnchor);
4210
+ if (settleBoundary != null) this.#armAutoFollowBoundary(settleBoundary, "jump-to-boundary-instant");
4106
4211
  options.onComplete?.();
4107
4212
  return;
4108
4213
  }
@@ -4110,6 +4215,7 @@ var JumpController = class {
4110
4215
  if (!Number.isFinite(startAnchor)) {
4111
4216
  this.#cancelJumpAnimation();
4112
4217
  this.#options.applyAnchor(targetAnchor);
4218
+ if (settleBoundary != null) this.#armAutoFollowBoundary(settleBoundary, "jump-to-boundary-instant");
4113
4219
  options.onComplete?.();
4114
4220
  return;
4115
4221
  }
@@ -4118,73 +4224,440 @@ var JumpController = class {
4118
4224
  if (duration <= 0 || path.totalDistance <= Number.EPSILON) {
4119
4225
  this.#cancelJumpAnimation();
4120
4226
  this.#options.applyAnchor(targetAnchor);
4227
+ if (settleBoundary != null) this.#armAutoFollowBoundary(settleBoundary, "jump-to-boundary-instant");
4121
4228
  options.onComplete?.();
4122
4229
  return;
4123
4230
  }
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;
4231
+ if (settleBoundary != null) {
4232
+ this.#pendingPostJumpBoundary = settleBoundary;
4233
+ this.#pendingPostJumpBoundaryBlocked = false;
4234
+ }
4235
+ this.#jumpAnimation = {
4236
+ path,
4237
+ startTime: getNow(),
4238
+ duration,
4239
+ needsMoreFrames: true,
4240
+ onComplete: options.onComplete
4241
+ };
4242
+ }
4243
+ #resolveBoundaryLatchTarget(index, block) {
4244
+ const itemCount = this.#options.getItemCount();
4245
+ if (itemCount <= 0) return;
4246
+ if (index === 0 && block === "start") return "top";
4247
+ if (index === itemCount - 1 && block === "end") return "bottom";
4248
+ }
4249
+ #resolveAutoFollowChange(change) {
4250
+ switch (change.type) {
4251
+ case "push":
4252
+ case "unshift": return change.animation?.autoFollow === true ? {
4253
+ change,
4254
+ boundary: change.type === "push" ? "bottom" : "top",
4255
+ direction: change.type,
4256
+ count: change.count,
4257
+ animation: change.animation
4258
+ } : void 0;
4259
+ default: return;
4260
+ }
4261
+ }
4262
+ #hasAutoFollowCapability(boundary) {
4263
+ return boundary === "top" ? this.#canAutoFollowTop : this.#canAutoFollowBottom;
4264
+ }
4265
+ #armAutoFollowBoundary(boundary, reason) {
4266
+ this.#setAutoFollowBoundary(boundary, true, reason);
4267
+ this.#lastArmedAutoFollowBoundary = boundary;
4268
+ if (boundary === "top") {
4269
+ this.#pendingAutoFollowRecomputeTop = false;
4270
+ return;
4271
+ }
4272
+ this.#pendingAutoFollowRecomputeBottom = false;
4273
+ }
4274
+ #markAutoFollowRecompute(boundary, reason) {
4275
+ if (boundary == null || boundary === "top") {
4276
+ this.#pendingAutoFollowRecomputeTop = true;
4277
+ this.#pendingAutoFollowRecomputeReasonTop = reason;
4278
+ }
4279
+ if (boundary == null || boundary === "bottom") {
4280
+ this.#pendingAutoFollowRecomputeBottom = true;
4281
+ this.#pendingAutoFollowRecomputeReasonBottom = reason;
4282
+ }
4283
+ }
4284
+ #clearPendingPostJumpBoundary() {
4285
+ this.#pendingPostJumpBoundary = void 0;
4286
+ this.#pendingPostJumpBoundaryBlocked = false;
4287
+ }
4288
+ #clearPendingTransitionSettleReconcile() {
4289
+ this.#pendingTransitionSettleReconcile = false;
4290
+ }
4291
+ #materializeAnimatedAnchor(now, direction, count = 0) {
4292
+ const animation = this.#jumpAnimation;
4293
+ if (animation == null) return;
4294
+ const progress = getProgress(animation.startTime, animation.duration, now);
4295
+ const eased = progress >= 1 ? 1 : smoothstep(progress);
4296
+ let anchor = getAnchorAtDistance(animation.path, animation.path.totalDistance * eased);
4297
+ if (direction === "unshift") anchor += count;
4298
+ this.#cancelJumpAnimation();
4299
+ this.#options.applyAnchor(anchor);
4300
+ }
4301
+ #setAutoFollowBoundary(boundary, value, reason) {
4302
+ if (boundary === "top") this.#canAutoFollowTop = value;
4303
+ else this.#canAutoFollowBottom = value;
4304
+ }
4305
+ #syncLastArmedBoundaryFromLatchedState() {
4306
+ if (this.#canAutoFollowTop === this.#canAutoFollowBottom) return;
4307
+ this.#lastArmedAutoFollowBoundary = this.#canAutoFollowTop ? "top" : "bottom";
4308
+ }
4309
+ #reconcileLatchedAutoFollowAfterTransitionSettle(capabilities) {
4310
+ if (!this.#canAutoFollowTop && !this.#canAutoFollowBottom) return;
4311
+ const preferredBoundary = this.#resolvePreferredLatchedBoundary(capabilities);
4312
+ if (preferredBoundary == null) return;
4313
+ const otherBoundary = preferredBoundary === "top" ? "bottom" : "top";
4314
+ if (this.#hasAutoFollowCapability(otherBoundary)) this.#setAutoFollowBoundary(otherBoundary, false, "strict-recompute:set");
4315
+ if (!this.#readCapabilityForBoundary(capabilities, preferredBoundary)) this.#startTransitionSettleSnap(preferredBoundary);
4316
+ this.#syncLastArmedBoundaryFromLatchedState();
4317
+ }
4318
+ #resolvePreferredLatchedBoundary(capabilities) {
4319
+ if (this.#canAutoFollowTop && this.#canAutoFollowBottom) {
4320
+ if (capabilities.top && capabilities.bottom) return;
4321
+ if (this.#lastArmedAutoFollowBoundary != null) return this.#lastArmedAutoFollowBoundary;
4322
+ if (capabilities.top !== capabilities.bottom) return capabilities.top ? "top" : "bottom";
4323
+ return "bottom";
4324
+ }
4325
+ if (this.#canAutoFollowTop) return "top";
4326
+ if (this.#canAutoFollowBottom) return "bottom";
4327
+ }
4328
+ #readCapabilityForBoundary(capabilities, boundary) {
4329
+ return boundary === "top" ? capabilities.top : capabilities.bottom;
4330
+ }
4331
+ #startTransitionSettleSnap(boundary) {
4332
+ if (this.#options.getItemCount() <= 0) return;
4333
+ this.#startJumpToIndex(boundary === "bottom" ? this.#options.getItemCount() - 1 : 0, {
4334
+ block: boundary === "bottom" ? "end" : "start",
4335
+ duration: JumpController.TRANSITION_SETTLE_SNAP_DURATION
4336
+ });
4337
+ }
4338
+ #syncScrollMutationVersion() {
4339
+ this.#lastHandledScrollMutationVersion = this.#options.readScrollMutation().version;
4340
+ }
4341
+ #handlePendingExternalScrollMutation() {
4342
+ const mutation = this.#options.readScrollMutation();
4343
+ if (mutation.version === this.#lastHandledScrollMutationVersion) return false;
4344
+ this.#lastHandledScrollMutationVersion = mutation.version;
4345
+ if (mutation.source !== "external") return false;
4346
+ this.#cancelJumpAnimation();
4347
+ this.#clearPendingPostJumpBoundary();
4348
+ this.#clearPendingTransitionSettleReconcile();
4349
+ this.#markAutoFollowRecompute(void 0, "manual-scroll");
4350
+ return true;
4351
+ }
4352
+ };
4353
+ //#endregion
4354
+ //#region src/renderer/virtualized/solver.ts
4355
+ function clamp(value, min, max) {
4356
+ return Math.min(Math.max(value, min), max);
4357
+ }
4358
+ function normalizeOffset(offset) {
4359
+ return Number.isFinite(offset) ? offset : 0;
4360
+ }
4361
+ function normalizeListPadding(padding) {
4362
+ return {
4363
+ top: typeof padding?.top === "number" && Number.isFinite(padding.top) ? Math.max(0, padding.top) : 0,
4364
+ bottom: typeof padding?.bottom === "number" && Number.isFinite(padding.bottom) ? Math.max(0, padding.bottom) : 0
4365
+ };
4366
+ }
4367
+ function resolveListViewport(outerHeight, padding) {
4368
+ const height = typeof outerHeight === "number" && Number.isFinite(outerHeight) ? Math.max(0, outerHeight) : 0;
4369
+ const resolvedPadding = normalizeListPadding(padding);
4370
+ const contentTop = resolvedPadding.top;
4371
+ const contentBottom = Math.max(contentTop, height - resolvedPadding.bottom);
4372
+ return {
4373
+ outerHeight: height,
4374
+ contentTop,
4375
+ contentBottom,
4376
+ contentHeight: contentBottom - contentTop,
4377
+ outerContentTop: -contentTop,
4378
+ outerContentBottom: height - contentTop
4379
+ };
4380
+ }
4381
+ function resolveListLayoutOptions(options = {}) {
4382
+ return {
4383
+ anchorMode: options.anchorMode ?? "top",
4384
+ underflowAlign: options.underflowAlign ?? "top",
4385
+ padding: normalizeListPadding(options.padding)
4386
+ };
4387
+ }
4388
+ function normalizeVisibleState(itemCount, state, layout) {
4389
+ if (itemCount <= 0) return {
4390
+ position: 0,
4391
+ offset: 0
4392
+ };
4393
+ const position = state.position;
4394
+ const fallbackPosition = layout.anchorMode === "top" ? 0 : itemCount - 1;
4395
+ if (typeof position !== "number" || !Number.isFinite(position)) return {
4396
+ position: fallbackPosition,
4397
+ offset: normalizeOffset(state.offset)
4398
+ };
4399
+ return {
4400
+ position: clamp(Math.trunc(position), 0, itemCount - 1),
4401
+ offset: normalizeOffset(state.offset)
4402
+ };
4403
+ }
4404
+ function resolveVisibleWindow(items, state, viewportHeight, resolveItem, layout) {
4405
+ const viewport = typeof viewportHeight === "number" ? resolveListViewport(viewportHeight, layout.padding) : viewportHeight;
4406
+ const contentHeight = viewport.contentHeight;
4407
+ const normalizedState = normalizeVisibleState(items.length, state, layout);
4408
+ const resolutionPath = /* @__PURE__ */ new Set();
4409
+ const readResolvedItem = (item, idx) => {
4410
+ resolutionPath.add(idx);
4411
+ return resolveItem(item, idx);
4412
+ };
4413
+ if (items.length === 0) return {
4414
+ normalizedState,
4415
+ resolutionPath: [],
4416
+ window: {
4417
+ drawList: [],
4418
+ shift: 0
4419
+ }
4420
+ };
4421
+ if (layout.anchorMode === "top") {
4422
+ let { position, offset } = normalizedState;
4423
+ let drawLength = 0;
4424
+ if (offset > 0) if (position === 0) offset = 0;
4425
+ else {
4426
+ for (let i = position - 1; i >= 0; i -= 1) {
4427
+ const { height } = readResolvedItem(items[i], i);
4428
+ position = i;
4429
+ offset -= height;
4430
+ if (offset <= 0) break;
4431
+ }
4432
+ if (position === 0 && offset > 0) offset = 0;
4433
+ }
4434
+ let y = offset;
4435
+ const drawList = [];
4436
+ for (let i = position; i < items.length; i += 1) {
4437
+ const { value, height } = readResolvedItem(items[i], i);
4438
+ if (y + height > 0) {
4439
+ drawList.push({
4440
+ index: i,
4441
+ value,
4442
+ offset: y,
4443
+ height
4444
+ });
4445
+ drawLength += height;
4446
+ } else {
4447
+ offset += height;
4448
+ position = i + 1;
4449
+ }
4450
+ y += height;
4451
+ if (y >= contentHeight) break;
4452
+ }
4453
+ let shift = 0;
4454
+ if (y < contentHeight) {
4455
+ if (drawList.length > 0 && drawList.at(-1)?.index === items.length - 1 && !(drawList.at(-1)?.height > Number.EPSILON)) return finalizeVisibleWindowResult(items.length, viewport, layout, {
4456
+ position,
4457
+ offset
4458
+ }, Array.from(resolutionPath), extendVisibleWindowToOuterBounds(items, {
4459
+ drawList,
4460
+ shift
4461
+ }, viewport, readResolvedItem));
4462
+ if (position === 0 && drawLength < contentHeight) {
4463
+ shift = -offset;
4464
+ offset = 0;
4465
+ } else {
4466
+ shift = contentHeight - y;
4467
+ y = offset += shift;
4468
+ let lastIdx = -1;
4469
+ for (let i = position - 1; i >= 0; i -= 1) {
4470
+ const { value, height } = readResolvedItem(items[i], i);
4471
+ drawLength += height;
4472
+ y -= height;
4473
+ drawList.push({
4474
+ index: i,
4475
+ value,
4476
+ offset: y - shift,
4477
+ height
4478
+ });
4479
+ lastIdx = i;
4480
+ if (y < 0) break;
4481
+ }
4482
+ if (lastIdx === 0 && drawLength < contentHeight) {
4483
+ shift = drawList.at(-1)?.offset == null ? 0 : -drawList.at(-1).offset;
4484
+ position = 0;
4485
+ offset = 0;
4486
+ }
4487
+ }
4488
+ }
4489
+ return finalizeVisibleWindowResult(items.length, viewport, layout, {
4490
+ position,
4491
+ offset
4492
+ }, Array.from(resolutionPath), extendVisibleWindowToOuterBounds(items, {
4493
+ drawList,
4494
+ shift
4495
+ }, viewport, readResolvedItem));
4496
+ }
4497
+ let { position, offset } = normalizedState;
4498
+ let drawLength = 0;
4499
+ if (offset < 0) if (position === items.length - 1) offset = 0;
4500
+ else for (let i = position + 1; i < items.length; i += 1) {
4501
+ const { height } = readResolvedItem(items[i], i);
4502
+ position = i;
4503
+ offset += height;
4504
+ if (offset > 0) break;
4505
+ }
4506
+ let y = contentHeight + offset;
4507
+ const drawList = [];
4508
+ for (let i = position; i >= 0; i -= 1) {
4509
+ const { value, height } = readResolvedItem(items[i], i);
4510
+ y -= height;
4511
+ if (y <= contentHeight) {
4512
+ drawList.push({
4513
+ index: i,
4514
+ value,
4515
+ offset: y,
4516
+ height
4517
+ });
4518
+ drawLength += height;
4519
+ } else {
4520
+ offset -= height;
4521
+ position = i - 1;
4522
+ }
4523
+ if (y < 0) break;
4524
+ }
4525
+ let shift = 0;
4526
+ if (y > 0) {
4527
+ shift = -y;
4528
+ if (drawLength < contentHeight) {
4529
+ y = drawLength;
4530
+ for (let i = position + 1; i < items.length; i += 1) {
4531
+ const { value, height } = readResolvedItem(items[i], i);
4532
+ drawList.push({
4533
+ index: i,
4534
+ value,
4535
+ offset: y - shift,
4536
+ height
4537
+ });
4538
+ y = drawLength += height;
4539
+ if (height > Number.EPSILON) position = i;
4540
+ if (y >= contentHeight) break;
4541
+ }
4542
+ offset = drawLength < contentHeight ? 0 : drawLength - contentHeight;
4543
+ } else offset = drawLength - contentHeight;
4544
+ }
4545
+ return finalizeVisibleWindowResult(items.length, viewport, layout, {
4546
+ position,
4547
+ offset
4548
+ }, Array.from(resolutionPath), extendVisibleWindowToOuterBounds(items, {
4549
+ drawList,
4550
+ shift
4551
+ }, viewport, readResolvedItem));
4552
+ }
4553
+ function finalizeVisibleWindowResult(itemCount, viewport, layout, normalizedState, resolutionPath, window) {
4554
+ const viewportHeight = viewport.contentHeight;
4555
+ if (window.drawList.length !== itemCount || itemCount <= 0) return {
4556
+ normalizedState,
4557
+ resolutionPath,
4558
+ window
4559
+ };
4560
+ let minIndex = Number.POSITIVE_INFINITY;
4561
+ let maxIndex = Number.NEGATIVE_INFINITY;
4562
+ let minOffset = Number.POSITIVE_INFINITY;
4563
+ let maxBottom = Number.NEGATIVE_INFINITY;
4564
+ let hasDeferredSlots = false;
4565
+ for (const entry of window.drawList) {
4566
+ if (!(entry.height > Number.EPSILON)) hasDeferredSlots = true;
4567
+ else {
4568
+ minOffset = Math.min(minOffset, entry.offset);
4569
+ maxBottom = Math.max(maxBottom, entry.offset + entry.height);
4570
+ }
4571
+ minIndex = Math.min(minIndex, entry.index);
4572
+ maxIndex = Math.max(maxIndex, entry.index);
4573
+ }
4574
+ if (!Number.isFinite(minOffset) || !Number.isFinite(maxBottom)) return {
4575
+ normalizedState,
4576
+ resolutionPath,
4577
+ window
4578
+ };
4579
+ const contentHeight = maxBottom - minOffset;
4580
+ if (minIndex !== 0 || maxIndex !== itemCount - 1 || !(contentHeight < viewportHeight - Number.EPSILON)) return {
4581
+ normalizedState,
4582
+ resolutionPath,
4583
+ window
4584
+ };
4585
+ const desiredTop = layout.underflowAlign === "bottom" ? viewportHeight - contentHeight : 0;
4586
+ return {
4587
+ normalizedState: hasDeferredSlots ? normalizedState : layout.anchorMode === "top" ? {
4588
+ position: 0,
4589
+ offset: 0
4590
+ } : {
4591
+ position: itemCount - 1,
4592
+ offset: 0
4593
+ },
4594
+ resolutionPath,
4595
+ window: {
4596
+ drawList: window.drawList,
4597
+ shift: desiredTop - minOffset
4598
+ }
4599
+ };
4600
+ }
4601
+ function extendVisibleWindowToOuterBounds(items, window, viewport, resolveItem) {
4602
+ if (window.drawList.length === 0 || items.length === 0) return window;
4603
+ const drawList = [...window.drawList];
4604
+ const existingIndices = new Set(drawList.map((entry) => entry.index));
4605
+ let topEntry = drawList[0];
4606
+ let bottomEntry = drawList[0];
4607
+ for (const entry of drawList) {
4608
+ if (entry.offset < topEntry.offset) topEntry = entry;
4609
+ if (entry.offset + entry.height > bottomEntry.offset + bottomEntry.height) bottomEntry = entry;
4610
+ }
4611
+ let topIdx = topEntry.index;
4612
+ let topY = topEntry.offset + window.shift;
4613
+ while (topIdx > 0) {
4614
+ const prevIdx = topIdx - 1;
4615
+ if (existingIndices.has(prevIdx)) {
4616
+ const existing = drawList.find((entry) => entry.index === prevIdx);
4617
+ topIdx = prevIdx;
4618
+ if (existing != null) topY = existing.offset + window.shift;
4619
+ continue;
4620
+ }
4621
+ const { value, height } = resolveItem(items[prevIdx], prevIdx);
4622
+ const prevY = topY - height;
4623
+ if (prevY + height <= viewport.outerContentTop) break;
4624
+ drawList.push({
4625
+ index: prevIdx,
4626
+ value,
4627
+ offset: prevY - window.shift,
4628
+ height
4629
+ });
4630
+ existingIndices.add(prevIdx);
4631
+ topIdx = prevIdx;
4632
+ topY = prevY;
4633
+ }
4634
+ let bottomIdx = bottomEntry.index;
4635
+ let bottomY = bottomEntry.offset + window.shift + bottomEntry.height;
4636
+ while (bottomIdx < items.length - 1) {
4637
+ const nextIdx = bottomIdx + 1;
4638
+ if (existingIndices.has(nextIdx)) {
4639
+ const existing = drawList.find((entry) => entry.index === nextIdx);
4640
+ bottomIdx = nextIdx;
4641
+ if (existing != null) bottomY = Math.max(bottomY, existing.offset + window.shift + existing.height);
4642
+ continue;
4145
4643
  }
4644
+ const { value, height } = resolveItem(items[nextIdx], nextIdx);
4645
+ if (bottomY >= viewport.outerContentBottom) break;
4646
+ drawList.push({
4647
+ index: nextIdx,
4648
+ value,
4649
+ offset: bottomY - window.shift,
4650
+ height
4651
+ });
4652
+ existingIndices.add(nextIdx);
4653
+ bottomIdx = nextIdx;
4654
+ bottomY += height;
4146
4655
  }
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
- };
4656
+ return {
4657
+ drawList,
4658
+ shift: window.shift
4659
+ };
4660
+ }
4188
4661
  //#endregion
4189
4662
  //#region src/renderer/virtualized/transition-snapshot.ts
4190
4663
  var VisibilitySnapshot = class {
@@ -4196,11 +4669,8 @@ var VisibilitySnapshot = class {
4196
4669
  #previousSnapshotState;
4197
4670
  #emptyState;
4198
4671
  #coversShortList = false;
4199
- #topGap = 0;
4200
- #bottomGap = 0;
4201
4672
  #atStartBoundary = false;
4202
4673
  #atEndBoundary = false;
4203
- #currentExtraShift = 0;
4204
4674
  #minDrawnIndex = Number.POSITIVE_INFINITY;
4205
4675
  #maxDrawnIndex = Number.NEGATIVE_INFINITY;
4206
4676
  #topBoundaryItem;
@@ -4208,18 +4678,12 @@ var VisibilitySnapshot = class {
4208
4678
  get coversShortList() {
4209
4679
  return this.#hasSnapshot && this.#snapshotState != null && this.#coversShortList;
4210
4680
  }
4211
- get topGap() {
4212
- return this.#topGap;
4213
- }
4214
- get bottomGap() {
4215
- return this.#bottomGap;
4681
+ get hasSnapshot() {
4682
+ return this.#hasSnapshot;
4216
4683
  }
4217
4684
  get previousState() {
4218
4685
  return this.#previousSnapshotState;
4219
4686
  }
4220
- get currentExtraShift() {
4221
- return this.#currentExtraShift;
4222
- }
4223
4687
  readDrawnIndexRange() {
4224
4688
  if (!Number.isFinite(this.#minDrawnIndex) || !Number.isFinite(this.#maxDrawnIndex)) return;
4225
4689
  return {
@@ -4230,7 +4694,7 @@ var VisibilitySnapshot = class {
4230
4694
  readBoundaryItem(boundary) {
4231
4695
  return boundary === "top" ? this.#topBoundaryItem : this.#bottomBoundaryItem;
4232
4696
  }
4233
- capture(window, _resolutionPath, items, viewportHeight, snapshotState, extraShift, readVisibleRange) {
4697
+ capture(window, _resolutionPath, items, viewport, snapshotState, readVisibleRange, readOuterVisibleRange) {
4234
4698
  this.#previousVisibleItems = this.#visibleItems;
4235
4699
  this.#previousSnapshotState = this.#snapshotState;
4236
4700
  const nextDrawnItems = /* @__PURE__ */ new Set();
@@ -4245,18 +4709,23 @@ var VisibilitySnapshot = class {
4245
4709
  let nextBottomBoundaryItem;
4246
4710
  let nextTopBoundaryY = Number.POSITIVE_INFINITY;
4247
4711
  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;
4712
+ const effectiveShift = window.shift;
4713
+ const contentOriginY = viewport.contentTop;
4714
+ for (const { index, offset, height } of window.drawList) {
4715
+ const y = offset + effectiveShift + contentOriginY;
4255
4716
  topMostY = Math.min(topMostY, y);
4256
4717
  bottomMostY = Math.max(bottomMostY, y + height);
4257
- const item = items[idx];
4258
- if (item != null) {
4718
+ const item = items[index];
4719
+ if (item != null && readOuterVisibleRange(y, height) != null) {
4259
4720
  nextDrawnItems.add(item);
4721
+ nextMinDrawnIndex = Math.min(nextMinDrawnIndex, index);
4722
+ nextMaxDrawnIndex = Math.max(nextMaxDrawnIndex, index);
4723
+ }
4724
+ if (item == null) continue;
4725
+ if (readVisibleRange(y, height) != null) {
4726
+ minVisibleIndex = Math.min(minVisibleIndex, index);
4727
+ maxVisibleIndex = Math.max(maxVisibleIndex, index);
4728
+ nextVisibleItems.add(item);
4260
4729
  if (y < nextTopBoundaryY) {
4261
4730
  nextTopBoundaryY = y;
4262
4731
  nextTopBoundaryItem = item;
@@ -4266,25 +4735,20 @@ var VisibilitySnapshot = class {
4266
4735
  nextBottomBoundaryItem = item;
4267
4736
  }
4268
4737
  }
4269
- if (item == null || readVisibleRange(y, height) == null) continue;
4270
- nextVisibleItems.add(item);
4271
4738
  }
4272
4739
  this.#drawnItems = nextDrawnItems;
4273
4740
  this.#visibleItems = nextVisibleItems;
4274
4741
  this.#hasSnapshot = true;
4275
4742
  this.#snapshotState = snapshotState;
4276
- this.#currentExtraShift = extraShift;
4277
4743
  this.#minDrawnIndex = nextMinDrawnIndex;
4278
4744
  this.#maxDrawnIndex = nextMaxDrawnIndex;
4279
4745
  this.#topBoundaryItem = nextTopBoundaryItem;
4280
4746
  this.#bottomBoundaryItem = nextBottomBoundaryItem;
4281
4747
  this.#emptyState = items.length === 0 && window.drawList.length === 0 ? snapshotState : void 0;
4282
4748
  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;
4749
+ this.#coversShortList = window.drawList.length > 0 && items.length > 0 && window.drawList.length === items.length && minVisibleIndex === 0 && maxVisibleIndex === items.length - 1 && topMostY >= viewport.contentTop - 1e-6 && bottomMostY <= viewport.contentBottom + 1e-6 && contentHeight < viewport.contentHeight - 1e-6;
4750
+ this.#atStartBoundary = window.drawList.length > 0 && items.length > 0 && minVisibleIndex === 0 && topMostY >= viewport.contentTop - 1e-6;
4751
+ this.#atEndBoundary = window.drawList.length > 0 && items.length > 0 && maxVisibleIndex === items.length - 1 && bottomMostY <= viewport.contentBottom + 1e-6;
4288
4752
  }
4289
4753
  matchesCurrentState(position, offset) {
4290
4754
  return this.#hasSnapshot && this.#snapshotState != null && sameState(this.#snapshotState, position, offset);
@@ -4324,11 +4788,8 @@ var VisibilitySnapshot = class {
4324
4788
  this.#previousSnapshotState = void 0;
4325
4789
  this.#emptyState = void 0;
4326
4790
  this.#coversShortList = false;
4327
- this.#topGap = 0;
4328
- this.#bottomGap = 0;
4329
4791
  this.#atStartBoundary = false;
4330
4792
  this.#atEndBoundary = false;
4331
- this.#currentExtraShift = 0;
4332
4793
  this.#minDrawnIndex = Number.POSITIVE_INFINITY;
4333
4794
  this.#maxDrawnIndex = Number.NEGATIVE_INFINITY;
4334
4795
  this.#topBoundaryItem = void 0;
@@ -4381,7 +4842,7 @@ var TransitionStore = class {
4381
4842
  }));
4382
4843
  }
4383
4844
  findInvisible(snapshot) {
4384
- return [...this.#transitions.entries()].filter(([item, transition]) => !snapshot.tracks(item, transition.retention)).map(([item, transition]) => ({
4845
+ return [...this.#transitions.entries()].filter(([item, transition]) => !snapshot.tracks(item, transition.retention) && !(transition.kind === "insert" && !snapshot.wasVisible(item))).map(([item, transition]) => ({
4385
4846
  item,
4386
4847
  transition
4387
4848
  }));
@@ -4395,9 +4856,6 @@ var TransitionStore = class {
4395
4856
  };
4396
4857
  //#endregion
4397
4858
  //#region src/renderer/virtualized/transition-planner.ts
4398
- function isFinitePositive(value) {
4399
- return Number.isFinite(value) && value > 0;
4400
- }
4401
4859
  function normalizeDuration(duration) {
4402
4860
  return Math.max(0, typeof duration === "number" && Number.isFinite(duration) ? duration : 0);
4403
4861
  }
@@ -4420,7 +4878,7 @@ function findVisibleEntry(index, resolveVisibleWindow, readVisibleRange) {
4420
4878
  if (index < 0) return;
4421
4879
  const solution = resolveVisibleWindow();
4422
4880
  for (const entry of solution.window.drawList) {
4423
- if (entry.idx !== index) continue;
4881
+ if (entry.index !== index) continue;
4424
4882
  if (readVisibleRange(entry.offset + solution.window.shift, entry.height) != null) return entry;
4425
4883
  }
4426
4884
  }
@@ -4429,13 +4887,16 @@ function isIndexVisible(index, resolveVisibleWindow, readVisibleRange) {
4429
4887
  }
4430
4888
  function resolveAnimationEligibility(params) {
4431
4889
  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);
4890
+ if (params.snapshot.matchesCurrentState(params.position, params.offset)) return params.snapshot.tracks(params.item, "drawn");
4891
+ return isIndexVisible(params.index, params.resolveVisibleWindow, params.readOuterVisibleRange);
4434
4892
  }
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";
4893
+ function hasVisibleBoundaryInsertItems(direction, count, ctx) {
4894
+ if (count <= 0) return false;
4895
+ const start = direction === "push" ? ctx.items.length - count : 0;
4896
+ const end = direction === "push" ? ctx.items.length : Math.min(count, ctx.items.length);
4897
+ if (start < 0 || end <= start) return false;
4898
+ const solution = ctx.resolveVisibleWindow();
4899
+ return solution.window.drawList.some((entry) => entry.index >= start && entry.index < end && ctx.readOuterVisibleRange(entry.offset + solution.window.shift, entry.height) != null);
4439
4900
  }
4440
4901
  function sampleScalarAnimation(animation, now) {
4441
4902
  return interpolate(animation.from, animation.to, animation.startTime, animation.duration, now);
@@ -4468,68 +4929,31 @@ function planExistingItemTransition(params) {
4468
4929
  kind: "update",
4469
4930
  layers,
4470
4931
  height: createScalarAnimation(params.currentVisualState.height, params.nextHeight, params.now, params.duration),
4471
- retention: "visible"
4932
+ retention: "drawn"
4472
4933
  };
4473
4934
  }
4474
4935
  return {
4475
4936
  kind: "delete",
4476
4937
  layers,
4477
4938
  height: createScalarAnimation(params.currentVisualState.height, 0, params.now, params.duration),
4478
- retention: "visible"
4939
+ retention: "drawn"
4479
4940
  };
4480
4941
  }
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
4942
  function planBoundaryInsertItems(params) {
4493
4943
  const entries = [];
4494
- const signedDistance = params.direction === "push" ? 1 : -1;
4495
4944
  for (const { item, node, height } of params.measuredItems) {
4496
4945
  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
4946
  entries.push({
4499
4947
  item,
4500
4948
  transition: {
4501
4949
  kind: "insert",
4502
- layers: [createLayerAnimation(node, 0, 1, params.now, params.duration, signedDistance * resolvedDistance, 0)],
4503
- height: createScalarAnimation(height, height, params.now, params.duration),
4950
+ layers: [createLayerAnimation(node, 0, 1, params.now, params.duration, 0, 0)],
4951
+ height: createScalarAnimation(params.animateHeight ? 0 : height, height, params.now, params.duration),
4504
4952
  retention: "drawn"
4505
4953
  }
4506
4954
  });
4507
4955
  }
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
- };
4956
+ return entries.length === 0 ? void 0 : { entries };
4533
4957
  }
4534
4958
  function measureBoundaryInsertItems(direction, count, ctx) {
4535
4959
  const start = direction === "push" ? ctx.items.length - count : 0;
@@ -4557,6 +4981,11 @@ function drawSampledLayers(sampled, y, adapter) {
4557
4981
  if (alpha <= .001) continue;
4558
4982
  adapter.graphics.save();
4559
4983
  try {
4984
+ if (sampled.kind === "insert") {
4985
+ adapter.graphics.beginPath();
4986
+ adapter.graphics.rect(0, y, adapter.graphics.canvas.clientWidth, sampled.slotHeight);
4987
+ adapter.graphics.clip();
4988
+ }
4560
4989
  if (typeof adapter.graphics.globalAlpha === "number") adapter.graphics.globalAlpha *= alpha;
4561
4990
  if (adapter.drawNode(layer.node, 0, y + layer.translateY)) result = true;
4562
4991
  } finally {
@@ -4580,7 +5009,7 @@ function planUpdateTransition(prevItem, nextItem, duration, now, currentVisualSt
4580
5009
  snapshot,
4581
5010
  hasActiveTransition: store.has(prevItem),
4582
5011
  resolveVisibleWindow: ctx.resolveVisibleWindow,
4583
- readVisibleRange: ctx.readVisibleRange
5012
+ readOuterVisibleRange: ctx.readOuterVisibleRange
4584
5013
  }),
4585
5014
  now,
4586
5015
  currentVisualState,
@@ -4601,27 +5030,26 @@ function planDeleteTransition(item, duration, now, currentVisualState, ctx, snap
4601
5030
  snapshot,
4602
5031
  hasActiveTransition: store.has(item),
4603
5032
  resolveVisibleWindow: ctx.resolveVisibleWindow,
4604
- readVisibleRange: ctx.readVisibleRange
5033
+ readOuterVisibleRange: ctx.readOuterVisibleRange
4605
5034
  }),
4606
5035
  now,
4607
5036
  currentVisualState
4608
5037
  });
4609
5038
  }
4610
- function planBoundaryInsertTransition(direction, count, duration, distance, now, currentTranslateY, ctx, snapshot) {
5039
+ function planBoundaryInsertTransition(direction, count, duration, now, ctx, snapshot) {
4611
5040
  const normalizedDuration = normalizeDuration(duration);
4612
5041
  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;
5042
+ const matchesBoundaryState = snapshot.matchesBoundaryInsertState(direction, count, ctx.position, ctx.offset);
5043
+ const matchesFollowState = snapshot.matchesFollowBoundaryInsertState(direction, count, ctx.position, ctx.offset);
5044
+ const matchesEmptyState = snapshot.matchesEmptyBoundaryInsertState(direction, count, ctx.position, ctx.offset);
5045
+ if (!(matchesBoundaryState || matchesFollowState || matchesEmptyState || snapshot.hasSnapshot && hasVisibleBoundaryInsertItems(direction, count, ctx))) return;
5046
+ const animateHeight = !(direction === "unshift" && matchesFollowState && !matchesBoundaryState && !matchesEmptyState);
4615
5047
  const measuredItems = measureBoundaryInsertItems(direction, count, ctx);
4616
5048
  if (measuredItems == null) return;
4617
- return planBoundaryInsert({
4618
- direction,
5049
+ return planBoundaryInsertItems({
4619
5050
  duration: normalizedDuration,
4620
- distance,
5051
+ animateHeight,
4621
5052
  now,
4622
- strategy,
4623
- snapshot,
4624
- currentTranslateY,
4625
5053
  measuredItems
4626
5054
  });
4627
5055
  }
@@ -4674,7 +5102,7 @@ function readCurrentVisualState(item, now, store, adapter) {
4674
5102
  translateY: 0
4675
5103
  };
4676
5104
  }
4677
- function handleTransitionStateChange(store, snapshot, currentViewportTranslateY, change, ctx, lifecycle) {
5105
+ function handleTransitionStateChange(store, snapshot, change, ctx, lifecycle) {
4678
5106
  switch (change.type) {
4679
5107
  case "update": {
4680
5108
  const now = getNow();
@@ -4682,10 +5110,10 @@ function handleTransitionStateChange(store, snapshot, currentViewportTranslateY,
4682
5110
  const transition = planUpdateTransition(change.prevItem, change.nextItem, change.animation?.duration, now, currentVisualState, ctx, snapshot, store);
4683
5111
  if (transition == null) {
4684
5112
  store.delete(change.prevItem);
4685
- return {};
5113
+ return;
4686
5114
  }
4687
5115
  store.replace(change.prevItem, change.nextItem, transition);
4688
- return {};
5116
+ return;
4689
5117
  }
4690
5118
  case "delete": {
4691
5119
  const now = getNow();
@@ -4694,32 +5122,36 @@ function handleTransitionStateChange(store, snapshot, currentViewportTranslateY,
4694
5122
  if (transition == null) {
4695
5123
  store.delete(change.item);
4696
5124
  lifecycle.onDeleteComplete(change.item);
4697
- return {};
5125
+ return;
4698
5126
  }
4699
5127
  store.set(change.item, transition);
4700
- return {};
5128
+ return;
4701
5129
  }
4702
5130
  case "delete-finalize":
4703
5131
  store.delete(change.item);
4704
- return {};
5132
+ return;
4705
5133
  case "unshift":
4706
5134
  case "push": {
4707
5135
  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 };
5136
+ const plan = planBoundaryInsertTransition(change.type, change.count, change.animation?.duration, now, ctx, snapshot);
5137
+ if (plan == null) return;
4711
5138
  for (const entry of plan.entries) store.set(entry.item, entry.transition);
4712
- return {};
5139
+ if (ctx.position == null && snapshot.coversShortList && (change.type === "push" && ctx.anchorMode === "bottom" || change.type === "unshift" && ctx.anchorMode === "top")) {
5140
+ const boundary = change.type === "push" ? "bottom" : "top";
5141
+ const boundaryItem = snapshot.readBoundaryItem(boundary);
5142
+ if (boundaryItem != null) lifecycle.snapItemToViewportBoundary(boundaryItem, boundary);
5143
+ }
5144
+ return;
4713
5145
  }
4714
5146
  case "reset":
4715
5147
  case "set":
4716
5148
  store.reset();
4717
5149
  snapshot.reset();
4718
- return {};
5150
+ return;
4719
5151
  }
4720
5152
  }
4721
5153
  //#endregion
4722
- //#region src/renderer/virtualized/base-transition.ts
5154
+ //#region src/renderer/virtualized/transition-controller.ts
4723
5155
  function remapAnchorAfterDeletes(anchor, deletedIndices) {
4724
5156
  if (!Number.isFinite(anchor) || deletedIndices.length === 0) return anchor;
4725
5157
  const sortedIndices = [...deletedIndices].filter((index) => Number.isFinite(index) && index >= 0).sort((a, b) => a - b);
@@ -4736,22 +5168,15 @@ function remapAnchorAfterDeletes(anchor, deletedIndices) {
4736
5168
  var TransitionController = class {
4737
5169
  #store = new TransitionStore();
4738
5170
  #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);
5171
+ captureVisibilitySnapshot(window, resolutionPath, items, viewport, snapshotState, readVisibleRange, readOuterVisibleRange) {
5172
+ this.#snapshot.capture(window, resolutionPath, items, viewport, snapshotState, readVisibleRange, readOuterVisibleRange);
4742
5173
  }
4743
5174
  pruneInvisible(ctx, lifecycle) {
4744
5175
  return this.pruneInvisibleAt(getNow(), ctx, lifecycle);
4745
5176
  }
4746
5177
  prepare(now, lifecycle) {
4747
5178
  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);
5179
+ return this.#store.prepare(now);
4755
5180
  }
4756
5181
  canAutoFollowBoundaryInsert(direction, count, position, offset) {
4757
5182
  return this.#snapshot.matchesFollowBoundaryInsertState(direction, count, position, offset);
@@ -4765,17 +5190,10 @@ var TransitionController = class {
4765
5190
  handleListStateChange(change, ctx, lifecycle) {
4766
5191
  const now = getNow();
4767
5192
  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;
5193
+ handleTransitionStateChange(this.#store, this.#snapshot, change, ctx, lifecycle);
4774
5194
  }
4775
5195
  settle(now, lifecycle) {
4776
- const changed = this.#settleTransitions(this.#store.findCompleted(now), now, lifecycle);
4777
- this.#cleanupViewportTranslateAnimation(now);
4778
- return changed;
5196
+ return this.#settleTransitions(this.#store.findCompleted(now), now, lifecycle);
4779
5197
  }
4780
5198
  pruneInvisibleAt(now, ctx, lifecycle) {
4781
5199
  const removals = this.#store.findInvisible(this.#snapshot);
@@ -4784,16 +5202,11 @@ var TransitionController = class {
4784
5202
  reset() {
4785
5203
  this.#store.reset();
4786
5204
  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
5205
  }
4794
5206
  #settleTransitions(removals, now, lifecycle, boundarySnap) {
4795
5207
  if (removals.length === 0) return false;
4796
5208
  const anchor = lifecycle.captureVisualAnchor(now);
5209
+ const beforeState = lifecycle.readScrollState();
4797
5210
  const completedDeleteIndices = [];
4798
5211
  for (const { item, transition } of removals) {
4799
5212
  if (transition.kind === "delete") {
@@ -4805,6 +5218,8 @@ var TransitionController = class {
4805
5218
  }
4806
5219
  if (anchor != null && Number.isFinite(anchor)) lifecycle.restoreVisualAnchor(remapAnchorAfterDeletes(anchor, completedDeleteIndices));
4807
5220
  if (boundarySnap != null) lifecycle.snapItemToViewportBoundary(boundarySnap.item, boundarySnap.boundary);
5221
+ const afterState = lifecycle.readScrollState();
5222
+ if (!sameState(beforeState, afterState.position, afterState.offset)) lifecycle.onTransitionSettleScrollAdjusted();
4808
5223
  return true;
4809
5224
  }
4810
5225
  #resolveNaturalBoundarySnap(removals, now, ctx, lifecycle) {
@@ -4816,7 +5231,7 @@ var TransitionController = class {
4816
5231
  if (transition.kind !== "update" && transition.kind !== "delete") continue;
4817
5232
  const index = lifecycle.readItemIndex(item);
4818
5233
  if (index < 0 || !this.#snapshot.wasVisible(item)) return;
4819
- if (this.#isTransitionVisibleInState(index, previousState, now, this.#snapshot.currentExtraShift, ctx)) return;
5234
+ if (this.#isTransitionVisibleInState(index, previousState, now, ctx)) return;
4820
5235
  naturalIndices.push(index);
4821
5236
  }
4822
5237
  if (naturalIndices.length === 0) return;
@@ -4835,11 +5250,11 @@ var TransitionController = class {
4835
5250
  };
4836
5251
  }
4837
5252
  }
4838
- #isTransitionVisibleInState(index, state, now, extraShift, ctx) {
5253
+ #isTransitionVisibleInState(index, state, now, ctx) {
4839
5254
  const solution = ctx.resolveVisibleWindowForState(state, now);
4840
5255
  for (const entry of solution.window.drawList) {
4841
- if (entry.idx !== index) continue;
4842
- return ctx.readVisibleRange(entry.offset + solution.window.shift + extraShift, entry.height) != null;
5256
+ if (entry.index !== index) continue;
5257
+ return ctx.readOuterVisibleRange(entry.offset + solution.window.shift, entry.height) != null;
4843
5258
  }
4844
5259
  return false;
4845
5260
  }
@@ -4863,6 +5278,7 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
4863
5278
  jumpDurationPerPixel: VirtualizedRenderer.JUMP_DURATION_PER_PIXEL,
4864
5279
  getItemCount: () => this.items.length,
4865
5280
  readListState: this._readListState.bind(this),
5281
+ readScrollMutation: () => readListScrollMutation(this.options.list),
4866
5282
  normalizeListState: this._normalizeListState.bind(this),
4867
5283
  readAnchor: (state) => this._readAnchor(state, this._getItemHeight.bind(this)),
4868
5284
  applyAnchor: this._applyAnchor.bind(this),
@@ -4879,18 +5295,10 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
4879
5295
  get position() {
4880
5296
  return this.options.list.position;
4881
5297
  }
4882
- /** Updates the current anchor item index. */
4883
- set position(value) {
4884
- this.options.list.position = value;
4885
- }
4886
5298
  /** Pixel offset from the anchored item edge. */
4887
5299
  get offset() {
4888
5300
  return this.options.list.offset;
4889
5301
  }
4890
- /** Updates the pixel offset from the anchored item edge. */
4891
- set offset(value) {
4892
- this.options.list.offset = value;
4893
- }
4894
5302
  /** Items currently available to the renderer. */
4895
5303
  get items() {
4896
5304
  return this.options.list.items;
@@ -4902,6 +5310,7 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
4902
5310
  /** Renders the current visible window. */
4903
5311
  render(feedback) {
4904
5312
  this.#jumpController.beforeFrame();
5313
+ this.#jumpController.noteViewportWidth(this.graphics.canvas.clientWidth);
4905
5314
  const now = getNow();
4906
5315
  const keepAnimating = this._prepareRender(now);
4907
5316
  const { clientWidth: viewportWidth, clientHeight: viewportHeight } = this.graphics.canvas;
@@ -4909,12 +5318,11 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
4909
5318
  const frame = prepareFrameSession({
4910
5319
  now,
4911
5320
  resolveVisibleWindow: (frameNow) => this._resolveVisibleWindow(frameNow),
4912
- getViewportTranslateY: (frameNow) => this.#transitionController.getViewportTranslateY(frameNow),
4913
- captureVisibleItemSnapshot: (solution, extraShift) => this._captureVisibleItemSnapshot(solution, extraShift),
5321
+ captureVisibleItemSnapshot: (solution) => this._captureVisibleItemSnapshot(solution),
4914
5322
  pruneTransitionAnimations: (window, frameNow) => this._pruneTransitionAnimations(window, frameNow)
4915
5323
  });
4916
- const autoFollowCapabilities = this.#jumpController.syncAutoFollowCapabilities(this._readAutoFollowCapabilities(frame.solution.window, frame.viewportTranslateY));
4917
- const requestRedraw = this._renderVisibleWindow(frame.solution.window, feedback, frame.viewportTranslateY);
5324
+ const autoFollowCapabilities = this.#jumpController.recomputeAutoFollowCapabilities(this._readAutoFollowCapabilities(frame.solution.window));
5325
+ const requestRedraw = this._renderVisibleWindow(frame.solution.window, feedback);
4918
5326
  if (feedback != null) {
4919
5327
  feedback.canAutoFollowTop = autoFollowCapabilities.top;
4920
5328
  feedback.canAutoFollowBottom = autoFollowCapabilities.bottom;
@@ -4925,17 +5333,17 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
4925
5333
  /** Hit-tests the current visible window. */
4926
5334
  hittest(test) {
4927
5335
  this.#jumpController.beforeFrame();
5336
+ this.#jumpController.noteViewportWidth(this.graphics.canvas.clientWidth);
4928
5337
  const now = getNow();
4929
5338
  this.#transitionController.settle(now, this.#getTransitionLifecycleAdapter());
4930
5339
  const frame = prepareFrameSession({
4931
5340
  now,
4932
5341
  resolveVisibleWindow: (frameNow) => this._resolveVisibleWindow(frameNow),
4933
- getViewportTranslateY: (frameNow) => this.#transitionController.getViewportTranslateY(frameNow),
4934
- captureVisibleItemSnapshot: (solution, extraShift) => this._captureVisibleItemSnapshot(solution, extraShift),
5342
+ captureVisibleItemSnapshot: (solution) => this._captureVisibleItemSnapshot(solution),
4935
5343
  pruneTransitionAnimations: (window, frameNow) => this._pruneTransitionAnimations(window, frameNow)
4936
5344
  });
4937
- this.#jumpController.syncAutoFollowCapabilities(this._readAutoFollowCapabilities(frame.solution.window, frame.viewportTranslateY));
4938
- return this._hittestVisibleWindow(frame.solution.window, test, frame.viewportTranslateY);
5345
+ this.#jumpController.recomputeAutoFollowCapabilities(this._readAutoFollowCapabilities(frame.solution.window));
5346
+ return this._hittestVisibleWindow(frame.solution.window, test);
4939
5347
  }
4940
5348
  _readListState() {
4941
5349
  return {
@@ -4947,8 +5355,7 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
4947
5355
  return this._resolveVisibleWindowForState(this._readListState(), now);
4948
5356
  }
4949
5357
  _commitListState(state) {
4950
- this.position = state.position;
4951
- this.offset = state.offset;
5358
+ writeInternalListScrollState(this.options.list, state);
4952
5359
  this.#jumpController.commit(state);
4953
5360
  }
4954
5361
  /**
@@ -4990,20 +5397,20 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
4990
5397
  }
4991
5398
  _renderDrawList(list, shift, feedback) {
4992
5399
  let result = false;
4993
- const viewportHeight = this.graphics.canvas.clientHeight;
4994
- for (const { idx, value: item, offset, height } of list) {
4995
- const y = offset + shift;
4996
- if (feedback != null) this._accumulateRenderFeedback(feedback, idx, y, height);
4997
- if (y + height < 0 || y > viewportHeight) continue;
5400
+ const viewport = this._getViewportMetrics();
5401
+ for (const { index, value: item, offset, height } of list) {
5402
+ const y = offset + shift + viewport.contentTop;
5403
+ if (feedback != null) this._accumulateRenderFeedback(feedback, index, y, height);
5404
+ if (y + height < 0 || y > viewport.outerHeight) continue;
4998
5405
  if (item.draw(y)) result = true;
4999
5406
  }
5000
5407
  return result;
5001
5408
  }
5002
- _renderVisibleWindow(window, feedback, extraShift = 0) {
5409
+ _renderVisibleWindow(window, feedback) {
5003
5410
  this._resetRenderFeedback(feedback);
5004
- return this._renderDrawList(window.drawList, window.shift + extraShift, feedback);
5411
+ return this._renderDrawList(window.drawList, window.shift, feedback);
5005
5412
  }
5006
- _readAutoFollowCapabilities(window, extraShift = 0) {
5413
+ _readAutoFollowCapabilities(window) {
5007
5414
  if (window.drawList.length === 0 || this.items.length === 0) return {
5008
5415
  top: false,
5009
5416
  bottom: false
@@ -5012,21 +5419,31 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
5012
5419
  let maxIndex = Number.NEGATIVE_INFINITY;
5013
5420
  let topMostY = Number.POSITIVE_INFINITY;
5014
5421
  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;
5422
+ const viewport = this._getViewportMetrics();
5423
+ for (const { index, offset, height } of window.drawList) {
5424
+ minIndex = Math.min(minIndex, index);
5425
+ maxIndex = Math.max(maxIndex, index);
5426
+ const y = offset + window.shift + viewport.contentTop;
5020
5427
  topMostY = Math.min(topMostY, y);
5021
5428
  bottomMostY = Math.max(bottomMostY, y + height);
5022
5429
  }
5023
- const viewportHeight = this.graphics.canvas.clientHeight;
5024
5430
  return {
5025
- top: minIndex === 0 && topMostY >= -Number.EPSILON,
5026
- bottom: maxIndex === this.items.length - 1 && bottomMostY <= viewportHeight + Number.EPSILON
5431
+ top: minIndex === 0 && topMostY >= viewport.contentTop - 1e-6,
5432
+ bottom: maxIndex === this.items.length - 1 && bottomMostY <= viewport.contentBottom + 1e-6
5027
5433
  };
5028
5434
  }
5029
5435
  _readVisibleRange(top, height) {
5436
+ if (!Number.isFinite(top) || !Number.isFinite(height) || height <= 0) return;
5437
+ const viewport = this._getViewportMetrics();
5438
+ const visibleTop = clamp$1(viewport.contentTop - top, 0, height);
5439
+ const visibleBottom = clamp$1(viewport.contentBottom - top, 0, height);
5440
+ if (visibleBottom <= visibleTop) return;
5441
+ return {
5442
+ top: visibleTop,
5443
+ bottom: visibleBottom
5444
+ };
5445
+ }
5446
+ _readOuterVisibleRange(top, height) {
5030
5447
  if (!Number.isFinite(top) || !Number.isFinite(height) || height <= 0) return;
5031
5448
  const viewportHeight = this.graphics.canvas.clientHeight;
5032
5449
  const visibleTop = clamp$1(-top, 0, height);
@@ -5040,17 +5457,19 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
5040
5457
  _pruneTransitionAnimations(_window, now) {
5041
5458
  return this.#transitionController.pruneInvisibleAt(now, this.#getTransitionPlanningAdapter(), this.#getTransitionLifecycleAdapter());
5042
5459
  }
5043
- _hittestVisibleWindow(window, test, extraShift = 0) {
5460
+ _hittestVisibleWindow(window, test) {
5461
+ const viewport = this._getViewportMetrics();
5044
5462
  for (const { value: item, offset, height } of window.drawList) {
5045
- const y = offset + window.shift + extraShift;
5463
+ const y = offset + window.shift + viewport.contentTop;
5046
5464
  if (test.y < y || test.y >= y + height) continue;
5047
5465
  return item.hittest(test, y);
5048
5466
  }
5049
5467
  return false;
5050
5468
  }
5051
- _captureVisibleItemSnapshot(solution, extraShift = 0) {
5469
+ _captureVisibleItemSnapshot(solution) {
5052
5470
  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));
5471
+ const viewport = this._getViewportMetrics();
5472
+ this.#transitionController.captureVisibilitySnapshot(solution.window, solution.resolutionPath, this.items, viewport, normalizedState, this._readVisibleRange.bind(this), this._readOuterVisibleRange.bind(this));
5054
5473
  }
5055
5474
  _prepareRender(now) {
5056
5475
  const keepTransitioning = this.#transitionController.prepare(now, this.#getTransitionLifecycleAdapter());
@@ -5090,26 +5509,34 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
5090
5509
  _resolveItem(item, _index, now) {
5091
5510
  return this.#transitionController.resolveItem(item, now, this.#getTransitionRenderAdapter(), this.#getTransitionLifecycleAdapter());
5092
5511
  }
5512
+ _getViewportMetrics() {
5513
+ return resolveListViewport(this.graphics.canvas.clientHeight, this._getLayoutOptions().padding);
5514
+ }
5093
5515
  #handleDeleteComplete(item) {
5094
- this.options.list.finalizeDelete(item);
5516
+ finalizeInternalListDelete(this.options.list, item);
5095
5517
  }
5096
5518
  #getTransitionLifecycleAdapter() {
5097
5519
  return {
5098
5520
  onDeleteComplete: this.#handleDeleteComplete.bind(this),
5099
5521
  captureVisualAnchor: this._readAnchorAt.bind(this),
5100
5522
  restoreVisualAnchor: this._restoreAnchor.bind(this),
5523
+ readScrollState: this._readListState.bind(this),
5101
5524
  readItemIndex: (item) => this.items.indexOf(item),
5102
- snapItemToViewportBoundary: this.#snapItemToViewportBoundary.bind(this)
5525
+ snapItemToViewportBoundary: this.#snapItemToViewportBoundary.bind(this),
5526
+ onTransitionSettleScrollAdjusted: () => this.#jumpController.reconcileAutoFollowAfterTransitionSettle()
5103
5527
  };
5104
5528
  }
5105
5529
  #getVirtualizedRuntime() {
5530
+ const viewport = this._getViewportMetrics();
5106
5531
  return {
5107
5532
  items: this.items,
5108
5533
  position: this.position,
5109
5534
  offset: this.offset,
5110
5535
  renderItem: this.options.renderItem,
5111
5536
  measureNode: this.measureRootNode.bind(this),
5537
+ viewport,
5112
5538
  readVisibleRange: this._readVisibleRange.bind(this),
5539
+ readOuterVisibleRange: this._readOuterVisibleRange.bind(this),
5113
5540
  resolveVisibleWindow: () => this._resolveVisibleWindow(getNow()),
5114
5541
  resolveVisibleWindowForState: (state, now) => this._resolveVisibleWindowForState(state, now)
5115
5542
  };
@@ -5127,7 +5554,7 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
5127
5554
  #getTransitionPlanningAdapter() {
5128
5555
  return {
5129
5556
  ...this.#getVirtualizedRuntime(),
5130
- underflowAlign: this._getLayoutOptions().underflowAlign
5557
+ anchorMode: this._getLayoutOptions().anchorMode
5131
5558
  };
5132
5559
  }
5133
5560
  #handleListStateChange(change) {
@@ -5204,212 +5631,6 @@ function getTargetAnchorForItem(itemCount, index, block, anchorMode, viewportHei
5204
5631
  }
5205
5632
  }
5206
5633
  //#endregion
5207
- //#region src/renderer/virtualized/solver.ts
5208
- function clamp(value, min, max) {
5209
- return Math.min(Math.max(value, min), max);
5210
- }
5211
- function normalizeOffset(offset) {
5212
- return Number.isFinite(offset) ? offset : 0;
5213
- }
5214
- function resolveListLayoutOptions(options = {}) {
5215
- return {
5216
- anchorMode: options.anchorMode ?? "top",
5217
- underflowAlign: options.underflowAlign ?? "top"
5218
- };
5219
- }
5220
- function normalizeVisibleState(itemCount, state, layout) {
5221
- if (itemCount <= 0) return {
5222
- position: 0,
5223
- offset: 0
5224
- };
5225
- const position = state.position;
5226
- const fallbackPosition = layout.anchorMode === "top" ? 0 : itemCount - 1;
5227
- if (typeof position !== "number" || !Number.isFinite(position)) return {
5228
- position: fallbackPosition,
5229
- offset: normalizeOffset(state.offset)
5230
- };
5231
- return {
5232
- position: clamp(Math.trunc(position), 0, itemCount - 1),
5233
- offset: normalizeOffset(state.offset)
5234
- };
5235
- }
5236
- function resolveVisibleWindow(items, state, viewportHeight, resolveItem, layout) {
5237
- const normalizedState = normalizeVisibleState(items.length, state, layout);
5238
- const resolutionPath = /* @__PURE__ */ new Set();
5239
- const readResolvedItem = (item, idx) => {
5240
- resolutionPath.add(idx);
5241
- return resolveItem(item, idx);
5242
- };
5243
- if (items.length === 0) return {
5244
- normalizedState,
5245
- resolutionPath: [],
5246
- window: {
5247
- drawList: [],
5248
- shift: 0
5249
- }
5250
- };
5251
- if (layout.anchorMode === "top") {
5252
- let { position, offset } = normalizedState;
5253
- let drawLength = 0;
5254
- if (offset > 0) if (position === 0) offset = 0;
5255
- else {
5256
- for (let i = position - 1; i >= 0; i -= 1) {
5257
- const { height } = readResolvedItem(items[i], i);
5258
- position = i;
5259
- offset -= height;
5260
- if (offset <= 0) break;
5261
- }
5262
- if (position === 0 && offset > 0) offset = 0;
5263
- }
5264
- let y = offset;
5265
- const drawList = [];
5266
- for (let i = position; i < items.length; i += 1) {
5267
- const { value, height } = readResolvedItem(items[i], i);
5268
- if (y + height > 0) {
5269
- drawList.push({
5270
- idx: i,
5271
- value,
5272
- offset: y,
5273
- height
5274
- });
5275
- drawLength += height;
5276
- } else {
5277
- offset += height;
5278
- position = i + 1;
5279
- }
5280
- y += height;
5281
- if (y >= viewportHeight) break;
5282
- }
5283
- let shift = 0;
5284
- if (y < viewportHeight) if (position === 0 && drawLength < viewportHeight) {
5285
- shift = -offset;
5286
- offset = 0;
5287
- } else {
5288
- shift = viewportHeight - y;
5289
- y = offset += shift;
5290
- let lastIdx = -1;
5291
- for (let i = position - 1; i >= 0; i -= 1) {
5292
- const { value, height } = readResolvedItem(items[i], i);
5293
- drawLength += height;
5294
- y -= height;
5295
- drawList.push({
5296
- idx: i,
5297
- value,
5298
- offset: y - shift,
5299
- height
5300
- });
5301
- lastIdx = i;
5302
- if (y < 0) break;
5303
- }
5304
- if (lastIdx === 0 && drawLength < viewportHeight) {
5305
- shift = drawList.at(-1)?.offset == null ? 0 : -drawList.at(-1).offset;
5306
- position = 0;
5307
- offset = 0;
5308
- }
5309
- }
5310
- return finalizeVisibleWindowResult(items.length, viewportHeight, layout, {
5311
- position,
5312
- offset
5313
- }, Array.from(resolutionPath), {
5314
- drawList,
5315
- shift
5316
- });
5317
- }
5318
- let { position, offset } = normalizedState;
5319
- let drawLength = 0;
5320
- if (offset < 0) if (position === items.length - 1) offset = 0;
5321
- else for (let i = position + 1; i < items.length; i += 1) {
5322
- const { height } = readResolvedItem(items[i], i);
5323
- position = i;
5324
- offset += height;
5325
- if (offset > 0) break;
5326
- }
5327
- let y = viewportHeight + offset;
5328
- const drawList = [];
5329
- for (let i = position; i >= 0; i -= 1) {
5330
- const { value, height } = readResolvedItem(items[i], i);
5331
- y -= height;
5332
- if (y <= viewportHeight) {
5333
- drawList.push({
5334
- idx: i,
5335
- value,
5336
- offset: y,
5337
- height
5338
- });
5339
- drawLength += height;
5340
- } else {
5341
- offset -= height;
5342
- position = i - 1;
5343
- }
5344
- if (y < 0) break;
5345
- }
5346
- let shift = 0;
5347
- if (y > 0) {
5348
- shift = -y;
5349
- if (drawLength < viewportHeight) {
5350
- y = drawLength;
5351
- for (let i = position + 1; i < items.length; i += 1) {
5352
- const { value, height } = readResolvedItem(items[i], i);
5353
- drawList.push({
5354
- idx: i,
5355
- value,
5356
- offset: y - shift,
5357
- height
5358
- });
5359
- y = drawLength += height;
5360
- position = i;
5361
- if (y >= viewportHeight) break;
5362
- }
5363
- offset = drawLength < viewportHeight ? 0 : drawLength - viewportHeight;
5364
- } else offset = drawLength - viewportHeight;
5365
- }
5366
- return finalizeVisibleWindowResult(items.length, viewportHeight, layout, {
5367
- position,
5368
- offset
5369
- }, Array.from(resolutionPath), {
5370
- drawList,
5371
- shift
5372
- });
5373
- }
5374
- function finalizeVisibleWindowResult(itemCount, viewportHeight, layout, normalizedState, resolutionPath, window) {
5375
- if (window.drawList.length !== itemCount || itemCount <= 0) return {
5376
- normalizedState,
5377
- resolutionPath,
5378
- window
5379
- };
5380
- let minIndex = Number.POSITIVE_INFINITY;
5381
- let maxIndex = Number.NEGATIVE_INFINITY;
5382
- let minOffset = Number.POSITIVE_INFINITY;
5383
- let maxBottom = Number.NEGATIVE_INFINITY;
5384
- for (const entry of window.drawList) {
5385
- minIndex = Math.min(minIndex, entry.idx);
5386
- maxIndex = Math.max(maxIndex, entry.idx);
5387
- minOffset = Math.min(minOffset, entry.offset);
5388
- maxBottom = Math.max(maxBottom, entry.offset + entry.height);
5389
- }
5390
- const contentHeight = maxBottom - minOffset;
5391
- if (minIndex !== 0 || maxIndex !== itemCount - 1 || !(contentHeight < viewportHeight - Number.EPSILON)) return {
5392
- normalizedState,
5393
- resolutionPath,
5394
- window
5395
- };
5396
- const desiredTop = layout.underflowAlign === "bottom" ? viewportHeight - contentHeight : 0;
5397
- return {
5398
- normalizedState: layout.anchorMode === "top" ? {
5399
- position: 0,
5400
- offset: 0
5401
- } : {
5402
- position: itemCount - 1,
5403
- offset: 0
5404
- },
5405
- resolutionPath,
5406
- window: {
5407
- drawList: window.drawList,
5408
- shift: desiredTop - minOffset
5409
- }
5410
- };
5411
- }
5412
- //#endregion
5413
5634
  //#region src/renderer/virtualized/list.ts
5414
5635
  /**
5415
5636
  * Virtualized list renderer with configurable anchor semantics.
@@ -5420,11 +5641,24 @@ var ListRenderer = class extends VirtualizedRenderer {
5420
5641
  super(graphics, options);
5421
5642
  this.#layout = resolveListLayoutOptions(options);
5422
5643
  }
5644
+ get padding() {
5645
+ return { ...this.#layout.padding };
5646
+ }
5647
+ set padding(value) {
5648
+ const nextPadding = normalizeListPadding(value);
5649
+ if (nextPadding.top === this.#layout.padding.top && nextPadding.bottom === this.#layout.padding.bottom) return;
5650
+ const anchor = this._readAnchorAt(performance.now());
5651
+ this.#layout = {
5652
+ ...this.#layout,
5653
+ padding: nextPadding
5654
+ };
5655
+ if (anchor != null) this._restoreAnchor(anchor);
5656
+ }
5423
5657
  _getLayoutOptions() {
5424
5658
  return this.#layout;
5425
5659
  }
5426
5660
  _resolveVisibleWindowForState(state, now) {
5427
- return resolveVisibleWindow(this.items, state, this.graphics.canvas.clientHeight, (item, idx) => this._resolveItem(item, idx, now), this.#layout);
5661
+ return resolveVisibleWindow(this.items, state, resolveListViewport(this.graphics.canvas.clientHeight, this.#layout.padding), (item, idx) => this._resolveItem(item, idx, now), this.#layout);
5428
5662
  }
5429
5663
  _getDefaultJumpBlock() {
5430
5664
  return this.#layout.anchorMode === "top" ? "start" : "end";
@@ -5441,7 +5675,7 @@ var ListRenderer = class extends VirtualizedRenderer {
5441
5675
  this._commitListState(state);
5442
5676
  }
5443
5677
  _getTargetAnchor(index, block) {
5444
- return getTargetAnchorForItem(this.items.length, index, block, this.#layout.anchorMode, this.graphics.canvas.clientHeight, this._getItemHeight.bind(this));
5678
+ return getTargetAnchorForItem(this.items.length, index, block, this.#layout.anchorMode, resolveListViewport(this.graphics.canvas.clientHeight, this.#layout.padding).contentHeight, this._getItemHeight.bind(this));
5445
5679
  }
5446
5680
  };
5447
5681
  //#endregion