mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
perf(app): recompute picker grid layout in scroll chunks (#1947)
This commit is contained in:
1 parent
21438b2d8f
commit
dceb9061d9
10 files changed
+478
-481
No files matched your search
@@ -18,8 +18,8 @@
|
|||||||
"build": "pnpm wasm:codegen && pnpm generate:colors && pnpm generate:message-layout && pnpm generate:theme-variables && pnpm generate:masks && pnpm generate:css-types && pnpm lingui:compile && rm -rf dist && rspack build --mode production && pnpm tsx scripts/build-sw.mjs",
|
"build": "pnpm wasm:codegen && pnpm generate:colors && pnpm generate:message-layout && pnpm generate:theme-variables && pnpm generate:masks && pnpm generate:css-types && pnpm lingui:compile && rm -rf dist && rspack build --mode production && pnpm tsx scripts/build-sw.mjs",
|
||||||
"dev": "TOKIO_WORKER_THREADS=4 RAYON_NUM_THREADS=4 cargo run --manifest-path ../tools/ci/Cargo.toml -- app-dev-server",
|
"dev": "TOKIO_WORKER_THREADS=4 RAYON_NUM_THREADS=4 cargo run --manifest-path ../tools/ci/Cargo.toml -- app-dev-server",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"bench:member-list": "vitest bench --run src/features/member/utils/MemberListRangeUtils.bench.ts src/features/member/state/MemberSidebar.bench.ts",
|
"bench:member-list": "vitest bench --run src/features/member/utils/MemberListRangeUtils.bench.ts",
|
||||||
"bench:gif-picker": "vitest bench --run src/features/channel/components/pickers/gif/GifPickerStateMachine.bench.ts src/features/channel/components/pickers/gif/GifPickerGridData.bench.ts src/features/channel/components/pickers/gif/GifPickerLoadingSkeletonGridLayout.bench.ts src/features/channel/components/pickers/shared/MasonryListComputer.bench.ts",
|
"bench:gif-picker": "vitest bench --run src/features/channel/components/pickers/gif/GifPickerStateMachine.bench.ts src/features/channel/components/pickers/gif/GifPickerGridData.bench.ts src/features/channel/components/pickers/gif/GifPickerLoadingSkeletonGridLayout.bench.ts src/features/channel/components/pickers/shared/TileFlowSolver.bench.ts",
|
||||||
"bench:messages": "vitest bench --run src/features/messaging/utils/MessageGroupingUtils.bench.ts src/features/channel/components/ChannelMessageStreamUtils.bench.ts src/features/messaging/components/markdown/MarkdownRendering.bench.ts src/features/channel/components/MessageAttachmentUtils.bench.ts src/features/messaging/utils/MessagePaginationUtils.bench.ts src/features/platform/utils/ScrollPosition.bench.ts src/features/messaging/state/ReactionStateMachine.bench.ts",
|
"bench:messages": "vitest bench --run src/features/messaging/utils/MessageGroupingUtils.bench.ts src/features/channel/components/ChannelMessageStreamUtils.bench.ts src/features/messaging/components/markdown/MarkdownRendering.bench.ts src/features/channel/components/MessageAttachmentUtils.bench.ts src/features/messaging/utils/MessagePaginationUtils.bench.ts src/features/platform/utils/ScrollPosition.bench.ts src/features/messaging/state/ReactionStateMachine.bench.ts",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"typecheck": "pnpm wasm:codegen && pnpm generate:colors && pnpm generate:message-layout && pnpm generate:theme-variables && pnpm generate:masks && pnpm generate:css-types && tsgo --noEmit",
|
"typecheck": "pnpm wasm:codegen && pnpm generate:colors && pnpm generate:message-layout && pnpm generate:theme-variables && pnpm generate:masks && pnpm generate:css-types && tsgo --noEmit",
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
import type {MasonryListComputer} from '@app/features/channel/components/MasonryListComputer';
|
import type {TileFlowSolver} from '@app/features/channel/components/TileFlowSolver';
|
||||||
import {isKeyboardActivationKey} from '@app/features/input/utils/KeyboardUtils';
|
import {isKeyboardActivationKey} from '@app/features/input/utils/KeyboardUtils';
|
||||||
import {useCallback, useEffect, useRef, useState} from 'react';
|
import {useCallback, useEffect, useRef, useState} from 'react';
|
||||||
|
|
||||||
interface UseMasonryGridNavigationOptions {
|
interface UseMasonryGridNavigationOptions {
|
||||||
gridData: MasonryListComputer['gridData'] | null;
|
navGrid: TileFlowSolver['navGrid'] | null;
|
||||||
itemKeys: ReadonlyArray<string>;
|
itemKeys: ReadonlyArray<string>;
|
||||||
columns: number;
|
columns: number;
|
||||||
onSelect?: (itemKey: string) => void;
|
onSelect?: (itemKey: string) => void;
|
||||||
@@ -15,15 +15,7 @@ interface UseMasonryGridNavigationOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useMasonryGridNavigation = (options: UseMasonryGridNavigationOptions) => {
|
export const useMasonryGridNavigation = (options: UseMasonryGridNavigationOptions) => {
|
||||||
const {
|
const {navGrid, itemKeys, columns, onSelect, containerRef, isEnabled = true, checkSuspension = () => false} = options;
|
||||||
gridData,
|
|
||||||
itemKeys,
|
|
||||||
columns,
|
|
||||||
onSelect,
|
|
||||||
containerRef,
|
|
||||||
isEnabled = true,
|
|
||||||
checkSuspension = () => false,
|
|
||||||
} = options;
|
|
||||||
const [focusedItemKey, setFocusedItemKey] = useState<string | null>(null);
|
const [focusedItemKey, setFocusedItemKey] = useState<string | null>(null);
|
||||||
const focusedItemRef = useRef<string | null>(null);
|
const focusedItemRef = useRef<string | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -47,31 +39,31 @@ export const useMasonryGridNavigation = (options: UseMasonryGridNavigationOption
|
|||||||
);
|
);
|
||||||
const getItemCoordinates = useCallback(
|
const getItemCoordinates = useCallback(
|
||||||
(itemKey: string) => {
|
(itemKey: string) => {
|
||||||
if (!gridData) return null;
|
if (!navGrid) return null;
|
||||||
return gridData.coordinates[itemKey] || null;
|
return navGrid.tileCells[itemKey] || null;
|
||||||
},
|
},
|
||||||
[gridData],
|
[navGrid],
|
||||||
);
|
);
|
||||||
const findItemByCoordinates = useCallback(
|
const findItemByCoordinates = useCallback(
|
||||||
(row: number, column: number): string | null => {
|
(row: number, column: number): string | null => {
|
||||||
if (!gridData) return null;
|
if (!navGrid) return null;
|
||||||
for (const itemKey of itemKeys) {
|
for (const itemKey of itemKeys) {
|
||||||
const coords = gridData.coordinates[itemKey];
|
const coords = navGrid.tileCells[itemKey];
|
||||||
if (coords && coords.row === row && coords.column === column) {
|
if (coords && coords.row === row && coords.column === column) {
|
||||||
return itemKey;
|
return itemKey;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
[gridData, itemKeys],
|
[navGrid, itemKeys],
|
||||||
);
|
);
|
||||||
const findNearestItemInColumn = useCallback(
|
const findNearestItemInColumn = useCallback(
|
||||||
(targetRow: number, targetColumn: number, direction: 1 | -1): string | null => {
|
(targetRow: number, targetColumn: number, direction: 1 | -1): string | null => {
|
||||||
if (!gridData) return null;
|
if (!navGrid) return null;
|
||||||
let bestItem: string | null = null;
|
let bestItem: string | null = null;
|
||||||
let bestDistance = Number.POSITIVE_INFINITY;
|
let bestDistance = Number.POSITIVE_INFINITY;
|
||||||
for (const itemKey of itemKeys) {
|
for (const itemKey of itemKeys) {
|
||||||
const coords = gridData.coordinates[itemKey];
|
const coords = navGrid.tileCells[itemKey];
|
||||||
if (!coords || coords.column !== targetColumn) continue;
|
if (!coords || coords.column !== targetColumn) continue;
|
||||||
const rowDiff = coords.row - targetRow;
|
const rowDiff = coords.row - targetRow;
|
||||||
if ((direction > 0 && rowDiff > 0) || (direction < 0 && rowDiff < 0)) {
|
if ((direction > 0 && rowDiff > 0) || (direction < 0 && rowDiff < 0)) {
|
||||||
@@ -84,12 +76,12 @@ export const useMasonryGridNavigation = (options: UseMasonryGridNavigationOption
|
|||||||
}
|
}
|
||||||
return bestItem;
|
return bestItem;
|
||||||
},
|
},
|
||||||
[gridData, itemKeys],
|
[navGrid, itemKeys],
|
||||||
);
|
);
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(event: KeyboardEvent) => {
|
(event: KeyboardEvent) => {
|
||||||
if (!isEnabled || checkSuspension()) return;
|
if (!isEnabled || checkSuspension()) return;
|
||||||
if (!gridData || itemKeys.length === 0) return;
|
if (!navGrid || itemKeys.length === 0) return;
|
||||||
const target = event.target as HTMLElement;
|
const target = event.target as HTMLElement;
|
||||||
if (
|
if (
|
||||||
target &&
|
target &&
|
||||||
@@ -172,7 +164,7 @@ export const useMasonryGridNavigation = (options: UseMasonryGridNavigationOption
|
|||||||
[
|
[
|
||||||
isEnabled,
|
isEnabled,
|
||||||
checkSuspension,
|
checkSuspension,
|
||||||
gridData,
|
navGrid,
|
||||||
itemKeys,
|
itemKeys,
|
||||||
columns,
|
columns,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
|||||||
@@ -1,362 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
type VisibleSection = Array<[string, number, number]>;
|
|
||||||
|
|
||||||
interface GridCoordinates {
|
|
||||||
section: number;
|
|
||||||
row: number;
|
|
||||||
column: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GridData {
|
|
||||||
coordinates: Record<string, GridCoordinates>;
|
|
||||||
boundaries: Array<number>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CoordsStyle {
|
|
||||||
position: 'absolute' | 'sticky';
|
|
||||||
left?: number;
|
|
||||||
right?: number;
|
|
||||||
width: number;
|
|
||||||
top: number;
|
|
||||||
height: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
type Padding =
|
|
||||||
| number
|
|
||||||
| {
|
|
||||||
top?: number;
|
|
||||||
bottom?: number;
|
|
||||||
left?: number;
|
|
||||||
right?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
function defaultGetSectionHeight(_section: number): number {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findMinColumnIndex(columnHeights: Array<number>): [number, number] {
|
|
||||||
let minHeight = columnHeights[0];
|
|
||||||
let minIndex = 0;
|
|
||||||
for (let i = 1; i < columnHeights.length; i++) {
|
|
||||||
if (columnHeights[i] < minHeight) {
|
|
||||||
minHeight = columnHeights[i];
|
|
||||||
minIndex = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [minHeight, minIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSectionHeaderKey(sectionIndex: number): string {
|
|
||||||
return `section-header-${sectionIndex}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSectionKey(sectionIndex: number): string {
|
|
||||||
return `section-${sectionIndex}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MasonryListComputer {
|
|
||||||
public visibleSections: Record<string, VisibleSection> = {};
|
|
||||||
public gridData: GridData = {coordinates: {}, boundaries: []};
|
|
||||||
public coordsMap: Record<string, CoordsStyle> = {};
|
|
||||||
public itemGrid: Array<Array<string>> = [];
|
|
||||||
public totalHeight: number = 0;
|
|
||||||
private columnHeights: Array<number> = [];
|
|
||||||
private columnWidth: number = 0;
|
|
||||||
private currentRow: number = 0;
|
|
||||||
private lastColumnIndex: number = 0;
|
|
||||||
private needsFullCompute: boolean = true;
|
|
||||||
private bufferWidth: number = 0;
|
|
||||||
private sections: Array<number> = [];
|
|
||||||
private columns: number = 0;
|
|
||||||
private itemGutter: number = 0;
|
|
||||||
private removeEdgeItemGutters: boolean = false;
|
|
||||||
private sectionGutter: number | null = null;
|
|
||||||
private padding: Padding | null = null;
|
|
||||||
private paddingVertical: number | null = null;
|
|
||||||
private paddingHorizontal: number | null = null;
|
|
||||||
private marginLeft: number | null = null;
|
|
||||||
private dir: 'ltr' | 'rtl' = 'ltr';
|
|
||||||
private version: number | string | null = null;
|
|
||||||
private getItemKey: (section: number, item: number) => string | null = () => {
|
|
||||||
throw new Error('MasonryListComputer: getItemKey has not been implemented');
|
|
||||||
};
|
|
||||||
private getItemHeight: (section: number, item: number, width: number) => number = () => {
|
|
||||||
throw new Error('MasonryListComputer: getItemHeight has not been implemented');
|
|
||||||
};
|
|
||||||
private getSectionHeight: (section: number) => number = defaultGetSectionHeight;
|
|
||||||
|
|
||||||
private getPadding(key: 'top' | 'bottom' | 'left' | 'right'): number {
|
|
||||||
if (this.padding == null) {
|
|
||||||
return this.itemGutter;
|
|
||||||
}
|
|
||||||
if (typeof this.padding === 'number') {
|
|
||||||
return this.padding;
|
|
||||||
}
|
|
||||||
return this.padding[key] ?? this.itemGutter;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getPaddingLeft(): number {
|
|
||||||
return this.paddingHorizontal != null ? this.paddingHorizontal : this.getPadding('left');
|
|
||||||
}
|
|
||||||
|
|
||||||
private getPaddingRight(): number {
|
|
||||||
return this.paddingHorizontal != null ? this.paddingHorizontal : this.getPadding('right');
|
|
||||||
}
|
|
||||||
|
|
||||||
private getPaddingTop(): number {
|
|
||||||
return this.paddingVertical != null ? this.paddingVertical : this.getPadding('top');
|
|
||||||
}
|
|
||||||
|
|
||||||
private getPaddingBottom(): number {
|
|
||||||
return this.paddingVertical != null ? this.paddingVertical : this.getPadding('bottom');
|
|
||||||
}
|
|
||||||
|
|
||||||
private getSectionGutter(): number {
|
|
||||||
return this.sectionGutter != null ? this.sectionGutter : this.itemGutter;
|
|
||||||
}
|
|
||||||
|
|
||||||
mergeProps(props: {
|
|
||||||
sections?: Array<number>;
|
|
||||||
columns?: number;
|
|
||||||
itemGutter?: number;
|
|
||||||
removeEdgeItemGutters?: boolean;
|
|
||||||
getItemKey?: (section: number, item: number) => string | null;
|
|
||||||
getItemHeight?: (section: number, item: number, width: number) => number;
|
|
||||||
getSectionHeight?: (section: number) => number;
|
|
||||||
bufferWidth?: number;
|
|
||||||
padding?: Padding;
|
|
||||||
paddingVertical?: number;
|
|
||||||
paddingHorizontal?: number;
|
|
||||||
marginLeft?: number;
|
|
||||||
sectionGutter?: number;
|
|
||||||
dir?: 'ltr' | 'rtl';
|
|
||||||
version?: number | string | null;
|
|
||||||
}): void {
|
|
||||||
const {
|
|
||||||
sections = this.sections,
|
|
||||||
columns = this.columns,
|
|
||||||
itemGutter = this.itemGutter,
|
|
||||||
removeEdgeItemGutters = this.removeEdgeItemGutters,
|
|
||||||
getItemKey = this.getItemKey,
|
|
||||||
getItemHeight = this.getItemHeight,
|
|
||||||
getSectionHeight = this.getSectionHeight,
|
|
||||||
bufferWidth = this.bufferWidth,
|
|
||||||
padding = this.padding,
|
|
||||||
paddingVertical = this.paddingVertical,
|
|
||||||
paddingHorizontal = this.paddingHorizontal,
|
|
||||||
marginLeft = this.marginLeft,
|
|
||||||
sectionGutter = this.sectionGutter,
|
|
||||||
dir = this.dir,
|
|
||||||
version = this.version,
|
|
||||||
} = props;
|
|
||||||
if (
|
|
||||||
this.sections !== sections ||
|
|
||||||
this.columns !== columns ||
|
|
||||||
this.itemGutter !== itemGutter ||
|
|
||||||
this.removeEdgeItemGutters !== removeEdgeItemGutters ||
|
|
||||||
this.getItemKey !== getItemKey ||
|
|
||||||
this.getSectionHeight !== getSectionHeight ||
|
|
||||||
this.getItemHeight !== getItemHeight ||
|
|
||||||
this.bufferWidth !== bufferWidth ||
|
|
||||||
this.padding !== padding ||
|
|
||||||
this.paddingVertical !== paddingVertical ||
|
|
||||||
this.paddingHorizontal !== paddingHorizontal ||
|
|
||||||
this.marginLeft !== marginLeft ||
|
|
||||||
this.sectionGutter !== sectionGutter ||
|
|
||||||
this.dir !== dir ||
|
|
||||||
this.version !== version
|
|
||||||
) {
|
|
||||||
this.needsFullCompute = true;
|
|
||||||
this.sections = sections;
|
|
||||||
this.columns = columns;
|
|
||||||
this.itemGutter = itemGutter;
|
|
||||||
this.removeEdgeItemGutters = removeEdgeItemGutters;
|
|
||||||
this.getItemKey = getItemKey;
|
|
||||||
this.getSectionHeight = getSectionHeight;
|
|
||||||
this.getItemHeight = getItemHeight;
|
|
||||||
this.bufferWidth = bufferWidth;
|
|
||||||
this.padding = padding;
|
|
||||||
this.paddingVertical = paddingVertical;
|
|
||||||
this.paddingHorizontal = paddingHorizontal;
|
|
||||||
this.marginLeft = marginLeft;
|
|
||||||
this.sectionGutter = sectionGutter;
|
|
||||||
this.dir = dir;
|
|
||||||
this.version = version;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private computeFullCoords(): void {
|
|
||||||
if (!this.needsFullCompute) return;
|
|
||||||
const {columns, getItemKey, getItemHeight, itemGutter, getSectionHeight, bufferWidth, removeEdgeItemGutters} = this;
|
|
||||||
const horizontalKey = this.dir === 'rtl' ? 'right' : 'left';
|
|
||||||
this.coordsMap = {};
|
|
||||||
this.gridData = {boundaries: [], coordinates: {}};
|
|
||||||
this.currentRow = 0;
|
|
||||||
this.lastColumnIndex = 0;
|
|
||||||
const paddingTop = this.getPaddingTop();
|
|
||||||
const paddingBottom = this.getPaddingBottom();
|
|
||||||
const paddingLeft = this.getPaddingLeft();
|
|
||||||
const paddingRight = this.getPaddingRight();
|
|
||||||
const marginLeft = this.marginLeft ?? 0;
|
|
||||||
this.columnHeights = Array(columns).fill(paddingTop);
|
|
||||||
this.columnWidth =
|
|
||||||
(bufferWidth -
|
|
||||||
paddingRight -
|
|
||||||
paddingLeft -
|
|
||||||
itemGutter * (columns - 1) -
|
|
||||||
(removeEdgeItemGutters ? itemGutter : 0)) /
|
|
||||||
columns;
|
|
||||||
this.itemGrid = [];
|
|
||||||
let sectionIndex = 0;
|
|
||||||
while (sectionIndex < this.sections.length) {
|
|
||||||
this.gridData.boundaries[sectionIndex] = this.currentRow;
|
|
||||||
this.currentRow = 0;
|
|
||||||
this.lastColumnIndex = 0;
|
|
||||||
const sectionLength = this.sections[sectionIndex];
|
|
||||||
let itemIndex = 0;
|
|
||||||
let minItemTop = Number.POSITIVE_INFINITY;
|
|
||||||
let maxItemBottom = Number.NEGATIVE_INFINITY;
|
|
||||||
const sectionHeight = getSectionHeight(sectionIndex);
|
|
||||||
let maxColumnHeight = this.getMaxColumnHeight(this.columnHeights);
|
|
||||||
if (sectionIndex > 0) {
|
|
||||||
maxColumnHeight = maxColumnHeight - itemGutter + this.getSectionGutter();
|
|
||||||
}
|
|
||||||
const sectionHeaderHeight = sectionHeight > 0 ? sectionHeight + itemGutter : 0;
|
|
||||||
for (let col = 0; col < this.columnHeights.length; col++) {
|
|
||||||
this.columnHeights[col] = maxColumnHeight + sectionHeaderHeight;
|
|
||||||
}
|
|
||||||
while (itemIndex < sectionLength) {
|
|
||||||
const itemKey = getItemKey(sectionIndex, itemIndex);
|
|
||||||
if (itemKey == null) {
|
|
||||||
itemIndex++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const [minHeight, minColumnIndex] = findMinColumnIndex(this.columnHeights);
|
|
||||||
if (minColumnIndex < this.lastColumnIndex) {
|
|
||||||
this.currentRow++;
|
|
||||||
}
|
|
||||||
this.lastColumnIndex = minColumnIndex;
|
|
||||||
const itemHeight = getItemHeight(sectionIndex, itemIndex, this.columnWidth);
|
|
||||||
const coords: CoordsStyle = {
|
|
||||||
position: 'absolute',
|
|
||||||
[horizontalKey]:
|
|
||||||
this.columnWidth * minColumnIndex + itemGutter * (minColumnIndex + 1) - itemGutter + paddingLeft,
|
|
||||||
width: this.columnWidth,
|
|
||||||
top: minHeight - maxColumnHeight,
|
|
||||||
height: itemHeight,
|
|
||||||
};
|
|
||||||
minItemTop = Math.min(minItemTop, coords.top);
|
|
||||||
maxItemBottom = Math.max(maxItemBottom, coords.top + coords.height);
|
|
||||||
const gridCoords: GridCoordinates = {
|
|
||||||
section: sectionIndex,
|
|
||||||
row: this.currentRow,
|
|
||||||
column: minColumnIndex,
|
|
||||||
};
|
|
||||||
this.coordsMap[itemKey] = coords;
|
|
||||||
this.gridData.coordinates[itemKey] = gridCoords;
|
|
||||||
this.columnHeights[minColumnIndex] = minHeight + itemHeight + itemGutter;
|
|
||||||
this.itemGrid[minColumnIndex] = this.itemGrid[minColumnIndex] ?? [];
|
|
||||||
this.itemGrid[minColumnIndex].push(itemKey);
|
|
||||||
itemIndex++;
|
|
||||||
}
|
|
||||||
if (sectionHeight > 0) {
|
|
||||||
this.coordsMap[getSectionHeaderKey(sectionIndex)] = {
|
|
||||||
position: 'sticky',
|
|
||||||
[horizontalKey]: paddingLeft,
|
|
||||||
width: this.columnWidth * columns + itemGutter * columns,
|
|
||||||
top: 0,
|
|
||||||
height: sectionHeight,
|
|
||||||
};
|
|
||||||
this.coordsMap[getSectionKey(sectionIndex)] = {
|
|
||||||
position: 'absolute',
|
|
||||||
[horizontalKey]: marginLeft,
|
|
||||||
width: this.columnWidth * columns + itemGutter * (columns - 1) + paddingLeft + paddingRight,
|
|
||||||
top: maxColumnHeight,
|
|
||||||
height: this.getMaxColumnHeight(this.columnHeights) - maxColumnHeight,
|
|
||||||
};
|
|
||||||
} else if (Number.isFinite(minItemTop) && Number.isFinite(maxItemBottom)) {
|
|
||||||
this.coordsMap[getSectionKey(sectionIndex)] = {
|
|
||||||
position: 'absolute',
|
|
||||||
[horizontalKey]: marginLeft,
|
|
||||||
width: this.columnWidth * columns + itemGutter * (columns - 1) + paddingLeft + paddingRight,
|
|
||||||
top: minItemTop,
|
|
||||||
height: maxItemBottom - minItemTop,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
sectionIndex++;
|
|
||||||
}
|
|
||||||
this.columnHeights = this.columnHeights.map((height) => height - itemGutter + paddingBottom);
|
|
||||||
this.totalHeight = this.getMaxColumnHeight();
|
|
||||||
this.visibleSections = {};
|
|
||||||
this.needsFullCompute = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
computeVisibleSections(start: number, end: number): void {
|
|
||||||
this.computeFullCoords();
|
|
||||||
const {getItemKey, coordsMap} = this;
|
|
||||||
this.visibleSections = {};
|
|
||||||
let sectionIndex = 0;
|
|
||||||
while (sectionIndex < this.sections.length) {
|
|
||||||
const sectionLength = this.sections[sectionIndex];
|
|
||||||
const sectionKey = getSectionKey(sectionIndex);
|
|
||||||
const sectionCoords = coordsMap[sectionKey];
|
|
||||||
if (sectionCoords == null) {
|
|
||||||
sectionIndex++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const {top} = sectionCoords;
|
|
||||||
const bottom = top + sectionCoords.height;
|
|
||||||
if (top > end) break;
|
|
||||||
if (bottom < start) {
|
|
||||||
sectionIndex++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let itemIndex = 0;
|
|
||||||
let direction = 1;
|
|
||||||
if (bottom < end && bottom > start) {
|
|
||||||
itemIndex = sectionLength - 1;
|
|
||||||
direction = -1;
|
|
||||||
}
|
|
||||||
this.visibleSections[sectionKey] = [];
|
|
||||||
while (itemIndex >= 0 && itemIndex < sectionLength) {
|
|
||||||
const itemKey = getItemKey(sectionIndex, itemIndex);
|
|
||||||
const itemCoords = itemKey != null ? coordsMap[itemKey] : null;
|
|
||||||
if (itemKey == null || itemCoords == null) {
|
|
||||||
itemIndex += direction;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const {top: itemTop, height: itemHeight} = itemCoords;
|
|
||||||
const itemAbsoluteTop = itemTop + top;
|
|
||||||
if (itemAbsoluteTop > start - itemHeight && itemAbsoluteTop < end) {
|
|
||||||
if (direction === -1) {
|
|
||||||
this.visibleSections[sectionKey].unshift([itemKey, sectionIndex, itemIndex]);
|
|
||||||
} else {
|
|
||||||
this.visibleSections[sectionKey].push([itemKey, sectionIndex, itemIndex]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
itemIndex += direction;
|
|
||||||
}
|
|
||||||
if (top < start && bottom > end) break;
|
|
||||||
sectionIndex++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getMaxColumnHeight(columnHeights: Array<number> = this.columnHeights): number {
|
|
||||||
return columnHeights.reduce((max, height) => Math.max(max, height), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
getState(): {
|
|
||||||
coordsMap: Record<string, CoordsStyle>;
|
|
||||||
gridData: GridData;
|
|
||||||
visibleSections: Record<string, VisibleSection>;
|
|
||||||
totalHeight: number;
|
|
||||||
} {
|
|
||||||
return {
|
|
||||||
coordsMap: this.coordsMap,
|
|
||||||
gridData: this.gridData,
|
|
||||||
visibleSections: this.visibleSections,
|
|
||||||
totalHeight: this.totalHeight,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,357 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
type SectionWindow = Array<[string, number, number]>;
|
||||||
|
|
||||||
|
interface GridCell {
|
||||||
|
section: number;
|
||||||
|
row: number;
|
||||||
|
column: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NavGrid {
|
||||||
|
tileCells: Record<string, GridCell>;
|
||||||
|
rowBase: Array<number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TileBox {
|
||||||
|
position: 'absolute' | 'sticky';
|
||||||
|
left?: number;
|
||||||
|
right?: number;
|
||||||
|
width: number;
|
||||||
|
top: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type LayoutPadding =
|
||||||
|
| number
|
||||||
|
| {
|
||||||
|
top?: number;
|
||||||
|
bottom?: number;
|
||||||
|
left?: number;
|
||||||
|
right?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function noSectionHeight(_section: number): number {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shortestLane(laneFill: Array<number>): [number, number] {
|
||||||
|
let minHeight = laneFill[0];
|
||||||
|
let minIndex = 0;
|
||||||
|
for (let i = 1; i < laneFill.length; i++) {
|
||||||
|
if (laneFill[i] < minHeight) {
|
||||||
|
minHeight = laneFill[i];
|
||||||
|
minIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [minHeight, minIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSectionHeaderKey(sectionIndex: number): string {
|
||||||
|
return `section-header-${sectionIndex}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSectionKey(sectionIndex: number): string {
|
||||||
|
return `section-${sectionIndex}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TileFlowSolver {
|
||||||
|
public windowedTiles: Record<string, SectionWindow> = {};
|
||||||
|
public navGrid: NavGrid = {tileCells: {}, rowBase: []};
|
||||||
|
public tileBoxes: Record<string, TileBox> = {};
|
||||||
|
public laneOrder: Array<Array<string>> = [];
|
||||||
|
public contentHeight: number = 0;
|
||||||
|
private laneFill: Array<number> = [];
|
||||||
|
private laneWidth: number = 0;
|
||||||
|
private rowCursor: number = 0;
|
||||||
|
private prevLaneIndex: number = 0;
|
||||||
|
private layoutDirty: boolean = true;
|
||||||
|
private viewportSpan: number = 0;
|
||||||
|
private sectionSizes: Array<number> = [];
|
||||||
|
private laneCount: number = 0;
|
||||||
|
private tileSpacing: number = 0;
|
||||||
|
private trimOuterSpacing: boolean = false;
|
||||||
|
private sectionSpacing: number | null = null;
|
||||||
|
private padBox: LayoutPadding | null = null;
|
||||||
|
private padAxisY: number | null = null;
|
||||||
|
private padAxisX: number | null = null;
|
||||||
|
private originShift: number | null = null;
|
||||||
|
private flowDirection: 'ltr' | 'rtl' = 'ltr';
|
||||||
|
private layoutStamp: number | string | null = null;
|
||||||
|
private tileKeyOf: (section: number, item: number) => string | null = () => {
|
||||||
|
throw new Error('TileFlowSolver requires a tile key resolver before layout can run');
|
||||||
|
};
|
||||||
|
private tileHeightOf: (section: number, item: number, width: number) => number = () => {
|
||||||
|
throw new Error('TileFlowSolver requires a tile height measurer before layout can run');
|
||||||
|
};
|
||||||
|
private sectionHeightOf: (section: number) => number = noSectionHeight;
|
||||||
|
|
||||||
|
private padFor(key: 'top' | 'bottom' | 'left' | 'right'): number {
|
||||||
|
if (this.padBox == null) {
|
||||||
|
return this.tileSpacing;
|
||||||
|
}
|
||||||
|
if (typeof this.padBox === 'number') {
|
||||||
|
return this.padBox;
|
||||||
|
}
|
||||||
|
return this.padBox[key] ?? this.tileSpacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
private padLeading(): number {
|
||||||
|
return this.padAxisX != null ? this.padAxisX : this.padFor('left');
|
||||||
|
}
|
||||||
|
|
||||||
|
private padTrailing(): number {
|
||||||
|
return this.padAxisX != null ? this.padAxisX : this.padFor('right');
|
||||||
|
}
|
||||||
|
|
||||||
|
private padTop(): number {
|
||||||
|
return this.padAxisY != null ? this.padAxisY : this.padFor('top');
|
||||||
|
}
|
||||||
|
|
||||||
|
private padBottom(): number {
|
||||||
|
return this.padAxisY != null ? this.padAxisY : this.padFor('bottom');
|
||||||
|
}
|
||||||
|
|
||||||
|
private spacingBetweenSections(): number {
|
||||||
|
return this.sectionSpacing != null ? this.sectionSpacing : this.tileSpacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
applySettings(settings: {
|
||||||
|
sectionSizes?: Array<number>;
|
||||||
|
laneCount?: number;
|
||||||
|
tileSpacing?: number;
|
||||||
|
trimOuterSpacing?: boolean;
|
||||||
|
tileKeyOf?: (section: number, item: number) => string | null;
|
||||||
|
tileHeightOf?: (section: number, item: number, width: number) => number;
|
||||||
|
sectionHeightOf?: (section: number) => number;
|
||||||
|
viewportSpan?: number;
|
||||||
|
padBox?: LayoutPadding;
|
||||||
|
padAxisY?: number;
|
||||||
|
padAxisX?: number;
|
||||||
|
originShift?: number;
|
||||||
|
sectionSpacing?: number;
|
||||||
|
flowDirection?: 'ltr' | 'rtl';
|
||||||
|
layoutStamp?: number | string | null;
|
||||||
|
}): void {
|
||||||
|
const {
|
||||||
|
sectionSizes = this.sectionSizes,
|
||||||
|
laneCount = this.laneCount,
|
||||||
|
tileSpacing = this.tileSpacing,
|
||||||
|
trimOuterSpacing = this.trimOuterSpacing,
|
||||||
|
tileKeyOf = this.tileKeyOf,
|
||||||
|
tileHeightOf = this.tileHeightOf,
|
||||||
|
sectionHeightOf = this.sectionHeightOf,
|
||||||
|
viewportSpan = this.viewportSpan,
|
||||||
|
padBox = this.padBox,
|
||||||
|
padAxisY = this.padAxisY,
|
||||||
|
padAxisX = this.padAxisX,
|
||||||
|
originShift = this.originShift,
|
||||||
|
sectionSpacing = this.sectionSpacing,
|
||||||
|
flowDirection = this.flowDirection,
|
||||||
|
layoutStamp = this.layoutStamp,
|
||||||
|
} = settings;
|
||||||
|
if (
|
||||||
|
this.sectionSizes !== sectionSizes ||
|
||||||
|
this.laneCount !== laneCount ||
|
||||||
|
this.tileSpacing !== tileSpacing ||
|
||||||
|
this.trimOuterSpacing !== trimOuterSpacing ||
|
||||||
|
this.tileKeyOf !== tileKeyOf ||
|
||||||
|
this.sectionHeightOf !== sectionHeightOf ||
|
||||||
|
this.tileHeightOf !== tileHeightOf ||
|
||||||
|
this.viewportSpan !== viewportSpan ||
|
||||||
|
this.padBox !== padBox ||
|
||||||
|
this.padAxisY !== padAxisY ||
|
||||||
|
this.padAxisX !== padAxisX ||
|
||||||
|
this.originShift !== originShift ||
|
||||||
|
this.sectionSpacing !== sectionSpacing ||
|
||||||
|
this.flowDirection !== flowDirection ||
|
||||||
|
this.layoutStamp !== layoutStamp
|
||||||
|
) {
|
||||||
|
this.layoutDirty = true;
|
||||||
|
this.sectionSizes = sectionSizes;
|
||||||
|
this.laneCount = laneCount;
|
||||||
|
this.tileSpacing = tileSpacing;
|
||||||
|
this.trimOuterSpacing = trimOuterSpacing;
|
||||||
|
this.tileKeyOf = tileKeyOf;
|
||||||
|
this.sectionHeightOf = sectionHeightOf;
|
||||||
|
this.tileHeightOf = tileHeightOf;
|
||||||
|
this.viewportSpan = viewportSpan;
|
||||||
|
this.padBox = padBox;
|
||||||
|
this.padAxisY = padAxisY;
|
||||||
|
this.padAxisX = padAxisX;
|
||||||
|
this.originShift = originShift;
|
||||||
|
this.sectionSpacing = sectionSpacing;
|
||||||
|
this.flowDirection = flowDirection;
|
||||||
|
this.layoutStamp = layoutStamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private rebuildLayout(): void {
|
||||||
|
if (!this.layoutDirty) return;
|
||||||
|
const {laneCount, tileKeyOf, tileHeightOf, tileSpacing, sectionHeightOf, viewportSpan, trimOuterSpacing} = this;
|
||||||
|
const horizontalKey = this.flowDirection === 'rtl' ? 'right' : 'left';
|
||||||
|
this.tileBoxes = {};
|
||||||
|
this.navGrid = {rowBase: [], tileCells: {}};
|
||||||
|
this.rowCursor = 0;
|
||||||
|
this.prevLaneIndex = 0;
|
||||||
|
const padTop = this.padTop();
|
||||||
|
const padBottom = this.padBottom();
|
||||||
|
const padLeading = this.padLeading();
|
||||||
|
const padTrailing = this.padTrailing();
|
||||||
|
const originShift = this.originShift ?? 0;
|
||||||
|
this.laneFill = Array(laneCount).fill(padTop);
|
||||||
|
this.laneWidth =
|
||||||
|
(viewportSpan - padTrailing - padLeading - tileSpacing * (laneCount - 1) - (trimOuterSpacing ? tileSpacing : 0)) /
|
||||||
|
laneCount;
|
||||||
|
this.laneOrder = [];
|
||||||
|
let sectionIndex = 0;
|
||||||
|
while (sectionIndex < this.sectionSizes.length) {
|
||||||
|
this.navGrid.rowBase[sectionIndex] = this.rowCursor;
|
||||||
|
this.rowCursor = 0;
|
||||||
|
this.prevLaneIndex = 0;
|
||||||
|
const sectionLength = this.sectionSizes[sectionIndex];
|
||||||
|
let itemIndex = 0;
|
||||||
|
let minItemTop = Number.POSITIVE_INFINITY;
|
||||||
|
let maxItemBottom = Number.NEGATIVE_INFINITY;
|
||||||
|
const sectionHeight = sectionHeightOf(sectionIndex);
|
||||||
|
let laneCeiling = this.tallestLane(this.laneFill);
|
||||||
|
if (sectionIndex > 0) {
|
||||||
|
laneCeiling = laneCeiling - tileSpacing + this.spacingBetweenSections();
|
||||||
|
}
|
||||||
|
const headerBand = sectionHeight > 0 ? sectionHeight + tileSpacing : 0;
|
||||||
|
for (let lane = 0; lane < this.laneFill.length; lane++) {
|
||||||
|
this.laneFill[lane] = laneCeiling + headerBand;
|
||||||
|
}
|
||||||
|
while (itemIndex < sectionLength) {
|
||||||
|
const tileKey = tileKeyOf(sectionIndex, itemIndex);
|
||||||
|
if (tileKey == null) {
|
||||||
|
itemIndex++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const [minHeight, laneIndex] = shortestLane(this.laneFill);
|
||||||
|
if (laneIndex < this.prevLaneIndex) {
|
||||||
|
this.rowCursor++;
|
||||||
|
}
|
||||||
|
this.prevLaneIndex = laneIndex;
|
||||||
|
const tileHeight = tileHeightOf(sectionIndex, itemIndex, this.laneWidth);
|
||||||
|
const box: TileBox = {
|
||||||
|
position: 'absolute',
|
||||||
|
[horizontalKey]: this.laneWidth * laneIndex + tileSpacing * (laneIndex + 1) - tileSpacing + padLeading,
|
||||||
|
width: this.laneWidth,
|
||||||
|
top: minHeight - laneCeiling,
|
||||||
|
height: tileHeight,
|
||||||
|
};
|
||||||
|
minItemTop = Math.min(minItemTop, box.top);
|
||||||
|
maxItemBottom = Math.max(maxItemBottom, box.top + box.height);
|
||||||
|
const cell: GridCell = {
|
||||||
|
section: sectionIndex,
|
||||||
|
row: this.rowCursor,
|
||||||
|
column: laneIndex,
|
||||||
|
};
|
||||||
|
this.tileBoxes[tileKey] = box;
|
||||||
|
this.navGrid.tileCells[tileKey] = cell;
|
||||||
|
this.laneFill[laneIndex] = minHeight + tileHeight + tileSpacing;
|
||||||
|
this.laneOrder[laneIndex] = this.laneOrder[laneIndex] ?? [];
|
||||||
|
this.laneOrder[laneIndex].push(tileKey);
|
||||||
|
itemIndex++;
|
||||||
|
}
|
||||||
|
if (sectionHeight > 0) {
|
||||||
|
this.tileBoxes[getSectionHeaderKey(sectionIndex)] = {
|
||||||
|
position: 'sticky',
|
||||||
|
[horizontalKey]: padLeading,
|
||||||
|
width: this.laneWidth * laneCount + tileSpacing * laneCount,
|
||||||
|
top: 0,
|
||||||
|
height: sectionHeight,
|
||||||
|
};
|
||||||
|
this.tileBoxes[getSectionKey(sectionIndex)] = {
|
||||||
|
position: 'absolute',
|
||||||
|
[horizontalKey]: originShift,
|
||||||
|
width: this.laneWidth * laneCount + tileSpacing * (laneCount - 1) + padLeading + padTrailing,
|
||||||
|
top: laneCeiling,
|
||||||
|
height: this.tallestLane(this.laneFill) - laneCeiling,
|
||||||
|
};
|
||||||
|
} else if (Number.isFinite(minItemTop) && Number.isFinite(maxItemBottom)) {
|
||||||
|
this.tileBoxes[getSectionKey(sectionIndex)] = {
|
||||||
|
position: 'absolute',
|
||||||
|
[horizontalKey]: originShift,
|
||||||
|
width: this.laneWidth * laneCount + tileSpacing * (laneCount - 1) + padLeading + padTrailing,
|
||||||
|
top: minItemTop,
|
||||||
|
height: maxItemBottom - minItemTop,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
sectionIndex++;
|
||||||
|
}
|
||||||
|
this.laneFill = this.laneFill.map((height) => height - tileSpacing + padBottom);
|
||||||
|
this.contentHeight = this.tallestLane();
|
||||||
|
this.windowedTiles = {};
|
||||||
|
this.layoutDirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectWindow(start: number, end: number): void {
|
||||||
|
this.rebuildLayout();
|
||||||
|
const {tileKeyOf, tileBoxes} = this;
|
||||||
|
this.windowedTiles = {};
|
||||||
|
let sectionIndex = 0;
|
||||||
|
while (sectionIndex < this.sectionSizes.length) {
|
||||||
|
const sectionLength = this.sectionSizes[sectionIndex];
|
||||||
|
const sectionKey = getSectionKey(sectionIndex);
|
||||||
|
const sectionExtent = tileBoxes[sectionKey];
|
||||||
|
if (sectionExtent == null) {
|
||||||
|
sectionIndex++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const {top} = sectionExtent;
|
||||||
|
const bottom = top + sectionExtent.height;
|
||||||
|
if (top > end) break;
|
||||||
|
if (bottom < start) {
|
||||||
|
sectionIndex++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let itemIndex = 0;
|
||||||
|
let direction = 1;
|
||||||
|
if (bottom < end && bottom > start) {
|
||||||
|
itemIndex = sectionLength - 1;
|
||||||
|
direction = -1;
|
||||||
|
}
|
||||||
|
this.windowedTiles[sectionKey] = [];
|
||||||
|
while (itemIndex >= 0 && itemIndex < sectionLength) {
|
||||||
|
const tileKey = tileKeyOf(sectionIndex, itemIndex);
|
||||||
|
const tileBox = tileKey != null ? tileBoxes[tileKey] : null;
|
||||||
|
if (tileKey == null || tileBox == null) {
|
||||||
|
itemIndex += direction;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const {top: tileTop, height: tileHeight} = tileBox;
|
||||||
|
const tileAbsoluteTop = tileTop + top;
|
||||||
|
if (tileAbsoluteTop > start - tileHeight && tileAbsoluteTop < end) {
|
||||||
|
if (direction === -1) {
|
||||||
|
this.windowedTiles[sectionKey].unshift([tileKey, sectionIndex, itemIndex]);
|
||||||
|
} else {
|
||||||
|
this.windowedTiles[sectionKey].push([tileKey, sectionIndex, itemIndex]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
itemIndex += direction;
|
||||||
|
}
|
||||||
|
if (top < start && bottom > end) break;
|
||||||
|
sectionIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private tallestLane(laneFill: Array<number> = this.laneFill): number {
|
||||||
|
return laneFill.reduce((max, height) => Math.max(max, height), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
readLayout(): {
|
||||||
|
tileBoxes: Record<string, TileBox>;
|
||||||
|
navGrid: NavGrid;
|
||||||
|
windowedTiles: Record<string, SectionWindow>;
|
||||||
|
contentHeight: number;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
tileBoxes: this.tileBoxes,
|
||||||
|
navGrid: this.navGrid,
|
||||||
|
windowedTiles: this.windowedTiles,
|
||||||
|
contentHeight: this.contentHeight,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,8 +50,8 @@ export const GifPickerGrid = observer(
|
|||||||
onContentSizeChange?: (contentSize: number) => void;
|
onContentSizeChange?: (contentSize: number) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const {i18n} = useLingui();
|
const {i18n} = useLingui();
|
||||||
const itemGutter = 8;
|
const tileSpacing = 8;
|
||||||
const columns = computeMasonryColumns(viewportWidth, itemGutter, {minColumns: 2});
|
const columns = computeMasonryColumns(viewportWidth, tileSpacing, {minColumns: 2});
|
||||||
const favoriteMemes = FavoriteMemes.memes;
|
const favoriteMemes = FavoriteMemes.memes;
|
||||||
const favoriteMemesVersion = favoriteMemes.length;
|
const favoriteMemesVersion = favoriteMemes.length;
|
||||||
const favoriteGifs = FavoriteGif.favoriteGifs;
|
const favoriteGifs = FavoriteGif.favoriteGifs;
|
||||||
@@ -131,7 +131,7 @@ export const GifPickerGrid = observer(
|
|||||||
data={data}
|
data={data}
|
||||||
itemKeys={itemKeys}
|
itemKeys={itemKeys}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
itemGutter={itemGutter}
|
tileSpacing={tileSpacing}
|
||||||
viewportWidth={viewportWidth}
|
viewportWidth={viewportWidth}
|
||||||
viewportHeight={viewportHeight}
|
viewportHeight={viewportHeight}
|
||||||
scrollTop={scrollTop}
|
scrollTop={scrollTop}
|
||||||
@@ -141,13 +141,13 @@ export const GifPickerGrid = observer(
|
|||||||
onSelectItemKey={(key) => {
|
onSelectItemKey={(key) => {
|
||||||
handleSelectByKey(key);
|
handleSelectByKey(key);
|
||||||
}}
|
}}
|
||||||
getItemKey={(item) => item.key}
|
tileKeyOf={(item) => item.key}
|
||||||
getItemHeight={(item, _index, columnWidth) => {
|
tileHeightOf={(item, _index, laneWidth) => {
|
||||||
if (item.type === 'gif') {
|
if (item.type === 'gif') {
|
||||||
const g = item.gif;
|
const g = item.gif;
|
||||||
return columnWidth * (g.height / g.width);
|
return laneWidth * (g.height / g.width);
|
||||||
}
|
}
|
||||||
return columnWidth * (item.height / item.width);
|
return laneWidth * (item.height / item.width);
|
||||||
}}
|
}}
|
||||||
extraSections={[
|
extraSections={[
|
||||||
{
|
{
|
||||||
|
|||||||
+16
-16
@@ -22,7 +22,7 @@ export interface GifPickerLoadingSkeletonLayoutItem extends GifPickerLoadingSkel
|
|||||||
export interface GifPickerLoadingSkeletonLayoutInput {
|
export interface GifPickerLoadingSkeletonLayoutInput {
|
||||||
viewportWidth: number;
|
viewportWidth: number;
|
||||||
viewportHeight: number;
|
viewportHeight: number;
|
||||||
itemGutter?: number;
|
tileSpacing?: number;
|
||||||
paddingPx?: number;
|
paddingPx?: number;
|
||||||
overscanPx?: number;
|
overscanPx?: number;
|
||||||
}
|
}
|
||||||
@@ -61,19 +61,19 @@ export const GIF_PICKER_LOADING_SKELETON_GIF_SPECS: ReadonlyArray<GifPickerLoadi
|
|||||||
export function buildGifPickerLoadingSkeletonLayout({
|
export function buildGifPickerLoadingSkeletonLayout({
|
||||||
viewportWidth,
|
viewportWidth,
|
||||||
viewportHeight,
|
viewportHeight,
|
||||||
itemGutter = 8,
|
tileSpacing = 8,
|
||||||
paddingPx = MASONRY_PADDING_PX,
|
paddingPx = MASONRY_PADDING_PX,
|
||||||
overscanPx = MASONRY_PADDING_PX * 2,
|
overscanPx = MASONRY_PADDING_PX * 2,
|
||||||
}: GifPickerLoadingSkeletonLayoutInput): Array<GifPickerLoadingSkeletonLayoutItem> {
|
}: GifPickerLoadingSkeletonLayoutInput): Array<GifPickerLoadingSkeletonLayoutItem> {
|
||||||
if (viewportWidth <= 0 || viewportHeight <= 0) {
|
if (viewportWidth <= 0 || viewportHeight <= 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const columns = computeMasonryColumns(viewportWidth, itemGutter, {minColumns: 2});
|
const columns = computeMasonryColumns(viewportWidth, tileSpacing, {minColumns: 2});
|
||||||
const columnWidth = (viewportWidth - paddingPx * 2 - itemGutter * Math.max(0, columns - 1)) / Math.max(1, columns);
|
const laneWidth = (viewportWidth - paddingPx * 2 - tileSpacing * Math.max(0, columns - 1)) / Math.max(1, columns);
|
||||||
if (columnWidth <= 0) {
|
if (laneWidth <= 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const columnHeights = Array.from({length: columns}, () => paddingPx);
|
const laneFill = Array.from({length: columns}, () => paddingPx);
|
||||||
const visibleBottom = viewportHeight + overscanPx;
|
const visibleBottom = viewportHeight + overscanPx;
|
||||||
const layout: Array<GifPickerLoadingSkeletonLayoutItem> = [];
|
const layout: Array<GifPickerLoadingSkeletonLayoutItem> = [];
|
||||||
for (let index = 0; index < GIF_PICKER_LOADING_SKELETON_GIF_SPECS.length; index += 1) {
|
for (let index = 0; index < GIF_PICKER_LOADING_SKELETON_GIF_SPECS.length; index += 1) {
|
||||||
@@ -81,11 +81,11 @@ export function buildGifPickerLoadingSkeletonLayout({
|
|||||||
if (!spec) {
|
if (!spec) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const column = findShortestColumnIndex(columnHeights);
|
const column = findShortestColumnIndex(laneFill);
|
||||||
const left = paddingPx + column * (columnWidth + itemGutter);
|
const left = paddingPx + column * (laneWidth + tileSpacing);
|
||||||
const top = columnHeights[column] ?? paddingPx;
|
const top = laneFill[column] ?? paddingPx;
|
||||||
const renderedHeight = columnWidth * (spec.height / spec.width);
|
const renderedHeight = laneWidth * (spec.height / spec.width);
|
||||||
columnHeights[column] = top + renderedHeight + itemGutter;
|
laneFill[column] = top + renderedHeight + tileSpacing;
|
||||||
if (top > visibleBottom) {
|
if (top > visibleBottom) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -95,18 +95,18 @@ export function buildGifPickerLoadingSkeletonLayout({
|
|||||||
column,
|
column,
|
||||||
left,
|
left,
|
||||||
top,
|
top,
|
||||||
renderedWidth: columnWidth,
|
renderedWidth: laneWidth,
|
||||||
renderedHeight,
|
renderedHeight,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return layout;
|
return layout;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findShortestColumnIndex(columnHeights: ReadonlyArray<number>): number {
|
function findShortestColumnIndex(laneFill: ReadonlyArray<number>): number {
|
||||||
let shortestColumn = 0;
|
let shortestColumn = 0;
|
||||||
let shortestHeight = columnHeights[0] ?? 0;
|
let shortestHeight = laneFill[0] ?? 0;
|
||||||
for (let column = 1; column < columnHeights.length; column += 1) {
|
for (let column = 1; column < laneFill.length; column += 1) {
|
||||||
const height = columnHeights[column];
|
const height = laneFill[column];
|
||||||
if (height < shortestHeight) {
|
if (height < shortestHeight) {
|
||||||
shortestColumn = column;
|
shortestColumn = column;
|
||||||
shortestHeight = height;
|
shortestHeight = height;
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ export const MemesGrid = observer(
|
|||||||
scrollTop: number;
|
scrollTop: number;
|
||||||
onContentSizeChange?: (contentSize: number) => void;
|
onContentSizeChange?: (contentSize: number) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const itemGutter = 8;
|
const tileSpacing = 8;
|
||||||
const columns = computeMasonryColumns(viewportWidth, itemGutter);
|
const columns = computeMasonryColumns(viewportWidth, tileSpacing);
|
||||||
const data = useMemo(
|
const data = useMemo(
|
||||||
() =>
|
() =>
|
||||||
memes.map((meme) => ({
|
memes.map((meme) => ({
|
||||||
@@ -46,15 +46,15 @@ export const MemesGrid = observer(
|
|||||||
data={data}
|
data={data}
|
||||||
itemKeys={itemKeys}
|
itemKeys={itemKeys}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
itemGutter={itemGutter}
|
tileSpacing={tileSpacing}
|
||||||
viewportWidth={viewportWidth}
|
viewportWidth={viewportWidth}
|
||||||
viewportHeight={viewportHeight}
|
viewportHeight={viewportHeight}
|
||||||
scrollTop={scrollTop}
|
scrollTop={scrollTop}
|
||||||
onContentSizeChange={onContentSizeChange}
|
onContentSizeChange={onContentSizeChange}
|
||||||
checkSuspension={() => QuickSwitcher.isOpen}
|
checkSuspension={() => QuickSwitcher.isOpen}
|
||||||
onSelectItemKey={handleSelectKey}
|
onSelectItemKey={handleSelectKey}
|
||||||
getItemKey={(item) => item.id}
|
tileKeyOf={(item) => item.id}
|
||||||
getItemHeight={(item, _index, columnWidth) => columnWidth * (item.height / item.width)}
|
tileHeightOf={(item, _index, laneWidth) => laneWidth * (item.height / item.width)}
|
||||||
renderItem={({item, itemKey, coords, isFocused}) => (
|
renderItem={({item, itemKey, coords, isFocused}) => (
|
||||||
<MemeGridItem
|
<MemeGridItem
|
||||||
key={itemKey}
|
key={itemKey}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
export interface ComputeColumnsOptions {
|
export interface ComputeColumnsOptions {
|
||||||
desiredItemWidth?: number;
|
targetTileWidth?: number;
|
||||||
maxColumns?: number;
|
maxLanes?: number;
|
||||||
minColumns?: number;
|
minColumns?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function computeMasonryColumns(
|
export function computeMasonryColumns(
|
||||||
containerWidth: number,
|
containerWidth: number,
|
||||||
itemGutter: number,
|
tileSpacing: number,
|
||||||
options: ComputeColumnsOptions = {},
|
options: ComputeColumnsOptions = {},
|
||||||
): number {
|
): number {
|
||||||
const desiredItemWidth = options.desiredItemWidth ?? 200;
|
const targetTileWidth = options.targetTileWidth ?? 200;
|
||||||
const maxColumns = options.maxColumns ?? 8;
|
const maxLanes = options.maxLanes ?? 8;
|
||||||
const minColumns = options.minColumns ?? 1;
|
const minColumns = options.minColumns ?? 1;
|
||||||
if (containerWidth <= 0) return minColumns;
|
if (containerWidth <= 0) return minColumns;
|
||||||
const columns = Math.floor((containerWidth + itemGutter) / (desiredItemWidth + itemGutter));
|
const columns = Math.floor((containerWidth + tileSpacing) / (targetTileWidth + tileSpacing));
|
||||||
return Math.max(minColumns, Math.min(columns, maxColumns));
|
return Math.max(minColumns, Math.min(columns, maxLanes));
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,18 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
import {useMasonryGridNavigation} from '@app/features/app/hooks/useMasonryGridNavigation';
|
import {useMasonryGridNavigation} from '@app/features/app/hooks/useMasonryGridNavigation';
|
||||||
import {MasonryListComputer} from '@app/features/channel/components/MasonryListComputer';
|
import {
|
||||||
import {MASONRY_OVERSCAN_PX, MASONRY_PADDING_PX} from '@app/features/channel/components/pickers/shared/PickerConstants';
|
MASONRY_OVERSCAN_PX,
|
||||||
|
MASONRY_PADDING_PX,
|
||||||
|
MASONRY_SCROLL_CHUNK_PX,
|
||||||
|
} from '@app/features/channel/components/pickers/shared/PickerConstants';
|
||||||
|
import {TileFlowSolver} from '@app/features/channel/components/TileFlowSolver';
|
||||||
import type * as React from 'react';
|
import type * as React from 'react';
|
||||||
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||||
|
|
||||||
type VisibleItemTuple = [itemKey: string, sectionIndex: number, itemIndex: number];
|
type VisibleItemTuple = [itemKey: string, sectionIndex: number, itemIndex: number];
|
||||||
|
|
||||||
interface Coords {
|
interface TileBox {
|
||||||
position: 'absolute' | 'sticky';
|
position: 'absolute' | 'sticky';
|
||||||
left?: number;
|
left?: number;
|
||||||
right?: number;
|
right?: number;
|
||||||
@@ -17,18 +21,18 @@ interface Coords {
|
|||||||
height: number;
|
height: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type CoordsMap = Record<string, Coords>;
|
type TileBoxMap = Record<string, TileBox>;
|
||||||
type VisibleSections = Record<string, Array<VisibleItemTuple>>;
|
type WindowedTiles = Record<string, Array<VisibleItemTuple>>;
|
||||||
|
|
||||||
interface GridCoordinates {
|
interface GridCell {
|
||||||
section: number;
|
section: number;
|
||||||
row: number;
|
row: number;
|
||||||
column: number;
|
column: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GridData {
|
interface NavGrid {
|
||||||
boundaries: Array<number>;
|
rowBase: Array<number>;
|
||||||
coordinates: Record<string, GridCoordinates>;
|
tileCells: Record<string, GridCell>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MasonryExtraSection {
|
export interface MasonryExtraSection {
|
||||||
@@ -43,12 +47,12 @@ export function MasonryVirtualGrid<T>({
|
|||||||
data,
|
data,
|
||||||
itemKeys,
|
itemKeys,
|
||||||
columns,
|
columns,
|
||||||
itemGutter,
|
tileSpacing,
|
||||||
viewportWidth,
|
viewportWidth,
|
||||||
viewportHeight,
|
viewportHeight,
|
||||||
scrollTop,
|
scrollTop,
|
||||||
getItemKey,
|
tileKeyOf,
|
||||||
getItemHeight,
|
tileHeightOf,
|
||||||
onSelectItemKey,
|
onSelectItemKey,
|
||||||
checkSuspension,
|
checkSuspension,
|
||||||
renderItem,
|
renderItem,
|
||||||
@@ -61,15 +65,15 @@ export function MasonryVirtualGrid<T>({
|
|||||||
data: ReadonlyArray<T>;
|
data: ReadonlyArray<T>;
|
||||||
itemKeys: ReadonlyArray<string>;
|
itemKeys: ReadonlyArray<string>;
|
||||||
columns: number;
|
columns: number;
|
||||||
itemGutter: number;
|
tileSpacing: number;
|
||||||
viewportWidth: number;
|
viewportWidth: number;
|
||||||
viewportHeight: number;
|
viewportHeight: number;
|
||||||
scrollTop: number;
|
scrollTop: number;
|
||||||
getItemKey: (item: T, index: number) => string;
|
tileKeyOf: (item: T, index: number) => string;
|
||||||
getItemHeight: (item: T, index: number, columnWidth: number) => number;
|
tileHeightOf: (item: T, index: number, laneWidth: number) => number;
|
||||||
onSelectItemKey: (itemKey: string) => void;
|
onSelectItemKey: (itemKey: string) => void;
|
||||||
checkSuspension: () => boolean;
|
checkSuspension: () => boolean;
|
||||||
renderItem: (args: {item: T; itemKey: string; coords: Coords; isFocused: boolean; index: number}) => React.ReactNode;
|
renderItem: (args: {item: T; itemKey: string; coords: TileBox; isFocused: boolean; index: number}) => React.ReactNode;
|
||||||
onContentSizeChange?: (contentSize: number) => void;
|
onContentSizeChange?: (contentSize: number) => void;
|
||||||
extraSections?: ReadonlyArray<MasonryExtraSection>;
|
extraSections?: ReadonlyArray<MasonryExtraSection>;
|
||||||
overscanPx?: number;
|
overscanPx?: number;
|
||||||
@@ -77,31 +81,31 @@ export function MasonryVirtualGrid<T>({
|
|||||||
bottomPaddingPx?: number;
|
bottomPaddingPx?: number;
|
||||||
}) {
|
}) {
|
||||||
const stableExtraSections = extraSections ?? EMPTY_EXTRA_SECTIONS;
|
const stableExtraSections = extraSections ?? EMPTY_EXTRA_SECTIONS;
|
||||||
const [masonryComputer] = useState(() => new MasonryListComputer());
|
const [masonrySolver] = useState(() => new TileFlowSolver());
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [version, setVersion] = useState(0);
|
const [version, setVersion] = useState(0);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setVersion((v) => v + 1);
|
setVersion((v) => v + 1);
|
||||||
}, [data, columns, itemGutter, viewportWidth, viewportHeight, itemKeys, stableExtraSections]);
|
}, [data, columns, tileSpacing, viewportWidth, viewportHeight, itemKeys, stableExtraSections]);
|
||||||
const sectionCount = 1 + stableExtraSections.length;
|
const sectionCount = 1 + stableExtraSections.length;
|
||||||
const getItemKeyForComputer = useCallback(
|
const tileKeyForSolver = useCallback(
|
||||||
(sectionIndex: number, itemIndex: number): string | null => {
|
(sectionIndex: number, itemIndex: number): string | null => {
|
||||||
if (sectionIndex !== 0) return null;
|
if (sectionIndex !== 0) return null;
|
||||||
const item = data[itemIndex];
|
const item = data[itemIndex];
|
||||||
return item != null ? getItemKey(item, itemIndex) : null;
|
return item != null ? tileKeyOf(item, itemIndex) : null;
|
||||||
},
|
},
|
||||||
[data, getItemKey],
|
[data, tileKeyOf],
|
||||||
);
|
);
|
||||||
const getItemHeightForComputer = useCallback(
|
const tileHeightForSolver = useCallback(
|
||||||
(sectionIndex: number, itemIndex: number, columnWidth: number): number => {
|
(sectionIndex: number, itemIndex: number, laneWidth: number): number => {
|
||||||
if (sectionIndex !== 0) return 0;
|
if (sectionIndex !== 0) return 0;
|
||||||
const item = data[itemIndex];
|
const item = data[itemIndex];
|
||||||
if (item == null) return 0;
|
if (item == null) return 0;
|
||||||
return getItemHeight(item, itemIndex, columnWidth);
|
return tileHeightOf(item, itemIndex, laneWidth);
|
||||||
},
|
},
|
||||||
[data, getItemHeight],
|
[data, tileHeightOf],
|
||||||
);
|
);
|
||||||
const getSectionHeightForComputer = useCallback(
|
const sectionHeightForSolver = useCallback(
|
||||||
(sectionIndex: number): number => {
|
(sectionIndex: number): number => {
|
||||||
if (sectionIndex === 0) return 0;
|
if (sectionIndex === 0) return 0;
|
||||||
const extra = stableExtraSections.find((s) => s.sectionIndex === sectionIndex);
|
const extra = stableExtraSections.find((s) => s.sectionIndex === sectionIndex);
|
||||||
@@ -109,54 +113,59 @@ export function MasonryVirtualGrid<T>({
|
|||||||
},
|
},
|
||||||
[stableExtraSections],
|
[stableExtraSections],
|
||||||
);
|
);
|
||||||
|
const scrollChunkPx = Math.max(1, Math.min(MASONRY_SCROLL_CHUNK_PX, overscanPx));
|
||||||
|
const windowStartChunk = Math.floor(scrollTop / scrollChunkPx);
|
||||||
|
const windowEndChunk = Math.ceil((scrollTop + viewportHeight) / scrollChunkPx);
|
||||||
const masonryState = useMemo(() => {
|
const masonryState = useMemo(() => {
|
||||||
if (viewportWidth <= 0 || viewportHeight <= 0) {
|
if (viewportWidth <= 0 || viewportHeight <= 0) {
|
||||||
return {
|
return {
|
||||||
coordsMap: {} as CoordsMap,
|
tileBoxes: {} as TileBoxMap,
|
||||||
visibleSections: {} as VisibleSections,
|
windowedTiles: {} as WindowedTiles,
|
||||||
totalHeight: 0,
|
contentHeight: 0,
|
||||||
gridData: null,
|
navGrid: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
masonryComputer.mergeProps({
|
masonrySolver.applySettings({
|
||||||
sections: [data.length, ...Array.from({length: sectionCount - 1}, () => 0)],
|
sectionSizes: [data.length, ...Array.from({length: sectionCount - 1}, () => 0)],
|
||||||
columns,
|
laneCount: columns,
|
||||||
itemGutter,
|
tileSpacing,
|
||||||
getItemKey: getItemKeyForComputer,
|
tileKeyOf: tileKeyForSolver,
|
||||||
getItemHeight: getItemHeightForComputer,
|
tileHeightOf: tileHeightForSolver,
|
||||||
getSectionHeight: getSectionHeightForComputer,
|
sectionHeightOf: sectionHeightForSolver,
|
||||||
bufferWidth: viewportWidth,
|
viewportSpan: viewportWidth,
|
||||||
padding: {left: paddingPx, right: paddingPx, top: 0, bottom: 0},
|
padBox: {left: paddingPx, right: paddingPx, top: 0, bottom: 0},
|
||||||
version,
|
layoutStamp: version,
|
||||||
});
|
});
|
||||||
const start = Math.max(0, scrollTop - overscanPx);
|
const start = Math.max(0, windowStartChunk * scrollChunkPx - overscanPx);
|
||||||
const end = scrollTop + viewportHeight + overscanPx;
|
const end = windowEndChunk * scrollChunkPx + overscanPx;
|
||||||
masonryComputer.computeVisibleSections(start, end);
|
masonrySolver.selectWindow(start, end);
|
||||||
const state = masonryComputer.getState() as {
|
const state = masonrySolver.readLayout() as {
|
||||||
coordsMap: CoordsMap;
|
tileBoxes: TileBoxMap;
|
||||||
visibleSections: VisibleSections;
|
windowedTiles: WindowedTiles;
|
||||||
totalHeight: number;
|
contentHeight: number;
|
||||||
gridData: GridData;
|
navGrid: NavGrid;
|
||||||
};
|
};
|
||||||
return state;
|
return state;
|
||||||
}, [
|
}, [
|
||||||
masonryComputer,
|
masonrySolver,
|
||||||
data.length,
|
data.length,
|
||||||
sectionCount,
|
sectionCount,
|
||||||
columns,
|
columns,
|
||||||
itemGutter,
|
tileSpacing,
|
||||||
getItemKeyForComputer,
|
tileKeyForSolver,
|
||||||
getItemHeightForComputer,
|
tileHeightForSolver,
|
||||||
getSectionHeightForComputer,
|
sectionHeightForSolver,
|
||||||
viewportWidth,
|
viewportWidth,
|
||||||
viewportHeight,
|
viewportHeight,
|
||||||
scrollTop,
|
windowStartChunk,
|
||||||
|
windowEndChunk,
|
||||||
|
scrollChunkPx,
|
||||||
overscanPx,
|
overscanPx,
|
||||||
paddingPx,
|
paddingPx,
|
||||||
version,
|
version,
|
||||||
]);
|
]);
|
||||||
const {focusedItemKey} = useMasonryGridNavigation({
|
const {focusedItemKey} = useMasonryGridNavigation({
|
||||||
gridData: masonryState.gridData,
|
navGrid: masonryState.navGrid,
|
||||||
itemKeys,
|
itemKeys,
|
||||||
columns,
|
columns,
|
||||||
onSelect: onSelectItemKey,
|
onSelect: onSelectItemKey,
|
||||||
@@ -164,11 +173,11 @@ export function MasonryVirtualGrid<T>({
|
|||||||
checkSuspension,
|
checkSuspension,
|
||||||
});
|
});
|
||||||
const topPadding = paddingPx;
|
const topPadding = paddingPx;
|
||||||
const contentSize = masonryState.totalHeight + topPadding + bottomPaddingPx;
|
const contentSize = masonryState.contentHeight + topPadding + bottomPaddingPx;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onContentSizeChange?.(contentSize);
|
onContentSizeChange?.(contentSize);
|
||||||
}, [contentSize, onContentSizeChange]);
|
}, [contentSize, onContentSizeChange]);
|
||||||
const visibleEntries = Object.entries(masonryState.visibleSections) as Array<[string, VisibleSections[string]]>;
|
const visibleEntries = Object.entries(masonryState.windowedTiles) as Array<[string, WindowedTiles[string]]>;
|
||||||
const parseSectionIndex = (sectionKey: string): number | null => {
|
const parseSectionIndex = (sectionKey: string): number | null => {
|
||||||
const prefix = 'section-';
|
const prefix = 'section-';
|
||||||
if (!sectionKey.startsWith(prefix)) return null;
|
if (!sectionKey.startsWith(prefix)) return null;
|
||||||
@@ -189,7 +198,7 @@ export function MasonryVirtualGrid<T>({
|
|||||||
data-flx="channel.pickers.masonry-virtual-grid.div"
|
data-flx="channel.pickers.masonry-virtual-grid.div"
|
||||||
>
|
>
|
||||||
{visibleEntries.flatMap(([sectionKey, items]): Array<React.ReactNode> => {
|
{visibleEntries.flatMap(([sectionKey, items]): Array<React.ReactNode> => {
|
||||||
const sectionCoords = masonryState.coordsMap[sectionKey];
|
const sectionCoords = masonryState.tileBoxes[sectionKey];
|
||||||
if (!sectionCoords) return [];
|
if (!sectionCoords) return [];
|
||||||
const sectionTop = (sectionCoords.top ?? 0) + topPadding;
|
const sectionTop = (sectionCoords.top ?? 0) + topPadding;
|
||||||
const sectionIndex = parseSectionIndex(sectionKey);
|
const sectionIndex = parseSectionIndex(sectionKey);
|
||||||
@@ -216,11 +225,11 @@ export function MasonryVirtualGrid<T>({
|
|||||||
}
|
}
|
||||||
return items.flatMap(([itemKey, itemSectionIndex, itemIndex]): Array<React.ReactNode> => {
|
return items.flatMap(([itemKey, itemSectionIndex, itemIndex]): Array<React.ReactNode> => {
|
||||||
if (itemSectionIndex !== 0) return [];
|
if (itemSectionIndex !== 0) return [];
|
||||||
const itemCoords = masonryState.coordsMap[itemKey];
|
const itemCoords = masonryState.tileBoxes[itemKey];
|
||||||
if (!itemCoords) return [];
|
if (!itemCoords) return [];
|
||||||
const item = data[itemIndex];
|
const item = data[itemIndex];
|
||||||
if (item == null) return [];
|
if (item == null) return [];
|
||||||
const absoluteItemCoords: Coords = {
|
const absoluteItemCoords: TileBox = {
|
||||||
...itemCoords,
|
...itemCoords,
|
||||||
top: (itemCoords.top ?? 0) + sectionTop,
|
top: (itemCoords.top ?? 0) + sectionTop,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,3 +2,4 @@
|
|||||||
|
|
||||||
export const MASONRY_OVERSCAN_PX = 480;
|
export const MASONRY_OVERSCAN_PX = 480;
|
||||||
export const MASONRY_PADDING_PX = 12;
|
export const MASONRY_PADDING_PX = 12;
|
||||||
|
export const MASONRY_SCROLL_CHUNK_PX = 128;
|
||||||
Reference in new issue
Block a user