Files
vscode/src/vs/editor/browser/widget/multiDiffEditor/compressedVirtualizedScrollLayout.ts
T

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}`);
}
}