diff --git a/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts b/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts index ea7b14832fc..9bfaefbf135 100644 --- a/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts +++ b/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts @@ -7,130 +7,51 @@ import * as Browser from 'vs/base/browser/browser'; import * as Platform from 'vs/base/common/platform'; import * as DomUtils from 'vs/base/browser/dom'; -import {IMouseEvent, StandardMouseEvent} from 'vs/base/browser/mouseEvent'; -import {IParent, Visibility} from 'vs/base/browser/ui/scrollbar/scrollableElement'; -import {Disposable} from 'vs/base/common/lifecycle'; +import {IMouseEvent, StandardMouseEvent, StandardMouseWheelEvent} from 'vs/base/browser/mouseEvent'; +import {Visibility} from 'vs/base/browser/ui/scrollbar/scrollableElement'; import {GlobalMouseMoveMonitor, IStandardMouseMoveEventData, standardMouseMoveMerger} from 'vs/base/browser/globalMouseMoveMonitor'; import {Widget} from 'vs/base/browser/ui/widget'; -import {TimeoutTimer} from 'vs/base/common/async'; import {FastDomNode, createFastDomNode} from 'vs/base/browser/styleMutator'; import {ScrollbarState} from 'vs/base/browser/ui/scrollbar/scrollbarState'; -import {ScrollbarArrow, IMouseWheelEventFactory} from 'vs/base/browser/ui/scrollbar/scrollbarArrow'; +import {ScrollbarArrow, ScrollbarArrowOptions} from 'vs/base/browser/ui/scrollbar/scrollbarArrow'; +import {ScrollbarVisibilityController} from 'vs/base/browser/ui/scrollbar/scrollbarVisibilityController'; +import {DelegateScrollable} from 'vs/base/common/scrollable'; /** * The orthogonal distance to the slider at which dragging "resets". This implements "snapping" */ const MOUSE_DRAG_RESET_DISTANCE = 140; -class VisibilityController extends Disposable { - private _visibility: Visibility; - private _visibleClassName: string; - private _invisibleClassName: string; - private _domNode: FastDomNode; - private _shouldBeVisible: boolean; - private _isNeeded: boolean; - private _isVisible: boolean; - private _revealTimer: TimeoutTimer; - - constructor(visibility: Visibility, visibleClassName: string, invisibleClassName: string) { - super(); - this._visibility = visibility; - this._visibleClassName = visibleClassName; - this._invisibleClassName = invisibleClassName; - this._domNode = null; - this._isVisible = false; - this._isNeeded = false; - this._shouldBeVisible = false; - this._revealTimer = this._register(new TimeoutTimer()); - } - - // ----------------- Hide / Reveal - - private applyVisibilitySetting(shouldBeVisible: boolean): boolean { - if (this._visibility === Visibility.Hidden) { - return false; - } - if (this._visibility === Visibility.Visible) { - return true; - } - return shouldBeVisible; - } - - public setShouldBeVisible(rawShouldBeVisible: boolean): void { - let shouldBeVisible = this.applyVisibilitySetting(rawShouldBeVisible); - - if (this._shouldBeVisible !== shouldBeVisible) { - this._shouldBeVisible = shouldBeVisible; - this.ensureVisibility(); - } - } - - public setIsNeeded(isNeeded: boolean): void { - if (this._isNeeded !== isNeeded) { - this._isNeeded = isNeeded; - this.ensureVisibility(); - } - } - - public setDomNode(domNode: FastDomNode): void { - this._domNode = domNode; - this._domNode.setClassName(this._invisibleClassName); - - // Now that the flags & the dom node are in a consistent state, ensure the Hidden/Visible configuration - this.setShouldBeVisible(false); - } - - public ensureVisibility(): void { - - if (!this._isNeeded) { - // Nothing to be rendered - this._hide(false); - return; - } - - if (this._shouldBeVisible) { - this._reveal(); - } else { - this._hide(true); - } - } - - - private _reveal(): void { - if (this._isVisible) { - return; - } - this._isVisible = true; - - // The CSS animation doesn't play otherwise - this._revealTimer.setIfNotSet(() => { - this._domNode.setClassName(this._visibleClassName); - }, 0); - } - - private _hide(withFadeAway: boolean): void { - this._revealTimer.cancel(); - if (!this._isVisible) { - return; - } - this._isVisible = false; - this._domNode.setClassName(this._invisibleClassName + (withFadeAway ? ' fade' : '')); - } -} - export interface IMouseMoveEventData { leftButton: boolean; posx: number; posy: number; } +export interface ScrollbarHost { + onMouseWheel(mouseWheelEvent: StandardMouseWheelEvent): void; + onDragStart(): void; + onDragEnd(): void; +} + +export interface AbstractScrollbarOptions { + forbidTranslate3dUse: boolean; + lazyRender:boolean; + host: ScrollbarHost; + scrollbarState: ScrollbarState; + visibility: Visibility; + extraScrollbarClassName: string; + scrollable: DelegateScrollable; +} + export abstract class AbstractScrollbar extends Widget { protected _forbidTranslate3dUse: boolean; + protected _host: ScrollbarHost; + protected _scrollable: DelegateScrollable; private _lazyRender: boolean; - private _parent: IParent; private _scrollbarState: ScrollbarState; - private _visibilityController: VisibilityController; + private _visibilityController: ScrollbarVisibilityController; private _mouseMoveMonitor: GlobalMouseMoveMonitor; public domNode: FastDomNode; @@ -138,23 +59,16 @@ export abstract class AbstractScrollbar extends Widget { protected _shouldRender: boolean; - constructor(forbidTranslate3dUse: boolean, lazyRender:boolean, parent: IParent, scrollbarState: ScrollbarState, visibility: Visibility, extraScrollbarClassName: string) { + constructor(opts:AbstractScrollbarOptions) { 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._forbidTranslate3dUse = opts.forbidTranslate3dUse; + this._lazyRender = opts.lazyRender; + this._host = opts.host; + this._scrollable = opts.scrollable; + this._scrollbarState = opts.scrollbarState; + this._visibilityController = this._register(new ScrollbarVisibilityController(opts.visibility, 'visible scrollbar ' + opts.extraScrollbarClassName, 'invisible scrollbar ' + opts.extraScrollbarClassName)); this._mouseMoveMonitor = this._register(new GlobalMouseMoveMonitor()); this._shouldRender = true; - } - - // ----------------- initialize & clean-up - - /** - * Creates the container dom node for the scrollbar & hooks up the events - */ - protected _createDomNode(): void { this.domNode = createFastDomNode(document.createElement('div')); if (!this._forbidTranslate3dUse && Browser.canUseTranslate3d) { // Put the scrollbar in its own layer @@ -167,11 +81,13 @@ export abstract class AbstractScrollbar extends Widget { this.onmousedown(this.domNode.domNode, (e) => this._domNodeMouseDown(e)); } + // ----------------- creation + /** * Creates the dom node for an arrow & adds it to the container */ - protected _createArrow(className: string, top: number, left: number, bottom: number, right: number, bgWidth: number, bgHeight: number, mouseWheelEventFactory: IMouseWheelEventFactory): void { - let arrow = this._register(new ScrollbarArrow(className, top, left, bottom, right, bgWidth, bgHeight, mouseWheelEventFactory, this._parent)); + protected _createArrow(opts:ScrollbarArrowOptions): void { + let arrow = this._register(new ScrollbarArrow(opts)); this.domNode.domNode.appendChild(arrow.bgDomNode); this.domNode.domNode.appendChild(arrow.domNode); } @@ -301,12 +217,12 @@ export abstract class AbstractScrollbar extends Widget { }, () => { this.slider.toggleClassName('active', false); - this._parent.onDragEnd(); + this._host.onDragEnd(); } ); e.preventDefault(); - this._parent.onDragStart(); + this._host.onDragStart(); } } diff --git a/src/vs/base/browser/ui/scrollbar/horizontalScrollbar.ts b/src/vs/base/browser/ui/scrollbar/horizontalScrollbar.ts index 2b82b01d090..31b904038a0 100644 --- a/src/vs/base/browser/ui/scrollbar/horizontalScrollbar.ts +++ b/src/vs/base/browser/ui/scrollbar/horizontalScrollbar.ts @@ -5,43 +5,61 @@ 'use strict'; import * as Browser from 'vs/base/browser/browser'; -import {AbstractScrollbar, IMouseMoveEventData} from 'vs/base/browser/ui/scrollbar/abstractScrollbar'; +import {AbstractScrollbar, ScrollbarHost, IMouseMoveEventData} from 'vs/base/browser/ui/scrollbar/abstractScrollbar'; import {IMouseEvent, StandardMouseWheelEvent} from 'vs/base/browser/mouseEvent'; import {IDomNodePosition} from 'vs/base/browser/dom'; -import {IParent, IScrollableElementOptions, Visibility} from 'vs/base/browser/ui/scrollbar/scrollableElement'; +import {ScrollableElementResolvedOptions, Visibility} from 'vs/base/browser/ui/scrollbar/scrollableElement'; import {DelegateScrollable} 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'; export class HorizontalScrollbar extends AbstractScrollbar { - private _scrollable: DelegateScrollable; + constructor(scrollable: DelegateScrollable, options: ScrollableElementResolvedOptions, host: ScrollbarHost) { + super({ + forbidTranslate3dUse: options.forbidTranslate3dUse, + lazyRender: options.lazyRender, + host: host, + scrollbarState: new ScrollbarState( + (options.horizontalHasArrows ? options.arrowSize : 0), + (options.horizontal === Visibility.Hidden ? 0 : options.horizontalScrollbarSize), + (options.vertical === Visibility.Hidden ? 0 : options.verticalScrollbarSize) + ), + visibility: options.horizontal, + extraScrollbarClassName: 'horizontal', + scrollable: scrollable + }); - constructor(scrollable: DelegateScrollable, parent: IParent, options: IScrollableElementOptions) { - let s = new ScrollbarState( - (options.horizontalHasArrows ? options.arrowSize : 0), - (options.horizontal === Visibility.Hidden ? 0 : options.horizontalScrollbarSize), - (options.vertical === Visibility.Hidden ? 0 : options.verticalScrollbarSize) - ); - super(options.forbidTranslate3dUse, options.lazyRender, parent, s, options.horizontal, 'horizontal'); - this._scrollable = scrollable; - - this._createDomNode(); if (options.horizontalHasArrows) { let arrowDelta = (options.arrowSize - ARROW_IMG_SIZE) / 2; let scrollbarDelta = (options.horizontalScrollbarSize - ARROW_IMG_SIZE) / 2; - this._createArrow('left-arrow', scrollbarDelta, arrowDelta, null, null, options.arrowSize, options.horizontalScrollbarSize, () => this._createMouseWheelEvent(1)); - this._createArrow('right-arrow', scrollbarDelta, null, null, arrowDelta, options.arrowSize, options.horizontalScrollbarSize, () => this._createMouseWheelEvent(-1)); + this._createArrow({ + className: 'left-arrow', + top: scrollbarDelta, + left: arrowDelta, + bottom: void 0, + right: void 0, + bgWidth: options.arrowSize, + bgHeight: options.horizontalScrollbarSize, + onActivate: () => this._host.onMouseWheel(new StandardMouseWheelEvent(null, 1, 0)), + }); + + this._createArrow({ + className: 'right-arrow', + top: scrollbarDelta, + left: void 0, + bottom: void 0, + right: arrowDelta, + bgWidth: options.arrowSize, + bgHeight: options.horizontalScrollbarSize, + onActivate: () => this._host.onMouseWheel(new StandardMouseWheelEvent(null, -1, 0)), + }); } this._createSlider(Math.floor((options.horizontalScrollbarSize - options.horizontalSliderSize) / 2), 0, null, options.horizontalSliderSize); } - protected _createMouseWheelEvent(sign: number) { - return new StandardMouseWheelEvent(null, sign, 0); - } - protected _updateSlider(sliderSize: number, sliderPosition: number): void { this.slider.setWidth(sliderSize); if (!this._forbidTranslate3dUse && Browser.canUseTranslate3d) { diff --git a/src/vs/base/browser/ui/scrollbar/scrollableElement.ts b/src/vs/base/browser/ui/scrollbar/scrollableElement.ts index 722eff71b5f..be1e2f0d7e1 100644 --- a/src/vs/base/browser/ui/scrollbar/scrollableElement.ts +++ b/src/vs/base/browser/ui/scrollbar/scrollableElement.ts @@ -4,205 +4,99 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -export interface IScrollableElementCreationOptions { +export interface ScrollableElementCreationOptions { /** * Prevent the scrollbar rendering from using translate3d. Defaults to false. */ 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. */ className?: string; - /** * Drop subtle horizontal and vertical shadows. * Defaults to false. */ useShadows?: boolean; - /** * Handle mouse wheel (listen to mouse wheel scrolling). * Defaults to true */ handleMouseWheel?: boolean; - /** * Flip axes. Treat vertical scrolling like horizontal and vice-versa. * Defaults to false; */ flipAxes?: boolean; - /** * A multiplier to be used on the `deltaX` and `deltaY` of mouse wheel scroll events. * Defaults to 1. */ mouseWheelScrollSensitivity?: number; - /** * Height for vertical arrows (top/bottom) and width for horizontal arrows (left/right). * Defaults to 11. */ arrowSize?: number; - /** * The dom node events should be bound to. * If no listenOnDomNode is provided, the dom node passed to the constructor will be used for event listening. */ listenOnDomNode?: HTMLElement; - /** * Control the visibility of the horizontal scrollbar. * Accepted values: 'auto' (on mouse over), 'visible' (always visible), 'hidden' (never visible) * Defaults to 'auto'. */ horizontal?: string; - /** * Height (in px) of the horizontal scrollbar. * Defaults to 10. */ horizontalScrollbarSize?: number; - /** * Height (in px) of the horizontal scrollbar slider. * Defaults to `horizontalScrollbarSize` */ horizontalSliderSize?: number; - /** * Render arrows (left/right) for the horizontal scrollbar. * Defaults to false. */ horizontalHasArrows?: boolean; - /** * Control the visibility of the vertical scrollbar. * Accepted values: 'auto' (on mouse over), 'visible' (always visible), 'hidden' (never visible) * Defaults to 'auto'. */ vertical?: string; - /** * Width (in px) of the vertical scrollbar. * Defaults to 10. */ verticalScrollbarSize?: number; - /** * Width (in px) of the vertical scrollbar slider. * Defaults to `verticalScrollbarSize` */ verticalSliderSize?: number; - /** * Render arrows (top/bottom) for the vertical scrollbar. * Defaults to false. */ verticalHasArrows?: boolean; - /** * Add a `last-scroll-time` attribute to scroll targets or parents of scroll targets matching the following class name */ saveLastScrollTimeOnClassName?: string; } -export interface IOverviewRulerLayoutInfo { - parent: HTMLElement; - insertBefore: HTMLElement; -} - -export interface IDimensions { - width: number; - height: number; -} - -/** - * An Element that uses fancy scrollbars. - */ -export interface IScrollableElement { - - /** - * Get the generated 'scrollable' dom node - */ - getDomNode(): HTMLElement; - - /** - * Let the scrollable element know that the generated dom node's width / height might have changed. - */ - onElementDimensions(dimensions?: IDimensions): void; - - /** - * Dispose. - */ - 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. - */ - updateClassName(newClassName: string): void; - - /** - * Update configuration options for the scrollbar. - * Really this is Editor.IEditorScrollbarOptions, but base shouldn't - * depend on Editor. - */ - updateOptions(newOptions: IScrollableElementCreationOptions): void; - - getOverviewRulerLayoutInfo(): IOverviewRulerLayoutInfo; - - /** - * Delegate a mouse down event to the vertical scrollbar. - * This is to help with clicking somewhere else and having the scrollbar react. - */ - delegateVerticalScrollbarMouseDown(browserEvent: MouseEvent): void; - -} - -export interface IMouseWheelEvent { - browserEvent: MouseWheelEvent; - deltaX: number; - deltaY: number; - preventDefault(): void; - stopPropagation(): void; -} - -export interface IParent { - onMouseWheel(mouseWheelEvent: IMouseWheelEvent): void; - onDragStart(): void; - onDragEnd(): void; -} - -export enum Visibility { - Auto, - Hidden, - Visible -} - -export function visibilityFromString(visibility: string): Visibility { - switch (visibility) { - case 'hidden': - return Visibility.Hidden; - case 'visible': - return Visibility.Visible; - default: - return Visibility.Auto; - } -} - -export interface IScrollableElementOptions { +export interface ScrollableElementResolvedOptions { forbidTranslate3dUse: boolean; lazyRender: boolean; className: string; @@ -222,3 +116,62 @@ export interface IScrollableElementOptions { verticalHasArrows: boolean; saveLastScrollTimeOnClassName: string; } + +export interface IOverviewRulerLayoutInfo { + parent: HTMLElement; + insertBefore: HTMLElement; +} + +export interface IDimensions { + width: number; + height: number; +} + +/** + * An Element that uses fancy scrollbars. + */ +export interface IScrollableElement { + + + getDomNode(): HTMLElement; + + + onElementDimensions(dimensions?: IDimensions): void; + + /** + * Dispose. + */ + dispose(): void; + + + renderNow(): void; + + + updateClassName(newClassName: string): void; + + + updateOptions(newOptions: ScrollableElementCreationOptions): void; + + getOverviewRulerLayoutInfo(): IOverviewRulerLayoutInfo; + + + delegateVerticalScrollbarMouseDown(browserEvent: MouseEvent): void; +} + +export enum Visibility { + Auto, + Hidden, + Visible +} + +export function visibilityFromString(visibility: string): Visibility { + switch (visibility) { + case 'hidden': + return Visibility.Hidden; + case 'visible': + return Visibility.Visible; + default: + return Visibility.Auto; + } +} + diff --git a/src/vs/base/browser/ui/scrollbar/scrollableElementImpl.ts b/src/vs/base/browser/ui/scrollbar/scrollableElementImpl.ts index 5f51e49f5e5..7f422ce0826 100644 --- a/src/vs/base/browser/ui/scrollbar/scrollableElementImpl.ts +++ b/src/vs/base/browser/ui/scrollbar/scrollableElementImpl.ts @@ -12,21 +12,22 @@ import {StandardMouseWheelEvent, IMouseEvent} from 'vs/base/browser/mouseEvent'; import {HorizontalScrollbar} from 'vs/base/browser/ui/scrollbar/horizontalScrollbar'; import {VerticalScrollbar} from 'vs/base/browser/ui/scrollbar/verticalScrollbar'; import { - IScrollableElementOptions, IDimensions, IMouseWheelEvent, visibilityFromString, - IScrollableElement, IScrollableElementCreationOptions, IOverviewRulerLayoutInfo + ScrollableElementResolvedOptions, IDimensions, visibilityFromString, + IScrollableElement, ScrollableElementCreationOptions, IOverviewRulerLayoutInfo } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import {IDisposable, dispose} 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'; +import {ScrollbarHost} from 'vs/base/browser/ui/scrollbar/abstractScrollbar'; const HIDE_TIMEOUT = 500; const SCROLL_WHEEL_SENSITIVITY = 50; export class ScrollableElement extends Widget implements IScrollableElement { - private _options: IScrollableElementOptions; + private _options: ScrollableElementResolvedOptions; private _scrollable: DelegateScrollable; private _verticalScrollbar: VerticalScrollbar; private _horizontalScrollbar: HorizontalScrollbar; @@ -47,15 +48,20 @@ export class ScrollableElement extends Widget implements IScrollableElement { private _hideTimeout: TimeoutTimer; private _shouldRender: boolean; - constructor(element: HTMLElement, scrollable:IScrollable, options: IScrollableElementCreationOptions, dimensions: IDimensions = null) { + constructor(element: HTMLElement, scrollable:IScrollable, options: ScrollableElementCreationOptions, dimensions: IDimensions = null) { super(); element.style.overflow = 'hidden'; - this._options = this._createOptions(options); + this._options = resolveOptions(options); this._scrollable = this._register(new DelegateScrollable(scrollable, () => this._onScroll())); - this._verticalScrollbar = this._register(new VerticalScrollbar(this._scrollable, this, this._options)); - this._horizontalScrollbar = this._register(new HorizontalScrollbar(this._scrollable, this, this._options)); + let scrollbarHost:ScrollbarHost = { + onMouseWheel: (mouseWheelEvent: StandardMouseWheelEvent) => this._onMouseWheel(mouseWheelEvent), + onDragStart: () => this._onDragStart(), + onDragEnd: () => this._onDragEnd(), + }; + this._verticalScrollbar = this._register(new VerticalScrollbar(this._scrollable, this._options, scrollbarHost)); + this._horizontalScrollbar = this._register(new HorizontalScrollbar(this._scrollable, this._options, scrollbarHost)); this._domNode = document.createElement('div'); this._domNode.className = 'monaco-scrollable-element ' + this._options.className; @@ -105,6 +111,9 @@ export class ScrollableElement extends Widget implements IScrollableElement { super.dispose(); } + /** + * Get the generated 'scrollable' dom node + */ public getDomNode(): HTMLElement { return this._domNode; } @@ -116,10 +125,17 @@ export class ScrollableElement extends Widget implements IScrollableElement { }; } + /** + * Delegate a mouse down event to the vertical scrollbar. + * This is to help with clicking somewhere else and having the scrollbar react. + */ public delegateVerticalScrollbarMouseDown(browserEvent: MouseEvent): void { this._verticalScrollbar.delegateMouseDown(browserEvent); } + /** + * Let the scrollable element know that the generated dom node's width / height might have changed. + */ public onElementDimensions(dimensions: IDimensions = null, synchronous: boolean = false): void { if (synchronous) { this._actualElementDimensions(dimensions); @@ -142,6 +158,9 @@ export class ScrollableElement extends Widget implements IScrollableElement { this._shouldRender = this._horizontalScrollbar.onElementSize(width) || this._shouldRender; } + /** + * Update the class name of the scrollable element. + */ public updateClassName(newClassName: string): void { this._options.className = newClassName; // Defaults are different on Macs @@ -151,9 +170,14 @@ export class ScrollableElement extends Widget implements IScrollableElement { this._domNode.className = 'monaco-scrollable-element ' + this._options.className; } - public updateOptions(newOptions: IScrollableElementCreationOptions): void { + /** + * Update configuration options for the scrollbar. + * Really this is Editor.IEditorScrollbarOptions, but base shouldn't + * depend on Editor. + */ + public updateOptions(newOptions: ScrollableElementCreationOptions): void { // only support handleMouseWheel changes for now - let massagedOptions = this._createOptions(newOptions); + let massagedOptions = resolveOptions(newOptions); this._options.handleMouseWheel = massagedOptions.handleMouseWheel; this._options.mouseWheelScrollSensitivity = massagedOptions.mouseWheelScrollSensitivity; this._setListeningToMouseWheel(this._options.handleMouseWheel); @@ -176,7 +200,7 @@ export class ScrollableElement extends Widget implements IScrollableElement { if (shouldListen) { let onMouseWheel = (browserEvent: MouseWheelEvent) => { let e = new StandardMouseWheelEvent(browserEvent); - this.onMouseWheel(e); + this._onMouseWheel(e); }; this._mouseWheelToDispose.push(DomUtils.addDisposableListener(this._listenOnDomNode, 'mousewheel', onMouseWheel)); @@ -184,7 +208,7 @@ export class ScrollableElement extends Widget implements IScrollableElement { } } - public onMouseWheel(e: IMouseWheelEvent): void { + private _onMouseWheel(e: StandardMouseWheelEvent): void { if (Platform.isMacintosh && e.browserEvent && this._options.saveLastScrollTimeOnClassName) { // Mark dom node with timestamp of wheel event let target = e.browserEvent.target; @@ -271,6 +295,10 @@ export class ScrollableElement extends Widget implements IScrollableElement { } } + /** + * Render / mutate the DOM now. + * Should be used together with the ctor option `lazyRender`. + */ public renderNow(): void { if (!this._options.lazyRender) { throw new Error('Please use `lazyRender` together with `renderNow`!'); @@ -301,12 +329,12 @@ export class ScrollableElement extends Widget implements IScrollableElement { // -------------------- fade in / fade out -------------------- - public onDragStart(): void { + private _onDragStart(): void { this._isDragging = true; this._reveal(); } - public onDragEnd(): void { + private _onDragEnd(): void { this._isDragging = false; this._hide(); } @@ -337,51 +365,41 @@ export class ScrollableElement extends Widget implements IScrollableElement { private _scheduleHide(): void { this._hideTimeout.cancelAndSet(() => this._hide(), HIDE_TIMEOUT); } - - // -------------------- size & layout -------------------- - - private _createOptions(options: IScrollableElementCreationOptions): IScrollableElementOptions { - - function ensureValue(source: any, prop: string, value: V) { - if (source.hasOwnProperty(prop)) { - return source[prop]; - } - return value; - } - - 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), - flipAxes: ensureValue(options, 'flipAxes', false), - mouseWheelScrollSensitivity: ensureValue(options, 'mouseWheelScrollSensitivity', 1), - arrowSize: ensureValue(options, 'arrowSize', 11), - - listenOnDomNode: ensureValue(options, 'listenOnDomNode', null), - - horizontal: visibilityFromString(ensureValue(options, 'horizontal', 'auto')), - horizontalScrollbarSize: ensureValue(options, 'horizontalScrollbarSize', 10), - horizontalSliderSize: 0, - horizontalHasArrows: ensureValue(options, 'horizontalHasArrows', false), - - vertical: visibilityFromString(ensureValue(options, 'vertical', 'auto')), - verticalScrollbarSize: ensureValue(options, 'verticalScrollbarSize', 10), - verticalHasArrows: ensureValue(options, 'verticalHasArrows', false), - verticalSliderSize: 0, - - saveLastScrollTimeOnClassName: ensureValue(options, 'saveLastScrollTimeOnClassName', null) - }; - - result.horizontalSliderSize = ensureValue(options, 'horizontalSliderSize', result.horizontalScrollbarSize); - result.verticalSliderSize = ensureValue(options, 'verticalSliderSize', result.verticalScrollbarSize); - - // Defaults are different on Macs - if (Platform.isMacintosh) { - result.className += ' mac'; - } - - return result; - } +} + +function resolveOptions(opts: ScrollableElementCreationOptions): ScrollableElementResolvedOptions { + let result: ScrollableElementResolvedOptions = { + forbidTranslate3dUse: (typeof opts.forbidTranslate3dUse !== 'undefined' ? opts.forbidTranslate3dUse : false), + lazyRender: (typeof opts.lazyRender !== 'undefined' ? opts.lazyRender : false), + className: (typeof opts.className !== 'undefined' ? opts.className : ''), + useShadows: (typeof opts.useShadows !== 'undefined' ? opts.useShadows : true), + handleMouseWheel: (typeof opts.handleMouseWheel !== 'undefined' ? opts.handleMouseWheel : true), + flipAxes: (typeof opts.flipAxes !== 'undefined' ? opts.flipAxes : false), + mouseWheelScrollSensitivity: (typeof opts.mouseWheelScrollSensitivity !== 'undefined' ? opts.mouseWheelScrollSensitivity : 1), + arrowSize: (typeof opts.arrowSize !== 'undefined' ? opts.arrowSize : 11), + + listenOnDomNode: (typeof opts.listenOnDomNode !== 'undefined' ? opts.listenOnDomNode : null), + + horizontal: visibilityFromString(typeof opts.horizontal !== 'undefined' ? opts.horizontal : 'auto'), + horizontalScrollbarSize: (typeof opts.horizontalScrollbarSize !== 'undefined' ? opts.horizontalScrollbarSize : 10), + horizontalSliderSize: (typeof opts.horizontalSliderSize !== 'undefined' ? opts.horizontalSliderSize : 0), + horizontalHasArrows: (typeof opts.horizontalHasArrows !== 'undefined' ? opts.horizontalHasArrows : false), + + vertical: visibilityFromString(typeof opts.vertical !== 'undefined' ? opts.vertical : 'auto'), + verticalScrollbarSize: (typeof opts.verticalScrollbarSize !== 'undefined' ? opts.verticalScrollbarSize : 10), + verticalHasArrows: (typeof opts.verticalHasArrows !== 'undefined' ? opts.verticalHasArrows : false), + verticalSliderSize: (typeof opts.verticalSliderSize !== 'undefined' ? opts.verticalSliderSize : 0), + + saveLastScrollTimeOnClassName: (typeof opts.saveLastScrollTimeOnClassName !== 'undefined' ? opts.saveLastScrollTimeOnClassName : null) + }; + + result.horizontalSliderSize = (typeof opts.horizontalSliderSize !== 'undefined' ? opts.horizontalSliderSize : result.horizontalScrollbarSize); + result.verticalSliderSize = (typeof opts.verticalSliderSize !== 'undefined' ? opts.verticalSliderSize : result.verticalScrollbarSize); + + // Defaults are different on Macs + if (Platform.isMacintosh) { + result.className += ' mac'; + } + + return result; } diff --git a/src/vs/base/browser/ui/scrollbar/scrollbarArrow.ts b/src/vs/base/browser/ui/scrollbar/scrollbarArrow.ts index eec3cd2c06e..1e70624cf0f 100644 --- a/src/vs/base/browser/ui/scrollbar/scrollbarArrow.ts +++ b/src/vs/base/browser/ui/scrollbar/scrollbarArrow.ts @@ -5,47 +5,76 @@ 'use strict'; import {IMouseEvent} from 'vs/base/browser/mouseEvent'; -import {IMouseWheelEvent, IParent} from 'vs/base/browser/ui/scrollbar/scrollableElement'; import {GlobalMouseMoveMonitor, IStandardMouseMoveEventData, standardMouseMoveMerger} from 'vs/base/browser/globalMouseMoveMonitor'; import {Widget} from 'vs/base/browser/ui/widget'; import {TimeoutTimer, IntervalTimer} from 'vs/base/common/async'; -export interface IMouseWheelEventFactory { - (): IMouseWheelEvent; -} - /** * The arrow image size. */ export const ARROW_IMG_SIZE = 11; +export interface ScrollbarArrowOptions { + onActivate: () => void; + className: string; + + bgWidth: number; + bgHeight: number; + + top?: number; + left?: number; + bottom?: number; + right?: number; +} + export class ScrollbarArrow extends Widget { - private _parent: IParent; - private _mouseWheelEventFactory: IMouseWheelEventFactory; + private _onActivate: () => void; public bgDomNode: HTMLElement; public domNode: HTMLElement; private _mousedownRepeatTimer: IntervalTimer; private _mousedownScheduleRepeatTimer: TimeoutTimer; private _mouseMoveMonitor: GlobalMouseMoveMonitor; - constructor(className: string, top: number, left: number, bottom: number, right: number, bgWidth: number, bgHeight: number, mouseWheelEventFactory: IMouseWheelEventFactory, parent: IParent) { + constructor(opts:ScrollbarArrowOptions) { super(); - this._parent = parent; - this._mouseWheelEventFactory = mouseWheelEventFactory; + this._onActivate = opts.onActivate; this.bgDomNode = document.createElement('div'); this.bgDomNode.className = 'arrow-background'; this.bgDomNode.style.position = 'absolute'; - setSize(this.bgDomNode, bgWidth, bgHeight); - setPosition(this.bgDomNode, (top !== null ? 0 : null), (left !== null ? 0 : null), (bottom !== null ? 0 : null), (right !== null ? 0 : null)); - + this.bgDomNode.style.width = opts.bgWidth + 'px'; + this.bgDomNode.style.height = opts.bgHeight + 'px'; + if (typeof opts.top !== 'undefined') { + this.bgDomNode.style.top = '0px'; + } + if (typeof opts.left !== 'undefined') { + this.bgDomNode.style.left = '0px'; + } + if (typeof opts.bottom !== 'undefined') { + this.bgDomNode.style.bottom = '0px'; + } + if (typeof opts.right !== 'undefined') { + this.bgDomNode.style.right = '0px'; + } this.domNode = document.createElement('div'); - this.domNode.className = className; + this.domNode.className = opts.className; this.domNode.style.position = 'absolute'; - setSize(this.domNode, ARROW_IMG_SIZE, ARROW_IMG_SIZE); - setPosition(this.domNode, top, left, bottom, right); + this.domNode.style.width = ARROW_IMG_SIZE + 'px'; + this.domNode.style.height = ARROW_IMG_SIZE + 'px'; + if (typeof opts.top !== 'undefined') { + this.domNode.style.top = opts.top + 'px'; + } + if (typeof opts.left !== 'undefined') { + this.domNode.style.left = opts.left + 'px'; + } + if (typeof opts.bottom !== 'undefined') { + this.domNode.style.bottom = opts.bottom + 'px'; + } + if (typeof opts.right !== 'undefined') { + this.domNode.style.right = opts.right + 'px'; + } this._mouseMoveMonitor = this._register(new GlobalMouseMoveMonitor()); this.onmousedown(this.bgDomNode, (e) => this._arrowMouseDown(e)); @@ -56,15 +85,11 @@ export class ScrollbarArrow extends Widget { } private _arrowMouseDown(e: IMouseEvent): void { - let repeater = () => { - this._parent.onMouseWheel(this._mouseWheelEventFactory()); - }; - let scheduleRepeater = () => { - this._mousedownRepeatTimer.cancelAndSet(repeater, 1000 / 24); + this._mousedownRepeatTimer.cancelAndSet(() => this._onActivate(), 1000 / 24); }; - repeater(); + this._onActivate(); this._mousedownRepeatTimer.cancel(); this._mousedownScheduleRepeatTimer.cancelAndSet(scheduleRepeater, 200); @@ -82,27 +107,3 @@ export class ScrollbarArrow extends Widget { e.preventDefault(); } } - -function setPosition(domNode: HTMLElement, top: number, left: number, bottom: number, right: number) { - if (top !== null) { - domNode.style.top = top + 'px'; - } - if (left !== null) { - domNode.style.left = left + 'px'; - } - if (bottom !== null) { - domNode.style.bottom = bottom + 'px'; - } - if (right !== null) { - domNode.style.right = right + 'px'; - } -} - -function setSize(domNode: HTMLElement, width: number, height: number) { - if (width !== null) { - domNode.style.width = width + 'px'; - } - if (height !== null) { - domNode.style.height = height + 'px'; - } -} diff --git a/src/vs/base/browser/ui/scrollbar/scrollbarVisibilityController.ts b/src/vs/base/browser/ui/scrollbar/scrollbarVisibilityController.ts new file mode 100644 index 00000000000..a71b53fcc77 --- /dev/null +++ b/src/vs/base/browser/ui/scrollbar/scrollbarVisibilityController.ts @@ -0,0 +1,105 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import {Visibility} from 'vs/base/browser/ui/scrollbar/scrollableElement'; +import {Disposable} from 'vs/base/common/lifecycle'; +import {TimeoutTimer} from 'vs/base/common/async'; +import {FastDomNode} from 'vs/base/browser/styleMutator'; + +export class ScrollbarVisibilityController extends Disposable { + private _visibility: Visibility; + private _visibleClassName: string; + private _invisibleClassName: string; + private _domNode: FastDomNode; + private _shouldBeVisible: boolean; + private _isNeeded: boolean; + private _isVisible: boolean; + private _revealTimer: TimeoutTimer; + + constructor(visibility: Visibility, visibleClassName: string, invisibleClassName: string) { + super(); + this._visibility = visibility; + this._visibleClassName = visibleClassName; + this._invisibleClassName = invisibleClassName; + this._domNode = null; + this._isVisible = false; + this._isNeeded = false; + this._shouldBeVisible = false; + this._revealTimer = this._register(new TimeoutTimer()); + } + + // ----------------- Hide / Reveal + + private applyVisibilitySetting(shouldBeVisible: boolean): boolean { + if (this._visibility === Visibility.Hidden) { + return false; + } + if (this._visibility === Visibility.Visible) { + return true; + } + return shouldBeVisible; + } + + public setShouldBeVisible(rawShouldBeVisible: boolean): void { + let shouldBeVisible = this.applyVisibilitySetting(rawShouldBeVisible); + + if (this._shouldBeVisible !== shouldBeVisible) { + this._shouldBeVisible = shouldBeVisible; + this.ensureVisibility(); + } + } + + public setIsNeeded(isNeeded: boolean): void { + if (this._isNeeded !== isNeeded) { + this._isNeeded = isNeeded; + this.ensureVisibility(); + } + } + + public setDomNode(domNode: FastDomNode): void { + this._domNode = domNode; + this._domNode.setClassName(this._invisibleClassName); + + // Now that the flags & the dom node are in a consistent state, ensure the Hidden/Visible configuration + this.setShouldBeVisible(false); + } + + public ensureVisibility(): void { + + if (!this._isNeeded) { + // Nothing to be rendered + this._hide(false); + return; + } + + if (this._shouldBeVisible) { + this._reveal(); + } else { + this._hide(true); + } + } + + private _reveal(): void { + if (this._isVisible) { + return; + } + this._isVisible = true; + + // The CSS animation doesn't play otherwise + this._revealTimer.setIfNotSet(() => { + this._domNode.setClassName(this._visibleClassName); + }, 0); + } + + private _hide(withFadeAway: boolean): void { + this._revealTimer.cancel(); + if (!this._isVisible) { + return; + } + this._isVisible = false; + this._domNode.setClassName(this._invisibleClassName + (withFadeAway ? ' fade' : '')); + } +} \ No newline at end of file diff --git a/src/vs/base/browser/ui/scrollbar/verticalScrollbar.ts b/src/vs/base/browser/ui/scrollbar/verticalScrollbar.ts index d9e356590ac..be764693fe0 100644 --- a/src/vs/base/browser/ui/scrollbar/verticalScrollbar.ts +++ b/src/vs/base/browser/ui/scrollbar/verticalScrollbar.ts @@ -5,44 +5,62 @@ 'use strict'; import * as Browser from 'vs/base/browser/browser'; -import {AbstractScrollbar, IMouseMoveEventData} from 'vs/base/browser/ui/scrollbar/abstractScrollbar'; +import {AbstractScrollbar, ScrollbarHost, IMouseMoveEventData} from 'vs/base/browser/ui/scrollbar/abstractScrollbar'; import {IMouseEvent, StandardMouseWheelEvent} from 'vs/base/browser/mouseEvent'; import {IDomNodePosition} from 'vs/base/browser/dom'; -import {IParent, IScrollableElementOptions, Visibility} from 'vs/base/browser/ui/scrollbar/scrollableElement'; +import {ScrollableElementResolvedOptions, Visibility} from 'vs/base/browser/ui/scrollbar/scrollableElement'; import {DelegateScrollable} 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'; export class VerticalScrollbar extends AbstractScrollbar { - private _scrollable: DelegateScrollable; + constructor(scrollable: DelegateScrollable, options: ScrollableElementResolvedOptions, host: ScrollbarHost) { + super({ + forbidTranslate3dUse: options.forbidTranslate3dUse, + lazyRender: options.lazyRender, + host: host, + scrollbarState: new ScrollbarState( + (options.verticalHasArrows ? options.arrowSize : 0), + (options.vertical === Visibility.Hidden ? 0 : options.verticalScrollbarSize), + // give priority to vertical scroll bar over horizontal and let it scroll all the way to the bottom + 0 + ), + visibility: options.vertical, + extraScrollbarClassName: 'vertical', + scrollable: scrollable + }); - constructor(scrollable: DelegateScrollable, parent: IParent, options: IScrollableElementOptions) { - let s = new ScrollbarState( - (options.verticalHasArrows ? options.arrowSize : 0), - (options.vertical === Visibility.Hidden ? 0 : options.verticalScrollbarSize), - // give priority to vertical scroll bar over horizontal and let it scroll all the way to the bottom - 0 - ); - super(options.forbidTranslate3dUse, options.lazyRender, parent, s, options.vertical, 'vertical'); - this._scrollable = scrollable; - - this._createDomNode(); if (options.verticalHasArrows) { let arrowDelta = (options.arrowSize - ARROW_IMG_SIZE) / 2; let scrollbarDelta = (options.verticalScrollbarSize - ARROW_IMG_SIZE) / 2; - this._createArrow('up-arrow', arrowDelta, scrollbarDelta, null, null, options.verticalScrollbarSize, options.arrowSize, () => this._createMouseWheelEvent(1)); - this._createArrow('down-arrow', null, scrollbarDelta, arrowDelta, null, options.verticalScrollbarSize, options.arrowSize, () => this._createMouseWheelEvent(-1)); + this._createArrow({ + className: 'up-arrow', + top: arrowDelta, + left: scrollbarDelta, + bottom: void 0, + right: void 0, + bgWidth: options.verticalScrollbarSize, + bgHeight: options.arrowSize, + onActivate: () => this._host.onMouseWheel(new StandardMouseWheelEvent(null, 0, 1)), + }); + + this._createArrow({ + className: 'down-arrow', + top: void 0, + left: scrollbarDelta, + bottom: arrowDelta, + right: void 0, + bgWidth: options.verticalScrollbarSize, + bgHeight: options.arrowSize, + onActivate: () => this._host.onMouseWheel(new StandardMouseWheelEvent(null, 0, -1)), + }); } this._createSlider(0, Math.floor((options.verticalScrollbarSize - options.verticalSliderSize) / 2), options.verticalSliderSize, null); } - protected _createMouseWheelEvent(sign: number) { - return new StandardMouseWheelEvent(null, 0, sign); - } - protected _updateSlider(sliderSize: number, sliderPosition: number): void { this.slider.setHeight(sliderSize); if (!this._forbidTranslate3dUse && Browser.canUseTranslate3d) { diff --git a/src/vs/editor/browser/viewLayout/scrollManager.ts b/src/vs/editor/browser/viewLayout/scrollManager.ts index fc4dd9665c1..150e39e4028 100644 --- a/src/vs/editor/browser/viewLayout/scrollManager.ts +++ b/src/vs/editor/browser/viewLayout/scrollManager.ts @@ -6,7 +6,7 @@ import {IDisposable, dispose} from 'vs/base/common/lifecycle'; import * as dom from 'vs/base/browser/dom'; -import {IOverviewRulerLayoutInfo, IScrollableElement, IScrollableElementCreationOptions} from 'vs/base/browser/ui/scrollbar/scrollableElement'; +import {IOverviewRulerLayoutInfo, IScrollableElement, ScrollableElementCreationOptions} from 'vs/base/browser/ui/scrollbar/scrollableElement'; import {ScrollableElement} from 'vs/base/browser/ui/scrollbar/scrollableElementImpl'; import {EventType, IConfiguration, IConfigurationChangedEvent, IScrollEvent, IViewEventBus} from 'vs/editor/common/editorCommon'; import {EditorScrollable} from 'vs/editor/common/viewLayout/editorScrollable'; @@ -41,7 +41,7 @@ export class ScrollManager implements IDisposable { var configScrollbarOpts = this.configuration.editor.scrollbar; - var scrollbarOptions:IScrollableElementCreationOptions = { + var scrollbarOptions:ScrollableElementCreationOptions = { listenOnDomNode: viewDomNode, vertical: configScrollbarOpts.vertical, horizontal: configScrollbarOpts.horizontal,