mirror of
https://github.com/microsoft/vscode.git
synced 2026-09-07 07:35:00 +01:00
130 lines
4.9 KiB
TypeScript
130 lines
4.9 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import { BugIndicatingError } from '../../../../base/common/errors.js';
|
|
import { OffsetRange } from '../../../common/core/ranges/offsetRange.js';
|
|
|
|
export type CompressedVirtualizedItemVisibility = 'before' | 'visible' | 'after';
|
|
|
|
export interface ICompressedVirtualizedItemLayout {
|
|
/** Complete item range in outer scroll coordinates. */
|
|
readonly contentRange: OffsetRange;
|
|
/** Item range in the compressed rendered coordinate system. */
|
|
readonly renderedRange: OffsetRange;
|
|
/** Height removed from the rendered row and represented by item-local scrolling. */
|
|
readonly maxScrollOffset: number;
|
|
/**
|
|
* Item-local scroll coordinates:
|
|
*
|
|
* 0 ---------------------------- maxScrollOffset
|
|
* item begins item end is aligned
|
|
*/
|
|
readonly scrollOffset: number;
|
|
readonly visibility: CompressedVirtualizedItemVisibility;
|
|
}
|
|
|
|
export interface ICompressedVirtualizedScrollLayout {
|
|
/** Clamped position in the complete outer scroll coordinates. */
|
|
readonly scrollTop: number;
|
|
/** Complete height of all items and inter-item gaps. */
|
|
readonly scrollHeight: number;
|
|
/** Height occupied by the compressed item rows and inter-item gaps. */
|
|
readonly renderedHeight: number;
|
|
readonly contentViewport: OffsetRange;
|
|
readonly renderedViewport: OffsetRange;
|
|
/** Full-content height above the viewport represented by item-local scrolling instead of DOM movement. */
|
|
readonly hiddenContentHeightAboveViewport: number;
|
|
readonly items: readonly ICompressedVirtualizedItemLayout[];
|
|
}
|
|
|
|
export interface ICompressedVirtualizedScrollLayoutInput {
|
|
readonly scrollTop: number;
|
|
readonly viewportHeight: number;
|
|
readonly itemGap: number;
|
|
readonly itemHeights: readonly number[];
|
|
}
|
|
|
|
/**
|
|
* Computes the complete and compressed vertical layout of a virtualized list.
|
|
*/
|
|
export function computeCompressedVirtualizedScrollLayout(input: ICompressedVirtualizedScrollLayoutInput): ICompressedVirtualizedScrollLayout {
|
|
assertNonNegative('viewportHeight', input.viewportHeight);
|
|
const scrollHeight = computeCompressedVirtualizedScrollHeight(input.itemHeights, input.itemGap);
|
|
const maxScrollTop = Math.max(0, scrollHeight - input.viewportHeight);
|
|
const scrollTop = Math.max(0, Math.min(input.scrollTop, maxScrollTop));
|
|
const contentViewport = OffsetRange.ofStartAndLength(scrollTop, input.viewportHeight);
|
|
|
|
let contentTop = 0;
|
|
let renderedTop = 0;
|
|
let hiddenContentHeightAboveViewport = 0;
|
|
const items: ICompressedVirtualizedItemLayout[] = [];
|
|
|
|
for (let index = 0; index < input.itemHeights.length; index++) {
|
|
const fullHeight = input.itemHeights[index];
|
|
const renderedHeight = Math.min(fullHeight, input.viewportHeight);
|
|
const maxScrollOffset = fullHeight - renderedHeight;
|
|
const contentRange = OffsetRange.ofStartAndLength(contentTop, fullHeight);
|
|
const renderedRange = OffsetRange.ofStartAndLength(renderedTop, renderedHeight);
|
|
|
|
let visibility: CompressedVirtualizedItemVisibility;
|
|
let scrollOffset: number;
|
|
if (contentRange.isBefore(contentViewport)) {
|
|
visibility = 'before';
|
|
scrollOffset = maxScrollOffset;
|
|
} else if (contentRange.isAfter(contentViewport)) {
|
|
visibility = 'after';
|
|
scrollOffset = 0;
|
|
} else {
|
|
visibility = 'visible';
|
|
scrollOffset = Math.max(0, Math.min(contentViewport.start - contentRange.start, maxScrollOffset));
|
|
}
|
|
|
|
hiddenContentHeightAboveViewport += scrollOffset;
|
|
items.push({
|
|
contentRange,
|
|
renderedRange,
|
|
maxScrollOffset,
|
|
scrollOffset,
|
|
visibility,
|
|
});
|
|
|
|
if (index < input.itemHeights.length - 1) {
|
|
contentTop += fullHeight + input.itemGap;
|
|
renderedTop += renderedHeight + input.itemGap;
|
|
} else {
|
|
contentTop += fullHeight;
|
|
renderedTop += renderedHeight;
|
|
}
|
|
}
|
|
|
|
const renderedScrollTop = scrollTop - hiddenContentHeightAboveViewport;
|
|
return {
|
|
scrollTop,
|
|
scrollHeight,
|
|
renderedHeight: renderedTop,
|
|
contentViewport,
|
|
renderedViewport: OffsetRange.ofStartAndLength(renderedScrollTop, input.viewportHeight),
|
|
hiddenContentHeightAboveViewport,
|
|
items,
|
|
};
|
|
}
|
|
|
|
export function computeCompressedVirtualizedScrollHeight(itemHeights: readonly number[], itemGap: number): number {
|
|
assertNonNegative('itemGap', itemGap);
|
|
for (let i = 0; i < itemHeights.length; i++) {
|
|
assertNonNegative(`itemHeights[${i}]`, itemHeights[i]);
|
|
}
|
|
if (itemHeights.length === 0) {
|
|
return 0;
|
|
}
|
|
return itemHeights.reduce((result, height) => result + height, 0) + itemGap * (itemHeights.length - 1);
|
|
}
|
|
|
|
function assertNonNegative(name: string, value: number): void {
|
|
if (!Number.isFinite(value) || value < 0) {
|
|
throw new BugIndicatingError(`${name} must be a finite non-negative number, got ${value}`);
|
|
}
|
|
}
|