Skip to content

Commit

Permalink
Merge pull request #198320 from microsoft/benibenj/treeStickyScroll
Browse files Browse the repository at this point in the history
Sticky Scroll Tree View
  • Loading branch information
benibenj authored Nov 21, 2023
2 parents 8af169e + 13b6a07 commit 86f09d8
Show file tree
Hide file tree
Showing 13 changed files with 772 additions and 104 deletions.
5 changes: 5 additions & 0 deletions src/vs/base/browser/ui/list/list.css
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@
touch-action: none;
}

/* Make sure the scrollbar renders above overlays (sticky scroll) */
.monaco-list .monaco-scrollable-element > .scrollbar {
z-index: 14;
}

/* for OS X ballistic scrolling */
.monaco-list-row.scrolling {
display: none !important;
Expand Down
14 changes: 10 additions & 4 deletions src/vs/base/browser/ui/list/listView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ export interface IListView<T> extends ISpliceable<T>, IDisposable {
readonly renderHeight: number;
readonly scrollHeight: number;
readonly firstVisibleIndex: number;
readonly firstMostlyVisibleIndex: number;
readonly lastVisibleIndex: number;
onDidScroll: Event<ScrollEvent>;
onWillScroll: Event<ScrollEvent>;
Expand Down Expand Up @@ -753,16 +754,21 @@ export class ListView<T> implements IListView<T> {

get firstVisibleIndex(): number {
const range = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight);
const firstElTop = this.rangeMap.positionAt(range.start);
const nextElTop = this.rangeMap.positionAt(range.start + 1);
return range.start;
}

get firstMostlyVisibleIndex(): number {
const firstVisibleIndex = this.firstVisibleIndex;
const firstElTop = this.rangeMap.positionAt(firstVisibleIndex);
const nextElTop = this.rangeMap.positionAt(firstVisibleIndex + 1);
if (nextElTop !== -1) {
const firstElMidpoint = (nextElTop - firstElTop) / 2 + firstElTop;
if (firstElMidpoint < this.scrollTop) {
return range.start + 1;
return firstVisibleIndex + 1;
}
}

return range.start;
return firstVisibleIndex;
}

get lastVisibleIndex(): number {
Expand Down
58 changes: 45 additions & 13 deletions src/vs/base/browser/ui/list/listWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,8 +256,8 @@ export function isInputElement(e: HTMLElement): boolean {
return e.tagName === 'INPUT' || e.tagName === 'TEXTAREA';
}

export function isMonacoEditor(e: HTMLElement): boolean {
if (e.classList.contains('monaco-editor')) {
function isListElementDescendantOfClass(e: HTMLElement, className: string): boolean {
if (e.classList.contains(className)) {
return true;
}

Expand All @@ -269,7 +269,27 @@ export function isMonacoEditor(e: HTMLElement): boolean {
return false;
}

return isMonacoEditor(e.parentElement);
return isListElementDescendantOfClass(e.parentElement, className);
}

export function isMonacoEditor(e: HTMLElement): boolean {
return isListElementDescendantOfClass(e, 'monaco-editor');
}

export function isMonacoCustomToggle(e: HTMLElement): boolean {
return isListElementDescendantOfClass(e, 'monaco-custom-toggle');
}

export function isActionItem(e: HTMLElement): boolean {
return isListElementDescendantOfClass(e, 'action-item');
}

export function isMonacoTwistie(e: HTMLElement): boolean {
return isListElementDescendantOfClass(e, 'monaco-tl-twistie');
}

export function isStickyScrollElement(e: HTMLElement): boolean {
return isListElementDescendantOfClass(e, 'monaco-tree-sticky-row');
}

export function isButton(e: HTMLElement): boolean {
Expand Down Expand Up @@ -1598,6 +1618,10 @@ export class List<T> implements ISpliceable<T>, IDisposable {
return this.view.firstVisibleIndex;
}

get firstMostlyVisibleIndex(): number {
return this.view.firstMostlyVisibleIndex;
}

get lastVisibleIndex(): number {
return this.view.lastVisibleIndex;
}
Expand Down Expand Up @@ -1830,7 +1854,7 @@ export class List<T> implements ISpliceable<T>, IDisposable {
return this.getFocus().map(i => this.view.element(i));
}

reveal(index: number, relativeTop?: number): void {
reveal(index: number, relativeTop?: number, paddingTop: number = 0): void {
if (index < 0 || index >= this.length) {
throw new ListError(this.user, `Invalid index ${index}`);
}
Expand All @@ -1841,16 +1865,16 @@ export class List<T> implements ISpliceable<T>, IDisposable {

if (isNumber(relativeTop)) {
// y = mx + b
const m = elementHeight - this.view.renderHeight;
this.view.setScrollTop(m * clamp(relativeTop, 0, 1) + elementTop);
const m = elementHeight - this.view.renderHeight + paddingTop;
this.view.setScrollTop(m * clamp(relativeTop, 0, 1) + elementTop - paddingTop);
} else {
const viewItemBottom = elementTop + elementHeight;
const scrollBottom = scrollTop + this.view.renderHeight;

if (elementTop < scrollTop && viewItemBottom >= scrollBottom) {
if (elementTop < scrollTop + paddingTop && viewItemBottom >= scrollBottom) {
// The element is already overflowing the viewport, no-op
} else if (elementTop < scrollTop || (viewItemBottom >= scrollBottom && elementHeight >= this.view.renderHeight)) {
this.view.setScrollTop(elementTop);
} else if (elementTop < scrollTop + paddingTop || (viewItemBottom >= scrollBottom && elementHeight >= this.view.renderHeight)) {
this.view.setScrollTop(elementTop - paddingTop);
} else if (viewItemBottom >= scrollBottom) {
this.view.setScrollTop(viewItemBottom - this.view.renderHeight);
}
Expand All @@ -1861,7 +1885,7 @@ export class List<T> implements ISpliceable<T>, IDisposable {
* Returns the relative position of an element rendered in the list.
* Returns `null` if the element isn't *entirely* in the visible viewport.
*/
getRelativeTop(index: number): number | null {
getRelativeTop(index: number, paddingTop: number = 0): number | null {
if (index < 0 || index >= this.length) {
throw new ListError(this.user, `Invalid index ${index}`);
}
Expand All @@ -1870,13 +1894,13 @@ export class List<T> implements ISpliceable<T>, IDisposable {
const elementTop = this.view.elementTop(index);
const elementHeight = this.view.elementHeight(index);

if (elementTop < scrollTop || elementTop + elementHeight > scrollTop + this.view.renderHeight) {
if (elementTop < scrollTop + paddingTop || elementTop + elementHeight > scrollTop + this.view.renderHeight) {
return null;
}

// y = mx + b
const m = elementHeight - this.view.renderHeight;
return Math.abs((scrollTop - elementTop) / m);
const m = elementHeight - this.view.renderHeight + paddingTop;
return Math.abs((scrollTop + paddingTop - elementTop) / m);
}

isDOMFocused(): boolean {
Expand All @@ -1887,10 +1911,18 @@ export class List<T> implements ISpliceable<T>, IDisposable {
return this.view.domNode;
}

getScrollableElement(): HTMLElement {
return this.view.scrollableElementDomNode;
}

getElementID(index: number): string {
return this.view.getElementDomId(index);
}

getElementTop(index: number): number {
return this.view.elementTop(index);
}

style(styles: IListStyles): void {
this.styleController.style(styles);
}
Expand Down
Loading

0 comments on commit 86f09d8

Please sign in to comment.