chat-layout 1.2.0-5 → 1.2.0-6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/example/chat.ts +4 -2
- package/index.d.mts +18 -1
- package/index.mjs +239 -42
- package/index.mjs.map +1 -1
- package/package.json +1 -1
package/example/chat.ts
CHANGED
|
@@ -513,6 +513,8 @@ function drawFrame(): void {
|
|
|
513
513
|
maxIdx: Number.NaN,
|
|
514
514
|
min: Number.NaN,
|
|
515
515
|
max: Number.NaN,
|
|
516
|
+
canAutoFollowTop: false,
|
|
517
|
+
canAutoFollowBottom: false,
|
|
516
518
|
};
|
|
517
519
|
renderer.render(feedback);
|
|
518
520
|
|
|
@@ -644,7 +646,7 @@ button("unshift", () => {
|
|
|
644
646
|
],
|
|
645
647
|
{
|
|
646
648
|
duration: INSERT_ANIMATION_DURATION,
|
|
647
|
-
|
|
649
|
+
autoFollow: true,
|
|
648
650
|
},
|
|
649
651
|
);
|
|
650
652
|
});
|
|
@@ -661,7 +663,7 @@ button("push", () => {
|
|
|
661
663
|
],
|
|
662
664
|
{
|
|
663
665
|
distance: 24,
|
|
664
|
-
|
|
666
|
+
autoFollow: true,
|
|
665
667
|
},
|
|
666
668
|
);
|
|
667
669
|
});
|
package/index.d.mts
CHANGED
|
@@ -19,6 +19,10 @@ interface RenderFeedback {
|
|
|
19
19
|
min: number;
|
|
20
20
|
/** Largest visible continuous item position, expressed in item coordinates rather than pixels. */
|
|
21
21
|
max: number;
|
|
22
|
+
/** Whether the current viewport may auto-follow inserts at the visual top edge. */
|
|
23
|
+
canAutoFollowTop: boolean;
|
|
24
|
+
/** Whether the current viewport may auto-follow inserts at the visual bottom edge. */
|
|
25
|
+
canAutoFollowBottom: boolean;
|
|
22
26
|
}
|
|
23
27
|
/**
|
|
24
28
|
* The main axis direction used by flex containers.
|
|
@@ -523,7 +527,7 @@ interface InsertListItemsAnimationOptions {
|
|
|
523
527
|
/** Enter offset in pixels measured from the final resting position. */
|
|
524
528
|
distance?: number;
|
|
525
529
|
/** Auto-follow the insertion edge when the viewport was already pinned there. */
|
|
526
|
-
|
|
530
|
+
autoFollow?: boolean;
|
|
527
531
|
}
|
|
528
532
|
type PushListItemsAnimationOptions = InsertListItemsAnimationOptions;
|
|
529
533
|
type UnshiftListItemsAnimationOptions = InsertListItemsAnimationOptions;
|
|
@@ -593,6 +597,10 @@ declare function memoRenderItemBy<C extends CanvasRenderingContext2D, T, K>(keyO
|
|
|
593
597
|
};
|
|
594
598
|
//#endregion
|
|
595
599
|
//#region src/renderer/virtualized/base-types.d.ts
|
|
600
|
+
type AutoFollowCapabilities = {
|
|
601
|
+
top: boolean;
|
|
602
|
+
bottom: boolean;
|
|
603
|
+
};
|
|
596
604
|
/** Per-item draw/hittest callbacks produced by the resolver. */
|
|
597
605
|
type VirtualizedResolvedItem = {
|
|
598
606
|
draw: (y: number) => boolean;
|
|
@@ -690,10 +698,19 @@ declare abstract class VirtualizedRenderer<C extends CanvasRenderingContext2D, T
|
|
|
690
698
|
* Scrolls the viewport to the requested item index.
|
|
691
699
|
*/
|
|
692
700
|
jumpTo(index: number, options?: JumpToOptions): void;
|
|
701
|
+
/**
|
|
702
|
+
* Scrolls the viewport to the visual top edge and arms top auto-follow immediately.
|
|
703
|
+
*/
|
|
704
|
+
jumpToTop(options?: JumpToOptions): void;
|
|
705
|
+
/**
|
|
706
|
+
* Scrolls the viewport to the visual bottom edge and arms bottom auto-follow immediately.
|
|
707
|
+
*/
|
|
708
|
+
jumpToBottom(options?: JumpToOptions): void;
|
|
693
709
|
protected _resetRenderFeedback(feedback?: RenderFeedback): void;
|
|
694
710
|
protected _accumulateRenderFeedback(feedback: RenderFeedback, idx: number, top: number, height: number): void;
|
|
695
711
|
protected _renderDrawList(list: VisibleWindow<VirtualizedResolvedItem>["drawList"], shift: number, feedback?: RenderFeedback): boolean;
|
|
696
712
|
protected _renderVisibleWindow(window: VisibleWindow<VirtualizedResolvedItem>, feedback?: RenderFeedback, extraShift?: number): boolean;
|
|
713
|
+
protected _readAutoFollowCapabilities(window: VisibleWindow<VirtualizedResolvedItem>, extraShift?: number): AutoFollowCapabilities;
|
|
697
714
|
protected _readVisibleRange(top: number, height: number): {
|
|
698
715
|
top: number;
|
|
699
716
|
bottom: number;
|
package/index.mjs
CHANGED
|
@@ -3661,7 +3661,7 @@ function normalizeInsertAnimation(animation) {
|
|
|
3661
3661
|
if (duration == null) return;
|
|
3662
3662
|
const normalizedAnimation = { duration };
|
|
3663
3663
|
if (typeof animation?.distance === "number" && Number.isFinite(animation.distance)) normalizedAnimation.distance = Math.max(0, animation.distance);
|
|
3664
|
-
if (animation?.
|
|
3664
|
+
if (animation?.autoFollow === true) normalizedAnimation.autoFollow = true;
|
|
3665
3665
|
return normalizedAnimation;
|
|
3666
3666
|
}
|
|
3667
3667
|
var ListState = class {
|
|
@@ -3981,18 +3981,21 @@ function prepareFrameSession(params) {
|
|
|
3981
3981
|
//#endregion
|
|
3982
3982
|
//#region src/renderer/virtualized/jump-controller.ts
|
|
3983
3983
|
var JumpController = class {
|
|
3984
|
-
#
|
|
3984
|
+
#confirmedAutoFollowTop = false;
|
|
3985
|
+
#confirmedAutoFollowBottom = false;
|
|
3985
3986
|
#controlledState;
|
|
3986
3987
|
#jumpAnimation;
|
|
3987
3988
|
#lastCommittedState;
|
|
3988
3989
|
#hasPendingListChange = false;
|
|
3990
|
+
#pendingBoundaryJumpTop = false;
|
|
3991
|
+
#pendingBoundaryJumpBottom = false;
|
|
3989
3992
|
#options;
|
|
3990
3993
|
constructor(options) {
|
|
3991
3994
|
this.#options = options;
|
|
3992
3995
|
}
|
|
3993
3996
|
beforeFrame() {
|
|
3994
3997
|
const currentState = this.#options.readListState();
|
|
3995
|
-
if (!this.#hasPendingListChange && this.#jumpAnimation == null && this.#lastCommittedState != null && !sameState(this.#lastCommittedState, currentState.position, currentState.offset)) this.#
|
|
3998
|
+
if (!this.#hasPendingListChange && this.#jumpAnimation == null && this.#lastCommittedState != null && !sameState(this.#lastCommittedState, currentState.position, currentState.offset)) this.#clearPendingBoundaryJumps();
|
|
3996
3999
|
this.#hasPendingListChange = false;
|
|
3997
4000
|
}
|
|
3998
4001
|
prepare(now) {
|
|
@@ -4003,7 +4006,7 @@ var JumpController = class {
|
|
|
4003
4006
|
return false;
|
|
4004
4007
|
}
|
|
4005
4008
|
if (this.#controlledState != null && !sameState(this.#controlledState, this.#options.readListState().position, this.#options.readListState().offset)) {
|
|
4006
|
-
this.#
|
|
4009
|
+
this.#clearPendingBoundaryJumps();
|
|
4007
4010
|
this.#cancelJumpAnimation();
|
|
4008
4011
|
return false;
|
|
4009
4012
|
}
|
|
@@ -4033,28 +4036,53 @@ var JumpController = class {
|
|
|
4033
4036
|
};
|
|
4034
4037
|
}
|
|
4035
4038
|
jumpTo(index, options = {}) {
|
|
4036
|
-
this.#
|
|
4039
|
+
this.#clearPendingBoundaryJumps();
|
|
4037
4040
|
if (this.#options.getItemCount() === 0) {
|
|
4038
4041
|
this.#cancelJumpAnimation();
|
|
4039
4042
|
return;
|
|
4040
4043
|
}
|
|
4041
4044
|
this.#startJumpToIndex(index, options, { kind: "manual" });
|
|
4042
4045
|
}
|
|
4046
|
+
jumpToBoundary(boundary, options = {}) {
|
|
4047
|
+
this.#clearPendingBoundaryJumps();
|
|
4048
|
+
if (this.#options.getItemCount() === 0) {
|
|
4049
|
+
this.#cancelJumpAnimation();
|
|
4050
|
+
return;
|
|
4051
|
+
}
|
|
4052
|
+
this.#armBoundaryJump(boundary);
|
|
4053
|
+
this.#startJumpToIndex(boundary === "bottom" ? this.#options.getItemCount() - 1 : 0, {
|
|
4054
|
+
...options,
|
|
4055
|
+
block: boundary === "bottom" ? "end" : "start"
|
|
4056
|
+
}, {
|
|
4057
|
+
kind: "boundary-jump",
|
|
4058
|
+
boundary
|
|
4059
|
+
});
|
|
4060
|
+
}
|
|
4061
|
+
syncAutoFollowCapabilities(capabilities) {
|
|
4062
|
+
this.#confirmedAutoFollowTop = capabilities.top;
|
|
4063
|
+
this.#confirmedAutoFollowBottom = capabilities.bottom;
|
|
4064
|
+
this.#clearPendingBoundaryJumps();
|
|
4065
|
+
return this.getEffectiveAutoFollowCapabilities();
|
|
4066
|
+
}
|
|
4067
|
+
getEffectiveAutoFollowCapabilities() {
|
|
4068
|
+
return {
|
|
4069
|
+
top: this.#hasEffectiveAutoFollowCapability("top"),
|
|
4070
|
+
bottom: this.#hasEffectiveAutoFollowCapability("bottom")
|
|
4071
|
+
};
|
|
4072
|
+
}
|
|
4043
4073
|
handleListStateChange(change) {
|
|
4044
4074
|
this.#hasPendingListChange = true;
|
|
4045
4075
|
const followChange = this.#resolveAutoFollowChange(change);
|
|
4046
|
-
const canChainAutoFollow = followChange != null ? this.#shouldChainAutoFollow(followChange.
|
|
4047
|
-
const
|
|
4048
|
-
|
|
4049
|
-
if (followChange != null && (canSnapshotAutoFollow || canChainAutoFollow || canLatchAutoFollow)) {
|
|
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)) {
|
|
4050
4079
|
if (canChainAutoFollow) this.#rebaseJumpAnchorForBoundaryInsert(followChange.direction, followChange.count, getNow());
|
|
4051
|
-
this.#
|
|
4052
|
-
|
|
4053
|
-
block: followChange.direction === "push" ? "end" : "start",
|
|
4080
|
+
this.#startJumpToIndex(followChange.boundary === "bottom" ? this.#options.getItemCount() - 1 : 0, {
|
|
4081
|
+
block: followChange.boundary === "bottom" ? "end" : "start",
|
|
4054
4082
|
duration: followChange.animation?.duration
|
|
4055
4083
|
}, {
|
|
4056
4084
|
kind: "auto-follow",
|
|
4057
|
-
|
|
4085
|
+
boundary: followChange.boundary
|
|
4058
4086
|
});
|
|
4059
4087
|
return {
|
|
4060
4088
|
...followChange.change,
|
|
@@ -4106,8 +4134,9 @@ var JumpController = class {
|
|
|
4106
4134
|
#resolveAutoFollowChange(change) {
|
|
4107
4135
|
switch (change.type) {
|
|
4108
4136
|
case "push":
|
|
4109
|
-
case "unshift": return change.animation?.
|
|
4137
|
+
case "unshift": return change.animation?.autoFollow === true ? {
|
|
4110
4138
|
change,
|
|
4139
|
+
boundary: change.type === "push" ? "bottom" : "top",
|
|
4111
4140
|
direction: change.type,
|
|
4112
4141
|
count: change.count,
|
|
4113
4142
|
animation: change.animation
|
|
@@ -4115,21 +4144,11 @@ var JumpController = class {
|
|
|
4115
4144
|
default: return;
|
|
4116
4145
|
}
|
|
4117
4146
|
}
|
|
4118
|
-
#
|
|
4119
|
-
|
|
4120
|
-
return this.#options.canAutoFollowBoundaryInsert(direction, count, this.#options.readListState().position, this.#options.readListState().offset);
|
|
4121
|
-
}
|
|
4122
|
-
#shouldLatchAutoFollow(direction, count, animation) {
|
|
4123
|
-
if (animation?.followIfAtBoundary !== true) return false;
|
|
4124
|
-
if (!this.#matchesLastCommittedStateAfterBoundaryInsert(direction, count)) {
|
|
4125
|
-
this.#clearAutoFollowLatch();
|
|
4126
|
-
return false;
|
|
4127
|
-
}
|
|
4128
|
-
return this.#autoFollowLatch === direction;
|
|
4147
|
+
#shouldAutoFollowFromCapability(boundary, direction, count) {
|
|
4148
|
+
return this.#hasEffectiveAutoFollowCapability(boundary) && this.#matchesLastCommittedStateAfterBoundaryInsert(direction, count);
|
|
4129
4149
|
}
|
|
4130
|
-
#shouldChainAutoFollow(
|
|
4131
|
-
|
|
4132
|
-
return this.#jumpAnimation?.source.kind === "auto-follow" ? this.#jumpAnimation.source.direction === direction : false;
|
|
4150
|
+
#shouldChainAutoFollow(boundary) {
|
|
4151
|
+
return this.#readJumpBoundary() === boundary;
|
|
4133
4152
|
}
|
|
4134
4153
|
#rebaseJumpAnchorForBoundaryInsert(direction, count, now) {
|
|
4135
4154
|
const animation = this.#jumpAnimation;
|
|
@@ -4148,8 +4167,22 @@ var JumpController = class {
|
|
|
4148
4167
|
offset: state.offset
|
|
4149
4168
|
}, this.#options.readListState().position, this.#options.readListState().offset);
|
|
4150
4169
|
}
|
|
4151
|
-
#
|
|
4152
|
-
|
|
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;
|
|
4153
4186
|
}
|
|
4154
4187
|
};
|
|
4155
4188
|
//#endregion
|
|
@@ -4157,14 +4190,21 @@ var JumpController = class {
|
|
|
4157
4190
|
var VisibilitySnapshot = class {
|
|
4158
4191
|
#drawnItems = /* @__PURE__ */ new Set();
|
|
4159
4192
|
#visibleItems = /* @__PURE__ */ new Set();
|
|
4193
|
+
#previousVisibleItems = /* @__PURE__ */ new Set();
|
|
4160
4194
|
#hasSnapshot = false;
|
|
4161
4195
|
#snapshotState;
|
|
4196
|
+
#previousSnapshotState;
|
|
4162
4197
|
#emptyState;
|
|
4163
4198
|
#coversShortList = false;
|
|
4164
4199
|
#topGap = 0;
|
|
4165
4200
|
#bottomGap = 0;
|
|
4166
4201
|
#atStartBoundary = false;
|
|
4167
4202
|
#atEndBoundary = false;
|
|
4203
|
+
#currentExtraShift = 0;
|
|
4204
|
+
#minDrawnIndex = Number.POSITIVE_INFINITY;
|
|
4205
|
+
#maxDrawnIndex = Number.NEGATIVE_INFINITY;
|
|
4206
|
+
#topBoundaryItem;
|
|
4207
|
+
#bottomBoundaryItem;
|
|
4168
4208
|
get coversShortList() {
|
|
4169
4209
|
return this.#hasSnapshot && this.#snapshotState != null && this.#coversShortList;
|
|
4170
4210
|
}
|
|
@@ -4174,22 +4214,58 @@ var VisibilitySnapshot = class {
|
|
|
4174
4214
|
get bottomGap() {
|
|
4175
4215
|
return this.#bottomGap;
|
|
4176
4216
|
}
|
|
4217
|
+
get previousState() {
|
|
4218
|
+
return this.#previousSnapshotState;
|
|
4219
|
+
}
|
|
4220
|
+
get currentExtraShift() {
|
|
4221
|
+
return this.#currentExtraShift;
|
|
4222
|
+
}
|
|
4223
|
+
readDrawnIndexRange() {
|
|
4224
|
+
if (!Number.isFinite(this.#minDrawnIndex) || !Number.isFinite(this.#maxDrawnIndex)) return;
|
|
4225
|
+
return {
|
|
4226
|
+
minIndex: this.#minDrawnIndex,
|
|
4227
|
+
maxIndex: this.#maxDrawnIndex
|
|
4228
|
+
};
|
|
4229
|
+
}
|
|
4230
|
+
readBoundaryItem(boundary) {
|
|
4231
|
+
return boundary === "top" ? this.#topBoundaryItem : this.#bottomBoundaryItem;
|
|
4232
|
+
}
|
|
4177
4233
|
capture(window, _resolutionPath, items, viewportHeight, snapshotState, extraShift, readVisibleRange) {
|
|
4234
|
+
this.#previousVisibleItems = this.#visibleItems;
|
|
4235
|
+
this.#previousSnapshotState = this.#snapshotState;
|
|
4178
4236
|
const nextDrawnItems = /* @__PURE__ */ new Set();
|
|
4179
4237
|
const nextVisibleItems = /* @__PURE__ */ new Set();
|
|
4180
4238
|
let minVisibleIndex = Number.POSITIVE_INFINITY;
|
|
4181
4239
|
let maxVisibleIndex = Number.NEGATIVE_INFINITY;
|
|
4182
4240
|
let topMostY = Number.POSITIVE_INFINITY;
|
|
4183
4241
|
let bottomMostY = Number.NEGATIVE_INFINITY;
|
|
4242
|
+
let nextMinDrawnIndex = Number.POSITIVE_INFINITY;
|
|
4243
|
+
let nextMaxDrawnIndex = Number.NEGATIVE_INFINITY;
|
|
4244
|
+
let nextTopBoundaryItem;
|
|
4245
|
+
let nextBottomBoundaryItem;
|
|
4246
|
+
let nextTopBoundaryY = Number.POSITIVE_INFINITY;
|
|
4247
|
+
let nextBottomBoundaryY = Number.NEGATIVE_INFINITY;
|
|
4184
4248
|
const effectiveShift = window.shift + extraShift;
|
|
4185
4249
|
for (const { idx, offset, height } of window.drawList) {
|
|
4186
4250
|
minVisibleIndex = Math.min(minVisibleIndex, idx);
|
|
4187
4251
|
maxVisibleIndex = Math.max(maxVisibleIndex, idx);
|
|
4252
|
+
nextMinDrawnIndex = Math.min(nextMinDrawnIndex, idx);
|
|
4253
|
+
nextMaxDrawnIndex = Math.max(nextMaxDrawnIndex, idx);
|
|
4188
4254
|
const y = offset + effectiveShift;
|
|
4189
4255
|
topMostY = Math.min(topMostY, y);
|
|
4190
4256
|
bottomMostY = Math.max(bottomMostY, y + height);
|
|
4191
4257
|
const item = items[idx];
|
|
4192
|
-
if (item != null)
|
|
4258
|
+
if (item != null) {
|
|
4259
|
+
nextDrawnItems.add(item);
|
|
4260
|
+
if (y < nextTopBoundaryY) {
|
|
4261
|
+
nextTopBoundaryY = y;
|
|
4262
|
+
nextTopBoundaryItem = item;
|
|
4263
|
+
}
|
|
4264
|
+
if (y + height > nextBottomBoundaryY) {
|
|
4265
|
+
nextBottomBoundaryY = y + height;
|
|
4266
|
+
nextBottomBoundaryItem = item;
|
|
4267
|
+
}
|
|
4268
|
+
}
|
|
4193
4269
|
if (item == null || readVisibleRange(y, height) == null) continue;
|
|
4194
4270
|
nextVisibleItems.add(item);
|
|
4195
4271
|
}
|
|
@@ -4197,6 +4273,11 @@ var VisibilitySnapshot = class {
|
|
|
4197
4273
|
this.#visibleItems = nextVisibleItems;
|
|
4198
4274
|
this.#hasSnapshot = true;
|
|
4199
4275
|
this.#snapshotState = snapshotState;
|
|
4276
|
+
this.#currentExtraShift = extraShift;
|
|
4277
|
+
this.#minDrawnIndex = nextMinDrawnIndex;
|
|
4278
|
+
this.#maxDrawnIndex = nextMaxDrawnIndex;
|
|
4279
|
+
this.#topBoundaryItem = nextTopBoundaryItem;
|
|
4280
|
+
this.#bottomBoundaryItem = nextBottomBoundaryItem;
|
|
4200
4281
|
this.#emptyState = items.length === 0 && window.drawList.length === 0 ? snapshotState : void 0;
|
|
4201
4282
|
const contentHeight = bottomMostY - topMostY;
|
|
4202
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;
|
|
@@ -4228,20 +4309,30 @@ var VisibilitySnapshot = class {
|
|
|
4228
4309
|
isVisible(item) {
|
|
4229
4310
|
return this.#visibleItems.has(item);
|
|
4230
4311
|
}
|
|
4312
|
+
wasVisible(item) {
|
|
4313
|
+
return this.#previousVisibleItems.has(item);
|
|
4314
|
+
}
|
|
4231
4315
|
tracks(item, retention) {
|
|
4232
4316
|
return retention === "drawn" ? this.#drawnItems.has(item) : this.#visibleItems.has(item);
|
|
4233
4317
|
}
|
|
4234
4318
|
reset() {
|
|
4235
4319
|
this.#drawnItems.clear();
|
|
4236
4320
|
this.#visibleItems.clear();
|
|
4321
|
+
this.#previousVisibleItems.clear();
|
|
4237
4322
|
this.#hasSnapshot = false;
|
|
4238
4323
|
this.#snapshotState = void 0;
|
|
4324
|
+
this.#previousSnapshotState = void 0;
|
|
4239
4325
|
this.#emptyState = void 0;
|
|
4240
4326
|
this.#coversShortList = false;
|
|
4241
4327
|
this.#topGap = 0;
|
|
4242
4328
|
this.#bottomGap = 0;
|
|
4243
4329
|
this.#atStartBoundary = false;
|
|
4244
4330
|
this.#atEndBoundary = false;
|
|
4331
|
+
this.#currentExtraShift = 0;
|
|
4332
|
+
this.#minDrawnIndex = Number.POSITIVE_INFINITY;
|
|
4333
|
+
this.#maxDrawnIndex = Number.NEGATIVE_INFINITY;
|
|
4334
|
+
this.#topBoundaryItem = void 0;
|
|
4335
|
+
this.#bottomBoundaryItem = void 0;
|
|
4245
4336
|
}
|
|
4246
4337
|
#matchesStateAfterBoundaryInsert(direction, count, position, offset) {
|
|
4247
4338
|
const snapshotState = this.#snapshotState;
|
|
@@ -4629,6 +4720,19 @@ function handleTransitionStateChange(store, snapshot, currentViewportTranslateY,
|
|
|
4629
4720
|
}
|
|
4630
4721
|
//#endregion
|
|
4631
4722
|
//#region src/renderer/virtualized/base-transition.ts
|
|
4723
|
+
function remapAnchorAfterDeletes(anchor, deletedIndices) {
|
|
4724
|
+
if (!Number.isFinite(anchor) || deletedIndices.length === 0) return anchor;
|
|
4725
|
+
const sortedIndices = [...deletedIndices].filter((index) => Number.isFinite(index) && index >= 0).sort((a, b) => a - b);
|
|
4726
|
+
let removedBeforeAnchor = 0;
|
|
4727
|
+
for (const index of sortedIndices) {
|
|
4728
|
+
if (anchor > index + 1) {
|
|
4729
|
+
removedBeforeAnchor += 1;
|
|
4730
|
+
continue;
|
|
4731
|
+
}
|
|
4732
|
+
if (anchor >= index) return index - removedBeforeAnchor;
|
|
4733
|
+
}
|
|
4734
|
+
return anchor - removedBeforeAnchor;
|
|
4735
|
+
}
|
|
4632
4736
|
var TransitionController = class {
|
|
4633
4737
|
#store = new TransitionStore();
|
|
4634
4738
|
#snapshot = new VisibilitySnapshot();
|
|
@@ -4636,8 +4740,8 @@ var TransitionController = class {
|
|
|
4636
4740
|
captureVisibilitySnapshot(window, resolutionPath, items, viewportHeight, snapshotState, extraShift, readVisibleRange) {
|
|
4637
4741
|
this.#snapshot.capture(window, resolutionPath, items, viewportHeight, snapshotState, extraShift, readVisibleRange);
|
|
4638
4742
|
}
|
|
4639
|
-
pruneInvisible(lifecycle) {
|
|
4640
|
-
return this.pruneInvisibleAt(getNow(), lifecycle);
|
|
4743
|
+
pruneInvisible(ctx, lifecycle) {
|
|
4744
|
+
return this.pruneInvisibleAt(getNow(), ctx, lifecycle);
|
|
4641
4745
|
}
|
|
4642
4746
|
prepare(now, lifecycle) {
|
|
4643
4747
|
this.settle(now, lifecycle);
|
|
@@ -4673,8 +4777,9 @@ var TransitionController = class {
|
|
|
4673
4777
|
this.#cleanupViewportTranslateAnimation(now);
|
|
4674
4778
|
return changed;
|
|
4675
4779
|
}
|
|
4676
|
-
pruneInvisibleAt(now, lifecycle) {
|
|
4677
|
-
|
|
4780
|
+
pruneInvisibleAt(now, ctx, lifecycle) {
|
|
4781
|
+
const removals = this.#store.findInvisible(this.#snapshot);
|
|
4782
|
+
return this.#settleTransitions(removals, now, lifecycle, this.#resolveNaturalBoundarySnap(removals, now, ctx, lifecycle));
|
|
4678
4783
|
}
|
|
4679
4784
|
reset() {
|
|
4680
4785
|
this.#store.reset();
|
|
@@ -4686,16 +4791,58 @@ var TransitionController = class {
|
|
|
4686
4791
|
if (animation == null) return;
|
|
4687
4792
|
if (getProgress(animation.startTime, animation.duration, now) >= 1) this.#viewportTranslateAnimation = void 0;
|
|
4688
4793
|
}
|
|
4689
|
-
#settleTransitions(removals, now, lifecycle) {
|
|
4794
|
+
#settleTransitions(removals, now, lifecycle, boundarySnap) {
|
|
4690
4795
|
if (removals.length === 0) return false;
|
|
4691
4796
|
const anchor = lifecycle.captureVisualAnchor(now);
|
|
4797
|
+
const completedDeleteIndices = [];
|
|
4692
4798
|
for (const { item, transition } of removals) {
|
|
4799
|
+
if (transition.kind === "delete") {
|
|
4800
|
+
const index = lifecycle.readItemIndex(item);
|
|
4801
|
+
if (index >= 0) completedDeleteIndices.push(index);
|
|
4802
|
+
}
|
|
4693
4803
|
this.#store.delete(item);
|
|
4694
4804
|
if (transition.kind === "delete") lifecycle.onDeleteComplete(item);
|
|
4695
4805
|
}
|
|
4696
|
-
if (anchor != null && Number.isFinite(anchor)) lifecycle.restoreVisualAnchor(anchor);
|
|
4806
|
+
if (anchor != null && Number.isFinite(anchor)) lifecycle.restoreVisualAnchor(remapAnchorAfterDeletes(anchor, completedDeleteIndices));
|
|
4807
|
+
if (boundarySnap != null) lifecycle.snapItemToViewportBoundary(boundarySnap.item, boundarySnap.boundary);
|
|
4697
4808
|
return true;
|
|
4698
4809
|
}
|
|
4810
|
+
#resolveNaturalBoundarySnap(removals, now, ctx, lifecycle) {
|
|
4811
|
+
const previousState = this.#snapshot.previousState;
|
|
4812
|
+
const drawnRange = this.#snapshot.readDrawnIndexRange();
|
|
4813
|
+
if (previousState == null || drawnRange == null) return;
|
|
4814
|
+
const naturalIndices = [];
|
|
4815
|
+
for (const { item, transition } of removals) {
|
|
4816
|
+
if (transition.kind !== "update" && transition.kind !== "delete") continue;
|
|
4817
|
+
const index = lifecycle.readItemIndex(item);
|
|
4818
|
+
if (index < 0 || !this.#snapshot.wasVisible(item)) return;
|
|
4819
|
+
if (this.#isTransitionVisibleInState(index, previousState, now, this.#snapshot.currentExtraShift, ctx)) return;
|
|
4820
|
+
naturalIndices.push(index);
|
|
4821
|
+
}
|
|
4822
|
+
if (naturalIndices.length === 0) return;
|
|
4823
|
+
if (naturalIndices.every((index) => index < drawnRange.minIndex)) {
|
|
4824
|
+
const item = this.#snapshot.readBoundaryItem("top");
|
|
4825
|
+
return item == null ? void 0 : {
|
|
4826
|
+
item,
|
|
4827
|
+
boundary: "top"
|
|
4828
|
+
};
|
|
4829
|
+
}
|
|
4830
|
+
if (naturalIndices.every((index) => index > drawnRange.maxIndex)) {
|
|
4831
|
+
const item = this.#snapshot.readBoundaryItem("bottom");
|
|
4832
|
+
return item == null ? void 0 : {
|
|
4833
|
+
item,
|
|
4834
|
+
boundary: "bottom"
|
|
4835
|
+
};
|
|
4836
|
+
}
|
|
4837
|
+
}
|
|
4838
|
+
#isTransitionVisibleInState(index, state, now, extraShift, ctx) {
|
|
4839
|
+
const solution = ctx.resolveVisibleWindowForState(state, now);
|
|
4840
|
+
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;
|
|
4843
|
+
}
|
|
4844
|
+
return false;
|
|
4845
|
+
}
|
|
4699
4846
|
};
|
|
4700
4847
|
//#endregion
|
|
4701
4848
|
//#region src/renderer/virtualized/base.ts
|
|
@@ -4722,8 +4869,7 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
|
|
|
4722
4869
|
getDefaultJumpBlock: this._getDefaultJumpBlock.bind(this),
|
|
4723
4870
|
getTargetAnchor: this._getTargetAnchor.bind(this),
|
|
4724
4871
|
clampItemIndex: this._clampItemIndex.bind(this),
|
|
4725
|
-
getItemHeight: this._getItemHeight.bind(this)
|
|
4726
|
-
canAutoFollowBoundaryInsert: (direction, count, position, offset) => this.#transitionController.canAutoFollowBoundaryInsert(direction, count, position, offset)
|
|
4872
|
+
getItemHeight: this._getItemHeight.bind(this)
|
|
4727
4873
|
});
|
|
4728
4874
|
subscribeListState(options.list, this, (owner, change) => {
|
|
4729
4875
|
owner.#handleListStateChange(change);
|
|
@@ -4767,7 +4913,12 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
|
|
|
4767
4913
|
captureVisibleItemSnapshot: (solution, extraShift) => this._captureVisibleItemSnapshot(solution, extraShift),
|
|
4768
4914
|
pruneTransitionAnimations: (window, frameNow) => this._pruneTransitionAnimations(window, frameNow)
|
|
4769
4915
|
});
|
|
4916
|
+
const autoFollowCapabilities = this.#jumpController.syncAutoFollowCapabilities(this._readAutoFollowCapabilities(frame.solution.window, frame.viewportTranslateY));
|
|
4770
4917
|
const requestRedraw = this._renderVisibleWindow(frame.solution.window, feedback, frame.viewportTranslateY);
|
|
4918
|
+
if (feedback != null) {
|
|
4919
|
+
feedback.canAutoFollowTop = autoFollowCapabilities.top;
|
|
4920
|
+
feedback.canAutoFollowBottom = autoFollowCapabilities.bottom;
|
|
4921
|
+
}
|
|
4771
4922
|
this._commitListState(frame.solution.normalizedState);
|
|
4772
4923
|
return this._finishRender(keepAnimating || requestRedraw || frame.requestSettleRedraw);
|
|
4773
4924
|
}
|
|
@@ -4783,6 +4934,7 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
|
|
|
4783
4934
|
captureVisibleItemSnapshot: (solution, extraShift) => this._captureVisibleItemSnapshot(solution, extraShift),
|
|
4784
4935
|
pruneTransitionAnimations: (window, frameNow) => this._pruneTransitionAnimations(window, frameNow)
|
|
4785
4936
|
});
|
|
4937
|
+
this.#jumpController.syncAutoFollowCapabilities(this._readAutoFollowCapabilities(frame.solution.window, frame.viewportTranslateY));
|
|
4786
4938
|
return this._hittestVisibleWindow(frame.solution.window, test, frame.viewportTranslateY);
|
|
4787
4939
|
}
|
|
4788
4940
|
_readListState() {
|
|
@@ -4805,12 +4957,26 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
|
|
|
4805
4957
|
jumpTo(index, options = {}) {
|
|
4806
4958
|
this.#jumpController.jumpTo(index, options);
|
|
4807
4959
|
}
|
|
4960
|
+
/**
|
|
4961
|
+
* Scrolls the viewport to the visual top edge and arms top auto-follow immediately.
|
|
4962
|
+
*/
|
|
4963
|
+
jumpToTop(options = {}) {
|
|
4964
|
+
this.#jumpController.jumpToBoundary("top", options);
|
|
4965
|
+
}
|
|
4966
|
+
/**
|
|
4967
|
+
* Scrolls the viewport to the visual bottom edge and arms bottom auto-follow immediately.
|
|
4968
|
+
*/
|
|
4969
|
+
jumpToBottom(options = {}) {
|
|
4970
|
+
this.#jumpController.jumpToBoundary("bottom", options);
|
|
4971
|
+
}
|
|
4808
4972
|
_resetRenderFeedback(feedback) {
|
|
4809
4973
|
if (feedback == null) return;
|
|
4810
4974
|
feedback.minIdx = NaN;
|
|
4811
4975
|
feedback.maxIdx = NaN;
|
|
4812
4976
|
feedback.min = NaN;
|
|
4813
4977
|
feedback.max = NaN;
|
|
4978
|
+
feedback.canAutoFollowTop = false;
|
|
4979
|
+
feedback.canAutoFollowBottom = false;
|
|
4814
4980
|
}
|
|
4815
4981
|
_accumulateRenderFeedback(feedback, idx, top, height) {
|
|
4816
4982
|
const visibleRange = this._readVisibleRange(top, height);
|
|
@@ -4837,6 +5003,29 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
|
|
|
4837
5003
|
this._resetRenderFeedback(feedback);
|
|
4838
5004
|
return this._renderDrawList(window.drawList, window.shift + extraShift, feedback);
|
|
4839
5005
|
}
|
|
5006
|
+
_readAutoFollowCapabilities(window, extraShift = 0) {
|
|
5007
|
+
if (window.drawList.length === 0 || this.items.length === 0) return {
|
|
5008
|
+
top: false,
|
|
5009
|
+
bottom: false
|
|
5010
|
+
};
|
|
5011
|
+
let minIndex = Number.POSITIVE_INFINITY;
|
|
5012
|
+
let maxIndex = Number.NEGATIVE_INFINITY;
|
|
5013
|
+
let topMostY = Number.POSITIVE_INFINITY;
|
|
5014
|
+
let bottomMostY = Number.NEGATIVE_INFINITY;
|
|
5015
|
+
const effectiveShift = window.shift + extraShift;
|
|
5016
|
+
for (const { idx, offset, height } of window.drawList) {
|
|
5017
|
+
minIndex = Math.min(minIndex, idx);
|
|
5018
|
+
maxIndex = Math.max(maxIndex, idx);
|
|
5019
|
+
const y = offset + effectiveShift;
|
|
5020
|
+
topMostY = Math.min(topMostY, y);
|
|
5021
|
+
bottomMostY = Math.max(bottomMostY, y + height);
|
|
5022
|
+
}
|
|
5023
|
+
const viewportHeight = this.graphics.canvas.clientHeight;
|
|
5024
|
+
return {
|
|
5025
|
+
top: minIndex === 0 && topMostY >= -Number.EPSILON,
|
|
5026
|
+
bottom: maxIndex === this.items.length - 1 && bottomMostY <= viewportHeight + Number.EPSILON
|
|
5027
|
+
};
|
|
5028
|
+
}
|
|
4840
5029
|
_readVisibleRange(top, height) {
|
|
4841
5030
|
if (!Number.isFinite(top) || !Number.isFinite(height) || height <= 0) return;
|
|
4842
5031
|
const viewportHeight = this.graphics.canvas.clientHeight;
|
|
@@ -4849,7 +5038,7 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
|
|
|
4849
5038
|
};
|
|
4850
5039
|
}
|
|
4851
5040
|
_pruneTransitionAnimations(_window, now) {
|
|
4852
|
-
return this.#transitionController.pruneInvisibleAt(now, this.#getTransitionLifecycleAdapter());
|
|
5041
|
+
return this.#transitionController.pruneInvisibleAt(now, this.#getTransitionPlanningAdapter(), this.#getTransitionLifecycleAdapter());
|
|
4853
5042
|
}
|
|
4854
5043
|
_hittestVisibleWindow(window, test, extraShift = 0) {
|
|
4855
5044
|
for (const { value: item, offset, height } of window.drawList) {
|
|
@@ -4893,6 +5082,11 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
|
|
|
4893
5082
|
if (!Number.isFinite(anchor) || this.items.length <= 0) return;
|
|
4894
5083
|
this._applyAnchor(anchor);
|
|
4895
5084
|
}
|
|
5085
|
+
#snapItemToViewportBoundary(item, boundary) {
|
|
5086
|
+
const index = this.items.indexOf(item);
|
|
5087
|
+
if (index < 0) return;
|
|
5088
|
+
this._applyAnchor(this._getTargetAnchor(index, boundary === "top" ? "start" : "end"));
|
|
5089
|
+
}
|
|
4896
5090
|
_resolveItem(item, _index, now) {
|
|
4897
5091
|
return this.#transitionController.resolveItem(item, now, this.#getTransitionRenderAdapter(), this.#getTransitionLifecycleAdapter());
|
|
4898
5092
|
}
|
|
@@ -4903,7 +5097,9 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
|
|
|
4903
5097
|
return {
|
|
4904
5098
|
onDeleteComplete: this.#handleDeleteComplete.bind(this),
|
|
4905
5099
|
captureVisualAnchor: this._readAnchorAt.bind(this),
|
|
4906
|
-
restoreVisualAnchor: this._restoreAnchor.bind(this)
|
|
5100
|
+
restoreVisualAnchor: this._restoreAnchor.bind(this),
|
|
5101
|
+
readItemIndex: (item) => this.items.indexOf(item),
|
|
5102
|
+
snapItemToViewportBoundary: this.#snapItemToViewportBoundary.bind(this)
|
|
4907
5103
|
};
|
|
4908
5104
|
}
|
|
4909
5105
|
#getVirtualizedRuntime() {
|
|
@@ -4914,7 +5110,8 @@ var VirtualizedRenderer = class VirtualizedRenderer extends BaseRenderer {
|
|
|
4914
5110
|
renderItem: this.options.renderItem,
|
|
4915
5111
|
measureNode: this.measureRootNode.bind(this),
|
|
4916
5112
|
readVisibleRange: this._readVisibleRange.bind(this),
|
|
4917
|
-
resolveVisibleWindow: () => this._resolveVisibleWindow(getNow())
|
|
5113
|
+
resolveVisibleWindow: () => this._resolveVisibleWindow(getNow()),
|
|
5114
|
+
resolveVisibleWindowForState: (state, now) => this._resolveVisibleWindowForState(state, now)
|
|
4918
5115
|
};
|
|
4919
5116
|
}
|
|
4920
5117
|
#getTransitionRenderAdapter() {
|