From 90377a73d4802984a0e6be852f70ea56924f81a1 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 21 Aug 2017 18:25:03 +0200 Subject: [PATCH] * separate scroll dimensions and scroll positions * have explicit methods for changing the scroll position (delayed/smooth vs now/direct) that bubble up to the editor internals. * have all other clients of ScrollableElement not use smooth scrolling --- src/vs/base/browser/ui/list/listView.ts | 14 +- .../browser/ui/scrollbar/abstractScrollbar.ts | 26 +- .../ui/scrollbar/horizontalScrollbar.ts | 17 +- .../browser/ui/scrollbar/scrollableElement.ts | 136 +++++---- .../ui/scrollbar/scrollableElementOptions.ts | 6 - .../browser/ui/scrollbar/verticalScrollbar.ts | 17 +- src/vs/base/common/scrollable.ts | 268 ++++++++++++------ src/vs/base/parts/tree/browser/treeView.ts | 16 +- .../editor/browser/controller/mouseHandler.ts | 6 +- .../editor/browser/controller/mouseTarget.ts | 14 +- .../browser/controller/pointerHandler.ts | 18 +- .../editorScrollbar/editorScrollbar.ts | 12 +- .../browser/viewParts/lines/viewLines.ts | 10 +- .../browser/viewParts/minimap/minimap.ts | 4 +- src/vs/editor/common/commonCodeEditor.ts | 10 +- .../editor/common/controller/coreCommands.ts | 2 +- src/vs/editor/common/controller/cursor.ts | 2 +- .../editor/common/controller/cursorCommon.ts | 4 +- src/vs/editor/common/viewLayout/viewLayout.ts | 130 +++++---- src/vs/editor/common/viewModel/viewModel.ts | 14 +- .../editor/common/viewModel/viewModelImpl.ts | 2 +- .../browser/parts/editor/tabsTitleControl.ts | 10 +- .../extensions/browser/extensionEditor.ts | 4 +- .../electron-browser/walkThroughPart.ts | 54 ++-- 24 files changed, 443 insertions(+), 353 deletions(-) diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index ae867ee1e21..235cd3fe9e9 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -124,7 +124,7 @@ export class ListView implements IDisposable { const scrollHeight = this.getContentHeight(); this.rowsContainer.style.height = `${scrollHeight}px`; - this.scrollableElement.updateState({ scrollHeight }); + this.scrollableElement.setScrollDimensions({ scrollHeight }); return deleted.map(i => i.element); } @@ -134,8 +134,8 @@ export class ListView implements IDisposable { } get renderHeight(): number { - const scrollState = this.scrollableElement.getScrollState(); - return scrollState.height; + const scrollDimensions = this.scrollableElement.getScrollDimensions(); + return scrollDimensions.height; } element(index: number): T { @@ -164,7 +164,7 @@ export class ListView implements IDisposable { } layout(height?: number): void { - this.scrollableElement.updateState({ + this.scrollableElement.setScrollDimensions({ height: height || DOM.getContentHeight(this._domNode) }); } @@ -221,12 +221,12 @@ export class ListView implements IDisposable { } getScrollTop(): number { - const scrollState = this.scrollableElement.getScrollState(); - return scrollState.scrollTop; + const scrollPosition = this.scrollableElement.getScrollPosition(); + return scrollPosition.scrollTop; } setScrollTop(scrollTop: number): void { - this.scrollableElement.updateState({ scrollTop }); + this.scrollableElement.setScrollPosition({ scrollTop }); } get scrollTop(): number { diff --git a/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts b/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts index 0473b04bf91..ce17af707bf 100644 --- a/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts +++ b/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts @@ -13,7 +13,7 @@ import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; import { ScrollbarState } from 'vs/base/browser/ui/scrollbar/scrollbarState'; import { ScrollbarArrow, ScrollbarArrowOptions } from 'vs/base/browser/ui/scrollbar/scrollbarArrow'; import { ScrollbarVisibilityController } from 'vs/base/browser/ui/scrollbar/scrollbarVisibilityController'; -import { Scrollable, ScrollbarVisibility } from 'vs/base/common/scrollable'; +import { Scrollable, ScrollbarVisibility, INewScrollPosition } from 'vs/base/common/scrollable'; /** * The orthogonal distance to the slider at which dragging "resets". This implements "snapping" @@ -193,7 +193,7 @@ export abstract class AbstractScrollbar extends Widget { private _onMouseDown(e: IMouseEvent): void { let domNodePosition = DomUtils.getDomNodePagePosition(this.domNode.domNode); - this.setDesiredScrollPosition(this._scrollbarState.getDesiredScrollPositionFromOffset(this._mouseDownRelativePosition(e, domNodePosition)), 0/* immediate */); + this._setDesiredScrollPositionNow(this._scrollbarState.getDesiredScrollPositionFromOffset(this._mouseDownRelativePosition(e, domNodePosition))); if (e.leftButton) { e.preventDefault(); this._sliderMouseDown(e, () => { /*nothing to do*/ }); @@ -214,13 +214,13 @@ export abstract class AbstractScrollbar extends Widget { if (Platform.isWindows && mouseOrthogonalDelta > MOUSE_DRAG_RESET_DISTANCE) { // The mouse has wondered away from the scrollbar => reset dragging - this.setDesiredScrollPosition(initialScrollbarState.getScrollPosition(), 0/* immediate */); + this._setDesiredScrollPositionNow(initialScrollbarState.getScrollPosition()); return; } const mousePosition = this._sliderMousePosition(mouseMoveData); const mouseDelta = mousePosition - initialMousePosition; - this.setDesiredScrollPosition(initialScrollbarState.getDesiredScrollPositionFromDelta(mouseDelta), 0/* immediate */); + this._setDesiredScrollPositionNow(initialScrollbarState.getDesiredScrollPositionFromDelta(mouseDelta)); }, () => { this.slider.toggleClassName('active', false); @@ -232,18 +232,12 @@ export abstract class AbstractScrollbar extends Widget { this._host.onDragStart(); } - public setDesiredScrollPosition(desiredScrollPosition: number, smoothScrollDuration: number): boolean { - desiredScrollPosition = this.validateScrollPosition(desiredScrollPosition); + private _setDesiredScrollPositionNow(_desiredScrollPosition: number): void { - let oldScrollPosition = this._getScrollPosition(); - this._setScrollPosition(desiredScrollPosition, smoothScrollDuration); - let newScrollPosition = this._getScrollPosition(); + let desiredScrollPosition: INewScrollPosition = {}; + this.writeScrollPosition(desiredScrollPosition, _desiredScrollPosition); - if (oldScrollPosition !== newScrollPosition) { - this._onElementScrollPosition(this._getScrollPosition()); - return true; - } - return false; + this._scrollable.setScrollPositionNow(desiredScrollPosition); } // ----------------- Overwrite these @@ -255,7 +249,5 @@ export abstract class AbstractScrollbar extends Widget { protected abstract _sliderMousePosition(e: ISimplifiedMouseEvent): number; protected abstract _sliderOrthogonalMousePosition(e: ISimplifiedMouseEvent): number; - protected abstract _getScrollPosition(): number; - protected abstract _setScrollPosition(elementScrollPosition: number, smoothScrollDuration: number): void; - public abstract validateScrollPosition(desiredScrollPosition: number): number; + public abstract writeScrollPosition(target: INewScrollPosition, scrollPosition: number): void; } diff --git a/src/vs/base/browser/ui/scrollbar/horizontalScrollbar.ts b/src/vs/base/browser/ui/scrollbar/horizontalScrollbar.ts index 6b278275b7e..ac973201db5 100644 --- a/src/vs/base/browser/ui/scrollbar/horizontalScrollbar.ts +++ b/src/vs/base/browser/ui/scrollbar/horizontalScrollbar.ts @@ -8,7 +8,7 @@ import { AbstractScrollbar, ScrollbarHost, ISimplifiedMouseEvent } from 'vs/base import { StandardMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { IDomNodePagePosition } from 'vs/base/browser/dom'; import { ScrollableElementResolvedOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions'; -import { Scrollable, ScrollEvent, ScrollbarVisibility } from 'vs/base/common/scrollable'; +import { Scrollable, ScrollEvent, ScrollbarVisibility, INewScrollPosition } from 'vs/base/common/scrollable'; import { ScrollbarState } from 'vs/base/browser/ui/scrollbar/scrollbarState'; import { ARROW_IMG_SIZE } from 'vs/base/browser/ui/scrollbar/scrollbarArrow'; @@ -89,18 +89,7 @@ export class HorizontalScrollbar extends AbstractScrollbar { return e.posy; } - protected _getScrollPosition(): number { - const scrollState = this._scrollable.getState(); - return scrollState.scrollLeft; - } - - protected _setScrollPosition(scrollPosition: number, smoothScrollDuration: number) { - this._scrollable.updateState({ - scrollLeft: scrollPosition - }, smoothScrollDuration); - } - - public validateScrollPosition(desiredScrollPosition: number): number { - return this._scrollable.validateScrollLeft(desiredScrollPosition); + public writeScrollPosition(target: INewScrollPosition, scrollPosition: number): void { + target.scrollLeft = scrollPosition; } } diff --git a/src/vs/base/browser/ui/scrollbar/scrollableElement.ts b/src/vs/base/browser/ui/scrollbar/scrollableElement.ts index 10ca3fe886d..b729ed33b05 100644 --- a/src/vs/base/browser/ui/scrollbar/scrollableElement.ts +++ b/src/vs/base/browser/ui/scrollbar/scrollableElement.ts @@ -13,7 +13,7 @@ import { HorizontalScrollbar } from 'vs/base/browser/ui/scrollbar/horizontalScro import { VerticalScrollbar } from 'vs/base/browser/ui/scrollbar/verticalScrollbar'; import { ScrollableElementCreationOptions, ScrollableElementChangeOptions, ScrollableElementResolvedOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { Scrollable, ScrollState, ScrollEvent, INewScrollState, ScrollbarVisibility } from 'vs/base/common/scrollable'; +import { Scrollable, ScrollEvent, ScrollbarVisibility, INewScrollDimensions, IScrollDimensions, INewScrollPosition, IScrollPosition } from 'vs/base/common/scrollable'; import { Widget } from 'vs/base/browser/ui/widget'; import { TimeoutTimer } from 'vs/base/common/async'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; @@ -29,41 +29,36 @@ export interface IOverviewRulerLayoutInfo { insertBefore: HTMLElement; } -export class ScrollableElement extends Widget { +export abstract class AbstractScrollableElement extends Widget { - private _options: ScrollableElementResolvedOptions; - private _scrollable: Scrollable; - private _verticalScrollbar: VerticalScrollbar; - private _horizontalScrollbar: HorizontalScrollbar; - private _domNode: HTMLElement; + private readonly _options: ScrollableElementResolvedOptions; + protected readonly _scrollable: Scrollable; + private readonly _verticalScrollbar: VerticalScrollbar; + private readonly _horizontalScrollbar: HorizontalScrollbar; + private readonly _domNode: HTMLElement; - private _leftShadowDomNode: FastDomNode; - private _topShadowDomNode: FastDomNode; - private _topLeftShadowDomNode: FastDomNode; + private readonly _leftShadowDomNode: FastDomNode; + private readonly _topShadowDomNode: FastDomNode; + private readonly _topLeftShadowDomNode: FastDomNode; - private _listenOnDomNode: HTMLElement; + private readonly _listenOnDomNode: HTMLElement; private _mouseWheelToDispose: IDisposable[]; private _isDragging: boolean; private _mouseIsOver: boolean; - private _hideTimeout: TimeoutTimer; + private readonly _hideTimeout: TimeoutTimer; private _shouldRender: boolean; - private _onScroll = this._register(new Emitter()); + private readonly _onScroll = this._register(new Emitter()); public onScroll: Event = this._onScroll.event; - constructor(element: HTMLElement, options: ScrollableElementCreationOptions, scrollable?: Scrollable) { + protected constructor(element: HTMLElement, options: ScrollableElementCreationOptions, scrollable?: Scrollable) { super(); element.style.overflow = 'hidden'; this._options = resolveOptions(options); - - if (typeof scrollable === 'undefined') { - this._scrollable = this._register(new Scrollable()); - } else { - this._scrollable = scrollable; - } + this._scrollable = scrollable; this._register(this._scrollable.onScroll((e) => { this._onDidScroll(e); @@ -153,12 +148,12 @@ export class ScrollableElement extends Widget { this._verticalScrollbar.delegateSliderMouseDown(e, onDragFinished); } - public updateState(newState: INewScrollState): void { - this._scrollable.updateState(newState, 0/* immediate */); + public getScrollDimensions(): IScrollDimensions { + return this._scrollable.getScrollDimensions(); } - public getScrollState(): ScrollState { - return this._scrollable.getState(); + public setScrollDimensions(dimensions: INewScrollDimensions): void { + this._scrollable.setScrollDimensions(dimensions); } /** @@ -215,8 +210,6 @@ export class ScrollableElement extends Widget { } private _onMouseWheel(e: StandardMouseWheelEvent): void { - let desiredScrollTop = -1; - let desiredScrollLeft = -1; if (e.deltaY || e.deltaX) { let deltaY = e.deltaY * this._options.mouseWheelScrollSensitivity; @@ -244,37 +237,39 @@ export class ScrollableElement extends Widget { } } - const scrollState = this._scrollable.getSmoothScrollTargetState(); + const futureScrollPosition = this._scrollable.getFutureScrollPosition(); + + let desiredScrollPosition: INewScrollPosition = {}; if (deltaY) { - let currentScrollTop = scrollState.scrollTop; - desiredScrollTop = this._verticalScrollbar.validateScrollPosition((desiredScrollTop !== -1 ? desiredScrollTop : currentScrollTop) - SCROLL_WHEEL_SENSITIVITY * deltaY); - if (desiredScrollTop === currentScrollTop) { - desiredScrollTop = -1; - } + const desiredScrollTop = futureScrollPosition.scrollTop - SCROLL_WHEEL_SENSITIVITY * deltaY; + this._verticalScrollbar.writeScrollPosition(desiredScrollPosition, desiredScrollTop); } if (deltaX) { - let currentScrollLeft = scrollState.scrollLeft; - desiredScrollLeft = this._horizontalScrollbar.validateScrollPosition((desiredScrollLeft !== -1 ? desiredScrollLeft : currentScrollLeft) - SCROLL_WHEEL_SENSITIVITY * deltaX); - if (desiredScrollLeft === currentScrollLeft) { - desiredScrollLeft = -1; - } + const desiredScrollLeft = futureScrollPosition.scrollLeft - SCROLL_WHEEL_SENSITIVITY * deltaX; + this._horizontalScrollbar.writeScrollPosition(desiredScrollPosition, desiredScrollLeft); } - if (desiredScrollTop !== -1 || desiredScrollLeft !== -1) { - if (desiredScrollTop !== -1) { - // If |∆y| is too small then do not apply smooth scroll animation, because in that case the input source must be a touchpad or something similar. - const applySmoothScroll = this._options.mouseWheelSmoothScroll && Math.abs(deltaY) > SCROLL_WHEEL_SMOOTH_SCROLL_THRESHOLD; - const shouldRender = this._verticalScrollbar.setDesiredScrollPosition(desiredScrollTop, applySmoothScroll ? this._options.mouseWheelSmoothScrollDuration : 0/* immediate */); - this._shouldRender = shouldRender || this._shouldRender; - desiredScrollTop = -1; - } - if (desiredScrollLeft !== -1) { - // If |∆x| is too small then do not apply smooth scroll animation, because in that case the input source must be a touchpad or something similar. - const applySmoothScroll = this._options.mouseWheelSmoothScroll && Math.abs(deltaX) > SCROLL_WHEEL_SMOOTH_SCROLL_THRESHOLD; - const shouldRender = this._horizontalScrollbar.setDesiredScrollPosition(desiredScrollLeft, applySmoothScroll ? this._options.mouseWheelSmoothScrollDuration : 0/* immediate */); - this._shouldRender = shouldRender || this._shouldRender; - desiredScrollLeft = -1; + // Check that we are scrolling towards a location which is valid + desiredScrollPosition = this._scrollable.validateScrollPosition(desiredScrollPosition); + + if (futureScrollPosition.scrollLeft !== desiredScrollPosition.scrollLeft || futureScrollPosition.scrollTop !== desiredScrollPosition.scrollTop) { + // TODO@smooth: [MUST] implement better heuristic for distinguishing inertia scrolling + // from physical mouse wheels + const ENABLE_MOUSE_WHEEL_SMOOTH = false; + + // If |∆x| and |∆y| are too small then do not apply smooth scroll animation, because in that case the input source must be a touchpad or something similar. + const smoothScrollThresholdReached = ( + Math.abs(deltaY) > SCROLL_WHEEL_SMOOTH_SCROLL_THRESHOLD + || Math.abs(deltaX) > SCROLL_WHEEL_SMOOTH_SCROLL_THRESHOLD + ); + + if (ENABLE_MOUSE_WHEEL_SMOOTH && this._options.mouseWheelSmoothScroll && smoothScrollThresholdReached) { + this._scrollable.setScrollPositionSmooth(desiredScrollPosition); + } else { + this._scrollable.setScrollPositionNow(desiredScrollPosition); } + + this._shouldRender = true; } } @@ -322,7 +317,7 @@ export class ScrollableElement extends Widget { this._verticalScrollbar.render(); if (this._options.useShadows) { - const scrollState = this._scrollable.getState(); + const scrollState = this._scrollable.getCurrentScrollPosition(); let enableTop = scrollState.scrollTop > 0; let enableLeft = scrollState.scrollLeft > 0; @@ -374,6 +369,33 @@ export class ScrollableElement extends Widget { } } +export class ScrollableElement extends AbstractScrollableElement { + + constructor(element: HTMLElement, options: ScrollableElementCreationOptions) { + options = options || {}; + options.mouseWheelSmoothScroll = false; + const scrollable = new Scrollable(0); + super(element, options, scrollable); + this._register(scrollable); + } + + public setScrollPosition(update: INewScrollPosition): void { + this._scrollable.setScrollPositionNow(update); + } + + public getScrollPosition(): IScrollPosition { + return this._scrollable.getCurrentScrollPosition(); + } +} + +export class SmoothScrollableElement extends AbstractScrollableElement { + + constructor(element: HTMLElement, options: ScrollableElementCreationOptions, scrollable: Scrollable) { + super(element, options, scrollable); + } + +} + export class DomScrollableElement extends ScrollableElement { private _element: HTMLElement; @@ -394,13 +416,14 @@ export class DomScrollableElement extends ScrollableElement { public scanDomNode(): void { // widh, scrollLeft, scrollWidth, height, scrollTop, scrollHeight - this.updateState({ + this.setScrollDimensions({ width: this._element.clientWidth, scrollWidth: this._element.scrollWidth, - scrollLeft: this._element.scrollLeft, - height: this._element.clientHeight, - scrollHeight: this._element.scrollHeight, + scrollHeight: this._element.scrollHeight + }); + this.setScrollPosition({ + scrollLeft: this._element.scrollLeft, scrollTop: this._element.scrollTop, }); } @@ -417,7 +440,6 @@ function resolveOptions(opts: ScrollableElementCreationOptions): ScrollableEleme scrollYToX: (typeof opts.scrollYToX !== 'undefined' ? opts.scrollYToX : false), mouseWheelScrollSensitivity: (typeof opts.mouseWheelScrollSensitivity !== 'undefined' ? opts.mouseWheelScrollSensitivity : 1), mouseWheelSmoothScroll: (typeof opts.mouseWheelSmoothScroll !== 'undefined' ? opts.mouseWheelSmoothScroll : true), - mouseWheelSmoothScrollDuration: (typeof opts.mouseWheelSmoothScrollDuration !== 'undefined' ? opts.mouseWheelSmoothScrollDuration : 100), arrowSize: (typeof opts.arrowSize !== 'undefined' ? opts.arrowSize : 11), listenOnDomNode: (typeof opts.listenOnDomNode !== 'undefined' ? opts.listenOnDomNode : null), diff --git a/src/vs/base/browser/ui/scrollbar/scrollableElementOptions.ts b/src/vs/base/browser/ui/scrollbar/scrollableElementOptions.ts index e28bbf90fed..069a6ea6860 100644 --- a/src/vs/base/browser/ui/scrollbar/scrollableElementOptions.ts +++ b/src/vs/base/browser/ui/scrollbar/scrollableElementOptions.ts @@ -31,11 +31,6 @@ export interface ScrollableElementCreationOptions { * Defaults to true. */ mouseWheelSmoothScroll?: boolean; - /** - * Duration in milliseconds for mouse wheel smooth scrolling animation. - * Defaults to 100. - */ - mouseWheelSmoothScrollDuration?: number; /** * Flip axes. Treat vertical scrolling like horizontal and vice-versa. * Defaults to false. @@ -125,7 +120,6 @@ export interface ScrollableElementResolvedOptions { alwaysConsumeMouseWheel: boolean; mouseWheelScrollSensitivity: number; mouseWheelSmoothScroll: boolean; - mouseWheelSmoothScrollDuration: number; arrowSize: number; listenOnDomNode: HTMLElement; horizontal: ScrollbarVisibility; diff --git a/src/vs/base/browser/ui/scrollbar/verticalScrollbar.ts b/src/vs/base/browser/ui/scrollbar/verticalScrollbar.ts index 53a76a0c0ff..98d74206690 100644 --- a/src/vs/base/browser/ui/scrollbar/verticalScrollbar.ts +++ b/src/vs/base/browser/ui/scrollbar/verticalScrollbar.ts @@ -8,7 +8,7 @@ import { AbstractScrollbar, ScrollbarHost, ISimplifiedMouseEvent } from 'vs/base import { StandardMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { IDomNodePagePosition } from 'vs/base/browser/dom'; import { ScrollableElementResolvedOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions'; -import { Scrollable, ScrollEvent, ScrollbarVisibility } from 'vs/base/common/scrollable'; +import { Scrollable, ScrollEvent, ScrollbarVisibility, INewScrollPosition } from 'vs/base/common/scrollable'; import { ScrollbarState } from 'vs/base/browser/ui/scrollbar/scrollbarState'; import { ARROW_IMG_SIZE } from 'vs/base/browser/ui/scrollbar/scrollbarArrow'; @@ -90,18 +90,7 @@ export class VerticalScrollbar extends AbstractScrollbar { return e.posx; } - protected _getScrollPosition(): number { - const scrollState = this._scrollable.getState(); - return scrollState.scrollTop; - } - - protected _setScrollPosition(scrollPosition: number, smoothScrollDuration: number): void { - this._scrollable.updateState({ - scrollTop: scrollPosition - }, smoothScrollDuration); - } - - public validateScrollPosition(desiredScrollPosition: number): number { - return this._scrollable.validateScrollTop(desiredScrollPosition); + public writeScrollPosition(target: INewScrollPosition, scrollPosition: number): void { + target.scrollTop = scrollPosition; } } diff --git a/src/vs/base/common/scrollable.ts b/src/vs/base/common/scrollable.ts index 2ce51291176..d304a0386b6 100644 --- a/src/vs/base/common/scrollable.ts +++ b/src/vs/base/common/scrollable.ts @@ -31,7 +31,7 @@ export interface ScrollEvent { scrollTopChanged: boolean; } -export class ScrollState { +export class ScrollState implements IScrollDimensions, IScrollPosition { _scrollStateBrand: void; public readonly width: number; @@ -95,13 +95,24 @@ export class ScrollState { ); } - public createUpdated(update: INewScrollState): ScrollState { + public withScrollDimensions(update: INewScrollDimensions): ScrollState { return new ScrollState( (typeof update.width !== 'undefined' ? update.width : this.width), (typeof update.scrollWidth !== 'undefined' ? update.scrollWidth : this.scrollWidth), - (typeof update.scrollLeft !== 'undefined' ? update.scrollLeft : this.scrollLeft), + this.scrollLeft, (typeof update.height !== 'undefined' ? update.height : this.height), (typeof update.scrollHeight !== 'undefined' ? update.scrollHeight : this.scrollHeight), + this.scrollTop + ); + } + + public withScrollPosition(update: INewScrollPosition): ScrollState { + return new ScrollState( + this.width, + this.scrollWidth, + (typeof update.scrollLeft !== 'undefined' ? update.scrollLeft : this.scrollLeft), + this.height, + this.scrollHeight, (typeof update.scrollTop !== 'undefined' ? update.scrollTop : this.scrollTop) ); } @@ -136,13 +147,25 @@ export class ScrollState { } -export interface INewScrollState { +export interface IScrollDimensions { + readonly width: number; + readonly scrollWidth: number; + readonly height: number; + readonly scrollHeight: number; +} +export interface INewScrollDimensions { width?: number; scrollWidth?: number; - scrollLeft?: number; - height?: number; scrollHeight?: number; +} + +export interface IScrollPosition { + readonly scrollLeft: number; + readonly scrollTop: number; +} +export interface INewScrollPosition { + scrollLeft?: number; scrollTop?: number; } @@ -150,126 +173,193 @@ export class Scrollable extends Disposable { _scrollableBrand: void; + private readonly _smoothScrollDuration: number; private _state: ScrollState; - private _smoothScrolling: boolean; - private _smoothScrollAnimationParams: ISmoothScrollAnimationParams; + private _smoothScrolling: SmoothScrollingOperation; private _onScroll = this._register(new Emitter()); public onScroll: Event = this._onScroll.event; - constructor() { + constructor(smoothScrollDuration: number) { super(); + this._smoothScrollDuration = smoothScrollDuration; this._state = new ScrollState(0, 0, 0, 0, 0, 0); - this._smoothScrolling = false; - this._smoothScrollAnimationParams = null; + this._smoothScrolling = null; + } + + public dispose(): void { + if (this._smoothScrolling) { + this._smoothScrolling.dispose(); + this._smoothScrolling = null; + } + super.dispose(); + } + + public validateScrollPosition(scrollPosition: INewScrollPosition): IScrollPosition { + return this._state.withScrollPosition(scrollPosition); } - public getState(): ScrollState { + public getScrollDimensions(): IScrollDimensions { return this._state; } - public validateScrollTop(desiredScrollTop: number): number { - desiredScrollTop = Math.round(desiredScrollTop); - desiredScrollTop = Math.max(desiredScrollTop, 0); - desiredScrollTop = Math.min(desiredScrollTop, this._state.scrollHeight - this._state.height); - return desiredScrollTop; + public setScrollDimensions(dimensions: INewScrollDimensions): void { + const newState = this._state.withScrollDimensions(dimensions); + this._setState(newState); + + // TODO@smooth: [MUST] validate outstanding animated scroll position request target + // (in case it becomes invalid) } - public validateScrollLeft(desiredScrollLeft: number): number { - desiredScrollLeft = Math.round(desiredScrollLeft); - desiredScrollLeft = Math.max(desiredScrollLeft, 0); - desiredScrollLeft = Math.min(desiredScrollLeft, this._state.scrollWidth - this._state.width); - return desiredScrollLeft; + /** + * Returns the final scroll position that the instance will have once the smooth scroll animation concludes. + * If no scroll animation is occuring, it will return the current scroll position instead. + */ + public getFutureScrollPosition(): IScrollPosition { + if (this._smoothScrolling) { + return this._smoothScrolling.to; + } + return this._state; } /** - * Returns the final scroll state that the instance will have once the smooth scroll animation concludes. - * If no scroll animation is occurring, it will return the actual scroll state instead. + * Returns the current scroll position. + * Note: This result might be an intermediate scroll position, as there might be an ongoing smooth scroll animation. */ - public getSmoothScrollTargetState(): ScrollState { - return this._smoothScrolling ? this._smoothScrollAnimationParams.newState : this._state; + public getCurrentScrollPosition(): IScrollPosition { + return this._state; } - public updateState(update: INewScrollState, smoothScrollDuration: number): void { + public setScrollPositionNow(update: INewScrollPosition): void { + // no smooth scrolling requested + const newState = this._state.withScrollPosition(update); - // If smooth scroll duration is not specified, then assume that the invoker intends to do an immediate update. - if (smoothScrollDuration === 0) { - const newState = this._state.createUpdated(update); + // Terminate any outstanding smooth scrolling + if (this._smoothScrolling) { + this._smoothScrolling.dispose(); + this._smoothScrolling = null; + } - // If smooth scrolling is in progress, terminate it. - if (this._smoothScrolling) { - this._smoothScrolling = false; - this._smoothScrollAnimationParams = null; - } + this._setState(newState); + } - // Update state immediately if it is different from the previous one. - if (!this._state.equals(newState)) { - this._updateState(newState); - } + public setScrollPositionSmooth(update: INewScrollPosition): void { + if (this._smoothScrollDuration === 0) { + // Smooth scrolling not supported. + return this.setScrollPositionNow(update); } - // Otherwise update scroll state incrementally. - else { - const targetState = this.getSmoothScrollTargetState(); - const newTargetState = targetState.createUpdated(update); - - // Proceed only if the new target state differs from the current one. - if (!targetState.equals(newTargetState)) { - // Initialize/update smooth scroll parameters. - this._smoothScrollAnimationParams = { - oldState: this._state, - newState: newTargetState, - startTime: Date.now(), - duration: smoothScrollDuration, - }; - - // Invoke smooth scrolling functionality in the next frame if it is not already in progress. - if (!this._smoothScrolling) { - this._smoothScrolling = true; - requestAnimationFrame(() => { this._performSmoothScroll(); }); - } + + if (this._smoothScrolling) { + const oldSmoothScrolling = this._smoothScrolling; + const newSmoothScrolling = oldSmoothScrolling.combine(this._state, update, this._smoothScrollDuration); + if (oldSmoothScrolling.softEquals(newSmoothScrolling)) { + // No change + return; } + oldSmoothScrolling.dispose(); + this._smoothScrolling = newSmoothScrolling; + } else { + this._smoothScrolling = new SmoothScrollingOperation(this._state, update, this._smoothScrollDuration); } + + // Begin smooth scrolling animation + this._smoothScrolling.animationFrameToken = requestAnimationFrame(() => { + this._smoothScrolling.animationFrameToken = -1; + this._performSmoothScrolling(); + }); } - private _performSmoothScroll(): void { - if (!this._smoothScrolling) { - // Smooth scrolling has been terminated. - return; - } + private _performSmoothScrolling(): void { + const update = this._smoothScrolling.tick(); + const newState = this._state.withScrollPosition(update); - const completion = (Date.now() - this._smoothScrollAnimationParams.startTime) / this._smoothScrollAnimationParams.duration; - const newState = this._smoothScrollAnimationParams.newState; + this._setState(newState); - if (completion < 1) { - const oldState = this._smoothScrollAnimationParams.oldState; - this._updateState(new ScrollState( - newState.width, - newState.scrollWidth, - oldState.scrollLeft + (newState.scrollLeft - oldState.scrollLeft) * completion, - newState.height, - newState.scrollHeight, - oldState.scrollTop + (newState.scrollTop - oldState.scrollTop) * completion - )); - requestAnimationFrame(() => { this._performSmoothScroll(); }); - } - else { - this._smoothScrolling = false; - this._smoothScrollAnimationParams = null; - this._updateState(newState); + if (!update.isDone) { + // Continue smooth scrolling animation + this._smoothScrolling.animationFrameToken = requestAnimationFrame(() => { + this._smoothScrolling.animationFrameToken = -1; + this._performSmoothScrolling(); + }); } } - private _updateState(newState: ScrollState): void { + private _setState(newState: ScrollState): void { const oldState = this._state; + if (oldState.equals(newState)) { + // no change + return; + } this._state = newState; this._onScroll.fire(this._state.createScrollEvent(oldState)); } } -interface ISmoothScrollAnimationParams { - oldState: ScrollState; - newState: ScrollState; - startTime: number; - duration: number; -} \ No newline at end of file +class SmoothScrollingUpdate implements IScrollPosition { + + public readonly scrollLeft: number; + public readonly scrollTop: number; + public readonly isDone: boolean; + + constructor(scrollLeft: number, scrollTop: number, isDone: boolean) { + this.scrollLeft = scrollLeft; + this.scrollTop = scrollTop; + this.isDone = isDone; + } + +} + +class SmoothScrollingOperation { + + public readonly from: IScrollPosition; + public readonly to: IScrollPosition; + public readonly duration: number; + private readonly _startTime: number; + public animationFrameToken: number; + + constructor(from: ScrollState, to: INewScrollPosition, duration: number) { + this.from = from; + this.to = from.withScrollPosition(to); + this.duration = duration; + this._startTime = Date.now(); + this.animationFrameToken = -1; + } + + public softEquals(other: SmoothScrollingOperation): boolean { + return ( + this.to.scrollLeft === other.to.scrollLeft + && this.to.scrollTop === other.to.scrollTop + ); + } + + public dispose(): void { + if (this.animationFrameToken !== -1) { + cancelAnimationFrame(this.animationFrameToken); + this.animationFrameToken = -1; + } + } + + public tick(): SmoothScrollingUpdate { + const completion = (Date.now() - this._startTime) / this.duration; + + if (completion < 1) { + const newScrollLeft = this.from.scrollLeft + (this.to.scrollLeft - this.from.scrollLeft) * completion; + const newScrollTop = this.from.scrollTop + (this.to.scrollTop - this.from.scrollTop) * completion; + return new SmoothScrollingUpdate(newScrollLeft, newScrollTop, false); + } + + return new SmoothScrollingUpdate(this.to.scrollLeft, this.to.scrollTop, true); + } + + public combine(from: ScrollState, to: INewScrollPosition, duration: number): SmoothScrollingOperation { + // Combine our scrollLeft/scrollTop with incoming scrollLeft/scrollTop + to = { + scrollLeft: (typeof to.scrollLeft === 'undefined' ? this.to.scrollLeft : to.scrollLeft), + scrollTop: (typeof to.scrollTop === 'undefined' ? this.to.scrollTop : to.scrollTop) + }; + + // TODO@smooth: This is our opportunity to combine animations + return new SmoothScrollingOperation(from, to, duration); + } +} diff --git a/src/vs/base/parts/tree/browser/treeView.ts b/src/vs/base/parts/tree/browser/treeView.ts index 38a82069f3e..ac5ba32ad5c 100644 --- a/src/vs/base/parts/tree/browser/treeView.ts +++ b/src/vs/base/parts/tree/browser/treeView.ts @@ -847,27 +847,29 @@ export class TreeView extends HeightMap { } public get viewHeight() { - const scrollState = this.scrollableElement.getScrollState(); - return scrollState.height; + const scrollDimensions = this.scrollableElement.getScrollDimensions(); + return scrollDimensions.height; } public set viewHeight(viewHeight: number) { - this.scrollableElement.updateState({ + this.scrollableElement.setScrollDimensions({ height: viewHeight, scrollHeight: this.getTotalHeight() }); } public get scrollTop(): number { - const scrollState = this.scrollableElement.getScrollState(); - return scrollState.scrollTop; + const scrollPosition = this.scrollableElement.getScrollPosition(); + return scrollPosition.scrollTop; } public set scrollTop(scrollTop: number) { - this.scrollableElement.updateState({ - scrollTop: scrollTop, + this.scrollableElement.setScrollDimensions({ scrollHeight: this.getTotalHeight() }); + this.scrollableElement.setScrollPosition({ + scrollTop: scrollTop + }); } public getScrollPosition(): number { diff --git a/src/vs/editor/browser/controller/mouseHandler.ts b/src/vs/editor/browser/controller/mouseHandler.ts index 60ff6e4d650..982043e7e5f 100644 --- a/src/vs/editor/browser/controller/mouseHandler.ts +++ b/src/vs/editor/browser/controller/mouseHandler.ts @@ -423,16 +423,16 @@ class MouseDownOperation extends Disposable { const mouseColumn = this._getMouseColumn(e); if (e.posy < editorContent.y) { - let aboveLineNumber = viewLayout.getLineNumberAtVerticalOffset(Math.max(viewLayout.getScrollTop() - (editorContent.y - e.posy), 0)); + let aboveLineNumber = viewLayout.getLineNumberAtVerticalOffset(Math.max(viewLayout.getCurrentScrollTop() - (editorContent.y - e.posy), 0)); return new MouseTarget(null, editorBrowser.MouseTargetType.OUTSIDE_EDITOR, mouseColumn, new Position(aboveLineNumber, 1)); } if (e.posy > editorContent.y + editorContent.height) { - let belowLineNumber = viewLayout.getLineNumberAtVerticalOffset(viewLayout.getScrollTop() + (e.posy - editorContent.y)); + let belowLineNumber = viewLayout.getLineNumberAtVerticalOffset(viewLayout.getCurrentScrollTop() + (e.posy - editorContent.y)); return new MouseTarget(null, editorBrowser.MouseTargetType.OUTSIDE_EDITOR, mouseColumn, new Position(belowLineNumber, model.getLineMaxColumn(belowLineNumber))); } - let possibleLineNumber = viewLayout.getLineNumberAtVerticalOffset(viewLayout.getScrollTop() + (e.posy - editorContent.y)); + let possibleLineNumber = viewLayout.getLineNumberAtVerticalOffset(viewLayout.getCurrentScrollTop() + (e.posy - editorContent.y)); if (e.posx < editorContent.x) { return new MouseTarget(null, editorBrowser.MouseTargetType.OUTSIDE_EDITOR, mouseColumn, new Position(possibleLineNumber, 1)); diff --git a/src/vs/editor/browser/controller/mouseTarget.ts b/src/vs/editor/browser/controller/mouseTarget.ts index 3626e6cfb70..4290a200487 100644 --- a/src/vs/editor/browser/controller/mouseTarget.ts +++ b/src/vs/editor/browser/controller/mouseTarget.ts @@ -327,12 +327,12 @@ class HitTestContext { return this._viewHelper.getPositionFromDOMInfo(spanNode, offset); } - public getScrollTop(): number { - return this._context.viewLayout.getScrollTop(); + public getCurrentScrollTop(): number { + return this._context.viewLayout.getCurrentScrollTop(); } - public getScrollLeft(): number { - return this._context.viewLayout.getScrollLeft(); + public getCurrentScrollLeft(): number { + return this._context.viewLayout.getCurrentScrollLeft(); } } @@ -351,8 +351,8 @@ abstract class BareHitTestRequest { this.editorPos = editorPos; this.pos = pos; - this.mouseVerticalOffset = Math.max(0, ctx.getScrollTop() + pos.y - editorPos.y); - this.mouseContentHorizontalOffset = ctx.getScrollLeft() + pos.x - editorPos.x - ctx.layoutInfo.contentLeft; + this.mouseVerticalOffset = Math.max(0, ctx.getCurrentScrollTop() + pos.y - editorPos.y); + this.mouseContentHorizontalOffset = ctx.getCurrentScrollLeft() + pos.x - editorPos.x - ctx.layoutInfo.contentLeft; this.isInMarginArea = (pos.x - editorPos.x < ctx.layoutInfo.contentLeft); this.isInContentArea = !this.isInMarginArea; this.mouseColumn = Math.max(0, MouseTargetFactory._getMouseColumn(this.mouseContentHorizontalOffset, ctx.typicalHalfwidthCharacterWidth)); @@ -649,7 +649,7 @@ export class MouseTargetFactory { public getMouseColumn(editorPos: EditorPagePosition, pos: PageCoordinates): number { let layoutInfo = this._context.configuration.editor.layoutInfo; - let mouseContentHorizontalOffset = this._context.viewLayout.getScrollLeft() + pos.x - editorPos.x - layoutInfo.contentLeft; + let mouseContentHorizontalOffset = this._context.viewLayout.getCurrentScrollLeft() + pos.x - editorPos.x - layoutInfo.contentLeft; return MouseTargetFactory._getMouseColumn(mouseContentHorizontalOffset, this._context.configuration.editor.fontInfo.typicalHalfwidthCharacterWidth); } diff --git a/src/vs/editor/browser/controller/pointerHandler.ts b/src/vs/editor/browser/controller/pointerHandler.ts index fdd809a2e1a..35b948c6634 100644 --- a/src/vs/editor/browser/controller/pointerHandler.ts +++ b/src/vs/editor/browser/controller/pointerHandler.ts @@ -99,11 +99,7 @@ class MsPointerHandler extends MouseHandler implements IDisposable { } private _onGestureChange(e: IThrottledGestureEvent): void { - const viewLayout = this._context.viewLayout; - viewLayout.setScrollPosition({ - scrollLeft: viewLayout.getScrollLeft() - e.translationX, - scrollTop: viewLayout.getScrollTop() - e.translationY, - }); + this._context.viewLayout.deltaScrollNow(-e.translationX, -e.translationY); } public dispose(): void { @@ -181,11 +177,7 @@ class StandardPointerHandler extends MouseHandler implements IDisposable { } private _onGestureChange(e: IThrottledGestureEvent): void { - const viewLayout = this._context.viewLayout; - viewLayout.setScrollPosition({ - scrollLeft: viewLayout.getScrollLeft() - e.translationX, - scrollTop: viewLayout.getScrollTop() - e.translationY, - }); + this._context.viewLayout.deltaScrollNow(-e.translationX, -e.translationY); } public dispose(): void { @@ -227,11 +219,7 @@ class TouchHandler extends MouseHandler { } private onChange(e: GestureEvent): void { - const viewLayout = this._context.viewLayout; - viewLayout.setScrollPosition({ - scrollLeft: viewLayout.getScrollLeft() - e.translationX, - scrollTop: viewLayout.getScrollTop() - e.translationY, - }); + this._context.viewLayout.deltaScrollNow(-e.translationX, -e.translationY); } } diff --git a/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts b/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts index 4f5f918abe7..552bb8f4d2a 100644 --- a/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts +++ b/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts @@ -6,7 +6,7 @@ import * as dom from 'vs/base/browser/dom'; import { ScrollableElementCreationOptions, ScrollableElementChangeOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions'; -import { IOverviewRulerLayoutInfo, ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; +import { IOverviewRulerLayoutInfo, SmoothScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { INewScrollPosition } from 'vs/editor/common/editorCommon'; import { ViewPart, PartFingerprint, PartFingerprints } from 'vs/editor/browser/view/viewPart'; import { ViewContext } from 'vs/editor/common/view/viewContext'; @@ -19,7 +19,7 @@ import { ISimplifiedMouseEvent } from 'vs/base/browser/ui/scrollbar/abstractScro export class EditorScrollbar extends ViewPart { - private scrollbar: ScrollableElement; + private scrollbar: SmoothScrollableElement; private scrollbarDomNode: FastDomNode; constructor( @@ -52,7 +52,7 @@ export class EditorScrollbar extends ViewPart { mouseWheelScrollSensitivity: configScrollbarOpts.mouseWheelScrollSensitivity, }; - this.scrollbar = this._register(new ScrollableElement(linesContent.domNode, scrollbarOptions, this._context.viewLayout.scrollable)); + this.scrollbar = this._register(new SmoothScrollableElement(linesContent.domNode, scrollbarOptions, this._context.viewLayout.scrollable)); PartFingerprints.write(this.scrollbar.getDomNode(), PartFingerprint.ScrollableElement); this.scrollbarDomNode = createFastDomNode(this.scrollbar.getDomNode()); @@ -69,7 +69,7 @@ export class EditorScrollbar extends ViewPart { if (lookAtScrollTop) { let deltaTop = domNode.scrollTop; if (deltaTop) { - newScrollPosition.scrollTop = this._context.viewLayout.getScrollTop() + deltaTop; + newScrollPosition.scrollTop = this._context.viewLayout.getCurrentScrollTop() + deltaTop; domNode.scrollTop = 0; } } @@ -77,12 +77,12 @@ export class EditorScrollbar extends ViewPart { if (lookAtScrollLeft) { let deltaLeft = domNode.scrollLeft; if (deltaLeft) { - newScrollPosition.scrollLeft = this._context.viewLayout.getScrollLeft() + deltaLeft; + newScrollPosition.scrollLeft = this._context.viewLayout.getCurrentScrollLeft() + deltaLeft; domNode.scrollLeft = 0; } } - this._context.viewLayout.setScrollPosition(newScrollPosition); + this._context.viewLayout.setScrollPositionNow(newScrollPosition); }; // I've seen this happen both on the view dom node & on the lines content dom node. diff --git a/src/vs/editor/browser/viewParts/lines/viewLines.ts b/src/vs/editor/browser/viewParts/lines/viewLines.ts index 60a1d51c3ed..3ad8d11179e 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLines.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLines.ts @@ -205,7 +205,7 @@ export class ViewLines extends ViewPart implements IVisibleLinesHost, this._lastCursorRevealRangeHorizontallyEvent = e; } - this._context.viewLayout.setScrollPosition({ // TODO@Alex: scrolling vertically can be moved to the view model + this._context.viewLayout.setScrollPositionSmooth({ // TODO@Alex: scrolling vertically can be moved to the view model scrollTop: newScrollTop }); @@ -441,6 +441,8 @@ export class ViewLines extends ViewPart implements IVisibleLinesHost, // - this must happen after the lines are in the DOM since it might need a line that rendered just now // - it might change `scrollWidth` and `scrollLeft` if (this._lastCursorRevealRangeHorizontallyEvent) { + // TODO@smooth: [MUST] the line might not be visible due to our smooth scrolling to it!!! + let revealHorizontalRange = this._lastCursorRevealRangeHorizontallyEvent.range; this._lastCursorRevealRangeHorizontallyEvent = null; @@ -457,16 +459,16 @@ export class ViewLines extends ViewPart implements IVisibleLinesHost, } // set `scrollLeft` - this._context.viewLayout.setScrollPosition({ + this._context.viewLayout.setScrollPositionSmooth({ scrollLeft: newScrollLeft.scrollLeft }); } // (3) handle scrolling this._linesContent.setLayerHinting(this._canUseLayerHinting); - const adjustedScrollTop = this._context.viewLayout.getScrollTop() - viewportData.bigNumbersDelta; + const adjustedScrollTop = this._context.viewLayout.getCurrentScrollTop() - viewportData.bigNumbersDelta; this._linesContent.setTop(-adjustedScrollTop); - this._linesContent.setLeft(-this._context.viewLayout.getScrollLeft()); + this._linesContent.setLeft(-this._context.viewLayout.getCurrentScrollLeft()); // Update max line width (not so important, it is just so the horizontal scrollbar doesn't get too small) this._asyncUpdateLineWidths.schedule(); diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.ts b/src/vs/editor/browser/viewParts/minimap/minimap.ts index 206351eb6da..0a0c8f77b81 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimap.ts @@ -528,14 +528,14 @@ export class Minimap extends ViewPart { if (platform.isWindows && mouseOrthogonalDelta > MOUSE_DRAG_RESET_DISTANCE) { // The mouse has wondered away from the scrollbar => reset dragging - this._context.viewLayout.setScrollPosition({ + this._context.viewLayout.setScrollPositionNow({ scrollTop: initialSliderState.scrollTop }); return; } const mouseDelta = mouseMoveData.posy - initialMousePosition; - this._context.viewLayout.setScrollPosition({ + this._context.viewLayout.setScrollPositionNow({ scrollTop: initialSliderState.getDesiredScrollTopFromDelta(mouseDelta) }); }, diff --git a/src/vs/editor/common/commonCodeEditor.ts b/src/vs/editor/common/commonCodeEditor.ts index c636c9f8f5f..1fbfb9f968d 100644 --- a/src/vs/editor/common/commonCodeEditor.ts +++ b/src/vs/editor/common/commonCodeEditor.ts @@ -513,7 +513,7 @@ export abstract class CommonCodeEditor extends Disposable implements editorCommo if (!this.hasView) { return -1; } - return this.viewModel.viewLayout.getScrollLeft(); + return this.viewModel.viewLayout.getCurrentScrollLeft(); } public getScrollHeight(): number { @@ -526,7 +526,7 @@ export abstract class CommonCodeEditor extends Disposable implements editorCommo if (!this.hasView) { return -1; } - return this.viewModel.viewLayout.getScrollTop(); + return this.viewModel.viewLayout.getCurrentScrollTop(); } public setScrollLeft(newScrollLeft: number): void { @@ -536,7 +536,7 @@ export abstract class CommonCodeEditor extends Disposable implements editorCommo if (typeof newScrollLeft !== 'number') { throw new Error('Invalid arguments'); } - this.viewModel.viewLayout.setScrollPosition({ + this.viewModel.viewLayout.setScrollPositionNow({ scrollLeft: newScrollLeft }); } @@ -547,7 +547,7 @@ export abstract class CommonCodeEditor extends Disposable implements editorCommo if (typeof newScrollTop !== 'number') { throw new Error('Invalid arguments'); } - this.viewModel.viewLayout.setScrollPosition({ + this.viewModel.viewLayout.setScrollPositionNow({ scrollTop: newScrollTop }); } @@ -555,7 +555,7 @@ export abstract class CommonCodeEditor extends Disposable implements editorCommo if (!this.hasView) { return; } - this.viewModel.viewLayout.setScrollPosition(position); + this.viewModel.viewLayout.setScrollPositionNow(position); } public saveViewState(): editorCommon.ICodeEditorViewState { diff --git a/src/vs/editor/common/controller/coreCommands.ts b/src/vs/editor/common/controller/coreCommands.ts index f3eaebb76c4..9022c8747e4 100644 --- a/src/vs/editor/common/controller/coreCommands.ts +++ b/src/vs/editor/common/controller/coreCommands.ts @@ -1114,7 +1114,7 @@ export namespace CoreNavigationCommands { noOfLines = args.value; } const deltaLines = (args.direction === EditorScroll_.Direction.Up ? -1 : 1) * noOfLines; - return context.getScrollTop() + deltaLines * context.config.lineHeight; + return context.getCurrentScrollTop() + deltaLines * context.config.lineHeight; } } diff --git a/src/vs/editor/common/controller/cursor.ts b/src/vs/editor/common/controller/cursor.ts index 2aa2755fe68..6cdcd4070ce 100644 --- a/src/vs/editor/common/controller/cursor.ts +++ b/src/vs/editor/common/controller/cursor.ts @@ -210,7 +210,7 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { } public scrollTo(desiredScrollTop: number): void { - this._viewModel.viewLayout.setScrollPosition({ + this._viewModel.viewLayout.setScrollPositionSmooth({ scrollTop: desiredScrollTop }); } diff --git a/src/vs/editor/common/controller/cursorCommon.ts b/src/vs/editor/common/controller/cursorCommon.ts index eb7b3771964..9196b6e8830 100644 --- a/src/vs/editor/common/controller/cursorCommon.ts +++ b/src/vs/editor/common/controller/cursorCommon.ts @@ -304,8 +304,8 @@ export class CursorContext { return this.viewModel.coordinatesConverter.convertModelRangeToViewRange(modelRange); } - public getScrollTop(): number { - return this.viewModel.viewLayout.getScrollTop(); + public getCurrentScrollTop(): number { + return this.viewModel.viewLayout.getCurrentScrollTop(); } public getCompletelyVisibleViewRange(): Range { diff --git a/src/vs/editor/common/viewLayout/viewLayout.ts b/src/vs/editor/common/viewLayout/viewLayout.ts index 3be7360c4dd..cde2f0bf46e 100644 --- a/src/vs/editor/common/viewLayout/viewLayout.ts +++ b/src/vs/editor/common/viewLayout/viewLayout.ts @@ -5,7 +5,7 @@ 'use strict'; import { Disposable } from 'vs/base/common/lifecycle'; -import { Scrollable, ScrollState, ScrollEvent, ScrollbarVisibility } from 'vs/base/common/scrollable'; +import { Scrollable, ScrollEvent, ScrollbarVisibility, IScrollDimensions } from 'vs/base/common/scrollable'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { LinesLayout } from 'vs/editor/common/viewLayout/linesLayout'; import { IViewLayout, IViewWhitespaceViewportData, Viewport } from 'vs/editor/common/viewModel/viewModel'; @@ -30,11 +30,12 @@ export class ViewLayout extends Disposable implements IViewLayout { this._configuration = configuration; this._linesLayout = new LinesLayout(lineCount, this._configuration.editor.lineHeight); - this.scrollable = this._register(new Scrollable()); - this.scrollable.updateState({ + // TODO@smooth: [MUST] have an editor option for smooth scrolling + this.scrollable = this._register(new Scrollable(125)); + this.scrollable.setScrollDimensions({ width: configuration.editor.layoutInfo.contentWidth, height: configuration.editor.layoutInfo.contentHeight - }, 0/* immediate */); + }); this.onDidScroll = this.scrollable.onScroll; this._updateHeight(); @@ -59,10 +60,10 @@ export class ViewLayout extends Disposable implements IViewLayout { this._linesLayout.setLineHeight(this._configuration.editor.lineHeight); } if (e.layoutInfo) { - this.scrollable.updateState({ + this.scrollable.setScrollDimensions({ width: this._configuration.editor.layoutInfo.contentWidth, height: this._configuration.editor.layoutInfo.contentHeight - }, 0/* immediate */); + }); } this._updateHeight(); } @@ -81,12 +82,12 @@ export class ViewLayout extends Disposable implements IViewLayout { // ---- end view event handlers - private _getHorizontalScrollbarHeight(scrollState: ScrollState): number { + private _getHorizontalScrollbarHeight(scrollDimensions: IScrollDimensions): number { if (this._configuration.editor.viewInfo.scrollbar.horizontal === ScrollbarVisibility.Hidden) { // horizontal scrollbar not visible return 0; } - if (scrollState.width >= scrollState.scrollWidth) { + if (scrollDimensions.width >= scrollDimensions.scrollWidth) { // horizontal scrollbar not visible return 0; } @@ -94,33 +95,34 @@ export class ViewLayout extends Disposable implements IViewLayout { } private _getTotalHeight(): number { - const scrollState = this.scrollable.getState(); + const scrollDimensions = this.scrollable.getScrollDimensions(); let result = this._linesLayout.getLinesTotalHeight(); if (this._configuration.editor.viewInfo.scrollBeyondLastLine) { - result += scrollState.height - this._configuration.editor.lineHeight; + result += scrollDimensions.height - this._configuration.editor.lineHeight; } else { - result += this._getHorizontalScrollbarHeight(scrollState); + result += this._getHorizontalScrollbarHeight(scrollDimensions); } - return Math.max(scrollState.height, result); + return Math.max(scrollDimensions.height, result); } private _updateHeight(): void { - this.scrollable.updateState({ + this.scrollable.setScrollDimensions({ scrollHeight: this._getTotalHeight() - }, 0/* immediate */); + }); } // ---- Layouting logic public getCurrentViewport(): Viewport { - const scrollState = this.scrollable.getState(); + const scrollDimensions = this.scrollable.getScrollDimensions(); + const currentScrollPosition = this.scrollable.getCurrentScrollPosition(); return new Viewport( - scrollState.scrollTop, - scrollState.scrollLeft, - scrollState.width, - scrollState.height + currentScrollPosition.scrollTop, + currentScrollPosition.scrollLeft, + scrollDimensions.width, + scrollDimensions.height ); } @@ -134,9 +136,9 @@ export class ViewLayout extends Disposable implements IViewLayout { public onMaxLineWidthChanged(maxLineWidth: number): void { let newScrollWidth = this._computeScrollWidth(maxLineWidth, this.getCurrentViewport().width); - this.scrollable.updateState({ + this.scrollable.setScrollDimensions({ scrollWidth: newScrollWidth - }, 0/* immediate */); + }); // The height might depend on the fact that there is a horizontal scrollbar or not this._updateHeight(); @@ -145,14 +147,14 @@ export class ViewLayout extends Disposable implements IViewLayout { // ---- view state public saveState(): editorCommon.IViewState { - const scrollState = this.scrollable.getState(); - let scrollTop = scrollState.scrollTop; + const currentScrollPosition = this.scrollable.getFutureScrollPosition(); + let scrollTop = currentScrollPosition.scrollTop; let firstLineNumberInViewport = this._linesLayout.getLineNumberAtOrAfterVerticalOffset(scrollTop); let whitespaceAboveFirstLine = this._linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(firstLineNumberInViewport); return { scrollTop: scrollTop, scrollTopWithoutViewZones: scrollTop - whitespaceAboveFirstLine, - scrollLeft: scrollState.scrollLeft + scrollLeft: currentScrollPosition.scrollLeft }; } @@ -161,10 +163,10 @@ export class ViewLayout extends Disposable implements IViewLayout { if (typeof state.scrollTopWithoutViewZones === 'number' && !this._linesLayout.hasWhitespace()) { restoreScrollTop = state.scrollTopWithoutViewZones; } - this.scrollable.updateState({ + this.scrollable.setScrollPositionNow({ scrollLeft: state.scrollLeft, scrollTop: restoreScrollTop - }, 0/* immediate */); + }); } // ---- IVerticalLayoutProvider @@ -197,14 +199,14 @@ export class ViewLayout extends Disposable implements IViewLayout { } public getLinesViewportDataAtScrollTop(scrollTop: number): IPartialViewLinesViewportData { // do some minimal validations on scrollTop - const scrollState = this.scrollable.getState(); - if (scrollTop + scrollState.height > scrollState.scrollHeight) { - scrollTop = scrollState.scrollHeight - scrollState.height; + const scrollDimensions = this.scrollable.getScrollDimensions(); + if (scrollTop + scrollDimensions.height > scrollDimensions.scrollHeight) { + scrollTop = scrollDimensions.scrollHeight - scrollDimensions.height; } if (scrollTop < 0) { scrollTop = 0; } - return this._linesLayout.getLinesViewportData(scrollTop, scrollTop + scrollState.height); + return this._linesLayout.getLinesViewportData(scrollTop, scrollTop + scrollDimensions.height); } public getWhitespaceViewportData(): IViewWhitespaceViewportData[] { const visibleBox = this.getCurrentViewport(); @@ -218,37 +220,45 @@ export class ViewLayout extends Disposable implements IViewLayout { public getScrollWidth(): number { - const scrollState = this.scrollable.getState(); - return scrollState.scrollWidth; - } - public getScrollLeft(): number { - const scrollState = this.scrollable.getState(); - return scrollState.scrollLeft; + const scrollDimensions = this.scrollable.getScrollDimensions(); + return scrollDimensions.scrollWidth; } public getScrollHeight(): number { - const scrollState = this.scrollable.getState(); - return scrollState.scrollHeight; - } - public getScrollTop(): number { - const scrollState = this.scrollable.getState(); - return scrollState.scrollTop; - } - - public setScrollPosition(position: editorCommon.INewScrollPosition): void { - const state = this.scrollable.getSmoothScrollTargetState(); - const xAbsChange = Math.abs(typeof position.scrollLeft !== 'undefined' ? position.scrollLeft - state.scrollLeft : 0); - const yAbsChange = Math.abs(typeof position.scrollTop !== 'undefined' ? position.scrollTop - state.scrollTop : 0); - - if (xAbsChange || yAbsChange) { - // If position change is big enough, then appply smooth scrolling - if (xAbsChange > state.width / 10 || - yAbsChange > state.height / 10) { - this.scrollable.updateState(position, 125); - } - // Otherwise update scroll position immediately - else { - this.scrollable.updateState(position, 0/* immediate */); - } - } + const scrollDimensions = this.scrollable.getScrollDimensions(); + return scrollDimensions.scrollHeight; + } + + public getCurrentScrollLeft(): number { + const currentScrollPosition = this.scrollable.getCurrentScrollPosition(); + return currentScrollPosition.scrollLeft; + } + public getCurrentScrollTop(): number { + const currentScrollPosition = this.scrollable.getCurrentScrollPosition(); + return currentScrollPosition.scrollTop; + } + + public getFutureScrollLeft(): number { + const currentScrollPosition = this.scrollable.getFutureScrollPosition(); + return currentScrollPosition.scrollLeft; + } + public getFutureScrollTop(): number { + const currentScrollPosition = this.scrollable.getFutureScrollPosition(); + return currentScrollPosition.scrollTop; + } + + public setScrollPositionNow(position: editorCommon.INewScrollPosition): void { + this.scrollable.setScrollPositionNow(position); + } + + public setScrollPositionSmooth(position: editorCommon.INewScrollPosition): void { + this.scrollable.setScrollPositionSmooth(position); + } + + public deltaScrollNow(deltaScrollLeft: number, deltaScrollTop: number): void { + const currentScrollPosition = this.scrollable.getCurrentScrollPosition(); + this.scrollable.setScrollPositionNow({ + scrollLeft: currentScrollPosition.scrollLeft + deltaScrollLeft, + scrollTop: currentScrollPosition.scrollTop + deltaScrollTop + }); } } diff --git a/src/vs/editor/common/viewModel/viewModel.ts b/src/vs/editor/common/viewModel/viewModel.ts index d7abff3dfd3..cde6379e0bd 100644 --- a/src/vs/editor/common/viewModel/viewModel.ts +++ b/src/vs/editor/common/viewModel/viewModel.ts @@ -44,12 +44,20 @@ export interface IViewLayout { onMaxLineWidthChanged(width: number): void; - getScrollLeft(): number; getScrollWidth(): number; getScrollHeight(): number; - getScrollTop(): number; + + getCurrentScrollLeft(): number; + getCurrentScrollTop(): number; + + getFutureScrollLeft(): number; + getFutureScrollTop(): number; + getCurrentViewport(): Viewport; - setScrollPosition(position: INewScrollPosition): void; + + setScrollPositionNow(position: INewScrollPosition): void; + setScrollPositionSmooth(position: INewScrollPosition): void; + deltaScrollNow(deltaScrollLeft: number, deltaScrollTop: number): void; getLinesViewportData(): IPartialViewLinesViewportData; getLinesViewportDataAtScrollTop(scrollTop: number): IPartialViewLinesViewportData; diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index 12b09a4a62d..1b6adb485de 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -125,7 +125,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel this.decorations.onLineMappingChanged(); this.viewLayout.onFlushed(this.getLineCount()); - if (this.viewLayout.getScrollTop() !== 0) { + if (this.viewLayout.getCurrentScrollTop() !== 0) { // Never change the scroll position from 0 to something else... revealPreviousCenteredModelRange = true; } diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index c0eb5a14a1b..bb00ee4d2b9 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -127,7 +127,7 @@ export class TabsTitleControl extends TitleControl { // Forward scrolling inside the container to our custom scrollbar this.toUnbind.push(DOM.addDisposableListener(this.tabsContainer, DOM.EventType.SCROLL, e => { if (DOM.hasClass(this.tabsContainer, 'scroll')) { - this.scrollbar.updateState({ + this.scrollbar.setScrollPosition({ scrollLeft: this.tabsContainer.scrollLeft // during DND the container gets scrolled so we need to update the custom scrollbar }); } @@ -458,7 +458,7 @@ export class TabsTitleControl extends TitleControl { const totalContainerWidth = this.tabsContainer.scrollWidth; // Update scrollbar - this.scrollbar.updateState({ + this.scrollbar.setScrollDimensions({ width: visibleContainerWidth, scrollWidth: totalContainerWidth }); @@ -478,14 +478,14 @@ export class TabsTitleControl extends TitleControl { // Tab is overflowing to the right: Scroll minimally until the element is fully visible to the right // Note: only try to do this if we actually have enough width to give to show the tab fully! if (activeTabFits && containerScrollPosX + visibleContainerWidth < activeTabPosX + activeTabWidth) { - this.scrollbar.updateState({ + this.scrollbar.setScrollPosition({ scrollLeft: containerScrollPosX + ((activeTabPosX + activeTabWidth) /* right corner of tab */ - (containerScrollPosX + visibleContainerWidth) /* right corner of view port */) }); } // Tab is overlflowng to the left or does not fit: Scroll it into view to the left else if (containerScrollPosX > activeTabPosX || !activeTabFits) { - this.scrollbar.updateState({ + this.scrollbar.setScrollPosition({ scrollLeft: this.activeTab.offsetLeft }); } @@ -565,7 +565,7 @@ export class TabsTitleControl extends TitleControl { } // moving in the tabs container can have an impact on scrolling position, so we need to update the custom scrollbar - this.scrollbar.updateState({ + this.scrollbar.setScrollPosition({ scrollLeft: this.tabsContainer.scrollLeft }); })); diff --git a/src/vs/workbench/parts/extensions/browser/extensionEditor.ts b/src/vs/workbench/parts/extensions/browser/extensionEditor.ts index c947f78d20d..fb9961d043e 100644 --- a/src/vs/workbench/parts/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/parts/extensions/browser/extensionEditor.ts @@ -445,8 +445,8 @@ export class ExtensionEditor extends BaseEditor { const tree = this.renderDependencies(content, extensionDependencies); const layout = () => { scrollableContent.scanDomNode(); - const scrollState = scrollableContent.getScrollState(); - tree.layout(scrollState.height); + const scrollDimensions = scrollableContent.getScrollDimensions(); + tree.layout(scrollDimensions.height); }; const removeLayoutParticipant = arrays.insert(this.layoutParticipants, { layout }); this.contentDisposables.push(toDisposable(removeLayoutParticipant)); diff --git a/src/vs/workbench/parts/welcome/walkThrough/electron-browser/walkThroughPart.ts b/src/vs/workbench/parts/welcome/walkThrough/electron-browser/walkThroughPart.ts index 763338760e9..c8c2a0ebe74 100644 --- a/src/vs/workbench/parts/welcome/walkThrough/electron-browser/walkThroughPart.ts +++ b/src/vs/workbench/parts/welcome/walkThrough/electron-browser/walkThroughPart.ts @@ -134,11 +134,12 @@ export class WalkThroughPart extends BaseEditor { } private updatedScrollPosition() { - const scrollState = this.scrollbar.getScrollState(); - const scrollHeight = scrollState.scrollHeight; + const scrollDimensions = this.scrollbar.getScrollDimensions(); + const scrollPosition = this.scrollbar.getScrollPosition(); + const scrollHeight = scrollDimensions.scrollHeight; if (scrollHeight && this.input instanceof WalkThroughInput) { - const scrollTop = scrollState.scrollTop; - const height = scrollState.height; + const scrollTop = scrollPosition.scrollTop; + const height = scrollDimensions.height; this.input.relativeScrollPosition(scrollTop / scrollHeight, (scrollTop + height) / scrollHeight); } } @@ -163,9 +164,9 @@ export class WalkThroughPart extends BaseEditor { this.disposables.push(this.addEventListener(this.content, 'focusin', e => { // Work around scrolling as side-effect of setting focus on the offscreen zone widget (#18929) if (e.target instanceof HTMLElement && e.target.classList.contains('zone-widget-container')) { - let scrollState = this.scrollbar.getScrollState(); - this.content.scrollTop = scrollState.scrollTop; - this.content.scrollLeft = scrollState.scrollLeft; + const scrollPosition = this.scrollbar.getScrollPosition(); + this.content.scrollTop = scrollPosition.scrollTop; + this.content.scrollLeft = scrollPosition.scrollLeft; } })); } @@ -186,7 +187,7 @@ export class WalkThroughPart extends BaseEditor { if (scrollTarget && innerContent) { const targetTop = scrollTarget.getBoundingClientRect().top - 20; const containerTop = innerContent.getBoundingClientRect().top; - this.scrollbar.updateState({ scrollTop: targetTop - containerTop }); + this.scrollbar.setScrollPosition({ scrollTop: targetTop - containerTop }); } } else { this.open(URI.parse(node.href)); @@ -261,13 +262,13 @@ export class WalkThroughPart extends BaseEditor { } arrowUp() { - const scrollState = this.scrollbar.getScrollState(); - this.scrollbar.updateState({ scrollTop: scrollState.scrollTop - this.getArrowScrollHeight() }); + const scrollPosition = this.scrollbar.getScrollPosition(); + this.scrollbar.setScrollPosition({ scrollTop: scrollPosition.scrollTop - this.getArrowScrollHeight() }); } arrowDown() { - const scrollState = this.scrollbar.getScrollState(); - this.scrollbar.updateState({ scrollTop: scrollState.scrollTop + this.getArrowScrollHeight() }); + const scrollPosition = this.scrollbar.getScrollPosition(); + this.scrollbar.setScrollPosition({ scrollTop: scrollPosition.scrollTop + this.getArrowScrollHeight() }); } private getArrowScrollHeight() { @@ -279,13 +280,15 @@ export class WalkThroughPart extends BaseEditor { } pageUp() { - const scrollState = this.scrollbar.getScrollState(); - this.scrollbar.updateState({ scrollTop: scrollState.scrollTop - scrollState.height }); + const scrollDimensions = this.scrollbar.getScrollDimensions(); + const scrollPosition = this.scrollbar.getScrollPosition(); + this.scrollbar.setScrollPosition({ scrollTop: scrollPosition.scrollTop - scrollDimensions.height }); } pageDown() { - const scrollState = this.scrollbar.getScrollState(); - this.scrollbar.updateState({ scrollTop: scrollState.scrollTop + scrollState.height }); + const scrollDimensions = this.scrollbar.getScrollDimensions(); + const scrollPosition = this.scrollbar.getScrollPosition(); + this.scrollbar.setScrollPosition({ scrollTop: scrollPosition.scrollTop + scrollDimensions.height }); } setInput(input: WalkThroughInput, options: EditorOptions): TPromise { @@ -367,13 +370,14 @@ export class WalkThroughPart extends BaseEditor { const lineHeight = editor.getConfiguration().lineHeight; const lineTop = (targetTop + (e.position.lineNumber - 1) * lineHeight) - containerTop; const lineBottom = lineTop + lineHeight; - const scrollState = this.scrollbar.getScrollState(); - const scrollTop = scrollState.scrollTop; - const height = scrollState.height; + const scrollDimensions = this.scrollbar.getScrollDimensions(); + const scrollPosition = this.scrollbar.getScrollPosition(); + const scrollTop = scrollPosition.scrollTop; + const height = scrollDimensions.height; if (scrollTop > lineTop) { - this.scrollbar.updateState({ scrollTop: lineTop }); + this.scrollbar.setScrollPosition({ scrollTop: lineTop }); } else if (scrollTop < lineBottom - height) { - this.scrollbar.updateState({ scrollTop: lineBottom - height }); + this.scrollbar.setScrollPosition({ scrollTop: lineBottom - height }); } } })); @@ -485,11 +489,11 @@ export class WalkThroughPart extends BaseEditor { memento[WALK_THROUGH_EDITOR_VIEW_STATE_PREFERENCE_KEY] = editorViewStateMemento; } - const scrollState = this.scrollbar.getScrollState(); + const scrollPosition = this.scrollbar.getScrollPosition(); const editorViewState: IWalkThroughEditorViewState = { viewState: { - scrollTop: scrollState.scrollTop, - scrollLeft: scrollState.scrollLeft + scrollTop: scrollPosition.scrollTop, + scrollLeft: scrollPosition.scrollLeft } }; @@ -512,7 +516,7 @@ export class WalkThroughPart extends BaseEditor { if (fileViewState) { const state: IWalkThroughEditorViewState = fileViewState[this.position]; if (state) { - this.scrollbar.updateState(state.viewState); + this.scrollbar.setScrollPosition(state.viewState); } } } -- GitLab