scrollable.ts 7.9 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
'use strict';

J
Johannes Rieken 已提交
7 8
import { Disposable } from 'vs/base/common/lifecycle';
import Event, { Emitter } from 'vs/base/common/event';
9

10 11 12 13 14 15
export enum ScrollbarVisibility {
	Auto = 1,
	Hidden = 2,
	Visible = 3
}

16 17
export interface ScrollEvent {
	width: number;
18
	scrollWidth: number;
19 20 21
	scrollLeft: number;

	height: number;
22
	scrollHeight: number;
23
	scrollTop: number;
24

25
	widthChanged: boolean;
26
	scrollWidthChanged: boolean;
27
	scrollLeftChanged: boolean;
28

29 30 31 32
	heightChanged: boolean;
	scrollHeightChanged: boolean;
	scrollTopChanged: boolean;
}
33

A
Alex Dima 已提交
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
export class ScrollState {
	_scrollStateBrand: void;

	public readonly width: number;
	public readonly scrollWidth: number;
	public readonly scrollLeft: number;
	public readonly height: number;
	public readonly scrollHeight: number;
	public readonly scrollTop: number;

	constructor(
		width: number,
		scrollWidth: number,
		scrollLeft: number,
		height: number,
		scrollHeight: number,
		scrollTop: number
	) {
		width = width | 0;
		scrollWidth = scrollWidth | 0;
		scrollLeft = scrollLeft | 0;
		height = height | 0;
		scrollHeight = scrollHeight | 0;
		scrollTop = scrollTop | 0;
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77

		if (width < 0) {
			width = 0;
		}
		if (scrollLeft + width > scrollWidth) {
			scrollLeft = scrollWidth - width;
		}
		if (scrollLeft < 0) {
			scrollLeft = 0;
		}

		if (height < 0) {
			height = 0;
		}
		if (scrollTop + height > scrollHeight) {
			scrollTop = scrollHeight - height;
		}
		if (scrollTop < 0) {
			scrollTop = 0;
		}
78

A
Alex Dima 已提交
79 80 81 82 83 84 85
		this.width = width;
		this.scrollWidth = scrollWidth;
		this.scrollLeft = scrollLeft;
		this.height = height;
		this.scrollHeight = scrollHeight;
		this.scrollTop = scrollTop;
	}
86

A
Alex Dima 已提交
87 88 89 90 91 92 93 94 95 96
	public equals(other: ScrollState): boolean {
		return (
			this.width === other.width
			&& this.scrollWidth === other.scrollWidth
			&& this.scrollLeft === other.scrollLeft
			&& this.height === other.height
			&& this.scrollHeight === other.scrollHeight
			&& this.scrollTop === other.scrollTop
		);
	}
97

98 99 100 101 102 103 104 105 106 107 108
	public createUpdated(update: INewScrollState): 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),
			(typeof update.height !== 'undefined' ? update.height : this.height),
			(typeof update.scrollHeight !== 'undefined' ? update.scrollHeight : this.scrollHeight),
			(typeof update.scrollTop !== 'undefined' ? update.scrollTop : this.scrollTop)
		);
	}

A
Alex Dima 已提交
109 110 111 112
	public createScrollEvent(previous: ScrollState): ScrollEvent {
		let widthChanged = (this.width !== previous.width);
		let scrollWidthChanged = (this.scrollWidth !== previous.scrollWidth);
		let scrollLeftChanged = (this.scrollLeft !== previous.scrollLeft);
113

A
Alex Dima 已提交
114 115 116
		let heightChanged = (this.height !== previous.height);
		let scrollHeightChanged = (this.scrollHeight !== previous.scrollHeight);
		let scrollTopChanged = (this.scrollTop !== previous.scrollTop);
117

A
Alex Dima 已提交
118 119 120 121
		return {
			width: this.width,
			scrollWidth: this.scrollWidth,
			scrollLeft: this.scrollLeft,
122

A
Alex Dima 已提交
123 124 125
			height: this.height,
			scrollHeight: this.scrollHeight,
			scrollTop: this.scrollTop,
126 127 128 129

			widthChanged: widthChanged,
			scrollWidthChanged: scrollWidthChanged,
			scrollLeftChanged: scrollLeftChanged,
130

131 132 133
			heightChanged: heightChanged,
			scrollHeightChanged: scrollHeightChanged,
			scrollTopChanged: scrollTopChanged,
A
Alex Dima 已提交
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
		};
	}

}

export interface INewScrollState {
	width?: number;
	scrollWidth?: number;
	scrollLeft?: number;

	height?: number;
	scrollHeight?: number;
	scrollTop?: number;
}

export class Scrollable extends Disposable {

	_scrollableBrand: void;

	private _state: ScrollState;
154 155
	private _smoothScrolling: boolean;
	private _smoothScrollAnimationParams: ISmoothScrollAnimationParams;
A
Alex Dima 已提交
156 157 158 159 160 161 162 163

	private _onScroll = this._register(new Emitter<ScrollEvent>());
	public onScroll: Event<ScrollEvent> = this._onScroll.event;

	constructor() {
		super();

		this._state = new ScrollState(0, 0, 0, 0, 0, 0);
164 165
		this._smoothScrolling = false;
		this._smoothScrollAnimationParams = null;
A
Alex Dima 已提交
166 167 168 169 170 171
	}

	public getState(): ScrollState {
		return this._state;
	}

A
Alex Dima 已提交
172 173 174 175 176 177 178 179 180 181 182 183 184 185
	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 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;
	}

186 187 188 189 190 191 192 193
	/**
	 * 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.
	 */
	public getSmoothScrollTargetState(): ScrollState {
		return this._smoothScrolling ? this._smoothScrollAnimationParams.newState : this._state;
	}

194
	public updateState(update: INewScrollState, smoothScrollDuration: number): void {
195 196

		// If smooth scroll duration is not specified, then assume that the invoker intends to do an immediate update.
197
		if (smoothScrollDuration === 0) {
198
			const newState = this._state.createUpdated(update);
A
Alex Dima 已提交
199

200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237
			// If smooth scrolling is in progress, terminate it.
			if (this._smoothScrolling) {
				this._smoothScrolling = false;
				this._smoothScrollAnimationParams = null;
			}

			// Update state immediately if it is different from the previous one.
			if (!this._state.equals(newState)) {
				this._updateState(newState);
			}
		}
		// 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(); });
				}
			}
		}
	}

	private _performSmoothScroll(): void {
		if (!this._smoothScrolling) {
			// Smooth scrolling has been terminated.
A
Alex Dima 已提交
238 239 240
			return;
		}

241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264
		const completion = (Date.now() - this._smoothScrollAnimationParams.startTime) / this._smoothScrollAnimationParams.duration;
		const newState = this._smoothScrollAnimationParams.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);
		}
	}

	private _updateState(newState: ScrollState): void {
		const oldState = this._state;
A
Alex Dima 已提交
265 266
		this._state = newState;
		this._onScroll.fire(this._state.createScrollEvent(oldState));
267
	}
E
Erich Gamma 已提交
268
}
269 270 271 272 273 274 275

interface ISmoothScrollAnimationParams {
	oldState: ScrollState;
	newState: ScrollState;
	startTime: number;
	duration: number;
}