Merge pull request #178627 from microsoft/rebornix/awake-lion

List View top padding
This commit is contained in:
Peng Lyu
2023-07-11 15:39:23 -07:00
committed by GitHub
9 changed files with 140 additions and 32 deletions
+1 -1
View File
@@ -108,7 +108,7 @@ export interface IPagedListOptions<T> {
readonly mouseSupport?: boolean;
readonly horizontalScrolling?: boolean;
readonly scrollByPage?: boolean;
readonly additionalScrollHeight?: number;
readonly paddingBottom?: number;
}
function fromPagedListOptions<T>(modelProvider: () => IPagedModel<T>, options: IPagedListOptions<T>): IListOptions<number> {
+25 -8
View File
@@ -56,12 +56,13 @@ export interface IListViewAccessibilityProvider<T> {
}
export interface IListViewOptionsUpdate {
readonly additionalScrollHeight?: number;
readonly smoothScrolling?: boolean;
readonly horizontalScrolling?: boolean;
readonly scrollByPage?: boolean;
readonly mouseWheelScrollSensitivity?: number;
readonly fastScrollSensitivity?: number;
readonly paddingTop?: number;
readonly paddingBottom?: number;
}
export interface IListViewOptions<T> extends IListViewOptionsUpdate {
@@ -298,7 +299,7 @@ export class ListView<T> implements IListView<T> {
private setRowLineHeight: boolean;
private setRowHeight: boolean;
private supportDynamicHeights: boolean;
private additionalScrollHeight: number;
private paddingBottom: number;
private accessibilityProvider: ListViewAccessibilityProvider<T>;
private scrollWidth: number | undefined;
@@ -364,7 +365,7 @@ export class ListView<T> implements IListView<T> {
this.items = [];
this.itemId = 0;
this.rangeMap = new RangeMap();
this.rangeMap = new RangeMap(options.paddingTop ?? 0);
for (const renderer of renderers) {
this.renderers.set(renderer.templateId, renderer);
@@ -386,7 +387,7 @@ export class ListView<T> implements IListView<T> {
this._horizontalScrolling = options.horizontalScrolling ?? DefaultOptions.horizontalScrolling;
this.domNode.classList.toggle('horizontal-scrolling', this._horizontalScrolling);
this.additionalScrollHeight = typeof options.additionalScrollHeight === 'undefined' ? 0 : options.additionalScrollHeight;
this.paddingBottom = typeof options.paddingBottom === 'undefined' ? 0 : options.paddingBottom;
this.accessibilityProvider = new ListViewAccessibilityProvider(options.accessibilityProvider);
@@ -441,8 +442,8 @@ export class ListView<T> implements IListView<T> {
}
updateOptions(options: IListViewOptionsUpdate) {
if (options.additionalScrollHeight !== undefined) {
this.additionalScrollHeight = options.additionalScrollHeight;
if (options.paddingBottom !== undefined) {
this.paddingBottom = options.paddingBottom;
this.scrollableElement.setScrollDimensions({ scrollHeight: this.scrollHeight });
}
@@ -471,6 +472,22 @@ export class ListView<T> implements IListView<T> {
if (scrollableOptions) {
this.scrollableElement.updateOptions(scrollableOptions);
}
if (options.paddingTop !== undefined && options.paddingTop !== this.rangeMap.paddingTop) {
// trigger a rerender
const lastRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight);
const offset = options.paddingTop - this.rangeMap.paddingTop;
this.rangeMap.paddingTop = options.paddingTop;
this.render(lastRenderRange, Math.max(0, this.lastRenderTop + offset), this.lastRenderHeight, undefined, undefined, true);
this.setScrollTop(this.lastRenderTop);
this.eventuallyUpdateScrollDimensions();
if (this.supportDynamicHeights) {
this._rerender(this.lastRenderTop, this.lastRenderHeight);
}
}
}
delegateScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent) {
@@ -602,7 +619,7 @@ export class ListView<T> implements IListView<T> {
// TODO@joao: improve this optimization to catch even more cases
if (start === 0 && deleteCount >= this.items.length) {
this.rangeMap = new RangeMap();
this.rangeMap = new RangeMap(this.rangeMap.paddingTop);
this.rangeMap.splice(0, 0, inserted);
deleted = this.items;
this.items = inserted;
@@ -1017,7 +1034,7 @@ export class ListView<T> implements IListView<T> {
}
get scrollHeight(): number {
return this._scrollHeight + (this.horizontalScrolling ? 10 : 0) + this.additionalScrollHeight;
return this._scrollHeight + (this.horizontalScrolling ? 10 : 0) + this.paddingBottom;
}
// Events
+2 -1
View File
@@ -991,12 +991,13 @@ export interface IListOptions<T> extends IListOptionsUpdate {
readonly mouseSupport?: boolean;
readonly horizontalScrolling?: boolean;
readonly scrollByPage?: boolean;
readonly additionalScrollHeight?: number;
readonly transformOptimization?: boolean;
readonly smoothScrolling?: boolean;
readonly scrollableElementChangeOptions?: ScrollableElementChangeOptions;
readonly alwaysConsumeMouseWheel?: boolean;
readonly initialSize?: Dimension;
readonly paddingTop?: number;
readonly paddingBottom?: number;
}
export interface IListStyles {
+22 -3
View File
@@ -91,6 +91,21 @@ export class RangeMap {
private groups: IRangedGroup[] = [];
private _size = 0;
private _paddingTop = 0;
get paddingTop() {
return this._paddingTop;
}
set paddingTop(paddingTop: number) {
this._size = this._size + paddingTop - this._paddingTop;
this._paddingTop = paddingTop;
}
constructor(topPadding?: number) {
this._paddingTop = topPadding ?? 0;
this._size = this._paddingTop;
}
splice(index: number, deleteCount: number, items: IItem[] = []): void {
const diff = items.length - deleteCount;
@@ -104,7 +119,7 @@ export class RangeMap {
}));
this.groups = concat(before, middle, after);
this._size = this.groups.reduce((t, g) => t + (g.size * (g.range.end - g.range.start)), 0);
this._size = this._paddingTop + this.groups.reduce((t, g) => t + (g.size * (g.range.end - g.range.start)), 0);
}
/**
@@ -135,8 +150,12 @@ export class RangeMap {
return -1;
}
if (position < this._paddingTop) {
return 0;
}
let index = 0;
let size = 0;
let size = this._paddingTop;
for (const group of this.groups) {
const count = group.range.end - group.range.start;
@@ -177,7 +196,7 @@ export class RangeMap {
const newCount = count + groupCount;
if (index < newCount) {
return position + ((index - count) * group.size);
return this._paddingTop + position + ((index - count) * group.size);
}
position += groupCount * group.size;
@@ -343,3 +343,83 @@ suite('RangeMap', () => {
});
});
});
suite('RangeMap with top padding', () => {
let rangeMap: RangeMap;
setup(() => {
rangeMap = new RangeMap(10);
});
test('empty', () => {
assert.strictEqual(rangeMap.size, 10);
assert.strictEqual(rangeMap.count, 0);
});
const one = { size: 1 };
const five = { size: 5 };
const ten = { size: 10 };
test('length & count', () => {
rangeMap.splice(0, 0, [one]);
assert.strictEqual(rangeMap.size, 11);
assert.strictEqual(rangeMap.count, 1);
});
test('length & count #2', () => {
rangeMap.splice(0, 0, [one, one, one, one, one]);
assert.strictEqual(rangeMap.size, 15);
assert.strictEqual(rangeMap.count, 5);
});
test('length & count #3', () => {
rangeMap.splice(0, 0, [five]);
assert.strictEqual(rangeMap.size, 15);
assert.strictEqual(rangeMap.count, 1);
});
test('length & count #4', () => {
rangeMap.splice(0, 0, [five, five, five, five, five]);
assert.strictEqual(rangeMap.size, 35);
assert.strictEqual(rangeMap.count, 5);
});
test('insert', () => {
rangeMap.splice(0, 0, [five, five, five, five, five]);
assert.strictEqual(rangeMap.size, 35);
assert.strictEqual(rangeMap.count, 5);
rangeMap.splice(0, 0, [five, five, five, five, five]);
assert.strictEqual(rangeMap.size, 60);
assert.strictEqual(rangeMap.count, 10);
rangeMap.splice(5, 0, [ten, ten]);
assert.strictEqual(rangeMap.size, 80);
assert.strictEqual(rangeMap.count, 12);
rangeMap.splice(12, 0, [{ size: 200 }]);
assert.strictEqual(rangeMap.size, 280);
assert.strictEqual(rangeMap.count, 13);
});
suite('indexAt, positionAt', () => {
test('empty', () => {
assert.strictEqual(rangeMap.indexAt(0), 0);
assert.strictEqual(rangeMap.indexAt(10), 0);
assert.strictEqual(rangeMap.indexAt(-1), -1);
assert.strictEqual(rangeMap.positionAt(0), -1);
assert.strictEqual(rangeMap.positionAt(10), -1);
assert.strictEqual(rangeMap.positionAt(-1), -1);
});
test('simple', () => {
rangeMap.splice(0, 0, [one]);
assert.strictEqual(rangeMap.indexAt(0), 0);
assert.strictEqual(rangeMap.indexAt(1), 0);
assert.strictEqual(rangeMap.indexAt(10), 0);
assert.strictEqual(rangeMap.indexAt(11), 1);
assert.strictEqual(rangeMap.positionAt(0), 10);
assert.strictEqual(rangeMap.positionAt(1), -1);
});
});
});
@@ -268,7 +268,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD
mouseSupport: true,
multipleSelectionSupport: false,
typeNavigationEnabled: true,
additionalScrollHeight: 0,
paddingBottom: 0,
// transformOptimization: (isMacintosh && isNative) || getTitleBarStyle(this.configurationService, this.environmentService) === 'native',
styleController: (_suffix: string) => { return this._list!; },
overrideStyles: {
@@ -754,15 +754,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD
}`);
}
// top insert toolbar
const topInsertToolbarHeight = this._notebookOptions.computeTopInsertToolbarHeight(this.viewModel?.viewType);
styleSheets.push(`.notebookOverlay .cell-list-top-cell-toolbar-container { top: -${topInsertToolbarHeight - 3}px }`);
styleSheets.push(`.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element,
.notebookOverlay > .cell-list-container > .notebook-gutter > .monaco-list > .monaco-scrollable-element {
padding-top: ${topInsertToolbarHeight}px !important;
box-sizing: border-box;
}`);
styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .code-cell-row div.cell.code { margin-left: ${codeCellLeftMargin + cellRunGutter}px; }`);
styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row div.cell { margin-right: ${cellRightMargin}px; }`);
styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .cell-inner-container { padding-top: ${cellTopMargin}px; }`);
@@ -878,7 +869,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD
multipleSelectionSupport: true,
selectionNavigation: true,
typeNavigationEnabled: true,
additionalScrollHeight: 0,
paddingTop: this._notebookOptions.computeTopInsertToolbarHeight(this.viewModel?.viewType),
paddingBottom: 0,
transformOptimization: false, //(isMacintosh && isNative) || getTitleBarStyle(this.configurationService, this.environmentService) === 'native',
initialSize: this._dimension,
styleController: (_suffix: string) => { return this._list; },
@@ -1477,8 +1469,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD
}));
if (this._dimension) {
const topInserToolbarHeight = this._notebookOptions.computeTopInsertToolbarHeight(this.viewModel?.viewType);
this._list.layout(this._dimension.height - topInserToolbarHeight, this._dimension.width);
this._list.layout(this._dimension.height, this._dimension.width);
} else {
this._list.layout();
}
@@ -1772,15 +1763,15 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD
DOM.size(this._body, dimension.width, newBodyHeight);
const topInserToolbarHeight = this._notebookOptions.computeTopInsertToolbarHeight(this.viewModel?.viewType);
const newCellListHeight = Math.max(newBodyHeight - topInserToolbarHeight, 0);
const newCellListHeight = newBodyHeight;
if (this._list.getRenderHeight() < newCellListHeight) {
// the new dimension is larger than the list viewport, update its additional height first, otherwise the list view will move down a bit (as the `scrollBottom` will move down)
this._list.updateOptions({ additionalScrollHeight: this._allowScrollBeyondLastLine() ? Math.max(0, (newCellListHeight - 50)) : topInserToolbarHeight });
this._list.updateOptions({ paddingBottom: this._allowScrollBeyondLastLine() ? Math.max(0, (newCellListHeight - 50)) : 0, paddingTop: topInserToolbarHeight });
this._list.layout(newCellListHeight, dimension.width);
} else {
// the new dimension is smaller than the list viewport, if we update the additional height, the `scrollBottom` will move up, which moves the whole list view upwards a bit. So we run a layout first.
this._list.layout(newCellListHeight, dimension.width);
this._list.updateOptions({ additionalScrollHeight: this._allowScrollBeyondLastLine() ? Math.max(0, (newCellListHeight - 50)) : topInserToolbarHeight });
this._list.updateOptions({ paddingBottom: this._allowScrollBeyondLastLine() ? Math.max(0, (newCellListHeight - 50)) : 0, paddingTop: topInserToolbarHeight });
}
this._overlayContainer.style.visibility = 'visible';
@@ -139,8 +139,8 @@ suite('NotebookCellList', () => {
});
const cellList = createNotebookCellList(instantiationService);
// without additionalscrollheight, the last 20 px will always be hidden due to `topInsertToolbarHeight`
cellList.updateOptions({ additionalScrollHeight: 100 });
// without paddingBottom, the last 20 px will always be hidden due to `topInsertToolbarHeight`
cellList.updateOptions({ paddingBottom: 100 });
cellList.attachViewModel(viewModel);
// render height 210, it can render 3 full cells and 1 partial cell
@@ -97,7 +97,7 @@ export class TerminalTabList extends WorkbenchList<ITerminalInstance> {
accessibilityProvider: instantiationService.createInstance(TerminalTabsAccessibilityProvider),
smoothScrolling: _configurationService.getValue<boolean>('workbench.list.smoothScrolling'),
multipleSelectionSupport: true,
additionalScrollHeight: TerminalTabsListSizes.TabHeight,
paddingBottom: TerminalTabsListSizes.TabHeight,
dnd: instantiationService.createInstance(TerminalTabsDragAndDrop),
openOnSingleClick: true
},