diff --git a/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts b/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts index 04d140075f3917a88088ff8e1535ed4efdad5a0b..4cb59ead4de7c8747e43ae0db886aaf39b50220f 100644 --- a/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts +++ b/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts @@ -377,6 +377,7 @@ export interface IMouseMoveEventData { export abstract class AbstractScrollbar extends Widget implements IScrollbar { protected _forbidTranslate3dUse: boolean; + private _lazyRender: boolean; private _parent: IParent; private _scrollbarState: ScrollbarState; private _visibilityController: VisibilityController; @@ -385,13 +386,17 @@ export abstract class AbstractScrollbar extends Widget implements IScrollbar { public domNode: FastDomNode; public slider: FastDomNode; - constructor(forbidTranslate3dUse: boolean, parent: IParent, scrollbarState: ScrollbarState, visibility: Visibility, extraScrollbarClassName: string) { + protected _shouldRender: boolean; + + constructor(forbidTranslate3dUse: boolean, lazyRender:boolean, parent: IParent, scrollbarState: ScrollbarState, visibility: Visibility, extraScrollbarClassName: string) { super(); this._forbidTranslate3dUse = forbidTranslate3dUse; + this._lazyRender = lazyRender; this._parent = parent; this._scrollbarState = scrollbarState; this._visibilityController = this._register(new VisibilityController(visibility, 'visible scrollbar ' + extraScrollbarClassName, 'invisible scrollbar ' + extraScrollbarClassName)); this._mouseMoveMonitor = this._register(new GlobalMouseMoveMonitor()); + this._shouldRender = true; } // ----------------- initialize & clean-up @@ -440,26 +445,37 @@ export abstract class AbstractScrollbar extends Widget implements IScrollbar { // ----------------- Update state - public onElementSize(visibleSize: number) { + public onElementSize(visibleSize: number): boolean { if (this._scrollbarState.setVisibleSize(visibleSize)) { - this._renderDomNode(this._scrollbarState.getRectangleLargeSize(), this._scrollbarState.getRectangleSmallSize()); - this._renderSlider(); this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded()); + this._shouldRender = true; + if (!this._lazyRender) { + this.render(); + } } + return this._shouldRender; } - public onElementScrollSize(elementScrollSize: number): void { + public onElementScrollSize(elementScrollSize: number): boolean { if (this._scrollbarState.setScrollSize(elementScrollSize)) { - this._renderSlider(); this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded()); + this._shouldRender = true; + if (!this._lazyRender) { + this.render(); + } } + return this._shouldRender; } - public onElementScrollPosition(elementScrollPosition: number): void { + public onElementScrollPosition(elementScrollPosition: number): boolean { if (this._scrollbarState.setScrollPosition(elementScrollPosition)) { - this._renderSlider(); this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded()); + this._shouldRender = true; + if (!this._lazyRender) { + this.render(); + } } + return this._shouldRender; } // ----------------- rendering @@ -472,10 +488,15 @@ export abstract class AbstractScrollbar extends Widget implements IScrollbar { this._visibilityController.setShouldBeVisible(false); } - private _renderSlider(): void { + public render(): void { + if (!this._shouldRender) { + return; + } + this._shouldRender = false; + + this._renderDomNode(this._scrollbarState.getRectangleLargeSize(), this._scrollbarState.getRectangleSmallSize()); this._updateSlider(this._scrollbarState.getSliderSize(), this._scrollbarState.getArrowSize() + this._scrollbarState.getSliderPosition()); } - // ----------------- DOM events private _domNodeMouseDown(e: IMouseEvent): void { @@ -543,12 +564,18 @@ export abstract class AbstractScrollbar extends Widget implements IScrollbar { return this._scrollbarState.validateScrollPosition(desiredScrollPosition); } - public setDesiredScrollPosition(desiredScrollPosition: number): void { + public setDesiredScrollPosition(desiredScrollPosition: number): boolean { desiredScrollPosition = this.validateScrollPosition(desiredScrollPosition); + let oldScrollPosition = this._getScrollPosition(); this._setScrollPosition(desiredScrollPosition); - this.onElementScrollPosition(desiredScrollPosition); - this._renderSlider(); + let newScrollPosition = this._getScrollPosition(); + + if (oldScrollPosition !== newScrollPosition) { + this.onElementScrollPosition(this._getScrollPosition()); + return true; + } + return false; } // ----------------- Overwrite these diff --git a/src/vs/base/browser/ui/scrollbar/horizontalScrollbar.ts b/src/vs/base/browser/ui/scrollbar/horizontalScrollbar.ts index 925f490d25af905fd0b595ad3bd6ca18e88c9c3d..ae9a0c891e6bd641b94dbe2b2023d0273f79e06b 100644 --- a/src/vs/base/browser/ui/scrollbar/horizontalScrollbar.ts +++ b/src/vs/base/browser/ui/scrollbar/horizontalScrollbar.ts @@ -21,7 +21,7 @@ export class HorizontalScrollbar extends AbstractScrollbar { (options.horizontal === Visibility.Hidden ? 0 : options.horizontalScrollbarSize), (options.vertical === Visibility.Hidden ? 0 : options.verticalScrollbarSize) ); - super(options.forbidTranslate3dUse, parent, s, options.horizontal, 'horizontal'); + super(options.forbidTranslate3dUse, options.lazyRender, parent, s, options.horizontal, 'horizontal'); this._scrollable = scrollable; this._createDomNode(); diff --git a/src/vs/base/browser/ui/scrollbar/scrollableElement.ts b/src/vs/base/browser/ui/scrollbar/scrollableElement.ts index d5a59df1bd048edafc14df8eba393bf2dce932ff..5c47dc53d75a93395f851ce6d2deb2df6d9ab48c 100644 --- a/src/vs/base/browser/ui/scrollbar/scrollableElement.ts +++ b/src/vs/base/browser/ui/scrollbar/scrollableElement.ts @@ -12,6 +12,12 @@ export interface IScrollableElementCreationOptions { */ forbidTranslate3dUse?: boolean; + /** + * The scrollable element should not do any DOM mutations until renderNow() is called. + * Defaults to false. + */ + lazyRender?: boolean; + /** * CSS Class name for the scrollable element. */ @@ -143,6 +149,12 @@ export interface IScrollableElement { */ dispose(): void; + /** + * Render / mutate the DOM now. + * Should be used together with the ctor option `lazyRender`. + */ + renderNow(): void; + /** * Update the class name of the scrollable element. */ @@ -176,15 +188,16 @@ export interface IMouseWheelEvent { export interface IScrollbar { domNode: FastDomNode; dispose(): void; - slider: FastDomNode; - onElementSize(size: number): void; - onElementScrollSize(scrollSize: number): void; - onElementScrollPosition(scrollPosition: number): void; + onElementSize(size: number): boolean; + onElementScrollSize(scrollSize: number): boolean; + onElementScrollPosition(scrollPosition: number): boolean; beginReveal(): void; beginHide(): void; delegateMouseDown(browserEvent: MouseEvent): void; validateScrollPosition(scrollPosition: number): number; - setDesiredScrollPosition(scrollPosition: number): void; + setDesiredScrollPosition(scrollPosition: number): boolean; + + render(): void; } export interface IParent { @@ -212,6 +225,7 @@ export function visibilityFromString(visibility: string): Visibility { export interface IScrollableElementOptions { forbidTranslate3dUse: boolean; + lazyRender: boolean; className: string; useShadows: boolean; handleMouseWheel: boolean; diff --git a/src/vs/base/browser/ui/scrollbar/scrollableElementImpl.ts b/src/vs/base/browser/ui/scrollbar/scrollableElementImpl.ts index 11a6677df6e03bce7ca3c3f8ad254447f5636c49..0cc1c3524dcaea437fa5320044b3e3111a544203 100644 --- a/src/vs/base/browser/ui/scrollbar/scrollableElementImpl.ts +++ b/src/vs/base/browser/ui/scrollbar/scrollableElementImpl.ts @@ -19,13 +19,13 @@ import {IDisposable, disposeAll} from 'vs/base/common/lifecycle'; import {IScrollable, DelegateScrollable} 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/styleMutator'; const HIDE_TIMEOUT = 500; const SCROLL_WHEEL_SENSITIVITY = 50; export class ScrollableElement extends Widget implements IScrollableElement { - private _originalElement: HTMLElement; private _options: IScrollableElementOptions; private _scrollable: DelegateScrollable; public verticalScrollbarWidth: number; @@ -34,9 +34,10 @@ export class ScrollableElement extends Widget implements IScrollableElement { private _horizontalScrollbar: IScrollbar; private _domNode: HTMLElement; - private _leftShadowDomNode: HTMLElement; - private _topShadowDomNode: HTMLElement; - private _topLeftShadowDomNode: HTMLElement; + private _leftShadowDomNode: FastDomNode; + private _topShadowDomNode: FastDomNode; + private _topLeftShadowDomNode: FastDomNode; + private _listenOnDomNode: HTMLElement; private _mouseWheelToDispose: IDisposable[]; @@ -45,13 +46,12 @@ export class ScrollableElement extends Widget implements IScrollableElement { private _isDragging: boolean; private _mouseIsOver: boolean; - private _dimensions: IDimensions; private _hideTimeout: TimeoutTimer; + private _shouldRender: boolean; constructor(element: HTMLElement, scrollable:IScrollable, options: IScrollableElementCreationOptions, dimensions: IDimensions = null) { super(); - this._originalElement = element; - this._originalElement.style.overflow = 'hidden'; + element.style.overflow = 'hidden'; this._options = this._createOptions(options); this._scrollable = this._register(new DelegateScrollable(scrollable, () => this._onScroll())); @@ -67,24 +67,22 @@ export class ScrollableElement extends Widget implements IScrollableElement { this._domNode.setAttribute('role', 'presentation'); this._domNode.style.position = 'relative'; this._domNode.style.overflow = 'hidden'; - this._domNode.appendChild(this._originalElement); + this._domNode.appendChild(element); this._domNode.appendChild(this._horizontalScrollbar.domNode.domNode); this._domNode.appendChild(this._verticalScrollbar.domNode.domNode); if (this._options.useShadows) { - this._leftShadowDomNode = document.createElement('div'); - this._leftShadowDomNode.className = 'shadow'; - this._domNode.appendChild(this._leftShadowDomNode); - } + this._leftShadowDomNode = createFastDomNode(document.createElement('div')); + this._leftShadowDomNode.setClassName('shadow'); + this._domNode.appendChild(this._leftShadowDomNode.domNode); - if (this._options.useShadows) { - this._topShadowDomNode = document.createElement('div'); - this._topShadowDomNode.className = 'shadow'; - this._domNode.appendChild(this._topShadowDomNode); + this._topShadowDomNode = createFastDomNode(document.createElement('div')); + this._topShadowDomNode.setClassName('shadow'); + this._domNode.appendChild(this._topShadowDomNode.domNode); - this._topLeftShadowDomNode = document.createElement('div'); - this._topLeftShadowDomNode.className = 'shadow top-left-corner'; - this._domNode.appendChild(this._topLeftShadowDomNode); + this._topLeftShadowDomNode = createFastDomNode(document.createElement('div')); + this._topLeftShadowDomNode.setClassName('shadow top-left-corner'); + this._domNode.appendChild(this._topLeftShadowDomNode.domNode); } this._listenOnDomNode = this._options.listenOnDomNode || this._domNode; @@ -101,8 +99,10 @@ export class ScrollableElement extends Widget implements IScrollableElement { this._mouseIsOver = false; this.onElementDimensions(dimensions, true); - this._horizontalScrollbar.onElementScrollSize(this._scrollable.getScrollWidth()); - this._verticalScrollbar.onElementScrollSize(this._scrollable.getScrollHeight()); + + this._shouldRender = true; + this._shouldRender = this._horizontalScrollbar.onElementScrollSize(this._scrollable.getScrollWidth()) || this._shouldRender; + this._shouldRender = this._verticalScrollbar.onElementScrollSize(this._scrollable.getScrollHeight()) || this._shouldRender; } public dispose(): void { @@ -141,9 +141,10 @@ export class ScrollableElement extends Widget implements IScrollableElement { height: this._domNode.clientHeight }; } - this._dimensions = this._computeDimensions(dimensions.width, dimensions.height); - this._verticalScrollbar.onElementSize(this._dimensions.height); - this._horizontalScrollbar.onElementSize(this._dimensions.width); + let width = Math.round(dimensions.width); + let height = Math.round(dimensions.height); + this._shouldRender = this._verticalScrollbar.onElementSize(height) || this._shouldRender; + this._shouldRender = this._horizontalScrollbar.onElementSize(width) || this._shouldRender; } public updateClassName(newClassName: string): void { @@ -239,11 +240,11 @@ export class ScrollableElement extends Widget implements IScrollableElement { if (desiredScrollTop !== -1 || desiredScrollLeft !== -1) { if (desiredScrollTop !== -1) { - this._verticalScrollbar.setDesiredScrollPosition(desiredScrollTop); + this._shouldRender = this._verticalScrollbar.setDesiredScrollPosition(desiredScrollTop) || this._shouldRender; desiredScrollTop = -1; } if (desiredScrollLeft !== -1) { - this._horizontalScrollbar.setDesiredScrollPosition(desiredScrollLeft); + this._shouldRender = this._horizontalScrollbar.setDesiredScrollPosition(desiredScrollLeft) || this._shouldRender; desiredScrollLeft = -1; } } @@ -259,33 +260,48 @@ export class ScrollableElement extends Widget implements IScrollableElement { let scrollWidth = this._scrollable.getScrollWidth(); let scrollLeft = this._scrollable.getScrollLeft(); - this._horizontalScrollbar.onElementScrollSize(scrollWidth); - this._verticalScrollbar.onElementScrollSize(scrollHeight); - this._verticalScrollbar.onElementScrollPosition(scrollTop); - this._horizontalScrollbar.onElementScrollPosition(scrollLeft); + this._shouldRender = this._horizontalScrollbar.onElementScrollSize(scrollWidth) || this._shouldRender; + this._shouldRender = this._verticalScrollbar.onElementScrollSize(scrollHeight) || this._shouldRender; + this._shouldRender = this._verticalScrollbar.onElementScrollPosition(scrollTop) || this._shouldRender; + this._shouldRender = this._horizontalScrollbar.onElementScrollPosition(scrollLeft) || this._shouldRender; if (this._options.useShadows) { - let enableTop = scrollHeight > 0 && scrollTop > 0; - let enableLeft = this._options.useShadows && scrollWidth > 0 && scrollLeft > 0; + this._shouldRender = true; + } - if (this._topShadowDomNode) { - DomUtils.toggleClass(this._topShadowDomNode, 'top', enableTop); - } + this._reveal(); - if (this._topLeftShadowDomNode) { - DomUtils.toggleClass(this._topLeftShadowDomNode, 'top', enableTop); - } + if (!this._options.lazyRender) { + this._render(); + } + } - if (this._leftShadowDomNode) { - DomUtils.toggleClass(this._leftShadowDomNode, 'left', enableLeft); - } + public renderNow(): void { + if (!this._options.lazyRender) { + throw new Error('Please use `lazyRender` together with `renderNow`!'); + } - if (this._topLeftShadowDomNode) { - DomUtils.toggleClass(this._topLeftShadowDomNode, 'left', enableLeft); - } + this._render(); + } + + private _render(): void { + if (!this._shouldRender) { + return; } - this._reveal(); + this._shouldRender = false; + + this._horizontalScrollbar.render(); + this._verticalScrollbar.render(); + + if (this._options.useShadows) { + let enableTop = this._scrollable.getScrollTop() > 0; + let enableLeft = this._scrollable.getScrollLeft() > 0; + + this._leftShadowDomNode.setClassName('shadow' + (enableLeft ? ' left' : '')); + this._topShadowDomNode.setClassName('shadow' + (enableTop ? ' top' : '')); + this._topLeftShadowDomNode.setClassName('shadow top-left-corner' + (enableTop ? ' top' : '') + (enableLeft ? ' left' : '')); + } } // -------------------- fade in / fade out -------------------- @@ -329,16 +345,6 @@ export class ScrollableElement extends Widget implements IScrollableElement { // -------------------- size & layout -------------------- - private _computeDimensions(clientWidth: number, clientHeight: number): IDimensions { - let width = clientWidth; - let height = clientHeight; - - return { - width: width, - height: height - }; - } - private _createOptions(options: IScrollableElementCreationOptions): IScrollableElementOptions { function ensureValue(source: any, prop: string, value: V) { @@ -350,6 +356,7 @@ export class ScrollableElement extends Widget implements IScrollableElement { let result: IScrollableElementOptions = { forbidTranslate3dUse: ensureValue(options, 'forbidTranslate3dUse', false), + lazyRender: ensureValue(options, 'lazyRender', false), className: ensureValue(options, 'className', ''), useShadows: ensureValue(options, 'useShadows', true), handleMouseWheel: ensureValue(options, 'handleMouseWheel', true), diff --git a/src/vs/base/browser/ui/scrollbar/verticalScrollbar.ts b/src/vs/base/browser/ui/scrollbar/verticalScrollbar.ts index f383b5692ce0d32b355dae19287876f42e3d0676..5a052eefe07941cbc0bb90f10ca2154f304e359c 100644 --- a/src/vs/base/browser/ui/scrollbar/verticalScrollbar.ts +++ b/src/vs/base/browser/ui/scrollbar/verticalScrollbar.ts @@ -22,7 +22,7 @@ export class VerticalScrollbar extends AbstractScrollbar { // give priority to vertical scroll bar over horizontal and let it scroll all the way to the bottom 0 ); - super(options.forbidTranslate3dUse, parent, s, options.vertical, 'vertical'); + super(options.forbidTranslate3dUse, options.lazyRender, parent, s, options.vertical, 'vertical'); this._scrollable = scrollable; this._createDomNode(); diff --git a/src/vs/editor/browser/view/viewImpl.ts b/src/vs/editor/browser/view/viewImpl.ts index 7cb2c456698a9fc70f20154c010a0eb7e7d27138..5dda3d4aba3bd81ef28a249b7e45a769f66ac10e 100644 --- a/src/vs/editor/browser/view/viewImpl.ts +++ b/src/vs/editor/browser/view/viewImpl.ts @@ -909,6 +909,9 @@ export class View extends ViewEventHandler implements editorBrowser.IView, IDisp viewPart.onDidRender(); } + // Render the scrollbar + this.layoutProvider.renderScrollbar(); + t.stop(); } diff --git a/src/vs/editor/browser/viewLayout/layoutProvider.ts b/src/vs/editor/browser/viewLayout/layoutProvider.ts index 6d724e3e15c342072fc72c4ed4d1518b872187fc..c0c0f731fcd9e85e04f16638df59a9a4a5b70d6f 100644 --- a/src/vs/editor/browser/viewLayout/layoutProvider.ts +++ b/src/vs/editor/browser/viewLayout/layoutProvider.ts @@ -249,4 +249,8 @@ export class LayoutProvider extends ViewEventHandler implements IDisposable, ILa public getScrolledTopFromAbsoluteTop(top:number): number { return top - this.scrollable.getScrollTop(); } + + public renderScrollbar(): void { + this.scrollManager.renderScrollbar(); + } } \ No newline at end of file diff --git a/src/vs/editor/browser/viewLayout/scrollManager.ts b/src/vs/editor/browser/viewLayout/scrollManager.ts index ce2e4aa986db7d7aca9c8b3c4f589eda224f795d..886ab3968a5dcb628723fe981b43be5cdeae9a49 100644 --- a/src/vs/editor/browser/viewLayout/scrollManager.ts +++ b/src/vs/editor/browser/viewLayout/scrollManager.ts @@ -47,6 +47,7 @@ export class ScrollManager implements IDisposable { horizontal: configScrollbarOpts.horizontal, className: ClassNames.SCROLLABLE_ELEMENT + ' ' + this.configuration.editor.theme, useShadows: false, + lazyRender: true, saveLastScrollTimeOnClassName: ClassNames.VIEW_LINE }; addPropertyIfPresent(configScrollbarOpts, scrollbarOptions, 'verticalHasArrows'); @@ -105,6 +106,10 @@ export class ScrollManager implements IDisposable { this.toDispose = disposeAll(this.toDispose); } + public renderScrollbar(): void { + this.scrollbar.renderNow(); + } + public onSizeProviderLayoutChanged(): void { if (this.scrollbar) { this.scrollbar.onElementDimensions({