@aiquants/virtualscroll 3.11.0 → 3.11.2
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/CHANGELOG.md +87 -0
- package/README.md +12 -7
- package/dist/ScrollBar.d.ts.map +1 -1
- package/dist/ScrollPane.d.ts.map +1 -1
- package/dist/TapScrollCircle.d.ts.map +1 -1
- package/dist/VirtualGrid.d.cts +18 -10
- package/dist/VirtualGrid.d.ts +18 -10
- package/dist/VirtualGrid.d.ts.map +1 -1
- package/dist/VirtualScroll.d.ts.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +2408 -2251
- package/dist/residualQuantizer.d.ts.map +1 -1
- package/dist/useFenwickMapTree.d.cts +48 -0
- package/dist/useFenwickMapTree.d.ts +48 -0
- package/dist/useFenwickMapTree.d.ts.map +1 -1
- package/dist/useGridTapScroll.d.ts.map +1 -1
- package/dist/useWheelBridge.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/ScrollBar.tsx +96 -37
- package/src/ScrollPane.tsx +74 -15
- package/src/TapScrollCircle.tsx +8 -0
- package/src/VirtualGrid.tsx +191 -94
- package/src/VirtualScroll.tsx +65 -73
- package/src/residualQuantizer.ts +14 -0
- package/src/useFenwickMapTree.ts +82 -1
- package/src/useGridTapScroll.ts +35 -9
- package/src/useLruCache.ts +4 -4
- package/src/useWheelBridge.ts +14 -0
package/src/VirtualScroll.tsx
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import React, { forwardRef, type ReactNode, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo,
|
|
1
|
+
import React, { forwardRef, type ReactNode, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react"
|
|
2
2
|
import { type DevicePixelSnapEdge, snapToDevicePixelGrid, usePaintingDevicePixelRatio } from "./devicePixelGrid.ts"
|
|
3
3
|
import { resolveVirtualScrollLabels, type VirtualScrollLabelOverrides, type VirtualScrollLocale } from "./labels.ts"
|
|
4
4
|
import { Logger } from "./logger.ts"
|
|
5
5
|
import { ScrollPane, type ScrollPaneContentInsets, type ScrollPaneHandle, type ScrollPaneProps } from "./ScrollPane.tsx"
|
|
6
|
-
import { useFenwickMapTree } from "./useFenwickMapTree.ts"
|
|
6
|
+
import { FENWICK_LOOKUP_ONLY, useFenwickMapTree, useFenwickTreeRevision } from "./useFenwickMapTree.ts"
|
|
7
7
|
import { getAxisScale, keepFocusOnPress, minmax } from "./utils.ts"
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -906,17 +906,6 @@ const ItemsWrapper = ({ translateY, snapEdge, children, boundaryRef }: ItemsWrap
|
|
|
906
906
|
*/
|
|
907
907
|
const DEFAULT_HORIZONTAL_KEY_STEP = 40
|
|
908
908
|
|
|
909
|
-
/**
|
|
910
|
-
* Advances VirtualScroll's tree revision by one (the reducer of the revision that the memos reading the row-height tree
|
|
911
|
-
* depend on).
|
|
912
|
-
*
|
|
913
|
-
* VirtualScroll の木の版数を 1 つ進める処理 (行の高さの木を読む memo が依存する版数の reducer)。
|
|
914
|
-
*
|
|
915
|
-
* @param revision - The current revision / 今の版数
|
|
916
|
-
* @returns The next revision / 次の版数
|
|
917
|
-
*/
|
|
918
|
-
const nextTreeRevision = (revision: number): number => revision + 1
|
|
919
|
-
|
|
920
909
|
/**
|
|
921
910
|
* Converts a numeric size into a non-negative bigint for large collection handling.
|
|
922
911
|
*
|
|
@@ -947,9 +936,6 @@ export type VisibleStartRow = {
|
|
|
947
936
|
*/
|
|
948
937
|
type VisibleStartRowTree = Pick<ReturnType<typeof useFenwickMapTree>, "findIndexAtOrAfter" | "prefixSum">
|
|
949
938
|
|
|
950
|
-
/** Tree reads that only look the rows up, never materialising them / 行を引くだけで具現化しない木の読み方 */
|
|
951
|
-
const LOOKUP_ONLY = { materializeOption: { materialize: false } } as const
|
|
952
|
-
|
|
953
939
|
/**
|
|
954
940
|
* Resolves the first visible row at a logical scroll position by the visible-start boundary rule — the one rule that
|
|
955
941
|
* `scrollTo` pins, `getScrollAnchor` reports, `updateItemSize` compensates above and `computeRenderingRanges` renders from
|
|
@@ -981,9 +967,9 @@ const LOOKUP_ONLY = { materializeOption: { materialize: false } } as const
|
|
|
981
967
|
*/
|
|
982
968
|
export const resolveVisibleStartRow = (fenwickTree: VisibleStartRowTree, position: number, itemCount: number): VisibleStartRow => {
|
|
983
969
|
const lastIndex = itemCount - 1
|
|
984
|
-
const found = fenwickTree.findIndexAtOrAfter(position,
|
|
970
|
+
const found = fenwickTree.findIndexAtOrAfter(position, FENWICK_LOOKUP_ONLY)
|
|
985
971
|
if (found.index === -1 || found.index > lastIndex || found.cumulative === undefined || found.currentValue === undefined) {
|
|
986
|
-
const last = fenwickTree.prefixSum(lastIndex,
|
|
972
|
+
const last = fenwickTree.prefixSum(lastIndex, FENWICK_LOOKUP_ONLY)
|
|
987
973
|
return { index: lastIndex, top: last.cumulative - last.currentValue }
|
|
988
974
|
}
|
|
989
975
|
if (found.cumulative === position && found.index < lastIndex) {
|
|
@@ -1059,11 +1045,11 @@ export const computeRenderingRangesHuge = (effectiveScrollPosition: number, view
|
|
|
1059
1045
|
// number へ安全に変換できない領域では木のクエリが不正確になるため打ち切る
|
|
1060
1046
|
return null
|
|
1061
1047
|
}
|
|
1062
|
-
const { cumulative: runBottom } = fenwickTree.prefixSum(lastNumber,
|
|
1048
|
+
const { cumulative: runBottom } = fenwickTree.prefixSum(lastNumber, FENWICK_LOOKUP_ONLY)
|
|
1063
1049
|
if (!Number.isFinite(runBottom)) {
|
|
1064
1050
|
return null
|
|
1065
1051
|
}
|
|
1066
|
-
const { index: jumpIndex } = fenwickTree.findIndexAtOrAfter(runBottom + 0.5,
|
|
1052
|
+
const { index: jumpIndex } = fenwickTree.findIndexAtOrAfter(runBottom + 0.5, FENWICK_LOOKUP_ONLY)
|
|
1067
1053
|
if (jumpIndex === -1) {
|
|
1068
1054
|
return null
|
|
1069
1055
|
}
|
|
@@ -1138,11 +1124,10 @@ export const computeRenderingRangesHuge = (effectiveScrollPosition: number, view
|
|
|
1138
1124
|
if (!Number.isSafeInteger(indexNumber)) {
|
|
1139
1125
|
break
|
|
1140
1126
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
}
|
|
1145
|
-
const { index: jumpIndex } = fenwickTree.findIndexAtOrAfter(runBottom - 0.5, LOOKUP_ONLY)
|
|
1127
|
+
// 後方の行は先頭の可視行より上にあり、木の値は負にならないので、その累積は先頭の可視行の上端 (有限の位置以下) を
|
|
1128
|
+
// 越えない。前方と違い、累積が溢れた行へは後方の走査が届かない
|
|
1129
|
+
const { cumulative: runBottom } = fenwickTree.prefixSum(indexNumber, FENWICK_LOOKUP_ONLY)
|
|
1130
|
+
const { index: jumpIndex } = fenwickTree.findIndexAtOrAfter(runBottom - 0.5, FENWICK_LOOKUP_ONLY)
|
|
1146
1131
|
const jumpBig = jumpIndex === -1 ? -1n : toSafeBigInt(jumpIndex)
|
|
1147
1132
|
if (jumpBig >= 0n && jumpBig < backwardStart) {
|
|
1148
1133
|
// 0 行の連続を飛び越えて直前の非 0 行から走査を続行する
|
|
@@ -1219,8 +1204,8 @@ export const computeRenderingRanges = (scrollPosition: number, viewportSize: num
|
|
|
1219
1204
|
if (currentHeight === 0) {
|
|
1220
1205
|
zeroRun++
|
|
1221
1206
|
if (zeroRun > ZERO_HEIGHT_RUN_LIMIT) {
|
|
1222
|
-
const { cumulative: runBottom } = fenwickTree.prefixSum(forwardCursor - 1,
|
|
1223
|
-
const { index: jumpIndex } = Number.isFinite(runBottom) ? fenwickTree.findIndexAtOrAfter(runBottom + 0.5,
|
|
1207
|
+
const { cumulative: runBottom } = fenwickTree.prefixSum(forwardCursor - 1, FENWICK_LOOKUP_ONLY)
|
|
1208
|
+
const { index: jumpIndex } = Number.isFinite(runBottom) ? fenwickTree.findIndexAtOrAfter(runBottom + 0.5, FENWICK_LOOKUP_ONLY) : { index: -1 }
|
|
1224
1209
|
if (jumpIndex > forwardCursor) {
|
|
1225
1210
|
// 0 行の連続を飛び越えて次の非 0 行から走査を続行する
|
|
1226
1211
|
forwardCursor = jumpIndex
|
|
@@ -1258,9 +1243,10 @@ export const computeRenderingRanges = (scrollPosition: number, viewportSize: num
|
|
|
1258
1243
|
if (itemHeight === 0) {
|
|
1259
1244
|
backwardZeroRun++
|
|
1260
1245
|
if (backwardZeroRun > ZERO_HEIGHT_RUN_LIMIT) {
|
|
1261
|
-
// 連続 0 行の直前にある非 0 行へ後方ジャンプ (木上の累積位置 -0.5 で探索)
|
|
1262
|
-
|
|
1263
|
-
const {
|
|
1246
|
+
// 連続 0 行の直前にある非 0 行へ後方ジャンプ (木上の累積位置 -0.5 で探索)。後方の行は先頭の可視行より上にあり、
|
|
1247
|
+
// 木の値は負にならないので、その累積は先頭の可視行の上端 (有限の位置以下) を越えない
|
|
1248
|
+
const { cumulative: runBottom } = fenwickTree.prefixSum(backwardCursor, FENWICK_LOOKUP_ONLY)
|
|
1249
|
+
const { index: jumpIndex } = fenwickTree.findIndexAtOrAfter(runBottom - 0.5, FENWICK_LOOKUP_ONLY)
|
|
1264
1250
|
if (jumpIndex !== -1 && jumpIndex < backwardCursor) {
|
|
1265
1251
|
// 0 行の連続を飛び越えて直前の非 0 行から走査を続行する
|
|
1266
1252
|
backwardCursor = jumpIndex
|
|
@@ -1315,8 +1301,8 @@ type OnRangeChangeCallback = (range: VirtualScrollRange) => void
|
|
|
1315
1301
|
* @param throttleMs Minimum time between invocations in ms / 実行間の最小時間(ミリ秒)
|
|
1316
1302
|
* @param invoke Wrapper function to execute the callback (e.g. for argument unwrapping) / コールバックを実行するラッパー関数
|
|
1317
1303
|
*/
|
|
1318
|
-
const useThrottledInvoker = <Callback, Payload>(callbackRef: React.MutableRefObject<Callback | undefined>, throttleMs: number
|
|
1319
|
-
const throttle = Math.max(0, throttleMs
|
|
1304
|
+
const useThrottledInvoker = <Callback, Payload>(callbackRef: React.MutableRefObject<Callback | undefined>, throttleMs: number, invoke: (callback: Callback, payload: Payload) => void) => {
|
|
1305
|
+
const throttle = Math.max(0, throttleMs)
|
|
1320
1306
|
// last: 最終実行時刻, id: RAF ID, arg: 待機中の引数
|
|
1321
1307
|
const state = useRef({ last: 0, id: null as number | null, arg: null as Payload | null }).current
|
|
1322
1308
|
|
|
@@ -1343,6 +1329,12 @@ const useThrottledInvoker = <Callback, Payload>(callbackRef: React.MutableRefObj
|
|
|
1343
1329
|
[state, callbackRef, invoke],
|
|
1344
1330
|
)
|
|
1345
1331
|
|
|
1332
|
+
/**
|
|
1333
|
+
* Schedules one invocation with the latest payload: at most once per animation frame, and not before `throttleMs` has passed
|
|
1334
|
+
* since the previous one; a payload that arrives while one is pending replaces it.
|
|
1335
|
+
* 最新のペイロードでの 1 回の実行を予約する処理。アニメーションのフレームごとに多くて 1 回で、前回から `throttleMs` が経つまでは実行しない。
|
|
1336
|
+
* 予約中に届いたペイロードは前のものを置き換える。
|
|
1337
|
+
*/
|
|
1346
1338
|
return useCallback(
|
|
1347
1339
|
(payload: Payload) => {
|
|
1348
1340
|
state.arg = payload
|
|
@@ -1572,34 +1564,10 @@ const VirtualScrollInner = <T,>(
|
|
|
1572
1564
|
const fenwickTree = useFenwickMapTree(itemCount, getItemHeight, fenwickTreeOptions)
|
|
1573
1565
|
|
|
1574
1566
|
// ❗ 木は同一性を保ったまま描画の外で書き換わる (updateItemSize・高さの照合・scrollToIndex の具現化)。接頭辞和の変化は総和が
|
|
1575
|
-
// 変わらなくても (差の和が 0 の測り直しの組)
|
|
1576
|
-
//
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
/**
|
|
1580
|
-
* Runs one change of the row-height tree made outside render (`updateItemSize`, the height reconciliation, the
|
|
1581
|
-
* materialisation of `scrollToIndex`) and, when the tree's revision moved (`FenwickMapTree.revision`), advances the tree
|
|
1582
|
-
* revision, so the next commit re-places the rendered rows and re-resolves the rendering range whether or not the total
|
|
1583
|
-
* changed.
|
|
1584
|
-
*
|
|
1585
|
-
* 描画の外で行う行の高さの木の変更を 1 回実行し (`updateItemSize`・高さの照合・`scrollToIndex` の具現化)、木の版
|
|
1586
|
-
* (`FenwickMapTree.revision`) が動いたら木の版数を進める処理。総和が変わったかどうかによらず、次の確定が描いた行を置き直し、
|
|
1587
|
-
* 描画範囲を解決し直す。
|
|
1588
|
-
*
|
|
1589
|
-
* @param change - The change of the tree / 木の変更
|
|
1590
|
-
* @returns What `change` returned / `change` の戻り値
|
|
1591
|
-
*/
|
|
1592
|
-
const changeTree = useCallback(
|
|
1593
|
-
<Result,>(change: () => Result): Result => {
|
|
1594
|
-
const revisionBefore = fenwickTree.revision
|
|
1595
|
-
const result = change()
|
|
1596
|
-
if (fenwickTree.revision !== revisionBefore) {
|
|
1597
|
-
advanceTreeRevision()
|
|
1598
|
-
}
|
|
1599
|
-
return result
|
|
1600
|
-
},
|
|
1601
|
-
[fenwickTree],
|
|
1602
|
-
)
|
|
1567
|
+
// 変わらなくても (差の和が 0 の測り直しの組) 描いた行の上端と高さ、描画範囲を変えるので、描画の外の変更はどれも changeTree を
|
|
1568
|
+
// 通し、木を読むどの memo も treeRevision に依存させる。総和 (中身の寸法) は相殺で変わらず、同じ値の書き込みは React が捨てるため、
|
|
1569
|
+
// 変化の合図にならない
|
|
1570
|
+
const { revision: treeRevision, change: changeTree } = useFenwickTreeRevision(fenwickTree)
|
|
1603
1571
|
|
|
1604
1572
|
const [initialValues] = useState(() => {
|
|
1605
1573
|
let position = 0
|
|
@@ -1613,19 +1581,19 @@ const VirtualScrollInner = <T,>(
|
|
|
1613
1581
|
const safeIndexFrom = minmax(safeIndex - overscanCount * 2, 0, itemCount - 1)
|
|
1614
1582
|
const safeIndexTo = minmax(safeIndex + overscanCount * 2, 0, itemCount - 1)
|
|
1615
1583
|
const options = safeIndex > 0 || anchorOffsetPx > 0 ? { materializeOption: { materialize: true, ranges: [{ from: safeIndexFrom, to: safeIndexTo }] } } : undefined
|
|
1616
|
-
const { cumulative,
|
|
1584
|
+
const { cumulative, currentValue } = fenwickTree.prefixSum(safeIndex, options)
|
|
1617
1585
|
const logicalOffset = Math.max(cumulative - currentValue + anchorOffsetPx, 0)
|
|
1618
1586
|
position = toPanePositionWithInset(logicalOffset, resolvedInsets.top)
|
|
1619
|
-
total =
|
|
1587
|
+
total = fenwickTree.getTotal()
|
|
1620
1588
|
} else if (typeof initialScrollIndex === "number") {
|
|
1621
1589
|
const safeIndex = minmax(initialScrollIndex, 0, itemCount - 1)
|
|
1622
1590
|
const safeIndexFrom = minmax(safeIndex - overscanCount * 2, 0, itemCount - 1)
|
|
1623
1591
|
const safeIndexTo = minmax(safeIndex + overscanCount * 2, 0, itemCount - 1)
|
|
1624
1592
|
const options = initialScrollIndex > 0 ? { materializeOption: { materialize: true, ranges: [{ from: safeIndexFrom, to: safeIndexTo }] } } : undefined
|
|
1625
|
-
const { cumulative,
|
|
1593
|
+
const { cumulative, currentValue } = fenwickTree.prefixSum(initialScrollIndex, options)
|
|
1626
1594
|
const logicalOffset = Math.max(cumulative - currentValue, 0)
|
|
1627
1595
|
position = toPanePositionWithInset(logicalOffset, resolvedInsets.top)
|
|
1628
|
-
total =
|
|
1596
|
+
total = fenwickTree.getTotal()
|
|
1629
1597
|
} else if (typeof initialScrollOffset === "number") {
|
|
1630
1598
|
position = toPanePositionWithInset(Math.max(initialScrollOffset, 0), resolvedInsets.top)
|
|
1631
1599
|
total = fenwickTree.getTotal()
|
|
@@ -2163,7 +2131,7 @@ const VirtualScrollInner = <T,>(
|
|
|
2163
2131
|
if (pendingVisibleStartIndexRef.current !== null) {
|
|
2164
2132
|
const { index, align, offset } = pendingVisibleStartIndexRef.current
|
|
2165
2133
|
const safeIndex = sanitizeIndex(index, itemCount)
|
|
2166
|
-
const { cumulative: itemBottom, currentValue: itemHeight } = fenwickTree.prefixSum(safeIndex,
|
|
2134
|
+
const { cumulative: itemBottom, currentValue: itemHeight } = fenwickTree.prefixSum(safeIndex, FENWICK_LOOKUP_ONLY)
|
|
2167
2135
|
|
|
2168
2136
|
if (itemBottom !== undefined && itemHeight !== undefined) {
|
|
2169
2137
|
const itemTop = Math.max(itemBottom - itemHeight, 0)
|
|
@@ -2349,7 +2317,7 @@ const VirtualScrollInner = <T,>(
|
|
|
2349
2317
|
throw new RangeError(`[VirtualScroll] scrollToIndex: align "nearest" takes no offset (it lands the row at the edge of the band a visible scroll-to-edge pill leaves); received an offset of type ${typeof offset}.`)
|
|
2350
2318
|
}
|
|
2351
2319
|
// 見せるかどうかは今の木で決める。描画の窓とその周りの行は照合で実際の高さを持ち、窓から遠い行は表示域の中にない
|
|
2352
|
-
const row = fenwickTree.prefixSum(safeIndex,
|
|
2320
|
+
const row = fenwickTree.prefixSum(safeIndex, FENWICK_LOOKUP_ONLY)
|
|
2353
2321
|
const reveal = resolveRevealAlignment({ top: row.cumulative - row.currentValue, bottom: row.cumulative }, toLogicalPositionWithInset(latestScrollPositionRef.current, resolvedInsets.top), viewportSize, readObscuredInsets())
|
|
2354
2322
|
if (reveal === null) {
|
|
2355
2323
|
return
|
|
@@ -2978,7 +2946,7 @@ const VirtualScrollInner = <T,>(
|
|
|
2978
2946
|
|
|
2979
2947
|
const safeRenderingStartIndex = sanitizeIndex(renderingStartIndex, itemCount)
|
|
2980
2948
|
const safeRenderingEndIndex = sanitizeIndex(renderingEndIndex, itemCount)
|
|
2981
|
-
const { cumulative, currentValue: oldHeight } = fenwickTree.prefixSum(safeRenderingStartIndex,
|
|
2949
|
+
const { cumulative, currentValue: oldHeight } = fenwickTree.prefixSum(safeRenderingStartIndex, FENWICK_LOOKUP_ONLY)
|
|
2982
2950
|
const startPosition = cumulative - oldHeight
|
|
2983
2951
|
|
|
2984
2952
|
// 量子化アンカーの再基準化: 描画ウィンドウ先頭が現アンカーから一定距離を超えて離れたときだけ
|
|
@@ -3069,13 +3037,12 @@ const VirtualScrollInner = <T,>(
|
|
|
3069
3037
|
}
|
|
3070
3038
|
|
|
3071
3039
|
// 照合した行の上端は描画の中で補ってあるが、木から導いた値 (getRange の総高さなど) は古い木のまま。総和が変わらない
|
|
3072
|
-
//
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
return
|
|
3076
|
-
}
|
|
3040
|
+
// 変化でも、次の確定で木から導き直させる。ここから先は同期で、上の確認の後にアンマウントは挟まらない
|
|
3041
|
+
changeTree(() => fenwickTree.updates(toUpdateHeights))
|
|
3042
|
+
const total = fenwickTree.getTotal()
|
|
3077
3043
|
writeContentSize(total)
|
|
3078
3044
|
Logger.debug("[VirtualScroll] Updated heights for items", toUpdateHeights, "New total height:", total)
|
|
3045
|
+
// 並行の描画では、初回の描画のマイクロタスクがその確定 (ペインの取り付け) より先に走ることがある
|
|
3079
3046
|
const panePosition = scrollPaneRef.current?.getScrollPosition() ?? latestScrollPositionRef.current
|
|
3080
3047
|
|
|
3081
3048
|
if (shiftAmount !== 0) {
|
|
@@ -3139,7 +3106,7 @@ const VirtualScrollInner = <T,>(
|
|
|
3139
3106
|
*/
|
|
3140
3107
|
const renderVisibleItems = useCallback(
|
|
3141
3108
|
(currentScrollPosition: number) => {
|
|
3142
|
-
const shouldUseThrottledPosition =
|
|
3109
|
+
const shouldUseThrottledPosition = callbackThrottleMs > 0
|
|
3143
3110
|
const diff = Math.abs(currentScrollPosition - scrollPosition)
|
|
3144
3111
|
const rawEffectiveScrollPosition = shouldUseThrottledPosition && diff > 0.5 ? scrollPosition : currentScrollPosition
|
|
3145
3112
|
const effectiveScrollPosition = toLogicalPositionWithInset(rawEffectiveScrollPosition, resolvedInsets.top)
|
|
@@ -3273,21 +3240,46 @@ const VirtualScrollInner = <T,>(
|
|
|
3273
3240
|
useImperativeHandle(
|
|
3274
3241
|
ref,
|
|
3275
3242
|
() => ({
|
|
3243
|
+
/**
|
|
3244
|
+
* Reads the logical scroll position (the pane's position less the top inset), or the `-1` sentinel while the pane is
|
|
3245
|
+
* unconnected.
|
|
3246
|
+
* 論理スクロール位置 (ペインの位置から上のインセットを引いたもの) を読む処理。ペインが未接続の間は番兵の `-1`。
|
|
3247
|
+
*/
|
|
3276
3248
|
getScrollPosition: () => {
|
|
3277
3249
|
// ❗ 論理座標で返す (2.0.0)。ペイン座標を返す実装へ戻さないこと: 消費者の換算コードが
|
|
3278
3250
|
// 座標混在バグの温床になる (この統一が本質修正)。inset は走行中も最新を ref から読む
|
|
3279
3251
|
const panePosition = scrollPaneRef.current?.getScrollPosition()
|
|
3280
3252
|
return typeof panePosition === "number" ? toLogicalPositionWithInset(panePosition, resolvedInsetsTopRef.current) : -1
|
|
3281
3253
|
},
|
|
3254
|
+
/**
|
|
3255
|
+
* Reads the pane's content size (insets included), or the `-1` sentinel while the pane is unconnected.
|
|
3256
|
+
* ペインの中身の寸法 (インセット込み) を読む処理。ペインが未接続の間は番兵の `-1`。
|
|
3257
|
+
*/
|
|
3282
3258
|
getContentSize: () => scrollPaneRef.current?.getContentSize() ?? -1,
|
|
3259
|
+
/**
|
|
3260
|
+
* Reads the pane's viewport size, or the `-1` sentinel while the pane is unconnected.
|
|
3261
|
+
* ペインの表示域の寸法を読む処理。ペインが未接続の間は番兵の `-1`。
|
|
3262
|
+
*/
|
|
3283
3263
|
getViewportSize: () => scrollPaneRef.current?.getViewportSize() ?? -1,
|
|
3284
3264
|
scrollTo: scrollToHandle,
|
|
3285
3265
|
scrollBy,
|
|
3286
3266
|
applyWheel,
|
|
3287
3267
|
scrollToIndex,
|
|
3268
|
+
/**
|
|
3269
|
+
* Reads the total of the row-height tree (insets excluded).
|
|
3270
|
+
* 行の高さの木の合計 (インセットを除く) を読む処理。
|
|
3271
|
+
*/
|
|
3288
3272
|
getFenwickTreeTotalHeight: () => fenwickTree.getTotal(),
|
|
3273
|
+
/**
|
|
3274
|
+
* Reads the number of rows the row-height tree holds.
|
|
3275
|
+
* 行の高さの木が持つ行の数を読む処理。
|
|
3276
|
+
*/
|
|
3289
3277
|
getFenwickSize: () => fenwickTree.getSize(),
|
|
3290
3278
|
focusItemAtIndex,
|
|
3279
|
+
/**
|
|
3280
|
+
* Reads the range the last commit published (one render behind).
|
|
3281
|
+
* 最後の確定が公開した範囲 (描画 1 回ぶん遅れる) を読む処理。
|
|
3282
|
+
*/
|
|
3291
3283
|
getRange: () => currentRangeRef.current,
|
|
3292
3284
|
getScrollAnchor,
|
|
3293
3285
|
updateItemSize,
|
package/src/residualQuantizer.ts
CHANGED
|
@@ -105,6 +105,12 @@ export const createResidualQuantizer = (options: ResidualQuantizerOptions): Resi
|
|
|
105
105
|
let residue = 0
|
|
106
106
|
|
|
107
107
|
return {
|
|
108
|
+
/**
|
|
109
|
+
* Adds a delta to the residue and emits the whole quanta it holds (a non-finite delta, or one beyond
|
|
110
|
+
* `Number.MAX_SAFE_INTEGER`, emits 0 and leaves the residue as it is).
|
|
111
|
+
* 差分を残差へ足し、残差が持つ量子の整数倍を放出する処理 (非有限の差分と `Number.MAX_SAFE_INTEGER` を越える差分は 0 を放出し、
|
|
112
|
+
* 残差を変えない)。
|
|
113
|
+
*/
|
|
108
114
|
push: (delta: number): number => {
|
|
109
115
|
// 非有限デルタは残差を汚さず 0 を返す (蓄積器に NaN が入ると以後全放出が死ぬ)。
|
|
110
116
|
// ❗ |delta| > Number.MAX_SAFE_INTEGER も同じ契約で見送る: 2^53 超では
|
|
@@ -125,7 +131,15 @@ export const createResidualQuantizer = (options: ResidualQuantizerOptions): Resi
|
|
|
125
131
|
residue -= emitted
|
|
126
132
|
return emitted
|
|
127
133
|
},
|
|
134
|
+
/**
|
|
135
|
+
* Reads the residue not yet emitted.
|
|
136
|
+
* まだ放出していない残差を読む処理。
|
|
137
|
+
*/
|
|
128
138
|
peekResidue: () => residue,
|
|
139
|
+
/**
|
|
140
|
+
* Drops the residue.
|
|
141
|
+
* 残差を捨てる処理。
|
|
142
|
+
*/
|
|
129
143
|
reset: () => {
|
|
130
144
|
residue = 0
|
|
131
145
|
},
|
package/src/useFenwickMapTree.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* それを管理するための React フック `useFenwickMapTree` の提供。
|
|
12
12
|
* 動的なアイテムサイズを持つ仮想スクロールのシナリオに最適化されている。
|
|
13
13
|
*/
|
|
14
|
-
import { useRef } from "react"
|
|
14
|
+
import { useCallback, useReducer, useRef } from "react"
|
|
15
15
|
import { Logger } from "./logger.ts"
|
|
16
16
|
import { minmax } from "./utils.ts"
|
|
17
17
|
|
|
@@ -32,6 +32,17 @@ type MaterializeConfig = { materializeOption?: MaterializeOption }
|
|
|
32
32
|
type DeltaUpdate = { index: number; change: number }
|
|
33
33
|
type ValueUpdate = { index: number; value: number }
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* The options of a tree read that only looks rows up and never materialises them (`prefixSum`, `get`, `getTotal`,
|
|
37
|
+
* `findIndexAtOrAfter`, `findIndexAtOrBefore`): the read leaves the stored values, the size and `revision` as they are. One
|
|
38
|
+
* frozen value that every lookup passes, so no caller spells the option out. Internal to the package (not in the barrel).
|
|
39
|
+
*
|
|
40
|
+
* 行を引くだけで具現化しない木の読み取り (`prefixSum`・`get`・`getTotal`・`findIndexAtOrAfter`・`findIndexAtOrBefore`) の選択肢。
|
|
41
|
+
* 読み取りは保持する値・要素数・`revision` をそのまま残す。どの引き当ても渡す凍結した 1 つの値で、呼び出し側は選択肢を書き下さない。
|
|
42
|
+
* パッケージの内部用 (バレル非公開)。
|
|
43
|
+
*/
|
|
44
|
+
export const FENWICK_LOOKUP_ONLY = Object.freeze({ materializeOption: Object.freeze({ materialize: false }) })
|
|
45
|
+
|
|
35
46
|
/**
|
|
36
47
|
* Validates that `valueFn` returned a finite number, throwing otherwise. A single NaN/Infinity
|
|
37
48
|
* propagated into the tree poisons `tree`/`total` irrecoverably, so the materialization and
|
|
@@ -1432,3 +1443,73 @@ export const useFenwickMapTree = (size: number, valueOrFn: number | ((index: num
|
|
|
1432
1443
|
|
|
1433
1444
|
return tree
|
|
1434
1445
|
}
|
|
1446
|
+
|
|
1447
|
+
/**
|
|
1448
|
+
* What `useFenwickTreeRevision` returns: the revision the memos that read one tree depend on, and the one way to change that
|
|
1449
|
+
* tree outside render so that they see the change.
|
|
1450
|
+
*
|
|
1451
|
+
* `useFenwickTreeRevision` の戻り値。1 本の木を読む memo が依存する版数と、memo が変化に気付くように描画の外でその木を変える
|
|
1452
|
+
* ただ 1 つの方法。
|
|
1453
|
+
*/
|
|
1454
|
+
export type FenwickTreeRevision = {
|
|
1455
|
+
/** Advances by one per change made through `change` that moved the tree's revision / `change` を通した変更が木の版を動かすたびに 1 つ進む数 */
|
|
1456
|
+
readonly revision: number
|
|
1457
|
+
/** Runs one change of the tree made outside render and returns its result; advances `revision` only when the tree's revision moved / 描画の外で行う木の変更を 1 回実行してその結果を返し、木の版が動いたときだけ `revision` を進める処理 */
|
|
1458
|
+
readonly change: <Result>(mutate: () => Result) => Result
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
/**
|
|
1462
|
+
* Advances a component's tree revision by one (the reducer behind `FenwickTreeRevision["revision"]`).
|
|
1463
|
+
*
|
|
1464
|
+
* コンポーネントの木の版数を 1 つ進める処理 (`FenwickTreeRevision["revision"]` の reducer)。
|
|
1465
|
+
*
|
|
1466
|
+
* @param revision - The current revision / 今の版数
|
|
1467
|
+
* @returns The next revision / 次の版数
|
|
1468
|
+
*/
|
|
1469
|
+
const nextTreeRevision = (revision: number): number => revision + 1
|
|
1470
|
+
|
|
1471
|
+
/**
|
|
1472
|
+
* Ties the memos of a component to the contents of a Fenwick tree. The tree keeps its identity while its contents change
|
|
1473
|
+
* (`useFenwickMapTree`), so a memo that derives anything from its prefix sums (row tops, a rendering range, the placed
|
|
1474
|
+
* columns) cannot see a change through the tree itself, and the total is no signal either: a batch of changes that cancel out
|
|
1475
|
+
* leaves it as it was. Every change made outside render goes through `change`, which advances `revision` exactly when the
|
|
1476
|
+
* tree's own revision (`FenwickMapTree.revision`) moved, so the next commit recomputes the memos that depend on `revision`,
|
|
1477
|
+
* and a change that stores the same values (an update to the value a row already has, a materialisation that finds the stored
|
|
1478
|
+
* value) adds no commit. Changes made during render (a reset or a resize by `useFenwickMapTree`) come with new props, which the
|
|
1479
|
+
* memos depend on already. Internal to the package (not in the barrel).
|
|
1480
|
+
*
|
|
1481
|
+
* コンポーネントの memo を Fenwick 木の中身に結び付けるフック。木は中身が変わっても同一性を保つ (`useFenwickMapTree`) ので、
|
|
1482
|
+
* 接頭辞和から何か (行の上端・描画範囲・配置した列) を導く memo は木そのものからは変化に気付けず、総和も合図にならない (打ち消し合う
|
|
1483
|
+
* 変更の組は総和を変えない)。描画の外の変更はすべて `change` を通し、`change` は木自身の版 (`FenwickMapTree.revision`) が動いたとき
|
|
1484
|
+
* ちょうどに `revision` を進める。だから次の確定は `revision` に依存する memo を計算し直し、同じ値を保つ変更 (行が既に持つ値への更新・
|
|
1485
|
+
* 保持する値と同じ値を見つけた具現化) は確定を足さない。描画中の変更 (`useFenwickMapTree` のリセットと大きさの変更) は新しい props と
|
|
1486
|
+
* 一緒に来るので、memo は既にそれに依存している。パッケージの内部用 (バレル非公開)。
|
|
1487
|
+
*
|
|
1488
|
+
* @param tree - The tree whose changes the memos must see / memo が変化に気付くべき木
|
|
1489
|
+
* @returns The revision and the change runner / 版数と変更の実行関数
|
|
1490
|
+
*/
|
|
1491
|
+
export const useFenwickTreeRevision = (tree: FenwickMapTree): FenwickTreeRevision => {
|
|
1492
|
+
const [revision, advanceRevision] = useReducer(nextTreeRevision, 0)
|
|
1493
|
+
|
|
1494
|
+
/**
|
|
1495
|
+
* Runs one change of the tree and advances the revision when the tree's revision moved.
|
|
1496
|
+
*
|
|
1497
|
+
* 木の変更を 1 回実行し、木の版が動いたら版数を進める処理。
|
|
1498
|
+
*
|
|
1499
|
+
* @param mutate - The change of the tree / 木の変更
|
|
1500
|
+
* @returns What `mutate` returned / `mutate` の戻り値
|
|
1501
|
+
*/
|
|
1502
|
+
const change = useCallback(
|
|
1503
|
+
<Result>(mutate: () => Result): Result => {
|
|
1504
|
+
const revisionBefore = tree.revision
|
|
1505
|
+
const result = mutate()
|
|
1506
|
+
if (tree.revision !== revisionBefore) {
|
|
1507
|
+
advanceRevision()
|
|
1508
|
+
}
|
|
1509
|
+
return result
|
|
1510
|
+
},
|
|
1511
|
+
[tree],
|
|
1512
|
+
)
|
|
1513
|
+
|
|
1514
|
+
return { revision, change }
|
|
1515
|
+
}
|
package/src/useGridTapScroll.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* immobile 停止則・延長成長の再武装・fail-closed のキャンセル 3 系。
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { useCallback, useEffect, useRef, useState } from "react"
|
|
11
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"
|
|
12
12
|
import { computeTapScrollVelocity, type TapScrollAxisSpeedParams } from "./computeTapScrollVelocity.ts"
|
|
13
13
|
import { TAP_SCROLL_CANCEL_EVENT, TAP_SCROLL_MAX_FRAME_DELTA_SECONDS } from "./ScrollBar.tsx"
|
|
14
14
|
import type { TapScrollCircleDragState, TapScrollCircleHandle } from "./TapScrollCircle.tsx"
|
|
@@ -113,6 +113,8 @@ export const useGridTapScroll = (params: UseGridTapScrollParams): UseGridTapScro
|
|
|
113
113
|
const tapCircleHandleRef = useRef<TapScrollCircleHandle | null>(null)
|
|
114
114
|
const [isTapActive, setIsTapActive] = useState(false)
|
|
115
115
|
const frameRef = useRef<number | null>(null)
|
|
116
|
+
// 生きているのは、フックの部品を置いた確定から取り除く確定まで (レイアウトの副作用の本体と後始末が書く)
|
|
117
|
+
const isLiveRef = useRef(false)
|
|
116
118
|
const lastTimestampRef = useRef<number | null>(null)
|
|
117
119
|
const xDriveRef = useRef<AxisDriveState>({ residual: 0, direction: 0 })
|
|
118
120
|
const yDriveRef = useRef<AxisDriveState>({ residual: 0, direction: 0 })
|
|
@@ -181,14 +183,20 @@ export const useGridTapScroll = (params: UseGridTapScrollParams): UseGridTapScro
|
|
|
181
183
|
}, [])
|
|
182
184
|
|
|
183
185
|
/**
|
|
184
|
-
* One integration frame of the two-axis loop.
|
|
185
|
-
*
|
|
186
|
+
* One integration frame of the two-axis loop. A frame that runs after the commit that unmounts the hook's component (one
|
|
187
|
+
* the browser had already scheduled, or one a drag report started after that commit) moves nothing and ends the loop.
|
|
188
|
+
* 2 軸ループの 1 積分フレーム。フックの部品をアンマウントする確定の後に走るフレーム (ブラウザが予約済みだったもの、またはその確定の
|
|
189
|
+
* 後のドラッグの知らせが始めたもの) は何も動かさず、ループを終える。
|
|
186
190
|
*/
|
|
187
191
|
const step = useCallback(
|
|
188
192
|
(timestamp: number) => {
|
|
189
193
|
// この rAF ループ意味論は ScrollBar.tsx の tap ループ (stepAutoScroll) と双子 — 片方を直したらもう片方も直すこと
|
|
190
194
|
const current = paramsRef.current
|
|
191
195
|
const state = tapDragStateRef.current
|
|
196
|
+
if (!isLiveRef.current) {
|
|
197
|
+
stopLoop()
|
|
198
|
+
return
|
|
199
|
+
}
|
|
192
200
|
if (!state.active || state.direction === 0) {
|
|
193
201
|
stopLoop()
|
|
194
202
|
return
|
|
@@ -348,20 +356,38 @@ export const useGridTapScroll = (params: UseGridTapScrollParams): UseGridTapScro
|
|
|
348
356
|
}, [enabled, resetTapScroll])
|
|
349
357
|
|
|
350
358
|
/**
|
|
351
|
-
* Cancel path (c): `enabled` → false resets
|
|
352
|
-
* キャンセル系 (c)
|
|
359
|
+
* Cancel path (c), first half: `enabled` → false resets.
|
|
360
|
+
* キャンセル系 (c) の前半: `enabled` → false でリセット。
|
|
353
361
|
*
|
|
354
|
-
* 目的:
|
|
355
|
-
* 依存関係: [enabled, resetTapScroll]
|
|
356
|
-
* クリーンアップ:
|
|
362
|
+
* 目的: 無効化時に rAF を残さない。
|
|
363
|
+
* 依存関係: [enabled, resetTapScroll]
|
|
364
|
+
* クリーンアップ: 不要 (アンマウントの停止は後半が担う)。
|
|
357
365
|
*/
|
|
358
366
|
useEffect(() => {
|
|
359
367
|
if (!enabled) {
|
|
360
368
|
resetTapScroll()
|
|
361
369
|
}
|
|
362
370
|
}, [enabled, resetTapScroll])
|
|
363
|
-
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Cancel path (c), second half: the liveness of the hook's component, and the stop at unmount. The loop stops in the commit
|
|
374
|
+
* that unmounts the component (a layout-effect cleanup), not in the passive cleanups after it: when the unmount is not
|
|
375
|
+
* synchronous, the browser can run a scheduled frame between that commit and the passive cleanups, and that frame would
|
|
376
|
+
* move the grid and call the host's `onScroll` after the grid is gone. A frame that still runs after the commit finds the
|
|
377
|
+
* hook not live (`step`).
|
|
378
|
+
* キャンセル系 (c) の後半: フックの部品が生きているかと、アンマウントでの停止。ループはその後の受け身の後始末ではなく、部品を
|
|
379
|
+
* アンマウントする確定 (レイアウトの副作用の後始末) で止める。アンマウントが同期でないとき、ブラウザはその確定と受け身の後始末の間に
|
|
380
|
+
* 予約済みのフレームを走らせられ、そのフレームはグリッドが消えた後にグリッドを動かしてホストの `onScroll` を呼んでしまう。確定の後に
|
|
381
|
+
* なお走るフレームは、フックが生きていないことを見て何もしない (`step`)。
|
|
382
|
+
*
|
|
383
|
+
* 目的: アンマウントの確定の後に位置を動かさない。
|
|
384
|
+
* 依存関係: [stopLoop] (同一性は不変なので、マウントで 1 回・アンマウントで 1 回)
|
|
385
|
+
* クリーンアップ: 生きていない印とループの停止。
|
|
386
|
+
*/
|
|
387
|
+
useLayoutEffect(() => {
|
|
388
|
+
isLiveRef.current = true
|
|
364
389
|
return () => {
|
|
390
|
+
isLiveRef.current = false
|
|
365
391
|
stopLoop()
|
|
366
392
|
}
|
|
367
393
|
}, [stopLoop])
|
package/src/useLruCache.ts
CHANGED
|
@@ -91,10 +91,10 @@ class DoublyLinkedList<K, V> {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* @returns {DoublyLinkedListNode<K, V> | null} The head node, or null if the list is empty
|
|
94
|
+
* Returns the head of the list (the least recently used item) without removing it.
|
|
95
|
+
* リストの先頭 (最も最近使用されていないアイテム) を外さずに返す処理。
|
|
96
|
+
*
|
|
97
|
+
* @returns {DoublyLinkedListNode<K, V> | null} The head node, or null if the list is empty / 先頭のノード (空なら null)
|
|
98
98
|
*/
|
|
99
99
|
peekHead(): DoublyLinkedListNode<K, V> | null {
|
|
100
100
|
return this.head
|
package/src/useWheelBridge.ts
CHANGED
|
@@ -95,6 +95,12 @@ export const useWheelBridge = (target: RefObject<WheelBridgeTarget | null>, opti
|
|
|
95
95
|
// WeakMap なので、外し損ねた要素があっても要素ごと GC される。
|
|
96
96
|
const attachedRef = useRef<WeakMap<HTMLElement, (event: WheelEvent) => void>>(new WeakMap())
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* The ref callback for an element outside the pane: it attaches one non-passive wheel listener per element, which applies each
|
|
100
|
+
* wheel event to the target, and returns the cleanup that removes it (React 19). Its identity never changes.
|
|
101
|
+
* ペインの外の要素に付ける ref コールバック。要素ごとに 1 つの非 passive なホイールのリスナーを付け (ホイールのイベントをそれぞれ対象へ当てる)、
|
|
102
|
+
* それを外す後始末を返す (React 19)。同一性は変わらない。
|
|
103
|
+
*/
|
|
98
104
|
return useCallback((node: HTMLElement | null) => {
|
|
99
105
|
// ❗ null 呼び出しでは何も外さない。どの要素の話か分からないため、ここで外すと
|
|
100
106
|
// 無関係の要素を巻き添えにする。解除は下のクリーンアップ (要素を知っている) が行う。
|
|
@@ -111,6 +117,10 @@ export const useWheelBridge = (target: RefObject<WheelBridgeTarget | null>, opti
|
|
|
111
117
|
return
|
|
112
118
|
}
|
|
113
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Applies one wheel event to the target unless the bridge is disabled.
|
|
122
|
+
* ブリッジが無効でなければ、ホイールのイベント 1 つを対象へ当てる処理。
|
|
123
|
+
*/
|
|
114
124
|
const listener = (event: WheelEvent) => {
|
|
115
125
|
if (optionsRef.current?.enableBridge === false) {
|
|
116
126
|
return
|
|
@@ -126,6 +136,10 @@ export const useWheelBridge = (target: RefObject<WheelBridgeTarget | null>, opti
|
|
|
126
136
|
node.addEventListener("wheel", listener, { passive: false })
|
|
127
137
|
attachedRef.current.set(node, listener)
|
|
128
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Removes the element's listener, when it is still the one this call attached.
|
|
141
|
+
* 要素のリスナーを、この呼び出しが付けたものである限り外す処理。
|
|
142
|
+
*/
|
|
129
143
|
// React 19 はこのクリーンアップを受け取ると `null` 呼び出しの代わりに実行する。
|
|
130
144
|
// ❗ listener の一致判定は**意図的な防御**である。ref コールバックの identity は固定
|
|
131
145
|
// (依存配列が空) なので、要素がマウントされている限り React は張り直さず、現状この分岐が
|